@mnstry/atelier 0.2.0-alpha.11 → 0.2.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1005 -0
- package/README.md +149 -9
- package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
- package/contracts/atelier-architecture.v1.schema.json +258 -0
- package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
- package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
- package/contracts/atelier-build.v1.schema.json +1076 -0
- package/contracts/atelier-capability.v1.schema.json +1252 -0
- package/contracts/atelier-coordination.v1.schema.json +1050 -0
- package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
- package/contracts/atelier-harness.v1.schema.json +234 -0
- package/contracts/atelier-ingestion.v1.schema.json +806 -0
- package/contracts/atelier-inquiry.v1.schema.json +1772 -0
- package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
- package/contracts/atelier-interaction.v1.schema.json +553 -0
- package/contracts/atelier-judgment.v1.schema.json +690 -0
- package/contracts/atelier-knowledge.v1.schema.json +1512 -0
- package/contracts/atelier-learning.v1.schema.json +1095 -0
- package/contracts/atelier-migration.v3.schema.json +64 -0
- package/contracts/atelier-obsidian-generation-manifest.v2.schema.json +372 -0
- package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
- package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
- package/contracts/atelier-template-adoption.v1.schema.json +223 -0
- package/contracts/atelier-template-profile.v1.schema.json +1168 -0
- package/contracts/atelier-trackable.v1.schema.json +781 -0
- package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
- package/contracts/compat-baseline.json +1 -1
- package/docs/architecture.md +89 -0
- package/docs/assurance-controls.md +4 -3
- package/docs/blocks/capability-stewardship.md +17 -0
- package/docs/blocks/claims.md +16 -3
- package/docs/blocks/inquiry-harnesses.md +19 -0
- package/docs/blocks/learning-harnesses.md +17 -0
- package/docs/capability-stewardship.md +269 -0
- package/docs/coordination.md +51 -0
- package/docs/decision-practices.md +111 -0
- package/docs/decisions.md +29 -0
- package/docs/evidence-navigation.md +237 -0
- package/docs/exact-upgrades.md +3 -2
- package/docs/ingestion.md +181 -0
- package/docs/inquiry-harnesses.md +211 -0
- package/docs/install.md +80 -3
- package/docs/intake-and-guides.md +5 -0
- package/docs/integration-contract-decisions.md +24 -2
- package/docs/interaction-and-reflection.md +72 -0
- package/docs/knowledge-graph.md +20 -0
- package/docs/knowledge-health/api.md +30 -0
- package/docs/knowledge-health/assessment/api.md +96 -0
- package/docs/knowledge-health/assessment/capabilities.md +40 -0
- package/docs/knowledge-health/assessment/evaluation.md +27 -0
- package/docs/knowledge-health/assessment/facilitator.md +30 -0
- package/docs/knowledge-health/assessment/quickstart.md +61 -0
- package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
- package/docs/knowledge-health/devday/facilitator.md +16 -0
- package/docs/knowledge-health/devday/participant-response.md +22 -0
- package/docs/knowledge-health/devday/public-default.md +107 -0
- package/docs/knowledge-health/devday/quickstart.md +18 -0
- package/docs/knowledge-health/devday/support.md +22 -0
- package/docs/knowledge-health/exercise.md +17 -0
- package/docs/knowledge-health/facilitator.md +20 -0
- package/docs/knowledge-health/quickstart.md +94 -0
- package/docs/knowledge-health/readme.md +15 -0
- package/docs/knowledge-health/support.md +34 -0
- package/docs/knowledge-setup.md +248 -0
- package/docs/knowledge-workspace.md +264 -0
- package/docs/learning-harnesses.md +238 -0
- package/docs/learning.md +215 -0
- package/docs/local-services.md +242 -26
- package/docs/obsidian-contract.md +878 -36
- package/docs/obsidian-plugin.md +619 -0
- package/docs/obsidian.md +809 -64
- package/docs/optional-ingestion.md +83 -0
- package/docs/personal-workspace.md +257 -0
- package/docs/practical-judgment.md +92 -0
- package/docs/progressive-ingestion-acceptance.md +89 -0
- package/docs/progressive-ingestion.md +235 -0
- package/docs/project-command-inventory.md +1 -0
- package/docs/release-engineering.md +56 -1
- package/docs/repo-boundary-guard.md +94 -0
- package/docs/research-starter-quickstart.md +66 -0
- package/docs/research-starter.md +114 -0
- package/docs/semantic-ingestion.md +198 -0
- package/docs/semantic-operation.md +317 -0
- package/docs/skill-steward.md +6 -0
- package/docs/template-profile.md +164 -0
- package/docs/template-upgrades.md +114 -0
- package/docs/trackables.md +111 -0
- package/docs/upgrade.md +68 -3
- package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
- package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
- package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
- package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
- package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
- package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
- package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
- package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
- package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
- package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
- package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
- package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
- package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
- package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
- package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
- package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
- package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
- package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
- package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
- package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
- package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
- package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
- package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
- package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
- package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
- package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
- package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
- package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
- package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
- package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
- package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
- package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
- package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
- package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
- package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
- package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
- package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
- package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
- package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
- package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
- package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
- package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
- package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
- package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
- package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
- package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
- package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
- package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
- package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
- package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
- package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
- package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
- package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
- package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
- package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
- package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
- package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
- package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
- package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
- package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
- package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
- package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
- package/fixtures/atelier-interaction/act/valid/document.json +24 -0
- package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
- package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
- package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
- package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
- package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
- package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
- package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
- package/fixtures/atelier-judgment/case/valid/document.json +75 -0
- package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
- package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
- package/fixtures/atelier-judgment/native-decision.md +3 -0
- package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
- package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
- package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
- package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
- package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
- package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
- package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
- package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
- package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
- package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
- package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
- package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
- package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
- package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-learning/invalid/authority.json +14 -0
- package/fixtures/atelier-learning/valid/capture.json +13 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
- package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
- package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
- package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
- package/fixtures/atelier-trackable/request/valid/document.json +22 -0
- package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
- package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
- package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
- package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
- package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
- package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
- package/fixtures/capability-packages/adoption.example.json +70 -0
- package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
- package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
- package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
- package/fixtures/capability-packages/research/capability-package.json +88 -0
- package/fixtures/capability-packages/research/capability-release.json +134 -0
- package/fixtures/capability-packages/research/evaluation.md +15 -0
- package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
- package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
- package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
- package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
- package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
- package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
- package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
- package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
- package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
- package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
- package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
- package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
- package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
- package/fixtures/evidence-navigation/valid/claim.json +36 -0
- package/fixtures/evidence-navigation/valid/host.json +16 -0
- package/fixtures/evidence-navigation/valid/profile.json +16 -0
- package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
- package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
- package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
- package/fixtures/harnesses/learning-cycle.json +780 -0
- package/fixtures/inquiry/workshop.json +675 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
- package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
- package/fixtures/knowledge-health/calibration-suite.json +26 -0
- package/fixtures/knowledge-health/calibration-trial.json +37 -0
- package/fixtures/knowledge-health/corrected/checklist.md +18 -0
- package/fixtures/knowledge-health/devday/participant-record.json +19 -0
- package/fixtures/knowledge-health/expected-summary.json +27 -0
- package/fixtures/knowledge-health/workshop/README.md +20 -0
- package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
- package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
- package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
- package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
- package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
- package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
- package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
- package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/case-only-collision.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/flat-note-path.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/hidden-folder.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/identity-in-the-body.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/link-breaking-name.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-identity-region.v2.json +156 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-layout-version.v2.json +159 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/note-named-like-a-folder.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/unknown-region-kind.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/v1-contract-version.v2.json +161 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/valid/three-notes.v2.json +160 -0
- package/fixtures/obsidian/edits/workspace.json +1 -1
- package/fixtures/obsidian/materialization/expected-full.json +21 -21
- package/fixtures/obsidian/materialization/expected-full.layout-1.json +99 -0
- package/fixtures/obsidian/materialization/expected-scoped.json +7 -7
- package/fixtures/obsidian/materialization/expected-scoped.layout-1.json +39 -0
- package/fixtures/personal-workspace/overlay.json +24 -0
- package/fixtures/templates/local-library.v1.json +75 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
- package/package.json +64 -6
- package/plugins/obsidian/main.js +444 -0
- package/plugins/obsidian/manifest.json +9 -0
- package/plugins/obsidian/styles.css +22 -0
- package/skills/claude/atelier-build-harness/SKILL.md +59 -0
- package/skills/claude/atelier-coordination/SKILL.md +32 -0
- package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/claude/atelier-interaction/SKILL.md +34 -0
- package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/claude/atelier-research-harness/SKILL.md +43 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
- package/skills/claude/atelier-trackables/SKILL.md +40 -0
- package/skills/codex/atelier-build-harness/SKILL.md +59 -0
- package/skills/codex/atelier-coordination/SKILL.md +32 -0
- package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/codex/atelier-interaction/SKILL.md +34 -0
- package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/codex/atelier-research-harness/SKILL.md +43 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
- package/skills/codex/atelier-trackables/SKILL.md +40 -0
- package/src/architecture/catalog.mjs +355 -0
- package/src/architecture/index.mjs +93 -0
- package/src/boundary/content-rules.mjs +4 -2
- package/src/boundary/delegation-command.mjs +158 -0
- package/src/boundary/delegation.mjs +396 -0
- package/src/boundary/policy.mjs +52 -10
- package/src/build/git.mjs +18 -0
- package/src/build/index.mjs +3 -0
- package/src/build/ledger.mjs +88 -0
- package/src/capabilities/adoption.mjs +381 -0
- package/src/capabilities/evidence.mjs +140 -0
- package/src/capabilities/files.mjs +124 -0
- package/src/capabilities/index.mjs +4 -0
- package/src/capabilities/instructions.mjs +250 -0
- package/src/capabilities/package.mjs +136 -0
- package/src/cli/command-failure.mjs +36 -0
- package/src/cli/execute-command.mjs +16 -2
- package/src/cli/run.mjs +65 -9
- package/src/coauthor/store.mjs +43 -2
- package/src/collaboration/proposals.mjs +189 -0
- package/src/commands/architecture.mjs +27 -0
- package/src/commands/boundary.mjs +3 -0
- package/src/commands/capability.mjs +73 -0
- package/src/commands/enroll.mjs +3 -0
- package/src/commands/harness.mjs +79 -0
- package/src/commands/ingest.mjs +39 -0
- package/src/commands/init.mjs +4 -1
- package/src/commands/inquiry.mjs +66 -0
- package/src/commands/knowledge-semantic.mjs +128 -0
- package/src/commands/knowledge.mjs +119 -0
- package/src/commands/learn.mjs +61 -0
- package/src/commands/obsidian.mjs +832 -62
- package/src/commands/practice.mjs +24 -0
- package/src/commands/setup.mjs +22 -2
- package/src/commands/trackable.mjs +19 -0
- package/src/contracts/corpus.mjs +36 -0
- package/src/coordination/index.mjs +110 -0
- package/src/egress/forbidden-egress.mjs +2 -0
- package/src/evidence-navigation/contracts.mjs +60 -0
- package/src/evidence-navigation/evaluate.mjs +69 -0
- package/src/evidence-navigation/generation.mjs +22 -0
- package/src/evidence-navigation/index.mjs +2 -0
- package/src/evidence-navigation/local.mjs +167 -0
- package/src/evidence-navigation/schema.generated.mjs +604 -0
- package/src/evidence-navigation/types.mjs +12 -0
- package/src/export/atelier-export-contract.mjs +35 -8
- package/src/graph/enroll.mjs +248 -0
- package/src/graph/graph.mjs +17 -1
- package/src/graph/knowledge-graph.mjs +58 -8
- package/src/harnesses/contracts.mjs +41 -0
- package/src/harnesses/exchange.mjs +154 -0
- package/src/harnesses/feedback.mjs +16 -0
- package/src/harnesses/history.mjs +37 -0
- package/src/harnesses/index.mjs +4 -0
- package/src/harnesses/store.mjs +29 -0
- package/src/ingestion/contracts.mjs +41 -0
- package/src/ingestion/evaluation.mjs +119 -0
- package/src/ingestion/optional/composition/index.mjs +171 -0
- package/src/ingestion/optional/evaluation/index.mjs +219 -0
- package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
- package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
- package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
- package/src/ingestion/optional/graphify/json_bridge.py +163 -0
- package/src/ingestion/optional/graphify/query_receiving.py +209 -0
- package/src/ingestion/optional/index.mjs +9 -0
- package/src/ingestion/optional/jev/bridge.mjs +166 -0
- package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
- package/src/ingestion/optional/jev/judgments.mjs +174 -0
- package/src/ingestion/optional/jev/native-translation.mjs +32 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
- package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
- package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
- package/src/ingestion/optional/vanilla/index.mjs +218 -0
- package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
- package/src/ingestion/processors.mjs +218 -0
- package/src/ingestion/semantic.mjs +203 -0
- package/src/ingestion/store.mjs +372 -0
- package/src/inquiry/beliefs.mjs +32 -0
- package/src/inquiry/contracts.mjs +23 -0
- package/src/inquiry/index.mjs +5 -0
- package/src/inquiry/ledger.mjs +135 -0
- package/src/inquiry/lenses.mjs +15 -0
- package/src/inquiry/projection.mjs +77 -0
- package/src/inquiry/store.mjs +35 -0
- package/src/intake/read-scope.mjs +12 -0
- package/src/intake/store.mjs +69 -6
- package/src/interaction/index.mjs +91 -0
- package/src/judgment/index.mjs +82 -0
- package/src/judgment/practice-evaluation.mjs +70 -0
- package/src/judgment/practice.mjs +144 -0
- package/src/knowledge/context.mjs +217 -0
- package/src/knowledge/index.mjs +5 -0
- package/src/knowledge/ingestion.mjs +79 -0
- package/src/knowledge/intake.mjs +33 -0
- package/src/knowledge/ledger.mjs +110 -0
- package/src/knowledge/participatory/guidance.mjs +36 -0
- package/src/knowledge/participatory/guided-contribution.mjs +44 -0
- package/src/knowledge/participatory/index.mjs +2 -0
- package/src/knowledge/participatory/installed-workshop.mjs +108 -0
- package/src/knowledge/plan.mjs +67 -0
- package/src/knowledge/projection.mjs +43 -0
- package/src/knowledge/semantic-operation.mjs +567 -0
- package/src/knowledge/sessions.mjs +467 -0
- package/src/knowledge/workspace.mjs +295 -0
- package/src/knowledge-health/assessment.mjs +165 -0
- package/src/knowledge-health/capabilities.mjs +350 -0
- package/src/knowledge-health/context.mjs +62 -0
- package/src/knowledge-health/example.mjs +47 -0
- package/src/knowledge-health/index.mjs +10 -0
- package/src/knowledge-health/owner-join.mjs +88 -0
- package/src/knowledge-health/proof/common.mjs +24 -0
- package/src/knowledge-health/proof/installed-check.mjs +100 -0
- package/src/knowledge-health/proof/native-readback.mjs +37 -0
- package/src/knowledge-health/proof/rehearse.mjs +68 -0
- package/src/knowledge-health/proof/validate-participant.mjs +17 -0
- package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
- package/src/knowledge-health/proof/workshop-check.mjs +36 -0
- package/src/knowledge-health/rule-identity.mjs +44 -0
- package/src/knowledge-health/workshop-composition.mjs +19 -0
- package/src/knowledge-health/workshop-profile.mjs +176 -0
- package/src/knowledge-health/workshop.mjs +99 -0
- package/src/learning/adapters.mjs +68 -0
- package/src/learning/assistance.mjs +127 -0
- package/src/learning/contracts.mjs +65 -0
- package/src/learning/store.mjs +272 -0
- package/src/personal-workspace/index.mjs +391 -0
- package/src/personal-workspace/selection.mjs +385 -0
- package/src/project/config.mjs +8 -0
- package/src/project/durable-state.mjs +3 -1
- package/src/project/file-class.mjs +1 -1
- package/src/project/private-state.mjs +10 -1
- package/src/projection/obsidian/contracts.mjs +224 -18
- package/src/projection/obsidian/edits/apply.mjs +63 -19
- package/src/projection/obsidian/edits/regions.mjs +137 -42
- package/src/projection/obsidian/materialize/index.mjs +6 -3
- package/src/projection/obsidian/materialize/path-registry.mjs +443 -57
- package/src/projection/obsidian/materialize/prepare-view.mjs +382 -131
- package/src/projection/obsidian/materialize/redaction.mjs +327 -0
- package/src/projection/obsidian/materialize/settings.mjs +113 -34
- package/src/projection/obsidian/personal-workspace.mjs +436 -0
- package/src/projection/obsidian/plugin-bridge/bundle.mjs +50 -0
- package/src/projection/obsidian/plugin-bridge/channel.mjs +174 -0
- package/src/projection/obsidian/plugin-bridge/index.mjs +10 -0
- package/src/projection/obsidian/proposals/adapter.mjs +47 -8
- package/src/projection/obsidian/proposals/observation.mjs +20 -7
- package/src/projection/obsidian/publication/bridge-script.mjs +11 -5
- package/src/projection/obsidian/publication/index.mjs +2 -1
- package/src/projection/obsidian/publication/publisher.mjs +312 -36
- package/src/projection/obsidian/publication/transport.mjs +63 -8
- package/src/projection/obsidian/publication/vault-list.mjs +200 -0
- package/src/projection/obsidian/recovery/index.mjs +4 -1
- package/src/projection/obsidian/recovery/restart.mjs +3 -1
- package/src/projection/obsidian/recovery/store.mjs +168 -8
- package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
- package/src/projection/obsidian/selection-ui/focus.mjs +14 -7
- package/src/projection/obsidian/selection-ui/selection-state.mjs +2 -2
- package/src/projection/obsidian/selection-ui/selection.mjs +8 -3
- package/src/readiness/readiness.mjs +10 -3
- package/src/reflection/index.mjs +56 -0
- package/src/runtime/obsidian/app-capability.mjs +104 -12
- package/src/runtime/obsidian/app-production-seams.mjs +161 -42
- package/src/runtime/obsidian/app-registration.mjs +313 -0
- package/src/runtime/obsidian/app-restart.mjs +154 -0
- package/src/runtime/obsidian/engine.mjs +288 -29
- package/src/runtime/obsidian/extension-points.mjs +1 -1
- package/src/runtime/obsidian/index.mjs +15 -8
- package/src/runtime/obsidian/launch-plan.mjs +91 -0
- package/src/runtime/obsidian/lifecycle.mjs +193 -22
- package/src/runtime/obsidian/login-item.mjs +332 -0
- package/src/runtime/obsidian/machine-settings.mjs +119 -12
- package/src/runtime/obsidian/opening.mjs +369 -63
- package/src/runtime/obsidian/pending-edits.mjs +10 -0
- package/src/runtime/obsidian/pipeline.mjs +71 -3
- package/src/runtime/obsidian/plugin-channel.mjs +289 -0
- package/src/runtime/obsidian/plugin-choice.mjs +182 -0
- package/src/runtime/obsidian/plugin-drift.mjs +124 -0
- package/src/runtime/obsidian/plugin-presence.mjs +61 -0
- package/src/runtime/obsidian/process-identity.mjs +20 -10
- package/src/runtime/obsidian/project-views.mjs +282 -0
- package/src/runtime/obsidian/questions.mjs +48 -0
- package/src/runtime/obsidian/release-watch.mjs +56 -0
- package/src/runtime/obsidian/service-main.mjs +113 -17
- package/src/runtime/obsidian/service-manager-production.mjs +61 -0
- package/src/runtime/obsidian/service-managers.mjs +275 -0
- package/src/runtime/obsidian/service-record.mjs +105 -5
- package/src/runtime/obsidian/service-server.mjs +50 -12
- package/src/runtime/obsidian/service.mjs +134 -7
- package/src/runtime/obsidian/startup-adapters.mjs +59 -21
- package/src/runtime/obsidian/state-store.mjs +13 -1
- package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
- package/src/runtime/obsidian/vault-location.mjs +300 -0
- package/src/runtime/obsidian/view-counts.mjs +34 -0
- package/src/server/local-sidecar.mjs +193 -7
- package/src/server/server.mjs +45 -6
- package/src/template-bindings/project.mjs +92 -0
- package/src/templates/profile.mjs +225 -0
- package/src/trackables/domain.mjs +175 -0
- package/src/trackables/store.mjs +114 -0
- package/src/ui/knowledge-page.mjs +1388 -0
- package/src/upgrade/explanation.mjs +1 -1
- package/src/upgrade/template-participant.mjs +171 -0
- package/src/upgrade/transaction.mjs +72 -25
- package/src/upgrade/upgrade.mjs +2 -1
- package/templates/external-project-workspace/pack-lifecycle.json +6 -1
- package/templates/knowledge-workspace/.gitattributes +2 -0
- package/templates/knowledge-workspace/AGENTS.md +27 -0
- package/templates/knowledge-workspace/README.md +44 -0
- package/templates/knowledge-workspace/atelier.project.json +22 -0
- package/templates/knowledge-workspace/gitignore +6 -0
- package/templates/knowledge-workspace/knowledge-plan.json +111 -0
- package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
- package/templates/knowledge-workspace/records/forecast.md +22 -0
- package/templates/knowledge-workspace/records/inspection.md +25 -0
- package/templates/knowledge-workspace/records/telescope.md +24 -0
- package/templates/knowledge-workspace/repo-access.v1.json +9 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { atomicReplacePrivateText, ensureContainedPrivateDirectory, readRegularTextNoFollow } from '../../project/private-state.mjs'
|
|
6
|
+
import { canonicalJson, closedObject, isoTime } from './documents.mjs'
|
|
7
|
+
import { refuse } from './errors.mjs'
|
|
8
|
+
import { ensureServiceSettings } from './lifecycle.mjs'
|
|
9
|
+
import { ensureWorkspaceIdentity, readLocalPointer, resolveDataRoot } from './machine-settings.mjs'
|
|
10
|
+
import { SERVICE_MANAGER_KINDS } from './service-managers.mjs'
|
|
11
|
+
import { readLastStartup, readServiceSettings, serviceNameFor, servicePaths, writeServiceSettings } from './service-record.mjs'
|
|
12
|
+
import { resolveServiceWorkspace } from './service.mjs'
|
|
13
|
+
import { STARTUP_PLATFORMS, buildStartupAdapter, startupSearchPath } from './startup-adapters.mjs'
|
|
14
|
+
|
|
15
|
+
// A login item: the operating system starts this workspace's maintenance
|
|
16
|
+
// service when the person logs in, so a vault is fresh after a restart
|
|
17
|
+
// without anything being run.
|
|
18
|
+
//
|
|
19
|
+
// macOS a launchd user agent: ~/Library/LaunchAgents/ai.mnstry.atelier.<project>.<workspace-id>.plist
|
|
20
|
+
// Linux a systemd user unit: ~/.config/systemd/user/atelier-obsidian-<workspace-id>.service
|
|
21
|
+
// Windows not offered (startup-platform-unqualified)
|
|
22
|
+
//
|
|
23
|
+
// One per workspace, each with its own label, port, log and consent, each
|
|
24
|
+
// running its own project's installed release. The unit runs the service
|
|
25
|
+
// entry of the package installed in the project, named by its path, so an
|
|
26
|
+
// upgrade in the project is what the next start runs; the Node that ran the
|
|
27
|
+
// installing command, by its real path, so a per-shell link of a version
|
|
28
|
+
// manager is never named; the search path the person had then, absolute
|
|
29
|
+
// entries only; the data root the workspace was resolved under and the
|
|
30
|
+
// workspace, so the service at login finds the same workspace whatever its
|
|
31
|
+
// manager's environment carries (XDG_DATA_HOME, the overlay's variable), and
|
|
32
|
+
// records a refusal there even when the project no longer leads to it; and
|
|
33
|
+
// `--startup`, under which the service refuses to run without a consent that
|
|
34
|
+
// covers startup.
|
|
35
|
+
//
|
|
36
|
+
// Installing records that consent first, since the manager starts the
|
|
37
|
+
// service as soon as it loads the unit, then hands the text to the injected
|
|
38
|
+
// service manager (service-managers.mjs) and keeps
|
|
39
|
+
//
|
|
40
|
+
// state/service/login-item.json atelier-obsidian-login-item/v1
|
|
41
|
+
//
|
|
42
|
+
// { label, file, digest of the text, program: { node, entry }, searchPath }.
|
|
43
|
+
// The label is allocated once and kept. Once installed, the service is
|
|
44
|
+
// started through the manager (loginItemStarter), never as a child beside
|
|
45
|
+
// it; a unit that differs from what would be written now (a Node that was
|
|
46
|
+
// removed, another entry, an earlier release's format) is written again on
|
|
47
|
+
// that way and reloaded, keeping the search path recorded at installation.
|
|
48
|
+
// Removing lowers the consent to the service alone first, so a unit left
|
|
49
|
+
// behind by a failed removal could only refuse, then removes the unit and
|
|
50
|
+
// the record. A person who switched the item off (System Settings, or
|
|
51
|
+
// `systemctl --user disable`) is not overridden: the manager is asked first,
|
|
52
|
+
// the unit is neither written again nor started through it, and the service
|
|
53
|
+
// is started as a child for that command only.
|
|
54
|
+
|
|
55
|
+
export const LOGIN_ITEM_SCHEMA = 'atelier-obsidian-login-item/v1'
|
|
56
|
+
export const LOGIN_ITEM_PACKAGE = '@mnstry/atelier'
|
|
57
|
+
const ENTRY_IN_PACKAGE = Object.freeze(['src', 'runtime', 'obsidian', 'service-main.mjs'])
|
|
58
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
59
|
+
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
|
60
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/
|
|
61
|
+
const CONTROL = /[\u0000-\u001f\u007f]/
|
|
62
|
+
// A package runner's cache: npx, pnpm dlx, bunx. A package there can be removed by the runner at any time.
|
|
63
|
+
const RUNNER_CACHES = Object.freeze([/[\\/]_npx[\\/]/, /[\\/]pnpm[\\/]dlx[\\/]/, /[\\/]\.bun[\\/]install[\\/]cache[\\/]/])
|
|
64
|
+
|
|
65
|
+
const sha256 = (text) => `sha256:${createHash('sha256').update(text).digest('hex')}`
|
|
66
|
+
const isAbsolutePlain = (value) => typeof value === 'string' && path.isAbsolute(value) && !CONTROL.test(value)
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// The record
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
function validateLoginItem(document, workspaceId) {
|
|
73
|
+
const code = 'invalid-login-item'
|
|
74
|
+
closedObject(document, { required: ['schema', 'workspaceId', 'kind', 'label', 'file', 'digest', 'program', 'searchPath', 'installedAt', 'updatedAt'] }, code, 'the login item record')
|
|
75
|
+
closedObject(document.program, { required: ['node', 'entry'] }, code, 'the program of the login item')
|
|
76
|
+
const ok = document.schema === LOGIN_ITEM_SCHEMA && document.workspaceId === workspaceId && SERVICE_MANAGER_KINDS.includes(document.kind)
|
|
77
|
+
&& typeof document.label === 'string' && IDENTIFIER.test(document.label) && isAbsolutePlain(document.file) && path.basename(document.file).startsWith(document.label)
|
|
78
|
+
&& typeof document.digest === 'string' && DIGEST.test(document.digest) && isAbsolutePlain(document.program.node) && isAbsolutePlain(document.program.entry)
|
|
79
|
+
&& (document.searchPath === null || (typeof document.searchPath === 'string' && document.searchPath.split(':').every(isAbsolutePlain)))
|
|
80
|
+
&& TIMESTAMP.test(document.installedAt) && TIMESTAMP.test(document.updatedAt)
|
|
81
|
+
if (!ok) refuse(code, 'the login item record is malformed')
|
|
82
|
+
return document
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function readLoginItemRecord({ workspaceRoot, workspaceId }) {
|
|
86
|
+
let text
|
|
87
|
+
try { text = readRegularTextNoFollow(servicePaths(workspaceRoot).loginItem) } catch (error) {
|
|
88
|
+
if (error.code === 'ENOENT') return null
|
|
89
|
+
return refuse('invalid-login-item', 'the login item record cannot be read', { cause: error.code ?? 'unreadable' })
|
|
90
|
+
}
|
|
91
|
+
let document
|
|
92
|
+
try { document = JSON.parse(text) } catch { return refuse('invalid-login-item', 'the login item record is not JSON') }
|
|
93
|
+
return validateLoginItem(document, workspaceId)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writeLoginItemRecord({ workspaceRoot, workspaceId, record }) {
|
|
97
|
+
validateLoginItem(record, workspaceId)
|
|
98
|
+
const directory = ensureContainedPrivateDirectory({ workspaceRoot, directory: servicePaths(workspaceRoot).directory, label: 'Obsidian service state' })
|
|
99
|
+
atomicReplacePrivateText(path.join(directory, 'login-item.json'), canonicalJson(record))
|
|
100
|
+
return record
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function removeLoginItemRecord({ workspaceRoot }) {
|
|
104
|
+
try { fs.unlinkSync(servicePaths(workspaceRoot).loginItem); return true } catch (error) { if (error.code === 'ENOENT') return false; throw error }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// What a unit is made of
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
// The part of a launchd label that stands for the project: lower-case letters, digits and dashes, at most 40, from
|
|
112
|
+
// its name, so a person who looks in ~/Library/LaunchAgents can tell whose it is. Pure.
|
|
113
|
+
export function labelSlug(name) {
|
|
114
|
+
const slug = String(name ?? '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40).replace(/-+$/, '')
|
|
115
|
+
return slug === '' ? 'project' : slug
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The label of this workspace's login item, as it is allocated the first time. Pure.
|
|
119
|
+
export function loginItemLabel({ platform, projectName, workspaceId }) {
|
|
120
|
+
const id = String(workspaceId).replaceAll(':', '_')
|
|
121
|
+
return platform === 'darwin' ? `ai.mnstry.atelier.${labelSlug(projectName)}.${id}` : serviceNameFor(workspaceId).replaceAll(':', '_')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The name people read for a project: its configured `name`, else the name of its folder.
|
|
125
|
+
export const projectNameOf = (project) => (typeof project?.config?.name === 'string' && project.config.name.trim() !== '' ? project.config.name : path.basename(project?.configDir ?? 'project'))
|
|
126
|
+
|
|
127
|
+
// Absolute folders whose contents do not outlive a restart, or not for long.
|
|
128
|
+
export function temporaryRoots({ tmpdir = os.tmpdir() } = {}) {
|
|
129
|
+
const roots = new Set(['/tmp', '/private/tmp', '/var/tmp', '/private/var/tmp'])
|
|
130
|
+
if (isAbsolutePlain(tmpdir)) { roots.add(path.resolve(tmpdir)); try { roots.add(fs.realpathSync(tmpdir)) } catch { /* only as given */ } }
|
|
131
|
+
return [...roots]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const inside = (file, root) => file === root || file.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`)
|
|
135
|
+
const isFile = (file) => { try { return fs.statSync(file).isFile() } catch { return false } }
|
|
136
|
+
|
|
137
|
+
// The entry a login item runs: the service entry of the package installed for this project, found as Node finds a
|
|
138
|
+
// package from the project's folder (in its node_modules or a folder's above), named by that path and not its real
|
|
139
|
+
// path, so an upgrade in place is what the next start runs; else `ownEntry`, the entry of the package this command
|
|
140
|
+
// runs from, unless that one is in a package runner's cache (npx) or a temporary folder, which are gone after a
|
|
141
|
+
// restart (`login-item-needs-installed-package`). { entryPath, source: 'project' | 'command' }.
|
|
142
|
+
export function resolveLoginItemEntry({ project, ownEntry, temporary = temporaryRoots(), exists = isFile } = {}) {
|
|
143
|
+
if (typeof project?.configDir === 'string' && path.isAbsolute(project.configDir)) {
|
|
144
|
+
for (let directory = path.resolve(project.configDir); ; directory = path.dirname(directory)) {
|
|
145
|
+
const candidate = path.join(directory, 'node_modules', ...LOGIN_ITEM_PACKAGE.split('/'), ...ENTRY_IN_PACKAGE)
|
|
146
|
+
if (exists(candidate)) return { entryPath: candidate, source: 'project' }
|
|
147
|
+
if (path.dirname(directory) === directory) break
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const transient = typeof ownEntry !== 'string' || !path.isAbsolute(ownEntry) || RUNNER_CACHES.some((pattern) => pattern.test(ownEntry)) || temporary.some((root) => inside(path.resolve(ownEntry), root))
|
|
151
|
+
if (transient) refuse('login-item-needs-installed-package', 'a login item runs the package installed in the project, and this project has none; the command runs from a package runner\'s cache or a temporary folder, which may be gone after a restart')
|
|
152
|
+
return { entryPath: ownEntry, source: 'command' }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// The Node a unit names: the one running this command, by its real path.
|
|
156
|
+
export const realNodePath = (execPath = process.execPath) => { try { return fs.realpathSync(execPath) } catch { return execPath } }
|
|
157
|
+
|
|
158
|
+
// The data root this workspace is resolved under for this command (the flag, the pointer, the overlay's preference or
|
|
159
|
+
// the platform default), which a unit names.
|
|
160
|
+
export const loginItemDataRoot = ({ project, dataRoot, env = process.env, platform = process.platform }) => resolveDataRoot({ dataRoot, pointer: readLocalPointer(project), project, env, platform })
|
|
161
|
+
|
|
162
|
+
// The unit this workspace's login item would have now. `dataRoot` is the resolved one (loginItemDataRoot), always named;
|
|
163
|
+
// `entryArgs` follow `--project`, `--data-root` and `--workspace-id`. Pure but for the builder's refusals.
|
|
164
|
+
export function planLoginItem({ platform, project, workspaceRoot, workspaceId, dataRoot, label, entryPath, entryArgs = [], nodePath, searchPath = null }) {
|
|
165
|
+
if (!isAbsolutePlain(dataRoot)) throw new TypeError('a login item names the absolute data root of its workspace')
|
|
166
|
+
if (typeof workspaceId !== 'string' || workspaceId === '') throw new TypeError('a login item names its workspace')
|
|
167
|
+
const args = [`--project=${project.configPath}`, `--data-root=${dataRoot}`, `--workspace-id=${workspaceId}`, ...entryArgs]
|
|
168
|
+
const unit = buildStartupAdapter({ platform, label, nodePath, entryPath, args, logPath: servicePaths(workspaceRoot).loginItemLog, searchPath })
|
|
169
|
+
return { ...unit, label, digest: sha256(unit.text), program: { node: nodePath, entry: entryPath }, searchPath }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function checkManager(manager, platform) {
|
|
173
|
+
if (!STARTUP_PLATFORMS.includes(platform)) buildStartupAdapter({ platform })
|
|
174
|
+
const operations = ['readUnit', 'install', 'start', 'remove', 'inspect']
|
|
175
|
+
if (manager === null || typeof manager !== 'object' || !operations.every((name) => typeof manager[name] === 'function') || !SERVICE_MANAGER_KINDS.includes(manager.kind)) {
|
|
176
|
+
throw new TypeError('a login item needs an injected service manager')
|
|
177
|
+
}
|
|
178
|
+
if (manager.platform !== platform) throw new TypeError('the service manager is not this platform\'s')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Install, remove, start through it, and what it looks like
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
// Installs, or installs again, this workspace's login item. `consent` ({ actor }) is the person's consent to the
|
|
186
|
+
// service running at login; without it, a recorded consent must already cover startup (`startup-consent-required`).
|
|
187
|
+
// `ownEntry` and `entryArgs` are the entry this command would start and its arguments (`--adapter=...`).
|
|
188
|
+
// `beforeInstall({ entryPath })`, when given, runs once the consent is recorded and before the manager loads the unit,
|
|
189
|
+
// which starts the service at once: the command stops a service of an earlier release there, so that start runs the
|
|
190
|
+
// entry the unit names instead of finding the earlier one running.
|
|
191
|
+
// { installed: true, label, file, kind, entry: { path, source }, node, searchPath } or, when the manager did not take
|
|
192
|
+
// the unit, { installed: false, reason, label } with the consent recorded before, or, when none was, the one just
|
|
193
|
+
// recorded covering the service alone.
|
|
194
|
+
export async function installLoginItem(options = {}) {
|
|
195
|
+
const { loadProject, dataRoot, env = process.env, platform = process.platform, manager, consent, ownEntry, entryArgs = [], nodePath = realNodePath(), pathValue = env.PATH, clock = () => new Date(), temporary = temporaryRoots(), beforeInstall = null } = options
|
|
196
|
+
checkManager(manager, platform)
|
|
197
|
+
const project = loadProject()
|
|
198
|
+
ensureWorkspaceIdentity({ project, ...(dataRoot === undefined ? {} : { dataRoot }) })
|
|
199
|
+
const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform, create: true })
|
|
200
|
+
const { entryPath, source } = resolveLoginItemEntry({ project, ownEntry, temporary })
|
|
201
|
+
const recorded = readLoginItemRecord(workspace)
|
|
202
|
+
const label = recorded?.label ?? loginItemLabel({ platform, projectName: projectNameOf(project), workspaceId: workspace.workspaceId })
|
|
203
|
+
const plan = planLoginItem({
|
|
204
|
+
platform, project, workspaceRoot: workspace.workspaceRoot, workspaceId: workspace.workspaceId, dataRoot: loginItemDataRoot({ project, dataRoot, env, platform }), label, entryPath, entryArgs, nodePath,
|
|
205
|
+
searchPath: startupSearchPath(pathValue, { temporary }),
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// The consent first: the manager starts the service as soon as it loads the unit.
|
|
209
|
+
const now = isoTime(clock)
|
|
210
|
+
const before = readServiceSettings(workspace)
|
|
211
|
+
if (consent === undefined && before?.consent.coverage !== 'service-and-startup') refuse('startup-consent-required', 'a login item needs a consent that covers the service starting at login; name who gives it')
|
|
212
|
+
if (consent !== undefined && (typeof consent?.actor !== 'string' || consent.actor === '')) refuse('startup-consent-required', 'a consent names its actor')
|
|
213
|
+
await ensureServiceSettings(workspace, { consent: consent === undefined ? undefined : { actor: consent.actor, coverage: 'service-and-startup' }, now })
|
|
214
|
+
if (typeof beforeInstall === 'function') await beforeInstall({ entryPath })
|
|
215
|
+
|
|
216
|
+
const installed = await manager.install({ label, fileName: plan.fileName, text: plan.text })
|
|
217
|
+
if (!installed.ok) {
|
|
218
|
+
restoreConsent(workspace, before, now)
|
|
219
|
+
return { installed: false, reason: installed.code, label, message: installed.message ?? null }
|
|
220
|
+
}
|
|
221
|
+
writeLoginItemRecord({
|
|
222
|
+
...workspace,
|
|
223
|
+
record: { schema: LOGIN_ITEM_SCHEMA, workspaceId: workspace.workspaceId, kind: manager.kind, label, file: installed.file, digest: plan.digest, program: plan.program, searchPath: plan.searchPath, installedAt: recorded?.installedAt ?? now, updatedAt: now },
|
|
224
|
+
})
|
|
225
|
+
return { installed: true, label, file: installed.file, kind: manager.kind, entry: { path: entryPath, source }, node: nodePath, searchPath: plan.searchPath }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Back to the consent that was recorded before an installation the manager refused: the service may still run, as a
|
|
229
|
+
// child, under the consent it had.
|
|
230
|
+
function restoreConsent(workspace, before, now) {
|
|
231
|
+
const current = readServiceSettings(workspace)
|
|
232
|
+
if (current === null) return
|
|
233
|
+
const consent = before?.consent ?? { ...current.consent, coverage: 'service' }
|
|
234
|
+
if (JSON.stringify(consent) !== JSON.stringify(current.consent)) writeServiceSettings({ ...workspace, settings: { ...current, consent, updatedAt: now } })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Removes this workspace's login item: the consent is lowered to the service alone first, then the manager unloads
|
|
238
|
+
// and deletes the unit (a service it runs is stopped as it stops it), then the record goes. { removed, label, file }
|
|
239
|
+
// or { removed: false, reason } when the manager kept it.
|
|
240
|
+
export async function removeLoginItem(options = {}) {
|
|
241
|
+
const { loadProject, dataRoot, env = process.env, platform = process.platform, manager, clock = () => new Date() } = options
|
|
242
|
+
checkManager(manager, platform)
|
|
243
|
+
const project = loadProject()
|
|
244
|
+
const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform })
|
|
245
|
+
if (!workspace?.workspaceRoot) return { removed: false, reason: 'workspace-not-prepared' }
|
|
246
|
+
const record = readLoginItemRecord(workspace)
|
|
247
|
+
const label = record?.label ?? loginItemLabel({ platform, projectName: projectNameOf(project), workspaceId: workspace.workspaceId })
|
|
248
|
+
const fileName = record === null ? `${label}${manager.kind === 'launchd-user-agent' ? '.plist' : '.service'}` : path.basename(record.file)
|
|
249
|
+
const now = isoTime(clock)
|
|
250
|
+
const settings = readServiceSettings(workspace)
|
|
251
|
+
if (settings?.consent.coverage === 'service-and-startup') await ensureServiceSettings(workspace, { consent: { actor: settings.consent.actor, coverage: 'service' }, now })
|
|
252
|
+
const removed = await manager.remove({ label, fileName })
|
|
253
|
+
if (!removed.ok) return { removed: false, reason: removed.code, label, message: removed.message ?? null }
|
|
254
|
+
removeLoginItemRecord(workspace)
|
|
255
|
+
return { removed: removed.removed === true || record !== null, label, file: manager.fileFor(fileName) }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// The unit this workspace's installed login item would have now, keeping its label and the search path recorded when
|
|
259
|
+
// it was installed; null when no entry for it can be found from here (the command runs from a package runner and the
|
|
260
|
+
// project has no package installed): what is installed then stays as it is.
|
|
261
|
+
export function currentLoginItemPlan({ record, project, workspaceRoot, dataRoot, env = process.env, platform, ownEntry, entryArgs = [], nodePath = realNodePath(), temporary = temporaryRoots() }) {
|
|
262
|
+
try {
|
|
263
|
+
const { entryPath } = resolveLoginItemEntry({ project, ownEntry, temporary })
|
|
264
|
+
return planLoginItem({
|
|
265
|
+
platform, project, workspaceRoot, workspaceId: record.workspaceId, dataRoot: loginItemDataRoot({ project, dataRoot, env, platform }), label: record.label, entryPath, entryArgs, nodePath,
|
|
266
|
+
searchPath: record.searchPath,
|
|
267
|
+
})
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (error?.code === 'login-item-needs-installed-package' || error?.code === 'startup-adapter-input-invalid') return null
|
|
270
|
+
throw error
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// { entryPath, start() } for startService: the entry the item runs, and a start through its manager. The manager is
|
|
275
|
+
// asked first whether the person switched the item off (System Settings, `systemctl --user disable`): one that is off
|
|
276
|
+
// is neither written again, reloaded, enabled nor started (`login-item-switched-off`), and startService starts a child
|
|
277
|
+
// for that command only. A unit that differs from `plan` is written again and reloaded first (the answer then carries
|
|
278
|
+
// `refreshed: true`); a unit whose file is gone was removed by somebody and is not put back.
|
|
279
|
+
export function loginItemStarter({ workspace, manager, record, plan = null, clock = () => new Date() }) {
|
|
280
|
+
const fileName = path.basename(record.file)
|
|
281
|
+
let entryPath = record.program.entry
|
|
282
|
+
return {
|
|
283
|
+
get entryPath() { return entryPath },
|
|
284
|
+
async start() {
|
|
285
|
+
let refreshed = false
|
|
286
|
+
const text = manager.readUnit({ label: record.label, fileName })
|
|
287
|
+
if (text === null) return { ok: false, code: 'login-item-file-missing', refreshed }
|
|
288
|
+
let seen = null
|
|
289
|
+
try { seen = await manager.inspect({ label: record.label, fileName }) } catch { seen = null }
|
|
290
|
+
if (seen?.ok === true && seen.disabled === true) return { ok: false, code: 'login-item-switched-off', refreshed }
|
|
291
|
+
if (plan !== null && plan.fileName === fileName && text !== plan.text) {
|
|
292
|
+
const installed = await manager.install({ label: record.label, fileName, text: plan.text })
|
|
293
|
+
if (!installed.ok) return { ok: false, code: installed.code, refreshed }
|
|
294
|
+
writeLoginItemRecord({ ...workspace, record: { ...record, file: installed.file, digest: plan.digest, program: plan.program, updatedAt: isoTime(clock) } })
|
|
295
|
+
entryPath = plan.program.entry
|
|
296
|
+
refreshed = true
|
|
297
|
+
}
|
|
298
|
+
const started = await manager.start({ label: record.label, fileName })
|
|
299
|
+
return { ...started, refreshed, entryPath }
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// What this workspace's login item looks like: the record, what the manager says about it, and how the last start by
|
|
305
|
+
// it ended. `manager` is a manager, or a function that answers one (asked only when an item is recorded), or null
|
|
306
|
+
// (it cannot be asked from here): the record alone is reported then.
|
|
307
|
+
// state loaded | not-loaded | switched-off | file-missing | unknown
|
|
308
|
+
export async function loginItemStatus({ workspace, manager: given = null }) {
|
|
309
|
+
const lastStartup = (() => { try { return readLastStartup(workspace) } catch { return null } })()
|
|
310
|
+
let record
|
|
311
|
+
try { record = readLoginItemRecord(workspace) } catch (error) { return { installed: null, reason: error.code ?? 'invalid-login-item', lastStartup } }
|
|
312
|
+
if (record === null) return { installed: false, lastStartup }
|
|
313
|
+
// A unit whose entry or Node is gone cannot start: its manager tries again every minute until it is removed or installed again.
|
|
314
|
+
const present = (file) => { try { return fs.statSync(file).isFile() } catch { return false } }
|
|
315
|
+
const shown = {
|
|
316
|
+
installed: true, label: record.label, file: record.file, kind: record.kind, entry: record.program.entry, node: record.program.node, installedAt: record.installedAt,
|
|
317
|
+
programPresent: present(record.program.entry) && present(record.program.node), lastStartup,
|
|
318
|
+
}
|
|
319
|
+
const manager = typeof given === 'function' ? await given() : given
|
|
320
|
+
if (manager === null) return { ...shown, state: 'unknown', inspected: false }
|
|
321
|
+
const fileName = path.basename(record.file)
|
|
322
|
+
let seen = null
|
|
323
|
+
try { seen = await manager.inspect({ label: record.label, fileName }) } catch { seen = null }
|
|
324
|
+
let text = null
|
|
325
|
+
try { text = manager.readUnit({ label: record.label, fileName }) } catch { text = null }
|
|
326
|
+
const state = seen === null || seen.ok !== true ? 'unknown'
|
|
327
|
+
: seen.present === false ? 'file-missing'
|
|
328
|
+
: seen.disabled === true ? 'switched-off'
|
|
329
|
+
: seen.loaded === true ? 'loaded'
|
|
330
|
+
: seen.loaded === false ? 'not-loaded' : 'unknown'
|
|
331
|
+
return { ...shown, state, inspected: true, running: seen?.running ?? null, pid: seen?.pid ?? null, lastExit: seen?.lastExit ?? null, changedOnDisk: typeof text === 'string' && sha256(text) !== record.digest }
|
|
332
|
+
}
|
|
@@ -6,12 +6,13 @@ import { LOCAL_STATE_DIR, localStateRoot } from '../../project/config.mjs'
|
|
|
6
6
|
import { checkManagedRoots } from '../../project/file-class.mjs'
|
|
7
7
|
import { atomicReplacePrivateText, ensureContainedPrivateDirectory, readRegularTextNoFollow } from '../../project/private-state.mjs'
|
|
8
8
|
import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, assertObsidianContract } from '../../projection/obsidian/contracts.mjs'
|
|
9
|
+
import { LOCAL_AUDIENCES } from '../../projection/policy.mjs'
|
|
9
10
|
import { refuse } from './errors.mjs'
|
|
10
11
|
import { canonicalJson, closedObject, isPlainObject } from './documents.mjs'
|
|
11
12
|
|
|
12
13
|
// Machine-private settings of the Obsidian maintenance runtime.
|
|
13
14
|
//
|
|
14
|
-
// <data>/obsidian/<workspace-id>/state/settings/machine.json mode, audiences, policy reference
|
|
15
|
+
// <data>/obsidian/<workspace-id>/state/settings/machine.json mode, audiences, policy reference, remembered decisions
|
|
15
16
|
// <data>/obsidian/<workspace-id>/state/settings/apply-policy.json the installed apply policy
|
|
16
17
|
// <project>/.atelier-local/obsidian.json pointer: workspace id, optional data root
|
|
17
18
|
//
|
|
@@ -22,14 +23,33 @@ import { canonicalJson, closedObject, isPlainObject } from './documents.mjs'
|
|
|
22
23
|
// checkout keeps its vaults) and, optionally, where this machine keeps the
|
|
23
24
|
// data directory.
|
|
24
25
|
|
|
25
|
-
export const MACHINE_SETTINGS_SCHEMA = 'atelier-obsidian-machine-settings/
|
|
26
|
+
export const MACHINE_SETTINGS_SCHEMA = 'atelier-obsidian-machine-settings/v2'
|
|
27
|
+
// What releases up to 0.2.0-alpha.12 wrote. It is still read, as the v2 document it stands for, and never written:
|
|
28
|
+
// the next write writes v2. A release that knows only v1 refuses a v2 document, so a downgrade stops, loudly, rather
|
|
29
|
+
// than working from settings it cannot read.
|
|
30
|
+
export const MACHINE_SETTINGS_SCHEMA_V1 = 'atelier-obsidian-machine-settings/v1'
|
|
26
31
|
export const LOCAL_POINTER_SCHEMA = 'atelier-obsidian-local-pointer/v1'
|
|
27
32
|
export const LOCAL_POINTER_FILE = 'obsidian.json'
|
|
28
33
|
export const MAINTENANCE_MODES = Object.freeze(['manual', 'automatic'])
|
|
29
34
|
|
|
35
|
+
// What a person decided once for this workspace on this machine, so that no later run has to ask again: who may see
|
|
36
|
+
// its vaults, where they live, whether maintenance starts at login, and that the installed app may be reached. Each is
|
|
37
|
+
// null until it is decided.
|
|
38
|
+
export const DECISIONS = Object.freeze(['audience', 'location', 'loginItem', 'adapter'])
|
|
39
|
+
// How a decision was made: answered to a question at a terminal, given on the command line (an operation or a flag),
|
|
40
|
+
// taken as the defaults, or carried over from a v1 document.
|
|
41
|
+
export const DECISION_SOURCES = Object.freeze(['question', 'command', 'defaults', 'v1'])
|
|
42
|
+
// "Only you": every audience of a note, except `sensitive`, which a vault takes only when it is named. A vault is a
|
|
43
|
+
// folder in which the app's community plugins run with full access to its files.
|
|
44
|
+
export const ONLY_YOU_AUDIENCES = Object.freeze(LOCAL_AUDIENCES.filter((audience) => audience !== 'sensitive').sort())
|
|
45
|
+
// Whether the notes that carry no classification enter this workspace's vaults. They may only for "only you": a vault
|
|
46
|
+
// of any other audience never shows them. Withheld unless a person decided otherwise.
|
|
47
|
+
export const UNCLASSIFIED_CHOICES = Object.freeze(['shown', 'withheld'])
|
|
48
|
+
|
|
30
49
|
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
31
50
|
const DIGEST = /^sha256:[0-9a-f]{64}$/
|
|
32
51
|
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/
|
|
52
|
+
const CONTROL = /[\u0000-\u001f\u007f]/
|
|
33
53
|
|
|
34
54
|
const segment = (identifier) => identifier.replaceAll(':', '_')
|
|
35
55
|
|
|
@@ -138,9 +158,9 @@ export function ensureWorkspaceIdentity({ project, dataRoot, randomBytes = crypt
|
|
|
138
158
|
// Machine settings
|
|
139
159
|
// ---------------------------------------------------------------------------
|
|
140
160
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
161
|
+
const MACHINE_FIELDS = Object.freeze(['schema', 'workspaceId', 'maintenanceMode', 'audienceAllow', 'applyPolicy', 'updatedAt'])
|
|
162
|
+
|
|
163
|
+
function validateCommonFields(document, workspaceId) {
|
|
144
164
|
if (document.workspaceId !== workspaceId) refuse('invalid-machine-settings', 'machine settings belong to another workspace')
|
|
145
165
|
if (!MAINTENANCE_MODES.includes(document.maintenanceMode)) refuse('invalid-machine-settings', 'maintenanceMode must be manual or automatic')
|
|
146
166
|
if (!Array.isArray(document.audienceAllow) || document.audienceAllow.length > 64 || document.audienceAllow.some((item) => typeof item !== 'string' || !IDENTIFIER.test(item))
|
|
@@ -155,29 +175,116 @@ function validateMachineSettings(document, workspaceId) {
|
|
|
155
175
|
}
|
|
156
176
|
}
|
|
157
177
|
if (typeof document.updatedAt !== 'string' || !TIMESTAMP.test(document.updatedAt)) refuse('invalid-machine-settings', 'updatedAt must be a UTC timestamp')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const isOneOf = (values) => (value) => values.includes(value)
|
|
181
|
+
const isAbsoluteDirectory = (value) => typeof value === 'string' && value.length <= 4096 && !CONTROL.test(value) && path.isAbsolute(value) && path.resolve(value) === value
|
|
182
|
+
// The members of each decision besides its stamp, and what each may hold. A decision gains a member here, and a
|
|
183
|
+
// document that carries it is refused by a release that does not know it, loudly.
|
|
184
|
+
const DECISION_MEMBERS = Object.freeze({
|
|
185
|
+
audience: Object.freeze({ choice: isOneOf(['only-you', 'custom']), unclassified: isOneOf(UNCLASSIFIED_CHOICES) }),
|
|
186
|
+
location: Object.freeze({ parent: isAbsoluteDirectory }),
|
|
187
|
+
loginItem: Object.freeze({ choice: isOneOf(['on', 'off']) }),
|
|
188
|
+
adapter: Object.freeze({ choice: isOneOf(['obsidian-cli']) }),
|
|
189
|
+
})
|
|
190
|
+
function sameMembers(left, right) {
|
|
191
|
+
const [sortedLeft, sortedRight] = [[...left].sort(), [...right].sort()]
|
|
192
|
+
return sortedLeft.length === sortedRight.length && sortedLeft.every((item, index) => item === sortedRight[index])
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// `deciding` is true when the audience decision is being recorded (withDecision). Only then must "only you" be exactly
|
|
196
|
+
// the set this release stands for. Read, or written back by any other change, an "only you" list is the one recorded
|
|
197
|
+
// when it was decided: any part of this release's set, never `sensitive` and never an audience this release does not
|
|
198
|
+
// know. So a later release that adds an audience to "only you" still reads what an earlier one recorded, and
|
|
199
|
+
// `audience set me` records the current set again.
|
|
200
|
+
function validateDecisions(document, { deciding = false } = {}) {
|
|
201
|
+
const code = 'invalid-machine-settings'
|
|
202
|
+
closedObject(document.decisions, { required: DECISIONS }, code, 'the remembered decisions')
|
|
203
|
+
for (const name of DECISIONS) {
|
|
204
|
+
const decision = document.decisions[name]
|
|
205
|
+
if (decision === null) continue
|
|
206
|
+
const members = DECISION_MEMBERS[name]
|
|
207
|
+
closedObject(decision, { required: [...Object.keys(members), 'decidedAt', 'decidedBy', 'via'] }, code, `the ${name} decision`)
|
|
208
|
+
for (const [member, holds] of Object.entries(members)) if (!holds(decision[member])) refuse(code, `the ${name} decision carries an unknown ${member}`)
|
|
209
|
+
if (typeof decision.decidedAt !== 'string' || !TIMESTAMP.test(decision.decidedAt)) refuse(code, `the ${name} decision needs a UTC time`)
|
|
210
|
+
// Who decided, when it is known: an identifier, or null.
|
|
211
|
+
if (decision.decidedBy !== null && (typeof decision.decidedBy !== 'string' || !IDENTIFIER.test(decision.decidedBy))) refuse(code, `the ${name} decision names who decided as an identifier or null`)
|
|
212
|
+
if (!DECISION_SOURCES.includes(decision.via)) refuse(code, `the ${name} decision says how it was made`)
|
|
213
|
+
}
|
|
214
|
+
// "Only you" is a set of audiences, and the list the engine reads is exactly that set when it is decided.
|
|
215
|
+
if (document.decisions.audience?.choice === 'only-you') {
|
|
216
|
+
const recorded = document.audienceAllow.length > 0 && document.audienceAllow.every((audience) => ONLY_YOU_AUDIENCES.includes(audience))
|
|
217
|
+
if (deciding ? !sameMembers(document.audienceAllow, ONLY_YOU_AUDIENCES) : !recorded) refuse(code, 'the audiences allowed are not the ones "only you" stands for')
|
|
218
|
+
}
|
|
219
|
+
if (document.decisions.audience?.unclassified === 'shown' && document.decisions.audience.choice !== 'only-you') {
|
|
220
|
+
refuse(code, 'notes without a classification are shown only in a vault that is only yours')
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function validateV1(document, workspaceId) {
|
|
225
|
+
closedObject(document, { required: MACHINE_FIELDS }, 'invalid-machine-settings', 'machine settings')
|
|
226
|
+
validateCommonFields(document, workspaceId)
|
|
158
227
|
return document
|
|
159
228
|
}
|
|
160
229
|
|
|
161
|
-
|
|
162
|
-
|
|
230
|
+
function validateV2(document, workspaceId, { deciding = false } = {}) {
|
|
231
|
+
closedObject(document, { required: [...MACHINE_FIELDS, 'decisions'] }, 'invalid-machine-settings', 'machine settings')
|
|
232
|
+
if (document.schema !== MACHINE_SETTINGS_SCHEMA) refuse('invalid-machine-settings', 'machine settings name an unknown schema')
|
|
233
|
+
validateCommonFields(document, workspaceId)
|
|
234
|
+
validateDecisions(document, { deciding })
|
|
235
|
+
return document
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// A v1 document as the v2 document it stands for. Nothing was remembered then, except that a list of audiences a
|
|
239
|
+
// person set (`audience set`) is their decision, and is carried over as one, with the notes that carry no
|
|
240
|
+
// classification withheld as they were; who made it is not known.
|
|
241
|
+
function fromV1(document) {
|
|
242
|
+
const { schema: _v1, ...fields } = document
|
|
243
|
+
const audience = fields.audienceAllow.length === 0 ? null : { choice: 'custom', unclassified: 'withheld', decidedAt: fields.updatedAt, decidedBy: null, via: 'v1' }
|
|
244
|
+
return { ...fields, schema: MACHINE_SETTINGS_SCHEMA, decisions: { audience, location: null, loginItem: null, adapter: null } }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Every document this module reads or is handed, as a validated v2 document.
|
|
248
|
+
function normalizeMachineSettings(document, workspaceId) {
|
|
249
|
+
if (isPlainObject(document) && document.schema === MACHINE_SETTINGS_SCHEMA_V1) return validateV2(fromV1(validateV1(document, workspaceId)), workspaceId)
|
|
250
|
+
return validateV2(document, workspaceId)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// The defaults fail closed: edits are only queued, no audience is visible,
|
|
254
|
+
// no apply policy is installed and nothing is decided until the person says
|
|
255
|
+
// otherwise.
|
|
163
256
|
export function defaultMachineSettings({ workspaceId, updatedAt }) {
|
|
164
|
-
return {
|
|
257
|
+
return {
|
|
258
|
+
schema: MACHINE_SETTINGS_SCHEMA, workspaceId, maintenanceMode: 'manual', audienceAllow: [], applyPolicy: null,
|
|
259
|
+
decisions: Object.fromEntries(DECISIONS.map((name) => [name, null])), updatedAt,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// The settings with one decision recorded, validated; nothing is written. `decision` holds the decision's own members
|
|
264
|
+
// (`{ choice, unclassified }`, `{ parent }`, `{ choice }`); the stamp says when, by whom (an identifier, or null when not
|
|
265
|
+
// known) and how.
|
|
266
|
+
export function withDecision(settings, name, decision, { decidedAt, decidedBy = null, via }) {
|
|
267
|
+
if (!DECISIONS.includes(name)) throw new TypeError(`unknown decision: ${String(name)}`)
|
|
268
|
+
const next = { ...settings, decisions: { ...settings.decisions, [name]: { ...decision, decidedAt, decidedBy, via } } }
|
|
269
|
+
return validateV2(next, settings.workspaceId, { deciding: name === 'audience' })
|
|
165
270
|
}
|
|
166
271
|
|
|
167
272
|
const settingsDirectory = (workspaceRoot) => ensureContainedPrivateDirectory({ workspaceRoot, directory: path.join(workspaceRoot, 'state', 'settings'), label: 'Obsidian machine settings' })
|
|
168
273
|
const settingsFile = (workspaceRoot, name) => path.join(workspaceRoot, 'state', 'settings', name)
|
|
169
274
|
|
|
275
|
+
// A v1 document is returned as v2 (see fromV1); the file keeps its v1 bytes until the next write.
|
|
170
276
|
export function readMachineSettings({ workspaceRoot, workspaceId }) {
|
|
171
277
|
const document = readJsonFile(settingsFile(workspaceRoot, 'machine.json'), 'invalid-machine-settings', 'machine settings')
|
|
172
|
-
return document === null ? null :
|
|
278
|
+
return document === null ? null : normalizeMachineSettings(document, workspaceId)
|
|
173
279
|
}
|
|
174
280
|
|
|
281
|
+
// Writes v2, whatever version it is handed, and returns what it wrote.
|
|
175
282
|
export function writeMachineSettings({ workspaceRoot, workspaceId, settings, repositoryRoots }) {
|
|
176
|
-
|
|
283
|
+
const document = normalizeMachineSettings(settings, workspaceId)
|
|
177
284
|
assertOutsideRepositories({ managedRoot: workspaceRoot, repositoryRoots })
|
|
178
285
|
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 })
|
|
179
|
-
atomicReplacePrivateText(path.join(settingsDirectory(workspaceRoot), 'machine.json'), canonicalJson(
|
|
180
|
-
return
|
|
286
|
+
atomicReplacePrivateText(path.join(settingsDirectory(workspaceRoot), 'machine.json'), canonicalJson(document))
|
|
287
|
+
return document
|
|
181
288
|
}
|
|
182
289
|
|
|
183
290
|
// ---------------------------------------------------------------------------
|