@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,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
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import {
|
|
4
|
+
loadBoundRun,
|
|
5
|
+
hashEvidence,
|
|
6
|
+
validBoundRun,
|
|
7
|
+
} from '../readiness-protocols/evidence.mjs'
|
|
8
|
+
import { validReviewArtifact } from './review-contracts.mjs'
|
|
9
|
+
import { createReviewStore, validContribution } from './review-store.mjs'
|
|
10
|
+
import {
|
|
11
|
+
compileDisclosurePatterns,
|
|
12
|
+
scanDisclosureText,
|
|
13
|
+
} from '../disclosure/content-scan.mjs'
|
|
14
|
+
import { openRegularFileNoFollow } from '../project/private-state.mjs'
|
|
15
|
+
|
|
16
|
+
function prohibitedKeys(value, depth = 0) {
|
|
17
|
+
if (
|
|
18
|
+
typeof value === 'string' &&
|
|
19
|
+
(/^(?:[A-Za-z]:[\\/]|\\\\|\/(?!\/))/.test(value) ||
|
|
20
|
+
/[a-z][a-z0-9+.-]*:\/\/[^/\s]*@/i.test(value))
|
|
21
|
+
)
|
|
22
|
+
throw new Error(
|
|
23
|
+
'machine path or credential-bearing URL in selected content',
|
|
24
|
+
)
|
|
25
|
+
if (depth > 32) throw new Error('bundle nesting exceeds bounds')
|
|
26
|
+
if (value && typeof value === 'object')
|
|
27
|
+
for (const [key, child] of Object.entries(value)) {
|
|
28
|
+
if (
|
|
29
|
+
/nonce|session|grant|credential|password|token|secret|private.?key|authorization/i.test(
|
|
30
|
+
key,
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
throw new Error('nontransferable field in selected content')
|
|
34
|
+
prohibitedKeys(child, depth + 1)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function prepareInspectionBundle(
|
|
38
|
+
project,
|
|
39
|
+
runIds,
|
|
40
|
+
{ denylistDocument, responseIds = [] } = {},
|
|
41
|
+
) {
|
|
42
|
+
if (!denylistDocument || !Array.isArray(denylistDocument.patterns))
|
|
43
|
+
throw new Error('private disclosure policy required before export')
|
|
44
|
+
if (
|
|
45
|
+
!Array.isArray(runIds) ||
|
|
46
|
+
!runIds.length ||
|
|
47
|
+
runIds.length > 20 ||
|
|
48
|
+
new Set(runIds).size !== runIds.length
|
|
49
|
+
)
|
|
50
|
+
throw new Error('select 1 to 20 unique run IDs')
|
|
51
|
+
const history = createReviewStore(project).records()
|
|
52
|
+
if (!history.ok) throw new Error(history.error)
|
|
53
|
+
if (
|
|
54
|
+
!Array.isArray(responseIds) ||
|
|
55
|
+
responseIds.length > 2000 ||
|
|
56
|
+
new Set(responseIds).size !== responseIds.length
|
|
57
|
+
)
|
|
58
|
+
throw new Error('invalid response selection')
|
|
59
|
+
const responses = responseIds.map((id) => {
|
|
60
|
+
const record = history.records.find(
|
|
61
|
+
(entry) =>
|
|
62
|
+
entry.input.requestId === id && entry.input.kind !== 'decision',
|
|
63
|
+
)
|
|
64
|
+
if (!record) throw new Error('selected response unavailable')
|
|
65
|
+
return record
|
|
66
|
+
})
|
|
67
|
+
prohibitedKeys(responses)
|
|
68
|
+
const members = runIds.map((id) => {
|
|
69
|
+
const bound = loadBoundRun(project, id)
|
|
70
|
+
if (!bound.ok) throw new Error(bound.error)
|
|
71
|
+
const value = {
|
|
72
|
+
schema: bound.schema,
|
|
73
|
+
run: bound.run,
|
|
74
|
+
runDigest: bound.runDigest,
|
|
75
|
+
snapshot: bound.snapshot,
|
|
76
|
+
decisions: history.records.filter(
|
|
77
|
+
(record) =>
|
|
78
|
+
record.input.kind === 'decision' && record.input.runId === id,
|
|
79
|
+
),
|
|
80
|
+
}
|
|
81
|
+
prohibitedKeys(value)
|
|
82
|
+
return {
|
|
83
|
+
id,
|
|
84
|
+
bytes: Buffer.byteLength(JSON.stringify(value)),
|
|
85
|
+
digest: hashEvidence(value),
|
|
86
|
+
value,
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
const body = {
|
|
90
|
+
schema: 'atelier-inspection-bundle@v1',
|
|
91
|
+
purpose: 'historical-inspection-only',
|
|
92
|
+
classification: 'owner-selected-private-review',
|
|
93
|
+
members,
|
|
94
|
+
responses,
|
|
95
|
+
}
|
|
96
|
+
const bytes = JSON.stringify(body, null, 2)
|
|
97
|
+
const disclosure = scanDisclosureText(bytes, {
|
|
98
|
+
denylistPatterns: compileDisclosurePatterns(denylistDocument.patterns),
|
|
99
|
+
})
|
|
100
|
+
if (!disclosure.ok)
|
|
101
|
+
throw new Error('selected content failed disclosure policy; export refused')
|
|
102
|
+
const bundle = { ...body, digest: hashEvidence(body) }
|
|
103
|
+
inspectBundle(bundle)
|
|
104
|
+
return bundle
|
|
105
|
+
}
|
|
106
|
+
export function inspectBundle(bundle) {
|
|
107
|
+
const serialized = JSON.stringify(bundle)
|
|
108
|
+
if (Buffer.byteLength(serialized) > 4 * 1024 * 1024)
|
|
109
|
+
throw new Error('bundle exceeds byte bound')
|
|
110
|
+
if (!validReviewArtifact('bundle', bundle))
|
|
111
|
+
throw new Error('inspection bundle contract invalid')
|
|
112
|
+
if (
|
|
113
|
+
!bundle ||
|
|
114
|
+
bundle.schema !== 'atelier-inspection-bundle@v1' ||
|
|
115
|
+
bundle.purpose !== 'historical-inspection-only' ||
|
|
116
|
+
!Array.isArray(bundle.members) ||
|
|
117
|
+
!bundle.members.length ||
|
|
118
|
+
bundle.members.length > 20 ||
|
|
119
|
+
Object.keys(bundle).some(
|
|
120
|
+
(key) =>
|
|
121
|
+
![
|
|
122
|
+
'schema',
|
|
123
|
+
'purpose',
|
|
124
|
+
'classification',
|
|
125
|
+
'members',
|
|
126
|
+
'responses',
|
|
127
|
+
'digest',
|
|
128
|
+
'ext',
|
|
129
|
+
'contractVersion',
|
|
130
|
+
].includes(key),
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
throw new Error('unsupported inspection bundle')
|
|
134
|
+
const { digest, ...body } = bundle
|
|
135
|
+
if (digest !== hashEvidence(body)) throw new Error('bundle digest mismatch')
|
|
136
|
+
const ids = new Set(),
|
|
137
|
+
requests = new Set()
|
|
138
|
+
const verifyRecord = (record) => {
|
|
139
|
+
if (!validContribution(record) || requests.has(record.input.requestId))
|
|
140
|
+
throw new Error('invalid or duplicate historical contribution')
|
|
141
|
+
requests.add(record.input.requestId)
|
|
142
|
+
}
|
|
143
|
+
prohibitedKeys(bundle.responses)
|
|
144
|
+
for (const record of bundle.responses) {
|
|
145
|
+
verifyRecord(record)
|
|
146
|
+
if (record.input.kind === 'decision')
|
|
147
|
+
throw new Error('decision outside run member')
|
|
148
|
+
}
|
|
149
|
+
for (const member of bundle.members) {
|
|
150
|
+
if (
|
|
151
|
+
!member ||
|
|
152
|
+
!/^readiness-run-[a-z0-9-]+$/.test(member.id ?? '') ||
|
|
153
|
+
ids.has(member.id) ||
|
|
154
|
+
Object.keys(member).some(
|
|
155
|
+
(key) => !['id', 'bytes', 'digest', 'value', 'ext'].includes(key),
|
|
156
|
+
) ||
|
|
157
|
+
member.digest !== hashEvidence(member.value) ||
|
|
158
|
+
member.bytes !== Buffer.byteLength(JSON.stringify(member.value))
|
|
159
|
+
)
|
|
160
|
+
throw new Error('invalid, duplicate or changed bundle member')
|
|
161
|
+
prohibitedKeys(member.value)
|
|
162
|
+
ids.add(member.id)
|
|
163
|
+
const value = member.value,
|
|
164
|
+
{ digest: snapshotDigest, ...snapshot } = value.snapshot ?? {}
|
|
165
|
+
const { decisions, ...bound } = value
|
|
166
|
+
if (
|
|
167
|
+
!validBoundRun(bound) ||
|
|
168
|
+
value.schema !== 'atelier-bound-run@v1' ||
|
|
169
|
+
value.run?.runId !== member.id ||
|
|
170
|
+
value.runDigest !== hashEvidence(value.run) ||
|
|
171
|
+
snapshotDigest !== hashEvidence(snapshot) ||
|
|
172
|
+
!Array.isArray(value.decisions)
|
|
173
|
+
)
|
|
174
|
+
throw new Error('invalid historical evidence')
|
|
175
|
+
for (const record of value.decisions) {
|
|
176
|
+
verifyRecord(record)
|
|
177
|
+
if (
|
|
178
|
+
record.input.kind !== 'decision' ||
|
|
179
|
+
record.input.runId !== member.id ||
|
|
180
|
+
record.input.evidenceDigest !== snapshotDigest ||
|
|
181
|
+
!value.run.claims.some(
|
|
182
|
+
(claim) => claim.claimId === record.input.targetId,
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
throw new Error('decision does not bind selected evidence')
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
schema: 'atelier-inspection-report@v1',
|
|
191
|
+
approvalAuthority: false,
|
|
192
|
+
authorship: 'unverified-historical-assertions',
|
|
193
|
+
writes: false,
|
|
194
|
+
members: bundle.members,
|
|
195
|
+
responses: bundle.responses,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
export function readInspectionBundle(file) {
|
|
199
|
+
const descriptor = openRegularFileNoFollow(file)
|
|
200
|
+
try {
|
|
201
|
+
if (fs.fstatSync(descriptor).size > 4 * 1024 * 1024)
|
|
202
|
+
throw new Error('bundle exceeds byte bound')
|
|
203
|
+
return inspectBundle(JSON.parse(fs.readFileSync(descriptor, 'utf8')))
|
|
204
|
+
} finally {
|
|
205
|
+
fs.closeSync(descriptor)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export function writeInspectionBundle(file, bundle) {
|
|
209
|
+
inspectBundle(bundle)
|
|
210
|
+
if (path.extname(file) !== '.json')
|
|
211
|
+
throw new Error('inspection bundle output must be a JSON file')
|
|
212
|
+
const descriptor = openRegularFileNoFollow(
|
|
213
|
+
file,
|
|
214
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
|
|
215
|
+
0o600,
|
|
216
|
+
)
|
|
217
|
+
try {
|
|
218
|
+
fs.writeFileSync(descriptor, JSON.stringify(bundle, null, 2) + '\n')
|
|
219
|
+
fs.fsyncSync(descriptor)
|
|
220
|
+
} finally {
|
|
221
|
+
fs.closeSync(descriptor)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import Ajv2020 from 'ajv/dist/2020.js'
|
|
3
|
+
import addFormats from 'ajv-formats'
|
|
4
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false })
|
|
5
|
+
addFormats(ajv)
|
|
6
|
+
for (const name of ['atelier-review'])
|
|
7
|
+
ajv.addSchema(
|
|
8
|
+
JSON.parse(
|
|
9
|
+
fs.readFileSync(
|
|
10
|
+
new URL(`../../contracts/${name}.v1.schema.json`, import.meta.url),
|
|
11
|
+
'utf8',
|
|
12
|
+
),
|
|
13
|
+
),
|
|
14
|
+
)
|
|
15
|
+
const validators = new Map()
|
|
16
|
+
export function validReviewArtifact(kind, value) {
|
|
17
|
+
if (!validators.has(kind))
|
|
18
|
+
validators.set(
|
|
19
|
+
kind,
|
|
20
|
+
ajv.compile({
|
|
21
|
+
$ref: `https://mnstry.ai/schemas/atelier/atelier-review.v1.schema.json#/$defs/${kind}`,
|
|
22
|
+
}),
|
|
23
|
+
)
|
|
24
|
+
return validators.get(kind)(value)
|
|
25
|
+
}
|