forgeos 0.1.0-alpha.13 → 0.1.0-alpha.15

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 (230) hide show
  1. package/AGENTS.md +6 -5
  2. package/CHANGELOG.md +22 -0
  3. package/adapters/java/README.md +68 -0
  4. package/adapters/java/pom.xml +34 -0
  5. package/adapters/java/src/main/java/dev/forgeos/adapter/Auth.java +20 -0
  6. package/adapters/java/src/main/java/dev/forgeos/adapter/Diagnostic.java +16 -0
  7. package/adapters/java/src/main/java/dev/forgeos/adapter/Entry.java +38 -0
  8. package/adapters/java/src/main/java/dev/forgeos/adapter/EntryKind.java +16 -0
  9. package/adapters/java/src/main/java/dev/forgeos/adapter/ErrorInfo.java +4 -0
  10. package/adapters/java/src/main/java/dev/forgeos/adapter/Forge.java +94 -0
  11. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeCall.java +12 -0
  12. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeContext.java +11 -0
  13. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHandler.java +8 -0
  14. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHttpHandler.java +179 -0
  15. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeRegistry.java +121 -0
  16. package/adapters/java/src/main/java/dev/forgeos/adapter/Json.java +14 -0
  17. package/adapters/java/src/main/java/dev/forgeos/adapter/Manifest.java +14 -0
  18. package/adapters/java/src/main/java/dev/forgeos/adapter/RequestEnvelope.java +6 -0
  19. package/adapters/java/src/main/java/dev/forgeos/adapter/ResponseEnvelope.java +25 -0
  20. package/adapters/java/src/main/java/dev/forgeos/adapter/Risk.java +18 -0
  21. package/adapters/java/src/main/java/dev/forgeos/adapter/Schemas.java +36 -0
  22. package/adapters/java/src/main/java/dev/forgeos/adapter/Service.java +65 -0
  23. package/adapters/java/src/main/java/dev/forgeos/adapter/TransactionMode.java +18 -0
  24. package/adapters/java/src/main/java/dev/forgeos/adapter/TypedForgeHandler.java +6 -0
  25. package/adapters/java/target/classes/dev/forgeos/adapter/Auth.class +0 -0
  26. package/adapters/java/target/classes/dev/forgeos/adapter/Diagnostic.class +0 -0
  27. package/adapters/java/target/classes/dev/forgeos/adapter/Entry.class +0 -0
  28. package/adapters/java/target/classes/dev/forgeos/adapter/EntryKind.class +0 -0
  29. package/adapters/java/target/classes/dev/forgeos/adapter/ErrorInfo.class +0 -0
  30. package/adapters/java/target/classes/dev/forgeos/adapter/Forge.class +0 -0
  31. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeCall.class +0 -0
  32. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeContext.class +0 -0
  33. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeHandler.class +0 -0
  34. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeHttpHandler.class +0 -0
  35. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$EntryOption.class +0 -0
  36. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$RegisteredEntry.class +0 -0
  37. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$RegistryOption.class +0 -0
  38. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry.class +0 -0
  39. package/adapters/java/target/classes/dev/forgeos/adapter/Json.class +0 -0
  40. package/adapters/java/target/classes/dev/forgeos/adapter/Manifest.class +0 -0
  41. package/adapters/java/target/classes/dev/forgeos/adapter/RequestEnvelope.class +0 -0
  42. package/adapters/java/target/classes/dev/forgeos/adapter/ResponseEnvelope.class +0 -0
  43. package/adapters/java/target/classes/dev/forgeos/adapter/Risk.class +0 -0
  44. package/adapters/java/target/classes/dev/forgeos/adapter/Schemas.class +0 -0
  45. package/adapters/java/target/classes/dev/forgeos/adapter/Service.class +0 -0
  46. package/adapters/java/target/classes/dev/forgeos/adapter/TransactionMode.class +0 -0
  47. package/adapters/java/target/classes/dev/forgeos/adapter/TypedForgeHandler.class +0 -0
  48. package/adapters/java/target/forge-java-adapter-0.1.0-alpha.11.jar +0 -0
  49. package/adapters/java/target/maven-archiver/pom.properties +3 -0
  50. package/adapters/java/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +23 -0
  51. package/adapters/java/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +20 -0
  52. package/adapters/java-spring-boot-starter/README.md +32 -0
  53. package/adapters/java-spring-boot-starter/pom.xml +36 -0
  54. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeCommand.java +22 -0
  55. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeExternalService.java +15 -0
  56. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeQuery.java +16 -0
  57. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.java +18 -0
  58. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.java +16 -0
  59. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringRuntime.java +104 -0
  60. package/adapters/java-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
  61. package/adapters/java-spring-boot-starter/target/classes/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
  62. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeCommand.class +0 -0
  63. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeExternalService.class +0 -0
  64. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeQuery.class +0 -0
  65. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.class +0 -0
  66. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.class +0 -0
  67. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeSpringRuntime.class +0 -0
  68. package/adapters/java-spring-boot-starter/target/forge-java-spring-boot-starter-0.1.0-alpha.11.jar +0 -0
  69. package/adapters/java-spring-boot-starter/target/maven-archiver/pom.properties +3 -0
  70. package/adapters/java-spring-boot-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +6 -0
  71. package/adapters/java-spring-boot-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +6 -0
  72. package/docs/changelog.md +8 -0
  73. package/docs/forge-protocol.md +33 -0
  74. package/examples/java-billing/pom.xml +52 -0
  75. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/CreateInvoiceInput.java +4 -0
  76. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Invoice.java +11 -0
  77. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Main.java +127 -0
  78. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/CreateInvoiceInput.class +0 -0
  79. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Invoice.class +0 -0
  80. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main$EmptyInput.class +0 -0
  81. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main$Options.class +0 -0
  82. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main.class +0 -0
  83. package/examples/java-billing/target/java-billing-0.1.0-alpha.11-all.jar +0 -0
  84. package/examples/java-billing/target/java-billing-0.1.0-alpha.11.jar +0 -0
  85. package/examples/java-billing/target/maven-archiver/pom.properties +3 -0
  86. package/examples/java-billing/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +5 -0
  87. package/examples/java-billing/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +3 -0
  88. package/package.json +4 -1
  89. package/src/forge/_generated/actionSubscriptions.json +1 -1
  90. package/src/forge/_generated/actionSubscriptions.ts +3 -3
  91. package/src/forge/_generated/agentAdapterManifest.json +1 -1
  92. package/src/forge/_generated/agentAdapterManifest.ts +3 -3
  93. package/src/forge/_generated/agentContract.json +1 -1
  94. package/src/forge/_generated/agentContract.ts +1274 -7
  95. package/src/forge/_generated/agentQuickstart.md +1 -1
  96. package/src/forge/_generated/agentTools.json +1 -1
  97. package/src/forge/_generated/agentTools.md +1 -1
  98. package/src/forge/_generated/agentTools.ts +2 -2
  99. package/src/forge/_generated/aiContext.ts +1 -1
  100. package/src/forge/_generated/aiModels.ts +1 -1
  101. package/src/forge/_generated/aiProviders.ts +1 -1
  102. package/src/forge/_generated/aiRegistry.json +1 -1
  103. package/src/forge/_generated/aiRegistry.ts +3 -3
  104. package/src/forge/_generated/api.json +1 -1
  105. package/src/forge/_generated/api.ts +1 -1
  106. package/src/forge/_generated/appGraph.json +1 -1
  107. package/src/forge/_generated/appGraph.ts +165 -98
  108. package/src/forge/_generated/appMap.md +1 -1
  109. package/src/forge/_generated/artifactManifest.json +1 -1
  110. package/src/forge/_generated/artifactManifest.ts +2 -2
  111. package/src/forge/_generated/authClaims.ts +1 -1
  112. package/src/forge/_generated/authConfig.ts +1 -1
  113. package/src/forge/_generated/authContext.ts +1 -1
  114. package/src/forge/_generated/authRegistry.ts +1 -1
  115. package/src/forge/_generated/buildInfo.json +1 -1
  116. package/src/forge/_generated/buildInfo.ts +4 -4
  117. package/src/forge/_generated/capabilityMap.json +1 -1
  118. package/src/forge/_generated/capabilityMap.md +1 -1
  119. package/src/forge/_generated/capabilityMap.ts +2 -2
  120. package/src/forge/_generated/client.ts +1 -1
  121. package/src/forge/_generated/clientApi.ts +1 -1
  122. package/src/forge/_generated/clientManifest.json +1 -1
  123. package/src/forge/_generated/clientManifest.ts +14 -3
  124. package/src/forge/_generated/clientTypes.ts +1 -1
  125. package/src/forge/_generated/configRegistry.ts +1 -1
  126. package/src/forge/_generated/dataGraph.json +1 -1
  127. package/src/forge/_generated/dataGraph.ts +3 -3
  128. package/src/forge/_generated/db.ts +1 -1
  129. package/src/forge/_generated/dbSecurityManifest.ts +1 -1
  130. package/src/forge/_generated/dbSessionContext.ts +1 -1
  131. package/src/forge/_generated/deployManifest.json +1 -1
  132. package/src/forge/_generated/deployManifest.ts +7 -7
  133. package/src/forge/_generated/devManifest.json +1 -1
  134. package/src/forge/_generated/devManifest.ts +3 -3
  135. package/src/forge/_generated/envSchema.ts +1 -1
  136. package/src/forge/_generated/externalServices.ts +1 -1
  137. package/src/forge/_generated/frontendGraph.ts +1 -1
  138. package/src/forge/_generated/importGuards.json +1 -1
  139. package/src/forge/_generated/importGuards.ts +35 -1
  140. package/src/forge/_generated/index.ts +3 -1
  141. package/src/forge/_generated/liveProductionManifest.ts +1 -1
  142. package/src/forge/_generated/liveProtocol.ts +1 -1
  143. package/src/forge/_generated/liveQueryRegistry.json +1 -1
  144. package/src/forge/_generated/liveQueryRegistry.ts +3 -3
  145. package/src/forge/_generated/liveTransportConfig.ts +1 -1
  146. package/src/forge/_generated/makeRegistry.json +1 -1
  147. package/src/forge/_generated/makeRegistry.ts +8 -8
  148. package/src/forge/_generated/makeTemplates.json +1 -1
  149. package/src/forge/_generated/makeTemplates.ts +2 -2
  150. package/src/forge/_generated/mockMap.ts +1 -1
  151. package/src/forge/_generated/operationPlaybooks.md +6 -6
  152. package/src/forge/_generated/packageGraph.json +1 -1
  153. package/src/forge/_generated/packageGraph.ts +52412 -2
  154. package/src/forge/_generated/packageUpgradeRegistry.json +1 -1
  155. package/src/forge/_generated/packageUpgradeRegistry.ts +2 -2
  156. package/src/forge/_generated/permissionMatrix.json +1 -1
  157. package/src/forge/_generated/permissionMatrix.ts +3 -3
  158. package/src/forge/_generated/policyRegistry.json +1 -1
  159. package/src/forge/_generated/policyRegistry.ts +3 -3
  160. package/src/forge/_generated/queryRegistry.json +1 -1
  161. package/src/forge/_generated/queryRegistry.ts +3 -3
  162. package/src/forge/_generated/react.d.ts +1 -1
  163. package/src/forge/_generated/react.ts +1 -1
  164. package/src/forge/_generated/reactManifest.json +1 -1
  165. package/src/forge/_generated/reactManifest.ts +3 -3
  166. package/src/forge/_generated/releaseManifest.json +1 -1
  167. package/src/forge/_generated/releaseManifest.ts +3 -3
  168. package/src/forge/_generated/rlsPolicies.sql +1 -1
  169. package/src/forge/_generated/rlsPolicies.ts +1 -1
  170. package/src/forge/_generated/runtimeGraph.json +1 -1
  171. package/src/forge/_generated/runtimeGraph.ts +3 -3
  172. package/src/forge/_generated/runtimeMatrix.json +1 -1
  173. package/src/forge/_generated/runtimeMatrix.ts +70357 -1
  174. package/src/forge/_generated/runtimeRegistry.ts +1 -1
  175. package/src/forge/_generated/runtimeRules.md +1 -1
  176. package/src/forge/_generated/secretRegistry.ts +1 -1
  177. package/src/forge/_generated/secretsContext.ts +1 -1
  178. package/src/forge/_generated/serverApi.ts +1 -1
  179. package/src/forge/_generated/sourceMapManifest.json +1 -1
  180. package/src/forge/_generated/sourceMapManifest.ts +2 -2
  181. package/src/forge/_generated/sqlPlan.ts +1 -1
  182. package/src/forge/_generated/subscriptionManifest.json +1 -1
  183. package/src/forge/_generated/subscriptionManifest.ts +3 -3
  184. package/src/forge/_generated/symbolicationManifest.json +1 -1
  185. package/src/forge/_generated/symbolicationManifest.ts +2 -2
  186. package/src/forge/_generated/telemetryRegistry.json +1 -1
  187. package/src/forge/_generated/telemetryRegistry.ts +3 -3
  188. package/src/forge/_generated/telemetrySinks.json +1 -1
  189. package/src/forge/_generated/telemetrySinks.ts +2 -2
  190. package/src/forge/_generated/tenantScope.json +1 -1
  191. package/src/forge/_generated/tenantScope.ts +3 -3
  192. package/src/forge/_generated/testGraph.json +1 -1
  193. package/src/forge/_generated/testGraph.ts +95 -5
  194. package/src/forge/_generated/testPlanRegistry.json +1 -1
  195. package/src/forge/_generated/testPlanRegistry.ts +2 -2
  196. package/src/forge/_generated/uiRoutes.ts +1 -1
  197. package/src/forge/_generated/uiScenarios.ts +1 -1
  198. package/src/forge/_generated/uiTestManifest.json +1 -1
  199. package/src/forge/_generated/uiTestManifest.ts +2 -2
  200. package/src/forge/_generated/vue.d.ts +25 -0
  201. package/src/forge/_generated/vue.ts +30 -0
  202. package/src/forge/_generated/vueManifest.json +1 -0
  203. package/src/forge/_generated/vueManifest.ts +19 -0
  204. package/src/forge/_generated/workflowRegistry.json +1 -1
  205. package/src/forge/_generated/workflowRegistry.ts +3 -3
  206. package/src/forge/_generated/workflowSubscriptions.json +1 -1
  207. package/src/forge/_generated/workflowSubscriptions.ts +3 -3
  208. package/src/forge/agent-adapters/index.ts +1 -1
  209. package/src/forge/brownfield-import/index.ts +726 -0
  210. package/src/forge/brownfield-import/types.ts +127 -0
  211. package/src/forge/cli/commands.ts +27 -0
  212. package/src/forge/cli/parse.ts +27 -1
  213. package/src/forge/cli/verify.ts +2 -0
  214. package/src/forge/compiler/agent-contract/build.ts +10 -9
  215. package/src/forge/compiler/client-sdk/build-manifest.ts +55 -0
  216. package/src/forge/compiler/client-sdk/render-client.ts +66 -1
  217. package/src/forge/compiler/diagnostics/create.ts +6 -1
  218. package/src/forge/compiler/emitter/barrel.ts +3 -0
  219. package/src/forge/compiler/frontend-graph/build.ts +85 -13
  220. package/src/forge/compiler/make-registry/build.ts +6 -7
  221. package/src/forge/compiler/orchestrator/plan.ts +13 -0
  222. package/src/forge/compiler/orchestrator/serialize.ts +6 -0
  223. package/src/forge/compiler/types/cli.ts +1 -0
  224. package/src/forge/compiler/types/frontend-graph.ts +2 -2
  225. package/src/forge/intent/index.ts +11 -6
  226. package/src/forge/make/index.ts +23 -3
  227. package/src/forge/make/templates.ts +153 -0
  228. package/src/forge/make/types.ts +2 -1
  229. package/src/forge/version.ts +1 -1
  230. package/src/forge/vue/index.ts +407 -0
