forgeos 0.1.0-alpha.2 → 0.1.0-alpha.21

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 (406) hide show
  1. package/.npmignore +4 -0
  2. package/AGENTS.md +168 -81
  3. package/CHANGELOG.md +211 -0
  4. package/README.md +88 -14
  5. package/adapters/go/README.md +23 -0
  6. package/adapters/go/go.mod +3 -0
  7. package/adapters/go/http.go +149 -0
  8. package/adapters/go/registry.go +234 -0
  9. package/adapters/go/types.go +136 -0
  10. package/adapters/java/README.md +68 -0
  11. package/adapters/java/pom.xml +34 -0
  12. package/adapters/java/src/main/java/dev/forgeos/adapter/Auth.java +20 -0
  13. package/adapters/java/src/main/java/dev/forgeos/adapter/Diagnostic.java +16 -0
  14. package/adapters/java/src/main/java/dev/forgeos/adapter/Entry.java +38 -0
  15. package/adapters/java/src/main/java/dev/forgeos/adapter/EntryKind.java +16 -0
  16. package/adapters/java/src/main/java/dev/forgeos/adapter/ErrorInfo.java +4 -0
  17. package/adapters/java/src/main/java/dev/forgeos/adapter/Forge.java +94 -0
  18. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeCall.java +12 -0
  19. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeContext.java +11 -0
  20. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHandler.java +8 -0
  21. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHttpHandler.java +179 -0
  22. package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeRegistry.java +121 -0
  23. package/adapters/java/src/main/java/dev/forgeos/adapter/Json.java +14 -0
  24. package/adapters/java/src/main/java/dev/forgeos/adapter/Manifest.java +14 -0
  25. package/adapters/java/src/main/java/dev/forgeos/adapter/RequestEnvelope.java +6 -0
  26. package/adapters/java/src/main/java/dev/forgeos/adapter/ResponseEnvelope.java +25 -0
  27. package/adapters/java/src/main/java/dev/forgeos/adapter/Risk.java +18 -0
  28. package/adapters/java/src/main/java/dev/forgeos/adapter/Schemas.java +36 -0
  29. package/adapters/java/src/main/java/dev/forgeos/adapter/Service.java +65 -0
  30. package/adapters/java/src/main/java/dev/forgeos/adapter/TransactionMode.java +18 -0
  31. package/adapters/java/src/main/java/dev/forgeos/adapter/TypedForgeHandler.java +6 -0
  32. package/adapters/java/target/classes/dev/forgeos/adapter/Auth.class +0 -0
  33. package/adapters/java/target/classes/dev/forgeos/adapter/Diagnostic.class +0 -0
  34. package/adapters/java/target/classes/dev/forgeos/adapter/Entry.class +0 -0
  35. package/adapters/java/target/classes/dev/forgeos/adapter/EntryKind.class +0 -0
  36. package/adapters/java/target/classes/dev/forgeos/adapter/ErrorInfo.class +0 -0
  37. package/adapters/java/target/classes/dev/forgeos/adapter/Forge.class +0 -0
  38. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeCall.class +0 -0
  39. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeContext.class +0 -0
  40. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeHandler.class +0 -0
  41. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeHttpHandler.class +0 -0
  42. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$EntryOption.class +0 -0
  43. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$RegisteredEntry.class +0 -0
  44. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$RegistryOption.class +0 -0
  45. package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry.class +0 -0
  46. package/adapters/java/target/classes/dev/forgeos/adapter/Json.class +0 -0
  47. package/adapters/java/target/classes/dev/forgeos/adapter/Manifest.class +0 -0
  48. package/adapters/java/target/classes/dev/forgeos/adapter/RequestEnvelope.class +0 -0
  49. package/adapters/java/target/classes/dev/forgeos/adapter/ResponseEnvelope.class +0 -0
  50. package/adapters/java/target/classes/dev/forgeos/adapter/Risk.class +0 -0
  51. package/adapters/java/target/classes/dev/forgeos/adapter/Schemas.class +0 -0
  52. package/adapters/java/target/classes/dev/forgeos/adapter/Service.class +0 -0
  53. package/adapters/java/target/classes/dev/forgeos/adapter/TransactionMode.class +0 -0
  54. package/adapters/java/target/classes/dev/forgeos/adapter/TypedForgeHandler.class +0 -0
  55. package/adapters/java/target/forge-java-adapter-0.1.0-alpha.11.jar +0 -0
  56. package/adapters/java/target/maven-archiver/pom.properties +3 -0
  57. package/adapters/java/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +23 -0
  58. package/adapters/java/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +20 -0
  59. package/adapters/java-spring-boot-starter/README.md +32 -0
  60. package/adapters/java-spring-boot-starter/pom.xml +36 -0
  61. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeCommand.java +22 -0
  62. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeExternalService.java +15 -0
  63. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeQuery.java +16 -0
  64. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.java +18 -0
  65. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.java +16 -0
  66. package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringRuntime.java +104 -0
  67. package/adapters/java-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
  68. package/adapters/java-spring-boot-starter/target/classes/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
  69. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeCommand.class +0 -0
  70. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeExternalService.class +0 -0
  71. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeQuery.class +0 -0
  72. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.class +0 -0
  73. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.class +0 -0
  74. package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeSpringRuntime.class +0 -0
  75. package/adapters/java-spring-boot-starter/target/forge-java-spring-boot-starter-0.1.0-alpha.11.jar +0 -0
  76. package/adapters/java-spring-boot-starter/target/maven-archiver/pom.properties +3 -0
  77. package/adapters/java-spring-boot-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +6 -0
  78. package/adapters/java-spring-boot-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +6 -0
  79. package/bin/forge.mjs +18 -0
  80. package/docs/changelog.md +242 -0
  81. package/docs/forge-protocol.md +189 -0
  82. package/examples/go-billing/go.mod +7 -0
  83. package/examples/go-billing/main.go +120 -0
  84. package/examples/java-billing/pom.xml +52 -0
  85. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/CreateInvoiceInput.java +4 -0
  86. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Invoice.java +11 -0
  87. package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Main.java +127 -0
  88. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/CreateInvoiceInput.class +0 -0
  89. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Invoice.class +0 -0
  90. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main$EmptyInput.class +0 -0
  91. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main$Options.class +0 -0
  92. package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main.class +0 -0
  93. package/examples/java-billing/target/java-billing-0.1.0-alpha.11-all.jar +0 -0
  94. package/examples/java-billing/target/java-billing-0.1.0-alpha.11.jar +0 -0
  95. package/examples/java-billing/target/maven-archiver/pom.properties +3 -0
  96. package/examples/java-billing/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +5 -0
  97. package/examples/java-billing/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +3 -0
  98. package/package.json +29 -7
  99. package/schemas/forge-manifest.schema.json +57 -0
  100. package/src/forge/_generated/releaseManifest.json +1 -2
  101. package/src/forge/_generated/releaseManifest.ts +3 -3
  102. package/src/forge/agent-adapters/index.ts +1511 -123
  103. package/src/forge/agent-adapters/types.ts +216 -1
  104. package/src/forge/agent-memory/bridge.ts +1245 -0
  105. package/src/forge/agent-memory/context-pack.ts +151 -0
  106. package/src/forge/agent-memory/hook-runner.ts +312 -0
  107. package/src/forge/agent-memory/mcp.ts +224 -0
  108. package/src/forge/agent-memory/normalize.ts +498 -0
  109. package/src/forge/agent-memory/redaction.ts +103 -0
  110. package/src/forge/agent-memory/sources/claude-code.ts +51 -0
  111. package/src/forge/agent-memory/sources/codex-hook-runner.mjs +273 -0
  112. package/src/forge/agent-memory/sources/codex.ts +119 -0
  113. package/src/forge/agent-memory/sources/cursor.ts +35 -0
  114. package/src/forge/agent-memory/types.ts +191 -0
  115. package/src/forge/bench.ts +248 -0
  116. package/src/forge/brownfield-import/index.ts +801 -0
  117. package/src/forge/brownfield-import/types.ts +127 -0
  118. package/src/forge/cair/action-journal.ts +61 -0
  119. package/src/forge/cair/action-parser.ts +314 -0
  120. package/src/forge/cair/action-validator.ts +40 -0
  121. package/src/forge/cair/actions.ts +1818 -0
  122. package/src/forge/cair/format.ts +77 -0
  123. package/src/forge/cair/index.ts +106 -0
  124. package/src/forge/cair/query.ts +478 -0
  125. package/src/forge/cair/snapshot.ts +315 -0
  126. package/src/forge/cair/types.ts +248 -0
  127. package/src/forge/cli/ai.ts +671 -3
  128. package/src/forge/cli/auth.ts +36 -1
  129. package/src/forge/cli/build.ts +20 -4
  130. package/src/forge/cli/changed.ts +300 -0
  131. package/src/forge/cli/codex-app-server.ts +877 -0
  132. package/src/forge/cli/commands.ts +1285 -7
  133. package/src/forge/cli/db.ts +121 -2
  134. package/src/forge/cli/deps.ts +79 -12
  135. package/src/forge/cli/dev.ts +502 -38
  136. package/src/forge/cli/docs.ts +265 -0
  137. package/src/forge/cli/handoff.ts +250 -0
  138. package/src/forge/cli/index.ts +1 -0
  139. package/src/forge/cli/main.ts +49 -3
  140. package/src/forge/cli/new.ts +3 -1
  141. package/src/forge/cli/next-actions.ts +23 -0
  142. package/src/forge/cli/output.ts +290 -1
  143. package/src/forge/cli/parse.ts +770 -36
  144. package/src/forge/cli/query.ts +32 -0
  145. package/src/forge/cli/release.ts +35 -11
  146. package/src/forge/cli/rls.ts +568 -17
  147. package/src/forge/cli/run.ts +41 -0
  148. package/src/forge/cli/secrets.ts +46 -1
  149. package/src/forge/cli/security.ts +381 -0
  150. package/src/forge/cli/self-host.ts +56 -14
  151. package/src/forge/cli/studio.ts +2163 -0
  152. package/src/forge/cli/verify.ts +1422 -32
  153. package/src/forge/compiler/agent-contract/build.ts +725 -41
  154. package/src/forge/compiler/agent-contract/types.ts +85 -0
  155. package/src/forge/compiler/ai-registry/build.ts +62 -1
  156. package/src/forge/compiler/ai-registry/constants.ts +1 -1
  157. package/src/forge/compiler/ai-registry/parse.ts +168 -5
  158. package/src/forge/compiler/api-surface/build.ts +47 -0
  159. package/src/forge/compiler/app-graph/build.ts +68 -8
  160. package/src/forge/compiler/app-graph/extract.ts +107 -0
  161. package/src/forge/compiler/app-graph/forge-apis.ts +1 -0
  162. package/src/forge/compiler/app-graph/module-graph.ts +73 -78
  163. package/src/forge/compiler/app-graph/parser.ts +24 -24
  164. package/src/forge/compiler/app-graph/profile.ts +26 -0
  165. package/src/forge/compiler/app-graph/versions.ts +1 -1
  166. package/src/forge/compiler/classifier/capabilities.ts +3 -2
  167. package/src/forge/compiler/classifier/classify.ts +32 -8
  168. package/src/forge/compiler/classifier/secrets.ts +3 -2
  169. package/src/forge/compiler/classifier/signals.ts +91 -1
  170. package/src/forge/compiler/client-sdk/build-manifest.ts +59 -0
  171. package/src/forge/compiler/client-sdk/render-client.ts +188 -13
  172. package/src/forge/compiler/data-graph/parse.ts +3 -3
  173. package/src/forge/compiler/data-graph/sql/ddl.ts +60 -2
  174. package/src/forge/compiler/data-graph/sql/serialize.ts +4 -0
  175. package/src/forge/compiler/data-graph/sql/types.ts +1 -0
  176. package/src/forge/compiler/dev-manifest/build.ts +3 -0
  177. package/src/forge/compiler/diagnostics/codes.ts +35 -0
  178. package/src/forge/compiler/diagnostics/create.ts +8 -3
  179. package/src/forge/compiler/diagnostics/index.ts +2 -0
  180. package/src/forge/compiler/emitter/barrel.ts +3 -0
  181. package/src/forge/compiler/emitter/render.ts +5 -0
  182. package/src/forge/compiler/external-manifest/registry.ts +205 -0
  183. package/src/forge/compiler/external-manifest/types.ts +91 -0
  184. package/src/forge/compiler/external-manifest/validate.ts +373 -0
  185. package/src/forge/compiler/frontend-graph/build.ts +85 -13
  186. package/src/forge/compiler/integration/add.ts +498 -22
  187. package/src/forge/compiler/integration/snapshot.ts +2 -0
  188. package/src/forge/compiler/make-registry/build.ts +19 -7
  189. package/src/forge/compiler/orchestrator/plan-profile.ts +23 -0
  190. package/src/forge/compiler/orchestrator/plan.ts +78 -7
  191. package/src/forge/compiler/orchestrator/profile.ts +65 -0
  192. package/src/forge/compiler/orchestrator/run.ts +97 -31
  193. package/src/forge/compiler/orchestrator/serialize.ts +101 -8
  194. package/src/forge/compiler/package-graph/compiler.ts +13 -3
  195. package/src/forge/compiler/package-manager/adapter.ts +4 -1
  196. package/src/forge/compiler/package-manager/commands.ts +4 -0
  197. package/src/forge/compiler/package-manager/executor.ts +30 -1
  198. package/src/forge/compiler/policy-registry/build.ts +44 -1
  199. package/src/forge/compiler/test-graph/build.ts +11 -3
  200. package/src/forge/compiler/types/ai-registry.ts +25 -1
  201. package/src/forge/compiler/types/app-graph.ts +9 -2
  202. package/src/forge/compiler/types/cli.ts +76 -1
  203. package/src/forge/compiler/types/dev-manifest.ts +3 -0
  204. package/src/forge/compiler/types/frontend-graph.ts +2 -2
  205. package/src/forge/delta/classifier.ts +52 -0
  206. package/src/forge/delta/explain.ts +126 -0
  207. package/src/forge/delta/git-observer.ts +43 -0
  208. package/src/forge/delta/ids.ts +44 -0
  209. package/src/forge/delta/index.ts +13 -0
  210. package/src/forge/delta/recorder.ts +402 -0
  211. package/src/forge/delta/redaction.ts +50 -0
  212. package/src/forge/delta/schema.ts +240 -0
  213. package/src/forge/delta/session.ts +142 -0
  214. package/src/forge/delta/status.ts +489 -0
  215. package/src/forge/delta/store.ts +2975 -0
  216. package/src/forge/delta/timeline.ts +104 -0
  217. package/src/forge/dev/server.ts +768 -15
  218. package/src/forge/dev/types.ts +15 -1
  219. package/src/forge/dev/watch.ts +17 -7
  220. package/src/forge/dev-console/cycle.ts +233 -21
  221. package/src/forge/dev-console/types.ts +46 -1
  222. package/src/forge/impact/index.ts +46 -8
  223. package/src/forge/impact/types.ts +6 -0
  224. package/src/forge/intent/index.ts +35 -16
  225. package/src/forge/make/index.ts +149 -6
  226. package/src/forge/make/templates.ts +343 -2
  227. package/src/forge/make/types.ts +3 -1
  228. package/src/forge/refactor/index.ts +1 -0
  229. package/src/forge/repair/rules/index.ts +2 -2
  230. package/src/forge/review/index.ts +158 -12
  231. package/src/forge/review/types.ts +15 -0
  232. package/src/forge/runtime/ai/context.ts +210 -5
  233. package/src/forge/runtime/ai/types.ts +70 -0
  234. package/src/forge/runtime/auth/claims.ts +32 -0
  235. package/src/forge/runtime/auth/errors.ts +2 -0
  236. package/src/forge/runtime/context/create-context.ts +30 -6
  237. package/src/forge/runtime/db/generated-client.ts +13 -2
  238. package/src/forge/runtime/db/memory-adapter.ts +2 -2
  239. package/src/forge/runtime/db/pglite-adapter.ts +77 -2
  240. package/src/forge/runtime/db/postgres-adapter.ts +6 -3
  241. package/src/forge/runtime/executor.ts +112 -2
  242. package/src/forge/runtime/external/bridge.ts +649 -0
  243. package/src/forge/runtime/runner/run-entry.ts +16 -7
  244. package/src/forge/runtime/telemetry/scrubber.ts +91 -10
  245. package/src/forge/runtime/webhooks/security.ts +184 -0
  246. package/src/forge/server.ts +100 -2
  247. package/src/forge/version.ts +1 -1
  248. package/src/forge/vue/index.ts +407 -0
  249. package/src/forge/workspace/change-summary.ts +209 -0
  250. package/src/forge/workspace/forge-cli.ts +14 -0
  251. package/src/forge/workspace/git-summary.ts +279 -0
  252. package/templates/agent-workroom/AGENTS.md +29 -0
  253. package/templates/agent-workroom/README.md +34 -0
  254. package/templates/agent-workroom/forge.config.ts +3 -0
  255. package/templates/agent-workroom/package.json +33 -0
  256. package/templates/agent-workroom/src/actions/indexAgentSignal.ts +10 -0
  257. package/templates/agent-workroom/src/commands/openWorkroom.ts +61 -0
  258. package/templates/agent-workroom/src/commands/recordAgentSignal.ts +119 -0
  259. package/templates/agent-workroom/src/commands/recordCheckRun.ts +52 -0
  260. package/templates/agent-workroom/src/forge/schema.ts +54 -0
  261. package/templates/agent-workroom/src/policies.ts +6 -0
  262. package/templates/agent-workroom/src/queries/listWorkrooms.ts +11 -0
  263. package/templates/agent-workroom/src/queries/liveWorkroom.ts +63 -0
  264. package/templates/agent-workroom/tsconfig.json +16 -0
  265. package/templates/agent-workroom/web/index.html +12 -0
  266. package/templates/agent-workroom/web/package.json +21 -0
  267. package/templates/agent-workroom/web/src/App.tsx +345 -0
  268. package/templates/agent-workroom/web/src/lib/forge.ts +13 -0
  269. package/templates/agent-workroom/web/src/main.tsx +13 -0
  270. package/templates/agent-workroom/web/src/styles.css +545 -0
  271. package/templates/agent-workroom/web/tsconfig.json +27 -0
  272. package/templates/b2b-support-web/package.json +2 -0
  273. package/templates/b2b-support-web/tsconfig.json +4 -1
  274. package/templates/b2b-support-web/web/package.json +1 -1
  275. package/templates/minimal-web/package.json +2 -1
  276. package/templates/minimal-web/tsconfig.json +3 -1
  277. package/templates/minimal-web/web/package.json +2 -2
  278. package/src/forge/_generated/actionSubscriptions.json +0 -2
  279. package/src/forge/_generated/actionSubscriptions.ts +0 -10
  280. package/src/forge/_generated/agentAdapterManifest.json +0 -2
  281. package/src/forge/_generated/agentAdapterManifest.ts +0 -73
  282. package/src/forge/_generated/agentContract.json +0 -2
  283. package/src/forge/_generated/agentContract.ts +0 -7696
  284. package/src/forge/_generated/agentQuickstart.md +0 -32
  285. package/src/forge/_generated/aiContext.ts +0 -59
  286. package/src/forge/_generated/aiModels.json +0 -2
  287. package/src/forge/_generated/aiModels.ts +0 -35
  288. package/src/forge/_generated/aiProviders.json +0 -2
  289. package/src/forge/_generated/aiProviders.ts +0 -23
  290. package/src/forge/_generated/aiRegistry.json +0 -2
  291. package/src/forge/_generated/aiRegistry.ts +0 -29
  292. package/src/forge/_generated/api.json +0 -2
  293. package/src/forge/_generated/api.ts +0 -8
  294. package/src/forge/_generated/appGraph.json +0 -2
  295. package/src/forge/_generated/appGraph.ts +0 -14667
  296. package/src/forge/_generated/appMap.md +0 -35
  297. package/src/forge/_generated/artifactManifest.json +0 -2
  298. package/src/forge/_generated/artifactManifest.ts +0 -7
  299. package/src/forge/_generated/authClaims.json +0 -2
  300. package/src/forge/_generated/authClaims.ts +0 -13
  301. package/src/forge/_generated/authConfig.json +0 -2
  302. package/src/forge/_generated/authConfig.ts +0 -17
  303. package/src/forge/_generated/authContext.ts +0 -23
  304. package/src/forge/_generated/authRegistry.json +0 -2
  305. package/src/forge/_generated/authRegistry.ts +0 -25
  306. package/src/forge/_generated/buildInfo.json +0 -2
  307. package/src/forge/_generated/buildInfo.ts +0 -9
  308. package/src/forge/_generated/capabilityMap.json +0 -2
  309. package/src/forge/_generated/capabilityMap.md +0 -15
  310. package/src/forge/_generated/capabilityMap.ts +0 -17
  311. package/src/forge/_generated/client.ts +0 -282
  312. package/src/forge/_generated/clientApi.ts +0 -9
  313. package/src/forge/_generated/clientManifest.json +0 -2
  314. package/src/forge/_generated/clientManifest.ts +0 -39
  315. package/src/forge/_generated/clientTypes.ts +0 -78
  316. package/src/forge/_generated/configRegistry.json +0 -2
  317. package/src/forge/_generated/configRegistry.ts +0 -4
  318. package/src/forge/_generated/dataGraph.json +0 -2
  319. package/src/forge/_generated/dataGraph.ts +0 -8
  320. package/src/forge/_generated/db.json +0 -2
  321. package/src/forge/_generated/db.ts +0 -2
  322. package/src/forge/_generated/dbSecurityManifest.json +0 -2
  323. package/src/forge/_generated/dbSecurityManifest.ts +0 -15
  324. package/src/forge/_generated/dbSessionContext.json +0 -2
  325. package/src/forge/_generated/dbSessionContext.ts +0 -39
  326. package/src/forge/_generated/deployManifest.json +0 -2
  327. package/src/forge/_generated/deployManifest.ts +0 -14
  328. package/src/forge/_generated/devManifest.json +0 -2
  329. package/src/forge/_generated/devManifest.ts +0 -47
  330. package/src/forge/_generated/envSchema.json +0 -2
  331. package/src/forge/_generated/envSchema.ts +0 -59
  332. package/src/forge/_generated/frontendGraph.json +0 -2
  333. package/src/forge/_generated/frontendGraph.ts +0 -27
  334. package/src/forge/_generated/importGuards.json +0 -2
  335. package/src/forge/_generated/importGuards.ts +0 -686
  336. package/src/forge/_generated/index.ts +0 -67
  337. package/src/forge/_generated/liveProductionManifest.json +0 -2
  338. package/src/forge/_generated/liveProductionManifest.ts +0 -23
  339. package/src/forge/_generated/liveProtocol.json +0 -2
  340. package/src/forge/_generated/liveProtocol.ts +0 -21
  341. package/src/forge/_generated/liveQueryRegistry.json +0 -2
  342. package/src/forge/_generated/liveQueryRegistry.ts +0 -9
  343. package/src/forge/_generated/liveTransportConfig.json +0 -2
  344. package/src/forge/_generated/liveTransportConfig.ts +0 -19
  345. package/src/forge/_generated/makeRegistry.json +0 -2
  346. package/src/forge/_generated/makeRegistry.ts +0 -163
  347. package/src/forge/_generated/makeTemplates.json +0 -2
  348. package/src/forge/_generated/makeTemplates.ts +0 -61
  349. package/src/forge/_generated/mockMap.json +0 -2
  350. package/src/forge/_generated/mockMap.ts +0 -7
  351. package/src/forge/_generated/operationPlaybooks.md +0 -147
  352. package/src/forge/_generated/packageGraph.json +0 -2
  353. package/src/forge/_generated/packageGraph.ts +0 -245249
  354. package/src/forge/_generated/packageUpgradeRegistry.json +0 -2
  355. package/src/forge/_generated/packageUpgradeRegistry.ts +0 -15
  356. package/src/forge/_generated/permissionMatrix.json +0 -2
  357. package/src/forge/_generated/permissionMatrix.ts +0 -7
  358. package/src/forge/_generated/policyRegistry.json +0 -2
  359. package/src/forge/_generated/policyRegistry.ts +0 -11
  360. package/src/forge/_generated/queryRegistry.json +0 -2
  361. package/src/forge/_generated/queryRegistry.ts +0 -9
  362. package/src/forge/_generated/react.d.ts +0 -22
  363. package/src/forge/_generated/react.ts +0 -29
  364. package/src/forge/_generated/reactManifest.json +0 -2
  365. package/src/forge/_generated/reactManifest.ts +0 -19
  366. package/src/forge/_generated/rlsPolicies.json +0 -2
  367. package/src/forge/_generated/rlsPolicies.sql +0 -34
  368. package/src/forge/_generated/rlsPolicies.ts +0 -6
  369. package/src/forge/_generated/runtimeGraph.json +0 -2
  370. package/src/forge/_generated/runtimeGraph.ts +0 -8
  371. package/src/forge/_generated/runtimeMatrix.json +0 -2
  372. package/src/forge/_generated/runtimeMatrix.ts +0 -327385
  373. package/src/forge/_generated/runtimeRegistry.ts +0 -2
  374. package/src/forge/_generated/runtimeRules.md +0 -79
  375. package/src/forge/_generated/secretRegistry.json +0 -2
  376. package/src/forge/_generated/secretRegistry.ts +0 -50
  377. package/src/forge/_generated/secretsContext.ts +0 -11
  378. package/src/forge/_generated/serverApi.ts +0 -10
  379. package/src/forge/_generated/sourceMapManifest.json +0 -2
  380. package/src/forge/_generated/sourceMapManifest.ts +0 -7
  381. package/src/forge/_generated/sqlPlan.json +0 -2
  382. package/src/forge/_generated/sqlPlan.ts +0 -88
  383. package/src/forge/_generated/subscriptionManifest.json +0 -2
  384. package/src/forge/_generated/subscriptionManifest.ts +0 -7
  385. package/src/forge/_generated/symbolicationManifest.json +0 -2
  386. package/src/forge/_generated/symbolicationManifest.ts +0 -17
  387. package/src/forge/_generated/telemetryRegistry.json +0 -2
  388. package/src/forge/_generated/telemetryRegistry.ts +0 -9
  389. package/src/forge/_generated/telemetrySinks.json +0 -2
  390. package/src/forge/_generated/telemetrySinks.ts +0 -11
  391. package/src/forge/_generated/tenantScope.json +0 -2
  392. package/src/forge/_generated/tenantScope.ts +0 -8
  393. package/src/forge/_generated/testGraph.json +0 -2
  394. package/src/forge/_generated/testGraph.ts +0 -3108
  395. package/src/forge/_generated/testPlanRegistry.json +0 -2
  396. package/src/forge/_generated/testPlanRegistry.ts +0 -33
  397. package/src/forge/_generated/uiRoutes.json +0 -2
  398. package/src/forge/_generated/uiRoutes.ts +0 -16
  399. package/src/forge/_generated/uiScenarios.json +0 -2
  400. package/src/forge/_generated/uiScenarios.ts +0 -30
  401. package/src/forge/_generated/uiTestManifest.json +0 -2
  402. package/src/forge/_generated/uiTestManifest.ts +0 -27
  403. package/src/forge/_generated/workflowRegistry.json +0 -2
  404. package/src/forge/_generated/workflowRegistry.ts +0 -9
  405. package/src/forge/_generated/workflowSubscriptions.json +0 -2
  406. package/src/forge/_generated/workflowSubscriptions.ts +0 -10
