@mnstry/atelier 0.2.0-alpha.12 → 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 +661 -0
- package/README.md +142 -6
- 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-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/docs/architecture.md +89 -0
- package/docs/assurance-controls.md +2 -2
- 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 +181 -16
- package/docs/obsidian-contract.md +223 -15
- package/docs/obsidian-plugin.md +38 -4
- package/docs/obsidian.md +613 -51
- 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 +49 -0
- 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 +47 -2
- 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/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 +59 -5
- 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 +754 -63
- 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 +30 -0
- package/src/coordination/index.mjs +110 -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/projection/obsidian/edits/apply.mjs +55 -16
- package/src/projection/obsidian/personal-workspace.mjs +436 -0
- package/src/projection/obsidian/proposals/adapter.mjs +47 -8
- package/src/projection/obsidian/proposals/observation.mjs +10 -2
- package/src/projection/obsidian/publication/publisher.mjs +142 -13
- package/src/projection/obsidian/publication/vault-list.mjs +4 -3
- package/src/projection/obsidian/recovery/index.mjs +4 -1
- package/src/projection/obsidian/recovery/store.mjs +160 -7
- package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
- package/src/readiness/readiness.mjs +10 -3
- package/src/reflection/index.mjs +56 -0
- package/src/runtime/obsidian/app-production-seams.mjs +48 -16
- package/src/runtime/obsidian/app-registration.mjs +175 -28
- package/src/runtime/obsidian/app-restart.mjs +154 -0
- package/src/runtime/obsidian/engine.mjs +163 -20
- package/src/runtime/obsidian/extension-points.mjs +1 -1
- package/src/runtime/obsidian/index.mjs +10 -7
- package/src/runtime/obsidian/launch-plan.mjs +7 -4
- package/src/runtime/obsidian/lifecycle.mjs +92 -12
- package/src/runtime/obsidian/login-item.mjs +332 -0
- package/src/runtime/obsidian/machine-settings.mjs +119 -12
- package/src/runtime/obsidian/opening.mjs +171 -37
- package/src/runtime/obsidian/pipeline.mjs +71 -3
- package/src/runtime/obsidian/plugin-channel.mjs +11 -3
- package/src/runtime/obsidian/plugin-choice.mjs +16 -4
- package/src/runtime/obsidian/plugin-drift.mjs +59 -11
- package/src/runtime/obsidian/plugin-presence.mjs +14 -1
- 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 +99 -16
- 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 +87 -23
- package/src/runtime/obsidian/service.mjs +36 -3
- package/src/runtime/obsidian/startup-adapters.mjs +59 -21
- 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 +169 -2
- package/src/server/server.mjs +28 -2
- 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 +1 -0
- 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
|
@@ -9,7 +9,7 @@ import { PLUGIN_DATA_MODE, PLUGIN_DATA_PATH, PLUGIN_SOURCE_MODE } from '../plugi
|
|
|
9
9
|
import { createJournal, newJournalId } from '../recovery/journal.mjs'
|
|
10
10
|
import { recheckDisplacedFiles } from '../recovery/late-writer.mjs'
|
|
11
11
|
import { publishedSinceCommit, reconcileUnit, recordDisplaced, recoverPublicationsLocked, retireStagedFile } from '../recovery/restart.mjs'
|
|
12
|
-
import { PublicationRefusal, acquireVaultLock, readFileBytes, readFileDigest, refuse, sha256Digest } from '../recovery/store.mjs'
|
|
12
|
+
import { PublicationRefusal, acquireVaultLock, allocatedFolderState, readFileBytes, readFileDigest, refuse, sha256Digest } from '../recovery/store.mjs'
|
|
13
13
|
import { PROTOCOL_ID, isAddressableVaultPath } from './bridge-script.mjs'
|
|
14
14
|
import { probeExchange } from './exchange.mjs'
|
|
15
15
|
import { CRASH_INJECTION_TEST_SEAM } from './test-seam.mjs'
|
|
@@ -44,11 +44,34 @@ import { createDirectAdapter } from './transport.mjs'
|
|
|
44
44
|
// displaced to recovery and never lost, and a plugin path that a person has to
|
|
45
45
|
// repair never holds the view back. The plugin's data file names a bearer and
|
|
46
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.
|
|
47
66
|
|
|
48
67
|
const hex = (digest) => digest.slice('sha256:'.length)
|
|
49
68
|
const iso = (clock) => { const value = clock(); return (value instanceof Date ? value : new Date(value)).toISOString() }
|
|
50
69
|
const REFUSED_BY_EDIT = new Set(['editor-edit', 'disk-changed'])
|
|
51
70
|
const unreleased = new Map()
|
|
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'
|
|
52
75
|
|
|
53
76
|
// A note another program replaces by rename can change between the check of its leaf and the open
|
|
54
77
|
// (ELEAFCHANGED). Every rename leaves a complete file, so it is read again; one that keeps changing is left to
|
|
@@ -212,17 +235,47 @@ function currentDigestOf(vaultRoot, relativePath) {
|
|
|
212
235
|
try { return readFileDigest(path.join(vaultRoot, relativePath)) } catch { return null }
|
|
213
236
|
}
|
|
214
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
|
+
|
|
215
264
|
// A generation that is already committed is published again, as it is, when a
|
|
216
265
|
// plugin file it carries is not on disk as it pins it: the person repaired a
|
|
217
266
|
// path Atelier had to leave, or removed or changed a file. The bytes are
|
|
218
267
|
// Atelier's and nothing else of the view changed, so the generation stays the
|
|
219
268
|
// same; its notes are kept, and its plugin units are planned from the disk as
|
|
220
|
-
// ever. A file prepared only where present that is gone is not a drift
|
|
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).
|
|
221
273
|
function pluginFilesDrifted(preparedView, store) {
|
|
222
274
|
return preparedView.files.some((file) => {
|
|
223
275
|
if (file.kind !== 'plugin') return false
|
|
224
276
|
const current = currentDigestOf(store.vaultRoot, file.path)
|
|
225
|
-
|
|
277
|
+
if (current === file.digest || (current === null && file.onlyIfPresent === true)) return false
|
|
278
|
+
try { return !leftForThePerson(store, file.path) } catch { return true }
|
|
226
279
|
})
|
|
227
280
|
}
|
|
228
281
|
|
|
@@ -264,8 +317,38 @@ export function planUnits({ files, priorManifest, pointer, ledger, pluginDisk =
|
|
|
264
317
|
return units
|
|
265
318
|
}
|
|
266
319
|
|
|
267
|
-
|
|
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) {
|
|
268
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 }
|
|
269
352
|
const seam = options[CRASH_INJECTION_TEST_SEAM] ?? null
|
|
270
353
|
const crash = (point) => { if (seam && seam.at === point) seam.halt(point) }
|
|
271
354
|
if (!store || typeof store.commitManifest !== 'function') throw new TypeError('publishView needs a recoveryStore')
|
|
@@ -280,19 +363,27 @@ export async function publishView(options = {}) {
|
|
|
280
363
|
// A release that could not be written (a full disk) is finished first.
|
|
281
364
|
for (const lockPath of [store.vaultLockPath, store.lockPath]) {
|
|
282
365
|
if (!unreleased.has(lockPath)) continue
|
|
283
|
-
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) }) }
|
|
284
367
|
unreleased.delete(lockPath)
|
|
285
368
|
}
|
|
286
369
|
// Two locks, the view's and then the vault's: a second view or a second
|
|
287
370
|
// workspace state pointed at the same vault refuses instead of racing.
|
|
288
371
|
const acquire = (lockPath, take, held) => {
|
|
289
372
|
try { releases.push([lockPath, take()]) } catch (error) {
|
|
373
|
+
if (error instanceof PublicationRefusal) throw error
|
|
290
374
|
if (error.code === 'EEXIST') refuse('publication-in-progress', `another publication ${held} holds the lock`)
|
|
291
375
|
refuse('state-unwritable', 'private publication state cannot be written; nothing in the vault was touched', { cause: error.code ?? String(error.message) })
|
|
292
376
|
}
|
|
293
377
|
}
|
|
294
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?.()
|
|
295
385
|
acquire(store.vaultLockPath, () => acquireVaultLock(store), 'into this vault')
|
|
386
|
+
store.checkAllocatedVault?.()
|
|
296
387
|
const recovered = recoverPublicationsLocked({ store, clock })
|
|
297
388
|
const pointer = store.readCurrent()
|
|
298
389
|
if (pointer?.generationId === manifest.generationId && !pluginFilesDrifted(preparedView, store)) {
|
|
@@ -303,8 +394,12 @@ export async function publishView(options = {}) {
|
|
|
303
394
|
// Path selection. An app with this vault open in several windows is refused as such: publishing through one of
|
|
304
395
|
// them would leave the others uncoordinated.
|
|
305
396
|
const probe = await adapter.probe({ vaultRoot: store.vaultRoot })
|
|
306
|
-
|
|
307
|
-
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'
|
|
308
403
|
const channel = mode === 'in-app' ? adapter : createDirectAdapter({ crashSeam: seam })
|
|
309
404
|
|
|
310
405
|
// Same volume, and an exchange that works on it.
|
|
@@ -329,7 +424,8 @@ export async function publishView(options = {}) {
|
|
|
329
424
|
let stagingDir = null
|
|
330
425
|
try {
|
|
331
426
|
stagingDir = store.stagingDir(journalId)
|
|
332
|
-
|
|
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))))) {
|
|
333
429
|
const existing = fs.lstatSync(path.join(store.vaultRoot, unit.path), { throwIfNoEntry: false })
|
|
334
430
|
unit.preparedPath = store.preparedPath(journalId, unit.unit)
|
|
335
431
|
stageCandidate(unit.preparedPath, unit.bytes, unit.mode ?? (existing?.isFile() ? existing.mode & 0o777 : 0o644), created)
|
|
@@ -368,7 +464,7 @@ export async function publishView(options = {}) {
|
|
|
368
464
|
}
|
|
369
465
|
crash('after-staging')
|
|
370
466
|
|
|
371
|
-
const context = { store, journal, journalId, channel, clock, crash, mode }
|
|
467
|
+
const context = { store, journal, journalId, channel, clock, crash, mode, rules }
|
|
372
468
|
const results = []
|
|
373
469
|
// Kept notes are compared first, several at a time; one that holds exactly its candidate bytes is unchanged.
|
|
374
470
|
const unchanged = await unchangedKeepUnits(units, store)
|
|
@@ -379,6 +475,8 @@ export async function publishView(options = {}) {
|
|
|
379
475
|
// passed since the last reading. An app that starts after a reading and
|
|
380
476
|
// before the next is not seen; that window is at most two seconds plus
|
|
381
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.
|
|
382
480
|
let lastProbe = null
|
|
383
481
|
for (const unit of units) {
|
|
384
482
|
if (mode === 'direct' && (lastProbe === null || Date.now() - lastProbe > 2000)) {
|
|
@@ -386,8 +484,13 @@ export async function publishView(options = {}) {
|
|
|
386
484
|
const again = await adapter.probe({ vaultRoot: store.vaultRoot })
|
|
387
485
|
if (again.state !== 'absent') context.uncoordinated = again.reason
|
|
388
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?.()
|
|
389
492
|
if (unit.op === 'keep' && unchanged.has(unit.path)) {
|
|
390
|
-
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) })
|
|
391
494
|
continue
|
|
392
495
|
}
|
|
393
496
|
try { results.push(await publishUnit(unit, context)) } catch (error) {
|
|
@@ -402,7 +505,7 @@ export async function publishView(options = {}) {
|
|
|
402
505
|
const retainedEdits = results.filter((result) => result.retained).map((result) => result.retained)
|
|
403
506
|
// Notes kept earlier stay surfaced until they leave the vault or come back into a view.
|
|
404
507
|
const base = { journalId, generationId: manifest.generationId, mode, notes: results.map(({ retained, ...rest }) => rest), retainedEdits, recovered }
|
|
405
|
-
if (blocking.length > 0) {
|
|
508
|
+
if (blocking.length > 0 || (mode === 'direct-unheld' && context.uncoordinated)) {
|
|
406
509
|
const lateWriters = recheckDisplacedFiles({ store, journalIds: [journalId], clock })
|
|
407
510
|
return { ...base, state: 'updating', lateWriters }
|
|
408
511
|
}
|
|
@@ -416,6 +519,7 @@ export async function publishView(options = {}) {
|
|
|
416
519
|
}
|
|
417
520
|
journal.append({ step: 'verify', outcome: 'ok', state: 'verifying', detail: { settled: true, retained: retainedEdits } })
|
|
418
521
|
crash('before-manifest-commit')
|
|
522
|
+
store.checkAllocatedVault?.()
|
|
419
523
|
store.commitManifest({ manifestBytes, generationId: manifest.generationId, journalId, retained: retainedEdits, committedAt: iso(clock) })
|
|
420
524
|
crash('after-manifest-pointer')
|
|
421
525
|
journal.append({ step: 'manifest-commit', outcome: 'ok', state: 'committed', detail: { manifestDigest: sha256Digest(manifestBytes) } })
|
|
@@ -429,6 +533,8 @@ export async function publishView(options = {}) {
|
|
|
429
533
|
}
|
|
430
534
|
return { state: 'refused', refusal: { code: error.code, message: error.message, detail: error.detail }, notes: [], retainedEdits: [], lateWriters: [] }
|
|
431
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.
|
|
432
538
|
for (const [lockPath, release] of releases.reverse()) try { release() } catch { unreleased.set(lockPath, release) }
|
|
433
539
|
}
|
|
434
540
|
}
|
|
@@ -487,9 +593,15 @@ async function publishOneUnit(unit, context) {
|
|
|
487
593
|
const { store, journal, journalId, clock } = context
|
|
488
594
|
const note = path.join(store.vaultRoot, unit.path)
|
|
489
595
|
const outcome = (code, extra = {}) => ({ path: unit.path, kind: unit.kind, op: unit.op, outcome: code, blocking: false, ...extra })
|
|
490
|
-
|
|
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')
|
|
491
599
|
const observe = (bytes) => store.retainObject(bytes)
|
|
492
|
-
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 })
|
|
493
605
|
// A settings path that cannot be written safely (a symlinked `.obsidian`,
|
|
494
606
|
// say) is reported and left alone; it must not keep every note from
|
|
495
607
|
// converging while the person repairs it. Notes and attachments block.
|
|
@@ -532,6 +644,11 @@ async function publishOneUnit(unit, context) {
|
|
|
532
644
|
if (plan.stagedPath) retire(plan, context)
|
|
533
645
|
return outcome('already-current')
|
|
534
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
|
+
}
|
|
535
652
|
|
|
536
653
|
if (unit.path === PLUGIN_DATA_PATH && plan.op !== 'remove') {
|
|
537
654
|
const privacy = ensurePrivateVaultRoot(store)
|
|
@@ -706,6 +823,11 @@ function planSettings(unit, context) {
|
|
|
706
823
|
if (Object.hasOwn(unit, 'expectedDigest') && (existing === null ? null : sha256Digest(existing)) !== unit.expectedDigest) {
|
|
707
824
|
return { path: unit.path, kind: 'settings', op: 'settings', outcome: 'settings-changed', blocking: true }
|
|
708
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
|
+
}
|
|
709
831
|
let file
|
|
710
832
|
try { file = preparePolicySettingsFile(unit.path, existing) } catch (error) {
|
|
711
833
|
if (error instanceof ObsidianContractRefusal) return { path: unit.path, kind: 'settings', op: 'settings', outcome: 'settings-invalid', blocking: false }
|
|
@@ -720,6 +842,13 @@ function planSettings(unit, context) {
|
|
|
720
842
|
return plan
|
|
721
843
|
}
|
|
722
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
|
+
|
|
723
852
|
function verify(unit, plan, { store, journal }, result) {
|
|
724
853
|
const bytes = readNote(path.join(store.vaultRoot, unit.path))
|
|
725
854
|
const digest = bytes === null ? null : sha256Digest(bytes)
|
|
@@ -109,13 +109,14 @@ const currentUid = () => (typeof process.getuid === 'function' ? process.getuid(
|
|
|
109
109
|
// { ok: false, code, message }. The user-data directory and the file must be
|
|
110
110
|
// this user's own, a real directory and a regular file; the file must be a
|
|
111
111
|
// JSON object whose `vaults`, when present, is an object. A missing directory
|
|
112
|
-
// or file means Obsidian has not run on this account
|
|
112
|
+
// or file means Obsidian has not run on this account, or has shown only its
|
|
113
|
+
// starter window, which writes no list: that refusal names `userDataDir`.
|
|
113
114
|
export function readObsidianSettings({ userDataDir, uid = currentUid() } = {}) {
|
|
114
115
|
if (typeof userDataDir !== 'string' || !path.isAbsolute(userDataDir)) return refused('obsidian-settings-location-unknown', 'where Obsidian keeps its settings on this system is not known')
|
|
115
116
|
const owned = (stat) => uid === null || stat.uid === uid
|
|
116
117
|
let directory
|
|
117
118
|
try { directory = fs.lstatSync(userDataDir) } catch (error) {
|
|
118
|
-
if (error.code === 'ENOENT') return refused('obsidian-settings-missing', 'Obsidian has no settings directory on this account; it has not run here')
|
|
119
|
+
if (error.code === 'ENOENT') return { ...refused('obsidian-settings-missing', 'Obsidian has no settings directory on this account; it has not run here'), userDataDir }
|
|
119
120
|
return refused('obsidian-settings-unreadable', 'the Obsidian settings directory cannot be read')
|
|
120
121
|
}
|
|
121
122
|
if (directory.isSymbolicLink() || !directory.isDirectory()) return refused('obsidian-settings-unsafe', 'the Obsidian settings directory is a link or not a directory')
|
|
@@ -123,7 +124,7 @@ export function readObsidianSettings({ userDataDir, uid = currentUid() } = {}) {
|
|
|
123
124
|
const file = path.join(userDataDir, OBSIDIAN_SETTINGS_FILE)
|
|
124
125
|
let leaf
|
|
125
126
|
try { leaf = fs.lstatSync(file) } catch (error) {
|
|
126
|
-
if (error.code === 'ENOENT') return refused('obsidian-settings-missing', 'Obsidian has no settings file on this account; it has not run here')
|
|
127
|
+
if (error.code === 'ENOENT') return { ...refused('obsidian-settings-missing', 'Obsidian has no settings file on this account; it has not run here'), userDataDir }
|
|
127
128
|
return refused('obsidian-settings-unreadable', 'the Obsidian settings file cannot be read')
|
|
128
129
|
}
|
|
129
130
|
if (leaf.isSymbolicLink() || !leaf.isFile()) return refused('obsidian-settings-unsafe', 'the Obsidian settings file is a link or not a regular file')
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Recovery state for Obsidian publication: the immutable store, the durable
|
|
2
2
|
// journal, restart recovery and the late-writer re-check.
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
EXCHANGE_CANDIDATE_NAME, LATE_EXCHANGE_CANDIDATE_NAME, PublicationRefusal, VAULT_ALLOCATION_SCHEMA, VAULT_LOCK_DIRECTORY, VAULT_ORIGINS, acquireVaultLock, allocatedFolderState, assertAllocatedFolder, allocationFile, folderIdentity,
|
|
5
|
+
createRecoveryStore, hasCommittedGeneration, legacyVaultRoot, readFileDigest, readVaultAllocation, sha256Digest, validateVaultAllocation, vaultRootFor, writeVaultAllocation,
|
|
6
|
+
} from './store.mjs'
|
|
4
7
|
export { JOURNAL_SCHEMA, createJournal, journalDetail, listJournals, newJournalId } from './journal.mjs'
|
|
5
8
|
export { classifyCandidateFile, namedCandidates, publishedSinceCommit, reconcileUnit, recoverPublications, retireStagedFile } from './restart.mjs'
|
|
6
9
|
export { recheckDisplacedFiles } from './late-writer.mjs'
|
|
@@ -7,7 +7,8 @@ import { atomicReplacePrivateText, ensureContainedPrivateDirectory, openRegularF
|
|
|
7
7
|
|
|
8
8
|
// Private per-workspace state for one Obsidian view:
|
|
9
9
|
//
|
|
10
|
-
// <workspaceRoot>/vaults/<scopeId>/ the editable vault
|
|
10
|
+
// <workspaceRoot>/vaults/<scopeId>/ the editable vault, unless it was allocated elsewhere or a vaultRoot is named
|
|
11
|
+
// <workspaceRoot>/state/allocations/<scopeId>.json where the view's vault was allocated, when it was (see below)
|
|
11
12
|
// <workspaceRoot>/state/manifests/<scopeId>/ trusted manifests and the current pointer
|
|
12
13
|
// <workspaceRoot>/state/journals/<scopeId>/<id>/ publication journals
|
|
13
14
|
// <workspaceRoot>/recovery/objects/<sha256>.bin immutable content-addressed bytes
|
|
@@ -49,7 +50,15 @@ export const VAULT_LOCK_DIRECTORY = '.atelier-publication'
|
|
|
49
50
|
// marker before its ticket, so the directory does not grow inside a person's
|
|
50
51
|
// vault and an interrupted removal never leaves a marker without its ticket.
|
|
51
52
|
export function acquireVaultLock(store) {
|
|
53
|
+
// Only in the vault the store was made for, reached through no link: the lock's folder is made inside whatever the
|
|
54
|
+
// vault root leads to.
|
|
55
|
+
let real = null
|
|
56
|
+
try { real = realPathAsStored(store.vaultRoot) } catch { /* not reachable: refused below */ }
|
|
57
|
+
if (real !== store.vaultRoot) refuse(store.allocation ? 'vault-allocation-moved' : 'vault-root-moved', 'the vault is no longer the folder this publication was prepared for: its path now leads somewhere else; nothing was written', { path: store.vaultRoot, leadsTo: real })
|
|
52
58
|
const directory = ensureContainedPrivateDirectory({ workspaceRoot: store.vaultRoot, directory: path.dirname(store.vaultLockPath), label: 'vault publication lock' })
|
|
59
|
+
// The lock's folder resolved, again, to the one inside the vault: a link put there after the check above is refused
|
|
60
|
+
// before any ticket is written (only the empty folder may have been made through it).
|
|
61
|
+
if (directory !== path.dirname(store.vaultLockPath)) refuse(store.allocation ? 'vault-allocation-moved' : 'vault-root-moved', 'the vault is no longer the folder this publication was prepared for: its path now leads somewhere else; no lock was taken', { path: store.vaultRoot, leadsTo: path.dirname(directory) })
|
|
53
62
|
const release = acquirePrivateLock(path.join(directory, path.basename(store.vaultLockPath)))
|
|
54
63
|
try {
|
|
55
64
|
const owners = `${store.vaultLockPath}.owners`
|
|
@@ -111,11 +120,135 @@ function publishOnce(file, bytes) {
|
|
|
111
120
|
return true
|
|
112
121
|
}
|
|
113
122
|
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Where a view's vault is
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
// A view of a workspace that decided where its vaults live gets a folder
|
|
128
|
+
// there, allocated once at its first publication (by the maintenance engine)
|
|
129
|
+
// and recorded, owner only, in the workspace's private state:
|
|
130
|
+
//
|
|
131
|
+
// <workspaceRoot>/state/allocations/<scopeId>.json atelier-obsidian-vault-allocation/v1
|
|
132
|
+
//
|
|
133
|
+
// A view without a record has its vault where every earlier release put it,
|
|
134
|
+
// `<workspaceRoot>/vaults/<scopeId>`: a vault published there stays there
|
|
135
|
+
// until it is moved, and a workspace that decided no place publishes there as
|
|
136
|
+
// before. The record is the one thing that names another folder, and the
|
|
137
|
+
// store reads it, so every reader of a view's vault (the engine, source apply,
|
|
138
|
+
// the proposal adapter, `open`) finds the same one.
|
|
139
|
+
|
|
140
|
+
export const VAULT_ALLOCATION_SCHEMA = 'atelier-obsidian-vault-allocation/v1'
|
|
141
|
+
export const VAULT_ORIGINS = Object.freeze(['allocated', 'legacy-data-root'])
|
|
142
|
+
|
|
143
|
+
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/
|
|
144
|
+
const CONTROL = /[\u0000-\u001f\u007f]/
|
|
145
|
+
// `device` and `inode` name the folder that was made, as decimal strings (a 64-bit number does not fit a JSON number):
|
|
146
|
+
// a folder at the recorded path that is another one (restored, moved in, made again by someone else) is never
|
|
147
|
+
// published into.
|
|
148
|
+
const ALLOCATION_KEYS = Object.freeze(['schema', 'workspaceId', 'scopeId', 'path', 'name', 'parent', 'device', 'inode', 'allocatedAt'])
|
|
149
|
+
const DECIMAL = /^(0|[1-9][0-9]{0,19})$/
|
|
150
|
+
const isPlainPath = (value) => typeof value === 'string' && value.length <= 4096 && !CONTROL.test(value) && path.isAbsolute(value) && path.resolve(value) === value
|
|
151
|
+
|
|
152
|
+
export const allocationFile = (workspaceRoot, scopeId) => path.join(workspaceRoot, 'state', 'allocations', `${segment(scopeId)}.json`)
|
|
153
|
+
export const legacyVaultRoot = (workspaceRoot, scopeId) => path.join(workspaceRoot, 'vaults', segment(scopeId))
|
|
154
|
+
|
|
155
|
+
export function validateVaultAllocation(document, { workspaceId, scopeId }) {
|
|
156
|
+
const code = 'invalid-vault-allocation'
|
|
157
|
+
if (document === null || typeof document !== 'object' || Array.isArray(document)) refuse(code, 'a vault allocation is an object')
|
|
158
|
+
const unknown = Object.keys(document).filter((key) => !ALLOCATION_KEYS.includes(key))
|
|
159
|
+
if (unknown.length > 0 || ALLOCATION_KEYS.some((key) => !Object.hasOwn(document, key))) refuse(code, 'a vault allocation carries exactly its own members', { unknown: unknown.sort() })
|
|
160
|
+
if (document.schema !== VAULT_ALLOCATION_SCHEMA) refuse(code, 'a vault allocation names an unknown schema')
|
|
161
|
+
if (document.workspaceId !== workspaceId || document.scopeId !== scopeId || !IDENTIFIER.test(String(scopeId))) refuse(code, 'the vault allocation belongs to another workspace or view')
|
|
162
|
+
if (!isPlainPath(document.path) || !isPlainPath(document.parent)) refuse(code, 'the vault allocation names a folder that is not an absolute, plainly written path')
|
|
163
|
+
if (path.dirname(document.path) !== document.parent || path.basename(document.path) !== document.name) refuse(code, 'the vault allocation names its folder inconsistently')
|
|
164
|
+
if (typeof document.device !== 'string' || !DECIMAL.test(document.device) || typeof document.inode !== 'string' || !DECIMAL.test(document.inode)) refuse(code, 'the vault allocation names its folder\'s device and inode as decimal strings')
|
|
165
|
+
if (typeof document.allocatedAt !== 'string' || !TIMESTAMP.test(document.allocatedAt)) refuse(code, 'the vault allocation needs a UTC time')
|
|
166
|
+
return document
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// The record of a view, validated, or null when it has none. Reads only.
|
|
170
|
+
export function readVaultAllocation({ workspaceRoot, workspaceId, scopeId }) {
|
|
171
|
+
let text
|
|
172
|
+
try { text = readRegularTextNoFollow(allocationFile(workspaceRoot, scopeId)) } catch (error) {
|
|
173
|
+
if (error.code === 'ENOENT') return null
|
|
174
|
+
return refuse('invalid-vault-allocation', 'the vault allocation cannot be read', { cause: error.code ?? String(error.message) })
|
|
175
|
+
}
|
|
176
|
+
let document
|
|
177
|
+
try { document = JSON.parse(text) } catch { return refuse('invalid-vault-allocation', 'the vault allocation is not JSON') }
|
|
178
|
+
return validateVaultAllocation(document, { workspaceId, scopeId })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// The device and inode of a folder, as an allocation records them; null when nothing is there.
|
|
182
|
+
export function folderIdentity(folder) {
|
|
183
|
+
const stat = fs.lstatSync(folder, { bigint: true, throwIfNoEntry: false })
|
|
184
|
+
return stat === undefined ? null : { device: String(stat.dev), inode: String(stat.ino), directory: stat.isDirectory(), link: stat.isSymbolicLink() }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Whether the folder at an allocation's path is the one it recorded: 'same', 'missing', or 'replaced'.
|
|
188
|
+
export function allocatedFolderState(allocation) {
|
|
189
|
+
const found = folderIdentity(allocation.path)
|
|
190
|
+
if (found === null) return 'missing'
|
|
191
|
+
return found.directory && !found.link && found.device === allocation.device && found.inode === allocation.inode ? 'same' : 'replaced'
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Refuses, typed, unless the folder at an allocation's path is the one it recorded, at that real path: checked when a store is made, by
|
|
195
|
+
// the maintenance engine before it uses a store again, and by the publisher before and after it takes the vault lock,
|
|
196
|
+
// so a folder replaced while a service runs is never published into.
|
|
197
|
+
export function assertAllocatedFolder(allocation) {
|
|
198
|
+
const found = allocatedFolderState(allocation)
|
|
199
|
+
if (found === 'missing') refuse('vault-allocation-missing', 'the folder allocated to this view is gone; the maintenance service makes it again at its next tick', { path: allocation.path })
|
|
200
|
+
if (found === 'replaced') refuse('vault-allocation-replaced', 'another folder is where this view\'s vault was allocated; Atelier publishes only into the folder it made', { path: allocation.path })
|
|
201
|
+
// Reached through no link: the record holds the folder's real path, so the same folder moved elsewhere (into a
|
|
202
|
+
// vault the app lists, say) and linked back, which keeps its device and inode, is not published into either.
|
|
203
|
+
let real = null
|
|
204
|
+
try { real = realPathAsStored(allocation.path) } catch { /* not reachable: refused below */ }
|
|
205
|
+
if (real !== allocation.path) refuse('vault-allocation-moved', 'this view\'s vault is now reached through a link, somewhere else than where it was allocated; Atelier publishes only into the folder at the path it recorded', { path: allocation.path, leadsTo: real })
|
|
206
|
+
return allocation
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Written only by the maintenance engine, under its lock, and by what moves a vault under the same lock.
|
|
210
|
+
export function writeVaultAllocation({ workspaceRoot, allocation }) {
|
|
211
|
+
validateVaultAllocation(allocation, allocation)
|
|
212
|
+
const directory = ensureContainedPrivateDirectory({ workspaceRoot, directory: path.dirname(allocationFile(workspaceRoot, allocation.scopeId)), label: 'Obsidian vault allocations' })
|
|
213
|
+
atomicReplacePrivateText(path.join(directory, path.basename(allocationFile(workspaceRoot, allocation.scopeId))), `${JSON.stringify(allocation, null, 2)}\n`)
|
|
214
|
+
return allocation
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// A view without a record that was published, and has no vault under the data root, where a view published there
|
|
218
|
+
// always has its folder: its record was lost (removed, or a backup restored without it), or, for a view that was never
|
|
219
|
+
// allocated a folder, its folder under the data root was removed. Refused, typed, rather than published again into a
|
|
220
|
+
// new, empty vault under the data root while a vault it had elsewhere stays orphaned.
|
|
221
|
+
function refuseLostAllocation({ workspaceRoot, scopeId }) {
|
|
222
|
+
const legacy = legacyVaultRoot(workspaceRoot, scopeId)
|
|
223
|
+
if (!hasCommittedGeneration({ workspaceRoot, scopeId }) || fs.lstatSync(legacy, { throwIfNoEntry: false }) !== undefined) return
|
|
224
|
+
refuse('vault-allocation-lost', `this view was published, and its vault is not where Atelier can find it: there is no record of a folder allocated for it, and no folder under the data root. If its vault was allocated elsewhere, restore ${allocationFile(workspaceRoot, scopeId)} from a backup; otherwise, or to publish the view under the data root from now on, make the folder ${legacy}: the view is published there at the next change at its sources`, { record: allocationFile(workspaceRoot, scopeId), path: legacy })
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Where a view's vault is, without creating anything: { path, origin, allocation }.
|
|
228
|
+
export function vaultRootFor({ workspaceRoot, workspaceId, scopeId }) {
|
|
229
|
+
const allocation = readVaultAllocation({ workspaceRoot, workspaceId, scopeId })
|
|
230
|
+
if (allocation === null) refuseLostAllocation({ workspaceRoot, scopeId })
|
|
231
|
+
return allocation === null
|
|
232
|
+
? { path: legacyVaultRoot(workspaceRoot, scopeId), origin: 'legacy-data-root', allocation: null }
|
|
233
|
+
: { path: allocation.path, origin: 'allocated', allocation }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Whether a view has a committed generation, whichever folder it was published into.
|
|
237
|
+
export const hasCommittedGeneration = ({ workspaceRoot, scopeId }) => fs.existsSync(path.join(workspaceRoot, 'state', 'manifests', segment(scopeId), 'current.json'))
|
|
238
|
+
|
|
114
239
|
// `repositoryRoots` is required: every enrolled repository root of the loaded
|
|
115
240
|
// project. Neither the workspace state nor the vault may overlap one of them,
|
|
116
241
|
// in either direction, lexically or by real path; the store refuses before it
|
|
117
242
|
// creates anything. A caller with no enrolled repository (a test in a
|
|
118
243
|
// temporary directory) says so with an explicit empty list.
|
|
244
|
+
//
|
|
245
|
+
// Without `vaultRoot` the vault is where the view's allocation says, else in
|
|
246
|
+
// the workspace's `vaults/` (vaultRootFor). An allocated folder must be the
|
|
247
|
+
// very folder the record names (device and inode): one that has gone is made
|
|
248
|
+
// again by the maintenance engine under its lock (ensureVaultAllocation), and
|
|
249
|
+
// until then, like one that another folder replaced, the store refuses. A
|
|
250
|
+
// published view whose record was lost is refused too, never published again
|
|
251
|
+
// into a new vault under the data root.
|
|
119
252
|
export function createRecoveryStore({ workspaceRoot, workspaceId, scopeId, vaultRoot, repositoryRoots } = {}) {
|
|
120
253
|
if (typeof workspaceRoot !== 'string' || !path.isAbsolute(workspaceRoot)) throw new TypeError('workspaceRoot must be an absolute path')
|
|
121
254
|
for (const [label, value] of [['workspaceId', workspaceId], ['scopeId', scopeId]]) {
|
|
@@ -125,20 +258,28 @@ export function createRecoveryStore({ workspaceRoot, workspaceId, scopeId, vault
|
|
|
125
258
|
throw new TypeError('repositoryRoots must list the absolute root of every enrolled repository; pass an explicit empty list when there is none')
|
|
126
259
|
}
|
|
127
260
|
if (vaultRoot !== undefined && (typeof vaultRoot !== 'string' || !path.isAbsolute(vaultRoot))) throw new TypeError('vaultRoot must be an absolute path')
|
|
128
|
-
const
|
|
261
|
+
const allocation = vaultRoot === undefined ? readVaultAllocation({ workspaceRoot, workspaceId, scopeId }) : null
|
|
262
|
+
const namedVault = vaultRoot ?? allocation?.path
|
|
263
|
+
// A folder that is there but is not the one allocated (a link put where it was, say) is refused as such first, with
|
|
264
|
+
// its own next step; the checks below only read. One that has gone is checked against the repositories first.
|
|
265
|
+
if (allocation !== null && allocatedFolderState(allocation) !== 'missing') assertAllocatedFolder(allocation)
|
|
266
|
+
const guard = checkManagedRoots({ managedRoots: [workspaceRoot, ...(namedVault === undefined ? [] : [namedVault])], repositoryRoots })
|
|
129
267
|
if (!guard.ok) refuse(guard.refusals[0].code, guard.refusals[0].message, { refusals: guard.refusals })
|
|
268
|
+
if (allocation !== null) assertAllocatedFolder(allocation)
|
|
269
|
+
else if (vaultRoot === undefined) refuseLostAllocation({ workspaceRoot, scopeId })
|
|
130
270
|
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 })
|
|
131
271
|
// Both as the file system stores them: the vault root is what the app is told, and what it compares its own
|
|
132
272
|
// working-directory spelling with.
|
|
133
273
|
const root = realPathAsStored(workspaceRoot)
|
|
134
274
|
const privateDir = (...parts) => ensureContainedPrivateDirectory({ workspaceRoot: root, directory: path.join(root, ...parts), label: 'Obsidian publication state' })
|
|
135
|
-
const requestedVault =
|
|
136
|
-
// A vault this store places itself
|
|
275
|
+
const requestedVault = namedVault ?? legacyVaultRoot(root, scopeId)
|
|
276
|
+
// A vault this store places itself, under the data root or where the view's vault was allocated, is private to this
|
|
277
|
+
// user from the start: it will hold the plugin's bearer.
|
|
137
278
|
fs.mkdirSync(requestedVault, { recursive: true, ...(vaultRoot === undefined ? { mode: 0o700 } : {}) })
|
|
138
279
|
const vault = realPathAsStored(requestedVault)
|
|
139
280
|
const inside = (parent, child) => { const relative = path.relative(parent, child); return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)) }
|
|
140
281
|
for (const area of ['state', 'recovery', 'staging']) {
|
|
141
|
-
if (inside(path.join(root, area), vault) || inside(vault, path.join(root, area)))
|
|
282
|
+
if (inside(path.join(root, area), vault) || inside(vault, path.join(root, area))) refuse('vault-overlaps-private-state', 'the vault may not overlap private publication state')
|
|
142
283
|
}
|
|
143
284
|
const manifests = privateDir('state', 'manifests', segment(scopeId))
|
|
144
285
|
const journals = privateDir('state', 'journals', segment(scopeId))
|
|
@@ -152,10 +293,22 @@ export function createRecoveryStore({ workspaceRoot, workspaceId, scopeId, vault
|
|
|
152
293
|
workspaceId,
|
|
153
294
|
scopeId,
|
|
154
295
|
vaultRoot: vault,
|
|
155
|
-
// Placed by this store under the workspace state
|
|
156
|
-
// that is a link leads to a folder someone else chose, which is only looked
|
|
296
|
+
// Placed by this store under the workspace state or where the view's vault was allocated, rather than named by the
|
|
297
|
+
// caller, and really there: a vault path that is a link leads to a folder someone else chose, which is only looked
|
|
298
|
+
// at and never receives the bearer.
|
|
157
299
|
managedVaultRoot: vaultRoot === undefined && vault === requestedVault,
|
|
158
300
|
linkedVaultRoot: vaultRoot === undefined && vault !== requestedVault,
|
|
301
|
+
// Where the vault is and why: named by the caller, allocated for the view, or under the data root.
|
|
302
|
+
vaultOrigin: vaultRoot !== undefined ? 'explicit' : allocation === null ? 'legacy-data-root' : 'allocated',
|
|
303
|
+
allocation,
|
|
304
|
+
// Refuses, typed, once the folder of an allocated vault is no longer the one this store was made for (see
|
|
305
|
+
// assertAllocatedFolder), and once any other vault root is gone or leads elsewhere (vault-root-moved).
|
|
306
|
+
checkAllocatedVault() {
|
|
307
|
+
if (allocation !== null) { assertAllocatedFolder(allocation); return }
|
|
308
|
+
let real = null
|
|
309
|
+
try { real = realPathAsStored(vault) } catch { /* gone: refused below */ }
|
|
310
|
+
if (real !== vault) refuse('vault-root-moved', 'this view\'s vault is gone, or its path now leads somewhere else; nothing more is written into it', { path: vault, leadsTo: real })
|
|
311
|
+
},
|
|
159
312
|
journalsRoot: journals,
|
|
160
313
|
lockPath: path.join(locks, `${segment(scopeId)}.lock`),
|
|
161
314
|
// One publisher per vault, whichever view or workspace state it belongs to.
|
|
@@ -4,7 +4,7 @@ import { isoTime } from '../../../runtime/obsidian/documents.mjs'
|
|
|
4
4
|
import { refuse } from '../../../runtime/obsidian/errors.mjs'
|
|
5
5
|
import { protectedRoots, readMachineSettings } from '../../../runtime/obsidian/machine-settings.mjs'
|
|
6
6
|
import { resolveScope } from '../../../runtime/obsidian/opening.mjs'
|
|
7
|
-
import {
|
|
7
|
+
import { createProductionSeams, eligibilityFor } from '../../../runtime/obsidian/pipeline.mjs'
|
|
8
8
|
import { createMaintenanceStateStore } from '../../../runtime/obsidian/state-store.mjs'
|
|
9
9
|
import { readConflictView } from './conflict-view.mjs'
|
|
10
10
|
import { runPolicySetup } from './policy-setup.mjs'
|
|
@@ -28,8 +28,9 @@ const EXIT = Object.freeze({ ok: 0, notSuccess: 3 })
|
|
|
28
28
|
const MAX_REQUEST_BYTES = 64 * 1024
|
|
29
29
|
|
|
30
30
|
// The canonical snapshot selectScope reads, built exactly as the engine
|
|
31
|
-
// builds it: the canonical graph with
|
|
32
|
-
// declared relations whose both endpoints the
|
|
31
|
+
// builds it: the canonical graph with the eligibility the machine settings
|
|
32
|
+
// decide (fail closed), and the declared relations whose both endpoints the
|
|
33
|
+
// census knows.
|
|
33
34
|
function snapshotNow({ project, seams, eligibility }) {
|
|
34
35
|
const graph = seams.buildGraph({ project, eligibility })
|
|
35
36
|
const known = new Set(graph.nodes.map((node) => node.id))
|
|
@@ -49,7 +50,7 @@ function readRequest(value) {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
export function createSelectionOperation({ seams = createProductionSeams(), eligibility =
|
|
53
|
+
export function createSelectionOperation({ seams = createProductionSeams(), eligibility: fixedEligibility = null } = {}) {
|
|
53
54
|
return {
|
|
54
55
|
name: 'selection',
|
|
55
56
|
summary: 'resolve ID | persist ID [allow-empty] | show ID | list The exact selected set of a declared view, its note paths and, for a focus, the graph query; persisted in Atelier state.',
|
|
@@ -74,7 +75,7 @@ export function createSelectionOperation({ seams = createProductionSeams(), elig
|
|
|
74
75
|
const usable = workspace !== null && workspace.workspaceRoot !== null
|
|
75
76
|
const machine = usable ? readMachineSettings(workspace) : null
|
|
76
77
|
const profile = seams.profileFor({ project, workspaceId: workspace?.workspaceId ?? 'ws-unprepared', audienceAllow: machine?.audienceAllow ?? [] })
|
|
77
|
-
const canonicalSnapshot = snapshotNow({ project, seams, eligibility })
|
|
78
|
+
const canonicalSnapshot = snapshotNow({ project, seams, eligibility: fixedEligibility ?? eligibilityFor({ machine, project }) })
|
|
78
79
|
const pathRegistry = usable ? createMaintenanceStateStore(workspace).readPathRegistry() : null
|
|
79
80
|
// Explicit empty is an honest answer of `resolve`; `persist` of an empty selection is refused and asks for it by name.
|
|
80
81
|
const selection = resolveSelection({ canonicalSnapshot, profile, scope, pathRegistry, allowEmpty: sub === 'resolve' || option === 'allow-empty' })
|