@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,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
|
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { canonicalize } from '../attestation/jcs.mjs'
|
|
6
|
+
import { buildGraph } from '../graph/graph.mjs'
|
|
7
|
+
import {
|
|
8
|
+
loadExtensionPacks,
|
|
9
|
+
createProtocolRegistry,
|
|
10
|
+
} from '../extension-packs/loader.mjs'
|
|
11
|
+
import { readRegularTextNoFollow } from '../project/private-state.mjs'
|
|
12
|
+
import { createCollaborationEventLedger } from '../collaboration/event-ledger.mjs'
|
|
13
|
+
import { inspectPackLifecycle } from '../extension-packs/lifecycle.mjs'
|
|
14
|
+
import { readLocalOverlay } from '../project/config.mjs'
|
|
15
|
+
import { readBoundedSource } from './source-read.mjs'
|
|
16
|
+
export { readBoundedSource } from './source-read.mjs'
|
|
17
|
+
import { validReviewArtifact } from '../collaboration/review-contracts.mjs'
|
|
18
|
+
import { buildReadinessRun, answerForField } from './runtime.mjs'
|
|
19
|
+
|
|
20
|
+
export const hashEvidence = (value) =>
|
|
21
|
+
`sha256:${crypto
|
|
22
|
+
.createHash('sha256')
|
|
23
|
+
.update(canonicalize(value))
|
|
24
|
+
.digest('hex')}`
|
|
25
|
+
const hashBytes = (value) =>
|
|
26
|
+
`sha256:${crypto.createHash('sha256').update(value).digest('hex')}`
|
|
27
|
+
const packageRoot = fileURLToPath(new URL('../../', import.meta.url))
|
|
28
|
+
function evaluatorInventory() {
|
|
29
|
+
const files = ['package.json']
|
|
30
|
+
function visit(relative) {
|
|
31
|
+
for (const name of fs
|
|
32
|
+
.readdirSync(path.join(packageRoot, relative))
|
|
33
|
+
.sort()) {
|
|
34
|
+
const next = path.posix.join(relative, name),
|
|
35
|
+
stat = fs.lstatSync(path.join(packageRoot, next))
|
|
36
|
+
if (stat.isSymbolicLink()) throw new Error('redirected evaluator input')
|
|
37
|
+
if (stat.isDirectory()) visit(next)
|
|
38
|
+
else if (stat.isFile() && /\.(mjs|json)$/.test(name)) files.push(next)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
visit('src')
|
|
42
|
+
visit('contracts')
|
|
43
|
+
return files
|
|
44
|
+
.sort()
|
|
45
|
+
.map((file) => ({
|
|
46
|
+
file,
|
|
47
|
+
digest: hashBytes(readRegularTextNoFollow(path.join(packageRoot, file))),
|
|
48
|
+
}))
|
|
49
|
+
}
|
|
50
|
+
const evaluator = hashEvidence(evaluatorInventory())
|
|
51
|
+
|
|
52
|
+
function currentInputs(project, protocolId, answers) {
|
|
53
|
+
if (
|
|
54
|
+
project.configPath &&
|
|
55
|
+
hashEvidence(JSON.parse(readRegularTextNoFollow(project.configPath))) !==
|
|
56
|
+
hashEvidence(project.config)
|
|
57
|
+
)
|
|
58
|
+
throw new Error('project config changed; resolve the project again')
|
|
59
|
+
if (hashEvidence(evaluatorInventory()) !== evaluator)
|
|
60
|
+
throw new Error('evaluator changed; restart before review')
|
|
61
|
+
if (
|
|
62
|
+
project.localOverlay &&
|
|
63
|
+
hashEvidence(readLocalOverlay({ configDir: project.configDir }).overlay) !==
|
|
64
|
+
hashEvidence(project.localOverlay.overlay)
|
|
65
|
+
)
|
|
66
|
+
throw new Error('local overlay changed; resolve again')
|
|
67
|
+
const loaded = loadExtensionPacks(project)
|
|
68
|
+
const packs = loaded.packs ?? loaded
|
|
69
|
+
const lifecycle = inspectPackLifecycle(project, packs)
|
|
70
|
+
if (!lifecycle.ok) throw new Error(lifecycle.errors.join('; '))
|
|
71
|
+
const registry = createProtocolRegistry({ packs })
|
|
72
|
+
const protocol = registry.protocolById(protocolId)
|
|
73
|
+
if (!protocol) throw new Error('protocol unavailable for evidence binding')
|
|
74
|
+
const graph = buildGraph(project)
|
|
75
|
+
if (graph.errors.length)
|
|
76
|
+
throw new Error('source graph is invalid; evidence capture refused')
|
|
77
|
+
if (graph.nodes.length > 256)
|
|
78
|
+
throw new Error('review evidence exceeds bounded source count')
|
|
79
|
+
const sources = [],
|
|
80
|
+
excerpts = new Map()
|
|
81
|
+
for (const node of graph.nodes) {
|
|
82
|
+
const repo = project.repos.find(
|
|
83
|
+
(entry) => entry.name === node.repo && !entry.external,
|
|
84
|
+
)
|
|
85
|
+
if (!repo?.path) throw new Error('source repository unavailable')
|
|
86
|
+
const source = readBoundedSource(repo.path, node.path)
|
|
87
|
+
const metadata = !/\.md$/i.test(node.path)
|
|
88
|
+
? readBoundedSource(repo.path, `${node.path}.kg.json`).digest
|
|
89
|
+
: null
|
|
90
|
+
excerpts.set(node.id, source.text.slice(0, 2000))
|
|
91
|
+
sources.push({
|
|
92
|
+
id: node.id,
|
|
93
|
+
repo: node.repo,
|
|
94
|
+
path: node.path,
|
|
95
|
+
audience: node.audience,
|
|
96
|
+
digest: source.digest,
|
|
97
|
+
metadata,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
sources.sort((a, b) =>
|
|
101
|
+
`${a.repo}/${a.path}`.localeCompare(`${b.repo}/${b.path}`, 'en'),
|
|
102
|
+
)
|
|
103
|
+
const claimEvidence = protocol.claimMappings.map((mapping) => {
|
|
104
|
+
const raw = answerForField(answers, mapping.evidence)
|
|
105
|
+
const references = (Array.isArray(raw) ? raw.flat(Infinity) : [raw])
|
|
106
|
+
.filter((value) => typeof value === 'string' && value.trim())
|
|
107
|
+
.map((value) => value.trim())
|
|
108
|
+
const resolved = [],
|
|
109
|
+
unresolved = []
|
|
110
|
+
for (const reference of references) {
|
|
111
|
+
const source = sources.find((item) => item.id === reference)
|
|
112
|
+
if (source)
|
|
113
|
+
resolved.push({
|
|
114
|
+
id: source.id,
|
|
115
|
+
repo: source.repo,
|
|
116
|
+
path: source.path,
|
|
117
|
+
digest: source.digest,
|
|
118
|
+
excerpt: excerpts.get(source.id),
|
|
119
|
+
})
|
|
120
|
+
else unresolved.push(reference)
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
mappingId: mapping.id,
|
|
124
|
+
sourceFields: mapping.sourceFields,
|
|
125
|
+
answers: Object.fromEntries(
|
|
126
|
+
mapping.sourceFields.map((field) => [
|
|
127
|
+
field,
|
|
128
|
+
answerForField(answers, field) ?? null,
|
|
129
|
+
]),
|
|
130
|
+
),
|
|
131
|
+
evidenceField: mapping.evidence,
|
|
132
|
+
references,
|
|
133
|
+
resolved,
|
|
134
|
+
unresolved,
|
|
135
|
+
status: !references.length
|
|
136
|
+
? 'missing'
|
|
137
|
+
: unresolved.length
|
|
138
|
+
? 'unresolved'
|
|
139
|
+
: 'linked-source',
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
const readPolicy = (file) =>
|
|
143
|
+
fs.existsSync(file) ? hashBytes(readRegularTextNoFollow(file)) : null
|
|
144
|
+
return {
|
|
145
|
+
schema: 'atelier-review-evidence@v1',
|
|
146
|
+
protocolId,
|
|
147
|
+
protocol,
|
|
148
|
+
protocolDigest: hashEvidence(protocol),
|
|
149
|
+
answers,
|
|
150
|
+
answersDigest: hashEvidence(answers),
|
|
151
|
+
configDigest: hashEvidence(project.config),
|
|
152
|
+
lifecycleDigest: hashEvidence(lifecycle),
|
|
153
|
+
policyDigest: hashEvidence({
|
|
154
|
+
access: readPolicy(project.repoAccessPath),
|
|
155
|
+
boundary: readPolicy(project.boundaryPolicyPath),
|
|
156
|
+
}),
|
|
157
|
+
packs: packs
|
|
158
|
+
.map(({ id, version, digest }) => ({ id, version, digest }))
|
|
159
|
+
.sort((a, b) => a.id.localeCompare(b.id, 'en')),
|
|
160
|
+
evaluator: { id: 'answer-completeness/v1', digest: evaluator },
|
|
161
|
+
sources,
|
|
162
|
+
claimEvidence,
|
|
163
|
+
limitations: [
|
|
164
|
+
'Answers are operator assertions; source presence does not substantiate each claim.',
|
|
165
|
+
'No calibrated evidence confidence or runtime acceptance is established.',
|
|
166
|
+
],
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function captureReviewEvidence(project, protocolId, answers) {
|
|
171
|
+
const first = currentInputs(project, protocolId, answers)
|
|
172
|
+
const second = currentInputs(project, protocolId, answers)
|
|
173
|
+
const digest = hashEvidence(first)
|
|
174
|
+
if (digest !== hashEvidence(second))
|
|
175
|
+
throw new Error('review inputs changed during capture')
|
|
176
|
+
const snapshot = { ...first, digest }
|
|
177
|
+
if (!validReviewArtifact('evidence', snapshot))
|
|
178
|
+
throw new Error('evidence contract invalid or exceeds bounds')
|
|
179
|
+
return snapshot
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function evidenceLedger(project) {
|
|
183
|
+
return createCollaborationEventLedger({
|
|
184
|
+
workspaceRoot: project.configDir,
|
|
185
|
+
ledgerPath: path.join(
|
|
186
|
+
project.configDir,
|
|
187
|
+
'.atelier-local/review/evidence.ndjson',
|
|
188
|
+
),
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function recordReviewEvidence(project, run) {
|
|
193
|
+
const snapshot = captureReviewEvidence(project, run.protocolId, run.answers)
|
|
194
|
+
if (
|
|
195
|
+
hashEvidence(
|
|
196
|
+
buildReadinessRun({
|
|
197
|
+
project,
|
|
198
|
+
protocol: snapshot.protocol,
|
|
199
|
+
answers: run.answers,
|
|
200
|
+
createdAt: run.createdAt,
|
|
201
|
+
}),
|
|
202
|
+
) !== hashEvidence(run)
|
|
203
|
+
)
|
|
204
|
+
throw new Error('run no longer matches its current protocol and evaluator')
|
|
205
|
+
const payload = {
|
|
206
|
+
schema: 'atelier-bound-run@v1',
|
|
207
|
+
run,
|
|
208
|
+
runDigest: hashEvidence(run),
|
|
209
|
+
snapshot,
|
|
210
|
+
}
|
|
211
|
+
if (!validBoundRun(payload)) throw new Error('bound run contract invalid')
|
|
212
|
+
const ledger = evidenceLedger(project)
|
|
213
|
+
const existing = ledger.eventsFor(run.runId)
|
|
214
|
+
if (!existing.ok) throw new Error(existing.error)
|
|
215
|
+
if (existing.events.length) {
|
|
216
|
+
if (hashEvidence(existing.events[0].payload) !== hashEvidence(payload))
|
|
217
|
+
throw new Error('run identity already has different evidence')
|
|
218
|
+
return existing.events[0].payload
|
|
219
|
+
}
|
|
220
|
+
const result = ledger.append({
|
|
221
|
+
aggregateId: run.runId,
|
|
222
|
+
expectedVersion: 0,
|
|
223
|
+
type: 'run-bound',
|
|
224
|
+
actor: 'local-runtime',
|
|
225
|
+
payload,
|
|
226
|
+
})
|
|
227
|
+
if (!result.ok) throw new Error(result.error)
|
|
228
|
+
return payload
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function validBoundRun(payload) {
|
|
232
|
+
if (!validReviewArtifact('bound', payload)) return false
|
|
233
|
+
const { digest, ...body } = payload.snapshot
|
|
234
|
+
const { run, snapshot } = payload
|
|
235
|
+
return (
|
|
236
|
+
payload.runDigest === hashEvidence(run) &&
|
|
237
|
+
digest === hashEvidence(body) &&
|
|
238
|
+
snapshot.protocolDigest === hashEvidence(snapshot.protocol) &&
|
|
239
|
+
snapshot.answersDigest === hashEvidence(snapshot.answers) &&
|
|
240
|
+
snapshot.answersDigest === hashEvidence(run.answers) &&
|
|
241
|
+
snapshot.protocol.id === run.protocolId &&
|
|
242
|
+
snapshot.protocolId === run.protocolId &&
|
|
243
|
+
snapshot.claimEvidence.length === run.claims.length &&
|
|
244
|
+
snapshot.claimEvidence.every(
|
|
245
|
+
(entry, index) =>
|
|
246
|
+
entry.mappingId === snapshot.protocol.claimMappings[index]?.id,
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function loadBoundRun(project, runId) {
|
|
252
|
+
const result = evidenceLedger(project).eventsFor(runId)
|
|
253
|
+
if (!result.ok) return result
|
|
254
|
+
if (result.events.length !== 1 || result.events[0].type !== 'run-bound')
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
status: 409,
|
|
258
|
+
error: 'run lacks immutable review evidence',
|
|
259
|
+
}
|
|
260
|
+
const payload = result.events[0].payload
|
|
261
|
+
const { digest, ...body } = payload.snapshot ?? {}
|
|
262
|
+
if (
|
|
263
|
+
!validBoundRun(payload) ||
|
|
264
|
+
payload.schema !== 'atelier-bound-run@v1' ||
|
|
265
|
+
payload.run?.runId !== runId ||
|
|
266
|
+
payload.runDigest !== hashEvidence(payload.run) ||
|
|
267
|
+
digest !== hashEvidence(body)
|
|
268
|
+
)
|
|
269
|
+
return { ok: false, status: 422, error: 'run evidence is invalid' }
|
|
270
|
+
return { ok: true, status: 200, ...payload }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function currentRunEligibility(project, bound) {
|
|
274
|
+
try {
|
|
275
|
+
return (
|
|
276
|
+
captureReviewEvidence(project, bound.run.protocolId, bound.run.answers)
|
|
277
|
+
.digest === bound.snapshot.digest
|
|
278
|
+
)
|
|
279
|
+
} catch {
|
|
280
|
+
return false
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -73,7 +73,7 @@ function nestedAnswer(value, parts) {
|
|
|
73
73
|
return nestedAnswer(value[head], rest)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function answerForField(answers, fieldId) {
|
|
76
|
+
export function answerForField(answers, fieldId) {
|
|
77
77
|
for (const key of pathVariants(fieldId)) {
|
|
78
78
|
if (Object.hasOwn(answers, key)) return answers[key]
|
|
79
79
|
}
|
|
@@ -119,9 +119,8 @@ export function claimsForProtocolRun({ protocol, answers, projectName, runId, cr
|
|
|
119
119
|
provider: 'atelier-readiness',
|
|
120
120
|
status: 'proposed',
|
|
121
121
|
promoted: false,
|
|
122
|
-
confidence: Object.values(answers).some(answerPresent) ? 0.7 : 0.2,
|
|
123
122
|
evidence: [`readiness-run:${runId}`],
|
|
124
|
-
notes: [`Proposed ${mapping.id} from ${protocol.title}
|
|
123
|
+
notes: [`Proposed ${mapping.id} from ${protocol.title}.`, 'Answer presence is not evidence confidence; human judgment is required.'],
|
|
125
124
|
createdAt,
|
|
126
125
|
}
|
|
127
126
|
})
|
|
@@ -147,6 +146,7 @@ export function buildReadinessRun({ project, protocol, answers = {}, createdAt =
|
|
|
147
146
|
claims: claimsForProtocolRun({ protocol, answers: normalizedAnswers, projectName, runId, createdAt }),
|
|
148
147
|
safety: {
|
|
149
148
|
runtimeMutation: false,
|
|
149
|
+
runtimeImport: false,
|
|
150
150
|
canonicalWrites: false,
|
|
151
151
|
claimOnly: true,
|
|
152
152
|
storage: 'ignored-local',
|
|
@@ -367,6 +367,7 @@ export function buildTenantPacket(project, { runs = listProtocolRuns(project), r
|
|
|
367
367
|
ext: {
|
|
368
368
|
'mnstry.atelier': {
|
|
369
369
|
extensionPacks: packContribution,
|
|
370
|
+
scoreMeaning: 'Legacy readinessScore measures completed input dimensions; it is not evidence confidence or runtime admission.',
|
|
370
371
|
},
|
|
371
372
|
},
|
|
372
373
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { openRegularFileNoFollow } from '../project/private-state.mjs'
|
|
5
|
+
const hashBytes = (value) =>
|
|
6
|
+
`sha256:${crypto.createHash('sha256').update(value).digest('hex')}`
|
|
7
|
+
|
|
8
|
+
export function readBoundedSource(root, relative) {
|
|
9
|
+
if (
|
|
10
|
+
typeof relative !== 'string' ||
|
|
11
|
+
path.isAbsolute(relative) ||
|
|
12
|
+
relative.split(/[\\/]/).some((part) => part === '..' || part === '.git')
|
|
13
|
+
)
|
|
14
|
+
throw new Error('source reference escapes declared boundary')
|
|
15
|
+
const realRoot = fs.realpathSync(root)
|
|
16
|
+
const file = path.resolve(realRoot, relative)
|
|
17
|
+
let cursor = realRoot
|
|
18
|
+
for (const part of path.relative(realRoot, file).split(path.sep)) {
|
|
19
|
+
cursor = path.join(cursor, part)
|
|
20
|
+
if (fs.lstatSync(cursor).isSymbolicLink())
|
|
21
|
+
throw new Error('redirected source reference refused')
|
|
22
|
+
}
|
|
23
|
+
const descriptor = openRegularFileNoFollow(file)
|
|
24
|
+
try {
|
|
25
|
+
const before = fs.fstatSync(descriptor)
|
|
26
|
+
if (before.size > 1024 * 1024)
|
|
27
|
+
throw new Error('source exceeds review byte bound')
|
|
28
|
+
const bytes = fs.readFileSync(descriptor)
|
|
29
|
+
const after = fs.fstatSync(descriptor)
|
|
30
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs)
|
|
31
|
+
throw new Error('source changed during read')
|
|
32
|
+
return {
|
|
33
|
+
digest: hashBytes(bytes),
|
|
34
|
+
text: new TextDecoder('utf-8', { fatal: true }).decode(bytes),
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
fs.closeSync(descriptor)
|
|
38
|
+
}
|
|
39
|
+
}
|