@mnstry/atelier 0.2.0-alpha.12 → 0.2.0-alpha.14
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 +894 -0
- package/README.md +159 -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 +267 -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 +210 -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 +237 -0
- package/docs/learning.md +215 -0
- package/docs/local-services.md +223 -19
- package/docs/obsidian-contract.md +246 -15
- package/docs/obsidian-plugin.md +252 -7
- package/docs/obsidian.md +874 -52
- package/docs/optional-ingestion.md +83 -0
- package/docs/personal-workspace.md +266 -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 +50 -0
- package/docs/repo-boundary-guard.md +120 -3
- 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 +318 -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 +219 -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 +76 -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 +60 -5
- package/plugins/obsidian/main.js +57 -8
- package/plugins/obsidian/manifest.json +1 -1
- 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 +536 -69
- package/src/boundary/delegation-command.mjs +158 -0
- package/src/boundary/delegation.mjs +396 -0
- package/src/boundary/policy.mjs +185 -64
- 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 +66 -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 +772 -65
- 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 +30 -6
- package/src/graph/knowledge-graph.mjs +77 -9
- 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 +938 -0
- package/src/ingestion/optional/graphify/json_bridge.py +163 -0
- package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
- package/src/ingestion/optional/graphify/native-pins.json +306 -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 +49 -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 +401 -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/path-match.mjs +2 -1
- package/src/projection/obsidian/edits/apply.mjs +55 -16
- package/src/projection/obsidian/materialize/prepare-view.mjs +87 -5
- package/src/projection/obsidian/personal-workspace.mjs +519 -0
- package/src/projection/obsidian/proposals/adapter.mjs +79 -17
- package/src/projection/obsidian/proposals/observation.mjs +50 -4
- package/src/projection/obsidian/publication/publisher.mjs +214 -18
- 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/projection/project.mjs +3 -2
- 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 +437 -56
- 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 +108 -23
- package/src/runtime/obsidian/login-item.mjs +335 -0
- package/src/runtime/obsidian/machine-settings.mjs +119 -12
- package/src/runtime/obsidian/observation.mjs +36 -4
- package/src/runtime/obsidian/opening.mjs +173 -38
- package/src/runtime/obsidian/personal-selection.mjs +125 -0
- package/src/runtime/obsidian/pipeline.mjs +115 -6
- 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/private-lock.mjs +39 -4
- 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 +148 -19
- 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 +102 -9
- 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/runtime/obsidian/view-permission.mjs +334 -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/control-system.mjs +201 -0
- package/src/ui/html-primitives.mjs +3 -0
- package/src/ui/knowledge-page.mjs +1388 -0
- package/src/upgrade/closure-diagnostics.mjs +135 -0
- package/src/upgrade/closure.mjs +649 -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 +20 -7
- package/templates/external-project-workspace/pack-lifecycle.json +7 -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
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { prepareViewCooperatively } from '../../projection/obsidian/materialize/prepare-view.mjs'
|
|
1
2
|
import { randomBytes as cryptoRandomBytes } from 'node:crypto'
|
|
2
3
|
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
3
5
|
import { AtelierDiagnosticError } from '../../project/config.mjs'
|
|
4
|
-
import { ObsidianContractRefusal } from '../../projection/obsidian/contracts.mjs'
|
|
6
|
+
import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal } from '../../projection/obsidian/contracts.mjs'
|
|
5
7
|
import { prepareView as productionPrepareView } from '../../projection/obsidian/materialize/index.mjs'
|
|
6
8
|
import { preparePluginFiles } from '../../projection/obsidian/plugin-bridge/bundle.mjs'
|
|
7
9
|
import { publishView as productionPublishView } from '../../projection/obsidian/publication/publisher.mjs'
|
|
@@ -21,6 +23,7 @@ import {
|
|
|
21
23
|
import { createServiceServer } from './service-server.mjs'
|
|
22
24
|
import { OPEN_EDIT_STATES, createMaintenanceStateStore } from './state-store.mjs'
|
|
23
25
|
import { DEFAULT_MAX_BACKOFF_MS, DEFAULT_TICK_INTERVAL_MS, createTickLoop } from './tick-loop.mjs'
|
|
26
|
+
import { createLoadReport, createViewPermission } from './view-permission.mjs'
|
|
24
27
|
import { createFsWatcherFactory } from './watchers.mjs'
|
|
25
28
|
|
|
26
29
|
// The maintenance service of one workspace, inside one process.
|
|
@@ -48,12 +51,33 @@ import { createFsWatcherFactory } from './watchers.mjs'
|
|
|
48
51
|
//
|
|
49
52
|
// `adapterFactory` has no default here either: whoever starts the service
|
|
50
53
|
// decides whether it may reach a running app.
|
|
54
|
+
//
|
|
55
|
+
// Started by a login item (`startup`), the service also asks `releaseWatch`
|
|
56
|
+
// after every tick whether the package it runs from changed on disk; when it
|
|
57
|
+
// did, it finishes that tick and shuts down with the reason RELEASE_CHANGED,
|
|
58
|
+
// which its process turns into an exit code the service manager restarts, on
|
|
59
|
+
// the new release (release-watch.mjs, service-main.mjs).
|
|
51
60
|
|
|
52
61
|
export const SERVICE_STATUS_SCHEMA = 'atelier-obsidian-service-status/v1'
|
|
53
62
|
export const DEFAULT_SHUTDOWN_GRACE_MS = 30 * 1000
|
|
63
|
+
export const RELEASE_CHANGED = 'release-changed'
|
|
64
|
+
|
|
65
|
+
// What of a project its workspace is resolved from, as a project that holds nothing else: the folder its pointer is
|
|
66
|
+
// under (readLocalPointer), the data root its local overlay prefers (resolveDataRoot), and the repositories private
|
|
67
|
+
// state may not overlap (protectedRoots). resolveServiceWorkspace reads these and nothing else of a project, so two
|
|
68
|
+
// projects with the same inputs resolve alike, and the plugin's status read compares exactly them (view-permission.mjs).
|
|
69
|
+
export function serviceWorkspaceInputs(project) {
|
|
70
|
+
const preference = project?.localOverlay?.overlay?.preferences?.[OBSIDIAN_EXT_KEY]
|
|
71
|
+
return {
|
|
72
|
+
...(typeof project?.configDir === 'string' ? { configDir: project.configDir } : {}),
|
|
73
|
+
repos: (Array.isArray(project?.repos) ? project.repos : []).map((repo) => ({ path: repo?.path, external: Boolean(repo?.external) })),
|
|
74
|
+
localOverlay: { overlay: { preferences: preference === undefined ? {} : { [OBSIDIAN_EXT_KEY]: preference } } },
|
|
75
|
+
}
|
|
76
|
+
}
|
|
54
77
|
|
|
55
78
|
// Where a workspace keeps its private state, without creating an identity.
|
|
56
|
-
export function resolveServiceWorkspace({ project, dataRoot, env = process.env, platform = process.platform, create = false }) {
|
|
79
|
+
export function resolveServiceWorkspace({ project: whole, dataRoot, env = process.env, platform = process.platform, create = false }) {
|
|
80
|
+
const project = serviceWorkspaceInputs(whole)
|
|
57
81
|
const pointer = readLocalPointer(project)
|
|
58
82
|
if (pointer === null) return null
|
|
59
83
|
const requested = workspaceStateRoot(resolveDataRoot({ dataRoot, pointer, project, env, platform }), pointer.workspaceId)
|
|
@@ -79,18 +103,37 @@ async function writeRecordPatiently(input) {
|
|
|
79
103
|
|
|
80
104
|
export async function runMaintenanceService(options = {}) {
|
|
81
105
|
const {
|
|
82
|
-
loadProject, dataRoot,
|
|
106
|
+
loadProject, dataRoot,
|
|
107
|
+
// What the workspace is resolved from at once, where the loader may answer a promise (locatePersonalHome): the service
|
|
108
|
+
// starts from it, and the engine awaits the loader at its ticks. Without it, the loader's own first answer is used.
|
|
109
|
+
locateProject = null, adapterFactory, entryPath, runtimeId = `rt-${cryptoRandomBytes(16).toString('hex')}`, startup = false,
|
|
83
110
|
intervalMs = DEFAULT_TICK_INTERVAL_MS, maxBackoffMs = Math.max(DEFAULT_MAX_BACKOFF_MS, intervalMs), shutdownGraceMs = DEFAULT_SHUTDOWN_GRACE_MS,
|
|
84
111
|
clock = () => new Date(), log = () => {}, pid = process.pid, randomBytes = cryptoRandomBytes, env = process.env, platform = process.platform,
|
|
85
112
|
engineOptions = {}, createEngine = createMaintenanceEngine,
|
|
86
113
|
// What the adapter factory last learned about the installed app, when it qualifies one. Codes and versions only.
|
|
87
114
|
appStatus = null,
|
|
115
|
+
// Under `startup`: { changed() } (createReleaseWatch), asked after every tick.
|
|
116
|
+
releaseWatch = null,
|
|
117
|
+
// The entry as this process was started by it (`process.argv[1]`), when that is not its real path: a login item
|
|
118
|
+
// names the installed entry by its path. It is recorded, so the process table can still prove this process ours.
|
|
119
|
+
invokedAs = null,
|
|
88
120
|
} = options
|
|
89
121
|
if (typeof loadProject !== 'function') throw new TypeError('the service needs loadProject')
|
|
90
122
|
if (typeof adapterFactory !== 'function') throw new TypeError('the service needs an adapterFactory')
|
|
91
123
|
if (typeof entryPath !== 'string') throw new TypeError('the service needs the path of its entry module')
|
|
92
124
|
|
|
93
|
-
|
|
125
|
+
// The workspace comes from the project the loader answers now, or the locator. A loader without a locator, or a locator,
|
|
126
|
+
// that answers a promise is refused, typed, before anything is read from the promise (it has no pointer, no overlay and
|
|
127
|
+
// no repositories); its answer is let go unread. A located project names its folder, or is refused.
|
|
128
|
+
const project = locateProject === null ? loadProject() : locateProject()
|
|
129
|
+
if (typeof project?.then === 'function') {
|
|
130
|
+
// Let go without calling a `then` of its own, which a promise whose `constructor` reads as this realm's `Promise`
|
|
131
|
+
// may carry and which may throw; any other thenable is adopted, its `then` called once.
|
|
132
|
+
Promise.prototype.then.call(Promise.resolve(project), undefined, () => {})
|
|
133
|
+
refuse('service-loader-not-synchronous', 'the service resolves its workspace from a project its loader answers at once; a loader that answers a promise is not supported when the service starts')
|
|
134
|
+
}
|
|
135
|
+
if (locateProject !== null && typeof project?.configDir !== 'string') refuse('service-locator-invalid', 'the locator of this service answered no folder to resolve its workspace from')
|
|
136
|
+
const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform })
|
|
94
137
|
if (!workspace?.workspaceRoot) refuse('service-workspace-not-prepared', 'this workspace has no private state yet; `start` prepares it')
|
|
95
138
|
const { workspaceId, workspaceRoot } = workspace
|
|
96
139
|
const settings = readServiceSettings({ workspaceRoot, workspaceId })
|
|
@@ -107,6 +150,7 @@ export async function runMaintenanceService(options = {}) {
|
|
|
107
150
|
}
|
|
108
151
|
|
|
109
152
|
const executable = executableIdentity(entryPath)
|
|
153
|
+
const alias = typeof invokedAs === 'string' && path.isAbsolute(invokedAs) && path.resolve(invokedAs) !== executable.path ? path.resolve(invokedAs) : null
|
|
110
154
|
const startedAt = isoTime(clock)
|
|
111
155
|
const identity = { serviceName: serviceNameFor(workspaceId), workspaceId, runtimeId, pid, host, port, executableDigest: executable.digest, startedAt }
|
|
112
156
|
const bearer = randomBytes(32).toString('base64url')
|
|
@@ -117,6 +161,10 @@ export async function runMaintenanceService(options = {}) {
|
|
|
117
161
|
let stopping = false
|
|
118
162
|
let consecutiveFailures = 0
|
|
119
163
|
let lastTick = null
|
|
164
|
+
// What the last tick that did not fail named each refused view for (refusalCauses). A view the next tick names for the
|
|
165
|
+
// same reason is not written to the log again: a refusal that lasts (a folder that stays unwritable is tried again at
|
|
166
|
+
// every tick) is written once, and again when its reason changes or after a tick that failed or did not name it.
|
|
167
|
+
let refusedLastTick = new Map()
|
|
120
168
|
let settleDone
|
|
121
169
|
const done = new Promise((resolve) => { settleDone = resolve })
|
|
122
170
|
|
|
@@ -126,7 +174,7 @@ export async function runMaintenanceService(options = {}) {
|
|
|
126
174
|
// view without the plugin for this tick; the view itself is prepared as ever.
|
|
127
175
|
const pluginFor = (scopeId) => {
|
|
128
176
|
try {
|
|
129
|
-
const vaultRoot = viewVaultRoot(workspaceRoot, scopeId)
|
|
177
|
+
const vaultRoot = viewVaultRoot(workspaceRoot, scopeId, workspaceId)
|
|
130
178
|
const { choice, community } = decidePluginChoice({ workspaceRoot, workspaceId, scopeId, vaultRoot, clock })
|
|
131
179
|
const off = choice.state === 'off'
|
|
132
180
|
const bearer = ensurePluginBearer({ workspaceRoot, workspaceId, scopeId, randomBytes, clock })
|
|
@@ -146,6 +194,14 @@ export async function runMaintenanceService(options = {}) {
|
|
|
146
194
|
}
|
|
147
195
|
}
|
|
148
196
|
const prepareWithPlugin = (input) => (engineOptions.seams?.prepareView ?? productionPrepareView)({ ...input, plugin: pluginFor(input.scope.scopeId) })
|
|
197
|
+
// The engine's rule (engine.mjs): a prepareView that is not the one that ships, handed in with the cooperative
|
|
198
|
+
// preparation left as it ships or absent, prepares the view, synchronously; the engine then gets no cooperative seam.
|
|
199
|
+
// Nor does it when the cooperative preparation was handed in as null: that says there is none, as it does to the engine.
|
|
200
|
+
const handed = engineOptions.seams ?? {}
|
|
201
|
+
const replacedPrepare = typeof handed.prepareView === 'function' && handed.prepareView !== productionPrepareView
|
|
202
|
+
&& (handed.prepareViewCooperatively === undefined || handed.prepareViewCooperatively === prepareViewCooperatively)
|
|
203
|
+
const cooperative = replacedPrepare || handed.prepareViewCooperatively === null ? null : handed.prepareViewCooperatively ?? prepareViewCooperatively
|
|
204
|
+
const prepareCooperativelyWithPlugin = cooperative === null ? null : (input) => cooperative({ ...input, plugin: pluginFor(input.scope.scopeId) })
|
|
149
205
|
// An entry offered to a vault and now in place is confirmed: from then on, a list without it is the person's decision.
|
|
150
206
|
const publishAndConfirm = async (input) => {
|
|
151
207
|
const result = await (engineOptions.seams?.publishView ?? productionPublishView)(input)
|
|
@@ -160,8 +216,8 @@ export async function runMaintenanceService(options = {}) {
|
|
|
160
216
|
// The engine also asks the factory itself (`forget`, on a tick somebody asked for): the wrapper keeps its methods.
|
|
161
217
|
const pluginAwareAdapterFactory = Object.assign((input) => adapterFactory({ ...input, pluginReport: typeof input?.scope?.scopeId === 'string' ? pluginReportOf(input.scope.scopeId) : null }), adapterFactory)
|
|
162
218
|
const engine = createEngine({
|
|
163
|
-
watcherFactory: createFsWatcherFactory(), ...engineOptions, seams: { ...(engineOptions.seams ?? {}), prepareView: prepareWithPlugin, publishView: publishAndConfirm },
|
|
164
|
-
loadProject, dataRoot, adapterFactory: pluginAwareAdapterFactory, clock, env, platform, lockOwner: { host, port, runtimeId },
|
|
219
|
+
watcherFactory: createFsWatcherFactory(), ...engineOptions, seams: { ...(engineOptions.seams ?? {}), prepareView: prepareWithPlugin, prepareViewCooperatively: prepareCooperativelyWithPlugin, publishView: publishAndConfirm },
|
|
220
|
+
loadProject, locateProject, dataRoot, adapterFactory: pluginAwareAdapterFactory, clock, env, platform, lockOwner: { host, port, runtimeId },
|
|
165
221
|
})
|
|
166
222
|
|
|
167
223
|
const recordIsOurs = () => {
|
|
@@ -173,11 +229,20 @@ export async function runMaintenanceService(options = {}) {
|
|
|
173
229
|
consecutiveFailures = outcome.consecutiveFailures
|
|
174
230
|
if (outcome.ok) {
|
|
175
231
|
lastTick = { at, state: outcome.report.state, reason: outcome.report.reason ?? outcome.report.refusal?.code ?? null }
|
|
232
|
+
// A view the tick refused for a file-system error: the view's freshness names the refusal, and this line the error.
|
|
233
|
+
const refused = new Map()
|
|
234
|
+
for (const item of outcome.report.refusalCauses ?? []) {
|
|
235
|
+
const reason = JSON.stringify([item.code, item.cause ?? null, item.cleanupCause ?? null])
|
|
236
|
+
refused.set(item.scopeId, reason)
|
|
237
|
+
if (refusedLastTick.get(item.scopeId) !== reason) log({ at, event: 'view-refused', scopeId: item.scopeId, code: item.code, ...(item.cause ? { cause: item.cause } : {}), ...(item.cleanupCause ? { cleanupCause: item.cleanupCause } : {}) })
|
|
238
|
+
}
|
|
239
|
+
refusedLastTick = refused
|
|
176
240
|
const previous = readLastServiceError({ workspaceRoot, workspaceId })
|
|
177
241
|
if (previous && previous.resolvedAt === null) writeLastServiceError({ workspaceRoot, workspaceId, document: { ...previous, consecutiveFailures: 0, resolvedAt: at } })
|
|
178
242
|
return
|
|
179
243
|
}
|
|
180
244
|
lastTick = { at, state: 'failed', reason: errorCode(outcome.error) }
|
|
245
|
+
refusedLastTick = new Map()
|
|
181
246
|
// The message may carry a path or a title: it goes to the private log and nowhere else.
|
|
182
247
|
log({ at, event: 'tick-failed', code: errorCode(outcome.error), name: errorName(outcome.error), message: String(outcome.error?.message ?? outcome.error), consecutiveFailures })
|
|
183
248
|
let previous = null
|
|
@@ -193,6 +258,18 @@ export async function runMaintenanceService(options = {}) {
|
|
|
193
258
|
log({ at: isoTime(clock), event: 'plugin-drift-not-read', code: errorCode(error), name: errorName(error) })
|
|
194
259
|
}
|
|
195
260
|
}
|
|
261
|
+
// What a publication of this tick could not change is no drift to ask about again (plugin-drift.mjs).
|
|
262
|
+
const settleDriftLooks = () => {
|
|
263
|
+
try { pluginDrift.settle([...pluginChannel.bearers().keys()]) } catch (error) {
|
|
264
|
+
log({ at: isoTime(clock), event: 'plugin-drift-not-read', code: errorCode(error), name: errorName(error) })
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// A package that changed on disk: another release, loaded module by module into this process from then on.
|
|
269
|
+
const releaseChanged = () => {
|
|
270
|
+
if (!startup || releaseWatch === null) return false
|
|
271
|
+
try { return releaseWatch.changed() === true } catch { return false }
|
|
272
|
+
}
|
|
196
273
|
|
|
197
274
|
const loop = createTickLoop({
|
|
198
275
|
intervalMs, maxBackoffMs, onOutcome: recordOutcome,
|
|
@@ -200,7 +277,14 @@ export async function runMaintenanceService(options = {}) {
|
|
|
200
277
|
// The service runs only while its record names it: a replaced or removed record ends it, cleanly.
|
|
201
278
|
if (!recordIsOurs()) { void shutdown('record-no-longer-names-this-runtime'); return { state: 'stopping', reason: 'record-no-longer-names-this-runtime' } }
|
|
202
279
|
askForDriftedViews()
|
|
203
|
-
|
|
280
|
+
try {
|
|
281
|
+
const report = await engine.tick()
|
|
282
|
+
if (report?.state === 'ticked') settleDriftLooks()
|
|
283
|
+
return report
|
|
284
|
+
} finally {
|
|
285
|
+
// After the tick, whether it succeeded or not: the next one runs in a process of the release now on disk.
|
|
286
|
+
if (!stopping && releaseChanged()) { log({ at: isoTime(clock), event: RELEASE_CHANGED }); void shutdown(RELEASE_CHANGED) }
|
|
287
|
+
}
|
|
204
288
|
},
|
|
205
289
|
})
|
|
206
290
|
|
|
@@ -218,8 +302,16 @@ export async function runMaintenanceService(options = {}) {
|
|
|
218
302
|
|
|
219
303
|
const healthStatus = () => (stopping ? 'stopped' : consecutiveFailures > 0 ? 'degraded' : 'healthy')
|
|
220
304
|
|
|
305
|
+
// What is stored about a view is not permission to tell it: the project is asked as it is now (view-permission.mjs).
|
|
306
|
+
// Its loads are counted for the status document; the log gets a line for those somebody has to see (createLoadReport).
|
|
307
|
+
const statusLoads = createLoadReport({ write: (entry) => log({ at: isoTime(clock), ...entry }) })
|
|
308
|
+
const viewPermitted = createViewPermission({
|
|
309
|
+
loadProject, resolveWorkspace: (loaded) => resolveServiceWorkspace({ project: loaded, dataRoot, env, platform }), workspaceInputsOf: serviceWorkspaceInputs,
|
|
310
|
+
workspaceId, workspaceRoot, onLoad: statusLoads.onLoad,
|
|
311
|
+
})
|
|
221
312
|
// One view as the plugin is told about it: its freshness entry with held notes counted, and its open pending edits.
|
|
222
313
|
function pluginStatusOf(scopeId) {
|
|
314
|
+
if (!viewPermitted(scopeId)) return { view: null, pendingEdits: null }
|
|
223
315
|
let entry = null
|
|
224
316
|
try { entry = stateStore.readFreshness()?.scopes.find((item) => item.scopeId === scopeId) ?? null } catch (error) { if (!isTyped(error)) throw error; return { view: null, pendingEdits: null } }
|
|
225
317
|
let open = null
|
|
@@ -248,6 +340,7 @@ export async function runMaintenanceService(options = {}) {
|
|
|
248
340
|
return {
|
|
249
341
|
schema: SERVICE_STATUS_SCHEMA, service: { ...identity, status: healthStatus() }, loop: loop.state(), lastTick, lastError, freshness: freshnessSummary(),
|
|
250
342
|
plugins: pluginPresence({ sessions: pluginSessions, scopeIds: [...pluginChannel.bearers().keys()], entryOf: (scopeId) => currentPluginChoice({ workspaceRoot, workspaceId, scopeId }).state }),
|
|
343
|
+
statusLoads: statusLoads.counts(),
|
|
251
344
|
...(typeof appStatus === 'function' ? { app: appStatus() } : {}),
|
|
252
345
|
}
|
|
253
346
|
},
|
|
@@ -296,7 +389,7 @@ export async function runMaintenanceService(options = {}) {
|
|
|
296
389
|
workspaceRoot, workspaceId,
|
|
297
390
|
record: {
|
|
298
391
|
schema: 'atelier-obsidian-service-state/v1', contractVersion: '1.0.0', workspaceId, serviceName: identity.serviceName, host, port, runtimeId, pid,
|
|
299
|
-
executable: { path: executable.path, digest: executable.digest, ext: { runner: process.execPath, release: releaseIdentity() } }, stateLocation: servicePaths(workspaceRoot).stateLocation,
|
|
392
|
+
executable: { path: executable.path, digest: executable.digest, ext: { runner: process.execPath, release: releaseIdentity(), ...(alias === null ? {} : { invokedAs: alias }) } }, stateLocation: servicePaths(workspaceRoot).stateLocation,
|
|
300
393
|
health: { status: 'healthy', checkedAt: startedAt }, consent: settings.consent, ext: { bearer },
|
|
301
394
|
},
|
|
302
395
|
})
|
|
@@ -4,9 +4,8 @@ import { refuse } from './errors.mjs'
|
|
|
4
4
|
// Builds the text of an operating-system startup unit for the maintenance
|
|
5
5
|
// service. Pure: it returns text and touches nothing. It writes no file,
|
|
6
6
|
// installs nothing and runs no service manager. Installing a unit is a
|
|
7
|
-
// separate
|
|
8
|
-
// recorded consent that covers startup
|
|
9
|
-
// without one.
|
|
7
|
+
// separate step (login-item.mjs) that a person asks for, and the service
|
|
8
|
+
// refuses `--startup` without a recorded consent that covers startup.
|
|
10
9
|
//
|
|
11
10
|
// Every machine-specific value is an input. Nothing is looked up here: no
|
|
12
11
|
// home directory, no environment, no current directory, no installed path.
|
|
@@ -15,17 +14,30 @@ import { refuse } from './errors.mjs'
|
|
|
15
14
|
// Linux a systemd user unit
|
|
16
15
|
// Windows refused, typed: startup there has not been qualified
|
|
17
16
|
//
|
|
18
|
-
// The unit runs the service entry in the foreground with `--startup
|
|
19
|
-
//
|
|
17
|
+
// The unit runs the service entry in the foreground with `--startup`, in the
|
|
18
|
+
// root directory (never one that could be a vault), with the search path it
|
|
19
|
+
// is given, so the service finds the same programs the person uses. It is
|
|
20
|
+
// restarted after a crash and after an exit with a code the service uses to
|
|
21
|
+
// ask for it (a new release on disk), never after a clean stop and never in a
|
|
22
|
+
// tight loop; a refusal under `--startup` exits cleanly. The service writes
|
|
23
|
+
// its own bounded log; the unit's output file receives only what happens
|
|
24
|
+
// before it can, such as a module that cannot be loaded.
|
|
20
25
|
|
|
21
26
|
export const STARTUP_PLATFORMS = Object.freeze(['darwin', 'linux'])
|
|
22
27
|
const LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
28
|
+
const CONTROL = /[\u0000-\u001f\u007f]/
|
|
29
|
+
// Seconds: the least time between two starts, and how long a stop may take before the manager ends the process. The
|
|
30
|
+
// service gives a tick in flight 30 seconds to finish when it is asked to stop.
|
|
31
|
+
const THROTTLE_SECONDS = 60
|
|
32
|
+
const EXIT_TIMEOUT_SECONDS = 60
|
|
23
33
|
|
|
24
34
|
const xml = (value) => value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''')
|
|
25
35
|
// systemd: quoted, with its specifier and variable characters doubled.
|
|
26
36
|
const unitWord = (value) => `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%').replaceAll('$', '$$$$')}"`
|
|
37
|
+
// systemd: one quoted assignment of Environment=, where `$` has no meaning and only specifiers are expanded.
|
|
38
|
+
const unitAssignment = (name, value) => `"${`${name}=${value}`.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%')}"`
|
|
27
39
|
|
|
28
|
-
function launchdText({ label, words, logPath, workingDirectory }) {
|
|
40
|
+
function launchdText({ label, words, logPath, workingDirectory, searchPath }) {
|
|
29
41
|
const lines = [
|
|
30
42
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
31
43
|
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
@@ -33,12 +45,15 @@ function launchdText({ label, words, logPath, workingDirectory }) {
|
|
|
33
45
|
'<dict>',
|
|
34
46
|
' <key>Label</key>', ` <string>${xml(label)}</string>`,
|
|
35
47
|
' <key>ProgramArguments</key>', ' <array>', ...words.map((word) => ` <string>${xml(word)}</string>`), ' </array>',
|
|
48
|
+
...(searchPath === null ? [] : [' <key>EnvironmentVariables</key>', ' <dict>', ' <key>PATH</key>', ` <string>${xml(searchPath)}</string>`, ' </dict>']),
|
|
49
|
+
' <key>WorkingDirectory</key>', ` <string>${xml(workingDirectory)}</string>`,
|
|
36
50
|
' <key>RunAtLoad</key>', ' <true/>',
|
|
37
|
-
// Restarted after a crash, not after a clean stop; a refusal is not retried
|
|
51
|
+
// Restarted after a crash or a non-zero exit, not after a clean stop; a refusal exits cleanly and is not retried.
|
|
38
52
|
' <key>KeepAlive</key>', ' <dict>', ' <key>SuccessfulExit</key>', ' <false/>', ' </dict>',
|
|
39
|
-
' <key>ThrottleInterval</key>',
|
|
40
|
-
' <key>
|
|
41
|
-
|
|
53
|
+
' <key>ThrottleInterval</key>', ` <integer>${THROTTLE_SECONDS}</integer>`,
|
|
54
|
+
' <key>ExitTimeOut</key>', ` <integer>${EXIT_TIMEOUT_SECONDS}</integer>`,
|
|
55
|
+
// Standard, not Background: the service's budgets were measured without the I/O throttling of a background process.
|
|
56
|
+
' <key>ProcessType</key>', ' <string>Standard</string>',
|
|
42
57
|
' <key>StandardOutPath</key>', ` <string>${xml(logPath)}</string>`,
|
|
43
58
|
' <key>StandardErrorPath</key>', ` <string>${xml(logPath)}</string>`,
|
|
44
59
|
'</dict>',
|
|
@@ -47,7 +62,7 @@ function launchdText({ label, words, logPath, workingDirectory }) {
|
|
|
47
62
|
return `${lines.join('\n')}\n`
|
|
48
63
|
}
|
|
49
64
|
|
|
50
|
-
function systemdText({ label, words, logPath, workingDirectory }) {
|
|
65
|
+
function systemdText({ label, words, logPath, workingDirectory, searchPath }) {
|
|
51
66
|
const lines = [
|
|
52
67
|
'[Unit]',
|
|
53
68
|
`Description=Atelier Obsidian maintenance (${label.replaceAll(/[^A-Za-z0-9._-]/g, '')})`,
|
|
@@ -55,11 +70,13 @@ function systemdText({ label, words, logPath, workingDirectory }) {
|
|
|
55
70
|
'[Service]',
|
|
56
71
|
'Type=simple',
|
|
57
72
|
`ExecStart=${words.map(unitWord).join(' ')}`,
|
|
58
|
-
...(
|
|
59
|
-
|
|
73
|
+
...(searchPath === null ? [] : [`Environment=${unitAssignment('PATH', searchPath)}`]),
|
|
74
|
+
`WorkingDirectory=${workingDirectory.replaceAll('%', '%%')}`,
|
|
75
|
+
// Restarted after a crash or a non-zero exit; not after a clean stop, and not after a refusal (exit status 2).
|
|
60
76
|
'Restart=on-failure',
|
|
61
|
-
|
|
77
|
+
`RestartSec=${THROTTLE_SECONDS}`,
|
|
62
78
|
'RestartPreventExitStatus=2',
|
|
79
|
+
`TimeoutStopSec=${EXIT_TIMEOUT_SECONDS}`,
|
|
63
80
|
`StandardOutput=append:${logPath.replaceAll('%', '%%')}`,
|
|
64
81
|
`StandardError=append:${logPath.replaceAll('%', '%%')}`,
|
|
65
82
|
'',
|
|
@@ -69,19 +86,40 @@ function systemdText({ label, words, logPath, workingDirectory }) {
|
|
|
69
86
|
return `${lines.join('\n')}\n`
|
|
70
87
|
}
|
|
71
88
|
|
|
72
|
-
//
|
|
73
|
-
|
|
89
|
+
// The search path a unit carries: the entries of `value` (a PATH as the environment holds it) that are absolute
|
|
90
|
+
// directories, in their order, each once, and none inside a temporary directory (`temporary`, absolute prefixes): a
|
|
91
|
+
// relative entry would be resolved in the unit's working directory, and a temporary one (an agent's session, say) is
|
|
92
|
+
// gone after a restart. Pure. Null when nothing is left.
|
|
93
|
+
export function startupSearchPath(value, { temporary = [] } = {}) {
|
|
94
|
+
if (typeof value !== 'string') return null
|
|
95
|
+
const inside = (entry) => temporary.some((prefix) => typeof prefix === 'string' && path.posix.isAbsolute(prefix) && (entry === prefix || entry.startsWith(prefix.endsWith('/') ? prefix : `${prefix}/`)))
|
|
96
|
+
const kept = []
|
|
97
|
+
for (const raw of value.split(':')) {
|
|
98
|
+
const entry = raw.length > 1 ? raw.replace(/\/+$/, '') : raw
|
|
99
|
+
if (!path.posix.isAbsolute(entry) || CONTROL.test(entry) || inside(entry) || kept.includes(entry)) continue
|
|
100
|
+
kept.push(entry)
|
|
101
|
+
}
|
|
102
|
+
return kept.length === 0 ? null : kept.join(':')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// { platform, kind, fileName, text }. `args` are the arguments of the entry after `--startup`; `searchPath` is the PATH
|
|
106
|
+
// the unit sets (absolute entries joined by `:`; see startupSearchPath), none when omitted.
|
|
107
|
+
export function buildStartupAdapter({ platform, label, nodePath, entryPath, args = [], logPath, workingDirectory = '/', searchPath = null } = {}) {
|
|
74
108
|
if (platform === 'win32') refuse('startup-platform-unqualified', 'operating-system startup on Windows has not been qualified; no unit is generated for it', { platform })
|
|
75
109
|
if (!STARTUP_PLATFORMS.includes(platform)) refuse('startup-platform-unsupported', 'no startup unit is known for this platform', { platform: String(platform) })
|
|
76
110
|
if (typeof label !== 'string' || !LABEL.test(label)) refuse('startup-adapter-input-invalid', 'the unit label must be a plain identifier')
|
|
77
111
|
const flavor = path.posix
|
|
78
|
-
for (const [name, value] of [['nodePath', nodePath], ['entryPath', entryPath], ['logPath', logPath],
|
|
79
|
-
if (typeof value !== 'string' || !flavor.isAbsolute(value) ||
|
|
112
|
+
for (const [name, value] of [['nodePath', nodePath], ['entryPath', entryPath], ['logPath', logPath], ['workingDirectory', workingDirectory]]) {
|
|
113
|
+
if (typeof value !== 'string' || !flavor.isAbsolute(value) || CONTROL.test(value)) refuse('startup-adapter-input-invalid', `${name} must be an absolute path given by the caller`, { name })
|
|
114
|
+
}
|
|
115
|
+
if (searchPath !== null && (typeof searchPath !== 'string' || searchPath.split(':').some((entry) => !flavor.isAbsolute(entry) || CONTROL.test(entry)))) {
|
|
116
|
+
refuse('startup-adapter-input-invalid', 'searchPath must be absolute directories joined by ":"', { name: 'searchPath' })
|
|
80
117
|
}
|
|
81
|
-
if (!Array.isArray(args) || args.some((word) => typeof word !== 'string' || word === '' ||
|
|
118
|
+
if (!Array.isArray(args) || args.some((word) => typeof word !== 'string' || word === '' || CONTROL.test(word))) refuse('startup-adapter-input-invalid', 'args must be a list of plain words')
|
|
82
119
|
if (args.includes('--startup')) refuse('startup-adapter-input-invalid', '--startup is added by the builder')
|
|
83
120
|
const words = [nodePath, entryPath, '--startup', ...args]
|
|
121
|
+
const input = { label, words, logPath, workingDirectory, searchPath }
|
|
84
122
|
return platform === 'darwin'
|
|
85
|
-
? { platform, kind: 'launchd-user-agent', fileName: `${label}.plist`, text: launchdText(
|
|
86
|
-
: { platform, kind: 'systemd-user-unit', fileName: `${label}.service`, text: systemdText(
|
|
123
|
+
? { platform, kind: 'launchd-user-agent', fileName: `${label}.plist`, text: launchdText(input) }
|
|
124
|
+
: { platform, kind: 'systemd-user-unit', fileName: `${label}.service`, text: systemdText(input) }
|
|
87
125
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
4
|
+
import { obsidianSandboxedBuild, obsidianUserDataDir, readObsidianSettings } from '../../projection/obsidian/publication/vault-list.mjs'
|
|
5
|
+
import { realPathOfLocation } from './vault-location.mjs'
|
|
6
|
+
|
|
7
|
+
// Whether the Obsidian this account runs lists a vault Atelier is filling for
|
|
8
|
+
// the first time, as the publisher's `direct-unheld` path needs to know
|
|
9
|
+
// (publisher.mjs; docs/obsidian-contract.md, "First publication into a vault
|
|
10
|
+
// no Obsidian lists"). Read from the app's settings file, never from the app:
|
|
11
|
+
//
|
|
12
|
+
// { unlisted: true } the file was read, and none of its entries is the vault root, a folder above
|
|
13
|
+
// it or a folder inside it
|
|
14
|
+
// { unlisted: false, reason, ... } anything else, which gives no evidence
|
|
15
|
+
//
|
|
16
|
+
// It errs towards "no evidence": a file that is missing (an app that has shown
|
|
17
|
+
// only its starter window writes none), that cannot be read after `attempts`
|
|
18
|
+
// reads `delayMs` apart (the app rewrites it with a plain write, so a reader
|
|
19
|
+
// can find it empty or cut short while it writes), that is not a JSON object,
|
|
20
|
+
// or that has an entry whose folder is not an absolute path or has a `..`
|
|
21
|
+
// segment (after a link, `..` leads where the text does not); and a Flatpak or
|
|
22
|
+
// snap build of the app anywhere on this account, whose list lives in its
|
|
23
|
+
// sandbox and not in this file. The root and every listed folder are compared
|
|
24
|
+
// as written and as their real paths, in any letter case, so a link on the way
|
|
25
|
+
// to either side, or another spelling, does not hide an entry.
|
|
26
|
+
|
|
27
|
+
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
28
|
+
const folded = (target) => target.normalize('NFC').toLowerCase()
|
|
29
|
+
// The real path of a folder that may not exist, and its lexical path when it cannot be resolved (a volume that is gone).
|
|
30
|
+
const realOrLexical = (target) => { try { return realPathOfLocation(target) } catch { return path.resolve(target) } }
|
|
31
|
+
const spellings = (target) => [...new Set([path.resolve(target), realOrLexical(target)].map(folded))]
|
|
32
|
+
// Strictly inside: `..x` is a folder name, not a way up.
|
|
33
|
+
const inside = (parent, child) => { const relative = path.relative(parent, child); return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) }
|
|
34
|
+
|
|
35
|
+
// How a listed folder stands to the vault root, or null when it is neither the root, above it, nor inside it.
|
|
36
|
+
function relationOf(entryPath, root) {
|
|
37
|
+
const listed = spellings(entryPath)
|
|
38
|
+
if (listed.some((folder) => root.includes(folder))) return 'vault-listed'
|
|
39
|
+
if (listed.some((folder) => root.some((candidate) => inside(folder, candidate)))) return 'vault-inside-listed-vault'
|
|
40
|
+
if (listed.some((folder) => root.some((candidate) => inside(candidate, folder)))) return 'listed-vault-inside-vault'
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const noEvidence = (reason, extra = {}) => ({ unlisted: false, reason, ...extra })
|
|
45
|
+
|
|
46
|
+
// `read` answers readObsidianSettings for the app's user-data directory; `sandboxed` answers whether a Flatpak or snap
|
|
47
|
+
// build is present. Either one that throws gives no evidence.
|
|
48
|
+
export async function readUnheldEvidence({ vaultRoot, read, sandboxed, attempts = 3, delayMs = 50, sleep = delay } = {}) {
|
|
49
|
+
if (typeof vaultRoot !== 'string' || !path.isAbsolute(vaultRoot)) return noEvidence('vault-root-unknown')
|
|
50
|
+
if (typeof read !== 'function' || typeof sandboxed !== 'function') return noEvidence('obsidian-settings-location-unknown')
|
|
51
|
+
let present = true
|
|
52
|
+
try { present = sandboxed() !== false } catch { present = true }
|
|
53
|
+
if (present) return noEvidence('obsidian-sandboxed')
|
|
54
|
+
let answer = null
|
|
55
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
56
|
+
try { answer = read() } catch { answer = { ok: false, code: 'obsidian-settings-unreadable' } }
|
|
57
|
+
if (answer?.ok === true || answer?.code !== 'obsidian-settings-unreadable') break
|
|
58
|
+
if (attempt < attempts) await sleep(delayMs)
|
|
59
|
+
}
|
|
60
|
+
if (answer?.ok !== true) return noEvidence(typeof answer?.code === 'string' ? answer.code : 'obsidian-settings-unreadable')
|
|
61
|
+
const vaults = answer.vaults ?? {}
|
|
62
|
+
if (!isPlainObject(vaults)) return noEvidence('obsidian-settings-not-object')
|
|
63
|
+
const root = spellings(vaultRoot)
|
|
64
|
+
for (const [id, entry] of Object.entries(vaults)) {
|
|
65
|
+
if (!isPlainObject(entry) || typeof entry.path !== 'string' || !path.isAbsolute(entry.path) || entry.path.split(/[\\/]/).includes('..')) return noEvidence('obsidian-settings-entry-unreadable', { id })
|
|
66
|
+
const relation = relationOf(entry.path, root)
|
|
67
|
+
if (relation !== null) return noEvidence(relation, { id, path: entry.path })
|
|
68
|
+
}
|
|
69
|
+
return { unlisted: true }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The reader the maintenance service gives its engine: the settings file of the app for this account (HOME, and on
|
|
73
|
+
// Linux XDG_CONFIG_HOME), and any Flatpak or snap build, installed or having left its list, whichever wrote last.
|
|
74
|
+
// `exists` answers whether a path exists: the build's system-wide places (/var/lib/flatpak, /snap) are looked up through
|
|
75
|
+
// it too.
|
|
76
|
+
export function createProductionUnheldEvidence({ platform = process.platform, env = process.env, exists = fs.existsSync } = {}) {
|
|
77
|
+
const userDataDir = obsidianUserDataDir({ platform, env })
|
|
78
|
+
return ({ vaultRoot }) => readUnheldEvidence({
|
|
79
|
+
vaultRoot,
|
|
80
|
+
read: () => (userDataDir === null ? { ok: false, code: 'obsidian-settings-location-unknown' } : readObsidianSettings({ userDataDir })),
|
|
81
|
+
// Present at all, not only when its list was written last: with no list time given, any installation or list counts.
|
|
82
|
+
sandboxed: () => obsidianSandboxedBuild({ platform, env, exists, modified: () => null }) !== null,
|
|
83
|
+
})
|
|
84
|
+
}
|