@mnstry/atelier 0.2.0-alpha.6 → 0.2.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +176 -0
- package/README.md +59 -15
- package/SECURITY.md +6 -1
- package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
- package/contracts/atelier-coauthor.v1.schema.json +163 -0
- package/contracts/atelier-decision-request.v1.schema.json +476 -0
- package/contracts/atelier-decision-result.v1.schema.json +500 -0
- package/contracts/atelier-guide.v1.schema.json +227 -0
- package/contracts/atelier-intake.v1.schema.json +151 -0
- package/contracts/atelier-migration.v2.schema.json +61 -0
- package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
- package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
- package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
- package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
- package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
- package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
- package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
- package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
- package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
- package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
- package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
- package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
- package/contracts/atelier-presentation.v1.schema.json +1293 -0
- package/contracts/atelier-review.v1.schema.json +1601 -0
- package/contracts/atelier-skill-steward.v1.schema.json +692 -0
- package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
- package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
- package/contracts/atelier-vault-publication.v1.schema.json +58 -0
- package/docs/assurance-controls.md +9 -1
- package/docs/atelier-sync.md +5 -4
- package/docs/blocks/claims.md +15 -10
- package/docs/blocks/will-not-do.md +4 -1
- package/docs/coauthor-session.md +57 -0
- package/docs/connected-composition.md +75 -0
- package/docs/decisions.md +167 -0
- package/docs/exact-upgrades.md +159 -0
- package/docs/extension-pack-lifecycle.md +33 -0
- package/docs/guided-upgrades.md +99 -0
- package/docs/install.md +65 -9
- package/docs/intake-and-guides.md +65 -0
- package/docs/integration-and-review-plan.md +409 -0
- package/docs/integration-contract-decisions.md +22 -0
- package/docs/local-review.md +84 -0
- package/docs/local-services.md +227 -0
- package/docs/local-state.md +107 -0
- package/docs/obsidian-contract.md +687 -0
- package/docs/obsidian.md +387 -0
- package/docs/portable-presentation.md +328 -0
- package/docs/presentation-consumer-boundaries.md +75 -0
- package/docs/presentation-review-disposition.md +47 -0
- package/docs/project-command-inventory.md +38 -0
- package/docs/project-options.md +53 -0
- package/docs/release-engineering.md +88 -0
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +55 -2
- package/docs/vault-service.md +292 -0
- package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
- package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
- package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
- package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
- package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
- package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
- package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
- package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
- package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
- package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
- package/fixtures/atelier-intake/valid/source.v1.json +7 -0
- package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
- package/fixtures/atelier-review/valid/bound.v1.json +229 -0
- package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
- package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
- package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
- package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
- package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
- package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
- package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
- package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
- package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
- package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
- package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
- package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
- package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
- package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
- package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
- package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
- package/fixtures/decisions/request/valid/triage.v1.json +60 -0
- package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
- package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
- package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
- package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
- package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
- package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
- package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
- package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
- package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
- package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
- package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
- package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
- package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
- package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
- package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
- package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
- package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
- package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
- package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
- package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
- package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
- package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
- package/fixtures/obsidian/edits/workspace.json +65 -0
- package/fixtures/obsidian/materialization/expected-full.json +99 -0
- package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
- package/fixtures/obsidian/materialization/workspace.json +54 -0
- package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
- package/fixtures/ui/presentation/pane.v1.json +7 -0
- package/fixtures/ui/presentation/reference.v1.json +293 -0
- package/package.json +57 -6
- package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/claude/atelier-open/SKILL.md +5 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
- package/skills/claude/mnstry-readiness/SKILL.md +5 -0
- package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/codex/atelier-open/SKILL.md +5 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
- package/skills/codex/mnstry-readiness/SKILL.md +5 -0
- package/src/access/preview-enforcer.mjs +84 -0
- package/src/boundary/policy.mjs +57 -25
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +43 -3
- package/src/coauthor/session.mjs +161 -0
- package/src/coauthor/store.mjs +173 -0
- package/src/collaboration/event-ledger.mjs +16 -33
- package/src/collaboration/inspection-bundle.mjs +223 -0
- package/src/collaboration/review-contracts.mjs +25 -0
- package/src/collaboration/review-store.mjs +305 -0
- package/src/commands/coauthor.mjs +26 -0
- package/src/commands/config.mjs +6 -1
- package/src/commands/extension-pack.mjs +15 -8
- package/src/commands/init.mjs +44 -5
- package/src/commands/obsidian.mjs +368 -0
- package/src/commands/review.mjs +147 -0
- package/src/commands/setup.mjs +51 -14
- package/src/commands/skills.mjs +199 -0
- package/src/commands/sync.mjs +8 -1
- package/src/composition/registry.mjs +27 -0
- package/src/composition/wire.mjs +122 -0
- package/src/contracts/corpus.mjs +29 -0
- package/src/decisions/contracts.d.mts +96 -0
- package/src/decisions/contracts.mjs +321 -0
- package/src/disclosure/content-scan.mjs +13 -0
- package/src/export/atelier-export-contract.mjs +16 -1
- package/src/extension-packs/lifecycle.mjs +154 -0
- package/src/graph/graph.mjs +64 -2
- package/src/graph/knowledge-graph.mjs +548 -24
- package/src/guides/contracts.mjs +54 -0
- package/src/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +26 -27
- package/src/project/durable-state.mjs +161 -0
- package/src/project/file-class.mjs +166 -0
- package/src/project/private-state.mjs +11 -1
- package/src/projection/obsidian/contracts.mjs +571 -0
- package/src/projection/obsidian/edits/align.mjs +169 -0
- package/src/projection/obsidian/edits/apply.mjs +889 -0
- package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
- package/src/projection/obsidian/edits/contribution.mjs +108 -0
- package/src/projection/obsidian/edits/index.mjs +19 -0
- package/src/projection/obsidian/edits/object-identity.mjs +61 -0
- package/src/projection/obsidian/edits/object-store.mjs +517 -0
- package/src/projection/obsidian/edits/observe.mjs +123 -0
- package/src/projection/obsidian/edits/policy.mjs +123 -0
- package/src/projection/obsidian/edits/regions.mjs +648 -0
- package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
- package/src/projection/obsidian/materialize/index.mjs +7 -0
- package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
- package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
- package/src/projection/obsidian/materialize/settings.mjs +57 -0
- package/src/projection/obsidian/materialize/stage.mjs +27 -0
- package/src/projection/obsidian/proposals/adapter.mjs +572 -0
- package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
- package/src/projection/obsidian/proposals/contribution.mjs +55 -0
- package/src/projection/obsidian/proposals/index.mjs +23 -0
- package/src/projection/obsidian/proposals/observation.mjs +216 -0
- package/src/projection/obsidian/proposals/queue.mjs +277 -0
- package/src/projection/obsidian/proposals/router.mjs +168 -0
- package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
- package/src/projection/obsidian/publication/exchange.mjs +117 -0
- package/src/projection/obsidian/publication/index.mjs +5 -0
- package/src/projection/obsidian/publication/publisher.mjs +588 -0
- package/src/projection/obsidian/publication/test-seam.mjs +16 -0
- package/src/projection/obsidian/publication/transport.mjs +139 -0
- package/src/projection/obsidian/recovery/index.mjs +6 -0
- package/src/projection/obsidian/recovery/journal.mjs +134 -0
- package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
- package/src/projection/obsidian/recovery/restart.mjs +316 -0
- package/src/projection/obsidian/recovery/store.mjs +234 -0
- package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
- package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
- package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
- package/src/projection/obsidian/selection-ui/index.mjs +12 -0
- package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
- package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
- package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
- package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
- package/src/projection/project.mjs +28 -19
- package/src/readiness-protocols/evidence.mjs +282 -0
- package/src/readiness-protocols/runtime.mjs +4 -3
- package/src/readiness-protocols/source-read.mjs +39 -0
- package/src/runtime/obsidian/app-capability.mjs +110 -0
- package/src/runtime/obsidian/app-production-seams.mjs +96 -0
- package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
- package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
- package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
- package/src/runtime/obsidian/contributions.mjs +27 -0
- package/src/runtime/obsidian/documents.mjs +37 -0
- package/src/runtime/obsidian/enablement.mjs +53 -0
- package/src/runtime/obsidian/engine.mjs +532 -0
- package/src/runtime/obsidian/errors.mjs +14 -0
- package/src/runtime/obsidian/extension-points.mjs +103 -0
- package/src/runtime/obsidian/index.mjs +34 -0
- package/src/runtime/obsidian/lifecycle.mjs +258 -0
- package/src/runtime/obsidian/machine-settings.mjs +260 -0
- package/src/runtime/obsidian/observation.mjs +158 -0
- package/src/runtime/obsidian/opening.mjs +243 -0
- package/src/runtime/obsidian/pending-edits.mjs +141 -0
- package/src/runtime/obsidian/pipeline.mjs +122 -0
- package/src/runtime/obsidian/private-lock.mjs +156 -0
- package/src/runtime/obsidian/process-identity.mjs +58 -0
- package/src/runtime/obsidian/service-client.mjs +66 -0
- package/src/runtime/obsidian/service-main.mjs +89 -0
- package/src/runtime/obsidian/service-record.mjs +169 -0
- package/src/runtime/obsidian/service-server.mjs +121 -0
- package/src/runtime/obsidian/service.mjs +216 -0
- package/src/runtime/obsidian/startup-adapters.mjs +87 -0
- package/src/runtime/obsidian/state-store.mjs +180 -0
- package/src/runtime/obsidian/sync-notice.mjs +33 -0
- package/src/runtime/obsidian/tick-loop.mjs +84 -0
- package/src/runtime/obsidian/watchers.mjs +36 -0
- package/src/runtime/supervisor.mjs +16 -1
- package/src/server/local-sidecar.mjs +44 -0
- package/src/server/server.mjs +1 -0
- package/src/skills/steward.mjs +780 -0
- package/src/ui/presentation/browser.mjs +278 -0
- package/src/ui/presentation/contract.mjs +116 -0
- package/src/ui/presentation/index.mjs +7 -0
- package/src/ui/presentation/native.mjs +153 -0
- package/src/ui/presentation/proof.mjs +57 -0
- package/src/ui/presentation/schema-check.mjs +30 -0
- package/src/ui/presentation/schema.generated.mjs +1371 -0
- package/src/ui/presentation/state.mjs +43 -0
- package/src/ui/presentation/styles.mjs +67 -0
- package/src/ui/presentation/tokens.mjs +74 -0
- package/src/ui/presentation/web.mjs +85 -0
- package/src/ui/review-page.mjs +32 -0
- package/src/upgrade/explanation.mjs +33 -0
- package/src/upgrade/provenance.mjs +338 -0
- package/src/upgrade/transaction-files.mjs +103 -0
- package/src/upgrade/transaction.mjs +475 -0
- package/src/upgrade/upgrade.mjs +63 -28
- package/src/vault/hosts.mjs +21 -0
- package/src/vault/identity.mjs +19 -0
- package/src/vault/index.mjs +7 -0
- package/src/vault/interface.mjs +11 -0
- package/src/vault/metadata.mjs +31 -0
- package/src/vault/privacy.mjs +148 -0
- package/src/vault/probe.mjs +89 -0
- package/src/vault/service.mjs +131 -0
- package/src/vault/source.mjs +45 -0
- package/src/vault/storage.mjs +23 -0
- package/templates/distribution-workspace/.claude/launch.json +6 -2
- package/templates/distribution-workspace/gitignore +1 -0
- package/templates/distribution-workspace/project/README.md +2 -2
- package/templates/external-project-workspace/README.md +45 -0
- package/templates/external-project-workspace/answers.example.json +4 -0
- package/templates/external-project-workspace/atelier.project.json +40 -0
- package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
- package/templates/external-project-workspace/gitignore +8 -0
- package/templates/external-project-workspace/pack-lifecycle.json +14 -0
- package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
- package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
- package/templates/external-project-workspace/repo-access.v1.json +9 -0
- package/templates/external-project-workspace/source/README.md +11 -0
- package/templates/launch.json +6 -2
- package/templates/private-domain-workspace/.claude/launch.json +6 -2
- package/templates/private-domain-workspace/domain/README.md +2 -2
- package/templates/private-domain-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/.claude/launch.json +6 -2
- package/templates/shared-project-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/project/README.md +2 -2
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { canonicalize } from '../attestation/jcs.mjs'
|
|
6
|
+
import { buildGraph } from '../graph/graph.mjs'
|
|
7
|
+
import {
|
|
8
|
+
loadExtensionPacks,
|
|
9
|
+
createProtocolRegistry,
|
|
10
|
+
} from '../extension-packs/loader.mjs'
|
|
11
|
+
import { readRegularTextNoFollow } from '../project/private-state.mjs'
|
|
12
|
+
import { createCollaborationEventLedger } from '../collaboration/event-ledger.mjs'
|
|
13
|
+
import { inspectPackLifecycle } from '../extension-packs/lifecycle.mjs'
|
|
14
|
+
import { readLocalOverlay } from '../project/config.mjs'
|
|
15
|
+
import { readBoundedSource } from './source-read.mjs'
|
|
16
|
+
export { readBoundedSource } from './source-read.mjs'
|
|
17
|
+
import { validReviewArtifact } from '../collaboration/review-contracts.mjs'
|
|
18
|
+
import { buildReadinessRun, answerForField } from './runtime.mjs'
|
|
19
|
+
|
|
20
|
+
export const hashEvidence = (value) =>
|
|
21
|
+
`sha256:${crypto
|
|
22
|
+
.createHash('sha256')
|
|
23
|
+
.update(canonicalize(value))
|
|
24
|
+
.digest('hex')}`
|
|
25
|
+
const hashBytes = (value) =>
|
|
26
|
+
`sha256:${crypto.createHash('sha256').update(value).digest('hex')}`
|
|
27
|
+
const packageRoot = fileURLToPath(new URL('../../', import.meta.url))
|
|
28
|
+
function evaluatorInventory() {
|
|
29
|
+
const files = ['package.json']
|
|
30
|
+
function visit(relative) {
|
|
31
|
+
for (const name of fs
|
|
32
|
+
.readdirSync(path.join(packageRoot, relative))
|
|
33
|
+
.sort()) {
|
|
34
|
+
const next = path.posix.join(relative, name),
|
|
35
|
+
stat = fs.lstatSync(path.join(packageRoot, next))
|
|
36
|
+
if (stat.isSymbolicLink()) throw new Error('redirected evaluator input')
|
|
37
|
+
if (stat.isDirectory()) visit(next)
|
|
38
|
+
else if (stat.isFile() && /\.(mjs|json)$/.test(name)) files.push(next)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
visit('src')
|
|
42
|
+
visit('contracts')
|
|
43
|
+
return files
|
|
44
|
+
.sort()
|
|
45
|
+
.map((file) => ({
|
|
46
|
+
file,
|
|
47
|
+
digest: hashBytes(readRegularTextNoFollow(path.join(packageRoot, file))),
|
|
48
|
+
}))
|
|
49
|
+
}
|
|
50
|
+
const evaluator = hashEvidence(evaluatorInventory())
|
|
51
|
+
|
|
52
|
+
function currentInputs(project, protocolId, answers) {
|
|
53
|
+
if (
|
|
54
|
+
project.configPath &&
|
|
55
|
+
hashEvidence(JSON.parse(readRegularTextNoFollow(project.configPath))) !==
|
|
56
|
+
hashEvidence(project.config)
|
|
57
|
+
)
|
|
58
|
+
throw new Error('project config changed; resolve the project again')
|
|
59
|
+
if (hashEvidence(evaluatorInventory()) !== evaluator)
|
|
60
|
+
throw new Error('evaluator changed; restart before review')
|
|
61
|
+
if (
|
|
62
|
+
project.localOverlay &&
|
|
63
|
+
hashEvidence(readLocalOverlay({ configDir: project.configDir }).overlay) !==
|
|
64
|
+
hashEvidence(project.localOverlay.overlay)
|
|
65
|
+
)
|
|
66
|
+
throw new Error('local overlay changed; resolve again')
|
|
67
|
+
const loaded = loadExtensionPacks(project)
|
|
68
|
+
const packs = loaded.packs ?? loaded
|
|
69
|
+
const lifecycle = inspectPackLifecycle(project, packs)
|
|
70
|
+
if (!lifecycle.ok) throw new Error(lifecycle.errors.join('; '))
|
|
71
|
+
const registry = createProtocolRegistry({ packs })
|
|
72
|
+
const protocol = registry.protocolById(protocolId)
|
|
73
|
+
if (!protocol) throw new Error('protocol unavailable for evidence binding')
|
|
74
|
+
const graph = buildGraph(project)
|
|
75
|
+
if (graph.errors.length)
|
|
76
|
+
throw new Error('source graph is invalid; evidence capture refused')
|
|
77
|
+
if (graph.nodes.length > 256)
|
|
78
|
+
throw new Error('review evidence exceeds bounded source count')
|
|
79
|
+
const sources = [],
|
|
80
|
+
excerpts = new Map()
|
|
81
|
+
for (const node of graph.nodes) {
|
|
82
|
+
const repo = project.repos.find(
|
|
83
|
+
(entry) => entry.name === node.repo && !entry.external,
|
|
84
|
+
)
|
|
85
|
+
if (!repo?.path) throw new Error('source repository unavailable')
|
|
86
|
+
const source = readBoundedSource(repo.path, node.path)
|
|
87
|
+
const metadata = !/\.md$/i.test(node.path)
|
|
88
|
+
? readBoundedSource(repo.path, `${node.path}.kg.json`).digest
|
|
89
|
+
: null
|
|
90
|
+
excerpts.set(node.id, source.text.slice(0, 2000))
|
|
91
|
+
sources.push({
|
|
92
|
+
id: node.id,
|
|
93
|
+
repo: node.repo,
|
|
94
|
+
path: node.path,
|
|
95
|
+
audience: node.audience,
|
|
96
|
+
digest: source.digest,
|
|
97
|
+
metadata,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
sources.sort((a, b) =>
|
|
101
|
+
`${a.repo}/${a.path}`.localeCompare(`${b.repo}/${b.path}`, 'en'),
|
|
102
|
+
)
|
|
103
|
+
const claimEvidence = protocol.claimMappings.map((mapping) => {
|
|
104
|
+
const raw = answerForField(answers, mapping.evidence)
|
|
105
|
+
const references = (Array.isArray(raw) ? raw.flat(Infinity) : [raw])
|
|
106
|
+
.filter((value) => typeof value === 'string' && value.trim())
|
|
107
|
+
.map((value) => value.trim())
|
|
108
|
+
const resolved = [],
|
|
109
|
+
unresolved = []
|
|
110
|
+
for (const reference of references) {
|
|
111
|
+
const source = sources.find((item) => item.id === reference)
|
|
112
|
+
if (source)
|
|
113
|
+
resolved.push({
|
|
114
|
+
id: source.id,
|
|
115
|
+
repo: source.repo,
|
|
116
|
+
path: source.path,
|
|
117
|
+
digest: source.digest,
|
|
118
|
+
excerpt: excerpts.get(source.id),
|
|
119
|
+
})
|
|
120
|
+
else unresolved.push(reference)
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
mappingId: mapping.id,
|
|
124
|
+
sourceFields: mapping.sourceFields,
|
|
125
|
+
answers: Object.fromEntries(
|
|
126
|
+
mapping.sourceFields.map((field) => [
|
|
127
|
+
field,
|
|
128
|
+
answerForField(answers, field) ?? null,
|
|
129
|
+
]),
|
|
130
|
+
),
|
|
131
|
+
evidenceField: mapping.evidence,
|
|
132
|
+
references,
|
|
133
|
+
resolved,
|
|
134
|
+
unresolved,
|
|
135
|
+
status: !references.length
|
|
136
|
+
? 'missing'
|
|
137
|
+
: unresolved.length
|
|
138
|
+
? 'unresolved'
|
|
139
|
+
: 'linked-source',
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
const readPolicy = (file) =>
|
|
143
|
+
fs.existsSync(file) ? hashBytes(readRegularTextNoFollow(file)) : null
|
|
144
|
+
return {
|
|
145
|
+
schema: 'atelier-review-evidence@v1',
|
|
146
|
+
protocolId,
|
|
147
|
+
protocol,
|
|
148
|
+
protocolDigest: hashEvidence(protocol),
|
|
149
|
+
answers,
|
|
150
|
+
answersDigest: hashEvidence(answers),
|
|
151
|
+
configDigest: hashEvidence(project.config),
|
|
152
|
+
lifecycleDigest: hashEvidence(lifecycle),
|
|
153
|
+
policyDigest: hashEvidence({
|
|
154
|
+
access: readPolicy(project.repoAccessPath),
|
|
155
|
+
boundary: readPolicy(project.boundaryPolicyPath),
|
|
156
|
+
}),
|
|
157
|
+
packs: packs
|
|
158
|
+
.map(({ id, version, digest }) => ({ id, version, digest }))
|
|
159
|
+
.sort((a, b) => a.id.localeCompare(b.id, 'en')),
|
|
160
|
+
evaluator: { id: 'answer-completeness/v1', digest: evaluator },
|
|
161
|
+
sources,
|
|
162
|
+
claimEvidence,
|
|
163
|
+
limitations: [
|
|
164
|
+
'Answers are operator assertions; source presence does not substantiate each claim.',
|
|
165
|
+
'No calibrated evidence confidence or runtime acceptance is established.',
|
|
166
|
+
],
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function captureReviewEvidence(project, protocolId, answers) {
|
|
171
|
+
const first = currentInputs(project, protocolId, answers)
|
|
172
|
+
const second = currentInputs(project, protocolId, answers)
|
|
173
|
+
const digest = hashEvidence(first)
|
|
174
|
+
if (digest !== hashEvidence(second))
|
|
175
|
+
throw new Error('review inputs changed during capture')
|
|
176
|
+
const snapshot = { ...first, digest }
|
|
177
|
+
if (!validReviewArtifact('evidence', snapshot))
|
|
178
|
+
throw new Error('evidence contract invalid or exceeds bounds')
|
|
179
|
+
return snapshot
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function evidenceLedger(project) {
|
|
183
|
+
return createCollaborationEventLedger({
|
|
184
|
+
workspaceRoot: project.configDir,
|
|
185
|
+
ledgerPath: path.join(
|
|
186
|
+
project.configDir,
|
|
187
|
+
'.atelier-local/review/evidence.ndjson',
|
|
188
|
+
),
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function recordReviewEvidence(project, run) {
|
|
193
|
+
const snapshot = captureReviewEvidence(project, run.protocolId, run.answers)
|
|
194
|
+
if (
|
|
195
|
+
hashEvidence(
|
|
196
|
+
buildReadinessRun({
|
|
197
|
+
project,
|
|
198
|
+
protocol: snapshot.protocol,
|
|
199
|
+
answers: run.answers,
|
|
200
|
+
createdAt: run.createdAt,
|
|
201
|
+
}),
|
|
202
|
+
) !== hashEvidence(run)
|
|
203
|
+
)
|
|
204
|
+
throw new Error('run no longer matches its current protocol and evaluator')
|
|
205
|
+
const payload = {
|
|
206
|
+
schema: 'atelier-bound-run@v1',
|
|
207
|
+
run,
|
|
208
|
+
runDigest: hashEvidence(run),
|
|
209
|
+
snapshot,
|
|
210
|
+
}
|
|
211
|
+
if (!validBoundRun(payload)) throw new Error('bound run contract invalid')
|
|
212
|
+
const ledger = evidenceLedger(project)
|
|
213
|
+
const existing = ledger.eventsFor(run.runId)
|
|
214
|
+
if (!existing.ok) throw new Error(existing.error)
|
|
215
|
+
if (existing.events.length) {
|
|
216
|
+
if (hashEvidence(existing.events[0].payload) !== hashEvidence(payload))
|
|
217
|
+
throw new Error('run identity already has different evidence')
|
|
218
|
+
return existing.events[0].payload
|
|
219
|
+
}
|
|
220
|
+
const result = ledger.append({
|
|
221
|
+
aggregateId: run.runId,
|
|
222
|
+
expectedVersion: 0,
|
|
223
|
+
type: 'run-bound',
|
|
224
|
+
actor: 'local-runtime',
|
|
225
|
+
payload,
|
|
226
|
+
})
|
|
227
|
+
if (!result.ok) throw new Error(result.error)
|
|
228
|
+
return payload
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function validBoundRun(payload) {
|
|
232
|
+
if (!validReviewArtifact('bound', payload)) return false
|
|
233
|
+
const { digest, ...body } = payload.snapshot
|
|
234
|
+
const { run, snapshot } = payload
|
|
235
|
+
return (
|
|
236
|
+
payload.runDigest === hashEvidence(run) &&
|
|
237
|
+
digest === hashEvidence(body) &&
|
|
238
|
+
snapshot.protocolDigest === hashEvidence(snapshot.protocol) &&
|
|
239
|
+
snapshot.answersDigest === hashEvidence(snapshot.answers) &&
|
|
240
|
+
snapshot.answersDigest === hashEvidence(run.answers) &&
|
|
241
|
+
snapshot.protocol.id === run.protocolId &&
|
|
242
|
+
snapshot.protocolId === run.protocolId &&
|
|
243
|
+
snapshot.claimEvidence.length === run.claims.length &&
|
|
244
|
+
snapshot.claimEvidence.every(
|
|
245
|
+
(entry, index) =>
|
|
246
|
+
entry.mappingId === snapshot.protocol.claimMappings[index]?.id,
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function loadBoundRun(project, runId) {
|
|
252
|
+
const result = evidenceLedger(project).eventsFor(runId)
|
|
253
|
+
if (!result.ok) return result
|
|
254
|
+
if (result.events.length !== 1 || result.events[0].type !== 'run-bound')
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
status: 409,
|
|
258
|
+
error: 'run lacks immutable review evidence',
|
|
259
|
+
}
|
|
260
|
+
const payload = result.events[0].payload
|
|
261
|
+
const { digest, ...body } = payload.snapshot ?? {}
|
|
262
|
+
if (
|
|
263
|
+
!validBoundRun(payload) ||
|
|
264
|
+
payload.schema !== 'atelier-bound-run@v1' ||
|
|
265
|
+
payload.run?.runId !== runId ||
|
|
266
|
+
payload.runDigest !== hashEvidence(payload.run) ||
|
|
267
|
+
digest !== hashEvidence(body)
|
|
268
|
+
)
|
|
269
|
+
return { ok: false, status: 422, error: 'run evidence is invalid' }
|
|
270
|
+
return { ok: true, status: 200, ...payload }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function currentRunEligibility(project, bound) {
|
|
274
|
+
try {
|
|
275
|
+
return (
|
|
276
|
+
captureReviewEvidence(project, bound.run.protocolId, bound.run.answers)
|
|
277
|
+
.digest === bound.snapshot.digest
|
|
278
|
+
)
|
|
279
|
+
} catch {
|
|
280
|
+
return false
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -73,7 +73,7 @@ function nestedAnswer(value, parts) {
|
|
|
73
73
|
return nestedAnswer(value[head], rest)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function answerForField(answers, fieldId) {
|
|
76
|
+
export function answerForField(answers, fieldId) {
|
|
77
77
|
for (const key of pathVariants(fieldId)) {
|
|
78
78
|
if (Object.hasOwn(answers, key)) return answers[key]
|
|
79
79
|
}
|
|
@@ -119,9 +119,8 @@ export function claimsForProtocolRun({ protocol, answers, projectName, runId, cr
|
|
|
119
119
|
provider: 'atelier-readiness',
|
|
120
120
|
status: 'proposed',
|
|
121
121
|
promoted: false,
|
|
122
|
-
confidence: Object.values(answers).some(answerPresent) ? 0.7 : 0.2,
|
|
123
122
|
evidence: [`readiness-run:${runId}`],
|
|
124
|
-
notes: [`Proposed ${mapping.id} from ${protocol.title}
|
|
123
|
+
notes: [`Proposed ${mapping.id} from ${protocol.title}.`, 'Answer presence is not evidence confidence; human judgment is required.'],
|
|
125
124
|
createdAt,
|
|
126
125
|
}
|
|
127
126
|
})
|
|
@@ -147,6 +146,7 @@ export function buildReadinessRun({ project, protocol, answers = {}, createdAt =
|
|
|
147
146
|
claims: claimsForProtocolRun({ protocol, answers: normalizedAnswers, projectName, runId, createdAt }),
|
|
148
147
|
safety: {
|
|
149
148
|
runtimeMutation: false,
|
|
149
|
+
runtimeImport: false,
|
|
150
150
|
canonicalWrites: false,
|
|
151
151
|
claimOnly: true,
|
|
152
152
|
storage: 'ignored-local',
|
|
@@ -367,6 +367,7 @@ export function buildTenantPacket(project, { runs = listProtocolRuns(project), r
|
|
|
367
367
|
ext: {
|
|
368
368
|
'mnstry.atelier': {
|
|
369
369
|
extensionPacks: packContribution,
|
|
370
|
+
scoreMeaning: 'Legacy readinessScore measures completed input dimensions; it is not evidence confidence or runtime admission.',
|
|
370
371
|
},
|
|
371
372
|
},
|
|
372
373
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { openRegularFileNoFollow } from '../project/private-state.mjs'
|
|
5
|
+
const hashBytes = (value) =>
|
|
6
|
+
`sha256:${crypto.createHash('sha256').update(value).digest('hex')}`
|
|
7
|
+
|
|
8
|
+
export function readBoundedSource(root, relative) {
|
|
9
|
+
if (
|
|
10
|
+
typeof relative !== 'string' ||
|
|
11
|
+
path.isAbsolute(relative) ||
|
|
12
|
+
relative.split(/[\\/]/).some((part) => part === '..' || part === '.git')
|
|
13
|
+
)
|
|
14
|
+
throw new Error('source reference escapes declared boundary')
|
|
15
|
+
const realRoot = fs.realpathSync(root)
|
|
16
|
+
const file = path.resolve(realRoot, relative)
|
|
17
|
+
let cursor = realRoot
|
|
18
|
+
for (const part of path.relative(realRoot, file).split(path.sep)) {
|
|
19
|
+
cursor = path.join(cursor, part)
|
|
20
|
+
if (fs.lstatSync(cursor).isSymbolicLink())
|
|
21
|
+
throw new Error('redirected source reference refused')
|
|
22
|
+
}
|
|
23
|
+
const descriptor = openRegularFileNoFollow(file)
|
|
24
|
+
try {
|
|
25
|
+
const before = fs.fstatSync(descriptor)
|
|
26
|
+
if (before.size > 1024 * 1024)
|
|
27
|
+
throw new Error('source exceeds review byte bound')
|
|
28
|
+
const bytes = fs.readFileSync(descriptor)
|
|
29
|
+
const after = fs.fstatSync(descriptor)
|
|
30
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs)
|
|
31
|
+
throw new Error('source changed during read')
|
|
32
|
+
return {
|
|
33
|
+
digest: hashBytes(bytes),
|
|
34
|
+
text: new TextDecoder('utf-8', { fatal: true }).decode(bytes),
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
fs.closeSync(descriptor)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { refuse } from './errors.mjs'
|
|
2
|
+
|
|
3
|
+
// Whether the installed Obsidian may be published through and opened.
|
|
4
|
+
//
|
|
5
|
+
// Nothing here looks at this machine. What is installed is learned through an
|
|
6
|
+
// injected `appProbe`:
|
|
7
|
+
//
|
|
8
|
+
// appProbe.inspect() -> { installed, cli, running, version }
|
|
9
|
+
// appProbe.vaultState({ vaultRoot }) -> { answered, indexReady }
|
|
10
|
+
//
|
|
11
|
+
// `installed` and `cli` are booleans, `running` is true, false or null (not
|
|
12
|
+
// known) and `version` is the app's own version text or null. The production
|
|
13
|
+
// probe is a separate module that only the real command-line entries load;
|
|
14
|
+
// every test passes its own.
|
|
15
|
+
|
|
16
|
+
// The publication protocol sets the view's undocumented `lastSavedData` field.
|
|
17
|
+
// 1.13.7 is the only app version that protocol was proven on, so it is the
|
|
18
|
+
// floor. Raising the ceiling needs the protocol cases re-run on that release.
|
|
19
|
+
export const MINIMUM_APP_VERSION = '1.13.7'
|
|
20
|
+
|
|
21
|
+
export const APP_OUTCOMES = Object.freeze(['qualified', 'app-missing', 'app-cli-unavailable', 'app-version-unsupported'])
|
|
22
|
+
|
|
23
|
+
const VERSION = /^v?(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})(?:-([0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*))?(?:\+[0-9A-Za-z.-]+)?(?=$|[\s(])/
|
|
24
|
+
|
|
25
|
+
// { major, minor, patch, prerelease: string[] } or null. Only a version at the
|
|
26
|
+
// very start of the text counts; anything else is not a version.
|
|
27
|
+
export function parseAppVersion(text) {
|
|
28
|
+
if (typeof text !== 'string' || text.length > 200) return null
|
|
29
|
+
const match = VERSION.exec(text.trim())
|
|
30
|
+
if (!match) return null
|
|
31
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] === undefined ? [] : match[4].split('.') }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function comparePrerelease(left, right) {
|
|
35
|
+
// No prerelease ranks above any prerelease of the same version.
|
|
36
|
+
if (left.length === 0 || right.length === 0) return left.length === right.length ? 0 : left.length === 0 ? 1 : -1
|
|
37
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
38
|
+
if (left[index] === undefined) return -1
|
|
39
|
+
if (right[index] === undefined) return 1
|
|
40
|
+
const [a, b] = [left[index], right[index]]
|
|
41
|
+
const [aNumeric, bNumeric] = [/^\d+$/.test(a), /^\d+$/.test(b)]
|
|
42
|
+
if (aNumeric && bNumeric) { if (Number(a) !== Number(b)) return Number(a) < Number(b) ? -1 : 1 } else if (aNumeric !== bNumeric) return aNumeric ? -1 : 1
|
|
43
|
+
else if (a !== b) return a < b ? -1 : 1
|
|
44
|
+
}
|
|
45
|
+
return 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Pure. -1, 0 or 1; null when either side is not a version.
|
|
49
|
+
export function compareAppVersions(left, right) {
|
|
50
|
+
const [a, b] = [parseAppVersion(left), parseAppVersion(right)]
|
|
51
|
+
if (a === null || b === null) return null
|
|
52
|
+
for (const part of ['major', 'minor', 'patch']) if (a[part] !== b[part]) return a[part] < b[part] ? -1 : 1
|
|
53
|
+
return comparePrerelease(a.prerelease, b.prerelease)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Pure. A version that cannot be read does not meet the floor.
|
|
57
|
+
export function meetsMinimumAppVersion(version, floor = MINIMUM_APP_VERSION) {
|
|
58
|
+
const order = compareAppVersions(version, floor)
|
|
59
|
+
return order !== null && order >= 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Pure. `requireVersion: false` lets an installed app that is positively not
|
|
63
|
+
// running pass without a version: nothing is published through an app that
|
|
64
|
+
// does not run, and the version is asked for again once it does.
|
|
65
|
+
export function qualifyApp(observation, { requireVersion = true, floor = MINIMUM_APP_VERSION } = {}) {
|
|
66
|
+
const seen = observation !== null && typeof observation === 'object' ? observation : {}
|
|
67
|
+
const base = { floor, version: typeof seen.version === 'string' ? seen.version.slice(0, 80) : null, running: seen.running === true ? true : seen.running === false ? false : null }
|
|
68
|
+
if (seen.installed !== true) return { ...base, outcome: 'app-missing', reason: 'no-app-found' }
|
|
69
|
+
if (seen.cli !== true) return { ...base, outcome: 'app-cli-unavailable', reason: 'cli-capability-absent' }
|
|
70
|
+
if (base.version === null) {
|
|
71
|
+
if (!requireVersion && base.running === false) return { ...base, outcome: 'qualified', reason: 'app-not-running-version-not-needed', versionChecked: false }
|
|
72
|
+
return { ...base, outcome: 'app-version-unsupported', reason: 'version-unknown' }
|
|
73
|
+
}
|
|
74
|
+
if (parseAppVersion(base.version) === null) return { ...base, outcome: 'app-version-unsupported', reason: 'version-unreadable' }
|
|
75
|
+
if (!meetsMinimumAppVersion(base.version, floor)) return { ...base, outcome: 'app-version-unsupported', reason: 'below-minimum-version' }
|
|
76
|
+
return { ...base, outcome: 'qualified', reason: 'meets-minimum-version', versionChecked: true }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function inspectApp(appProbe) {
|
|
80
|
+
if (typeof appProbe?.inspect !== 'function') throw new TypeError('an appProbe with inspect() is required')
|
|
81
|
+
try { return await appProbe.inspect() } catch { return { installed: false, cli: false, running: null, version: null } }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The adapter factory of a service that reaches a real app. The editor adapter
|
|
85
|
+
// is constructed only after the app qualified; otherwise the factory refuses,
|
|
86
|
+
// typed, and the engine records that reason and publishes nothing. The answer
|
|
87
|
+
// is remembered briefly so a tick over many views asks once.
|
|
88
|
+
export function createQualifiedAdapterFactory({ appProbe, createAdapter, floor = MINIMUM_APP_VERSION, maxAgeMs = 10_000, now = () => Date.now() } = {}) {
|
|
89
|
+
if (typeof appProbe?.inspectSync !== 'function') throw new TypeError('the qualified adapter factory needs an appProbe with inspectSync()')
|
|
90
|
+
if (typeof createAdapter !== 'function') throw new TypeError('the qualified adapter factory needs createAdapter')
|
|
91
|
+
let last = null
|
|
92
|
+
const qualification = () => {
|
|
93
|
+
if (last === null || now() - last.at > maxAgeMs) {
|
|
94
|
+
let observation
|
|
95
|
+
try { observation = appProbe.inspectSync() } catch { observation = null }
|
|
96
|
+
last = { at: now(), result: qualifyApp(observation, { requireVersion: false, floor }) }
|
|
97
|
+
}
|
|
98
|
+
return last.result
|
|
99
|
+
}
|
|
100
|
+
const factory = (input) => {
|
|
101
|
+
const result = qualification()
|
|
102
|
+
// No app at all is not an unqualified app: the publisher's own path needs none, and its adapter finds no process.
|
|
103
|
+
if (result.outcome !== 'qualified' && result.outcome !== 'app-missing') refuse(result.outcome, 'the installed Obsidian does not qualify; nothing is published through it', { reason: result.reason, floor: result.floor, version: result.version })
|
|
104
|
+
return createAdapter(input)
|
|
105
|
+
}
|
|
106
|
+
factory.qualification = qualification
|
|
107
|
+
// What was last learned, without asking again: for a status answer.
|
|
108
|
+
factory.lastQualification = () => last?.result ?? null
|
|
109
|
+
return factory
|
|
110
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { execFile, execFileSync } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { defaultCliPath, defaultObsidianProcessProbe } from '../../projection/obsidian/publication/transport.mjs'
|
|
5
|
+
|
|
6
|
+
// The production seams of `obsidian open` and of the service's adapter
|
|
7
|
+
// factory: the only code that asks the installed Obsidian anything or asks the
|
|
8
|
+
// operating system to open it.
|
|
9
|
+
//
|
|
10
|
+
// This module is imported by exactly two places, both real command-line
|
|
11
|
+
// entries and both behind an explicit `--adapter=obsidian-cli`: the service
|
|
12
|
+
// entry (service-main.mjs) and the command entry (src/commands/obsidian.mjs,
|
|
13
|
+
// `production: true`). No test imports it, and importing it leaves a trace
|
|
14
|
+
// below that the test suite asserts is absent.
|
|
15
|
+
//
|
|
16
|
+
// Status: written against the documented command-line interface and NOT yet
|
|
17
|
+
// exercised against a running app. The acceptance work qualifies it on an
|
|
18
|
+
// isolated host before any of it is relied on; until then every answer it
|
|
19
|
+
// cannot establish is the failing one.
|
|
20
|
+
globalThis[Symbol.for('mnstry.atelier.obsidian.production-seams-loaded')] = true
|
|
21
|
+
|
|
22
|
+
const CLI_TIMEOUT_MS = 5000
|
|
23
|
+
|
|
24
|
+
function installedAppPath(platform) {
|
|
25
|
+
if (platform === 'darwin') return '/Applications/Obsidian.app'
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cliExists(cliPath) {
|
|
30
|
+
if (!path.isAbsolute(cliPath)) return null // found through PATH or not at all: only running it tells
|
|
31
|
+
try { return fs.statSync(cliPath).isFile() } catch { return false }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const observationOf = ({ platform, cliPath, processes, versionText }) => {
|
|
35
|
+
const appPath = installedAppPath(platform)
|
|
36
|
+
const present = cliExists(cliPath)
|
|
37
|
+
const answered = typeof versionText === 'string' && versionText.trim() !== ''
|
|
38
|
+
return {
|
|
39
|
+
installed: appPath === null ? (present !== false && (answered || present === true)) : fs.existsSync(appPath),
|
|
40
|
+
cli: present === true || answered,
|
|
41
|
+
running: processes === 'running' ? true : processes === 'absent' ? false : null,
|
|
42
|
+
version: answered ? versionText.trim() : null,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createProductionAppProbe({ platform = process.platform, env = process.env, cliPath = defaultCliPath(platform) } = {}) {
|
|
47
|
+
const options = { env, timeout: CLI_TIMEOUT_MS, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 1024 * 1024 }
|
|
48
|
+
return {
|
|
49
|
+
// For the service's adapter factory, which is synchronous. The version is asked for only while an app runs.
|
|
50
|
+
inspectSync() {
|
|
51
|
+
const processes = defaultObsidianProcessProbe({ platform })
|
|
52
|
+
let versionText = null
|
|
53
|
+
if (processes !== 'absent') { try { versionText = execFileSync(cliPath, ['version'], { ...options, stdio: ['ignore', 'pipe', 'ignore'] }) } catch { versionText = null } }
|
|
54
|
+
return observationOf({ platform, cliPath, processes, versionText })
|
|
55
|
+
},
|
|
56
|
+
inspect() {
|
|
57
|
+
const processes = defaultObsidianProcessProbe({ platform })
|
|
58
|
+
if (processes === 'absent') return Promise.resolve(observationOf({ platform, cliPath, processes, versionText: null }))
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
execFile(cliPath, ['version'], options, (error, stdout) => resolve(observationOf({ platform, cliPath, processes, versionText: error ? null : stdout })))
|
|
61
|
+
})
|
|
62
|
+
},
|
|
63
|
+
// Whether the app answers for exactly this vault and has finished reading it.
|
|
64
|
+
vaultState({ vaultRoot }) {
|
|
65
|
+
const code = '(()=>JSON.stringify({basePath:app.vault.adapter.basePath,ready:app.metadataCache.initialized===true}))()'
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
execFile(cliPath, ['eval', `code=${code}`], options, (error, stdout) => {
|
|
68
|
+
if (error) return resolve({ answered: false, indexReady: false })
|
|
69
|
+
try {
|
|
70
|
+
const text = String(stdout)
|
|
71
|
+
const value = JSON.parse(JSON.parse(text.slice(text.indexOf('=> ') + 3)))
|
|
72
|
+
return resolve({ answered: value.basePath === vaultRoot, indexReady: value.basePath === vaultRoot && value.ready === true })
|
|
73
|
+
} catch { return resolve({ answered: false, indexReady: false }) }
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Asks the operating system to open the vault in Obsidian. No shell.
|
|
81
|
+
export function createProductionLauncher({ platform = process.platform, env = process.env } = {}) {
|
|
82
|
+
const command = platform === 'darwin' ? '/usr/bin/open' : platform === 'linux' ? 'xdg-open' : null
|
|
83
|
+
return {
|
|
84
|
+
open({ vaultRoot }) {
|
|
85
|
+
if (command === null) return Promise.resolve({ launched: false, reason: 'launcher-platform-unqualified' })
|
|
86
|
+
const uri = `obsidian://open?path=${encodeURIComponent(vaultRoot)}`
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
execFile(command, [uri], { env, timeout: 15_000 }, (error) => resolve(error ? { launched: false, reason: 'os-open-failed' } : { launched: true, reason: 'os-open-accepted' }))
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createProductionAppSeams(options = {}) {
|
|
95
|
+
return { appProbe: createProductionAppProbe(options), launcher: createProductionLauncher(options) }
|
|
96
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { createProposalAdapterContribution } from '../../../projection/obsidian/proposals/contribution.mjs'
|
|
2
|
+
|
|
3
|
+
// The proposal adapter: what the engine hands structural edits to on a tick, and `atelier obsidian proposals`.
|
|
4
|
+
export default createProposalAdapterContribution()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
|
+
import { refuse } from './errors.mjs'
|
|
5
|
+
|
|
6
|
+
// Where later work plugs in without touching a shared file: every module in
|
|
7
|
+
// `contributions/` beside this file is a contribution. Its default export is
|
|
8
|
+
// `{ id, register({ extensions, operations }) }`. The command and the service
|
|
9
|
+
// entry both load the directory, so an apply operation registered there is
|
|
10
|
+
// the one the service dispatches to and the one `status` reports.
|
|
11
|
+
//
|
|
12
|
+
// Only regular `.mjs` files directly in that directory are loaded, in name
|
|
13
|
+
// order. The directory ships with the package; nothing outside it is read.
|
|
14
|
+
|
|
15
|
+
export const CONTRIBUTIONS_DIRECTORY = path.join(path.dirname(fileURLToPath(import.meta.url)), 'contributions')
|
|
16
|
+
|
|
17
|
+
export async function loadContributions({ directory = CONTRIBUTIONS_DIRECTORY } = {}) {
|
|
18
|
+
let entries
|
|
19
|
+
try { entries = fs.readdirSync(directory, { withFileTypes: true }) } catch (error) { if (error.code === 'ENOENT') return []; throw error }
|
|
20
|
+
const contributions = []
|
|
21
|
+
for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith('.mjs')).sort((left, right) => (left.name < right.name ? -1 : 1))) {
|
|
22
|
+
const loaded = await import(pathToFileURL(path.join(directory, entry.name)).href)
|
|
23
|
+
if (loaded.default === null || typeof loaded.default !== 'object' || typeof loaded.default.register !== 'function') refuse('invalid-extension', 'a contribution module must export { id, register } by default', { file: entry.name })
|
|
24
|
+
contributions.push(loaded.default)
|
|
25
|
+
}
|
|
26
|
+
return contributions
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { refuse } from './errors.mjs'
|
|
2
|
+
|
|
3
|
+
// Helpers shared by this module's private documents. Each document is
|
|
4
|
+
// versioned by its `schema` string, closed (an unknown key refuses) and
|
|
5
|
+
// written in one canonical form, so equal state is equal bytes.
|
|
6
|
+
|
|
7
|
+
export const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
8
|
+
|
|
9
|
+
function sortKeys(value) {
|
|
10
|
+
if (Array.isArray(value)) return value.map(sortKeys)
|
|
11
|
+
if (!isPlainObject(value)) return value
|
|
12
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortKeys(value[key])]))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Keys sorted at every depth, two-space indentation, one final newline.
|
|
16
|
+
export function canonicalJson(value) {
|
|
17
|
+
return `${JSON.stringify(sortKeys(value), null, 2)}\n`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function closedObject(value, { required = [], optional = [] }, code, label) {
|
|
21
|
+
if (!isPlainObject(value)) refuse(code, `${label} must be an object`)
|
|
22
|
+
const allowed = new Set([...required, ...optional])
|
|
23
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key))
|
|
24
|
+
if (unknown.length > 0) refuse(code, `${label} carries an unknown key`, { keys: unknown.sort() })
|
|
25
|
+
const missing = required.filter((key) => !Object.hasOwn(value, key))
|
|
26
|
+
if (missing.length > 0) refuse(code, `${label} lacks a required key`, { keys: missing })
|
|
27
|
+
return value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const compareText = (left, right) => (left < right ? -1 : left > right ? 1 : 0)
|
|
31
|
+
|
|
32
|
+
export function isoTime(clock) {
|
|
33
|
+
const value = clock()
|
|
34
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
35
|
+
if (Number.isNaN(date.getTime())) refuse('missing-clock', 'the injected clock did not return a time')
|
|
36
|
+
return date.toISOString()
|
|
37
|
+
}
|