@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
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { chmod, lstat, mkdir, open, readdir, realpath, rename, rmdir, unlink } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { canonicalJson, canonicalSha256, isRecord } from '../../domain/governance/activation/canonical-json.js';
|
|
6
|
+
import { FileSystemError } from '../../domain/project/errors.js';
|
|
7
|
+
import { validateArtifactPathParts } from '../../domain/project/paths.js';
|
|
8
|
+
import { reviewedUpdateTransactionPathParts } from '../../domain/project/reviewed-update-artifacts.js';
|
|
9
|
+
import { errorCode, errorMessage } from './errors.js';
|
|
10
|
+
import { withProjectMutationLock } from './project-lock.js';
|
|
11
|
+
import { ProjectFileTransactionError } from './project-transaction.js';
|
|
12
|
+
export { reviewedUpdateTransactionPathParts, reviewedUpdateTransactionSchemaVersion } from '../../domain/project/reviewed-update-artifacts.js';
|
|
13
|
+
export class ReviewedUpdateTransactionError extends ProjectFileTransactionError {
|
|
14
|
+
committed = false;
|
|
15
|
+
constructor(message, rollbackFailures = []) {
|
|
16
|
+
super(message, rollbackFailures);
|
|
17
|
+
this.name = 'ReviewedUpdateTransactionError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const MAX_MUTATIONS = 1024;
|
|
21
|
+
const MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
22
|
+
const MAX_SNAPSHOT_BYTES = 16 * 1024 * 1024;
|
|
23
|
+
const MAX_JOURNAL_BYTES = 32 * 1024 * 1024;
|
|
24
|
+
const DIGEST = /^[a-f0-9]{64}$/;
|
|
25
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
26
|
+
const privateFileMode = process.platform === 'win32' ? 0o666 : 0o600;
|
|
27
|
+
function fail(message) {
|
|
28
|
+
throw new FileSystemError(`Reviewed update transaction: ${message}`);
|
|
29
|
+
}
|
|
30
|
+
function hash(bytes) {
|
|
31
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
32
|
+
}
|
|
33
|
+
function key(parts) {
|
|
34
|
+
return parts.join('/');
|
|
35
|
+
}
|
|
36
|
+
function folded(value) {
|
|
37
|
+
return value.normalize('NFC').toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
function validParts(value) {
|
|
40
|
+
const parts = validateArtifactPathParts(value, 'Reviewed update path');
|
|
41
|
+
if (parts.length > 64 || key(parts).length > 2048 ||
|
|
42
|
+
parts.some((part) => part.length > 255 || /[<>:"|?*\u0000-\u001f\u007f]/u.test(part))) {
|
|
43
|
+
fail('a path is too long or contains non-portable characters.');
|
|
44
|
+
}
|
|
45
|
+
return parts;
|
|
46
|
+
}
|
|
47
|
+
function exactKeys(value, keys) {
|
|
48
|
+
if (!isRecord(value) || Object.keys(value).length !== keys.length ||
|
|
49
|
+
keys.some((entry) => !Object.hasOwn(value, entry)))
|
|
50
|
+
fail('malformed recovery journal fields.');
|
|
51
|
+
}
|
|
52
|
+
function assertDigest(value) {
|
|
53
|
+
if (typeof value !== 'string' || !DIGEST.test(value))
|
|
54
|
+
fail('expected a full lowercase SHA-256 digest.');
|
|
55
|
+
}
|
|
56
|
+
function assertMode(value) {
|
|
57
|
+
if (!Number.isInteger(value) || value < 0 || value > 0o7777) {
|
|
58
|
+
fail('invalid snapshot mode.');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function targetMode(mode, original) {
|
|
62
|
+
return reviewedUpdateTargetMode(mode, original.kind === 'file' ? original.mode : undefined);
|
|
63
|
+
}
|
|
64
|
+
export function reviewedUpdateTargetMode(mode, originalMode) {
|
|
65
|
+
if (mode === undefined)
|
|
66
|
+
return originalMode ?? privateFileMode;
|
|
67
|
+
return process.platform === 'win32' ? (mode & 0o200 ? 0o666 : 0o444) : mode;
|
|
68
|
+
}
|
|
69
|
+
async function canonicalRoot(projectRoot) {
|
|
70
|
+
const root = path.resolve(projectRoot);
|
|
71
|
+
const details = await lstat(root);
|
|
72
|
+
if (!details.isDirectory() || details.isSymbolicLink())
|
|
73
|
+
fail('project root must be a directory, not a symlink or junction.');
|
|
74
|
+
return realpath(root);
|
|
75
|
+
}
|
|
76
|
+
async function safePath(root, parts) {
|
|
77
|
+
validParts(parts);
|
|
78
|
+
let current = root;
|
|
79
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
80
|
+
const part = parts[index];
|
|
81
|
+
let names;
|
|
82
|
+
try {
|
|
83
|
+
names = await readdir(current);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (errorCode(error) === 'ENOENT')
|
|
87
|
+
return path.join(root, ...parts);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
const aliases = names.filter((name) => folded(name) === folded(part));
|
|
91
|
+
if (aliases.length > 1 || aliases.length === 1 && aliases[0] !== part) {
|
|
92
|
+
fail(`case or Unicode collision at ${key(parts.slice(0, index + 1))}.`);
|
|
93
|
+
}
|
|
94
|
+
current = path.join(current, part);
|
|
95
|
+
try {
|
|
96
|
+
const details = await lstat(current);
|
|
97
|
+
if (details.isSymbolicLink())
|
|
98
|
+
fail(`symlink or junction at ${key(parts.slice(0, index + 1))}.`);
|
|
99
|
+
if (index < parts.length - 1 && !details.isDirectory()) {
|
|
100
|
+
fail(`path parent is not a directory: ${key(parts.slice(0, index + 1))}.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (errorCode(error) === 'ENOENT')
|
|
105
|
+
return path.join(root, ...parts);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return current;
|
|
110
|
+
}
|
|
111
|
+
async function readSnapshot(root, parts, maximum = MAX_FILE_BYTES) {
|
|
112
|
+
const target = await safePath(root, parts);
|
|
113
|
+
let handle;
|
|
114
|
+
try {
|
|
115
|
+
const before = await lstat(target);
|
|
116
|
+
if (!before.isFile() || before.isSymbolicLink())
|
|
117
|
+
fail(`not a regular file: ${key(parts)}.`);
|
|
118
|
+
if (before.size > maximum)
|
|
119
|
+
fail(`snapshot exceeds the bounded size limit: ${key(parts)}.`);
|
|
120
|
+
handle = await open(target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
121
|
+
const details = await handle.stat();
|
|
122
|
+
if (!details.isFile() || details.dev !== before.dev || details.ino !== before.ino || details.size > maximum) {
|
|
123
|
+
fail(`file changed while reading: ${key(parts)}.`);
|
|
124
|
+
}
|
|
125
|
+
const buffer = Buffer.alloc(details.size + 1);
|
|
126
|
+
let length = 0;
|
|
127
|
+
while (length < buffer.length) {
|
|
128
|
+
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
|
|
129
|
+
if (bytesRead === 0)
|
|
130
|
+
break;
|
|
131
|
+
length += bytesRead;
|
|
132
|
+
}
|
|
133
|
+
const content = buffer.subarray(0, length);
|
|
134
|
+
const after = await lstat(target);
|
|
135
|
+
if (content.length > maximum || content.length !== details.size ||
|
|
136
|
+
after.dev !== details.dev || after.ino !== details.ino ||
|
|
137
|
+
after.size !== content.length || after.mtimeMs !== details.mtimeMs ||
|
|
138
|
+
after.mode !== details.mode)
|
|
139
|
+
fail(`file changed while reading: ${key(parts)}.`);
|
|
140
|
+
return { pathParts: [...parts], content, mode: details.mode & 0o7777 };
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (!handle && errorCode(error) === 'ENOENT')
|
|
144
|
+
return { pathParts: [...parts] };
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await handle?.close();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function storeSnapshot(snapshot) {
|
|
152
|
+
if (snapshot.content === undefined) {
|
|
153
|
+
if (snapshot.mode !== undefined)
|
|
154
|
+
fail('a missing snapshot cannot have a mode.');
|
|
155
|
+
return { kind: 'missing' };
|
|
156
|
+
}
|
|
157
|
+
if (!Buffer.isBuffer(snapshot.content) || snapshot.content.length > MAX_FILE_BYTES) {
|
|
158
|
+
fail('invalid or oversized snapshot bytes.');
|
|
159
|
+
}
|
|
160
|
+
assertMode(snapshot.mode);
|
|
161
|
+
return { kind: 'file', bytes: snapshot.content.toString('base64'), sha256: hash(snapshot.content), mode: snapshot.mode };
|
|
162
|
+
}
|
|
163
|
+
function matches(snapshot, stored) {
|
|
164
|
+
return stored.kind === 'missing'
|
|
165
|
+
? snapshot.content === undefined
|
|
166
|
+
: snapshot.content !== undefined && snapshot.mode === stored.mode && hash(snapshot.content) === stored.sha256;
|
|
167
|
+
}
|
|
168
|
+
async function assertSnapshot(root, parts, stored) {
|
|
169
|
+
if (!matches(await readSnapshot(root, parts), stored))
|
|
170
|
+
fail(`target changed after review: ${key(parts)}.`);
|
|
171
|
+
}
|
|
172
|
+
function parseSnapshot(value) {
|
|
173
|
+
if (isRecord(value) && value.kind === 'missing') {
|
|
174
|
+
exactKeys(value, ['kind']);
|
|
175
|
+
return { kind: 'missing' };
|
|
176
|
+
}
|
|
177
|
+
exactKeys(value, ['kind', 'bytes', 'sha256', 'mode']);
|
|
178
|
+
assertMode(value.mode);
|
|
179
|
+
assertDigest(value.sha256);
|
|
180
|
+
if (value.kind !== 'file' || typeof value.bytes !== 'string' ||
|
|
181
|
+
value.bytes.length > Math.ceil(MAX_FILE_BYTES / 3) * 4)
|
|
182
|
+
fail('invalid stored file snapshot.');
|
|
183
|
+
const bytes = Buffer.from(value.bytes, 'base64');
|
|
184
|
+
if (bytes.length > MAX_FILE_BYTES || bytes.toString('base64') !== value.bytes || hash(bytes) !== value.sha256) {
|
|
185
|
+
fail('stored snapshot digest or encoding does not match its exact bytes.');
|
|
186
|
+
}
|
|
187
|
+
return { kind: 'file', bytes: value.bytes, sha256: value.sha256, mode: value.mode };
|
|
188
|
+
}
|
|
189
|
+
function validatePaths(paths) {
|
|
190
|
+
const files = new Set();
|
|
191
|
+
const spelling = new Map();
|
|
192
|
+
for (const parts of [...paths, [...reviewedUpdateTransactionPathParts]]) {
|
|
193
|
+
for (let count = 1; count <= parts.length; count += 1) {
|
|
194
|
+
const prefix = key(parts.slice(0, count));
|
|
195
|
+
const identity = folded(prefix);
|
|
196
|
+
const prior = spelling.get(identity);
|
|
197
|
+
if (prior !== undefined && prior !== prefix)
|
|
198
|
+
fail(`case-colliding inventory path: ${prefix}.`);
|
|
199
|
+
if (count < parts.length && files.has(identity))
|
|
200
|
+
fail(`file/directory inventory collision: ${prefix}.`);
|
|
201
|
+
spelling.set(identity, prefix);
|
|
202
|
+
}
|
|
203
|
+
const identity = folded(key(parts));
|
|
204
|
+
if (files.has(identity) || [...spelling.keys()].some((name) => name.startsWith(`${identity}/`))) {
|
|
205
|
+
fail(`duplicate or overlapping mutation: ${key(parts)}.`);
|
|
206
|
+
}
|
|
207
|
+
files.add(identity);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function validateInventory(mutations) {
|
|
211
|
+
validatePaths(mutations.map((entry) => entry.pathParts));
|
|
212
|
+
const size = mutations.reduce((sum, mutation) => sum +
|
|
213
|
+
(mutation.original.kind === 'file' ? Buffer.byteLength(mutation.original.bytes, 'base64') : 0) +
|
|
214
|
+
(mutation.target.kind === 'file' ? Buffer.byteLength(mutation.target.bytes, 'base64') : 0), 0);
|
|
215
|
+
if (size > MAX_SNAPSHOT_BYTES)
|
|
216
|
+
fail('transaction snapshots exceed the bounded size limit.');
|
|
217
|
+
}
|
|
218
|
+
function bodyOf(header) {
|
|
219
|
+
const { transactionDigest: _digest, ...body } = header;
|
|
220
|
+
return body;
|
|
221
|
+
}
|
|
222
|
+
function parseHeader(value, root) {
|
|
223
|
+
exactKeys(value, ['schemaVersion', 'projectRoot', 'planFingerprint', 'nonce', 'mutations', 'missingDirectories', 'transactionDigest']);
|
|
224
|
+
assertDigest(value.planFingerprint);
|
|
225
|
+
assertDigest(value.transactionDigest);
|
|
226
|
+
if (value.schemaVersion !== 1 || value.projectRoot !== root ||
|
|
227
|
+
typeof value.nonce !== 'string' || !UUID.test(value.nonce) ||
|
|
228
|
+
!Array.isArray(value.mutations) || value.mutations.length === 0 || value.mutations.length > MAX_MUTATIONS ||
|
|
229
|
+
!Array.isArray(value.missingDirectories) || value.missingDirectories.length > MAX_MUTATIONS * 64 + 1) {
|
|
230
|
+
fail('unsupported, wrong-project, or oversized recovery journal.');
|
|
231
|
+
}
|
|
232
|
+
const mutations = value.mutations.map((entry) => {
|
|
233
|
+
const hasMode = isRecord(entry) && Object.hasOwn(entry, 'mode');
|
|
234
|
+
exactKeys(entry, ['type', 'pathParts', 'original', 'target', ...(hasMode ? ['mode'] : [])]);
|
|
235
|
+
if (hasMode)
|
|
236
|
+
assertMode(entry.mode);
|
|
237
|
+
const original = parseSnapshot(entry.original);
|
|
238
|
+
const target = parseSnapshot(entry.target);
|
|
239
|
+
if (entry.type !== 'write' && entry.type !== 'delete' ||
|
|
240
|
+
entry.type === 'write' && target.kind !== 'file' ||
|
|
241
|
+
entry.type === 'delete' && target.kind !== 'missing')
|
|
242
|
+
fail('invalid serialized mutation type or target.');
|
|
243
|
+
if (entry.type === 'delete' && hasMode ||
|
|
244
|
+
target.kind === 'file' && target.mode !== targetMode(entry.mode, original)) {
|
|
245
|
+
fail('stored target mode does not match its approved mutation.');
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
type: entry.type, pathParts: validParts(entry.pathParts), original, target,
|
|
249
|
+
...(hasMode ? { mode: entry.mode } : {})
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
validateInventory(mutations);
|
|
253
|
+
const missingDirectories = value.missingDirectories.map(validParts);
|
|
254
|
+
const seen = new Set();
|
|
255
|
+
for (const parts of missingDirectories) {
|
|
256
|
+
const name = key(parts);
|
|
257
|
+
if (seen.has(folded(name)) ||
|
|
258
|
+
name !== '.liftoff' && !mutations.some((entry) => entry.type === 'write' && key(entry.pathParts).startsWith(`${name}/`))) {
|
|
259
|
+
fail('invalid or duplicate directory cleanup inventory.');
|
|
260
|
+
}
|
|
261
|
+
if (mutations.some((entry) => entry.original.kind === 'file' && key(entry.pathParts).startsWith(`${name}/`))) {
|
|
262
|
+
fail('an original file cannot have an originally missing parent.');
|
|
263
|
+
}
|
|
264
|
+
seen.add(folded(name));
|
|
265
|
+
}
|
|
266
|
+
const header = {
|
|
267
|
+
schemaVersion: 1, projectRoot: root, planFingerprint: value.planFingerprint, nonce: value.nonce,
|
|
268
|
+
mutations, missingDirectories, transactionDigest: value.transactionDigest
|
|
269
|
+
};
|
|
270
|
+
if (canonicalSha256(bodyOf(header)) !== header.transactionDigest)
|
|
271
|
+
fail('transaction digest does not match the journal.');
|
|
272
|
+
return header;
|
|
273
|
+
}
|
|
274
|
+
function frameDigest(header, frame) {
|
|
275
|
+
return canonicalSha256({ schemaVersion: 1, transactionDigest: header.transactionDigest, ...frame });
|
|
276
|
+
}
|
|
277
|
+
function rollbackCleanupDigest(header) {
|
|
278
|
+
return canonicalSha256({ schemaVersion: 1, transactionDigest: header.transactionDigest, phase: 'rollback-cleanup-only' });
|
|
279
|
+
}
|
|
280
|
+
function parseCanonicalLine(line) {
|
|
281
|
+
let value;
|
|
282
|
+
try {
|
|
283
|
+
value = JSON.parse(line);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
fail('malformed recovery journal JSON.');
|
|
287
|
+
}
|
|
288
|
+
if (canonicalJson(value) !== `${line}\n`)
|
|
289
|
+
fail('recovery journal is not strict canonical JSON.');
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
async function loadJournal(root, store) {
|
|
293
|
+
const snapshot = await readSnapshot(root, reviewedUpdateTransactionPathParts, MAX_JOURNAL_BYTES);
|
|
294
|
+
if (!snapshot.content)
|
|
295
|
+
return undefined;
|
|
296
|
+
if (snapshot.mode !== privateFileMode)
|
|
297
|
+
fail('recovery journal must have restrictive permissions.');
|
|
298
|
+
const text = snapshot.content.toString('utf8');
|
|
299
|
+
if (!Buffer.from(text, 'utf8').equals(snapshot.content))
|
|
300
|
+
fail('recovery journal is not UTF-8.');
|
|
301
|
+
const lines = text.split('\n');
|
|
302
|
+
const tail = lines.pop();
|
|
303
|
+
if (!lines.length)
|
|
304
|
+
fail('incomplete recovery journal header.');
|
|
305
|
+
const header = parseHeader(parseCanonicalLine(lines[0]), root);
|
|
306
|
+
let pendingIndex = -1;
|
|
307
|
+
let committed = false;
|
|
308
|
+
let lastFrame;
|
|
309
|
+
for (const line of lines.slice(1)) {
|
|
310
|
+
const frame = parseCanonicalLine(line);
|
|
311
|
+
if (committed || !isRecord(frame))
|
|
312
|
+
fail('invalid recovery phase sequence.');
|
|
313
|
+
if (frame.phase === 'mutation') {
|
|
314
|
+
exactKeys(frame, ['phase', 'index']);
|
|
315
|
+
if (frame.index !== pendingIndex + 1 || pendingIndex + 1 >= header.mutations.length)
|
|
316
|
+
fail('invalid mutation checkpoint.');
|
|
317
|
+
pendingIndex += 1;
|
|
318
|
+
lastFrame = { phase: 'mutation', index: pendingIndex };
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
exactKeys(frame, ['phase']);
|
|
322
|
+
if (frame.phase !== 'committed' || pendingIndex !== header.mutations.length - 1)
|
|
323
|
+
fail('invalid commit checkpoint.');
|
|
324
|
+
committed = true;
|
|
325
|
+
lastFrame = { phase: 'committed' };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (tail) {
|
|
329
|
+
const next = pendingIndex + 1 < header.mutations.length
|
|
330
|
+
? { phase: 'mutation', index: pendingIndex + 1 } : { phase: 'committed' };
|
|
331
|
+
// An interrupted append is accepted only as an exact prefix of the one possible next record.
|
|
332
|
+
if (committed || !canonicalJson(next).startsWith(tail))
|
|
333
|
+
fail('malformed trailing recovery checkpoint.');
|
|
334
|
+
}
|
|
335
|
+
if (!store) {
|
|
336
|
+
fail('missing or invalid user-local transaction approval; the project journal cannot authorize recovery.');
|
|
337
|
+
}
|
|
338
|
+
const approved = await store.verify(header.planFingerprint, header.transactionDigest) === true;
|
|
339
|
+
const cleanupOnly = !approved && await store.verify(header.planFingerprint, rollbackCleanupDigest(header)) === true;
|
|
340
|
+
if (!approved && !cleanupOnly) {
|
|
341
|
+
fail('missing or invalid user-local transaction approval; the project journal cannot authorize recovery.');
|
|
342
|
+
}
|
|
343
|
+
if (approved && lastFrame && await store.verify(header.planFingerprint, frameDigest(header, lastFrame)) !== true) {
|
|
344
|
+
fail('recovery checkpoint has no matching user-local approval seal.');
|
|
345
|
+
}
|
|
346
|
+
// A durable external commit seal wins even if the local commit append was interrupted or truncated.
|
|
347
|
+
const sealedCommit = await store.verify(header.planFingerprint, frameDigest(header, { phase: 'committed' }));
|
|
348
|
+
if (committed && sealedCommit !== true)
|
|
349
|
+
fail('committed journal has no matching external commit seal.');
|
|
350
|
+
if (cleanupOnly && !sealedCommit) {
|
|
351
|
+
// This separately sealed capability cannot restore files: every original must already be intact.
|
|
352
|
+
for (const mutation of header.mutations)
|
|
353
|
+
await assertSnapshot(root, mutation.pathParts, mutation.original);
|
|
354
|
+
}
|
|
355
|
+
return { header, snapshot, pendingIndex, committed: sealedCommit === true, rollbackComplete: cleanupOnly && !sealedCommit };
|
|
356
|
+
}
|
|
357
|
+
function temporaryParts(header, index, restore) {
|
|
358
|
+
return [
|
|
359
|
+
...header.mutations[index].pathParts.slice(0, -1),
|
|
360
|
+
`.liftoff-reviewed-${header.transactionDigest}-${index}-${restore ? 'original' : 'target'}.tmp`
|
|
361
|
+
];
|
|
362
|
+
}
|
|
363
|
+
async function syncDirectory(directory) {
|
|
364
|
+
let handle;
|
|
365
|
+
try {
|
|
366
|
+
handle = await open(directory, 'r');
|
|
367
|
+
await handle.sync();
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
if (process.platform !== 'win32' ||
|
|
371
|
+
!['EACCES', 'EPERM', 'EINVAL', 'ENOTSUP', 'EISDIR'].includes(String(errorCode(error))))
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
await handle?.close();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async function ensureParents(root, parts, permitted) {
|
|
379
|
+
for (let count = 1; count < parts.length; count += 1) {
|
|
380
|
+
const parent = parts.slice(0, count);
|
|
381
|
+
const native = await safePath(root, parent);
|
|
382
|
+
try {
|
|
383
|
+
if (!(await lstat(native)).isDirectory())
|
|
384
|
+
fail(`not a directory: ${key(parent)}.`);
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
if (errorCode(error) !== 'ENOENT')
|
|
388
|
+
throw error;
|
|
389
|
+
if (!permitted.some((allowed) => key(allowed) === key(parent)))
|
|
390
|
+
fail(`parent changed after review: ${key(parent)}.`);
|
|
391
|
+
await mkdir(native, { mode: 0o700 });
|
|
392
|
+
await chmod(await safePath(root, parent), 0o700);
|
|
393
|
+
await syncDirectory(path.dirname(native));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async function assertJournalCurrent(root, snapshot) {
|
|
398
|
+
const current = await readSnapshot(root, reviewedUpdateTransactionPathParts, MAX_JOURNAL_BYTES);
|
|
399
|
+
if (!current.content?.equals(snapshot.content) || current.mode !== snapshot.mode) {
|
|
400
|
+
fail('recovery journal changed; the changed file was preserved.');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function createJournal(root, header, lease) {
|
|
404
|
+
await ensureParents(root, reviewedUpdateTransactionPathParts, header.missingDirectories);
|
|
405
|
+
const native = await safePath(root, reviewedUpdateTransactionPathParts);
|
|
406
|
+
await lease.assertHeld();
|
|
407
|
+
const handle = await open(native, 'wx', 0o600);
|
|
408
|
+
let identity;
|
|
409
|
+
let closed = false;
|
|
410
|
+
const bytes = Buffer.from(canonicalJson(header));
|
|
411
|
+
try {
|
|
412
|
+
identity = await handle.stat();
|
|
413
|
+
await handle.chmod(0o600);
|
|
414
|
+
await handle.writeFile(bytes);
|
|
415
|
+
await handle.sync();
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
const failures = [];
|
|
419
|
+
try {
|
|
420
|
+
await handle.close();
|
|
421
|
+
closed = true;
|
|
422
|
+
await lease.assertHeld();
|
|
423
|
+
const current = await readSnapshot(root, reviewedUpdateTransactionPathParts, MAX_JOURNAL_BYTES);
|
|
424
|
+
const details = await lstat(native);
|
|
425
|
+
if (!identity || details.dev !== identity.dev || details.ino !== identity.ino ||
|
|
426
|
+
current.mode !== privateFileMode || !current.content ||
|
|
427
|
+
!bytes.subarray(0, current.content.length).equals(current.content)) {
|
|
428
|
+
fail('partially created recovery journal changed; it was preserved.');
|
|
429
|
+
}
|
|
430
|
+
await unlink(native);
|
|
431
|
+
await syncDirectory(path.dirname(native));
|
|
432
|
+
}
|
|
433
|
+
catch (cleanupError) {
|
|
434
|
+
failures.push(errorMessage(cleanupError));
|
|
435
|
+
}
|
|
436
|
+
throw new FileSystemError(`Unable to create ${key(reviewedUpdateTransactionPathParts)}: ${errorMessage(error)}` +
|
|
437
|
+
(failures.length ? ` Cleanup failed: ${failures.join('; ')}` : ''));
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
if (!closed)
|
|
441
|
+
await handle.close();
|
|
442
|
+
}
|
|
443
|
+
await syncDirectory(path.dirname(native));
|
|
444
|
+
return readSnapshot(root, reviewedUpdateTransactionPathParts, MAX_JOURNAL_BYTES);
|
|
445
|
+
}
|
|
446
|
+
async function appendFrame(root, snapshot, frame, lease) {
|
|
447
|
+
await lease.assertHeld();
|
|
448
|
+
await assertJournalCurrent(root, snapshot);
|
|
449
|
+
const native = await safePath(root, reviewedUpdateTransactionPathParts);
|
|
450
|
+
const handle = await open(native, constants.O_WRONLY | constants.O_APPEND | (constants.O_NOFOLLOW ?? 0));
|
|
451
|
+
try {
|
|
452
|
+
await handle.writeFile(canonicalJson(frame));
|
|
453
|
+
await handle.sync();
|
|
454
|
+
}
|
|
455
|
+
finally {
|
|
456
|
+
await handle.close();
|
|
457
|
+
}
|
|
458
|
+
return readSnapshot(root, reviewedUpdateTransactionPathParts, MAX_JOURNAL_BYTES);
|
|
459
|
+
}
|
|
460
|
+
async function durableMutation(root, header, index, restore, lease, onStaged) {
|
|
461
|
+
const mutation = header.mutations[index];
|
|
462
|
+
const before = restore ? mutation.target : mutation.original;
|
|
463
|
+
const after = restore ? mutation.original : mutation.target;
|
|
464
|
+
await lease.assertHeld();
|
|
465
|
+
await assertSnapshot(root, mutation.pathParts, before);
|
|
466
|
+
if (after.kind === 'missing') {
|
|
467
|
+
if (before.kind === 'missing')
|
|
468
|
+
return;
|
|
469
|
+
await unlink(await safePath(root, mutation.pathParts));
|
|
470
|
+
await syncDirectory(path.dirname(path.join(root, ...mutation.pathParts)));
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
await ensureParents(root, mutation.pathParts, header.missingDirectories);
|
|
474
|
+
const temporary = await safePath(root, temporaryParts(header, index, restore));
|
|
475
|
+
const handle = await open(temporary, 'wx', 0o600);
|
|
476
|
+
try {
|
|
477
|
+
await handle.chmod(0o600);
|
|
478
|
+
await handle.writeFile(Buffer.from(after.bytes, 'base64'));
|
|
479
|
+
await handle.chmod(after.mode);
|
|
480
|
+
await handle.sync();
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
await handle.close();
|
|
484
|
+
}
|
|
485
|
+
await onStaged?.();
|
|
486
|
+
await lease.assertHeld();
|
|
487
|
+
await assertSnapshot(root, mutation.pathParts, before);
|
|
488
|
+
await assertSnapshot(root, temporaryParts(header, index, restore), after);
|
|
489
|
+
await rename(temporary, await safePath(root, mutation.pathParts));
|
|
490
|
+
await syncDirectory(path.dirname(temporary));
|
|
491
|
+
}
|
|
492
|
+
await assertSnapshot(root, mutation.pathParts, after);
|
|
493
|
+
}
|
|
494
|
+
async function cleanupTemporary(root, header, index, restore, lease) {
|
|
495
|
+
const parts = temporaryParts(header, index, restore);
|
|
496
|
+
const snapshot = await readSnapshot(root, parts);
|
|
497
|
+
if (snapshot.content === undefined)
|
|
498
|
+
return;
|
|
499
|
+
const intended = restore ? header.mutations[index].original : header.mutations[index].target;
|
|
500
|
+
if (intended.kind !== 'file' || !Buffer.from(intended.bytes, 'base64').subarray(0, snapshot.content.length).equals(snapshot.content) ||
|
|
501
|
+
snapshot.mode !== privateFileMode && snapshot.mode !== intended.mode) {
|
|
502
|
+
fail(`temporary changed; it was preserved: ${key(parts)}.`);
|
|
503
|
+
}
|
|
504
|
+
await lease.assertHeld();
|
|
505
|
+
const current = await readSnapshot(root, parts);
|
|
506
|
+
if (!current.content?.equals(snapshot.content) || current.mode !== snapshot.mode)
|
|
507
|
+
fail(`temporary changed: ${key(parts)}.`);
|
|
508
|
+
await unlink(await safePath(root, parts));
|
|
509
|
+
await syncDirectory(path.dirname(path.join(root, ...parts)));
|
|
510
|
+
}
|
|
511
|
+
async function cleanupJournal(root, loaded, store, lease) {
|
|
512
|
+
const failures = [];
|
|
513
|
+
const removeApproval = async (digest) => {
|
|
514
|
+
try {
|
|
515
|
+
await store.remove(loaded.header.planFingerprint, digest);
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
failures.push(`user-local approval ${digest}: ${errorMessage(error)}`);
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
if (!loaded.committed) {
|
|
524
|
+
await lease.assertHeld();
|
|
525
|
+
await assertJournalCurrent(root, loaded.snapshot);
|
|
526
|
+
// A failed revocation must leave the journal blocking new work after rollback.
|
|
527
|
+
if (!await removeApproval(loaded.header.transactionDigest))
|
|
528
|
+
return failures;
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
await lease.assertHeld();
|
|
532
|
+
await assertJournalCurrent(root, loaded.snapshot);
|
|
533
|
+
await unlink(await safePath(root, reviewedUpdateTransactionPathParts));
|
|
534
|
+
await syncDirectory(path.join(root, '.liftoff'));
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
return [`${key(reviewedUpdateTransactionPathParts)}: ${errorMessage(error)}`];
|
|
538
|
+
}
|
|
539
|
+
// Never discard the commit seal while an approval capable of authorizing rollback remains.
|
|
540
|
+
if (loaded.committed && !await removeApproval(loaded.header.transactionDigest))
|
|
541
|
+
return failures;
|
|
542
|
+
const digests = [
|
|
543
|
+
...loaded.header.mutations.map((_entry, index) => frameDigest(loaded.header, { phase: 'mutation', index })),
|
|
544
|
+
frameDigest(loaded.header, { phase: 'committed' }),
|
|
545
|
+
rollbackCleanupDigest(loaded.header)
|
|
546
|
+
];
|
|
547
|
+
for (const digest of digests)
|
|
548
|
+
await removeApproval(digest);
|
|
549
|
+
if (!loaded.committed && loaded.header.missingDirectories.some((parts) => key(parts) === '.liftoff')) {
|
|
550
|
+
try {
|
|
551
|
+
await lease.assertHeld();
|
|
552
|
+
await rmdir(await safePath(root, ['.liftoff']));
|
|
553
|
+
await syncDirectory(root);
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
if (errorCode(error) !== 'ENOENT')
|
|
557
|
+
failures.push(`.liftoff: ${errorMessage(error)}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return failures;
|
|
561
|
+
}
|
|
562
|
+
async function destinations(root, loaded) {
|
|
563
|
+
const result = [];
|
|
564
|
+
for (const [index, mutation] of loaded.header.mutations.entries()) {
|
|
565
|
+
const current = await readSnapshot(root, mutation.pathParts);
|
|
566
|
+
result.push({
|
|
567
|
+
pathParts: [...mutation.pathParts], attempted: index <= loaded.pendingIndex,
|
|
568
|
+
disposition: matches(current, mutation.original) ? 'original'
|
|
569
|
+
: index <= loaded.pendingIndex && matches(current, mutation.target) ? 'target' : 'changed'
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return result;
|
|
573
|
+
}
|
|
574
|
+
function outcome(status, loaded) {
|
|
575
|
+
return {
|
|
576
|
+
status, committed: loaded?.committed ?? false,
|
|
577
|
+
...(loaded ? { planFingerprint: loaded.header.planFingerprint, transactionDigest: loaded.header.transactionDigest } : {}),
|
|
578
|
+
rollbackFailures: [], cleanupFailures: []
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
async function recoverLocked(root, loaded, store, lease) {
|
|
582
|
+
if (loaded.committed) {
|
|
583
|
+
return { ...outcome('committed', loaded), cleanupFailures: await cleanupJournal(root, loaded, store, lease) };
|
|
584
|
+
}
|
|
585
|
+
if (loaded.rollbackComplete) {
|
|
586
|
+
for (const mutation of loaded.header.mutations)
|
|
587
|
+
await assertSnapshot(root, mutation.pathParts, mutation.original);
|
|
588
|
+
const cleanupFailures = await cleanupJournal(root, loaded, store, lease);
|
|
589
|
+
return { ...outcome(cleanupFailures.length ? 'blocked' : 'rolled-back', loaded), cleanupFailures };
|
|
590
|
+
}
|
|
591
|
+
const result = outcome('rolled-back', loaded);
|
|
592
|
+
const entries = await destinations(root, loaded);
|
|
593
|
+
for (const [index, mutation] of [...loaded.header.mutations.entries()].reverse()) {
|
|
594
|
+
try {
|
|
595
|
+
await lease.assertHeld();
|
|
596
|
+
await assertJournalCurrent(root, loaded.snapshot);
|
|
597
|
+
const current = entries[index];
|
|
598
|
+
if (current.disposition === 'changed')
|
|
599
|
+
fail(`target changed before rollback; it was preserved: ${key(mutation.pathParts)}.`);
|
|
600
|
+
if (current.attempted) {
|
|
601
|
+
await cleanupTemporary(root, loaded.header, index, false, lease);
|
|
602
|
+
await cleanupTemporary(root, loaded.header, index, true, lease);
|
|
603
|
+
}
|
|
604
|
+
if (current.disposition === 'target')
|
|
605
|
+
await durableMutation(root, loaded.header, index, true, lease);
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
result.rollbackFailures.push(`${key(mutation.pathParts)}: ${errorMessage(error)}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
for (const parts of [...loaded.header.missingDirectories].sort((left, right) => right.length - left.length)) {
|
|
612
|
+
if (key(parts) === '.liftoff')
|
|
613
|
+
continue;
|
|
614
|
+
if (!loaded.header.mutations.some((mutation, index) => index <= loaded.pendingIndex &&
|
|
615
|
+
mutation.type === 'write' && key(mutation.pathParts).startsWith(`${key(parts)}/`)))
|
|
616
|
+
continue;
|
|
617
|
+
try {
|
|
618
|
+
await lease.assertHeld();
|
|
619
|
+
await assertJournalCurrent(root, loaded.snapshot);
|
|
620
|
+
await rmdir(await safePath(root, parts));
|
|
621
|
+
await syncDirectory(path.dirname(path.join(root, ...parts)));
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
if (errorCode(error) !== 'ENOENT')
|
|
625
|
+
result.rollbackFailures.push(`${key(parts)}: ${errorMessage(error)}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (result.rollbackFailures.length)
|
|
629
|
+
result.status = 'blocked';
|
|
630
|
+
else
|
|
631
|
+
result.cleanupFailures = await cleanupJournal(root, loaded, store, lease);
|
|
632
|
+
if (result.cleanupFailures.length)
|
|
633
|
+
result.status = 'blocked';
|
|
634
|
+
return result;
|
|
635
|
+
}
|
|
636
|
+
async function withReviewedMutationLock(root, operation) {
|
|
637
|
+
let result;
|
|
638
|
+
try {
|
|
639
|
+
return await withProjectMutationLock(root, async (lease) => {
|
|
640
|
+
result = await operation(lease);
|
|
641
|
+
return result;
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
if (result?.committed) {
|
|
646
|
+
return { ...result, cleanupFailures: [...result.cleanupFailures, `Project mutation lock cleanup: ${errorMessage(error)}`] };
|
|
647
|
+
}
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
export async function inspectReviewedUpdateTransaction(projectRoot, options = {}) {
|
|
652
|
+
const journalPath = path.join(path.resolve(projectRoot), ...reviewedUpdateTransactionPathParts);
|
|
653
|
+
try {
|
|
654
|
+
const root = await canonicalRoot(projectRoot);
|
|
655
|
+
const loaded = await loadJournal(root, options.approvalStore);
|
|
656
|
+
if (!loaded)
|
|
657
|
+
return { status: 'absent', committed: false, journalPath, destinations: [] };
|
|
658
|
+
return {
|
|
659
|
+
status: loaded.committed ? 'committed' : 'interrupted', committed: loaded.committed, journalPath,
|
|
660
|
+
planFingerprint: loaded.header.planFingerprint, transactionDigest: loaded.header.transactionDigest,
|
|
661
|
+
destinations: loaded.committed ? [] : await destinations(root, loaded)
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
return { status: 'blocked', committed: false, journalPath, reason: errorMessage(error), destinations: [] };
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
export async function recoverReviewedUpdateTransaction(projectRoot, options = {}) {
|
|
669
|
+
return withReviewedMutationLock(projectRoot, async (lease) => {
|
|
670
|
+
try {
|
|
671
|
+
const root = await canonicalRoot(projectRoot);
|
|
672
|
+
const loaded = await loadJournal(root, options.approvalStore);
|
|
673
|
+
if (!loaded)
|
|
674
|
+
return outcome('absent');
|
|
675
|
+
return await recoverLocked(root, loaded, options.approvalStore, lease);
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
return { ...outcome('blocked'), rollbackFailures: [errorMessage(error)] };
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
export async function applyReviewedUpdateTransaction(projectRoot, mutations, options) {
|
|
683
|
+
assertDigest(options.planFingerprint);
|
|
684
|
+
const planFingerprint = options.planFingerprint;
|
|
685
|
+
if (!options.approvalStore)
|
|
686
|
+
fail('a user-local transaction approval store is required.');
|
|
687
|
+
if (!Array.isArray(mutations) || mutations.length > MAX_MUTATIONS)
|
|
688
|
+
fail('invalid or oversized mutation inventory.');
|
|
689
|
+
const selected = mutations.map((mutation) => {
|
|
690
|
+
exactKeys(mutation, mutation.type === 'write'
|
|
691
|
+
? ['type', 'pathParts', 'content', ...(Object.hasOwn(mutation, 'mode') ? ['mode'] : [])]
|
|
692
|
+
: ['type', 'pathParts']);
|
|
693
|
+
const pathParts = validParts(mutation.pathParts);
|
|
694
|
+
if (mutation.type === 'delete')
|
|
695
|
+
return { type: 'delete', pathParts };
|
|
696
|
+
if (mutation.type !== 'write' || typeof mutation.content !== 'string' && !Buffer.isBuffer(mutation.content)) {
|
|
697
|
+
fail('invalid mutation type or bytes.');
|
|
698
|
+
}
|
|
699
|
+
if (mutation.mode !== undefined)
|
|
700
|
+
assertMode(mutation.mode);
|
|
701
|
+
return { type: 'write', pathParts, content: typeof mutation.content === 'string'
|
|
702
|
+
? Buffer.from(mutation.content, 'utf8') : Buffer.from(mutation.content),
|
|
703
|
+
...(mutation.mode === undefined ? {} : { mode: mutation.mode }) };
|
|
704
|
+
});
|
|
705
|
+
const conditions = new Map();
|
|
706
|
+
if ((options.preconditions?.length ?? 0) > MAX_MUTATIONS * 4)
|
|
707
|
+
fail('too many preconditions.');
|
|
708
|
+
for (const snapshot of options.preconditions ?? []) {
|
|
709
|
+
const parts = validParts(snapshot.pathParts);
|
|
710
|
+
const identity = folded(key(parts));
|
|
711
|
+
if (conditions.has(identity))
|
|
712
|
+
fail(`duplicate or case-colliding preconditions: ${key(parts)}.`);
|
|
713
|
+
conditions.set(identity, { pathParts: parts, stored: storeSnapshot(snapshot) });
|
|
714
|
+
}
|
|
715
|
+
return withReviewedMutationLock(projectRoot, async (lease) => {
|
|
716
|
+
const root = await canonicalRoot(projectRoot);
|
|
717
|
+
if ((await readSnapshot(root, reviewedUpdateTransactionPathParts, MAX_JOURNAL_BYTES)).content !== undefined) {
|
|
718
|
+
fail('an existing recovery journal blocks new work; recover it, then run liftoff update --check.');
|
|
719
|
+
}
|
|
720
|
+
await options.validatePlan?.();
|
|
721
|
+
await lease.assertHeld();
|
|
722
|
+
const stored = [];
|
|
723
|
+
const missing = new Map();
|
|
724
|
+
for (const mutation of selected) {
|
|
725
|
+
const expected = conditions.get(folded(key(mutation.pathParts)));
|
|
726
|
+
if (expected && key(expected.pathParts) !== key(mutation.pathParts))
|
|
727
|
+
fail('case-colliding source and destination.');
|
|
728
|
+
const original = expected?.stored ?? storeSnapshot(await readSnapshot(root, mutation.pathParts));
|
|
729
|
+
const target = mutation.type === 'delete' ? { kind: 'missing' }
|
|
730
|
+
: { kind: 'file', bytes: mutation.content.toString('base64'),
|
|
731
|
+
sha256: hash(mutation.content),
|
|
732
|
+
mode: targetMode(mutation.mode, original) };
|
|
733
|
+
if (mutation.type === 'write' && mutation.content.length > MAX_FILE_BYTES)
|
|
734
|
+
fail(`oversized target: ${key(mutation.pathParts)}.`);
|
|
735
|
+
stored.push({
|
|
736
|
+
type: mutation.type, pathParts: mutation.pathParts, original, target,
|
|
737
|
+
...(mutation.type === 'write' && mutation.mode !== undefined ? { mode: mutation.mode } : {})
|
|
738
|
+
});
|
|
739
|
+
conditions.set(folded(key(mutation.pathParts)), { pathParts: mutation.pathParts, stored: original });
|
|
740
|
+
}
|
|
741
|
+
validateInventory(stored);
|
|
742
|
+
validatePaths([...conditions.values()].map((entry) => entry.pathParts));
|
|
743
|
+
const assertConditions = async () => {
|
|
744
|
+
for (const condition of conditions.values())
|
|
745
|
+
await assertSnapshot(root, condition.pathParts, condition.stored);
|
|
746
|
+
};
|
|
747
|
+
await assertConditions();
|
|
748
|
+
if (!stored.length)
|
|
749
|
+
return outcome('absent');
|
|
750
|
+
for (const parts of [...stored.filter((entry) => entry.type === 'write').map((entry) => entry.pathParts),
|
|
751
|
+
[...reviewedUpdateTransactionPathParts]]) {
|
|
752
|
+
for (let count = 1; count < parts.length; count += 1) {
|
|
753
|
+
const parent = parts.slice(0, count);
|
|
754
|
+
try {
|
|
755
|
+
if (!(await lstat(await safePath(root, parent))).isDirectory())
|
|
756
|
+
fail(`not a directory: ${key(parent)}.`);
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
if (errorCode(error) !== 'ENOENT')
|
|
760
|
+
throw error;
|
|
761
|
+
missing.set(key(parent), parent);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const body = {
|
|
766
|
+
schemaVersion: 1, projectRoot: root, planFingerprint, nonce: randomUUID(),
|
|
767
|
+
mutations: stored, missingDirectories: [...missing.values()]
|
|
768
|
+
};
|
|
769
|
+
const header = { ...body, transactionDigest: canonicalSha256(body) };
|
|
770
|
+
for (const [index] of stored.entries()) {
|
|
771
|
+
for (const restore of [false, true]) {
|
|
772
|
+
if ((await readSnapshot(root, temporaryParts(header, index, restore))).content !== undefined) {
|
|
773
|
+
fail(`reserved temporary already exists: ${key(temporaryParts(header, index, restore))}.`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
let loaded;
|
|
778
|
+
let committed = false;
|
|
779
|
+
let operation = 'persist user-local transaction approval';
|
|
780
|
+
try {
|
|
781
|
+
await options.approvalStore.write(header.planFingerprint, header.transactionDigest);
|
|
782
|
+
await options.approvalStore.write(header.planFingerprint, rollbackCleanupDigest(header));
|
|
783
|
+
if (await options.approvalStore.verify(header.planFingerprint, header.transactionDigest) !== true ||
|
|
784
|
+
await options.approvalStore.verify(header.planFingerprint, rollbackCleanupDigest(header)) !== true) {
|
|
785
|
+
fail('the user-local approval store did not persist its transaction seal.');
|
|
786
|
+
}
|
|
787
|
+
await lease.assertHeld();
|
|
788
|
+
await assertConditions();
|
|
789
|
+
operation = `create ${key(reviewedUpdateTransactionPathParts)}`;
|
|
790
|
+
loaded = { header, snapshot: await createJournal(root, header, lease), pendingIndex: -1, committed: false };
|
|
791
|
+
await options.onCheckpoint?.({ phase: 'prepared' });
|
|
792
|
+
for (const [index, mutation] of selected.entries()) {
|
|
793
|
+
operation = `${mutation.type} ${key(mutation.pathParts)}`;
|
|
794
|
+
const callbackMutation = mutation.type === 'write'
|
|
795
|
+
? { ...mutation, pathParts: [...mutation.pathParts], content: Buffer.from(mutation.content) }
|
|
796
|
+
: { ...mutation, pathParts: [...mutation.pathParts] };
|
|
797
|
+
await options.onBeforeMutation?.(callbackMutation, index);
|
|
798
|
+
await lease.assertHeld();
|
|
799
|
+
await assertConditions();
|
|
800
|
+
const frame = { phase: 'mutation', index };
|
|
801
|
+
await options.approvalStore.write(header.planFingerprint, frameDigest(header, frame));
|
|
802
|
+
loaded.snapshot = await appendFrame(root, loaded.snapshot, frame, lease);
|
|
803
|
+
loaded.pendingIndex = index;
|
|
804
|
+
await options.onCheckpoint?.({ phase: 'before-mutation', index });
|
|
805
|
+
await assertConditions();
|
|
806
|
+
await durableMutation(root, header, index, false, lease, () => options.onCheckpoint?.({ phase: 'staged', index }) ?? Promise.resolve());
|
|
807
|
+
conditions.set(folded(key(mutation.pathParts)), { pathParts: mutation.pathParts, stored: stored[index].target });
|
|
808
|
+
await options.onCheckpoint?.({ phase: 'after-mutation', index });
|
|
809
|
+
}
|
|
810
|
+
operation = 'commit reviewed update';
|
|
811
|
+
await options.onCheckpoint?.({ phase: 'before-commit' });
|
|
812
|
+
await lease.assertHeld();
|
|
813
|
+
await assertConditions();
|
|
814
|
+
await assertJournalCurrent(root, loaded.snapshot);
|
|
815
|
+
await options.approvalStore.write(header.planFingerprint, frameDigest(header, { phase: 'committed' }));
|
|
816
|
+
committed = true;
|
|
817
|
+
loaded.committed = true;
|
|
818
|
+
loaded.snapshot = await appendFrame(root, loaded.snapshot, { phase: 'committed' }, lease);
|
|
819
|
+
await options.onCheckpoint?.({ phase: 'committed' });
|
|
820
|
+
return { ...outcome('committed', loaded), cleanupFailures: await cleanupJournal(root, loaded, options.approvalStore, lease) };
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
if (committed) {
|
|
824
|
+
return {
|
|
825
|
+
...outcome('committed', loaded),
|
|
826
|
+
committed: true, cleanupFailures: [`Committed transaction finalization: ${errorMessage(error)}`]
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
let recovered;
|
|
830
|
+
try {
|
|
831
|
+
const current = await loadJournal(root, options.approvalStore);
|
|
832
|
+
if (current) {
|
|
833
|
+
recovered = await recoverLocked(root, current, options.approvalStore, lease);
|
|
834
|
+
if (recovered.committed) {
|
|
835
|
+
recovered.cleanupFailures.unshift(`Committed transaction finalization: ${errorMessage(error)}`);
|
|
836
|
+
return recovered;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
await options.approvalStore.remove(header.planFingerprint, header.transactionDigest);
|
|
841
|
+
await options.approvalStore.remove(header.planFingerprint, rollbackCleanupDigest(header));
|
|
842
|
+
if (header.missingDirectories.some((parts) => key(parts) === '.liftoff')) {
|
|
843
|
+
try {
|
|
844
|
+
await lease.assertHeld();
|
|
845
|
+
await rmdir(await safePath(root, ['.liftoff']));
|
|
846
|
+
await syncDirectory(root);
|
|
847
|
+
}
|
|
848
|
+
catch (cleanupError) {
|
|
849
|
+
if (errorCode(cleanupError) !== 'ENOENT')
|
|
850
|
+
throw cleanupError;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
catch (recoveryError) {
|
|
856
|
+
throw new ReviewedUpdateTransactionError(`Project update failed to ${operation}: ${errorMessage(error)} Recovery blocked: ${errorMessage(recoveryError)}`, [errorMessage(recoveryError)]);
|
|
857
|
+
}
|
|
858
|
+
const failures = [...recovered?.rollbackFailures ?? [], ...recovered?.cleanupFailures ?? []];
|
|
859
|
+
throw new ReviewedUpdateTransactionError(`Project update failed to ${operation}: ${errorMessage(error)} ${failures.length
|
|
860
|
+
? `Recovery incomplete: ${failures.join('; ')}` : 'All attributable changes were rolled back.'}`, failures);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
//# sourceMappingURL=reviewed-update-transaction.js.map
|