forgeos 0.1.0-alpha.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.
Files changed (540) hide show
  1. package/.npmignore +1 -0
  2. package/AGENTS.md +277 -0
  3. package/CHANGELOG.md +8 -0
  4. package/CONTRIBUTING.md +58 -0
  5. package/README.md +377 -0
  6. package/bin/forge-bun.mjs +110 -0
  7. package/bin/forge.mjs +19 -0
  8. package/package.json +96 -0
  9. package/packages/eslint-plugin-forge/index.ts +15 -0
  10. package/packages/eslint-plugin-forge/package.json +10 -0
  11. package/packages/eslint-plugin-forge/src/check-source.ts +95 -0
  12. package/packages/eslint-plugin-forge/src/load-artifacts.ts +24 -0
  13. package/packages/eslint-plugin-forge/src/rule-no-forge-guard-violation.ts +93 -0
  14. package/src/forge/_generated/actionSubscriptions.json +2 -0
  15. package/src/forge/_generated/actionSubscriptions.ts +10 -0
  16. package/src/forge/_generated/agentAdapterManifest.json +2 -0
  17. package/src/forge/_generated/agentAdapterManifest.ts +73 -0
  18. package/src/forge/_generated/agentContract.json +2 -0
  19. package/src/forge/_generated/agentContract.ts +912 -0
  20. package/src/forge/_generated/agentQuickstart.md +32 -0
  21. package/src/forge/_generated/aiContext.ts +59 -0
  22. package/src/forge/_generated/aiModels.json +2 -0
  23. package/src/forge/_generated/aiModels.ts +35 -0
  24. package/src/forge/_generated/aiProviders.json +2 -0
  25. package/src/forge/_generated/aiProviders.ts +23 -0
  26. package/src/forge/_generated/aiRegistry.json +2 -0
  27. package/src/forge/_generated/aiRegistry.ts +29 -0
  28. package/src/forge/_generated/api.json +2 -0
  29. package/src/forge/_generated/api.ts +8 -0
  30. package/src/forge/_generated/appGraph.json +2 -0
  31. package/src/forge/_generated/appGraph.ts +14511 -0
  32. package/src/forge/_generated/appMap.md +35 -0
  33. package/src/forge/_generated/artifactManifest.json +2 -0
  34. package/src/forge/_generated/artifactManifest.ts +7 -0
  35. package/src/forge/_generated/authClaims.json +2 -0
  36. package/src/forge/_generated/authClaims.ts +13 -0
  37. package/src/forge/_generated/authConfig.json +2 -0
  38. package/src/forge/_generated/authConfig.ts +17 -0
  39. package/src/forge/_generated/authContext.ts +23 -0
  40. package/src/forge/_generated/authRegistry.json +2 -0
  41. package/src/forge/_generated/authRegistry.ts +25 -0
  42. package/src/forge/_generated/buildInfo.json +2 -0
  43. package/src/forge/_generated/buildInfo.ts +9 -0
  44. package/src/forge/_generated/capabilityMap.json +2 -0
  45. package/src/forge/_generated/capabilityMap.md +15 -0
  46. package/src/forge/_generated/capabilityMap.ts +17 -0
  47. package/src/forge/_generated/client.ts +282 -0
  48. package/src/forge/_generated/clientApi.ts +9 -0
  49. package/src/forge/_generated/clientManifest.json +2 -0
  50. package/src/forge/_generated/clientManifest.ts +39 -0
  51. package/src/forge/_generated/clientTypes.ts +78 -0
  52. package/src/forge/_generated/configRegistry.json +2 -0
  53. package/src/forge/_generated/configRegistry.ts +4 -0
  54. package/src/forge/_generated/dataGraph.json +2 -0
  55. package/src/forge/_generated/dataGraph.ts +8 -0
  56. package/src/forge/_generated/db.json +2 -0
  57. package/src/forge/_generated/db.ts +2 -0
  58. package/src/forge/_generated/dbSecurityManifest.json +2 -0
  59. package/src/forge/_generated/dbSecurityManifest.ts +15 -0
  60. package/src/forge/_generated/dbSessionContext.json +2 -0
  61. package/src/forge/_generated/dbSessionContext.ts +39 -0
  62. package/src/forge/_generated/deployManifest.json +2 -0
  63. package/src/forge/_generated/deployManifest.ts +14 -0
  64. package/src/forge/_generated/devManifest.json +2 -0
  65. package/src/forge/_generated/devManifest.ts +47 -0
  66. package/src/forge/_generated/envSchema.json +2 -0
  67. package/src/forge/_generated/envSchema.ts +59 -0
  68. package/src/forge/_generated/frontendGraph.json +2 -0
  69. package/src/forge/_generated/frontendGraph.ts +27 -0
  70. package/src/forge/_generated/importGuards.json +2 -0
  71. package/src/forge/_generated/importGuards.ts +652 -0
  72. package/src/forge/_generated/index.ts +67 -0
  73. package/src/forge/_generated/liveProductionManifest.json +2 -0
  74. package/src/forge/_generated/liveProductionManifest.ts +23 -0
  75. package/src/forge/_generated/liveProtocol.json +2 -0
  76. package/src/forge/_generated/liveProtocol.ts +21 -0
  77. package/src/forge/_generated/liveQueryRegistry.json +2 -0
  78. package/src/forge/_generated/liveQueryRegistry.ts +9 -0
  79. package/src/forge/_generated/liveTransportConfig.json +2 -0
  80. package/src/forge/_generated/liveTransportConfig.ts +19 -0
  81. package/src/forge/_generated/makeRegistry.json +2 -0
  82. package/src/forge/_generated/makeRegistry.ts +163 -0
  83. package/src/forge/_generated/makeTemplates.json +2 -0
  84. package/src/forge/_generated/makeTemplates.ts +61 -0
  85. package/src/forge/_generated/mockMap.json +2 -0
  86. package/src/forge/_generated/mockMap.ts +7 -0
  87. package/src/forge/_generated/operationPlaybooks.md +145 -0
  88. package/src/forge/_generated/packageGraph.json +2 -0
  89. package/src/forge/_generated/packageGraph.ts +168569 -0
  90. package/src/forge/_generated/packageUpgradeRegistry.json +2 -0
  91. package/src/forge/_generated/packageUpgradeRegistry.ts +15 -0
  92. package/src/forge/_generated/permissionMatrix.json +2 -0
  93. package/src/forge/_generated/permissionMatrix.ts +7 -0
  94. package/src/forge/_generated/policyRegistry.json +2 -0
  95. package/src/forge/_generated/policyRegistry.ts +11 -0
  96. package/src/forge/_generated/queryRegistry.json +2 -0
  97. package/src/forge/_generated/queryRegistry.ts +9 -0
  98. package/src/forge/_generated/react.d.ts +22 -0
  99. package/src/forge/_generated/react.ts +29 -0
  100. package/src/forge/_generated/reactManifest.json +2 -0
  101. package/src/forge/_generated/reactManifest.ts +19 -0
  102. package/src/forge/_generated/releaseManifest.json +2 -0
  103. package/src/forge/_generated/releaseManifest.ts +25 -0
  104. package/src/forge/_generated/rlsPolicies.json +2 -0
  105. package/src/forge/_generated/rlsPolicies.sql +34 -0
  106. package/src/forge/_generated/rlsPolicies.ts +6 -0
  107. package/src/forge/_generated/runtimeGraph.json +2 -0
  108. package/src/forge/_generated/runtimeGraph.ts +8 -0
  109. package/src/forge/_generated/runtimeMatrix.json +2 -0
  110. package/src/forge/_generated/runtimeMatrix.ts +229125 -0
  111. package/src/forge/_generated/runtimeRegistry.ts +2 -0
  112. package/src/forge/_generated/runtimeRules.md +79 -0
  113. package/src/forge/_generated/secretRegistry.json +2 -0
  114. package/src/forge/_generated/secretRegistry.ts +50 -0
  115. package/src/forge/_generated/secretsContext.ts +11 -0
  116. package/src/forge/_generated/serverApi.ts +10 -0
  117. package/src/forge/_generated/sourceMapManifest.json +2 -0
  118. package/src/forge/_generated/sourceMapManifest.ts +7 -0
  119. package/src/forge/_generated/sqlPlan.json +2 -0
  120. package/src/forge/_generated/sqlPlan.ts +88 -0
  121. package/src/forge/_generated/subscriptionManifest.json +2 -0
  122. package/src/forge/_generated/subscriptionManifest.ts +7 -0
  123. package/src/forge/_generated/symbolicationManifest.json +2 -0
  124. package/src/forge/_generated/symbolicationManifest.ts +17 -0
  125. package/src/forge/_generated/telemetryRegistry.json +2 -0
  126. package/src/forge/_generated/telemetryRegistry.ts +9 -0
  127. package/src/forge/_generated/telemetrySinks.json +2 -0
  128. package/src/forge/_generated/telemetrySinks.ts +11 -0
  129. package/src/forge/_generated/tenantScope.json +2 -0
  130. package/src/forge/_generated/tenantScope.ts +8 -0
  131. package/src/forge/_generated/testGraph.json +2 -0
  132. package/src/forge/_generated/testGraph.ts +3054 -0
  133. package/src/forge/_generated/testPlanRegistry.json +2 -0
  134. package/src/forge/_generated/testPlanRegistry.ts +33 -0
  135. package/src/forge/_generated/uiRoutes.json +2 -0
  136. package/src/forge/_generated/uiRoutes.ts +16 -0
  137. package/src/forge/_generated/uiScenarios.json +2 -0
  138. package/src/forge/_generated/uiScenarios.ts +30 -0
  139. package/src/forge/_generated/uiTestManifest.json +2 -0
  140. package/src/forge/_generated/uiTestManifest.ts +27 -0
  141. package/src/forge/_generated/workflowRegistry.json +2 -0
  142. package/src/forge/_generated/workflowRegistry.ts +9 -0
  143. package/src/forge/_generated/workflowSubscriptions.json +2 -0
  144. package/src/forge/_generated/workflowSubscriptions.ts +10 -0
  145. package/src/forge/agent-adapters/index.ts +1002 -0
  146. package/src/forge/agent-adapters/types.ts +135 -0
  147. package/src/forge/cli/agent-contract.ts +50 -0
  148. package/src/forge/cli/ai.ts +148 -0
  149. package/src/forge/cli/auth.ts +198 -0
  150. package/src/forge/cli/build.ts +105 -0
  151. package/src/forge/cli/bun-exec.ts +4 -0
  152. package/src/forge/cli/commands.ts +1130 -0
  153. package/src/forge/cli/db.ts +316 -0
  154. package/src/forge/cli/deps.ts +277 -0
  155. package/src/forge/cli/dev.ts +529 -0
  156. package/src/forge/cli/doctor.ts +209 -0
  157. package/src/forge/cli/feature.ts +485 -0
  158. package/src/forge/cli/index.ts +25 -0
  159. package/src/forge/cli/lint-forge.ts +119 -0
  160. package/src/forge/cli/live.ts +179 -0
  161. package/src/forge/cli/main.ts +92 -0
  162. package/src/forge/cli/make.ts +133 -0
  163. package/src/forge/cli/new.ts +505 -0
  164. package/src/forge/cli/outbox.ts +297 -0
  165. package/src/forge/cli/output.ts +114 -0
  166. package/src/forge/cli/parse.ts +2211 -0
  167. package/src/forge/cli/policy.ts +204 -0
  168. package/src/forge/cli/query.ts +91 -0
  169. package/src/forge/cli/refactor.ts +221 -0
  170. package/src/forge/cli/release.ts +285 -0
  171. package/src/forge/cli/rls.ts +322 -0
  172. package/src/forge/cli/run.ts +76 -0
  173. package/src/forge/cli/secrets.ts +274 -0
  174. package/src/forge/cli/self-host.ts +468 -0
  175. package/src/forge/cli/serve.ts +93 -0
  176. package/src/forge/cli/telemetry.ts +219 -0
  177. package/src/forge/cli/verify.ts +587 -0
  178. package/src/forge/cli/version.ts +1 -0
  179. package/src/forge/cli/windows.ts +413 -0
  180. package/src/forge/cli/worker.ts +87 -0
  181. package/src/forge/cli/workflow.ts +424 -0
  182. package/src/forge/compiler/action-subscriptions/build.ts +116 -0
  183. package/src/forge/compiler/action-subscriptions/constants.ts +2 -0
  184. package/src/forge/compiler/action-subscriptions/index.ts +6 -0
  185. package/src/forge/compiler/action-subscriptions/parse.ts +6 -0
  186. package/src/forge/compiler/agent-contract/build.ts +1651 -0
  187. package/src/forge/compiler/agent-contract/types.ts +326 -0
  188. package/src/forge/compiler/ai-registry/build.ts +165 -0
  189. package/src/forge/compiler/ai-registry/constants.ts +2 -0
  190. package/src/forge/compiler/ai-registry/parse.ts +56 -0
  191. package/src/forge/compiler/api-surface/build.ts +107 -0
  192. package/src/forge/compiler/app-graph/build.ts +121 -0
  193. package/src/forge/compiler/app-graph/classify.ts +10 -0
  194. package/src/forge/compiler/app-graph/dup-symbol.ts +29 -0
  195. package/src/forge/compiler/app-graph/extract.ts +124 -0
  196. package/src/forge/compiler/app-graph/forge-apis.ts +29 -0
  197. package/src/forge/compiler/app-graph/index.ts +15 -0
  198. package/src/forge/compiler/app-graph/module-graph.ts +320 -0
  199. package/src/forge/compiler/app-graph/parser.ts +119 -0
  200. package/src/forge/compiler/app-graph/symbols.ts +48 -0
  201. package/src/forge/compiler/app-graph/tsconfig-hash.ts +62 -0
  202. package/src/forge/compiler/app-graph/types.ts +43 -0
  203. package/src/forge/compiler/app-graph/versions.ts +14 -0
  204. package/src/forge/compiler/cache/index.ts +17 -0
  205. package/src/forge/compiler/cache/key.ts +46 -0
  206. package/src/forge/compiler/cache/scheduler.ts +72 -0
  207. package/src/forge/compiler/cache/store.ts +78 -0
  208. package/src/forge/compiler/classifier/capabilities.ts +78 -0
  209. package/src/forge/compiler/classifier/classify.ts +113 -0
  210. package/src/forge/compiler/classifier/contexts.ts +188 -0
  211. package/src/forge/compiler/classifier/index.ts +18 -0
  212. package/src/forge/compiler/classifier/runtime-matrix.ts +45 -0
  213. package/src/forge/compiler/classifier/secrets.ts +41 -0
  214. package/src/forge/compiler/classifier/signals.ts +129 -0
  215. package/src/forge/compiler/client-sdk/build-manifest.ts +151 -0
  216. package/src/forge/compiler/client-sdk/render-client.ts +432 -0
  217. package/src/forge/compiler/data-graph/build.ts +131 -0
  218. package/src/forge/compiler/data-graph/constants.ts +5 -0
  219. package/src/forge/compiler/data-graph/index.ts +6 -0
  220. package/src/forge/compiler/data-graph/parse.ts +176 -0
  221. package/src/forge/compiler/data-graph/rls/build.ts +222 -0
  222. package/src/forge/compiler/data-graph/rls/types.ts +62 -0
  223. package/src/forge/compiler/data-graph/sql/ddl.ts +390 -0
  224. package/src/forge/compiler/data-graph/sql/naming.ts +10 -0
  225. package/src/forge/compiler/data-graph/sql/serialize.ts +85 -0
  226. package/src/forge/compiler/data-graph/sql/types.ts +37 -0
  227. package/src/forge/compiler/dev-manifest/build.ts +170 -0
  228. package/src/forge/compiler/dev-manifest/constants.ts +5 -0
  229. package/src/forge/compiler/diagnostics/codes.ts +611 -0
  230. package/src/forge/compiler/diagnostics/create.ts +245 -0
  231. package/src/forge/compiler/diagnostics/index.ts +55 -0
  232. package/src/forge/compiler/emitter/artifact-kind.ts +14 -0
  233. package/src/forge/compiler/emitter/barrel.ts +44 -0
  234. package/src/forge/compiler/emitter/constants.ts +7 -0
  235. package/src/forge/compiler/emitter/emit.ts +237 -0
  236. package/src/forge/compiler/emitter/index.ts +24 -0
  237. package/src/forge/compiler/emitter/lock.ts +62 -0
  238. package/src/forge/compiler/emitter/render.ts +73 -0
  239. package/src/forge/compiler/emitter/write.ts +35 -0
  240. package/src/forge/compiler/frontend-graph/build.ts +495 -0
  241. package/src/forge/compiler/fs/index.ts +23 -0
  242. package/src/forge/compiler/fs/memory.ts +233 -0
  243. package/src/forge/compiler/fs/node.ts +139 -0
  244. package/src/forge/compiler/fs/profile.ts +108 -0
  245. package/src/forge/compiler/fs/types.ts +52 -0
  246. package/src/forge/compiler/guards/artifacts.ts +96 -0
  247. package/src/forge/compiler/guards/check-ai-usage.ts +98 -0
  248. package/src/forge/compiler/guards/check-import-guards.ts +106 -0
  249. package/src/forge/compiler/guards/check-process-env.ts +98 -0
  250. package/src/forge/compiler/guards/check-query-usage.ts +76 -0
  251. package/src/forge/compiler/guards/index.ts +11 -0
  252. package/src/forge/compiler/guards/propagate-contexts.ts +57 -0
  253. package/src/forge/compiler/index.ts +17 -0
  254. package/src/forge/compiler/integration/add.ts +496 -0
  255. package/src/forge/compiler/integration/index.ts +17 -0
  256. package/src/forge/compiler/integration/plan.ts +283 -0
  257. package/src/forge/compiler/integration/render.ts +189 -0
  258. package/src/forge/compiler/integration/snapshot.ts +52 -0
  259. package/src/forge/compiler/integration/templates/ai.ts +131 -0
  260. package/src/forge/compiler/integration/templates/index.ts +8 -0
  261. package/src/forge/compiler/integration/templates/posthog.ts +145 -0
  262. package/src/forge/compiler/integration/templates/render.ts +113 -0
  263. package/src/forge/compiler/integration/templates/sentry.ts +151 -0
  264. package/src/forge/compiler/integration/templates/stripe.ts +109 -0
  265. package/src/forge/compiler/integration/templates/types.ts +14 -0
  266. package/src/forge/compiler/integration/templates/zod.ts +55 -0
  267. package/src/forge/compiler/live-production/types.ts +122 -0
  268. package/src/forge/compiler/live-query-registry/build.ts +150 -0
  269. package/src/forge/compiler/live-query-registry/constants.ts +2 -0
  270. package/src/forge/compiler/make-registry/build.ts +179 -0
  271. package/src/forge/compiler/orchestrator/discover.ts +214 -0
  272. package/src/forge/compiler/orchestrator/fast-check.ts +117 -0
  273. package/src/forge/compiler/orchestrator/generate-lock.ts +138 -0
  274. package/src/forge/compiler/orchestrator/guards.ts +5 -0
  275. package/src/forge/compiler/orchestrator/index.ts +27 -0
  276. package/src/forge/compiler/orchestrator/manifest-hashes.ts +21 -0
  277. package/src/forge/compiler/orchestrator/manifest.ts +92 -0
  278. package/src/forge/compiler/orchestrator/orphans.ts +51 -0
  279. package/src/forge/compiler/orchestrator/plan.ts +876 -0
  280. package/src/forge/compiler/orchestrator/profile.ts +36 -0
  281. package/src/forge/compiler/orchestrator/run.ts +277 -0
  282. package/src/forge/compiler/orchestrator/serialize.ts +886 -0
  283. package/src/forge/compiler/orchestrator/session.ts +96 -0
  284. package/src/forge/compiler/orchestrator/types.ts +31 -0
  285. package/src/forge/compiler/orchestrator/verify.ts +38 -0
  286. package/src/forge/compiler/orchestrator/workspace-index.ts +154 -0
  287. package/src/forge/compiler/package-graph/capabilities-stub.ts +33 -0
  288. package/src/forge/compiler/package-graph/checksum.ts +97 -0
  289. package/src/forge/compiler/package-graph/compiler.ts +392 -0
  290. package/src/forge/compiler/package-graph/constants.ts +4 -0
  291. package/src/forge/compiler/package-graph/dts-extractor.ts +142 -0
  292. package/src/forge/compiler/package-graph/exports-discovery.ts +84 -0
  293. package/src/forge/compiler/package-graph/extract-dts.ts +32 -0
  294. package/src/forge/compiler/package-graph/index.ts +33 -0
  295. package/src/forge/compiler/package-graph/jsdoc.ts +62 -0
  296. package/src/forge/compiler/package-graph/read-file.ts +21 -0
  297. package/src/forge/compiler/package-graph/resolve.ts +127 -0
  298. package/src/forge/compiler/package-manager/adapter.ts +237 -0
  299. package/src/forge/compiler/package-manager/bun-executable.ts +92 -0
  300. package/src/forge/compiler/package-manager/commands.ts +47 -0
  301. package/src/forge/compiler/package-manager/detect.ts +79 -0
  302. package/src/forge/compiler/package-manager/executor.ts +117 -0
  303. package/src/forge/compiler/package-manager/index.ts +22 -0
  304. package/src/forge/compiler/package-manager/parse-spec.ts +16 -0
  305. package/src/forge/compiler/package-manager/version.ts +27 -0
  306. package/src/forge/compiler/package-upgrades/apply.ts +195 -0
  307. package/src/forge/compiler/package-upgrades/comparator.ts +181 -0
  308. package/src/forge/compiler/package-upgrades/impact.ts +139 -0
  309. package/src/forge/compiler/package-upgrades/markdown.ts +97 -0
  310. package/src/forge/compiler/package-upgrades/planner.ts +532 -0
  311. package/src/forge/compiler/package-upgrades/risk.ts +208 -0
  312. package/src/forge/compiler/package-upgrades/types.ts +174 -0
  313. package/src/forge/compiler/policy-registry/build.ts +266 -0
  314. package/src/forge/compiler/policy-registry/constants.ts +2 -0
  315. package/src/forge/compiler/policy-registry/parse.ts +81 -0
  316. package/src/forge/compiler/primitives/compare.ts +26 -0
  317. package/src/forge/compiler/primitives/hash.ts +40 -0
  318. package/src/forge/compiler/primitives/header.ts +45 -0
  319. package/src/forge/compiler/primitives/index.ts +45 -0
  320. package/src/forge/compiler/primitives/paths.ts +24 -0
  321. package/src/forge/compiler/primitives/result.ts +164 -0
  322. package/src/forge/compiler/primitives/serialize.ts +66 -0
  323. package/src/forge/compiler/primitives/sort.ts +87 -0
  324. package/src/forge/compiler/query-registry/build.ts +114 -0
  325. package/src/forge/compiler/query-registry/constants.ts +2 -0
  326. package/src/forge/compiler/recipes/definitions.ts +289 -0
  327. package/src/forge/compiler/recipes/helpers.ts +37 -0
  328. package/src/forge/compiler/recipes/index.ts +21 -0
  329. package/src/forge/compiler/recipes/registry.ts +102 -0
  330. package/src/forge/compiler/release/build.ts +100 -0
  331. package/src/forge/compiler/release/types.ts +119 -0
  332. package/src/forge/compiler/runtime-graph/build.ts +137 -0
  333. package/src/forge/compiler/runtime-graph/constants.ts +5 -0
  334. package/src/forge/compiler/runtime-graph/index.ts +5 -0
  335. package/src/forge/compiler/sandbox/artifact-sanitize.ts +26 -0
  336. package/src/forge/compiler/sandbox/backends/child.ts +123 -0
  337. package/src/forge/compiler/sandbox/backends/docker.ts +173 -0
  338. package/src/forge/compiler/sandbox/index.ts +51 -0
  339. package/src/forge/compiler/sandbox/inspect.ts +143 -0
  340. package/src/forge/compiler/sandbox/inspector-entry.ts +115 -0
  341. package/src/forge/compiler/sandbox/limits.ts +31 -0
  342. package/src/forge/compiler/sandbox/scrub-env.ts +60 -0
  343. package/src/forge/compiler/sandbox/secret-scan.ts +54 -0
  344. package/src/forge/compiler/sandbox/serialize.ts +106 -0
  345. package/src/forge/compiler/sandbox/types.ts +7 -0
  346. package/src/forge/compiler/secret-registry/build.ts +123 -0
  347. package/src/forge/compiler/telemetry-registry/build.ts +89 -0
  348. package/src/forge/compiler/telemetry-registry/constants.ts +2 -0
  349. package/src/forge/compiler/telemetry-registry/parse.ts +13 -0
  350. package/src/forge/compiler/test-graph/build.ts +277 -0
  351. package/src/forge/compiler/types/action-subscriptions.ts +19 -0
  352. package/src/forge/compiler/types/ai-registry.ts +33 -0
  353. package/src/forge/compiler/types/app-graph.ts +80 -0
  354. package/src/forge/compiler/types/capability.ts +29 -0
  355. package/src/forge/compiler/types/classification.ts +9 -0
  356. package/src/forge/compiler/types/cli.ts +159 -0
  357. package/src/forge/compiler/types/data-graph.ts +24 -0
  358. package/src/forge/compiler/types/dev-manifest.ts +41 -0
  359. package/src/forge/compiler/types/diagnostic.ts +12 -0
  360. package/src/forge/compiler/types/emit.ts +25 -0
  361. package/src/forge/compiler/types/frontend-graph.ts +81 -0
  362. package/src/forge/compiler/types/import-guards.ts +19 -0
  363. package/src/forge/compiler/types/index.ts +98 -0
  364. package/src/forge/compiler/types/integration.ts +25 -0
  365. package/src/forge/compiler/types/json.ts +3 -0
  366. package/src/forge/compiler/types/live-query-registry.ts +32 -0
  367. package/src/forge/compiler/types/lock.ts +37 -0
  368. package/src/forge/compiler/types/package-graph.ts +84 -0
  369. package/src/forge/compiler/types/policy-registry.ts +69 -0
  370. package/src/forge/compiler/types/query-registry.ts +18 -0
  371. package/src/forge/compiler/types/runtime-graph.ts +30 -0
  372. package/src/forge/compiler/types/runtime-matrix.ts +16 -0
  373. package/src/forge/compiler/types/runtime.ts +30 -0
  374. package/src/forge/compiler/types/sandbox.ts +24 -0
  375. package/src/forge/compiler/types/secret-registry.ts +38 -0
  376. package/src/forge/compiler/types/telemetry-registry.ts +26 -0
  377. package/src/forge/compiler/types/test-graph.ts +45 -0
  378. package/src/forge/compiler/types/workflow-registry.ts +42 -0
  379. package/src/forge/compiler/workflow-registry/build.ts +180 -0
  380. package/src/forge/compiler/workflow-registry/constants.ts +2 -0
  381. package/src/forge/compiler/workflow-registry/index.ts +5 -0
  382. package/src/forge/compiler/workflow-registry/parse.ts +19 -0
  383. package/src/forge/dev/server.ts +1379 -0
  384. package/src/forge/dev/types.ts +49 -0
  385. package/src/forge/dev/watch.ts +109 -0
  386. package/src/forge/dev-console/cycle.ts +652 -0
  387. package/src/forge/dev-console/types.ts +99 -0
  388. package/src/forge/feature/compiler.ts +656 -0
  389. package/src/forge/feature/examples.ts +125 -0
  390. package/src/forge/feature/types.ts +177 -0
  391. package/src/forge/impact/index.ts +1160 -0
  392. package/src/forge/impact/types.ts +151 -0
  393. package/src/forge/intent/index.ts +490 -0
  394. package/src/forge/intent/types.ts +73 -0
  395. package/src/forge/make/fields.ts +146 -0
  396. package/src/forge/make/index.ts +1101 -0
  397. package/src/forge/make/naming.ts +42 -0
  398. package/src/forge/make/templates.ts +525 -0
  399. package/src/forge/make/types.ts +151 -0
  400. package/src/forge/platform/module.ts +20 -0
  401. package/src/forge/policy.ts +1 -0
  402. package/src/forge/react/index.ts +418 -0
  403. package/src/forge/refactor/index.ts +1936 -0
  404. package/src/forge/refactor/text-utils.ts +34 -0
  405. package/src/forge/refactor/types.ts +191 -0
  406. package/src/forge/refactor/workspace-fs.ts +171 -0
  407. package/src/forge/repair/index.ts +656 -0
  408. package/src/forge/repair/rules/index.ts +476 -0
  409. package/src/forge/repair/types.ts +175 -0
  410. package/src/forge/review/index.ts +992 -0
  411. package/src/forge/review/types.ts +196 -0
  412. package/src/forge/runtime/ai/check.ts +86 -0
  413. package/src/forge/runtime/ai/context.ts +394 -0
  414. package/src/forge/runtime/ai/cost-estimator.ts +41 -0
  415. package/src/forge/runtime/ai/mock.ts +49 -0
  416. package/src/forge/runtime/ai/providers.ts +78 -0
  417. package/src/forge/runtime/ai/state.ts +17 -0
  418. package/src/forge/runtime/ai/types.ts +67 -0
  419. package/src/forge/runtime/auth/authenticate.ts +58 -0
  420. package/src/forge/runtime/auth/claims.ts +119 -0
  421. package/src/forge/runtime/auth/config.ts +148 -0
  422. package/src/forge/runtime/auth/errors.ts +45 -0
  423. package/src/forge/runtime/auth/evaluate.ts +126 -0
  424. package/src/forge/runtime/auth/resolve.ts +74 -0
  425. package/src/forge/runtime/auth/types.ts +87 -0
  426. package/src/forge/runtime/auth/verifier.ts +138 -0
  427. package/src/forge/runtime/context/create-context.ts +204 -0
  428. package/src/forge/runtime/context/create-query-context.ts +34 -0
  429. package/src/forge/runtime/db/adapter.ts +31 -0
  430. package/src/forge/runtime/db/factory.ts +83 -0
  431. package/src/forge/runtime/db/generated-client.ts +294 -0
  432. package/src/forge/runtime/db/memory-adapter.ts +706 -0
  433. package/src/forge/runtime/db/migrate.ts +132 -0
  434. package/src/forge/runtime/db/outbox.ts +54 -0
  435. package/src/forge/runtime/db/pglite-adapter.ts +51 -0
  436. package/src/forge/runtime/db/postgres-adapter.ts +112 -0
  437. package/src/forge/runtime/db/read-only-client.ts +97 -0
  438. package/src/forge/runtime/db/session-context.ts +62 -0
  439. package/src/forge/runtime/executor.ts +446 -0
  440. package/src/forge/runtime/live/dependency-tracker.ts +57 -0
  441. package/src/forge/runtime/live/invalidation-log.ts +189 -0
  442. package/src/forge/runtime/live/live-query-runner.ts +267 -0
  443. package/src/forge/runtime/live/registry.ts +28 -0
  444. package/src/forge/runtime/live/sse.ts +75 -0
  445. package/src/forge/runtime/live/subscription-manager.ts +443 -0
  446. package/src/forge/runtime/live/types.ts +143 -0
  447. package/src/forge/runtime/outbox/claim.ts +153 -0
  448. package/src/forge/runtime/outbox/process.ts +298 -0
  449. package/src/forge/runtime/outbox/retry.ts +8 -0
  450. package/src/forge/runtime/outbox/subscriptions.ts +33 -0
  451. package/src/forge/runtime/outbox/types.ts +69 -0
  452. package/src/forge/runtime/policy/check.ts +157 -0
  453. package/src/forge/runtime/policy/load.ts +55 -0
  454. package/src/forge/runtime/query/registry.ts +19 -0
  455. package/src/forge/runtime/query/run-query.ts +347 -0
  456. package/src/forge/runtime/release/runtime.ts +322 -0
  457. package/src/forge/runtime/release/symbolicate.ts +175 -0
  458. package/src/forge/runtime/runner/command-transaction.ts +193 -0
  459. package/src/forge/runtime/runner/run-entry.ts +226 -0
  460. package/src/forge/runtime/secrets/check.ts +78 -0
  461. package/src/forge/runtime/secrets/create-context.ts +138 -0
  462. package/src/forge/runtime/secrets/env-loader.ts +94 -0
  463. package/src/forge/runtime/secrets/runtime-bundle.ts +47 -0
  464. package/src/forge/runtime/secrets/types.ts +31 -0
  465. package/src/forge/runtime/telemetry/buffer.ts +87 -0
  466. package/src/forge/runtime/telemetry/context.ts +192 -0
  467. package/src/forge/runtime/telemetry/correlation.ts +13 -0
  468. package/src/forge/runtime/telemetry/flush.ts +190 -0
  469. package/src/forge/runtime/telemetry/process.ts +20 -0
  470. package/src/forge/runtime/telemetry/scrubber.ts +115 -0
  471. package/src/forge/runtime/telemetry/sinks/local-jsonl.ts +39 -0
  472. package/src/forge/runtime/telemetry/sinks/posthog.ts +64 -0
  473. package/src/forge/runtime/telemetry/sinks/sentry.ts +60 -0
  474. package/src/forge/runtime/telemetry/spans.ts +58 -0
  475. package/src/forge/runtime/telemetry/types.ts +64 -0
  476. package/src/forge/runtime/workflows/cancel.ts +26 -0
  477. package/src/forge/runtime/workflows/create-run.ts +98 -0
  478. package/src/forge/runtime/workflows/process-run.ts +182 -0
  479. package/src/forge/runtime/workflows/process-step.ts +190 -0
  480. package/src/forge/runtime/workflows/process.ts +260 -0
  481. package/src/forge/runtime/workflows/registry.ts +51 -0
  482. package/src/forge/runtime/workflows/resolve-step.ts +46 -0
  483. package/src/forge/runtime/workflows/retry-run.ts +44 -0
  484. package/src/forge/runtime/workflows/retry.ts +8 -0
  485. package/src/forge/runtime/workflows/sanitize.ts +19 -0
  486. package/src/forge/runtime/workflows/start-from-outbox.ts +71 -0
  487. package/src/forge/runtime/workflows/types.ts +77 -0
  488. package/src/forge/server.ts +96 -0
  489. package/src/forge/ui/index.ts +770 -0
  490. package/src/forge/ui/types.ts +191 -0
  491. package/templates/b2b-support-web/.env.example +22 -0
  492. package/templates/b2b-support-web/.vscode/settings.json +14 -0
  493. package/templates/b2b-support-web/AGENTS.md +108 -0
  494. package/templates/b2b-support-web/README.md +48 -0
  495. package/templates/b2b-support-web/forge.config.ts +3 -0
  496. package/templates/b2b-support-web/package.json +34 -0
  497. package/templates/b2b-support-web/src/actions/captureTicketCreated.ts +14 -0
  498. package/templates/b2b-support-web/src/commands/closeTicket.ts +20 -0
  499. package/templates/b2b-support-web/src/commands/createTicket.ts +47 -0
  500. package/templates/b2b-support-web/src/commands/manageBilling.ts +9 -0
  501. package/templates/b2b-support-web/src/forge/schema.ts +35 -0
  502. package/templates/b2b-support-web/src/policies.ts +9 -0
  503. package/templates/b2b-support-web/src/queries/getTicket.ts +6 -0
  504. package/templates/b2b-support-web/src/queries/listTickets.ts +6 -0
  505. package/templates/b2b-support-web/src/queries/liveTickets.ts +9 -0
  506. package/templates/b2b-support-web/src/workflows/triageTicketWorkflow.ts +64 -0
  507. package/templates/b2b-support-web/tsconfig.json +14 -0
  508. package/templates/b2b-support-web/web/app/globals.css +77 -0
  509. package/templates/b2b-support-web/web/app/layout.tsx +13 -0
  510. package/templates/b2b-support-web/web/app/page.tsx +13 -0
  511. package/templates/b2b-support-web/web/app/providers.tsx +21 -0
  512. package/templates/b2b-support-web/web/app/tickets/page.tsx +21 -0
  513. package/templates/b2b-support-web/web/components/CreateTicketForm.tsx +43 -0
  514. package/templates/b2b-support-web/web/components/PolicyDeniedDemo.tsx +31 -0
  515. package/templates/b2b-support-web/web/components/TicketList.tsx +52 -0
  516. package/templates/b2b-support-web/web/components/TraceDetails.tsx +18 -0
  517. package/templates/b2b-support-web/web/components/TriageStatus.tsx +13 -0
  518. package/templates/b2b-support-web/web/lib/forge.ts +13 -0
  519. package/templates/b2b-support-web/web/next-env.d.ts +5 -0
  520. package/templates/b2b-support-web/web/next.config.ts +8 -0
  521. package/templates/b2b-support-web/web/package.json +21 -0
  522. package/templates/b2b-support-web/web/tsconfig.json +30 -0
  523. package/templates/minimal-web/.vscode/settings.json +14 -0
  524. package/templates/minimal-web/README.md +21 -0
  525. package/templates/minimal-web/forge.config.ts +3 -0
  526. package/templates/minimal-web/package.json +32 -0
  527. package/templates/minimal-web/src/actions/logNoteCreated.ts +11 -0
  528. package/templates/minimal-web/src/commands/createNote.ts +26 -0
  529. package/templates/minimal-web/src/forge/schema.ts +12 -0
  530. package/templates/minimal-web/src/policies.ts +6 -0
  531. package/templates/minimal-web/src/queries/listNotes.ts +8 -0
  532. package/templates/minimal-web/src/queries/liveNotes.ts +8 -0
  533. package/templates/minimal-web/tsconfig.json +15 -0
  534. package/templates/minimal-web/web/index.html +12 -0
  535. package/templates/minimal-web/web/package.json +21 -0
  536. package/templates/minimal-web/web/src/App.tsx +89 -0
  537. package/templates/minimal-web/web/src/lib/forge.ts +13 -0
  538. package/templates/minimal-web/web/src/main.tsx +13 -0
  539. package/templates/minimal-web/web/src/styles.css +156 -0
  540. package/templates/minimal-web/web/tsconfig.json +18 -0