@@ -1,21 +1,38 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
1
2
  import { nodeFileSystem } from "../compiler/fs/index.ts";
2
- import { join } from "node:path";
3
+ import { dirname, join } from "node:path";
3
4
  import { spawn } from "node:child_process";
5
+ import { availableParallelism, tmpdir } from "node:os";
6
+ import { createHash } from "node:crypto";
7
+ import { stripDeterministicHeader } from "../compiler/primitives/header.ts";
8
+ import { canonicalJson, serializeCanonical } from "../compiler/primitives/serialize.ts";
4
9
  import { createDiagnostic } from "../compiler/diagnostics/create.ts";
5
10
  import type { Diagnostic } from "../compiler/types/diagnostic.ts";
6
- import type { VerifyOptions, VerifyProfile, VerifyResult, VerifyStep } from "../compiler/types/cli.ts";
11
+ import type { TestCost, TestGraph } from "../compiler/types/test-graph.ts";
12
+ import type {
13
+ VerifyOptions,
14
+ VerifyProfile,
15
+ VerifyResult,
16
+ VerifyStep,
17
+ VerifyTestGraphDurationSource,
18
+ VerifyTestGraphLane,
19
+ VerifyTestGraphPlan,
20
+ VerifyTestGraphPlanChunk,
21
+ } from "../compiler/types/cli.ts";
7
22
  import {
23
+ FORGE_VERIFY_NO_TESTS_SELECTED,
8
24
  FORGE_VERIFY_POLICY,
9
25
  FORGE_VERIFY_SCRIPT_TIMEOUT,
10
26
  } from "../compiler/diagnostics/codes.ts";
