@falconwry/creator 0.36.0-alpha.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/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@falconwry/creator",
3
+ "version": "0.36.0-alpha.1",
4
+ "private": false,
5
+ "main": "./dist/index.js",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "typecheck": "tsc --noEmit",
9
+ "dev": "tsx src/cli.ts"
10
+ },
11
+ "dependencies": {
12
+ "@falconwry/scanner-vue": "workspace:*",
13
+ "@opencode-ai/sdk": "^1.17.8"
14
+ },
15
+ "devDependencies": {
16
+ "tsx": "^4.19.0",
17
+ "typescript": "^5.5.0"
18
+ }
19
+ }
@@ -0,0 +1,331 @@
1
+ import type { Analyzer, AnalyzedTool, RawTool, PromptContext, Scanner } from './types.js'
2
+ import type { Logger } from './logger.js'
3
+ import { RuleBasedAnalyzer } from './analyzer.js'
4
+
5
+ const MAX_RETRY = 3
6
+ const BATCH_SIZE = 15
7
+ const TIMEOUT_MS = 60_000
8
+ const MAX_FEEDBACK_LEN = 2000
9
+
10
+ function buildBatchPrompt(
11
+ batch: RawTool[],
12
+ context: PromptContext,
13
+ errorFeedback?: string,
14
+ ): string {
15
+ let prompt = `你是 ${context.projectType} 的一个代码分析助手。
16
+ 以下列出了该项目中多个封装好的 ${context.toolConcept},请为每个工具生成分析信息。
17
+
18
+ 工具列表:
19
+ ---
20
+ `
21
+ for (let i = 0; i < batch.length; i++) {
22
+ const tool = batch[i]
23
+ prompt += `序号: ${i + 1}
24
+ 文件路径: ${tool.src}
25
+ 行号: ${tool.startLine}-${tool.endLine}
26
+ 代码:
27
+ \`\`\`
28
+ ${tool.code}
29
+ \`\`\`
30
+ ---
31
+ `
32
+ }
33
+
34
+ prompt += `
35
+ 请为每个工具返回一个 JSON 数组,每个元素包含以下字段:
36
+ 1. name:工具名称(必须与上面给出的名称一致)
37
+ 2. description:一句话概括该工具的功能(中文,50 字左右)
38
+ 3. category:分类,必须是以下之一:${context.categories.join(' / ')}
39
+ 4. tags:5 个左右的标签,如:${context.exampleTags.join('、')}
40
+
41
+ 请严格返回 JSON 数组格式,不要包含任何其他文字。
42
+ `
43
+
44
+ if (errorFeedback) {
45
+ prompt += `\n注意:${errorFeedback}\n`
46
+ }
47
+
48
+ return prompt
49
+ }
50
+
51
+ function tryParseJSON(text: string): any | null {
52
+ // Try to extract JSON array from the response
53
+ const cleaned = text.trim()
54
+ .replace(/^```json\s*/i, '')
55
+ .replace(/^```\s*/i, '')
56
+ .replace(/\s*```$/, '')
57
+
58
+ try {
59
+ return JSON.parse(cleaned)
60
+ } catch {
61
+ // Try to find a JSON array in the text
62
+ const match = cleaned.match(/\[\s*\{[\s\S]*\}\s*\]/)
63
+ if (match) {
64
+ try {
65
+ return JSON.parse(match[0])
66
+ } catch {
67
+ return null
68
+ }
69
+ }
70
+ return null
71
+ }
72
+ }
73
+
74
+ function validateStructural(
75
+ output: any[],
76
+ batch: RawTool[],
77
+ ): string[] {
78
+ const errors: string[] = []
79
+
80
+ if (!Array.isArray(output)) {
81
+ return ['Response is not a JSON array']
82
+ }
83
+
84
+ if (output.length !== batch.length) {
85
+ errors.push(`Expected ${batch.length} tools in response, got ${output.length}`)
86
+ }
87
+
88
+ for (let i = 0; i < Math.min(output.length, batch.length); i++) {
89
+ const item = output[i]
90
+ const prefix = `Tool #${i + 1} ("${batch[i]?.name ?? '?'}"):`
91
+
92
+ if (!item.name) {
93
+ errors.push(`${prefix} missing "name" field`)
94
+ }
95
+
96
+ if (!item.description || typeof item.description !== 'string') {
97
+ errors.push(`${prefix} missing or invalid "description" field`)
98
+ }
99
+
100
+ if (!item.category || typeof item.category !== 'string') {
101
+ errors.push(`${prefix} missing "category" field`)
102
+ }
103
+
104
+ if (!item.tags || !Array.isArray(item.tags)) {
105
+ errors.push(`${prefix} missing or invalid "tags" field`)
106
+ }
107
+ }
108
+
109
+ return errors
110
+ }
111
+
112
+ function truncateFeedback(text: string): string {
113
+ if (text.length <= MAX_FEEDBACK_LEN) return text
114
+ return text.slice(0, MAX_FEEDBACK_LEN - 3) + '...'
115
+ }
116
+
117
+ async function analyzeBatch(
118
+ batch: RawTool[],
119
+ context: PromptContext,
120
+ scanner: Scanner,
121
+ log: Logger,
122
+ retryCount: number,
123
+ errorFeedback?: string,
124
+ ): Promise<AnalyzedTool[]> {
125
+ const { createOpencodeClient } = await import('@opencode-ai/sdk')
126
+
127
+ const client = createOpencodeClient({ baseUrl: 'http://localhost:4096' } as any)
128
+
129
+ const sessionResult = await client.session.create({
130
+ body: { title: 'falconwry-analyze' },
131
+ })
132
+
133
+ const sessionId = sessionResult.data?.id
134
+ if (!sessionId) {
135
+ throw new Error('Failed to create session')
136
+ }
137
+
138
+ try {
139
+ const promptText = buildBatchPrompt(batch, context, errorFeedback)
140
+ const controller = new AbortController()
141
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS)
142
+
143
+ const result = await client.session.prompt({
144
+ path: { id: sessionId },
145
+ body: {
146
+ model: { providerID: 'deepseek', modelID: 'deepseek-v4-pro' },
147
+ parts: [
148
+ {
149
+ type: 'text',
150
+ text: promptText,
151
+ },
152
+ ],
153
+ },
154
+ })
155
+
156
+ clearTimeout(timeout)
157
+
158
+ // Extract text from response parts
159
+ const parts = result.data?.parts ?? []
160
+ const responseText = parts
161
+ .filter((p: any) => p.type === 'text')
162
+ .map((p: any) => p.text)
163
+ .join('\n')
164
+
165
+ const output = tryParseJSON(responseText)
166
+ if (!output || !Array.isArray(output)) {
167
+ log.warn('AI response was not valid JSON')
168
+ if (retryCount < MAX_RETRY) {
169
+ console.warn(` ⚠️ Batch parse failed, retrying (${retryCount + 1}/${MAX_RETRY})...`)
170
+ return analyzeBatch(
171
+ batch,
172
+ context,
173
+ scanner,
174
+ log,
175
+ retryCount + 1,
176
+ 'Response was not valid JSON. Please return ONLY a JSON array.',
177
+ )
178
+ }
179
+ console.warn(` ❌ Batch failed after ${MAX_RETRY} retries, using rule-based fallback`)
180
+ log.error(`Batch failed after ${MAX_RETRY} retries: not valid JSON`)
181
+ return RuleBasedAnalyzer.analyze(batch, context)
182
+ }
183
+
184
+ // Step 1: Structural validation (creator)
185
+ const structuralErrors = validateStructural(output, batch)
186
+ if (structuralErrors.length > 0) {
187
+ console.warn(` ⚠️ ${structuralErrors.length} structural error(s):`)
188
+ for (const e of structuralErrors.slice(0, 3)) {
189
+ console.warn(` - ${e}`)
190
+ }
191
+ log.warn(`Structural validation failed (${structuralErrors.length} errors):`)
192
+ for (const e of structuralErrors) {
193
+ log.warn(` ${e}`)
194
+ }
195
+ if (retryCount < MAX_RETRY) {
196
+ console.warn(` Retrying (${retryCount + 1}/${MAX_RETRY})...`)
197
+ return analyzeBatch(
198
+ batch, context, scanner, log,
199
+ retryCount + 1,
200
+ truncateFeedback(structuralErrors.join('; ')),
201
+ )
202
+ }
203
+ console.warn(` ❌ Structural validation failed after ${MAX_RETRY} retries, using rule-based fallback`)
204
+ log.error(`Structural validation failed after ${MAX_RETRY} retries`)
205
+ return RuleBasedAnalyzer.analyze(batch, context)
206
+ }
207
+
208
+ // Step 2: Content validation (scanner per tool, creator iterates)
209
+ const rules = scanner.getValidationRules()
210
+ const contentErrors: string[] = []
211
+
212
+ for (const [i, item] of output.entries()) {
213
+ const tool = { ...batch[i], ...item }
214
+ const toolErrors = scanner.validateTool(tool, rules)
215
+ for (const e of toolErrors) {
216
+ contentErrors.push(`Tool #${i + 1} ("${tool.name}"): ${e}`)
217
+ }
218
+ }
219
+
220
+ if (contentErrors.length > 0) {
221
+ // Console: first 3 errors
222
+ console.warn(` ⚠️ ${contentErrors.length} validation error(s):`)
223
+ for (const e of contentErrors.slice(0, 3)) {
224
+ console.warn(` - ${e}`)
225
+ }
226
+ if (contentErrors.length > 3) {
227
+ console.warn(` ... and ${contentErrors.length - 3} more (see .falconry/log/creator.log)`)
228
+ }
229
+
230
+ // Log: all errors
231
+ log.warn(`Content validation failed (${contentErrors.length} errors):`)
232
+ for (const e of contentErrors) {
233
+ log.warn(` ${e}`)
234
+ }
235
+
236
+ if (retryCount < MAX_RETRY) {
237
+ console.warn(` Retrying (${retryCount + 1}/${MAX_RETRY})...`)
238
+ return analyzeBatch(
239
+ batch, context, scanner, log,
240
+ retryCount + 1,
241
+ truncateFeedback(contentErrors.join('; ')),
242
+ )
243
+ }
244
+ console.warn(` ❌ Validation failed after ${MAX_RETRY} retries, using rule-based fallback`)
245
+ log.error(`Validation failed after ${MAX_RETRY} retries`)
246
+ return RuleBasedAnalyzer.analyze(batch, context)
247
+ }
248
+
249
+ return output.map((item: any, i: number) => ({
250
+ ...batch[i],
251
+ description: item.description,
252
+ category: item.category,
253
+ tags: item.tags,
254
+ }))
255
+ } finally {
256
+ // Clean up session
257
+ try {
258
+ await client.session.delete({ path: { id: sessionId } })
259
+ } catch {
260
+ // Ignore cleanup errors
261
+ }
262
+ }
263
+ }
264
+
265
+ function chunkArray<T>(arr: T[], size: number): T[][] {
266
+ const chunks: T[][] = []
267
+ for (let i = 0; i < arr.length; i += size) {
268
+ chunks.push(arr.slice(i, i + size))
269
+ }
270
+ return chunks
271
+ }
272
+
273
+ export function createOpenCodeAnalyzer(scanner: Scanner, log: Logger): Analyzer {
274
+ return {
275
+ async analyze(tools: RawTool[], context: PromptContext): Promise<AnalyzedTool[]> {
276
+ if (tools.length === 0) return []
277
+
278
+ // Pre-flight: check if OpenCode server is reachable
279
+ try {
280
+ const { createOpencodeClient } = await import('@opencode-ai/sdk')
281
+ const client = createOpencodeClient({ baseUrl: 'http://localhost:4096' } as any)
282
+ await client.session.list()
283
+ } catch {
284
+ throw new Error(
285
+ 'OpenCode server not available at localhost:4096. ' +
286
+ 'Start the server or the analysis will fall back to rules.',
287
+ )
288
+ }
289
+
290
+ const batches = chunkArray(tools, BATCH_SIZE)
291
+ const results: AnalyzedTool[] = []
292
+
293
+ log.info(`AI analysis started: ${tools.length} tools in ${batches.length} batch(es)`)
294
+
295
+ for (let i = 0; i < batches.length; i++) {
296
+ const batch = batches[i]
297
+ console.log(` 🧠 AI analyzing batch ${i + 1}/${batches.length} (${batch.length} tools)...`)
298
+ log.info(`Batch ${i + 1}/${batches.length}: ${batch.length} tools`)
299
+ try {
300
+ const analyzed = await analyzeBatch(batch, context, scanner, log, 0)
301
+ results.push(...analyzed)
302
+ log.info(`Batch ${i + 1} complete: ${analyzed.length} tools analyzed`)
303
+ } catch (err: any) {
304
+ console.warn(` ⚠️ Batch ${i + 1} failed: ${err.message}`)
305
+ console.warn(` ⬇ Falling back to rule-based analysis for this batch`)
306
+ log.warn(`Batch ${i + 1} failed: ${err.message}, falling back to rules`)
307
+ const fallback = await RuleBasedAnalyzer.analyze(batch, context)
308
+ results.push(...fallback)
309
+ }
310
+ }
311
+
312
+ log.info(`AI analysis complete: ${results.length} tools`)
313
+ return results
314
+ },
315
+ }
316
+ }
317
+
318
+ // Legacy static export for compatibility
319
+ export const OpenCodeAnalyzer: Analyzer = createOpenCodeAnalyzer(
320
+ {
321
+ name: 'noop',
322
+ scanFiles: () => [],
323
+ scanRegistrations: () => [],
324
+ scanDependencies: () => ({}),
325
+ buildRegistry: () => [],
326
+ getPromptContext: () => ({ projectType: '', toolConcept: '', categories: [], exampleTags: [] }),
327
+ getValidationRules: () => ({ description: { minLength: 0, maxLength: 999 }, tags: { minCount: 0, maxCount: 99 }, nameMatch: 'loose', allowedCategories: [] }),
328
+ validateTool: () => [],
329
+ },
330
+ { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
331
+ )
@@ -0,0 +1,154 @@
1
+ import type { RawTool, AnalyzedTool, Analyzer, PromptContext } from './types.js'
2
+
3
+ const KEYWORD_MAP: Record<string, string> = {
4
+ date: '日期',
5
+ time: '时间',
6
+ format: '格式化',
7
+ currency: '货币',
8
+ number: '数字',
9
+ button: '按钮',
10
+ table: '表格',
11
+ modal: '弹窗',
12
+ dialog: '弹窗',
13
+ request: '请求',
14
+ fetch: '请求',
15
+ pagination: '分页',
16
+ page: '分页',
17
+ focus: '聚焦',
18
+ badge: '角标',
19
+ icon: '图标',
20
+ data: '数据',
21
+ base: '基础',
22
+ upload: '上传',
23
+ download: '下载',
24
+ select: '选择',
25
+ input: '输入',
26
+ form: '表单',
27
+ validate: '验证',
28
+ toast: '提示',
29
+ loading: '加载',
30
+ error: '错误',
31
+ search: '搜索',
32
+ filter: '过滤',
33
+ sort: '排序',
34
+ copy: '复制',
35
+ clipboard: '剪贴板',
36
+ storage: '存储',
37
+ cookie: 'Cookie',
38
+ router: '路由',
39
+ auth: '认证',
40
+ permission: '权限',
41
+ user: '用户',
42
+ list: '列表',
43
+ card: '卡片',
44
+ avatar: '头像',
45
+ progress: '进度',
46
+ tooltip: '提示',
47
+ dropdown: '下拉',
48
+ menu: '菜单',
49
+ nav: '导航',
50
+ tab: '标签页',
51
+ tree: '树',
52
+ chart: '图表',
53
+ }
54
+
55
+ const CATEGORY_SUFFIX: Record<string, string> = {
56
+ component: '组件',
57
+ composable: '组合式函数',
58
+ function: '工具',
59
+ directive: '指令',
60
+ mixin: '混入',
61
+ plugin: '插件',
62
+ }
63
+
64
+ const COMMON_PREFIXES = new Set([
65
+ 'use', 'v', 'on', 'handle', 'get', 'set', 'is', 'has', 'create',
66
+ ])
67
+
68
+ function splitCamelCase(name: string): string[] {
69
+ const words: string[] = []
70
+ let current = ''
71
+ for (const ch of name) {
72
+ if (ch >= 'A' && ch <= 'Z') {
73
+ if (current) words.push(current)
74
+ current = ch
75
+ } else {
76
+ current += ch
77
+ }
78
+ }
79
+ if (current) words.push(current)
80
+ return words
81
+ }
82
+
83
+ function wordsToChinese(words: string[]): string[] {
84
+ const result: string[] = []
85
+ for (const w of words) {
86
+ const lower = w.toLowerCase()
87
+ const cn = KEYWORD_MAP[lower]
88
+ if (cn) {
89
+ // Avoid consecutive duplicates (e.g., "Modal" + "Dialog" → only one "弹窗")
90
+ if (result.length > 0 && result[result.length - 1] === cn) continue
91
+ result.push(cn)
92
+ }
93
+ }
94
+ return result
95
+ }
96
+
97
+ function generateDescription(name: string, category: string, src: string): string {
98
+ const rawWords = splitCamelCase(name)
99
+ const meaningful = rawWords.filter((w) => !COMMON_PREFIXES.has(w))
100
+ const chineseWords = wordsToChinese(meaningful)
101
+
102
+ // For components, use the raw words if no Chinese mapping found
103
+ const suffix = CATEGORY_SUFFIX[category] || ''
104
+ if (chineseWords.length === 0) {
105
+ const fallback = meaningful.join('')
106
+ return fallback ? `${fallback}${suffix}` : name
107
+ }
108
+
109
+ return chineseWords.join('') + suffix
110
+ }
111
+
112
+ function generateTags(name: string, src: string): string[] {
113
+ const rawWords = splitCamelCase(name)
114
+ const meaningful = rawWords.filter((w) => !COMMON_PREFIXES.has(w))
115
+ const chineseWords = wordsToChinese(meaningful)
116
+
117
+ // Also add tags from the directory name
118
+ const dirParts = src.split('/')
119
+ if (dirParts.length >= 2) {
120
+ const parentDir = dirParts[dirParts.length - 2]
121
+ const dirTag = KEYWORD_MAP[parentDir.toLowerCase()]
122
+ if (dirTag && !chineseWords.includes(dirTag)) {
123
+ chineseWords.push(dirTag)
124
+ }
125
+ }
126
+
127
+ return chineseWords.length > 0 ? chineseWords : [name]
128
+ }
129
+
130
+ function inferCategory(name: string, src: string): string {
131
+ if (src.endsWith('.vue')) return 'component'
132
+ if (/^use[A-Z]/.test(name)) return 'composable'
133
+ if (/^v[A-Z]/.test(name)) return 'directive'
134
+ if (/mixin$/i.test(name)) return 'mixin'
135
+ if (src.includes('/directives/')) return 'directive'
136
+ if (src.includes('/composables/')) return 'composable'
137
+ if (src.includes('/mixins/')) return 'mixin'
138
+ if (src.includes('/components/')) return 'component'
139
+ return 'function'
140
+ }
141
+
142
+ export const RuleBasedAnalyzer: Analyzer = {
143
+ async analyze(tools: RawTool[], _context: PromptContext): Promise<AnalyzedTool[]> {
144
+ return tools.map((tool) => {
145
+ const category = inferCategory(tool.name, tool.src)
146
+ return {
147
+ ...tool,
148
+ category,
149
+ description: generateDescription(tool.name, category, tool.src),
150
+ tags: generateTags(tool.name, tool.src),
151
+ }
152
+ })
153
+ },
154
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { runPipeline } from './pipeline.js'
3
+
4
+ const args = process.argv.slice(2)
5
+ const cwdIndex = args.indexOf('--cwd')
6
+ const cwd = cwdIndex !== -1 ? args[cwdIndex + 1] : process.cwd()
7
+
8
+ console.log('🔍 Falconwry Creator v0.36.0-alpha.1')
9
+ console.log(`📦 Scanner: @falconwry/scanner-vue`)
10
+ console.log(`📂 Project: ${cwd}\n`)
11
+
12
+ runPipeline(cwd)
13
+ .then(({ stats }) => {
14
+ console.log(`📂 File scan: ${stats.fileTools} tools`)
15
+ console.log(`🔗 Registrations: ${stats.regTools} tools`)
16
+ console.log(`📎 Dependencies: ${stats.depsEntries} resolved`)
17
+ console.log(`🔀 Merged: ${stats.merged}`)
18
+ console.log(`🔬 Filtered: ${stats.filtered}`)
19
+ console.log(`📝 Registry: ${stats.registry} entries`)
20
+ console.log(`✅ Done in ${stats.elapsed}ms`)
21
+ console.log(`📄 Output: .falconry/map.json`)
22
+ })
23
+ .catch((err) => {
24
+ console.error('❌ Error:', err.message)
25
+ process.exit(1)
26
+ })
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export type {
2
+ RawTool,
3
+ AnalyzedTool,
4
+ BaseRegistry,
5
+ FrontendRegistry,
6
+ DependentsMap,
7
+ ValidationRules,
8
+ Scanner,
9
+ Analyzer,
10
+ PromptContext,
11
+ } from './types.js'
12
+
13
+ export { RuleBasedAnalyzer } from './analyzer.js'
14
+ export { OpenCodeAnalyzer, createOpenCodeAnalyzer } from './ai-analyzer.js'
15
+ export { runPipeline } from './pipeline.js'
16
+ export type { PipelineStats } from './pipeline.js'
package/src/logger.ts ADDED
@@ -0,0 +1,58 @@
1
+ import * as fs from 'fs'
2
+ import * as nodePath from 'path'
3
+
4
+ export type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'
5
+
6
+ const NAME = 'creator'
7
+
8
+ const LEVEL_ORDER: Record<LogLevel, number> = {
9
+ DEBUG: 0,
10
+ INFO: 1,
11
+ WARN: 2,
12
+ ERROR: 3,
13
+ }
14
+
15
+ function resolveLevel(minLevel?: LogLevel): number {
16
+ const env = (process.env.FALCONRY_LOG_LEVEL?.toUpperCase() || 'INFO') as LogLevel
17
+ const threshold = LEVEL_ORDER[minLevel || env]
18
+ return threshold !== undefined ? threshold : LEVEL_ORDER.INFO
19
+ }
20
+
21
+ function ensureLogDir(projectRoot: string): string {
22
+ const dir = nodePath.join(projectRoot, '.falconry', 'log')
23
+ if (!fs.existsSync(dir)) {
24
+ fs.mkdirSync(dir, { recursive: true })
25
+ }
26
+ return dir
27
+ }
28
+
29
+ function writeLine(dir: string, file: string, line: string) {
30
+ try {
31
+ fs.appendFileSync(nodePath.join(dir, file), line + '\n', 'utf-8')
32
+ } catch {
33
+ // Silently ignore write failures
34
+ }
35
+ }
36
+
37
+ export function createLogger(projectRoot: string, minLevel?: LogLevel) {
38
+ const dir = ensureLogDir(projectRoot)
39
+ const threshold = resolveLevel(minLevel)
40
+ const levels = Object.keys(LEVEL_ORDER)
41
+
42
+ function log(level: LogLevel, msg: string) {
43
+ if (LEVEL_ORDER[level] < threshold) return
44
+ const ts = new Date().toISOString()
45
+ const line = `${ts} [${level.padEnd(5)}] [${NAME}] ${msg}`
46
+ writeLine(dir, `${NAME}.log`, line)
47
+ writeLine(dir, 'all.log', line)
48
+ }
49
+
50
+ return {
51
+ debug: (msg: string) => log('DEBUG', msg),
52
+ info: (msg: string) => log('INFO', msg),
53
+ warn: (msg: string) => log('WARN', msg),
54
+ error: (msg: string) => log('ERROR', msg),
55
+ }
56
+ }
57
+
58
+ export type Logger = ReturnType<typeof createLogger>
@@ -0,0 +1,130 @@
1
+ import * as fs from 'fs'
2
+ import * as nodePath from 'path'
3
+ import { VueScanner } from '@falconwry/scanner-vue'
4
+ import { RuleBasedAnalyzer } from './analyzer.js'
5
+ import { createOpenCodeAnalyzer } from './ai-analyzer.js'
6
+ import { createLogger } from './logger.js'
7
+ import type { RawTool, AnalyzedTool, BaseRegistry, DependentsMap, Scanner } from './types.js'
8
+
9
+ const EXCLUDE_NAMES = new Set([
10
+ 'App',
11
+ 'main',
12
+ 'createApp',
13
+ 'default',
14
+ ])
15
+
16
+ function filterNonTools(tools: RawTool[]): RawTool[] {
17
+ return tools.filter((tool) => {
18
+ if (EXCLUDE_NAMES.has(tool.name)) return false
19
+ // Filter out type-only exports (no real code)
20
+ if (tool.code.trim().startsWith('type ') || tool.code.trim().startsWith('interface ')) return false
21
+ // Filter out empty or tiny exports
22
+ if (tool.endLine - tool.startLine < 1) return false
23
+ return true
24
+ })
25
+ }
26
+
27
+ function mergeAndDedupe(fileTools: RawTool[], regTools: RawTool[]): RawTool[] {
28
+ const seen = new Set<string>()
29
+ const merged: RawTool[] = []
30
+
31
+ for (const tool of fileTools) {
32
+ const key = `${tool.src}:${tool.name}`
33
+ if (!seen.has(key)) {
34
+ seen.add(key)
35
+ merged.push(tool)
36
+ }
37
+ }
38
+
39
+ for (const tool of regTools) {
40
+ const key = `${tool.src}:${tool.name}`
41
+ if (!seen.has(key)) {
42
+ seen.add(key)
43
+ merged.push(tool)
44
+ }
45
+ }
46
+
47
+ return merged
48
+ }
49
+
50
+ export async function runPipeline(
51
+ projectRoot: string,
52
+ scanner: Scanner = VueScanner,
53
+ ): Promise<{ registry: BaseRegistry[]; stats: PipelineStats }> {
54
+ const log = createLogger(projectRoot)
55
+ const startTime = Date.now()
56
+
57
+ log.info('Pipeline started')
58
+ log.info(`Project: ${projectRoot}`)
59
+ log.info(`Scanner: ${scanner.name}`)
60
+
61
+ // Step 1+2: Scan files and registrations
62
+ log.info('Step 1: Scanning files...')
63
+ const t1 = Date.now()
64
+ const fileTools = scanner.scanFiles(projectRoot)
65
+ const regTools = scanner.scanRegistrations(projectRoot)
66
+ const deps: DependentsMap = scanner.scanDependencies(projectRoot)
67
+ log.info(`File scan complete: ${fileTools.length} tools (${Date.now() - t1}ms)`)
68
+ log.info(`Registration scan complete: ${regTools.length} registrations`)
69
+ log.info(`Dependency scan complete: ${Object.keys(deps).length} resolved`)
70
+
71
+ // Step 3: Merge and filter
72
+ const merged = mergeAndDedupe(fileTools, regTools)
73
+ const filtered = filterNonTools(merged)
74
+ log.info(`Merge: ${fileTools.length} + ${regTools.length} → ${merged.length} (filtered to ${filtered.length})`)
75
+
76
+ // Step 4: Analyze (AI first, fallback to rule-based)
77
+ const context = scanner.getPromptContext(projectRoot)
78
+ log.info(`Context: ${context.projectType}`)
79
+ let analyzed: AnalyzedTool[]
80
+ const t4 = Date.now()
81
+ try {
82
+ log.info('Analysis: trying AI via OpenCode server...')
83
+ analyzed = await createOpenCodeAnalyzer(scanner, log).analyze(filtered, context)
84
+ log.info(`AI analysis complete: ${analyzed.length} tools (${Date.now() - t4}ms)`)
85
+ } catch {
86
+ console.warn('⚠️ OpenCode server not available, falling back to rule-based analysis')
87
+ log.warn('OpenCode server not available, falling back to rule-based analysis')
88
+ analyzed = await RuleBasedAnalyzer.analyze(filtered, context)
89
+ log.info(`Rule-based analysis complete: ${analyzed.length} tools (${Date.now() - t4}ms)`)
90
+ }
91
+
92
+ // Step 5: Build registry
93
+ const registry = scanner.buildRegistry(analyzed, deps)
94
+ log.info(`Registry built: ${registry.length} entries`)
95
+
96
+ // Step 6: Write .falconry/map.json
97
+ const falconryDir = nodePath.join(projectRoot, '.falconry')
98
+ if (!fs.existsSync(falconryDir)) {
99
+ fs.mkdirSync(falconryDir, { recursive: true })
100
+ }
101
+ const mapPath = nodePath.join(falconryDir, 'map.json')
102
+ fs.writeFileSync(mapPath, JSON.stringify(registry, null, 2), 'utf-8')
103
+ log.info(`Registry written to ${mapPath}`)
104
+
105
+ const elapsed = Date.now() - startTime
106
+ log.info(`Pipeline complete: ${registry.length} entries in ${elapsed}ms`)
107
+
108
+ return {
109
+ registry,
110
+ stats: {
111
+ fileTools: fileTools.length,
112
+ regTools: regTools.length,
113
+ depsEntries: Object.keys(deps).length,
114
+ merged: merged.length,
115
+ filtered: filtered.length,
116
+ registry: registry.length,
117
+ elapsed,
118
+ },
119
+ }
120
+ }
121
+
122
+ export interface PipelineStats {
123
+ fileTools: number
124
+ regTools: number
125
+ depsEntries: number
126
+ merged: number
127
+ filtered: number
128
+ registry: number
129
+ elapsed: number
130
+ }
package/src/types.ts ADDED
@@ -0,0 +1,74 @@
1
+ // @falconwry/creator — 核心类型定义
2
+ // Scanner 包通过 creator 的导出引入
3
+
4
+ /** Scanner 产出的原始工具条目(语言无关) */
5
+ export type RawTool = {
6
+ name: string
7
+ src: string
8
+ startLine: number
9
+ endLine: number
10
+ code: string
11
+ }
12
+
13
+ /** AI Prompt 上下文,由 Scanner 提供 */
14
+ export type PromptContext = {
15
+ projectType: string
16
+ toolConcept: string
17
+ categories: string[]
18
+ exampleTags: string[]
19
+ }
20
+
21
+ /** 分析后的工具条目(RawTool + AI/规则分析结果) */
22
+ export type AnalyzedTool = RawTool & {
23
+ description: string
24
+ category: string
25
+ tags: string[]
26
+ }
27
+
28
+ /** 引用关系映射,key = `${src}:${name}` */
29
+ export type DependentsMap = Record<string, {
30
+ count: number
31
+ list: string[]
32
+ }>
33
+
34
+ /** 基础注册表字段(所有语言通用) */
35
+ export type BaseRegistry = {
36
+ name: string
37
+ description: string
38
+ src: string
39
+ startLine: number
40
+ endLine: number
41
+ category: string
42
+ tags: string[]
43
+ dependents: { count: number; list: string[] }
44
+ }
45
+
46
+ /** 前端 JS/TS/Vue 注册表扩展 */
47
+ export type FrontendRegistry = BaseRegistry & {
48
+ exportName: string
49
+ }
50
+
51
+ /** 分析器接口 — V0.2 规则引擎,V0.3 替换为 AI */
52
+ export interface Analyzer {
53
+ analyze(tools: RawTool[], context: PromptContext): Promise<AnalyzedTool[]>
54
+ }
55
+
56
+ /** 校验规则 — 类型由 creator 定义,值由各 Scanner 提供 */
57
+ export type ValidationRules = {
58
+ description: { minLength: number; maxLength: number }
59
+ tags: { minCount: number; maxCount: number }
60
+ nameMatch: 'strict' | 'loose'
61
+ allowedCategories: string[]
62
+ }
63
+
64
+ /** Scanner 插件接口 */
65
+ export interface Scanner {
66
+ name: string
67
+ scanFiles(projectRoot: string): RawTool[]
68
+ scanRegistrations(projectRoot: string): RawTool[]
69
+ scanDependencies(projectRoot: string): DependentsMap
70
+ buildRegistry(tools: AnalyzedTool[], deps: DependentsMap): BaseRegistry[]
71
+ getPromptContext(projectRoot: string): PromptContext
72
+ getValidationRules(): ValidationRules
73
+ validateTool(tool: AnalyzedTool, rules: ValidationRules): string[]
74
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src"]
9
+ }