@superblocksteam/vite-plugin-file-sync 2.0.119-next.1 → 2.0.120-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-service/agent/middleware.d.ts.map +1 -1
- package/dist/ai-service/agent/middleware.js +2 -5
- package/dist/ai-service/agent/middleware.js.map +1 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js +17 -11
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.d.ts +17 -0
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.d.ts.map +1 -0
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.js +219 -0
- package/dist/ai-service/agent/prompts/build-security-scan-prompt.js.map +1 -0
- package/dist/ai-service/agent/tool-message-utils.d.ts.map +1 -1
- package/dist/ai-service/agent/tool-message-utils.js +64 -6
- package/dist/ai-service/agent/tool-message-utils.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/api-comparator.d.ts +36 -0
- package/dist/ai-service/agent/tools/apis/api-comparator.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/apis/api-comparator.js +369 -0
- package/dist/ai-service/agent/tools/apis/api-comparator.js.map +1 -0
- package/dist/ai-service/agent/tools/apis/api-validation-orchestrator.d.ts +4 -0
- package/dist/ai-service/agent/tools/apis/api-validation-orchestrator.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/api-validation-orchestrator.js +16 -5
- package/dist/ai-service/agent/tools/apis/api-validation-orchestrator.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/build-api-artifact.d.ts +1 -0
- package/dist/ai-service/agent/tools/apis/build-api-artifact.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/build-api-artifact.js +17 -2
- package/dist/ai-service/agent/tools/apis/build-api-artifact.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/build-api.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/build-api.js +17 -15
- package/dist/ai-service/agent/tools/apis/build-api.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/get-api-docs.d.ts +1 -1
- package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.js +18 -13
- package/dist/ai-service/agent/tools/apis/get-sdk-api-docs.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/test-api.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/test-api.js +138 -2
- package/dist/ai-service/agent/tools/apis/test-api.js.map +1 -1
- package/dist/ai-service/agent/tools/build-copy-directory.d.ts +12 -0
- package/dist/ai-service/agent/tools/build-copy-directory.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/build-copy-directory.js +51 -0
- package/dist/ai-service/agent/tools/build-copy-directory.js.map +1 -0
- package/dist/ai-service/agent/tools/build-copy-file.d.ts +12 -0
- package/dist/ai-service/agent/tools/build-copy-file.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/build-copy-file.js +52 -0
- package/dist/ai-service/agent/tools/build-copy-file.js.map +1 -0
- package/dist/ai-service/agent/tools/build-copy-utils.d.ts +57 -0
- package/dist/ai-service/agent/tools/build-copy-utils.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/build-copy-utils.js +37 -0
- package/dist/ai-service/agent/tools/build-copy-utils.js.map +1 -0
- package/dist/ai-service/agent/tools/build-debug.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-debug.js +17 -5
- package/dist/ai-service/agent/tools/build-debug.js.map +1 -1
- package/dist/ai-service/agent/tools/build-finalize.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-finalize.js +42 -50
- package/dist/ai-service/agent/tools/build-finalize.js.map +1 -1
- package/dist/ai-service/agent/tools/build-manage-checklist.d.ts +7 -5
- package/dist/ai-service/agent/tools/build-manage-checklist.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-manage-checklist.js +54 -108
- package/dist/ai-service/agent/tools/build-manage-checklist.js.map +1 -1
- package/dist/ai-service/agent/tools/databases/dev-database.d.ts +103 -0
- package/dist/ai-service/agent/tools/databases/dev-database.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/databases/dev-database.js +117 -0
- package/dist/ai-service/agent/tools/databases/dev-database.js.map +1 -0
- package/dist/ai-service/agent/tools/get-logs.d.ts +1 -1
- package/dist/ai-service/agent/tools/index.d.ts +4 -0
- package/dist/ai-service/agent/tools/index.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/index.js +4 -0
- package/dist/ai-service/agent/tools/index.js.map +1 -1
- package/dist/ai-service/agent/tools/integrations/delete-integration.d.ts +18 -0
- package/dist/ai-service/agent/tools/integrations/delete-integration.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/integrations/delete-integration.js +99 -0
- package/dist/ai-service/agent/tools/integrations/delete-integration.js.map +1 -0
- package/dist/ai-service/agent/tools/integrations/execute-request.d.ts +1 -1
- package/dist/ai-service/agent/tools/integrations/index.d.ts +1 -0
- package/dist/ai-service/agent/tools/integrations/index.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/integrations/index.js +1 -0
- package/dist/ai-service/agent/tools/integrations/index.js.map +1 -1
- package/dist/ai-service/agent/tools/integrations/metadata.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/integrations/metadata.js +30 -4
- package/dist/ai-service/agent/tools/integrations/metadata.js.map +1 -1
- package/dist/ai-service/agent/tools/report-security-findings.d.ts +163 -0
- package/dist/ai-service/agent/tools/report-security-findings.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/report-security-findings.js +52 -0
- package/dist/ai-service/agent/tools/report-security-findings.js.map +1 -0
- package/dist/ai-service/agent/tools.d.ts +2 -0
- package/dist/ai-service/agent/tools.d.ts.map +1 -1
- package/dist/ai-service/agent/tools.js +74 -6
- package/dist/ai-service/agent/tools.js.map +1 -1
- package/dist/ai-service/agent/tools2/example.js +1 -1
- package/dist/ai-service/agent/tools2/example.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/ask-multi-choice.d.ts +7 -0
- package/dist/ai-service/agent/tools2/tools/ask-multi-choice.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/ask-multi-choice.js +11 -1
- package/dist/ai-service/agent/tools2/tools/ask-multi-choice.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/ask-searchable-dropdown.d.ts +7 -0
- package/dist/ai-service/agent/tools2/tools/ask-searchable-dropdown.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/ask-searchable-dropdown.js +3 -1
- package/dist/ai-service/agent/tools2/tools/ask-searchable-dropdown.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/download-attachments.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/download-attachments.js +4 -3
- package/dist/ai-service/agent/tools2/tools/download-attachments.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.d.ts +9 -0
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js +15 -1
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/list-attachments.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/list-attachments.js +8 -4
- package/dist/ai-service/agent/tools2/tools/list-attachments.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/spawn-coding-subagents.d.ts +21 -4
- package/dist/ai-service/agent/tools2/tools/spawn-coding-subagents.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/spawn-coding-subagents.js +87 -11
- package/dist/ai-service/agent/tools2/tools/spawn-coding-subagents.js.map +1 -1
- package/dist/ai-service/agent/tools2/types.d.ts +10 -2
- package/dist/ai-service/agent/tools2/types.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/types.js.map +1 -1
- package/dist/ai-service/agent/utils.d.ts.map +1 -1
- package/dist/ai-service/agent/utils.js +2 -0
- package/dist/ai-service/agent/utils.js.map +1 -1
- package/dist/ai-service/app-interface/filesystem/draft-manager.d.ts +1 -1
- package/dist/ai-service/app-interface/filesystem/draft-manager.d.ts.map +1 -1
- package/dist/ai-service/app-interface/filesystem/draft-manager.js.map +1 -1
- package/dist/ai-service/app-interface/npm-registry.d.ts +137 -0
- package/dist/ai-service/app-interface/npm-registry.d.ts.map +1 -0
- package/dist/ai-service/app-interface/npm-registry.js +415 -0
- package/dist/ai-service/app-interface/npm-registry.js.map +1 -0
- package/dist/ai-service/app-interface/shell.d.ts +38 -0
- package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
- package/dist/ai-service/app-interface/shell.js +222 -1
- package/dist/ai-service/app-interface/shell.js.map +1 -1
- package/dist/ai-service/attachments/uploaded-content-part.d.ts +5 -0
- package/dist/ai-service/attachments/uploaded-content-part.d.ts.map +1 -1
- package/dist/ai-service/attachments/uploaded-content-part.js +31 -21
- package/dist/ai-service/attachments/uploaded-content-part.js.map +1 -1
- package/dist/ai-service/checklist/persisted-checklist-store.d.ts +105 -0
- package/dist/ai-service/checklist/persisted-checklist-store.d.ts.map +1 -0
- package/dist/ai-service/checklist/persisted-checklist-store.js +498 -0
- package/dist/ai-service/checklist/persisted-checklist-store.js.map +1 -0
- package/dist/ai-service/context-download.d.ts +14 -1
- package/dist/ai-service/context-download.d.ts.map +1 -1
- package/dist/ai-service/context-download.js +80 -0
- package/dist/ai-service/context-download.js.map +1 -1
- package/dist/ai-service/dev-database-client.d.ts +90 -0
- package/dist/ai-service/dev-database-client.d.ts.map +1 -0
- package/dist/ai-service/dev-database-client.js +166 -0
- package/dist/ai-service/dev-database-client.js.map +1 -0
- package/dist/ai-service/features.d.ts +16 -0
- package/dist/ai-service/features.d.ts.map +1 -1
- package/dist/ai-service/features.js +10 -0
- package/dist/ai-service/features.js.map +1 -1
- package/dist/ai-service/filter-disabled-tools-for-migration.d.ts +6 -0
- package/dist/ai-service/filter-disabled-tools-for-migration.d.ts.map +1 -0
- package/dist/ai-service/filter-disabled-tools-for-migration.js +35 -0
- package/dist/ai-service/filter-disabled-tools-for-migration.js.map +1 -0
- package/dist/ai-service/index.d.ts +11 -1
- package/dist/ai-service/index.d.ts.map +1 -1
- package/dist/ai-service/index.js +86 -37
- package/dist/ai-service/index.js.map +1 -1
- package/dist/ai-service/integrations/store.d.ts +5 -0
- package/dist/ai-service/integrations/store.d.ts.map +1 -1
- package/dist/ai-service/integrations/store.js +19 -2
- package/dist/ai-service/integrations/store.js.map +1 -1
- package/dist/ai-service/judge/tools/submit-feedback.d.ts +1 -1
- package/dist/ai-service/llm/context-v2/context.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/context.js +6 -2
- package/dist/ai-service/llm/context-v2/context.js.map +1 -1
- package/dist/ai-service/llm/context-v2/manager.d.ts +8 -1
- package/dist/ai-service/llm/context-v2/manager.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/manager.js +17 -1
- package/dist/ai-service/llm/context-v2/manager.js.map +1 -1
- package/dist/ai-service/llm/context-v2/prompts/compaction.d.ts +1 -1
- package/dist/ai-service/llm/context-v2/prompts/compaction.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/prompts/compaction.js +3 -3
- package/dist/ai-service/llm/context-v2/types.d.ts +7 -0
- package/dist/ai-service/llm/context-v2/types.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/types.js +33 -0
- package/dist/ai-service/llm/context-v2/types.js.map +1 -1
- package/dist/ai-service/llm/impl/clark.d.ts.map +1 -1
- package/dist/ai-service/llm/impl/clark.js +3 -3
- package/dist/ai-service/llm/impl/clark.js.map +1 -1
- package/dist/ai-service/llm/provider.d.ts.map +1 -1
- package/dist/ai-service/llm/provider.js +22 -7
- package/dist/ai-service/llm/provider.js.map +1 -1
- package/dist/ai-service/llm/types.d.ts +14 -1
- package/dist/ai-service/llm/types.d.ts.map +1 -1
- package/dist/ai-service/llmobs/context-registry.d.ts +62 -0
- package/dist/ai-service/llmobs/context-registry.d.ts.map +1 -0
- package/dist/ai-service/llmobs/context-registry.js +115 -0
- package/dist/ai-service/llmobs/context-registry.js.map +1 -0
- package/dist/ai-service/llmobs/otel-exporter.d.ts +23 -0
- package/dist/ai-service/llmobs/otel-exporter.d.ts.map +1 -1
- package/dist/ai-service/llmobs/otel-exporter.js +112 -10
- package/dist/ai-service/llmobs/otel-exporter.js.map +1 -1
- package/dist/ai-service/llmobs/tracer.d.ts +7 -0
- package/dist/ai-service/llmobs/tracer.d.ts.map +1 -1
- package/dist/ai-service/llmobs/tracer.js +38 -0
- package/dist/ai-service/llmobs/tracer.js.map +1 -1
- package/dist/ai-service/skills/system/_registry.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/_registry.generated.js +2 -0
- package/dist/ai-service/skills/system/_registry.generated.js.map +1 -1
- package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.js +2 -0
- package/dist/ai-service/skills/system/superblocks-frontend/skill.generated.js.map +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/references/focused-debug.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/references/focused-debug.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/references/focused-debug.generated.js +3 -1
- package/dist/ai-service/skills/system/superblocks-migration/references/focused-debug.generated.js.map +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/references/yaml-block-mapping.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/references/yaml-block-mapping.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/references/yaml-block-mapping.generated.js +29 -0
- package/dist/ai-service/skills/system/superblocks-migration/references/yaml-block-mapping.generated.js.map +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js +139 -7
- package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js.map +1 -1
- package/dist/ai-service/skills/system/third-party-migration/claude-design.generated.d.ts +2 -0
- package/dist/ai-service/skills/system/third-party-migration/claude-design.generated.d.ts.map +1 -0
- package/dist/ai-service/skills/system/third-party-migration/claude-design.generated.js +107 -0
- package/dist/ai-service/skills/system/third-party-migration/claude-design.generated.js.map +1 -0
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.js +33 -3
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.js.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.d.ts +21 -0
- package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.js +79 -6
- package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.d.ts +10 -0
- package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.js +69 -41
- package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/peer.d.ts +35 -7
- package/dist/ai-service/state-machine/helpers/peer.d.ts.map +1 -1
- package/dist/ai-service/state-machine/helpers/peer.js +81 -15
- package/dist/ai-service/state-machine/helpers/peer.js.map +1 -1
- package/dist/ai-service/template-renderer.d.ts +14 -1
- package/dist/ai-service/template-renderer.d.ts.map +1 -1
- package/dist/ai-service/template-renderer.js +144 -41
- package/dist/ai-service/template-renderer.js.map +1 -1
- package/dist/ai-service/transform/api-builder/to-sdk-transformer.js +2 -2
- package/dist/ai-service/transform/api-builder/to-sdk-transformer.js.map +1 -1
- package/dist/ai-service/transform/api-builder/to-yaml-transformer.js +2 -2
- package/dist/ai-service/transform/api-builder/to-yaml-transformer.js.map +1 -1
- package/dist/draft-interface.d.ts +1 -1
- package/dist/draft-interface.d.ts.map +1 -1
- package/dist/file-sync-vite-plugin.d.ts.map +1 -1
- package/dist/file-sync-vite-plugin.js +34 -27
- package/dist/file-sync-vite-plugin.js.map +1 -1
- package/dist/file-system-helpers.d.ts +4 -0
- package/dist/file-system-helpers.d.ts.map +1 -1
- package/dist/file-system-helpers.js +13 -0
- package/dist/file-system-helpers.js.map +1 -1
- package/dist/inject-index-vite-plugin.d.ts.map +1 -1
- package/dist/inject-index-vite-plugin.js +15 -1
- package/dist/inject-index-vite-plugin.js.map +1 -1
- package/dist/injected-index.d.ts.map +1 -1
- package/dist/injected-index.js +15 -1
- package/dist/injected-index.js.map +1 -1
- package/dist/lock-service/index.d.ts.map +1 -1
- package/dist/lock-service/index.js +8 -10
- package/dist/lock-service/index.js.map +1 -1
- package/dist/migration/migration-checklist.d.ts +51 -2
- package/dist/migration/migration-checklist.d.ts.map +1 -1
- package/dist/migration/migration-checklist.js +79 -151
- package/dist/migration/migration-checklist.js.map +1 -1
- package/dist/migration/migration-routes.d.ts.map +1 -1
- package/dist/migration/migration-routes.js +290 -30
- package/dist/migration/migration-routes.js.map +1 -1
- package/dist/migration/migration-verification.d.ts +206 -0
- package/dist/migration/migration-verification.d.ts.map +1 -0
- package/dist/migration/migration-verification.js +1006 -0
- package/dist/migration/migration-verification.js.map +1 -0
- package/dist/migration/recommended-user-deps.d.ts +39 -0
- package/dist/migration/recommended-user-deps.d.ts.map +1 -0
- package/dist/migration/recommended-user-deps.js +209 -0
- package/dist/migration/recommended-user-deps.js.map +1 -0
- package/dist/migration/restructure.d.ts +29 -2
- package/dist/migration/restructure.d.ts.map +1 -1
- package/dist/migration/restructure.js +145 -6
- package/dist/migration/restructure.js.map +1 -1
- package/dist/migration/scan-imports.d.ts +7 -0
- package/dist/migration/scan-imports.d.ts.map +1 -0
- package/dist/migration/scan-imports.js +178 -0
- package/dist/migration/scan-imports.js.map +1 -0
- package/dist/migration/translation-prompt.d.ts.map +1 -1
- package/dist/migration/translation-prompt.js +2 -0
- package/dist/migration/translation-prompt.js.map +1 -1
- package/dist/migration/unsupported-integrations.d.ts.map +1 -1
- package/dist/migration/unsupported-integrations.js +9 -0
- package/dist/migration/unsupported-integrations.js.map +1 -1
- package/dist/migration/yaml-walk.d.ts +18 -0
- package/dist/migration/yaml-walk.d.ts.map +1 -0
- package/dist/migration/yaml-walk.js +45 -0
- package/dist/migration/yaml-walk.js.map +1 -0
- package/dist/migration-templates/app-fullstack/client/components/hooks/use-mobile.ts +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/accordion.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/avatar.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/breadcrumb.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/button.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/calendar.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/chart.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/file-dropzone.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/file-input.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/hover-card.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/image.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/input.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/label.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/navigation-menu.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/pagination.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/popover.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/progress.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/select.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/sheet.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/sidebar.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/slider.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/switch.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/table.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/tabs.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/toggle-group.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/toggle.tsx +1 -1
- package/dist/migration-templates/app-fullstack/client/components/ui/tooltip.tsx +1 -1
- package/dist/socket-manager.d.ts.map +1 -1
- package/dist/socket-manager.js +8 -0
- package/dist/socket-manager.js.map +1 -1
- package/dist/sync-service/hash-dir-tree.d.ts +1 -1
- package/dist/sync-service/hash-dir-tree.d.ts.map +1 -1
- package/dist/sync-service/hash-dir-tree.js +3 -3
- package/dist/sync-service/hash-dir-tree.js.map +1 -1
- package/dist/sync-service/index.d.ts +0 -14
- package/dist/sync-service/index.d.ts.map +1 -1
- package/dist/sync-service/index.js +1 -44
- package/dist/sync-service/index.js.map +1 -1
- package/dist/sync-service/list-dir.d.ts +1 -1
- package/dist/sync-service/list-dir.d.ts.map +1 -1
- package/dist/sync-service/list-dir.js +36 -3
- package/dist/sync-service/list-dir.js.map +1 -1
- package/dist/sync-service/snapshot/take-snapshot.d.ts +1 -1
- package/dist/sync-service/snapshot/take-snapshot.d.ts.map +1 -1
- package/dist/sync-service/snapshot/take-snapshot.js +4 -8
- package/dist/sync-service/snapshot/take-snapshot.js.map +1 -1
- package/dist/util/log-sanitizer.d.ts +6 -5
- package/dist/util/log-sanitizer.d.ts.map +1 -1
- package/dist/util/log-sanitizer.js +21 -6
- package/dist/util/log-sanitizer.js.map +1 -1
- package/package.json +9 -8
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import yaml from "yaml";
|
|
5
|
+
import z from "zod";
|
|
6
|
+
import { AiEntityType, AiPermissionType, } from "@superblocksteam/library-shared/types";
|
|
7
|
+
import { LanguagePluginID } from "@superblocksteam/shared";
|
|
8
|
+
import { extractIntegrationIdsFromSource, extractMutations, } from "../ai-service/agent/tools/apis/analysis.js";
|
|
9
|
+
import { compareApiResults, } from "../ai-service/agent/tools/apis/api-comparator.js";
|
|
10
|
+
import { ApiExecutor, } from "../ai-service/agent/tools/apis/api-executor.js";
|
|
11
|
+
import { createToolFactory, PermissionLevel, ToolCategory, } from "../ai-service/agent/tools2/types.js";
|
|
12
|
+
import { updateChecklistItem } from "../ai-service/checklist/persisted-checklist-store.js";
|
|
13
|
+
import { sanitizeLogMessage } from "../util/log-sanitizer.js";
|
|
14
|
+
import { getLogger } from "../util/logger.js";
|
|
15
|
+
import { OperationQueue } from "../util/operation-queue.js";
|
|
16
|
+
import { classifyYamlStepMutation } from "./migration-classifier.js";
|
|
17
|
+
import { forEachYamlStep } from "./yaml-walk.js";
|
|
18
|
+
const SCRATCH_DIR = "scratch";
|
|
19
|
+
const VERIFICATION_DIR = "migration-verification";
|
|
20
|
+
const V2_BACKUP_DIR = "v2-backup";
|
|
21
|
+
const DIFF_SIZE_CAP = 1024 * 1024; // 1 MB
|
|
22
|
+
const PIPELINE_TIMEOUT_MS = 30_000;
|
|
23
|
+
/**
|
|
24
|
+
* Hard cap on verification attempts per API per migration. The agent does NOT
|
|
25
|
+
* decide when to stop iterating — this counter is computed from on-disk
|
|
26
|
+
* history and enforced inside `runMigrationVerification`. An LLM agent asked
|
|
27
|
+
* to self-assess "am I converging?" reliably picks the socially acceptable
|
|
28
|
+
* answer ("yes"); the cap removes that judgment from the loop.
|
|
29
|
+
*/
|
|
30
|
+
export const MAX_VERIFICATION_ATTEMPTS = 10;
|
|
31
|
+
const LANGUAGE_PLUGIN_IDS = new Set(Object.values(LanguagePluginID));
|
|
32
|
+
/**
|
|
33
|
+
* Verify a single API by running both the v2 YAML and v3 TS versions with the
|
|
34
|
+
* supplied inputs and comparing their outputs.
|
|
35
|
+
*
|
|
36
|
+
* Wraps `computeVerificationOutcome` with on-disk history bookkeeping:
|
|
37
|
+
* - Reads `scratch/migration-verification/<apiName>.history.json` and refuses
|
|
38
|
+
* to run beyond `MAX_VERIFICATION_ATTEMPTS`, returning `kind: "exhausted"`.
|
|
39
|
+
* - For `diverged` outcomes, computes `progress` (`first_attempt` |
|
|
40
|
+
* `converging` | `stalled`) by hashing (path, kind) tuples and comparing to
|
|
41
|
+
* the prior diverged attempt's hash.
|
|
42
|
+
* - Appends a history entry on every run so the next call has accurate state.
|
|
43
|
+
*
|
|
44
|
+
* History persistence is best-effort: bookkeeping I/O failures are logged but
|
|
45
|
+
* do not surface as verification errors.
|
|
46
|
+
*/
|
|
47
|
+
export async function runMigrationVerification(params) {
|
|
48
|
+
// Serialize the entire read-cap-check + pipeline + append for a given API
|
|
49
|
+
// through the per-API queue. Two concurrent calls for the same API used to
|
|
50
|
+
// both observe `attempts.length = MAX-1`, both pass the cap check, and
|
|
51
|
+
// both run the pipeline + append — silently exceeding the documented hard
|
|
52
|
+
// cap by N. Wrapping end-to-end (rather than serializing only the append)
|
|
53
|
+
// costs us same-API parallelism — but parallel verifications for the same
|
|
54
|
+
// API are wasteful work anyway (the second result clobbers the first or
|
|
55
|
+
// hits the cap), so the cost is purely on a degenerate path. Different
|
|
56
|
+
// APIs use different queues and remain fully parallel.
|
|
57
|
+
return getVerificationHistoryQueue(params.appRootDir, params.apiName).enqueue(async () => {
|
|
58
|
+
const history = await readHistory(params.appRootDir, params.apiName);
|
|
59
|
+
if (history.attempts.length >= MAX_VERIFICATION_ATTEMPTS) {
|
|
60
|
+
return {
|
|
61
|
+
kind: "exhausted",
|
|
62
|
+
attempts: history.attempts.length,
|
|
63
|
+
lastDivergence: await readLastDivergedDiff(params.appRootDir, params.apiName),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const raw = await computeVerificationOutcome(params);
|
|
67
|
+
let outcome;
|
|
68
|
+
let divergenceHashForHistory;
|
|
69
|
+
if (raw.kind === "diverged") {
|
|
70
|
+
const currentHash = divergenceShapeHash(raw.diff.summaryForAgent);
|
|
71
|
+
divergenceHashForHistory = currentHash;
|
|
72
|
+
const lastDivergedHash = findLastDivergedShapeHash(history);
|
|
73
|
+
const progress = lastDivergedHash === undefined
|
|
74
|
+
? "first_attempt"
|
|
75
|
+
: lastDivergedHash === currentHash
|
|
76
|
+
? "stalled"
|
|
77
|
+
: "converging";
|
|
78
|
+
outcome = {
|
|
79
|
+
kind: "diverged",
|
|
80
|
+
diff: raw.diff,
|
|
81
|
+
attempt: history.attempts.length + 1,
|
|
82
|
+
progress,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
outcome = raw;
|
|
87
|
+
}
|
|
88
|
+
// Single decision-point log so an operator chasing "Clark says diverged
|
|
89
|
+
// but I see passed" has a stdout breadcrumb to correlate. Logged here at
|
|
90
|
+
// the single return path of `runMigrationVerification` rather than in
|
|
91
|
+
// each tool-side branch — wraps every outcome (passed, diverged,
|
|
92
|
+
// both_failed, exhausted, skipped_mutation, v2_unrunnable, error) with a
|
|
93
|
+
// consistent shape. Apply name only; raw failure strings stay scrubbed.
|
|
94
|
+
getLogger().info(`[migration-verify] outcome`, {
|
|
95
|
+
apiName: params.apiName,
|
|
96
|
+
attempt: history.attempts.length + 1,
|
|
97
|
+
maxAttempts: MAX_VERIFICATION_ATTEMPTS,
|
|
98
|
+
kind: outcome.kind,
|
|
99
|
+
progress: outcome.kind === "diverged" ? outcome.progress : undefined,
|
|
100
|
+
divergenceCount: outcome.kind === "diverged"
|
|
101
|
+
? outcome.diff.summaryForAgent.length
|
|
102
|
+
: undefined,
|
|
103
|
+
});
|
|
104
|
+
// Direct (non-enqueued) write — we already hold this API's queue, so
|
|
105
|
+
// re-entering it would deadlock.
|
|
106
|
+
await writeHistoryEntry(params.appRootDir, params.apiName, {
|
|
107
|
+
ts: new Date().toISOString(),
|
|
108
|
+
kind: outcome.kind,
|
|
109
|
+
divergenceShapeHash: divergenceHashForHistory,
|
|
110
|
+
divergenceCount: outcome.kind === "diverged"
|
|
111
|
+
? outcome.diff.summaryForAgent.length
|
|
112
|
+
: undefined,
|
|
113
|
+
});
|
|
114
|
+
return outcome;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async function computeVerificationOutcome({ apiName, appRootDir, inputs, runV2Pipeline, runV3Sdk, allowAskPolicy, }) {
|
|
118
|
+
const verificationInputs = inputs ?? {};
|
|
119
|
+
// 1. Detect mutations in the v3 TS source before running anything.
|
|
120
|
+
const apiTsPath = join(appRootDir, "server", "apis", apiName, "api.ts");
|
|
121
|
+
let source;
|
|
122
|
+
try {
|
|
123
|
+
source = await readFile(apiTsPath, "utf-8");
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
return {
|
|
127
|
+
kind: "error",
|
|
128
|
+
reason: `Cannot read v3 source at ${apiTsPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// Extract integration IDs up-front so they can be passed to runV3Sdk for
|
|
132
|
+
// the auth preflight (matching test-api.ts). Falls back to undefined on AST
|
|
133
|
+
// parse failure — runV3Sdk will then rely on the Redux store as before.
|
|
134
|
+
let integrationIds;
|
|
135
|
+
try {
|
|
136
|
+
const ids = extractIntegrationIdsFromSource(source);
|
|
137
|
+
if (ids.length > 0)
|
|
138
|
+
integrationIds = ids;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// AST parse failure: leave undefined.
|
|
142
|
+
}
|
|
143
|
+
let mutations;
|
|
144
|
+
try {
|
|
145
|
+
mutations = extractMutations(source);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
// AST parse failure: we cannot prove the source is mutation-free, so fall
|
|
149
|
+
// back to the same policy gate as if mutations were detected. The user
|
|
150
|
+
// can still get the API verified by allow-and-retry or manually verified.
|
|
151
|
+
return {
|
|
152
|
+
kind: "skipped_mutation",
|
|
153
|
+
mutationDetails: [
|
|
154
|
+
`unable to analyze source for mutations: ${error instanceof Error ? error.message : String(error)}`,
|
|
155
|
+
],
|
|
156
|
+
skipPolicy: "ask",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const v2BackupPath = join(appRootDir, SCRATCH_DIR, V2_BACKUP_DIR, "apis", apiName, "api.yaml");
|
|
160
|
+
// Also check the v2 backup for writes — a half-migrated API where v3 looks
|
|
161
|
+
// read-only but the original v2 still mutates would otherwise bypass the
|
|
162
|
+
// policy gate and run the v2 pipeline against real integrations.
|
|
163
|
+
const v2Analysis = await analyzeV2BackupMutations(v2BackupPath);
|
|
164
|
+
const mutationDetails = [
|
|
165
|
+
...mutations.map((m) => `v3 ${m.type} "${m.name}": ${m.details}`),
|
|
166
|
+
...v2Analysis.mutationDetails,
|
|
167
|
+
];
|
|
168
|
+
if (mutationDetails.length > 0) {
|
|
169
|
+
const { effective } = await getEffectiveMigrationWritePolicy({
|
|
170
|
+
appRootDir,
|
|
171
|
+
apiName,
|
|
172
|
+
integrationIds,
|
|
173
|
+
});
|
|
174
|
+
if (effective === "deny" || (effective === "ask" && !allowAskPolicy)) {
|
|
175
|
+
return {
|
|
176
|
+
kind: "skipped_mutation",
|
|
177
|
+
mutationDetails,
|
|
178
|
+
skipPolicy: effective,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// effective === "allow": fall through to run both pipelines
|
|
182
|
+
}
|
|
183
|
+
// 2. Run v2 YAML pipeline.
|
|
184
|
+
let v2Result;
|
|
185
|
+
try {
|
|
186
|
+
const pipelineResult = await withTimeout(runV2Pipeline(v2BackupPath, verificationInputs), PIPELINE_TIMEOUT_MS, "v2 pipeline");
|
|
187
|
+
if (!pipelineResult.executionResult) {
|
|
188
|
+
return {
|
|
189
|
+
kind: "v2_unrunnable",
|
|
190
|
+
reason: "v2 pipeline returned no execution result",
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
v2Result = pipelineResult.executionResult;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return {
|
|
197
|
+
kind: "v2_unrunnable",
|
|
198
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// 3. Run v3 SDK.
|
|
202
|
+
let v3Result;
|
|
203
|
+
try {
|
|
204
|
+
v3Result = await withTimeout(runV3Sdk(integrationIds, verificationInputs), PIPELINE_TIMEOUT_MS, "v3 SDK");
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return {
|
|
208
|
+
kind: "error",
|
|
209
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// 3.5. If both executions returned failure, surface that explicitly rather
|
|
213
|
+
// than letting compareApiResults produce a vacuous "diff.empty" pass.
|
|
214
|
+
if (!v2Result.success && !v3Result.success) {
|
|
215
|
+
return {
|
|
216
|
+
kind: "both_failed",
|
|
217
|
+
v2Error: extractFailureMessage(v2Result, "v2"),
|
|
218
|
+
v3Error: extractFailureMessage(v3Result, "v3"),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// 3.6. One-sided failure: if one side succeeded and the other failed, that is
|
|
222
|
+
// a divergence regardless of what the output diff says. Without this
|
|
223
|
+
// guard, a v3 that returns `success: false` with empty `outputs` would
|
|
224
|
+
// compare equal to a v2 with empty `outputs`, producing a false "passed".
|
|
225
|
+
if (v2Result.success !== v3Result.success) {
|
|
226
|
+
const failedSide = v2Result.success ? "v3" : "v2";
|
|
227
|
+
const failureMessage = extractFailureMessage(v2Result.success ? v3Result : v2Result, failedSide);
|
|
228
|
+
// Server-side log so divergence shows up in the dev-server log stream
|
|
229
|
+
// (and DataDog when the EE forwards stdout). The actual failure text is
|
|
230
|
+
// intentionally NOT logged here — extractFailureMessage may include
|
|
231
|
+
// upstream response bodies, query strings, or credentials echoed back by
|
|
232
|
+
// the integration. The agent already gets failureMessage via
|
|
233
|
+
// summaryForAgent, and the user sees full detail in the verification UI.
|
|
234
|
+
// What we want from the server log is a "this happened" marker:
|
|
235
|
+
// apiName, which side failed, and the error type/source classification.
|
|
236
|
+
// Logger.warn accepts free-form key/value pairs; .error has a stricter
|
|
237
|
+
// ErrorMeta shape that doesn't fit this diagnostic record.
|
|
238
|
+
getLogger().warn(`Migration verification diverged for ${apiName}`, {
|
|
239
|
+
apiName,
|
|
240
|
+
failedSide,
|
|
241
|
+
v2: {
|
|
242
|
+
success: v2Result.success,
|
|
243
|
+
hasSystemError: Boolean(v2Result.systemError),
|
|
244
|
+
firstErrorType: v2Result.errors?.[0]?.type,
|
|
245
|
+
},
|
|
246
|
+
v3: { success: v3Result.success },
|
|
247
|
+
});
|
|
248
|
+
// The agent-facing `failureMessage` is scrubbed via `redactForAgent`
|
|
249
|
+
// because it ends up in LLM context (and from there into chat
|
|
250
|
+
// transcripts persisted by the client). The UI-facing `valuesForUi.reason`
|
|
251
|
+
// intentionally keeps the raw text — that channel renders to the
|
|
252
|
+
// user's own browser, not into the model context.
|
|
253
|
+
return {
|
|
254
|
+
kind: "diverged",
|
|
255
|
+
diff: {
|
|
256
|
+
empty: false,
|
|
257
|
+
summaryForAgent: [
|
|
258
|
+
{
|
|
259
|
+
path: "success",
|
|
260
|
+
kind: "value_mismatch",
|
|
261
|
+
expectedHash: String(v2Result.success),
|
|
262
|
+
actualHash: String(v3Result.success),
|
|
263
|
+
failureMessage: redactForAgent(failureMessage),
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
valuesForUi: [
|
|
267
|
+
{
|
|
268
|
+
path: "success",
|
|
269
|
+
expected: v2Result.success,
|
|
270
|
+
actual: v3Result.success,
|
|
271
|
+
reason: `${failedSide} execution failed while the other succeeded: ${failureMessage}`,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// 4. Compare.
|
|
278
|
+
const diff = compareApiResults(v2Result, v3Result);
|
|
279
|
+
if (diff.empty) {
|
|
280
|
+
return { kind: "passed" };
|
|
281
|
+
}
|
|
282
|
+
// 5. Persist the diff to disk for the UI to render (valuesForUi only).
|
|
283
|
+
const verificationDir = join(appRootDir, SCRATCH_DIR, VERIFICATION_DIR);
|
|
284
|
+
await mkdir(verificationDir, { recursive: true });
|
|
285
|
+
const diffPath = join(verificationDir, `${apiName}.diff.json`);
|
|
286
|
+
const diffJson = JSON.stringify({ valuesForUi: diff.valuesForUi }, null, 2);
|
|
287
|
+
const payload = diffJson.length > DIFF_SIZE_CAP
|
|
288
|
+
? JSON.stringify({ valuesForUi: diff.valuesForUi.slice(0, 50), truncated: true }, null, 2)
|
|
289
|
+
: diffJson;
|
|
290
|
+
await writeFile(diffPath, payload, "utf-8");
|
|
291
|
+
return { kind: "diverged", diff };
|
|
292
|
+
}
|
|
293
|
+
/** Relative path (from appRootDir) for a diff JSON file. */
|
|
294
|
+
export function diffRelativePath(apiName) {
|
|
295
|
+
return `${SCRATCH_DIR}/${VERIFICATION_DIR}/${apiName}.diff.json`;
|
|
296
|
+
}
|
|
297
|
+
export const migrationVerificationToolFactory = createToolFactory("runMigrationVerification", ({ clark, services, }) => ({
|
|
298
|
+
category: ToolCategory.DEBUG,
|
|
299
|
+
getRequiredPermissions: async (input) => {
|
|
300
|
+
const apiName = input?.apiName;
|
|
301
|
+
if (!apiName)
|
|
302
|
+
return [];
|
|
303
|
+
let source;
|
|
304
|
+
try {
|
|
305
|
+
source = await services.appShell.readFile(`server/apis/${apiName}/api.ts`);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
let mutations;
|
|
311
|
+
try {
|
|
312
|
+
mutations = extractMutations(source);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
if (mutations.length === 0)
|
|
318
|
+
return [];
|
|
319
|
+
let integrationIds;
|
|
320
|
+
try {
|
|
321
|
+
const ids = extractIntegrationIdsFromSource(source);
|
|
322
|
+
if (ids.length > 0)
|
|
323
|
+
integrationIds = ids;
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
integrationIds = undefined;
|
|
327
|
+
}
|
|
328
|
+
const { effective, sourceIds } = await getEffectiveMigrationWritePolicy({
|
|
329
|
+
appRootDir: services.appRootDirPath,
|
|
330
|
+
apiName,
|
|
331
|
+
integrationIds,
|
|
332
|
+
});
|
|
333
|
+
if (effective === "allow" || sourceIds.length === 0) {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
return sourceIds.map((entityId) => ({
|
|
337
|
+
entityType: AiEntityType.INTEGRATION,
|
|
338
|
+
entityId,
|
|
339
|
+
permissionType: AiPermissionType.WRITE,
|
|
340
|
+
}));
|
|
341
|
+
},
|
|
342
|
+
getActionName: (input) => {
|
|
343
|
+
const name = input?.apiName ?? "API";
|
|
344
|
+
return {
|
|
345
|
+
future: `verify ${name}`,
|
|
346
|
+
present: `verifying ${name}`,
|
|
347
|
+
past: `verified ${name}`,
|
|
348
|
+
};
|
|
349
|
+
},
|
|
350
|
+
description: "Verify a migrated API by running both the v2 YAML and v3 TS versions " +
|
|
351
|
+
"and comparing their outputs. Pass `inputs` to inject test parameters " +
|
|
352
|
+
"for any bindings the API references (the same way `testApi` does — " +
|
|
353
|
+
"`Input1.value`, `Table1.selectedRow`, etc.); both v2 and v3 receive " +
|
|
354
|
+
"the same inputs so the comparison is apples-to-apples. Omit `inputs` " +
|
|
355
|
+
"(or pass `{}`) only when the API takes no inputs. Each pipeline has a " +
|
|
356
|
+
`${PIPELINE_TIMEOUT_MS / 1000}s reporting timeout (reject-only — the ` +
|
|
357
|
+
"underlying execution can't be cancelled, so allow-policy mutations " +
|
|
358
|
+
"may continue server-side after this returns). Returns a structured outcome: " +
|
|
359
|
+
"'passed', 'diverged' (with summaryForAgent), 'both_failed' (both " +
|
|
360
|
+
"executions errored — cannot determine correctness), " +
|
|
361
|
+
"'skipped_mutation', 'v2_unrunnable', or 'error'. On 'diverged' use " +
|
|
362
|
+
"`summaryForAgent` to revise `server/apis/<apiName>/api.ts` and re-run " +
|
|
363
|
+
"the tool — the attempt counter is tracked server-side from on-disk " +
|
|
364
|
+
"history; the response always includes the current `attempt` and " +
|
|
365
|
+
"`maxAttempts` so the agent doesn't have to thread it.",
|
|
366
|
+
inputSchema: z.object({
|
|
367
|
+
apiName: z
|
|
368
|
+
.string()
|
|
369
|
+
.describe("The API name to verify (must match the directory under " +
|
|
370
|
+
"server/apis/, may contain only letters, digits, hyphens, or " +
|
|
371
|
+
"underscores). Bare .string() — `.regex()` emits a JSON Schema " +
|
|
372
|
+
"`pattern` keyword that Snowflake Cortex rejects; the format is " +
|
|
373
|
+
"validated below at runtime.")
|
|
374
|
+
.refine((v) => /^[a-zA-Z0-9_-]+$/.test(v), {
|
|
375
|
+
message: "apiName must contain only letters, digits, hyphens, or underscores",
|
|
376
|
+
}),
|
|
377
|
+
inputs: z
|
|
378
|
+
.record(z.string(), z.any())
|
|
379
|
+
.optional()
|
|
380
|
+
.describe("Test input values for the API's bindings, applied to BOTH the " +
|
|
381
|
+
"v2 YAML and v3 TS executions so the diff is meaningful. Use the " +
|
|
382
|
+
"same approach as `testApi`: a key per binding the API references " +
|
|
383
|
+
"(component values like `Input1.value`, table selections like " +
|
|
384
|
+
"`Table1.selectedRow`, state variables, workflow params, etc.) " +
|
|
385
|
+
"with realistic mock values that match the binding's expected " +
|
|
386
|
+
"type. Omit (defaults to `{}`) only when the API takes no inputs " +
|
|
387
|
+
"— passing `{}` for an API that needs inputs typically produces " +
|
|
388
|
+
"a vacuous 'both_failed' result."),
|
|
389
|
+
// `attemptCount` was previously declared here for the agent to thread
|
|
390
|
+
// through retries, but `execute` never read it — the real attempt
|
|
391
|
+
// count is computed from on-disk history inside
|
|
392
|
+
// `runMigrationVerification` so an agent that lies about its attempt
|
|
393
|
+
// index can't bypass `MAX_VERIFICATION_ATTEMPTS`. The field is dropped
|
|
394
|
+
// to match what the tool actually uses; Zod's default object policy
|
|
395
|
+
// strips unknown keys so any agent still passing it in-flight is a
|
|
396
|
+
// no-op rather than an error.
|
|
397
|
+
}),
|
|
398
|
+
async execute({ apiName, inputs }) {
|
|
399
|
+
const appRootDir = services.appRootDirPath;
|
|
400
|
+
const outcome = await runMigrationVerification({
|
|
401
|
+
apiName,
|
|
402
|
+
appRootDir,
|
|
403
|
+
inputs,
|
|
404
|
+
allowAskPolicy: true,
|
|
405
|
+
runV2Pipeline: async (sourcePath, pipelineInputs) => {
|
|
406
|
+
// runApiValidationPipeline's sourcePath only controls metadata
|
|
407
|
+
// loading (validateMetadata: false here), not execution — it always
|
|
408
|
+
// calls aiExecuteV2Api({ type: "fetch", apiName }) which resolves the
|
|
409
|
+
// API by name in the editor. After migration that name points to the
|
|
410
|
+
// v3 TS version, so the v2 backup YAML was never actually run.
|
|
411
|
+
// Load the backup YAML directly and execute inline instead.
|
|
412
|
+
const yamlContent = await readFile(sourcePath, "utf-8");
|
|
413
|
+
const apiDefinition = yaml.parse(yamlContent);
|
|
414
|
+
const editorClient = clark.context.peer;
|
|
415
|
+
if (!editorClient)
|
|
416
|
+
throw new Error("No editor client available");
|
|
417
|
+
const executor = new ApiExecutor(editorClient, services.integrationStore);
|
|
418
|
+
const executionResult = await executor.execute({ type: "inline", definition: apiDefinition }, pipelineInputs, { includeStepOutputs: false });
|
|
419
|
+
return { success: executionResult.success, executionResult };
|
|
420
|
+
},
|
|
421
|
+
runV3Sdk: async (integrationIds, sdkInputs) => {
|
|
422
|
+
const editorClient = clark.context.peer;
|
|
423
|
+
if (!editorClient)
|
|
424
|
+
throw new Error("No editor client available");
|
|
425
|
+
const sdkResult = await editorClient.call.aiExecuteSdkApi({
|
|
426
|
+
apiName,
|
|
427
|
+
input: sdkInputs,
|
|
428
|
+
entryPoint: `server/apis/${apiName}/api.ts`,
|
|
429
|
+
integrationIds,
|
|
430
|
+
});
|
|
431
|
+
// SdkApiExecutionResult uses `output` (singular) while
|
|
432
|
+
// ApiExecutionResult uses `outputs` (plural) — normalize here.
|
|
433
|
+
// Also forward the SDK error so divergence diagnostics can show the
|
|
434
|
+
// real reason instead of a bare success: false.
|
|
435
|
+
const errors = sdkResult.error
|
|
436
|
+
? [
|
|
437
|
+
{
|
|
438
|
+
type: "execution",
|
|
439
|
+
message: sdkResult.error.message,
|
|
440
|
+
},
|
|
441
|
+
]
|
|
442
|
+
: undefined;
|
|
443
|
+
return {
|
|
444
|
+
success: sdkResult.success,
|
|
445
|
+
outputs: sdkResult.output,
|
|
446
|
+
errors,
|
|
447
|
+
};
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
// Update checklist with verification metadata. Each branch explicitly
|
|
451
|
+
// clears fields from the previous run that no longer apply (null = clear).
|
|
452
|
+
if (outcome.kind === "passed") {
|
|
453
|
+
await updateChecklistItem({
|
|
454
|
+
appRootDirPath: appRootDir,
|
|
455
|
+
itemId: `api_${apiName}`,
|
|
456
|
+
verificationOutcome: "auto_passed",
|
|
457
|
+
verificationSkippedReason: null,
|
|
458
|
+
lastVerificationAt: new Date().toISOString(),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
else if (outcome.kind === "skipped_mutation") {
|
|
462
|
+
await updateChecklistItem({
|
|
463
|
+
appRootDirPath: appRootDir,
|
|
464
|
+
itemId: `api_${apiName}`,
|
|
465
|
+
verificationSkippedReason: outcome.skipPolicy === "ask" ? "mutation_ask" : "mutation",
|
|
466
|
+
verificationOutcome: null,
|
|
467
|
+
lastVerificationAt: new Date().toISOString(),
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
else if (outcome.kind === "v2_unrunnable") {
|
|
471
|
+
await updateChecklistItem({
|
|
472
|
+
appRootDirPath: appRootDir,
|
|
473
|
+
itemId: `api_${apiName}`,
|
|
474
|
+
verificationSkippedReason: "v2_unrunnable",
|
|
475
|
+
verificationOutcome: null,
|
|
476
|
+
lastVerificationAt: new Date().toISOString(),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
else if (outcome.kind === "both_failed") {
|
|
480
|
+
// Both v2 and v3 errored — most often because the API reads bindings
|
|
481
|
+
// we didn't supply. Per the migration skill, the agent should
|
|
482
|
+
// synthesize realistic inputs and retry rather than treat this as a
|
|
483
|
+
// terminal outcome, so we deliberately do NOT mark `status: completed`
|
|
484
|
+
// here (a previous version did, which contradicted the skill and left
|
|
485
|
+
// the item stuck `completed` even after the agent kept iterating).
|
|
486
|
+
// We still record `verificationSkippedReason: "both_failed"` so the
|
|
487
|
+
// UI can surface the transient state; subsequent diverged/passed
|
|
488
|
+
// attempts clear it via `verificationSkippedReason: null`.
|
|
489
|
+
await updateChecklistItem({
|
|
490
|
+
appRootDirPath: appRootDir,
|
|
491
|
+
itemId: `api_${apiName}`,
|
|
492
|
+
verificationSkippedReason: "both_failed",
|
|
493
|
+
verificationOutcome: null,
|
|
494
|
+
lastVerificationAt: new Date().toISOString(),
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
else if (outcome.kind === "diverged") {
|
|
498
|
+
await updateChecklistItem({
|
|
499
|
+
appRootDirPath: appRootDir,
|
|
500
|
+
itemId: `api_${apiName}`,
|
|
501
|
+
verificationOutcome: null,
|
|
502
|
+
verificationSkippedReason: null,
|
|
503
|
+
lastVerificationAt: new Date().toISOString(),
|
|
504
|
+
lastVerificationDiffPath: diffRelativePath(apiName),
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
else if (outcome.kind === "exhausted") {
|
|
508
|
+
// Hard cap reached. Mark the item failed so classifyItem routes it to
|
|
509
|
+
// the manual-verification lane and the user knows automatic
|
|
510
|
+
// reconciliation gave up. The agent must NOT keep calling
|
|
511
|
+
// runMigrationVerification for this API in this migration run; the
|
|
512
|
+
// tool will return `exhausted` again until the migration is reset.
|
|
513
|
+
await updateChecklistItem({
|
|
514
|
+
appRootDirPath: appRootDir,
|
|
515
|
+
itemId: `api_${apiName}`,
|
|
516
|
+
status: "failed",
|
|
517
|
+
failureReason: `verification_exhausted: hit MAX_VERIFICATION_ATTEMPTS (${MAX_VERIFICATION_ATTEMPTS}) without converging. See scratch/migration-verification/${apiName}.history.json for the per-attempt log.`,
|
|
518
|
+
verificationOutcome: null,
|
|
519
|
+
verificationSkippedReason: null,
|
|
520
|
+
lastVerificationAt: new Date().toISOString(),
|
|
521
|
+
...(outcome.lastDivergence
|
|
522
|
+
? { lastVerificationDiffPath: diffRelativePath(apiName) }
|
|
523
|
+
: {}),
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
// outcome.kind === "error": clear stale fields so classifyItem doesn't
|
|
528
|
+
// misclassify this item as verified or skipped.
|
|
529
|
+
await updateChecklistItem({
|
|
530
|
+
appRootDirPath: appRootDir,
|
|
531
|
+
itemId: `api_${apiName}`,
|
|
532
|
+
verificationOutcome: null,
|
|
533
|
+
verificationSkippedReason: null,
|
|
534
|
+
lastVerificationAt: new Date().toISOString(),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
if (outcome.kind === "diverged") {
|
|
538
|
+
return {
|
|
539
|
+
kind: "diverged",
|
|
540
|
+
summaryForAgent: outcome.diff.summaryForAgent,
|
|
541
|
+
diffPath: diffRelativePath(apiName),
|
|
542
|
+
attempt: outcome.attempt,
|
|
543
|
+
maxAttempts: MAX_VERIFICATION_ATTEMPTS,
|
|
544
|
+
progress: outcome.progress,
|
|
545
|
+
guidance: progressGuidance(outcome.progress, outcome.attempt),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
if (outcome.kind === "exhausted") {
|
|
549
|
+
// Deliberately do NOT include the persisted diff payload (which holds
|
|
550
|
+
// raw response values) in the agent-facing tool result — those values
|
|
551
|
+
// belong to the user's data plane and stay scoped to local scratch
|
|
552
|
+
// and the UI render path. The checklist item already carries
|
|
553
|
+
// `lastVerificationDiffPath` for the UI to read directly from disk.
|
|
554
|
+
return {
|
|
555
|
+
kind: "exhausted",
|
|
556
|
+
attempts: outcome.attempts,
|
|
557
|
+
summary: `\`${apiName}\` hit the hard cap of ${MAX_VERIFICATION_ATTEMPTS} verification attempts without converging. ` +
|
|
558
|
+
`The checklist item has been marked failed; do NOT call runMigrationVerification again for this API in this migration run. ` +
|
|
559
|
+
`Continue to the next API.`,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
if (outcome.kind === "skipped_mutation") {
|
|
563
|
+
return {
|
|
564
|
+
kind: "skipped_mutation",
|
|
565
|
+
mutationDetails: outcome.mutationDetails,
|
|
566
|
+
summary: `\`${apiName}\` makes writes — skipped automatic verification (policy: ${outcome.skipPolicy}). Mark as manually verified once you have confirmed the API behaves correctly.`,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
if (outcome.kind === "passed") {
|
|
570
|
+
return {
|
|
571
|
+
kind: "passed",
|
|
572
|
+
summary: `\`${apiName}\` verified — v2 and v3 outputs matched exactly.`,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (outcome.kind === "v2_unrunnable") {
|
|
576
|
+
return {
|
|
577
|
+
kind: "v2_unrunnable",
|
|
578
|
+
summary: `The original pre-migration version of \`${apiName}\` couldn't be run for comparison (${outcome.reason}), ` +
|
|
579
|
+
`so automatic verification was skipped. The API has been marked complete — ` +
|
|
580
|
+
`test it in the app to confirm it works as expected.`,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (outcome.kind === "both_failed") {
|
|
584
|
+
// Scrub raw upstream error strings before surfacing them to the
|
|
585
|
+
// model — same threat model as the diverged branch's
|
|
586
|
+
// `redactForAgent(failureMessage)`. The persisted server log already
|
|
587
|
+
// omits the raw message; the agent-facing channel must too.
|
|
588
|
+
return {
|
|
589
|
+
kind: "both_failed",
|
|
590
|
+
summary: `Both the original and migrated versions of \`${apiName}\` returned errors ` +
|
|
591
|
+
`(typically because the API reads bindings — Input1.value, Table1.selectedRow, ` +
|
|
592
|
+
`workflow params, etc. — that the verification call did not supply). The item ` +
|
|
593
|
+
`is NOT marked complete; per the migration skill, synthesize realistic mock ` +
|
|
594
|
+
`inputs that satisfy the bindings the API references and call ` +
|
|
595
|
+
`runMigrationVerification again. Only treat this as terminal if the failure is ` +
|
|
596
|
+
`clearly environmental (auth/credentials/integration unavailable). ` +
|
|
597
|
+
`Original error: ${redactForAgent(outcome.v2Error)}. ` +
|
|
598
|
+
`Migrated error: ${redactForAgent(outcome.v3Error)}.`,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
// outcome.kind === "error" — the only remaining case
|
|
602
|
+
return {
|
|
603
|
+
kind: "error",
|
|
604
|
+
reason: outcome.reason,
|
|
605
|
+
};
|
|
606
|
+
},
|
|
607
|
+
}));
|
|
608
|
+
/**
|
|
609
|
+
* Soft UX timeout for the verification pipeline.
|
|
610
|
+
*
|
|
611
|
+
* IMPORTANT: this is a *reporting* timeout, not an execution boundary. The
|
|
612
|
+
* underlying work — `aiExecuteV2Api` and `aiExecuteSdkApi` editor-client RPCs —
|
|
613
|
+
* has no cancellation channel (the socket `MethodSchema` is a plain
|
|
614
|
+
* `(params) => Promise<Response>`; there is no AbortSignal plumbed through
|
|
615
|
+
* the RPC, the browser-side handler, or the SDK runtime). When this rejects,
|
|
616
|
+
* the in-flight execution keeps running until the server completes or the
|
|
617
|
+
* socket disconnects.
|
|
618
|
+
*
|
|
619
|
+
* What this does protect: the verification UI doesn't stall indefinitely on a
|
|
620
|
+
* slow API; the agent gets a deterministic outcome to act on.
|
|
621
|
+
*
|
|
622
|
+
* What this does NOT protect: write-capable side effects already in flight.
|
|
623
|
+
* Mutation safety is provided by the `integrationWritePolicy` check that runs
|
|
624
|
+
* BEFORE this timeout race ever fires — `runMigrationVerification` inspects
|
|
625
|
+
* BOTH the v3 source (via `extractMutations`) and the v2 backup YAML (via
|
|
626
|
+
* `analyzeV2BackupMutations`); if either side has any write or unclassified
|
|
627
|
+
* step, the pipelines are skipped entirely unless the user has explicitly
|
|
628
|
+
* marked every involved integration as "allow". By the time this `withTimeout`
|
|
629
|
+
* runs, any side effects in flight have already been opted into. Verification
|
|
630
|
+
* is not a sandbox; it relies on the policy gate, not on cancellation.
|
|
631
|
+
*
|
|
632
|
+
* Adding real cancellation would require extending `MethodSchema` with abort
|
|
633
|
+
* signaling and propagating it through the editorClient RPC layer. Tracked
|
|
634
|
+
* separately; out of scope for this verification helper.
|
|
635
|
+
*/
|
|
636
|
+
function withTimeout(promise, ms, label) {
|
|
637
|
+
let timer;
|
|
638
|
+
return Promise.race([
|
|
639
|
+
promise.finally(() => clearTimeout(timer)),
|
|
640
|
+
new Promise((_, reject) => {
|
|
641
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms / 1000}s`)), ms);
|
|
642
|
+
}),
|
|
643
|
+
]);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Inspect a v2 backup YAML for mutating steps and the integrations they touch.
|
|
647
|
+
*
|
|
648
|
+
* Returns empty results when the backup is missing or unparseable — the
|
|
649
|
+
* verification flow handles that downstream as `v2_unrunnable`. The goal here
|
|
650
|
+
* is only to feed the policy gate so v2-only writes don't bypass it.
|
|
651
|
+
*
|
|
652
|
+
* Walks the whole tree via `forEachYamlStep` so writes nested inside
|
|
653
|
+
* control-flow blocks (`tryCatch`, `conditional`, `foreach`, `parallel`) are
|
|
654
|
+
* caught — a flat scan over `parsed.blocks` would let nested v2 writes bypass
|
|
655
|
+
* the policy gate, especially now that the language-plugin filter has removed
|
|
656
|
+
* the accidental safety net where javascript's missing policy used to block
|
|
657
|
+
* all verification.
|
|
658
|
+
*
|
|
659
|
+
* Treats `unknown` step classifications (dynamic SQL bodies, plugins we don't
|
|
660
|
+
* recognize) as mutations: better to ask the user than silently run a write.
|
|
661
|
+
*/
|
|
662
|
+
export async function analyzeV2BackupMutations(v2BackupPath) {
|
|
663
|
+
let raw;
|
|
664
|
+
try {
|
|
665
|
+
raw = await readFile(v2BackupPath, "utf-8");
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
return { mutationDetails: [], integrationIds: [] };
|
|
669
|
+
}
|
|
670
|
+
let parsed;
|
|
671
|
+
try {
|
|
672
|
+
parsed = yaml.parse(raw);
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
return { mutationDetails: [], integrationIds: [] };
|
|
676
|
+
}
|
|
677
|
+
const integrationIds = new Set();
|
|
678
|
+
const mutationDetails = [];
|
|
679
|
+
forEachYamlStep(parsed, (step) => {
|
|
680
|
+
const integrationId = step["integration"];
|
|
681
|
+
if (typeof integrationId === "string" && integrationId.length > 0) {
|
|
682
|
+
integrationIds.add(integrationId);
|
|
683
|
+
}
|
|
684
|
+
const classification = classifyYamlStepMutation(step);
|
|
685
|
+
if (classification === "write" || classification === "unknown") {
|
|
686
|
+
const stepName = typeof step["name"] === "string" ? step["name"] : "<unnamed>";
|
|
687
|
+
mutationDetails.push(`v2 step "${stepName}" classified as ${classification}`);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
return {
|
|
691
|
+
mutationDetails,
|
|
692
|
+
integrationIds: Array.from(integrationIds),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
async function readIntegrationWritePolicy(appRootDir) {
|
|
696
|
+
try {
|
|
697
|
+
const raw = await readFile(join(appRootDir, SCRATCH_DIR, "migration-state.json"), "utf-8");
|
|
698
|
+
const parsed = JSON.parse(raw);
|
|
699
|
+
const pol = parsed.integrationWritePolicy;
|
|
700
|
+
if (pol !== null && typeof pol === "object" && !Array.isArray(pol)) {
|
|
701
|
+
return pol;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
// missing or unreadable — default to ask
|
|
706
|
+
}
|
|
707
|
+
return {};
|
|
708
|
+
}
|
|
709
|
+
function mostRestrictivePolicy(policies) {
|
|
710
|
+
if (policies.includes("deny"))
|
|
711
|
+
return "deny";
|
|
712
|
+
if (policies.includes("ask"))
|
|
713
|
+
return "ask";
|
|
714
|
+
return "allow";
|
|
715
|
+
}
|
|
716
|
+
async function getEffectiveMigrationWritePolicy({ appRootDir, apiName, integrationIds, }) {
|
|
717
|
+
const policy = await readIntegrationWritePolicy(appRootDir);
|
|
718
|
+
const v2BackupPath = join(appRootDir, SCRATCH_DIR, V2_BACKUP_DIR, "apis", apiName, "api.yaml");
|
|
719
|
+
// Use IDs from source — not Object.keys(policy) — so a UUID present in
|
|
720
|
+
// source but absent from the policy map gets a real "ask" vote rather
|
|
721
|
+
// than being ignored. Otherwise an "allow" on one integration would be
|
|
722
|
+
// taken as blanket allow for everything the API touches. Union v2 and v3
|
|
723
|
+
// integrations so the gate sees every integration the verification will
|
|
724
|
+
// touch, not just the ones still referenced by v3.
|
|
725
|
+
//
|
|
726
|
+
// Language-runtime "integrations" (`javascript`, `python`) are inline
|
|
727
|
+
// user code, not data integrations; the modal hides them and they have
|
|
728
|
+
// no real write-policy decision attached. Excluding them here keeps a
|
|
729
|
+
// mixed API (e.g. postgres + javascript) from being blocked just because
|
|
730
|
+
// javascript has no policy entry.
|
|
731
|
+
const v2Analysis = await analyzeV2BackupMutations(v2BackupPath);
|
|
732
|
+
const sourceIds = Array.from(new Set([...(integrationIds ?? []), ...v2Analysis.integrationIds])).filter((id) => !LANGUAGE_PLUGIN_IDS.has(id));
|
|
733
|
+
const effective = sourceIds.length > 0
|
|
734
|
+
? mostRestrictivePolicy(sourceIds.map((id) => policy[id] ?? "ask"))
|
|
735
|
+
: "ask";
|
|
736
|
+
return { effective, sourceIds };
|
|
737
|
+
}
|
|
738
|
+
export const checkRunMigrationVerificationPermissions = async (services, input) => {
|
|
739
|
+
if (!input || !input.apiName)
|
|
740
|
+
return PermissionLevel.PROMPT;
|
|
741
|
+
const apiName = input.apiName;
|
|
742
|
+
let source;
|
|
743
|
+
try {
|
|
744
|
+
source = await services.appShell.readFile(`server/apis/${apiName}/api.ts`);
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
return PermissionLevel.PROMPT;
|
|
748
|
+
}
|
|
749
|
+
let mutations;
|
|
750
|
+
try {
|
|
751
|
+
mutations = extractMutations(source);
|
|
752
|
+
}
|
|
753
|
+
catch {
|
|
754
|
+
return PermissionLevel.PROMPT;
|
|
755
|
+
}
|
|
756
|
+
if (mutations.length === 0)
|
|
757
|
+
return PermissionLevel.ALLOW;
|
|
758
|
+
let integrationIds;
|
|
759
|
+
try {
|
|
760
|
+
const ids = extractIntegrationIdsFromSource(source);
|
|
761
|
+
if (ids.length > 0)
|
|
762
|
+
integrationIds = ids;
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
integrationIds = undefined;
|
|
766
|
+
}
|
|
767
|
+
const { effective } = await getEffectiveMigrationWritePolicy({
|
|
768
|
+
appRootDir: services.appRootDirPath,
|
|
769
|
+
apiName,
|
|
770
|
+
integrationIds,
|
|
771
|
+
});
|
|
772
|
+
if (effective === "allow")
|
|
773
|
+
return PermissionLevel.ALLOW;
|
|
774
|
+
if (effective === "deny")
|
|
775
|
+
return PermissionLevel.BLOCK;
|
|
776
|
+
return PermissionLevel.PROMPT;
|
|
777
|
+
};
|
|
778
|
+
/**
|
|
779
|
+
* Extract the most specific failure message from a Pick<ApiExecutionResult>.
|
|
780
|
+
* Tries errors[0].message first, then systemError, then a placeholder.
|
|
781
|
+
*/
|
|
782
|
+
function extractFailureMessage(result, side) {
|
|
783
|
+
return (result.errors?.[0]?.message ??
|
|
784
|
+
result.systemError ??
|
|
785
|
+
`${side} execution returned success: false with no error details`);
|
|
786
|
+
}
|
|
787
|
+
const REDACTED_AGENT_FAILURE_MAX_LEN = 512;
|
|
788
|
+
/**
|
|
789
|
+
* Scrub raw upstream-error strings before they enter LLM context. Upstream
|
|
790
|
+
* integrations can echo back bearer tokens, JWTs, or `api_key=...` query
|
|
791
|
+
* strings in error messages — the existing `sanitizeLogMessage` helper in
|
|
792
|
+
* `util/log-sanitizer.ts` is the canonical scrubber for those patterns and
|
|
793
|
+
* is what the rest of the vite plugin already uses for log lines. We
|
|
794
|
+
* additionally cap the length so a single misbehaving upstream can't crowd
|
|
795
|
+
* out the rest of the LLM context window.
|
|
796
|
+
*
|
|
797
|
+
* Used on the agent-facing channels (`summaryForAgent.failureMessage` for
|
|
798
|
+
* diverged outcomes, the `summary` string for `both_failed`) — the
|
|
799
|
+
* comment at the `getLogger().warn` call site for "Migration verification
|
|
800
|
+
* diverged" already keeps the server log clean of these strings; this
|
|
801
|
+
* applies the same treatment to the durable LLM channel.
|
|
802
|
+
*/
|
|
803
|
+
function redactForAgent(msg, maxLen = REDACTED_AGENT_FAILURE_MAX_LEN) {
|
|
804
|
+
if (msg === undefined || msg === null)
|
|
805
|
+
return "";
|
|
806
|
+
const sanitized = sanitizeLogMessage(String(msg));
|
|
807
|
+
if (sanitized.length <= maxLen)
|
|
808
|
+
return sanitized;
|
|
809
|
+
return sanitized.slice(0, maxLen) + "… [truncated]";
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Reject any `apiName` that could escape `scratch/migration-verification/`.
|
|
813
|
+
* Checklist items are persisted to disk and parsed back in without a strict
|
|
814
|
+
* shape guard, so a poisoned `apiName` containing path separators or `..`
|
|
815
|
+
* segments would let `historyPath`/`diffPath` (and therefore the `rm` calls
|
|
816
|
+
* in `clearVerificationHistory`) target arbitrary files.
|
|
817
|
+
*
|
|
818
|
+
* v2 → v3 migration API names are derived from server YAML/SDK identifiers
|
|
819
|
+
* which are restricted to `[A-Za-z0-9_-]`, so this check is conservative
|
|
820
|
+
* for legitimate inputs.
|
|
821
|
+
*/
|
|
822
|
+
function assertSafeApiName(apiName) {
|
|
823
|
+
if (!/^[A-Za-z0-9_-]+$/.test(apiName)) {
|
|
824
|
+
throw new Error(`Refusing to operate on verification artifacts for unsafe apiName: ${JSON.stringify(apiName)}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
function historyPath(appRootDir, apiName) {
|
|
828
|
+
assertSafeApiName(apiName);
|
|
829
|
+
return join(appRootDir, SCRATCH_DIR, VERIFICATION_DIR, `${apiName}.history.json`);
|
|
830
|
+
}
|
|
831
|
+
function diffPath(appRootDir, apiName) {
|
|
832
|
+
assertSafeApiName(apiName);
|
|
833
|
+
return join(appRootDir, SCRATCH_DIR, VERIFICATION_DIR, `${apiName}.diff.json`);
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Delete the per-API verification history (and stale diff snapshot) so the
|
|
837
|
+
* next `runMigrationVerification` call starts from a clean slate. Called by
|
|
838
|
+
* the "Fix with Clark" retry endpoints — without this, an item that hit the
|
|
839
|
+
* `MAX_VERIFICATION_ATTEMPTS` cap on the first run would immediately return
|
|
840
|
+
* `kind: "exhausted"` on retry, making the retry a no-op.
|
|
841
|
+
*
|
|
842
|
+
* Routed through the same per-API `verificationHistoryQueues` queue used by
|
|
843
|
+
* `runMigrationVerification`. Without this, a clear could race with an
|
|
844
|
+
* in-flight verification: the verification reads history, runs the pipeline,
|
|
845
|
+
* the clear `rm()`s the file, then the verification's append re-creates the
|
|
846
|
+
* file with a stale attempt count — defeating the clear and immediately
|
|
847
|
+
* returning `exhausted` on the user's next retry.
|
|
848
|
+
*
|
|
849
|
+
* Best-effort: missing files are not an error. The safety check on
|
|
850
|
+
* `apiName` (via `historyPath`/`diffPath`) runs *before* enqueueing so a
|
|
851
|
+
* poisoned name throws synchronously, never holding the queue.
|
|
852
|
+
*/
|
|
853
|
+
export async function clearVerificationHistory(appRootDir, apiName) {
|
|
854
|
+
const histPath = historyPath(appRootDir, apiName);
|
|
855
|
+
const dPath = diffPath(appRootDir, apiName);
|
|
856
|
+
await getVerificationHistoryQueue(appRootDir, apiName).enqueue(async () => {
|
|
857
|
+
await Promise.all([
|
|
858
|
+
rm(histPath, { force: true }),
|
|
859
|
+
rm(dPath, { force: true }),
|
|
860
|
+
]);
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Static guidance string keyed off the deterministic `progress` signal — the
|
|
865
|
+
* agent doesn't have to decide whether to keep iterating, the tool tells it.
|
|
866
|
+
*/
|
|
867
|
+
function progressGuidance(progress, attempt) {
|
|
868
|
+
if (progress === "first_attempt") {
|
|
869
|
+
return `First diverged attempt for this API. Use the summaryForAgent entries to revise server/apis/<apiName>/api.ts and call runMigrationVerification again.`;
|
|
870
|
+
}
|
|
871
|
+
if (progress === "converging") {
|
|
872
|
+
return `Converging — the set of diverging (path, kind) pairs changed since the last attempt (#${attempt - 1}). Your previous edit had structural effect. Keep iterating: pick the next summaryForAgent entry, fix it, re-run.`;
|
|
873
|
+
}
|
|
874
|
+
// stalled
|
|
875
|
+
return `STALLED — the set of diverging (path, kind) pairs is identical to attempt #${attempt - 1}. Your last edit had no structural effect on the divergence. Either you edited the wrong block, the edit was a no-op, or this divergence is structurally irreconcilable (v2 uses a YAML construct with no v3 equivalent; v2 reads runtime state v3 can't observe). On your next attempt: try a different fix, or — if you are confident the divergence cannot be reconciled — mark the item failed with failureReason starting with "verification_diverged_irreconcilable:" and continue. Do NOT submit the identical edit again.`;
|
|
876
|
+
}
|
|
877
|
+
function divergenceShapeHash(entries) {
|
|
878
|
+
// Sort tuples so the hash is order-independent — the comparator's emit
|
|
879
|
+
// order is structural-walk based, but small refactors to compareApiResults
|
|
880
|
+
// shouldn't accidentally invalidate the convergence signal.
|
|
881
|
+
const tuples = entries
|
|
882
|
+
.map((e) => `${e.path}::${e.kind}`)
|
|
883
|
+
.sort()
|
|
884
|
+
.join("\n");
|
|
885
|
+
return crypto.createHash("sha256").update(tuples).digest("hex").slice(0, 16);
|
|
886
|
+
}
|
|
887
|
+
function findLastDivergedShapeHash(history) {
|
|
888
|
+
for (let i = history.attempts.length - 1; i >= 0; i--) {
|
|
889
|
+
const entry = history.attempts[i];
|
|
890
|
+
if (entry && entry.kind === "diverged" && entry.divergenceShapeHash) {
|
|
891
|
+
return entry.divergenceShapeHash;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return undefined;
|
|
895
|
+
}
|
|
896
|
+
async function readHistory(appRootDir, apiName) {
|
|
897
|
+
let raw;
|
|
898
|
+
try {
|
|
899
|
+
raw = await readFile(historyPath(appRootDir, apiName), "utf-8");
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
// Missing — fresh API, no history yet. Distinct from "present but
|
|
903
|
+
// unparseable", which we want to surface as a warning so an operator
|
|
904
|
+
// chasing a stuck migration can tell amnesia apart from no-prior-run.
|
|
905
|
+
return { attempts: [] };
|
|
906
|
+
}
|
|
907
|
+
try {
|
|
908
|
+
const parsed = JSON.parse(raw);
|
|
909
|
+
if (parsed &&
|
|
910
|
+
typeof parsed === "object" &&
|
|
911
|
+
"attempts" in parsed &&
|
|
912
|
+
Array.isArray(parsed.attempts)) {
|
|
913
|
+
return parsed;
|
|
914
|
+
}
|
|
915
|
+
getLogger().warn(`Verification history for ${apiName} parsed but had unexpected shape; treating as empty`, { apiName });
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
getLogger().warn(`Verification history for ${apiName} was unreadable JSON (likely a partial-write crash); treating as empty`, {
|
|
919
|
+
apiName,
|
|
920
|
+
error: error instanceof Error ? error.message : String(error),
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
return { attempts: [] };
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Per-(appRootDir, apiName) serialization of the read-modify-write on the
|
|
927
|
+
* `<apiName>.history.json` file. Without this, two parallel
|
|
928
|
+
* `runMigrationVerification` calls for the same API can both observe
|
|
929
|
+
* `attempts.length = MAX-1`, both pass the cap check, and both write — the
|
|
930
|
+
* later clobbers the earlier and the cap is silently bypassed. We re-use the
|
|
931
|
+
* same `OperationQueue` primitive `migration-checklist.ts` uses for the
|
|
932
|
+
* shared checklist mutations.
|
|
933
|
+
*/
|
|
934
|
+
const verificationHistoryQueues = new Map();
|
|
935
|
+
function getVerificationHistoryQueue(appRootDir, apiName) {
|
|
936
|
+
const key = `${appRootDir}\0${apiName}`;
|
|
937
|
+
let queue = verificationHistoryQueues.get(key);
|
|
938
|
+
if (!queue) {
|
|
939
|
+
queue = new OperationQueue();
|
|
940
|
+
verificationHistoryQueues.set(key, queue);
|
|
941
|
+
}
|
|
942
|
+
return queue;
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Append a history entry without enqueueing — caller MUST already hold the
|
|
946
|
+
* per-API verification queue. Used from `runMigrationVerification`'s queued
|
|
947
|
+
* body, where re-entering the same queue would deadlock.
|
|
948
|
+
*/
|
|
949
|
+
async function writeHistoryEntry(appRootDir, apiName, entry) {
|
|
950
|
+
try {
|
|
951
|
+
const history = await readHistory(appRootDir, apiName);
|
|
952
|
+
history.attempts.push(entry);
|
|
953
|
+
const dir = join(appRootDir, SCRATCH_DIR, VERIFICATION_DIR);
|
|
954
|
+
await mkdir(dir, { recursive: true });
|
|
955
|
+
// Atomic write via tmp + rename: a crash mid-write would otherwise
|
|
956
|
+
// leave the file truncated, and `readHistory`'s catch would silently
|
|
957
|
+
// amnesia-reset the cap to zero. POSIX `rename` is atomic on the same
|
|
958
|
+
// filesystem, so a reader either sees the full prior file or the full
|
|
959
|
+
// new file — never a partial write.
|
|
960
|
+
const finalPath = historyPath(appRootDir, apiName);
|
|
961
|
+
const tmpPath = `${finalPath}.tmp`;
|
|
962
|
+
await writeFile(tmpPath, JSON.stringify(history, null, 2), "utf-8");
|
|
963
|
+
await rename(tmpPath, finalPath);
|
|
964
|
+
}
|
|
965
|
+
catch (error) {
|
|
966
|
+
// History persistence is best-effort: a write failure here should not
|
|
967
|
+
// surface as a verification error to the agent (the verification
|
|
968
|
+
// result itself is still valid). Worst case: the next attempt sees
|
|
969
|
+
// stale history and computes `progress: "first_attempt"`, which is a
|
|
970
|
+
// softer failure mode than throwing here.
|
|
971
|
+
getLogger().warn(`Failed to persist verification history for ${apiName}`, {
|
|
972
|
+
apiName,
|
|
973
|
+
error: error instanceof Error ? error.message : String(error),
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Read the on-disk diff JSON written by the most recent diverged run, if any.
|
|
979
|
+
* Used by the `exhausted` outcome to surface "what did we get stuck on" to
|
|
980
|
+
* the agent without re-running the pipeline.
|
|
981
|
+
*/
|
|
982
|
+
async function readLastDivergedDiff(appRootDir, apiName) {
|
|
983
|
+
try {
|
|
984
|
+
const raw = await readFile(diffPath(appRootDir, apiName), "utf-8");
|
|
985
|
+
const parsed = JSON.parse(raw);
|
|
986
|
+
if (parsed &&
|
|
987
|
+
typeof parsed === "object" &&
|
|
988
|
+
"valuesForUi" in parsed &&
|
|
989
|
+
Array.isArray(parsed.valuesForUi)) {
|
|
990
|
+
// We only persist `valuesForUi` (see writeFile call in
|
|
991
|
+
// computeVerificationOutcome), not summaryForAgent — synthesize a
|
|
992
|
+
// minimal ApiComparisonDiff so the exhausted outcome carries
|
|
993
|
+
// something the UI/agent can render.
|
|
994
|
+
return {
|
|
995
|
+
empty: false,
|
|
996
|
+
valuesForUi: parsed.valuesForUi,
|
|
997
|
+
summaryForAgent: [],
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
catch {
|
|
1002
|
+
// No prior diff or unreadable — fine, return undefined.
|
|
1003
|
+
}
|
|
1004
|
+
return undefined;
|
|
1005
|
+
}
|
|
1006
|
+
//# sourceMappingURL=migration-verification.js.map
|