@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.
- package/.llmwiki/schema.md +107 -0
- package/AGENTS.md +42 -0
- package/CHANGELOG.md +91 -0
- package/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/bin/repo-wiki.d.ts +2 -0
- package/dist/bin/repo-wiki.js +7 -0
- package/dist/bin/repo-wiki.js.map +1 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +404 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/compiler.d.ts +55 -0
- package/dist/src/compiler.js +2046 -0
- package/dist/src/compiler.js.map +1 -0
- package/dist/src/config.d.ts +63 -0
- package/dist/src/config.js +86 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/context-assembler.d.ts +68 -0
- package/dist/src/context-assembler.js +378 -0
- package/dist/src/context-assembler.js.map +1 -0
- package/dist/src/data-model-signals.d.ts +1 -0
- package/dist/src/data-model-signals.js +13 -0
- package/dist/src/data-model-signals.js.map +1 -0
- package/dist/src/docs-ingestor.d.ts +138 -0
- package/dist/src/docs-ingestor.js +844 -0
- package/dist/src/docs-ingestor.js.map +1 -0
- package/dist/src/docs-linter.d.ts +14 -0
- package/dist/src/docs-linter.js +164 -0
- package/dist/src/docs-linter.js.map +1 -0
- package/dist/src/docs-validation.d.ts +36 -0
- package/dist/src/docs-validation.js +297 -0
- package/dist/src/docs-validation.js.map +1 -0
- package/dist/src/extractors.d.ts +50 -0
- package/dist/src/extractors.js +2275 -0
- package/dist/src/extractors.js.map +1 -0
- package/dist/src/frontmatter.d.ts +46 -0
- package/dist/src/frontmatter.js +377 -0
- package/dist/src/frontmatter.js.map +1 -0
- package/dist/src/index.d.ts +26 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/init.d.ts +12 -0
- package/dist/src/init.js +121 -0
- package/dist/src/init.js.map +1 -0
- package/dist/src/language.d.ts +2 -0
- package/dist/src/language.js +62 -0
- package/dist/src/language.js.map +1 -0
- package/dist/src/linter.d.ts +33 -0
- package/dist/src/linter.js +398 -0
- package/dist/src/linter.js.map +1 -0
- package/dist/src/llm-provider.d.ts +267 -0
- package/dist/src/llm-provider.js +474 -0
- package/dist/src/llm-provider.js.map +1 -0
- package/dist/src/page-ownership.d.ts +38 -0
- package/dist/src/page-ownership.js +96 -0
- package/dist/src/page-ownership.js.map +1 -0
- package/dist/src/planner.d.ts +55 -0
- package/dist/src/planner.js +422 -0
- package/dist/src/planner.js.map +1 -0
- package/dist/src/prompts.d.ts +103 -0
- package/dist/src/prompts.js +344 -0
- package/dist/src/prompts.js.map +1 -0
- package/dist/src/publisher.d.ts +68 -0
- package/dist/src/publisher.js +662 -0
- package/dist/src/publisher.js.map +1 -0
- package/dist/src/repository-analysis.d.ts +88 -0
- package/dist/src/repository-analysis.js +485 -0
- package/dist/src/repository-analysis.js.map +1 -0
- package/dist/src/scanner.d.ts +122 -0
- package/dist/src/scanner.js +309 -0
- package/dist/src/scanner.js.map +1 -0
- package/dist/src/search.d.ts +71 -0
- package/dist/src/search.js +410 -0
- package/dist/src/search.js.map +1 -0
- package/dist/src/secret-patterns.d.ts +3 -0
- package/dist/src/secret-patterns.js +14 -0
- package/dist/src/secret-patterns.js.map +1 -0
- package/dist/src/utils/args.d.ts +2 -0
- package/dist/src/utils/args.js +19 -0
- package/dist/src/utils/args.js.map +1 -0
- package/dist/src/utils/dotenv.d.ts +7 -0
- package/dist/src/utils/dotenv.js +73 -0
- package/dist/src/utils/dotenv.js.map +1 -0
- package/dist/src/utils/fs.d.ts +22 -0
- package/dist/src/utils/fs.js +83 -0
- package/dist/src/utils/fs.js.map +1 -0
- package/dist/src/utils/git.d.ts +13 -0
- package/dist/src/utils/git.js +39 -0
- package/dist/src/utils/git.js.map +1 -0
- package/dist/src/wiki-graph.d.ts +74 -0
- package/dist/src/wiki-graph.js +335 -0
- package/dist/src/wiki-graph.js.map +1 -0
- package/dist/src/wiki-patch.d.ts +152 -0
- package/dist/src/wiki-patch.js +489 -0
- package/dist/src/wiki-patch.js.map +1 -0
- package/dist/src/wiki-query.d.ts +63 -0
- package/dist/src/wiki-query.js +255 -0
- package/dist/src/wiki-query.js.map +1 -0
- package/dist/test/cli.test.d.ts +1 -0
- package/dist/test/cli.test.js +514 -0
- package/dist/test/cli.test.js.map +1 -0
- package/dist/test/compiler-eval.test.d.ts +1 -0
- package/dist/test/compiler-eval.test.js +234 -0
- package/dist/test/compiler-eval.test.js.map +1 -0
- package/dist/test/compiler.test.d.ts +1 -0
- package/dist/test/compiler.test.js +2537 -0
- package/dist/test/compiler.test.js.map +1 -0
- package/dist/test/context-assembler.test.d.ts +1 -0
- package/dist/test/context-assembler.test.js +379 -0
- package/dist/test/context-assembler.test.js.map +1 -0
- package/dist/test/docs-linter.test.d.ts +1 -0
- package/dist/test/docs-linter.test.js +900 -0
- package/dist/test/docs-linter.test.js.map +1 -0
- package/dist/test/dotenv.test.d.ts +1 -0
- package/dist/test/dotenv.test.js +77 -0
- package/dist/test/dotenv.test.js.map +1 -0
- package/dist/test/extractors-go.test.d.ts +1 -0
- package/dist/test/extractors-go.test.js +393 -0
- package/dist/test/extractors-go.test.js.map +1 -0
- package/dist/test/extractors-rust.test.d.ts +1 -0
- package/dist/test/extractors-rust.test.js +219 -0
- package/dist/test/extractors-rust.test.js.map +1 -0
- package/dist/test/extractors-utils.test.d.ts +1 -0
- package/dist/test/extractors-utils.test.js +786 -0
- package/dist/test/extractors-utils.test.js.map +1 -0
- package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.d.ts +1 -0
- package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.js +4 -0
- package/dist/test/fixtures/compiler-e2e/basic-node-service/repo/infra/deploy.js.map +1 -0
- package/dist/test/frontmatter.test.d.ts +1 -0
- package/dist/test/frontmatter.test.js +287 -0
- package/dist/test/frontmatter.test.js.map +1 -0
- package/dist/test/init-planner.test.d.ts +1 -0
- package/dist/test/init-planner.test.js +688 -0
- package/dist/test/init-planner.test.js.map +1 -0
- package/dist/test/linter.test.d.ts +1 -0
- package/dist/test/linter.test.js +426 -0
- package/dist/test/linter.test.js.map +1 -0
- package/dist/test/llm-provider.test.d.ts +1 -0
- package/dist/test/llm-provider.test.js +783 -0
- package/dist/test/llm-provider.test.js.map +1 -0
- package/dist/test/page-ownership.test.d.ts +1 -0
- package/dist/test/page-ownership.test.js +247 -0
- package/dist/test/page-ownership.test.js.map +1 -0
- package/dist/test/publisher.test.d.ts +1 -0
- package/dist/test/publisher.test.js +1297 -0
- package/dist/test/publisher.test.js.map +1 -0
- package/dist/test/repository-analysis.test.d.ts +1 -0
- package/dist/test/repository-analysis.test.js +182 -0
- package/dist/test/repository-analysis.test.js.map +1 -0
- package/dist/test/run-compiled-tests.d.ts +1 -0
- package/dist/test/run-compiled-tests.js +48 -0
- package/dist/test/run-compiled-tests.js.map +1 -0
- package/dist/test/scanner.test.d.ts +1 -0
- package/dist/test/scanner.test.js +551 -0
- package/dist/test/scanner.test.js.map +1 -0
- package/dist/test/search.test.d.ts +1 -0
- package/dist/test/search.test.js +92 -0
- package/dist/test/search.test.js.map +1 -0
- package/dist/test/update-changelog.test.d.ts +1 -0
- package/dist/test/update-changelog.test.js +125 -0
- package/dist/test/update-changelog.test.js.map +1 -0
- package/dist/test/wiki-graph.test.d.ts +1 -0
- package/dist/test/wiki-graph.test.js +164 -0
- package/dist/test/wiki-graph.test.js.map +1 -0
- package/dist/test/wiki-patch.test.d.ts +1 -0
- package/dist/test/wiki-patch.test.js +610 -0
- package/dist/test/wiki-patch.test.js.map +1 -0
- package/dist/test/wiki-query.test.d.ts +1 -0
- package/dist/test/wiki-query.test.js +163 -0
- package/dist/test/wiki-query.test.js.map +1 -0
- package/docs/PLAN.md +993 -0
- package/docs/WHY.md +61 -0
- package/docs/plans/agent-integration.md +85 -0
- package/docs/plans/ci-publishing.md +111 -0
- package/docs/plans/doc-validation.md +92 -0
- package/docs/plans/github-action.md +113 -0
- package/docs/plans/incremental-mode.md +98 -0
- package/docs/plans/karpathy-llm-wiki-alignment.md +84 -0
- package/docs/plans/llm-compiler.md +160 -0
- package/docs/plans/production-scanner.md +104 -0
- package/docs/plans/query-and-file-back.md +103 -0
- package/docs/plans/search-index.md +118 -0
- package/docs/plans/trust-hardening.md +74 -0
- package/docs/plans/wiki-graph.md +183 -0
- package/docs/plans/wiki-health.md +76 -0
- package/package.json +83 -0
- package/prompts/compiler.md +16 -0
- package/prompts/lint.md +18 -0
- package/prompts/page-templates.md +25 -0
- 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
|