@@ -0,0 +1,1160 @@
1
+ import { delimiter, dirname, join, relative, resolve } from "node:path";
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { nodeFileSystem } from "../compiler/fs/index.ts";
5
+ import { createDiagnostic } from "../compiler/diagnostics/create.ts";
6
+ import { hashStable } from "../compiler/primitives/hash.ts";
7
+ import { canonicalJson, serializeCanonical } from "../compiler/primitives/serialize.ts";
8
+ import { stripDeterministicHeader } from "../compiler/primitives/header.ts";
9
+ import type { Diagnostic } from "../compiler/types/diagnostic.ts";
10
+ import type { AppGraph } from "../compiler/types/app-graph.ts";
11
+ import type { DataGraph } from "../compiler/types/data-graph.ts";
12
+ import type { PackageGraph } from "../compiler/types/package-graph.ts";
13
+ import type { PolicyRegistry } from "../compiler/types/policy-registry.ts";
14
+ import type { RuntimeGraph } from "../compiler/types/runtime-graph.ts";
15
+ import type { QueryRegistry } from "../compiler/types/query-registry.ts";
16
+ import type { LiveQueryRegistry } from "../compiler/types/live-query-registry.ts";
17
+ import type { WorkflowRegistry, WorkflowSubscriptions } from "../compiler/types/workflow-registry.ts";
18
+ import type { ActionSubscriptions } from "../compiler/types/action-subscriptions.ts";
19
+ import type { TestCost, TestGraph } from "../compiler/types/test-graph.ts";
20
+ import { resolveCommandArgv } from "../compiler/package-manager/executor.ts";
21
+ import type {
22
+ ImpactCommandOptions,
23
+ ImpactReport,
24
+ ImpactRisk,
25
+ ImpactSource,
26
+ ImpactedSystems,
27
+ ImpactResult,
28
+ ImpactTestPlan,
29
+ TargetedTest,
30
+ TestCommandOptions,
31
+ TestPlanCheck,
32
+ TestRunRecord,
33
+ TestRunStep,
34
+ } from "./types.ts";
35
+
36
+ const GENERATED = "src/forge/_generated";
37
+ const TEST_PLAN_DIR = ".forge/test-plans";
38
+ const TEST_RUN_DIR = ".forge/test-runs";
39
+ const COST_ORDER: TestCost[] = ["instant", "fast", "standard", "slow", "docker", "browser"];
40
+ const DEFAULT_TEST_COMMAND_TIMEOUT_MS = 30 * 60 * 1000;
41
+
42
+ function diag(severity: Diagnostic["severity"], code: string, message: string, file?: string): Diagnostic {
43
+ return createDiagnostic({ severity, code, message, ...(file ? { file } : {}) });
44
+ }
45
+
46
+ function normalize(path: string): string {
47
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
48
+ }
49
+
50
+ function readJson<T>(workspaceRoot: string, relative: string, fallback: T): T {
51
+ const absolute = join(workspaceRoot, relative);
52
+ const content = nodeFileSystem.readText(absolute);
53
+ if (content === null) {
54
+ return fallback;
55
+ }
56
+ return JSON.parse(stripDeterministicHeader(content)) as T;
57
+ }
58
+
59
+ function emptyImpacted(): ImpactedSystems {
60
+ return {
61
+ data: { tables: [], fields: [] },
62
+ runtime: { commands: [], queries: [], liveQueries: [], actions: [], workflows: [] },
63
+ frontend: { components: [], pages: [] },
64
+ policies: [],
65
+ packages: [],
66
+ generatedArtifacts: [],
67
+ deploy: [],
68
+ };
69
+ }
70
+
71
+ function push(values: string[], value: string | undefined): void {
72
+ if (value && !values.includes(value)) {
73
+ values.push(value);
74
+ }
75
+ }
76
+
77
+ function sortImpact(impact: ImpactedSystems): ImpactedSystems {
78
+ for (const values of [
79
+ impact.data.tables,
80
+ impact.data.fields,
81
+ impact.runtime.commands,
82
+ impact.runtime.queries,
83
+ impact.runtime.liveQueries,
84
+ impact.runtime.actions,
85
+ impact.runtime.workflows,
86
+ impact.frontend.components,
87
+ impact.frontend.pages,
88
+ impact.policies,
89
+ impact.packages,
90
+ impact.generatedArtifacts,
91
+ impact.deploy,
92
+ ]) {
93
+ values.sort();
94
+ }
95
+ return impact;
96
+ }
97
+
98
+ function fileText(workspaceRoot: string, file: string): string {
99
+ try {
100
+ return nodeFileSystem.readText(join(workspaceRoot, file)) ?? "";
101
+ } catch {
102
+ return "";
103
+ }
104
+ }
105
+
106
+ function basenameNoExt(file: string): string {
107
+ const name = normalize(file).split("/").pop() ?? file;
108
+ return name.replace(/\.(test|spec)?\.?(ts|tsx|js|jsx|json|md|sql|yml|yaml)$/, "");
109
+ }
110
+
111
+ function componentName(file: string): string {
112
+ return basenameNoExt(file);
113
+ }
114
+
115
+ function git(args: string[], workspaceRoot: string): { ok: boolean; files: string[]; error?: string } {
116
+ const result = spawnSync("git", args, {
117
+ cwd: workspaceRoot,
118
+ encoding: "utf8",
119
+ windowsHide: true,
120
+ });
121
+ if (result.status !== 0) {
122
+ return { ok: false, files: [], error: result.stderr || result.stdout || "git command failed" };
123
+ }
124
+ return {
125
+ ok: true,
126
+ files: result.stdout.split(/\r?\n/).map(normalize).filter(Boolean).sort(),
127
+ };
128
+ }
129
+
130
+ function gitRoot(workspaceRoot: string): string | null {
131
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
132
+ cwd: workspaceRoot,
133
+ encoding: "utf8",
134
+ windowsHide: true,
135
+ });
136
+ if (result.status !== 0) {
137
+ return null;
138
+ }
139
+ return result.stdout.trim() || null;
140
+ }
141
+
142
+ function scopeGitFilesToWorkspace(workspaceRoot: string, files: string[]): string[] {
143
+ const root = gitRoot(workspaceRoot);
144
+ if (!root) {
145
+ return files;
146
+ }
147
+ const gitTop = normalize(resolve(root));
148
+ const workspace = normalize(resolve(workspaceRoot));
149
+ if (gitTop === workspace) {
150
+ return files;
151
+ }
152
+
153
+ const prefix = normalize(relative(gitTop, workspace));
154
+ if (!prefix || prefix.startsWith("..") || prefix.includes(":")) {
155
+ return files;
156
+ }
157
+
158
+ return files
159
+ .filter((file) => file.startsWith(`${prefix}/`))
160
+ .map((file) => file.slice(prefix.length + 1))
161
+ .filter(Boolean)
162
+ .sort();
163
+ }
164
+
165
+ function untrackedFiles(workspaceRoot: string): string[] {
166
+ const result = git(["ls-files", "--others", "--exclude-standard"], workspaceRoot);
167
+ return result.ok ? result.files : [];
168
+ }
169
+
170
+ function isVolatileForgeState(file: string): boolean {
171
+ const normalized = normalize(file);
172
+ return normalized.startsWith(".forge/locks/") ||
173
+ normalized.startsWith(".forge/test-runs/");
174
+ }
175
+
176
+ function sourceFromOptions(options: ImpactCommandOptions | TestCommandOptions): ImpactSource {
177
+ if (options.staged) return { mode: "staged", base: "index" };
178
+ if (options.since) return { mode: "since", base: options.since };
179
+ if (options.featureId) return { mode: "feature", id: options.featureId };
180
+ if (options.refactorId) return { mode: "refactor", id: options.refactorId };
181
+ if (options.upgradeId) return { mode: "upgrade", id: options.upgradeId };
182
+ return { mode: "changed", base: "HEAD" };
183
+ }
184
+
185
+ function readPlanFiles(workspaceRoot: string, source: ImpactSource): string[] {
186
+ const candidates =
187
+ source.mode === "feature"
188
+ ? [source.id ?? "", `.forge/features/plans/${source.id}/plan.json`]
189
+ : source.mode === "refactor"
190
+ ? [source.id ?? "", `.forge/refactors/${source.id}/plan.json`]
191
+ : source.mode === "upgrade"
192
+ ? [source.id ?? "", `.forge/upgrades/${source.id}/plan.json`]
193
+ : [];
194
+ for (const candidate of candidates) {
195
+ if (!candidate) continue;
196
+ const absolute = join(workspaceRoot, candidate);
197
+ const raw = nodeFileSystem.readText(absolute);
198
+ if (raw === null) continue;
199
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
200
+ const files = [
201
+ ...(((parsed.filesToCreate as Array<{ file?: string }>) ?? []).map((file) => file.file)),
202
+ ...(((parsed.filesToModify as Array<{ file?: string }>) ?? []).map((file) => file.file)),
203
+ ...(((parsed.filesToDelete as Array<{ file?: string }>) ?? []).map((file) => file.file)),
204
+ ...(((parsed.generatedChanges as Array<{ file?: string }>) ?? []).map((file) => file.file)),
205
+ ...(((parsed.affected as { files?: string[] } | undefined)?.files) ?? []),
206
+ ];
207
+ return files.filter((file): file is string => Boolean(file)).map(normalize).sort();
208
+ }
209
+ return [];
210
+ }
211
+
212
+ export function detectChangedFiles(
213
+ workspaceRoot: string,
214
+ source: ImpactSource,
215
+ ): { files: string[]; diagnostics: Diagnostic[] } {
216
+ if (source.mode === "feature" || source.mode === "refactor" || source.mode === "upgrade") {
217
+ return { files: readPlanFiles(workspaceRoot, source), diagnostics: [] };
218
+ }
219
+ const diagnostics: Diagnostic[] = [];
220
+ const result =
221
+ source.mode === "staged"
222
+ ? git(["diff", "--cached", "--name-only"], workspaceRoot)
223
+ : source.mode === "since" && source.base
224
+ ? git(["diff", "--name-only", source.base, "--"], workspaceRoot)
225
+ : git(["diff", "--name-only", "HEAD", "--"], workspaceRoot);
226
+ if (!result.ok) {
227
+ diagnostics.push(diag("error", "FORGE_IMPACT_GIT_UNAVAILABLE", result.error ?? "git unavailable"));
228
+ return { files: [], diagnostics };
229
+ }
230
+ const files = new Set(result.files);
231
+ if (source.mode === "changed") {
232
+ for (const file of untrackedFiles(workspaceRoot)) {
233
+ files.add(file);
234
+ }
235
+ }
236
+ return {
237
+ files: scopeGitFilesToWorkspace(workspaceRoot, [...files].sort()).filter(
238
+ (file) => !isVolatileForgeState(file),
239
+ ),
240
+ diagnostics,
241
+ };
242
+ }
243
+
244
+ function addRuntimeUsingTables(args: {
245
+ impact: ImpactedSystems;
246
+ tableNames: string[];
247
+ appGraph: AppGraph;
248
+ queryRegistry: QueryRegistry;
249
+ liveQueryRegistry: LiveQueryRegistry;
250
+ runtimeGraph: RuntimeGraph;
251
+ }): void {
252
+ for (const table of args.tableNames) {
253
+ for (const symbol of args.appGraph.symbols) {
254
+ if (!["command", "query", "liveQuery", "action", "workflow"].includes(symbol.kind)) continue;
255
+ const metaText = canonicalJson(symbol.meta).toLowerCase();
256
+ const fileTextValue = symbol.file ? "" : "";
257
+ if (!metaText.includes(table.toLowerCase()) && !fileTextValue.includes(table.toLowerCase())) {
258
+ continue;
259
+ }
260
+ if (symbol.kind === "command") push(args.impact.runtime.commands, symbol.name);
261
+ if (symbol.kind === "query") push(args.impact.runtime.queries, symbol.name);
262
+ if (symbol.kind === "liveQuery") push(args.impact.runtime.liveQueries, symbol.name);
263
+ if (symbol.kind === "action") push(args.impact.runtime.actions, symbol.name);
264
+ if (symbol.kind === "workflow") push(args.impact.runtime.workflows, symbol.name);
265
+ }
266
+ }
267
+ if (args.tableNames.length > 0) {
268
+ for (const entry of args.runtimeGraph.entries) {
269
+ push(entry.kind === "command" ? args.impact.runtime.commands : args.impact.runtime.actions, entry.name);
270
+ }
271
+ for (const query of args.queryRegistry.queries) push(args.impact.runtime.queries, query.name);
272
+ for (const liveQuery of args.liveQueryRegistry.liveQueries) push(args.impact.runtime.liveQueries, liveQuery.name);
273
+ }
274
+ }
275
+
276
+ function eventsFromText(text: string): string[] {
277
+ const events: string[] = [];
278
+ const regex = /ctx\.emit\s*\(\s*["'`]([^"'`]+)["'`]/g;
279
+ let match: RegExpExecArray | null;
280
+ while ((match = regex.exec(text))) {
281
+ push(events, match[1]);
282
+ }
283
+ return events.sort();
284
+ }
285
+
286
+ function importsPackage(text: string, packageName: string): boolean {
287
+ const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
288
+ return new RegExp(
289
+ `(?:from\\s+["']${escaped}(?:/[^"']*)?["']|import\\s*\\(\\s*["']${escaped}(?:/[^"']*)?["']\\s*\\)|require\\s*\\(\\s*["']${escaped}(?:/[^"']*)?["']\\s*\\))`,
290
+ ).test(text);
291
+ }
292
+
293
+ function isPackageDependencyFile(file: string): boolean {
294
+ return [
295
+ "package.json",
296
+ "package-lock.json",
297
+ "npm-shrinkwrap.json",
298
+ "pnpm-lock.yaml",
299
+ "yarn.lock",
300
+ "bun.lock",
301
+ "bun.lockb",
302
+ ].includes(normalize(file));
303
+ }
304
+
305
+ const PACKAGE_JSON_DEPENDENCY_KEYS = [
306
+ "dependencies",
307
+ "devDependencies",
308
+ "optionalDependencies",
309
+ "peerDependencies",
310
+ "packageManager",
311
+ "overrides",
312
+ "resolutions",
313
+ "engines",
314
+ ] as const;
315
+
316
+ function readHeadFile(workspaceRoot: string, file: string): string | null {
317
+ const root = gitRoot(workspaceRoot);
318
+ if (!root) {
319
+ return null;
320
+ }
321
+
322
+ const relativeToGitRoot = normalize(relative(resolve(root), resolve(workspaceRoot, file)));
323
+ if (!relativeToGitRoot || relativeToGitRoot.startsWith("..") || relativeToGitRoot.includes(":")) {
324
+ return null;
325
+ }
326
+
327
+ const result = spawnSync("git", ["show", `HEAD:${relativeToGitRoot}`], {
328
+ cwd: workspaceRoot,
329
+ encoding: "utf8",
330
+ windowsHide: true,
331
+ });
332
+ if (result.status !== 0) {
333
+ return null;
334
+ }
335
+ return result.stdout;
336
+ }
337
+
338
+ function packageJsonDependencyFingerprint(raw: string): string | null {
339
+ try {
340
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
341
+ const relevant: Record<string, unknown> = {};
342
+ for (const key of PACKAGE_JSON_DEPENDENCY_KEYS) {
343
+ if (Object.prototype.hasOwnProperty.call(parsed, key)) {
344
+ relevant[key] = parsed[key];
345
+ }
346
+ }
347
+ return canonicalJson(relevant);
348
+ } catch {
349
+ return null;
350
+ }
351
+ }
352
+
353
+ function packageJsonHasDependencyImpact(workspaceRoot: string): boolean {
354
+ const current = nodeFileSystem.readText(join(workspaceRoot, "package.json"));
355
+ const previous = readHeadFile(workspaceRoot, "package.json");
356
+ if (current === null || previous === null) {
357
+ return true;
358
+ }
359
+
360
+ const currentFingerprint = packageJsonDependencyFingerprint(current);
361
+ const previousFingerprint = packageJsonDependencyFingerprint(previous);
362
+ if (currentFingerprint === null || previousFingerprint === null) {
363
+ return true;
364
+ }
365
+ return currentFingerprint !== previousFingerprint;
366
+ }
367
+
368
+ function packageDependencyFileImpactsPackages(workspaceRoot: string, file: string): boolean {
369
+ const normalized = normalize(file);
370
+ if (!isPackageDependencyFile(normalized)) {
371
+ return false;
372
+ }
373
+ if (normalized !== "package.json") {
374
+ return true;
375
+ }
376
+ return packageJsonHasDependencyImpact(workspaceRoot);
377
+ }
378
+
379
+ function analyzeFiles(args: {
380
+ workspaceRoot: string;
381
+ files: string[];
382
+ includeGenerated: boolean;
383
+ excludeTests: boolean;
384
+ appGraph: AppGraph;
385
+ dataGraph: DataGraph;
386
+ packageGraph: PackageGraph;
387
+ runtimeGraph: RuntimeGraph;
388
+ queryRegistry: QueryRegistry;
389
+ liveQueryRegistry: LiveQueryRegistry;
390
+ policyRegistry: PolicyRegistry;
391
+ actionSubscriptions: ActionSubscriptions;
392
+ workflowRegistry: WorkflowRegistry;
393
+ workflowSubscriptions: WorkflowSubscriptions;
394
+ }): ImpactedSystems {
395
+ const impact = emptyImpacted();
396
+ const tableNames = args.dataGraph.tables.map((table) => table.name).sort();
397
+ const policyNames = args.policyRegistry.policies.map((policy) => policy.name).sort();
398
+ const packageNames = args.packageGraph.packages.map((pkg) => pkg.name).sort();
399
+
400
+ for (const file of args.files) {
401
+ if (!args.includeGenerated && file.startsWith(`${GENERATED}/`)) {
402
+ push(impact.generatedArtifacts, file);
403
+ continue;
404
+ }
405
+ if (file === "forge.lock") {
406
+ push(impact.generatedArtifacts, file);
407
+ continue;
408
+ }
409
+ if (args.excludeTests && /\.(test|spec)\.(ts|tsx)$/.test(file)) {
410
+ continue;
411
+ }
412
+ const text = fileText(args.workspaceRoot, file);
413
+ const lowered = file.toLowerCase();
414
+ const packageDependencyImpact = packageDependencyFileImpactsPackages(args.workspaceRoot, file);
415
+
416
+ if (file === "src/schema.ts" || file === "src/forge/schema.ts" || lowered.includes("datagraph")) {
417
+ for (const table of tableNames) push(impact.data.tables, table);
418
+ addRuntimeUsingTables({ impact, tableNames, appGraph: args.appGraph, queryRegistry: args.queryRegistry, liveQueryRegistry: args.liveQueryRegistry, runtimeGraph: args.runtimeGraph });
419
+ for (const policy of policyNames) push(impact.policies, policy);
420
+ push(impact.generatedArtifacts, "src/forge/_generated/db.ts");
421
+ push(impact.generatedArtifacts, "src/forge/_generated/clientTypes.ts");
422
+ }
423
+
424
+ for (const table of tableNames) {
425
+ if (text.includes(table) || file.includes(table)) {
426
+ push(impact.data.tables, table);
427
+ }
428
+ }
429
+ for (const policy of policyNames) {
430
+ if (text.includes(policy) || file.includes(policy)) {
431
+ push(impact.policies, policy);
432
+ }
433
+ }
434
+ for (const pkg of packageNames) {
435
+ if (importsPackage(text, pkg) || packageDependencyImpact) {
436
+ push(impact.packages, pkg);
437
+ }
438
+ }
439
+
440
+ if (lowered.includes("/commands/")) {
441
+ const name = basenameNoExt(file);
442
+ push(impact.runtime.commands, name);
443
+ for (const event of eventsFromText(text)) {
444
+ for (const sub of args.actionSubscriptions.byEvent?.[event] ?? []) push(impact.runtime.actions, sub.actionName);
445
+ for (const sub of args.workflowSubscriptions.byEvent?.[event] ?? []) push(impact.runtime.workflows, sub.workflowName);
446
+ }
447
+ } else if (lowered.includes("/queries/")) {
448
+ const name = basenameNoExt(file);
449
+ if (name.toLowerCase().startsWith("live")) push(impact.runtime.liveQueries, name);
450
+ else push(impact.runtime.queries, name);
451
+ } else if (lowered.includes("/actions/")) {
452
+ push(impact.runtime.actions, basenameNoExt(file));
453
+ } else if (lowered.includes("/workflows/")) {
454
+ push(impact.runtime.workflows, basenameNoExt(file));
455
+ }
456
+
457
+ if (lowered.includes("/components/") && file.endsWith(".tsx")) {
458
+ push(impact.frontend.components, componentName(file));
459
+ }
460
+ if (lowered.includes("/app/") && file.endsWith(".tsx")) {
461
+ push(impact.frontend.pages, file.replace(/^web\/app/, "").replace(/\/page\.tsx$/, "") || "/");
462
+ }
463
+ if (["dockerfile", "docker-compose.yml", "docker-compose.yaml"].some((name) => lowered.endsWith(name)) || lowered.includes("/deploy/")) {
464
+ push(impact.deploy, file);
465
+ }
466
+ }
467
+
468
+ if (impact.data.tables.length > 0) {
469
+ addRuntimeUsingTables({ impact, tableNames: impact.data.tables, appGraph: args.appGraph, queryRegistry: args.queryRegistry, liveQueryRegistry: args.liveQueryRegistry, runtimeGraph: args.runtimeGraph });
470
+ }
471
+
472
+ return sortImpact(impact);
473
+ }
474
+
475
+ function riskFor(impact: ImpactedSystems, files: string[]): ImpactRisk {
476
+ const reasons: string[] = [];
477
+ let level: ImpactRisk["level"] = "low";
478
+ if (impact.data.tables.length > 0) {
479
+ level = "high";
480
+ reasons.push("DataGraph or schema impact detected");
481
+ }
482
+ if (impact.policies.length > 0) {
483
+ level = "high";
484
+ reasons.push("Policy surface changed");
485
+ }
486
+ if (impact.runtime.liveQueries.length > 0 || impact.runtime.workflows.length > 0) {
487
+ if (level === "low") level = "medium";
488
+ reasons.push("Reactive or workflow runtime impacted");
489
+ }
490
+ if (impact.runtime.commands.length > 0 || impact.runtime.queries.length > 0 || impact.runtime.actions.length > 0) {
491
+ if (level === "low") level = "medium";
492
+ reasons.push("Runtime entry impacted");
493
+ }
494
+ if (impact.packages.length > 0) {
495
+ level = "high";
496
+ reasons.push("Package graph or lockfile impact detected");
497
+ }
498
+ if (impact.deploy.length > 0) {
499
+ level = "high";
500
+ reasons.push("Deploy or release artifact impact detected");
501
+ }
502
+ if (files.some((file) => file.startsWith(`${GENERATED}/`))) {
503
+ if (level === "low") level = "medium";
504
+ reasons.push("Generated artifact changed");
505
+ }
506
+ return { level, reasons: reasons.sort() };
507
+ }
508
+
509
+ function loadArtifacts(workspaceRoot: string) {
510
+ return {
511
+ appGraph: readJson<AppGraph>(workspaceRoot, `${GENERATED}/appGraph.json`, { schemaVersion: "", generatorVersion: "", analyzerVersion: "", inputHash: "", symbols: [], edges: [], moduleGraph: { nodes: [] }, diagnostics: [] }),
512
+ dataGraph: readJson<DataGraph>(workspaceRoot, `${GENERATED}/dataGraph.json`, { schemaVersion: "", generatorVersion: "", analyzerVersion: "", inputHash: "", tables: [], diagnostics: [] }),
513
+ packageGraph: readJson<PackageGraph>(workspaceRoot, `${GENERATED}/packageGraph.json`, { schemaVersion: "", generatorVersion: "", analyzerVersion: "", packages: [] }),
514
+ runtimeGraph: readJson<RuntimeGraph>(workspaceRoot, `${GENERATED}/runtimeGraph.json`, { schemaVersion: "", generatorVersion: "", analyzerVersion: "", inputHash: "", entries: [], diagnostics: [] }),
515
+ queryRegistry: readJson<QueryRegistry>(workspaceRoot, `${GENERATED}/queryRegistry.json`, { schemaVersion: "", generatorVersion: "", analyzerVersion: "", inputHash: "", queries: [], diagnostics: [] }),
516
+ liveQueryRegistry: readJson<LiveQueryRegistry>(workspaceRoot, `${GENERATED}/liveQueryRegistry.json`, { schemaVersion: "", generatorVersion: "", analyzerVersion: "", inputHash: "", liveQueries: [], diagnostics: [] }),
517
+ policyRegistry: readJson<PolicyRegistry>(workspaceRoot, `${GENERATED}/policyRegistry.json`, { schemaVersion: "", generatorVersion: "", analyzerVersion: "", inputHash: "", policies: [], commandAuth: [], queryAuth: [], diagnostics: [] }),
518
+ actionSubscriptions: readJson<ActionSubscriptions>(workspaceRoot, `${GENERATED}/actionSubscriptions.json`, { schemaVersion: "", generatorVersion: "", analyzerVersion: "", inputHash: "", subscriptions: [], byEvent: {}, diagnostics: [] }),
519
+ workflowRegistry: readJson<WorkflowRegistry>(workspaceRoot, `${GENERATED}/workflowRegistry.json`, { schemaVersion: "", generatorVersion: "", analyzerVersion: "", inputHash: "", workflows: [], diagnostics: [] }),
520
+ workflowSubscriptions: readJson<WorkflowSubscriptions>(workspaceRoot, `${GENERATED}/workflowSubscriptions.json`, { schemaVersion: "", generatorVersion: "", analyzerVersion: "", inputHash: "", subscriptions: [], byEvent: {}, diagnostics: [] }),
521
+ testGraph: readJson<TestGraph>(workspaceRoot, `${GENERATED}/testGraph.json`, { schemaVersion: "0.1.0", generatorVersion: "", analyzerVersion: "", inputHash: "", tests: [], diagnostics: [] }),
522
+ };
523
+ }
524
+
525
+ function readLastRunByCommand(workspaceRoot: string): Map<string, TestRunStep> {
526
+ const raw = nodeFileSystem.readText(join(workspaceRoot, TEST_RUN_DIR, "last.json"));
527
+ if (raw === null) {
528
+ return new Map();
529
+ }
530
+ try {
531
+ const record = JSON.parse(stripDeterministicHeader(raw)) as TestRunRecord;
532
+ return new Map(record.results.map((result) => [result.command, result]));
533
+ } catch {
534
+ return new Map();
535
+ }
536
+ }
537
+
538
+ function requiredChecks(impact: ImpactedSystems): TestPlanCheck[] {
539
+ const checks: TestPlanCheck[] = [
540
+ { kind: "forge", command: "forge generate --check", cost: "fast", reason: "generated artifacts must stay deterministic" },
541
+ { kind: "forge", command: "forge check", cost: "instant", reason: "runtime guards and static Forge checks" },
542
+ ];
543
+ if (impact.data.tables.length > 0) {
544
+ checks.push({ kind: "forge", command: "forge db diff --json", cost: "standard", reason: "schema/table impact requires migration diff" });
545
+ checks.push({ kind: "forge", command: "forge rls check --json", cost: "standard", reason: "data impact can affect tenant isolation" });
546
+ }
547
+ if (impact.policies.length > 0) {
548
+ checks.push({ kind: "forge", command: "forge policy check --strict-policies", cost: "fast", reason: "policy impact detected" });
549
+ }
550
+ if (impact.packages.length > 0) {
551
+ checks.push({ kind: "forge", command: "forge deps upgrade-check --json", cost: "standard", reason: "package impact detected" });
552
+ }
553
+ if (impact.deploy.length > 0) {
554
+ checks.push({ kind: "forge", command: "forge self-host check", cost: "standard", reason: "deployment impact detected" });
555
+ }
556
+ return checks;
557
+ }
558
+
559
+ function costAllowed(cost: TestCost, maxCost: TestCost, includeDocker: boolean, includeBrowser: boolean): boolean {
560
+ if (cost === "docker" && !includeDocker) return false;
561
+ if (cost === "browser" && !includeBrowser) return false;
562
+ return COST_ORDER.indexOf(cost) <= COST_ORDER.indexOf(maxCost);
563
+ }
564
+
565
+ function intersects(a: string[], b: string[]): string | null {
566
+ for (const value of a) {
567
+ if (b.includes(value)) return value;
568
+ }
569
+ return null;
570
+ }
571
+
572
+ function selectTests(
573
+ testGraph: TestGraph,
574
+ impact: ImpactedSystems,
575
+ changedFiles: string[],
576
+ options: {
577
+ workspaceRoot: string;
578
+ maxCost: TestCost;
579
+ includeDocker: boolean;
580
+ includeBrowser: boolean;
581
+ lastRunByCommand: Map<string, TestRunStep>;
582
+ },
583
+ ): TargetedTest[] {
584
+ const selected: TargetedTest[] = [];
585
+ for (const test of testGraph.tests) {
586
+ const changedTestFile = changedFiles.includes(test.file);
587
+ if (test.confidence === "weak" && !changedTestFile) continue;
588
+ if (!costAllowed(test.cost, options.maxCost, options.includeDocker, options.includeBrowser)) continue;
589
+ const reason =
590
+ intersects(test.covers.commands, impact.runtime.commands)?.replace(/^/, "covers impacted command ") ??
591
+ intersects(test.covers.queries, impact.runtime.queries)?.replace(/^/, "covers impacted query ") ??
592
+ intersects(test.covers.liveQueries, impact.runtime.liveQueries)?.replace(/^/, "covers impacted liveQuery ") ??
593
+ intersects(test.covers.actions, impact.runtime.actions)?.replace(/^/, "covers impacted action ") ??
594
+ intersects(test.covers.workflows, impact.runtime.workflows)?.replace(/^/, "covers impacted workflow ") ??
595
+ intersects(test.covers.tables, impact.data.tables)?.replace(/^/, "covers impacted table ") ??
596
+ intersects(test.covers.policies, impact.policies)?.replace(/^/, "covers impacted policy ") ??
597
+ intersects(test.covers.components, impact.frontend.components)?.replace(/^/, "covers impacted component ") ??
598
+ intersects(test.covers.packages, impact.packages)?.replace(/^/, "covers impacted package ");
599
+ if (!reason && !changedTestFile) continue;
600
+ const command = testCommandForFile(options.workspaceRoot, test.file);
601
+ const legacyCommand = legacyBunTestCommand(test.file);
602
+ const lastRun =
603
+ options.lastRunByCommand.get(command) ?? options.lastRunByCommand.get(legacyCommand);
604
+ selected.push({
605
+ file: test.file,
606
+ command,
607
+ reason: reason ?? "changed test file",
608
+ cost: test.cost,
609
+ confidence: test.confidence,
610
+ ...(lastRun ? { lastDurationMs: lastRun.durationMs, lastRunOk: lastRun.ok } : {}),
611
+ });
612
+ }
613
+ return selected.sort((a, b) => {
614
+ const failedBias = Number(a.lastRunOk === false) - Number(b.lastRunOk === false);
615
+ if (failedBias !== 0) return -failedBias;
616
+ const costBias = COST_ORDER.indexOf(a.cost) - COST_ORDER.indexOf(b.cost);
617
+ if (costBias !== 0) return costBias;
618
+ const durationBias = (a.lastDurationMs ?? Number.MAX_SAFE_INTEGER) - (b.lastDurationMs ?? Number.MAX_SAFE_INTEGER);
619
+ if (durationBias !== 0) return durationBias;
620
+ return a.file.localeCompare(b.file);
621
+ });
622
+ }
623
+
624
+ function legacyBunTestCommand(file: string): string {
625
+ return `bun test ${file}`;
626
+ }
627
+
628
+ function testCommandForFile(workspaceRoot: string, file: string): string {
629
+ if (nodeFileSystem.exists(join(workspaceRoot, "bin", "forge-bun.mjs"))) {
630
+ return `node ./bin/forge-bun.mjs test ${file}`;
631
+ }
632
+ return legacyBunTestCommand(file);
633
+ }
634
+
635
+ function selectUiScenarioChecks(
636
+ impact: ImpactedSystems,
637
+ includeBrowser: boolean,
638
+ ): TestPlanCheck[] {
639
+ if (!includeBrowser) return [];
640
+ const checks: TestPlanCheck[] = [];
641
+ const hasFrontend = impact.frontend.components.length > 0 || impact.frontend.pages.length > 0;
642
+ if (hasFrontend) {
643
+ checks.push({
644
+ kind: "forge",
645
+ command: "forge ui smoke --scenario home-loads",
646
+ cost: "browser",
647
+ reason: "frontend impact should load in a browser",
648
+ });
649
+ }
650
+ if (impact.runtime.liveQueries.length > 0 || impact.data.tables.length > 0) {
651
+ checks.push({
652
+ kind: "forge",
653
+ command: "forge ui smoke --scenario tickets-live-update",
654
+ cost: "browser",
655
+ reason: "liveQuery/data impact should verify browser reactivity",
656
+ });
657
+ }
658
+ if (impact.policies.length > 0) {
659
+ checks.push({
660
+ kind: "forge",
661
+ command: "forge ui smoke --scenario policy-denied-visible",
662
+ cost: "browser",
663
+ reason: "policy impact should verify browser error/traceId handling",
664
+ });
665
+ }
666
+ return checks;
667
+ }
668
+
669
+ export function analyzeImpact(options: ImpactCommandOptions): ImpactReport {
670
+ const source = sourceFromOptions(options);
671
+ const detected = detectChangedFiles(options.workspaceRoot, source);
672
+ const artifacts = loadArtifacts(options.workspaceRoot);
673
+ const changedFiles = detected.files;
674
+ const impacted = analyzeFiles({
675
+ workspaceRoot: options.workspaceRoot,
676
+ files: changedFiles,
677
+ includeGenerated: options.includeGenerated,
678
+ excludeTests: options.excludeTests,
679
+ ...artifacts,
680
+ });
681
+ const risk = riskFor(impacted, changedFiles);
682
+ const checks = requiredChecks(impacted).map((check) => check.command);
683
+ return {
684
+ ok: detected.diagnostics.every((diagnostic) => diagnostic.severity !== "error"),
685
+ source,
686
+ changedFiles,
687
+ impacted,
688
+ risk,
689
+ recommendedChecks: checks,
690
+ finalVerification: ["forge verify --strict"],
691
+ diagnostics: detected.diagnostics,
692
+ exitCode: detected.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0,
693
+ };
694
+ }
695
+
696
+ export function buildImpactTestPlan(options: TestCommandOptions): ImpactTestPlan {
697
+ const report = analyzeImpact({
698
+ workspaceRoot: options.workspaceRoot,
699
+ json: options.json,
700
+ write: false,
701
+ changed: options.changed,
702
+ staged: options.staged,
703
+ since: options.since,
704
+ featureId: options.featureId,
705
+ refactorId: options.refactorId,
706
+ upgradeId: options.upgradeId,
707
+ includeGenerated: false,
708
+ excludeTests: false,
709
+ });
710
+ const artifacts = loadArtifacts(options.workspaceRoot);
711
+ const lastRunByCommand = readLastRunByCommand(options.workspaceRoot);
712
+ const tests = selectTests(artifacts.testGraph, report.impacted, report.changedFiles, {
713
+ workspaceRoot: options.workspaceRoot,
714
+ maxCost: options.maxCost,
715
+ includeDocker: options.includeDocker,
716
+ includeBrowser: options.includeBrowser,
717
+ lastRunByCommand,
718
+ });
719
+ return {
720
+ schemaVersion: "0.1.0",
721
+ source: report.source,
722
+ changedFiles: report.changedFiles,
723
+ impacted: report.impacted,
724
+ risk: report.risk,
725
+ requiredChecks: [
726
+ ...requiredChecks(report.impacted),
727
+ ...selectUiScenarioChecks(report.impacted, options.includeBrowser),
728
+ ].filter((check) =>
729
+ costAllowed(check.cost, options.maxCost, options.includeDocker, options.includeBrowser),
730
+ ),
731
+ tests,
732
+ optionalChecks: ([
733
+ { kind: "forge", command: "forge verify --standard", cost: "standard", reason: "TypeScript surface changed" },
734
+ ] satisfies TestPlanCheck[]).filter((check) =>
735
+ costAllowed(check.cost, options.maxCost, options.includeDocker, options.includeBrowser),
736
+ ),
737
+ finalVerification: ["forge verify --strict"],
738
+ };
739
+ }
740
+
741
+ export function writeTestPlan(workspaceRoot: string, plan: ImpactTestPlan): string {
742
+ const dir = join(workspaceRoot, TEST_PLAN_DIR, plan.source.mode);
743
+ nodeFileSystem.mkdirp(dir);
744
+ const jsonPath = join(dir, "plan.json");
745
+ nodeFileSystem.writeText(jsonPath, serializeCanonical(plan));
746
+ nodeFileSystem.writeText(join(dir, "plan.md"), renderTestPlanMarkdown(plan));
747
+ return normalize(jsonPath.replace(`${workspaceRoot}/`, ""));
748
+ }
749
+
750
+ export function renderTestPlanMarkdown(plan: ImpactTestPlan): string {
751
+ const tests = plan.tests
752
+ .map((test) => {
753
+ const notes = [
754
+ test.lastDurationMs !== undefined ? `last ${test.lastDurationMs}ms` : null,
755
+ test.lastRunOk === false ? "failed last run" : null,
756
+ ].filter(Boolean).join(", ");
757
+ return notes ? `${test.command} # ${notes}` : test.command;
758
+ })
759
+ .join("\n") || "# no targeted tests selected";
760
+ const checks = plan.requiredChecks.map((check) => check.command).join("\n") || "# no checks";
761
+ return `# Test Plan
762
+
763
+ Risk: ${plan.risk.level}
764
+
765
+ ## Changed Files
766
+
767
+ ${plan.changedFiles.map((file) => `- ${file}`).join("\n") || "- none"}
768
+
769
+ ## Required Checks
770
+
771
+ \`\`\`bash
772
+ ${checks}
773
+ \`\`\`
774
+
775
+ ## Targeted Tests
776
+
777
+ \`\`\`bash
778
+ ${tests}
779
+ \`\`\`
780
+
781
+ ## Final
782
+
783
+ \`\`\`bash
784
+ forge verify --strict
785
+ \`\`\`
786
+ `;
787
+ }
788
+
789
+ function localBinExecutable(workspaceRoot: string, command: string): string | null {
790
+ if (
791
+ command.includes("\\") ||
792
+ command.includes("/") ||
793
+ command.includes(":") ||
794
+ /\.[a-z0-9]+$/i.test(command)
795
+ ) {
796
+ return null;
797
+ }
798
+ const extensions = process.platform === "win32" ? [".cmd", ".exe", ".bat", ""] : [""];
799
+ for (const extension of extensions) {
800
+ const candidate = join(workspaceRoot, "node_modules", ".bin", `${command}${extension}`);
801
+ if (nodeFileSystem.exists(candidate)) {
802
+ return candidate;
803
+ }
804
+ }
805
+ return null;
806
+ }
807
+
808
+ function sourceRepoForgeBin(): string | null {
809
+ const candidate = fileURLToPath(new URL("../../../bin/forge.mjs", import.meta.url));
810
+ return nodeFileSystem.exists(candidate) ? candidate : null;
811
+ }
812
+
813
+ function nodeForgeArgs(binPath: string, args: string[]): { executable: string; args: string[] } {
814
+ const argv = resolveCommandArgv(["node", binPath, ...args]);
815
+ return { executable: argv[0]!, args: argv.slice(1) };
816
+ }
817
+
818
+ function addBunTestTimeout(command: string, timeoutMs: number): string {
819
+ const parts = command.split(/\s+/).filter(Boolean);
820
+ const usesBunWrapper =
821
+ parts[0] === "node" &&
822
+ parts[1]?.replace(/\\/g, "/").endsWith("bin/forge-bun.mjs") &&
823
+ parts[2] === "test";
824
+ if (
825
+ ((parts[0] === "bun" && parts[1] === "test") || usesBunWrapper) &&
826
+ !parts.some((part) => part === "--timeout" || part.startsWith("--timeout="))
827
+ ) {
828
+ return `${command} --timeout ${timeoutMs}`;
829
+ }
830
+ return command;
831
+ }
832
+
833
+ function commandArgs(workspaceRoot: string, command: string): { executable: string; args: string[] } {
834
+ const parts = command.split(/\s+/).filter(Boolean);
835
+ if (parts.length === 0) {
836
+ return { executable: process.execPath, args: ["-e", "process.exit(0)"] };
837
+ }
838
+ if (parts[0] === "forge") {
839
+ const localForge = localBinExecutable(workspaceRoot, "forge");
840
+ if (localForge) {
841
+ return { executable: localForge, args: parts.slice(1) };
842
+ }
843
+ const frameworkBin = join(workspaceRoot, "bin", "forge.mjs");
844
+ if (nodeFileSystem.exists(frameworkBin)) {
845
+ return nodeForgeArgs(frameworkBin, parts.slice(1));
846
+ }
847
+ const sourceBin = sourceRepoForgeBin();
848
+ if (sourceBin) {
849
+ return nodeForgeArgs(sourceBin, parts.slice(1));
850
+ }
851
+ }
852
+ const local = localBinExecutable(workspaceRoot, parts[0]!);
853
+ const argv = resolveCommandArgv([local ?? parts[0]!, ...parts.slice(1)]);
854
+ return { executable: argv[0]!, args: argv.slice(1) };
855
+ }
856
+
857
+ function resolveTestCommandTimeoutMs(timeoutMs?: number): number {
858
+ if (timeoutMs && Number.isFinite(timeoutMs) && timeoutMs >= 1) {
859
+ return Math.floor(timeoutMs);
860
+ }
861
+ const fromEnv = process.env.FORGE_TEST_COMMAND_TIMEOUT_MS;
862
+ if (fromEnv) {
863
+ const parsed = Number(fromEnv);
864
+ if (Number.isFinite(parsed) && parsed >= 1) {
865
+ return Math.floor(parsed);
866
+ }
867
+ }
868
+ return DEFAULT_TEST_COMMAND_TIMEOUT_MS;
869
+ }
870
+
871
+ function inferFailureKind(command: string, exitCode: number, stdout: string, stderr: string): string | undefined {
872
+ if (exitCode === 0) {
873
+ return undefined;
874
+ }
875
+ const output = `${stdout}\n${stderr}`;
876
+ if (
877
+ command.startsWith("forge generate --check") &&
878
+ output.includes("FORGE_DRIFT")
879
+ ) {
880
+ return "generated-drift";
881
+ }
882
+ return "test-failure";
883
+ }
884
+
885
+ function runCommand(workspaceRoot: string, command: string, timeoutMs: number): Promise<TestRunStep> {
886
+ const started = Date.now();
887
+ let resolved: { executable: string; args: string[] };
888
+ const effectiveCommand = addBunTestTimeout(command, timeoutMs);
889
+ try {
890
+ resolved = commandArgs(workspaceRoot, effectiveCommand);
891
+ } catch (error) {
892
+ return Promise.resolve({
893
+ command: effectiveCommand,
894
+ ok: false,
895
+ exitCode: 1,
896
+ durationMs: Date.now() - started,
897
+ failureKind: "command-resolution-error",
898
+ stderr: error instanceof Error ? error.message : String(error),
899
+ });
900
+ }
901
+ const path = [join(workspaceRoot, "node_modules", ".bin"), process.env.PATH ?? ""].filter(Boolean).join(delimiter);
902
+ return new Promise((resolve) => {
903
+ let settled = false;
904
+ let timedOut = false;
905
+ const child = spawn(resolved.executable, resolved.args, {
906
+ cwd: workspaceRoot,
907
+ env: { ...process.env, PATH: path },
908
+ stdio: ["ignore", "pipe", "pipe"],
909
+ windowsHide: true,
910
+ });
911
+ const timer = setTimeout(() => {
912
+ timedOut = true;
913
+ try {
914
+ child.kill();
915
+ } catch {
916
+ // Process may have already exited.
917
+ }
918
+ }, timeoutMs);
919
+ let stdout = "";
920
+ let stderr = "";
921
+ child.stdout?.on("data", (chunk) => { stdout += String(chunk); });
922
+ child.stderr?.on("data", (chunk) => { stderr += String(chunk); });
923
+ child.on("error", (error) => {
924
+ clearTimeout(timer);
925
+ if (settled) return;
926
+ settled = true;
927
+ resolve({
928
+ command: effectiveCommand,
929
+ ok: false,
930
+ exitCode: 1,
931
+ durationMs: Date.now() - started,
932
+ failureKind: "spawn-error",
933
+ stderr: error.message,
934
+ });
935
+ });
936
+ child.on("close", (code) => {
937
+ clearTimeout(timer);
938
+ if (settled) return;
939
+ settled = true;
940
+ resolve({
941
+ command: effectiveCommand,
942
+ ok: !timedOut && (code ?? 1) === 0,
943
+ exitCode: timedOut ? 1 : code ?? 1,
944
+ durationMs: Date.now() - started,
945
+ timedOut,
946
+ failureKind: timedOut ? "timeout" : inferFailureKind(effectiveCommand, code ?? 1, stdout, stderr),
947
+ stdout,
948
+ stderr,
949
+ });
950
+ });
951
+ });
952
+ }
953
+
954
+ export async function runImpactTestPlan(
955
+ workspaceRoot: string,
956
+ plan: ImpactTestPlan,
957
+ options: { bail: boolean; report?: string; timeoutMs?: number },
958
+ ): Promise<TestRunRecord> {
959
+ const commands = [
960
+ ...plan.requiredChecks.map((check) => check.command),
961
+ ...plan.tests.map((test) => test.command),
962
+ ];
963
+ const results: TestRunStep[] = [];
964
+ const started = Date.now();
965
+ const timeoutMs = resolveTestCommandTimeoutMs(options.timeoutMs);
966
+ for (const command of commands) {
967
+ const result = await runCommand(workspaceRoot, command, timeoutMs);
968
+ results.push(result);
969
+ if (!result.ok && options.bail) {
970
+ break;
971
+ }
972
+ }
973
+ const record: TestRunRecord = {
974
+ schemaVersion: "0.1.0",
975
+ id: `run_${hashStable(`${Date.now()}:${commands.join("|")}`).slice(0, 12)}`,
976
+ changedHash: `sha256:${hashStable(canonicalJson(plan.changedFiles))}`,
977
+ planHash: `sha256:${hashStable(canonicalJson(plan))}`,
978
+ source: plan.source,
979
+ commands,
980
+ timeoutMs,
981
+ results,
982
+ failed: results.filter((result) => !result.ok).map((result) => result.command),
983
+ durationMs: Date.now() - started,
984
+ };
985
+ const reportPath = options.report ?? join(TEST_RUN_DIR, "last.json");
986
+ const absolute = join(workspaceRoot, reportPath);
987
+ nodeFileSystem.mkdirp(dirname(absolute));
988
+ nodeFileSystem.writeText(absolute, serializeCanonical(record));
989
+ if (!options.report) {
990
+ const archive = join(workspaceRoot, TEST_RUN_DIR, `${record.id}.json`);
991
+ nodeFileSystem.writeText(archive, serializeCanonical(record));
992
+ }
993
+ return record;
994
+ }
995
+
996
+ export function explainTest(workspaceRoot: string, testFile: string): ImpactResult {
997
+ const graph = loadArtifacts(workspaceRoot).testGraph;
998
+ const normalized = normalize(testFile);
999
+ const test = graph.tests.find((entry) => entry.file === normalized);
1000
+ if (!test) {
1001
+ return {
1002
+ ok: false,
1003
+ diagnostics: [diag("error", "FORGE_TEST_NOT_FOUND", `test not found in TestGraph: ${testFile}`, testFile)],
1004
+ exitCode: 1,
1005
+ };
1006
+ }
1007
+ return { ok: true, test, diagnostics: [], exitCode: 0 };
1008
+ }
1009
+
1010
+ export function diagnosticsForImpactTestRun(run: TestRunRecord): Diagnostic[] {
1011
+ const timedOut = run.results.filter((result) => result.timedOut).map((result) => result.command);
1012
+ if (timedOut.length > 0) {
1013
+ return [diag("error", "FORGE_TEST_RUN_TIMEOUT", `impact-selected command timed out: ${timedOut.join(", ")}`)];
1014
+ }
1015
+
1016
+ const resolutionFailures = run.results.filter((result) => result.failureKind === "command-resolution-error");
1017
+ if (resolutionFailures.length > 0) {
1018
+ return [diag(
1019
+ "error",
1020
+ "FORGE_TEST_COMMAND_RESOLUTION_FAILED",
1021
+ `impact-selected command could not be resolved: ${resolutionFailures.map((result) => result.command).join(", ")}`,
1022
+ )];
1023
+ }
1024
+
1025
+ const generatedDrift = run.results.filter((result) => result.failureKind === "generated-drift");
1026
+ if (generatedDrift.length > 0) {
1027
+ return [diag(
1028
+ "error",
1029
+ "FORGE_IMPACT_GENERATED_DRIFT",
1030
+ `generated artifacts are stale: ${generatedDrift.map((result) => result.command).join(", ")}`,
1031
+ )];
1032
+ }
1033
+
1034
+ if (run.failed.length > 0) {
1035
+ return [diag("error", "FORGE_TEST_RUN_FAILED", "one or more impact-selected tests failed")];
1036
+ }
1037
+
1038
+ return [];
1039
+ }
1040
+
1041
+ export async function runTestCommand(options: TestCommandOptions): Promise<ImpactResult> {
1042
+ if (options.subcommand === "explain") {
1043
+ return explainTest(options.workspaceRoot, options.testFile ?? "");
1044
+ }
1045
+ let plan: ImpactTestPlan;
1046
+ if (options.subcommand === "run" && options.planPath) {
1047
+ const raw = nodeFileSystem.readText(join(options.workspaceRoot, options.planPath));
1048
+ plan = JSON.parse(raw ?? "{}") as ImpactTestPlan;
1049
+ } else {
1050
+ plan = buildImpactTestPlan(options);
1051
+ }
1052
+ if (options.write) {
1053
+ writeTestPlan(options.workspaceRoot, plan);
1054
+ }
1055
+ if (options.subcommand === "plan") {
1056
+ return { ok: true, plan, diagnostics: [], exitCode: 0 };
1057
+ }
1058
+ if (plan.requiredChecks.length === 0 && plan.tests.length === 0) {
1059
+ const diagnostic = diag("warning", "FORGE_TEST_PLAN_EMPTY", "test plan has no commands to run");
1060
+ return { ok: true, plan, diagnostics: [diagnostic], exitCode: 0 };
1061
+ }
1062
+ const run = await runImpactTestPlan(options.workspaceRoot, plan, {
1063
+ bail: options.bail,
1064
+ report: options.report,
1065
+ timeoutMs: options.timeoutMs,
1066
+ });
1067
+ return {
1068
+ ok: run.failed.length === 0,
1069
+ plan,
1070
+ run,
1071
+ diagnostics: diagnosticsForImpactTestRun(run),
1072
+ exitCode: run.failed.length > 0 ? 1 : 0,
1073
+ };
1074
+ }
1075
+
1076
+ export function runImpactCommand(options: ImpactCommandOptions): ImpactResult {
1077
+ const report = analyzeImpact(options);
1078
+ if (options.write) {
1079
+ const plan = buildImpactTestPlan({
1080
+ subcommand: "plan",
1081
+ workspaceRoot: options.workspaceRoot,
1082
+ json: options.json,
1083
+ write: true,
1084
+ changed: options.changed,
1085
+ staged: options.staged,
1086
+ since: options.since,
1087
+ featureId: options.featureId,
1088
+ refactorId: options.refactorId,
1089
+ upgradeId: options.upgradeId,
1090
+ maxCost: "standard",
1091
+ includeDocker: false,
1092
+ includeBrowser: false,
1093
+ bail: false,
1094
+ });
1095
+ writeTestPlan(options.workspaceRoot, plan);
1096
+ }
1097
+ return { ok: report.ok, report, diagnostics: report.diagnostics, exitCode: report.exitCode };
1098
+ }
1099
+
1100
+ export function formatImpactJson(result: ImpactResult): string {
1101
+ if (result.run) {
1102
+ return `${JSON.stringify({
1103
+ ok: result.ok,
1104
+ plan: result.plan,
1105
+ run: result.run,
1106
+ diagnostics: result.diagnostics,
1107
+ exitCode: result.exitCode,
1108
+ }, null, 2)}\n`;
1109
+ }
1110
+ return `${JSON.stringify(result.report ?? result.plan ?? result.test ?? result.run ?? result, null, 2)}\n`;
1111
+ }
1112
+
1113
+ export function formatImpactHuman(result: ImpactResult): string {
1114
+ if (result.report) {
1115
+ const report = result.report;
1116
+ return `Impact detected
1117
+
1118
+ Risk: ${report.risk.level}
1119
+
1120
+ Changed files:
1121
+ ${report.changedFiles.map((file) => ` - ${file}`).join("\n") || " - none"}
1122
+
1123
+ Runtime:
1124
+ ${[
1125
+ ...report.impacted.runtime.commands.map((name) => ` - command ${name}`),
1126
+ ...report.impacted.runtime.queries.map((name) => ` - query ${name}`),
1127
+ ...report.impacted.runtime.liveQueries.map((name) => ` - liveQuery ${name}`),
1128
+ ...report.impacted.runtime.actions.map((name) => ` - action ${name}`),
1129
+ ...report.impacted.runtime.workflows.map((name) => ` - workflow ${name}`),
1130
+ ].join("\n") || " - none"}
1131
+
1132
+ Required checks:
1133
+ ${report.recommendedChecks.map((check) => ` - ${check}`).join("\n") || " - none"}
1134
+
1135
+ Final:
1136
+ - forge verify --strict
1137
+ `;
1138
+ }
1139
+ if (result.plan) {
1140
+ return renderTestPlanMarkdown(result.plan);
1141
+ }
1142
+ if (result.test) {
1143
+ return `Test: ${result.test.file}
1144
+ Cost: ${result.test.cost}
1145
+ Confidence: ${result.test.confidence}
1146
+ Covers: ${JSON.stringify(result.test.covers, null, 2)}
1147
+ `;
1148
+ }
1149
+ if (result.run) {
1150
+ return `Impact test run ${result.run.id}
1151
+
1152
+ ${result.run.results.map((step) => {
1153
+ const timeout = step.timedOut ? `, timed out after ${result.run?.timeoutMs ?? "unknown"}ms` : "";
1154
+ const resolution = step.failureKind === "command-resolution-error" ? `, command resolution failed: ${step.stderr ?? "unknown error"}` : "";
1155
+ return `${step.ok ? "OK" : "FAIL"} ${step.command} (${step.durationMs}ms${timeout}${resolution})`;
1156
+ }).join("\n")}
1157
+ `;
1158
+ }
1159
+ return result.diagnostics.map((diagnostic) => diagnostic.message).join("\n");
1160
+ }