@mnstry/atelier 0.2.0-alpha.6 → 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 +176 -0
- package/README.md +59 -15
- 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-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 +9 -1
- package/docs/atelier-sync.md +5 -4
- package/docs/blocks/claims.md +15 -10
- package/docs/blocks/will-not-do.md +4 -1
- 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 +88 -0
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +55 -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-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 +57 -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/policy.mjs +57 -25
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +43 -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 +8 -1
- package/src/composition/registry.mjs +27 -0
- package/src/composition/wire.mjs +122 -0
- package/src/contracts/corpus.mjs +29 -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/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +26 -27
- 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/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/supervisor.mjs +16 -1
- 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,368 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { AtelierDiagnosticError, resolveProjectConfig } from '../project/config.mjs'
|
|
6
|
+
import { ObsidianContractRefusal } from '../projection/obsidian/contracts.mjs'
|
|
7
|
+
import { applyPolicyDigest } from '../projection/obsidian/edits/policy.mjs'
|
|
8
|
+
import { MINIMUM_APP_VERSION } from '../runtime/obsidian/app-capability.mjs'
|
|
9
|
+
import { loadContributions } from '../runtime/obsidian/contributions.mjs'
|
|
10
|
+
import { isoTime } from '../runtime/obsidian/documents.mjs'
|
|
11
|
+
import { readObsidianEnablement } from '../runtime/obsidian/enablement.mjs'
|
|
12
|
+
import { ObsidianMaintenanceRefusal, refuse } from '../runtime/obsidian/errors.mjs'
|
|
13
|
+
import { BUILT_IN_OPERATIONS, UNAVAILABLE_APPLY_OPERATION, createObsidianRegistry } from '../runtime/obsidian/extension-points.mjs'
|
|
14
|
+
import { LIFECYCLE_PRIMITIVES, readServiceStatusDocument, serviceStatus, startService, stopService } from '../runtime/obsidian/lifecycle.mjs'
|
|
15
|
+
import {
|
|
16
|
+
authorizeAutomaticApply, defaultMachineSettings, ensureWorkspaceIdentity, installApplyPolicy, protectedRoots, readInstalledApplyPolicy, readMachineSettings,
|
|
17
|
+
revokeApplyPolicy, writeMachineSettings,
|
|
18
|
+
} from '../runtime/obsidian/machine-settings.mjs'
|
|
19
|
+
import { APPLY_UNAVAILABLE, OPENING_OUTCOMES, OPENING_PRIMITIVES, openScopeForOracleTests, resolveScope, scopeReport } from '../runtime/obsidian/opening.mjs'
|
|
20
|
+
import { serviceNameFor, servicePaths } from '../runtime/obsidian/service-record.mjs'
|
|
21
|
+
import { resolveServiceWorkspace } from '../runtime/obsidian/service.mjs'
|
|
22
|
+
import { buildStartupAdapter } from '../runtime/obsidian/startup-adapters.mjs'
|
|
23
|
+
|
|
24
|
+
// `atelier obsidian <operation>`: status, views, audiences, apply policy, the
|
|
25
|
+
// owned maintenance service, and opening a view.
|
|
26
|
+
//
|
|
27
|
+
// Noninteractive: every input is an argument, nothing is ever asked. With
|
|
28
|
+
// `--json` exactly one JSON document is printed on stdout, for a refusal too.
|
|
29
|
+
//
|
|
30
|
+
// Exit codes: 0 done (for `open`: the view is current and open); 1 an error
|
|
31
|
+
// nobody typed; 2 a typed refusal or a usage error; 3 the operation ran and
|
|
32
|
+
// its answer is not success (any opening outcome but `current`, a service
|
|
33
|
+
// that did not start, a stop that was refused).
|
|
34
|
+
//
|
|
35
|
+
// Reaching the installed app or the operating system goes through seams
|
|
36
|
+
// (`appProbe`, `launcher`, `service`). A caller passes them. The production
|
|
37
|
+
// seams are constructed only when this module runs as the real command-line
|
|
38
|
+
// entry (`production: true`) AND `--adapter=obsidian-cli` was given, the same
|
|
39
|
+
// explicit rule the service entry has; there is no default.
|
|
40
|
+
|
|
41
|
+
export const COMMAND_SCHEMA = 'atelier-obsidian-command/v1'
|
|
42
|
+
export const EXIT = Object.freeze({ ok: 0, error: 1, refused: 2, notSuccess: 3 })
|
|
43
|
+
export { BUILT_IN_OPERATIONS }
|
|
44
|
+
const PRODUCTION_ADAPTER = 'obsidian-cli'
|
|
45
|
+
const MAX_POLICY_BYTES = 64 * 1024
|
|
46
|
+
const AUDIENCE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
47
|
+
|
|
48
|
+
export const USAGE = `Usage: atelier obsidian <operation> [--project atelier.project.json] [--data-root DIR] [--json]
|
|
49
|
+
|
|
50
|
+
status Enablement, machine settings, service and per-view freshness. Read-only.
|
|
51
|
+
scope list | scope show ID The views this project declares. Read-only.
|
|
52
|
+
audience show | set A,B | clear The audiences this machine lets into a view (private; none by default).
|
|
53
|
+
mode show | set manual|automatic Whether queued edits wait for a person or are applied under the policy.
|
|
54
|
+
policy show | install FILE | revoke The private apply policy. Automatic mode needs an installed, active one.
|
|
55
|
+
policy digest FILE The digest FILE has to carry to be installed. Reads FILE; writes nothing.
|
|
56
|
+
service start [--consent-actor ID] --adapter=${PRODUCTION_ADAPTER}
|
|
57
|
+
service status | stop The owned maintenance service of this workspace.
|
|
58
|
+
service unit --print --adapter=${PRODUCTION_ADAPTER}
|
|
59
|
+
Print an operating-system startup unit. Writes and installs nothing.
|
|
60
|
+
open [--scope ID] [--consent-actor ID] [--allow-stale] --adapter=${PRODUCTION_ADAPTER}
|
|
61
|
+
Start or reconnect maintenance, verify the view, open it in Obsidian.
|
|
62
|
+
|
|
63
|
+
Contributed operations, registered by the modules shipped under src/runtime/obsidian/contributions/:
|
|
64
|
+
apply list | show EDIT | run EDIT [--actor ID] | recover
|
|
65
|
+
Pending edits, and the explicit apply of one of them to its source file.
|
|
66
|
+
conflicts [ID] The shared conflict state of edited objects. Read-only.
|
|
67
|
+
apply-policy create FILE | show | revoke
|
|
68
|
+
Build, digest and install an apply policy from a JSON request; show it; revoke it.
|
|
69
|
+
selection resolve ID | persist ID [allow-empty] | show ID | list
|
|
70
|
+
The exact selected set of a declared view; persisted in Atelier state.
|
|
71
|
+
proposals list | show OPERATION Structural edits routed as copy-only proposals. Read-only.
|
|
72
|
+
\`status\` lists the operations this command registered under "operations".
|
|
73
|
+
|
|
74
|
+
Opening outcomes: ${Object.keys(OPENING_OUTCOMES).join(', ')}.
|
|
75
|
+
Pending edits additionally report ${APPLY_UNAVAILABLE} while no apply operation is registered.
|
|
76
|
+
Minimum Obsidian version: ${MINIMUM_APP_VERSION}.
|
|
77
|
+
Exit codes: 0 done; 1 internal error; 2 refusal or usage; 3 ran, and the answer is not success.`
|
|
78
|
+
|
|
79
|
+
const FLAGS = Object.freeze({ json: 'flag', 'allow-stale': 'flag', print: 'flag', help: 'flag', project: 'value', 'project-config': 'value', 'data-root': 'value', scope: 'value', 'consent-actor': 'value', actor: 'value', adapter: 'value', 'wait-ms': 'value' })
|
|
80
|
+
|
|
81
|
+
function parse(argv) {
|
|
82
|
+
const positionals = []
|
|
83
|
+
const flags = {}
|
|
84
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
85
|
+
const arg = argv[index]
|
|
86
|
+
if (arg === '-h') { flags.help = true; continue }
|
|
87
|
+
if (!arg.startsWith('--')) { positionals.push(arg); continue }
|
|
88
|
+
const [name, inline] = arg.slice(2).split(/=(.*)/s, 2)
|
|
89
|
+
if (!Object.hasOwn(FLAGS, name)) refuse('usage', `unknown option --${name}`)
|
|
90
|
+
if (FLAGS[name] === 'flag') { if (inline !== undefined) refuse('usage', `--${name} takes no value`); flags[name] = true; continue }
|
|
91
|
+
const value = inline ?? argv[index += 1]
|
|
92
|
+
if (value === undefined || value === '' || (inline === undefined && value.startsWith('--'))) refuse('usage', `--${name} needs a value`)
|
|
93
|
+
flags[name] = value
|
|
94
|
+
}
|
|
95
|
+
return { positionals, flags }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const isTyped = (error) => error instanceof ObsidianMaintenanceRefusal || error instanceof AtelierDiagnosticError || error instanceof ObsidianContractRefusal
|
|
99
|
+
const plainMessage = (error) => String(error.message ?? '').replace(new RegExp(`^${error.code}: `), '')
|
|
100
|
+
const NEXT = Object.freeze({
|
|
101
|
+
usage: 'run `atelier obsidian --help`',
|
|
102
|
+
'seams-required': 'pass the seams, or run the real command-line entry',
|
|
103
|
+
'app-adapter-not-selected': `pass --adapter=${PRODUCTION_ADAPTER} to let this command reach the installed app`,
|
|
104
|
+
'startup-consent-required': 'pass --consent-actor ID to record who allows the maintenance service to run',
|
|
105
|
+
'automatic-mode-refused': 'install an active automatic policy with `obsidian policy install FILE`',
|
|
106
|
+
[APPLY_UNAVAILABLE]: 'no apply operation is registered on this command; edits stay preserved and queued',
|
|
107
|
+
'policy-digest-mismatch': 'set the "digest" member of the file to the expected digest (`obsidian policy digest FILE` prints it), then install again',
|
|
108
|
+
disabled: 'declare the Obsidian settings in the project configuration',
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// The decisions the command's oracles are sensitive to are those of opening and of the lifecycle; tests substitute
|
|
112
|
+
// broken ones here to prove the oracles can fail. `runObsidianCommand` always uses the production ones.
|
|
113
|
+
export async function runObsidianCommandForOracleTests(options = {}, rules = {}) {
|
|
114
|
+
const openingRules = rules.opening ?? OPENING_PRIMITIVES
|
|
115
|
+
const lifecycleRules = rules.lifecycle ?? LIFECYCLE_PRIMITIVES
|
|
116
|
+
const {
|
|
117
|
+
argv = [], seams = null, production = false, env = process.env, cwd = process.cwd(), platform = process.platform, clock = () => new Date(),
|
|
118
|
+
stdout = (text) => process.stdout.write(`${text}\n`), stderr = (text) => process.stderr.write(`${text}\n`), contributions = null, contributionsDirectory,
|
|
119
|
+
} = options
|
|
120
|
+
let json = argv.includes('--json')
|
|
121
|
+
let operationName = null
|
|
122
|
+
const emit = ({ exit, document, human }) => {
|
|
123
|
+
const complete = { schema: COMMAND_SCHEMA, ok: exit === EXIT.ok, operation: operationName, ...document }
|
|
124
|
+
if (json) stdout(JSON.stringify(complete, null, 2))
|
|
125
|
+
else for (const line of human ?? [JSON.stringify(complete, null, 2)]) (exit === EXIT.ok ? stdout : stderr)(line)
|
|
126
|
+
return exit
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const { positionals, flags } = parse(argv)
|
|
130
|
+
json = flags.json === true
|
|
131
|
+
operationName = positionals[0] ?? null
|
|
132
|
+
if (flags.help === true || operationName === null || operationName === 'help') { operationName = 'help'; return emit({ exit: EXIT.ok, document: { usage: USAGE }, human: [USAGE] }) }
|
|
133
|
+
// Nothing runs without a decision about the seams: a caller that forgot them never falls through to a real app.
|
|
134
|
+
if (seams === null && production !== true) refuse('seams-required', 'no seams were passed and this is not the real command-line entry')
|
|
135
|
+
|
|
136
|
+
const dataRoot = flags['data-root'] === undefined ? options.dataRoot : path.resolve(cwd, flags['data-root'])
|
|
137
|
+
const configFlag = flags.project ?? flags['project-config']
|
|
138
|
+
const loadProject = options.loadProject ?? (() => resolveProjectConfig({ argv: configFlag === undefined ? [] : [`--project=${path.resolve(cwd, configFlag)}`], cwd, env, writeLocalState: false }))
|
|
139
|
+
const registry = createObsidianRegistry({ reservedOperations: BUILT_IN_OPERATIONS, contributions: contributions ?? await loadContributions(contributionsDirectory === undefined ? {} : { directory: contributionsDirectory }) })
|
|
140
|
+
const applyAvailable = registry.extensions.applyOperation() !== UNAVAILABLE_APPLY_OPERATION
|
|
141
|
+
const lifecycle = { loadProject, dataRoot, env, platform, ...(options.probeTimeoutMs === undefined ? {} : { probeTimeoutMs: options.probeTimeoutMs }) }
|
|
142
|
+
|
|
143
|
+
const serviceSeam = async () => {
|
|
144
|
+
if (seams !== null) { if (typeof seams.service?.entryPath !== 'string') refuse('seams-required', 'the service seam names the entry this command may start'); return seams.service }
|
|
145
|
+
if (flags.adapter !== PRODUCTION_ADAPTER) refuse('app-adapter-not-selected', 'no editor adapter was selected; a service that reaches the installed app is never a default')
|
|
146
|
+
const { SERVICE_ENTRY_PATH } = await import('../runtime/obsidian/service-main.mjs')
|
|
147
|
+
return { entryPath: SERVICE_ENTRY_PATH, entryArgs: [`--adapter=${PRODUCTION_ADAPTER}`] }
|
|
148
|
+
}
|
|
149
|
+
const appSeams = async () => {
|
|
150
|
+
if (seams !== null) return { appProbe: seams.appProbe, launcher: seams.launcher }
|
|
151
|
+
if (flags.adapter !== PRODUCTION_ADAPTER) refuse('app-adapter-not-selected', 'no editor adapter was selected; reaching the installed app is never a default')
|
|
152
|
+
const { createProductionAppSeams } = await import('../runtime/obsidian/app-production-seams.mjs')
|
|
153
|
+
return createProductionAppSeams({ env, platform })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const configured = () => {
|
|
157
|
+
const project = loadProject()
|
|
158
|
+
const enablement = readObsidianEnablement(project)
|
|
159
|
+
if (enablement.reason === 'not-configured') refuse('disabled', 'this project declares no Obsidian settings')
|
|
160
|
+
return { project, enablement }
|
|
161
|
+
}
|
|
162
|
+
// Private state of this workspace, created on demand for an operation that writes it.
|
|
163
|
+
const writable = () => {
|
|
164
|
+
const { project, enablement } = configured()
|
|
165
|
+
ensureWorkspaceIdentity({ project, ...(dataRoot === undefined ? {} : { dataRoot }) })
|
|
166
|
+
const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform, create: true })
|
|
167
|
+
return { project, enablement, workspace, repositoryRoots: protectedRoots(project), now: isoTime(clock) }
|
|
168
|
+
}
|
|
169
|
+
const readable = () => {
|
|
170
|
+
const project = loadProject()
|
|
171
|
+
const enablement = readObsidianEnablement(project)
|
|
172
|
+
const workspace = enablement.reason === 'not-configured' ? null : resolveServiceWorkspace({ project, dataRoot, env, platform })
|
|
173
|
+
return { project, enablement, workspace: workspace?.workspaceRoot ? workspace : null, workspaceId: workspace?.workspaceId ?? null }
|
|
174
|
+
}
|
|
175
|
+
const machineOf = (workspace) => (workspace === null ? null : readMachineSettings(workspace))
|
|
176
|
+
const consent = flags['consent-actor'] === undefined ? undefined : { actor: flags['consent-actor'], coverage: 'service' }
|
|
177
|
+
const shownMachine = (machine, workspace) => {
|
|
178
|
+
const settings = machine ?? { maintenanceMode: 'manual', audienceAllow: [], applyPolicy: null }
|
|
179
|
+
const automatic = workspace === null ? { authorized: false, reason: 'machine-settings-absent' } : authorizeAutomaticApply(workspace)
|
|
180
|
+
return { maintenanceMode: settings.maintenanceMode, audienceAllow: settings.audienceAllow, applyPolicy: settings.applyPolicy, automaticApply: { authorized: automatic.authorized, reason: automatic.reason } }
|
|
181
|
+
}
|
|
182
|
+
const applyShown = { available: applyAvailable, state: applyAvailable ? 'available' : APPLY_UNAVAILABLE, operationId: registry.extensions.applyOperation().id }
|
|
183
|
+
const [, sub, value] = positionals
|
|
184
|
+
|
|
185
|
+
const operations = {
|
|
186
|
+
async status() {
|
|
187
|
+
const { project, enablement, workspace, workspaceId } = readable()
|
|
188
|
+
const service = enablement.reason === 'not-configured' ? { state: 'stopped', reason: 'not-configured' } : await serviceStatus(lifecycle, lifecycleRules)
|
|
189
|
+
const running = service.state === 'healthy' ? (await readServiceStatusDocument(lifecycle, lifecycleRules)).document : null
|
|
190
|
+
const scopes = workspace === null
|
|
191
|
+
? enablement.scopes.map(({ scopeId }) => ({ scopeId, outcome: enablement.state === 'disabled' ? 'disabled' : 'not-prepared', reason: enablement.state === 'disabled' ? enablement.reason : 'workspace-not-prepared' }))
|
|
192
|
+
: enablement.scopes.map(({ scopeId }) => {
|
|
193
|
+
const { vaultRoot: _vault, summary: _summary, ...report } = scopeReport({ workspace, scopeId, repositoryRoots: protectedRoots(project), serviceState: service.state, applyAvailable }, openingRules)
|
|
194
|
+
return enablement.state === 'disabled' ? { ...report, outcome: 'disabled', reason: enablement.reason, next: OPENING_OUTCOMES.disabled.next } : report
|
|
195
|
+
})
|
|
196
|
+
const document = {
|
|
197
|
+
enablement: { state: enablement.state, reason: enablement.reason, defaultScopeId: enablement.defaultScopeId }, workspace: { workspaceId, prepared: workspace !== null },
|
|
198
|
+
machine: shownMachine(machineOf(workspace), workspace), apply: applyShown,
|
|
199
|
+
service: { state: service.state, reason: service.reason ?? null, address: service.address ?? null, runtimeId: service.record?.runtimeId ?? null, pid: service.record?.pid ?? null, lastTick: running?.lastTick ?? null, lastError: running?.lastError ?? null, app: running?.app ?? null },
|
|
200
|
+
app: { probed: false, minimumVersion: MINIMUM_APP_VERSION }, scopes, extensions: registry.extensions.describe(), operations: registry.operations.describe(),
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
exit: EXIT.ok, document,
|
|
204
|
+
human: [
|
|
205
|
+
`obsidian: ${enablement.state} (${enablement.reason}); mode ${document.machine.maintenanceMode}; audiences ${document.machine.audienceAllow.join(', ') || 'none'}`,
|
|
206
|
+
`service: ${service.state} (${service.reason ?? 'no reason'})${running?.app ? `; app ${running.app.outcome}` : ''}`,
|
|
207
|
+
`apply: ${applyShown.state}`,
|
|
208
|
+
...scopes.map((scope) => `view ${scope.scopeId}: ${scope.outcome} (${scope.reason})${scope.pendingEdits?.open ? `; ${scope.pendingEdits.open} pending edit(s), apply ${scope.pendingEdits.apply}` : ''}`),
|
|
209
|
+
],
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
async scope() {
|
|
214
|
+
const { enablement } = readable()
|
|
215
|
+
if (sub === 'list' || sub === undefined) return { exit: EXIT.ok, document: { defaultScopeId: enablement.defaultScopeId, scopes: enablement.scopes }, human: enablement.scopes.map((scope) => `${scope.scopeId}\t${scope.mode}${scope.scopeId === enablement.defaultScopeId ? '\tdefault' : ''}`) }
|
|
216
|
+
if (sub !== 'show' || value === undefined) refuse('usage', 'scope list | scope show ID')
|
|
217
|
+
const scope = enablement.scopes.find((item) => item.scopeId === resolveScope(enablement, value))
|
|
218
|
+
return { exit: EXIT.ok, document: { scope }, human: [JSON.stringify(scope, null, 2)] }
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
async audience() {
|
|
222
|
+
if (sub === 'show' || sub === undefined) {
|
|
223
|
+
const { workspace } = readable()
|
|
224
|
+
const { audienceAllow } = shownMachine(machineOf(workspace), workspace)
|
|
225
|
+
return { exit: EXIT.ok, document: { audienceAllow }, human: [audienceAllow.join(', ') || 'no audience is allowed: every view is empty'] }
|
|
226
|
+
}
|
|
227
|
+
if (sub !== 'set' && sub !== 'clear') refuse('usage', 'audience show | audience set A,B | audience clear')
|
|
228
|
+
const audienceAllow = sub === 'clear' ? [] : String(value ?? '').split(',').map((item) => item.trim()).filter((item) => item !== '')
|
|
229
|
+
if (sub === 'set' && audienceAllow.length === 0) refuse('usage', 'audience set needs at least one audience; `audience clear` allows none')
|
|
230
|
+
if (audienceAllow.some((item) => !AUDIENCE.test(item)) || new Set(audienceAllow).size !== audienceAllow.length || audienceAllow.length > 64) refuse('invalid-audience', 'audiences are distinct identifiers, at most 64')
|
|
231
|
+
const { workspace, repositoryRoots, now } = writable()
|
|
232
|
+
const current = machineOf(workspace) ?? defaultMachineSettings({ workspaceId: workspace.workspaceId, updatedAt: now })
|
|
233
|
+
const changed = JSON.stringify(current.audienceAllow) !== JSON.stringify(audienceAllow)
|
|
234
|
+
writeMachineSettings({ ...workspace, repositoryRoots, settings: { ...current, audienceAllow, updatedAt: now } })
|
|
235
|
+
// The engine compares a digest of these on every tick: a change invalidates every view at the next one.
|
|
236
|
+
return { exit: EXIT.ok, document: { audienceAllow, changed, takesEffect: 'next-tick' }, human: [`audiences: ${audienceAllow.join(', ') || 'none'}; views are rebuilt at the next tick`] }
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
async mode() {
|
|
240
|
+
if (sub === 'show' || sub === undefined) {
|
|
241
|
+
const { workspace } = readable()
|
|
242
|
+
const { maintenanceMode, automaticApply } = shownMachine(machineOf(workspace), workspace)
|
|
243
|
+
return { exit: EXIT.ok, document: { maintenanceMode, automaticApply, apply: applyShown }, human: [`${maintenanceMode}; automatic apply ${automaticApply.authorized ? 'authorized' : `not authorized (${automaticApply.reason})`}; apply ${applyShown.state}`] }
|
|
244
|
+
}
|
|
245
|
+
if (sub !== 'set' || !['manual', 'automatic'].includes(value)) refuse('usage', 'mode show | mode set manual|automatic')
|
|
246
|
+
const { workspace, repositoryRoots, now } = writable()
|
|
247
|
+
const current = machineOf(workspace) ?? writeMachineSettings({ ...workspace, repositoryRoots, settings: defaultMachineSettings({ workspaceId: workspace.workspaceId, updatedAt: now }) })
|
|
248
|
+
if (value === 'automatic') {
|
|
249
|
+
const would = authorizeAutomaticApply({ ...workspace, assumeAutomatic: true })
|
|
250
|
+
if (!would.authorized) refuse('automatic-mode-refused', 'automatic mode needs an installed, matching, active automatic policy; nothing was changed', { reason: would.reason })
|
|
251
|
+
}
|
|
252
|
+
writeMachineSettings({ ...workspace, repositoryRoots, settings: { ...current, maintenanceMode: value, updatedAt: now } })
|
|
253
|
+
return { exit: EXIT.ok, document: { maintenanceMode: value, apply: applyShown }, human: [`mode: ${value}${value === 'automatic' && !applyAvailable ? `; every apply reports ${APPLY_UNAVAILABLE} while no apply operation is registered` : ''}`] }
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
async policy() {
|
|
257
|
+
if (sub === 'show' || sub === undefined) {
|
|
258
|
+
const { workspace } = readable()
|
|
259
|
+
const policy = workspace === null ? null : readInstalledApplyPolicy(workspace)
|
|
260
|
+
const { applyPolicy, automaticApply } = shownMachine(machineOf(workspace), workspace)
|
|
261
|
+
return { exit: EXIT.ok, document: { installed: policy !== null, reference: applyPolicy, policy, automaticApply }, human: [policy === null ? 'no apply policy is installed' : `${policy.policyId} v${policy.version} ${policy.mode} ${policy.status}; automatic apply ${automaticApply.authorized ? 'authorized' : `not authorized (${automaticApply.reason})`}`] }
|
|
262
|
+
}
|
|
263
|
+
const readPolicyFile = () => {
|
|
264
|
+
if (value === undefined) refuse('usage', `policy ${sub} FILE`)
|
|
265
|
+
const file = path.resolve(cwd, value)
|
|
266
|
+
try {
|
|
267
|
+
if (fs.statSync(file).size > MAX_POLICY_BYTES) refuse('invalid-apply-policy', 'the policy file is larger than a policy can be')
|
|
268
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error instanceof ObsidianMaintenanceRefusal) throw error
|
|
271
|
+
return refuse('invalid-apply-policy', 'the policy file cannot be read as JSON', { cause: error.code ?? 'not-json' })
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (sub === 'digest') {
|
|
275
|
+
// Read-only: the digest of the canonical form of FILE, for a person or an agent to fill in. Nothing is installed.
|
|
276
|
+
const policy = readPolicyFile()
|
|
277
|
+
if (policy === null || typeof policy !== 'object' || Array.isArray(policy)) refuse('invalid-apply-policy', 'a policy is a JSON object')
|
|
278
|
+
const digest = applyPolicyDigest(policy)
|
|
279
|
+
const carried = typeof policy.digest === 'string' ? policy.digest : null
|
|
280
|
+
return { exit: EXIT.ok, document: { digest, carried, matches: carried === digest }, human: [digest, carried === digest ? 'the file carries this digest' : 'set the "digest" member of the file to this value, then run `policy install FILE`'] }
|
|
281
|
+
}
|
|
282
|
+
if (sub === 'install') {
|
|
283
|
+
const policy = readPolicyFile()
|
|
284
|
+
const { workspace, repositoryRoots, now } = writable()
|
|
285
|
+
// Validated against the frozen contract, its digest verified, and stored owner-only beside the machine settings, outside every repository and vault.
|
|
286
|
+
installApplyPolicy({ ...workspace, policy, repositoryRoots, updatedAt: now, digestOf: applyPolicyDigest })
|
|
287
|
+
return { exit: EXIT.ok, document: { installed: true, reference: { policyId: policy.policyId, version: policy.version, digest: policy.digest }, status: policy.status, mode: policy.mode }, human: [`installed ${policy.policyId} v${policy.version} (${policy.mode}, ${policy.status}); the maintenance mode is unchanged`] }
|
|
288
|
+
}
|
|
289
|
+
if (sub !== 'revoke') refuse('usage', 'policy show | policy install FILE | policy digest FILE | policy revoke')
|
|
290
|
+
const { workspace, repositoryRoots, now } = writable()
|
|
291
|
+
const result = revokeApplyPolicy({ ...workspace, repositoryRoots, updatedAt: now })
|
|
292
|
+
return { exit: EXIT.ok, document: { revoked: result.revoked, reason: result.reason, maintenanceMode: 'manual' }, human: [result.revoked ? 'revoked; no queued edit is applied from now on, and the mode is manual' : 'no apply policy was installed'] }
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
async service() {
|
|
296
|
+
const shown = ({ child: _child, ...result }) => result
|
|
297
|
+
if (sub === 'status' || sub === undefined) {
|
|
298
|
+
const status = await serviceStatus(lifecycle, lifecycleRules)
|
|
299
|
+
return { exit: EXIT.ok, document: { service: status }, human: [`${status.state} (${status.reason})`] }
|
|
300
|
+
}
|
|
301
|
+
if (sub === 'start') {
|
|
302
|
+
configured()
|
|
303
|
+
const seam = await serviceSeam()
|
|
304
|
+
const result = shown(await startService({ ...lifecycle, detached: true, ...seam, ...(consent === undefined ? {} : { consent }) }, lifecycleRules))
|
|
305
|
+
const running = result.state === 'healthy' || result.state === 'busy'
|
|
306
|
+
return { exit: running ? EXIT.ok : EXIT.notSuccess, document: { service: result }, human: [result.state === 'busy' ? 'running, and busy in a long tick; nothing was started beside it' : `${result.state}${result.started ? ' (started)' : result.alreadyRunning ? ' (already running)' : ` (${result.reason})`}`] }
|
|
307
|
+
}
|
|
308
|
+
if (sub === 'stop') {
|
|
309
|
+
const result = await stopService(lifecycle, lifecycleRules)
|
|
310
|
+
return { exit: result.refused ? EXIT.notSuccess : EXIT.ok, document: { service: result }, human: [result.refused ? `not stopped: ${result.state} (${result.reason})${result.retry ? '; ask again in a moment' : ''}` : result.stopped ? 'stopped' : `nothing to stop (${result.reason})`] }
|
|
311
|
+
}
|
|
312
|
+
if (sub !== 'unit' || flags.print !== true) refuse('usage', 'service start | status | stop | unit --print (installing a startup unit is not offered here)')
|
|
313
|
+
const { project, workspace } = readable()
|
|
314
|
+
if (workspace === null) refuse('service-workspace-not-prepared', 'this workspace has no private state yet; `service start` prepares it')
|
|
315
|
+
const seam = await serviceSeam()
|
|
316
|
+
// Pure text from the builder. Nothing is written, installed or handed to a service manager.
|
|
317
|
+
const unit = buildStartupAdapter({
|
|
318
|
+
platform, label: serviceNameFor(workspace.workspaceId), nodePath: process.execPath, entryPath: seam.entryPath, logPath: servicePaths(workspace.workspaceRoot).log,
|
|
319
|
+
args: [`--project=${project.configPath}`, ...(dataRoot === undefined ? [] : [`--data-root=${dataRoot}`]), ...(seam.entryArgs ?? [])],
|
|
320
|
+
})
|
|
321
|
+
return { exit: EXIT.ok, document: { unit, installed: false, note: 'running this unit needs a recorded consent that covers operating-system startup' }, human: [unit.text] }
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
async open() {
|
|
325
|
+
const seam = await serviceSeam()
|
|
326
|
+
const result = await openScopeForOracleTests({
|
|
327
|
+
...lifecycle, ...(await appSeams()), service: seam, scopeId: flags.scope, consent, allowStale: flags['allow-stale'] === true, extensions: registry.extensions,
|
|
328
|
+
...(flags['wait-ms'] === undefined ? {} : { tickTimeoutMs: Number(flags['wait-ms']) || undefined }), ...(options.open ?? {}),
|
|
329
|
+
}, openingRules, lifecycleRules)
|
|
330
|
+
const { ok: _ok, ...document } = result
|
|
331
|
+
return { exit: result.ok ? EXIT.ok : EXIT.notSuccess, document, human: [`${result.outcome}: ${result.summary}${result.reason ? ` (${result.reason})` : ''}`, `Next: ${result.next}`, ...(result.pendingEdits?.open ? [`${result.pendingEdits.open} pending edit(s); apply ${result.pendingEdits.apply}`] : [])] }
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
// The placeholder that answers when no contribution registered an apply operation; the shipped source-apply contribution replaces it.
|
|
335
|
+
async apply() {
|
|
336
|
+
return refuse(APPLY_UNAVAILABLE, 'no apply operation is registered; pending edits stay preserved and queued', { operationId: registry.extensions.applyOperation().id })
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const contributed = registry.operations.get(operationName)
|
|
341
|
+
if (contributed === null && !Object.hasOwn(operations, operationName)) refuse('usage', `unknown operation: ${String(operationName).slice(0, 40)}`)
|
|
342
|
+
// One option table serves every operation. No built-in operation has a use for an actor, and a contributed one takes
|
|
343
|
+
// it only when it declares so (`options: ['actor']`); the apply operation then decides which of its verbs takes it.
|
|
344
|
+
if (flags.actor !== undefined && !(Array.isArray(contributed?.options) && contributed.options.includes('actor'))) refuse('usage', '--actor belongs to `apply run`; this operation does not take it')
|
|
345
|
+
const result = contributed !== null
|
|
346
|
+
? await contributed.run({ args: positionals.slice(1), flags: { ...flags }, registry, loadProject, dataRoot, env, platform, clock, readable, writable })
|
|
347
|
+
: await operations[operationName]()
|
|
348
|
+
if (result === null || typeof result !== 'object' || !Number.isInteger(result.exit) || result.document === null || typeof result.document !== 'object') refuse('invalid-extension', 'an operation answers { exit, document, human }')
|
|
349
|
+
return emit(result)
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (!isTyped(error)) {
|
|
352
|
+
if (env.ATELIER_DEBUG === '1') stderr(error?.stack ?? String(error))
|
|
353
|
+
return emit({ exit: EXIT.error, document: { error: { code: 'internal-error', message: 'the command failed without a typed reason', next: 'rerun with ATELIER_DEBUG=1 to see the stack locally' } }, human: ['[internal-error] the command failed without a typed reason', 'Next: rerun with ATELIER_DEBUG=1 to see the stack locally'] })
|
|
354
|
+
}
|
|
355
|
+
const next = NEXT[error.code] ?? error.hint ?? 'see `atelier obsidian status`'
|
|
356
|
+
return emit({ exit: EXIT.refused, document: { error: { code: error.code, message: plainMessage(error), next, detail: error.detail ?? {} } }, human: [`[${error.code}] ${plainMessage(error)}`, `Next: ${next}`] })
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Production entry point: the production decisions, always.
|
|
361
|
+
export function runObsidianCommand(options = {}) {
|
|
362
|
+
return runObsidianCommandForOracleTests(options, {})
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export default runObsidianCommand
|
|
366
|
+
|
|
367
|
+
const invokedDirectly = (() => { try { return Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)) } catch { return false } })()
|
|
368
|
+
if (invokedDirectly) process.exitCode = await runObsidianCommand({ argv: process.argv.slice(2), production: true })
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { parseProjectOptions } from '../cli/project-options.mjs'
|
|
4
|
+
import { commandProject, parseArgs, readJson } from '../project/config.mjs'
|
|
5
|
+
import {
|
|
6
|
+
loadExtensionPacks,
|
|
7
|
+
createProtocolRegistry,
|
|
8
|
+
} from '../extension-packs/loader.mjs'
|
|
9
|
+
import { runProtocol } from '../readiness-protocols/runtime.mjs'
|
|
10
|
+
import { recordReviewEvidence } from '../readiness-protocols/evidence.mjs'
|
|
11
|
+
import { createReviewStore } from '../collaboration/review-store.mjs'
|
|
12
|
+
import {
|
|
13
|
+
inspectPackLifecycle,
|
|
14
|
+
planPackLifecycleMigration,
|
|
15
|
+
} from '../extension-packs/lifecycle.mjs'
|
|
16
|
+
import {
|
|
17
|
+
prepareInspectionBundle,
|
|
18
|
+
writeInspectionBundle,
|
|
19
|
+
readInspectionBundle,
|
|
20
|
+
} from '../collaboration/inspection-bundle.mjs'
|
|
21
|
+
|
|
22
|
+
const argv = process.argv.slice(2)
|
|
23
|
+
const remaining = parseProjectOptions(argv).remaining
|
|
24
|
+
const subcommand = remaining[0]
|
|
25
|
+
const specs = {
|
|
26
|
+
run: { answers: 'value' },
|
|
27
|
+
history: {},
|
|
28
|
+
handoff: {},
|
|
29
|
+
packs: { 'migration-plan': 'flag' },
|
|
30
|
+
export: {
|
|
31
|
+
runs: 'value',
|
|
32
|
+
responses: 'value',
|
|
33
|
+
denylist: 'value',
|
|
34
|
+
write: 'flag',
|
|
35
|
+
out: 'value',
|
|
36
|
+
},
|
|
37
|
+
inspect: {},
|
|
38
|
+
}
|
|
39
|
+
if (!Object.hasOwn(specs, subcommand ?? ''))
|
|
40
|
+
throw new Error(
|
|
41
|
+
'Usage: atelier review run|history|handoff|packs|export|inspect',
|
|
42
|
+
)
|
|
43
|
+
const positional = []
|
|
44
|
+
for (let i = 1; i < remaining.length; i++) {
|
|
45
|
+
const arg = remaining[i]
|
|
46
|
+
if (!arg.startsWith('--')) {
|
|
47
|
+
positional.push(arg)
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
const name = arg.slice(2).split('=')[0],
|
|
51
|
+
kind = specs[subcommand][name]
|
|
52
|
+
if (!Object.hasOwn(specs[subcommand], name))
|
|
53
|
+
throw new Error('unknown review option')
|
|
54
|
+
if (kind === 'flag') {
|
|
55
|
+
if (arg.includes('='))
|
|
56
|
+
throw new Error('review flag does not accept a value')
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
const value = arg.includes('=')
|
|
60
|
+
? arg.slice(arg.indexOf('=') + 1)
|
|
61
|
+
: remaining[++i]
|
|
62
|
+
if (!value || value.startsWith('--'))
|
|
63
|
+
throw new Error('review option requires a value')
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
positional.length !==
|
|
67
|
+
(['run', 'handoff', 'inspect'].includes(subcommand) ? 1 : 0)
|
|
68
|
+
)
|
|
69
|
+
throw new Error('incorrect review arguments')
|
|
70
|
+
const args = parseArgs(argv)
|
|
71
|
+
const project = args._[0] === 'inspect' ? null : commandProject({ argv })
|
|
72
|
+
let result
|
|
73
|
+
if (args._[0] === 'run') {
|
|
74
|
+
const registry = createProtocolRegistry({
|
|
75
|
+
packs: loadExtensionPacks(project).packs,
|
|
76
|
+
})
|
|
77
|
+
const answers = args.answers ? readJson(path.resolve(args.answers)) : {}
|
|
78
|
+
const { run } = runProtocol(project, args._[1], {
|
|
79
|
+
answers,
|
|
80
|
+
write: false,
|
|
81
|
+
createProposal: false,
|
|
82
|
+
registry,
|
|
83
|
+
})
|
|
84
|
+
const bound = recordReviewEvidence(project, run)
|
|
85
|
+
result = {
|
|
86
|
+
ok: true,
|
|
87
|
+
runId: run.runId,
|
|
88
|
+
evidenceDigest: bound.snapshot.digest,
|
|
89
|
+
inputCompleteness: run.score,
|
|
90
|
+
blockers: run.blockers,
|
|
91
|
+
confidence: 'not-calibrated',
|
|
92
|
+
next: 'Run atelier dev --review with this project, then open /review.',
|
|
93
|
+
}
|
|
94
|
+
} else if (args._[0] === 'inspect')
|
|
95
|
+
result = readInspectionBundle(path.resolve(args._[1]))
|
|
96
|
+
else if (args._[0] === 'export') {
|
|
97
|
+
const denylistDocument = process.env.ATELIER_DENYLIST_JSON
|
|
98
|
+
? JSON.parse(process.env.ATELIER_DENYLIST_JSON)
|
|
99
|
+
: args.denylist
|
|
100
|
+
? readJson(path.resolve(args.denylist))
|
|
101
|
+
: null
|
|
102
|
+
const bundle = prepareInspectionBundle(
|
|
103
|
+
project,
|
|
104
|
+
String(args.runs ?? '')
|
|
105
|
+
.split(',')
|
|
106
|
+
.filter(Boolean),
|
|
107
|
+
{
|
|
108
|
+
denylistDocument,
|
|
109
|
+
responseIds: String(args.responses ?? '')
|
|
110
|
+
.split(',')
|
|
111
|
+
.filter(Boolean),
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
if (args.write) {
|
|
115
|
+
if (!args.out) throw new Error('--out is required for --write')
|
|
116
|
+
writeInspectionBundle(path.resolve(args.out), bundle)
|
|
117
|
+
}
|
|
118
|
+
result = {
|
|
119
|
+
ok: true,
|
|
120
|
+
purpose: bundle.purpose,
|
|
121
|
+
written: Boolean(args.write),
|
|
122
|
+
digest: bundle.digest,
|
|
123
|
+
members: bundle.members.map((member) => ({
|
|
124
|
+
id: member.id,
|
|
125
|
+
bytes: member.bytes,
|
|
126
|
+
digest: member.digest,
|
|
127
|
+
})),
|
|
128
|
+
responses: bundle.responses.map((record) => ({
|
|
129
|
+
id: record.input.requestId,
|
|
130
|
+
digest: record.inputDigest,
|
|
131
|
+
})),
|
|
132
|
+
notice:
|
|
133
|
+
'Inspection only; no approval transfer. Export requires --write --out FILE.',
|
|
134
|
+
}
|
|
135
|
+
} else if (args._[0] === 'packs')
|
|
136
|
+
result = (
|
|
137
|
+
args['migration-plan'] ? planPackLifecycleMigration : inspectPackLifecycle
|
|
138
|
+
)(project, loadExtensionPacks(project).packs)
|
|
139
|
+
else if (args._[0] === 'handoff')
|
|
140
|
+
result = createReviewStore(project).handoff(args._[1])
|
|
141
|
+
else if (args._[0] === 'history') result = createReviewStore(project).records()
|
|
142
|
+
else
|
|
143
|
+
throw new Error(
|
|
144
|
+
'Usage: atelier review run PROTOCOL --answers FILE | history | handoff REQUEST_ID | packs | export --runs ID [--responses ID] [--write --out FILE] | inspect FILE [--project PATH]',
|
|
145
|
+
)
|
|
146
|
+
console.log(JSON.stringify(result, null, 2))
|
|
147
|
+
process.exitCode = result.ok ? 0 : 1
|
package/src/commands/setup.mjs
CHANGED
|
@@ -9,8 +9,11 @@ import {
|
|
|
9
9
|
firstString,
|
|
10
10
|
parseArgs,
|
|
11
11
|
resolveProjectConfig,
|
|
12
|
+
validateProjectConfigDoc,
|
|
12
13
|
writeJson,
|
|
13
14
|
} from '../project/config.mjs'
|
|
15
|
+
import { writeAtelierLock, loadAtelierLock, checkAtelierLock } from '../upgrade/upgrade.mjs'
|
|
16
|
+
import { loadBoundaryPolicy, validateBoundaryPolicy } from '../boundary/policy.mjs'
|
|
14
17
|
import { auditRepoIdentities } from '../project/repo-identity.mjs'
|
|
15
18
|
|
|
16
19
|
const PROFILE_SET = new Set(['single-repo', 'private-domain', 'shared-project', 'multi-repo', 'monorepo', 'control-workspace'])
|
|
@@ -121,8 +124,10 @@ function runSetup(argv) {
|
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
function baseBoundaryPolicy({ repoName, profile, actor }) {
|
|
124
|
-
const privateDomain = profile === 'shared-project' ? `${actor}-private` : repoName
|
|
125
127
|
const shared = profile === 'shared-project'
|
|
128
|
+
const privateDomain = shared
|
|
129
|
+
? (repoName === `${actor}-private` ? `${actor}-private-domain` : `${actor}-private`)
|
|
130
|
+
: repoName
|
|
126
131
|
return {
|
|
127
132
|
schema: 'mnstry.atelier-boundary-policy@v1',
|
|
128
133
|
mode: 'strict',
|
|
@@ -134,6 +139,11 @@ function baseBoundaryPolicy({ repoName, profile, actor }) {
|
|
|
134
139
|
},
|
|
135
140
|
},
|
|
136
141
|
repos: {
|
|
142
|
+
...(shared ? { [privateDomain]: {
|
|
143
|
+
kind: 'private_domain', ownerActor: actor, readBoundary: 'private',
|
|
144
|
+
allowedAudiences: ['private', 'sensitive', 'team', 'operator', 'staff', 'public'],
|
|
145
|
+
forbiddenAudiences: [], autoCommit: 'guarded',
|
|
146
|
+
} } : {}),
|
|
137
147
|
[repoName]: {
|
|
138
148
|
kind: shared ? 'shared' : 'private_domain',
|
|
139
149
|
ownerActor: shared ? undefined : actor,
|
|
@@ -178,23 +188,42 @@ function runAdopt(argv) {
|
|
|
178
188
|
if (profile === 'monorepo' && !firstString(args.include, args.includes)) {
|
|
179
189
|
throw new Error('monorepo adopt requires --include so Atelier does not scan the whole repo by accident')
|
|
180
190
|
}
|
|
181
|
-
fs.mkdirSync(target, { recursive: true })
|
|
182
|
-
appendIgnoreLines(target)
|
|
183
191
|
const repoName = slug(firstString(args.name) || path.basename(target))
|
|
184
192
|
const actor = slug(firstString(args.actor) || process.env.USER || 'owner')
|
|
185
193
|
const projectPath = path.join(target, 'atelier.project.json')
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
194
|
+
const existingProject = fs.existsSync(projectPath)
|
|
195
|
+
? resolveProjectConfig({ cwd: target, argv: ['--project', projectPath], writeLocalState: false }) : null
|
|
196
|
+
const proposedConfig = existingProject?.config || {
|
|
197
|
+
schema: 'mnstry.atelier-project-config@v1',
|
|
198
|
+
name: repoName,
|
|
199
|
+
roots: { workspace: '.', repoOps: '.' },
|
|
200
|
+
graph: { repoAccessPath: 'repo-access.v1.json', outputPath: 'atelier-output/knowledge.graph.json' },
|
|
201
|
+
projection: { outputRoot: 'atelier-output', readinessPath: 'atelier-output/atelier-readiness.json' },
|
|
202
|
+
boundaries: { policyPath: 'boundary-policy.v1.json', governanceLedgerPath: 'governance/repo-boundary-ledger.md', strictNewRepos: true },
|
|
203
|
+
setup: { profile, include: firstString(args.include, args.includes) || null, exclude: firstString(args.exclude, args.excludes) || null },
|
|
204
|
+
repos: [{ name: repoName, path: '.', readBoundary: profile === 'shared-project' ? 'team' : 'private', role: profile }],
|
|
205
|
+
}
|
|
206
|
+
const configErrors = validateProjectConfigDoc(proposedConfig)
|
|
207
|
+
if (configErrors.length) throw new Error(`adopt requires a valid project config: ${configErrors.join('; ')}`)
|
|
208
|
+
const proposedProject = existingProject || { configDir: target, config: proposedConfig, repos: proposedConfig.repos }
|
|
209
|
+
// Parse and validate retained state before any scaffold or overlay writes.
|
|
210
|
+
const prior = loadAtelierLock(existingProject || { configDir: target })
|
|
211
|
+
if (fs.existsSync(prior.lockPath) && !prior.lock) throw new Error('existing lock must contain a valid lock object')
|
|
212
|
+
if (prior.lock && !existingProject) throw new Error('existing lock requires an existing project configuration')
|
|
213
|
+
const priorPolicy = loadBoundaryPolicy(proposedProject)
|
|
214
|
+
if (existingProject || fs.existsSync(priorPolicy.policyPath)) {
|
|
215
|
+
if (!priorPolicy.ok) throw new Error(`adopt requires a valid boundary policy: ${priorPolicy.errors.join('; ')}`)
|
|
197
216
|
}
|
|
217
|
+
const proposedPolicy = priorPolicy.ok ? priorPolicy.policy : cleanUndefined(baseBoundaryPolicy({ repoName, profile, actor }))
|
|
218
|
+
const proposedPolicyErrors = validateBoundaryPolicy(proposedPolicy, proposedProject)
|
|
219
|
+
if (proposedPolicyErrors.length) throw new Error(`adopt requires a valid boundary policy: ${proposedPolicyErrors.join('; ')}`)
|
|
220
|
+
if (prior.lock) {
|
|
221
|
+
const report = checkAtelierLock(existingProject)
|
|
222
|
+
if (!report.ok) throw new Error(`adopt cannot accept existing lock drift: ${report.errors.join('; ')}`)
|
|
223
|
+
}
|
|
224
|
+
fs.mkdirSync(target, { recursive: true })
|
|
225
|
+
appendIgnoreLines(target)
|
|
226
|
+
if (!existingProject) writeJson(projectPath, proposedConfig)
|
|
198
227
|
if (!fs.existsSync(path.join(target, 'repo-access.v1.json'))) {
|
|
199
228
|
writeJson(path.join(target, 'repo-access.v1.json'), {
|
|
200
229
|
schema: 'mnstry.atelier-repo-access@v1',
|
|
@@ -206,8 +235,16 @@ function runAdopt(argv) {
|
|
|
206
235
|
writeJson(path.join(target, 'boundary-policy.v1.json'), cleanUndefined(baseBoundaryPolicy({ repoName, profile, actor })))
|
|
207
236
|
}
|
|
208
237
|
const project = resolveProjectConfig({ cwd: target, argv: ['--project', projectPath] })
|
|
238
|
+
const loadedPolicy = loadBoundaryPolicy(project)
|
|
239
|
+
const policyErrors = loadedPolicy.ok ? validateBoundaryPolicy(loadedPolicy.policy, project) : loadedPolicy.errors
|
|
240
|
+
if (policyErrors.length) throw new Error(`adopt requires a valid boundary policy: ${policyErrors.join('; ')}`)
|
|
209
241
|
ensureLocalState(project, { write: true })
|
|
210
242
|
writeLocalOverlay(project)
|
|
243
|
+
// Adoption creates the first lock, but never accepts drift in an existing one.
|
|
244
|
+
const { lockPath } = loadAtelierLock(project)
|
|
245
|
+
if (!fs.existsSync(lockPath)) writeAtelierLock({ project, templateId: `adopt:${profile}` })
|
|
246
|
+
const lockReport = checkAtelierLock(project)
|
|
247
|
+
if (!lockReport.ok) throw new Error(`adopt lock check failed: ${lockReport.errors.join('; ')}`)
|
|
211
248
|
console.log(JSON.stringify({ ok: true, command: 'adopt', profile, target, projectPath }, null, 2))
|
|
212
249
|
}
|
|
213
250
|
|