@intrect/openswarm 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -331
- package/config.example.yaml +36 -13
- package/dist/adapters/agenticLoop.d.ts +90 -0
- package/dist/adapters/agenticLoop.d.ts.map +1 -0
- package/dist/adapters/agenticLoop.js +141 -0
- package/dist/adapters/agenticLoop.js.map +1 -0
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js +4 -0
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/cryptoQuantAdapter.js +1 -1
- package/dist/adapters/cryptoQuantAdapter.js.map +1 -1
- package/dist/adapters/gpt.d.ts +19 -0
- package/dist/adapters/gpt.d.ts.map +1 -0
- package/dist/adapters/gpt.js +251 -0
- package/dist/adapters/gpt.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +6 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/local.d.ts +31 -0
- package/dist/adapters/local.d.ts.map +1 -0
- package/dist/adapters/local.js +320 -0
- package/dist/adapters/local.js.map +1 -0
- package/dist/adapters/tools.d.ts +30 -0
- package/dist/adapters/tools.d.ts.map +1 -0
- package/dist/adapters/tools.js +219 -0
- package/dist/adapters/tools.js.map +1 -0
- package/dist/adapters/types.d.ts +6 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/agents/pairPipeline.d.ts +7 -0
- package/dist/agents/pairPipeline.d.ts.map +1 -1
- package/dist/agents/pairPipeline.js +99 -7
- package/dist/agents/pairPipeline.js.map +1 -1
- package/dist/agents/pipelineGuards.d.ts.map +1 -1
- package/dist/agents/pipelineGuards.js +84 -2
- package/dist/agents/pipelineGuards.js.map +1 -1
- package/dist/agents/worker.d.ts +3 -0
- package/dist/agents/worker.d.ts.map +1 -1
- package/dist/agents/worker.js +1 -0
- package/dist/agents/worker.js.map +1 -1
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oauthPkce.d.ts +21 -0
- package/dist/auth/oauthPkce.d.ts.map +1 -0
- package/dist/auth/oauthPkce.js +212 -0
- package/dist/auth/oauthPkce.js.map +1 -0
- package/dist/auth/oauthStore.d.ts +24 -0
- package/dist/auth/oauthStore.d.ts.map +1 -0
- package/dist/auth/oauthStore.js +96 -0
- package/dist/auth/oauthStore.js.map +1 -0
- package/dist/automation/autonomousRunner.d.ts +5 -5
- package/dist/automation/runnerTypes.d.ts +1 -1
- package/dist/automation/runnerTypes.d.ts.map +1 -1
- package/dist/cli/authHandler.d.ts +16 -0
- package/dist/cli/authHandler.d.ts.map +1 -0
- package/dist/cli/authHandler.js +93 -0
- package/dist/cli/authHandler.js.map +1 -0
- package/dist/cli/checkHandler.d.ts +25 -0
- package/dist/cli/checkHandler.d.ts.map +1 -0
- package/dist/cli/checkHandler.js +465 -0
- package/dist/cli/checkHandler.js.map +1 -0
- package/dist/cli.js +64 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/config.d.ts +17 -4
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +21 -8
- package/dist/core/config.js.map +1 -1
- package/dist/core/service.d.ts.map +1 -1
- package/dist/core/service.js +18 -8
- package/dist/core/service.js.map +1 -1
- package/dist/core/types.d.ts +4 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/issues/graphql/resolvers.d.ts +252 -0
- package/dist/issues/graphql/resolvers.d.ts.map +1 -0
- package/dist/issues/graphql/resolvers.js +88 -0
- package/dist/issues/graphql/resolvers.js.map +1 -0
- package/dist/issues/graphql/server.d.ts +13 -0
- package/dist/issues/graphql/server.d.ts.map +1 -0
- package/dist/issues/graphql/server.js +56 -0
- package/dist/issues/graphql/server.js.map +1 -0
- package/dist/issues/graphql/typeDefs.d.ts +2 -0
- package/dist/issues/graphql/typeDefs.d.ts.map +1 -0
- package/dist/issues/graphql/typeDefs.js +251 -0
- package/dist/issues/graphql/typeDefs.js.map +1 -0
- package/dist/issues/index.d.ts +8 -0
- package/dist/issues/index.d.ts.map +1 -0
- package/dist/issues/index.js +11 -0
- package/dist/issues/index.js.map +1 -0
- package/dist/issues/issueBoardHtml.d.ts +2 -0
- package/dist/issues/issueBoardHtml.d.ts.map +1 -0
- package/dist/issues/issueBoardHtml.js +677 -0
- package/dist/issues/issueBoardHtml.js.map +1 -0
- package/dist/issues/linearBridge.d.ts +27 -0
- package/dist/issues/linearBridge.d.ts.map +1 -0
- package/dist/issues/linearBridge.js +211 -0
- package/dist/issues/linearBridge.js.map +1 -0
- package/dist/issues/memoryBridge.d.ts +35 -0
- package/dist/issues/memoryBridge.d.ts.map +1 -0
- package/dist/issues/memoryBridge.js +184 -0
- package/dist/issues/memoryBridge.js.map +1 -0
- package/dist/issues/schema.d.ts +162 -0
- package/dist/issues/schema.d.ts.map +1 -0
- package/dist/issues/schema.js +121 -0
- package/dist/issues/schema.js.map +1 -0
- package/dist/issues/sqliteStore.d.ts +90 -0
- package/dist/issues/sqliteStore.d.ts.map +1 -0
- package/dist/issues/sqliteStore.js +488 -0
- package/dist/issues/sqliteStore.js.map +1 -0
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/index.js +9 -3
- package/dist/knowledge/index.js.map +1 -1
- package/dist/linear/linear.d.ts +4 -0
- package/dist/linear/linear.d.ts.map +1 -1
- package/dist/linear/linear.js +27 -0
- package/dist/linear/linear.js.map +1 -1
- package/dist/locale/prompts/en.d.ts.map +1 -1
- package/dist/locale/prompts/en.js +32 -2
- package/dist/locale/prompts/en.js.map +1 -1
- package/dist/locale/prompts/ko.d.ts.map +1 -1
- package/dist/locale/prompts/ko.js +32 -2
- package/dist/locale/prompts/ko.js.map +1 -1
- package/dist/locale/types.d.ts +17 -0
- package/dist/locale/types.d.ts.map +1 -1
- package/dist/registry/bsDetector.d.ts +24 -0
- package/dist/registry/bsDetector.d.ts.map +1 -0
- package/dist/registry/bsDetector.js +276 -0
- package/dist/registry/bsDetector.js.map +1 -0
- package/dist/registry/entityScanner.d.ts +36 -0
- package/dist/registry/entityScanner.d.ts.map +1 -0
- package/dist/registry/entityScanner.js +693 -0
- package/dist/registry/entityScanner.js.map +1 -0
- package/dist/registry/graphql/resolvers.d.ts +778 -0
- package/dist/registry/graphql/resolvers.d.ts.map +1 -0
- package/dist/registry/graphql/resolvers.js +127 -0
- package/dist/registry/graphql/resolvers.js.map +1 -0
- package/dist/registry/graphql/typeDefs.d.ts +2 -0
- package/dist/registry/graphql/typeDefs.d.ts.map +1 -0
- package/dist/registry/graphql/typeDefs.js +276 -0
- package/dist/registry/graphql/typeDefs.js.map +1 -0
- package/dist/registry/index.d.ts +12 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +18 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/issueBridge.d.ts +8 -0
- package/dist/registry/issueBridge.d.ts.map +1 -0
- package/dist/registry/issueBridge.js +30 -0
- package/dist/registry/issueBridge.js.map +1 -0
- package/dist/registry/memoryBridge.d.ts +13 -0
- package/dist/registry/memoryBridge.d.ts.map +1 -0
- package/dist/registry/memoryBridge.js +60 -0
- package/dist/registry/memoryBridge.js.map +1 -0
- package/dist/registry/schema.d.ts +307 -0
- package/dist/registry/schema.d.ts.map +1 -0
- package/dist/registry/schema.js +139 -0
- package/dist/registry/schema.js.map +1 -0
- package/dist/registry/sqliteStore.d.ts +101 -0
- package/dist/registry/sqliteStore.d.ts.map +1 -0
- package/dist/registry/sqliteStore.js +688 -0
- package/dist/registry/sqliteStore.js.map +1 -0
- package/dist/support/chatBackend.d.ts.map +1 -1
- package/dist/support/chatBackend.js +35 -4
- package/dist/support/chatBackend.js.map +1 -1
- package/dist/support/chatTui.d.ts.map +1 -1
- package/dist/support/chatTui.js +109 -3
- package/dist/support/chatTui.js.map +1 -1
- package/dist/support/dashboardHtml.d.ts +1 -1
- package/dist/support/dashboardHtml.d.ts.map +1 -1
- package/dist/support/dashboardHtml.js +1 -0
- package/dist/support/dashboardHtml.js.map +1 -1
- package/dist/support/web.d.ts.map +1 -1
- package/dist/support/web.js +16 -3
- package/dist/support/web.js.map +1 -1
- package/package.json +8 -2
- package/templates/TOOLS.md +2 -2
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// OpenSwarm - Entity Scanner
|
|
3
|
+
// Created: 2026-04-10
|
|
4
|
+
// Purpose: 레포 소스 파일에서 함수/클래스/타입/상수 선언을 추출하여 레지스트리에 등록
|
|
5
|
+
// Dependencies: registry/sqliteStore
|
|
6
|
+
// Supported: TypeScript, JavaScript, Python, Go, Rust, Java, C, C++, C#
|
|
7
|
+
// ============================================
|
|
8
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
9
|
+
import { join, extname, dirname } from 'node:path';
|
|
10
|
+
import { getRegistryStore } from './sqliteStore.js';
|
|
11
|
+
// ============ 상수 ============
|
|
12
|
+
const SKIP_DIRS = new Set([
|
|
13
|
+
'node_modules', '.git', 'dist', 'build', '__pycache__',
|
|
14
|
+
'.next', '.venv', 'venv', '.tox', '.mypy_cache', '.pytest_cache',
|
|
15
|
+
'coverage', '.turbo', '.cache', '.parcel-cache',
|
|
16
|
+
'.venv-mcp', 'site-packages', '.openswarm',
|
|
17
|
+
'trash', 'testing', 'vendor', 'third_party',
|
|
18
|
+
'target', // Rust/Java
|
|
19
|
+
'bin', 'obj', // C#
|
|
20
|
+
'cmake-build-debug', 'cmake-build-release', // C/C++
|
|
21
|
+
]);
|
|
22
|
+
const SKIP_DIR_PREFIXES = ['.venv'];
|
|
23
|
+
const MAX_FILE_SIZE = 512 * 1024;
|
|
24
|
+
const MAX_DEPTH = 15;
|
|
25
|
+
const SCAN_TIMEOUT_MS = 60_000;
|
|
26
|
+
// ---- TypeScript / JavaScript ----
|
|
27
|
+
const TS_CONFIG = {
|
|
28
|
+
extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
|
|
29
|
+
testPatterns: [/\.test\.[tj]sx?$/, /\.spec\.[tj]sx?$/],
|
|
30
|
+
blockStyle: 'brace',
|
|
31
|
+
skipIndented: ['function'],
|
|
32
|
+
commentPrefixes: ['//', '*', '/*'],
|
|
33
|
+
patterns: [
|
|
34
|
+
{ pattern: /^export\s+(?:async\s+)?function\s+(\w+)\s*(\([^)]*\)(?:\s*:\s*[^{]+)?)?\s*\{?/, kind: 'function', sigGroup: 2 },
|
|
35
|
+
{ pattern: /^export\s+(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>|function)/, kind: 'function' },
|
|
36
|
+
{ pattern: /^export\s+(?:abstract\s+)?class\s+(\w+)/, kind: 'class' },
|
|
37
|
+
{ pattern: /^export\s+(?:interface|type)\s+(\w+)/, kind: 'type' },
|
|
38
|
+
{ pattern: /^export\s+(?:const\s+)?enum\s+(\w+)/, kind: 'type' },
|
|
39
|
+
{ pattern: /^export\s+const\s+([A-Z][A-Z0-9_]+)\s*=/, kind: 'constant' },
|
|
40
|
+
{ pattern: /^(?:async\s+)?function\s+(\w+)\s*(\([^)]*\)(?:\s*:\s*[^{]+)?)?\s*\{?/, kind: 'function', sigGroup: 2 },
|
|
41
|
+
{ pattern: /^(?:abstract\s+)?class\s+(\w+)/, kind: 'class' },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
// ---- Python ----
|
|
45
|
+
const PY_CONFIG = {
|
|
46
|
+
extensions: ['.py', '.pyw'],
|
|
47
|
+
testPatterns: [/_test\.py$/, /test_.*\.py$/, /\.test\.py$/],
|
|
48
|
+
blockStyle: 'indent',
|
|
49
|
+
commentPrefixes: ['#'],
|
|
50
|
+
patterns: [
|
|
51
|
+
{ pattern: /^(?:async\s+)?def\s+(\w+)\s*(\([^)]*\)(?:\s*->\s*[^:]+)?)?\s*:/, kind: 'function', sigGroup: 2 },
|
|
52
|
+
{ pattern: /^class\s+(\w+)/, kind: 'class' },
|
|
53
|
+
// 타입 별칭: Name = Literal[...] / Name = Union[...] / Name: TypeAlias = ...
|
|
54
|
+
{ pattern: /^([A-Z]\w+)\s*(?::\s*TypeAlias\s*)?=\s*(?:Literal|Union|Optional|Type|Annotated|Final)\[/, kind: 'type' },
|
|
55
|
+
// 상수: UPPER_CASE = ...
|
|
56
|
+
{ pattern: /^([A-Z][A-Z0-9_]+)\s*(?::\s*\w[^=]*)?\s*=/, kind: 'constant' },
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
// ---- Go ----
|
|
60
|
+
const GO_CONFIG = {
|
|
61
|
+
extensions: ['.go'],
|
|
62
|
+
testPatterns: [/_test\.go$/],
|
|
63
|
+
blockStyle: 'brace',
|
|
64
|
+
commentPrefixes: ['//', '*', '/*'],
|
|
65
|
+
patterns: [
|
|
66
|
+
// func FuncName(params) returnType {
|
|
67
|
+
{ pattern: /^func\s+(\w+)\s*(\([^)]*\)(?:\s*(?:\([^)]*\)|[^{]+))?)?\s*\{?/, kind: 'function', sigGroup: 2 },
|
|
68
|
+
// func (receiver) MethodName(params) — 메서드 (receiver 있음)
|
|
69
|
+
{ pattern: /^func\s+\([^)]+\)\s+(\w+)\s*(\([^)]*\)(?:\s*(?:\([^)]*\)|[^{]+))?)?\s*\{?/, kind: 'function', sigGroup: 2 },
|
|
70
|
+
// type StructName struct {
|
|
71
|
+
{ pattern: /^type\s+(\w+)\s+struct\s*\{?/, kind: 'class' },
|
|
72
|
+
// type InterfaceName interface {
|
|
73
|
+
{ pattern: /^type\s+(\w+)\s+interface\s*\{?/, kind: 'type' },
|
|
74
|
+
// type TypeName = ... / type TypeName ...
|
|
75
|
+
{ pattern: /^type\s+(\w+)\s+[^si]/, kind: 'type' },
|
|
76
|
+
// const ConstName = ... (단일 const)
|
|
77
|
+
{ pattern: /^const\s+(\w+)\s*(?:\w+)?\s*=/, kind: 'constant' },
|
|
78
|
+
// var VarName = ... (패키지 수준)
|
|
79
|
+
{ pattern: /^var\s+(\w+)\s+/, kind: 'constant' },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
// ---- Rust ----
|
|
83
|
+
const RUST_CONFIG = {
|
|
84
|
+
extensions: ['.rs'],
|
|
85
|
+
testPatterns: [], // Rust는 같은 파일 내 #[cfg(test)] mod tests {}
|
|
86
|
+
blockStyle: 'brace',
|
|
87
|
+
commentPrefixes: ['//', '///', '*', '/*'],
|
|
88
|
+
patterns: [
|
|
89
|
+
// pub fn func_name(params) -> ReturnType {
|
|
90
|
+
{ pattern: /^(?:pub(?:\(crate\))?\s+)?(?:async\s+)?fn\s+(\w+)\s*(<[^>]*>)?\s*(\([^)]*\)(?:\s*->\s*[^{]+)?)?\s*(?:where\s+[^{]*)?\{?/, kind: 'function', sigGroup: 3 },
|
|
91
|
+
// pub struct StructName {
|
|
92
|
+
{ pattern: /^(?:pub(?:\(crate\))?\s+)?struct\s+(\w+)/, kind: 'class' },
|
|
93
|
+
// pub enum EnumName {
|
|
94
|
+
{ pattern: /^(?:pub(?:\(crate\))?\s+)?enum\s+(\w+)/, kind: 'type' },
|
|
95
|
+
// pub trait TraitName {
|
|
96
|
+
{ pattern: /^(?:pub(?:\(crate\))?\s+)?trait\s+(\w+)/, kind: 'type' },
|
|
97
|
+
// type TypeAlias = ...
|
|
98
|
+
{ pattern: /^(?:pub(?:\(crate\))?\s+)?type\s+(\w+)/, kind: 'type' },
|
|
99
|
+
// const CONST_NAME: Type = ...
|
|
100
|
+
{ pattern: /^(?:pub(?:\(crate\))?\s+)?const\s+([A-Z][A-Z0-9_]+)\s*:/, kind: 'constant' },
|
|
101
|
+
// static STATIC_NAME: Type = ...
|
|
102
|
+
{ pattern: /^(?:pub(?:\(crate\))?\s+)?static\s+(?:mut\s+)?([A-Z][A-Z0-9_]+)\s*:/, kind: 'constant' },
|
|
103
|
+
// impl StructName {
|
|
104
|
+
{ pattern: /^impl(?:<[^>]*>)?\s+(\w+)(?:<[^>]*>)?\s*(?:for\s+\w+)?\s*\{/, kind: 'class' },
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
// ---- Java ----
|
|
108
|
+
const JAVA_CONFIG = {
|
|
109
|
+
extensions: ['.java'],
|
|
110
|
+
testPatterns: [/Test\.java$/, /Tests\.java$/, /IT\.java$/],
|
|
111
|
+
blockStyle: 'brace',
|
|
112
|
+
commentPrefixes: ['//', '*', '/*', '@'],
|
|
113
|
+
patterns: [
|
|
114
|
+
// public/private/protected ReturnType methodName(params) {
|
|
115
|
+
{ pattern: /^(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?(?:abstract\s+)?(?:native\s+)?(?:<[^>]+>\s+)?(?:\w+(?:<[^>]*>)?(?:\[\])*)\s+(\w+)\s*(\([^)]*\))\s*(?:throws\s+[^{]+)?\s*\{?/, kind: 'function', sigGroup: 2 },
|
|
116
|
+
// public class ClassName {
|
|
117
|
+
{ pattern: /^(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:final\s+)?(?:abstract\s+)?class\s+(\w+)/, kind: 'class' },
|
|
118
|
+
// public interface InterfaceName {
|
|
119
|
+
{ pattern: /^(?:(?:public|private|protected)\s+)?(?:static\s+)?interface\s+(\w+)/, kind: 'type' },
|
|
120
|
+
// public enum EnumName {
|
|
121
|
+
{ pattern: /^(?:(?:public|private|protected)\s+)?enum\s+(\w+)/, kind: 'type' },
|
|
122
|
+
// @interface AnnotationName {
|
|
123
|
+
{ pattern: /^(?:(?:public|private|protected)\s+)?@interface\s+(\w+)/, kind: 'type' },
|
|
124
|
+
// public record RecordName(...)
|
|
125
|
+
{ pattern: /^(?:(?:public|private|protected)\s+)?record\s+(\w+)/, kind: 'class' },
|
|
126
|
+
// public static final Type CONST_NAME = ...
|
|
127
|
+
{ pattern: /^(?:(?:public|private|protected)\s+)?static\s+final\s+\w+(?:<[^>]*>)?\s+([A-Z][A-Z0-9_]+)\s*=/, kind: 'constant' },
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
// ---- C ----
|
|
131
|
+
const C_CONFIG = {
|
|
132
|
+
extensions: ['.c', '.h'],
|
|
133
|
+
testPatterns: [/_test\.c$/, /test_.*\.c$/],
|
|
134
|
+
blockStyle: 'brace',
|
|
135
|
+
commentPrefixes: ['//', '*', '/*'],
|
|
136
|
+
patterns: [
|
|
137
|
+
// returnType funcName(params) { — top-level 함수
|
|
138
|
+
// 캡처: 반환형 다음 함수명, 괄호
|
|
139
|
+
{ pattern: /^(?:static\s+)?(?:inline\s+)?(?:extern\s+)?(?:const\s+)?(?:unsigned\s+)?(?:signed\s+)?(?:long\s+)?(?:short\s+)?\w+[\s*]+(\w+)\s*(\([^)]*\))\s*\{/, kind: 'function', sigGroup: 2 },
|
|
140
|
+
// typedef struct { ... } Name;
|
|
141
|
+
{ pattern: /^typedef\s+struct\s+(?:\w+\s*)?\{/, kind: 'class' },
|
|
142
|
+
// struct Name {
|
|
143
|
+
{ pattern: /^(?:typedef\s+)?struct\s+(\w+)\s*\{?/, kind: 'class' },
|
|
144
|
+
// enum Name {
|
|
145
|
+
{ pattern: /^(?:typedef\s+)?enum\s+(\w+)/, kind: 'type' },
|
|
146
|
+
// typedef returnType (*Name)(params);
|
|
147
|
+
{ pattern: /^typedef\s+\w+[\s*]*\(\s*\*\s*(\w+)\s*\)/, kind: 'type' },
|
|
148
|
+
// typedef ... Name;
|
|
149
|
+
{ pattern: /^typedef\s+.+\s+(\w+)\s*;/, kind: 'type' },
|
|
150
|
+
// #define MACRO_NAME
|
|
151
|
+
{ pattern: /^#define\s+([A-Z][A-Z0-9_]+)/, kind: 'constant' },
|
|
152
|
+
// const type CONST = ... / static const ...
|
|
153
|
+
{ pattern: /^(?:static\s+)?const\s+\w+\s+([A-Z][A-Z0-9_]+)\s*=/, kind: 'constant' },
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
// ---- C++ ----
|
|
157
|
+
const CPP_CONFIG = {
|
|
158
|
+
extensions: ['.cpp', '.cxx', '.cc', '.hpp', '.hxx', '.hh'],
|
|
159
|
+
testPatterns: [/_test\.cpp$/, /test_.*\.cpp$/, /_test\.cc$/, /Test\.cpp$/],
|
|
160
|
+
blockStyle: 'brace',
|
|
161
|
+
commentPrefixes: ['//', '*', '/*'],
|
|
162
|
+
patterns: [
|
|
163
|
+
// class ClassName { (C++ 우선 — 함수보다 먼저 매칭)
|
|
164
|
+
{ pattern: /^(?:template\s*<[^>]*>\s*)?(?:class|struct)\s+(?:\[\[.*?\]\]\s+)?(\w+)(?:\s*final)?\s*(?::\s*(?:public|private|protected)\s+[^{]+)?\s*\{/, kind: 'class' },
|
|
165
|
+
// namespace Name {
|
|
166
|
+
{ pattern: /^namespace\s+(\w+)/, kind: 'module' },
|
|
167
|
+
// ReturnType ClassName::methodName(params) { — 이건 메서드이므로 별도 처리
|
|
168
|
+
{ pattern: /^(?:template\s*<[^>]*>\s*)?(?:\w+[\s*&]+)?(\w+)::(\w+)\s*(\([^)]*\))/, kind: 'function', sigGroup: 3 },
|
|
169
|
+
// returnType funcName(params) {
|
|
170
|
+
{ pattern: /^(?:template\s*<[^>]*>\s*)?(?:static\s+)?(?:inline\s+)?(?:virtual\s+)?(?:explicit\s+)?(?:constexpr\s+)?(?:const\s+)?(?:unsigned\s+)?\w+[\s*&]+(\w+)\s*(\([^)]*\))\s*(?:const)?\s*(?:override|final|noexcept)?\s*\{?/, kind: 'function', sigGroup: 2 },
|
|
171
|
+
// enum class Name {
|
|
172
|
+
{ pattern: /^enum\s+(?:class\s+)?(\w+)/, kind: 'type' },
|
|
173
|
+
// using Name = ...
|
|
174
|
+
{ pattern: /^using\s+(\w+)\s*=/, kind: 'type' },
|
|
175
|
+
// typedef
|
|
176
|
+
{ pattern: /^typedef\s+.+\s+(\w+)\s*;/, kind: 'type' },
|
|
177
|
+
// constexpr auto CONST = ...
|
|
178
|
+
{ pattern: /^(?:static\s+)?(?:inline\s+)?constexpr\s+\w+\s+([A-Z][A-Z0-9_]+)\s*[={]/, kind: 'constant' },
|
|
179
|
+
{ pattern: /^#define\s+([A-Z][A-Z0-9_]+)/, kind: 'constant' },
|
|
180
|
+
{ pattern: /^(?:static\s+)?const\s+\w+\s+([A-Z][A-Z0-9_]+)\s*=/, kind: 'constant' },
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
// ---- C# ----
|
|
184
|
+
const CSHARP_CONFIG = {
|
|
185
|
+
extensions: ['.cs'],
|
|
186
|
+
testPatterns: [/Tests?\.cs$/, /\.test\.cs$/],
|
|
187
|
+
blockStyle: 'brace',
|
|
188
|
+
commentPrefixes: ['//', '///', '*', '/*'],
|
|
189
|
+
patterns: [
|
|
190
|
+
// public class ClassName
|
|
191
|
+
{ pattern: /^(?:\[.*?\]\s*)?(?:(?:public|private|protected|internal)\s+)?(?:static\s+)?(?:sealed\s+)?(?:abstract\s+)?(?:partial\s+)?class\s+(\w+)/, kind: 'class' },
|
|
192
|
+
// public struct StructName
|
|
193
|
+
{ pattern: /^(?:\[.*?\]\s*)?(?:(?:public|private|protected|internal)\s+)?(?:readonly\s+)?(?:ref\s+)?(?:partial\s+)?struct\s+(\w+)/, kind: 'class' },
|
|
194
|
+
// public record RecordName
|
|
195
|
+
{ pattern: /^(?:\[.*?\]\s*)?(?:(?:public|private|protected|internal)\s+)?(?:sealed\s+)?(?:abstract\s+)?record\s+(?:struct\s+|class\s+)?(\w+)/, kind: 'class' },
|
|
196
|
+
// public interface IInterfaceName
|
|
197
|
+
{ pattern: /^(?:\[.*?\]\s*)?(?:(?:public|private|protected|internal)\s+)?(?:partial\s+)?interface\s+(\w+)/, kind: 'type' },
|
|
198
|
+
// public enum EnumName
|
|
199
|
+
{ pattern: /^(?:\[.*?\]\s*)?(?:(?:public|private|protected|internal)\s+)?enum\s+(\w+)/, kind: 'type' },
|
|
200
|
+
// public delegate ReturnType DelegateName(params);
|
|
201
|
+
{ pattern: /^(?:(?:public|private|protected|internal)\s+)?delegate\s+\w+[\s<>[\],*]*\s+(\w+)\s*[(<]/, kind: 'type' },
|
|
202
|
+
// public ReturnType MethodName(params) {
|
|
203
|
+
{ pattern: /^(?:\[.*?\]\s*)?(?:(?:public|private|protected|internal)\s+)?(?:static\s+)?(?:virtual\s+)?(?:override\s+)?(?:abstract\s+)?(?:async\s+)?(?:new\s+)?(?:\w+(?:<[^>]*>)?(?:\[\]|\?)?)\s+(\w+)\s*(<[^>]*>)?\s*(\([^)]*\))\s*(?:where\s+[^{]*)?\s*[{=>]/, kind: 'function', sigGroup: 3 },
|
|
204
|
+
// public const Type CONST = ...
|
|
205
|
+
{ pattern: /^(?:(?:public|private|protected|internal)\s+)?(?:static\s+)?(?:readonly\s+)?const\s+\w+\s+(\w+)\s*=/, kind: 'constant' },
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
// ============ 언어 레지스트리 ============
|
|
209
|
+
const LANGUAGE_CONFIGS = {
|
|
210
|
+
typescript: TS_CONFIG,
|
|
211
|
+
python: PY_CONFIG,
|
|
212
|
+
go: GO_CONFIG,
|
|
213
|
+
rust: RUST_CONFIG,
|
|
214
|
+
java: JAVA_CONFIG,
|
|
215
|
+
c: C_CONFIG,
|
|
216
|
+
cpp: CPP_CONFIG,
|
|
217
|
+
csharp: CSHARP_CONFIG,
|
|
218
|
+
};
|
|
219
|
+
// 확장자 → 언어 매핑
|
|
220
|
+
const EXT_TO_LANGUAGE = {};
|
|
221
|
+
for (const [lang, config] of Object.entries(LANGUAGE_CONFIGS)) {
|
|
222
|
+
for (const ext of config.extensions) {
|
|
223
|
+
EXT_TO_LANGUAGE[ext] = lang;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// 지원되는 확장자 전체
|
|
227
|
+
const SOURCE_EXTENSIONS = new Set(Object.keys(EXT_TO_LANGUAGE));
|
|
228
|
+
function detectLanguage(ext) {
|
|
229
|
+
return EXT_TO_LANGUAGE[ext] ?? null;
|
|
230
|
+
}
|
|
231
|
+
function isTestFile(name, language) {
|
|
232
|
+
return LANGUAGE_CONFIGS[language].testPatterns.some(p => p.test(name));
|
|
233
|
+
}
|
|
234
|
+
// ============ 엔티티 추출 ============
|
|
235
|
+
/**
|
|
236
|
+
* 소스 파일 내용에서 엔티티 선언을 추출
|
|
237
|
+
*/
|
|
238
|
+
export function extractEntities(content, filePath, language) {
|
|
239
|
+
const lines = content.split('\n');
|
|
240
|
+
const entities = [];
|
|
241
|
+
const config = LANGUAGE_CONFIGS[language];
|
|
242
|
+
const seen = new Set();
|
|
243
|
+
for (let i = 0; i < lines.length; i++) {
|
|
244
|
+
const trimmed = lines[i].trimStart();
|
|
245
|
+
if (!trimmed)
|
|
246
|
+
continue;
|
|
247
|
+
// 주석 스킵
|
|
248
|
+
if (config.commentPrefixes.some(p => trimmed.startsWith(p)))
|
|
249
|
+
continue;
|
|
250
|
+
for (const { pattern, kind, sigGroup } of config.patterns) {
|
|
251
|
+
const match = trimmed.match(pattern);
|
|
252
|
+
if (!match)
|
|
253
|
+
continue;
|
|
254
|
+
const name = match[1];
|
|
255
|
+
if (!name)
|
|
256
|
+
continue;
|
|
257
|
+
const key = `${name}:${i}`;
|
|
258
|
+
if (seen.has(key))
|
|
259
|
+
continue;
|
|
260
|
+
seen.add(key);
|
|
261
|
+
// Python: 인덴트 있는 함수 = 메서드 → 스킵
|
|
262
|
+
if (language === 'python' && kind === 'function' && lines[i].match(/^\s+/))
|
|
263
|
+
continue;
|
|
264
|
+
// TS: skipIndented 설정된 kind가 인덴트 있으면 스킵
|
|
265
|
+
if (config.skipIndented?.includes(kind) && lines[i].match(/^\s{2,}/))
|
|
266
|
+
continue;
|
|
267
|
+
const isExported = language === 'go'
|
|
268
|
+
? /^[A-Z]/.test(name) // Go: 대문자 시작 = exported
|
|
269
|
+
: trimmed.startsWith('export') || trimmed.startsWith('pub');
|
|
270
|
+
const signature = sigGroup ? match[sigGroup]?.trim() : undefined;
|
|
271
|
+
// 블록 끝 추정
|
|
272
|
+
let lineEnd;
|
|
273
|
+
if (config.blockStyle === 'brace') {
|
|
274
|
+
lineEnd = findBraceBlockEnd(lines, i);
|
|
275
|
+
}
|
|
276
|
+
else if (config.blockStyle === 'indent') {
|
|
277
|
+
lineEnd = findIndentBlockEnd(lines, i);
|
|
278
|
+
}
|
|
279
|
+
// 복잡도 산정
|
|
280
|
+
const complexity = (kind === 'function' || kind === 'class')
|
|
281
|
+
? computeBlockMetrics(lines, i, lineEnd)
|
|
282
|
+
: { loc: 0, nestingDepth: 0, paramCount: 0 };
|
|
283
|
+
entities.push({
|
|
284
|
+
kind,
|
|
285
|
+
name,
|
|
286
|
+
filePath,
|
|
287
|
+
lineStart: i + 1,
|
|
288
|
+
lineEnd: lineEnd ? lineEnd + 1 : undefined,
|
|
289
|
+
signature,
|
|
290
|
+
isExported,
|
|
291
|
+
loc: complexity.loc,
|
|
292
|
+
nestingDepth: complexity.nestingDepth,
|
|
293
|
+
paramCount: complexity.paramCount,
|
|
294
|
+
});
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return entities;
|
|
299
|
+
}
|
|
300
|
+
// ============ 블록 끝 추정 ============
|
|
301
|
+
/** 중괄호 카운팅 (C계열, Go, Rust, Java, C#) */
|
|
302
|
+
function findBraceBlockEnd(lines, startIdx) {
|
|
303
|
+
let depth = 0;
|
|
304
|
+
let started = false;
|
|
305
|
+
for (let i = startIdx; i < Math.min(startIdx + 500, lines.length); i++) {
|
|
306
|
+
for (const ch of lines[i]) {
|
|
307
|
+
if (ch === '{') {
|
|
308
|
+
depth++;
|
|
309
|
+
started = true;
|
|
310
|
+
}
|
|
311
|
+
if (ch === '}')
|
|
312
|
+
depth--;
|
|
313
|
+
}
|
|
314
|
+
if (started && depth <= 0)
|
|
315
|
+
return i;
|
|
316
|
+
}
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
/** 인덴트 기반 (Python) */
|
|
320
|
+
function findIndentBlockEnd(lines, startIdx) {
|
|
321
|
+
// 선언 줄의 인덴트 수준 파악
|
|
322
|
+
const startLine = lines[startIdx];
|
|
323
|
+
const baseIndent = startLine.length - startLine.trimStart().length;
|
|
324
|
+
for (let i = startIdx + 1; i < Math.min(startIdx + 500, lines.length); i++) {
|
|
325
|
+
const line = lines[i];
|
|
326
|
+
// 빈 줄은 건너뜀
|
|
327
|
+
if (line.trim().length === 0)
|
|
328
|
+
continue;
|
|
329
|
+
const currentIndent = line.length - line.trimStart().length;
|
|
330
|
+
// 인덴트가 기본 수준 이하로 돌아오면 블록 끝
|
|
331
|
+
if (currentIndent <= baseIndent)
|
|
332
|
+
return i - 1;
|
|
333
|
+
}
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
// ============ 복잡도 산정 ============
|
|
337
|
+
function computeBlockMetrics(lines, startIdx, endIdx) {
|
|
338
|
+
const end = endIdx ?? Math.min(startIdx + 50, lines.length);
|
|
339
|
+
const blockLines = lines.slice(startIdx, end + 1);
|
|
340
|
+
const loc = blockLines.filter(l => l.trim().length > 0).length;
|
|
341
|
+
let maxNesting = 0;
|
|
342
|
+
let currentNesting = 0;
|
|
343
|
+
for (const line of blockLines) {
|
|
344
|
+
for (const ch of line) {
|
|
345
|
+
if (ch === '{' || ch === '(')
|
|
346
|
+
currentNesting++;
|
|
347
|
+
if (ch === '}' || ch === ')')
|
|
348
|
+
currentNesting--;
|
|
349
|
+
}
|
|
350
|
+
if (currentNesting > maxNesting)
|
|
351
|
+
maxNesting = currentNesting;
|
|
352
|
+
}
|
|
353
|
+
const firstLine = blockLines[0] ?? '';
|
|
354
|
+
const paramMatch = firstLine.match(/\(([^)]*)\)/);
|
|
355
|
+
const paramCount = paramMatch
|
|
356
|
+
? paramMatch[1].split(',').filter(p => p.trim().length > 0).length
|
|
357
|
+
: 0;
|
|
358
|
+
return { loc, nestingDepth: maxNesting, paramCount };
|
|
359
|
+
}
|
|
360
|
+
function computeComplexityFromMetrics(loc, nestingDepth, paramCount) {
|
|
361
|
+
let score = 0;
|
|
362
|
+
if (loc > 100)
|
|
363
|
+
score += 3;
|
|
364
|
+
else if (loc > 50)
|
|
365
|
+
score += 2;
|
|
366
|
+
else if (loc > 20)
|
|
367
|
+
score += 1;
|
|
368
|
+
if (nestingDepth > 6)
|
|
369
|
+
score += 3;
|
|
370
|
+
else if (nestingDepth > 4)
|
|
371
|
+
score += 2;
|
|
372
|
+
else if (nestingDepth > 2)
|
|
373
|
+
score += 1;
|
|
374
|
+
if (paramCount > 6)
|
|
375
|
+
score += 2;
|
|
376
|
+
else if (paramCount > 3)
|
|
377
|
+
score += 1;
|
|
378
|
+
return Math.min(score, 10);
|
|
379
|
+
}
|
|
380
|
+
function computeRisk(complexityScore, hasTests) {
|
|
381
|
+
if (complexityScore >= 8)
|
|
382
|
+
return 'high';
|
|
383
|
+
if (complexityScore >= 6 && !hasTests)
|
|
384
|
+
return 'high';
|
|
385
|
+
if (complexityScore >= 6)
|
|
386
|
+
return 'medium';
|
|
387
|
+
if (complexityScore >= 4 && !hasTests)
|
|
388
|
+
return 'medium';
|
|
389
|
+
return 'low';
|
|
390
|
+
}
|
|
391
|
+
const TS_IMPORT_FROM = /import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
|
|
392
|
+
// import 예약어 — 테스트 프레임워크 함수 등
|
|
393
|
+
const RESERVED_NAMES = new Set([
|
|
394
|
+
'describe', 'it', 'test', 'expect', 'beforeEach', 'afterEach',
|
|
395
|
+
'beforeAll', 'afterAll', 'vi', 'jest', 'mock', 'fn', 'spyOn',
|
|
396
|
+
'console', 'setTimeout', 'setInterval', 'Promise', 'Date',
|
|
397
|
+
'Array', 'Object', 'String', 'Number', 'Boolean', 'Map', 'Set',
|
|
398
|
+
'JSON', 'Math', 'Error', 'RegExp', 'require', 'import',
|
|
399
|
+
'resolve', 'reject', 'then', 'catch', 'finally', 'push',
|
|
400
|
+
'filter', 'map', 'reduce', 'forEach', 'find', 'includes',
|
|
401
|
+
'join', 'split', 'trim', 'slice', 'splice', 'pop', 'shift',
|
|
402
|
+
'assert', 'assertEqual', 'assertTrue', 'assertFalse', 'assertRaises', // Python
|
|
403
|
+
'testing', 'Errorf', 'Fatalf', 'Run', 'Equal', 'NotNil', // Go
|
|
404
|
+
'assert_eq', 'assert_ne', 'assert', 'panic', 'println', // Rust
|
|
405
|
+
'assertEquals', 'assertNotNull', 'assertThrows', 'assertTrue', // Java
|
|
406
|
+
'Assert', 'Fact', 'Theory', // C#
|
|
407
|
+
]);
|
|
408
|
+
function parseTestFile(content, testFilePath, language) {
|
|
409
|
+
const importedSymbols = new Map();
|
|
410
|
+
const referencedNames = new Set();
|
|
411
|
+
// TS/JS: import { A, B } from './path'
|
|
412
|
+
if (language === 'typescript') {
|
|
413
|
+
TS_IMPORT_FROM.lastIndex = 0;
|
|
414
|
+
let match;
|
|
415
|
+
while ((match = TS_IMPORT_FROM.exec(content)) !== null) {
|
|
416
|
+
const symbols = match[1].split(',')
|
|
417
|
+
.map(s => s.trim().split(/\s+as\s+/)[0].trim())
|
|
418
|
+
.filter(s => s && !s.startsWith('type '));
|
|
419
|
+
const importPath = match[2];
|
|
420
|
+
if (importPath.startsWith('.')) {
|
|
421
|
+
const resolved = resolveImportPath(testFilePath, importPath);
|
|
422
|
+
if (resolved) {
|
|
423
|
+
const existing = importedSymbols.get(resolved) ?? new Set();
|
|
424
|
+
for (const s of symbols)
|
|
425
|
+
existing.add(s);
|
|
426
|
+
importedSymbols.set(resolved, existing);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Go: 함수 호출 패턴으로 매핑 (import는 패키지 단위라 심볼 매핑 어려움)
|
|
432
|
+
// Java/C#: import/using 은 클래스 단위
|
|
433
|
+
// Rust: use crate::module::Name
|
|
434
|
+
// 범용: 코드 내 함수 호출 참조 수집
|
|
435
|
+
for (const line of content.split('\n')) {
|
|
436
|
+
const trimmed = line.trim();
|
|
437
|
+
const calls = trimmed.matchAll(/\b([a-zA-Z_]\w+)\s*\(/g);
|
|
438
|
+
for (const c of calls) {
|
|
439
|
+
if (!RESERVED_NAMES.has(c[1]))
|
|
440
|
+
referencedNames.add(c[1]);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return { testFilePath, importedSymbols, referencedNames };
|
|
444
|
+
}
|
|
445
|
+
function resolveImportPath(fromFile, importPath) {
|
|
446
|
+
const dir = dirname(fromFile);
|
|
447
|
+
const cleaned = importPath.replace(/\.[jt]sx?$/, '');
|
|
448
|
+
const resolved = join(dir, cleaned).replace(/\\/g, '/');
|
|
449
|
+
if (resolved.startsWith('..'))
|
|
450
|
+
return null;
|
|
451
|
+
return resolved.replace(/^\.\//, '');
|
|
452
|
+
}
|
|
453
|
+
function buildTestMap(entities, testFiles) {
|
|
454
|
+
const result = new Map();
|
|
455
|
+
const entitiesByFile = new Map();
|
|
456
|
+
const entityByName = new Map();
|
|
457
|
+
for (const e of entities) {
|
|
458
|
+
const list = entitiesByFile.get(e.filePath) ?? [];
|
|
459
|
+
list.push(e);
|
|
460
|
+
entitiesByFile.set(e.filePath, list);
|
|
461
|
+
const nameList = entityByName.get(e.name) ?? [];
|
|
462
|
+
nameList.push(e);
|
|
463
|
+
entityByName.set(e.name, nameList);
|
|
464
|
+
}
|
|
465
|
+
for (const tf of testFiles) {
|
|
466
|
+
// import 기반 매핑 (TS)
|
|
467
|
+
for (const [sourcePath, symbols] of tf.importedSymbols) {
|
|
468
|
+
const candidates = [
|
|
469
|
+
sourcePath + '.ts', sourcePath + '.tsx', sourcePath + '.js',
|
|
470
|
+
sourcePath + '/index.ts', sourcePath,
|
|
471
|
+
];
|
|
472
|
+
for (const candidate of candidates) {
|
|
473
|
+
const fileEntities = entitiesByFile.get(candidate);
|
|
474
|
+
if (!fileEntities)
|
|
475
|
+
continue;
|
|
476
|
+
for (const entity of fileEntities) {
|
|
477
|
+
if (symbols.has(entity.name)) {
|
|
478
|
+
result.set(`${entity.filePath}::${entity.name}`, { hasTests: true, testFile: tf.testFilePath });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// 참조 이름 기반 (범용)
|
|
485
|
+
for (const refName of tf.referencedNames) {
|
|
486
|
+
const matchingEntities = entityByName.get(refName);
|
|
487
|
+
if (!matchingEntities)
|
|
488
|
+
continue;
|
|
489
|
+
for (const entity of matchingEntities) {
|
|
490
|
+
const qName = `${entity.filePath}::${entity.name}`;
|
|
491
|
+
if (result.has(qName))
|
|
492
|
+
continue;
|
|
493
|
+
if (isNearbyTest(entity.filePath, tf.testFilePath)) {
|
|
494
|
+
result.set(qName, { hasTests: true, testFile: tf.testFilePath });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
function isNearbyTest(sourceFile, testFile) {
|
|
502
|
+
const sourceDir = dirname(sourceFile);
|
|
503
|
+
const testDir = dirname(testFile);
|
|
504
|
+
const sourceBase = sourceFile.replace(/\.[^.]+$/, '');
|
|
505
|
+
const testBase = testFile
|
|
506
|
+
.replace(/\.test\.[^.]+$|\.spec\.[^.]+$/, '')
|
|
507
|
+
.replace(/_test\.[^.]+$/, '')
|
|
508
|
+
.replace(/Test\.[^.]+$/, '')
|
|
509
|
+
.replace(/Tests\.[^.]+$/, '');
|
|
510
|
+
// 파일명 매칭 (확장자 제외)
|
|
511
|
+
const srcName = sourceBase.split('/').pop();
|
|
512
|
+
const tstName = testBase.split('/').pop();
|
|
513
|
+
if (srcName && tstName && srcName === tstName)
|
|
514
|
+
return true;
|
|
515
|
+
if (sourceDir === testDir)
|
|
516
|
+
return true;
|
|
517
|
+
if (testDir === `${sourceDir}/__tests__`)
|
|
518
|
+
return true;
|
|
519
|
+
if (testDir === `${sourceDir}/tests`)
|
|
520
|
+
return true;
|
|
521
|
+
if (testDir === `${sourceDir}/test`)
|
|
522
|
+
return true;
|
|
523
|
+
const sourceParent = dirname(sourceDir);
|
|
524
|
+
if (testDir === `${sourceParent}/__tests__` || testDir === 'src/__tests__')
|
|
525
|
+
return true;
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
// ============ 메인 스캔 함수 ============
|
|
529
|
+
export async function scanRepository(projectPath, projectId, options) {
|
|
530
|
+
const startTime = Date.now();
|
|
531
|
+
const maxDepth = options?.maxDepth ?? MAX_DEPTH;
|
|
532
|
+
const timeoutMs = options?.timeoutMs ?? SCAN_TIMEOUT_MS;
|
|
533
|
+
const verbose = options?.verbose ?? false;
|
|
534
|
+
const store = getRegistryStore();
|
|
535
|
+
const allExtracted = [];
|
|
536
|
+
const testFiles = [];
|
|
537
|
+
const errors = [];
|
|
538
|
+
const languageBreakdown = {};
|
|
539
|
+
let scannedFiles = 0;
|
|
540
|
+
async function walk(dirPath, relPath, depth) {
|
|
541
|
+
if (depth > maxDepth)
|
|
542
|
+
return;
|
|
543
|
+
if (Date.now() - startTime > timeoutMs)
|
|
544
|
+
return;
|
|
545
|
+
let entries;
|
|
546
|
+
try {
|
|
547
|
+
entries = await readdir(dirPath, { withFileTypes: true });
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
if (verbose)
|
|
551
|
+
console.log(` [scan] skip dir ${relPath}: ${err instanceof Error ? err.message : 'access denied'}`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
for (const entry of entries) {
|
|
555
|
+
if (Date.now() - startTime > timeoutMs)
|
|
556
|
+
return;
|
|
557
|
+
const fullPath = join(dirPath, entry.name);
|
|
558
|
+
const entryRelPath = relPath ? `${relPath}/${entry.name}` : entry.name;
|
|
559
|
+
if (entry.isDirectory()) {
|
|
560
|
+
if (SKIP_DIRS.has(entry.name) || SKIP_DIR_PREFIXES.some(p => entry.name.startsWith(p)))
|
|
561
|
+
continue;
|
|
562
|
+
await walk(fullPath, entryRelPath, depth + 1);
|
|
563
|
+
}
|
|
564
|
+
else if (entry.isFile()) {
|
|
565
|
+
const ext = extname(entry.name);
|
|
566
|
+
if (!SOURCE_EXTENSIONS.has(ext))
|
|
567
|
+
continue;
|
|
568
|
+
const language = detectLanguage(ext);
|
|
569
|
+
if (!language)
|
|
570
|
+
continue;
|
|
571
|
+
try {
|
|
572
|
+
const fileStat = await stat(fullPath);
|
|
573
|
+
if (fileStat.size > MAX_FILE_SIZE)
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
catch (err) {
|
|
577
|
+
if (verbose)
|
|
578
|
+
console.log(` [scan] skip stat ${entryRelPath}: ${err instanceof Error ? err.message : 'access denied'}`);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
583
|
+
if (isTestFile(entry.name, language)) {
|
|
584
|
+
testFiles.push(parseTestFile(content, entryRelPath, language));
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
const entities = extractEntities(content, entryRelPath, language);
|
|
588
|
+
allExtracted.push(...entities);
|
|
589
|
+
scannedFiles++;
|
|
590
|
+
languageBreakdown[language] = (languageBreakdown[language] ?? 0) + 1;
|
|
591
|
+
if (verbose && entities.length > 0) {
|
|
592
|
+
console.log(` [scan] ${entryRelPath}: ${entities.length} entities (${language})`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
errors.push(`${entryRelPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
await walk(projectPath, '', 0);
|
|
603
|
+
// 테스트 매핑 빌드
|
|
604
|
+
const testMap = buildTestMap(allExtracted, testFiles);
|
|
605
|
+
if (verbose && testMap.size > 0) {
|
|
606
|
+
console.log(` [test-map] ${testMap.size} entities mapped to tests`);
|
|
607
|
+
}
|
|
608
|
+
// 레지스트리 동기화
|
|
609
|
+
const existing = store.listEntities({ projectId, limit: 100_000, offset: 0 });
|
|
610
|
+
const existingByQName = new Map(existing.entities.map(e => [e.qualifiedName, e]));
|
|
611
|
+
const extractedQNames = new Set();
|
|
612
|
+
let registered = 0;
|
|
613
|
+
let updated = 0;
|
|
614
|
+
let testsMapped = 0;
|
|
615
|
+
for (const ext of allExtracted) {
|
|
616
|
+
const qualifiedName = `${ext.filePath}::${ext.name}`;
|
|
617
|
+
extractedQNames.add(qualifiedName);
|
|
618
|
+
const testInfo = testMap.get(qualifiedName);
|
|
619
|
+
const hasTests = testInfo?.hasTests ?? false;
|
|
620
|
+
const testFile = testInfo?.testFile;
|
|
621
|
+
const score = computeComplexityFromMetrics(ext.loc, ext.nestingDepth, ext.paramCount);
|
|
622
|
+
const riskLevel = computeRisk(score, hasTests);
|
|
623
|
+
if (hasTests)
|
|
624
|
+
testsMapped++;
|
|
625
|
+
const existingEntity = existingByQName.get(qualifiedName);
|
|
626
|
+
if (!existingEntity) {
|
|
627
|
+
try {
|
|
628
|
+
store.registerEntity({
|
|
629
|
+
projectId,
|
|
630
|
+
kind: ext.kind,
|
|
631
|
+
name: ext.name,
|
|
632
|
+
filePath: ext.filePath,
|
|
633
|
+
lineStart: ext.lineStart,
|
|
634
|
+
lineEnd: ext.lineEnd,
|
|
635
|
+
signature: ext.signature,
|
|
636
|
+
status: 'active',
|
|
637
|
+
hasTests,
|
|
638
|
+
testFile,
|
|
639
|
+
complexityScore: score,
|
|
640
|
+
riskLevel,
|
|
641
|
+
author: 'scanner',
|
|
642
|
+
});
|
|
643
|
+
registered++;
|
|
644
|
+
}
|
|
645
|
+
catch (err) {
|
|
646
|
+
if (!(err instanceof Error && err.message.includes('UNIQUE'))) {
|
|
647
|
+
errors.push(`register ${qualifiedName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
const needsUpdate = existingEntity.lineStart !== ext.lineStart ||
|
|
653
|
+
existingEntity.lineEnd !== ext.lineEnd ||
|
|
654
|
+
existingEntity.signature !== ext.signature ||
|
|
655
|
+
existingEntity.hasTests !== hasTests ||
|
|
656
|
+
existingEntity.testFile !== testFile ||
|
|
657
|
+
existingEntity.complexityScore !== score ||
|
|
658
|
+
existingEntity.riskLevel !== riskLevel;
|
|
659
|
+
if (needsUpdate) {
|
|
660
|
+
store.updateEntity(existingEntity.id, {
|
|
661
|
+
lineStart: ext.lineStart,
|
|
662
|
+
lineEnd: ext.lineEnd,
|
|
663
|
+
signature: ext.signature,
|
|
664
|
+
hasTests,
|
|
665
|
+
testFile,
|
|
666
|
+
complexityScore: score,
|
|
667
|
+
riskLevel,
|
|
668
|
+
}, 'scanner');
|
|
669
|
+
updated++;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// 사라진 엔티티 → broken
|
|
674
|
+
let removed = 0;
|
|
675
|
+
for (const [qName, entity] of existingByQName) {
|
|
676
|
+
if (!extractedQNames.has(qName) && entity.author === 'scanner' && entity.status === 'active') {
|
|
677
|
+
store.changeEntityStatus(entity.id, 'broken', 'scanner');
|
|
678
|
+
removed++;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
scanned: scannedFiles,
|
|
683
|
+
extracted: allExtracted.length,
|
|
684
|
+
registered,
|
|
685
|
+
updated,
|
|
686
|
+
removed,
|
|
687
|
+
testsMapped,
|
|
688
|
+
errors,
|
|
689
|
+
durationMs: Date.now() - startTime,
|
|
690
|
+
languageBreakdown,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
//# sourceMappingURL=entityScanner.js.map
|