@msn-control/liftoff 0.10.4 → 0.11.2
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/DEVELOPER.md +238 -77
- package/README.md +38 -33
- package/assets/supported-stack.json +3 -136
- package/dist/adapters/azure/governance-assessment.d.ts +2 -0
- package/dist/adapters/azure/governance-assessment.js +21 -0
- package/dist/adapters/azure/governance-assessment.js.map +1 -0
- package/dist/adapters/filesystem/atomic-write.d.ts +2 -0
- package/dist/adapters/filesystem/atomic-write.js +74 -0
- package/dist/adapters/filesystem/atomic-write.js.map +1 -0
- package/dist/adapters/filesystem/errors.d.ts +2 -0
- package/dist/adapters/filesystem/errors.js +8 -0
- package/dist/adapters/filesystem/errors.js.map +1 -0
- package/dist/adapters/filesystem/manifest-file.d.ts +1 -0
- package/dist/adapters/filesystem/manifest-file.js +21 -0
- package/dist/adapters/filesystem/manifest-file.js.map +1 -0
- package/dist/adapters/filesystem/project-config.d.ts +4 -0
- package/dist/adapters/filesystem/project-config.js +157 -0
- package/dist/adapters/filesystem/project-config.js.map +1 -0
- package/dist/adapters/filesystem/project-discovery.d.ts +2 -0
- package/dist/adapters/filesystem/project-discovery.js +30 -0
- package/dist/adapters/filesystem/project-discovery.js.map +1 -0
- package/dist/adapters/filesystem/project-files.d.ts +6 -0
- package/dist/adapters/filesystem/project-files.js +68 -0
- package/dist/adapters/filesystem/project-files.js.map +1 -0
- package/dist/adapters/filesystem/project-lock.d.ts +8 -0
- package/dist/adapters/filesystem/project-lock.js +164 -0
- package/dist/adapters/filesystem/project-lock.js.map +1 -0
- package/dist/adapters/filesystem/project-paths.d.ts +2 -0
- package/dist/adapters/filesystem/project-paths.js +60 -0
- package/dist/adapters/filesystem/project-paths.js.map +1 -0
- package/dist/adapters/filesystem/project-transaction.d.ts +24 -0
- package/dist/adapters/filesystem/project-transaction.js +177 -0
- package/dist/adapters/filesystem/project-transaction.js.map +1 -0
- package/dist/adapters/filesystem/reviewed-update-transaction.d.ts +53 -0
- package/dist/adapters/filesystem/reviewed-update-transaction.js +864 -0
- package/dist/adapters/filesystem/reviewed-update-transaction.js.map +1 -0
- package/dist/adapters/filesystem/update-previews.d.ts +50 -0
- package/dist/adapters/filesystem/update-previews.js +613 -0
- package/dist/adapters/filesystem/update-previews.js.map +1 -0
- package/dist/adapters/git/governance-assessment.d.ts +22 -0
- package/dist/adapters/git/governance-assessment.js +161 -0
- package/dist/adapters/git/governance-assessment.js.map +1 -0
- package/dist/adapters/github/governance-assessment.d.ts +2 -0
- package/dist/adapters/github/governance-assessment.js +18 -0
- package/dist/adapters/github/governance-assessment.js.map +1 -0
- package/dist/adapters/packaged-assets/package-root.d.ts +3 -0
- package/dist/adapters/packaged-assets/package-root.js +21 -0
- package/dist/adapters/packaged-assets/package-root.js.map +1 -0
- package/dist/adapters/packaged-assets/supported-stack.d.ts +4 -0
- package/dist/adapters/packaged-assets/supported-stack.js +18 -0
- package/dist/adapters/packaged-assets/supported-stack.js.map +1 -0
- package/dist/adapters/packaged-assets/template-assets.d.ts +23 -0
- package/dist/adapters/packaged-assets/template-assets.js +36 -0
- package/dist/adapters/packaged-assets/template-assets.js.map +1 -0
- package/dist/adapters/process/shell-command.d.ts +6 -0
- package/dist/adapters/process/shell-command.js +27 -0
- package/dist/adapters/process/shell-command.js.map +1 -0
- package/dist/application/context.d.ts +25 -0
- package/dist/application/context.js +2 -0
- package/dist/application/context.js.map +1 -0
- package/dist/application/diagnose/doctor.d.ts +21 -0
- package/dist/application/diagnose/doctor.js +580 -0
- package/dist/application/diagnose/doctor.js.map +1 -0
- package/dist/application/diagnose/generated-project.d.ts +1 -0
- package/dist/application/diagnose/generated-project.js +105 -0
- package/dist/application/diagnose/generated-project.js.map +1 -0
- package/dist/application/diagnose/validate.d.ts +6 -0
- package/dist/application/diagnose/validate.js +27 -0
- package/dist/application/diagnose/validate.js.map +1 -0
- package/dist/application/initialize/fixture.d.ts +2 -0
- package/dist/application/initialize/fixture.js +39 -0
- package/dist/application/initialize/fixture.js.map +1 -0
- package/dist/application/initialize/inputs.d.ts +3 -0
- package/dist/application/initialize/inputs.js +35 -0
- package/dist/application/initialize/inputs.js.map +1 -0
- package/dist/application/initialize/use-case.d.ts +23 -0
- package/dist/application/initialize/use-case.js +360 -0
- package/dist/application/initialize/use-case.js.map +1 -0
- package/dist/application/migrate/use-case.d.ts +7 -0
- package/dist/application/migrate/use-case.js +378 -0
- package/dist/application/migrate/use-case.js.map +1 -0
- package/dist/application/project/catalog.d.ts +46 -0
- package/dist/application/project/catalog.js +12 -0
- package/dist/application/project/catalog.js.map +1 -0
- package/dist/application/project/manifest.d.ts +2 -0
- package/dist/application/project/manifest.js +29 -0
- package/dist/application/project/manifest.js.map +1 -0
- package/dist/application/project/planning.d.ts +6 -0
- package/dist/application/project/planning.js +12 -0
- package/dist/application/project/planning.js.map +1 -0
- package/dist/application/update/approval.d.ts +39 -0
- package/dist/application/update/approval.js +54 -0
- package/dist/application/update/approval.js.map +1 -0
- package/dist/application/update/command-guidance.d.ts +2 -0
- package/dist/application/update/command-guidance.js +14 -0
- package/dist/application/update/command-guidance.js.map +1 -0
- package/dist/application/update/inspection.d.ts +37 -0
- package/dist/application/update/inspection.js +235 -0
- package/dist/application/update/inspection.js.map +1 -0
- package/dist/application/update/migration-runtime.d.ts +14 -0
- package/dist/application/update/migration-runtime.js +178 -0
- package/dist/application/update/migration-runtime.js.map +1 -0
- package/dist/application/update/output.d.ts +134 -0
- package/dist/application/update/output.js +182 -0
- package/dist/application/update/output.js.map +1 -0
- package/dist/application/update/planning.d.ts +40 -0
- package/dist/application/update/planning.js +293 -0
- package/dist/application/update/planning.js.map +1 -0
- package/dist/application/update/preview.d.ts +49 -0
- package/dist/application/update/preview.js +220 -0
- package/dist/application/update/preview.js.map +1 -0
- package/dist/application/update/protected-source.d.ts +25 -0
- package/dist/application/update/protected-source.js +81 -0
- package/dist/application/update/protected-source.js.map +1 -0
- package/dist/application/update/reporting.d.ts +42 -0
- package/dist/application/update/reporting.js +326 -0
- package/dist/application/update/reporting.js.map +1 -0
- package/dist/application/update/revalidation-plan.d.ts +22 -0
- package/dist/application/update/revalidation-plan.js +108 -0
- package/dist/application/update/revalidation-plan.js.map +1 -0
- package/dist/application/update/revalidation.d.ts +70 -0
- package/dist/application/update/revalidation.js +337 -0
- package/dist/application/update/revalidation.js.map +1 -0
- package/dist/application/update/review-plan.d.ts +25 -0
- package/dist/application/update/review-plan.js +89 -0
- package/dist/application/update/review-plan.js.map +1 -0
- package/dist/application/update/transaction-approval.d.ts +29 -0
- package/dist/application/update/transaction-approval.js +82 -0
- package/dist/application/update/transaction-approval.js.map +1 -0
- package/dist/application/update/use-case.d.ts +9 -0
- package/dist/application/update/use-case.js +271 -0
- package/dist/application/update/use-case.js.map +1 -0
- package/dist/application/update/write-plan.d.ts +15 -0
- package/dist/application/update/write-plan.js +115 -0
- package/dist/application/update/write-plan.js.map +1 -0
- package/dist/application/upgrade/use-case.d.ts +6 -0
- package/dist/application/upgrade/use-case.js +82 -0
- package/dist/application/upgrade/use-case.js.map +1 -0
- package/dist/args.d.ts +5 -65
- package/dist/args.js +4 -472
- package/dist/args.js.map +1 -1
- package/dist/artifact-lifecycle.d.ts +1 -21
- package/dist/artifact-lifecycle.js +1 -75
- package/dist/artifact-lifecycle.js.map +1 -1
- package/dist/catalogs.d.ts +1 -50
- package/dist/catalogs.js +1 -472
- package/dist/catalogs.js.map +1 -1
- package/dist/cli/args/contracts.d.ts +51 -0
- package/dist/cli/args/contracts.js +2 -0
- package/dist/cli/args/contracts.js.map +1 -0
- package/dist/cli/args/definitions.d.ts +6 -0
- package/dist/cli/args/definitions.js +188 -0
- package/dist/cli/args/definitions.js.map +1 -0
- package/dist/cli/args/help.d.ts +5 -0
- package/dist/cli/args/help.js +142 -0
- package/dist/cli/args/help.js.map +1 -0
- package/dist/cli/args/parser.d.ts +5 -0
- package/dist/cli/args/parser.js +160 -0
- package/dist/cli/args/parser.js.map +1 -0
- package/dist/cli/args/readers.d.ts +4 -0
- package/dist/cli/args/readers.js +19 -0
- package/dist/cli/args/readers.js.map +1 -0
- package/dist/cli/commands/diagnose.d.ts +4 -0
- package/dist/cli/commands/diagnose.js +12 -0
- package/dist/cli/commands/diagnose.js.map +1 -0
- package/dist/cli/commands/dispatch.d.ts +3 -0
- package/dist/cli/commands/dispatch.js +94 -0
- package/dist/cli/commands/dispatch.js.map +1 -0
- package/dist/cli/commands/help.d.ts +3 -0
- package/dist/cli/commands/help.js +32 -0
- package/dist/cli/commands/help.js.map +1 -0
- package/dist/cli/commands/helpers.d.ts +8 -0
- package/dist/cli/commands/helpers.js +97 -0
- package/dist/cli/commands/helpers.js.map +1 -0
- package/dist/cli/commands/initialize.d.ts +3 -0
- package/dist/cli/commands/initialize.js +7 -0
- package/dist/cli/commands/initialize.js.map +1 -0
- package/dist/cli/commands/migrate.d.ts +3 -0
- package/dist/cli/commands/migrate.js +8 -0
- package/dist/cli/commands/migrate.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +3 -0
- package/dist/cli/commands/plan.js +34 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/reference.d.ts +5 -0
- package/dist/cli/commands/reference.js +43 -0
- package/dist/cli/commands/reference.js.map +1 -0
- package/dist/cli/commands/update.d.ts +3 -0
- package/dist/cli/commands/update.js +10 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/upgrade.d.ts +3 -0
- package/dist/cli/commands/upgrade.js +7 -0
- package/dist/cli/commands/upgrade.js.map +1 -0
- package/dist/cli/context.d.ts +1 -0
- package/dist/cli/context.js +2 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/project-options.d.ts +3 -0
- package/dist/cli/project-options.js +41 -0
- package/dist/cli/project-options.js.map +1 -0
- package/dist/cli.d.ts +3 -3
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +4 -35
- package/dist/commands.js +3 -2969
- package/dist/commands.js.map +1 -1
- package/dist/container-validation.d.ts +1 -1
- package/dist/domain/governance/activation/approvals.d.ts +351 -0
- package/dist/domain/governance/activation/approvals.js +528 -0
- package/dist/domain/governance/activation/approvals.js.map +1 -0
- package/dist/domain/governance/activation/canonical-json.d.ts +4 -0
- package/dist/domain/governance/activation/canonical-json.js +39 -0
- package/dist/domain/governance/activation/canonical-json.js.map +1 -0
- package/dist/domain/governance/activation/capabilities.d.ts +7 -0
- package/dist/domain/governance/activation/capabilities.js +33 -0
- package/dist/domain/governance/activation/capabilities.js.map +1 -0
- package/dist/domain/governance/activation/evidence.d.ts +63 -0
- package/dist/domain/governance/activation/evidence.js +444 -0
- package/dist/domain/governance/activation/evidence.js.map +1 -0
- package/dist/domain/governance/activation/graph.d.ts +397 -0
- package/dist/domain/governance/activation/graph.js +441 -0
- package/dist/domain/governance/activation/graph.js.map +1 -0
- package/dist/domain/governance/activation/inputs.d.ts +25 -0
- package/dist/domain/governance/activation/inputs.js +63 -0
- package/dist/domain/governance/activation/inputs.js.map +1 -0
- package/dist/domain/governance/activation/operations.d.ts +19 -0
- package/dist/domain/governance/activation/operations.js +181 -0
- package/dist/domain/governance/activation/operations.js.map +1 -0
- package/dist/domain/governance/activation/readiness.d.ts +27 -0
- package/dist/domain/governance/activation/readiness.js +269 -0
- package/dist/domain/governance/activation/readiness.js.map +1 -0
- package/dist/domain/governance/activation/types.d.ts +399 -0
- package/dist/domain/governance/activation/types.js +109 -0
- package/dist/domain/governance/activation/types.js.map +1 -0
- package/dist/domain/governance/activation/validators.d.ts +22 -0
- package/dist/domain/governance/activation/validators.js +1347 -0
- package/dist/domain/governance/activation/validators.js.map +1 -0
- package/dist/domain/governance/assessment/catalog.d.ts +14 -0
- package/dist/domain/governance/assessment/catalog.js +105 -0
- package/dist/domain/governance/assessment/catalog.js.map +1 -0
- package/dist/domain/governance/assessment/errors.d.ts +10 -0
- package/dist/domain/governance/assessment/errors.js +22 -0
- package/dist/domain/governance/assessment/errors.js.map +1 -0
- package/dist/domain/governance/assessment/index.d.ts +8 -0
- package/dist/domain/governance/assessment/index.js +9 -0
- package/dist/domain/governance/assessment/index.js.map +1 -0
- package/dist/domain/governance/assessment/live-normalize.d.ts +19 -0
- package/dist/domain/governance/assessment/live-normalize.js +403 -0
- package/dist/domain/governance/assessment/live-normalize.js.map +1 -0
- package/dist/domain/governance/assessment/predicates.d.ts +36 -0
- package/dist/domain/governance/assessment/predicates.js +545 -0
- package/dist/domain/governance/assessment/predicates.js.map +1 -0
- package/dist/domain/governance/assessment/report.d.ts +23 -0
- package/dist/domain/governance/assessment/report.js +266 -0
- package/dist/domain/governance/assessment/report.js.map +1 -0
- package/dist/domain/governance/assessment/sanitize.d.ts +8 -0
- package/dist/domain/governance/assessment/sanitize.js +60 -0
- package/dist/domain/governance/assessment/sanitize.js.map +1 -0
- package/dist/domain/governance/assessment/types.d.ts +174 -0
- package/dist/domain/governance/assessment/types.js +13 -0
- package/dist/domain/governance/assessment/types.js.map +1 -0
- package/dist/domain/governance/assessment/yaml.d.ts +6 -0
- package/dist/domain/governance/assessment/yaml.js +30 -0
- package/dist/domain/governance/assessment/yaml.js.map +1 -0
- package/dist/domain/governance/policy/identity.d.ts +64 -0
- package/dist/domain/governance/policy/identity.js +112 -0
- package/dist/domain/governance/policy/identity.js.map +1 -0
- package/dist/domain/migration/dependencies.d.ts +7 -0
- package/dist/domain/migration/dependencies.js +221 -0
- package/dist/domain/migration/dependencies.js.map +1 -0
- package/dist/domain/migration/inventory.d.ts +3 -0
- package/dist/domain/migration/inventory.js +15 -0
- package/dist/domain/migration/inventory.js.map +1 -0
- package/dist/domain/project/artifact-lifecycle.d.ts +69 -0
- package/dist/domain/project/artifact-lifecycle.js +137 -0
- package/dist/domain/project/artifact-lifecycle.js.map +1 -0
- package/dist/domain/project/catalog.d.ts +48 -0
- package/dist/domain/project/catalog.js +507 -0
- package/dist/domain/project/catalog.js.map +1 -0
- package/dist/domain/project/contracts.d.ts +247 -0
- package/dist/domain/project/contracts.js +2 -0
- package/dist/domain/project/contracts.js.map +1 -0
- package/dist/domain/project/errors.d.ts +3 -0
- package/dist/domain/project/errors.js +7 -0
- package/dist/domain/project/errors.js.map +1 -0
- package/dist/domain/project/infrastructure-layout.d.ts +26 -0
- package/dist/domain/project/infrastructure-layout.js +125 -0
- package/dist/domain/project/infrastructure-layout.js.map +1 -0
- package/dist/domain/project/inputs.d.ts +21 -0
- package/dist/domain/project/inputs.js +41 -0
- package/dist/domain/project/inputs.js.map +1 -0
- package/dist/domain/project/manifest/artifacts.d.ts +8 -0
- package/dist/domain/project/manifest/artifacts.js +150 -0
- package/dist/domain/project/manifest/artifacts.js.map +1 -0
- package/dist/domain/project/manifest/context.d.ts +13 -0
- package/dist/domain/project/manifest/context.js +2 -0
- package/dist/domain/project/manifest/context.js.map +1 -0
- package/dist/domain/project/manifest/fields.d.ts +6 -0
- package/dist/domain/project/manifest/fields.js +36 -0
- package/dist/domain/project/manifest/fields.js.map +1 -0
- package/dist/domain/project/manifest/governance.d.ts +8 -0
- package/dist/domain/project/manifest/governance.js +161 -0
- package/dist/domain/project/manifest/governance.js.map +1 -0
- package/dist/domain/project/manifest/project-identity.d.ts +6 -0
- package/dist/domain/project/manifest/project-identity.js +273 -0
- package/dist/domain/project/manifest/project-identity.js.map +1 -0
- package/dist/domain/project/manifest/reader.d.ts +9 -0
- package/dist/domain/project/manifest/reader.js +141 -0
- package/dist/domain/project/manifest/reader.js.map +1 -0
- package/dist/domain/project/paths.d.ts +2 -0
- package/dist/domain/project/paths.js +29 -0
- package/dist/domain/project/paths.js.map +1 -0
- package/dist/domain/project/planning.d.ts +19 -0
- package/dist/domain/project/planning.js +329 -0
- package/dist/domain/project/planning.js.map +1 -0
- package/dist/domain/project/retired-workload.d.ts +4 -0
- package/dist/domain/project/retired-workload.js +18 -0
- package/dist/domain/project/retired-workload.js.map +1 -0
- package/dist/domain/project/reviewed-update-artifacts.d.ts +2 -0
- package/dist/domain/project/reviewed-update-artifacts.js +3 -0
- package/dist/domain/project/reviewed-update-artifacts.js.map +1 -0
- package/dist/domain/project/supported-stack.d.ts +113 -0
- package/dist/domain/project/supported-stack.js +426 -0
- package/dist/domain/project/supported-stack.js.map +1 -0
- package/dist/domain/workstation/constraints.d.ts +6 -0
- package/dist/domain/workstation/constraints.js +8 -0
- package/dist/domain/workstation/constraints.js.map +1 -0
- package/dist/file-system.d.ts +10 -44
- package/dist/file-system.js +9 -1245
- package/dist/file-system.js.map +1 -1
- package/dist/framework-adapters.d.ts +1 -1
- package/dist/framework-validation.d.ts +1 -1
- package/dist/framework-validation.js +1 -1
- package/dist/framework-validation.js.map +1 -1
- package/dist/genai-templates.d.ts +2 -9
- package/dist/genai-templates.js +4 -5
- package/dist/genai-templates.js.map +1 -1
- package/dist/generators/common/artifacts.d.ts +6 -0
- package/dist/generators/common/artifacts.js +30 -0
- package/dist/generators/common/artifacts.js.map +1 -0
- package/dist/generators/common/base.d.ts +17 -0
- package/dist/generators/common/base.js +425 -0
- package/dist/generators/common/base.js.map +1 -0
- package/dist/generators/common/dotenv-validation.d.ts +2 -0
- package/dist/generators/common/dotenv-validation.js +80 -0
- package/dist/generators/common/dotenv-validation.js.map +1 -0
- package/dist/generators/common/environments.d.ts +3 -0
- package/dist/generators/common/environments.js +15 -0
- package/dist/generators/common/environments.js.map +1 -0
- package/dist/generators/common/frontend.d.ts +15 -0
- package/dist/generators/common/frontend.js +166 -0
- package/dist/generators/common/frontend.js.map +1 -0
- package/dist/generators/common/python-settings.d.ts +2 -0
- package/dist/generators/common/python-settings.js +70 -0
- package/dist/generators/common/python-settings.js.map +1 -0
- package/dist/generators/common/spec-workflow.d.ts +28 -0
- package/dist/generators/common/spec-workflow.js +567 -0
- package/dist/generators/common/spec-workflow.js.map +1 -0
- package/dist/generators/common/values.d.ts +13 -0
- package/dist/generators/common/values.js +16 -0
- package/dist/generators/common/values.js.map +1 -0
- package/dist/generators/containers/compose.d.ts +7 -0
- package/dist/generators/containers/compose.js +196 -0
- package/dist/generators/containers/compose.js.map +1 -0
- package/dist/generators/containers/context.d.ts +1 -0
- package/dist/generators/containers/context.js +43 -0
- package/dist/generators/containers/context.js.map +1 -0
- package/dist/generators/containers/images.d.ts +7 -0
- package/dist/generators/containers/images.js +119 -0
- package/dist/generators/containers/images.js.map +1 -0
- package/dist/generators/context.d.ts +42 -0
- package/dist/generators/context.js +38 -0
- package/dist/generators/context.js.map +1 -0
- package/dist/generators/genai/backend.d.ts +20 -0
- package/dist/generators/genai/backend.js +667 -0
- package/dist/generators/genai/backend.js.map +1 -0
- package/dist/generators/genai/configuration.d.ts +3 -0
- package/dist/generators/genai/configuration.js +43 -0
- package/dist/generators/genai/configuration.js.map +1 -0
- package/dist/generators/genai/database.d.ts +7 -0
- package/dist/generators/genai/database.js +71 -0
- package/dist/generators/genai/database.js.map +1 -0
- package/dist/generators/genai/functions.d.ts +15 -0
- package/dist/generators/genai/functions.js +156 -0
- package/dist/generators/genai/functions.js.map +1 -0
- package/dist/generators/genai/index.d.ts +8 -0
- package/dist/generators/genai/index.js +11 -0
- package/dist/generators/genai/index.js.map +1 -0
- package/dist/generators/genai/patterns.d.ts +9 -0
- package/dist/generators/genai/patterns.js +309 -0
- package/dist/generators/genai/patterns.js.map +1 -0
- package/dist/generators/infrastructure/azure.d.ts +18 -0
- package/dist/generators/infrastructure/azure.js +655 -0
- package/dist/generators/infrastructure/azure.js.map +1 -0
- package/dist/generators/infrastructure/names.d.ts +23 -0
- package/dist/generators/infrastructure/names.js +51 -0
- package/dist/generators/infrastructure/names.js.map +1 -0
- package/dist/generators/standard/configuration.d.ts +3 -0
- package/dist/generators/standard/configuration.js +26 -0
- package/dist/generators/standard/configuration.js.map +1 -0
- package/dist/generators/standard/go.d.ts +17 -0
- package/dist/generators/standard/go.js +338 -0
- package/dist/generators/standard/go.js.map +1 -0
- package/dist/generators/standard/index.d.ts +12 -0
- package/dist/generators/standard/index.js +12 -0
- package/dist/generators/standard/index.js.map +1 -0
- package/dist/generators/standard/node.d.ts +20 -0
- package/dist/generators/standard/node.js +314 -0
- package/dist/generators/standard/node.js.map +1 -0
- package/dist/generators/standard/python.d.ts +18 -0
- package/dist/generators/standard/python.js +191 -0
- package/dist/generators/standard/python.js.map +1 -0
- package/dist/go-template-assets.js +3 -3
- package/dist/go-template-assets.js.map +1 -1
- package/dist/governance-activation/activation-state.d.ts +1 -1
- package/dist/governance-activation/activation-state.js +49 -9
- package/dist/governance-activation/activation-state.js.map +1 -1
- package/dist/governance-activation/approvals.d.ts +1 -188
- package/dist/governance-activation/approvals.js +1 -507
- package/dist/governance-activation/approvals.js.map +1 -1
- package/dist/governance-activation/canonical-json.d.ts +1 -3
- package/dist/governance-activation/canonical-json.js +1 -35
- package/dist/governance-activation/canonical-json.js.map +1 -1
- package/dist/governance-activation/commands.d.ts +6 -1
- package/dist/governance-activation/commands.js +135 -107
- package/dist/governance-activation/commands.js.map +1 -1
- package/dist/governance-activation/compatibility.d.ts +30 -5
- package/dist/governance-activation/compatibility.js +74 -8
- package/dist/governance-activation/compatibility.js.map +1 -1
- package/dist/governance-activation/credentials.d.ts +3 -3
- package/dist/governance-activation/credentials.js +5 -5
- package/dist/governance-activation/credentials.js.map +1 -1
- package/dist/governance-activation/doctor.d.ts +1 -1
- package/dist/governance-activation/doctor.js +59 -100
- package/dist/governance-activation/doctor.js.map +1 -1
- package/dist/governance-activation/evidence.d.ts +1 -49
- package/dist/governance-activation/evidence.js +1 -245
- package/dist/governance-activation/evidence.js.map +1 -1
- package/dist/governance-activation/git-ignore.d.ts +9 -0
- package/dist/governance-activation/git-ignore.js +76 -0
- package/dist/governance-activation/git-ignore.js.map +1 -0
- package/dist/governance-activation/graph.d.ts +1 -397
- package/dist/governance-activation/graph.js +1 -440
- package/dist/governance-activation/graph.js.map +1 -1
- package/dist/governance-activation/historical-state.d.ts +81 -0
- package/dist/governance-activation/historical-state.js +817 -0
- package/dist/governance-activation/historical-state.js.map +1 -0
- package/dist/governance-activation/history-contracts.d.ts +109 -0
- package/dist/governance-activation/history-contracts.js +303 -0
- package/dist/governance-activation/history-contracts.js.map +1 -0
- package/dist/governance-activation/identity.d.ts +1 -35
- package/dist/governance-activation/identity.js +1 -88
- package/dist/governance-activation/identity.js.map +1 -1
- package/dist/governance-activation/index.d.ts +3 -0
- package/dist/governance-activation/index.js +3 -0
- package/dist/governance-activation/index.js.map +1 -1
- package/dist/governance-activation/inputs.d.ts +10 -0
- package/dist/governance-activation/inputs.js +158 -0
- package/dist/governance-activation/inputs.js.map +1 -0
- package/dist/governance-activation/migration-history.d.ts +97 -0
- package/dist/governance-activation/migration-history.js +388 -0
- package/dist/governance-activation/migration-history.js.map +1 -0
- package/dist/governance-activation/migration.d.ts +4 -3
- package/dist/governance-activation/migration.js +14 -6
- package/dist/governance-activation/migration.js.map +1 -1
- package/dist/governance-activation/phase-bootstrap-state.d.ts +3 -0
- package/dist/governance-activation/phase-bootstrap-state.js +128 -0
- package/dist/governance-activation/phase-bootstrap-state.js.map +1 -0
- package/dist/governance-activation/phase-discovery.d.ts +2 -0
- package/dist/governance-activation/phase-discovery.js +88 -0
- package/dist/governance-activation/phase-discovery.js.map +1 -0
- package/dist/governance-activation/phase-governance.d.ts +4 -0
- package/dist/governance-activation/phase-governance.js +141 -0
- package/dist/governance-activation/phase-governance.js.map +1 -0
- package/dist/governance-activation/phase-publication.d.ts +8 -0
- package/dist/governance-activation/phase-publication.js +318 -0
- package/dist/governance-activation/phase-publication.js.map +1 -0
- package/dist/governance-activation/proof-records.d.ts +3 -0
- package/dist/governance-activation/proof-records.js +78 -0
- package/dist/governance-activation/proof-records.js.map +1 -0
- package/dist/governance-activation/read-only.d.ts +30 -0
- package/dist/governance-activation/read-only.js +33 -0
- package/dist/governance-activation/read-only.js.map +1 -0
- package/dist/governance-activation/readiness.d.ts +1 -25
- package/dist/governance-activation/readiness.js +1 -222
- package/dist/governance-activation/readiness.js.map +1 -1
- package/dist/governance-activation/reconciliation.d.ts +1 -1
- package/dist/governance-activation/reconciliation.js +3 -3
- package/dist/governance-activation/reconciliation.js.map +1 -1
- package/dist/governance-activation/release-integrity.d.ts +1 -1
- package/dist/governance-activation/release-integrity.js +2 -2
- package/dist/governance-activation/release-integrity.js.map +1 -1
- package/dist/governance-activation/seed-lifecycle.d.ts +36 -6
- package/dist/governance-activation/seed-lifecycle.js +444 -77
- package/dist/governance-activation/seed-lifecycle.js.map +1 -1
- package/dist/governance-activation/source-of-truth.d.ts +5 -3
- package/dist/governance-activation/source-of-truth.js +32 -52
- package/dist/governance-activation/source-of-truth.js.map +1 -1
- package/dist/governance-activation/spec-kit-seed.d.ts +9 -0
- package/dist/governance-activation/spec-kit-seed.js +43 -0
- package/dist/governance-activation/spec-kit-seed.js.map +1 -0
- package/dist/governance-activation/task-projection.d.ts +1 -1
- package/dist/governance-activation/transition-planning.d.ts +17 -0
- package/dist/governance-activation/transition-planning.js +259 -0
- package/dist/governance-activation/transition-planning.js.map +1 -0
- package/dist/governance-activation/transition-ports.d.ts +150 -0
- package/dist/governance-activation/transition-ports.js +2 -0
- package/dist/governance-activation/transition-ports.js.map +1 -0
- package/dist/governance-activation/transition-process.d.ts +7 -0
- package/dist/governance-activation/transition-process.js +21 -0
- package/dist/governance-activation/transition-process.js.map +1 -0
- package/dist/governance-activation/transition-records.d.ts +56 -0
- package/dist/governance-activation/transition-records.js +208 -0
- package/dist/governance-activation/transition-records.js.map +1 -0
- package/dist/governance-activation/transitions.d.ts +14 -167
- package/dist/governance-activation/transitions.js +173 -2061
- package/dist/governance-activation/transitions.js.map +1 -1
- package/dist/governance-activation/types.d.ts +1 -390
- package/dist/governance-activation/types.js +1 -107
- package/dist/governance-activation/types.js.map +1 -1
- package/dist/governance-activation/validators.d.ts +1 -17
- package/dist/governance-activation/validators.js +1 -1304
- package/dist/governance-activation/validators.js.map +1 -1
- package/dist/governance-assessment/catalog.d.ts +1 -2
- package/dist/governance-assessment/catalog.js +18 -85
- package/dist/governance-assessment/catalog.js.map +1 -1
- package/dist/governance-assessment/command.d.ts +1 -1
- package/dist/governance-assessment/command.js +8 -12
- package/dist/governance-assessment/command.js.map +1 -1
- package/dist/governance-assessment/engine.d.ts +6 -11
- package/dist/governance-assessment/engine.js +490 -114
- package/dist/governance-assessment/engine.js.map +1 -1
- package/dist/governance-assessment/live-normalize.d.ts +1 -19
- package/dist/governance-assessment/live-normalize.js +1 -402
- package/dist/governance-assessment/live-normalize.js.map +1 -1
- package/dist/governance-assessment/live-transport.d.ts +2 -6
- package/dist/governance-assessment/live-transport.js +10 -22
- package/dist/governance-assessment/live-transport.js.map +1 -1
- package/dist/governance-assessment/live.js +55 -5
- package/dist/governance-assessment/live.js.map +1 -1
- package/dist/governance-assessment/predicates.d.ts +1 -27
- package/dist/governance-assessment/predicates.js +1 -329
- package/dist/governance-assessment/predicates.js.map +1 -1
- package/dist/governance-assessment/project.d.ts +20 -17
- package/dist/governance-assessment/project.js +101 -151
- package/dist/governance-assessment/project.js.map +1 -1
- package/dist/governance-assessment/readers.d.ts +1 -5
- package/dist/governance-assessment/readers.js +5 -13
- package/dist/governance-assessment/readers.js.map +1 -1
- package/dist/governance-assessment/report.d.ts +1 -23
- package/dist/governance-assessment/report.js +1 -265
- package/dist/governance-assessment/report.js.map +1 -1
- package/dist/governance-assessment/sanitize.d.ts +1 -8
- package/dist/governance-assessment/sanitize.js +1 -56
- package/dist/governance-assessment/sanitize.js.map +1 -1
- package/dist/governance-assessment/types.d.ts +1 -173
- package/dist/governance-assessment/types.js +1 -12
- package/dist/governance-assessment/types.js.map +1 -1
- package/dist/governance-assessment/yaml.d.ts +1 -6
- package/dist/governance-assessment/yaml.js +1 -29
- package/dist/governance-assessment/yaml.js.map +1 -1
- package/dist/init-filesystem.d.ts +1 -1
- package/dist/init-filesystem.js +126 -174
- package/dist/init-filesystem.js.map +1 -1
- package/dist/interactive.d.ts +2 -2
- package/dist/interactive.js +42 -41
- package/dist/interactive.js.map +1 -1
- package/dist/migrate-plan.d.ts +1 -1
- package/dist/migrate-plan.js +14 -8
- package/dist/migrate-plan.js.map +1 -1
- package/dist/npm-template-assets.js +5 -4
- package/dist/npm-template-assets.js.map +1 -1
- package/dist/openspec-profile.d.ts +1 -1
- package/dist/opentofu-template-assets.js +3 -3
- package/dist/opentofu-template-assets.js.map +1 -1
- package/dist/package-identity.d.ts +3 -0
- package/dist/package-identity.js +13 -2
- package/dist/package-identity.js.map +1 -1
- package/dist/planner.d.ts +1 -19
- package/dist/planner.js +1 -565
- package/dist/planner.js.map +1 -1
- package/dist/process-runner.d.ts +1 -1
- package/dist/project-dependencies.d.ts +5 -3
- package/dist/project-dependencies.js +74 -63
- package/dist/project-dependencies.js.map +1 -1
- package/dist/published-verifier.js +61 -30
- package/dist/published-verifier.js.map +1 -1
- package/dist/python-template-assets.js +6 -9
- package/dist/python-template-assets.js.map +1 -1
- package/dist/reconcile.d.ts +1 -1
- package/dist/reconcile.js +2 -2
- package/dist/reconcile.js.map +1 -1
- package/dist/repository-governance.d.ts +5 -2
- package/dist/repository-governance.js +140 -113
- package/dist/repository-governance.js.map +1 -1
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/scan.d.ts +6 -2
- package/dist/scan.js +78 -30
- package/dist/scan.js.map +1 -1
- package/dist/self-upgrade.d.ts +1 -1
- package/dist/self-upgrade.js +20 -7
- package/dist/self-upgrade.js.map +1 -1
- package/dist/stable-release.js +7 -1
- package/dist/stable-release.js.map +1 -1
- package/dist/standard-templates.d.ts +2 -2
- package/dist/standard-templates.js +6 -841
- package/dist/standard-templates.js.map +1 -1
- package/dist/supported-stack.d.ts +3 -115
- package/dist/supported-stack.js +4 -438
- package/dist/supported-stack.js.map +1 -1
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/templates.d.ts +10 -20
- package/dist/templates.js +52 -3261
- package/dist/templates.js.map +1 -1
- package/dist/types.d.ts +1 -263
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/version.js +2 -6
- package/dist/version.js.map +1 -1
- package/dist/workstation-catalog.d.ts +5 -2
- package/dist/workstation-catalog.js +33 -2
- package/dist/workstation-catalog.js.map +1 -1
- package/dist/workstation.d.ts +4 -3
- package/dist/workstation.js +86 -32
- package/dist/workstation.js.map +1 -1
- package/docs/assets/liftoff-terminal.svg +3 -3
- package/docs/azure-deployment.md +107 -10
- package/docs/cli-reference.md +144 -35
- package/docs/configuration-and-manifests.md +152 -35
- package/docs/existing-repositories.md +48 -12
- package/docs/getting-started.md +44 -31
- package/docs/prerequisites.md +34 -35
- package/docs/project-structure.md +39 -46
- package/docs/repository-governance.md +96 -26
- package/docs/safety-and-consent.md +90 -28
- package/docs/spec-workflows-and-agents.md +31 -28
- package/docs/supported-stack.md +11 -9
- package/docs/troubleshooting.md +114 -54
- package/docs/workloads.md +52 -67
- package/package.json +4 -7
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/UPSTREAM_LICENSE.txt +0 -21
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/catalog.json +0 -520
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/packaged/gitignore +0 -27
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/README.md +0 -25
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/components.json +0 -22
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/eslint.config.js +0 -23
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/index.html +0 -13
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package-lock.json +0 -6156
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package.json +0 -60
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/public/power-apps.svg +0 -55
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/App.tsx +0 -17
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/assets/react.svg +0 -1
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/mode-toggle.tsx +0 -37
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/badge.tsx +0 -46
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/button.tsx +0 -60
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/calendar.tsx +0 -211
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/card.tsx +0 -92
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/chart.tsx +0 -355
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/checkbox.tsx +0 -32
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/command.tsx +0 -184
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dialog.tsx +0 -141
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dropdown-menu.tsx +0 -257
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/input.tsx +0 -21
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/label.tsx +0 -22
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/popover.tsx +0 -46
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/progress.tsx +0 -29
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/select.tsx +0 -185
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/separator.tsx +0 -26
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/skeleton.tsx +0 -13
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/table.tsx +0 -114
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tabs.tsx +0 -66
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/textarea.tsx +0 -18
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tooltip.tsx +0 -61
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/hooks/use-theme.ts +0 -11
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/index.css +0 -131
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/lib/utils.ts +0 -6
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/main.tsx +0 -10
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/_layout.tsx +0 -31
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/home.tsx +0 -59
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/not-found.tsx +0 -14
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/query-provider.tsx +0 -26
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/sonner-provider.tsx +0 -23
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/theme-provider.tsx +0 -64
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/router.tsx +0 -24
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.app.json +0 -34
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.json +0 -13
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.node.json +0 -26
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/vite.config.ts +0 -19
- package/assets/power-apps-code-app/README.md +0 -27
- package/dist/code-apps-plugin.d.ts +0 -13
- package/dist/code-apps-plugin.js +0 -69
- package/dist/code-apps-plugin.js.map +0 -1
- package/dist/power-apps-assets.d.ts +0 -30
- package/dist/power-apps-assets.js +0 -131
- package/dist/power-apps-assets.js.map +0 -1
- package/dist/power-apps-templates.d.ts +0 -3
- package/dist/power-apps-templates.js +0 -177
- package/dist/power-apps-templates.js.map +0 -1
- package/dist/power-apps-validation.d.ts +0 -1
- package/dist/power-apps-validation.js +0 -79
- package/dist/power-apps-validation.js.map +0 -1
|
@@ -1,1947 +1,91 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { executeSeedOperations } from './seed-lifecycle.js';
|
|
2
|
+
import { executeGitOperations } from './phase-publication.js';
|
|
3
|
+
import { discoverPhase0 } from './phase-discovery.js';
|
|
4
|
+
import { executeActivationApproval, executeCredentialReady, executeRulesetPhase } from './phase-governance.js';
|
|
5
|
+
import { remoteImportRetention, executeBootstrapStateDisposal } from './phase-bootstrap-state.js';
|
|
6
|
+
import { phaseOrder, rollbackPlanFromCompletedOperations, phaseById, assertPlanOperationsAllowed, assertOperationAllowed, rollbackPlanForPhase } from '../domain/governance/activation/operations.js';
|
|
3
7
|
import { NodeCommandRunner } from '../process-runner.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { canonicalJson, canonicalSha256 } from '
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { phaseContractDigests } from './graph.js';
|
|
11
|
-
import { buildApprovedPhase0FactsFromState, renderGovernanceChangeWritePlan, stateWithSelectedActiveChange } from './source-of-truth.js';
|
|
12
|
-
import { phaseIds } from './types.js';
|
|
13
|
-
import { validateEvidenceHeader, validateSavedTransitionPlan, validateUserActivationState } from './validators.js';
|
|
8
|
+
import { validateManifestActivationForExecution } from '../domain/governance/activation/validators.js';
|
|
9
|
+
import { withProjectMutationLock } from '../adapters/filesystem/project-lock.js';
|
|
10
|
+
import { initializeExecutionAnchor, saveTransitionPlan, blockedState, writeOutcomeTransaction, safeTimestamp, evidenceHeaderFor, evidencePathParts, nextStateForOutcome, evidenceWriteOperation, stateWriteOperation } from './transition-records.js';
|
|
11
|
+
import { canonicalJson, canonicalSha256, isRecord } from '../domain/governance/activation/canonical-json.js';
|
|
12
|
+
import { buildSavedTransitionPlan, previewApplyNext, comparePlanFreshness } from './transition-planning.js';
|
|
13
|
+
import { readActivationInputSnapshot, phaseInputDigest, remoteBindingDigest } from './inputs.js';
|
|
14
14
|
import { activationStateContentHash } from './activation-state.js';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
if (result.errorCode || result.errorMessage) {
|
|
34
|
-
return `${result.displayCommand}: ${[result.errorCode, result.errorMessage].filter(Boolean).join(': ')}`;
|
|
35
|
-
}
|
|
36
|
-
return `${result.displayCommand} exited ${result.status ?? 'unknown'}${result.stderr ? `: ${result.stderr.trim()}` : ''}`;
|
|
37
|
-
}
|
|
38
|
-
function phaseById(graph, phaseId) {
|
|
39
|
-
const phase = graph.phases.find((entry) => entry.id === phaseId);
|
|
40
|
-
if (!phase) {
|
|
41
|
-
throw new Error(`Unknown phase ${phaseId}.`);
|
|
42
|
-
}
|
|
43
|
-
return phase;
|
|
44
|
-
}
|
|
45
|
-
function safeTimestamp(value) {
|
|
46
|
-
return value.replace(/[^0-9A-Za-z]/g, '');
|
|
47
|
-
}
|
|
48
|
-
export function transitionPlanPathParts(plan) {
|
|
49
|
-
return validateArtifactPathParts([
|
|
50
|
-
...governancePlanDirectoryPathParts,
|
|
51
|
-
`${plan.phaseId}-${safeTimestamp(plan.createdAt)}-${plan.planDigest.slice(0, 12)}.json`
|
|
52
|
-
], 'Governance transition plan path');
|
|
53
|
-
}
|
|
54
|
-
function evidencePathParts(evidenceId) {
|
|
55
|
-
return validateArtifactPathParts(['governance', 'evidence', `${evidenceId}.json`], 'Governance evidence path');
|
|
56
|
-
}
|
|
57
|
-
function activationStatePathParts() {
|
|
58
|
-
return validateArtifactPathParts(['governance', 'activation-state.json'], 'Activation state path');
|
|
59
|
-
}
|
|
60
|
-
function cloneState(state) {
|
|
61
|
-
return JSON.parse(JSON.stringify(state));
|
|
62
|
-
}
|
|
63
|
-
function isRecord(value) {
|
|
64
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
65
|
-
}
|
|
66
|
-
function phaseOrder(phaseId) {
|
|
67
|
-
return phaseIds.indexOf(phaseId);
|
|
68
|
-
}
|
|
69
|
-
function transitionDestination(type, identity, extras = {}) {
|
|
70
|
-
return { type, identity, ...extras };
|
|
71
|
-
}
|
|
72
|
-
function operation(input) {
|
|
73
|
-
return input;
|
|
74
|
-
}
|
|
75
|
-
const allowedActionsByPhase = {
|
|
76
|
-
'seed-valid': [
|
|
77
|
-
'openspec.seed.validate',
|
|
78
|
-
'governance.evidence.write',
|
|
79
|
-
'governance.activation-state.write'
|
|
80
|
-
],
|
|
81
|
-
'seed-verified': [
|
|
82
|
-
'openspec.seed.baseline-verify',
|
|
83
|
-
'governance.evidence.write',
|
|
84
|
-
'governance.activation-state.write'
|
|
85
|
-
],
|
|
86
|
-
'seed-archived': [
|
|
87
|
-
'openspec.seed.archive',
|
|
88
|
-
'governance.evidence.write',
|
|
89
|
-
'governance.activation-state.write'
|
|
90
|
-
],
|
|
91
|
-
committed: [
|
|
92
|
-
'git.init',
|
|
93
|
-
'git.add-reviewed',
|
|
94
|
-
'git.commit-reviewed',
|
|
95
|
-
'git.verify-existing-commit',
|
|
96
|
-
'governance.evidence.write',
|
|
97
|
-
'governance.activation-state.write'
|
|
98
|
-
],
|
|
99
|
-
pushed: [
|
|
100
|
-
'git.push-approved-ref',
|
|
101
|
-
'git.verify-existing-push',
|
|
102
|
-
'governance.evidence.write',
|
|
103
|
-
'governance.activation-state.write'
|
|
104
|
-
],
|
|
105
|
-
'phase-0-complete': [
|
|
106
|
-
'github.phase0.discover',
|
|
107
|
-
'azure.phase0.discover',
|
|
108
|
-
'governance.evidence.write',
|
|
109
|
-
'governance.activation-state.write'
|
|
110
|
-
],
|
|
111
|
-
'activation-approved': [
|
|
112
|
-
'openspec.governance.create-change',
|
|
113
|
-
'spec-kit.governance.create-change',
|
|
114
|
-
'governance.activation-state.write'
|
|
115
|
-
],
|
|
116
|
-
'credential-ready': [
|
|
117
|
-
'github.credential.verify-policy',
|
|
118
|
-
'github.credential.enroll-masked',
|
|
119
|
-
'governance.evidence.write',
|
|
120
|
-
'governance.activation-state.write'
|
|
121
|
-
],
|
|
122
|
-
'provider-ready': [
|
|
123
|
-
'azure.provider.ensure-ready',
|
|
124
|
-
'governance.evidence.write',
|
|
125
|
-
'governance.activation-state.write'
|
|
126
|
-
],
|
|
127
|
-
'state-path-selected': [
|
|
128
|
-
'azure.state-path.select',
|
|
129
|
-
'governance.evidence.write',
|
|
130
|
-
'governance.activation-state.write'
|
|
131
|
-
],
|
|
132
|
-
'existing-private-path': [
|
|
133
|
-
'azure.existing-private-path.verify',
|
|
134
|
-
'governance.evidence.write',
|
|
135
|
-
'governance.activation-state.write'
|
|
136
|
-
],
|
|
137
|
-
'bootstrap-local': [
|
|
138
|
-
'azure.bootstrap-local.apply',
|
|
139
|
-
'github.bootstrap-local.configure',
|
|
140
|
-
'governance.evidence.write',
|
|
141
|
-
'governance.activation-state.write'
|
|
142
|
-
],
|
|
143
|
-
'runner-ready': [
|
|
144
|
-
'github.runner.ensure-ready',
|
|
145
|
-
'governance.evidence.write',
|
|
146
|
-
'governance.activation-state.write'
|
|
147
|
-
],
|
|
148
|
-
'private-backend-proof': [
|
|
149
|
-
'github.runner.backend-proof',
|
|
150
|
-
'azure.remote-state.read',
|
|
151
|
-
'governance.evidence.write',
|
|
152
|
-
'governance.activation-state.write'
|
|
153
|
-
],
|
|
154
|
-
'remote-import-verified': [
|
|
155
|
-
'azure.remote-import.verify',
|
|
156
|
-
'governance.evidence.write',
|
|
157
|
-
'governance.activation-state.write'
|
|
158
|
-
],
|
|
159
|
-
'remote-ready': [
|
|
160
|
-
'azure.remote-ready.verify',
|
|
161
|
-
'governance.evidence.write',
|
|
162
|
-
'governance.activation-state.write'
|
|
163
|
-
],
|
|
164
|
-
'application-foundation': [
|
|
165
|
-
'azure.application-foundation.apply',
|
|
166
|
-
'openspec.governance.update',
|
|
167
|
-
'governance.evidence.write',
|
|
168
|
-
'governance.activation-state.write'
|
|
169
|
-
],
|
|
170
|
-
'workflow-source-ready': [
|
|
171
|
-
'local.workflow-source.write',
|
|
172
|
-
'local.ruleset-source.write',
|
|
173
|
-
'governance.evidence.write',
|
|
174
|
-
'governance.activation-state.write'
|
|
175
|
-
],
|
|
176
|
-
'dev-proof': [
|
|
177
|
-
'github.checks.dev-proof',
|
|
178
|
-
'governance.evidence.write',
|
|
179
|
-
'governance.activation-state.write'
|
|
180
|
-
],
|
|
181
|
-
'staging-qualified': [
|
|
182
|
-
'github.checks.staging',
|
|
183
|
-
'azure.staging.readback',
|
|
184
|
-
'governance.evidence.write',
|
|
185
|
-
'governance.activation-state.write'
|
|
186
|
-
],
|
|
187
|
-
'production-rehearsed': [
|
|
188
|
-
'github.checks.production-rehearsal',
|
|
189
|
-
'azure.production-readback',
|
|
190
|
-
'governance.evidence.write',
|
|
191
|
-
'governance.activation-state.write'
|
|
192
|
-
],
|
|
193
|
-
'green-red-proof': [
|
|
194
|
-
'github.checks.green-red-proof',
|
|
195
|
-
'governance.evidence.write',
|
|
196
|
-
'governance.activation-state.write'
|
|
197
|
-
],
|
|
198
|
-
'enforcement-approved': [
|
|
199
|
-
'governance.activation-state.write'
|
|
200
|
-
],
|
|
201
|
-
'rulesets-applied': [
|
|
202
|
-
'github.ruleset.apply',
|
|
203
|
-
'github.ruleset.readback',
|
|
204
|
-
'governance.evidence.write',
|
|
205
|
-
'governance.activation-state.write'
|
|
206
|
-
],
|
|
207
|
-
'live-readback': [
|
|
208
|
-
'github.ruleset.readback',
|
|
209
|
-
'governance.evidence.write',
|
|
210
|
-
'governance.activation-state.write'
|
|
211
|
-
],
|
|
212
|
-
'bootstrap-state-disposed': [
|
|
213
|
-
'local.bootstrap-state.dispose',
|
|
214
|
-
'governance.evidence.write',
|
|
215
|
-
'governance.activation-state.write'
|
|
216
|
-
]
|
|
15
|
+
import { evidenceHeaderDigest, validateEvidenceFreshness } from '../domain/governance/activation/evidence.js';
|
|
16
|
+
export { governancePlanDirectoryPathParts, transitionPlanPathParts } from './transition-records.js';
|
|
17
|
+
export { buildSavedTransitionPlan, previewApplyNext } from './transition-planning.js';
|
|
18
|
+
export { rollbackPlanFromCompletedOperations, planDigestFor } from '../domain/governance/activation/operations.js';
|
|
19
|
+
const builtInExecutors = {
|
|
20
|
+
'seed-valid': executeSeedOperations,
|
|
21
|
+
'seed-verified': executeSeedOperations,
|
|
22
|
+
'seed-archived': executeSeedOperations,
|
|
23
|
+
committed: executeGitOperations,
|
|
24
|
+
pushed: executeGitOperations,
|
|
25
|
+
'phase-0-complete': discoverPhase0,
|
|
26
|
+
'activation-approved': executeActivationApproval,
|
|
27
|
+
'credential-ready': executeCredentialReady,
|
|
28
|
+
'remote-ready': remoteImportRetention,
|
|
29
|
+
'bootstrap-state-disposed': executeBootstrapStateDisposal,
|
|
30
|
+
'rulesets-applied': executeRulesetPhase,
|
|
31
|
+
'live-readback': executeRulesetPhase
|
|
217
32
|
};
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
if (!isAllowedAction(phase.id, op.actionId)) {
|
|
226
|
-
throw new Error(`Operation ${op.actionId} is not allowlisted for phase ${phase.id}.`);
|
|
227
|
-
}
|
|
228
|
-
const allowed = op.remote ? phase.allowedMutations.remote : phase.allowedMutations.local;
|
|
229
|
-
if (!allowed.includes(op.mutationClass)) {
|
|
230
|
-
throw new Error(`Operation ${op.actionId} mutation ${op.mutationClass} is not declared in phase ${phase.id} ${op.remote ? 'remote' : 'local'} mutations.`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
function assertPlanOperationsAllowed(plan, phase) {
|
|
234
|
-
for (const op of plan.operations) {
|
|
235
|
-
assertOperationAllowed(phase, op);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
function evidenceWriteOperation(phase, pathParts) {
|
|
239
|
-
return operation({
|
|
240
|
-
adapter: 'local-evidence',
|
|
241
|
-
actionId: 'governance.evidence.write',
|
|
242
|
-
mutationClass: 'write-evidence',
|
|
243
|
-
phaseId: phase.id,
|
|
244
|
-
inputs: { pathParts },
|
|
245
|
-
destination: transitionDestination('local', pathParts.join('/'), { pathParts }),
|
|
246
|
-
remote: false,
|
|
247
|
-
destructive: false
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
function stateWriteOperation(phase) {
|
|
251
|
-
return operation({
|
|
252
|
-
adapter: 'local-evidence',
|
|
253
|
-
actionId: 'governance.activation-state.write',
|
|
254
|
-
mutationClass: 'write-activation-state',
|
|
255
|
-
phaseId: phase.id,
|
|
256
|
-
inputs: { pathParts: activationStatePathParts() },
|
|
257
|
-
destination: transitionDestination('local', 'governance/activation-state.json', {
|
|
258
|
-
pathParts: activationStatePathParts()
|
|
259
|
-
}),
|
|
260
|
-
remote: false,
|
|
261
|
-
destructive: false
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
async function allProjectFiles(projectRoot) {
|
|
265
|
-
const files = [];
|
|
266
|
-
async function visit(current, relative) {
|
|
267
|
-
const entries = await readdir(current, { withFileTypes: true });
|
|
268
|
-
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name, 'en'))) {
|
|
269
|
-
if (entry.name === '.git' ||
|
|
270
|
-
entry.name === 'node_modules' ||
|
|
271
|
-
entry.name === '.cache' ||
|
|
272
|
-
(relative === '' && entry.name === 'governance')) {
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
const child = path.join(current, entry.name);
|
|
276
|
-
const childRelative = relative ? `${relative}/${entry.name}` : entry.name;
|
|
277
|
-
if (entry.isDirectory()) {
|
|
278
|
-
await visit(child, childRelative);
|
|
279
|
-
}
|
|
280
|
-
else if (entry.isFile()) {
|
|
281
|
-
files.push(childRelative);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
await visit(projectRoot, '');
|
|
286
|
-
return files;
|
|
287
|
-
}
|
|
288
|
-
function parsePorcelainStatus(output) {
|
|
289
|
-
const entries = output.split('\0').filter(Boolean);
|
|
290
|
-
const parsed = [];
|
|
291
|
-
for (const entry of entries) {
|
|
292
|
-
const index = entry.slice(0, 1);
|
|
293
|
-
const worktree = entry.slice(1, 2);
|
|
294
|
-
const rawPath = entry.slice(3);
|
|
295
|
-
const renamed = rawPath.includes('\0') ? rawPath.split('\0').at(-1) : rawPath;
|
|
296
|
-
parsed.push({ index, worktree, path: renamed });
|
|
297
|
-
}
|
|
298
|
-
return parsed.filter((entry) => !entry.path.startsWith('governance/'));
|
|
299
|
-
}
|
|
300
|
-
async function runCommand(runner, command, cwd) {
|
|
301
|
-
return await runner.run(command, { cwd });
|
|
302
|
-
}
|
|
303
|
-
async function runGit(runner, projectRoot, args) {
|
|
304
|
-
return await runCommand(runner, { executable: 'git', args }, projectRoot);
|
|
305
|
-
}
|
|
306
|
-
async function inspectGitRepository(projectRoot, runner) {
|
|
307
|
-
const rootResult = await runGit(runner, projectRoot, ['rev-parse', '--show-toplevel']);
|
|
308
|
-
if (!commandSucceeded(rootResult)) {
|
|
309
|
-
return {
|
|
310
|
-
insideWorkTree: false,
|
|
311
|
-
root: null,
|
|
312
|
-
branch: null,
|
|
313
|
-
head: null,
|
|
314
|
-
upstream: null,
|
|
315
|
-
status: [],
|
|
316
|
-
remotes: [],
|
|
317
|
-
issues: []
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
const root = path.resolve(rootResult.stdout.trim());
|
|
321
|
-
if (root !== path.resolve(projectRoot)) {
|
|
322
|
-
return {
|
|
323
|
-
insideWorkTree: true,
|
|
324
|
-
root,
|
|
325
|
-
branch: null,
|
|
326
|
-
head: null,
|
|
327
|
-
upstream: null,
|
|
328
|
-
status: [],
|
|
329
|
-
remotes: [],
|
|
330
|
-
issues: [`Git repository root ${root} does not match Liftoff project root ${projectRoot}.`]
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
const branchResult = await runGit(runner, projectRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
334
|
-
const branch = commandSucceeded(branchResult) ? branchResult.stdout.trim() : null;
|
|
335
|
-
const headResult = await runGit(runner, projectRoot, ['rev-parse', '--verify', 'HEAD']);
|
|
336
|
-
const head = commandSucceeded(headResult) ? headResult.stdout.trim() : null;
|
|
337
|
-
const upstreamResult = await runGit(runner, projectRoot, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
|
|
338
|
-
const upstream = commandSucceeded(upstreamResult) ? upstreamResult.stdout.trim() : null;
|
|
339
|
-
const statusResult = await runGit(runner, projectRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);
|
|
340
|
-
const status = commandSucceeded(statusResult) ? parsePorcelainStatus(statusResult.stdout) : [];
|
|
341
|
-
const remoteResult = await runGit(runner, projectRoot, ['remote', '-v']);
|
|
342
|
-
const remotes = commandSucceeded(remoteResult) ? parseRemotes(remoteResult.stdout) : [];
|
|
343
|
-
const issues = [];
|
|
344
|
-
if (!branch) {
|
|
345
|
-
issues.push('Detached HEAD or unborn branch cannot be reviewed for deterministic governance commit/push.');
|
|
346
|
-
}
|
|
347
|
-
if (!commandSucceeded(statusResult)) {
|
|
348
|
-
issues.push(commandFailure(statusResult));
|
|
349
|
-
}
|
|
350
|
-
if (status.some((entry) => entry.index === 'D' || entry.worktree === 'D')) {
|
|
351
|
-
issues.push('Unknown file deletion is outside the reviewed initial commit plan.');
|
|
352
|
-
}
|
|
353
|
-
return { insideWorkTree: true, root, branch, head, upstream, status, remotes, issues };
|
|
354
|
-
}
|
|
355
|
-
function parseRemotes(output) {
|
|
356
|
-
const remotes = new Map();
|
|
357
|
-
for (const line of output.split(/\r?\n/u)) {
|
|
358
|
-
const match = line.match(/^(\S+)\s+(\S+)\s+\((fetch|push)\)$/u);
|
|
359
|
-
if (match?.[3] === 'fetch') {
|
|
360
|
-
remotes.set(match[1], match[2]);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return [...remotes.entries()]
|
|
364
|
-
.map(([name, url]) => ({ name, url }))
|
|
365
|
-
.sort((left, right) => left.name.localeCompare(right.name, 'en'));
|
|
366
|
-
}
|
|
367
|
-
function gitStatusDigest(inspection) {
|
|
368
|
-
return canonicalSha256({
|
|
369
|
-
branch: inspection.branch,
|
|
370
|
-
head: inspection.head,
|
|
371
|
-
status: inspection.status,
|
|
372
|
-
remotes: inspection.remotes
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
async function remoteHead(runner, projectRoot, remote, branch) {
|
|
376
|
-
const result = await runGit(runner, projectRoot, ['ls-remote', '--heads', remote, branch]);
|
|
377
|
-
if (!commandSucceeded(result)) {
|
|
378
|
-
throw new Error(commandFailure(result));
|
|
379
|
-
}
|
|
380
|
-
const first = result.stdout.trim().split(/\s+/u)[0];
|
|
381
|
-
return first && /^[a-f0-9]{40}$/u.test(first) ? first : null;
|
|
382
|
-
}
|
|
383
|
-
async function isAncestor(runner, projectRoot, ancestor, descendant) {
|
|
384
|
-
const result = await runGit(runner, projectRoot, ['merge-base', '--is-ancestor', ancestor, descendant]);
|
|
385
|
-
return result.status === 0;
|
|
386
|
-
}
|
|
387
|
-
function defaultBranch(state) {
|
|
388
|
-
return state.repository.defaultBranch || 'main';
|
|
389
|
-
}
|
|
390
|
-
async function gitCommitOperations(inspection, phase, runner) {
|
|
391
|
-
const git = await inspectGitRepository(inspection.projectRoot, runner);
|
|
392
|
-
if (git.issues.length > 0) {
|
|
393
|
-
throw new Error(git.issues.join(' '));
|
|
394
|
-
}
|
|
395
|
-
const branch = defaultBranch(inspection.state);
|
|
396
|
-
if (!git.insideWorkTree) {
|
|
397
|
-
const files = await allProjectFiles(inspection.projectRoot);
|
|
398
|
-
return [
|
|
399
|
-
operation({
|
|
400
|
-
adapter: 'git',
|
|
401
|
-
actionId: 'git.init',
|
|
402
|
-
mutationClass: 'git-commit',
|
|
403
|
-
phaseId: phase.id,
|
|
404
|
-
inputs: { branch },
|
|
405
|
-
destination: transitionDestination('local', '.git'),
|
|
406
|
-
remote: false,
|
|
407
|
-
destructive: false
|
|
408
|
-
}),
|
|
409
|
-
operation({
|
|
410
|
-
adapter: 'git',
|
|
411
|
-
actionId: 'git.add-reviewed',
|
|
412
|
-
mutationClass: 'git-commit',
|
|
413
|
-
phaseId: phase.id,
|
|
414
|
-
inputs: { paths: files, statusDigest: canonicalSha256(files) },
|
|
415
|
-
destination: transitionDestination('local', 'git-index'),
|
|
416
|
-
remote: false,
|
|
417
|
-
destructive: false
|
|
418
|
-
}),
|
|
419
|
-
operation({
|
|
420
|
-
adapter: 'git',
|
|
421
|
-
actionId: 'git.commit-reviewed',
|
|
422
|
-
mutationClass: 'git-commit',
|
|
423
|
-
phaseId: phase.id,
|
|
424
|
-
inputs: {
|
|
425
|
-
message: 'Initial Liftoff baseline',
|
|
426
|
-
branch,
|
|
427
|
-
paths: files
|
|
428
|
-
},
|
|
429
|
-
destination: transitionDestination('local', branch, { ref: branch }),
|
|
430
|
-
remote: false,
|
|
431
|
-
destructive: false
|
|
432
|
-
})
|
|
433
|
-
];
|
|
434
|
-
}
|
|
435
|
-
if (git.branch !== branch) {
|
|
436
|
-
throw new Error(`Expected branch ${branch}, found ${git.branch ?? 'detached'}.`);
|
|
437
|
-
}
|
|
438
|
-
if (git.upstream) {
|
|
439
|
-
const divergence = await runGit(runner, inspection.projectRoot, ['rev-list', '--left-right', '--count', `${git.upstream}...HEAD`]);
|
|
440
|
-
if (!commandSucceeded(divergence)) {
|
|
441
|
-
throw new Error(commandFailure(divergence));
|
|
442
|
-
}
|
|
443
|
-
const [behind, ahead] = divergence.stdout.trim().split(/\s+/u).map((value) => Number.parseInt(value, 10));
|
|
444
|
-
if ((behind ?? 0) > 0 || (ahead ?? 0) > 0) {
|
|
445
|
-
throw new Error(`Branch ${branch} diverges from ${git.upstream}; refusing commit before explicit reconciliation.`);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
if (git.status.length === 0 && git.head) {
|
|
449
|
-
return [
|
|
450
|
-
operation({
|
|
451
|
-
adapter: 'git',
|
|
452
|
-
actionId: 'git.verify-existing-commit',
|
|
453
|
-
mutationClass: 'read-worktree',
|
|
454
|
-
phaseId: phase.id,
|
|
455
|
-
inputs: { head: git.head, branch, statusDigest: gitStatusDigest(git) },
|
|
456
|
-
destination: transitionDestination('local', branch, { ref: branch }),
|
|
457
|
-
remote: false,
|
|
458
|
-
destructive: false
|
|
459
|
-
})
|
|
460
|
-
];
|
|
461
|
-
}
|
|
462
|
-
const paths = git.status.map((entry) => entry.path).sort((left, right) => left.localeCompare(right, 'en'));
|
|
463
|
-
return [
|
|
464
|
-
operation({
|
|
465
|
-
adapter: 'git',
|
|
466
|
-
actionId: 'git.add-reviewed',
|
|
467
|
-
mutationClass: 'git-commit',
|
|
468
|
-
phaseId: phase.id,
|
|
469
|
-
inputs: { paths, statusDigest: gitStatusDigest(git) },
|
|
470
|
-
destination: transitionDestination('local', 'git-index'),
|
|
471
|
-
remote: false,
|
|
472
|
-
destructive: false
|
|
473
|
-
}),
|
|
474
|
-
operation({
|
|
475
|
-
adapter: 'git',
|
|
476
|
-
actionId: 'git.commit-reviewed',
|
|
477
|
-
mutationClass: 'git-commit',
|
|
478
|
-
phaseId: phase.id,
|
|
479
|
-
inputs: {
|
|
480
|
-
message: 'Initial Liftoff baseline',
|
|
481
|
-
branch,
|
|
482
|
-
paths
|
|
483
|
-
},
|
|
484
|
-
destination: transitionDestination('local', branch, { ref: branch }),
|
|
485
|
-
remote: false,
|
|
486
|
-
destructive: false
|
|
487
|
-
})
|
|
488
|
-
];
|
|
489
|
-
}
|
|
490
|
-
async function gitPushOperations(inspection, phase, runner) {
|
|
491
|
-
const git = await inspectGitRepository(inspection.projectRoot, runner);
|
|
492
|
-
if (git.issues.length > 0) {
|
|
493
|
-
throw new Error(git.issues.join(' '));
|
|
494
|
-
}
|
|
495
|
-
if (!git.insideWorkTree || !git.head) {
|
|
496
|
-
throw new Error('A reviewed local commit is required before push.');
|
|
497
|
-
}
|
|
498
|
-
const branch = defaultBranch(inspection.state);
|
|
499
|
-
if (git.branch !== branch) {
|
|
500
|
-
throw new Error(`Expected branch ${branch}, found ${git.branch ?? 'detached'}.`);
|
|
501
|
-
}
|
|
502
|
-
if (git.status.length > 0) {
|
|
503
|
-
throw new Error(`Worktree has unreviewed changes outside governance/plans: ${git.status.map((entry) => entry.path).join(', ')}.`);
|
|
504
|
-
}
|
|
505
|
-
const remoteNames = new Set(git.remotes.map((remote) => remote.name));
|
|
506
|
-
if (!remoteNames.has('origin') || git.remotes.length !== 1) {
|
|
507
|
-
throw new Error(`Expected exactly one approved origin remote; found ${git.remotes.map((remote) => remote.name).join(', ') || 'none'}.`);
|
|
508
|
-
}
|
|
509
|
-
const remoteSha = await remoteHead(runner, inspection.projectRoot, 'origin', branch);
|
|
510
|
-
if (remoteSha && remoteSha !== git.head && !(await isAncestor(runner, inspection.projectRoot, remoteSha, git.head))) {
|
|
511
|
-
throw new Error('Remote branch is not a fast-forward target; force, reset, rebase, and non-fast-forward push are forbidden.');
|
|
512
|
-
}
|
|
513
|
-
return [
|
|
514
|
-
operation({
|
|
515
|
-
adapter: 'git',
|
|
516
|
-
actionId: remoteSha === git.head ? 'git.verify-existing-push' : 'git.push-approved-ref',
|
|
517
|
-
mutationClass: remoteSha === git.head ? 'github-read' : 'git-push',
|
|
518
|
-
phaseId: phase.id,
|
|
519
|
-
inputs: {
|
|
520
|
-
remote: 'origin',
|
|
521
|
-
branch,
|
|
522
|
-
localHead: git.head,
|
|
523
|
-
remoteHead: remoteSha,
|
|
524
|
-
statusDigest: gitStatusDigest(git)
|
|
525
|
-
},
|
|
526
|
-
destination: transitionDestination('repository', inspection.state.repository.name, {
|
|
527
|
-
repository: inspection.state.repository.name,
|
|
528
|
-
ref: `refs/heads/${branch}`
|
|
529
|
-
}),
|
|
530
|
-
remote: true,
|
|
531
|
-
destructive: false
|
|
532
|
-
})
|
|
533
|
-
];
|
|
534
|
-
}
|
|
535
|
-
async function phaseOperations(inspection, phase, runner, createdAt) {
|
|
536
|
-
const statePath = activationStatePathParts();
|
|
537
|
-
const baseEvidencePath = evidencePathParts(`${phase.id}-${safeTimestamp(createdAt)}`);
|
|
538
|
-
const writeOps = (includeEvidence = true) => [
|
|
539
|
-
...(includeEvidence ? [evidenceWriteOperation(phase, baseEvidencePath)] : []),
|
|
540
|
-
stateWriteOperation(phase)
|
|
541
|
-
];
|
|
542
|
-
switch (phase.id) {
|
|
543
|
-
case 'seed-valid':
|
|
544
|
-
return [
|
|
545
|
-
operation({
|
|
546
|
-
adapter: 'selected-spec-workflow',
|
|
547
|
-
actionId: 'openspec.seed.validate',
|
|
548
|
-
mutationClass: 'read-worktree',
|
|
549
|
-
phaseId: phase.id,
|
|
550
|
-
inputs: { changeName: generatedSeedChangeName(inspection.manifest) },
|
|
551
|
-
destination: transitionDestination('local', inspection.projectRoot),
|
|
552
|
-
remote: false,
|
|
553
|
-
destructive: false
|
|
554
|
-
}),
|
|
555
|
-
...writeOps()
|
|
556
|
-
];
|
|
557
|
-
case 'seed-verified':
|
|
558
|
-
return [
|
|
559
|
-
operation({
|
|
560
|
-
adapter: 'selected-spec-workflow',
|
|
561
|
-
actionId: 'openspec.seed.baseline-verify',
|
|
562
|
-
mutationClass: 'read-worktree',
|
|
563
|
-
phaseId: phase.id,
|
|
564
|
-
inputs: { changeName: generatedSeedChangeName(inspection.manifest) },
|
|
565
|
-
destination: transitionDestination('local', inspection.projectRoot),
|
|
566
|
-
remote: false,
|
|
567
|
-
destructive: false
|
|
568
|
-
}),
|
|
569
|
-
...writeOps()
|
|
570
|
-
];
|
|
571
|
-
case 'seed-archived':
|
|
572
|
-
return [
|
|
573
|
-
operation({
|
|
574
|
-
adapter: 'selected-spec-workflow',
|
|
575
|
-
actionId: 'openspec.seed.archive',
|
|
576
|
-
mutationClass: 'write-openspec-seed',
|
|
577
|
-
phaseId: phase.id,
|
|
578
|
-
inputs: { changeName: generatedSeedChangeName(inspection.manifest) },
|
|
579
|
-
destination: transitionDestination('local', 'openspec/changes'),
|
|
580
|
-
remote: false,
|
|
581
|
-
destructive: false
|
|
582
|
-
}),
|
|
583
|
-
...writeOps()
|
|
584
|
-
];
|
|
585
|
-
case 'committed':
|
|
586
|
-
return [...await gitCommitOperations(inspection, phase, runner), ...writeOps()];
|
|
587
|
-
case 'pushed':
|
|
588
|
-
return [...await gitPushOperations(inspection, phase, runner), ...writeOps()];
|
|
589
|
-
case 'phase-0-complete':
|
|
590
|
-
return [
|
|
591
|
-
operation({
|
|
592
|
-
adapter: 'github',
|
|
593
|
-
actionId: 'github.phase0.discover',
|
|
594
|
-
mutationClass: 'github-read',
|
|
595
|
-
phaseId: phase.id,
|
|
596
|
-
inputs: { repository: inspection.state.repository.name },
|
|
597
|
-
destination: transitionDestination('repository', inspection.state.repository.name, {
|
|
598
|
-
repository: inspection.state.repository.name
|
|
599
|
-
}),
|
|
600
|
-
remote: true,
|
|
601
|
-
destructive: false
|
|
602
|
-
}),
|
|
603
|
-
operation({
|
|
604
|
-
adapter: 'azure-opentofu',
|
|
605
|
-
actionId: 'azure.phase0.discover',
|
|
606
|
-
mutationClass: 'azure-read',
|
|
607
|
-
phaseId: phase.id,
|
|
608
|
-
inputs: { cloud: 'cloud' in inspection.manifest.project.workload ? inspection.manifest.project.workload.cloud : 'power-platform' },
|
|
609
|
-
destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
|
|
610
|
-
subscriptionId: 'phase-0-discovered-subscription'
|
|
611
|
-
}),
|
|
612
|
-
remote: true,
|
|
613
|
-
destructive: false
|
|
614
|
-
}),
|
|
615
|
-
...writeOps()
|
|
616
|
-
];
|
|
617
|
-
case 'activation-approved': {
|
|
618
|
-
const actionId = inspection.manifest.project.specWorkflow === 'openspec'
|
|
619
|
-
? 'openspec.governance.create-change'
|
|
620
|
-
: 'spec-kit.governance.create-change';
|
|
621
|
-
return [
|
|
622
|
-
operation({
|
|
623
|
-
adapter: 'selected-spec-workflow',
|
|
624
|
-
actionId,
|
|
625
|
-
mutationClass: 'write-openspec-governance',
|
|
626
|
-
phaseId: phase.id,
|
|
627
|
-
inputs: { workflowKind: inspection.manifest.project.specWorkflow },
|
|
628
|
-
destination: transitionDestination('local', 'governance change'),
|
|
629
|
-
remote: false,
|
|
630
|
-
destructive: false
|
|
631
|
-
}),
|
|
632
|
-
stateWriteOperation(phase)
|
|
633
|
-
];
|
|
634
|
-
}
|
|
635
|
-
case 'credential-ready':
|
|
636
|
-
return [
|
|
637
|
-
operation({
|
|
638
|
-
adapter: 'github',
|
|
639
|
-
actionId: 'github.credential.verify-policy',
|
|
640
|
-
mutationClass: 'github-read',
|
|
641
|
-
phaseId: phase.id,
|
|
642
|
-
inputs: { policyPathParts: credentialPolicyPathParts },
|
|
643
|
-
destination: transitionDestination('repository', inspection.state.repository.name, {
|
|
644
|
-
repository: inspection.state.repository.name
|
|
645
|
-
}),
|
|
646
|
-
remote: true,
|
|
647
|
-
destructive: false
|
|
648
|
-
}),
|
|
649
|
-
...writeOps()
|
|
650
|
-
];
|
|
651
|
-
case 'provider-ready':
|
|
652
|
-
return [
|
|
653
|
-
operation({
|
|
654
|
-
adapter: 'azure-opentofu',
|
|
655
|
-
actionId: 'azure.provider.ensure-ready',
|
|
656
|
-
mutationClass: 'azure-provider-register',
|
|
657
|
-
phaseId: phase.id,
|
|
658
|
-
inputs: { retainedCapability: true },
|
|
659
|
-
destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
|
|
660
|
-
subscriptionId: 'phase-0-discovered-subscription'
|
|
661
|
-
}),
|
|
662
|
-
remote: true,
|
|
663
|
-
destructive: false
|
|
664
|
-
}),
|
|
665
|
-
...writeOps()
|
|
666
|
-
];
|
|
667
|
-
case 'state-path-selected':
|
|
668
|
-
return [
|
|
669
|
-
operation({
|
|
670
|
-
adapter: 'azure-opentofu',
|
|
671
|
-
actionId: 'azure.state-path.select',
|
|
672
|
-
mutationClass: 'azure-read',
|
|
673
|
-
phaseId: phase.id,
|
|
674
|
-
inputs: { allowed: ['existing-private', 'bootstrap-local'] },
|
|
675
|
-
destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
|
|
676
|
-
subscriptionId: 'phase-0-discovered-subscription'
|
|
677
|
-
}),
|
|
678
|
-
remote: true,
|
|
679
|
-
destructive: false
|
|
680
|
-
}),
|
|
681
|
-
...writeOps()
|
|
682
|
-
];
|
|
683
|
-
case 'existing-private-path':
|
|
684
|
-
return [
|
|
685
|
-
operation({
|
|
686
|
-
adapter: 'azure-opentofu',
|
|
687
|
-
actionId: 'azure.existing-private-path.verify',
|
|
688
|
-
mutationClass: 'azure-read',
|
|
689
|
-
phaseId: phase.id,
|
|
690
|
-
inputs: { statePath: 'existing-private' },
|
|
691
|
-
destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
|
|
692
|
-
subscriptionId: 'phase-0-discovered-subscription'
|
|
693
|
-
}),
|
|
694
|
-
remote: true,
|
|
695
|
-
destructive: false
|
|
696
|
-
}),
|
|
697
|
-
...writeOps()
|
|
698
|
-
];
|
|
699
|
-
case 'bootstrap-local':
|
|
700
|
-
return [
|
|
701
|
-
operation({
|
|
702
|
-
adapter: 'azure-opentofu',
|
|
703
|
-
actionId: 'azure.bootstrap-local.apply',
|
|
704
|
-
mutationClass: 'azure-network-provision',
|
|
705
|
-
phaseId: phase.id,
|
|
706
|
-
inputs: { boundedLocalBootstrap: true },
|
|
707
|
-
destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
|
|
708
|
-
subscriptionId: 'phase-0-discovered-subscription'
|
|
709
|
-
}),
|
|
710
|
-
remote: true,
|
|
711
|
-
destructive: false
|
|
712
|
-
}),
|
|
713
|
-
operation({
|
|
714
|
-
adapter: 'github',
|
|
715
|
-
actionId: 'github.bootstrap-local.configure',
|
|
716
|
-
mutationClass: 'github-write',
|
|
717
|
-
phaseId: phase.id,
|
|
718
|
-
inputs: { repository: inspection.state.repository.name },
|
|
719
|
-
destination: transitionDestination('repository', inspection.state.repository.name, {
|
|
720
|
-
repository: inspection.state.repository.name
|
|
721
|
-
}),
|
|
722
|
-
remote: true,
|
|
723
|
-
destructive: false
|
|
724
|
-
}),
|
|
725
|
-
...writeOps()
|
|
726
|
-
];
|
|
727
|
-
case 'runner-ready':
|
|
728
|
-
return [
|
|
729
|
-
operation({
|
|
730
|
-
adapter: 'github',
|
|
731
|
-
actionId: 'github.runner.ensure-ready',
|
|
732
|
-
mutationClass: 'github-write',
|
|
733
|
-
phaseId: phase.id,
|
|
734
|
-
inputs: { restrictedRunner: true },
|
|
735
|
-
destination: transitionDestination('repository', inspection.state.repository.name, {
|
|
736
|
-
repository: inspection.state.repository.name
|
|
737
|
-
}),
|
|
738
|
-
remote: true,
|
|
739
|
-
destructive: false
|
|
740
|
-
}),
|
|
741
|
-
...writeOps()
|
|
742
|
-
];
|
|
743
|
-
case 'private-backend-proof':
|
|
744
|
-
return [
|
|
745
|
-
operation({
|
|
746
|
-
adapter: 'github',
|
|
747
|
-
actionId: 'github.runner.backend-proof',
|
|
748
|
-
mutationClass: 'github-read',
|
|
749
|
-
phaseId: phase.id,
|
|
750
|
-
inputs: { requiredConclusion: 'success' },
|
|
751
|
-
destination: transitionDestination('repository', inspection.state.repository.name, {
|
|
752
|
-
repository: inspection.state.repository.name
|
|
753
|
-
}),
|
|
754
|
-
remote: true,
|
|
755
|
-
destructive: false
|
|
756
|
-
}),
|
|
757
|
-
...writeOps()
|
|
758
|
-
];
|
|
759
|
-
case 'remote-import-verified':
|
|
760
|
-
return [
|
|
761
|
-
operation({
|
|
762
|
-
adapter: 'azure-opentofu',
|
|
763
|
-
actionId: 'azure.remote-import.verify',
|
|
764
|
-
mutationClass: 'azure-state-import',
|
|
765
|
-
phaseId: phase.id,
|
|
766
|
-
inputs: { noChangePlanRequired: true },
|
|
767
|
-
destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
|
|
768
|
-
subscriptionId: 'phase-0-discovered-subscription'
|
|
769
|
-
}),
|
|
770
|
-
remote: true,
|
|
771
|
-
destructive: false
|
|
772
|
-
}),
|
|
773
|
-
...writeOps()
|
|
774
|
-
];
|
|
775
|
-
case 'remote-ready':
|
|
776
|
-
return [
|
|
777
|
-
operation({
|
|
778
|
-
adapter: 'azure-opentofu',
|
|
779
|
-
actionId: 'azure.remote-ready.verify',
|
|
780
|
-
mutationClass: 'azure-read',
|
|
781
|
-
phaseId: phase.id,
|
|
782
|
-
inputs: { retainBootstrapStateForDays: 30 },
|
|
783
|
-
destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
|
|
784
|
-
subscriptionId: 'phase-0-discovered-subscription'
|
|
785
|
-
}),
|
|
786
|
-
remote: true,
|
|
787
|
-
destructive: false
|
|
788
|
-
}),
|
|
789
|
-
...writeOps()
|
|
790
|
-
];
|
|
791
|
-
case 'application-foundation':
|
|
792
|
-
return [
|
|
793
|
-
operation({
|
|
794
|
-
adapter: 'azure-opentofu',
|
|
795
|
-
actionId: 'azure.application-foundation.apply',
|
|
796
|
-
mutationClass: 'azure-resource-provision',
|
|
797
|
-
phaseId: phase.id,
|
|
798
|
-
inputs: { noProviderUnregisterRollback: true },
|
|
799
|
-
destination: transitionDestination('subscription', 'phase-0-discovered-subscription', {
|
|
800
|
-
subscriptionId: 'phase-0-discovered-subscription'
|
|
801
|
-
}),
|
|
802
|
-
remote: true,
|
|
803
|
-
destructive: false
|
|
804
|
-
}),
|
|
805
|
-
...writeOps()
|
|
806
|
-
];
|
|
807
|
-
case 'workflow-source-ready':
|
|
808
|
-
return [
|
|
809
|
-
operation({
|
|
810
|
-
adapter: 'local-state',
|
|
811
|
-
actionId: 'local.workflow-source.write',
|
|
812
|
-
mutationClass: 'write-workflows',
|
|
813
|
-
phaseId: phase.id,
|
|
814
|
-
inputs: { path: '.github/workflows' },
|
|
815
|
-
destination: transitionDestination('local', '.github/workflows', {
|
|
816
|
-
pathParts: ['.github', 'workflows']
|
|
817
|
-
}),
|
|
818
|
-
remote: false,
|
|
819
|
-
destructive: false
|
|
820
|
-
}),
|
|
821
|
-
operation({
|
|
822
|
-
adapter: 'local-state',
|
|
823
|
-
actionId: 'local.ruleset-source.write',
|
|
824
|
-
mutationClass: 'write-ruleset-source',
|
|
825
|
-
phaseId: phase.id,
|
|
826
|
-
inputs: { path: '.github/rulesets' },
|
|
827
|
-
destination: transitionDestination('local', '.github/rulesets', {
|
|
828
|
-
pathParts: ['.github', 'rulesets']
|
|
829
|
-
}),
|
|
830
|
-
remote: false,
|
|
831
|
-
destructive: false
|
|
832
|
-
}),
|
|
833
|
-
...writeOps()
|
|
834
|
-
];
|
|
835
|
-
case 'dev-proof':
|
|
836
|
-
return [
|
|
837
|
-
operation({
|
|
838
|
-
adapter: 'github',
|
|
839
|
-
actionId: 'github.checks.dev-proof',
|
|
840
|
-
mutationClass: 'github-read',
|
|
841
|
-
phaseId: phase.id,
|
|
842
|
-
inputs: { acceptedConclusions: ['success'] },
|
|
843
|
-
destination: transitionDestination('repository', inspection.state.repository.name, {
|
|
844
|
-
repository: inspection.state.repository.name
|
|
845
|
-
}),
|
|
846
|
-
remote: true,
|
|
847
|
-
destructive: false
|
|
848
|
-
}),
|
|
849
|
-
...writeOps()
|
|
850
|
-
];
|
|
851
|
-
case 'staging-qualified':
|
|
852
|
-
return [
|
|
853
|
-
operation({
|
|
854
|
-
adapter: 'github',
|
|
855
|
-
actionId: 'github.checks.staging',
|
|
856
|
-
mutationClass: 'github-read',
|
|
857
|
-
phaseId: phase.id,
|
|
858
|
-
inputs: { acceptedConclusions: ['success'] },
|
|
859
|
-
destination: transitionDestination('repository', inspection.state.repository.name, {
|
|
860
|
-
repository: inspection.state.repository.name
|
|
861
|
-
}),
|
|
862
|
-
remote: true,
|
|
863
|
-
destructive: false
|
|
864
|
-
}),
|
|
865
|
-
...writeOps()
|
|
866
|
-
];
|
|
867
|
-
case 'production-rehearsed':
|
|
868
|
-
return [
|
|
869
|
-
operation({
|
|
870
|
-
adapter: 'github',
|
|
871
|
-
actionId: 'github.checks.production-rehearsal',
|
|
872
|
-
mutationClass: 'github-read',
|
|
873
|
-
phaseId: phase.id,
|
|
874
|
-
inputs: { acceptedConclusions: ['success'] },
|
|
875
|
-
destination: transitionDestination('repository', inspection.state.repository.name, {
|
|
876
|
-
repository: inspection.state.repository.name
|
|
877
|
-
}),
|
|
878
|
-
remote: true,
|
|
879
|
-
destructive: false
|
|
880
|
-
}),
|
|
881
|
-
...writeOps()
|
|
882
|
-
];
|
|
883
|
-
case 'green-red-proof':
|
|
884
|
-
return [
|
|
885
|
-
operation({
|
|
886
|
-
adapter: 'github',
|
|
887
|
-
actionId: 'github.checks.green-red-proof',
|
|
888
|
-
mutationClass: 'github-read',
|
|
889
|
-
phaseId: phase.id,
|
|
890
|
-
inputs: { required: ['green-success', 'deliberate-red-failure'] },
|
|
891
|
-
destination: transitionDestination('repository', inspection.state.repository.name, {
|
|
892
|
-
repository: inspection.state.repository.name
|
|
893
|
-
}),
|
|
894
|
-
remote: true,
|
|
895
|
-
destructive: false
|
|
896
|
-
}),
|
|
897
|
-
...writeOps()
|
|
898
|
-
];
|
|
899
|
-
case 'enforcement-approved':
|
|
900
|
-
return [stateWriteOperation(phase)];
|
|
901
|
-
case 'rulesets-applied':
|
|
902
|
-
return [
|
|
903
|
-
operation({
|
|
904
|
-
adapter: 'github',
|
|
905
|
-
actionId: 'github.ruleset.apply',
|
|
906
|
-
mutationClass: 'github-ruleset-write',
|
|
907
|
-
phaseId: phase.id,
|
|
908
|
-
inputs: { sourceDigest: rulesetSourceDigestFromEvidence(inspection) ?? 'missing' },
|
|
909
|
-
destination: transitionDestination('repository', inspection.state.repository.name, {
|
|
910
|
-
repository: inspection.state.repository.name
|
|
911
|
-
}),
|
|
912
|
-
remote: true,
|
|
913
|
-
destructive: false
|
|
914
|
-
}),
|
|
915
|
-
operation({
|
|
916
|
-
adapter: 'github',
|
|
917
|
-
actionId: 'github.ruleset.readback',
|
|
918
|
-
mutationClass: 'github-read',
|
|
919
|
-
phaseId: phase.id,
|
|
920
|
-
inputs: { sourceDigest: rulesetSourceDigestFromEvidence(inspection) ?? 'missing' },
|
|
921
|
-
destination: transitionDestination('repository', inspection.state.repository.name, {
|
|
922
|
-
repository: inspection.state.repository.name
|
|
923
|
-
}),
|
|
924
|
-
remote: true,
|
|
925
|
-
destructive: false
|
|
926
|
-
}),
|
|
927
|
-
...writeOps()
|
|
928
|
-
];
|
|
929
|
-
case 'live-readback':
|
|
930
|
-
return [
|
|
931
|
-
operation({
|
|
932
|
-
adapter: 'github',
|
|
933
|
-
actionId: 'github.ruleset.readback',
|
|
934
|
-
mutationClass: 'github-read',
|
|
935
|
-
phaseId: phase.id,
|
|
936
|
-
inputs: { sourceDigest: rulesetSourceDigestFromEvidence(inspection) ?? 'missing' },
|
|
937
|
-
destination: transitionDestination('repository', inspection.state.repository.name, {
|
|
938
|
-
repository: inspection.state.repository.name
|
|
939
|
-
}),
|
|
940
|
-
remote: true,
|
|
941
|
-
destructive: false
|
|
942
|
-
}),
|
|
943
|
-
...writeOps()
|
|
944
|
-
];
|
|
945
|
-
case 'bootstrap-state-disposed': {
|
|
946
|
-
const paths = disposalPathInputs(inspection.state.bootstrapState);
|
|
947
|
-
return [
|
|
948
|
-
operation({
|
|
949
|
-
adapter: 'local-state',
|
|
950
|
-
actionId: 'local.bootstrap-state.dispose',
|
|
951
|
-
mutationClass: 'delete-local-state',
|
|
952
|
-
phaseId: phase.id,
|
|
953
|
-
inputs: paths,
|
|
954
|
-
destination: transitionDestination('local', 'bootstrap-state', {
|
|
955
|
-
pathParts: statePath
|
|
956
|
-
}),
|
|
957
|
-
remote: false,
|
|
958
|
-
destructive: true
|
|
959
|
-
}),
|
|
960
|
-
...writeOps()
|
|
961
|
-
];
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
function disposalPathInputs(retention) {
|
|
966
|
-
return {
|
|
967
|
-
encryptedStatePathParts: retention?.encryptedStatePathParts ?? [],
|
|
968
|
-
encryptionKeyPathParts: retention?.encryptionKeyPathParts ?? []
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
function rulesetSourceDigestFromEvidence(inspection) {
|
|
972
|
-
const record = latestRecordWithPayload(inspection, 'workflow-source-ready');
|
|
973
|
-
if (!record || !isRecord(record.payload)) {
|
|
974
|
-
return null;
|
|
975
|
-
}
|
|
976
|
-
if (record.payload.kind !== workflowSourcePayloadKind) {
|
|
977
|
-
return null;
|
|
978
|
-
}
|
|
979
|
-
const digest = record.payload.rulesetSourceDigest;
|
|
980
|
-
return typeof digest === 'string' && /^[a-f0-9]{64}$/u.test(digest) ? digest : null;
|
|
981
|
-
}
|
|
982
|
-
function latestRecordWithPayload(inspection, phaseId) {
|
|
983
|
-
const records = inspection.evidence.filter((record) => record.header.phaseId === phaseId);
|
|
984
|
-
const selection = selectLatestPhaseEvidence(records, inspection.contexts[phaseId]);
|
|
985
|
-
if (!selection.selected) {
|
|
986
|
-
return null;
|
|
987
|
-
}
|
|
988
|
-
return records.find((record) => record.evidenceId === selection.selected?.evidenceId) ?? null;
|
|
989
|
-
}
|
|
990
|
-
function rollbackPlanForPhase(phase, completed = []) {
|
|
991
|
-
return rollbackPlanFromCompletedOperations(phase.id, phase.rollback.kind, phase.rollback.target, completed);
|
|
992
|
-
}
|
|
993
|
-
export function rollbackPlanFromCompletedOperations(phaseId, strategy, target, completed) {
|
|
994
|
-
const retained = [];
|
|
995
|
-
const cleanupWarnings = [];
|
|
996
|
-
const operations = [];
|
|
997
|
-
for (const op of [...completed].reverse()) {
|
|
998
|
-
if (op.mutationClass === 'azure-provider-register') {
|
|
999
|
-
retained.push(`${op.phaseId}:${op.actionId}:provider-registration`);
|
|
1000
|
-
continue;
|
|
1001
|
-
}
|
|
1002
|
-
if (op.actionId.toLowerCase().includes('unregister')) {
|
|
1003
|
-
cleanupWarnings.push(`Refused to generate provider unregister rollback for ${op.actionId}.`);
|
|
1004
|
-
continue;
|
|
1005
|
-
}
|
|
1006
|
-
if (op.mutationClass === 'github-ruleset-write') {
|
|
1007
|
-
operations.push({
|
|
1008
|
-
adapter: 'github',
|
|
1009
|
-
actionId: 'github.ruleset.disable',
|
|
1010
|
-
mutationClass: 'github-ruleset-write',
|
|
1011
|
-
phaseId,
|
|
1012
|
-
inputs: { fromOperation: op.actionId, cannotExpandScope: true },
|
|
1013
|
-
destination: op.destination,
|
|
1014
|
-
remote: true,
|
|
1015
|
-
destructive: false
|
|
1016
|
-
});
|
|
1017
|
-
continue;
|
|
1018
|
-
}
|
|
1019
|
-
if (op.mutationClass === 'azure-network-provision' || op.mutationClass === 'azure-resource-provision') {
|
|
1020
|
-
operations.push({
|
|
1021
|
-
adapter: 'azure-opentofu',
|
|
1022
|
-
actionId: 'azure.resource.cleanup',
|
|
1023
|
-
mutationClass: op.mutationClass,
|
|
1024
|
-
phaseId,
|
|
1025
|
-
inputs: { fromOperation: op.actionId, noProviderUnregister: true },
|
|
1026
|
-
destination: op.destination,
|
|
1027
|
-
remote: true,
|
|
1028
|
-
destructive: true
|
|
1029
|
-
});
|
|
1030
|
-
continue;
|
|
1031
|
-
}
|
|
1032
|
-
if (op.mutationClass === 'write-local-state' || op.mutationClass === 'delete-local-state') {
|
|
1033
|
-
operations.push({
|
|
1034
|
-
adapter: 'local-state',
|
|
1035
|
-
actionId: 'local.state.rollback',
|
|
1036
|
-
mutationClass: op.mutationClass,
|
|
1037
|
-
phaseId,
|
|
1038
|
-
inputs: { fromOperation: op.actionId },
|
|
1039
|
-
destination: op.destination,
|
|
1040
|
-
remote: false,
|
|
1041
|
-
destructive: op.mutationClass === 'delete-local-state'
|
|
1042
|
-
});
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
return {
|
|
1046
|
-
phaseId,
|
|
1047
|
-
strategy,
|
|
1048
|
-
target,
|
|
1049
|
-
operations,
|
|
1050
|
-
retained,
|
|
1051
|
-
cleanupWarnings
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
|
-
export function planDigestFor(input) {
|
|
1055
|
-
return canonicalSha256({
|
|
1056
|
-
phaseId: input.phase.id,
|
|
1057
|
-
transitionDigest: input.transitionDigest,
|
|
1058
|
-
approvalPlanDigest: input.approvalPlanDigest,
|
|
1059
|
-
operations: input.operations
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
function stateHash(inspection) {
|
|
1063
|
-
return inspection.loadedState?.contentHash ?? null;
|
|
1064
|
-
}
|
|
1065
|
-
function assertNoSecrets(plan) {
|
|
1066
|
-
const scan = detectCredentialLeaks([{
|
|
1067
|
-
source: 'generated-artifact',
|
|
1068
|
-
label: 'governance transition plan',
|
|
1069
|
-
text: canonicalJson(plan)
|
|
1070
|
-
}]);
|
|
1071
|
-
if (scan.status === 'compromised') {
|
|
1072
|
-
throw new Error(`Governance transition plan contains credential-shaped content: ${scan.leaks.map((leak) => leak.pattern).join(', ')}.`);
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
export async function buildSavedTransitionPlan(input) {
|
|
1076
|
-
const phaseId = input.inspection.readiness.nextReadyPhase;
|
|
1077
|
-
if (!phaseId) {
|
|
1078
|
-
return null;
|
|
1079
|
-
}
|
|
1080
|
-
const runner = input.runner ?? new NodeCommandRunner();
|
|
1081
|
-
const phase = phaseById(input.inspection.graph, phaseId);
|
|
1082
|
-
const createdAt = (input.now ?? new Date()).toISOString();
|
|
1083
|
-
const expiresAt = new Date(Date.parse(createdAt) + planLifetimeMs).toISOString();
|
|
1084
|
-
const context = input.inspection.contexts[phaseId];
|
|
1085
|
-
const approvalPlan = transitionPlanForPhase(phase, input.inspection.state, context.transition, input.inspection.projectRoot);
|
|
1086
|
-
const evaluation = evaluateApprovalForTransitionPlan(approvalPlan, input.inspection.approvals, {
|
|
1087
|
-
now: input.now
|
|
1088
|
-
});
|
|
1089
|
-
const operations = await phaseOperations(input.inspection, phase, runner, createdAt);
|
|
1090
|
-
const digest = planDigestFor({
|
|
1091
|
-
phase,
|
|
1092
|
-
transitionDigest: context.transition.transitionDigest,
|
|
1093
|
-
operations,
|
|
1094
|
-
approvalPlanDigest: approvalPlan.planDigest
|
|
1095
|
-
});
|
|
1096
|
-
const plan = validateSavedTransitionPlan({
|
|
1097
|
-
schemaVersion: 1,
|
|
1098
|
-
phaseId,
|
|
1099
|
-
createdAt,
|
|
1100
|
-
expiresAt,
|
|
1101
|
-
identity: input.inspection.state.identity,
|
|
1102
|
-
graphHash: input.inspection.graphHash,
|
|
1103
|
-
stateHash: stateHash(input.inspection),
|
|
1104
|
-
baselineDigest: context.baselineSha,
|
|
1105
|
-
inputDigest: context.inputDigest,
|
|
1106
|
-
transitionDigest: context.transition.transitionDigest,
|
|
1107
|
-
planDigest: digest,
|
|
1108
|
-
mutationClasses: phase.allowedMutations,
|
|
1109
|
-
operations,
|
|
1110
|
-
approval: {
|
|
1111
|
-
gateKind: phase.approvalGate.kind,
|
|
1112
|
-
required: phase.approvalGate.required,
|
|
1113
|
-
evaluation,
|
|
1114
|
-
envelopeId: evaluation.envelopeId,
|
|
1115
|
-
envelopeHash: evaluation.envelopeHash
|
|
1116
|
-
},
|
|
1117
|
-
rollbackPlan: rollbackPlanForPhase(phase),
|
|
1118
|
-
noSecrets: true
|
|
1119
|
-
});
|
|
1120
|
-
assertPlanOperationsAllowed(plan, phase);
|
|
1121
|
-
assertNoSecrets(plan);
|
|
1122
|
-
return plan;
|
|
1123
|
-
}
|
|
1124
|
-
export async function previewApplyNext(input) {
|
|
1125
|
-
let plan = null;
|
|
1126
|
-
const blockers = [];
|
|
1127
|
-
try {
|
|
1128
|
-
plan = await buildSavedTransitionPlan(input);
|
|
1129
|
-
}
|
|
1130
|
-
catch (error) {
|
|
1131
|
-
blockers.push(errorMessage(error));
|
|
1132
|
-
}
|
|
1133
|
-
const nextReadyPhase = input.inspection.readiness.nextReadyPhase;
|
|
1134
|
-
if (!nextReadyPhase || blockers.length > 0 || !plan) {
|
|
1135
|
-
return {
|
|
1136
|
-
schemaVersion: 1,
|
|
1137
|
-
command: 'governance apply-next',
|
|
1138
|
-
projectRoot: input.inspection.projectRoot,
|
|
1139
|
-
execute: input.execute,
|
|
1140
|
-
applied: false,
|
|
1141
|
-
authorized: false,
|
|
1142
|
-
reason: 'blocked',
|
|
1143
|
-
message: blockers[0] ?? 'No phase is ready for execution.',
|
|
1144
|
-
selectedPhase: nextReadyPhase,
|
|
1145
|
-
nextReadyPhase,
|
|
1146
|
-
approval: plan?.approval ?? null,
|
|
1147
|
-
proposedMutations: {
|
|
1148
|
-
local: plan?.mutationClasses.local ?? ['none'],
|
|
1149
|
-
remote: plan?.mutationClasses.remote ?? ['none'],
|
|
1150
|
-
operations: plan?.operations ?? []
|
|
1151
|
-
},
|
|
1152
|
-
savedPlan: null,
|
|
1153
|
-
noWrites: true,
|
|
1154
|
-
blockers: blockers.length > 0 ? blockers : ['No phase is ready for execution.']
|
|
1155
|
-
};
|
|
1156
|
-
}
|
|
1157
|
-
if (plan.approval.evaluation.approvalRequired) {
|
|
1158
|
-
return {
|
|
1159
|
-
schemaVersion: 1,
|
|
1160
|
-
command: 'governance apply-next',
|
|
1161
|
-
projectRoot: input.inspection.projectRoot,
|
|
1162
|
-
execute: input.execute,
|
|
1163
|
-
applied: false,
|
|
1164
|
-
authorized: false,
|
|
1165
|
-
reason: 'approval-required',
|
|
1166
|
-
message: plan.approval.evaluation.reasons.join('; '),
|
|
1167
|
-
selectedPhase: nextReadyPhase,
|
|
1168
|
-
nextReadyPhase,
|
|
1169
|
-
approval: plan.approval,
|
|
1170
|
-
proposedMutations: {
|
|
1171
|
-
local: plan.mutationClasses.local,
|
|
1172
|
-
remote: plan.mutationClasses.remote,
|
|
1173
|
-
operations: plan.operations
|
|
1174
|
-
},
|
|
1175
|
-
savedPlan: null,
|
|
1176
|
-
noWrites: true,
|
|
1177
|
-
blockers: plan.approval.evaluation.reasons
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
return {
|
|
1181
|
-
schemaVersion: 1,
|
|
1182
|
-
command: 'governance apply-next',
|
|
1183
|
-
projectRoot: input.inspection.projectRoot,
|
|
1184
|
-
execute: input.execute,
|
|
1185
|
-
applied: false,
|
|
1186
|
-
authorized: true,
|
|
1187
|
-
reason: input.execute ? 'execute-requested' : 'execute-required',
|
|
1188
|
-
message: input.execute
|
|
1189
|
-
? 'Execution requested; the plan must be saved and revalidated before any mutation.'
|
|
1190
|
-
: 'Preview only. Rerun with --execute to save this plan and execute at most one phase.',
|
|
1191
|
-
selectedPhase: nextReadyPhase,
|
|
1192
|
-
nextReadyPhase,
|
|
1193
|
-
approval: plan.approval,
|
|
1194
|
-
proposedMutations: {
|
|
1195
|
-
local: plan.mutationClasses.local,
|
|
1196
|
-
remote: plan.mutationClasses.remote,
|
|
1197
|
-
operations: plan.operations
|
|
1198
|
-
},
|
|
1199
|
-
savedPlan: null,
|
|
1200
|
-
noWrites: true,
|
|
1201
|
-
blockers: []
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
async function saveTransitionPlan(projectRoot, plan) {
|
|
1205
|
-
const validated = validateSavedTransitionPlan(plan);
|
|
1206
|
-
assertNoSecrets(validated);
|
|
1207
|
-
const pathParts = transitionPlanPathParts(validated);
|
|
1208
|
-
const existing = await readProjectFile(projectRoot, pathParts);
|
|
1209
|
-
if (existing !== undefined) {
|
|
1210
|
-
throw new Error(`Refusing to overwrite existing governance transition plan ${pathParts.join('/')}.`);
|
|
1211
|
-
}
|
|
1212
|
-
const content = `${canonicalJson(validated)}\n`;
|
|
1213
|
-
await applyProjectFileTransaction(projectRoot, [{
|
|
1214
|
-
type: 'write',
|
|
1215
|
-
pathParts,
|
|
1216
|
-
content
|
|
1217
|
-
}]);
|
|
1218
|
-
return { pathParts, digest: canonicalSha256(validated) };
|
|
1219
|
-
}
|
|
1220
|
-
function comparePlanFreshness(saved, fresh) {
|
|
1221
|
-
if (!fresh) {
|
|
1222
|
-
return ['No phase remained ready after saving the transition plan.'];
|
|
1223
|
-
}
|
|
1224
|
-
const issues = [];
|
|
1225
|
-
if (saved.phaseId !== fresh.phaseId) {
|
|
1226
|
-
issues.push(`Ready phase changed from ${saved.phaseId} to ${fresh.phaseId}.`);
|
|
1227
|
-
}
|
|
1228
|
-
for (const field of ['stateHash', 'baselineDigest', 'inputDigest', 'transitionDigest']) {
|
|
1229
|
-
if (saved[field] !== fresh[field]) {
|
|
1230
|
-
issues.push(`${field} changed after plan save.`);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
if (canonicalSha256(saved.operations) !== canonicalSha256(fresh.operations)) {
|
|
1234
|
-
issues.push('Proposed operations changed after plan save.');
|
|
1235
|
-
}
|
|
1236
|
-
if (saved.approval.evaluation.approvalRequired || fresh.approval.evaluation.approvalRequired) {
|
|
1237
|
-
issues.push('Approval was not valid immediately before execution.');
|
|
1238
|
-
}
|
|
1239
|
-
return issues;
|
|
1240
|
-
}
|
|
1241
|
-
function evidenceHeaderFor(input) {
|
|
1242
|
-
const context = input.inspection.contexts[input.phase.id];
|
|
1243
|
-
return validateEvidenceHeader({
|
|
1244
|
-
schemaVersion: input.inspection.state.identity.evidenceHeaderSchemaVersion,
|
|
1245
|
-
repositoryId: input.inspection.state.repository.id,
|
|
1246
|
-
identity: input.inspection.state.identity,
|
|
1247
|
-
phaseGraphHash: input.inspection.state.identity.phaseGraphHash,
|
|
1248
|
-
phaseId: input.phase.id,
|
|
1249
|
-
phaseContractDigest: phaseContractDigests(input.inspection.graph)[input.phase.id],
|
|
1250
|
-
inputDigest: input.plan.inputDigest,
|
|
1251
|
-
baselineSha: input.plan.baselineDigest,
|
|
1252
|
-
transition: context.transition,
|
|
1253
|
-
producedAt: input.now.toISOString(),
|
|
1254
|
-
producer: engineProducer,
|
|
1255
|
-
result: input.result
|
|
1256
|
-
});
|
|
1257
|
-
}
|
|
1258
|
-
function appendUnique(values, value) {
|
|
1259
|
-
if (value === null || values.includes(value)) {
|
|
1260
|
-
return [...values];
|
|
1261
|
-
}
|
|
1262
|
-
return [...values, value];
|
|
1263
|
-
}
|
|
1264
|
-
function nextStateForOutcome(input) {
|
|
1265
|
-
const base = cloneState(input.override ?? input.inspection.state);
|
|
1266
|
-
if (input.inspection.sourceOfTruth.status === 'selected' &&
|
|
1267
|
-
input.inspection.sourceOfTruth.recordActiveChangeOnNextMutation) {
|
|
1268
|
-
const selected = stateWithSelectedActiveChange(base, input.inspection.sourceOfTruth.selected);
|
|
1269
|
-
base.activeChange = selected.activeChange;
|
|
1270
|
-
}
|
|
1271
|
-
const phaseState = input.resultState === 'approved' ? 'approved' : input.resultState;
|
|
1272
|
-
base.phases[input.phase.id] = {
|
|
1273
|
-
state: phaseState,
|
|
1274
|
-
updatedAt: input.now.toISOString(),
|
|
1275
|
-
evidence: input.evidenceReference
|
|
1276
|
-
? [...base.phases[input.phase.id].evidence, input.evidenceReference]
|
|
1277
|
-
: base.phases[input.phase.id].evidence,
|
|
1278
|
-
approvals: appendUnique(base.phases[input.phase.id].approvals, input.plan.approval.envelopeId),
|
|
1279
|
-
blockers: input.blocker ? [input.blocker] : []
|
|
1280
|
-
};
|
|
1281
|
-
base.updatedAt = input.now.toISOString();
|
|
1282
|
-
return validateUserActivationState(base);
|
|
1283
|
-
}
|
|
1284
|
-
function blockedState(input) {
|
|
1285
|
-
const base = cloneState(input.inspection.state);
|
|
1286
|
-
base.phases[input.phase.id] = {
|
|
1287
|
-
state: 'blocked',
|
|
1288
|
-
updatedAt: input.now.toISOString(),
|
|
1289
|
-
evidence: base.phases[input.phase.id].evidence,
|
|
1290
|
-
approvals: appendUnique(base.phases[input.phase.id].approvals, input.plan.approval.envelopeId),
|
|
1291
|
-
blockers: [input.blocker]
|
|
1292
|
-
};
|
|
1293
|
-
base.updatedAt = input.now.toISOString();
|
|
1294
|
-
return validateUserActivationState(base);
|
|
1295
|
-
}
|
|
1296
|
-
async function assertLoadedStateHash(projectRoot, expectedHash) {
|
|
1297
|
-
const bytes = await readProjectFile(projectRoot, activationStatePathParts());
|
|
1298
|
-
const currentHash = bytes === undefined ? null : activationStateContentHash(bytes);
|
|
1299
|
-
if (currentHash !== expectedHash) {
|
|
1300
|
-
throw new Error(`Activation state changed after inspection: expected ${expectedHash ?? 'absent'}, found ${currentHash ?? 'absent'}.`);
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
async function writeOutcomeTransaction(input) {
|
|
1304
|
-
await assertLoadedStateHash(input.projectRoot, input.plan.stateHash);
|
|
1305
|
-
const stateContent = `${canonicalJson(validateUserActivationState(input.nextState))}\n`;
|
|
1306
|
-
const mutations = [
|
|
1307
|
-
...(input.fileMutations ?? []).map((mutation) => ({
|
|
1308
|
-
...mutation,
|
|
1309
|
-
pathParts: validateArtifactPathParts([...mutation.pathParts], 'Governance transition file mutation path')
|
|
1310
|
-
})),
|
|
1311
|
-
...(input.evidenceRecord && input.evidencePathParts
|
|
1312
|
-
? [{
|
|
1313
|
-
type: 'write',
|
|
1314
|
-
pathParts: [...input.evidencePathParts],
|
|
1315
|
-
content: `${canonicalJson(input.evidenceRecord)}\n`
|
|
1316
|
-
}]
|
|
1317
|
-
: []),
|
|
1318
|
-
{
|
|
1319
|
-
type: 'write',
|
|
1320
|
-
pathParts: activationStatePathParts(),
|
|
1321
|
-
content: stateContent
|
|
1322
|
-
}
|
|
1323
|
-
];
|
|
1324
|
-
if (input.evidencePathParts) {
|
|
1325
|
-
const existing = await readProjectFile(input.projectRoot, [...input.evidencePathParts]);
|
|
1326
|
-
if (existing !== undefined) {
|
|
1327
|
-
throw new Error(`Refusing to overwrite existing governance evidence ${input.evidencePathParts.join('/')}.`);
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
await applyProjectFileTransaction(input.projectRoot, mutations);
|
|
1331
|
-
return {
|
|
1332
|
-
stateHash: activationStateContentHash(stateContent),
|
|
1333
|
-
evidence: input.evidenceRecord && input.evidencePathParts
|
|
1334
|
-
? {
|
|
1335
|
-
evidenceId: input.evidenceRecord.evidenceId,
|
|
1336
|
-
pathParts: input.evidencePathParts,
|
|
1337
|
-
headerDigest: evidenceHeaderDigest(input.evidenceRecord.header),
|
|
1338
|
-
result: input.evidenceRecord.header.result
|
|
1339
|
-
}
|
|
1340
|
-
: null
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1343
|
-
async function executeGitOperations(input, operations) {
|
|
1344
|
-
if (input.phase.id !== 'committed' && input.phase.id !== 'pushed') {
|
|
1345
|
-
return null;
|
|
1346
|
-
}
|
|
1347
|
-
const completed = [];
|
|
1348
|
-
for (const op of operations.filter((entry) => entry.adapter === 'git')) {
|
|
1349
|
-
if (op.actionId === 'git.verify-existing-commit' || op.actionId === 'git.verify-existing-push') {
|
|
1350
|
-
completed.push(op);
|
|
1351
|
-
continue;
|
|
1352
|
-
}
|
|
1353
|
-
if (op.actionId === 'git.init') {
|
|
1354
|
-
const result = await runGit(input.runner, input.inspection.projectRoot, ['init', '-b', String(op.inputs.branch)]);
|
|
1355
|
-
if (!commandSucceeded(result)) {
|
|
1356
|
-
return { status: 'blocked', blocker: commandFailure(result), completedOperations: completed };
|
|
1357
|
-
}
|
|
1358
|
-
completed.push(op);
|
|
1359
|
-
}
|
|
1360
|
-
else if (op.actionId === 'git.add-reviewed') {
|
|
1361
|
-
const paths = Array.isArray(op.inputs.paths) ? op.inputs.paths.map(String) : [];
|
|
1362
|
-
if (paths.length === 0) {
|
|
1363
|
-
completed.push(op);
|
|
1364
|
-
continue;
|
|
1365
|
-
}
|
|
1366
|
-
const result = await runGit(input.runner, input.inspection.projectRoot, ['add', '--', ...paths]);
|
|
1367
|
-
if (!commandSucceeded(result)) {
|
|
1368
|
-
return { status: 'blocked', blocker: commandFailure(result), completedOperations: completed };
|
|
1369
|
-
}
|
|
1370
|
-
completed.push(op);
|
|
1371
|
-
}
|
|
1372
|
-
else if (op.actionId === 'git.commit-reviewed') {
|
|
1373
|
-
const message = String(op.inputs.message);
|
|
1374
|
-
const result = await runGit(input.runner, input.inspection.projectRoot, ['commit', '-m', message]);
|
|
1375
|
-
if (!commandSucceeded(result)) {
|
|
1376
|
-
return { status: 'blocked', blocker: commandFailure(result), completedOperations: completed };
|
|
1377
|
-
}
|
|
1378
|
-
completed.push(op);
|
|
1379
|
-
}
|
|
1380
|
-
else if (op.actionId === 'git.push-approved-ref') {
|
|
1381
|
-
const branch = String(op.inputs.branch);
|
|
1382
|
-
const result = await runGit(input.runner, input.inspection.projectRoot, ['push', 'origin', `HEAD:refs/heads/${branch}`]);
|
|
1383
|
-
if (!commandSucceeded(result)) {
|
|
1384
|
-
return { status: 'blocked', blocker: commandFailure(result), completedOperations: completed };
|
|
1385
|
-
}
|
|
1386
|
-
completed.push(op);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
return {
|
|
1390
|
-
status: 'completed',
|
|
1391
|
-
resultState: 'verified',
|
|
1392
|
-
completedOperations: completed
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
async function executeSeedOperations(input) {
|
|
1396
|
-
if (!input.phase.id.startsWith('seed-')) {
|
|
1397
|
-
return null;
|
|
1398
|
-
}
|
|
1399
|
-
if (input.phase.id === 'seed-valid') {
|
|
1400
|
-
const result = await validateGeneratedSeedForPhase(input.inspection.projectRoot, input.runner);
|
|
1401
|
-
if (result.status === 'blocked') {
|
|
1402
|
-
return { status: 'blocked', blocker: result.issues[0] ?? 'Seed validation failed.', completedOperations: [] };
|
|
1403
|
-
}
|
|
1404
|
-
return { status: 'completed', resultState: 'verified', completedOperations: input.plan.operations.filter((op) => op.actionId === 'openspec.seed.validate') };
|
|
1405
|
-
}
|
|
1406
|
-
if (input.phase.id === 'seed-verified') {
|
|
1407
|
-
const result = await verifyGeneratedSeedBaselineForPhase(input.inspection.projectRoot, input.runner);
|
|
1408
|
-
if (result.status === 'blocked') {
|
|
1409
|
-
return { status: 'blocked', blocker: result.issues[0] ?? 'Seed baseline verification failed.', completedOperations: [] };
|
|
1410
|
-
}
|
|
1411
|
-
return {
|
|
1412
|
-
status: 'completed',
|
|
1413
|
-
resultState: 'verified',
|
|
1414
|
-
evidencePayload: {
|
|
1415
|
-
kind: 'seed-verified.v1',
|
|
1416
|
-
checks: result.checks.map((check) => ({
|
|
1417
|
-
id: check.id,
|
|
1418
|
-
taskId: check.taskId,
|
|
1419
|
-
status: check.status
|
|
1420
|
-
}))
|
|
1421
|
-
},
|
|
1422
|
-
completedOperations: input.plan.operations.filter((op) => op.actionId === 'openspec.seed.baseline-verify')
|
|
1423
|
-
};
|
|
1424
|
-
}
|
|
1425
|
-
const result = await archiveGeneratedSeedForPhase(input.inspection.projectRoot, input.runner);
|
|
1426
|
-
if (result.status === 'blocked') {
|
|
1427
|
-
return {
|
|
1428
|
-
status: 'blocked',
|
|
1429
|
-
blocker: result.issues[0] ?? 'Seed archive failed.',
|
|
1430
|
-
completedOperations: result.archiveCompleted
|
|
1431
|
-
? input.plan.operations.filter((op) => op.actionId === 'openspec.seed.archive')
|
|
1432
|
-
: [],
|
|
1433
|
-
retryableWithoutStateMutation: result.retryableAfterRepair
|
|
1434
|
-
};
|
|
33
|
+
async function executeBuiltInPhase(input, localRevalidation = false) {
|
|
34
|
+
if (localRevalidation) {
|
|
35
|
+
const outcome = await executeSeedOperations(input, { localRevalidation: true });
|
|
36
|
+
if (!outcome)
|
|
37
|
+
throw new Error(`Local revalidation cannot execute ${input.phase.id}.`);
|
|
38
|
+
return outcome;
|
|
1435
39
|
}
|
|
1436
|
-
return {
|
|
1437
|
-
status: 'completed',
|
|
1438
|
-
resultState: 'verified',
|
|
1439
|
-
evidencePayload: {
|
|
1440
|
-
kind: 'seed-archived.v1',
|
|
1441
|
-
archiveSyncBehavior: result.archiveSyncBehavior ?? null
|
|
1442
|
-
},
|
|
1443
|
-
completedOperations: input.plan.operations.filter((op) => op.actionId === 'openspec.seed.archive')
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
async function discoverPhase0(input) {
|
|
1447
|
-
if (input.phase.id !== 'phase-0-complete') {
|
|
1448
|
-
return null;
|
|
1449
|
-
}
|
|
1450
|
-
const gh = await runCommand(input.runner, {
|
|
1451
|
-
executable: 'gh',
|
|
1452
|
-
args: ['repo', 'view', '--json', 'id,nameWithOwner,defaultBranchRef,isPrivate']
|
|
1453
|
-
}, input.inspection.projectRoot);
|
|
1454
|
-
if (!commandSucceeded(gh)) {
|
|
1455
|
-
return {
|
|
1456
|
-
status: 'blocked',
|
|
1457
|
-
blocker: `Phase 0 GitHub read-only discovery failed: ${commandFailure(gh)}`,
|
|
1458
|
-
completedOperations: []
|
|
1459
|
-
};
|
|
1460
|
-
}
|
|
1461
|
-
let repo;
|
|
1462
|
-
try {
|
|
1463
|
-
repo = JSON.parse(gh.stdout);
|
|
1464
|
-
}
|
|
1465
|
-
catch (error) {
|
|
1466
|
-
return {
|
|
1467
|
-
status: 'blocked',
|
|
1468
|
-
blocker: `Phase 0 GitHub discovery returned invalid JSON: ${errorMessage(error)}`,
|
|
1469
|
-
completedOperations: []
|
|
1470
|
-
};
|
|
1471
|
-
}
|
|
1472
|
-
if (!repo.id || !repo.nameWithOwner || !repo.defaultBranchRef?.name) {
|
|
1473
|
-
return {
|
|
1474
|
-
status: 'blocked',
|
|
1475
|
-
blocker: 'Phase 0 GitHub discovery did not return repository id, nameWithOwner, and default branch.',
|
|
1476
|
-
completedOperations: []
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
1479
|
-
const az = await runCommand(input.runner, {
|
|
1480
|
-
executable: 'az',
|
|
1481
|
-
args: ['account', 'show', '--output', 'json']
|
|
1482
|
-
}, input.inspection.projectRoot);
|
|
1483
|
-
const azureAccount = commandSucceeded(az) ? safeJson(az.stdout) : null;
|
|
1484
|
-
const git = await inspectGitRepository(input.inspection.projectRoot, input.runner);
|
|
1485
|
-
const baselineDigest = canonicalSha256({
|
|
1486
|
-
gitHead: git.head,
|
|
1487
|
-
repositoryId: repo.id,
|
|
1488
|
-
repositoryName: repo.nameWithOwner,
|
|
1489
|
-
defaultBranch: repo.defaultBranchRef.name
|
|
1490
|
-
});
|
|
1491
|
-
const facts = {
|
|
1492
|
-
repositoryId: repo.id,
|
|
1493
|
-
repositoryName: repo.nameWithOwner,
|
|
1494
|
-
defaultBranch: repo.defaultBranchRef.name,
|
|
1495
|
-
baselineDigest,
|
|
1496
|
-
privateStagingDast: false,
|
|
1497
|
-
credentialRequired: false,
|
|
1498
|
-
statePath: 'none',
|
|
1499
|
-
approvedFacts: [
|
|
1500
|
-
{ id: 'repository.id', value: repo.id },
|
|
1501
|
-
{ id: 'repository.nameWithOwner', value: repo.nameWithOwner },
|
|
1502
|
-
{ id: 'repository.defaultBranch', value: repo.defaultBranchRef.name },
|
|
1503
|
-
{ id: 'repository.isPrivate', value: repo.isPrivate ?? null },
|
|
1504
|
-
{ id: 'azure.accountReadable', value: azureAccount !== null }
|
|
1505
|
-
]
|
|
1506
|
-
};
|
|
1507
|
-
const nextState = cloneState(input.inspection.state);
|
|
1508
|
-
nextState.repository = {
|
|
1509
|
-
id: facts.repositoryId,
|
|
1510
|
-
name: facts.repositoryName,
|
|
1511
|
-
defaultBranch: facts.defaultBranch
|
|
1512
|
-
};
|
|
1513
|
-
nextState.applicability = {
|
|
1514
|
-
statePath: facts.statePath,
|
|
1515
|
-
privateStagingDast: facts.privateStagingDast,
|
|
1516
|
-
credentialRequired: facts.credentialRequired
|
|
1517
|
-
};
|
|
1518
|
-
return {
|
|
1519
|
-
status: 'completed',
|
|
1520
|
-
resultState: 'verified',
|
|
1521
|
-
evidencePayload: {
|
|
1522
|
-
kind: 'phase-0-discovery.v1',
|
|
1523
|
-
facts: facts.approvedFacts,
|
|
1524
|
-
activationCreateChangePlan: {
|
|
1525
|
-
deterministic: true,
|
|
1526
|
-
graphHash: input.inspection.graphHash
|
|
1527
|
-
}
|
|
1528
|
-
},
|
|
1529
|
-
stateOverride: nextState,
|
|
1530
|
-
completedOperations: input.plan.operations.filter((op) => op.actionId === 'github.phase0.discover' || op.actionId === 'azure.phase0.discover')
|
|
1531
|
-
};
|
|
1532
|
-
}
|
|
1533
|
-
function safeJson(value) {
|
|
1534
|
-
try {
|
|
1535
|
-
return JSON.parse(value);
|
|
1536
|
-
}
|
|
1537
|
-
catch {
|
|
1538
|
-
return null;
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
async function executeActivationApproval(input) {
|
|
1542
|
-
if (input.phase.id !== 'activation-approved') {
|
|
1543
|
-
return null;
|
|
1544
|
-
}
|
|
1545
|
-
const state = cloneState(input.inspection.state);
|
|
1546
|
-
const fileMutations = [];
|
|
1547
|
-
if (input.inspection.sourceOfTruth.status === 'none') {
|
|
1548
|
-
const facts = buildApprovedPhase0FactsFromState(input.inspection.manifest, input.inspection.state, input.inspection.evidence);
|
|
1549
|
-
if (!facts || input.inspection.sourceOfTruth.createPlan.status !== 'ready') {
|
|
1550
|
-
return {
|
|
1551
|
-
status: 'blocked',
|
|
1552
|
-
blocker: input.inspection.sourceOfTruth.createPlan.reason,
|
|
1553
|
-
completedOperations: []
|
|
1554
|
-
};
|
|
1555
|
-
}
|
|
1556
|
-
const writePlan = renderGovernanceChangeWritePlan(facts);
|
|
1557
|
-
for (const file of writePlan.files) {
|
|
1558
|
-
const existing = await readProjectFile(input.inspection.projectRoot, [...file.pathParts]);
|
|
1559
|
-
if (existing !== undefined) {
|
|
1560
|
-
return {
|
|
1561
|
-
status: 'blocked',
|
|
1562
|
-
blocker: `Refusing to overwrite existing governance artifact ${file.pathParts.join('/')}.`,
|
|
1563
|
-
completedOperations: []
|
|
1564
|
-
};
|
|
1565
|
-
}
|
|
1566
|
-
fileMutations.push({
|
|
1567
|
-
type: 'write',
|
|
1568
|
-
pathParts: [...file.pathParts],
|
|
1569
|
-
content: file.content
|
|
1570
|
-
});
|
|
1571
|
-
}
|
|
1572
|
-
state.activeChange = {
|
|
1573
|
-
id: writePlan.changeId,
|
|
1574
|
-
kind: writePlan.workflowKind === 'openspec' ? 'openspec' : 'spec-kit'
|
|
1575
|
-
};
|
|
1576
|
-
}
|
|
1577
|
-
else if (input.inspection.sourceOfTruth.status === 'selected') {
|
|
1578
|
-
state.activeChange = {
|
|
1579
|
-
id: input.inspection.sourceOfTruth.selected.changeId,
|
|
1580
|
-
kind: input.inspection.sourceOfTruth.selected.workflowKind === 'openspec' ? 'openspec' : 'spec-kit'
|
|
1581
|
-
};
|
|
1582
|
-
}
|
|
1583
|
-
else {
|
|
1584
|
-
return {
|
|
1585
|
-
status: 'blocked',
|
|
1586
|
-
blocker: 'A compatible governance source of truth is required before activation approval can be recorded.',
|
|
1587
|
-
completedOperations: []
|
|
1588
|
-
};
|
|
1589
|
-
}
|
|
1590
|
-
return {
|
|
1591
|
-
status: 'completed',
|
|
1592
|
-
resultState: 'approved',
|
|
1593
|
-
stateOverride: state,
|
|
1594
|
-
fileMutations,
|
|
1595
|
-
completedOperations: input.plan.operations.filter((op) => op.actionId.endsWith('governance.create-change'))
|
|
1596
|
-
};
|
|
1597
|
-
}
|
|
1598
|
-
async function executeCredentialReady(input) {
|
|
1599
|
-
if (input.phase.id !== 'credential-ready') {
|
|
1600
|
-
return null;
|
|
1601
|
-
}
|
|
1602
|
-
const policy = await readProjectFile(input.inspection.projectRoot, [...credentialPolicyPathParts]);
|
|
1603
|
-
if (policy === undefined) {
|
|
1604
|
-
return {
|
|
1605
|
-
status: 'blocked',
|
|
1606
|
-
blocker: 'Credential enrollment requires a secure masked input channel or an existing selected-repository App; automated noninteractive apply cannot collect a PAT.',
|
|
1607
|
-
completedOperations: []
|
|
1608
|
-
};
|
|
1609
|
-
}
|
|
1610
|
-
const scan = detectCredentialLeaks([{
|
|
1611
|
-
source: 'imported-evidence',
|
|
1612
|
-
label: credentialPolicyPathParts.join('/'),
|
|
1613
|
-
text: policy.toString('utf8')
|
|
1614
|
-
}]);
|
|
1615
|
-
if (scan.status === 'compromised') {
|
|
1616
|
-
return {
|
|
1617
|
-
status: 'blocked',
|
|
1618
|
-
blocker: scan.guidance.join(' '),
|
|
1619
|
-
completedOperations: []
|
|
1620
|
-
};
|
|
1621
|
-
}
|
|
1622
|
-
return {
|
|
1623
|
-
status: 'completed',
|
|
1624
|
-
resultState: 'verified',
|
|
1625
|
-
evidencePayload: {
|
|
1626
|
-
kind: 'credential-ready.v1',
|
|
1627
|
-
policyPathParts: credentialPolicyPathParts,
|
|
1628
|
-
payloadFree: true,
|
|
1629
|
-
policyDigest: canonicalSha256(policy.toString('utf8'))
|
|
1630
|
-
},
|
|
1631
|
-
completedOperations: input.plan.operations.filter((op) => op.actionId === 'github.credential.verify-policy')
|
|
1632
|
-
};
|
|
1633
|
-
}
|
|
1634
|
-
function remoteImportRetention(input) {
|
|
1635
|
-
if (input.phase.id !== 'remote-ready') {
|
|
1636
|
-
return null;
|
|
1637
|
-
}
|
|
1638
|
-
if (input.inspection.state.applicability.statePath !== 'bootstrap-local') {
|
|
1639
|
-
return {
|
|
1640
|
-
status: 'completed',
|
|
1641
|
-
resultState: 'verified',
|
|
1642
|
-
evidencePayload: { kind: 'remote-ready.v1', retention: 'not-applicable' },
|
|
1643
|
-
completedOperations: input.plan.operations.filter((op) => op.actionId === 'azure.remote-ready.verify')
|
|
1644
|
-
};
|
|
1645
|
-
}
|
|
1646
|
-
const importRecord = latestRecordWithPayload(input.inspection, 'remote-import-verified');
|
|
1647
|
-
if (!importRecord || !isRecord(importRecord.payload) || importRecord.payload.kind !== remoteImportPayloadKind) {
|
|
1648
|
-
return {
|
|
1649
|
-
status: 'blocked',
|
|
1650
|
-
blocker: 'Remote import retention requires immutable remote-import evidence with state and key identifiers.',
|
|
1651
|
-
completedOperations: []
|
|
1652
|
-
};
|
|
1653
|
-
}
|
|
1654
|
-
const encryptedStatePathParts = pathPartLists(importRecord.payload.encryptedStatePathParts, 'encryptedStatePathParts');
|
|
1655
|
-
const encryptionKeyPathParts = pathPartLists(importRecord.payload.encryptionKeyPathParts, 'encryptionKeyPathParts');
|
|
1656
|
-
const retainedAt = input.now.toISOString();
|
|
1657
|
-
const disposeAfter = new Date(input.now.getTime() + 30 * dayMs).toISOString();
|
|
1658
|
-
const state = cloneState(input.inspection.state);
|
|
1659
|
-
state.bootstrapState = {
|
|
1660
|
-
status: 'retained',
|
|
1661
|
-
remoteImportEvidenceId: importRecord.evidenceId,
|
|
1662
|
-
remoteImportEvidenceDigest: evidenceHeaderDigest(importRecord.header),
|
|
1663
|
-
retainedAt,
|
|
1664
|
-
disposeAfter,
|
|
1665
|
-
encryptedStatePathParts,
|
|
1666
|
-
encryptionKeyPathParts
|
|
1667
|
-
};
|
|
1668
|
-
return {
|
|
1669
|
-
status: 'completed',
|
|
1670
|
-
resultState: 'retained',
|
|
1671
|
-
stateOverride: state,
|
|
1672
|
-
evidencePayload: {
|
|
1673
|
-
kind: 'remote-ready.v1',
|
|
1674
|
-
retention: state.bootstrapState
|
|
1675
|
-
},
|
|
1676
|
-
completedOperations: input.plan.operations.filter((op) => op.actionId === 'azure.remote-ready.verify')
|
|
1677
|
-
};
|
|
1678
|
-
}
|
|
1679
|
-
function pathPartLists(value, label) {
|
|
1680
|
-
if (!Array.isArray(value)) {
|
|
1681
|
-
throw new Error(`${label} must be an array of path-part arrays.`);
|
|
1682
|
-
}
|
|
1683
|
-
return value.map((entry, index) => validateArtifactPathParts(entry, `${label}[${index}]`));
|
|
1684
|
-
}
|
|
1685
|
-
async function executeBootstrapStateDisposal(input) {
|
|
1686
|
-
if (input.phase.id !== 'bootstrap-state-disposed') {
|
|
1687
|
-
return null;
|
|
1688
|
-
}
|
|
1689
|
-
const retention = input.inspection.state.bootstrapState;
|
|
1690
|
-
if (!retention || input.inspection.state.applicability.statePath !== 'bootstrap-local') {
|
|
1691
|
-
return {
|
|
1692
|
-
status: 'completed',
|
|
1693
|
-
resultState: 'inapplicable',
|
|
1694
|
-
evidencePayload: { kind: bootstrapStateDisposedPayloadKind, reason: 'no retained bootstrap state' },
|
|
1695
|
-
completedOperations: []
|
|
1696
|
-
};
|
|
1697
|
-
}
|
|
1698
|
-
if (retention.status === 'disposed') {
|
|
1699
|
-
return {
|
|
1700
|
-
status: 'completed',
|
|
1701
|
-
resultState: 'disposed',
|
|
1702
|
-
evidencePayload: { kind: bootstrapStateDisposedPayloadKind, reason: 'already disposed', deletionEvidenceId: retention.deletionEvidenceId ?? null },
|
|
1703
|
-
completedOperations: []
|
|
1704
|
-
};
|
|
1705
|
-
}
|
|
1706
|
-
if (Date.parse(retention.disposeAfter) > input.now.getTime()) {
|
|
1707
|
-
return {
|
|
1708
|
-
status: 'blocked',
|
|
1709
|
-
blocker: `Retained bootstrap state is not disposable until ${retention.disposeAfter}.`,
|
|
1710
|
-
completedOperations: []
|
|
1711
|
-
};
|
|
1712
|
-
}
|
|
1713
|
-
const importRecord = input.inspection.evidence.find((record) => record.evidenceId === retention.remoteImportEvidenceId);
|
|
1714
|
-
if (!importRecord || evidenceHeaderDigest(importRecord.header) !== retention.remoteImportEvidenceDigest) {
|
|
1715
|
-
return {
|
|
1716
|
-
status: 'blocked',
|
|
1717
|
-
blocker: 'Destructive disposal requires the immutable remote-import evidence referenced by retained bootstrap state.',
|
|
1718
|
-
completedOperations: []
|
|
1719
|
-
};
|
|
1720
|
-
}
|
|
1721
|
-
if (!isRecord(importRecord.payload) || importRecord.payload.kind !== remoteImportPayloadKind) {
|
|
1722
|
-
return {
|
|
1723
|
-
status: 'blocked',
|
|
1724
|
-
blocker: 'Destructive disposal requires verified remote backend and no-change evidence.',
|
|
1725
|
-
completedOperations: []
|
|
1726
|
-
};
|
|
1727
|
-
}
|
|
1728
|
-
const remoteBackendDigest = importRecord.payload.remoteBackendDigest;
|
|
1729
|
-
const noChangePlanDigest = importRecord.payload.noChangePlanDigest;
|
|
1730
|
-
if (typeof remoteBackendDigest !== 'string' ||
|
|
1731
|
-
!/^[a-f0-9]{64}$/u.test(remoteBackendDigest) ||
|
|
1732
|
-
typeof noChangePlanDigest !== 'string' ||
|
|
1733
|
-
!/^[a-f0-9]{64}$/u.test(noChangePlanDigest)) {
|
|
1734
|
-
return {
|
|
1735
|
-
status: 'blocked',
|
|
1736
|
-
blocker: 'Destructive disposal requires payload-free remote backend and no-change plan digests.',
|
|
1737
|
-
completedOperations: []
|
|
1738
|
-
};
|
|
1739
|
-
}
|
|
1740
|
-
const allPaths = [...retention.encryptedStatePathParts, ...retention.encryptionKeyPathParts]
|
|
1741
|
-
.map((parts) => validateArtifactPathParts([...parts], 'Bootstrap disposal path'));
|
|
1742
|
-
const incompleteCleanup = [];
|
|
1743
|
-
for (const parts of allPaths) {
|
|
1744
|
-
try {
|
|
1745
|
-
const target = await resolveProjectPath(input.inspection.projectRoot, parts);
|
|
1746
|
-
const details = await stat(target);
|
|
1747
|
-
if (!details.isFile()) {
|
|
1748
|
-
incompleteCleanup.push(`${parts.join('/')} is not a regular file.`);
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
catch (error) {
|
|
1752
|
-
incompleteCleanup.push(`${parts.join('/')} was already absent before disposal: ${errorMessage(error)}`);
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
const state = cloneState(input.inspection.state);
|
|
1756
|
-
state.bootstrapState = {
|
|
1757
|
-
...retention,
|
|
1758
|
-
status: 'disposed',
|
|
1759
|
-
disposedAt: input.now.toISOString(),
|
|
1760
|
-
deletionEvidenceId: `${input.phase.id}-${safeTimestamp(input.now.toISOString())}`,
|
|
1761
|
-
incompleteCleanup
|
|
1762
|
-
};
|
|
1763
|
-
return {
|
|
1764
|
-
status: 'completed',
|
|
1765
|
-
resultState: 'disposed',
|
|
1766
|
-
stateOverride: state,
|
|
1767
|
-
evidencePayload: {
|
|
1768
|
-
kind: bootstrapStateDisposedPayloadKind,
|
|
1769
|
-
deletedPathParts: allPaths,
|
|
1770
|
-
remoteBackendDigest,
|
|
1771
|
-
noChangePlanDigest,
|
|
1772
|
-
incompleteCleanup,
|
|
1773
|
-
payloadFree: true
|
|
1774
|
-
},
|
|
1775
|
-
fileMutations: allPaths.map((parts) => ({ type: 'delete', pathParts: parts })),
|
|
1776
|
-
completedOperations: input.plan.operations.filter((op) => op.actionId === 'local.bootstrap-state.dispose'),
|
|
1777
|
-
cleanupWarnings: incompleteCleanup
|
|
1778
|
-
};
|
|
1779
|
-
}
|
|
1780
|
-
function assertGreenRedProof(input) {
|
|
1781
|
-
const record = latestRecordWithPayload(input.inspection, 'green-red-proof');
|
|
1782
|
-
if (!record || !isRecord(record.payload) || record.payload.kind !== greenRedPayloadKind) {
|
|
1783
|
-
throw new Error('Ruleset enforcement requires current green-red-proof evidence with typed proof payload.');
|
|
1784
|
-
}
|
|
1785
|
-
const green = isRecord(record.payload.green) ? record.payload.green : null;
|
|
1786
|
-
const red = isRecord(record.payload.deliberateRed) ? record.payload.deliberateRed : null;
|
|
1787
|
-
const greenConclusion = green?.conclusion;
|
|
1788
|
-
const redConclusion = red?.conclusion;
|
|
1789
|
-
const badConclusions = [greenConclusion, redConclusion].filter((value) => value === 'skipped' || value === 'cancelled' || value === 'neutral');
|
|
1790
|
-
if (badConclusions.length > 0) {
|
|
1791
|
-
throw new Error('Skipped, cancelled, and neutral checks cannot satisfy green/red proof.');
|
|
1792
|
-
}
|
|
1793
|
-
if (!red || greenConclusion !== 'success' || redConclusion !== 'failure' || red.deliberate !== true) {
|
|
1794
|
-
throw new Error('Ruleset enforcement requires green success and deliberate red failure proof.');
|
|
1795
|
-
}
|
|
1796
|
-
return record;
|
|
1797
|
-
}
|
|
1798
|
-
async function executeRulesetPhase(input) {
|
|
1799
|
-
if (input.phase.id !== 'rulesets-applied' && input.phase.id !== 'live-readback') {
|
|
1800
|
-
return null;
|
|
1801
|
-
}
|
|
1802
|
-
try {
|
|
1803
|
-
assertGreenRedProof(input);
|
|
1804
|
-
}
|
|
1805
|
-
catch (error) {
|
|
1806
|
-
return { status: 'blocked', blocker: errorMessage(error), completedOperations: [] };
|
|
1807
|
-
}
|
|
1808
|
-
const sourceDigest = rulesetSourceDigestFromEvidence(input.inspection);
|
|
1809
|
-
if (!sourceDigest) {
|
|
1810
|
-
return {
|
|
1811
|
-
status: 'blocked',
|
|
1812
|
-
blocker: 'Ruleset enforcement requires a saved ruleset source digest from workflow-source-ready evidence.',
|
|
1813
|
-
completedOperations: []
|
|
1814
|
-
};
|
|
1815
|
-
}
|
|
1816
|
-
const adapter = input.adapters.githubRulesets;
|
|
1817
|
-
if (!adapter) {
|
|
1818
|
-
return {
|
|
1819
|
-
status: 'blocked',
|
|
1820
|
-
blocker: 'No injected GitHub ruleset adapter is configured; refusing live ruleset calls in this execution context.',
|
|
1821
|
-
completedOperations: []
|
|
1822
|
-
};
|
|
1823
|
-
}
|
|
1824
|
-
const envelopeId = input.plan.approval.envelopeId;
|
|
1825
|
-
if (input.phase.id === 'rulesets-applied' && !envelopeId) {
|
|
1826
|
-
return {
|
|
1827
|
-
status: 'blocked',
|
|
1828
|
-
blocker: 'Ruleset enforcement requires a persisted enforcement approval envelope.',
|
|
1829
|
-
completedOperations: []
|
|
1830
|
-
};
|
|
1831
|
-
}
|
|
1832
|
-
const completed = [];
|
|
1833
|
-
const write = input.phase.id === 'rulesets-applied'
|
|
1834
|
-
? await adapter.applyRuleset({
|
|
1835
|
-
repository: input.inspection.state.repository.name,
|
|
1836
|
-
sourceDigest,
|
|
1837
|
-
approvalEnvelopeId: envelopeId ?? 'ungated-readback'
|
|
1838
|
-
})
|
|
1839
|
-
: await adapter.readRuleset({
|
|
1840
|
-
repository: input.inspection.state.repository.name,
|
|
1841
|
-
sourceDigest
|
|
1842
|
-
});
|
|
1843
|
-
completed.push(...input.plan.operations.filter((op) => input.phase.id === 'rulesets-applied'
|
|
1844
|
-
? op.actionId === 'github.ruleset.apply' || op.actionId === 'github.ruleset.readback'
|
|
1845
|
-
: op.actionId === 'github.ruleset.readback'));
|
|
1846
|
-
if (write.sourceDigest !== sourceDigest || write.readbackDigest !== sourceDigest) {
|
|
1847
|
-
return {
|
|
1848
|
-
status: 'blocked',
|
|
1849
|
-
blocker: 'Post-write live ruleset readback did not match the saved source digest.',
|
|
1850
|
-
completedOperations: completed
|
|
1851
|
-
};
|
|
1852
|
-
}
|
|
1853
|
-
const header = evidenceHeaderFor({
|
|
1854
|
-
inspection: input.inspection,
|
|
1855
|
-
phase: input.phase,
|
|
1856
|
-
plan: input.plan,
|
|
1857
|
-
result: 'verified',
|
|
1858
|
-
now: input.now
|
|
1859
|
-
});
|
|
1860
|
-
const liveReadback = {
|
|
1861
|
-
schemaVersion: input.inspection.state.identity.evidenceHeaderSchemaVersion,
|
|
1862
|
-
repositoryId: header.repositoryId,
|
|
1863
|
-
identity: header.identity,
|
|
1864
|
-
phaseGraphHash: header.phaseGraphHash,
|
|
1865
|
-
phaseId: header.phaseId,
|
|
1866
|
-
baselineSha: header.baselineSha,
|
|
1867
|
-
inputDigest: header.inputDigest,
|
|
1868
|
-
transition: header.transition,
|
|
1869
|
-
observedAt: input.now.toISOString(),
|
|
1870
|
-
provider: 'github',
|
|
1871
|
-
resourceType: 'ruleset',
|
|
1872
|
-
resourceId: write.resourceId,
|
|
1873
|
-
sourceDigest,
|
|
1874
|
-
readbackDigest: write.readbackDigest,
|
|
1875
|
-
matches: true
|
|
1876
|
-
};
|
|
1877
|
-
return {
|
|
1878
|
-
status: 'completed',
|
|
1879
|
-
resultState: 'verified',
|
|
1880
|
-
liveReadback: [liveReadback],
|
|
1881
|
-
evidencePayload: {
|
|
1882
|
-
kind: `${input.phase.id}.v1`,
|
|
1883
|
-
sourceDigest,
|
|
1884
|
-
resourceId: write.resourceId,
|
|
1885
|
-
readbackDigest: write.readbackDigest
|
|
1886
|
-
},
|
|
1887
|
-
completedOperations: completed
|
|
1888
|
-
};
|
|
1889
|
-
}
|
|
1890
|
-
async function executeBuiltInPhase(input) {
|
|
1891
40
|
const custom = input.adapters.phases?.[input.phase.id];
|
|
1892
|
-
if (custom)
|
|
41
|
+
if (custom)
|
|
1893
42
|
return await custom.execute(input);
|
|
1894
|
-
|
|
1895
|
-
for (const attempt of [
|
|
1896
|
-
executeSeedOperations,
|
|
1897
|
-
async (execution) => executeGitOperations(execution, execution.plan.operations),
|
|
1898
|
-
discoverPhase0,
|
|
1899
|
-
executeActivationApproval,
|
|
1900
|
-
executeCredentialReady,
|
|
1901
|
-
async (execution) => remoteImportRetention(execution),
|
|
1902
|
-
executeBootstrapStateDisposal,
|
|
1903
|
-
executeRulesetPhase
|
|
1904
|
-
]) {
|
|
1905
|
-
const outcome = await attempt(input);
|
|
1906
|
-
if (outcome) {
|
|
1907
|
-
return outcome;
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
return {
|
|
43
|
+
return await builtInExecutors[input.phase.id]?.(input) ?? {
|
|
1911
44
|
status: 'blocked',
|
|
1912
45
|
blocker: `No production adapter is configured for ${input.phase.id}; refusing success-shaped fallback.`,
|
|
1913
46
|
completedOperations: []
|
|
1914
47
|
};
|
|
1915
48
|
}
|
|
1916
49
|
function sourceOfTruthAllowsPhase(inspection, phaseId) {
|
|
1917
|
-
if (phaseOrder(phaseId) <= phaseOrder('phase-0-complete'))
|
|
50
|
+
if (phaseOrder(phaseId) <= phaseOrder('phase-0-complete'))
|
|
1918
51
|
return null;
|
|
1919
|
-
}
|
|
1920
52
|
const source = inspection.sourceOfTruth;
|
|
1921
|
-
if (phaseId === 'activation-approved' && (source.status === 'none' || source.status === 'selected'))
|
|
53
|
+
if (phaseId === 'activation-approved' && (source.status === 'none' || source.status === 'selected'))
|
|
1922
54
|
return null;
|
|
1923
|
-
|
|
1924
|
-
if (source.status === 'selected' && source.reconciliation.status === 'not-required') {
|
|
55
|
+
if (source.status === 'selected' && source.reconciliation.status === 'not-required')
|
|
1925
56
|
return null;
|
|
1926
|
-
|
|
1927
|
-
if (source.status === 'seed-blocked') {
|
|
1928
|
-
return source.blockers.join('; ');
|
|
1929
|
-
}
|
|
1930
|
-
if (source.status === 'ambiguous' || source.status === 'incompatible') {
|
|
57
|
+
if (source.status === 'seed-blocked' || source.status === 'ambiguous' || source.status === 'incompatible')
|
|
1931
58
|
return source.blockers.join('; ');
|
|
1932
|
-
|
|
1933
|
-
if (source.status === 'none') {
|
|
59
|
+
if (source.status === 'none')
|
|
1934
60
|
return source.createPlan.reason;
|
|
1935
|
-
}
|
|
1936
61
|
return 'Active governance source of truth is not ready.';
|
|
1937
62
|
}
|
|
1938
63
|
export async function executeApplyNext(input) {
|
|
64
|
+
validateManifestActivationForExecution(input.inspection.manifest);
|
|
65
|
+
if (input.localRevalidation && (!input.inspection.loadedState || input.inspection.state.repository.id === 'unbound' ||
|
|
66
|
+
!input.assertReviewedPlan || !input.assertProtectedInputs)) {
|
|
67
|
+
throw new Error('Local revalidation requires a committed anchored v2 state and exact reviewed-plan/protected-input guards.');
|
|
68
|
+
}
|
|
69
|
+
return withProjectMutationLock(input.inspection.projectRoot, async (lease) => {
|
|
70
|
+
await lease.assertHeld();
|
|
71
|
+
await input.assertProtectedInputs?.();
|
|
72
|
+
if (!input.inspection.loadedState && input.inspection.state.repository.id === 'unbound' &&
|
|
73
|
+
input.inspection.readiness.nextReadyPhase?.startsWith('seed-')) {
|
|
74
|
+
await initializeExecutionAnchor(input.inspection, input.now ?? input.clock?.() ?? new Date());
|
|
75
|
+
input = { ...input, inspection: await input.reinspect() };
|
|
76
|
+
}
|
|
77
|
+
return executeApplyNextLocked(input, lease);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async function executeApplyNextLocked(input, lease) {
|
|
1939
81
|
const runner = input.runner ?? new NodeCommandRunner();
|
|
1940
82
|
const adapters = input.adapters ?? {};
|
|
1941
|
-
const
|
|
1942
|
-
const
|
|
83
|
+
const clock = () => input.now ?? input.clock?.() ?? new Date();
|
|
84
|
+
const now = clock();
|
|
85
|
+
const localRevalidation = input.localRevalidation ?? false;
|
|
86
|
+
const initialPlan = await buildSavedTransitionPlan({ inspection: input.inspection, runner, now, localRevalidation });
|
|
1943
87
|
if (!initialPlan) {
|
|
1944
|
-
const preview = await previewApplyNext({ inspection: input.inspection, runner, now, execute: true });
|
|
88
|
+
const preview = await previewApplyNext({ inspection: input.inspection, runner, now, execute: true, localRevalidation });
|
|
1945
89
|
return {
|
|
1946
90
|
...preview,
|
|
1947
91
|
applied: false,
|
|
@@ -1957,170 +101,158 @@ export async function executeApplyNext(input) {
|
|
|
1957
101
|
const phase = phaseById(input.inspection.graph, initialPlan.phaseId);
|
|
1958
102
|
assertPlanOperationsAllowed(initialPlan, phase);
|
|
1959
103
|
if (initialPlan.approval.evaluation.approvalRequired) {
|
|
1960
|
-
const preview = await previewApplyNext({ inspection: input.inspection, runner, now, execute: true });
|
|
104
|
+
const preview = await previewApplyNext({ inspection: input.inspection, runner, now, execute: true, localRevalidation });
|
|
1961
105
|
return {
|
|
1962
|
-
...preview,
|
|
1963
|
-
|
|
1964
|
-
executedPhase: null,
|
|
1965
|
-
noWrites: false,
|
|
1966
|
-
executedOperations: [],
|
|
1967
|
-
evidence: null,
|
|
1968
|
-
stateHash: null,
|
|
1969
|
-
rollbackPlan: initialPlan.rollbackPlan,
|
|
1970
|
-
cleanupWarnings: []
|
|
106
|
+
...preview, applied: false, executedPhase: null, noWrites: false,
|
|
107
|
+
executedOperations: [], evidence: null, stateHash: null, rollbackPlan: initialPlan.rollbackPlan, cleanupWarnings: []
|
|
1971
108
|
};
|
|
1972
109
|
}
|
|
110
|
+
await input.assertReviewedPlan?.(initialPlan);
|
|
111
|
+
await input.assertProtectedInputs?.();
|
|
1973
112
|
const saved = await saveTransitionPlan(input.inspection.projectRoot, initialPlan);
|
|
1974
113
|
const freshInspection = await input.reinspect();
|
|
1975
|
-
const freshPlan = await buildSavedTransitionPlan({ inspection: freshInspection, runner, now });
|
|
114
|
+
const freshPlan = await buildSavedTransitionPlan({ inspection: freshInspection, runner, now, localRevalidation });
|
|
1976
115
|
const freshnessIssues = comparePlanFreshness(initialPlan, freshPlan);
|
|
1977
116
|
if (freshnessIssues.length > 0) {
|
|
1978
117
|
return {
|
|
1979
|
-
schemaVersion: 1,
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
execute: true,
|
|
1983
|
-
applied: false,
|
|
1984
|
-
authorized: false,
|
|
1985
|
-
reason: 'stale-after-plan-save',
|
|
1986
|
-
message: freshnessIssues.join(' '),
|
|
1987
|
-
selectedPhase: initialPlan.phaseId,
|
|
1988
|
-
executedPhase: null,
|
|
1989
|
-
nextReadyPhase: freshInspection.readiness.nextReadyPhase,
|
|
118
|
+
schemaVersion: 1, command: 'governance apply-next', projectRoot: input.inspection.projectRoot,
|
|
119
|
+
execute: true, applied: false, authorized: false, reason: 'stale-after-plan-save', message: freshnessIssues.join(' '),
|
|
120
|
+
selectedPhase: initialPlan.phaseId, executedPhase: null, nextReadyPhase: freshInspection.readiness.nextReadyPhase,
|
|
1990
121
|
approval: initialPlan.approval,
|
|
1991
|
-
proposedMutations: {
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
operations: initialPlan.operations
|
|
1995
|
-
},
|
|
1996
|
-
savedPlan: saved,
|
|
1997
|
-
noWrites: false,
|
|
1998
|
-
blockers: freshnessIssues,
|
|
1999
|
-
executedOperations: [],
|
|
2000
|
-
evidence: null,
|
|
2001
|
-
stateHash: null,
|
|
2002
|
-
rollbackPlan: initialPlan.rollbackPlan,
|
|
2003
|
-
cleanupWarnings: []
|
|
122
|
+
proposedMutations: { local: initialPlan.mutationClasses.local, remote: initialPlan.mutationClasses.remote, operations: initialPlan.operations },
|
|
123
|
+
savedPlan: saved, noWrites: false, blockers: freshnessIssues, executedOperations: [],
|
|
124
|
+
evidence: null, stateHash: null, rollbackPlan: initialPlan.rollbackPlan, cleanupWarnings: []
|
|
2004
125
|
};
|
|
2005
126
|
}
|
|
127
|
+
if (freshPlan)
|
|
128
|
+
await input.assertReviewedPlan?.(freshPlan);
|
|
129
|
+
await input.assertProtectedInputs?.();
|
|
2006
130
|
const sourceBlocker = sourceOfTruthAllowsPhase(freshInspection, initialPlan.phaseId);
|
|
2007
131
|
if (sourceBlocker) {
|
|
2008
132
|
const nextState = blockedState({ inspection: freshInspection, phase, plan: initialPlan, blocker: sourceBlocker, now });
|
|
2009
|
-
const write = await writeOutcomeTransaction({
|
|
2010
|
-
projectRoot: freshInspection.projectRoot,
|
|
2011
|
-
plan: initialPlan,
|
|
2012
|
-
nextState
|
|
2013
|
-
});
|
|
133
|
+
const write = await writeOutcomeTransaction({ projectRoot: freshInspection.projectRoot, plan: initialPlan, nextState });
|
|
2014
134
|
return executionBlockedResult(freshInspection, initialPlan, saved, sourceBlocker, [], write.stateHash);
|
|
2015
135
|
}
|
|
136
|
+
await lease.assertHeld();
|
|
2016
137
|
const outcome = await executeBuiltInPhase({
|
|
2017
|
-
inspection: freshInspection,
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
adapters,
|
|
2022
|
-
now
|
|
2023
|
-
});
|
|
138
|
+
inspection: freshInspection, plan: initialPlan, phase, runner, adapters, now,
|
|
139
|
+
clock, lease
|
|
140
|
+
}, localRevalidation);
|
|
141
|
+
await input.assertProtectedInputs?.();
|
|
2024
142
|
const completedOperations = outcome.completedOperations ?? [];
|
|
143
|
+
for (const completed of completedOperations) {
|
|
144
|
+
assertOperationAllowed(phase, completed);
|
|
145
|
+
if (!initialPlan.operations.some((planned) => canonicalSha256(planned) === canonicalSha256(completed))) {
|
|
146
|
+
throw new Error(`Completed operation ${completed.actionId} was not in the reviewed plan.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const mutation of outcome.fileMutations ?? []) {
|
|
150
|
+
if (localRevalidation)
|
|
151
|
+
throw new Error('Local revalidation cannot persist source, spec, or seed file mutations.');
|
|
152
|
+
const allowed = initialPlan.operations.some((operation) => {
|
|
153
|
+
const prefix = operation.destination.pathParts;
|
|
154
|
+
return !operation.remote && prefix && prefix.every((part, index) => mutation.pathParts[index] === part) &&
|
|
155
|
+
['write-seed-tasks', 'write-openspec-governance', 'write-workflows', 'write-ruleset-source', 'delete-local-state'].includes(operation.mutationClass) &&
|
|
156
|
+
(mutation.type !== 'delete' || operation.destructive && prefix.length === mutation.pathParts.length);
|
|
157
|
+
});
|
|
158
|
+
if (!allowed)
|
|
159
|
+
throw new Error(`Outcome file mutation ${mutation.pathParts.join('/')} is outside the reviewed plan destinations.`);
|
|
160
|
+
}
|
|
161
|
+
const postSnapshot = await readActivationInputSnapshot(freshInspection.projectRoot, freshInspection.manifest, runner);
|
|
162
|
+
if (postSnapshot.baselineSha !== initialPlan.baselineDigest || phaseInputDigest(phase.id, postSnapshot) !== initialPlan.inputDigest) {
|
|
163
|
+
return executionBlockedResult(freshInspection, initialPlan, saved, 'Relevant project inputs changed during execution; no outcome was persisted.', completedOperations, freshInspection.loadedState?.contentHash ?? '', outcome.cleanupWarnings ?? [], false);
|
|
164
|
+
}
|
|
2025
165
|
if (outcome.status === 'blocked') {
|
|
2026
166
|
const blocker = outcome.blocker ?? `Phase ${phase.id} blocked.`;
|
|
2027
167
|
if (outcome.retryableWithoutStateMutation) {
|
|
2028
|
-
const stateHash = freshInspection.loadedState?.contentHash ??
|
|
2029
|
-
activationStateContentHash(canonicalJson(freshInspection.state));
|
|
168
|
+
const stateHash = freshInspection.loadedState?.contentHash ?? activationStateContentHash(canonicalJson(freshInspection.state));
|
|
2030
169
|
return executionBlockedResult(freshInspection, initialPlan, saved, blocker, completedOperations, stateHash, outcome.cleanupWarnings ?? [], false);
|
|
2031
170
|
}
|
|
2032
171
|
const nextState = blockedState({ inspection: freshInspection, phase, plan: initialPlan, blocker, now });
|
|
2033
|
-
const write = await writeOutcomeTransaction({
|
|
2034
|
-
projectRoot: freshInspection.projectRoot,
|
|
2035
|
-
plan: initialPlan,
|
|
2036
|
-
nextState
|
|
2037
|
-
});
|
|
172
|
+
const write = await writeOutcomeTransaction({ projectRoot: freshInspection.projectRoot, plan: initialPlan, nextState });
|
|
2038
173
|
return executionBlockedResult(freshInspection, initialPlan, saved, blocker, completedOperations, write.stateHash, outcome.cleanupWarnings ?? []);
|
|
2039
174
|
}
|
|
2040
175
|
const resultState = outcome.resultState ?? 'verified';
|
|
2041
176
|
if (!phase.terminalStates.includes(resultState)) {
|
|
2042
177
|
const blocker = `Phase adapter returned ${resultState}, which is not an allowed terminal state for ${phase.id}.`;
|
|
2043
178
|
const nextState = blockedState({ inspection: freshInspection, phase, plan: initialPlan, blocker, now });
|
|
2044
|
-
const write = await writeOutcomeTransaction({
|
|
2045
|
-
projectRoot: freshInspection.projectRoot,
|
|
2046
|
-
plan: initialPlan,
|
|
2047
|
-
nextState
|
|
2048
|
-
});
|
|
179
|
+
const write = await writeOutcomeTransaction({ projectRoot: freshInspection.projectRoot, plan: initialPlan, nextState });
|
|
2049
180
|
return executionBlockedResult(freshInspection, initialPlan, saved, blocker, completedOperations, write.stateHash, outcome.cleanupWarnings ?? []);
|
|
2050
181
|
}
|
|
182
|
+
if (outcome.stateOverride && outcome.stateOverride.repository.id !== freshInspection.state.repository.id) {
|
|
183
|
+
throw new Error('A phase outcome must not replace the immutable local execution anchor.');
|
|
184
|
+
}
|
|
185
|
+
if (outcome.stateOverride) {
|
|
186
|
+
if (canonicalSha256(outcome.stateOverride.phases) !== canonicalSha256(freshInspection.state.phases)) {
|
|
187
|
+
throw new Error('A phase adapter cannot rewrite authoritative phase history or unrelated phase states.');
|
|
188
|
+
}
|
|
189
|
+
if (phase.id !== 'phase-0-complete' &&
|
|
190
|
+
canonicalSha256(outcome.stateOverride.remoteBinding ?? null) !== canonicalSha256(freshInspection.state.remoteBinding ?? null)) {
|
|
191
|
+
throw new Error('Only verified Phase 0 discovery may establish a remote repository binding.');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (phase.approvalGate.required) {
|
|
195
|
+
const finalInspection = await input.reinspect();
|
|
196
|
+
const finalPlan = await buildSavedTransitionPlan({ inspection: finalInspection, runner, now: clock(), localRevalidation });
|
|
197
|
+
if (finalPlan)
|
|
198
|
+
await input.assertReviewedPlan?.(finalPlan);
|
|
199
|
+
if (!finalPlan || finalPlan.phaseId !== phase.id || finalPlan.approval.evaluation.approvalRequired ||
|
|
200
|
+
finalPlan.approval.envelopeHash !== initialPlan.approval.envelopeHash) {
|
|
201
|
+
return executionBlockedResult(freshInspection, initialPlan, saved, 'Approval changed or expired before outcome persistence; no successful outcome was recorded.', completedOperations, freshInspection.loadedState?.contentHash ?? '', outcome.cleanupWarnings ?? [], false);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const outcomeInspection = { ...freshInspection, state: outcome.stateOverride ?? freshInspection.state };
|
|
205
|
+
const boundPayload = isRecord(outcome.evidencePayload)
|
|
206
|
+
? { ...outcome.evidencePayload, planDigest: initialPlan.planDigest, savedPlanDigest: canonicalSha256(initialPlan) }
|
|
207
|
+
: outcome.evidencePayload;
|
|
208
|
+
const outcomeNow = clock();
|
|
2051
209
|
let evidenceRecord;
|
|
2052
210
|
let evidenceParts;
|
|
2053
211
|
let evidenceReference;
|
|
2054
212
|
if (resultState !== 'approved') {
|
|
2055
213
|
const evidenceId = `${phase.id}-${safeTimestamp(now.toISOString())}`;
|
|
2056
214
|
const header = evidenceHeaderFor({
|
|
2057
|
-
inspection:
|
|
2058
|
-
|
|
2059
|
-
plan: initialPlan,
|
|
2060
|
-
result: resultState,
|
|
2061
|
-
now
|
|
215
|
+
inspection: outcomeInspection, phase, plan: initialPlan, result: resultState, now: outcomeNow,
|
|
216
|
+
payload: boundPayload, liveReadback: outcome.liveReadback
|
|
2062
217
|
});
|
|
2063
218
|
evidenceRecord = {
|
|
2064
|
-
evidenceId,
|
|
2065
|
-
header,
|
|
219
|
+
evidenceId, header,
|
|
2066
220
|
...(outcome.liveReadback ? { liveReadback: outcome.liveReadback } : {}),
|
|
2067
|
-
...(
|
|
221
|
+
...(boundPayload !== undefined ? { payload: boundPayload } : {})
|
|
2068
222
|
};
|
|
2069
223
|
evidenceParts = evidencePathParts(evidenceId);
|
|
2070
|
-
evidenceReference = {
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
224
|
+
evidenceReference = { phaseId: phase.id, evidenceId, headerDigest: evidenceHeaderDigest(header), result: header.result };
|
|
225
|
+
const validation = validateEvidenceFreshness(evidenceRecord, {
|
|
226
|
+
...freshInspection.contexts[phase.id],
|
|
227
|
+
remoteBindingDigest: remoteBindingDigest(outcomeInspection.state.remoteBinding),
|
|
228
|
+
evidenceReferences: [evidenceReference], reviewedPlans: [initialPlan],
|
|
229
|
+
...(phase.id === 'seed-archived' && postSnapshot.workflowSpecDigest ? { workflowSpecDigest: postSnapshot.workflowSpecDigest } : {}),
|
|
230
|
+
now: outcomeNow
|
|
231
|
+
});
|
|
232
|
+
if (!validation.valid) {
|
|
233
|
+
return executionBlockedResult(freshInspection, initialPlan, saved, `Completed outcome rejected: ${validation.issues.map((issue) => issue.message).join(' ')}`, completedOperations, freshInspection.loadedState?.contentHash ?? '', outcome.cleanupWarnings ?? [], false);
|
|
234
|
+
}
|
|
2076
235
|
}
|
|
2077
236
|
const nextState = nextStateForOutcome({
|
|
2078
|
-
inspection: freshInspection,
|
|
2079
|
-
|
|
2080
|
-
plan: initialPlan,
|
|
2081
|
-
resultState,
|
|
2082
|
-
evidenceReference,
|
|
2083
|
-
override: outcome.stateOverride,
|
|
2084
|
-
now
|
|
237
|
+
inspection: freshInspection, phase, plan: initialPlan, resultState,
|
|
238
|
+
evidenceReference, override: outcome.stateOverride, now
|
|
2085
239
|
});
|
|
240
|
+
await input.assertProtectedInputs?.();
|
|
2086
241
|
const write = await writeOutcomeTransaction({
|
|
2087
|
-
projectRoot: freshInspection.projectRoot,
|
|
2088
|
-
|
|
2089
|
-
nextState,
|
|
2090
|
-
evidenceRecord,
|
|
2091
|
-
evidencePathParts: evidenceParts,
|
|
2092
|
-
fileMutations: outcome.fileMutations
|
|
242
|
+
projectRoot: freshInspection.projectRoot, plan: initialPlan, nextState, evidenceRecord,
|
|
243
|
+
evidencePathParts: evidenceParts, fileMutations: outcome.fileMutations, filePreconditions: outcome.filePreconditions
|
|
2093
244
|
});
|
|
245
|
+
await input.assertProtectedInputs?.();
|
|
2094
246
|
const rollbackPlan = rollbackPlanForPhase(phase, completedOperations);
|
|
2095
247
|
return {
|
|
2096
|
-
schemaVersion: 1,
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
selectedPhase: phase.id,
|
|
2105
|
-
executedPhase: phase.id,
|
|
2106
|
-
nextReadyPhase: phase.id,
|
|
2107
|
-
approval: initialPlan.approval,
|
|
2108
|
-
proposedMutations: {
|
|
2109
|
-
local: initialPlan.mutationClasses.local,
|
|
2110
|
-
remote: initialPlan.mutationClasses.remote,
|
|
2111
|
-
operations: initialPlan.operations
|
|
2112
|
-
},
|
|
2113
|
-
savedPlan: saved,
|
|
2114
|
-
noWrites: false,
|
|
2115
|
-
blockers: [],
|
|
2116
|
-
executedOperations: [
|
|
2117
|
-
...completedOperations,
|
|
2118
|
-
...(evidenceParts ? [evidenceWriteOperation(phase, evidenceParts)] : []),
|
|
2119
|
-
stateWriteOperation(phase)
|
|
2120
|
-
],
|
|
2121
|
-
evidence: write.evidence,
|
|
2122
|
-
stateHash: write.stateHash,
|
|
2123
|
-
rollbackPlan,
|
|
248
|
+
schemaVersion: 1, command: 'governance apply-next', projectRoot: freshInspection.projectRoot,
|
|
249
|
+
execute: true, applied: true, authorized: true, reason: 'phase-executed',
|
|
250
|
+
message: `Executed one phase: ${phase.id}.`, selectedPhase: phase.id, executedPhase: phase.id,
|
|
251
|
+
nextReadyPhase: phase.id, approval: initialPlan.approval,
|
|
252
|
+
proposedMutations: { local: initialPlan.mutationClasses.local, remote: initialPlan.mutationClasses.remote, operations: initialPlan.operations },
|
|
253
|
+
savedPlan: saved, noWrites: false, blockers: [],
|
|
254
|
+
executedOperations: [...completedOperations, ...(evidenceParts ? [evidenceWriteOperation(phase, evidenceParts)] : []), stateWriteOperation(phase)],
|
|
255
|
+
evidence: write.evidence, stateHash: write.stateHash, rollbackPlan,
|
|
2124
256
|
cleanupWarnings: [...rollbackPlan.cleanupWarnings, ...(outcome.cleanupWarnings ?? [])]
|
|
2125
257
|
};
|
|
2126
258
|
}
|
|
@@ -2128,33 +260,13 @@ function executionBlockedResult(inspection, plan, saved, blocker, completedOpera
|
|
|
2128
260
|
const phase = phaseById(inspection.graph, plan.phaseId);
|
|
2129
261
|
const rollbackPlan = rollbackPlanForPhase(phase, completedOperations);
|
|
2130
262
|
return {
|
|
2131
|
-
schemaVersion: 1,
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
message: blocker,
|
|
2139
|
-
selectedPhase: plan.phaseId,
|
|
2140
|
-
executedPhase: null,
|
|
2141
|
-
nextReadyPhase: plan.phaseId,
|
|
2142
|
-
approval: plan.approval,
|
|
2143
|
-
proposedMutations: {
|
|
2144
|
-
local: plan.mutationClasses.local,
|
|
2145
|
-
remote: plan.mutationClasses.remote,
|
|
2146
|
-
operations: plan.operations
|
|
2147
|
-
},
|
|
2148
|
-
savedPlan: saved,
|
|
2149
|
-
noWrites: false,
|
|
2150
|
-
blockers: [blocker],
|
|
2151
|
-
executedOperations: [
|
|
2152
|
-
...completedOperations,
|
|
2153
|
-
...(stateWritten ? [stateWriteOperation(phase)] : [])
|
|
2154
|
-
],
|
|
2155
|
-
evidence: null,
|
|
2156
|
-
stateHash: stateHashValue,
|
|
2157
|
-
rollbackPlan,
|
|
263
|
+
schemaVersion: 1, command: 'governance apply-next', projectRoot: inspection.projectRoot,
|
|
264
|
+
execute: true, applied: false, authorized: false, reason: 'blocked', message: blocker,
|
|
265
|
+
selectedPhase: plan.phaseId, executedPhase: null, nextReadyPhase: plan.phaseId, approval: plan.approval,
|
|
266
|
+
proposedMutations: { local: plan.mutationClasses.local, remote: plan.mutationClasses.remote, operations: plan.operations },
|
|
267
|
+
savedPlan: saved, noWrites: false, blockers: [blocker],
|
|
268
|
+
executedOperations: [...completedOperations, ...(stateWritten ? [stateWriteOperation(phase)] : [])],
|
|
269
|
+
evidence: null, stateHash: stateHashValue, rollbackPlan,
|
|
2158
270
|
cleanupWarnings: [...rollbackPlan.cleanupWarnings, ...cleanupWarnings]
|
|
2159
271
|
};
|
|
2160
272
|
}
|