@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,124 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { readRegularTextNoFollow } from '../../project/private-state.mjs'
|
|
4
|
+
import { OBSIDIAN_EXT_KEY } from '../../projection/obsidian/contracts.mjs'
|
|
5
|
+
import { sha256Digest } from '../../projection/obsidian/materialize/byte-lens.mjs'
|
|
6
|
+
import { readFileBytes, readFileDigest } from '../../projection/obsidian/recovery/store.mjs'
|
|
7
|
+
import { viewVaultRoot } from './plugin-choice.mjs'
|
|
8
|
+
|
|
9
|
+
// Whether the plugin files a view's committed generation pins are on disk as
|
|
10
|
+
// pinned. A person may remove one, change one, or repair a path Atelier had to
|
|
11
|
+
// leave (a folder where a file goes, say). The publisher then publishes the
|
|
12
|
+
// same generation again (publisher.mjs), but only when the view is prepared
|
|
13
|
+
// again, which nothing else asks for while its sources are unchanged.
|
|
14
|
+
//
|
|
15
|
+
// `observe(scopeIds)` looks at each view once per tick and returns the views
|
|
16
|
+
// to prepare again: those whose files differ from their pins and look
|
|
17
|
+
// different from the last look. A drift Atelier leaves for the person (a link
|
|
18
|
+
// where a file goes) is asked about once, not at every tick; a change to it is
|
|
19
|
+
// asked about again. Where the plugin is turned off in the vault, a pinned
|
|
20
|
+
// file that is gone is no drift: the publisher never makes one again.
|
|
21
|
+
// `settle(scopeIds)` looks again after each tick, and takes as seen what a
|
|
22
|
+
// publication of that tick could not change, so a drift publishing cannot
|
|
23
|
+
// repair is not published a second time at the next look (at a service's
|
|
24
|
+
// start, say).
|
|
25
|
+
|
|
26
|
+
const segment = (identifier) => identifier.replaceAll(':', '_')
|
|
27
|
+
const TURNED_OFF = 'turned-off-in-this-vault'
|
|
28
|
+
|
|
29
|
+
// The plugin record of the view's committed manifest, read as the store reads
|
|
30
|
+
// it: the pointer names the manifest and its digest. Null when there is none.
|
|
31
|
+
function pinnedPlugin(workspaceRoot, workspaceId, scopeId) {
|
|
32
|
+
const manifests = path.join(workspaceRoot, 'state', 'manifests', segment(scopeId))
|
|
33
|
+
try {
|
|
34
|
+
const pointer = JSON.parse(readRegularTextNoFollow(path.join(manifests, 'current.json')))
|
|
35
|
+
if (pointer?.workspaceId !== workspaceId || pointer?.scopeId !== scopeId || typeof pointer.manifestFile !== 'string' || pointer.manifestFile.includes('/')) return null
|
|
36
|
+
const bytes = readFileBytes(path.join(manifests, pointer.manifestFile))
|
|
37
|
+
if (sha256Digest(bytes) !== pointer.manifestDigest) return null
|
|
38
|
+
const owned = JSON.parse(bytes.toString('utf8'))?.ext?.[OBSIDIAN_EXT_KEY]?.settings?.pluginOwned
|
|
39
|
+
if (!Array.isArray(owned?.files)) return null
|
|
40
|
+
return { manifest: pointer.manifestDigest, files: owned.files.filter((file) => typeof file?.path === 'string' && typeof file?.digest === 'string'), off: owned.withheldBecause === TURNED_OFF }
|
|
41
|
+
} catch {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// What one pinned path holds: its digest, 'absent', or 'unreadable' (a link, a
|
|
47
|
+
// folder where a file goes, a path nobody may read).
|
|
48
|
+
function onDisk(vaultRoot, relativePath) {
|
|
49
|
+
let current = vaultRoot
|
|
50
|
+
for (const part of relativePath.split('/').slice(0, -1)) {
|
|
51
|
+
current = path.join(current, part)
|
|
52
|
+
let stat
|
|
53
|
+
try { stat = fs.lstatSync(current, { throwIfNoEntry: false }) } catch { return 'unreadable' }
|
|
54
|
+
if (!stat) return 'absent'
|
|
55
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return 'unreadable'
|
|
56
|
+
}
|
|
57
|
+
try { return readFileDigest(path.join(vaultRoot, relativePath)) ?? 'absent' } catch { return 'unreadable' }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A look, as it is compared: the vault root, and each pinned file with its pin and what the disk holds.
|
|
61
|
+
const signatureOf = (record) => JSON.stringify([record.root, record.files.map((file) => file.digest), record.files.map((file) => file.state)])
|
|
62
|
+
// Never what a look finds on disk: a path recorded so is asked about at the next look whatever it holds then.
|
|
63
|
+
const NOT_SEEN = 'not-seen'
|
|
64
|
+
|
|
65
|
+
export function createPluginDriftObserver({ workspaceRoot, workspaceId }) {
|
|
66
|
+
// Per view: the last look ({ root, files: [{ path, digest, state }], manifest }).
|
|
67
|
+
const looked = new Map()
|
|
68
|
+
// The views the last observe asked to be prepared again: the tick that follows publishes them.
|
|
69
|
+
let asked = new Set()
|
|
70
|
+
// What the pinned plugin files of a view look like on disk now; null when its committed generation pins none.
|
|
71
|
+
const look = (scopeId) => {
|
|
72
|
+
const pinned = pinnedPlugin(workspaceRoot, workspaceId, scopeId)
|
|
73
|
+
if (pinned === null || pinned.files.length === 0) return null
|
|
74
|
+
// The view's vault wherever it is; one that cannot be found is refused by the engine, not observed here.
|
|
75
|
+
let vaultRoot
|
|
76
|
+
try { vaultRoot = viewVaultRoot(workspaceRoot, scopeId, workspaceId) } catch (error) { if (typeof error?.code !== 'string') throw error; return null }
|
|
77
|
+
let root = 'absent'
|
|
78
|
+
try { const stat = fs.lstatSync(vaultRoot); root = stat.isSymbolicLink() ? 'link' : (stat.mode & 0o777).toString(8) } catch { root = 'absent' }
|
|
79
|
+
const files = pinned.files.map((file) => ({ path: file.path, digest: file.digest, state: onDisk(vaultRoot, file.path) }))
|
|
80
|
+
const drifted = (file) => file.state !== file.digest && !(pinned.off && file.state === 'absent')
|
|
81
|
+
return { root, files, manifest: pinned.manifest, drifted, differs: files.some(drifted) }
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
observe(scopeIds) {
|
|
85
|
+
const drifted = []
|
|
86
|
+
for (const scopeId of scopeIds) {
|
|
87
|
+
const seen = look(scopeId)
|
|
88
|
+
if (seen === null) { looked.delete(scopeId); continue }
|
|
89
|
+
const last = looked.get(scopeId)
|
|
90
|
+
if (seen.differs && (last === undefined || signatureOf(last) !== signatureOf(seen))) drifted.push(scopeId)
|
|
91
|
+
looked.set(scopeId, { root: seen.root, files: seen.files, manifest: seen.manifest })
|
|
92
|
+
}
|
|
93
|
+
asked = new Set(drifted)
|
|
94
|
+
return drifted
|
|
95
|
+
},
|
|
96
|
+
// After a tick. A view this tick published (one the last observe asked for, one whose committed generation
|
|
97
|
+
// changed since the last look, or one never looked at before) had its plugin files written or left for the person
|
|
98
|
+
// by its publisher. Of what the disk shows now, only what that publication could not change is taken as seen,
|
|
99
|
+
// without asking: a file as it pins it, or one that holds what it held before the tick under the same pin and the
|
|
100
|
+
// same vault root. So a drift publishing cannot repair (the data file of a vault root that is a link, say) is not
|
|
101
|
+
// asked about again once it was asked about under its pin. Anything else is not taken as seen, and the next look
|
|
102
|
+
// asks about it: a file another writer removed or changed after the publication, while the tick went on, or
|
|
103
|
+
// brought back to what it held under a pin the tick replaced; a file the publication left for the person, which
|
|
104
|
+
// the person may have repaired before the tick ended; and any file, when the vault root changed during the tick.
|
|
105
|
+
// Asking about a file that is still left writes no vault file: the publisher does not publish a committed
|
|
106
|
+
// generation again for it (pluginFilesDrifted). Any other view keeps its last look.
|
|
107
|
+
settle(scopeIds) {
|
|
108
|
+
for (const scopeId of scopeIds) {
|
|
109
|
+
const seen = look(scopeId)
|
|
110
|
+
if (seen === null) { looked.delete(scopeId); continue }
|
|
111
|
+
const last = looked.get(scopeId)
|
|
112
|
+
if (!asked.has(scopeId) && last !== undefined && last.manifest === seen.manifest) continue
|
|
113
|
+
// Keyed by path and pin: what a file held under a pin this tick replaced says nothing about the new one.
|
|
114
|
+
const key = (file) => `${file.path}\u0000${file.digest}`
|
|
115
|
+
const before = new Map((last?.files ?? []).map((file) => [key(file), file.state]))
|
|
116
|
+
// And only under the same vault root: a root made private during the tick changes what the publisher leaves.
|
|
117
|
+
const sameRoot = last !== undefined && last.root === seen.root
|
|
118
|
+
const files = seen.files.map((file) => (!seen.drifted(file) || (sameRoot && before.get(key(file)) === file.state) ? file : { ...file, state: NOT_SEEN }))
|
|
119
|
+
looked.set(scopeId, { root: seen.root, files, manifest: seen.manifest })
|
|
120
|
+
}
|
|
121
|
+
asked = new Set()
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { isPlainObject } from './documents.mjs'
|
|
2
|
+
|
|
3
|
+
// What `status` and `open` say about Atelier's plugin, read from the status
|
|
4
|
+
// document of the running service (`plugins`, see plugin-channel.mjs). The
|
|
5
|
+
// plugin is present for a view while it holds a live lease on it.
|
|
6
|
+
|
|
7
|
+
export const PLUGIN_PRESENCE_REASONS = Object.freeze(['live-lease', 'no-live-lease', 'turned-off-in-this-vault', 'service-not-running', 'service-reports-no-plugin-channel'])
|
|
8
|
+
|
|
9
|
+
// The two ways back, for a view whose vault turned the plugin off.
|
|
10
|
+
export const turnPluginOnNext = (scopeId) => `turn Atelier on in Obsidian (Settings, Community plugins), or run \`atelier obsidian plugin on --scope ${scopeId}\`, which restores it at the view's next publication`
|
|
11
|
+
|
|
12
|
+
// A view kept `current` while a plugin file its committed generation pins waits for an app that qualifies: the notes
|
|
13
|
+
// are verified, the plugin file (often `data.json`) is not written yet (engine.mjs). Its freshness reason says so.
|
|
14
|
+
export const PLUGIN_FILES_WAIT_FOR_APP = 'verified-by-read-back-plugin-waits-for-app'
|
|
15
|
+
export const PLUGIN_FILES_WAIT_NEXT = 'the view\'s notes are current, but a file of Atelier\'s plugin in this vault is not written yet, since Obsidian could not be coordinated with (it runs without its command line answering, say): it is written once Obsidian answers its command line or is quit, and it is retried automatically'
|
|
16
|
+
|
|
17
|
+
// The presence, with the plugin files that wait for the app when the view's freshness says so (`files`, and the next
|
|
18
|
+
// step unless the presence has its own).
|
|
19
|
+
export function withPluginFilesPending(presence, freshness) {
|
|
20
|
+
if (freshness?.state !== 'current' || freshness.reason !== PLUGIN_FILES_WAIT_FOR_APP) return presence
|
|
21
|
+
return { ...presence, files: 'waits-for-app', next: presence.next ?? PLUGIN_FILES_WAIT_NEXT }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function pluginPresenceOf(statusDocument, scopeId) {
|
|
25
|
+
if (!isPlainObject(statusDocument)) return { present: false, reason: 'service-not-running' }
|
|
26
|
+
const scopes = isPlainObject(statusDocument.plugins) && Array.isArray(statusDocument.plugins.scopes) ? statusDocument.plugins.scopes : null
|
|
27
|
+
if (scopes === null) return { present: false, reason: 'service-reports-no-plugin-channel' }
|
|
28
|
+
const entry = scopes.find((item) => isPlainObject(item) && item.scopeId === scopeId)
|
|
29
|
+
if (entry?.present === true && typeof entry.appVersion === 'string') {
|
|
30
|
+
return { present: true, reason: 'live-lease', appVersion: entry.appVersion, pluginVersion: typeof entry.pluginVersion === 'string' ? entry.pluginVersion : null, sessions: Number.isInteger(entry.sessions) ? entry.sessions : 1 }
|
|
31
|
+
}
|
|
32
|
+
if (entry?.entry === 'off') return { present: false, reason: 'turned-off-in-this-vault', next: turnPluginOnNext(scopeId) }
|
|
33
|
+
return { present: false, reason: 'no-live-lease' }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// An app probe that takes the version from the plugin when one holds the view
|
|
37
|
+
// and the command-line tool gives none: that plugin runs inside the app, so its
|
|
38
|
+
// version is the app's. A version the tool did give decides, since the tool may
|
|
39
|
+
// reach another app that holds the same vault. Whether the app is installed and
|
|
40
|
+
// has its command-line capability is still the probe's answer; that capability
|
|
41
|
+
// opens and coordinates the vault in this phase. So is whether the app runs: a
|
|
42
|
+
// lease outlives a crashed app by up to its lease time.
|
|
43
|
+
// The presence is asked at most once per `maxAgeMs`: open's wait loop asks the
|
|
44
|
+
// app far more often, and each answer about the plugin costs a round trip to
|
|
45
|
+
// the service.
|
|
46
|
+
export function withPluginReportedVersion(appProbe, readPresence, { maxAgeMs = 1000, now = () => Date.now() } = {}) {
|
|
47
|
+
let last = null
|
|
48
|
+
const presenceNow = () => {
|
|
49
|
+
if (last === null || now() - last.at >= maxAgeMs) last = { at: now(), presence: Promise.resolve().then(readPresence).catch(() => null) }
|
|
50
|
+
return last.presence
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
async inspect() {
|
|
54
|
+
const [observation, presence] = await Promise.all([appProbe.inspect(), presenceNow()])
|
|
55
|
+
// A command line that answered it is turned off stays that answer: a plugin's version never hides it.
|
|
56
|
+
if (presence?.present !== true || observation?.installed !== true || observation?.cli !== true || observation.running === false || typeof observation.version === 'string' || observation.cliOff === true) return observation
|
|
57
|
+
return { ...observation, running: true, version: presence.appVersion, noVaultOpen: false, versionSource: 'plugin' }
|
|
58
|
+
},
|
|
59
|
+
vaultState: (input) => appProbe.vaultState(input),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
3
4
|
import { executableIdentity } from './service-record.mjs'
|
|
4
5
|
|
|
5
6
|
// Whether a live PID provably runs the executable a service record names,
|
|
@@ -32,27 +33,36 @@ export function readProcessCommandLine(pid, { platform = process.platform, readF
|
|
|
32
33
|
return null
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
// Pure. The command line must name the recorded entry module,
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
// Pure. The command line must name the recorded entry module, by one of
|
|
37
|
+
// `entries` (its recorded path, and the path it was started by when that is
|
|
38
|
+
// another name for it), and a runtime identifier it carries must be the
|
|
39
|
+
// recorded one.
|
|
40
|
+
export function commandLineNamesRecord(commandLine, record, entries = [record?.executable?.path]) {
|
|
41
|
+
const names = entries.filter((entry) => typeof entry === 'string' && entry !== '')
|
|
42
|
+
if (names.length === 0 || typeof record?.runtimeId !== 'string') return false
|
|
40
43
|
if (Array.isArray(commandLine)) {
|
|
41
|
-
if (!commandLine.slice(1).includes(
|
|
44
|
+
if (!commandLine.slice(1).some((word) => names.includes(word))) return false
|
|
42
45
|
const named = commandLine.filter((word) => word.startsWith(RUNTIME_ARGUMENT)).map((word) => word.slice(RUNTIME_ARGUMENT.length))
|
|
43
46
|
return named.every((runtimeId) => runtimeId === record.runtimeId)
|
|
44
47
|
}
|
|
45
48
|
if (typeof commandLine !== 'string') return false
|
|
46
|
-
if (
|
|
49
|
+
if (!names.some((entry) => ` ${commandLine} `.includes(` ${entry} `))) return false
|
|
47
50
|
const named = [...commandLine.matchAll(/(?:^|\s)--runtime-id=(\S+)/g)].map((match) => match[1])
|
|
48
51
|
return named.every((runtimeId) => runtimeId === record.runtimeId)
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
// The recorded executable is still the bytes that were recorded, and the live
|
|
52
|
-
// process was started on it
|
|
53
|
-
|
|
55
|
+
// process was started on it: by its recorded path, or by the path the record
|
|
56
|
+
// says it was started by (a login item names the installed entry by its path),
|
|
57
|
+
// when that still leads to the recorded one.
|
|
58
|
+
export function processRunsRecordedExecutable(record, { commandLineOf = readProcessCommandLine, identityOf = executableIdentity, realpathOf = fs.realpathSync } = {}) {
|
|
54
59
|
try {
|
|
55
60
|
if (identityOf(record.executable.path).digest !== record.executable.digest) return false
|
|
56
61
|
} catch { return false }
|
|
57
|
-
|
|
62
|
+
const entries = [record.executable.path]
|
|
63
|
+
const invokedAs = record.executable.ext?.invokedAs
|
|
64
|
+
if (typeof invokedAs === 'string' && path.isAbsolute(invokedAs)) {
|
|
65
|
+
try { if (realpathOf(invokedAs) === record.executable.path) entries.push(invokedAs) } catch { /* not another name for it */ }
|
|
66
|
+
}
|
|
67
|
+
return commandLineNamesRecord(commandLineOf(record.pid), record, entries)
|
|
58
68
|
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { validateProjectConfigDoc } from '../../project/config.mjs'
|
|
4
|
+
import { MAX_EXPANSION_DEPTH, OBSIDIAN_EXT_KEY, ObsidianContractRefusal } from '../../projection/obsidian/contracts.mjs'
|
|
5
|
+
import { readObsidianEnablement } from './enablement.mjs'
|
|
6
|
+
import { ObsidianMaintenanceRefusal, refuse } from './errors.mjs'
|
|
7
|
+
|
|
8
|
+
// The views a project declares, written into its `atelier.project.json` for a
|
|
9
|
+
// person, so nobody edits JSON by hand: `view add`, and the view a project
|
|
10
|
+
// that never used Obsidian gets on its first open.
|
|
11
|
+
//
|
|
12
|
+
// The project file is committed source. Atelier changes it only when asked,
|
|
13
|
+
// shows the exact change as a diff, and never commits. It rewrites a file only
|
|
14
|
+
// when the file is in the form Atelier writes JSON in (two-space indentation,
|
|
15
|
+
// with LF or CRLF line ends, with or without a final one), so the change is
|
|
16
|
+
// the Obsidian member and nothing else; and only over the bytes it read. The
|
|
17
|
+
// workspace pointer is only ever written into an ignored `.atelier-local/`, so
|
|
18
|
+
// a project where that folder is not ignored gets the ignore line in the same
|
|
19
|
+
// change.
|
|
20
|
+
|
|
21
|
+
export const EXT_SETTINGS_SCHEMA = 'atelier-obsidian-ext-settings/v1'
|
|
22
|
+
// Every note this machine may show, in one view that is the default. Its name reads well in a command
|
|
23
|
+
// (`--scope everything`) and in a vault's name (`harbor-notes (everything)`).
|
|
24
|
+
export const DEFAULT_VIEW = Object.freeze({ scopeId: 'everything', mode: 'full', selector: Object.freeze({ all: true }) })
|
|
25
|
+
const LOCAL_STATE_LINE = '.atelier-local/'
|
|
26
|
+
const SCOPE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
27
|
+
const MAX_EXPANSION_NODES = 100000
|
|
28
|
+
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
29
|
+
|
|
30
|
+
const linesOf = (text) => { const lines = text.split(/\r?\n/); if (lines.at(-1) === '') lines.pop(); return lines }
|
|
31
|
+
// A hunk's range: the first line and the count, or the line before an empty range.
|
|
32
|
+
const range = (start, count) => (count === 0 ? `${start},0` : `${start + 1},${count}`)
|
|
33
|
+
|
|
34
|
+
// The lines that differ between two texts, as one unified hunk with up to three lines of context on each side. Enough
|
|
35
|
+
// for a change that inserts or replaces one run of lines, which is every change made here.
|
|
36
|
+
export function unifiedDiff(before, after, { label }) {
|
|
37
|
+
if (before === after) return ''
|
|
38
|
+
const [left, right] = [linesOf(before), linesOf(after)]
|
|
39
|
+
let prefix = 0
|
|
40
|
+
while (prefix < left.length && prefix < right.length && left[prefix] === right[prefix]) prefix += 1
|
|
41
|
+
let suffix = 0
|
|
42
|
+
while (suffix < left.length - prefix && suffix < right.length - prefix && left[left.length - 1 - suffix] === right[right.length - 1 - suffix]) suffix += 1
|
|
43
|
+
const context = 3
|
|
44
|
+
const start = Math.max(0, prefix - context)
|
|
45
|
+
const leftEnd = Math.min(left.length, left.length - suffix + context)
|
|
46
|
+
const rightEnd = Math.min(right.length, right.length - suffix + context)
|
|
47
|
+
const lines = [
|
|
48
|
+
`--- ${label}`, `+++ ${label}`, `@@ -${range(start, leftEnd - start)} +${range(start, rightEnd - start)} @@`,
|
|
49
|
+
...left.slice(start, prefix).map((line) => ` ${line}`),
|
|
50
|
+
...left.slice(prefix, left.length - suffix).map((line) => `-${line}`),
|
|
51
|
+
...right.slice(prefix, right.length - suffix).map((line) => `+${line}`),
|
|
52
|
+
...right.slice(right.length - suffix, rightEnd).map((line) => ` ${line}`),
|
|
53
|
+
]
|
|
54
|
+
return `${lines.join('\n')}\n`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const jsonText = (document, { eol, final }) => {
|
|
58
|
+
const text = JSON.stringify(document, null, 2)
|
|
59
|
+
return `${eol === '\n' ? text : text.replaceAll('\n', '\r\n')}${final ? eol : ''}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The form `text` is `document` written in, or null when it is in none Atelier writes.
|
|
63
|
+
function formOf(text, document) {
|
|
64
|
+
for (const eol of ['\n', '\r\n']) for (const final of [true, false]) if (text === jsonText(document, { eol, final })) return { eol, final }
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A file Atelier may replace: a regular file, not a link to one elsewhere. `bytes` is null when there is none.
|
|
69
|
+
function readRegularFile(file, { code, what }) {
|
|
70
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false })
|
|
71
|
+
if (stat === undefined) return { bytes: null, mode: null }
|
|
72
|
+
if (!stat.isFile()) refuse('project-file-not-regular', `${what} is not a regular file, so Atelier does not rewrite it; make the change by hand`, { file: path.basename(file) })
|
|
73
|
+
try { return { bytes: fs.readFileSync(file), mode: stat.mode & 0o777 } } catch (error) { return refuse(code, `${what} cannot be read`, { cause: error.code ?? 'unreadable' }) }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The text of a file's bytes, only when they are UTF-8 that decodes and encodes back to exactly them: a file in another
|
|
77
|
+
// encoding is never rewritten, since a byte that is not UTF-8 would be replaced without the diff showing it.
|
|
78
|
+
function textOf(bytes, refusal) {
|
|
79
|
+
const text = bytes.toString('utf8')
|
|
80
|
+
if (!Buffer.from(text, 'utf8').equals(bytes)) refusal()
|
|
81
|
+
return text
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The settings as enablement will read them, or a typed refusal naming what is wrong.
|
|
85
|
+
function checkedSettings(document, settings) {
|
|
86
|
+
try {
|
|
87
|
+
readObsidianEnablement({ config: { ...document, ext: { ...(isPlainObject(document.ext) ? document.ext : {}), [OBSIDIAN_EXT_KEY]: settings } } })
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof ObsidianMaintenanceRefusal || error instanceof ObsidianContractRefusal) refuse('invalid-view', 'the views would not satisfy the Obsidian settings contract; nothing was written', { errors: error.detail?.errors ?? [] })
|
|
90
|
+
throw error
|
|
91
|
+
}
|
|
92
|
+
return settings
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The project file with `settings` as its Obsidian member: { file, bytes, after, mode, diff }, `bytes` being the ones
|
|
96
|
+
// it was made from. `document` is the file as read. Refuses a file that is not in a form Atelier writes, since
|
|
97
|
+
// rewriting it would change more than the member, and names the member to add by hand.
|
|
98
|
+
function planMember({ file, bytes, before, mode, document, settings }) {
|
|
99
|
+
const form = isPlainObject(document) && (document.ext === undefined || isPlainObject(document.ext)) ? formOf(before, document) : null
|
|
100
|
+
if (form === null) {
|
|
101
|
+
refuse('project-config-format-unknown', `${path.basename(file)} is not in the form Atelier writes (two-space JSON), so rewriting it would change more than the Obsidian member; nothing was written. Add the member by hand, under "ext"`, { member: { [OBSIDIAN_EXT_KEY]: settings } })
|
|
102
|
+
}
|
|
103
|
+
const next = { ...document, ext: { ...(document.ext ?? {}), [OBSIDIAN_EXT_KEY]: settings } }
|
|
104
|
+
const errors = validateProjectConfigDoc(next)
|
|
105
|
+
if (errors.length > 0) refuse('invalid-view', 'the project configuration would not be valid; nothing was written', { errors })
|
|
106
|
+
const after = jsonText(next, form)
|
|
107
|
+
return { file, what: path.basename(file), bytes, after, mode, diff: unifiedDiff(before, after, { label: path.basename(file) }) }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The ignore line a project needs before the workspace pointer can be written:
|
|
111
|
+
// { needed, file, bytes, after, mode, diff }.
|
|
112
|
+
function planIgnore(project) {
|
|
113
|
+
if (project?.localState?.ignored === true) return { needed: false }
|
|
114
|
+
const file = path.join(project.configDir, '.gitignore')
|
|
115
|
+
const { bytes, mode } = readRegularFile(file, { code: 'gitignore-unreadable', what: 'the project\'s .gitignore' })
|
|
116
|
+
const before = bytes === null ? '' : textOf(bytes, () => refuse('gitignore-not-utf8', `the project's .gitignore is not UTF-8, so Atelier does not rewrite it; add the line ${LOCAL_STATE_LINE} to it by hand; nothing was written`, { line: LOCAL_STATE_LINE }))
|
|
117
|
+
const eol = before.includes('\r\n') ? '\r\n' : '\n'
|
|
118
|
+
const after = `${before}${before === '' || before.endsWith('\n') ? '' : eol}${LOCAL_STATE_LINE}${eol}`
|
|
119
|
+
return { needed: true, file, what: 'the project\'s .gitignore', bytes, after, mode, diff: unifiedDiff(before, after, { label: '.gitignore' }) }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The project file as it is now: its bytes and their text, its mode, and its document (null when it is not JSON).
|
|
123
|
+
function readProjectFile(project) {
|
|
124
|
+
const file = project?.configPath
|
|
125
|
+
if (typeof file !== 'string') refuse('project-config-unreadable', 'this project has no configuration file to write the view into')
|
|
126
|
+
const { bytes, mode } = readRegularFile(file, { code: 'project-config-unreadable', what: path.basename(file) })
|
|
127
|
+
if (bytes === null) refuse('project-config-unreadable', `${path.basename(file)} is not there`)
|
|
128
|
+
// Not UTF-8: in no form Atelier writes. The member is named for the person when the rest of the file can be read.
|
|
129
|
+
let before
|
|
130
|
+
try { before = textOf(bytes, () => { throw new SyntaxError('not UTF-8') }) } catch {
|
|
131
|
+
return { file, bytes, before: null, mode, document: null }
|
|
132
|
+
}
|
|
133
|
+
let document = null
|
|
134
|
+
try { document = JSON.parse(before) } catch { document = null }
|
|
135
|
+
return { file, bytes, before, mode, document }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// A folder as the repository-relative prefix of the notes a view selects, `./` and a trailing `/` dropped; null for
|
|
139
|
+
// the whole repository.
|
|
140
|
+
function folderPrefix(text) {
|
|
141
|
+
const cleaned = String(text).replace(/^(?:\.\/)+/, '').replace(/\/+$/, '')
|
|
142
|
+
if (cleaned === '' || cleaned === '.') return null
|
|
143
|
+
if (cleaned.startsWith('/') || cleaned.startsWith('~') || /^[A-Za-z]:/.test(cleaned) || cleaned.includes('\\') || cleaned.includes('\u0000') || cleaned.split('/').includes('..')) {
|
|
144
|
+
refuse('invalid-view', 'a folder is written relative to its repository\'s root, with "/", and without ".." (docs/notes, say)', { folder: String(text).slice(0, 200) })
|
|
145
|
+
}
|
|
146
|
+
return cleaned
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// `DEPTH:MAX`: follow outgoing links up to DEPTH steps from the selected notes, adding at most MAX notes in all.
|
|
150
|
+
function expansionOf(text) {
|
|
151
|
+
const match = /^([0-9]{1,2}):([0-9]{1,6})$/.exec(String(text))
|
|
152
|
+
const depth = match ? Number(match[1]) : NaN
|
|
153
|
+
const maxNodes = match ? Number(match[2]) : NaN
|
|
154
|
+
if (!(depth >= 1 && depth <= MAX_EXPANSION_DEPTH && maxNodes >= 1 && maxNodes <= MAX_EXPANSION_NODES)) {
|
|
155
|
+
refuse('invalid-view', `--expand is DEPTH:MAX, a depth from 1 to ${MAX_EXPANSION_DEPTH} and at most ${MAX_EXPANSION_NODES} notes in all (2:200, say)`)
|
|
156
|
+
}
|
|
157
|
+
return { depth, maxNodes, direction: 'outgoing', order: 'canonical-id' }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The view `view add` asks for: every note (`all`, a full view), or a scoped view of the notes under folders of one
|
|
161
|
+
// repository or with a tag, optionally expanded along outgoing links. `repositories` are the names of the project's
|
|
162
|
+
// managed repositories; `repo` may be left out when there is one. (The contract's `type` selector matches a note's
|
|
163
|
+
// file type, not its `kg.type`, so it is not offered here.)
|
|
164
|
+
export function viewFromRequest({ scopeId, all = false, folders = [], repo, tag, expand, repositories = [] }) {
|
|
165
|
+
if (typeof scopeId !== 'string' || !SCOPE_ID.test(scopeId)) refuse('usage', 'a view is named by an identifier: a letter or digit, then letters, digits, ".", "_", ":" or "-", 128 at most')
|
|
166
|
+
const kinds = [all === true, folders.length > 0, tag !== undefined].filter(Boolean).length
|
|
167
|
+
if (kinds !== 1) refuse('usage', 'a view selects its notes with exactly one of --all, --folder PATH (as often as needed) or --tag T')
|
|
168
|
+
if (repo !== undefined && folders.length === 0) refuse('usage', '--repo names the repository of --folder')
|
|
169
|
+
if (all === true && expand !== undefined) refuse('usage', 'a view of every note has nothing to expand')
|
|
170
|
+
const expansion = expand === undefined ? undefined : expansionOf(expand)
|
|
171
|
+
let selector
|
|
172
|
+
if (all === true) selector = { all: true }
|
|
173
|
+
else if (tag !== undefined) selector = { tag }
|
|
174
|
+
else {
|
|
175
|
+
if (repo === undefined && repositories.length !== 1) refuse('view-repository-ambiguous', 'this project enrols more than one repository; name the one the folders are in with --repo', { repositories })
|
|
176
|
+
const repoId = repo ?? repositories[0]
|
|
177
|
+
if (!repositories.includes(repoId)) refuse('unknown-repo', 'the project enrols no repository of that name', { repositories })
|
|
178
|
+
const prefixes = [...new Set(folders.map(folderPrefix))]
|
|
179
|
+
const members = prefixes.includes(null) ? [{ repo: repoId }] : prefixes.map((pathPrefix) => ({ repo: repoId, pathPrefix }))
|
|
180
|
+
selector = members.length === 1 ? members[0] : { union: members }
|
|
181
|
+
}
|
|
182
|
+
return { scopeId, mode: all === true ? 'full' : 'scoped', selector, ...(expansion === undefined ? {} : { expansion }) }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// One more view: the project's Obsidian member with `scope` added (the member made, enabled, when there is none), and
|
|
186
|
+
// the default when asked or when it is the first view. Refuses a view whose name is taken. Writes nothing:
|
|
187
|
+
// { settings, member, ignore, diff }, for writeViewPlan.
|
|
188
|
+
export function planViewAdd(project, { scope, makeDefault = false }) {
|
|
189
|
+
const source = readProjectFile(project)
|
|
190
|
+
const document = isPlainObject(source.document) ? source.document : project.config
|
|
191
|
+
const present = isPlainObject(document?.ext) ? document.ext[OBSIDIAN_EXT_KEY] : undefined
|
|
192
|
+
const scopes = Array.isArray(present?.scopes) ? present.scopes : []
|
|
193
|
+
if (scopes.some((item) => item?.scopeId === scope.scopeId)) refuse('view-exists', 'the project declares a view of this name already', { scopeId: scope.scopeId })
|
|
194
|
+
// A member of its own keeps its keys in their order; a new one is written as the documentation shows it.
|
|
195
|
+
const next = present === undefined
|
|
196
|
+
? { schema: EXT_SETTINGS_SCHEMA, enabled: true, defaultScopeId: scope.scopeId, scopes: [scope] }
|
|
197
|
+
: { ...present, scopes: [...scopes, scope], ...(makeDefault || scopes.length === 0 ? { defaultScopeId: scope.scopeId } : {}) }
|
|
198
|
+
const settings = checkedSettings(document ?? {}, next)
|
|
199
|
+
const member = planMember({ ...source, settings })
|
|
200
|
+
const ignore = planIgnore(project)
|
|
201
|
+
return { settings, member, ignore, diff: `${member.diff}${ignore.needed ? ignore.diff : ''}` }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// A planned file still holds exactly the bytes its plan was made from (a file that was not there is still not there).
|
|
205
|
+
function assertUnchanged({ file, what, bytes: planned }) {
|
|
206
|
+
const { bytes } = readRegularFile(file, { code: 'project-file-unreadable', what })
|
|
207
|
+
if (planned === null ? bytes !== null : bytes === null || !bytes.equals(planned)) {
|
|
208
|
+
refuse('project-config-changed', `${path.basename(file)} changed while the change was prepared; nothing was written`, { file: path.basename(file) })
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// The temporary file a planned file's new text is written to, beside it: `.<name>.<pid>.<ms>.atelier.tmp`.
|
|
213
|
+
const temporaryOf = (file) => path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.atelier.tmp`)
|
|
214
|
+
const isAlive = (pid) => { try { process.kill(pid, 0); return true } catch (error) { return error.code === 'EPERM' } }
|
|
215
|
+
// Temporary files an earlier run left beside a planned file when it stopped between writing and renaming (a crash):
|
|
216
|
+
// removed, unless the run that made them still runs, so none is left to be committed.
|
|
217
|
+
function removeLeftovers(file) {
|
|
218
|
+
const pattern = new RegExp(`^\\.${path.basename(file).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.([0-9]+)\\.[0-9]+\\.atelier\\.tmp$`)
|
|
219
|
+
let names = []
|
|
220
|
+
try { names = fs.readdirSync(path.dirname(file)) } catch { return }
|
|
221
|
+
for (const name of names) {
|
|
222
|
+
const match = pattern.exec(name)
|
|
223
|
+
if (match !== null && (Number(match[1]) === process.pid || !isAlive(Number(match[1])))) fs.rmSync(path.join(path.dirname(file), name), { force: true })
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// A planned file's new text in its temporary file, keeping its mode, and on the disk before any file is replaced.
|
|
228
|
+
function writeTemporary({ file, after, mode }) {
|
|
229
|
+
const temporary = temporaryOf(file)
|
|
230
|
+
const descriptor = fs.openSync(temporary, 'wx', mode ?? 0o666)
|
|
231
|
+
try {
|
|
232
|
+
fs.writeFileSync(descriptor, after)
|
|
233
|
+
if (mode !== null) fs.fchmodSync(descriptor, mode)
|
|
234
|
+
fs.fsyncSync(descriptor)
|
|
235
|
+
} catch (error) {
|
|
236
|
+
fs.closeSync(descriptor)
|
|
237
|
+
fs.rmSync(temporary, { force: true })
|
|
238
|
+
throw error
|
|
239
|
+
} finally {
|
|
240
|
+
try { fs.closeSync(descriptor) } catch { /* closed above */ }
|
|
241
|
+
}
|
|
242
|
+
return temporary
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// The folder of a replaced file, on the disk, so the renames survive a crash. A system that cannot sync a folder
|
|
246
|
+
// (Windows) keeps the renames all the same.
|
|
247
|
+
function syncFolder(folder) {
|
|
248
|
+
let descriptor
|
|
249
|
+
try { descriptor = fs.openSync(folder, 'r'); fs.fsyncSync(descriptor) } catch { /* not supported here */ } finally { if (descriptor !== undefined) fs.closeSync(descriptor) }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Writes a plan of planViewAdd when every file it changes still holds the bytes it was made from: every new text is
|
|
253
|
+
// written and synced to its temporary file first, then the files are replaced, the ignore line first, so a project
|
|
254
|
+
// file that names a view never waits on it. A failure before the first replacement changes nothing
|
|
255
|
+
// (`project-files-not-written`); one after it says which files changed (`project-files-partly-written`). No temporary
|
|
256
|
+
// file is left. Commits nothing. Answers the files written. `rename` is injectable for tests.
|
|
257
|
+
export function writeViewPlan(plan, { rename = fs.renameSync } = {}) {
|
|
258
|
+
const targets = [...(plan.ignore.needed ? [plan.ignore] : []), plan.member]
|
|
259
|
+
for (const target of targets) removeLeftovers(target.file)
|
|
260
|
+
for (const target of targets) assertUnchanged(target)
|
|
261
|
+
const prepared = []
|
|
262
|
+
const discard = () => { for (const { temporary } of prepared) fs.rmSync(temporary, { force: true }) }
|
|
263
|
+
try {
|
|
264
|
+
for (const target of targets) prepared.push({ target, temporary: writeTemporary(target) })
|
|
265
|
+
} catch (error) {
|
|
266
|
+
discard()
|
|
267
|
+
refuse('project-files-not-written', 'the change could not be written; nothing was changed', { cause: error.code ?? String(error.message) })
|
|
268
|
+
}
|
|
269
|
+
const written = []
|
|
270
|
+
for (const { target, temporary } of prepared) {
|
|
271
|
+
try { rename(temporary, target.file) } catch (error) {
|
|
272
|
+
discard()
|
|
273
|
+
const names = (list) => list.map((file) => path.basename(file))
|
|
274
|
+
const rest = targets.map((item) => item.file).filter((file) => !written.includes(file))
|
|
275
|
+
if (written.length === 0) refuse('project-files-not-written', 'the change could not be written; nothing was changed', { cause: error.code ?? String(error.message) })
|
|
276
|
+
refuse('project-files-partly-written', `${names(written).join(', ')} changed, and ${names(rest).join(', ')} could not be; finish the change by hand, or undo it`, { written: names(written), notWritten: names(rest), cause: error.code ?? String(error.message) })
|
|
277
|
+
}
|
|
278
|
+
written.push(target.file)
|
|
279
|
+
}
|
|
280
|
+
for (const folder of new Set(written.map((file) => path.dirname(file)))) syncFolder(folder)
|
|
281
|
+
return { written }
|
|
282
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import readline from 'node:readline'
|
|
2
|
+
|
|
3
|
+
// Questions for a person at a terminal, one line each, over the streams the
|
|
4
|
+
// caller gives, so a test types the answers. A question never waits forever:
|
|
5
|
+
// no line within the timeout (10 minutes), or the end of the input, is no
|
|
6
|
+
// answer, so an agent that was handed a terminal still never hangs. Whether a
|
|
7
|
+
// person is there at all is decided before anything is asked
|
|
8
|
+
// (src/commands/obsidian.mjs `isInteractive`).
|
|
9
|
+
|
|
10
|
+
export const ANSWER_TIMEOUT_MS = 10 * 60 * 1000
|
|
11
|
+
const MAX_ANSWER_LENGTH = 4096
|
|
12
|
+
const clean = (line) => line.slice(0, MAX_ANSWER_LENGTH).trim()
|
|
13
|
+
|
|
14
|
+
// `ask(question)` writes the question and answers the next line typed, trimmed, or null when none came. A line typed
|
|
15
|
+
// before its question is kept for it. `close()` stops reading.
|
|
16
|
+
export function createQuestioner({ input, output, timeoutMs = ANSWER_TIMEOUT_MS }) {
|
|
17
|
+
const typed = []
|
|
18
|
+
const waiting = []
|
|
19
|
+
let ended = false
|
|
20
|
+
const lines = readline.createInterface({ input, terminal: false, crlfDelay: Infinity })
|
|
21
|
+
lines.on('line', (line) => { const next = waiting.shift(); if (next === undefined) typed.push(line); else next(line) })
|
|
22
|
+
lines.on('close', () => { ended = true; for (const next of waiting.splice(0)) next(null) })
|
|
23
|
+
return {
|
|
24
|
+
ask(question) {
|
|
25
|
+
output.write(question)
|
|
26
|
+
if (typed.length > 0) return Promise.resolve(clean(typed.shift()))
|
|
27
|
+
if (ended) return Promise.resolve(null)
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const answer = (line) => { clearTimeout(timer); resolve(line === null ? null : clean(line)) }
|
|
30
|
+
const timer = setTimeout(() => { waiting.splice(waiting.indexOf(answer), 1); output.write('\n'); resolve(null) }, timeoutMs)
|
|
31
|
+
waiting.push(answer)
|
|
32
|
+
})
|
|
33
|
+
},
|
|
34
|
+
close() { lines.close() },
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A go-ahead: Enter, y or yes is yes; n or no is no; anything else is asked again, three times in all. null: no answer.
|
|
39
|
+
export async function askGoAhead(questioner, question) {
|
|
40
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
41
|
+
const answer = await questioner.ask(question)
|
|
42
|
+
if (answer === null) return null
|
|
43
|
+
const word = answer.toLowerCase()
|
|
44
|
+
if (word === '' || word === 'y' || word === 'yes') return true
|
|
45
|
+
if (word === 'n' || word === 'no') return false
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { packageRootOfEntry, readReleaseIdentity, walkRelease } from './service-record.mjs'
|
|
4
|
+
|
|
5
|
+
// Whether the package a service runs from changed on disk since it started.
|
|
6
|
+
//
|
|
7
|
+
// A service started by a login item runs until the person logs out. When the
|
|
8
|
+
// package it was started from is upgraded meanwhile (`npm i -D
|
|
9
|
+
// @mnstry/atelier@<new>` in the project), it would go on running the release
|
|
10
|
+
// it loaded, and load any module it had not needed yet from the new one. So,
|
|
11
|
+
// under `--startup`, the service asks after every tick whether its package
|
|
12
|
+
// changed, finishes that tick and exits with a code its service manager
|
|
13
|
+
// restarts (service.mjs, service-main.mjs): the next process runs the new
|
|
14
|
+
// release.
|
|
15
|
+
//
|
|
16
|
+
// The package is read through the path the service was started by, not its
|
|
17
|
+
// real path: a linked install (`file:`, `npm link`) that is pointed elsewhere
|
|
18
|
+
// is then seen. What is compared is the release identity (readReleaseIdentity
|
|
19
|
+
// in service-record.mjs, the one a service records at its start): the package
|
|
20
|
+
// version and a digest of every file under `src/`, `contracts/` and
|
|
21
|
+
// `plugins/`, by relative path and content. It is cheap to ask: the files'
|
|
22
|
+
// status (inode, size, change and modification time) is compared first, and
|
|
23
|
+
// the digest is computed only when that differs. A package that cannot be read right now (an install in
|
|
24
|
+
// progress) is not a change yet; it is asked again after the next tick.
|
|
25
|
+
|
|
26
|
+
export { packageRootOfEntry, readReleaseIdentity }
|
|
27
|
+
|
|
28
|
+
// The status of every file the identity covers and of package.json, as one comparable text. A package replaced by an
|
|
29
|
+
// install has new inodes and change times even where the archive kept the modification times.
|
|
30
|
+
export function releaseStatSignature({ root }) {
|
|
31
|
+
const lines = []
|
|
32
|
+
const stat = (relative, absolute) => { const found = fs.lstatSync(absolute); lines.push(`${relative}\0${found.dev}\0${found.ino}\0${found.size}\0${found.mtimeMs}\0${found.ctimeMs}`) }
|
|
33
|
+
walkRelease(root, stat)
|
|
34
|
+
stat('package.json', path.join(root, 'package.json'))
|
|
35
|
+
return lines.join('\n')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// { root, started, changed() }. `started` is the identity the service runs, read when the watch is made; `changed()`
|
|
39
|
+
// answers true once the package on disk is another release, and false while it is the same or cannot be read.
|
|
40
|
+
export function createReleaseWatch({ root, identityOf = readReleaseIdentity, signatureOf = releaseStatSignature } = {}) {
|
|
41
|
+
if (typeof root !== 'string' || !path.isAbsolute(root)) throw new TypeError('a release watch needs the absolute root of a package')
|
|
42
|
+
let signature = signatureOf({ root })
|
|
43
|
+
const started = identityOf({ root })
|
|
44
|
+
return {
|
|
45
|
+
root, started,
|
|
46
|
+
changed() {
|
|
47
|
+
let now
|
|
48
|
+
try { now = signatureOf({ root }) } catch { return false }
|
|
49
|
+
if (now === signature) return false
|
|
50
|
+
let identity
|
|
51
|
+
try { identity = identityOf({ root }) } catch { return false }
|
|
52
|
+
if (identity.version === started.version && identity.digest === started.digest) { signature = now; return false }
|
|
53
|
+
return true
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
}
|