forgeos 0.1.0-alpha.2 → 0.1.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.npmignore +4 -0
- package/AGENTS.md +168 -81
- package/CHANGELOG.md +211 -0
- package/README.md +88 -14
- package/adapters/go/README.md +23 -0
- package/adapters/go/go.mod +3 -0
- package/adapters/go/http.go +149 -0
- package/adapters/go/registry.go +234 -0
- package/adapters/go/types.go +136 -0
- package/adapters/java/README.md +68 -0
- package/adapters/java/pom.xml +34 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Auth.java +20 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Diagnostic.java +16 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Entry.java +38 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/EntryKind.java +16 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ErrorInfo.java +4 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Forge.java +94 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeCall.java +12 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeContext.java +11 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHandler.java +8 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHttpHandler.java +179 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeRegistry.java +121 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Json.java +14 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Manifest.java +14 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/RequestEnvelope.java +6 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ResponseEnvelope.java +25 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Risk.java +18 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Schemas.java +36 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Service.java +65 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/TransactionMode.java +18 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/TypedForgeHandler.java +6 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Auth.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Diagnostic.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Entry.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/EntryKind.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ErrorInfo.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Forge.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeCall.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeContext.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeHandler.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeHttpHandler.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$EntryOption.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$RegisteredEntry.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry$RegistryOption.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ForgeRegistry.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Json.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Manifest.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/RequestEnvelope.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/ResponseEnvelope.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Risk.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Schemas.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/Service.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/TransactionMode.class +0 -0
- package/adapters/java/target/classes/dev/forgeos/adapter/TypedForgeHandler.class +0 -0
- package/adapters/java/target/forge-java-adapter-0.1.0-alpha.11.jar +0 -0
- package/adapters/java/target/maven-archiver/pom.properties +3 -0
- package/adapters/java/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +23 -0
- package/adapters/java/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +20 -0
- package/adapters/java-spring-boot-starter/README.md +32 -0
- package/adapters/java-spring-boot-starter/pom.xml +36 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeCommand.java +22 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeExternalService.java +15 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeQuery.java +16 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.java +18 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.java +16 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringRuntime.java +104 -0
- package/adapters/java-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
- package/adapters/java-spring-boot-starter/target/classes/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
- package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeCommand.class +0 -0
- package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeExternalService.class +0 -0
- package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeQuery.class +0 -0
- package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.class +0 -0
- package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.class +0 -0
- package/adapters/java-spring-boot-starter/target/classes/dev/forgeos/adapter/spring/ForgeSpringRuntime.class +0 -0
- package/adapters/java-spring-boot-starter/target/forge-java-spring-boot-starter-0.1.0-alpha.11.jar +0 -0
- package/adapters/java-spring-boot-starter/target/maven-archiver/pom.properties +3 -0
- package/adapters/java-spring-boot-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +6 -0
- package/adapters/java-spring-boot-starter/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +6 -0
- package/bin/forge.mjs +18 -0
- package/docs/changelog.md +242 -0
- package/docs/forge-protocol.md +189 -0
- package/examples/go-billing/go.mod +7 -0
- package/examples/go-billing/main.go +120 -0
- package/examples/java-billing/pom.xml +52 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/CreateInvoiceInput.java +4 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Invoice.java +11 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Main.java +127 -0
- package/examples/java-billing/target/classes/dev/forgeos/examples/billing/CreateInvoiceInput.class +0 -0
- package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Invoice.class +0 -0
- package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main$EmptyInput.class +0 -0
- package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main$Options.class +0 -0
- package/examples/java-billing/target/classes/dev/forgeos/examples/billing/Main.class +0 -0
- package/examples/java-billing/target/java-billing-0.1.0-alpha.11-all.jar +0 -0
- package/examples/java-billing/target/java-billing-0.1.0-alpha.11.jar +0 -0
- package/examples/java-billing/target/maven-archiver/pom.properties +3 -0
- package/examples/java-billing/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +5 -0
- package/examples/java-billing/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +3 -0
- package/package.json +29 -7
- package/schemas/forge-manifest.schema.json +57 -0
- package/src/forge/_generated/releaseManifest.json +1 -2
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-adapters/index.ts +1511 -123
- package/src/forge/agent-adapters/types.ts +216 -1
- package/src/forge/agent-memory/bridge.ts +1245 -0
- package/src/forge/agent-memory/context-pack.ts +151 -0
- package/src/forge/agent-memory/hook-runner.ts +312 -0
- package/src/forge/agent-memory/mcp.ts +224 -0
- package/src/forge/agent-memory/normalize.ts +498 -0
- package/src/forge/agent-memory/redaction.ts +103 -0
- package/src/forge/agent-memory/sources/claude-code.ts +51 -0
- package/src/forge/agent-memory/sources/codex-hook-runner.mjs +273 -0
- package/src/forge/agent-memory/sources/codex.ts +119 -0
- package/src/forge/agent-memory/sources/cursor.ts +35 -0
- package/src/forge/agent-memory/types.ts +191 -0
- package/src/forge/bench.ts +248 -0
- package/src/forge/brownfield-import/index.ts +801 -0
- package/src/forge/brownfield-import/types.ts +127 -0
- package/src/forge/cair/action-journal.ts +61 -0
- package/src/forge/cair/action-parser.ts +314 -0
- package/src/forge/cair/action-validator.ts +40 -0
- package/src/forge/cair/actions.ts +1818 -0
- package/src/forge/cair/format.ts +77 -0
- package/src/forge/cair/index.ts +106 -0
- package/src/forge/cair/query.ts +478 -0
- package/src/forge/cair/snapshot.ts +315 -0
- package/src/forge/cair/types.ts +248 -0
- package/src/forge/cli/ai.ts +671 -3
- package/src/forge/cli/auth.ts +36 -1
- package/src/forge/cli/build.ts +20 -4
- package/src/forge/cli/changed.ts +300 -0
- package/src/forge/cli/codex-app-server.ts +877 -0
- package/src/forge/cli/commands.ts +1285 -7
- package/src/forge/cli/db.ts +121 -2
- package/src/forge/cli/deps.ts +79 -12
- package/src/forge/cli/dev.ts +502 -38
- package/src/forge/cli/docs.ts +265 -0
- package/src/forge/cli/handoff.ts +250 -0
- package/src/forge/cli/index.ts +1 -0
- package/src/forge/cli/main.ts +49 -3
- package/src/forge/cli/new.ts +3 -1
- package/src/forge/cli/next-actions.ts +23 -0
- package/src/forge/cli/output.ts +290 -1
- package/src/forge/cli/parse.ts +770 -36
- package/src/forge/cli/query.ts +32 -0
- package/src/forge/cli/release.ts +35 -11
- package/src/forge/cli/rls.ts +568 -17
- package/src/forge/cli/run.ts +41 -0
- package/src/forge/cli/secrets.ts +46 -1
- package/src/forge/cli/security.ts +381 -0
- package/src/forge/cli/self-host.ts +56 -14
- package/src/forge/cli/studio.ts +2163 -0
- package/src/forge/cli/verify.ts +1422 -32
- package/src/forge/compiler/agent-contract/build.ts +725 -41
- package/src/forge/compiler/agent-contract/types.ts +85 -0
- package/src/forge/compiler/ai-registry/build.ts +62 -1
- package/src/forge/compiler/ai-registry/constants.ts +1 -1
- package/src/forge/compiler/ai-registry/parse.ts +168 -5
- package/src/forge/compiler/api-surface/build.ts +47 -0
- package/src/forge/compiler/app-graph/build.ts +68 -8
- package/src/forge/compiler/app-graph/extract.ts +107 -0
- package/src/forge/compiler/app-graph/forge-apis.ts +1 -0
- package/src/forge/compiler/app-graph/module-graph.ts +73 -78
- package/src/forge/compiler/app-graph/parser.ts +24 -24
- package/src/forge/compiler/app-graph/profile.ts +26 -0
- package/src/forge/compiler/app-graph/versions.ts +1 -1
- package/src/forge/compiler/classifier/capabilities.ts +3 -2
- package/src/forge/compiler/classifier/classify.ts +32 -8
- package/src/forge/compiler/classifier/secrets.ts +3 -2
- package/src/forge/compiler/classifier/signals.ts +91 -1
- package/src/forge/compiler/client-sdk/build-manifest.ts +59 -0
- package/src/forge/compiler/client-sdk/render-client.ts +188 -13
- package/src/forge/compiler/data-graph/parse.ts +3 -3
- package/src/forge/compiler/data-graph/sql/ddl.ts +60 -2
- package/src/forge/compiler/data-graph/sql/serialize.ts +4 -0
- package/src/forge/compiler/data-graph/sql/types.ts +1 -0
- package/src/forge/compiler/dev-manifest/build.ts +3 -0
- package/src/forge/compiler/diagnostics/codes.ts +35 -0
- package/src/forge/compiler/diagnostics/create.ts +8 -3
- package/src/forge/compiler/diagnostics/index.ts +2 -0
- package/src/forge/compiler/emitter/barrel.ts +3 -0
- package/src/forge/compiler/emitter/render.ts +5 -0
- package/src/forge/compiler/external-manifest/registry.ts +205 -0
- package/src/forge/compiler/external-manifest/types.ts +91 -0
- package/src/forge/compiler/external-manifest/validate.ts +373 -0
- package/src/forge/compiler/frontend-graph/build.ts +85 -13
- package/src/forge/compiler/integration/add.ts +498 -22
- package/src/forge/compiler/integration/snapshot.ts +2 -0
- package/src/forge/compiler/make-registry/build.ts +19 -7
- package/src/forge/compiler/orchestrator/plan-profile.ts +23 -0
- package/src/forge/compiler/orchestrator/plan.ts +78 -7
- package/src/forge/compiler/orchestrator/profile.ts +65 -0
- package/src/forge/compiler/orchestrator/run.ts +97 -31
- package/src/forge/compiler/orchestrator/serialize.ts +101 -8
- package/src/forge/compiler/package-graph/compiler.ts +13 -3
- package/src/forge/compiler/package-manager/adapter.ts +4 -1
- package/src/forge/compiler/package-manager/commands.ts +4 -0
- package/src/forge/compiler/package-manager/executor.ts +30 -1
- package/src/forge/compiler/policy-registry/build.ts +44 -1
- package/src/forge/compiler/test-graph/build.ts +11 -3
- package/src/forge/compiler/types/ai-registry.ts +25 -1
- package/src/forge/compiler/types/app-graph.ts +9 -2
- package/src/forge/compiler/types/cli.ts +76 -1
- package/src/forge/compiler/types/dev-manifest.ts +3 -0
- package/src/forge/compiler/types/frontend-graph.ts +2 -2
- package/src/forge/delta/classifier.ts +52 -0
- package/src/forge/delta/explain.ts +126 -0
- package/src/forge/delta/git-observer.ts +43 -0
- package/src/forge/delta/ids.ts +44 -0
- package/src/forge/delta/index.ts +13 -0
- package/src/forge/delta/recorder.ts +402 -0
- package/src/forge/delta/redaction.ts +50 -0
- package/src/forge/delta/schema.ts +240 -0
- package/src/forge/delta/session.ts +142 -0
- package/src/forge/delta/status.ts +489 -0
- package/src/forge/delta/store.ts +2975 -0
- package/src/forge/delta/timeline.ts +104 -0
- package/src/forge/dev/server.ts +768 -15
- package/src/forge/dev/types.ts +15 -1
- package/src/forge/dev/watch.ts +17 -7
- package/src/forge/dev-console/cycle.ts +233 -21
- package/src/forge/dev-console/types.ts +46 -1
- package/src/forge/impact/index.ts +46 -8
- package/src/forge/impact/types.ts +6 -0
- package/src/forge/intent/index.ts +35 -16
- package/src/forge/make/index.ts +149 -6
- package/src/forge/make/templates.ts +343 -2
- package/src/forge/make/types.ts +3 -1
- package/src/forge/refactor/index.ts +1 -0
- package/src/forge/repair/rules/index.ts +2 -2
- package/src/forge/review/index.ts +158 -12
- package/src/forge/review/types.ts +15 -0
- package/src/forge/runtime/ai/context.ts +210 -5
- package/src/forge/runtime/ai/types.ts +70 -0
- package/src/forge/runtime/auth/claims.ts +32 -0
- package/src/forge/runtime/auth/errors.ts +2 -0
- package/src/forge/runtime/context/create-context.ts +30 -6
- package/src/forge/runtime/db/generated-client.ts +13 -2
- package/src/forge/runtime/db/memory-adapter.ts +2 -2
- package/src/forge/runtime/db/pglite-adapter.ts +77 -2
- package/src/forge/runtime/db/postgres-adapter.ts +6 -3
- package/src/forge/runtime/executor.ts +112 -2
- package/src/forge/runtime/external/bridge.ts +649 -0
- package/src/forge/runtime/runner/run-entry.ts +16 -7
- package/src/forge/runtime/telemetry/scrubber.ts +91 -10
- package/src/forge/runtime/webhooks/security.ts +184 -0
- package/src/forge/server.ts +100 -2
- package/src/forge/version.ts +1 -1
- package/src/forge/vue/index.ts +407 -0
- package/src/forge/workspace/change-summary.ts +209 -0
- package/src/forge/workspace/forge-cli.ts +14 -0
- package/src/forge/workspace/git-summary.ts +279 -0
- package/templates/agent-workroom/AGENTS.md +29 -0
- package/templates/agent-workroom/README.md +34 -0
- package/templates/agent-workroom/forge.config.ts +3 -0
- package/templates/agent-workroom/package.json +33 -0
- package/templates/agent-workroom/src/actions/indexAgentSignal.ts +10 -0
- package/templates/agent-workroom/src/commands/openWorkroom.ts +61 -0
- package/templates/agent-workroom/src/commands/recordAgentSignal.ts +119 -0
- package/templates/agent-workroom/src/commands/recordCheckRun.ts +52 -0
- package/templates/agent-workroom/src/forge/schema.ts +54 -0
- package/templates/agent-workroom/src/policies.ts +6 -0
- package/templates/agent-workroom/src/queries/listWorkrooms.ts +11 -0
- package/templates/agent-workroom/src/queries/liveWorkroom.ts +63 -0
- package/templates/agent-workroom/tsconfig.json +16 -0
- package/templates/agent-workroom/web/index.html +12 -0
- package/templates/agent-workroom/web/package.json +21 -0
- package/templates/agent-workroom/web/src/App.tsx +345 -0
- package/templates/agent-workroom/web/src/lib/forge.ts +13 -0
- package/templates/agent-workroom/web/src/main.tsx +13 -0
- package/templates/agent-workroom/web/src/styles.css +545 -0
- package/templates/agent-workroom/web/tsconfig.json +27 -0
- package/templates/b2b-support-web/package.json +2 -0
- package/templates/b2b-support-web/tsconfig.json +4 -1
- package/templates/b2b-support-web/web/package.json +1 -1
- package/templates/minimal-web/package.json +2 -1
- package/templates/minimal-web/tsconfig.json +3 -1
- package/templates/minimal-web/web/package.json +2 -2
- package/src/forge/_generated/actionSubscriptions.json +0 -2
- package/src/forge/_generated/actionSubscriptions.ts +0 -10
- package/src/forge/_generated/agentAdapterManifest.json +0 -2
- package/src/forge/_generated/agentAdapterManifest.ts +0 -73
- package/src/forge/_generated/agentContract.json +0 -2
- package/src/forge/_generated/agentContract.ts +0 -7696
- package/src/forge/_generated/agentQuickstart.md +0 -32
- package/src/forge/_generated/aiContext.ts +0 -59
- package/src/forge/_generated/aiModels.json +0 -2
- package/src/forge/_generated/aiModels.ts +0 -35
- package/src/forge/_generated/aiProviders.json +0 -2
- package/src/forge/_generated/aiProviders.ts +0 -23
- package/src/forge/_generated/aiRegistry.json +0 -2
- package/src/forge/_generated/aiRegistry.ts +0 -29
- package/src/forge/_generated/api.json +0 -2
- package/src/forge/_generated/api.ts +0 -8
- package/src/forge/_generated/appGraph.json +0 -2
- package/src/forge/_generated/appGraph.ts +0 -14667
- package/src/forge/_generated/appMap.md +0 -35
- package/src/forge/_generated/artifactManifest.json +0 -2
- package/src/forge/_generated/artifactManifest.ts +0 -7
- package/src/forge/_generated/authClaims.json +0 -2
- package/src/forge/_generated/authClaims.ts +0 -13
- package/src/forge/_generated/authConfig.json +0 -2
- package/src/forge/_generated/authConfig.ts +0 -17
- package/src/forge/_generated/authContext.ts +0 -23
- package/src/forge/_generated/authRegistry.json +0 -2
- package/src/forge/_generated/authRegistry.ts +0 -25
- package/src/forge/_generated/buildInfo.json +0 -2
- package/src/forge/_generated/buildInfo.ts +0 -9
- package/src/forge/_generated/capabilityMap.json +0 -2
- package/src/forge/_generated/capabilityMap.md +0 -15
- package/src/forge/_generated/capabilityMap.ts +0 -17
- package/src/forge/_generated/client.ts +0 -282
- package/src/forge/_generated/clientApi.ts +0 -9
- package/src/forge/_generated/clientManifest.json +0 -2
- package/src/forge/_generated/clientManifest.ts +0 -39
- package/src/forge/_generated/clientTypes.ts +0 -78
- package/src/forge/_generated/configRegistry.json +0 -2
- package/src/forge/_generated/configRegistry.ts +0 -4
- package/src/forge/_generated/dataGraph.json +0 -2
- package/src/forge/_generated/dataGraph.ts +0 -8
- package/src/forge/_generated/db.json +0 -2
- package/src/forge/_generated/db.ts +0 -2
- package/src/forge/_generated/dbSecurityManifest.json +0 -2
- package/src/forge/_generated/dbSecurityManifest.ts +0 -15
- package/src/forge/_generated/dbSessionContext.json +0 -2
- package/src/forge/_generated/dbSessionContext.ts +0 -39
- package/src/forge/_generated/deployManifest.json +0 -2
- package/src/forge/_generated/deployManifest.ts +0 -14
- package/src/forge/_generated/devManifest.json +0 -2
- package/src/forge/_generated/devManifest.ts +0 -47
- package/src/forge/_generated/envSchema.json +0 -2
- package/src/forge/_generated/envSchema.ts +0 -59
- package/src/forge/_generated/frontendGraph.json +0 -2
- package/src/forge/_generated/frontendGraph.ts +0 -27
- package/src/forge/_generated/importGuards.json +0 -2
- package/src/forge/_generated/importGuards.ts +0 -686
- package/src/forge/_generated/index.ts +0 -67
- package/src/forge/_generated/liveProductionManifest.json +0 -2
- package/src/forge/_generated/liveProductionManifest.ts +0 -23
- package/src/forge/_generated/liveProtocol.json +0 -2
- package/src/forge/_generated/liveProtocol.ts +0 -21
- package/src/forge/_generated/liveQueryRegistry.json +0 -2
- package/src/forge/_generated/liveQueryRegistry.ts +0 -9
- package/src/forge/_generated/liveTransportConfig.json +0 -2
- package/src/forge/_generated/liveTransportConfig.ts +0 -19
- package/src/forge/_generated/makeRegistry.json +0 -2
- package/src/forge/_generated/makeRegistry.ts +0 -163
- package/src/forge/_generated/makeTemplates.json +0 -2
- package/src/forge/_generated/makeTemplates.ts +0 -61
- package/src/forge/_generated/mockMap.json +0 -2
- package/src/forge/_generated/mockMap.ts +0 -7
- package/src/forge/_generated/operationPlaybooks.md +0 -147
- package/src/forge/_generated/packageGraph.json +0 -2
- package/src/forge/_generated/packageGraph.ts +0 -245249
- package/src/forge/_generated/packageUpgradeRegistry.json +0 -2
- package/src/forge/_generated/packageUpgradeRegistry.ts +0 -15
- package/src/forge/_generated/permissionMatrix.json +0 -2
- package/src/forge/_generated/permissionMatrix.ts +0 -7
- package/src/forge/_generated/policyRegistry.json +0 -2
- package/src/forge/_generated/policyRegistry.ts +0 -11
- package/src/forge/_generated/queryRegistry.json +0 -2
- package/src/forge/_generated/queryRegistry.ts +0 -9
- package/src/forge/_generated/react.d.ts +0 -22
- package/src/forge/_generated/react.ts +0 -29
- package/src/forge/_generated/reactManifest.json +0 -2
- package/src/forge/_generated/reactManifest.ts +0 -19
- package/src/forge/_generated/rlsPolicies.json +0 -2
- package/src/forge/_generated/rlsPolicies.sql +0 -34
- package/src/forge/_generated/rlsPolicies.ts +0 -6
- package/src/forge/_generated/runtimeGraph.json +0 -2
- package/src/forge/_generated/runtimeGraph.ts +0 -8
- package/src/forge/_generated/runtimeMatrix.json +0 -2
- package/src/forge/_generated/runtimeMatrix.ts +0 -327385
- package/src/forge/_generated/runtimeRegistry.ts +0 -2
- package/src/forge/_generated/runtimeRules.md +0 -79
- package/src/forge/_generated/secretRegistry.json +0 -2
- package/src/forge/_generated/secretRegistry.ts +0 -50
- package/src/forge/_generated/secretsContext.ts +0 -11
- package/src/forge/_generated/serverApi.ts +0 -10
- package/src/forge/_generated/sourceMapManifest.json +0 -2
- package/src/forge/_generated/sourceMapManifest.ts +0 -7
- package/src/forge/_generated/sqlPlan.json +0 -2
- package/src/forge/_generated/sqlPlan.ts +0 -88
- package/src/forge/_generated/subscriptionManifest.json +0 -2
- package/src/forge/_generated/subscriptionManifest.ts +0 -7
- package/src/forge/_generated/symbolicationManifest.json +0 -2
- package/src/forge/_generated/symbolicationManifest.ts +0 -17
- package/src/forge/_generated/telemetryRegistry.json +0 -2
- package/src/forge/_generated/telemetryRegistry.ts +0 -9
- package/src/forge/_generated/telemetrySinks.json +0 -2
- package/src/forge/_generated/telemetrySinks.ts +0 -11
- package/src/forge/_generated/tenantScope.json +0 -2
- package/src/forge/_generated/tenantScope.ts +0 -8
- package/src/forge/_generated/testGraph.json +0 -2
- package/src/forge/_generated/testGraph.ts +0 -3108
- package/src/forge/_generated/testPlanRegistry.json +0 -2
- package/src/forge/_generated/testPlanRegistry.ts +0 -33
- package/src/forge/_generated/uiRoutes.json +0 -2
- package/src/forge/_generated/uiRoutes.ts +0 -16
- package/src/forge/_generated/uiScenarios.json +0 -2
- package/src/forge/_generated/uiScenarios.ts +0 -30
- package/src/forge/_generated/uiTestManifest.json +0 -2
- package/src/forge/_generated/uiTestManifest.ts +0 -27
- package/src/forge/_generated/workflowRegistry.json +0 -2
- package/src/forge/_generated/workflowRegistry.ts +0 -9
- package/src/forge/_generated/workflowSubscriptions.json +0 -2
- package/src/forge/_generated/workflowSubscriptions.ts +0 -10
|
@@ -0,0 +1,1818 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
statSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
13
|
+
import ts from "typescript";
|
|
14
|
+
import { createDiagnostic } from "../compiler/diagnostics/create.ts";
|
|
15
|
+
import type { Diagnostic } from "../compiler/types/diagnostic.ts";
|
|
16
|
+
import { planMakeCommand } from "../make/index.ts";
|
|
17
|
+
import type { MakeCommandOptions, MakePrimitive } from "../make/types.ts";
|
|
18
|
+
import type {
|
|
19
|
+
CairActionResult,
|
|
20
|
+
CairActionStepResult,
|
|
21
|
+
CairFileChange,
|
|
22
|
+
CairObservation,
|
|
23
|
+
CairParsedAction,
|
|
24
|
+
CairSnapshot,
|
|
25
|
+
CairSymbolRef,
|
|
26
|
+
} from "./types.ts";
|
|
27
|
+
import { parseCairAction, parseCairActionScript } from "./action-parser.ts";
|
|
28
|
+
import { writeCairActionJournal, writeCairActionPlan } from "./action-journal.ts";
|
|
29
|
+
import { validateSemanticExpectations } from "./action-validator.ts";
|
|
30
|
+
export { splitCairActionScript } from "./action-parser.ts";
|
|
31
|
+
|
|
32
|
+
interface CairActionRunOptions {
|
|
33
|
+
workspaceRoot: string;
|
|
34
|
+
snapshot: CairSnapshot;
|
|
35
|
+
script: string;
|
|
36
|
+
dryRun: boolean;
|
|
37
|
+
plan: boolean;
|
|
38
|
+
allowGenerated: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ResolvedPath {
|
|
42
|
+
repoPath: string;
|
|
43
|
+
absolutePath: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ResolvedTextTarget {
|
|
47
|
+
path: ResolvedPath;
|
|
48
|
+
span?: { start: number; end: number };
|
|
49
|
+
symbolHash?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function observation(code: string, text: string, data?: Record<string, unknown>): CairObservation {
|
|
53
|
+
return data ? { code, text, data } : { code, text };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function actionError(message: string, file?: string): Diagnostic {
|
|
57
|
+
return createDiagnostic({
|
|
58
|
+
severity: "error",
|
|
59
|
+
code: "FORGE_CAIR_ACTION",
|
|
60
|
+
message,
|
|
61
|
+
...(file ? { file } : {}),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hashText(value: string): string {
|
|
66
|
+
return createHash("sha256").update(value).digest("hex");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hashMatches(actual: string, expected: string): boolean {
|
|
70
|
+
return actual === expected || actual.startsWith(expected);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function byteLength(value: string): number {
|
|
74
|
+
return Buffer.byteLength(value, "utf8");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeSlashes(value: string): string {
|
|
78
|
+
return value.replace(/\\/g, "/");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isGeneratedPath(repoPath: string): boolean {
|
|
82
|
+
return repoPath.split("/").includes("_generated");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveRepoPath(
|
|
86
|
+
workspaceRoot: string,
|
|
87
|
+
value: string,
|
|
88
|
+
allowGenerated: boolean,
|
|
89
|
+
): { path?: ResolvedPath; diagnostic?: Diagnostic } {
|
|
90
|
+
const normalized = normalizeSlashes(value.trim());
|
|
91
|
+
if (!normalized || normalized.includes("\0")) {
|
|
92
|
+
return { diagnostic: actionError("CAIR path is empty or invalid") };
|
|
93
|
+
}
|
|
94
|
+
if (isAbsolute(normalized) || /^[A-Za-z]:/.test(normalized)) {
|
|
95
|
+
return { diagnostic: actionError(`CAIR paths must be repo-relative: ${value}`) };
|
|
96
|
+
}
|
|
97
|
+
const absolutePath = resolve(workspaceRoot, normalized);
|
|
98
|
+
const repoPath = normalizeSlashes(relative(workspaceRoot, absolutePath));
|
|
99
|
+
if (!repoPath || repoPath === "." || repoPath.startsWith("../") || repoPath === "..") {
|
|
100
|
+
return { diagnostic: actionError(`CAIR path escapes the workspace: ${value}`) };
|
|
101
|
+
}
|
|
102
|
+
if (!allowGenerated && isGeneratedPath(repoPath)) {
|
|
103
|
+
return {
|
|
104
|
+
diagnostic: actionError(
|
|
105
|
+
`CAIR refuses to edit generated files without --include-generated: ${repoPath}`,
|
|
106
|
+
repoPath,
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return { path: { repoPath, absolutePath } };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveModulePath(snapshot: CairSnapshot, id: string): string | null {
|
|
114
|
+
return snapshot.lexicon.modules.find((module) => module.id === id)?.file ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveSymbolTarget(
|
|
118
|
+
snapshot: CairSnapshot,
|
|
119
|
+
id: string,
|
|
120
|
+
): { file: string; span: { start: number; end: number }; hash: string } | null {
|
|
121
|
+
const symbol = snapshot.lexicon.symbols.find((candidate) => candidate.id === id);
|
|
122
|
+
if (!symbol) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return { file: symbol.file, span: symbol.span, hash: symbol.hash };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveSymbolRef(snapshot: CairSnapshot, id: string | undefined): CairSymbolRef | null {
|
|
129
|
+
if (!id?.startsWith("S#")) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return snapshot.lexicon.symbols.find((candidate) => candidate.id === id) ?? null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolveActionPath(
|
|
136
|
+
action: CairParsedAction,
|
|
137
|
+
snapshot: CairSnapshot,
|
|
138
|
+
workspaceRoot: string,
|
|
139
|
+
allowGenerated: boolean,
|
|
140
|
+
): { path?: ResolvedPath; diagnostic?: Diagnostic } {
|
|
141
|
+
const value = action.args.path ?? action.args.file;
|
|
142
|
+
if (!value) {
|
|
143
|
+
return { diagnostic: actionError(`${action.verb} requires path=<repo-path> or file=<M#|repo-path>`) };
|
|
144
|
+
}
|
|
145
|
+
const repoPath = value.startsWith("M#") ? resolveModulePath(snapshot, value) : value;
|
|
146
|
+
if (!repoPath) {
|
|
147
|
+
return { diagnostic: actionError(`module reference not found: ${value}`) };
|
|
148
|
+
}
|
|
149
|
+
return resolveRepoPath(workspaceRoot, repoPath, allowGenerated);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resolveTextTarget(
|
|
153
|
+
action: CairParsedAction,
|
|
154
|
+
snapshot: CairSnapshot,
|
|
155
|
+
workspaceRoot: string,
|
|
156
|
+
allowGenerated: boolean,
|
|
157
|
+
): { target?: ResolvedTextTarget; diagnostic?: Diagnostic } {
|
|
158
|
+
const targetRef = action.args.target;
|
|
159
|
+
if (targetRef?.startsWith("S#")) {
|
|
160
|
+
const symbol = resolveSymbolTarget(snapshot, targetRef);
|
|
161
|
+
if (!symbol) {
|
|
162
|
+
return { diagnostic: actionError(`symbol reference not found: ${targetRef}`) };
|
|
163
|
+
}
|
|
164
|
+
const resolved = resolveRepoPath(workspaceRoot, symbol.file, allowGenerated);
|
|
165
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
166
|
+
return { diagnostic: resolved.diagnostic };
|
|
167
|
+
}
|
|
168
|
+
return { target: { path: resolved.path, span: symbol.span, symbolHash: symbol.hash } };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const resolved = resolveActionPath(action, snapshot, workspaceRoot, allowGenerated);
|
|
172
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
173
|
+
return { diagnostic: resolved.diagnostic };
|
|
174
|
+
}
|
|
175
|
+
const span = parseSpan(action.args.span);
|
|
176
|
+
if (action.args.span && !span) {
|
|
177
|
+
return { diagnostic: actionError(`invalid span '${action.args.span}'`, resolved.path.repoPath) };
|
|
178
|
+
}
|
|
179
|
+
return { target: { path: resolved.path, ...(span ? { span } : {}) } };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseSpan(value: string | undefined): { start: number; end: number } | null {
|
|
183
|
+
if (!value) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const match = /^(\d+):(\d+)$/.exec(value);
|
|
187
|
+
if (!match) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const start = Number(match[1]);
|
|
191
|
+
const end = Number(match[2]);
|
|
192
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
return { start, end };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function actionText(action: CairParsedAction): string | null {
|
|
199
|
+
return action.body ?? action.args.text ?? action.args.replacement ?? action.args.content ?? null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function symbolDeclaration(action: CairParsedAction): { declaration?: string; diagnostic?: Diagnostic } {
|
|
203
|
+
const name = action.args.name ?? action.args.symbol;
|
|
204
|
+
const kind = (action.args.kind ?? "function").toLowerCase();
|
|
205
|
+
const explicit = actionText(action);
|
|
206
|
+
if (explicit !== null) {
|
|
207
|
+
if (name && !explicit.includes(name)) {
|
|
208
|
+
return { diagnostic: actionError(`CREATE.SYMBOL body does not contain symbol name '${name}'`) };
|
|
209
|
+
}
|
|
210
|
+
return { declaration: explicit.trimEnd() };
|
|
211
|
+
}
|
|
212
|
+
if (!name) {
|
|
213
|
+
return { diagnostic: actionError("CREATE.SYMBOL requires name=<symbol> when no body is provided") };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const exported = action.args.export === "true" ? "export " : "";
|
|
217
|
+
switch (kind) {
|
|
218
|
+
case "function":
|
|
219
|
+
return { declaration: `${exported}function ${name}() {\n throw new Error("not implemented");\n}` };
|
|
220
|
+
case "const":
|
|
221
|
+
return { declaration: `${exported}const ${name} = undefined;` };
|
|
222
|
+
case "type":
|
|
223
|
+
return { declaration: `${exported}type ${name} = unknown;` };
|
|
224
|
+
case "interface":
|
|
225
|
+
return { declaration: `${exported}interface ${name} {\n}` };
|
|
226
|
+
case "class":
|
|
227
|
+
return { declaration: `${exported}class ${name} {\n}` };
|
|
228
|
+
default:
|
|
229
|
+
return { diagnostic: actionError(`unsupported CREATE.SYMBOL kind '${kind}'`) };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function ensureParentDir(path: string): void {
|
|
234
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function maybeWrite(path: string, content: string, dryRun: boolean): void {
|
|
238
|
+
if (dryRun) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
ensureParentDir(path);
|
|
242
|
+
writeFileSync(path, content, "utf8");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function createFileAction(
|
|
246
|
+
action: CairParsedAction,
|
|
247
|
+
snapshot: CairSnapshot,
|
|
248
|
+
workspaceRoot: string,
|
|
249
|
+
dryRun: boolean,
|
|
250
|
+
allowGenerated: boolean,
|
|
251
|
+
): CairActionStepResult {
|
|
252
|
+
const resolved = resolveActionPath(action, snapshot, workspaceRoot, allowGenerated);
|
|
253
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
254
|
+
return failedStep(action, dryRun, [resolved.diagnostic ?? actionError("failed to resolve path")]);
|
|
255
|
+
}
|
|
256
|
+
const content = actionText(action) ?? "";
|
|
257
|
+
const exists = existsSync(resolved.path.absolutePath);
|
|
258
|
+
const overwrite = action.args.overwrite === "true";
|
|
259
|
+
const ifMissing = action.args.ifmissing !== "false";
|
|
260
|
+
if (exists && !overwrite) {
|
|
261
|
+
if (ifMissing) {
|
|
262
|
+
const before = readFileSync(resolved.path.absolutePath, "utf8");
|
|
263
|
+
const changes: CairFileChange[] = [{
|
|
264
|
+
path: resolved.path.repoPath,
|
|
265
|
+
operation: "noop",
|
|
266
|
+
beforeHash: hashText(before),
|
|
267
|
+
bytesBefore: byteLength(before),
|
|
268
|
+
}];
|
|
269
|
+
return completedStep(action, dryRun, false, [
|
|
270
|
+
observation("O FILE.EXISTS", `path=${resolved.path.repoPath} action=noop`),
|
|
271
|
+
], changes, workspaceRoot);
|
|
272
|
+
}
|
|
273
|
+
return failedStep(action, dryRun, [actionError(`file already exists: ${resolved.path.repoPath}`, resolved.path.repoPath)]);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const before = exists ? readFileSync(resolved.path.absolutePath, "utf8") : "";
|
|
277
|
+
maybeWrite(resolved.path.absolutePath, content, dryRun);
|
|
278
|
+
const changes: CairFileChange[] = [{
|
|
279
|
+
path: resolved.path.repoPath,
|
|
280
|
+
operation: "create",
|
|
281
|
+
...(exists ? { beforeHash: hashText(before), bytesBefore: byteLength(before) } : {}),
|
|
282
|
+
afterHash: hashText(content),
|
|
283
|
+
bytesAfter: byteLength(content),
|
|
284
|
+
...(exists ? { beforeContent: before } : {}),
|
|
285
|
+
afterContent: content,
|
|
286
|
+
}];
|
|
287
|
+
return completedStep(action, dryRun, !dryRun, [
|
|
288
|
+
observation(
|
|
289
|
+
dryRun ? "O FILE.PLAN" : "O FILE.CREATED",
|
|
290
|
+
`path=${resolved.path.repoPath} bytes=${byteLength(content)}${dryRun ? " dryRun=true" : ""}`,
|
|
291
|
+
{ changes },
|
|
292
|
+
),
|
|
293
|
+
], changes, workspaceRoot);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function createSymbolAction(
|
|
297
|
+
action: CairParsedAction,
|
|
298
|
+
snapshot: CairSnapshot,
|
|
299
|
+
workspaceRoot: string,
|
|
300
|
+
dryRun: boolean,
|
|
301
|
+
allowGenerated: boolean,
|
|
302
|
+
): CairActionStepResult {
|
|
303
|
+
const afterRef = action.args.after;
|
|
304
|
+
const afterSymbol = afterRef?.startsWith("S#") ? resolveSymbolTarget(snapshot, afterRef) : null;
|
|
305
|
+
const pathValue = action.args.path ?? action.args.file ?? afterSymbol?.file;
|
|
306
|
+
if (!pathValue) {
|
|
307
|
+
return failedStep(action, dryRun, [actionError("CREATE.SYMBOL requires path=<repo-path>, file=<M#|repo-path>, or after=<S#>")]);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const pathAction: CairParsedAction = {
|
|
311
|
+
...action,
|
|
312
|
+
args: { ...action.args, path: pathValue },
|
|
313
|
+
};
|
|
314
|
+
const resolved = resolveActionPath(pathAction, snapshot, workspaceRoot, allowGenerated);
|
|
315
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
316
|
+
return failedStep(action, dryRun, [resolved.diagnostic ?? actionError("failed to resolve symbol target")]);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const declaration = symbolDeclaration(action);
|
|
320
|
+
if (declaration.diagnostic || declaration.declaration === undefined) {
|
|
321
|
+
return failedStep(action, dryRun, [declaration.diagnostic ?? actionError("failed to build symbol declaration", resolved.path.repoPath)]);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const createFile = action.args.createfile === "true" || action.args["create-file"] === "true";
|
|
325
|
+
const existed = existsSync(resolved.path.absolutePath);
|
|
326
|
+
if (!existed && !createFile) {
|
|
327
|
+
return failedStep(action, dryRun, [actionError(`file not found: ${resolved.path.repoPath}`, resolved.path.repoPath)]);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const current = existed
|
|
331
|
+
? readFileSync(resolved.path.absolutePath, "utf8")
|
|
332
|
+
: "";
|
|
333
|
+
const name = action.args.name ?? action.args.symbol;
|
|
334
|
+
if (name && current.includes(name) && action.args.overwrite !== "true") {
|
|
335
|
+
const changes: CairFileChange[] = [{
|
|
336
|
+
path: resolved.path.repoPath,
|
|
337
|
+
operation: "noop",
|
|
338
|
+
beforeHash: hashText(current),
|
|
339
|
+
bytesBefore: byteLength(current),
|
|
340
|
+
}];
|
|
341
|
+
return completedStep(action, dryRun, false, [
|
|
342
|
+
observation("O SYMBOL.EXISTS", `path=${resolved.path.repoPath} name=${name} action=noop`),
|
|
343
|
+
], changes, workspaceRoot);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let insertAt = current.length;
|
|
347
|
+
if (afterRef?.startsWith("S#")) {
|
|
348
|
+
if (!afterSymbol) {
|
|
349
|
+
return failedStep(action, dryRun, [actionError(`symbol reference not found: ${afterRef}`)]);
|
|
350
|
+
}
|
|
351
|
+
if (normalizeSlashes(afterSymbol.file) !== resolved.path.repoPath) {
|
|
352
|
+
return failedStep(action, dryRun, [actionError(`after=${afterRef} is in ${afterSymbol.file}, not ${resolved.path.repoPath}`, resolved.path.repoPath)]);
|
|
353
|
+
}
|
|
354
|
+
insertAt = afterSymbol.span.end;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const prefix = current.slice(0, insertAt);
|
|
358
|
+
const suffix = current.slice(insertAt);
|
|
359
|
+
const beforeGap = prefix.length === 0 || prefix.endsWith("\n\n") ? "" : prefix.endsWith("\n") ? "\n" : "\n\n";
|
|
360
|
+
const afterGap = suffix.length === 0 || suffix.startsWith("\n") ? "\n" : "\n\n";
|
|
361
|
+
const next = `${prefix}${beforeGap}${declaration.declaration}${afterGap}${suffix}`;
|
|
362
|
+
maybeWrite(resolved.path.absolutePath, next, dryRun);
|
|
363
|
+
const changes: CairFileChange[] = [{
|
|
364
|
+
path: resolved.path.repoPath,
|
|
365
|
+
operation: existed ? "insert" : "create",
|
|
366
|
+
beforeHash: hashText(current),
|
|
367
|
+
afterHash: hashText(next),
|
|
368
|
+
bytesBefore: byteLength(current),
|
|
369
|
+
bytesAfter: byteLength(next),
|
|
370
|
+
beforeContent: current,
|
|
371
|
+
afterContent: next,
|
|
372
|
+
}];
|
|
373
|
+
return completedStep(action, dryRun, !dryRun, [
|
|
374
|
+
observation(
|
|
375
|
+
dryRun ? "O SYMBOL.PLAN" : "O SYMBOL.CREATED",
|
|
376
|
+
[
|
|
377
|
+
`path=${resolved.path.repoPath}`,
|
|
378
|
+
name ? `name=${name}` : null,
|
|
379
|
+
`kind=${action.args.kind ?? "body"}`,
|
|
380
|
+
afterRef ? `after=${afterRef}` : null,
|
|
381
|
+
dryRun ? "dryRun=true" : null,
|
|
382
|
+
].filter(Boolean).join(" "),
|
|
383
|
+
{ changes },
|
|
384
|
+
),
|
|
385
|
+
], changes, workspaceRoot);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function patchAction(
|
|
389
|
+
action: CairParsedAction,
|
|
390
|
+
snapshot: CairSnapshot,
|
|
391
|
+
workspaceRoot: string,
|
|
392
|
+
dryRun: boolean,
|
|
393
|
+
allowGenerated: boolean,
|
|
394
|
+
): CairActionStepResult {
|
|
395
|
+
const resolved = resolveTextTarget(action, snapshot, workspaceRoot, allowGenerated);
|
|
396
|
+
if (resolved.diagnostic || !resolved.target) {
|
|
397
|
+
return failedStep(action, dryRun, [resolved.diagnostic ?? actionError("failed to resolve patch target")]);
|
|
398
|
+
}
|
|
399
|
+
if (!existsSync(resolved.target.path.absolutePath)) {
|
|
400
|
+
return failedStep(action, dryRun, [actionError(`file not found: ${resolved.target.path.repoPath}`, resolved.target.path.repoPath)]);
|
|
401
|
+
}
|
|
402
|
+
const replacement = actionText(action);
|
|
403
|
+
if (replacement === null) {
|
|
404
|
+
return failedStep(action, dryRun, [actionError("PATCH requires replacement text or a <<CODE body", resolved.target.path.repoPath)]);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const current = readFileSync(resolved.target.path.absolutePath, "utf8");
|
|
408
|
+
const span = resolved.target.span ?? { start: 0, end: current.length };
|
|
409
|
+
if (span.end > current.length) {
|
|
410
|
+
return failedStep(action, dryRun, [actionError(`span ${span.start}:${span.end} exceeds file length`, resolved.target.path.repoPath)]);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const expectHash = action.args["expect.hash"];
|
|
414
|
+
if (expectHash && resolved.target.symbolHash && resolved.target.symbolHash !== expectHash) {
|
|
415
|
+
return failedStep(action, dryRun, [
|
|
416
|
+
actionError(`symbol hash mismatch for ${action.args.target}: expected ${expectHash}, got ${resolved.target.symbolHash}`, resolved.target.path.repoPath),
|
|
417
|
+
]);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const selected = current.slice(span.start, span.end);
|
|
421
|
+
const selectedHash = hashText(selected);
|
|
422
|
+
const guardHash = action.args.hash;
|
|
423
|
+
if (!guardHash) {
|
|
424
|
+
return failedStep(action, dryRun, [actionError("PATCH requires hash=<sha256-prefix-of-current-span>", resolved.target.path.repoPath)]);
|
|
425
|
+
}
|
|
426
|
+
if (!hashMatches(selectedHash, guardHash)) {
|
|
427
|
+
return failedStep(action, dryRun, [
|
|
428
|
+
actionError(`patch hash mismatch: expected ${guardHash}, got ${selectedHash}`, resolved.target.path.repoPath),
|
|
429
|
+
]);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const next = `${current.slice(0, span.start)}${replacement}${current.slice(span.end)}`;
|
|
433
|
+
maybeWrite(resolved.target.path.absolutePath, next, dryRun);
|
|
434
|
+
const changes: CairFileChange[] = [{
|
|
435
|
+
path: resolved.target.path.repoPath,
|
|
436
|
+
operation: "patch",
|
|
437
|
+
beforeHash: hashText(current),
|
|
438
|
+
afterHash: hashText(next),
|
|
439
|
+
bytesBefore: byteLength(current),
|
|
440
|
+
bytesAfter: byteLength(next),
|
|
441
|
+
beforeContent: current,
|
|
442
|
+
afterContent: next,
|
|
443
|
+
}];
|
|
444
|
+
return completedStep(action, dryRun, !dryRun, [
|
|
445
|
+
observation(
|
|
446
|
+
dryRun ? "O PATCH.PLAN" : "O PATCH.APPLIED",
|
|
447
|
+
[
|
|
448
|
+
`path=${resolved.target.path.repoPath}`,
|
|
449
|
+
`span=${span.start}:${span.end}`,
|
|
450
|
+
`before=${selectedHash.slice(0, 12)}`,
|
|
451
|
+
`after=${hashText(replacement).slice(0, 12)}`,
|
|
452
|
+
dryRun ? "dryRun=true" : null,
|
|
453
|
+
].filter(Boolean).join(" "),
|
|
454
|
+
{ changes },
|
|
455
|
+
),
|
|
456
|
+
], changes, workspaceRoot);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function formattedNamedImport(symbol: string, from: string, isType: boolean): string {
|
|
460
|
+
return `import ${isType ? "type " : ""}{ ${symbol} } from "${from}";`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function formattedNamedExport(symbol: string, from: string): string {
|
|
464
|
+
return `export { ${symbol} } from "${from}";`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function addImportAction(
|
|
468
|
+
action: CairParsedAction,
|
|
469
|
+
snapshot: CairSnapshot,
|
|
470
|
+
workspaceRoot: string,
|
|
471
|
+
dryRun: boolean,
|
|
472
|
+
allowGenerated: boolean,
|
|
473
|
+
): CairActionStepResult {
|
|
474
|
+
const resolved = resolveActionPath(action, snapshot, workspaceRoot, allowGenerated);
|
|
475
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
476
|
+
return failedStep(action, dryRun, [resolved.diagnostic ?? actionError("failed to resolve import target")]);
|
|
477
|
+
}
|
|
478
|
+
const symbol = action.args.symbol ?? action.args.name;
|
|
479
|
+
const from = action.args.from;
|
|
480
|
+
if (!symbol || !from) {
|
|
481
|
+
return failedStep(action, dryRun, [actionError("ADD.IMPORT requires symbol=<name> and from=<specifier>", resolved.path.repoPath)]);
|
|
482
|
+
}
|
|
483
|
+
if (!existsSync(resolved.path.absolutePath)) {
|
|
484
|
+
return failedStep(action, dryRun, [actionError(`file not found: ${resolved.path.repoPath}`, resolved.path.repoPath)]);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const current = readFileSync(resolved.path.absolutePath, "utf8");
|
|
488
|
+
const importLine = formattedNamedImport(symbol, from, action.args.type === "true");
|
|
489
|
+
if (current.includes(importLine)) {
|
|
490
|
+
const changes: CairFileChange[] = [{
|
|
491
|
+
path: resolved.path.repoPath,
|
|
492
|
+
operation: "noop",
|
|
493
|
+
beforeHash: hashText(current),
|
|
494
|
+
bytesBefore: byteLength(current),
|
|
495
|
+
}];
|
|
496
|
+
return completedStep(action, dryRun, false, [
|
|
497
|
+
observation("O IMPORT.EXISTS", `path=${resolved.path.repoPath} symbol=${symbol} from=${from} action=noop`),
|
|
498
|
+
], changes, workspaceRoot);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const lines = current.split("\n");
|
|
502
|
+
let insertAt = 0;
|
|
503
|
+
if (lines[0]?.startsWith("#!")) {
|
|
504
|
+
insertAt = 1;
|
|
505
|
+
}
|
|
506
|
+
for (let index = insertAt; index < lines.length; index++) {
|
|
507
|
+
const trimmed = lines[index]?.trim() ?? "";
|
|
508
|
+
if (trimmed.startsWith("import ")) {
|
|
509
|
+
insertAt = index + 1;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (!trimmed) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
lines.splice(insertAt, 0, importLine);
|
|
518
|
+
const next = lines.join("\n");
|
|
519
|
+
maybeWrite(resolved.path.absolutePath, next, dryRun);
|
|
520
|
+
const changes: CairFileChange[] = [{
|
|
521
|
+
path: resolved.path.repoPath,
|
|
522
|
+
operation: "insert",
|
|
523
|
+
beforeHash: hashText(current),
|
|
524
|
+
afterHash: hashText(next),
|
|
525
|
+
bytesBefore: byteLength(current),
|
|
526
|
+
bytesAfter: byteLength(next),
|
|
527
|
+
beforeContent: current,
|
|
528
|
+
afterContent: next,
|
|
529
|
+
}];
|
|
530
|
+
return completedStep(action, dryRun, !dryRun, [
|
|
531
|
+
observation(
|
|
532
|
+
dryRun ? "O IMPORT.PLAN" : "O IMPORT.ADDED",
|
|
533
|
+
`path=${resolved.path.repoPath} symbol=${symbol} from=${from}${dryRun ? " dryRun=true" : ""}`,
|
|
534
|
+
{ changes },
|
|
535
|
+
),
|
|
536
|
+
], changes, workspaceRoot);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function addExportAction(
|
|
540
|
+
action: CairParsedAction,
|
|
541
|
+
snapshot: CairSnapshot,
|
|
542
|
+
workspaceRoot: string,
|
|
543
|
+
dryRun: boolean,
|
|
544
|
+
allowGenerated: boolean,
|
|
545
|
+
): CairActionStepResult {
|
|
546
|
+
const resolved = resolveActionPath(action, snapshot, workspaceRoot, allowGenerated);
|
|
547
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
548
|
+
return failedStep(action, dryRun, [resolved.diagnostic ?? actionError("failed to resolve export target")]);
|
|
549
|
+
}
|
|
550
|
+
const symbol = action.args.symbol ?? action.args.name;
|
|
551
|
+
const from = action.args.from;
|
|
552
|
+
if (!symbol || !from) {
|
|
553
|
+
return failedStep(action, dryRun, [actionError("ADD.EXPORT requires symbol=<name> and from=<specifier>", resolved.path.repoPath)]);
|
|
554
|
+
}
|
|
555
|
+
if (!existsSync(resolved.path.absolutePath)) {
|
|
556
|
+
return failedStep(action, dryRun, [actionError(`file not found: ${resolved.path.repoPath}`, resolved.path.repoPath)]);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const current = readFileSync(resolved.path.absolutePath, "utf8");
|
|
560
|
+
const exportLine = formattedNamedExport(symbol, from);
|
|
561
|
+
if (current.includes(exportLine)) {
|
|
562
|
+
const changes: CairFileChange[] = [{
|
|
563
|
+
path: resolved.path.repoPath,
|
|
564
|
+
operation: "noop",
|
|
565
|
+
beforeHash: hashText(current),
|
|
566
|
+
bytesBefore: byteLength(current),
|
|
567
|
+
}];
|
|
568
|
+
return completedStep(action, dryRun, false, [
|
|
569
|
+
observation("O EXPORT.EXISTS", `path=${resolved.path.repoPath} symbol=${symbol} from=${from} action=noop`),
|
|
570
|
+
], changes, workspaceRoot);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const separator = current.endsWith("\n") || current.length === 0 ? "" : "\n";
|
|
574
|
+
const next = `${current}${separator}${exportLine}\n`;
|
|
575
|
+
maybeWrite(resolved.path.absolutePath, next, dryRun);
|
|
576
|
+
const changes: CairFileChange[] = [{
|
|
577
|
+
path: resolved.path.repoPath,
|
|
578
|
+
operation: "append",
|
|
579
|
+
beforeHash: hashText(current),
|
|
580
|
+
afterHash: hashText(next),
|
|
581
|
+
bytesBefore: byteLength(current),
|
|
582
|
+
bytesAfter: byteLength(next),
|
|
583
|
+
beforeContent: current,
|
|
584
|
+
afterContent: next,
|
|
585
|
+
}];
|
|
586
|
+
return completedStep(action, dryRun, !dryRun, [
|
|
587
|
+
observation(
|
|
588
|
+
dryRun ? "O EXPORT.PLAN" : "O EXPORT.ADDED",
|
|
589
|
+
`path=${resolved.path.repoPath} symbol=${symbol} from=${from}${dryRun ? " dryRun=true" : ""}`,
|
|
590
|
+
{ changes },
|
|
591
|
+
),
|
|
592
|
+
], changes, workspaceRoot);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function readTsConfig(workspaceRoot: string, snapshot: CairSnapshot): {
|
|
596
|
+
rootNames: string[];
|
|
597
|
+
options: ts.CompilerOptions;
|
|
598
|
+
} {
|
|
599
|
+
const configPath = ts.findConfigFile(workspaceRoot, ts.sys.fileExists, "tsconfig.json");
|
|
600
|
+
if (!configPath) {
|
|
601
|
+
return {
|
|
602
|
+
rootNames: snapshot.lexicon.modules
|
|
603
|
+
.map((module) => resolve(workspaceRoot, module.file))
|
|
604
|
+
.filter((file) => existsSync(file)),
|
|
605
|
+
options: {
|
|
606
|
+
target: ts.ScriptTarget.ES2022,
|
|
607
|
+
module: ts.ModuleKind.ESNext,
|
|
608
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
609
|
+
allowImportingTsExtensions: true,
|
|
610
|
+
strict: true,
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const config = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
615
|
+
if (config.error) {
|
|
616
|
+
return { rootNames: [], options: {} };
|
|
617
|
+
}
|
|
618
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
619
|
+
config.config,
|
|
620
|
+
ts.sys,
|
|
621
|
+
dirname(configPath),
|
|
622
|
+
);
|
|
623
|
+
return { rootNames: parsed.fileNames, options: parsed.options };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function buildLanguageService(workspaceRoot: string, snapshot: CairSnapshot): ts.LanguageService {
|
|
627
|
+
const config = readTsConfig(workspaceRoot, snapshot);
|
|
628
|
+
const rootNames = new Set(config.rootNames.map((file) => resolve(file)));
|
|
629
|
+
for (const module of snapshot.lexicon.modules) {
|
|
630
|
+
const absolute = resolve(workspaceRoot, module.file);
|
|
631
|
+
if (existsSync(absolute)) {
|
|
632
|
+
rootNames.add(absolute);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const versions = new Map<string, string>();
|
|
636
|
+
const host: ts.LanguageServiceHost = {
|
|
637
|
+
getScriptFileNames: () => [...rootNames],
|
|
638
|
+
getScriptVersion: (fileName) => versions.get(resolve(fileName)) ?? "0",
|
|
639
|
+
getScriptSnapshot: (fileName) => {
|
|
640
|
+
if (!existsSync(fileName)) {
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
return ts.ScriptSnapshot.fromString(readFileSync(fileName, "utf8"));
|
|
644
|
+
},
|
|
645
|
+
getCurrentDirectory: () => workspaceRoot,
|
|
646
|
+
getCompilationSettings: () => config.options,
|
|
647
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
648
|
+
fileExists: ts.sys.fileExists,
|
|
649
|
+
readFile: ts.sys.readFile,
|
|
650
|
+
readDirectory: ts.sys.readDirectory,
|
|
651
|
+
directoryExists: ts.sys.directoryExists,
|
|
652
|
+
getDirectories: ts.sys.getDirectories,
|
|
653
|
+
};
|
|
654
|
+
return ts.createLanguageService(host);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function symbolNamePosition(symbol: CairSymbolRef, source: string): number {
|
|
658
|
+
const selected = source.slice(symbol.span.start, symbol.span.end);
|
|
659
|
+
const local = selected.indexOf(symbol.name);
|
|
660
|
+
return local >= 0 ? symbol.span.start + local : symbol.span.start;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function applyFileRewrites(
|
|
664
|
+
action: CairParsedAction,
|
|
665
|
+
workspaceRoot: string,
|
|
666
|
+
dryRun: boolean,
|
|
667
|
+
rewrites: Array<{ absolutePath: string; next: string }>,
|
|
668
|
+
code: string,
|
|
669
|
+
): CairActionStepResult {
|
|
670
|
+
const changes: CairFileChange[] = [];
|
|
671
|
+
for (const rewrite of rewrites) {
|
|
672
|
+
const repoPath = normalizeSlashes(relative(workspaceRoot, rewrite.absolutePath));
|
|
673
|
+
const resolved = resolveRepoPath(workspaceRoot, repoPath, action.args["allow-generated"] === "true");
|
|
674
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
675
|
+
return failedStep(action, dryRun, [resolved.diagnostic ?? actionError(`failed to resolve rewrite target ${repoPath}`)]);
|
|
676
|
+
}
|
|
677
|
+
const current = existsSync(resolved.path.absolutePath)
|
|
678
|
+
? readFileSync(resolved.path.absolutePath, "utf8")
|
|
679
|
+
: "";
|
|
680
|
+
if (current === rewrite.next) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
maybeWrite(resolved.path.absolutePath, rewrite.next, dryRun);
|
|
684
|
+
changes.push({
|
|
685
|
+
path: resolved.path.repoPath,
|
|
686
|
+
operation: "patch",
|
|
687
|
+
beforeHash: hashText(current),
|
|
688
|
+
afterHash: hashText(rewrite.next),
|
|
689
|
+
bytesBefore: byteLength(current),
|
|
690
|
+
bytesAfter: byteLength(rewrite.next),
|
|
691
|
+
beforeContent: current,
|
|
692
|
+
afterContent: rewrite.next,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
return completedStep(action, dryRun, !dryRun && changes.length > 0, [
|
|
696
|
+
observation(
|
|
697
|
+
dryRun ? `${code}.PLAN` : `${code}.APPLIED`,
|
|
698
|
+
`changes=${changes.length}${dryRun ? " dryRun=true" : ""}`,
|
|
699
|
+
{ changes },
|
|
700
|
+
),
|
|
701
|
+
], changes, workspaceRoot);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function applyTextChangesToContent(text: string, changes: readonly ts.TextChange[]): string {
|
|
705
|
+
let next = text;
|
|
706
|
+
for (const change of [...changes].sort((left, right) => right.span.start - left.span.start)) {
|
|
707
|
+
next = `${next.slice(0, change.span.start)}${change.newText}${next.slice(change.span.start + change.span.length)}`;
|
|
708
|
+
}
|
|
709
|
+
return next;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function applyLanguageServiceChanges(
|
|
713
|
+
action: CairParsedAction,
|
|
714
|
+
workspaceRoot: string,
|
|
715
|
+
dryRun: boolean,
|
|
716
|
+
changesByFile: Map<string, readonly ts.TextChange[]>,
|
|
717
|
+
code: string,
|
|
718
|
+
): CairActionStepResult {
|
|
719
|
+
const rewrites: Array<{ absolutePath: string; next: string }> = [];
|
|
720
|
+
for (const [fileName, changes] of changesByFile) {
|
|
721
|
+
const absolutePath = resolve(fileName);
|
|
722
|
+
if (!existsSync(absolutePath) || changes.length === 0) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
rewrites.push({
|
|
726
|
+
absolutePath,
|
|
727
|
+
next: applyTextChangesToContent(readFileSync(absolutePath, "utf8"), changes),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
return applyFileRewrites(action, workspaceRoot, dryRun, rewrites, code);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function renameSymbolAction(
|
|
734
|
+
action: CairParsedAction,
|
|
735
|
+
snapshot: CairSnapshot,
|
|
736
|
+
workspaceRoot: string,
|
|
737
|
+
dryRun: boolean,
|
|
738
|
+
): CairActionStepResult {
|
|
739
|
+
const symbol = resolveSymbolRef(snapshot, action.args.target);
|
|
740
|
+
if (!symbol) {
|
|
741
|
+
return failedStep(action, dryRun, [actionError("RENAME.SYMBOL requires target=<S#>")]);
|
|
742
|
+
}
|
|
743
|
+
const expectationErrors = validateSemanticExpectations(action, symbol);
|
|
744
|
+
if (expectationErrors.length > 0) {
|
|
745
|
+
return failedStep(action, dryRun, expectationErrors);
|
|
746
|
+
}
|
|
747
|
+
const newName = action.args.newname ?? action.args.name;
|
|
748
|
+
if (!newName) {
|
|
749
|
+
return failedStep(action, dryRun, [actionError("RENAME.SYMBOL requires newName=<identifier>", symbol.file)]);
|
|
750
|
+
}
|
|
751
|
+
const absolute = resolve(workspaceRoot, symbol.file);
|
|
752
|
+
if (!existsSync(absolute)) {
|
|
753
|
+
return failedStep(action, dryRun, [actionError(`file not found: ${symbol.file}`, symbol.file)]);
|
|
754
|
+
}
|
|
755
|
+
const service = buildLanguageService(workspaceRoot, snapshot);
|
|
756
|
+
const position = symbolNamePosition(symbol, readFileSync(absolute, "utf8"));
|
|
757
|
+
const locations = service.findRenameLocations(absolute, position, false, false, { providePrefixAndSuffixTextForRename: false }) ?? [];
|
|
758
|
+
if (locations.length === 0) {
|
|
759
|
+
return failedStep(action, dryRun, [actionError(`no rename locations found for ${symbol.id}`, symbol.file)]);
|
|
760
|
+
}
|
|
761
|
+
const changesByFile = new Map<string, ts.TextChange[]>();
|
|
762
|
+
for (const location of locations) {
|
|
763
|
+
const existing = changesByFile.get(location.fileName) ?? [];
|
|
764
|
+
existing.push({ span: location.textSpan, newText: newName });
|
|
765
|
+
changesByFile.set(location.fileName, existing);
|
|
766
|
+
}
|
|
767
|
+
return applyLanguageServiceChanges(action, workspaceRoot, dryRun, changesByFile, "O RENAME");
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function moveSymbolAction(
|
|
771
|
+
action: CairParsedAction,
|
|
772
|
+
snapshot: CairSnapshot,
|
|
773
|
+
workspaceRoot: string,
|
|
774
|
+
dryRun: boolean,
|
|
775
|
+
allowGenerated: boolean,
|
|
776
|
+
): CairActionStepResult {
|
|
777
|
+
const symbol = resolveSymbolRef(snapshot, action.args.target);
|
|
778
|
+
if (!symbol) {
|
|
779
|
+
return failedStep(action, dryRun, [actionError("MOVE.SYMBOL requires target=<S#>")]);
|
|
780
|
+
}
|
|
781
|
+
const expectationErrors = validateSemanticExpectations(action, symbol);
|
|
782
|
+
if (expectationErrors.length > 0) {
|
|
783
|
+
return failedStep(action, dryRun, expectationErrors);
|
|
784
|
+
}
|
|
785
|
+
const source = resolveRepoPath(workspaceRoot, symbol.file, allowGenerated);
|
|
786
|
+
const destinationRef = action.args.to ?? action.args.file ?? action.args.path;
|
|
787
|
+
if (!destinationRef) {
|
|
788
|
+
return failedStep(action, dryRun, [actionError("MOVE.SYMBOL requires to=<M#|repo-path>", symbol.file)]);
|
|
789
|
+
}
|
|
790
|
+
const destinationPath = destinationRef.startsWith("M#")
|
|
791
|
+
? resolveModulePath(snapshot, destinationRef)
|
|
792
|
+
: destinationRef;
|
|
793
|
+
if (!destinationPath) {
|
|
794
|
+
return failedStep(action, dryRun, [actionError(`module reference not found: ${destinationRef}`)]);
|
|
795
|
+
}
|
|
796
|
+
const destination = resolveRepoPath(workspaceRoot, destinationPath, allowGenerated);
|
|
797
|
+
if (source.diagnostic || !source.path || destination.diagnostic || !destination.path) {
|
|
798
|
+
return failedStep(action, dryRun, [
|
|
799
|
+
source.diagnostic ?? destination.diagnostic ?? actionError("failed to resolve MOVE.SYMBOL paths"),
|
|
800
|
+
]);
|
|
801
|
+
}
|
|
802
|
+
const sourceText = readFileSync(source.path.absolutePath, "utf8");
|
|
803
|
+
const declaration = sourceText.slice(symbol.span.start, symbol.span.end).trim();
|
|
804
|
+
const nextSource = `${sourceText.slice(0, symbol.span.start)}${sourceText.slice(symbol.span.end)}`.replace(/\n{3,}/g, "\n\n");
|
|
805
|
+
const destinationText = existsSync(destination.path.absolutePath)
|
|
806
|
+
? readFileSync(destination.path.absolutePath, "utf8")
|
|
807
|
+
: "";
|
|
808
|
+
const nextDestination = `${destinationText}${destinationText.endsWith("\n") || !destinationText ? "" : "\n"}${declaration}\n`;
|
|
809
|
+
return applyFileRewrites(action, workspaceRoot, dryRun, [
|
|
810
|
+
{ absolutePath: source.path.absolutePath, next: nextSource },
|
|
811
|
+
{ absolutePath: destination.path.absolutePath, next: nextDestination },
|
|
812
|
+
], "O MOVE");
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function updateSignatureAction(
|
|
816
|
+
action: CairParsedAction,
|
|
817
|
+
snapshot: CairSnapshot,
|
|
818
|
+
workspaceRoot: string,
|
|
819
|
+
dryRun: boolean,
|
|
820
|
+
allowGenerated: boolean,
|
|
821
|
+
): CairActionStepResult {
|
|
822
|
+
const symbol = resolveSymbolRef(snapshot, action.args.target);
|
|
823
|
+
if (!symbol) {
|
|
824
|
+
return failedStep(action, dryRun, [actionError("UPDATE.SIGNATURE requires target=<S#>")]);
|
|
825
|
+
}
|
|
826
|
+
const expectationErrors = validateSemanticExpectations(action, symbol);
|
|
827
|
+
if (expectationErrors.length > 0) {
|
|
828
|
+
return failedStep(action, dryRun, expectationErrors);
|
|
829
|
+
}
|
|
830
|
+
const signature = action.args.signature ?? action.body;
|
|
831
|
+
if (!signature) {
|
|
832
|
+
return failedStep(action, dryRun, [actionError("UPDATE.SIGNATURE requires signature=<text> or body", symbol.file)]);
|
|
833
|
+
}
|
|
834
|
+
const resolved = resolveRepoPath(workspaceRoot, symbol.file, allowGenerated);
|
|
835
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
836
|
+
return failedStep(action, dryRun, [resolved.diagnostic ?? actionError("failed to resolve signature target")]);
|
|
837
|
+
}
|
|
838
|
+
const current = readFileSync(resolved.path.absolutePath, "utf8");
|
|
839
|
+
const selected = current.slice(symbol.span.start, symbol.span.end);
|
|
840
|
+
const braceIndex = selected.indexOf("{");
|
|
841
|
+
if (braceIndex < 0) {
|
|
842
|
+
return failedStep(action, dryRun, [actionError("UPDATE.SIGNATURE only supports block declarations", symbol.file)]);
|
|
843
|
+
}
|
|
844
|
+
const replacement = `${signature.trim()} `;
|
|
845
|
+
const next = `${current.slice(0, symbol.span.start)}${replacement}${selected.slice(braceIndex)}${current.slice(symbol.span.end)}`;
|
|
846
|
+
return applyFileRewrites(action, workspaceRoot, dryRun, [{ absolutePath: resolved.path.absolutePath, next }], "O SIGNATURE");
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function addParamAction(
|
|
850
|
+
action: CairParsedAction,
|
|
851
|
+
snapshot: CairSnapshot,
|
|
852
|
+
workspaceRoot: string,
|
|
853
|
+
dryRun: boolean,
|
|
854
|
+
allowGenerated: boolean,
|
|
855
|
+
): CairActionStepResult {
|
|
856
|
+
const symbol = resolveSymbolRef(snapshot, action.args.target);
|
|
857
|
+
if (!symbol) {
|
|
858
|
+
return failedStep(action, dryRun, [actionError("ADD.PARAM requires target=<S#>")]);
|
|
859
|
+
}
|
|
860
|
+
const expectationErrors = validateSemanticExpectations(action, symbol);
|
|
861
|
+
if (expectationErrors.length > 0) {
|
|
862
|
+
return failedStep(action, dryRun, expectationErrors);
|
|
863
|
+
}
|
|
864
|
+
const name = action.args.name;
|
|
865
|
+
const type = action.args.type ?? "unknown";
|
|
866
|
+
if (!name) {
|
|
867
|
+
return failedStep(action, dryRun, [actionError("ADD.PARAM requires name=<param>", symbol.file)]);
|
|
868
|
+
}
|
|
869
|
+
const defaultValue = action.args.default;
|
|
870
|
+
const param = `${name}: ${type}${defaultValue ? ` = ${defaultValue}` : ""}`;
|
|
871
|
+
const resolved = resolveRepoPath(workspaceRoot, symbol.file, allowGenerated);
|
|
872
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
873
|
+
return failedStep(action, dryRun, [resolved.diagnostic ?? actionError("failed to resolve parameter target")]);
|
|
874
|
+
}
|
|
875
|
+
const current = readFileSync(resolved.path.absolutePath, "utf8");
|
|
876
|
+
const selected = current.slice(symbol.span.start, symbol.span.end);
|
|
877
|
+
const open = selected.indexOf("(");
|
|
878
|
+
const close = selected.indexOf(")", open + 1);
|
|
879
|
+
if (open < 0 || close < 0) {
|
|
880
|
+
return failedStep(action, dryRun, [actionError("ADD.PARAM only supports declarations with parameter lists", symbol.file)]);
|
|
881
|
+
}
|
|
882
|
+
const existing = selected.slice(open + 1, close).trim();
|
|
883
|
+
const nextParams = existing ? `${existing}, ${param}` : param;
|
|
884
|
+
const nextSelected = `${selected.slice(0, open + 1)}${nextParams}${selected.slice(close)}`;
|
|
885
|
+
const next = `${current.slice(0, symbol.span.start)}${nextSelected}${current.slice(symbol.span.end)}`;
|
|
886
|
+
return applyFileRewrites(action, workspaceRoot, dryRun, [{ absolutePath: resolved.path.absolutePath, next }], "O PARAM");
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function findMatchingParen(text: string, openIndex: number): number {
|
|
890
|
+
let depth = 0;
|
|
891
|
+
let quote: "'" | "\"" | "`" | null = null;
|
|
892
|
+
let escaped = false;
|
|
893
|
+
for (let index = openIndex; index < text.length; index++) {
|
|
894
|
+
const char = text[index];
|
|
895
|
+
if (escaped) {
|
|
896
|
+
escaped = false;
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
if (char === "\\") {
|
|
900
|
+
escaped = true;
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (quote) {
|
|
904
|
+
if (char === quote) {
|
|
905
|
+
quote = null;
|
|
906
|
+
}
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (char === "'" || char === "\"" || char === "`") {
|
|
910
|
+
quote = char;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
if (char === "(") {
|
|
914
|
+
depth += 1;
|
|
915
|
+
} else if (char === ")") {
|
|
916
|
+
depth -= 1;
|
|
917
|
+
if (depth === 0) {
|
|
918
|
+
return index;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return -1;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function updateCallsitesAction(
|
|
926
|
+
action: CairParsedAction,
|
|
927
|
+
snapshot: CairSnapshot,
|
|
928
|
+
workspaceRoot: string,
|
|
929
|
+
dryRun: boolean,
|
|
930
|
+
): CairActionStepResult {
|
|
931
|
+
const symbol = resolveSymbolRef(snapshot, action.args.target);
|
|
932
|
+
if (!symbol) {
|
|
933
|
+
return failedStep(action, dryRun, [actionError("UPDATE.CALLSITES requires target=<S#>")]);
|
|
934
|
+
}
|
|
935
|
+
const expectationErrors = validateSemanticExpectations(action, symbol);
|
|
936
|
+
if (expectationErrors.length > 0) {
|
|
937
|
+
return failedStep(action, dryRun, expectationErrors);
|
|
938
|
+
}
|
|
939
|
+
const appendArg = action.args.appendarg ?? action.args.arg ?? action.body;
|
|
940
|
+
if (!appendArg) {
|
|
941
|
+
return failedStep(action, dryRun, [actionError("UPDATE.CALLSITES requires appendArg=<expr> or body", symbol.file)]);
|
|
942
|
+
}
|
|
943
|
+
const service = buildLanguageService(workspaceRoot, snapshot);
|
|
944
|
+
const absolute = resolve(workspaceRoot, symbol.file);
|
|
945
|
+
const position = symbolNamePosition(symbol, readFileSync(absolute, "utf8"));
|
|
946
|
+
const references = service.getReferencesAtPosition(absolute, position) ?? [];
|
|
947
|
+
const rewritesByFile = new Map<string, string>();
|
|
948
|
+
for (const reference of references) {
|
|
949
|
+
const fileName = resolve(reference.fileName);
|
|
950
|
+
if (fileName === absolute && reference.textSpan.start === position) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (!existsSync(fileName)) {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
const text = rewritesByFile.get(fileName) ?? readFileSync(fileName, "utf8");
|
|
957
|
+
const afterName = reference.textSpan.start + reference.textSpan.length;
|
|
958
|
+
const open = text.slice(afterName).search(/\S/);
|
|
959
|
+
const openIndex = open >= 0 ? afterName + open : -1;
|
|
960
|
+
if (openIndex < 0 || text[openIndex] !== "(") {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
const closeIndex = findMatchingParen(text, openIndex);
|
|
964
|
+
if (closeIndex < 0) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const existing = text.slice(openIndex + 1, closeIndex).trim();
|
|
968
|
+
const insertion = existing ? `, ${appendArg.trim()}` : appendArg.trim();
|
|
969
|
+
rewritesByFile.set(fileName, `${text.slice(0, closeIndex)}${insertion}${text.slice(closeIndex)}`);
|
|
970
|
+
}
|
|
971
|
+
return applyFileRewrites(
|
|
972
|
+
action,
|
|
973
|
+
workspaceRoot,
|
|
974
|
+
dryRun,
|
|
975
|
+
[...rewritesByFile].map(([absolutePath, next]) => ({ absolutePath, next })),
|
|
976
|
+
"O CALLSITES",
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function organizeImportsAction(
|
|
981
|
+
action: CairParsedAction,
|
|
982
|
+
snapshot: CairSnapshot,
|
|
983
|
+
workspaceRoot: string,
|
|
984
|
+
dryRun: boolean,
|
|
985
|
+
allowGenerated: boolean,
|
|
986
|
+
): CairActionStepResult {
|
|
987
|
+
const resolved = resolveActionPath(action, snapshot, workspaceRoot, allowGenerated);
|
|
988
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
989
|
+
return failedStep(action, dryRun, [resolved.diagnostic ?? actionError("ORGANIZE.IMPORTS requires file=<M#|repo-path>")]);
|
|
990
|
+
}
|
|
991
|
+
const service = buildLanguageService(workspaceRoot, snapshot);
|
|
992
|
+
const changes = service.organizeImports(
|
|
993
|
+
{ type: "file", fileName: resolved.path.absolutePath },
|
|
994
|
+
{},
|
|
995
|
+
{},
|
|
996
|
+
);
|
|
997
|
+
return applyLanguageServiceChanges(
|
|
998
|
+
action,
|
|
999
|
+
workspaceRoot,
|
|
1000
|
+
dryRun,
|
|
1001
|
+
new Map(changes.map((change) => [change.fileName, change.textChanges])),
|
|
1002
|
+
"O IMPORTS",
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function formatAction(
|
|
1007
|
+
action: CairParsedAction,
|
|
1008
|
+
snapshot: CairSnapshot,
|
|
1009
|
+
workspaceRoot: string,
|
|
1010
|
+
dryRun: boolean,
|
|
1011
|
+
allowGenerated: boolean,
|
|
1012
|
+
): CairActionStepResult {
|
|
1013
|
+
const resolved = resolveActionPath(action, snapshot, workspaceRoot, allowGenerated);
|
|
1014
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
1015
|
+
return failedStep(action, dryRun, [resolved.diagnostic ?? actionError("FORMAT requires file=<M#|repo-path>")]);
|
|
1016
|
+
}
|
|
1017
|
+
const service = buildLanguageService(workspaceRoot, snapshot);
|
|
1018
|
+
const changes = service.getFormattingEditsForDocument(resolved.path.absolutePath, {
|
|
1019
|
+
indentSize: 2,
|
|
1020
|
+
tabSize: 2,
|
|
1021
|
+
convertTabsToSpaces: true,
|
|
1022
|
+
newLineCharacter: "\n",
|
|
1023
|
+
});
|
|
1024
|
+
const current = readFileSync(resolved.path.absolutePath, "utf8");
|
|
1025
|
+
const languageFormatted = applyTextChangesToContent(current, changes);
|
|
1026
|
+
const fallback = languageFormatted
|
|
1027
|
+
.replace(/\bexport\s+const\s+/g, "export const ")
|
|
1028
|
+
.replace(/\s*=\s*/g, " = ")
|
|
1029
|
+
.replace(/^(.+[^;\s])$/gm, (line) =>
|
|
1030
|
+
/^\s*(import|export|const|let|var)\b/.test(line) && !/[{};]$/.test(line) ? `${line};` : line,
|
|
1031
|
+
);
|
|
1032
|
+
return applyFileRewrites(
|
|
1033
|
+
action,
|
|
1034
|
+
workspaceRoot,
|
|
1035
|
+
dryRun,
|
|
1036
|
+
fallback === current ? [] : [{ absolutePath: resolved.path.absolutePath, next: fallback }],
|
|
1037
|
+
"O FORMAT",
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function rollbackAction(
|
|
1042
|
+
action: CairParsedAction,
|
|
1043
|
+
workspaceRoot: string,
|
|
1044
|
+
dryRun: boolean,
|
|
1045
|
+
): CairActionStepResult {
|
|
1046
|
+
const journal = action.args.journal ?? action.args.path;
|
|
1047
|
+
if (!journal) {
|
|
1048
|
+
return failedStep(action, dryRun, [actionError("ROLLBACK requires journal=<repo-path>")]);
|
|
1049
|
+
}
|
|
1050
|
+
const resolved = resolveRepoPath(workspaceRoot, journal, true);
|
|
1051
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
1052
|
+
return failedStep(action, dryRun, [resolved.diagnostic ?? actionError("failed to resolve rollback journal")]);
|
|
1053
|
+
}
|
|
1054
|
+
if (!existsSync(resolved.path.absolutePath)) {
|
|
1055
|
+
return failedStep(action, dryRun, [actionError(`journal not found: ${resolved.path.repoPath}`, resolved.path.repoPath)]);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
let parsed: { changes?: CairFileChange[] };
|
|
1059
|
+
try {
|
|
1060
|
+
parsed = JSON.parse(readFileSync(resolved.path.absolutePath, "utf8")) as { changes?: CairFileChange[] };
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
return failedStep(action, dryRun, [
|
|
1063
|
+
actionError(`could not parse journal ${resolved.path.repoPath}: ${error instanceof Error ? error.message : String(error)}`),
|
|
1064
|
+
]);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const journalChanges = parsed.changes ?? [];
|
|
1068
|
+
const rollbackChanges: CairFileChange[] = [];
|
|
1069
|
+
for (const change of [...journalChanges].reverse()) {
|
|
1070
|
+
const target = resolveRepoPath(workspaceRoot, change.path, true);
|
|
1071
|
+
if (target.diagnostic || !target.path) {
|
|
1072
|
+
return failedStep(action, dryRun, [target.diagnostic ?? actionError(`failed to resolve rollback target ${change.path}`)]);
|
|
1073
|
+
}
|
|
1074
|
+
const current = existsSync(target.path.absolutePath)
|
|
1075
|
+
? readFileSync(target.path.absolutePath, "utf8")
|
|
1076
|
+
: "";
|
|
1077
|
+
if (change.beforeContent !== undefined) {
|
|
1078
|
+
maybeWrite(target.path.absolutePath, change.beforeContent, dryRun);
|
|
1079
|
+
rollbackChanges.push({
|
|
1080
|
+
path: target.path.repoPath,
|
|
1081
|
+
operation: "patch",
|
|
1082
|
+
beforeHash: hashText(current),
|
|
1083
|
+
afterHash: hashText(change.beforeContent),
|
|
1084
|
+
bytesBefore: byteLength(current),
|
|
1085
|
+
bytesAfter: byteLength(change.beforeContent),
|
|
1086
|
+
beforeContent: current,
|
|
1087
|
+
afterContent: change.beforeContent,
|
|
1088
|
+
});
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
if (!dryRun && existsSync(target.path.absolutePath)) {
|
|
1092
|
+
rmSync(target.path.absolutePath, { force: true });
|
|
1093
|
+
}
|
|
1094
|
+
rollbackChanges.push({
|
|
1095
|
+
path: target.path.repoPath,
|
|
1096
|
+
operation: "patch",
|
|
1097
|
+
beforeHash: hashText(current),
|
|
1098
|
+
afterHash: hashText(""),
|
|
1099
|
+
bytesBefore: byteLength(current),
|
|
1100
|
+
bytesAfter: 0,
|
|
1101
|
+
beforeContent: current,
|
|
1102
|
+
afterContent: "",
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return {
|
|
1107
|
+
ok: true,
|
|
1108
|
+
action,
|
|
1109
|
+
dryRun,
|
|
1110
|
+
applied: !dryRun,
|
|
1111
|
+
observations: [
|
|
1112
|
+
observation(
|
|
1113
|
+
dryRun ? "O ROLLBACK.PLAN" : "O ROLLBACK.APPLIED",
|
|
1114
|
+
`journal=${resolved.path.repoPath} changes=${rollbackChanges.length}${dryRun ? " dryRun=true" : ""}`,
|
|
1115
|
+
{ changes: rollbackChanges },
|
|
1116
|
+
),
|
|
1117
|
+
],
|
|
1118
|
+
diagnostics: [],
|
|
1119
|
+
changes: rollbackChanges,
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function resolvePlanPath(workspaceRoot: string, planRef: string): ResolvedPath | null {
|
|
1124
|
+
if (!planRef.startsWith("P#")) {
|
|
1125
|
+
const resolved = resolveRepoPath(workspaceRoot, planRef, true);
|
|
1126
|
+
return resolved.path ?? null;
|
|
1127
|
+
}
|
|
1128
|
+
const planDir = join(workspaceRoot, ".forge", "cair", "plans");
|
|
1129
|
+
if (!existsSync(planDir)) {
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
for (const entry of readdirSync(planDir).sort().reverse()) {
|
|
1133
|
+
if (!entry.endsWith(".json")) {
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
const absolutePath = join(planDir, entry);
|
|
1137
|
+
try {
|
|
1138
|
+
const parsed = JSON.parse(readFileSync(absolutePath, "utf8")) as { id?: string };
|
|
1139
|
+
if (parsed.id === planRef) {
|
|
1140
|
+
return {
|
|
1141
|
+
repoPath: normalizeSlashes(relative(workspaceRoot, absolutePath)),
|
|
1142
|
+
absolutePath,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
} catch {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function applyPlanAction(
|
|
1153
|
+
action: CairParsedAction,
|
|
1154
|
+
workspaceRoot: string,
|
|
1155
|
+
dryRun: boolean,
|
|
1156
|
+
): CairActionStepResult {
|
|
1157
|
+
const planRef = action.args.plan ?? action.args.path ?? action.args.id;
|
|
1158
|
+
if (!planRef) {
|
|
1159
|
+
return failedStep(action, dryRun, [actionError("APPLY requires plan=<P#|repo-path>")]);
|
|
1160
|
+
}
|
|
1161
|
+
const planPath = resolvePlanPath(workspaceRoot, planRef);
|
|
1162
|
+
if (!planPath || !existsSync(planPath.absolutePath)) {
|
|
1163
|
+
return failedStep(action, dryRun, [actionError(`plan not found: ${planRef}`)]);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
let parsed: { id?: string; changes?: CairFileChange[] };
|
|
1167
|
+
try {
|
|
1168
|
+
parsed = JSON.parse(readFileSync(planPath.absolutePath, "utf8")) as { id?: string; changes?: CairFileChange[] };
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
return failedStep(action, dryRun, [
|
|
1171
|
+
actionError(`could not parse plan ${planPath.repoPath}: ${error instanceof Error ? error.message : String(error)}`),
|
|
1172
|
+
]);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const changes = parsed.changes ?? [];
|
|
1176
|
+
const appliedChanges: CairFileChange[] = [];
|
|
1177
|
+
for (const change of changes) {
|
|
1178
|
+
if (change.afterContent === undefined) {
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
const target = resolveRepoPath(workspaceRoot, change.path, true);
|
|
1182
|
+
if (target.diagnostic || !target.path) {
|
|
1183
|
+
return failedStep(action, dryRun, [target.diagnostic ?? actionError(`failed to resolve plan target ${change.path}`)]);
|
|
1184
|
+
}
|
|
1185
|
+
const current = existsSync(target.path.absolutePath)
|
|
1186
|
+
? readFileSync(target.path.absolutePath, "utf8")
|
|
1187
|
+
: "";
|
|
1188
|
+
if (change.beforeHash && hashText(current) !== change.beforeHash) {
|
|
1189
|
+
return failedStep(action, dryRun, [
|
|
1190
|
+
actionError(`plan target changed since plan creation: ${change.path}`, change.path),
|
|
1191
|
+
]);
|
|
1192
|
+
}
|
|
1193
|
+
maybeWrite(target.path.absolutePath, change.afterContent, dryRun);
|
|
1194
|
+
appliedChanges.push({
|
|
1195
|
+
path: target.path.repoPath,
|
|
1196
|
+
operation: change.operation === "noop" ? "patch" : change.operation,
|
|
1197
|
+
beforeHash: hashText(current),
|
|
1198
|
+
afterHash: hashText(change.afterContent),
|
|
1199
|
+
bytesBefore: byteLength(current),
|
|
1200
|
+
bytesAfter: byteLength(change.afterContent),
|
|
1201
|
+
beforeContent: current,
|
|
1202
|
+
afterContent: change.afterContent,
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
return completedStep(action, dryRun, !dryRun && appliedChanges.length > 0, [
|
|
1207
|
+
observation(
|
|
1208
|
+
dryRun ? "O APPLY.PLAN" : "O APPLY.APPLIED",
|
|
1209
|
+
`plan=${parsed.id ?? planPath.repoPath} changes=${appliedChanges.length}${dryRun ? " dryRun=true" : ""}`,
|
|
1210
|
+
{ plan: { id: parsed.id, path: planPath.repoPath }, changes: appliedChanges },
|
|
1211
|
+
),
|
|
1212
|
+
], appliedChanges, workspaceRoot);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function shouldScanFile(path: string): boolean {
|
|
1216
|
+
return /\.(ts|tsx|js|jsx|mts|cts)$/.test(path) && !path.includes("/_generated/") && !path.includes("/node_modules/");
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function listScopeFiles(workspaceRoot: string, scope: string): string[] {
|
|
1220
|
+
const resolved = resolveRepoPath(workspaceRoot, scope.replace(/\*\*.*$/, ""), true);
|
|
1221
|
+
const root = resolved.path?.absolutePath ?? workspaceRoot;
|
|
1222
|
+
const files: string[] = [];
|
|
1223
|
+
function visit(path: string): void {
|
|
1224
|
+
if (!existsSync(path)) {
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
const stat = statSync(path);
|
|
1228
|
+
if (stat.isFile()) {
|
|
1229
|
+
const repoPath = normalizeSlashes(relative(workspaceRoot, path));
|
|
1230
|
+
if (shouldScanFile(repoPath)) {
|
|
1231
|
+
files.push(path);
|
|
1232
|
+
}
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (!stat.isDirectory()) {
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
const repoPath = normalizeSlashes(relative(workspaceRoot, path));
|
|
1239
|
+
if (repoPath.includes("node_modules") || repoPath.includes("_generated")) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
for (const entry of readdirSync(path)) {
|
|
1243
|
+
visit(join(path, entry));
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
visit(root);
|
|
1247
|
+
return files.sort();
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function patternToRegex(pattern: string): RegExp {
|
|
1251
|
+
const marker = "__CAIR_MULTI__";
|
|
1252
|
+
const escaped = pattern
|
|
1253
|
+
.replace(/\$\$\$[A-Z_][A-Z0-9_]*/gi, marker)
|
|
1254
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
1255
|
+
.replaceAll(marker, "([\\s\\S]*?)");
|
|
1256
|
+
return new RegExp(escaped, "g");
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function replacementFromMatch(replacement: string, match: RegExpExecArray): string {
|
|
1260
|
+
let index = 1;
|
|
1261
|
+
return replacement.replace(/\$\$\$[A-Z_][A-Z0-9_]*/gi, () => match[index++] ?? "");
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function findPatternAction(
|
|
1265
|
+
action: CairParsedAction,
|
|
1266
|
+
workspaceRoot: string,
|
|
1267
|
+
dryRun: boolean,
|
|
1268
|
+
): CairActionStepResult {
|
|
1269
|
+
const pattern = action.args.pattern ?? action.body;
|
|
1270
|
+
if (!pattern) {
|
|
1271
|
+
return failedStep(action, dryRun, [actionError("FIND.PATTERN requires pattern=<code> or body")]);
|
|
1272
|
+
}
|
|
1273
|
+
const scope = action.args.scope ?? "src";
|
|
1274
|
+
const regex = patternToRegex(pattern);
|
|
1275
|
+
const matches: Array<{ file: string; start: number; end: number; text: string }> = [];
|
|
1276
|
+
for (const file of listScopeFiles(workspaceRoot, scope)) {
|
|
1277
|
+
const text = readFileSync(file, "utf8");
|
|
1278
|
+
regex.lastIndex = 0;
|
|
1279
|
+
for (let match = regex.exec(text); match; match = regex.exec(text)) {
|
|
1280
|
+
matches.push({
|
|
1281
|
+
file: normalizeSlashes(relative(workspaceRoot, file)),
|
|
1282
|
+
start: match.index,
|
|
1283
|
+
end: match.index + match[0].length,
|
|
1284
|
+
text: match[0],
|
|
1285
|
+
});
|
|
1286
|
+
if (match[0].length === 0) {
|
|
1287
|
+
regex.lastIndex += 1;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return {
|
|
1292
|
+
ok: true,
|
|
1293
|
+
action,
|
|
1294
|
+
dryRun,
|
|
1295
|
+
applied: false,
|
|
1296
|
+
observations: [
|
|
1297
|
+
observation("O PATTERN.MATCHES", `matches=${matches.length} scope=${scope}`, { matches }),
|
|
1298
|
+
],
|
|
1299
|
+
diagnostics: [],
|
|
1300
|
+
changes: [],
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function rewritePatternAction(
|
|
1305
|
+
action: CairParsedAction,
|
|
1306
|
+
workspaceRoot: string,
|
|
1307
|
+
dryRun: boolean,
|
|
1308
|
+
): CairActionStepResult {
|
|
1309
|
+
const pattern = action.args.pattern;
|
|
1310
|
+
const replacement = action.args.replacement ?? action.body;
|
|
1311
|
+
if (!pattern || replacement === undefined) {
|
|
1312
|
+
return failedStep(action, dryRun, [actionError("REWRITE.PATTERN requires pattern=<code> and replacement=<code> or body")]);
|
|
1313
|
+
}
|
|
1314
|
+
const scope = action.args.scope ?? "src";
|
|
1315
|
+
const regex = patternToRegex(pattern);
|
|
1316
|
+
const rewrites: Array<{ absolutePath: string; next: string }> = [];
|
|
1317
|
+
for (const file of listScopeFiles(workspaceRoot, scope)) {
|
|
1318
|
+
const text = readFileSync(file, "utf8");
|
|
1319
|
+
regex.lastIndex = 0;
|
|
1320
|
+
let changed = false;
|
|
1321
|
+
const next = text.replace(regex, (...parts: unknown[]) => {
|
|
1322
|
+
const match = parts[0] as string;
|
|
1323
|
+
const captures = parts.slice(1, -2) as string[];
|
|
1324
|
+
const exec = [match, ...captures] as unknown as RegExpExecArray;
|
|
1325
|
+
changed = true;
|
|
1326
|
+
return replacementFromMatch(replacement, exec);
|
|
1327
|
+
});
|
|
1328
|
+
if (changed && next !== text) {
|
|
1329
|
+
rewrites.push({ absolutePath: file, next });
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return applyFileRewrites(action, workspaceRoot, dryRun, rewrites, "O PATTERN.REWRITE");
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function makePrimitiveFromVerb(verb: CairParsedAction["verb"]): MakePrimitive {
|
|
1336
|
+
switch (verb) {
|
|
1337
|
+
case "MAKE.COMMAND":
|
|
1338
|
+
return "command";
|
|
1339
|
+
case "MAKE.QUERY":
|
|
1340
|
+
return "query";
|
|
1341
|
+
case "MAKE.ACTION":
|
|
1342
|
+
return "action";
|
|
1343
|
+
case "MAKE.TABLE":
|
|
1344
|
+
return "table";
|
|
1345
|
+
default:
|
|
1346
|
+
return "command";
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function makeAction(
|
|
1351
|
+
action: CairParsedAction,
|
|
1352
|
+
workspaceRoot: string,
|
|
1353
|
+
dryRun: boolean,
|
|
1354
|
+
): CairActionStepResult {
|
|
1355
|
+
const name = action.args.name;
|
|
1356
|
+
if (!name) {
|
|
1357
|
+
return failedStep(action, dryRun, [actionError(`${action.verb} requires name=<name>`)]);
|
|
1358
|
+
}
|
|
1359
|
+
const options: MakeCommandOptions = {
|
|
1360
|
+
primitive: makePrimitiveFromVerb(action.verb),
|
|
1361
|
+
name,
|
|
1362
|
+
workspaceRoot,
|
|
1363
|
+
json: true,
|
|
1364
|
+
dryRun,
|
|
1365
|
+
plan: action.args.plan === "true",
|
|
1366
|
+
apply: !dryRun,
|
|
1367
|
+
yes: true,
|
|
1368
|
+
force: action.args.force === "true",
|
|
1369
|
+
noGenerate: action.args["no-generate"] === "true",
|
|
1370
|
+
noVerify: action.args["no-verify"] === "true",
|
|
1371
|
+
keepFailed: false,
|
|
1372
|
+
tenantScoped: action.args.tenantscoped === "true" || action.args["tenant-scoped"] === "true",
|
|
1373
|
+
fieldSpecs: action.args.field ? [action.args.field] : [],
|
|
1374
|
+
fieldsRaw: action.args.fields,
|
|
1375
|
+
type: action.args.type,
|
|
1376
|
+
values: action.args.values,
|
|
1377
|
+
defaultValue: action.args.default,
|
|
1378
|
+
index: action.args.index === "true",
|
|
1379
|
+
roles: action.args.roles,
|
|
1380
|
+
table: action.args.table,
|
|
1381
|
+
policy: action.args.policy,
|
|
1382
|
+
emit: action.args.emit,
|
|
1383
|
+
event: action.args.event,
|
|
1384
|
+
trigger: action.args.trigger,
|
|
1385
|
+
component: action.args.component,
|
|
1386
|
+
framework: action.args.framework as "vite" | "next" | "nuxt" | undefined,
|
|
1387
|
+
withAi: action.args.withai === "true" || action.args["with-ai"] === "true",
|
|
1388
|
+
withCrud: action.args.withcrud === "true" || action.args["with-crud"] === "true",
|
|
1389
|
+
withLiveQuery: action.args.withlivequery === "true" || action.args["with-livequery"] === "true",
|
|
1390
|
+
withReact: action.args.withreact === "true" || action.args["with-react"] === "true",
|
|
1391
|
+
withUi: action.args.withui === "true" || action.args["with-ui"] === "true",
|
|
1392
|
+
withTests: action.args.withtests === "true" || action.args["with-tests"] === "true",
|
|
1393
|
+
withCreateForm: action.args.withcreateform === "true" || action.args["with-create-form"] === "true",
|
|
1394
|
+
};
|
|
1395
|
+
const result = planMakeCommand(options);
|
|
1396
|
+
const plannedFiles = [
|
|
1397
|
+
...(result.plan?.filesToCreate.map((file) => file.file) ?? []),
|
|
1398
|
+
...(result.plan?.filesToModify.map((file) => file.file) ?? []),
|
|
1399
|
+
];
|
|
1400
|
+
return {
|
|
1401
|
+
ok: result.ok,
|
|
1402
|
+
action,
|
|
1403
|
+
dryRun,
|
|
1404
|
+
applied: result.applied === true,
|
|
1405
|
+
observations: [
|
|
1406
|
+
observation(
|
|
1407
|
+
result.applied ? "O MAKE.APPLIED" : "O MAKE.PLAN",
|
|
1408
|
+
`primitive=${options.primitive} name=${name} files=${plannedFiles.length}${dryRun ? " dryRun=true" : ""}`,
|
|
1409
|
+
{ make: result, files: plannedFiles },
|
|
1410
|
+
),
|
|
1411
|
+
],
|
|
1412
|
+
diagnostics: result.diagnostics,
|
|
1413
|
+
changes: plannedFiles.map((file) => ({
|
|
1414
|
+
path: file,
|
|
1415
|
+
operation: result.applied ? "patch" : "noop",
|
|
1416
|
+
})),
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function importSpecifier(fromFile: string, toFile: string): string {
|
|
1421
|
+
const withoutExtension = toFile.replace(/\.(tsx?|jsx?|mts|cts)$/u, "");
|
|
1422
|
+
const specifier = normalizeSlashes(relative(dirname(fromFile), withoutExtension));
|
|
1423
|
+
return specifier.startsWith(".") ? specifier : `./${specifier}`;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function kebabName(value: string): string {
|
|
1427
|
+
return value
|
|
1428
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
1429
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
1430
|
+
.replace(/^-|-$/g, "")
|
|
1431
|
+
.toLowerCase() || "symbol";
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function addTestAction(
|
|
1435
|
+
action: CairParsedAction,
|
|
1436
|
+
snapshot: CairSnapshot,
|
|
1437
|
+
workspaceRoot: string,
|
|
1438
|
+
dryRun: boolean,
|
|
1439
|
+
allowGenerated: boolean,
|
|
1440
|
+
): CairActionStepResult {
|
|
1441
|
+
const symbol = resolveSymbolRef(snapshot, action.args.target);
|
|
1442
|
+
if (!symbol) {
|
|
1443
|
+
return failedStep(action, dryRun, [actionError("ADD.TEST requires target=<S#>")]);
|
|
1444
|
+
}
|
|
1445
|
+
const kind = action.args.kind ?? "unit";
|
|
1446
|
+
const testPath = action.args.path ?? `tests/${symbol.kind.replace(/[^\w.-]+/g, "-")}/${kebabName(symbol.name)}.test.ts`;
|
|
1447
|
+
const resolved = resolveRepoPath(workspaceRoot, testPath, allowGenerated);
|
|
1448
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
1449
|
+
return failedStep(action, dryRun, [resolved.diagnostic ?? actionError("failed to resolve ADD.TEST path")]);
|
|
1450
|
+
}
|
|
1451
|
+
const body = action.body ?? [
|
|
1452
|
+
'import { describe, expect, test } from "bun:test";',
|
|
1453
|
+
`import { ${symbol.name} } from "${importSpecifier(resolved.path.repoPath, symbol.file)}";`,
|
|
1454
|
+
"",
|
|
1455
|
+
`describe("${symbol.name}", () => {`,
|
|
1456
|
+
` test("${kind}", () => {`,
|
|
1457
|
+
` expect(${symbol.name}).toBeDefined();`,
|
|
1458
|
+
" });",
|
|
1459
|
+
"});",
|
|
1460
|
+
"",
|
|
1461
|
+
].join("\n");
|
|
1462
|
+
const current = existsSync(resolved.path.absolutePath)
|
|
1463
|
+
? readFileSync(resolved.path.absolutePath, "utf8")
|
|
1464
|
+
: "";
|
|
1465
|
+
if (current && action.args.force !== "true") {
|
|
1466
|
+
return failedStep(action, dryRun, [actionError(`test already exists: ${resolved.path.repoPath}`, resolved.path.repoPath)]);
|
|
1467
|
+
}
|
|
1468
|
+
return applyFileRewrites(action, workspaceRoot, dryRun, [
|
|
1469
|
+
{ absolutePath: resolved.path.absolutePath, next: body },
|
|
1470
|
+
], "O TEST");
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function wireExportAction(
|
|
1474
|
+
action: CairParsedAction,
|
|
1475
|
+
snapshot: CairSnapshot,
|
|
1476
|
+
workspaceRoot: string,
|
|
1477
|
+
dryRun: boolean,
|
|
1478
|
+
allowGenerated: boolean,
|
|
1479
|
+
): CairActionStepResult {
|
|
1480
|
+
const symbol = resolveSymbolRef(snapshot, action.args.target);
|
|
1481
|
+
if (!symbol) {
|
|
1482
|
+
return failedStep(action, dryRun, [actionError("WIRE.EXPORT requires target=<S#>")]);
|
|
1483
|
+
}
|
|
1484
|
+
const barrelPath = action.args.file ?? action.args.path ?? "src/index.ts";
|
|
1485
|
+
const resolved = resolveRepoPath(workspaceRoot, barrelPath, allowGenerated);
|
|
1486
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
1487
|
+
return failedStep(action, dryRun, [resolved.diagnostic ?? actionError("failed to resolve WIRE.EXPORT path")]);
|
|
1488
|
+
}
|
|
1489
|
+
const current = existsSync(resolved.path.absolutePath)
|
|
1490
|
+
? readFileSync(resolved.path.absolutePath, "utf8")
|
|
1491
|
+
: "";
|
|
1492
|
+
const line = `export { ${symbol.name} } from "${importSpecifier(resolved.path.repoPath, symbol.file)}";`;
|
|
1493
|
+
const next = current.includes(line)
|
|
1494
|
+
? current
|
|
1495
|
+
: `${current}${current.endsWith("\n") || !current ? "" : "\n"}${line}\n`;
|
|
1496
|
+
return applyFileRewrites(action, workspaceRoot, dryRun, [
|
|
1497
|
+
{ absolutePath: resolved.path.absolutePath, next },
|
|
1498
|
+
], "O EXPORT");
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function verifyAction(
|
|
1502
|
+
action: CairParsedAction,
|
|
1503
|
+
snapshot: CairSnapshot,
|
|
1504
|
+
workspaceRoot: string,
|
|
1505
|
+
dryRun: boolean,
|
|
1506
|
+
): CairActionStepResult {
|
|
1507
|
+
const kind = action.phase === "V"
|
|
1508
|
+
? action.raw.trim().split(/\s+/)[1]?.toLowerCase()
|
|
1509
|
+
: (action.args.kind ?? action.args.check ?? "typecheck").toLowerCase();
|
|
1510
|
+
if (kind === "impact") {
|
|
1511
|
+
const symbol = resolveSymbolRef(snapshot, action.args.target);
|
|
1512
|
+
const tests = symbol
|
|
1513
|
+
? snapshot.lexicon.tests.filter((test) =>
|
|
1514
|
+
Object.values(test.covers).some((covered) =>
|
|
1515
|
+
covered.includes(symbol.name) ||
|
|
1516
|
+
covered.includes(symbol.qualifiedName) ||
|
|
1517
|
+
covered.includes(symbol.sourceId) ||
|
|
1518
|
+
covered.includes(symbol.file),
|
|
1519
|
+
),
|
|
1520
|
+
)
|
|
1521
|
+
: [];
|
|
1522
|
+
return {
|
|
1523
|
+
ok: true,
|
|
1524
|
+
action,
|
|
1525
|
+
dryRun,
|
|
1526
|
+
applied: false,
|
|
1527
|
+
observations: [
|
|
1528
|
+
observation(
|
|
1529
|
+
"O VERIFY.IMPACT",
|
|
1530
|
+
`target=${action.args.target ?? "none"} tests=${tests.map((test) => test.id).join(",") || "none"} matches=${tests.length}`,
|
|
1531
|
+
{ symbol, tests },
|
|
1532
|
+
),
|
|
1533
|
+
],
|
|
1534
|
+
diagnostics: [],
|
|
1535
|
+
changes: [],
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
if (dryRun) {
|
|
1539
|
+
return completedStep(action, dryRun, false, [
|
|
1540
|
+
observation("O VERIFY.PLAN", `kind=${kind || "typecheck"} dryRun=true`),
|
|
1541
|
+
], [], workspaceRoot);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if (kind === "typecheck" || kind === "tsc") {
|
|
1545
|
+
const tscPath = join(workspaceRoot, "node_modules", "typescript", "bin", "tsc");
|
|
1546
|
+
if (!existsSync(tscPath)) {
|
|
1547
|
+
return failedStep(action, dryRun, [actionError("VERIFY typecheck requires node_modules/typescript/bin/tsc")]);
|
|
1548
|
+
}
|
|
1549
|
+
const startedAt = Date.now();
|
|
1550
|
+
const result = spawnSync(process.execPath, [tscPath, "--noEmit"], {
|
|
1551
|
+
cwd: workspaceRoot,
|
|
1552
|
+
encoding: "utf8",
|
|
1553
|
+
timeout: 120_000,
|
|
1554
|
+
windowsHide: true,
|
|
1555
|
+
});
|
|
1556
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1557
|
+
const ok = result.status === 0;
|
|
1558
|
+
return {
|
|
1559
|
+
ok,
|
|
1560
|
+
action,
|
|
1561
|
+
dryRun,
|
|
1562
|
+
applied: false,
|
|
1563
|
+
observations: [
|
|
1564
|
+
observation(
|
|
1565
|
+
"O VERIFY.TYPECHECK",
|
|
1566
|
+
`ok=${ok} exit=${result.status ?? "null"} ms=${elapsedMs}`,
|
|
1567
|
+
{
|
|
1568
|
+
stdout: result.stdout,
|
|
1569
|
+
stderr: result.stderr,
|
|
1570
|
+
},
|
|
1571
|
+
),
|
|
1572
|
+
],
|
|
1573
|
+
diagnostics: ok ? [] : [actionError(`typecheck failed with exit ${result.status ?? "null"}`)],
|
|
1574
|
+
changes: [],
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
if (kind === "test") {
|
|
1579
|
+
const file = action.args.file;
|
|
1580
|
+
const runner = join(workspaceRoot, "bin", "forge-bun.mjs");
|
|
1581
|
+
if (!existsSync(runner)) {
|
|
1582
|
+
return failedStep(action, dryRun, [actionError("VERIFY test requires bin/forge-bun.mjs")]);
|
|
1583
|
+
}
|
|
1584
|
+
const startedAt = Date.now();
|
|
1585
|
+
const result = spawnSync(
|
|
1586
|
+
process.execPath,
|
|
1587
|
+
[runner, "test", ...(file ? [file] : []), "--timeout", action.args.timeout ?? "120000"],
|
|
1588
|
+
{
|
|
1589
|
+
cwd: workspaceRoot,
|
|
1590
|
+
encoding: "utf8",
|
|
1591
|
+
timeout: Number(action.args.timeout ?? 120_000),
|
|
1592
|
+
windowsHide: true,
|
|
1593
|
+
},
|
|
1594
|
+
);
|
|
1595
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1596
|
+
const ok = result.status === 0;
|
|
1597
|
+
return {
|
|
1598
|
+
ok,
|
|
1599
|
+
action,
|
|
1600
|
+
dryRun,
|
|
1601
|
+
applied: false,
|
|
1602
|
+
observations: [
|
|
1603
|
+
observation(
|
|
1604
|
+
"O VERIFY.TEST",
|
|
1605
|
+
`ok=${ok} exit=${result.status ?? "null"} ms=${elapsedMs}${file ? ` file=${file}` : ""}`,
|
|
1606
|
+
{
|
|
1607
|
+
stdout: result.stdout,
|
|
1608
|
+
stderr: result.stderr,
|
|
1609
|
+
},
|
|
1610
|
+
),
|
|
1611
|
+
],
|
|
1612
|
+
diagnostics: ok ? [] : [actionError(`test failed with exit ${result.status ?? "null"}`)],
|
|
1613
|
+
changes: [],
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
return failedStep(action, dryRun, [actionError(`unknown VERIFY kind '${kind}'`)]);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function failedStep(
|
|
1621
|
+
action: CairParsedAction,
|
|
1622
|
+
dryRun: boolean,
|
|
1623
|
+
diagnostics: Diagnostic[],
|
|
1624
|
+
): CairActionStepResult {
|
|
1625
|
+
return {
|
|
1626
|
+
ok: false,
|
|
1627
|
+
action,
|
|
1628
|
+
dryRun,
|
|
1629
|
+
applied: false,
|
|
1630
|
+
observations: diagnostics.map((diagnostic) =>
|
|
1631
|
+
observation("O ACTION.FAIL", `code=${diagnostic.code} message=${JSON.stringify(diagnostic.message)}`),
|
|
1632
|
+
),
|
|
1633
|
+
diagnostics,
|
|
1634
|
+
changes: [],
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function completedStep(
|
|
1639
|
+
action: CairParsedAction,
|
|
1640
|
+
dryRun: boolean,
|
|
1641
|
+
applied: boolean,
|
|
1642
|
+
observations: CairObservation[],
|
|
1643
|
+
changes: CairFileChange[],
|
|
1644
|
+
workspaceRoot: string,
|
|
1645
|
+
): CairActionStepResult {
|
|
1646
|
+
const journalPath = writeCairActionJournal(workspaceRoot, action, changes, dryRun);
|
|
1647
|
+
const planRef = dryRun ? writeCairActionPlan(workspaceRoot, action, changes) : undefined;
|
|
1648
|
+
const planObservations = changes.length > 0
|
|
1649
|
+
? [
|
|
1650
|
+
observation(
|
|
1651
|
+
"O ACTION.PLAN",
|
|
1652
|
+
[
|
|
1653
|
+
`verb=${action.verb}`,
|
|
1654
|
+
`changes=${changes.length}`,
|
|
1655
|
+
`files=${changes.map((change) => change.path).join(",") || "none"}`,
|
|
1656
|
+
dryRun ? "dryRun=true" : null,
|
|
1657
|
+
].filter(Boolean).join(" "),
|
|
1658
|
+
{ changes },
|
|
1659
|
+
),
|
|
1660
|
+
]
|
|
1661
|
+
: [];
|
|
1662
|
+
const planObservationsWithRef = planRef
|
|
1663
|
+
? [...planObservations, observation("O PLAN", `id=${planRef.id} path=${planRef.path}`, { plan: planRef })]
|
|
1664
|
+
: planObservations;
|
|
1665
|
+
return {
|
|
1666
|
+
ok: true,
|
|
1667
|
+
action,
|
|
1668
|
+
dryRun,
|
|
1669
|
+
applied,
|
|
1670
|
+
observations: journalPath
|
|
1671
|
+
? [...planObservationsWithRef, ...observations, observation("O JOURNAL", `path=${journalPath}`)]
|
|
1672
|
+
: [...planObservationsWithRef, ...observations],
|
|
1673
|
+
diagnostics: [],
|
|
1674
|
+
changes,
|
|
1675
|
+
...(journalPath ? { journalPath } : {}),
|
|
1676
|
+
...(planRef ? { planPath: planRef.path, planId: planRef.id } : {}),
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function runCairActionStep(
|
|
1681
|
+
action: CairParsedAction,
|
|
1682
|
+
snapshot: CairSnapshot,
|
|
1683
|
+
workspaceRoot: string,
|
|
1684
|
+
dryRun: boolean,
|
|
1685
|
+
allowGenerated: boolean,
|
|
1686
|
+
): CairActionStepResult {
|
|
1687
|
+
switch (action.verb) {
|
|
1688
|
+
case "CREATE.FILE":
|
|
1689
|
+
return createFileAction(action, snapshot, workspaceRoot, dryRun, allowGenerated);
|
|
1690
|
+
case "CREATE.SYMBOL":
|
|
1691
|
+
return createSymbolAction(action, snapshot, workspaceRoot, dryRun, allowGenerated);
|
|
1692
|
+
case "PATCH":
|
|
1693
|
+
return patchAction(action, snapshot, workspaceRoot, dryRun, allowGenerated);
|
|
1694
|
+
case "ADD.IMPORT":
|
|
1695
|
+
return addImportAction(action, snapshot, workspaceRoot, dryRun, allowGenerated);
|
|
1696
|
+
case "ADD.EXPORT":
|
|
1697
|
+
return addExportAction(action, snapshot, workspaceRoot, dryRun, allowGenerated);
|
|
1698
|
+
case "APPLY":
|
|
1699
|
+
return applyPlanAction(action, workspaceRoot, dryRun);
|
|
1700
|
+
case "ROLLBACK":
|
|
1701
|
+
return rollbackAction(action, workspaceRoot, dryRun);
|
|
1702
|
+
case "RENAME.SYMBOL":
|
|
1703
|
+
return renameSymbolAction(action, snapshot, workspaceRoot, dryRun);
|
|
1704
|
+
case "MOVE.SYMBOL":
|
|
1705
|
+
return moveSymbolAction(action, snapshot, workspaceRoot, dryRun, allowGenerated);
|
|
1706
|
+
case "UPDATE.SIGNATURE":
|
|
1707
|
+
return updateSignatureAction(action, snapshot, workspaceRoot, dryRun, allowGenerated);
|
|
1708
|
+
case "ADD.PARAM":
|
|
1709
|
+
return addParamAction(action, snapshot, workspaceRoot, dryRun, allowGenerated);
|
|
1710
|
+
case "UPDATE.CALLSITES":
|
|
1711
|
+
return updateCallsitesAction(action, snapshot, workspaceRoot, dryRun);
|
|
1712
|
+
case "ORGANIZE.IMPORTS":
|
|
1713
|
+
return organizeImportsAction(action, snapshot, workspaceRoot, dryRun, allowGenerated);
|
|
1714
|
+
case "FORMAT":
|
|
1715
|
+
return formatAction(action, snapshot, workspaceRoot, dryRun, allowGenerated);
|
|
1716
|
+
case "FIND.PATTERN":
|
|
1717
|
+
return findPatternAction(action, workspaceRoot, dryRun);
|
|
1718
|
+
case "REWRITE.PATTERN":
|
|
1719
|
+
return rewritePatternAction(action, workspaceRoot, dryRun);
|
|
1720
|
+
case "MAKE.COMMAND":
|
|
1721
|
+
case "MAKE.QUERY":
|
|
1722
|
+
case "MAKE.ACTION":
|
|
1723
|
+
case "MAKE.TABLE":
|
|
1724
|
+
return makeAction(action, workspaceRoot, dryRun);
|
|
1725
|
+
case "ADD.TEST":
|
|
1726
|
+
return addTestAction(action, snapshot, workspaceRoot, dryRun, allowGenerated);
|
|
1727
|
+
case "WIRE.EXPORT":
|
|
1728
|
+
return wireExportAction(action, snapshot, workspaceRoot, dryRun, allowGenerated);
|
|
1729
|
+
case "VERIFY":
|
|
1730
|
+
return verifyAction(action, snapshot, workspaceRoot, dryRun);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
export function runCairActionScript(options: CairActionRunOptions): CairActionResult {
|
|
1735
|
+
const parsedScript = parseCairActionScript(options.script);
|
|
1736
|
+
const blocks = parsedScript.blocks;
|
|
1737
|
+
const scriptDiagnostics = [...parsedScript.diagnostics];
|
|
1738
|
+
if (parsedScript.header?.snapshot && parsedScript.header.snapshot !== options.snapshot.snapshotId) {
|
|
1739
|
+
scriptDiagnostics.push(actionError(
|
|
1740
|
+
`CAIR snapshot mismatch: script=${parsedScript.header.snapshot} current=${options.snapshot.snapshotId}`,
|
|
1741
|
+
));
|
|
1742
|
+
}
|
|
1743
|
+
if (blocks.length === 0) {
|
|
1744
|
+
const diagnostics = scriptDiagnostics.length > 0 ? scriptDiagnostics : [actionError("empty CAIR action script")];
|
|
1745
|
+
return {
|
|
1746
|
+
ok: false,
|
|
1747
|
+
dryRun: options.dryRun,
|
|
1748
|
+
plan: options.plan,
|
|
1749
|
+
...(parsedScript.header ? { header: parsedScript.header } : {}),
|
|
1750
|
+
actionCount: 0,
|
|
1751
|
+
steps: [],
|
|
1752
|
+
observations: diagnostics.map((diagnostic) =>
|
|
1753
|
+
observation("O ACTION.FAIL", `code=${diagnostic.code} message=${JSON.stringify(diagnostic.message)}`),
|
|
1754
|
+
),
|
|
1755
|
+
diagnostics,
|
|
1756
|
+
journalPaths: [],
|
|
1757
|
+
planPaths: [],
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
const steps: CairActionStepResult[] = [];
|
|
1762
|
+
const parseDiagnostics: Diagnostic[] = [...scriptDiagnostics];
|
|
1763
|
+
for (const block of blocks) {
|
|
1764
|
+
if (parseDiagnostics.length > 0) {
|
|
1765
|
+
break;
|
|
1766
|
+
}
|
|
1767
|
+
const parsed = parseCairAction(block);
|
|
1768
|
+
if (parsed.diagnostics.length > 0 || !parsed.action) {
|
|
1769
|
+
parseDiagnostics.push(...parsed.diagnostics);
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
steps.push(
|
|
1773
|
+
runCairActionStep(
|
|
1774
|
+
parsed.action,
|
|
1775
|
+
options.snapshot,
|
|
1776
|
+
options.workspaceRoot,
|
|
1777
|
+
options.dryRun,
|
|
1778
|
+
options.allowGenerated,
|
|
1779
|
+
),
|
|
1780
|
+
);
|
|
1781
|
+
if (!steps[steps.length - 1]?.ok) {
|
|
1782
|
+
break;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
const diagnostics = [...parseDiagnostics, ...steps.flatMap((step) => step.diagnostics)];
|
|
1787
|
+
const observations = [
|
|
1788
|
+
...parseDiagnostics.map((diagnostic) =>
|
|
1789
|
+
observation("O ACTION.FAIL", `code=${diagnostic.code} message=${JSON.stringify(diagnostic.message)}`),
|
|
1790
|
+
),
|
|
1791
|
+
...steps.flatMap((step) => step.observations),
|
|
1792
|
+
];
|
|
1793
|
+
const journalPaths = steps.flatMap((step) => step.journalPath ? [step.journalPath] : []);
|
|
1794
|
+
const planPaths = steps.flatMap((step) => step.planPath ? [step.planPath] : []);
|
|
1795
|
+
return {
|
|
1796
|
+
ok: diagnostics.length === 0 && steps.every((step) => step.ok),
|
|
1797
|
+
dryRun: options.dryRun,
|
|
1798
|
+
plan: options.plan,
|
|
1799
|
+
...(parsedScript.header ? { header: parsedScript.header } : {}),
|
|
1800
|
+
actionCount: blocks.length,
|
|
1801
|
+
steps,
|
|
1802
|
+
observations,
|
|
1803
|
+
diagnostics,
|
|
1804
|
+
journalPaths,
|
|
1805
|
+
planPaths,
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
export function statActionInput(workspaceRoot: string, inputPath: string): string {
|
|
1810
|
+
const resolved = resolveRepoPath(workspaceRoot, inputPath, true);
|
|
1811
|
+
if (resolved.diagnostic || !resolved.path) {
|
|
1812
|
+
throw new Error(resolved.diagnostic?.message ?? "invalid CAIR input path");
|
|
1813
|
+
}
|
|
1814
|
+
if (!existsSync(resolved.path.absolutePath) || !statSync(resolved.path.absolutePath).isFile()) {
|
|
1815
|
+
throw new Error(`CAIR input file not found: ${resolved.path.repoPath}`);
|
|
1816
|
+
}
|
|
1817
|
+
return readFileSync(resolved.path.absolutePath, "utf8");
|
|
1818
|
+
}
|