@mnstry/atelier 0.2.0-alpha.12 → 0.2.0-alpha.14
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 +894 -0
- package/README.md +159 -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 +267 -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 +210 -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 +237 -0
- package/docs/learning.md +215 -0
- package/docs/local-services.md +223 -19
- package/docs/obsidian-contract.md +246 -15
- package/docs/obsidian-plugin.md +252 -7
- package/docs/obsidian.md +874 -52
- package/docs/optional-ingestion.md +83 -0
- package/docs/personal-workspace.md +266 -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 +50 -0
- package/docs/repo-boundary-guard.md +120 -3
- 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 +318 -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 +219 -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 +76 -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 +60 -5
- package/plugins/obsidian/main.js +57 -8
- package/plugins/obsidian/manifest.json +1 -1
- 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 +536 -69
- package/src/boundary/delegation-command.mjs +158 -0
- package/src/boundary/delegation.mjs +396 -0
- package/src/boundary/policy.mjs +185 -64
- 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 +66 -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 +772 -65
- 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 +30 -6
- package/src/graph/knowledge-graph.mjs +77 -9
- 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 +938 -0
- package/src/ingestion/optional/graphify/json_bridge.py +163 -0
- package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
- package/src/ingestion/optional/graphify/native-pins.json +306 -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 +49 -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 +401 -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/path-match.mjs +2 -1
- package/src/projection/obsidian/edits/apply.mjs +55 -16
- package/src/projection/obsidian/materialize/prepare-view.mjs +87 -5
- package/src/projection/obsidian/personal-workspace.mjs +519 -0
- package/src/projection/obsidian/proposals/adapter.mjs +79 -17
- package/src/projection/obsidian/proposals/observation.mjs +50 -4
- package/src/projection/obsidian/publication/publisher.mjs +214 -18
- 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/projection/project.mjs +3 -2
- 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 +437 -56
- 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 +108 -23
- package/src/runtime/obsidian/login-item.mjs +335 -0
- package/src/runtime/obsidian/machine-settings.mjs +119 -12
- package/src/runtime/obsidian/observation.mjs +36 -4
- package/src/runtime/obsidian/opening.mjs +173 -38
- package/src/runtime/obsidian/personal-selection.mjs +125 -0
- package/src/runtime/obsidian/pipeline.mjs +115 -6
- 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/private-lock.mjs +39 -4
- 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 +148 -19
- 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 +102 -9
- 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/runtime/obsidian/view-permission.mjs +334 -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/control-system.mjs +201 -0
- package/src/ui/html-primitives.mjs +3 -0
- package/src/ui/knowledge-page.mjs +1388 -0
- package/src/upgrade/closure-diagnostics.mjs +135 -0
- package/src/upgrade/closure.mjs +649 -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 +20 -7
- package/templates/external-project-workspace/pack-lifecycle.json +7 -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,23 +1,28 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { performance } from 'node:perf_hooks'
|
|
3
4
|
import { AtelierDiagnosticError } from '../../project/config.mjs'
|
|
4
5
|
import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, manifestLayoutVersion } from '../../projection/obsidian/contracts.mjs'
|
|
6
|
+
import { createCooperativeBudget } from '../../projection/obsidian/materialize/prepare-view.mjs'
|
|
7
|
+
import { PERSONAL_VALIDATION_PENDING, notePersonalSelectionNow, personalSelectionBehind, personalWorkspaceBindingOf, personalWorkspaceInputs, validatePersonalWorkspace } from '../../projection/obsidian/personal-workspace.mjs'
|
|
5
8
|
import { PROTOCOL_ID } from '../../projection/obsidian/publication/bridge-script.mjs'
|
|
6
|
-
import { PublicationRefusal } from '../../projection/obsidian/recovery/store.mjs'
|
|
9
|
+
import { PublicationRefusal, allocatedFolderState, hasCommittedGeneration, readVaultAllocation } from '../../projection/obsidian/recovery/store.mjs'
|
|
7
10
|
import { canonicalJson, compareText, isoTime } from './documents.mjs'
|
|
8
11
|
import { readObsidianEnablement } from './enablement.mjs'
|
|
9
|
-
import { ObsidianMaintenanceRefusal } from './errors.mjs'
|
|
12
|
+
import { ObsidianMaintenanceRefusal, refuse } from './errors.mjs'
|
|
10
13
|
import { createMaintenanceExtensions } from './extension-points.mjs'
|
|
11
14
|
import {
|
|
12
15
|
assertOutsideRepositories, authorizeAutomaticApply, defaultMachineSettings, ensureWorkspaceIdentity, protectedRoots, readLocalPointer,
|
|
13
16
|
readMachineSettings, resolveDataRoot, workspaceStateRoot, writeMachineSettings,
|
|
14
17
|
} from './machine-settings.mjs'
|
|
15
|
-
import { configKey, listConfigFiles, listSourceFiles, listVaultNotes, readFileFacts, reconcile, sha256Digest, sourceKey, vaultKey } from './observation.mjs'
|
|
18
|
+
import { configKey, hasIndexedHint, listConfigFiles, listSourceFiles, listVaultNotes, readFileFacts, reconcile, reconcileCooperatively, sha256Digest, sourceKey, vaultKey } from './observation.mjs'
|
|
16
19
|
import { dispatchAutomaticApply, heldPaths, layoutHeldPaths, observeVaultEdits, preserveInRecoveryStore, trustedNoteBases } from './pending-edits.mjs'
|
|
17
|
-
import {
|
|
20
|
+
import { createProductionSeams, eligibilityFor } from './pipeline.mjs'
|
|
21
|
+
import { PLUGIN_FILES_WAIT_FOR_APP } from './plugin-presence.mjs'
|
|
18
22
|
import { ENGINE_LOCK_DIRECTORY, acquirePrivateGenerationLock, createAbandonmentProof } from './private-lock.mjs'
|
|
19
23
|
import { probeHealth } from './service-client.mjs'
|
|
20
24
|
import { FRESHNESS_SCHEMA, LATE_WRITERS_SCHEMA, createMaintenanceStateStore } from './state-store.mjs'
|
|
25
|
+
import { ensureVaultAllocation, projectDisplayName } from './vault-location.mjs'
|
|
21
26
|
import { createNullWatcherFactory } from './watchers.mjs'
|
|
22
27
|
|
|
23
28
|
// The maintenance engine. One explicit `tick()`; no timer, no process, no
|
|
@@ -100,6 +105,15 @@ export const ENGINE_PRIMITIVES = Object.freeze({
|
|
|
100
105
|
},
|
|
101
106
|
// The embedded assets observed beside the walk: those the last built graph lets a view copy, and no withheld one.
|
|
102
107
|
observedAssetsOf: (graph) => (graph.assets ?? []).filter((asset) => asset.eligible === true).map(({ repo, path: assetPath }) => ({ repo, path: assetPath })),
|
|
108
|
+
// A change the guard sees in the middle of a tick is read, where it may be a confirmed selection, while it is there.
|
|
109
|
+
noteSelectionAtGuard: notePersonalSelectionNow,
|
|
110
|
+
// A loaded project pinned to a selection record below the highest record this process has read the history ending at
|
|
111
|
+
// is loaded again at the next tick, whatever the files say: another reader in the process may have read a
|
|
112
|
+
// confirmation that is gone again before this engine looked (personalSelectionBehind).
|
|
113
|
+
reloadsBehindSelection: personalSelectionBehind,
|
|
114
|
+
// After a tick that failed without a typed reason, the next one looks at every view again, as the first tick of a
|
|
115
|
+
// process does: see tick().
|
|
116
|
+
rechecksEveryViewAfterFailure: true,
|
|
103
117
|
// One engine per workspace at a time, across processes.
|
|
104
118
|
acquireEngineLock: acquirePrivateGenerationLock,
|
|
105
119
|
// The registered proposal adapter observes the open edits on every tick, so a structural edit is recorded as
|
|
@@ -123,10 +137,45 @@ export const ENGINE_PRIMITIVES = Object.freeze({
|
|
|
123
137
|
heldNoteDigest({ file }) {
|
|
124
138
|
try { return readFileFacts(file)?.digest ?? null } catch { return null }
|
|
125
139
|
},
|
|
140
|
+
// Whether an error met by the check of a tick's settings, at a wait, is a failure to read them: a file-system error
|
|
141
|
+
// (no permission, an I/O error, no descriptor left), recognised as one is where a vault is placed. Anything else is a
|
|
142
|
+
// fault of the engine and stops the tick.
|
|
143
|
+
guardReadFailed: (error) => typeof error?.code === 'string' && /^E[A-Z]+$/.test(error.code),
|
|
144
|
+
// Whether an error met when the graph build, a view's preparation or the look again before its commit reads a
|
|
145
|
+
// source is a source, or a folder of sources, removed after this tick observed it: no such file (or a parent that is
|
|
146
|
+
// no longer a folder), at a path that is gone now and that this tick's walks over the sources (before the build, and
|
|
147
|
+
// the look again after it) recorded with a digest (a source of a repository the project observes), or a folder that
|
|
148
|
+
// held such a source. A source changed during the tick is refused as `mixed-read`; one removed is refused the same,
|
|
149
|
+
// and the next tick reads everything again. Anything else stops the tick, as before: a path still there, a path the
|
|
150
|
+
// walk listed and could not read (it was recorded without a digest), or one it never listed, so a path that can never
|
|
151
|
+
// be read still fails every tick.
|
|
152
|
+
sourceRemovedSinceObserved(error, { project, index }) {
|
|
153
|
+
if ((error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') || typeof error.path !== 'string' || !(index instanceof Map)) return false
|
|
154
|
+
const missing = path.resolve(error.path)
|
|
155
|
+
if (fs.existsSync(missing)) return false
|
|
156
|
+
for (const repo of project?.repos ?? []) {
|
|
157
|
+
if (repo?.external || typeof repo?.path !== 'string' || typeof repo?.name !== 'string') continue
|
|
158
|
+
const relative = path.relative(path.resolve(repo.path), missing)
|
|
159
|
+
if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) continue
|
|
160
|
+
const posix = relative.split(path.sep).join('/')
|
|
161
|
+
if (typeof index.get(sourceKey(repo.name, posix))?.digest === 'string') return true
|
|
162
|
+
const folder = sourceKey(repo.name, `${posix}/`)
|
|
163
|
+
for (const [key, entry] of index) if (key.startsWith(folder) && typeof entry?.digest === 'string') return true
|
|
164
|
+
}
|
|
165
|
+
return false
|
|
166
|
+
},
|
|
126
167
|
})
|
|
127
168
|
|
|
128
169
|
const isTypedRefusal = (error) => error instanceof ObsidianMaintenanceRefusal || error instanceof AtelierDiagnosticError || error instanceof ObsidianContractRefusal || error instanceof PublicationRefusal
|
|
170
|
+
// The code a view is refused with when its own vault cannot be placed: a typed refusal's, or `vault-location-unusable`
|
|
171
|
+
// for a file-system error there (a file in the way, no permission, a read-only volume). Null for anything else, which
|
|
172
|
+
// is a fault of the engine and stops the tick.
|
|
173
|
+
const viewRefusalCode = (error) => (isTypedRefusal(error) ? error.code : typeof error?.code === 'string' && /^E[A-Z]+$/.test(error.code) ? 'vault-location-unusable' : null)
|
|
129
174
|
const digestOfJson = (value) => sha256Digest(Buffer.from(canonicalJson(value)))
|
|
175
|
+
// The configuration a tick observes. A project bound to a personal workspace adds the person's authored manifest and
|
|
176
|
+
// overlay and its generation's record: a change to any of them is a configuration change, so every view is prepared
|
|
177
|
+
// again, asks the composition, and is refused at once when the generation no longer holds.
|
|
178
|
+
export const configFilesOf = (project) => [...listConfigFiles(project), ...personalWorkspaceInputs(project).map((absolute) => ({ key: configKey(absolute), changeClass: 'config', absolute }))]
|
|
130
179
|
|
|
131
180
|
// An editor adapter that `build` makes, synchronously or not, the first time the publisher calls it; a refusal of
|
|
132
181
|
// `build` is thrown from that call, and `refusal()` answers it afterwards.
|
|
@@ -142,8 +191,16 @@ function builtOnFirstUse(build) {
|
|
|
142
191
|
publish: async (payload) => (await built()).publish(payload),
|
|
143
192
|
}
|
|
144
193
|
}
|
|
194
|
+
// The adapter a publication gets when the factory refused the app: it coordinates with nothing and never calls it.
|
|
195
|
+
function uncoordinatedAdapter(refusal) {
|
|
196
|
+
const never = async () => { throw refusal }
|
|
197
|
+
return { probe: async () => ({ state: 'uncoordinated', reason: `${refusal.code}: the installed Obsidian does not qualify, and nothing is published through it` }), inspect: never, collect: never, publish: never }
|
|
198
|
+
}
|
|
145
199
|
// Views a tick may be asked to prepare again, at most, before it runs.
|
|
146
200
|
const MAX_PREPARATION_REQUESTS = 64
|
|
201
|
+
// At most this many views a tick refuses for a file-system error are named with that error's code (`refusalCauses`); the
|
|
202
|
+
// others of that tick are not named.
|
|
203
|
+
const MAX_REFUSAL_CAUSES = 64
|
|
147
204
|
|
|
148
205
|
function journalTime(journalId) {
|
|
149
206
|
const match = /^journal-(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{3})-/.exec(journalId)
|
|
@@ -166,19 +223,48 @@ function journalsToRecheck(store, { nowMs, full, windowMs }) {
|
|
|
166
223
|
export function createMaintenanceEngineForOracleTests(options = {}, primitives = ENGINE_PRIMITIVES) {
|
|
167
224
|
const {
|
|
168
225
|
loadProject, dataRoot, adapterFactory, clock,
|
|
169
|
-
|
|
226
|
+
// Which notes may enter a view: what the machine settings decide at each tick (eligibilityFor), unless a test fixes it.
|
|
227
|
+
watcherFactory = createNullWatcherFactory(), extensions = createMaintenanceExtensions(), eligibility: fixedEligibility = null,
|
|
170
228
|
fullReconciliationIntervalMs = DEFAULT_FULL_RECONCILIATION_INTERVAL_MS, retryIntervalMs = DEFAULT_RETRY_INTERVAL_MS, lateWriterWindowMs = DEFAULT_LATE_WRITER_WINDOW_MS,
|
|
171
229
|
publicationRetryMs = DEFAULT_PUBLICATION_RETRY_MS, observeApp = null,
|
|
230
|
+
// Reads the app's own vault list without asking the app (readAppVaultListForAllocation): { ok: true, vaults } with
|
|
231
|
+
// the map id -> { path }, or { ok: false, code }. A folder for a new vault is never allocated inside a vault it
|
|
232
|
+
// lists, nor under a name a vault it lists already has, nor while the list cannot be read.
|
|
233
|
+
readAppVaultList = null,
|
|
234
|
+
// Reads the app's own vault list from its file, without asking the app (unheld-evidence.mjs): { unlisted: true } only
|
|
235
|
+
// when no list names a view's vault, a folder above it or one inside it. A view never published into its allocated
|
|
236
|
+
// folder is then published even while an app runs that cannot be coordinated with, creating files only (the
|
|
237
|
+
// publisher's `direct-unheld` path). An engine given no reader never takes that path.
|
|
238
|
+
readUnheldEvidence = null,
|
|
172
239
|
quietPeriodMs, lstat = fs.lstatSync, randomBytes, env = process.env, platform = process.platform,
|
|
173
240
|
// A service names where it answers health, so a lock it leaves behind can be proven abandoned.
|
|
174
241
|
lockOwner = null, lockProbe = probeHealth,
|
|
242
|
+
// Answers at once, without loading, what the workspace is resolved from (locatePersonalHome), for a loader that can
|
|
243
|
+
// refuse before any project was loaded: the views published before are then told why nothing is published now.
|
|
244
|
+
locateProject = null,
|
|
175
245
|
} = options
|
|
176
246
|
if (typeof loadProject !== 'function') throw new TypeError('the engine needs loadProject')
|
|
177
247
|
if (typeof clock !== 'function') throw new TypeError('the engine needs an injected clock')
|
|
178
248
|
// No default: the production adapter reaches a running app, and only the owner of the lifecycle may decide that.
|
|
179
249
|
if (typeof adapterFactory !== 'function') throw new TypeError('the engine needs an adapterFactory')
|
|
180
|
-
const
|
|
250
|
+
const production = createProductionSeams()
|
|
251
|
+
const seams = { ...production, ...(options.seams ?? {}) }
|
|
252
|
+
// A caller that replaced prepareView and left the cooperative preparation as it ships prepares views its own way: its
|
|
253
|
+
// seam is used, synchronously, whether it handed in that one member or the production seams with that member replaced.
|
|
254
|
+
if (seams.prepareView !== production.prepareView && seams.prepareViewCooperatively === production.prepareViewCooperatively) seams.prepareViewCooperatively = null
|
|
255
|
+
// The same for the graph: a caller that replaced buildGraph and left the cooperative build as it ships builds its own way.
|
|
256
|
+
if (seams.buildGraph !== production.buildGraph && seams.buildGraphCooperatively === production.buildGraphCooperatively) seams.buildGraphCooperatively = null
|
|
181
257
|
const rules = { ...ENGINE_PRIMITIVES, ...primitives }
|
|
258
|
+
// Optional local qualification telemetry. It has no authority to change a
|
|
259
|
+
// tick's result, and records elapsed boundaries rather than a budget claim.
|
|
260
|
+
const phaseTime = () => performance.timeOrigin + performance.now()
|
|
261
|
+
const reportPhase = (phase, startedMs) => {
|
|
262
|
+
try { seams.onPhase?.({ phase, startedMs, endedMs: phaseTime() }) } catch { /* telemetry never changes custody */ }
|
|
263
|
+
}
|
|
264
|
+
const measured = (phase, operation) => {
|
|
265
|
+
const startedMs = phaseTime()
|
|
266
|
+
try { return operation() } finally { reportPhase(phase, startedMs) }
|
|
267
|
+
}
|
|
182
268
|
|
|
183
269
|
const index = new Map()
|
|
184
270
|
const hintedKeys = new Set()
|
|
@@ -203,6 +289,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
203
289
|
let watching = { signature: null, handle: null }
|
|
204
290
|
let known = null // { stateStore, maintenanceMode } once a workspace has been resolved
|
|
205
291
|
let heldLock = null
|
|
292
|
+
let releaseOwed = false
|
|
206
293
|
// Views somebody asked to have prepared and published once more, at the next tick.
|
|
207
294
|
const preparationRequests = new Set()
|
|
208
295
|
// The views the project declared at the last tick; null before the first.
|
|
@@ -215,7 +302,23 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
215
302
|
const waitingForApp = new Map()
|
|
216
303
|
const proveAbandoned = createAbandonmentProof({ probe: lockProbe })
|
|
217
304
|
|
|
305
|
+
// Whether a view kept from publication by an app that does not qualify may still be published on the publisher's
|
|
306
|
+
// `direct-unheld` path: never committed, its vault the folder allocated for it, and the app's list naming no folder
|
|
307
|
+
// on the way. The publisher checks every condition again, under its locks.
|
|
308
|
+
async function unheldMayApply(store, pointer) {
|
|
309
|
+
if (typeof readUnheldEvidence !== 'function' || pointer !== null || store.vaultOrigin !== 'allocated') return false
|
|
310
|
+
try { return (await readUnheldEvidence({ vaultRoot: store.vaultRoot }))?.unlisted === true } catch { return false }
|
|
311
|
+
}
|
|
312
|
+
|
|
218
313
|
async function takeLock(workspaceRoot, workspaceId) {
|
|
314
|
+
// A lock an earlier tick could not give back is still this process's, and its ticket on disk unreleased: a tick
|
|
315
|
+
// that tried to take it anew would find its own ticket and answer `busy` until the process ends. It is given back
|
|
316
|
+
// first. A release that fails again fails this tick, and is owed to the next.
|
|
317
|
+
if (heldLock && releaseOwed) {
|
|
318
|
+
heldLock.release()
|
|
319
|
+
heldLock = null
|
|
320
|
+
releaseOwed = false
|
|
321
|
+
}
|
|
219
322
|
if (heldLock) return { acquired: true }
|
|
220
323
|
const lock = await rules.acquireEngineLock({
|
|
221
324
|
workspaceRoot, directory: path.join(workspaceRoot, ENGINE_LOCK_DIRECTORY), workspaceId, purpose: 'maintenance-engine', service: lockOwner, clock, proveAbandoned,
|
|
@@ -224,10 +327,18 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
224
327
|
return lock
|
|
225
328
|
}
|
|
226
329
|
|
|
330
|
+
// Gives the lock back at the end of a tick. One that cannot be given back (its released ticket could not be written)
|
|
331
|
+
// stays held, and the failure is the tick's: the next tick gives it back before anything else. One given back owes
|
|
332
|
+
// nothing more, whether this tick took it or an earlier one could not give it back.
|
|
227
333
|
function dropLock() {
|
|
228
334
|
const lock = heldLock
|
|
335
|
+
if (lock === null) return
|
|
336
|
+
try { lock.release() } catch (error) {
|
|
337
|
+
releaseOwed = true
|
|
338
|
+
throw error
|
|
339
|
+
}
|
|
229
340
|
heldLock = null
|
|
230
|
-
|
|
341
|
+
releaseOwed = false
|
|
231
342
|
}
|
|
232
343
|
|
|
233
344
|
function onWatcherEvent(roots, { rootId, relative }) {
|
|
@@ -249,9 +360,18 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
249
360
|
}
|
|
250
361
|
|
|
251
362
|
function storeFor(scope, workspaceRoot, workspaceId, repositoryRoots) {
|
|
252
|
-
|
|
363
|
+
// A view's vault can be allocated, made again or moved while the engine runs: the store follows its record. A
|
|
364
|
+
// store is used again only while its allocated folder is still the one the record names; otherwise it is dropped,
|
|
365
|
+
// and the store made in its place refuses, typed (vault-allocation-replaced, say), so a folder replaced while the
|
|
366
|
+
// service runs, or a link put where it was, is never published into.
|
|
367
|
+
const signature = JSON.stringify([workspaceRoot, workspaceId, repositoryRoots, readVaultAllocation({ workspaceRoot, workspaceId, scopeId: scope.scopeId })])
|
|
253
368
|
const cached = stores.get(scope.scopeId)
|
|
254
|
-
|
|
369
|
+
// A vault root that is gone (a folder under the data root the person removed, say) is looked for again too, so the
|
|
370
|
+
// store made in its place says why rather than failing on the folder that is not there.
|
|
371
|
+
if (cached?.signature === signature && fs.lstatSync(cached.store.vaultRoot, { throwIfNoEntry: false }) !== undefined) {
|
|
372
|
+
try { cached.store.checkAllocatedVault?.(); return cached.store } catch (error) { if (!isTypedRefusal(error)) throw error }
|
|
373
|
+
}
|
|
374
|
+
stores.delete(scope.scopeId)
|
|
255
375
|
const store = seams.createRecoveryStore({ workspaceRoot, workspaceId, scopeId: scope.scopeId, repositoryRoots })
|
|
256
376
|
stores.set(scope.scopeId, { signature, store })
|
|
257
377
|
return store
|
|
@@ -275,11 +395,13 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
275
395
|
// Persists "refused" or "disabled" over whatever is known, and only when a workspace already has state.
|
|
276
396
|
async function persistOutcome({ enablement, state, reason, now, scopeIds = [] }) {
|
|
277
397
|
if (!known) {
|
|
398
|
+
let located = project
|
|
399
|
+
try { located ??= locateProject === null ? null : locateProject() } catch { located = null }
|
|
278
400
|
let pointer = null
|
|
279
|
-
try { pointer =
|
|
401
|
+
try { pointer = located ? readLocalPointer(located) : null } catch { pointer = null }
|
|
280
402
|
if (!pointer) return
|
|
281
403
|
let root
|
|
282
|
-
try { root = workspaceStateRoot(resolveDataRoot({ dataRoot, pointer, project, env, platform }), pointer.workspaceId) } catch { return }
|
|
404
|
+
try { root = workspaceStateRoot(resolveDataRoot({ dataRoot, pointer, project: located, env, platform }), pointer.workspaceId) } catch { return }
|
|
283
405
|
const stateStore = createMaintenanceStateStore({ workspaceRoot: root, workspaceId: pointer.workspaceId })
|
|
284
406
|
if (!stateStore.exists()) return
|
|
285
407
|
known = { stateStore: createMaintenanceStateStore({ workspaceRoot: fs.realpathSync(root), workspaceId: pointer.workspaceId }), maintenanceMode: 'manual' }
|
|
@@ -318,15 +440,19 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
318
440
|
|
|
319
441
|
// 1. Project and enablement. The digests are seeded before the project is
|
|
320
442
|
// loaded for good, so the loaded project is never older than the index.
|
|
443
|
+
// A loader may answer a promise (one that composes a personal workspace off
|
|
444
|
+
// the event loop). A project bound to a personal workspace is loaded once:
|
|
445
|
+
// what a generation holds is fixed, and an input that changed after the load
|
|
446
|
+
// changes the validity key every build is checked against.
|
|
321
447
|
if (project === null) {
|
|
322
|
-
project = loadProject()
|
|
323
|
-
reconcile({ index, files:
|
|
324
|
-
project = loadProject()
|
|
448
|
+
project = await loadProject()
|
|
449
|
+
reconcile({ index, files: configFilesOf(project), prefix: CONFIG_PREFIX, full: true, lstat })
|
|
450
|
+
if (personalWorkspaceBindingOf(project) === null) project = await loadProject()
|
|
325
451
|
} else {
|
|
326
|
-
const config = reconcile({ index, files:
|
|
327
|
-
if (config.changes.length > 0) {
|
|
328
|
-
project = loadProject()
|
|
329
|
-
reconcile({ index, files:
|
|
452
|
+
const config = reconcile({ index, files: configFilesOf(project), prefix: CONFIG_PREFIX, full, hinted, lstat })
|
|
453
|
+
if (config.changes.length > 0 || rules.reloadsBehindSelection(project)) {
|
|
454
|
+
project = await loadProject()
|
|
455
|
+
reconcile({ index, files: configFilesOf(project), prefix: CONFIG_PREFIX, full: true, lstat })
|
|
330
456
|
}
|
|
331
457
|
}
|
|
332
458
|
const enablement = readObsidianEnablement(project)
|
|
@@ -351,33 +477,112 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
351
477
|
if (!lock.acquired) return { state: 'busy', reason: 'engine-lock-held', lock: { reason: lock.reason, holder: lock.holder ?? null }, changes: [], scopes: [], pendingEdits: [], dispatched: [], lateWriters: [] }
|
|
352
478
|
const machine = readMachineSettings({ workspaceRoot, workspaceId })
|
|
353
479
|
?? writeMachineSettings({ workspaceRoot, workspaceId, repositoryRoots, settings: defaultMachineSettings({ workspaceId, updatedAt: now }) })
|
|
480
|
+
const eligibility = fixedEligibility ?? eligibilityFor({ machine, project })
|
|
354
481
|
const stateStore = createMaintenanceStateStore({ workspaceRoot, workspaceId })
|
|
355
482
|
known = { stateStore, maintenanceMode: machine.maintenanceMode }
|
|
356
|
-
|
|
483
|
+
// A view refused for a file-system error (a vault folder that could not be allocated or placed, a configuration
|
|
484
|
+
// file that could not be read again, a source removed while it was read, candidates that could not be staged) is
|
|
485
|
+
// settled under the refusal's code alone, and such a tick ends without a failure. The error's code (`EACCES`, `EIO`,
|
|
486
|
+
// `ENOENT`), and that of a cleanup after it that failed too, are reported beside it for the service's log. Only
|
|
487
|
+
// codes shaped as a file-system error's are kept: never a message, which may name a path.
|
|
488
|
+
const refusalCauses = []
|
|
489
|
+
const errnoOf = (value) => (typeof value === 'string' && /^E[A-Z]+$/.test(value) ? value : null)
|
|
490
|
+
const noteRefusalCause = (scopeId, refusal) => {
|
|
491
|
+
const cause = errnoOf(refusal?.detail?.cause)
|
|
492
|
+
const cleanupCause = errnoOf(refusal?.detail?.cleanupCause)
|
|
493
|
+
if ((cause === null && cleanupCause === null) || refusalCauses.length >= MAX_REFUSAL_CAUSES) return
|
|
494
|
+
refusalCauses.push({ scopeId, code: refusal.code, ...(cause === null ? {} : { cause }), ...(cleanupCause === null ? {} : { cleanupCause }) })
|
|
495
|
+
}
|
|
496
|
+
// A view whose own vault cannot be placed: a typed refusal's code and detail, or `vault-location-unusable` for a
|
|
497
|
+
// file-system error there, whose code is its cause.
|
|
498
|
+
const refuseViewAlone = (scopeId, error) => {
|
|
499
|
+
const code = viewRefusalCode(error)
|
|
500
|
+
if (code === null) throw error
|
|
501
|
+
allocationRefusals.set(scopeId, code)
|
|
502
|
+
noteRefusalCause(scopeId, isTypedRefusal(error) ? error : { code, detail: { cause: error.code } })
|
|
503
|
+
}
|
|
504
|
+
// Each view's vault: allocated now where this workspace decided its vaults live, when it has none and was never
|
|
505
|
+
// published under the data root (vault-location.mjs). A view whose folder cannot be allocated is not prepared on
|
|
506
|
+
// this tick, and its freshness says why; the other views go on.
|
|
507
|
+
const allocationRefusals = new Map()
|
|
508
|
+
if (machine.decisions.location !== null) {
|
|
509
|
+
// The app's list, read once per tick and only when a view is about to be allocated. An engine given no reader
|
|
510
|
+
// coordinates with no app, and knows no list. A list that cannot be read allocates nothing: the view says why,
|
|
511
|
+
// and its folder is allocated at a later tick, once the list reads.
|
|
512
|
+
let list = null
|
|
513
|
+
const appList = async () => {
|
|
514
|
+
if (typeof readAppVaultList !== 'function') return { ok: true, vaults: null }
|
|
515
|
+
if (list === null) {
|
|
516
|
+
let answer
|
|
517
|
+
try { answer = await readAppVaultList() } catch { answer = null }
|
|
518
|
+
list = answer?.ok === true ? { ok: true, vaults: answer.vaults ?? {} } : { ok: false, code: typeof answer?.code === 'string' ? answer.code : 'obsidian-settings-unreadable' }
|
|
519
|
+
}
|
|
520
|
+
return list
|
|
521
|
+
}
|
|
522
|
+
// The folders allocated to the views; a record that cannot be read refuses only its own view, below.
|
|
523
|
+
const allocatedPaths = enablement.scopes.map((scope) => { try { return readVaultAllocation({ workspaceRoot, workspaceId, scopeId: scope.scopeId })?.path } catch { return undefined } }).filter((item) => typeof item === 'string')
|
|
524
|
+
for (const scope of enablement.scopes) {
|
|
525
|
+
try {
|
|
526
|
+
let vaults = null
|
|
527
|
+
// The list is read where a folder is about to be made: a first allocation, or an allocated folder that has gone.
|
|
528
|
+
const record = readVaultAllocation({ workspaceRoot, workspaceId, scopeId: scope.scopeId })
|
|
529
|
+
if (record === null ? !hasCommittedGeneration({ workspaceRoot, scopeId: scope.scopeId }) : allocatedFolderState(record) === 'missing') {
|
|
530
|
+
const known = await appList()
|
|
531
|
+
if (!known.ok) refuse('app-vault-list-unreadable', 'Obsidian\'s vault list could not be read, so no folder is allocated for this view yet; it is tried again at the next tick', { cause: known.code })
|
|
532
|
+
vaults = known.vaults
|
|
533
|
+
}
|
|
534
|
+
ensureVaultAllocation({ workspaceRoot, workspaceId, scopeId: scope.scopeId, location: machine.decisions.location, projectName: projectDisplayName(project), repositoryRoots, vaults, allocatedPaths, now })
|
|
535
|
+
} catch (error) { refuseViewAlone(scope.scopeId, error) }
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// Each view's store, on its own: one whose vault cannot be placed (a record naming a folder inside a repository,
|
|
539
|
+
// say) is refused alone, and the other views go on.
|
|
540
|
+
const scopes = []
|
|
541
|
+
for (const scope of enablement.scopes) {
|
|
542
|
+
if (allocationRefusals.has(scope.scopeId)) continue
|
|
543
|
+
try { scopes.push({ scope, store: storeFor(scope, workspaceRoot, workspaceId, repositoryRoots) }) } catch (error) { refuseViewAlone(scope.scopeId, error) }
|
|
544
|
+
}
|
|
357
545
|
|
|
358
546
|
watch([
|
|
359
547
|
...(project.repos ?? []).filter((repo) => !repo.external && typeof repo.path === 'string').map((repo) => ({ id: `repo:${repo.name}`, path: repo.path, recursive: true, keyPrefix: sourceKey(repo.name, ''), keyOf: (relative) => sourceKey(repo.name, relative) })),
|
|
360
548
|
...scopes.map(({ scope, store }) => ({ id: `vault:${scope.scopeId}`, path: store.vaultRoot, recursive: true, keyPrefix: vaultKey(scope.scopeId, ''), keyOf: (relative) => vaultKey(scope.scopeId, relative) })),
|
|
361
|
-
...[...new Set(
|
|
549
|
+
...[...new Set(configFilesOf(project).map((file) => path.dirname(file.absolute)))].filter((directory) => fs.existsSync(directory))
|
|
362
550
|
.map((directory) => ({ id: `config:${directory}`, path: directory, recursive: false, keyPrefix: CONFIG_PREFIX, keyOf: (relative) => configKey(path.join(directory, relative)) })),
|
|
363
551
|
])
|
|
364
552
|
|
|
365
553
|
// 3. Vault notes: preserve, then queue. Compared with the bytes each note
|
|
366
554
|
// was generated with, not with the previous look.
|
|
367
|
-
|
|
368
|
-
|
|
555
|
+
//
|
|
556
|
+
// Every view's notes are looked at first, and the walk waits between notes, so a large vault does not hold the
|
|
557
|
+
// listener for its whole length (at a full reconciliation every note is hashed, and after a publication the
|
|
558
|
+
// watcher has named each note it wrote). Nothing is checked at those waits and nothing is written: within
|
|
559
|
+
// Atelier only this engine commits a manifest or writes the pending edits, under the lock this tick holds, so the
|
|
560
|
+
// manifest read before the walk is the one after it. A note changed after its turn is the next tick's to see, by
|
|
561
|
+
// its stat hint or the watcher, as one changed just after a walk that ran through is. The pending edits are then
|
|
562
|
+
// read, compared and written in one stretch, as before; the bytes an edit keeps are read there, not at the walk.
|
|
369
563
|
const basesOf = new Map()
|
|
370
564
|
const earlierLayout = new Set()
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
565
|
+
const looked = []
|
|
566
|
+
const vaultStartedMs = phaseTime()
|
|
567
|
+
try {
|
|
568
|
+
const budget = createCooperativeBudget()
|
|
569
|
+
for (const { scope, store } of scopes) {
|
|
570
|
+
const { manifest, bases } = trustedNoteBases(store)
|
|
571
|
+
basesOf.set(scope.scopeId, bases)
|
|
572
|
+
if (!manifest) continue
|
|
573
|
+
if (manifestLayoutVersion(manifest) === 1) earlierLayout.add(scope.scopeId)
|
|
574
|
+
const vault = await reconcileCooperatively({ index, files: listVaultNotes({ scopeId: scope.scopeId, vaultRoot: store.vaultRoot, manifest }), prefix: vaultKey(scope.scopeId, ''), full, hinted, lstat }, budget)
|
|
575
|
+
looked.push({ scope, store, manifest, bases, vault })
|
|
576
|
+
}
|
|
577
|
+
} finally { reportPhase('vault-observation', vaultStartedMs) }
|
|
578
|
+
const pending = stateStore.readPendingEdits()
|
|
579
|
+
let edits = pending.edits
|
|
580
|
+
for (const { scope, store, manifest, bases, vault } of looked) {
|
|
377
581
|
const digestOf = (notePath) => index.get(vaultKey(scope.scopeId, notePath))?.digest ?? null
|
|
378
582
|
const observed = observeVaultEdits({ store, workspaceId, scopeId: scope.scopeId, manifest, bases, digestOf, edits, now, preserve: rules.preserve })
|
|
379
583
|
edits = observed.edits
|
|
380
|
-
// A note that has gone is
|
|
584
|
+
// A note that has gone is a change, once: the view is prepared again and its read-back says the note is missing.
|
|
585
|
+
// The publisher writes it back when it next publishes a generation of the view.
|
|
381
586
|
if (observed.events.length > 0 || vault.changes.some((change) => change.kind === 'removed')) changes.push({ changeClass: 'vault-note', scopeId: scope.scopeId })
|
|
382
587
|
}
|
|
383
588
|
stateStore.writePendingEdits({ ...pending, edits })
|
|
@@ -385,9 +590,12 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
385
590
|
// Observation. The registered proposal adapter is handed a copy of the pending edits and observes the open ones
|
|
386
591
|
// the object store does not know yet, a bounded number per tick: each is classified from its preserved bytes
|
|
387
592
|
// against the source as it is now and recorded as what it is, in manual and in automatic mode alike, and no
|
|
388
|
-
// source is written. What refused before anything was recorded is offered again on a full reconciliation only
|
|
593
|
+
// source is written. What refused before anything was recorded is offered again on a full reconciliation only;
|
|
594
|
+
// for a personal workspace, also on the tick after one whose graph was pending, which offers every earlier
|
|
595
|
+
// refusal again.
|
|
389
596
|
const proposalAdapter = extensions.get('proposal-adapter')
|
|
390
|
-
|
|
597
|
+
// A personal workspace is never composed on this event loop by the adapter: its builds defer, as the engine's do.
|
|
598
|
+
const adapterContext = () => ({ project, workspaceRoot, workspaceId, repositoryRoots, edits: structuredClone(edits), clock, env, deferPersonalValidation: true })
|
|
391
599
|
let observed = null
|
|
392
600
|
if (proposalAdapter !== null && rules.observeEdits(proposalAdapter)) {
|
|
393
601
|
try { observed = await proposalAdapter.observe(adapterContext(), { retryRefused: full }) } catch { observed = { adapterId: proposalAdapter.id, failed: 'proposal-adapter-threw' } }
|
|
@@ -417,24 +625,33 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
417
625
|
const lateWriters = []
|
|
418
626
|
const lateDocument = stateStore.readLateWriters()
|
|
419
627
|
const knownFindings = new Set(lateDocument.findings.map((item) => `${item.scopeId}\u0000${item.journalId}\u0000${item.unit}\u0000${item.observedDigest}`))
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
628
|
+
const lateStartedMs = phaseTime()
|
|
629
|
+
try {
|
|
630
|
+
for (const { scope, store } of scopes) {
|
|
631
|
+
const journalIds = journalsToRecheck(store, { nowMs, full, windowMs: lateWriterWindowMs })
|
|
632
|
+
if (journalIds.length === 0) continue
|
|
633
|
+
for (const finding of seams.recheckDisplacedFiles({ store, journalIds, clock })) {
|
|
634
|
+
if (finding.code !== 'late-writer-captured') continue
|
|
635
|
+
const key = `${scope.scopeId}\u0000${finding.journalId}\u0000${finding.unit}\u0000${finding.observedDigest}`
|
|
636
|
+
if (knownFindings.has(key)) continue
|
|
637
|
+
knownFindings.add(key)
|
|
638
|
+
lateWriters.push({ scopeId: scope.scopeId, journalId: finding.journalId, unit: finding.unit, notePath: finding.notePath ?? null, digestAtMove: finding.digestAtMove ?? null, observedDigest: finding.observedDigest, objectRef: finding.objectRef, detectedAt: now })
|
|
639
|
+
}
|
|
429
640
|
}
|
|
430
|
-
}
|
|
641
|
+
} finally { reportPhase('late-writer-recheck', lateStartedMs) }
|
|
431
642
|
if (lateWriters.length > 0) {
|
|
432
643
|
const findings = [...lateDocument.findings, ...lateWriters].sort((left, right) => compareText(left.detectedAt, right.detectedAt) || compareText(left.journalId, right.journalId) || left.unit - right.unit || compareText(left.observedDigest, right.observedDigest))
|
|
433
644
|
stateStore.writeLateWriters({ schema: LATE_WRITERS_SCHEMA, workspaceId, findings })
|
|
434
645
|
}
|
|
435
646
|
|
|
436
|
-
// 6. Sources, settings, scopes, eligibility.
|
|
437
|
-
|
|
647
|
+
// 6. Sources, settings, scopes, eligibility. The walk over the sources waits between files, as the walk over the
|
|
648
|
+
// vault does; at a full reconciliation it hashes every one. A source changed after its turn is the next tick's to
|
|
649
|
+
// see, by its stat hint or the watcher, or at the latest by the next full reconciliation. When this tick builds the
|
|
650
|
+
// graph, the look again at the sources after the build (`graph-source-recheck`) refuses such a change as
|
|
651
|
+
// `mixed-read`.
|
|
652
|
+
const sourcesStartedMs = phaseTime()
|
|
653
|
+
let sources
|
|
654
|
+
try { sources = await reconcileCooperatively({ index, files: listSourceFiles(project, { assets: observedAssets }), prefix: SOURCE_PREFIX, full, hinted, lstat }, createCooperativeBudget()) } finally { reportPhase('source-observation', sourcesStartedMs) }
|
|
438
655
|
for (const change of sources.changes) changes.push({ changeClass: change.changeClass })
|
|
439
656
|
const { scopes: _declared, ...settingsWithoutScopes } = enablement.settings
|
|
440
657
|
// What the configuration says apart from this integration's own member: a change to the member alone is an
|
|
@@ -455,7 +672,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
455
672
|
if (full) { lastFullMs = nowMs; forceFull = false }
|
|
456
673
|
|
|
457
674
|
// 7. Which views are invalid.
|
|
458
|
-
declaredScopes = new Set(scopes.map((
|
|
675
|
+
declaredScopes = new Set(enablement.scopes.map((scope) => scope.scopeId))
|
|
459
676
|
const previous = readPreviousFreshness(stateStore)
|
|
460
677
|
const entries = new Map()
|
|
461
678
|
const unseen = new Set()
|
|
@@ -479,35 +696,151 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
479
696
|
if (earlierLayout.has(scope.scopeId) && entry.state === 'current' && layoutHeldOf(scope.scopeId).length === 0) invalidate(scope.scopeId, null)
|
|
480
697
|
}
|
|
481
698
|
for (const change of changes) for (const { scope } of scopes) if (change.scopeId === undefined || change.scopeId === scope.scopeId) invalidate(scope.scopeId, change.changeClass)
|
|
699
|
+
// A bound personal workspace is composed again, off the event loop, at every full reconciliation, whatever else is
|
|
700
|
+
// being prepared: a cause outside its validity key can stop a generation holding without a file this engine
|
|
701
|
+
// observes. Its views are prepared again only when the composition refuses, or confirms another key.
|
|
702
|
+
let personalRefusal = null
|
|
703
|
+
if (full && personalWorkspaceBindingOf(project) !== null) {
|
|
704
|
+
const verdict = await validatePersonalWorkspace(project)
|
|
705
|
+
if (verdict.ok !== true) personalRefusal = verdict.code
|
|
706
|
+
if (verdict.ok !== true || verdict.changed) for (const { scope } of scopes) invalidate(scope.scopeId, null)
|
|
707
|
+
}
|
|
482
708
|
|
|
483
709
|
// Invalidation is durable before any work: a view is not reported current while it is being rebuilt.
|
|
484
710
|
for (const [scopeId, classes] of attempt) entries.set(scopeId, { ...demote(entries.get(scopeId), 'stale', 'invalidated', now), changeClasses: [...classes].sort() })
|
|
711
|
+
for (const [scopeId, code] of allocationRefusals) {
|
|
712
|
+
entries.set(scopeId, demote(previous?.scopes.find((entry) => entry.scopeId === scopeId && entry.state !== 'disabled') ?? blankEntry(scopeId, now), 'stale', code, now))
|
|
713
|
+
}
|
|
485
714
|
for (const entry of previous?.scopes ?? []) if (!entries.has(entry.scopeId)) entries.set(entry.scopeId, demote(entry, 'disabled', 'scope-not-configured', now))
|
|
486
715
|
const persist = () => stateStore.writeFreshness(freshnessDocument({ workspaceId, enablement: 'enabled', maintenanceMode: machine.maintenanceMode, now, entries }))
|
|
487
716
|
persist()
|
|
488
717
|
|
|
489
718
|
// 8. Rebuild and publish.
|
|
490
719
|
if (attempt.size > 0) {
|
|
720
|
+
// Cooperative preparation/publication can outlive the settings that
|
|
721
|
+
// selected this tick. Reuse the existing configuration/personal-input
|
|
722
|
+
// observation keys, machine settings and eligibility revision as the
|
|
723
|
+
// fence; do not invent another selection authority.
|
|
724
|
+
const bindings = configFilesOf(project).map(file => ({ ...file, digest: index.get(file.key)?.digest ?? null }))
|
|
725
|
+
const machineDigest = digestOfJson(machine)
|
|
726
|
+
const eligibilityRevision = String(eligibility.revision())
|
|
727
|
+
// The history's end is read once for a run of stopped views, however many views one change stops; a check that
|
|
728
|
+
// passes ends the run, so a change that stops a later view is read again.
|
|
729
|
+
let selectionNoted = false
|
|
730
|
+
const noteSelectionOnce = () => { if (!selectionNoted) { selectionNoted = true; rules.noteSelectionAtGuard(project) } }
|
|
731
|
+
const assertBindings = () => {
|
|
732
|
+
let changed
|
|
733
|
+
try {
|
|
734
|
+
changed = bindings.some(file => (readFileFacts(file.absolute)?.digest ?? null) !== file.digest)
|
|
735
|
+
|| digestOfJson(readMachineSettings({ workspaceRoot, workspaceId })) !== machineDigest
|
|
736
|
+
|| String(eligibility.revision()) !== eligibilityRevision
|
|
737
|
+
} catch (error) {
|
|
738
|
+
// A file that cannot be read now is not known to hold what this tick observed. The view is refused as one
|
|
739
|
+
// whose settings changed, and the next tick reads everything again: what an unreadable configuration file
|
|
740
|
+
// means is decided there, by observation and the project loader, as before.
|
|
741
|
+
if (!rules.guardReadFailed(error)) throw error
|
|
742
|
+
forceFull = true
|
|
743
|
+
// The file may be a selection record that cannot be read now (no permission, an I/O error, a file replaced while
|
|
744
|
+
// it was read): the history's end is read for the note.
|
|
745
|
+
noteSelectionOnce()
|
|
746
|
+
refuse('mixed-read', 'a file that decides source selection could not be read again while a view was being prepared or published', { cause: error.code })
|
|
747
|
+
}
|
|
748
|
+
if (changed) {
|
|
749
|
+
forceFull = true
|
|
750
|
+
// The change may be a confirmation in a personal home. The guard compares files and reads no history, so the
|
|
751
|
+
// history's end is read here for the note: a record that stopped this view counts from now on, also when it
|
|
752
|
+
// is gone again before the next tick looks.
|
|
753
|
+
noteSelectionOnce()
|
|
754
|
+
refuse('mixed-read', 'source selection or eligibility changed while a view was being prepared or published')
|
|
755
|
+
}
|
|
756
|
+
selectionNoted = false
|
|
757
|
+
}
|
|
491
758
|
let built = null
|
|
759
|
+
// Whether a selected source of a prepared view moved since this tick observed it, decided as observation
|
|
760
|
+
// decides (reconcile): a source that still has the stat hint it was indexed with is taken as unchanged, and one
|
|
761
|
+
// whose hint moved is read and must still be the pinned bytes. Bytes that drift under an unchanged hint stay
|
|
762
|
+
// within observation's own bound, as they do for the graph and for a reused note: the next full reconciliation
|
|
763
|
+
// sees them. It waits between sources, and the settings that selected the view are checked at each wait.
|
|
764
|
+
const assertSourcesUnmoved = async (prepared) => {
|
|
765
|
+
const startedMs = phaseTime()
|
|
766
|
+
try {
|
|
767
|
+
const budget = createCooperativeBudget({ guard: assertBindings })
|
|
768
|
+
budget.check()
|
|
769
|
+
const roots = new Map((project.repos ?? []).filter((repo) => !repo.external && typeof repo.path === 'string').map((repo) => [repo.name, repo.path]))
|
|
770
|
+
const selected = new Set(prepared.manifest.notes.map((note) => note.nodeId))
|
|
771
|
+
const attachments = new Set(prepared.manifest.attachments.map((item) => {
|
|
772
|
+
const source = item.ext?.[OBSIDIAN_EXT_KEY]
|
|
773
|
+
return source?.kind === 'embedded-asset' ? sourceKey(source.repoId, source.assetPath) : null
|
|
774
|
+
}))
|
|
775
|
+
const nodes = built.snapshot.graph.nodes.filter((node) => selected.has(node.id))
|
|
776
|
+
const assets = (built.snapshot.graph.assets ?? []).filter((asset) => attachments.has(sourceKey(asset.repo, asset.path)))
|
|
777
|
+
const pins = new Map(built.snapshot.document.repositories.flatMap((repo) => repo.files.map((file) => [sourceKey(repo.repoId, file.path), file])))
|
|
778
|
+
for (const source of [...nodes, ...assets]) {
|
|
779
|
+
await budget.checkpoint('commit-source-recheck')
|
|
780
|
+
const key = sourceKey(source.repo, source.path)
|
|
781
|
+
const root = roots.get(source.repo)
|
|
782
|
+
if (root !== undefined && hasIndexedHint(index.get(key), path.join(root, ...source.path.split('/')), lstat)) continue
|
|
783
|
+
const bytes = built.snapshot.readSource(source.repo, source.path)
|
|
784
|
+
const pin = pins.get(key)
|
|
785
|
+
if (!pin || !Buffer.isBuffer(bytes) || bytes.length !== pin.byteLength || sha256Digest(bytes) !== pin.rawDigest) refuse('mixed-read', 'a selected source changed while its view was being prepared or published; its generation is not committed')
|
|
786
|
+
}
|
|
787
|
+
budget.check()
|
|
788
|
+
} finally { reportPhase('commit-source-recheck', startedMs) }
|
|
789
|
+
}
|
|
492
790
|
try {
|
|
493
|
-
|
|
791
|
+
if (personalRefusal !== null) refuse(personalRefusal, 'the personal workspace refused this generation; nothing is prepared from it', { source: 'personal-workspace' })
|
|
792
|
+
// A personal workspace whose validity key is not confirmed yet is composed off the event loop, then built again.
|
|
793
|
+
const build = () => seams.buildGraph({ project, eligibility, cache: graphCache, index, deferPersonalValidation: true })
|
|
794
|
+
let graph
|
|
795
|
+
try {
|
|
796
|
+
// Any other project is built with the event loop given a turn between the Markdown sources of the census: a
|
|
797
|
+
// first tick reads every source. The settings that select the views are checked at each wait, as during a
|
|
798
|
+
// preparation, and the look again at the sources after the build sees a source that moved during it.
|
|
799
|
+
if (typeof seams.buildGraphCooperatively === 'function' && personalWorkspaceBindingOf(project) === null) {
|
|
800
|
+
graph = await seams.buildGraphCooperatively({ project, eligibility, cache: graphCache, index, scheduling: { guard: assertBindings } })
|
|
801
|
+
} else try { graph = build() } catch (error) {
|
|
802
|
+
if (error?.code !== PERSONAL_VALIDATION_PENDING) throw error
|
|
803
|
+
const verdict = await validatePersonalWorkspace(project)
|
|
804
|
+
if (verdict?.ok !== true) refuse(verdict?.code ?? 'personal-binding-unrecognized', 'the personal workspace refused this generation; nothing is prepared from it', { source: 'personal-workspace' })
|
|
805
|
+
try { graph = build() } catch (again) {
|
|
806
|
+
if (again?.code !== PERSONAL_VALIDATION_PENDING) throw again
|
|
807
|
+
refuse('personal-graph-unconfirmed', 'the graph built for the personal workspace is not the one its composition built; nothing is prepared from it', { source: 'personal-workspace' })
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
} catch (error) {
|
|
811
|
+
if (!isTypedRefusal(error) && rules.sourceRemovedSinceObserved(error, { project, index })) refuse('mixed-read', 'a source was removed while the canonical graph was being built', { cause: error.code })
|
|
812
|
+
throw error
|
|
813
|
+
}
|
|
494
814
|
// The assets this graph lets a view copy are observed from now on, and hashed now so the snapshot pins
|
|
495
815
|
// what observation saw. A withheld asset is not observed: its bytes can change no view.
|
|
496
816
|
const formerAssetKeys = new Set(observedAssets.map((asset) => sourceKey(asset.repo, asset.path)))
|
|
497
817
|
observedAssets = rules.observedAssetsOf(graph)
|
|
498
818
|
const assetKeys = new Set(observedAssets.map((asset) => sourceKey(asset.repo, asset.path)))
|
|
499
|
-
const settled = reconcile({ index, files: listSourceFiles(project, { assets: observedAssets }), prefix: SOURCE_PREFIX, full: false, lstat })
|
|
819
|
+
const settled = measured('graph-source-recheck', () => reconcile({ index, files: listSourceFiles(project, { assets: observedAssets }), prefix: SOURCE_PREFIX, full: false, lstat }))
|
|
500
820
|
// An asset newly observed, or one no longer embedded, is the list changing. Anything else that moved since this tick looked at the sources moved while the graph was being read.
|
|
501
821
|
if (settled.changes.some((change) => !(change.kind === 'added' && assetKeys.has(change.key)) && !(change.kind === 'removed' && formerAssetKeys.has(change.key) && !assetKeys.has(change.key)))) throw new ObsidianMaintenanceRefusal('mixed-read', 'a source changed while the canonical graph was being built')
|
|
502
822
|
const configDigest = digestOfJson([...index].filter(([key]) => key.startsWith(CONFIG_PREFIX)).map(([key, entry]) => [path.basename(key.slice(CONFIG_PREFIX.length)), entry.digest]))
|
|
823
|
+
const snapshot = seams.captureSnapshot({ project, graph, workspaceId, index, configDigest, capturedAt: now })
|
|
824
|
+
const observedProject = project
|
|
503
825
|
built = {
|
|
504
|
-
|
|
826
|
+
// A view's preparation, and the look again at its sources before the commit, read a source through the
|
|
827
|
+
// snapshot. A selected source removed after this tick observed it, read there, refuses the view as
|
|
828
|
+
// `mixed-read`, as a removal during the build does; the other views of the tick are still prepared.
|
|
829
|
+
snapshot: { ...snapshot, readSource: (repoId, relative) => {
|
|
830
|
+
try { return snapshot.readSource(repoId, relative) } catch (error) {
|
|
831
|
+
if (!isTypedRefusal(error) && rules.sourceRemovedSinceObserved(error, { project: observedProject, index })) refuse('mixed-read', 'a selected source was removed while its view was being prepared or published', { cause: error.code })
|
|
832
|
+
throw error
|
|
833
|
+
}
|
|
834
|
+
} },
|
|
505
835
|
profile: seams.profileFor({ project, workspaceId, audienceAllow: machine.audienceAllow }),
|
|
506
836
|
}
|
|
507
837
|
} catch (error) {
|
|
508
838
|
if (!isTypedRefusal(error)) throw error
|
|
509
|
-
for (const scopeId of attempt.keys()) entries.set(scopeId, demote(entries.get(scopeId), 'stale', error.code, now))
|
|
839
|
+
for (const scopeId of attempt.keys()) { entries.set(scopeId, demote(entries.get(scopeId), 'stale', error.code, now)); noteRefusalCause(scopeId, error) }
|
|
510
840
|
if (REREAD_CODES.has(error.code)) forceFull = true
|
|
841
|
+
// The personal workspace refused the bound generation: the project is loaded again at the next tick, so a
|
|
842
|
+
// loader that now binds another generation is followed.
|
|
843
|
+
if (error.detail?.source === 'personal-workspace') project = null
|
|
511
844
|
}
|
|
512
845
|
for (const { scope, store } of built ? scopes.filter((item) => attempt.has(item.scope.scopeId)) : []) {
|
|
513
846
|
const { scopeId } = scope
|
|
@@ -520,11 +853,17 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
520
853
|
}
|
|
521
854
|
try {
|
|
522
855
|
const held = heldPaths(edits, scopeId)
|
|
523
|
-
const prepared = seams.prepareView({
|
|
856
|
+
const prepared = await (seams.prepareViewCooperatively ?? seams.prepareView)({
|
|
524
857
|
snapshot: built.snapshot, profile: built.profile, scope, persistentPathRegistry: stateStore.readPathRegistry(), priorManifest: store.readCurrentManifest(),
|
|
525
858
|
existingSettings: null, clock, vaultRootBytes: Buffer.byteLength(store.vaultRoot, 'utf8'), cache: preparationCacheFor(scopeId),
|
|
526
859
|
heldNotePaths: held, layoutHeldNotePaths: layoutHeldOf(scopeId), viewScopeIds: scopes.map((item) => item.scope.scopeId),
|
|
860
|
+
// The cooperative preparation would read every selected source once more before it returns. The engine
|
|
861
|
+
// checks them itself, below and again before the commit, without opening a source observation would not.
|
|
862
|
+
scheduling: { guard: assertBindings, recheckSources: false },
|
|
527
863
|
})
|
|
864
|
+
assertBindings()
|
|
865
|
+
// Before anything is written: a source that moved while the view was being prepared refuses it here.
|
|
866
|
+
await assertSourcesUnmoved(prepared)
|
|
528
867
|
stateStore.writePathRegistry(prepared.persistentPathRegistry)
|
|
529
868
|
diagnostics = (prepared.manifest.ext?.[OBSIDIAN_EXT_KEY]?.diagnostics ?? []).slice(0, 100)
|
|
530
869
|
const preparedGenerationId = prepared.manifest.generationId
|
|
@@ -543,13 +882,32 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
543
882
|
// that cannot be qualified never makes a current view stale. Any other publication builds the adapter first,
|
|
544
883
|
// and an app that does not qualify keeps the publisher from being reached at all.
|
|
545
884
|
const lazy = trusted()?.generationId === preparedGenerationId ? builtOnFirstUse(() => adapterFactory({ store, scope })) : null
|
|
546
|
-
|
|
885
|
+
// A view never published into its allocated folder is not kept from its first publication by an app that
|
|
886
|
+
// does not qualify, while that app's list shows no entry for the folder: the publisher is reached with an
|
|
887
|
+
// adapter that coordinates with nothing, and may take its `direct-unheld` path. Any other outcome is the
|
|
888
|
+
// factory's refusal, as before.
|
|
889
|
+
let heldBackBy = null
|
|
890
|
+
let adapter = lazy
|
|
891
|
+
if (adapter === null) {
|
|
892
|
+
try { adapter = await adapterFactory({ store, scope }) } catch (error) {
|
|
893
|
+
if (!isTypedRefusal(error) || !(await unheldMayApply(store, trusted()))) throw error
|
|
894
|
+
heldBackBy = error
|
|
895
|
+
adapter = uncoordinatedAdapter(error)
|
|
896
|
+
}
|
|
897
|
+
}
|
|
547
898
|
let result
|
|
548
899
|
try {
|
|
900
|
+
// Once more while the publisher holds its locks, before it says the publication settled.
|
|
901
|
+
const beforeCommit = () => assertSourcesUnmoved(prepared)
|
|
549
902
|
result = await seams.publishView({
|
|
550
903
|
preparedView: prepared, protocolId: PROTOCOL_ID, expectedGeneration: trusted()?.generationId ?? null, recoveryStore: store, adapter, clock,
|
|
551
904
|
...(quietPeriodMs === undefined ? {} : { quietPeriodMs }),
|
|
905
|
+
...(typeof readUnheldEvidence === 'function' ? { unheldEvidence: readUnheldEvidence, platform } : {}),
|
|
906
|
+
scheduling: { guard: assertBindings }, beforeCommit,
|
|
552
907
|
})
|
|
908
|
+
// Stopped before its commit for want of an app that coordinates: the app's refusal is what the person has to
|
|
909
|
+
// act on, and `open` answers it before it would add the vault to the app.
|
|
910
|
+
if (heldBackBy !== null && (result.state === 'refused' || (result.state === 'updating' && result.notes.some((note) => note.blocking && note.outcome === 'editor-uncoordinated')))) throw heldBackBy
|
|
553
911
|
} catch (error) {
|
|
554
912
|
// The publisher asked for the app only to publish the committed generation again (a plugin file drifted),
|
|
555
913
|
// and the app did not qualify: nothing was written, and the committed generation is read back as it is.
|
|
@@ -563,15 +921,24 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
563
921
|
const common = { generationId: pointerNow?.generationId ?? null, preparedGenerationId, retainedEdits: pointerNow?.retained?.length ?? 0 }
|
|
564
922
|
if (result.state === 'refused') {
|
|
565
923
|
settle(CONFLICT_REFUSALS.has(result.refusal.code) ? 'publisher-conflict' : 'stale', result.refusal.code, { ...common, heldNotes: held })
|
|
924
|
+
noteRefusalCause(scopeId, result.refusal)
|
|
566
925
|
} else if (result.state === 'updating') {
|
|
567
926
|
const blocking = result.notes.filter((note) => note.blocking)
|
|
568
927
|
const byEdit = blocking.length > 0 && blocking.every((note) => EDIT_OUTCOMES.has(note.outcome))
|
|
569
928
|
settle(byEdit ? 'held-for-your-edit' : 'publisher-conflict', blocking[0]?.outcome ?? 'publication-incomplete', { ...common, heldNotes: [...new Set([...held, ...blocking.filter((note) => EDIT_OUTCOMES.has(note.outcome)).map((note) => note.path)])].sort(compareText) })
|
|
570
929
|
} else {
|
|
571
|
-
// Read back: every note of the committed generation, by digest.
|
|
572
|
-
|
|
930
|
+
// Read back: every note of the committed generation, by digest. The walk waits between notes, so a large
|
|
931
|
+
// view does not hold the service's listener for its whole length. Nothing is checked at those waits: the
|
|
932
|
+
// generation is committed, the view is reported as it was read, and what changes from here on is the next
|
|
933
|
+
// tick's to see, as it is after a read-back that ran through.
|
|
934
|
+
const readBackStartedMs = phaseTime()
|
|
935
|
+
let bases
|
|
936
|
+
try {
|
|
937
|
+
const committed = trustedNoteBases(store)
|
|
938
|
+
bases = committed.bases
|
|
939
|
+
await reconcileCooperatively({ index, files: listVaultNotes({ scopeId, vaultRoot: store.vaultRoot, manifest: committed.manifest }), prefix: vaultKey(scopeId, ''), full: true, lstat }, createCooperativeBudget())
|
|
940
|
+
} finally { reportPhase('publication-read-back', readBackStartedMs) }
|
|
573
941
|
basesOf.set(scopeId, bases)
|
|
574
|
-
reconcile({ index, files: listVaultNotes({ scopeId, vaultRoot: store.vaultRoot, manifest }), prefix: vaultKey(scopeId, ''), full: true, lstat })
|
|
575
942
|
const differing = [...bases].filter(([notePath, base]) => index.get(vaultKey(scopeId, notePath))?.digest !== base.digest).map(([notePath]) => notePath).sort(compareText)
|
|
576
943
|
const editedUnderPublisher = result.notes.some((note) => note.outcome === 'edit-kept' || note.changedAfterPublication === true)
|
|
577
944
|
if (held.length > 0) settle('held-for-your-edit', 'edit-pending', { ...common, heldNotes: held })
|
|
@@ -579,7 +946,8 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
579
946
|
// Somebody wrote during the publication. The next tick preserves and queues it.
|
|
580
947
|
else if (differing.length > 0 || editedUnderPublisher) settle('updating', 'read-back-differs', common)
|
|
581
948
|
else if (common.generationId !== preparedGenerationId) settle('stale', 'committed-generation-differs', common)
|
|
582
|
-
|
|
949
|
+
// Kept current while a plugin file waits for the app: the reason says so, and status shows it on the plugin line.
|
|
950
|
+
else settle('current', result.waitsForApp === true ? PLUGIN_FILES_WAIT_FOR_APP : result.alreadyCommitted ? 'verified-by-read-back' : 'published-and-verified', { ...common, heldNotes: [], verified: true })
|
|
583
951
|
}
|
|
584
952
|
} catch (error) {
|
|
585
953
|
if (!isTypedRefusal(error)) { settle('stale', 'publisher-error'); persist(); throw error }
|
|
@@ -587,6 +955,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
587
955
|
const where = error.code === 'redaction-failure' ? error.detail ?? {} : {}
|
|
588
956
|
diagnostics = typeof where.rule === 'string' ? [{ code: error.code, rule: where.rule, ...(typeof where.notePath === 'string' ? { notePath: where.notePath } : {}), ...(typeof where.filePath === 'string' ? { filePath: where.filePath } : {}) }] : []
|
|
589
957
|
settle('stale', error.code)
|
|
958
|
+
noteRefusalCause(scopeId, error)
|
|
590
959
|
if (REREAD_CODES.has(error.code)) forceFull = true
|
|
591
960
|
}
|
|
592
961
|
}
|
|
@@ -605,7 +974,7 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
605
974
|
state: 'ticked', full, workspaceId, maintenanceMode: machine.maintenanceMode,
|
|
606
975
|
changes: changes.map((change) => ({ ...change })), scopes: [...entries.values()].sort((left, right) => compareText(left.scopeId, right.scopeId)),
|
|
607
976
|
pendingEdits: edits.filter((edit) => edit.closedAt === null).map(({ editId, scopeId, path: notePath, state }) => ({ editId, scopeId, path: notePath, state })),
|
|
608
|
-
dispatched, lateWriters, ...(observed === null ? {} : { observed }), ...(proposals === null ? {} : { proposals }),
|
|
977
|
+
dispatched, lateWriters, ...(observed === null ? {} : { observed }), ...(proposals === null ? {} : { proposals }), ...(refusalCauses.length === 0 ? {} : { refusalCauses }),
|
|
609
978
|
}
|
|
610
979
|
}
|
|
611
980
|
|
|
@@ -627,7 +996,19 @@ export function createMaintenanceEngineForOracleTests(options = {}, primitives =
|
|
|
627
996
|
try {
|
|
628
997
|
return await runTick()
|
|
629
998
|
} catch (error) {
|
|
630
|
-
if (!isTypedRefusal(error))
|
|
999
|
+
if (!isTypedRefusal(error)) {
|
|
1000
|
+
// A tick that failed without a typed reason may have looked at files and recorded what it saw in the index
|
|
1001
|
+
// without acting on it: a note gone from a vault, a source that changed, or a configuration file that changed
|
|
1002
|
+
// before the project could be loaded again, is a change once, and the next look would find nothing to change.
|
|
1003
|
+
// The next tick therefore starts as the first tick of a process does: it loads the project again and looks
|
|
1004
|
+
// at every view, so whatever is missing or changed makes its view stale, or is published, not left current.
|
|
1005
|
+
if (rules.rechecksEveryViewAfterFailure) {
|
|
1006
|
+
project = null
|
|
1007
|
+
semantic = null
|
|
1008
|
+
firstTick = true
|
|
1009
|
+
}
|
|
1010
|
+
throw error
|
|
1011
|
+
}
|
|
631
1012
|
// Fail closed: nothing is published under a refusal, and every known view says why.
|
|
632
1013
|
try { await persistOutcome({ enablement: 'refused', state: 'stale', reason: error.code, now: isoTime(clock) }) } catch { /* the refusal itself is still reported */ }
|
|
633
1014
|
project = null
|