forgeos 0.1.0-alpha.4 → 0.1.0-alpha.41

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