forgeos 0.1.0-alpha.68 → 0.1.0-alpha.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.agents/skills/forge-agent-fabric/SKILL.md +21 -1
  2. package/.agents/skills/forge-agent-fabric/references/repository-analysis.md +102 -0
  3. package/.agents/skills/forge-agent-fabric/scripts/repository.mjs +44 -0
  4. package/AGENTS.md +1 -1
  5. package/CHANGELOG.md +16 -0
  6. package/README.md +12 -3
  7. package/docs/agent-fabric.md +13 -0
  8. package/docs/architecture/agent-fabric/REPOSITORY_MANIFEST_PLAN.md +272 -0
  9. package/docs/changelog.md +32 -0
  10. package/docs/repository-agent-benchmark.md +179 -0
  11. package/docs/repository-analysis.md +139 -0
  12. package/docs/repository-context-quality.md +179 -0
  13. package/docs/repository-runtime-observation.md +82 -0
  14. package/package.json +14 -2
  15. package/schemas/forge-manifest.schema.json +385 -53
  16. package/src/forge/_generated/releaseManifest.json +1 -1
  17. package/src/forge/_generated/releaseManifest.ts +3 -3
  18. package/src/forge/agent-fabric/managed-run-contract.ts +1 -0
  19. package/src/forge/agent-fabric/managed-run-service.ts +10 -1
  20. package/src/forge/agent-fabric/managed-run-store.ts +20 -0
  21. package/src/forge/agent-fabric/managed-workspace.ts +56 -18
  22. package/src/forge/agent-fabric/repository-context.ts +83 -0
  23. package/src/forge/agent-memory/mcp.ts +38 -1
  24. package/src/forge/cair/index.ts +15 -3
  25. package/src/forge/cair/types.ts +9 -2
  26. package/src/forge/cli/commands.ts +19 -0
  27. package/src/forge/cli/main.ts +15 -0
  28. package/src/forge/cli/parse.ts +70 -16
  29. package/src/forge/cli/repository.ts +151 -0
  30. package/src/forge/compiler/external-manifest/registry.ts +21 -9
  31. package/src/forge/delta/process-identity.ts +22 -16
  32. package/src/forge/repository-analysis/adapters/common.ts +72 -0
  33. package/src/forge/repository-analysis/adapters/docker.ts +185 -0
  34. package/src/forge/repository-analysis/adapters/java.ts +133 -0
  35. package/src/forge/repository-analysis/adapters/typescript.ts +350 -0
  36. package/src/forge/repository-analysis/adapters/vue.ts +71 -0
  37. package/src/forge/repository-analysis/analyze.ts +434 -0
  38. package/src/forge/repository-analysis/benchmark.ts +336 -0
  39. package/src/forge/repository-analysis/cache-gc.ts +154 -0
  40. package/src/forge/repository-analysis/check-selection.ts +123 -0
  41. package/src/forge/repository-analysis/context-node.ts +17 -0
  42. package/src/forge/repository-analysis/context.ts +456 -0
  43. package/src/forge/repository-analysis/http-resolution.ts +135 -0
  44. package/src/forge/repository-analysis/identity.ts +45 -0
  45. package/src/forge/repository-analysis/index.ts +18 -0
  46. package/src/forge/repository-analysis/nuxt-resolution.ts +188 -0
  47. package/src/forge/repository-analysis/quality.ts +60 -0
  48. package/src/forge/repository-analysis/retrieval.ts +309 -0
  49. package/src/forge/repository-analysis/runtime-artifacts.ts +195 -0
  50. package/src/forge/repository-analysis/runtime-observation.ts +316 -0
  51. package/src/forge/repository-analysis/scanner.ts +174 -0
  52. package/src/forge/repository-analysis/storage.ts +193 -0
  53. package/src/forge/repository-analysis/test-associations.ts +59 -0
  54. package/src/forge/repository-analysis/types.ts +99 -0
  55. package/src/forge/repository-manifest/index.ts +178 -0
  56. package/src/forge/repository-manifest/types.ts +63 -0
  57. package/src/forge/version.ts +1 -1
