@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
4
5
|
import { atomicReplacePrivateText, ensureContainedPrivateDirectory, openRegularFileNoFollow, readRegularTextNoFollow } from '../../project/private-state.mjs'
|
|
5
6
|
import { ObsidianContractRefusal, assertObsidianContract } from '../../projection/obsidian/contracts.mjs'
|
|
6
7
|
import { canonicalJson, closedObject } from './documents.mjs'
|
|
@@ -10,11 +11,14 @@ import { LOOPBACK_HOSTS } from './service-client.mjs'
|
|
|
10
11
|
// Private documents of the maintenance service of one workspace, under
|
|
11
12
|
//
|
|
12
13
|
// <data>/obsidian/<workspace-id>/state/service/
|
|
13
|
-
// runtime.json
|
|
14
|
-
// settings.json
|
|
15
|
-
// last-error.json
|
|
16
|
-
//
|
|
17
|
-
//
|
|
14
|
+
// runtime.json the adapter record of the running service (service-state v1)
|
|
15
|
+
// settings.json what this machine chose: loopback host, port, startup consent
|
|
16
|
+
// last-error.json the last tick that failed for a reason nobody typed
|
|
17
|
+
// last-startup.json how the last start by the login item ended: started, or the refusal's code
|
|
18
|
+
// login-item.json the installed login item (login-item.mjs)
|
|
19
|
+
// service.log the operational log of the service process
|
|
20
|
+
// login-item.log what a login item's service printed before it could open service.log
|
|
21
|
+
// start-lock/ serializes `start` for this workspace
|
|
18
22
|
//
|
|
19
23
|
// Owner-only, replaced atomically, outside every repository and every vault.
|
|
20
24
|
// Every document is validated on every read. One that does not validate, or
|
|
@@ -23,7 +27,9 @@ import { LOOPBACK_HOSTS } from './service-client.mjs'
|
|
|
23
27
|
|
|
24
28
|
export const SERVICE_SETTINGS_SCHEMA = 'atelier-obsidian-service-settings/v1'
|
|
25
29
|
export const SERVICE_ERROR_SCHEMA = 'atelier-obsidian-service-last-error/v1'
|
|
30
|
+
export const LAST_STARTUP_SCHEMA = 'atelier-obsidian-last-startup/v1'
|
|
26
31
|
export const CONSENT_COVERAGES = Object.freeze(['service', 'service-and-startup'])
|
|
32
|
+
export const STARTUP_OUTCOMES = Object.freeze(['started', 'refused'])
|
|
27
33
|
|
|
28
34
|
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
29
35
|
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
|
@@ -40,6 +46,7 @@ export function servicePaths(workspaceRoot) {
|
|
|
40
46
|
return {
|
|
41
47
|
stateLocation: path.join(workspaceRoot, 'state'), directory, record: path.join(directory, 'runtime.json'), settings: path.join(directory, 'settings.json'),
|
|
42
48
|
lastError: path.join(directory, 'last-error.json'), log: path.join(directory, 'service.log'), startLock: path.join(directory, 'start-lock'),
|
|
49
|
+
lastStartup: path.join(directory, 'last-startup.json'), loginItem: path.join(directory, 'login-item.json'), loginItemLog: path.join(directory, 'login-item.log'),
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -60,6 +67,72 @@ export function executableIdentity(entryPath) {
|
|
|
60
67
|
return { path: resolved, digest: `sha256:${createHash('sha256').update(fs.readFileSync(resolved)).digest('hex')}` }
|
|
61
68
|
}
|
|
62
69
|
|
|
70
|
+
// The package this module ships in.
|
|
71
|
+
const PACKAGE_ROOT = fileURLToPath(new URL('../../../', import.meta.url))
|
|
72
|
+
const byName = (left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0)
|
|
73
|
+
const releases = new Map()
|
|
74
|
+
|
|
75
|
+
// The release that runs, beyond its entry module: the version of the package
|
|
76
|
+
// and a digest of the runtime it ships, by relative path and content. That is
|
|
77
|
+
// what the service loads: every module under `src/`, the `contracts/` it
|
|
78
|
+
// reads, and the plugin under `plugins/` it publishes into every vault it
|
|
79
|
+
// maintains; outside those it reads nothing else of the package but the
|
|
80
|
+
// version in `package.json`. A release that changes any of it, and not only
|
|
81
|
+
// the entry, differs. A package without `plugins/` reads as one with an empty
|
|
82
|
+
// one.
|
|
83
|
+
|
|
84
|
+
// Every regular file a release identity covers, in a fixed order: `visit(relative, absolute)`. A missing `src/` or
|
|
85
|
+
// `contracts/` throws (the package cannot be read); a missing `plugins/` is empty.
|
|
86
|
+
export function walkRelease(root, visit) {
|
|
87
|
+
const walk = (relative, { optional = false } = {}) => {
|
|
88
|
+
let entries
|
|
89
|
+
try { entries = fs.readdirSync(path.join(root, relative), { withFileTypes: true }) } catch (error) { if (optional && error.code === 'ENOENT') return; throw error }
|
|
90
|
+
for (const entry of entries.sort(byName)) {
|
|
91
|
+
const child = `${relative}/${entry.name}`
|
|
92
|
+
if (entry.isDirectory()) walk(child)
|
|
93
|
+
else if (entry.isFile()) visit(child, path.join(root, child))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const part of ['src', 'contracts']) walk(part)
|
|
97
|
+
walk('plugins', { optional: true })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The release identity of the package at `root`, read now and never cached: { version, digest }.
|
|
101
|
+
export function readReleaseIdentity({ root }) {
|
|
102
|
+
if (typeof root !== 'string' || !path.isAbsolute(root)) throw new TypeError('a release identity is read from the absolute root of a package')
|
|
103
|
+
const hash = createHash('sha256')
|
|
104
|
+
walkRelease(root, (relative, absolute) => { hash.update(`${relative}\0${createHash('sha256').update(fs.readFileSync(absolute)).digest('hex')}\n`) })
|
|
105
|
+
let version = null
|
|
106
|
+
try { const read = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version; version = typeof read === 'string' ? read : null } catch { version = null }
|
|
107
|
+
return Object.freeze({ version, digest: `sha256:${hash.digest('hex')}` })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// readReleaseIdentity, computed once per process and package root, so a service keeps the identity of the code it
|
|
111
|
+
// loaded.
|
|
112
|
+
export function releaseIdentity({ root = PACKAGE_ROOT } = {}) {
|
|
113
|
+
if (!releases.has(root)) releases.set(root, readReleaseIdentity({ root }))
|
|
114
|
+
return releases.get(root)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const ENTRY_IN_PACKAGE = Object.freeze(['src', 'runtime', 'obsidian', 'service-main.mjs'])
|
|
118
|
+
|
|
119
|
+
// The root of the package whose service entry is `entryPath` (`<root>/src/runtime/obsidian/service-main.mjs`), as that
|
|
120
|
+
// path names it, without resolving links; null for any other entry.
|
|
121
|
+
export function packageRootOfEntry(entryPath) {
|
|
122
|
+
if (typeof entryPath !== 'string' || !path.isAbsolute(entryPath)) return null
|
|
123
|
+
const parts = path.resolve(entryPath).split(path.sep)
|
|
124
|
+
if (parts.length <= ENTRY_IN_PACKAGE.length || ENTRY_IN_PACKAGE.some((part, index) => parts[parts.length - ENTRY_IN_PACKAGE.length + index] !== part)) return null
|
|
125
|
+
return parts.slice(0, parts.length - ENTRY_IN_PACKAGE.length).join(path.sep) || path.sep
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The release that starting `entryPath` would run, read now from the package that path names (so a link pointed
|
|
129
|
+
// elsewhere, or a `file:` install, is the package it leads to now); for an entry that is not a package's service entry,
|
|
130
|
+
// this package's own, as it was loaded.
|
|
131
|
+
export function releaseOfEntry(entryPath) {
|
|
132
|
+
const root = packageRootOfEntry(entryPath)
|
|
133
|
+
return root === null ? releaseIdentity() : readReleaseIdentity({ root })
|
|
134
|
+
}
|
|
135
|
+
|
|
63
136
|
// ---------------------------------------------------------------------------
|
|
64
137
|
// The adapter record
|
|
65
138
|
// ---------------------------------------------------------------------------
|
|
@@ -159,6 +232,33 @@ export function writeLastServiceError({ workspaceRoot, workspaceId, document })
|
|
|
159
232
|
return document
|
|
160
233
|
}
|
|
161
234
|
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// How the last start by a login item ended
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
// { schema, workspaceId, at, outcome: 'started' | 'refused', code }: the code of the refusal, null for a start. Written by
|
|
240
|
+
// the service when it runs with `--startup`, so a login item that exits cleanly on a refusal (and is therefore not
|
|
241
|
+
// started again in a loop) still says why.
|
|
242
|
+
function validateLastStartup(document, workspaceId) {
|
|
243
|
+
const code = 'invalid-service-last-startup'
|
|
244
|
+
closedObject(document, { required: ['schema', 'workspaceId', 'at', 'outcome', 'code'] }, code, 'the last startup')
|
|
245
|
+
const ok = document.schema === LAST_STARTUP_SCHEMA && document.workspaceId === workspaceId && TIMESTAMP.test(document.at) && STARTUP_OUTCOMES.includes(document.outcome)
|
|
246
|
+
&& (document.outcome === 'started' ? document.code === null : typeof document.code === 'string' && /^[A-Za-z0-9_.:-]{1,64}$/.test(document.code))
|
|
247
|
+
if (!ok) refuse(code, 'the last startup is malformed')
|
|
248
|
+
return document
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function readLastStartup({ workspaceRoot, workspaceId }) {
|
|
252
|
+
const document = readJson(servicePaths(workspaceRoot).lastStartup, 'invalid-service-last-startup', 'the last startup')
|
|
253
|
+
return document === null ? null : validateLastStartup(document, workspaceId)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function writeLastStartup({ workspaceRoot, workspaceId, document }) {
|
|
257
|
+
validateLastStartup(document, workspaceId)
|
|
258
|
+
atomicReplacePrivateText(path.join(serviceDirectory(workspaceRoot), 'last-startup.json'), canonicalJson(document))
|
|
259
|
+
return document
|
|
260
|
+
}
|
|
261
|
+
|
|
162
262
|
// The log is bounded: past the ceiling it becomes the one previous log, and a new one begins.
|
|
163
263
|
export const SERVICE_LOG_MAX_BYTES = 1024 * 1024
|
|
164
264
|
|
|
@@ -1,31 +1,50 @@
|
|
|
1
1
|
import { createHash, timingSafeEqual } from 'node:crypto'
|
|
2
2
|
import http from 'node:http'
|
|
3
|
+
import { isContractIdentifier } from '../../projection/obsidian/contracts.mjs'
|
|
4
|
+
import { PLUGIN_MAX_REQUEST_BYTES, pluginCommandOf, validatePluginRequest } from '../../projection/obsidian/plugin-bridge/channel.mjs'
|
|
3
5
|
import { requestHeader, sameOrigin } from '../../server/security.mjs'
|
|
4
6
|
import { isPlainObject } from './documents.mjs'
|
|
5
7
|
import { HEALTH_SCHEMA, LOOPBACK_HOSTS, authorityOf } from './service-client.mjs'
|
|
6
8
|
|
|
7
|
-
// The listener of the maintenance service: four fixed operations
|
|
8
|
-
//
|
|
9
|
-
// takes a path, a name or
|
|
9
|
+
// The listener of the maintenance service: four fixed operations, the five
|
|
10
|
+
// fixed commands of Atelier's plugin, and nothing else. There is no file
|
|
11
|
+
// serving, no command, no evaluation and no route that takes a path, a name or
|
|
12
|
+
// code from a request.
|
|
10
13
|
//
|
|
11
14
|
// GET /health who answers here; open, and says nothing else
|
|
12
15
|
// GET /status service and per-view freshness summary; bearer required
|
|
13
16
|
// POST /tick run one tick now; bearer required
|
|
14
17
|
// POST /stop finish the tick in flight and exit; bearer required
|
|
15
18
|
//
|
|
19
|
+
// POST /plugin/challenge | /plugin/hello | /plugin/lease | /plugin/release |
|
|
20
|
+
// /plugin/status
|
|
21
|
+
// Atelier's plugin inside one vault, authenticated by a
|
|
22
|
+
// handshake over that vault's key (plugin-bridge/channel.mjs)
|
|
23
|
+
//
|
|
16
24
|
// Every request, before its operation is even looked up:
|
|
17
25
|
//
|
|
18
26
|
// - `Host` is exactly the literal loopback authority this listener is bound
|
|
19
27
|
// to, so a name that resolves to loopback (DNS rebinding) is refused;
|
|
20
28
|
// - a `Sec-Fetch-Site` other than `none` or `same-origin`, or an `Origin`
|
|
21
29
|
// that is not this listener itself, is refused: no web page drives this;
|
|
22
|
-
// - the path is one of the
|
|
30
|
+
// - the path is one of the nine, exactly, with no query; the method is the
|
|
23
31
|
// one that path has.
|
|
24
32
|
//
|
|
25
|
-
// Everything but health needs the per-runtime
|
|
26
|
-
// in the owner-only record. A POST body is JSON
|
|
27
|
-
// runtime it is meant for, so a request aimed at an
|
|
28
|
-
// same port does nothing.
|
|
33
|
+
// Everything but health and the plugin's commands needs the per-runtime
|
|
34
|
+
// random bearer that exists only in the owner-only record. A POST body is JSON
|
|
35
|
+
// of at most 1 KiB naming the runtime it is meant for, so a request aimed at an
|
|
36
|
+
// earlier runtime on the same port does nothing (409). A tick may also name one
|
|
37
|
+
// view (`scopeId`, a contract identifier), which is then prepared and published
|
|
38
|
+
// once more on that tick; any other member is refused (400,
|
|
39
|
+
// `request-member-unknown`).
|
|
40
|
+
//
|
|
41
|
+
// A plugin command carries no bearer: the plugin never sends its vault's key.
|
|
42
|
+
// Its body, JSON of at most 1 KiB with exactly the fields of its command, is
|
|
43
|
+
// judged by the plugin channel (plugin-channel.mjs): a challenge the service
|
|
44
|
+
// answers only for a key it holds, a hello that proves the same key, and
|
|
45
|
+
// session commands under the key the handshake derived. What it grants is
|
|
46
|
+
// these commands for that one vault. The runtime bearer is not accepted there,
|
|
47
|
+
// and nothing a plugin holds reaches status, tick or stop.
|
|
29
48
|
|
|
30
49
|
export const MAX_REQUEST_BYTES = 1024
|
|
31
50
|
export const SERVICE_OPERATIONS = Object.freeze({ '/health': 'GET', '/status': 'GET', '/tick': 'POST', '/stop': 'POST' })
|
|
@@ -35,6 +54,7 @@ const digestOf = (value) => createHash('sha256').update(String(value)).digest()
|
|
|
35
54
|
// The decisions the request oracles are sensitive to; tests substitute broken ones to prove the oracles can fail.
|
|
36
55
|
export const SERVER_PRIMITIVES = Object.freeze({
|
|
37
56
|
bearerMatches: (presented, expected) => typeof presented === 'string' && presented !== '' && timingSafeEqual(digestOf(presented), digestOf(expected)),
|
|
57
|
+
maxPluginRequestBytes: PLUGIN_MAX_REQUEST_BYTES,
|
|
38
58
|
hostMatches: (header, authority) => header === authority,
|
|
39
59
|
originAllowed(headers, authority) {
|
|
40
60
|
const site = requestHeader(headers, 'sec-fetch-site').toLowerCase()
|
|
@@ -73,7 +93,9 @@ function readBoundedJson(request, limit) {
|
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
// identity: { serviceName, workspaceId, runtimeId, pid, host, port, executableDigest, startedAt }
|
|
76
|
-
// operations: { healthStatus(), status(), tick(), stop() }
|
|
96
|
+
// operations: { healthStatus(), status(), tick(), stop() }, and, where the
|
|
97
|
+
// service holds the plugin channel, { plugin(command, { body, authority }) ->
|
|
98
|
+
// { statusCode, body } }
|
|
77
99
|
export function createServiceServerForOracleTests({ identity, bearer, operations }, primitives = SERVER_PRIMITIVES) {
|
|
78
100
|
if (!LOOPBACK_HOSTS.includes(identity?.host)) throw new TypeError('the service listens on a literal loopback address only')
|
|
79
101
|
if (typeof bearer !== 'string' || bearer.length < 32) throw new TypeError('the service needs its random bearer')
|
|
@@ -83,6 +105,19 @@ export function createServiceServerForOracleTests({ identity, bearer, operations
|
|
|
83
105
|
const authority = authorityOf(identity.host, identity.port)
|
|
84
106
|
if (!rules.hostMatches(requestHeader(request.headers, 'host'), authority)) return send(response, 403, { error: 'host-not-this-loopback-listener' })
|
|
85
107
|
if (!rules.originAllowed(request.headers, authority)) return send(response, 403, { error: 'cross-site-request' })
|
|
108
|
+
const presented = /^Bearer ([A-Za-z0-9_-]+)$/.exec(requestHeader(request.headers, 'authorization'))?.[1] ?? null
|
|
109
|
+
const command = typeof operations.plugin === 'function' ? pluginCommandOf(request.url) : null
|
|
110
|
+
if (command !== null) {
|
|
111
|
+
if (request.method !== 'POST') return send(response, 405, { error: 'method-not-allowed' })
|
|
112
|
+
// No credential travels in a header here: a request that sends one was not made by the plugin.
|
|
113
|
+
if (requestHeader(request.headers, 'authorization') !== '') return send(response, 400, { error: 'plugin-command-takes-no-bearer' })
|
|
114
|
+
const payload = await readBoundedJson(request, rules.maxPluginRequestBytes)
|
|
115
|
+
if (!payload.ok) return send(response, payload.statusCode, { error: payload.code })
|
|
116
|
+
const checked = validatePluginRequest(command, payload.body)
|
|
117
|
+
if (!checked.ok) return send(response, checked.code === 'protocol-unsupported' ? 409 : 400, { error: checked.code })
|
|
118
|
+
const answer = await operations.plugin(command, { body: checked.body, authority })
|
|
119
|
+
return send(response, answer.statusCode, answer.body)
|
|
120
|
+
}
|
|
86
121
|
const method = Object.hasOwn(SERVICE_OPERATIONS, request.url) ? SERVICE_OPERATIONS[request.url] : null
|
|
87
122
|
if (method === null) return send(response, 404, { error: 'unknown-operation' })
|
|
88
123
|
if (request.method !== method) return send(response, 405, { error: 'method-not-allowed' })
|
|
@@ -90,13 +125,16 @@ export function createServiceServerForOracleTests({ identity, bearer, operations
|
|
|
90
125
|
const { serviceName, workspaceId, runtimeId, pid, host, port, executableDigest, startedAt } = identity
|
|
91
126
|
return send(response, 200, { schema: HEALTH_SCHEMA, serviceName, workspaceId, runtimeId, pid, host, port, executableDigest, startedAt, status: operations.healthStatus() })
|
|
92
127
|
}
|
|
93
|
-
const presented = /^Bearer ([A-Za-z0-9_-]+)$/.exec(requestHeader(request.headers, 'authorization'))?.[1] ?? null
|
|
94
128
|
if (!rules.bearerMatches(presented, bearer)) return send(response, 401, { error: 'bearer-required' })
|
|
95
129
|
if (request.url === '/status') return send(response, 200, await operations.status())
|
|
96
130
|
const payload = await readBoundedJson(request, rules.maxRequestBytes)
|
|
97
131
|
if (!payload.ok) return send(response, payload.statusCode, { error: payload.code })
|
|
98
|
-
|
|
99
|
-
if (
|
|
132
|
+
const { runtimeId, scopeId, ...unknown } = payload.body
|
|
133
|
+
if (runtimeId !== identity.runtimeId) return send(response, 409, { error: 'request-names-another-runtime' })
|
|
134
|
+
// A member the operation does not take: a view on a stop, or anything else.
|
|
135
|
+
if (Object.keys(unknown).length > 0 || (scopeId !== undefined && request.url !== '/tick')) return send(response, 400, { error: 'request-member-unknown' })
|
|
136
|
+
if (scopeId !== undefined && !isContractIdentifier(scopeId)) return send(response, 400, { error: 'request-invalid' })
|
|
137
|
+
if (request.url === '/tick') return send(response, 200, await operations.tick(scopeId === undefined ? {} : { scopeId }))
|
|
100
138
|
// The answer leaves first; the service then finishes its tick and exits.
|
|
101
139
|
response.once('finish', () => { void operations.stop() })
|
|
102
140
|
return send(response, 202, { stopping: true, runtimeId: identity.runtimeId, pid: identity.pid })
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import { randomBytes as cryptoRandomBytes } from 'node:crypto'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
3
4
|
import { AtelierDiagnosticError } from '../../project/config.mjs'
|
|
4
5
|
import { ObsidianContractRefusal } from '../../projection/obsidian/contracts.mjs'
|
|
6
|
+
import { prepareView as productionPrepareView } from '../../projection/obsidian/materialize/index.mjs'
|
|
7
|
+
import { preparePluginFiles } from '../../projection/obsidian/plugin-bridge/bundle.mjs'
|
|
8
|
+
import { publishView as productionPublishView } from '../../projection/obsidian/publication/publisher.mjs'
|
|
5
9
|
import { isoTime } from './documents.mjs'
|
|
6
10
|
import { createMaintenanceEngine } from './engine.mjs'
|
|
7
11
|
import { ObsidianMaintenanceRefusal, refuse } from './errors.mjs'
|
|
8
12
|
import { assertOutsideRepositories, protectedRoots, readLocalPointer, resolveDataRoot, workspaceStateRoot } from './machine-settings.mjs'
|
|
13
|
+
import { createPluginChannel, createPluginSessions, ensurePluginBearer, pluginPresence } from './plugin-channel.mjs'
|
|
14
|
+
import { confirmPluginEntry, confirmPluginSeen, currentPluginChoice, decidePluginChoice, vaultFilePresent, viewVaultRoot } from './plugin-choice.mjs'
|
|
15
|
+
import { createPluginDriftObserver } from './plugin-drift.mjs'
|
|
9
16
|
import { isProcessAlive } from './private-lock.mjs'
|
|
10
17
|
import { probeHealth } from './service-client.mjs'
|
|
11
18
|
import {
|
|
12
|
-
SERVICE_ERROR_SCHEMA, executableIdentity, readLastServiceError, readServiceRecord, readServiceSettings, removeServiceRecord, serviceNameFor, servicePaths,
|
|
19
|
+
SERVICE_ERROR_SCHEMA, executableIdentity, readLastServiceError, readServiceRecord, readServiceSettings, releaseIdentity, removeServiceRecord, serviceNameFor, servicePaths,
|
|
13
20
|
writeLastServiceError, writeServiceRecord,
|
|
14
21
|
} from './service-record.mjs'
|
|
15
22
|
import { createServiceServer } from './service-server.mjs'
|
|
16
|
-
import { createMaintenanceStateStore } from './state-store.mjs'
|
|
23
|
+
import { OPEN_EDIT_STATES, createMaintenanceStateStore } from './state-store.mjs'
|
|
17
24
|
import { DEFAULT_MAX_BACKOFF_MS, DEFAULT_TICK_INTERVAL_MS, createTickLoop } from './tick-loop.mjs'
|
|
18
25
|
import { createFsWatcherFactory } from './watchers.mjs'
|
|
19
26
|
|
|
@@ -33,11 +40,25 @@ import { createFsWatcherFactory } from './watchers.mjs'
|
|
|
33
40
|
// publication that was cut short is settled by the publisher's own restart
|
|
34
41
|
// recovery on a later tick, never by deleting what it left behind.
|
|
35
42
|
//
|
|
43
|
+
// It also holds the plugin channel (plugin-channel.mjs). Every view it
|
|
44
|
+
// prepares carries Atelier's plugin, whose data file names this listener and
|
|
45
|
+
// the view's bearer, unless the person turned the plugin off in that vault
|
|
46
|
+
// (plugin-choice.mjs): then the entry is left alone and only the plugin files
|
|
47
|
+
// that are still there are kept current. A plugin that holds a view open is
|
|
48
|
+
// reported by status and tells the adapter factory the app version it runs in.
|
|
49
|
+
//
|
|
36
50
|
// `adapterFactory` has no default here either: whoever starts the service
|
|
37
51
|
// decides whether it may reach a running app.
|
|
52
|
+
//
|
|
53
|
+
// Started by a login item (`startup`), the service also asks `releaseWatch`
|
|
54
|
+
// after every tick whether the package it runs from changed on disk; when it
|
|
55
|
+
// did, it finishes that tick and shuts down with the reason RELEASE_CHANGED,
|
|
56
|
+
// which its process turns into an exit code the service manager restarts, on
|
|
57
|
+
// the new release (release-watch.mjs, service-main.mjs).
|
|
38
58
|
|
|
39
59
|
export const SERVICE_STATUS_SCHEMA = 'atelier-obsidian-service-status/v1'
|
|
40
60
|
export const DEFAULT_SHUTDOWN_GRACE_MS = 30 * 1000
|
|
61
|
+
export const RELEASE_CHANGED = 'release-changed'
|
|
41
62
|
|
|
42
63
|
// Where a workspace keeps its private state, without creating an identity.
|
|
43
64
|
export function resolveServiceWorkspace({ project, dataRoot, env = process.env, platform = process.platform, create = false }) {
|
|
@@ -72,6 +93,11 @@ export async function runMaintenanceService(options = {}) {
|
|
|
72
93
|
engineOptions = {}, createEngine = createMaintenanceEngine,
|
|
73
94
|
// What the adapter factory last learned about the installed app, when it qualifies one. Codes and versions only.
|
|
74
95
|
appStatus = null,
|
|
96
|
+
// Under `startup`: { changed() } (createReleaseWatch), asked after every tick.
|
|
97
|
+
releaseWatch = null,
|
|
98
|
+
// The entry as this process was started by it (`process.argv[1]`), when that is not its real path: a login item
|
|
99
|
+
// names the installed entry by its path. It is recorded, so the process table can still prove this process ours.
|
|
100
|
+
invokedAs = null,
|
|
75
101
|
} = options
|
|
76
102
|
if (typeof loadProject !== 'function') throw new TypeError('the service needs loadProject')
|
|
77
103
|
if (typeof adapterFactory !== 'function') throw new TypeError('the service needs an adapterFactory')
|
|
@@ -94,10 +120,13 @@ export async function runMaintenanceService(options = {}) {
|
|
|
94
120
|
}
|
|
95
121
|
|
|
96
122
|
const executable = executableIdentity(entryPath)
|
|
123
|
+
const alias = typeof invokedAs === 'string' && path.isAbsolute(invokedAs) && path.resolve(invokedAs) !== executable.path ? path.resolve(invokedAs) : null
|
|
97
124
|
const startedAt = isoTime(clock)
|
|
98
125
|
const identity = { serviceName: serviceNameFor(workspaceId), workspaceId, runtimeId, pid, host, port, executableDigest: executable.digest, startedAt }
|
|
99
126
|
const bearer = randomBytes(32).toString('base64url')
|
|
100
127
|
const stateStore = createMaintenanceStateStore({ workspaceRoot, workspaceId })
|
|
128
|
+
// Session identities live in memory only and are always random, whatever `randomBytes` the service was given.
|
|
129
|
+
const pluginSessions = createPluginSessions()
|
|
101
130
|
|
|
102
131
|
let stopping = false
|
|
103
132
|
let consecutiveFailures = 0
|
|
@@ -105,7 +134,49 @@ export async function runMaintenanceService(options = {}) {
|
|
|
105
134
|
let settleDone
|
|
106
135
|
const done = new Promise((resolve) => { settleDone = resolve })
|
|
107
136
|
|
|
108
|
-
|
|
137
|
+
// Every prepared view carries the plugin with this listener's address and the view's bearer, as the person's choice
|
|
138
|
+
// for that vault allows: the choice is read, and recorded when the vault shows it changed, from the community plugin
|
|
139
|
+
// list as it is now, and those very bytes travel with the view. A bearer or a choice that cannot be kept leaves that
|
|
140
|
+
// view without the plugin for this tick; the view itself is prepared as ever.
|
|
141
|
+
const pluginFor = (scopeId) => {
|
|
142
|
+
try {
|
|
143
|
+
const vaultRoot = viewVaultRoot(workspaceRoot, scopeId, workspaceId)
|
|
144
|
+
const { choice, community } = decidePluginChoice({ workspaceRoot, workspaceId, scopeId, vaultRoot, clock })
|
|
145
|
+
const off = choice.state === 'off'
|
|
146
|
+
const bearer = ensurePluginBearer({ workspaceRoot, workspaceId, scopeId, randomBytes, clock })
|
|
147
|
+
if (pluginChannel.bearers().get(scopeId) !== bearer) pluginChannel.bearersChanged()
|
|
148
|
+
const plugin = preparePluginFiles({ channel: { host, port }, scopeId, bearer, onlyIfPresent: off })
|
|
149
|
+
// Turned off: the files still there are kept current; a folder the person removed is not made again.
|
|
150
|
+
const files = off ? plugin.files.filter((file) => vaultFilePresent(vaultRoot, file.path)) : plugin.files
|
|
151
|
+
const kept = new Set(files.map((file) => file.path))
|
|
152
|
+
const unreadable = community.file.state === 'unreadable'
|
|
153
|
+
return {
|
|
154
|
+
files, ownership: { ...plugin.ownership, files: plugin.ownership.files.filter((entry) => kept.has(entry.path)) },
|
|
155
|
+
community: off || unreadable ? { entry: 'withheld', reason: off ? 'turned-off-in-this-vault' : 'list-unreadable' } : { entry: 'owned', existing: community.file.bytes },
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
log({ at: isoTime(clock), event: 'plugin-not-prepared', code: errorCode(error), name: errorName(error) })
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const prepareWithPlugin = (input) => (engineOptions.seams?.prepareView ?? productionPrepareView)({ ...input, plugin: pluginFor(input.scope.scopeId) })
|
|
163
|
+
// An entry offered to a vault and now in place is confirmed: from then on, a list without it is the person's decision.
|
|
164
|
+
const publishAndConfirm = async (input) => {
|
|
165
|
+
const result = await (engineOptions.seams?.publishView ?? productionPublishView)(input)
|
|
166
|
+
try { confirmPluginEntry({ workspaceRoot, workspaceId, scopeId: input.recoveryStore.scopeId, result, clock }) } catch (error) {
|
|
167
|
+
log({ at: isoTime(clock), event: 'plugin-entry-not-confirmed', code: errorCode(error), name: errorName(error) })
|
|
168
|
+
}
|
|
169
|
+
return result
|
|
170
|
+
}
|
|
171
|
+
// The app version a live plugin reports counts as checked for the view it holds open, while one launch of the plugin
|
|
172
|
+
// alone holds it: with two apps holding the vault, which one the command-line tool reaches is unknown.
|
|
173
|
+
const pluginReportOf = (scopeId) => { const report = pluginSessions.report(scopeId); return report?.instances === 1 ? report : null }
|
|
174
|
+
// The engine also asks the factory itself (`forget`, on a tick somebody asked for): the wrapper keeps its methods.
|
|
175
|
+
const pluginAwareAdapterFactory = Object.assign((input) => adapterFactory({ ...input, pluginReport: typeof input?.scope?.scopeId === 'string' ? pluginReportOf(input.scope.scopeId) : null }), adapterFactory)
|
|
176
|
+
const engine = createEngine({
|
|
177
|
+
watcherFactory: createFsWatcherFactory(), ...engineOptions, seams: { ...(engineOptions.seams ?? {}), prepareView: prepareWithPlugin, publishView: publishAndConfirm },
|
|
178
|
+
loadProject, dataRoot, adapterFactory: pluginAwareAdapterFactory, clock, env, platform, lockOwner: { host, port, runtimeId },
|
|
179
|
+
})
|
|
109
180
|
|
|
110
181
|
const recordIsOurs = () => {
|
|
111
182
|
try { const record = readServiceRecord({ workspaceRoot, workspaceId }); return record !== null && record.runtimeId === runtimeId && record.pid === pid } catch { return false }
|
|
@@ -128,12 +199,41 @@ export async function runMaintenanceService(options = {}) {
|
|
|
128
199
|
writeLastServiceError({ workspaceRoot, workspaceId, document: { schema: SERVICE_ERROR_SCHEMA, workspaceId, runtimeId, code: errorCode(outcome.error), name: errorName(outcome.error), at, consecutiveFailures, totalFailures: (previous?.totalFailures ?? 0) + 1, resolvedAt: null } })
|
|
129
200
|
}
|
|
130
201
|
|
|
202
|
+
// A plugin file a view's committed generation pins that is not on disk as pinned has the view prepared again, so the
|
|
203
|
+
// publisher writes it again; a drift is asked about once, not at every tick.
|
|
204
|
+
const pluginDrift = createPluginDriftObserver({ workspaceRoot, workspaceId })
|
|
205
|
+
const askForDriftedViews = () => {
|
|
206
|
+
try { for (const scopeId of pluginDrift.observe([...pluginChannel.bearers().keys()])) engine.requestPreparation(scopeId) } catch (error) {
|
|
207
|
+
log({ at: isoTime(clock), event: 'plugin-drift-not-read', code: errorCode(error), name: errorName(error) })
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// What a publication of this tick could not change is no drift to ask about again (plugin-drift.mjs).
|
|
211
|
+
const settleDriftLooks = () => {
|
|
212
|
+
try { pluginDrift.settle([...pluginChannel.bearers().keys()]) } catch (error) {
|
|
213
|
+
log({ at: isoTime(clock), event: 'plugin-drift-not-read', code: errorCode(error), name: errorName(error) })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// A package that changed on disk: another release, loaded module by module into this process from then on.
|
|
218
|
+
const releaseChanged = () => {
|
|
219
|
+
if (!startup || releaseWatch === null) return false
|
|
220
|
+
try { return releaseWatch.changed() === true } catch { return false }
|
|
221
|
+
}
|
|
222
|
+
|
|
131
223
|
const loop = createTickLoop({
|
|
132
224
|
intervalMs, maxBackoffMs, onOutcome: recordOutcome,
|
|
133
225
|
async tick() {
|
|
134
226
|
// The service runs only while its record names it: a replaced or removed record ends it, cleanly.
|
|
135
227
|
if (!recordIsOurs()) { void shutdown('record-no-longer-names-this-runtime'); return { state: 'stopping', reason: 'record-no-longer-names-this-runtime' } }
|
|
136
|
-
|
|
228
|
+
askForDriftedViews()
|
|
229
|
+
try {
|
|
230
|
+
const report = await engine.tick()
|
|
231
|
+
if (report?.state === 'ticked') settleDriftLooks()
|
|
232
|
+
return report
|
|
233
|
+
} finally {
|
|
234
|
+
// After the tick, whether it succeeded or not: the next one runs in a process of the release now on disk.
|
|
235
|
+
if (!stopping && releaseChanged()) { log({ at: isoTime(clock), event: RELEASE_CHANGED }); void shutdown(RELEASE_CHANGED) }
|
|
236
|
+
}
|
|
137
237
|
},
|
|
138
238
|
})
|
|
139
239
|
|
|
@@ -150,6 +250,26 @@ export async function runMaintenanceService(options = {}) {
|
|
|
150
250
|
}
|
|
151
251
|
|
|
152
252
|
const healthStatus = () => (stopping ? 'stopped' : consecutiveFailures > 0 ? 'degraded' : 'healthy')
|
|
253
|
+
|
|
254
|
+
// One view as the plugin is told about it: its freshness entry with held notes counted, and its open pending edits.
|
|
255
|
+
function pluginStatusOf(scopeId) {
|
|
256
|
+
let entry = null
|
|
257
|
+
try { entry = stateStore.readFreshness()?.scopes.find((item) => item.scopeId === scopeId) ?? null } catch (error) { if (!isTyped(error)) throw error; return { view: null, pendingEdits: null } }
|
|
258
|
+
let open = null
|
|
259
|
+
try { open = stateStore.readPendingEdits().edits.filter((edit) => edit.scopeId === scopeId && OPEN_EDIT_STATES.includes(edit.state)).length } catch (error) { if (!isTyped(error)) throw error }
|
|
260
|
+
const view = entry === null ? null : {
|
|
261
|
+
state: entry.state, reason: entry.reason, verified: entry.verified, generationId: entry.generationId, preparedGenerationId: entry.preparedGenerationId,
|
|
262
|
+
heldNoteCount: entry.heldNotes.length, retainedEdits: entry.retainedEdits, checkedAt: entry.checkedAt,
|
|
263
|
+
}
|
|
264
|
+
return { view, pendingEdits: open === null ? null : { open } }
|
|
265
|
+
}
|
|
266
|
+
// A plugin that holds a view open shows that its app has the view's entry: an entry only offered so far is confirmed.
|
|
267
|
+
const pluginSeen = (scopeId) => {
|
|
268
|
+
try { confirmPluginSeen({ workspaceRoot, workspaceId, scopeId, clock }) } catch (error) {
|
|
269
|
+
log({ at: isoTime(clock), event: 'plugin-entry-not-confirmed', code: errorCode(error), name: errorName(error) })
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const pluginChannel = createPluginChannel({ workspaceRoot, workspaceId, runtimeId, sessions: pluginSessions, statusOf: pluginStatusOf, serviceStatus: healthStatus, onSessionOpened: pluginSeen })
|
|
153
273
|
const server = createServiceServer({
|
|
154
274
|
identity, bearer,
|
|
155
275
|
operations: {
|
|
@@ -158,14 +278,21 @@ export async function runMaintenanceService(options = {}) {
|
|
|
158
278
|
let lastError = null
|
|
159
279
|
try { lastError = readLastServiceError({ workspaceRoot, workspaceId }) } catch (error) { if (!isTyped(error)) throw error; lastError = { unreadable: error.code } }
|
|
160
280
|
if (lastError?.schema) { const { schema: _schema, workspaceId: _workspace, ...shown } = lastError; lastError = shown }
|
|
161
|
-
return {
|
|
281
|
+
return {
|
|
282
|
+
schema: SERVICE_STATUS_SCHEMA, service: { ...identity, status: healthStatus() }, loop: loop.state(), lastTick, lastError, freshness: freshnessSummary(),
|
|
283
|
+
plugins: pluginPresence({ sessions: pluginSessions, scopeIds: [...pluginChannel.bearers().keys()], entryOf: (scopeId) => currentPluginChoice({ workspaceRoot, workspaceId, scopeId }).state }),
|
|
284
|
+
...(typeof appStatus === 'function' ? { app: appStatus() } : {}),
|
|
285
|
+
}
|
|
162
286
|
},
|
|
163
|
-
|
|
287
|
+
// A tick asked for one view (`open` asks for its own) prepares and publishes that view once more.
|
|
288
|
+
async tick({ scopeId } = {}) {
|
|
289
|
+
if (scopeId !== undefined) engine.requestPreparation?.(scopeId)
|
|
164
290
|
const outcome = await loop.tickNow()
|
|
165
291
|
if (outcome.stopped) return { ok: false, stopped: true }
|
|
166
292
|
return outcome.ok ? { ok: true, ...summary(outcome.report) } : { ok: false, error: { code: errorCode(outcome.error), name: errorName(outcome.error) } }
|
|
167
293
|
},
|
|
168
294
|
stop: () => shutdown('stop-requested'),
|
|
295
|
+
plugin: (command, request) => pluginChannel.handle(command, request),
|
|
169
296
|
},
|
|
170
297
|
})
|
|
171
298
|
|
|
@@ -202,7 +329,7 @@ export async function runMaintenanceService(options = {}) {
|
|
|
202
329
|
workspaceRoot, workspaceId,
|
|
203
330
|
record: {
|
|
204
331
|
schema: 'atelier-obsidian-service-state/v1', contractVersion: '1.0.0', workspaceId, serviceName: identity.serviceName, host, port, runtimeId, pid,
|
|
205
|
-
executable: { path: executable.path, digest: executable.digest, ext: { runner: process.execPath } }, stateLocation: servicePaths(workspaceRoot).stateLocation,
|
|
332
|
+
executable: { path: executable.path, digest: executable.digest, ext: { runner: process.execPath, release: releaseIdentity(), ...(alias === null ? {} : { invokedAs: alias }) } }, stateLocation: servicePaths(workspaceRoot).stateLocation,
|
|
206
333
|
health: { status: 'healthy', checkedAt: startedAt }, consent: settings.consent, ext: { bearer },
|
|
207
334
|
},
|
|
208
335
|
})
|