@mnstry/atelier 0.2.0-alpha.5 → 0.2.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +191 -0
- package/README.md +86 -25
- package/SECURITY.md +6 -1
- package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
- package/contracts/atelier-coauthor.v1.schema.json +163 -0
- package/contracts/atelier-decision-request.v1.schema.json +476 -0
- package/contracts/atelier-decision-result.v1.schema.json +500 -0
- package/contracts/atelier-guide.v1.schema.json +227 -0
- package/contracts/atelier-intake.v1.schema.json +151 -0
- package/contracts/atelier-migration.v2.schema.json +61 -0
- package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
- package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
- package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
- package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
- package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
- package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
- package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
- package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
- package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
- package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
- package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
- package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
- package/contracts/atelier-presentation.v1.schema.json +1293 -0
- package/contracts/atelier-repository-observation.v1.schema.json +163 -0
- package/contracts/atelier-review.v1.schema.json +1601 -0
- package/contracts/atelier-skill-steward.v1.schema.json +692 -0
- package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
- package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
- package/contracts/atelier-vault-publication.v1.schema.json +58 -0
- package/docs/assurance-controls.md +12 -2
- package/docs/atelier-runtime.md +13 -2
- package/docs/atelier-sync.md +172 -0
- package/docs/blocks/claims.md +28 -18
- package/docs/blocks/will-not-do.md +12 -3
- package/docs/coauthor-session.md +57 -0
- package/docs/connected-composition.md +75 -0
- package/docs/decisions.md +167 -0
- package/docs/exact-upgrades.md +159 -0
- package/docs/extension-pack-lifecycle.md +33 -0
- package/docs/guided-upgrades.md +99 -0
- package/docs/install.md +65 -9
- package/docs/intake-and-guides.md +65 -0
- package/docs/integration-and-review-plan.md +409 -0
- package/docs/integration-contract-decisions.md +22 -0
- package/docs/local-review.md +84 -0
- package/docs/local-services.md +227 -0
- package/docs/local-state.md +107 -0
- package/docs/obsidian-contract.md +687 -0
- package/docs/obsidian.md +387 -0
- package/docs/portable-presentation.md +328 -0
- package/docs/presentation-consumer-boundaries.md +75 -0
- package/docs/presentation-review-disposition.md +47 -0
- package/docs/project-command-inventory.md +38 -0
- package/docs/project-options.md +53 -0
- package/docs/release-engineering.md +99 -2
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +70 -2
- package/docs/vault-service.md +292 -0
- package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
- package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
- package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
- package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
- package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
- package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
- package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
- package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
- package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
- package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
- package/fixtures/atelier-intake/valid/source.v1.json +7 -0
- package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
- package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
- package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
- package/fixtures/atelier-review/valid/bound.v1.json +229 -0
- package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
- package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
- package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
- package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
- package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
- package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
- package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
- package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
- package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
- package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
- package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
- package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
- package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
- package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
- package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
- package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
- package/fixtures/decisions/request/valid/triage.v1.json +60 -0
- package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
- package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
- package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
- package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
- package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
- package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
- package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
- package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
- package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
- package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
- package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
- package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
- package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
- package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
- package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
- package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
- package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
- package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
- package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
- package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
- package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
- package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
- package/fixtures/obsidian/edits/workspace.json +65 -0
- package/fixtures/obsidian/materialization/expected-full.json +99 -0
- package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
- package/fixtures/obsidian/materialization/workspace.json +54 -0
- package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
- package/fixtures/ui/presentation/pane.v1.json +7 -0
- package/fixtures/ui/presentation/reference.v1.json +293 -0
- package/package.json +61 -6
- package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/claude/atelier-open/SKILL.md +5 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
- package/skills/claude/mnstry-readiness/SKILL.md +5 -0
- package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/codex/atelier-open/SKILL.md +5 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
- package/skills/codex/mnstry-readiness/SKILL.md +5 -0
- package/src/access/preview-enforcer.mjs +84 -0
- package/src/boundary/content-rules.mjs +9 -4
- package/src/boundary/policy.mjs +74 -42
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +61 -3
- package/src/coauthor/session.mjs +161 -0
- package/src/coauthor/store.mjs +173 -0
- package/src/collaboration/event-ledger.mjs +16 -33
- package/src/collaboration/inspection-bundle.mjs +223 -0
- package/src/collaboration/review-contracts.mjs +25 -0
- package/src/collaboration/review-store.mjs +305 -0
- package/src/commands/coauthor.mjs +26 -0
- package/src/commands/config.mjs +6 -1
- package/src/commands/extension-pack.mjs +15 -8
- package/src/commands/init.mjs +44 -5
- package/src/commands/obsidian.mjs +368 -0
- package/src/commands/review.mjs +147 -0
- package/src/commands/setup.mjs +51 -14
- package/src/commands/skills.mjs +199 -0
- package/src/commands/sync.mjs +107 -0
- package/src/composition/registry.mjs +27 -0
- package/src/composition/wire.mjs +122 -0
- package/src/contracts/corpus.mjs +35 -0
- package/src/decisions/contracts.d.mts +96 -0
- package/src/decisions/contracts.mjs +321 -0
- package/src/disclosure/content-scan.mjs +13 -0
- package/src/export/atelier-export-contract.mjs +16 -1
- package/src/extension-packs/lifecycle.mjs +154 -0
- package/src/graph/graph.mjs +64 -2
- package/src/graph/knowledge-graph.mjs +548 -24
- package/src/guides/contracts.mjs +54 -0
- package/src/index.mjs +29 -0
- package/src/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +50 -49
- package/src/project/durable-state.mjs +161 -0
- package/src/project/file-class.mjs +166 -0
- package/src/project/private-state.mjs +11 -1
- package/src/projection/obsidian/contracts.mjs +571 -0
- package/src/projection/obsidian/edits/align.mjs +169 -0
- package/src/projection/obsidian/edits/apply.mjs +889 -0
- package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
- package/src/projection/obsidian/edits/contribution.mjs +108 -0
- package/src/projection/obsidian/edits/index.mjs +19 -0
- package/src/projection/obsidian/edits/object-identity.mjs +61 -0
- package/src/projection/obsidian/edits/object-store.mjs +517 -0
- package/src/projection/obsidian/edits/observe.mjs +123 -0
- package/src/projection/obsidian/edits/policy.mjs +123 -0
- package/src/projection/obsidian/edits/regions.mjs +648 -0
- package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
- package/src/projection/obsidian/materialize/index.mjs +7 -0
- package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
- package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
- package/src/projection/obsidian/materialize/settings.mjs +57 -0
- package/src/projection/obsidian/materialize/stage.mjs +27 -0
- package/src/projection/obsidian/proposals/adapter.mjs +572 -0
- package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
- package/src/projection/obsidian/proposals/contribution.mjs +55 -0
- package/src/projection/obsidian/proposals/index.mjs +23 -0
- package/src/projection/obsidian/proposals/observation.mjs +216 -0
- package/src/projection/obsidian/proposals/queue.mjs +277 -0
- package/src/projection/obsidian/proposals/router.mjs +168 -0
- package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
- package/src/projection/obsidian/publication/exchange.mjs +117 -0
- package/src/projection/obsidian/publication/index.mjs +5 -0
- package/src/projection/obsidian/publication/publisher.mjs +588 -0
- package/src/projection/obsidian/publication/test-seam.mjs +16 -0
- package/src/projection/obsidian/publication/transport.mjs +139 -0
- package/src/projection/obsidian/recovery/index.mjs +6 -0
- package/src/projection/obsidian/recovery/journal.mjs +134 -0
- package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
- package/src/projection/obsidian/recovery/restart.mjs +316 -0
- package/src/projection/obsidian/recovery/store.mjs +234 -0
- package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
- package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
- package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
- package/src/projection/obsidian/selection-ui/index.mjs +12 -0
- package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
- package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
- package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
- package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
- package/src/projection/project.mjs +28 -19
- package/src/readiness-protocols/evidence.mjs +282 -0
- package/src/readiness-protocols/runtime.mjs +4 -3
- package/src/readiness-protocols/source-read.mjs +39 -0
- package/src/runtime/git-adapter.mjs +189 -0
- package/src/runtime/local-state.mjs +439 -0
- package/src/runtime/obsidian/app-capability.mjs +110 -0
- package/src/runtime/obsidian/app-production-seams.mjs +96 -0
- package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
- package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
- package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
- package/src/runtime/obsidian/contributions.mjs +27 -0
- package/src/runtime/obsidian/documents.mjs +37 -0
- package/src/runtime/obsidian/enablement.mjs +53 -0
- package/src/runtime/obsidian/engine.mjs +532 -0
- package/src/runtime/obsidian/errors.mjs +14 -0
- package/src/runtime/obsidian/extension-points.mjs +103 -0
- package/src/runtime/obsidian/index.mjs +34 -0
- package/src/runtime/obsidian/lifecycle.mjs +258 -0
- package/src/runtime/obsidian/machine-settings.mjs +260 -0
- package/src/runtime/obsidian/observation.mjs +158 -0
- package/src/runtime/obsidian/opening.mjs +243 -0
- package/src/runtime/obsidian/pending-edits.mjs +141 -0
- package/src/runtime/obsidian/pipeline.mjs +122 -0
- package/src/runtime/obsidian/private-lock.mjs +156 -0
- package/src/runtime/obsidian/process-identity.mjs +58 -0
- package/src/runtime/obsidian/service-client.mjs +66 -0
- package/src/runtime/obsidian/service-main.mjs +89 -0
- package/src/runtime/obsidian/service-record.mjs +169 -0
- package/src/runtime/obsidian/service-server.mjs +121 -0
- package/src/runtime/obsidian/service.mjs +216 -0
- package/src/runtime/obsidian/startup-adapters.mjs +87 -0
- package/src/runtime/obsidian/state-store.mjs +180 -0
- package/src/runtime/obsidian/sync-notice.mjs +33 -0
- package/src/runtime/obsidian/tick-loop.mjs +84 -0
- package/src/runtime/obsidian/watchers.mjs +36 -0
- package/src/runtime/repository-observation.mjs +491 -0
- package/src/runtime/supervisor.mjs +803 -0
- package/src/server/local-sidecar.mjs +44 -0
- package/src/server/server.mjs +1 -0
- package/src/skills/steward.mjs +780 -0
- package/src/ui/presentation/browser.mjs +278 -0
- package/src/ui/presentation/contract.mjs +116 -0
- package/src/ui/presentation/index.mjs +7 -0
- package/src/ui/presentation/native.mjs +153 -0
- package/src/ui/presentation/proof.mjs +57 -0
- package/src/ui/presentation/schema-check.mjs +30 -0
- package/src/ui/presentation/schema.generated.mjs +1371 -0
- package/src/ui/presentation/state.mjs +43 -0
- package/src/ui/presentation/styles.mjs +67 -0
- package/src/ui/presentation/tokens.mjs +74 -0
- package/src/ui/presentation/web.mjs +85 -0
- package/src/ui/review-page.mjs +32 -0
- package/src/upgrade/explanation.mjs +33 -0
- package/src/upgrade/provenance.mjs +338 -0
- package/src/upgrade/transaction-files.mjs +103 -0
- package/src/upgrade/transaction.mjs +475 -0
- package/src/upgrade/upgrade.mjs +63 -28
- package/src/vault/hosts.mjs +21 -0
- package/src/vault/identity.mjs +19 -0
- package/src/vault/index.mjs +7 -0
- package/src/vault/interface.mjs +11 -0
- package/src/vault/metadata.mjs +31 -0
- package/src/vault/privacy.mjs +148 -0
- package/src/vault/probe.mjs +89 -0
- package/src/vault/service.mjs +131 -0
- package/src/vault/source.mjs +45 -0
- package/src/vault/storage.mjs +23 -0
- package/templates/distribution-workspace/.claude/launch.json +6 -2
- package/templates/distribution-workspace/gitignore +1 -0
- package/templates/distribution-workspace/project/README.md +2 -2
- package/templates/external-project-workspace/README.md +45 -0
- package/templates/external-project-workspace/answers.example.json +4 -0
- package/templates/external-project-workspace/atelier.project.json +40 -0
- package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
- package/templates/external-project-workspace/gitignore +8 -0
- package/templates/external-project-workspace/pack-lifecycle.json +14 -0
- package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
- package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
- package/templates/external-project-workspace/repo-access.v1.json +9 -0
- package/templates/external-project-workspace/source/README.md +11 -0
- package/templates/launch.json +6 -2
- package/templates/private-domain-workspace/.claude/launch.json +6 -2
- package/templates/private-domain-workspace/domain/README.md +2 -2
- package/templates/private-domain-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/.claude/launch.json +6 -2
- package/templates/shared-project-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/project/README.md +2 -2
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { resolveGitExecutable, runGit, parseNullConfig } from '../runtime/git-adapter.mjs'
|
|
6
|
+
import { resolveProjectConfig } from '../project/config.mjs'
|
|
7
|
+
import { validateJsonSchema } from '../export/atelier-export-contract.mjs'
|
|
8
|
+
import { buildAtelierLock } from './upgrade.mjs'
|
|
9
|
+
import { checkBoundaryPolicy, loadBoundaryPolicy } from '../boundary/policy.mjs'
|
|
10
|
+
import { buildGraph } from '../graph/graph.mjs'
|
|
11
|
+
import { buildProjectProjection, buildProjectManifest } from '../projection/project.mjs'
|
|
12
|
+
import { buildReadiness } from '../readiness/readiness.mjs'
|
|
13
|
+
import { hashBytes, hashObject, same, jsonText, contained, readBytes, fileState, inventory, syncDirectory, publish, privateDirectory, replaceExpected, MAX_BYTES } from './transaction-files.mjs'
|
|
14
|
+
|
|
15
|
+
const packageRoot = fileURLToPath(new URL('../../', import.meta.url))
|
|
16
|
+
const PRIVATE = '.atelier-local/upgrades'
|
|
17
|
+
const POLICY = 'atelier.adoption-policy.json'
|
|
18
|
+
const SCHEMAS = { policy: 'atelier-adoption-policy.v1', plan: 'atelier-upgrade-plan.v2', migration: 'atelier-migration.v2', event: 'atelier-upgrade-receipt.v1', lock: 'atelier-lock.v1' }
|
|
19
|
+
const schemaDocs = Object.fromEntries(Object.entries(SCHEMAS).map(([key, name]) => [key, JSON.parse(fs.readFileSync(path.join(packageRoot, 'contracts', `${name}.schema.json`)))]))
|
|
20
|
+
export function validateUpgradeDocument(kind, doc) {
|
|
21
|
+
const errors = validateJsonSchema(schemaDocs[kind], doc)
|
|
22
|
+
if (errors.length) throw new Error(`invalid upgrade ${kind}: ${errors.join('; ')}`)
|
|
23
|
+
return doc
|
|
24
|
+
}
|
|
25
|
+
const readJson = (file) => JSON.parse(readBytes(file))
|
|
26
|
+
const git = (root, args, options = {}) => runGit(resolveGitExecutable(), root, ['-c', 'core.fsmonitor=false', ...(args[0] === 'check-ignore' ? [] : ['--literal-pathspecs']), ...args], options)
|
|
27
|
+
const text = (root, args) => git(root, args).stdout.trim()
|
|
28
|
+
const snapshot = (root) => inventory(root, { exclude: ['.git', '.atelier-local'] })
|
|
29
|
+
|
|
30
|
+
function privateRoot(root) {
|
|
31
|
+
// An ignored pathname alone is insufficient when its files were force-added.
|
|
32
|
+
if (text(root, ['ls-files', '--', '.atelier-local']).length || !git(root, ['check-ignore', '-q', '.atelier-local/upgrades/probe'], { allowFailure: true }).ok) throw new Error('private upgrade state must be ignored and untracked')
|
|
33
|
+
return privateDirectory(root, PRIVATE)
|
|
34
|
+
}
|
|
35
|
+
function policy(root) {
|
|
36
|
+
const result = validateUpgradeDocument('policy', readJson(contained(root, POLICY)))
|
|
37
|
+
if (!result.enabled) throw new Error('upgrade policy is disabled')
|
|
38
|
+
return result
|
|
39
|
+
}
|
|
40
|
+
function canonicalProject(project) {
|
|
41
|
+
const root = fs.realpathSync(project.configDir)
|
|
42
|
+
const remap = (value) => value ? path.resolve(root, path.relative(project.configDir, value)) : value
|
|
43
|
+
const copy = { ...project, configDir: root, repos: project.repos.map((repo) => ({ ...repo, path: remap(repo.path) })) }
|
|
44
|
+
for (const key of ['configPath', 'workspaceRoot', 'repoOpsRoot', 'repoAccessPath', 'boundaryPolicyPath', 'graphPath', 'readinessPath', 'outputRoot']) copy[key] = remap(project[key])
|
|
45
|
+
return copy
|
|
46
|
+
}
|
|
47
|
+
function checkProject(project) {
|
|
48
|
+
if (Object.keys(process.env).some((key) => /^GIT_(?!PAGER$|TERMINAL_PROMPT$|OPTIONAL_LOCKS$)/i.test(key))) throw new Error('Git environment overrides unsupported during exact preparation')
|
|
49
|
+
const root = fs.realpathSync(project.configDir)
|
|
50
|
+
if (text(root, ['rev-parse', '--show-toplevel']) !== root) throw new Error('configuration must be at repository root')
|
|
51
|
+
const gitDir = text(root, ['rev-parse', '--absolute-git-dir'])
|
|
52
|
+
const common = path.resolve(root, text(root, ['rev-parse', '--git-common-dir']))
|
|
53
|
+
if (gitDir === common) throw new Error('upgrade requires an isolated linked worktree')
|
|
54
|
+
if (!text(root, ['symbolic-ref', '--quiet', 'HEAD']).startsWith('refs/heads/')) throw new Error('upgrade requires a candidate branch')
|
|
55
|
+
if (project.repos.length !== 1 || project.repos[0].external || fs.realpathSync(project.repos[0].path) !== root || fs.realpathSync(project.workspaceRoot) !== root || fs.realpathSync(project.repoOpsRoot) !== root) throw new Error('slice 1 requires one repository with workspace and configuration at its root')
|
|
56
|
+
if (project.localOverlay.paths.length || project.config.runtime || project.config.alignment || project.governanceLedgerPath) throw new Error('local overlays and runtime/alignment participants are unsupported')
|
|
57
|
+
if (project.config.ext && Object.entries(project.config.ext).some(([key, value]) => key !== 'mnstry.atelier' || Object.keys(value).some((k) => k !== 'distribution'))) throw new Error('extension pack and custom extension participants are unsupported')
|
|
58
|
+
if (fs.existsSync(path.join(root, '.atelier-local', 'readiness'))) throw new Error('private readiness inputs require a later read-set participant')
|
|
59
|
+
for (const file of [project.configPath, project.repoAccessPath, project.boundaryPolicyPath, project.graphPath, project.readinessPath, path.join(project.outputRoot, 'index.html')]) contained(root, path.relative(root, file))
|
|
60
|
+
if (fs.existsSync(path.join(root, '.gitmodules'))) throw new Error('submodules unsupported')
|
|
61
|
+
return root
|
|
62
|
+
}
|
|
63
|
+
function gitAuxiliary(root) {
|
|
64
|
+
const result = git(root, ['config', '--path', '--get', 'core.excludesFile'], { allowFailure: true })
|
|
65
|
+
if (!result.ok && result.status !== 1) throw new Error('Git ignore configuration unavailable')
|
|
66
|
+
const globalIgnore = result.ok ? path.resolve(root, result.stdout.trim()) : path.join(process.env.XDG_CONFIG_HOME || path.join(process.env.HOME || '', '.config'), 'git', 'ignore')
|
|
67
|
+
const attributes = ['GIT_ATTR_GLOBAL', 'GIT_ATTR_SYSTEM'].map((variable) => {
|
|
68
|
+
const location = text(root, ['var', variable])
|
|
69
|
+
if (!path.isAbsolute(location)) throw new Error('Git attributes location unavailable')
|
|
70
|
+
return location
|
|
71
|
+
})
|
|
72
|
+
const files = [text(root, ['rev-parse', '--git-path', 'info/exclude']), text(root, ['rev-parse', '--git-path', 'info/attributes']), globalIgnore, ...attributes].map((file) => path.resolve(root, file))
|
|
73
|
+
return files.map((file) => ({ path: file, state: fileState(file) }))
|
|
74
|
+
}
|
|
75
|
+
function gitEvidence(root) {
|
|
76
|
+
const result = git(root, ['config', '--path', '--get', 'core.hooksPath'], { allowFailure: true })
|
|
77
|
+
if (!result.ok && result.status !== 1) throw new Error('hook configuration unavailable')
|
|
78
|
+
const hooksPath = path.resolve(root, result.ok ? result.stdout.trim() : text(root, ['rev-parse', '--git-path', 'hooks']))
|
|
79
|
+
const config = parseNullConfig(git(root, ['config', '--null', '--list']).stdout)
|
|
80
|
+
if (config.some(({ key, value }) => /^core\.attributesfile$|^filter\.|^extensions\.(partialclone|worktreeconfig)$|^remote\..*\.promisor$/.test(key) || (/^core\.(sparsecheckout|autocrlf|fsmonitor)$/.test(key) && !/^(false|no|off|0)$/.test(value)))) throw new Error('unsupported Git transformation or checkout configuration')
|
|
81
|
+
if (fs.existsSync(hooksPath) && (fs.lstatSync(hooksPath).isSymbolicLink() || fs.realpathSync(hooksPath) !== hooksPath)) throw new Error('redirected hook directory refused')
|
|
82
|
+
const auxiliary = gitAuxiliary(root)
|
|
83
|
+
for (const entry of [auxiliary[1], ...auxiliary.slice(3)]) {
|
|
84
|
+
if (entry.state && readBytes(entry.path).length) throw new Error('Git attribute transformations unsupported in slice 1')
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
gitAuxDigest: hashObject(auxiliary),
|
|
88
|
+
gitDigest: hashBytes(readBytes(resolveGitExecutable(), { allowLinks: true })),
|
|
89
|
+
gitConfigDigest: hashBytes(git(root, ['config', '--null', '--list', '--show-origin']).stdout),
|
|
90
|
+
hooksPath,
|
|
91
|
+
hooksDigest: hashObject(fs.existsSync(hooksPath) ? inventory(hooksPath) : []),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function executorDigest() {
|
|
95
|
+
const inputs = ['src', 'contracts', 'bin'].map((dir) => ({ dir, files: inventory(path.join(packageRoot, dir)) }))
|
|
96
|
+
for (const file of ['package.json', 'package-lock.json']) inputs.push({ file, state: fileState(path.join(packageRoot, file)) })
|
|
97
|
+
// Resolve from the actual importing module, then follow each package's own
|
|
98
|
+
// dependency resolution. npm may hoist a dependency; nested copies must be
|
|
99
|
+
// bound to the importer that actually selects them, not a guessed layout.
|
|
100
|
+
const roots = new Map()
|
|
101
|
+
const edges = []
|
|
102
|
+
function dependency(specifier, importer, via) {
|
|
103
|
+
const entry = fs.realpathSync(createRequire(importer).resolve(specifier))
|
|
104
|
+
let dir = path.dirname(entry)
|
|
105
|
+
while (!fs.existsSync(path.join(dir, 'package.json'))) {
|
|
106
|
+
const parent = path.dirname(dir)
|
|
107
|
+
if (parent === dir) throw new Error('dependency package identity unavailable')
|
|
108
|
+
dir = parent
|
|
109
|
+
}
|
|
110
|
+
const metadata = readJson(path.join(dir, 'package.json'))
|
|
111
|
+
const id = hashObject({ name: metadata.name, version: metadata.version, files: inventory(dir, { exclude: ['node_modules'], allowLinks: true }) })
|
|
112
|
+
edges.push({ via, specifier, id })
|
|
113
|
+
if (roots.has(dir)) return
|
|
114
|
+
if (roots.size >= 64) throw new Error('executor dependency closure exceeds bounds')
|
|
115
|
+
roots.set(dir, id)
|
|
116
|
+
for (const child of Object.keys(metadata.dependencies ?? {}).sort()) dependency(child, path.join(dir, 'package.json'), id)
|
|
117
|
+
}
|
|
118
|
+
const importer = fileURLToPath(new URL('../export/atelier-export-contract.mjs', import.meta.url))
|
|
119
|
+
dependency('ajv/dist/2020.js', importer, 'schema-validator')
|
|
120
|
+
dependency('ajv-formats', importer, 'schema-validator')
|
|
121
|
+
inputs.push({ dependencies: edges })
|
|
122
|
+
return hashObject({ inputs, node: process.version })
|
|
123
|
+
}
|
|
124
|
+
function identity(root) {
|
|
125
|
+
return hashObject({ root, gitDir: text(root, ['rev-parse', '--absolute-git-dir']), common: text(root, ['rev-parse', '--git-common-dir']) })
|
|
126
|
+
}
|
|
127
|
+
function clean(root) {
|
|
128
|
+
if (git(root, ['status', '--porcelain=v1', '-z', '--untracked-files=all']).stdout) throw new Error('upgrade requires a clean index and working tree')
|
|
129
|
+
const flags = git(root, ['ls-files', '-v', '-z']).stdout.split('\0').filter(Boolean)
|
|
130
|
+
if (flags.some((s) => !s.startsWith('H '))) throw new Error('special index flags unsupported')
|
|
131
|
+
for (const name of ['MERGE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD', 'rebase-merge', 'rebase-apply']) {
|
|
132
|
+
if (fs.existsSync(path.resolve(root, text(root, ['rev-parse', '--git-path', name])))) throw new Error('Git operation already in progress')
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function lease(root, body) {
|
|
136
|
+
const state = privateRoot(root)
|
|
137
|
+
if (fs.readdirSync(state).some((name) => name.startsWith('prepare-'))) throw new Error('stale preparation directory under .atelier-local/upgrades: verify its writer has stopped, then inspect and separately remove only the abandoned prepare-* directory before retrying; no automatic cleanup performed')
|
|
138
|
+
if (inventory(state).reduce((sum, x) => sum + fs.statSync(path.join(state, x.path)).size, 0) > MAX_BYTES / 2) throw new Error('local evidence capacity exhausted; export and verify before retention maintenance')
|
|
139
|
+
const lock = path.join(state, 'writer')
|
|
140
|
+
fs.mkdirSync(lock) // No time-based stealing, including after an interrupted run.
|
|
141
|
+
publish(path.join(lock, 'owner.json'), jsonText({ pid: process.pid, startedAt: new Date().toISOString() }))
|
|
142
|
+
syncDirectory(state)
|
|
143
|
+
try { return body() } finally { fs.rmSync(lock, { recursive: true }); syncDirectory(state) }
|
|
144
|
+
}
|
|
145
|
+
function allowedPaths(project) {
|
|
146
|
+
const paths = ['atelier.lock.json', project.graphPath, path.join(project.outputRoot, 'index.html'), path.join(project.outputRoot, 'atelier.manifest.json'), project.readinessPath].map((p) => path.isAbsolute(p) ? path.relative(project.configDir, p) : p)
|
|
147
|
+
if (new Set(paths).size !== 5 || paths.slice(1).some((p) => !p.startsWith('atelier-output/'))) throw new Error('slice 1 generated writes must be distinct files inside atelier-output')
|
|
148
|
+
paths.forEach((p) => contained(project.configDir, p))
|
|
149
|
+
return paths
|
|
150
|
+
}
|
|
151
|
+
function boundaryCheck(project, staged = false) {
|
|
152
|
+
const loaded = loadBoundaryPolicy(project)
|
|
153
|
+
if (!loaded.ok) throw new Error('boundary policy unavailable')
|
|
154
|
+
const report = checkBoundaryPolicy({ project, policy: loaded.policy, staged, gitExecutable: resolveGitExecutable(), allowNetworkActorResolution: false, forceActorErrors: true })
|
|
155
|
+
if (!report.ok) throw new Error(`boundary postcheck refused: ${report.errors.map((e) => e.code).join(', ')}`)
|
|
156
|
+
}
|
|
157
|
+
function preservedLock(project, generatedAt) {
|
|
158
|
+
const file = path.join(project.configDir, 'atelier.lock.json')
|
|
159
|
+
const previous = fs.existsSync(file) ? validateUpgradeDocument('lock', readJson(file)) : null
|
|
160
|
+
// Pack adoption and template changes are separate participants.
|
|
161
|
+
const next = buildAtelierLock({ project })
|
|
162
|
+
if (previous && !same(previous.boundaryPolicy, next.boundaryPolicy)) throw new Error('boundary policy adoption requires a separate participant')
|
|
163
|
+
if (previous && !same(previous.extensionPacks, next.extensionPacks)) throw new Error('pack adoption requires a separate participant')
|
|
164
|
+
next.generatedAt = generatedAt
|
|
165
|
+
if (previous) { next.appliedMigrations = previous.appliedMigrations; next.template = previous.template; next.lastSuccessfulUpgrade = previous.lastSuccessfulUpgrade }
|
|
166
|
+
return validateUpgradeDocument('lock', next)
|
|
167
|
+
}
|
|
168
|
+
function render(project, readSet, createdAt) {
|
|
169
|
+
const root = project.configDir
|
|
170
|
+
const scratch = fs.mkdtempSync(path.join(privateRoot(root), 'prepare-'))
|
|
171
|
+
const allowed = allowedPaths(project)
|
|
172
|
+
try {
|
|
173
|
+
for (const entry of readSet) {
|
|
174
|
+
if (entry.path.split('/').includes('.gitattributes')) throw new Error('Git attribute transformations unsupported in slice 1')
|
|
175
|
+
const dest = contained(scratch, entry.path)
|
|
176
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
177
|
+
fs.writeFileSync(dest, readBytes(contained(root, entry.path)), { mode: entry.state.mode === '100755' ? 0o755 : 0o644 })
|
|
178
|
+
}
|
|
179
|
+
// Private preparation has no commits or hooks. Keep Git ignore semantics
|
|
180
|
+
// for the existing graph builder without sharing the enrolled index.
|
|
181
|
+
const empty = path.join(scratch, '.atelier-local', 'empty-template')
|
|
182
|
+
fs.mkdirSync(empty, { recursive: true })
|
|
183
|
+
git(scratch, ['init', `--template=${empty}`])
|
|
184
|
+
const [exclude, , globalIgnore] = gitAuxiliary(root)
|
|
185
|
+
if (exclude.state) { fs.mkdirSync(path.join(scratch, '.git/info'), { recursive: true }); fs.writeFileSync(path.join(scratch, '.git/info/exclude'), readBytes(exclude.path)) }
|
|
186
|
+
git(scratch, ['config', 'core.excludesFile', globalIgnore.path])
|
|
187
|
+
const tracked = git(root, ['ls-files', '-z']).stdout.split('\0').filter(Boolean)
|
|
188
|
+
for (let start = 0; start < tracked.length; start += 100) git(scratch, ['add', '-f', '--', ...tracked.slice(start, start + 100)])
|
|
189
|
+
const clone = resolveProjectConfig({ cwd: scratch, argv: ['--project', path.join(scratch, path.basename(project.configPath))], env: {}, writeLocalState: false })
|
|
190
|
+
if (clone.workspaceRoot !== scratch || clone.repoOpsRoot !== scratch || clone.repos.length !== 1 || clone.repos[0].path !== scratch || clone.localOverlay.paths.length) throw new Error('preparation inputs escape isolated workspace')
|
|
191
|
+
for (const file of [clone.configPath, clone.repoAccessPath, clone.boundaryPolicyPath, clone.graphPath, clone.readinessPath, path.join(clone.outputRoot, 'index.html')]) contained(scratch, path.relative(scratch, file))
|
|
192
|
+
const before = snapshot(scratch)
|
|
193
|
+
const write = (rel, bytes) => {
|
|
194
|
+
if (!allowed.includes(rel)) throw new Error('undeclared preparation write')
|
|
195
|
+
const file = contained(scratch, rel)
|
|
196
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
197
|
+
fs.writeFileSync(file, bytes)
|
|
198
|
+
}
|
|
199
|
+
const lock = preservedLock(project, createdAt)
|
|
200
|
+
write('atelier.lock.json', jsonText(lock))
|
|
201
|
+
const graph = buildGraph(clone)
|
|
202
|
+
graph.project = path.basename(root)
|
|
203
|
+
if (graph.errors.length) throw new Error('graph postcheck failed')
|
|
204
|
+
write(path.relative(scratch, clone.graphPath), jsonText(graph))
|
|
205
|
+
const projection = buildProjectProjection(clone)
|
|
206
|
+
write(path.relative(scratch, projection.output), projection.html)
|
|
207
|
+
write(path.relative(scratch, path.join(clone.outputRoot, 'atelier.manifest.json')), jsonText(buildProjectManifest(clone, projection)))
|
|
208
|
+
const readiness = buildReadiness({ project: clone, graph })
|
|
209
|
+
readiness.graph.path = project.graphPath
|
|
210
|
+
readiness.projection.outputRoot = project.outputRoot
|
|
211
|
+
readiness.projection.entry = path.join(project.outputRoot, 'index.html')
|
|
212
|
+
if (!readiness.ready) throw new Error('readiness postcheck failed')
|
|
213
|
+
write(path.relative(scratch, clone.readinessPath), jsonText(readiness))
|
|
214
|
+
const after = snapshot(scratch)
|
|
215
|
+
const writes = after.filter((entry) => !same(entry.state, before.find((p) => p.path === entry.path)?.state ?? null)).map((entry) => {
|
|
216
|
+
if (!allowed.includes(entry.path)) throw new Error('unaccounted preparation write')
|
|
217
|
+
const previous = before.find((p) => p.path === entry.path)?.state ?? null
|
|
218
|
+
return { path: entry.path, owner: entry.path === 'atelier.lock.json' ? 'lock' : 'generated', action: previous ? 'update' : 'create', before: previous, after: entry.state, content: readBytes(contained(scratch, entry.path)).toString('base64') }
|
|
219
|
+
})
|
|
220
|
+
if (before.some((entry) => !after.some((p) => p.path === entry.path))) throw new Error('unaccounted preparation deletion')
|
|
221
|
+
return { writes, allowed }
|
|
222
|
+
} finally { fs.rmSync(scratch, { recursive: true }); syncDirectory(privateRoot(root)) }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function planDigest(plan) { const { digest, ...authority } = plan; return hashObject(authority) }
|
|
226
|
+
function requireDurableHost() {
|
|
227
|
+
if (!['linux', 'darwin'].includes(process.platform)) throw new Error('exact upgrade transactions require a qualified Linux or macOS filesystem; this host is unsupported')
|
|
228
|
+
}
|
|
229
|
+
export function prepareUpgrade({ project, now = new Date() }) {
|
|
230
|
+
requireDurableHost()
|
|
231
|
+
project = canonicalProject(project)
|
|
232
|
+
const root = checkProject(project)
|
|
233
|
+
return lease(root, () => {
|
|
234
|
+
clean(root)
|
|
235
|
+
const adopted = policy(root)
|
|
236
|
+
const readSet = snapshot(root)
|
|
237
|
+
boundaryCheck(project)
|
|
238
|
+
const createdAt = now.toISOString()
|
|
239
|
+
const evidence = gitEvidence(root)
|
|
240
|
+
const head = text(root, ['rev-parse', 'HEAD'])
|
|
241
|
+
const branch = text(root, ['symbolic-ref', 'HEAD'])
|
|
242
|
+
const executor = executorDigest()
|
|
243
|
+
const { writes, allowed } = render(project, readSet, createdAt)
|
|
244
|
+
if (!same(snapshot(root), readSet) || text(root, ['rev-parse', 'HEAD']) !== head || !same(gitEvidence(root), evidence)) throw new Error('inputs changed during preparation')
|
|
245
|
+
clean(root)
|
|
246
|
+
const plan = {
|
|
247
|
+
schema: 'mnstry.atelier-upgrade-plan@v2', digest: '', operation: 'prepare-local-candidate', workspace: root,
|
|
248
|
+
repositoryId: identity(root), configPath: path.basename(project.configPath), baseHead: head, baseTree: text(root, ['rev-parse', 'HEAD^{tree}']), branch,
|
|
249
|
+
createdAt, expiresAt: new Date(now.getTime() + adopted.maxAgeSeconds * 1000).toISOString(),
|
|
250
|
+
policy: adopted, policyDigest: hashObject(adopted), executorDigest: executor, ...evidence, readSet,
|
|
251
|
+
migration: { schema: 'mnstry.atelier-migration@v2', id: 'local-lock-projections@1', executorDigest: executor, readScope: 'whole-enrolled-repository', allowedWrites: allowed, sideEffects: ['git-index', 'git-commit'], postChecks: ['exact-inventory', 'lock-schema', 'exact-index', 'commit-parent-tree-message'] },
|
|
252
|
+
writes, releaseEvidence: null, dependencyInstallation: 'excluded', mode: 'manual-exact-plan', recoveryCoverage: 'local-only', message: 'Prepare Atelier local lock and projections',
|
|
253
|
+
}
|
|
254
|
+
plan.digest = planDigest(plan)
|
|
255
|
+
validateUpgradeDocument('plan', plan)
|
|
256
|
+
const dir = privateDirectory(root, `${PRIVATE}/plans`)
|
|
257
|
+
const savedPlan = path.join(dir, `${plan.digest.slice(7)}.json`)
|
|
258
|
+
if (Buffer.byteLength(jsonText(plan)) > 8 * 1024 * 1024) throw new Error('saved plan exceeds 8 MiB capacity')
|
|
259
|
+
publish(savedPlan, jsonText(plan))
|
|
260
|
+
return { ok: true, savedPlan, plan, confirmationIsAuthenticatedIdentity: false }
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function loadPlan(root, file) {
|
|
265
|
+
const location = path.resolve(file)
|
|
266
|
+
const relative = path.relative(root, location)
|
|
267
|
+
if (!/^\.atelier-local\/upgrades\/plans\/[a-f0-9]{64}\.json$/.test(relative)) throw new Error('plan must be the saved local digest-addressed file')
|
|
268
|
+
const plan = validateUpgradeDocument('plan', readJson(contained(root, relative)))
|
|
269
|
+
if (planDigest(plan) !== plan.digest || path.basename(file) !== `${plan.digest.slice(7)}.json`) throw new Error('plan digest mismatch')
|
|
270
|
+
if (plan.workspace !== root || plan.repositoryId !== identity(root)) throw new Error('plan belongs to another workspace')
|
|
271
|
+
const allowed = plan.migration.allowedWrites
|
|
272
|
+
if (new Set(plan.writes.map((w) => w.path)).size !== plan.writes.length || new Set(plan.readSet.map((r) => r.path)).size !== plan.readSet.length) throw new Error('duplicate plan paths')
|
|
273
|
+
for (const entry of plan.readSet) contained(root, entry.path)
|
|
274
|
+
for (const entry of plan.writes) {
|
|
275
|
+
contained(root, entry.path)
|
|
276
|
+
if (!allowed.includes(entry.path) || (entry.owner === 'lock' ? entry.path !== 'atelier.lock.json' : !entry.path.startsWith('atelier-output/'))) throw new Error('unregistered write path')
|
|
277
|
+
const content = Buffer.from(entry.content, 'base64')
|
|
278
|
+
if (content.toString('base64') !== entry.content || !entry.after || hashBytes(content) !== entry.after.digest || !same(entry.before, plan.readSet.find((r) => r.path === entry.path)?.state ?? null) || entry.action !== (entry.before ? 'update' : 'create')) throw new Error('invalid write evidence')
|
|
279
|
+
}
|
|
280
|
+
return plan
|
|
281
|
+
}
|
|
282
|
+
function expectedSnapshot(plan, count) {
|
|
283
|
+
const entries = new Map(plan.readSet.map((entry) => [entry.path, entry.state]))
|
|
284
|
+
for (const write of plan.writes.slice(0, count)) entries.set(write.path, write.after)
|
|
285
|
+
return [...entries].map(([file, state]) => ({ path: file, state })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Read-only explanation of verified saved bytes. This never records consent or
|
|
289
|
+
// reserves a writer; application remains responsible for its full live checks.
|
|
290
|
+
export function explainSavedUpgrade({ project, planFile, now = new Date() }) {
|
|
291
|
+
requireDurableHost()
|
|
292
|
+
project = canonicalProject(project)
|
|
293
|
+
const root = checkProject(project)
|
|
294
|
+
const plan = loadPlan(root, planFile)
|
|
295
|
+
const blockers = []
|
|
296
|
+
try {
|
|
297
|
+
clean(root)
|
|
298
|
+
liveBindings(root, plan, 0, now)
|
|
299
|
+
if (!same(allowedPaths(project), plan.migration.allowedWrites) || path.basename(project.configPath) !== plan.configPath) throw new Error('project binding changed')
|
|
300
|
+
if (fs.existsSync(operationDirectory(root, plan.digest.slice(7)))) throw new Error('plan already consumed; inspect its operation status')
|
|
301
|
+
const state = contained(root, PRIVATE)
|
|
302
|
+
if (fs.existsSync(path.join(state, 'writer'))) throw new Error('writer lease exists; inspect its owner')
|
|
303
|
+
if (fs.readdirSync(state).some((name) => name.startsWith('prepare-'))) throw new Error('abandoned preparation requires inspection')
|
|
304
|
+
} catch (error) { blockers.push(error.message) }
|
|
305
|
+
return {
|
|
306
|
+
schema: 'mnstry.atelier-upgrade-explanation@v1', ok: true,
|
|
307
|
+
planDigest: plan.digest, baseHead: plan.baseHead, branch: plan.branch,
|
|
308
|
+
expiresAt: plan.expiresAt, bindingsCurrent: blockers.length === 0, blockers,
|
|
309
|
+
writes: plan.writes.map(({ path, owner, action, before, after }) => ({ path, owner, action, before, after })),
|
|
310
|
+
provenance: { executorDigest: plan.executorDigest, gitDigest: plan.gitDigest, gitConfigDigest: plan.gitConfigDigest, gitAuxDigest: plan.gitAuxDigest, hooksDigest: plan.hooksDigest, sourceFiles: plan.readSet.length, releaseEvidence: plan.releaseEvidence },
|
|
311
|
+
consent: { mode: plan.mode, requiredEffects: plan.policy.allowedEffects, humanApprovalAuthenticated: false, applicationAuthorized: false },
|
|
312
|
+
outcome: 'local candidate commit', dependencyInstallation: plan.dependencyInstallation,
|
|
313
|
+
activation: 'excluded', recovery: 'local backups and inspection only; no automatic rollback',
|
|
314
|
+
caveats: ['Generated readiness bytes can contain absolute host paths; inspect the saved writes before sharing.', 'Local receipts prove consistency, not publisher identity or an authenticated human decision.', 'Application rechecks current state; this report neither reserves the workspace nor grants permission.'],
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function liveBindings(root, plan, count, now = new Date()) {
|
|
319
|
+
for (const rel of ['.atelier-local/readiness', '.atelier-local/atelier.local.json', 'atelier.local.json', 'atelier.workspace.local.json']) if (fs.existsSync(contained(root, rel))) throw new Error('unsupported local context appeared')
|
|
320
|
+
if (now < new Date(plan.createdAt) || now >= new Date(plan.expiresAt) || new Date(plan.expiresAt) - new Date(plan.createdAt) > plan.policy.maxAgeSeconds * 1000) throw new Error('plan expired or clock moved backward')
|
|
321
|
+
if (!same(policy(root), plan.policy) || hashObject(plan.policy) !== plan.policyDigest) throw new Error('policy changed or revoked')
|
|
322
|
+
if (executorDigest() !== plan.executorDigest || plan.migration.executorDigest !== plan.executorDigest) throw new Error('executor changed')
|
|
323
|
+
if (!same(gitEvidence(root), Object.fromEntries(['gitDigest', 'gitConfigDigest', 'gitAuxDigest', 'hooksPath', 'hooksDigest'].map((k) => [k, plan[k]])))) throw new Error('Git or hook identity changed')
|
|
324
|
+
if (text(root, ['rev-parse', 'HEAD']) !== plan.baseHead || text(root, ['symbolic-ref', 'HEAD']) !== plan.branch || !same(snapshot(root), expectedSnapshot(plan, count))) throw new Error('repository changed from saved plan')
|
|
325
|
+
}
|
|
326
|
+
function operationDirectory(root, id) {
|
|
327
|
+
if (!/^[a-f0-9]{64}$/.test(id)) throw new Error('invalid operation ID')
|
|
328
|
+
return contained(root, `${PRIVATE}/operations/${id}`)
|
|
329
|
+
}
|
|
330
|
+
function events(root, id) {
|
|
331
|
+
const dir = contained(root, `${PRIVATE}/operations/${id}/events`)
|
|
332
|
+
const records = []
|
|
333
|
+
let previous = null
|
|
334
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
335
|
+
const record = validateUpgradeDocument('event', readJson(contained(root, `${PRIVATE}/operations/${id}/events/${name}`)))
|
|
336
|
+
const digest = hashObject(record)
|
|
337
|
+
if (name !== `${String(records.length).padStart(6, '0')}-${digest.slice(7)}.json` || record.sequence !== records.length || record.previous !== previous || record.operationId !== id || record.planDigest !== `sha256:${id}` || record.confirmation !== record.planDigest || (records.length && new Date(record.time) < new Date(records.at(-1).time))) throw new Error('broken upgrade journal; recovery required')
|
|
338
|
+
if ((!records.length && record.phase !== 'accepted') || ['completed', 'commit-refused', 'recovery-required'].includes(records.at(-1)?.phase)) throw new Error('invalid journal transition')
|
|
339
|
+
records.push(record); previous = digest
|
|
340
|
+
}
|
|
341
|
+
return records
|
|
342
|
+
}
|
|
343
|
+
function append(root, plan, phase, { step = null, expected = null, observed = null } = {}) {
|
|
344
|
+
const id = plan.digest.slice(7)
|
|
345
|
+
const chain = events(root, id)
|
|
346
|
+
const event = {
|
|
347
|
+
schema: 'mnstry.atelier-upgrade-receipt@v1', kind: 'private-event', operationId: id, planDigest: plan.digest,
|
|
348
|
+
sequence: chain.length, previous: chain.length ? hashObject(chain.at(-1)) : null,
|
|
349
|
+
time: new Date().toISOString(), phase, step, expected, observed, confirmation: plan.digest,
|
|
350
|
+
}
|
|
351
|
+
if (chain.length && new Date(event.time) < new Date(chain.at(-1).time)) throw new Error('clock moved backward during operation')
|
|
352
|
+
validateUpgradeDocument('event', event)
|
|
353
|
+
publish(path.join(operationDirectory(root, id), 'events', `${String(chain.length).padStart(6, '0')}-${hashObject(event).slice(7)}.json`), jsonText(event))
|
|
354
|
+
events(root, id)
|
|
355
|
+
}
|
|
356
|
+
function indexManifest(root) {
|
|
357
|
+
return git(root, ['ls-files', '--stage', '-z']).stdout.split('\0').filter(Boolean).map((record) => {
|
|
358
|
+
const match = /^(\d+) ([a-f0-9]+) 0\t(.+)$/.exec(record)
|
|
359
|
+
if (!match) throw new Error('unmerged or malformed index')
|
|
360
|
+
return { path: match[3], mode: match[1], oid: match[2] }
|
|
361
|
+
}).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
|
|
362
|
+
}
|
|
363
|
+
function plannedIndex(root, plan) {
|
|
364
|
+
const entries = git(root, ['ls-tree', '-r', '-z', plan.baseHead]).stdout.split('\0').filter(Boolean).map((record) => {
|
|
365
|
+
const match = /^(100644|100755) blob ([a-f0-9]+)\t(.+)$/.exec(record)
|
|
366
|
+
if (!match) throw new Error('unsupported base tree entry')
|
|
367
|
+
return { path: match[3], mode: match[1], oid: match[2] }
|
|
368
|
+
})
|
|
369
|
+
const byPath = new Map(entries.map((e) => [e.path, e]))
|
|
370
|
+
for (const entry of plan.writes) byPath.set(entry.path, { path: entry.path, mode: entry.after.mode, oid: git(root, ['hash-object', '--stdin', '--no-filters'], { input: Buffer.from(entry.content, 'base64') }).stdout.trim() })
|
|
371
|
+
return [...byPath.values()].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
|
|
372
|
+
}
|
|
373
|
+
function verifyCommit(root, plan, commit, tree) {
|
|
374
|
+
if (!/^[a-f0-9]{40,64}$/.test(commit) || text(root, ['show', '-s', '--format=%P', commit]) !== plan.baseHead || text(root, ['show', '-s', '--format=%T', commit]) !== tree || text(root, ['show', '-s', '--format=%B', commit]) !== plan.message) throw new Error('commit parent/tree/message mismatch')
|
|
375
|
+
}
|
|
376
|
+
export function applySavedUpgrade({ project, planFile, confirm }) {
|
|
377
|
+
requireDurableHost()
|
|
378
|
+
project = canonicalProject(project)
|
|
379
|
+
const root = checkProject(project)
|
|
380
|
+
return lease(root, () => {
|
|
381
|
+
const plan = loadPlan(root, planFile)
|
|
382
|
+
if (confirm !== plan.digest) throw new Error('exact plan confirmation required')
|
|
383
|
+
clean(root)
|
|
384
|
+
liveBindings(root, plan, 0)
|
|
385
|
+
if (!same(allowedPaths(project), plan.migration.allowedWrites) || path.basename(project.configPath) !== plan.configPath) throw new Error('project binding changed')
|
|
386
|
+
const id = plan.digest.slice(7)
|
|
387
|
+
privateDirectory(root, `${PRIVATE}/operations`)
|
|
388
|
+
const operation = operationDirectory(root, id)
|
|
389
|
+
fs.mkdirSync(operation) // A prior attempt consumes this plan, even if interrupted.
|
|
390
|
+
syncDirectory(path.dirname(operation))
|
|
391
|
+
privateDirectory(root, `${PRIVATE}/operations/${id}/events`)
|
|
392
|
+
privateDirectory(root, `${PRIVATE}/operations/${id}/backups`)
|
|
393
|
+
append(root, plan, 'accepted', { expected: plan.digest, observed: 'manual content selection; local recovery only' })
|
|
394
|
+
try {
|
|
395
|
+
const baseIndex = indexManifest(root)
|
|
396
|
+
for (const [i, entry] of plan.writes.entries()) {
|
|
397
|
+
liveBindings(root, plan, i)
|
|
398
|
+
if (!same(indexManifest(root), baseIndex)) throw new Error('index changed before staging')
|
|
399
|
+
if (entry.before) publish(path.join(operation, 'backups', `${i}.bin`), readBytes(contained(root, entry.path)))
|
|
400
|
+
append(root, plan, 'write-intent', { step: entry.path, expected: hashObject({ before: entry.before, after: entry.after }) })
|
|
401
|
+
replaceExpected(root, entry)
|
|
402
|
+
append(root, plan, 'write-complete', { step: entry.path, observed: entry.after.digest })
|
|
403
|
+
}
|
|
404
|
+
liveBindings(root, plan, plan.writes.length)
|
|
405
|
+
validateUpgradeDocument('lock', readJson(path.join(root, 'atelier.lock.json')))
|
|
406
|
+
boundaryCheck(project)
|
|
407
|
+
const expectedIndex = plannedIndex(root, plan)
|
|
408
|
+
if (!same(indexManifest(root), baseIndex)) throw new Error('index changed before staging')
|
|
409
|
+
append(root, plan, 'stage-intent', { expected: hashObject(expectedIndex) })
|
|
410
|
+
// Literal, concrete paths only. Explicitly planned ignored outputs are
|
|
411
|
+
// admitted here; no stage-all, glob expansion or implicit authored files.
|
|
412
|
+
for (const entry of plan.writes) git(root, ['add', '-f', '--', entry.path])
|
|
413
|
+
if (!same(indexManifest(root), expectedIndex)) throw new Error('staged blobs differ from saved plan')
|
|
414
|
+
liveBindings(root, plan, plan.writes.length)
|
|
415
|
+
boundaryCheck(project, true)
|
|
416
|
+
const tree = text(root, ['write-tree'])
|
|
417
|
+
append(root, plan, 'commit-intent', { expected: tree })
|
|
418
|
+
const result = git(root, ['commit', '-m', plan.message], { allowFailure: true, timeout: 30_000 })
|
|
419
|
+
const head = text(root, ['rev-parse', 'HEAD'])
|
|
420
|
+
const unchanged = same(indexManifest(root), expectedIndex) && same(snapshot(root), expectedSnapshot(plan, plan.writes.length))
|
|
421
|
+
if (!result.ok) {
|
|
422
|
+
append(root, plan, head === plan.baseHead && unchanged && !result.error && !result.signal ? 'commit-refused' : 'recovery-required', { expected: tree, observed: `Git exit ${result.status}; ${result.error || result.stderr}` })
|
|
423
|
+
return { ok: false, operationId: id, status: events(root, id).at(-1).phase }
|
|
424
|
+
}
|
|
425
|
+
if (!unchanged || text(root, ['symbolic-ref', 'HEAD']) !== plan.branch) throw new Error('hook changed unreviewed state')
|
|
426
|
+
verifyCommit(root, plan, head, tree)
|
|
427
|
+
// Hooks may alter policy, their own bytes, or configuration too.
|
|
428
|
+
if (!same(gitEvidence(root), Object.fromEntries(['gitDigest', 'gitConfigDigest', 'gitAuxDigest', 'hooksPath', 'hooksDigest'].map((k) => [k, plan[k]])))) throw new Error('hook or Git configuration changed during commit')
|
|
429
|
+
clean(root)
|
|
430
|
+
append(root, plan, 'completed', { expected: tree, observed: head })
|
|
431
|
+
return { ok: true, operationId: id, status: 'completed', commit: head, activated: false, lockSuccessMetadata: 'historical; terminal receipt is authoritative' }
|
|
432
|
+
} catch (error) {
|
|
433
|
+
// Never reset user files or retry a commit. A failing journal write
|
|
434
|
+
// remains a nonterminal/broken chain and status refuses success.
|
|
435
|
+
if (!['completed', 'commit-refused', 'recovery-required'].includes(events(root, id).at(-1)?.phase)) append(root, plan, 'recovery-required', { observed: error.message })
|
|
436
|
+
return { ok: false, operationId: id, status: 'recovery-required', reason: error.message }
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
export function upgradeOperationStatus({ project, operationId }) {
|
|
441
|
+
const root = fs.realpathSync(project.configDir)
|
|
442
|
+
privateRoot(root)
|
|
443
|
+
const plan = loadPlan(root, path.join(root, PRIVATE, 'plans', `${operationId}.json`))
|
|
444
|
+
const chain = events(root, operationId)
|
|
445
|
+
const last = chain.at(-1)
|
|
446
|
+
const phase = ['completed', 'commit-refused', 'recovery-required'].includes(last?.phase) ? last.phase : 'recovery-required'
|
|
447
|
+
if (phase === 'completed') verifyCommit(root, plan, last.observed, last.expected)
|
|
448
|
+
return { ok: phase === 'completed', operationId, status: phase, lastEvent: last ?? null, events: chain.length, activated: false, recoveryCoverage: 'local-only', lockSuccessMetadata: 'historical; terminal receipt is authoritative', writerLeasePresent: fs.existsSync(path.join(root, PRIVATE, 'writer')) }
|
|
449
|
+
}
|
|
450
|
+
export function recoverUpgradeDryRun({ project, operationId }) {
|
|
451
|
+
const root = fs.realpathSync(project.configDir)
|
|
452
|
+
const status = upgradeOperationStatus({ project, operationId })
|
|
453
|
+
const plan = loadPlan(root, path.join(root, PRIVATE, 'plans', `${operationId}.json`))
|
|
454
|
+
const chain = events(root, operationId)
|
|
455
|
+
const head = text(root, ['rev-parse', 'HEAD'])
|
|
456
|
+
const index = indexManifest(root)
|
|
457
|
+
const planned = plannedIndex(root, plan)
|
|
458
|
+
const base = plannedIndex(root, { ...plan, writes: [] })
|
|
459
|
+
const entries = plan.writes.map((entry, i) => {
|
|
460
|
+
const current = fileState(contained(root, entry.path))
|
|
461
|
+
const began = chain.some((e) => e.phase === 'write-intent' && e.step === entry.path)
|
|
462
|
+
const backup = path.join(operationDirectory(root, operationId), 'backups', `${i}.bin`)
|
|
463
|
+
const validBackup = !entry.before || (fs.existsSync(backup) && hashBytes(readBytes(backup)) === entry.before.digest)
|
|
464
|
+
const staged = index.find((e) => e.path === entry.path)
|
|
465
|
+
const stagedExpected = planned.find((e) => e.path === entry.path)
|
|
466
|
+
const safe = began && head === plan.baseHead && same(current, entry.after) && validBackup && (same(staged ?? null, stagedExpected ?? null) || same(staged ?? null, base.find((e) => e.path === entry.path) ?? null))
|
|
467
|
+
return { path: entry.path, state: same(current, entry.before) ? 'already-original' : safe ? 'restorable-after-new-confirmation' : 'conflict', restore: safe ? entry.before ? 'backup' : 'remove-created-file' : null }
|
|
468
|
+
})
|
|
469
|
+
const current = snapshot(root)
|
|
470
|
+
const knownWrites = new Set(plan.writes.map((w) => w.path))
|
|
471
|
+
const observedPaths = new Set([...current, ...plan.readSet].map((e) => e.path))
|
|
472
|
+
const unexpected = [...observedPaths].filter((file) => !knownWrites.has(file) && !same(current.find((e) => e.path === file)?.state ?? null, plan.readSet.find((e) => e.path === file)?.state ?? null))
|
|
473
|
+
const proposal = { operationId, status: status.status, head, indexDigest: hashObject(index), entries, unexpected, mutation: false, requiresNewConfirmation: true }
|
|
474
|
+
return { ...proposal, digest: hashObject(proposal), note: head === plan.baseHead ? 'No files or index entries restored. Review conflicts and exact current state before a separately authorized recovery.' : 'HEAD moved; preserve committed history. No automatic reset or rollback is offered.' }
|
|
475
|
+
}
|
package/src/upgrade/upgrade.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import crypto from 'node:crypto'
|
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { buildGraph } from '../graph/graph.mjs'
|
|
6
|
-
import { buildProjectProjection } from '../projection/project.mjs'
|
|
6
|
+
import { buildProjectProjection, buildProjectManifest } from '../projection/project.mjs'
|
|
7
7
|
import { buildReadiness } from '../readiness/readiness.mjs'
|
|
8
8
|
import { validateJsonSchema } from '../export/atelier-export-contract.mjs'
|
|
9
9
|
import {
|
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
} from '../boundary/policy.mjs'
|
|
24
24
|
import { bundledMnstryReadinessPackV1 } from '../readiness-protocols/bundled-pack.mjs'
|
|
25
25
|
import { loadExtensionPacks } from '../extension-packs/loader.mjs'
|
|
26
|
+
import { inspectPackageProvenance, legacyPackageSource } from './provenance.mjs'
|
|
27
|
+
import { prepareUpgrade, applySavedUpgrade, upgradeOperationStatus, recoverUpgradeDryRun, explainSavedUpgrade } from './transaction.mjs'
|
|
28
|
+
import { renderUpgradeExplanation } from './explanation.mjs'
|
|
29
|
+
export { prepareUpgrade, applySavedUpgrade, upgradeOperationStatus, recoverUpgradeDryRun, explainSavedUpgrade }
|
|
26
30
|
|
|
27
31
|
export const ATELIER_LOCK_SCHEMA = 'mnstry.atelier-lock@v1'
|
|
28
32
|
export const ATELIER_MIGRATION_SCHEMA = 'mnstry.atelier-migration@v1'
|
|
@@ -77,18 +81,7 @@ function packageGitSha() {
|
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
function packageSource() {
|
|
80
|
-
|
|
81
|
-
const repository = packageGitRoot()
|
|
82
|
-
? git(packageRoot, ['config', '--get', 'remote.origin.url']) || packageJson.repository?.url || null
|
|
83
|
-
: packageJson.repository?.url || null
|
|
84
|
-
if (repository && gitSha) return { type: 'git', repository, gitSha }
|
|
85
|
-
if (repository) return { type: 'private_github', repository, gitSha: null }
|
|
86
|
-
return {
|
|
87
|
-
type: 'local_path',
|
|
88
|
-
path: '.',
|
|
89
|
-
gitSha,
|
|
90
|
-
repository,
|
|
91
|
-
}
|
|
84
|
+
return legacyPackageSource(inspectPackageProvenance(packageRoot, { includeInventory: false }))
|
|
92
85
|
}
|
|
93
86
|
|
|
94
87
|
export function lockPathForProject(project) {
|
|
@@ -172,7 +165,7 @@ export function buildAtelierLock({ project, templateId = 'existing-workspace', a
|
|
|
172
165
|
}
|
|
173
166
|
}
|
|
174
167
|
|
|
175
|
-
export function writeAtelierLock({ project, templateId
|
|
168
|
+
export function writeAtelierLock({ project, templateId } = {}) {
|
|
176
169
|
const lockPath = lockPathForProject(project)
|
|
177
170
|
// Packs load in throwing mode on purpose: a broken extension pack cannot be
|
|
178
171
|
// locked. Lock verification is disabled for this one load so re-pinning is
|
|
@@ -180,8 +173,18 @@ export function writeAtelierLock({ project, templateId = 'existing-workspace' }
|
|
|
180
173
|
// moved or set aside, so it exists at every observable point, and writeJson
|
|
181
174
|
// replaces it atomically (temp file + renameSync). A load failure throws
|
|
182
175
|
// before any write and leaves the previous lock untouched.
|
|
176
|
+
const previous = readOptionalJson(lockPath)
|
|
177
|
+
if (previous) {
|
|
178
|
+
const errors = validateJsonSchema(lockSchema, previous)
|
|
179
|
+
if (errors.length) throw new Error(`cannot preserve invalid lock history: ${errors.join('; ')}`)
|
|
180
|
+
}
|
|
183
181
|
const { packs } = loadExtensionPacks(project, { verifyLock: false })
|
|
184
|
-
const lock = buildAtelierLock({ project, templateId, packs })
|
|
182
|
+
const lock = buildAtelierLock({ project, templateId: templateId || previous?.template?.id || 'existing-workspace', packs })
|
|
183
|
+
if (previous) {
|
|
184
|
+
lock.appliedMigrations = previous.appliedMigrations
|
|
185
|
+
lock.lastSuccessfulUpgrade = previous.lastSuccessfulUpgrade
|
|
186
|
+
lock.template = { ...previous.template, ...(templateId ? { id: templateId } : {}) }
|
|
187
|
+
}
|
|
185
188
|
writeJson(lockPath, lock)
|
|
186
189
|
return lock
|
|
187
190
|
}
|
|
@@ -519,13 +522,7 @@ function runGeneratedRefresh(project) {
|
|
|
519
522
|
const projection = buildProjectProjection(project)
|
|
520
523
|
fs.mkdirSync(path.dirname(projection.output), { recursive: true })
|
|
521
524
|
fs.writeFileSync(projection.output, projection.html)
|
|
522
|
-
writeJson(path.join(project.outputRoot, 'atelier.manifest.json'),
|
|
523
|
-
schema: 'mnstry.atelier-manifest@v1',
|
|
524
|
-
generatedAt: 'deterministic',
|
|
525
|
-
graphPath: project.graphPath,
|
|
526
|
-
entry: 'index.html',
|
|
527
|
-
nodes: projection.graph.nodes.map((node) => ({ id: node.id, title: node.title, audience: node.audience, path: node.path })),
|
|
528
|
-
})
|
|
525
|
+
writeJson(path.join(project.outputRoot, 'atelier.manifest.json'), buildProjectManifest(project, projection))
|
|
529
526
|
writeJson(project.readinessPath, buildReadiness({ project, graph }))
|
|
530
527
|
}
|
|
531
528
|
|
|
@@ -617,7 +614,7 @@ export function checkAtelierLock(project) {
|
|
|
617
614
|
if (lock && lock.package?.version !== packageJson.version) errors.push(`lock package version ${lock.package?.version} does not match ${packageJson.version}`)
|
|
618
615
|
if (lock && lock.contracts?.boundaryPolicy !== BOUNDARY_POLICY_SCHEMA) errors.push(`lock boundary policy contract must be ${BOUNDARY_POLICY_SCHEMA}`)
|
|
619
616
|
const loadedPolicy = loadPolicyForLock(project)
|
|
620
|
-
if (lock
|
|
617
|
+
if (lock && (lock.boundaryPolicy?.digest ?? null) !== loadedPolicy.digest) {
|
|
621
618
|
errors.push('boundary policy digest has changed; run upgrade --dry-run before applying')
|
|
622
619
|
}
|
|
623
620
|
if (lock) {
|
|
@@ -656,24 +653,62 @@ export function checkAtelierLock(project) {
|
|
|
656
653
|
export function runLockCommand(argv = process.argv.slice(2)) {
|
|
657
654
|
const args = parseArgs(argv)
|
|
658
655
|
const subcommand = args._[0] || 'check'
|
|
656
|
+
if (subcommand === 'provenance') {
|
|
657
|
+
const report = inspectPackageProvenance(packageRoot)
|
|
658
|
+
console.log(JSON.stringify(report, null, 2))
|
|
659
|
+
process.exitCode = args['exact-source-required'] && !report.sourceVerified ? 1 : 0
|
|
660
|
+
return
|
|
661
|
+
}
|
|
659
662
|
const project = commandProject({ argv })
|
|
660
663
|
if (subcommand === 'write') {
|
|
661
|
-
const lock = writeAtelierLock({ project, templateId: firstString(args.template, args['template-id'], project.config?.template?.id) ||
|
|
664
|
+
const lock = writeAtelierLock({ project, templateId: firstString(args.template, args['template-id'], project.config?.template?.id) || undefined })
|
|
662
665
|
console.log(JSON.stringify({ ok: true, path: lockPathForProject(project), lock }, null, 2))
|
|
663
|
-
process.
|
|
666
|
+
process.exitCode = 0
|
|
667
|
+
return
|
|
664
668
|
}
|
|
665
669
|
if (subcommand === 'check') {
|
|
666
670
|
const report = checkAtelierLock(project)
|
|
671
|
+
if (args['exact-source-required']) {
|
|
672
|
+
report.provenance = inspectPackageProvenance(packageRoot)
|
|
673
|
+
if (!report.provenance.sourceVerified) { report.ok = false; report.errors.push('installed package source is unverified') }
|
|
674
|
+
}
|
|
667
675
|
console.log(JSON.stringify(report, null, 2))
|
|
668
|
-
process.
|
|
676
|
+
process.exitCode = report.ok ? 0 : 1
|
|
677
|
+
return
|
|
669
678
|
}
|
|
670
679
|
console.error(`Unknown lock command: ${subcommand}`)
|
|
671
|
-
process.
|
|
680
|
+
process.exitCode = 1
|
|
672
681
|
}
|
|
673
682
|
|
|
674
683
|
export function runUpgradeCommand(argv = process.argv.slice(2)) {
|
|
675
684
|
const args = parseArgs(argv)
|
|
676
|
-
const project = commandProject({ argv })
|
|
685
|
+
const project = commandProject({ argv, writeLocalState: args._[0] !== 'explain' })
|
|
686
|
+
if (['plan', 'apply', 'status', 'recover', 'explain'].includes(args._[0])) {
|
|
687
|
+
try {
|
|
688
|
+
let result
|
|
689
|
+
if (args._[0] === 'explain') {
|
|
690
|
+
if (!firstString(args.plan)) throw new Error('--plan is required')
|
|
691
|
+
if (args.format != null && !['json', 'markdown'].includes(args.format)) throw new Error('--format must be json or markdown')
|
|
692
|
+
result = explainSavedUpgrade({ project, planFile: args.plan })
|
|
693
|
+
console.log(args.format === 'markdown' ? renderUpgradeExplanation(result) : JSON.stringify(result, null, 2))
|
|
694
|
+
return
|
|
695
|
+
} else if (args._[0] === 'plan') {
|
|
696
|
+
if (args.save !== true) throw new Error('exact planning requires --save')
|
|
697
|
+
result = prepareUpgrade({ project })
|
|
698
|
+
} else if (args._[0] === 'apply') {
|
|
699
|
+
if (!firstString(args.plan)) throw new Error('--plan is required')
|
|
700
|
+
result = applySavedUpgrade({ project, planFile: args.plan, confirm: args.confirm })
|
|
701
|
+
} else if (args._[0] === 'status') {
|
|
702
|
+
result = upgradeOperationStatus({ project, operationId: args.operation })
|
|
703
|
+
} else {
|
|
704
|
+
if (args['dry-run'] !== true) throw new Error('recovery supports --dry-run only; mutation requires a separately reviewed recovery plan')
|
|
705
|
+
result = recoverUpgradeDryRun({ project, operationId: args.operation })
|
|
706
|
+
}
|
|
707
|
+
console.log(JSON.stringify(result, null, 2))
|
|
708
|
+
process.exitCode = result.ok === false ? 1 : 0
|
|
709
|
+
} catch (error) { console.error(error.message); process.exitCode = 1 }
|
|
710
|
+
return
|
|
711
|
+
}
|
|
677
712
|
const confirmBreaking = asArray(args['confirm-breaking']).concat(firstString(args['confirm-breaking']) ? [String(args['confirm-breaking'])] : [])
|
|
678
713
|
const allowDirtyGenerated = Boolean(args['allow-dirty-generated'])
|
|
679
714
|
if (args.check) {
|