@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,113 @@
|
|
|
1
|
+
import { COLLABORATION_LEDGER_LIMITS } from '../../../collaboration/event-ledger.mjs'
|
|
2
|
+
|
|
3
|
+
// The limits of the existing proposal ledger are contracts of that ledger:
|
|
4
|
+
// 256 KiB a line, 16 MiB in all, 10,000 events, and a line that cannot be read
|
|
5
|
+
// refuses every append. This module asks about them BEFORE the adapter appends
|
|
6
|
+
// anything, and decides what becomes of an operation the ledger cannot take.
|
|
7
|
+
//
|
|
8
|
+
// proposal-too-large the event would be longer than a line may be. Refused; a proposal refers to the preserved
|
|
9
|
+
// bytes by digest instead of holding them, and nothing is ever cut to fit.
|
|
10
|
+
// ledger-full the ledger has no room (or too little: see the reserve). The operation waits in the adapter
|
|
11
|
+
// queue as `backpressure`, its preserved edit untouched, and is tried again later.
|
|
12
|
+
// ledger-corrupt a line of the ledger cannot be read. Refused, for that repository only.
|
|
13
|
+
// store-unavailable the store could not be read or is locked by another writer right now. Waits, like a full one.
|
|
14
|
+
//
|
|
15
|
+
// Nothing here, and nothing in the adapter, compacts, rotates or deletes a
|
|
16
|
+
// ledger or a proposal. Making room is a decision of a person.
|
|
17
|
+
|
|
18
|
+
export const PROPOSAL_LEDGER_LIMITS = Object.freeze({
|
|
19
|
+
maxLineBytes: COLLABORATION_LEDGER_LIMITS.maxLineBytes,
|
|
20
|
+
maxBytes: COLLABORATION_LEDGER_LIMITS.maxBytes,
|
|
21
|
+
maxEvents: COLLABORATION_LEDGER_LIMITS.maxEvents,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const PROPOSAL_BACKPRESSURE = Object.freeze({
|
|
25
|
+
// A proposal the adapter creates is reviewed by people, and a review is two more events. The adapter never takes
|
|
26
|
+
// the last of the room they need to settle what is already there.
|
|
27
|
+
reserveEvents: 32,
|
|
28
|
+
reserveBytes: 512 * 1024,
|
|
29
|
+
// What the store adds around a proposal when it writes the event (identifiers, timestamps) is a few hundred bytes
|
|
30
|
+
// of fixed length; the estimate is that and this much more.
|
|
31
|
+
lineMarginBytes: 1024,
|
|
32
|
+
// Per tick and per repository.
|
|
33
|
+
maxBatchPerRepository: 8,
|
|
34
|
+
// Pending edits looked at per tick to find new structural edits.
|
|
35
|
+
maxExaminedPerTick: 64,
|
|
36
|
+
// Pending edits the object store does not know yet that one tick observes and records, never applies.
|
|
37
|
+
maxObservedPerTick: 16,
|
|
38
|
+
// An operation under backpressure is tried at most this many times, each time later than the last.
|
|
39
|
+
maxAttempts: 8,
|
|
40
|
+
retryIntervalMs: 60 * 1000,
|
|
41
|
+
maxRetryIntervalMs: 60 * 60 * 1000,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export const BACKPRESSURE_CODES = Object.freeze(['ledger-full', 'store-unavailable', 'route-visibility-unknown'])
|
|
45
|
+
export const STORE_REFUSAL_CODES = Object.freeze(['proposal-too-large', 'ledger-corrupt', 'store-refused', 'identity-not-preserved'])
|
|
46
|
+
|
|
47
|
+
// What a read of the whole ledger says about it. `read` is the answer of
|
|
48
|
+
// `eventLedger.readAll()` (or of `listProposals()`, which carries the same
|
|
49
|
+
// status, diagnostics and stats).
|
|
50
|
+
export function classifyLedgerRead(read, limits = PROPOSAL_LEDGER_LIMITS) {
|
|
51
|
+
const events = read?.stats?.eventCount ?? 0
|
|
52
|
+
const bytes = read?.stats?.bytes ?? 0
|
|
53
|
+
const headroom = { events: Math.max(0, limits.maxEvents - events), bytes: Math.max(0, limits.maxBytes - bytes) }
|
|
54
|
+
if (read?.ok) return { state: 'readable', code: null, events, bytes, headroom }
|
|
55
|
+
const diagnostics = Array.isArray(read?.diagnostics) ? read.diagnostics : []
|
|
56
|
+
// Larger than a ledger may be, or more events than one may hold: full, and then some. Not a line that is wrong.
|
|
57
|
+
if (read?.status === 413 || (diagnostics.length > 0 && diagnostics.every((item) => item.code === 'ledger-event-limit'))) {
|
|
58
|
+
return { state: 'full', code: 'ledger-full', events, bytes, headroom: { events: 0, bytes: read?.status === 413 ? 0 : headroom.bytes } }
|
|
59
|
+
}
|
|
60
|
+
if (read?.status === 422) return { state: 'corrupt', code: 'ledger-corrupt', events, bytes, headroom: { events: 0, bytes: 0 }, lines: diagnostics.length }
|
|
61
|
+
return { state: 'unavailable', code: 'store-unavailable', events, bytes, headroom: { events: 0, bytes: 0 } }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The length of the line the store would write for `body`, to within the margin, and never less than it. It repeats
|
|
65
|
+
// what createProposal makes of a body: the members it copies, and the payload, which it keeps whole. The identifiers
|
|
66
|
+
// and timestamps the store adds have a fixed length, so placeholders of that length stand for them.
|
|
67
|
+
export function estimateEventLineBytes({ body, workspaceId, marginBytes = PROPOSAL_BACKPRESSURE.lineMarginBytes }) {
|
|
68
|
+
const at = '2000-01-01T00:00:00.000Z'
|
|
69
|
+
const id = `proposal-${'0'.repeat(32)}`
|
|
70
|
+
const record = {
|
|
71
|
+
schema: 'atelier-proposal@v1', workspaceId,
|
|
72
|
+
proposal: {
|
|
73
|
+
id, status: 'proposed', createdAt: at, updatedAt: at, sessionId: String(body.sessionId ?? ''), viewId: String(body.viewId ?? ''), path: String(body.path ?? ''), action: String(body.action ?? ''),
|
|
74
|
+
intent: String(body.intent ?? ''), reason: String(body.reason ?? ''), storage: { kind: 'local', ignored: true },
|
|
75
|
+
authority: { action: String(body.action ?? ''), capability: 'proposal.copy-only', copyOnly: true, directWrite: false, applyEndpoint: null }, eventVersion: 1,
|
|
76
|
+
},
|
|
77
|
+
diff: typeof body.diff === 'string' ? body.diff : '',
|
|
78
|
+
payload: body.proposal ?? {},
|
|
79
|
+
}
|
|
80
|
+
const event = { schema: 'atelier-collaboration-event@v1', aggregateId: id, version: 1, type: 'proposal-created', actor: String(body.actor ?? ''), at, payload: { record }, id: `event-${'0'.repeat(32)}` }
|
|
81
|
+
return Buffer.byteLength(`${JSON.stringify(event)}\n`) + marginBytes
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// { ok: true } or { ok: false, code, detail }. `ledger` is a classifyLedgerRead answer that is `readable`.
|
|
85
|
+
export function preflightAppend({ ledger, lineBytes, limits = PROPOSAL_LEDGER_LIMITS, bounds = PROPOSAL_BACKPRESSURE }) {
|
|
86
|
+
if (lineBytes > limits.maxLineBytes) return { ok: false, code: 'proposal-too-large', detail: { lineBytes, limit: limits.maxLineBytes } }
|
|
87
|
+
if (ledger.headroom.events < 1 + bounds.reserveEvents) return { ok: false, code: 'ledger-full', detail: { member: 'events', headroom: ledger.headroom.events, reserve: bounds.reserveEvents } }
|
|
88
|
+
if (ledger.headroom.bytes < lineBytes + bounds.reserveBytes) return { ok: false, code: 'ledger-full', detail: { member: 'bytes', headroom: ledger.headroom.bytes, reserve: bounds.reserveBytes, lineBytes } }
|
|
89
|
+
return { ok: true }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// What a refused createProposal means, from its status.
|
|
93
|
+
export function classifyStoreRefusal(status) {
|
|
94
|
+
if (status === 413) return 'ledger-full'
|
|
95
|
+
if (status === 422) return 'ledger-corrupt'
|
|
96
|
+
if (status === 423 || status === 500 || status === 409) return 'store-unavailable'
|
|
97
|
+
return 'store-refused'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// When an operation under backpressure is tried next: twice as long after each attempt, up to the maximum.
|
|
101
|
+
export function nextAttemptAt({ attempts, nowMs, bounds = PROPOSAL_BACKPRESSURE }) {
|
|
102
|
+
const wait = Math.min(bounds.maxRetryIntervalMs, bounds.retryIntervalMs * 2 ** Math.max(0, Math.min(attempts, 30) - 1))
|
|
103
|
+
return new Date(nowMs + wait).toISOString()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const isExhausted = (record, bounds = PROPOSAL_BACKPRESSURE) => record.state === 'backpressure' && record.attempts >= bounds.maxAttempts
|
|
107
|
+
|
|
108
|
+
// Whether the adapter looks at this operation on this tick. Never for one that is settled, refused or exhausted, and
|
|
109
|
+
// never before its time: an unchanged tick reads the queue and nothing else.
|
|
110
|
+
export function isDue(record, { nowMs, bounds = PROPOSAL_BACKPRESSURE }) {
|
|
111
|
+
if (record.state === 'acknowledged' || record.state === 'refused' || isExhausted(record, bounds)) return false
|
|
112
|
+
return record.nextAttemptAt === null || Date.parse(record.nextAttemptAt) <= nowMs
|
|
113
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { refuse } from '../../../runtime/obsidian/errors.mjs'
|
|
2
|
+
import { protectedRoots } from '../../../runtime/obsidian/machine-settings.mjs'
|
|
3
|
+
import { createProposalAdapter } from './adapter.mjs'
|
|
4
|
+
import { ProposalQueueRefusal } from './queue.mjs'
|
|
5
|
+
import { isAdapterOperationId } from './router.mjs'
|
|
6
|
+
|
|
7
|
+
// Binds the proposal adapter to the two places that reach it: the maintenance
|
|
8
|
+
// engine, which hands it the pending edits on a tick, and `atelier obsidian
|
|
9
|
+
// proposals`, which is how a person or an agent reads what became of them.
|
|
10
|
+
// The command reads; it creates, accepts and applies nothing.
|
|
11
|
+
|
|
12
|
+
export const PROPOSAL_ADAPTER_CONTRIBUTION_ID = 'atelier.proposal-adapter'
|
|
13
|
+
const EXIT = Object.freeze({ ok: 0 })
|
|
14
|
+
const SUMMARY = 'list | show OPERATION Structural edits routed as copy-only proposals: per repository what is queued, submitted, acknowledged, waiting or refused, and the room its ledger has. Read-only.'
|
|
15
|
+
|
|
16
|
+
export function createProposalsCommandOperation({ create = createProposalAdapter } = {}) {
|
|
17
|
+
return {
|
|
18
|
+
name: 'proposals',
|
|
19
|
+
summary: SUMMARY,
|
|
20
|
+
async run({ args, env, clock, readable }) {
|
|
21
|
+
const [sub = 'list', operationId, ...extra] = args
|
|
22
|
+
const { project, workspace, workspaceId } = readable()
|
|
23
|
+
if (workspace === null) refuse('workspace-not-prepared', 'this workspace has no private state yet; nothing was ever routed')
|
|
24
|
+
const context = { project, workspaceRoot: workspace.workspaceRoot, workspaceId, repositoryRoots: protectedRoots(project) }
|
|
25
|
+
const adapter = create({ env, clock })
|
|
26
|
+
// An adapter queue that cannot be read is an answer with a code, never an internal error.
|
|
27
|
+
const typed = (operation) => { try { return operation() } catch (error) { if (error instanceof ProposalQueueRefusal) refuse(error.code, 'the record of the proposal adapter cannot be read; nothing was changed'); throw error } }
|
|
28
|
+
if (sub === 'list' && operationId === undefined) {
|
|
29
|
+
const status = typed(() => adapter.status(context))
|
|
30
|
+
const operations = typed(() => adapter.list(context))
|
|
31
|
+
return {
|
|
32
|
+
exit: EXIT.ok, document: { status, operations },
|
|
33
|
+
human: status.repositories.length === 0 ? ['no enrolled repository'] : status.repositories.map((item) => `${item.repoId}\tqueued ${item.queued}\tsubmitted ${item.submitted}\tacknowledged ${item.acknowledged}\twaiting ${item.backpressure}\trefused ${item.refused}\tledger ${item.ledger.state}${item.ledger.headroom ? ` (${item.ledger.headroom.events} events, ${item.ledger.headroom.bytes} bytes free)` : ''}`),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (sub === 'show' && extra.length === 0) {
|
|
37
|
+
if (!isAdapterOperationId(operationId)) refuse('usage', 'name the operation: an identifier that `obsidian proposals list` printed')
|
|
38
|
+
const shown = typed(() => adapter.show(context, { adapterOperationId: operationId }))
|
|
39
|
+
if (shown === null) refuse('unknown-operation', 'the proposal adapter holds no operation of that name')
|
|
40
|
+
return { exit: EXIT.ok, document: shown, human: [JSON.stringify(shown, null, 2)] }
|
|
41
|
+
}
|
|
42
|
+
return refuse('usage', 'proposals takes list or show OPERATION')
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createProposalAdapterContribution(options = {}) {
|
|
48
|
+
return {
|
|
49
|
+
id: PROPOSAL_ADAPTER_CONTRIBUTION_ID,
|
|
50
|
+
register({ extensions, operations }) {
|
|
51
|
+
extensions.register('proposal-adapter', (options.create ?? createProposalAdapter)(options.adapter ?? {}))
|
|
52
|
+
operations.register(createProposalsCommandOperation(options))
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Structural edits made in a vault, routed as copy-only proposals: which
|
|
2
|
+
// existing proposal store of which enrolled repository an edit belongs to
|
|
3
|
+
// (router.mjs), the private record the adapter keeps of every operation before
|
|
4
|
+
// it asks a store for anything (queue.mjs), what it does when the ledger of a
|
|
5
|
+
// store has no room or cannot be read (backpressure.mjs), and the adapter that
|
|
6
|
+
// creates exactly one proposal per operation (adapter.mjs). Nothing here
|
|
7
|
+
// writes a source file or a vault, and no proposal holds the text of a note.
|
|
8
|
+
export {
|
|
9
|
+
MAX_CHANGE_OCCURRENCES, PROPOSAL_ADAPTER_ACTOR, PROPOSAL_ADAPTER_CRASH_STEPS, PROPOSAL_ADAPTER_ID, PROPOSAL_ADAPTER_PRIMITIVES, PROPOSAL_LOCK_PURPOSE, PROPOSAL_PAYLOAD_KIND, PROPOSAL_PAYLOAD_SCHEMA,
|
|
10
|
+
buildProposalContent, createProposalAdapter, createProposalAdapterForOracleTests, describeChange,
|
|
11
|
+
} from './adapter.mjs'
|
|
12
|
+
export {
|
|
13
|
+
BACKPRESSURE_CODES, PROPOSAL_BACKPRESSURE, PROPOSAL_LEDGER_LIMITS, STORE_REFUSAL_CODES, classifyLedgerRead, classifyStoreRefusal, estimateEventLineBytes, isDue, isExhausted, nextAttemptAt, preflightAppend,
|
|
14
|
+
} from './backpressure.mjs'
|
|
15
|
+
export { PROPOSAL_ADAPTER_CONTRIBUTION_ID, createProposalAdapterContribution, createProposalsCommandOperation } from './contribution.mjs'
|
|
16
|
+
export { createTickObservation, manifestOf, recordedOperationOf, retained } from './observation.mjs'
|
|
17
|
+
export {
|
|
18
|
+
OPEN_QUEUE_STATES, PROPOSAL_OPERATION_SCHEMA, PROPOSAL_QUEUE_CRASH_STEPS, PROPOSAL_QUEUE_DIRECTORY, PROPOSAL_QUEUE_LIMITS, PROPOSAL_QUEUE_STATES, ProposalQueueRefusal, isProposalOperationRecord, openProposalQueue,
|
|
19
|
+
} from './queue.mjs'
|
|
20
|
+
export {
|
|
21
|
+
MAX_ROUTED_SOURCE_PATH, PROPOSAL_ROUTER_PRIMITIVES, PROPOSAL_ROUTE_REFUSALS, PROPOSAL_STORE_DIRECTORY, PROPOSAL_STORE_LEDGER, TRANSIENT_ROUTE_REFUSALS, adapterOperationId, createProposalRouterForOracleTests, isAdapterOperationId, isRoutableSourcePath, proposalStoreId, resolveProposalRoute,
|
|
22
|
+
survivesStoreNormalisation,
|
|
23
|
+
} from './router.mjs'
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { AtelierDiagnosticError } from '../../../project/config.mjs'
|
|
4
|
+
import { compareText, isoTime } from '../../../runtime/obsidian/documents.mjs'
|
|
5
|
+
import { readObsidianEnablement } from '../../../runtime/obsidian/enablement.mjs'
|
|
6
|
+
import { ObsidianMaintenanceRefusal } from '../../../runtime/obsidian/errors.mjs'
|
|
7
|
+
import { OPEN_EDIT_STATES, createMaintenanceStateStore } from '../../../runtime/obsidian/state-store.mjs'
|
|
8
|
+
import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal } from '../contracts.mjs'
|
|
9
|
+
import { SOURCE_APPLY_PRIMITIVES, SourceApplyRefusal, locateSource } from '../edits/apply.mjs'
|
|
10
|
+
import { EditArbitrationRefusal } from '../edits/arbitrate.mjs'
|
|
11
|
+
import { createEditObserverForOracleTests, editIdempotencyKey, observeEdit } from '../edits/observe.mjs'
|
|
12
|
+
import { decideApply } from '../edits/policy.mjs'
|
|
13
|
+
import { PublicationRefusal, readFileBytes, sha256Digest } from '../recovery/store.mjs'
|
|
14
|
+
import { PROPOSAL_BACKPRESSURE } from './backpressure.mjs'
|
|
15
|
+
|
|
16
|
+
// Observation on a tick: the part of a source apply that comes before its
|
|
17
|
+
// lease, for every open pending edit the object store does not know yet.
|
|
18
|
+
//
|
|
19
|
+
// In manual mode nothing on a tick applies, so until here a structural edit
|
|
20
|
+
// was recorded as `proposed` only after a person asked apply to look at it.
|
|
21
|
+
// Here the same observation runs on the tick itself, in manual and in
|
|
22
|
+
// automatic mode alike: the source is read now, the lens runs over the
|
|
23
|
+
// PRESERVED edit bytes (never the live note) against it, and the resulting
|
|
24
|
+
// operation is recorded in the object store exactly as an apply records it,
|
|
25
|
+
// through the same observer and the same store. A body replacement is recorded
|
|
26
|
+
// `pending` and left for apply, which is the only thing that writes a source;
|
|
27
|
+
// a structural edit is recorded `proposed`, and the adapter routes it in the
|
|
28
|
+
// same tick; a lens refusal is recorded `refused`; a base that moved on is
|
|
29
|
+
// recorded `conflicted`. Nothing here writes a source file or a vault, and
|
|
30
|
+
// nothing takes a lease.
|
|
31
|
+
//
|
|
32
|
+
// Quiet: the object store answers the same for an origin it already holds and
|
|
33
|
+
// appends nothing, and an edit it holds is not offered again. A refusal that
|
|
34
|
+
// comes before anything can be recorded (a source that is missing, a manifest
|
|
35
|
+
// that cannot be read, an object this machine may not see, a note that cannot
|
|
36
|
+
// be prepared again) is remembered in memory with its code and offered again
|
|
37
|
+
// only when the caller says so, which the engine does at its full
|
|
38
|
+
// reconciliation cadence, never on every tick. A bounded number of edits is
|
|
39
|
+
// looked at per tick, in the order they were observed, starting where the last
|
|
40
|
+
// tick stopped.
|
|
41
|
+
|
|
42
|
+
const EXT = OBSIDIAN_EXT_KEY
|
|
43
|
+
const GONE = new Set(['ENOENT', 'ENOTDIR', 'ELOOP'])
|
|
44
|
+
const segment = (identifier) => identifier.replaceAll(':', '_')
|
|
45
|
+
const refuse = (code, detail) => { throw new SourceApplyRefusal(code, detail) }
|
|
46
|
+
const isTyped = (error) => error instanceof SourceApplyRefusal || error instanceof EditArbitrationRefusal || error instanceof PublicationRefusal || error instanceof ObsidianMaintenanceRefusal
|
|
47
|
+
|| error instanceof ObsidianContractRefusal || error instanceof AtelierDiagnosticError
|
|
48
|
+
|
|
49
|
+
// Observation of an edit whose base source is no longer the source: a conflict, decided without the lens. The same
|
|
50
|
+
// observer source apply uses for that case.
|
|
51
|
+
const STALE_OBSERVER = createEditObserverForOracleTests({
|
|
52
|
+
preserve({ store, edit, baseSourceBytes }) {
|
|
53
|
+
let edited = null
|
|
54
|
+
try { edited = store.readObject(edit.observedDigest) } catch (error) { if (error.code !== 'ENOENT' && error.code !== 'recovery-object-corrupt') throw error }
|
|
55
|
+
return edited === null ? { edited: null } : { edited, publishedRef: undefined, baseRef: store.retainObject(baseSourceBytes).ref }
|
|
56
|
+
},
|
|
57
|
+
classify: () => ({ kind: 'refusal', code: 'stale-base', detail: {} }),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// One open, never through a symbolic link, and only of a regular file.
|
|
61
|
+
function readNoFollow(file) {
|
|
62
|
+
const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0))
|
|
63
|
+
try {
|
|
64
|
+
if (!fs.fstatSync(descriptor).isFile()) throw Object.assign(new Error('not a regular file'), { code: 'EISDIR' })
|
|
65
|
+
return fs.readFileSync(descriptor)
|
|
66
|
+
} finally { fs.closeSync(descriptor) }
|
|
67
|
+
}
|
|
68
|
+
const digestOrNull = (file) => { try { return sha256Digest(readNoFollow(file)) } catch { return null } }
|
|
69
|
+
|
|
70
|
+
// The immutable manifest of one generation of one view: the current one when it is that generation, else the stored
|
|
71
|
+
// file, whose name carries the generation and the head of its digest. `workspace` answers `recoveryOf(scopeId)`.
|
|
72
|
+
export function manifestOf(workspace, scopeId, generationId) {
|
|
73
|
+
const current = workspace.recoveryOf(scopeId).readCurrentManifest()
|
|
74
|
+
if (current?.generationId === generationId) return current
|
|
75
|
+
const directory = path.join(workspace.workspaceRoot, 'state', 'manifests', segment(scopeId))
|
|
76
|
+
let names = []
|
|
77
|
+
try { names = fs.readdirSync(directory) } catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
78
|
+
for (const name of names.filter((item) => item.startsWith(`${segment(generationId)}--`) && item.endsWith('.json')).sort()) {
|
|
79
|
+
let bytes
|
|
80
|
+
try { bytes = readFileBytes(path.join(directory, name)) } catch (error) { if (error.code === 'ENOENT') continue; throw error }
|
|
81
|
+
if (name !== `${segment(generationId)}--${sha256Digest(bytes).slice(7, 19)}.json`) continue
|
|
82
|
+
const manifest = JSON.parse(bytes.toString('utf8'))
|
|
83
|
+
if (manifest.generationId === generationId && manifest.scopeId === scopeId) return manifest
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The retained bytes of that digest, or null when the store does not hold them intact.
|
|
89
|
+
export function retained(store, digest) {
|
|
90
|
+
if (typeof digest !== 'string') return null
|
|
91
|
+
try { const bytes = store.readObject(digest); return sha256Digest(bytes) === digest ? bytes : null } catch (error) {
|
|
92
|
+
if (error?.code === 'ENOENT' || error?.code === 'recovery-object-corrupt') return null
|
|
93
|
+
throw error
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The operation the object store recorded for this edit, or null.
|
|
98
|
+
export function recordedOperationOf(objects, edit) {
|
|
99
|
+
return objects.stateOf({ repoId: edit.identity.repoId, nodeId: edit.identity.nodeId }).operations.find((entry) => entry.origins.some((origin) => origin.editId === edit.editId && origin.scopeId === edit.scopeId)) ?? null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// `seams` prepare a view again when the published note is not retained; `isGitIgnored` and `gitDirectory` are what
|
|
103
|
+
// git is asked through; `bounds.maxObservedPerTick` is how many edits one tick looks at.
|
|
104
|
+
export function createTickObservation(options = {}) {
|
|
105
|
+
const { seams, isGitIgnored = SOURCE_APPLY_PRIMITIVES.isGitIgnored, gitDirectory = SOURCE_APPLY_PRIMITIVES.gitDirectory, bounds = PROPOSAL_BACKPRESSURE } = options
|
|
106
|
+
if (!seams || typeof seams.captureSnapshot !== 'function' || typeof seams.prepareView !== 'function') throw new TypeError('tick observation needs the seams that prepare a view')
|
|
107
|
+
// Edits whose operation the object store holds: a cache, losing it costs one read of an object. And edits whose
|
|
108
|
+
// observation refused before anything could be recorded, with the code.
|
|
109
|
+
const recorded = new Set()
|
|
110
|
+
const unobservable = new Map()
|
|
111
|
+
let cursor = 0
|
|
112
|
+
|
|
113
|
+
// Everything about one pending edit that the manifest of its view says.
|
|
114
|
+
function resolveEdit(workspace, edit) {
|
|
115
|
+
const { repoId, nodeId } = edit.identity
|
|
116
|
+
const scope = readObsidianEnablement(workspace.project).scopes.find((item) => item.scopeId === edit.scopeId)
|
|
117
|
+
if (!scope) refuse('unknown-scope')
|
|
118
|
+
const manifest = manifestOf(workspace, edit.scopeId, edit.generationId)
|
|
119
|
+
const noteEntry = manifest?.notes.find((note) => note.repoId === repoId && note.nodeId === nodeId && note.path === edit.path)
|
|
120
|
+
if (!noteEntry) refuse('manifest-unavailable')
|
|
121
|
+
const source = noteEntry.ext?.[EXT]?.source
|
|
122
|
+
if (typeof source?.rawDigest !== 'string' || typeof source?.path !== 'string') refuse('manifest-unavailable')
|
|
123
|
+
return { scope, manifest, noteEntry, recorded: source, baseSourceDigest: source.rawDigest, idempotencyKey: editIdempotencyKey({ workspaceId: workspace.workspaceId, repoId, nodeId, baseSourceDigest: source.rawDigest, observedDigest: edit.observedDigest }) }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The note exactly as it was published, which the lens needs to tell generated bytes from authored ones. The
|
|
127
|
+
// publisher keeps no copy of a note it created, so the note is prepared again from the sources as they are now and
|
|
128
|
+
// used only when it has the digest the manifest recorded; a retained object of that digest serves as well.
|
|
129
|
+
function publishedNoteOf(workspace, { scope, manifest, noteEntry }, now) {
|
|
130
|
+
const store = workspace.recoveryOf(scope.scopeId)
|
|
131
|
+
const kept = retained(store, noteEntry.noteDigest)
|
|
132
|
+
if (kept) return kept
|
|
133
|
+
workspace.prepared ??= new Map()
|
|
134
|
+
if (!workspace.prepared.has(scope.scopeId)) {
|
|
135
|
+
let files = []
|
|
136
|
+
try {
|
|
137
|
+
const { graph, profile } = workspace.corpus()
|
|
138
|
+
const snapshot = seams.captureSnapshot({ project: workspace.project, graph, workspaceId: workspace.workspaceId, index: new Map(), configDigest: manifest.ext?.[EXT]?.configDigest ?? `sha256:${'0'.repeat(64)}`, capturedAt: now })
|
|
139
|
+
const persistentPathRegistry = createMaintenanceStateStore({ workspaceRoot: workspace.workspaceRoot, workspaceId: workspace.workspaceId }).readPathRegistry()
|
|
140
|
+
files = seams.prepareView({ snapshot, profile, scope, persistentPathRegistry, priorManifest: manifest, existingSettings: null, clock: workspace.clock, vaultRootBytes: Buffer.byteLength(store.vaultRoot, 'utf8') }).files
|
|
141
|
+
} catch (error) { if (!isTyped(error) && !GONE.has(error?.code)) throw error }
|
|
142
|
+
workspace.prepared.set(scope.scopeId, files)
|
|
143
|
+
}
|
|
144
|
+
const file = workspace.prepared.get(scope.scopeId).find((item) => item.path === noteEntry.path && item.digest === noteEntry.noteDigest)
|
|
145
|
+
return file ? file.bytes : null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// One edit, in the order of an apply up to its lease. Returns what was recorded; throws a typed refusal.
|
|
149
|
+
function observeOne(workspace, edit, now) {
|
|
150
|
+
if (edit.identity.workspaceId !== workspace.workspaceId) refuse('foreign-workspace')
|
|
151
|
+
const identity = { repoId: edit.identity.repoId, nodeId: edit.identity.nodeId }
|
|
152
|
+
const known = recordedOperationOf(workspace.objects(), edit)
|
|
153
|
+
if (known) return { code: 'already-observed', kind: known.kind, operationState: known.state, idempotencyKey: known.idempotencyKey }
|
|
154
|
+
|
|
155
|
+
// Whether this machine may see the object is asked first, of the canonical graph as it is now, before the
|
|
156
|
+
// manifest, the record of the object or the source path is looked at: an object that is withheld and one that
|
|
157
|
+
// is absent get this one answer, with nothing read and nothing recorded.
|
|
158
|
+
const { graph, profile } = workspace.corpus()
|
|
159
|
+
const visibility = decideApply({ request: { editId: edit.editId, mode: 'manual' }, workspace: { workspaceRoot: workspace.workspaceRoot, workspaceId: workspace.workspaceId }, graph, profile, object: identity, editClass: 'body-replacement', attempts: [] })
|
|
160
|
+
if (!visibility.allowed && visibility.code === 'object-not-visible') refuse('object-not-visible')
|
|
161
|
+
const resolved = resolveEdit(workspace, edit)
|
|
162
|
+
const node = graph.nodes.find((item) => item.repo === identity.repoId && item.id === identity.nodeId)
|
|
163
|
+
if (!node) refuse('source-not-in-graph')
|
|
164
|
+
if (node.path !== resolved.recorded.path) refuse('source-moved')
|
|
165
|
+
|
|
166
|
+
// The source, now: the one file of an enrolled repository the manifest names, reached through no link.
|
|
167
|
+
const vaultRoots = readObsidianEnablement(workspace.project).scopes.map((scope) => workspace.recoveryOf(scope.scopeId).vaultRoot)
|
|
168
|
+
const located = locateSource({ project: workspace.project, repoId: identity.repoId, relative: node.path, managedRoots: [workspace.workspaceRoot, ...vaultRoots], isGitIgnored, gitDirectory, env: workspace.env ?? process.env })
|
|
169
|
+
let sourceBytes
|
|
170
|
+
try { sourceBytes = readNoFollow(located.absolute) } catch (error) { refuse(error.code === 'ENOENT' ? 'source-missing' : error.code === 'ELOOP' ? 'source-symlink' : 'source-not-regular-file') }
|
|
171
|
+
const sourceDigest = sha256Digest(sourceBytes)
|
|
172
|
+
|
|
173
|
+
// The lens over the preserved bytes, or the conflict when the source moved on; then the record, through the store.
|
|
174
|
+
const store = workspace.recoveryOf(edit.scopeId)
|
|
175
|
+
let observed
|
|
176
|
+
if (sourceDigest !== resolved.baseSourceDigest) {
|
|
177
|
+
observed = STALE_OBSERVER({ edit, manifest: resolved.manifest, publishedNoteBytes: Buffer.alloc(0), baseSourceBytes: sourceBytes, store })
|
|
178
|
+
delete observed.operation.ext[EXT].publishedNoteRef
|
|
179
|
+
} else {
|
|
180
|
+
const publishedNoteBytes = publishedNoteOf(workspace, resolved, now)
|
|
181
|
+
// Preparing the note reads the source again: a source that changed since it was read here is a stale source.
|
|
182
|
+
if (publishedNoteBytes === null && digestOrNull(located.absolute) !== sourceDigest) refuse('stale-source', { cause: 'changed-while-reading' })
|
|
183
|
+
if (publishedNoteBytes === null) refuse('published-note-unavailable')
|
|
184
|
+
observed = observeEdit({ edit, manifest: resolved.manifest, publishedNoteBytes, baseSourceBytes: sourceBytes, store })
|
|
185
|
+
}
|
|
186
|
+
const result = workspace.objects().observe(observed.operation, { presentSourceDigest: sourceDigest })
|
|
187
|
+
return { code: result.appended ? 'observed' : 'already-observed', kind: observed.operation.kind, operationState: result.operation?.state ?? observed.operation.state, idempotencyKey: observed.operation.idempotencyKey }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// `workspace` is the opened workspace of the adapter; `edits` the pending edit records, open and closed;
|
|
191
|
+
// `retryRefused` offers the edits whose observation refused earlier once more. Returns one entry per edit looked
|
|
192
|
+
// at: { editId, repoId, nodeId, status: 'observed' | 'refused', code, kind?, operationState?, idempotencyKey? }.
|
|
193
|
+
return function observePendingEdits(workspace, { edits, retryRefused = false }) {
|
|
194
|
+
if (retryRefused) unobservable.clear()
|
|
195
|
+
const now = isoTime(workspace.clock)
|
|
196
|
+
const candidates = edits.filter((edit) => edit.closedAt === null && OPEN_EDIT_STATES.includes(edit.state) && !recorded.has(edit.editId) && !unobservable.has(edit.editId))
|
|
197
|
+
.sort((left, right) => compareText(left.observedAt, right.observedAt) || compareText(left.editId, right.editId))
|
|
198
|
+
const start = candidates.length === 0 ? 0 : cursor % candidates.length
|
|
199
|
+
const looked = [...candidates.slice(start), ...candidates.slice(0, start)].slice(0, bounds.maxObservedPerTick)
|
|
200
|
+
cursor = candidates.length > bounds.maxObservedPerTick ? start + looked.length : 0
|
|
201
|
+
const observed = []
|
|
202
|
+
for (const edit of looked) {
|
|
203
|
+
const entry = { editId: edit.editId, repoId: edit.identity.repoId, nodeId: edit.identity.nodeId }
|
|
204
|
+
try {
|
|
205
|
+
const outcome = observeOne(workspace, edit, now)
|
|
206
|
+
recorded.add(edit.editId)
|
|
207
|
+
observed.push({ ...entry, status: 'observed', ...outcome })
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (!isTyped(error)) throw error
|
|
210
|
+
unobservable.set(edit.editId, error.code)
|
|
211
|
+
observed.push({ ...entry, status: 'refused', code: error.code })
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return observed
|
|
215
|
+
}
|
|
216
|
+
}
|