@@ -0,0 +1,350 @@
1
+ import ts from "typescript";
2
+ import type { AdapterResult, AnalysisSource, RepositoryNode } from "../types.ts";
3
+ import { emptyResult, evidence, makeEdge, makeNode, staticHttpPath } from "./common.ts";
4
+
5
+ /** Parses JS/TS with the bundled compiler; resolves lexical references without loading libraries/dependencies. */
6
+ export function analyzeTypeScript(source: AnalysisSource, scriptText = source.text, offset = 0, scriptLanguage?: string): AdapterResult {
7
+ const result = emptyResult();
8
+ const filename = source.path.endsWith(".vue") ? `${source.path}.${scriptLanguage ?? "js"}` : source.path;
9
+ const kind = filename.endsWith(".tsx") ? ts.ScriptKind.TSX : filename.endsWith(".jsx") ? ts.ScriptKind.JSX : /\.[cm]?js$/.test(filename) ? ts.ScriptKind.JS : ts.ScriptKind.TS;
10
+ const sourceFile = ts.createSourceFile(filename, scriptText, ts.ScriptTarget.Latest, true, kind);
11
+ const options: ts.CompilerOptions = { noLib: true, noResolve: true, allowJs: true, target: ts.ScriptTarget.Latest, module: ts.ModuleKind.ESNext };
12
+ const host: ts.CompilerHost = {
13
+ getSourceFile: (path) => path === filename ? sourceFile : undefined,
14
+ getDefaultLibFileName: () => "", writeFile: () => undefined,
15
+ getCurrentDirectory: () => "/", getDirectories: () => [],
16
+ fileExists: (path) => path === filename, readFile: (path) => path === filename ? scriptText : undefined,
17
+ getCanonicalFileName: (path) => path, useCaseSensitiveFileNames: () => true, getNewLine: () => "\n",
18
+ };
19
+ const program = ts.createProgram([filename], options, host);
20
+ const checker = program.getTypeChecker();
21
+ for (const diagnostic of program.getSyntacticDiagnostics(sourceFile)) result.diagnostics.push({ code: "REPOSITORY_TS_PARSE", severity: "error", file: source.path, message: ts.flattenDiagnosticMessageText(diagnostic.messageText, " ") });
22
+ const declarations = new Map<ts.Symbol, RepositoryNode>();
23
+ const owners = new Map<ts.Node, RepositoryNode>();
24
+ const importBindings = new Map<string, { node: RepositoryNode; imported: string }>();
25
+ const scopeName = (node: ts.Node): string => {
26
+ const parts: string[] = [];
27
+ for (let parent = node.parent; parent; parent = parent.parent) {
28
+ if ((ts.isClassDeclaration(parent) || ts.isFunctionDeclaration(parent) || ts.isMethodDeclaration(parent)) && parent.name) parts.unshift(parent.name.getText(sourceFile));
29
+ }
30
+ return parts.join(".");
31
+ };
32
+ const addDeclaration = (node: ts.Node, name: ts.Identifier | ts.StringLiteral, kind: string) => {
33
+ const scope = scopeName(node);
34
+ const qualified = scope ? `${scope}.${name.text}` : name.text;
35
+ const symbol = checker.getSymbolAtLocation(name);
36
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
37
+ const repositoryNode = makeNode(source, "symbol", name.text, offset + node.getStart(sourceFile), offset + node.end, { symbolKind: kind, qualifiedName: qualified, exported: modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) || ts.isVariableDeclaration(node) && ts.isVariableDeclarationList(node.parent) && ts.isVariableStatement(node.parent.parent) && ts.getModifiers(node.parent.parent)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) || false, defaultExport: modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) || false }, `${qualified}:${kind}:${offset + node.pos}`);
38
+ // Position is only a disambiguator for repeated declarations; edits never alias a different symbol silently.
39
+ if (symbol) declarations.set(symbol, repositoryNode);
40
+ owners.set(node, repositoryNode);
41
+ result.nodes.push(repositoryNode);
42
+ };
43
+ const visitDeclarations = (node: ts.Node) => {
44
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
45
+ const bindings: { local: string; imported: string }[] = [];
46
+ if (node.importClause?.name) bindings.push({ local: node.importClause.name.text, imported: "default" });
47
+ const named = node.importClause?.namedBindings;
48
+ if (named && ts.isNamedImports(named)) for (const item of named.elements) bindings.push({ local: item.name.text, imported: item.propertyName?.text ?? item.name.text });
49
+ if (named && ts.isNamespaceImport(named)) bindings.push({ local: named.name.text, imported: "*" });
50
+ const repositoryNode = makeNode(source, "import", node.moduleSpecifier.text, offset + node.getStart(sourceFile), offset + node.end, { specifier: node.moduleSpecifier.text, bindings }, `import:${offset + node.pos}:${node.moduleSpecifier.text}`);
51
+ result.nodes.push(repositoryNode);
52
+ for (const binding of bindings) importBindings.set(binding.local, { node: repositoryNode, imported: binding.imported });
53
+ }
54
+ if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) && node.name) addDeclaration(node, node.name, ts.SyntaxKind[node.kind]);
55
+ else if ((ts.isVariableDeclaration(node) || ts.isMethodDeclaration(node)) && node.name && (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name))) addDeclaration(node, node.name, ts.SyntaxKind[node.kind]);
56
+ ts.forEachChild(node, visitDeclarations);
57
+ };
58
+ visitDeclarations(sourceFile);
59
+ const ownerOf = (node: ts.Node): RepositoryNode | undefined => {
60
+ for (let current: ts.Node | undefined = node; current; current = current.parent) if (owners.has(current)) return owners.get(current);
61
+ return undefined;
62
+ };
63
+ // Initializer variables (for example `response = await api.get(...)`) are not callables.
64
+ const callableOwnerOf = (node: ts.Node): RepositoryNode | undefined => {
65
+ for (let current: ts.Node | undefined = node; current; current = current.parent) {
66
+ if (ts.isFunctionDeclaration(current) || ts.isMethodDeclaration(current)) return owners.get(current);
67
+ if (ts.isArrowFunction(current) || ts.isFunctionExpression(current)) {
68
+ let wrapped: ts.Node = current;
69
+ while (wrapped.parent && (ts.isParenthesizedExpression(wrapped.parent) || ts.isAsExpression(wrapped.parent) || ts.isSatisfiesExpression(wrapped.parent))) wrapped = wrapped.parent;
70
+ return ts.isVariableDeclaration(wrapped.parent) && wrapped.parent.initializer === wrapped ? owners.get(wrapped.parent) : undefined;
71
+ }
72
+ }
73
+ return ownerOf(node);
74
+ };
75
+ const literal = (node: ts.Node | undefined) => node && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) ? node.text : undefined;
76
+ type Scalar = string | number | boolean;
77
+ const mutatedObjects = new Set<ts.Symbol>();
78
+ const collectMutations = (node: ts.Node) => {
79
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment) {
80
+ const target = node.left;
81
+ if (ts.isPropertyAccessExpression(target) || ts.isElementAccessExpression(target)) { const symbol = checker.getSymbolAtLocation(target.expression); if (symbol) mutatedObjects.add(symbol); }
82
+ }
83
+ ts.forEachChild(node, collectMutations);
84
+ };
85
+ collectMutations(sourceFile);
86
+ const unwrapped = (expression: ts.Expression): ts.Expression => {
87
+ while (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression) || ts.isTypeAssertionExpression(expression) || ts.isAwaitExpression(expression)) expression = expression.expression;
88
+ return expression;
89
+ };
90
+ const constant = (expression: ts.Expression | undefined, parameters = new Map<ts.Symbol, Scalar>(), seen = new Set<ts.Symbol>(), depth = 0): Scalar | undefined => {
91
+ if (!expression || depth > 20) return undefined;
92
+ expression = unwrapped(expression);
93
+ if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
94
+ if (ts.isNumericLiteral(expression)) return Number(expression.text);
95
+ if (expression.kind === ts.SyntaxKind.TrueKeyword || expression.kind === ts.SyntaxKind.FalseKeyword) return expression.kind === ts.SyntaxKind.TrueKeyword;
96
+ if (ts.isIdentifier(expression)) {
97
+ if (/(?:password|secret|credential|authorization|token)/i.test(expression.text)) return undefined;
98
+ const symbol = checker.getSymbolAtLocation(expression);
99
+ if (!symbol || seen.has(symbol)) return undefined;
100
+ if (parameters.has(symbol)) return parameters.get(symbol);
101
+ const declaration = symbol.valueDeclaration;
102
+ if (!declaration || !ts.isVariableDeclaration(declaration) || !ts.isVariableDeclarationList(declaration.parent) || !(declaration.parent.flags & ts.NodeFlags.Const)) return undefined;
103
+ const next = new Set(seen); next.add(symbol);
104
+ return constant(declaration.initializer, parameters, next, depth + 1);
105
+ }
106
+ if (ts.isTemplateExpression(expression)) {
107
+ let value = expression.head.text;
108
+ for (const span of expression.templateSpans) { const part = constant(span.expression, parameters, seen, depth + 1); if (part === undefined) return undefined; value += String(part) + span.literal.text; }
109
+ return value;
110
+ }
111
+ if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.PlusToken) {
112
+ const left = constant(expression.left, parameters, seen, depth + 1); const right = constant(expression.right, parameters, seen, depth + 1);
113
+ if (left === undefined || right === undefined) return undefined;
114
+ return typeof left === "number" && typeof right === "number" ? left + right : String(left) + String(right);
115
+ }
116
+ if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression) && !/(?:password|secret|credential|authorization|token)/i.test(expression.name.text)) {
117
+ const symbol = checker.getSymbolAtLocation(expression.expression); const declaration = symbol?.valueDeclaration;
118
+ if (symbol && mutatedObjects.has(symbol)) return undefined;
119
+ if (declaration && ts.isVariableDeclaration(declaration) && ts.isVariableDeclarationList(declaration.parent) && declaration.parent.flags & ts.NodeFlags.Const && declaration.initializer && ts.isObjectLiteralExpression(unwrapped(declaration.initializer))) {
120
+ const value = unwrapped(declaration.initializer) as ts.ObjectLiteralExpression;
121
+ const property = value.properties.find((item) => ts.isPropertyAssignment(item) && item.name.getText(sourceFile).replace(/["']/g, "") === expression.name.text);
122
+ if (property && ts.isPropertyAssignment(property)) return constant(property.initializer, parameters, seen, depth + 1);
123
+ }
124
+ }
125
+ return undefined;
126
+ };
127
+ const staticString = (expression: ts.Expression | undefined, parameters?: Map<ts.Symbol, Scalar>): string | undefined => { const value = constant(expression, parameters); return value === undefined ? undefined : String(value); };
128
+ const segmentPathPattern = (expression: ts.Expression | undefined): { requestPathPattern: string; requestPatternParameters: { name: string; kind: string }[] } | undefined => {
129
+ if (!expression) return undefined;
130
+ expression = unwrapped(expression);
131
+ if (!ts.isTemplateExpression(expression) || !/^\/(?!\/)/.test(expression.head.text) || expression.templateSpans.length > 8) return undefined;
132
+ let path = expression.head.text;
133
+ const parameters: { name: string; kind: string }[] = [];
134
+ for (const span of expression.templateSpans) {
135
+ if (!path.endsWith("/") || span.literal.text && !span.literal.text.startsWith("/")) return undefined;
136
+ const value = constant(span.expression);
137
+ if (value !== undefined) { if (!/^[A-Za-z0-9_~-]+$/.test(String(value))) return undefined; path += String(value) + span.literal.text; continue; }
138
+ let parameter = unwrapped(span.expression); let encoded = false;
139
+ if (ts.isCallExpression(parameter) && ts.isIdentifier(parameter.expression) && parameter.expression.text === "encodeURIComponent" && !checker.getSymbolAtLocation(parameter.expression) && parameter.arguments.length === 1) { parameter = unwrapped(parameter.arguments[0]); encoded = true; }
140
+ if (!ts.isIdentifier(parameter) || /(?:password|secret|credential|authorization|token)/i.test(parameter.text)) return undefined;
141
+ const declaration = checker.getSymbolAtLocation(parameter)?.valueDeclaration;
142
+ if (!declaration || !ts.isParameter(declaration) || declaration.type?.kind !== ts.SyntaxKind.NumberKeyword && !(encoded && declaration.type?.kind === ts.SyntaxKind.StringKeyword)) return undefined;
143
+ parameters.push({ name: parameter.text, kind: declaration.type?.kind === ts.SyntaxKind.NumberKeyword ? "number" : "encoded-string" });
144
+ path += `{${parameter.text}}${span.literal.text}`;
145
+ }
146
+ if (!parameters.length || path.length > 2048 || /[?#\\%]|\/\//.test(path) || path.split("/").some((segment) => segment === "." || segment === ".." || segment && !/^(?:[A-Za-z0-9_.~-]+|\{[A-Za-z_$][\w$]*\})$/.test(segment))) return undefined;
147
+ return { requestPathPattern: path, requestPatternParameters: parameters };
148
+ };
149
+ const staticObject = (expression: ts.Expression | undefined): ts.ObjectLiteralExpression | undefined => {
150
+ if (!expression) return undefined; expression = unwrapped(expression);
151
+ if (ts.isObjectLiteralExpression(expression)) return expression;
152
+ if (ts.isIdentifier(expression)) {
153
+ const symbol = checker.getSymbolAtLocation(expression); const declaration = symbol?.valueDeclaration;
154
+ if (symbol && !mutatedObjects.has(symbol) && declaration && ts.isVariableDeclaration(declaration) && ts.isVariableDeclarationList(declaration.parent) && declaration.parent.flags & ts.NodeFlags.Const && declaration.initializer) { const value = unwrapped(declaration.initializer); if (ts.isObjectLiteralExpression(value)) return value; }
155
+ }
156
+ return undefined;
157
+ };
158
+ const propertyValue = (object: ts.Expression | undefined, name: string): ts.Expression | undefined => {
159
+ const value = staticObject(object); if (!value) return undefined;
160
+ for (const item of [...value.properties].reverse()) {
161
+ if (ts.isSpreadAssignment(item)) return undefined;
162
+ if (ts.isPropertyAssignment(item) && item.name.getText(sourceFile).replace(/["']/g, "") === name) return item.initializer;
163
+ if (ts.isShorthandPropertyAssignment(item) && item.name.text === name) return item.name;
164
+ }
165
+ return undefined;
166
+ };
167
+ const urlMetadata = (url: string | undefined, base?: string) => {
168
+ const absolute = url !== undefined && /^(?:https?:)?\/\//.test(url);
169
+ const combined = url === undefined ? undefined : !absolute && base ? `${base.replace(/\/$/, "")}/${url.replace(/^\//, "")}` : url;
170
+ const path = combined !== undefined ? staticHttpPath(combined) : undefined;
171
+ let origin: string | undefined;
172
+ if (combined && /^https?:\/\//.test(combined)) { try { origin = new URL(combined).origin; } catch { /* unresolved invalid URL */ } }
173
+ return { path, origin, requestPath: url !== undefined ? staticHttpPath(url) : undefined, requestAbsolute: absolute };
174
+ };
175
+ interface Client { node: RepositoryNode; baseURL?: string; baseKnown: boolean }
176
+ const clients = new Map<ts.Symbol, Client>();
177
+ const builtinName = (expression: ts.Expression): string | undefined => {
178
+ if (!ts.isIdentifier(expression)) return undefined;
179
+ const imported = importBindings.get(expression.text);
180
+ if (imported && ["axios", "ofetch"].includes(imported.node.name)) return imported.node.name === "axios" ? "axios" : "$fetch";
181
+ const symbol = checker.getSymbolAtLocation(expression);
182
+ return !symbol && ["fetch", "$fetch", "useFetch", "axios", "ofetch"].includes(expression.text) ? expression.text : undefined;
183
+ };
184
+ const collectClients = (node: ts.Node) => {
185
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(unwrapped(node.initializer))) {
186
+ const factory = unwrapped(node.initializer) as ts.CallExpression;
187
+ if (ts.isPropertyAccessExpression(factory.expression) && factory.expression.name.text === "create" && builtinName(factory.expression.expression)) {
188
+ const baseExpression = propertyValue(factory.arguments[0], "baseURL");
189
+ const baseURL = staticString(baseExpression);
190
+ const options = staticObject(factory.arguments[0]);
191
+ const baseKnown = !factory.arguments[0] || !!options && (baseExpression ? baseURL !== undefined : !options.properties.some((property) => ts.isSpreadAssignment(property)));
192
+ const symbol = checker.getSymbolAtLocation(node.name);
193
+ const model = makeNode(source, "http-client", node.name.text, offset + node.getStart(sourceFile), offset + node.end, { clientId: node.name.text, baseKnown, ...urlMetadata("", baseURL), exported: symbol ? declarations.get(symbol)?.metadata.exported : false, factory: builtinName(factory.expression.expression) }, `client:${offset + node.pos}`);
194
+ result.nodes.push(model);
195
+ if (symbol) clients.set(symbol, { node: model, baseURL, baseKnown });
196
+ }
197
+ }
198
+ ts.forEachChild(node, collectClients);
199
+ };
200
+ collectClients(sourceFile);
201
+ interface Request { method: string; url?: string; clientId: string; confidence: string; client?: Client; clientImportId?: string; baseURL?: string; baseKnown: boolean }
202
+ const request = (node: ts.CallExpression, parameters?: Map<ts.Symbol, Scalar>): Request | undefined => {
203
+ const expression = node.expression;
204
+ if (ts.isPropertyAccessExpression(expression) && /^(get|post|put|patch|delete|head|options)$/i.test(expression.name.text) && ts.isIdentifier(expression.expression)) {
205
+ const receiver = expression.expression;
206
+ const symbol = checker.getSymbolAtLocation(receiver); const client = symbol ? clients.get(symbol) : undefined;
207
+ const imported = importBindings.get(receiver.text);
208
+ const builtin = builtinName(receiver);
209
+ const binding = symbol?.valueDeclaration;
210
+ const declaration = binding && ts.isBindingElement(binding) && ts.isObjectBindingPattern(binding.parent) && ts.isVariableDeclaration(binding.parent.parent) ? binding.parent.parent : undefined;
211
+ const initializer = declaration?.initializer && unwrapped(declaration.initializer);
212
+ const nuxtProvided = initializer && ts.isCallExpression(initializer) && ts.isIdentifier(initializer.expression) && initializer.expression.text === "useNuxtApp" && !checker.getSymbolAtLocation(initializer.expression);
213
+ if (!client && !imported && !builtin && !nuxtProvided && !/^(?:\$?api|http|client)$/.test(receiver.text)) return undefined;
214
+ const options = node.arguments[["post", "put", "patch"].includes(expression.name.text.toLowerCase()) ? 2 : 1];
215
+ const baseExpression = propertyValue(options, "baseURL"); const baseURL = staticString(baseExpression, parameters) ?? client?.baseURL;
216
+ return { method: expression.name.text.toUpperCase(), url: staticString(node.arguments[0], parameters), clientId: receiver.text, confidence: client ? "factory" : builtin ? "builtin" : "candidate", client, clientImportId: imported?.node.id, baseURL, baseKnown: baseExpression ? staticString(baseExpression, parameters) !== undefined : client?.baseKnown ?? !!builtin };
217
+ }
218
+ if (ts.isIdentifier(expression)) {
219
+ const symbol = checker.getSymbolAtLocation(expression); const client = symbol ? clients.get(symbol) : undefined;
220
+ const builtin = builtinName(expression);
221
+ if (!client && !builtin) return undefined;
222
+ const options = node.arguments[1];
223
+ const baseExpression = builtin === "fetch" ? undefined : propertyValue(options, "baseURL"); const methodExpression = propertyValue(options, "method");
224
+ const method = methodExpression ? staticString(methodExpression, parameters)?.toUpperCase() ?? "UNKNOWN" : "GET";
225
+ const baseURL = staticString(baseExpression, parameters) ?? client?.baseURL;
226
+ const url = staticString(node.arguments[0], parameters);
227
+ const documentRelative = url !== undefined && !/^(?:\/|https?:\/\/)/.test(url) && !baseURL;
228
+ return { method, url, clientId: expression.text, confidence: client ? "factory" : "builtin", client, baseURL, baseKnown: !documentRelative && (baseExpression ? staticString(baseExpression, parameters) !== undefined : client?.baseKnown ?? true) };
229
+ }
230
+ return undefined;
231
+ };
232
+ const wrappers = new Map<ts.Symbol, { method: string; urlTemplate: string; parameterIndex: number; baseURL?: string; baseKnown: boolean; clientId: string }>();
233
+ const collectWrappers = (node: ts.Node) => {
234
+ let callable: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | undefined; let symbol: ts.Symbol | undefined;
235
+ if (ts.isFunctionDeclaration(node) && node.name) { callable = node; symbol = checker.getSymbolAtLocation(node.name); }
236
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(unwrapped(node.initializer)) || ts.isFunctionExpression(unwrapped(node.initializer)))) { callable = unwrapped(node.initializer) as ts.ArrowFunction | ts.FunctionExpression; symbol = checker.getSymbolAtLocation(node.name); }
237
+ if (callable && symbol && callable.body) {
238
+ const body = callable.body;
239
+ const returns = ts.isBlock(body) ? body.statements.filter((statement): statement is ts.ReturnStatement => ts.isReturnStatement(statement)) : [];
240
+ const returned = ts.isBlock(body) ? returns.length === 1 && body.statements.every((statement) => ts.isReturnStatement(statement) || ts.isVariableStatement(statement)) ? returns[0].expression : undefined : body;
241
+ const expression = returned && unwrapped(returned);
242
+ if (expression && ts.isCallExpression(expression)) {
243
+ const params = new Map<ts.Symbol, Scalar>();
244
+ callable.parameters.forEach((parameter, index) => { if (ts.isIdentifier(parameter.name)) { const bound = checker.getSymbolAtLocation(parameter.name); if (bound) params.set(bound, `__FORGE_URL_PARAM_${index}__`); } });
245
+ const model = request(expression, params);
246
+ const tokens = model?.url?.match(/__FORGE_URL_PARAM_\d+__/g);
247
+ if (model?.url && tokens?.length === 1 && model.confidence !== "candidate" && model.method !== "UNKNOWN") {
248
+ const wrapper = { method: model.method, urlTemplate: model.url, parameterIndex: Number(/\d+/.exec(tokens[0])![0]), baseURL: model.baseURL, baseKnown: model.baseKnown, clientId: model.clientId };
249
+ wrappers.set(symbol, wrapper);
250
+ const safeTemplate = staticHttpPath(model.url);
251
+ const declaration = declarations.get(symbol); if (declaration && safeTemplate?.includes(tokens[0])) declaration.metadata.httpWrapper = { ...wrapper, urlTemplate: safeTemplate, baseURL: undefined, ...urlMetadata("", model.baseURL), origin: urlMetadata(model.url, model.baseURL).origin };
252
+ }
253
+ }
254
+ }
255
+ ts.forEachChild(node, collectWrappers);
256
+ };
257
+ collectWrappers(sourceFile);
258
+ const visit = (node: ts.Node) => {
259
+ if (ts.isIdentifier(node)) {
260
+ const symbol = checker.getSymbolAtLocation(node);
261
+ const target = symbol ? declarations.get(symbol) : undefined;
262
+ const parent = node.parent;
263
+ const declarationName = (ts.isDeclarationStatement(parent) || ts.isVariableDeclaration(parent) || ts.isMethodDeclaration(parent) || ts.isParameter(parent)) && "name" in parent && parent.name === node;
264
+ if (target && !declarationName && !ts.isImportSpecifier(parent) && !ts.isImportClause(parent)) {
265
+ const owner = ts.isCallExpression(parent) && parent.expression === node ? callableOwnerOf(node) : ownerOf(node);
266
+ const edge = makeEdge(owner ?? makeNode(source, "file", source.path), target, ts.isCallExpression(parent) && parent.expression === node ? "calls" : "references", evidence(source, "resolved", "complete"), { binding: "lexical", offset: offset + node.getStart(sourceFile) });
267
+ result.edges.push(edge);
268
+ }
269
+ const imported = importBindings.get(node.text);
270
+ if (imported && symbol?.declarations?.some((declaration) => ts.isImportSpecifier(declaration) || ts.isImportClause(declaration) || ts.isNamespaceImport(declaration)) && !ts.isImportSpecifier(parent) && !ts.isImportClause(parent) && !ts.isNamespaceImport(parent)) {
271
+ const invocation = ts.isCallExpression(parent) && parent.expression === node ? parent : imported.imported === "*" && ts.isPropertyAccessExpression(parent) && parent.expression === node && ts.isCallExpression(parent.parent) && parent.parent.expression === parent ? parent.parent : undefined;
272
+ const call = !!invocation;
273
+ const argument = invocation ? staticString(invocation.arguments[0]) : undefined;
274
+ const argumentPaths = invocation ? invocation.arguments.map((argument) => { const value = staticString(argument); return value && /^(?:\/|https?:\/\/)/.test(value) ? staticHttpPath(value) : undefined; }) : undefined;
275
+ const reference = makeNode(source, "import-reference", node.text, offset + node.getStart(sourceFile), offset + node.end, { importId: imported.node.id, imported: imported.imported === "*" && ts.isPropertyAccessExpression(parent) ? parent.name.text : imported.imported, namespace: imported.imported === "*", owner: (call ? callableOwnerOf(node) : ownerOf(node))?.id, call, argumentPaths, ...(argument ? { argumentPath: staticHttpPath(argument), argumentAbsolute: /^(?:https?:)?\/\//.test(argument) } : {}) }, `import-reference:${offset + node.getStart(sourceFile)}`);
276
+ result.nodes.push(reference);
277
+ }
278
+ if (!symbol && ts.isCallExpression(parent) && parent.expression === node && !builtinName(node)) {
279
+ const argument = staticString(parent.arguments[0]);
280
+ const argumentPaths = parent.arguments.map((argument) => { const value = staticString(argument); return value && /^(?:\/|https?:\/\/)/.test(value) ? staticHttpPath(value) : undefined; });
281
+ result.nodes.push(makeNode(source, "unresolved-reference", node.text, offset + node.getStart(sourceFile), offset + node.end, { owner: callableOwnerOf(node)?.id, call: true, argumentPaths, ...(argument ? { argumentPath: staticHttpPath(argument), argumentAbsolute: /^(?:https?:)?\/\//.test(argument) } : {}) }, `unresolved-reference:${offset + node.getStart(sourceFile)}`));
282
+ }
283
+ }
284
+ if (ts.isCallExpression(node)) {
285
+ const expression = node.expression;
286
+ const functionName = expression.getText(sourceFile);
287
+ let model = request(node);
288
+ if (!model && ts.isIdentifier(expression)) {
289
+ const symbol = checker.getSymbolAtLocation(expression); const wrapper = symbol && wrappers.get(symbol);
290
+ const argument = wrapper && staticString(node.arguments[wrapper.parameterIndex]);
291
+ if (wrapper && argument !== undefined) model = { method: wrapper.method, url: wrapper.urlTemplate.replace(`__FORGE_URL_PARAM_${wrapper.parameterIndex}__`, argument), clientId: expression.text, confidence: "wrapper", baseURL: wrapper.baseURL, baseKnown: wrapper.baseKnown };
292
+ }
293
+ if (model) {
294
+ const info = urlMetadata(model.url, model.baseURL);
295
+ const pattern = model.url === undefined ? segmentPathPattern(node.arguments[0]) : undefined;
296
+ const call = makeNode(source, "http-call", `${model.method} ${info.path ?? "<dynamic>"}`, offset + node.getStart(sourceFile), offset + node.end, { method: model.method, ...info, ...pattern, ...(pattern ? { origin: urlMetadata("", model.baseURL).origin } : {}), clientId: model.clientId, clientConfidence: model.confidence, clientModelId: model.client?.node.id, clientImportId: model.clientImportId, baseKnown: model.baseKnown, basePath: model.baseURL ? urlMetadata("", model.baseURL).path : undefined }, `http:${offset + node.pos}`);
297
+ result.nodes.push(call);
298
+ const owner = callableOwnerOf(node); if (owner) result.edges.push(makeEdge(owner, call, "calls", evidence(source)));
299
+ if (!info.path || !model.baseKnown) result.diagnostics.push({ code: "REPOSITORY_DYNAMIC_URL", severity: "info", file: source.path, message: "HTTP URL or client base cannot be statically resolved" });
300
+ }
301
+ if (ts.isPropertyAccessExpression(expression) && /^(?:app|router)\.(get|post|put|patch|delete|head|options)$/.test(functionName)) {
302
+ const path = literal(node.arguments[0]);
303
+ if (path) result.nodes.push(makeNode(source, "endpoint", `${expression.name.text.toUpperCase()} ${path}`, offset + node.getStart(sourceFile), offset + node.end, { method: expression.name.text.toUpperCase(), path }, `endpoint:${offset + node.pos}`));
304
+ }
305
+ if (/^(?:test|it|describe)(?:\.(?:only|skip|todo))?$/.test(functionName)) {
306
+ const name = literal(node.arguments[0]);
307
+ const callback = node.arguments.find((argument) => ts.isArrowFunction(argument) || ts.isFunctionExpression(argument));
308
+ if (name) result.nodes.push(makeNode(source, functionName.startsWith("describe") ? "test-suite" : "test", name, offset + node.getStart(sourceFile), offset + node.end, { framework: "jest-compatible", observedCoverage: false, ...(callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) ? { testBody: { start: offset + callback.body.getStart(sourceFile), end: offset + callback.body.end } } : {}) }, `test:${offset + node.pos}`));
309
+ }
310
+ if (["defineProps", "defineEmits", "defineStore"].includes(functionName)) {
311
+ const names: string[] = [];
312
+ for (const type of node.typeArguments ?? []) if (ts.isTypeLiteralNode(type)) for (const member of type.members) if (member.name && (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name))) names.push(member.name.text);
313
+ const argument = node.arguments[0];
314
+ if (argument && ts.isArrayLiteralExpression(argument)) for (const item of argument.elements) { const name = literal(item); if (name) names.push(name); }
315
+ if (argument && ts.isObjectLiteralExpression(argument)) for (const property of argument.properties) if (property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) names.push(property.name.text);
316
+ result.nodes.push(makeNode(source, functionName === "defineStore" ? "store" : "declaration", functionName, offset + node.getStart(sourceFile), offset + node.end, { macro: functionName, names: [...new Set(names)] }, `macro:${offset + node.pos}`));
317
+ }
318
+ }
319
+ // Static Vue Router route entries; no evaluation of arbitrary JS configuration.
320
+ if (ts.isObjectLiteralExpression(node)) {
321
+ const provide = propertyValue(node, "provide");
322
+ if (provide && ts.isObjectLiteralExpression(provide)) for (const property of provide.properties) {
323
+ const variable = ts.isShorthandPropertyAssignment(property) ? property.name : ts.isPropertyAssignment(property) && ts.isIdentifier(property.initializer) ? property.initializer : undefined;
324
+ if (!variable || !property.name || !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) continue;
325
+ const symbol = ts.isShorthandPropertyAssignment(property) ? checker.getShorthandAssignmentValueSymbol(property) : checker.getSymbolAtLocation(variable);
326
+ const client = symbol && clients.get(symbol);
327
+ if (client) client.node.metadata.providedAs = [...new Set([...(Array.isArray(client.node.metadata.providedAs) ? client.node.metadata.providedAs as string[] : []), `$${property.name.text}`])];
328
+ }
329
+ let route: string | undefined; let componentName: string | undefined;
330
+ for (const property of node.properties) if (ts.isPropertyAssignment(property)) {
331
+ if (property.name.getText(sourceFile) === "path") route = literal(property.initializer);
332
+ if (property.name.getText(sourceFile) === "component" && ts.isIdentifier(property.initializer)) componentName = property.initializer.text;
333
+ }
334
+ if (route && componentName) {
335
+ const page = makeNode(source, "page", route, offset + node.getStart(sourceFile), offset + node.end, { route, componentName }, `route:${offset + node.pos}`);
336
+ result.nodes.push(page);
337
+ const imported = importBindings.get(componentName); if (imported) result.edges.push(makeEdge(page, imported.node, "routes-to", evidence(source)));
338
+ }
339
+ }
340
+ if (ts.isExportAssignment(node) && ts.isIdentifier(node.expression)) {
341
+ const symbol = checker.getSymbolAtLocation(node.expression); const declaration = symbol && declarations.get(symbol); const client = symbol && clients.get(symbol);
342
+ if (declaration) { declaration.metadata.defaultExport = true; declaration.metadata.exported = true; }
343
+ if (client) { client.node.metadata.defaultExport = true; client.node.metadata.exported = true; }
344
+ }
345
+ ts.forEachChild(node, visit);
346
+ };
347
+ visit(sourceFile);
348
+ result.limitations.push("TypeScript: lexical bindings are resolved; package types, dynamic imports/configuration and full cross-file type semantics are not resolved");
349
+ return result;
350
+ }
@@ -0,0 +1,71 @@
1
+ import { parse, compileTemplate } from "@vue/compiler-sfc";
2
+ import { basename } from "node:path";
3
+ import type { AdapterResult, AnalysisSource } from "../types.ts";
4
+ import { emptyResult, evidence, makeEdge, makeNode } from "./common.ts";
5
+ import { analyzeTypeScript } from "./typescript.ts";
6
+
7
+ interface TemplateNode {
8
+ type: number;
9
+ tag?: string;
10
+ children?: TemplateNode[];
11
+ branches?: TemplateNode[];
12
+ props?: { type: number; name: string; arg?: { content?: string }; exp?: { content?: string }; loc?: { start: { offset: number }; end: { offset: number } } }[];
13
+ loc?: { start: { offset: number }; end: { offset: number } };
14
+ }
15
+
16
+ export function analyzeVue(source: AnalysisSource): AdapterResult {
17
+ const result = emptyResult();
18
+ const parsed = parse(source.text, { filename: source.path });
19
+ for (const error of parsed.errors) result.diagnostics.push({ code: "REPOSITORY_VUE_PARSE", severity: "error", file: source.path, message: typeof error === "string" ? error : error.message });
20
+ const descriptor = parsed.descriptor;
21
+ const ui = makeNode(source, "ui-component", basename(source.path, ".vue"), 0, source.text.length, { framework: "vue" });
22
+ result.nodes.push(ui);
23
+ for (const block of [descriptor.script, descriptor.scriptSetup]) {
24
+ if (!block) continue;
25
+ if (block.src) {
26
+ result.nodes.push(makeNode(source, "import", block.src, block.loc.start.offset, block.loc.end.offset, { specifier: block.src, bindings: [] }, `external-script:${block.src}`));
27
+ continue;
28
+ }
29
+ if (block.lang && !["ts", "tsx", "js", "jsx"].includes(block.lang)) {
30
+ result.diagnostics.push({ code: "REPOSITORY_VUE_SCRIPT_LANGUAGE", severity: "warning", file: source.path, message: `Unsupported script language: ${block.lang}` }); continue;
31
+ }
32
+ const analyzed = analyzeTypeScript(source, block.content, block.loc.start.offset, block.lang ?? "js");
33
+ result.nodes.push(...analyzed.nodes); result.edges.push(...analyzed.edges);
34
+ result.diagnostics.push(...analyzed.diagnostics); result.limitations.push(...analyzed.limitations);
35
+ for (const node of analyzed.nodes.filter((node) => ["symbol", "http-call", "store", "declaration"].includes(node.kind))) result.edges.push(makeEdge(ui, node, "contains", evidence(source)));
36
+ }
37
+ if (descriptor.template) {
38
+ const block = descriptor.template;
39
+ if (block.src) result.diagnostics.push({ code: "REPOSITORY_VUE_TEMPLATE_EXTERNAL", severity: "warning", file: source.path, message: "External template src is declared but not loaded or preprocessed by static SFC analysis" });
40
+ else if (block.lang && block.lang !== "html") result.diagnostics.push({ code: "REPOSITORY_VUE_TEMPLATE_LANGUAGE", severity: "warning", file: source.path, message: `Unsupported template preprocessor: ${block.lang}` });
41
+ else {
42
+ const compiled = compileTemplate({ source: block.content, filename: source.path, id: source.hash });
43
+ for (const error of compiled.errors) result.diagnostics.push({ code: "REPOSITORY_VUE_TEMPLATE_PARSE", severity: "error", file: source.path, message: typeof error === "string" ? error : error.message });
44
+ const visit = (node: TemplateNode) => {
45
+ if (node.type === 1 && node.tag && node.loc) {
46
+ const dynamic = node.tag === "component";
47
+ const importedComponent = result.nodes.find((candidate) => candidate.kind === "import" && Array.isArray(candidate.metadata.bindings) && (candidate.metadata.bindings as { local: string }[]).some((binding) => binding.local === node.tag || binding.local.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase() === node.tag));
48
+ if (importedComponent) {
49
+ const edge = makeEdge(ui, importedComponent, "renders", evidence(source));
50
+ result.edges.push(edge);
51
+ } else if (/^[A-Z]/.test(node.tag) || node.tag.includes("-") || dynamic) {
52
+ const unresolved = makeNode(source, "ui-reference", node.tag, block.loc.start.offset + node.loc.start.offset, block.loc.start.offset + node.loc.end.offset, { dynamic, autoimport: !dynamic, resolved: false }, `template:${node.loc.start.offset}`);
53
+ result.nodes.push(unresolved); result.edges.push(makeEdge(ui, unresolved, "renders", evidence(source, "syntactic", "unresolved")));
54
+ result.diagnostics.push({ code: "REPOSITORY_VUE_COMPONENT_UNRESOLVED", severity: "info", file: source.path, message: `Component '${node.tag}' has no explicit resolved import; autoimports/dynamic components remain unresolved` });
55
+ }
56
+ }
57
+ for (const child of node.children ?? []) visit(child);
58
+ for (const branch of node.branches ?? []) visit(branch);
59
+ };
60
+ if (compiled.ast) visit(compiled.ast as unknown as TemplateNode);
61
+ }
62
+ }
63
+ const pageMatch = /(?:^|\/)(?:pages|app\/pages)\/(.+)\.vue$/.exec(source.path);
64
+ if (pageMatch) {
65
+ const route = `/${pageMatch[1].replace(/(?:^|\/)index$/, "").replace(/\[([^\]]+)\]/g, ":$1")}`.replace(/\/$/, "") || "/";
66
+ const page = makeNode(source, "page", route, 0, source.text.length, { framework: "nuxt", route, convention: true });
67
+ page.evidence = evidence(source, "inferred"); result.nodes.push(page); result.edges.push(makeEdge(page, ui, "renders", evidence(source, "inferred")));
68
+ }
69
+ result.limitations.push("Vue: explicit imports, SFC blocks and HTML templates parsed; runtime component registration, Nuxt autoimports, template expression type resolution and custom preprocessors remain unresolved");
70
+ return result;
71
+ }