@mnstry/atelier 0.2.0-alpha.5 → 0.2.0-alpha.8
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 +191 -0
- package/README.md +86 -25
- package/SECURITY.md +6 -1
- package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
- package/contracts/atelier-coauthor.v1.schema.json +163 -0
- package/contracts/atelier-decision-request.v1.schema.json +476 -0
- package/contracts/atelier-decision-result.v1.schema.json +500 -0
- package/contracts/atelier-guide.v1.schema.json +227 -0
- package/contracts/atelier-intake.v1.schema.json +151 -0
- package/contracts/atelier-migration.v2.schema.json +61 -0
- package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
- package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
- package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
- package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
- package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
- package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
- package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
- package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
- package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
- package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
- package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
- package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
- package/contracts/atelier-presentation.v1.schema.json +1293 -0
- package/contracts/atelier-repository-observation.v1.schema.json +163 -0
- package/contracts/atelier-review.v1.schema.json +1601 -0
- package/contracts/atelier-skill-steward.v1.schema.json +692 -0
- package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
- package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
- package/contracts/atelier-vault-publication.v1.schema.json +58 -0
- package/docs/assurance-controls.md +12 -2
- package/docs/atelier-runtime.md +13 -2
- package/docs/atelier-sync.md +172 -0
- package/docs/blocks/claims.md +28 -18
- package/docs/blocks/will-not-do.md +12 -3
- package/docs/coauthor-session.md +57 -0
- package/docs/connected-composition.md +75 -0
- package/docs/decisions.md +167 -0
- package/docs/exact-upgrades.md +159 -0
- package/docs/extension-pack-lifecycle.md +33 -0
- package/docs/guided-upgrades.md +99 -0
- package/docs/install.md +65 -9
- package/docs/intake-and-guides.md +65 -0
- package/docs/integration-and-review-plan.md +409 -0
- package/docs/integration-contract-decisions.md +22 -0
- package/docs/local-review.md +84 -0
- package/docs/local-services.md +227 -0
- package/docs/local-state.md +107 -0
- package/docs/obsidian-contract.md +687 -0
- package/docs/obsidian.md +387 -0
- package/docs/portable-presentation.md +328 -0
- package/docs/presentation-consumer-boundaries.md +75 -0
- package/docs/presentation-review-disposition.md +47 -0
- package/docs/project-command-inventory.md +38 -0
- package/docs/project-options.md +53 -0
- package/docs/release-engineering.md +99 -2
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +70 -2
- package/docs/vault-service.md +292 -0
- package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
- package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
- package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
- package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
- package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
- package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
- package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
- package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
- package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
- package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
- package/fixtures/atelier-intake/valid/source.v1.json +7 -0
- package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
- package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
- package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
- package/fixtures/atelier-review/valid/bound.v1.json +229 -0
- package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
- package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
- package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
- package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
- package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
- package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
- package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
- package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
- package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
- package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
- package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
- package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
- package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
- package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
- package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
- package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
- package/fixtures/decisions/request/valid/triage.v1.json +60 -0
- package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
- package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
- package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
- package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
- package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
- package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
- package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
- package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
- package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
- package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
- package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
- package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
- package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
- package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
- package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
- package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
- package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
- package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
- package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
- package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
- package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
- package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
- package/fixtures/obsidian/edits/workspace.json +65 -0
- package/fixtures/obsidian/materialization/expected-full.json +99 -0
- package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
- package/fixtures/obsidian/materialization/workspace.json +54 -0
- package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
- package/fixtures/ui/presentation/pane.v1.json +7 -0
- package/fixtures/ui/presentation/reference.v1.json +293 -0
- package/package.json +61 -6
- package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/claude/atelier-open/SKILL.md +5 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
- package/skills/claude/mnstry-readiness/SKILL.md +5 -0
- package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/codex/atelier-open/SKILL.md +5 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
- package/skills/codex/mnstry-readiness/SKILL.md +5 -0
- package/src/access/preview-enforcer.mjs +84 -0
- package/src/boundary/content-rules.mjs +9 -4
- package/src/boundary/policy.mjs +74 -42
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +61 -3
- package/src/coauthor/session.mjs +161 -0
- package/src/coauthor/store.mjs +173 -0
- package/src/collaboration/event-ledger.mjs +16 -33
- package/src/collaboration/inspection-bundle.mjs +223 -0
- package/src/collaboration/review-contracts.mjs +25 -0
- package/src/collaboration/review-store.mjs +305 -0
- package/src/commands/coauthor.mjs +26 -0
- package/src/commands/config.mjs +6 -1
- package/src/commands/extension-pack.mjs +15 -8
- package/src/commands/init.mjs +44 -5
- package/src/commands/obsidian.mjs +368 -0
- package/src/commands/review.mjs +147 -0
- package/src/commands/setup.mjs +51 -14
- package/src/commands/skills.mjs +199 -0
- package/src/commands/sync.mjs +107 -0
- package/src/composition/registry.mjs +27 -0
- package/src/composition/wire.mjs +122 -0
- package/src/contracts/corpus.mjs +35 -0
- package/src/decisions/contracts.d.mts +96 -0
- package/src/decisions/contracts.mjs +321 -0
- package/src/disclosure/content-scan.mjs +13 -0
- package/src/export/atelier-export-contract.mjs +16 -1
- package/src/extension-packs/lifecycle.mjs +154 -0
- package/src/graph/graph.mjs +64 -2
- package/src/graph/knowledge-graph.mjs +548 -24
- package/src/guides/contracts.mjs +54 -0
- package/src/index.mjs +29 -0
- package/src/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +50 -49
- package/src/project/durable-state.mjs +161 -0
- package/src/project/file-class.mjs +166 -0
- package/src/project/private-state.mjs +11 -1
- package/src/projection/obsidian/contracts.mjs +571 -0
- package/src/projection/obsidian/edits/align.mjs +169 -0
- package/src/projection/obsidian/edits/apply.mjs +889 -0
- package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
- package/src/projection/obsidian/edits/contribution.mjs +108 -0
- package/src/projection/obsidian/edits/index.mjs +19 -0
- package/src/projection/obsidian/edits/object-identity.mjs +61 -0
- package/src/projection/obsidian/edits/object-store.mjs +517 -0
- package/src/projection/obsidian/edits/observe.mjs +123 -0
- package/src/projection/obsidian/edits/policy.mjs +123 -0
- package/src/projection/obsidian/edits/regions.mjs +648 -0
- package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
- package/src/projection/obsidian/materialize/index.mjs +7 -0
- package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
- package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
- package/src/projection/obsidian/materialize/settings.mjs +57 -0
- package/src/projection/obsidian/materialize/stage.mjs +27 -0
- package/src/projection/obsidian/proposals/adapter.mjs +572 -0
- package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
- package/src/projection/obsidian/proposals/contribution.mjs +55 -0
- package/src/projection/obsidian/proposals/index.mjs +23 -0
- package/src/projection/obsidian/proposals/observation.mjs +216 -0
- package/src/projection/obsidian/proposals/queue.mjs +277 -0
- package/src/projection/obsidian/proposals/router.mjs +168 -0
- package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
- package/src/projection/obsidian/publication/exchange.mjs +117 -0
- package/src/projection/obsidian/publication/index.mjs +5 -0
- package/src/projection/obsidian/publication/publisher.mjs +588 -0
- package/src/projection/obsidian/publication/test-seam.mjs +16 -0
- package/src/projection/obsidian/publication/transport.mjs +139 -0
- package/src/projection/obsidian/recovery/index.mjs +6 -0
- package/src/projection/obsidian/recovery/journal.mjs +134 -0
- package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
- package/src/projection/obsidian/recovery/restart.mjs +316 -0
- package/src/projection/obsidian/recovery/store.mjs +234 -0
- package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
- package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
- package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
- package/src/projection/obsidian/selection-ui/index.mjs +12 -0
- package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
- package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
- package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
- package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
- package/src/projection/project.mjs +28 -19
- package/src/readiness-protocols/evidence.mjs +282 -0
- package/src/readiness-protocols/runtime.mjs +4 -3
- package/src/readiness-protocols/source-read.mjs +39 -0
- package/src/runtime/git-adapter.mjs +189 -0
- package/src/runtime/local-state.mjs +439 -0
- package/src/runtime/obsidian/app-capability.mjs +110 -0
- package/src/runtime/obsidian/app-production-seams.mjs +96 -0
- package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
- package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
- package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
- package/src/runtime/obsidian/contributions.mjs +27 -0
- package/src/runtime/obsidian/documents.mjs +37 -0
- package/src/runtime/obsidian/enablement.mjs +53 -0
- package/src/runtime/obsidian/engine.mjs +532 -0
- package/src/runtime/obsidian/errors.mjs +14 -0
- package/src/runtime/obsidian/extension-points.mjs +103 -0
- package/src/runtime/obsidian/index.mjs +34 -0
- package/src/runtime/obsidian/lifecycle.mjs +258 -0
- package/src/runtime/obsidian/machine-settings.mjs +260 -0
- package/src/runtime/obsidian/observation.mjs +158 -0
- package/src/runtime/obsidian/opening.mjs +243 -0
- package/src/runtime/obsidian/pending-edits.mjs +141 -0
- package/src/runtime/obsidian/pipeline.mjs +122 -0
- package/src/runtime/obsidian/private-lock.mjs +156 -0
- package/src/runtime/obsidian/process-identity.mjs +58 -0
- package/src/runtime/obsidian/service-client.mjs +66 -0
- package/src/runtime/obsidian/service-main.mjs +89 -0
- package/src/runtime/obsidian/service-record.mjs +169 -0
- package/src/runtime/obsidian/service-server.mjs +121 -0
- package/src/runtime/obsidian/service.mjs +216 -0
- package/src/runtime/obsidian/startup-adapters.mjs +87 -0
- package/src/runtime/obsidian/state-store.mjs +180 -0
- package/src/runtime/obsidian/sync-notice.mjs +33 -0
- package/src/runtime/obsidian/tick-loop.mjs +84 -0
- package/src/runtime/obsidian/watchers.mjs +36 -0
- package/src/runtime/repository-observation.mjs +491 -0
- package/src/runtime/supervisor.mjs +803 -0
- package/src/server/local-sidecar.mjs +44 -0
- package/src/server/server.mjs +1 -0
- package/src/skills/steward.mjs +780 -0
- package/src/ui/presentation/browser.mjs +278 -0
- package/src/ui/presentation/contract.mjs +116 -0
- package/src/ui/presentation/index.mjs +7 -0
- package/src/ui/presentation/native.mjs +153 -0
- package/src/ui/presentation/proof.mjs +57 -0
- package/src/ui/presentation/schema-check.mjs +30 -0
- package/src/ui/presentation/schema.generated.mjs +1371 -0
- package/src/ui/presentation/state.mjs +43 -0
- package/src/ui/presentation/styles.mjs +67 -0
- package/src/ui/presentation/tokens.mjs +74 -0
- package/src/ui/presentation/web.mjs +85 -0
- package/src/ui/review-page.mjs +32 -0
- package/src/upgrade/explanation.mjs +33 -0
- package/src/upgrade/provenance.mjs +338 -0
- package/src/upgrade/transaction-files.mjs +103 -0
- package/src/upgrade/transaction.mjs +475 -0
- package/src/upgrade/upgrade.mjs +63 -28
- package/src/vault/hosts.mjs +21 -0
- package/src/vault/identity.mjs +19 -0
- package/src/vault/index.mjs +7 -0
- package/src/vault/interface.mjs +11 -0
- package/src/vault/metadata.mjs +31 -0
- package/src/vault/privacy.mjs +148 -0
- package/src/vault/probe.mjs +89 -0
- package/src/vault/service.mjs +131 -0
- package/src/vault/source.mjs +45 -0
- package/src/vault/storage.mjs +23 -0
- package/templates/distribution-workspace/.claude/launch.json +6 -2
- package/templates/distribution-workspace/gitignore +1 -0
- package/templates/distribution-workspace/project/README.md +2 -2
- package/templates/external-project-workspace/README.md +45 -0
- package/templates/external-project-workspace/answers.example.json +4 -0
- package/templates/external-project-workspace/atelier.project.json +40 -0
- package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
- package/templates/external-project-workspace/gitignore +8 -0
- package/templates/external-project-workspace/pack-lifecycle.json +14 -0
- package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
- package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
- package/templates/external-project-workspace/repo-access.v1.json +9 -0
- package/templates/external-project-workspace/source/README.md +11 -0
- package/templates/launch.json +6 -2
- package/templates/private-domain-workspace/.claude/launch.json +6 -2
- package/templates/private-domain-workspace/domain/README.md +2 -2
- package/templates/private-domain-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/.claude/launch.json +6 -2
- package/templates/shared-project-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/project/README.md +2 -2
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { createProposalStore } from '../../../collaboration/proposals.mjs'
|
|
5
|
+
import { AtelierDiagnosticError } from '../../../project/config.mjs'
|
|
6
|
+
import { compareText, isoTime } from '../../../runtime/obsidian/documents.mjs'
|
|
7
|
+
import { ObsidianMaintenanceRefusal } from '../../../runtime/obsidian/errors.mjs'
|
|
8
|
+
import { readMachineSettings } from '../../../runtime/obsidian/machine-settings.mjs'
|
|
9
|
+
import { DEFAULT_ELIGIBILITY, createProductionSeams } from '../../../runtime/obsidian/pipeline.mjs'
|
|
10
|
+
import { acquirePrivateGenerationLock, createAbandonmentProof } from '../../../runtime/obsidian/private-lock.mjs'
|
|
11
|
+
import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, assertObsidianContract } from '../contracts.mjs'
|
|
12
|
+
import { SOURCE_APPLY_PRIMITIVES } from '../edits/apply.mjs'
|
|
13
|
+
import { EditArbitrationRefusal } from '../edits/arbitrate.mjs'
|
|
14
|
+
import { isIdentifier } from '../edits/object-identity.mjs'
|
|
15
|
+
import { openObjectStore } from '../edits/object-store.mjs'
|
|
16
|
+
import { decideApply } from '../edits/policy.mjs'
|
|
17
|
+
import { applyEditLens } from '../edits/regions.mjs'
|
|
18
|
+
import { PublicationRefusal } from '../recovery/store.mjs'
|
|
19
|
+
import {
|
|
20
|
+
PROPOSAL_BACKPRESSURE, PROPOSAL_LEDGER_LIMITS, classifyLedgerRead, classifyStoreRefusal, estimateEventLineBytes, isDue, isExhausted, nextAttemptAt, preflightAppend,
|
|
21
|
+
} from './backpressure.mjs'
|
|
22
|
+
import { createTickObservation, manifestOf, recordedOperationOf, retained } from './observation.mjs'
|
|
23
|
+
import { OPEN_QUEUE_STATES, ProposalQueueRefusal, openProposalQueue } from './queue.mjs'
|
|
24
|
+
import { PROPOSAL_STORE_DIRECTORY, adapterOperationId, isAdapterOperationId, isRoutableSourcePath, proposalStoreId, resolveProposalRoute } from './router.mjs'
|
|
25
|
+
|
|
26
|
+
// The proposal adapter: a structural edit somebody made in a vault becomes ONE
|
|
27
|
+
// durable copy-only proposal in the store of the repository that owns the
|
|
28
|
+
// source, and nothing else happens to it.
|
|
29
|
+
//
|
|
30
|
+
// An edit the byte lens cannot turn into source bytes (a new or changed link
|
|
31
|
+
// to another note, an edited front matter) is recorded by the object store as
|
|
32
|
+
// an operation of kind `semantic-proposal`, state `proposed`. On a tick the
|
|
33
|
+
// adapter first observes the open pending edits the object store does not know
|
|
34
|
+
// yet (observation.mjs: the pre-lease part of an apply, which records what
|
|
35
|
+
// each edit is and writes no source), then is handed the pending edits; for
|
|
36
|
+
// each such operation it has not settled, in this order and under the private
|
|
37
|
+
// lock of that repository:
|
|
38
|
+
//
|
|
39
|
+
// 1. records the operation in its queue (queue.mjs), before anything else;
|
|
40
|
+
// 2. reads the ledger of the store and refuses or waits when it has no room
|
|
41
|
+
// or cannot be read (backpressure.mjs);
|
|
42
|
+
// 3. LOOKS in the persisted store for a proposal that already carries this
|
|
43
|
+
// adapter operation identity, and creates one only when there is none;
|
|
44
|
+
// 4. records `submitted`, reads the proposal back, and records
|
|
45
|
+
// `acknowledged` with a receipt that binds the edit to the proposal.
|
|
46
|
+
//
|
|
47
|
+
// The identifier the store gives a proposal is seeded with the time, so it
|
|
48
|
+
// names a proposal and decides nothing here: what makes two proposals the same
|
|
49
|
+
// request is the adapter operation identity inside the payload. A crash at any
|
|
50
|
+
// point leaves either no proposal and a queued record, or a proposal that step
|
|
51
|
+
// 3 finds on the next tick. Two ticks or two processes cannot both create,
|
|
52
|
+
// because step 3 and the creation happen under one lock, which is taken from a
|
|
53
|
+
// holder only with proof that its process is gone and never because time
|
|
54
|
+
// passed.
|
|
55
|
+
//
|
|
56
|
+
// What a proposal says. Identifiers, the repository-relative path of the
|
|
57
|
+
// source, codes, byte offsets, digests and references to the preserved bytes,
|
|
58
|
+
// and sentences made from those codes. It holds NO text of any note: the bytes
|
|
59
|
+
// a person typed may name or quote another document (a link carries the title
|
|
60
|
+
// of its target), and a reviewer of one repository must not learn the title of
|
|
61
|
+
// a document of another, or of one withheld from them, from a proposal. The
|
|
62
|
+
// preserved bytes stay in private state, named by digest.
|
|
63
|
+
//
|
|
64
|
+
// The adapter writes no source file and no vault, and no status of a proposal
|
|
65
|
+
// is an instruction to it: a proposal a reviewer accepts is a note for a
|
|
66
|
+
// person or an agent with a checkout, exactly as the store says.
|
|
67
|
+
|
|
68
|
+
export const PROPOSAL_ADAPTER_ID = 'atelier.obsidian-proposal-adapter/v1'
|
|
69
|
+
export const PROPOSAL_ADAPTER_ACTOR = 'atelier obsidian proposal adapter'
|
|
70
|
+
export const PROPOSAL_PAYLOAD_SCHEMA = 'atelier-obsidian-structural-proposal/v1'
|
|
71
|
+
export const PROPOSAL_PAYLOAD_KIND = 'obsidian-structural-edit'
|
|
72
|
+
export const PROPOSAL_LOCK_PURPOSE = 'proposal-adapter'
|
|
73
|
+
export const PROPOSAL_ADAPTER_CRASH_STEPS = Object.freeze(['queued', 'before-append', 'appended', 'submitted', 'acknowledged'])
|
|
74
|
+
export const MAX_CHANGE_OCCURRENCES = 64
|
|
75
|
+
|
|
76
|
+
const EXT = OBSIDIAN_EXT_KEY
|
|
77
|
+
const STRUCTURAL_CODES = Object.freeze(['unsupported-structural-edit', 'unsupported-frontmatter-edit'])
|
|
78
|
+
const CODE = /^[a-z][a-z0-9-]{0,63}$/
|
|
79
|
+
const CHANGE_SENTENCES = Object.freeze({
|
|
80
|
+
'unsupported-structural-edit': 'A link to another note of the vault was added, changed or removed.',
|
|
81
|
+
'unsupported-frontmatter-edit': 'The front matter of the note was edited.',
|
|
82
|
+
unclassified: 'The edit changes the structure of the note in a way that was not classified further.',
|
|
83
|
+
})
|
|
84
|
+
const isTyped = (error) => error instanceof ObsidianMaintenanceRefusal || error instanceof AtelierDiagnosticError || error instanceof ObsidianContractRefusal || error instanceof PublicationRefusal
|
|
85
|
+
|| error instanceof EditArbitrationRefusal || error instanceof ProposalQueueRefusal
|
|
86
|
+
const hashOf = (parts) => createHash('sha256').update(parts.join('\u0000')).digest('hex')
|
|
87
|
+
const isOffset = (value) => Number.isInteger(value) && value >= 0
|
|
88
|
+
|
|
89
|
+
// What of a lens refusal a proposal repeats: the code, a reason code and byte offsets. Anything else it holds
|
|
90
|
+
// (a digest of the note, a name) stays behind.
|
|
91
|
+
export function describeChange(outcome) {
|
|
92
|
+
if (outcome?.kind !== 'refusal' || !STRUCTURAL_CODES.includes(outcome.code)) return { code: 'unclassified', reason: typeof outcome?.code === 'string' && CODE.test(outcome.code) ? outcome.code : null }
|
|
93
|
+
const detail = outcome.detail ?? {}
|
|
94
|
+
const change = { code: outcome.code, reason: typeof detail.reason === 'string' && CODE.test(detail.reason) ? detail.reason : null }
|
|
95
|
+
for (const name of ['firstDifference', 'publishedStart', 'publishedEnd']) if (isOffset(detail[name])) change[name] = detail[name]
|
|
96
|
+
if (Array.isArray(detail.occurrences)) {
|
|
97
|
+
const occurrences = detail.occurrences.filter((item) => isOffset(item?.noteStart) && isOffset(item?.noteEnd)).map(({ noteStart, noteEnd }) => ({ noteStart, noteEnd }))
|
|
98
|
+
// Said, never silent: how many there were, and how many of them are listed.
|
|
99
|
+
change.occurrenceCount = occurrences.length
|
|
100
|
+
change.occurrences = occurrences.slice(0, MAX_CHANGE_OCCURRENCES)
|
|
101
|
+
}
|
|
102
|
+
return change
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The body handed to createProposal. `item` is the operation, `route` where it goes, `change` what describeChange said.
|
|
106
|
+
export function buildProposalContent({ item, route, change }) {
|
|
107
|
+
const sentence = CHANGE_SENTENCES[change.code] ?? CHANGE_SENTENCES.unclassified
|
|
108
|
+
const summary = [
|
|
109
|
+
'Somebody edited the generated note of this document in an Obsidian view, and the edit cannot be written back as a replacement of the body.',
|
|
110
|
+
sentence,
|
|
111
|
+
`Code: ${change.code}${change.reason ? ` (${change.reason})` : ''}.${change.occurrenceCount === undefined ? '' : ` Places in the edited note: ${change.occurrenceCount}.`}`,
|
|
112
|
+
'The edited bytes and the source they were judged against are kept in the private state of the workspace that observed the edit, under the digests below. Nothing was written to the source.',
|
|
113
|
+
]
|
|
114
|
+
return {
|
|
115
|
+
path: route.sourcePath,
|
|
116
|
+
action: 'copy.agentPrompt',
|
|
117
|
+
actor: PROPOSAL_ADAPTER_ACTOR,
|
|
118
|
+
viewId: item.scopeId,
|
|
119
|
+
intent: `Review a structural edit of ${route.sourcePath} made in an Obsidian view (${change.code}).`.slice(0, 500),
|
|
120
|
+
reason: sentence,
|
|
121
|
+
diff: '',
|
|
122
|
+
proposal: {
|
|
123
|
+
schema: PROPOSAL_PAYLOAD_SCHEMA,
|
|
124
|
+
kind: PROPOSAL_PAYLOAD_KIND,
|
|
125
|
+
adapter: { id: PROPOSAL_ADAPTER_ID, operationId: item.adapterOperationId },
|
|
126
|
+
identity: { workspaceId: item.workspaceId, repoId: item.repoId, nodeId: item.nodeId },
|
|
127
|
+
sourcePath: route.sourcePath,
|
|
128
|
+
editId: item.editId,
|
|
129
|
+
idempotencyKey: item.idempotencyKey,
|
|
130
|
+
origin: { scopeId: item.scopeId, generationId: item.generationId },
|
|
131
|
+
change,
|
|
132
|
+
references: {
|
|
133
|
+
observed: { digest: item.observed.digest, byteLength: item.observed.byteLength, recoveryRef: item.observed.recoveryRef },
|
|
134
|
+
baseSourceDigest: item.baseSourceDigest,
|
|
135
|
+
...(item.publishedNoteDigest ? { publishedNoteDigest: item.publishedNoteDigest } : {}),
|
|
136
|
+
},
|
|
137
|
+
summary,
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The decisions the oracles of test/obsidian-proposals.test.mjs are sensitive
|
|
143
|
+
// to. Production always uses these; the tests substitute deliberately broken
|
|
144
|
+
// ones through createProposalAdapterForOracleTests to prove each oracle can fail.
|
|
145
|
+
export const PROPOSAL_ADAPTER_PRIMITIVES = Object.freeze({
|
|
146
|
+
// The proposals of the persisted store that are this operation: the ones whose payload carries its identity.
|
|
147
|
+
// Never the identifier of a proposal, which is seeded with the time it was made.
|
|
148
|
+
existingFor: ({ proposals, item }) => proposals.filter((record) => record?.payload?.adapter?.id === PROPOSAL_ADAPTER_ID && record.payload.adapter.operationId === item.adapterOperationId),
|
|
149
|
+
// The store is asked before anything is created, on the first attempt and on every one after it.
|
|
150
|
+
lookBeforeCreate: true,
|
|
151
|
+
// An operation that is acknowledged or refused is not looked at again, and one that waits is looked at when it is due.
|
|
152
|
+
handOver: ({ head, nowMs, bounds }) => isDue(head, { nowMs, bounds }),
|
|
153
|
+
isSettled: ({ head }) => head.state === 'acknowledged' || head.state === 'refused',
|
|
154
|
+
content: buildProposalContent,
|
|
155
|
+
// The check-then-create of one repository is serialized between ticks and between processes by a private lock.
|
|
156
|
+
acquireLock: acquirePrivateGenerationLock,
|
|
157
|
+
// What the adapter does to a ledger that has no room: nothing.
|
|
158
|
+
relieve: () => {},
|
|
159
|
+
// What the adapter does when it sees how a reviewer settled a proposal: nothing. It is shown, and that is all.
|
|
160
|
+
onReviewStatus: () => {},
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
export function createProposalAdapterForOracleTests(primitives = PROPOSAL_ADAPTER_PRIMITIVES) {
|
|
164
|
+
const rules = { ...PROPOSAL_ADAPTER_PRIMITIVES, ...primitives }
|
|
165
|
+
|
|
166
|
+
// `clock` is injected. `crash(step, detail)` is the crash seam. `openStore` opens the existing proposal store of a
|
|
167
|
+
// repository root; `objectStore` the arbitration store; `isVisible(identity, context)` and `isGitIgnored` replace the
|
|
168
|
+
// two questions asked of the machine; `proveAbandoned` is the proof a held lock is passed over with.
|
|
169
|
+
return function createProposalAdapter(options = {}) {
|
|
170
|
+
const {
|
|
171
|
+
crash = () => {}, bounds: boundsInput = {}, limits = PROPOSAL_LEDGER_LIMITS, eligibility = DEFAULT_ELIGIBILITY,
|
|
172
|
+
openStore = createProposalStore, objectStore = openObjectStore, isGitIgnored = SOURCE_APPLY_PRIMITIVES.isGitIgnored, proveAbandoned = createAbandonmentProof(), queueCrash = () => {},
|
|
173
|
+
} = options
|
|
174
|
+
const bounds = { ...PROPOSAL_BACKPRESSURE, ...boundsInput }
|
|
175
|
+
// The environment git is asked under: the one given when the adapter was made, else the one of the engine that calls it.
|
|
176
|
+
const env = options.env
|
|
177
|
+
// The clock given when the adapter was made; else the one of the engine that calls it; else the time of day.
|
|
178
|
+
let callerClock = null
|
|
179
|
+
const clock = () => (options.clock ?? callerClock ?? (() => new Date()))()
|
|
180
|
+
const seams = { ...createProductionSeams(), ...(options.seams ?? {}) }
|
|
181
|
+
let cursor = 0
|
|
182
|
+
const queues = new Map()
|
|
183
|
+
// The observation of a tick keeps what it recorded and what it could not; one per workspace, like the queue.
|
|
184
|
+
let observation = { key: null, run: null }
|
|
185
|
+
// Edits whose operation is known not to be a proposal. A cache: losing it costs one read of an object.
|
|
186
|
+
const notProposals = new Set()
|
|
187
|
+
|
|
188
|
+
// -- the workspace of one call -------------------------------------------
|
|
189
|
+
|
|
190
|
+
function open(context) {
|
|
191
|
+
const { project, workspaceRoot, workspaceId, repositoryRoots } = context
|
|
192
|
+
if (typeof workspaceRoot !== 'string' || !path.isAbsolute(workspaceRoot) || !isIdentifier(workspaceId) || !Array.isArray(repositoryRoots) || project === null || typeof project !== 'object') {
|
|
193
|
+
throw new TypeError('the proposal adapter needs the project, the private workspace root, the workspace identity and the repository roots')
|
|
194
|
+
}
|
|
195
|
+
callerClock = typeof context.clock === 'function' ? context.clock : null
|
|
196
|
+
const stores = new Map()
|
|
197
|
+
const recoveryOf = (scopeId) => {
|
|
198
|
+
if (!stores.has(scopeId)) stores.set(scopeId, seams.createRecoveryStore({ workspaceRoot, workspaceId, scopeId, repositoryRoots }))
|
|
199
|
+
return stores.get(scopeId)
|
|
200
|
+
}
|
|
201
|
+
// The canonical graph as it is now and the corpus profile of this machine, built once per call. Throws typed.
|
|
202
|
+
let corpus = null
|
|
203
|
+
const currentCorpus = () => (corpus ??= {
|
|
204
|
+
graph: seams.buildGraph({ project, eligibility }),
|
|
205
|
+
profile: seams.profileFor({ project, workspaceId, audienceAllow: readMachineSettings({ workspaceRoot, workspaceId })?.audienceAllow ?? [] }),
|
|
206
|
+
})
|
|
207
|
+
// true, false, or null when the canonical graph cannot be read now. The rule is the one source apply asks.
|
|
208
|
+
const visible = options.isVisible ? (identity) => options.isVisible(identity, context) : ({ repoId, nodeId }) => {
|
|
209
|
+
let built
|
|
210
|
+
try { built = currentCorpus() } catch (error) { if (isTyped(error)) return null; throw error }
|
|
211
|
+
if (!built.graph.nodes.some((node) => node.repo === repoId && node.id === nodeId)) return false
|
|
212
|
+
const decision = decideApply({ request: { mode: 'manual', editId: 'edit-route' }, workspace: { workspaceRoot, workspaceId }, graph: built.graph, profile: built.profile, object: { repoId, nodeId }, editClass: 'body-replacement' })
|
|
213
|
+
return decision.allowed === true || decision.code !== 'object-not-visible'
|
|
214
|
+
}
|
|
215
|
+
let objects = null
|
|
216
|
+
// Records are immutable, so the queue of a workspace, and what it has verified, is kept from one tick to the next.
|
|
217
|
+
const queueKey = JSON.stringify([workspaceRoot, workspaceId, repositoryRoots])
|
|
218
|
+
const queue = () => {
|
|
219
|
+
if (queues.get(queueKey) === undefined) { queues.clear(); queues.set(queueKey, openProposalQueue({ stateRoot: workspaceRoot, workspaceId, repositoryRoots, clock, crash: queueCrash })) }
|
|
220
|
+
return queues.get(queueKey)
|
|
221
|
+
}
|
|
222
|
+
if (observation.key !== queueKey) observation = { key: queueKey, run: createTickObservation({ seams, isGitIgnored, bounds }) }
|
|
223
|
+
return {
|
|
224
|
+
...context, recoveryOf, visible, clock, env: env ?? context.env ?? process.env,
|
|
225
|
+
corpus: currentCorpus,
|
|
226
|
+
objects: () => (objects ??= objectStore({ stateRoot: workspaceRoot, workspaceId, repositoryRoots, clock })),
|
|
227
|
+
queue,
|
|
228
|
+
managedRoots: () => [workspaceRoot, ...[...stores.values()].map((store) => store.vaultRoot)],
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// A pending edit as the operation the object store recorded for it, or null when that is not a proposed
|
|
233
|
+
// structural edit (or is not recorded yet). The live note is never read: only bytes that were preserved.
|
|
234
|
+
function describe(workspace, edit) {
|
|
235
|
+
const { workspaceId, repoId, nodeId } = edit.identity
|
|
236
|
+
const entry = recordedOperationOf(workspace.objects(), edit)
|
|
237
|
+
if (!entry) return null
|
|
238
|
+
if (entry.kind !== 'semantic-proposal' || entry.state !== 'proposed') { notProposals.add(edit.editId); return null }
|
|
239
|
+
const origin = entry.origins.find((item) => item.editId === edit.editId && item.scopeId === edit.scopeId)
|
|
240
|
+
const item = {
|
|
241
|
+
workspaceId, repoId, nodeId, editId: edit.editId, idempotencyKey: entry.idempotencyKey, scopeId: origin.scopeId, generationId: origin.generationId,
|
|
242
|
+
baseSourceDigest: entry.baseSourceDigest, observed: { digest: entry.observedDigest, byteLength: entry.observedByteLength, recoveryRef: origin.recoveryRef },
|
|
243
|
+
adapterOperationId: adapterOperationId({ workspaceId, repoId, nodeId, idempotencyKey: entry.idempotencyKey }), sourcePath: null, publishedNoteDigest: null, change: { code: 'unclassified', reason: 'manifest-unavailable' },
|
|
244
|
+
}
|
|
245
|
+
const manifest = manifestOf(workspace, origin.scopeId, origin.generationId)
|
|
246
|
+
const noteEntry = manifest?.notes.find((note) => note.repoId === repoId && note.nodeId === nodeId && note.path === edit.path)
|
|
247
|
+
if (!noteEntry) return item
|
|
248
|
+
const recordedPath = noteEntry.ext?.[EXT]?.source?.path
|
|
249
|
+
item.sourcePath = typeof recordedPath === 'string' ? recordedPath : null
|
|
250
|
+
item.publishedNoteDigest = noteEntry.noteDigest ?? null
|
|
251
|
+
const store = workspace.recoveryOf(origin.scopeId)
|
|
252
|
+
const [publishedNoteBytes, editedNoteBytes, baseSourceBytes] = [retained(store, noteEntry.noteDigest), retained(store, entry.observedDigest), retained(store, entry.baseSourceDigest)]
|
|
253
|
+
if (!publishedNoteBytes || !editedNoteBytes || !baseSourceBytes) { item.change = { code: 'unclassified', reason: 'preserved-bytes-unavailable' }; return item }
|
|
254
|
+
let outcome
|
|
255
|
+
try { outcome = applyEditLens({ manifest, repoId, nodeId, publishedNoteBytes, editedNoteBytes, baseSourceBytes }) } catch (error) { if (!isTyped(error)) throw error; outcome = { kind: 'refusal', code: error.code } }
|
|
256
|
+
item.change = describeChange(outcome)
|
|
257
|
+
return item
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// -- one operation, under the lock of its repository ----------------------
|
|
261
|
+
|
|
262
|
+
function receiptOf({ item, storeId, sequence, proposalId, dedupe, backpressure }) {
|
|
263
|
+
const receipt = {
|
|
264
|
+
schema: 'atelier-obsidian-proposal-receipt/v1', contractVersion: '1.0.0', receiptId: `pr-${hashOf([item.adapterOperationId, String(sequence)]).slice(0, 48)}`,
|
|
265
|
+
repoId: item.repoId, storeId, adapterOperationId: item.adapterOperationId, editId: item.editId, proposalId, dedupe, backpressure, recordedAt: isoTime(clock),
|
|
266
|
+
}
|
|
267
|
+
assertObsidianContract('proposal-receipt', receipt)
|
|
268
|
+
return receipt
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function settleOne(workspace, item, { requireOpenEdit = true, editState = 'open' } = {}) {
|
|
272
|
+
const queue = workspace.queue()
|
|
273
|
+
const { repoId, adapterOperationId: operationId } = item
|
|
274
|
+
const storeId = proposalStoreId(item)
|
|
275
|
+
const step = (name) => crash(name, { adapterOperationId: operationId, repoId })
|
|
276
|
+
const outcome = (status, record, extra = {}) => ({ status, code: record?.code ?? null, editId: item.editId, repoId, nodeId: item.nodeId, adapterOperationId: operationId, state: record?.state ?? null, proposalId: record?.proposalId ?? null, ...extra })
|
|
277
|
+
let history = queue.read(repoId, operationId)
|
|
278
|
+
const known = history !== null
|
|
279
|
+
const sequenceNext = () => (history?.records.length ?? 0) + 1
|
|
280
|
+
const write = (fields) => { const record = queue.append(repoId, operationId, fields); history = queue.read(repoId, operationId); return record }
|
|
281
|
+
const refused = (code) => {
|
|
282
|
+
const receipt = receiptOf({ item, storeId, sequence: sequenceNext(), proposalId: null, dedupe: known ? 'duplicate' : 'new', backpressure: 'refused' })
|
|
283
|
+
return outcome('refused', write({ ...identityFields(item, storeId), state: 'refused', code, proposalId: null, dedupe: null, nextAttemptAt: null, receipt }), { receipt })
|
|
284
|
+
}
|
|
285
|
+
const deferred = (code, attempts) => {
|
|
286
|
+
const record = write({ ...identityFields(item, storeId), state: 'backpressure', code, attempts, lastAttemptAt: isoTime(clock), nextAttemptAt: nextAttemptAt({ attempts, nowMs: Date.parse(isoTime(clock)), bounds }), proposalId: null, dedupe: null, receipt: null })
|
|
287
|
+
return outcome('deferred', record, { receipt: receiptOf({ item, storeId, sequence: record.sequence, proposalId: null, dedupe: known ? 'duplicate' : 'new', backpressure: 'deferred' }), exhausted: isExhausted(record, bounds) })
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (history && rules.isSettled({ head: history.head })) {
|
|
291
|
+
// Offered again: answered from the record. Nothing is read from the store and nothing is written.
|
|
292
|
+
const { head } = history
|
|
293
|
+
return outcome('duplicate', head, { receipt: { ...head.receipt, dedupe: 'duplicate' } })
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let proposalId = history?.head.state === 'submitted' ? history.head.proposalId : null
|
|
297
|
+
let dedupe = history?.head.state === 'submitted' ? history.head.dedupe : null
|
|
298
|
+
let attempts = history?.head.attempts ?? 0
|
|
299
|
+
let store = null
|
|
300
|
+
let route = null
|
|
301
|
+
|
|
302
|
+
if (proposalId === null) {
|
|
303
|
+
// A person took the edit back, or made another over it: the request is theirs to withdraw.
|
|
304
|
+
if (requireOpenEdit && editState !== 'open') return known ? refused(`edit-${editState}`) : outcome('skipped', null, { code: `edit-${editState}` })
|
|
305
|
+
// The object store no longer says this is a proposed structural edit, so there is nothing to describe.
|
|
306
|
+
if (item.observed === null) return refused('operation-not-proposed')
|
|
307
|
+
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
|
+
if (!resolved.ok && !resolved.transient) return refused(resolved.code)
|
|
309
|
+
// Recorded before anything is asked of the store.
|
|
310
|
+
if (!known) write({ ...identityFields(item, storeId), state: 'queued', code: null, proposalId: null, dedupe: null, receipt: null, nextAttemptAt: null })
|
|
311
|
+
step('queued')
|
|
312
|
+
if (!resolved.ok) return deferred(resolved.code, attempts + 1)
|
|
313
|
+
route = resolved.route
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
attempts += 1
|
|
317
|
+
const repositoryRoot = route?.repositoryRoot ?? repositoryRootOf(workspace.project, repoId)
|
|
318
|
+
if (repositoryRoot === null) return proposalId === null ? refused('repository-not-enrolled') : outcome('deferred', history.head, { code: 'repository-not-enrolled' })
|
|
319
|
+
try { store = openStore({ workspaceRoot: repositoryRoot, workspaceId: workspace.workspaceId }) } catch { return proposalId === null ? deferred('store-unavailable', attempts) : outcome('deferred', history.head, { code: 'store-unavailable' }) }
|
|
320
|
+
|
|
321
|
+
if (proposalId === null) {
|
|
322
|
+
const ledger = classifyLedgerRead(store.eventLedger.readAll(), limits)
|
|
323
|
+
if (ledger.state === 'corrupt') return refused(ledger.code)
|
|
324
|
+
if (ledger.state === 'full') { rules.relieve({ store, ledger }); return deferred(ledger.code, attempts) }
|
|
325
|
+
if (ledger.state !== 'readable') return deferred(ledger.code, attempts)
|
|
326
|
+
|
|
327
|
+
let existing = []
|
|
328
|
+
if (rules.lookBeforeCreate) {
|
|
329
|
+
const listed = store.listProposals()
|
|
330
|
+
if (!listed.ok) { const read = classifyLedgerRead(listed, limits); return read.state === 'corrupt' ? refused(read.code) : deferred(read.code, attempts) }
|
|
331
|
+
existing = rules.existingFor({ proposals: listed.proposals, item }).sort((left, right) => compareText(left.proposal.createdAt, right.proposal.createdAt) || compareText(left.proposal.id, right.proposal.id))
|
|
332
|
+
}
|
|
333
|
+
if (existing.length > 0) {
|
|
334
|
+
proposalId = existing[0].proposal.id
|
|
335
|
+
dedupe = 'recovered'
|
|
336
|
+
} else {
|
|
337
|
+
const body = rules.content({ item, route, change: item.change })
|
|
338
|
+
const fits = preflightAppend({ ledger, lineBytes: estimateEventLineBytes({ body, workspaceId: workspace.workspaceId, marginBytes: bounds.lineMarginBytes }), limits, bounds })
|
|
339
|
+
if (!fits.ok) {
|
|
340
|
+
if (fits.code === 'proposal-too-large') return refused(fits.code)
|
|
341
|
+
rules.relieve({ store, ledger })
|
|
342
|
+
return deferred(fits.code, attempts)
|
|
343
|
+
}
|
|
344
|
+
step('before-append')
|
|
345
|
+
const created = store.createProposal(body)
|
|
346
|
+
if (!created.ok) {
|
|
347
|
+
const code = classifyStoreRefusal(created.status)
|
|
348
|
+
return code === 'store-refused' || code === 'ledger-corrupt' ? refused(code) : deferred(code, attempts)
|
|
349
|
+
}
|
|
350
|
+
step('appended')
|
|
351
|
+
// What went in is what is there: the identity in the payload and the path, byte for byte.
|
|
352
|
+
if (created.record?.payload?.adapter?.operationId !== operationId || created.record?.proposal?.path !== route.sourcePath) return refused('identity-not-preserved')
|
|
353
|
+
proposalId = created.record.proposal.id
|
|
354
|
+
dedupe = 'new'
|
|
355
|
+
}
|
|
356
|
+
write({ ...identityFields(item, storeId), state: 'submitted', code: null, attempts, lastAttemptAt: isoTime(clock), nextAttemptAt: null, proposalId, dedupe, receipt: null })
|
|
357
|
+
step('submitted')
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Read back from the persisted store before it is acknowledged.
|
|
361
|
+
const read = store.readProposal(proposalId)
|
|
362
|
+
if (!read.ok || read.record?.payload?.adapter?.operationId !== operationId) {
|
|
363
|
+
if (read.status === 404 || read.ok) { write({ ...identityFields(item, storeId), state: 'queued', code: 'proposal-not-found', proposalId: null, dedupe: null, receipt: null, nextAttemptAt: null }); return outcome('deferred', history.head) }
|
|
364
|
+
const record = write({ state: 'submitted', code: classifyLedgerRead(read, limits).code, attempts, lastAttemptAt: isoTime(clock), nextAttemptAt: nextAttemptAt({ attempts, nowMs: Date.parse(isoTime(clock)), bounds }) })
|
|
365
|
+
return outcome('deferred', record)
|
|
366
|
+
}
|
|
367
|
+
const receipt = receiptOf({ item, storeId, sequence: sequenceNext(), proposalId, dedupe, backpressure: 'accepted' })
|
|
368
|
+
const record = write({ state: 'acknowledged', code: null, nextAttemptAt: null, receipt })
|
|
369
|
+
step('acknowledged')
|
|
370
|
+
return outcome('acknowledged', record, { receipt, dedupe })
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const identityFields = (item, storeId) => ({
|
|
374
|
+
nodeId: item.nodeId, editId: item.editId, idempotencyKey: item.idempotencyKey, scopeId: item.scopeId, generationId: item.generationId, storeId,
|
|
375
|
+
sourcePath: isRoutableSourcePath(item.sourcePath) ? item.sourcePath : null,
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
function repositoryRootOf(project, repoId) {
|
|
379
|
+
const named = (project?.repos ?? []).filter((item) => item?.name === repoId && !item.external && typeof item.path === 'string')
|
|
380
|
+
if (named.length !== 1) return null
|
|
381
|
+
try { return fs.realpathSync.native(named[0].path) } catch { return null }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function withRepositoryLock(workspace, repoId, operation) {
|
|
385
|
+
const queue = workspace.queue()
|
|
386
|
+
queue.ensureRepository(repoId)
|
|
387
|
+
let lock
|
|
388
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
389
|
+
try { lock = await rules.acquireLock({ workspaceRoot: queue.stateRoot, directory: queue.lockDirectory(repoId), workspaceId: workspace.workspaceId, purpose: PROPOSAL_LOCK_PURPOSE, clock, proveAbandoned }); break } catch (error) {
|
|
390
|
+
if (error.code !== 'EEXIST' || attempt >= 8) throw error
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (!lock.acquired) return { locked: false, reason: lock.reason }
|
|
394
|
+
let crashed = false
|
|
395
|
+
try { return { locked: true, value: await operation() } } catch (error) { crashed = error?.crashSeam === true; throw error } finally {
|
|
396
|
+
// A process that died released nothing; the test that stands for it says so.
|
|
397
|
+
if (!crashed) lock.release()
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const itemOfHead = (head, edit) => ({
|
|
402
|
+
workspaceId: head.workspaceId, repoId: head.repoId, nodeId: head.nodeId, editId: head.editId, idempotencyKey: head.idempotencyKey, scopeId: head.scopeId, generationId: head.generationId,
|
|
403
|
+
adapterOperationId: head.adapterOperationId, sourcePath: head.sourcePath, baseSourceDigest: null, observed: null, publishedNoteDigest: null, change: { code: 'unclassified', reason: null }, edit,
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
// One repository: its lock, then each of its operations in turn. What goes wrong with one repository, or with one
|
|
407
|
+
// operation, is reported by its code and stops nothing else.
|
|
408
|
+
async function settleRepository(workspace, repoId, works, report) {
|
|
409
|
+
try {
|
|
410
|
+
const held = await withRepositoryLock(workspace, repoId, () => {
|
|
411
|
+
for (const work of works) {
|
|
412
|
+
try { report.outcomes.push(settleOne(workspace, work.item, work)) } catch (error) {
|
|
413
|
+
if (error?.crashSeam === true || !isTyped(error)) throw error
|
|
414
|
+
report.outcomes.push({ status: 'failed', code: error.code, editId: work.item.editId, repoId, nodeId: work.item.nodeId, adapterOperationId: work.item.adapterOperationId, state: null, proposalId: null })
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
if (!held.locked) report.repositories.push({ repoId, code: 'adapter-lock-held', reason: held.reason })
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (error?.crashSeam === true || !isTyped(error)) throw error
|
|
421
|
+
report.repositories.push({ repoId, code: error.code })
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
id: PROPOSAL_ADAPTER_ID,
|
|
427
|
+
|
|
428
|
+
// The observation of a tick, before `propose`: every open pending edit the object store does not know yet is
|
|
429
|
+
// observed from its preserved bytes against the source as it is now and recorded as what it is, a bounded number
|
|
430
|
+
// per tick and never twice. `retryRefused` offers once more the edits whose observation refused before anything
|
|
431
|
+
// could be recorded. Writes no source and no vault. Returns codes and identifiers only.
|
|
432
|
+
async observe(context, { retryRefused = false } = {}) {
|
|
433
|
+
const workspace = open(context)
|
|
434
|
+
const edits = Array.isArray(context.edits) ? context.edits : []
|
|
435
|
+
return { adapterId: PROPOSAL_ADAPTER_ID, observed: observation.run(workspace, { edits, retryRefused }) }
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
// The tick. `context` is { project, workspaceRoot, workspaceId, repositoryRoots, edits }: the pending edit
|
|
439
|
+
// records of the engine, open and closed. Returns codes and identifiers only.
|
|
440
|
+
async propose(context) {
|
|
441
|
+
const workspace = open(context)
|
|
442
|
+
const nowMs = Date.parse(isoTime(clock))
|
|
443
|
+
const report = { adapterId: PROPOSAL_ADAPTER_ID, examined: 0, outcomes: [], repositories: [] }
|
|
444
|
+
const edits = Array.isArray(context.edits) ? context.edits : []
|
|
445
|
+
const { operations: heads, unreadable } = workspace.queue().heads()
|
|
446
|
+
for (const item of unreadable) report.repositories.push({ repoId: null, code: item.code })
|
|
447
|
+
const byEdit = new Map(edits.map((edit) => [edit.editId, edit]))
|
|
448
|
+
const knownEdits = new Set(heads.map((head) => head.editId))
|
|
449
|
+
const works = new Map()
|
|
450
|
+
const add = (repoId, work) => { if (!works.has(repoId)) works.set(repoId, []); if (works.get(repoId).length < bounds.maxBatchPerRepository) works.get(repoId).push(work) }
|
|
451
|
+
|
|
452
|
+
// Operations the queue already knows, when they are due.
|
|
453
|
+
for (const head of [...heads].sort((left, right) => compareText(left.recordedAt, right.recordedAt) || compareText(left.adapterOperationId, right.adapterOperationId))) {
|
|
454
|
+
if (!rules.handOver({ head, nowMs, bounds })) continue
|
|
455
|
+
const edit = byEdit.get(head.editId) ?? null
|
|
456
|
+
// An operation whose edit this caller did not hand over is not this caller's to decide: it waits as it is.
|
|
457
|
+
if (edit === null && head.state !== 'submitted') continue
|
|
458
|
+
const editState = edit === null || edit.closedAt === null ? 'open' : ['withdrawn', 'superseded'].includes(edit.state) ? edit.state : 'closed'
|
|
459
|
+
let item = itemOfHead(head, edit)
|
|
460
|
+
if (edit !== null && head.state !== 'submitted') {
|
|
461
|
+
try { item = { ...item, ...(describe(workspace, edit) ?? {}) } } catch (error) { if (!isTyped(error)) throw error; report.outcomes.push({ status: 'failed', code: error.code, editId: head.editId, repoId: head.repoId, nodeId: head.nodeId, adapterOperationId: head.adapterOperationId, state: head.state, proposalId: null }); continue }
|
|
462
|
+
}
|
|
463
|
+
add(head.repoId, { item, editState })
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Pending edits the queue does not know: a bounded number per tick, starting where the last tick stopped.
|
|
467
|
+
const fresh = edits.filter((edit) => edit.closedAt === null && !knownEdits.has(edit.editId) && !notProposals.has(edit.editId)).sort((left, right) => compareText(left.observedAt, right.observedAt) || compareText(left.editId, right.editId))
|
|
468
|
+
const start = fresh.length === 0 ? 0 : cursor % fresh.length
|
|
469
|
+
const looked = [...fresh.slice(start), ...fresh.slice(0, start)].slice(0, bounds.maxExaminedPerTick)
|
|
470
|
+
cursor = fresh.length > bounds.maxExaminedPerTick ? start + looked.length : 0
|
|
471
|
+
for (const edit of looked) {
|
|
472
|
+
report.examined += 1
|
|
473
|
+
let item
|
|
474
|
+
try { item = describe(workspace, edit) } catch (error) { if (!isTyped(error)) throw error; report.outcomes.push({ status: 'failed', code: error.code, editId: edit.editId, repoId: edit.identity.repoId, nodeId: edit.identity.nodeId, adapterOperationId: null, state: null, proposalId: null }); continue }
|
|
475
|
+
if (item !== null) add(item.repoId, { item, editState: 'open' })
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
for (const [repoId, list] of [...works].sort(([left], [right]) => compareText(left, right))) await settleRepository(workspace, repoId, list, report)
|
|
479
|
+
return report
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
// One edit operation document, offered directly: what a caller that already holds the operation uses, and what
|
|
483
|
+
// the tests of routing and identity use. `sourcePath` is the repository-relative path the manifest records.
|
|
484
|
+
async offer(context, { operation, sourcePath, change = { code: 'unclassified', reason: null }, publishedNoteDigest = null }) {
|
|
485
|
+
assertObsidianContract('edit-operation', operation)
|
|
486
|
+
if (operation.kind !== 'semantic-proposal' || operation.state !== 'proposed') return { status: 'refused', code: 'invalid-operation', editId: operation.editId, repoId: operation.repoId, nodeId: operation.nodeId, adapterOperationId: null, state: null, proposalId: null }
|
|
487
|
+
const workspace = open(context)
|
|
488
|
+
const item = {
|
|
489
|
+
workspaceId: operation.workspaceId, repoId: operation.repoId, nodeId: operation.nodeId, editId: operation.editId, idempotencyKey: operation.idempotencyKey, scopeId: operation.origin.scopeId, generationId: operation.origin.generationId,
|
|
490
|
+
baseSourceDigest: operation.baseSourceDigest, observed: { ...operation.observed }, adapterOperationId: adapterOperationId(operation), sourcePath, publishedNoteDigest, change,
|
|
491
|
+
}
|
|
492
|
+
if (item.workspaceId !== workspace.workspaceId) return { status: 'refused', code: 'foreign-workspace', editId: item.editId, repoId: item.repoId, nodeId: item.nodeId, adapterOperationId: item.adapterOperationId, state: null, proposalId: null }
|
|
493
|
+
const report = { outcomes: [], repositories: [] }
|
|
494
|
+
await settleRepository(workspace, item.repoId, [{ item, requireOpenEdit: false }], report)
|
|
495
|
+
return report.outcomes[0] ?? { status: 'deferred', code: report.repositories[0]?.code ?? 'adapter-lock-held', editId: item.editId, repoId: item.repoId, nodeId: item.nodeId, adapterOperationId: item.adapterOperationId, state: null, proposalId: null }
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
// A refused operation, or one whose retries are spent, asked for again by a person or an agent acting for them.
|
|
499
|
+
async requeue(context, { repoId, adapterOperationId: operationId }) {
|
|
500
|
+
const workspace = open(context)
|
|
501
|
+
if (!isIdentifier(repoId) || !isAdapterOperationId(operationId)) return { requeued: false, code: 'invalid-operation' }
|
|
502
|
+
const held = await withRepositoryLock(workspace, repoId, () => {
|
|
503
|
+
const history = workspace.queue().read(repoId, operationId)
|
|
504
|
+
if (history === null) return { requeued: false, code: 'unknown-operation' }
|
|
505
|
+
if (history.head.state !== 'refused' && !isExhausted(history.head, bounds)) return { requeued: false, code: 'operation-not-refused', state: history.head.state }
|
|
506
|
+
const record = workspace.queue().append(repoId, operationId, { state: 'queued', code: null, attempts: 0, lastAttemptAt: null, nextAttemptAt: null, proposalId: null, dedupe: null, receipt: null })
|
|
507
|
+
return { requeued: true, state: record.state, sequence: record.sequence }
|
|
508
|
+
})
|
|
509
|
+
return held.locked ? held.value : { requeued: false, code: 'adapter-lock-held' }
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
// Read-only. Per repository: how many operations are in each state, the codes of the refused and the waiting,
|
|
513
|
+
// and how much room the ledger of its store has. No note text, no source text, no path of this machine.
|
|
514
|
+
status(context) {
|
|
515
|
+
const workspace = open(context)
|
|
516
|
+
const { operations, unreadable } = workspace.queue().heads()
|
|
517
|
+
const enrolled = (workspace.project.repos ?? []).filter((repo) => !repo.external && typeof repo.path === 'string' && isIdentifier(repo.name)).map((repo) => repo.name)
|
|
518
|
+
const repositories = [...new Set([...enrolled, ...operations.map((record) => record.repoId)])].sort(compareText).map((repoId) => {
|
|
519
|
+
const own = operations.filter((record) => record.repoId === repoId)
|
|
520
|
+
const count = (state) => own.filter((record) => record.state === state).length
|
|
521
|
+
const codes = (state) => Object.fromEntries([...new Set(own.filter((record) => record.state === state).map((record) => record.code))].sort(compareText).map((code) => [code, own.filter((record) => record.state === state && record.code === code).length]))
|
|
522
|
+
return {
|
|
523
|
+
repoId, enrolled: enrolled.includes(repoId), storeId: proposalStoreId({ workspaceId: workspace.workspaceId, repoId }),
|
|
524
|
+
queued: count('queued'), submitted: count('submitted'), acknowledged: count('acknowledged'), backpressure: count('backpressure'), refused: count('refused'),
|
|
525
|
+
exhausted: own.filter((record) => isExhausted(record, bounds)).length, backpressureCodes: codes('backpressure'), refusedCodes: codes('refused'),
|
|
526
|
+
ledger: ledgerOf(workspace, repoId),
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
return { schema: 'atelier-obsidian-proposal-status/v1', workspaceId: workspace.workspaceId, adapterId: PROPOSAL_ADAPTER_ID, limits: { ...limits }, reserve: { events: bounds.reserveEvents, bytes: bounds.reserveBytes }, repositories, unreadable: unreadable.map(({ adapterOperationId: operationId, code }) => ({ adapterOperationId: operationId, code })) }
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
// Every operation of the queue: identifiers, states, codes and times.
|
|
533
|
+
list(context) {
|
|
534
|
+
const { operations } = open(context).queue().heads()
|
|
535
|
+
return operations.sort((left, right) => compareText(left.repoId, right.repoId) || compareText(left.recordedAt, right.recordedAt) || compareText(left.adapterOperationId, right.adapterOperationId))
|
|
536
|
+
.map(({ adapterOperationId: operationId, repoId, nodeId, editId, state, code, attempts, nextAttemptAt: next, proposalId, recordedAt }) => ({ adapterOperationId: operationId, repoId, nodeId, editId, state, code, attempts, nextAttemptAt: next, exhausted: state === 'backpressure' && attempts >= bounds.maxAttempts, proposalId, recordedAt }))
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
// One operation: its record, its receipt, and how the reviewers of the repository have settled its proposal so
|
|
540
|
+
// far. That last part is shown and nothing more: no status of a proposal makes this adapter do anything.
|
|
541
|
+
show(context, { adapterOperationId: operationId }) {
|
|
542
|
+
const workspace = open(context)
|
|
543
|
+
const head = workspace.queue().heads().operations.find((record) => record.adapterOperationId === operationId)
|
|
544
|
+
if (!head) return null
|
|
545
|
+
let review = null
|
|
546
|
+
if (head.proposalId !== null) {
|
|
547
|
+
const repositoryRoot = repositoryRootOf(workspace.project, head.repoId)
|
|
548
|
+
if (repositoryRoot !== null && fs.existsSync(path.join(repositoryRoot, PROPOSAL_STORE_DIRECTORY))) {
|
|
549
|
+
const read = openStore({ workspaceRoot: repositoryRoot, workspaceId: workspace.workspaceId }).readProposal(head.proposalId)
|
|
550
|
+
review = read.ok ? { status: read.record.proposal.status, updatedAt: read.record.proposal.updatedAt } : { status: 'unreadable', updatedAt: null }
|
|
551
|
+
if (read.ok) rules.onReviewStatus({ status: read.record.proposal.status, record: read.record, head, repositoryRoot })
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return { operation: head, review }
|
|
555
|
+
},
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function ledgerOf(workspace, repoId) {
|
|
559
|
+
const repositoryRoot = repositoryRootOf(workspace.project, repoId)
|
|
560
|
+
if (repositoryRoot === null) return { state: 'not-enrolled', events: null, bytes: null, headroom: null }
|
|
561
|
+
// A store nobody made yet is not made to be asked: it has all of its room.
|
|
562
|
+
if (!fs.existsSync(path.join(repositoryRoot, PROPOSAL_STORE_DIRECTORY))) return { state: 'absent', events: 0, bytes: 0, headroom: { events: limits.maxEvents, bytes: limits.maxBytes } }
|
|
563
|
+
try {
|
|
564
|
+
const { state, events, bytes, headroom } = classifyLedgerRead(openStore({ workspaceRoot: repositoryRoot, workspaceId: workspace.workspaceId }).eventLedger.readAll(), limits)
|
|
565
|
+
return { state, events, bytes, headroom }
|
|
566
|
+
} catch { return { state: 'unavailable', events: null, bytes: null, headroom: null } }
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export const createProposalAdapter = createProposalAdapterForOracleTests()
|
|
572
|
+
export { OPEN_QUEUE_STATES }
|