@planu/cli 4.14.1 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -1
- package/LICENSE +55 -12
- package/README.md +10 -2
- package/dist/cli/commands/install.d.ts +1 -12
- package/dist/cli/commands/install.js +9 -65
- package/dist/cli/commands/job.js +4 -3
- package/dist/cli/commands/spec.js +2 -2
- package/dist/cli/commands/validate.js +2 -2
- package/dist/cli/router.js +1 -3
- package/dist/config/environment-schema.json +1 -3
- package/dist/config/official-sdd-tools.d.ts +4 -0
- package/dist/config/official-sdd-tools.js +35 -0
- package/dist/config/product-public.d.ts +139 -3
- package/dist/config/product-public.js +8 -1
- package/dist/config/product-public.json +7 -36
- package/dist/config/registries/hosts/codex.json +20 -363
- package/dist/config/release-policy.json +84 -2
- package/dist/config/runtime-policy.json +12 -1
- package/dist/config/skill-templates/planu-resume-work.md +2 -2
- package/dist/core/index.d.ts +1 -1
- package/dist/core/spec-validator.d.ts +4 -4
- package/dist/core/spec-validator.js +1 -1
- package/dist/engine/abortable-process-runner.d.ts +20 -2
- package/dist/engine/abortable-process-runner.js +243 -74
- package/dist/engine/ambiguity-scorer.js +4 -26
- package/dist/engine/auth/scope-mapper.js +1 -7
- package/dist/engine/autopilot/state-updater.js +4 -4
- package/dist/engine/cascade-hooks/hooks/review-enricher.hook.js +9 -6
- package/dist/engine/cascade-hooks/state-drift-detector.d.ts +3 -3
- package/dist/engine/cascade-hooks/state-drift-detector.js +1 -1
- package/dist/engine/ci-generator/planu-config.js +1 -7
- package/dist/engine/clarification-gate/gate.d.ts +3 -3
- package/dist/engine/code-scanner/layer-scanner.js +6 -22
- package/dist/engine/core-bridge.d.ts +3 -3
- package/dist/engine/cost-guardrails.d.ts +1 -1
- package/dist/engine/cost-guardrails.js +1 -1
- package/dist/engine/criterion-identity.d.ts +1 -0
- package/dist/engine/criterion-identity.js +7 -0
- package/dist/engine/dashboard/data-loader.js +2 -7
- package/dist/engine/dashboard/templates-layout.js +0 -3
- package/dist/engine/dashboard/templates-project.js +1 -13
- package/dist/engine/docs-site-generator/data-collector.d.ts +1 -1
- package/dist/engine/docs-site-generator/data-collector.js +7 -28
- package/dist/engine/docs-site-generator/tools-renderer.js +3 -10
- package/dist/engine/evidence-gates/artifact-reader.d.ts +10 -1
- package/dist/engine/evidence-gates/artifact-reader.js +115 -2
- package/dist/engine/evidence-gates/lifecycle-gate.js +4 -3
- package/dist/engine/evidence-gates/reconciliation-freshness.d.ts +11 -0
- package/dist/engine/evidence-gates/reconciliation-freshness.js +72 -0
- package/dist/engine/evidence-index/done-drift.d.ts +1 -1
- package/dist/engine/evidence-index/done-drift.js +40 -18
- package/dist/engine/execution/context.js +3 -0
- package/dist/engine/execution/durable-job-heartbeat-runtime.d.ts +4 -0
- package/dist/engine/execution/durable-job-heartbeat-runtime.js +400 -0
- package/dist/engine/execution/job-runtime.d.ts +30 -0
- package/dist/engine/execution/job-runtime.js +114 -6
- package/dist/engine/execution/public-job-sanitizer.d.ts +4 -0
- package/dist/engine/execution/public-job-sanitizer.js +31 -0
- package/dist/engine/execution/validate-job-executor.d.ts +12 -1
- package/dist/engine/execution/validate-job-executor.js +549 -86
- package/dist/engine/execution/validate-job-payload.js +107 -50
- package/dist/engine/execution/validate-job-recovery.js +1 -1
- package/dist/engine/execution/validate-worker-runtime.d.ts +5 -0
- package/dist/engine/execution/validate-worker-runtime.js +90 -2
- package/dist/engine/execution/validate-worker-thread.js +17 -24
- package/dist/engine/handoff-artifacts/schemas.d.ts +2 -1
- package/dist/engine/handoff-artifacts/schemas.js +33 -0
- package/dist/engine/handoff-packager.d.ts +19 -0
- package/dist/engine/handoff-packager.js +415 -13
- package/dist/engine/hooks/effort-configurator.js +1 -1
- package/dist/engine/impact-detector/test-break-predictor.js +3 -3
- package/dist/engine/impact-detector/tool-registration.js +1 -1
- package/dist/engine/implementation-contract/renderer.js +23 -8
- package/dist/engine/lifecycle-reconciliation-io.d.ts +11 -0
- package/dist/engine/lifecycle-reconciliation-io.js +180 -0
- package/dist/engine/lifecycle-reconciliation.d.ts +16 -0
- package/dist/engine/lifecycle-reconciliation.js +406 -0
- package/dist/engine/local-first/tool-classification.js +2 -0
- package/dist/engine/macos-keychain-native-port.d.ts +5 -0
- package/dist/engine/macos-keychain-native-port.js +19 -7
- package/dist/engine/migrations/remove-commercial-state.d.ts +3 -0
- package/dist/engine/migrations/remove-commercial-state.js +343 -0
- package/dist/engine/network-policy.js +1 -0
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +10 -10
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +10 -10
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +10 -10
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +10 -10
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +10 -10
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +10 -10
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +10 -10
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +10 -10
- package/dist/engine/readiness-checker.d.ts +1 -16
- package/dist/engine/readiness-checker.js +27 -88
- package/dist/engine/runtime-policy.d.ts +2 -0
- package/dist/engine/runtime-policy.js +42 -5
- package/dist/engine/runtime-security/rate-limiter.js +0 -1
- package/dist/engine/safety/contained-project-file.d.ts +3 -0
- package/dist/engine/safety/contained-project-file.js +23 -0
- package/dist/engine/scope-boundaries/contradiction-checker.js +63 -1
- package/dist/engine/scope-boundaries/scope-validator.js +45 -7
- package/dist/engine/session/checkpoint-writer.js +0 -9
- package/dist/engine/spec-format/acceptance-criteria.d.ts +6 -2
- package/dist/engine/spec-format/acceptance-criteria.js +77 -18
- package/dist/engine/spec-format/markdown-sections.d.ts +14 -0
- package/dist/engine/spec-format/markdown-sections.js +105 -0
- package/dist/engine/spec-format/read-technical-section.js +33 -5
- package/dist/engine/spec-format/technical-md-populator.js +3 -2
- package/dist/engine/spec-format/unified-spec-builder.d.ts +7 -0
- package/dist/engine/spec-format/unified-spec-builder.js +202 -0
- package/dist/engine/spec-migrator/canonical-deduplicator.d.ts +18 -0
- package/dist/engine/spec-migrator/canonical-deduplicator.js +479 -0
- package/dist/engine/spec-migrator/filesystem-import.d.ts +4 -8
- package/dist/engine/spec-migrator/filesystem-import.js +121 -14
- package/dist/engine/spec-migrator/frontmatter-parser.js +32 -15
- package/dist/engine/spec-migrator/index.d.ts +1 -0
- package/dist/engine/spec-migrator/index.js +1 -0
- package/dist/engine/spec-state-machine/transition-spec.d.ts +6 -1
- package/dist/engine/spec-state-machine/transition-spec.js +10 -1
- package/dist/engine/technical-enricher/index.js +16 -8
- package/dist/engine/technical-enricher/render-enriched.d.ts +1 -0
- package/dist/engine/technical-enricher/render-enriched.js +42 -14
- package/dist/engine/telemetry/health-checker.js +53 -17
- package/dist/engine/telemetry/telemetry-client.js +26 -8
- package/dist/engine/text-signal-boundaries.d.ts +2 -0
- package/dist/engine/text-signal-boundaries.js +25 -2
- package/dist/engine/time/relative-date.d.ts +3 -0
- package/dist/engine/time/relative-date.js +16 -0
- package/dist/engine/timing/budget.d.ts +11 -0
- package/dist/engine/timing/budget.js +27 -9
- package/dist/engine/token-optimizer/analytics.d.ts +1 -1
- package/dist/engine/token-optimizer/analytics.js +5 -6
- package/dist/engine/usage-tracker/core.d.ts +2 -2
- package/dist/engine/usage-tracker/core.js +1 -2
- package/dist/engine/usage-tracker/index.d.ts +0 -3
- package/dist/engine/usage-tracker/index.js +0 -3
- package/dist/engine/usage-tracker/stats.d.ts +2 -2
- package/dist/engine/usage-tracker/stats.js +1 -28
- package/dist/engine/validation/durable-validation.d.ts +1 -1
- package/dist/engine/validation/durable-validation.js +3 -82
- package/dist/engine/validation/validation-freshness.d.ts +11 -1
- package/dist/engine/validation/validation-freshness.js +231 -91
- package/dist/engine/validation/validation-receipt.d.ts +9 -6
- package/dist/engine/validation/validation-receipt.js +84 -24
- package/dist/engine/validation/validation-source-digest.d.ts +2 -0
- package/dist/engine/validation/validation-source-digest.js +86 -0
- package/dist/engine/validation/validation-submission.d.ts +5 -4
- package/dist/engine/validation/validation-submission.js +25 -11
- package/dist/engine/validation/validation-worktree.d.ts +21 -0
- package/dist/engine/validation/validation-worktree.js +260 -0
- package/dist/engine/validation-impact-planner.d.ts +14 -2
- package/dist/engine/validation-impact-planner.js +0 -3
- package/dist/engine/validator/dor-dod.d.ts +1 -0
- package/dist/engine/validator/dor-dod.js +5 -3
- package/dist/engine/validator/extractors.d.ts +1 -5
- package/dist/engine/validator/extractors.js +24 -69
- package/dist/engine/validator/spec-compliance-runner.d.ts +12 -24
- package/dist/engine/validator/spec-compliance-runner.js +429 -211
- package/dist/engine/validator.js +35 -22
- package/dist/index.js +4 -16
- package/dist/resources/usage-stats-resource.js +2 -18
- package/dist/security/adapters/linux-secret-service.js +7 -3
- package/dist/security/adapters/windows-credential-manager.js +6 -4
- package/dist/server/routes/specs.js +10 -0
- package/dist/storage/base-store.d.ts +1 -1
- package/dist/storage/base-store.js +1 -1
- package/dist/storage/feedback-remote.d.ts +2 -3
- package/dist/storage/feedback-remote.js +45 -31
- package/dist/storage/global-store.d.ts +1 -17
- package/dist/storage/global-store.js +1 -42
- package/dist/storage/index.d.ts +0 -2
- package/dist/storage/index.js +0 -2
- package/dist/storage/migrations/canonical-storage.js +124 -11
- package/dist/storage/runtime-db.d.ts +9 -0
- package/dist/storage/runtime-db.js +64 -8
- package/dist/storage/spec-store.d.ts +14 -2
- package/dist/storage/spec-store.js +104 -14
- package/dist/storage/status-store/atomic-status-write.d.ts +5 -0
- package/dist/storage/status-store/atomic-status-write.js +57 -0
- package/dist/storage/status-store/file-lock.js +30 -11
- package/dist/storage/status-store/self-healing.js +3 -3
- package/dist/storage/status-store/version-sync.d.ts +2 -1
- package/dist/storage/status-store/version-sync.js +35 -40
- package/dist/storage/usage-store.d.ts +1 -5
- package/dist/storage/usage-store.js +0 -35
- package/dist/tools/auto-update-handler.d.ts +3 -3
- package/dist/tools/auto-update-handler.js +3 -3
- package/dist/tools/challenge-spec/resilience-challenge-scenarios.d.ts +2 -1
- package/dist/tools/challenge-spec/resilience-challenge-scenarios.js +10 -7
- package/dist/tools/challenge-spec/scenario-collector.js +1 -1
- package/dist/tools/challenge-spec/scenarios-data.js +1 -1
- package/dist/tools/challenge-spec/scenarios-utils.d.ts +1 -1
- package/dist/tools/challenge-spec/scenarios-utils.js +35 -11
- package/dist/tools/challenge-spec.js +5 -5
- package/dist/tools/check-readiness.js +1 -1
- package/dist/tools/configure-telemetry.js +30 -2
- package/dist/tools/create-spec/spec-builder.js +0 -11
- package/dist/tools/create-spec.d.ts +1 -0
- package/dist/tools/create-spec.js +444 -239
- package/dist/tools/git/branch-ops.d.ts +1 -1
- package/dist/tools/git/branch-ops.js +3 -2
- package/dist/tools/init-project/handler.js +10 -12
- package/dist/tools/init-project/host-assets-writer.js +26 -1
- package/dist/tools/init-project/lint-scaffolder.d.ts +3 -3
- package/dist/tools/init-project/migration-runner.d.ts +2 -4
- package/dist/tools/init-project/migration-runner.js +3 -2
- package/dist/tools/init-project/portable-index-reconciler.d.ts +4 -0
- package/dist/tools/init-project/portable-index-reconciler.js +14 -0
- package/dist/tools/init-project/result-builder.js +1 -3
- package/dist/tools/jobs/handlers.js +38 -6
- package/dist/tools/list-specs.js +5 -26
- package/dist/tools/merge-risk-handler.js +2 -2
- package/dist/tools/package-handoff.js +2 -0
- package/dist/tools/reconcile-spec.js +0 -27
- package/dist/tools/register-agent-squad-tools.js +2 -2
- package/dist/tools/register-platform-tools/design-stack-tools.js +11 -11
- package/dist/tools/register-platform-tools/lifecycle-infra-tools.js +7 -7
- package/dist/tools/register-sdd-tools.d.ts +1 -1
- package/dist/tools/register-sdd-tools.js +2 -29
- package/dist/tools/register-spec-tools/analysis-tools.js +8 -8
- package/dist/tools/register-spec-tools/core-spec-tools.js +17 -8
- package/dist/tools/safe-handler.d.ts +5 -13
- package/dist/tools/safe-handler.js +7 -23
- package/dist/tools/schemas/index.d.ts +1 -1
- package/dist/tools/schemas/index.js +1 -1
- package/dist/tools/schemas/output-schemas.d.ts +0 -23
- package/dist/tools/schemas/output-schemas.js +0 -28
- package/dist/tools/schemas/spec.d.ts +4 -0
- package/dist/tools/schemas/spec.js +3 -0
- package/dist/tools/schemas/validate-output-schema.d.ts +11 -0
- package/dist/tools/schemas/validate-output-schema.js +7 -0
- package/dist/tools/spec-prompt-handler.js +3 -3
- package/dist/tools/sync-spec-state-handler.d.ts +1 -1
- package/dist/tools/sync-spec-state-handler.js +1 -1
- package/dist/tools/tool-entry.d.ts +4 -4
- package/dist/tools/tool-entry.js +3 -3
- package/dist/tools/tool-registry/core-tools.js +31 -15
- package/dist/tools/tool-registry/group-infra.js +27 -110
- package/dist/tools/tool-registry/group-quality-compliance.js +26 -26
- package/dist/tools/tool-registry/inventory.d.ts +2 -0
- package/dist/tools/tool-registry/inventory.js +2 -0
- package/dist/tools/tool-registry-helpers.d.ts +3 -3
- package/dist/tools/tool-registry-helpers.js +3 -3
- package/dist/tools/tool-usage-report/registered-tools.d.ts +1 -1
- package/dist/tools/tool-usage-report/registered-tools.js +3 -15
- package/dist/tools/update-status/approval-gates.js +3 -0
- package/dist/tools/update-status/done-receipt-verifier.js +40 -27
- package/dist/tools/update-status/evidence-gate.js +36 -1
- package/dist/tools/update-status/file-sync.d.ts +2 -1
- package/dist/tools/update-status/file-sync.js +57 -6
- package/dist/tools/update-status/index.d.ts +2 -0
- package/dist/tools/update-status/index.js +95 -58
- package/dist/tools/update-status/response-builder.js +3 -3
- package/dist/tools/update-status/side-effects.d.ts +2 -0
- package/dist/tools/update-status/side-effects.js +2 -2
- package/dist/tools/update-status/transition-guard.js +32 -15
- package/dist/tools/update-status-actions.d.ts +4 -2
- package/dist/tools/update-status-actions.js +16 -14
- package/dist/tools/usage-report.js +1 -16
- package/dist/tools/usage-stats.js +1 -18
- package/dist/tools/usage-tracking.d.ts +4 -0
- package/dist/tools/usage-tracking.js +20 -0
- package/dist/tools/validate-lint.d.ts +1 -0
- package/dist/tools/validate-lint.js +1 -1
- package/dist/tools/validate.d.ts +2 -1
- package/dist/tools/validate.js +114 -34
- package/dist/transports/http-transport.js +6 -8
- package/dist/transports/mcp-types.d.ts +0 -2
- package/dist/transports/session-context.js +1 -1
- package/dist/types/cascade-hooks.d.ts +1 -1
- package/dist/types/ci.d.ts +0 -4
- package/dist/types/clarification-token.d.ts +1 -1
- package/dist/types/claude-code-runtime.d.ts +0 -27
- package/dist/types/commercial-migration.d.ts +17 -0
- package/dist/types/commercial-migration.js +2 -0
- package/dist/types/common/primitives.d.ts +0 -2
- package/dist/types/core-bridge.d.ts +4 -10
- package/dist/types/dashboard.d.ts +3 -7
- package/dist/types/docs.d.ts +0 -1
- package/dist/types/durable-job.d.ts +4 -4
- package/dist/types/durable-validation.d.ts +100 -2
- package/dist/types/durable-validation.js +165 -1
- package/dist/types/errors/structured-error.d.ts +3 -3
- package/dist/types/estimation.d.ts +2 -0
- package/dist/types/evidence-gates.d.ts +9 -1
- package/dist/types/execution.d.ts +2 -0
- package/dist/types/gemini-integration.d.ts +0 -11
- package/dist/types/handoff-artifacts.d.ts +41 -0
- package/dist/types/hooks-advanced.d.ts +0 -13
- package/dist/types/impact-detection.d.ts +2 -2
- package/dist/types/index.d.ts +2 -6
- package/dist/types/index.js +1 -5
- package/dist/types/kiro-integration.d.ts +0 -6
- package/dist/types/network-policy.d.ts +1 -1
- package/dist/types/project/lint-scaffolder.d.ts +1 -1
- package/dist/types/runtime-policy.d.ts +11 -0
- package/dist/types/session-safeguard.d.ts +0 -31
- package/dist/types/spec/canonical-repair.d.ts +81 -0
- package/dist/types/spec/canonical-repair.js +2 -0
- package/dist/types/spec/core.d.ts +14 -0
- package/dist/types/spec/index.d.ts +1 -0
- package/dist/types/spec/index.js +1 -0
- package/dist/types/spec/inputs.d.ts +29 -0
- package/dist/types/spec-format.d.ts +24 -0
- package/dist/types/telemetry.d.ts +4 -1
- package/dist/types/transport.d.ts +0 -2
- package/dist/types/usage.d.ts +1 -36
- package/dist/types/usage.js +1 -1
- package/dist/types/validate-worker.d.ts +6 -0
- package/dist/types/validation-receipt.d.ts +1 -1
- package/dist/types/validation.d.ts +2 -2
- package/package.json +16 -12
- package/planu-native.json +1 -1
- package/planu-plugin.json +23 -40
- package/scripts/lib/pending-release-file.mjs +93 -0
- package/src/i18n/messages/en.json +0 -41
- package/src/i18n/messages/es.json +0 -41
- package/src/i18n/messages/pt.json +0 -41
- package/dist/cli/commands/activate.d.ts +0 -14
- package/dist/cli/commands/activate.js +0 -174
- package/dist/config/license-plans.json +0 -251
- package/dist/config/product-public.ts +0 -148
- package/dist/config/server-instructions.ts +0 -121
- package/dist/config/version-defaults.ts +0 -76
- package/dist/config/version.ts +0 -21
- package/dist/engine/ai-integration/gemini/settings-generator.d.ts +0 -4
- package/dist/engine/ai-integration/gemini/settings-generator.js +0 -195
- package/dist/engine/ai-integration/kiro/hooks-generator.d.ts +0 -6
- package/dist/engine/ai-integration/kiro/hooks-generator.js +0 -75
- package/dist/engine/hooks/full-spectrum-generator.d.ts +0 -32
- package/dist/engine/hooks/full-spectrum-generator.js +0 -117
- package/dist/engine/license-validator/config-loader.d.ts +0 -5
- package/dist/engine/license-validator/config-loader.js +0 -16
- package/dist/engine/license-validator/core.d.ts +0 -15
- package/dist/engine/license-validator/core.js +0 -151
- package/dist/engine/license-validator/fingerprint.d.ts +0 -3
- package/dist/engine/license-validator/fingerprint.js +0 -17
- package/dist/engine/license-validator/lemon-squeezy.d.ts +0 -5
- package/dist/engine/license-validator/lemon-squeezy.js +0 -55
- package/dist/engine/license-validator.d.ts +0 -5
- package/dist/engine/license-validator.js +0 -6
- package/dist/engine/product-intelligence/index.d.ts +0 -7
- package/dist/engine/product-intelligence/index.js +0 -211
- package/dist/engine/session/auto-save.d.ts +0 -18
- package/dist/engine/session/auto-save.js +0 -98
- package/dist/engine/session/context-snapshot.d.ts +0 -8
- package/dist/engine/session/context-snapshot.js +0 -49
- package/dist/engine/session/memory-sync.d.ts +0 -12
- package/dist/engine/session/memory-sync.js +0 -34
- package/dist/engine/session/session-diff.d.ts +0 -11
- package/dist/engine/session/session-diff.js +0 -42
- package/dist/engine/session/session-merge.d.ts +0 -12
- package/dist/engine/session/session-merge.js +0 -55
- package/dist/engine/session-safeguard/checkpoint-runner.d.ts +0 -4
- package/dist/engine/session-safeguard/checkpoint-runner.js +0 -70
- package/dist/engine/session-safeguard/learnings-buffer.d.ts +0 -15
- package/dist/engine/session-safeguard/learnings-buffer.js +0 -91
- package/dist/engine/session-safeguard/precompaction-drain.d.ts +0 -10
- package/dist/engine/session-safeguard/precompaction-drain.js +0 -82
- package/dist/engine/session-safeguard/unpushed-detector.d.ts +0 -7
- package/dist/engine/session-safeguard/unpushed-detector.js +0 -59
- package/dist/engine/spec-changelog/core.d.ts +0 -16
- package/dist/engine/spec-changelog/core.js +0 -176
- package/dist/engine/spec-changelog/diff.d.ts +0 -18
- package/dist/engine/spec-changelog/diff.js +0 -121
- package/dist/engine/spec-changelog/index.d.ts +0 -3
- package/dist/engine/spec-changelog/index.js +0 -4
- package/dist/engine/trial-engine.d.ts +0 -5
- package/dist/engine/trial-engine.js +0 -14
- package/dist/engine/usage-tracker/rate-check.d.ts +0 -7
- package/dist/engine/usage-tracker/rate-check.js +0 -18
- package/dist/engine/usage-tracker/rate-limiter.d.ts +0 -14
- package/dist/engine/usage-tracker/rate-limiter.js +0 -73
- package/dist/engine/usage-tracker/trial.d.ts +0 -27
- package/dist/engine/usage-tracker/trial.js +0 -49
- package/dist/engine/webhook/event-handlers.d.ts +0 -25
- package/dist/engine/webhook/event-handlers.js +0 -89
- package/dist/engine/webhook/index.d.ts +0 -4
- package/dist/engine/webhook/index.js +0 -5
- package/dist/engine/webhook/server.d.ts +0 -12
- package/dist/engine/webhook/server.js +0 -249
- package/dist/engine/webhook/signature.d.ts +0 -16
- package/dist/engine/webhook/signature.js +0 -49
- package/dist/storage/license-store.d.ts +0 -6
- package/dist/storage/license-store.js +0 -63
- package/dist/storage/session-store.d.ts +0 -20
- package/dist/storage/session-store.js +0 -136
- package/dist/tools/activate-license.d.ts +0 -6
- package/dist/tools/activate-license.js +0 -132
- package/dist/tools/checkpoint-handler.d.ts +0 -3
- package/dist/tools/checkpoint-handler.js +0 -130
- package/dist/tools/export-session.d.ts +0 -3
- package/dist/tools/export-session.js +0 -90
- package/dist/tools/license-gate.d.ts +0 -18
- package/dist/tools/license-gate.js +0 -222
- package/dist/tools/license-status.d.ts +0 -3
- package/dist/tools/license-status.js +0 -93
- package/dist/tools/list-sessions.d.ts +0 -3
- package/dist/tools/list-sessions.js +0 -46
- package/dist/tools/planu-session-checkpoint.d.ts +0 -6
- package/dist/tools/planu-session-checkpoint.js +0 -42
- package/dist/tools/restore-session.d.ts +0 -3
- package/dist/tools/restore-session.js +0 -71
- package/dist/tools/spec-history.d.ts +0 -3
- package/dist/tools/spec-history.js +0 -59
- package/dist/tools/webhook.d.ts +0 -6
- package/dist/tools/webhook.js +0 -150
- package/dist/types/changelog.d.ts +0 -49
- package/dist/types/changelog.js +0 -3
- package/dist/types/licensing.d.ts +0 -109
- package/dist/types/licensing.js +0 -4
- package/dist/types/product-intelligence.d.ts +0 -60
- package/dist/types/product-intelligence.js +0 -3
- package/dist/types/session.d.ts +0 -84
- package/dist/types/session.js +0 -3
- package/dist/types/webhook.d.ts +0 -62
- package/dist/types/webhook.js +0 -4
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/* eslint-disable max-lines, max-lines-per-function, complexity -- one audited administrative transaction */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdir as mkdirFs, readFile as readFileFs } from 'node:fs/promises';
|
|
4
|
+
import { join, relative, resolve } from 'node:path';
|
|
5
|
+
import { atomicWriteFile } from '../safety/atomic-write-file.js';
|
|
6
|
+
import { projectDataDir } from '../../storage/base-store.js';
|
|
7
|
+
import { invalidateSpecsCache, validateCanonicalSpecRows, withCanonicalSpecRepairLock, } from '../../storage/spec-store.js';
|
|
8
|
+
function digest(value) {
|
|
9
|
+
return createHash('sha256').update(value).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
function canonicalJson(value) {
|
|
12
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
13
|
+
}
|
|
14
|
+
function privateFailure(message) {
|
|
15
|
+
return new Error(`[Planu] Canonical repair blocked: ${message}`);
|
|
16
|
+
}
|
|
17
|
+
function repairPaths(projectId) {
|
|
18
|
+
const root = projectDataDir(projectId);
|
|
19
|
+
return { store: join(root, 'specs.json'), dir: join(root, 'repairs', 'spec-id-dedup') };
|
|
20
|
+
}
|
|
21
|
+
function isContained(parent, child) {
|
|
22
|
+
const rel = relative(resolve(parent), resolve(child));
|
|
23
|
+
return rel !== '' && !rel.startsWith('..') && !rel.includes('/../');
|
|
24
|
+
}
|
|
25
|
+
function artifactPath(projectId, artifactId) {
|
|
26
|
+
const { dir } = repairPaths(projectId);
|
|
27
|
+
if (!artifactId.startsWith('repairs/spec-id-dedup/')) {
|
|
28
|
+
throw privateFailure('repair artifact is outside the project repair directory');
|
|
29
|
+
}
|
|
30
|
+
const candidate = resolve(projectDataDir(projectId), artifactId);
|
|
31
|
+
if (!isContained(dir, candidate)) {
|
|
32
|
+
throw privateFailure('repair artifact is outside the project repair directory');
|
|
33
|
+
}
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
function parseRawStore(raw) {
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(raw.toString('utf8'));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw privateFailure('store is not valid JSON');
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(parsed)) {
|
|
45
|
+
return { envelope: false, rows: parsed };
|
|
46
|
+
}
|
|
47
|
+
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.data)) {
|
|
48
|
+
return { envelope: true, rows: parsed.data };
|
|
49
|
+
}
|
|
50
|
+
throw privateFailure('store shape is invalid');
|
|
51
|
+
}
|
|
52
|
+
function serializeStore(store, rows) {
|
|
53
|
+
return Buffer.from(canonicalJson(store.envelope ? { schemaVersion: 1, data: rows } : rows), 'utf8');
|
|
54
|
+
}
|
|
55
|
+
function requireString(value, label) {
|
|
56
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
57
|
+
throw privateFailure(`${label} is missing`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function validateManifest(manifest, requestedProjectId) {
|
|
62
|
+
if (manifest.projectId !== requestedProjectId) {
|
|
63
|
+
throw privateFailure('manifest project does not match requested project');
|
|
64
|
+
}
|
|
65
|
+
if (!/^[a-f0-9]{64}$/.test(manifest.sourceDigest)) {
|
|
66
|
+
throw privateFailure('manifest source digest is invalid');
|
|
67
|
+
}
|
|
68
|
+
if (!manifest.sourceProjectAliases.includes(manifest.canonicalSourceProjectAlias) ||
|
|
69
|
+
manifest.canonicalSourceProjectAlias.length === 0) {
|
|
70
|
+
throw privateFailure('canonical source alias is not declared');
|
|
71
|
+
}
|
|
72
|
+
if (new Set(manifest.foreignOwnerSingletonIds).size !== manifest.foreignOwnerSingletonIds.length) {
|
|
73
|
+
throw privateFailure('foreign singleton IDs are duplicated');
|
|
74
|
+
}
|
|
75
|
+
for (const override of Object.values(manifest.lifecycleOverrides ?? {})) {
|
|
76
|
+
if (override.reason.length < 100 || !override.canonicalUuid) {
|
|
77
|
+
throw privateFailure('lifecycle override is incomplete');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function identityFieldsAgree(rows) {
|
|
82
|
+
const first = rows[0];
|
|
83
|
+
return (first !== undefined &&
|
|
84
|
+
rows.every((row) => row.id === first.id &&
|
|
85
|
+
row.title === first.title &&
|
|
86
|
+
row.slug === first.slug &&
|
|
87
|
+
row.specPath === first.specPath));
|
|
88
|
+
}
|
|
89
|
+
function validateRawUuidIdentities(rows) {
|
|
90
|
+
const identities = new Set();
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
const primary = requireString(row.uuid, 'primary UUID');
|
|
93
|
+
for (const identity of [primary, ...(row.uuidAliases ?? [])]) {
|
|
94
|
+
const normalized = requireString(identity, 'UUID alias');
|
|
95
|
+
if (identities.has(normalized)) {
|
|
96
|
+
throw privateFailure('raw UUID identity is repeated or ambiguous');
|
|
97
|
+
}
|
|
98
|
+
identities.add(normalized);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function planPayload(plan) {
|
|
103
|
+
return {
|
|
104
|
+
changed: plan.changed,
|
|
105
|
+
projectId: plan.projectId,
|
|
106
|
+
sourceDigest: plan.sourceDigest,
|
|
107
|
+
manifestDigest: plan.manifestDigest,
|
|
108
|
+
resultDigest: plan.resultDigest,
|
|
109
|
+
duplicateGroups: plan.duplicateGroups,
|
|
110
|
+
foreignOwnerSingletons: plan.foreignOwnerSingletons,
|
|
111
|
+
fieldChanges: plan.fieldChanges,
|
|
112
|
+
artifactId: plan.artifactId,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Pure planner. It does not read or write the canonical store. */
|
|
116
|
+
export function buildCanonicalRepairPlan(raw, manifest, manifestDigest = digest(canonicalJson(manifest)), requestedProjectId = manifest.projectId) {
|
|
117
|
+
validateManifest(manifest, requestedProjectId);
|
|
118
|
+
const sourceDigest = digest(raw);
|
|
119
|
+
if (sourceDigest !== manifest.sourceDigest) {
|
|
120
|
+
const repaired = parseRawStore(raw);
|
|
121
|
+
try {
|
|
122
|
+
validateCanonicalSpecRows(requestedProjectId, repaired.rows);
|
|
123
|
+
const unchanged = {
|
|
124
|
+
changed: false,
|
|
125
|
+
projectId: requestedProjectId,
|
|
126
|
+
sourceDigest,
|
|
127
|
+
manifestDigest,
|
|
128
|
+
resultDigest: sourceDigest,
|
|
129
|
+
duplicateGroups: 0,
|
|
130
|
+
foreignOwnerSingletons: 0,
|
|
131
|
+
fieldChanges: [],
|
|
132
|
+
artifactId: `repairs/spec-id-dedup/${sourceDigest}.dry-run.json`,
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
...unchanged,
|
|
136
|
+
planDigest: digest(canonicalJson(planPayload(unchanged))),
|
|
137
|
+
resultBytes: raw,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
throw privateFailure('source digest does not match manifest');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const store = parseRawStore(raw);
|
|
145
|
+
validateRawUuidIdentities(store.rows);
|
|
146
|
+
const groups = new Map();
|
|
147
|
+
for (const row of store.rows) {
|
|
148
|
+
const id = requireString(row.id, 'spec ID');
|
|
149
|
+
const group = groups.get(id);
|
|
150
|
+
if (group) {
|
|
151
|
+
group.push(row);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
groups.set(id, [row]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const fieldChanges = [];
|
|
158
|
+
const nextRows = [];
|
|
159
|
+
const listedSingletons = new Set(manifest.foreignOwnerSingletonIds);
|
|
160
|
+
let duplicates = 0;
|
|
161
|
+
let singletonChanges = 0;
|
|
162
|
+
for (const [id, rows] of groups) {
|
|
163
|
+
const row = rows[0];
|
|
164
|
+
if (!row) {
|
|
165
|
+
throw privateFailure('empty duplicate group');
|
|
166
|
+
}
|
|
167
|
+
if (rows.length === 1) {
|
|
168
|
+
if (row.projectId === requestedProjectId) {
|
|
169
|
+
nextRows.push({ ...row });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!listedSingletons.delete(id) || !manifest.sourceProjectAliases.includes(row.projectId)) {
|
|
173
|
+
throw privateFailure('foreign singleton is not explicitly authorized');
|
|
174
|
+
}
|
|
175
|
+
nextRows.push({ ...row, projectId: requestedProjectId });
|
|
176
|
+
singletonChanges += 1;
|
|
177
|
+
fieldChanges.push({ id, field: 'projectId', from: row.projectId, to: requestedProjectId });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
duplicates += 1;
|
|
181
|
+
if (!identityFieldsAgree(rows)) {
|
|
182
|
+
throw privateFailure('duplicate group has incompatible identity fields');
|
|
183
|
+
}
|
|
184
|
+
const canonicalRows = rows.filter((candidate) => candidate.projectId === manifest.canonicalSourceProjectAlias);
|
|
185
|
+
if (canonicalRows.length !== 1) {
|
|
186
|
+
throw privateFailure('duplicate group must have exactly one canonical-source row');
|
|
187
|
+
}
|
|
188
|
+
const canonical = canonicalRows[0];
|
|
189
|
+
if (!canonical) {
|
|
190
|
+
throw privateFailure('canonical-source row is missing');
|
|
191
|
+
}
|
|
192
|
+
const primaryUuid = requireString(canonical.uuid, 'canonical UUID');
|
|
193
|
+
const statuses = new Set(rows.map((candidate) => candidate.status));
|
|
194
|
+
const override = manifest.lifecycleOverrides?.[id];
|
|
195
|
+
if (statuses.size > 1) {
|
|
196
|
+
if (!override) {
|
|
197
|
+
throw privateFailure('lifecycle disagreement has no valid override');
|
|
198
|
+
}
|
|
199
|
+
if (override.canonicalUuid !== primaryUuid ||
|
|
200
|
+
override.retainedStatus !== canonical.status ||
|
|
201
|
+
override.reason.length < 100) {
|
|
202
|
+
throw privateFailure('lifecycle disagreement has no valid override');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const aliases = new Set(canonical.uuidAliases ?? []);
|
|
206
|
+
for (const candidate of rows) {
|
|
207
|
+
const candidateUuid = requireString(candidate.uuid, 'duplicate UUID');
|
|
208
|
+
if (candidate !== canonical) {
|
|
209
|
+
if (!manifest.sourceProjectAliases.includes(candidate.projectId)) {
|
|
210
|
+
throw privateFailure('duplicate row has an unknown source project alias');
|
|
211
|
+
}
|
|
212
|
+
aliases.add(candidateUuid);
|
|
213
|
+
for (const alias of candidate.uuidAliases ?? []) {
|
|
214
|
+
aliases.add(alias);
|
|
215
|
+
}
|
|
216
|
+
for (const field of ['technicalPath', 'progressPath']) {
|
|
217
|
+
if (candidate[field] !== canonical[field]) {
|
|
218
|
+
fieldChanges.push({
|
|
219
|
+
id,
|
|
220
|
+
field,
|
|
221
|
+
from: candidate[field] ?? null,
|
|
222
|
+
to: canonical[field] ?? null,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
aliases.delete(primaryUuid);
|
|
229
|
+
nextRows.push({
|
|
230
|
+
...canonical,
|
|
231
|
+
projectId: requestedProjectId,
|
|
232
|
+
...(aliases.size > 0
|
|
233
|
+
? { uuidAliases: [...aliases].sort((left, right) => left.localeCompare(right)) }
|
|
234
|
+
: {}),
|
|
235
|
+
});
|
|
236
|
+
fieldChanges.push({
|
|
237
|
+
id,
|
|
238
|
+
field: 'projectId',
|
|
239
|
+
from: canonical.projectId,
|
|
240
|
+
to: requestedProjectId,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (listedSingletons.size > 0) {
|
|
244
|
+
throw privateFailure('manifest lists a missing foreign singleton');
|
|
245
|
+
}
|
|
246
|
+
validateCanonicalSpecRows(requestedProjectId, nextRows);
|
|
247
|
+
const resultBytes = serializeStore(store, nextRows);
|
|
248
|
+
const base = {
|
|
249
|
+
changed: duplicates > 0 || singletonChanges > 0,
|
|
250
|
+
projectId: requestedProjectId,
|
|
251
|
+
sourceDigest,
|
|
252
|
+
manifestDigest,
|
|
253
|
+
resultDigest: digest(resultBytes),
|
|
254
|
+
duplicateGroups: duplicates,
|
|
255
|
+
foreignOwnerSingletons: singletonChanges,
|
|
256
|
+
fieldChanges,
|
|
257
|
+
artifactId: `repairs/spec-id-dedup/${sourceDigest}.dry-run.json`,
|
|
258
|
+
};
|
|
259
|
+
return { ...base, planDigest: digest(canonicalJson(planPayload(base))), resultBytes };
|
|
260
|
+
}
|
|
261
|
+
function toPrivateReport(plan) {
|
|
262
|
+
return {
|
|
263
|
+
schemaVersion: 1,
|
|
264
|
+
...planPayload(plan),
|
|
265
|
+
planDigest: plan.planDigest,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function buildReceipt(plan, raw, review, authorizationReason, manifest) {
|
|
269
|
+
const source = parseRawStore(raw).rows;
|
|
270
|
+
const groups = new Map();
|
|
271
|
+
for (const row of source) {
|
|
272
|
+
const group = groups.get(row.id);
|
|
273
|
+
if (group) {
|
|
274
|
+
group.push(row);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
groups.set(row.id, [row]);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const removedRows = [];
|
|
281
|
+
const singletonOwnershipChanges = [];
|
|
282
|
+
const retainedUuids = [];
|
|
283
|
+
const aliasesAdded = [];
|
|
284
|
+
for (const [id, rows] of groups) {
|
|
285
|
+
if (rows.length === 1) {
|
|
286
|
+
const only = rows[0];
|
|
287
|
+
if (!only) {
|
|
288
|
+
throw privateFailure('singleton row is missing');
|
|
289
|
+
}
|
|
290
|
+
if (only.projectId !== manifest.projectId) {
|
|
291
|
+
singletonOwnershipChanges.push({
|
|
292
|
+
id,
|
|
293
|
+
beforeProjectId: only.projectId,
|
|
294
|
+
afterProjectId: manifest.projectId,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const canonical = rows.find((row) => row.projectId === manifest.canonicalSourceProjectAlias);
|
|
300
|
+
if (!canonical) {
|
|
301
|
+
throw privateFailure('canonical-source row is missing');
|
|
302
|
+
}
|
|
303
|
+
retainedUuids.push({ id, uuid: canonical.uuid });
|
|
304
|
+
const aliases = rows
|
|
305
|
+
.filter((row) => row !== canonical)
|
|
306
|
+
.flatMap((row) => [row.uuid, ...(row.uuidAliases ?? [])])
|
|
307
|
+
.filter((uuid) => typeof uuid === 'string' && uuid.length > 0)
|
|
308
|
+
.sort((left, right) => left.localeCompare(right));
|
|
309
|
+
aliasesAdded.push({ id, aliases });
|
|
310
|
+
removedRows.push(...rows.filter((row) => row !== canonical));
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
schemaVersion: 1,
|
|
314
|
+
state: 'prepared',
|
|
315
|
+
projectId: manifest.projectId,
|
|
316
|
+
reviewer: review.reviewer,
|
|
317
|
+
verdict: review.verdict,
|
|
318
|
+
authorizationReason,
|
|
319
|
+
sourceDigest: plan.sourceDigest,
|
|
320
|
+
manifestDigest: plan.manifestDigest,
|
|
321
|
+
planDigest: plan.planDigest,
|
|
322
|
+
resultDigest: plan.resultDigest,
|
|
323
|
+
backupArtifactId: `repairs/spec-id-dedup/${plan.sourceDigest}.backup.json`,
|
|
324
|
+
backupDigest: digest(raw),
|
|
325
|
+
removedRows,
|
|
326
|
+
singletonOwnershipChanges,
|
|
327
|
+
retainedUuids,
|
|
328
|
+
aliasesAdded,
|
|
329
|
+
fieldChanges: plan.fieldChanges,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function validateApplyInput(plan, input, review) {
|
|
333
|
+
const authorizationReason = input.authorizationReason;
|
|
334
|
+
if (authorizationReason === undefined || authorizationReason.length < 100) {
|
|
335
|
+
throw privateFailure('authorization reason is too short');
|
|
336
|
+
}
|
|
337
|
+
if (input.expectedSourceDigest !== plan.sourceDigest ||
|
|
338
|
+
input.expectedManifestDigest !== plan.manifestDigest ||
|
|
339
|
+
input.expectedPlanDigest !== plan.planDigest ||
|
|
340
|
+
input.expectedResultDigest !== plan.resultDigest) {
|
|
341
|
+
throw privateFailure('reviewed digest does not match current plan');
|
|
342
|
+
}
|
|
343
|
+
if (review.reviewer !== 'planu-data-repair-reviewer' ||
|
|
344
|
+
review.verdict !== 'approved' ||
|
|
345
|
+
review.sourceDigest !== plan.sourceDigest ||
|
|
346
|
+
review.manifestDigest !== plan.manifestDigest ||
|
|
347
|
+
review.planDigest !== plan.planDigest ||
|
|
348
|
+
review.resultDigest !== plan.resultDigest) {
|
|
349
|
+
throw privateFailure('independent review evidence is invalid');
|
|
350
|
+
}
|
|
351
|
+
return authorizationReason;
|
|
352
|
+
}
|
|
353
|
+
function ioFor(io) {
|
|
354
|
+
return {
|
|
355
|
+
readFile: io?.readFile ?? ((path) => readFileFs(path)),
|
|
356
|
+
writeAtomic: io?.writeAtomic ?? ((path, bytes) => atomicWriteFile(path, bytes, { mode: 0o600 })),
|
|
357
|
+
mkdir: io?.mkdir ??
|
|
358
|
+
(async (path) => {
|
|
359
|
+
await mkdirFs(path, { recursive: true });
|
|
360
|
+
}),
|
|
361
|
+
withLock: io?.withLock ?? withCanonicalSpecRepairLock,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
async function readJsonArtifact(path, label, io) {
|
|
365
|
+
try {
|
|
366
|
+
return JSON.parse((await io.readFile(path)).toString('utf8'));
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
throw privateFailure(`${label} is unreadable`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** Runs dry-run or the explicitly authorized, lock-protected repair. */
|
|
373
|
+
export async function repairDuplicateSpecIds(input) {
|
|
374
|
+
const io = ioFor(input.io);
|
|
375
|
+
const manifestBytes = await io.readFile(input.manifestPath).catch(() => {
|
|
376
|
+
throw privateFailure('manifest is unreadable');
|
|
377
|
+
});
|
|
378
|
+
let manifest;
|
|
379
|
+
try {
|
|
380
|
+
manifest = JSON.parse(manifestBytes.toString('utf8'));
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
throw privateFailure('manifest is not valid JSON');
|
|
384
|
+
}
|
|
385
|
+
// This binding intentionally precedes every canonical store read and write.
|
|
386
|
+
validateManifest(manifest, input.projectId);
|
|
387
|
+
const manifestDigest = digest(manifestBytes);
|
|
388
|
+
const paths = repairPaths(input.projectId);
|
|
389
|
+
if (input.dryRun === true) {
|
|
390
|
+
const raw = await io.readFile(paths.store);
|
|
391
|
+
const plan = buildCanonicalRepairPlan(raw, manifest, manifestDigest, input.projectId);
|
|
392
|
+
await io.mkdir(paths.dir);
|
|
393
|
+
await io.writeAtomic(artifactPath(input.projectId, plan.artifactId), canonicalJson(toPrivateReport(plan)));
|
|
394
|
+
return plan;
|
|
395
|
+
}
|
|
396
|
+
if (!input.reviewPath) {
|
|
397
|
+
throw privateFailure('apply requires review evidence');
|
|
398
|
+
}
|
|
399
|
+
const review = await readJsonArtifact(input.reviewPath, 'review evidence', io);
|
|
400
|
+
return io.withLock(input.projectId, async () => {
|
|
401
|
+
const raw = await io.readFile(paths.store);
|
|
402
|
+
const plan = buildCanonicalRepairPlan(raw, manifest, manifestDigest, input.projectId);
|
|
403
|
+
if (!plan.changed) {
|
|
404
|
+
return plan;
|
|
405
|
+
}
|
|
406
|
+
const authorizationReason = validateApplyInput(plan, input, review);
|
|
407
|
+
const receipt = buildReceipt(plan, raw, review, authorizationReason, manifest);
|
|
408
|
+
const backupPath = artifactPath(input.projectId, receipt.backupArtifactId);
|
|
409
|
+
const receiptId = `repairs/spec-id-dedup/${plan.sourceDigest}.receipt.json`;
|
|
410
|
+
const receiptPath = artifactPath(input.projectId, receiptId);
|
|
411
|
+
await io.mkdir(paths.dir);
|
|
412
|
+
await io.writeAtomic(backupPath, raw);
|
|
413
|
+
await io.writeAtomic(receiptPath, canonicalJson(receipt));
|
|
414
|
+
await io.writeAtomic(paths.store, plan.resultBytes);
|
|
415
|
+
try {
|
|
416
|
+
await io.writeAtomic(receiptPath, canonicalJson({ ...receipt, state: 'committed' }));
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
invalidateSpecsCache(input.projectId);
|
|
420
|
+
return { ...plan, finalizationDegraded: true };
|
|
421
|
+
}
|
|
422
|
+
invalidateSpecsCache(input.projectId);
|
|
423
|
+
return plan;
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
function isValidReceipt(value, projectId) {
|
|
427
|
+
if (!value || typeof value !== 'object') {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
const receipt = value;
|
|
431
|
+
return (receipt.schemaVersion === 1 &&
|
|
432
|
+
receipt.projectId === projectId &&
|
|
433
|
+
(receipt.state === 'prepared' || receipt.state === 'committed') &&
|
|
434
|
+
typeof receipt.sourceDigest === 'string' &&
|
|
435
|
+
/^[a-f0-9]{64}$/.test(receipt.sourceDigest) &&
|
|
436
|
+
typeof receipt.manifestDigest === 'string' &&
|
|
437
|
+
/^[a-f0-9]{64}$/.test(receipt.manifestDigest) &&
|
|
438
|
+
typeof receipt.planDigest === 'string' &&
|
|
439
|
+
/^[a-f0-9]{64}$/.test(receipt.planDigest) &&
|
|
440
|
+
typeof receipt.resultDigest === 'string' &&
|
|
441
|
+
/^[a-f0-9]{64}$/.test(receipt.resultDigest) &&
|
|
442
|
+
typeof receipt.backupDigest === 'string' &&
|
|
443
|
+
/^[a-f0-9]{64}$/.test(receipt.backupDigest) &&
|
|
444
|
+
typeof receipt.backupArtifactId === 'string' &&
|
|
445
|
+
receipt.backupArtifactId.length > 0);
|
|
446
|
+
}
|
|
447
|
+
export async function rollbackCanonicalRepair(projectId, receiptPath) {
|
|
448
|
+
const paths = repairPaths(projectId);
|
|
449
|
+
if (!isContained(paths.dir, receiptPath)) {
|
|
450
|
+
throw privateFailure('receipt is outside the project repair directory');
|
|
451
|
+
}
|
|
452
|
+
const io = ioFor(undefined);
|
|
453
|
+
const receipt = await readJsonArtifact(receiptPath, 'receipt', io);
|
|
454
|
+
if (!isValidReceipt(receipt, projectId)) {
|
|
455
|
+
throw privateFailure('receipt schema, project, state, or digest is invalid');
|
|
456
|
+
}
|
|
457
|
+
const backupPath = artifactPath(projectId, receipt.backupArtifactId);
|
|
458
|
+
return withCanonicalSpecRepairLock(projectId, async () => {
|
|
459
|
+
const current = await io.readFile(paths.store);
|
|
460
|
+
if (digest(current) !== receipt.resultDigest) {
|
|
461
|
+
throw privateFailure('rollback source does not match committed result');
|
|
462
|
+
}
|
|
463
|
+
const backup = await io.readFile(backupPath).catch(() => {
|
|
464
|
+
throw privateFailure('backup is unreadable');
|
|
465
|
+
});
|
|
466
|
+
if (digest(backup) !== receipt.backupDigest) {
|
|
467
|
+
throw privateFailure('backup digest does not match receipt');
|
|
468
|
+
}
|
|
469
|
+
await io.writeAtomic(paths.store, backup);
|
|
470
|
+
invalidateSpecsCache(projectId);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
export const CANONICAL_DEDUPLICATOR_INTERNALS = {
|
|
474
|
+
digest,
|
|
475
|
+
parseRawStore,
|
|
476
|
+
artifactPath,
|
|
477
|
+
isContained,
|
|
478
|
+
};
|
|
479
|
+
//# sourceMappingURL=canonical-deduplicator.js.map
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FilesystemImportDeps, FilesystemImportResult } from '../../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Import specs that exist on disk (planu/specs/) but are missing from the store.
|
|
4
4
|
* Reads frontmatter from each spec.md and creates a minimal Spec record in the store.
|
|
5
5
|
* This ensures list_specs returns ALL specs, not just those created via create_spec.
|
|
6
6
|
*/
|
|
7
|
-
export declare function importFilesystemSpecs(projectPath: string, deps: {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}, projectId: string): Promise<{
|
|
11
|
-
importedCount: number;
|
|
12
|
-
errors: string[];
|
|
13
|
-
}>;
|
|
7
|
+
export declare function importFilesystemSpecs(projectPath: string, deps: FilesystemImportDeps, projectId: string, options?: {
|
|
8
|
+
strictPortableContract?: boolean;
|
|
9
|
+
}): Promise<FilesystemImportResult>;
|
|
14
10
|
//# sourceMappingURL=filesystem-import.d.ts.map
|
|
@@ -3,16 +3,74 @@ import { readdir, readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { parseFrontmatterLocal, validSpecType, validSpecScope, validSpecStatus, validRiskLevel, validSpecTarget, validDifficulty, } from './frontmatter-parser.js';
|
|
5
5
|
const SPEC_FOLDER_ID_RE = /^(SPEC-\d{3,})/;
|
|
6
|
+
function optionalString(value) {
|
|
7
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
function compatibleIdentity(existing, candidate) {
|
|
10
|
+
return (existing.id === candidate.id &&
|
|
11
|
+
existing.title === candidate.title &&
|
|
12
|
+
existing.slug === candidate.slug &&
|
|
13
|
+
(existing.uuid === undefined ||
|
|
14
|
+
candidate.uuid === undefined ||
|
|
15
|
+
existing.uuid === candidate.uuid));
|
|
16
|
+
}
|
|
17
|
+
function hasUuidIdentity(spec, uuid) {
|
|
18
|
+
return uuid !== undefined && (spec.uuid === uuid || spec.uuidAliases?.includes(uuid) === true);
|
|
19
|
+
}
|
|
20
|
+
function hasAnyUuidIdentity(existing, candidate) {
|
|
21
|
+
return [candidate.uuid, ...(candidate.uuidAliases ?? [])].some((uuid) => hasUuidIdentity(existing, uuid));
|
|
22
|
+
}
|
|
23
|
+
function isValidPortableMetadata(specId, metadata) {
|
|
24
|
+
return (metadata.id === specId &&
|
|
25
|
+
typeof metadata.title === 'string' &&
|
|
26
|
+
metadata.title.trim().length > 0 &&
|
|
27
|
+
validSpecType(metadata.type) !== null &&
|
|
28
|
+
validSpecScope(metadata.scope) !== null &&
|
|
29
|
+
validSpecStatus(metadata.status) !== null &&
|
|
30
|
+
validRiskLevel(metadata.risk) !== null &&
|
|
31
|
+
validSpecTarget(metadata.target) !== null &&
|
|
32
|
+
validDifficulty(metadata.difficulty) !== null);
|
|
33
|
+
}
|
|
34
|
+
function generationFromMetadata(metadata, fallbackGeneratedAt) {
|
|
35
|
+
const value = metadata.generation;
|
|
36
|
+
if (typeof value !== 'object' || value === null) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const generationMetadata = value;
|
|
40
|
+
const method = generationMetadata.method;
|
|
41
|
+
if (method !== 'deterministic' && method !== 'internal-model' && method !== 'host') {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const host = optionalString(generationMetadata.host);
|
|
45
|
+
const modelId = optionalString(generationMetadata.modelId);
|
|
46
|
+
return {
|
|
47
|
+
method,
|
|
48
|
+
generatedAt: optionalString(generationMetadata.generatedAt) ?? fallbackGeneratedAt,
|
|
49
|
+
...(host ? { host } : {}),
|
|
50
|
+
...(modelId ? { modelId } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// eslint-disable-next-line max-lines-per-function, complexity -- portable metadata is normalized at one boundary
|
|
6
54
|
function buildSpecFromMetadata(specId, entryName, projectPath, projectId, metadata) {
|
|
7
55
|
const now = new Date().toISOString();
|
|
8
56
|
const folder = join(projectPath, 'planu', 'specs', entryName);
|
|
9
57
|
const specPath = join(folder, 'spec.md');
|
|
10
58
|
const slug = entryName.replace(/^SPEC-\d{3,}-/, '');
|
|
11
59
|
const stringList = (value) => Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
60
|
+
const generation = generationFromMetadata(metadata, now);
|
|
12
61
|
return {
|
|
13
62
|
id: specId,
|
|
63
|
+
...(optionalString(metadata.uuid) ? { uuid: optionalString(metadata.uuid) } : {}),
|
|
64
|
+
...(Array.isArray(metadata.uuidAliases)
|
|
65
|
+
? {
|
|
66
|
+
uuidAliases: metadata.uuidAliases
|
|
67
|
+
.filter((value) => typeof value === 'string' && value.length > 0)
|
|
68
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
69
|
+
}
|
|
70
|
+
: {}),
|
|
14
71
|
title: typeof metadata.title === 'string' ? metadata.title : specId,
|
|
15
72
|
slug,
|
|
73
|
+
...(optionalString(metadata.feature) ? { feature: optionalString(metadata.feature) } : {}),
|
|
16
74
|
type: validSpecType(metadata.type) ?? 'feature',
|
|
17
75
|
scope: validSpecScope(metadata.scope) ?? 'feature',
|
|
18
76
|
status: validSpecStatus(metadata.status) ?? 'draft',
|
|
@@ -46,6 +104,21 @@ function buildSpecFromMetadata(specId, entryName, projectPath, projectId, metada
|
|
|
46
104
|
tags: stringList(metadata.tags),
|
|
47
105
|
dependencies: stringList(metadata.dependencies),
|
|
48
106
|
blockedBy: stringList(metadata.blockedBy),
|
|
107
|
+
outOfScope: stringList(metadata.outOfScope),
|
|
108
|
+
...(metadata.priority === 'P0'
|
|
109
|
+
? { priority: 'high' }
|
|
110
|
+
: metadata.priority === 'high' ||
|
|
111
|
+
metadata.priority === 'medium' ||
|
|
112
|
+
metadata.priority === 'low'
|
|
113
|
+
? { priority: metadata.priority }
|
|
114
|
+
: {}),
|
|
115
|
+
...(metadata.spec_format_version === '0.x' || metadata.spec_format_version === '1.0'
|
|
116
|
+
? { spec_format_version: metadata.spec_format_version }
|
|
117
|
+
: {}),
|
|
118
|
+
...(optionalString(metadata.spec_version)
|
|
119
|
+
? { spec_version: optionalString(metadata.spec_version) }
|
|
120
|
+
: {}),
|
|
121
|
+
...(generation ? { generation } : {}),
|
|
49
122
|
gitBranch: typeof metadata.branch === 'string'
|
|
50
123
|
? metadata.branch
|
|
51
124
|
: `feat/${specId.toLowerCase()}-${slug}`,
|
|
@@ -57,18 +130,18 @@ function buildSpecFromMetadata(specId, entryName, projectPath, projectId, metada
|
|
|
57
130
|
* Reads frontmatter from each spec.md and creates a minimal Spec record in the store.
|
|
58
131
|
* This ensures list_specs returns ALL specs, not just those created via create_spec.
|
|
59
132
|
*/
|
|
60
|
-
export async function importFilesystemSpecs(projectPath, deps, projectId) {
|
|
133
|
+
export async function importFilesystemSpecs(projectPath, deps, projectId, options = {}) {
|
|
61
134
|
const specsRoot = join(projectPath, 'planu/specs');
|
|
62
|
-
const
|
|
63
|
-
|
|
135
|
+
const failures = [];
|
|
136
|
+
const importedSpecIds = [];
|
|
64
137
|
const storeSpecs = await deps.listSpecs(projectId);
|
|
65
|
-
const
|
|
138
|
+
const specsById = new Map(storeSpecs.map((spec) => [spec.id, spec]));
|
|
66
139
|
let entries;
|
|
67
140
|
try {
|
|
68
141
|
entries = await readdir(specsRoot, { withFileTypes: true });
|
|
69
142
|
}
|
|
70
143
|
catch {
|
|
71
|
-
return { importedCount: 0, errors: [] };
|
|
144
|
+
return { importedCount: 0, importedSpecIds, failures, errors: [] };
|
|
72
145
|
}
|
|
73
146
|
for (const entry of entries) {
|
|
74
147
|
if (!entry.isDirectory()) {
|
|
@@ -79,22 +152,56 @@ export async function importFilesystemSpecs(projectPath, deps, projectId) {
|
|
|
79
152
|
continue;
|
|
80
153
|
}
|
|
81
154
|
const specId = idMatch[1];
|
|
82
|
-
if (storeIds.has(specId)) {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
155
|
const specMdPath = join(specsRoot, entry.name, 'spec.md');
|
|
156
|
+
const relativeSpecPath = `planu/specs/${entry.name}/spec.md`;
|
|
157
|
+
let spec;
|
|
86
158
|
try {
|
|
87
159
|
const content = await readFile(specMdPath, 'utf-8');
|
|
88
160
|
const { metadata } = parseFrontmatterLocal(content);
|
|
89
|
-
|
|
161
|
+
if (options.strictPortableContract === true && !isValidPortableMetadata(specId, metadata)) {
|
|
162
|
+
failures.push({ specId, path: relativeSpecPath, code: 'INVALID_CONTRACT' });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
spec = buildSpecFromMetadata(specId, entry.name, projectPath, projectId, metadata);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
failures.push({ specId, path: relativeSpecPath, code: 'READ_FAILED' });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const existing = specsById.get(specId);
|
|
172
|
+
if (existing !== undefined) {
|
|
173
|
+
if (!compatibleIdentity(existing, spec)) {
|
|
174
|
+
failures.push({ specId, path: relativeSpecPath, code: 'INCOMPATIBLE_IDENTITY' });
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (storeSpecs.some((stored) => hasAnyUuidIdentity(stored, spec))) {
|
|
179
|
+
failures.push({ specId, path: relativeSpecPath, code: 'INCOMPATIBLE_IDENTITY' });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
90
183
|
await deps.createSpec(projectId, spec);
|
|
91
|
-
|
|
92
|
-
|
|
184
|
+
specsById.set(specId, spec);
|
|
185
|
+
importedSpecIds.push(specId);
|
|
93
186
|
}
|
|
94
|
-
catch
|
|
95
|
-
|
|
187
|
+
catch {
|
|
188
|
+
const winner = await deps.getSpecFresh?.(projectId, specId).catch(() => null);
|
|
189
|
+
if (winner !== undefined && winner !== null && compatibleIdentity(winner, spec)) {
|
|
190
|
+
specsById.set(specId, winner);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
failures.push({
|
|
194
|
+
specId,
|
|
195
|
+
path: relativeSpecPath,
|
|
196
|
+
code: winner ? 'INCOMPATIBLE_IDENTITY' : 'CREATE_FAILED',
|
|
197
|
+
});
|
|
96
198
|
}
|
|
97
199
|
}
|
|
98
|
-
return {
|
|
200
|
+
return {
|
|
201
|
+
importedCount: importedSpecIds.length,
|
|
202
|
+
importedSpecIds,
|
|
203
|
+
failures,
|
|
204
|
+
errors: failures.map((failure) => `${failure.specId}: ${failure.code}`),
|
|
205
|
+
};
|
|
99
206
|
}
|
|
100
207
|
//# sourceMappingURL=filesystem-import.js.map
|