@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,385 @@
|
|
|
1
|
+
// Explicit selection, inventory and restore for a personal workspace's private
|
|
2
|
+
// home. Composition decides eligibility; this module records a person's choice,
|
|
3
|
+
// lists what the home retains, and restores authored inputs from an earlier
|
|
4
|
+
// generation without ever widening enrollment or bindings. It never deletes
|
|
5
|
+
// authored inputs, generations or records; it removes only its own temporary
|
|
6
|
+
// files. Every failure is a PersonalWorkspaceRefusal with a stable code and no path.
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
10
|
+
import { composePersonalWorkspace, loadPersonalManifest, loadPersonalOverlay, PersonalWorkspaceRefusal } from './index.mjs'
|
|
11
|
+
|
|
12
|
+
const MANIFEST_FILE = 'atelier.personal.json'
|
|
13
|
+
const OVERLAY_FILE = 'atelier.overlay.json'
|
|
14
|
+
const SELECTION_SCHEMA = 'atelier-personal-workspace-selection@v1'
|
|
15
|
+
const RESTORE_SCHEMA = 'atelier-personal-workspace-restore@v1'
|
|
16
|
+
const GENESIS = 'genesis'
|
|
17
|
+
const MAX_RECORD = 1024 * 1024
|
|
18
|
+
// The generation module bounds every generation file at eight times its input limit.
|
|
19
|
+
const MAX_GENERATION_FILE = 8 * 1024 * 1024
|
|
20
|
+
const MAX_RESTORE_RECORD = 4 * MAX_RECORD
|
|
21
|
+
const ID = /^[0-9a-f]{64}$/
|
|
22
|
+
// Every temporary name this module writes, including restore aside copies
|
|
23
|
+
// (.restore-<uuid>-aside-<key>.json).
|
|
24
|
+
const TEMPORARY = /^\.(selection|restore|validate)-[0-9a-f-]{36}(-[a-z]+)*\.json$/
|
|
25
|
+
const ASIDE = (key) => new RegExp(`^\\.restore-[0-9a-f-]{36}-aside-${key}\\.json$`)
|
|
26
|
+
const hash = (bytes) => createHash('sha256').update(bytes).digest('hex')
|
|
27
|
+
const sortObject = (v) => Array.isArray(v) ? v.map(sortObject) : v && typeof v === 'object'
|
|
28
|
+
? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sortObject(v[k])])) : v
|
|
29
|
+
// The same canonical form the generation id is computed over.
|
|
30
|
+
const canonical = (v) => `${JSON.stringify(sortObject(v))}\n`
|
|
31
|
+
const refuse = (code, extra) => { throw Object.assign(new PersonalWorkspaceRefusal(code), extra ?? {}) }
|
|
32
|
+
const freeze = (v) => { if (v && typeof v === 'object') { Object.values(v).forEach(freeze); Object.freeze(v) } return v }
|
|
33
|
+
const restorePlans = new WeakSet()
|
|
34
|
+
|
|
35
|
+
// Maps any non-refusal failure to a stable code, so no raw error or path escapes.
|
|
36
|
+
function safe(fn, fallback) {
|
|
37
|
+
try { return fn() } catch (error) { if (error instanceof PersonalWorkspaceRefusal) throw error; refuse(fallback) }
|
|
38
|
+
}
|
|
39
|
+
function privateHome(personalHome) {
|
|
40
|
+
if (typeof personalHome !== 'string' || !path.isAbsolute(personalHome) || path.resolve(personalHome) !== personalHome) refuse('path-not-absolute')
|
|
41
|
+
if (process.platform === 'win32' || typeof process.getuid !== 'function') refuse('private-root-unverifiable')
|
|
42
|
+
let stat
|
|
43
|
+
try { stat = fs.lstatSync(personalHome) } catch { refuse('root-missing') }
|
|
44
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || fs.realpathSync(personalHome) !== personalHome) refuse('root-symlinked')
|
|
45
|
+
if (stat.uid !== process.getuid() || (stat.mode & 0o022) !== 0) refuse('not-private-location')
|
|
46
|
+
return personalHome
|
|
47
|
+
}
|
|
48
|
+
function readBytes(file, limit, tooLarge = 'malformed-input') {
|
|
49
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW)
|
|
50
|
+
try {
|
|
51
|
+
const stat = fs.fstatSync(fd)
|
|
52
|
+
if (!stat.isFile()) refuse('malformed-input')
|
|
53
|
+
if (stat.size > limit) refuse(tooLarge)
|
|
54
|
+
const bytes = fs.readFileSync(fd)
|
|
55
|
+
if (bytes.length > limit) refuse(tooLarge)
|
|
56
|
+
return bytes
|
|
57
|
+
} finally { fs.closeSync(fd) }
|
|
58
|
+
}
|
|
59
|
+
const bytesOrNull = (file, limit) => { try { return readBytes(file, limit) } catch (error) { if (error.code === 'ENOENT') return null; throw error } }
|
|
60
|
+
function syncDir(dir) { const fd = fs.openSync(dir, 'r'); try { fs.fsyncSync(fd) } finally { fs.closeSync(fd) } }
|
|
61
|
+
function checkPrivateDir(dir) {
|
|
62
|
+
const stat = fs.lstatSync(dir)
|
|
63
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid() || (stat.mode & 0o022) !== 0) refuse('not-private-location')
|
|
64
|
+
return dir
|
|
65
|
+
}
|
|
66
|
+
function privateDir(personalHome, name, create) {
|
|
67
|
+
const dir = path.join(personalHome, name)
|
|
68
|
+
if (create) {
|
|
69
|
+
try { fs.mkdirSync(dir, { mode: 0o700 }); syncDir(personalHome) } catch (error) { if (error.code !== 'EEXIST') throw error }
|
|
70
|
+
} else {
|
|
71
|
+
try { fs.lstatSync(dir) } catch (error) { if (error.code === 'ENOENT') return null; throw error }
|
|
72
|
+
}
|
|
73
|
+
return checkPrivateDir(dir)
|
|
74
|
+
}
|
|
75
|
+
function writeExclusive(file, content) {
|
|
76
|
+
const fd = fs.openSync(file, 'wx', 0o600)
|
|
77
|
+
try { fs.writeFileSync(fd, content); fs.fsyncSync(fd) }
|
|
78
|
+
catch (error) { fs.closeSync(fd); fs.rmSync(file, { force: true }); throw error }
|
|
79
|
+
fs.closeSync(fd)
|
|
80
|
+
}
|
|
81
|
+
// Writes complete bytes to a temporary name, then publishes them under `final`
|
|
82
|
+
// with link, which never replaces an existing name. A crash leaves only a
|
|
83
|
+
// temporary file, which readers ignore and the inventory lists.
|
|
84
|
+
function publishExclusive(dir, final, content, kind) {
|
|
85
|
+
const temporary = path.join(dir, `.${kind}-${randomUUID()}.json`)
|
|
86
|
+
writeExclusive(temporary, content)
|
|
87
|
+
try { fs.linkSync(temporary, path.join(dir, final)) } catch (error) { fs.rmSync(temporary, { force: true }); throw error }
|
|
88
|
+
// Published. A temporary that cannot be removed now stays listed by the inventory,
|
|
89
|
+
// and a failed directory sync does not turn a published record into a failure.
|
|
90
|
+
try { fs.unlinkSync(temporary) } catch {}
|
|
91
|
+
try { syncDir(dir) } catch {}
|
|
92
|
+
}
|
|
93
|
+
// An inventory entry for a file that may be missing, oversized, not regular or
|
|
94
|
+
// unreadable: it is listed with its size and without a digest rather than
|
|
95
|
+
// failing the whole inventory.
|
|
96
|
+
function inventoryEntry(file, limit) {
|
|
97
|
+
try {
|
|
98
|
+
const bytes = readBytes(file, limit)
|
|
99
|
+
return { bytes: bytes.length, digest: hash(bytes) }
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error.code === 'ENOENT') return null
|
|
102
|
+
let size = null
|
|
103
|
+
try { size = fs.lstatSync(file).size } catch {}
|
|
104
|
+
return { bytes: size, digest: null }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const eligibility = (personalHome, generationId) => {
|
|
108
|
+
try { composePersonalWorkspace({ personalHome, generationId }); return { eligible: true, reason: null } }
|
|
109
|
+
catch (error) { if (error instanceof PersonalWorkspaceRefusal) return { eligible: false, reason: error.code }; throw error }
|
|
110
|
+
}
|
|
111
|
+
// Validates exact bytes with the module's own loader, through a private temporary copy.
|
|
112
|
+
function validateBytes(personalHome, bytes, kind) {
|
|
113
|
+
const temporary = path.join(personalHome, `.validate-${randomUUID()}.json`)
|
|
114
|
+
writeExclusive(temporary, bytes)
|
|
115
|
+
try { return (kind === 'manifest' ? loadPersonalManifest : loadPersonalOverlay)(temporary, { personalHome }) }
|
|
116
|
+
finally { fs.unlinkSync(temporary) }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The selection history is an append-only hash chain of numbered records. It
|
|
120
|
+
// proves internal consistency only: an edited record whose successors were not
|
|
121
|
+
// re-linked, a gap, or a reordering refuses. Truncating, or consistently rewriting,
|
|
122
|
+
// any trailing run of records (up to the whole history) yields another valid chain
|
|
123
|
+
// and is detectable only by comparing the head with one the host observed earlier.
|
|
124
|
+
function readHistory(personalHome) {
|
|
125
|
+
const dir = privateDir(personalHome, 'selections', false)
|
|
126
|
+
if (!dir) return { records: [], files: [], head: GENESIS }
|
|
127
|
+
const names = fs.readdirSync(dir).filter((name) => !name.startsWith('.')).sort()
|
|
128
|
+
const records = [], files = []
|
|
129
|
+
let head = GENESIS
|
|
130
|
+
for (const [index, name] of names.entries()) {
|
|
131
|
+
if (name !== `${String(index + 1).padStart(6, '0')}.json`) refuse('selection-history-corrupt')
|
|
132
|
+
let bytes
|
|
133
|
+
try { bytes = readBytes(path.join(dir, name), MAX_RECORD, 'selection-history-corrupt') } catch { refuse('selection-history-corrupt') }
|
|
134
|
+
let record
|
|
135
|
+
try { record = JSON.parse(bytes) } catch { refuse('selection-history-corrupt') }
|
|
136
|
+
if (record?.schema !== SELECTION_SCHEMA || record.sequence !== index + 1 || record.previous !== head || !ID.test(record.generationId ?? '') ||
|
|
137
|
+
canonical(record) !== bytes.toString('utf8')) refuse('selection-history-corrupt')
|
|
138
|
+
records.push(record)
|
|
139
|
+
head = hash(bytes)
|
|
140
|
+
files.push({ name, bytes: bytes.length, digest: head })
|
|
141
|
+
}
|
|
142
|
+
return { records, files, head }
|
|
143
|
+
}
|
|
144
|
+
export function selectionConfirmDigest(options) {
|
|
145
|
+
return safe(() => {
|
|
146
|
+
const { generationId, previous } = options ?? {}
|
|
147
|
+
if (typeof generationId !== 'string' || typeof previous !== 'string') refuse('malformed-input')
|
|
148
|
+
return `sha256:${hash(canonical({ action: 'select', generationId, previous }))}`
|
|
149
|
+
}, 'malformed-input')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Records the person's explicit choice of the currently eligible generation.
|
|
153
|
+
// The caller first reads the current head and shows the person what it selects.
|
|
154
|
+
export function selectPersonalGeneration(options) {
|
|
155
|
+
return safe(() => {
|
|
156
|
+
const { personalHome, generationId, confirm } = options ?? {}
|
|
157
|
+
privateHome(personalHome)
|
|
158
|
+
if (typeof generationId !== 'string' || !ID.test(generationId)) refuse('malformed-input')
|
|
159
|
+
const { records, head } = readHistory(personalHome)
|
|
160
|
+
if (confirm !== selectionConfirmDigest({ generationId, previous: head })) refuse('confirmation-mismatch')
|
|
161
|
+
const { eligible, reason } = eligibility(personalHome, generationId)
|
|
162
|
+
if (!eligible) refuse(reason)
|
|
163
|
+
const dir = privateDir(personalHome, 'selections', true)
|
|
164
|
+
const record = { schema: SELECTION_SCHEMA, sequence: records.length + 1, previous: head, generationId }
|
|
165
|
+
const content = canonical(record)
|
|
166
|
+
try { publishExclusive(dir, `${String(record.sequence).padStart(6, '0')}.json`, content, 'selection') }
|
|
167
|
+
catch (error) { if (error.code === 'EEXIST') refuse('selection-concurrent'); throw error }
|
|
168
|
+
return freeze({ generationId, sequence: record.sequence, head: hash(content) })
|
|
169
|
+
}, 'selection-write-failed')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// The current selection and whether it is still eligible now. A selection never
|
|
173
|
+
// keeps a generation eligible after its inputs, roots or enrollment change.
|
|
174
|
+
export function readPersonalSelection(options) {
|
|
175
|
+
return safe(() => {
|
|
176
|
+
const { personalHome } = options ?? {}
|
|
177
|
+
privateHome(personalHome)
|
|
178
|
+
const { records, head } = readHistory(personalHome)
|
|
179
|
+
const current = records.at(-1) ?? null
|
|
180
|
+
if (!current) return freeze({ selected: null, head, eligible: false, reason: 'nothing-selected' })
|
|
181
|
+
return freeze({ selected: current.generationId, sequence: current.sequence, head, ...eligibility(personalHome, current.generationId) })
|
|
182
|
+
}, 'personal-home-unavailable')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Lists entries without failing the whole inventory: a concurrently removed entry
|
|
186
|
+
// is skipped; an unreadable, foreign or oversized one is reported without a digest.
|
|
187
|
+
function listFiles(dir, keep) {
|
|
188
|
+
if (!dir) return []
|
|
189
|
+
const listed = []
|
|
190
|
+
for (const name of fs.readdirSync(dir).filter(keep).sort()) {
|
|
191
|
+
try {
|
|
192
|
+
const bytes = readBytes(path.join(dir, name), MAX_RESTORE_RECORD, 'record-too-large')
|
|
193
|
+
listed.push({ name, bytes: bytes.length, digest: hash(bytes) })
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (error.code === 'ENOENT') continue
|
|
196
|
+
let size = null
|
|
197
|
+
try { size = fs.lstatSync(path.join(dir, name)).size } catch {}
|
|
198
|
+
listed.push({ name, bytes: size, digest: null, unreadable: true })
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return listed
|
|
202
|
+
}
|
|
203
|
+
// A read-only inventory of what this module writes in the private home (authored
|
|
204
|
+
// inputs, generations, staging, selection and restore records, and its temporary
|
|
205
|
+
// files), for a person to review before any deletion they choose to confirm. It
|
|
206
|
+
// writes and deletes nothing.
|
|
207
|
+
export function inventoryPersonalHome(options) {
|
|
208
|
+
return safe(() => {
|
|
209
|
+
const { personalHome } = options ?? {}
|
|
210
|
+
privateHome(personalHome)
|
|
211
|
+
const authored = [MANIFEST_FILE, OVERLAY_FILE].map((name) => {
|
|
212
|
+
const entry = inventoryEntry(path.join(personalHome, name), MAX_RECORD)
|
|
213
|
+
return { name, bytes: entry?.bytes ?? null, digest: entry?.digest ?? null }
|
|
214
|
+
})
|
|
215
|
+
const root = privateDir(personalHome, 'generations', false)
|
|
216
|
+
const entries = root ? fs.readdirSync(root).sort() : []
|
|
217
|
+
const generations = entries.filter((name) => ID.test(name)).map((generationId) => {
|
|
218
|
+
const record = inventoryEntry(path.join(root, generationId, 'generation.json'), MAX_GENERATION_FILE)
|
|
219
|
+
return { generationId, recordDigest: record?.digest ?? null, ...eligibility(personalHome, generationId) }
|
|
220
|
+
})
|
|
221
|
+
const staging = entries.filter((name) => name.startsWith('.staging-'))
|
|
222
|
+
const selectionsDir = privateDir(personalHome, 'selections', false)
|
|
223
|
+
const restoresDir = privateDir(personalHome, 'restores', false)
|
|
224
|
+
let history
|
|
225
|
+
try { history = readHistory(personalHome) } catch (error) {
|
|
226
|
+
if (!(error instanceof PersonalWorkspaceRefusal)) throw error
|
|
227
|
+
history = { files: listFiles(selectionsDir, (name) => !name.startsWith('.')), head: null, corrupt: true }
|
|
228
|
+
}
|
|
229
|
+
const { files: selections, head } = history
|
|
230
|
+
const restores = listFiles(restoresDir, (name) => !name.startsWith('.'))
|
|
231
|
+
const temporary = [
|
|
232
|
+
...listFiles(personalHome, (name) => TEMPORARY.test(name)).map((file) => ({ ...file, location: '.' })),
|
|
233
|
+
...listFiles(selectionsDir, (name) => TEMPORARY.test(name)).map((file) => ({ ...file, location: 'selections' })),
|
|
234
|
+
...listFiles(restoresDir, (name) => TEMPORARY.test(name)).map((file) => ({ ...file, location: 'restores' })),
|
|
235
|
+
]
|
|
236
|
+
return freeze({ authored, generations, staging, selections: { head, records: selections, corrupt: history.corrupt === true }, restores, temporary })
|
|
237
|
+
}, 'personal-home-unavailable')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// A generation's recorded inputs, verified against its own id: the id is the
|
|
241
|
+
// digest of the canonical inputs and this private home, so altered or moved
|
|
242
|
+
// history cannot be restored.
|
|
243
|
+
function recordedInputs(personalHome, generationId) {
|
|
244
|
+
if (typeof generationId !== 'string' || !ID.test(generationId)) refuse('malformed-input')
|
|
245
|
+
const root = privateDir(personalHome, 'generations', false)
|
|
246
|
+
if (!root) refuse('generation-missing')
|
|
247
|
+
let entry
|
|
248
|
+
try { entry = fs.lstatSync(path.join(root, generationId)) } catch { refuse('generation-missing') }
|
|
249
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) refuse('generation-corrupt')
|
|
250
|
+
let bytes
|
|
251
|
+
try { bytes = readBytes(path.join(root, generationId, 'inputs.json'), MAX_GENERATION_FILE, 'generation-inputs-too-large') }
|
|
252
|
+
catch (error) { if (error instanceof PersonalWorkspaceRefusal) throw error; refuse('generation-corrupt') }
|
|
253
|
+
if (hash(canonical({ inputsDigest: hash(bytes), personalHome })) !== generationId) refuse('generation-corrupt')
|
|
254
|
+
let inputs
|
|
255
|
+
try { inputs = JSON.parse(bytes) } catch { refuse('generation-corrupt') }
|
|
256
|
+
if (!inputs?.manifest || !inputs?.overlay || canonical(inputs) !== bytes.toString('utf8')) refuse('generation-corrupt')
|
|
257
|
+
return inputs
|
|
258
|
+
}
|
|
259
|
+
// The current authored file, read once: its exact bytes, digest and validated value.
|
|
260
|
+
function currentAuthored(personalHome, name, kind) {
|
|
261
|
+
const bytes = bytesOrNull(path.join(personalHome, name), MAX_RECORD)
|
|
262
|
+
if (bytes === null) {
|
|
263
|
+
// A restore interrupted between moving the file aside and linking its
|
|
264
|
+
// replacement leaves the name absent and the aside listed by the inventory.
|
|
265
|
+
if (fs.readdirSync(personalHome).some((entry) => ASIDE(kind).test(entry))) refuse('restore-interrupted')
|
|
266
|
+
refuse('malformed-input')
|
|
267
|
+
}
|
|
268
|
+
return { bytes, digest: hash(bytes), value: validateBytes(personalHome, bytes, kind) }
|
|
269
|
+
}
|
|
270
|
+
// A restore may narrow enrollment and bindings, never widen them: every
|
|
271
|
+
// repository the target enrolls must be enrolled now with the same root and
|
|
272
|
+
// remote, and every binding the target declares must be declared now.
|
|
273
|
+
function wideningCheck(target, current) {
|
|
274
|
+
const now = new Map(current.repos.filter((r) => r.enrolled).map((r) => [r.repoId, r]))
|
|
275
|
+
const readmitted = [], changed = []
|
|
276
|
+
for (const repo of target.repos.filter((r) => r.enrolled)) {
|
|
277
|
+
const present = now.get(repo.repoId)
|
|
278
|
+
if (!present) readmitted.push(repo.repoId)
|
|
279
|
+
else if (present.root !== repo.root || present.remote !== repo.remote) changed.push(repo.repoId)
|
|
280
|
+
}
|
|
281
|
+
if (readmitted.length) refuse('rollback-readmits-repository', { repoIds: Object.freeze(readmitted.sort()) })
|
|
282
|
+
if (changed.length) refuse('rollback-identity-changed', { repoIds: Object.freeze(changed.sort()) })
|
|
283
|
+
const added = target.bindings.filter((binding) => !current.bindings.includes(binding))
|
|
284
|
+
if (added.length) refuse('rollback-readds-binding', { count: added.length })
|
|
285
|
+
}
|
|
286
|
+
const summaryOf = (target, current, targets) => ({
|
|
287
|
+
enrollmentRemoved: current.repos.filter((r) => r.enrolled && !target.repos.some((t) => t.enrolled && t.repoId === r.repoId)).map((r) => r.repoId).sort(),
|
|
288
|
+
bindingsRemoved: current.bindings.filter((b) => !target.bindings.includes(b)).length,
|
|
289
|
+
manifestChanged: targets.manifestChanged,
|
|
290
|
+
overlayChanged: targets.overlayChanged,
|
|
291
|
+
})
|
|
292
|
+
export const restoreConfirmDigest = (plan) => `sha256:${hash(canonical({ action: 'restore', generationId: plan.generationId, from: plan.from, to: plan.to }))}`
|
|
293
|
+
|
|
294
|
+
// Plans restoring the authored manifest and overlay recorded by an earlier
|
|
295
|
+
// generation. The target files are validated now. Re-admission or a re-added
|
|
296
|
+
// binding is the person's fresh edit, never a restore's side effect. Restored
|
|
297
|
+
// files are written in the generation's canonical form (sorted keys, no
|
|
298
|
+
// indentation), not the person's original formatting.
|
|
299
|
+
export function planPersonalRestore(options) {
|
|
300
|
+
return safe(() => {
|
|
301
|
+
const { personalHome, generationId } = options ?? {}
|
|
302
|
+
privateHome(personalHome)
|
|
303
|
+
const inputs = recordedInputs(personalHome, generationId)
|
|
304
|
+
validateBytes(personalHome, canonical(inputs.manifest), 'manifest')
|
|
305
|
+
validateBytes(personalHome, canonical(inputs.overlay), 'overlay')
|
|
306
|
+
const manifest = currentAuthored(personalHome, MANIFEST_FILE, 'manifest')
|
|
307
|
+
const overlay = currentAuthored(personalHome, OVERLAY_FILE, 'overlay')
|
|
308
|
+
wideningCheck(inputs.manifest, manifest.value)
|
|
309
|
+
const to = { manifest: hash(canonical(inputs.manifest)), overlay: hash(canonical(inputs.overlay)) }
|
|
310
|
+
const from = { manifest: manifest.digest, overlay: overlay.digest }
|
|
311
|
+
const fields = { generationId, from, to }
|
|
312
|
+
const plan = freeze({ ...fields, summary: summaryOf(inputs.manifest, manifest.value, { manifestChanged: from.manifest !== to.manifest, overlayChanged: from.overlay !== to.overlay }),
|
|
313
|
+
confirm: restoreConfirmDigest(fields) })
|
|
314
|
+
restorePlans.add(plan)
|
|
315
|
+
return plan
|
|
316
|
+
}, 'personal-home-unavailable')
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Replaces `file` with `staged` only if `file` still holds exactly `expected`:
|
|
320
|
+
// the current file is moved aside, its bytes are verified, and the staged file
|
|
321
|
+
// is linked into place, which fails if another writer recreated the name. On any
|
|
322
|
+
// mismatch or failure to read the moved file, it is put back; when its name was
|
|
323
|
+
// taken meanwhile it stays as a listed temporary file. Writers that replace or
|
|
324
|
+
// recreate the file by path therefore keep their bytes. While the swap is in
|
|
325
|
+
// progress the name is briefly absent.
|
|
326
|
+
function compareAndSwap(personalHome, file, staged, expected, key) {
|
|
327
|
+
const aside = path.join(personalHome, `.restore-${randomUUID()}-aside-${key}.json`)
|
|
328
|
+
fs.renameSync(file, aside)
|
|
329
|
+
const putBack = () => {
|
|
330
|
+
try { fs.linkSync(aside, file) } catch { return false }
|
|
331
|
+
try { fs.unlinkSync(aside) } catch {}
|
|
332
|
+
try { syncDir(personalHome) } catch {}
|
|
333
|
+
return true
|
|
334
|
+
}
|
|
335
|
+
let moved
|
|
336
|
+
try { moved = readBytes(aside, MAX_RECORD) } catch { putBack(); refuse('authored-input-changed') }
|
|
337
|
+
if (hash(moved) !== expected) { putBack(); refuse('authored-input-changed') }
|
|
338
|
+
try { fs.linkSync(staged, file) } catch (error) {
|
|
339
|
+
if (error.code === 'EEXIST') refuse('authored-input-changed')
|
|
340
|
+
putBack(); throw error
|
|
341
|
+
}
|
|
342
|
+
fs.unlinkSync(staged)
|
|
343
|
+
fs.unlinkSync(aside)
|
|
344
|
+
syncDir(personalHome)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Restores a plan from planPersonalRestore. The current authored bytes are read
|
|
348
|
+
// once, validated, required to match the plan and re-checked for widening; those
|
|
349
|
+
// exact bytes are kept in an append-only restore record first. Both restored
|
|
350
|
+
// files are staged and validated before either is replaced, then each is
|
|
351
|
+
// replaced by compare-and-swap. A rerun completes an interrupted restore.
|
|
352
|
+
export function restorePersonalInputs(plan, options) {
|
|
353
|
+
return safe(() => {
|
|
354
|
+
const { personalHome, confirm } = options ?? {}
|
|
355
|
+
privateHome(personalHome)
|
|
356
|
+
if (!restorePlans.has(plan) || confirm !== plan.confirm || confirm !== restoreConfirmDigest(plan)) refuse('confirmation-mismatch')
|
|
357
|
+
const inputs = recordedInputs(personalHome, plan.generationId)
|
|
358
|
+
const targets = { manifest: [MANIFEST_FILE, canonical(inputs.manifest)], overlay: [OVERLAY_FILE, canonical(inputs.overlay)] }
|
|
359
|
+
const current = {}
|
|
360
|
+
for (const [key, [name, content]] of Object.entries(targets)) {
|
|
361
|
+
if (hash(content) !== plan.to[key]) refuse('generation-corrupt')
|
|
362
|
+
current[key] = currentAuthored(personalHome, name, key)
|
|
363
|
+
if (current[key].digest !== plan.from[key] && current[key].digest !== plan.to[key]) refuse('authored-input-changed')
|
|
364
|
+
}
|
|
365
|
+
wideningCheck(inputs.manifest, current.manifest.value)
|
|
366
|
+
const dir = privateDir(personalHome, 'restores', true)
|
|
367
|
+
const record = canonical({ schema: RESTORE_SCHEMA, generationId: plan.generationId, from: plan.from, to: plan.to,
|
|
368
|
+
replaced: { manifest: current.manifest.bytes.toString('base64'), overlay: current.overlay.bytes.toString('base64') } })
|
|
369
|
+
publishExclusive(dir, `${Date.now()}-${randomUUID()}.json`, record, 'restore')
|
|
370
|
+
const pending = Object.entries(targets).filter(([key]) => current[key].digest !== plan.to[key])
|
|
371
|
+
const staged = []
|
|
372
|
+
try {
|
|
373
|
+
for (const [key, [, content]] of pending) {
|
|
374
|
+
const file = path.join(personalHome, `.restore-${randomUUID()}-${key}.json`)
|
|
375
|
+
writeExclusive(file, content)
|
|
376
|
+
staged.push([key, file])
|
|
377
|
+
;(key === 'manifest' ? loadPersonalManifest : loadPersonalOverlay)(file, { personalHome })
|
|
378
|
+
}
|
|
379
|
+
for (const [key, file] of staged) compareAndSwap(personalHome, path.join(personalHome, targets[key][0]), file, current[key].digest, key)
|
|
380
|
+
} finally {
|
|
381
|
+
for (const [, file] of staged) fs.rmSync(file, { force: true })
|
|
382
|
+
}
|
|
383
|
+
return freeze({ generationId: plan.generationId, ...eligibility(personalHome, plan.generationId) })
|
|
384
|
+
}, 'restore-write-failed')
|
|
385
|
+
}
|
package/src/project/config.mjs
CHANGED
|
@@ -556,6 +556,14 @@ export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {})
|
|
|
556
556
|
if (setup.profile != null && !['single-repo', 'private-domain', 'shared-project', 'multi-repo', 'monorepo', 'control-workspace'].includes(setup.profile)) {
|
|
557
557
|
errors.push('setup.profile is invalid')
|
|
558
558
|
}
|
|
559
|
+
// The census reads these as path patterns relative to the project config's
|
|
560
|
+
// folder (docs/knowledge-graph.md), under the contract's pathString rules.
|
|
561
|
+
for (const key of ['include', 'exclude']) {
|
|
562
|
+
errors.push(...stringPathErrors(setup[key], `setup.${key}`))
|
|
563
|
+
if (typeof setup[key] === 'string' && (setup[key].includes('\u0000') || /^[a-z][a-z0-9+.-]*:/.test(setup[key]) || path.isAbsolute(setup[key]))) {
|
|
564
|
+
errors.push(`setup.${key} must be a path pattern relative to the project config folder`)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
559
567
|
if (alignment.appRepo != null && !firstString(alignment.appRepo)) errors.push('alignment.appRepo must be a non-empty string')
|
|
560
568
|
|
|
561
569
|
const repoNames = new Set()
|
|
@@ -149,7 +149,9 @@ export function createVerifiedFileSequence({ directory, initial, apply, ignoreFi
|
|
|
149
149
|
if (next.length < names.length) throw new Error('history shortened; preserve and inspect');
|
|
150
150
|
const ids = next.map(n => identity(path.join(directory, n)));
|
|
151
151
|
const reuse = names.every((n, i) => n === next[i] && identities[i] === ids[i]);
|
|
152
|
-
|
|
152
|
+
// Replay into a copy: a refused read must not leave the verified value
|
|
153
|
+
// half-applied, or every later read of this reader would be refused.
|
|
154
|
+
let result = reuse ? structuredClone(value) : initial();
|
|
153
155
|
for (let i = reuse ? names.length : 0; i < next.length; i++) {
|
|
154
156
|
const file = path.join(directory, next[i]);
|
|
155
157
|
result = apply(readRegularTextNoFollow(file), result, i + 1, next[i]);
|
|
@@ -188,7 +188,7 @@ export function classifyManagedPath(relativePath) {
|
|
|
188
188
|
// moves up a level. Any other failure (a directory that cannot be searched,
|
|
189
189
|
// a link loop) is thrown: a lexical path standing in for a real one would
|
|
190
190
|
// weaken the alias check exactly where it matters.
|
|
191
|
-
function realLocation(target, realpath) {
|
|
191
|
+
export function realLocation(target, realpath = fs.realpathSync.native) {
|
|
192
192
|
const missing = []
|
|
193
193
|
let current = path.resolve(target)
|
|
194
194
|
for (;;) {
|
|
@@ -9,10 +9,11 @@ import { canonicalJson, compareText, isoTime } from '../../../runtime/obsidian/d
|
|
|
9
9
|
import { readObsidianEnablement } from '../../../runtime/obsidian/enablement.mjs'
|
|
10
10
|
import { ObsidianMaintenanceRefusal } from '../../../runtime/obsidian/errors.mjs'
|
|
11
11
|
import { protectedRoots, readInstalledApplyPolicy, readLocalPointer, readMachineSettings, resolveDataRoot, workspaceStateRoot } from '../../../runtime/obsidian/machine-settings.mjs'
|
|
12
|
-
import {
|
|
12
|
+
import { createProductionSeams, eligibilityFor } from '../../../runtime/obsidian/pipeline.mjs'
|
|
13
13
|
import { OPEN_EDIT_STATES, createMaintenanceStateStore } from '../../../runtime/obsidian/state-store.mjs'
|
|
14
14
|
import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, manifestLayoutVersion } from '../contracts.mjs'
|
|
15
15
|
import { readMarkdownLens } from '../materialize/byte-lens.mjs'
|
|
16
|
+
import { personalOverlayRepoOf } from '../personal-workspace.mjs'
|
|
16
17
|
import { exchangeFiles, probeExchange } from '../publication/exchange.mjs'
|
|
17
18
|
import { createJournal, newJournalId } from '../recovery/journal.mjs'
|
|
18
19
|
import { recheckDisplacedFiles } from '../recovery/late-writer.mjs'
|
|
@@ -91,6 +92,7 @@ export const SOURCE_APPLY_REFUSALS = Object.freeze([
|
|
|
91
92
|
'source-inside-managed-root', 'source-inside-git-directory', 'source-git-ignored', 'source-ignore-state-unknown', 'sibling-edit-unobservable', 'lease-held', 'object-conflicted',
|
|
92
93
|
'stale-source', 'edit-not-applicable', 'change-outside-authored-body', 'no-source-change', 'exchange-unavailable', 'apply-volume-mismatch', 'batch-bound-reached',
|
|
93
94
|
'concurrent-source-writer', 'source-changed-during-apply', 'interrupted-before-exchange', 'apply-interrupted-needs-person', 'recovery-state-unreadable', 'apply-outcome-unknown', 'source-changed-after-apply',
|
|
95
|
+
'personal-overlay-proposals-only',
|
|
94
96
|
])
|
|
95
97
|
|
|
96
98
|
// Refusals that say the source or the object is contested, not that this machine or this request cannot apply.
|
|
@@ -137,6 +139,10 @@ function readNoFollow(file, { withMode = false } = {}) {
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
const NOT_A_FILE = new Set(['EISDIR', 'EINVAL', 'ENOTDIR', 'EMLINK'])
|
|
142
|
+
// How often a file another program replaces by rename is read again: the pointer to the current manifest, as the
|
|
143
|
+
// publisher reads a note, and the published note prepared again. One that keeps changing is a typed refusal.
|
|
144
|
+
const READ_ATTEMPTS = 5
|
|
145
|
+
const PREPARE_ATTEMPTS = 3
|
|
140
146
|
const UNREADABLE = new Set(['EACCES', 'EPERM', 'EIO'])
|
|
141
147
|
|
|
142
148
|
// The digest of the file, null when nothing is there, 'unreadable' when what is there is not a regular file: whatever
|
|
@@ -265,7 +271,7 @@ export function createSourceApplyForOracleTests(primitives = SOURCE_APPLY_PRIMIT
|
|
|
265
271
|
// ones the workspace has.
|
|
266
272
|
return function createSourceApply(context = {}) {
|
|
267
273
|
const {
|
|
268
|
-
loadProject, dataRoot, env = process.env, platform = process.platform, clock = () => new Date(), eligibility =
|
|
274
|
+
loadProject, dataRoot, env = process.env, platform = process.platform, clock = () => new Date(), eligibility: fixedEligibility = null,
|
|
269
275
|
quietPeriodMs = DEFAULT_APPLY_QUIET_PERIOD_MS, recheckWindowMs = DEFAULT_APPLY_RECHECK_WINDOW_MS, exchangeOptions = {},
|
|
270
276
|
crash = () => {}, beforeExchange = async () => {}, leasePid, extraManagedRoots = [], manualBatchBound = MAX_MANUAL_BATCH_SIZE, objectStore = openObjectStore,
|
|
271
277
|
} = context
|
|
@@ -303,26 +309,41 @@ export function createSourceApplyForOracleTests(primitives = SOURCE_APPLY_PRIMIT
|
|
|
303
309
|
return workspace
|
|
304
310
|
}
|
|
305
311
|
|
|
306
|
-
// The canonical graph as it is now, and the corpus profile of this machine. Built once per call
|
|
312
|
+
// The canonical graph as it is now, and the corpus profile of this machine. Built once per call, with the notes the
|
|
313
|
+
// machine settings let into a view, exactly as the engine builds it (eligibilityFor), unless a test fixes them.
|
|
307
314
|
function currentCorpus(workspace) {
|
|
308
|
-
// The graph reads every enrolled source. A file this process may not read is a refusal that names no file.
|
|
315
|
+
// The graph reads every enrolled source. A file this process may not read is a refusal that names no file. So is
|
|
316
|
+
// one another program removes or replaces while the graph is built (a save by delete and create, a checkout):
|
|
317
|
+
// the graph cannot be built from what is there at this moment, and the cause says which of the two it was.
|
|
309
318
|
try {
|
|
310
319
|
workspace.corpus ??= {
|
|
311
|
-
graph: seams.buildGraph({ project: workspace.project, eligibility }),
|
|
320
|
+
graph: seams.buildGraph({ project: workspace.project, eligibility: fixedEligibility ?? eligibilityFor({ machine: workspace.machine, project: workspace.project }) }),
|
|
312
321
|
profile: seams.profileFor({ project: workspace.project, workspaceId: workspace.workspaceId, audienceAllow: workspace.machine.audienceAllow }),
|
|
313
322
|
}
|
|
314
323
|
} catch (error) {
|
|
315
|
-
if (isTyped(error) || !UNREADABLE.has(error?.code)) throw error
|
|
324
|
+
if (isTyped(error) || !(UNREADABLE.has(error?.code) || GONE.has(error?.code) || error?.code === 'ELEAFCHANGED')) throw error
|
|
316
325
|
refuse('corpus-unreadable', { cause: error.code })
|
|
317
326
|
}
|
|
318
327
|
return workspace.corpus
|
|
319
328
|
}
|
|
320
329
|
|
|
330
|
+
// The pointer to the current manifest is replaced by rename as the last step of every publication, so a
|
|
331
|
+
// publication by the engine can replace it between the look at its path and its open (ELEAFCHANGED). Every rename
|
|
332
|
+
// leaves a complete pointer, so it is read again, as the publisher reads a note; one that keeps changing refuses.
|
|
333
|
+
function currentManifestOf(store) {
|
|
334
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
335
|
+
try { return store.readCurrentManifest() } catch (error) {
|
|
336
|
+
if (error?.code !== 'ELEAFCHANGED') throw error
|
|
337
|
+
if (attempt >= READ_ATTEMPTS) refuse('manifest-unavailable', { cause: 'changed-while-reading' })
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
321
342
|
// The immutable manifest of one generation of one view. The current one when it is that generation; otherwise
|
|
322
343
|
// the stored file, whose name carries the generation and the head of its digest.
|
|
323
344
|
function manifestOf(workspace, scopeId, generationId) {
|
|
324
345
|
const store = workspace.storeOf(scopeId)
|
|
325
|
-
const current = store
|
|
346
|
+
const current = currentManifestOf(store)
|
|
326
347
|
if (current?.generationId === generationId) return current
|
|
327
348
|
const directory = path.join(workspace.workspaceRoot, 'state', 'manifests', segment(scopeId))
|
|
328
349
|
let names = []
|
|
@@ -341,21 +362,35 @@ export function createSourceApplyForOracleTests(primitives = SOURCE_APPLY_PRIMIT
|
|
|
341
362
|
// publisher keeps no copy of a note it created, so the note is prepared again from the sources as they are now,
|
|
342
363
|
// in the vault layout of that generation, and used only when it has the digest the manifest recorded; a retained
|
|
343
364
|
// object of that digest serves as well.
|
|
365
|
+
//
|
|
366
|
+
// A source replaced between the look at its path and its open (ELEAFCHANGED) is another program writing it, as it
|
|
367
|
+
// is to the publisher. A rename leaves a complete file, so the note is prepared again from what is there now: a
|
|
368
|
+
// source saved with the same bytes, or another source of the scope replaced, then prepares the note as published,
|
|
369
|
+
// and a source with other bytes prepares another note, which the caller tells from the digest and refuses
|
|
370
|
+
// stale-source. A preparation that cannot be carried out answers null, and the caller reads the source once more
|
|
371
|
+
// to tell which typed refusal that is. Only a preparation that succeeded is kept for its scope and generation: a
|
|
372
|
+
// failure tells of one moment, not of the generation, and is never the answer to a later edit.
|
|
344
373
|
function publishedNoteOf(workspace, { scope, manifest, noteEntry }) {
|
|
345
374
|
const store = workspace.storeOf(scope.scopeId)
|
|
346
375
|
try { return store.readObject(noteEntry.noteDigest) } catch (error) { if (error.code !== 'ENOENT' && error.code !== 'recovery-object-corrupt') throw error }
|
|
347
376
|
workspace.prepared ??= new Map()
|
|
348
377
|
const preparedKey = `${scope.scopeId}\u0000${manifest.generationId}`
|
|
349
378
|
if (!workspace.prepared.has(preparedKey)) {
|
|
350
|
-
let files
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
379
|
+
let files
|
|
380
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
381
|
+
try {
|
|
382
|
+
const { graph, profile } = currentCorpus(workspace)
|
|
383
|
+
const snapshot = seams.captureSnapshot({ project: workspace.project, graph, workspaceId: workspace.workspaceId, index: new Map(), configDigest: manifest.ext?.[EXT]?.configDigest ?? `sha256:${'0'.repeat(64)}`, capturedAt: isoTime(clock) })
|
|
384
|
+
files = seams.prepareView({
|
|
385
|
+
snapshot, profile, scope, persistentPathRegistry: workspace.stateStore.readPathRegistry(), priorManifest: manifest, existingSettings: null, clock,
|
|
386
|
+
vaultRootBytes: Buffer.byteLength(store.vaultRoot, 'utf8'), layout: manifestLayoutVersion(manifest),
|
|
387
|
+
}).files
|
|
388
|
+
break
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (!isTyped(error) && error?.code !== 'ELEAFCHANGED' && !GONE.has(error?.code) && !UNREADABLE.has(error?.code)) throw error
|
|
391
|
+
if (error?.code !== 'ELEAFCHANGED' || attempt >= PREPARE_ATTEMPTS) return null
|
|
392
|
+
}
|
|
393
|
+
}
|
|
359
394
|
workspace.prepared.set(preparedKey, files)
|
|
360
395
|
}
|
|
361
396
|
const file = workspace.prepared.get(preparedKey).find((item) => item.path === noteEntry.path && item.digest === noteEntry.noteDigest)
|
|
@@ -533,6 +568,10 @@ export function createSourceApplyForOracleTests(primitives = SOURCE_APPLY_PRIMIT
|
|
|
533
568
|
const decideWith = (editClass, attempts) => rules.decide({ request, workspace: { workspaceRoot: workspace.workspaceRoot, workspaceId: workspace.workspaceId }, graph, profile, object: identity, editClass, attempts })
|
|
534
569
|
const visibility = decideWith('body-replacement', [])
|
|
535
570
|
if (!visibility.allowed && visibility.code === 'object-not-visible') refuse('object-not-visible')
|
|
571
|
+
// A private note of a personal workspace is authored in its overlay, and its file belongs to a generation the
|
|
572
|
+
// personal-workspace module owns: an edit to it is never written there. It is not proposed yet either; it stays
|
|
573
|
+
// held in the vault.
|
|
574
|
+
if (identity.repoId === personalOverlayRepoOf(workspace.project)) refuse('personal-overlay-proposals-only')
|
|
536
575
|
|
|
537
576
|
const resolved = resolveEdit(workspace, edit)
|
|
538
577
|
const key = resolved.idempotencyKey
|