11
27
  import { detectPackageManager } from "../compiler/package-manager/detect.ts";
12
- import { resolvePackageManagerArgv } from "../compiler/package-manager/executor.ts";
28
+ import { resolveCommandArgv, resolvePackageManagerArgv } from "../compiler/package-manager/executor.ts";
13
29
  import { runCheckCommand, runGenerateCommand } from "./commands.ts";
14
30
  import { lintForgeGuards } from "./lint-forge.ts";
15
31
  import { runPolicyCommand } from "./policy.ts";
16
32
  import { runAuthCommand } from "./auth.ts";
17
33
  import { runRlsCommand } from "./rls.ts";
18
34
  import { buildImpactTestPlan, diagnosticsForImpactTestRun, runImpactTestPlan } from "../impact/index.ts";
35
+ import type { TestRunRecord, TestRunStep } from "../impact/types.ts";
19
36
  import { runAgentCheck } from "../agent-adapters/index.ts";
20
37
  import type { AgentAdapterTarget } from "../agent-adapters/types.ts";
21
38
 
@@ -26,6 +43,28 @@ interface PackageScripts {
26
43
  }
27
44
 
28
45
  const DEFAULT_SCRIPT_TIMEOUT_MS = 30 * 60 * 1000;
46
+ type TypecheckerChoice = "tsc" | "native" | "ts7" | "tsgo" | "auto";
47
+
48
+ interface ScriptRunResult {
49
+ exitCode: number;
50
+ stdout: string;
51
+ stderr: string;
52
+ command: string;
53
+ durationMs: number;
54
+ timedOut: boolean;
55
+ spawnError?: boolean;
56
+ }
57
+
58
+ interface PackageJsonWithBin {
59
+ version?: unknown;
60
+ bin?: unknown;
61
+ }
62
+
63
+ interface TypecheckCandidate {
64
+ label: string;
65
+ argv: string[];
66
+ command: string;
67
+ }
29
68
 
30
69
  function readPackageScripts(workspaceRoot: string): PackageScripts {
31
70
  const packageJsonPath = join(workspaceRoot, "package.json");
@@ -43,33 +82,78 @@ function readPackageScripts(workspaceRoot: string): PackageScripts {
43
82
  }
44
83
  }
45
84
 
85
+ function readWorkspacePackageJson(workspaceRoot: string): Record<string, unknown> {
86
+ const packageJsonPath = join(workspaceRoot, "package.json");
87
+ if (!nodeFileSystem.exists(packageJsonPath)) {
88
+ return {};
89
+ }
90
+ try {
91
+ return JSON.parse(nodeFileSystem.readText(packageJsonPath) ?? "{}") as Record<string, unknown>;
92
+ } catch {
93
+ return {};
94
+ }
95
+ }
96
+
97
+ function isForgeOsFrameworkWorkspace(workspaceRoot: string): boolean {
98
+ const pkg = readWorkspacePackageJson(workspaceRoot);
99
+ return (
100
+ pkg.name === "forgeos" &&
101
+ nodeFileSystem.exists(join(workspaceRoot, "src/forge/cli/verify.ts")) &&
102
+ nodeFileSystem.exists(join(workspaceRoot, "bin/forge.mjs"))
103
+ );
104
+ }
105
+
46
106
  async function spawnPackageRun(
47
107
  workspaceRoot: string,
48
108
  scriptName: string,
49
109
  timeoutMs: number,
50
- ): Promise<{
51
- exitCode: number;
52
- stdout: string;
53
- stderr: string;
54
- command: string;
55
- durationMs: number;
56
- timedOut: boolean;
57
- }> {
110
+ ): Promise<ScriptRunResult> {
58
111
  const packageManager = detectPackageManager(workspaceRoot);
59
112
  let argv = resolvePackageManagerArgv([packageManager, "run", scriptName]);
60
113
  if (process.platform === "win32" && /\.(cmd|bat)$/i.test(argv[0] ?? "")) {
61
114
  argv = [process.env.ComSpec ?? "cmd.exe", "/d", "/c", packageManager, "run", scriptName];
62
115
  }
116
+ return spawnArgv(workspaceRoot, argv, timeoutMs, argv.join(" "));
117
+ }
118
+
119
+ function quoteWindowsCommandArg(value: string): string {
120
+ if (!/[\s"]/u.test(value)) {
121
+ return value;
122
+ }
123
+ return `"${value.replace(/"/g, "\"\"")}"`;
124
+ }
125
+
126
+ function wrapWindowsCommandScript(argv: string[]): string[] {
127
+ if (process.platform !== "win32" || !/\.(cmd|bat)$/iu.test(argv[0] ?? "")) {
128
+ return argv;
129
+ }
130
+ return [
131
+ process.env.ComSpec ?? "cmd.exe",
132
+ "/d",
133
+ "/s",
134
+ "/c",
135
+ argv.map(quoteWindowsCommandArg).join(" "),
136
+ ];
137
+ }
138
+
139
+ async function spawnArgv(
140
+ workspaceRoot: string,
141
+ argv: string[],
142
+ timeoutMs: number,
143
+ command = argv.join(" "),
144
+ envOverrides?: Record<string, string>,
145
+ ): Promise<ScriptRunResult> {
63
146
  const started = Date.now();
64
147
 
65
148
  return new Promise((resolve) => {
66
149
  let settled = false;
67
150
  let timedOut = false;
68
151
  let child: ReturnType<typeof spawn>;
152
+ const spawnCommand = wrapWindowsCommandScript(argv);
69
153
  try {
70
- child = spawn(argv[0]!, argv.slice(1), {
154
+ child = spawn(spawnCommand[0]!, spawnCommand.slice(1), {
71
155
  cwd: workspaceRoot,
72
- env: process.env,
156
+ env: envOverrides ? { ...process.env, ...envOverrides } : process.env,
73
157
  stdio: ["ignore", "pipe", "pipe"],
74
158
  windowsHide: true,
75
159
  });
@@ -78,9 +162,10 @@ async function spawnPackageRun(
78
162
  exitCode: 1,
79
163
  stdout: "",
80
164
  stderr: error instanceof Error ? error.message : String(error),
81
- command: argv.join(" "),
165
+ command,
82
166
  durationMs: Date.now() - started,
83
167
  timedOut: false,
168
+ spawnError: true,
84
169
  });
85
170
  return;
86
171
  }
@@ -110,9 +195,10 @@ async function spawnPackageRun(
110
195
  exitCode: 1,
111
196
  stdout,
112
197
  stderr: error instanceof Error ? error.message : String(error),
113
- command: argv.join(" "),
198
+ command,
114
199
  durationMs: Date.now() - started,
115
200
  timedOut,
201
+ spawnError: true,
116
202
  });
117
203
  }
118
204
  });
@@ -124,7 +210,7 @@ async function spawnPackageRun(
124
210
  exitCode: timedOut ? 1 : code ?? 1,
125
211
  stdout,
126
212
  stderr,
127
- command: argv.join(" "),
213
+ command,
128
214
  durationMs: Date.now() - started,
129
215
  timedOut,
130
216
  });
@@ -137,14 +223,7 @@ async function runPackageScript(
137
223
  workspaceRoot: string,
138
224
  scriptName: string,
139
225
  timeoutMs: number,
140
- ): Promise<{
141
- exitCode: number;
142
- stdout: string;
143
- stderr: string;
144
- command: string;
145
- durationMs: number;
146
- timedOut: boolean;
147
- }> {
226
+ ): Promise<ScriptRunResult> {
148
227
  return spawnPackageRun(workspaceRoot, scriptName, timeoutMs);
149
228
  }
150
229
 
@@ -229,7 +308,1171 @@ function packageScriptFailureDiagnostic(
229
308
  });
230
309
  }
231
310
 
