@mfittko/repo-wiki 0.2.1

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 (190) hide show
  1. package/.llmwiki/schema.md +107 -0
  2. package/AGENTS.md +42 -0
  3. package/CHANGELOG.md +91 -0
  4. package/LICENSE +21 -0
  5. package/README.md +254 -0
  6. package/dist/bin/repo-wiki.d.ts +2 -0
  7. package/dist/bin/repo-wiki.js +7 -0
  8. package/dist/bin/repo-wiki.js.map +1 -0
  9. package/dist/src/cli.d.ts +1 -0
  10. package/dist/src/cli.js +404 -0
  11. package/dist/src/cli.js.map +1 -0
  12. package/dist/src/compiler.d.ts +55 -0
  13. package/dist/src/compiler.js +2046 -0
  14. package/dist/src/compiler.js.map +1 -0
  15. package/dist/src/config.d.ts +63 -0
  16. package/dist/src/config.js +86 -0
  17. package/dist/src/config.js.map +1 -0
  18. package/dist/src/context-assembler.d.ts +68 -0
  19. package/dist/src/context-assembler.js +378 -0
  20. package/dist/src/context-assembler.js.map +1 -0
  21. package/dist/src/data-model-signals.d.ts +1 -0
  22. package/dist/src/data-model-signals.js +13 -0
  23. package/dist/src/data-model-signals.js.map +1 -0
  24. package/dist/src/docs-ingestor.d.ts +138 -0
  25. package/dist/src/docs-ingestor.js +844 -0
  26. package/dist/src/docs-ingestor.js.map +1 -0
  27. package/dist/src/docs-linter.d.ts +14 -0
  28. package/dist/src/docs-linter.js +164 -0
  29. package/dist/src/docs-linter.js.map +1 -0
  30. package/dist/src/docs-validation.d.ts +36 -0
  31. package/dist/src/docs-validation.js +297 -0
  32. package/dist/src/docs-validation.js.map +1 -0
  33. package/dist/src/extractors.d.ts +50 -0
  34. package/dist/src/extractors.js +2275 -0
  35. package/dist/src/extractors.js.map +1 -0
  36. package/dist/src/frontmatter.d.ts +46 -0
  37. package/dist/src/frontmatter.js +377 -0
  38. package/dist/src/frontmatter.js.map +1 -0
  39. package/dist/src/index.d.ts +26 -0
  40. package/dist/src/index.js +18 -0
  41. package/dist/src/index.js.map +1 -0
  42. package/dist/src/init.d.ts +12 -0
  43. package/dist/src/init.js +121 -0
  44. package/dist/src/init.js.map +1 -0
  45. package/dist/src/language.d.ts +2 -0
  46. package/dist/src/language.js +62 -0
  47. package/dist/src/language.js.map +1 -0
  48. package/dist/src/linter.d.ts +33 -0
  49. package/dist/src/linter.js +398 -0
  50. package/dist/src/linter.js.map +1 -0
  51. package/dist/src/llm-provider.d.ts +267 -0
  52. package/dist/src/llm-provider.js +474 -0
  53. package/dist/src/llm-provider.js.map +1 -0
  54. package/dist/src/page-ownership.d.ts +38 -0
  55. package/dist/src/page-ownership.js +96 -0
  56. package/dist/src/page-ownership.js.map +1 -0
  57. package/dist/src/planner.d.ts +55 -0
  58. package/dist/src/planner.js +422 -0
  59. package/dist/src/planner.js.map +1 -0
  60. package/dist/src/prompts.d.ts +103 -0
  61. package/dist/src/prompts.js +344 -0
  62. package/dist/src/prompts.js.map +1 -0
  63. package/dist/src/publisher.d.ts +68 -0
  64. package/dist/src/publisher.js +662 -0
  65. package/dist/src/publisher.js.map +1 -0
  66. package/dist/src/repository-analysis.d.ts +88 -0
  67. package/dist/src/repository-analysis.js +485 -0
  68. package/dist/src/repository-analysis.js.map +1 -0
  69. package/dist/src/scanner.d.ts +122 -0
  70. package/dist/src/scanner.js +309 -0
  71. package/dist/src/scanner.js.map +1 -0
  72. package/dist/src/search.d.ts +71 -0
  73. package/dist/src/search.js +410 -0
  74. package/dist/src/search.js.map +1 -0
  75. package/dist/src/secret-patterns.d.ts +3 -0
  76. package/dist/src/secret-patterns.js +14 -0
  77. package/dist/src/secret-patterns.js.map +1 -0
  78. package/dist/src/utils/args.d.ts +2 -0
  79. package/dist/src/utils/args.js +19 -0
  80. package/dist/src/utils/args.js.map +1 -0
  81. package/dist/src/utils/dotenv.d.ts +7 -0
  82. package/dist/src/utils/dotenv.js +73 -0
  83. package/dist/src/utils/dotenv.js.map +1 -0
  84. package/dist/src/utils/fs.d.ts +22 -0
  85. package/dist/src/utils/fs.js +83 -0
  86. package/dist/src/utils/fs.js.map +1 -0
  87. package/dist/src/utils/git.d.ts +13 -0
  88. package/dist/src/utils/git.js +39 -0
  89. package/dist/src/utils/git.js.map +1 -0
  90. package/dist/src/wiki-graph.d.ts +74 -0
  91. package/dist/src/wiki-graph.js +335 -0
  92. package/dist/src/wiki-graph.js.map +1 -0
  93. package/dist/src/wiki-patch.d.ts +152 -0
  94. package/dist/src/wiki-patch.js +489 -0
  95. package/dist/src/wiki-patch.js.map +1 -0
  96. package/dist/src/wiki-query.d.ts +63 -0
  97. package/dist/src/wiki-query.js +255 -0
  98. package/dist/src/wiki-query.js.map +1 -0
  99. package/dist/test/cli.test.d.ts +1 -0
  100. package/dist/test/cli.test.js +514 -0
  101. package/dist/test/cli.test.js.map +1 -0
  102. package/dist/test/compiler-eval.test.d.ts +1 -0
  103. package/dist/test/compiler-eval.test.js +234 -0
  104. package/dist/test/compiler-eval.test.js.map +1 -0
  105. package/dist/test/compiler.test.d.ts +1 -0
  106. package/dist/test/compiler.test.js +2537 -0
  107. package/dist/test/compiler.test.js.map +1 -0
  108. package/dist/test/context-assembler.test.d.ts +1 -0
  109. package/dist/test/context-assembler.test.js +379 -0
  110. package/dist/test/context-assembler.test.js.map +1 -0
  111. package/dist/test/docs-linter.test.d.ts +1 -0
  112. package/dist/test/docs-linter.test.js +900 -0
  113. package/dist/test/docs-linter.test.js.map +1 -0
  114. package/dist/test/dotenv.test.d.ts +1 -0
  115. package/dist/test/dotenv.test.js +77 -0
  116. package/dist/test/dotenv.test.js.map +1 -0
  117. package/dist/test/extractors-go.test.d.ts +1 -0
  118. package/dist/test/extractors-go.test.js +393 -0
  119. package/dist/test/extractors-go.test.js.map +1 -0
  120. package/dist/test/extractors-rust.test.d.ts +1 -0
  121. package/dist/test/extractors-rust.test.js +219 -0
  122. package/dist/test/extractors-rust.test.js.map +1 -0
  123. package/dist/test/extractors-utils.test.d.ts +1 -0
  124. package/dist/test/extractors-utils.test.js +786 -0
  125. package/dist/test/extractors-utils.test.js.map +1 -0
  126. package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.d.ts +1 -0
  127. package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.js +4 -0
  128. package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.js.map +1 -0
  129. package/dist/test/frontmatter.test.d.ts +1 -0
  130. package/dist/test/frontmatter.test.js +287 -0
  131. package/dist/test/frontmatter.test.js.map +1 -0
  132. package/dist/test/init-planner.test.d.ts +1 -0
  133. package/dist/test/init-planner.test.js +688 -0
  134. package/dist/test/init-planner.test.js.map +1 -0
  135. package/dist/test/linter.test.d.ts +1 -0
  136. package/dist/test/linter.test.js +426 -0
  137. package/dist/test/linter.test.js.map +1 -0
  138. package/dist/test/llm-provider.test.d.ts +1 -0
  139. package/dist/test/llm-provider.test.js +783 -0
  140. package/dist/test/llm-provider.test.js.map +1 -0
  141. package/dist/test/page-ownership.test.d.ts +1 -0
  142. package/dist/test/page-ownership.test.js +247 -0
  143. package/dist/test/page-ownership.test.js.map +1 -0
  144. package/dist/test/publisher.test.d.ts +1 -0
  145. package/dist/test/publisher.test.js +1297 -0
  146. package/dist/test/publisher.test.js.map +1 -0
  147. package/dist/test/repository-analysis.test.d.ts +1 -0
  148. package/dist/test/repository-analysis.test.js +182 -0
  149. package/dist/test/repository-analysis.test.js.map +1 -0
  150. package/dist/test/run-compiled-tests.d.ts +1 -0
  151. package/dist/test/run-compiled-tests.js +48 -0
  152. package/dist/test/run-compiled-tests.js.map +1 -0
  153. package/dist/test/scanner.test.d.ts +1 -0
  154. package/dist/test/scanner.test.js +551 -0
  155. package/dist/test/scanner.test.js.map +1 -0
  156. package/dist/test/search.test.d.ts +1 -0
  157. package/dist/test/search.test.js +92 -0
  158. package/dist/test/search.test.js.map +1 -0
  159. package/dist/test/update-changelog.test.d.ts +1 -0
  160. package/dist/test/update-changelog.test.js +125 -0
  161. package/dist/test/update-changelog.test.js.map +1 -0
  162. package/dist/test/wiki-graph.test.d.ts +1 -0
  163. package/dist/test/wiki-graph.test.js +164 -0
  164. package/dist/test/wiki-graph.test.js.map +1 -0
  165. package/dist/test/wiki-patch.test.d.ts +1 -0
  166. package/dist/test/wiki-patch.test.js +610 -0
  167. package/dist/test/wiki-patch.test.js.map +1 -0
  168. package/dist/test/wiki-query.test.d.ts +1 -0
  169. package/dist/test/wiki-query.test.js +163 -0
  170. package/dist/test/wiki-query.test.js.map +1 -0
  171. package/docs/PLAN.md +993 -0
  172. package/docs/WHY.md +61 -0
  173. package/docs/plans/agent-integration.md +85 -0
  174. package/docs/plans/ci-publishing.md +111 -0
  175. package/docs/plans/doc-validation.md +92 -0
  176. package/docs/plans/github-action.md +113 -0
  177. package/docs/plans/incremental-mode.md +98 -0
  178. package/docs/plans/karpathy-llm-wiki-alignment.md +84 -0
  179. package/docs/plans/llm-compiler.md +160 -0
  180. package/docs/plans/production-scanner.md +104 -0
  181. package/docs/plans/query-and-file-back.md +103 -0
  182. package/docs/plans/search-index.md +118 -0
  183. package/docs/plans/trust-hardening.md +74 -0
  184. package/docs/plans/wiki-graph.md +183 -0
  185. package/docs/plans/wiki-health.md +76 -0
  186. package/package.json +83 -0
  187. package/prompts/compiler.md +16 -0
  188. package/prompts/lint.md +18 -0
  189. package/prompts/page-templates.md +25 -0
  190. package/skills/repo-wiki-cli/SKILL.md +139 -0
