@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
|
@@ -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
|
+
}
|
|
@@ -3,20 +3,29 @@ import fs from 'node:fs'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { firstString, parseArgs, resolveProjectConfig } from '../../project/config.mjs'
|
|
6
|
-
import {
|
|
6
|
+
import { workspaceStateRoot } from './machine-settings.mjs'
|
|
7
|
+
import { RELEASE_CHANGED, resolveServiceWorkspace, runMaintenanceService } from './service.mjs'
|
|
7
8
|
|
|
8
9
|
// The process of the maintenance service. `start` runs it as a child; a unit
|
|
9
10
|
// installed at the operating-system level runs it with `--startup`.
|
|
10
11
|
//
|
|
11
12
|
// --project=<absolute atelier.project.json> required
|
|
12
13
|
// --data-root=<absolute directory> optional; otherwise the pointer, the overlay or the platform default
|
|
14
|
+
// --workspace-id=<identifier> named by a login item: under `--startup`, where a refusal is recorded
|
|
15
|
+
// when the project no longer leads to a workspace
|
|
13
16
|
// --runtime-id=<identifier> given by `start`, so it can recognise the child it created
|
|
14
17
|
// --interval-ms=<milliseconds> optional
|
|
15
|
-
// --startup run by
|
|
18
|
+
// --startup run by a login item; needs a consent that covers startup
|
|
16
19
|
// --adapter=obsidian-cli the editor adapter; there is no default
|
|
17
20
|
//
|
|
18
21
|
// Without `--adapter` the process refuses before it listens or ticks: reaching
|
|
19
22
|
// a running app is a decision of whoever starts the service, never a default.
|
|
23
|
+
//
|
|
24
|
+
// Exit codes: 0 after a clean stop, and under `--startup` after a refusal too;
|
|
25
|
+
// 2 (EXIT_REFUSED) after a refusal otherwise; 75 (EXIT_RELEASE_CHANGED) under
|
|
26
|
+
// `--startup` once the package changed on disk; 1 after an error without a
|
|
27
|
+
// code. A refusal is any error that carries a string code, Node's system
|
|
28
|
+
// errors (EACCES, EADDRNOTAVAIL, EPERM) included.
|
|
20
29
|
|
|
21
30
|
// Reaching a real app happens here and nowhere else in the service: the
|
|
22
31
|
// production probe and the CLI transport are imported only once `--adapter`
|
|
@@ -33,52 +42,121 @@ import { runMaintenanceService } from './service.mjs'
|
|
|
33
42
|
// only read here.
|
|
34
43
|
const ADAPTERS = Object.freeze({
|
|
35
44
|
'obsidian-cli': async () => {
|
|
36
|
-
const [{ createObsidianCliAdapter, defaultObsidianProcessProbe }, { createProductionAppProbe }, { createQualifiedAdapterFactory }, { obsidianUserDataDir, readObsidianSettings }, { appStateSignature }] = await Promise.all([
|
|
45
|
+
const [{ createObsidianCliAdapter, defaultObsidianProcessProbe }, { createProductionAppProbe }, { createQualifiedAdapterFactory }, { obsidianSandboxedBuild, obsidianUserDataDir, readObsidianSettings }, { appStateSignature }, { readAppVaultListForAllocation }, { createProductionUnheldEvidence }] = await Promise.all([
|
|
37
46
|
import('../../projection/obsidian/publication/transport.mjs'), import('./app-production-seams.mjs'), import('./app-capability.mjs'),
|
|
38
|
-
import('../../projection/obsidian/publication/vault-list.mjs'), import('./app-registration.mjs'),
|
|
47
|
+
import('../../projection/obsidian/publication/vault-list.mjs'), import('./app-registration.mjs'), import('./vault-location.mjs'), import('./unheld-evidence.mjs'),
|
|
39
48
|
])
|
|
40
49
|
const adapterFactory = createQualifiedAdapterFactory({ appProbe: createProductionAppProbe(), createAdapter: ({ qualification }) => createObsidianCliAdapter({ qualification }) })
|
|
41
50
|
const userDataDir = obsidianUserDataDir()
|
|
42
51
|
const observeApp = () => appStateSignature({ processes: defaultObsidianProcessProbe(), settings: userDataDir === null ? null : readObsidianSettings({ userDataDir }) })
|
|
52
|
+
// The app's own vault list, read from its file and never written, so a new vault's folder is never allocated inside
|
|
53
|
+
// a vault it lists nor under a name one of its vaults has; a list that cannot be read allocates nothing.
|
|
54
|
+
const readAppVaultList = () => readAppVaultListForAllocation({
|
|
55
|
+
read: () => (userDataDir === null ? { ok: false, code: 'obsidian-settings-location-unknown' } : readObsidianSettings({ userDataDir })),
|
|
56
|
+
sandboxed: obsidianSandboxedBuild() !== null,
|
|
57
|
+
})
|
|
43
58
|
return {
|
|
44
|
-
|
|
59
|
+
// The same file tells the publisher whether a view's first publication may go into a vault no Obsidian lists yet.
|
|
60
|
+
adapterFactory, engineOptions: { observeApp, readAppVaultList, readUnheldEvidence: createProductionUnheldEvidence() },
|
|
45
61
|
appStatus: () => { const known = adapterFactory.lastQualification(); return known === null ? null : { outcome: known.outcome, reason: known.reason, version: known.version, floor: known.floor } },
|
|
46
62
|
}
|
|
47
63
|
},
|
|
48
64
|
})
|
|
49
65
|
|
|
66
|
+
// What the tests read: which reader each adapter hands its engine. Calling one builds the production seams, and
|
|
67
|
+
// reaches no app and no file until the engine uses them.
|
|
68
|
+
export { ADAPTERS as SERVICE_ADAPTERS }
|
|
69
|
+
|
|
70
|
+
import { createReleaseWatch, packageRootOfEntry } from './release-watch.mjs'
|
|
50
71
|
import { SERVICE_ENTRY_PATH } from './service-entry-path.mjs'
|
|
72
|
+
import { LAST_STARTUP_SCHEMA, openServiceLog, writeLastStartup } from './service-record.mjs'
|
|
51
73
|
|
|
52
74
|
export { SERVICE_ENTRY_PATH }
|
|
53
75
|
export const EXIT_REFUSED = 2
|
|
76
|
+
// A service a login item started exits with this code when the package it runs from changed on disk: its service
|
|
77
|
+
// manager restarts it (a non-zero exit), on the release now installed. EX_TEMPFAIL.
|
|
78
|
+
export const EXIT_RELEASE_CHANGED = 75
|
|
54
79
|
|
|
55
|
-
const
|
|
80
|
+
const printed = (entry) => { try { process.stdout.write(`${JSON.stringify(entry)}\n`) } catch { /* a closed log never ends the service */ } }
|
|
81
|
+
|
|
82
|
+
// The workspace of a service a login item started, when it can be found: where it keeps its log and records how its
|
|
83
|
+
// start ended. When the project no longer leads to one (it moved, its pointer is gone), the workspace the unit names by
|
|
84
|
+
// its data root and identity, if that exists; it is never created here. Null otherwise; a refusal is then printed
|
|
85
|
+
// only, into the unit's own output file.
|
|
86
|
+
function startupWorkspace({ loadProject, dataRoot, workspaceId, env = process.env, platform = process.platform }) {
|
|
87
|
+
try {
|
|
88
|
+
const workspace = resolveServiceWorkspace({ project: loadProject(), dataRoot, env, platform })
|
|
89
|
+
if (workspace?.workspaceRoot) return workspace
|
|
90
|
+
} catch { /* the workspace the unit names, below */ }
|
|
91
|
+
if (typeof dataRoot !== 'string' || !path.isAbsolute(dataRoot) || typeof workspaceId !== 'string') return null
|
|
92
|
+
try {
|
|
93
|
+
const root = workspaceStateRoot(path.resolve(dataRoot), workspaceId)
|
|
94
|
+
return fs.lstatSync(root).isDirectory() ? { workspaceId, workspaceRoot: fs.realpathSync(root) } : null
|
|
95
|
+
} catch { return null }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The service's own log, bounded as under `start`, which hands a child that same file. Under a login item the output
|
|
99
|
+
// goes to the unit's file instead, which nothing bounds; it keeps only what happens before this log is open.
|
|
100
|
+
function workspaceLog(workspaceRoot) {
|
|
101
|
+
let descriptor
|
|
102
|
+
try { descriptor = openServiceLog(workspaceRoot).descriptor } catch { return printed }
|
|
103
|
+
return (entry) => { try { fs.writeSync(descriptor, `${JSON.stringify(entry)}\n`) } catch { /* a closed log never ends the service */ } }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function recordStartup(workspace, { at, outcome, code }) {
|
|
107
|
+
if (workspace === null) return
|
|
108
|
+
try { writeLastStartup({ ...workspace, document: { schema: LAST_STARTUP_SCHEMA, workspaceId: workspace.workspaceId, at, outcome, code } }) } catch { /* reported by the log only */ }
|
|
109
|
+
}
|
|
56
110
|
|
|
57
111
|
// The common body of a service process: run, end cleanly on a signal, exit 0
|
|
58
112
|
// after a clean stop and EXIT_REFUSED when the service refused to run.
|
|
113
|
+
//
|
|
114
|
+
// Started by a login item (`startup`), it logs into its workspace's bounded
|
|
115
|
+
// service log, records how the start ended (`last-startup.json`), exits 0 on
|
|
116
|
+
// a refusal as well, so the service manager does not start it again every
|
|
117
|
+
// minute over something that does not change by itself, and exits
|
|
118
|
+
// EXIT_RELEASE_CHANGED after the tick that found another release on disk.
|
|
119
|
+
// Anything else that ends it (an error nobody typed, a signal) is a crash,
|
|
120
|
+
// and the manager restarts it after its throttle.
|
|
59
121
|
export async function runServiceProcess(options) {
|
|
122
|
+
const startup = options.startup === true
|
|
123
|
+
const workspace = startup ? startupWorkspace(options) : null
|
|
124
|
+
const log = workspace === null ? printed : workspaceLog(workspace.workspaceRoot)
|
|
60
125
|
try {
|
|
61
|
-
const service = await runMaintenanceService({ log, ...options })
|
|
126
|
+
const service = await runMaintenanceService({ log, invokedAs: process.argv[1], ...options })
|
|
127
|
+
if (startup) recordStartup(workspace, { at: new Date().toISOString(), outcome: 'started', code: null })
|
|
62
128
|
for (const signal of ['SIGTERM', 'SIGINT']) process.on(signal, () => { void service.shutdown(`signal-${signal}`) })
|
|
63
|
-
await service.done
|
|
129
|
+
const { reason } = await service.done
|
|
64
130
|
// Everything durable is on disk by now; nothing left over may keep a stopped service's PID alive.
|
|
65
|
-
process.exit(0)
|
|
131
|
+
process.exit(reason === RELEASE_CHANGED ? EXIT_RELEASE_CHANGED : 0)
|
|
66
132
|
} catch (error) {
|
|
67
|
-
|
|
68
|
-
|
|
133
|
+
const at = new Date().toISOString()
|
|
134
|
+
const code = typeof error?.code === 'string' ? error.code : null
|
|
135
|
+
log({ at, event: 'refused', code: code ?? 'untyped-error', message: String(error?.message ?? error) })
|
|
136
|
+
if (startup && code !== null) { recordStartup(workspace, { at, outcome: 'refused', code }); process.exitCode = 0; return }
|
|
137
|
+
process.exitCode = code !== null ? EXIT_REFUSED : 1
|
|
69
138
|
}
|
|
70
139
|
}
|
|
71
140
|
|
|
141
|
+
// The release a service a login item started runs, read through the path it was started by (`argv[1]`), so an
|
|
142
|
+
// upgrade, or a linked install pointed elsewhere, is seen. Null when that package cannot be read.
|
|
143
|
+
function watchRelease(invokedAs) {
|
|
144
|
+
const root = packageRootOfEntry(invokedAs) ?? packageRootOfEntry(SERVICE_ENTRY_PATH)
|
|
145
|
+
if (root === null) return null
|
|
146
|
+
try { return createReleaseWatch({ root }) } catch { return null }
|
|
147
|
+
}
|
|
148
|
+
|
|
72
149
|
export function serviceOptionsFromArgv(argv, { env = process.env } = {}) {
|
|
73
150
|
const args = parseArgs(argv)
|
|
74
151
|
const configPath = firstString(args.project)
|
|
75
152
|
if (!configPath || !path.isAbsolute(configPath)) throw Object.assign(new Error('--project must be the absolute path of a project configuration'), { code: 'service-arguments-invalid' })
|
|
76
153
|
const dataRoot = firstString(args['data-root']) ?? undefined
|
|
154
|
+
const workspaceId = firstString(args['workspace-id']) ?? undefined
|
|
77
155
|
const intervalMs = args['interval-ms'] === undefined ? undefined : Number(args['interval-ms'])
|
|
78
156
|
if (intervalMs !== undefined && (!Number.isInteger(intervalMs) || intervalMs < 1)) throw Object.assign(new Error('--interval-ms must be a positive integer'), { code: 'service-arguments-invalid' })
|
|
79
157
|
return {
|
|
80
158
|
loadProject: () => resolveProjectConfig({ argv: [`--project=${configPath}`], cwd: path.dirname(configPath), env, writeLocalState: false }),
|
|
81
|
-
...(dataRoot === undefined ? {} : { dataRoot }), ...(intervalMs === undefined ? {} : { intervalMs }),
|
|
159
|
+
...(dataRoot === undefined ? {} : { dataRoot }), ...(workspaceId === undefined ? {} : { workspaceId }), ...(intervalMs === undefined ? {} : { intervalMs }),
|
|
82
160
|
...(firstString(args['runtime-id']) ? { runtimeId: firstString(args['runtime-id']) } : {}),
|
|
83
161
|
startup: args.startup === true, adapter: firstString(args.adapter),
|
|
84
162
|
}
|
|
@@ -87,13 +165,18 @@ export function serviceOptionsFromArgv(argv, { env = process.env } = {}) {
|
|
|
87
165
|
const invokedDirectly = (() => { try { return Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)) } catch { return false } })()
|
|
88
166
|
|
|
89
167
|
if (invokedDirectly) {
|
|
168
|
+
const argv = process.argv.slice(2)
|
|
169
|
+
// As early as this entry can: its static imports have already loaded most of the modules, so a release that changes
|
|
170
|
+
// between this process's start and this reading is not noticed until the next change (docs/obsidian.md says so).
|
|
171
|
+
const releaseWatch = argv.includes('--startup') ? watchRelease(process.argv[1]) : null
|
|
90
172
|
let options = null
|
|
91
173
|
try {
|
|
92
|
-
options = serviceOptionsFromArgv(
|
|
174
|
+
options = serviceOptionsFromArgv(argv)
|
|
93
175
|
if (!options.adapter || !Object.hasOwn(ADAPTERS, options.adapter)) throw Object.assign(new Error('no editor adapter was selected; pass --adapter explicitly'), { code: 'service-adapter-not-selected' })
|
|
94
176
|
} catch (error) {
|
|
95
|
-
|
|
96
|
-
|
|
177
|
+
printed({ at: new Date().toISOString(), event: 'refused', code: error.code ?? 'untyped-error', message: error.message })
|
|
178
|
+
// Under a login item arguments that do not work never will: it ends cleanly, and is not started again every minute.
|
|
179
|
+
process.exitCode = argv.includes('--startup') ? 0 : EXIT_REFUSED
|
|
97
180
|
options = null
|
|
98
181
|
}
|
|
99
182
|
if (options) {
|
|
@@ -101,6 +184,6 @@ if (invokedDirectly) {
|
|
|
101
184
|
const [{ loadContributions }, { createObsidianRegistry }] = await Promise.all([import('./contributions.mjs'), import('./extension-points.mjs')])
|
|
102
185
|
const registry = createObsidianRegistry({ contributions: await loadContributions() })
|
|
103
186
|
const { engineOptions, ...seams } = await ADAPTERS[adapter]()
|
|
104
|
-
await runServiceProcess({ ...rest, entryPath: SERVICE_ENTRY_PATH, ...seams, engineOptions: { ...engineOptions, extensions: registry.extensions } })
|
|
187
|
+
await runServiceProcess({ ...rest, entryPath: SERVICE_ENTRY_PATH, ...seams, engineOptions: { ...engineOptions, extensions: registry.extensions }, releaseWatch })
|
|
105
188
|
}
|
|
106
189
|
}
|