@mnstry/atelier 0.2.0-alpha.12 → 0.2.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +661 -0
- package/README.md +142 -6
- package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
- package/contracts/atelier-architecture.v1.schema.json +258 -0
- package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
- package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
- package/contracts/atelier-build.v1.schema.json +1076 -0
- package/contracts/atelier-capability.v1.schema.json +1252 -0
- package/contracts/atelier-coordination.v1.schema.json +1050 -0
- package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
- package/contracts/atelier-harness.v1.schema.json +234 -0
- package/contracts/atelier-ingestion.v1.schema.json +806 -0
- package/contracts/atelier-inquiry.v1.schema.json +1772 -0
- package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
- package/contracts/atelier-interaction.v1.schema.json +553 -0
- package/contracts/atelier-judgment.v1.schema.json +690 -0
- package/contracts/atelier-knowledge.v1.schema.json +1512 -0
- package/contracts/atelier-learning.v1.schema.json +1095 -0
- package/contracts/atelier-migration.v3.schema.json +64 -0
- package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
- package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
- package/contracts/atelier-template-adoption.v1.schema.json +223 -0
- package/contracts/atelier-template-profile.v1.schema.json +1168 -0
- package/contracts/atelier-trackable.v1.schema.json +781 -0
- package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
- package/docs/architecture.md +89 -0
- package/docs/assurance-controls.md +2 -2
- package/docs/blocks/capability-stewardship.md +17 -0
- package/docs/blocks/claims.md +16 -3
- package/docs/blocks/inquiry-harnesses.md +19 -0
- package/docs/blocks/learning-harnesses.md +17 -0
- package/docs/capability-stewardship.md +269 -0
- package/docs/coordination.md +51 -0
- package/docs/decision-practices.md +111 -0
- package/docs/decisions.md +29 -0
- package/docs/evidence-navigation.md +237 -0
- package/docs/exact-upgrades.md +3 -2
- package/docs/ingestion.md +181 -0
- package/docs/inquiry-harnesses.md +211 -0
- package/docs/install.md +80 -3
- package/docs/intake-and-guides.md +5 -0
- package/docs/integration-contract-decisions.md +24 -2
- package/docs/interaction-and-reflection.md +72 -0
- package/docs/knowledge-graph.md +20 -0
- package/docs/knowledge-health/api.md +30 -0
- package/docs/knowledge-health/assessment/api.md +96 -0
- package/docs/knowledge-health/assessment/capabilities.md +40 -0
- package/docs/knowledge-health/assessment/evaluation.md +27 -0
- package/docs/knowledge-health/assessment/facilitator.md +30 -0
- package/docs/knowledge-health/assessment/quickstart.md +61 -0
- package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
- package/docs/knowledge-health/devday/facilitator.md +16 -0
- package/docs/knowledge-health/devday/participant-response.md +22 -0
- package/docs/knowledge-health/devday/public-default.md +107 -0
- package/docs/knowledge-health/devday/quickstart.md +18 -0
- package/docs/knowledge-health/devday/support.md +22 -0
- package/docs/knowledge-health/exercise.md +17 -0
- package/docs/knowledge-health/facilitator.md +20 -0
- package/docs/knowledge-health/quickstart.md +94 -0
- package/docs/knowledge-health/readme.md +15 -0
- package/docs/knowledge-health/support.md +34 -0
- package/docs/knowledge-setup.md +248 -0
- package/docs/knowledge-workspace.md +264 -0
- package/docs/learning-harnesses.md +238 -0
- package/docs/learning.md +215 -0
- package/docs/local-services.md +181 -16
- package/docs/obsidian-contract.md +223 -15
- package/docs/obsidian-plugin.md +38 -4
- package/docs/obsidian.md +613 -51
- package/docs/optional-ingestion.md +83 -0
- package/docs/personal-workspace.md +257 -0
- package/docs/practical-judgment.md +92 -0
- package/docs/progressive-ingestion-acceptance.md +89 -0
- package/docs/progressive-ingestion.md +235 -0
- package/docs/project-command-inventory.md +1 -0
- package/docs/release-engineering.md +49 -0
- package/docs/repo-boundary-guard.md +94 -0
- package/docs/research-starter-quickstart.md +66 -0
- package/docs/research-starter.md +114 -0
- package/docs/semantic-ingestion.md +198 -0
- package/docs/semantic-operation.md +317 -0
- package/docs/skill-steward.md +6 -0
- package/docs/template-profile.md +164 -0
- package/docs/template-upgrades.md +114 -0
- package/docs/trackables.md +111 -0
- package/docs/upgrade.md +47 -2
- package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
- package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
- package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
- package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
- package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
- package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
- package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
- package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
- package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
- package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
- package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
- package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
- package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
- package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
- package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
- package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
- package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
- package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
- package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
- package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
- package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
- package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
- package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
- package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
- package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
- package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
- package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
- package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
- package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
- package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
- package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
- package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
- package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
- package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
- package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
- package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
- package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
- package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
- package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
- package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
- package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
- package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
- package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
- package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
- package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
- package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
- package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
- package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
- package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
- package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
- package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
- package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
- package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
- package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
- package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
- package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
- package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
- package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
- package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
- package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
- package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
- package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
- package/fixtures/atelier-interaction/act/valid/document.json +24 -0
- package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
- package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
- package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
- package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
- package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
- package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
- package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
- package/fixtures/atelier-judgment/case/valid/document.json +75 -0
- package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
- package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
- package/fixtures/atelier-judgment/native-decision.md +3 -0
- package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
- package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
- package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
- package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
- package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
- package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
- package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
- package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
- package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
- package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
- package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
- package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
- package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
- package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-learning/invalid/authority.json +14 -0
- package/fixtures/atelier-learning/valid/capture.json +13 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
- package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
- package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
- package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
- package/fixtures/atelier-trackable/request/valid/document.json +22 -0
- package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
- package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
- package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
- package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
- package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
- package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
- package/fixtures/capability-packages/adoption.example.json +70 -0
- package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
- package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
- package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
- package/fixtures/capability-packages/research/capability-package.json +88 -0
- package/fixtures/capability-packages/research/capability-release.json +134 -0
- package/fixtures/capability-packages/research/evaluation.md +15 -0
- package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
- package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
- package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
- package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
- package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
- package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
- package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
- package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
- package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
- package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
- package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
- package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
- package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
- package/fixtures/evidence-navigation/valid/claim.json +36 -0
- package/fixtures/evidence-navigation/valid/host.json +16 -0
- package/fixtures/evidence-navigation/valid/profile.json +16 -0
- package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
- package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
- package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
- package/fixtures/harnesses/learning-cycle.json +780 -0
- package/fixtures/inquiry/workshop.json +675 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
- package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
- package/fixtures/knowledge-health/calibration-suite.json +26 -0
- package/fixtures/knowledge-health/calibration-trial.json +37 -0
- package/fixtures/knowledge-health/corrected/checklist.md +18 -0
- package/fixtures/knowledge-health/devday/participant-record.json +19 -0
- package/fixtures/knowledge-health/expected-summary.json +27 -0
- package/fixtures/knowledge-health/workshop/README.md +20 -0
- package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
- package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
- package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
- package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
- package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
- package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
- package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
- package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
- package/fixtures/personal-workspace/overlay.json +24 -0
- package/fixtures/templates/local-library.v1.json +75 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
- package/package.json +59 -5
- package/skills/claude/atelier-build-harness/SKILL.md +59 -0
- package/skills/claude/atelier-coordination/SKILL.md +32 -0
- package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/claude/atelier-interaction/SKILL.md +34 -0
- package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/claude/atelier-research-harness/SKILL.md +43 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
- package/skills/claude/atelier-trackables/SKILL.md +40 -0
- package/skills/codex/atelier-build-harness/SKILL.md +59 -0
- package/skills/codex/atelier-coordination/SKILL.md +32 -0
- package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/codex/atelier-interaction/SKILL.md +34 -0
- package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/codex/atelier-research-harness/SKILL.md +43 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
- package/skills/codex/atelier-trackables/SKILL.md +40 -0
- package/src/architecture/catalog.mjs +355 -0
- package/src/architecture/index.mjs +93 -0
- package/src/boundary/content-rules.mjs +4 -2
- package/src/boundary/delegation-command.mjs +158 -0
- package/src/boundary/delegation.mjs +396 -0
- package/src/boundary/policy.mjs +52 -10
- package/src/build/git.mjs +18 -0
- package/src/build/index.mjs +3 -0
- package/src/build/ledger.mjs +88 -0
- package/src/capabilities/adoption.mjs +381 -0
- package/src/capabilities/evidence.mjs +140 -0
- package/src/capabilities/files.mjs +124 -0
- package/src/capabilities/index.mjs +4 -0
- package/src/capabilities/instructions.mjs +250 -0
- package/src/capabilities/package.mjs +136 -0
- package/src/cli/command-failure.mjs +36 -0
- package/src/cli/execute-command.mjs +16 -2
- package/src/cli/run.mjs +65 -9
- package/src/coauthor/store.mjs +43 -2
- package/src/collaboration/proposals.mjs +189 -0
- package/src/commands/architecture.mjs +27 -0
- package/src/commands/boundary.mjs +3 -0
- package/src/commands/capability.mjs +73 -0
- package/src/commands/enroll.mjs +3 -0
- package/src/commands/harness.mjs +79 -0
- package/src/commands/ingest.mjs +39 -0
- package/src/commands/init.mjs +4 -1
- package/src/commands/inquiry.mjs +66 -0
- package/src/commands/knowledge-semantic.mjs +128 -0
- package/src/commands/knowledge.mjs +119 -0
- package/src/commands/learn.mjs +61 -0
- package/src/commands/obsidian.mjs +754 -63
- package/src/commands/practice.mjs +24 -0
- package/src/commands/setup.mjs +22 -2
- package/src/commands/trackable.mjs +19 -0
- package/src/contracts/corpus.mjs +30 -0
- package/src/coordination/index.mjs +110 -0
- package/src/evidence-navigation/contracts.mjs +60 -0
- package/src/evidence-navigation/evaluate.mjs +69 -0
- package/src/evidence-navigation/generation.mjs +22 -0
- package/src/evidence-navigation/index.mjs +2 -0
- package/src/evidence-navigation/local.mjs +167 -0
- package/src/evidence-navigation/schema.generated.mjs +604 -0
- package/src/evidence-navigation/types.mjs +12 -0
- package/src/export/atelier-export-contract.mjs +35 -8
- package/src/graph/enroll.mjs +248 -0
- package/src/graph/graph.mjs +17 -1
- package/src/graph/knowledge-graph.mjs +58 -8
- package/src/harnesses/contracts.mjs +41 -0
- package/src/harnesses/exchange.mjs +154 -0
- package/src/harnesses/feedback.mjs +16 -0
- package/src/harnesses/history.mjs +37 -0
- package/src/harnesses/index.mjs +4 -0
- package/src/harnesses/store.mjs +29 -0
- package/src/ingestion/contracts.mjs +41 -0
- package/src/ingestion/evaluation.mjs +119 -0
- package/src/ingestion/optional/composition/index.mjs +171 -0
- package/src/ingestion/optional/evaluation/index.mjs +219 -0
- package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
- package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
- package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
- package/src/ingestion/optional/graphify/json_bridge.py +163 -0
- package/src/ingestion/optional/graphify/query_receiving.py +209 -0
- package/src/ingestion/optional/index.mjs +9 -0
- package/src/ingestion/optional/jev/bridge.mjs +166 -0
- package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
- package/src/ingestion/optional/jev/judgments.mjs +174 -0
- package/src/ingestion/optional/jev/native-translation.mjs +32 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
- package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
- package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
- package/src/ingestion/optional/vanilla/index.mjs +218 -0
- package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
- package/src/ingestion/processors.mjs +218 -0
- package/src/ingestion/semantic.mjs +203 -0
- package/src/ingestion/store.mjs +372 -0
- package/src/inquiry/beliefs.mjs +32 -0
- package/src/inquiry/contracts.mjs +23 -0
- package/src/inquiry/index.mjs +5 -0
- package/src/inquiry/ledger.mjs +135 -0
- package/src/inquiry/lenses.mjs +15 -0
- package/src/inquiry/projection.mjs +77 -0
- package/src/inquiry/store.mjs +35 -0
- package/src/intake/read-scope.mjs +12 -0
- package/src/intake/store.mjs +69 -6
- package/src/interaction/index.mjs +91 -0
- package/src/judgment/index.mjs +82 -0
- package/src/judgment/practice-evaluation.mjs +70 -0
- package/src/judgment/practice.mjs +144 -0
- package/src/knowledge/context.mjs +217 -0
- package/src/knowledge/index.mjs +5 -0
- package/src/knowledge/ingestion.mjs +79 -0
- package/src/knowledge/intake.mjs +33 -0
- package/src/knowledge/ledger.mjs +110 -0
- package/src/knowledge/participatory/guidance.mjs +36 -0
- package/src/knowledge/participatory/guided-contribution.mjs +44 -0
- package/src/knowledge/participatory/index.mjs +2 -0
- package/src/knowledge/participatory/installed-workshop.mjs +108 -0
- package/src/knowledge/plan.mjs +67 -0
- package/src/knowledge/projection.mjs +43 -0
- package/src/knowledge/semantic-operation.mjs +567 -0
- package/src/knowledge/sessions.mjs +467 -0
- package/src/knowledge/workspace.mjs +295 -0
- package/src/knowledge-health/assessment.mjs +165 -0
- package/src/knowledge-health/capabilities.mjs +350 -0
- package/src/knowledge-health/context.mjs +62 -0
- package/src/knowledge-health/example.mjs +47 -0
- package/src/knowledge-health/index.mjs +10 -0
- package/src/knowledge-health/owner-join.mjs +88 -0
- package/src/knowledge-health/proof/common.mjs +24 -0
- package/src/knowledge-health/proof/installed-check.mjs +100 -0
- package/src/knowledge-health/proof/native-readback.mjs +37 -0
- package/src/knowledge-health/proof/rehearse.mjs +68 -0
- package/src/knowledge-health/proof/validate-participant.mjs +17 -0
- package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
- package/src/knowledge-health/proof/workshop-check.mjs +36 -0
- package/src/knowledge-health/rule-identity.mjs +44 -0
- package/src/knowledge-health/workshop-composition.mjs +19 -0
- package/src/knowledge-health/workshop-profile.mjs +176 -0
- package/src/knowledge-health/workshop.mjs +99 -0
- package/src/learning/adapters.mjs +68 -0
- package/src/learning/assistance.mjs +127 -0
- package/src/learning/contracts.mjs +65 -0
- package/src/learning/store.mjs +272 -0
- package/src/personal-workspace/index.mjs +391 -0
- package/src/personal-workspace/selection.mjs +385 -0
- package/src/project/config.mjs +8 -0
- package/src/project/durable-state.mjs +3 -1
- package/src/project/file-class.mjs +1 -1
- package/src/projection/obsidian/edits/apply.mjs +55 -16
- package/src/projection/obsidian/personal-workspace.mjs +436 -0
- package/src/projection/obsidian/proposals/adapter.mjs +47 -8
- package/src/projection/obsidian/proposals/observation.mjs +10 -2
- package/src/projection/obsidian/publication/publisher.mjs +142 -13
- package/src/projection/obsidian/publication/vault-list.mjs +4 -3
- package/src/projection/obsidian/recovery/index.mjs +4 -1
- package/src/projection/obsidian/recovery/store.mjs +160 -7
- package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
- package/src/readiness/readiness.mjs +10 -3
- package/src/reflection/index.mjs +56 -0
- package/src/runtime/obsidian/app-production-seams.mjs +48 -16
- package/src/runtime/obsidian/app-registration.mjs +175 -28
- package/src/runtime/obsidian/app-restart.mjs +154 -0
- package/src/runtime/obsidian/engine.mjs +163 -20
- package/src/runtime/obsidian/extension-points.mjs +1 -1
- package/src/runtime/obsidian/index.mjs +10 -7
- package/src/runtime/obsidian/launch-plan.mjs +7 -4
- package/src/runtime/obsidian/lifecycle.mjs +92 -12
- package/src/runtime/obsidian/login-item.mjs +332 -0
- package/src/runtime/obsidian/machine-settings.mjs +119 -12
- package/src/runtime/obsidian/opening.mjs +171 -37
- package/src/runtime/obsidian/pipeline.mjs +71 -3
- package/src/runtime/obsidian/plugin-channel.mjs +11 -3
- package/src/runtime/obsidian/plugin-choice.mjs +16 -4
- package/src/runtime/obsidian/plugin-drift.mjs +59 -11
- package/src/runtime/obsidian/plugin-presence.mjs +14 -1
- package/src/runtime/obsidian/process-identity.mjs +20 -10
- package/src/runtime/obsidian/project-views.mjs +282 -0
- package/src/runtime/obsidian/questions.mjs +48 -0
- package/src/runtime/obsidian/release-watch.mjs +56 -0
- package/src/runtime/obsidian/service-main.mjs +99 -16
- package/src/runtime/obsidian/service-manager-production.mjs +61 -0
- package/src/runtime/obsidian/service-managers.mjs +275 -0
- package/src/runtime/obsidian/service-record.mjs +87 -23
- package/src/runtime/obsidian/service.mjs +36 -3
- package/src/runtime/obsidian/startup-adapters.mjs +59 -21
- package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
- package/src/runtime/obsidian/vault-location.mjs +300 -0
- package/src/runtime/obsidian/view-counts.mjs +34 -0
- package/src/server/local-sidecar.mjs +169 -2
- package/src/server/server.mjs +28 -2
- package/src/template-bindings/project.mjs +92 -0
- package/src/templates/profile.mjs +225 -0
- package/src/trackables/domain.mjs +175 -0
- package/src/trackables/store.mjs +114 -0
- package/src/ui/knowledge-page.mjs +1388 -0
- package/src/upgrade/explanation.mjs +1 -1
- package/src/upgrade/template-participant.mjs +171 -0
- package/src/upgrade/transaction.mjs +72 -25
- package/src/upgrade/upgrade.mjs +1 -0
- package/templates/external-project-workspace/pack-lifecycle.json +6 -1
- package/templates/knowledge-workspace/.gitattributes +2 -0
- package/templates/knowledge-workspace/AGENTS.md +27 -0
- package/templates/knowledge-workspace/README.md +44 -0
- package/templates/knowledge-workspace/atelier.project.json +22 -0
- package/templates/knowledge-workspace/gitignore +6 -0
- package/templates/knowledge-workspace/knowledge-plan.json +111 -0
- package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
- package/templates/knowledge-workspace/records/forecast.md +22 -0
- package/templates/knowledge-workspace/records/inspection.md +25 -0
- package/templates/knowledge-workspace/records/telescope.md +24 -0
- package/templates/knowledge-workspace/repo-access.v1.json +9 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads'
|
|
5
|
+
import { PersonalWorkspaceRefusal, composePersonalWorkspace } from '@mnstry/atelier/personal-workspace'
|
|
6
|
+
import { ensureLocalState, resolveProjectConfig, validateProjectConfigDoc } from '@mnstry/atelier/project'
|
|
7
|
+
import { buildCanonicalGraph } from '../../graph/graph.mjs'
|
|
8
|
+
import { refuse } from '../../runtime/obsidian/errors.mjs'
|
|
9
|
+
import { OBSIDIAN_EXT_KEY } from './contracts.mjs'
|
|
10
|
+
|
|
11
|
+
// The one personal-workspace route of the Obsidian projection.
|
|
12
|
+
//
|
|
13
|
+
// A person's private home holds the authored manifest and overlay, and the
|
|
14
|
+
// generations the personal-workspace module materialized from them. A binding
|
|
15
|
+
// names one home and one generation, both explicitly: nothing here discovers
|
|
16
|
+
// either from the environment or the working directory. The module's
|
|
17
|
+
// `composePersonalWorkspace` is the only authority on whether that generation
|
|
18
|
+
// may be used.
|
|
19
|
+
//
|
|
20
|
+
// One graph. Every caller (the engine, view counts, source apply, the proposal
|
|
21
|
+
// adapter and the selection operation) builds through the pipeline's
|
|
22
|
+
// `buildGraph`, which builds a bound project here: the canonical graph of the
|
|
23
|
+
// generation's configuration, with the composition's own rule that no private
|
|
24
|
+
// note is a link target, and the caller's file cache. A plain build would
|
|
25
|
+
// differ wherever a shared note's link names a private note.
|
|
26
|
+
//
|
|
27
|
+
// Validity. A graph is returned only under a validity key the composition has
|
|
28
|
+
// confirmed: the key names the inputs a stat or a bounded read can see (see
|
|
29
|
+
// inputsDigest) and a digest of the built graph's facts. The inputs are read
|
|
30
|
+
// before composing, so a key is confirmed only when the composition accepted
|
|
31
|
+
// those inputs and composed a graph with exactly those facts, and a returned
|
|
32
|
+
// graph is always one the composition built too (a change and its exact undo
|
|
33
|
+
// during one composition is caught at the next full reconciliation). A key not yet confirmed is
|
|
34
|
+
// confirmed by composing: synchronously for a caller that builds once, and off
|
|
35
|
+
// the event loop, in a worker with a deadline, for the engine and the proposal
|
|
36
|
+
// adapter it runs (`validatePersonalWorkspace`). What no key names is asked of
|
|
37
|
+
// the composition at every full reconciliation.
|
|
38
|
+
//
|
|
39
|
+
// What a binding returns:
|
|
40
|
+
// project the generation's atelier.project.json, resolved, with the
|
|
41
|
+
// Obsidian settings member built in memory from the overlay's
|
|
42
|
+
// saved views (each a scope) plus an `everything` scope, and the
|
|
43
|
+
// frozen binding under one symbol key. No generation file is
|
|
44
|
+
// ever written.
|
|
45
|
+
// binding { personalHome, generationId, overlayRepoId }, frozen.
|
|
46
|
+
// preferences the overlay's preferences, read only. A theme is reported,
|
|
47
|
+
// never written: the vault's appearance is the person's.
|
|
48
|
+
// coverage passed through as the module returned it. Nothing here reads
|
|
49
|
+
// it: it describes what is not enforced, and is never permission.
|
|
50
|
+
//
|
|
51
|
+
// The project's own folder is the private home rather than the generation:
|
|
52
|
+
// the projection keeps its workspace pointer in `<folder>/.atelier-local/`,
|
|
53
|
+
// and a generation is a closed, verified inventory that any extra file
|
|
54
|
+
// invalidates. A home is outside every Git worktree, so its local state is
|
|
55
|
+
// ignored by construction.
|
|
56
|
+
|
|
57
|
+
const BINDING = Symbol('atelier.obsidian.personal-workspace-binding')
|
|
58
|
+
// What each binding was made with: the composition that admitted it, the build that serves its graph, and whether the
|
|
59
|
+
// composition can run in a worker (only the module's own can).
|
|
60
|
+
const ROUTES = new WeakMap()
|
|
61
|
+
// The last validity key the composition confirmed for each binding: { inputs, facts }.
|
|
62
|
+
const CONFIRMED = new WeakMap()
|
|
63
|
+
export const EVERYTHING_SCOPE_ID = 'everything'
|
|
64
|
+
const EXT_SCHEMA = 'atelier-obsidian-ext-settings/v1'
|
|
65
|
+
// The settings member of a bound project names its generation here. It survives any copy of the project (a structured
|
|
66
|
+
// clone, a JSON round trip) that drops the symbol-keyed binding, so such a copy is refused instead of being built as an
|
|
67
|
+
// ordinary project; and a rebound generation changes the member, which invalidates every view. An ordinary project's
|
|
68
|
+
// settings may not use this key.
|
|
69
|
+
export const PERSONAL_MEMBER_KEY = 'mnstry.atelier.personal-workspace'
|
|
70
|
+
// The Obsidian settings contract's limits: at most 256 scopes, one of them `everything`, and at most 256 members in a
|
|
71
|
+
// selector's union.
|
|
72
|
+
const MAX_SAVED_VIEWS = 255
|
|
73
|
+
const MAX_VIEW_REPOSITORIES = 256
|
|
74
|
+
// A build that found its validity key unconfirmed, for a caller that confirms it off the event loop and builds again.
|
|
75
|
+
export const PERSONAL_VALIDATION_PENDING = 'personal-validation-pending'
|
|
76
|
+
const WORKER_KIND = 'atelier-obsidian-personal-composition'
|
|
77
|
+
|
|
78
|
+
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex')
|
|
79
|
+
const deepFreeze = (value) => { if (value && typeof value === 'object') { Object.values(value).forEach(deepFreeze); Object.freeze(value) } return value }
|
|
80
|
+
const sortKeys = (value) => (Array.isArray(value) ? value.map(sortKeys)
|
|
81
|
+
: value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortKeys(value[key])])) : value)
|
|
82
|
+
const digestOf = (value) => sha256(JSON.stringify(sortKeys(value)))
|
|
83
|
+
const generationRoot = ({ personalHome, generationId }) => path.join(personalHome, 'generations', generationId)
|
|
84
|
+
const linkTargetRule = (overlayRepoId) => (node) => node.repo !== overlayRepoId
|
|
85
|
+
|
|
86
|
+
// The facts of a graph every caller reads: nodes, edges, embeds, assets, resolved links and every finding.
|
|
87
|
+
const graphFactsDigest = (graph) => digestOf([graph.nodes, graph.edges, graph.embeds ?? [], graph.assets ?? [], graph.links ?? [], graph.linkDiagnostics ?? [], graph.diagnostics ?? []])
|
|
88
|
+
|
|
89
|
+
// The personal-workspace module's read limits: an authored input, and a generation file.
|
|
90
|
+
const AUTHORED_LIMIT = 1024 * 1024
|
|
91
|
+
const GENERATION_LIMIT = 8 * 1024 * 1024
|
|
92
|
+
const NONBLOCK = fs.constants.O_NONBLOCK ?? 0
|
|
93
|
+
|
|
94
|
+
// One regular file, opened without following a link and without waiting on it (a FIFO or a device never blocks the
|
|
95
|
+
// caller), and read up to `limit`. Anything else refuses `code`, as the composition would.
|
|
96
|
+
function regularFileBytes(file, limit, code, { follow = false } = {}) {
|
|
97
|
+
let fd
|
|
98
|
+
try { fd = fs.openSync(file, fs.constants.O_RDONLY | (follow ? 0 : fs.constants.O_NOFOLLOW) | NONBLOCK) } catch (error) {
|
|
99
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null
|
|
100
|
+
throw new PersonalWorkspaceRefusal(code)
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const stat = fs.fstatSync(fd)
|
|
104
|
+
if (!stat.isFile() || stat.size > limit) throw new PersonalWorkspaceRefusal(code)
|
|
105
|
+
return fs.readFileSync(fd)
|
|
106
|
+
} finally { fs.closeSync(fd) }
|
|
107
|
+
}
|
|
108
|
+
const regularFileDigest = (file, limit, code) => { const bytes = regularFileBytes(file, limit, code); return bytes === null ? 'missing' : sha256(bytes) }
|
|
109
|
+
// A Git configuration file, which Git itself reads through a link (a managed dotfile): followed, but still only a
|
|
110
|
+
// regular file, bounded, and never waited on.
|
|
111
|
+
const configDigest = (file) => { try { const bytes = regularFileBytes(file, AUTHORED_LIMIT, 'malformed-input', { follow: true }); return bytes === null ? 'missing' : sha256(bytes) } catch { return 'not-regular' } }
|
|
112
|
+
// The configuration files of an enrolled repository: `.git/config` (through `.git` when it is a link to a directory),
|
|
113
|
+
// or, where `.git` is a file (a separate Git directory, a linked worktree, a submodule), the configuration of the
|
|
114
|
+
// directory it names and of its common directory. A root that cannot be looked at (a file, a link loop, no permission)
|
|
115
|
+
// is recorded as such, so the composition answers with its own code.
|
|
116
|
+
function repositoryConfigDigests(repoRoot) {
|
|
117
|
+
const dotGit = path.join(repoRoot, '.git')
|
|
118
|
+
let kind
|
|
119
|
+
try { kind = fs.lstatSync(dotGit, { throwIfNoEntry: false }) } catch (error) { return [`unreadable:${error.code ?? 'error'}`] }
|
|
120
|
+
if (kind === undefined) return ['missing']
|
|
121
|
+
if (kind.isDirectory()) return [configDigest(path.join(dotGit, 'config'))]
|
|
122
|
+
if (kind.isSymbolicLink()) {
|
|
123
|
+
try { if (fs.statSync(dotGit).isDirectory()) return ['linked', fs.realpathSync(dotGit), configDigest(path.join(dotGit, 'config'))] } catch (error) { return [`unreadable:${error.code ?? 'error'}`] }
|
|
124
|
+
}
|
|
125
|
+
let pointer
|
|
126
|
+
try { pointer = regularFileBytes(dotGit, AUTHORED_LIMIT, 'malformed-input', { follow: true }) } catch { return ['not-regular'] }
|
|
127
|
+
const named = /^gitdir:\s*(.+?)\s*$/m.exec(pointer?.toString('utf8') ?? '')
|
|
128
|
+
if (named === null) return ['unnamed']
|
|
129
|
+
const gitDir = path.resolve(repoRoot, named[1])
|
|
130
|
+
let commonDir = null
|
|
131
|
+
try { const common = regularFileBytes(path.join(gitDir, 'commondir'), AUTHORED_LIMIT, 'malformed-input', { follow: true }); if (common !== null) commonDir = path.resolve(gitDir, common.toString('utf8').trim()) } catch { commonDir = 'not-regular' }
|
|
132
|
+
return [gitDir, configDigest(path.join(gitDir, 'config')), commonDir, commonDir === null || commonDir === 'not-regular' ? null : configDigest(path.join(commonDir, 'config'))]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Everything outside the graph that a composition's answer depends on and a stat or a bounded read can see: the authored
|
|
136
|
+
// inputs (regular files only), every file of the generation (an extra, missing or irregular one too), the private home's
|
|
137
|
+
// own state and any `.git` above it, each enrolled root's link status and Git configuration (its remote and its helpers),
|
|
138
|
+
// the user's Git configuration, and any ambient Git variable. A file that is not regular refuses here with the code the
|
|
139
|
+
// composition gives it, before anything waits on it.
|
|
140
|
+
function inputsDigest({ personalHome, generationId }) {
|
|
141
|
+
const stat = (file) => { try { const s = fs.lstatSync(file); return [s.mode, s.uid, s.ino, s.isSymbolicLink()] } catch (error) { return `missing:${error.code ?? 'error'}` } }
|
|
142
|
+
const authoredBytes = ['atelier.personal.json', 'atelier.overlay.json'].map((name) => regularFileBytes(path.join(personalHome, name), AUTHORED_LIMIT, 'malformed-input'))
|
|
143
|
+
const authored = ['atelier.personal.json', 'atelier.overlay.json'].map((name, index) => [stat(path.join(personalHome, name)), authoredBytes[index] === null ? 'missing' : sha256(authoredBytes[index])])
|
|
144
|
+
const generation = []
|
|
145
|
+
const walk = (directory, prefix) => {
|
|
146
|
+
let entries
|
|
147
|
+
try { entries = fs.readdirSync(directory, { withFileTypes: true }) } catch (error) { generation.push([prefix, `unreadable:${error.code ?? 'error'}`]); return }
|
|
148
|
+
for (const entry of entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))) {
|
|
149
|
+
const relative = prefix === '' ? entry.name : `${prefix}/${entry.name}`
|
|
150
|
+
const absolute = path.join(directory, entry.name)
|
|
151
|
+
if (entry.isDirectory()) { generation.push([relative, stat(absolute)]); walk(absolute, relative) } else if (entry.isFile()) generation.push([relative, regularFileDigest(absolute, GENERATION_LIMIT, 'generation-corrupt')])
|
|
152
|
+
else throw new PersonalWorkspaceRefusal('generation-corrupt')
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const root = generationRoot({ personalHome, generationId })
|
|
156
|
+
walk(root, '')
|
|
157
|
+
let realHome
|
|
158
|
+
try { realHome = fs.realpathSync(personalHome) === personalHome } catch { realHome = false }
|
|
159
|
+
const ancestorsWithGit = []
|
|
160
|
+
for (let ancestor = personalHome; ; ancestor = path.dirname(ancestor)) {
|
|
161
|
+
if (fs.lstatSync(path.join(ancestor, '.git'), { throwIfNoEntry: false }) !== undefined) ancestorsWithGit.push(ancestor)
|
|
162
|
+
if (ancestor === path.dirname(ancestor)) break
|
|
163
|
+
}
|
|
164
|
+
let repos = []
|
|
165
|
+
// The bytes read above, never the file again.
|
|
166
|
+
try { repos = JSON.parse(authoredBytes[0]?.toString('utf8') ?? '{}').repos.filter((repo) => repo.enrolled).map((repo) => repo.root) } catch { repos = [] }
|
|
167
|
+
const enrolled = repos.map((repoRoot) => {
|
|
168
|
+
let real
|
|
169
|
+
try { real = fs.realpathSync(repoRoot) === repoRoot } catch { real = false }
|
|
170
|
+
return [repoRoot, stat(repoRoot), real, repositoryConfigDigests(repoRoot)]
|
|
171
|
+
})
|
|
172
|
+
const home = process.env.HOME
|
|
173
|
+
const userGit = typeof home === 'string' && path.isAbsolute(home)
|
|
174
|
+
? [path.join(home, '.gitconfig'), path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'git', 'config')].map(configDigest)
|
|
175
|
+
: ['no-home']
|
|
176
|
+
return digestOf({
|
|
177
|
+
authored,
|
|
178
|
+
generation,
|
|
179
|
+
roots: [stat(personalHome), realHome, stat(path.join(personalHome, 'generations')), stat(path.join(personalHome, 'generations', '.git')), stat(root), ancestorsWithGit],
|
|
180
|
+
enrolled,
|
|
181
|
+
userGit,
|
|
182
|
+
ambientGit: Object.keys(process.env).filter((name) => /^GIT_/i.test(name)).sort(),
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// One generation file, read without following a link, and checked against the
|
|
187
|
+
// digest the generation records for it (and compose verified a moment ago).
|
|
188
|
+
function generationFile(generation, final, relative) {
|
|
189
|
+
const listed = generation.files.find((file) => file.path === relative)
|
|
190
|
+
if (!listed) throw new PersonalWorkspaceRefusal('generation-corrupt')
|
|
191
|
+
let bytes
|
|
192
|
+
try {
|
|
193
|
+
const fd = fs.openSync(path.join(final, ...relative.split('/')), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW)
|
|
194
|
+
try { bytes = fs.readFileSync(fd) } finally { fs.closeSync(fd) }
|
|
195
|
+
} catch { throw new PersonalWorkspaceRefusal('generation-corrupt') }
|
|
196
|
+
if (sha256(bytes) !== listed.sha256) throw new PersonalWorkspaceRefusal('generation-corrupt')
|
|
197
|
+
return bytes
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function selectorFor(repoIds) {
|
|
201
|
+
const repos = [...new Set(repoIds)]
|
|
202
|
+
if (repos.length === 0) return { ids: [] }
|
|
203
|
+
if (repos.length === 1) return { repo: repos[0] }
|
|
204
|
+
return { union: repos.map((repo) => ({ repo })) }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The Obsidian settings member of a personal binding: one scoped view per saved
|
|
208
|
+
// selection of repositories, in the overlay's order, and one full view of the
|
|
209
|
+
// whole composed graph. A preferred view that names a saved one is the default.
|
|
210
|
+
// Refused, typed, where the overlay allows more than a projection can declare.
|
|
211
|
+
export function personalWorkspaceScopes(overlay, generationId) {
|
|
212
|
+
if (overlay.views.some((view) => view.id === EVERYTHING_SCOPE_ID)) {
|
|
213
|
+
refuse('personal-view-id-reserved', `a saved view may not be named "${EVERYTHING_SCOPE_ID}"; that view is the whole personal workspace`)
|
|
214
|
+
}
|
|
215
|
+
if (overlay.views.length > MAX_SAVED_VIEWS) refuse('personal-views-exceed-settings-limit', `a personal workspace can show at most ${MAX_SAVED_VIEWS} saved views; nothing is bound`, { limit: MAX_SAVED_VIEWS, kind: 'views' })
|
|
216
|
+
if (overlay.views.some((view) => new Set(view.repoIds).size > MAX_VIEW_REPOSITORIES)) {
|
|
217
|
+
refuse('personal-views-exceed-settings-limit', `a saved view can name at most ${MAX_VIEW_REPOSITORIES} repositories; nothing is bound`, { limit: MAX_VIEW_REPOSITORIES, kind: 'repositories' })
|
|
218
|
+
}
|
|
219
|
+
const scopes = [
|
|
220
|
+
...overlay.views.map((view) => ({ scopeId: view.id, mode: 'scoped', selector: selectorFor(view.repoIds) })),
|
|
221
|
+
{ scopeId: EVERYTHING_SCOPE_ID, mode: 'full', selector: { all: true } },
|
|
222
|
+
]
|
|
223
|
+
const preferred = overlay.preferences?.defaultView
|
|
224
|
+
const defaultScopeId = typeof preferred === 'string' && overlay.views.some((view) => view.id === preferred) ? preferred : EVERYTHING_SCOPE_ID
|
|
225
|
+
return { schema: EXT_SCHEMA, enabled: true, defaultScopeId, scopes, ext: { [PERSONAL_MEMBER_KEY]: { generationId } } }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// The bound project, from a composition the caller just made: `composed` carries the generation's record, the
|
|
229
|
+
// preferences and the coverage, and the validity key that composition confirmed.
|
|
230
|
+
function assemble({ personalHome, generationId }, composed, route) {
|
|
231
|
+
const final = generationRoot({ personalHome, generationId })
|
|
232
|
+
const projectBytes = generationFile(composed.generation, final, 'atelier.project.json')
|
|
233
|
+
const inputs = JSON.parse(generationFile(composed.generation, final, 'inputs.json').toString('utf8'))
|
|
234
|
+
const configPath = path.join(final, 'atelier.project.json')
|
|
235
|
+
const resolved = resolveProjectConfig({ argv: [`--project-config=${configPath}`], cwd: final, env: { PATH: process.env.PATH }, writeLocalState: false })
|
|
236
|
+
// The same pinned resolution compose made; anything else means the generation changed between the two reads.
|
|
237
|
+
if (JSON.stringify(resolved.config) !== JSON.stringify(JSON.parse(projectBytes.toString('utf8')))) throw new PersonalWorkspaceRefusal('generation-corrupt')
|
|
238
|
+
if (validateProjectConfigDoc(resolved.config).length > 0) throw new PersonalWorkspaceRefusal('generation-corrupt')
|
|
239
|
+
if (resolved.localOverlay.paths.length > 0 || resolved.repos.some((repo) => repo.pathSource !== 'tracked-config')) throw new PersonalWorkspaceRefusal('ambient-overlay-present')
|
|
240
|
+
const overlayRepoId = `personal-${inputs.manifest.workspaceId}`
|
|
241
|
+
const overlayRepo = resolved.repos.find((repo) => repo.name === overlayRepoId)
|
|
242
|
+
if (!overlayRepo || overlayRepo.path !== path.join(final, 'overlay')) throw new PersonalWorkspaceRefusal('generation-corrupt')
|
|
243
|
+
|
|
244
|
+
const binding = Object.freeze({ personalHome, generationId, overlayRepoId })
|
|
245
|
+
ROUTES.set(binding, route)
|
|
246
|
+
CONFIRMED.set(binding, composed.key)
|
|
247
|
+
const member = deepFreeze(personalWorkspaceScopes(inputs.overlay, generationId))
|
|
248
|
+
const project = {
|
|
249
|
+
...resolved,
|
|
250
|
+
config: { ...resolved.config, ext: { ...(resolved.config.ext ?? {}), [OBSIDIAN_EXT_KEY]: member } },
|
|
251
|
+
configDir: personalHome,
|
|
252
|
+
[BINDING]: binding,
|
|
253
|
+
}
|
|
254
|
+
project.localState = ensureLocalState(project, { write: false, env: { PATH: process.env.PATH } })
|
|
255
|
+
return Object.freeze({ project, binding, preferences: deepFreeze(structuredClone(composed.preferences ?? {})), coverage: composed.coverage })
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// A composition in this thread, as the summary a worker sends back.
|
|
259
|
+
// The inputs are digested BEFORE composing: a single change that lands while the composition runs then either is seen by
|
|
260
|
+
// it, or leaves the confirmed key behind, so the next build composes again. A change and its exact undo within one
|
|
261
|
+
// composition (A, then B, then A again) is not seen this way: it is caught at the next full reconciliation.
|
|
262
|
+
function composeHere(compose, binding) {
|
|
263
|
+
const inputs = inputsDigest(binding)
|
|
264
|
+
const composed = compose({ personalHome: binding.personalHome, generationId: binding.generationId })
|
|
265
|
+
return { generation: composed.generation, preferences: composed.preferences, coverage: composed.coverage, key: { inputs, facts: graphFactsDigest(composed.graph) } }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// A composition in a worker thread, so the event loop of the caller stays free while it runs. Resolves to the summary, or
|
|
269
|
+
// to `{ code }` for a refusal; a worker that cannot run answers `personal-composition-unavailable`.
|
|
270
|
+
// A worker that does not answer within the deadline is terminated and let go of, and the composition refuses
|
|
271
|
+
// `personal-composition-unavailable`: the tick does not wait on it. A worker blocked in a system call (a FIFO opened
|
|
272
|
+
// for reading, say) cannot be stopped until that call returns: until then it keeps the process from exiting, and each
|
|
273
|
+
// such expiry leaves one thread behind. A service stopped in that state needs a kill.
|
|
274
|
+
export const WORKER_DEADLINE_MS = 120 * 1000
|
|
275
|
+
function composeInWorker({ personalHome, generationId }, { deadlineMs = WORKER_DEADLINE_MS, stallMs = 0 } = {}) {
|
|
276
|
+
return new Promise((resolve) => {
|
|
277
|
+
let settled = false
|
|
278
|
+
let worker = null
|
|
279
|
+
let timer = null
|
|
280
|
+
const settle = (value) => {
|
|
281
|
+
if (settled) return
|
|
282
|
+
settled = true
|
|
283
|
+
clearTimeout(timer)
|
|
284
|
+
if (value.code === 'personal-composition-unavailable' && worker !== null) { worker.unref(); worker.terminate().catch(() => {}) }
|
|
285
|
+
resolve(value)
|
|
286
|
+
}
|
|
287
|
+
try { worker = new Worker(new URL(import.meta.url), { workerData: { kind: WORKER_KIND, personalHome, generationId, stallMs } }) } catch { settle({ code: 'personal-composition-unavailable' }); return }
|
|
288
|
+
timer = setTimeout(() => settle({ code: 'personal-composition-unavailable' }), deadlineMs)
|
|
289
|
+
timer.unref?.()
|
|
290
|
+
worker.once('message', settle)
|
|
291
|
+
worker.once('error', () => settle({ code: 'personal-composition-unavailable' }))
|
|
292
|
+
worker.once('exit', () => settle({ code: 'personal-composition-unavailable' }))
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Test seam only: `compose` replaces the module's composePersonalWorkspace for the binding and every validation of
|
|
297
|
+
// it, `build` the canonical build that serves its graph, and `worker` the worker's deadline (and a stall, so a worker that
|
|
298
|
+
// never answers can be shown). Production code uses bindPersonalWorkspace.
|
|
299
|
+
export function createPersonalWorkspaceBinderForOracleTests({ compose = composePersonalWorkspace, build = null, worker = {} } = {}) {
|
|
300
|
+
const route = { compose, offThread: compose === composePersonalWorkspace, build, worker }
|
|
301
|
+
const bind = ({ personalHome, generationId } = {}) => {
|
|
302
|
+
// The validity authority first: a refusal propagates as the module's own typed refusal, and nothing is read or
|
|
303
|
+
// written after it.
|
|
304
|
+
const composed = composeHere(compose, { personalHome, generationId })
|
|
305
|
+
return assemble({ personalHome, generationId }, composed, route)
|
|
306
|
+
}
|
|
307
|
+
// The same binding, with the composition off the event loop.
|
|
308
|
+
bind.offThread = async ({ personalHome, generationId } = {}) => {
|
|
309
|
+
const composed = route.offThread ? await composeInWorker({ personalHome, generationId }, route.worker) : composeHere(compose, { personalHome, generationId })
|
|
310
|
+
if (typeof composed.code === 'string') {
|
|
311
|
+
if (composed.code === 'personal-composition-unavailable') refuse(composed.code, 'the personal workspace could not be composed off the event loop; nothing is bound')
|
|
312
|
+
throw new PersonalWorkspaceRefusal(composed.code)
|
|
313
|
+
}
|
|
314
|
+
return assemble({ personalHome, generationId }, composed, route)
|
|
315
|
+
}
|
|
316
|
+
return bind
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export const bindPersonalWorkspace = createPersonalWorkspaceBinderForOracleTests()
|
|
320
|
+
|
|
321
|
+
// A refusal of the personal-workspace module becomes the maintenance refusal with the same code, as at every build, so
|
|
322
|
+
// the engine and every command report it typed and no view stays current.
|
|
323
|
+
const asMaintenanceRefusal = (error, message) => {
|
|
324
|
+
if (error instanceof PersonalWorkspaceRefusal) refuse(error.code, message, { source: 'personal-workspace' })
|
|
325
|
+
throw error
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// The project of a binding, for a loader that may block (a command run once).
|
|
329
|
+
export function loadBoundProject({ personalHome, generationId } = {}, { bind = bindPersonalWorkspace } = {}) {
|
|
330
|
+
try { return bind({ personalHome, generationId }).project } catch (error) { return asMaintenanceRefusal(error, 'the personal workspace refused this generation; nothing is loaded from it') }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// The project of a binding, for a loader on a service's event loop: the composition runs in a worker. The engine
|
|
334
|
+
// awaits its loader, so this is the loader a service uses.
|
|
335
|
+
export async function loadBoundProjectOffThread({ personalHome, generationId } = {}, { bind = bindPersonalWorkspace } = {}) {
|
|
336
|
+
try { return (await bind.offThread({ personalHome, generationId })).project } catch (error) { return asMaintenanceRefusal(error, 'the personal workspace refused this generation; nothing is loaded from it') }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// The binding a project carries, or null for every other project.
|
|
340
|
+
export function personalWorkspaceBindingOf(project) {
|
|
341
|
+
return project !== null && typeof project === 'object' && Object.hasOwn(project, BINDING) ? project[BINDING] : null
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// The graph of a bound project, or null when the project carries no binding. `fileCache` and `observedDigest` are the
|
|
345
|
+
// caller's, as for any build. With `defer`, a key the composition has not confirmed is not confirmed here: the build
|
|
346
|
+
// refuses `personal-validation-pending`, and the caller confirms it off the event loop (validatePersonalWorkspace) and
|
|
347
|
+
// builds again.
|
|
348
|
+
export function personalWorkspaceGraph(project, { fileCache = null, observedDigest = null, defer = false } = {}) {
|
|
349
|
+
const binding = personalWorkspaceBindingOf(project)
|
|
350
|
+
if (binding === null) {
|
|
351
|
+
if (project?.config?.ext?.[OBSIDIAN_EXT_KEY]?.ext?.[PERSONAL_MEMBER_KEY] !== undefined) {
|
|
352
|
+
refuse('personal-binding-lost', `the project's Obsidian settings use the reserved key ext["${PERSONAL_MEMBER_KEY}"], which only a personal-workspace binding sets, and the project carries no binding; nothing is prepared from it`)
|
|
353
|
+
}
|
|
354
|
+
return null
|
|
355
|
+
}
|
|
356
|
+
const route = ROUTES.get(binding)
|
|
357
|
+
if (route === undefined) refuse('personal-binding-unrecognized', 'the project carries a personal-workspace binding this release did not make; nothing is prepared from it')
|
|
358
|
+
if (project.configPath !== path.join(generationRoot(binding), 'atelier.project.json')) refuse('personal-binding-mismatch', 'the project is not the configuration of the generation it is bound to; nothing is prepared from it')
|
|
359
|
+
const pending = () => refuse(PERSONAL_VALIDATION_PENDING, 'the personal workspace has changed since it was last composed; it is composed again before a graph is used', { source: 'personal-workspace-pending' })
|
|
360
|
+
const confirmed = CONFIRMED.get(binding)
|
|
361
|
+
// Inputs that changed are composed before anything is built from them.
|
|
362
|
+
let inputs
|
|
363
|
+
try { inputs = inputsDigest(binding) } catch (error) { CONFIRMED.delete(binding); asMaintenanceRefusal(error, 'the personal workspace refused this generation; nothing is prepared from it') }
|
|
364
|
+
if (confirmed?.inputs !== inputs) {
|
|
365
|
+
if (defer) pending()
|
|
366
|
+
confirmHere(route, binding)
|
|
367
|
+
}
|
|
368
|
+
const build = route.build ?? ((target, options) => buildCanonicalGraph(target, { ...options, isLinkTargetEligible: linkTargetRule(binding.overlayRepoId) }))
|
|
369
|
+
const graph = build(project, { fileCache, observedDigest })
|
|
370
|
+
if (CONFIRMED.get(binding)?.facts === graphFactsDigest(graph)) return graph
|
|
371
|
+
if (defer) pending()
|
|
372
|
+
confirmHere(route, binding)
|
|
373
|
+
if (CONFIRMED.get(binding)?.facts === graphFactsDigest(graph)) return graph
|
|
374
|
+
// Composed just now, and still another graph: the sources moved between the two reads, or the build is not the
|
|
375
|
+
// composition's. Nothing is prepared from it.
|
|
376
|
+
return 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' })
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function confirmHere(route, binding) {
|
|
380
|
+
try { CONFIRMED.set(binding, composeHere(route.compose, binding).key) } catch (error) { CONFIRMED.delete(binding); asMaintenanceRefusal(error, 'the personal workspace refused this generation; nothing is prepared from it') }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Asks the composition again, off the event loop when it can: `{ ok: true, changed }` when it accepts the generation
|
|
384
|
+
// (`changed` when its key differs from the one last confirmed), `{ ok: false, code }` when it refuses. Null for a project
|
|
385
|
+
// without a binding. The engine asks this before a build that found its key unconfirmed, and at every full
|
|
386
|
+
// reconciliation of a bound project.
|
|
387
|
+
export async function validatePersonalWorkspace(project) {
|
|
388
|
+
const binding = personalWorkspaceBindingOf(project)
|
|
389
|
+
if (binding === null) return null
|
|
390
|
+
const route = ROUTES.get(binding)
|
|
391
|
+
if (route === undefined) return { ok: false, code: 'personal-binding-unrecognized' }
|
|
392
|
+
let composed
|
|
393
|
+
if (route.offThread) composed = await composeInWorker(binding, route.worker)
|
|
394
|
+
else {
|
|
395
|
+
try { composed = composeHere(route.compose, binding) } catch (error) { if (!(error instanceof PersonalWorkspaceRefusal)) throw error; composed = { code: error.code } }
|
|
396
|
+
}
|
|
397
|
+
if (typeof composed.code === 'string') { CONFIRMED.delete(binding); return { ok: false, code: composed.code } }
|
|
398
|
+
const previous = CONFIRMED.get(binding)
|
|
399
|
+
CONFIRMED.set(binding, composed.key)
|
|
400
|
+
return { ok: true, changed: previous?.inputs !== composed.key.inputs || previous?.facts !== composed.key.facts }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// The authored and generation files whose change means the binding must be asked again: the manifest, the overlay and
|
|
404
|
+
// the bound generation's record. Observed as configuration by the engine. Empty for any other project.
|
|
405
|
+
export function personalWorkspaceInputs(project) {
|
|
406
|
+
const binding = personalWorkspaceBindingOf(project)
|
|
407
|
+
if (binding === null) return []
|
|
408
|
+
return [path.join(binding.personalHome, 'atelier.personal.json'), path.join(binding.personalHome, 'atelier.overlay.json'), path.join(generationRoot(binding), 'generation.json')]
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// The repository of a bound project's private notes, or null. A vault edit to one of them is never applied into a
|
|
412
|
+
// module-owned generation, and is not proposed yet: it stays held in the vault.
|
|
413
|
+
export function personalOverlayRepoOf(project) {
|
|
414
|
+
return personalWorkspaceBindingOf(project)?.overlayRepoId ?? null
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Settings that a personal binding takes from the overlay or does not have yet. A saved view is authored in the
|
|
418
|
+
// overlay, so `view add` would write a module-owned generation file; who may see and where vaults live wait for a
|
|
419
|
+
// stable per-person projection configuration. Refused before anything is resolved or written.
|
|
420
|
+
export function refuseOnPersonalBinding(project, operation) {
|
|
421
|
+
if (personalWorkspaceBindingOf(project) === null) return
|
|
422
|
+
if (operation === 'view add') refuse('views-from-personal-overlay', 'the views of a personal workspace come from its overlay; nothing was written', { operation })
|
|
423
|
+
refuse('personal-binding-decision-unavailable', `${operation} is not available for a personal workspace yet; nothing was written`, { operation })
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// The worker side of composeInWorker: one composition, its summary, and exit.
|
|
427
|
+
if (!isMainThread && workerData?.kind === WORKER_KIND) {
|
|
428
|
+
const binding = { personalHome: workerData.personalHome, generationId: workerData.generationId }
|
|
429
|
+
// A worker that never answers, for the deadline test only: blocked as a system call would block it.
|
|
430
|
+
if (workerData.stallMs > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, workerData.stallMs)
|
|
431
|
+
try {
|
|
432
|
+
parentPort.postMessage(composeHere(composePersonalWorkspace, binding))
|
|
433
|
+
} catch (error) {
|
|
434
|
+
parentPort.postMessage({ code: error instanceof PersonalWorkspaceRefusal ? error.code : 'personal-composition-unavailable' })
|
|
435
|
+
}
|
|
436
|
+
}
|
|
@@ -6,7 +6,7 @@ import { AtelierDiagnosticError } from '../../../project/config.mjs'
|
|
|
6
6
|
import { compareText, isoTime } from '../../../runtime/obsidian/documents.mjs'
|
|
7
7
|
import { ObsidianMaintenanceRefusal } from '../../../runtime/obsidian/errors.mjs'
|
|
8
8
|
import { readMachineSettings } from '../../../runtime/obsidian/machine-settings.mjs'
|
|
9
|
-
import {
|
|
9
|
+
import { createProductionSeams, eligibilityFor } from '../../../runtime/obsidian/pipeline.mjs'
|
|
10
10
|
import { acquirePrivateGenerationLock, createAbandonmentProof } from '../../../runtime/obsidian/private-lock.mjs'
|
|
11
11
|
import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, assertObsidianContract } from '../contracts.mjs'
|
|
12
12
|
import { SOURCE_APPLY_PRIMITIVES } from '../edits/apply.mjs'
|
|
@@ -15,6 +15,7 @@ import { isIdentifier } from '../edits/object-identity.mjs'
|
|
|
15
15
|
import { openObjectStore } from '../edits/object-store.mjs'
|
|
16
16
|
import { decideApply } from '../edits/policy.mjs'
|
|
17
17
|
import { applyEditLens } from '../edits/regions.mjs'
|
|
18
|
+
import { PERSONAL_VALIDATION_PENDING, personalOverlayRepoOf } from '../personal-workspace.mjs'
|
|
18
19
|
import { PublicationRefusal } from '../recovery/store.mjs'
|
|
19
20
|
import {
|
|
20
21
|
PROPOSAL_BACKPRESSURE, PROPOSAL_LEDGER_LIMITS, classifyLedgerRead, classifyStoreRefusal, estimateEventLineBytes, isDue, isExhausted, nextAttemptAt, preflightAppend,
|
|
@@ -168,7 +169,7 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
|
|
|
168
169
|
// two questions asked of the machine; `proveAbandoned` is the proof a held lock is passed over with.
|
|
169
170
|
return function createProposalAdapter(options = {}) {
|
|
170
171
|
const {
|
|
171
|
-
crash = () => {}, bounds: boundsInput = {}, limits = PROPOSAL_LEDGER_LIMITS, eligibility =
|
|
172
|
+
crash = () => {}, bounds: boundsInput = {}, limits = PROPOSAL_LEDGER_LIMITS, eligibility: fixedEligibility = null,
|
|
172
173
|
openStore = createProposalStore, objectStore = openObjectStore, isGitIgnored = SOURCE_APPLY_PRIMITIVES.isGitIgnored, proveAbandoned = createAbandonmentProof(), queueCrash = () => {},
|
|
173
174
|
} = options
|
|
174
175
|
const bounds = { ...PROPOSAL_BACKPRESSURE, ...boundsInput }
|
|
@@ -184,6 +185,9 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
|
|
|
184
185
|
let observation = { key: null, run: null }
|
|
185
186
|
// Edits whose operation is known not to be a proposal. A cache: losing it costs one read of an object.
|
|
186
187
|
const notProposals = new Set()
|
|
188
|
+
// Set when an observation found the personal workspace's graph pending: the edits it could not look at are offered
|
|
189
|
+
// again on the next observation, not parked until a full reconciliation.
|
|
190
|
+
let observePendingAgain = false
|
|
187
191
|
|
|
188
192
|
// -- the workspace of one call -------------------------------------------
|
|
189
193
|
|
|
@@ -198,12 +202,31 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
|
|
|
198
202
|
if (!stores.has(scopeId)) stores.set(scopeId, seams.createRecoveryStore({ workspaceRoot, workspaceId, scopeId, repositoryRoots }))
|
|
199
203
|
return stores.get(scopeId)
|
|
200
204
|
}
|
|
201
|
-
// The canonical graph as it is now and the corpus profile of this machine, built once per call
|
|
205
|
+
// The canonical graph as it is now and the corpus profile of this machine, built once per call with the notes the
|
|
206
|
+
// machine settings let into a view, as the engine builds it (eligibilityFor). Throws typed.
|
|
202
207
|
let corpus = null
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
208
|
+
// A pending answer is kept for the call too, so a call that looks at several edits builds at most once.
|
|
209
|
+
let pendingCorpus = null
|
|
210
|
+
const currentCorpus = () => {
|
|
211
|
+
if (corpus !== null) return corpus
|
|
212
|
+
if (pendingCorpus !== null) throw pendingCorpus
|
|
213
|
+
const machine = readMachineSettings({ workspaceRoot, workspaceId })
|
|
214
|
+
corpus = {
|
|
215
|
+
// The engine hands its deferral over: a personal workspace is then never composed on its event loop here.
|
|
216
|
+
graph: (() => {
|
|
217
|
+
try { return seams.buildGraph({ project, eligibility: fixedEligibility ?? eligibilityFor({ machine, project }), deferPersonalValidation: context.deferPersonalValidation === true }) } catch (error) {
|
|
218
|
+
if (error?.code === PERSONAL_VALIDATION_PENDING) pendingCorpus = error
|
|
219
|
+
throw error
|
|
220
|
+
}
|
|
221
|
+
})(),
|
|
222
|
+
profile: seams.profileFor({ project, workspaceId, audienceAllow: machine?.audienceAllow ?? [] }),
|
|
223
|
+
}
|
|
224
|
+
return corpus
|
|
225
|
+
}
|
|
226
|
+
// Whether the personal workspace's graph is pending on this call. Asked only where a graph would be built anyway.
|
|
227
|
+
const personalPending = () => {
|
|
228
|
+
try { currentCorpus(); return false } catch (error) { if (error?.code === PERSONAL_VALIDATION_PENDING) return true; if (isTyped(error)) return false; throw error }
|
|
229
|
+
}
|
|
207
230
|
// true, false, or null when the canonical graph cannot be read now. The rule is the one source apply asks.
|
|
208
231
|
const visible = options.isVisible ? (identity) => options.isVisible(identity, context) : ({ repoId, nodeId }) => {
|
|
209
232
|
let built
|
|
@@ -223,6 +246,7 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
|
|
|
223
246
|
return {
|
|
224
247
|
...context, recoveryOf, visible, clock, env: env ?? context.env ?? process.env,
|
|
225
248
|
corpus: currentCorpus,
|
|
249
|
+
personalPending,
|
|
226
250
|
objects: () => (objects ??= objectStore({ stateRoot: workspaceRoot, workspaceId, repositoryRoots, clock })),
|
|
227
251
|
queue,
|
|
228
252
|
managedRoots: () => [workspaceRoot, ...[...stores.values()].map((store) => store.vaultRoot)],
|
|
@@ -304,6 +328,8 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
|
|
|
304
328
|
if (requireOpenEdit && editState !== 'open') return known ? refused(`edit-${editState}`) : outcome('skipped', null, { code: `edit-${editState}` })
|
|
305
329
|
// The object store no longer says this is a proposed structural edit, so there is nothing to describe.
|
|
306
330
|
if (item.observed === null) return refused('operation-not-proposed')
|
|
331
|
+
// A personal workspace whose graph is pending leaves the operation as it is: no attempt is spent on this tick.
|
|
332
|
+
if (workspace.deferPersonalValidation === true && personalOverlayRepoOf(workspace.project) !== null && workspace.personalPending()) return outcome('deferred', history?.head ?? null, { code: PERSONAL_VALIDATION_PENDING })
|
|
307
333
|
const resolved = resolveProposalRoute({ project: workspace.project, workspaceId: workspace.workspaceId, identity: item, sourcePath: item.sourcePath, managedRoots: workspace.managedRoots(), isVisible: workspace.visible, isGitIgnored, env: env ?? workspace.env ?? process.env })
|
|
308
334
|
if (!resolved.ok && !resolved.transient) return refused(resolved.code)
|
|
309
335
|
// Recorded before anything is asked of the store.
|
|
@@ -432,7 +458,20 @@ export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTE
|
|
|
432
458
|
async observe(context, { retryRefused = false } = {}) {
|
|
433
459
|
const workspace = open(context)
|
|
434
460
|
const edits = Array.isArray(context.edits) ? context.edits : []
|
|
435
|
-
|
|
461
|
+
// A private note of a personal workspace lives in a generation the personal-workspace module owns: its edit is
|
|
462
|
+
// not observed, recorded or proposed, and stays held in the vault. Answered by name, with nothing written.
|
|
463
|
+
const overlayRepoId = personalOverlayRepoOf(workspace.project)
|
|
464
|
+
const isPrivate = (edit) => overlayRepoId !== null && edit?.identity?.repoId === overlayRepoId
|
|
465
|
+
const privateNotes = edits.filter((edit) => isPrivate(edit) && edit.closedAt === null)
|
|
466
|
+
.map((edit) => ({ editId: edit.editId, repoId: edit.identity.repoId, nodeId: edit.identity.nodeId, status: 'refused', code: 'personal-overlay-not-proposed' }))
|
|
467
|
+
const shared = edits.filter((edit) => !isPrivate(edit))
|
|
468
|
+
// The graph is built only when an edit is looked at, as for any project. When the personal workspace's graph is
|
|
469
|
+
// pending, the edits it reached are not reported or recorded, and are offered again on the next observation.
|
|
470
|
+
const retry = retryRefused || observePendingAgain
|
|
471
|
+
const looked = observation.run(workspace, { edits: shared, retryRefused: retry })
|
|
472
|
+
const pending = looked.filter((item) => item.code === PERSONAL_VALIDATION_PENDING)
|
|
473
|
+
observePendingAgain = pending.length > 0
|
|
474
|
+
return { adapterId: PROPOSAL_ADAPTER_ID, observed: [...privateNotes, ...looked.filter((item) => item.code !== PERSONAL_VALIDATION_PENDING)], ...(pending.length > 0 ? { deferred: PERSONAL_VALIDATION_PENDING } : {}) }
|
|
436
475
|
},
|
|
437
476
|
|
|
438
477
|
// The tick. `context` is { project, workspaceRoot, workspaceId, repositoryRoots, edits }: the pending edit
|
|
@@ -126,6 +126,11 @@ export function createTickObservation(options = {}) {
|
|
|
126
126
|
// The note exactly as it was published, which the lens needs to tell generated bytes from authored ones. The
|
|
127
127
|
// publisher keeps no copy of a note it created, so the note is prepared again from the sources as they are now and
|
|
128
128
|
// used only when it has the digest the manifest recorded; a retained object of that digest serves as well.
|
|
129
|
+
//
|
|
130
|
+
// A source replaced between the look at its path and its open (ELEAFCHANGED) is another program writing it, as it
|
|
131
|
+
// is to the publisher and to source apply: the preparation answers null, and the caller reads the source again and
|
|
132
|
+
// refuses stale-source when it changed. Only a preparation that succeeded is kept for its scope and generation: one
|
|
133
|
+
// failure tells of one moment and is never the answer for the other edits of that generation in the tick.
|
|
129
134
|
function publishedNoteOf(workspace, { scope, manifest, noteEntry }, now) {
|
|
130
135
|
const store = workspace.recoveryOf(scope.scopeId)
|
|
131
136
|
const kept = retained(store, noteEntry.noteDigest)
|
|
@@ -134,7 +139,7 @@ export function createTickObservation(options = {}) {
|
|
|
134
139
|
// Prepared in the vault layout of the edit's generation, once per generation.
|
|
135
140
|
const preparedKey = `${scope.scopeId}\u0000${manifest.generationId}`
|
|
136
141
|
if (!workspace.prepared.has(preparedKey)) {
|
|
137
|
-
let files
|
|
142
|
+
let files
|
|
138
143
|
try {
|
|
139
144
|
const { graph, profile } = workspace.corpus()
|
|
140
145
|
const snapshot = seams.captureSnapshot({ project: workspace.project, graph, workspaceId: workspace.workspaceId, index: new Map(), configDigest: manifest.ext?.[EXT]?.configDigest ?? `sha256:${'0'.repeat(64)}`, capturedAt: now })
|
|
@@ -143,7 +148,10 @@ export function createTickObservation(options = {}) {
|
|
|
143
148
|
snapshot, profile, scope, persistentPathRegistry, priorManifest: manifest, existingSettings: null, clock: workspace.clock,
|
|
144
149
|
vaultRootBytes: Buffer.byteLength(store.vaultRoot, 'utf8'), layout: manifestLayoutVersion(manifest),
|
|
145
150
|
}).files
|
|
146
|
-
} catch (error) {
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (!isTyped(error) && error?.code !== 'ELEAFCHANGED' && !GONE.has(error?.code)) throw error
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
147
155
|
workspace.prepared.set(preparedKey, files)
|
|
148
156
|
}
|
|
149
157
|
const file = workspace.prepared.get(preparedKey).find((item) => item.path === noteEntry.path && item.digest === noteEntry.noteDigest)
|