@mnstry/atelier 0.2.0-alpha.11 → 0.2.0-alpha.13
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 +1005 -0
- package/README.md +149 -9
- package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
- package/contracts/atelier-architecture.v1.schema.json +258 -0
- package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
- package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
- package/contracts/atelier-build.v1.schema.json +1076 -0
- package/contracts/atelier-capability.v1.schema.json +1252 -0
- package/contracts/atelier-coordination.v1.schema.json +1050 -0
- package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
- package/contracts/atelier-harness.v1.schema.json +234 -0
- package/contracts/atelier-ingestion.v1.schema.json +806 -0
- package/contracts/atelier-inquiry.v1.schema.json +1772 -0
- package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
- package/contracts/atelier-interaction.v1.schema.json +553 -0
- package/contracts/atelier-judgment.v1.schema.json +690 -0
- package/contracts/atelier-knowledge.v1.schema.json +1512 -0
- package/contracts/atelier-learning.v1.schema.json +1095 -0
- package/contracts/atelier-migration.v3.schema.json +64 -0
- package/contracts/atelier-obsidian-generation-manifest.v2.schema.json +372 -0
- package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
- package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
- package/contracts/atelier-template-adoption.v1.schema.json +223 -0
- package/contracts/atelier-template-profile.v1.schema.json +1168 -0
- package/contracts/atelier-trackable.v1.schema.json +781 -0
- package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
- package/contracts/compat-baseline.json +1 -1
- package/docs/architecture.md +89 -0
- package/docs/assurance-controls.md +4 -3
- package/docs/blocks/capability-stewardship.md +17 -0
- package/docs/blocks/claims.md +16 -3
- package/docs/blocks/inquiry-harnesses.md +19 -0
- package/docs/blocks/learning-harnesses.md +17 -0
- package/docs/capability-stewardship.md +269 -0
- package/docs/coordination.md +51 -0
- package/docs/decision-practices.md +111 -0
- package/docs/decisions.md +29 -0
- package/docs/evidence-navigation.md +237 -0
- package/docs/exact-upgrades.md +3 -2
- package/docs/ingestion.md +181 -0
- package/docs/inquiry-harnesses.md +211 -0
- package/docs/install.md +80 -3
- package/docs/intake-and-guides.md +5 -0
- package/docs/integration-contract-decisions.md +24 -2
- package/docs/interaction-and-reflection.md +72 -0
- package/docs/knowledge-graph.md +20 -0
- package/docs/knowledge-health/api.md +30 -0
- package/docs/knowledge-health/assessment/api.md +96 -0
- package/docs/knowledge-health/assessment/capabilities.md +40 -0
- package/docs/knowledge-health/assessment/evaluation.md +27 -0
- package/docs/knowledge-health/assessment/facilitator.md +30 -0
- package/docs/knowledge-health/assessment/quickstart.md +61 -0
- package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
- package/docs/knowledge-health/devday/facilitator.md +16 -0
- package/docs/knowledge-health/devday/participant-response.md +22 -0
- package/docs/knowledge-health/devday/public-default.md +107 -0
- package/docs/knowledge-health/devday/quickstart.md +18 -0
- package/docs/knowledge-health/devday/support.md +22 -0
- package/docs/knowledge-health/exercise.md +17 -0
- package/docs/knowledge-health/facilitator.md +20 -0
- package/docs/knowledge-health/quickstart.md +94 -0
- package/docs/knowledge-health/readme.md +15 -0
- package/docs/knowledge-health/support.md +34 -0
- package/docs/knowledge-setup.md +248 -0
- package/docs/knowledge-workspace.md +264 -0
- package/docs/learning-harnesses.md +238 -0
- package/docs/learning.md +215 -0
- package/docs/local-services.md +242 -26
- package/docs/obsidian-contract.md +878 -36
- package/docs/obsidian-plugin.md +619 -0
- package/docs/obsidian.md +809 -64
- package/docs/optional-ingestion.md +83 -0
- package/docs/personal-workspace.md +257 -0
- package/docs/practical-judgment.md +92 -0
- package/docs/progressive-ingestion-acceptance.md +89 -0
- package/docs/progressive-ingestion.md +235 -0
- package/docs/project-command-inventory.md +1 -0
- package/docs/release-engineering.md +56 -1
- package/docs/repo-boundary-guard.md +94 -0
- package/docs/research-starter-quickstart.md +66 -0
- package/docs/research-starter.md +114 -0
- package/docs/semantic-ingestion.md +198 -0
- package/docs/semantic-operation.md +317 -0
- package/docs/skill-steward.md +6 -0
- package/docs/template-profile.md +164 -0
- package/docs/template-upgrades.md +114 -0
- package/docs/trackables.md +111 -0
- package/docs/upgrade.md +68 -3
- package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
- package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
- package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
- package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
- package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
- package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
- package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
- package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
- package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
- package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
- package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
- package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
- package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
- package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
- package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
- package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
- package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
- package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
- package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
- package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
- package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
- package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
- package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
- package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
- package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
- package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
- package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
- package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
- package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
- package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
- package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
- package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
- package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
- package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
- package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
- package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
- package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
- package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
- package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
- package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
- package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
- package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
- package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
- package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
- package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
- package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
- package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
- package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
- package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
- package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
- package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
- package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
- package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
- package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
- package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
- package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
- package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
- package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
- package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
- package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
- package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
- package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
- package/fixtures/atelier-interaction/act/valid/document.json +24 -0
- package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
- package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
- package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
- package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
- package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
- package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
- package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
- package/fixtures/atelier-judgment/case/valid/document.json +75 -0
- package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
- package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
- package/fixtures/atelier-judgment/native-decision.md +3 -0
- package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
- package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
- package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
- package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
- package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
- package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
- package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
- package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
- package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
- package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
- package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
- package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
- package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
- package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-learning/invalid/authority.json +14 -0
- package/fixtures/atelier-learning/valid/capture.json +13 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
- package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
- package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
- package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
- package/fixtures/atelier-trackable/request/valid/document.json +22 -0
- package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
- package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
- package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
- package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
- package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
- package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
- package/fixtures/capability-packages/adoption.example.json +70 -0
- package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
- package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
- package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
- package/fixtures/capability-packages/research/capability-package.json +88 -0
- package/fixtures/capability-packages/research/capability-release.json +134 -0
- package/fixtures/capability-packages/research/evaluation.md +15 -0
- package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
- package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
- package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
- package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
- package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
- package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
- package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
- package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
- package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
- package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
- package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
- package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
- package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
- package/fixtures/evidence-navigation/valid/claim.json +36 -0
- package/fixtures/evidence-navigation/valid/host.json +16 -0
- package/fixtures/evidence-navigation/valid/profile.json +16 -0
- package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
- package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
- package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
- package/fixtures/harnesses/learning-cycle.json +780 -0
- package/fixtures/inquiry/workshop.json +675 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
- package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
- package/fixtures/knowledge-health/calibration-suite.json +26 -0
- package/fixtures/knowledge-health/calibration-trial.json +37 -0
- package/fixtures/knowledge-health/corrected/checklist.md +18 -0
- package/fixtures/knowledge-health/devday/participant-record.json +19 -0
- package/fixtures/knowledge-health/expected-summary.json +27 -0
- package/fixtures/knowledge-health/workshop/README.md +20 -0
- package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
- package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
- package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
- package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
- package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
- package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
- package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
- package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/case-only-collision.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/flat-note-path.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/hidden-folder.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/identity-in-the-body.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/link-breaking-name.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-identity-region.v2.json +156 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-layout-version.v2.json +159 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/note-named-like-a-folder.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/unknown-region-kind.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/v1-contract-version.v2.json +161 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/valid/three-notes.v2.json +160 -0
- package/fixtures/obsidian/edits/workspace.json +1 -1
- package/fixtures/obsidian/materialization/expected-full.json +21 -21
- package/fixtures/obsidian/materialization/expected-full.layout-1.json +99 -0
- package/fixtures/obsidian/materialization/expected-scoped.json +7 -7
- package/fixtures/obsidian/materialization/expected-scoped.layout-1.json +39 -0
- package/fixtures/personal-workspace/overlay.json +24 -0
- package/fixtures/templates/local-library.v1.json +75 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
- package/package.json +64 -6
- package/plugins/obsidian/main.js +444 -0
- package/plugins/obsidian/manifest.json +9 -0
- package/plugins/obsidian/styles.css +22 -0
- package/skills/claude/atelier-build-harness/SKILL.md +59 -0
- package/skills/claude/atelier-coordination/SKILL.md +32 -0
- package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/claude/atelier-interaction/SKILL.md +34 -0
- package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/claude/atelier-research-harness/SKILL.md +43 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
- package/skills/claude/atelier-trackables/SKILL.md +40 -0
- package/skills/codex/atelier-build-harness/SKILL.md +59 -0
- package/skills/codex/atelier-coordination/SKILL.md +32 -0
- package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/codex/atelier-interaction/SKILL.md +34 -0
- package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/codex/atelier-research-harness/SKILL.md +43 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
- package/skills/codex/atelier-trackables/SKILL.md +40 -0
- package/src/architecture/catalog.mjs +355 -0
- package/src/architecture/index.mjs +93 -0
- package/src/boundary/content-rules.mjs +4 -2
- package/src/boundary/delegation-command.mjs +158 -0
- package/src/boundary/delegation.mjs +396 -0
- package/src/boundary/policy.mjs +52 -10
- package/src/build/git.mjs +18 -0
- package/src/build/index.mjs +3 -0
- package/src/build/ledger.mjs +88 -0
- package/src/capabilities/adoption.mjs +381 -0
- package/src/capabilities/evidence.mjs +140 -0
- package/src/capabilities/files.mjs +124 -0
- package/src/capabilities/index.mjs +4 -0
- package/src/capabilities/instructions.mjs +250 -0
- package/src/capabilities/package.mjs +136 -0
- package/src/cli/command-failure.mjs +36 -0
- package/src/cli/execute-command.mjs +16 -2
- package/src/cli/run.mjs +65 -9
- package/src/coauthor/store.mjs +43 -2
- package/src/collaboration/proposals.mjs +189 -0
- package/src/commands/architecture.mjs +27 -0
- package/src/commands/boundary.mjs +3 -0
- package/src/commands/capability.mjs +73 -0
- package/src/commands/enroll.mjs +3 -0
- package/src/commands/harness.mjs +79 -0
- package/src/commands/ingest.mjs +39 -0
- package/src/commands/init.mjs +4 -1
- package/src/commands/inquiry.mjs +66 -0
- package/src/commands/knowledge-semantic.mjs +128 -0
- package/src/commands/knowledge.mjs +119 -0
- package/src/commands/learn.mjs +61 -0
- package/src/commands/obsidian.mjs +832 -62
- package/src/commands/practice.mjs +24 -0
- package/src/commands/setup.mjs +22 -2
- package/src/commands/trackable.mjs +19 -0
- package/src/contracts/corpus.mjs +36 -0
- package/src/coordination/index.mjs +110 -0
- package/src/egress/forbidden-egress.mjs +2 -0
- package/src/evidence-navigation/contracts.mjs +60 -0
- package/src/evidence-navigation/evaluate.mjs +69 -0
- package/src/evidence-navigation/generation.mjs +22 -0
- package/src/evidence-navigation/index.mjs +2 -0
- package/src/evidence-navigation/local.mjs +167 -0
- package/src/evidence-navigation/schema.generated.mjs +604 -0
- package/src/evidence-navigation/types.mjs +12 -0
- package/src/export/atelier-export-contract.mjs +35 -8
- package/src/graph/enroll.mjs +248 -0
- package/src/graph/graph.mjs +17 -1
- package/src/graph/knowledge-graph.mjs +58 -8
- package/src/harnesses/contracts.mjs +41 -0
- package/src/harnesses/exchange.mjs +154 -0
- package/src/harnesses/feedback.mjs +16 -0
- package/src/harnesses/history.mjs +37 -0
- package/src/harnesses/index.mjs +4 -0
- package/src/harnesses/store.mjs +29 -0
- package/src/ingestion/contracts.mjs +41 -0
- package/src/ingestion/evaluation.mjs +119 -0
- package/src/ingestion/optional/composition/index.mjs +171 -0
- package/src/ingestion/optional/evaluation/index.mjs +219 -0
- package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
- package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
- package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
- package/src/ingestion/optional/graphify/json_bridge.py +163 -0
- package/src/ingestion/optional/graphify/query_receiving.py +209 -0
- package/src/ingestion/optional/index.mjs +9 -0
- package/src/ingestion/optional/jev/bridge.mjs +166 -0
- package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
- package/src/ingestion/optional/jev/judgments.mjs +174 -0
- package/src/ingestion/optional/jev/native-translation.mjs +32 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
- package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
- package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
- package/src/ingestion/optional/vanilla/index.mjs +218 -0
- package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
- package/src/ingestion/processors.mjs +218 -0
- package/src/ingestion/semantic.mjs +203 -0
- package/src/ingestion/store.mjs +372 -0
- package/src/inquiry/beliefs.mjs +32 -0
- package/src/inquiry/contracts.mjs +23 -0
- package/src/inquiry/index.mjs +5 -0
- package/src/inquiry/ledger.mjs +135 -0
- package/src/inquiry/lenses.mjs +15 -0
- package/src/inquiry/projection.mjs +77 -0
- package/src/inquiry/store.mjs +35 -0
- package/src/intake/read-scope.mjs +12 -0
- package/src/intake/store.mjs +69 -6
- package/src/interaction/index.mjs +91 -0
- package/src/judgment/index.mjs +82 -0
- package/src/judgment/practice-evaluation.mjs +70 -0
- package/src/judgment/practice.mjs +144 -0
- package/src/knowledge/context.mjs +217 -0
- package/src/knowledge/index.mjs +5 -0
- package/src/knowledge/ingestion.mjs +79 -0
- package/src/knowledge/intake.mjs +33 -0
- package/src/knowledge/ledger.mjs +110 -0
- package/src/knowledge/participatory/guidance.mjs +36 -0
- package/src/knowledge/participatory/guided-contribution.mjs +44 -0
- package/src/knowledge/participatory/index.mjs +2 -0
- package/src/knowledge/participatory/installed-workshop.mjs +108 -0
- package/src/knowledge/plan.mjs +67 -0
- package/src/knowledge/projection.mjs +43 -0
- package/src/knowledge/semantic-operation.mjs +567 -0
- package/src/knowledge/sessions.mjs +467 -0
- package/src/knowledge/workspace.mjs +295 -0
- package/src/knowledge-health/assessment.mjs +165 -0
- package/src/knowledge-health/capabilities.mjs +350 -0
- package/src/knowledge-health/context.mjs +62 -0
- package/src/knowledge-health/example.mjs +47 -0
- package/src/knowledge-health/index.mjs +10 -0
- package/src/knowledge-health/owner-join.mjs +88 -0
- package/src/knowledge-health/proof/common.mjs +24 -0
- package/src/knowledge-health/proof/installed-check.mjs +100 -0
- package/src/knowledge-health/proof/native-readback.mjs +37 -0
- package/src/knowledge-health/proof/rehearse.mjs +68 -0
- package/src/knowledge-health/proof/validate-participant.mjs +17 -0
- package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
- package/src/knowledge-health/proof/workshop-check.mjs +36 -0
- package/src/knowledge-health/rule-identity.mjs +44 -0
- package/src/knowledge-health/workshop-composition.mjs +19 -0
- package/src/knowledge-health/workshop-profile.mjs +176 -0
- package/src/knowledge-health/workshop.mjs +99 -0
- package/src/learning/adapters.mjs +68 -0
- package/src/learning/assistance.mjs +127 -0
- package/src/learning/contracts.mjs +65 -0
- package/src/learning/store.mjs +272 -0
- package/src/personal-workspace/index.mjs +391 -0
- package/src/personal-workspace/selection.mjs +385 -0
- package/src/project/config.mjs +8 -0
- package/src/project/durable-state.mjs +3 -1
- package/src/project/file-class.mjs +1 -1
- package/src/project/private-state.mjs +10 -1
- package/src/projection/obsidian/contracts.mjs +224 -18
- package/src/projection/obsidian/edits/apply.mjs +63 -19
- package/src/projection/obsidian/edits/regions.mjs +137 -42
- package/src/projection/obsidian/materialize/index.mjs +6 -3
- package/src/projection/obsidian/materialize/path-registry.mjs +443 -57
- package/src/projection/obsidian/materialize/prepare-view.mjs +382 -131
- package/src/projection/obsidian/materialize/redaction.mjs +327 -0
- package/src/projection/obsidian/materialize/settings.mjs +113 -34
- package/src/projection/obsidian/personal-workspace.mjs +436 -0
- package/src/projection/obsidian/plugin-bridge/bundle.mjs +50 -0
- package/src/projection/obsidian/plugin-bridge/channel.mjs +174 -0
- package/src/projection/obsidian/plugin-bridge/index.mjs +10 -0
- package/src/projection/obsidian/proposals/adapter.mjs +47 -8
- package/src/projection/obsidian/proposals/observation.mjs +20 -7
- package/src/projection/obsidian/publication/bridge-script.mjs +11 -5
- package/src/projection/obsidian/publication/index.mjs +2 -1
- package/src/projection/obsidian/publication/publisher.mjs +312 -36
- package/src/projection/obsidian/publication/transport.mjs +63 -8
- package/src/projection/obsidian/publication/vault-list.mjs +200 -0
- package/src/projection/obsidian/recovery/index.mjs +4 -1
- package/src/projection/obsidian/recovery/restart.mjs +3 -1
- package/src/projection/obsidian/recovery/store.mjs +168 -8
- package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
- package/src/projection/obsidian/selection-ui/focus.mjs +14 -7
- package/src/projection/obsidian/selection-ui/selection-state.mjs +2 -2
- package/src/projection/obsidian/selection-ui/selection.mjs +8 -3
- package/src/readiness/readiness.mjs +10 -3
- package/src/reflection/index.mjs +56 -0
- package/src/runtime/obsidian/app-capability.mjs +104 -12
- package/src/runtime/obsidian/app-production-seams.mjs +161 -42
- package/src/runtime/obsidian/app-registration.mjs +313 -0
- package/src/runtime/obsidian/app-restart.mjs +154 -0
- package/src/runtime/obsidian/engine.mjs +288 -29
- package/src/runtime/obsidian/extension-points.mjs +1 -1
- package/src/runtime/obsidian/index.mjs +15 -8
- package/src/runtime/obsidian/launch-plan.mjs +91 -0
- package/src/runtime/obsidian/lifecycle.mjs +193 -22
- package/src/runtime/obsidian/login-item.mjs +332 -0
- package/src/runtime/obsidian/machine-settings.mjs +119 -12
- package/src/runtime/obsidian/opening.mjs +369 -63
- package/src/runtime/obsidian/pending-edits.mjs +10 -0
- package/src/runtime/obsidian/pipeline.mjs +71 -3
- package/src/runtime/obsidian/plugin-channel.mjs +289 -0
- package/src/runtime/obsidian/plugin-choice.mjs +182 -0
- package/src/runtime/obsidian/plugin-drift.mjs +124 -0
- package/src/runtime/obsidian/plugin-presence.mjs +61 -0
- package/src/runtime/obsidian/process-identity.mjs +20 -10
- package/src/runtime/obsidian/project-views.mjs +282 -0
- package/src/runtime/obsidian/questions.mjs +48 -0
- package/src/runtime/obsidian/release-watch.mjs +56 -0
- package/src/runtime/obsidian/service-main.mjs +113 -17
- package/src/runtime/obsidian/service-manager-production.mjs +61 -0
- package/src/runtime/obsidian/service-managers.mjs +275 -0
- package/src/runtime/obsidian/service-record.mjs +105 -5
- package/src/runtime/obsidian/service-server.mjs +50 -12
- package/src/runtime/obsidian/service.mjs +134 -7
- package/src/runtime/obsidian/startup-adapters.mjs +59 -21
- package/src/runtime/obsidian/state-store.mjs +13 -1
- package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
- package/src/runtime/obsidian/vault-location.mjs +300 -0
- package/src/runtime/obsidian/view-counts.mjs +34 -0
- package/src/server/local-sidecar.mjs +193 -7
- package/src/server/server.mjs +45 -6
- package/src/template-bindings/project.mjs +92 -0
- package/src/templates/profile.mjs +225 -0
- package/src/trackables/domain.mjs +175 -0
- package/src/trackables/store.mjs +114 -0
- package/src/ui/knowledge-page.mjs +1388 -0
- package/src/upgrade/explanation.mjs +1 -1
- package/src/upgrade/template-participant.mjs +171 -0
- package/src/upgrade/transaction.mjs +72 -25
- package/src/upgrade/upgrade.mjs +2 -1
- package/templates/external-project-workspace/pack-lifecycle.json +6 -1
- package/templates/knowledge-workspace/.gitattributes +2 -0
- package/templates/knowledge-workspace/AGENTS.md +27 -0
- package/templates/knowledge-workspace/README.md +44 -0
- package/templates/knowledge-workspace/atelier.project.json +22 -0
- package/templates/knowledge-workspace/gitignore +6 -0
- package/templates/knowledge-workspace/knowledge-plan.json +111 -0
- package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
- package/templates/knowledge-workspace/records/forecast.md +22 -0
- package/templates/knowledge-workspace/records/inspection.md +25 -0
- package/templates/knowledge-workspace/records/telescope.md +24 -0
- package/templates/knowledge-workspace/repo-access.v1.json +9 -0
|
@@ -3,12 +3,13 @@ import fsp from 'node:fs/promises'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
5
5
|
import { acquirePrivateLock, publishPrivateFile, syncPrivateDirectory } from '../../../project/durable-state.mjs'
|
|
6
|
-
import { ObsidianContractRefusal, assertObsidianContract } from '../contracts.mjs'
|
|
7
|
-
import { isPolicySettingsPath, isUserOwnedSettingsPath,
|
|
6
|
+
import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, assertObsidianContract, manifestLayoutVersion } from '../contracts.mjs'
|
|
7
|
+
import { isPluginOwnedPath, isPolicySettingsPath, isUserOwnedSettingsPath, preparePolicySettingsFile } from '../materialize/settings.mjs'
|
|
8
|
+
import { PLUGIN_DATA_MODE, PLUGIN_DATA_PATH, PLUGIN_SOURCE_MODE } from '../plugin-bridge/channel.mjs'
|
|
8
9
|
import { createJournal, newJournalId } from '../recovery/journal.mjs'
|
|
9
10
|
import { recheckDisplacedFiles } from '../recovery/late-writer.mjs'
|
|
10
11
|
import { publishedSinceCommit, reconcileUnit, recordDisplaced, recoverPublicationsLocked, retireStagedFile } from '../recovery/restart.mjs'
|
|
11
|
-
import { PublicationRefusal, acquireVaultLock, readFileBytes, readFileDigest, refuse, sha256Digest } from '../recovery/store.mjs'
|
|
12
|
+
import { PublicationRefusal, acquireVaultLock, allocatedFolderState, readFileBytes, readFileDigest, refuse, sha256Digest } from '../recovery/store.mjs'
|
|
12
13
|
import { PROTOCOL_ID, isAddressableVaultPath } from './bridge-script.mjs'
|
|
13
14
|
import { probeExchange } from './exchange.mjs'
|
|
14
15
|
import { CRASH_INJECTION_TEST_SEAM } from './test-seam.mjs'
|
|
@@ -36,14 +37,53 @@ import { createDirectAdapter } from './transport.mjs'
|
|
|
36
37
|
// from `recovery/<journalId>/<unit>/`, never from staging. Staging holds only
|
|
37
38
|
// generated bytes that no exchange names: every candidate while it is written,
|
|
38
39
|
// and the candidates of created files until they are linked into the vault.
|
|
40
|
+
//
|
|
41
|
+
// Atelier's own plugin files (kind `plugin`) go through the same units, with
|
|
42
|
+
// two differences: each is compared with what the disk holds when the run
|
|
43
|
+
// begins rather than with an earlier generation, so whatever is there is
|
|
44
|
+
// displaced to recovery and never lost, and a plugin path that a person has to
|
|
45
|
+
// repair never holds the view back. The plugin's data file names a bearer and
|
|
46
|
+
// is written only into a vault root that is private to this user.
|
|
47
|
+
//
|
|
48
|
+
// First publication into a vault no Obsidian lists (`direct-unheld`). A running
|
|
49
|
+
// or unknowable app that cannot be coordinated with still refuses the whole
|
|
50
|
+
// publication, as ever, except in one case: the view has never been committed,
|
|
51
|
+
// its vault is the folder Atelier allocated for it (the record's device and
|
|
52
|
+
// inode), and the evidence (`unheldEvidence`, read by the caller from the app's
|
|
53
|
+
// own settings file) says no list this account's Obsidian keeps names that
|
|
54
|
+
// folder, a folder above it or one inside it, on macOS or Linux. The vault is
|
|
55
|
+
// then filled in this process, with no editor, by creating files only: every
|
|
56
|
+
// file appears through the exclusive link, where nothing is, or not at all. A
|
|
57
|
+
// unit that would replace or remove a file, or write a settings file over one
|
|
58
|
+
// that is there, refuses as `editor-uncoordinated` (blocking) and writes
|
|
59
|
+
// nothing. No editor can hold an unsaved buffer of a file that does not exist,
|
|
60
|
+
// so nothing a person wrote can be lost, whoever holds the vault. The evidence
|
|
61
|
+
// is read again immediately before the first unit and whenever two seconds
|
|
62
|
+
// have passed since the last reading; once a reading gives no evidence, every
|
|
63
|
+
// remaining unit refuses and nothing is committed. A run that completes is
|
|
64
|
+
// committed before `open` tells the app about the vault; a run that stops
|
|
65
|
+
// partway reports why, and what it left is published through the app.
|
|
39
66
|
|
|
40
67
|
const hex = (digest) => digest.slice('sha256:'.length)
|
|
41
68
|
const iso = (clock) => { const value = clock(); return (value instanceof Date ? value : new Date(value)).toISOString() }
|
|
42
69
|
const REFUSED_BY_EDIT = new Set(['editor-edit', 'disk-changed'])
|
|
43
70
|
const unreleased = new Map()
|
|
44
|
-
|
|
71
|
+
// A release kept earlier whose lock folder is gone when it is tried again (the vault deleted, replaced, or made again
|
|
72
|
+
// empty at its path) holds nothing there, and is dropped rather than holding every later publication of that vault back.
|
|
73
|
+
// A vault put back holds its ticket again, and the release kept for it then succeeds.
|
|
74
|
+
const lockFolderGone = (error) => error?.code === 'ENOENT' || error?.code === 'ENOTDIR'
|
|
75
|
+
|
|
76
|
+
// A note another program replaces by rename can change between the check of its leaf and the open
|
|
77
|
+
// (ELEAFCHANGED). Every rename leaves a complete file, so it is read again; one that keeps changing is left to
|
|
78
|
+
// publishView, which reports it for that unit only.
|
|
79
|
+
const READ_ATTEMPTS = 5
|
|
45
80
|
function readNote(file) {
|
|
46
|
-
|
|
81
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
82
|
+
try { return readFileBytes(file) } catch (error) {
|
|
83
|
+
if (error.code === 'ENOENT') return null
|
|
84
|
+
if (error.code !== 'ELEAFCHANGED' || attempt >= READ_ATTEMPTS) throw error
|
|
85
|
+
}
|
|
86
|
+
}
|
|
47
87
|
}
|
|
48
88
|
|
|
49
89
|
function validatePreparedView(preparedView, store) {
|
|
@@ -61,22 +101,35 @@ function validatePreparedView(preparedView, store) {
|
|
|
61
101
|
if (expected.has(declared)) refuse('invalid-prepared-view', 'the manifest declares one path more than once')
|
|
62
102
|
expected.set(declared, fileDigest)
|
|
63
103
|
}
|
|
104
|
+
// Atelier's plugin files are pinned by the settings unit's record in the manifest.
|
|
105
|
+
const pinnedPlugin = manifest.ext?.[OBSIDIAN_EXT_KEY]?.settings?.pluginOwned?.files
|
|
106
|
+
const pinned = new Map(Array.isArray(pinnedPlugin) ? pinnedPlugin.map((file) => [file?.path, file?.digest]) : [])
|
|
64
107
|
const seen = new Set()
|
|
65
108
|
for (const file of preparedView.files) {
|
|
66
109
|
if (!isAddressableVaultPath(file.path) || isUserOwnedSettingsPath(file.path)) refuse('invalid-prepared-view', 'a prepared path is outside what the publisher may write')
|
|
67
110
|
if (seen.has(file.path)) refuse('invalid-prepared-view', 'a prepared path appears twice')
|
|
68
111
|
seen.add(file.path)
|
|
69
112
|
if (file.kind === 'settings') {
|
|
70
|
-
if (!isPolicySettingsPath(file.path)) refuse('invalid-prepared-view', 'only
|
|
113
|
+
if (!isPolicySettingsPath(file.path)) refuse('invalid-prepared-view', 'only a policy settings file may be prepared as settings')
|
|
71
114
|
continue
|
|
72
115
|
}
|
|
73
|
-
if (
|
|
116
|
+
if (file.kind === 'plugin') {
|
|
117
|
+
if (!isPluginOwnedPath(file.path) || !Buffer.isBuffer(file.bytes) || sha256Digest(file.bytes) !== file.digest || pinned.get(file.path) !== file.digest) {
|
|
118
|
+
refuse('invalid-prepared-view', 'a plugin file, its digest and the digest the manifest pins disagree')
|
|
119
|
+
}
|
|
120
|
+
// Read by the app, never run by anyone: the data file owner-only, the others readable.
|
|
121
|
+
if (file.mode !== (file.path === PLUGIN_DATA_PATH ? PLUGIN_DATA_MODE : PLUGIN_SOURCE_MODE)) refuse('invalid-prepared-view', 'a plugin file has a mode Atelier never gives one')
|
|
122
|
+
pinned.delete(file.path)
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
if (!['note', 'attachment'].includes(file.kind) || isPolicySettingsPath(file.path) || isPluginOwnedPath(file.path)) refuse('invalid-prepared-view', 'a prepared file has an unknown kind')
|
|
74
126
|
if (!Buffer.isBuffer(file.bytes) || sha256Digest(file.bytes) !== file.digest || expected.get(file.path) !== file.digest) {
|
|
75
127
|
refuse('invalid-prepared-view', 'prepared bytes, their digest and the manifest disagree')
|
|
76
128
|
}
|
|
77
129
|
expected.delete(file.path)
|
|
78
130
|
}
|
|
79
131
|
if (expected.size > 0) refuse('invalid-prepared-view', 'the manifest names a file the prepared view does not carry')
|
|
132
|
+
if (pinned.size > 0) refuse('invalid-prepared-view', 'the manifest pins a plugin file the prepared view does not carry')
|
|
80
133
|
let parsedBytes = null
|
|
81
134
|
if (Buffer.isBuffer(preparedView.manifestBytes)) {
|
|
82
135
|
try { parsedBytes = JSON.parse(preparedView.manifestBytes.toString('utf8')) } catch { refuse('invalid-prepared-view', 'manifest bytes are not JSON') }
|
|
@@ -169,7 +222,65 @@ async function holdsCandidateBytes(vaultRoot, relativePath, candidateDigest) {
|
|
|
169
222
|
}
|
|
170
223
|
}
|
|
171
224
|
|
|
172
|
-
|
|
225
|
+
// The digest of what a vault path holds now: null when it is absent, sits
|
|
226
|
+
// under a parent that is not a real directory, is not a regular file or cannot
|
|
227
|
+
// be read. Only Atelier's plugin files are planned from it.
|
|
228
|
+
function currentDigestOf(vaultRoot, relativePath) {
|
|
229
|
+
let current = vaultRoot
|
|
230
|
+
for (const part of relativePath.split('/').slice(0, -1)) {
|
|
231
|
+
current = path.join(current, part)
|
|
232
|
+
const stat = fs.lstatSync(current, { throwIfNoEntry: false })
|
|
233
|
+
if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) return null
|
|
234
|
+
}
|
|
235
|
+
try { return readFileDigest(path.join(vaultRoot, relativePath)) } catch { return null }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Whether publishing would only leave this plugin path for the person, as it
|
|
239
|
+
// stands: a parent that is a link or not a directory, a leaf that is not a
|
|
240
|
+
// regular file (path-unsafe), or the data file where the vault root cannot hold
|
|
241
|
+
// the bearer and is not one Atelier makes private itself (vault-not-private;
|
|
242
|
+
// see ensurePrivateVaultRoot). Read-only. Conservative: a leaf nobody may
|
|
243
|
+
// read, a parent that cannot be looked into, and a vault root Atelier keeps
|
|
244
|
+
// whose mode cannot be changed are left for the person too, but count here as
|
|
245
|
+
// a drift, as before.
|
|
246
|
+
function leftForThePerson(store, relativePath) {
|
|
247
|
+
let current = store.vaultRoot
|
|
248
|
+
for (const part of relativePath.split('/').slice(0, -1)) {
|
|
249
|
+
current = path.join(current, part)
|
|
250
|
+
const stat = fs.lstatSync(current, { throwIfNoEntry: false })
|
|
251
|
+
if (!stat) break
|
|
252
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return true
|
|
253
|
+
}
|
|
254
|
+
const leaf = fs.lstatSync(path.join(store.vaultRoot, relativePath), { throwIfNoEntry: false })
|
|
255
|
+
if (leaf && !leaf.isFile()) return true
|
|
256
|
+
if (relativePath !== PLUGIN_DATA_PATH) return false
|
|
257
|
+
if (store.linkedVaultRoot === true) return true
|
|
258
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : null
|
|
259
|
+
const root = fs.lstatSync(store.vaultRoot, { throwIfNoEntry: false })
|
|
260
|
+
if (!root?.isDirectory() || uid === null || root.uid !== uid) return true
|
|
261
|
+
return (root.mode & 0o077) !== 0 && store.managedVaultRoot !== true
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// A generation that is already committed is published again, as it is, when a
|
|
265
|
+
// plugin file it carries is not on disk as it pins it: the person repaired a
|
|
266
|
+
// path Atelier had to leave, or removed or changed a file. The bytes are
|
|
267
|
+
// Atelier's and nothing else of the view changed, so the generation stays the
|
|
268
|
+
// same; its notes are kept, and its plugin units are planned from the disk as
|
|
269
|
+
// ever. A file prepared only where present that is gone is not a drift, and
|
|
270
|
+
// neither is one that publishing would only leave for the person again: it
|
|
271
|
+
// would change nothing, and it would need the app for nothing (a view that
|
|
272
|
+
// waits for an app for it would wait for a write that never comes).
|
|
273
|
+
function pluginFilesDrifted(preparedView, store) {
|
|
274
|
+
return preparedView.files.some((file) => {
|
|
275
|
+
if (file.kind !== 'plugin') return false
|
|
276
|
+
const current = currentDigestOf(store.vaultRoot, file.path)
|
|
277
|
+
if (current === file.digest || (current === null && file.onlyIfPresent === true)) return false
|
|
278
|
+
try { return !leftForThePerson(store, file.path) } catch { return true }
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// One unit per file the publication creates, keeps, replaces or removes. Exported for the oracle tests.
|
|
283
|
+
export function planUnits({ files, priorManifest, pointer, ledger, pluginDisk = new Map() }) {
|
|
173
284
|
const trusted = new Map()
|
|
174
285
|
for (const note of priorManifest?.notes ?? []) trusted.set(note.path, note.noteDigest)
|
|
175
286
|
for (const attachment of priorManifest?.attachments ?? []) trusted.set(attachment.path, attachment.digest)
|
|
@@ -177,24 +288,67 @@ function planUnits({ files, priorManifest, pointer, ledger }) {
|
|
|
177
288
|
for (const item of pointer?.retained ?? []) if (item.priorDigest && !item.movedToRecovery) trusted.set(item.path, item.priorDigest)
|
|
178
289
|
for (const [notePath, digest] of ledger) { if (digest === null) trusted.delete(notePath); else trusted.set(notePath, digest) }
|
|
179
290
|
|
|
291
|
+
// A file leaves as an attachment when the prior generation listed it as one. Layout 1 kept every file under
|
|
292
|
+
// `attachments/`, so there that folder says it too (of a file an interrupted run published); in layout 2 it is only
|
|
293
|
+
// a repository's folder.
|
|
294
|
+
const priorAttachments = new Set((priorManifest?.attachments ?? []).map((attachment) => attachment.path))
|
|
295
|
+
const underAttachments = !priorManifest || manifestLayoutVersion(priorManifest) === 1
|
|
180
296
|
const units = []
|
|
181
297
|
const present = new Set()
|
|
182
298
|
for (const file of files) {
|
|
183
299
|
present.add(file.path)
|
|
184
|
-
|
|
300
|
+
// A settings file prepared from the bytes it held when the view was prepared carries their digest: it is written only over them.
|
|
301
|
+
if (file.kind === 'settings') { units.push({ path: file.path, kind: 'settings', op: 'settings', ...(Object.hasOwn(file, 'expectedDigest') ? { expectedDigest: file.expectedDigest } : {}) }); continue }
|
|
302
|
+
if (file.kind === 'plugin') {
|
|
303
|
+
const current = pluginDisk.get(file.path) ?? null
|
|
304
|
+
// A plugin file the person removed while the plugin is off in this vault stays removed.
|
|
305
|
+
if (current === null && file.onlyIfPresent === true) { units.push({ path: file.path, kind: 'plugin', op: 'leave-absent' }); continue }
|
|
306
|
+
units.push({ path: file.path, kind: 'plugin', op: current === null ? 'create' : current === file.digest ? 'keep' : 'replace', baseDigest: current, candidateDigest: file.digest, bytes: file.bytes, mode: file.mode })
|
|
307
|
+
continue
|
|
308
|
+
}
|
|
185
309
|
const base = trusted.get(file.path)
|
|
186
310
|
const op = base === undefined ? 'create' : base === file.digest ? 'keep' : 'replace'
|
|
187
311
|
units.push({ path: file.path, kind: file.kind, op, baseDigest: base ?? null, candidateDigest: file.digest, bytes: file.bytes })
|
|
188
312
|
}
|
|
189
313
|
for (const [notePath, digest] of [...trusted].sort(([left], [right]) => (left < right ? -1 : 1))) {
|
|
190
|
-
if (!present.has(notePath)) units.push({ path: notePath, kind: notePath.startsWith('attachments/') ? 'attachment' : 'note', op: 'remove', baseDigest: digest, candidateDigest: null })
|
|
314
|
+
if (!present.has(notePath)) units.push({ path: notePath, kind: priorAttachments.has(notePath) || (underAttachments && notePath.startsWith('attachments/')) ? 'attachment' : 'note', op: 'remove', baseDigest: digest, candidateDigest: null })
|
|
191
315
|
}
|
|
192
316
|
units.forEach((unit, index) => { unit.unit = index })
|
|
193
317
|
return units
|
|
194
318
|
}
|
|
195
319
|
|
|
196
|
-
|
|
320
|
+
// The rules the oracle tests are sensitive to. Production always uses these; the tests substitute a deliberately
|
|
321
|
+
// broken one through publishViewForOracleTests to prove the oracle can fail.
|
|
322
|
+
export const PUBLICATION_PRIMITIVES = Object.freeze({
|
|
323
|
+
// The one operation the `direct-unheld` path writes into the vault: a file created where nothing is.
|
|
324
|
+
unheldWrites: (op) => op === 'create',
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// Whether the `direct-unheld` path applies, after the adapter found an app it cannot coordinate with (see the head of
|
|
328
|
+
// this file). Every condition must hold; an evidence reader that is absent, throws or answers anything but
|
|
329
|
+
// `unlisted: true` gives none.
|
|
330
|
+
async function unheldPathApplies({ probe, pointer, store, unheldEvidence, platform }) {
|
|
331
|
+
if (probe.code !== undefined || pointer !== null) return false
|
|
332
|
+
if (platform !== 'darwin' && platform !== 'linux') return false
|
|
333
|
+
const { allocation } = store
|
|
334
|
+
if (store.vaultOrigin !== 'allocated' || store.managedVaultRoot !== true || !allocation || store.vaultRoot !== allocation.path || allocatedFolderState(allocation) !== 'same') return false
|
|
335
|
+
return readsUnlisted(unheldEvidence, store)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function readsUnlisted(unheldEvidence, store) {
|
|
339
|
+
if (typeof unheldEvidence !== 'function') return false
|
|
340
|
+
try { return (await unheldEvidence({ vaultRoot: store.vaultRoot }))?.unlisted === true } catch { return false }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function publishView(options = {}) {
|
|
344
|
+
return publishViewForOracleTests(options, PUBLICATION_PRIMITIVES)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function publishViewForOracleTests(options = {}, primitives = PUBLICATION_PRIMITIVES) {
|
|
197
348
|
const { preparedView, protocolId, expectedGeneration, recoveryStore: store, adapter, clock = () => new Date(), quietPeriodMs = 1500, exchangeOptions = {} } = options
|
|
349
|
+
// `unheldEvidence({ vaultRoot })` answers whether the app's own list names this vault (see the head of this file).
|
|
350
|
+
const { unheldEvidence = null, platform = process.platform } = options
|
|
351
|
+
const rules = { ...PUBLICATION_PRIMITIVES, ...primitives }
|
|
198
352
|
const seam = options[CRASH_INJECTION_TEST_SEAM] ?? null
|
|
199
353
|
const crash = (point) => { if (seam && seam.at === point) seam.halt(point) }
|
|
200
354
|
if (!store || typeof store.commitManifest !== 'function') throw new TypeError('publishView needs a recoveryStore')
|
|
@@ -209,30 +363,43 @@ export async function publishView(options = {}) {
|
|
|
209
363
|
// A release that could not be written (a full disk) is finished first.
|
|
210
364
|
for (const lockPath of [store.vaultLockPath, store.lockPath]) {
|
|
211
365
|
if (!unreleased.has(lockPath)) continue
|
|
212
|
-
try { unreleased.get(lockPath)() } catch (error) { refuse('state-unwritable', 'private publication state cannot be written; nothing in the vault was touched', { cause: error.code ?? String(error.message) }) }
|
|
366
|
+
try { unreleased.get(lockPath)() } catch (error) { if (!lockFolderGone(error)) refuse('state-unwritable', 'private publication state cannot be written; nothing in the vault was touched', { cause: error.code ?? String(error.message) }) }
|
|
213
367
|
unreleased.delete(lockPath)
|
|
214
368
|
}
|
|
215
369
|
// Two locks, the view's and then the vault's: a second view or a second
|
|
216
370
|
// workspace state pointed at the same vault refuses instead of racing.
|
|
217
371
|
const acquire = (lockPath, take, held) => {
|
|
218
372
|
try { releases.push([lockPath, take()]) } catch (error) {
|
|
373
|
+
if (error instanceof PublicationRefusal) throw error
|
|
219
374
|
if (error.code === 'EEXIST') refuse('publication-in-progress', `another publication ${held} holds the lock`)
|
|
220
375
|
refuse('state-unwritable', 'private publication state cannot be written; nothing in the vault was touched', { cause: error.code ?? String(error.message) })
|
|
221
376
|
}
|
|
222
377
|
}
|
|
223
378
|
acquire(store.lockPath, () => acquirePrivateLock(store.lockPath), 'of this view')
|
|
379
|
+
// A vault allocated for the view is still the folder it made, before anything is written in it (the vault lock
|
|
380
|
+
// included) and again once the lock is held: a store kept by a long-running service may outlive its folder. It is
|
|
381
|
+
// checked again after every wait on the app, before each unit and before the commit, so a folder replaced during a
|
|
382
|
+
// publication refuses it and is never reported current. (Only this account can replace it; the check and the
|
|
383
|
+
// write that follows it are still two steps.)
|
|
384
|
+
store.checkAllocatedVault?.()
|
|
224
385
|
acquire(store.vaultLockPath, () => acquireVaultLock(store), 'into this vault')
|
|
386
|
+
store.checkAllocatedVault?.()
|
|
225
387
|
const recovered = recoverPublicationsLocked({ store, clock })
|
|
226
388
|
const pointer = store.readCurrent()
|
|
227
|
-
if (pointer?.generationId === manifest.generationId) {
|
|
389
|
+
if (pointer?.generationId === manifest.generationId && !pluginFilesDrifted(preparedView, store)) {
|
|
228
390
|
return { state: 'committed', alreadyCommitted: true, generationId: manifest.generationId, journalId: pointer.journalId, notes: [], retainedEdits: pointer.retained ?? [], lateWriters: [], recovered }
|
|
229
391
|
}
|
|
230
392
|
if ((pointer?.generationId ?? null) !== expectedGeneration) refuse('generation-mismatch', 'the committed generation is not the one this publication expects', { committed: pointer?.generationId ?? null })
|
|
231
393
|
|
|
232
|
-
// Path selection.
|
|
394
|
+
// Path selection. An app with this vault open in several windows is refused as such: publishing through one of
|
|
395
|
+
// them would leave the others uncoordinated.
|
|
233
396
|
const probe = await adapter.probe({ vaultRoot: store.vaultRoot })
|
|
234
|
-
|
|
235
|
-
const
|
|
397
|
+
store.checkAllocatedVault?.()
|
|
398
|
+
const unheld = probe.state !== 'coordinated' && probe.state !== 'absent' && await unheldPathApplies({ probe, pointer, store, unheldEvidence, platform })
|
|
399
|
+
// The evidence took time to read: the folder is checked again.
|
|
400
|
+
if (unheld) store.checkAllocatedVault?.()
|
|
401
|
+
if (probe.state !== 'coordinated' && probe.state !== 'absent' && !unheld) refuse(probe.code === 'vault-open-in-several-windows' ? probe.code : 'editor-uncoordinated', `an Obsidian process may have this vault open and cannot be coordinated with: ${probe.reason}`)
|
|
402
|
+
const mode = probe.state === 'coordinated' ? 'in-app' : unheld ? 'direct-unheld' : 'direct'
|
|
236
403
|
const channel = mode === 'in-app' ? adapter : createDirectAdapter({ crashSeam: seam })
|
|
237
404
|
|
|
238
405
|
// Same volume, and an exchange that works on it.
|
|
@@ -247,7 +414,8 @@ export async function publishView(options = {}) {
|
|
|
247
414
|
const exchange = probeExchange({ directory: probeDir, ...exchangeOptions })
|
|
248
415
|
if (!exchange.supported) refuse(exchange.code, exchange.message)
|
|
249
416
|
|
|
250
|
-
const
|
|
417
|
+
const pluginDisk = new Map(preparedView.files.filter((file) => file.kind === 'plugin').map((file) => [file.path, currentDigestOf(store.vaultRoot, file.path)]))
|
|
418
|
+
const units = planUnits({ files: preparedView.files, priorManifest: store.readCurrentManifest(), pointer, ledger: publishedSinceCommit({ store }), pluginDisk })
|
|
251
419
|
|
|
252
420
|
// Stage every known candidate before anything is touched.
|
|
253
421
|
const journalId = newJournalId(clock)
|
|
@@ -256,10 +424,11 @@ export async function publishView(options = {}) {
|
|
|
256
424
|
let stagingDir = null
|
|
257
425
|
try {
|
|
258
426
|
stagingDir = store.stagingDir(journalId)
|
|
259
|
-
|
|
427
|
+
// On the `direct-unheld` path a replacement is never written, so no candidate is staged for one.
|
|
428
|
+
for (const unit of units.filter((item) => item.op === 'create' || (item.op === 'replace' && (mode !== 'direct-unheld' || rules.unheldWrites(item.op))))) {
|
|
260
429
|
const existing = fs.lstatSync(path.join(store.vaultRoot, unit.path), { throwIfNoEntry: false })
|
|
261
430
|
unit.preparedPath = store.preparedPath(journalId, unit.unit)
|
|
262
|
-
stageCandidate(unit.preparedPath, unit.bytes, existing?.isFile() ? existing.mode & 0o777 : 0o644, created)
|
|
431
|
+
stageCandidate(unit.preparedPath, unit.bytes, unit.mode ?? (existing?.isFile() ? existing.mode & 0o777 : 0o644), created)
|
|
263
432
|
// A replacement is exchanged, and the exchange leaves the displaced bytes at this path: it is in recovery.
|
|
264
433
|
unit.stagedPath = unit.op === 'replace' ? store.exchangeCandidatePath(journalId, unit.unit) : unit.preparedPath
|
|
265
434
|
}
|
|
@@ -270,7 +439,7 @@ export async function publishView(options = {}) {
|
|
|
270
439
|
detail: { mode, manifestRef: store.ref(manifestFile), manifestDigest: sha256Digest(manifestBytes),
|
|
271
440
|
// The units this run may write. A kept note is compared and never written; the one case that writes
|
|
272
441
|
// it after all (a kept note gone missing) is a late candidate with its own write-ahead entry.
|
|
273
|
-
units: units.filter((item) => item.op !== 'keep').map(({ unit, path: unitPath, kind, op }) => ({ unit, path: unitPath, kind, op })),
|
|
442
|
+
units: units.filter((item) => item.op !== 'keep' && item.op !== 'leave-absent').map(({ unit, path: unitPath, kind, op }) => ({ unit, path: unitPath, kind, op })),
|
|
274
443
|
staged: units.filter((item) => item.stagedPath).map((item) => ({ unit: item.unit, path: item.path, candidateDigest: item.candidateDigest, stagedRef: store.ref(item.stagedPath),
|
|
275
444
|
...(item.stagedPath === item.preparedPath ? {} : { preparedRef: store.ref(item.preparedPath) }) })) } })
|
|
276
445
|
crash('before-candidate-move')
|
|
@@ -295,7 +464,7 @@ export async function publishView(options = {}) {
|
|
|
295
464
|
}
|
|
296
465
|
crash('after-staging')
|
|
297
466
|
|
|
298
|
-
const context = { store, journal, journalId, channel, clock, crash, mode }
|
|
467
|
+
const context = { store, journal, journalId, channel, clock, crash, mode, rules }
|
|
299
468
|
const results = []
|
|
300
469
|
// Kept notes are compared first, several at a time; one that holds exactly its candidate bytes is unchanged.
|
|
301
470
|
const unchanged = await unchangedKeepUnits(units, store)
|
|
@@ -306,6 +475,8 @@ export async function publishView(options = {}) {
|
|
|
306
475
|
// passed since the last reading. An app that starts after a reading and
|
|
307
476
|
// before the next is not seen; that window is at most two seconds plus
|
|
308
477
|
// one note's publication.
|
|
478
|
+
// The `direct-unheld` path reads its evidence on the same schedule, and once a reading gives none, every remaining
|
|
479
|
+
// unit refuses, a kept one included, so nothing is committed.
|
|
309
480
|
let lastProbe = null
|
|
310
481
|
for (const unit of units) {
|
|
311
482
|
if (mode === 'direct' && (lastProbe === null || Date.now() - lastProbe > 2000)) {
|
|
@@ -313,18 +484,28 @@ export async function publishView(options = {}) {
|
|
|
313
484
|
const again = await adapter.probe({ vaultRoot: store.vaultRoot })
|
|
314
485
|
if (again.state !== 'absent') context.uncoordinated = again.reason
|
|
315
486
|
}
|
|
487
|
+
if (mode === 'direct-unheld' && !context.uncoordinated && (lastProbe === null || Date.now() - lastProbe > 2000)) {
|
|
488
|
+
lastProbe = Date.now()
|
|
489
|
+
if (!(await readsUnlisted(unheldEvidence, store))) context.uncoordinated = 'the vault list of the app no longer shows, positively, that no Obsidian lists this vault'
|
|
490
|
+
}
|
|
491
|
+
store.checkAllocatedVault?.()
|
|
316
492
|
if (unit.op === 'keep' && unchanged.has(unit.path)) {
|
|
317
|
-
results.push({ path: unit.path, kind: unit.kind, op: unit.op, outcome: context.uncoordinated ? 'editor-uncoordinated' : 'unchanged', blocking:
|
|
493
|
+
results.push({ path: unit.path, kind: unit.kind, op: unit.op, outcome: context.uncoordinated ? 'editor-uncoordinated' : 'unchanged', blocking: mode === 'direct-unheld' && Boolean(context.uncoordinated) })
|
|
318
494
|
continue
|
|
319
495
|
}
|
|
320
|
-
results.push(await publishUnit(unit, context))
|
|
496
|
+
try { results.push(await publishUnit(unit, context)) } catch (error) {
|
|
497
|
+
if (error.code !== 'ELEAFCHANGED') throw error
|
|
498
|
+
// Another program kept replacing this file while it was read: its bytes are left as they are, the unit's
|
|
499
|
+
// journal stays open for recovery like an interrupted one, and the other units go on.
|
|
500
|
+
results.push({ path: unit.path, kind: unit.kind, op: unit.op, outcome: 'disk-changed', blocking: true })
|
|
501
|
+
}
|
|
321
502
|
}
|
|
322
503
|
|
|
323
504
|
const blocking = results.filter((result) => result.blocking)
|
|
324
505
|
const retainedEdits = results.filter((result) => result.retained).map((result) => result.retained)
|
|
325
506
|
// Notes kept earlier stay surfaced until they leave the vault or come back into a view.
|
|
326
507
|
const base = { journalId, generationId: manifest.generationId, mode, notes: results.map(({ retained, ...rest }) => rest), retainedEdits, recovered }
|
|
327
|
-
if (blocking.length > 0) {
|
|
508
|
+
if (blocking.length > 0 || (mode === 'direct-unheld' && context.uncoordinated)) {
|
|
328
509
|
const lateWriters = recheckDisplacedFiles({ store, journalIds: [journalId], clock })
|
|
329
510
|
return { ...base, state: 'updating', lateWriters }
|
|
330
511
|
}
|
|
@@ -338,6 +519,7 @@ export async function publishView(options = {}) {
|
|
|
338
519
|
}
|
|
339
520
|
journal.append({ step: 'verify', outcome: 'ok', state: 'verifying', detail: { settled: true, retained: retainedEdits } })
|
|
340
521
|
crash('before-manifest-commit')
|
|
522
|
+
store.checkAllocatedVault?.()
|
|
341
523
|
store.commitManifest({ manifestBytes, generationId: manifest.generationId, journalId, retained: retainedEdits, committedAt: iso(clock) })
|
|
342
524
|
crash('after-manifest-pointer')
|
|
343
525
|
journal.append({ step: 'manifest-commit', outcome: 'ok', state: 'committed', detail: { manifestDigest: sha256Digest(manifestBytes) } })
|
|
@@ -351,26 +533,85 @@ export async function publishView(options = {}) {
|
|
|
351
533
|
}
|
|
352
534
|
return { state: 'refused', refusal: { code: error.code, message: error.message, detail: error.detail }, notes: [], retainedEdits: [], lateWriters: [] }
|
|
353
535
|
} finally {
|
|
536
|
+
// A release that fails is kept and tried again before the next publication of that lock, even when the lock's folder
|
|
537
|
+
// is not there now: a vault moved away keeps its ticket and may be put back.
|
|
354
538
|
for (const [lockPath, release] of releases.reverse()) try { release() } catch { unreleased.set(lockPath, release) }
|
|
355
539
|
}
|
|
356
540
|
}
|
|
357
541
|
|
|
358
542
|
// ---------------------------------------------------------------------------
|
|
359
|
-
// One note, attachment or
|
|
543
|
+
// One note, attachment, settings file or plugin file
|
|
360
544
|
// ---------------------------------------------------------------------------
|
|
361
545
|
|
|
546
|
+
// Atelier's own plugin never waits on a person: a plugin path that cannot be
|
|
547
|
+
// written safely, a vault root not private enough for the bearer, or a file
|
|
548
|
+
// that could not be made or replaced there (a plugin folder the person can read
|
|
549
|
+
// but not write, say) is reported and left for the person while every note
|
|
550
|
+
// converges, and its candidate is retired at once. A plugin file that another
|
|
551
|
+
// writer changed under this run is a race: it blocks like a note, so the view
|
|
552
|
+
// is tried again and the file is planned from the disk anew. Nothing reads a
|
|
553
|
+
// plugin file as a person's edit of a note: its outcomes carry their own names.
|
|
554
|
+
const PLUGIN_LEFT_FOR_A_PERSON = new Set(['path-unsafe', 'vault-not-private', 'create-failed', 'exchange-failed'])
|
|
555
|
+
// A leaf or a parent nobody may look into or read (another owner, mode 000) or a link loop: the person's to repair,
|
|
556
|
+
// like a leaf that is not a file. It is reported, never thrown, so the rest of the view and every later view go on.
|
|
557
|
+
const UNREADABLE_PATH = new Set(['EACCES', 'EPERM', 'ELOOP'])
|
|
558
|
+
const PLUGIN_FILE_CHANGED = new Set(['edit-kept', 'disk-changed', 'editor-edit'])
|
|
559
|
+
|
|
362
560
|
async function publishUnit(unit, context) {
|
|
561
|
+
let result
|
|
562
|
+
try { result = await publishOneUnit(unit, context) } catch (error) {
|
|
563
|
+
// A plugin or settings file another program kept replacing while it was opened (see readNote) is a race of that
|
|
564
|
+
// file, like one that changed under the run: blocking, tried again, and never taken for a person's edit of a note.
|
|
565
|
+
if (error?.code !== 'ELEAFCHANGED' || (unit.kind !== 'plugin' && unit.kind !== 'settings')) throw error
|
|
566
|
+
// Its candidate is left as #83 leaves a note's: the unit's journal stays open for recovery like an interrupted one.
|
|
567
|
+
result = { path: unit.path, kind: unit.kind, op: unit.op, outcome: unit.kind === 'settings' ? 'settings-changed' : 'disk-changed', blocking: true }
|
|
568
|
+
}
|
|
569
|
+
if (unit.kind !== 'plugin') return result
|
|
570
|
+
const { changedAfterPublication, retained: _retained, ...rest } = result
|
|
571
|
+
if (rest.outcome === 'path-unsafe' && unit.stagedPath) retire(unit, context)
|
|
572
|
+
const changed = PLUGIN_FILE_CHANGED.has(rest.outcome)
|
|
573
|
+
return { ...rest, outcome: changed ? 'plugin-file-changed' : rest.outcome, blocking: changed || (rest.blocking && !PLUGIN_LEFT_FOR_A_PERSON.has(rest.outcome)), ...(changedAfterPublication ? { pluginFileChanged: true } : {}) }
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// The vault root holds the plugin's bearer once its data file is there, so it
|
|
577
|
+
// must be private to this user. A vault root Atelier placed under its own data
|
|
578
|
+
// root is made private here; any other one is only looked at, and a vault path
|
|
579
|
+
// under the data root that is a link to somewhere else never receives it.
|
|
580
|
+
function ensurePrivateVaultRoot(store) {
|
|
581
|
+
if (store.linkedVaultRoot === true) return { private: false, reason: 'vault-root-is-a-link' }
|
|
582
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : null
|
|
583
|
+
let stat = fs.lstatSync(store.vaultRoot, { throwIfNoEntry: false })
|
|
584
|
+
if (!stat?.isDirectory() || uid === null || stat.uid !== uid) return { private: false, reason: 'vault-root-not-owned' }
|
|
585
|
+
if ((stat.mode & 0o077) !== 0 && store.managedVaultRoot === true) {
|
|
586
|
+
try { fs.chmodSync(store.vaultRoot, stat.mode & 0o700) } catch { /* looked at again below */ }
|
|
587
|
+
stat = fs.lstatSync(store.vaultRoot, { throwIfNoEntry: false })
|
|
588
|
+
}
|
|
589
|
+
return stat?.isDirectory() && (stat.mode & 0o077) === 0 ? { private: true } : { private: false, reason: 'vault-root-not-private' }
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function publishOneUnit(unit, context) {
|
|
363
593
|
const { store, journal, journalId, clock } = context
|
|
364
594
|
const note = path.join(store.vaultRoot, unit.path)
|
|
365
595
|
const outcome = (code, extra = {}) => ({ path: unit.path, kind: unit.kind, op: unit.op, outcome: code, blocking: false, ...extra })
|
|
596
|
+
// On the `direct-unheld` path a unit after a failed reading refuses, whatever it would have done, so the run says why
|
|
597
|
+
// it stopped.
|
|
598
|
+
if (unit.op === 'leave-absent') return context.mode === 'direct-unheld' && context.uncoordinated ? outcome('editor-uncoordinated', { blocking: true }) : outcome('left-absent')
|
|
366
599
|
const observe = (bytes) => store.retainObject(bytes)
|
|
367
|
-
if (context.uncoordinated) return outcome('editor-uncoordinated', { blocking: unit.op !== 'keep' })
|
|
600
|
+
if (context.uncoordinated) return outcome('editor-uncoordinated', { blocking: unit.op !== 'keep' || context.mode === 'direct-unheld' })
|
|
601
|
+
// The `direct-unheld` path creates files and nothing else. A unit planned as a replacement or a removal has no
|
|
602
|
+
// candidate staged, and is refused before its file is even read. (A kept unit may still become a create; a settings
|
|
603
|
+
// unit is decided in planSettings.)
|
|
604
|
+
if (context.mode === 'direct-unheld' && unit.op !== 'keep' && unit.op !== 'settings' && !context.rules.unheldWrites(unit.op)) return outcome('editor-uncoordinated', { blocking: true })
|
|
368
605
|
// A settings path that cannot be written safely (a symlinked `.obsidian`,
|
|
369
606
|
// say) is reported and left alone; it must not keep every note from
|
|
370
607
|
// converging while the person repairs it. Notes and attachments block.
|
|
371
608
|
const unsafeBlocks = unit.op !== 'settings'
|
|
372
|
-
if (!
|
|
373
|
-
|
|
609
|
+
const unreadable = (error) => { if (!UNREADABLE_PATH.has(error?.code)) throw error; return outcome('path-unsafe', { blocking: unsafeBlocks, errorCode: error.code }) }
|
|
610
|
+
let leaf
|
|
611
|
+
try {
|
|
612
|
+
if (!ensureParents(store.vaultRoot, unit.path)) return outcome('path-unsafe', { blocking: unsafeBlocks })
|
|
613
|
+
leaf = fs.lstatSync(note, { throwIfNoEntry: false })
|
|
614
|
+
} catch (error) { return unreadable(error) }
|
|
374
615
|
if (leaf && !leaf.isFile()) return outcome('path-unsafe', { blocking: unsafeBlocks })
|
|
375
616
|
|
|
376
617
|
let plan = unit
|
|
@@ -378,7 +619,8 @@ async function publishUnit(unit, context) {
|
|
|
378
619
|
plan = planSettings(unit, context)
|
|
379
620
|
if (plan.outcome) return plan
|
|
380
621
|
}
|
|
381
|
-
|
|
622
|
+
let current
|
|
623
|
+
try { current = readNote(note) } catch (error) { return unreadable(error) }
|
|
382
624
|
const currentDigest = current === null ? null : sha256Digest(current)
|
|
383
625
|
|
|
384
626
|
if (plan.op === 'keep') {
|
|
@@ -402,6 +644,19 @@ async function publishUnit(unit, context) {
|
|
|
402
644
|
if (plan.stagedPath) retire(plan, context)
|
|
403
645
|
return outcome('already-current')
|
|
404
646
|
}
|
|
647
|
+
// Whatever a unit became on the way here, the `direct-unheld` path writes nothing below this point but a create.
|
|
648
|
+
if (context.mode === 'direct-unheld' && !context.rules.unheldWrites(plan.op)) {
|
|
649
|
+
if (plan.stagedPath) retire(plan, context)
|
|
650
|
+
return outcome('editor-uncoordinated', { blocking: true })
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (unit.path === PLUGIN_DATA_PATH && plan.op !== 'remove') {
|
|
654
|
+
const privacy = ensurePrivateVaultRoot(store)
|
|
655
|
+
if (!privacy.private) {
|
|
656
|
+
if (plan.stagedPath) retire(plan, context)
|
|
657
|
+
return outcome('vault-not-private', { reason: privacy.reason })
|
|
658
|
+
}
|
|
659
|
+
}
|
|
405
660
|
|
|
406
661
|
const operationId = `${journalId}:${unit.unit}`
|
|
407
662
|
const displaced = plan.op === 'create' ? null : store.displacedPath(journalId, unit.unit)
|
|
@@ -552,18 +807,32 @@ function stageLate(plan, { store, journal, journalId, crash, updating }) {
|
|
|
552
807
|
return unit.stagedPath
|
|
553
808
|
}
|
|
554
809
|
|
|
555
|
-
// Policy settings are owned per key. The candidate is the file on
|
|
556
|
-
// with only
|
|
557
|
-
//
|
|
810
|
+
// Policy settings are owned per key or entry. The candidate is the file on
|
|
811
|
+
// disk now with only what Atelier owns set; the base is that same file. A file
|
|
812
|
+
// whose shape the policy does not know is the person's to repair and is left
|
|
813
|
+
// alone.
|
|
558
814
|
function planSettings(unit, context) {
|
|
559
815
|
const note = path.join(context.store.vaultRoot, unit.path)
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
816
|
+
let existing
|
|
817
|
+
try { existing = readNote(note) } catch (error) {
|
|
818
|
+
if (!UNREADABLE_PATH.has(error?.code)) throw error
|
|
819
|
+
return { path: unit.path, kind: 'settings', op: 'settings', outcome: 'path-unsafe', blocking: false, errorCode: error.code }
|
|
820
|
+
}
|
|
821
|
+
// Prepared from other bytes than these: the person changed the file since, and whatever Atelier decided from the old
|
|
822
|
+
// bytes is not written over the change. The view is tried again, and the next preparation reads the file as it is.
|
|
823
|
+
if (Object.hasOwn(unit, 'expectedDigest') && (existing === null ? null : sha256Digest(existing)) !== unit.expectedDigest) {
|
|
824
|
+
return { path: unit.path, kind: 'settings', op: 'settings', outcome: 'settings-changed', blocking: true }
|
|
825
|
+
}
|
|
826
|
+
// On the `direct-unheld` path a settings file that is there is never written over, and no candidate is staged for it.
|
|
827
|
+
// Only one that already holds exactly what the policy asks (Atelier's own, from an interrupted run) is left as it is.
|
|
828
|
+
if (context.mode === 'direct-unheld' && existing !== null && !context.rules.unheldWrites('replace') && !holdsPolicy(unit.path, existing)) {
|
|
829
|
+
return { path: unit.path, kind: 'settings', op: 'settings', outcome: 'editor-uncoordinated', blocking: true }
|
|
830
|
+
}
|
|
831
|
+
let file
|
|
832
|
+
try { file = preparePolicySettingsFile(unit.path, existing) } catch (error) {
|
|
563
833
|
if (error instanceof ObsidianContractRefusal) return { path: unit.path, kind: 'settings', op: 'settings', outcome: 'settings-invalid', blocking: false }
|
|
564
834
|
throw error
|
|
565
835
|
}
|
|
566
|
-
const [file] = prepared.files
|
|
567
836
|
if (existing !== null && file.bytes.equals(existing)) return { path: unit.path, kind: 'settings', op: 'settings', outcome: 'policy-satisfied', blocking: false }
|
|
568
837
|
const plan = { ...unit, op: existing === null ? 'create' : 'replace', baseDigest: existing === null ? null : sha256Digest(existing), candidateDigest: file.digest, bytes: file.bytes,
|
|
569
838
|
mode: existing === null ? 0o644 : fs.lstatSync(note).mode & 0o777 }
|
|
@@ -573,6 +842,13 @@ function planSettings(unit, context) {
|
|
|
573
842
|
return plan
|
|
574
843
|
}
|
|
575
844
|
|
|
845
|
+
function holdsPolicy(settingsPath, bytes) {
|
|
846
|
+
try { return preparePolicySettingsFile(settingsPath, bytes).bytes.equals(bytes) } catch (error) {
|
|
847
|
+
if (error instanceof ObsidianContractRefusal) return false
|
|
848
|
+
throw error
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
576
852
|
function verify(unit, plan, { store, journal }, result) {
|
|
577
853
|
const bytes = readNote(path.join(store.vaultRoot, unit.path))
|
|
578
854
|
const digest = bytes === null ? null : sha256Digest(bytes)
|