forgeos 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.npmignore +1 -0
- package/AGENTS.md +277 -0
- package/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +58 -0
- package/README.md +377 -0
- package/bin/forge-bun.mjs +110 -0
- package/bin/forge.mjs +19 -0
- package/package.json +96 -0
- package/packages/eslint-plugin-forge/index.ts +15 -0
- package/packages/eslint-plugin-forge/package.json +10 -0
- package/packages/eslint-plugin-forge/src/check-source.ts +95 -0
- package/packages/eslint-plugin-forge/src/load-artifacts.ts +24 -0
- package/packages/eslint-plugin-forge/src/rule-no-forge-guard-violation.ts +93 -0
- package/src/forge/_generated/actionSubscriptions.json +2 -0
- package/src/forge/_generated/actionSubscriptions.ts +10 -0
- package/src/forge/_generated/agentAdapterManifest.json +2 -0
- package/src/forge/_generated/agentAdapterManifest.ts +73 -0
- package/src/forge/_generated/agentContract.json +2 -0
- package/src/forge/_generated/agentContract.ts +912 -0
- package/src/forge/_generated/agentQuickstart.md +32 -0
- package/src/forge/_generated/aiContext.ts +59 -0
- package/src/forge/_generated/aiModels.json +2 -0
- package/src/forge/_generated/aiModels.ts +35 -0
- package/src/forge/_generated/aiProviders.json +2 -0
- package/src/forge/_generated/aiProviders.ts +23 -0
- package/src/forge/_generated/aiRegistry.json +2 -0
- package/src/forge/_generated/aiRegistry.ts +29 -0
- package/src/forge/_generated/api.json +2 -0
- package/src/forge/_generated/api.ts +8 -0
- package/src/forge/_generated/appGraph.json +2 -0
- package/src/forge/_generated/appGraph.ts +14511 -0
- package/src/forge/_generated/appMap.md +35 -0
- package/src/forge/_generated/artifactManifest.json +2 -0
- package/src/forge/_generated/artifactManifest.ts +7 -0
- package/src/forge/_generated/authClaims.json +2 -0
- package/src/forge/_generated/authClaims.ts +13 -0
- package/src/forge/_generated/authConfig.json +2 -0
- package/src/forge/_generated/authConfig.ts +17 -0
- package/src/forge/_generated/authContext.ts +23 -0
- package/src/forge/_generated/authRegistry.json +2 -0
- package/src/forge/_generated/authRegistry.ts +25 -0
- package/src/forge/_generated/buildInfo.json +2 -0
- package/src/forge/_generated/buildInfo.ts +9 -0
- package/src/forge/_generated/capabilityMap.json +2 -0
- package/src/forge/_generated/capabilityMap.md +15 -0
- package/src/forge/_generated/capabilityMap.ts +17 -0
- package/src/forge/_generated/client.ts +282 -0
- package/src/forge/_generated/clientApi.ts +9 -0
- package/src/forge/_generated/clientManifest.json +2 -0
- package/src/forge/_generated/clientManifest.ts +39 -0
- package/src/forge/_generated/clientTypes.ts +78 -0
- package/src/forge/_generated/configRegistry.json +2 -0
- package/src/forge/_generated/configRegistry.ts +4 -0
- package/src/forge/_generated/dataGraph.json +2 -0
- package/src/forge/_generated/dataGraph.ts +8 -0
- package/src/forge/_generated/db.json +2 -0
- package/src/forge/_generated/db.ts +2 -0
- package/src/forge/_generated/dbSecurityManifest.json +2 -0
- package/src/forge/_generated/dbSecurityManifest.ts +15 -0
- package/src/forge/_generated/dbSessionContext.json +2 -0
- package/src/forge/_generated/dbSessionContext.ts +39 -0
- package/src/forge/_generated/deployManifest.json +2 -0
- package/src/forge/_generated/deployManifest.ts +14 -0
- package/src/forge/_generated/devManifest.json +2 -0
- package/src/forge/_generated/devManifest.ts +47 -0
- package/src/forge/_generated/envSchema.json +2 -0
- package/src/forge/_generated/envSchema.ts +59 -0
- package/src/forge/_generated/frontendGraph.json +2 -0
- package/src/forge/_generated/frontendGraph.ts +27 -0
- package/src/forge/_generated/importGuards.json +2 -0
- package/src/forge/_generated/importGuards.ts +652 -0
- package/src/forge/_generated/index.ts +67 -0
- package/src/forge/_generated/liveProductionManifest.json +2 -0
- package/src/forge/_generated/liveProductionManifest.ts +23 -0
- package/src/forge/_generated/liveProtocol.json +2 -0
- package/src/forge/_generated/liveProtocol.ts +21 -0
- package/src/forge/_generated/liveQueryRegistry.json +2 -0
- package/src/forge/_generated/liveQueryRegistry.ts +9 -0
- package/src/forge/_generated/liveTransportConfig.json +2 -0
- package/src/forge/_generated/liveTransportConfig.ts +19 -0
- package/src/forge/_generated/makeRegistry.json +2 -0
- package/src/forge/_generated/makeRegistry.ts +163 -0
- package/src/forge/_generated/makeTemplates.json +2 -0
- package/src/forge/_generated/makeTemplates.ts +61 -0
- package/src/forge/_generated/mockMap.json +2 -0
- package/src/forge/_generated/mockMap.ts +7 -0
- package/src/forge/_generated/operationPlaybooks.md +145 -0
- package/src/forge/_generated/packageGraph.json +2 -0
- package/src/forge/_generated/packageGraph.ts +168569 -0
- package/src/forge/_generated/packageUpgradeRegistry.json +2 -0
- package/src/forge/_generated/packageUpgradeRegistry.ts +15 -0
- package/src/forge/_generated/permissionMatrix.json +2 -0
- package/src/forge/_generated/permissionMatrix.ts +7 -0
- package/src/forge/_generated/policyRegistry.json +2 -0
- package/src/forge/_generated/policyRegistry.ts +11 -0
- package/src/forge/_generated/queryRegistry.json +2 -0
- package/src/forge/_generated/queryRegistry.ts +9 -0
- package/src/forge/_generated/react.d.ts +22 -0
- package/src/forge/_generated/react.ts +29 -0
- package/src/forge/_generated/reactManifest.json +2 -0
- package/src/forge/_generated/reactManifest.ts +19 -0
- package/src/forge/_generated/releaseManifest.json +2 -0
- package/src/forge/_generated/releaseManifest.ts +25 -0
- package/src/forge/_generated/rlsPolicies.json +2 -0
- package/src/forge/_generated/rlsPolicies.sql +34 -0
- package/src/forge/_generated/rlsPolicies.ts +6 -0
- package/src/forge/_generated/runtimeGraph.json +2 -0
- package/src/forge/_generated/runtimeGraph.ts +8 -0
- package/src/forge/_generated/runtimeMatrix.json +2 -0
- package/src/forge/_generated/runtimeMatrix.ts +229125 -0
- package/src/forge/_generated/runtimeRegistry.ts +2 -0
- package/src/forge/_generated/runtimeRules.md +79 -0
- package/src/forge/_generated/secretRegistry.json +2 -0
- package/src/forge/_generated/secretRegistry.ts +50 -0
- package/src/forge/_generated/secretsContext.ts +11 -0
- package/src/forge/_generated/serverApi.ts +10 -0
- package/src/forge/_generated/sourceMapManifest.json +2 -0
- package/src/forge/_generated/sourceMapManifest.ts +7 -0
- package/src/forge/_generated/sqlPlan.json +2 -0
- package/src/forge/_generated/sqlPlan.ts +88 -0
- package/src/forge/_generated/subscriptionManifest.json +2 -0
- package/src/forge/_generated/subscriptionManifest.ts +7 -0
- package/src/forge/_generated/symbolicationManifest.json +2 -0
- package/src/forge/_generated/symbolicationManifest.ts +17 -0
- package/src/forge/_generated/telemetryRegistry.json +2 -0
- package/src/forge/_generated/telemetryRegistry.ts +9 -0
- package/src/forge/_generated/telemetrySinks.json +2 -0
- package/src/forge/_generated/telemetrySinks.ts +11 -0
- package/src/forge/_generated/tenantScope.json +2 -0
- package/src/forge/_generated/tenantScope.ts +8 -0
- package/src/forge/_generated/testGraph.json +2 -0
- package/src/forge/_generated/testGraph.ts +3054 -0
- package/src/forge/_generated/testPlanRegistry.json +2 -0
- package/src/forge/_generated/testPlanRegistry.ts +33 -0
- package/src/forge/_generated/uiRoutes.json +2 -0
- package/src/forge/_generated/uiRoutes.ts +16 -0
- package/src/forge/_generated/uiScenarios.json +2 -0
- package/src/forge/_generated/uiScenarios.ts +30 -0
- package/src/forge/_generated/uiTestManifest.json +2 -0
- package/src/forge/_generated/uiTestManifest.ts +27 -0
- package/src/forge/_generated/workflowRegistry.json +2 -0
- package/src/forge/_generated/workflowRegistry.ts +9 -0
- package/src/forge/_generated/workflowSubscriptions.json +2 -0
- package/src/forge/_generated/workflowSubscriptions.ts +10 -0
- package/src/forge/agent-adapters/index.ts +1002 -0
- package/src/forge/agent-adapters/types.ts +135 -0
- package/src/forge/cli/agent-contract.ts +50 -0
- package/src/forge/cli/ai.ts +148 -0
- package/src/forge/cli/auth.ts +198 -0
- package/src/forge/cli/build.ts +105 -0
- package/src/forge/cli/bun-exec.ts +4 -0
- package/src/forge/cli/commands.ts +1130 -0
- package/src/forge/cli/db.ts +316 -0
- package/src/forge/cli/deps.ts +277 -0
- package/src/forge/cli/dev.ts +529 -0
- package/src/forge/cli/doctor.ts +209 -0
- package/src/forge/cli/feature.ts +485 -0
- package/src/forge/cli/index.ts +25 -0
- package/src/forge/cli/lint-forge.ts +119 -0
- package/src/forge/cli/live.ts +179 -0
- package/src/forge/cli/main.ts +92 -0
- package/src/forge/cli/make.ts +133 -0
- package/src/forge/cli/new.ts +505 -0
- package/src/forge/cli/outbox.ts +297 -0
- package/src/forge/cli/output.ts +114 -0
- package/src/forge/cli/parse.ts +2211 -0
- package/src/forge/cli/policy.ts +204 -0
- package/src/forge/cli/query.ts +91 -0
- package/src/forge/cli/refactor.ts +221 -0
- package/src/forge/cli/release.ts +285 -0
- package/src/forge/cli/rls.ts +322 -0
- package/src/forge/cli/run.ts +76 -0
- package/src/forge/cli/secrets.ts +274 -0
- package/src/forge/cli/self-host.ts +468 -0
- package/src/forge/cli/serve.ts +93 -0
- package/src/forge/cli/telemetry.ts +219 -0
- package/src/forge/cli/verify.ts +587 -0
- package/src/forge/cli/version.ts +1 -0
- package/src/forge/cli/windows.ts +413 -0
- package/src/forge/cli/worker.ts +87 -0
- package/src/forge/cli/workflow.ts +424 -0
- package/src/forge/compiler/action-subscriptions/build.ts +116 -0
- package/src/forge/compiler/action-subscriptions/constants.ts +2 -0
- package/src/forge/compiler/action-subscriptions/index.ts +6 -0
- package/src/forge/compiler/action-subscriptions/parse.ts +6 -0
- package/src/forge/compiler/agent-contract/build.ts +1651 -0
- package/src/forge/compiler/agent-contract/types.ts +326 -0
- package/src/forge/compiler/ai-registry/build.ts +165 -0
- package/src/forge/compiler/ai-registry/constants.ts +2 -0
- package/src/forge/compiler/ai-registry/parse.ts +56 -0
- package/src/forge/compiler/api-surface/build.ts +107 -0
- package/src/forge/compiler/app-graph/build.ts +121 -0
- package/src/forge/compiler/app-graph/classify.ts +10 -0
- package/src/forge/compiler/app-graph/dup-symbol.ts +29 -0
- package/src/forge/compiler/app-graph/extract.ts +124 -0
- package/src/forge/compiler/app-graph/forge-apis.ts +29 -0
- package/src/forge/compiler/app-graph/index.ts +15 -0
- package/src/forge/compiler/app-graph/module-graph.ts +320 -0
- package/src/forge/compiler/app-graph/parser.ts +119 -0
- package/src/forge/compiler/app-graph/symbols.ts +48 -0
- package/src/forge/compiler/app-graph/tsconfig-hash.ts +62 -0
- package/src/forge/compiler/app-graph/types.ts +43 -0
- package/src/forge/compiler/app-graph/versions.ts +14 -0
- package/src/forge/compiler/cache/index.ts +17 -0
- package/src/forge/compiler/cache/key.ts +46 -0
- package/src/forge/compiler/cache/scheduler.ts +72 -0
- package/src/forge/compiler/cache/store.ts +78 -0
- package/src/forge/compiler/classifier/capabilities.ts +78 -0
- package/src/forge/compiler/classifier/classify.ts +113 -0
- package/src/forge/compiler/classifier/contexts.ts +188 -0
- package/src/forge/compiler/classifier/index.ts +18 -0
- package/src/forge/compiler/classifier/runtime-matrix.ts +45 -0
- package/src/forge/compiler/classifier/secrets.ts +41 -0
- package/src/forge/compiler/classifier/signals.ts +129 -0
- package/src/forge/compiler/client-sdk/build-manifest.ts +151 -0
- package/src/forge/compiler/client-sdk/render-client.ts +432 -0
- package/src/forge/compiler/data-graph/build.ts +131 -0
- package/src/forge/compiler/data-graph/constants.ts +5 -0
- package/src/forge/compiler/data-graph/index.ts +6 -0
- package/src/forge/compiler/data-graph/parse.ts +176 -0
- package/src/forge/compiler/data-graph/rls/build.ts +222 -0
- package/src/forge/compiler/data-graph/rls/types.ts +62 -0
- package/src/forge/compiler/data-graph/sql/ddl.ts +390 -0
- package/src/forge/compiler/data-graph/sql/naming.ts +10 -0
- package/src/forge/compiler/data-graph/sql/serialize.ts +85 -0
- package/src/forge/compiler/data-graph/sql/types.ts +37 -0
- package/src/forge/compiler/dev-manifest/build.ts +170 -0
- package/src/forge/compiler/dev-manifest/constants.ts +5 -0
- package/src/forge/compiler/diagnostics/codes.ts +611 -0
- package/src/forge/compiler/diagnostics/create.ts +245 -0
- package/src/forge/compiler/diagnostics/index.ts +55 -0
- package/src/forge/compiler/emitter/artifact-kind.ts +14 -0
- package/src/forge/compiler/emitter/barrel.ts +44 -0
- package/src/forge/compiler/emitter/constants.ts +7 -0
- package/src/forge/compiler/emitter/emit.ts +237 -0
- package/src/forge/compiler/emitter/index.ts +24 -0
- package/src/forge/compiler/emitter/lock.ts +62 -0
- package/src/forge/compiler/emitter/render.ts +73 -0
- package/src/forge/compiler/emitter/write.ts +35 -0
- package/src/forge/compiler/frontend-graph/build.ts +495 -0
- package/src/forge/compiler/fs/index.ts +23 -0
- package/src/forge/compiler/fs/memory.ts +233 -0
- package/src/forge/compiler/fs/node.ts +139 -0
- package/src/forge/compiler/fs/profile.ts +108 -0
- package/src/forge/compiler/fs/types.ts +52 -0
- package/src/forge/compiler/guards/artifacts.ts +96 -0
- package/src/forge/compiler/guards/check-ai-usage.ts +98 -0
- package/src/forge/compiler/guards/check-import-guards.ts +106 -0
- package/src/forge/compiler/guards/check-process-env.ts +98 -0
- package/src/forge/compiler/guards/check-query-usage.ts +76 -0
- package/src/forge/compiler/guards/index.ts +11 -0
- package/src/forge/compiler/guards/propagate-contexts.ts +57 -0
- package/src/forge/compiler/index.ts +17 -0
- package/src/forge/compiler/integration/add.ts +496 -0
- package/src/forge/compiler/integration/index.ts +17 -0
- package/src/forge/compiler/integration/plan.ts +283 -0
- package/src/forge/compiler/integration/render.ts +189 -0
- package/src/forge/compiler/integration/snapshot.ts +52 -0
- package/src/forge/compiler/integration/templates/ai.ts +131 -0
- package/src/forge/compiler/integration/templates/index.ts +8 -0
- package/src/forge/compiler/integration/templates/posthog.ts +145 -0
- package/src/forge/compiler/integration/templates/render.ts +113 -0
- package/src/forge/compiler/integration/templates/sentry.ts +151 -0
- package/src/forge/compiler/integration/templates/stripe.ts +109 -0
- package/src/forge/compiler/integration/templates/types.ts +14 -0
- package/src/forge/compiler/integration/templates/zod.ts +55 -0
- package/src/forge/compiler/live-production/types.ts +122 -0
- package/src/forge/compiler/live-query-registry/build.ts +150 -0
- package/src/forge/compiler/live-query-registry/constants.ts +2 -0
- package/src/forge/compiler/make-registry/build.ts +179 -0
- package/src/forge/compiler/orchestrator/discover.ts +214 -0
- package/src/forge/compiler/orchestrator/fast-check.ts +117 -0
- package/src/forge/compiler/orchestrator/generate-lock.ts +138 -0
- package/src/forge/compiler/orchestrator/guards.ts +5 -0
- package/src/forge/compiler/orchestrator/index.ts +27 -0
- package/src/forge/compiler/orchestrator/manifest-hashes.ts +21 -0
- package/src/forge/compiler/orchestrator/manifest.ts +92 -0
- package/src/forge/compiler/orchestrator/orphans.ts +51 -0
- package/src/forge/compiler/orchestrator/plan.ts +876 -0
- package/src/forge/compiler/orchestrator/profile.ts +36 -0
- package/src/forge/compiler/orchestrator/run.ts +277 -0
- package/src/forge/compiler/orchestrator/serialize.ts +886 -0
- package/src/forge/compiler/orchestrator/session.ts +96 -0
- package/src/forge/compiler/orchestrator/types.ts +31 -0
- package/src/forge/compiler/orchestrator/verify.ts +38 -0
- package/src/forge/compiler/orchestrator/workspace-index.ts +154 -0
- package/src/forge/compiler/package-graph/capabilities-stub.ts +33 -0
- package/src/forge/compiler/package-graph/checksum.ts +97 -0
- package/src/forge/compiler/package-graph/compiler.ts +392 -0
- package/src/forge/compiler/package-graph/constants.ts +4 -0
- package/src/forge/compiler/package-graph/dts-extractor.ts +142 -0
- package/src/forge/compiler/package-graph/exports-discovery.ts +84 -0
- package/src/forge/compiler/package-graph/extract-dts.ts +32 -0
- package/src/forge/compiler/package-graph/index.ts +33 -0
- package/src/forge/compiler/package-graph/jsdoc.ts +62 -0
- package/src/forge/compiler/package-graph/read-file.ts +21 -0
- package/src/forge/compiler/package-graph/resolve.ts +127 -0
- package/src/forge/compiler/package-manager/adapter.ts +237 -0
- package/src/forge/compiler/package-manager/bun-executable.ts +92 -0
- package/src/forge/compiler/package-manager/commands.ts +47 -0
- package/src/forge/compiler/package-manager/detect.ts +79 -0
- package/src/forge/compiler/package-manager/executor.ts +117 -0
- package/src/forge/compiler/package-manager/index.ts +22 -0
- package/src/forge/compiler/package-manager/parse-spec.ts +16 -0
- package/src/forge/compiler/package-manager/version.ts +27 -0
- package/src/forge/compiler/package-upgrades/apply.ts +195 -0
- package/src/forge/compiler/package-upgrades/comparator.ts +181 -0
- package/src/forge/compiler/package-upgrades/impact.ts +139 -0
- package/src/forge/compiler/package-upgrades/markdown.ts +97 -0
- package/src/forge/compiler/package-upgrades/planner.ts +532 -0
- package/src/forge/compiler/package-upgrades/risk.ts +208 -0
- package/src/forge/compiler/package-upgrades/types.ts +174 -0
- package/src/forge/compiler/policy-registry/build.ts +266 -0
- package/src/forge/compiler/policy-registry/constants.ts +2 -0
- package/src/forge/compiler/policy-registry/parse.ts +81 -0
- package/src/forge/compiler/primitives/compare.ts +26 -0
- package/src/forge/compiler/primitives/hash.ts +40 -0
- package/src/forge/compiler/primitives/header.ts +45 -0
- package/src/forge/compiler/primitives/index.ts +45 -0
- package/src/forge/compiler/primitives/paths.ts +24 -0
- package/src/forge/compiler/primitives/result.ts +164 -0
- package/src/forge/compiler/primitives/serialize.ts +66 -0
- package/src/forge/compiler/primitives/sort.ts +87 -0
- package/src/forge/compiler/query-registry/build.ts +114 -0
- package/src/forge/compiler/query-registry/constants.ts +2 -0
- package/src/forge/compiler/recipes/definitions.ts +289 -0
- package/src/forge/compiler/recipes/helpers.ts +37 -0
- package/src/forge/compiler/recipes/index.ts +21 -0
- package/src/forge/compiler/recipes/registry.ts +102 -0
- package/src/forge/compiler/release/build.ts +100 -0
- package/src/forge/compiler/release/types.ts +119 -0
- package/src/forge/compiler/runtime-graph/build.ts +137 -0
- package/src/forge/compiler/runtime-graph/constants.ts +5 -0
- package/src/forge/compiler/runtime-graph/index.ts +5 -0
- package/src/forge/compiler/sandbox/artifact-sanitize.ts +26 -0
- package/src/forge/compiler/sandbox/backends/child.ts +123 -0
- package/src/forge/compiler/sandbox/backends/docker.ts +173 -0
- package/src/forge/compiler/sandbox/index.ts +51 -0
- package/src/forge/compiler/sandbox/inspect.ts +143 -0
- package/src/forge/compiler/sandbox/inspector-entry.ts +115 -0
- package/src/forge/compiler/sandbox/limits.ts +31 -0
- package/src/forge/compiler/sandbox/scrub-env.ts +60 -0
- package/src/forge/compiler/sandbox/secret-scan.ts +54 -0
- package/src/forge/compiler/sandbox/serialize.ts +106 -0
- package/src/forge/compiler/sandbox/types.ts +7 -0
- package/src/forge/compiler/secret-registry/build.ts +123 -0
- package/src/forge/compiler/telemetry-registry/build.ts +89 -0
- package/src/forge/compiler/telemetry-registry/constants.ts +2 -0
- package/src/forge/compiler/telemetry-registry/parse.ts +13 -0
- package/src/forge/compiler/test-graph/build.ts +277 -0
- package/src/forge/compiler/types/action-subscriptions.ts +19 -0
- package/src/forge/compiler/types/ai-registry.ts +33 -0
- package/src/forge/compiler/types/app-graph.ts +80 -0
- package/src/forge/compiler/types/capability.ts +29 -0
- package/src/forge/compiler/types/classification.ts +9 -0
- package/src/forge/compiler/types/cli.ts +159 -0
- package/src/forge/compiler/types/data-graph.ts +24 -0
- package/src/forge/compiler/types/dev-manifest.ts +41 -0
- package/src/forge/compiler/types/diagnostic.ts +12 -0
- package/src/forge/compiler/types/emit.ts +25 -0
- package/src/forge/compiler/types/frontend-graph.ts +81 -0
- package/src/forge/compiler/types/import-guards.ts +19 -0
- package/src/forge/compiler/types/index.ts +98 -0
- package/src/forge/compiler/types/integration.ts +25 -0
- package/src/forge/compiler/types/json.ts +3 -0
- package/src/forge/compiler/types/live-query-registry.ts +32 -0
- package/src/forge/compiler/types/lock.ts +37 -0
- package/src/forge/compiler/types/package-graph.ts +84 -0
- package/src/forge/compiler/types/policy-registry.ts +69 -0
- package/src/forge/compiler/types/query-registry.ts +18 -0
- package/src/forge/compiler/types/runtime-graph.ts +30 -0
- package/src/forge/compiler/types/runtime-matrix.ts +16 -0
- package/src/forge/compiler/types/runtime.ts +30 -0
- package/src/forge/compiler/types/sandbox.ts +24 -0
- package/src/forge/compiler/types/secret-registry.ts +38 -0
- package/src/forge/compiler/types/telemetry-registry.ts +26 -0
- package/src/forge/compiler/types/test-graph.ts +45 -0
- package/src/forge/compiler/types/workflow-registry.ts +42 -0
- package/src/forge/compiler/workflow-registry/build.ts +180 -0
- package/src/forge/compiler/workflow-registry/constants.ts +2 -0
- package/src/forge/compiler/workflow-registry/index.ts +5 -0
- package/src/forge/compiler/workflow-registry/parse.ts +19 -0
- package/src/forge/dev/server.ts +1379 -0
- package/src/forge/dev/types.ts +49 -0
- package/src/forge/dev/watch.ts +109 -0
- package/src/forge/dev-console/cycle.ts +652 -0
- package/src/forge/dev-console/types.ts +99 -0
- package/src/forge/feature/compiler.ts +656 -0
- package/src/forge/feature/examples.ts +125 -0
- package/src/forge/feature/types.ts +177 -0
- package/src/forge/impact/index.ts +1160 -0
- package/src/forge/impact/types.ts +151 -0
- package/src/forge/intent/index.ts +490 -0
- package/src/forge/intent/types.ts +73 -0
- package/src/forge/make/fields.ts +146 -0
- package/src/forge/make/index.ts +1101 -0
- package/src/forge/make/naming.ts +42 -0
- package/src/forge/make/templates.ts +525 -0
- package/src/forge/make/types.ts +151 -0
- package/src/forge/platform/module.ts +20 -0
- package/src/forge/policy.ts +1 -0
- package/src/forge/react/index.ts +418 -0
- package/src/forge/refactor/index.ts +1936 -0
- package/src/forge/refactor/text-utils.ts +34 -0
- package/src/forge/refactor/types.ts +191 -0
- package/src/forge/refactor/workspace-fs.ts +171 -0
- package/src/forge/repair/index.ts +656 -0
- package/src/forge/repair/rules/index.ts +476 -0
- package/src/forge/repair/types.ts +175 -0
- package/src/forge/review/index.ts +992 -0
- package/src/forge/review/types.ts +196 -0
- package/src/forge/runtime/ai/check.ts +86 -0
- package/src/forge/runtime/ai/context.ts +394 -0
- package/src/forge/runtime/ai/cost-estimator.ts +41 -0
- package/src/forge/runtime/ai/mock.ts +49 -0
- package/src/forge/runtime/ai/providers.ts +78 -0
- package/src/forge/runtime/ai/state.ts +17 -0
- package/src/forge/runtime/ai/types.ts +67 -0
- package/src/forge/runtime/auth/authenticate.ts +58 -0
- package/src/forge/runtime/auth/claims.ts +119 -0
- package/src/forge/runtime/auth/config.ts +148 -0
- package/src/forge/runtime/auth/errors.ts +45 -0
- package/src/forge/runtime/auth/evaluate.ts +126 -0
- package/src/forge/runtime/auth/resolve.ts +74 -0
- package/src/forge/runtime/auth/types.ts +87 -0
- package/src/forge/runtime/auth/verifier.ts +138 -0
- package/src/forge/runtime/context/create-context.ts +204 -0
- package/src/forge/runtime/context/create-query-context.ts +34 -0
- package/src/forge/runtime/db/adapter.ts +31 -0
- package/src/forge/runtime/db/factory.ts +83 -0
- package/src/forge/runtime/db/generated-client.ts +294 -0
- package/src/forge/runtime/db/memory-adapter.ts +706 -0
- package/src/forge/runtime/db/migrate.ts +132 -0
- package/src/forge/runtime/db/outbox.ts +54 -0
- package/src/forge/runtime/db/pglite-adapter.ts +51 -0
- package/src/forge/runtime/db/postgres-adapter.ts +112 -0
- package/src/forge/runtime/db/read-only-client.ts +97 -0
- package/src/forge/runtime/db/session-context.ts +62 -0
- package/src/forge/runtime/executor.ts +446 -0
- package/src/forge/runtime/live/dependency-tracker.ts +57 -0
- package/src/forge/runtime/live/invalidation-log.ts +189 -0
- package/src/forge/runtime/live/live-query-runner.ts +267 -0
- package/src/forge/runtime/live/registry.ts +28 -0
- package/src/forge/runtime/live/sse.ts +75 -0
- package/src/forge/runtime/live/subscription-manager.ts +443 -0
- package/src/forge/runtime/live/types.ts +143 -0
- package/src/forge/runtime/outbox/claim.ts +153 -0
- package/src/forge/runtime/outbox/process.ts +298 -0
- package/src/forge/runtime/outbox/retry.ts +8 -0
- package/src/forge/runtime/outbox/subscriptions.ts +33 -0
- package/src/forge/runtime/outbox/types.ts +69 -0
- package/src/forge/runtime/policy/check.ts +157 -0
- package/src/forge/runtime/policy/load.ts +55 -0
- package/src/forge/runtime/query/registry.ts +19 -0
- package/src/forge/runtime/query/run-query.ts +347 -0
- package/src/forge/runtime/release/runtime.ts +322 -0
- package/src/forge/runtime/release/symbolicate.ts +175 -0
- package/src/forge/runtime/runner/command-transaction.ts +193 -0
- package/src/forge/runtime/runner/run-entry.ts +226 -0
- package/src/forge/runtime/secrets/check.ts +78 -0
- package/src/forge/runtime/secrets/create-context.ts +138 -0
- package/src/forge/runtime/secrets/env-loader.ts +94 -0
- package/src/forge/runtime/secrets/runtime-bundle.ts +47 -0
- package/src/forge/runtime/secrets/types.ts +31 -0
- package/src/forge/runtime/telemetry/buffer.ts +87 -0
- package/src/forge/runtime/telemetry/context.ts +192 -0
- package/src/forge/runtime/telemetry/correlation.ts +13 -0
- package/src/forge/runtime/telemetry/flush.ts +190 -0
- package/src/forge/runtime/telemetry/process.ts +20 -0
- package/src/forge/runtime/telemetry/scrubber.ts +115 -0
- package/src/forge/runtime/telemetry/sinks/local-jsonl.ts +39 -0
- package/src/forge/runtime/telemetry/sinks/posthog.ts +64 -0
- package/src/forge/runtime/telemetry/sinks/sentry.ts +60 -0
- package/src/forge/runtime/telemetry/spans.ts +58 -0
- package/src/forge/runtime/telemetry/types.ts +64 -0
- package/src/forge/runtime/workflows/cancel.ts +26 -0
- package/src/forge/runtime/workflows/create-run.ts +98 -0
- package/src/forge/runtime/workflows/process-run.ts +182 -0
- package/src/forge/runtime/workflows/process-step.ts +190 -0
- package/src/forge/runtime/workflows/process.ts +260 -0
- package/src/forge/runtime/workflows/registry.ts +51 -0
- package/src/forge/runtime/workflows/resolve-step.ts +46 -0
- package/src/forge/runtime/workflows/retry-run.ts +44 -0
- package/src/forge/runtime/workflows/retry.ts +8 -0
- package/src/forge/runtime/workflows/sanitize.ts +19 -0
- package/src/forge/runtime/workflows/start-from-outbox.ts +71 -0
- package/src/forge/runtime/workflows/types.ts +77 -0
- package/src/forge/server.ts +96 -0
- package/src/forge/ui/index.ts +770 -0
- package/src/forge/ui/types.ts +191 -0
- package/templates/b2b-support-web/.env.example +22 -0
- package/templates/b2b-support-web/.vscode/settings.json +14 -0
- package/templates/b2b-support-web/AGENTS.md +108 -0
- package/templates/b2b-support-web/README.md +48 -0
- package/templates/b2b-support-web/forge.config.ts +3 -0
- package/templates/b2b-support-web/package.json +34 -0
- package/templates/b2b-support-web/src/actions/captureTicketCreated.ts +14 -0
- package/templates/b2b-support-web/src/commands/closeTicket.ts +20 -0
- package/templates/b2b-support-web/src/commands/createTicket.ts +47 -0
- package/templates/b2b-support-web/src/commands/manageBilling.ts +9 -0
- package/templates/b2b-support-web/src/forge/schema.ts +35 -0
- package/templates/b2b-support-web/src/policies.ts +9 -0
- package/templates/b2b-support-web/src/queries/getTicket.ts +6 -0
- package/templates/b2b-support-web/src/queries/listTickets.ts +6 -0
- package/templates/b2b-support-web/src/queries/liveTickets.ts +9 -0
- package/templates/b2b-support-web/src/workflows/triageTicketWorkflow.ts +64 -0
- package/templates/b2b-support-web/tsconfig.json +14 -0
- package/templates/b2b-support-web/web/app/globals.css +77 -0
- package/templates/b2b-support-web/web/app/layout.tsx +13 -0
- package/templates/b2b-support-web/web/app/page.tsx +13 -0
- package/templates/b2b-support-web/web/app/providers.tsx +21 -0
- package/templates/b2b-support-web/web/app/tickets/page.tsx +21 -0
- package/templates/b2b-support-web/web/components/CreateTicketForm.tsx +43 -0
- package/templates/b2b-support-web/web/components/PolicyDeniedDemo.tsx +31 -0
- package/templates/b2b-support-web/web/components/TicketList.tsx +52 -0
- package/templates/b2b-support-web/web/components/TraceDetails.tsx +18 -0
- package/templates/b2b-support-web/web/components/TriageStatus.tsx +13 -0
- package/templates/b2b-support-web/web/lib/forge.ts +13 -0
- package/templates/b2b-support-web/web/next-env.d.ts +5 -0
- package/templates/b2b-support-web/web/next.config.ts +8 -0
- package/templates/b2b-support-web/web/package.json +21 -0
- package/templates/b2b-support-web/web/tsconfig.json +30 -0
- package/templates/minimal-web/.vscode/settings.json +14 -0
- package/templates/minimal-web/README.md +21 -0
- package/templates/minimal-web/forge.config.ts +3 -0
- package/templates/minimal-web/package.json +32 -0
- package/templates/minimal-web/src/actions/logNoteCreated.ts +11 -0
- package/templates/minimal-web/src/commands/createNote.ts +26 -0
- package/templates/minimal-web/src/forge/schema.ts +12 -0
- package/templates/minimal-web/src/policies.ts +6 -0
- package/templates/minimal-web/src/queries/listNotes.ts +8 -0
- package/templates/minimal-web/src/queries/liveNotes.ts +8 -0
- package/templates/minimal-web/tsconfig.json +15 -0
- package/templates/minimal-web/web/index.html +12 -0
- package/templates/minimal-web/web/package.json +21 -0
- package/templates/minimal-web/web/src/App.tsx +89 -0
- package/templates/minimal-web/web/src/lib/forge.ts +13 -0
- package/templates/minimal-web/web/src/main.tsx +13 -0
- package/templates/minimal-web/web/src/styles.css +156 -0
- package/templates/minimal-web/web/tsconfig.json +18 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
import { basename, join } from "node:path";
|
|
2
|
+
import { nodeFileSystem } from "../compiler/fs/index.ts";
|
|
3
|
+
import { createDiagnostic } from "../compiler/diagnostics/create.ts";
|
|
4
|
+
import { hashStable } from "../compiler/primitives/hash.ts";
|
|
5
|
+
import { serializeCanonical } from "../compiler/primitives/serialize.ts";
|
|
6
|
+
import { stripDeterministicHeader } from "../compiler/primitives/header.ts";
|
|
7
|
+
import { analyzeImpact, buildImpactTestPlan, detectChangedFiles } from "../impact/index.ts";
|
|
8
|
+
import type { ImpactCommandOptions, ImpactSource } from "../impact/types.ts";
|
|
9
|
+
import type {
|
|
10
|
+
ReviewChanged,
|
|
11
|
+
ReviewCommandOptions,
|
|
12
|
+
ReviewContext,
|
|
13
|
+
ReviewFailOn,
|
|
14
|
+
ReviewFinding,
|
|
15
|
+
ReviewFindingCategory,
|
|
16
|
+
ReviewFindingSeverity,
|
|
17
|
+
ReviewReport,
|
|
18
|
+
ReviewResult,
|
|
19
|
+
ReviewRisk,
|
|
20
|
+
ReviewRuleDoc,
|
|
21
|
+
ReviewSource,
|
|
22
|
+
ReviewWriteResult,
|
|
23
|
+
} from "./types.ts";
|
|
24
|
+
|
|
25
|
+
const REVIEW_VERSION = "review-0.1.0";
|
|
26
|
+
const GENERATED = "src/forge/_generated";
|
|
27
|
+
const REVIEW_DIR = ".forge/reviews";
|
|
28
|
+
|
|
29
|
+
const ALL_CATEGORIES: ReviewFindingCategory[] = [
|
|
30
|
+
"runtime",
|
|
31
|
+
"data",
|
|
32
|
+
"policy",
|
|
33
|
+
"secrets",
|
|
34
|
+
"package",
|
|
35
|
+
"workflow",
|
|
36
|
+
"livequery",
|
|
37
|
+
"frontend",
|
|
38
|
+
"test",
|
|
39
|
+
"deploy",
|
|
40
|
+
"release",
|
|
41
|
+
"agent",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const RULE_DOCS: ReviewRuleDoc[] = [
|
|
45
|
+
{
|
|
46
|
+
id: "runtime-command-forbidden-import",
|
|
47
|
+
category: "runtime",
|
|
48
|
+
title: "Command imports side-effect package",
|
|
49
|
+
description: "Commands must stay deterministic and transactional; external network/integration packages belong in actions or workflows.",
|
|
50
|
+
typicalFix: ["Move the side effect to an action or workflow.", "Emit an event from the command with ctx.emit."],
|
|
51
|
+
relatedCommands: ["forge refactor extract-action", "forge make action", "forge repair diagnose"],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "secret-env-example-missing",
|
|
55
|
+
category: "secrets",
|
|
56
|
+
title: "Secret missing from .env.example",
|
|
57
|
+
description: "New required secrets should be documented by name without leaking values.",
|
|
58
|
+
typicalFix: ["Add the secret name to .env.example.", "Run forge secrets check."],
|
|
59
|
+
relatedCommands: ["forge secrets check", "forge refactor replace-process-env <ENV_VAR>"],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "frontend-server-import",
|
|
63
|
+
category: "frontend",
|
|
64
|
+
title: "Frontend imports server-only surface",
|
|
65
|
+
description: "Browser code must use generated client/react surfaces instead of server-only adapters.",
|
|
66
|
+
typicalFix: ["Use src/forge/_generated/react hooks.", "Use the client-safe generated API."],
|
|
67
|
+
relatedCommands: ["forge agent-contract check", "forge test plan --changed"],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "test-high-risk-untested",
|
|
71
|
+
category: "test",
|
|
72
|
+
title: "High risk change lacks targeted test evidence",
|
|
73
|
+
description: "High impact changes should include changed tests or a recent H28 test plan/run record.",
|
|
74
|
+
typicalFix: ["Run forge test plan --changed --write.", "Run forge verify --strict before merge."],
|
|
75
|
+
relatedCommands: ["forge test plan --changed", "forge test run --changed", "forge verify --strict"],
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
function normalize(path: string): string {
|
|
80
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readText(workspaceRoot: string, relative: string): string {
|
|
84
|
+
try {
|
|
85
|
+
return nodeFileSystem.readText(join(workspaceRoot, relative)) ?? "";
|
|
86
|
+
} catch {
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readJson<T>(workspaceRoot: string, relative: string, fallback: T): T {
|
|
92
|
+
const raw = readText(workspaceRoot, relative);
|
|
93
|
+
if (!raw) return fallback;
|
|
94
|
+
return JSON.parse(stripDeterministicHeader(raw)) as T;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sourceFromOptions(options: ReviewCommandOptions): ReviewSource {
|
|
98
|
+
if (options.staged) return { kind: "staged" };
|
|
99
|
+
if (options.base) return { kind: "base", base: options.base, head: "HEAD" };
|
|
100
|
+
if (options.featureId) return { kind: "feature", featureId: options.featureId };
|
|
101
|
+
if (options.refactorId) return { kind: "refactor", planId: options.refactorId };
|
|
102
|
+
if (options.upgradeId) return { kind: "upgrade", planId: options.upgradeId };
|
|
103
|
+
if (options.releaseId) return { kind: "release", releaseId: options.releaseId };
|
|
104
|
+
return { kind: "changed" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function impactSourceFromReview(source: ReviewSource): ImpactSource {
|
|
108
|
+
if (source.kind === "staged") return { mode: "staged", base: "index" };
|
|
109
|
+
if (source.kind === "base") return { mode: "since", base: source.base };
|
|
110
|
+
if (source.kind === "feature") return { mode: "feature", id: source.featureId };
|
|
111
|
+
if (source.kind === "refactor") return { mode: "refactor", id: source.planId };
|
|
112
|
+
if (source.kind === "upgrade") return { mode: "upgrade", id: source.planId };
|
|
113
|
+
return { mode: "changed", base: "HEAD" };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function changedFromFiles(files: string[]): ReviewChanged {
|
|
117
|
+
return {
|
|
118
|
+
files,
|
|
119
|
+
tests: files.filter((file) => /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(file)).sort(),
|
|
120
|
+
sourceFiles: files.filter((file) => /\.(ts|tsx|js|jsx)$/.test(file) && !/\.(test|spec)\./.test(file)).sort(),
|
|
121
|
+
generated: files.filter((file) => file.startsWith(`${GENERATED}/`)).sort(),
|
|
122
|
+
packageFiles: files.filter((file) => ["package.json", "bun.lock", "bun.lockb", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"].includes(file) || file.endsWith("/package.json")).sort(),
|
|
123
|
+
deployFiles: files.filter((file) => /(^|\/)(Dockerfile|docker-compose\.ya?ml)$/.test(file) || file.includes("/deploy/") || file.startsWith("deploy/")).sort(),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function loadContext(options: ReviewCommandOptions): ReviewContext {
|
|
128
|
+
const source = sourceFromOptions(options);
|
|
129
|
+
const impactSource = impactSourceFromReview(source);
|
|
130
|
+
const detected = detectChangedFiles(options.workspaceRoot, impactSource);
|
|
131
|
+
const changed = changedFromFiles(detected.files);
|
|
132
|
+
const impact = analyzeImpact({
|
|
133
|
+
workspaceRoot: options.workspaceRoot,
|
|
134
|
+
json: options.json,
|
|
135
|
+
write: false,
|
|
136
|
+
changed: impactSource.mode === "changed",
|
|
137
|
+
staged: impactSource.mode === "staged",
|
|
138
|
+
since: impactSource.mode === "since" ? impactSource.base : undefined,
|
|
139
|
+
featureId: impactSource.mode === "feature" ? impactSource.id : undefined,
|
|
140
|
+
refactorId: impactSource.mode === "refactor" ? impactSource.id : undefined,
|
|
141
|
+
upgradeId: impactSource.mode === "upgrade" ? impactSource.id : undefined,
|
|
142
|
+
includeGenerated: false,
|
|
143
|
+
excludeTests: false,
|
|
144
|
+
} satisfies ImpactCommandOptions);
|
|
145
|
+
const fileTexts = new Map<string, string>();
|
|
146
|
+
for (const file of changed.files) fileTexts.set(file, readText(options.workspaceRoot, file));
|
|
147
|
+
return {
|
|
148
|
+
workspaceRoot: options.workspaceRoot,
|
|
149
|
+
source,
|
|
150
|
+
impactSource,
|
|
151
|
+
changed,
|
|
152
|
+
impacted: impact.impacted,
|
|
153
|
+
fileTexts,
|
|
154
|
+
generated: {
|
|
155
|
+
actionSubscriptions: readJson(options.workspaceRoot, `${GENERATED}/actionSubscriptions.json`, {}),
|
|
156
|
+
workflowSubscriptions: readJson(options.workspaceRoot, `${GENERATED}/workflowSubscriptions.json`, {}),
|
|
157
|
+
policyRegistry: readJson(options.workspaceRoot, `${GENERATED}/policyRegistry.json`, {}),
|
|
158
|
+
secretRegistry: readJson(options.workspaceRoot, `${GENERATED}/secretRegistry.json`, {}),
|
|
159
|
+
agentContract: nodeFileSystem.exists(join(options.workspaceRoot, `${GENERATED}/agentContract.json`))
|
|
160
|
+
? readJson(options.workspaceRoot, `${GENERATED}/agentContract.json`, null)
|
|
161
|
+
: null,
|
|
162
|
+
},
|
|
163
|
+
envExample: readText(options.workspaceRoot, ".env.example"),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function finding(input: Omit<ReviewFinding, "id">): ReviewFinding {
|
|
168
|
+
const id = `${input.category}_${hashStable(`${input.code}:${input.file ?? ""}:${input.message}`).slice(0, 10)}`;
|
|
169
|
+
return { id, ...input };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function basenameNoExt(file: string): string {
|
|
173
|
+
return basename(file).replace(/\.(test|spec)?\.?(ts|tsx|js|jsx|json|md|sql|yml|yaml)$/, "");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isCommandFile(file: string): boolean {
|
|
177
|
+
return normalize(file).includes("/commands/") && /\.(ts|tsx|js|jsx)$/.test(file);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isQueryFile(file: string): boolean {
|
|
181
|
+
return normalize(file).includes("/queries/") && /\.(ts|tsx|js|jsx)$/.test(file);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isLiveQueryFile(file: string): boolean {
|
|
185
|
+
const normalized = normalize(file).toLowerCase();
|
|
186
|
+
return isQueryFile(file) && (normalized.includes("live") || normalized.includes("livequery"));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isTestFile(file: string): boolean {
|
|
190
|
+
return /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(file) || normalize(file).startsWith("tests/");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isForgeToolingFile(file: string): boolean {
|
|
194
|
+
const normalized = normalize(file);
|
|
195
|
+
return normalized.startsWith("src/forge/cli/") || normalized.startsWith("src/forge/review/");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function includesCategory(options: ReviewCommandOptions, category: ReviewFindingCategory): boolean {
|
|
199
|
+
if (options.exclude.includes(category)) return false;
|
|
200
|
+
return options.include.length === 0 || options.include.includes(category);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function secretNamesFromText(text: string): string[] {
|
|
204
|
+
const names = new Set<string>();
|
|
205
|
+
for (const regex of [/process\.env\.([A-Z0-9_]+)/g, /process\.env\[['"]([A-Z0-9_]+)['"]\]/g, /ctx\.secrets\.get\(['"]([A-Z0-9_]+)['"]\)/g]) {
|
|
206
|
+
let match: RegExpExecArray | null;
|
|
207
|
+
while ((match = regex.exec(text))) names.add(match[1]);
|
|
208
|
+
}
|
|
209
|
+
return [...names].sort();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function processEnvNamesFromText(text: string): string[] {
|
|
213
|
+
const names = new Set<string>();
|
|
214
|
+
for (const regex of [/process\.env\.([A-Z0-9_]+)/g, /process\.env\[['"]([A-Z0-9_]+)['"]\]/g]) {
|
|
215
|
+
let match: RegExpExecArray | null;
|
|
216
|
+
while ((match = regex.exec(text))) names.add(match[1]);
|
|
217
|
+
}
|
|
218
|
+
return [...names].sort();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function eventNamesFromText(text: string): string[] {
|
|
222
|
+
const events = new Set<string>();
|
|
223
|
+
const regex = /ctx\.emit\s*\(\s*["'`]([^"'`]+)["'`]/g;
|
|
224
|
+
let match: RegExpExecArray | null;
|
|
225
|
+
while ((match = regex.exec(text))) events.add(match[1]);
|
|
226
|
+
return [...events].sort();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function hasSubscriber(ctx: ReviewContext, event: string): boolean {
|
|
230
|
+
return Boolean(
|
|
231
|
+
ctx.generated.actionSubscriptions.byEvent?.[event]?.length ||
|
|
232
|
+
ctx.generated.workflowSubscriptions.byEvent?.[event]?.length ||
|
|
233
|
+
ctx.generated.actionSubscriptions.subscriptions?.some((sub) => sub.eventType === event) ||
|
|
234
|
+
ctx.generated.workflowSubscriptions.subscriptions?.some((sub) => sub.eventType === event),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function runtimeRules(ctx: ReviewContext): ReviewFinding[] {
|
|
239
|
+
const findings: ReviewFinding[] = [];
|
|
240
|
+
for (const [file, text] of ctx.fileTexts) {
|
|
241
|
+
if (isCommandFile(file)) {
|
|
242
|
+
const forbiddenImport = text.match(/from\s+["'](stripe|openai|@aws-sdk\/[^"']+|resend|nodemailer)["']/);
|
|
243
|
+
if (forbiddenImport) {
|
|
244
|
+
findings.push(finding({
|
|
245
|
+
severity: "blocking",
|
|
246
|
+
category: "runtime",
|
|
247
|
+
code: "runtime-command-forbidden-import",
|
|
248
|
+
title: "Command imports side-effect package",
|
|
249
|
+
message: `Command ${basenameNoExt(file)} imports ${forbiddenImport[1]}; move side effects to an action or workflow.`,
|
|
250
|
+
file,
|
|
251
|
+
affected: { commands: [basenameNoExt(file)] },
|
|
252
|
+
suggestedCommands: ["forge refactor extract-action", "forge make action", "forge repair diagnose"],
|
|
253
|
+
autoRepair: { available: true, command: "forge refactor extract-action", confidence: "medium" },
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
if (/ctx\.secrets|ctx\.ai|fetch\s*\(/.test(text)) {
|
|
257
|
+
findings.push(finding({
|
|
258
|
+
severity: "blocking",
|
|
259
|
+
category: "runtime",
|
|
260
|
+
code: "runtime-command-side-effect-capability",
|
|
261
|
+
title: "Command uses forbidden capability",
|
|
262
|
+
message: `Command ${basenameNoExt(file)} uses secrets, AI, or raw network access in a deterministic runtime.`,
|
|
263
|
+
file,
|
|
264
|
+
affected: { commands: [basenameNoExt(file)] },
|
|
265
|
+
suggestedCommands: ["forge refactor extract-action", "forge repair diagnose"],
|
|
266
|
+
autoRepair: { available: true, command: "forge refactor extract-action", confidence: "medium" },
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (isQueryFile(file) && /ctx\.db\.(insert|update|delete)|\.(insert|update|delete)\s*\(/.test(text)) {
|
|
271
|
+
findings.push(finding({
|
|
272
|
+
severity: "blocking",
|
|
273
|
+
category: "runtime",
|
|
274
|
+
code: isLiveQueryFile(file) ? "runtime-livequery-write" : "runtime-query-write",
|
|
275
|
+
title: "Read runtime performs writes",
|
|
276
|
+
message: `${isLiveQueryFile(file) ? "LiveQuery" : "Query"} ${basenameNoExt(file)} appears to perform a write.`,
|
|
277
|
+
file,
|
|
278
|
+
affected: isLiveQueryFile(file) ? { liveQueries: [basenameNoExt(file)] } : { queries: [basenameNoExt(file)] },
|
|
279
|
+
suggestedCommands: ["forge check", "forge verify --strict"],
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return findings;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function dataRules(ctx: ReviewContext): ReviewFinding[] {
|
|
287
|
+
const findings: ReviewFinding[] = [];
|
|
288
|
+
for (const [file, text] of ctx.fileTexts) {
|
|
289
|
+
if (!/(schema|dataGraph)/i.test(file)) continue;
|
|
290
|
+
const mentionsTenant = /tenantId/.test(text);
|
|
291
|
+
const hasTenantIndex = /index\s*\([^)]*tenantId|tenantId[^.\n]*(index|indexed)/i.test(text);
|
|
292
|
+
if (mentionsTenant && !hasTenantIndex) {
|
|
293
|
+
findings.push(finding({
|
|
294
|
+
severity: "warning",
|
|
295
|
+
category: "data",
|
|
296
|
+
code: "data-tenant-index-missing",
|
|
297
|
+
title: "Tenant field may lack index",
|
|
298
|
+
message: "Schema change mentions tenantId but no tenantId index was detected.",
|
|
299
|
+
file,
|
|
300
|
+
affected: { tables: ctx.impacted.data.tables },
|
|
301
|
+
suggestedCommands: ["forge db diff --json", "forge rls check", "forge verify --strict"],
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
if (/required\s*:\s*true|\.notNull\(\)|nullable\s*:\s*false/.test(text)) {
|
|
305
|
+
findings.push(finding({
|
|
306
|
+
severity: "warning",
|
|
307
|
+
category: "data",
|
|
308
|
+
code: "data-required-field-added",
|
|
309
|
+
title: "Required field change needs migration review",
|
|
310
|
+
message: "A required/non-null field appears in a schema diff; confirm existing rows are handled.",
|
|
311
|
+
file,
|
|
312
|
+
affected: { tables: ctx.impacted.data.tables },
|
|
313
|
+
suggestedCommands: ["forge db diff --json", "forge verify --strict"],
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return findings;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function policyRules(ctx: ReviewContext): ReviewFinding[] {
|
|
321
|
+
const findings: ReviewFinding[] = [];
|
|
322
|
+
for (const [file, text] of ctx.fileTexts) {
|
|
323
|
+
if (!/polic|auth|claims|oidc|jwt/i.test(file)) continue;
|
|
324
|
+
if (/roles?\s*:\s*\[[^\]]*(owner|admin|member|viewer)[^\]]*,[^\]]*(owner|admin|member|viewer)/i.test(text)) {
|
|
325
|
+
findings.push(finding({
|
|
326
|
+
severity: "warning",
|
|
327
|
+
category: "policy",
|
|
328
|
+
code: "policy-widened",
|
|
329
|
+
title: "Policy role set changed",
|
|
330
|
+
message: "Policy/auth change appears to widen or alter role access; confirm this is intentional.",
|
|
331
|
+
file,
|
|
332
|
+
affected: { policies: ctx.impacted.policies },
|
|
333
|
+
suggestedCommands: ["forge policy matrix", "forge policy check --strict-policies", "forge auth check"],
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
if (/allowDevAuth\s*:\s*true|FORGE_AUTH_MODE\s*=\s*dev/i.test(text)) {
|
|
337
|
+
findings.push(finding({
|
|
338
|
+
severity: "blocking",
|
|
339
|
+
category: "policy",
|
|
340
|
+
code: "auth-dev-headers-production",
|
|
341
|
+
title: "Dev auth enabled in auth config",
|
|
342
|
+
message: "Auth changes enable dev-header style auth; do not ship this in production.",
|
|
343
|
+
file,
|
|
344
|
+
suggestedCommands: ["forge auth check", "forge verify --strict"],
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return findings;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function secretRules(ctx: ReviewContext): ReviewFinding[] {
|
|
352
|
+
const findings: ReviewFinding[] = [];
|
|
353
|
+
const documented = ctx.envExample;
|
|
354
|
+
for (const [file, text] of ctx.fileTexts) {
|
|
355
|
+
if (isTestFile(file) || isForgeToolingFile(file)) continue;
|
|
356
|
+
const names = secretNamesFromText(text);
|
|
357
|
+
if (processEnvNamesFromText(text).length > 0) {
|
|
358
|
+
findings.push(finding({
|
|
359
|
+
severity: "error",
|
|
360
|
+
category: "secrets",
|
|
361
|
+
code: "secret-direct-process-env",
|
|
362
|
+
title: "Direct process.env usage introduced",
|
|
363
|
+
message: "ForgeOS code should access secrets through ctx.secrets or generated config context.",
|
|
364
|
+
file,
|
|
365
|
+
suggestedCommands: ["forge secrets check", "forge refactor replace-process-env <ENV_VAR>"],
|
|
366
|
+
autoRepair: { available: true, command: "forge refactor replace-process-env <ENV_VAR>", confidence: "medium" },
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
for (const name of names) {
|
|
370
|
+
if (!documented.includes(name) && !name.startsWith("PUBLIC_") && !name.startsWith("NEXT_PUBLIC_")) {
|
|
371
|
+
findings.push(finding({
|
|
372
|
+
severity: "error",
|
|
373
|
+
category: "secrets",
|
|
374
|
+
code: "secret-env-example-missing",
|
|
375
|
+
title: "Secret missing from .env.example",
|
|
376
|
+
message: `${name} is referenced by changed code but is not documented in .env.example.`,
|
|
377
|
+
file,
|
|
378
|
+
suggestedCommands: ["forge secrets check"],
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
if (/(_SECRET|SECRET_|TOKEN|KEY)/.test(name) && (name.startsWith("PUBLIC_") || name.startsWith("NEXT_PUBLIC_"))) {
|
|
382
|
+
findings.push(finding({
|
|
383
|
+
severity: "blocking",
|
|
384
|
+
category: "secrets",
|
|
385
|
+
code: "public-env-secret",
|
|
386
|
+
title: "Secret-looking variable exposed publicly",
|
|
387
|
+
message: `${name} looks sensitive but uses a public environment prefix.`,
|
|
388
|
+
file,
|
|
389
|
+
suggestedCommands: ["forge secrets check"],
|
|
390
|
+
}));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return findings;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function packageRules(ctx: ReviewContext): ReviewFinding[] {
|
|
398
|
+
if (ctx.changed.packageFiles.length === 0) return [];
|
|
399
|
+
const hasPlan = nodeFileSystem.exists(join(ctx.workspaceRoot, ".forge/upgrades"));
|
|
400
|
+
return [finding({
|
|
401
|
+
severity: hasPlan ? "info" : "warning",
|
|
402
|
+
category: "package",
|
|
403
|
+
code: "package-change-without-plan",
|
|
404
|
+
title: "Package files changed",
|
|
405
|
+
message: hasPlan
|
|
406
|
+
? "Package files changed; confirm the matching upgrade plan applies."
|
|
407
|
+
: "Package files changed but no .forge/upgrades plan directory was found.",
|
|
408
|
+
file: ctx.changed.packageFiles[0],
|
|
409
|
+
affected: { packages: ctx.impacted.packages },
|
|
410
|
+
suggestedCommands: ["forge deps upgrade-plan <package>", "forge deps upgrade-check --json", "forge generate"],
|
|
411
|
+
})];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function workflowRules(ctx: ReviewContext): ReviewFinding[] {
|
|
415
|
+
const findings: ReviewFinding[] = [];
|
|
416
|
+
for (const [file, text] of ctx.fileTexts) {
|
|
417
|
+
if (isTestFile(file)) continue;
|
|
418
|
+
for (const event of eventNamesFromText(text)) {
|
|
419
|
+
if (!hasSubscriber(ctx, event)) {
|
|
420
|
+
findings.push(finding({
|
|
421
|
+
severity: "warning",
|
|
422
|
+
category: "workflow",
|
|
423
|
+
code: "event-no-subscriber",
|
|
424
|
+
title: "Emitted event has no subscriber",
|
|
425
|
+
message: `${event} is emitted but no generated action/workflow subscription was found.`,
|
|
426
|
+
file,
|
|
427
|
+
affected: { commands: isCommandFile(file) ? [basenameNoExt(file)] : undefined },
|
|
428
|
+
suggestedCommands: [`forge make action ${event.replace(/[^a-zA-Z0-9]/g, "-")}`, "forge workflow list", "forge outbox list"],
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return findings;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function liveQueryRules(ctx: ReviewContext): ReviewFinding[] {
|
|
437
|
+
const findings: ReviewFinding[] = [];
|
|
438
|
+
const liveFiles = [...ctx.fileTexts.keys()].filter(isLiveQueryFile);
|
|
439
|
+
if (liveFiles.length > 0 && ctx.changed.tests.filter((file) => /live|query|tenant/i.test(file)).length === 0) {
|
|
440
|
+
findings.push(finding({
|
|
441
|
+
severity: "warning",
|
|
442
|
+
category: "livequery",
|
|
443
|
+
code: "livequery-invalidation-test-missing",
|
|
444
|
+
title: "LiveQuery change lacks targeted live test",
|
|
445
|
+
message: "A liveQuery changed without a changed live/tenant/invalidation test.",
|
|
446
|
+
file: liveFiles[0],
|
|
447
|
+
affected: { liveQueries: liveFiles.map(basenameNoExt) },
|
|
448
|
+
suggestedCommands: ["forge live test", "forge test plan --changed --include live"],
|
|
449
|
+
}));
|
|
450
|
+
}
|
|
451
|
+
for (const file of liveFiles) {
|
|
452
|
+
const text = ctx.fileTexts.get(file) ?? "";
|
|
453
|
+
if (/rawSql|sql`|ctx\.db\.raw/i.test(text)) {
|
|
454
|
+
findings.push(finding({
|
|
455
|
+
severity: "warning",
|
|
456
|
+
category: "livequery",
|
|
457
|
+
code: "livequery-raw-sql-dependency-unknown",
|
|
458
|
+
title: "LiveQuery raw SQL dependency may be unknown",
|
|
459
|
+
message: "Raw SQL in liveQuery can hide dependency tracking from invalidation.",
|
|
460
|
+
file,
|
|
461
|
+
affected: { liveQueries: [basenameNoExt(file)] },
|
|
462
|
+
suggestedCommands: ["forge live debug <subscriptionId>", "forge test plan --changed --include live"],
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return findings;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function frontendRules(ctx: ReviewContext): ReviewFinding[] {
|
|
470
|
+
const findings: ReviewFinding[] = [];
|
|
471
|
+
for (const [file, text] of ctx.fileTexts) {
|
|
472
|
+
if (!/\.(tsx|jsx)$/.test(file) && !file.startsWith("web/")) continue;
|
|
473
|
+
if (/from\s+["'][^"']*forge\/_generated\/(serverApi|db|runtimeRegistry|secretsContext)|from\s+["'][^"']*\/server["']/.test(text)) {
|
|
474
|
+
findings.push(finding({
|
|
475
|
+
severity: "blocking",
|
|
476
|
+
category: "frontend",
|
|
477
|
+
code: "frontend-server-import",
|
|
478
|
+
title: "Frontend imports server-only generated surface",
|
|
479
|
+
message: "Frontend code imports a server-only Forge surface; use generated client/react hooks instead.",
|
|
480
|
+
file,
|
|
481
|
+
affected: { components: [basenameNoExt(file)] },
|
|
482
|
+
suggestedCommands: ["forge agent-contract check", "forge test plan --changed"],
|
|
483
|
+
}));
|
|
484
|
+
}
|
|
485
|
+
if (/fetch\s*\(\s*["'`]\/?(api|forge|runtime)\//.test(text)) {
|
|
486
|
+
findings.push(finding({
|
|
487
|
+
severity: "warning",
|
|
488
|
+
category: "frontend",
|
|
489
|
+
code: "frontend-direct-runtime-fetch",
|
|
490
|
+
title: "Frontend bypasses generated client",
|
|
491
|
+
message: "Frontend appears to call the runtime directly instead of using the generated client SDK.",
|
|
492
|
+
file,
|
|
493
|
+
affected: { components: [basenameNoExt(file)] },
|
|
494
|
+
suggestedCommands: ["Use src/forge/_generated/react hooks", "Use ForgeProvider"],
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
if (/catch\s*\([^)]*\)\s*{[^}]*console\.error/s.test(text) && !/traceId/.test(text)) {
|
|
498
|
+
findings.push(finding({
|
|
499
|
+
severity: "warning",
|
|
500
|
+
category: "frontend",
|
|
501
|
+
code: "frontend-missing-trace-error",
|
|
502
|
+
title: "Frontend error path may hide traceId",
|
|
503
|
+
message: "Error handling changed without surfacing traceId.",
|
|
504
|
+
file,
|
|
505
|
+
affected: { components: [basenameNoExt(file)] },
|
|
506
|
+
}));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return findings;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function testRules(ctx: ReviewContext, preliminaryRisk: ReviewRisk): ReviewFinding[] {
|
|
513
|
+
const findings: ReviewFinding[] = [];
|
|
514
|
+
const hasLastPlan = nodeFileSystem.exists(join(ctx.workspaceRoot, ".forge/test-plans")) || nodeFileSystem.exists(join(ctx.workspaceRoot, ".forge/test-runs/last.json"));
|
|
515
|
+
const hasUiRun = nodeFileSystem.exists(join(ctx.workspaceRoot, ".forge/ui-runs/last.json"));
|
|
516
|
+
if ((ctx.impacted.frontend.components.length > 0 || ctx.impacted.frontend.pages.length > 0) && !hasUiRun) {
|
|
517
|
+
findings.push(finding({
|
|
518
|
+
severity: "warning",
|
|
519
|
+
category: "test",
|
|
520
|
+
code: "review-ui-smoke-missing",
|
|
521
|
+
title: "Frontend change lacks UI smoke evidence",
|
|
522
|
+
message: "Frontend impact detected but no .forge/ui-runs/last.json report exists.",
|
|
523
|
+
suggestedCommands: ["forge ui smoke --json", "forge ui report last --json"],
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
if ((preliminaryRisk.level === "high" || preliminaryRisk.level === "critical") && ctx.changed.tests.length === 0 && !hasLastPlan) {
|
|
527
|
+
findings.push(finding({
|
|
528
|
+
severity: "warning",
|
|
529
|
+
category: "test",
|
|
530
|
+
code: "test-high-risk-untested",
|
|
531
|
+
title: "High risk change lacks test evidence",
|
|
532
|
+
message: "High risk impact detected with no changed tests or stored H28 test plan/run evidence.",
|
|
533
|
+
suggestedCommands: ["forge test plan --changed", "forge test run --changed", "forge verify --strict"],
|
|
534
|
+
}));
|
|
535
|
+
}
|
|
536
|
+
return findings;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function deployRules(ctx: ReviewContext): ReviewFinding[] {
|
|
540
|
+
const findings: ReviewFinding[] = [];
|
|
541
|
+
for (const file of ctx.changed.deployFiles) {
|
|
542
|
+
const text = ctx.fileTexts.get(file) ?? "";
|
|
543
|
+
if (/ALLOW_DEV_AUTH\s*[:=]\s*["']?true|FORGE_AUTH_MODE\s*[:=]\s*["']?dev|POSTGRES_USER\s*[:=]\s*["']?postgres|DATABASE_URL=.*postgres:postgres/i.test(text)) {
|
|
544
|
+
findings.push(finding({
|
|
545
|
+
severity: "blocking",
|
|
546
|
+
category: "deploy",
|
|
547
|
+
code: /POSTGRES|DATABASE_URL/i.test(text) ? "deploy-db-superuser" : "deploy-dev-auth",
|
|
548
|
+
title: "Deployment config contains unsafe production default",
|
|
549
|
+
message: "Deploy/release file appears to enable dev auth or use a superuser database identity.",
|
|
550
|
+
file,
|
|
551
|
+
suggestedCommands: ["forge self-host check", "forge release check", "forge rls test --db postgres"],
|
|
552
|
+
}));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return findings;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function agentRules(ctx: ReviewContext): ReviewFinding[] {
|
|
559
|
+
const changedAgents = ctx.changed.files.includes("AGENTS.md");
|
|
560
|
+
const changedContract = ctx.changed.generated.some((file) => file.endsWith("agentContract.json") || file.endsWith("agentContract.ts"));
|
|
561
|
+
if (changedAgents && !changedContract) {
|
|
562
|
+
return [finding({
|
|
563
|
+
severity: "warning",
|
|
564
|
+
category: "agent",
|
|
565
|
+
code: "agent-contract-stale",
|
|
566
|
+
title: "Agent contract may be stale",
|
|
567
|
+
message: "AGENTS.md changed without agentContract artifacts changing in the same review source.",
|
|
568
|
+
file: "AGENTS.md",
|
|
569
|
+
suggestedCommands: ["forge agent-contract check", "forge agent export --target generic", "forge agent check"],
|
|
570
|
+
})];
|
|
571
|
+
}
|
|
572
|
+
return [];
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function releaseRules(ctx: ReviewContext): ReviewFinding[] {
|
|
576
|
+
if (ctx.changed.files.some((file) => file.includes("sourceMap") || file.includes("release") || file.endsWith(".map"))) {
|
|
577
|
+
return [finding({
|
|
578
|
+
severity: "warning",
|
|
579
|
+
category: "release",
|
|
580
|
+
code: "release-review-required",
|
|
581
|
+
title: "Release/source-map surface changed",
|
|
582
|
+
message: "Release or source-map files changed; confirm source maps are not publicly exposed.",
|
|
583
|
+
suggestedCommands: ["forge release check", "forge self-host check"],
|
|
584
|
+
})];
|
|
585
|
+
}
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function riskFor(ctx: ReviewContext, findings: ReviewFinding[]): ReviewRisk {
|
|
590
|
+
let score = 0;
|
|
591
|
+
const reasons: ReviewRisk["reasons"] = [];
|
|
592
|
+
const blockers = findings.filter((finding) => finding.severity === "blocking").map((finding) => finding.code).sort();
|
|
593
|
+
for (const item of findings) {
|
|
594
|
+
const weight = item.severity === "blocking" ? 80 : item.severity === "error" ? 30 : item.severity === "warning" ? 10 : 1;
|
|
595
|
+
score += weight;
|
|
596
|
+
reasons.push({
|
|
597
|
+
code: item.code,
|
|
598
|
+
message: item.title,
|
|
599
|
+
severity: item.severity === "blocking" ? "error" : item.severity,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
const impactWeights: Array<[boolean, number, string]> = [
|
|
603
|
+
[ctx.impacted.data.tables.length > 0, 15, "data schema impact"],
|
|
604
|
+
[ctx.impacted.policies.length > 0, 20, "policy/auth impact"],
|
|
605
|
+
[ctx.changed.packageFiles.length > 0, 30, "package file impact"],
|
|
606
|
+
[ctx.changed.deployFiles.length > 0, 20, "deploy/release impact"],
|
|
607
|
+
[ctx.changed.generated.some((file) => file.includes("rls")), 25, "RLS generated artifact impact"],
|
|
608
|
+
[ctx.impacted.frontend.components.length > 0 || ctx.impacted.frontend.pages.length > 0, 5, "frontend impact"],
|
|
609
|
+
];
|
|
610
|
+
for (const [active, weight, message] of impactWeights) {
|
|
611
|
+
if (!active) continue;
|
|
612
|
+
score += weight;
|
|
613
|
+
reasons.push({ code: `impact-${message.replace(/[^a-z]+/g, "-")}`, message, severity: weight >= 20 ? "warning" : "info" });
|
|
614
|
+
}
|
|
615
|
+
if (ctx.changed.tests.length > 0) {
|
|
616
|
+
score = Math.max(0, score - 5);
|
|
617
|
+
reasons.push({ code: "tests-added", message: "changed tests present", severity: "info" });
|
|
618
|
+
}
|
|
619
|
+
const level = blockers.length > 0 || score >= 80 ? "critical" : score >= 50 ? "high" : score >= 20 ? "medium" : "low";
|
|
620
|
+
return {
|
|
621
|
+
level,
|
|
622
|
+
score,
|
|
623
|
+
reasons: reasons.sort((a, b) => a.code.localeCompare(b.code)),
|
|
624
|
+
blockers,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function preliminaryRisk(ctx: ReviewContext): ReviewRisk {
|
|
629
|
+
return riskFor(ctx, []);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function runRules(ctx: ReviewContext, options: ReviewCommandOptions): ReviewFinding[] {
|
|
633
|
+
const groups: Record<ReviewFindingCategory, ReviewFinding[]> = {
|
|
634
|
+
runtime: runtimeRules(ctx),
|
|
635
|
+
data: dataRules(ctx),
|
|
636
|
+
policy: policyRules(ctx),
|
|
637
|
+
secrets: secretRules(ctx),
|
|
638
|
+
package: packageRules(ctx),
|
|
639
|
+
workflow: workflowRules(ctx),
|
|
640
|
+
livequery: liveQueryRules(ctx),
|
|
641
|
+
frontend: frontendRules(ctx),
|
|
642
|
+
deploy: deployRules(ctx),
|
|
643
|
+
release: releaseRules(ctx),
|
|
644
|
+
agent: agentRules(ctx),
|
|
645
|
+
test: [],
|
|
646
|
+
};
|
|
647
|
+
const riskBeforeTests = preliminaryRisk(ctx);
|
|
648
|
+
groups.test = testRules(ctx, riskBeforeTests);
|
|
649
|
+
return ALL_CATEGORIES
|
|
650
|
+
.filter((category) => includesCategory(options, category))
|
|
651
|
+
.flatMap((category) => groups[category])
|
|
652
|
+
.sort((a, b) => `${a.severity}:${a.category}:${a.code}:${a.file ?? ""}`.localeCompare(`${b.severity}:${b.category}:${b.code}:${b.file ?? ""}`));
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function recommendedCommands(ctx: ReviewContext, findings: ReviewFinding[]): string[] {
|
|
656
|
+
const commands = new Set<string>([
|
|
657
|
+
"forge generate --check",
|
|
658
|
+
"forge check",
|
|
659
|
+
"forge test plan --changed",
|
|
660
|
+
"forge verify --strict",
|
|
661
|
+
]);
|
|
662
|
+
for (const finding of findings) {
|
|
663
|
+
for (const command of finding.suggestedCommands ?? []) commands.add(command);
|
|
664
|
+
}
|
|
665
|
+
if (ctx.changed.packageFiles.length > 0) commands.add("forge deps upgrade-check --json");
|
|
666
|
+
if (ctx.changed.deployFiles.length > 0) commands.add("forge self-host check");
|
|
667
|
+
return [...commands].sort();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function buildReport(options: ReviewCommandOptions): ReviewReport {
|
|
671
|
+
const ctx = loadContext(options);
|
|
672
|
+
const findings = runRules(ctx, options);
|
|
673
|
+
const risk = riskFor(ctx, findings);
|
|
674
|
+
const testPlan = buildImpactTestPlan({
|
|
675
|
+
subcommand: "plan",
|
|
676
|
+
workspaceRoot: options.workspaceRoot,
|
|
677
|
+
json: options.json,
|
|
678
|
+
write: false,
|
|
679
|
+
changed: ctx.impactSource.mode === "changed",
|
|
680
|
+
staged: ctx.impactSource.mode === "staged",
|
|
681
|
+
since: ctx.impactSource.mode === "since" ? ctx.impactSource.base : undefined,
|
|
682
|
+
featureId: ctx.impactSource.mode === "feature" ? ctx.impactSource.id : undefined,
|
|
683
|
+
refactorId: ctx.impactSource.mode === "refactor" ? ctx.impactSource.id : undefined,
|
|
684
|
+
upgradeId: ctx.impactSource.mode === "upgrade" ? ctx.impactSource.id : undefined,
|
|
685
|
+
maxCost: options.mode === "quick" ? "fast" : "standard",
|
|
686
|
+
includeDocker: options.mode === "strict",
|
|
687
|
+
includeBrowser: options.mode === "strict",
|
|
688
|
+
bail: false,
|
|
689
|
+
});
|
|
690
|
+
const id = `review_${hashStable(serializeCanonical({
|
|
691
|
+
source: ctx.source,
|
|
692
|
+
files: ctx.changed.files,
|
|
693
|
+
findings: findings.map((item) => [item.code, item.file, item.severity]),
|
|
694
|
+
})).slice(0, 12)}`;
|
|
695
|
+
const generatedPaths = ctx.changed.generated;
|
|
696
|
+
return {
|
|
697
|
+
schemaVersion: "0.1.0",
|
|
698
|
+
reviewVersion: REVIEW_VERSION,
|
|
699
|
+
id,
|
|
700
|
+
source: ctx.source,
|
|
701
|
+
summary: {
|
|
702
|
+
title: "Forge Review",
|
|
703
|
+
bullets: [
|
|
704
|
+
`${ctx.changed.files.length} changed file(s) reviewed.`,
|
|
705
|
+
`${findings.length} finding(s) detected.`,
|
|
706
|
+
`Risk is ${risk.level}.`,
|
|
707
|
+
],
|
|
708
|
+
},
|
|
709
|
+
risk,
|
|
710
|
+
findings,
|
|
711
|
+
changed: ctx.changed,
|
|
712
|
+
impacted: ctx.impacted,
|
|
713
|
+
checks: [
|
|
714
|
+
{ name: "impact-analysis", ok: true, message: `risk ${risk.level}` },
|
|
715
|
+
{ name: "test-plan", ok: true, command: "forge test plan --changed", message: `${testPlan.tests.length} targeted test(s)` },
|
|
716
|
+
{ name: "generated-drift", ok: true, command: "forge generate --check" },
|
|
717
|
+
],
|
|
718
|
+
recommendedCommands: recommendedCommands(ctx, findings),
|
|
719
|
+
humanChecklist: [
|
|
720
|
+
{ id: "policies", text: "Are new or changed policies intentional?", required: ctx.impacted.policies.length > 0, category: "policy" },
|
|
721
|
+
{ id: "tenant-indexes", text: "Are tenant-scoped tables indexed correctly?", required: ctx.impacted.data.tables.length > 0, category: "data" },
|
|
722
|
+
{ id: "side-effects", text: "Are commands free of direct side effects?", required: ctx.impacted.runtime.commands.length > 0, category: "runtime" },
|
|
723
|
+
{ id: "live-tests", text: "Are liveQueries tested for invalidation and tenant isolation?", required: ctx.impacted.runtime.liveQueries.length > 0, category: "livequery" },
|
|
724
|
+
{ id: "secrets", text: "Are new secrets documented without values?", required: findings.some((finding) => finding.category === "secrets"), category: "secrets" },
|
|
725
|
+
{ id: "deploy", text: "Are deploy/release changes safe for production?", required: ctx.changed.deployFiles.length > 0, category: "deploy" },
|
|
726
|
+
],
|
|
727
|
+
agentInstructions: [
|
|
728
|
+
"Fix blocking findings before merge.",
|
|
729
|
+
"Prefer forge repair/refactor/make suggestions when available.",
|
|
730
|
+
"Run targeted checks before forge verify --strict.",
|
|
731
|
+
],
|
|
732
|
+
generatedArtifacts: {
|
|
733
|
+
paths: generatedPaths,
|
|
734
|
+
stale: findings.filter((finding) => finding.code.includes("agent-contract-stale")).map((finding) => finding.file ?? "AGENTS.md"),
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function shouldFail(report: ReviewReport, failOn?: ReviewFailOn): boolean {
|
|
740
|
+
if (!failOn) return report.findings.some((finding) => finding.severity === "blocking");
|
|
741
|
+
const order: Record<ReviewFindingSeverity, number> = { info: 0, warning: 1, error: 2, blocking: 3 };
|
|
742
|
+
const threshold = failOn === "warning" ? 1 : failOn === "error" ? 2 : 3;
|
|
743
|
+
return report.findings.some((finding) => order[finding.severity] >= threshold);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export function renderReviewMarkdown(report: ReviewReport): string {
|
|
747
|
+
const findings = report.findings.map((item) => `- ${item.severity.toUpperCase()} ${item.code}${item.file ? ` (${item.file})` : ""}: ${item.message}`).join("\n") || "- none";
|
|
748
|
+
return `# Forge Review
|
|
749
|
+
|
|
750
|
+
Risk: ${report.risk.level} (${report.risk.score})
|
|
751
|
+
|
|
752
|
+
## Summary
|
|
753
|
+
|
|
754
|
+
${report.summary.bullets.map((bullet) => `- ${bullet}`).join("\n")}
|
|
755
|
+
|
|
756
|
+
## Changed Files
|
|
757
|
+
|
|
758
|
+
${report.changed.files.map((file) => `- ${file}`).join("\n") || "- none"}
|
|
759
|
+
|
|
760
|
+
## Findings
|
|
761
|
+
|
|
762
|
+
${findings}
|
|
763
|
+
|
|
764
|
+
## Recommended Commands
|
|
765
|
+
|
|
766
|
+
\`\`\`bash
|
|
767
|
+
${report.recommendedCommands.join("\n")}
|
|
768
|
+
\`\`\`
|
|
769
|
+
`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export function renderPrSummary(report: ReviewReport): string {
|
|
773
|
+
return `# PR Summary
|
|
774
|
+
|
|
775
|
+
## What changed
|
|
776
|
+
|
|
777
|
+
${report.summary.bullets.map((bullet) => `- ${bullet}`).join("\n")}
|
|
778
|
+
|
|
779
|
+
## Risk
|
|
780
|
+
|
|
781
|
+
${report.risk.level}.
|
|
782
|
+
|
|
783
|
+
## Reviewer focus
|
|
784
|
+
|
|
785
|
+
${report.humanChecklist.filter((item) => item.required).map((item) => `- ${item.text}`).join("\n") || "- Standard ForgeOS review."}
|
|
786
|
+
`;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
export function renderRiskReport(report: ReviewReport): string {
|
|
790
|
+
return `# Risk Report
|
|
791
|
+
|
|
792
|
+
Level: ${report.risk.level}
|
|
793
|
+
Score: ${report.risk.score}
|
|
794
|
+
|
|
795
|
+
## Reasons
|
|
796
|
+
|
|
797
|
+
${report.risk.reasons.map((reason) => `- ${reason.severity} ${reason.code}: ${reason.message}`).join("\n") || "- none"}
|
|
798
|
+
|
|
799
|
+
## Blockers
|
|
800
|
+
|
|
801
|
+
${report.risk.blockers.map((blocker) => `- ${blocker}`).join("\n") || "- none"}
|
|
802
|
+
`;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export function renderHumanChecklist(report: ReviewReport): string {
|
|
806
|
+
return `# Human Review Checklist
|
|
807
|
+
|
|
808
|
+
${report.humanChecklist.map((item) => `- [ ] ${item.required ? "(required) " : ""}${item.text}`).join("\n")}
|
|
809
|
+
`;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
export function renderTestPlan(report: ReviewReport): string {
|
|
813
|
+
return `# Review Test Plan
|
|
814
|
+
|
|
815
|
+
\`\`\`bash
|
|
816
|
+
${report.recommendedCommands.filter((command) => command.includes("test") || command.includes("verify") || command.includes("check")).join("\n")}
|
|
817
|
+
\`\`\`
|
|
818
|
+
`;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export function renderSarif(report: ReviewReport): string {
|
|
822
|
+
const sarif = {
|
|
823
|
+
version: "2.1.0",
|
|
824
|
+
runs: [{
|
|
825
|
+
tool: {
|
|
826
|
+
driver: {
|
|
827
|
+
name: "Forge Review",
|
|
828
|
+
informationUri: "https://forgeos.local/review",
|
|
829
|
+
rules: report.findings.map((finding) => ({
|
|
830
|
+
id: finding.code,
|
|
831
|
+
name: finding.title,
|
|
832
|
+
shortDescription: { text: finding.title },
|
|
833
|
+
fullDescription: { text: finding.message },
|
|
834
|
+
})),
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
results: report.findings.map((finding) => ({
|
|
838
|
+
ruleId: finding.code,
|
|
839
|
+
level: finding.severity === "blocking" || finding.severity === "error" ? "error" : finding.severity === "warning" ? "warning" : "note",
|
|
840
|
+
message: { text: finding.message },
|
|
841
|
+
locations: finding.file ? [{
|
|
842
|
+
physicalLocation: {
|
|
843
|
+
artifactLocation: { uri: finding.file },
|
|
844
|
+
region: finding.span ? { startLine: finding.span.start, endLine: finding.span.end } : undefined,
|
|
845
|
+
},
|
|
846
|
+
}] : [],
|
|
847
|
+
})),
|
|
848
|
+
}],
|
|
849
|
+
};
|
|
850
|
+
return `${JSON.stringify(sarif, null, 2)}\n`;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export function writeReviewReport(workspaceRoot: string, report: ReviewReport, includeSarif: boolean): ReviewWriteResult {
|
|
854
|
+
const relativeDir = join(REVIEW_DIR, report.id).replace(/\\/g, "/");
|
|
855
|
+
const dir = join(workspaceRoot, relativeDir);
|
|
856
|
+
nodeFileSystem.mkdirp(dir);
|
|
857
|
+
const files: Array<[string, string]> = [
|
|
858
|
+
["review.json", serializeCanonical(report)],
|
|
859
|
+
["review.md", renderReviewMarkdown(report)],
|
|
860
|
+
["pr-summary.md", renderPrSummary(report)],
|
|
861
|
+
["risk-report.md", renderRiskReport(report)],
|
|
862
|
+
["test-plan.md", renderTestPlan(report)],
|
|
863
|
+
["human-checklist.md", renderHumanChecklist(report)],
|
|
864
|
+
];
|
|
865
|
+
if (includeSarif) files.push(["review.sarif", renderSarif(report)]);
|
|
866
|
+
for (const [file, content] of files) {
|
|
867
|
+
nodeFileSystem.writeText(join(dir, file), content);
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
dir: relativeDir,
|
|
871
|
+
files: files.map(([file]) => `${relativeDir}/${file}`),
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export function explainReviewRule(ruleId: string): ReviewResult {
|
|
876
|
+
const doc = RULE_DOCS.find((rule) => rule.id === ruleId);
|
|
877
|
+
if (!doc) {
|
|
878
|
+
return {
|
|
879
|
+
ok: false,
|
|
880
|
+
diagnostics: [createDiagnostic({ severity: "error", code: "FORGE_REVIEW_RULE_UNKNOWN", message: `unknown review rule: ${ruleId}` })],
|
|
881
|
+
exitCode: 1,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
ok: true,
|
|
886
|
+
explanation: `Rule: ${doc.id}
|
|
887
|
+
|
|
888
|
+
${doc.description}
|
|
889
|
+
|
|
890
|
+
Typical fix:
|
|
891
|
+
${doc.typicalFix.map((fix) => ` - ${fix}`).join("\n")}
|
|
892
|
+
|
|
893
|
+
Related commands:
|
|
894
|
+
${doc.relatedCommands.map((command) => ` - ${command}`).join("\n")}
|
|
895
|
+
`,
|
|
896
|
+
diagnostics: [],
|
|
897
|
+
exitCode: 0,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export function listReviews(workspaceRoot: string): ReviewResult {
|
|
902
|
+
const dir = join(workspaceRoot, REVIEW_DIR);
|
|
903
|
+
if (!nodeFileSystem.exists(dir)) return { ok: true, reports: [], diagnostics: [], exitCode: 0 };
|
|
904
|
+
const reports = nodeFileSystem
|
|
905
|
+
.readDir(dir)
|
|
906
|
+
.filter((entry) => entry.isDirectory)
|
|
907
|
+
.map((entry) => ({ id: entry.name, dir: `${REVIEW_DIR}/${entry.name}` }))
|
|
908
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
909
|
+
return { ok: true, reports, diagnostics: [], exitCode: 0 };
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
export function inspectReview(workspaceRoot: string, reviewId: string): ReviewResult {
|
|
913
|
+
const path = join(workspaceRoot, REVIEW_DIR, reviewId, "review.json");
|
|
914
|
+
if (!nodeFileSystem.exists(path)) {
|
|
915
|
+
return {
|
|
916
|
+
ok: false,
|
|
917
|
+
diagnostics: [createDiagnostic({ severity: "error", code: "FORGE_REVIEW_NOT_FOUND", message: `review not found: ${reviewId}` })],
|
|
918
|
+
exitCode: 1,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
ok: true,
|
|
923
|
+
report: JSON.parse(nodeFileSystem.readText(path) ?? "{}") as ReviewReport,
|
|
924
|
+
diagnostics: [],
|
|
925
|
+
exitCode: 0,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export function runReviewCommand(options: ReviewCommandOptions): ReviewResult {
|
|
930
|
+
if (options.subcommand === "explain") {
|
|
931
|
+
return explainReviewRule(options.ruleId ?? "");
|
|
932
|
+
}
|
|
933
|
+
if (options.subcommand === "list") {
|
|
934
|
+
return listReviews(options.workspaceRoot);
|
|
935
|
+
}
|
|
936
|
+
if (options.subcommand === "inspect") {
|
|
937
|
+
return inspectReview(options.workspaceRoot, options.reviewId ?? "");
|
|
938
|
+
}
|
|
939
|
+
try {
|
|
940
|
+
const report = buildReport(options);
|
|
941
|
+
const writeResult = options.write ? writeReviewReport(options.workspaceRoot, report, options.sarif) : undefined;
|
|
942
|
+
const fail = shouldFail(report, options.failOn);
|
|
943
|
+
return {
|
|
944
|
+
ok: !fail,
|
|
945
|
+
report,
|
|
946
|
+
writeResult,
|
|
947
|
+
diagnostics: fail
|
|
948
|
+
? [createDiagnostic({ severity: "error", code: "FORGE_REVIEW_BLOCKING_FINDINGS", message: `review failed with risk ${report.risk.level}` })]
|
|
949
|
+
: [],
|
|
950
|
+
exitCode: fail ? 1 : 0,
|
|
951
|
+
};
|
|
952
|
+
} catch (error) {
|
|
953
|
+
return {
|
|
954
|
+
ok: false,
|
|
955
|
+
diagnostics: [createDiagnostic({ severity: "error", code: "FORGE_REVIEW_OUTPUT_FAILED", message: error instanceof Error ? error.message : "review failed" })],
|
|
956
|
+
exitCode: 1,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
export function formatReviewJson(result: ReviewResult): string {
|
|
962
|
+
return `${JSON.stringify(result.report ?? result.reports ?? result.explanation ?? result, null, 2)}\n`;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
export function formatReviewHuman(result: ReviewResult): string {
|
|
966
|
+
if (result.explanation) return result.explanation;
|
|
967
|
+
if (result.reports) {
|
|
968
|
+
return `Forge Reviews
|
|
969
|
+
|
|
970
|
+
${result.reports.map((report) => `- ${report.id}: ${report.dir}`).join("\n") || "- none"}
|
|
971
|
+
`;
|
|
972
|
+
}
|
|
973
|
+
if (!result.report) {
|
|
974
|
+
return `${result.diagnostics.map((diagnostic) => `${diagnostic.severity} ${diagnostic.code}: ${diagnostic.message}`).join("\n")}\n`;
|
|
975
|
+
}
|
|
976
|
+
const report = result.report;
|
|
977
|
+
return `Forge Review
|
|
978
|
+
|
|
979
|
+
Risk: ${report.risk.level} (${report.risk.score})
|
|
980
|
+
Findings: ${report.findings.length}
|
|
981
|
+
|
|
982
|
+
Blocking issues:
|
|
983
|
+
${report.findings.filter((finding) => finding.severity === "blocking").map((finding) => ` - ${finding.code}: ${finding.message}`).join("\n") || " - none"}
|
|
984
|
+
|
|
985
|
+
Warnings:
|
|
986
|
+
${report.findings.filter((finding) => finding.severity === "warning").map((finding) => ` - ${finding.code}: ${finding.message}`).join("\n") || " - none"}
|
|
987
|
+
|
|
988
|
+
Recommended:
|
|
989
|
+
${report.recommendedCommands.map((command) => ` - ${command}`).join("\n")}
|
|
990
|
+
${result.writeResult ? `\nWritten: ${result.writeResult.dir}\n` : ""}
|
|
991
|
+
`;
|
|
992
|
+
}
|