@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,58 @@
|
|
|
1
|
+
// Shared project-location syntax. Command-specific arguments remain owned by
|
|
2
|
+
// their command; this parser does not make unknown options globally valid.
|
|
3
|
+
export const PROJECT_OPTION_SPEC = Object.freeze({
|
|
4
|
+
project: 'value',
|
|
5
|
+
'project-config': 'value',
|
|
6
|
+
'repo-path': 'value',
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
function invalid(message) {
|
|
10
|
+
const error = new Error(message)
|
|
11
|
+
error.code = 'project-option-invalid'
|
|
12
|
+
error.exitCode = 2
|
|
13
|
+
throw error
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function projectOptionAt(argv, index, configArgPrefix = '--project-config=') {
|
|
17
|
+
const arg = argv[index]
|
|
18
|
+
if (typeof arg !== 'string' || !arg.startsWith('--')) return null
|
|
19
|
+
const name = arg.slice(2).split('=')[0]
|
|
20
|
+
const custom = configArgPrefix !== '--project-config=' && arg.startsWith(configArgPrefix)
|
|
21
|
+
if (!custom && !Object.hasOwn(PROJECT_OPTION_SPEC, name)) return null
|
|
22
|
+
const inline = custom || arg.includes('=')
|
|
23
|
+
const value = custom ? arg.slice(configArgPrefix.length) : inline ? arg.slice(arg.indexOf('=') + 1) : argv[index + 1]
|
|
24
|
+
if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
|
|
25
|
+
invalid(`--${custom ? 'project-config' : name} requires a non-empty value`)
|
|
26
|
+
}
|
|
27
|
+
const option = custom ? 'project-config' : name
|
|
28
|
+
if (option === 'repo-path') {
|
|
29
|
+
const separator = value.indexOf('=')
|
|
30
|
+
if (separator < 1 || !value.slice(0, separator).trim() || !value.slice(separator + 1).trim()) {
|
|
31
|
+
invalid('--repo-path requires NAME=PATH')
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { name: option, value, nextIndex: index + (inline ? 1 : 2) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseProjectOptions(argv = [], configArgPrefix = '--project-config=') {
|
|
38
|
+
let project = null
|
|
39
|
+
const repoPaths = new Map()
|
|
40
|
+
const remaining = []
|
|
41
|
+
for (let index = 0; index < argv.length;) {
|
|
42
|
+
const option = projectOptionAt(argv, index, configArgPrefix)
|
|
43
|
+
if (!option) {
|
|
44
|
+
remaining.push(argv[index++])
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
if (option.name === 'repo-path') {
|
|
48
|
+
const separator = option.value.indexOf('=')
|
|
49
|
+
repoPaths.set(option.value.slice(0, separator).trim(), option.value.slice(separator + 1))
|
|
50
|
+
} else if (project === null) {
|
|
51
|
+
// Preserve the resolver's first project selection and last named repo
|
|
52
|
+
// override behavior, including aliases in the same invocation.
|
|
53
|
+
project = option.value.trim()
|
|
54
|
+
}
|
|
55
|
+
index = option.nextIndex
|
|
56
|
+
}
|
|
57
|
+
return { project, repoPaths, remaining }
|
|
58
|
+
}
|
package/src/cli/run.mjs
CHANGED
|
@@ -58,6 +58,11 @@ export const commandMap = new Map([
|
|
|
58
58
|
['feedback:check', ['src/commands/feedback.mjs', 'check']],
|
|
59
59
|
['announcements', ['src/commands/announcements.mjs']],
|
|
60
60
|
['announcements:list', ['src/commands/announcements.mjs', 'list']],
|
|
61
|
+
['sync', ['src/commands/sync.mjs']],
|
|
62
|
+
['review', ['src/commands/review.mjs']],
|
|
63
|
+
['coauthor', ['src/commands/coauthor.mjs']],
|
|
64
|
+
['skills', ['src/commands/skills.mjs']],
|
|
65
|
+
['obsidian', ['src/commands/obsidian.mjs']],
|
|
61
66
|
])
|
|
62
67
|
|
|
63
68
|
function isDefaultBrand(brand) {
|
|
@@ -115,14 +120,30 @@ Core commands:
|
|
|
115
120
|
support bundle --dry-run Preview a no-send support bundle.
|
|
116
121
|
feedback --message TEXT Write a local, never-sent feedback report.
|
|
117
122
|
announcements list List and verify MNSTRY announcements.
|
|
123
|
+
sync enroll Explicitly enroll one repository for supervision.
|
|
124
|
+
sync status Fully observe the enrolled repository.
|
|
125
|
+
sync reconcile Fetch and perform only proven fast-forwards.
|
|
126
|
+
sync plan Prepare one bounded, reviewable commit plan.
|
|
127
|
+
sync commit Execute an exactly confirmed commit plan.
|
|
128
|
+
sync run --once Run one full-state supervisor cycle.
|
|
129
|
+
obsidian status|open|apply Maintain and open Obsidian views of the corpus; apply edits made in them.
|
|
118
130
|
egress check Check extracted Atelier paths for forbidden egress.
|
|
119
131
|
boundary check Enforce private/shared repo placement rules.
|
|
120
132
|
boundary audit Report content-rule matches tree-wide without blocking.
|
|
121
133
|
boundary install-hooks Install staged/private-domain Git guards.
|
|
122
134
|
promote Record a git.promote disclosure event.
|
|
123
|
-
upgrade --
|
|
135
|
+
upgrade plan --save Save an exact local candidate plan.
|
|
136
|
+
upgrade explain --plan Explain saved changes and consent without writes.
|
|
137
|
+
upgrade apply --plan --confirm Apply the selected plan through commit hooks.
|
|
138
|
+
upgrade status|recover Inspect receipts or preview recovery.
|
|
139
|
+
upgrade --dry-run Plan a legacy package/template upgrade.
|
|
124
140
|
upgrade --apply Apply a branch-based reviewable upgrade.
|
|
125
141
|
lock check|write Verify or create atelier.lock.json.
|
|
142
|
+
lock provenance Inspect actual installed package identity.
|
|
143
|
+
review run|history|handoff Evidence-bound local human review.
|
|
144
|
+
review export|inspect|packs Preview inspection bundles and pack lifecycle.
|
|
145
|
+
coauthor start|read|event|recover Save and resume private authoring drafts.
|
|
146
|
+
skills audit|observe|candidates|sync Audit and manage local skill projections.
|
|
126
147
|
config check Validate project config.
|
|
127
148
|
extension-pack validate Validate declared extension packs.
|
|
128
149
|
extension-pack list List declared extension packs.
|
|
@@ -138,6 +159,9 @@ Attestation commands:
|
|
|
138
159
|
Commands whose usage names --project accept --project=PATH or --project PATH.
|
|
139
160
|
The project resolver also accepts --project-config=PATH and
|
|
140
161
|
MNSTRY_ATELIER_PROJECT_CONFIG=PATH; each command's own help is authoritative.
|
|
162
|
+
Project-aware commands accept repeated --repo-path NAME=PATH or
|
|
163
|
+
--repo-path=NAME=PATH. Overrides change location, never declared read authority.
|
|
164
|
+
Run config check --explain for path-free resolution sources.
|
|
141
165
|
Machine-local repo paths belong in
|
|
142
166
|
.atelier-local/, atelier.local.json, or atelier.workspace.local.json.`
|
|
143
167
|
}
|
|
@@ -145,7 +169,14 @@ Machine-local repo paths belong in
|
|
|
145
169
|
export function buildCommandHelpText(command, brand = DEFAULT_BRAND) {
|
|
146
170
|
const c = brand.command
|
|
147
171
|
const help = {
|
|
148
|
-
|
|
172
|
+
skills: `Usage: ${c} skills audit|observe|candidates|sync
|
|
173
|
+
Audit bundled skills or --root DIR [--peer DIR].
|
|
174
|
+
Sync previews by default; applying requires --apply --confirm PLAN_DIGEST.
|
|
175
|
+
Observations are local metadata, never automatic telemetry.`,
|
|
176
|
+
coauthor: `Usage: ${c} coauthor start|read|event|recover
|
|
177
|
+
|
|
178
|
+
Read one JSON request from stdin (maximum 1 MiB). Start takes {"config":{"id":"SESSION","fields":[{"id":"FIELD","source":{"ref":"packet.md","digest":"SHA256"}}]}}. Read/recover take {"sessionId":"SESSION"}. Event takes {"sessionId":"SESSION","event":{"id":"UNIQUE","expectedRevision":0,"type":"answer","text":"ANSWER"}}. Run from the intended Git workspace with ignored .atelier-local/. Saves are private drafts, never canonical source edits.`,
|
|
179
|
+
init: `Usage: ${c} init [--template private-domain|shared-project|sample-workspace|distribution|external-project] [--target DIR] [--actor ID]
|
|
149
180
|
|
|
150
181
|
Creates tracked starter files and an Atelier lockfile. It does not install hooks unless asked separately. An unrecognized --template exits 1 and writes nothing; omit --template for the blank scaffold.`,
|
|
151
182
|
adopt: `Usage: ${c} adopt [--profile single-repo|private-domain|shared-project|multi-repo|monorepo|control-workspace] [--target DIR] [--yes]
|
|
@@ -176,7 +207,7 @@ Runs the bundled MNSTRY readiness pack claim-first. Protocol state and packet dr
|
|
|
176
207
|
${c} extension-pack validate [--json] [--project ./atelier.project.json]
|
|
177
208
|
${c} extension-pack list [--json] [--project ./atelier.project.json]
|
|
178
209
|
|
|
179
|
-
Loads every extension pack declared under ext["mnstry.atelier"].extensionPacks in the tracked project config and reports one line per pack. Packs load additively alongside the bundled MNSTRY protocols and can never replace them. list is the default subcommand.`,
|
|
210
|
+
Loads every extension pack declared under ext["mnstry.atelier"].extensionPacks in the tracked project config and reports one line per pack. Packs load additively alongside the bundled MNSTRY protocols and can never replace them. list is the default subcommand. Shared --project-config and repeated --repo-path NAME=PATH options are supported in both space and equals forms.`,
|
|
180
211
|
distribution: `Usage: ${c} distribution check [--target DIR] [--pack DIR]
|
|
181
212
|
|
|
182
213
|
Checks a distribution package for the required MNSTRY attribution markers. Blocking: the distribution README.md byte check, and a CLI probe that EXECUTES the target's declared bin with --version (spawned with the current Node, cwd set to the target — only run this against distributions you trust) and requires the attribution in its output; a target that looks like a distribution but declares no probe-able bin, or ships a malformed package.json, is also blocking. The extension-pack manifest attribution key is advisory and reported only. The normative wording lives in TRADEMARKS.md under "Required attribution"; see also docs/attestation.md and docs/distributions.md.`,
|
|
@@ -198,6 +229,33 @@ Assembles a local feedback report under ignored .atelier-local/feedback/ (mode 0
|
|
|
198
229
|
announcements: `Usage: ${c} announcements list [--dir DIR] [--public-key FILE] | verify <file> [--public-key FILE] [--json] | show <file> [--public-key FILE]
|
|
199
230
|
|
|
200
231
|
MNSTRY announcements are a pull-only channel: signed JSON documents under announcements/ in the repository. The trust anchor is always the committed MNSTRY key, or one you pass explicitly with --public-key; --dir changes only where documents are read from and never which key verifies them. Every run names the key and keyId it used. The kit never fetches anything — receiving announcements is the git pull you chose to run, and show refuses to print a body whose signature does not verify.`,
|
|
232
|
+
obsidian: `Usage: ${c} obsidian status|scope|audience|mode|policy|service|open|apply|conflicts|apply-policy|selection|proposals [--project atelier.project.json] [--data-root DIR] [--json]
|
|
233
|
+
|
|
234
|
+
${c} obsidian status
|
|
235
|
+
${c} obsidian scope list | scope show ID
|
|
236
|
+
${c} obsidian audience show | set A,B | clear
|
|
237
|
+
${c} obsidian mode show | set manual|automatic
|
|
238
|
+
${c} obsidian policy show | install FILE | revoke
|
|
239
|
+
${c} obsidian service start [--consent-actor ID] --adapter=obsidian-cli | status | stop | unit --print --adapter=obsidian-cli
|
|
240
|
+
${c} obsidian open [--scope ID] [--consent-actor ID] [--allow-stale] --adapter=obsidian-cli
|
|
241
|
+
${c} obsidian apply list | show EDIT | run EDIT [--actor ID] | recover
|
|
242
|
+
${c} obsidian conflicts [ID]
|
|
243
|
+
${c} obsidian apply-policy create FILE | show | revoke
|
|
244
|
+
${c} obsidian selection resolve ID | persist ID [allow-empty] | show ID | list
|
|
245
|
+
${c} obsidian proposals list | show OPERATION
|
|
246
|
+
|
|
247
|
+
Noninteractive. With --json exactly one JSON document is printed, for a refusal too. status, scope, audience show, mode show, policy show and service status are read-only. open reports current only when the owned maintenance service ticked after the request, the view's trusted generation is the prepared one and reads back byte for byte, and the installed Obsidian meets the minimum version and answers for that vault; every other outcome is typed and exits 3. Reaching the installed app is never a default: open, service start and service unit need --adapter=obsidian-cli. Audiences, mode and the apply policy are private machine settings, never project configuration and never a note. apply, conflicts, apply-policy, selection and proposals are contributed operations registered from modules shipped under src/runtime/obsidian/contributions/; the built-in names cannot be replaced. While no apply operation is registered every apply reports apply-unavailable. Exit codes: 0 done, 1 internal error, 2 refusal or usage, 3 ran and the answer is not success.`,
|
|
248
|
+
sync: `Usage:
|
|
249
|
+
${c} sync enroll --repo DIR [--project atelier.project.json] [--git ABSOLUTE_PATH]
|
|
250
|
+
${c} sync status --repo DIR
|
|
251
|
+
${c} sync reconcile --repo DIR [--retries 3]
|
|
252
|
+
${c} sync run --repo DIR [--once] [--interval 30]
|
|
253
|
+
${c} sync plan --repo DIR --path FILE [--path FILE] --message TEXT [--publish]
|
|
254
|
+
${c} sync commit --repo DIR --operation ID --confirm ID
|
|
255
|
+
${c} sync pause|freeze|resume --repo DIR
|
|
256
|
+
${c} sync trace --repo DIR
|
|
257
|
+
|
|
258
|
+
Enrolls exactly one repository and keeps Git plus readable files authoritative. Reconciliation observes the complete repository every cycle and performs only fast-forward updates. Commit creation is a two-phase user-confirmed operation: plan shows one bounded change set, and commit refuses unless the repository is unchanged and --confirm exactly repeats the operation id. No semantic conflict resolution, force operation, browser apply endpoint, telemetry, or desktop shell is present.`,
|
|
201
259
|
}
|
|
202
260
|
return help[command] || `Usage: ${c} ${command} [args]\n\nRun ${c} --help for the command list.`
|
|
203
261
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalize } from '../attestation/jcs.mjs';
|
|
3
|
+
|
|
4
|
+
// Experimental adapter seam. This reducer performs no storage or authorization.
|
|
5
|
+
export const COAUTHOR_SESSION_VERSION = 'atelier-coauthor-session/experimental-v1';
|
|
6
|
+
export function contentDigest(text) {
|
|
7
|
+
if (typeof text !== 'string') throw new TypeError('text required');
|
|
8
|
+
if (Buffer.from(text, 'utf8').toString('utf8') !== text) throw new TypeError('text must be lossless UTF-8');
|
|
9
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
10
|
+
}
|
|
11
|
+
const digestPattern = /^[a-f0-9]{64}$/;
|
|
12
|
+
function requireText(value, label) {
|
|
13
|
+
if (typeof value !== 'string' || !value.trim() || value.length > 100000) {
|
|
14
|
+
throw new TypeError(`${label} required and bounded`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function sourceCheck(source) {
|
|
18
|
+
if (!source || typeof source !== 'object') throw new TypeError('source required');
|
|
19
|
+
requireText(source.ref, 'source ref');
|
|
20
|
+
if (!digestPattern.test(source.digest)) throw new TypeError('source digest required');
|
|
21
|
+
}
|
|
22
|
+
export function createSession({ id, fields }) {
|
|
23
|
+
requireText(id, 'session id');
|
|
24
|
+
if (!Array.isArray(fields) || fields.length === 0 || fields.length > 1000) throw new TypeError('fields required');
|
|
25
|
+
const ids = new Set();
|
|
26
|
+
const targets = fields.map(({ id: fieldId, source }) => {
|
|
27
|
+
requireText(fieldId, 'field id');
|
|
28
|
+
if (ids.has(fieldId)) throw new Error('duplicate field');
|
|
29
|
+
ids.add(fieldId);
|
|
30
|
+
sourceCheck(source);
|
|
31
|
+
return { id: fieldId, source: { ref: source.ref, digest: source.digest } };
|
|
32
|
+
});
|
|
33
|
+
return { version: COAUTHOR_SESSION_VERSION, id, revision: 0, phase: 'input', index: 0,
|
|
34
|
+
fields: targets, answers: [], proposal: null, saved: [], pending: null, events: [] };
|
|
35
|
+
}
|
|
36
|
+
function checkEvent(event) {
|
|
37
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) throw new TypeError('event required');
|
|
38
|
+
requireText(event.id, 'event id');
|
|
39
|
+
if (!Number.isSafeInteger(event.expectedRevision) || event.expectedRevision < 0) throw new TypeError('revision required');
|
|
40
|
+
const keys = {
|
|
41
|
+
answer: ['text', 'continueAfterSave'], propose: ['text'], confirm: [], reject: [],
|
|
42
|
+
save: [], receipt: ['receipt'], failed: [], retry: [], advance: [], pause: [], resume: [],
|
|
43
|
+
}[event.type];
|
|
44
|
+
if (!keys || Object.keys(event).some(key => !['id', 'type', 'expectedRevision', ...keys].includes(key))) {
|
|
45
|
+
throw new TypeError('unknown event or field');
|
|
46
|
+
}
|
|
47
|
+
if (['answer', 'propose'].includes(event.type)) requireText(event.text, 'answer text');
|
|
48
|
+
if (event.continueAfterSave !== undefined && typeof event.continueAfterSave !== 'boolean') throw new TypeError('continue must be boolean');
|
|
49
|
+
if (event.type === 'receipt') {
|
|
50
|
+
const r = event.receipt;
|
|
51
|
+
if (!r || Object.keys(r).sort().join(',') !== 'fieldId,requestId,sessionId,sourceDigest,sourceRef,valueDigest') throw new TypeError('invalid receipt shape');
|
|
52
|
+
for (const key of ['requestId', 'sessionId', 'fieldId', 'sourceRef']) requireText(r[key], key);
|
|
53
|
+
for (const key of ['sourceDigest', 'valueDigest']) if (!digestPattern.test(r[key])) throw new TypeError('invalid receipt digest');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function need(state, phases) {
|
|
57
|
+
if (!phases.includes(state.phase)) throw new Error(`event refused in ${state.phase}`);
|
|
58
|
+
}
|
|
59
|
+
function writeEffect(state) {
|
|
60
|
+
const field = state.fields[state.index];
|
|
61
|
+
return { type: 'write-field', requestId: state.pending.requestId, sessionId: state.id,
|
|
62
|
+
fieldId: field.id, sourceRef: field.source.ref, sourceDigest: field.source.digest,
|
|
63
|
+
text: state.proposal.text, valueDigest: contentDigest(state.proposal.text) };
|
|
64
|
+
}
|
|
65
|
+
function advance(state) {
|
|
66
|
+
state.index += 1;
|
|
67
|
+
state.phase = state.index === state.fields.length ? 'complete' : 'input';
|
|
68
|
+
state.proposal = null;
|
|
69
|
+
state.pending = null;
|
|
70
|
+
}
|
|
71
|
+
export function transition(session, event) {
|
|
72
|
+
// State is trusted adapter-owned memory. Rehydrate by replaying validated events,
|
|
73
|
+
// not by accepting an arbitrary caller-supplied session snapshot.
|
|
74
|
+
if (session?.version !== COAUTHOR_SESSION_VERSION) throw new Error('unsupported session');
|
|
75
|
+
checkEvent(event);
|
|
76
|
+
const fingerprint = contentDigest(canonicalize(event));
|
|
77
|
+
const prior = session.events.find(item => item.id === event.id);
|
|
78
|
+
if (prior) {
|
|
79
|
+
if (prior.fingerprint !== fingerprint) throw new Error('event id conflict');
|
|
80
|
+
return { session: structuredClone(session), effects: [], duplicate: true };
|
|
81
|
+
}
|
|
82
|
+
if (event.expectedRevision !== session.revision) throw new Error('stale revision');
|
|
83
|
+
const state = structuredClone(session);
|
|
84
|
+
const effects = [];
|
|
85
|
+
switch (event.type) {
|
|
86
|
+
case 'answer':
|
|
87
|
+
need(state, ['input', 'draft']);
|
|
88
|
+
state.answers.push({ eventId: event.id, fieldId: state.fields[state.index].id, text: event.text });
|
|
89
|
+
state.proposal = { text: event.text, originalEventId: event.id, confirmed: true,
|
|
90
|
+
continueAfterSave: event.continueAfterSave ?? false };
|
|
91
|
+
state.phase = 'draft';
|
|
92
|
+
break;
|
|
93
|
+
case 'propose':
|
|
94
|
+
need(state, ['draft']);
|
|
95
|
+
// Every machine revision requires confirmation; no heuristic decides meaning.
|
|
96
|
+
state.proposal.text = event.text;
|
|
97
|
+
state.proposal.confirmed = false;
|
|
98
|
+
state.phase = 'confirmation';
|
|
99
|
+
break;
|
|
100
|
+
case 'confirm':
|
|
101
|
+
need(state, ['confirmation']);
|
|
102
|
+
state.proposal.confirmed = true;
|
|
103
|
+
state.phase = 'draft';
|
|
104
|
+
break;
|
|
105
|
+
case 'reject':
|
|
106
|
+
need(state, ['confirmation']);
|
|
107
|
+
state.proposal.text = state.answers.find(answer => answer.eventId === state.proposal.originalEventId).text;
|
|
108
|
+
state.proposal.confirmed = true;
|
|
109
|
+
state.phase = 'draft';
|
|
110
|
+
break;
|
|
111
|
+
case 'save':
|
|
112
|
+
need(state, ['draft']);
|
|
113
|
+
state.pending = { requestId: event.id, retries: 0 };
|
|
114
|
+
state.phase = 'saving';
|
|
115
|
+
effects.push(writeEffect(state));
|
|
116
|
+
break;
|
|
117
|
+
case 'failed':
|
|
118
|
+
need(state, ['saving']);
|
|
119
|
+
state.phase = 'recovery';
|
|
120
|
+
break;
|
|
121
|
+
case 'retry':
|
|
122
|
+
need(state, ['recovery']);
|
|
123
|
+
if (state.pending.retries >= 1) throw new Error('retry budget exhausted; adapter reconciliation required');
|
|
124
|
+
state.pending.retries += 1;
|
|
125
|
+
state.phase = 'saving';
|
|
126
|
+
effects.push(writeEffect(state));
|
|
127
|
+
break;
|
|
128
|
+
case 'receipt': {
|
|
129
|
+
need(state, ['saving', 'recovery']);
|
|
130
|
+
const expected = writeEffect(state);
|
|
131
|
+
for (const key of ['requestId', 'sessionId', 'fieldId', 'sourceRef', 'sourceDigest', 'valueDigest']) {
|
|
132
|
+
if (event.receipt[key] !== expected[key]) throw new Error(`receipt mismatch: ${key}`);
|
|
133
|
+
}
|
|
134
|
+
state.saved.push({ fieldId: expected.fieldId, text: state.proposal.text, receipt: structuredClone(event.receipt) });
|
|
135
|
+
state.phase = 'saved';
|
|
136
|
+
state.pending = null;
|
|
137
|
+
if (state.proposal.continueAfterSave) advance(state);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'advance':
|
|
141
|
+
need(state, ['saved']);
|
|
142
|
+
advance(state);
|
|
143
|
+
break;
|
|
144
|
+
case 'pause':
|
|
145
|
+
need(state, ['input', 'draft', 'confirmation', 'saved', 'recovery']);
|
|
146
|
+
state.resumePhase = state.phase;
|
|
147
|
+
state.phase = 'paused';
|
|
148
|
+
break;
|
|
149
|
+
case 'resume':
|
|
150
|
+
need(state, ['paused']);
|
|
151
|
+
state.phase = state.resumePhase;
|
|
152
|
+
delete state.resumePhase;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
state.revision += 1;
|
|
156
|
+
state.events.push({ id: event.id, fingerprint, event: structuredClone(event) });
|
|
157
|
+
return { session: state, effects, duplicate: false };
|
|
158
|
+
}
|
|
159
|
+
export function replaySession(config, events) {
|
|
160
|
+
return events.reduce((session, event) => transition(session, event).session, createSession(config));
|
|
161
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { withPrivateLock, publishPrivateFile } from '../project/durable-state.mjs';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { canonicalize } from '../attestation/jcs.mjs';
|
|
6
|
+
import { createCollaborationEventLedger } from '../collaboration/event-ledger.mjs';
|
|
7
|
+
import { ensureContainedPrivateDirectory, openRegularFileNoFollow } from '../project/private-state.mjs';
|
|
8
|
+
import { createSession, transition, contentDigest } from './session.mjs';
|
|
9
|
+
import { validateJsonSchema } from '../export/atelier-export-contract.mjs';
|
|
10
|
+
|
|
11
|
+
const schema = JSON.parse(fs.readFileSync(new URL('../../contracts/atelier-coauthor.v1.schema.json', import.meta.url), 'utf8'));
|
|
12
|
+
function validate(value, kind) {
|
|
13
|
+
const errors = validateJsonSchema({ $schema: schema.$schema, $defs: schema.$defs, $ref: `#/$defs/${kind}` }, value);
|
|
14
|
+
if (errors.length) throw new Error(`invalid coauthor ${kind}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const MAX_SOURCE_BYTES = 1024 * 1024;
|
|
18
|
+
function readText(file) {
|
|
19
|
+
const fd = openRegularFileNoFollow(file);
|
|
20
|
+
try {
|
|
21
|
+
if (fs.fstatSync(fd).size > MAX_SOURCE_BYTES) throw new Error('coauthor file exceeds byte limit');
|
|
22
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(fd));
|
|
23
|
+
} finally { fs.closeSync(fd); }
|
|
24
|
+
}
|
|
25
|
+
function sourcePath(root, ref) {
|
|
26
|
+
if (typeof ref !== 'string' || !ref || ref.includes('\\') || path.isAbsolute(ref) || ref.split('/').some(s => !s || s === '.' || s === '..' || s.startsWith('.'))) {
|
|
27
|
+
throw new Error('source must be a visible workspace-relative file');
|
|
28
|
+
}
|
|
29
|
+
let current = root;
|
|
30
|
+
for (const part of ref.split('/')) {
|
|
31
|
+
current = path.join(current, part);
|
|
32
|
+
if (fs.lstatSync(current).isSymbolicLink()) throw new Error('redirected source refused');
|
|
33
|
+
}
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Local CLI boundary, not an authenticated multi-user service. Responses remain
|
|
38
|
+
// private drafts; this store never modifies a canonical source file.
|
|
39
|
+
export function createCoauthorStore({ workspaceRoot = process.cwd() } = {}) {
|
|
40
|
+
const root = fs.realpathSync(workspaceRoot);
|
|
41
|
+
function privatePlacement() {
|
|
42
|
+
const gitEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('GIT_')));
|
|
43
|
+
try {
|
|
44
|
+
const tracked = execFileSync('git', ['-C', root, 'ls-files', '-z', '--', '.atelier-local'], { env: gitEnv, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
45
|
+
if (tracked) throw new Error('tracked private state');
|
|
46
|
+
execFileSync('git', ['-C', root, 'check-ignore', '--quiet', '.atelier-local/'], { env: gitEnv, stdio: 'ignore' });
|
|
47
|
+
} catch { throw new Error('coauthor requires a Git workspace with untracked, ignored .atelier-local/ state'); }
|
|
48
|
+
}
|
|
49
|
+
privatePlacement();
|
|
50
|
+
const directory = () => ensureContainedPrivateDirectory({ workspaceRoot: root,
|
|
51
|
+
directory: path.join(root, '.atelier-local', 'coauthor'), label: 'coauthor state' });
|
|
52
|
+
const ledger = () => createCollaborationEventLedger({ workspaceRoot: root,
|
|
53
|
+
ledgerPath: path.join(directory(), 'events.ndjson') });
|
|
54
|
+
const aggregate = id => `coauthor-${contentDigest(id)}`;
|
|
55
|
+
function locked(operation) {
|
|
56
|
+
privatePlacement();
|
|
57
|
+
const lockPath = path.join(directory(), 'operation.lock');
|
|
58
|
+
return withPrivateLock(lockPath, operation);
|
|
59
|
+
}
|
|
60
|
+
function load(id) {
|
|
61
|
+
const result = ledger().eventsFor(aggregate(id));
|
|
62
|
+
if (!result.ok) throw new Error(result.error);
|
|
63
|
+
if (!result.events.length) throw new Error('coauthor session not found');
|
|
64
|
+
let state;
|
|
65
|
+
let previous = null;
|
|
66
|
+
let version = 0;
|
|
67
|
+
let config;
|
|
68
|
+
for (const record of result.events) {
|
|
69
|
+
if (record.version !== ++version || record.payload.previousEventId !== previous) throw new Error('coauthor event chain is incomplete');
|
|
70
|
+
if (version === 1) {
|
|
71
|
+
if (record.type !== 'coauthor.started') throw new Error('coauthor start missing');
|
|
72
|
+
config = record.payload.config;
|
|
73
|
+
validate(config, 'config');
|
|
74
|
+
if (config.id !== id) throw new Error('coauthor identity mismatch');
|
|
75
|
+
state = createSession(config);
|
|
76
|
+
} else {
|
|
77
|
+
if (record.type !== 'coauthor.transition') throw new Error('unknown coauthor record');
|
|
78
|
+
const next = transition(state, record.payload.event);
|
|
79
|
+
if (next.duplicate) throw new Error('duplicate persisted transition');
|
|
80
|
+
state = next.session;
|
|
81
|
+
}
|
|
82
|
+
previous = record.id;
|
|
83
|
+
}
|
|
84
|
+
return { state, config, version, previous };
|
|
85
|
+
}
|
|
86
|
+
function verifySources(config) {
|
|
87
|
+
for (const field of config.fields) {
|
|
88
|
+
if (contentDigest(readText(sourcePath(root, field.source.ref))) !== field.source.digest) throw new Error('source changed; preserve history and start a newly bound session');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function append(id, current, event) {
|
|
92
|
+
const next = transition(current.state, event);
|
|
93
|
+
if (next.duplicate) return current;
|
|
94
|
+
const result = ledger().append({ aggregateId: aggregate(id), expectedVersion: current.version,
|
|
95
|
+
type: 'coauthor.transition', actor: 'local-harness', payload: { previousEventId: current.previous, event } });
|
|
96
|
+
if (!result.ok) throw new Error(result.error);
|
|
97
|
+
return { ...current, state: next.session, version: result.event.version, previous: result.event.id };
|
|
98
|
+
}
|
|
99
|
+
function reconcile(id, current) {
|
|
100
|
+
if (!['saving', 'recovery'].includes(current.state.phase)) return current;
|
|
101
|
+
verifySources(current.config);
|
|
102
|
+
const state = current.state;
|
|
103
|
+
const field = state.fields[state.index];
|
|
104
|
+
const requestId = state.pending.requestId;
|
|
105
|
+
const values = ensureContainedPrivateDirectory({ workspaceRoot: root,
|
|
106
|
+
directory: path.join(directory(), 'values', contentDigest(id)), label: 'coauthor values' });
|
|
107
|
+
const target = path.join(values, `${contentDigest(requestId)}.json`);
|
|
108
|
+
const receipt = { sessionId: id, requestId, fieldId: field.id, sourceRef: field.source.ref,
|
|
109
|
+
sourceDigest: field.source.digest, valueDigest: contentDigest(state.proposal.text) };
|
|
110
|
+
const saved = { schema: 'atelier-coauthor-draft@v1', status: 'draft', text: state.proposal.text, receipt };
|
|
111
|
+
validate(saved, 'draft');
|
|
112
|
+
const bytes = canonicalize(saved);
|
|
113
|
+
try {
|
|
114
|
+
// Existing values are immutable. A retry only verifies identical bytes.
|
|
115
|
+
try {
|
|
116
|
+
fs.lstatSync(target);
|
|
117
|
+
if (readText(target) !== bytes) throw new Error('saved draft differs from pending value');
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error.code !== 'ENOENT') throw error;
|
|
120
|
+
publishPrivateFile(target, bytes);
|
|
121
|
+
}
|
|
122
|
+
if (readText(target) !== bytes) throw new Error('saved draft readback mismatch');
|
|
123
|
+
verifySources(current.config);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (state.phase === 'saving') append(id, current, {
|
|
126
|
+
id: `failure-${contentDigest(requestId)}-${state.revision}`, expectedRevision: state.revision, type: 'failed',
|
|
127
|
+
});
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
return append(id, current, { id: `receipt-${contentDigest(requestId)}`,
|
|
131
|
+
expectedRevision: state.revision, type: 'receipt', receipt });
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
start(config) {
|
|
135
|
+
return locked(() => {
|
|
136
|
+
validate(config, 'config');
|
|
137
|
+
const clean = createSession(config);
|
|
138
|
+
const canonicalConfig = { id: clean.id, fields: clean.fields };
|
|
139
|
+
verifySources(canonicalConfig);
|
|
140
|
+
const current = ledger().eventsFor(aggregate(clean.id));
|
|
141
|
+
if (!current.ok) throw new Error(current.error);
|
|
142
|
+
if (current.events.length) {
|
|
143
|
+
const existing = load(clean.id);
|
|
144
|
+
if (canonicalize(existing.config) !== canonicalize(canonicalConfig)) throw new Error('session id already bound to another configuration');
|
|
145
|
+
return existing.state;
|
|
146
|
+
}
|
|
147
|
+
const result = ledger().append({ aggregateId: aggregate(clean.id), expectedVersion: 0,
|
|
148
|
+
type: 'coauthor.started', actor: 'local-harness', payload: { previousEventId: null, config: canonicalConfig } });
|
|
149
|
+
if (!result.ok) throw new Error(result.error);
|
|
150
|
+
return load(clean.id).state;
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
read(id) { return load(id).state; },
|
|
154
|
+
dispatch(id, event) {
|
|
155
|
+
return locked(() => {
|
|
156
|
+
if (['receipt', 'failed'].includes(event?.type)) throw new Error('receipt and failure events are store-owned');
|
|
157
|
+
const current = load(id);
|
|
158
|
+
verifySources(current.config);
|
|
159
|
+
const next = append(id, current, event);
|
|
160
|
+
// Duplicate historical events never cause repeated effects.
|
|
161
|
+
if (next === current) return next.state;
|
|
162
|
+
return ['save', 'retry'].includes(event.type) ? reconcile(id, next).state : next.state;
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
recover(id) { return locked(() => {
|
|
166
|
+
const current = load(id);
|
|
167
|
+
// Paused and exhausted sessions require an explicit resume or operator
|
|
168
|
+
// reconciliation. Never turn a read into another automatic write attempt.
|
|
169
|
+
if (current.state.phase !== 'saving') return current.state;
|
|
170
|
+
return reconcile(id, current).state;
|
|
171
|
+
}); },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import { withPrivateLock } from '../project/durable-state.mjs'
|
|
4
5
|
import { canonicalize } from '../attestation/jcs.mjs'
|
|
5
6
|
import {
|
|
6
7
|
atomicReplacePrivateText,
|
|
@@ -26,18 +27,13 @@ function validTimestamp(value) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function secureAppendLine(file, line) {
|
|
29
|
-
|
|
30
|
-
file,
|
|
31
|
-
fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT,
|
|
32
|
-
0o600,
|
|
33
|
-
)
|
|
30
|
+
let previous = '';
|
|
34
31
|
try {
|
|
35
|
-
|
|
36
|
-
fs.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
32
|
+
const fd = openRegularFileNoFollow(file);
|
|
33
|
+
try { previous = fs.readFileSync(fd, 'utf8'); } finally { fs.closeSync(fd); }
|
|
34
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
35
|
+
// The ledger is bounded. Replace its complete validated prefix atomically.
|
|
36
|
+
atomicReplacePrivateText(file, previous + line);
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
function eventId(event) {
|
|
@@ -215,30 +211,12 @@ export function createCollaborationEventLedger({
|
|
|
215
211
|
}
|
|
216
212
|
|
|
217
213
|
function withWriteLock(operation) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
fs.writeFileSync(descriptor, `${process.pid}\n`)
|
|
222
|
-
fs.fsyncSync(descriptor)
|
|
223
|
-
} catch (error) {
|
|
224
|
-
if (descriptor != null) fs.closeSync(descriptor)
|
|
225
|
-
if (error?.code === 'EEXIST') return { ok: false, status: 423, error: 'collaboration ledger is locked; retry after the active writer finishes' }
|
|
226
|
-
return { ok: false, status: 500, error: `collaboration ledger lock failed: ${error.message}` }
|
|
227
|
-
}
|
|
228
|
-
try {
|
|
229
|
-
return operation()
|
|
230
|
-
} finally {
|
|
231
|
-
fs.closeSync(descriptor)
|
|
232
|
-
try {
|
|
233
|
-
fs.unlinkSync(lockPath)
|
|
234
|
-
} catch {
|
|
235
|
-
// The completed write remains authoritative; cleanup can be diagnosed
|
|
236
|
-
// by the next writer's explicit locked result.
|
|
237
|
-
}
|
|
238
|
-
}
|
|
214
|
+
try { return withPrivateLock(lockPath, operation); }
|
|
215
|
+
catch (error) { return { ok: false, status: error.code === 'EEXIST' ? 423 : 500,
|
|
216
|
+
error: `collaboration ledger lock/write failed: ${error.message}` }; }
|
|
239
217
|
}
|
|
240
218
|
|
|
241
|
-
function append({ aggregateId, expectedVersion = 0, type, actor, at = clock(), payload = {} }) {
|
|
219
|
+
function append({ aggregateId, expectedVersion = 0, type, actor, at = clock(), payload = {}, precondition = null }) {
|
|
242
220
|
return withWriteLock(() => {
|
|
243
221
|
const current = eventsFor(aggregateId)
|
|
244
222
|
if (!current.ok) return current
|
|
@@ -249,6 +227,11 @@ export function createCollaborationEventLedger({
|
|
|
249
227
|
error: `stale collaboration event refused: expected ${expectedVersion}, current ${current.currentVersion}`,
|
|
250
228
|
}
|
|
251
229
|
}
|
|
230
|
+
if (precondition !== null) {
|
|
231
|
+
if (typeof precondition !== 'function') return { ok: false, status: 400, error: 'append precondition must be a function' }
|
|
232
|
+
const permitted = precondition()
|
|
233
|
+
if (!permitted?.ok) return { ok: false, status: permitted?.status ?? 409, error: permitted?.error ?? 'append precondition refused' }
|
|
234
|
+
}
|
|
252
235
|
if (current.stats.eventCount >= limits.maxEvents) {
|
|
253
236
|
return { ok: false, status: 413, error: `collaboration ledger reached ${limits.maxEvents} event hard ceiling; compact it explicitly before appending` }
|
|
254
237
|
}
|