@planu/cli 4.12.3 → 4.14.1
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/CHANGELOG.md +35 -0
- package/README.md +29 -25
- package/dist/cli/commands/inspect.d.ts +4 -0
- package/dist/cli/commands/inspect.js +144 -0
- package/dist/cli/commands/install.d.ts +19 -1
- package/dist/cli/commands/install.js +145 -43
- package/dist/cli/commands/job.d.ts +3 -0
- package/dist/cli/commands/job.js +119 -0
- package/dist/cli/commands/offline.d.ts +15 -0
- package/dist/cli/commands/offline.js +132 -0
- package/dist/cli/commands/serve.js +4 -0
- package/dist/cli/commands/spec.js +1 -1
- package/dist/cli/commands/storage.d.ts +3 -0
- package/dist/cli/commands/storage.js +102 -0
- package/dist/cli/commands/uninstall.d.ts +10 -0
- package/dist/cli/commands/uninstall.js +93 -7
- package/dist/cli/commands/validate.js +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/router.js +9 -1
- package/dist/config/crash-shield-framework-rules.json +93 -0
- package/dist/config/environment-schema.json +149 -0
- package/dist/config/hardcode-exceptions.json +4 -0
- package/dist/config/license-plans.json +7 -0
- package/dist/config/native-release-public-key.pem +3 -0
- package/dist/config/native-targets.json +87 -0
- package/dist/config/product-public.d.ts +89 -0
- package/dist/config/product-public.js +140 -0
- package/dist/config/product-public.json +312 -0
- package/dist/config/product-public.ts +148 -0
- package/dist/config/release-compatibility.json +13 -0
- package/dist/config/release-policy.json +26 -0
- package/dist/config/runtime-policy.json +44 -0
- package/dist/config/skill-templates/planu-validate.md +7 -4
- package/dist/config/subagent-templates/planu-spec-implementer.md +7 -3
- package/dist/config/subagent-templates/planu-validator.md +10 -5
- package/dist/config/technology-registry.json +120 -0
- package/dist/config/version.js +7 -8
- package/dist/config/version.ts +10 -7
- package/dist/engine/abortable-process-runner.js +40 -9
- package/dist/engine/advanced-testing/accessibility-checker.js +12 -14
- package/dist/engine/advanced-testing/mutation-advisor.js +3 -4
- package/dist/engine/advanced-testing/property-based.js +2 -3
- package/dist/engine/advanced-testing/test-data-advisor.js +2 -3
- package/dist/engine/agent-generator.js +2 -1
- package/dist/engine/agent-handoff-store.js +2 -1
- package/dist/engine/agent-prompt-generator.js +7 -5
- package/dist/engine/agent-telemetry.js +2 -1
- package/dist/engine/ai-integration/gemini/extensions-advisor.js +41 -9
- package/dist/engine/ambiguity-detector.js +12 -11
- package/dist/engine/analyzer/db-engine.js +2 -3
- package/dist/engine/analyzer/detectors.js +13 -14
- package/dist/engine/analyzer/env-setup.js +9 -10
- package/dist/engine/analyzer-env-scanner/env-example.js +7 -8
- package/dist/engine/api-spec-generator/graphql-parser.js +2 -2
- package/dist/engine/api-validation/graphql-schema-validator.js +2 -1
- package/dist/engine/api-validation/openapi-impl-validator.js +6 -7
- package/dist/engine/architecture-detector.js +10 -8
- package/dist/engine/auth/config.d.ts +1 -1
- package/dist/engine/auth/config.js +4 -3
- package/dist/engine/auto-updater/atomic-upgrade.d.ts +8 -0
- package/dist/engine/auto-updater/atomic-upgrade.js +381 -0
- package/dist/engine/auto-updater/config-detector.js +9 -3
- package/dist/engine/auto-updater/config-patcher.d.ts +0 -4
- package/dist/engine/auto-updater/config-patcher.js +10 -15
- package/dist/engine/auto-updater/index.d.ts +3 -1
- package/dist/engine/auto-updater/index.js +44 -30
- package/dist/engine/auto-updater/npm-registry.js +10 -2
- package/dist/engine/auto-updater/release-verifier.d.ts +3 -0
- package/dist/engine/auto-updater/release-verifier.js +112 -0
- package/dist/engine/auto-updater/upgrade-lock.d.ts +6 -0
- package/dist/engine/auto-updater/upgrade-lock.js +28 -0
- package/dist/engine/auto-updater/upgrade-trust-state.d.ts +7 -0
- package/dist/engine/auto-updater/upgrade-trust-state.js +59 -0
- package/dist/engine/autopilot/audit-logger.js +2 -5
- package/dist/engine/best-practices-library.js +2 -1
- package/dist/engine/browser-validator.js +2 -1
- package/dist/engine/build-tool-detector.js +3 -2
- package/dist/engine/ci-generator/context-builders.js +7 -5
- package/dist/engine/ci-generator/stack-detector.js +0 -2
- package/dist/engine/ci-generator/yaml-builder.js +0 -2
- package/dist/engine/claude-config-auditor/rules-auditor.js +4 -3
- package/dist/engine/code-transforms/typescript/ast-utils.js +2 -3
- package/dist/engine/complexity-budget/trivial-detector.js +2 -1
- package/dist/engine/compliance-test-generator/index.js +7 -2
- package/dist/engine/compliance-test-generator/test-formatter.js +2 -2
- package/dist/engine/config-health/mcp-config-checker.js +2 -1
- package/dist/engine/config-health/ts-checker-scripts.js +4 -3
- package/dist/engine/config-health/ts-checker.js +3 -2
- package/dist/engine/convention-injector.js +5 -7
- package/dist/engine/convention-scanner/convention-parser.js +3 -2
- package/dist/engine/core-bridge-duplicate-blocks.d.ts +3 -0
- package/dist/engine/core-bridge-duplicate-blocks.js +57 -0
- package/dist/engine/core-bridge-gaps.d.ts +4 -0
- package/dist/engine/core-bridge-gaps.js +75 -0
- package/dist/engine/core-bridge.d.ts +9 -9
- package/dist/engine/core-bridge.js +196 -283
- package/dist/engine/cost-tracking/ledger.js +2 -5
- package/dist/engine/coverage-gap-analyzer.js +10 -1
- package/dist/engine/crash-shield/detectors/framework-detector.d.ts +2 -0
- package/dist/engine/crash-shield/detectors/framework-detector.js +110 -192
- package/dist/engine/crash-shield/detectors/typescript-detector.js +2 -1
- package/dist/engine/dashboard/data-loader.js +2 -1
- package/dist/engine/data-projects-gc/gc-runner.d.ts +7 -0
- package/dist/engine/data-projects-gc/gc-runner.js +113 -3
- package/dist/engine/data-projects-gc/scanner.js +5 -1
- package/dist/engine/decision-store.js +11 -14
- package/dist/engine/deleter/core.js +5 -5
- package/dist/engine/dep-auditor/abandonment-data.js +2 -1
- package/dist/engine/dep-auditor/abandonment-registry.js +3 -1
- package/dist/engine/dep-auditor/cve-fetcher.js +3 -0
- package/dist/engine/dep-auditor/dep-duplicates-detector.js +36 -4
- package/dist/engine/dep-auditor/license-registry.js +2 -0
- package/dist/engine/dependency-evaluator.d.ts +14 -0
- package/dist/engine/dependency-evaluator.js +81 -0
- package/dist/engine/deploy/providers/fly.js +7 -7
- package/dist/engine/deploy/providers/netlify.js +7 -7
- package/dist/engine/deploy/providers/railway.js +10 -12
- package/dist/engine/deploy/providers/vercel.js +7 -9
- package/dist/engine/deprecation-scanner/pattern-loader.js +0 -2
- package/dist/engine/desktop-detector.js +2 -3
- package/dist/engine/detect-duplication.js +2 -1
- package/dist/engine/detectors/advanced-framework-detector/css-state-detector.js +2 -1
- package/dist/engine/detectors/advanced-framework-detector/legacy-detector.js +2 -3
- package/dist/engine/detectors/advanced-framework-detector/modern-detector.js +3 -4
- package/dist/engine/detectors/agent-framework-detector.js +3 -2
- package/dist/engine/detectors/ai-native-detector.js +4 -6
- package/dist/engine/detectors/cache-db-detector.js +4 -3
- package/dist/engine/detectors/cicd-detector.js +4 -3
- package/dist/engine/detectors/deployment-detector.js +11 -12
- package/dist/engine/detectors/frontend-perf-detector.js +14 -9
- package/dist/engine/detectors/library-detector-multilang-a.js +16 -3
- package/dist/engine/detectors/library-detector-node.js +14 -4
- package/dist/engine/detectors/library-detector-python.js +7 -1
- package/dist/engine/detectors/library-detector.js +0 -2
- package/dist/engine/detectors/llm-detector/provider-maps.js +3 -2
- package/dist/engine/detectors/mcp-candidate-detector.js +12 -16
- package/dist/engine/detectors/mfe-detector.js +6 -7
- package/dist/engine/detectors/monitoring-detector.js +4 -3
- package/dist/engine/detectors/otel-detector.js +3 -2
- package/dist/engine/detectors/otel-ecosystem-detectors.d.ts +2 -2
- package/dist/engine/detectors/otel-ecosystem-detectors.js +4 -3
- package/dist/engine/detectors/platform-specialty-detector.js +3 -5
- package/dist/engine/detectors/template-engine-detector.js +3 -4
- package/dist/engine/doc-generator/builders.js +2 -3
- package/dist/engine/doc-generator/mobile-distribution-guide.js +13 -17
- package/dist/engine/doc-generator/portal/dirty-flag-map.d.ts +2 -2
- package/dist/engine/doc-generator/portal/portal-regenerator.js +3 -47
- package/dist/engine/domain-detector.js +3 -6
- package/dist/engine/dynamic-knowledge/knowledge-cache.js +3 -4
- package/dist/engine/dynamic-knowledge/knowledge-merger.js +17 -2
- package/dist/engine/dynamic-knowledge/web-researcher.js +15 -14
- package/dist/engine/dynamic-migration-detector/manifest-readers-extended.js +0 -2
- package/dist/engine/e2e-test-generator.js +4 -3
- package/dist/engine/ecosystem-absorber/knowledge-cache.js +2 -1
- package/dist/engine/ecosystem-absorber/platform-crawler.js +2 -2
- package/dist/engine/escalator/retry-counter.js +2 -1
- package/dist/engine/evidence-gates/artifact-reader.d.ts +6 -0
- package/dist/engine/evidence-gates/artifact-reader.js +105 -21
- package/dist/engine/evidence-gates/lifecycle-gate.d.ts +2 -1
- package/dist/engine/evidence-gates/lifecycle-gate.js +54 -0
- package/dist/engine/execution/context.d.ts +19 -0
- package/dist/engine/execution/context.js +127 -0
- package/dist/engine/execution/deadline-io.d.ts +21 -0
- package/dist/engine/execution/deadline-io.js +45 -0
- package/dist/engine/execution/job-executor.d.ts +16 -0
- package/dist/engine/execution/job-executor.js +140 -0
- package/dist/engine/execution/job-runtime.d.ts +78 -0
- package/dist/engine/execution/job-runtime.js +220 -0
- package/dist/engine/execution/native-relevance-scorer.d.ts +6 -0
- package/dist/engine/execution/native-relevance-scorer.js +64 -0
- package/dist/engine/execution/native-worker-dispatch.d.ts +3 -0
- package/dist/engine/execution/native-worker-dispatch.js +46 -0
- package/dist/engine/execution/native-worker-protocol.d.ts +6 -0
- package/dist/engine/execution/native-worker-protocol.js +33 -0
- package/dist/engine/execution/native-worker-runtime.d.ts +4 -0
- package/dist/engine/execution/native-worker-runtime.js +177 -0
- package/dist/engine/execution/native-worker-thread.d.ts +2 -0
- package/dist/engine/execution/native-worker-thread.js +66 -0
- package/dist/engine/execution/operation-journal.d.ts +66 -0
- package/dist/engine/execution/operation-journal.js +260 -0
- package/dist/engine/execution/outbox-worker.d.ts +31 -0
- package/dist/engine/execution/outbox-worker.js +192 -0
- package/dist/engine/execution/validate-job-executor.d.ts +24 -0
- package/dist/engine/execution/validate-job-executor.js +342 -0
- package/dist/engine/execution/validate-job-payload.d.ts +5 -0
- package/dist/engine/execution/validate-job-payload.js +87 -0
- package/dist/engine/execution/validate-job-recovery.d.ts +4 -0
- package/dist/engine/execution/validate-job-recovery.js +47 -0
- package/dist/engine/execution/validate-worker-runtime.d.ts +5 -0
- package/dist/engine/execution/validate-worker-runtime.js +100 -0
- package/dist/engine/execution/validate-worker-thread.d.ts +2 -0
- package/dist/engine/execution/validate-worker-thread.js +71 -0
- package/dist/engine/execution-plan/desktop-distribution.js +3 -7
- package/dist/engine/execution-plan/mobile-distribution.js +4 -9
- package/dist/engine/execution-plan/phases.js +4 -5
- package/dist/engine/execution-plan/plan-utils.js +8 -5
- package/dist/engine/facilitate/context-loader.js +3 -9
- package/dist/engine/figma/code-connect-fetcher.js +9 -8
- package/dist/engine/filesystem-watcher/index.d.ts +3 -3
- package/dist/engine/filesystem-watcher/index.js +5 -5
- package/dist/engine/framework-detector.js +14 -15
- package/dist/engine/git/planu-autocommit.js +8 -7
- package/dist/engine/git/repository-probe.d.ts +3 -0
- package/dist/engine/git/repository-probe.js +13 -0
- package/dist/engine/global-rules-manager.js +2 -2
- package/dist/engine/handoff-artifacts/io.js +2 -3
- package/dist/engine/handoff-artifacts/schemas/red-tests-handoff.js +8 -1
- package/dist/engine/handoff-artifacts/schemas.d.ts +4 -0
- package/dist/engine/handoff-artifacts/schemas.js +5 -0
- package/dist/engine/hook-generator/stack-hook-templates.js +4 -4
- package/dist/engine/hooks/config-loader.js +1 -1
- package/dist/engine/hooks/context-injector.js +4 -7
- package/dist/engine/hooks/file-watcher.js +1 -14
- package/dist/engine/hooks-reconciler.js +6 -4
- package/dist/engine/infrastructure/component-mapper.js +5 -7
- package/dist/engine/infrastructure/docker-compose-generator.js +8 -10
- package/dist/engine/infrastructure/index.js +6 -3
- package/dist/engine/infrastructure/kubernetes-generator.js +2 -2
- package/dist/engine/infrastructure/railway-generator.js +10 -8
- package/dist/engine/infrastructure/signal-detector.js +6 -8
- package/dist/engine/infrastructure/terraform-generator.js +6 -8
- package/dist/engine/legal-compliance/detector.js +3 -2
- package/dist/engine/local-first/storage-advisor.js +5 -6
- package/dist/engine/macos-keychain-native-port.d.ts +5 -0
- package/dist/engine/macos-keychain-native-port.js +26 -0
- package/dist/engine/marketplace-fetcher/cache.js +3 -1
- package/dist/engine/mcp-config/mcp-config-writer.d.ts +2 -2
- package/dist/engine/mcp-config/mcp-config-writer.js +10 -8
- package/dist/engine/migration/db-migrator.js +2 -5
- package/dist/engine/migration/equivalence-mapper.js +12 -13
- package/dist/engine/migration/service-catalogs.js +6 -7
- package/dist/engine/migration/service-migrator-catalogs.js +6 -7
- package/dist/engine/mobile-detector-ios.js +2 -3
- package/dist/engine/mobile-detector.js +16 -4
- package/dist/engine/model-router/complexity-analyzer.js +4 -7
- package/dist/engine/multi-agent-review/security-analyzer.js +3 -1
- package/dist/engine/multi-app-detector/app-type.js +26 -21
- package/dist/engine/multi-app-detector/core.js +3 -2
- package/dist/engine/multi-teammate-review/disagreement-counter.js +2 -1
- package/dist/engine/multi-teammate-review/panel-orchestrator.js +0 -2
- package/dist/engine/mutation-config-generator.js +10 -7
- package/dist/engine/native-capability-policy.d.ts +4 -0
- package/dist/engine/native-capability-policy.js +24 -0
- package/dist/engine/native-platform.d.ts +3 -0
- package/dist/engine/native-platform.js +44 -0
- package/dist/engine/native-runtime-handshake.d.ts +20 -0
- package/dist/engine/native-runtime-handshake.js +230 -0
- package/dist/engine/network-policy.d.ts +27 -0
- package/dist/engine/network-policy.js +174 -0
- package/dist/engine/next-spec-resolver/dependency-filter.js +2 -22
- package/dist/engine/nfr-profile-detector.js +12 -16
- package/dist/engine/nli-keywords.js +8 -9
- package/dist/engine/oauth/config-guard.js +4 -5
- package/dist/engine/observability/local-execution-spans.d.ts +15 -0
- package/dist/engine/observability/local-execution-spans.js +63 -0
- package/dist/engine/orchestrator/runtime.js +0 -1
- package/dist/engine/package-registry.js +2 -6
- package/dist/engine/performance-analyzer.js +2 -5
- package/dist/engine/permissions-merger/stack-detectors.js +5 -5
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +39 -0
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +1310 -0
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +39 -0
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +1310 -0
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +38 -0
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +1310 -0
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +38 -0
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +1310 -0
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +38 -0
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +1310 -0
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +38 -0
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +1310 -0
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +38 -0
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +1310 -0
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +38 -0
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +1310 -0
- package/dist/engine/platform-detector/desktop.js +15 -2
- package/dist/engine/platform-detector/extensions.js +2 -1
- package/dist/engine/platform-detector/games.js +2 -1
- package/dist/engine/platform-detector.js +3 -2
- package/dist/engine/platform-engineering/idp-detector.js +2 -1
- package/dist/engine/plugin-installer/stack-matcher.js +9 -17
- package/dist/engine/plugins/lifecycle.js +6 -2
- package/dist/engine/prior-decisions/contradiction-detector.js +60 -30
- package/dist/engine/project-dna/stack-detector.js +15 -17
- package/dist/engine/project-graph/builder.js +11 -0
- package/dist/engine/project-graph/cache.js +1 -1
- package/dist/engine/project-graph/freshness-projection.d.ts +16 -0
- package/dist/engine/project-graph/freshness-projection.js +95 -0
- package/dist/engine/project-graph/index.d.ts +1 -0
- package/dist/engine/project-graph/index.js +1 -0
- package/dist/engine/project-graph/query.js +15 -25
- package/dist/engine/project-health-checker.js +3 -4
- package/dist/engine/project-scanner/stack-detector.js +17 -17
- package/dist/engine/property-test-generator.js +2 -3
- package/dist/engine/qa-gate.d.ts +1 -0
- package/dist/engine/qa-gate.js +6 -4
- package/dist/engine/quality-gates/gate-catalog.js +47 -46
- package/dist/engine/quality-gates/gate-injector.js +9 -4
- package/dist/engine/registry/core.js +0 -2
- package/dist/engine/registry/loader.js +9 -12
- package/dist/engine/registry/matcher.js +129 -24
- package/dist/engine/resilience-detector/concurrency-detector.js +4 -4
- package/dist/engine/resilience-detector/middleware-detector.js +11 -13
- package/dist/engine/resilience-detector/validation-detector.js +17 -4
- package/dist/engine/reverse-engineer/config-analyzer.js +9 -8
- package/dist/engine/reverse-engineer/orchestrator.js +5 -9
- package/dist/engine/reverse-engineer/test-file-parser.js +6 -3
- package/dist/engine/rules-generator/index.js +6 -5
- package/dist/engine/runtime-policy.d.ts +7 -0
- package/dist/engine/runtime-policy.js +101 -0
- package/dist/engine/runtime-security/checkers/content-security.js +21 -6
- package/dist/engine/runtime-security/checkers/file-permissions.js +0 -2
- package/dist/engine/safety/atomic-write-file.js +20 -9
- package/dist/engine/safety/atomic-writer.js +10 -3
- package/dist/engine/safety/cross-process-lock.d.ts +3 -40
- package/dist/engine/safety/cross-process-lock.js +268 -272
- package/dist/engine/safety/file-mutex.d.ts +2 -9
- package/dist/engine/safety/file-mutex.js +155 -87
- package/dist/engine/safety/health-monitor.js +15 -6
- package/dist/engine/safety/lock-lease-store.d.ts +79 -0
- package/dist/engine/safety/lock-lease-store.js +372 -0
- package/dist/engine/safety/lock-scavenger.d.ts +2 -5
- package/dist/engine/safety/lock-scavenger.js +158 -76
- package/dist/engine/safety/lock-staleness.js +4 -1
- package/dist/engine/safety/obsolete-lock-guard.d.ts +8 -0
- package/dist/engine/safety/obsolete-lock-guard.js +31 -0
- package/dist/engine/safety/transaction.js +3 -3
- package/dist/engine/sandbox/index.js +2 -1
- package/dist/engine/sandbox/runner-detect.js +3 -2
- package/dist/engine/sandbox/runner-docker.js +4 -3
- package/dist/engine/scan-project/module-discoverer.js +3 -2
- package/dist/engine/schema-generator.js +16 -9
- package/dist/engine/security-analyzer/criteria-generator.js +2 -3
- package/dist/engine/self-healing/healer.js +4 -4
- package/dist/engine/session-safeguard/learnings-buffer.d.ts +1 -2
- package/dist/engine/session-safeguard/learnings-buffer.js +42 -65
- package/dist/engine/shared-agent-memory.js +66 -74
- package/dist/engine/skill-adapter/command-rewriter.js +3 -2
- package/dist/engine/skill-adapter/index.js +3 -3
- package/dist/engine/skill-adapter/placeholder-map.js +3 -2
- package/dist/engine/skill-bootstrap/stack-matcher.js +47 -48
- package/dist/engine/skill-generation/stack-content-generator.js +0 -2
- package/dist/engine/skill-generator/workflow-skill-generator.js +2 -1
- package/dist/engine/skill-registry/ttl-refresh.js +3 -1
- package/dist/engine/skills/skills-fetcher.js +3 -6
- package/dist/engine/skills-reconciler.js +19 -39
- package/dist/engine/spec-coverage/test-finder.js +7 -6
- package/dist/engine/spec-diff-engine.js +3 -3
- package/dist/engine/spec-generator/api-key-resolver.d.ts +1 -1
- package/dist/engine/spec-generator/api-key-resolver.js +3 -24
- package/dist/engine/spec-generators/issue-extractor.js +9 -3
- package/dist/engine/spec-migrator/drift-detector.js +5 -13
- package/dist/engine/spec-migrator/filesystem-import.js +14 -10
- package/dist/engine/spec-migrator/planu-canonical-policy.js +10 -1
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +24 -25
- package/dist/engine/spec-registry/adapter.d.ts +0 -5
- package/dist/engine/spec-registry/adapter.js +45 -46
- package/dist/engine/spec-registry/client.js +2 -0
- package/dist/engine/spec-source-sync.js +3 -2
- package/dist/engine/spec-templates/custom-loader.js +2 -3
- package/dist/engine/spec-templates/templates-domain-specific.js +8 -5
- package/dist/engine/spec-templates/templates-perf-integration.js +2 -3
- package/dist/engine/stack-auditor/manifest-reader.js +0 -2
- package/dist/engine/staleness/stale-implementing.js +7 -7
- package/dist/engine/tdd-scaffold-generator.js +24 -11
- package/dist/engine/teammate-router/green-phase-verifier.js +2 -1
- package/dist/engine/teammate-router/red-phase-verifier.js +2 -1
- package/dist/engine/technology-registry.d.ts +11 -0
- package/dist/engine/technology-registry.js +142 -0
- package/dist/engine/telemetry/error-reporter.d.ts +1 -2
- package/dist/engine/telemetry/error-reporter.js +11 -18
- package/dist/engine/telemetry/health-checker.js +5 -14
- package/dist/engine/telemetry/index.d.ts +0 -1
- package/dist/engine/telemetry/index.js +0 -1
- package/dist/engine/telemetry/stance.js +3 -2
- package/dist/engine/telemetry/telemetry-client.d.ts +1 -5
- package/dist/engine/telemetry/telemetry-client.js +45 -29
- package/dist/engine/telemetry/telemetry-store.d.ts +3 -3
- package/dist/engine/telemetry/telemetry-store.js +30 -23
- package/dist/engine/test-framework-detector/detectors.js +8 -8
- package/dist/engine/test-framework-detector/strategy.js +8 -9
- package/dist/engine/test-generators/agent-test-templates.js +2 -3
- package/dist/engine/test-generators/contract-test-generator.d.ts +1 -1
- package/dist/engine/test-generators/contract-test-generator.js +9 -202
- package/dist/engine/test-generators/platform-test-templates-b.js +2 -3
- package/dist/engine/test-generators/platform-test-templates.js +2 -5
- package/dist/engine/test-mocks-generator.js +4 -3
- package/dist/engine/test-plan-generator.js +30 -12
- package/dist/engine/test-scaffold-generator/advanced-scaffold.js +4 -5
- package/dist/engine/test-scaffold-generator/unit-scaffold.js +5 -6
- package/dist/engine/timing/budget.d.ts +6 -17
- package/dist/engine/timing/budget.js +80 -64
- package/dist/engine/tool-groups/group-manager.js +3 -3
- package/dist/engine/universal-rules/installer.js +1 -1
- package/dist/engine/universal-rules/rules/planu-modes.js +2 -0
- package/dist/engine/universal-rules/rules/planu-release-policy.js +1 -0
- package/dist/engine/update-notifier.js +8 -5
- package/dist/engine/validation/approved-spec-portability.d.ts +12 -0
- package/dist/engine/validation/approved-spec-portability.js +107 -0
- package/dist/engine/validation/durable-validation.d.ts +36 -0
- package/dist/engine/validation/durable-validation.js +335 -0
- package/dist/engine/validation/validation-freshness.d.ts +36 -0
- package/dist/engine/validation/validation-freshness.js +427 -0
- package/dist/engine/validation/validation-receipt-codec.d.ts +13 -0
- package/dist/engine/validation/validation-receipt-codec.js +182 -0
- package/dist/engine/validation/validation-receipt.d.ts +59 -0
- package/dist/engine/validation/validation-receipt.js +282 -0
- package/dist/engine/validation/validation-source-policy.d.ts +4 -0
- package/dist/engine/validation/validation-source-policy.js +16 -0
- package/dist/engine/validation/validation-submission.d.ts +36 -0
- package/dist/engine/validation/validation-submission.js +128 -0
- package/dist/engine/validation-evidence-ledger.js +41 -0
- package/dist/engine/validation-impact-planner.js +25 -9
- package/dist/engine/validator/spec-compliance-runner.d.ts +1 -1
- package/dist/engine/validator/spec-compliance-runner.js +17 -8
- package/dist/engine/validator/validation-report-writer.js +1 -1
- package/dist/engine/validator.js +2 -1
- package/dist/engine/verify-integrations/index.js +1 -1
- package/dist/engine/version-resolver.js +5 -6
- package/dist/engine/web-fetcher/stack-advisor.js +156 -92
- package/dist/engine/web-fetcher/stack-detector.js +114 -54
- package/dist/engine/workspace-manager.js +3 -2
- package/dist/errors/classified-degradation.d.ts +3 -0
- package/dist/errors/classified-degradation.js +10 -0
- package/dist/errors/degradation-sink.d.ts +3 -0
- package/dist/errors/degradation-sink.js +5 -0
- package/dist/errors/error-taxonomy.d.ts +12 -0
- package/dist/errors/error-taxonomy.js +62 -0
- package/dist/hosts/claude-code/ux/mcp-prompts.js +10 -4
- package/dist/hosts/claude-code/ux/mcp-resources.js +4 -2
- package/dist/hosts/codex/config-scaffold.js +4 -3
- package/dist/hosts/gemini/config-scaffold.js +2 -1
- package/dist/hosts/gemini/multimodal-spec-flow.js +7 -4
- package/dist/index.js +21 -6
- package/dist/resources/patterns.js +3 -1
- package/dist/resources/specs.js +3 -1
- package/dist/resources/templates.js +3 -1
- package/dist/security/adapters/environment.d.ts +12 -0
- package/dist/security/adapters/environment.js +25 -0
- package/dist/security/adapters/linux-secret-service.d.ts +8 -0
- package/dist/security/adapters/linux-secret-service.js +60 -0
- package/dist/security/adapters/macos-keychain.d.ts +18 -0
- package/dist/security/adapters/macos-keychain.js +90 -0
- package/dist/security/adapters/windows-credential-manager.d.ts +8 -0
- package/dist/security/adapters/windows-credential-manager.js +58 -0
- package/dist/security/redactor.d.ts +5 -0
- package/dist/security/redactor.js +58 -0
- package/dist/security/secret-field.d.ts +5 -0
- package/dist/security/secret-field.js +17 -0
- package/dist/security/secret-migration.d.ts +3 -0
- package/dist/security/secret-migration.js +123 -0
- package/dist/security/secret-provider.d.ts +23 -0
- package/dist/security/secret-provider.js +145 -0
- package/dist/security/secure-json.d.ts +3 -0
- package/dist/security/secure-json.js +29 -0
- package/dist/storage/api-surfaces-store.js +2 -2
- package/dist/storage/approval-operation-lock.d.ts +1 -1
- package/dist/storage/approval-operation-lock.js +5 -3
- package/dist/storage/audit-trail-store.d.ts +1 -1
- package/dist/storage/audit-trail-store.js +48 -62
- package/dist/storage/autopilot-log-store.js +12 -5
- package/dist/storage/base-store.d.ts +8 -8
- package/dist/storage/base-store.js +57 -20
- package/dist/storage/checkpoint-store.js +5 -2
- package/dist/storage/comments-store.js +2 -2
- package/dist/storage/compliance-audit-store.js +5 -2
- package/dist/storage/compliance-gate-config-store.js +7 -4
- package/dist/storage/compliance-store.js +2 -2
- package/dist/storage/context-profile-store.js +2 -2
- package/dist/storage/crash-shield-store.js +8 -4
- package/dist/storage/feedback-store.js +10 -6
- package/dist/storage/figma-store.js +8 -4
- package/dist/storage/file-mutex.js +9 -1
- package/dist/storage/gaps-log.js +7 -4
- package/dist/storage/global-projects-store.js +4 -2
- package/dist/storage/ideas-store.js +2 -2
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/knowledge-store/knowledge.js +27 -33
- package/dist/storage/lessons-store.js +2 -2
- package/dist/storage/license-store.js +13 -41
- package/dist/storage/migrations/canonical-storage.d.ts +15 -0
- package/dist/storage/migrations/canonical-storage.js +599 -0
- package/dist/storage/oauth-store.js +36 -8
- package/dist/storage/path-resolver.js +16 -3
- package/dist/storage/pr-store.js +2 -2
- package/dist/storage/project-drift-store.js +20 -8
- package/dist/storage/project-identity.d.ts +32 -0
- package/dist/storage/project-identity.js +194 -0
- package/dist/storage/project-resolver.js +14 -7
- package/dist/storage/release-notes-store.js +2 -6
- package/dist/storage/runtime-db.d.ts +186 -0
- package/dist/storage/runtime-db.js +1054 -0
- package/dist/storage/schema-registry.d.ts +5 -0
- package/dist/storage/schema-registry.js +74 -0
- package/dist/storage/sentry-store.js +20 -9
- package/dist/storage/session-state-store.js +12 -5
- package/dist/storage/session-store.js +12 -4
- package/dist/storage/skill-registry-storage.d.ts +1 -1
- package/dist/storage/skill-registry-storage.js +13 -11
- package/dist/storage/slack-store.js +10 -5
- package/dist/storage/spec-store.js +31 -24
- package/dist/storage/status-store/file-lock.js +31 -9
- package/dist/storage/status-store/self-healing.js +10 -5
- package/dist/storage/status-store/version-sync.js +8 -6
- package/dist/storage/steering-store.js +2 -2
- package/dist/storage/storage-bundle.d.ts +5 -0
- package/dist/storage/storage-bundle.js +117 -0
- package/dist/storage/storage-catalog-validation.d.ts +3 -0
- package/dist/storage/storage-catalog-validation.js +20 -0
- package/dist/storage/storage-catalog.d.ts +25 -0
- package/dist/storage/storage-catalog.js +390 -0
- package/dist/storage/storage-layout.d.ts +24 -0
- package/dist/storage/storage-layout.js +101 -0
- package/dist/storage/storybook-store.js +2 -2
- package/dist/storage/supabase-store.js +24 -6
- package/dist/storage/tdd-policy-store.js +8 -6
- package/dist/storage/tech-debt-store.js +3 -3
- package/dist/storage/technology-selection-store.d.ts +1 -1
- package/dist/storage/technology-selection-store.js +6 -3
- package/dist/storage/token-cache-store.js +3 -3
- package/dist/storage/token-ledger-store.js +13 -8
- package/dist/storage/transition-log.js +7 -4
- package/dist/storage/trash-store.js +15 -6
- package/dist/storage/vector-store/migrator.js +7 -2
- package/dist/storage/vector-store/sqlite-adapter.js +4 -1
- package/dist/storage/webhook-server-store.js +2 -2
- package/dist/tools/api-spec-generator-handler.js +3 -2
- package/dist/tools/auto-update-handler.d.ts +1 -1
- package/dist/tools/auto-update-handler.js +1 -1
- package/dist/tools/browser-validate-handler.js +2 -1
- package/dist/tools/challenge-spec/platform-challenge-scenarios-b.js +8 -5
- package/dist/tools/challenge-spec/scenarios-desktop.js +8 -3
- package/dist/tools/challenge-spec/scenarios-failure.js +7 -2
- package/dist/tools/challenge-spec/security-challenge-scenarios.js +3 -3
- package/dist/tools/clarify-requirements/multiple-choice.js +53 -41
- package/dist/tools/comments-handler.js +2 -3
- package/dist/tools/compliance-test-handler.js +2 -2
- package/dist/tools/configure-telemetry.js +11 -11
- package/dist/tools/create-spec/auto-pipeline.js +13 -1
- package/dist/tools/create-spec/autopilot-analyzer.d.ts +1 -1
- package/dist/tools/create-spec/autopilot-analyzer.js +2 -4
- package/dist/tools/create-spec/mcp-tool-contract.d.ts +10 -4
- package/dist/tools/create-spec/mcp-tool-contract.js +7 -36
- package/dist/tools/create-spec/post-creation.d.ts +2 -1
- package/dist/tools/create-spec/post-creation.js +56 -54
- package/dist/tools/create-spec/relevance-scorer.d.ts +1 -1
- package/dist/tools/create-spec/relevance-scorer.js +17 -1
- package/dist/tools/create-spec/spec-builder.js +2 -12
- package/dist/tools/create-spec/spec-created-outbox-consumer.d.ts +14 -0
- package/dist/tools/create-spec/spec-created-outbox-consumer.js +176 -0
- package/dist/tools/create-spec.js +385 -520
- package/dist/tools/define-ui-contract/agent-interaction-contract.js +2 -4
- package/dist/tools/define-ui-contract-desktop/desktop-contract.js +12 -4
- package/dist/tools/define-ui-contract-desktop/native-contract.js +7 -3
- package/dist/tools/define-ui-contract-extensions.js +2 -3
- package/dist/tools/define-ui-contract-mobile.js +5 -9
- package/dist/tools/define-ui-contract-web/component-tree.js +4 -3
- package/dist/tools/define-ui-contract.js +7 -4
- package/dist/tools/deploy/deploy-spec.js +2 -4
- package/dist/tools/deploy/deploy-status.js +2 -4
- package/dist/tools/design-schema/nosql-category-schemas.js +2 -3
- package/dist/tools/design-schema-sql/migrations.js +3 -2
- package/dist/tools/design-schema-sql/tables.js +4 -1
- package/dist/tools/design-schema.js +4 -3
- package/dist/tools/feedback-handler.js +2 -4
- package/dist/tools/gc-data-projects.js +5 -5
- package/dist/tools/generate-execution-plan.js +3 -4
- package/dist/tools/generate-sub-agent.js +2 -1
- package/dist/tools/generate-tests/generators/a11y-tests-generator.js +5 -4
- package/dist/tools/generate-tests/generators/advanced-testing-generator.js +5 -4
- package/dist/tools/generate-tests/generators/concurrency-test-generator/js-templates.js +9 -4
- package/dist/tools/generate-tests/generators/concurrency-test-generator.js +8 -4
- package/dist/tools/generate-tests/generators/database/detectors.js +9 -3
- package/dist/tools/generate-tests/generators/database/index.js +2 -4
- package/dist/tools/generate-tests/generators/event-test-generator.js +5 -4
- package/dist/tools/generate-tests/generators/game-tests-generator.js +2 -1
- package/dist/tools/generate-tests/generators/graphql-test-generator.js +11 -5
- package/dist/tools/generate-tests/generators/grpc-test-generator.js +2 -3
- package/dist/tools/generate-tests/generators/llm-security-test-generator.js +4 -3
- package/dist/tools/generate-tests/generators/local-first-test-generator.js +4 -3
- package/dist/tools/generate-tests/generators/mcp-tests-generator.js +11 -9
- package/dist/tools/generate-tests/generators/microservices/index.js +2 -3
- package/dist/tools/generate-tests/generators/otel-test-generator.js +6 -3
- package/dist/tools/generate-tests/generators/security/detectors.js +2 -5
- package/dist/tools/generate-tests/generators/security/index.js +2 -4
- package/dist/tools/generate-tests/generators/supply-chain-test-generator.js +2 -3
- package/dist/tools/generate-tests/generators/visual-regression-generator.js +14 -17
- package/dist/tools/generate-tests/generators/websocket-test-generator.js +2 -3
- package/dist/tools/generate-tests/test-helpers.js +11 -12
- package/dist/tools/generate-tests-content.js +4 -3
- package/dist/tools/generate-tests.js +7 -4
- package/dist/tools/git/git-helpers.d.ts +0 -2
- package/dist/tools/git/git-helpers.js +7 -7
- package/dist/tools/github-pr-handler.js +10 -3
- package/dist/tools/heal-spec-docs.js +6 -53
- package/dist/tools/hook-generator-handler.js +6 -6
- package/dist/tools/init-constitution/platform-principles.js +4 -2
- package/dist/tools/init-project/agents-md-writer.js +14 -5
- package/dist/tools/init-project/claude-md-generator.js +19 -12
- package/dist/tools/init-project/handler.js +165 -73
- package/dist/tools/init-project/host-assets-writer.d.ts +1 -1
- package/dist/tools/init-project/host-assets-writer.js +4 -2
- package/dist/tools/init-project/lifecycle-helpers.js +3 -5
- package/dist/tools/init-project/lint-scaffolder.js +6 -2
- package/dist/tools/init-project/migration-runner.d.ts +10 -1
- package/dist/tools/init-project/migration-runner.js +68 -3
- package/dist/tools/init-project/per-client-files-writer.js +14 -4
- package/dist/tools/init-project/rules-generator.js +10 -2
- package/dist/tools/init-project/scaffold-writer.d.ts +1 -1
- package/dist/tools/init-project/scaffold-writer.js +17 -15
- package/dist/tools/jobs/handlers.d.ts +15 -0
- package/dist/tools/jobs/handlers.js +126 -0
- package/dist/tools/list-specs.js +35 -34
- package/dist/tools/manage-plugins-handler.js +17 -9
- package/dist/tools/migrate-tech/advanced-handlers.js +3 -2
- package/dist/tools/migrate-tech/build-tool-handlers.js +3 -2
- package/dist/tools/migrate-tech/core-handlers.js +5 -2
- package/dist/tools/migrate-tech/data-handlers.js +6 -3
- package/dist/tools/migrate-tech-advanced/rollback-handlers.js +3 -2
- package/dist/tools/migrate-tech-advanced/service-handlers.js +5 -2
- package/dist/tools/oauth-handler.js +7 -1
- package/dist/tools/register-sdd-tools.d.ts +1 -1
- package/dist/tools/register-sdd-tools.js +5 -0
- package/dist/tools/register-spec-tools/core-spec-tools.js +31 -0
- package/dist/tools/registry/auth.js +13 -5
- package/dist/tools/registry/publish.js +7 -4
- package/dist/tools/resolve-project-path.d.ts +2 -2
- package/dist/tools/resolve-project-path.js +7 -7
- package/dist/tools/reverse-engineer/analyzer.js +14 -14
- package/dist/tools/rollback-release.js +3 -2
- package/dist/tools/safe-handler.js +295 -171
- package/dist/tools/schema-generator-handler.js +2 -1
- package/dist/tools/schemas/output-schemas.d.ts +88 -82
- package/dist/tools/schemas/output-schemas.js +12 -94
- package/dist/tools/schemas/plugins-schemas.js +0 -2
- package/dist/tools/schemas/registry.js +0 -2
- package/dist/tools/schemas/skill-registry-schemas.js +0 -2
- package/dist/tools/schemas/validate-output-schema.d.ts +256 -0
- package/dist/tools/schemas/validate-output-schema.js +212 -0
- package/dist/tools/sentry-handler.js +6 -1
- package/dist/tools/skill-bootstrap-handler.js +7 -4
- package/dist/tools/status-handler.js +4 -0
- package/dist/tools/storage-bundle-handler.d.ts +9 -0
- package/dist/tools/storage-bundle-handler.js +29 -0
- package/dist/tools/storage-paths-handler.d.ts +9 -0
- package/dist/tools/storage-paths-handler.js +17 -0
- package/dist/tools/suggest-mcp-server.js +11 -5
- package/dist/tools/suggest-mcps.js +29 -9
- package/dist/tools/suggest-tooling/advanced-testing-catalog.js +4 -5
- package/dist/tools/suggest-tooling/build-tool-catalog.js +2 -1
- package/dist/tools/suggest-tooling/css-framework-catalog.js +13 -7
- package/dist/tools/suggest-tooling/framework-catalog.js +3 -5
- package/dist/tools/suggest-tooling/frontend-perf-catalog.js +2 -6
- package/dist/tools/suggest-tooling/handler.js +10 -4
- package/dist/tools/suggest-tooling/hypermedia-catalog.js +11 -3
- package/dist/tools/suggest-tooling/linting-catalog.js +4 -7
- package/dist/tools/suggest-tooling/llm-security-catalog.js +2 -1
- package/dist/tools/suggest-tooling/local-first-catalog.js +7 -5
- package/dist/tools/suggest-tooling/observability-catalog.js +3 -4
- package/dist/tools/suggest-tooling/otel-catalog.js +4 -1
- package/dist/tools/suggest-tooling/resilience-catalog.js +16 -5
- package/dist/tools/suggest-tooling/skills-catalog.js +10 -9
- package/dist/tools/suggest-tooling/supply-chain-catalog.js +2 -3
- package/dist/tools/supabase-handler.js +2 -1
- package/dist/tools/token-intelligence-handler.js +2 -2
- package/dist/tools/token-recording.js +2 -2
- package/dist/tools/token-savings-handler.js +2 -2
- package/dist/tools/tool-registry/core-tools.js +131 -0
- package/dist/tools/tool-registry/group-infra.js +10 -12
- package/dist/tools/tool-registry/group-quality-compliance.js +5 -12
- package/dist/tools/update-status/approval-gates.d.ts +13 -0
- package/dist/tools/update-status/approval-gates.js +81 -0
- package/dist/tools/update-status/batch.js +4 -2
- package/dist/tools/update-status/dod-gates.d.ts +6 -24
- package/dist/tools/update-status/dod-gates.js +60 -135
- package/dist/tools/update-status/done-receipt-verifier.d.ts +15 -0
- package/dist/tools/update-status/done-receipt-verifier.js +145 -0
- package/dist/tools/update-status/evidence-gate.js +154 -2
- package/dist/tools/update-status/file-sync.js +23 -12
- package/dist/tools/update-status/gate-harness.d.ts +7 -0
- package/dist/tools/update-status/gate-harness.js +8 -0
- package/dist/tools/update-status/index.js +228 -325
- package/dist/tools/update-status/qa-gate.d.ts +1 -1
- package/dist/tools/update-status/qa-gate.js +2 -2
- package/dist/tools/update-status/side-effects.js +30 -37
- package/dist/tools/update-status/transition-guard.d.ts +4 -2
- package/dist/tools/update-status/transition-guard.js +57 -10
- package/dist/tools/update-status-convention-gate.js +24 -23
- package/dist/tools/update-status-reconcile.js +2 -2
- package/dist/tools/validate-api-contract-handler.js +3 -3
- package/dist/tools/validate-lint.js +20 -13
- package/dist/tools/validate.d.ts +3 -1
- package/dist/tools/validate.js +213 -5
- package/dist/transports/http-transport.js +84 -61
- package/dist/transports/middleware/with-telemetry.js +3 -2
- package/dist/types/abortable-process.d.ts +2 -0
- package/dist/types/advanced-framework.js +0 -2
- package/dist/types/atomic-upgrade.d.ts +71 -0
- package/dist/types/atomic-upgrade.js +2 -0
- package/dist/types/ci.js +0 -2
- package/dist/types/core-bridge.d.ts +78 -1
- package/dist/types/crash-shield-framework-rules.d.ts +24 -0
- package/dist/types/crash-shield-framework-rules.js +2 -0
- package/dist/types/data-gc.d.ts +16 -0
- package/dist/types/deploy.js +0 -2
- package/dist/types/durable-job.d.ts +78 -0
- package/dist/types/durable-job.js +2 -0
- package/dist/types/durable-validation.d.ts +60 -0
- package/dist/types/durable-validation.js +2 -0
- package/dist/types/estimation.d.ts +2 -0
- package/dist/types/evidence-gates.d.ts +9 -1
- package/dist/types/execution.d.ts +43 -0
- package/dist/types/handoff-artifacts.d.ts +2 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/infrastructure.js +0 -2
- package/dist/types/lifecycle.js +0 -2
- package/dist/types/migration/canonical-storage.d.ts +37 -0
- package/dist/types/migration/canonical-storage.js +2 -0
- package/dist/types/migration/index.d.ts +1 -1
- package/dist/types/native-worker.d.ts +48 -0
- package/dist/types/native-worker.js +2 -0
- package/dist/types/network-policy.d.ts +12 -0
- package/dist/types/network-policy.js +2 -0
- package/dist/types/oauth.js +0 -2
- package/dist/types/observability.d.ts +32 -0
- package/dist/types/outbox-worker.d.ts +35 -0
- package/dist/types/outbox-worker.js +11 -0
- package/dist/types/permissions-config.d.ts +0 -27
- package/dist/types/permissions-config.js +0 -2
- package/dist/types/plugin-install.d.ts +0 -11
- package/dist/types/plugin-install.js +0 -2
- package/dist/types/portal.d.ts +1 -0
- package/dist/types/product-public.d.ts +75 -0
- package/dist/types/product-public.js +2 -0
- package/dist/types/project/inputs.d.ts +2 -0
- package/dist/types/project/mfe-detection.js +0 -2
- package/dist/types/project/platform-mobile.js +0 -2
- package/dist/types/project/platform-specialty.js +0 -2
- package/dist/types/project-knowledge-graph.d.ts +7 -0
- package/dist/types/runtime-database.d.ts +10 -0
- package/dist/types/runtime-database.js +2 -0
- package/dist/types/runtime-policy.d.ts +40 -0
- package/dist/types/runtime-policy.js +2 -0
- package/dist/types/safety.d.ts +4 -5
- package/dist/types/schema-registry.d.ts +12 -0
- package/dist/types/schema-registry.js +2 -0
- package/dist/types/security.d.ts +52 -0
- package/dist/types/skill-registry.js +0 -2
- package/dist/types/spec/inputs.d.ts +2 -0
- package/dist/types/spec-lock-v2.d.ts +13 -7
- package/dist/types/spec-lock-v2.js +1 -1
- package/dist/types/spec-registry.js +0 -2
- package/dist/types/storage-bundle.d.ts +13 -0
- package/dist/types/storage-bundle.js +2 -0
- package/dist/types/storage-catalog.d.ts +18 -0
- package/dist/types/storage-catalog.js +2 -0
- package/dist/types/team-planner.js +0 -2
- package/dist/types/technology-registry.d.ts +35 -0
- package/dist/types/technology-registry.js +2 -0
- package/dist/types/technology-symbols.generated.d.ts +117 -0
- package/dist/types/technology-symbols.generated.js +2 -0
- package/dist/types/telemetry.d.ts +3 -3
- package/dist/types/transport.d.ts +2 -0
- package/dist/types/validate-worker.d.ts +44 -0
- package/dist/types/validate-worker.js +2 -0
- package/dist/types/validation-evidence.d.ts +1 -1
- package/dist/types/validation-receipt.d.ts +104 -0
- package/dist/types/validation-receipt.js +2 -0
- package/package.json +31 -20
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
- package/dist/engine/telemetry/geo-resolver.d.ts +0 -9
- package/dist/engine/telemetry/geo-resolver.js +0 -43
- package/dist/storage/data-dir-discovery.d.ts +0 -31
- package/dist/storage/data-dir-discovery.js +0 -140
- package/dist/storage/migrations/cwd-to-absolute.d.ts +0 -25
- package/dist/storage/migrations/cwd-to-absolute.js +0 -281
- package/dist/tools/create-spec/adapters/prior-decisions-hint.d.ts +0 -14
- package/dist/tools/create-spec/adapters/prior-decisions-hint.js +0 -30
- package/dist/tools/init-project/ancestor-data-detector.d.ts +0 -22
- package/dist/tools/init-project/ancestor-data-detector.js +0 -81
- package/dist/tools/update-status/force-done-audit.d.ts +0 -2
- package/dist/tools/update-status/force-done-audit.js +0 -23
- package/dist/types/migration/cwd-to-absolute.d.ts +0 -8
- package/dist/types/migration/cwd-to-absolute.js +0 -3
|
@@ -0,0 +1,1054 @@
|
|
|
1
|
+
/* eslint-disable max-lines -- runtime schema, migrations and transactional operations remain co-located for auditability */
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
const CURRENT_SCHEMA_VERSION = 4;
|
|
6
|
+
const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
|
|
7
|
+
const DEFAULT_MAX_CONNECTIONS = 4;
|
|
8
|
+
const openConnections = new Map();
|
|
9
|
+
export const DURABLE_JOB_STATES = [
|
|
10
|
+
'accepted',
|
|
11
|
+
'running',
|
|
12
|
+
'checkpointed',
|
|
13
|
+
'cancelled',
|
|
14
|
+
'failed',
|
|
15
|
+
'dead-letter',
|
|
16
|
+
'completed',
|
|
17
|
+
];
|
|
18
|
+
export class RuntimeDatabaseCorruptionError extends Error {
|
|
19
|
+
databasePath;
|
|
20
|
+
detail;
|
|
21
|
+
constructor(databasePath, detail) {
|
|
22
|
+
super(`[Planu] Runtime database failed integrity check: ${detail}`);
|
|
23
|
+
this.databasePath = databasePath;
|
|
24
|
+
this.detail = detail;
|
|
25
|
+
this.name = 'RuntimeDatabaseCorruptionError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class RuntimeDatabaseResourceExhaustedError extends Error {
|
|
29
|
+
databasePath;
|
|
30
|
+
maxConnections;
|
|
31
|
+
constructor(databasePath, maxConnections) {
|
|
32
|
+
super(`[Planu] Runtime database connection limit reached (${String(maxConnections)})`);
|
|
33
|
+
this.databasePath = databasePath;
|
|
34
|
+
this.maxConnections = maxConnections;
|
|
35
|
+
this.name = 'RuntimeDatabaseResourceExhaustedError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function requireIdentifier(value, label) {
|
|
39
|
+
if (!value || value.length > 128 || !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value)) {
|
|
40
|
+
throw new Error(`[Planu] Invalid ${label}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function parseJson(raw) {
|
|
44
|
+
if (typeof raw !== 'string') {
|
|
45
|
+
throw new Error('[Planu] Runtime database contains invalid JSON');
|
|
46
|
+
}
|
|
47
|
+
return JSON.parse(raw);
|
|
48
|
+
}
|
|
49
|
+
function stringifyJson(value) {
|
|
50
|
+
const serialized = JSON.stringify(value);
|
|
51
|
+
if (serialized === undefined) {
|
|
52
|
+
throw new Error('[Planu] Runtime value is not JSON serializable');
|
|
53
|
+
}
|
|
54
|
+
return serialized;
|
|
55
|
+
}
|
|
56
|
+
/** A bounded (one connection), transactional SQLite runtime store. */
|
|
57
|
+
export class RuntimeDatabase {
|
|
58
|
+
path;
|
|
59
|
+
schemaVersion = CURRENT_SCHEMA_VERSION;
|
|
60
|
+
database;
|
|
61
|
+
connectionKey;
|
|
62
|
+
closed = false;
|
|
63
|
+
inTransaction = false;
|
|
64
|
+
// eslint-disable-next-line complexity -- constructor owns fail-safe acquisition and rollback
|
|
65
|
+
constructor(options) {
|
|
66
|
+
if (!options.path.trim()) {
|
|
67
|
+
throw new Error('[Planu] Runtime database path is required');
|
|
68
|
+
}
|
|
69
|
+
const busyTimeoutMs = options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS;
|
|
70
|
+
if (!Number.isSafeInteger(busyTimeoutMs) || busyTimeoutMs < 0 || busyTimeoutMs > 60_000) {
|
|
71
|
+
throw new Error('[Planu] busyTimeoutMs must be an integer between 0 and 60000');
|
|
72
|
+
}
|
|
73
|
+
if (options.path !== ':memory:' && !isAbsolute(options.path)) {
|
|
74
|
+
throw new Error('[Planu] Runtime database path must be absolute');
|
|
75
|
+
}
|
|
76
|
+
const maxConnections = options.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
|
|
77
|
+
if (!Number.isSafeInteger(maxConnections) || maxConnections < 1 || maxConnections > 32) {
|
|
78
|
+
throw new Error('[Planu] maxConnections must be an integer between 1 and 32');
|
|
79
|
+
}
|
|
80
|
+
this.path = options.path === ':memory:' ? options.path : resolve(options.path);
|
|
81
|
+
this.connectionKey =
|
|
82
|
+
this.path === ':memory:' ? `${this.path}:${randomConnectionKey()}` : this.path;
|
|
83
|
+
const currentConnections = openConnections.get(this.connectionKey) ?? 0;
|
|
84
|
+
if (currentConnections >= maxConnections) {
|
|
85
|
+
throw new RuntimeDatabaseResourceExhaustedError(this.path, maxConnections);
|
|
86
|
+
}
|
|
87
|
+
openConnections.set(this.connectionKey, currentConnections + 1);
|
|
88
|
+
let database;
|
|
89
|
+
try {
|
|
90
|
+
if (this.path !== ':memory:') {
|
|
91
|
+
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
92
|
+
}
|
|
93
|
+
database = new DatabaseSync(this.path, {
|
|
94
|
+
timeout: busyTimeoutMs,
|
|
95
|
+
enableForeignKeyConstraints: true,
|
|
96
|
+
enableDoubleQuotedStringLiterals: false,
|
|
97
|
+
allowExtension: false,
|
|
98
|
+
});
|
|
99
|
+
this.database = database;
|
|
100
|
+
this.database.exec('PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;');
|
|
101
|
+
this.migrate();
|
|
102
|
+
if (options.checkIntegrity !== false) {
|
|
103
|
+
this.assertIntegrity();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
database?.close();
|
|
108
|
+
this.releaseConnection();
|
|
109
|
+
if (error instanceof Error && /malformed|not a database|disk image/i.test(error.message)) {
|
|
110
|
+
throw new RuntimeDatabaseCorruptionError(this.path, error.message);
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
releaseConnection() {
|
|
116
|
+
const count = openConnections.get(this.connectionKey) ?? 0;
|
|
117
|
+
if (count <= 1) {
|
|
118
|
+
openConnections.delete(this.connectionKey);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
openConnections.set(this.connectionKey, count - 1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
assertOpen() {
|
|
125
|
+
if (this.closed) {
|
|
126
|
+
throw new Error('[Planu] Runtime database is closed');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// eslint-disable-next-line max-lines-per-function -- versioned DDL is intentionally auditable in one transaction
|
|
130
|
+
migrate() {
|
|
131
|
+
const row = this.database.prepare('PRAGMA user_version').get();
|
|
132
|
+
if (row.user_version > CURRENT_SCHEMA_VERSION) {
|
|
133
|
+
throw new Error(`[Planu] Runtime schema ${String(row.user_version)} is newer than supported`);
|
|
134
|
+
}
|
|
135
|
+
if (row.user_version === 0) {
|
|
136
|
+
this.database.exec(`
|
|
137
|
+
BEGIN IMMEDIATE;
|
|
138
|
+
CREATE TABLE runtime_records (
|
|
139
|
+
project_id TEXT NOT NULL,
|
|
140
|
+
workspace_id TEXT NOT NULL,
|
|
141
|
+
kind TEXT NOT NULL CHECK (kind IN ('mutation', 'index', 'audit-state')),
|
|
142
|
+
record_key TEXT NOT NULL,
|
|
143
|
+
value_json TEXT NOT NULL,
|
|
144
|
+
source_digest TEXT,
|
|
145
|
+
updated_at TEXT NOT NULL,
|
|
146
|
+
PRIMARY KEY (project_id, workspace_id, kind, record_key)
|
|
147
|
+
) STRICT;
|
|
148
|
+
CREATE TABLE jobs (
|
|
149
|
+
id TEXT NOT NULL,
|
|
150
|
+
project_id TEXT NOT NULL,
|
|
151
|
+
workspace_id TEXT NOT NULL,
|
|
152
|
+
state TEXT NOT NULL CHECK (state IN ('accepted', 'running', 'checkpointed', 'cancelled', 'failed', 'dead-letter', 'completed')),
|
|
153
|
+
payload_json TEXT NOT NULL,
|
|
154
|
+
checkpoint_json TEXT,
|
|
155
|
+
result_json TEXT,
|
|
156
|
+
error_message TEXT,
|
|
157
|
+
attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
|
158
|
+
max_attempts INTEGER NOT NULL DEFAULT 3 CHECK (max_attempts >= 1),
|
|
159
|
+
cancel_requested INTEGER NOT NULL DEFAULT 0 CHECK (cancel_requested IN (0, 1)),
|
|
160
|
+
lease_owner TEXT,
|
|
161
|
+
lease_expires_at TEXT,
|
|
162
|
+
fencing_token INTEGER NOT NULL DEFAULT 0 CHECK (fencing_token >= 0),
|
|
163
|
+
created_at TEXT NOT NULL,
|
|
164
|
+
updated_at TEXT NOT NULL,
|
|
165
|
+
PRIMARY KEY (project_id, workspace_id, id)
|
|
166
|
+
) STRICT;
|
|
167
|
+
CREATE TABLE outbox (
|
|
168
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
169
|
+
project_id TEXT NOT NULL,
|
|
170
|
+
workspace_id TEXT NOT NULL,
|
|
171
|
+
topic TEXT NOT NULL,
|
|
172
|
+
payload_json TEXT NOT NULL,
|
|
173
|
+
created_at TEXT NOT NULL,
|
|
174
|
+
delivered_at TEXT
|
|
175
|
+
) STRICT;
|
|
176
|
+
CREATE TABLE audit_log (
|
|
177
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
178
|
+
project_id TEXT NOT NULL,
|
|
179
|
+
workspace_id TEXT NOT NULL,
|
|
180
|
+
event_type TEXT NOT NULL,
|
|
181
|
+
payload_json TEXT NOT NULL,
|
|
182
|
+
created_at TEXT NOT NULL
|
|
183
|
+
) STRICT;
|
|
184
|
+
CREATE INDEX jobs_project_state ON jobs(project_id, workspace_id, state);
|
|
185
|
+
CREATE INDEX jobs_lease_expiry ON jobs(state, lease_expires_at);
|
|
186
|
+
CREATE INDEX outbox_pending ON outbox(project_id, workspace_id, delivered_at, id);
|
|
187
|
+
CREATE INDEX audit_project_time ON audit_log(project_id, workspace_id, created_at);
|
|
188
|
+
PRAGMA user_version = 4;
|
|
189
|
+
COMMIT;
|
|
190
|
+
`);
|
|
191
|
+
}
|
|
192
|
+
if (row.user_version === 1) {
|
|
193
|
+
this.database.exec(`
|
|
194
|
+
BEGIN IMMEDIATE;
|
|
195
|
+
CREATE TABLE jobs_v2 (
|
|
196
|
+
id TEXT NOT NULL,
|
|
197
|
+
project_id TEXT NOT NULL,
|
|
198
|
+
workspace_id TEXT NOT NULL,
|
|
199
|
+
state TEXT NOT NULL CHECK (state IN ('pending', 'running', 'completed', 'failed', 'cancelled')),
|
|
200
|
+
payload_json TEXT NOT NULL,
|
|
201
|
+
created_at TEXT NOT NULL,
|
|
202
|
+
updated_at TEXT NOT NULL,
|
|
203
|
+
PRIMARY KEY (project_id, workspace_id, id)
|
|
204
|
+
) STRICT;
|
|
205
|
+
INSERT INTO jobs_v2 SELECT id, project_id, workspace_id, state, payload_json, created_at, updated_at FROM jobs;
|
|
206
|
+
DROP TABLE jobs;
|
|
207
|
+
ALTER TABLE jobs_v2 RENAME TO jobs;
|
|
208
|
+
CREATE TABLE outbox_v2 (
|
|
209
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
210
|
+
project_id TEXT NOT NULL,
|
|
211
|
+
workspace_id TEXT NOT NULL,
|
|
212
|
+
topic TEXT NOT NULL,
|
|
213
|
+
payload_json TEXT NOT NULL,
|
|
214
|
+
created_at TEXT NOT NULL,
|
|
215
|
+
delivered_at TEXT
|
|
216
|
+
) STRICT;
|
|
217
|
+
INSERT INTO outbox_v2 (id, project_id, workspace_id, topic, payload_json, created_at, delivered_at)
|
|
218
|
+
SELECT id, '__legacy__', 'global', topic, payload_json, created_at, delivered_at FROM outbox;
|
|
219
|
+
DROP TABLE outbox;
|
|
220
|
+
ALTER TABLE outbox_v2 RENAME TO outbox;
|
|
221
|
+
CREATE INDEX jobs_project_state ON jobs(project_id, workspace_id, state);
|
|
222
|
+
CREATE INDEX outbox_pending ON outbox(project_id, workspace_id, delivered_at, id);
|
|
223
|
+
PRAGMA user_version = 2;
|
|
224
|
+
COMMIT;
|
|
225
|
+
`);
|
|
226
|
+
}
|
|
227
|
+
const migrated = this.database.prepare('PRAGMA user_version').get();
|
|
228
|
+
if (migrated.user_version === 2) {
|
|
229
|
+
this.database.exec(`
|
|
230
|
+
BEGIN IMMEDIATE;
|
|
231
|
+
CREATE TABLE jobs_v3 (
|
|
232
|
+
id TEXT NOT NULL,
|
|
233
|
+
project_id TEXT NOT NULL,
|
|
234
|
+
workspace_id TEXT NOT NULL,
|
|
235
|
+
state TEXT NOT NULL CHECK (state IN ('accepted', 'running', 'checkpointed', 'cancelled', 'failed', 'dead-letter', 'completed')),
|
|
236
|
+
payload_json TEXT NOT NULL,
|
|
237
|
+
checkpoint_json TEXT,
|
|
238
|
+
result_json TEXT,
|
|
239
|
+
error_message TEXT,
|
|
240
|
+
attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
|
241
|
+
max_attempts INTEGER NOT NULL DEFAULT 3 CHECK (max_attempts >= 1),
|
|
242
|
+
cancel_requested INTEGER NOT NULL DEFAULT 0 CHECK (cancel_requested IN (0, 1)),
|
|
243
|
+
lease_owner TEXT,
|
|
244
|
+
lease_expires_at TEXT,
|
|
245
|
+
created_at TEXT NOT NULL,
|
|
246
|
+
updated_at TEXT NOT NULL,
|
|
247
|
+
PRIMARY KEY (project_id, workspace_id, id)
|
|
248
|
+
) STRICT;
|
|
249
|
+
INSERT INTO jobs_v3
|
|
250
|
+
(id, project_id, workspace_id, state, payload_json, created_at, updated_at)
|
|
251
|
+
SELECT id, project_id, workspace_id,
|
|
252
|
+
CASE state
|
|
253
|
+
WHEN 'pending' THEN 'accepted'
|
|
254
|
+
WHEN 'running' THEN 'checkpointed'
|
|
255
|
+
ELSE state
|
|
256
|
+
END,
|
|
257
|
+
payload_json, created_at, updated_at
|
|
258
|
+
FROM jobs;
|
|
259
|
+
DROP TABLE jobs;
|
|
260
|
+
ALTER TABLE jobs_v3 RENAME TO jobs;
|
|
261
|
+
CREATE INDEX jobs_project_state ON jobs(project_id, workspace_id, state);
|
|
262
|
+
CREATE INDEX jobs_lease_expiry ON jobs(state, lease_expires_at);
|
|
263
|
+
PRAGMA user_version = 3;
|
|
264
|
+
COMMIT;
|
|
265
|
+
`);
|
|
266
|
+
}
|
|
267
|
+
const current = this.database.prepare('PRAGMA user_version').get();
|
|
268
|
+
if (current.user_version === 3) {
|
|
269
|
+
this.database.exec(`
|
|
270
|
+
BEGIN IMMEDIATE;
|
|
271
|
+
ALTER TABLE jobs ADD COLUMN fencing_token INTEGER NOT NULL DEFAULT 0 CHECK (fencing_token >= 0);
|
|
272
|
+
UPDATE jobs SET
|
|
273
|
+
state = CASE
|
|
274
|
+
WHEN cancel_requested = 1 THEN 'cancelled'
|
|
275
|
+
WHEN attempts >= max_attempts THEN 'dead-letter'
|
|
276
|
+
ELSE 'checkpointed'
|
|
277
|
+
END,
|
|
278
|
+
error_message = CASE
|
|
279
|
+
WHEN attempts >= max_attempts THEN 'Lease expired after maximum attempts'
|
|
280
|
+
ELSE error_message
|
|
281
|
+
END,
|
|
282
|
+
lease_owner = NULL,
|
|
283
|
+
lease_expires_at = NULL
|
|
284
|
+
WHERE state = 'running';
|
|
285
|
+
PRAGMA user_version = 4;
|
|
286
|
+
COMMIT;
|
|
287
|
+
`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
assertIntegrity() {
|
|
291
|
+
this.assertOpen();
|
|
292
|
+
const rows = this.database.prepare('PRAGMA quick_check').all();
|
|
293
|
+
const failures = rows.map((row) => row.quick_check).filter((value) => value !== 'ok');
|
|
294
|
+
if (failures.length > 0) {
|
|
295
|
+
throw new RuntimeDatabaseCorruptionError(this.path, failures.join('; '));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
journalMode() {
|
|
299
|
+
this.assertOpen();
|
|
300
|
+
const row = this.database.prepare('PRAGMA journal_mode').get();
|
|
301
|
+
return row.journal_mode;
|
|
302
|
+
}
|
|
303
|
+
transaction(operation) {
|
|
304
|
+
this.assertOpen();
|
|
305
|
+
if (this.inTransaction) {
|
|
306
|
+
throw new Error('[Planu] Nested runtime transactions are not supported');
|
|
307
|
+
}
|
|
308
|
+
this.database.exec('BEGIN IMMEDIATE');
|
|
309
|
+
this.inTransaction = true;
|
|
310
|
+
try {
|
|
311
|
+
const result = operation();
|
|
312
|
+
this.database.exec('COMMIT');
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
this.database.exec('ROLLBACK');
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
this.inTransaction = false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/** Run a consistent read-only snapshot without acquiring an immediate writer lock. */
|
|
324
|
+
snapshot(operation) {
|
|
325
|
+
this.assertOpen();
|
|
326
|
+
if (this.inTransaction) {
|
|
327
|
+
throw new Error('[Planu] Nested runtime transactions are not supported');
|
|
328
|
+
}
|
|
329
|
+
this.database.exec('BEGIN');
|
|
330
|
+
this.inTransaction = true;
|
|
331
|
+
try {
|
|
332
|
+
const result = operation();
|
|
333
|
+
this.database.exec('COMMIT');
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
this.database.exec('ROLLBACK');
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
this.inTransaction = false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
putRecord(record) {
|
|
345
|
+
this.assertOpen();
|
|
346
|
+
requireIdentifier(record.projectId, 'projectId');
|
|
347
|
+
requireIdentifier(record.workspaceId, 'workspaceId');
|
|
348
|
+
requireIdentifier(record.key, 'record key');
|
|
349
|
+
const valueJson = stringifyJson(record.value);
|
|
350
|
+
this.database
|
|
351
|
+
.prepare(`
|
|
352
|
+
INSERT INTO runtime_records
|
|
353
|
+
(project_id, workspace_id, kind, record_key, value_json, source_digest, updated_at)
|
|
354
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
355
|
+
ON CONFLICT(project_id, workspace_id, kind, record_key) DO UPDATE SET
|
|
356
|
+
value_json = excluded.value_json,
|
|
357
|
+
source_digest = excluded.source_digest,
|
|
358
|
+
updated_at = excluded.updated_at
|
|
359
|
+
WHERE excluded.updated_at >= runtime_records.updated_at
|
|
360
|
+
`)
|
|
361
|
+
.run(record.projectId, record.workspaceId, record.kind, record.key, valueJson, record.sourceDigest ?? null, record.updatedAt ?? new Date().toISOString());
|
|
362
|
+
}
|
|
363
|
+
getRecord(projectId, workspaceId, kind, key) {
|
|
364
|
+
this.assertOpen();
|
|
365
|
+
requireIdentifier(projectId, 'projectId');
|
|
366
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
367
|
+
requireIdentifier(key, 'record key');
|
|
368
|
+
const row = this.database
|
|
369
|
+
.prepare(`
|
|
370
|
+
SELECT project_id, workspace_id, kind, record_key, value_json, source_digest, updated_at
|
|
371
|
+
FROM runtime_records
|
|
372
|
+
WHERE project_id = ? AND workspace_id = ? AND kind = ? AND record_key = ?
|
|
373
|
+
`)
|
|
374
|
+
.get(projectId, workspaceId, kind, key);
|
|
375
|
+
return row ? mapRuntimeRecordRow(row) : undefined;
|
|
376
|
+
}
|
|
377
|
+
listRecords(projectId, workspaceId, kind) {
|
|
378
|
+
this.assertOpen();
|
|
379
|
+
requireIdentifier(projectId, 'projectId');
|
|
380
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
381
|
+
const rows = (kind
|
|
382
|
+
? this.database
|
|
383
|
+
.prepare(`SELECT project_id, workspace_id, kind, record_key, value_json, source_digest, updated_at
|
|
384
|
+
FROM runtime_records
|
|
385
|
+
WHERE project_id = ? AND workspace_id = ? AND kind = ?
|
|
386
|
+
ORDER BY record_key`)
|
|
387
|
+
.all(projectId, workspaceId, kind)
|
|
388
|
+
: this.database
|
|
389
|
+
.prepare(`SELECT project_id, workspace_id, kind, record_key, value_json, source_digest, updated_at
|
|
390
|
+
FROM runtime_records
|
|
391
|
+
WHERE project_id = ? AND workspace_id = ?
|
|
392
|
+
ORDER BY kind, record_key`)
|
|
393
|
+
.all(projectId, workspaceId));
|
|
394
|
+
return rows.map(mapRuntimeRecordRow);
|
|
395
|
+
}
|
|
396
|
+
enqueueJob(input) {
|
|
397
|
+
this.assertOpen();
|
|
398
|
+
requireIdentifier(input.id, 'job id');
|
|
399
|
+
requireIdentifier(input.projectId, 'projectId');
|
|
400
|
+
requireIdentifier(input.workspaceId, 'workspaceId');
|
|
401
|
+
const maxAttempts = input.maxAttempts ?? 3;
|
|
402
|
+
if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 100) {
|
|
403
|
+
throw new Error('[Planu] maxAttempts must be an integer between 1 and 100');
|
|
404
|
+
}
|
|
405
|
+
const now = new Date().toISOString();
|
|
406
|
+
this.database
|
|
407
|
+
.prepare(`
|
|
408
|
+
INSERT INTO jobs
|
|
409
|
+
(id, project_id, workspace_id, state, payload_json, max_attempts, created_at, updated_at)
|
|
410
|
+
VALUES (?, ?, ?, 'accepted', ?, ?, ?, ?)
|
|
411
|
+
`)
|
|
412
|
+
.run(input.id, input.projectId, input.workspaceId, stringifyJson(input.payload), maxAttempts, now, now);
|
|
413
|
+
}
|
|
414
|
+
getJob(projectId, workspaceId, id) {
|
|
415
|
+
this.assertOpen();
|
|
416
|
+
requireIdentifier(projectId, 'projectId');
|
|
417
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
418
|
+
requireIdentifier(id, 'job id');
|
|
419
|
+
const row = this.database
|
|
420
|
+
.prepare(`SELECT * FROM jobs WHERE project_id = ? AND workspace_id = ? AND id = ?`)
|
|
421
|
+
.get(projectId, workspaceId, id);
|
|
422
|
+
return row ? mapJobRow(row) : undefined;
|
|
423
|
+
}
|
|
424
|
+
listJobs(projectId, workspaceId) {
|
|
425
|
+
this.assertOpen();
|
|
426
|
+
requireIdentifier(projectId, 'projectId');
|
|
427
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
428
|
+
const rows = this.database
|
|
429
|
+
.prepare(`SELECT * FROM jobs WHERE project_id = ? AND workspace_id = ? ORDER BY created_at, id`)
|
|
430
|
+
.all(projectId, workspaceId);
|
|
431
|
+
return rows.map(mapJobRow);
|
|
432
|
+
}
|
|
433
|
+
listRunnableJobScopes() {
|
|
434
|
+
this.assertOpen();
|
|
435
|
+
return this.database
|
|
436
|
+
.prepare(`SELECT DISTINCT project_id AS projectId, workspace_id AS workspaceId
|
|
437
|
+
FROM jobs WHERE state IN ('accepted', 'checkpointed', 'running')
|
|
438
|
+
ORDER BY project_id, workspace_id`)
|
|
439
|
+
.all();
|
|
440
|
+
}
|
|
441
|
+
claimJob(input) {
|
|
442
|
+
this.assertOpen();
|
|
443
|
+
validateLeaseInput(input);
|
|
444
|
+
const now = input.now ?? new Date().toISOString();
|
|
445
|
+
const expiresAt = new Date(Date.parse(now) + input.leaseMs).toISOString();
|
|
446
|
+
return this.transaction(() => {
|
|
447
|
+
this.recoverExpiredJobs(now, input.projectId, input.workspaceId);
|
|
448
|
+
const result = this.database
|
|
449
|
+
.prepare(`UPDATE jobs
|
|
450
|
+
SET state = 'running', attempts = attempts + 1, fencing_token = fencing_token + 1, lease_owner = ?,
|
|
451
|
+
lease_expires_at = ?, updated_at = ?
|
|
452
|
+
WHERE project_id = ? AND workspace_id = ? AND id = ?
|
|
453
|
+
AND state IN ('accepted', 'checkpointed') AND cancel_requested = 0
|
|
454
|
+
AND attempts < max_attempts`)
|
|
455
|
+
.run(input.workerId, expiresAt, now, input.projectId, input.workspaceId, input.id);
|
|
456
|
+
return result.changes === 1
|
|
457
|
+
? this.getJob(input.projectId, input.workspaceId, input.id)
|
|
458
|
+
: undefined;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
renewJobLease(input) {
|
|
462
|
+
this.assertOpen();
|
|
463
|
+
validateLeaseInput(input);
|
|
464
|
+
const now = input.now ?? new Date().toISOString();
|
|
465
|
+
const expiresAt = new Date(Date.parse(now) + input.leaseMs).toISOString();
|
|
466
|
+
const result = this.database
|
|
467
|
+
.prepare(`UPDATE jobs SET lease_expires_at = ?, updated_at = ?
|
|
468
|
+
WHERE project_id = ? AND workspace_id = ? AND id = ?
|
|
469
|
+
AND state = 'running' AND lease_owner = ? AND fencing_token = ? AND cancel_requested = 0
|
|
470
|
+
AND lease_expires_at > ?`)
|
|
471
|
+
.run(expiresAt, now, input.projectId, input.workspaceId, input.id, input.workerId, requireFencingToken(input), now);
|
|
472
|
+
return result.changes === 1;
|
|
473
|
+
}
|
|
474
|
+
recordJobProgress(input) {
|
|
475
|
+
this.requireLeasedJob(input);
|
|
476
|
+
const now = input.now ?? new Date().toISOString();
|
|
477
|
+
const result = this.database
|
|
478
|
+
.prepare(`UPDATE jobs SET checkpoint_json = ?, updated_at = ?
|
|
479
|
+
WHERE project_id = ? AND workspace_id = ? AND id = ?
|
|
480
|
+
AND state = 'running' AND lease_owner = ? AND fencing_token = ? AND cancel_requested = 0
|
|
481
|
+
AND lease_expires_at > ?`)
|
|
482
|
+
.run(stringifyJson(input.checkpoint), now, input.projectId, input.workspaceId, input.id, input.workerId, requireFencingToken(input), now);
|
|
483
|
+
if (result.changes !== 1) {
|
|
484
|
+
throw new Error('[Planu] Job lease was lost before checkpoint');
|
|
485
|
+
}
|
|
486
|
+
return requireJobRecord(this.getJob(input.projectId, input.workspaceId, input.id), input.id);
|
|
487
|
+
}
|
|
488
|
+
checkpointJob(input) {
|
|
489
|
+
return this.finishLeasedUpdate(input, 'checkpointed', {
|
|
490
|
+
checkpointJson: stringifyJson(input.checkpoint),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
completeJob(input) {
|
|
494
|
+
return this.finishLeasedUpdate(input, 'completed', {
|
|
495
|
+
resultJson: stringifyJson(input.result),
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
failJob(input) {
|
|
499
|
+
if (!input.error.trim() || input.error.length > 4_096) {
|
|
500
|
+
throw new Error('[Planu] job error must contain between 1 and 4096 characters');
|
|
501
|
+
}
|
|
502
|
+
const current = this.requireLeasedJob(input);
|
|
503
|
+
const state = current.attempts >= current.maxAttempts ? 'dead-letter' : 'failed';
|
|
504
|
+
return this.finishLeasedUpdate(input, state, { errorMessage: input.error });
|
|
505
|
+
}
|
|
506
|
+
retryJob(projectId, workspaceId, id) {
|
|
507
|
+
this.assertOpen();
|
|
508
|
+
requireIdentifier(projectId, 'projectId');
|
|
509
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
510
|
+
requireIdentifier(id, 'job id');
|
|
511
|
+
const now = new Date().toISOString();
|
|
512
|
+
const result = this.database
|
|
513
|
+
.prepare(`UPDATE jobs SET state = 'accepted', error_message = NULL, updated_at = ?
|
|
514
|
+
WHERE project_id = ? AND workspace_id = ? AND id = ?
|
|
515
|
+
AND state = 'failed' AND cancel_requested = 0 AND attempts < max_attempts`)
|
|
516
|
+
.run(now, projectId, workspaceId, id);
|
|
517
|
+
if (result.changes !== 1) {
|
|
518
|
+
throw new Error('[Planu] Job is not retryable');
|
|
519
|
+
}
|
|
520
|
+
return requireJobRecord(this.getJob(projectId, workspaceId, id), id);
|
|
521
|
+
}
|
|
522
|
+
restartJob(projectId, workspaceId, id) {
|
|
523
|
+
this.assertOpen();
|
|
524
|
+
requireIdentifier(projectId, 'projectId');
|
|
525
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
526
|
+
requireIdentifier(id, 'job id');
|
|
527
|
+
const now = new Date().toISOString();
|
|
528
|
+
this.transaction(() => {
|
|
529
|
+
const current = this.getJob(projectId, workspaceId, id);
|
|
530
|
+
if (!current ||
|
|
531
|
+
!['accepted', 'checkpointed', 'failed', 'dead-letter', 'cancelled', 'completed'].includes(current.state)) {
|
|
532
|
+
throw new Error('[Planu] Job is not restartable');
|
|
533
|
+
}
|
|
534
|
+
this.archiveCheckpointBeforeRestart(projectId, workspaceId, id, now);
|
|
535
|
+
const result = this.database
|
|
536
|
+
.prepare(`UPDATE jobs SET state = 'accepted', attempts = 0, cancel_requested = 0,
|
|
537
|
+
checkpoint_json = NULL, result_json = NULL, error_message = NULL,
|
|
538
|
+
lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
|
|
539
|
+
WHERE project_id = ? AND workspace_id = ? AND id = ?
|
|
540
|
+
AND state IN ('accepted', 'checkpointed', 'failed', 'dead-letter', 'cancelled', 'completed')`)
|
|
541
|
+
.run(now, projectId, workspaceId, id);
|
|
542
|
+
if (result.changes !== 1) {
|
|
543
|
+
throw new Error('[Planu] Job is not restartable');
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
return requireJobRecord(this.getJob(projectId, workspaceId, id), id);
|
|
547
|
+
}
|
|
548
|
+
restartJobWithPayload(projectId, workspaceId, id, payload) {
|
|
549
|
+
this.assertOpen();
|
|
550
|
+
requireIdentifier(projectId, 'projectId');
|
|
551
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
552
|
+
requireIdentifier(id, 'job id');
|
|
553
|
+
const now = new Date().toISOString();
|
|
554
|
+
this.transaction(() => {
|
|
555
|
+
const current = this.getJob(projectId, workspaceId, id);
|
|
556
|
+
if (!current ||
|
|
557
|
+
!['failed', 'dead-letter', 'cancelled', 'completed'].includes(current.state)) {
|
|
558
|
+
throw new Error('[Planu] Job payload is not replaceable while work is active');
|
|
559
|
+
}
|
|
560
|
+
this.archiveCheckpointBeforeRestart(projectId, workspaceId, id, now);
|
|
561
|
+
const result = this.database
|
|
562
|
+
.prepare(`UPDATE jobs SET state = 'accepted', attempts = 0, cancel_requested = 0,
|
|
563
|
+
payload_json = ?, checkpoint_json = NULL, result_json = NULL, error_message = NULL,
|
|
564
|
+
lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
|
|
565
|
+
WHERE project_id = ? AND workspace_id = ? AND id = ?
|
|
566
|
+
AND state IN ('failed', 'dead-letter', 'cancelled', 'completed')`)
|
|
567
|
+
.run(stringifyJson(payload), now, projectId, workspaceId, id);
|
|
568
|
+
if (result.changes !== 1) {
|
|
569
|
+
throw new Error('[Planu] Job payload is not replaceable while work is active');
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
return requireJobRecord(this.getJob(projectId, workspaceId, id), id);
|
|
573
|
+
}
|
|
574
|
+
cancelJob(projectId, workspaceId, id) {
|
|
575
|
+
this.assertOpen();
|
|
576
|
+
requireIdentifier(projectId, 'projectId');
|
|
577
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
578
|
+
requireIdentifier(id, 'job id');
|
|
579
|
+
const current = this.getJob(projectId, workspaceId, id);
|
|
580
|
+
if (!current) {
|
|
581
|
+
throw new Error(`[Planu] Job ${id} was not found`);
|
|
582
|
+
}
|
|
583
|
+
if (current.state === 'completed' ||
|
|
584
|
+
current.state === 'cancelled' ||
|
|
585
|
+
current.state === 'dead-letter') {
|
|
586
|
+
return current;
|
|
587
|
+
}
|
|
588
|
+
const now = new Date().toISOString();
|
|
589
|
+
this.database
|
|
590
|
+
.prepare(`UPDATE jobs SET state = 'cancelled', cancel_requested = 1,
|
|
591
|
+
lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
|
|
592
|
+
WHERE project_id = ? AND workspace_id = ? AND id = ?`)
|
|
593
|
+
.run(now, projectId, workspaceId, id);
|
|
594
|
+
return requireJobRecord(this.getJob(projectId, workspaceId, id), id);
|
|
595
|
+
}
|
|
596
|
+
archiveCheckpointBeforeRestart(projectId, workspaceId, id, restartedAt) {
|
|
597
|
+
const current = this.getJob(projectId, workspaceId, id);
|
|
598
|
+
if (current?.checkpoint === undefined) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
this.putRecord({
|
|
602
|
+
projectId,
|
|
603
|
+
workspaceId,
|
|
604
|
+
kind: 'audit-state',
|
|
605
|
+
key: `job-attempt:${id.slice(0, 64)}:${process.hrtime.bigint().toString()}`,
|
|
606
|
+
value: {
|
|
607
|
+
operationId: id,
|
|
608
|
+
attempts: current.attempts,
|
|
609
|
+
previousState: current.state,
|
|
610
|
+
checkpoint: current.checkpoint,
|
|
611
|
+
restartedAt,
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
recoverExpiredJobs(now = new Date().toISOString(), projectId, workspaceId) {
|
|
616
|
+
this.assertOpen();
|
|
617
|
+
const scope = projectId && workspaceId ? ' AND project_id = ? AND workspace_id = ?' : '';
|
|
618
|
+
if ((projectId && !workspaceId) || (!projectId && workspaceId)) {
|
|
619
|
+
throw new Error('[Planu] projectId and workspaceId must be provided together');
|
|
620
|
+
}
|
|
621
|
+
if (projectId && workspaceId) {
|
|
622
|
+
requireIdentifier(projectId, 'projectId');
|
|
623
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
624
|
+
}
|
|
625
|
+
const statement = this.database.prepare(`UPDATE jobs
|
|
626
|
+
SET state = CASE
|
|
627
|
+
WHEN cancel_requested = 1 THEN 'cancelled'
|
|
628
|
+
WHEN attempts >= max_attempts THEN 'dead-letter'
|
|
629
|
+
ELSE 'checkpointed'
|
|
630
|
+
END,
|
|
631
|
+
error_message = CASE WHEN attempts >= max_attempts THEN 'Lease expired after maximum attempts' ELSE error_message END,
|
|
632
|
+
lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
|
|
633
|
+
WHERE state = 'running' AND lease_expires_at <= ?${scope}`);
|
|
634
|
+
const result = projectId && workspaceId
|
|
635
|
+
? statement.run(now, now, projectId, workspaceId)
|
|
636
|
+
: statement.run(now, now);
|
|
637
|
+
return Number(result.changes);
|
|
638
|
+
}
|
|
639
|
+
requireLeasedJob(input) {
|
|
640
|
+
validateLeaseInput(input);
|
|
641
|
+
const current = this.getJob(input.projectId, input.workspaceId, input.id);
|
|
642
|
+
if (current?.state !== 'running' ||
|
|
643
|
+
current.leaseOwner !== input.workerId ||
|
|
644
|
+
current.fencingToken !== requireFencingToken(input)) {
|
|
645
|
+
throw new Error('[Planu] Job lease is stale or not owned by this worker');
|
|
646
|
+
}
|
|
647
|
+
const now = input.now ?? new Date().toISOString();
|
|
648
|
+
if (!current.leaseExpiresAt || current.leaseExpiresAt <= now) {
|
|
649
|
+
throw new Error('[Planu] Job lease has expired');
|
|
650
|
+
}
|
|
651
|
+
return current;
|
|
652
|
+
}
|
|
653
|
+
finishLeasedUpdate(input, state, values) {
|
|
654
|
+
this.requireLeasedJob(input);
|
|
655
|
+
const now = input.now ?? new Date().toISOString();
|
|
656
|
+
const result = this.database
|
|
657
|
+
.prepare(`UPDATE jobs SET state = ?, checkpoint_json = COALESCE(?, checkpoint_json),
|
|
658
|
+
result_json = COALESCE(?, result_json), error_message = ?,
|
|
659
|
+
lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
|
|
660
|
+
WHERE project_id = ? AND workspace_id = ? AND id = ?
|
|
661
|
+
AND state = 'running' AND lease_owner = ? AND fencing_token = ?
|
|
662
|
+
AND lease_expires_at > ? AND cancel_requested = 0`)
|
|
663
|
+
.run(state, values.checkpointJson ?? null, values.resultJson ?? null, values.errorMessage ?? null, now, input.projectId, input.workspaceId, input.id, input.workerId, requireFencingToken(input), now);
|
|
664
|
+
if (result.changes !== 1) {
|
|
665
|
+
throw new Error('[Planu] Job lease was lost before commit');
|
|
666
|
+
}
|
|
667
|
+
return requireJobRecord(this.getJob(input.projectId, input.workspaceId, input.id), input.id);
|
|
668
|
+
}
|
|
669
|
+
enqueueOutbox(input) {
|
|
670
|
+
this.assertOpen();
|
|
671
|
+
requireIdentifier(input.projectId, 'projectId');
|
|
672
|
+
requireIdentifier(input.workspaceId, 'workspaceId');
|
|
673
|
+
requireIdentifier(input.topic, 'outbox topic');
|
|
674
|
+
const result = this.database
|
|
675
|
+
.prepare('INSERT INTO outbox (project_id, workspace_id, topic, payload_json, created_at) VALUES (?, ?, ?, ?, ?)')
|
|
676
|
+
.run(input.projectId, input.workspaceId, input.topic, stringifyJson(input.payload), new Date().toISOString());
|
|
677
|
+
return Number(result.lastInsertRowid);
|
|
678
|
+
}
|
|
679
|
+
listOutbox(projectId, workspaceId, options = {}) {
|
|
680
|
+
this.assertOpen();
|
|
681
|
+
requireIdentifier(projectId, 'projectId');
|
|
682
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
683
|
+
const limit = options.limit ?? 100;
|
|
684
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1_000) {
|
|
685
|
+
throw new Error('[Planu] outbox limit must be an integer between 1 and 1000');
|
|
686
|
+
}
|
|
687
|
+
const pendingClause = options.pendingOnly === false ? '' : ' AND delivered_at IS NULL';
|
|
688
|
+
const topics = options.topics ?? [];
|
|
689
|
+
for (const topic of topics) {
|
|
690
|
+
requireIdentifier(topic, 'outbox topic');
|
|
691
|
+
}
|
|
692
|
+
const topicsClause = topics.length > 0 ? ` AND topic IN (${topics.map(() => '?').join(', ')})` : '';
|
|
693
|
+
const rows = this.database
|
|
694
|
+
.prepare(`SELECT id, project_id, workspace_id, topic, payload_json, created_at, delivered_at
|
|
695
|
+
FROM outbox
|
|
696
|
+
WHERE project_id = ? AND workspace_id = ?${pendingClause}${topicsClause}
|
|
697
|
+
ORDER BY id LIMIT ?`)
|
|
698
|
+
.all(projectId, workspaceId, ...topics, limit);
|
|
699
|
+
return rows.map(mapOutboxRow);
|
|
700
|
+
}
|
|
701
|
+
listPendingOutboxScopes() {
|
|
702
|
+
this.assertOpen();
|
|
703
|
+
return this.database
|
|
704
|
+
.prepare(`SELECT DISTINCT project_id AS projectId, workspace_id AS workspaceId
|
|
705
|
+
FROM outbox WHERE delivered_at IS NULL ORDER BY project_id, workspace_id`)
|
|
706
|
+
.all();
|
|
707
|
+
}
|
|
708
|
+
markOutboxDelivered(projectId, workspaceId, id, deliveredAt = new Date().toISOString()) {
|
|
709
|
+
this.assertOpen();
|
|
710
|
+
requireIdentifier(projectId, 'projectId');
|
|
711
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
712
|
+
if (!Number.isSafeInteger(id) || id < 1) {
|
|
713
|
+
throw new Error('[Planu] outbox id must be a positive integer');
|
|
714
|
+
}
|
|
715
|
+
if (!Number.isFinite(Date.parse(deliveredAt))) {
|
|
716
|
+
throw new Error('[Planu] deliveredAt must be a valid ISO date');
|
|
717
|
+
}
|
|
718
|
+
const result = this.database
|
|
719
|
+
.prepare(`UPDATE outbox SET delivered_at = ?
|
|
720
|
+
WHERE id = ? AND project_id = ? AND workspace_id = ? AND delivered_at IS NULL`)
|
|
721
|
+
.run(deliveredAt, id, projectId, workspaceId);
|
|
722
|
+
return result.changes === 1;
|
|
723
|
+
}
|
|
724
|
+
appendAudit(input) {
|
|
725
|
+
this.assertOpen();
|
|
726
|
+
requireIdentifier(input.projectId, 'projectId');
|
|
727
|
+
requireIdentifier(input.workspaceId, 'workspaceId');
|
|
728
|
+
requireIdentifier(input.eventType, 'event type');
|
|
729
|
+
const result = this.database
|
|
730
|
+
.prepare(`
|
|
731
|
+
INSERT INTO audit_log (project_id, workspace_id, event_type, payload_json, created_at)
|
|
732
|
+
VALUES (?, ?, ?, ?, ?)
|
|
733
|
+
`)
|
|
734
|
+
.run(input.projectId, input.workspaceId, input.eventType, stringifyJson(input.payload), new Date().toISOString());
|
|
735
|
+
return Number(result.lastInsertRowid);
|
|
736
|
+
}
|
|
737
|
+
listAudit(projectId, workspaceId, options = {}) {
|
|
738
|
+
this.assertOpen();
|
|
739
|
+
requireIdentifier(projectId, 'projectId');
|
|
740
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
741
|
+
const afterId = options.afterId ?? 0;
|
|
742
|
+
const limit = options.limit ?? 100;
|
|
743
|
+
if (!Number.isSafeInteger(afterId) || afterId < 0) {
|
|
744
|
+
throw new Error('[Planu] audit afterId must be a non-negative integer');
|
|
745
|
+
}
|
|
746
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1_000) {
|
|
747
|
+
throw new Error('[Planu] audit limit must be an integer between 1 and 1000');
|
|
748
|
+
}
|
|
749
|
+
const rows = this.database
|
|
750
|
+
.prepare(`SELECT id, project_id, workspace_id, event_type, payload_json, created_at
|
|
751
|
+
FROM audit_log
|
|
752
|
+
WHERE project_id = ? AND workspace_id = ? AND id > ?
|
|
753
|
+
ORDER BY id LIMIT ?`)
|
|
754
|
+
.all(projectId, workspaceId, afterId, limit);
|
|
755
|
+
return rows.map(mapAuditRow);
|
|
756
|
+
}
|
|
757
|
+
/** Recover every authoritative mutable family for one isolated workspace. */
|
|
758
|
+
recoverWorkspace(projectId, workspaceId) {
|
|
759
|
+
this.assertOpen();
|
|
760
|
+
requireIdentifier(projectId, 'projectId');
|
|
761
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
762
|
+
this.recoverExpiredJobs(new Date().toISOString(), projectId, workspaceId);
|
|
763
|
+
return this.snapshot(() => {
|
|
764
|
+
const outbox = this.database
|
|
765
|
+
.prepare(`SELECT id, project_id, workspace_id, topic, payload_json, created_at, delivered_at
|
|
766
|
+
FROM outbox WHERE project_id = ? AND workspace_id = ? ORDER BY id`)
|
|
767
|
+
.all(projectId, workspaceId);
|
|
768
|
+
const audit = this.database
|
|
769
|
+
.prepare(`SELECT id, project_id, workspace_id, event_type, payload_json, created_at
|
|
770
|
+
FROM audit_log WHERE project_id = ? AND workspace_id = ? ORDER BY id`)
|
|
771
|
+
.all(projectId, workspaceId);
|
|
772
|
+
return {
|
|
773
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
774
|
+
projectId,
|
|
775
|
+
workspaceId,
|
|
776
|
+
records: this.listRecords(projectId, workspaceId),
|
|
777
|
+
jobs: this.listJobs(projectId, workspaceId),
|
|
778
|
+
outbox: outbox.map(mapOutboxRow),
|
|
779
|
+
audit: audit.map(mapAuditRow),
|
|
780
|
+
};
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
exportProject(projectId) {
|
|
784
|
+
this.assertOpen();
|
|
785
|
+
requireIdentifier(projectId, 'projectId');
|
|
786
|
+
return this.snapshot(() => this.exportProjectSnapshot(projectId));
|
|
787
|
+
}
|
|
788
|
+
exportProjectSnapshot(projectId) {
|
|
789
|
+
const records = this.database
|
|
790
|
+
.prepare('SELECT project_id, workspace_id, kind, record_key, value_json, source_digest, updated_at FROM runtime_records WHERE project_id = ? ORDER BY workspace_id, kind, record_key')
|
|
791
|
+
.all(projectId);
|
|
792
|
+
const mapRows = (rows) => rows.map((row) => Object.fromEntries(Object.entries(row).map(([key, value]) => [
|
|
793
|
+
key,
|
|
794
|
+
key.endsWith('_json') && value !== null ? parseJson(value) : value,
|
|
795
|
+
])));
|
|
796
|
+
const mappedRecords = records.map((row) => ({
|
|
797
|
+
projectId: String(row.project_id),
|
|
798
|
+
workspaceId: String(row.workspace_id),
|
|
799
|
+
kind: row.kind,
|
|
800
|
+
key: String(row.record_key),
|
|
801
|
+
value: parseJson(row.value_json),
|
|
802
|
+
sourceDigest: typeof row.source_digest === 'string' ? row.source_digest : undefined,
|
|
803
|
+
updatedAt: String(row.updated_at),
|
|
804
|
+
}));
|
|
805
|
+
const jobs = this.database
|
|
806
|
+
.prepare('SELECT * FROM jobs WHERE project_id = ? ORDER BY workspace_id, id')
|
|
807
|
+
.all(projectId);
|
|
808
|
+
const outbox = this.database
|
|
809
|
+
.prepare('SELECT * FROM outbox WHERE project_id = ? ORDER BY workspace_id, id')
|
|
810
|
+
.all(projectId);
|
|
811
|
+
const audit = this.database
|
|
812
|
+
.prepare('SELECT * FROM audit_log WHERE project_id = ? ORDER BY workspace_id, id')
|
|
813
|
+
.all(projectId);
|
|
814
|
+
return {
|
|
815
|
+
schemaVersion: 1,
|
|
816
|
+
projectId,
|
|
817
|
+
records: mappedRecords,
|
|
818
|
+
jobs: mapRows(jobs),
|
|
819
|
+
outbox: mapRows(outbox),
|
|
820
|
+
audit: mapRows(audit),
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
// eslint-disable-next-line complexity -- portable job validation is intentionally explicit
|
|
824
|
+
importJob(job) {
|
|
825
|
+
requireIdentifier(String(job.id), 'job id');
|
|
826
|
+
requireIdentifier(String(job.workspace_id), 'workspaceId');
|
|
827
|
+
const importedState = String(job.state);
|
|
828
|
+
const attempts = job.attempts === undefined ? 0 : Number(job.attempts);
|
|
829
|
+
const maxAttempts = job.max_attempts === undefined ? 3 : Number(job.max_attempts);
|
|
830
|
+
const fencingToken = job.fencing_token === undefined ? 0 : Number(job.fencing_token);
|
|
831
|
+
if (!Number.isSafeInteger(attempts) ||
|
|
832
|
+
attempts < 0 ||
|
|
833
|
+
!Number.isSafeInteger(maxAttempts) ||
|
|
834
|
+
maxAttempts < 1 ||
|
|
835
|
+
maxAttempts > 100 ||
|
|
836
|
+
!Number.isSafeInteger(fencingToken) ||
|
|
837
|
+
fencingToken < 0) {
|
|
838
|
+
throw new Error('[Planu] Runtime bundle contains invalid job attempts');
|
|
839
|
+
}
|
|
840
|
+
const importedRunning = importedState === 'running';
|
|
841
|
+
const state = importedState === 'pending'
|
|
842
|
+
? 'accepted'
|
|
843
|
+
: importedRunning
|
|
844
|
+
? job.cancel_requested === 1
|
|
845
|
+
? 'cancelled'
|
|
846
|
+
: attempts >= maxAttempts
|
|
847
|
+
? 'dead-letter'
|
|
848
|
+
: 'checkpointed'
|
|
849
|
+
: importedState;
|
|
850
|
+
if (!DURABLE_JOB_STATES.includes(state)) {
|
|
851
|
+
throw new Error('[Planu] Runtime bundle contains invalid job state');
|
|
852
|
+
}
|
|
853
|
+
this.database
|
|
854
|
+
.prepare(`INSERT INTO jobs
|
|
855
|
+
(id, project_id, workspace_id, state, payload_json, checkpoint_json, result_json,
|
|
856
|
+
error_message, attempts, max_attempts, cancel_requested, lease_owner,
|
|
857
|
+
lease_expires_at, fencing_token, created_at, updated_at)
|
|
858
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
859
|
+
ON CONFLICT(project_id, workspace_id, id) DO UPDATE SET
|
|
860
|
+
state = excluded.state, payload_json = excluded.payload_json,
|
|
861
|
+
checkpoint_json = excluded.checkpoint_json, result_json = excluded.result_json,
|
|
862
|
+
error_message = excluded.error_message, attempts = excluded.attempts,
|
|
863
|
+
max_attempts = excluded.max_attempts, cancel_requested = excluded.cancel_requested,
|
|
864
|
+
lease_owner = excluded.lease_owner, lease_expires_at = excluded.lease_expires_at,
|
|
865
|
+
fencing_token = MAX(jobs.fencing_token, excluded.fencing_token),
|
|
866
|
+
updated_at = excluded.updated_at
|
|
867
|
+
WHERE jobs.state != 'running' AND excluded.updated_at >= jobs.updated_at
|
|
868
|
+
AND NOT (
|
|
869
|
+
jobs.state IN ('cancelled', 'failed', 'dead-letter', 'completed')
|
|
870
|
+
AND excluded.state IN ('accepted', 'running', 'checkpointed')
|
|
871
|
+
)`)
|
|
872
|
+
.run(String(job.id), String(job.project_id), String(job.workspace_id), state, stringifyJson(job.payload_json), job.checkpoint_json === null || job.checkpoint_json === undefined
|
|
873
|
+
? null
|
|
874
|
+
: stringifyJson(job.checkpoint_json), job.result_json === null || job.result_json === undefined
|
|
875
|
+
? null
|
|
876
|
+
: stringifyJson(job.result_json), typeof job.error_message === 'string' ? job.error_message : null, attempts, maxAttempts, job.cancel_requested === 1 ? 1 : 0, !importedRunning && typeof job.lease_owner === 'string' ? job.lease_owner : null, !importedRunning && typeof job.lease_expires_at === 'string' ? job.lease_expires_at : null, fencingToken, String(job.created_at), String(job.updated_at));
|
|
877
|
+
}
|
|
878
|
+
assertLedgerIdAvailable(table, id, projectId, workspaceId) {
|
|
879
|
+
const existing = this.database
|
|
880
|
+
.prepare(`SELECT project_id, workspace_id FROM ${table} WHERE id = ?`)
|
|
881
|
+
.get(id);
|
|
882
|
+
if (existing && (existing.project_id !== projectId || existing.workspace_id !== workspaceId)) {
|
|
883
|
+
throw new Error(`[Planu] Runtime bundle ${table} id collides with another project`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
importOutbox(item, projectId) {
|
|
887
|
+
const id = Number(item.id);
|
|
888
|
+
if (!Number.isSafeInteger(id) || id < 1) {
|
|
889
|
+
throw new Error('[Planu] Runtime bundle contains invalid outbox id');
|
|
890
|
+
}
|
|
891
|
+
const workspaceId = String(item.workspace_id);
|
|
892
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
893
|
+
requireIdentifier(String(item.topic), 'outbox topic');
|
|
894
|
+
this.assertLedgerIdAvailable('outbox', id, projectId, workspaceId);
|
|
895
|
+
this.database
|
|
896
|
+
.prepare(`INSERT INTO outbox
|
|
897
|
+
(id, project_id, workspace_id, topic, payload_json, created_at, delivered_at)
|
|
898
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
899
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
900
|
+
topic = excluded.topic,
|
|
901
|
+
payload_json = excluded.payload_json,
|
|
902
|
+
delivered_at = excluded.delivered_at
|
|
903
|
+
WHERE outbox.project_id = excluded.project_id
|
|
904
|
+
AND outbox.workspace_id = excluded.workspace_id`)
|
|
905
|
+
.run(id, projectId, workspaceId, String(item.topic), stringifyJson(item.payload_json), String(item.created_at), typeof item.delivered_at === 'string' ? item.delivered_at : null);
|
|
906
|
+
}
|
|
907
|
+
importAudit(event, projectId) {
|
|
908
|
+
const id = Number(event.id);
|
|
909
|
+
if (!Number.isSafeInteger(id) || id < 1) {
|
|
910
|
+
throw new Error('[Planu] Runtime bundle contains invalid audit id');
|
|
911
|
+
}
|
|
912
|
+
const workspaceId = String(event.workspace_id);
|
|
913
|
+
requireIdentifier(workspaceId, 'workspaceId');
|
|
914
|
+
requireIdentifier(String(event.event_type), 'event type');
|
|
915
|
+
this.assertLedgerIdAvailable('audit_log', id, projectId, workspaceId);
|
|
916
|
+
this.database
|
|
917
|
+
.prepare(`INSERT INTO audit_log
|
|
918
|
+
(id, project_id, workspace_id, event_type, payload_json, created_at)
|
|
919
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
920
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
921
|
+
event_type = excluded.event_type,
|
|
922
|
+
payload_json = excluded.payload_json
|
|
923
|
+
WHERE audit_log.project_id = excluded.project_id
|
|
924
|
+
AND audit_log.workspace_id = excluded.workspace_id`)
|
|
925
|
+
.run(id, projectId, workspaceId, String(event.event_type), stringifyJson(event.payload_json), String(event.created_at));
|
|
926
|
+
}
|
|
927
|
+
importProject(bundle) {
|
|
928
|
+
requireIdentifier(bundle.projectId, 'projectId');
|
|
929
|
+
const foreignProject = [
|
|
930
|
+
...bundle.records.map((item) => item.projectId),
|
|
931
|
+
...bundle.jobs.map((item) => String(item.project_id)),
|
|
932
|
+
...bundle.outbox.map((item) => String(item.project_id)),
|
|
933
|
+
...bundle.audit.map((item) => String(item.project_id)),
|
|
934
|
+
].find((projectId) => projectId !== bundle.projectId);
|
|
935
|
+
if (foreignProject) {
|
|
936
|
+
throw new Error('[Planu] Runtime bundle contains cross-project data');
|
|
937
|
+
}
|
|
938
|
+
this.transaction(() => {
|
|
939
|
+
for (const record of bundle.records) {
|
|
940
|
+
this.putRecord(record);
|
|
941
|
+
}
|
|
942
|
+
for (const job of bundle.jobs) {
|
|
943
|
+
this.importJob(job);
|
|
944
|
+
}
|
|
945
|
+
for (const item of bundle.outbox) {
|
|
946
|
+
this.importOutbox(item, bundle.projectId);
|
|
947
|
+
}
|
|
948
|
+
for (const event of bundle.audit) {
|
|
949
|
+
this.importAudit(event, bundle.projectId);
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
close() {
|
|
954
|
+
if (this.closed) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
try {
|
|
958
|
+
this.database.close();
|
|
959
|
+
}
|
|
960
|
+
finally {
|
|
961
|
+
this.releaseConnection();
|
|
962
|
+
this.closed = true;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
[Symbol.dispose]() {
|
|
966
|
+
this.close();
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
let inMemoryConnectionSequence = 0;
|
|
970
|
+
function randomConnectionKey() {
|
|
971
|
+
inMemoryConnectionSequence += 1;
|
|
972
|
+
return String(inMemoryConnectionSequence);
|
|
973
|
+
}
|
|
974
|
+
function validateLeaseInput(input) {
|
|
975
|
+
requireIdentifier(input.id, 'job id');
|
|
976
|
+
requireIdentifier(input.projectId, 'projectId');
|
|
977
|
+
requireIdentifier(input.workspaceId, 'workspaceId');
|
|
978
|
+
requireIdentifier(input.workerId, 'worker id');
|
|
979
|
+
if (!Number.isSafeInteger(input.leaseMs) || input.leaseMs < 100 || input.leaseMs > 300_000) {
|
|
980
|
+
throw new Error('[Planu] leaseMs must be an integer between 100 and 300000');
|
|
981
|
+
}
|
|
982
|
+
if (input.now !== undefined && !Number.isFinite(Date.parse(input.now))) {
|
|
983
|
+
throw new Error('[Planu] lease timestamp must be a valid ISO date');
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function requireFencingToken(input) {
|
|
987
|
+
const token = input.fencingToken;
|
|
988
|
+
if (token === undefined || !Number.isSafeInteger(token) || token < 1) {
|
|
989
|
+
throw new Error('[Planu] Job lease requires a positive fencing token');
|
|
990
|
+
}
|
|
991
|
+
return token;
|
|
992
|
+
}
|
|
993
|
+
function optionalJson(value) {
|
|
994
|
+
return typeof value === 'string' ? parseJson(value) : undefined;
|
|
995
|
+
}
|
|
996
|
+
function mapRuntimeRecordRow(row) {
|
|
997
|
+
return {
|
|
998
|
+
projectId: row.project_id,
|
|
999
|
+
workspaceId: row.workspace_id,
|
|
1000
|
+
kind: row.kind,
|
|
1001
|
+
key: row.record_key,
|
|
1002
|
+
value: parseJson(row.value_json),
|
|
1003
|
+
sourceDigest: row.source_digest ?? undefined,
|
|
1004
|
+
updatedAt: row.updated_at,
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function mapOutboxRow(row) {
|
|
1008
|
+
return {
|
|
1009
|
+
id: Number(row.id),
|
|
1010
|
+
projectId: String(row.project_id),
|
|
1011
|
+
workspaceId: String(row.workspace_id),
|
|
1012
|
+
topic: String(row.topic),
|
|
1013
|
+
payload: parseJson(row.payload_json),
|
|
1014
|
+
createdAt: String(row.created_at),
|
|
1015
|
+
deliveredAt: typeof row.delivered_at === 'string' ? row.delivered_at : undefined,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
function mapAuditRow(row) {
|
|
1019
|
+
return {
|
|
1020
|
+
id: Number(row.id),
|
|
1021
|
+
projectId: String(row.project_id),
|
|
1022
|
+
workspaceId: String(row.workspace_id),
|
|
1023
|
+
eventType: String(row.event_type),
|
|
1024
|
+
payload: parseJson(row.payload_json),
|
|
1025
|
+
createdAt: String(row.created_at),
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function requireJobRecord(job, id) {
|
|
1029
|
+
if (!job) {
|
|
1030
|
+
throw new Error(`[Planu] Job ${id} disappeared after a committed update`);
|
|
1031
|
+
}
|
|
1032
|
+
return job;
|
|
1033
|
+
}
|
|
1034
|
+
function mapJobRow(row) {
|
|
1035
|
+
return {
|
|
1036
|
+
id: String(row.id),
|
|
1037
|
+
projectId: String(row.project_id),
|
|
1038
|
+
workspaceId: String(row.workspace_id),
|
|
1039
|
+
state: row.state,
|
|
1040
|
+
payload: parseJson(row.payload_json),
|
|
1041
|
+
checkpoint: optionalJson(row.checkpoint_json),
|
|
1042
|
+
result: optionalJson(row.result_json),
|
|
1043
|
+
error: typeof row.error_message === 'string' ? row.error_message : undefined,
|
|
1044
|
+
attempts: Number(row.attempts),
|
|
1045
|
+
maxAttempts: Number(row.max_attempts),
|
|
1046
|
+
cancelRequested: row.cancel_requested === 1,
|
|
1047
|
+
leaseOwner: typeof row.lease_owner === 'string' ? row.lease_owner : undefined,
|
|
1048
|
+
fencingToken: Number(row.fencing_token),
|
|
1049
|
+
leaseExpiresAt: typeof row.lease_expires_at === 'string' ? row.lease_expires_at : undefined,
|
|
1050
|
+
createdAt: String(row.created_at),
|
|
1051
|
+
updatedAt: String(row.updated_at),
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
//# sourceMappingURL=runtime-db.js.map
|