@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,141 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { isoTime } from '../../../runtime/obsidian/documents.mjs'
|
|
4
|
+
import { refuse } from '../../../runtime/obsidian/errors.mjs'
|
|
5
|
+
import { protectedRoots, readMachineSettings } from '../../../runtime/obsidian/machine-settings.mjs'
|
|
6
|
+
import { resolveScope } from '../../../runtime/obsidian/opening.mjs'
|
|
7
|
+
import { DEFAULT_ELIGIBILITY, createProductionSeams } from '../../../runtime/obsidian/pipeline.mjs'
|
|
8
|
+
import { createMaintenanceStateStore } from '../../../runtime/obsidian/state-store.mjs'
|
|
9
|
+
import { readConflictView } from './conflict-view.mjs'
|
|
10
|
+
import { runPolicySetup } from './policy-setup.mjs'
|
|
11
|
+
import { listSelectionStates, readSelectionState, writeSelectionState } from './selection-state.mjs'
|
|
12
|
+
import { resolveSelection } from './selection.mjs'
|
|
13
|
+
|
|
14
|
+
// The `obsidian` command operations of selection, conflict state and policy
|
|
15
|
+
// setup, registered on the command's operation registry the way AOP-1 laid
|
|
16
|
+
// out: a contribution `{ id, register({ operations }) }`. Nothing in the
|
|
17
|
+
// command dispatcher is edited. The built-in `scope`, `policy` and `mode`
|
|
18
|
+
// names are reserved, so these are `selection`, `conflicts` and
|
|
19
|
+
// `apply-policy`.
|
|
20
|
+
//
|
|
21
|
+
// Every operation is noninteractive and read-only towards sources, vaults
|
|
22
|
+
// and the app: `selection persist` writes the workspace's private selection
|
|
23
|
+
// state, `apply-policy create|revoke` write the private policy files through
|
|
24
|
+
// AOP-1's installer and revocation, and nothing else writes at all.
|
|
25
|
+
|
|
26
|
+
export const SELECTION_CONTRIBUTION_ID = 'atelier.selection-ui'
|
|
27
|
+
const EXIT = Object.freeze({ ok: 0, notSuccess: 3 })
|
|
28
|
+
const MAX_REQUEST_BYTES = 64 * 1024
|
|
29
|
+
|
|
30
|
+
// The canonical snapshot selectScope reads, built exactly as the engine
|
|
31
|
+
// builds it: the canonical graph with fail-closed eligibility, and the
|
|
32
|
+
// declared relations whose both endpoints the census knows.
|
|
33
|
+
function snapshotNow({ project, seams, eligibility }) {
|
|
34
|
+
const graph = seams.buildGraph({ project, eligibility })
|
|
35
|
+
const known = new Set(graph.nodes.map((node) => node.id))
|
|
36
|
+
return { nodes: graph.nodes, edges: graph.edges.filter((edge) => known.has(edge.source) && known.has(edge.target)) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// FILE is resolved from the working directory of the process, as the built-in `policy install FILE` resolves its file.
|
|
40
|
+
function readRequest(value) {
|
|
41
|
+
if (value === undefined) refuse('usage', 'apply-policy create FILE')
|
|
42
|
+
const file = path.resolve(value)
|
|
43
|
+
try {
|
|
44
|
+
if (fs.statSync(file).size > MAX_REQUEST_BYTES) refuse('invalid-apply-policy', 'the request file is larger than a request can be')
|
|
45
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (typeof error?.code === 'string' && error.name === 'ObsidianMaintenanceRefusal') throw error
|
|
48
|
+
return refuse('invalid-apply-policy', 'the request file cannot be read as JSON', { cause: error.code ?? 'not-json' })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createSelectionOperation({ seams = createProductionSeams(), eligibility = DEFAULT_ELIGIBILITY } = {}) {
|
|
53
|
+
return {
|
|
54
|
+
name: 'selection',
|
|
55
|
+
summary: 'resolve ID | persist ID [allow-empty] | show ID | list The exact selected set of a declared view, its note paths and, for a focus, the graph query; persisted in Atelier state.',
|
|
56
|
+
async run({ args, readable, writable, clock }) {
|
|
57
|
+
const [sub = 'list', requested, option] = args
|
|
58
|
+
if (option !== undefined && !(sub === 'persist' && option === 'allow-empty')) refuse('usage', 'selection persist ID [allow-empty]')
|
|
59
|
+
if (sub === 'list') {
|
|
60
|
+
const { workspace, workspaceId } = readable()
|
|
61
|
+
const selections = workspace === null ? [] : listSelectionStates({ workspaceRoot: workspace.workspaceRoot, workspaceId })
|
|
62
|
+
return { exit: EXIT.ok, document: { selections }, human: selections.length === 0 ? ['no persisted selection'] : selections.map((item) => `${item.scopeId}\t${item.mode}\t${item.updatedAt}${item.focus ? `\t${item.focus.paths.length} focused note(s)` : ''}`) }
|
|
63
|
+
}
|
|
64
|
+
if (!['resolve', 'persist', 'show'].includes(sub)) refuse('usage', 'selection resolve ID | persist ID | show ID | list')
|
|
65
|
+
if (sub === 'show') {
|
|
66
|
+
const { enablement, workspace, workspaceId } = readable()
|
|
67
|
+
const scopeId = resolveScope(enablement, requested)
|
|
68
|
+
const selection = workspace === null ? null : readSelectionState({ workspaceRoot: workspace.workspaceRoot, workspaceId, scopeId })
|
|
69
|
+
return { exit: selection === null ? EXIT.notSuccess : EXIT.ok, document: { scopeId, persisted: selection !== null, selection }, human: [selection === null ? `no persisted selection for ${scopeId}` : JSON.stringify(selection, null, 2)] }
|
|
70
|
+
}
|
|
71
|
+
const { project, enablement, workspace, repositoryRoots, now } = sub === 'persist' ? writable() : { ...readable(), repositoryRoots: null, now: isoTime(clock) }
|
|
72
|
+
const scopeId = resolveScope(enablement, requested)
|
|
73
|
+
const scope = enablement.scopes.find((item) => item.scopeId === scopeId)
|
|
74
|
+
const usable = workspace !== null && workspace.workspaceRoot !== null
|
|
75
|
+
const machine = usable ? readMachineSettings(workspace) : null
|
|
76
|
+
const profile = seams.profileFor({ project, workspaceId: workspace?.workspaceId ?? 'ws-unprepared', audienceAllow: machine?.audienceAllow ?? [] })
|
|
77
|
+
const canonicalSnapshot = snapshotNow({ project, seams, eligibility })
|
|
78
|
+
const pathRegistry = usable ? createMaintenanceStateStore(workspace).readPathRegistry() : null
|
|
79
|
+
// Explicit empty is an honest answer of `resolve`; `persist` of an empty selection is refused and asks for it by name.
|
|
80
|
+
const selection = resolveSelection({ canonicalSnapshot, profile, scope, pathRegistry, allowEmpty: sub === 'resolve' || option === 'allow-empty' })
|
|
81
|
+
if (sub === 'resolve') {
|
|
82
|
+
return { exit: EXIT.ok, document: { scopeId, selection, persisted: false }, human: [`${scopeId}: ${selection.mode}, ${selection.nodes.length} note(s), ${selection.edges.length} edge(s)${selection.truncated ? ', expansion truncated' : ''}${selection.focus ? `; focus query ${selection.focus.query}` : ''}`] }
|
|
83
|
+
}
|
|
84
|
+
const written = writeSelectionState({ workspaceRoot: workspace.workspaceRoot, workspaceId: workspace.workspaceId, repositoryRoots, selection, now })
|
|
85
|
+
return { exit: EXIT.ok, document: { scopeId, selection, persisted: true, changed: written.changed, file: path.relative(workspace.workspaceRoot, written.file).split(path.sep).join('/') }, human: [`${scopeId}: persisted${written.changed ? '' : ' (unchanged)'}; ${selection.nodes.length} note(s)${selection.focus ? `; focus query ${selection.focus.query}` : ''}`, 'No file under .obsidian/ was written; apply a focus in the app.'] }
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createConflictsOperation() {
|
|
91
|
+
return {
|
|
92
|
+
name: 'conflicts',
|
|
93
|
+
summary: '[ID] The shared conflict state of edited objects, read from the arbitration record: what is conflicted, pending or unreadable, and what a person does next. Read-only.',
|
|
94
|
+
async run({ args, readable, clock }) {
|
|
95
|
+
const [requested] = args
|
|
96
|
+
const { project, enablement, workspace, workspaceId } = readable()
|
|
97
|
+
const scopeId = requested === undefined ? undefined : resolveScope(enablement, requested)
|
|
98
|
+
if (workspace === null) return { exit: EXIT.ok, document: { scopeId: scopeId ?? null, objects: [], summary: { total: 0 }, needsPerson: false, workspace: 'not-prepared' }, human: ['no private state yet: no edit was ever recorded'] }
|
|
99
|
+
const view = readConflictView({ workspaceRoot: workspace.workspaceRoot, workspaceId, repositoryRoots: protectedRoots(project), clock, scopeId })
|
|
100
|
+
return {
|
|
101
|
+
exit: EXIT.ok, document: view,
|
|
102
|
+
human: view.objects.length === 0 ? ['no recorded object'] : view.objects.map((object) => `${object.repoId}\t${object.nodeId}\t${object.state}${object.needsPerson ? '\tneeds a person' : ''}\t${object.next}`),
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function createApplyPolicyOperation() {
|
|
109
|
+
return {
|
|
110
|
+
name: 'apply-policy',
|
|
111
|
+
summary: 'create FILE | show | revoke Build, digest and install a manual or automatic apply policy from a JSON request; show it; revoke it durably.',
|
|
112
|
+
async run({ args, readable, writable }) {
|
|
113
|
+
const [action, value] = args
|
|
114
|
+
if (action === undefined || action === 'show') {
|
|
115
|
+
const { workspace } = readable()
|
|
116
|
+
if (workspace === null) return { exit: EXIT.ok, document: { ok: true, action: 'show', installed: false, policy: null, reference: null, automaticApply: { authorized: false, reason: 'machine-settings-absent' } }, human: ['no apply policy is installed'] }
|
|
117
|
+
const result = runPolicySetup({ action: 'show', workspace })
|
|
118
|
+
return { exit: result.ok ? EXIT.ok : EXIT.notSuccess, document: result, human: [result.ok ? (result.installed ? `${result.policy.policyId} v${result.policy.version} ${result.policy.mode} ${result.policy.status}; automatic apply ${result.automaticApply.authorized ? 'authorized' : `not authorized (${result.automaticApply.reason})`}` : 'no apply policy is installed') : `[${result.refusal.code}] ${result.refusal.message}`] }
|
|
119
|
+
}
|
|
120
|
+
if (action !== 'create' && action !== 'revoke') refuse('usage', 'apply-policy create FILE | show | revoke')
|
|
121
|
+
const input = action === 'create' ? readRequest(value) : {}
|
|
122
|
+
const { workspace, repositoryRoots, now } = writable()
|
|
123
|
+
const result = runPolicySetup({ action, input, workspace, repositoryRoots, now })
|
|
124
|
+
const human = result.ok
|
|
125
|
+
? [action === 'create' ? `installed ${result.policy.policyId} v${result.policy.version} (${result.policy.mode}, ${result.policy.status}); maintenance mode ${result.maintenanceMode}` : (result.revoked ? `revoked (${result.reason}); no queued edit is applied from now on, and the mode is manual` : 'no apply policy was installed')]
|
|
126
|
+
: [`[${result.refusal.code}] ${result.refusal.message}`]
|
|
127
|
+
return { exit: result.ok ? EXIT.ok : EXIT.notSuccess, document: result, human }
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createSelectionContribution(options = {}) {
|
|
133
|
+
return {
|
|
134
|
+
id: SELECTION_CONTRIBUTION_ID,
|
|
135
|
+
register({ operations }) {
|
|
136
|
+
operations.register(createSelectionOperation(options))
|
|
137
|
+
operations.register(createConflictsOperation())
|
|
138
|
+
operations.register(createApplyPolicyOperation())
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { refuse } from '../../../runtime/obsidian/errors.mjs'
|
|
3
|
+
|
|
4
|
+
// Focus: the full vault stays on disk and the native Graph view is filtered
|
|
5
|
+
// with a "Search files" query derived from the selected set. The query names
|
|
6
|
+
// every selected note by its exact vault-relative path, quoted and escaped
|
|
7
|
+
// the way the app's search syntax reads a quoted term, joined with OR.
|
|
8
|
+
//
|
|
9
|
+
// Nothing here writes a file. The query and the core-Bookmark payload are
|
|
10
|
+
// values a person, an agent or the acceptance procedure applies in the app;
|
|
11
|
+
// `.obsidian/workspace.json`, `.obsidian/graph.json` and
|
|
12
|
+
// `.obsidian/bookmarks.json` belong to the person and are never written by
|
|
13
|
+
// Atelier. Whether the installed app applies the query as built is an
|
|
14
|
+
// acceptance question (AP-02), not something this module can answer.
|
|
15
|
+
|
|
16
|
+
export const FOCUS_QUERY_VERSION = 'obsidian-graph-search-paths/v1'
|
|
17
|
+
export const FOCUS_BOOKMARK_TYPE = 'graph'
|
|
18
|
+
// The files under .obsidian/ the app's UI owns. Atelier reads none of them
|
|
19
|
+
// and writes none of them; a focus is applied through the app, never by
|
|
20
|
+
// editing these.
|
|
21
|
+
export const UI_OWNED_SETTINGS_FILES = Object.freeze(['workspace.json', 'graph.json', 'bookmarks.json'])
|
|
22
|
+
export const MAX_FOCUS_PATHS = 100000
|
|
23
|
+
|
|
24
|
+
// A quoted search term: backslash and double quote are escaped; anything
|
|
25
|
+
// that is not printable text cannot be typed into a search box and refuses.
|
|
26
|
+
export const FOCUS_QUERY_PRIMITIVES = Object.freeze({
|
|
27
|
+
escape(value) {
|
|
28
|
+
return value.replace(/[\\"]/g, (character) => `\\${character}`)
|
|
29
|
+
},
|
|
30
|
+
term(path) {
|
|
31
|
+
return `path:"${path}"`
|
|
32
|
+
},
|
|
33
|
+
join(terms) {
|
|
34
|
+
return terms.join(' OR ')
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
function normalizePath(value, index) {
|
|
39
|
+
if (typeof value !== 'string' || value === '') refuse('focus-path-unrepresentable', 'a focus path must be a non-empty string', { index })
|
|
40
|
+
const normalized = value.normalize('NFC')
|
|
41
|
+
// eslint-disable-next-line no-control-regex
|
|
42
|
+
if (/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/.test(normalized)) refuse('focus-path-unrepresentable', 'a focus path carries a control character and cannot be a search term', { index })
|
|
43
|
+
if (normalized.startsWith('/') || normalized.includes('\\') || normalized.split('/').some((part) => part === '' || part === '.' || part === '..')) {
|
|
44
|
+
refuse('focus-path-unrepresentable', 'a focus path must be a normalized vault-relative path', { index })
|
|
45
|
+
}
|
|
46
|
+
return normalized
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Test seam: the oracles in test/obsidian-acceptance.test.mjs substitute a
|
|
50
|
+
// builder that drops escaping to prove the escaping oracle can fail.
|
|
51
|
+
// Production uses buildFocusQuery.
|
|
52
|
+
export function createFocusQueryBuilderForOracleTests(primitives = FOCUS_QUERY_PRIMITIVES) {
|
|
53
|
+
const { escape, term, join } = { ...FOCUS_QUERY_PRIMITIVES, ...primitives }
|
|
54
|
+
return function buildFocusQuery(paths) {
|
|
55
|
+
if (!Array.isArray(paths)) refuse('focus-selection-empty', 'a focus needs the list of selected note paths')
|
|
56
|
+
if (paths.length === 0) refuse('focus-selection-empty', 'a focus with no selected note would filter nothing; it is refused, never widened to the whole vault')
|
|
57
|
+
if (paths.length > MAX_FOCUS_PATHS) refuse('focus-too-large', `a focus names at most ${MAX_FOCUS_PATHS} notes`, { count: paths.length })
|
|
58
|
+
const normalized = paths.map(normalizePath)
|
|
59
|
+
if (new Set(normalized).size !== normalized.length) refuse('duplicate-identity', 'a focus names a note path more than once')
|
|
60
|
+
// The order is the order given (canonical-id order from the selection), so the same selection is the same query.
|
|
61
|
+
const terms = normalized.map((path) => term(escape(path)))
|
|
62
|
+
const query = join(terms)
|
|
63
|
+
return {
|
|
64
|
+
version: FOCUS_QUERY_VERSION,
|
|
65
|
+
query,
|
|
66
|
+
queryDigest: `sha256:${createHash('sha256').update(query, 'utf8').digest('hex')}`,
|
|
67
|
+
paths: normalized,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const buildFocusQuery = createFocusQueryBuilderForOracleTests()
|
|
73
|
+
|
|
74
|
+
// The payload of a core-plugin graph bookmark that retains this focus. The
|
|
75
|
+
// app assigns the creation time when the bookmark is made there; nothing
|
|
76
|
+
// nondeterministic is part of the payload. Only the graph type is supported:
|
|
77
|
+
// core Bookmarks retain graph views, not local-graph views.
|
|
78
|
+
export function focusBookmarkPayload({ scopeId, query }) {
|
|
79
|
+
if (typeof scopeId !== 'string' || scopeId === '') refuse('invalid-scope', 'a focus bookmark names its scope')
|
|
80
|
+
if (typeof query !== 'string' || query === '') refuse('focus-selection-empty', 'a focus bookmark carries a non-empty query')
|
|
81
|
+
return { type: FOCUS_BOOKMARK_TYPE, title: `Atelier focus ${scopeId}`, options: { search: query } }
|
|
82
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Selection, focus, apply policy setup, conflict state and acceptance receipt
|
|
2
|
+
// validation for the Obsidian projection. Selection and focus are pure;
|
|
3
|
+
// selection state, policy setup and the conflict view read and write the
|
|
4
|
+
// workspace's private state through the runtime's own primitives; the receipt
|
|
5
|
+
// validator is schema validation only and closes no gate.
|
|
6
|
+
export { FOCUS_BOOKMARK_TYPE, FOCUS_QUERY_PRIMITIVES, FOCUS_QUERY_VERSION, MAX_FOCUS_PATHS, UI_OWNED_SETTINGS_FILES, buildFocusQuery, createFocusQueryBuilderForOracleTests, focusBookmarkPayload } from './focus.mjs'
|
|
7
|
+
export { EMPTY_SELECTION_REASONS, SCOPE_CONTRACT_SCHEMA, SELECTION_SCHEMA, resolveSelection, scopeDocumentOf } from './selection.mjs'
|
|
8
|
+
export { SELECTION_DIRECTORY, SELECTION_STATE_SCHEMA, assertWritableSelectionPath, listSelectionStates, readSelectionState, selectionStateDocument, validateSelectionState, writeSelectionState } from './selection-state.mjs'
|
|
9
|
+
export { CONFLICT_DISPOSITION, DEFAULT_MAX_BATCH_SIZE, DEFAULT_RETRY_BUDGET, POLICY_SETUP_ACTIONS, POLICY_SETUP_SCHEMA, buildApplyPolicy, dispatchGate, runPolicySetup } from './policy-setup.mjs'
|
|
10
|
+
export { CONFLICT_VIEW_SCHEMA, OBJECT_VIEW_STATES, conflictView, readConflictView } from './conflict-view.mjs'
|
|
11
|
+
export { ACCEPTANCE_KINDS, RECEIPT_EXT_KEY, RECEIPT_GATES, RECEIPT_GATE_IDS, RECEIPT_RULES, RECEIPT_VALIDATION_LABEL, createReceiptValidatorForOracleTests, receiptRequirementsFor, validateAcceptanceReceipt } from './receipt.mjs'
|
|
12
|
+
export { SELECTION_CONTRIBUTION_ID, createApplyPolicyOperation, createConflictsOperation, createSelectionContribution, createSelectionOperation } from './contribution.mjs'
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { ObsidianMaintenanceRefusal, refuse } from '../../../runtime/obsidian/errors.mjs'
|
|
2
|
+
import { authorizeAutomaticApply, installApplyPolicy, readInstalledApplyPolicy, readMachineSettings, revokeApplyPolicy } from '../../../runtime/obsidian/machine-settings.mjs'
|
|
3
|
+
import { IMPLEMENTED_EDIT_CLASSES, ObsidianContractRefusal, assertObsidianContract } from '../contracts.mjs'
|
|
4
|
+
import { APPLY_MODES, applyPolicyDigest, withApplyPolicyDigest } from '../edits/policy.mjs'
|
|
5
|
+
|
|
6
|
+
// Apply policy setup, noninteractive: a structured request in, a structured
|
|
7
|
+
// answer out. Three actions:
|
|
8
|
+
//
|
|
9
|
+
// create build a manual or automatic policy document from a request,
|
|
10
|
+
// digest it, validate it against the frozen contract and install
|
|
11
|
+
// it through AOP-1's installer. Only the implemented edit class
|
|
12
|
+
// can be allowed; the conflict disposition is `hold`, the only
|
|
13
|
+
// one that exists.
|
|
14
|
+
// show the installed policy, the reference machine settings hold, and
|
|
15
|
+
// whether an automatic apply would be authorized right now.
|
|
16
|
+
// revoke AOP-1's revocation: the stored policy is written `revoked`
|
|
17
|
+
// first, then maintenance goes back to manual. Every later
|
|
18
|
+
// authorization read denies, and the engine reads authorization
|
|
19
|
+
// again immediately before each queued dispatch, so a revocation
|
|
20
|
+
// is durable and wins over anything queued behind it.
|
|
21
|
+
//
|
|
22
|
+
// Nothing here changes the maintenance mode to automatic: that is the
|
|
23
|
+
// person's separate `mode set automatic`, which itself checks that an active
|
|
24
|
+
// automatic policy is installed.
|
|
25
|
+
|
|
26
|
+
export const POLICY_SETUP_ACTIONS = Object.freeze(['create', 'show', 'revoke'])
|
|
27
|
+
export const POLICY_SETUP_SCHEMA = 'atelier-obsidian-policy-setup/v1'
|
|
28
|
+
export const DEFAULT_MAX_BATCH_SIZE = 10
|
|
29
|
+
export const DEFAULT_RETRY_BUDGET = 0
|
|
30
|
+
export const CONFLICT_DISPOSITION = 'hold'
|
|
31
|
+
|
|
32
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
33
|
+
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
34
|
+
const isTyped = (error) => error instanceof ObsidianMaintenanceRefusal || error instanceof ObsidianContractRefusal
|
|
35
|
+
|
|
36
|
+
// The policy document a request describes, with the digest of its content.
|
|
37
|
+
// `version` is taken from the request, else one above the installed policy of
|
|
38
|
+
// the same identity, else 1.
|
|
39
|
+
export function buildApplyPolicy({ workspaceId, policyId, mode, actor, selector, allowedEditClasses, maxBatchSize = DEFAULT_MAX_BATCH_SIZE, retryBudget = DEFAULT_RETRY_BUDGET, version = 1, status = 'active', ext } = {}) {
|
|
40
|
+
if (!APPLY_MODES.includes(mode)) refuse('invalid-apply-policy', 'policy mode must be manual or automatic', { mode: String(mode).slice(0, 32) })
|
|
41
|
+
if (!isPlainObject(actor) || !['user', 'agent'].includes(actor.kind) || typeof actor.id !== 'string' || !IDENTIFIER.test(actor.id)) refuse('invalid-apply-policy', 'the actor is { kind: user|agent, id }')
|
|
42
|
+
const classes = allowedEditClasses === undefined ? (mode === 'automatic' ? [...IMPLEMENTED_EDIT_CLASSES] : []) : allowedEditClasses
|
|
43
|
+
if (!Array.isArray(classes) || classes.some((item) => typeof item !== 'string')) refuse('invalid-apply-policy', 'allowedEditClasses is a list of edit classes')
|
|
44
|
+
const unimplemented = classes.filter((item) => !IMPLEMENTED_EDIT_CLASSES.includes(item))
|
|
45
|
+
if (unimplemented.length > 0) refuse('unimplemented-edit-class', 'only an edit class with an apply implementation can be allowed', { editClasses: unimplemented.map((item) => item.slice(0, 64)) })
|
|
46
|
+
const content = {
|
|
47
|
+
schema: 'atelier-obsidian-apply-policy/v1',
|
|
48
|
+
policyId, workspaceId, mode, status,
|
|
49
|
+
actor: { kind: actor.kind, id: actor.id },
|
|
50
|
+
version, allowedEditClasses: classes, selector, maxBatchSize, retryBudget,
|
|
51
|
+
conflictDisposition: CONFLICT_DISPOSITION,
|
|
52
|
+
...(ext === undefined ? {} : { ext }),
|
|
53
|
+
}
|
|
54
|
+
const policy = withApplyPolicyDigest(content)
|
|
55
|
+
try { assertObsidianContract('apply-policy', policy) } catch (error) {
|
|
56
|
+
if (error instanceof ObsidianContractRefusal) refuse('invalid-apply-policy', 'the policy does not satisfy its contract', { errors: error.detail?.errors ?? [] })
|
|
57
|
+
throw error
|
|
58
|
+
}
|
|
59
|
+
return policy
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const reference = (policy) => (policy === null ? null : { policyId: policy.policyId, version: policy.version, digest: policy.digest, mode: policy.mode, status: policy.status })
|
|
63
|
+
const authorizationOf = (workspace) => {
|
|
64
|
+
const { authorized, reason } = authorizeAutomaticApply(workspace)
|
|
65
|
+
return { authorized, reason }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The authorization the engine reads before each queued dispatch, in the
|
|
69
|
+
// form a caller sees: read from disk now, never cached.
|
|
70
|
+
export function dispatchGate(workspace) {
|
|
71
|
+
const authorization = authorizeAutomaticApply(workspace)
|
|
72
|
+
return { authorized: authorization.authorized, reason: authorization.reason, policyDigest: authorization.policy?.digest ?? null }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function showDocument(workspace) {
|
|
76
|
+
const policy = readInstalledApplyPolicy(workspace)
|
|
77
|
+
let machine = null
|
|
78
|
+
try { machine = readMachineSettings(workspace) } catch (error) { if (!isTyped(error)) throw error }
|
|
79
|
+
return { installed: policy !== null, policy, reference: machine?.applyPolicy ?? null, maintenanceMode: machine?.maintenanceMode ?? null, automaticApply: authorizationOf(workspace) }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// `workspace` is { workspaceRoot, workspaceId }; `repositoryRoots` the roots
|
|
83
|
+
// private state may not overlap; `now` the timestamp to record.
|
|
84
|
+
export function runPolicySetup({ action, input = {}, workspace, repositoryRoots, now }) {
|
|
85
|
+
const answer = (document) => ({ schema: POLICY_SETUP_SCHEMA, ok: true, action, ...document })
|
|
86
|
+
try {
|
|
87
|
+
if (!POLICY_SETUP_ACTIONS.includes(action)) refuse('usage', 'policy setup takes create, show or revoke', { action: String(action).slice(0, 32) })
|
|
88
|
+
if (!isPlainObject(workspace) || typeof workspace.workspaceRoot !== 'string' || typeof workspace.workspaceId !== 'string') refuse('usage', 'policy setup needs the workspace')
|
|
89
|
+
if (action === 'show') return answer(showDocument(workspace))
|
|
90
|
+
if (action === 'revoke') {
|
|
91
|
+
const result = revokeApplyPolicy({ ...workspace, repositoryRoots, updatedAt: now })
|
|
92
|
+
return answer({ revoked: result.revoked, reason: result.reason, policy: reference(result.policy), maintenanceMode: 'manual', automaticApply: authorizationOf(workspace) })
|
|
93
|
+
}
|
|
94
|
+
if (!isPlainObject(input)) refuse('usage', 'create takes a request object')
|
|
95
|
+
const installed = readInstalledApplyPolicy(workspace)
|
|
96
|
+
const version = input.version ?? (installed !== null && installed.policyId === input.policyId ? installed.version + 1 : 1)
|
|
97
|
+
if (installed !== null && installed.policyId === input.policyId && installed.version >= version) {
|
|
98
|
+
refuse('policy-version-not-newer', 'a policy of this identity is installed with an equal or higher version', { installed: installed.version, requested: version })
|
|
99
|
+
}
|
|
100
|
+
const policy = buildApplyPolicy({ ...input, workspaceId: workspace.workspaceId, version })
|
|
101
|
+
installApplyPolicy({ ...workspace, policy, repositoryRoots, updatedAt: now, digestOf: applyPolicyDigest })
|
|
102
|
+
return answer({ installed: true, policy, reference: reference(policy), maintenanceMode: readMachineSettings(workspace)?.maintenanceMode ?? 'manual', automaticApply: authorizationOf(workspace) })
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (!isTyped(error)) throw error
|
|
105
|
+
return { schema: POLICY_SETUP_SCHEMA, ok: false, action, refusal: { code: error.code, message: String(error.message).replace(`${error.code}: `, ''), detail: error.detail ?? {} } }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { OBSIDIAN_EXT_KEY, validateObsidianContract } from '../contracts.mjs'
|
|
2
|
+
|
|
3
|
+
// Acceptance receipt validation for the gates AOP-4 owns. This is schema
|
|
4
|
+
// validation and required-field checking of a document. It proves that a
|
|
5
|
+
// receipt is shaped as a receipt of that gate: exact candidate, pinned
|
|
6
|
+
// versions, host and operator identity, hashed raw evidence in every role
|
|
7
|
+
// the procedure calls for, and the separately recorded human or adopter
|
|
8
|
+
// acceptance where the gate needs one.
|
|
9
|
+
//
|
|
10
|
+
// It proves nothing about what happened in the app, on the host or with the
|
|
11
|
+
// adopter. `gateClosed` is always false. A gate is closed by its owner
|
|
12
|
+
// inspecting the actual evidence and recording that in the parent matrix; a
|
|
13
|
+
// document that passes here is an input to that inspection, never its result.
|
|
14
|
+
|
|
15
|
+
export const RECEIPT_VALIDATION_LABEL = 'schema-validation-only'
|
|
16
|
+
export const RECEIPT_EXT_KEY = OBSIDIAN_EXT_KEY
|
|
17
|
+
export const ACCEPTANCE_KINDS = Object.freeze(['human', 'adopter'])
|
|
18
|
+
|
|
19
|
+
// Per gate: the procedure of ACCEPTANCE.md that produces it, the evidence
|
|
20
|
+
// type the receipt carries, the evidence roles that must each name a hashed
|
|
21
|
+
// raw-evidence entry, and what is separately recorded.
|
|
22
|
+
export const RECEIPT_GATES = Object.freeze({
|
|
23
|
+
G07: Object.freeze({ procedure: 'AP-01', evidenceType: 'real-app', roles: Object.freeze(['cli-link-inspection', 'app-observation']), acceptance: 'human', wallClock: false, dataset: false, tarball: false }),
|
|
24
|
+
G13: Object.freeze({ procedure: 'AP-02', evidenceType: 'real-app', roles: Object.freeze(['on-disk-membership', 'app-index-membership', 'graph-filter-observation']), acceptance: 'human', wallClock: false, dataset: false, tarball: false }),
|
|
25
|
+
G14: Object.freeze({ procedure: 'AP-03', evidenceType: 'host', roles: Object.freeze(['source-refresh-trace', 'dropped-event-recovery', 'sleep-wake-clock']), acceptance: null, wallClock: true, dataset: false, tarball: false }),
|
|
26
|
+
G15: Object.freeze({ procedure: 'AP-03', evidenceType: 'host', roles: Object.freeze(['ownership-health', 'terminal-closure']), acceptance: null, wallClock: true, dataset: false, tarball: false }),
|
|
27
|
+
G16: Object.freeze({ procedure: 'AP-04', evidenceType: 'real-app', roles: Object.freeze(['dataset-manifest', 'resource-samples', 'app-indexing-timings', 'warm-update-latencies']), acceptance: null, wallClock: true, dataset: true, tarball: false }),
|
|
28
|
+
G17: Object.freeze({ procedure: 'AP-05', evidenceType: 'real-app', roles: Object.freeze(['multi-vault-edit-trace', 'manual-apply-trace', 'automatic-apply-trace', 'uninstall-retention']), acceptance: 'human', wallClock: false, dataset: false, tarball: false }),
|
|
29
|
+
G18: Object.freeze({ procedure: 'AP-06', evidenceType: 'human', roles: Object.freeze(['tarball-audit', 'adopter-acceptance']), acceptance: 'adopter', wallClock: false, dataset: false, tarball: true }),
|
|
30
|
+
})
|
|
31
|
+
export const RECEIPT_GATE_IDS = Object.freeze(Object.keys(RECEIPT_GATES))
|
|
32
|
+
|
|
33
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
34
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/
|
|
35
|
+
const PLACEHOLDER = /^(?:unknown|n\/a|na|none|null|tbd|todo|latest|-+|\?+|0)$/i
|
|
36
|
+
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
37
|
+
const isIdentifier = (value) => typeof value === 'string' && IDENTIFIER.test(value)
|
|
38
|
+
const isTime = (value) => typeof value === 'string' && !Number.isNaN(Date.parse(value))
|
|
39
|
+
const pinned = (value) => typeof value === 'string' && value.trim() !== '' && !PLACEHOLDER.test(value.trim())
|
|
40
|
+
|
|
41
|
+
// Every check answers a list of { code, pointer, message }. Each is a
|
|
42
|
+
// separate rule so the mutation controls of the test suite can switch one
|
|
43
|
+
// off and prove the negative case that depends on it then passes.
|
|
44
|
+
export const RECEIPT_RULES = Object.freeze({
|
|
45
|
+
gate(receipt, { gateId }) {
|
|
46
|
+
return receipt.gate === gateId ? [] : [{ code: 'gate-mismatch', pointer: '/gate', message: `the receipt is for ${receipt.gate}, not ${gateId}` }]
|
|
47
|
+
},
|
|
48
|
+
procedure(receipt, { gate }) {
|
|
49
|
+
const expected = new RegExp(`^${gate.procedure}(?:[.:-][A-Za-z0-9._:-]+)?$`)
|
|
50
|
+
return expected.test(receipt.procedureId) ? [] : [{ code: 'procedure-mismatch', pointer: '/procedureId', message: `this gate is produced by ${gate.procedure}` }]
|
|
51
|
+
},
|
|
52
|
+
evidenceType(receipt, { gate }) {
|
|
53
|
+
return receipt.evidenceType === gate.evidenceType ? [] : [{ code: 'evidence-type-mismatch', pointer: '/evidenceType', message: `this gate needs ${gate.evidenceType} evidence; a node result cannot stand for it` }]
|
|
54
|
+
},
|
|
55
|
+
versionPins(receipt) {
|
|
56
|
+
const missing = []
|
|
57
|
+
for (const [pointer, value] of [['/environment/os/name', receipt.environment.os.name], ['/environment/os/version', receipt.environment.os.version], ['/environment/app/name', receipt.environment.app.name], ['/environment/app/version', receipt.environment.app.version], ['/environment/cli/version', receipt.environment.cli.version]]) {
|
|
58
|
+
if (!pinned(value)) missing.push({ code: 'version-not-pinned', pointer, message: 'an exact installed version is recorded, never a placeholder' })
|
|
59
|
+
}
|
|
60
|
+
return missing
|
|
61
|
+
},
|
|
62
|
+
candidateIdentity(receipt, { gate }) {
|
|
63
|
+
const missing = []
|
|
64
|
+
if (/^0{40}$/.test(receipt.candidate.commit)) missing.push({ code: 'candidate-not-pinned', pointer: '/candidate/commit', message: 'the candidate commit is a placeholder' })
|
|
65
|
+
if (!DIGEST.test(String(receipt.candidate.treeDigest))) missing.push({ code: 'candidate-not-pinned', pointer: '/candidate/treeDigest', message: 'the candidate tree digest is missing' })
|
|
66
|
+
if (gate.tarball && !DIGEST.test(String(receipt.candidate.tarballDigest ?? ''))) missing.push({ code: 'candidate-not-pinned', pointer: '/candidate/tarballDigest', message: 'this gate binds the exact tarball' })
|
|
67
|
+
return missing
|
|
68
|
+
},
|
|
69
|
+
hostIdentity(receipt, { ext }) {
|
|
70
|
+
const missing = []
|
|
71
|
+
if (!isPlainObject(ext.host) || !isIdentifier(ext.host.id)) missing.push({ code: 'host-identity-missing', pointer: `/ext/${RECEIPT_EXT_KEY}/host/id`, message: 'the host the procedure ran on is identified' })
|
|
72
|
+
if (!isPlainObject(ext.operator) || !isIdentifier(ext.operator.id)) missing.push({ code: 'operator-identity-missing', pointer: `/ext/${RECEIPT_EXT_KEY}/operator/id`, message: 'the operator who ran the procedure is identified' })
|
|
73
|
+
return missing
|
|
74
|
+
},
|
|
75
|
+
evidenceHashes(receipt, { gate, ext }) {
|
|
76
|
+
const missing = []
|
|
77
|
+
const names = new Map(receipt.evidence.map((item) => [item.name, item]))
|
|
78
|
+
receipt.evidence.forEach((item, index) => {
|
|
79
|
+
if (!DIGEST.test(String(item.digest)) || !Number.isInteger(item.byteLength) || item.byteLength < 1) missing.push({ code: 'evidence-not-hashed', pointer: `/evidence/${index}`, message: 'every raw evidence file is hashed and non-empty' })
|
|
80
|
+
})
|
|
81
|
+
const roles = isPlainObject(ext.evidenceRoles) ? ext.evidenceRoles : {}
|
|
82
|
+
for (const role of gate.roles) {
|
|
83
|
+
const name = roles[role]
|
|
84
|
+
if (typeof name !== 'string' || !names.has(name)) missing.push({ code: 'evidence-role-missing', pointer: `/ext/${RECEIPT_EXT_KEY}/evidenceRoles/${role}`, message: `the procedure records ${role} as hashed raw evidence` })
|
|
85
|
+
}
|
|
86
|
+
for (const role of Object.keys(roles)) if (!gate.roles.includes(role)) missing.push({ code: 'evidence-role-unknown', pointer: `/ext/${RECEIPT_EXT_KEY}/evidenceRoles/${role}`, message: 'this gate has no such evidence role' })
|
|
87
|
+
return missing
|
|
88
|
+
},
|
|
89
|
+
acceptance(receipt, { gate, ext }) {
|
|
90
|
+
const pointer = `/ext/${RECEIPT_EXT_KEY}/acceptance`
|
|
91
|
+
if (gate.acceptance === null) return ext.acceptance === undefined ? [] : [{ code: 'acceptance-not-expected', pointer, message: 'this gate records automation and host evidence; a separate acceptance belongs to another gate' }]
|
|
92
|
+
const acceptance = ext.acceptance
|
|
93
|
+
if (!isPlainObject(acceptance)) return [{ code: 'acceptance-missing', pointer, message: `this gate needs the separately recorded ${gate.acceptance} acceptance` }]
|
|
94
|
+
const missing = []
|
|
95
|
+
if (acceptance.kind !== gate.acceptance) missing.push({ code: 'acceptance-kind-mismatch', pointer: `${pointer}/kind`, message: `this gate needs a ${gate.acceptance} acceptance` })
|
|
96
|
+
if (!isIdentifier(acceptance.actor)) missing.push({ code: 'acceptance-missing', pointer: `${pointer}/actor`, message: 'the person who accepted is identified' })
|
|
97
|
+
if (!isTime(acceptance.recordedAt)) missing.push({ code: 'acceptance-missing', pointer: `${pointer}/recordedAt`, message: 'when the acceptance was recorded' })
|
|
98
|
+
if (typeof acceptance.evidenceName !== 'string' || !receipt.evidence.some((item) => item.name === acceptance.evidenceName)) missing.push({ code: 'acceptance-missing', pointer: `${pointer}/evidenceName`, message: 'the acceptance names its own hashed evidence entry' })
|
|
99
|
+
if (isPlainObject(ext.operator) && isIdentifier(acceptance.actor) && acceptance.actor === ext.operator.id && gate.acceptance === 'adopter') missing.push({ code: 'acceptance-not-separate', pointer: `${pointer}/actor`, message: 'the adopter acceptance is recorded by the adopter, not the operator' })
|
|
100
|
+
return missing
|
|
101
|
+
},
|
|
102
|
+
wallClock(receipt, { gate, ext }) {
|
|
103
|
+
if (!gate.wallClock) return []
|
|
104
|
+
const clock = ext.wallClock
|
|
105
|
+
const pointer = `/ext/${RECEIPT_EXT_KEY}/wallClock`
|
|
106
|
+
if (!isPlainObject(clock) || !isTime(clock.startedAt) || !isTime(clock.endedAt)) return [{ code: 'wall-clock-missing', pointer, message: 'host procedures record the wall clock before and after' }]
|
|
107
|
+
return Date.parse(clock.endedAt) >= Date.parse(clock.startedAt) ? [] : [{ code: 'wall-clock-missing', pointer, message: 'the wall clock ends after it starts' }]
|
|
108
|
+
},
|
|
109
|
+
dataset(receipt, { gate, ext }) {
|
|
110
|
+
if (!gate.dataset) return []
|
|
111
|
+
const dataset = ext.dataset
|
|
112
|
+
const pointer = `/ext/${RECEIPT_EXT_KEY}/dataset`
|
|
113
|
+
if (!isPlainObject(dataset) || !Number.isInteger(dataset.nodes) || dataset.nodes < 1 || !Number.isInteger(dataset.edges) || dataset.edges < 0 || !DIGEST.test(String(dataset.fixtureDigest))) {
|
|
114
|
+
return [{ code: 'dataset-missing', pointer, message: 'the generated dataset is described by node count, edge count and fixture digest' }]
|
|
115
|
+
}
|
|
116
|
+
return []
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const EXT_KEYS = new Set(['host', 'operator', 'evidenceRoles', 'acceptance', 'wallClock', 'dataset'])
|
|
121
|
+
|
|
122
|
+
function extensionOf(receipt) {
|
|
123
|
+
const ext = receipt.ext?.[RECEIPT_EXT_KEY]
|
|
124
|
+
if (ext === undefined) return { ext: {}, missing: [{ code: 'extension-missing', pointer: `/ext/${RECEIPT_EXT_KEY}`, message: 'host, operator, evidence roles and acceptance are recorded under the extension key' }] }
|
|
125
|
+
if (!isPlainObject(ext)) return { ext: {}, missing: [{ code: 'extension-missing', pointer: `/ext/${RECEIPT_EXT_KEY}`, message: 'the extension member is an object' }] }
|
|
126
|
+
const unknown = Object.keys(ext).filter((key) => !EXT_KEYS.has(key)).sort()
|
|
127
|
+
return { ext, missing: unknown.map((key) => ({ code: 'extension-key-unknown', pointer: `/ext/${RECEIPT_EXT_KEY}/${key}`, message: 'the receipt extension is a closed shape' })) }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Test seam: the mutation controls substitute a rule table with one rule
|
|
131
|
+
// switched off. Production uses validateAcceptanceReceipt.
|
|
132
|
+
export function createReceiptValidatorForOracleTests(rules = RECEIPT_RULES) {
|
|
133
|
+
const table = { ...RECEIPT_RULES, ...rules }
|
|
134
|
+
return function validateAcceptanceReceipt(receipt, { gate: gateId } = {}) {
|
|
135
|
+
const constant = { label: RECEIPT_VALIDATION_LABEL, gateClosed: false, closes: 'nothing', note: 'A schema-valid receipt closes no gate. The gate owner inspects the actual app, host or adopter evidence and records the closing in the parent matrix.' }
|
|
136
|
+
const requested = gateId ?? (isPlainObject(receipt) ? receipt.gate : undefined)
|
|
137
|
+
if (!RECEIPT_GATE_IDS.includes(requested)) {
|
|
138
|
+
return { ...constant, gate: typeof requested === 'string' ? requested.slice(0, 8) : null, schemaValid: false, requirementsMet: false, valid: false, outcome: null, evidenceType: null, missing: [{ code: 'gate-not-owned', pointer: '/gate', message: `this validator knows the gates ${RECEIPT_GATE_IDS.join(', ')}` }] }
|
|
139
|
+
}
|
|
140
|
+
const gate = RECEIPT_GATES[requested]
|
|
141
|
+
const schemaErrors = validateObsidianContract('acceptance-receipt', receipt)
|
|
142
|
+
if (schemaErrors.length > 0) {
|
|
143
|
+
return { ...constant, gate: requested, schemaValid: false, requirementsMet: false, valid: false, outcome: null, evidenceType: null, missing: schemaErrors.map((error) => ({ code: error.code, pointer: null, message: error.message })) }
|
|
144
|
+
}
|
|
145
|
+
const { ext, missing: extensionShape } = extensionOf(receipt)
|
|
146
|
+
const context = { gateId: requested, gate, ext }
|
|
147
|
+
// The rules run in table order, gate identity first, so a receipt of another gate is told that before anything about its extension.
|
|
148
|
+
const missing = []
|
|
149
|
+
for (const name of Object.keys(RECEIPT_RULES)) missing.push(...table[name](receipt, context))
|
|
150
|
+
missing.push(...extensionShape)
|
|
151
|
+
const requirementsMet = missing.length === 0
|
|
152
|
+
return { ...constant, gate: requested, schemaValid: true, requirementsMet, valid: requirementsMet, outcome: receipt.outcome, evidenceType: receipt.evidenceType, missing }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const validateAcceptanceReceipt = createReceiptValidatorForOracleTests()
|
|
157
|
+
|
|
158
|
+
// The field table, for documentation and for the test that pins it.
|
|
159
|
+
export function receiptRequirementsFor(gateId) {
|
|
160
|
+
const gate = RECEIPT_GATES[gateId]
|
|
161
|
+
if (!gate) return null
|
|
162
|
+
return {
|
|
163
|
+
gate: gateId,
|
|
164
|
+
procedure: gate.procedure,
|
|
165
|
+
evidenceType: gate.evidenceType,
|
|
166
|
+
always: ['candidate.commit', 'candidate.treeDigest', 'environment.os.{name,version}', 'environment.app.{name,version}', 'environment.cli.version', 'evidence[].{name,digest,byteLength>0}', `ext.${RECEIPT_EXT_KEY}.host.id`, `ext.${RECEIPT_EXT_KEY}.operator.id`],
|
|
167
|
+
evidenceRoles: [...gate.roles],
|
|
168
|
+
acceptance: gate.acceptance,
|
|
169
|
+
wallClock: gate.wallClock,
|
|
170
|
+
dataset: gate.dataset,
|
|
171
|
+
tarballDigest: gate.tarball,
|
|
172
|
+
}
|
|
173
|
+
}
|