311
+ function strictGraphFailureDiagnostic(result: {
312
+ exitCode: number;
313
+ stdout: string;
314
+ stderr: string;
315
+ command: string;
316
+ failedFiles: string[];
317
+ failedChunk?: number;
318
+ reportPath?: string;
319
+ }): Diagnostic {
320
+ const excerpt = outputExcerpt(result.stdout, result.stderr);
321
+ const files = result.failedFiles.slice(0, 8);
322
+ const hidden = Math.max(0, result.failedFiles.length - files.length);
323
+ const fileSummary = files.length > 0
324
+ ? `${files.join(", ")}${hidden > 0 ? `, ... +${hidden} more` : ""}`
325
+ : "unknown files";
326
+ const report = result.reportPath ?? ".forge/test-runs/last.json";
327
+ return createDiagnostic({
328
+ severity: "error",
329
+ code: "FORGE_VERIFY_TESTS",
330
+ message: `strict TestGraph failed${result.failedChunk ? ` in chunk ${result.failedChunk}` : ""} with exit code ${result.exitCode}: ${fileSummary}`,
331
+ fixHint: excerpt
332
+ ? `Inspect ${report} and rerun the failing files. Last output: ${excerpt}`
333
+ : `Inspect ${report} and rerun the failing files.`,
334
+ suggestedCommands: [
335
+ result.failedFiles.length > 0
336
+ ? `bun test ${result.failedFiles.join(" ")}`
337
+ : result.command,
338
+ "forge repair diagnose --from-last-test-run --json",
339
+ "forge verify --strict",
340
+ ],
341
+ });
342
+ }
343
+
344
+ function resolveTypechecker(options: VerifyOptions): TypecheckerChoice {
345
+ if (options.typechecker) {
346
+ return options.typechecker;
347
+ }
348
+ const fromEnv = process.env.FORGE_TYPECHECKER;
349
+ return (
350
+ fromEnv === "native" ||
351
+ fromEnv === "ts7" ||
352
+ fromEnv === "tsgo" ||
353
+ fromEnv === "auto" ||
354
+ fromEnv === "tsc"
355
+ )
356
+ ? fromEnv
357
+ : "tsc";
358
+ }
359
+
360
+ function nodeModulePackageRoot(workspaceRoot: string, packageName: string): string {
361
+ return join(workspaceRoot, "node_modules", ...packageName.split("/"));
362
+ }
363
+
364
+ function readNodeModulePackageJson(
365
+ workspaceRoot: string,
366
+ packageName: string,
367
+ ): PackageJsonWithBin | undefined {
368
+ const packageJsonPath = join(nodeModulePackageRoot(workspaceRoot, packageName), "package.json");
369
+ if (!nodeFileSystem.exists(packageJsonPath)) {
370
+ return undefined;
371
+ }
372
+
373
+ try {
374
+ return JSON.parse(nodeFileSystem.readText(packageJsonPath) ?? "{}") as PackageJsonWithBin;
375
+ } catch {
376
+ return undefined;
377
+ }
378
+ }
379
+
380
+ function packageVersion(workspaceRoot: string, packageName: string): string | undefined {
381
+ const version = readNodeModulePackageJson(workspaceRoot, packageName)?.version;
382
+ return typeof version === "string" ? version : undefined;
383
+ }
384
+
385
+ function packageMajorVersion(workspaceRoot: string, packageName: string): number | undefined {
386
+ const version = packageVersion(workspaceRoot, packageName);
387
+ const match = version?.match(/^(\d+)/u);
388
+ if (!match) {
389
+ return undefined;
390
+ }
391
+ const parsed = Number(match[1]);
392
+ return Number.isInteger(parsed) ? parsed : undefined;
393
+ }
394
+
395
+ function packageBinPath(workspaceRoot: string, packageName: string, binName: string): string | undefined {
396
+ const packageRoot = nodeModulePackageRoot(workspaceRoot, packageName);
397
+ const packageJson = readNodeModulePackageJson(workspaceRoot, packageName);
398
+ if (!packageJson) {
399
+ return undefined;
400
+ }
401
+
402
+ let relativeBin: string | undefined;
403
+ if (typeof packageJson.bin === "string") {
404
+ relativeBin = packageJson.bin;
405
+ } else if (packageJson.bin && typeof packageJson.bin === "object") {
406
+ const value = (packageJson.bin as Record<string, unknown>)[binName];
407
+ relativeBin = typeof value === "string" ? value : undefined;
408
+ }
409
+
410
+ const candidates = [
411
+ relativeBin ? join(packageRoot, relativeBin) : undefined,
412
+ join(packageRoot, "bin", binName),
413
+ ].filter((candidate): candidate is string => typeof candidate === "string");
414
+
415
+ return candidates.find((candidate) => nodeFileSystem.exists(candidate));
416
+ }
417
+
418
+ function isLikelyPath(value: string): boolean {
419
+ return value.includes("/") || value.includes("\\") || /^[a-z]:/iu.test(value);
420
+ }
421
+
422
+ function isNodeRunnableBin(executable: string): boolean {
423
+ if (/\.(cjs|js|mjs)$/iu.test(executable)) {
424
+ return true;
425
+ }
426
+ if (!nodeFileSystem.exists(executable)) {
427
+ return false;
428
+ }
429
+ try {
430
+ const head = (nodeFileSystem.readText(executable) ?? "").slice(0, 256);
431
+ const firstLine = head.split(/\r?\n/u)[0] ?? "";
432
+ return firstLine.includes("node") || head.includes("require(") || head.includes("import ");
433
+ } catch {
434
+ return false;
435
+ }
436
+ }
437
+
438
+ function argvForExecutable(executable: string, args: string[]): string[] {
439
+ const trimmed = executable.trim();
440
+ if (!isLikelyPath(trimmed)) {
441
+ return resolveCommandArgv([trimmed, ...args]);
442
+ }
443
+ return isNodeRunnableBin(trimmed) ? [process.execPath, trimmed, ...args] : [trimmed, ...args];
444
+ }
445
+
446
+ function argvForPackageBin(binPath: string, args: string[]): string[] {
447
+ return isNodeRunnableBin(binPath) ? [process.execPath, binPath, ...args] : [binPath, ...args];
448
+ }
449
+
450
+ function positiveIntegerEnv(name: string): string | undefined {
451
+ const value = process.env[name]?.trim();
452
+ if (!value) {
453
+ return undefined;
454
+ }
455
+ const parsed = Number(value);
456
+ return Number.isInteger(parsed) && parsed >= 1 ? String(parsed) : undefined;
457
+ }
458
+
459
+ function nativeTypecheckArgs(): string[] {
460
+ const args = ["--noEmit"];
461
+ const checkers = positiveIntegerEnv("FORGE_TS7_CHECKERS");
462
+ const builders = positiveIntegerEnv("FORGE_TS7_BUILDERS");
463
+ const singleThreaded = process.env.FORGE_TS7_SINGLE_THREADED?.trim().toLowerCase();
464
+ if (checkers) {
465
+ args.push("--checkers", checkers);
466
+ }
467
+ if (builders) {
468
+ args.push("--builders", builders);
469
+ }
470
+ if (singleThreaded === "1" || singleThreaded === "true" || singleThreaded === "yes") {
471
+ args.push("--singleThreaded");
472
+ }
473
+ return args;
474
+ }
475
+
476
+ function nativeTypecheckCandidates(workspaceRoot: string): TypecheckCandidate[] {
477
+ const args = nativeTypecheckArgs();
478
+ const candidates: TypecheckCandidate[] = [];
479
+ const explicitTsc = process.env.FORGE_TS7_TSC?.trim();
480
+ if (explicitTsc) {
481
+ candidates.push({
482
+ label: "FORGE_TS7_TSC",
483
+ argv: argvForExecutable(explicitTsc, args),
484
+ command: `${explicitTsc} ${args.join(" ")}`,
485
+ });
486
+ }
487
+
488
+ const aliasedTs7 = packageBinPath(workspaceRoot, "typescript-7", "tsc");
489
+ if (aliasedTs7) {
490
+ candidates.push({
491
+ label: "typescript-7",
492
+ argv: argvForPackageBin(aliasedTs7, args),
493
+ command: "typescript-7 tsc --noEmit",
494
+ });
495
+ }
496
+
497
+ if ((packageMajorVersion(workspaceRoot, "typescript") ?? 0) >= 7) {
498
+ const rootTs = packageBinPath(workspaceRoot, "typescript", "tsc");
499
+ if (rootTs) {
500
+ candidates.push({
501
+ label: "typescript@7",
502
+ argv: argvForPackageBin(rootTs, args),
503
+ command: "typescript@7 tsc --noEmit",
504
+ });
505
+ }
506
+ }
507
+
508
+ const nativePreview = packageBinPath(workspaceRoot, "@typescript/native-preview", "tsgo");
509
+ if (nativePreview) {
510
+ candidates.push({
511
+ label: "@typescript/native-preview",
512
+ argv: argvForPackageBin(nativePreview, args),
513
+ command: "@typescript/native-preview tsgo --noEmit",
514
+ });
515
+ }
516
+
517
+ return candidates;
518
+ }
519
+
520
+ function missingNativeTypecheckResult(): ScriptRunResult {
521
+ return {
522
+ exitCode: 1,
523
+ stdout: "",
524
+ stderr: [
525
+ "No TypeScript native checker was found.",
526
+ "Install an aliased RC with `npm install -D typescript-7@npm:typescript@rc`,",
527
+ "set FORGE_TS7_TSC, or install @typescript/native-preview.",
528
+ ].join(" "),
529
+ command: "typescript native --noEmit",
530
+ durationMs: 0,
531
+ timedOut: false,
532
+ spawnError: true,
533
+ };
534
+ }
535
+
536
+ async function runTscTypecheck(
537
+ workspaceRoot: string,
538
+ scripts: PackageScripts,
539
+ timeoutMs: number,
540
+ ): Promise<ScriptRunResult> {
541
+ if (scripts.typecheck) {
542
+ return runPackageScript(workspaceRoot, "typecheck", timeoutMs);
543
+ }
544
+ const argv = resolveCommandArgv(["tsc", "--noEmit"]);
545
+ return spawnArgv(workspaceRoot, argv, timeoutMs, "tsc --noEmit");
546
+ }
547
+
548
+ async function runNativeTypecheck(workspaceRoot: string, timeoutMs: number): Promise<ScriptRunResult> {
549
+ const [candidate] = nativeTypecheckCandidates(workspaceRoot);
550
+ if (!candidate) {
551
+ return missingNativeTypecheckResult();
552
+ }
553
+ return spawnArgv(workspaceRoot, candidate.argv, timeoutMs, candidate.command);
554
+ }
555
+
556
+ async function runTsgoTypecheck(workspaceRoot: string, timeoutMs: number): Promise<ScriptRunResult> {
557
+ const args = nativeTypecheckArgs();
558
+ const nativePreview = packageBinPath(workspaceRoot, "@typescript/native-preview", "tsgo");
559
+ if (nativePreview) {
560
+ return spawnArgv(
561
+ workspaceRoot,
562
+ argvForPackageBin(nativePreview, args),
563
+ timeoutMs,
564
+ "@typescript/native-preview tsgo --noEmit",
565
+ );
566
+ }
567
+ const argv = resolveCommandArgv(["tsgo", ...args]);
568
+ return spawnArgv(workspaceRoot, argv, timeoutMs, "tsgo --noEmit");
569
+ }
570
+
571
+ function typecheckAttemptSummary(label: string, result: ScriptRunResult): string {
572
+ if (result.timedOut) {
573
+ return `${label}: timed out`;
574
+ }
575
+ if (result.spawnError) {
576
+ return `${label}: command unavailable`;
577
+ }
578
+ return `${label}: exit code ${result.exitCode}`;
579
+ }
580
+
581
+ function typecheckerFallbackDiagnostic(
582
+ choice: TypecheckerChoice,
583
+ attempts: Array<{ label: string; result: ScriptRunResult }>,
584
+ ): Diagnostic {
585
+ const excerpt = attempts
586
+ .map((attempt) => outputExcerpt(attempt.result.stdout, attempt.result.stderr))
587
+ .find(Boolean);
588
+ return createDiagnostic({
589
+ severity: "warning",
590
+ code: "FORGE_VERIFY_TYPECHECKER_FALLBACK",
591
+ message: `${choice} typecheck failed; fell back to tsc (${attempts
592
+ .map((attempt) => typecheckAttemptSummary(attempt.label, attempt.result))
593
+ .join("; ")})`,
594
+ fixHint: excerpt ? `Last native output: ${excerpt}` : undefined,
595
+ suggestedCommands: [
596
+ "npm install -D typescript-7@npm:typescript@rc",
597
+ "npm install -D @typescript/native-preview",
598
+ "forge verify --typechecker tsc",
599
+ ],
600
+ });
601
+ }
602
+
603
+ async function runPreferredTypecheck(
604
+ options: VerifyOptions,
605
+ scripts: PackageScripts,
606
+ timeoutMs: number,
607
+ ): Promise<{ result: ScriptRunResult; diagnostics: Diagnostic[]; label: string }> {
608
+ const choice = resolveTypechecker(options);
609
+ if (choice === "tsc") {
610
+ return { result: await runTscTypecheck(options.workspaceRoot, scripts, timeoutMs), diagnostics: [], label: "tsc" };
611
+ }
612
+
613
+ const attempts: Array<{ label: string; result: ScriptRunResult }> = [];
614
+ if (choice === "native" || choice === "ts7" || choice === "auto") {
615
+ const native = await runNativeTypecheck(options.workspaceRoot, timeoutMs);
616
+ if (native.exitCode === 0) {
617
+ return {
618
+ result: native,
619
+ diagnostics: [],
620
+ label: choice === "auto" ? "auto->native" : choice,
621
+ };
622
+ }
623
+ attempts.push({ label: "native", result: native });
624
+ }
625
+
626
+ if (choice === "tsgo" || choice === "auto") {
627
+ const tsgo = await runTsgoTypecheck(options.workspaceRoot, timeoutMs);
628
+ if (tsgo.exitCode === 0) {
629
+ return {
630
+ result: tsgo,
631
+ diagnostics: [],
632
+ label: choice === "auto" ? "auto->tsgo" : "tsgo",
633
+ };
634
+ }
635
+ attempts.push({ label: "tsgo", result: tsgo });
636
+ }
637
+
638
+ const fallback = await runTscTypecheck(options.workspaceRoot, scripts, timeoutMs);
639
+ return {
640
+ result: fallback,
641
+ diagnostics: [typecheckerFallbackDiagnostic(choice, attempts)],
642
+ label: `${choice}->tsc`,
643
+ };
644
+ }
645
+
646
+ const STRICT_TEST_COSTS: TestCost[] = ["instant", "fast", "standard", "slow"];
647
+ const STRICT_TEST_CHUNK_SIZE = 12;
648
+ const STRICT_TEST_MAX_DEFAULT_JOBS = 6;
649
+ // The isolated lane (one heavy file per chunk: node-compat, dev-server, CLI) is
650
+ // the makespan bottleneck, so it gets more default concurrency than before. The
651
+ // overall budget is still capped by STRICT_TEST_MAX_DEFAULT_JOBS and CPU count.
652
+ const STRICT_ISOLATED_TEST_MAX_DEFAULT_JOBS = 4;
653
+ const TESTGRAPH_PROFILE_RELATIVE_PATH = ".forge/test-runs/testgraph-profile.json";
654
+ const TEST_COST_FALLBACK_MS: Record<TestCost, number> = {
655
+ instant: 250,
656
+ fast: 1_000,
657
+ standard: 3_000,
658
+ slow: 12_000,
659
+ docker: 60_000,
660
+ browser: 60_000,
661
+ };
662
+ const TEST_COST_RANK: Record<TestCost, number> = {
663
+ instant: 0,
664
+ fast: 1,
665
+ standard: 2,
666
+ slow: 3,
667
+ docker: 4,
668
+ browser: 5,
669
+ };
670
+ const STRICT_TEST_FALLBACK_MS_BY_PATH: Array<{ pattern: RegExp; estimatedMs: number }> = [
671
+ { pattern: /^tests\/cli\/node-compat\.test\.ts$/, estimatedMs: 12_000 },
672
+ { pattern: /^tests\/cli\/node-compat-dev-server\.test\.ts$/, estimatedMs: 6_000 },
673
+ { pattern: /^tests\/cli\/node-compat-new\.test\.ts$/, estimatedMs: 8_000 },
674
+ { pattern: /^tests\/cli\/cli\.test\.ts$/, estimatedMs: 3_000 },
675
+ { pattern: /^tests\/cli\/cli-generation\.test\.ts$/, estimatedMs: 12_000 },
676
+ { pattern: /^tests\/cli\/cli-verify\.test\.ts$/, estimatedMs: 12_000 },
677
+ { pattern: /^tests\/cli\/cli-verify-changed\.test\.ts$/, estimatedMs: 5_000 },
678
+ { pattern: /^tests\/db\/pglite-adapter\.test\.ts$/, estimatedMs: 12_000 },
679
+ { pattern: /^tests\/dev\/dev-workflow-worker\.test\.ts$/, estimatedMs: 6_000 },
680
+ { pattern: /^tests\/external-manifest\/external-runtime-bridge\.test\.ts$/, estimatedMs: 4_000 },
681
+ { pattern: /^tests\/external-manifest\/external-runtime-cli\.test\.ts$/, estimatedMs: 6_000 },
682
+ { pattern: /^tests\/external-manifest\/external-runtime-node-cli\.test\.ts$/, estimatedMs: 12_000 },
683
+ { pattern: /^tests\/external-manifest\/go-adapter-conformance\.test\.ts$/, estimatedMs: 5_000 },
684
+ { pattern: /^tests\/external-manifest\/java-adapter-conformance\.test\.ts$/, estimatedMs: 20_000 },
685
+ { pattern: /^tests\/impact\/h28-impact\.test\.ts$/, estimatedMs: 8_000 },
686
+ { pattern: /^tests\/impact\/h28-impact-runner\.test\.ts$/, estimatedMs: 7_000 },
687
+ { pattern: /^tests\/impact\/h28-impact-runner-diagnostics\.test\.ts$/, estimatedMs: 3_000 },
688
+ { pattern: /^tests\/refactor\/h27-refactor\.test\.ts$/, estimatedMs: 6_000 },
689
+ { pattern: /^tests\/refactor\/h27-refactor-extract-action-apply\.test\.ts$/, estimatedMs: 10_000 },
690
+ { pattern: /^tests\/refactor\/h27-refactor-extract-action\.test\.ts$/, estimatedMs: 21_000 },
691
+ { pattern: /^tests\/refactor\/h27-refactor-extract-action-bindings\.test\.ts$/, estimatedMs: 10_000 },
692
+ { pattern: /^tests\/release\/h23-release-artifacts\.test\.ts$/, estimatedMs: 8_000 },
693
+ { pattern: /^tests\/release\/h23-release-self-host\.test\.ts$/, estimatedMs: 4_000 },
694
+ { pattern: /^tests\/release\/h23-release\.test\.ts$/, estimatedMs: 3_000 },
695
+ { pattern: /^tests\/templates\/new-b2b-support-web\.test\.ts$/, estimatedMs: 12_000 },
696
+ { pattern: /^tests\/templates\/new-agent-workroom\.test\.ts$/, estimatedMs: 12_000 },
697
+ { pattern: /^tests\/templates\/new-minimal-web\.test\.ts$/, estimatedMs: 12_000 },
698
+ { pattern: /^tests\/templates\/create-forge-app\.test\.ts$/, estimatedMs: 8_000 },
699
+ ];
700
+ const STRICT_ISOLATED_TEST_PATTERNS = [
701
+ /^tests\/ai\//,
702
+ /^tests\/cli\/cli-generation\.test\.ts$/,
703
+ /^tests\/cli\/cli\.test\.ts$/,
704
+ /^tests\/cli\/cli-verify\.test\.ts$/,
705
+ /^tests\/cli\/cli-verify-changed\.test\.ts$/,
706
+ /^tests\/cli\/node-compat-dev-server\.test\.ts$/,
707
+ /^tests\/cli\/node-compat-new\.test\.ts$/,
708
+ /^tests\/cli\/windows\.test\.ts$/,
709
+ /^tests\/client\//,
710
+ /^tests\/db\/pglite-adapter\.test\.ts$/,
711
+ /^tests\/dev\//,
712
+ /^tests\/external-manifest\/external-manifest\.test\.ts$/,
713
+ /^tests\/external-manifest\/go-adapter-conformance\.test\.ts$/,
714
+ /^tests\/external-manifest\/java-adapter-conformance\.test\.ts$/,
715
+ /^tests\/external-manifest\/external-runtime-bridge\.test\.ts$/,
716
+ /^tests\/external-manifest\/external-runtime-node-cli\.test\.ts$/,
717
+ /^tests\/impact\/h28-impact\.test\.ts$/,
718
+ /^tests\/impact\/h28-impact-runner\.test\.ts$/,
719
+ /^tests\/impact\/h28-impact-runner-diagnostics\.test\.ts$/,
720
+ /^tests\/live\//,
721
+ /^tests\/queries\/query-dev-server\.test\.ts$/,
722
+ // refactor extract-action/rename tests use ts.createProgram fresh per call
723
+ // against isolated temp workspaces (no shared global or server state), so they
724
+ // run safely co-located in the parallel lane and share one process warm-up
725
+ // instead of paying a cold start per isolated chunk.
726
+ /^tests\/release\/h23-release-artifacts\.test\.ts$/,
727
+ /^tests\/release\/h23-release-self-host\.test\.ts$/,
728
+ /^tests\/release\/h23-release\.test\.ts$/,
729
+ /^tests\/security\/tenant-isolation\/http-runtime\.test\.ts$/,
730
+ /^tests\/templates\/new-b2b-support-web\.test\.ts$/,
731
+ /^tests\/templates\/new-agent-workroom\.test\.ts$/,
732
+ /^tests\/templates\/new-minimal-web\.test\.ts$/,
733
+ /^tests\/telemetry\/telemetry-dev-server\.test\.ts$/,
734
+ ];
735
+ const STRICT_SERIAL_TEST_PATTERNS: RegExp[] = [];
736
+
737
+ interface StrictTestEntry {
738
+ file: string;
739
+ cost: TestCost;
740
+ lane: StrictTestLane;
741
+ estimatedMs: number;
742
+ durationSource: VerifyTestGraphDurationSource;
743
+ }
744
+
745
+ interface StrictGraphChunkResult extends ScriptRunResult {
746
+ files: string[];
747
+ lane: StrictTestLane;
748
+ chunkIndex: number;
749
+ chunkCount: number;
750
+ }
751
+
752
+ interface TestGraphProfileFile {
753
+ schemaVersion: "0.1.0";
754
+ updatedAt: string;
755
+ files: Record<string, {
756
+ durationMs: number;
757
+ runs: number;
758
+ lane: StrictTestLane;
759
+ sourceHash?: string;
760
+ lastExitCode: number;
761
+ lastRunAt: string;
762
+ }>;
763
+ }
764
+
765
+ function readTestGraph(workspaceRoot: string): TestGraph | null {
766
+ const raw = nodeFileSystem.readText(join(workspaceRoot, "src/forge/_generated/testGraph.json"));
767
+ if (!raw) {
768
+ return null;
769
+ }
770
+ return JSON.parse(stripDeterministicHeader(raw)) as TestGraph;
771
+ }
772
+
773
+ function strictTestEntries(workspaceRoot: string): Array<{ file: string; cost: TestCost }> {
774
+ const graph = readTestGraph(workspaceRoot);
775
+ if (!graph) {
776
+ return [];
777
+ }
778
+ const byFile = new Map<string, TestCost>();
779
+ for (const test of graph.tests) {
780
+ if (!STRICT_TEST_COSTS.includes(test.cost)) {
781
+ continue;
782
+ }
783
+ const existing = byFile.get(test.file);
784
+ if (!existing || TEST_COST_RANK[test.cost] > TEST_COST_RANK[existing]) {
785
+ byFile.set(test.file, test.cost);
786
+ }
787
+ }
788
+ return [...byFile.entries()]
789
+ .map(([file, cost]) => ({ file, cost }))
790
+ .sort((left, right) => left.file.localeCompare(right.file));
791
+ }
792
+
793
+ export function chunkFiles(files: string[], size: number): string[][] {
794
+ const chunks: string[][] = [];
795
+ for (let index = 0; index < files.length; index += size) {
796
+ chunks.push(files.slice(index, index + size));
797
+ }
798
+ return chunks;
799
+ }
800
+
801
+ export function resolveStrictTestJobs(options: {
802
+ requested?: number;
803
+ env?: NodeJS.ProcessEnv;
804
+ chunkCount: number;
805
+ }): number {
806
+ if (options.chunkCount <= 1) {
807
+ return 1;
808
+ }
809
+ const fromEnv = options.env?.FORGE_VERIFY_TEST_JOBS;
810
+ const parsedEnv = fromEnv ? Number(fromEnv) : undefined;
811
+ const requested = options.requested ?? parsedEnv;
812
+ if (requested !== undefined && Number.isInteger(requested) && requested >= 1) {
813
+ return Math.min(requested, options.chunkCount);
814
+ }
815
+
816
+ const cpuBound = Math.max(2, Math.floor(availableParallelism() / 2));
817
+ return Math.min(STRICT_TEST_MAX_DEFAULT_JOBS, cpuBound, options.chunkCount);
818
+ }
819
+
820
+ function resolveStrictLaneJobs(options: {
821
+ totalJobs: number;
822
+ parallelChunkCount: number;
823
+ isolatedChunkCount: number;
824
+ env?: NodeJS.ProcessEnv;
825
+ }): { parallelJobs: number; isolatedJobs: number } {
826
+ if (options.parallelChunkCount === 0) {
827
+ return {
828
+ parallelJobs: 0,
829
+ isolatedJobs: resolveStrictIsolatedTestJobs({
830
+ env: options.env,
831
+ chunkCount: Math.min(options.totalJobs, options.isolatedChunkCount),
832
+ }),
833
+ };
834
+ }
835
+ if (options.isolatedChunkCount === 0) {
836
+ return {
837
+ parallelJobs: Math.min(options.totalJobs, options.parallelChunkCount),
838
+ isolatedJobs: 0,
839
+ };
840
+ }
841
+ const requestedIsolated = resolveStrictIsolatedTestJobs({
842
+ env: options.env,
843
+ chunkCount: options.isolatedChunkCount,
844
+ });
845
+ const isolatedJobs = Math.min(
846
+ requestedIsolated,
847
+ options.isolatedChunkCount,
848
+ Math.max(1, options.totalJobs - 1),
849
+ );
850
+ const parallelJobs = Math.min(
851
+ options.parallelChunkCount,
852
+ Math.max(1, options.totalJobs - isolatedJobs),
853
+ );
854
+ return { parallelJobs, isolatedJobs };
855
+ }
856
+
857
+ export function resolveStrictIsolatedTestJobs(options: {
858
+ requested?: number;
859
+ env?: NodeJS.ProcessEnv;
860
+ chunkCount: number;
861
+ }): number {
862
+ if (options.chunkCount <= 1) {
863
+ return 1;
864
+ }
865
+ const fromEnv = options.env?.FORGE_VERIFY_ISOLATED_TEST_JOBS;
866
+ const parsedEnv = fromEnv ? Number(fromEnv) : undefined;
867
+ const requested = options.requested ?? parsedEnv;
868
+ if (requested !== undefined && Number.isInteger(requested) && requested >= 1) {
869
+ return Math.min(requested, options.chunkCount);
870
+ }
871
+ return Math.min(STRICT_ISOLATED_TEST_MAX_DEFAULT_JOBS, options.chunkCount);
872
+ }
873
+
874
+ function normalizeTestPath(file: string): string {
875
+ return file.replace(/\\/g, "/");
876
+ }
877
+
878
+ export type StrictTestLane = VerifyTestGraphLane;
879
+
880
+ export function classifyStrictTestFile(file: string): StrictTestLane {
881
+ const normalized = normalizeTestPath(file);
882
+ if (STRICT_SERIAL_TEST_PATTERNS.some((pattern) => pattern.test(normalized))) {
883
+ return "serial";
884
+ }
885
+ if (STRICT_ISOLATED_TEST_PATTERNS.some((pattern) => pattern.test(normalized))) {
886
+ return "isolated";
887
+ }
888
+ return "parallel";
889
+ }
890
+
891
+ function testGraphProfilePath(workspaceRoot: string): string {
892
+ return join(workspaceRoot, TESTGRAPH_PROFILE_RELATIVE_PATH);
893
+ }
894
+
895
+ function testFileSourceHash(workspaceRoot: string, file: string): string | null {
896
+ const source = nodeFileSystem.readText(join(workspaceRoot, file));
897
+ if (source === null) {
898
+ return null;
899
+ }
900
+ return createHash("sha256")
901
+ .update(normalizeTestPath(file))
902
+ .update("\0")
903
+ .update(source)
904
+ .digest("hex")
905
+ .slice(0, 16);
906
+ }
907
+
908
+ function readTestGraphProfile(workspaceRoot: string): TestGraphProfileFile | null {
909
+ const raw = nodeFileSystem.readText(testGraphProfilePath(workspaceRoot));
910
+ if (!raw) {
911
+ return null;
912
+ }
913
+ try {
914
+ const parsed = JSON.parse(raw) as TestGraphProfileFile;
915
+ if (parsed.schemaVersion !== "0.1.0" || typeof parsed.files !== "object") {
916
+ return null;
917
+ }
918
+ return parsed;
919
+ } catch {
920
+ return null;
921
+ }
922
+ }
923
+
924
+ function estimateStrictTestEntry(
925
+ workspaceRoot: string,
926
+ file: string,
927
+ cost: TestCost,
928
+ lane: StrictTestLane,
929
+ profile: TestGraphProfileFile | null,
930
+ ): { estimatedMs: number; source: VerifyTestGraphDurationSource } {
931
+ const profiled = profile?.files[file];
932
+ const sourceHash = testFileSourceHash(workspaceRoot, file);
933
+ if (
934
+ profiled &&
935
+ sourceHash !== null &&
936
+ profiled.sourceHash === sourceHash &&
937
+ Number.isFinite(profiled.durationMs) &&
938
+ profiled.durationMs > 0
939
+ ) {
940
+ return { estimatedMs: Math.max(1, Math.round(profiled.durationMs)), source: "profile" };
941
+ }
942
+ const normalized = normalizeTestPath(file);
943
+ const pathOverride = STRICT_TEST_FALLBACK_MS_BY_PATH.find((entry) => entry.pattern.test(normalized));
944
+ const fallback = pathOverride?.estimatedMs ?? TEST_COST_FALLBACK_MS[cost] ?? TEST_COST_FALLBACK_MS.standard;
945
+ if (lane === "serial") {
946
+ return { estimatedMs: Math.max(fallback, 8_000), source: "fallback" };
947
+ }
948
+ if (lane === "isolated") {
949
+ return { estimatedMs: Math.max(fallback, 3_000), source: "fallback" };
950
+ }
951
+ return { estimatedMs: fallback, source: "fallback" };
952
+ }
953
+
954
+ function weightedStrictTestEntries(
955
+ workspaceRoot: string,
956
+ profile: TestGraphProfileFile | null,
957
+ ): StrictTestEntry[] {
958
+ return strictTestEntries(workspaceRoot).map(({ file, cost }) => {
959
+ const lane = classifyStrictTestFile(file);
960
+ const estimate = estimateStrictTestEntry(workspaceRoot, file, cost, lane, profile);
961
+ return {
962
+ file,
963
+ cost,
964
+ lane,
965
+ estimatedMs: estimate.estimatedMs,
966
+ durationSource: estimate.source,
967
+ };
968
+ });
969
+ }
970
+
971
+ function partitionStrictTestEntries(entries: StrictTestEntry[]): {
972
+ parallel: StrictTestEntry[];
973
+ isolated: StrictTestEntry[];
974
+ serial: StrictTestEntry[];
975
+ } {
976
+ const parallel: StrictTestEntry[] = [];
977
+ const isolated: StrictTestEntry[] = [];
978
+ const serial: StrictTestEntry[] = [];
979
+ for (const entry of entries) {
980
+ const lane = entry.lane;
981
+ if (lane === "serial") {
982
+ serial.push(entry);
983
+ continue;
984
+ }
985
+ if (lane === "isolated") {
986
+ isolated.push(entry);
987
+ continue;
988
+ }
989
+ parallel.push(entry);
990
+ }
991
+ return { parallel, isolated, serial };
992
+ }
993
+
994
+ export function packWeightedStrictTestChunks(
995
+ entries: Array<{ file: string; estimatedMs: number; durationSource: VerifyTestGraphDurationSource }>,
996
+ size: number,
997
+ ): Array<{ files: string[]; estimatedMs: number; durationSource: VerifyTestGraphDurationSource }> {
998
+ if (entries.length === 0) {
999
+ return [];
1000
+ }
1001
+ const binCount = Math.max(1, Math.ceil(entries.length / Math.max(1, size)));
1002
+ const bins = Array.from({ length: binCount }, () => ({
1003
+ files: [] as string[],
1004
+ estimatedMs: 0,
1005
+ durationSource: "profile" as VerifyTestGraphDurationSource,
1006
+ }));
1007
+ const ordered = [...entries].sort((left, right) => {
1008
+ const byEstimate = right.estimatedMs - left.estimatedMs;
1009
+ return byEstimate !== 0 ? byEstimate : left.file.localeCompare(right.file);
1010
+ });
1011
+ for (const entry of ordered) {
1012
+ const target = bins
1013
+ .filter((bin) => bin.files.length < size)
1014
+ .sort((left, right) => {
1015
+ const byEstimate = left.estimatedMs - right.estimatedMs;
1016
+ return byEstimate !== 0 ? byEstimate : left.files.length - right.files.length;
1017
+ })[0] ?? bins[0]!;
1018
+ target.files.push(entry.file);
1019
+ target.files.sort();
1020
+ target.estimatedMs += entry.estimatedMs;
1021
+ if (entry.durationSource === "fallback") {
1022
+ target.durationSource = "fallback";
1023
+ }
1024
+ }
1025
+ return bins.filter((bin) => bin.files.length > 0);
1026
+ }
1027
+
1028
+ function oneFileChunks(
1029
+ entries: StrictTestEntry[],
1030
+ ): Array<{ files: string[]; estimatedMs: number; durationSource: VerifyTestGraphDurationSource }> {
1031
+ return [...entries]
1032
+ .sort((left, right) => {
1033
+ const byEstimate = right.estimatedMs - left.estimatedMs;
1034
+ return byEstimate !== 0 ? byEstimate : left.file.localeCompare(right.file);
1035
+ })
1036
+ .map((entry) => ({
1037
+ files: [entry.file],
1038
+ estimatedMs: entry.estimatedMs,
1039
+ durationSource: entry.durationSource,
1040
+ }));
1041
+ }
1042
+
1043
+ function laneEstimate(chunks: VerifyTestGraphPlanChunk[], jobs: number): number {
1044
+ const workers = Array.from({ length: Math.max(1, jobs) }, () => 0);
1045
+ for (const chunk of chunks) {
1046
+ workers.sort((left, right) => left - right);
1047
+ workers[0] += chunk.estimatedMs;
1048
+ }
1049
+ return Math.max(...workers, 0);
1050
+ }
1051
+
1052
+ function strictPlanRecommendations(plan: VerifyTestGraphPlan): string[] {
1053
+ const recommendations: string[] = [];
1054
+ if (!plan.profileFound) {
1055
+ recommendations.push(`Run forge verify --strict once to create ${TESTGRAPH_PROFILE_RELATIVE_PATH}; later plans use measured durations.`);
1056
+ }
1057
+ if (plan.lanes.serial.chunkCount > 0 && plan.lanes.serial.estimatedMs > plan.criticalPathEstimateMs * 0.35) {
1058
+ recommendations.push("Split or de-globalize the slowest serial tests; serial work is now the main critical-path limiter.");
1059
+ } else if (plan.lanes.serial.chunkCount === 0 && plan.lanes.isolated.chunkCount > 0) {
1060
+ recommendations.push("No current strict TestGraph files require the serial lane; optimize isolated runtime/template tests next.");
1061
+ }
1062
+ if (plan.lanes.isolated.chunkCount > 0 && plan.isolatedJobs < STRICT_ISOLATED_TEST_MAX_DEFAULT_JOBS) {
1063
+ recommendations.push(`Set FORGE_VERIFY_ISOLATED_TEST_JOBS=${STRICT_ISOLATED_TEST_MAX_DEFAULT_JOBS} on machines that can run isolated runtime tests concurrently.`);
1064
+ }
1065
+ const slowest = plan.slowestFiles[0];
1066
+ if (slowest) {
1067
+ recommendations.push(`Inspect ${slowest.file}; it is currently the heaviest estimated TestGraph file.`);
1068
+ }
1069
+ return recommendations;
1070
+ }
1071
+
1072
+ export function buildStrictTestGraphPlan(
1073
+ workspaceRoot: string,
1074
+ testJobs?: number,
1075
+ env: NodeJS.ProcessEnv = process.env,
1076
+ ): VerifyTestGraphPlan {
1077
+ const profile = readTestGraphProfile(workspaceRoot);
1078
+ const entries = weightedStrictTestEntries(workspaceRoot, profile);
1079
+ const partitioned = partitionStrictTestEntries(entries);
1080
+ const parallelRaw = packWeightedStrictTestChunks(partitioned.parallel, STRICT_TEST_CHUNK_SIZE);
1081
+ const isolatedRaw = oneFileChunks(partitioned.isolated);
1082
+ const serialRaw = oneFileChunks(partitioned.serial);
1083
+ const totalJobs = resolveStrictTestJobs({
1084
+ requested: testJobs,
1085
+ env,
1086
+ chunkCount: parallelRaw.length + isolatedRaw.length,
1087
+ });
1088
+ const { parallelJobs, isolatedJobs } = resolveStrictLaneJobs({
1089
+ totalJobs,
1090
+ parallelChunkCount: parallelRaw.length,
1091
+ isolatedChunkCount: isolatedRaw.length,
1092
+ env,
1093
+ });
1094
+ let index = 1;
1095
+ const toPlanChunks = (
1096
+ lane: StrictTestLane,
1097
+ chunks: Array<{ files: string[]; estimatedMs: number; durationSource: VerifyTestGraphDurationSource }>,
1098
+ ): VerifyTestGraphPlanChunk[] => chunks.map((chunk) => ({
1099
+ index: index++,
1100
+ lane,
1101
+ files: chunk.files,
1102
+ estimatedMs: chunk.estimatedMs,
1103
+ durationSource: chunk.durationSource,
1104
+ }));
1105
+ const parallelChunks = toPlanChunks("parallel", parallelRaw);
1106
+ const isolatedChunks = toPlanChunks("isolated", isolatedRaw);
1107
+ const serialChunks = toPlanChunks("serial", serialRaw);
1108
+ const laneMode =
1109
+ totalJobs <= 1 && parallelChunks.length > 0 && isolatedChunks.length > 0
1110
+ ? "sequential"
1111
+ : "overlap";
1112
+ const chunks = [...parallelChunks, ...isolatedChunks, ...serialChunks];
1113
+ const lanes = {
1114
+ parallel: {
1115
+ fileCount: partitioned.parallel.length,
1116
+ chunkCount: parallelChunks.length,
1117
+ estimatedMs: parallelChunks.reduce((sum, chunk) => sum + chunk.estimatedMs, 0),
1118
+ },
1119
+ isolated: {
1120
+ fileCount: partitioned.isolated.length,
1121
+ chunkCount: isolatedChunks.length,
1122
+ estimatedMs: isolatedChunks.reduce((sum, chunk) => sum + chunk.estimatedMs, 0),
1123
+ },
1124
+ serial: {
1125
+ fileCount: partitioned.serial.length,
1126
+ chunkCount: serialChunks.length,
1127
+ estimatedMs: serialChunks.reduce((sum, chunk) => sum + chunk.estimatedMs, 0),
1128
+ },
1129
+ };
1130
+ const plan: VerifyTestGraphPlan = {
1131
+ schemaVersion: "0.1.0",
1132
+ fileCount: entries.length,
1133
+ chunkCount: chunks.length,
1134
+ totalJobs,
1135
+ laneMode,
1136
+ jobs: parallelJobs,
1137
+ isolatedJobs,
1138
+ lanes,
1139
+ chunks,
1140
+ criticalPathEstimateMs:
1141
+ (laneMode === "sequential"
1142
+ ? laneEstimate(parallelChunks, parallelJobs) + laneEstimate(isolatedChunks, isolatedJobs)
1143
+ : Math.max(
1144
+ laneEstimate(parallelChunks, parallelJobs),
1145
+ laneEstimate(isolatedChunks, isolatedJobs),
1146
+ )) +
1147
+ lanes.serial.estimatedMs,
1148
+ profilePath: TESTGRAPH_PROFILE_RELATIVE_PATH,
1149
+ profileFound: profile !== null,
1150
+ slowestFiles: [...entries]
1151
+ .sort((left, right) => {
1152
+ const byEstimate = right.estimatedMs - left.estimatedMs;
1153
+ return byEstimate !== 0 ? byEstimate : left.file.localeCompare(right.file);
1154
+ })
1155
+ .slice(0, 10)
1156
+ .map((entry) => ({
1157
+ file: entry.file,
1158
+ lane: entry.lane,
1159
+ estimatedMs: entry.estimatedMs,
1160
+ source: entry.durationSource,
1161
+ })),
1162
+ recommendations: [],
1163
+ };
1164
+ plan.recommendations = strictPlanRecommendations(plan);
1165
+ return plan;
1166
+ }
1167
+
1168
+ async function runStrictGraphChunkPool(
1169
+ workspaceRoot: string,
1170
+ chunks: VerifyTestGraphPlanChunk[],
1171
+ timeoutMs: number,
1172
+ jobs: number,
1173
+ totalChunks = chunks.length,
1174
+ ): Promise<{ results: Array<StrictGraphChunkResult | undefined>; timedOut: boolean }> {
1175
+ const results: Array<StrictGraphChunkResult | undefined> = [];
1176
+ let nextChunk = 0;
1177
+ let stopScheduling = false;
1178
+
1179
+ async function runNextChunk(): Promise<void> {
1180
+ while (!stopScheduling) {
1181
+ const chunkIndex = nextChunk;
1182
+ nextChunk += 1;
1183
+ const chunk = chunks[chunkIndex];
1184
+ if (!chunk) {
1185
+ return;
1186
+ }
1187
+ const result = await runStrictGraphTestChunk(
1188
+ workspaceRoot,
1189
+ chunk.files,
1190
+ chunk.index - 1,
1191
+ totalChunks,
1192
+ timeoutMs,
1193
+ );
1194
+ results[chunkIndex] = {
1195
+ ...result,
1196
+ files: chunk.files,
1197
+ lane: chunk.lane,
1198
+ chunkIndex: chunk.index,
1199
+ chunkCount: totalChunks,
1200
+ };
1201
+ if (result.exitCode !== 0) {
1202
+ stopScheduling = true;
1203
+ return;
1204
+ }
1205
+ }
1206
+ }
1207
+
1208
+ await Promise.all(Array.from({ length: jobs }, () => runNextChunk()));
1209
+ return {
1210
+ results,
1211
+ timedOut: results.some((result) => result?.timedOut),
1212
+ };
1213
+ }
1214
+
1215
+ function writeTestGraphProfile(
1216
+ workspaceRoot: string,
1217
+ results: Array<StrictGraphChunkResult | undefined>,
1218
+ ): void {
1219
+ const existing = readTestGraphProfile(workspaceRoot);
1220
+ const now = new Date().toISOString();
1221
+ const files = { ...(existing?.files ?? {}) };
1222
+ for (const result of results) {
1223
+ if (!result || result.files.length === 0) {
1224
+ continue;
1225
+ }
1226
+ const perFileDuration = Math.max(1, Math.round(result.durationMs / result.files.length));
1227
+ for (const file of result.files) {
1228
+ const previous = files[file];
1229
+ files[file] = {
1230
+ durationMs: perFileDuration,
1231
+ runs: (previous?.runs ?? 0) + 1,
1232
+ lane: result.lane,
1233
+ sourceHash: testFileSourceHash(workspaceRoot, file) ?? previous?.sourceHash,
1234
+ lastExitCode: result.exitCode,
1235
+ lastRunAt: now,
1236
+ };
1237
+ }
1238
+ }
1239
+ const profile: TestGraphProfileFile = {
1240
+ schemaVersion: "0.1.0",
1241
+ updatedAt: now,
1242
+ files,
1243
+ };
1244
+ const path = testGraphProfilePath(workspaceRoot);
1245
+ nodeFileSystem.mkdirp(dirname(path));
1246
+ nodeFileSystem.writeText(path, `${JSON.stringify(profile, null, 2)}\n`);
1247
+ }
1248
+
1249
+ function strictGraphChunkToTestRunStep(result: StrictGraphChunkResult, timeoutMs: number): TestRunStep & {
1250
+ files: string[];
1251
+ lane: StrictTestLane;
1252
+ chunkIndex: number;
1253
+ chunkCount: number;
1254
+ reproduceCommand: string;
1255
+ } {
1256
+ return {
1257
+ command: result.command,
1258
+ ok: result.exitCode === 0,
1259
+ exitCode: result.exitCode,
1260
+ durationMs: result.durationMs,
1261
+ timedOut: result.timedOut,
1262
+ failureKind: packageScriptFailureKind(result),
1263
+ stdout: result.stdout,
1264
+ stderr: result.stderr,
1265
+ files: result.files,
1266
+ lane: result.lane,
1267
+ chunkIndex: result.chunkIndex,
1268
+ chunkCount: result.chunkCount,
1269
+ reproduceCommand: `bun test ${result.files.join(" ")} --timeout ${timeoutMs}`,
1270
+ };
1271
+ }
1272
+
1273
+ function writeStrictGraphTestRunRecord(
1274
+ workspaceRoot: string,
1275
+ plan: VerifyTestGraphPlan,
1276
+ results: StrictGraphChunkResult[],
1277
+ timeoutMs: number,
1278
+ durationMs: number,
1279
+ ): TestRunRecord {
1280
+ const commands = results.map((result) => result.command);
1281
+ const record: TestRunRecord = {
1282
+ schemaVersion: "0.1.0",
1283
+ id: `run_${createHash("sha256")
1284
+ .update(`${Date.now()}:${commands.join("|")}:${plan.fileCount}:${plan.chunkCount}`)
1285
+ .digest("hex")
1286
+ .slice(0, 12)}`,
1287
+ changedHash: `sha256:${createHash("sha256").update(canonicalJson(plan.chunks)).digest("hex")}`,
1288
+ planHash: `sha256:${createHash("sha256").update(canonicalJson(plan)).digest("hex")}`,
1289
+ source: { mode: "changed", id: "verify-strict-testgraph" },
1290
+ commands,
1291
+ timeoutMs,
1292
+ results: results.map((result) => strictGraphChunkToTestRunStep(result, timeoutMs)),
1293
+ failed: results.filter((result) => result.exitCode !== 0).map((result) => result.command),
1294
+ durationMs,
1295
+ };
1296
+ const runDir = join(workspaceRoot, ".forge/test-runs");
1297
+ nodeFileSystem.mkdirp(runDir);
1298
+ nodeFileSystem.writeText(join(runDir, "last.json"), serializeCanonical(record));
1299
+ nodeFileSystem.writeText(join(runDir, `${record.id}.json`), serializeCanonical(record));
1300
+ return record;
1301
+ }
1302
+
1303
+ async function runStrictGraphTests(
1304
+ workspaceRoot: string,
1305
+ timeoutMs: number,
1306
+ testJobs?: number,
1307
+ ): Promise<ScriptRunResult & {
1308
+ fileCount: number;
1309
+ chunkCount: number;
1310
+ jobs: number;
1311
+ isolatedJobs: number;
1312
+ plan: VerifyTestGraphPlan;
1313
+ failedFiles: string[];
1314
+ failedChunk?: number;
1315
+ reportPath?: string;
1316
+ }> {
1317
+ const plan = buildStrictTestGraphPlan(workspaceRoot, testJobs);
1318
+ if (plan.fileCount === 0) {
1319
+ return {
1320
+ exitCode: 1,
1321
+ stdout: "",
1322
+ stderr: "TestGraph has no non-docker/browser tests",
1323
+ command: "forge strict TestGraph tests",
1324
+ durationMs: 0,
1325
+ timedOut: false,
1326
+ spawnError: true,
1327
+ fileCount: 0,
1328
+ chunkCount: 0,
1329
+ jobs: 0,
1330
+ isolatedJobs: 0,
1331
+ plan,
1332
+ failedFiles: [],
1333
+ };
1334
+ }
1335
+
1336
+ const started = Date.now();
1337
+ let stdout = "";
1338
+ let stderr = "";
1339
+ let timedOut = false;
1340
+ let exitCode = 0;
1341
+ let failedOutput: Pick<ScriptRunResult, "stdout" | "stderr"> | null = null;
1342
+ const parallelChunks = plan.chunks.filter((chunk) => chunk.lane === "parallel");
1343
+ const isolatedChunks = plan.chunks.filter((chunk) => chunk.lane === "isolated");
1344
+ const serialChunks = plan.chunks.filter((chunk) => chunk.lane === "serial");
1345
+
1346
+ let command = `bun test <${plan.fileCount} TestGraph files in ${plan.chunkCount} chunks, ${plan.laneMode} lanes, total jobs ${plan.totalJobs}, parallel jobs ${plan.jobs}, isolated jobs ${plan.isolatedJobs}, isolated ${isolatedChunks.length}, serial ${serialChunks.length}> --timeout ${timeoutMs}`;
1347
+ const parallelPool = () => runStrictGraphChunkPool(
1348
+ workspaceRoot,
1349
+ parallelChunks,
1350
+ timeoutMs,
1351
+ plan.jobs,
1352
+ plan.chunkCount,
1353
+ );
1354
+ const isolatedPool = () => runStrictGraphChunkPool(
1355
+ workspaceRoot,
1356
+ isolatedChunks,
1357
+ timeoutMs,
1358
+ plan.isolatedJobs,
1359
+ plan.chunkCount,
1360
+ );
1361
+ const [parallelRun, isolatedRun] = plan.laneMode === "sequential"
1362
+ ? [await parallelPool(), await isolatedPool()]
1363
+ : await Promise.all([parallelPool(), isolatedPool()]);
1364
+ const orderedResults: StrictGraphChunkResult[] = [];
1365
+ for (const result of parallelRun.results) {
1366
+ if (result) {
1367
+ orderedResults.push(result);
1368
+ }
1369
+ }
1370
+ for (const result of isolatedRun.results) {
1371
+ if (result) {
1372
+ orderedResults.push(result);
1373
+ }
1374
+ }
1375
+ timedOut = timedOut || parallelRun.timedOut;
1376
+ timedOut = timedOut || isolatedRun.timedOut;
1377
+
1378
+ if (
1379
+ parallelRun.results.every((result) => result?.exitCode === 0) &&
1380
+ isolatedRun.results.every((result) => result?.exitCode === 0)
1381
+ ) {
1382
+ for (const chunk of serialChunks) {
1383
+ const result = await runStrictGraphTestChunk(
1384
+ workspaceRoot,
1385
+ chunk.files,
1386
+ chunk.index - 1,
1387
+ plan.chunkCount,
1388
+ timeoutMs,
1389
+ );
1390
+ orderedResults.push({
1391
+ ...result,
1392
+ files: chunk.files,
1393
+ lane: chunk.lane,
1394
+ chunkIndex: chunk.index,
1395
+ chunkCount: plan.chunkCount,
1396
+ });
1397
+ timedOut = timedOut || result.timedOut;
1398
+ if (result.exitCode !== 0) {
1399
+ break;
1400
+ }
1401
+ }
1402
+ }
1403
+
1404
+ writeTestGraphProfile(workspaceRoot, orderedResults);
1405
+
1406
+ let failedResult: StrictGraphChunkResult | undefined;
1407
+ for (const result of orderedResults) {
1408
+ if (!result) {
1409
+ continue;
1410
+ }
1411
+ stdout += result.stdout;
1412
+ stderr += result.stderr;
1413
+ timedOut = timedOut || result.timedOut;
1414
+ if (result.exitCode !== 0) {
1415
+ exitCode = result.exitCode;
1416
+ command = result.command;
1417
+ failedOutput = { stdout: result.stdout, stderr: result.stderr };
1418
+ failedResult = result;
1419
+ break;
1420
+ }
1421
+ }
1422
+
1423
+ const report = writeStrictGraphTestRunRecord(
1424
+ workspaceRoot,
1425
+ plan,
1426
+ orderedResults,
1427
+ timeoutMs,
1428
+ Date.now() - started,
1429
+ );
1430
+
1431
+ return {
1432
+ exitCode,
1433
+ stdout: failedOutput?.stdout ?? stdout,
1434
+ stderr: failedOutput?.stderr ?? stderr,
1435
+ command,
1436
+ durationMs: Date.now() - started,
1437
+ timedOut,
1438
+ fileCount: plan.fileCount,
1439
+ chunkCount: plan.chunkCount,
1440
+ jobs: plan.jobs,
1441
+ isolatedJobs: plan.isolatedJobs,
1442
+ plan,
1443
+ failedFiles: failedResult?.files ?? [],
1444
+ failedChunk: failedResult?.chunkIndex,
1445
+ reportPath: `.forge/test-runs/${report.id}.json`,
1446
+ };
1447
+ }
1448
+
1449
+ function runStrictGraphTestChunk(
1450
+ workspaceRoot: string,
1451
+ chunk: string[],
1452
+ chunkIndex: number,
1453
+ chunkCount: number,
1454
+ timeoutMs: number,
1455
+ ): Promise<ScriptRunResult> {
1456
+ const argv = resolveCommandArgv(["bun", "test", ...chunk, "--timeout", String(timeoutMs)]);
1457
+ const chunkCommand = `bun test <TestGraph chunk ${chunkIndex + 1}/${chunkCount}, ${chunk.length} files> --timeout ${timeoutMs}`;
1458
+ const chunkTempDir = mkdtempSync(join(tmpdir(), `forge-testgraph-${chunkIndex + 1}-`));
1459
+ return spawnArgv(workspaceRoot, argv, timeoutMs, chunkCommand, {
1460
+ TMP: chunkTempDir,
1461
+ TEMP: chunkTempDir,
1462
+ TMPDIR: chunkTempDir,
1463
+ FORGE_TEST_TMPDIR: chunkTempDir,
1464
+ FORGE_VERIFY_CHUNK_INDEX: String(chunkIndex + 1),
1465
+ FORGE_VERIFY_CHUNK_COUNT: String(chunkCount),
1466
+ FORGE_DEV_PORT: "0",
1467
+ }).finally(() => {
1468
+ rmSync(chunkTempDir, { recursive: true, force: true });
1469
+ });
1470
+ }
1471
+
232
1472
  function resolveVerifyProfile(options: VerifyOptions): VerifyProfile {
1473
+ if (options.internal) {
1474
+ return "internal";
1475
+ }
233
1476
  if (options.changed) {
234
1477
  return "changed";
235
1478
  }
@@ -247,7 +1490,7 @@ function resolveVerifyProfile(options: VerifyOptions): VerifyProfile {
247
1490
 
248
1491
  async function runStandardImpactTests(
249
1492
  options: VerifyOptions,
250
- ): Promise<{ steps: VerifyStep[]; diagnostics: Diagnostic[] }> {
1493
+ ): Promise<{ steps: VerifyStep[]; diagnostics: Diagnostic[]; testCoverageReason?: string }> {
251
1494
  const started = Date.now();
252
1495
  const diagnostics: Diagnostic[] = [];
253
1496
  const steps: VerifyStep[] = [];
@@ -270,8 +1513,23 @@ async function runStandardImpactTests(
270
1513
  const commands = impactOnlyPlan.tests.map((test) => test.command);
271
1514
 
272
1515
  if (commands.length === 0) {
1516
+ const reason = plan.derivedOnly
1517
+ ? "changed files are derived generated artifacts only"
1518
+ : "impact planner selected no test files for the current changes";
273
1519
  steps.push(skippedStep("impact-tests", "no changed files selected an impact test"));
274
- return { steps, diagnostics };
1520
+ diagnostics.push(
1521
+ createDiagnostic({
1522
+ severity: "warning",
1523
+ code: FORGE_VERIFY_NO_TESTS_SELECTED,
1524
+ message: "standard verification did not select any impact tests; only non-test checks ran",
1525
+ fixHint: "Run forge verify --strict when you need full test-suite coverage.",
1526
+ suggestedCommands: [
1527
+ "forge test plan --changed --json",
1528
+ "forge verify --strict",
1529
+ ],
1530
+ }),
1531
+ );
1532
+ return { steps, diagnostics, testCoverageReason: reason };
275
1533
  }
276
1534
 
277
1535
  const record = await runImpactTestPlan(options.workspaceRoot, impactOnlyPlan, {
@@ -331,6 +1589,69 @@ export async function runVerifyCommand(
331
1589
  const scripts = readPackageScripts(options.workspaceRoot);
332
1590
  const scriptTimeoutMs = resolveScriptTimeoutMs(options);
333
1591
  const profile = resolveVerifyProfile(options);
1592
+ const frameworkWorkspace = isForgeOsFrameworkWorkspace(options.workspaceRoot);
1593
+ const canRunInternalTests = options.internal || !frameworkWorkspace;
1594
+ let testGraphPlan: VerifyTestGraphPlan | undefined;
1595
+ let testCoverageReason: string | undefined;
1596
+
1597
+ if (options.testPlan) {
1598
+ if (frameworkWorkspace && !options.internal) {
1599
+ steps.push({
1600
+ name: "tests:framework-testgraph-plan",
1601
+ ok: true,
1602
+ skipped: true,
1603
+ skipReason: "ForgeOS framework TestGraph is maintainer-only; use forge verify framework --test-plan --json",
1604
+ });
1605
+ diagnostics.push(
1606
+ createDiagnostic({
1607
+ severity: "warning",
1608
+ code: "FORGE_VERIFY_INTERNAL_TESTS_SKIPPED",
1609
+ message: "Skipped ForgeOS framework TestGraph plan during app-level verify.",
1610
+ fixHint: "Run forge verify framework --test-plan --json when maintaining ForgeOS itself.",
1611
+ suggestedCommands: [
1612
+ "forge verify framework --test-plan --json",
1613
+ "forge verify --standard --json",
1614
+ ],
1615
+ }),
1616
+ );
1617
+ return {
1618
+ ok: true,
1619
+ profile,
1620
+ steps,
1621
+ diagnostics,
1622
+ durationMs: Date.now() - started,
1623
+ exitCode: 0,
1624
+ };
1625
+ }
1626
+ testGraphPlan = buildStrictTestGraphPlan(options.workspaceRoot, options.testJobs);
1627
+ steps.push({
1628
+ name: "tests:testgraph-plan",
1629
+ ok: testGraphPlan.fileCount > 0,
1630
+ skipped: false,
1631
+ exitCode: testGraphPlan.fileCount > 0 ? 0 : 1,
1632
+ command: `forge verify --strict --test-plan (${testGraphPlan.fileCount} files, ${testGraphPlan.chunkCount} chunks)`,
1633
+ durationMs: Date.now() - started,
1634
+ });
1635
+ if (testGraphPlan.fileCount === 0) {
1636
+ diagnostics.push(
1637
+ createDiagnostic({
1638
+ severity: "error",
1639
+ code: "FORGE_VERIFY_TESTGRAPH_EMPTY",
1640
+ message: "TestGraph has no non-docker/browser tests",
1641
+ }),
1642
+ );
1643
+ }
1644
+ const ok = steps.every((step) => step.ok);
1645
+ return {
1646
+ ok,
1647
+ profile,
1648
+ steps,
1649
+ diagnostics,
1650
+ testGraphPlan,
1651
+ durationMs: Date.now() - started,
1652
+ exitCode: ok ? 0 : 1,
1653
+ };
1654
+ }
334
1655
 
335
1656
  if (options.changed) {
336
1657
  const plan = buildImpactTestPlan({
@@ -400,7 +1721,7 @@ export async function runVerifyCommand(
400
1721
  printProgress(options, "verify: forge-check");
401
1722
  const checkStarted = Date.now();
402
1723
  const forgeCheck = await runCheckCommand(options.workspaceRoot, {
403
- strictSecrets: options.strict,
1724
+ strictSecrets: options.strict || options.internal === true,
404
1725
  });
405
1726
  steps.push({
406
1727
  name: "forge-check",
@@ -410,7 +1731,7 @@ export async function runVerifyCommand(
410
1731
  });
411
1732
  diagnostics.push(...forgeCheck.errors, ...forgeCheck.warnings);
412
1733
 
413
- if (profile === "strict" || profile === "standard") {
1734
+ if (profile === "strict" || profile === "standard" || profile === "internal") {
414
1735
  printProgress(options, "verify: policy-check-strict");
415
1736
  const policyStarted = Date.now();
416
1737
  const policyCheck = await runPolicyCommand({
@@ -505,11 +1826,14 @@ export async function runVerifyCommand(
505
1826
 
506
1827
  if (options.skipTypecheck) {
507
1828
  steps.push(skippedStep("typecheck", "--skip-typecheck"));
508
- } else if (!scripts.typecheck) {
509
- steps.push(skippedStep("typecheck", "no typecheck script in package.json"));
510
1829
  } else {
511
- printProgress(options, `verify: typecheck (${scriptTimeoutMs}ms timeout)`);
512
- const typecheck = await runPackageScript(options.workspaceRoot, "typecheck", scriptTimeoutMs);
1830
+ const typechecker = resolveTypechecker(options);
1831
+ printProgress(options, `verify: typecheck (${typechecker}, ${scriptTimeoutMs}ms timeout)`);
1832
+ const { result: typecheck, diagnostics: typecheckDiagnostics } = await runPreferredTypecheck(
1833
+ options,
1834
+ scripts,
1835
+ scriptTimeoutMs,
1836
+ );
513
1837
  steps.push({
514
1838
  name: "typecheck",
515
1839
  ok: typecheck.exitCode === 0,
@@ -519,6 +1843,7 @@ export async function runVerifyCommand(
519
1843
  timedOut: typecheck.timedOut,
520
1844
  failureKind: packageScriptFailureKind(typecheck),
521
1845
  });
1846
+ diagnostics.push(...typecheckDiagnostics);
522
1847
  if (typecheck.timedOut) {
523
1848
  diagnostics.push(timedOutDiagnostic("typecheck", scriptTimeoutMs));
524
1849
  } else if (typecheck.exitCode !== 0) {
@@ -537,7 +1862,70 @@ export async function runVerifyCommand(
537
1862
  const impact = await runStandardImpactTests(options);
538
1863
  steps.push(...impact.steps);
539
1864
  diagnostics.push(...impact.diagnostics);
1865
+ testCoverageReason = impact.testCoverageReason;
540
1866
  steps.push(skippedStep("tests", "--standard uses impact-selected tests; use --strict for the full test script"));
1867
+ } else if ((profile === "strict" || profile === "internal") && !options.fullTests) {
1868
+ if (!canRunInternalTests) {
1869
+ steps.push({
1870
+ name: "tests:framework-testgraph",
1871
+ ok: true,
1872
+ skipped: true,
1873
+ skipReason: "ForgeOS framework tests are maintainer-only; use forge verify framework or --internal",
1874
+ });
1875
+ diagnostics.push(
1876
+ createDiagnostic({
1877
+ severity: "warning",
1878
+ code: "FORGE_VERIFY_INTERNAL_TESTS_SKIPPED",
1879
+ message: "Skipped ForgeOS framework tests during app-level verify.",
1880
+ fixHint: "Run forge verify framework when maintaining ForgeOS itself. App projects still run their own TestGraph under forge verify --strict.",
1881
+ suggestedCommands: [
1882
+ "forge verify framework",
1883
+ "forge verify --internal",
1884
+ "forge verify --standard",
1885
+ ],
1886
+ }),
1887
+ );
1888
+ } else {
1889
+ printProgress(options, `verify: tests (strict TestGraph, ${scriptTimeoutMs}ms timeout)`);
1890
+ const tests = await runStrictGraphTests(options.workspaceRoot, scriptTimeoutMs, options.testJobs);
1891
+ testGraphPlan = tests.plan;
1892
+ steps.push({
1893
+ name: "tests:testgraph-strict",
1894
+ ok: tests.exitCode === 0,
1895
+ exitCode: tests.exitCode,
1896
+ command: tests.command,
1897
+ durationMs: tests.durationMs,
1898
+ timedOut: tests.timedOut,
1899
+ failureKind: packageScriptFailureKind(tests),
1900
+ });
1901
+ if (tests.timedOut) {
1902
+ diagnostics.push(timedOutDiagnostic("test", scriptTimeoutMs));
1903
+ } else if (tests.exitCode !== 0) {
1904
+ diagnostics.push(
1905
+ strictGraphFailureDiagnostic(tests),
1906
+ );
1907
+ }
1908
+ }
1909
+ } else if ((profile === "strict" || profile === "internal") && options.fullTests && !canRunInternalTests) {
1910
+ steps.push({
1911
+ name: "tests:framework-full",
1912
+ ok: true,
1913
+ skipped: true,
1914
+ skipReason: "ForgeOS framework package tests are maintainer-only; use forge verify framework --full or --internal --full",
1915
+ });
1916
+ diagnostics.push(
1917
+ createDiagnostic({
1918
+ severity: "warning",
1919
+ code: "FORGE_VERIFY_INTERNAL_TESTS_SKIPPED",
1920
+ message: "Skipped ForgeOS framework package tests during app-level verify.",
1921
+ fixHint: "Run forge verify framework --full when maintaining ForgeOS itself.",
1922
+ suggestedCommands: [
1923
+ "forge verify framework --full",
1924
+ "forge verify --internal --full",
1925
+ "forge verify --standard",
1926
+ ],
1927
+ }),
1928
+ );
541
1929
  } else if (!scripts.test) {
542
1930
  steps.push(skippedStep("tests", "no test script in package.json"));
543
1931
  } else {
@@ -581,6 +1969,8 @@ export async function runVerifyCommand(
581
1969
  profile,
582
1970
  steps,
583
1971
  diagnostics,
1972
+ testGraphPlan,
1973
+ testCoverageReason,
584
1974
  durationMs: Date.now() - started,
585
1975
  exitCode: ok ? 0 : 1,
586
1976
  };