@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,234 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { acquirePrivateLock, publishPrivateFile } from '../../../project/durable-state.mjs'
|
|
5
|
+
import { checkManagedRoots } from '../../../project/file-class.mjs'
|
|
6
|
+
import { atomicReplacePrivateText, ensureContainedPrivateDirectory, openRegularFileNoFollow, readRegularTextNoFollow } from '../../../project/private-state.mjs'
|
|
7
|
+
|
|
8
|
+
// Private per-workspace state for one Obsidian view:
|
|
9
|
+
//
|
|
10
|
+
// <workspaceRoot>/vaults/<scopeId>/ the editable vault (or an explicit vaultRoot)
|
|
11
|
+
// <workspaceRoot>/state/manifests/<scopeId>/ trusted manifests and the current pointer
|
|
12
|
+
// <workspaceRoot>/state/journals/<scopeId>/<id>/ publication journals
|
|
13
|
+
// <workspaceRoot>/recovery/objects/<sha256>.bin immutable content-addressed bytes
|
|
14
|
+
// <workspaceRoot>/recovery/<journalId>/<unit>/ displaced files, receipts, and the unit's exchange candidate
|
|
15
|
+
// <workspaceRoot>/recovery/.exchange-probe/ scratch files of the exchange self-test, nothing else
|
|
16
|
+
// <workspaceRoot>/staging/<journalId>/ disposable generated bytes that no exchange ever names
|
|
17
|
+
// <vault>/.atelier-publication/ the vault lock, shared by every view of the vault
|
|
18
|
+
//
|
|
19
|
+
// Immutable records are published with publishPrivateFile: a complete file
|
|
20
|
+
// appears atomically under a name that is never overwritten. What that gives
|
|
21
|
+
// is an all-or-nothing record that survives a process crash; it is fsynced,
|
|
22
|
+
// which on some platforms is weaker than a full device flush. A displaced
|
|
23
|
+
// file is different: it is the very file that occupied a note path, it is
|
|
24
|
+
// moved and never copied over, and a program that still holds it open may
|
|
25
|
+
// write into it later. It is therefore never treated as immutable; its digest
|
|
26
|
+
// at the time of the move is recorded in a receipt and re-checked.
|
|
27
|
+
//
|
|
28
|
+
// A candidate that replaces an existing file is exchanged with it, so after
|
|
29
|
+
// the exchange the candidate's path holds the displaced bytes. That path is
|
|
30
|
+
// therefore in the unit's recovery directory, never in staging: staging is
|
|
31
|
+
// discardable, and displaced bytes must not sit in a discardable area for an
|
|
32
|
+
// instant. The candidate is written and fsynced in staging, where it is only
|
|
33
|
+
// generated bytes, named with its digest by the journal, and only then moved
|
|
34
|
+
// to its exchange path. What a file at an exchange path is, is decided from
|
|
35
|
+
// the journal: bytes with the recorded candidate digest are generated and
|
|
36
|
+
// ours; anything else is somebody's bytes and is kept. The candidate names
|
|
37
|
+
// end in `.candidate`; everything else the store puts in a unit directory
|
|
38
|
+
// ends in `.bin` or `.json`, so the names cannot collide.
|
|
39
|
+
|
|
40
|
+
export const EXCHANGE_CANDIDATE_NAME = 'exchange.candidate'
|
|
41
|
+
export const LATE_EXCHANGE_CANDIDATE_NAME = 'exchange.late.candidate'
|
|
42
|
+
|
|
43
|
+
export const VAULT_LOCK_DIRECTORY = '.atelier-publication'
|
|
44
|
+
|
|
45
|
+
// The vault lock, with the semantics of every private lock: the newest ticket
|
|
46
|
+
// owns it, a release marker frees it, and a ticket whose process is gone on
|
|
47
|
+
// this host is stale and is taken over. Nothing else is ever written here.
|
|
48
|
+
// Tickets of superseded generations are removed while the lock is held, the
|
|
49
|
+
// marker before its ticket, so the directory does not grow inside a person's
|
|
50
|
+
// vault and an interrupted removal never leaves a marker without its ticket.
|
|
51
|
+
export function acquireVaultLock(store) {
|
|
52
|
+
const directory = ensureContainedPrivateDirectory({ workspaceRoot: store.vaultRoot, directory: path.dirname(store.vaultLockPath), label: 'vault publication lock' })
|
|
53
|
+
const release = acquirePrivateLock(path.join(directory, path.basename(store.vaultLockPath)))
|
|
54
|
+
try {
|
|
55
|
+
const owners = `${store.vaultLockPath}.owners`
|
|
56
|
+
const names = fs.readdirSync(owners).filter((name) => /^\d{12}\.(json|released)$/.test(name)).sort()
|
|
57
|
+
const newest = names.filter((name) => name.endsWith('.json')).at(-1)?.slice(0, 12)
|
|
58
|
+
for (const name of names.filter((item) => item.slice(0, 12) < newest).sort((left, right) => (left.endsWith('.released') ? -1 : 1) - (right.endsWith('.released') ? -1 : 1))) {
|
|
59
|
+
fs.rmSync(path.join(owners, name), { force: true })
|
|
60
|
+
}
|
|
61
|
+
} catch { /* history that cannot be pruned is harmless */ }
|
|
62
|
+
return release
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
66
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/
|
|
67
|
+
|
|
68
|
+
export const sha256Digest = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`
|
|
69
|
+
|
|
70
|
+
export class PublicationRefusal extends Error {
|
|
71
|
+
constructor(code, message, detail = {}) {
|
|
72
|
+
super(`${code}: ${message}`)
|
|
73
|
+
this.name = 'PublicationRefusal'
|
|
74
|
+
this.code = code
|
|
75
|
+
this.detail = detail
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function refuse(code, message, detail) {
|
|
80
|
+
throw new PublicationRefusal(code, message, detail)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Identifiers become directory names. ':' is legal in an identifier and is not
|
|
84
|
+
// legal in a file name everywhere.
|
|
85
|
+
const segment = (identifier) => identifier.replaceAll(':', '_')
|
|
86
|
+
|
|
87
|
+
export function readFileDigest(file) {
|
|
88
|
+
let descriptor
|
|
89
|
+
try {
|
|
90
|
+
descriptor = openRegularFileNoFollow(file)
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error.code === 'ENOENT') return null
|
|
93
|
+
throw error
|
|
94
|
+
}
|
|
95
|
+
try { return sha256Digest(fs.readFileSync(descriptor)) } finally { fs.closeSync(descriptor) }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function readFileBytes(file) {
|
|
99
|
+
const descriptor = openRegularFileNoFollow(file)
|
|
100
|
+
try { return fs.readFileSync(descriptor) } finally { fs.closeSync(descriptor) }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function publishOnce(file, bytes) {
|
|
104
|
+
try {
|
|
105
|
+
publishPrivateFile(file, bytes)
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// The name is taken by different bytes. An immutable record is never replaced.
|
|
108
|
+
if (error.code !== 'EEXIST') throw error
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// `repositoryRoots` is required: every enrolled repository root of the loaded
|
|
115
|
+
// project. Neither the workspace state nor the vault may overlap one of them,
|
|
116
|
+
// in either direction, lexically or by real path; the store refuses before it
|
|
117
|
+
// creates anything. A caller with no enrolled repository (a test in a
|
|
118
|
+
// temporary directory) says so with an explicit empty list.
|
|
119
|
+
export function createRecoveryStore({ workspaceRoot, workspaceId, scopeId, vaultRoot, repositoryRoots } = {}) {
|
|
120
|
+
if (typeof workspaceRoot !== 'string' || !path.isAbsolute(workspaceRoot)) throw new TypeError('workspaceRoot must be an absolute path')
|
|
121
|
+
for (const [label, value] of [['workspaceId', workspaceId], ['scopeId', scopeId]]) {
|
|
122
|
+
if (typeof value !== 'string' || !IDENTIFIER.test(value)) throw new TypeError(`${label} must be a contract identifier`)
|
|
123
|
+
}
|
|
124
|
+
if (!Array.isArray(repositoryRoots) || repositoryRoots.some((item) => typeof item !== 'string' || !path.isAbsolute(item))) {
|
|
125
|
+
throw new TypeError('repositoryRoots must list the absolute root of every enrolled repository; pass an explicit empty list when there is none')
|
|
126
|
+
}
|
|
127
|
+
if (vaultRoot !== undefined && (typeof vaultRoot !== 'string' || !path.isAbsolute(vaultRoot))) throw new TypeError('vaultRoot must be an absolute path')
|
|
128
|
+
const guard = checkManagedRoots({ managedRoots: [workspaceRoot, ...(vaultRoot === undefined ? [] : [vaultRoot])], repositoryRoots })
|
|
129
|
+
if (!guard.ok) refuse(guard.refusals[0].code, guard.refusals[0].message, { refusals: guard.refusals })
|
|
130
|
+
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 })
|
|
131
|
+
const root = fs.realpathSync(workspaceRoot)
|
|
132
|
+
const privateDir = (...parts) => ensureContainedPrivateDirectory({ workspaceRoot: root, directory: path.join(root, ...parts), label: 'Obsidian publication state' })
|
|
133
|
+
const requestedVault = vaultRoot ?? path.join(root, 'vaults', segment(scopeId))
|
|
134
|
+
fs.mkdirSync(requestedVault, { recursive: true })
|
|
135
|
+
const vault = fs.realpathSync(requestedVault)
|
|
136
|
+
const inside = (parent, child) => { const relative = path.relative(parent, child); return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)) }
|
|
137
|
+
for (const area of ['state', 'recovery', 'staging']) {
|
|
138
|
+
if (inside(path.join(root, area), vault) || inside(vault, path.join(root, area))) throw new TypeError('the vault may not overlap private publication state')
|
|
139
|
+
}
|
|
140
|
+
const manifests = privateDir('state', 'manifests', segment(scopeId))
|
|
141
|
+
const journals = privateDir('state', 'journals', segment(scopeId))
|
|
142
|
+
const locks = privateDir('state', 'locks')
|
|
143
|
+
const objects = privateDir('recovery', 'objects')
|
|
144
|
+
const staging = privateDir('staging')
|
|
145
|
+
const pointerFile = path.join(manifests, 'current.json')
|
|
146
|
+
|
|
147
|
+
const store = {
|
|
148
|
+
workspaceRoot: root,
|
|
149
|
+
workspaceId,
|
|
150
|
+
scopeId,
|
|
151
|
+
vaultRoot: vault,
|
|
152
|
+
journalsRoot: journals,
|
|
153
|
+
lockPath: path.join(locks, `${segment(scopeId)}.lock`),
|
|
154
|
+
// One publisher per vault, whichever view or workspace state it belongs to.
|
|
155
|
+
// Two stores share nothing but the vault, so this lock lives under the
|
|
156
|
+
// vault's real path, in a dot-directory no note path can name.
|
|
157
|
+
vaultLockPath: path.join(vault, VAULT_LOCK_DIRECTORY, 'vault.lock'),
|
|
158
|
+
ref: (absolute) => path.relative(root, absolute).split(path.sep).join('/'),
|
|
159
|
+
resolve(ref) {
|
|
160
|
+
const absolute = path.resolve(root, ref)
|
|
161
|
+
if (!inside(root, absolute) || absolute === root) throw new TypeError('a store reference must stay inside the workspace state')
|
|
162
|
+
return absolute
|
|
163
|
+
},
|
|
164
|
+
journalDir: (journalId) => privateDir('state', 'journals', segment(scopeId), segment(journalId)),
|
|
165
|
+
stagingDir: (journalId) => privateDir('staging', segment(journalId)),
|
|
166
|
+
stagingRoot: staging,
|
|
167
|
+
// Where the exchange self-test runs: inside recovery, because that is where exchange candidates live.
|
|
168
|
+
exchangeProbeDir: () => privateDir('recovery', '.exchange-probe'),
|
|
169
|
+
// Generated bytes on their way to a path: written here, and for a created file linked from here.
|
|
170
|
+
preparedPath: (journalId, unit, { late = false } = {}) => path.join(store.stagingDir(journalId), `${String(unit).padStart(6, '0')}${late ? '.late' : ''}.candidate`),
|
|
171
|
+
// The path handed to the exchange. Computed without creating anything; unitDir creates the directory.
|
|
172
|
+
exchangeCandidatePath: (journalId, unit, { late = false } = {}) => path.join(root, 'recovery', segment(journalId), String(unit).padStart(6, '0'), late ? LATE_EXCHANGE_CANDIDATE_NAME : EXCHANGE_CANDIDATE_NAME),
|
|
173
|
+
unitsRoot: (journalId) => path.join(root, 'recovery', segment(journalId)),
|
|
174
|
+
unitDir: (journalId, unit) => privateDir('recovery', segment(journalId), String(unit).padStart(6, '0')),
|
|
175
|
+
displacedPath: (journalId, unit, name = 'displaced.bin') => path.join(store.unitDir(journalId, unit), name),
|
|
176
|
+
|
|
177
|
+
// Immutable, content-addressed bytes (a base, an observed edit, an outside writer's bytes).
|
|
178
|
+
retainObject(bytes) {
|
|
179
|
+
const digest = sha256Digest(bytes)
|
|
180
|
+
const file = path.join(objects, `${digest.slice('sha256:'.length)}.bin`)
|
|
181
|
+
publishPrivateFile(file, bytes)
|
|
182
|
+
return { digest, ref: store.ref(file) }
|
|
183
|
+
},
|
|
184
|
+
readObject(digest) {
|
|
185
|
+
if (!DIGEST.test(digest)) throw new TypeError('digest required')
|
|
186
|
+
const bytes = readFileBytes(path.join(objects, `${digest.slice('sha256:'.length)}.bin`))
|
|
187
|
+
if (sha256Digest(bytes) !== digest) refuse('recovery-object-corrupt', 'a retained object no longer matches its name')
|
|
188
|
+
return bytes
|
|
189
|
+
},
|
|
190
|
+
writeReceipt(journalId, unit, receipt) {
|
|
191
|
+
const body = Buffer.from(`${JSON.stringify({ schema: 'atelier-obsidian-recovery-receipt/v1', journalId, unit, ...receipt }, null, 2)}\n`, 'utf8')
|
|
192
|
+
const file = path.join(store.unitDir(journalId, unit), `receipt-${receipt.role}-${sha256Digest(body).slice(7, 19)}.json`)
|
|
193
|
+
publishOnce(file, body)
|
|
194
|
+
return store.ref(file)
|
|
195
|
+
},
|
|
196
|
+
listReceipts(journalId) {
|
|
197
|
+
const base = path.join(root, 'recovery', segment(journalId))
|
|
198
|
+
if (!fs.existsSync(base)) return []
|
|
199
|
+
const receipts = []
|
|
200
|
+
for (const unit of fs.readdirSync(base).sort()) {
|
|
201
|
+
for (const name of fs.readdirSync(path.join(base, unit)).filter((entry) => /^receipt-.*\.json$/.test(entry)).sort()) {
|
|
202
|
+
receipts.push(JSON.parse(readRegularTextNoFollow(path.join(base, unit, name))))
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return receipts
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
// Trusted manifest. The manifest file is immutable; only the small pointer
|
|
209
|
+
// is replaced, and only as the last step of a publication.
|
|
210
|
+
readCurrent() {
|
|
211
|
+
let text
|
|
212
|
+
try { text = readRegularTextNoFollow(pointerFile) } catch (error) { if (error.code === 'ENOENT') return null; throw error }
|
|
213
|
+
const pointer = JSON.parse(text)
|
|
214
|
+
if (pointer.scopeId !== scopeId || pointer.workspaceId !== workspaceId) refuse('state-mismatch', 'the manifest pointer belongs to another view')
|
|
215
|
+
return pointer
|
|
216
|
+
},
|
|
217
|
+
readCurrentManifest() {
|
|
218
|
+
const pointer = store.readCurrent()
|
|
219
|
+
if (!pointer) return null
|
|
220
|
+
const bytes = readFileBytes(path.join(manifests, pointer.manifestFile))
|
|
221
|
+
if (sha256Digest(bytes) !== pointer.manifestDigest) refuse('state-mismatch', 'the trusted manifest no longer matches its pointer')
|
|
222
|
+
return JSON.parse(bytes.toString('utf8'))
|
|
223
|
+
},
|
|
224
|
+
commitManifest({ manifestBytes, generationId, journalId, retained = [], committedAt }) {
|
|
225
|
+
const manifestDigest = sha256Digest(manifestBytes)
|
|
226
|
+
const manifestFile = `${segment(generationId)}--${manifestDigest.slice(7, 19)}.json`
|
|
227
|
+
publishPrivateFile(path.join(manifests, manifestFile), manifestBytes)
|
|
228
|
+
const pointer = { schema: 'atelier-obsidian-current-manifest/v1', workspaceId, scopeId, generationId, manifestFile, manifestDigest, journalId, committedAt, retained }
|
|
229
|
+
atomicReplacePrivateText(pointerFile, `${JSON.stringify(pointer, null, 2)}\n`)
|
|
230
|
+
return pointer
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
return store
|
|
234
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { compareText } from '../../../runtime/obsidian/documents.mjs'
|
|
2
|
+
import { OPEN_EDIT_STATES, createMaintenanceStateStore } from '../../../runtime/obsidian/state-store.mjs'
|
|
3
|
+
import { openObjectStore } from '../edits/object-store.mjs'
|
|
4
|
+
|
|
5
|
+
// A read-only, typed view of the shared conflict state AOP-2's arbitration
|
|
6
|
+
// keeps per object: which objects are conflicted, pending, settled or
|
|
7
|
+
// unreadable, which operations are involved and why, and which pending edit
|
|
8
|
+
// records of which views feed them. Identities, states, codes and digests
|
|
9
|
+
// only: no note text, title or path.
|
|
10
|
+
//
|
|
11
|
+
// `conflictView` is pure: it is a function of the object entries (the
|
|
12
|
+
// object store's `list()` or an `arbitrateEvents` summary) and the pending
|
|
13
|
+
// edit records. `readConflictView` reads both from the workspace's private
|
|
14
|
+
// state. Neither decides anything, writes an event or touches a source file
|
|
15
|
+
// or a vault.
|
|
16
|
+
|
|
17
|
+
export const CONFLICT_VIEW_SCHEMA = 'atelier-obsidian-conflict-view/v1'
|
|
18
|
+
export const OBJECT_VIEW_STATES = Object.freeze(['conflicted', 'pending', 'settled', 'empty', 'inconsistent', 'unreadable'])
|
|
19
|
+
|
|
20
|
+
// What a person or an agent does next. A conflict is resolved by a new
|
|
21
|
+
// operation that names the ones it resolves; nothing here chooses a winner.
|
|
22
|
+
const NEXT = Object.freeze({
|
|
23
|
+
conflicted: 'offer a resolution that names every conflicted operation; no operation is dropped and no winner is chosen',
|
|
24
|
+
pending: 'apply explicitly, or let an active automatic policy dispatch it',
|
|
25
|
+
settled: 'nothing: every recorded operation is applied, superseded or refused',
|
|
26
|
+
empty: 'nothing: no operation is recorded for this object',
|
|
27
|
+
inconsistent: 'a person looks at the object log; an event the log did not allow is recorded',
|
|
28
|
+
unreadable: 'a person looks at the object directory; nothing is decided until then',
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const NEEDS_PERSON = new Set(['conflicted', 'inconsistent', 'unreadable'])
|
|
32
|
+
|
|
33
|
+
function operationView(operation) {
|
|
34
|
+
return {
|
|
35
|
+
idempotencyKey: operation.idempotencyKey,
|
|
36
|
+
kind: operation.kind ?? null,
|
|
37
|
+
state: operation.state,
|
|
38
|
+
reason: operation.reason ?? null,
|
|
39
|
+
by: operation.by ?? null,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function conflictView({ objects, edits = [], scopeId } = {}) {
|
|
44
|
+
if (!Array.isArray(objects)) throw new TypeError('conflictView needs the list of object entries')
|
|
45
|
+
const openEdits = edits.filter((edit) => OPEN_EDIT_STATES.includes(edit.state) && (scopeId === undefined || edit.scopeId === scopeId))
|
|
46
|
+
const editsOf = (repoId, nodeId) => openEdits
|
|
47
|
+
.filter((edit) => edit.identity?.repoId === repoId && edit.identity?.nodeId === nodeId)
|
|
48
|
+
.map((edit) => ({ editId: edit.editId, scopeId: edit.scopeId, state: edit.state, lastCode: edit.lastResult?.code ?? null }))
|
|
49
|
+
.sort((left, right) => compareText(left.scopeId, right.scopeId) || compareText(left.editId, right.editId))
|
|
50
|
+
|
|
51
|
+
const entries = objects.map((object) => {
|
|
52
|
+
const state = OBJECT_VIEW_STATES.includes(object.state) ? object.state : 'unreadable'
|
|
53
|
+
const identity = object.identity ?? object
|
|
54
|
+
const repoId = identity.repoId ?? null
|
|
55
|
+
const nodeId = identity.nodeId ?? null
|
|
56
|
+
const operations = (object.operations ?? []).map(operationView)
|
|
57
|
+
const intentOutcomeUnknown = object.intentOutcomeUnknown === true || (object.intent !== undefined && object.intent !== null)
|
|
58
|
+
const needsPerson = NEEDS_PERSON.has(state) || intentOutcomeUnknown
|
|
59
|
+
return {
|
|
60
|
+
repoId, nodeId, state,
|
|
61
|
+
code: object.code ?? null,
|
|
62
|
+
sourceDigest: object.sourceDigest ?? null,
|
|
63
|
+
leaseHeld: object.leaseHeld === true || (object.lease !== undefined && object.lease !== null),
|
|
64
|
+
intentOutcomeUnknown,
|
|
65
|
+
operations,
|
|
66
|
+
conflictedOperations: operations.filter((operation) => operation.state === 'conflicted').map((operation) => operation.idempotencyKey),
|
|
67
|
+
pendingEdits: repoId === null ? [] : editsOf(repoId, nodeId),
|
|
68
|
+
needsPerson,
|
|
69
|
+
next: intentOutcomeUnknown && !NEEDS_PERSON.has(state) ? 'an apply was started and its outcome is not recorded; the next lease holder reads the source and decides' : NEXT[state],
|
|
70
|
+
}
|
|
71
|
+
}).sort((left, right) => compareText(left.repoId ?? '', right.repoId ?? '') || compareText(left.nodeId ?? '', right.nodeId ?? ''))
|
|
72
|
+
|
|
73
|
+
const byState = Object.fromEntries(OBJECT_VIEW_STATES.map((state) => [state, entries.filter((entry) => entry.state === state).length]))
|
|
74
|
+
return {
|
|
75
|
+
schema: CONFLICT_VIEW_SCHEMA,
|
|
76
|
+
scopeId: scopeId ?? null,
|
|
77
|
+
objects: entries,
|
|
78
|
+
summary: { total: entries.length, byState, needsPerson: entries.filter((entry) => entry.needsPerson).length, openEdits: openEdits.length },
|
|
79
|
+
needsPerson: entries.some((entry) => entry.needsPerson),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Reads the object index (a cache the store rebuilds from the events where
|
|
84
|
+
// it is not current) and the pending edit records of the workspace.
|
|
85
|
+
export function readConflictView({ workspaceRoot, workspaceId, repositoryRoots, clock, scopeId, objectStore = openObjectStore }) {
|
|
86
|
+
const store = objectStore({ stateRoot: workspaceRoot, workspaceId, repositoryRoots, clock })
|
|
87
|
+
const { objects, foreign } = store.list(scopeId === undefined ? {} : { scopeId })
|
|
88
|
+
const edits = createMaintenanceStateStore({ workspaceRoot, workspaceId }).readPendingEdits().edits
|
|
89
|
+
const view = conflictView({ objects, edits, scopeId })
|
|
90
|
+
return { ...view, foreign }
|
|
91
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { isoTime } from '../../../runtime/obsidian/documents.mjs'
|
|
4
|
+
import { refuse } from '../../../runtime/obsidian/errors.mjs'
|
|
5
|
+
import { protectedRoots, readMachineSettings } from '../../../runtime/obsidian/machine-settings.mjs'
|
|
6
|
+
import { resolveScope } from '../../../runtime/obsidian/opening.mjs'
|
|
7
|
+
import { DEFAULT_ELIGIBILITY, createProductionSeams } from '../../../runtime/obsidian/pipeline.mjs'
|
|
8
|
+
import { createMaintenanceStateStore } from '../../../runtime/obsidian/state-store.mjs'
|
|
9
|
+
import { readConflictView } from './conflict-view.mjs'
|
|
10
|
+
import { runPolicySetup } from './policy-setup.mjs'
|
|
11
|
+
import { listSelectionStates, readSelectionState, writeSelectionState } from './selection-state.mjs'
|
|
12
|
+
import { resolveSelection } from './selection.mjs'
|
|
13
|
+
|
|
14
|
+
// The `obsidian` command operations of selection, conflict state and policy
|
|
15
|
+
// setup, registered on the command's operation registry the way AOP-1 laid
|
|
16
|
+
// out: a contribution `{ id, register({ operations }) }`. Nothing in the
|
|
17
|
+
// command dispatcher is edited. The built-in `scope`, `policy` and `mode`
|
|
18
|
+
// names are reserved, so these are `selection`, `conflicts` and
|
|
19
|
+
// `apply-policy`.
|
|
20
|
+
//
|
|
21
|
+
// Every operation is noninteractive and read-only towards sources, vaults
|
|
22
|
+
// and the app: `selection persist` writes the workspace's private selection
|
|
23
|
+
// state, `apply-policy create|revoke` write the private policy files through
|
|
24
|
+
// AOP-1's installer and revocation, and nothing else writes at all.
|
|
25
|
+
|
|
26
|
+
export const SELECTION_CONTRIBUTION_ID = 'atelier.selection-ui'
|
|
27
|
+
const EXIT = Object.freeze({ ok: 0, notSuccess: 3 })
|
|
28
|
+
const MAX_REQUEST_BYTES = 64 * 1024
|
|
29
|
+
|
|
30
|
+
// The canonical snapshot selectScope reads, built exactly as the engine
|
|
31
|
+
// builds it: the canonical graph with fail-closed eligibility, and the
|
|
32
|
+
// declared relations whose both endpoints the census knows.
|
|
33
|
+
function snapshotNow({ project, seams, eligibility }) {
|
|
34
|
+
const graph = seams.buildGraph({ project, eligibility })
|
|
35
|
+
const known = new Set(graph.nodes.map((node) => node.id))
|
|
36
|
+
return { nodes: graph.nodes, edges: graph.edges.filter((edge) => known.has(edge.source) && known.has(edge.target)) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// FILE is resolved from the working directory of the process, as the built-in `policy install FILE` resolves its file.
|
|
40
|
+
function readRequest(value) {
|
|
41
|
+
if (value === undefined) refuse('usage', 'apply-policy create FILE')
|
|
42
|
+
const file = path.resolve(value)
|
|
43
|
+
try {
|
|
44
|
+
if (fs.statSync(file).size > MAX_REQUEST_BYTES) refuse('invalid-apply-policy', 'the request file is larger than a request can be')
|
|
45
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (typeof error?.code === 'string' && error.name === 'ObsidianMaintenanceRefusal') throw error
|
|
48
|
+
return refuse('invalid-apply-policy', 'the request file cannot be read as JSON', { cause: error.code ?? 'not-json' })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createSelectionOperation({ seams = createProductionSeams(), eligibility = DEFAULT_ELIGIBILITY } = {}) {
|
|
53
|
+
return {
|
|
54
|
+
name: 'selection',
|
|
55
|
+
summary: 'resolve ID | persist ID [allow-empty] | show ID | list The exact selected set of a declared view, its note paths and, for a focus, the graph query; persisted in Atelier state.',
|
|
56
|
+
async run({ args, readable, writable, clock }) {
|
|
57
|
+
const [sub = 'list', requested, option] = args
|
|
58
|
+
if (option !== undefined && !(sub === 'persist' && option === 'allow-empty')) refuse('usage', 'selection persist ID [allow-empty]')
|
|
59
|
+
if (sub === 'list') {
|
|
60
|
+
const { workspace, workspaceId } = readable()
|
|
61
|
+
const selections = workspace === null ? [] : listSelectionStates({ workspaceRoot: workspace.workspaceRoot, workspaceId })
|
|
62
|
+
return { exit: EXIT.ok, document: { selections }, human: selections.length === 0 ? ['no persisted selection'] : selections.map((item) => `${item.scopeId}\t${item.mode}\t${item.updatedAt}${item.focus ? `\t${item.focus.paths.length} focused note(s)` : ''}`) }
|
|
63
|
+
}
|
|
64
|
+
if (!['resolve', 'persist', 'show'].includes(sub)) refuse('usage', 'selection resolve ID | persist ID | show ID | list')
|
|
65
|
+
if (sub === 'show') {
|
|
66
|
+
const { enablement, workspace, workspaceId } = readable()
|
|
67
|
+
const scopeId = resolveScope(enablement, requested)
|
|
68
|
+
const selection = workspace === null ? null : readSelectionState({ workspaceRoot: workspace.workspaceRoot, workspaceId, scopeId })
|
|
69
|
+
return { exit: selection === null ? EXIT.notSuccess : EXIT.ok, document: { scopeId, persisted: selection !== null, selection }, human: [selection === null ? `no persisted selection for ${scopeId}` : JSON.stringify(selection, null, 2)] }
|
|
70
|
+
}
|
|
71
|
+
const { project, enablement, workspace, repositoryRoots, now } = sub === 'persist' ? writable() : { ...readable(), repositoryRoots: null, now: isoTime(clock) }
|
|
72
|
+
const scopeId = resolveScope(enablement, requested)
|
|
73
|
+
const scope = enablement.scopes.find((item) => item.scopeId === scopeId)
|
|
74
|
+
const usable = workspace !== null && workspace.workspaceRoot !== null
|
|
75
|
+
const machine = usable ? readMachineSettings(workspace) : null
|
|
76
|
+
const profile = seams.profileFor({ project, workspaceId: workspace?.workspaceId ?? 'ws-unprepared', audienceAllow: machine?.audienceAllow ?? [] })
|
|
77
|
+
const canonicalSnapshot = snapshotNow({ project, seams, eligibility })
|
|
78
|
+
const pathRegistry = usable ? createMaintenanceStateStore(workspace).readPathRegistry() : null
|
|
79
|
+
// Explicit empty is an honest answer of `resolve`; `persist` of an empty selection is refused and asks for it by name.
|
|
80
|
+
const selection = resolveSelection({ canonicalSnapshot, profile, scope, pathRegistry, allowEmpty: sub === 'resolve' || option === 'allow-empty' })
|
|
81
|
+
if (sub === 'resolve') {
|
|
82
|
+
return { exit: EXIT.ok, document: { scopeId, selection, persisted: false }, human: [`${scopeId}: ${selection.mode}, ${selection.nodes.length} note(s), ${selection.edges.length} edge(s)${selection.truncated ? ', expansion truncated' : ''}${selection.focus ? `; focus query ${selection.focus.query}` : ''}`] }
|
|
83
|
+
}
|
|
84
|
+
const written = writeSelectionState({ workspaceRoot: workspace.workspaceRoot, workspaceId: workspace.workspaceId, repositoryRoots, selection, now })
|
|
85
|
+
return { exit: EXIT.ok, document: { scopeId, selection, persisted: true, changed: written.changed, file: path.relative(workspace.workspaceRoot, written.file).split(path.sep).join('/') }, human: [`${scopeId}: persisted${written.changed ? '' : ' (unchanged)'}; ${selection.nodes.length} note(s)${selection.focus ? `; focus query ${selection.focus.query}` : ''}`, 'No file under .obsidian/ was written; apply a focus in the app.'] }
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createConflictsOperation() {
|
|
91
|
+
return {
|
|
92
|
+
name: 'conflicts',
|
|
93
|
+
summary: '[ID] The shared conflict state of edited objects, read from the arbitration record: what is conflicted, pending or unreadable, and what a person does next. Read-only.',
|
|
94
|
+
async run({ args, readable, clock }) {
|
|
95
|
+
const [requested] = args
|
|
96
|
+
const { project, enablement, workspace, workspaceId } = readable()
|
|
97
|
+
const scopeId = requested === undefined ? undefined : resolveScope(enablement, requested)
|
|
98
|
+
if (workspace === null) return { exit: EXIT.ok, document: { scopeId: scopeId ?? null, objects: [], summary: { total: 0 }, needsPerson: false, workspace: 'not-prepared' }, human: ['no private state yet: no edit was ever recorded'] }
|
|
99
|
+
const view = readConflictView({ workspaceRoot: workspace.workspaceRoot, workspaceId, repositoryRoots: protectedRoots(project), clock, scopeId })
|
|
100
|
+
return {
|
|
101
|
+
exit: EXIT.ok, document: view,
|
|
102
|
+
human: view.objects.length === 0 ? ['no recorded object'] : view.objects.map((object) => `${object.repoId}\t${object.nodeId}\t${object.state}${object.needsPerson ? '\tneeds a person' : ''}\t${object.next}`),
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function createApplyPolicyOperation() {
|
|
109
|
+
return {
|
|
110
|
+
name: 'apply-policy',
|
|
111
|
+
summary: 'create FILE | show | revoke Build, digest and install a manual or automatic apply policy from a JSON request; show it; revoke it durably.',
|
|
112
|
+
async run({ args, readable, writable }) {
|
|
113
|
+
const [action, value] = args
|
|
114
|
+
if (action === undefined || action === 'show') {
|
|
115
|
+
const { workspace } = readable()
|
|
116
|
+
if (workspace === null) return { exit: EXIT.ok, document: { ok: true, action: 'show', installed: false, policy: null, reference: null, automaticApply: { authorized: false, reason: 'machine-settings-absent' } }, human: ['no apply policy is installed'] }
|
|
117
|
+
const result = runPolicySetup({ action: 'show', workspace })
|
|
118
|
+
return { exit: result.ok ? EXIT.ok : EXIT.notSuccess, document: result, human: [result.ok ? (result.installed ? `${result.policy.policyId} v${result.policy.version} ${result.policy.mode} ${result.policy.status}; automatic apply ${result.automaticApply.authorized ? 'authorized' : `not authorized (${result.automaticApply.reason})`}` : 'no apply policy is installed') : `[${result.refusal.code}] ${result.refusal.message}`] }
|
|
119
|
+
}
|
|
120
|
+
if (action !== 'create' && action !== 'revoke') refuse('usage', 'apply-policy create FILE | show | revoke')
|
|
121
|
+
const input = action === 'create' ? readRequest(value) : {}
|
|
122
|
+
const { workspace, repositoryRoots, now } = writable()
|
|
123
|
+
const result = runPolicySetup({ action, input, workspace, repositoryRoots, now })
|
|
124
|
+
const human = result.ok
|
|
125
|
+
? [action === 'create' ? `installed ${result.policy.policyId} v${result.policy.version} (${result.policy.mode}, ${result.policy.status}); maintenance mode ${result.maintenanceMode}` : (result.revoked ? `revoked (${result.reason}); no queued edit is applied from now on, and the mode is manual` : 'no apply policy was installed')]
|
|
126
|
+
: [`[${result.refusal.code}] ${result.refusal.message}`]
|
|
127
|
+
return { exit: result.ok ? EXIT.ok : EXIT.notSuccess, document: result, human }
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createSelectionContribution(options = {}) {
|
|
133
|
+
return {
|
|
134
|
+
id: SELECTION_CONTRIBUTION_ID,
|
|
135
|
+
register({ operations }) {
|
|
136
|
+
operations.register(createSelectionOperation(options))
|
|
137
|
+
operations.register(createConflictsOperation())
|
|
138
|
+
operations.register(createApplyPolicyOperation())
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { refuse } from '../../../runtime/obsidian/errors.mjs'
|
|
3
|
+
|
|
4
|
+
// Focus: the full vault stays on disk and the native Graph view is filtered
|
|
5
|
+
// with a "Search files" query derived from the selected set. The query names
|
|
6
|
+
// every selected note by its exact vault-relative path, quoted and escaped
|
|
7
|
+
// the way the app's search syntax reads a quoted term, joined with OR.
|
|
8
|
+
//
|
|
9
|
+
// Nothing here writes a file. The query and the core-Bookmark payload are
|
|
10
|
+
// values a person, an agent or the acceptance procedure applies in the app;
|
|
11
|
+
// `.obsidian/workspace.json`, `.obsidian/graph.json` and
|
|
12
|
+
// `.obsidian/bookmarks.json` belong to the person and are never written by
|
|
13
|
+
// Atelier. Whether the installed app applies the query as built is an
|
|
14
|
+
// acceptance question (AP-02), not something this module can answer.
|
|
15
|
+
|
|
16
|
+
export const FOCUS_QUERY_VERSION = 'obsidian-graph-search-paths/v1'
|
|
17
|
+
export const FOCUS_BOOKMARK_TYPE = 'graph'
|
|
18
|
+
// The files under .obsidian/ the app's UI owns. Atelier reads none of them
|
|
19
|
+
// and writes none of them; a focus is applied through the app, never by
|
|
20
|
+
// editing these.
|
|
21
|
+
export const UI_OWNED_SETTINGS_FILES = Object.freeze(['workspace.json', 'graph.json', 'bookmarks.json'])
|
|
22
|
+
export const MAX_FOCUS_PATHS = 100000
|
|
23
|
+
|
|
24
|
+
// A quoted search term: backslash and double quote are escaped; anything
|
|
25
|
+
// that is not printable text cannot be typed into a search box and refuses.
|
|
26
|
+
export const FOCUS_QUERY_PRIMITIVES = Object.freeze({
|
|
27
|
+
escape(value) {
|
|
28
|
+
return value.replace(/[\\"]/g, (character) => `\\${character}`)
|
|
29
|
+
},
|
|
30
|
+
term(path) {
|
|
31
|
+
return `path:"${path}"`
|
|
32
|
+
},
|
|
33
|
+
join(terms) {
|
|
34
|
+
return terms.join(' OR ')
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
function normalizePath(value, index) {
|
|
39
|
+
if (typeof value !== 'string' || value === '') refuse('focus-path-unrepresentable', 'a focus path must be a non-empty string', { index })
|
|
40
|
+
const normalized = value.normalize('NFC')
|
|
41
|
+
// eslint-disable-next-line no-control-regex
|
|
42
|
+
if (/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/.test(normalized)) refuse('focus-path-unrepresentable', 'a focus path carries a control character and cannot be a search term', { index })
|
|
43
|
+
if (normalized.startsWith('/') || normalized.includes('\\') || normalized.split('/').some((part) => part === '' || part === '.' || part === '..')) {
|
|
44
|
+
refuse('focus-path-unrepresentable', 'a focus path must be a normalized vault-relative path', { index })
|
|
45
|
+
}
|
|
46
|
+
return normalized
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Test seam: the oracles in test/obsidian-acceptance.test.mjs substitute a
|
|
50
|
+
// builder that drops escaping to prove the escaping oracle can fail.
|
|
51
|
+
// Production uses buildFocusQuery.
|
|
52
|
+
export function createFocusQueryBuilderForOracleTests(primitives = FOCUS_QUERY_PRIMITIVES) {
|
|
53
|
+
const { escape, term, join } = { ...FOCUS_QUERY_PRIMITIVES, ...primitives }
|
|
54
|
+
return function buildFocusQuery(paths) {
|
|
55
|
+
if (!Array.isArray(paths)) refuse('focus-selection-empty', 'a focus needs the list of selected note paths')
|
|
56
|
+
if (paths.length === 0) refuse('focus-selection-empty', 'a focus with no selected note would filter nothing; it is refused, never widened to the whole vault')
|
|
57
|
+
if (paths.length > MAX_FOCUS_PATHS) refuse('focus-too-large', `a focus names at most ${MAX_FOCUS_PATHS} notes`, { count: paths.length })
|
|
58
|
+
const normalized = paths.map(normalizePath)
|
|
59
|
+
if (new Set(normalized).size !== normalized.length) refuse('duplicate-identity', 'a focus names a note path more than once')
|
|
60
|
+
// The order is the order given (canonical-id order from the selection), so the same selection is the same query.
|
|
61
|
+
const terms = normalized.map((path) => term(escape(path)))
|
|
62
|
+
const query = join(terms)
|
|
63
|
+
return {
|
|
64
|
+
version: FOCUS_QUERY_VERSION,
|
|
65
|
+
query,
|
|
66
|
+
queryDigest: `sha256:${createHash('sha256').update(query, 'utf8').digest('hex')}`,
|
|
67
|
+
paths: normalized,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const buildFocusQuery = createFocusQueryBuilderForOracleTests()
|
|
73
|
+
|
|
74
|
+
// The payload of a core-plugin graph bookmark that retains this focus. The
|
|
75
|
+
// app assigns the creation time when the bookmark is made there; nothing
|
|
76
|
+
// nondeterministic is part of the payload. Only the graph type is supported:
|
|
77
|
+
// core Bookmarks retain graph views, not local-graph views.
|
|
78
|
+
export function focusBookmarkPayload({ scopeId, query }) {
|
|
79
|
+
if (typeof scopeId !== 'string' || scopeId === '') refuse('invalid-scope', 'a focus bookmark names its scope')
|
|
80
|
+
if (typeof query !== 'string' || query === '') refuse('focus-selection-empty', 'a focus bookmark carries a non-empty query')
|
|
81
|
+
return { type: FOCUS_BOOKMARK_TYPE, title: `Atelier focus ${scopeId}`, options: { search: query } }
|
|
82
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Selection, focus, apply policy setup, conflict state and acceptance receipt
|
|
2
|
+
// validation for the Obsidian projection. Selection and focus are pure;
|
|
3
|
+
// selection state, policy setup and the conflict view read and write the
|
|
4
|
+
// workspace's private state through the runtime's own primitives; the receipt
|
|
5
|
+
// validator is schema validation only and closes no gate.
|
|
6
|
+
export { FOCUS_BOOKMARK_TYPE, FOCUS_QUERY_PRIMITIVES, FOCUS_QUERY_VERSION, MAX_FOCUS_PATHS, UI_OWNED_SETTINGS_FILES, buildFocusQuery, createFocusQueryBuilderForOracleTests, focusBookmarkPayload } from './focus.mjs'
|
|
7
|
+
export { EMPTY_SELECTION_REASONS, SCOPE_CONTRACT_SCHEMA, SELECTION_SCHEMA, resolveSelection, scopeDocumentOf } from './selection.mjs'
|
|
8
|
+
export { SELECTION_DIRECTORY, SELECTION_STATE_SCHEMA, assertWritableSelectionPath, listSelectionStates, readSelectionState, selectionStateDocument, validateSelectionState, writeSelectionState } from './selection-state.mjs'
|
|
9
|
+
export { CONFLICT_DISPOSITION, DEFAULT_MAX_BATCH_SIZE, DEFAULT_RETRY_BUDGET, POLICY_SETUP_ACTIONS, POLICY_SETUP_SCHEMA, buildApplyPolicy, dispatchGate, runPolicySetup } from './policy-setup.mjs'
|
|
10
|
+
export { CONFLICT_VIEW_SCHEMA, OBJECT_VIEW_STATES, conflictView, readConflictView } from './conflict-view.mjs'
|
|
11
|
+
export { ACCEPTANCE_KINDS, RECEIPT_EXT_KEY, RECEIPT_GATES, RECEIPT_GATE_IDS, RECEIPT_RULES, RECEIPT_VALIDATION_LABEL, createReceiptValidatorForOracleTests, receiptRequirementsFor, validateAcceptanceReceipt } from './receipt.mjs'
|
|
12
|
+
export { SELECTION_CONTRIBUTION_ID, createApplyPolicyOperation, createConflictsOperation, createSelectionContribution, createSelectionOperation } from './contribution.mjs'
|