@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,687 @@
|
|
|
1
|
+
# Obsidian projection contract
|
|
2
|
+
|
|
3
|
+
This document freezes what later work receives from the contract and
|
|
4
|
+
feasibility track. It states what is proven, on what, and what is not. Nothing
|
|
5
|
+
here enables a feature: a publisher exists under
|
|
6
|
+
`src/projection/obsidian/publication/`, and no command or service calls it yet.
|
|
7
|
+
|
|
8
|
+
## Registered shapes
|
|
9
|
+
|
|
10
|
+
Eleven closed v1 schemas, `contracts/atelier-obsidian-<shape>.v1.schema.json`,
|
|
11
|
+
are registered in `src/contracts/corpus.mjs` with valid and invalid fixtures
|
|
12
|
+
under `fixtures/obsidian/contracts/<shape>/`. `src/projection/obsidian/contracts.mjs`
|
|
13
|
+
validates them and adds the refusals a schema cannot express.
|
|
14
|
+
|
|
15
|
+
| Shape | Portable | Purpose |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `corpus-profile` | yes | Workspace and repository identities, enrollment, audience |
|
|
18
|
+
| `scope` | yes | Mode and selector set-AST; an empty selection is valid and empty |
|
|
19
|
+
| `source-snapshot` | yes | Raw byte digests, graph pin, dirty state, single consistent read |
|
|
20
|
+
| `generation-manifest` | yes | Path map, byte regions, link inversion map, completeness |
|
|
21
|
+
| `publication-journal` | no | Conditional operations, partial transition, restart; protocol ID required |
|
|
22
|
+
| `service-state` | no | Literal loopback, port, runtime ID, PID, executable, consent |
|
|
23
|
+
| `edit-operation` | yes | Object identity, origin generation, base digest, idempotency key |
|
|
24
|
+
| `apply-policy` | no | `manual` or `automatic`; only `body-replacement` is an accepted edit class |
|
|
25
|
+
| `proposal-receipt` | yes | Repository-scoped store, adapter operation identity, dedupe outcome |
|
|
26
|
+
| `acceptance-receipt` | yes | Candidate identity, environment versions, evidence hashes, outcome |
|
|
27
|
+
| `ext-settings` | yes | The object under `ext["mnstry.atelier.obsidian"]` |
|
|
28
|
+
|
|
29
|
+
Portable shapes refuse absolute paths. `atelier-project-config.v1` is
|
|
30
|
+
unchanged: extension settings are validated by their own schema, and an
|
|
31
|
+
unknown extension key refuses in the adapter, not in the project validator.
|
|
32
|
+
|
|
33
|
+
## Selection
|
|
34
|
+
|
|
35
|
+
`selectScope({ canonicalSnapshot, profile, selector, expansion })` is the only
|
|
36
|
+
runtime entry point. Visibility fails closed: a node is selectable only when it
|
|
37
|
+
is explicitly eligible, its repository is enrolled and its audience is allowed.
|
|
38
|
+
Edges to a withheld endpoint are dropped. Absent and withheld identities are
|
|
39
|
+
reported together. Expansion requires an explicit depth and node budget,
|
|
40
|
+
proceeds in canonical-identity order and reports truncation. Relation types are
|
|
41
|
+
`related`, `supports`, `supersedes`, `implements`, `depends_on`, `evidences`,
|
|
42
|
+
`contradicts`, `belongs_to` and the derived `links_to`. Literal oracles and
|
|
43
|
+
refusals live in `fixtures/obsidian/contracts/oracles/scope-cases.json`.
|
|
44
|
+
|
|
45
|
+
### What redaction covers
|
|
46
|
+
|
|
47
|
+
Emission is byte-faithful: a visible author's bytes are emitted unchanged. A
|
|
48
|
+
visible author's own references to withheld or out-of-selection documents
|
|
49
|
+
(canonical identities in front matter, repository-relative paths, link text)
|
|
50
|
+
therefore appear in the vault exactly as written. Only generated bytes are
|
|
51
|
+
redacted: generated regions, note paths, manifest entries and link inversions
|
|
52
|
+
never carry a withheld node's identity, title, summary, tags or path. A scoped
|
|
53
|
+
vault is not a confidentiality boundary against what visible authors wrote. A
|
|
54
|
+
redaction boundary over authored bytes cannot coexist with byte-faithful
|
|
55
|
+
emission; it would be a separate mode with its own evidence.
|
|
56
|
+
|
|
57
|
+
Each note may carry the generated line `Relationships leading outside this
|
|
58
|
+
view: N`. N counts that note's relationships to notes that are visible but not
|
|
59
|
+
selected, so it reveals that count, and two views of one workspace can be
|
|
60
|
+
compared. It never counts a withheld node: edges to a withheld endpoint are
|
|
61
|
+
dropped before the count is taken.
|
|
62
|
+
|
|
63
|
+
## Note paths
|
|
64
|
+
|
|
65
|
+
`notes/<readable title>--<identity suffix>.md`. The suffix derives from the
|
|
66
|
+
stable repository identity plus node identity, starts at 12 hexadecimal
|
|
67
|
+
characters and lengthens on collision. Titles are Unicode-normalized, platform
|
|
68
|
+
reserved names are avoided, and case-folding collisions are detected. Duplicate
|
|
69
|
+
titles are allowed; duplicate canonical identities refuse; a title-only link
|
|
70
|
+
that matches more than one visible note refuses. Paths are allocated once per
|
|
71
|
+
workspace and reused by every view.
|
|
72
|
+
|
|
73
|
+
## Graph behaviour changes since the shared link resolver
|
|
74
|
+
|
|
75
|
+
The resolver that produces `links_to` edges changed in these ways, each pinned by a
|
|
76
|
+
test in `test/graph-knowledge-graph.test.mjs`. Repository artifacts committed by a
|
|
77
|
+
consumer may differ after upgrading in exactly these classes and no others:
|
|
78
|
+
|
|
79
|
+
1. Links inside fenced code (backtick or tilde, any info string, up to three
|
|
80
|
+
spaces of indent, CRLF, CommonMark fence-length rules), inside inline code
|
|
81
|
+
(including two stray backticks that happen to pair across a link) and inside
|
|
82
|
+
front matter no longer produce edges. Links after an unbalanced fence that
|
|
83
|
+
runs to the end of the file are inside code.
|
|
84
|
+
2. A link to a directory resolves to that directory's `README.md`, then
|
|
85
|
+
`index.md`, testing eligibility per candidate; a link to a parent directory
|
|
86
|
+
now resolves where the earlier reader missed it.
|
|
87
|
+
3. A link that climbs above its own repository root and re-enters through the
|
|
88
|
+
checkout's directory name is reported as leaving the enrolled roots, as it
|
|
89
|
+
always was; it is never turned into a repository-local edge.
|
|
90
|
+
4. Malformed percent-encoding in a link is a `link-href-malformed` finding;
|
|
91
|
+
it no longer throws out of the graph build.
|
|
92
|
+
5. The workspace graph (not repository artifacts) additionally carries
|
|
93
|
+
wikilink edges and cross-repository Markdown-link edges, de-duplicated.
|
|
94
|
+
|
|
95
|
+
## Embedded assets
|
|
96
|
+
|
|
97
|
+
An embed (`` or `![[file]]`) whose target is not a document of the
|
|
98
|
+
census resolves in the canonical graph, never in the emitter. The resolver
|
|
99
|
+
reports `embeds` beside `links`: one `embeds_asset` record per occurrence, with
|
|
100
|
+
the same UTF-16 and byte offsets, naming an asset
|
|
101
|
+
`{ id, repo, path, extension }` whose id is `<repository>:asset:<path>`.
|
|
102
|
+
`buildCanonicalGraph` returns `embeds` and the de-duplicated `assets`. Neither
|
|
103
|
+
is a node or an edge: committed graph artifacts and `markdownLinkEdges` are
|
|
104
|
+
byte-identical with and without assets.
|
|
105
|
+
|
|
106
|
+
An asset is a regular file inside an enrolled repository root. A link on disk,
|
|
107
|
+
a file reached through one, a git-ignored path, anything inside `.git`, a
|
|
108
|
+
Markdown file and a census node are never assets. Markdown embeds resolve by
|
|
109
|
+
relative path like links. A wikilink embed whose target contains `/` resolves
|
|
110
|
+
by repository-relative path, in the source's repository first and then as
|
|
111
|
+
`<repository>/<path>`; a bare file name resolves by basename across enrolled
|
|
112
|
+
repositories and refuses as `link-target-ambiguous` when more than one file
|
|
113
|
+
matches. `isAssetEligible({ repo, path })` fails closed and is asked per
|
|
114
|
+
candidate before choosing: a refused asset is reported exactly as an absent
|
|
115
|
+
one (`link-target-unresolved`), so a finding never confirms that a withheld
|
|
116
|
+
file exists.
|
|
117
|
+
|
|
118
|
+
Behaviour change: an embed that resolves to an asset no longer produces a
|
|
119
|
+
`link-target-unresolved` finding. No other finding changes.
|
|
120
|
+
|
|
121
|
+
In a view, an asset is copied only when the embedding note is in the vault
|
|
122
|
+
set, the asset record carries `eligible: true` (`withEligibility` takes a
|
|
123
|
+
second predicate; without it every asset is withheld) and the profile enrols
|
|
124
|
+
its repository under the same audience rule as a node without an audience of
|
|
125
|
+
its own. It is copied once, byte for byte, to
|
|
126
|
+
`attachments/<readable stem>--<identity suffix>.<ext>`, read through the pinned
|
|
127
|
+
snapshot (`source-not-in-snapshot`, `mixed-read`). The embed target is
|
|
128
|
+
rewritten through the link inversion machinery: a Markdown embed to the
|
|
129
|
+
percent-encoded vault-root path, a wikilink embed to the plain path with its
|
|
130
|
+
size or fragment left as authored and no alias added. The manifest lists the
|
|
131
|
+
copy in `attachments[]` with
|
|
132
|
+
`ext["mnstry.atelier.obsidian"] = { kind: "embedded-asset", repoId, assetPath }`
|
|
133
|
+
and records the inversions on the embedding note at
|
|
134
|
+
`notes[].ext["mnstry.atelier.obsidian"].assetEmbeds = [{ attachment, inversions }]`,
|
|
135
|
+
in the shape of link inversions. A withheld or out-of-selection asset leaves
|
|
136
|
+
the authored embed untouched and appears in no output, manifest entry or
|
|
137
|
+
diagnostic.
|
|
138
|
+
|
|
139
|
+
## Unclosed code fence at the end of a source
|
|
140
|
+
|
|
141
|
+
An authored Markdown body that ends inside a fenced code block would turn any
|
|
142
|
+
generated section after it into code. When, and only when, a generated section
|
|
143
|
+
follows such a body, the emitter writes a closing fence as the first bytes of
|
|
144
|
+
the first generated region. The fence is generated, not authored: it lies
|
|
145
|
+
inside that region's range, so authored ranges and inversion stay exact. It
|
|
146
|
+
repeats the opener's indentation (0 to 3 spaces), character and length, starts
|
|
147
|
+
on its own line (a line break is added first when the body has none) and uses
|
|
148
|
+
the source's line ending, CRLF or LF. The region records it as
|
|
149
|
+
`ext["mnstry.atelier.obsidian"].fenceClosure = { fence, byteLength }`, where
|
|
150
|
+
`fence` is the emitted fence line without its line ending and `byteLength`
|
|
151
|
+
counts every closure byte, line breaks included. The view is not refused; its
|
|
152
|
+
diagnostics carry `unclosed-code-fence-closed-in-generated-region`. With no
|
|
153
|
+
generated section, nothing is emitted and nothing is reported.
|
|
154
|
+
|
|
155
|
+
Fence detection is the canonical graph scanner's: `unclosedFenceAtEnd` in
|
|
156
|
+
`src/graph/knowledge-graph.mjs` shares the rules that decide which text is
|
|
157
|
+
scanned for links. Front matter is never read for fences.
|
|
158
|
+
|
|
159
|
+
## Incremental preparation
|
|
160
|
+
|
|
161
|
+
A maintenance tick rebuilds the canonical graph and prepares the whole view.
|
|
162
|
+
Both stages accept a cache that makes repeated runs proportional to what
|
|
163
|
+
changed, without changing a single emitted byte. Each cache is derived,
|
|
164
|
+
in-memory, droppable state: it is never written anywhere, a dropped cache
|
|
165
|
+
costs a full build or preparation, and the result is by construction the one
|
|
166
|
+
a run with no cache produces. `test/obsidian-incremental.test.mjs` proves the
|
|
167
|
+
equality over the materialization fixture under both scopes and over random
|
|
168
|
+
change sequences (edits, retitles, relations, links, added and removed notes,
|
|
169
|
+
eligibility and scope changes), and shows with mutation controls that the
|
|
170
|
+
oracle sees a wrong cached byte, entry, inversion, node or scan.
|
|
171
|
+
|
|
172
|
+
Graph stage: `buildKnowledgeGraph` / `buildCanonicalGraph` take `fileCache`
|
|
173
|
+
(`createGraphFileCache`) and, optionally, `observedDigest`. A census node and
|
|
174
|
+
link scan are reused only under an equal sha256 of the bytes and equal
|
|
175
|
+
per-file inputs outside the bytes (coverage and the repository's read
|
|
176
|
+
boundary), and the cache is rebuilt to hold exactly the current census.
|
|
177
|
+
Without `observedDigest` every Markdown source is read and hashed on every
|
|
178
|
+
build. The maintenance engine passes `observedDigest` from its observation
|
|
179
|
+
index, and a source whose observed digest equals the cached one is then not
|
|
180
|
+
opened: the bound is observation's own, a stat hint between full
|
|
181
|
+
reconciliations, so bytes that change under an unchanged stat hint are not
|
|
182
|
+
seen by the graph, the snapshot or the view until observation hashes the file
|
|
183
|
+
again. The result reports `fileCensus: { reused, derived, read }`.
|
|
184
|
+
|
|
185
|
+
Preparation stage: `prepareView` takes `cache` (`createPreparationCache`).
|
|
186
|
+
Each note's bytes and manifest entry are a function of its pinned source
|
|
187
|
+
digest, node record, allocated path, generated rows, outside-selection count
|
|
188
|
+
and rewritten occurrences with their emitted targets; those inputs form a
|
|
189
|
+
dependency key, and a note whose key equals the cached one reuses the cached
|
|
190
|
+
bytes, manifest entry, attachment record and inversions. Everything else
|
|
191
|
+
(asset copies, links, settings, collision checks, the manifest and the
|
|
192
|
+
redaction guard over the whole result) runs as before. The result reports
|
|
193
|
+
`preparation: { emitted, reused }`, and a preparation that emits exactly the
|
|
194
|
+
notes whose output changed is what the tests pin.
|
|
195
|
+
|
|
196
|
+
One bound is stated rather than hidden: a reused note's source is not read
|
|
197
|
+
again, so bytes that drift under an unchanged pinned digest are not seen by
|
|
198
|
+
`mixed-read` on that call. The emitted note is exactly the one the pin
|
|
199
|
+
describes, and the next observation by digest sees the drift. A note whose pin
|
|
200
|
+
changed is read and verified against the pin as always.
|
|
201
|
+
|
|
202
|
+
The engine holds one graph file cache and one preparation cache per scope for
|
|
203
|
+
its lifetime and hands them through the `createGraphCache` and
|
|
204
|
+
`createPreparationCache` production seams; a test replaces either with
|
|
205
|
+
`() => null` to build or prepare in full.
|
|
206
|
+
|
|
207
|
+
## Publication protocol `obsidian-cli-critical-section/v1`
|
|
208
|
+
|
|
209
|
+
The journal's `protocolId` names this protocol. A publisher may use it only
|
|
210
|
+
within the proven boundary below.
|
|
211
|
+
|
|
212
|
+
1. Stage the candidate on the vault's volume and bind it by SHA-256. A
|
|
213
|
+
candidate that replaces an existing file is staged in that file's unit
|
|
214
|
+
recovery directory (see "Where candidates are staged").
|
|
215
|
+
2. Inside the app, in one synchronous step: refuse if any editor of the note in
|
|
216
|
+
any window is unsaved or differs from the expected base; refuse if the bytes
|
|
217
|
+
on disk differ from the expected base; refuse a staged file whose digest
|
|
218
|
+
differs; atomically exchange the staged candidate with the note, so
|
|
219
|
+
whatever occupied the path becomes the recovery file; update every open
|
|
220
|
+
editor in one transaction and record the view as saved with that content.
|
|
221
|
+
The app must not write the note as a result of publication.
|
|
222
|
+
3. Reply immediately. Record the outcome in the app. A caller whose reply is
|
|
223
|
+
lost re-reads the outcome and never resends.
|
|
224
|
+
4. Re-check the recovery file after a quiet period: a program that held the
|
|
225
|
+
note open before the exchange writes into it.
|
|
226
|
+
5. Commit the trusted manifest last. Until then the transition is reported as
|
|
227
|
+
updating. A view converges to one verified generation; atomic visibility
|
|
228
|
+
across notes is not claimed.
|
|
229
|
+
|
|
230
|
+
A refusal is always an acceptable outcome. A note being edited stays one
|
|
231
|
+
generation behind until its editor is clean.
|
|
232
|
+
|
|
233
|
+
### Where candidates are staged
|
|
234
|
+
|
|
235
|
+
The exchange in step 2 leaves the displaced bytes at the candidate's path until
|
|
236
|
+
they are moved to their recovery name. Those bytes may be a person's only copy,
|
|
237
|
+
and `staging/` is classed discardable, so that path is never in `staging/`.
|
|
238
|
+
This is a layout inside the private store; the protocol is unchanged.
|
|
239
|
+
|
|
240
|
+
| Bytes | Path |
|
|
241
|
+
| --- | --- |
|
|
242
|
+
| Any candidate while it is written and fsynced (generated bytes only) | `staging/<journalId>/NNNNNN.candidate`, or `NNNNNN.late.candidate` for one that could not be known when the run began |
|
|
243
|
+
| Candidate of a file that is created (exclusive link, no exchange) | stays at that staging path until it is linked |
|
|
244
|
+
| Candidate of a file that is replaced (note, attachment, policy settings) | moved, complete, to `recovery/<journalId>/<unit>/exchange.candidate`, or `exchange.late.candidate` |
|
|
245
|
+
| Displaced bytes | the vault path, then that same candidate path, then `recovery/<journalId>/<unit>/displaced.bin` |
|
|
246
|
+
|
|
247
|
+
The journal names every exchange candidate, by path and SHA-256, before the
|
|
248
|
+
file is at that path: in the header for candidates known when the run began, in
|
|
249
|
+
a write-ahead entry for a late one. A file at a candidate path whose digest is
|
|
250
|
+
the recorded one is a generated candidate and may be deleted; any other file
|
|
251
|
+
there is displaced bytes and is moved to its recovery name with a receipt,
|
|
252
|
+
never deleted. Restart recovery decides only this way. The candidate names end
|
|
253
|
+
in `.candidate`; every other name in a unit directory ends in `.bin` or
|
|
254
|
+
`.json`. The same-volume check covers staging and recovery, and the exchange
|
|
255
|
+
self-test runs inside the recovery area, in `recovery/.exchange-probe/`.
|
|
256
|
+
|
|
257
|
+
### One publisher per vault
|
|
258
|
+
|
|
259
|
+
A publication holds two locks: the view's, in its private state, and the
|
|
260
|
+
vault's. Two views, or two sets of workspace state, that point at one vault
|
|
261
|
+
share nothing but the vault, so the vault lock lives under the vault's real
|
|
262
|
+
path in `.atelier-publication/`, a dot-directory that no note path can name
|
|
263
|
+
and that the app does not show. The second publisher refuses with
|
|
264
|
+
`publication-in-progress` and writes nothing. It is the only thing the
|
|
265
|
+
publisher writes in a vault besides notes, attachments and the policy
|
|
266
|
+
settings file.
|
|
267
|
+
|
|
268
|
+
Release and recovery follow the view lock exactly. A finished publication
|
|
269
|
+
writes a release marker. A publisher that dies leaves a ticket naming its
|
|
270
|
+
process; the next publisher on the same host sees that the process is gone and
|
|
271
|
+
takes the lock over, so a crash does not wedge the vault. Superseded tickets
|
|
272
|
+
are removed while the lock is held. Two cases refuse until a person acts: a
|
|
273
|
+
ticket written on another host (a vault reached through a shared or
|
|
274
|
+
synchronized folder), and an unrelated live process that reuses the dead
|
|
275
|
+
publisher's process ID, which clears when that process exits. When no
|
|
276
|
+
publisher is running anywhere, deleting `.atelier-publication/` is safe.
|
|
277
|
+
|
|
278
|
+
### The path with no app
|
|
279
|
+
|
|
280
|
+
When the process table shows, positively, that no Obsidian runs, the same
|
|
281
|
+
critical section runs in the publisher's own process with no editor to
|
|
282
|
+
coordinate with. The table is read at path selection, again immediately before
|
|
283
|
+
the first note whatever time has passed, and again whenever two seconds have
|
|
284
|
+
passed since the last reading. Once a reading is anything other than absent,
|
|
285
|
+
every remaining note that would be written refuses.
|
|
286
|
+
|
|
287
|
+
Residual window, stated plainly: an app that starts after a reading and before
|
|
288
|
+
the next is not seen, for at most two seconds plus one note's publication. In
|
|
289
|
+
that window a note is exchanged with no editor check. Bytes saved to disk are
|
|
290
|
+
still protected by the on-disk comparison and the exchange. An unsaved buffer
|
|
291
|
+
in the newly started app is not: the app then takes its own
|
|
292
|
+
external-modification merge, which can drop overlapping edits. The probe also
|
|
293
|
+
cannot see an app on another machine that reaches the vault through a shared
|
|
294
|
+
or synchronized folder, or an app packaged under another executable name.
|
|
295
|
+
|
|
296
|
+
### Proven boundary
|
|
297
|
+
|
|
298
|
+
macOS (Darwin 25, arm64) with Obsidian 1.13.7 (installer 1.12.7), CLI enabled,
|
|
299
|
+
no community plugin. Sixteen interleavings, 25 rounds for each racing case, in
|
|
300
|
+
four complete clean runs of the prototype in `experiments/obsidian-publication/`
|
|
301
|
+
at commits `34f5fab` and `c5142e7`. The receipts record those same trees under their
|
|
302
|
+
pre-sign-off identities `5930f85` and `93303ff`; the branch was rewritten only to add
|
|
303
|
+
sign-off trailers, with identical content. Receipt SHA-256 digests:
|
|
304
|
+
|
|
305
|
+
- `379992a6cc0e6c95581459412e8cab68d9b260a631fa891089613f120b40a67a`
|
|
306
|
+
- `63a5b69b203125b6099f2398d1ebddd0e4f3265a0401becbddff5e6d72fee16e`
|
|
307
|
+
- `95995051859e02042d648d1a0b513d86fa9f1d72d82359030e21858cdba5d959`
|
|
308
|
+
- `0984c35087ff91c0cc0d0f37f533eea1fc6c6380705491362eec98368f64fe01`
|
|
309
|
+
|
|
310
|
+
A fifth run aborted because the harness could not open a note after the
|
|
311
|
+
second-window case; no bytes were involved. The receipts are maintainer-held
|
|
312
|
+
and are not part of this repository.
|
|
313
|
+
|
|
314
|
+
### Receiving obligations for the production publisher
|
|
315
|
+
|
|
316
|
+
The publisher may not claim the protocol outside the proven boundary until
|
|
317
|
+
each open item is discharged with its own evidence.
|
|
318
|
+
|
|
319
|
+
| Obligation | State |
|
|
320
|
+
| --- | --- |
|
|
321
|
+
| Atomic exchange without the system Python | Done. The system Python is gone. The exchange is a raw syscall (`renameatx_np` with `RENAME_SWAP` on macOS, `renameat2` with `RENAME_EXCHANGE` on Linux) reached through the system perl, with no perl module, compiler or package dependency. |
|
|
322
|
+
| Interpreter trust | Done. The perl binary is used only when uid 0 owns it and neither group nor others can write it, in the publisher and in the script the app runs; otherwise `exchange-interpreter-untrusted`. An interpreter is still a dependency, and the exchange still costs a process start inside the critical section. |
|
|
323
|
+
| Exit status of the exchange helper | Done. The critical section reads the staged path after the call and decides from the bytes whether the exchange took place; a helper killed after the call returned no longer reads as a failure. |
|
|
324
|
+
| Linux | Partly open. The exchange primitive is proven in a container on aarch64. The app suite (the interleavings above) has not been run on Linux. |
|
|
325
|
+
| x86_64 | Open. Never run, on macOS or Linux; the syscall numbers are present and untested. |
|
|
326
|
+
| Windows | Refuses. No direct equivalent is known; the publisher refuses with `exchange-unsupported-platform` and publishes nothing. |
|
|
327
|
+
| App capability floor | Partly open. The step that prevents an app write sets the view's undocumented `lastSavedData`; open notes refuse when it is absent. The floor is pinned: `MINIMUM_APP_VERSION` in `src/runtime/obsidian/app-capability.mjs` is 1.13.7, the only version the protocol was proven on, and an older, unreadable or unknown version is refused as `app-version-unsupported`. There is no ceiling: a newer app is admitted, and the cases must be re-run on each app release. |
|
|
328
|
+
| Late-writer re-check | Partly open. The publisher re-checks displaced files twice per publication (on the blocking path, and after the quiet period). A holder can write later still, so the check must be repeated by the future maintenance service; carried to the maintenance track. |
|
|
329
|
+
| Displaced bytes between exchange and recovery move | Done, by the maintenance track's staging layout change. The exchange still leaves the displaced bytes at the candidate's path until the next step moves them to their recovery name, and that path is now in the unit recovery directory: candidates for replaced files are staged there, and staging holds only generated bytes. A crash in between is settled by restart recovery from the journal, which names that path and the candidate's digest. A staging sweeper is still not provided; any future one may discard `staging/` only and must never touch `recovery/`. |
|
|
330
|
+
| Transport | CLI replies are occasionally lost while the app stays responsive. Calls are serialized; every call is idempotent or outcome-recorded, and a publish is never resent. |
|
|
331
|
+
| Timer throttling | A hidden app window delays the app's own autosave. Do not read that as a fault. |
|
|
332
|
+
| Unreproduced anomaly | One early run ended with typed text on disk but absent from the editor buffer. It did not recur in any later run. Keep the typing-race case in every qualification run and treat a recurrence as a failed gate. |
|
|
333
|
+
| Link scanner cost | Open, carried. The Markdown link scanner is roughly quadratic in skipped regions times link occurrences on pathological inputs; fine for ordinary documents, minutes for several megabytes of adversarial Markdown. |
|
|
334
|
+
| Link-then-rename | Rejected. It leaves a window in which a concurrent replacement is destroyed. |
|
|
335
|
+
| App-driven save after replacement | Rejected. Observed losing an outside writer's bytes; kept as a negative control. |
|
|
336
|
+
|
|
337
|
+
Source application back into canonical files carries the same conditional-write
|
|
338
|
+
obligation against other source writers; its protocol follows.
|
|
339
|
+
|
|
340
|
+
## Source apply protocol `source-apply-exchange/v1`
|
|
341
|
+
|
|
342
|
+
Source apply writes one edit made in a vault back to the one source file the
|
|
343
|
+
note was generated from. It is the only operation of this integration that
|
|
344
|
+
writes a source file. A person's explicit Apply (`atelier obsidian apply run
|
|
345
|
+
EDIT [--actor ID]`, or the same call through the API by an agent acting for them)
|
|
346
|
+
and an automatic policy reach the same function; they differ only in who
|
|
347
|
+
authorises. It never stages, commits or otherwise asks git to change anything:
|
|
348
|
+
its two git calls ask whether the path is ignored and where the git directory
|
|
349
|
+
is.
|
|
350
|
+
|
|
351
|
+
### Who authorises
|
|
352
|
+
|
|
353
|
+
| Mode | Authority | Actor recorded |
|
|
354
|
+
| --- | --- | --- |
|
|
355
|
+
| `manual` | an explicit request that names the edit; the integration must be enabled | the request's actor, else `manual-request` |
|
|
356
|
+
| `automatic` | the installed policy, read from private state at the decision and again immediately before the write: machine mode `automatic`, status `active`, the policy's recomputed digest equal to the digest it carries, the object inside the policy's `selector`, `body-replacement` in `allowedEditClasses`, retry budget not spent | the policy's actor, with the policy identifier and digest |
|
|
357
|
+
|
|
358
|
+
In both modes the object must be visible in the canonical graph as it is now
|
|
359
|
+
(eligible, enrolled, audience allowed). An absent object and a withheld one get
|
|
360
|
+
the same answer, `object-not-visible`, and they get it first: before the
|
|
361
|
+
manifest, the record of the object or the source path is looked at, with
|
|
362
|
+
nothing recorded, so a moved or deleted source, an earlier apply or a missing
|
|
363
|
+
manifest never tells the two apart. A source deleted from the corpus is
|
|
364
|
+
therefore answered `object-not-visible` as well. There is no ambient agent mode: an automatic request with no
|
|
365
|
+
matching active policy is refused.
|
|
366
|
+
|
|
367
|
+
The digest of a policy is `sha256:` and the hex SHA-256 of its canonical form:
|
|
368
|
+
the policy document without its `digest` member, keys sorted at every depth,
|
|
369
|
+
two-space indentation, one final newline, UTF-8. `atelier obsidian policy
|
|
370
|
+
digest FILE` prints it and writes nothing; `policy install FILE` recomputes it
|
|
371
|
+
and refuses `policy-digest-mismatch`, naming the digest the file has to carry.
|
|
372
|
+
The file is never rewritten for the person. `maxBatchSize` bounds an
|
|
373
|
+
automatic dispatch (the engine's, and `applyBatch` in automatic mode); a batch
|
|
374
|
+
a person names through `applyBatch` is bounded too, by 1000, the largest bound
|
|
375
|
+
a policy can carry, and the rest of either batch refuses `batch-bound-reached`. `retryBudget` allows one attempt and
|
|
376
|
+
that many retries per operation under one revision of the policy; it is counted
|
|
377
|
+
over the refusals recorded in the events of the object, so a restart does not
|
|
378
|
+
refill it, and a spent budget refuses before anything is recorded.
|
|
379
|
+
`conflictDisposition` is `hold`: a conflict stays queued with its bytes, and
|
|
380
|
+
nothing a policy says overrides a stale source.
|
|
381
|
+
|
|
382
|
+
### Order of one apply
|
|
383
|
+
|
|
384
|
+
1. Resolve the workspace and the pending edit. Build the canonical graph now
|
|
385
|
+
and ask whether this machine may see the object. Then resolve the view and
|
|
386
|
+
the immutable manifest of the generation the edit was observed under.
|
|
387
|
+
2. The identity must still name the path the
|
|
388
|
+
manifest recorded, in an enrolled repository. That path must be a regular
|
|
389
|
+
file with one name, reached through no symbolic link, inside the repository,
|
|
390
|
+
outside every managed root and every git directory, and not git-ignored.
|
|
391
|
+
A git directory is any path with a segment spelled `.git` in any case (a
|
|
392
|
+
nested repository, a submodule), anything under `<root>/.git`, and anything
|
|
393
|
+
under the directory git itself names for the repository, which a `gitdir:`
|
|
394
|
+
file can place elsewhere; a git that cannot name it refuses
|
|
395
|
+
`source-ignore-state-unknown`. The only git calls ask whether the path is
|
|
396
|
+
ignored and where the git directory is.
|
|
397
|
+
3. Read the source. Run the lens from the preserved edit bytes, never from the
|
|
398
|
+
note as it is now. Record the observation of this edit and of every other
|
|
399
|
+
open edit of the same object, so a divergent edit in another view makes the
|
|
400
|
+
object conflicted before anything is written.
|
|
401
|
+
4. Take the object lease. An earlier apply whose outcome is unknown is settled
|
|
402
|
+
first, from digests on disk.
|
|
403
|
+
5. Refuse, writing nothing: a repeated request (answered from the record), a
|
|
404
|
+
stale source, a conflicted object, a lens refusal, a change outside the
|
|
405
|
+
authored body, a result equal to the source, the policy, a volume that is
|
|
406
|
+
not the source's, a missing exchange.
|
|
407
|
+
6. Write the apply record, then the candidate (the new source bytes, with the
|
|
408
|
+
source's mode, fsynced) in `recovery/<applyId>/000000/` of the private
|
|
409
|
+
workspace state.
|
|
410
|
+
7. Record `apply-intent` in the object store.
|
|
411
|
+
8. Read the policy again. Exchange the candidate with the source atomically.
|
|
412
|
+
9. Read what the exchange displaced. Equal to the base: it stays as the
|
|
413
|
+
retained backup under its recovery name, with a receipt binding its digest;
|
|
414
|
+
the source is verified; `applied` is recorded with old and new digests, the
|
|
415
|
+
actor and the policy. Not equal: another program saved the source between
|
|
416
|
+
the read and the exchange. Its bytes are retained as an immutable object and
|
|
417
|
+
the files are exchanged back, so its bytes return to the source path; what
|
|
418
|
+
that displaces must be the candidate, and anything else is kept with a
|
|
419
|
+
receipt. `apply-refused`, `concurrent-source-writer`, with every reference.
|
|
420
|
+
Nothing is retried inside one call. An exchange that reports a failure is
|
|
421
|
+
not believed either way: only the candidate still at its path beside the
|
|
422
|
+
source as it was read means that nothing was exchanged
|
|
423
|
+
(`exchange-unavailable`); anything else is decided as restart recovery
|
|
424
|
+
decides it, so an exchange that did take place is recorded as applied
|
|
425
|
+
(`applied-after-restart`) with its backup.
|
|
426
|
+
10. After a quiet period the backup is read again. A program that opened the
|
|
427
|
+
source before the exchange still holds the old file and can write into it
|
|
428
|
+
at any later time: `source-changed-after-apply`, both byte sets retained.
|
|
429
|
+
An applied source leaves a closed journal of this protocol beside the
|
|
430
|
+
publication journals of the view, so the maintenance engine's late-writer
|
|
431
|
+
re-check covers the backup on the tick of the apply and on later ticks,
|
|
432
|
+
for as long as it covers a publication.
|
|
433
|
+
|
|
434
|
+
The candidate is never written inside a repository working tree, where a stray
|
|
435
|
+
file could be committed by somebody. An exchange cannot cross a volume, so the
|
|
436
|
+
private workspace state and the source must share one; otherwise the apply
|
|
437
|
+
refuses `apply-volume-mismatch`. Where no atomic exchange exists (Windows
|
|
438
|
+
today) it refuses `exchange-unavailable`. Both write nothing.
|
|
439
|
+
|
|
440
|
+
### Refusals
|
|
441
|
+
|
|
442
|
+
| Code | When |
|
|
443
|
+
| --- | --- |
|
|
444
|
+
| `integration-disabled`, `workspace-not-prepared`, `unknown-edit`, `foreign-workspace`, `unknown-scope`, `edit-not-open` | the request cannot be resolved |
|
|
445
|
+
| `corpus-unreadable` | the canonical graph cannot be built on this machine: an enrolled file may not be read; no file is named |
|
|
446
|
+
| `manifest-unavailable`, `published-note-unavailable` | the generation's manifest, or the note as it was published, cannot be established |
|
|
447
|
+
| `repository-not-enrolled`, `source-not-in-graph`, `source-moved` | the identity of a visible object no longer names that path: a renamed or moved source (a deleted one is `object-not-visible`) |
|
|
448
|
+
| `source-missing`, `source-symlink`, `source-not-regular-file`, `source-unreadable`, `source-hard-linked`, `source-outside-repository`, `source-inside-managed-root`, `source-inside-git-directory`, `source-git-ignored`, `source-ignore-state-unknown` | the path is not one this operation writes |
|
|
449
|
+
| `invalid-apply-request`, `object-not-visible`, `edit-class-not-allowed`, `conflict-disposition-unsupported`, `maintenance-mode-manual`, `no-apply-policy-installed`, `apply-policy-revoked`, `apply-policy-paused`, `apply-policy-invalid`, `apply-policy-reference-mismatch`, `policy-digest-mismatch`, `policy-changed-since-dispatch`, `policy-selector-invalid`, `outside-policy-selection`, `retry-budget-exhausted`, `batch-bound-reached` | the decision |
|
|
450
|
+
| `stale-source`, `object-conflicted`, `sibling-edit-unobservable`, `lease-held` | arbitration; the operation stays conflicted or pending with its bytes |
|
|
451
|
+
| `edit-not-applicable`, `change-outside-authored-body`, `no-source-change` | the lens result is not an applicable body replacement |
|
|
452
|
+
| `exchange-unavailable`, `apply-volume-mismatch` | this machine cannot write conditionally here |
|
|
453
|
+
| `concurrent-source-writer`, `source-changed-during-apply` | another program wrote the source during the apply; every byte is retained |
|
|
454
|
+
| `interrupted-before-exchange`, `apply-interrupted-needs-person` | what restart recovery decided for an interrupted apply |
|
|
455
|
+
| `recovery-state-unreadable` | a candidate, backup or source path of an interrupted apply may not be looked at or read; nothing is settled from a read this process was denied, that record is reported with its intent still open, and every other one is still settled |
|
|
456
|
+
| `apply-outcome-unknown` | the source was exchanged and the settlement from digests could not be carried out; the source may have been changed, the intent stays open and `apply recover` decides |
|
|
457
|
+
|
|
458
|
+
A path that another program removes or replaces between two steps, before the
|
|
459
|
+
intent is recorded, answers one of these refusals (`source-missing`,
|
|
460
|
+
`source-not-regular-file`, `workspace-not-prepared`), never an exception; one
|
|
461
|
+
that this process may not look at or read answers `corpus-unreadable` while the
|
|
462
|
+
graph is built and `source-unreadable` from then on. The
|
|
463
|
+
mode of the source is read from the descriptor its bytes were read from.
|
|
464
|
+
|
|
465
|
+
### Restart recovery
|
|
466
|
+
|
|
467
|
+
An `apply-intent` with no outcome is never guessed. `atelier obsidian apply
|
|
468
|
+
recover`, and the next apply of the same object, decide from digests on disk.
|
|
469
|
+
Only bytes with the recorded candidate digest are ours to delete. Every
|
|
470
|
+
interrupted apply is settled on its own: an object whose record cannot be read
|
|
471
|
+
is reported with its code, left exactly as it is, and delays no other. `apply
|
|
472
|
+
list`, `show`, `run` and `recover` answer such a record as a typed refusal.
|
|
473
|
+
|
|
474
|
+
| At the candidate path | Source | Decision |
|
|
475
|
+
| --- | --- | --- |
|
|
476
|
+
| the candidate | anything | nothing was exchanged, or it was exchanged back: the candidate is retired, `interrupted-before-exchange` |
|
|
477
|
+
| the base | the candidate | applied: the base becomes the backup, `applied` is recorded |
|
|
478
|
+
| anything else | anything | everything is kept with receipts, `apply-interrupted-needs-person` |
|
|
479
|
+
| nothing, backup recorded as the base | the candidate | applied |
|
|
480
|
+
| nothing, no backup | the base | `interrupted-before-exchange` |
|
|
481
|
+
|
|
482
|
+
### Limits
|
|
483
|
+
|
|
484
|
+
- The applied source is a new file: a hard link is refused up front, extended
|
|
485
|
+
attributes and ownership are those of the candidate, and only the permission
|
|
486
|
+
bits are carried over.
|
|
487
|
+
- A hard link is refused when the path is checked. One that another program
|
|
488
|
+
creates on the source after that check and before the exchange keeps the old
|
|
489
|
+
bytes under its other name; the source path itself ends as the applied file,
|
|
490
|
+
and nothing detects the second name.
|
|
491
|
+
- Every component of the source path is checked for a symbolic link, and the
|
|
492
|
+
file itself is opened without following one. A directory component that
|
|
493
|
+
another program replaces with a symbolic link after that check and before the
|
|
494
|
+
exchange is not detected; what contains it is the commit rule: unless the
|
|
495
|
+
file the exchange displaced holds exactly the bytes that were read, the files
|
|
496
|
+
are exchanged back and everything is retained.
|
|
497
|
+
- When the exchange back reports a failure, or the displaced file cannot be
|
|
498
|
+
retained first, nothing is guessed and the report is not believed either way:
|
|
499
|
+
the apply is settled from the digests on disk by the restart table above.
|
|
500
|
+
If the exchange back did not take place, the source path keeps the candidate,
|
|
501
|
+
whole, the other program's bytes stay where recovery references name them,
|
|
502
|
+
and the answer is `apply-interrupted-needs-person`. If it did take place
|
|
503
|
+
before it reported the failure, the source path holds the other program's
|
|
504
|
+
bytes, the candidate is retired, and the answer is
|
|
505
|
+
`interrupted-before-exchange`. If the digests show that the first exchange
|
|
506
|
+
displaced the base after all, the apply happened and is answered as applied.
|
|
507
|
+
- A refusal says that nothing was written. From the moment the first exchange
|
|
508
|
+
may have taken place (a clean exchange, or one that reported a failure while
|
|
509
|
+
the digests no longer show the untouched state), a failure is therefore never
|
|
510
|
+
returned as a plain refusal with the intent open: it is settled from the
|
|
511
|
+
digests on disk, and only when that settlement itself cannot be carried out
|
|
512
|
+
is the answer `apply-outcome-unknown`, which says that the source may have
|
|
513
|
+
been changed and that `apply recover` decides. Once the outcome is durable
|
|
514
|
+
that answer is no longer given.
|
|
515
|
+
- A read that this process is denied (a permission or I/O error) proves
|
|
516
|
+
nothing about a file, unlike a path that is no longer a regular file. It is
|
|
517
|
+
never taken as evidence that the source changed or that another program
|
|
518
|
+
wrote: the apply is settled again from digests when the path can be read.
|
|
519
|
+
A typed failure of the late-writer check after a durable apply leaves the
|
|
520
|
+
answer applied; the engine repeats that check.
|
|
521
|
+
- An enrolled file this process may not read while the canonical graph is built
|
|
522
|
+
refuses `corpus-unreadable`, naming no file. A source that cannot be read at
|
|
523
|
+
the moment of the apply refuses `source-unreadable`. Both carry the system's
|
|
524
|
+
error code as the cause. Only bytes with the recorded candidate digest are
|
|
525
|
+
ours to delete, so a file that cannot be read is never retired as a generated
|
|
526
|
+
candidate.
|
|
527
|
+
- Where no atomic exchange exists the whole apply half of the test suite is
|
|
528
|
+
skipped: on such a platform the only executed evidence is that apply refuses
|
|
529
|
+
`exchange-unavailable` and writes nothing.
|
|
530
|
+
- Lifting the hold after an apply relies on the publisher: it reads the note
|
|
531
|
+
again under its own lock and settles it as already current only while the
|
|
532
|
+
note still holds the prepared bytes. The engine's own read of the held note
|
|
533
|
+
narrows the window; the publisher's expected-bytes check closes it.
|
|
534
|
+
- Between the exchange and the exchange back, readers of the source path see
|
|
535
|
+
the candidate for a moment. A third write in that moment is kept: the source
|
|
536
|
+
ends as one whole version and the other is in recovery with a receipt.
|
|
537
|
+
- A program that holds the old file open and writes later has its bytes
|
|
538
|
+
retained and surfaced, not merged: deciding what the source should be is a
|
|
539
|
+
person's work.
|
|
540
|
+
- Generated regions are never applied. The edited note must still end with the
|
|
541
|
+
exact generated bytes, or have lost them whole while the end of its authored
|
|
542
|
+
text is provably the published end: the last authored line and its line
|
|
543
|
+
endings byte for byte, followed by nothing but what is left of the separator
|
|
544
|
+
or fence closure. A region cut in the middle of a line from either side, a
|
|
545
|
+
fragment of one left in the body, and text typed at the very end of the body
|
|
546
|
+
together with a removed region all refuse `generated-region-edited`. Some of
|
|
547
|
+
those refusals a person could have been spared; a generated byte in a source
|
|
548
|
+
is never accepted in exchange.
|
|
549
|
+
- After an apply the next tick prepares the view again. When the prepared note
|
|
550
|
+
is byte for byte the note the person edited (read from the vault at the
|
|
551
|
+
moment of that decision, never taken from the engine's observation index; a
|
|
552
|
+
note that cannot be read stays held), it is published over nothing,
|
|
553
|
+
the hold lifts and the pending record closes as `withdrawn`; the `applied`
|
|
554
|
+
record of the object store is the authority. When it is not (the person
|
|
555
|
+
removed a generated region, for example) the view stays held.
|
|
556
|
+
- `--actor` is an option of `apply run`. The command has one option table for
|
|
557
|
+
every operation; every other operation, of `apply` or not, refuses it as a
|
|
558
|
+
usage error and does nothing.
|
|
559
|
+
- Proven on macOS arm64 on APFS with a real second process. Linux, x86_64 and
|
|
560
|
+
other filesystems carry the open obligations of the exchange listed above.
|
|
561
|
+
|
|
562
|
+
## Proposal adapter for structural edits
|
|
563
|
+
|
|
564
|
+
An edit the byte lens cannot turn into source bytes (a new or changed link to
|
|
565
|
+
another note of the vault, an edited front matter) is recorded as an edit
|
|
566
|
+
operation of kind `semantic-proposal`, state `proposed`. It is never applied.
|
|
567
|
+
The proposal adapter (`src/projection/obsidian/proposals/`) turns each such
|
|
568
|
+
operation into exactly one copy-only proposal in the existing proposal store of
|
|
569
|
+
the repository that owns the source, so that a reviewer of that repository sees
|
|
570
|
+
one durable request. It writes no source file and no vault, it never accepts or
|
|
571
|
+
applies a proposal, and no status of a proposal is an instruction to it.
|
|
572
|
+
|
|
573
|
+
The adapter is a contribution (`src/runtime/obsidian/contributions/`). On a
|
|
574
|
+
tick the engine first lets it observe the open pending edits (below), then,
|
|
575
|
+
after the automatic dispatch, hands it a copy of the pending edits once.
|
|
576
|
+
|
|
577
|
+
### Observation on a tick
|
|
578
|
+
|
|
579
|
+
An operation exists once the edit has been observed: the source is read now,
|
|
580
|
+
the lens runs over the PRESERVED edit bytes (never the live note) against it,
|
|
581
|
+
and the resulting edit operation is recorded in the object store. Source apply
|
|
582
|
+
does this before it takes its lease. The adapter does the same on every tick
|
|
583
|
+
(`src/projection/obsidian/proposals/observation.mjs`), through the same
|
|
584
|
+
observer and the same store, for every open pending edit the object store does
|
|
585
|
+
not know yet, in manual and in automatic mode alike, so that a structural edit
|
|
586
|
+
becomes `proposed`, and is routed in the same tick, without anybody running
|
|
587
|
+
`atelier obsidian apply run EDIT`. What it records is what an apply would have
|
|
588
|
+
recorded: a body replacement `pending`, which stays queued and is written only
|
|
589
|
+
by apply; a structural edit `proposed`; a lens refusal `refused`; a base source
|
|
590
|
+
that moved on `conflicted`. It writes no source file and no vault, and takes no
|
|
591
|
+
lease.
|
|
592
|
+
|
|
593
|
+
Ticks stay quiet. The object store answers the same for an origin it already
|
|
594
|
+
holds and appends nothing, and an edit whose operation is recorded is not
|
|
595
|
+
offered again: the record of the object is the authority, and an adapter that
|
|
596
|
+
has just started finds it there. A refusal that comes before anything can be
|
|
597
|
+
recorded (`object-not-visible`, `source-not-in-graph`, `source-moved`, the
|
|
598
|
+
`source-*` codes of locating the file, `manifest-unavailable`,
|
|
599
|
+
`published-note-unavailable`, `stale-source`) is remembered in memory with its
|
|
600
|
+
code and offered again only at the full reconciliation cadence of the engine,
|
|
601
|
+
never on every tick. At most 16 edits are observed per tick, in the order they
|
|
602
|
+
were observed, starting where the last tick stopped. The tick reports
|
|
603
|
+
`observed`: for each edit looked at, its identifiers, `observed` or `refused`,
|
|
604
|
+
a code, and for a recorded one the kind and state of the operation.
|
|
605
|
+
|
|
606
|
+
### Routing
|
|
607
|
+
|
|
608
|
+
The routing key is the whole identity `(workspaceId, repoId, nodeId)`. The store
|
|
609
|
+
is the one every other writer of proposals uses for a root:
|
|
610
|
+
`<repository root>/.atelier-proposals`. Two repositories that hold the same
|
|
611
|
+
relative path and the same local node id have different stores and different
|
|
612
|
+
operation identities. A route that cannot be resolved refuses, writes nothing,
|
|
613
|
+
and leaves the preserved bytes and the record of the object as they were:
|
|
614
|
+
|
|
615
|
+
| Code | Meaning |
|
|
616
|
+
| --- | --- |
|
|
617
|
+
| `invalid-operation`, `foreign-workspace` | the identity is malformed or belongs to another workspace |
|
|
618
|
+
| `repository-not-enrolled`, `repository-external`, `repository-root-unreadable` | the project does not enrol the repository on this machine |
|
|
619
|
+
| `route-withheld` | this machine may not see the object now (the rule source apply asks); `route-visibility-unknown` waits instead |
|
|
620
|
+
| `source-path-invalid`, `source-path-not-preservable` | the repository-relative path is unusable, or the store would trim or cut it (500 characters) |
|
|
621
|
+
| `proposal-store-unsafe`, `proposal-store-inside-managed-root` | the store directory is a link or a file, or overlaps private state or a vault |
|
|
622
|
+
| `proposal-store-not-ignored`, `proposal-store-ignore-unknown` | the store does not exist yet and git would report it; nothing is created that changes `git status` |
|
|
623
|
+
|
|
624
|
+
### Operation identity and deduplication
|
|
625
|
+
|
|
626
|
+
The adapter operation identity is `pa-` and the SHA-256 of the identity and the
|
|
627
|
+
idempotency key of the edit operation, joined by a character no identifier can
|
|
628
|
+
hold: 67 lower-case characters. It is carried in `payload.adapter.operationId`
|
|
629
|
+
of the proposal, a member the store persists as JSON without normalising it.
|
|
630
|
+
The identifier the store gives a proposal is seeded with the time and decides
|
|
631
|
+
nothing here.
|
|
632
|
+
|
|
633
|
+
Per operation, under a private lock per repository (the generation lock of the
|
|
634
|
+
maintenance engine; taken over only with proof that its holder is gone, never
|
|
635
|
+
because time passed):
|
|
636
|
+
|
|
637
|
+
1. the operation is recorded in the adapter queue,
|
|
638
|
+
`<state>/state/proposals/<repo>/operations/<operation>--NNNNNN.json`:
|
|
639
|
+
immutable, owner-only, canonical files, each naming the digest of the one
|
|
640
|
+
before it;
|
|
641
|
+
2. the ledger of the store is read and its room judged;
|
|
642
|
+
3. the persisted store is searched for a proposal that carries the operation
|
|
643
|
+
identity, and one is created only when there is none;
|
|
644
|
+
4. `submitted` is recorded, the proposal is read back, and `acknowledged` is
|
|
645
|
+
recorded with an `atelier-obsidian-proposal-receipt/v1` that binds the edit
|
|
646
|
+
to the proposal (`dedupe`: `new`, `recovered` after a lost acknowledgement,
|
|
647
|
+
`duplicate` when an acknowledged operation is offered again).
|
|
648
|
+
|
|
649
|
+
A crash before the append leaves a queued record and no proposal; after it, a
|
|
650
|
+
proposal that step 3 finds. An unchanged tick reads the queue and nothing else:
|
|
651
|
+
no store is opened and nothing is written. An operation whose edit was withdrawn
|
|
652
|
+
or superseded while it waited is refused `edit-withdrawn` or `edit-superseded`.
|
|
653
|
+
|
|
654
|
+
### What a proposal holds
|
|
655
|
+
|
|
656
|
+
Identifiers, the repository-relative source path, the lens code and reason,
|
|
657
|
+
byte offsets into the edited note, digests and recovery references of the
|
|
658
|
+
preserved bytes, and sentences made from those codes. It holds no text of any
|
|
659
|
+
note: what a person typed can carry the title of another document, and a
|
|
660
|
+
reviewer of one repository must not learn a title of another repository, or of
|
|
661
|
+
a withheld document, from a proposal. The preserved bytes stay in private state.
|
|
662
|
+
|
|
663
|
+
### Ledger limits and backpressure
|
|
664
|
+
|
|
665
|
+
The limits are those of the existing ledger and are not widened: 256 KiB a
|
|
666
|
+
line, 16 MiB, 10,000 events, and an unreadable line refuses every append.
|
|
667
|
+
|
|
668
|
+
| Code | Outcome |
|
|
669
|
+
| --- | --- |
|
|
670
|
+
| `proposal-too-large` | refused before anything is appended; nothing is cut to fit |
|
|
671
|
+
| `ledger-full` | fewer than 1 + 32 events or less than the line + 512 KiB of room (the reserve is the reviewers'); the operation waits as `backpressure` with its edit retained |
|
|
672
|
+
| `ledger-corrupt` | refused, for that repository only; other repositories progress in the same tick |
|
|
673
|
+
| `store-unavailable` | the store cannot be read or is locked now; waits like a full one |
|
|
674
|
+
|
|
675
|
+
A waiting operation is tried at most 8 times, 60 seconds after the first attempt
|
|
676
|
+
and twice as long after each, up to an hour. At most 8 operations per repository
|
|
677
|
+
and 64 unexamined edits are looked at per tick. A repository holds at most 4096
|
|
678
|
+
operations, 256 of them open, 64 records of 16 KiB each; a full queue refuses
|
|
679
|
+
`queue-full` and records nothing. Nothing is compacted, rotated or deleted, in
|
|
680
|
+
the ledger or in the queue. A refused or exhausted operation is final until the
|
|
681
|
+
adapter's `requeue` is called for it, which is a person's decision after making
|
|
682
|
+
room or fixing the route; it is an exported function, and no command binds it
|
|
683
|
+
yet.
|
|
684
|
+
|
|
685
|
+
`atelier obsidian proposals list` and `show OPERATION` are read-only: per
|
|
686
|
+
repository the counts by state, the codes of the waiting and the refused, and
|
|
687
|
+
the events and bytes the ledger has left; never note or source text.
|