@tyroneross/navgator 0.1.0 → 0.2.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 +15 -9
- package/dist/cli/index.js +249 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/diagram.d.ts.map +1 -1
- package/dist/diagram.js +33 -0
- package/dist/diagram.js.map +1 -1
- package/dist/scanner.d.ts +2 -0
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +131 -12
- package/dist/scanner.js.map +1 -1
- package/dist/scanners/connections/ast-scanner.d.ts.map +1 -1
- package/dist/scanners/connections/ast-scanner.js +0 -3
- package/dist/scanners/connections/ast-scanner.js.map +1 -1
- package/dist/scanners/connections/llm-call-tracer.d.ts +83 -0
- package/dist/scanners/connections/llm-call-tracer.d.ts.map +1 -0
- package/dist/scanners/connections/llm-call-tracer.js +801 -0
- package/dist/scanners/connections/llm-call-tracer.js.map +1 -0
- package/dist/scanners/connections/service-calls.d.ts.map +1 -1
- package/dist/scanners/connections/service-calls.js +44 -47
- package/dist/scanners/connections/service-calls.js.map +1 -1
- package/dist/scanners/infrastructure/index.d.ts.map +1 -1
- package/dist/scanners/infrastructure/index.js +34 -4
- package/dist/scanners/infrastructure/index.js.map +1 -1
- package/dist/scanners/packages/swift.d.ts +14 -0
- package/dist/scanners/packages/swift.d.ts.map +1 -0
- package/dist/scanners/packages/swift.js +320 -0
- package/dist/scanners/packages/swift.js.map +1 -0
- package/dist/scanners/prompts/detector.d.ts +16 -0
- package/dist/scanners/prompts/detector.d.ts.map +1 -1
- package/dist/scanners/prompts/detector.js +90 -1
- package/dist/scanners/prompts/detector.js.map +1 -1
- package/dist/scanners/prompts/types.d.ts +4 -0
- package/dist/scanners/prompts/types.d.ts.map +1 -1
- package/dist/scanners/prompts/types.js.map +1 -1
- package/dist/scanners/swift/code-scanner.d.ts +14 -0
- package/dist/scanners/swift/code-scanner.d.ts.map +1 -0
- package/dist/scanners/swift/code-scanner.js +803 -0
- package/dist/scanners/swift/code-scanner.js.map +1 -0
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +41 -2
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +26 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ui-server.d.ts +7 -7
- package/dist/ui-server.d.ts.map +1 -1
- package/dist/ui-server.js +80 -792
- package/dist/ui-server.js.map +1 -1
- package/package.json +15 -10
- package/web/public/apple-icon.png +0 -0
- package/web/public/icon-dark-32x32.png +0 -0
- package/web/public/icon-light-32x32.png +0 -0
- package/web/public/icon.svg +26 -0
- package/web/public/navgator-logo.png +0 -0
- package/web/public/placeholder-logo.png +0 -0
- package/web/public/placeholder-logo.svg +1 -0
- package/web/public/placeholder-user.jpg +0 -0
- package/web/public/placeholder.jpg +0 -0
- package/web/public/placeholder.svg +1 -0
- package/scripts/ibr-ui-test.mjs +0 -359
- package/scripts/postinstall.cjs +0 -35
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swift Code Scanner
|
|
3
|
+
* Detects runtime connections in .swift files:
|
|
4
|
+
* - String-keyed deps (UserDefaults, @AppStorage, NotificationCenter, asset names)
|
|
5
|
+
* - Protocol conformance
|
|
6
|
+
* - State observation (@Published, @Observable, @EnvironmentObject)
|
|
7
|
+
* - URLSession calls to LLM APIs
|
|
8
|
+
* - Entitlement requirements from framework usage
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { glob } from 'glob';
|
|
13
|
+
import { generateComponentId, generateConnectionId, } from '../../types.js';
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// FRAMEWORK → ENTITLEMENT MAP
|
|
16
|
+
// =============================================================================
|
|
17
|
+
const FRAMEWORK_ENTITLEMENTS = {
|
|
18
|
+
'HealthKit': { entitlement: 'com.apple.developer.healthkit', plistKey: 'NSHealthShareUsageDescription' },
|
|
19
|
+
'CloudKit': { entitlement: 'com.apple.developer.icloud-services' },
|
|
20
|
+
'HomeKit': { entitlement: 'com.apple.developer.homekit', plistKey: 'NSHomeKitUsageDescription' },
|
|
21
|
+
'CoreLocation': { plistKey: 'NSLocationWhenInUseUsageDescription' },
|
|
22
|
+
'AVFoundation': { plistKey: 'NSCameraUsageDescription' },
|
|
23
|
+
'Photos': { plistKey: 'NSPhotoLibraryUsageDescription' },
|
|
24
|
+
'PhotosUI': { plistKey: 'NSPhotoLibraryUsageDescription' },
|
|
25
|
+
'Contacts': { plistKey: 'NSContactsUsageDescription' },
|
|
26
|
+
'EventKit': { plistKey: 'NSCalendarsUsageDescription' },
|
|
27
|
+
'Speech': { plistKey: 'NSSpeechRecognitionUsageDescription' },
|
|
28
|
+
'LocalAuthentication': { plistKey: 'NSFaceIDUsageDescription' },
|
|
29
|
+
'CoreBluetooth': { plistKey: 'NSBluetoothAlwaysUsageDescription' },
|
|
30
|
+
'CoreMotion': { plistKey: 'NSMotionUsageDescription' },
|
|
31
|
+
'UserNotifications': { entitlement: 'com.apple.developer.push-notifications' },
|
|
32
|
+
'StoreKit': { entitlement: 'com.apple.developer.in-app-payments' },
|
|
33
|
+
'MapKit': { plistKey: 'NSLocationWhenInUseUsageDescription' },
|
|
34
|
+
'NearbyInteraction': { plistKey: 'NSNearbyInteractionUsageDescription' },
|
|
35
|
+
};
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// LLM API URL PATTERNS
|
|
38
|
+
// =============================================================================
|
|
39
|
+
const LLM_URL_PATTERNS = [
|
|
40
|
+
{ pattern: /api\.anthropic\.com/, provider: 'Claude (Anthropic)' },
|
|
41
|
+
{ pattern: /api\.openai\.com/, provider: 'OpenAI' },
|
|
42
|
+
{ pattern: /generativelanguage\.googleapis\.com/, provider: 'Gemini (Google)' },
|
|
43
|
+
{ pattern: /api\.groq\.com/, provider: 'Groq' },
|
|
44
|
+
{ pattern: /api\.cohere\.ai/, provider: 'Cohere' },
|
|
45
|
+
{ pattern: /api\.mistral\.ai/, provider: 'Mistral' },
|
|
46
|
+
{ pattern: /api-inference\.huggingface\.co/, provider: 'HuggingFace' },
|
|
47
|
+
{ pattern: /api\.replicate\.com/, provider: 'Replicate' },
|
|
48
|
+
{ pattern: /api\.together\.xyz/, provider: 'Together AI' },
|
|
49
|
+
{ pattern: /api\.fireworks\.ai/, provider: 'Fireworks AI' },
|
|
50
|
+
];
|
|
51
|
+
// Swift SDK import patterns for LLMs
|
|
52
|
+
const LLM_IMPORT_PATTERNS = [
|
|
53
|
+
{ pattern: /^import\s+OpenAI\b/, provider: 'OpenAI' },
|
|
54
|
+
{ pattern: /^import\s+Anthropic\b/, provider: 'Claude (Anthropic)' },
|
|
55
|
+
{ pattern: /^import\s+GoogleGenerativeAI\b/, provider: 'Gemini (Google)' },
|
|
56
|
+
{ pattern: /^import\s+FoundationModels\b/, provider: 'Apple Intelligence' },
|
|
57
|
+
];
|
|
58
|
+
// Swift SDK call patterns
|
|
59
|
+
const LLM_CALL_PATTERNS = [
|
|
60
|
+
{ pattern: /ChatQuery\(/, provider: 'OpenAI' },
|
|
61
|
+
{ pattern: /\.chats\(query:/, provider: 'OpenAI' },
|
|
62
|
+
{ pattern: /\.completions\.create\(/, provider: 'OpenAI' },
|
|
63
|
+
{ pattern: /AnthropicClient\(/, provider: 'Claude (Anthropic)' },
|
|
64
|
+
{ pattern: /\.messages\.create\(/, provider: 'Claude (Anthropic)' },
|
|
65
|
+
{ pattern: /GenerativeModel\(name:/, provider: 'Gemini (Google)' },
|
|
66
|
+
{ pattern: /\.generateContent\(/, provider: 'Gemini (Google)' },
|
|
67
|
+
{ pattern: /LanguageModelSession\(\)/, provider: 'Apple Intelligence' },
|
|
68
|
+
{ pattern: /\.respond\(to:/, provider: 'Apple Intelligence' },
|
|
69
|
+
{ pattern: /@Generable\b/, provider: 'Apple Intelligence' },
|
|
70
|
+
];
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// MAIN SCANNER
|
|
73
|
+
// =============================================================================
|
|
74
|
+
export async function scanSwiftCode(projectRoot) {
|
|
75
|
+
const components = [];
|
|
76
|
+
const connections = [];
|
|
77
|
+
const warnings = [];
|
|
78
|
+
const timestamp = Date.now();
|
|
79
|
+
// Load all Swift files
|
|
80
|
+
const swiftFiles = await glob('**/*.swift', {
|
|
81
|
+
cwd: projectRoot,
|
|
82
|
+
ignore: ['.build/**', 'DerivedData/**', '.swiftpm/**', 'Pods/**', 'Carthage/**', '*.playground/**'],
|
|
83
|
+
});
|
|
84
|
+
const files = [];
|
|
85
|
+
for (const relPath of swiftFiles) {
|
|
86
|
+
try {
|
|
87
|
+
const content = await fs.promises.readFile(path.join(projectRoot, relPath), 'utf-8');
|
|
88
|
+
files.push({ relativePath: relPath, content, lines: content.split('\n') });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// skip unreadable
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (files.length === 0) {
|
|
95
|
+
return { components, connections, warnings, projectMeta: {} };
|
|
96
|
+
}
|
|
97
|
+
// ---- String-keyed runtime deps ----
|
|
98
|
+
const stringKeys = scanStringKeys(files);
|
|
99
|
+
const fragileKeys = buildFragileKeys(stringKeys);
|
|
100
|
+
// Create components + connections for shared keys
|
|
101
|
+
const keyGroups = groupByKey(stringKeys);
|
|
102
|
+
for (const [groupKey, hits] of keyGroups) {
|
|
103
|
+
if (hits.length < 1)
|
|
104
|
+
continue;
|
|
105
|
+
const keyType = hits[0].type;
|
|
106
|
+
const rawKey = hits[0].key;
|
|
107
|
+
const compId = generateComponentId('other', groupKey);
|
|
108
|
+
components.push({
|
|
109
|
+
component_id: compId,
|
|
110
|
+
name: groupKey,
|
|
111
|
+
type: 'other',
|
|
112
|
+
role: { purpose: `${keyType} key "${rawKey}"`, layer: 'backend', critical: hits.length > 1 },
|
|
113
|
+
source: { detection_method: 'auto', config_files: [], confidence: 0.95 },
|
|
114
|
+
connects_to: [],
|
|
115
|
+
connected_from: [],
|
|
116
|
+
status: 'active',
|
|
117
|
+
tags: ['swift', 'string-key', keyType.toLowerCase(), hits.length > 1 ? 'shared' : 'single'],
|
|
118
|
+
metadata: { keyType, key: rawKey, fileCount: hits.length, files: [...new Set(hits.map(h => h.file))] },
|
|
119
|
+
timestamp,
|
|
120
|
+
last_updated: timestamp,
|
|
121
|
+
});
|
|
122
|
+
for (const hit of hits) {
|
|
123
|
+
connections.push({
|
|
124
|
+
connection_id: generateConnectionId('stores'),
|
|
125
|
+
from: { component_id: compId, location: { file: hit.file, line: hit.line } },
|
|
126
|
+
to: { component_id: compId },
|
|
127
|
+
connection_type: 'stores',
|
|
128
|
+
code_reference: {
|
|
129
|
+
file: hit.file,
|
|
130
|
+
symbol: hit.symbol,
|
|
131
|
+
symbol_type: 'variable',
|
|
132
|
+
line_start: hit.line,
|
|
133
|
+
code_snippet: hit.snippet.slice(0, 100),
|
|
134
|
+
},
|
|
135
|
+
description: `${hit.type} key "${rawKey}" in ${hit.file}`,
|
|
136
|
+
detected_from: 'swift-code-scanner',
|
|
137
|
+
confidence: 0.95,
|
|
138
|
+
timestamp,
|
|
139
|
+
last_verified: timestamp,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ---- Protocol conformance ----
|
|
144
|
+
const conformances = scanProtocolConformance(files);
|
|
145
|
+
const protocolMap = new Map();
|
|
146
|
+
for (const c of conformances) {
|
|
147
|
+
for (const proto of c.protocols) {
|
|
148
|
+
if (!protocolMap.has(proto))
|
|
149
|
+
protocolMap.set(proto, []);
|
|
150
|
+
protocolMap.get(proto).push(c);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const [proto, conformers] of protocolMap) {
|
|
154
|
+
if (conformers.length < 1)
|
|
155
|
+
continue;
|
|
156
|
+
const compId = generateComponentId('other', `protocol:${proto}`);
|
|
157
|
+
components.push({
|
|
158
|
+
component_id: compId,
|
|
159
|
+
name: `protocol:${proto}`,
|
|
160
|
+
type: 'other',
|
|
161
|
+
role: { purpose: `Protocol ${proto} (${conformers.length} conformer${conformers.length > 1 ? 's' : ''})`, layer: 'backend', critical: conformers.length > 2 },
|
|
162
|
+
source: { detection_method: 'auto', config_files: [], confidence: 0.85 },
|
|
163
|
+
connects_to: [],
|
|
164
|
+
connected_from: [],
|
|
165
|
+
status: 'active',
|
|
166
|
+
tags: ['swift', 'protocol', conformers.length > 2 ? 'widely-used' : 'local'],
|
|
167
|
+
metadata: { conformers: conformers.map(c => ({ type: c.typeName, file: c.file, line: c.line })) },
|
|
168
|
+
timestamp,
|
|
169
|
+
last_updated: timestamp,
|
|
170
|
+
});
|
|
171
|
+
for (const conf of conformers) {
|
|
172
|
+
connections.push({
|
|
173
|
+
connection_id: generateConnectionId('conforms-to'),
|
|
174
|
+
from: { component_id: generateComponentId('other', conf.typeName), location: { file: conf.file, line: conf.line } },
|
|
175
|
+
to: { component_id: compId },
|
|
176
|
+
connection_type: 'conforms-to',
|
|
177
|
+
code_reference: {
|
|
178
|
+
file: conf.file,
|
|
179
|
+
symbol: conf.typeName,
|
|
180
|
+
symbol_type: 'class',
|
|
181
|
+
line_start: conf.line,
|
|
182
|
+
code_snippet: `${conf.typeName}: ${conf.protocols.join(', ')}`,
|
|
183
|
+
},
|
|
184
|
+
description: `${conf.typeName} conforms to ${proto}`,
|
|
185
|
+
detected_from: 'swift-code-scanner',
|
|
186
|
+
confidence: 0.85,
|
|
187
|
+
timestamp,
|
|
188
|
+
last_verified: timestamp,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// ---- State observation (@Published, @EnvironmentObject, etc.) ----
|
|
193
|
+
const observations = scanStateObservation(files);
|
|
194
|
+
// Group @Published → find consumers via @ObservedObject/@EnvironmentObject/@StateObject
|
|
195
|
+
const publishers = observations.filter(o => o.wrapper === '@Published');
|
|
196
|
+
const consumers = observations.filter(o => ['@ObservedObject', '@EnvironmentObject', '@StateObject'].includes(o.wrapper));
|
|
197
|
+
for (const pub of publishers) {
|
|
198
|
+
for (const con of consumers) {
|
|
199
|
+
// Match if the consumer type matches the publisher's owner type
|
|
200
|
+
if (con.ownerType !== pub.ownerType)
|
|
201
|
+
continue;
|
|
202
|
+
connections.push({
|
|
203
|
+
connection_id: generateConnectionId('observes'),
|
|
204
|
+
from: { component_id: generateComponentId('component', con.ownerType), location: { file: con.file, line: con.line } },
|
|
205
|
+
to: { component_id: generateComponentId('component', pub.ownerType), location: { file: pub.file, line: pub.line } },
|
|
206
|
+
connection_type: 'observes',
|
|
207
|
+
code_reference: {
|
|
208
|
+
file: con.file,
|
|
209
|
+
symbol: con.propertyName,
|
|
210
|
+
symbol_type: 'variable',
|
|
211
|
+
line_start: con.line,
|
|
212
|
+
code_snippet: `${con.wrapper} var ${con.propertyName}: ${con.ownerType}`,
|
|
213
|
+
},
|
|
214
|
+
description: `${con.file} observes ${pub.ownerType}.${pub.propertyName}`,
|
|
215
|
+
detected_from: 'swift-code-scanner',
|
|
216
|
+
confidence: 0.8,
|
|
217
|
+
timestamp,
|
|
218
|
+
last_verified: timestamp,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ---- LLM API calls ----
|
|
223
|
+
const llmCalls = scanLLMCalls(files);
|
|
224
|
+
for (const call of llmCalls) {
|
|
225
|
+
const compId = generateComponentId('llm', call.provider);
|
|
226
|
+
// Only add component if not already present
|
|
227
|
+
if (!components.find(c => c.name === call.provider && c.type === 'llm')) {
|
|
228
|
+
components.push({
|
|
229
|
+
component_id: compId,
|
|
230
|
+
name: call.provider,
|
|
231
|
+
type: 'llm',
|
|
232
|
+
role: { purpose: `${call.provider} LLM API`, layer: 'external', critical: true },
|
|
233
|
+
source: { detection_method: 'auto', config_files: [], confidence: 0.9 },
|
|
234
|
+
connects_to: [],
|
|
235
|
+
connected_from: [],
|
|
236
|
+
status: 'active',
|
|
237
|
+
tags: ['swift', 'llm', 'external'],
|
|
238
|
+
timestamp,
|
|
239
|
+
last_updated: timestamp,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
connections.push({
|
|
243
|
+
connection_id: generateConnectionId('service-call'),
|
|
244
|
+
from: { component_id: generateComponentId('other', call.file), location: { file: call.file, line: call.line } },
|
|
245
|
+
to: { component_id: compId },
|
|
246
|
+
connection_type: 'service-call',
|
|
247
|
+
code_reference: {
|
|
248
|
+
file: call.file,
|
|
249
|
+
symbol: call.symbol,
|
|
250
|
+
symbol_type: 'function',
|
|
251
|
+
line_start: call.line,
|
|
252
|
+
code_snippet: call.snippet.slice(0, 100),
|
|
253
|
+
},
|
|
254
|
+
description: `${call.provider} API call in ${call.file}`,
|
|
255
|
+
detected_from: 'swift-code-scanner',
|
|
256
|
+
confidence: 0.9,
|
|
257
|
+
timestamp,
|
|
258
|
+
last_verified: timestamp,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
// ---- Entitlement requirements ----
|
|
262
|
+
const frameworkImports = scanFrameworkImports(files);
|
|
263
|
+
const entitlementReqs = [];
|
|
264
|
+
for (const imp of frameworkImports) {
|
|
265
|
+
const req = FRAMEWORK_ENTITLEMENTS[imp.framework];
|
|
266
|
+
if (req) {
|
|
267
|
+
if (req.entitlement) {
|
|
268
|
+
entitlementReqs.push({ key: req.entitlement, framework: imp.framework, file: imp.file, line: imp.line });
|
|
269
|
+
}
|
|
270
|
+
if (req.plistKey) {
|
|
271
|
+
entitlementReqs.push({ key: req.plistKey, framework: imp.framework, file: imp.file, line: imp.line });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const req of entitlementReqs) {
|
|
276
|
+
connections.push({
|
|
277
|
+
connection_id: generateConnectionId('requires-entitlement'),
|
|
278
|
+
from: { component_id: generateComponentId('framework', req.framework), location: { file: req.file, line: req.line } },
|
|
279
|
+
to: { component_id: generateComponentId('other', `entitlement:${req.key}`) },
|
|
280
|
+
connection_type: 'requires-entitlement',
|
|
281
|
+
code_reference: {
|
|
282
|
+
file: req.file,
|
|
283
|
+
symbol: `import ${req.framework}`,
|
|
284
|
+
symbol_type: 'import',
|
|
285
|
+
line_start: req.line,
|
|
286
|
+
code_snippet: `import ${req.framework} → requires ${req.key}`,
|
|
287
|
+
},
|
|
288
|
+
description: `${req.framework} requires entitlement/plist key: ${req.key}`,
|
|
289
|
+
detected_from: 'swift-code-scanner',
|
|
290
|
+
confidence: 0.85,
|
|
291
|
+
timestamp,
|
|
292
|
+
last_verified: timestamp,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// ---- Prompt patterns in Swift ----
|
|
296
|
+
const prompts = scanSwiftPrompts(files);
|
|
297
|
+
for (const prompt of prompts) {
|
|
298
|
+
const compId = generateComponentId('prompt', prompt.name);
|
|
299
|
+
components.push({
|
|
300
|
+
component_id: compId,
|
|
301
|
+
name: prompt.name,
|
|
302
|
+
type: 'prompt',
|
|
303
|
+
role: { purpose: `AI prompt: ${prompt.name}`, layer: 'backend', critical: false },
|
|
304
|
+
source: { detection_method: 'auto', config_files: [], confidence: prompt.confidence },
|
|
305
|
+
connects_to: [],
|
|
306
|
+
connected_from: [],
|
|
307
|
+
status: 'active',
|
|
308
|
+
tags: ['swift', 'prompt'],
|
|
309
|
+
metadata: { preview: prompt.preview },
|
|
310
|
+
timestamp,
|
|
311
|
+
last_updated: timestamp,
|
|
312
|
+
});
|
|
313
|
+
connections.push({
|
|
314
|
+
connection_id: generateConnectionId('prompt-location'),
|
|
315
|
+
from: { component_id: compId, location: { file: prompt.file, line: prompt.line } },
|
|
316
|
+
to: { component_id: compId },
|
|
317
|
+
connection_type: 'prompt-location',
|
|
318
|
+
code_reference: {
|
|
319
|
+
file: prompt.file,
|
|
320
|
+
symbol: prompt.name,
|
|
321
|
+
symbol_type: 'variable',
|
|
322
|
+
line_start: prompt.line,
|
|
323
|
+
code_snippet: prompt.preview.slice(0, 100),
|
|
324
|
+
},
|
|
325
|
+
description: `Prompt "${prompt.name}" defined in ${prompt.file}`,
|
|
326
|
+
detected_from: 'swift-code-scanner',
|
|
327
|
+
confidence: prompt.confidence,
|
|
328
|
+
timestamp,
|
|
329
|
+
last_verified: timestamp,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
// ---- Build project metadata ----
|
|
333
|
+
const projectMeta = buildProjectMetadata(files, frameworkImports, projectRoot, fragileKeys, entitlementReqs);
|
|
334
|
+
return { components, connections, warnings, projectMeta };
|
|
335
|
+
}
|
|
336
|
+
// =============================================================================
|
|
337
|
+
// STRING KEY DETECTION
|
|
338
|
+
// =============================================================================
|
|
339
|
+
function scanStringKeys(files) {
|
|
340
|
+
const hits = [];
|
|
341
|
+
for (const file of files) {
|
|
342
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
343
|
+
const line = file.lines[i];
|
|
344
|
+
const trimmed = line.trim();
|
|
345
|
+
// Skip comments
|
|
346
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*'))
|
|
347
|
+
continue;
|
|
348
|
+
// @AppStorage("key") — normalizes to UserDefaults since @AppStorage is a UserDefaults wrapper
|
|
349
|
+
const appStorageMatch = line.match(/@AppStorage\(["']([^"']+)["']\)/);
|
|
350
|
+
if (appStorageMatch) {
|
|
351
|
+
hits.push({
|
|
352
|
+
key: appStorageMatch[1],
|
|
353
|
+
type: 'UserDefaults',
|
|
354
|
+
file: file.relativePath,
|
|
355
|
+
line: i + 1,
|
|
356
|
+
symbol: extractNearestSymbol(file.lines, i) || appStorageMatch[1],
|
|
357
|
+
snippet: trimmed,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
// UserDefaults.standard.set(..., forKey: "key") / .object(forKey: "key") / etc.
|
|
361
|
+
const udWriteMatch = line.match(/UserDefaults\.(?:standard|[a-zA-Z]+)\.(?:set|setValue|removeObject)\([^)]*forKey:\s*["']([^"']+)["']\)/);
|
|
362
|
+
if (udWriteMatch) {
|
|
363
|
+
hits.push({
|
|
364
|
+
key: udWriteMatch[1],
|
|
365
|
+
type: 'UserDefaults',
|
|
366
|
+
file: file.relativePath,
|
|
367
|
+
line: i + 1,
|
|
368
|
+
symbol: extractNearestSymbol(file.lines, i) || udWriteMatch[1],
|
|
369
|
+
snippet: trimmed,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
const udReadMatch = line.match(/UserDefaults\.(?:standard|[a-zA-Z]+)\.(?:object|string|integer|bool|double|float|array|dictionary|data|url)\(forKey:\s*["']([^"']+)["']\)/);
|
|
373
|
+
if (udReadMatch) {
|
|
374
|
+
hits.push({
|
|
375
|
+
key: udReadMatch[1],
|
|
376
|
+
type: 'UserDefaults',
|
|
377
|
+
file: file.relativePath,
|
|
378
|
+
line: i + 1,
|
|
379
|
+
symbol: extractNearestSymbol(file.lines, i) || udReadMatch[1],
|
|
380
|
+
snippet: trimmed,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
// NotificationCenter — .post(name: .someNotification) or Notification.Name("string")
|
|
384
|
+
const notifPostMatch = line.match(/\.post\(name:\s*(?:\.(\w+)|Notification\.Name\(["']([^"']+)["']\))/);
|
|
385
|
+
if (notifPostMatch) {
|
|
386
|
+
const key = notifPostMatch[1] || notifPostMatch[2];
|
|
387
|
+
hits.push({
|
|
388
|
+
key,
|
|
389
|
+
type: 'NotificationCenter',
|
|
390
|
+
file: file.relativePath,
|
|
391
|
+
line: i + 1,
|
|
392
|
+
symbol: extractNearestSymbol(file.lines, i) || key,
|
|
393
|
+
snippet: trimmed,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
const notifObserveMatch = line.match(/\.addObserver\([^)]*name:\s*(?:\.(\w+)|Notification\.Name\(["']([^"']+)["']\))/);
|
|
397
|
+
if (notifObserveMatch) {
|
|
398
|
+
const key = notifObserveMatch[1] || notifObserveMatch[2];
|
|
399
|
+
hits.push({
|
|
400
|
+
key,
|
|
401
|
+
type: 'NotificationCenter',
|
|
402
|
+
file: file.relativePath,
|
|
403
|
+
line: i + 1,
|
|
404
|
+
symbol: extractNearestSymbol(file.lines, i) || key,
|
|
405
|
+
snippet: trimmed,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
// Image("name") or Image(systemName: "name") — asset names
|
|
409
|
+
const imageMatch = line.match(/Image\(\s*["']([^"']+)["']\s*\)/);
|
|
410
|
+
if (imageMatch) {
|
|
411
|
+
hits.push({
|
|
412
|
+
key: imageMatch[1],
|
|
413
|
+
type: 'AssetName',
|
|
414
|
+
file: file.relativePath,
|
|
415
|
+
line: i + 1,
|
|
416
|
+
symbol: extractNearestSymbol(file.lines, i) || imageMatch[1],
|
|
417
|
+
snippet: trimmed,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
// UIImage(named: "name")
|
|
421
|
+
const uiImageMatch = line.match(/UIImage\(named:\s*["']([^"']+)["']\)/);
|
|
422
|
+
if (uiImageMatch) {
|
|
423
|
+
hits.push({
|
|
424
|
+
key: uiImageMatch[1],
|
|
425
|
+
type: 'AssetName',
|
|
426
|
+
file: file.relativePath,
|
|
427
|
+
line: i + 1,
|
|
428
|
+
symbol: extractNearestSymbol(file.lines, i) || uiImageMatch[1],
|
|
429
|
+
snippet: trimmed,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
// Color("name")
|
|
433
|
+
const colorMatch = line.match(/Color\(\s*["']([^"']+)["']\s*\)/);
|
|
434
|
+
if (colorMatch) {
|
|
435
|
+
hits.push({
|
|
436
|
+
key: colorMatch[1],
|
|
437
|
+
type: 'AssetName',
|
|
438
|
+
file: file.relativePath,
|
|
439
|
+
line: i + 1,
|
|
440
|
+
symbol: extractNearestSymbol(file.lines, i) || colorMatch[1],
|
|
441
|
+
snippet: trimmed,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
// NSSound(named: .init("name"))
|
|
445
|
+
const nsSoundMatch = line.match(/NSSound\(named:\s*\.init\(["']([^"']+)["']\)\)/);
|
|
446
|
+
if (nsSoundMatch) {
|
|
447
|
+
hits.push({
|
|
448
|
+
key: nsSoundMatch[1],
|
|
449
|
+
type: 'AssetName',
|
|
450
|
+
file: file.relativePath,
|
|
451
|
+
line: i + 1,
|
|
452
|
+
symbol: extractNearestSymbol(file.lines, i) || nsSoundMatch[1],
|
|
453
|
+
snippet: trimmed,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
// Keychain — kSecAttrService or KeychainAccess
|
|
457
|
+
const keychainMatch = line.match(/(?:kSecAttrService|kSecAttrAccount|Keychain\(service:)\s*(?::|as\s+.+?,\s*)?["']([^"']+)["']/);
|
|
458
|
+
if (keychainMatch) {
|
|
459
|
+
hits.push({
|
|
460
|
+
key: keychainMatch[1],
|
|
461
|
+
type: 'Keychain',
|
|
462
|
+
file: file.relativePath,
|
|
463
|
+
line: i + 1,
|
|
464
|
+
symbol: extractNearestSymbol(file.lines, i) || keychainMatch[1],
|
|
465
|
+
snippet: trimmed,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return hits;
|
|
471
|
+
}
|
|
472
|
+
// =============================================================================
|
|
473
|
+
// PROTOCOL CONFORMANCE
|
|
474
|
+
// =============================================================================
|
|
475
|
+
function scanProtocolConformance(files) {
|
|
476
|
+
const conformances = [];
|
|
477
|
+
// Well-known protocols worth tracking
|
|
478
|
+
const interestingProtocols = new Set([
|
|
479
|
+
'ObservableObject', 'Observable', 'Codable', 'Decodable', 'Encodable',
|
|
480
|
+
'Identifiable', 'Hashable', 'Equatable', 'Comparable',
|
|
481
|
+
'View', 'App', 'Scene', 'Widget', 'TimelineProvider',
|
|
482
|
+
'Sendable', 'Actor',
|
|
483
|
+
'URLSessionDelegate', 'URLSessionDataDelegate',
|
|
484
|
+
]);
|
|
485
|
+
for (const file of files) {
|
|
486
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
487
|
+
const line = file.lines[i];
|
|
488
|
+
// Match: struct/class/enum/actor TypeName: Protocol1, Protocol2 {
|
|
489
|
+
const match = line.match(/^\s*(?:(?:public|private|internal|open|final|@\w+)\s+)*(?:struct|class|enum|actor)\s+(\w+)\s*(?:<[^>]*>)?\s*:\s*([^{]+)/);
|
|
490
|
+
if (match) {
|
|
491
|
+
const typeName = match[1];
|
|
492
|
+
const rawProtocols = match[2].split(',').map(p => p.trim()).filter(Boolean);
|
|
493
|
+
const protocols = rawProtocols.filter(p => {
|
|
494
|
+
// Filter out generic superclasses (rough heuristic: known protocols or capitalized single words)
|
|
495
|
+
return interestingProtocols.has(p) || /^[A-Z]\w+(?:Protocol|Delegate|DataSource|able)$/.test(p);
|
|
496
|
+
});
|
|
497
|
+
if (protocols.length > 0) {
|
|
498
|
+
conformances.push({ typeName, protocols, file: file.relativePath, line: i + 1 });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return conformances;
|
|
504
|
+
}
|
|
505
|
+
// =============================================================================
|
|
506
|
+
// STATE OBSERVATION
|
|
507
|
+
// =============================================================================
|
|
508
|
+
function scanStateObservation(files) {
|
|
509
|
+
const observations = [];
|
|
510
|
+
const wrappers = ['@Published', '@ObservedObject', '@EnvironmentObject', '@StateObject', '@State', '@Binding'];
|
|
511
|
+
for (const file of files) {
|
|
512
|
+
let currentType = '';
|
|
513
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
514
|
+
const line = file.lines[i];
|
|
515
|
+
// Track current type context
|
|
516
|
+
const typeMatch = line.match(/^\s*(?:(?:public|private|internal|open|final|@\w+)\s+)*(?:struct|class|enum|actor)\s+(\w+)/);
|
|
517
|
+
if (typeMatch) {
|
|
518
|
+
currentType = typeMatch[1];
|
|
519
|
+
}
|
|
520
|
+
for (const wrapper of wrappers) {
|
|
521
|
+
if (!line.includes(wrapper))
|
|
522
|
+
continue;
|
|
523
|
+
// Match: @Published var name: Type
|
|
524
|
+
const propMatch = line.match(new RegExp(`${wrapper.replace('$', '\\$')}\\s+(?:private\\s+|private\\(set\\)\\s+)?var\\s+(\\w+)\\s*(?::\\s*(\\w+))?`));
|
|
525
|
+
if (propMatch) {
|
|
526
|
+
const propName = propMatch[1];
|
|
527
|
+
const propType = propMatch[2] || '';
|
|
528
|
+
// For @Published, the ownerType is the containing type's name (what gets observed)
|
|
529
|
+
// For @ObservedObject/@EnvironmentObject, the propType IS the observed type
|
|
530
|
+
const ownerType = wrapper === '@Published' ? currentType : propType;
|
|
531
|
+
observations.push({
|
|
532
|
+
propertyName: propName,
|
|
533
|
+
wrapper,
|
|
534
|
+
ownerType,
|
|
535
|
+
file: file.relativePath,
|
|
536
|
+
line: i + 1,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return observations;
|
|
543
|
+
}
|
|
544
|
+
// =============================================================================
|
|
545
|
+
// LLM CALL DETECTION
|
|
546
|
+
// =============================================================================
|
|
547
|
+
function scanLLMCalls(files) {
|
|
548
|
+
const calls = [];
|
|
549
|
+
for (const file of files) {
|
|
550
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
551
|
+
const line = file.lines[i];
|
|
552
|
+
const trimmed = line.trim();
|
|
553
|
+
// Skip comments
|
|
554
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
555
|
+
continue;
|
|
556
|
+
// Check URL patterns in string literals
|
|
557
|
+
for (const { pattern, provider } of LLM_URL_PATTERNS) {
|
|
558
|
+
if (pattern.test(line)) {
|
|
559
|
+
calls.push({
|
|
560
|
+
provider,
|
|
561
|
+
url: line.match(/"([^"]*)"/)?.[1],
|
|
562
|
+
file: file.relativePath,
|
|
563
|
+
line: i + 1,
|
|
564
|
+
symbol: extractNearestSymbol(file.lines, i) || 'urlRequest',
|
|
565
|
+
snippet: trimmed,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Check SDK import patterns
|
|
570
|
+
for (const { pattern, provider } of LLM_IMPORT_PATTERNS) {
|
|
571
|
+
if (pattern.test(trimmed)) {
|
|
572
|
+
calls.push({
|
|
573
|
+
provider,
|
|
574
|
+
file: file.relativePath,
|
|
575
|
+
line: i + 1,
|
|
576
|
+
symbol: `import ${provider}`,
|
|
577
|
+
snippet: trimmed,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// Check SDK call patterns
|
|
582
|
+
for (const { pattern, provider } of LLM_CALL_PATTERNS) {
|
|
583
|
+
if (pattern.test(line)) {
|
|
584
|
+
calls.push({
|
|
585
|
+
provider,
|
|
586
|
+
file: file.relativePath,
|
|
587
|
+
line: i + 1,
|
|
588
|
+
symbol: extractNearestSymbol(file.lines, i) || provider,
|
|
589
|
+
snippet: trimmed,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
// Deduplicate by file+provider (keep first occurrence)
|
|
596
|
+
const seen = new Set();
|
|
597
|
+
return calls.filter(c => {
|
|
598
|
+
const key = `${c.file}:${c.provider}`;
|
|
599
|
+
if (seen.has(key))
|
|
600
|
+
return false;
|
|
601
|
+
seen.add(key);
|
|
602
|
+
return true;
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
// =============================================================================
|
|
606
|
+
// FRAMEWORK IMPORT SCANNING (for entitlement detection)
|
|
607
|
+
// =============================================================================
|
|
608
|
+
function scanFrameworkImports(files) {
|
|
609
|
+
const results = [];
|
|
610
|
+
const entitlementFrameworks = new Set(Object.keys(FRAMEWORK_ENTITLEMENTS));
|
|
611
|
+
for (const file of files) {
|
|
612
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
613
|
+
const match = file.lines[i].match(/^\s*import\s+(\w+)/);
|
|
614
|
+
if (match && entitlementFrameworks.has(match[1])) {
|
|
615
|
+
results.push({ framework: match[1], file: file.relativePath, line: i + 1 });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// Deduplicate by framework (keep first)
|
|
620
|
+
const seen = new Set();
|
|
621
|
+
return results.filter(r => {
|
|
622
|
+
if (seen.has(r.framework))
|
|
623
|
+
return false;
|
|
624
|
+
seen.add(r.framework);
|
|
625
|
+
return true;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
// =============================================================================
|
|
629
|
+
// PROMPT DETECTION IN SWIFT
|
|
630
|
+
// =============================================================================
|
|
631
|
+
function scanSwiftPrompts(files) {
|
|
632
|
+
const prompts = [];
|
|
633
|
+
for (const file of files) {
|
|
634
|
+
for (let i = 0; i < file.lines.length; i++) {
|
|
635
|
+
const line = file.lines[i];
|
|
636
|
+
// Match: static let systemPrompt = """ ... """
|
|
637
|
+
// Match: let prompt = "You are a..."
|
|
638
|
+
// Match: var SYSTEM_PROMPT = """
|
|
639
|
+
const promptVarMatch = line.match(/(?:static\s+)?(?:let|var)\s+(\w*(?:[Pp]rompt|[Ss]ystem|[Ii]nstruction)\w*)\s*(?::\s*String)?\s*=\s*(".*"|""")/);
|
|
640
|
+
if (promptVarMatch) {
|
|
641
|
+
const name = promptVarMatch[1];
|
|
642
|
+
let preview = promptVarMatch[2];
|
|
643
|
+
// For multi-line strings, grab more lines
|
|
644
|
+
if (preview === '"""') {
|
|
645
|
+
const nextLines = [];
|
|
646
|
+
for (let j = i + 1; j < Math.min(i + 10, file.lines.length); j++) {
|
|
647
|
+
if (file.lines[j].includes('"""'))
|
|
648
|
+
break;
|
|
649
|
+
nextLines.push(file.lines[j].trim());
|
|
650
|
+
}
|
|
651
|
+
preview = nextLines.join(' ').slice(0, 200);
|
|
652
|
+
}
|
|
653
|
+
prompts.push({
|
|
654
|
+
name,
|
|
655
|
+
file: file.relativePath,
|
|
656
|
+
line: i + 1,
|
|
657
|
+
preview: preview.replace(/^"|"$/g, '').slice(0, 200),
|
|
658
|
+
confidence: 0.85,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
// Match: "role": "system" or role: .system in messages arrays
|
|
662
|
+
if (/role.*system/i.test(line) && /content|message/i.test(file.lines[Math.min(i + 1, file.lines.length - 1)] || '')) {
|
|
663
|
+
const name = extractNearestSymbol(file.lines, i) || `prompt_${file.relativePath}:${i + 1}`;
|
|
664
|
+
prompts.push({
|
|
665
|
+
name,
|
|
666
|
+
file: file.relativePath,
|
|
667
|
+
line: i + 1,
|
|
668
|
+
preview: line.trim().slice(0, 200),
|
|
669
|
+
confidence: 0.7,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return prompts;
|
|
675
|
+
}
|
|
676
|
+
// =============================================================================
|
|
677
|
+
// PROJECT METADATA BUILDER
|
|
678
|
+
// =============================================================================
|
|
679
|
+
function buildProjectMetadata(files, frameworkImports, projectRoot, fragileKeys, entitlementReqs) {
|
|
680
|
+
const meta = { type: 'swift-app' };
|
|
681
|
+
// Detect platforms from framework usage
|
|
682
|
+
const platforms = new Set();
|
|
683
|
+
const allImports = new Set();
|
|
684
|
+
for (const file of files) {
|
|
685
|
+
for (const line of file.lines) {
|
|
686
|
+
const m = line.match(/^\s*import\s+(\w+)/);
|
|
687
|
+
if (m)
|
|
688
|
+
allImports.add(m[1]);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (allImports.has('UIKit') || allImports.has('SwiftUI'))
|
|
692
|
+
platforms.add('iOS');
|
|
693
|
+
if (allImports.has('AppKit'))
|
|
694
|
+
platforms.add('macOS');
|
|
695
|
+
if (allImports.has('WatchKit'))
|
|
696
|
+
platforms.add('watchOS');
|
|
697
|
+
if (allImports.has('WidgetKit'))
|
|
698
|
+
platforms.add('iOS'); // Widgets are iOS typically
|
|
699
|
+
// SwiftUI can be any platform — check for platform-specific APIs
|
|
700
|
+
if (allImports.has('SwiftUI') && !allImports.has('UIKit') && !allImports.has('AppKit')) {
|
|
701
|
+
platforms.add('iOS'); // Default assumption for SwiftUI-only
|
|
702
|
+
platforms.add('macOS');
|
|
703
|
+
}
|
|
704
|
+
meta.platforms = [...platforms];
|
|
705
|
+
// Detect architecture pattern
|
|
706
|
+
if (allImports.has('ComposableArchitecture')) {
|
|
707
|
+
meta.architecture_pattern = 'TCA (Composable Architecture)';
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
// Check for MVVM indicators: ObservableObject/Observable classes separate from Views
|
|
711
|
+
const hasObservableObjects = files.some(f => f.content.includes('ObservableObject') || f.content.includes('@Observable'));
|
|
712
|
+
const hasViews = files.some(f => f.content.includes(': View'));
|
|
713
|
+
if (hasObservableObjects && hasViews) {
|
|
714
|
+
meta.architecture_pattern = 'MVVM';
|
|
715
|
+
}
|
|
716
|
+
else if (allImports.has('UIKit') && files.some(f => f.content.includes(': UIViewController'))) {
|
|
717
|
+
meta.architecture_pattern = 'MVC';
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
// Detect deployment target from Package.swift
|
|
721
|
+
try {
|
|
722
|
+
const pkgSwiftPath = path.join(projectRoot, 'Package.swift');
|
|
723
|
+
if (fs.existsSync(pkgSwiftPath)) {
|
|
724
|
+
const pkgContent = fs.readFileSync(pkgSwiftPath, 'utf-8');
|
|
725
|
+
const deployments = {};
|
|
726
|
+
const platformMatches = pkgContent.matchAll(/\.(iOS|macOS|watchOS|tvOS|visionOS)\("([^"]+)"\)/g);
|
|
727
|
+
for (const m of platformMatches) {
|
|
728
|
+
deployments[m[1]] = m[2];
|
|
729
|
+
}
|
|
730
|
+
const platformMatchesV2 = pkgContent.matchAll(/\.(iOS|macOS|watchOS|tvOS|visionOS)\(\.v(\d+)/g);
|
|
731
|
+
for (const m of platformMatchesV2) {
|
|
732
|
+
deployments[m[1]] = `${m[2]}.0`;
|
|
733
|
+
}
|
|
734
|
+
if (Object.keys(deployments).length > 0) {
|
|
735
|
+
meta.min_deployment = deployments;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
// Skip if unreadable
|
|
741
|
+
}
|
|
742
|
+
// Parse targets from Package.swift
|
|
743
|
+
try {
|
|
744
|
+
const pkgSwiftPath = path.join(projectRoot, 'Package.swift');
|
|
745
|
+
if (fs.existsSync(pkgSwiftPath)) {
|
|
746
|
+
const pkgContent = fs.readFileSync(pkgSwiftPath, 'utf-8');
|
|
747
|
+
const targets = [];
|
|
748
|
+
const targetMatches = pkgContent.matchAll(/\.(?:executableTarget|target|testTarget)\(\s*name:\s*"([^"]+)"/g);
|
|
749
|
+
for (const m of targetMatches) {
|
|
750
|
+
const targetType = m[0].includes('testTarget') ? 'test' : m[0].includes('executableTarget') ? 'executable' : 'library';
|
|
751
|
+
targets.push({ name: m[1], type: targetType, dependencies: [] });
|
|
752
|
+
}
|
|
753
|
+
if (targets.length > 0) {
|
|
754
|
+
meta.targets = targets;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
// Skip
|
|
760
|
+
}
|
|
761
|
+
// Entitlements
|
|
762
|
+
if (entitlementReqs.length > 0) {
|
|
763
|
+
meta.entitlements = entitlementReqs.map(r => ({ key: r.key, file: r.framework }));
|
|
764
|
+
}
|
|
765
|
+
// Fragile keys
|
|
766
|
+
meta.fragile_keys = fragileKeys;
|
|
767
|
+
return meta;
|
|
768
|
+
}
|
|
769
|
+
// =============================================================================
|
|
770
|
+
// HELPERS
|
|
771
|
+
// =============================================================================
|
|
772
|
+
function extractNearestSymbol(lines, lineIndex) {
|
|
773
|
+
// Look backwards for func/var/let/class/struct declaration
|
|
774
|
+
for (let j = lineIndex; j >= Math.max(0, lineIndex - 5); j--) {
|
|
775
|
+
const funcMatch = lines[j].match(/(?:func|var|let|class|struct|enum)\s+(\w+)/);
|
|
776
|
+
if (funcMatch)
|
|
777
|
+
return funcMatch[1];
|
|
778
|
+
}
|
|
779
|
+
return undefined;
|
|
780
|
+
}
|
|
781
|
+
function groupByKey(hits) {
|
|
782
|
+
const groups = new Map();
|
|
783
|
+
for (const hit of hits) {
|
|
784
|
+
const key = `${hit.type}:${hit.key}`;
|
|
785
|
+
if (!groups.has(key))
|
|
786
|
+
groups.set(key, []);
|
|
787
|
+
groups.get(key).push(hit);
|
|
788
|
+
}
|
|
789
|
+
return groups;
|
|
790
|
+
}
|
|
791
|
+
function buildFragileKeys(hits) {
|
|
792
|
+
const groups = groupByKey(hits);
|
|
793
|
+
const fragile = [];
|
|
794
|
+
for (const [key, keyHits] of groups) {
|
|
795
|
+
fragile.push({
|
|
796
|
+
key,
|
|
797
|
+
type: keyHits[0].type,
|
|
798
|
+
files: [...new Set(keyHits.map(h => h.file))],
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
return fragile.length > 0 ? fragile : undefined;
|
|
802
|
+
}
|
|
803
|
+
//# sourceMappingURL=code-scanner.js.map
|