dodo-mcp 1.0.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/LICENSE +21 -0
- package/README.md +425 -0
- package/dist/auth/adapter.js +43 -0
- package/dist/auth/adapter.js.map +1 -0
- package/dist/auth/clientDeletion.js +45 -0
- package/dist/auth/clientDeletion.js.map +1 -0
- package/dist/auth/clients.js +55 -0
- package/dist/auth/clients.js.map +1 -0
- package/dist/auth/constants.js +10 -0
- package/dist/auth/constants.js.map +1 -0
- package/dist/auth/interactions.js +184 -0
- package/dist/auth/interactions.js.map +1 -0
- package/dist/auth/keys.js +56 -0
- package/dist/auth/keys.js.map +1 -0
- package/dist/auth/provider.js +131 -0
- package/dist/auth/provider.js.map +1 -0
- package/dist/auth/verifier.js +74 -0
- package/dist/auth/verifier.js.map +1 -0
- package/dist/cli/kill.js +124 -0
- package/dist/cli/kill.js.map +1 -0
- package/dist/cli/main.js +1393 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/menu.js +127 -0
- package/dist/cli/menu.js.map +1 -0
- package/dist/cli/terminal.js +30 -0
- package/dist/cli/terminal.js.map +1 -0
- package/dist/config/globalConfig.js +144 -0
- package/dist/config/globalConfig.js.map +1 -0
- package/dist/config/limits.js +42 -0
- package/dist/config/limits.js.map +1 -0
- package/dist/config/paths.js +94 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/projectConfig.js +63 -0
- package/dist/config/projectConfig.js.map +1 -0
- package/dist/config/stateImport.js +306 -0
- package/dist/config/stateImport.js.map +1 -0
- package/dist/config/tunnelConfig.js +23 -0
- package/dist/config/tunnelConfig.js.map +1 -0
- package/dist/errors.js +82 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluation/contracts.js +86 -0
- package/dist/evaluation/contracts.js.map +1 -0
- package/dist/evaluation/dodoBench.js +69 -0
- package/dist/evaluation/dodoBench.js.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc/authentication.js +117 -0
- package/dist/ipc/authentication.js.map +1 -0
- package/dist/ipc/client.js +86 -0
- package/dist/ipc/client.js.map +1 -0
- package/dist/ipc/installationClient.js +83 -0
- package/dist/ipc/installationClient.js.map +1 -0
- package/dist/ipc/ownerControl.js +46 -0
- package/dist/ipc/ownerControl.js.map +1 -0
- package/dist/ipc/protocol.js +2 -0
- package/dist/ipc/protocol.js.map +1 -0
- package/dist/ipc/server.js +108 -0
- package/dist/ipc/server.js.map +1 -0
- package/dist/platform/execResolve.js +64 -0
- package/dist/platform/execResolve.js.map +1 -0
- package/dist/platform/fileIdentity.js +26 -0
- package/dist/platform/fileIdentity.js.map +1 -0
- package/dist/platform/fsRetry.js +22 -0
- package/dist/platform/fsRetry.js.map +1 -0
- package/dist/platform/pathPolicy.js +29 -0
- package/dist/platform/pathPolicy.js.map +1 -0
- package/dist/platform/privateFs.js +116 -0
- package/dist/platform/privateFs.js.map +1 -0
- package/dist/platform/processTree.js +31 -0
- package/dist/platform/processTree.js.map +1 -0
- package/dist/platform/shell.js +74 -0
- package/dist/platform/shell.js.map +1 -0
- package/dist/platform/speech.js +94 -0
- package/dist/platform/speech.js.map +1 -0
- package/dist/platform/system.js +28 -0
- package/dist/platform/system.js.map +1 -0
- package/dist/platform/windowsSandbox.js +129 -0
- package/dist/platform/windowsSandbox.js.map +1 -0
- package/dist/projects/federation.js +312 -0
- package/dist/projects/federation.js.map +1 -0
- package/dist/projects/registry.js +291 -0
- package/dist/projects/registry.js.map +1 -0
- package/dist/security/accessMode.js +19 -0
- package/dist/security/accessMode.js.map +1 -0
- package/dist/security/approvals.js +56 -0
- package/dist/security/approvals.js.map +1 -0
- package/dist/security/env.js +57 -0
- package/dist/security/env.js.map +1 -0
- package/dist/security/mutationQueue.js +78 -0
- package/dist/security/mutationQueue.js.map +1 -0
- package/dist/security/policy.js +22 -0
- package/dist/security/policy.js.map +1 -0
- package/dist/security/projectAuthority.js +22 -0
- package/dist/security/projectAuthority.js.map +1 -0
- package/dist/security/redact.js +30 -0
- package/dist/security/redact.js.map +1 -0
- package/dist/server/aiAdmin.js +201 -0
- package/dist/server/aiAdmin.js.map +1 -0
- package/dist/server/appServer.js +461 -0
- package/dist/server/appServer.js.map +1 -0
- package/dist/server/bootstrap.js +185 -0
- package/dist/server/bootstrap.js.map +1 -0
- package/dist/server/configUi/app.css +347 -0
- package/dist/server/configUi/app.js +958 -0
- package/dist/server/configUi/index.html +379 -0
- package/dist/server/configUi/ui/alerts.js +92 -0
- package/dist/server/configUi/ui/dom.js +19 -0
- package/dist/server/configUi/ui/tooltips.js +115 -0
- package/dist/server/configUi/vendor/VERSION.txt +1 -0
- package/dist/server/configUi/vendor/sweetalert2.min.css +1 -0
- package/dist/server/configUi/vendor/sweetalert2.min.js +5 -0
- package/dist/server/configUi/workbench.css +62 -0
- package/dist/server/configUi/workbench.js +394 -0
- package/dist/server/installationRuntime.js +155 -0
- package/dist/server/installationRuntime.js.map +1 -0
- package/dist/server/instructions.js +17 -0
- package/dist/server/instructions.js.map +1 -0
- package/dist/server/ipcDispatch.js +225 -0
- package/dist/server/ipcDispatch.js.map +1 -0
- package/dist/server/localConfig.js +561 -0
- package/dist/server/localConfig.js.map +1 -0
- package/dist/server/optionalServices.js +30 -0
- package/dist/server/optionalServices.js.map +1 -0
- package/dist/server/projectLease.js +46 -0
- package/dist/server/projectLease.js.map +1 -0
- package/dist/server/stdioServer.js +75 -0
- package/dist/server/stdioServer.js.map +1 -0
- package/dist/server/version.js +13 -0
- package/dist/server/version.js.map +1 -0
- package/dist/server/workspaceHost.js +194 -0
- package/dist/server/workspaceHost.js.map +1 -0
- package/dist/services/agent/agentService.js +598 -0
- package/dist/services/agent/agentService.js.map +1 -0
- package/dist/services/agent/contracts.js +102 -0
- package/dist/services/agent/contracts.js.map +1 -0
- package/dist/services/ai/adapters.js +287 -0
- package/dist/services/ai/adapters.js.map +1 -0
- package/dist/services/ai/contracts.js +39 -0
- package/dist/services/ai/contracts.js.map +1 -0
- package/dist/services/ai/credentials.js +77 -0
- package/dist/services/ai/credentials.js.map +1 -0
- package/dist/services/ai/network.js +113 -0
- package/dist/services/ai/network.js.map +1 -0
- package/dist/services/ai/settings.js +178 -0
- package/dist/services/ai/settings.js.map +1 -0
- package/dist/services/ai/subagents.js +468 -0
- package/dist/services/ai/subagents.js.map +1 -0
- package/dist/services/assistance/analysis.js +352 -0
- package/dist/services/assistance/analysis.js.map +1 -0
- package/dist/services/assistance/contracts.js +54 -0
- package/dist/services/assistance/contracts.js.map +1 -0
- package/dist/services/assistance/verification.js +274 -0
- package/dist/services/assistance/verification.js.map +1 -0
- package/dist/services/brain/brainParser.js +97 -0
- package/dist/services/brain/brainParser.js.map +1 -0
- package/dist/services/brain/brainService.js +733 -0
- package/dist/services/brain/brainService.js.map +1 -0
- package/dist/services/brain/brainWorker.js +212 -0
- package/dist/services/brain/brainWorker.js.map +1 -0
- package/dist/services/brain/contracts.js +154 -0
- package/dist/services/brain/contracts.js.map +1 -0
- package/dist/services/brain/protocol.js +2 -0
- package/dist/services/brain/protocol.js.map +1 -0
- package/dist/services/changes/applier.js +544 -0
- package/dist/services/changes/applier.js.map +1 -0
- package/dist/services/changes/contentBudget.js +11 -0
- package/dist/services/changes/contentBudget.js.map +1 -0
- package/dist/services/changes/planner.js +284 -0
- package/dist/services/changes/planner.js.map +1 -0
- package/dist/services/changes/types.js +3 -0
- package/dist/services/changes/types.js.map +1 -0
- package/dist/services/context/contextEngine.js +746 -0
- package/dist/services/context/contextEngine.js.map +1 -0
- package/dist/services/context/contracts.js +88 -0
- package/dist/services/context/contracts.js.map +1 -0
- package/dist/services/desktop/desktopPolicy.js +76 -0
- package/dist/services/desktop/desktopPolicy.js.map +1 -0
- package/dist/services/desktop/desktopService.js +130 -0
- package/dist/services/desktop/desktopService.js.map +1 -0
- package/dist/services/desktop/nativeBackend.js +174 -0
- package/dist/services/desktop/nativeBackend.js.map +1 -0
- package/dist/services/desktop/protocol.js +32 -0
- package/dist/services/desktop/protocol.js.map +1 -0
- package/dist/services/files/readService.js +136 -0
- package/dist/services/files/readService.js.map +1 -0
- package/dist/services/git/gitService.js +369 -0
- package/dist/services/git/gitService.js.map +1 -0
- package/dist/services/intelligence/intelService.js +136 -0
- package/dist/services/intelligence/intelService.js.map +1 -0
- package/dist/services/intelligence/protocol.js +2 -0
- package/dist/services/intelligence/protocol.js.map +1 -0
- package/dist/services/intelligence/tsWorker.js +419 -0
- package/dist/services/intelligence/tsWorker.js.map +1 -0
- package/dist/services/jobs/commandInput.js +34 -0
- package/dist/services/jobs/commandInput.js.map +1 -0
- package/dist/services/jobs/jobManager.js +398 -0
- package/dist/services/jobs/jobManager.js.map +1 -0
- package/dist/services/jobs/sandbox.js +156 -0
- package/dist/services/jobs/sandbox.js.map +1 -0
- package/dist/services/jobs/sandboxWiring.js +37 -0
- package/dist/services/jobs/sandboxWiring.js.map +1 -0
- package/dist/services/jobs/spool.js +112 -0
- package/dist/services/jobs/spool.js.map +1 -0
- package/dist/services/jobs/windowsSandboxChild.js +24 -0
- package/dist/services/jobs/windowsSandboxChild.js.map +1 -0
- package/dist/services/jobs/windowsSandboxWorker.js +155 -0
- package/dist/services/jobs/windowsSandboxWorker.js.map +1 -0
- package/dist/services/lsp/documentUri.js +16 -0
- package/dist/services/lsp/documentUri.js.map +1 -0
- package/dist/services/lsp/jsonrpc.js +382 -0
- package/dist/services/lsp/jsonrpc.js.map +1 -0
- package/dist/services/lsp/lspService.js +1051 -0
- package/dist/services/lsp/lspService.js.map +1 -0
- package/dist/services/memory/contracts.js +94 -0
- package/dist/services/memory/contracts.js.map +1 -0
- package/dist/services/memory/memoryService.js +683 -0
- package/dist/services/memory/memoryService.js.map +1 -0
- package/dist/services/multimodal/browserService.js +407 -0
- package/dist/services/multimodal/browserService.js.map +1 -0
- package/dist/services/multimodal/byteRange.js +31 -0
- package/dist/services/multimodal/byteRange.js.map +1 -0
- package/dist/services/multimodal/contracts.js +59 -0
- package/dist/services/multimodal/contracts.js.map +1 -0
- package/dist/services/multimodal/gameService.js +95 -0
- package/dist/services/multimodal/gameService.js.map +1 -0
- package/dist/services/multimodal/images.js +107 -0
- package/dist/services/multimodal/images.js.map +1 -0
- package/dist/services/multimodal/mediaService.js +279 -0
- package/dist/services/multimodal/mediaService.js.map +1 -0
- package/dist/services/multimodal/mediaWorker.js +143 -0
- package/dist/services/multimodal/mediaWorker.js.map +1 -0
- package/dist/services/multimodal/multimodalService.js +51 -0
- package/dist/services/multimodal/multimodalService.js.map +1 -0
- package/dist/services/multimodal/network.js +69 -0
- package/dist/services/multimodal/network.js.map +1 -0
- package/dist/services/multimodal/operations.js +40 -0
- package/dist/services/multimodal/operations.js.map +1 -0
- package/dist/services/multimodal/storage.js +160 -0
- package/dist/services/multimodal/storage.js.map +1 -0
- package/dist/services/multimodal/workflowService.js +179 -0
- package/dist/services/multimodal/workflowService.js.map +1 -0
- package/dist/services/overview.js +284 -0
- package/dist/services/overview.js.map +1 -0
- package/dist/services/resources/casStore.js +304 -0
- package/dist/services/resources/casStore.js.map +1 -0
- package/dist/services/resources/contracts.js +75 -0
- package/dist/services/resources/contracts.js.map +1 -0
- package/dist/services/resources/mime.js +77 -0
- package/dist/services/resources/mime.js.map +1 -0
- package/dist/services/resources/providerRegistry.js +22 -0
- package/dist/services/resources/providerRegistry.js.map +1 -0
- package/dist/services/resources/providers.js +120 -0
- package/dist/services/resources/providers.js.map +1 -0
- package/dist/services/resources/resourceService.js +282 -0
- package/dist/services/resources/resourceService.js.map +1 -0
- package/dist/services/runtime/contracts.js +65 -0
- package/dist/services/runtime/contracts.js.map +1 -0
- package/dist/services/runtime/runtimeService.js +364 -0
- package/dist/services/runtime/runtimeService.js.map +1 -0
- package/dist/services/schedules/scheduleService.js +198 -0
- package/dist/services/schedules/scheduleService.js.map +1 -0
- package/dist/services/search/searchService.js +434 -0
- package/dist/services/search/searchService.js.map +1 -0
- package/dist/services/search/searchWorker.js +58 -0
- package/dist/services/search/searchWorker.js.map +1 -0
- package/dist/setup/componentProbes.js +259 -0
- package/dist/setup/componentProbes.js.map +1 -0
- package/dist/setup/download.js +139 -0
- package/dist/setup/download.js.map +1 -0
- package/dist/setup/managedTools.js +118 -0
- package/dist/setup/managedTools.js.map +1 -0
- package/dist/setup/sandboxProbe.js +172 -0
- package/dist/setup/sandboxProbe.js.map +1 -0
- package/dist/setup/setup.js +522 -0
- package/dist/setup/setup.js.map +1 -0
- package/dist/setup/windowsChromiumAcl.js +56 -0
- package/dist/setup/windowsChromiumAcl.js.map +1 -0
- package/dist/setup/windowsSandboxInstall.js +94 -0
- package/dist/setup/windowsSandboxInstall.js.map +1 -0
- package/dist/setup/windowsSpeechInstall.js +86 -0
- package/dist/setup/windowsSpeechInstall.js.map +1 -0
- package/dist/store/db.js +711 -0
- package/dist/store/db.js.map +1 -0
- package/dist/store/scopeEncoding.js +15 -0
- package/dist/store/scopeEncoding.js.map +1 -0
- package/dist/store/store.js +467 -0
- package/dist/store/store.js.map +1 -0
- package/dist/tools/agentRuntimeTools.js +216 -0
- package/dist/tools/agentRuntimeTools.js.map +1 -0
- package/dist/tools/agentTools.js +317 -0
- package/dist/tools/agentTools.js.map +1 -0
- package/dist/tools/assistanceTools.js +126 -0
- package/dist/tools/assistanceTools.js.map +1 -0
- package/dist/tools/brainTools.js +130 -0
- package/dist/tools/brainTools.js.map +1 -0
- package/dist/tools/catalog.js +30 -0
- package/dist/tools/catalog.js.map +1 -0
- package/dist/tools/changeTools.js +188 -0
- package/dist/tools/changeTools.js.map +1 -0
- package/dist/tools/context.js +243 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/contextTools.js +57 -0
- package/dist/tools/contextTools.js.map +1 -0
- package/dist/tools/coreCatalog.js +82 -0
- package/dist/tools/coreCatalog.js.map +1 -0
- package/dist/tools/desktopTools.js +41 -0
- package/dist/tools/desktopTools.js.map +1 -0
- package/dist/tools/directTools.js +273 -0
- package/dist/tools/directTools.js.map +1 -0
- package/dist/tools/envelope.js +71 -0
- package/dist/tools/envelope.js.map +1 -0
- package/dist/tools/extraTools.js +298 -0
- package/dist/tools/extraTools.js.map +1 -0
- package/dist/tools/gitTools.js +93 -0
- package/dist/tools/gitTools.js.map +1 -0
- package/dist/tools/jobTools.js +454 -0
- package/dist/tools/jobTools.js.map +1 -0
- package/dist/tools/memoryTools.js +102 -0
- package/dist/tools/memoryTools.js.map +1 -0
- package/dist/tools/metaTools.js +153 -0
- package/dist/tools/metaTools.js.map +1 -0
- package/dist/tools/multimodalTools.js +262 -0
- package/dist/tools/multimodalTools.js.map +1 -0
- package/dist/tools/projectTools.js +261 -0
- package/dist/tools/projectTools.js.map +1 -0
- package/dist/tools/resourceTools.js +81 -0
- package/dist/tools/resourceTools.js.map +1 -0
- package/dist/tools/runtimeTools.js +137 -0
- package/dist/tools/runtimeTools.js.map +1 -0
- package/dist/tools/scheduleTools.js +10 -0
- package/dist/tools/scheduleTools.js.map +1 -0
- package/dist/tools/semanticTools.js +141 -0
- package/dist/tools/semanticTools.js.map +1 -0
- package/dist/tools/subagentTools.js +32 -0
- package/dist/tools/subagentTools.js.map +1 -0
- package/dist/tools/surface.js +644 -0
- package/dist/tools/surface.js.map +1 -0
- package/dist/tunnel/control.js +113 -0
- package/dist/tunnel/control.js.map +1 -0
- package/dist/tunnel/credentials.js +392 -0
- package/dist/tunnel/credentials.js.map +1 -0
- package/dist/tunnel/log.js +84 -0
- package/dist/tunnel/log.js.map +1 -0
- package/dist/tunnel/runtime.js +55 -0
- package/dist/tunnel/runtime.js.map +1 -0
- package/dist/tunnel/supervisor.js +280 -0
- package/dist/tunnel/supervisor.js.map +1 -0
- package/dist/util/bytes.js +53 -0
- package/dist/util/bytes.js.map +1 -0
- package/dist/util/hash.js +53 -0
- package/dist/util/hash.js.map +1 -0
- package/dist/workspace/fs.js +305 -0
- package/dist/workspace/fs.js.map +1 -0
- package/dist/workspace/identity.js +17 -0
- package/dist/workspace/identity.js.map +1 -0
- package/dist/workspace/ignores.js +173 -0
- package/dist/workspace/ignores.js.map +1 -0
- package/dist/workspace/root.js +59 -0
- package/dist/workspace/root.js.map +1 -0
- package/docs/AGENT_RUNTIME.md +154 -0
- package/docs/AI_PROVIDERS.md +110 -0
- package/docs/ARCHITECTURE.md +331 -0
- package/docs/ASSISTANCE.md +31 -0
- package/docs/AUTH.md +59 -0
- package/docs/BRAIN.md +92 -0
- package/docs/COMPATIBILITY.md +92 -0
- package/docs/CONFIRMATION.md +13 -0
- package/docs/CONTEXT.md +87 -0
- package/docs/DESKTOP.md +35 -0
- package/docs/EVALUATION.md +86 -0
- package/docs/LARGE_INPUTS.md +21 -0
- package/docs/LSP.md +21 -0
- package/docs/MANUAL_ACCEPTANCE.md +283 -0
- package/docs/MEMORY.md +128 -0
- package/docs/MULTIMODAL.md +32 -0
- package/docs/PROJECTS.md +146 -0
- package/docs/RELEASE_1.0.0.md +111 -0
- package/docs/RELEASE_NOTES.md +92 -0
- package/docs/RESOURCES.md +83 -0
- package/docs/RUNTIME.md +125 -0
- package/docs/SCHEDULES.md +23 -0
- package/docs/SECURITY.md +250 -0
- package/docs/TEST_REPORT.md +312 -0
- package/docs/TUNNEL.md +86 -0
- package/docs/WEB_CLIENTS.md +43 -0
- package/docs/WINDOWS.md +79 -0
- package/docs/adr/001-one-process-one-workspace.md +22 -0
- package/docs/adr/002-external-tunnel.md +19 -0
- package/docs/adr/003-sdk-v2-legacy-stateless.md +20 -0
- package/docs/adr/004-embedded-oauth-provider.md +19 -0
- package/docs/adr/005-static-client-baseline.md +19 -0
- package/docs/adr/006-guarded-fs-explicit-exec.md +16 -0
- package/docs/adr/007-preview-hash-journal.md +18 -0
- package/docs/adr/008-ts-js-semantics-first.md +17 -0
- package/docs/adr/009-jobs-polling.md +18 -0
- package/docs/adr/010-sqlite-better-sqlite3.md +18 -0
- package/docs/adr/011-env-example-secure-default.md +19 -0
- package/docs/adr/012-workspace-scope-grant.md +24 -0
- package/docs/adr/013-loopback-proxy-trust.md +18 -0
- package/docs/adr/014-direct-coding-tier.md +48 -0
- package/docs/adr/015-coding-agent-parity.md +56 -0
- package/docs/adr/016-oidc-openid-consent.md +15 -0
- package/docs/adr/017-installation-identity-local-config.md +44 -0
- package/docs/adr/018-access-scope-encoding.md +11 -0
- package/docs/adr/019-runtime-workspace-switch.md +59 -0
- package/docs/adr/020-desktop-access.md +50 -0
- package/docs/adr/021-persistent-desktop-consent.md +39 -0
- package/docs/adr/022-owner-kill-preserves-login.md +30 -0
- package/docs/adr/023-large-coding-inputs.md +49 -0
- package/docs/adr/024-usage-rounds-and-schedules.md +72 -0
- package/docs/adr/025-owner-chat-whitelist.md +53 -0
- package/docs/adr/026-remove-conversation-confirmation.md +35 -0
- package/docs/adr/027-workspace-client-list.md +20 -0
- package/docs/adr/028-owner-client-deletion.md +25 -0
- package/docs/adr/029-compact-tool-surface.md +121 -0
- package/docs/adr/030-windows-native-candidate.md +71 -0
- package/docs/adr/031-owner-selected-tunnel-supervision.md +13 -0
- package/docs/adr/032-owner-project-registry.md +43 -0
- package/docs/adr/033-read-only-project-federation.md +61 -0
- package/docs/adr/034-universal-resource-cas.md +60 -0
- package/docs/adr/035-project-brain-incremental-index.md +51 -0
- package/docs/adr/036-context-engine-evidence.md +53 -0
- package/docs/adr/037-owner-reviewed-memory.md +46 -0
- package/docs/adr/038-runtime-intelligence.md +46 -0
- package/docs/adr/039-advanced-agent-runtime.md +57 -0
- package/docs/adr/040-dodobench-release-gate.md +43 -0
- package/docs/adr/041-local-macos-docker-linux-gates.md +32 -0
- package/docs/adr/042-project-directory-generation-identity.md +36 -0
- package/docs/adr/043-trusted-self-hosted-platform-gates.md +38 -0
- package/docs/adr/044-global-launcher-and-cli-menu.md +43 -0
- package/docs/adr/045-ai-providers-multiproject.md +56 -0
- package/docs/adr/046-personal-access-mode.md +32 -0
- package/docs/adr/README.md +64 -0
- package/examples/.dodo.json.example +20 -0
- package/examples/cloudflared.yml +23 -0
- package/examples/reverse-proxy-fixture.mjs +37 -0
- package/native/desktop-linux.py +387 -0
- package/native/desktop-windows.cs +180 -0
- package/native/desktop.swift +212 -0
- package/package.json +145 -0
- package/schemas/global-config.schema.json +497 -0
- package/schemas/project-config.schema.json +75 -0
- package/schemas/tools.compact.json +3236 -0
- package/schemas/tools.hybrid.json +8194 -0
- package/schemas/tools.json +34518 -0
- package/scripts/guard-publish.mjs +12 -0
- package/scripts/setup-multimodal.mjs +70 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { DodoError, toDodoError } from '../../errors.js';
|
|
4
|
+
import { digestOf, newId } from '../../util/hash.js';
|
|
5
|
+
import { liveAccess } from '../multimodal/storage.js';
|
|
6
|
+
import { BrainParser } from './brainParser.js';
|
|
7
|
+
import { BRAIN_FILE_MAX_BYTES, BRAIN_QUERY_MAX, BRAIN_SCHEMA_VERSION, BrainRunMetrics, ParsedBrainFile, } from './contracts.js';
|
|
8
|
+
import ts from 'typescript';
|
|
9
|
+
export const BRAIN_PARSER_VERSION = `typescript-${ts.version}:brain-1`;
|
|
10
|
+
const AUTO_INITIAL_DELAY_MS = 750;
|
|
11
|
+
const AUTO_REFRESH_MS = 5000;
|
|
12
|
+
const RUN_HISTORY_MAX = 20;
|
|
13
|
+
const EMPTY_METRICS = {
|
|
14
|
+
scannedFiles: 0, parsedFiles: 0, reusedFiles: 0, movedFiles: 0,
|
|
15
|
+
removedFiles: 0, skippedFiles: 0, affectedFiles: 0, nodes: 0,
|
|
16
|
+
edges: 0, syntaxErrors: 0,
|
|
17
|
+
};
|
|
18
|
+
const SCRIPT = /\.(?:[cm]?[jt]sx?)$/i;
|
|
19
|
+
function principalKey(ctx) { return digestOf({ grantId: ctx.principal.grantId, clientId: ctx.principal.clientId }); }
|
|
20
|
+
function safeMessage(error) { return toDodoError(error).message.slice(0, 600); }
|
|
21
|
+
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
22
|
+
function sameStat(left, right) { return left.bytes === right.bytes && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs; }
|
|
23
|
+
function externalPackage(specifier) {
|
|
24
|
+
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#'))
|
|
25
|
+
return undefined;
|
|
26
|
+
const parts = specifier.split('/');
|
|
27
|
+
return specifier.startsWith('@') ? (parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier) : parts[0];
|
|
28
|
+
}
|
|
29
|
+
/** Durable, bounded, source-verifying project structure index. */
|
|
30
|
+
export class ProjectBrainService {
|
|
31
|
+
services;
|
|
32
|
+
parser;
|
|
33
|
+
configHash;
|
|
34
|
+
cursorKey;
|
|
35
|
+
namespace;
|
|
36
|
+
initialTimer;
|
|
37
|
+
refreshTimer;
|
|
38
|
+
currentPromise;
|
|
39
|
+
runToken = 0;
|
|
40
|
+
closed = false;
|
|
41
|
+
forceFull = false;
|
|
42
|
+
constructor(services, installSecret) {
|
|
43
|
+
this.services = services;
|
|
44
|
+
this.parser = new BrainParser(services.limits);
|
|
45
|
+
this.configHash = digestOf({
|
|
46
|
+
schema: BRAIN_SCHEMA_VERSION,
|
|
47
|
+
parser: BRAIN_PARSER_VERSION,
|
|
48
|
+
fileMax: BRAIN_FILE_MAX_BYTES,
|
|
49
|
+
fileCount: services.limits.semanticFilesMax,
|
|
50
|
+
secretDeny: services.config.secretDeny,
|
|
51
|
+
secretAllow: services.config.secretAllow,
|
|
52
|
+
excludes: services.projectConfig.config.exclude,
|
|
53
|
+
});
|
|
54
|
+
const registered = services.store.db.prepare('SELECT id FROM project_registry WHERE workspace_id=? AND removed_at IS NULL ORDER BY updated_at DESC LIMIT 1').get(services.workspaceId);
|
|
55
|
+
this.namespace = registered?.id ?? services.workspaceId;
|
|
56
|
+
this.cursorKey = createHmac('sha256', installSecret).update('dodo-project-brain-cursor-v1').digest();
|
|
57
|
+
this.recoverState();
|
|
58
|
+
this.initialTimer = setTimeout(() => { void this.autoIndex(); }, AUTO_INITIAL_DELAY_MS);
|
|
59
|
+
this.initialTimer.unref();
|
|
60
|
+
this.refreshTimer = setInterval(() => { void this.autoIndex(); }, AUTO_REFRESH_MS);
|
|
61
|
+
this.refreshTimer.unref();
|
|
62
|
+
}
|
|
63
|
+
async close() {
|
|
64
|
+
if (this.closed)
|
|
65
|
+
return;
|
|
66
|
+
this.closed = true;
|
|
67
|
+
if (this.initialTimer)
|
|
68
|
+
clearTimeout(this.initialTimer);
|
|
69
|
+
if (this.refreshTimer)
|
|
70
|
+
clearInterval(this.refreshTimer);
|
|
71
|
+
this.runToken += 1;
|
|
72
|
+
this.parser.cancel();
|
|
73
|
+
await this.currentPromise?.catch(() => undefined);
|
|
74
|
+
await this.parser.close();
|
|
75
|
+
}
|
|
76
|
+
status() {
|
|
77
|
+
this.check();
|
|
78
|
+
return this.statusFromRow(this.state());
|
|
79
|
+
}
|
|
80
|
+
async start(mode, trigger = 'manual') {
|
|
81
|
+
this.check();
|
|
82
|
+
const state = this.state();
|
|
83
|
+
if (state.paused === 1)
|
|
84
|
+
throw new DodoError('CONFLICT', 'project indexing is paused; resume it before rebuilding');
|
|
85
|
+
if (this.currentPromise || state.status === 'running')
|
|
86
|
+
throw new DodoError('CONFLICT', 'project indexing is already running', { retryable: true, detail: { runId: state.active_run_id } });
|
|
87
|
+
if (!this.parser.available())
|
|
88
|
+
throw new DodoError('NOT_SUPPORTED', 'project brain parser is not built; run npm run build');
|
|
89
|
+
const runId = newId('brainrun', 12);
|
|
90
|
+
const token = ++this.runToken;
|
|
91
|
+
const effectiveMode = this.forceFull ? 'full' : mode;
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
this.services.store.db.prepare(`UPDATE brain_index_state SET status='running',generation=generation+1,active_run_id=?,last_run_id=?,last_started_at=?,last_error=NULL WHERE workspace_id=?`)
|
|
94
|
+
.run(runId, runId, now, this.services.workspaceId);
|
|
95
|
+
this.services.store.db.prepare('INSERT INTO brain_runs(id,workspace_id,mode,trigger_kind,status,started_at,metrics) VALUES (?,?,?,?,?,?,?)')
|
|
96
|
+
.run(runId, this.services.workspaceId, effectiveMode, trigger, 'running', now, JSON.stringify(EMPTY_METRICS));
|
|
97
|
+
this.currentPromise = this.execute(runId, effectiveMode, token)
|
|
98
|
+
.finally(() => { this.currentPromise = undefined; });
|
|
99
|
+
return { runId, changed: true };
|
|
100
|
+
}
|
|
101
|
+
async wait(runId, waitMs) {
|
|
102
|
+
const deadline = Date.now() + waitMs;
|
|
103
|
+
while (Date.now() < deadline) {
|
|
104
|
+
const row = this.services.store.db.prepare('SELECT status FROM brain_runs WHERE id=? AND workspace_id=?').get(runId, this.services.workspaceId);
|
|
105
|
+
if (!row || row.status !== 'running')
|
|
106
|
+
break;
|
|
107
|
+
await sleep(50);
|
|
108
|
+
}
|
|
109
|
+
return this.status();
|
|
110
|
+
}
|
|
111
|
+
async pause(paused) {
|
|
112
|
+
this.check();
|
|
113
|
+
const before = this.state().paused === 1;
|
|
114
|
+
if (before === paused)
|
|
115
|
+
return { changed: false };
|
|
116
|
+
if (paused && this.currentPromise) {
|
|
117
|
+
this.runToken += 1;
|
|
118
|
+
this.parser.cancel();
|
|
119
|
+
await this.currentPromise.catch(() => undefined);
|
|
120
|
+
}
|
|
121
|
+
this.services.store.db.prepare("UPDATE brain_index_state SET paused=?,status=? WHERE workspace_id=?")
|
|
122
|
+
.run(paused ? 1 : 0, paused ? 'paused' : 'idle', this.services.workspaceId);
|
|
123
|
+
if (!paused)
|
|
124
|
+
this.initialTimer = setTimeout(() => { void this.autoIndex(); }, 0);
|
|
125
|
+
return { changed: true };
|
|
126
|
+
}
|
|
127
|
+
async cancel(runId) {
|
|
128
|
+
this.check();
|
|
129
|
+
const state = this.state();
|
|
130
|
+
if (!this.currentPromise || state.status !== 'running' || (runId !== undefined && runId !== state.active_run_id))
|
|
131
|
+
return { changed: false };
|
|
132
|
+
this.runToken += 1;
|
|
133
|
+
this.parser.cancel();
|
|
134
|
+
await this.currentPromise.catch(() => undefined);
|
|
135
|
+
return { changed: true };
|
|
136
|
+
}
|
|
137
|
+
async query(ctx, input) {
|
|
138
|
+
this.check();
|
|
139
|
+
liveAccess(ctx, 'dodo:read');
|
|
140
|
+
let normalizedPath;
|
|
141
|
+
if (input.path !== undefined) {
|
|
142
|
+
normalizedPath = this.services.wfs.normalizeRel(input.path);
|
|
143
|
+
const resolved = this.services.wfs.resolve(normalizedPath, { allowMissing: true });
|
|
144
|
+
if (resolved.stat && !resolved.stat.isFile())
|
|
145
|
+
throw new DodoError('PATH_DENIED', 'brain path filter must identify a file');
|
|
146
|
+
}
|
|
147
|
+
const signature = digestOf({ query: input.query ?? '', path: normalizedPath ?? '', nodeTypes: input.nodeTypes ?? [], edgeTypes: input.edgeTypes ?? [], includeStale: input.includeStale });
|
|
148
|
+
let nodeOffset = 0, edgeOffset = 0;
|
|
149
|
+
if (input.cursor)
|
|
150
|
+
({ n: nodeOffset, e: edgeOffset } = this.verifyCursor(ctx, input.cursor, signature));
|
|
151
|
+
const nodes = [], edges = [];
|
|
152
|
+
const freshness = new Map();
|
|
153
|
+
let staleOmitted = 0, inaccessibleOmitted = 0, nodeExhausted = false, edgeExhausted = false;
|
|
154
|
+
const take = Math.min(input.limit, BRAIN_QUERY_MAX);
|
|
155
|
+
while (nodes.length + edges.length < take && (!nodeExhausted || !edgeExhausted)) {
|
|
156
|
+
if (!nodeExhausted) {
|
|
157
|
+
const batch = Math.max(20, take * 2);
|
|
158
|
+
const rows = this.nodeRows(input.query, normalizedPath, input.nodeTypes, nodeOffset, batch);
|
|
159
|
+
let consumed = 0;
|
|
160
|
+
for (const row of rows) {
|
|
161
|
+
consumed += 1;
|
|
162
|
+
nodeOffset += 1;
|
|
163
|
+
const state = this.sourceFreshness(String(row['path']), String(row['source_hash']), freshness);
|
|
164
|
+
if (state === 'inaccessible') {
|
|
165
|
+
inaccessibleOmitted += 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (state !== 'current' && !input.includeStale) {
|
|
169
|
+
staleOmitted += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
nodes.push(this.materializeNode(row, state));
|
|
173
|
+
if (nodes.length + edges.length >= take)
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
nodeExhausted = consumed === rows.length && rows.length < batch;
|
|
177
|
+
if (nodes.length + edges.length >= take)
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
if (!edgeExhausted) {
|
|
181
|
+
const batch = Math.max(20, take * 2);
|
|
182
|
+
const rows = this.edgeRows(input.query, normalizedPath, input.edgeTypes, edgeOffset, batch);
|
|
183
|
+
let consumed = 0;
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
consumed += 1;
|
|
186
|
+
edgeOffset += 1;
|
|
187
|
+
const state = this.sourceFreshness(String(row['source_path']), String(row['source_hash']), freshness);
|
|
188
|
+
if (state === 'inaccessible') {
|
|
189
|
+
inaccessibleOmitted += 1;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (state !== 'current' && !input.includeStale) {
|
|
193
|
+
staleOmitted += 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
edges.push(this.materializeEdge(row, state));
|
|
197
|
+
if (nodes.length + edges.length >= take)
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
edgeExhausted = consumed === rows.length && rows.length < batch;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const truncated = !nodeExhausted || !edgeExhausted;
|
|
204
|
+
const nextCursor = truncated ? this.signCursor(ctx, { v: 1, n: nodeOffset, e: edgeOffset, q: signature, w: this.services.workspaceId, a: principalKey(ctx), exp: Date.now() + 10 * 60 * 1000 }) : null;
|
|
205
|
+
return {
|
|
206
|
+
data: {
|
|
207
|
+
status: this.status(), nodes, edges, staleOmitted, inaccessibleOmitted, truncated, nextCursor,
|
|
208
|
+
evidence: { sourceVerified: true, checkedAt: Date.now(), note: 'Every returned item was re-authorized and compared with the current guarded source hash.' },
|
|
209
|
+
},
|
|
210
|
+
truncated,
|
|
211
|
+
nextCursor,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async symbol(ctx, uri) {
|
|
215
|
+
this.check();
|
|
216
|
+
liveAccess(ctx, 'dodo:read');
|
|
217
|
+
if (!uri.startsWith(`symbol://${this.namespace}/`))
|
|
218
|
+
throw new DodoError('WORKSPACE_MISMATCH', 'symbol URI belongs to another project namespace');
|
|
219
|
+
const row = this.services.store.db.prepare('SELECT * FROM brain_nodes WHERE workspace_id=? AND uri=? AND node_type=?').get(this.services.workspaceId, uri, 'symbol');
|
|
220
|
+
if (!row)
|
|
221
|
+
throw new DodoError('NOT_FOUND', 'symbol is not present in the current project brain');
|
|
222
|
+
const state = this.sourceFreshness(row['path'], row['source_hash'], new Map());
|
|
223
|
+
if (state === 'inaccessible')
|
|
224
|
+
throw new DodoError('PATH_DENIED', 'symbol source is no longer readable under the workspace policy');
|
|
225
|
+
return this.materializeNode(row, state);
|
|
226
|
+
}
|
|
227
|
+
async autoIndex() {
|
|
228
|
+
if (this.closed || this.currentPromise)
|
|
229
|
+
return;
|
|
230
|
+
const state = this.state();
|
|
231
|
+
if (state.paused === 1)
|
|
232
|
+
return;
|
|
233
|
+
try {
|
|
234
|
+
await this.start(this.forceFull ? 'full' : 'incremental', 'automatic');
|
|
235
|
+
}
|
|
236
|
+
catch { /* status and the next bounded poll expose/retry failures */ }
|
|
237
|
+
}
|
|
238
|
+
recoverState() {
|
|
239
|
+
const existing = this.services.store.db.prepare('SELECT * FROM brain_index_state WHERE workspace_id=?').get(this.services.workspaceId);
|
|
240
|
+
if (!existing) {
|
|
241
|
+
this.services.store.db.prepare(`INSERT INTO brain_index_state
|
|
242
|
+
(workspace_id,namespace,schema_version,parser_version,config_hash,status,paused,generation,metrics)
|
|
243
|
+
VALUES (?,?,?,?,?,'idle',0,0,?)`)
|
|
244
|
+
.run(this.services.workspaceId, this.namespace, BRAIN_SCHEMA_VERSION, BRAIN_PARSER_VERSION, this.configHash, JSON.stringify(EMPTY_METRICS));
|
|
245
|
+
this.forceFull = true;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const mismatch = existing.schema_version !== BRAIN_SCHEMA_VERSION || existing.parser_version !== BRAIN_PARSER_VERSION || existing.config_hash !== this.configHash || existing.namespace !== this.namespace;
|
|
249
|
+
if (existing.status === 'running') {
|
|
250
|
+
this.forceFull = true;
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
this.services.store.db.prepare("UPDATE brain_runs SET status='interrupted',ended_at=?,error=? WHERE id=? AND status='running'").run(now, 'server stopped before index commit', existing.active_run_id);
|
|
253
|
+
this.services.store.db.prepare("UPDATE brain_index_state SET status='interrupted',active_run_id=NULL,last_completed_at=?,last_error=? WHERE workspace_id=?")
|
|
254
|
+
.run(now, 'previous indexing run was interrupted; a recovery scan is queued', this.services.workspaceId);
|
|
255
|
+
}
|
|
256
|
+
if (mismatch) {
|
|
257
|
+
this.forceFull = true;
|
|
258
|
+
this.services.store.db.prepare(`UPDATE brain_index_state SET namespace=?,schema_version=?,parser_version=?,config_hash=?,status='idle',active_run_id=NULL,last_error=? WHERE workspace_id=?`)
|
|
259
|
+
.run(this.namespace, BRAIN_SCHEMA_VERSION, BRAIN_PARSER_VERSION, this.configHash, 'index contract changed; full rebuild queued', this.services.workspaceId);
|
|
260
|
+
this.services.store.db.prepare("UPDATE brain_nodes SET freshness='stale' WHERE workspace_id=?").run(this.services.workspaceId);
|
|
261
|
+
this.services.store.db.prepare("UPDATE brain_edges SET freshness='stale' WHERE workspace_id=?").run(this.services.workspaceId);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async execute(runId, mode, token) {
|
|
265
|
+
const metrics = { ...EMPTY_METRICS };
|
|
266
|
+
try {
|
|
267
|
+
const candidates = this.scanCandidates();
|
|
268
|
+
metrics.scannedFiles = candidates.length;
|
|
269
|
+
const existingRows = this.services.store.db.prepare('SELECT * FROM brain_file_cache WHERE workspace_id=? ORDER BY path').all(this.services.workspaceId);
|
|
270
|
+
const existing = new Map(existingRows.map((row) => [row.path, row]));
|
|
271
|
+
const candidatePaths = new Set(candidates.map((candidate) => candidate.path));
|
|
272
|
+
const removed = existingRows.filter((row) => !candidatePaths.has(row.path));
|
|
273
|
+
metrics.removedFiles = removed.length;
|
|
274
|
+
const snapshots = new Map();
|
|
275
|
+
const prepared = new Map();
|
|
276
|
+
const changedPaths = new Set(removed.map((row) => row.path));
|
|
277
|
+
for (const candidate of candidates) {
|
|
278
|
+
this.assertRun(token);
|
|
279
|
+
const cached = existing.get(candidate.path);
|
|
280
|
+
const parsed = cached ? ParsedBrainFile.safeParse(this.parseJson(cached.payload)) : undefined;
|
|
281
|
+
if (mode === 'incremental' && cached && parsed?.success && cached.parser_version === BRAIN_PARSER_VERSION && cached.schema_version === BRAIN_SCHEMA_VERSION && cached.config_hash === this.configHash && sameStat(candidate, { path: cached.path, bytes: cached.bytes, mtimeMs: cached.mtime_ms, ctimeMs: cached.ctime_ms })) {
|
|
282
|
+
metrics.reusedFiles += 1;
|
|
283
|
+
prepared.set(candidate.path, { ...cached, parsed: parsed.data });
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
snapshots.set(candidate.path, this.snapshot(candidate));
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
metrics.skippedFiles += 1;
|
|
291
|
+
changedPaths.add(candidate.path);
|
|
292
|
+
if (cached && parsed?.success)
|
|
293
|
+
prepared.set(candidate.path, { ...cached, parsed: parsed.data });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Exact-content moves preserve file_id and parsed cache, making symbol
|
|
298
|
+
// identity independent from the file path.
|
|
299
|
+
const removedByHash = new Map();
|
|
300
|
+
for (const row of removed)
|
|
301
|
+
removedByHash.set(row.content_hash, [...(removedByHash.get(row.content_hash) ?? []), row]);
|
|
302
|
+
const consumedMoves = new Set();
|
|
303
|
+
const reusableByHash = new Map();
|
|
304
|
+
for (const row of existingRows)
|
|
305
|
+
if (!reusableByHash.has(row.content_hash))
|
|
306
|
+
reusableByHash.set(row.content_hash, row);
|
|
307
|
+
for (const candidate of candidates) {
|
|
308
|
+
this.assertRun(token);
|
|
309
|
+
if (prepared.has(candidate.path))
|
|
310
|
+
continue;
|
|
311
|
+
const snapshot = snapshots.get(candidate.path);
|
|
312
|
+
if (!snapshot)
|
|
313
|
+
continue;
|
|
314
|
+
const current = existing.get(candidate.path);
|
|
315
|
+
let fileId = current?.file_id;
|
|
316
|
+
let parsed;
|
|
317
|
+
if (!current) {
|
|
318
|
+
const moveRows = (removedByHash.get(snapshot.hash) ?? []).filter((row) => !consumedMoves.has(row.path));
|
|
319
|
+
if (moveRows.length === 1) {
|
|
320
|
+
const moved = moveRows[0];
|
|
321
|
+
const value = ParsedBrainFile.safeParse(this.parseJson(moved.payload));
|
|
322
|
+
if (value.success && moved.parser_version === BRAIN_PARSER_VERSION && moved.schema_version === BRAIN_SCHEMA_VERSION && moved.config_hash === this.configHash) {
|
|
323
|
+
fileId = moved.file_id;
|
|
324
|
+
parsed = value.data;
|
|
325
|
+
consumedMoves.add(moved.path);
|
|
326
|
+
metrics.movedFiles += 1;
|
|
327
|
+
metrics.reusedFiles += 1;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (!parsed && mode === 'incremental') {
|
|
332
|
+
const reusable = reusableByHash.get(snapshot.hash);
|
|
333
|
+
const value = reusable ? ParsedBrainFile.safeParse(this.parseJson(reusable.payload)) : undefined;
|
|
334
|
+
if (reusable && value?.success && reusable.parser_version === BRAIN_PARSER_VERSION && reusable.schema_version === BRAIN_SCHEMA_VERSION && reusable.config_hash === this.configHash) {
|
|
335
|
+
parsed = value.data;
|
|
336
|
+
metrics.reusedFiles += 1;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (!parsed) {
|
|
340
|
+
parsed = await this.parser.parse(candidate.path, snapshot.text);
|
|
341
|
+
metrics.parsedFiles += 1;
|
|
342
|
+
}
|
|
343
|
+
metrics.syntaxErrors += parsed.diagnostics.filter((diagnostic) => diagnostic.category === 'error').length;
|
|
344
|
+
const generation = this.state().generation;
|
|
345
|
+
prepared.set(candidate.path, {
|
|
346
|
+
workspace_id: this.services.workspaceId, path: candidate.path, file_id: fileId ?? newId('brainfile', 12),
|
|
347
|
+
content_hash: snapshot.hash, bytes: snapshot.bytes, mtime_ms: snapshot.mtimeMs, ctime_ms: snapshot.ctimeMs,
|
|
348
|
+
parser_version: BRAIN_PARSER_VERSION, schema_version: BRAIN_SCHEMA_VERSION, config_hash: this.configHash,
|
|
349
|
+
payload: JSON.stringify(parsed), indexed_at: Date.now(), generation, parsed,
|
|
350
|
+
});
|
|
351
|
+
changedPaths.add(candidate.path);
|
|
352
|
+
}
|
|
353
|
+
this.assertRun(token);
|
|
354
|
+
const allFiles = [...prepared.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
355
|
+
const affected = this.affectedPaths(changedPaths, allFiles);
|
|
356
|
+
if (mode === 'full')
|
|
357
|
+
for (const file of allFiles)
|
|
358
|
+
affected.add(file.path);
|
|
359
|
+
metrics.affectedFiles = affected.size;
|
|
360
|
+
const generation = this.state().generation;
|
|
361
|
+
const graph = this.buildGraph(allFiles, affected);
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
const commit = this.services.store.db.transaction(() => {
|
|
364
|
+
this.assertRun(token);
|
|
365
|
+
const deleteFile = this.services.store.db.prepare('DELETE FROM brain_file_cache WHERE workspace_id=? AND path=?');
|
|
366
|
+
for (const row of removed)
|
|
367
|
+
deleteFile.run(this.services.workspaceId, row.path);
|
|
368
|
+
const upsert = this.services.store.db.prepare(`INSERT INTO brain_file_cache
|
|
369
|
+
(workspace_id,path,file_id,content_hash,bytes,mtime_ms,ctime_ms,parser_version,schema_version,config_hash,payload,indexed_at,generation)
|
|
370
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
371
|
+
ON CONFLICT(workspace_id,path) DO UPDATE SET file_id=excluded.file_id,content_hash=excluded.content_hash,bytes=excluded.bytes,mtime_ms=excluded.mtime_ms,ctime_ms=excluded.ctime_ms,parser_version=excluded.parser_version,schema_version=excluded.schema_version,config_hash=excluded.config_hash,payload=excluded.payload,indexed_at=excluded.indexed_at,generation=excluded.generation`);
|
|
372
|
+
for (const file of allFiles)
|
|
373
|
+
upsert.run(file.workspace_id, file.path, file.file_id, file.content_hash, file.bytes, file.mtime_ms, file.ctime_ms, file.parser_version, file.schema_version, file.config_hash, file.payload, file.indexed_at, generation);
|
|
374
|
+
const deleteNodes = this.services.store.db.prepare('DELETE FROM brain_nodes WHERE workspace_id=? AND path=?');
|
|
375
|
+
const deleteEdges = this.services.store.db.prepare('DELETE FROM brain_edges WHERE workspace_id=? AND source_path=?');
|
|
376
|
+
for (const sourcePath of affected) {
|
|
377
|
+
deleteEdges.run(this.services.workspaceId, sourcePath);
|
|
378
|
+
deleteNodes.run(this.services.workspaceId, sourcePath);
|
|
379
|
+
}
|
|
380
|
+
const insertNode = this.services.store.db.prepare(`INSERT INTO brain_nodes
|
|
381
|
+
(workspace_id,node_id,uri,node_type,name,qualified_name,path,line,column_no,end_line,end_column,source_hash,parser_version,schema_version,freshness,details,updated_at,generation)
|
|
382
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,'current',?,?,?)`);
|
|
383
|
+
for (const node of graph.nodes)
|
|
384
|
+
insertNode.run(this.services.workspaceId, node.id, node.uri, node.type, node.name, node.qualifiedName, node.path, node.line, node.column, node.endLine, node.endColumn, node.sourceHash, BRAIN_PARSER_VERSION, BRAIN_SCHEMA_VERSION, JSON.stringify(node.details), now, generation);
|
|
385
|
+
const insertEdge = this.services.store.db.prepare(`INSERT INTO brain_edges
|
|
386
|
+
(workspace_id,edge_id,edge_type,from_node_id,to_node_id,target_key,source_path,target_path,line,source_hash,parser_version,schema_version,freshness,details,updated_at,generation)
|
|
387
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,'current',?,?,?)`);
|
|
388
|
+
for (const edge of graph.edges)
|
|
389
|
+
insertEdge.run(this.services.workspaceId, edge.id, edge.type, edge.from, edge.to, edge.targetKey, edge.sourcePath, edge.targetPath, edge.line, edge.sourceHash, BRAIN_PARSER_VERSION, BRAIN_SCHEMA_VERSION, JSON.stringify(edge.details), now, generation);
|
|
390
|
+
const counts = this.counts();
|
|
391
|
+
metrics.nodes = counts.nodes;
|
|
392
|
+
metrics.edges = counts.edges;
|
|
393
|
+
const sourceHash = digestOf(allFiles.map((file) => ({ path: file.path, hash: file.content_hash })));
|
|
394
|
+
this.services.store.db.prepare(`UPDATE brain_index_state SET status='completed',active_run_id=NULL,last_completed_at=?,last_error=NULL,source_hash=?,metrics=? WHERE workspace_id=?`)
|
|
395
|
+
.run(now, sourceHash, JSON.stringify(metrics), this.services.workspaceId);
|
|
396
|
+
this.services.store.db.prepare("UPDATE brain_runs SET status='completed',ended_at=?,metrics=? WHERE id=?").run(now, JSON.stringify(metrics), runId);
|
|
397
|
+
this.services.store.db.prepare(`DELETE FROM brain_runs WHERE workspace_id=? AND id NOT IN (SELECT id FROM brain_runs WHERE workspace_id=? ORDER BY started_at DESC LIMIT ?)`)
|
|
398
|
+
.run(this.services.workspaceId, this.services.workspaceId, RUN_HISTORY_MAX);
|
|
399
|
+
});
|
|
400
|
+
commit.immediate();
|
|
401
|
+
this.forceFull = false;
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
const canceled = token !== this.runToken || this.closed;
|
|
405
|
+
const status = canceled ? 'canceled' : 'failed';
|
|
406
|
+
const message = canceled ? 'indexing canceled before commit' : safeMessage(error);
|
|
407
|
+
const now = Date.now();
|
|
408
|
+
try {
|
|
409
|
+
this.services.store.db.prepare('UPDATE brain_index_state SET status=?,active_run_id=NULL,last_completed_at=?,last_error=?,metrics=? WHERE workspace_id=?')
|
|
410
|
+
.run(this.state().paused === 1 ? 'paused' : status, now, message, JSON.stringify(metrics), this.services.workspaceId);
|
|
411
|
+
this.services.store.db.prepare('UPDATE brain_runs SET status=?,ended_at=?,metrics=?,error=? WHERE id=?')
|
|
412
|
+
.run(status, now, JSON.stringify(metrics), message, runId);
|
|
413
|
+
}
|
|
414
|
+
catch { /* database shutdown/corruption: bootstrap owns final recovery */ }
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
scanCandidates() {
|
|
418
|
+
const out = [];
|
|
419
|
+
let visited = 0;
|
|
420
|
+
for (const entry of this.services.wfs.walk({ maxEntries: this.services.limits.semanticFilesMax * 8, maxDepth: 64 })) {
|
|
421
|
+
if (++visited > this.services.limits.semanticFilesMax * 8)
|
|
422
|
+
break;
|
|
423
|
+
if (!entry.stat.isFile())
|
|
424
|
+
continue;
|
|
425
|
+
if (!SCRIPT.test(entry.rel) && entry.rel !== 'package.json' && !entry.rel.endsWith('/package.json'))
|
|
426
|
+
continue;
|
|
427
|
+
if (entry.stat.size > BRAIN_FILE_MAX_BYTES)
|
|
428
|
+
continue;
|
|
429
|
+
out.push({ path: entry.rel, bytes: entry.stat.size, mtimeMs: entry.stat.mtimeMs, ctimeMs: entry.stat.ctimeMs });
|
|
430
|
+
if (out.length >= this.services.limits.semanticFilesMax)
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
434
|
+
}
|
|
435
|
+
snapshot(candidate) {
|
|
436
|
+
const read = this.services.wfs.readTextFile(candidate.path, BRAIN_FILE_MAX_BYTES);
|
|
437
|
+
const afterResolved = this.services.wfs.resolve(candidate.path);
|
|
438
|
+
const after = this.services.wfs.assertRegularFileForDirectAccess(afterResolved);
|
|
439
|
+
const observed = { path: read.rel, bytes: after.size, mtimeMs: after.mtimeMs, ctimeMs: after.ctimeMs };
|
|
440
|
+
if (!sameStat(candidate, observed) || read.stat.dev !== after.dev || read.stat.ino !== after.ino)
|
|
441
|
+
throw new DodoError('FILE_CHANGED', 'source changed while project brain was snapshotting it', { retryable: true });
|
|
442
|
+
return { ...observed, text: read.text, hash: read.hash };
|
|
443
|
+
}
|
|
444
|
+
affectedPaths(changed, files) {
|
|
445
|
+
const affected = new Set(changed);
|
|
446
|
+
if (changed.size === 0)
|
|
447
|
+
return affected;
|
|
448
|
+
const oldEdges = this.services.store.db.prepare('SELECT source_path,target_path FROM brain_edges WHERE workspace_id=? AND target_path IS NOT NULL').all(this.services.workspaceId);
|
|
449
|
+
for (const edge of oldEdges)
|
|
450
|
+
if (changed.has(edge.target_path))
|
|
451
|
+
affected.add(edge.source_path);
|
|
452
|
+
const changedList = [...changed];
|
|
453
|
+
const oldSymbols = this.services.store.db.prepare(`SELECT name FROM brain_nodes WHERE workspace_id=? AND node_type='symbol' AND path IN (${changedList.map(() => '?').join(',')})`)
|
|
454
|
+
.all(this.services.workspaceId, ...changedList);
|
|
455
|
+
const changedNames = new Set(oldSymbols.map((row) => row.name));
|
|
456
|
+
for (const file of files)
|
|
457
|
+
if (changed.has(file.path))
|
|
458
|
+
for (const symbol of file.parsed.symbols)
|
|
459
|
+
changedNames.add(symbol.name);
|
|
460
|
+
const allPaths = new Set(files.map((file) => file.path));
|
|
461
|
+
for (const file of files) {
|
|
462
|
+
if (file.parsed.references.some((reference) => changedNames.has(reference.name)))
|
|
463
|
+
affected.add(file.path);
|
|
464
|
+
for (const imported of file.parsed.imports) {
|
|
465
|
+
const target = this.resolveImport(file.path, imported.specifier, allPaths);
|
|
466
|
+
if (target && changed.has(target))
|
|
467
|
+
affected.add(file.path);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return affected;
|
|
471
|
+
}
|
|
472
|
+
buildGraph(files, affected) {
|
|
473
|
+
const allPaths = new Set(files.map((file) => file.path));
|
|
474
|
+
const fileNodes = new Map();
|
|
475
|
+
const symbolNodes = new Map();
|
|
476
|
+
const dependencyNodes = new Map();
|
|
477
|
+
const dependencyTargets = new Map();
|
|
478
|
+
for (const file of files) {
|
|
479
|
+
const fileId = this.nodeId('file', file.file_id);
|
|
480
|
+
fileNodes.set(file.path, fileId);
|
|
481
|
+
for (const symbol of file.parsed.symbols) {
|
|
482
|
+
const id = this.nodeId('symbol', file.file_id, symbol.kind, symbol.qualifiedName, String(symbol.ordinal));
|
|
483
|
+
symbolNodes.set(symbol.name, [...(symbolNodes.get(symbol.name) ?? []), { id, path: file.path }]);
|
|
484
|
+
}
|
|
485
|
+
for (const dependency of file.parsed.dependencies) {
|
|
486
|
+
const id = this.nodeId('dependency', file.file_id, dependency.scope, dependency.name);
|
|
487
|
+
dependencyNodes.set(`${file.path}\0${dependency.scope}\0${dependency.name}`, id);
|
|
488
|
+
if (!dependencyTargets.has(dependency.name))
|
|
489
|
+
dependencyTargets.set(dependency.name, id);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const nodes = [], edges = [];
|
|
493
|
+
const addEdge = (source, type, from, to, targetKey, targetPath, line, details) => {
|
|
494
|
+
const id = this.nodeId('edge', type, from, to ?? targetKey ?? '', source.path, String(line), String(edges.length));
|
|
495
|
+
edges.push({ id, type, from, to, targetKey, sourcePath: source.path, targetPath, line, sourceHash: source.content_hash, details });
|
|
496
|
+
};
|
|
497
|
+
for (const file of files) {
|
|
498
|
+
if (!affected.has(file.path))
|
|
499
|
+
continue;
|
|
500
|
+
const fileNode = fileNodes.get(file.path);
|
|
501
|
+
nodes.push({ id: fileNode, uri: `file://${this.namespace}/${file.file_id}`, type: 'file', name: path.posix.basename(file.path), qualifiedName: null, path: file.path, line: 1, column: 1, endLine: 1, endColumn: 1, sourceHash: file.content_hash, details: { provider: file.parsed.provider, language: file.parsed.language, diagnostics: file.parsed.diagnostics, truncated: file.parsed.truncated } });
|
|
502
|
+
for (const symbol of file.parsed.symbols) {
|
|
503
|
+
const id = this.nodeId('symbol', file.file_id, symbol.kind, symbol.qualifiedName, String(symbol.ordinal));
|
|
504
|
+
nodes.push({ id, uri: `symbol://${this.namespace}/${id}`, type: 'symbol', name: symbol.name, qualifiedName: symbol.qualifiedName, path: file.path, line: symbol.line, column: symbol.column, endLine: symbol.endLine, endColumn: symbol.endColumn, sourceHash: file.content_hash, details: { kind: symbol.kind, ordinal: symbol.ordinal, signatureHash: symbol.signatureHash, exported: symbol.exported } });
|
|
505
|
+
addEdge(file, 'contains', fileNode, id, null, file.path, symbol.line, { relation: 'declaration' });
|
|
506
|
+
}
|
|
507
|
+
for (const route of file.parsed.routes) {
|
|
508
|
+
const id = this.nodeId('route', file.file_id, route.method, route.route, String(route.ordinal));
|
|
509
|
+
nodes.push({ id, uri: `route://${this.namespace}/${id}`, type: 'route', name: `${route.method} ${route.route}`, qualifiedName: null, path: file.path, line: route.line, column: route.column, endLine: route.line, endColumn: route.column, sourceHash: file.content_hash, details: { method: route.method, route: route.route, evidence: 'syntax_heuristic' } });
|
|
510
|
+
addEdge(file, 'contains', fileNode, id, null, file.path, route.line, { relation: 'route_heuristic' });
|
|
511
|
+
}
|
|
512
|
+
for (const test of file.parsed.tests) {
|
|
513
|
+
const id = this.nodeId('test', file.file_id, test.kind, test.name, String(test.ordinal));
|
|
514
|
+
nodes.push({ id, uri: `test://${this.namespace}/${id}`, type: 'test', name: test.name, qualifiedName: null, path: file.path, line: test.line, column: test.column, endLine: test.line, endColumn: test.column, sourceHash: file.content_hash, details: { kind: test.kind, evidence: 'syntax_heuristic' } });
|
|
515
|
+
addEdge(file, 'contains', fileNode, id, null, file.path, test.line, { relation: 'test_declaration' });
|
|
516
|
+
}
|
|
517
|
+
for (const dependency of file.parsed.dependencies) {
|
|
518
|
+
const id = dependencyNodes.get(`${file.path}\0${dependency.scope}\0${dependency.name}`);
|
|
519
|
+
nodes.push({ id, uri: `dependency://${this.namespace}/${id}`, type: 'dependency', name: dependency.name, qualifiedName: null, path: file.path, line: 1, column: 1, endLine: 1, endColumn: 1, sourceHash: file.content_hash, details: { scope: dependency.scope, version: dependency.version } });
|
|
520
|
+
addEdge(file, 'depends_on', fileNode, id, `package:${dependency.name}`, file.path, 1, { scope: dependency.scope, version: dependency.version });
|
|
521
|
+
}
|
|
522
|
+
for (const imported of file.parsed.imports) {
|
|
523
|
+
const targetPath = this.resolveImport(file.path, imported.specifier, allPaths);
|
|
524
|
+
const pkg = externalPackage(imported.specifier);
|
|
525
|
+
const target = targetPath ? fileNodes.get(targetPath) ?? null : pkg ? dependencyTargets.get(pkg) ?? null : null;
|
|
526
|
+
addEdge(file, imported.kind, fileNode, target, targetPath ? null : pkg ? `package:${pkg}` : `module:${imported.specifier}`, targetPath ?? null, imported.line, { specifier: imported.specifier, resolved: target !== null });
|
|
527
|
+
}
|
|
528
|
+
const seenReferences = new Set();
|
|
529
|
+
for (const reference of file.parsed.references) {
|
|
530
|
+
const targets = symbolNodes.get(reference.name) ?? [];
|
|
531
|
+
const target = targets.length === 1 ? targets[0] : undefined;
|
|
532
|
+
const key = `${reference.name}:${reference.line}:${reference.column}:${target?.id ?? ''}`;
|
|
533
|
+
if (seenReferences.has(key))
|
|
534
|
+
continue;
|
|
535
|
+
seenReferences.add(key);
|
|
536
|
+
addEdge(file, 'references', fileNode, target?.id ?? null, target ? null : `symbol-name:${reference.name}`, target?.path ?? null, reference.line, { name: reference.name, column: reference.column, call: reference.call, ambiguous: targets.length > 1 });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return { nodes, edges };
|
|
540
|
+
}
|
|
541
|
+
resolveImport(sourcePath, specifier, allPaths) {
|
|
542
|
+
if (!specifier.startsWith('.'))
|
|
543
|
+
return undefined;
|
|
544
|
+
const base = path.posix.normalize(path.posix.join(path.posix.dirname(sourcePath), specifier));
|
|
545
|
+
if (base === '..' || base.startsWith('../') || base.startsWith('/'))
|
|
546
|
+
return undefined;
|
|
547
|
+
const candidates = [base, ...['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'].map((ext) => `${base}${ext}`), ...['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'].map((ext) => `${base}/index${ext}`)];
|
|
548
|
+
return candidates.find((candidate) => allPaths.has(candidate));
|
|
549
|
+
}
|
|
550
|
+
nodeId(...parts) { return `brain_${digestOf([this.namespace, ...parts]).slice('sha256:'.length, 'sha256:'.length + 32)}`; }
|
|
551
|
+
state() {
|
|
552
|
+
const row = this.services.store.db.prepare('SELECT * FROM brain_index_state WHERE workspace_id=?').get(this.services.workspaceId);
|
|
553
|
+
if (!row)
|
|
554
|
+
throw new DodoError('RECOVERY_REQUIRED', 'project brain state is missing; restart DODO to recreate it');
|
|
555
|
+
return row;
|
|
556
|
+
}
|
|
557
|
+
statusFromRow(row) {
|
|
558
|
+
const metrics = BrainRunMetrics.safeParse(this.parseJson(row.metrics));
|
|
559
|
+
if (!metrics.success) {
|
|
560
|
+
this.scheduleCorruptionRecovery();
|
|
561
|
+
throw new DodoError('RECOVERY_REQUIRED', 'project brain metrics are corrupt; a full rebuild was queued');
|
|
562
|
+
}
|
|
563
|
+
const counts = this.counts();
|
|
564
|
+
return {
|
|
565
|
+
schemaVersion: BRAIN_SCHEMA_VERSION,
|
|
566
|
+
parserVersion: BRAIN_PARSER_VERSION,
|
|
567
|
+
namespace: this.namespace,
|
|
568
|
+
status: row.status,
|
|
569
|
+
paused: row.paused === 1,
|
|
570
|
+
activeRunId: row.active_run_id,
|
|
571
|
+
lastRunId: row.last_run_id,
|
|
572
|
+
lastStartedAt: row.last_started_at,
|
|
573
|
+
lastCompletedAt: row.last_completed_at,
|
|
574
|
+
lastError: row.last_error,
|
|
575
|
+
sourceHash: row.source_hash,
|
|
576
|
+
files: counts.files,
|
|
577
|
+
nodes: counts.nodes,
|
|
578
|
+
edges: counts.edges,
|
|
579
|
+
staleFiles: this.metadataStaleCount(),
|
|
580
|
+
metrics: metrics.data,
|
|
581
|
+
automaticRefresh: true,
|
|
582
|
+
note: 'Index metadata is untrusted evidence, never permission. Query results recheck the guarded source hash.',
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
counts() {
|
|
586
|
+
const file = this.services.store.db.prepare('SELECT COUNT(*) AS count FROM brain_file_cache WHERE workspace_id=?').get(this.services.workspaceId);
|
|
587
|
+
const node = this.services.store.db.prepare('SELECT COUNT(*) AS count FROM brain_nodes WHERE workspace_id=?').get(this.services.workspaceId);
|
|
588
|
+
const edge = this.services.store.db.prepare('SELECT COUNT(*) AS count FROM brain_edges WHERE workspace_id=?').get(this.services.workspaceId);
|
|
589
|
+
return { files: file.count, nodes: node.count, edges: edge.count };
|
|
590
|
+
}
|
|
591
|
+
metadataStaleCount() {
|
|
592
|
+
const rows = this.services.store.db.prepare('SELECT path,bytes,mtime_ms,ctime_ms FROM brain_file_cache WHERE workspace_id=?').all(this.services.workspaceId);
|
|
593
|
+
let stale = 0;
|
|
594
|
+
for (const row of rows) {
|
|
595
|
+
try {
|
|
596
|
+
const resolved = this.services.wfs.resolve(row.path);
|
|
597
|
+
const stat = this.services.wfs.assertRegularFileForDirectAccess(resolved);
|
|
598
|
+
if (stat.size !== row.bytes || stat.mtimeMs !== row.mtime_ms || stat.ctimeMs !== row.ctime_ms)
|
|
599
|
+
stale += 1;
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
stale += 1;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return stale;
|
|
606
|
+
}
|
|
607
|
+
sourceFreshness(sourcePath, expectedHash, cache) {
|
|
608
|
+
const existing = cache.get(sourcePath);
|
|
609
|
+
if (existing)
|
|
610
|
+
return existing;
|
|
611
|
+
try {
|
|
612
|
+
const read = this.services.wfs.readTextFile(sourcePath, BRAIN_FILE_MAX_BYTES);
|
|
613
|
+
const result = read.hash === expectedHash ? 'current' : 'stale';
|
|
614
|
+
cache.set(sourcePath, result);
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
const code = toDodoError(error).code;
|
|
619
|
+
const result = code === 'NOT_FOUND' ? 'missing' : 'inaccessible';
|
|
620
|
+
cache.set(sourcePath, result);
|
|
621
|
+
return result;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
nodeRows(query, sourcePath, types, offset, limit) {
|
|
625
|
+
const clauses = ['workspace_id=?'], params = [this.services.workspaceId];
|
|
626
|
+
if (query) {
|
|
627
|
+
clauses.push("(name LIKE ? ESCAPE '\\' OR qualified_name LIKE ? ESCAPE '\\')");
|
|
628
|
+
const like = `%${this.escapeLike(query)}%`;
|
|
629
|
+
params.push(like, like);
|
|
630
|
+
}
|
|
631
|
+
if (sourcePath) {
|
|
632
|
+
clauses.push('path=?');
|
|
633
|
+
params.push(sourcePath);
|
|
634
|
+
}
|
|
635
|
+
if (types?.length) {
|
|
636
|
+
clauses.push(`node_type IN (${types.map(() => '?').join(',')})`);
|
|
637
|
+
params.push(...types);
|
|
638
|
+
}
|
|
639
|
+
params.push(limit, offset);
|
|
640
|
+
return this.services.store.db.prepare(`SELECT * FROM brain_nodes WHERE ${clauses.join(' AND ')} ORDER BY node_type,name,path,line,node_id LIMIT ? OFFSET ?`).all(...params);
|
|
641
|
+
}
|
|
642
|
+
edgeRows(query, sourcePath, types, offset, limit) {
|
|
643
|
+
const clauses = ['workspace_id=?'], params = [this.services.workspaceId];
|
|
644
|
+
if (query) {
|
|
645
|
+
clauses.push("(target_key LIKE ? ESCAPE '\\' OR details LIKE ? ESCAPE '\\')");
|
|
646
|
+
const like = `%${this.escapeLike(query)}%`;
|
|
647
|
+
params.push(like, like);
|
|
648
|
+
}
|
|
649
|
+
if (sourcePath) {
|
|
650
|
+
clauses.push('source_path=?');
|
|
651
|
+
params.push(sourcePath);
|
|
652
|
+
}
|
|
653
|
+
if (types?.length) {
|
|
654
|
+
clauses.push(`edge_type IN (${types.map(() => '?').join(',')})`);
|
|
655
|
+
params.push(...types);
|
|
656
|
+
}
|
|
657
|
+
params.push(limit, offset);
|
|
658
|
+
return this.services.store.db.prepare(`SELECT * FROM brain_edges WHERE ${clauses.join(' AND ')} ORDER BY edge_type,source_path,line,edge_id LIMIT ? OFFSET ?`).all(...params);
|
|
659
|
+
}
|
|
660
|
+
materializeNode(row, freshness) {
|
|
661
|
+
return {
|
|
662
|
+
id: row['node_id'], uri: row['uri'], type: row['node_type'], name: row['name'], qualifiedName: row['qualified_name'], path: row['path'],
|
|
663
|
+
line: row['line'], column: row['column_no'], endLine: row['end_line'], endColumn: row['end_column'], sourceHash: row['source_hash'],
|
|
664
|
+
parserVersion: row['parser_version'], schemaVersion: row['schema_version'], freshness, details: this.details(row['details']),
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
materializeEdge(row, freshness) {
|
|
668
|
+
return {
|
|
669
|
+
id: row['edge_id'], type: row['edge_type'], from: row['from_node_id'], to: row['to_node_id'], targetKey: row['target_key'],
|
|
670
|
+
sourcePath: row['source_path'], targetPath: row['target_path'], line: row['line'], sourceHash: row['source_hash'],
|
|
671
|
+
parserVersion: row['parser_version'], schemaVersion: row['schema_version'], freshness, details: this.details(row['details']),
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
details(value) {
|
|
675
|
+
const parsed = this.parseJson(String(value));
|
|
676
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
677
|
+
this.scheduleCorruptionRecovery();
|
|
678
|
+
throw new DodoError('RECOVERY_REQUIRED', 'project brain row is corrupt; a full rebuild was queued');
|
|
679
|
+
}
|
|
680
|
+
return parsed;
|
|
681
|
+
}
|
|
682
|
+
parseJson(value) { try {
|
|
683
|
+
return JSON.parse(value);
|
|
684
|
+
}
|
|
685
|
+
catch {
|
|
686
|
+
return undefined;
|
|
687
|
+
} }
|
|
688
|
+
escapeLike(value) { return value.replace(/[\\%_]/g, (part) => `\\${part}`); }
|
|
689
|
+
assertRun(token) { if (this.closed || token !== this.runToken)
|
|
690
|
+
throw new DodoError('CONFLICT', 'project indexing canceled', { retryable: true }); }
|
|
691
|
+
check() { if (this.closed)
|
|
692
|
+
throw new DodoError('STALE_WORKSPACE', 'project brain workspace is closed'); }
|
|
693
|
+
scheduleCorruptionRecovery() {
|
|
694
|
+
if (this.closed)
|
|
695
|
+
return;
|
|
696
|
+
this.forceFull = true;
|
|
697
|
+
try {
|
|
698
|
+
this.services.store.db.prepare("UPDATE brain_index_state SET status='failed',last_error=? WHERE workspace_id=?").run('corrupt index row detected; full rebuild queued', this.services.workspaceId);
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
this.initialTimer = setTimeout(() => { void this.autoIndex(); }, 0);
|
|
704
|
+
this.initialTimer.unref();
|
|
705
|
+
}
|
|
706
|
+
signCursor(ctx, payload) {
|
|
707
|
+
const encoded = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
708
|
+
return `b1.${encoded}.${createHmac('sha256', this.cursorKey).update(encoded).digest('base64url')}`;
|
|
709
|
+
}
|
|
710
|
+
verifyCursor(ctx, token, signature) {
|
|
711
|
+
const match = /^b1\.([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)$/.exec(token);
|
|
712
|
+
if (!match)
|
|
713
|
+
throw new DodoError('INVALID_INPUT', 'invalid project brain cursor');
|
|
714
|
+
const body = Buffer.from(match[1], 'base64url'), actual = Buffer.from(match[2], 'base64url');
|
|
715
|
+
if (body.toString('base64url') !== match[1] || actual.toString('base64url') !== match[2])
|
|
716
|
+
throw new DodoError('INVALID_INPUT', 'invalid project brain cursor');
|
|
717
|
+
const expected = createHmac('sha256', this.cursorKey).update(match[1]).digest();
|
|
718
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
|
|
719
|
+
throw new DodoError('INVALID_INPUT', 'invalid project brain cursor');
|
|
720
|
+
let payload;
|
|
721
|
+
try {
|
|
722
|
+
payload = JSON.parse(body.toString('utf8'));
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
throw new DodoError('INVALID_INPUT', 'invalid project brain cursor');
|
|
726
|
+
}
|
|
727
|
+
if (payload.v !== 1 || payload.q !== signature || payload.w !== this.services.workspaceId || payload.a !== principalKey(ctx) || payload.exp <= Date.now() || !Number.isSafeInteger(payload.n) || payload.n < 0 || !Number.isSafeInteger(payload.e) || payload.e < 0) {
|
|
728
|
+
throw new DodoError('STALE_WORKSPACE', 'project brain cursor expired or belongs to another query/client/workspace');
|
|
729
|
+
}
|
|
730
|
+
return payload;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
//# sourceMappingURL=brainService.js.map
|