ai-delivery-workflow 0.2.2 → 0.4.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/README.md +45 -14
- package/bin/ai-delivery.mjs +181 -6
- package/bin/benchmark-autonomy.mjs +217 -0
- package/bin/release-check.mjs +382 -0
- package/docs/CLI-PARAMETER-REFERENCE.zh-CN.md +346 -0
- package/docs/CODEGRAPH-INTEGRATION.zh-CN.md +89 -0
- package/docs/DUAL-REPOSITORY-WORKSPACE.zh-CN.md +2 -2
- package/docs/EVOLUTION-MAINTENANCE.zh-CN.md +7 -7
- package/docs/EVOLUTION-USER-GUIDE.zh-CN.md +2 -2
- package/docs/FILE-REFERENCE.zh-CN.md +120 -39
- package/docs/MAINTENANCE-VERIFICATION.zh-CN.md +51 -0
- package/docs/MOCK-SYSTEM.zh-CN.md +141 -0
- package/docs/NPM-RELEASE-MANAGEMENT.zh-CN.md +106 -0
- package/docs/PROJECT-MANUAL.zh-CN.md +154 -95
- package/docs/PROTOTYPE-MIGRATION.zh-CN.md +164 -0
- package/docs/STATE-CLI-MAINTENANCE.zh-CN.md +35 -17
- package/docs/STATE-CLI-USER-GUIDE.zh-CN.md +115 -39
- package/docs/VIEWER-MAINTENANCE.zh-CN.md +101 -324
- package/docs/VIEWER-USER-GUIDE.zh-CN.md +68 -190
- package/docs/adr/0001-prototype-first-ui-governance.md +3 -0
- package/docs/adr/0002-microcks-as-default-mock-system.md +3 -0
- package/docs/adr/0003-workflow-manager-governed-control-surface.md +46 -0
- package/docs/adr/0004-operational-carbon-visual-standard.md +9 -0
- package/docs/adr/0005-prototype-management-as-routed-pages.md +7 -0
- package/docs/agents/domain.md +12 -0
- package/docs/agents/issue-tracker.md +27 -0
- package/docs/agents/triage-labels.md +11 -0
- package/docs/agents/workflow-manager-acceptance-matrix.md +93 -0
- package/docs/agents/workflow-manager-agent-interface.md +145 -0
- package/docs/agents/workflow-manager-current-state.md +205 -0
- package/lib/artifact-files.mjs +47 -0
- package/lib/codegraph.mjs +405 -0
- package/lib/delivery-state.mjs +3079 -198
- package/lib/frontend-guard.mjs +288 -0
- package/lib/fs-utils.mjs +53 -0
- package/lib/history-anchor.mjs +98 -0
- package/lib/mock-scenario-pack.mjs +687 -0
- package/lib/mock-system.mjs +897 -0
- package/lib/npm-package-result.mjs +11 -0
- package/lib/png-evidence.mjs +149 -0
- package/lib/project-bootstrap.mjs +96 -55
- package/lib/project-installer.mjs +1484 -164
- package/lib/project-migration.mjs +1193 -0
- package/lib/prototype-migration.mjs +713 -0
- package/lib/prototype-page-review.mjs +384 -0
- package/lib/prototype-review-target.mjs +334 -0
- package/lib/prototype-revision.mjs +812 -0
- package/lib/stable-value.mjs +15 -0
- package/lib/toml-hooks.mjs +69 -0
- package/lib/ui-acceptance.mjs +565 -0
- package/lib/visual-standard.mjs +232 -0
- package/lib/workflow-manager-contracts.mjs +367 -0
- package/lib/workflow-manager-facts.mjs +778 -0
- package/lib/workflow-manager-governance.mjs +514 -0
- package/lib/workflow-manager-mock.mjs +183 -0
- package/lib/workflow-manager-prototypes.mjs +1847 -0
- package/lib/workflow-manager-public/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/lib/workflow-manager-public/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/lib/workflow-manager-public/assets/index-Chlg6r8f.css +1 -0
- package/lib/workflow-manager-public/assets/index-E-hGB5ZD.js +13 -0
- package/lib/workflow-manager-public/fonts/OFL-1.1.txt +90 -0
- package/lib/workflow-manager-public/index.html +14 -0
- package/lib/workflow-manager-systems.mjs +519 -0
- package/lib/workflow-manager-transaction.mjs +397 -0
- package/lib/workflow-manager.mjs +745 -0
- package/package.json +8 -2
- package/skills/ai-delivery-assemble-release/agents/openai.yaml +3 -3
- package/skills/ai-delivery-assemble-release/assets/release-material-index.csv +1 -1
- package/skills/ai-delivery-assemble-release/assets/version-inclusion.csv +1 -1
- package/skills/ai-delivery-bootstrap/SKILL.md +4 -4
- package/skills/ai-delivery-bootstrap/agents/openai.yaml +3 -3
- package/skills/ai-delivery-bootstrap/references/bootstrap-contract.md +4 -2
- package/skills/ai-delivery-checkpoint-task/SKILL.md +17 -3
- package/skills/ai-delivery-checkpoint-task/agents/openai.yaml +3 -3
- package/skills/ai-delivery-checkpoint-task/assets/runtime-template/task.json +7 -1
- package/skills/ai-delivery-checkpoint-task/references/checkpoint-contract.md +22 -8
- package/skills/ai-delivery-checkpoint-task/scripts/hook-event.mjs +91 -0
- package/skills/ai-delivery-checkpoint-task/scripts/task-state.mjs +1086 -51
- package/skills/ai-delivery-close-version/SKILL.md +9 -8
- package/skills/ai-delivery-close-version/agents/openai.yaml +3 -3
- package/skills/ai-delivery-close-version/assets/version-lineage.yaml +1 -1
- package/skills/ai-delivery-close-version/references/version-closeout-contract.md +2 -2
- package/skills/ai-delivery-define-product/SKILL.md +8 -4
- package/skills/ai-delivery-define-product/agents/openai.yaml +4 -4
- package/skills/ai-delivery-define-product/references/product-contract.md +2 -2
- package/skills/ai-delivery-deploy-production/agents/openai.yaml +3 -3
- package/skills/ai-delivery-design-architecture/SKILL.md +9 -5
- package/skills/ai-delivery-design-architecture/assets/architecture-template/prototype-architecture-validation.yaml +16 -16
- package/skills/ai-delivery-design-architecture/references/architecture-contract.md +1 -1
- package/skills/ai-delivery-design-experience/SKILL.md +36 -30
- package/skills/ai-delivery-design-experience/agents/openai.yaml +4 -4
- package/skills/ai-delivery-design-experience/assets/experience-template/LEGACY-COMPATIBILITY.md +16 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/design-reference-selection.yaml +32 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/experience-baseline.yaml +2 -2
- package/skills/ai-delivery-design-experience/assets/experience-template/formal-ui-confirmation.yaml +16 -3
- package/skills/ai-delivery-design-experience/assets/experience-template/frontend-change-scope.yaml +30 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/interaction-contract.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/mock-admin-export.yaml +8 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/mock-scenario-mappings.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/mock-scenario-pack-request.yaml +24 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/product-prototype-reconciliation.yaml +2 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-adjustment-log.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-component-bindings.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-component-candidates.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-components.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-confirmation.yaml +18 -18
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-data-contracts.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-impact-assessment.yaml +5 -5
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-implementation-bindings.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-manifest.yaml +2 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-pages.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-product-coverage.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-comments.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-decision.yaml +13 -12
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-scenes.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-sessions.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-unit-pages.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-review-units.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-revision.yaml +88 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-routes.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-set.yaml +2 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-source-inspection-snapshots.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-terminals.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-tool-candidates.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-tool-selection.yaml +2 -3
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-traceability.csv +1 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-trigger-conditions.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype.yaml +6 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/ui-framework-selection.yaml +17 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/ui-handoff.yaml +6 -5
- package/skills/ai-delivery-design-experience/assets/experience-template/ui-mapping.csv +1 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/visual-direction.yaml +4 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/visual-reference-manifest.yaml +23 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/visual-reference.html +163 -0
- package/skills/ai-delivery-design-experience/assets/experience-template/visual-system-confirmation.yaml +11 -0
- package/skills/ai-delivery-design-experience/references/experience-contract.md +26 -27
- package/skills/ai-delivery-design-experience/references/prototype-management-contract.md +42 -21
- package/skills/ai-delivery-design-tests/SKILL.md +13 -7
- package/skills/ai-delivery-design-tests/assets/test-design-template/prototype-test-scope.csv +1 -1
- package/skills/ai-delivery-design-tests/assets/test-design-template/prototype-test-validation.yaml +17 -16
- package/skills/ai-delivery-design-tests/references/test-design-contract.md +7 -5
- package/skills/ai-delivery-develop-iteration/SKILL.md +18 -11
- package/skills/ai-delivery-develop-iteration/agents/openai.yaml +3 -3
- package/skills/ai-delivery-develop-iteration/assets/development-template/candidate-manifest.yaml +36 -0
- package/skills/ai-delivery-develop-iteration/assets/development-template/task-prototype-traceability.csv +1 -1
- package/skills/ai-delivery-develop-iteration/references/development-contract.md +5 -3
- package/skills/ai-delivery-evolve-workflow/agents/openai.yaml +3 -3
- package/skills/ai-delivery-execute-test/SKILL.md +6 -2
- package/skills/ai-delivery-execute-test/agents/openai.yaml +4 -4
- package/skills/ai-delivery-execute-work-package/SKILL.md +10 -3
- package/skills/ai-delivery-execute-work-package/agents/openai.yaml +4 -4
- package/skills/ai-delivery-execute-work-package/references/work-package-contract.md +1 -1
- package/skills/ai-delivery-manage-git/agents/openai.yaml +3 -3
- package/skills/ai-delivery-manage-standards/agents/openai.yaml +4 -4
- package/skills/ai-delivery-orchestrate/SKILL.md +47 -29
- package/skills/ai-delivery-orchestrate/agents/openai.yaml +3 -3
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/control/migrations/.gitkeep +0 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/control/repairs/.gitkeep +0 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/project.yaml +1 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/delivery/workflow-state.yaml +2 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/frontend-guard/frontend-guard.mjs +50 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/hooks/hook-event.mjs +91 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/manager/manager.mjs +64 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/manager/start.cmd +4 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/manager/start.ps1 +2 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/{viewer → manager}/start.sh +1 -1
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/mock/mock.mjs +50 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/ui-acceptance/ui-acceptance.mjs +37 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/AGENTS.md +32 -6
- package/skills/ai-delivery-orchestrate/assets/project-template/gitignore.template +1 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/CONTRIBUTING.md +25 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/LICENSE +21 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/README.md +250 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/UPSTREAM.yaml +19 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/airbnb/DESIGN.md +545 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/airbnb/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/airtable/DESIGN.md +554 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/airtable/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/apple/DESIGN.md +562 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/apple/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/binance/DESIGN.md +634 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/binance/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bmw/DESIGN.md +544 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bmw/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bmw-m/DESIGN.md +503 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bmw-m/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bugatti/DESIGN.md +454 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/bugatti/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cal/DESIGN.md +542 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cal/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/claude/DESIGN.md +589 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/claude/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/clay/DESIGN.md +541 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/clay/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/clickhouse/DESIGN.md +544 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/clickhouse/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cohere/DESIGN.md +451 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cohere/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/coinbase/DESIGN.md +570 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/coinbase/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/composio/DESIGN.md +506 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/composio/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cursor/DESIGN.md +537 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/cursor/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/dell-1996/DESIGN.md +632 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/dell-1996/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/elevenlabs/DESIGN.md +504 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/elevenlabs/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/expo/DESIGN.md +526 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/expo/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ferrari/DESIGN.md +531 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ferrari/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/figma/DESIGN.md +578 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/figma/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/framer/DESIGN.md +544 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/framer/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/hashicorp/DESIGN.md +575 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/hashicorp/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/hp/DESIGN.md +670 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/hp/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ibm/DESIGN.md +550 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ibm/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/intercom/DESIGN.md +546 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/intercom/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/kraken/DESIGN.md +125 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/kraken/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/lamborghini/DESIGN.md +288 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/lamborghini/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/linear.app/DESIGN.md +548 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/linear.app/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/lovable/DESIGN.md +298 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/lovable/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mastercard/DESIGN.md +365 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mastercard/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/meta/DESIGN.md +683 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/meta/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/minimax/DESIGN.md +746 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/minimax/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mintlify/DESIGN.md +852 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mintlify/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/miro/DESIGN.md +825 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/miro/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mistral.ai/DESIGN.md +773 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mistral.ai/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mongodb/DESIGN.md +767 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/mongodb/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nike/DESIGN.md +575 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nike/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nintendo-2001/DESIGN.md +649 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nintendo-2001/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/notion/DESIGN.md +821 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/notion/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nvidia/DESIGN.md +640 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/nvidia/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ollama/DESIGN.md +539 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/ollama/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/opencode.ai/DESIGN.md +521 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/opencode.ai/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/pinterest/DESIGN.md +597 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/pinterest/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/playstation/DESIGN.md +661 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/playstation/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/posthog/DESIGN.md +690 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/posthog/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/raycast/DESIGN.md +669 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/raycast/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/renault/DESIGN.md +589 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/renault/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/replicate/DESIGN.md +616 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/replicate/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/resend/DESIGN.md +585 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/resend/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/revolut/DESIGN.md +636 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/revolut/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/runwayml/DESIGN.md +244 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/runwayml/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/sanity/DESIGN.md +357 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/sanity/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/sentry/DESIGN.md +551 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/sentry/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/shopify/DESIGN.md +516 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/shopify/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/slack/DESIGN.md +482 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/spacex/DESIGN.md +363 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/spacex/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/spotify/DESIGN.md +246 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/spotify/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/starbucks/DESIGN.md +580 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/starbucks/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/stripe/DESIGN.md +487 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/stripe/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/supabase/DESIGN.md +462 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/supabase/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/superhuman/DESIGN.md +448 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/superhuman/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/tesla/DESIGN.md +286 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/tesla/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/theverge/DESIGN.md +339 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/theverge/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/together.ai/DESIGN.md +633 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/together.ai/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/uber/DESIGN.md +636 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/uber/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/vercel/DESIGN.md +736 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/vercel/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/vodafone/DESIGN.md +538 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/vodafone/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/voltagent/DESIGN.md +521 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/voltagent/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/warp/DESIGN.md +526 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/warp/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/webflow/DESIGN.md +588 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/webflow/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/wired/DESIGN.md +497 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/wired/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/wise/DESIGN.md +544 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/wise/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/x.ai/DESIGN.md +465 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/x.ai/README.md +5 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/zapier/DESIGN.md +537 -0
- package/skills/ai-delivery-orchestrate/assets/project-template/ref/ui/design-md/zapier/README.md +5 -0
- package/skills/ai-delivery-orchestrate/references/formal-state-contract.md +65 -20
- package/skills/ai-delivery-orchestrate/references/workflow-model.md +7 -5
- package/skills/ai-delivery-orchestrate-release/SKILL.md +2 -2
- package/skills/ai-delivery-orchestrate-release/agents/openai.yaml +4 -4
- package/skills/ai-delivery-orchestrate-release/references/release-workflow-contract.md +1 -1
- package/skills/ai-delivery-plan-iteration/SKILL.md +11 -4
- package/skills/ai-delivery-plan-iteration/agents/openai.yaml +4 -4
- package/skills/ai-delivery-plan-iteration/assets/planning-template/scope-manifest.yaml +17 -0
- package/skills/ai-delivery-plan-iteration/assets/planning-template/slice-prototype-traceability.csv +1 -1
- package/skills/ai-delivery-plan-iteration/references/planning-contract.md +8 -2
- package/skills/ai-delivery-prepare-platform/SKILL.md +5 -1
- package/skills/ai-delivery-prepare-platform/agents/openai.yaml +4 -4
- package/skills/ai-delivery-prepare-release/agents/openai.yaml +4 -4
- package/skills/ai-delivery-review-change/SKILL.md +14 -4
- package/skills/ai-delivery-review-change/agents/openai.yaml +4 -4
- package/skills/ai-delivery-review-change/references/review-contract.md +6 -0
- package/skills/ai-delivery-triage-failure/SKILL.md +5 -1
- package/skills/ai-delivery-triage-failure/agents/openai.yaml +4 -4
- package/skills/ai-delivery-validate-artifacts/SKILL.md +6 -3
- package/skills/ai-delivery-validate-artifacts/agents/openai.yaml +4 -4
- package/skills/ai-delivery-validate-artifacts/references/artifact-contract.md +13 -13
- package/skills/ai-delivery-verify-candidate/SKILL.md +13 -6
- package/skills/ai-delivery-verify-candidate/agents/openai.yaml +3 -3
- package/skills/ai-delivery-verify-candidate/assets/verification-template/ui-acceptance-request.yaml +19 -0
- package/skills/ai-delivery-verify-candidate/assets/verification-template/ui-acceptance-results.yaml +53 -0
- package/skills/ai-delivery-verify-candidate/references/candidate-verification-contract.md +10 -2
- package/skills/ai-delivery-verify-production/agents/openai.yaml +3 -3
- package/skills/ai-delivery-design-experience/assets/experience-template/low-fidelity-confirmation.yaml +0 -32
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/app.js +0 -546
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/index.html +0 -199
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/public/styles.css +0 -529
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/server.mjs +0 -780
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.cmd +0 -4
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/viewer/start.ps1 +0 -2
|
@@ -0,0 +1,1193 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
5
|
+
import { atomicWrite, filesUnder, resolveContainedRelativePath } from "./fs-utils.mjs";
|
|
6
|
+
import { readVerifiedHistoryAnchor } from "./history-anchor.mjs";
|
|
7
|
+
import {
|
|
8
|
+
applyPrototypeMigration,
|
|
9
|
+
planPrototypeMigration,
|
|
10
|
+
rollbackPrototypeMigration,
|
|
11
|
+
} from "./prototype-migration.mjs";
|
|
12
|
+
|
|
13
|
+
const CLOSED_TASK_STATUSES = new Set(["completed", "failed", "abandoned"]);
|
|
14
|
+
const TASK_CONTRACT_FIELDS = [
|
|
15
|
+
"iteration_id",
|
|
16
|
+
"node",
|
|
17
|
+
"skill",
|
|
18
|
+
"goal",
|
|
19
|
+
"scope",
|
|
20
|
+
"responsibility",
|
|
21
|
+
"depends_on",
|
|
22
|
+
"acceptance",
|
|
23
|
+
"planned_outputs",
|
|
24
|
+
"actual_outputs",
|
|
25
|
+
"branch",
|
|
26
|
+
"worktree",
|
|
27
|
+
"code_repository_id",
|
|
28
|
+
];
|
|
29
|
+
const MIGRATION_FORMAL_STREAMS = new Map([
|
|
30
|
+
["workflow-state", { snapshotFile: "workflow-state.yaml", normalize: (value) => value }],
|
|
31
|
+
["artifact-registry", { snapshotFile: "artifact-registry.yaml", normalize: (value) => value }],
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
function toPosix(value) {
|
|
35
|
+
return value.replaceAll("\\", "/");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sha256(value) {
|
|
39
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function treeDigest(root) {
|
|
43
|
+
const hash = crypto.createHash("sha256");
|
|
44
|
+
for (const file of filesUnder(root)) {
|
|
45
|
+
hash.update(toPosix(path.relative(root, file)));
|
|
46
|
+
hash.update("\0");
|
|
47
|
+
hash.update(fs.readFileSync(file));
|
|
48
|
+
hash.update("\0");
|
|
49
|
+
}
|
|
50
|
+
return hash.digest("hex");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readStructured(file, label) {
|
|
54
|
+
try {
|
|
55
|
+
const text = fs.readFileSync(file, "utf8");
|
|
56
|
+
const value = path.extname(file).toLowerCase() === ".json" ? JSON.parse(text) : parseYaml(text);
|
|
57
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
58
|
+
throw new Error("expected a mapping");
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw new Error(`Cannot read ${label} (${toPosix(file)}): ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function stableValue(value) {
|
|
67
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
68
|
+
if (!value || typeof value !== "object") return value;
|
|
69
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function stableJson(value) {
|
|
73
|
+
return JSON.stringify(stableValue(value));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function uniqueSorted(values) {
|
|
77
|
+
return [...new Set((Array.isArray(values) ? values : []).map(String))].sort();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function taskContract(task) {
|
|
81
|
+
return {
|
|
82
|
+
iteration_id: task.iteration_id || null,
|
|
83
|
+
node: task.node || null,
|
|
84
|
+
skill: task.skill || null,
|
|
85
|
+
goal: task.goal || null,
|
|
86
|
+
scope: uniqueSorted(task.scope),
|
|
87
|
+
responsibility: task.responsibility || task.skill || null,
|
|
88
|
+
depends_on: uniqueSorted(task.depends_on),
|
|
89
|
+
acceptance: uniqueSorted(task.acceptance),
|
|
90
|
+
planned_outputs: uniqueSorted(task.planned_outputs),
|
|
91
|
+
actual_outputs: uniqueSorted(task.actual_outputs),
|
|
92
|
+
branch: task.branch || null,
|
|
93
|
+
worktree: task.worktree || null,
|
|
94
|
+
code_repository_id: task.code_repository_id || task.repo_id || null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function taskRelationships(task, knownIds) {
|
|
99
|
+
return ["derived_from", "supersedes"]
|
|
100
|
+
.map((type) => ({ type, task_id: task[type] || null }))
|
|
101
|
+
.filter((item) => item.task_id && knownIds.has(item.task_id));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function taskFamily(taskId) {
|
|
105
|
+
let family = String(taskId || "");
|
|
106
|
+
let previous;
|
|
107
|
+
do {
|
|
108
|
+
previous = family;
|
|
109
|
+
family = family.replace(/[-_.](?:F|R|T|P)\d+$/i, "").replace(/[-_.]v\d+$/i, "");
|
|
110
|
+
} while (family !== previous);
|
|
111
|
+
return family || String(taskId || "");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readTasks(project, workflowRoot) {
|
|
115
|
+
const sources = [
|
|
116
|
+
path.join(project, workflowRoot, "control", "tasks"),
|
|
117
|
+
path.join(project, workflowRoot, "delivery", "runtime", "tasks"),
|
|
118
|
+
];
|
|
119
|
+
const byId = new Map();
|
|
120
|
+
for (const directory of sources) {
|
|
121
|
+
for (const file of filesUnder(directory)) {
|
|
122
|
+
if (!/\.(json|ya?ml)$/i.test(file)) continue;
|
|
123
|
+
const task = readStructured(file, "migration task");
|
|
124
|
+
if (!task.task_id) continue;
|
|
125
|
+
const relative = toPosix(path.relative(project, file));
|
|
126
|
+
const existing = byId.get(task.task_id);
|
|
127
|
+
if (!existing || relative.includes("/control/tasks/")) {
|
|
128
|
+
byId.set(task.task_id, { ...task, source_path: relative });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return [...byId.values()].sort((left, right) => String(left.task_id).localeCompare(String(right.task_id)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function taskGrouping(tasks) {
|
|
136
|
+
const exact = new Map();
|
|
137
|
+
const families = new Map();
|
|
138
|
+
const knownIds = new Set(tasks.map((task) => task.task_id));
|
|
139
|
+
for (const task of tasks) {
|
|
140
|
+
const contract = taskContract(task);
|
|
141
|
+
const signature = sha256(stableJson(contract));
|
|
142
|
+
if (!exact.has(signature)) exact.set(signature, []);
|
|
143
|
+
exact.get(signature).push(task);
|
|
144
|
+
const family = taskFamily(task.task_id);
|
|
145
|
+
if (!families.has(family)) families.set(family, []);
|
|
146
|
+
families.get(family).push({ task, contract, signature });
|
|
147
|
+
}
|
|
148
|
+
const groups = [...exact.entries()]
|
|
149
|
+
.filter(([, members]) => members.length > 1)
|
|
150
|
+
.map(([signature, members]) => ({
|
|
151
|
+
group_id: `LOGICAL-${signature.slice(0, 16).toUpperCase()}`,
|
|
152
|
+
confidence: "exact-semantic-contract",
|
|
153
|
+
source_task_ids: members.map((task) => task.task_id).sort(),
|
|
154
|
+
status_policy: "preserve-source-facts",
|
|
155
|
+
contract_checksum: signature,
|
|
156
|
+
relationship_edges: members.flatMap((task) => taskRelationships(task, knownIds)
|
|
157
|
+
.map((relationship) => ({
|
|
158
|
+
source_task_id: task.task_id,
|
|
159
|
+
...relationship,
|
|
160
|
+
}))),
|
|
161
|
+
}))
|
|
162
|
+
.sort((left, right) => left.group_id.localeCompare(right.group_id));
|
|
163
|
+
const ambiguities = [];
|
|
164
|
+
for (const [family, members] of families) {
|
|
165
|
+
if (members.length < 2 || new Set(members.map((item) => item.signature)).size < 2) continue;
|
|
166
|
+
const differingFields = TASK_CONTRACT_FIELDS.filter((field) =>
|
|
167
|
+
new Set(members.map((item) => stableJson(item.contract[field]))).size > 1);
|
|
168
|
+
ambiguities.push({
|
|
169
|
+
family,
|
|
170
|
+
source_task_ids: members.map((item) => item.task.task_id).sort(),
|
|
171
|
+
differing_fields: differingFields,
|
|
172
|
+
relationship_edges: members.flatMap((item) => taskRelationships(item.task, knownIds)
|
|
173
|
+
.map((relationship) => ({
|
|
174
|
+
source_task_id: item.task.task_id,
|
|
175
|
+
...relationship,
|
|
176
|
+
}))),
|
|
177
|
+
decision_required: true,
|
|
178
|
+
suffixes_are_semantic: false,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
ambiguities.sort((left, right) => left.family.localeCompare(right.family));
|
|
182
|
+
return {
|
|
183
|
+
strategy: "semantic-contract-v1",
|
|
184
|
+
source_task_count: tasks.length,
|
|
185
|
+
proposed_logical_task_count: tasks.length - groups.reduce((total, group) =>
|
|
186
|
+
total + group.source_task_ids.length - 1, 0),
|
|
187
|
+
groups,
|
|
188
|
+
ambiguities,
|
|
189
|
+
unresolved_ambiguities: ambiguities,
|
|
190
|
+
decisions: [],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function groupingDecisionTemplate(sourceChecksum, ambiguities) {
|
|
195
|
+
return {
|
|
196
|
+
schema_version: 1,
|
|
197
|
+
source_checksum: sourceChecksum,
|
|
198
|
+
actor: "human:<identity>",
|
|
199
|
+
decisions: ambiguities.map((ambiguity) => ({
|
|
200
|
+
family: ambiguity.family,
|
|
201
|
+
action: "keep-separate",
|
|
202
|
+
source_task_ids: ambiguity.source_task_ids,
|
|
203
|
+
rationale: "<why these source tasks are separate or one logical task>",
|
|
204
|
+
})),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function applyGroupingDecisions(grouping, tasks, decisionsFile, sourceChecksum) {
|
|
209
|
+
const result = {
|
|
210
|
+
...grouping,
|
|
211
|
+
groups: grouping.groups.map((group) => ({ ...group })),
|
|
212
|
+
ambiguities: grouping.ambiguities.map((ambiguity) => ({ ...ambiguity })),
|
|
213
|
+
unresolved_ambiguities: grouping.ambiguities.map((ambiguity) => ({ ...ambiguity })),
|
|
214
|
+
decisions: [],
|
|
215
|
+
decision_template: groupingDecisionTemplate(sourceChecksum, grouping.ambiguities),
|
|
216
|
+
};
|
|
217
|
+
if (!decisionsFile) return result;
|
|
218
|
+
const absolute = path.resolve(decisionsFile);
|
|
219
|
+
const document = readStructured(absolute, "migration grouping decisions");
|
|
220
|
+
if (document.schema_version !== 1 || document.source_checksum !== sourceChecksum) {
|
|
221
|
+
throw new Error("Migration grouping decisions do not match the current source checksum.");
|
|
222
|
+
}
|
|
223
|
+
const actor = String(document.actor || "").trim();
|
|
224
|
+
if (!actor.startsWith("human:")) {
|
|
225
|
+
throw new Error("Migration grouping decisions require actor human:<identity>.");
|
|
226
|
+
}
|
|
227
|
+
if (!Array.isArray(document.decisions)) {
|
|
228
|
+
throw new Error("Migration grouping decisions must contain a decisions list.");
|
|
229
|
+
}
|
|
230
|
+
const decisionByFamily = new Map();
|
|
231
|
+
for (const decision of document.decisions) {
|
|
232
|
+
const family = String(decision?.family || "");
|
|
233
|
+
if (!family || decisionByFamily.has(family)) {
|
|
234
|
+
throw new Error(`Migration grouping decision family is missing or duplicated: ${family || "missing"}.`);
|
|
235
|
+
}
|
|
236
|
+
decisionByFamily.set(family, decision);
|
|
237
|
+
}
|
|
238
|
+
const tasksById = new Map(tasks.map((task) => [task.task_id, task]));
|
|
239
|
+
for (const ambiguity of grouping.ambiguities) {
|
|
240
|
+
const decision = decisionByFamily.get(ambiguity.family);
|
|
241
|
+
if (!decision) continue;
|
|
242
|
+
const sourceTaskIds = uniqueSorted(decision.source_task_ids);
|
|
243
|
+
if (stableJson(sourceTaskIds) !== stableJson(ambiguity.source_task_ids)) {
|
|
244
|
+
throw new Error(`Migration grouping decision source tasks changed for ${ambiguity.family}.`);
|
|
245
|
+
}
|
|
246
|
+
const action = String(decision.action || "");
|
|
247
|
+
if (!new Set(["keep-separate", "group"]).has(action)) {
|
|
248
|
+
throw new Error(`Migration grouping decision action is invalid for ${ambiguity.family}.`);
|
|
249
|
+
}
|
|
250
|
+
const rationale = String(decision.rationale || "").trim();
|
|
251
|
+
if (!rationale) throw new Error(`Migration grouping decision requires rationale for ${ambiguity.family}.`);
|
|
252
|
+
const normalized = { family: ambiguity.family, action, source_task_ids: sourceTaskIds, actor, rationale };
|
|
253
|
+
if (action === "group") {
|
|
254
|
+
const canonicalSourceTaskId = String(decision.canonical_source_task_id || "");
|
|
255
|
+
if (!sourceTaskIds.includes(canonicalSourceTaskId)) {
|
|
256
|
+
throw new Error(`Grouped family ${ambiguity.family} requires canonical_source_task_id from source_task_ids.`);
|
|
257
|
+
}
|
|
258
|
+
const logicalTaskId = String(decision.logical_task_id || ambiguity.family);
|
|
259
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(logicalTaskId)) {
|
|
260
|
+
throw new Error(`Grouped family ${ambiguity.family} has an invalid logical_task_id.`);
|
|
261
|
+
}
|
|
262
|
+
result.groups = result.groups.filter((group) =>
|
|
263
|
+
!group.source_task_ids.some((taskId) => sourceTaskIds.includes(taskId)));
|
|
264
|
+
result.groups.push({
|
|
265
|
+
group_id: logicalTaskId,
|
|
266
|
+
confidence: "human-confirmed",
|
|
267
|
+
source_task_ids: sourceTaskIds,
|
|
268
|
+
canonical_source_task_id: canonicalSourceTaskId,
|
|
269
|
+
status_policy: "preserve-source-facts",
|
|
270
|
+
contract_checksum: sha256(stableJson(taskContract(tasksById.get(canonicalSourceTaskId)))),
|
|
271
|
+
relationship_edges: ambiguity.relationship_edges,
|
|
272
|
+
});
|
|
273
|
+
normalized.logical_task_id = logicalTaskId;
|
|
274
|
+
normalized.canonical_source_task_id = canonicalSourceTaskId;
|
|
275
|
+
}
|
|
276
|
+
result.decisions.push(normalized);
|
|
277
|
+
result.ambiguities = result.ambiguities.map((item) => item.family === ambiguity.family
|
|
278
|
+
? { ...item, decision_required: false, resolution: normalized }
|
|
279
|
+
: item);
|
|
280
|
+
}
|
|
281
|
+
for (const family of decisionByFamily.keys()) {
|
|
282
|
+
if (!grouping.ambiguities.some((ambiguity) => ambiguity.family === family)) {
|
|
283
|
+
throw new Error(`Migration grouping decision does not match an ambiguity: ${family}.`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
result.groups.sort((left, right) => left.group_id.localeCompare(right.group_id));
|
|
287
|
+
result.decisions.sort((left, right) => left.family.localeCompare(right.family));
|
|
288
|
+
const resolved = new Set(result.decisions.map((decision) => decision.family));
|
|
289
|
+
result.unresolved_ambiguities = grouping.ambiguities.filter((ambiguity) => !resolved.has(ambiguity.family));
|
|
290
|
+
result.decision_source = {
|
|
291
|
+
path: toPosix(absolute),
|
|
292
|
+
sha256: sha256(fs.readFileSync(absolute)),
|
|
293
|
+
};
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function yamlEventFormat(bytes) {
|
|
298
|
+
const text = bytes.toString("utf8");
|
|
299
|
+
const schemaVersion = text.match(/^schema_version:[ \t]*([^\r\n#]+)/m)?.[1]?.trim();
|
|
300
|
+
const afterValue = text.match(/^after_value:[ \t]*([^\r\n#]*)/m)?.[1]?.trim();
|
|
301
|
+
const operations = text.match(/^operations:[ \t]*([^\r\n#]*)/m)?.[1]?.trim();
|
|
302
|
+
return {
|
|
303
|
+
legacySnapshot: afterValue !== undefined
|
|
304
|
+
&& !new Set(["null", "~", "false", "0", "''", '\"\"']).has(afterValue),
|
|
305
|
+
delta: schemaVersion === "2"
|
|
306
|
+
&& operations !== undefined
|
|
307
|
+
&& (operations === "" || operations.startsWith("[")),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function eventFormat(file, bytes) {
|
|
312
|
+
if (path.extname(file).toLowerCase() !== ".json") return yamlEventFormat(bytes);
|
|
313
|
+
const record = JSON.parse(bytes.toString("utf8"));
|
|
314
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
|
315
|
+
throw new Error(`Cannot read formal event (${toPosix(file)}): expected a mapping`);
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
legacySnapshot: Boolean(record.after_value),
|
|
319
|
+
delta: record.schema_version === 2 && Array.isArray(record.operations),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function fileSha256(file, checksumCache) {
|
|
324
|
+
const cached = checksumCache?.get(path.resolve(file));
|
|
325
|
+
if (cached) return cached;
|
|
326
|
+
const checksum = sha256(fs.readFileSync(file));
|
|
327
|
+
checksumCache?.set(path.resolve(file), checksum);
|
|
328
|
+
return checksum;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function eventFormats(project, workflowRoot, checksumCache = null) {
|
|
332
|
+
const eventRoot = path.join(project, workflowRoot, "control", "events");
|
|
333
|
+
let legacySnapshots = 0;
|
|
334
|
+
let deltas = 0;
|
|
335
|
+
const deltaHash = crypto.createHash("sha256");
|
|
336
|
+
const treeHash = crypto.createHash("sha256");
|
|
337
|
+
for (const file of filesUnder(eventRoot)) {
|
|
338
|
+
const bytes = fs.readFileSync(file);
|
|
339
|
+
const relative = toPosix(path.relative(eventRoot, file));
|
|
340
|
+
checksumCache?.set(path.resolve(file), sha256(bytes));
|
|
341
|
+
treeHash.update(relative);
|
|
342
|
+
treeHash.update("\0");
|
|
343
|
+
treeHash.update(bytes);
|
|
344
|
+
treeHash.update("\0");
|
|
345
|
+
if (!/\.(json|ya?ml)$/i.test(file)) continue;
|
|
346
|
+
const format = eventFormat(file, bytes);
|
|
347
|
+
if (format.legacySnapshot) legacySnapshots += 1;
|
|
348
|
+
if (format.delta) {
|
|
349
|
+
deltas += 1;
|
|
350
|
+
deltaHash.update(relative);
|
|
351
|
+
deltaHash.update("\0");
|
|
352
|
+
deltaHash.update(bytes);
|
|
353
|
+
deltaHash.update("\0");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
legacySnapshots,
|
|
358
|
+
deltas,
|
|
359
|
+
fingerprint: treeHash.digest("hex"),
|
|
360
|
+
deltaFingerprint: deltaHash.digest("hex"),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function trustedAnchors(project, workflowRoot) {
|
|
365
|
+
const verified = readVerifiedHistoryAnchor({
|
|
366
|
+
historySnapshots: path.join(
|
|
367
|
+
project, workflowRoot, "control", "snapshots", "formal-state",
|
|
368
|
+
),
|
|
369
|
+
events: path.join(project, workflowRoot, "delivery", "state-events.jsonl"),
|
|
370
|
+
streams: MIGRATION_FORMAL_STREAMS,
|
|
371
|
+
readStructured,
|
|
372
|
+
stringify: (value) => stringifyYaml(value, { lineWidth: 0 }),
|
|
373
|
+
});
|
|
374
|
+
if (!verified) return [];
|
|
375
|
+
const { anchor, anchorFile, pointer, pointerFile, snapshotFiles } = verified;
|
|
376
|
+
return [{
|
|
377
|
+
anchor_id: anchor.anchor_id,
|
|
378
|
+
anchor_checksum: pointer.anchor_checksum,
|
|
379
|
+
pointer_path: toPosix(path.relative(project, pointerFile)),
|
|
380
|
+
anchor_path: toPosix(path.relative(project, anchorFile)),
|
|
381
|
+
snapshots: [...MIGRATION_FORMAL_STREAMS.keys()].map((stream) => ({
|
|
382
|
+
stream,
|
|
383
|
+
path: toPosix(path.relative(project, snapshotFiles[stream])),
|
|
384
|
+
revision: anchor.streams[stream].revision,
|
|
385
|
+
sha256: anchor.streams[stream].view_checksum,
|
|
386
|
+
})),
|
|
387
|
+
verified: true,
|
|
388
|
+
}];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function archiveConflicts(project, workflowRoot, tasks) {
|
|
392
|
+
const archiveRoot = path.join(project, workflowRoot, "control", "archives", "versions");
|
|
393
|
+
const byIteration = new Map();
|
|
394
|
+
for (const task of tasks) {
|
|
395
|
+
if (!task.iteration_id) continue;
|
|
396
|
+
if (!byIteration.has(task.iteration_id)) byIteration.set(task.iteration_id, []);
|
|
397
|
+
byIteration.get(task.iteration_id).push(task);
|
|
398
|
+
}
|
|
399
|
+
const invalidated = verifiedArchiveInvalidations(project, readRepairTransactions(project));
|
|
400
|
+
const conflicts = [];
|
|
401
|
+
const manifestFiles = fs.existsSync(archiveRoot)
|
|
402
|
+
? fs.readdirSync(archiveRoot, { withFileTypes: true })
|
|
403
|
+
.filter((entry) => entry.isDirectory())
|
|
404
|
+
.map((entry) => path.join(archiveRoot, entry.name, "version-manifest.json"))
|
|
405
|
+
.filter((file) => fs.existsSync(file) && fs.statSync(file).isFile())
|
|
406
|
+
.sort((left, right) => left.localeCompare(right))
|
|
407
|
+
: [];
|
|
408
|
+
for (const file of manifestFiles) {
|
|
409
|
+
const manifest = readStructured(file, "version archive manifest");
|
|
410
|
+
if (invalidated.has(manifest.iteration_id)) continue;
|
|
411
|
+
const activeStoreTasks = byIteration.get(manifest.iteration_id) || [];
|
|
412
|
+
if (!activeStoreTasks.length) continue;
|
|
413
|
+
conflicts.push({
|
|
414
|
+
type: "archive-active-task-conflict",
|
|
415
|
+
iteration_id: manifest.iteration_id,
|
|
416
|
+
archive_path: toPosix(path.relative(project, file)),
|
|
417
|
+
archive_status: manifest.version_status || null,
|
|
418
|
+
source_task_ids: activeStoreTasks.map((task) => task.task_id).sort(),
|
|
419
|
+
retryable_task_ids: activeStoreTasks
|
|
420
|
+
.filter((task) => task.status === "failed")
|
|
421
|
+
.map((task) => task.task_id)
|
|
422
|
+
.sort(),
|
|
423
|
+
decision_required: true,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return conflicts.sort((left, right) => left.iteration_id.localeCompare(right.iteration_id));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function unsignedRepairRecord(record) {
|
|
430
|
+
const unsigned = { ...record };
|
|
431
|
+
delete unsigned.record_checksum;
|
|
432
|
+
return unsigned;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function readRepairTransactions(project) {
|
|
436
|
+
const directory = path.join(project, ".workflow", "control", "repairs", "transactions");
|
|
437
|
+
const records = [];
|
|
438
|
+
let previousChecksum = null;
|
|
439
|
+
for (const file of filesUnder(directory).filter((item) => item.endsWith(".json"))) {
|
|
440
|
+
const record = readStructured(file, "repair transaction");
|
|
441
|
+
if (record.schema_version !== 1
|
|
442
|
+
|| !Number.isInteger(record.sequence)
|
|
443
|
+
|| record.sequence !== records.length + 1
|
|
444
|
+
|| record.previous_checksum !== previousChecksum
|
|
445
|
+
|| record.record_checksum !== sha256(stableJson(unsignedRepairRecord(record)))) {
|
|
446
|
+
throw new Error(`Repair transaction chain is invalid: ${toPosix(path.relative(project, file))}.`);
|
|
447
|
+
}
|
|
448
|
+
records.push(record);
|
|
449
|
+
previousChecksum = record.record_checksum;
|
|
450
|
+
}
|
|
451
|
+
return records;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function verifiedArchiveInvalidations(project, records) {
|
|
455
|
+
const invalidated = new Set();
|
|
456
|
+
for (const record of records.filter((item) => item.transaction_type === "archive-invalidation")) {
|
|
457
|
+
const iterationId = archiveIterationId(record.target?.iteration_id);
|
|
458
|
+
const expectedArchivePath = `.workflow/control/archives/versions/${iterationId}`;
|
|
459
|
+
const expectedManifestPath = `${expectedArchivePath}/version-manifest.json`;
|
|
460
|
+
if (record.target.archive_path !== expectedArchivePath
|
|
461
|
+
|| record.target.manifest_path !== expectedManifestPath
|
|
462
|
+
|| !/^[a-f0-9]{64}$/.test(record.target.archive_sha256 || "")
|
|
463
|
+
|| !/^[a-f0-9]{64}$/.test(record.target.manifest_sha256 || "")) {
|
|
464
|
+
throw new Error(`Archive invalidation target is invalid: ${record.transaction_id}.`);
|
|
465
|
+
}
|
|
466
|
+
const archiveDirectory = path.join(project, ...expectedArchivePath.split("/"));
|
|
467
|
+
const manifestFile = path.join(project, ...expectedManifestPath.split("/"));
|
|
468
|
+
if (!fs.existsSync(manifestFile)
|
|
469
|
+
|| treeDigest(archiveDirectory) !== record.target.archive_sha256
|
|
470
|
+
|| sha256(fs.readFileSync(manifestFile)) !== record.target.manifest_sha256) {
|
|
471
|
+
throw new Error(`Invalidated archive ${iterationId} bytes changed after ${record.transaction_id}.`);
|
|
472
|
+
}
|
|
473
|
+
invalidated.add(iterationId);
|
|
474
|
+
}
|
|
475
|
+
return invalidated;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function repairTransactionPath(sequence, transactionId) {
|
|
479
|
+
return `.workflow/control/repairs/transactions/${String(sequence).padStart(6, "0")}-${transactionId}.json`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function archiveIterationId(value) {
|
|
483
|
+
const iterationId = String(value || "");
|
|
484
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(iterationId)) {
|
|
485
|
+
throw new Error(`Invalid archive iteration ID: ${iterationId || "missing"}.`);
|
|
486
|
+
}
|
|
487
|
+
return iterationId;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function detectLayout(project) {
|
|
491
|
+
const currentState = path.join(project, ".workflow", "delivery", "workflow-state.yaml");
|
|
492
|
+
const legacyState = path.join(project, "delivery", "workflow-state.yaml");
|
|
493
|
+
const current = fs.existsSync(currentState);
|
|
494
|
+
const legacy = fs.existsSync(legacyState);
|
|
495
|
+
if (current && legacy) {
|
|
496
|
+
return {
|
|
497
|
+
root: ".workflow",
|
|
498
|
+
roots: [".workflow", "."],
|
|
499
|
+
source: "current-workflow-root",
|
|
500
|
+
conflicts: [{
|
|
501
|
+
type: "split-workflow-layout",
|
|
502
|
+
current_state_path: ".workflow/delivery/workflow-state.yaml",
|
|
503
|
+
legacy_state_path: "delivery/workflow-state.yaml",
|
|
504
|
+
decision_required: true,
|
|
505
|
+
}],
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
if (current) return { root: ".workflow", roots: [".workflow"], source: "current-workflow-root", conflicts: [] };
|
|
509
|
+
if (legacy) return { root: ".", roots: ["."], source: "legacy-project-root", conflicts: [] };
|
|
510
|
+
throw new Error("Cannot locate workflow-state.yaml in .workflow/delivery or legacy delivery layout.");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function migrationSourceFiles(project, workflowRoot) {
|
|
514
|
+
const workflowBase = path.join(project, workflowRoot);
|
|
515
|
+
const deliveryRoot = path.join(workflowBase, "delivery");
|
|
516
|
+
const controlRoot = path.join(workflowBase, "control");
|
|
517
|
+
const files = [
|
|
518
|
+
...filesUnderExcluding(deliveryRoot, [path.join(deliveryRoot, "runtime")]),
|
|
519
|
+
...filesUnder(path.join(deliveryRoot, "runtime", "tasks")),
|
|
520
|
+
...filesUnderExcluding(controlRoot, [
|
|
521
|
+
path.join(controlRoot, "migrations"),
|
|
522
|
+
...(workflowRoot === ".workflow" ? [path.join(controlRoot, "archives")] : []),
|
|
523
|
+
]),
|
|
524
|
+
...filesUnder(path.join(workflowBase, "standards")),
|
|
525
|
+
];
|
|
526
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function filesUnderExcluding(root, excludedDirectories) {
|
|
530
|
+
if (!fs.existsSync(root)) return [];
|
|
531
|
+
const excluded = new Set(excludedDirectories.map((directory) => path.resolve(directory)));
|
|
532
|
+
const files = [];
|
|
533
|
+
const visit = (directory) => {
|
|
534
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
535
|
+
const absolute = path.join(directory, entry.name);
|
|
536
|
+
if (entry.isDirectory()) {
|
|
537
|
+
if (!excluded.has(path.resolve(absolute))) visit(absolute);
|
|
538
|
+
} else if (entry.isFile()) {
|
|
539
|
+
files.push(absolute);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
visit(root);
|
|
544
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function projectedAttemptStatus(status) {
|
|
548
|
+
return new Map([
|
|
549
|
+
["completed", "completed"],
|
|
550
|
+
["failed", "failed"],
|
|
551
|
+
["abandoned", "abandoned"],
|
|
552
|
+
["blocked", "paused"],
|
|
553
|
+
["interrupted", "paused"],
|
|
554
|
+
]).get(status) || "active";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function taskWithExplicitAttempts(task) {
|
|
558
|
+
if (Array.isArray(task.attempts)) return task;
|
|
559
|
+
const legacyNumber = Number(task.attempt || 0);
|
|
560
|
+
const attempts = legacyNumber > 0 && (task.started_at || task.status !== "queued")
|
|
561
|
+
? [{
|
|
562
|
+
attempt_id: `${task.task_id}/A${legacyNumber}`,
|
|
563
|
+
number: legacyNumber,
|
|
564
|
+
status: projectedAttemptStatus(task.status),
|
|
565
|
+
started_at: task.started_at || task.created_at || null,
|
|
566
|
+
finished_at: CLOSED_TASK_STATUSES.has(task.status)
|
|
567
|
+
? task.finished_at || task.updated_at || null
|
|
568
|
+
: null,
|
|
569
|
+
phases: [],
|
|
570
|
+
time_entries: [],
|
|
571
|
+
time_by_category: {},
|
|
572
|
+
}]
|
|
573
|
+
: [];
|
|
574
|
+
return { ...task, attempts };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function migratedTaskBytes(file) {
|
|
578
|
+
const task = readStructured(file, "legacy migration task");
|
|
579
|
+
return Buffer.from(`${JSON.stringify(taskWithExplicitAttempts(task), null, 2)}\n`, "utf8");
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function isTaskStatePath(relative) {
|
|
583
|
+
return relative.startsWith("control/tasks/") || relative.startsWith("delivery/runtime/tasks/");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function plannedFileChanges(root, layout, migrationId) {
|
|
587
|
+
const copies = [];
|
|
588
|
+
const replacements = [];
|
|
589
|
+
const conflicts = [];
|
|
590
|
+
if (layout.source === "legacy-project-root") {
|
|
591
|
+
const mappings = migrationSourceFiles(root, ".").map((source) => ({
|
|
592
|
+
source,
|
|
593
|
+
source_path: toPosix(path.relative(root, source)),
|
|
594
|
+
target_path: `.workflow/${toPosix(path.relative(root, source))}`,
|
|
595
|
+
}));
|
|
596
|
+
for (const [sourcePath, targetPath] of [
|
|
597
|
+
[".ai-delivery/workspace.yaml", ".workflow/config/workspace.yaml"],
|
|
598
|
+
[".ai-delivery/install-manifest.json", ".workflow/config/install-manifest.json"],
|
|
599
|
+
]) {
|
|
600
|
+
const source = path.join(root, ...sourcePath.split("/"));
|
|
601
|
+
if (fs.existsSync(source)) mappings.push({ source, source_path: sourcePath, target_path: targetPath });
|
|
602
|
+
}
|
|
603
|
+
for (const mapping of mappings) {
|
|
604
|
+
const sourceBytes = fs.readFileSync(mapping.source);
|
|
605
|
+
const logicalPath = mapping.source_path.replace(/^(?:\.workflow\/)?/, "");
|
|
606
|
+
const targetBytes = isTaskStatePath(logicalPath) ? migratedTaskBytes(mapping.source) : sourceBytes;
|
|
607
|
+
const target = path.join(root, ...mapping.target_path.split("/"));
|
|
608
|
+
if (fs.existsSync(target)) {
|
|
609
|
+
if (!fs.readFileSync(target).equals(targetBytes)) {
|
|
610
|
+
conflicts.push({
|
|
611
|
+
type: "migration-target-conflict",
|
|
612
|
+
source_path: mapping.source_path,
|
|
613
|
+
target_path: mapping.target_path,
|
|
614
|
+
decision_required: true,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
copies.push({
|
|
620
|
+
source_path: mapping.source_path,
|
|
621
|
+
target_path: mapping.target_path,
|
|
622
|
+
source_sha256: sha256(sourceBytes),
|
|
623
|
+
target_sha256: sha256(targetBytes),
|
|
624
|
+
transformation: isTaskStatePath(logicalPath) ? "add-explicit-attempts" : "byte-for-byte",
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
const taskRoots = [
|
|
629
|
+
path.join(root, ".workflow", "control", "tasks"),
|
|
630
|
+
path.join(root, ".workflow", "delivery", "runtime", "tasks"),
|
|
631
|
+
];
|
|
632
|
+
for (const file of taskRoots.flatMap(filesUnder).filter((item) => /\.(json|ya?ml)$/i.test(item))) {
|
|
633
|
+
const before = fs.readFileSync(file);
|
|
634
|
+
const after = migratedTaskBytes(file);
|
|
635
|
+
if (before.equals(after)) continue;
|
|
636
|
+
const relative = toPosix(path.relative(root, file));
|
|
637
|
+
const backupPath = `.workflow/control/migrations/${migrationId}/backups/${relative.replace(/^\.workflow\//, "")}`;
|
|
638
|
+
replacements.push({
|
|
639
|
+
path: relative,
|
|
640
|
+
backup_path: backupPath,
|
|
641
|
+
before_sha256: sha256(before),
|
|
642
|
+
after_sha256: sha256(after),
|
|
643
|
+
transformation: "add-explicit-attempts",
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return { copies, replacements, conflicts };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function planMigration(project, options = {}) {
|
|
651
|
+
const root = path.resolve(project);
|
|
652
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
653
|
+
throw new Error(`Project directory does not exist: ${root}`);
|
|
654
|
+
}
|
|
655
|
+
const layout = detectLayout(root);
|
|
656
|
+
const workflowState = readStructured(
|
|
657
|
+
path.join(root, layout.root, "delivery", "workflow-state.yaml"),
|
|
658
|
+
"workflow state",
|
|
659
|
+
);
|
|
660
|
+
const artifactRegistry = readStructured(
|
|
661
|
+
path.join(root, layout.root, "delivery", "artifact-registry.yaml"),
|
|
662
|
+
"artifact registry",
|
|
663
|
+
);
|
|
664
|
+
const tasks = readTasks(root, layout.root);
|
|
665
|
+
const initialGrouping = taskGrouping(tasks);
|
|
666
|
+
const sourceChecksums = new Map();
|
|
667
|
+
const events = eventFormats(root, layout.root, sourceChecksums);
|
|
668
|
+
const anchors = layout.roots.flatMap((workflowRoot) => trustedAnchors(root, workflowRoot));
|
|
669
|
+
const repairFingerprint = treeDigest(
|
|
670
|
+
path.join(root, layout.root, "control", "repairs", "transactions"),
|
|
671
|
+
);
|
|
672
|
+
const legacyConfigSources = layout.source === "legacy-project-root"
|
|
673
|
+
? [".ai-delivery/workspace.yaml", ".ai-delivery/install-manifest.json"]
|
|
674
|
+
.map((relative) => path.join(root, ...relative.split("/")))
|
|
675
|
+
.filter((file) => fs.existsSync(file))
|
|
676
|
+
: [];
|
|
677
|
+
const initialPrototypeMigration = planPrototypeMigration(root);
|
|
678
|
+
const sourceFiles = [...new Set([
|
|
679
|
+
...layout.roots.flatMap((workflowRoot) => migrationSourceFiles(root, workflowRoot)),
|
|
680
|
+
...legacyConfigSources,
|
|
681
|
+
...initialPrototypeMigration.preserved_sources.map((item) =>
|
|
682
|
+
path.join(root, ...item.path.split("/"))),
|
|
683
|
+
].map((file) => path.resolve(file)))]
|
|
684
|
+
.sort((left, right) => left.localeCompare(right));
|
|
685
|
+
const conflicts = [...layout.conflicts, ...archiveConflicts(root, layout.root, tasks)];
|
|
686
|
+
const sourceDigest = sha256(stableJson({
|
|
687
|
+
layout: layout.source,
|
|
688
|
+
roots: layout.roots,
|
|
689
|
+
sources: sourceFiles.map((file) => ({
|
|
690
|
+
path: toPosix(path.relative(root, file)),
|
|
691
|
+
sha256: fileSha256(file, sourceChecksums),
|
|
692
|
+
})),
|
|
693
|
+
}));
|
|
694
|
+
const migrationId = `MIG-${sourceDigest.slice(0, 16).toUpperCase()}`;
|
|
695
|
+
const manifestPath = `.workflow/control/migrations/${migrationId}/migration-manifest.json`;
|
|
696
|
+
const grouping = applyGroupingDecisions(initialGrouping, tasks, options.decisions, sourceDigest);
|
|
697
|
+
const prototypeMigration = planPrototypeMigration(root, {
|
|
698
|
+
decisionsFile: options.decisions,
|
|
699
|
+
sourceChecksum: sourceDigest,
|
|
700
|
+
});
|
|
701
|
+
const changes = plannedFileChanges(root, layout, migrationId);
|
|
702
|
+
conflicts.push(...changes.conflicts);
|
|
703
|
+
const preservedSourcePaths = sourceFiles;
|
|
704
|
+
return {
|
|
705
|
+
command: "migrate",
|
|
706
|
+
dry_run: true,
|
|
707
|
+
applied: false,
|
|
708
|
+
migration_id: migrationId,
|
|
709
|
+
schemas: {
|
|
710
|
+
workspace_layout: {
|
|
711
|
+
source: layout.source,
|
|
712
|
+
target: "current-workflow-root",
|
|
713
|
+
action: layout.source === "current-workflow-root" ? "preserve" : "copy-preserve-source",
|
|
714
|
+
},
|
|
715
|
+
formal_state: {
|
|
716
|
+
workflow_state: workflowState.schema_version ?? null,
|
|
717
|
+
artifact_registry: artifactRegistry.schema_version ?? null,
|
|
718
|
+
legacy_snapshot_events: events.legacySnapshots,
|
|
719
|
+
delta_events: events.deltas,
|
|
720
|
+
target_event_schema: 2,
|
|
721
|
+
action: "preserve-legacy-base-and-append-deltas",
|
|
722
|
+
},
|
|
723
|
+
task_state: {
|
|
724
|
+
source: tasks.some((task) => !Array.isArray(task.attempts))
|
|
725
|
+
? "legacy-flat-tasks"
|
|
726
|
+
: "logical-task-groups-with-attempts",
|
|
727
|
+
target: "logical-task-groups-with-attempts",
|
|
728
|
+
action: "add-explicit-attempts-and-record-semantic-grouping-without-lifecycle-mutation",
|
|
729
|
+
},
|
|
730
|
+
prototype_state: {
|
|
731
|
+
source: "legacy-prototype-set-members",
|
|
732
|
+
target: "single-prototype",
|
|
733
|
+
action: prototypeMigration.status,
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
task_grouping: grouping,
|
|
737
|
+
prototype_migration: prototypeMigration,
|
|
738
|
+
conflicts,
|
|
739
|
+
anchors: {
|
|
740
|
+
trusted: anchors,
|
|
741
|
+
proposed: {
|
|
742
|
+
kind: "migration-baseline",
|
|
743
|
+
source_checksum: sourceDigest,
|
|
744
|
+
preserves_legacy_history: true,
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
file_changes: {
|
|
748
|
+
additions: [{ path: manifestPath, kind: "migration-manifest" }],
|
|
749
|
+
copies: changes.copies,
|
|
750
|
+
replacements: changes.replacements,
|
|
751
|
+
preserved: [
|
|
752
|
+
{ path: `${layout.root === "." ? "" : `${layout.root}/`}delivery/workflow-state.yaml`, reason: "business state is immutable during structural migration" },
|
|
753
|
+
{ path: `${layout.root === "." ? "" : `${layout.root}/`}delivery/artifact-registry.yaml`, reason: "formal artifact semantics are preserved" },
|
|
754
|
+
{ path: `${layout.root === "." ? "" : `${layout.root}/`}control/events`, reason: "legacy event history remains in place" },
|
|
755
|
+
],
|
|
756
|
+
conflicts: changes.conflicts,
|
|
757
|
+
},
|
|
758
|
+
business_state: {
|
|
759
|
+
completion_inferred: false,
|
|
760
|
+
tasks_activated: [],
|
|
761
|
+
tasks_closed: [],
|
|
762
|
+
scope_changed: false,
|
|
763
|
+
},
|
|
764
|
+
can_apply: conflicts.length === 0 && grouping.unresolved_ambiguities.length === 0
|
|
765
|
+
&& prototypeMigration.can_apply,
|
|
766
|
+
forward_state: {
|
|
767
|
+
source_workflow_root: layout.root,
|
|
768
|
+
transaction_workflow_root: ".workflow",
|
|
769
|
+
new_format_event_fingerprint: events.deltaFingerprint,
|
|
770
|
+
repair_transaction_fingerprint: repairFingerprint,
|
|
771
|
+
},
|
|
772
|
+
preserved_sources: [...new Set(preservedSourcePaths.map((file) => path.resolve(file)))]
|
|
773
|
+
.sort((left, right) => left.localeCompare(right))
|
|
774
|
+
.map((file) => ({
|
|
775
|
+
path: toPosix(path.relative(root, file)),
|
|
776
|
+
sha256: fileSha256(file, sourceChecksums),
|
|
777
|
+
})),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function appliedMigration(root) {
|
|
782
|
+
const directory = path.join(root, ".workflow", "control", "migrations");
|
|
783
|
+
const manifests = filesUnder(directory)
|
|
784
|
+
.filter((file) => path.basename(file) === "migration-manifest.json")
|
|
785
|
+
.sort((left, right) => left.localeCompare(right));
|
|
786
|
+
const found = [];
|
|
787
|
+
for (const file of manifests) {
|
|
788
|
+
const manifest = readStructured(file, "applied migration manifest");
|
|
789
|
+
if (!new Set([1, 2, 3]).has(manifest.schema_version) || manifest.status !== "applied"
|
|
790
|
+
|| manifest.record_checksum !== sha256(stableJson(unsignedMigrationManifest(manifest)))) {
|
|
791
|
+
throw new Error(`Applied migration manifest is invalid: ${toPosix(path.relative(root, file))}.`);
|
|
792
|
+
}
|
|
793
|
+
found.push({ manifest, path: toPosix(path.relative(root, file)) });
|
|
794
|
+
}
|
|
795
|
+
found.sort((left, right) =>
|
|
796
|
+
Number(right.manifest.schema_version) - Number(left.manifest.schema_version)
|
|
797
|
+
|| String(right.manifest.applied_at || "").localeCompare(String(left.manifest.applied_at || ""))
|
|
798
|
+
|| right.path.localeCompare(left.path));
|
|
799
|
+
return found[0] || null;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function reportFromManifest(found, dryRun) {
|
|
803
|
+
const { manifest } = found;
|
|
804
|
+
return {
|
|
805
|
+
command: "migrate",
|
|
806
|
+
dry_run: Boolean(dryRun),
|
|
807
|
+
applied: false,
|
|
808
|
+
migration_id: manifest.migration_id,
|
|
809
|
+
schemas: manifest.schemas,
|
|
810
|
+
task_grouping: manifest.task_grouping,
|
|
811
|
+
prototype_migration: manifest.prototype_migration || null,
|
|
812
|
+
conflicts: manifest.conflicts,
|
|
813
|
+
anchors: manifest.anchors,
|
|
814
|
+
file_changes: manifest.file_changes || { additions: [], copies: [], replacements: [], preserved: [], conflicts: [] },
|
|
815
|
+
business_state: manifest.business_state,
|
|
816
|
+
can_apply: true,
|
|
817
|
+
forward_state: manifest.forward_state,
|
|
818
|
+
preserved_sources: manifest.preserved_sources,
|
|
819
|
+
manifest_path: found.path,
|
|
820
|
+
already_applied: true,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function migrationTargetForSource(sourcePath) {
|
|
825
|
+
if (sourcePath === ".ai-delivery/workspace.yaml") return ".workflow/config/workspace.yaml";
|
|
826
|
+
if (sourcePath === ".ai-delivery/install-manifest.json") {
|
|
827
|
+
return ".workflow/config/install-manifest.json";
|
|
828
|
+
}
|
|
829
|
+
if (sourcePath.startsWith("control/migrations/")) return null;
|
|
830
|
+
if (["delivery", "control", "standards"].some((prefix) =>
|
|
831
|
+
sourcePath.startsWith(`${prefix}/`))) return `.workflow/${sourcePath}`;
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function validatedMigrationOperations(root, manifest, migrationId) {
|
|
836
|
+
const fileChanges = manifest.file_changes;
|
|
837
|
+
if (!fileChanges || !Array.isArray(fileChanges.copies) || !Array.isArray(fileChanges.replacements)) {
|
|
838
|
+
throw new Error(`Migration ${migrationId} file changes are invalid.`);
|
|
839
|
+
}
|
|
840
|
+
const targetPaths = new Set();
|
|
841
|
+
const copies = fileChanges.copies.map((copy) => {
|
|
842
|
+
const expectedTarget = migrationTargetForSource(String(copy.source_path || ""));
|
|
843
|
+
if (!expectedTarget || copy.target_path !== expectedTarget
|
|
844
|
+
|| !new Set(["byte-for-byte", "add-explicit-attempts"]).has(copy.transformation)
|
|
845
|
+
|| !/^[a-f0-9]{64}$/.test(copy.source_sha256 || "")
|
|
846
|
+
|| !/^[a-f0-9]{64}$/.test(copy.target_sha256 || "")) {
|
|
847
|
+
throw new Error(`Migration copy path or checksum is invalid: ${copy.source_path || "missing"}.`);
|
|
848
|
+
}
|
|
849
|
+
const source = resolveContainedRelativePath(root, copy.source_path, [
|
|
850
|
+
"delivery", "control", "standards",
|
|
851
|
+
".ai-delivery/workspace.yaml", ".ai-delivery/install-manifest.json",
|
|
852
|
+
]);
|
|
853
|
+
const target = resolveContainedRelativePath(root, copy.target_path, [".workflow"]);
|
|
854
|
+
if (!fs.existsSync(source) || sha256(fs.readFileSync(source)) !== copy.source_sha256) {
|
|
855
|
+
throw new Error(`Migration preserved source changed after ${migrationId}: ${copy.source_path}.`);
|
|
856
|
+
}
|
|
857
|
+
if (targetPaths.has(copy.target_path)) {
|
|
858
|
+
throw new Error(`Migration target path is duplicated: ${copy.target_path}.`);
|
|
859
|
+
}
|
|
860
|
+
targetPaths.add(copy.target_path);
|
|
861
|
+
return { ...copy, source, target };
|
|
862
|
+
});
|
|
863
|
+
const replacements = fileChanges.replacements.map((replacement) => {
|
|
864
|
+
const replacementPath = String(replacement.path || "");
|
|
865
|
+
const expectedBackup = `.workflow/control/migrations/${migrationId}/backups/${replacementPath.replace(/^\.workflow\//, "")}`;
|
|
866
|
+
if (replacement.backup_path !== expectedBackup
|
|
867
|
+
|| replacement.transformation !== "add-explicit-attempts"
|
|
868
|
+
|| !/^[a-f0-9]{64}$/.test(replacement.before_sha256 || "")
|
|
869
|
+
|| !/^[a-f0-9]{64}$/.test(replacement.after_sha256 || "")) {
|
|
870
|
+
throw new Error(`Migration replacement path or checksum is invalid: ${replacementPath || "missing"}.`);
|
|
871
|
+
}
|
|
872
|
+
const target = resolveContainedRelativePath(root, replacementPath, [
|
|
873
|
+
".workflow/control/tasks", ".workflow/delivery/runtime/tasks",
|
|
874
|
+
]);
|
|
875
|
+
const backup = resolveContainedRelativePath(root, replacement.backup_path, [
|
|
876
|
+
`.workflow/control/migrations/${migrationId}/backups`,
|
|
877
|
+
]);
|
|
878
|
+
if (targetPaths.has(replacementPath)) {
|
|
879
|
+
throw new Error(`Migration target path is duplicated: ${replacementPath}.`);
|
|
880
|
+
}
|
|
881
|
+
targetPaths.add(replacementPath);
|
|
882
|
+
return { ...replacement, target, backup };
|
|
883
|
+
});
|
|
884
|
+
return { copies, replacements };
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function rollbackMigration(project, migrationId) {
|
|
888
|
+
const root = path.resolve(project);
|
|
889
|
+
const id = String(migrationId || "");
|
|
890
|
+
if (!/^MIG-[A-F0-9]{16}$/.test(id)) throw new Error(`Invalid migration rollback identifier: ${id || "missing"}.`);
|
|
891
|
+
const migrationDirectory = path.join(root, ".workflow", "control", "migrations", id);
|
|
892
|
+
const manifestFile = path.join(migrationDirectory, "migration-manifest.json");
|
|
893
|
+
const manifest = readStructured(manifestFile, "migration rollback manifest");
|
|
894
|
+
if (!new Set([1, 2, 3]).has(manifest.schema_version)
|
|
895
|
+
|| manifest.migration_id !== id || manifest.status !== "applied") {
|
|
896
|
+
throw new Error(`Migration ${id} is not in an applied rollback state.`);
|
|
897
|
+
}
|
|
898
|
+
if (manifest.record_checksum !== sha256(stableJson(unsignedMigrationManifest(manifest)))) {
|
|
899
|
+
throw new Error(`Migration ${id} manifest checksum is invalid.`);
|
|
900
|
+
}
|
|
901
|
+
const operations = manifest.schema_version >= 2
|
|
902
|
+
? validatedMigrationOperations(root, manifest, id)
|
|
903
|
+
: { copies: [], replacements: [] };
|
|
904
|
+
const transactionWorkflowRoot = manifest.forward_state?.transaction_workflow_root
|
|
905
|
+
|| manifest.forward_state?.source_workflow_root;
|
|
906
|
+
if (!new Set([".", ".workflow"]).has(transactionWorkflowRoot)) {
|
|
907
|
+
throw new Error(`Migration ${id} does not contain a compatible rollback boundary.`);
|
|
908
|
+
}
|
|
909
|
+
const currentEvents = eventFormats(root, transactionWorkflowRoot);
|
|
910
|
+
const currentRepairs = treeDigest(path.join(
|
|
911
|
+
root, ".workflow", "control", "repairs", "transactions",
|
|
912
|
+
));
|
|
913
|
+
if (currentEvents.deltaFingerprint !== manifest.forward_state.new_format_event_fingerprint
|
|
914
|
+
|| currentRepairs !== manifest.forward_state.repair_transaction_fingerprint) {
|
|
915
|
+
throw new Error(
|
|
916
|
+
"A new-format formal transaction was written after this migration; rollback is blocked. Use forward repair.",
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
if (manifest.schema_version >= 2) {
|
|
920
|
+
const replacements = [...operations.replacements].reverse();
|
|
921
|
+
const copies = [...operations.copies].reverse();
|
|
922
|
+
for (const replacement of replacements) {
|
|
923
|
+
if (!fs.existsSync(replacement.target)
|
|
924
|
+
|| sha256(fs.readFileSync(replacement.target)) !== replacement.after_sha256
|
|
925
|
+
|| !fs.existsSync(replacement.backup)
|
|
926
|
+
|| sha256(fs.readFileSync(replacement.backup)) !== replacement.before_sha256) {
|
|
927
|
+
throw new Error(`Migrated task bytes changed after ${id}; rollback is blocked. Use forward repair.`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
for (const copy of copies) {
|
|
931
|
+
if (!fs.existsSync(copy.target) || sha256(fs.readFileSync(copy.target)) !== copy.target_sha256) {
|
|
932
|
+
throw new Error(`Migrated layout bytes changed after ${id}; rollback is blocked. Use forward repair.`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
for (const replacement of replacements) {
|
|
936
|
+
atomicWrite(replacement.target, fs.readFileSync(replacement.backup));
|
|
937
|
+
}
|
|
938
|
+
for (const copy of copies) {
|
|
939
|
+
fs.rmSync(copy.target, { force: true });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (manifest.schema_version >= 3 && manifest.prototype_migration?.applied) {
|
|
943
|
+
rollbackPrototypeMigration(root, manifest.prototype_migration);
|
|
944
|
+
}
|
|
945
|
+
fs.rmSync(migrationDirectory, { recursive: true, force: true });
|
|
946
|
+
return {
|
|
947
|
+
command: "migrate rollback",
|
|
948
|
+
migration_id: id,
|
|
949
|
+
rolled_back: true,
|
|
950
|
+
forward_repair_required: false,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function unsignedMigrationManifest(manifest) {
|
|
955
|
+
const unsigned = { ...manifest };
|
|
956
|
+
delete unsigned.record_checksum;
|
|
957
|
+
return unsigned;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
export function migrateProject(project, options = {}) {
|
|
961
|
+
if (options.rollback) return rollbackMigration(project, options.rollback);
|
|
962
|
+
const root = path.resolve(project);
|
|
963
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
964
|
+
throw new Error(`Project directory does not exist: ${root}`);
|
|
965
|
+
}
|
|
966
|
+
const existing = appliedMigration(root);
|
|
967
|
+
const currentPrototypePlan = planPrototypeMigration(root);
|
|
968
|
+
const prototypeMigrationPending = new Set(["ready", "blocked"]).has(currentPrototypePlan.status);
|
|
969
|
+
if (existing && !prototypeMigrationPending) return reportFromManifest(existing, options.dryRun);
|
|
970
|
+
const report = planMigration(root, { decisions: options.decisions });
|
|
971
|
+
report.dry_run = Boolean(options.dryRun);
|
|
972
|
+
if (report.dry_run) return report;
|
|
973
|
+
if (!report.can_apply) {
|
|
974
|
+
const reasons = [
|
|
975
|
+
...report.conflicts.map((item) => item.type),
|
|
976
|
+
...report.task_grouping.unresolved_ambiguities
|
|
977
|
+
.map((item) => `ambiguous-task-family:${item.family}`),
|
|
978
|
+
...report.prototype_migration.unresolved_conflicts
|
|
979
|
+
.map((item) => `prototype-decision:${item.conflict_id}`),
|
|
980
|
+
];
|
|
981
|
+
throw new Error(
|
|
982
|
+
`Migration requires explicit repair, grouping, or Prototype decisions before apply: ${reasons.join(", ")}.`,
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
for (const copy of report.file_changes.copies) {
|
|
986
|
+
const source = path.join(root, ...copy.source_path.split("/"));
|
|
987
|
+
const target = path.join(root, ...copy.target_path.split("/"));
|
|
988
|
+
if (!fs.existsSync(source) || sha256(fs.readFileSync(source)) !== copy.source_sha256
|
|
989
|
+
|| fs.existsSync(target)) {
|
|
990
|
+
throw new Error(`Migration source or target changed during apply: ${copy.source_path}.`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
for (const replacement of report.file_changes.replacements) {
|
|
994
|
+
const target = path.join(root, ...replacement.path.split("/"));
|
|
995
|
+
if (!fs.existsSync(target) || sha256(fs.readFileSync(target)) !== replacement.before_sha256) {
|
|
996
|
+
throw new Error(`Migration task source changed during apply: ${replacement.path}.`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const manifestRelative = report.file_changes.additions[0].path;
|
|
1000
|
+
const manifestFile = path.join(root, ...manifestRelative.split("/"));
|
|
1001
|
+
const manifest = {
|
|
1002
|
+
schema_version: 3,
|
|
1003
|
+
migration_id: report.migration_id,
|
|
1004
|
+
status: "applied",
|
|
1005
|
+
applied_at: new Date().toISOString(),
|
|
1006
|
+
source_checksum: report.anchors.proposed.source_checksum,
|
|
1007
|
+
schemas: report.schemas,
|
|
1008
|
+
task_grouping: report.task_grouping,
|
|
1009
|
+
prototype_migration: report.prototype_migration,
|
|
1010
|
+
conflicts: report.conflicts,
|
|
1011
|
+
anchors: report.anchors,
|
|
1012
|
+
business_state: report.business_state,
|
|
1013
|
+
file_changes: report.file_changes,
|
|
1014
|
+
preserved_sources: report.preserved_sources,
|
|
1015
|
+
forward_state: report.forward_state,
|
|
1016
|
+
};
|
|
1017
|
+
manifest.record_checksum = sha256(stableJson(manifest));
|
|
1018
|
+
const operations = validatedMigrationOperations(root, manifest, report.migration_id);
|
|
1019
|
+
const copiedTargets = [];
|
|
1020
|
+
const replacedTargets = [];
|
|
1021
|
+
let prototypeApplied = false;
|
|
1022
|
+
try {
|
|
1023
|
+
for (const copy of operations.copies) {
|
|
1024
|
+
const content = copy.transformation === "add-explicit-attempts"
|
|
1025
|
+
? migratedTaskBytes(copy.source)
|
|
1026
|
+
: fs.readFileSync(copy.source);
|
|
1027
|
+
if (sha256(content) !== copy.target_sha256) {
|
|
1028
|
+
throw new Error(`Migration copy plan changed during apply: ${copy.source_path}.`);
|
|
1029
|
+
}
|
|
1030
|
+
atomicWrite(copy.target, content);
|
|
1031
|
+
copiedTargets.push(copy.target);
|
|
1032
|
+
}
|
|
1033
|
+
for (const replacement of operations.replacements) {
|
|
1034
|
+
const before = fs.readFileSync(replacement.target);
|
|
1035
|
+
atomicWrite(replacement.backup, before);
|
|
1036
|
+
const after = migratedTaskBytes(replacement.target);
|
|
1037
|
+
if (sha256(after) !== replacement.after_sha256) {
|
|
1038
|
+
throw new Error(`Migration task plan changed during apply: ${replacement.path}.`);
|
|
1039
|
+
}
|
|
1040
|
+
atomicWrite(replacement.target, after);
|
|
1041
|
+
replacedTargets.push({ target: replacement.target, before });
|
|
1042
|
+
}
|
|
1043
|
+
applyPrototypeMigration(root, report.prototype_migration);
|
|
1044
|
+
prototypeApplied = report.prototype_migration.applied;
|
|
1045
|
+
manifest.prototype_migration = report.prototype_migration;
|
|
1046
|
+
delete manifest.record_checksum;
|
|
1047
|
+
manifest.record_checksum = sha256(stableJson(manifest));
|
|
1048
|
+
atomicWrite(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
if (prototypeApplied) rollbackPrototypeMigration(root, report.prototype_migration);
|
|
1051
|
+
for (const replacement of replacedTargets.reverse()) atomicWrite(replacement.target, replacement.before);
|
|
1052
|
+
for (const target of copiedTargets.reverse()) fs.rmSync(target, { force: true });
|
|
1053
|
+
fs.rmSync(path.dirname(manifestFile), { recursive: true, force: true });
|
|
1054
|
+
throw error;
|
|
1055
|
+
}
|
|
1056
|
+
report.applied = true;
|
|
1057
|
+
report.manifest_path = manifestRelative;
|
|
1058
|
+
report.already_applied = false;
|
|
1059
|
+
return report;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export function repairProject(project, options = {}) {
|
|
1063
|
+
const root = path.resolve(project);
|
|
1064
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
1065
|
+
throw new Error(`Project directory does not exist: ${root}`);
|
|
1066
|
+
}
|
|
1067
|
+
const iterationId = archiveIterationId(options.invalidateArchive);
|
|
1068
|
+
const reason = String(options.reason || "").trim();
|
|
1069
|
+
const actor = String(options.actor || "").trim();
|
|
1070
|
+
const evidence = uniqueSorted(options.evidence);
|
|
1071
|
+
if (!reason) throw new Error("Archive invalidation requires --reason.");
|
|
1072
|
+
if (!actor.startsWith("human:")) {
|
|
1073
|
+
throw new Error("Archive invalidation requires an explicit human actor through --actor human:<identity>.");
|
|
1074
|
+
}
|
|
1075
|
+
if (evidence.length < 2) {
|
|
1076
|
+
throw new Error("Archive invalidation requires at least two --evidence values for RCA and recovery evidence.");
|
|
1077
|
+
}
|
|
1078
|
+
const archiveDirectory = path.join(
|
|
1079
|
+
root, ".workflow", "control", "archives", "versions", iterationId,
|
|
1080
|
+
);
|
|
1081
|
+
const manifestFile = path.join(archiveDirectory, "version-manifest.json");
|
|
1082
|
+
if (!fs.existsSync(manifestFile)) throw new Error(`Version archive not found: ${iterationId}.`);
|
|
1083
|
+
const manifest = readStructured(manifestFile, "version archive manifest");
|
|
1084
|
+
if (manifest.iteration_id !== iterationId) {
|
|
1085
|
+
throw new Error(`Version archive identity mismatch: expected ${iterationId}, found ${manifest.iteration_id || "missing"}.`);
|
|
1086
|
+
}
|
|
1087
|
+
const target = {
|
|
1088
|
+
iteration_id: iterationId,
|
|
1089
|
+
archive_path: toPosix(path.relative(root, archiveDirectory)),
|
|
1090
|
+
manifest_path: toPosix(path.relative(root, manifestFile)),
|
|
1091
|
+
manifest_sha256: sha256(fs.readFileSync(manifestFile)),
|
|
1092
|
+
archive_sha256: treeDigest(archiveDirectory),
|
|
1093
|
+
original_bytes_preserved: true,
|
|
1094
|
+
};
|
|
1095
|
+
const identity = {
|
|
1096
|
+
transaction_type: "archive-invalidation",
|
|
1097
|
+
target,
|
|
1098
|
+
correction: { actor, reason, evidence },
|
|
1099
|
+
};
|
|
1100
|
+
const idempotencyKey = `archive-invalidation:${iterationId}:${sha256(stableJson(identity))}`;
|
|
1101
|
+
const existingTransactions = readRepairTransactions(root);
|
|
1102
|
+
verifiedArchiveInvalidations(root, existingTransactions);
|
|
1103
|
+
const existing = existingTransactions.find((record) => record.idempotency_key === idempotencyKey);
|
|
1104
|
+
const sequence = existing?.sequence || existingTransactions.length + 1;
|
|
1105
|
+
const transactionId = existing?.transaction_id
|
|
1106
|
+
|| `REPAIR-${sha256(idempotencyKey).slice(0, 16).toUpperCase()}`;
|
|
1107
|
+
const transactionPath = repairTransactionPath(sequence, transactionId);
|
|
1108
|
+
const report = {
|
|
1109
|
+
command: "repair",
|
|
1110
|
+
action: "archive-invalidation",
|
|
1111
|
+
dry_run: Boolean(options.dryRun),
|
|
1112
|
+
applied: false,
|
|
1113
|
+
forward_only: true,
|
|
1114
|
+
original_bytes_preserved: true,
|
|
1115
|
+
target,
|
|
1116
|
+
actor,
|
|
1117
|
+
reason,
|
|
1118
|
+
evidence,
|
|
1119
|
+
transaction: {
|
|
1120
|
+
transaction_id: transactionId,
|
|
1121
|
+
path: transactionPath,
|
|
1122
|
+
previous_checksum: existing?.previous_checksum || existingTransactions.at(-1)?.record_checksum || null,
|
|
1123
|
+
},
|
|
1124
|
+
eligibility: existing ? {
|
|
1125
|
+
iteration_id: iterationId,
|
|
1126
|
+
current: "invalidated",
|
|
1127
|
+
source_transaction_id: transactionId,
|
|
1128
|
+
} : {
|
|
1129
|
+
iteration_id: iterationId,
|
|
1130
|
+
current: "eligible",
|
|
1131
|
+
source_transaction_id: null,
|
|
1132
|
+
},
|
|
1133
|
+
};
|
|
1134
|
+
if (report.dry_run || existing) return report;
|
|
1135
|
+
|
|
1136
|
+
const repairRoot = path.join(root, ".workflow", "control", "repairs");
|
|
1137
|
+
const lockFile = path.join(repairRoot, ".repair.lock");
|
|
1138
|
+
fs.mkdirSync(repairRoot, { recursive: true });
|
|
1139
|
+
let lock;
|
|
1140
|
+
try {
|
|
1141
|
+
lock = fs.openSync(lockFile, "wx");
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
throw new Error(`Repair transaction is locked by another writer: ${error.message}`);
|
|
1144
|
+
}
|
|
1145
|
+
try {
|
|
1146
|
+
const currentTransactions = readRepairTransactions(root);
|
|
1147
|
+
const duplicate = currentTransactions.find((record) => record.idempotency_key === idempotencyKey);
|
|
1148
|
+
if (duplicate) {
|
|
1149
|
+
report.transaction.transaction_id = duplicate.transaction_id;
|
|
1150
|
+
report.transaction.path = repairTransactionPath(duplicate.sequence, duplicate.transaction_id);
|
|
1151
|
+
report.eligibility = {
|
|
1152
|
+
iteration_id: iterationId,
|
|
1153
|
+
current: "invalidated",
|
|
1154
|
+
source_transaction_id: duplicate.transaction_id,
|
|
1155
|
+
};
|
|
1156
|
+
return report;
|
|
1157
|
+
}
|
|
1158
|
+
if (currentTransactions.length !== existingTransactions.length
|
|
1159
|
+
|| (currentTransactions.at(-1)?.record_checksum || null)
|
|
1160
|
+
!== (existingTransactions.at(-1)?.record_checksum || null)) {
|
|
1161
|
+
throw new Error("Repair transaction revision changed during apply; rerun the command.");
|
|
1162
|
+
}
|
|
1163
|
+
if (treeDigest(archiveDirectory) !== target.archive_sha256
|
|
1164
|
+
|| sha256(fs.readFileSync(manifestFile)) !== target.manifest_sha256) {
|
|
1165
|
+
throw new Error(`Version archive changed during repair apply: ${iterationId}.`);
|
|
1166
|
+
}
|
|
1167
|
+
const transaction = {
|
|
1168
|
+
schema_version: 1,
|
|
1169
|
+
sequence,
|
|
1170
|
+
transaction_id: transactionId,
|
|
1171
|
+
transaction_type: "archive-invalidation",
|
|
1172
|
+
idempotency_key: idempotencyKey,
|
|
1173
|
+
occurred_at: new Date().toISOString(),
|
|
1174
|
+
target,
|
|
1175
|
+
correction: { actor, reason, evidence },
|
|
1176
|
+
previous_checksum: existingTransactions.at(-1)?.record_checksum || null,
|
|
1177
|
+
};
|
|
1178
|
+
transaction.record_checksum = sha256(stableJson(transaction));
|
|
1179
|
+
const transactionFile = path.join(root, ...transactionPath.split("/"));
|
|
1180
|
+
atomicWrite(transactionFile, `${JSON.stringify(transaction, null, 2)}\n`);
|
|
1181
|
+
report.applied = true;
|
|
1182
|
+
report.transaction.previous_checksum = transaction.previous_checksum;
|
|
1183
|
+
report.eligibility = {
|
|
1184
|
+
iteration_id: iterationId,
|
|
1185
|
+
current: "invalidated",
|
|
1186
|
+
source_transaction_id: transactionId,
|
|
1187
|
+
};
|
|
1188
|
+
return report;
|
|
1189
|
+
} finally {
|
|
1190
|
+
if (lock !== undefined) fs.closeSync(lock);
|
|
1191
|
+
fs.rmSync(lockFile, { force: true });
|
|
1192
|
+
}
|
|
1193
|
+
}
|