@mnstry/atelier 0.2.0-alpha.12 → 0.2.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +661 -0
- package/README.md +142 -6
- package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
- package/contracts/atelier-architecture.v1.schema.json +258 -0
- package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
- package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
- package/contracts/atelier-build.v1.schema.json +1076 -0
- package/contracts/atelier-capability.v1.schema.json +1252 -0
- package/contracts/atelier-coordination.v1.schema.json +1050 -0
- package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
- package/contracts/atelier-harness.v1.schema.json +234 -0
- package/contracts/atelier-ingestion.v1.schema.json +806 -0
- package/contracts/atelier-inquiry.v1.schema.json +1772 -0
- package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
- package/contracts/atelier-interaction.v1.schema.json +553 -0
- package/contracts/atelier-judgment.v1.schema.json +690 -0
- package/contracts/atelier-knowledge.v1.schema.json +1512 -0
- package/contracts/atelier-learning.v1.schema.json +1095 -0
- package/contracts/atelier-migration.v3.schema.json +64 -0
- package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
- package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
- package/contracts/atelier-template-adoption.v1.schema.json +223 -0
- package/contracts/atelier-template-profile.v1.schema.json +1168 -0
- package/contracts/atelier-trackable.v1.schema.json +781 -0
- package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
- package/docs/architecture.md +89 -0
- package/docs/assurance-controls.md +2 -2
- package/docs/blocks/capability-stewardship.md +17 -0
- package/docs/blocks/claims.md +16 -3
- package/docs/blocks/inquiry-harnesses.md +19 -0
- package/docs/blocks/learning-harnesses.md +17 -0
- package/docs/capability-stewardship.md +269 -0
- package/docs/coordination.md +51 -0
- package/docs/decision-practices.md +111 -0
- package/docs/decisions.md +29 -0
- package/docs/evidence-navigation.md +237 -0
- package/docs/exact-upgrades.md +3 -2
- package/docs/ingestion.md +181 -0
- package/docs/inquiry-harnesses.md +211 -0
- package/docs/install.md +80 -3
- package/docs/intake-and-guides.md +5 -0
- package/docs/integration-contract-decisions.md +24 -2
- package/docs/interaction-and-reflection.md +72 -0
- package/docs/knowledge-graph.md +20 -0
- package/docs/knowledge-health/api.md +30 -0
- package/docs/knowledge-health/assessment/api.md +96 -0
- package/docs/knowledge-health/assessment/capabilities.md +40 -0
- package/docs/knowledge-health/assessment/evaluation.md +27 -0
- package/docs/knowledge-health/assessment/facilitator.md +30 -0
- package/docs/knowledge-health/assessment/quickstart.md +61 -0
- package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
- package/docs/knowledge-health/devday/facilitator.md +16 -0
- package/docs/knowledge-health/devday/participant-response.md +22 -0
- package/docs/knowledge-health/devday/public-default.md +107 -0
- package/docs/knowledge-health/devday/quickstart.md +18 -0
- package/docs/knowledge-health/devday/support.md +22 -0
- package/docs/knowledge-health/exercise.md +17 -0
- package/docs/knowledge-health/facilitator.md +20 -0
- package/docs/knowledge-health/quickstart.md +94 -0
- package/docs/knowledge-health/readme.md +15 -0
- package/docs/knowledge-health/support.md +34 -0
- package/docs/knowledge-setup.md +248 -0
- package/docs/knowledge-workspace.md +264 -0
- package/docs/learning-harnesses.md +238 -0
- package/docs/learning.md +215 -0
- package/docs/local-services.md +181 -16
- package/docs/obsidian-contract.md +223 -15
- package/docs/obsidian-plugin.md +38 -4
- package/docs/obsidian.md +613 -51
- package/docs/optional-ingestion.md +83 -0
- package/docs/personal-workspace.md +257 -0
- package/docs/practical-judgment.md +92 -0
- package/docs/progressive-ingestion-acceptance.md +89 -0
- package/docs/progressive-ingestion.md +235 -0
- package/docs/project-command-inventory.md +1 -0
- package/docs/release-engineering.md +49 -0
- package/docs/repo-boundary-guard.md +94 -0
- package/docs/research-starter-quickstart.md +66 -0
- package/docs/research-starter.md +114 -0
- package/docs/semantic-ingestion.md +198 -0
- package/docs/semantic-operation.md +317 -0
- package/docs/skill-steward.md +6 -0
- package/docs/template-profile.md +164 -0
- package/docs/template-upgrades.md +114 -0
- package/docs/trackables.md +111 -0
- package/docs/upgrade.md +47 -2
- package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
- package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
- package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
- package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
- package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
- package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
- package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
- package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
- package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
- package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
- package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
- package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
- package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
- package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
- package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
- package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
- package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
- package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
- package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
- package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
- package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
- package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
- package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
- package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
- package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
- package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
- package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
- package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
- package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
- package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
- package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
- package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
- package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
- package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
- package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
- package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
- package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
- package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
- package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
- package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
- package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
- package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
- package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
- package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
- package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
- package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
- package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
- package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
- package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
- package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
- package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
- package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
- package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
- package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
- package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
- package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
- package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
- package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
- package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
- package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
- package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
- package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
- package/fixtures/atelier-interaction/act/valid/document.json +24 -0
- package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
- package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
- package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
- package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
- package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
- package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
- package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
- package/fixtures/atelier-judgment/case/valid/document.json +75 -0
- package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
- package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
- package/fixtures/atelier-judgment/native-decision.md +3 -0
- package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
- package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
- package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
- package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
- package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
- package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
- package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
- package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
- package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
- package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
- package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
- package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
- package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
- package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-learning/invalid/authority.json +14 -0
- package/fixtures/atelier-learning/valid/capture.json +13 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
- package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
- package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
- package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
- package/fixtures/atelier-trackable/request/valid/document.json +22 -0
- package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
- package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
- package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
- package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
- package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
- package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
- package/fixtures/capability-packages/adoption.example.json +70 -0
- package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
- package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
- package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
- package/fixtures/capability-packages/research/capability-package.json +88 -0
- package/fixtures/capability-packages/research/capability-release.json +134 -0
- package/fixtures/capability-packages/research/evaluation.md +15 -0
- package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
- package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
- package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
- package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
- package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
- package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
- package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
- package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
- package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
- package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
- package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
- package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
- package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
- package/fixtures/evidence-navigation/valid/claim.json +36 -0
- package/fixtures/evidence-navigation/valid/host.json +16 -0
- package/fixtures/evidence-navigation/valid/profile.json +16 -0
- package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
- package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
- package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
- package/fixtures/harnesses/learning-cycle.json +780 -0
- package/fixtures/inquiry/workshop.json +675 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
- package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
- package/fixtures/knowledge-health/calibration-suite.json +26 -0
- package/fixtures/knowledge-health/calibration-trial.json +37 -0
- package/fixtures/knowledge-health/corrected/checklist.md +18 -0
- package/fixtures/knowledge-health/devday/participant-record.json +19 -0
- package/fixtures/knowledge-health/expected-summary.json +27 -0
- package/fixtures/knowledge-health/workshop/README.md +20 -0
- package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
- package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
- package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
- package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
- package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
- package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
- package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
- package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
- package/fixtures/personal-workspace/overlay.json +24 -0
- package/fixtures/templates/local-library.v1.json +75 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
- package/package.json +59 -5
- package/skills/claude/atelier-build-harness/SKILL.md +59 -0
- package/skills/claude/atelier-coordination/SKILL.md +32 -0
- package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/claude/atelier-interaction/SKILL.md +34 -0
- package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/claude/atelier-research-harness/SKILL.md +43 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
- package/skills/claude/atelier-trackables/SKILL.md +40 -0
- package/skills/codex/atelier-build-harness/SKILL.md +59 -0
- package/skills/codex/atelier-coordination/SKILL.md +32 -0
- package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/codex/atelier-interaction/SKILL.md +34 -0
- package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/codex/atelier-research-harness/SKILL.md +43 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
- package/skills/codex/atelier-trackables/SKILL.md +40 -0
- package/src/architecture/catalog.mjs +355 -0
- package/src/architecture/index.mjs +93 -0
- package/src/boundary/content-rules.mjs +4 -2
- package/src/boundary/delegation-command.mjs +158 -0
- package/src/boundary/delegation.mjs +396 -0
- package/src/boundary/policy.mjs +52 -10
- package/src/build/git.mjs +18 -0
- package/src/build/index.mjs +3 -0
- package/src/build/ledger.mjs +88 -0
- package/src/capabilities/adoption.mjs +381 -0
- package/src/capabilities/evidence.mjs +140 -0
- package/src/capabilities/files.mjs +124 -0
- package/src/capabilities/index.mjs +4 -0
- package/src/capabilities/instructions.mjs +250 -0
- package/src/capabilities/package.mjs +136 -0
- package/src/cli/command-failure.mjs +36 -0
- package/src/cli/execute-command.mjs +16 -2
- package/src/cli/run.mjs +65 -9
- package/src/coauthor/store.mjs +43 -2
- package/src/collaboration/proposals.mjs +189 -0
- package/src/commands/architecture.mjs +27 -0
- package/src/commands/boundary.mjs +3 -0
- package/src/commands/capability.mjs +73 -0
- package/src/commands/enroll.mjs +3 -0
- package/src/commands/harness.mjs +79 -0
- package/src/commands/ingest.mjs +39 -0
- package/src/commands/init.mjs +4 -1
- package/src/commands/inquiry.mjs +66 -0
- package/src/commands/knowledge-semantic.mjs +128 -0
- package/src/commands/knowledge.mjs +119 -0
- package/src/commands/learn.mjs +61 -0
- package/src/commands/obsidian.mjs +754 -63
- package/src/commands/practice.mjs +24 -0
- package/src/commands/setup.mjs +22 -2
- package/src/commands/trackable.mjs +19 -0
- package/src/contracts/corpus.mjs +30 -0
- package/src/coordination/index.mjs +110 -0
- package/src/evidence-navigation/contracts.mjs +60 -0
- package/src/evidence-navigation/evaluate.mjs +69 -0
- package/src/evidence-navigation/generation.mjs +22 -0
- package/src/evidence-navigation/index.mjs +2 -0
- package/src/evidence-navigation/local.mjs +167 -0
- package/src/evidence-navigation/schema.generated.mjs +604 -0
- package/src/evidence-navigation/types.mjs +12 -0
- package/src/export/atelier-export-contract.mjs +35 -8
- package/src/graph/enroll.mjs +248 -0
- package/src/graph/graph.mjs +17 -1
- package/src/graph/knowledge-graph.mjs +58 -8
- package/src/harnesses/contracts.mjs +41 -0
- package/src/harnesses/exchange.mjs +154 -0
- package/src/harnesses/feedback.mjs +16 -0
- package/src/harnesses/history.mjs +37 -0
- package/src/harnesses/index.mjs +4 -0
- package/src/harnesses/store.mjs +29 -0
- package/src/ingestion/contracts.mjs +41 -0
- package/src/ingestion/evaluation.mjs +119 -0
- package/src/ingestion/optional/composition/index.mjs +171 -0
- package/src/ingestion/optional/evaluation/index.mjs +219 -0
- package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
- package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
- package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
- package/src/ingestion/optional/graphify/json_bridge.py +163 -0
- package/src/ingestion/optional/graphify/query_receiving.py +209 -0
- package/src/ingestion/optional/index.mjs +9 -0
- package/src/ingestion/optional/jev/bridge.mjs +166 -0
- package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
- package/src/ingestion/optional/jev/judgments.mjs +174 -0
- package/src/ingestion/optional/jev/native-translation.mjs +32 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
- package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
- package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
- package/src/ingestion/optional/vanilla/index.mjs +218 -0
- package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
- package/src/ingestion/processors.mjs +218 -0
- package/src/ingestion/semantic.mjs +203 -0
- package/src/ingestion/store.mjs +372 -0
- package/src/inquiry/beliefs.mjs +32 -0
- package/src/inquiry/contracts.mjs +23 -0
- package/src/inquiry/index.mjs +5 -0
- package/src/inquiry/ledger.mjs +135 -0
- package/src/inquiry/lenses.mjs +15 -0
- package/src/inquiry/projection.mjs +77 -0
- package/src/inquiry/store.mjs +35 -0
- package/src/intake/read-scope.mjs +12 -0
- package/src/intake/store.mjs +69 -6
- package/src/interaction/index.mjs +91 -0
- package/src/judgment/index.mjs +82 -0
- package/src/judgment/practice-evaluation.mjs +70 -0
- package/src/judgment/practice.mjs +144 -0
- package/src/knowledge/context.mjs +217 -0
- package/src/knowledge/index.mjs +5 -0
- package/src/knowledge/ingestion.mjs +79 -0
- package/src/knowledge/intake.mjs +33 -0
- package/src/knowledge/ledger.mjs +110 -0
- package/src/knowledge/participatory/guidance.mjs +36 -0
- package/src/knowledge/participatory/guided-contribution.mjs +44 -0
- package/src/knowledge/participatory/index.mjs +2 -0
- package/src/knowledge/participatory/installed-workshop.mjs +108 -0
- package/src/knowledge/plan.mjs +67 -0
- package/src/knowledge/projection.mjs +43 -0
- package/src/knowledge/semantic-operation.mjs +567 -0
- package/src/knowledge/sessions.mjs +467 -0
- package/src/knowledge/workspace.mjs +295 -0
- package/src/knowledge-health/assessment.mjs +165 -0
- package/src/knowledge-health/capabilities.mjs +350 -0
- package/src/knowledge-health/context.mjs +62 -0
- package/src/knowledge-health/example.mjs +47 -0
- package/src/knowledge-health/index.mjs +10 -0
- package/src/knowledge-health/owner-join.mjs +88 -0
- package/src/knowledge-health/proof/common.mjs +24 -0
- package/src/knowledge-health/proof/installed-check.mjs +100 -0
- package/src/knowledge-health/proof/native-readback.mjs +37 -0
- package/src/knowledge-health/proof/rehearse.mjs +68 -0
- package/src/knowledge-health/proof/validate-participant.mjs +17 -0
- package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
- package/src/knowledge-health/proof/workshop-check.mjs +36 -0
- package/src/knowledge-health/rule-identity.mjs +44 -0
- package/src/knowledge-health/workshop-composition.mjs +19 -0
- package/src/knowledge-health/workshop-profile.mjs +176 -0
- package/src/knowledge-health/workshop.mjs +99 -0
- package/src/learning/adapters.mjs +68 -0
- package/src/learning/assistance.mjs +127 -0
- package/src/learning/contracts.mjs +65 -0
- package/src/learning/store.mjs +272 -0
- package/src/personal-workspace/index.mjs +391 -0
- package/src/personal-workspace/selection.mjs +385 -0
- package/src/project/config.mjs +8 -0
- package/src/project/durable-state.mjs +3 -1
- package/src/project/file-class.mjs +1 -1
- package/src/projection/obsidian/edits/apply.mjs +55 -16
- package/src/projection/obsidian/personal-workspace.mjs +436 -0
- package/src/projection/obsidian/proposals/adapter.mjs +47 -8
- package/src/projection/obsidian/proposals/observation.mjs +10 -2
- package/src/projection/obsidian/publication/publisher.mjs +142 -13
- package/src/projection/obsidian/publication/vault-list.mjs +4 -3
- package/src/projection/obsidian/recovery/index.mjs +4 -1
- package/src/projection/obsidian/recovery/store.mjs +160 -7
- package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
- package/src/readiness/readiness.mjs +10 -3
- package/src/reflection/index.mjs +56 -0
- package/src/runtime/obsidian/app-production-seams.mjs +48 -16
- package/src/runtime/obsidian/app-registration.mjs +175 -28
- package/src/runtime/obsidian/app-restart.mjs +154 -0
- package/src/runtime/obsidian/engine.mjs +163 -20
- package/src/runtime/obsidian/extension-points.mjs +1 -1
- package/src/runtime/obsidian/index.mjs +10 -7
- package/src/runtime/obsidian/launch-plan.mjs +7 -4
- package/src/runtime/obsidian/lifecycle.mjs +92 -12
- package/src/runtime/obsidian/login-item.mjs +332 -0
- package/src/runtime/obsidian/machine-settings.mjs +119 -12
- package/src/runtime/obsidian/opening.mjs +171 -37
- package/src/runtime/obsidian/pipeline.mjs +71 -3
- package/src/runtime/obsidian/plugin-channel.mjs +11 -3
- package/src/runtime/obsidian/plugin-choice.mjs +16 -4
- package/src/runtime/obsidian/plugin-drift.mjs +59 -11
- package/src/runtime/obsidian/plugin-presence.mjs +14 -1
- package/src/runtime/obsidian/process-identity.mjs +20 -10
- package/src/runtime/obsidian/project-views.mjs +282 -0
- package/src/runtime/obsidian/questions.mjs +48 -0
- package/src/runtime/obsidian/release-watch.mjs +56 -0
- package/src/runtime/obsidian/service-main.mjs +99 -16
- package/src/runtime/obsidian/service-manager-production.mjs +61 -0
- package/src/runtime/obsidian/service-managers.mjs +275 -0
- package/src/runtime/obsidian/service-record.mjs +87 -23
- package/src/runtime/obsidian/service.mjs +36 -3
- package/src/runtime/obsidian/startup-adapters.mjs +59 -21
- package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
- package/src/runtime/obsidian/vault-location.mjs +300 -0
- package/src/runtime/obsidian/view-counts.mjs +34 -0
- package/src/server/local-sidecar.mjs +169 -2
- package/src/server/server.mjs +28 -2
- package/src/template-bindings/project.mjs +92 -0
- package/src/templates/profile.mjs +225 -0
- package/src/trackables/domain.mjs +175 -0
- package/src/trackables/store.mjs +114 -0
- package/src/ui/knowledge-page.mjs +1388 -0
- package/src/upgrade/explanation.mjs +1 -1
- package/src/upgrade/template-participant.mjs +171 -0
- package/src/upgrade/transaction.mjs +72 -25
- package/src/upgrade/upgrade.mjs +1 -0
- package/templates/external-project-workspace/pack-lifecycle.json +6 -1
- package/templates/knowledge-workspace/.gitattributes +2 -0
- package/templates/knowledge-workspace/AGENTS.md +27 -0
- package/templates/knowledge-workspace/README.md +44 -0
- package/templates/knowledge-workspace/atelier.project.json +22 -0
- package/templates/knowledge-workspace/gitignore +6 -0
- package/templates/knowledge-workspace/knowledge-plan.json +111 -0
- package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
- package/templates/knowledge-workspace/records/forecast.md +22 -0
- package/templates/knowledge-workspace/records/inspection.md +25 -0
- package/templates/knowledge-workspace/records/telescope.md +24 -0
- package/templates/knowledge-workspace/repo-access.v1.json +9 -0
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { enclosingVaults, findVaultEntry, vaultRoute } from '../../projection/obsidian/publication/vault-list.mjs'
|
|
4
|
-
import { createRecoveryStore as createStore, readFileBytes, sha256Digest } from '../../projection/obsidian/recovery/store.mjs'
|
|
4
|
+
import { createRecoveryStore as createStore, readFileBytes, sha256Digest, vaultRootFor } from '../../projection/obsidian/recovery/store.mjs'
|
|
5
5
|
import { inspectApp, qualifyApp } from './app-capability.mjs'
|
|
6
|
+
import { settingsWriteOutlook } from './app-registration.mjs'
|
|
6
7
|
import { readObsidianEnablement } from './enablement.mjs'
|
|
7
8
|
import { ObsidianMaintenanceRefusal, refuse } from './errors.mjs'
|
|
8
9
|
import { UNAVAILABLE_APPLY_OPERATION } from './extension-points.mjs'
|
|
@@ -30,20 +31,32 @@ import { OPEN_EDIT_STATES, createMaintenanceStateStore } from './state-store.mjs
|
|
|
30
31
|
//
|
|
31
32
|
// Nothing a person does by hand is needed on the way. The app is made to know
|
|
32
33
|
// the vault: through the app itself while it runs and answers, or, while no
|
|
33
|
-
// Obsidian runs, in the app's own vault list (app-registration.mjs)
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
34
|
+
// Obsidian runs, in the app's own vault list (app-registration.mjs), which is
|
|
35
|
+
// created for an Obsidian that never started, and whose command-line switch is
|
|
36
|
+
// turned on in the same write. A view that the app kept from being published
|
|
37
|
+
// (an app that runs without the vault, or that could not be qualified) is
|
|
38
|
+
// published again once the app holds the vault, through the app, before the
|
|
39
|
+
// answer is given.
|
|
40
|
+
//
|
|
41
|
+
// Two states of a running app cannot be got through that way: its command line
|
|
42
|
+
// is turned off, or it has no vault open and does not list this one. Only when
|
|
43
|
+
// the person asked for it (`restartApp`, `--restart-obsidian`; never implied),
|
|
44
|
+
// and only after a read-only check that the write can succeed and a copy of the
|
|
45
|
+
// settings file kept beside it, the app is quit (app-restart.mjs), the vault
|
|
46
|
+
// added with the switch on while it is quit, and the app started again plainly,
|
|
47
|
+
// which reopens every vault it had open. Without that, those states are
|
|
48
|
+
// answered, naming the flag.
|
|
37
49
|
//
|
|
38
50
|
// Nothing here talks to an app or an operating system itself: `appProbe`,
|
|
39
|
-
// `registry` and `
|
|
51
|
+
// `registry`, `launcher` and `quitter` are injected, and there is no default for
|
|
52
|
+
// any.
|
|
40
53
|
|
|
41
54
|
export const OPENING_OUTCOMES = Object.freeze({
|
|
42
55
|
current: { summary: 'the vault is the present generation, verified by read-back, and the app has it open', next: 'nothing to do' },
|
|
43
56
|
updating: { summary: 'maintenance is publishing or has not finished; the vault is not confirmed current', next: 'run `obsidian open` again in a moment' },
|
|
44
57
|
'held-for-your-edit': { summary: 'a note you edited is preserved and held; the view is not republished over it', next: 'apply or withdraw the pending edit, then open again' },
|
|
45
58
|
'stale-readable': { summary: 'a last good vault exists and can be read, but it is not proven to be the present generation', next: 'see `obsidian status` for the reason; `obsidian open --allow-stale` opens it as it is' },
|
|
46
|
-
'not-prepared': { summary: 'no generation of this view
|
|
59
|
+
'not-prepared': { summary: 'no generation of this view can be read back: none was published yet, or its vault cannot be found or used (the reason says which)', next: 'see `obsidian status` for the reason' },
|
|
47
60
|
'app-missing': { summary: 'no Obsidian installation was found', next: 'install Obsidian, then open again' },
|
|
48
61
|
'app-version-unsupported': { summary: 'the installed Obsidian is below the minimum supported version, or its version cannot be read', next: 'update Obsidian, then open again' },
|
|
49
62
|
'app-cli-unavailable': { summary: 'the installed Obsidian has no usable command-line capability', next: 'enable the command-line interface in Obsidian, then open again' },
|
|
@@ -80,11 +93,16 @@ const QUIT = 'quit Obsidian (on Linux, also any app that runs on a system Electr
|
|
|
80
93
|
// Obsidian's settings file could not be used to add the vault while the app was quit: adding it through the app works instead.
|
|
81
94
|
const THROUGH_THE_APP = 'start Obsidian with any vault open, then open again: the vault is then added through the app'
|
|
82
95
|
export const REASON_NEXT = Object.freeze({
|
|
83
|
-
'cli-turned-off': 'Obsidian\'s command line is turned off (the default of a new installation): turn it on in Obsidian under Settings > General > Advanced > Command line interface, then open again',
|
|
84
|
-
'no-vault-open': '
|
|
96
|
+
'cli-turned-off': 'Obsidian\'s command line is turned off (the default of a new installation), and it runs, so its settings are its own: run `atelier obsidian open --restart-obsidian` to let Atelier quit Obsidian, turn its command line on, add this view\'s vault and start it again (the vaults it has open reopen); or turn it on yourself in Obsidian under Settings > General > Advanced > Command line interface, then open again',
|
|
97
|
+
'no-vault-open': 'Obsidian runs with no vault open, so it can be asked nothing: run `atelier obsidian open --restart-obsidian` to let Atelier quit Obsidian (no vault is open, so nothing closes), add this view\'s vault and start it again; or open any vault in Obsidian, or quit Obsidian, then open again',
|
|
98
|
+
'restart-platform-unqualified': 'Atelier restarts Obsidian on macOS only; turn its command line on yourself (Settings > General > Advanced > Command line interface), or open any vault in Obsidian, or quit it, then open again',
|
|
99
|
+
'restart-unavailable': 'this command was given no way to quit Obsidian; quit Obsidian yourself, then open again',
|
|
100
|
+
'app-main-process-unproven': 'Obsidian was not asked to quit: its main process could not be proven from the process table (`restart.detail` says why: more than one Obsidian main process, an Obsidian process of another user, a helper that is not its child, or a table that could not be read); quit Obsidian yourself, then open again',
|
|
101
|
+
'app-did-not-quit': 'Obsidian was asked to quit and did not within the wait; nothing was written to its settings. Quit it yourself, or wait until it has quit, then open again',
|
|
102
|
+
'app-not-started-again': 'Obsidian was quit to add this view\'s vault and could not be started again; start Obsidian yourself: it reopens the vaults it had open',
|
|
85
103
|
'vault-open-cli-silent': 'Obsidian has this view\'s vault open, as Atelier\'s plugin in it shows, but its command line did not answer, so the vault can be neither found nor opened through it; make sure the command-line interface is turned on in Obsidian\'s settings, then open again',
|
|
86
104
|
'editor-uncoordinated': `${UNCOORDINATED}; \`atelier obsidian open\` adds this view's vault to Obsidian and publishes through it, or ${QUIT}; it is retried automatically`,
|
|
87
|
-
'obsidian-settings-missing': 'Obsidian has
|
|
105
|
+
'obsidian-settings-missing': 'Obsidian has no settings file on this account, and the folder it would be in does not exist or is not yours, so Atelier cannot create it: open any vault in Obsidian (starting it if it is not running; it then writes its settings), then open again',
|
|
88
106
|
'obsidian-settings-location-unknown': THROUGH_THE_APP,
|
|
89
107
|
'obsidian-settings-unsafe': `Obsidian's settings file is a link or not a regular file, so it is not written; ${THROUGH_THE_APP}`,
|
|
90
108
|
'obsidian-settings-not-owned': `Obsidian's settings file belongs to another user, so it is not written; ${THROUGH_THE_APP}`,
|
|
@@ -105,6 +123,17 @@ export const REASON_NEXT = Object.freeze({
|
|
|
105
123
|
'service-outdated': 'the maintenance service runs an earlier release of Atelier that could not be replaced; run `atelier obsidian service stop`, then open again',
|
|
106
124
|
'service-other-release': 'the maintenance service runs a later release of Atelier than this command, which never replaces a later release by itself; run `atelier obsidian service stop`, then open again, or open with the later release',
|
|
107
125
|
'vault-open-in-several-windows': 'Obsidian\'s vault list marks this view\'s folder open under more than one entry (in another letter case, or through a link), so it may hold the vault in more than one window, and a publication coordinates with one window only; remove the extra entries from Obsidian\'s vault list (Obsidian keeps the last window it closed marked open, so closing windows does not clear this), then open again',
|
|
126
|
+
'vault-allocation-lost': 'this view was published, but its vault is not where Atelier can find it: no record of a folder allocated for it (state/allocations/<view>.json in Atelier\'s private state), and no folder under the data root (vaults/<view>); restore that record from a backup if its vault was allocated elsewhere, or else make the folder vaults/<view> under the data root: the view is published there at the next change at its sources',
|
|
127
|
+
'vault-allocation-moved': 'this view\'s vault is now reached through a link, or leads somewhere other than where it was allocated, and Atelier publishes only into the folder at the path it recorded; put that folder back at the path `atelier obsidian location show` names, with no link on the way',
|
|
128
|
+
'vault-allocation-replaced': 'another folder, or a link, is where this view\'s vault was allocated, and Atelier publishes only into the folder it made; put the vault back there, or move what is there away: a folder that is gone is made again at the next tick, and the view published into it',
|
|
129
|
+
'vault-note-missing': 'a note of the generation this view last published is missing from its vault; the view is published again, and the note written back, at the next change at its sources',
|
|
130
|
+
'vault-root-moved': 'this view\'s vault was removed, moved or replaced by a link while it was being published, and nothing more was written into it; a vault allocated for the view is made again at the next tick and published into; a vault under the data root is asked for as lost until its folder is there again',
|
|
131
|
+
'vault-allocation-missing': 'the folder allocated to this view is gone; the maintenance service makes it again at its next tick, and publishes the view into it',
|
|
132
|
+
// A publisher-conflict whose first blocking cause is a file Atelier writes in the vault's settings folder. One change
|
|
133
|
+
// is enough to stop a publication, and the reason names only the first file that did: nothing says that no other
|
|
134
|
+
// publisher or editor is involved.
|
|
135
|
+
'plugin-file-changed': 'a file of Atelier\'s plugin in this vault (under .obsidian/plugins) changed while the view was published, by another program or by you, so the publication did not finish; it is retried automatically, and if this keeps happening, stop what keeps writing there (a sync tool, say)',
|
|
136
|
+
'settings-changed': 'a settings file Atelier writes in this vault (the plugin list or a plugin setting under .obsidian) changed while the view was published, by Obsidian, another program or you, and was not written over; it is retried automatically, reading the file as it is then',
|
|
108
137
|
'vault-inside-another-vault': 'Obsidian lists another vault at a folder that contains this view\'s vault; open never adds a vault inside another one, and never sends a call that could reach that vault instead: remove that vault from Obsidian\'s vault list, or keep Atelier\'s data root outside that folder, then open again',
|
|
109
138
|
})
|
|
110
139
|
|
|
@@ -136,11 +165,13 @@ export const OPENING_PRIMITIVES = Object.freeze({
|
|
|
136
165
|
// Whether a view that is not current was kept from publication by the app: then making the app hold the vault
|
|
137
166
|
// and asking for the view again can make it current. A vault open in several windows is asked for again once one
|
|
138
167
|
// window is left; while more are open, open names them instead.
|
|
139
|
-
|
|
168
|
+
// A view the service did not publish because the app's command line is turned off is one of them: once the app runs
|
|
169
|
+
// with it on and holds the vault, it is published through it.
|
|
170
|
+
keptByApp: (view) => ['publisher-conflict', 'stale-readable', 'not-prepared'].includes(view.outcome) && ['editor-uncoordinated', 'vault-open-in-several-windows', 'app-version-unsupported', 'app-cli-unavailable'].includes(view.reason),
|
|
140
171
|
})
|
|
141
172
|
|
|
142
173
|
const segment = (identifier) => identifier.replaceAll(':', '_')
|
|
143
|
-
const STORE_AREAS = (scopeId) => [['
|
|
174
|
+
const STORE_AREAS = (scopeId) => [['state', 'manifests', segment(scopeId)], ['state', 'journals', segment(scopeId)], ['state', 'locks'], ['recovery', 'objects'], ['staging']]
|
|
144
175
|
|
|
145
176
|
// Reads the trusted pointer of a view and every note of its vault. Read-only:
|
|
146
177
|
// the store is only constructed when everything it would create already exists.
|
|
@@ -148,7 +179,13 @@ const STORE_AREAS = (scopeId) => [['vaults', segment(scopeId)], ['state', 'manif
|
|
|
148
179
|
// store could be constructed, even before a first generation.
|
|
149
180
|
export function readBackTrustedGeneration({ workspaceRoot, workspaceId, scopeId, repositoryRoots, createRecoveryStore = createStore }) {
|
|
150
181
|
const unreadable = (reason) => ({ readable: false, reason, generationId: null, intact: false, noteCount: 0, differing: 0, missing: 0 })
|
|
151
|
-
|
|
182
|
+
// The view's vault wherever it is: allocated for the view, or under the data root.
|
|
183
|
+
let vault
|
|
184
|
+
try { vault = vaultRootFor({ workspaceRoot, workspaceId, scopeId }) } catch (error) {
|
|
185
|
+
if (typeof error?.code === 'string') return unreadable(error.code)
|
|
186
|
+
throw error
|
|
187
|
+
}
|
|
188
|
+
if (!fs.existsSync(vault.path) || !STORE_AREAS(scopeId).every((parts) => fs.existsSync(path.join(workspaceRoot, ...parts)))) return unreadable('no-published-vault')
|
|
152
189
|
let store
|
|
153
190
|
let trusted
|
|
154
191
|
try {
|
|
@@ -204,8 +241,14 @@ export function scopeReport({ workspace, scopeId, repositoryRoots, serviceState,
|
|
|
204
241
|
reason = edited ? 'vault-differs-from-trusted-generation' : verification.readable ? 'trusted-generation-differs' : verification.reason
|
|
205
242
|
} else if (serviceState !== 'healthy') { outcome = 'stale-readable'; reason = serviceState === 'busy' ? 'maintenance-busy-not-rechecked' : 'maintenance-not-running' }
|
|
206
243
|
else { outcome = 'current'; reason = entry.reason }
|
|
244
|
+
// Where the view's vault is, published or not: allocated for the view, or under the data root.
|
|
245
|
+
let vault
|
|
246
|
+
try { const found = vaultRootFor({ ...workspace, scopeId }); vault = { path: found.path, origin: found.origin } } catch (error) {
|
|
247
|
+
if (typeof error?.code !== 'string') throw error
|
|
248
|
+
vault = { path: null, origin: 'unreadable', reason: error.code }
|
|
249
|
+
}
|
|
207
250
|
return {
|
|
208
|
-
scopeId, ...describeOutcome(outcome), next: nextStep(outcome, reason), reason,
|
|
251
|
+
scopeId, ...describeOutcome(outcome), next: nextStep(outcome, reason), reason, vault,
|
|
209
252
|
freshness: entry === null ? null : { state: entry.state, reason: entry.reason, verified: entry.verified, generationId: entry.generationId, preparedGenerationId: entry.preparedGenerationId, heldNoteCount: entry.heldNotes.length, retainedEdits: entry.retainedEdits, checkedAt: entry.checkedAt },
|
|
210
253
|
readBack: { readable: verification.readable, reason: verification.reason, generationId: verification.generationId, intact: verification.intact, noteCount: verification.noteCount, differing: verification.differing, missing: verification.missing },
|
|
211
254
|
pendingEdits: pendingSummary(stateStore, scopeId, applyAvailable),
|
|
@@ -226,6 +269,9 @@ const defaultSleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms)
|
|
|
226
269
|
const READABLE = new Set(['stale-readable', 'held-for-your-edit', 'updating', 'publisher-conflict'])
|
|
227
270
|
// After a launch: the app is still starting, or still opening the vault it was asked for.
|
|
228
271
|
const NOT_UP_YET = new Set(['version-unknown', 'no-vault-open'])
|
|
272
|
+
// After a launch: how many rounds in a row the command line must be silent, with only the plugin giving the version,
|
|
273
|
+
// before `open` says the command line is what is missing.
|
|
274
|
+
const SILENT_ROUNDS = 2
|
|
229
275
|
const REGISTRY_OPERATIONS = ['listThroughApp', 'registerThroughApp', 'readSettings', 'registerInSettings']
|
|
230
276
|
|
|
231
277
|
const attempt = async (operation) => { try { return await operation() } catch { return null } }
|
|
@@ -250,28 +296,29 @@ const attempt = async (operation) => { try { return await operation() } catch {
|
|
|
250
296
|
// (`vault-inside-another-vault`): the app would show its notes in that vault
|
|
251
297
|
// too, and a call run in its folder would reach that vault.
|
|
252
298
|
//
|
|
253
|
-
// { ok: true, path, how, vaults } with the path the app knows the vault
|
|
254
|
-
//
|
|
299
|
+
// { ok: true, path, how, open, vaults } with the path the app knows the vault
|
|
300
|
+
// by, whether its list flags it open (an app started plainly reopens it then),
|
|
301
|
+
// and the list it was found in, or { ok: false, reason }.
|
|
255
302
|
//
|
|
256
303
|
// The window the app opens for an added vault takes the next command-line
|
|
257
304
|
// call while it may still be loading, and can then answer that a command
|
|
258
305
|
// does not exist yet: the list is asked again, a bounded number of times,
|
|
259
306
|
// before an addition counts as not verified.
|
|
260
307
|
const VERIFY_ATTEMPTS = 10
|
|
261
|
-
async function ensureAppKnowsVault({ registry, observation, vaultRoot, sleep, pollMs }) {
|
|
308
|
+
async function ensureAppKnowsVault({ registry, observation, vaultRoot, sleep, pollMs, keepBackups = [] }) {
|
|
262
309
|
const inside = (vaults) => enclosingVaults({ vaults, vaultRoot }).length > 0
|
|
263
310
|
if (observation.noVaultOpen === true) {
|
|
264
311
|
const settings = await attempt(() => registry.readSettings())
|
|
265
312
|
const vaults = settings?.ok === true ? settings.vaults : null
|
|
266
313
|
const entry = findVaultEntry(vaults, vaultRoot)
|
|
267
|
-
if (entry) return { ok: true, path: entry.path, how: 'listed', vaults }
|
|
314
|
+
if (entry) return { ok: true, path: entry.path, how: 'listed', open: entry.open === true, vaults }
|
|
268
315
|
return { ok: false, reason: inside(vaults) ? 'vault-inside-another-vault' : observation.fromPlugin === true ? 'vault-open-cli-silent' : 'no-vault-open' }
|
|
269
316
|
}
|
|
270
317
|
if (observation.answering === true) {
|
|
271
318
|
const listed = await attempt(() => registry.listThroughApp())
|
|
272
319
|
if (listed?.answered !== true) return { ok: false, reason: listed?.reason === 'no-vault-open' ? 'no-vault-open' : 'app-did-not-list-its-vaults' }
|
|
273
320
|
const known = findVaultEntry(listed.vaults, vaultRoot)
|
|
274
|
-
if (known) return { ok: true, path: known.path, how: 'listed', vaults: listed.vaults }
|
|
321
|
+
if (known) return { ok: true, path: known.path, how: 'listed', open: known.open === true, vaults: listed.vaults }
|
|
275
322
|
if (inside(listed.vaults)) return { ok: false, reason: 'vault-inside-another-vault' }
|
|
276
323
|
const asked = await attempt(() => registry.registerThroughApp({ vaultRoot }))
|
|
277
324
|
if (asked?.answered !== true && asked?.reason === 'no-vault-open') return { ok: false, reason: 'no-vault-open' }
|
|
@@ -280,16 +327,30 @@ async function ensureAppKnowsVault({ registry, observation, vaultRoot, sleep, po
|
|
|
280
327
|
for (let attempts = 1; ; attempts += 1) {
|
|
281
328
|
const again = await attempt(() => registry.listThroughApp())
|
|
282
329
|
const added = again?.answered === true ? findVaultEntry(again.vaults, vaultRoot) : null
|
|
283
|
-
if (added) return { ok: true, path: added.path, how: 'added-through-app', vaults: again.vaults }
|
|
330
|
+
if (added) return { ok: true, path: added.path, how: 'added-through-app', open: true, vaults: again.vaults }
|
|
284
331
|
if (attempts >= VERIFY_ATTEMPTS) return { ok: false, reason: asked?.answered === true ? 'registration-not-verified' : 'addition-not-answered' }
|
|
285
332
|
await sleep(pollMs)
|
|
286
333
|
}
|
|
287
334
|
}
|
|
288
|
-
const written = await attempt(() => registry.registerInSettings({ vaultRoot }))
|
|
335
|
+
const written = await attempt(() => registry.registerInSettings({ vaultRoot, keepBackups }))
|
|
289
336
|
if (written === null) return { ok: false, reason: 'obsidian-settings-unwritable' }
|
|
290
337
|
if (written.ok !== true) return { ok: false, reason: typeof written.code === 'string' ? written.code : 'obsidian-settings-unwritable' }
|
|
291
|
-
|
|
292
|
-
return { ok:
|
|
338
|
+
const settings = settingsWritten(written)
|
|
339
|
+
if (written.confirmed !== true) return { ok: false, reason: written.reason === 'registration-not-read-back' ? 'registration-not-read-back' : 'app-started-during-registration', ...(settings === null ? {} : { settings }) }
|
|
340
|
+
const how = written.registered === 'created' ? 'created-settings' : written.registered === 'already' ? 'listed' : 'added-to-settings'
|
|
341
|
+
// A vault already listed keeps its own open flag; one written here is written flagged open.
|
|
342
|
+
return { ok: true, path: written.entry.path, how, open: written.registered === 'already' ? written.entry.open === true : true, vaults: written.vaults, ...(settings === null ? {} : { settings }) }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// What was written to Obsidian's settings file, for the answer to show: null when nothing was.
|
|
346
|
+
function settingsWritten(written) {
|
|
347
|
+
const created = written.registered === 'created'
|
|
348
|
+
const cliTurnedOn = written.cliTurnedOn === true
|
|
349
|
+
if (!created && written.registered !== 'written' && !cliTurnedOn) return null
|
|
350
|
+
return {
|
|
351
|
+
...(typeof written.file === 'string' ? { file: written.file } : {}), created, ...(created ? { directoryCreated: written.created?.directory === true } : {}),
|
|
352
|
+
vaultAdded: written.registered !== 'already', cliTurnedOn, ...(typeof written.backupPath === 'string' ? { backupPath: written.backupPath } : {}),
|
|
353
|
+
}
|
|
293
354
|
}
|
|
294
355
|
|
|
295
356
|
// Starts or reconnects the owned service, asks it for a tick, reads the view
|
|
@@ -297,7 +358,7 @@ async function ensureAppKnowsVault({ registry, observation, vaultRoot, sleep, po
|
|
|
297
358
|
// again for a view the app kept from being published, and reports one outcome.
|
|
298
359
|
export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIMITIVES, lifecycleRules = LIFECYCLE_PRIMITIVES) {
|
|
299
360
|
const {
|
|
300
|
-
loadProject, dataRoot, scopeId: requestedScope, appProbe, launcher, registry, service = {}, consent, allowStale = false, extensions = null,
|
|
361
|
+
loadProject, dataRoot, scopeId: requestedScope, appProbe, launcher, registry, quitter = null, restartApp = false, service = {}, consent, allowStale = false, extensions = null,
|
|
301
362
|
tickTimeoutMs = 120 * 1000, appWaitMs = 30 * 1000, appPollMs = 500, sleep = defaultSleep, monotonic = () => Date.now(),
|
|
302
363
|
env = process.env, platform = process.platform, probeTimeoutMs,
|
|
303
364
|
} = options
|
|
@@ -311,9 +372,13 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
|
|
|
311
372
|
const project = loadProject()
|
|
312
373
|
const enablement = readObsidianEnablement(project)
|
|
313
374
|
const applyAvailable = extensions !== null && extensions.applyOperation() !== UNAVAILABLE_APPLY_OPERATION
|
|
375
|
+
// A restart this run made, and what it wrote to Obsidian's settings: every answer after them shows both.
|
|
376
|
+
let restart = null
|
|
377
|
+
let settingsShown = null
|
|
314
378
|
const finish = (outcome, extra = {}) => {
|
|
315
379
|
const { afterOpen = false, ...shown } = extra
|
|
316
|
-
|
|
380
|
+
const next = restart?.quit === true && restart.startedAgain === false ? REASON_NEXT['app-not-started-again'] : nextStep(outcome, shown.reason, { afterOpen })
|
|
381
|
+
return { ok: outcome === 'current', ...describeOutcome(outcome), next, launched: false, ...shown, ...(restart === null ? {} : { restart }), ...(settingsShown === null ? {} : { obsidianSettings: settingsShown }) }
|
|
317
382
|
}
|
|
318
383
|
if (enablement.state === 'disabled') return finish('disabled', { reason: enablement.reason, scopeId: requestedScope ?? null })
|
|
319
384
|
const scopeId = resolveScope(enablement, requestedScope)
|
|
@@ -323,6 +388,8 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
|
|
|
323
388
|
// release, under the consent already recorded, when a tick is asked of it (requestServiceTick).
|
|
324
389
|
let status = await serviceStatus(lifecycle, lifecycleRules)
|
|
325
390
|
if (status.state === 'busy') return finish('busy', { scopeId, reason: status.reason, service: { state: status.state } })
|
|
391
|
+
// How a login item took part in starting the service (started through it, refreshed, or not used and why).
|
|
392
|
+
let loginItem = null
|
|
326
393
|
if (status.state !== 'healthy') {
|
|
327
394
|
let started
|
|
328
395
|
try {
|
|
@@ -331,8 +398,10 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
|
|
|
331
398
|
if (!(error instanceof ObsidianMaintenanceRefusal)) throw error
|
|
332
399
|
return finish('service-unavailable', { scopeId, reason: error.code, service: { state: status.state } })
|
|
333
400
|
}
|
|
334
|
-
|
|
335
|
-
|
|
401
|
+
loginItem = started.loginItem ?? null
|
|
402
|
+
const item = loginItem === null ? {} : { loginItem }
|
|
403
|
+
if (started.state === 'busy') return finish('busy', { scopeId, reason: started.reason, service: { state: 'busy', started: started.started, ...item } })
|
|
404
|
+
if (started.state !== 'healthy') return finish('service-unavailable', { scopeId, reason: started.reason ?? started.state, service: { state: started.state, ...item } })
|
|
336
405
|
status = started
|
|
337
406
|
}
|
|
338
407
|
let runtimeId = status.record?.runtimeId ?? null
|
|
@@ -346,7 +415,7 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
|
|
|
346
415
|
const asked = await requestServiceTick({ ...lifecycle, tickTimeoutMs, scopeId, service }, lifecycleRules)
|
|
347
416
|
if (asked.restarted) restarted = asked.restarted
|
|
348
417
|
if (typeof asked.runtimeId === 'string') runtimeId = asked.runtimeId
|
|
349
|
-
const serviceShown = { state: asked.state, runtimeId, ...(restarted === null ? {} : { restarted }) }
|
|
418
|
+
const serviceShown = { state: asked.state, runtimeId, ...(restarted === null ? {} : { restarted }), ...(loginItem === null ? {} : { loginItem }) }
|
|
350
419
|
if (asked.state === 'busy') return { answer: { outcome: 'busy', extra: { scopeId, reason: asked.reason, service: serviceShown } } }
|
|
351
420
|
if (!asked.requested) return { answer: { outcome: 'service-unavailable', extra: { scopeId, reason: asked.reason, service: serviceShown } } }
|
|
352
421
|
let view = report()
|
|
@@ -372,8 +441,45 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
|
|
|
372
441
|
if (view.outcome !== 'current' && !readable && !keptByApp) return finish(view.outcome, common)
|
|
373
442
|
|
|
374
443
|
// 3. The app, before anything is launched. With no vault open it answers nothing; a vault it already lists can still be opened by path.
|
|
375
|
-
|
|
444
|
+
let before = qualifyApp(await inspectApp(appProbe), { requireVersion: false })
|
|
376
445
|
const app = (qualification) => ({ outcome: qualification.outcome, reason: qualification.reason, version: qualification.version, floor: qualification.floor })
|
|
446
|
+
|
|
447
|
+
// A running app that cannot be reached (`state`: its command line is off, or it has no vault open and does not list
|
|
448
|
+
// this one) is quit, only when the person asked for it. { quit: true } with the app gone, or { answer } to give.
|
|
449
|
+
const QUIT_APP = { installed: true, cli: true, running: false, version: null }
|
|
450
|
+
const quitToRestart = async (state, stateOutcome) => {
|
|
451
|
+
if (!restartApp) return { answer: finish(stateOutcome, { ...common, reason: state, app: app(before) }) }
|
|
452
|
+
if (typeof quitter?.quit !== 'function' || typeof launcher.startPlain !== 'function' || typeof registry.backupSettings !== 'function') return { answer: finish(stateOutcome, { ...common, reason: 'restart-unavailable', app: app(before) }) }
|
|
453
|
+
// Read-only, before anything is sent: the write the restart is for can succeed. What it would refuse is answered
|
|
454
|
+
// now, with the app left running.
|
|
455
|
+
const notRestarted = (outcome, reason) => { restart = { asked: true, state, quit: false, signalled: false, signals: 0, reason }; return { answer: finish(outcome, { ...common, reason, app: app(before) }) } }
|
|
456
|
+
if (typeof view.vaultRoot !== 'string') return notRestarted('not-prepared', 'no-vault-folder')
|
|
457
|
+
const outlook = settingsWriteOutlook({ settings: await attempt(() => registry.readSettings()), vaultRoot: view.vaultRoot })
|
|
458
|
+
if (!outlook.ok) return notRestarted('launch-failed', outlook.code)
|
|
459
|
+
// A copy of the settings as they are, kept beside them once the main process is proven, right before the first
|
|
460
|
+
// signal; a copy that cannot be kept sends nothing. An app that has shown only its starter window has no file yet:
|
|
461
|
+
// there is nothing to copy, and the write creates it.
|
|
462
|
+
const quit = await attempt(() => quitter.quit({ beforeSignal: outlook.create === true ? null : () => registry.backupSettings() }))
|
|
463
|
+
restart = {
|
|
464
|
+
asked: true, state, quit: quit?.quit === true, signalled: quit?.signalled === true, ...(Number.isSafeInteger(quit?.pid) ? { pid: quit.pid } : {}),
|
|
465
|
+
signals: Number.isSafeInteger(quit?.signals) ? quit.signals : 0, ...(typeof quit?.settingsCopy === 'string' ? { settingsCopy: quit.settingsCopy } : {}),
|
|
466
|
+
}
|
|
467
|
+
if (quit?.quit === true) { before = qualifyApp(QUIT_APP, { requireVersion: false }); return { quit: true } }
|
|
468
|
+
restart.reason = typeof quit?.reason === 'string' ? quit.reason : 'app-did-not-quit'
|
|
469
|
+
if (typeof quit?.detail === 'string') restart.detail = quit.detail
|
|
470
|
+
// An app that was signalled and has not gone is neither started again nor written for: the person decides.
|
|
471
|
+
return { answer: finish(quit?.signalled === true ? 'launch-failed' : stateOutcome, { ...common, reason: restart.reason, app: app(before) }) }
|
|
472
|
+
}
|
|
473
|
+
// After a restart, every answer given before the launch starts the app again as it was: plainly, so it reopens the
|
|
474
|
+
// vaults it had open. Nothing was added for this view.
|
|
475
|
+
const giveBack = async () => {
|
|
476
|
+
if (restart?.quit !== true || restart.startedAgain !== undefined) return
|
|
477
|
+
restart.startedAgain = (await attempt(() => launcher.startPlain())) === true
|
|
478
|
+
}
|
|
479
|
+
if (before.reason === 'cli-turned-off') {
|
|
480
|
+
const stopped = await quitToRestart('cli-turned-off', before.outcome)
|
|
481
|
+
if (stopped.answer) return stopped.answer
|
|
482
|
+
}
|
|
377
483
|
const noVaultOpen = before.reason === 'no-vault-open'
|
|
378
484
|
if (!rules.appQualifies(before) && !noVaultOpen) {
|
|
379
485
|
// No app answered and none is known to run (a process table that could not be read, on Linux any app on a system
|
|
@@ -386,19 +492,27 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
|
|
|
386
492
|
// call about it then reaches it and no other vault: in its folder, or by its id when a vault listed at a folder
|
|
387
493
|
// above it would take a call run there.
|
|
388
494
|
const { vaultRoot } = view
|
|
389
|
-
if (typeof vaultRoot !== 'string') return finish('not-prepared', { ...common, reason: 'no-vault-folder', app: app(before) })
|
|
495
|
+
if (typeof vaultRoot !== 'string') { await giveBack(); return finish('not-prepared', { ...common, reason: 'no-vault-folder', app: app(before) }) }
|
|
390
496
|
// A version Atelier's plugin reported is not the command line answering (withPluginReportedVersion).
|
|
391
497
|
const fromPlugin = before.versionSource === 'plugin'
|
|
392
|
-
const
|
|
498
|
+
const knowVault = () => ensureAppKnowsVault({ registry, observation: { answering: typeof before.version === 'string' && !fromPlugin, noVaultOpen: before.reason === 'no-vault-open' || fromPlugin, fromPlugin }, vaultRoot, sleep, pollMs: appPollMs, keepBackups: typeof restart?.settingsCopy === 'string' ? [restart.settingsCopy] : [] })
|
|
499
|
+
let known = await knowVault()
|
|
500
|
+
// With no vault open, a vault the app does not list cannot be added while it runs: a restart the person asked for.
|
|
501
|
+
if (!known.ok && known.reason === 'no-vault-open' && restart === null) {
|
|
502
|
+
const stopped = await quitToRestart('no-vault-open', 'app-version-unsupported')
|
|
503
|
+
if (stopped.answer) return stopped.answer
|
|
504
|
+
known = await knowVault()
|
|
505
|
+
}
|
|
506
|
+
if (known.settings) settingsShown = known.settings
|
|
393
507
|
// An app that answered its version and then closed its last vault window is one with no vault open; one that holds
|
|
394
508
|
// this vault open but whose command line does not answer has no usable command line.
|
|
395
|
-
if (!known.ok) return finish(known.reason === 'no-vault-open' ? 'app-version-unsupported' : known.reason === 'vault-open-cli-silent' ? 'app-cli-unavailable' : 'launch-failed', { ...common, reason: known.reason, app: app(before) })
|
|
509
|
+
if (!known.ok) { await giveBack(); return finish(known.reason === 'no-vault-open' ? 'app-version-unsupported' : known.reason === 'vault-open-cli-silent' ? 'app-cli-unavailable' : 'launch-failed', { ...common, reason: known.reason, app: app(before) }) }
|
|
396
510
|
const registration = { how: known.how }
|
|
397
511
|
const route = vaultRoute({ vaults: known.vaults, vaultRoot })
|
|
398
512
|
// Open in several windows, one per entry of the list that names its folder: each holds the vault, and a publication
|
|
399
513
|
// coordinates with one only. Nothing is launched; the entries are named.
|
|
400
|
-
if (route.how === 'duplicated') return finish('publisher-conflict', { ...common, reason: 'vault-open-in-several-windows', duplicates: route.entries, app: app(before), registration })
|
|
401
|
-
if (route.how !== 'folder' && route.how !== 'id') return finish('launch-failed', { ...common, reason: route.how === 'ambiguous' ? 'vault-inside-another-vault' : 'registration-not-verified', app: app(before), registration })
|
|
514
|
+
if (route.how === 'duplicated') { await giveBack(); return finish('publisher-conflict', { ...common, reason: 'vault-open-in-several-windows', duplicates: route.entries, app: app(before), registration }) }
|
|
515
|
+
if (route.how !== 'folder' && route.how !== 'id') { await giveBack(); return finish('launch-failed', { ...common, reason: route.how === 'ambiguous' ? 'vault-inside-another-vault' : 'registration-not-verified', app: app(before), registration }) }
|
|
402
516
|
|
|
403
517
|
// 5. Launch, then wait, bounded, for the app to answer for exactly this vault.
|
|
404
518
|
let launch
|
|
@@ -408,11 +522,21 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
|
|
|
408
522
|
// otherwise the vault's own path, which the app matches exactly since the folder is listed.
|
|
409
523
|
const target = route.how === 'id' ? { vaultId: route.id, vaultPath: known.path } : { vaultPath: known.path }
|
|
410
524
|
try { launch = await launcher.open({ vaultRoot: known.path, ...target, appRunning: before.running === true }) } catch { launch = { launched: false, reason: 'launcher-threw' } }
|
|
525
|
+
// A restart ends here: the app was started again, or could not be (the next step then says to start it).
|
|
526
|
+
if (restart?.quit === true) restart.startedAgain = launch?.launched === true
|
|
411
527
|
if (launch?.launched !== true) return finish('launch-failed', { ...common, reason: typeof launch?.reason === 'string' ? launch.reason : 'launcher-refused', app: app(before), registration })
|
|
412
528
|
const deadline = monotonic() + appWaitMs
|
|
413
529
|
let after = before
|
|
414
530
|
let vault = { answered: false, indexReady: false }
|
|
531
|
+
// Rounds in a row in which only Atelier's plugin gave the version and the command line did not answer for the vault.
|
|
532
|
+
let silentRounds = 0
|
|
533
|
+
// A quit app started plainly reopens only the vaults its list flags open. A listed vault that is closed opens through
|
|
534
|
+
// the link alone, so a link the started app did not take (it answered its tool, then refused the link) is handed
|
|
535
|
+
// once more, to the app that now runs, one round later, before the vault counts as not answering.
|
|
536
|
+
let linkRetry = launch.reason === 'app-started-link-not-taken' && known.open !== true ? 'due' : 'none'
|
|
537
|
+
let rounds = 0
|
|
415
538
|
for (;;) {
|
|
539
|
+
rounds += 1
|
|
416
540
|
after = qualifyApp(await inspectApp(appProbe), { requireVersion: true })
|
|
417
541
|
// A running app below the floor is final; an app that has not come up yet, or not yet opened a vault, is asked again.
|
|
418
542
|
if (!rules.appQualifies(after) && !NOT_UP_YET.has(after.reason)) return finish(after.outcome, { ...common, launched: true, reason: after.reason, app: app(after), registration })
|
|
@@ -420,13 +544,23 @@ export async function openScopeForOracleTests(options = {}, rules = OPENING_PRIM
|
|
|
420
544
|
try { vault = await appProbe.vaultState({ vaultRoot, route }) } catch { vault = { answered: false, indexReady: false } }
|
|
421
545
|
if (vault?.answered === true && vault.indexReady === true) break
|
|
422
546
|
// Only the command line answers for a vault. An app whose version still only Atelier's plugin reports gives no
|
|
423
|
-
// answer there, and waiting changes nothing: its command line is what is missing, not a launch.
|
|
424
|
-
|
|
425
|
-
|
|
547
|
+
// answer there, and waiting changes nothing: its command line is what is missing, not a launch. One such round
|
|
548
|
+
// is not enough: a tool call that timed out while the app was busy opening the window looks the same once, so
|
|
549
|
+
// the answer is given at the second round in a row.
|
|
550
|
+
silentRounds = vault?.answered !== true && after.versionSource === 'plugin' ? silentRounds + 1 : 0
|
|
551
|
+
if (silentRounds >= SILENT_ROUNDS) return finish('app-cli-unavailable', { ...common, launched: true, reason: 'vault-open-cli-silent', app: app(after), registration })
|
|
552
|
+
if (linkRetry === 'due' && rounds >= 2 && vault?.answered !== true) {
|
|
553
|
+
linkRetry = 'done'
|
|
554
|
+
// Through the app's tool only: this `open` started the app, so the link never goes to the operating system.
|
|
555
|
+
try { await launcher.open({ vaultRoot: known.path, ...target, appRunning: true, startedByThisOpen: true }) } catch { /* the wait decides */ }
|
|
556
|
+
}
|
|
557
|
+
} else silentRounds = 0
|
|
426
558
|
if (monotonic() >= deadline) break
|
|
427
559
|
await sleep(appPollMs)
|
|
428
560
|
}
|
|
429
561
|
if (!rules.appQualifies(after)) return finish(after.outcome, { ...common, launched: true, reason: after.reason, app: app(after), registration })
|
|
562
|
+
// The wait ended on a silent round with only the plugin giving the version: the command line is what did not answer.
|
|
563
|
+
if (vault?.answered !== true && silentRounds > 0) return finish('app-cli-unavailable', { ...common, launched: true, reason: 'vault-open-cli-silent', app: app(after), registration })
|
|
430
564
|
if (vault?.answered !== true) return finish('launch-failed', { ...common, launched: true, reason: 'app-did-not-answer-for-this-vault', app: app(after), registration })
|
|
431
565
|
if (vault.indexReady !== true) return finish('indexing', { ...common, launched: true, reason: 'metadata-cache-not-ready', app: app(after), registration })
|
|
432
566
|
|