@@ -0,0 +1,786 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, writeFile, mkdir, rm } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { detectRuntimeHints, extractEnvironmentVariables, extractExportedSymbols, extractImports, extractMigrationSurfaces, extractModelSurfaces, extractRouteSurfaces, extractSymbols } from '../src/extractors.js';
7
+ import { classifyPath, detectLanguage } from '../src/language.js';
8
+ import { getGitCommit, getGitRemote, getGitStatus, runGit } from '../src/utils/git.js';
9
+ import { lintDocs } from '../src/docs-linter.js';
10
+ const richSource = `
11
+ import lib from 'lib';
12
+ export { helper } from './helper.js';
13
+ const dep = require('../legacy.cjs');
14
+ export default function () {}
15
+ export default class {}
16
+ export async function runTask() {}
17
+ export class Service {}
18
+ export const value = 1;
19
+ export let count = 0;
20
+ export var flag = true;
21
+ export type Name = string;
22
+ export interface Shape { value: string }
23
+ export enum State { Ready }
24
+ export { value as renamed, type Name as Alias };
25
+ const { API_KEY, PORT: localPort = '3000', ...rest } = process.env;
26
+ const { VITE_HOST } = import.meta.env;
27
+ const baseUrl = optionalEnv(env, 'LLMWIKI_LLM_BASE_URL');
28
+ const router = Router();
29
+ router.get('/health', healthCheck);
30
+ const api = fastify();
31
+ api.route({ method: ['post', 'get'], url: '/items', handler: handleItems });
32
+ export async function GET() {}
33
+ `;
34
+ test('extractors cover imports, exports, env vars, routes, and runtime hints', () => {
35
+ assert.deepEqual(extractImports('print(1)', 'Python'), []);
36
+ assert.deepEqual(extractSymbols('print(1)', 'Python'), []);
37
+ assert.deepEqual(extractExportedSymbols('print(1)', 'Python'), []);
38
+ assert.deepEqual(extractEnvironmentVariables('print(1)', 'Python'), []);
39
+ assert.deepEqual(extractRouteSurfaces('routes.py', 'print(1)', 'Python'), []);
40
+ assert.deepEqual(extractImports(richSource, 'TypeScript'), ['../legacy.cjs', './helper.js', 'lib']);
41
+ assert.deepEqual(extractSymbols(richSource, 'TypeScript'), ['GET', 'Name', 'Service', 'Shape', 'State', 'api', 'baseUrl', 'count', 'default', 'dep', 'flag', 'router', 'runTask', 'value']);
42
+ const exported = extractExportedSymbols(richSource, 'TypeScript');
43
+ assert.ok(exported.some((entry) => entry.name === 'default' && entry.kind === 'function'));
44
+ assert.ok(exported.some((entry) => entry.name === 'default' && entry.kind === 'class'));
45
+ assert.ok(exported.some((entry) => entry.name === 'helper' && entry.kind === 'named-export'));
46
+ assert.ok(exported.some((entry) => entry.name === 'Alias' && entry.kind === 'named-export'));
47
+ assert.ok(exported.some((entry) => entry.name === 'GET' && entry.kind === 'function'));
48
+ assert.ok(exported.some((entry) => entry.name === 'Service' && entry.kind === 'class'));
49
+ assert.ok(exported.some((entry) => entry.name === 'Name' && entry.kind === 'type'));
50
+ assert.ok(exported.some((entry) => entry.name === 'Shape' && entry.kind === 'interface'));
51
+ assert.ok(exported.some((entry) => entry.name === 'State' && entry.kind === 'enum'));
52
+ assert.deepEqual(extractEnvironmentVariables(richSource, 'TypeScript'), ['API_KEY', 'LLMWIKI_LLM_BASE_URL', 'PORT', 'VITE_HOST']);
53
+ assert.deepEqual(extractMigrationSurfaces('src/query.sql', 'SQL'), []);
54
+ assert.deepEqual(extractMigrationSurfaces('db/migrations/V2__add_orders_table.sql', 'SQL'), [
55
+ { kind: 'sql-migration', id: '2', name: 'add orders table' }
56
+ ]);
57
+ assert.deepEqual(extractMigrationSurfaces('prisma/migrations/20250507120000_create_users/migration.sql', 'SQL'), [
58
+ { kind: 'prisma-migration', id: '20250507120000', name: 'create users' }
59
+ ]);
60
+ assert.deepEqual(extractModelSurfaces('prisma/schema.prisma', `
61
+ datasource db { provider = "postgresql" url = env("DATABASE_URL") }
62
+ model User { id Int @id }
63
+ model AuditLog { id Int @id }
64
+ `, 'Text'), [
65
+ { name: 'AuditLog', kind: 'model', framework: 'prisma' },
66
+ { name: 'User', kind: 'model', framework: 'prisma' }
67
+ ]);
68
+ const ormModels = extractModelSurfaces('src/models/account.ts', `
69
+ @Entity()
70
+ export class AccountEntity {}
71
+ @Entity('users')
72
+ @Index(['email'])
73
+ export class UserEntity {}
74
+ class Session extends Model {}
75
+ const User = sequelize.define('User', {});
76
+ const Profile = mongoose.model('Profile', profileSchema);
77
+ `, 'TypeScript');
78
+ assert.ok(ormModels.some((entry) => entry.framework === 'typeorm' && entry.kind === 'entity' && entry.name === 'AccountEntity'));
79
+ assert.ok(ormModels.some((entry) => entry.framework === 'typeorm' && entry.kind === 'entity' && entry.name === 'UserEntity'));
80
+ assert.ok(ormModels.some((entry) => entry.framework === 'sequelize' && entry.kind === 'model' && entry.name === 'Session'));
81
+ assert.ok(ormModels.some((entry) => entry.framework === 'sequelize' && entry.kind === 'model' && entry.name === 'User'));
82
+ assert.ok(ormModels.some((entry) => entry.framework === 'mongoose' && entry.kind === 'model' && entry.name === 'Profile'));
83
+ assert.deepEqual(extractRouteSurfaces('src/server.ts', richSource, 'TypeScript'), [
84
+ {
85
+ kind: 'http-route',
86
+ framework: 'express',
87
+ target: 'router',
88
+ methods: ['GET'],
89
+ path: '/health',
90
+ handler: 'healthCheck'
91
+ },
92
+ {
93
+ kind: 'http-route',
94
+ framework: 'fastify',
95
+ target: 'api',
96
+ methods: ['GET', 'POST'],
97
+ path: '/items',
98
+ handler: 'handleItems'
99
+ }
100
+ ]);
101
+ assert.deepEqual(extractRouteSurfaces('src/app/api/users/route.ts', 'export async function GET() {}\nexport const POST = () => {};', 'TypeScript'), [
102
+ {
103
+ kind: 'http-route',
104
+ framework: 'route-handler',
105
+ target: 'module',
106
+ methods: ['GET'],
107
+ path: '/api/users',
108
+ handler: 'GET'
109
+ },
110
+ {
111
+ kind: 'http-route',
112
+ framework: 'route-handler',
113
+ target: 'module',
114
+ methods: ['POST'],
115
+ path: '/api/users',
116
+ handler: 'POST'
117
+ }
118
+ ]);
119
+ assert.deepEqual(extractRouteSurfaces('src/more-routes.ts', `
120
+ fastify.get('/ready', readyHandler);
121
+ router.post('/jobs', createJob);
122
+ `, 'TypeScript'), [
123
+ {
124
+ kind: 'http-route',
125
+ framework: 'router',
126
+ target: 'router',
127
+ methods: ['POST'],
128
+ path: '/jobs',
129
+ handler: 'createJob'
130
+ },
131
+ {
132
+ kind: 'http-route',
133
+ framework: 'fastify',
134
+ target: 'fastify',
135
+ methods: ['GET'],
136
+ path: '/ready',
137
+ handler: 'readyHandler'
138
+ }
139
+ ]);
140
+ assert.deepEqual(detectRuntimeHints('infra/Dockerfile', richSource + '\ncron.schedule()', {
141
+ routeSurfaces: extractRouteSurfaces('src/server.ts', richSource, 'TypeScript'),
142
+ environmentVariables: extractEnvironmentVariables(richSource, 'TypeScript')
143
+ }), ['background-work', 'deployment', 'environment-variable', 'http-route']);
144
+ assert.deepEqual(detectRuntimeHints('prisma/migrations/20250507120000_create_users/migration.sql', '-- migration', {
145
+ migrationSurfaces: extractMigrationSurfaces('prisma/migrations/20250507120000_create_users/migration.sql', 'SQL')
146
+ }), ['data-model', 'database-migration']);
147
+ assert.deepEqual(detectRuntimeHints('src/models/account.ts', "import { Model } from 'sequelize';\nclass Session extends Model {}", {}), ['data-model', 'orm-model']);
148
+ assert.deepEqual(detectRuntimeHints('migrations/001_create_users.py', '', {}), []);
149
+ });
150
+ test('AST symbol extraction covers default exports, type-only declarations, and invalid source recovery', () => {
151
+ const jsSource = `
152
+ const localValue = 1;
153
+ function helper() { return localValue; }
154
+ class Service {}
155
+ export default helper;
156
+ export const answer = 42;
157
+ `;
158
+ assert.deepEqual(extractSymbols(jsSource, 'JavaScript'), ['Service', 'answer', 'default', 'helper', 'localValue']);
159
+ assert.deepEqual(extractExportedSymbols(jsSource, 'JavaScript'), [
160
+ { name: 'answer', kind: 'const' },
161
+ { name: 'default', kind: 'function' }
162
+ ]);
163
+ const forwardDefaultSource = `
164
+ export default helper;
165
+ function helper() { return 1; }
166
+ `;
167
+ assert.deepEqual(extractExportedSymbols(forwardDefaultSource, 'JavaScript'), [
168
+ { name: 'default', kind: 'function' }
169
+ ]);
170
+ const namedDefaultFunctionSource = `
171
+ export default function createService() {}
172
+ `;
173
+ assert.deepEqual(extractSymbols(namedDefaultFunctionSource, 'JavaScript'), ['createService']);
174
+ assert.deepEqual(extractExportedSymbols(namedDefaultFunctionSource, 'JavaScript'), [
175
+ { name: 'default', kind: 'function' }
176
+ ]);
177
+ const namedDefaultClassSource = `
178
+ export default class Service {}
179
+ `;
180
+ assert.deepEqual(extractSymbols(namedDefaultClassSource, 'JavaScript'), ['Service']);
181
+ assert.deepEqual(extractExportedSymbols(namedDefaultClassSource, 'JavaScript'), [
182
+ { name: 'default', kind: 'class' }
183
+ ]);
184
+ const tsSource = `
185
+ type InternalType = { id: string };
186
+ interface InternalShape { value: number }
187
+ export type ApiType = InternalType;
188
+ export interface ApiShape extends InternalShape {}
189
+ `;
190
+ assert.deepEqual(extractSymbols(tsSource, 'TypeScript'), ['ApiShape', 'ApiType', 'InternalShape', 'InternalType']);
191
+ assert.deepEqual(extractExportedSymbols(tsSource, 'TypeScript'), [
192
+ { name: 'ApiShape', kind: 'interface' },
193
+ { name: 'ApiType', kind: 'type' }
194
+ ]);
195
+ const invalidSource = `
196
+ export function workingOne() {}
197
+ export const value = 1
198
+ export default (
199
+ `;
200
+ assert.deepEqual(extractSymbols(invalidSource, 'TypeScript'), ['default', 'value', 'workingOne']);
201
+ assert.deepEqual(extractExportedSymbols(invalidSource, 'TypeScript'), [
202
+ { name: 'default', kind: 'default' },
203
+ { name: 'value', kind: 'const' },
204
+ { name: 'workingOne', kind: 'function' }
205
+ ]);
206
+ });
207
+ test('language detection and classification cover the major path cases', () => {
208
+ assert.equal(detectLanguage('Dockerfile'), 'Dockerfile');
209
+ assert.equal(detectLanguage('src/component.tsx'), 'TypeScript React');
210
+ assert.equal(detectLanguage('src/module.py'), 'Python');
211
+ assert.equal(detectLanguage('src/lib.rs'), 'Rust');
212
+ assert.equal(detectLanguage('src/module.rb'), 'Ruby');
213
+ assert.equal(detectLanguage('Gemfile'), 'Ruby');
214
+ assert.equal(detectLanguage('apps\\worker\\Gemfile'), 'Ruby');
215
+ assert.equal(detectLanguage('Rakefile'), 'Ruby');
216
+ assert.equal(detectLanguage('app/config.ru'), 'Ruby');
217
+ assert.equal(detectLanguage('repo-wiki.gemspec'), 'Ruby');
218
+ assert.equal(detectLanguage('README'), 'Text');
219
+ assert.equal(classifyPath('tests/foo.spec.ts'), 'test');
220
+ assert.equal(classifyPath('spec/models/user_spec.rb'), 'test');
221
+ assert.equal(classifyPath('app\\spec\\models\\user_spec.rb'), 'test');
222
+ assert.equal(classifyPath('src/models/user_test.rb'), 'test');
223
+ assert.equal(classifyPath('.github/workflows/ci.yml'), 'ci');
224
+ assert.equal(classifyPath('docs/guide.md'), 'docs');
225
+ assert.equal(classifyPath('db/migrations/001.sql'), 'data');
226
+ assert.equal(classifyPath('prisma/schema.prisma'), 'data');
227
+ assert.equal(classifyPath('ops/infra/main.tf'), 'infra');
228
+ assert.equal(classifyPath('package-lock.json'), 'package');
229
+ assert.equal(classifyPath('src/index.ts'), 'source');
230
+ });
231
+ test('ruby extraction captures requires, modules, classes, methods, singleton methods, constants, and malformed fallback', () => {
232
+ const rubySource = `
233
+ # require 'ignored'
234
+ require "json"
235
+ require 'openssl' if config.ssl?
236
+ require_relative 'lib/service'
237
+ require_relative("./support/helpers") unless production?
238
+
239
+ module RepoWiki
240
+ VERSION = "1.0.0"
241
+ TimeoutError = Class.new(StandardError)
242
+ BANNER = <<~TEXT
243
+ require "hidden"
244
+ module Hidden
245
+ end
246
+ TEXT
247
+
248
+ class << self
249
+ def configure
250
+ true
251
+ end
252
+ end
253
+
254
+ class Scanner
255
+ DEFAULT_LIMIT ||= 50
256
+
257
+ def run
258
+ status = :end
259
+ quoted_status = :"end"
260
+ message = "end"
261
+ literal = 'end'
262
+ items << value # end
263
+ if ready then :ok end
264
+ if true
265
+ [1].each do |value|
266
+ case value
267
+ when 1
268
+ true
269
+ end
270
+ end
271
+ end
272
+ true
273
+ end
274
+
275
+ def self.build
276
+ new
277
+ end
278
+
279
+ class << self
280
+ def from_config
281
+ build
282
+ end
283
+ end
284
+ end
285
+ end
286
+
287
+ execute <<~SQL
288
+ require "ignored_sql"
289
+ module IgnoredSql
290
+ end
291
+ SQL
292
+
293
+ logger.debug(<<~TEXT)
294
+ def fake_method
295
+ end
296
+ TEXT
297
+
298
+ query = <<~ 'RUBYSQL'
299
+ class IgnoredQuotedHeredoc
300
+ end
301
+ RUBYSQL
302
+
303
+ command = <<~ \`RUBYCMD\`
304
+ class IgnoredBacktickHeredoc
305
+ end
306
+ RUBYCMD
307
+
308
+ =begin
309
+ require "ignored_block"
310
+ class IgnoredBlock
311
+ end
312
+ =end
313
+
314
+ class Worker
315
+ def perform!
316
+ while ready do
317
+ tick
318
+ end
319
+ true
320
+ end
321
+ end
322
+
323
+ class AfterAppend
324
+ end
325
+
326
+ class Other
327
+ end
328
+
329
+ def top_level_method?
330
+ true
331
+ end
332
+ `;
333
+ assert.deepEqual(extractImports(rubySource, 'Ruby'), ['./lib/service', './support/helpers', 'json', 'openssl']);
334
+ assert.deepEqual(extractSymbols(rubySource, 'Ruby'), [
335
+ 'AfterAppend',
336
+ 'Other',
337
+ 'RepoWiki',
338
+ 'RepoWiki.configure',
339
+ 'RepoWiki::BANNER',
340
+ 'RepoWiki::Scanner',
341
+ 'RepoWiki::Scanner#run',
342
+ 'RepoWiki::Scanner.build',
343
+ 'RepoWiki::Scanner.from_config',
344
+ 'RepoWiki::Scanner::DEFAULT_LIMIT',
345
+ 'RepoWiki::TimeoutError',
346
+ 'RepoWiki::VERSION',
347
+ 'Worker',
348
+ 'Worker#perform!',
349
+ 'top_level_method?'
350
+ ]);
351
+ const malformedRuby = `
352
+ module Broken
353
+ class Worker
354
+ def perform
355
+ true
356
+ def self.recover
357
+ true
358
+ BROKEN_CONST = 1
359
+ `;
360
+ assert.doesNotThrow(() => extractSymbols(malformedRuby, 'Ruby'));
361
+ assert.deepEqual(extractSymbols(malformedRuby, 'Ruby'), [
362
+ 'Broken',
363
+ 'Broken::Worker',
364
+ 'Broken::Worker#perform',
365
+ 'Broken::Worker.recover',
366
+ 'Broken::Worker::BROKEN_CONST'
367
+ ]);
368
+ });
369
+ test('python extraction captures imports, classes, functions, async functions, constants, and malformed input fallback', () => {
370
+ const pythonSource = `
371
+ """
372
+ import fake_mod
373
+ from fake_pkg import hidden
374
+ def fake():
375
+ return 0
376
+ """
377
+
378
+ import os
379
+ import pkg.mod as mod, json
380
+ from collections import defaultdict
381
+ from .local.module import value as local_value
382
+
383
+ CONSTANT = "value"
384
+ MAX_RETRIES: int = 3
385
+ SPECIAL_TOKEN: Literal["="] = "="
386
+ TRIPLE_MARKER = "'''"
387
+
388
+ class Service:
389
+ def method(self):
390
+ return 1
391
+
392
+ def helper(name: str):
393
+ return name
394
+
395
+ async def fetch_data():
396
+ return None
397
+
398
+ def multi_line(
399
+ name: str,
400
+ ) -> str:
401
+ return name
402
+
403
+ async def multi_async(
404
+ value: int,
405
+ ):
406
+ return value
407
+ `;
408
+ assert.deepEqual(extractImports(pythonSource, 'Python'), ['.local.module', 'collections', 'json', 'os', 'pkg.mod']);
409
+ assert.deepEqual(extractSymbols(pythonSource, 'Python'), ['CONSTANT', 'MAX_RETRIES', 'SPECIAL_TOKEN', 'Service', 'TRIPLE_MARKER', 'fetch_data', 'helper', 'multi_async', 'multi_line']);
410
+ const malformedSource = `
411
+ import requests
412
+ def still_ok():
413
+ return True
414
+ def broken(
415
+ class Recoverable:
416
+ pass
417
+ RESULT = 1
418
+ `;
419
+ assert.doesNotThrow(() => extractSymbols(malformedSource, 'Python'));
420
+ assert.deepEqual(extractSymbols(malformedSource, 'Python'), ['RESULT', 'Recoverable', 'still_ok']);
421
+ assert.deepEqual(extractImports(malformedSource, 'Python'), ['requests']);
422
+ const quoteEdgeCases = `
423
+ '''example import hidden_single_quote_docstring'''
424
+ VALUE_WITH_HASH = "not a # comment"
425
+ VALUE_WITH_ESCAPED_QUOTE = "keeps \\"# still string"
426
+ TRIPLE_MARKER = "'''"
427
+ DOUBLE_TRIPLE_MARKER = '\"\"\"'
428
+ NESTED = {"key": "="}
429
+ LIST_VALUE = ["="]
430
+ COMPARES = 1 == 1
431
+ NOT_EQUAL = 1 != 2
432
+
433
+ def with_defaults(value: str = "(", other: dict = {"x": ":"}) -> str:
434
+ return value
435
+
436
+ def no_colon(value) -> str
437
+ class AfterMalformed:
438
+ pass
439
+ async def :
440
+ pass
441
+ `;
442
+ assert.deepEqual(extractSymbols(quoteEdgeCases, 'Python'), [
443
+ 'AfterMalformed',
444
+ 'COMPARES',
445
+ 'DOUBLE_TRIPLE_MARKER',
446
+ 'LIST_VALUE',
447
+ 'NESTED',
448
+ 'NOT_EQUAL',
449
+ 'TRIPLE_MARKER',
450
+ 'VALUE_WITH_ESCAPED_QUOTE',
451
+ 'VALUE_WITH_HASH',
452
+ 'with_defaults'
453
+ ]);
454
+ });
455
+ test('git helpers cover success and fallback paths', async () => {
456
+ const repoDir = await mkdtemp(path.join(os.tmpdir(), 'repo-wiki-git-'));
457
+ const nonRepoDir = await mkdtemp(path.join(os.tmpdir(), 'repo-wiki-non-git-'));
458
+ try {
459
+ await writeFile(path.join(repoDir, 'README.md'), '# temp\n', 'utf8');
460
+ await runGit(['init'], { cwd: repoDir });
461
+ await runGit(['config', 'user.name', 'Repo Wiki'], { cwd: repoDir });
462
+ await runGit(['config', 'user.email', 'repo-wiki@example.com'], { cwd: repoDir });
463
+ await runGit(['remote', 'add', 'origin', 'https://example.com/repo.git'], { cwd: repoDir });
464
+ await runGit(['add', 'README.md'], { cwd: repoDir });
465
+ await runGit(['commit', '-m', 'init'], { cwd: repoDir });
466
+ const commit = await getGitCommit(repoDir, 'fallback');
467
+ assert.match(commit, /^[0-9a-f]{40}$/);
468
+ assert.equal(await getGitRemote(repoDir, 'fallback'), 'https://example.com/repo.git');
469
+ assert.equal(await getGitStatus(repoDir), '');
470
+ assert.equal(await getGitCommit(nonRepoDir, 'fallback'), 'fallback');
471
+ assert.equal(await getGitRemote(nonRepoDir, 'fallback'), 'fallback');
472
+ assert.equal(await getGitStatus(nonRepoDir), '');
473
+ }
474
+ finally {
475
+ await rm(repoDir, { recursive: true, force: true });
476
+ await rm(nonRepoDir, { recursive: true, force: true });
477
+ }
478
+ });
479
+ test('lintDocs reports stale, contradicted, unvalidated, and broken-link issues', async () => {
480
+ const repoDir = await mkdtemp(path.join(os.tmpdir(), 'repo-wiki-lint-docs-'));
481
+ const scanDir = path.join(repoDir, '.llmwiki', 'run');
482
+ try {
483
+ await mkdir(path.join(repoDir, 'docs'), { recursive: true });
484
+ await mkdir(scanDir, { recursive: true });
485
+ await writeFile(path.join(repoDir, 'docs', 'existing.md'), '# Existing\n', 'utf8');
486
+ await writeFile(path.join(repoDir, '.llmwiki', 'config.json'), JSON.stringify({
487
+ lint: {
488
+ stale_docs: 'warning',
489
+ contradicted_docs: 'error',
490
+ unvalidated_doc_claims: 'warning'
491
+ }
492
+ }), 'utf8');
493
+ const manifest = {
494
+ documentation: {
495
+ files: [
496
+ {
497
+ path: 'docs/stale.md',
498
+ stale: true,
499
+ age_days: 365,
500
+ validation: { contradictions: [], commands: [], env_vars: [] },
501
+ claims: [],
502
+ status: 'stale',
503
+ links: []
504
+ },
505
+ {
506
+ path: 'docs/contradicted.md',
507
+ stale: false,
508
+ age_days: 1,
509
+ validation: { contradictions: [{ text: 'deprecated' }], commands: [], env_vars: [] },
510
+ claims: [{ text: 'deprecated' }],
511
+ status: 'contradicted',
512
+ links: ['missing.md', 'existing.md', '#local', 'https://example.com', 'mailto:test@example.com']
513
+ },
514
+ {
515
+ path: 'docs/unvalidated.md',
516
+ stale: false,
517
+ age_days: 1,
518
+ validation: { contradictions: [], commands: [], env_vars: [] },
519
+ claims: [{ text: 'run npm test' }],
520
+ status: 'unvalidated',
521
+ links: []
522
+ }
523
+ ]
524
+ }
525
+ };
526
+ await writeFile(path.join(scanDir, 'manifest.json'), JSON.stringify(manifest), 'utf8');
527
+ const result = await lintDocs({ scanDir, repoPath: repoDir });
528
+ assert.equal(result.summary.errors, 1);
529
+ assert.equal(result.summary.warnings, 3);
530
+ assert.deepEqual(result.issues.map((issue) => issue.code).sort(), [
531
+ 'broken-documentation-link',
532
+ 'contradicted-documentation',
533
+ 'stale-documentation',
534
+ 'unvalidated-documentation-claims'
535
+ ]);
536
+ }
537
+ finally {
538
+ await rm(repoDir, { recursive: true, force: true });
539
+ }
540
+ });
541
+ test('extractors cover additional framework, route-shape, and fallback branches', () => {
542
+ const routeContent = `
543
+ const app = express();
544
+ app.use('/middleware');
545
+ app.get('/health', healthCheck);
546
+ app.get('/health', healthCheck);
547
+ const server = createServer();
548
+ server.post('/server-post');
549
+ const fastifyApi = Fastify();
550
+ fastifyApi.get('/fast', fastHandler);
551
+ const honoApp = new Hono();
552
+ honoApp.put('/hono', honoHandler);
553
+ const custom = makeRouter();
554
+ custom.route({ path: '/skip-no-method' });
555
+ custom.route({ method: 'patch', path: '/patch' });
556
+ `;
557
+ assert.deepEqual(extractRouteSurfaces('src/server.ts', routeContent, 'TypeScript'), [
558
+ { kind: 'http-route', framework: 'fastify', target: 'fastifyApi', methods: ['GET'], path: '/fast', handler: 'fastHandler' },
559
+ { kind: 'http-route', framework: 'express', target: 'app', methods: ['GET'], path: '/health', handler: 'healthCheck' },
560
+ { kind: 'http-route', framework: 'hono', target: 'honoApp', methods: ['PUT'], path: '/hono', handler: 'honoHandler' },
561
+ { kind: 'http-route', framework: 'express', target: 'app', methods: ['USE'], path: '/middleware', handler: null },
562
+ { kind: 'http-route', framework: 'unknown', target: 'custom', methods: ['PATCH'], path: '/patch', handler: null },
563
+ { kind: 'http-route', framework: 'http-server', target: 'server', methods: ['POST'], path: '/server-post', handler: null }
564
+ ]);
565
+ assert.deepEqual(extractRouteSurfaces('src/pages/api/status.ts', 'export const GET = async () => {};', 'TypeScript'), [
566
+ { kind: 'http-route', framework: 'route-handler', target: 'module', methods: ['GET'], path: '/api/status', handler: 'GET' }
567
+ ]);
568
+ assert.deepEqual(extractRouteSurfaces('src/routes/orders/index.ts', 'export const POST = async () => {};', 'TypeScript'), [
569
+ { kind: 'http-route', framework: 'route-handler', target: 'module', methods: ['POST'], path: '/routes/orders', handler: 'POST' }
570
+ ]);
571
+ assert.deepEqual(extractRouteSurfaces('src/feature.ts', "const custom = makeRouter(); custom.route({ path: '/missing-method' });", 'TypeScript'), []);
572
+ assert.deepEqual(detectRuntimeHints('src/jobs/worker.ts', 'process.env.API_TOKEN\nqueue.add()', {}), ['background-work', 'environment-variable']);
573
+ assert.deepEqual(detectRuntimeHints('src/plain.ts', 'const value = 1;', {}), []);
574
+ });
575
+ test('extractRouteSurfaces detects NestJS, Koa, tRPC, GraphQL, and OpenAPI patterns', () => {
576
+ const frameworkContent = `
577
+ import Router from '@koa/router';
578
+ import Koa from 'koa';
579
+ import { initTRPC } from '@trpc/server';
580
+ import { graphql } from 'graphql';
581
+ const koaApp = new Koa();
582
+ koaApp.use('/koa-middleware', koaMiddleware);
583
+ const koaRouter = new Router();
584
+ koaRouter.get('/koa-health', koaHealth);
585
+
586
+ @Controller('/users')
587
+ export class UsersController {
588
+ @Get('/profile')
589
+ getProfile() { return true; }
590
+
591
+ @Post()
592
+ createUser() { return true; }
593
+ }
594
+
595
+ const t = initTRPC.create();
596
+ const appRouter = t.router({
597
+ hello: t.procedure.query(() => 'ok'),
598
+ createUser: t.procedure.mutation(() => ({ id: 1 })),
599
+ procedureWithoutQueryOrMutation: t.procedure
600
+ });
601
+
602
+ const resolvers = {
603
+ Query: {
604
+ health: () => 'ok',
605
+ user: {
606
+ type: UserType,
607
+ resolve: () => ({ id: '1' })
608
+ }
609
+ },
610
+ Mutation: {
611
+ createPost: () => ({})
612
+ }
613
+ };
614
+
615
+ registry.registerPath({
616
+ method: 'get',
617
+ path: '/openapi/pets',
618
+ operationId: 'listPets'
619
+ });
620
+ registry.registerPath({ path: '/openapi/missing-method' });
621
+ `;
622
+ assert.deepEqual(extractRouteSurfaces('src/server.ts', frameworkContent, 'TypeScript'), [
623
+ { kind: 'rpc-route', framework: 'trpc', target: 'router', methods: ['MUTATION'], path: '/createUser', handler: 'createUser' },
624
+ { kind: 'graphql-operation', framework: 'graphql', target: 'Mutation', methods: ['MUTATION'], path: '/graphql', handler: 'createPost' },
625
+ { kind: 'graphql-operation', framework: 'graphql', target: 'Query', methods: ['QUERY'], path: '/graphql', handler: 'health' },
626
+ { kind: 'graphql-operation', framework: 'graphql', target: 'Query', methods: ['QUERY'], path: '/graphql', handler: 'user' },
627
+ { kind: 'rpc-route', framework: 'trpc', target: 'router', methods: ['QUERY'], path: '/hello', handler: 'hello' },
628
+ { kind: 'http-route', framework: 'koa', target: 'koaRouter', methods: ['GET'], path: '/koa-health', handler: 'koaHealth' },
629
+ { kind: 'http-route', framework: 'koa', target: 'koaApp', methods: ['USE'], path: '/koa-middleware', handler: 'koaMiddleware' },
630
+ { kind: 'openapi-operation', framework: 'openapi', target: 'registry', methods: ['GET'], path: '/openapi/pets', handler: 'listPets' },
631
+ { kind: 'http-route', framework: 'nestjs', target: 'UsersController', methods: ['POST'], path: '/users', handler: 'createUser' },
632
+ { kind: 'http-route', framework: 'nestjs', target: 'UsersController', methods: ['GET'], path: '/users/profile', handler: 'getProfile' }
633
+ ]);
634
+ assert.deepEqual(extractRouteSurfaces('src/plain.ts', `
635
+ import { graphql } from 'graphql';
636
+ const resolvers = {
637
+ Query: {
638
+ nonFunctionResolver: true
639
+ }
640
+ };
641
+ const router = t.router({
642
+ invalidOnly: t.procedure
643
+ });
644
+ registry.registerPath({ path: '/missing', operationId: 'missingMethod' });
645
+ `, 'TypeScript'), []);
646
+ });
647
+ test('extractRouteSurfaces handles strings, comments, and template literals inside resolver maps', () => {
648
+ // Exercise readBalancedObjectBody and isTopLevelObjectKey branches for
649
+ // single-quoted strings, double-quoted strings, template literals,
650
+ // line comments, and block comments with embedded braces.
651
+ const content = `
652
+ import { graphql } from 'graphql';
653
+ const resolvers = {
654
+ Query: {
655
+ withStrings: () => {
656
+ const a = "hello { world }";
657
+ const b = 'curly { brace }';
658
+ const c = \`template \${ '{' } literal\`;
659
+ // line comment with { brace
660
+ /* block comment with { brace } */
661
+ return { ok: true };
662
+ },
663
+ simple: () => 'ok'
664
+ },
665
+ Mutation: {
666
+ escaped: () => {
667
+ const s = 'it\\'s a \\"test\\"';
668
+ return {};
669
+ }
670
+ }
671
+ };
672
+ `;
673
+ const surfaces = extractRouteSurfaces('src/resolvers.ts', content, 'TypeScript');
674
+ const names = surfaces.map(s => s.handler).sort();
675
+ assert.ok(names.includes('withStrings'), 'withStrings should be detected');
676
+ assert.ok(names.includes('simple'), 'simple should be detected');
677
+ assert.ok(names.includes('escaped'), 'escaped should be detected');
678
+ });
679
+ test('extractRouteSurfaces handles Koa middleware with inline comments', () => {
680
+ const content = `
681
+ import Router from '@koa/router';
682
+ const r = new Router();
683
+ r.post('/items', /* auth middleware */ handleItems);
684
+ r.get('/health', healthCheck); // health route
685
+ `;
686
+ const surfaces = extractRouteSurfaces('src/app.ts', content, 'TypeScript');
687
+ assert.equal(surfaces.length, 2);
688
+ assert.equal(surfaces[0].path, '/health');
689
+ assert.equal(surfaces[1].path, '/items');
690
+ });
691
+ test('extractRouteSurfaces handles tRPC with nested router', () => {
692
+ const content = `
693
+ import { initTRPC } from '@trpc/server';
694
+ const t = initTRPC.create();
695
+ const router = t.router({
696
+ getItems: t.procedure.query(() => []),
697
+ addItem: t.procedure.mutation(() => ({ id: 1 }))
698
+ });
699
+ `;
700
+ const surfaces = extractRouteSurfaces('src/trpc.ts', content, 'TypeScript');
701
+ assert.equal(surfaces.length, 2);
702
+ assert.deepEqual(surfaces.map(s => s.handler).sort(), ['addItem', 'getItems']);
703
+ });
704
+ test('extractRouteSurfaces does not infer tRPC from generic router objects', () => {
705
+ const content = `
706
+ const router = ({
707
+ lookup: db.query('select 1'),
708
+ save: db.mutation('insert')
709
+ });
710
+ `;
711
+ assert.deepEqual(extractRouteSurfaces('src/plain.ts', content, 'TypeScript'), []);
712
+ });
713
+ test('extractRouteSurfaces handles OpenAPI with array method', () => {
714
+ const content = `
715
+ import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
716
+ const reg = new OpenAPIRegistry();
717
+ reg.registerPath({
718
+ method: 'post',
719
+ path: '/api/upload',
720
+ operationId: 'uploadFile'
721
+ });
722
+ `;
723
+ const surfaces = extractRouteSurfaces('src/openapi.ts', content, 'TypeScript');
724
+ assert.equal(surfaces.length, 1);
725
+ assert.equal(surfaces[0].path, '/api/upload');
726
+ assert.equal(surfaces[0].handler, 'uploadFile');
727
+ });
728
+ test('extractRouteSurfaces handles NestJS with multiple decorators', () => {
729
+ const content = `
730
+ @Controller('/api')
731
+ export class ApiController {
732
+ @Get('/list')
733
+ list() { return []; }
734
+
735
+ @Delete('/item')
736
+ remove() { return null; }
737
+
738
+ @Put('/item')
739
+ update() { return {}; }
740
+
741
+ @Patch('/item')
742
+ patch() { return {}; }
743
+ }
744
+ `;
745
+ const surfaces = extractRouteSurfaces('src/api.controller.ts', content, 'TypeScript');
746
+ assert.ok(surfaces.length >= 4, `Expected at least 4 surfaces, got ${surfaces.length}`);
747
+ });
748
+ test('inferFileRoutePath detects Next.js app router and pages patterns', () => {
749
+ // pages/api pattern
750
+ const pages = extractRouteSurfaces('src/pages/api/users.ts', 'export async function GET() {}', 'TypeScript');
751
+ assert.ok(pages.some(s => s.path === '/api/users'), 'pages/api pattern');
752
+ // app/api/route pattern
753
+ const app = extractRouteSurfaces('src/app/api/items/route.ts', 'export async function POST() {}', 'TypeScript');
754
+ assert.ok(app.some(s => s.path === '/api/items'), 'app/api route pattern');
755
+ });
756
+ test('python extractImports ignores invalid specifiers and handles escape sequences in strings', () => {
757
+ // Quoted specifier (Go-style) should be ignored — not a valid Python identifier
758
+ assert.deepEqual(extractImports('import "fmt"', 'Python'), []);
759
+ assert.deepEqual(extractImports('import 123bad', 'Python'), []);
760
+ // Escaped single-quote inside inline string should not start triple-quote block
761
+ const src = `import os\nx = 'it\\'s fine'\nimport sys\n`;
762
+ const result = extractImports(src, 'Python');
763
+ assert.ok(result.includes('os'), 'os imported');
764
+ assert.ok(result.includes('sys'), 'sys imported');
765
+ });
766
+ test('python extractSymbols handles def with no parenthesis match and -> without colon', () => {
767
+ // def with no opening paren: should be skipped
768
+ const src1 = `def no_paren str:\n pass\n`;
769
+ assert.deepEqual(extractSymbols(src1, 'Python'), []);
770
+ // async def with return annotation but no colon: should be skipped
771
+ const src2 = `async def incomplete() -> str\n`;
772
+ assert.deepEqual(extractSymbols(src2, 'Python'), []);
773
+ // multiline collectPythonSignature that hits empty-line break mid-multiline
774
+ const src3 = `def broken(\n\n x: int,\n) -> int:\n return x\n`;
775
+ const syms = extractSymbols(src3, 'Python');
776
+ assert.ok(syms.includes('broken') || syms.length === 0, 'gracefully handles mid-signature empty line');
777
+ });
778
+ test('stripPythonTripleQuotedStrings handles escaped triple-quote and inline quote before triple', () => {
779
+ // Inline single quote before triple-quote block: triple-quote block is still stripped
780
+ const src = `x = 'hello'\n"""\ndocstring content\nimport hidden\n"""\nimport real\n`;
781
+ assert.deepEqual(extractImports(src, 'Python'), ['real']);
782
+ // Double-escaped backslash before triple-quote is not an escape of the quote
783
+ const src2 = `import a\nx = "test"\nimport b\n`;
784
+ assert.deepEqual(extractImports(src2, 'Python'), ['a', 'b']);
785
+ });
786
+ //# sourceMappingURL=extractors-utils.test.js.map