@mnstry/atelier 0.2.0-alpha.1 → 0.2.0-alpha.11
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 +413 -0
- package/README.md +402 -205
- 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/contracts/public-api-baseline.json +57 -0
- package/docs/assurance-controls.md +49 -0
- package/docs/atelier-runtime.md +28 -2
- package/docs/atelier-sync.md +172 -0
- package/docs/blocks/audience-visibility.md +9 -0
- package/docs/blocks/claims.md +61 -0
- package/docs/blocks/conformance-admission.md +15 -0
- package/docs/blocks/will-not-do.md +18 -0
- package/docs/coauthor-session.md +57 -0
- package/docs/connected-composition.md +75 -0
- package/docs/continuity.md +3 -4
- package/docs/decisions.md +167 -0
- package/docs/design.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 +99 -16
- 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/knowledge-graph.md +8 -4
- package/docs/local-review.md +84 -0
- package/docs/local-services.md +328 -0
- package/docs/local-state.md +107 -0
- package/docs/obsidian-contract.md +698 -0
- package/docs/obsidian.md +484 -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 +174 -12
- package/docs/repo-boundary-guard.md +12 -2
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +155 -5
- 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/projects/sample-workspace/content/source.html.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +4 -1
- package/fixtures/ui/presentation/pane.v1.json +7 -0
- package/fixtures/ui/presentation/reference.v1.json +293 -0
- package/package.json +86 -10
- package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/claude/atelier-local-service/SKILL.md +47 -0
- package/skills/claude/atelier-open/SKILL.md +11 -5
- package/skills/claude/atelier-public-boundary/SKILL.md +31 -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-local-service/SKILL.md +47 -0
- package/skills/codex/atelier-open/SKILL.md +11 -5
- package/skills/codex/atelier-public-boundary/SKILL.md +31 -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 +283 -20
- package/src/boundary/policy.mjs +213 -91
- package/src/cli/execute-command.mjs +36 -0
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +78 -10
- package/src/coauthor/session.mjs +161 -0
- package/src/coauthor/store.mjs +173 -0
- package/src/collaboration/event-ledger.mjs +348 -0
- package/src/collaboration/index.mjs +17 -0
- package/src/collaboration/inspection-bundle.mjs +223 -0
- package/src/collaboration/proposals.mjs +265 -65
- package/src/collaboration/review-contracts.mjs +25 -0
- package/src/collaboration/review-store.mjs +305 -0
- package/src/commands/attestation.mjs +20 -6
- package/src/commands/coauthor.mjs +26 -0
- package/src/commands/config.mjs +6 -1
- package/src/commands/disclosure.mjs +133 -0
- package/src/commands/distribution.mjs +2 -1
- package/src/commands/extension-pack.mjs +17 -9
- package/src/commands/init.mjs +46 -6
- package/src/commands/obsidian.mjs +371 -0
- package/src/commands/review.mjs +147 -0
- package/src/commands/server.mjs +1 -4
- 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 +206 -0
- package/src/egress/check.mjs +7 -38
- package/src/egress/forbidden-egress.mjs +32 -18
- package/src/export/atelier-export-contract.mjs +16 -1
- package/src/extension-packs/lifecycle.mjs +154 -0
- package/src/graph/graph.mjs +176 -316
- package/src/graph/knowledge-graph.mjs +643 -43
- package/src/guides/contracts.mjs +54 -0
- package/src/harness/context-client.mjs +9 -1
- package/src/index.mjs +41 -0
- package/src/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +112 -52
- package/src/project/durable-state.mjs +161 -0
- package/src/project/file-class.mjs +180 -0
- package/src/project/package-root.mjs +10 -0
- package/src/project/path-match.mjs +38 -15
- package/src/project/private-state.mjs +120 -0
- 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 +212 -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 +138 -0
- package/src/runtime/obsidian/app-production-seams.mjs +107 -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 +280 -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-entry-path.mjs +9 -0
- package/src/runtime/obsidian/service-main.mjs +93 -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 +125 -59
- package/src/server/security.mjs +89 -4
- package/src/server/server.mjs +4 -2
- package/src/skills/steward.mjs +780 -0
- package/src/support/feedback-report.mjs +4 -3
- 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 +65 -29
- 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,180 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { atomicReplacePrivateText, ensureContainedPrivateDirectory, readRegularTextNoFollow } from '../../project/private-state.mjs'
|
|
4
|
+
import { PATH_REGISTRY_SCHEMA } from '../../projection/obsidian/materialize/path-registry.mjs'
|
|
5
|
+
import { canonicalJson, closedObject, compareText } from './documents.mjs'
|
|
6
|
+
import { refuse } from './errors.mjs'
|
|
7
|
+
import { APPLY_RESULT_STATUSES } from './extension-points.mjs'
|
|
8
|
+
|
|
9
|
+
// Private, versioned documents of the maintenance engine, under
|
|
10
|
+
//
|
|
11
|
+
// <data>/obsidian/<workspace-id>/state/maintenance/
|
|
12
|
+
// freshness.json per-scope freshness, read by the opening commands
|
|
13
|
+
// pending-edits.json edits made in a vault, preserved and waiting
|
|
14
|
+
// late-writers.json writes that arrived in a displaced file after publication
|
|
15
|
+
// path-registry.json the workspace path registry prepareView grows
|
|
16
|
+
//
|
|
17
|
+
// None of the frozen contracts describes these, so each is a closed document
|
|
18
|
+
// of this module: an unknown key, state or schema refuses on read and on
|
|
19
|
+
// write. Files are owner-only and replaced atomically. A document is written
|
|
20
|
+
// only when its bytes change.
|
|
21
|
+
|
|
22
|
+
export const FRESHNESS_SCHEMA = 'atelier-obsidian-freshness/v1'
|
|
23
|
+
export const PENDING_EDITS_SCHEMA = 'atelier-obsidian-pending-edits/v1'
|
|
24
|
+
export const LATE_WRITERS_SCHEMA = 'atelier-obsidian-late-writers/v1'
|
|
25
|
+
|
|
26
|
+
export const FRESHNESS_STATES = Object.freeze(['current', 'updating', 'held-for-your-edit', 'stale', 'publisher-conflict', 'disabled'])
|
|
27
|
+
export const CHANGE_CLASSES = Object.freeze(['source-body', 'sidecar', 'asset', 'config', 'ext-settings', 'scope', 'eligibility', 'vault-note'])
|
|
28
|
+
|
|
29
|
+
// queued waiting for a person (manual) or for the next dispatch (automatic)
|
|
30
|
+
// apply-unavailable dispatched, and no apply operation exists yet
|
|
31
|
+
// apply-failed dispatched, and the operation refused, conflicted or failed
|
|
32
|
+
// retry-exhausted the retry budget is spent; still pending, no longer dispatched
|
|
33
|
+
// applied the operation reported the edit applied to source
|
|
34
|
+
// withdrawn the note is back at the bytes it was generated with
|
|
35
|
+
// superseded the note was edited again; a newer record carries the newer bytes
|
|
36
|
+
export const OPEN_EDIT_STATES = Object.freeze(['queued', 'apply-unavailable', 'apply-failed', 'retry-exhausted'])
|
|
37
|
+
export const CLOSED_EDIT_STATES = Object.freeze(['applied', 'withdrawn', 'superseded'])
|
|
38
|
+
export const EDIT_STATES = Object.freeze([...OPEN_EDIT_STATES, ...CLOSED_EDIT_STATES])
|
|
39
|
+
|
|
40
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
41
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/
|
|
42
|
+
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
|
43
|
+
const EDIT_ID = /^edit-[0-9a-f]{32}$/
|
|
44
|
+
|
|
45
|
+
const isIdentifier = (value) => typeof value === 'string' && IDENTIFIER.test(value)
|
|
46
|
+
const isDigest = (value) => typeof value === 'string' && DIGEST.test(value)
|
|
47
|
+
const isTimestamp = (value) => typeof value === 'string' && TIMESTAMP.test(value)
|
|
48
|
+
const isRelative = (value) => typeof value === 'string' && value !== '' && !value.startsWith('/') && !value.includes('\\') && !value.split('/').some((part) => part === '' || part === '.' || part === '..')
|
|
49
|
+
const isCount = (value) => Number.isInteger(value) && value >= 0
|
|
50
|
+
|
|
51
|
+
function check(condition, code, message) {
|
|
52
|
+
if (!condition) refuse(code, message)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Validators
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
export function validateFreshness(document, workspaceId) {
|
|
60
|
+
const code = 'invalid-freshness-state'
|
|
61
|
+
closedObject(document, { required: ['schema', 'workspaceId', 'enablement', 'maintenanceMode', 'lastTickAt', 'lastFullReconciliationAt', 'scopes'] }, code, 'the freshness document')
|
|
62
|
+
check(document.schema === FRESHNESS_SCHEMA, code, 'the freshness document names an unknown schema')
|
|
63
|
+
check(document.workspaceId === workspaceId, code, 'the freshness document belongs to another workspace')
|
|
64
|
+
check(['enabled', 'disabled', 'refused'].includes(document.enablement), code, 'enablement is unknown')
|
|
65
|
+
check(['manual', 'automatic'].includes(document.maintenanceMode), code, 'maintenanceMode is unknown')
|
|
66
|
+
check(isTimestamp(document.lastTickAt) && (document.lastFullReconciliationAt === null || isTimestamp(document.lastFullReconciliationAt)), code, 'a tick time is not a UTC timestamp')
|
|
67
|
+
check(Array.isArray(document.scopes), code, 'scopes must be a list')
|
|
68
|
+
const seen = new Set()
|
|
69
|
+
for (const scope of document.scopes) {
|
|
70
|
+
closedObject(scope, { required: ['scopeId', 'state', 'reason', 'generationId', 'preparedGenerationId', 'verified', 'heldNotes', 'changeClasses', 'retainedEdits', 'checkedAt'] }, code, 'a scope freshness entry')
|
|
71
|
+
check(isIdentifier(scope.scopeId) && !seen.has(scope.scopeId), code, 'a scope identity is malformed or repeated')
|
|
72
|
+
seen.add(scope.scopeId)
|
|
73
|
+
check(FRESHNESS_STATES.includes(scope.state), code, 'a scope carries an unknown freshness state')
|
|
74
|
+
check(typeof scope.reason === 'string' && scope.reason !== '' && scope.reason.length <= 160, code, 'a scope reason must be a short code')
|
|
75
|
+
check((scope.generationId === null || isIdentifier(scope.generationId)) && (scope.preparedGenerationId === null || isIdentifier(scope.preparedGenerationId)), code, 'a generation identity is malformed')
|
|
76
|
+
check(typeof scope.verified === 'boolean', code, 'verified must be a boolean')
|
|
77
|
+
check(Array.isArray(scope.heldNotes) && scope.heldNotes.every(isRelative), code, 'heldNotes must list vault-relative paths')
|
|
78
|
+
// `current` is a claim about exact generations and verified bytes; a document that claims it otherwise refuses.
|
|
79
|
+
check(scope.state !== 'current' || (scope.verified && scope.generationId !== null && scope.generationId === scope.preparedGenerationId && scope.heldNotes.length === 0), code, 'a scope claims current without an exact verified generation')
|
|
80
|
+
check(Array.isArray(scope.changeClasses) && scope.changeClasses.every((item) => CHANGE_CLASSES.includes(item)), code, 'changeClasses names an unknown class')
|
|
81
|
+
check(isCount(scope.retainedEdits) && isTimestamp(scope.checkedAt), code, 'a scope entry is malformed')
|
|
82
|
+
}
|
|
83
|
+
return document
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function validatePendingEdits(document, workspaceId) {
|
|
87
|
+
const code = 'invalid-pending-edits'
|
|
88
|
+
closedObject(document, { required: ['schema', 'workspaceId', 'edits'] }, code, 'the pending edit document')
|
|
89
|
+
check(document.schema === PENDING_EDITS_SCHEMA, code, 'the pending edit document names an unknown schema')
|
|
90
|
+
check(document.workspaceId === workspaceId, code, 'the pending edit document belongs to another workspace')
|
|
91
|
+
check(Array.isArray(document.edits), code, 'edits must be a list')
|
|
92
|
+
const seen = new Set()
|
|
93
|
+
for (const edit of document.edits) {
|
|
94
|
+
closedObject(edit, { required: ['editId', 'identity', 'scopeId', 'generationId', 'path', 'baseNoteDigest', 'observedDigest', 'objectRef', 'observedAt', 'state', 'attempts', 'retryBudget', 'lastAttemptAt', 'lastResult', 'closedAt'] }, code, 'a pending edit')
|
|
95
|
+
check(typeof edit.editId === 'string' && EDIT_ID.test(edit.editId) && !seen.has(edit.editId), code, 'an edit identity is malformed or repeated')
|
|
96
|
+
seen.add(edit.editId)
|
|
97
|
+
closedObject(edit.identity, { required: ['workspaceId', 'repoId', 'nodeId'] }, code, 'a pending edit identity')
|
|
98
|
+
check(edit.identity.workspaceId === workspaceId && isIdentifier(edit.identity.repoId) && isIdentifier(edit.identity.nodeId), code, 'a pending edit identity is malformed')
|
|
99
|
+
check(isIdentifier(edit.scopeId) && isIdentifier(edit.generationId) && isRelative(edit.path), code, 'a pending edit origin is malformed')
|
|
100
|
+
check(isDigest(edit.baseNoteDigest) && isDigest(edit.observedDigest) && edit.baseNoteDigest !== edit.observedDigest, code, 'a pending edit needs two different digests')
|
|
101
|
+
// The edited bytes are preserved before an edit is recorded, so a record without its object refuses.
|
|
102
|
+
check(edit.objectRef === `recovery/objects/${edit.observedDigest.slice('sha256:'.length)}.bin`, code, 'a pending edit must name the retained object of its observed bytes')
|
|
103
|
+
check(isTimestamp(edit.observedAt) && EDIT_STATES.includes(edit.state) && isCount(edit.attempts), code, 'a pending edit is malformed')
|
|
104
|
+
check(edit.retryBudget === null || (isCount(edit.retryBudget) && edit.retryBudget <= 16), code, 'retryBudget is out of range')
|
|
105
|
+
check(edit.lastAttemptAt === null || isTimestamp(edit.lastAttemptAt), code, 'lastAttemptAt is malformed')
|
|
106
|
+
if (edit.lastResult !== null) {
|
|
107
|
+
closedObject(edit.lastResult, { required: ['status', 'code', 'operationId', 'policyDigest'] }, code, 'a pending edit result')
|
|
108
|
+
check(APPLY_RESULT_STATUSES.includes(edit.lastResult.status) && typeof edit.lastResult.code === 'string' && typeof edit.lastResult.operationId === 'string' && isDigest(edit.lastResult.policyDigest), code, 'a pending edit result is malformed')
|
|
109
|
+
}
|
|
110
|
+
check(CLOSED_EDIT_STATES.includes(edit.state) ? isTimestamp(edit.closedAt) : edit.closedAt === null, code, 'closedAt must be set exactly when an edit is closed')
|
|
111
|
+
check(edit.state !== 'applied' || edit.lastResult?.status === 'applied', code, 'an edit is applied only when the apply operation said so')
|
|
112
|
+
}
|
|
113
|
+
return document
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function validateLateWriters(document, workspaceId) {
|
|
117
|
+
const code = 'invalid-late-writers'
|
|
118
|
+
closedObject(document, { required: ['schema', 'workspaceId', 'findings'] }, code, 'the late writer document')
|
|
119
|
+
check(document.schema === LATE_WRITERS_SCHEMA && document.workspaceId === workspaceId && Array.isArray(document.findings), code, 'the late writer document is malformed')
|
|
120
|
+
for (const finding of document.findings) {
|
|
121
|
+
closedObject(finding, { required: ['scopeId', 'journalId', 'unit', 'notePath', 'digestAtMove', 'observedDigest', 'objectRef', 'detectedAt'] }, code, 'a late writer finding')
|
|
122
|
+
check(isIdentifier(finding.scopeId) && isIdentifier(finding.journalId) && isCount(finding.unit) && (finding.notePath === null || isRelative(finding.notePath)), code, 'a late writer finding is malformed')
|
|
123
|
+
check((finding.digestAtMove === null || isDigest(finding.digestAtMove)) && isDigest(finding.observedDigest) && isRelative(finding.objectRef) && isTimestamp(finding.detectedAt), code, 'a late writer finding is malformed')
|
|
124
|
+
}
|
|
125
|
+
return document
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Store
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
export function sortEdits(edits) {
|
|
133
|
+
return [...edits].sort((left, right) => compareText(left.identity.repoId, right.identity.repoId) || compareText(left.identity.nodeId, right.identity.nodeId)
|
|
134
|
+
|| compareText(left.scopeId, right.scopeId) || compareText(left.observedAt, right.observedAt) || compareText(left.editId, right.editId))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createMaintenanceStateStore({ workspaceRoot, workspaceId }) {
|
|
138
|
+
const directory = () => ensureContainedPrivateDirectory({ workspaceRoot, directory: path.join(workspaceRoot, 'state', 'maintenance'), label: 'Obsidian maintenance state' })
|
|
139
|
+
const file = (name) => path.join(workspaceRoot, 'state', 'maintenance', name)
|
|
140
|
+
|
|
141
|
+
function read(name, validate, code) {
|
|
142
|
+
let text
|
|
143
|
+
try { text = readRegularTextNoFollow(file(name)) } catch (error) {
|
|
144
|
+
if (error.code === 'ENOENT') return null
|
|
145
|
+
refuse(code, 'a maintenance state document cannot be read', { cause: error.code ?? String(error.message) })
|
|
146
|
+
}
|
|
147
|
+
let document
|
|
148
|
+
try { document = JSON.parse(text) } catch { refuse(code, 'a maintenance state document is not JSON') }
|
|
149
|
+
return validate(document, workspaceId)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function write(name, document, validate) {
|
|
153
|
+
validate(document, workspaceId)
|
|
154
|
+
const text = canonicalJson(document)
|
|
155
|
+
let existing = null
|
|
156
|
+
try { existing = fs.readFileSync(file(name), 'utf8') } catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
157
|
+
if (existing === text) return false
|
|
158
|
+
atomicReplacePrivateText(path.join(directory(), name), text)
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const registryValidator = (document) => {
|
|
163
|
+
if (document?.schema !== PATH_REGISTRY_SCHEMA || document.workspaceId !== workspaceId || !Array.isArray(document.entries)) refuse('invalid-path-registry', 'the persisted path registry is malformed')
|
|
164
|
+
return document
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
workspaceRoot,
|
|
169
|
+
workspaceId,
|
|
170
|
+
exists: () => fs.existsSync(path.join(workspaceRoot, 'state', 'maintenance')),
|
|
171
|
+
readFreshness: () => read('freshness.json', validateFreshness, 'invalid-freshness-state'),
|
|
172
|
+
writeFreshness: (document) => write('freshness.json', { ...document, scopes: [...document.scopes].sort((left, right) => compareText(left.scopeId, right.scopeId)) }, validateFreshness),
|
|
173
|
+
readPendingEdits: () => read('pending-edits.json', validatePendingEdits, 'invalid-pending-edits') ?? { schema: PENDING_EDITS_SCHEMA, workspaceId, edits: [] },
|
|
174
|
+
writePendingEdits: (document) => write('pending-edits.json', { ...document, edits: sortEdits(document.edits) }, validatePendingEdits),
|
|
175
|
+
readLateWriters: () => read('late-writers.json', validateLateWriters, 'invalid-late-writers') ?? { schema: LATE_WRITERS_SCHEMA, workspaceId, findings: [] },
|
|
176
|
+
writeLateWriters: (document) => write('late-writers.json', document, validateLateWriters),
|
|
177
|
+
readPathRegistry: () => read('path-registry.json', registryValidator, 'invalid-path-registry'),
|
|
178
|
+
writePathRegistry: (document) => write('path-registry.json', document, registryValidator),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readObsidianEnablement } from './enablement.mjs'
|
|
2
|
+
import { ObsidianMaintenanceRefusal } from './errors.mjs'
|
|
3
|
+
import { readServiceRecord } from './service-record.mjs'
|
|
4
|
+
import { resolveServiceWorkspace } from './service.mjs'
|
|
5
|
+
import { createMaintenanceStateStore } from './state-store.mjs'
|
|
6
|
+
|
|
7
|
+
// What Git synchronization may say about Obsidian maintenance: a report, read
|
|
8
|
+
// from persisted state, and a hint. Null when the project does not declare
|
|
9
|
+
// the integration or has it switched off, so the output of `sync` is then
|
|
10
|
+
// exactly what it always was. Nothing here starts or asks a service, ticks an
|
|
11
|
+
// engine, publishes a view or writes a file.
|
|
12
|
+
export function maintenanceNoticeFor(project, { dataRoot, env = process.env, platform = process.platform } = {}) {
|
|
13
|
+
let enablement
|
|
14
|
+
try { enablement = readObsidianEnablement(project) } catch (error) {
|
|
15
|
+
if (!(error instanceof ObsidianMaintenanceRefusal)) throw error
|
|
16
|
+
return { state: 'refused', code: error.code, hint: 'atelier obsidian status' }
|
|
17
|
+
}
|
|
18
|
+
if (enablement.state !== 'enabled') return null
|
|
19
|
+
const notice = { state: 'enabled', maintainedBy: 'the Obsidian maintenance service, not by sync', hint: 'atelier obsidian status' }
|
|
20
|
+
try {
|
|
21
|
+
const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform })
|
|
22
|
+
if (!workspace?.workspaceRoot) return { ...notice, service: 'never-started', scopes: [] }
|
|
23
|
+
const freshness = createMaintenanceStateStore(workspace).readFreshness()
|
|
24
|
+
// A record is not proof that a service runs; only `obsidian service status` proves that.
|
|
25
|
+
return {
|
|
26
|
+
...notice, service: readServiceRecord(workspace) === null ? 'no-record' : 'recorded-not-verified', lastTickAt: freshness?.lastTickAt ?? null,
|
|
27
|
+
scopes: (freshness?.scopes ?? []).map(({ scopeId, state, reason, checkedAt }) => ({ scopeId, lastKnownState: state, reason, checkedAt })),
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (!(error instanceof ObsidianMaintenanceRefusal)) throw error
|
|
31
|
+
return { ...notice, service: 'unknown', code: error.code, scopes: [] }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Calls one `tick` again and again, never two at a time.
|
|
2
|
+
//
|
|
3
|
+
// The next tick is scheduled only when the previous one has settled, so ticks
|
|
4
|
+
// cannot overlap however long one takes. A tick that throws does not end the
|
|
5
|
+
// loop and does not change the next tick: the error is handed to `onOutcome`,
|
|
6
|
+
// and the delay before the next attempt doubles per consecutive failure up to
|
|
7
|
+
// a ceiling, so a disk that stays full is retried slowly instead of in a tight
|
|
8
|
+
// loop. One success returns the loop to its interval.
|
|
9
|
+
//
|
|
10
|
+
// `tickNow()` asks for a tick that starts after the request: while a tick is
|
|
11
|
+
// in flight the request is answered by one follow-up tick, shared by everyone
|
|
12
|
+
// who asked meanwhile.
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_TICK_INTERVAL_MS = 30 * 1000
|
|
15
|
+
export const DEFAULT_MAX_BACKOFF_MS = 15 * 60 * 1000
|
|
16
|
+
|
|
17
|
+
export const TICK_LOOP_PRIMITIVES = Object.freeze({
|
|
18
|
+
// Whether the loop goes on after a tick threw.
|
|
19
|
+
continuesAfterError: () => true,
|
|
20
|
+
delayAfter: ({ intervalMs, maxBackoffMs, consecutiveFailures }) => (consecutiveFailures === 0 ? intervalMs : Math.min(maxBackoffMs, intervalMs * 2 ** Math.min(consecutiveFailures, 20))),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export function createTickLoopForOracleTests({ tick, intervalMs = DEFAULT_TICK_INTERVAL_MS, maxBackoffMs = DEFAULT_MAX_BACKOFF_MS, setTimer = setTimeout, clearTimer = clearTimeout, onOutcome = () => {} }, primitives = TICK_LOOP_PRIMITIVES) {
|
|
24
|
+
if (typeof tick !== 'function') throw new TypeError('the tick loop needs a tick')
|
|
25
|
+
if (!Number.isInteger(intervalMs) || intervalMs < 1 || !Number.isInteger(maxBackoffMs) || maxBackoffMs < intervalMs) throw new TypeError('the tick loop needs a positive interval and a backoff ceiling no smaller than it')
|
|
26
|
+
const rules = { ...TICK_LOOP_PRIMITIVES, ...primitives }
|
|
27
|
+
let timer = null
|
|
28
|
+
let inFlight = null
|
|
29
|
+
let followUp = null // { promise, resolve } while somebody waits for a tick that starts after now
|
|
30
|
+
let started = false
|
|
31
|
+
let stopped = false
|
|
32
|
+
let consecutiveFailures = 0
|
|
33
|
+
let ticks = 0
|
|
34
|
+
|
|
35
|
+
function schedule() {
|
|
36
|
+
if (stopped || !started || timer !== null) return
|
|
37
|
+
timer = setTimer(() => { timer = null; void run() }, rules.delayAfter({ intervalMs, maxBackoffMs, consecutiveFailures }))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function once() {
|
|
41
|
+
let outcome
|
|
42
|
+
try { outcome = { ok: true, report: await tick() } } catch (error) { outcome = { ok: false, error } }
|
|
43
|
+
ticks += 1
|
|
44
|
+
consecutiveFailures = outcome.ok ? 0 : consecutiveFailures + 1
|
|
45
|
+
try { await onOutcome({ ...outcome, consecutiveFailures }) } catch { /* recording an outcome never ends the loop */ }
|
|
46
|
+
if (!outcome.ok && !rules.continuesAfterError()) stopped = true
|
|
47
|
+
return outcome
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function run() {
|
|
51
|
+
if (stopped) return Promise.resolve({ ok: false, stopped: true })
|
|
52
|
+
if (inFlight) return inFlight
|
|
53
|
+
if (timer !== null) { clearTimer(timer); timer = null }
|
|
54
|
+
inFlight = once().finally(() => {
|
|
55
|
+
inFlight = null
|
|
56
|
+
const waiting = followUp
|
|
57
|
+
followUp = null
|
|
58
|
+
if (waiting) run().then(waiting.resolve)
|
|
59
|
+
else schedule()
|
|
60
|
+
})
|
|
61
|
+
return inFlight
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
start() { if (!started && !stopped) { started = true; void run() } },
|
|
66
|
+
tickNow() {
|
|
67
|
+
if (stopped) return Promise.resolve({ ok: false, stopped: true })
|
|
68
|
+
if (!inFlight) return run()
|
|
69
|
+
if (!followUp) { let resolve; const promise = new Promise((settle) => { resolve = settle }); followUp = { promise, resolve } }
|
|
70
|
+
return followUp.promise
|
|
71
|
+
},
|
|
72
|
+
// Lets the tick in flight finish; starts no other.
|
|
73
|
+
async stop() {
|
|
74
|
+
stopped = true
|
|
75
|
+
if (timer !== null) { clearTimer(timer); timer = null }
|
|
76
|
+
if (inFlight) await inFlight.catch(() => {})
|
|
77
|
+
},
|
|
78
|
+
state: () => ({ started, stopped, ticking: inFlight !== null, ticks, consecutiveFailures, nextDelayMs: rules.delayAfter({ intervalMs, maxBackoffMs, consecutiveFailures }) }),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createTickLoop(options) {
|
|
83
|
+
return createTickLoopForOracleTests(options, TICK_LOOP_PRIMITIVES)
|
|
84
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
|
|
3
|
+
// Filesystem watchers, as hints only.
|
|
4
|
+
//
|
|
5
|
+
// factory({ roots: [{ id, path, recursive }], onEvent }) -> { close() }
|
|
6
|
+
// onEvent({ rootId, relative }) relative is a '/'-separated path under the
|
|
7
|
+
// root, or null for "something under it"
|
|
8
|
+
//
|
|
9
|
+
// A watcher may drop, repeat or delay events and may fail to start at all; the
|
|
10
|
+
// engine never depends on one. It only hashes a named file sooner than the
|
|
11
|
+
// next stat scan would have. Tests inject a factory they drive by hand.
|
|
12
|
+
|
|
13
|
+
export function createNullWatcherFactory() {
|
|
14
|
+
return () => ({ close() {} })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// fs.watch, recursive where the platform supports it. It does not keep the
|
|
18
|
+
// process alive, and a root that cannot be watched is reported, not thrown:
|
|
19
|
+
// digest reconciliation still covers it.
|
|
20
|
+
export function createFsWatcherFactory({ watch = fs.watch, onUnavailable = () => {} } = {}) {
|
|
21
|
+
return ({ roots, onEvent }) => {
|
|
22
|
+
const watchers = []
|
|
23
|
+
for (const root of roots) {
|
|
24
|
+
try {
|
|
25
|
+
const watcher = watch(root.path, { recursive: root.recursive !== false, persistent: false }, (_eventType, filename) => {
|
|
26
|
+
onEvent({ rootId: root.id, relative: typeof filename === 'string' && filename !== '' ? filename.split('\\').join('/') : null })
|
|
27
|
+
})
|
|
28
|
+
watcher.on('error', () => onUnavailable({ rootId: root.id }))
|
|
29
|
+
watchers.push(watcher)
|
|
30
|
+
} catch {
|
|
31
|
+
onUnavailable({ rootId: root.id })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { close() { for (const watcher of watchers.splice(0)) try { watcher.close() } catch { /* already closed */ } } }
|
|
35
|
+
}
|
|
36
|
+
}
|