@@ -0,0 +1,726 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, extname, join, relative, sep } from "node:path";
3
+ import type {
4
+ BrownfieldImportArtifacts,
5
+ BrownfieldImportCommandOptions,
6
+ BrownfieldImportResult,
7
+ ImportedCandidateEntry,
8
+ ImportedDependencyInventory,
9
+ ImportedFrontendCall,
10
+ ImportedInventory,
11
+ ImportedRiskFinding,
12
+ ImportedRiskReport,
13
+ ImportedRoute,
14
+ ImportedRouteSource,
15
+ } from "./types.ts";
16
+
17
+ const IMPORT_DIR = ".forge/import";
18
+ const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
19
+ const IGNORED_DIRS = new Set([
20
+ ".git",
21
+ ".forge",
22
+ ".next",
23
+ ".nuxt",
24
+ ".output",
25
+ "__tests__",
26
+ "_generated",
27
+ "coverage",
28
+ "dist",
29
+ "build",
30
+ "node_modules",
31
+ "out",
32
+ "target",
33
+ "test",
34
+ "tests",
35
+ ]);
36
+ const PREFERRED_SOURCE_ROOTS = ["src", "app", "pages", "server", "web", "apps", "packages"];
37
+
38
+ export const BROWNFIELD_IMPORT_ARTIFACTS: BrownfieldImportArtifacts = {
39
+ inventory: `${IMPORT_DIR}/inventory.json`,
40
+ routes: `${IMPORT_DIR}/routes.json`,
41
+ frontendCalls: `${IMPORT_DIR}/frontendCalls.json`,
42
+ candidateEntries: `${IMPORT_DIR}/candidateEntries.json`,
43
+ riskReport: `${IMPORT_DIR}/riskReport.json`,
44
+ migrationPlan: `${IMPORT_DIR}/migrationPlan.md`,
45
+ importedAgentContract: `${IMPORT_DIR}/importedAgentContract.json`,
46
+ };
47
+
48
+ interface SourceFile {
49
+ relativePath: string;
50
+ absolutePath: string;
51
+ text: string;
52
+ }
53
+
54
+ function normalizePath(path: string): string {
55
+ return path.split(sep).join("/");
56
+ }
57
+
58
+ function artifactPath(workspaceRoot: string, relativePath: string): string {
59
+ return join(workspaceRoot, ...relativePath.split("/"));
60
+ }
61
+
62
+ function readJson<T>(path: string): T | null {
63
+ if (!existsSync(path)) {
64
+ return null;
65
+ }
66
+ try {
67
+ return JSON.parse(readFileSync(path, "utf8")) as T;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ function readPackageJson(workspaceRoot: string): Record<string, unknown> {
74
+ return readJson<Record<string, unknown>>(join(workspaceRoot, "package.json")) ?? {};
75
+ }
76
+
77
+ function objectKeys(value: unknown): string[] {
78
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
79
+ return [];
80
+ }
81
+ return Object.keys(value as Record<string, unknown>).sort();
82
+ }
83
+
84
+ function collectSourceFiles(workspaceRoot: string): SourceFile[] {
85
+ const files: SourceFile[] = [];
86
+ const visit = (absoluteDir: string): void => {
87
+ for (const entry of readdirSync(absoluteDir, { withFileTypes: true })) {
88
+ const absolutePath = join(absoluteDir, entry.name);
89
+ const relativePath = normalizePath(relative(workspaceRoot, absolutePath));
90
+ if (entry.isDirectory()) {
91
+ if (
92
+ !entry.name.startsWith(".") &&
93
+ !IGNORED_DIRS.has(entry.name) &&
94
+ !relativePath.includes("/src/forge/_generated")
95
+ ) {
96
+ visit(absolutePath);
97
+ }
98
+ continue;
99
+ }
100
+ if (!entry.isFile() || !SOURCE_EXTENSIONS.has(extname(entry.name))) {
101
+ continue;
102
+ }
103
+ if (statSync(absolutePath).size > 1_000_000) {
104
+ continue;
105
+ }
106
+ files.push({
107
+ absolutePath,
108
+ relativePath,
109
+ text: readFileSync(absolutePath, "utf8"),
110
+ });
111
+ }
112
+ };
113
+ const preferredRoots = PREFERRED_SOURCE_ROOTS
114
+ .map((name) => join(workspaceRoot, name))
115
+ .filter((absolutePath) => existsSync(absolutePath) && statSync(absolutePath).isDirectory());
116
+ const roots = preferredRoots.length > 0 ? preferredRoots : [workspaceRoot];
117
+ for (const root of roots) {
118
+ visit(root);
119
+ }
120
+ return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
121
+ }
122
+
123
+ function hasAny(names: string[], candidates: string[]): boolean {
124
+ return candidates.some((candidate) => names.includes(candidate));
125
+ }
126
+
127
+ function buildDependencyInventory(workspaceRoot: string): {
128
+ packageName?: string;
129
+ dependencies: ImportedDependencyInventory;
130
+ } {
131
+ const pkg = readPackageJson(workspaceRoot);
132
+ const dependencies = objectKeys(pkg.dependencies);
133
+ const devDependencies = objectKeys(pkg.devDependencies);
134
+ const all = [...dependencies, ...devDependencies];
135
+ const scripts = objectKeys(pkg.scripts);
136
+ const frameworks = [
137
+ hasAny(all, ["next"]) ? "next" : null,
138
+ hasAny(all, ["react"]) ? "react" : null,
139
+ hasAny(all, ["vue", "nuxt"]) ? "vue" : null,
140
+ hasAny(all, ["nuxt"]) ? "nuxt" : null,
141
+ hasAny(all, ["express"]) ? "express" : null,
142
+ hasAny(all, ["@nestjs/core"]) ? "nest" : null,
143
+ hasAny(all, ["fastify"]) ? "fastify" : null,
144
+ hasAny(all, ["hono"]) ? "hono" : null,
145
+ ].filter((value): value is string => value !== null);
146
+ const dataPackages = all.filter((name) =>
147
+ ["@prisma/client", "prisma", "drizzle-orm", "typeorm", "mongoose", "sequelize", "knex"].includes(name),
148
+ );
149
+ const externalPackages = all.filter((name) =>
150
+ [
151
+ "stripe",
152
+ "resend",
153
+ "nodemailer",
154
+ "openai",
155
+ "@ai-sdk/openai",
156
+ "@ai-sdk/anthropic",
157
+ "aws-sdk",
158
+ "@aws-sdk/client-s3",
159
+ "sendgrid",
160
+ "@sendgrid/mail",
161
+ "twilio",
162
+ ].includes(name),
163
+ );
164
+ return {
165
+ packageName: typeof pkg.name === "string" ? pkg.name : undefined,
166
+ dependencies: {
167
+ dependencies,
168
+ devDependencies,
169
+ scripts,
170
+ frameworks: Array.from(new Set(frameworks)).sort(),
171
+ dataPackages,
172
+ externalPackages,
173
+ },
174
+ };
175
+ }
176
+
177
+ function stableId(prefix: string, parts: string[]): string {
178
+ let hash = 2166136261;
179
+ for (const char of parts.join("|")) {
180
+ hash ^= char.charCodeAt(0);
181
+ hash = Math.imul(hash, 16777619);
182
+ }
183
+ return `${prefix}_${(hash >>> 0).toString(36)}`;
184
+ }
185
+
186
+ function stripExtension(segment: string): string {
187
+ return segment.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/u, "");
188
+ }
189
+
190
+ function normalizeRouteSegment(segment: string): string | null {
191
+ const clean = stripExtension(segment);
192
+ if (clean === "index" || clean === "route" || clean === "page") {
193
+ return null;
194
+ }
195
+ if (clean.startsWith("(") && clean.endsWith(")")) {
196
+ return null;
197
+ }
198
+ const catchAll = clean.match(/^\[\.\.\.(.+)\]$/u);
199
+ if (catchAll) {
200
+ return `:${catchAll[1]}*`;
201
+ }
202
+ const dynamic = clean.match(/^\[(.+)\]$/u);
203
+ if (dynamic) {
204
+ return `:${dynamic[1]}`;
205
+ }
206
+ return clean;
207
+ }
208
+
209
+ function routePathFromFile(relativePath: string, marker: string): string {
210
+ const afterMarker = relativePath.slice(relativePath.indexOf(marker) + marker.length);
211
+ const segments = afterMarker
212
+ .split("/")
213
+ .map(normalizeRouteSegment)
214
+ .filter((segment): segment is string => Boolean(segment));
215
+ return `/${segments.join("/")}`.replace(/\/+/gu, "/");
216
+ }
217
+
218
+ function joinRoutePath(base: string, child: string): string {
219
+ return `/${[base, child].map((part) => part.replace(/^\/|\/$/gu, "")).filter(Boolean).join("/")}`.replace(/\/+/gu, "/");
220
+ }
221
+
222
+ function addRoute(
223
+ routes: ImportedRoute[],
224
+ method: string,
225
+ path: string,
226
+ file: string,
227
+ source: ImportedRouteSource,
228
+ confidence: number,
229
+ handler?: string,
230
+ ): void {
231
+ routes.push({
232
+ id: stableId("route", [method.toUpperCase(), path, file, source, handler ?? ""]),
233
+ method: method.toUpperCase(),
234
+ path,
235
+ file,
236
+ source,
237
+ handler,
238
+ confidence,
239
+ });
240
+ }
241
+
242
+ function detectRoutes(files: SourceFile[]): ImportedRoute[] {
243
+ const routes: ImportedRoute[] = [];
244
+ const methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
245
+ for (const file of files) {
246
+ if (file.relativePath.includes("/app/api/") && basename(file.relativePath).startsWith("route.")) {
247
+ const path = routePathFromFile(file.relativePath, "/app/");
248
+ for (const method of methods) {
249
+ if (new RegExp(`export\\s+(?:async\\s+)?function\\s+${method}\\b`, "u").test(file.text)) {
250
+ addRoute(routes, method, path, file.relativePath, "next-app-router", 0.92, method);
251
+ }
252
+ }
253
+ }
254
+
255
+ if (file.relativePath.includes("/pages/api/")) {
256
+ const path = routePathFromFile(file.relativePath, "/pages/");
257
+ addRoute(routes, "ANY", path, file.relativePath, "next-pages-api", 0.78, "default");
258
+ }
259
+
260
+ const expressRoute = /\b(?:app|router)\s*\.\s*(get|post|put|patch|delete|all)\s*\(\s*["'`]([^"'`]+)["'`]/giu;
261
+ for (const match of file.text.matchAll(expressRoute)) {
262
+ addRoute(routes, match[1] ?? "all", match[2] ?? "/", file.relativePath, "express", 0.84);
263
+ }
264
+
265
+ const controller = file.text.match(/@Controller\s*\(\s*["'`]([^"'`]*)["'`]\s*\)/u);
266
+ if (controller) {
267
+ const nestRoute = /@(Get|Post|Put|Patch|Delete|All)\s*\(\s*(?:["'`]([^"'`]*)["'`])?\s*\)/giu;
268
+ for (const match of file.text.matchAll(nestRoute)) {
269
+ addRoute(
270
+ routes,
271
+ match[1] ?? "All",
272
+ joinRoutePath(controller[1] ?? "", match[2] ?? ""),
273
+ file.relativePath,
274
+ "nest",
275
+ 0.78,
276
+ );
277
+ }
278
+ }
279
+ }
280
+ const seen = new Set<string>();
281
+ return routes
282
+ .filter((route) => {
283
+ const key = `${route.method}:${route.path}:${route.file}:${route.source}`;
284
+ if (seen.has(key)) {
285
+ return false;
286
+ }
287
+ seen.add(key);
288
+ return true;
289
+ })
290
+ .sort((left, right) => `${left.path}:${left.method}`.localeCompare(`${right.path}:${right.method}`));
291
+ }
292
+
293
+ function detectFrontendCalls(files: SourceFile[], routes: ImportedRoute[]): ImportedFrontendCall[] {
294
+ const calls: ImportedFrontendCall[] = [];
295
+ const addCall = (file: string, client: "fetch" | "axios", method: string, url: string): void => {
296
+ const route = routes.find((candidate) => candidate.path === url || url.startsWith(`${candidate.path}/`));
297
+ calls.push({
298
+ id: stableId("call", [file, client, method, url]),
299
+ file,
300
+ client,
301
+ method: method.toUpperCase(),
302
+ url,
303
+ routeId: route?.id,
304
+ confidence: route ? 0.78 : 0.55,
305
+ });
306
+ };
307
+ for (const file of files) {
308
+ if (file.relativePath.includes("/app/api/") || file.relativePath.includes("/pages/api/")) {
309
+ continue;
310
+ }
311
+ const fetchCall = /\bfetch\s*\(\s*["'`]([^"'`]+)["'`]\s*(?:,\s*\{(?<options>[\s\S]{0,300}?)\})?/giu;
312
+ for (const match of file.text.matchAll(fetchCall)) {
313
+ const url = match[1] ?? "";
314
+ if (!url.startsWith("/api/") && !url.startsWith("http")) {
315
+ continue;
316
+ }
317
+ const options = match.groups?.options ?? "";
318
+ const method = options.match(/method\s*:\s*["'`]([A-Z]+)["'`]/iu)?.[1] ?? "GET";
319
+ addCall(file.relativePath, "fetch", method, url);
320
+ }
321
+ const axiosMethodCall = /\baxios\.(get|post|put|patch|delete)\s*\(\s*["'`]([^"'`]+)["'`]/giu;
322
+ for (const match of file.text.matchAll(axiosMethodCall)) {
323
+ const url = match[2] ?? "";
324
+ if (url.startsWith("/api/") || url.startsWith("http")) {
325
+ addCall(file.relativePath, "axios", match[1] ?? "GET", url);
326
+ }
327
+ }
328
+ }
329
+ return calls.sort((left, right) => `${left.file}:${left.url}`.localeCompare(`${right.file}:${right.url}`));
330
+ }
331
+
332
+ function collectEnv(workspaceRoot: string, files: SourceFile[]): ImportedInventory["env"] {
333
+ const names = new Set<string>();
334
+ for (const file of files) {
335
+ for (const match of file.text.matchAll(/\bprocess\.env\.([A-Z0-9_]+)/gu)) {
336
+ names.add(match[1] ?? "");
337
+ }
338
+ }
339
+ const envFiles = [".env", ".env.local", ".env.example", ".env.sample"]
340
+ .filter((name) => existsSync(join(workspaceRoot, name)))
341
+ .sort();
342
+ for (const envFile of envFiles) {
343
+ const raw = readFileSync(join(workspaceRoot, envFile), "utf8");
344
+ for (const line of raw.split(/\r?\n/u)) {
345
+ const match = line.match(/^\s*([A-Z0-9_]+)\s*=/u);
346
+ if (match) {
347
+ names.add(match[1] ?? "");
348
+ }
349
+ }
350
+ }
351
+ return {
352
+ processEnv: Array.from(names).filter(Boolean).sort(),
353
+ envFiles,
354
+ };
355
+ }
356
+
357
+ function sourceTextForRoute(route: ImportedRoute, files: SourceFile[]): string {
358
+ return files.find((file) => file.relativePath === route.file)?.text ?? "";
359
+ }
360
+
361
+ function classifyCandidate(route: ImportedRoute, text: string): Pick<ImportedCandidateEntry, "kind" | "confidence" | "risks" | "evidence" | "needsApproval"> {
362
+ const method = route.method.toUpperCase();
363
+ const lowerPath = route.path.toLowerCase();
364
+ const lowerText = text.toLowerCase();
365
+ const risks = new Set<string>();
366
+ const evidence: string[] = [`${route.source} ${method} ${route.path}`];
367
+ const isQuery = method === "GET" || method === "HEAD";
368
+ const isDestructive = method === "DELETE" || /(delete|remove|destroy|cancel|refund|void|purge)/u.test(lowerPath);
369
+ const writes = /(\.create\(|\.update\(|\.delete\(|\.upsert\(|\.insert\(|\.save\(|\.destroy\(|\.remove\()/u.test(lowerText);
370
+ const external = /(stripe|resend|sendgrid|twilio|nodemailer|openai|anthropic|https?:\/\/|\.send\()/u.test(lowerText);
371
+ const auth = /(auth|session|currentuser|getserversession|clerk|nextauth|requireuser|requireauth)/u.test(lowerText);
372
+ const tenant = /(tenantid|tenant_id|organizationid|orgid|accountid)/u.test(lowerText);
373
+ const methodUnknown = method === "ANY" || method === "ALL";
374
+ if (!isQuery || writes) {
375
+ risks.add("writes-state");
376
+ }
377
+ if (isDestructive) {
378
+ risks.add("destructive");
379
+ }
380
+ if (external) {
381
+ risks.add("external-side-effect");
382
+ }
383
+ if (!auth) {
384
+ risks.add("auth-unknown");
385
+ }
386
+ if (tenant) {
387
+ risks.add("tenant-sensitive");
388
+ }
389
+ if (/\bprocess\.env\./u.test(text)) {
390
+ risks.add("secret-sensitive");
391
+ }
392
+ if (methodUnknown) {
393
+ risks.add("method-unknown");
394
+ }
395
+ const commandLike = !isQuery || writes || isDestructive || external;
396
+ return {
397
+ kind: commandLike ? "command" : "query",
398
+ confidence: commandLike ? (isDestructive ? 0.9 : 0.78) : 0.86,
399
+ risks: Array.from(risks).sort(),
400
+ evidence,
401
+ needsApproval: commandLike || external || isDestructive || methodUnknown,
402
+ };
403
+ }
404
+
405
+ function nameForCandidate(route: ImportedRoute, kind: "command" | "query" | "unknown"): string {
406
+ const nouns = route.path
407
+ .replace(/^\/api\//u, "")
408
+ .replace(/:\w+\*?/gu, "byId")
409
+ .split("/")
410
+ .filter(Boolean)
411
+ .map((segment) => segment.replace(/[^a-zA-Z0-9]/gu, ""))
412
+ .filter(Boolean);
413
+ const base = nouns.length > 0 ? nouns.join(".") : "imported.route";
414
+ const method = route.method.toUpperCase();
415
+ const action =
416
+ kind === "query" ? "read" :
417
+ method === "POST" ? "create" :
418
+ method === "PUT" || method === "PATCH" ? "update" :
419
+ method === "DELETE" ? "delete" :
420
+ "call";
421
+ return `${base}.${action}`;
422
+ }
423
+
424
+ function buildCandidates(routes: ImportedRoute[], files: SourceFile[]): ImportedCandidateEntry[] {
425
+ const usedNames = new Map<string, number>();
426
+ return routes.map((route) => {
427
+ const text = sourceTextForRoute(route, files);
428
+ const classification = classifyCandidate(route, text);
429
+ const baseName = nameForCandidate(route, classification.kind);
430
+ const count = usedNames.get(baseName) ?? 0;
431
+ usedNames.set(baseName, count + 1);
432
+ const name = count === 0 ? baseName : `${baseName}${count + 1}`;
433
+ return {
434
+ id: stableId("entry", [route.id, name]),
435
+ name,
436
+ kind: classification.kind,
437
+ method: route.method,
438
+ path: route.path,
439
+ routeId: route.id,
440
+ file: route.file,
441
+ origin: "imported",
442
+ assurance: "static-scan",
443
+ reviewStatus: "needs-review",
444
+ visibleToAgent: false,
445
+ needsApproval: classification.needsApproval,
446
+ confidence: classification.confidence,
447
+ risks: classification.risks,
448
+ evidence: classification.evidence,
449
+ };
450
+ });
451
+ }
452
+
453
+ function buildRiskReport(
454
+ routes: ImportedRoute[],
455
+ frontendCalls: ImportedFrontendCall[],
456
+ candidates: ImportedCandidateEntry[],
457
+ files: SourceFile[],
458
+ ): ImportedRiskReport {
459
+ const findings: ImportedRiskFinding[] = [];
460
+ for (const candidate of candidates) {
461
+ if (candidate.visibleToAgent) {
462
+ findings.push({
463
+ code: "FORGE_IMPORT_VISIBLE",
464
+ severity: "error",
465
+ file: candidate.file,
466
+ routeId: candidate.routeId,
467
+ message: "Imported entries must stay hidden from agents until a human approves them.",
468
+ });
469
+ }
470
+ if (candidate.risks.includes("auth-unknown") && candidate.kind === "command") {
471
+ findings.push({
472
+ code: "FORGE_IMPORT_AUTH_UNKNOWN",
473
+ severity: "warning",
474
+ file: candidate.file,
475
+ routeId: candidate.routeId,
476
+ message: `${candidate.name} looks command-like but static scan did not find an obvious auth guard.`,
477
+ });
478
+ }
479
+ if (candidate.risks.includes("destructive")) {
480
+ findings.push({
481
+ code: "FORGE_IMPORT_DESTRUCTIVE",
482
+ severity: "warning",
483
+ file: candidate.file,
484
+ routeId: candidate.routeId,
485
+ message: `${candidate.name} is destructive and must keep needsApproval=true.`,
486
+ });
487
+ }
488
+ const text = sourceTextForRoute({ ...candidate, id: candidate.routeId, source: "unknown", confidence: 0 }, files);
489
+ if (/\b(req\.body|body|input)\.tenantId\b/u.test(text) || /\b(req\.body|body|input)\.tenant_id\b/u.test(text)) {
490
+ findings.push({
491
+ code: "FORGE_IMPORT_TENANT_SPOOFABLE",
492
+ severity: "warning",
493
+ file: candidate.file,
494
+ routeId: candidate.routeId,
495
+ message: `${candidate.name} appears to accept tenant identity from input; review tenant isolation before migration.`,
496
+ });
497
+ }
498
+ }
499
+ return {
500
+ schemaVersion: "0.1.0",
501
+ summary: {
502
+ routeCount: routes.length,
503
+ frontendCallCount: frontendCalls.length,
504
+ candidateCount: candidates.length,
505
+ commandCount: candidates.filter((candidate) => candidate.kind === "command").length,
506
+ queryCount: candidates.filter((candidate) => candidate.kind === "query").length,
507
+ hiddenFromAgents: candidates.filter((candidate) => !candidate.visibleToAgent).length,
508
+ needsApproval: candidates.filter((candidate) => candidate.needsApproval).length,
509
+ },
510
+ findings,
511
+ };
512
+ }
513
+
514
+ function buildInventory(workspaceRoot: string, files: SourceFile[]): ImportedInventory {
515
+ const dependencyInventory = buildDependencyInventory(workspaceRoot);
516
+ return {
517
+ schemaVersion: "0.1.0",
518
+ origin: "imported",
519
+ assurance: "static-scan",
520
+ workspaceRoot,
521
+ generatedAt: new Date().toISOString(),
522
+ packageName: dependencyInventory.packageName,
523
+ dependencies: dependencyInventory.dependencies,
524
+ filesScanned: files.length,
525
+ sourceFiles: files.map((file) => file.relativePath),
526
+ env: collectEnv(workspaceRoot, files),
527
+ };
528
+ }
529
+
530
+ function buildMigrationPlan(
531
+ inventory: ImportedInventory,
532
+ routes: ImportedRoute[],
533
+ frontendCalls: ImportedFrontendCall[],
534
+ candidates: ImportedCandidateEntry[],
535
+ riskReport: ImportedRiskReport,
536
+ ): string {
537
+ const lines = [
538
+ "# Brownfield Import Migration Plan",
539
+ "",
540
+ "This plan was produced by `forge import analyze` from static evidence only. Imported entries are hidden from agents until reviewed.",
541
+ "",
542
+ "## Summary",
543
+ "",
544
+ `- Package: ${inventory.packageName ?? "unknown"}`,
545
+ `- Files scanned: ${inventory.filesScanned}`,
546
+ `- Routes detected: ${routes.length}`,
547
+ `- Frontend calls detected: ${frontendCalls.length}`,
548
+ `- Candidate entries: ${candidates.length}`,
549
+ `- Hidden from agents: ${riskReport.summary.hiddenFromAgents}`,
550
+ `- Entries requiring approval: ${riskReport.summary.needsApproval}`,
551
+ "",
552
+ "## Review Order",
553
+ "",
554
+ "1. Review destructive and external-side-effect candidates first.",
555
+ "2. Confirm auth and tenant boundaries before exposing any imported entry.",
556
+ "3. Convert read-only GET candidates into Forge queries only after validating schema ownership.",
557
+ "4. Convert mutating candidates into Forge commands/actions with `ctx.emit` or durable workflows for side effects.",
558
+ "5. Replace frontend raw API calls with generated Forge client bindings after each reviewed migration.",
559
+ "",
560
+ "## Candidate Entries",
561
+ "",
562
+ ...candidates.map((candidate) =>
563
+ `- \`${candidate.name}\` (${candidate.kind}, ${candidate.method} ${candidate.path}) - confidence ${candidate.confidence.toFixed(2)}, risks: ${candidate.risks.join(", ") || "none"}`,
564
+ ),
565
+ "",
566
+ "## Findings",
567
+ "",
568
+ ...(riskReport.findings.length === 0
569
+ ? ["- No high-signal risk findings beyond conservative imported defaults."]
570
+ : riskReport.findings.map((finding) => `- ${finding.severity.toUpperCase()} ${finding.code}: ${finding.message}`)),
571
+ "",
572
+ ];
573
+ return `${lines.join("\n")}\n`;
574
+ }
575
+
576
+ function buildImportedAgentContract(
577
+ inventory: ImportedInventory,
578
+ routes: ImportedRoute[],
579
+ frontendCalls: ImportedFrontendCall[],
580
+ candidates: ImportedCandidateEntry[],
581
+ riskReport: ImportedRiskReport,
582
+ ): Record<string, unknown> {
583
+ return {
584
+ schemaVersion: "0.1.0",
585
+ origin: "imported",
586
+ assurance: "static-scan",
587
+ reviewStatus: "needs-review",
588
+ visibleToAgent: false,
589
+ generatedAt: inventory.generatedAt,
590
+ summary: riskReport.summary,
591
+ frameworks: inventory.dependencies.frameworks,
592
+ routes,
593
+ frontendCalls,
594
+ entries: candidates,
595
+ findings: riskReport.findings,
596
+ };
597
+ }
598
+
599
+ function writeArtifact(workspaceRoot: string, relativePath: string, value: unknown): void {
600
+ const absolute = artifactPath(workspaceRoot, relativePath);
601
+ mkdirSync(dirname(absolute), { recursive: true });
602
+ const content = typeof value === "string" ? value : `${JSON.stringify(value, null, 2)}\n`;
603
+ writeFileSync(absolute, content, "utf8");
604
+ }
605
+
606
+ function analyze(workspaceRoot: string, dryRun: boolean): BrownfieldImportResult {
607
+ const files = collectSourceFiles(workspaceRoot);
608
+ const inventory = buildInventory(workspaceRoot, files);
609
+ const routes = detectRoutes(files);
610
+ const frontendCalls = detectFrontendCalls(files, routes);
611
+ const candidateEntries = buildCandidates(routes, files);
612
+ const riskReport = buildRiskReport(routes, frontendCalls, candidateEntries, files);
613
+ const migrationPlan = buildMigrationPlan(inventory, routes, frontendCalls, candidateEntries, riskReport);
614
+ const importedAgentContract = buildImportedAgentContract(inventory, routes, frontendCalls, candidateEntries, riskReport);
615
+ if (!dryRun) {
616
+ writeArtifact(workspaceRoot, BROWNFIELD_IMPORT_ARTIFACTS.inventory, inventory);
617
+ writeArtifact(workspaceRoot, BROWNFIELD_IMPORT_ARTIFACTS.routes, routes);
618
+ writeArtifact(workspaceRoot, BROWNFIELD_IMPORT_ARTIFACTS.frontendCalls, frontendCalls);
619
+ writeArtifact(workspaceRoot, BROWNFIELD_IMPORT_ARTIFACTS.candidateEntries, candidateEntries);
620
+ writeArtifact(workspaceRoot, BROWNFIELD_IMPORT_ARTIFACTS.riskReport, riskReport);
621
+ writeArtifact(workspaceRoot, BROWNFIELD_IMPORT_ARTIFACTS.migrationPlan, migrationPlan);
622
+ writeArtifact(workspaceRoot, BROWNFIELD_IMPORT_ARTIFACTS.importedAgentContract, importedAgentContract);
623
+ }
624
+ return {
625
+ schemaVersion: "0.1.0",
626
+ feature: "H49",
627
+ subcommand: "analyze",
628
+ workspaceRoot,
629
+ wroteArtifacts: !dryRun,
630
+ artifacts: BROWNFIELD_IMPORT_ARTIFACTS,
631
+ inventory,
632
+ routes,
633
+ frontendCalls,
634
+ candidateEntries,
635
+ riskReport,
636
+ migrationPlan,
637
+ exitCode: 0,
638
+ };
639
+ }
640
+
641
+ export function inspectBrownfieldImport(workspaceRoot: string): BrownfieldImportResult {
642
+ const inventory = readJson<ImportedInventory>(artifactPath(workspaceRoot, BROWNFIELD_IMPORT_ARTIFACTS.inventory));
643
+ const routes = readJson<ImportedRoute[]>(artifactPath(workspaceRoot, BROWNFIELD_IMPORT_ARTIFACTS.routes)) ?? [];
644
+ const frontendCalls =
645
+ readJson<ImportedFrontendCall[]>(artifactPath(workspaceRoot, BROWNFIELD_IMPORT_ARTIFACTS.frontendCalls)) ?? [];
646
+ const candidateEntries =
647
+ readJson<ImportedCandidateEntry[]>(artifactPath(workspaceRoot, BROWNFIELD_IMPORT_ARTIFACTS.candidateEntries)) ?? [];
648
+ const riskReport = readJson<ImportedRiskReport>(artifactPath(workspaceRoot, BROWNFIELD_IMPORT_ARTIFACTS.riskReport));
649
+ const migrationPlanPath = artifactPath(workspaceRoot, BROWNFIELD_IMPORT_ARTIFACTS.migrationPlan);
650
+ const migrationPlan = existsSync(migrationPlanPath) ? readFileSync(migrationPlanPath, "utf8") : null;
651
+ const missing = !inventory || !riskReport;
652
+ return {
653
+ schemaVersion: "0.1.0",
654
+ feature: "H49",
655
+ subcommand: "inspect",
656
+ workspaceRoot,
657
+ wroteArtifacts: false,
658
+ artifacts: BROWNFIELD_IMPORT_ARTIFACTS,
659
+ inventory,
660
+ routes,
661
+ frontendCalls,
662
+ candidateEntries,
663
+ riskReport,
664
+ migrationPlan,
665
+ exitCode: missing ? 1 : 0,
666
+ failureKind: missing ? "missing_import_artifacts" : undefined,
667
+ };
668
+ }
669
+
670
+ export function runBrownfieldImportCommand(options: BrownfieldImportCommandOptions): BrownfieldImportResult {
671
+ if (options.subcommand === "analyze") {
672
+ return analyze(options.workspaceRoot, options.dryRun);
673
+ }
674
+ const result = inspectBrownfieldImport(options.workspaceRoot);
675
+ if (options.entry) {
676
+ result.candidateEntries = result.candidateEntries.filter((entry) => entry.id === options.entry || entry.name === options.entry);
677
+ }
678
+ if (options.target === "routes") {
679
+ result.frontendCalls = [];
680
+ result.candidateEntries = [];
681
+ } else if (options.target === "frontend-calls") {
682
+ result.routes = [];
683
+ result.candidateEntries = [];
684
+ } else if (options.target === "candidate-entries") {
685
+ result.routes = [];
686
+ result.frontendCalls = [];
687
+ }
688
+ return result;
689
+ }
690
+
691
+ export function formatBrownfieldImportJson(result: BrownfieldImportResult): Record<string, unknown> {
692
+ return {
693
+ schemaVersion: result.schemaVersion,
694
+ feature: result.feature,
695
+ subcommand: result.subcommand,
696
+ workspaceRoot: result.workspaceRoot,
697
+ wroteArtifacts: result.wroteArtifacts,
698
+ artifacts: result.artifacts,
699
+ inventory: result.inventory,
700
+ routes: result.routes,
701
+ frontendCalls: result.frontendCalls,
702
+ candidateEntries: result.candidateEntries,
703
+ riskReport: result.riskReport,
704
+ migrationPlan: result.migrationPlan,
705
+ exitCode: result.exitCode,
706
+ failureKind: result.failureKind ?? null,
707
+ };
708
+ }
709
+
710
+ export function formatBrownfieldImportHuman(result: BrownfieldImportResult): string {
711
+ if (result.exitCode !== 0 && !result.inventory) {
712
+ return "No brownfield import artifacts found. Run `forge import analyze` first.\n";
713
+ }
714
+ const summary = result.riskReport?.summary;
715
+ return [
716
+ `forge import ${result.subcommand}`,
717
+ `artifacts: ${result.wroteArtifacts ? "written" : "read"} at ${IMPORT_DIR}`,
718
+ `files scanned: ${result.inventory?.filesScanned ?? 0}`,
719
+ `routes: ${summary?.routeCount ?? result.routes.length}`,
720
+ `frontend calls: ${summary?.frontendCallCount ?? result.frontendCalls.length}`,
721
+ `candidate entries: ${summary?.candidateCount ?? result.candidateEntries.length}`,
722
+ `hidden from agents: ${summary?.hiddenFromAgents ?? result.candidateEntries.filter((entry) => !entry.visibleToAgent).length}`,
723
+ `needs approval: ${summary?.needsApproval ?? result.candidateEntries.filter((entry) => entry.needsApproval).length}`,
724
+ "",
725
+ ].join("\n");
726
+ }