@mnstry/atelier 0.2.0-alpha.5 → 0.2.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +191 -0
- package/README.md +86 -25
- package/SECURITY.md +6 -1
- package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
- package/contracts/atelier-coauthor.v1.schema.json +163 -0
- package/contracts/atelier-decision-request.v1.schema.json +476 -0
- package/contracts/atelier-decision-result.v1.schema.json +500 -0
- package/contracts/atelier-guide.v1.schema.json +227 -0
- package/contracts/atelier-intake.v1.schema.json +151 -0
- package/contracts/atelier-migration.v2.schema.json +61 -0
- package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
- package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
- package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
- package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
- package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
- package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
- package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
- package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
- package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
- package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
- package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
- package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
- package/contracts/atelier-presentation.v1.schema.json +1293 -0
- package/contracts/atelier-repository-observation.v1.schema.json +163 -0
- package/contracts/atelier-review.v1.schema.json +1601 -0
- package/contracts/atelier-skill-steward.v1.schema.json +692 -0
- package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
- package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
- package/contracts/atelier-vault-publication.v1.schema.json +58 -0
- package/docs/assurance-controls.md +12 -2
- package/docs/atelier-runtime.md +13 -2
- package/docs/atelier-sync.md +172 -0
- package/docs/blocks/claims.md +28 -18
- package/docs/blocks/will-not-do.md +12 -3
- package/docs/coauthor-session.md +57 -0
- package/docs/connected-composition.md +75 -0
- package/docs/decisions.md +167 -0
- package/docs/exact-upgrades.md +159 -0
- package/docs/extension-pack-lifecycle.md +33 -0
- package/docs/guided-upgrades.md +99 -0
- package/docs/install.md +65 -9
- package/docs/intake-and-guides.md +65 -0
- package/docs/integration-and-review-plan.md +409 -0
- package/docs/integration-contract-decisions.md +22 -0
- package/docs/local-review.md +84 -0
- package/docs/local-services.md +227 -0
- package/docs/local-state.md +107 -0
- package/docs/obsidian-contract.md +687 -0
- package/docs/obsidian.md +387 -0
- package/docs/portable-presentation.md +328 -0
- package/docs/presentation-consumer-boundaries.md +75 -0
- package/docs/presentation-review-disposition.md +47 -0
- package/docs/project-command-inventory.md +38 -0
- package/docs/project-options.md +53 -0
- package/docs/release-engineering.md +99 -2
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +70 -2
- package/docs/vault-service.md +292 -0
- package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
- package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
- package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
- package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
- package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
- package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
- package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
- package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
- package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
- package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
- package/fixtures/atelier-intake/valid/source.v1.json +7 -0
- package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
- package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
- package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
- package/fixtures/atelier-review/valid/bound.v1.json +229 -0
- package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
- package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
- package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
- package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
- package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
- package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
- package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
- package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
- package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
- package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
- package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
- package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
- package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
- package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
- package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
- package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
- package/fixtures/decisions/request/valid/triage.v1.json +60 -0
- package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
- package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
- package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
- package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
- package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
- package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
- package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
- package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
- package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
- package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
- package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
- package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
- package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
- package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
- package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
- package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
- package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
- package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
- package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
- package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
- package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
- package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
- package/fixtures/obsidian/edits/workspace.json +65 -0
- package/fixtures/obsidian/materialization/expected-full.json +99 -0
- package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
- package/fixtures/obsidian/materialization/workspace.json +54 -0
- package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
- package/fixtures/ui/presentation/pane.v1.json +7 -0
- package/fixtures/ui/presentation/reference.v1.json +293 -0
- package/package.json +61 -6
- package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/claude/atelier-open/SKILL.md +5 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
- package/skills/claude/mnstry-readiness/SKILL.md +5 -0
- package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/codex/atelier-open/SKILL.md +5 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
- package/skills/codex/mnstry-readiness/SKILL.md +5 -0
- package/src/access/preview-enforcer.mjs +84 -0
- package/src/boundary/content-rules.mjs +9 -4
- package/src/boundary/policy.mjs +74 -42
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +61 -3
- package/src/coauthor/session.mjs +161 -0
- package/src/coauthor/store.mjs +173 -0
- package/src/collaboration/event-ledger.mjs +16 -33
- package/src/collaboration/inspection-bundle.mjs +223 -0
- package/src/collaboration/review-contracts.mjs +25 -0
- package/src/collaboration/review-store.mjs +305 -0
- package/src/commands/coauthor.mjs +26 -0
- package/src/commands/config.mjs +6 -1
- package/src/commands/extension-pack.mjs +15 -8
- package/src/commands/init.mjs +44 -5
- package/src/commands/obsidian.mjs +368 -0
- package/src/commands/review.mjs +147 -0
- package/src/commands/setup.mjs +51 -14
- package/src/commands/skills.mjs +199 -0
- package/src/commands/sync.mjs +107 -0
- package/src/composition/registry.mjs +27 -0
- package/src/composition/wire.mjs +122 -0
- package/src/contracts/corpus.mjs +35 -0
- package/src/decisions/contracts.d.mts +96 -0
- package/src/decisions/contracts.mjs +321 -0
- package/src/disclosure/content-scan.mjs +13 -0
- package/src/export/atelier-export-contract.mjs +16 -1
- package/src/extension-packs/lifecycle.mjs +154 -0
- package/src/graph/graph.mjs +64 -2
- package/src/graph/knowledge-graph.mjs +548 -24
- package/src/guides/contracts.mjs +54 -0
- package/src/index.mjs +29 -0
- package/src/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +50 -49
- package/src/project/durable-state.mjs +161 -0
- package/src/project/file-class.mjs +166 -0
- package/src/project/private-state.mjs +11 -1
- package/src/projection/obsidian/contracts.mjs +571 -0
- package/src/projection/obsidian/edits/align.mjs +169 -0
- package/src/projection/obsidian/edits/apply.mjs +889 -0
- package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
- package/src/projection/obsidian/edits/contribution.mjs +108 -0
- package/src/projection/obsidian/edits/index.mjs +19 -0
- package/src/projection/obsidian/edits/object-identity.mjs +61 -0
- package/src/projection/obsidian/edits/object-store.mjs +517 -0
- package/src/projection/obsidian/edits/observe.mjs +123 -0
- package/src/projection/obsidian/edits/policy.mjs +123 -0
- package/src/projection/obsidian/edits/regions.mjs +648 -0
- package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
- package/src/projection/obsidian/materialize/index.mjs +7 -0
- package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
- package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
- package/src/projection/obsidian/materialize/settings.mjs +57 -0
- package/src/projection/obsidian/materialize/stage.mjs +27 -0
- package/src/projection/obsidian/proposals/adapter.mjs +572 -0
- package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
- package/src/projection/obsidian/proposals/contribution.mjs +55 -0
- package/src/projection/obsidian/proposals/index.mjs +23 -0
- package/src/projection/obsidian/proposals/observation.mjs +216 -0
- package/src/projection/obsidian/proposals/queue.mjs +277 -0
- package/src/projection/obsidian/proposals/router.mjs +168 -0
- package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
- package/src/projection/obsidian/publication/exchange.mjs +117 -0
- package/src/projection/obsidian/publication/index.mjs +5 -0
- package/src/projection/obsidian/publication/publisher.mjs +588 -0
- package/src/projection/obsidian/publication/test-seam.mjs +16 -0
- package/src/projection/obsidian/publication/transport.mjs +139 -0
- package/src/projection/obsidian/recovery/index.mjs +6 -0
- package/src/projection/obsidian/recovery/journal.mjs +134 -0
- package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
- package/src/projection/obsidian/recovery/restart.mjs +316 -0
- package/src/projection/obsidian/recovery/store.mjs +234 -0
- package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
- package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
- package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
- package/src/projection/obsidian/selection-ui/index.mjs +12 -0
- package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
- package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
- package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
- package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
- package/src/projection/project.mjs +28 -19
- package/src/readiness-protocols/evidence.mjs +282 -0
- package/src/readiness-protocols/runtime.mjs +4 -3
- package/src/readiness-protocols/source-read.mjs +39 -0
- package/src/runtime/git-adapter.mjs +189 -0
- package/src/runtime/local-state.mjs +439 -0
- package/src/runtime/obsidian/app-capability.mjs +110 -0
- package/src/runtime/obsidian/app-production-seams.mjs +96 -0
- package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
- package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
- package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
- package/src/runtime/obsidian/contributions.mjs +27 -0
- package/src/runtime/obsidian/documents.mjs +37 -0
- package/src/runtime/obsidian/enablement.mjs +53 -0
- package/src/runtime/obsidian/engine.mjs +532 -0
- package/src/runtime/obsidian/errors.mjs +14 -0
- package/src/runtime/obsidian/extension-points.mjs +103 -0
- package/src/runtime/obsidian/index.mjs +34 -0
- package/src/runtime/obsidian/lifecycle.mjs +258 -0
- package/src/runtime/obsidian/machine-settings.mjs +260 -0
- package/src/runtime/obsidian/observation.mjs +158 -0
- package/src/runtime/obsidian/opening.mjs +243 -0
- package/src/runtime/obsidian/pending-edits.mjs +141 -0
- package/src/runtime/obsidian/pipeline.mjs +122 -0
- package/src/runtime/obsidian/private-lock.mjs +156 -0
- package/src/runtime/obsidian/process-identity.mjs +58 -0
- package/src/runtime/obsidian/service-client.mjs +66 -0
- package/src/runtime/obsidian/service-main.mjs +89 -0
- package/src/runtime/obsidian/service-record.mjs +169 -0
- package/src/runtime/obsidian/service-server.mjs +121 -0
- package/src/runtime/obsidian/service.mjs +216 -0
- package/src/runtime/obsidian/startup-adapters.mjs +87 -0
- package/src/runtime/obsidian/state-store.mjs +180 -0
- package/src/runtime/obsidian/sync-notice.mjs +33 -0
- package/src/runtime/obsidian/tick-loop.mjs +84 -0
- package/src/runtime/obsidian/watchers.mjs +36 -0
- package/src/runtime/repository-observation.mjs +491 -0
- package/src/runtime/supervisor.mjs +803 -0
- package/src/server/local-sidecar.mjs +44 -0
- package/src/server/server.mjs +1 -0
- package/src/skills/steward.mjs +780 -0
- package/src/ui/presentation/browser.mjs +278 -0
- package/src/ui/presentation/contract.mjs +116 -0
- package/src/ui/presentation/index.mjs +7 -0
- package/src/ui/presentation/native.mjs +153 -0
- package/src/ui/presentation/proof.mjs +57 -0
- package/src/ui/presentation/schema-check.mjs +30 -0
- package/src/ui/presentation/schema.generated.mjs +1371 -0
- package/src/ui/presentation/state.mjs +43 -0
- package/src/ui/presentation/styles.mjs +67 -0
- package/src/ui/presentation/tokens.mjs +74 -0
- package/src/ui/presentation/web.mjs +85 -0
- package/src/ui/review-page.mjs +32 -0
- package/src/upgrade/explanation.mjs +33 -0
- package/src/upgrade/provenance.mjs +338 -0
- package/src/upgrade/transaction-files.mjs +103 -0
- package/src/upgrade/transaction.mjs +475 -0
- package/src/upgrade/upgrade.mjs +63 -28
- package/src/vault/hosts.mjs +21 -0
- package/src/vault/identity.mjs +19 -0
- package/src/vault/index.mjs +7 -0
- package/src/vault/interface.mjs +11 -0
- package/src/vault/metadata.mjs +31 -0
- package/src/vault/privacy.mjs +148 -0
- package/src/vault/probe.mjs +89 -0
- package/src/vault/service.mjs +131 -0
- package/src/vault/source.mjs +45 -0
- package/src/vault/storage.mjs +23 -0
- package/templates/distribution-workspace/.claude/launch.json +6 -2
- package/templates/distribution-workspace/gitignore +1 -0
- package/templates/distribution-workspace/project/README.md +2 -2
- package/templates/external-project-workspace/README.md +45 -0
- package/templates/external-project-workspace/answers.example.json +4 -0
- package/templates/external-project-workspace/atelier.project.json +40 -0
- package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
- package/templates/external-project-workspace/gitignore +8 -0
- package/templates/external-project-workspace/pack-lifecycle.json +14 -0
- package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
- package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
- package/templates/external-project-workspace/repo-access.v1.json +9 -0
- package/templates/external-project-workspace/source/README.md +11 -0
- package/templates/launch.json +6 -2
- package/templates/private-domain-workspace/.claude/launch.json +6 -2
- package/templates/private-domain-workspace/domain/README.md +2 -2
- package/templates/private-domain-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/.claude/launch.json +6 -2
- package/templates/shared-project-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/project/README.md +2 -2
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { atomicReplacePrivateText, ensureContainedPrivateDirectory, readRegularTextNoFollow } from '../../../project/private-state.mjs'
|
|
4
|
+
import { canonicalJson, closedObject, compareText } from '../../../runtime/obsidian/documents.mjs'
|
|
5
|
+
import { refuse } from '../../../runtime/obsidian/errors.mjs'
|
|
6
|
+
import { assertOutsideRepositories } from '../../../runtime/obsidian/machine-settings.mjs'
|
|
7
|
+
import { SCOPE_MODES, assertObsidianContract } from '../contracts.mjs'
|
|
8
|
+
import { SETTINGS_ROOT } from '../materialize/settings.mjs'
|
|
9
|
+
import { FOCUS_BOOKMARK_TYPE, FOCUS_QUERY_VERSION } from './focus.mjs'
|
|
10
|
+
import { SELECTION_SCHEMA } from './selection.mjs'
|
|
11
|
+
|
|
12
|
+
// Where a selector is kept: Atelier's own private state of the workspace,
|
|
13
|
+
//
|
|
14
|
+
// <data>/obsidian/<workspace-id>/state/selection/<scope-id>.json
|
|
15
|
+
//
|
|
16
|
+
// beside the maintenance and settings documents AOP-1 keeps there, through
|
|
17
|
+
// the same private-state primitives (owner-only, atomic replace, no symlink
|
|
18
|
+
// following) and the same guard that keeps private state outside every
|
|
19
|
+
// enrolled repository. It is not under any vault, and it is never one of the
|
|
20
|
+
// files the app's UI owns under `.obsidian/`.
|
|
21
|
+
|
|
22
|
+
export const SELECTION_STATE_SCHEMA = 'atelier-obsidian-selection-state/v1'
|
|
23
|
+
export const SELECTION_DIRECTORY = path.join('state', 'selection')
|
|
24
|
+
|
|
25
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
26
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/
|
|
27
|
+
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/
|
|
28
|
+
const segment = (identifier) => identifier.replaceAll(':', '_')
|
|
29
|
+
|
|
30
|
+
export function validateSelectionState(document, workspaceId) {
|
|
31
|
+
const code = 'invalid-selection-state'
|
|
32
|
+
closedObject(document, { required: ['schema', 'workspaceId', 'scopeId', 'mode', 'selector', 'expansion', 'focus', 'updatedAt'] }, code, 'the selection state')
|
|
33
|
+
if (document.schema !== SELECTION_STATE_SCHEMA) refuse(code, 'the selection state names an unknown schema')
|
|
34
|
+
if (document.workspaceId !== workspaceId) refuse(code, 'the selection state belongs to another workspace')
|
|
35
|
+
if (typeof document.scopeId !== 'string' || !IDENTIFIER.test(document.scopeId)) refuse(code, 'the selection state names no scope')
|
|
36
|
+
if (!SCOPE_MODES.includes(document.mode)) refuse(code, 'the selection state carries an unknown mode')
|
|
37
|
+
// The selector and the expansion are exactly a scope document's: the contract decides whether they are well formed.
|
|
38
|
+
assertObsidianContract('scope', { schema: 'atelier-obsidian-scope/v1', scopeId: document.scopeId, mode: document.mode, selector: document.selector, ...(document.expansion === null ? {} : { expansion: document.expansion }) })
|
|
39
|
+
if (document.focus !== null) {
|
|
40
|
+
closedObject(document.focus, { required: ['version', 'query', 'queryDigest', 'paths', 'bookmark'] }, code, 'the persisted focus')
|
|
41
|
+
if (document.focus.version !== FOCUS_QUERY_VERSION || typeof document.focus.query !== 'string' || document.focus.query === '' || !DIGEST.test(String(document.focus.queryDigest))) refuse(code, 'the persisted focus is malformed')
|
|
42
|
+
if (!Array.isArray(document.focus.paths) || document.focus.paths.length === 0 || document.focus.paths.some((item) => typeof item !== 'string')) refuse(code, 'the persisted focus names no path')
|
|
43
|
+
closedObject(document.focus.bookmark, { required: ['type', 'title', 'options'] }, code, 'the persisted bookmark payload')
|
|
44
|
+
if (document.focus.bookmark.type !== FOCUS_BOOKMARK_TYPE) refuse(code, 'the persisted bookmark payload is not a graph bookmark')
|
|
45
|
+
}
|
|
46
|
+
if (document.mode === 'focus' ? document.focus === null : document.focus !== null) refuse(code, 'a focus is persisted exactly for a focus scope')
|
|
47
|
+
if (typeof document.updatedAt !== 'string' || !TIMESTAMP.test(document.updatedAt)) refuse(code, 'updatedAt must be a UTC timestamp')
|
|
48
|
+
return document
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A path Atelier may write: under the workspace's private state and never under a vault's `.obsidian/`.
|
|
52
|
+
export function assertWritableSelectionPath({ workspaceRoot, file }) {
|
|
53
|
+
const relative = path.relative(workspaceRoot, file)
|
|
54
|
+
if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) refuse('selection-state-outside-workspace', 'the selection state is written under the workspace private state only')
|
|
55
|
+
const parts = relative.split(path.sep)
|
|
56
|
+
if (parts.includes(SETTINGS_ROOT) || parts[0] === 'vaults') refuse('ui-owned-file-refused', 'Atelier never writes a vault or a file under .obsidian/; the app owns those')
|
|
57
|
+
if (parts.length !== 3 || parts[0] !== 'state' || parts[1] !== 'selection') refuse('selection-state-outside-workspace', 'the selection state lives in state/selection')
|
|
58
|
+
return file
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function selectionStateDocument({ workspaceId, selection, now }) {
|
|
62
|
+
if (selection?.schema !== SELECTION_SCHEMA) refuse('invalid-selection', 'a resolved selection is persisted, nothing else')
|
|
63
|
+
return {
|
|
64
|
+
schema: SELECTION_STATE_SCHEMA,
|
|
65
|
+
workspaceId,
|
|
66
|
+
scopeId: selection.scope.scopeId,
|
|
67
|
+
mode: selection.scope.mode,
|
|
68
|
+
selector: selection.scope.selector,
|
|
69
|
+
expansion: selection.scope.expansion ?? null,
|
|
70
|
+
focus: selection.focus === null ? null : { version: selection.focus.version, query: selection.focus.query, queryDigest: selection.focus.queryDigest, paths: selection.focus.paths, bookmark: selection.focus.bookmark },
|
|
71
|
+
updatedAt: now,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const selectionFile = (workspaceRoot, scopeId) => path.join(workspaceRoot, SELECTION_DIRECTORY, `${segment(scopeId)}.json`)
|
|
76
|
+
|
|
77
|
+
// Writes only when the bytes change. Returns { file, changed, document }.
|
|
78
|
+
export function writeSelectionState({ workspaceRoot, workspaceId, repositoryRoots, selection, now }) {
|
|
79
|
+
const document = validateSelectionState(selectionStateDocument({ workspaceId, selection, now }), workspaceId)
|
|
80
|
+
assertOutsideRepositories({ managedRoot: workspaceRoot, repositoryRoots })
|
|
81
|
+
const file = assertWritableSelectionPath({ workspaceRoot, file: selectionFile(workspaceRoot, document.scopeId) })
|
|
82
|
+
const text = canonicalJson(document)
|
|
83
|
+
let existing = null
|
|
84
|
+
try { existing = readRegularTextNoFollow(file) } catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
85
|
+
// A rewrite that changes only the time is not a change to the selection.
|
|
86
|
+
if (existing !== null) {
|
|
87
|
+
let previous = null
|
|
88
|
+
try { previous = JSON.parse(existing) } catch { previous = null }
|
|
89
|
+
if (previous !== null && canonicalJson({ ...previous, updatedAt: document.updatedAt }) === text) return { file, changed: false, document: previous }
|
|
90
|
+
}
|
|
91
|
+
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 })
|
|
92
|
+
const directory = ensureContainedPrivateDirectory({ workspaceRoot, directory: path.join(workspaceRoot, SELECTION_DIRECTORY), label: 'Obsidian selection state' })
|
|
93
|
+
atomicReplacePrivateText(path.join(directory, path.basename(file)), text)
|
|
94
|
+
return { file, changed: true, document }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function readSelectionState({ workspaceRoot, workspaceId, scopeId }) {
|
|
98
|
+
if (typeof scopeId !== 'string' || !IDENTIFIER.test(scopeId)) refuse('invalid-scope', 'name the scope whose selection to read')
|
|
99
|
+
let text
|
|
100
|
+
try { text = readRegularTextNoFollow(selectionFile(workspaceRoot, scopeId)) } catch (error) {
|
|
101
|
+
if (error.code === 'ENOENT') return null
|
|
102
|
+
refuse('invalid-selection-state', 'the selection state cannot be read', { cause: error.code ?? String(error.message) })
|
|
103
|
+
}
|
|
104
|
+
let document
|
|
105
|
+
try { document = JSON.parse(text) } catch { refuse('invalid-selection-state', 'the selection state is not JSON') }
|
|
106
|
+
const validated = validateSelectionState(document, workspaceId)
|
|
107
|
+
if (validated.scopeId !== scopeId) refuse('invalid-selection-state', 'the selection state names another scope')
|
|
108
|
+
return validated
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function listSelectionStates({ workspaceRoot, workspaceId }) {
|
|
112
|
+
let names
|
|
113
|
+
try { names = fs.readdirSync(path.join(workspaceRoot, SELECTION_DIRECTORY)) } catch (error) { if (error.code === 'ENOENT') return []; throw error }
|
|
114
|
+
const documents = []
|
|
115
|
+
for (const name of names.filter((item) => item.endsWith('.json')).sort(compareText)) {
|
|
116
|
+
const text = readRegularTextNoFollow(path.join(workspaceRoot, SELECTION_DIRECTORY, name))
|
|
117
|
+
let document
|
|
118
|
+
try { document = JSON.parse(text) } catch { refuse('invalid-selection-state', 'a selection state is not JSON', { file: name }) }
|
|
119
|
+
documents.push(validateSelectionState(document, workspaceId))
|
|
120
|
+
}
|
|
121
|
+
return documents.sort((left, right) => compareText(left.scopeId, right.scopeId))
|
|
122
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { refuse } from '../../../runtime/obsidian/errors.mjs'
|
|
2
|
+
import { EXPANSION_DIRECTIONS, EXPANSION_ORDERS, MAX_EXPANSION_DEPTH, ObsidianContractRefusal, SCOPE_MODES, assertObsidianContract, selectScope } from '../contracts.mjs'
|
|
3
|
+
import { allocateWorkspacePaths } from '../materialize/path-registry.mjs'
|
|
4
|
+
import { buildFocusQuery, focusBookmarkPayload } from './focus.mjs'
|
|
5
|
+
|
|
6
|
+
// Selection: a requested scope (full, scoped or focus, with an optional
|
|
7
|
+
// bounded expansion) becomes an exact scope document, the exact selected set
|
|
8
|
+
// and, for a focus, the graph query and bookmark payload derived from it.
|
|
9
|
+
//
|
|
10
|
+
// Selection is the contract's selectScope, unchanged: this module adds no
|
|
11
|
+
// selector, no default and no fallback. An empty result is reported as
|
|
12
|
+
// empty, or refused when the caller did not say empty is acceptable. It is
|
|
13
|
+
// never widened. The note paths are the ones the workspace path registry
|
|
14
|
+
// allocates, so a focus names the files a full vault of this workspace holds.
|
|
15
|
+
|
|
16
|
+
export const SELECTION_SCHEMA = 'atelier-obsidian-selection/v1'
|
|
17
|
+
export const SCOPE_CONTRACT_SCHEMA = 'atelier-obsidian-scope/v1'
|
|
18
|
+
export const EMPTY_SELECTION_REASONS = Object.freeze(['explicit-empty', 'no-visible-members'])
|
|
19
|
+
|
|
20
|
+
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
21
|
+
|
|
22
|
+
// The exact document: the caller's members, the two expansion defaults the
|
|
23
|
+
// contract allows filled in, and nothing else. A budget is never defaulted.
|
|
24
|
+
export function scopeDocumentOf({ scopeId, mode, selector, expansion, ext } = {}) {
|
|
25
|
+
if (typeof scopeId !== 'string' || scopeId === '') refuse('invalid-scope', 'a scope names itself with a scopeId')
|
|
26
|
+
if (!SCOPE_MODES.includes(mode)) refuse('invalid-scope', 'scope mode must be full, focus or scoped', { mode: String(mode).slice(0, 32) })
|
|
27
|
+
if (!isPlainObject(selector)) refuse('invalid-scope', 'a scope carries a selector object')
|
|
28
|
+
let normalizedExpansion
|
|
29
|
+
if (expansion !== undefined && expansion !== null) {
|
|
30
|
+
if (!isPlainObject(expansion)) refuse('invalid-expansion', 'expansion must be an object')
|
|
31
|
+
if (expansion.depth === undefined || expansion.maxNodes === undefined) refuse('missing-expansion-budget', 'expansion requires both a depth and a maxNodes budget; neither is defaulted')
|
|
32
|
+
// The budgets are checked here, before the contract, so a bad budget is an expansion fault and not a generic scope fault.
|
|
33
|
+
if (!Number.isInteger(expansion.depth) || expansion.depth < 1 || expansion.depth > MAX_EXPANSION_DEPTH) refuse('invalid-expansion', `depth must be an integer from 1 to ${MAX_EXPANSION_DEPTH}`)
|
|
34
|
+
if (!Number.isInteger(expansion.maxNodes) || expansion.maxNodes < 1) refuse('invalid-expansion', 'maxNodes must be a positive integer')
|
|
35
|
+
const direction = expansion.direction ?? 'outgoing'
|
|
36
|
+
const order = expansion.order ?? 'canonical-id'
|
|
37
|
+
if (!EXPANSION_DIRECTIONS.includes(direction)) refuse('invalid-expansion', 'unknown expansion direction')
|
|
38
|
+
if (!EXPANSION_ORDERS.includes(order)) refuse('invalid-expansion', 'unknown expansion order')
|
|
39
|
+
normalizedExpansion = { depth: expansion.depth, maxNodes: expansion.maxNodes, direction, order, ...(expansion.ext === undefined ? {} : { ext: expansion.ext }) }
|
|
40
|
+
}
|
|
41
|
+
const document = {
|
|
42
|
+
schema: SCOPE_CONTRACT_SCHEMA,
|
|
43
|
+
scopeId,
|
|
44
|
+
mode,
|
|
45
|
+
selector,
|
|
46
|
+
...(normalizedExpansion === undefined ? {} : { expansion: normalizedExpansion }),
|
|
47
|
+
...(ext === undefined ? {} : { ext }),
|
|
48
|
+
}
|
|
49
|
+
try { assertObsidianContract('scope', document) } catch (error) {
|
|
50
|
+
if (error instanceof ObsidianContractRefusal) refuse('invalid-scope', 'the requested scope does not satisfy the scope contract', { errors: error.detail?.errors ?? [] })
|
|
51
|
+
throw error
|
|
52
|
+
}
|
|
53
|
+
return document
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function contractRefusalToTyped(error) {
|
|
57
|
+
if (error instanceof ObsidianContractRefusal) refuse(error.code, error.message.replace(`${error.code}: `, ''), error.detail ?? {})
|
|
58
|
+
throw error
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// `pathRegistry` is the workspace's persisted path registry when the caller
|
|
62
|
+
// has one (AOP-1 keeps it in the maintenance state store); without it the
|
|
63
|
+
// paths are those a fresh workspace would allocate. Both are deterministic.
|
|
64
|
+
export function resolveSelection({ canonicalSnapshot, profile, scope, allowEmpty = false, pathRegistry = null, select = selectScope } = {}) {
|
|
65
|
+
const document = scopeDocumentOf(scope)
|
|
66
|
+
let result
|
|
67
|
+
try {
|
|
68
|
+
result = select({ canonicalSnapshot, profile, selector: document.selector, expansion: document.expansion, mode: document.mode })
|
|
69
|
+
} catch (error) { contractRefusalToTyped(error) }
|
|
70
|
+
|
|
71
|
+
const empty = result.nodes.length === 0
|
|
72
|
+
const emptyReason = empty ? (result.diagnostics.includes('selection-has-no-visible-members') ? 'no-visible-members' : 'explicit-empty') : null
|
|
73
|
+
if (empty && document.mode === 'focus') {
|
|
74
|
+
// A graph filter naming no note would show the whole vault or nothing, depending on the app. Neither is the request.
|
|
75
|
+
refuse('focus-selection-empty', 'the focus selects no visible note; a focus is never widened', { reason: emptyReason, unresolvedIds: result.unresolvedIds })
|
|
76
|
+
}
|
|
77
|
+
if (empty && allowEmpty !== true) refuse('selection-empty', 'the selection has no visible member; pass allowEmpty to produce an honest empty scope', { reason: emptyReason, unresolvedIds: result.unresolvedIds })
|
|
78
|
+
|
|
79
|
+
const workspaceId = typeof profile?.workspaceId === 'string' ? profile.workspaceId : 'ws-unnamed'
|
|
80
|
+
const nodeById = new Map(canonicalSnapshot.nodes.map((node) => [node.id, node]))
|
|
81
|
+
const vaultNodes = result.vaultNodes.map((id) => nodeById.get(id))
|
|
82
|
+
let pathOf
|
|
83
|
+
try { ({ pathOf } = allocateWorkspacePaths({ registry: pathRegistry, workspaceId, nodes: vaultNodes })) } catch (error) { contractRefusalToTyped(error) }
|
|
84
|
+
const notePaths = Object.fromEntries(result.nodes.map((id) => [id, pathOf(nodeById.get(id).repo, id)]))
|
|
85
|
+
|
|
86
|
+
let focus = null
|
|
87
|
+
if (document.mode === 'focus') {
|
|
88
|
+
const built = buildFocusQuery(result.nodes.map((id) => notePaths[id]))
|
|
89
|
+
focus = { ...built, bookmark: focusBookmarkPayload({ scopeId: document.scopeId, query: built.query }) }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
schema: SELECTION_SCHEMA,
|
|
94
|
+
scope: document,
|
|
95
|
+
mode: document.mode,
|
|
96
|
+
nodes: result.nodes,
|
|
97
|
+
edges: result.edges,
|
|
98
|
+
outsideSelectionEdges: result.outsideSelectionEdges,
|
|
99
|
+
vaultNodes: result.vaultNodes,
|
|
100
|
+
vaultEdges: result.vaultEdges,
|
|
101
|
+
truncated: result.truncated,
|
|
102
|
+
unresolvedIds: result.unresolvedIds,
|
|
103
|
+
diagnostics: result.diagnostics,
|
|
104
|
+
empty,
|
|
105
|
+
emptyReason,
|
|
106
|
+
notePaths,
|
|
107
|
+
focus,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import { commandProject, readJson, writeJson } from '../project/config.mjs'
|
|
4
5
|
import { summarizeReadinessJourney } from '../readiness-protocols/runtime.mjs'
|
|
@@ -50,7 +51,7 @@ export function buildProjectProjection(project) {
|
|
|
50
51
|
const dimensionCards = readinessJourney.dimensions.map((item) => `<article class="readiness-card" data-protocol="${esc(item.protocolId)}">
|
|
51
52
|
<p class="eyebrow">${esc(item.label)}</p>
|
|
52
53
|
<h3>${esc(item.title)}</h3>
|
|
53
|
-
<p><strong>${esc(item.status)}</strong> · ${esc(item.score)} / 100</p>
|
|
54
|
+
<p><strong>${esc(item.status)}</strong> · ${esc(item.score)} / 100 answers complete</p>
|
|
54
55
|
<p>${esc(item.blockers[0] || 'review-ready')}</p>
|
|
55
56
|
<button type="button" data-copy="${esc(item.agentPrompt)}">Copy protocol prompt</button>
|
|
56
57
|
</article>`).join('\n')
|
|
@@ -93,6 +94,7 @@ ${branding.themeCss}</style>
|
|
|
93
94
|
<div>
|
|
94
95
|
<p class="eyebrow">MNSTRY Tenant Readiness</p>
|
|
95
96
|
<h2 id="tenant-readiness-title">Readiness Journey</h2>
|
|
97
|
+
<p>Scores measure answer completeness, not evidence confidence, approval or runtime readiness.</p>
|
|
96
98
|
<p>${esc(readinessJourney.dimensions.length)} dimensions · next protocol: ${esc(readinessJourney.nextProtocol || 'none')}</p>
|
|
97
99
|
</div>
|
|
98
100
|
<div class="readiness-score">${esc(readinessJourney.score)}%</div>
|
|
@@ -117,36 +119,43 @@ document.addEventListener('click', async (event) => {
|
|
|
117
119
|
return { output, html, graph, readinessJourney }
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
export function buildProjectManifest(project, projection) {
|
|
123
|
+
return {
|
|
124
|
+
schema: 'mnstry.atelier-manifest@v1',
|
|
125
|
+
generatedAt: 'deterministic',
|
|
126
|
+
graphPath: path.relative(project.outputRoot, project.graphPath).split(path.sep).join('/'),
|
|
127
|
+
entry: 'index.html',
|
|
128
|
+
tenantReadiness: {
|
|
129
|
+
score: projection.readinessJourney.score,
|
|
130
|
+
ready: projection.readinessJourney.ready,
|
|
131
|
+
dimensions: projection.readinessJourney.dimensions.map((item) => ({
|
|
132
|
+
key: item.key,
|
|
133
|
+
protocolId: item.protocolId,
|
|
134
|
+
status: item.status,
|
|
135
|
+
score: item.score,
|
|
136
|
+
})),
|
|
137
|
+
},
|
|
138
|
+
nodes: projection.graph.nodes.map((node) => ({ id: node.id, title: node.title, audience: node.audience, path: node.path })),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
120
142
|
export function runProjectCommand(argv = process.argv.slice(2)) {
|
|
121
143
|
const check = argv.includes('--check')
|
|
122
144
|
const project = commandProject({ argv })
|
|
123
145
|
const projection = buildProjectProjection(project)
|
|
124
146
|
if (check) {
|
|
125
147
|
const current = fs.existsSync(projection.output) ? fs.readFileSync(projection.output, 'utf8') : null
|
|
126
|
-
|
|
148
|
+
const manifestPath = path.join(project.outputRoot, 'atelier.manifest.json')
|
|
149
|
+
let manifest = null
|
|
150
|
+
try { manifest = readJson(manifestPath) } catch { /* Missing or invalid artifacts are stale. */ }
|
|
151
|
+
if (current !== projection.html || !isDeepStrictEqual(manifest, JSON.parse(JSON.stringify(buildProjectManifest(project, projection))))) {
|
|
127
152
|
console.error(`project projection is stale: ${projection.output}`)
|
|
128
153
|
process.exit(1)
|
|
129
154
|
}
|
|
130
155
|
} else {
|
|
131
156
|
fs.mkdirSync(path.dirname(projection.output), { recursive: true })
|
|
132
157
|
fs.writeFileSync(projection.output, projection.html)
|
|
133
|
-
writeJson(path.join(project.outputRoot, 'atelier.manifest.json'),
|
|
134
|
-
schema: 'mnstry.atelier-manifest@v1',
|
|
135
|
-
generatedAt: 'deterministic',
|
|
136
|
-
graphPath: project.graphPath,
|
|
137
|
-
entry: 'index.html',
|
|
138
|
-
tenantReadiness: {
|
|
139
|
-
score: projection.readinessJourney.score,
|
|
140
|
-
ready: projection.readinessJourney.ready,
|
|
141
|
-
dimensions: projection.readinessJourney.dimensions.map((item) => ({
|
|
142
|
-
key: item.key,
|
|
143
|
-
protocolId: item.protocolId,
|
|
144
|
-
status: item.status,
|
|
145
|
-
score: item.score,
|
|
146
|
-
})),
|
|
147
|
-
},
|
|
148
|
-
nodes: projection.graph.nodes.map((node) => ({ id: node.id, title: node.title, audience: node.audience, path: node.path })),
|
|
149
|
-
})
|
|
158
|
+
writeJson(path.join(project.outputRoot, 'atelier.manifest.json'), buildProjectManifest(project, projection))
|
|
150
159
|
}
|
|
151
160
|
console.log(`project projection: ${projection.graph.counts.nodes} nodes -> ${projection.output}`)
|
|
152
161
|
}
|