@mnstry/atelier 0.2.0-alpha.1 → 0.2.0-alpha.10
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 +372 -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 +690 -0
- package/docs/obsidian.md +423 -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 +146 -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 +160 -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 +129 -0
- package/src/runtime/obsidian/app-production-seams.mjs +105 -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 +263 -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 +91 -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
package/src/boundary/policy.mjs
CHANGED
|
@@ -4,14 +4,14 @@ import path from 'node:path'
|
|
|
4
4
|
import { buildGraph } from '../graph/graph.mjs'
|
|
5
5
|
import { commandProject, firstString, parseArgs, readJson, resolvePathValue, writeJson } from '../project/config.mjs'
|
|
6
6
|
import { matchesPathPattern } from '../project/path-match.mjs'
|
|
7
|
+
import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
|
|
7
8
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
parsePushRefUpdates,
|
|
9
|
+
CHECK_AGGREGATE_MAX_BYTES,
|
|
10
|
+
parsePushRefInput,
|
|
11
11
|
resolveContentRules,
|
|
12
|
-
|
|
12
|
+
scanPushUpdate,
|
|
13
|
+
scanStagedRepository,
|
|
13
14
|
scanTree,
|
|
14
|
-
stagedDiff,
|
|
15
15
|
validateContentRuleExceptions,
|
|
16
16
|
validateContentRules,
|
|
17
17
|
} from './content-rules.mjs'
|
|
@@ -146,7 +146,7 @@ export function validateBoundaryPolicy(policy, project = null) {
|
|
|
146
146
|
const declaredRepos = isObject(repos) ? repos : {}
|
|
147
147
|
for (const [index, exception] of policy.contentRuleExceptions.entries()) {
|
|
148
148
|
const name = firstString(exception?.repo)
|
|
149
|
-
if (name && Object.keys(declaredRepos).length && !declaredRepos
|
|
149
|
+
if (name && Object.keys(declaredRepos).length && !Object.hasOwn(declaredRepos, name)) {
|
|
150
150
|
errors.push(`contentRuleExceptions[${index}].repo ${name} is not declared in repos`)
|
|
151
151
|
}
|
|
152
152
|
}
|
|
@@ -163,7 +163,7 @@ export function validateBoundaryPolicy(policy, project = null) {
|
|
|
163
163
|
}
|
|
164
164
|
if (!firstString(actor.privateDomainRepo)) errors.push(`actors.${actorId}.privateDomainRepo is required`)
|
|
165
165
|
if (firstString(actor.privateDomainRepo) && isObject(repos)) {
|
|
166
|
-
const privateRepo = repos[actor.privateDomainRepo]
|
|
166
|
+
const privateRepo = Object.hasOwn(repos, actor.privateDomainRepo) ? repos[actor.privateDomainRepo] : null
|
|
167
167
|
if (!privateRepo) {
|
|
168
168
|
errors.push(`actors.${actorId}.privateDomainRepo ${actor.privateDomainRepo} is not declared in repos`)
|
|
169
169
|
} else if (privateRepo.kind !== 'private_domain') {
|
|
@@ -188,7 +188,7 @@ export function validateBoundaryPolicy(policy, project = null) {
|
|
|
188
188
|
errors.push(...validAudienceList(repo.forbiddenAudiences, `repos.${repoName}.forbiddenAudiences`))
|
|
189
189
|
if (repo.kind === 'private_domain') {
|
|
190
190
|
if (!firstString(repo.ownerActor)) errors.push(`repos.${repoName}.ownerActor is required for private_domain repos`)
|
|
191
|
-
if (repo.ownerActor && isObject(actors) && !actors
|
|
191
|
+
if (repo.ownerActor && isObject(actors) && !Object.hasOwn(actors, repo.ownerActor)) errors.push(`repos.${repoName}.ownerActor ${repo.ownerActor} is not declared`)
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -197,41 +197,71 @@ export function validateBoundaryPolicy(policy, project = null) {
|
|
|
197
197
|
if (repo.external) {
|
|
198
198
|
// Declaring a boundary for a repo nobody manages is the confusion the
|
|
199
199
|
// external kind exists to remove.
|
|
200
|
-
if (repos
|
|
200
|
+
if (Object.hasOwn(repos, repo.name)) errors.push(`policy repos.${repo.name} must not be declared; it is an external (unmanaged) repo`)
|
|
201
201
|
continue
|
|
202
202
|
}
|
|
203
|
-
if (!repos
|
|
203
|
+
if (!Object.hasOwn(repos, repo.name)) errors.push(`policy repos.${repo.name} must be declared`)
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
return errors
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
function resolveExplicitActor({ policy, actor, env }) {
|
|
211
211
|
const actors = policy?.actors ?? {}
|
|
212
|
-
const explicit = actor || env.MNSTRY_ATELIER_ACTOR
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const actorEmails = new Set(asArray(info.gitEmails).map((email) => email.toLowerCase()))
|
|
217
|
-
if (gitEmails.some((email) => actorEmails.has(email.toLowerCase()))) return { actorId, source: 'git-email', gitEmails }
|
|
212
|
+
const explicit = actor || env.MNSTRY_ATELIER_ACTOR
|
|
213
|
+
const unverified = (reason) => ({ actorId: null, source: 'unverified', reason })
|
|
214
|
+
if (actor && env.MNSTRY_ATELIER_ACTOR && actor !== env.MNSTRY_ATELIER_ACTOR) {
|
|
215
|
+
return unverified('conflicting-explicit-actors')
|
|
218
216
|
}
|
|
219
|
-
|
|
217
|
+
if (explicit && !Object.hasOwn(actors, explicit)) return unverified('unknown-explicit-actor')
|
|
218
|
+
return explicit ? { actorId: explicit, source: 'explicit' } : null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function resolveCurrentActor({ policy, project, actor = null, env = process.env, gitExecutable = 'git', allowNetworkActorResolution = true, allowHistoryActorResolution = false } = {}) {
|
|
222
|
+
const actors = policy?.actors ?? {}
|
|
223
|
+
const explicit = resolveExplicitActor({ policy, actor, env })
|
|
224
|
+
if (explicit) return explicit
|
|
225
|
+
const unverified = (reason) => ({ actorId: null, source: 'unverified', reason })
|
|
226
|
+
// Without an explicit local selector, a platform login precedes Git metadata.
|
|
227
|
+
// It is platform attribution only; this function cannot authenticate its env.
|
|
228
|
+
const platformLogin = env.GITHUB_ACTOR
|
|
229
|
+
if (platformLogin) {
|
|
230
|
+
const matches = Object.entries(actors).filter(([, info]) =>
|
|
231
|
+
!String(info.githubLogin || '').endsWith('_GITHUB_LOGIN_PLACEHOLDER') &&
|
|
232
|
+
String(info.githubLogin || '').toLowerCase() === String(platformLogin).toLowerCase())
|
|
233
|
+
if (matches.length !== 1) return unverified(matches.length ? 'ambiguous-platform-actor' : 'unknown-platform-actor')
|
|
234
|
+
return { actorId: matches[0][0], source: 'github-login', githubLogin: platformLogin }
|
|
235
|
+
}
|
|
236
|
+
// Keep the legacy option accepted by the API, but history is never evidence
|
|
237
|
+
// of the person currently operating a checkout.
|
|
238
|
+
const gitEmails = gitEmailsForProject(project, { gitExecutable, env, allowHistoryActorResolution: false })
|
|
239
|
+
const matches = Object.entries(actors).filter(([, info]) => {
|
|
240
|
+
const actorEmails = new Set(asArray(info.gitEmails).map((email) => email.toLowerCase()))
|
|
241
|
+
return gitEmails.some((email) => actorEmails.has(email.toLowerCase()))
|
|
242
|
+
})
|
|
243
|
+
if (matches.length > 1) return unverified('ambiguous-git-actor')
|
|
244
|
+
if (matches.length === 1) return { actorId: matches[0][0], source: 'git-email', gitEmails }
|
|
245
|
+
const login = allowNetworkActorResolution ? ghLogin() : null
|
|
220
246
|
if (login) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
247
|
+
const matches = Object.entries(actors).filter(([, info]) => !String(info.githubLogin || '').endsWith('_GITHUB_LOGIN_PLACEHOLDER') && String(info.githubLogin || '').toLowerCase() === String(login).toLowerCase())
|
|
248
|
+
if (matches.length !== 1) return unverified(matches.length ? 'ambiguous-platform-actor' : 'unknown-platform-actor')
|
|
249
|
+
return { actorId: matches[0][0], source: 'github-login', githubLogin: login }
|
|
224
250
|
}
|
|
225
|
-
return { actorId: null, source: 'unverified', gitEmails, githubLogin:
|
|
251
|
+
return { actorId: null, source: 'unverified', gitEmails, githubLogin: null }
|
|
226
252
|
}
|
|
227
253
|
|
|
228
|
-
function gitEmailsForProject(project) {
|
|
254
|
+
function gitEmailsForProject(project, { gitExecutable = 'git', env = process.env, allowHistoryActorResolution = false } = {}) {
|
|
229
255
|
const roots = unique([project?.repoOpsRoot, project?.workspaceRoot, ...managedRepos(project).map((repo) => repo.path)])
|
|
230
256
|
const emails = []
|
|
231
257
|
for (const root of roots) {
|
|
232
258
|
if (!root || !fs.existsSync(root)) continue
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
259
|
+
const probes = [['config', 'user.email']]
|
|
260
|
+
if (allowHistoryActorResolution) probes.push(['log', '-1', '--format=%ae'])
|
|
261
|
+
for (const args of probes) {
|
|
262
|
+
const result = spawnSync(gitExecutable, ['-C', root, ...args], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
|
|
263
|
+
if (result.status === 0 && result.stdout.trim()) emails.push(result.stdout.trim())
|
|
264
|
+
}
|
|
235
265
|
}
|
|
236
266
|
return unique(emails)
|
|
237
267
|
}
|
|
@@ -245,7 +275,7 @@ function ghLogin() {
|
|
|
245
275
|
}
|
|
246
276
|
|
|
247
277
|
function repoPolicy(policy, repoName) {
|
|
248
|
-
return policy?.repos
|
|
278
|
+
return policy?.repos && Object.hasOwn(policy.repos, repoName) ? policy.repos[repoName] : null
|
|
249
279
|
}
|
|
250
280
|
|
|
251
281
|
function finding({ severity = 'error', code, message, repo = null, path: itemPath = null, node = null, details = {} }) {
|
|
@@ -276,15 +306,21 @@ function nodePlacementFindings({ node, policy }) {
|
|
|
276
306
|
return findings
|
|
277
307
|
}
|
|
278
308
|
|
|
279
|
-
function actorFindings({ policy, project, actor }) {
|
|
309
|
+
function actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors }) {
|
|
280
310
|
const findings = []
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
311
|
+
const operatedRepos = new Set(managedRepos(project).map((repo) => repo.name))
|
|
312
|
+
const ownedRepos = Object.entries(policy.repos ?? {}).filter(([name, repo]) =>
|
|
313
|
+
operatedRepos.has(name) && repo?.kind === 'private_domain' && repo.ownerActor)
|
|
314
|
+
const explicit = resolveExplicitActor({ policy, actor, env: process.env })
|
|
315
|
+
if (explicit?.reason) return [finding({ severity: 'error', code: 'actor-resolution-refused', message: explicit.reason })]
|
|
316
|
+
// Shared-only work needs no derived identity and must not invoke Git or gh.
|
|
317
|
+
if (!ownedRepos.length) return findings
|
|
318
|
+
const current = explicit || resolveCurrentActor({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution })
|
|
319
|
+
const severity = forceActorErrors ? 'error' : severityFor(policy)
|
|
320
|
+
for (const [repoName, repo] of ownedRepos) {
|
|
286
321
|
if (!current.actorId) {
|
|
287
|
-
findings.push(finding({ severity, code: 'private-domain-actor-unverified', repo: repoName,
|
|
322
|
+
findings.push(finding({ severity, code: 'private-domain-actor-unverified', repo: repoName,
|
|
323
|
+
message: `${repoName}: could not verify local actor for private domain repo owned by ${repo.ownerActor}${current.reason ? ` (${current.reason})` : ''}` }))
|
|
288
324
|
} else if (current.actorId !== repo.ownerActor) {
|
|
289
325
|
findings.push(finding({ severity, code: 'private-domain-actor-mismatch', repo: repoName, message: `${repoName}: private domain repo is owned by ${repo.ownerActor}, but current actor is ${current.actorId}` }))
|
|
290
326
|
}
|
|
@@ -292,11 +328,11 @@ function actorFindings({ policy, project, actor }) {
|
|
|
292
328
|
return findings
|
|
293
329
|
}
|
|
294
330
|
|
|
295
|
-
export function stagedPathsForProject(project) {
|
|
331
|
+
export function stagedPathsForProject(project, { gitExecutable = 'git' } = {}) {
|
|
296
332
|
const paths = []
|
|
297
333
|
for (const repo of managedRepos(project)) {
|
|
298
334
|
if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) continue
|
|
299
|
-
const result = spawnSync(
|
|
335
|
+
const result = spawnSync(gitExecutable, ['-C', repo.path, 'diff', '--cached', '--name-only', '--diff-filter=ACMR'], { encoding: 'utf8', env: sanitizedGitEnvironment() })
|
|
300
336
|
if (result.status !== 0) continue
|
|
301
337
|
for (const rel of result.stdout.split('\n').map((line) => line.trim()).filter(Boolean)) {
|
|
302
338
|
paths.push({ repo: repo.name, repoRoot: repo.path, path: normalize(rel) })
|
|
@@ -308,7 +344,7 @@ export function stagedPathsForProject(project) {
|
|
|
308
344
|
function forbiddenPathFindings({ policy, stagedPaths }) {
|
|
309
345
|
const patterns = [...DEFAULT_FORBIDDEN_PATHS, ...asArray(policy.forbiddenPaths)]
|
|
310
346
|
const findings = []
|
|
311
|
-
const severity =
|
|
347
|
+
const severity = 'error'
|
|
312
348
|
for (const item of stagedPaths) {
|
|
313
349
|
const matched = patterns.find((pattern) => matchesPathPattern(pattern, item.path))
|
|
314
350
|
if (matched) {
|
|
@@ -390,13 +426,20 @@ export function semanticChangesInFile(file) {
|
|
|
390
426
|
return changes
|
|
391
427
|
}
|
|
392
428
|
|
|
393
|
-
function semanticDiffFindings({ policy, project }) {
|
|
429
|
+
function semanticDiffFindings({ policy, project, gitExecutable = 'git' }) {
|
|
394
430
|
const findings = []
|
|
395
|
-
const severity =
|
|
431
|
+
const severity = 'error'
|
|
396
432
|
for (const repo of managedRepos(project)) {
|
|
397
|
-
if (!repo.path || !fs.existsSync(path.join(repo.path, '.git')))
|
|
398
|
-
|
|
399
|
-
|
|
433
|
+
if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) {
|
|
434
|
+
findings.push(unscannableRepoFinding(repo, 'configured path is absent or is not a Git checkout'))
|
|
435
|
+
continue
|
|
436
|
+
}
|
|
437
|
+
const result = spawnSync(gitExecutable, ['-C', repo.path, 'diff', '--cached', '--unified=0', '--', '*.md', '*.kg.json'], { encoding: 'utf8', env: sanitizedGitEnvironment() })
|
|
438
|
+
if (result.status !== 0) {
|
|
439
|
+
findings.push(unscannableRepoFinding(repo, 'git diff --cached failed'))
|
|
440
|
+
continue
|
|
441
|
+
}
|
|
442
|
+
if (!result.stdout.trim()) continue
|
|
400
443
|
for (const file of diffFileSections(result.stdout)) {
|
|
401
444
|
const changes = semanticChangesInFile(file)
|
|
402
445
|
if (!changes.length) continue
|
|
@@ -419,27 +462,32 @@ function semanticDiffFindings({ policy, project }) {
|
|
|
419
462
|
return findings
|
|
420
463
|
}
|
|
421
464
|
|
|
422
|
-
function
|
|
465
|
+
function stagedContentFindings({ policy, project, gitExecutable = 'git' }) {
|
|
423
466
|
const rules = resolveContentRules(policy)
|
|
424
467
|
const exceptions = asArray(policy?.contentRuleExceptions)
|
|
425
468
|
const findings = []
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
469
|
+
let totalBytes = 0
|
|
470
|
+
for (const repo of managedRepos(project)) {
|
|
471
|
+
if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) {
|
|
472
|
+
findings.push(unscannableRepoFinding(repo, 'configured path is absent or is not a Git checkout'))
|
|
473
|
+
continue
|
|
474
|
+
}
|
|
475
|
+
const result = scanStagedRepository({ repoRoot: repo.path, rules, exceptions, repo: repo.name, gitExecutable })
|
|
476
|
+
findings.push(...result.findings, ...result.diagnostics)
|
|
477
|
+
totalBytes += result.bytes
|
|
478
|
+
if (totalBytes > CHECK_AGGREGATE_MAX_BYTES) {
|
|
479
|
+
findings.push(finding({
|
|
480
|
+
severity: 'error',
|
|
481
|
+
code: 'content-scan-incomplete',
|
|
482
|
+
repo: repo.name,
|
|
483
|
+
message: `staged evidence exceeds the ${CHECK_AGGREGATE_MAX_BYTES}-byte aggregate limit`,
|
|
484
|
+
}))
|
|
485
|
+
break
|
|
431
486
|
}
|
|
432
487
|
}
|
|
433
488
|
return findings
|
|
434
489
|
}
|
|
435
490
|
|
|
436
|
-
function stagedContentFindings({ policy, project }) {
|
|
437
|
-
const files = managedRepos(project)
|
|
438
|
-
.filter((repo) => repo.path && fs.existsSync(path.join(repo.path, '.git')))
|
|
439
|
-
.map((repo) => ({ repo: repo.name, files: parseAddedContent(stagedDiff(repo.path)) }))
|
|
440
|
-
return contentRuleFindings({ policy, project, files })
|
|
441
|
-
}
|
|
442
|
-
|
|
443
491
|
function realPath(value) {
|
|
444
492
|
try {
|
|
445
493
|
return fs.realpathSync(path.resolve(value))
|
|
@@ -467,7 +515,7 @@ function repoForCwd(project, cwd) {
|
|
|
467
515
|
* which reports without blocking, so an accepted usage elsewhere in the repo can
|
|
468
516
|
* never strand unrelated work on the machine.
|
|
469
517
|
*/
|
|
470
|
-
export function checkPushContent({ project, policy, repo, updates, cwd = process.cwd() } = {}) {
|
|
518
|
+
export function checkPushContent({ project, policy, repo, updates, cwd = process.cwd(), gitRunner = spawnSync } = {}) {
|
|
471
519
|
const target = repo ?? repoForCwd(project, cwd)
|
|
472
520
|
if (!target) {
|
|
473
521
|
// Fail closed. This hook is only installed into managed repos, so failing to
|
|
@@ -480,20 +528,41 @@ export function checkPushContent({ project, policy, repo, updates, cwd = process
|
|
|
480
528
|
})
|
|
481
529
|
return { ok: false, repo: null, findings: [unresolved], errors: [unresolved], warnings: [] }
|
|
482
530
|
}
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
const findings =
|
|
531
|
+
const rules = resolveContentRules(policy)
|
|
532
|
+
const exceptions = asArray(policy?.contentRuleExceptions)
|
|
533
|
+
const findings = []
|
|
534
|
+
let totalBytes = 0
|
|
535
|
+
for (const update of updates) {
|
|
536
|
+
const result = scanPushUpdate({ repoRoot: target.path, update, rules, exceptions, repo: target.name, gitRunner })
|
|
537
|
+
findings.push(...result.findings, ...result.diagnostics)
|
|
538
|
+
totalBytes += result.bytes
|
|
539
|
+
if (totalBytes > CHECK_AGGREGATE_MAX_BYTES) {
|
|
540
|
+
findings.push(finding({
|
|
541
|
+
severity: 'error',
|
|
542
|
+
code: 'content-scan-incomplete',
|
|
543
|
+
repo: target.name,
|
|
544
|
+
message: `push evidence exceeds the ${CHECK_AGGREGATE_MAX_BYTES}-byte aggregate limit`,
|
|
545
|
+
}))
|
|
546
|
+
break
|
|
547
|
+
}
|
|
548
|
+
}
|
|
486
549
|
const errors = findings.filter((item) => item.severity === 'error')
|
|
487
550
|
return { ok: errors.length === 0, repo: target.name, updates, findings, errors, warnings: findings.filter((item) => item.severity !== 'error') }
|
|
488
551
|
}
|
|
489
552
|
|
|
490
|
-
export function auditContentRules({ project, policy } = {}) {
|
|
553
|
+
export function auditContentRules({ project, policy, source = 'working-tree' } = {}) {
|
|
491
554
|
const rules = resolveContentRules(policy)
|
|
492
555
|
const exceptions = asArray(policy?.contentRuleExceptions)
|
|
493
556
|
const findings = []
|
|
557
|
+
const diagnostics = []
|
|
494
558
|
for (const repo of managedRepos(project)) {
|
|
495
|
-
if (!repo.path || !fs.existsSync(path.join(repo.path, '.git')))
|
|
496
|
-
|
|
559
|
+
if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) {
|
|
560
|
+
diagnostics.push(unscannableRepoFinding(repo, 'configured path is absent or is not a Git checkout'))
|
|
561
|
+
continue
|
|
562
|
+
}
|
|
563
|
+
const result = scanTree(repo.path, { rules, exceptions, repo: repo.name, source })
|
|
564
|
+
findings.push(...result.findings)
|
|
565
|
+
diagnostics.push(...result.diagnostics)
|
|
497
566
|
}
|
|
498
567
|
const accepted = []
|
|
499
568
|
for (const repo of managedRepos(project)) {
|
|
@@ -502,7 +571,16 @@ export function auditContentRules({ project, policy } = {}) {
|
|
|
502
571
|
accepted.push({ repo: repo.name, rule: exception.rule, paths: exception.paths, reason: exception.reason })
|
|
503
572
|
}
|
|
504
573
|
}
|
|
505
|
-
return { schema: BOUNDARY_POLICY_SCHEMA, findings, accepted }
|
|
574
|
+
return { schema: BOUNDARY_POLICY_SCHEMA, source, findings, diagnostics, accepted }
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function unscannableRepoFinding(repo, reason) {
|
|
578
|
+
return finding({
|
|
579
|
+
severity: 'error',
|
|
580
|
+
code: 'repo-unscannable',
|
|
581
|
+
repo: repo?.name ?? null,
|
|
582
|
+
message: `${repo?.name ?? '(unnamed repo)'} cannot be scanned: ${reason}`,
|
|
583
|
+
})
|
|
506
584
|
}
|
|
507
585
|
|
|
508
586
|
function promotionRecords(project, policy) {
|
|
@@ -557,24 +635,22 @@ function promotionFindings({ policy, project, graph }) {
|
|
|
557
635
|
return findings
|
|
558
636
|
}
|
|
559
637
|
|
|
560
|
-
export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnly = false, actor = null } = {}) {
|
|
638
|
+
export function checkBoundaryPolicy({ project, policy, staged = false, stagedOnly = false, actor = null, gitExecutable = 'git', allowNetworkActorResolution = true, allowHistoryActorResolution = false, forceActorErrors = false } = {}) {
|
|
561
639
|
const validationErrors = validateBoundaryPolicy(policy, project)
|
|
562
640
|
let graph = null
|
|
563
|
-
const findings = validationErrors.map((message) => finding({ severity:
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
findings.push(...stagedContentFindings({ policy, project }))
|
|
577
|
-
}
|
|
641
|
+
const findings = validationErrors.map((message) => finding({ severity: 'error', code: 'boundary-policy-invalid', message }))
|
|
642
|
+
if (!stagedOnly) {
|
|
643
|
+
graph = buildGraph(project)
|
|
644
|
+
findings.push(...graph.errors.map((message) => finding({ severity: 'error', code: 'knowledge-graph-invalid', message })))
|
|
645
|
+
for (const node of graph.nodes ?? []) findings.push(...nodePlacementFindings({ node, policy }))
|
|
646
|
+
findings.push(...promotionFindings({ policy, project, graph }))
|
|
647
|
+
}
|
|
648
|
+
findings.push(...actorFindings({ policy, project, actor, gitExecutable, allowNetworkActorResolution, allowHistoryActorResolution, forceActorErrors }))
|
|
649
|
+
if (staged) {
|
|
650
|
+
const stagedPaths = stagedPathsForProject(project, { gitExecutable })
|
|
651
|
+
findings.push(...forbiddenPathFindings({ policy, stagedPaths }))
|
|
652
|
+
findings.push(...semanticDiffFindings({ policy, project, gitExecutable }))
|
|
653
|
+
findings.push(...stagedContentFindings({ policy, project, gitExecutable }))
|
|
578
654
|
}
|
|
579
655
|
const errors = findings.filter((item) => item.severity === 'error')
|
|
580
656
|
const warnings = findings.filter((item) => item.severity !== 'error')
|
|
@@ -616,12 +692,12 @@ export function installBoundaryHooks({ project, force = false } = {}) {
|
|
|
616
692
|
const skipped = []
|
|
617
693
|
for (const repo of managedRepos(project)) {
|
|
618
694
|
if (!repo.path || !fs.existsSync(path.join(repo.path, '.git'))) continue
|
|
619
|
-
const hooksDir =
|
|
695
|
+
const hooksDir = gitHooksDir(repo.path)
|
|
620
696
|
fs.mkdirSync(hooksDir, { recursive: true })
|
|
621
697
|
for (const hookName of ['pre-commit', 'pre-push']) {
|
|
622
698
|
const hookPath = path.join(hooksDir, hookName)
|
|
623
699
|
const existing = fs.existsSync(hookPath) ? fs.readFileSync(hookPath, 'utf8') : ''
|
|
624
|
-
const script =
|
|
700
|
+
const script = boundaryHookScript(project.configPath, hookName, repo.path)
|
|
625
701
|
if (existing && !existing.includes('MNSTRY_ATELIER_BOUNDARY_GUARD') && !force) {
|
|
626
702
|
const sidecar = `${hookPath}.mnstry-atelier-boundary`
|
|
627
703
|
fs.writeFileSync(sidecar, script)
|
|
@@ -637,8 +713,32 @@ export function installBoundaryHooks({ project, force = false } = {}) {
|
|
|
637
713
|
return { installed, skipped }
|
|
638
714
|
}
|
|
639
715
|
|
|
640
|
-
function
|
|
641
|
-
const
|
|
716
|
+
function gitHooksDir(repoPath) {
|
|
717
|
+
const result = spawnSync('git', ['-C', repoPath, 'rev-parse', '--git-path', 'hooks'], { encoding: 'utf8' })
|
|
718
|
+
if (result.status !== 0 || !result.stdout.trim()) throw new Error(`cannot resolve Git hooks directory for ${repoPath}`)
|
|
719
|
+
const value = result.stdout.trim()
|
|
720
|
+
return path.isAbsolute(value) ? value : path.resolve(repoPath, value)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function shellSingleQuote(value) {
|
|
724
|
+
const text = String(value ?? '')
|
|
725
|
+
if (!text || /[\0\r\n]/.test(text)) throw new Error('project config path contains unsupported control characters')
|
|
726
|
+
return `'${text.replaceAll("'", `'"'"'`)}'`
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function boundaryHookScript(projectConfigPath, hookName, repoPath) {
|
|
730
|
+
const relativeConfigPath = projectConfigPath && repoPath ? path.relative(repoPath, projectConfigPath) : null
|
|
731
|
+
const configLivesInRepo = relativeConfigPath && relativeConfigPath !== '..' && !relativeConfigPath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativeConfigPath)
|
|
732
|
+
const portableRelativeConfig = configLivesInRepo ? relativeConfigPath.replaceAll(path.sep, '/') : null
|
|
733
|
+
const configSetup = configLivesInRepo
|
|
734
|
+
? `ATELIER_HOOK_REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
735
|
+
ATELIER_HOOK_PROJECT_CONFIG="$ATELIER_HOOK_REPO_ROOT"/${shellSingleQuote(portableRelativeConfig)}`
|
|
736
|
+
: ''
|
|
737
|
+
const config = configLivesInRepo
|
|
738
|
+
? '--project-config="$ATELIER_HOOK_PROJECT_CONFIG"'
|
|
739
|
+
: projectConfigPath
|
|
740
|
+
? `--project-config=${shellSingleQuote(projectConfigPath)}`
|
|
741
|
+
: ''
|
|
642
742
|
// pre-push reads the ref updates git writes to stdin and judges only that range;
|
|
643
743
|
// pre-commit judges the staged diff. Neither scans the whole tree — that view is
|
|
644
744
|
// `atelier boundary audit`, which reports without blocking.
|
|
@@ -646,6 +746,7 @@ function hookScript(projectConfigPath, hookName) {
|
|
|
646
746
|
return `#!/usr/bin/env bash
|
|
647
747
|
# MNSTRY_ATELIER_BOUNDARY_GUARD ${hookName}
|
|
648
748
|
set -euo pipefail
|
|
749
|
+
${configSetup}
|
|
649
750
|
if command -v atelier >/dev/null 2>&1; then
|
|
650
751
|
exec atelier ${invocation}
|
|
651
752
|
elif [ -x "./node_modules/.bin/atelier" ]; then
|
|
@@ -663,7 +764,7 @@ fi
|
|
|
663
764
|
`
|
|
664
765
|
}
|
|
665
766
|
|
|
666
|
-
export function runBoundaryPushCheckCommand(argv = process.argv.slice(2), { stdin =
|
|
767
|
+
export function runBoundaryPushCheckCommand(argv = process.argv.slice(2), { stdin = readBoundaryPushInput() } = {}) {
|
|
667
768
|
const args = parseArgs(argv)
|
|
668
769
|
const project = commandProject({ argv })
|
|
669
770
|
const loaded = loadBoundaryPolicy(project)
|
|
@@ -671,12 +772,23 @@ export function runBoundaryPushCheckCommand(argv = process.argv.slice(2), { stdi
|
|
|
671
772
|
console.error(loaded.errors.join('\n'))
|
|
672
773
|
process.exit(1)
|
|
673
774
|
}
|
|
674
|
-
const
|
|
675
|
-
|
|
775
|
+
const input = stdin && typeof stdin === 'object' && Object.hasOwn(stdin, 'ok')
|
|
776
|
+
? stdin
|
|
777
|
+
: { ok: true, text: String(stdin ?? '') }
|
|
778
|
+
if (!input.ok) {
|
|
779
|
+
console.error('[boundary:push-check] could not read pre-push ref updates')
|
|
780
|
+
process.exit(1)
|
|
781
|
+
}
|
|
782
|
+
const parsed = parsePushRefInput(input.text)
|
|
783
|
+
if (!parsed.ok) {
|
|
784
|
+
console.error(`[boundary:push-check] invalid pre-push input: ${parsed.issues.join('; ')}`)
|
|
785
|
+
process.exit(1)
|
|
786
|
+
}
|
|
787
|
+
if (parsed.kind === 'empty') {
|
|
676
788
|
console.log('[boundary:push-check] no ref updates on stdin · nothing to judge')
|
|
677
789
|
process.exit(0)
|
|
678
790
|
}
|
|
679
|
-
const report = checkPushContent({ project, policy: loaded.policy, updates, cwd: process.cwd() })
|
|
791
|
+
const report = checkPushContent({ project, policy: loaded.policy, updates: parsed.updates, cwd: process.cwd() })
|
|
680
792
|
if (args.json) {
|
|
681
793
|
console.log(JSON.stringify(report, null, 2))
|
|
682
794
|
} else {
|
|
@@ -691,6 +803,10 @@ export function runBoundaryPushCheckCommand(argv = process.argv.slice(2), { stdi
|
|
|
691
803
|
process.exit(report.ok ? 0 : 1)
|
|
692
804
|
}
|
|
693
805
|
|
|
806
|
+
export function resolveBoundaryAuditSource(args = {}) {
|
|
807
|
+
return args.head ? 'head' : firstString(args.source) || 'working-tree'
|
|
808
|
+
}
|
|
809
|
+
|
|
694
810
|
export function runBoundaryAuditCommand(argv = process.argv.slice(2)) {
|
|
695
811
|
const args = parseArgs(argv)
|
|
696
812
|
const project = commandProject({ argv })
|
|
@@ -699,23 +815,29 @@ export function runBoundaryAuditCommand(argv = process.argv.slice(2)) {
|
|
|
699
815
|
console.error(loaded.errors.join('\n'))
|
|
700
816
|
process.exit(1)
|
|
701
817
|
}
|
|
702
|
-
const
|
|
818
|
+
const source = resolveBoundaryAuditSource(args)
|
|
819
|
+
if (!['working-tree', 'head'].includes(source)) {
|
|
820
|
+
console.error('boundary audit --source must be working-tree or head')
|
|
821
|
+
process.exit(1)
|
|
822
|
+
}
|
|
823
|
+
const report = auditContentRules({ project, policy: loaded.policy, source })
|
|
703
824
|
if (args.json) {
|
|
704
825
|
console.log(JSON.stringify(report, null, 2))
|
|
705
826
|
} else {
|
|
706
|
-
console.log(`[boundary:audit] ${report.findings.length} content-rule matches
|
|
827
|
+
console.log(`[boundary:audit] ${report.findings.length} content-rule matches · ${report.diagnostics.length} incomplete reads · ${report.accepted.length} declared exceptions · source ${report.source}`)
|
|
707
828
|
for (const item of report.findings.slice(0, 100)) console.log(`${item.severity} ${item.rule}: ${item.message}`)
|
|
829
|
+
for (const item of report.diagnostics.slice(0, 100)) console.log(`${item.severity} ${item.code}: ${item.message}`)
|
|
708
830
|
for (const item of report.accepted) console.log(`accepted ${item.rule} in ${item.repo} (${item.paths.join(', ')}): ${item.reason}`)
|
|
709
831
|
}
|
|
710
832
|
// Reporting only. A pre-existing accepted usage must never block unrelated work.
|
|
711
833
|
process.exit(0)
|
|
712
834
|
}
|
|
713
835
|
|
|
714
|
-
function
|
|
836
|
+
export function readBoundaryPushInput(reader = () => fs.readFileSync(0, 'utf8')) {
|
|
715
837
|
try {
|
|
716
|
-
return
|
|
838
|
+
return { ok: true, text: reader() }
|
|
717
839
|
} catch {
|
|
718
|
-
return ''
|
|
840
|
+
return { ok: false, text: '', error: 'stdin-read-failed' }
|
|
719
841
|
}
|
|
720
842
|
}
|
|
721
843
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const [, , requestedScript, ...args] = process.argv
|
|
7
|
+
|
|
8
|
+
function renderError(error) {
|
|
9
|
+
const debug = process.env.ATELIER_DEBUG === '1'
|
|
10
|
+
if (debug) {
|
|
11
|
+
console.error(error?.stack || String(error))
|
|
12
|
+
return Number.isInteger(error?.exitCode) ? error.exitCode : 1
|
|
13
|
+
}
|
|
14
|
+
if (typeof error?.code === 'string' && error.code) {
|
|
15
|
+
console.error(`[${error.code}] ${error.message}`)
|
|
16
|
+
if (error.hint) console.error(`Next: ${error.hint}`)
|
|
17
|
+
return Number.isInteger(error.exitCode) ? error.exitCode : 2
|
|
18
|
+
}
|
|
19
|
+
console.error('[internal-error] command failed without a safe diagnostic')
|
|
20
|
+
console.error('Next: rerun with ATELIER_DEBUG=1 to inspect the stack locally.')
|
|
21
|
+
return 1
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!requestedScript) {
|
|
25
|
+
console.error('[command-missing] no command module was selected')
|
|
26
|
+
process.exit(2)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const scriptPath = path.resolve(requestedScript)
|
|
30
|
+
process.argv = [process.execPath, scriptPath, ...args]
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await import(pathToFileURL(scriptPath).href)
|
|
34
|
+
} catch (error) {
|
|
35
|
+
process.exitCode = renderError(error)
|
|
36
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Shared project-location syntax. Command-specific arguments remain owned by
|
|
2
|
+
// their command; this parser does not make unknown options globally valid.
|
|
3
|
+
export const PROJECT_OPTION_SPEC = Object.freeze({
|
|
4
|
+
project: 'value',
|
|
5
|
+
'project-config': 'value',
|
|
6
|
+
'repo-path': 'value',
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
function invalid(message) {
|
|
10
|
+
const error = new Error(message)
|
|
11
|
+
error.code = 'project-option-invalid'
|
|
12
|
+
error.exitCode = 2
|
|
13
|
+
throw error
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function projectOptionAt(argv, index, configArgPrefix = '--project-config=') {
|
|
17
|
+
const arg = argv[index]
|
|
18
|
+
if (typeof arg !== 'string' || !arg.startsWith('--')) return null
|
|
19
|
+
const name = arg.slice(2).split('=')[0]
|
|
20
|
+
const custom = configArgPrefix !== '--project-config=' && arg.startsWith(configArgPrefix)
|
|
21
|
+
if (!custom && !Object.hasOwn(PROJECT_OPTION_SPEC, name)) return null
|
|
22
|
+
const inline = custom || arg.includes('=')
|
|
23
|
+
const value = custom ? arg.slice(configArgPrefix.length) : inline ? arg.slice(arg.indexOf('=') + 1) : argv[index + 1]
|
|
24
|
+
if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
|
|
25
|
+
invalid(`--${custom ? 'project-config' : name} requires a non-empty value`)
|
|
26
|
+
}
|
|
27
|
+
const option = custom ? 'project-config' : name
|
|
28
|
+
if (option === 'repo-path') {
|
|
29
|
+
const separator = value.indexOf('=')
|
|
30
|
+
if (separator < 1 || !value.slice(0, separator).trim() || !value.slice(separator + 1).trim()) {
|
|
31
|
+
invalid('--repo-path requires NAME=PATH')
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { name: option, value, nextIndex: index + (inline ? 1 : 2) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseProjectOptions(argv = [], configArgPrefix = '--project-config=') {
|
|
38
|
+
let project = null
|
|
39
|
+
const repoPaths = new Map()
|
|
40
|
+
const remaining = []
|
|
41
|
+
for (let index = 0; index < argv.length;) {
|
|
42
|
+
const option = projectOptionAt(argv, index, configArgPrefix)
|
|
43
|
+
if (!option) {
|
|
44
|
+
remaining.push(argv[index++])
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
if (option.name === 'repo-path') {
|
|
48
|
+
const separator = option.value.indexOf('=')
|
|
49
|
+
repoPaths.set(option.value.slice(0, separator).trim(), option.value.slice(separator + 1))
|
|
50
|
+
} else if (project === null) {
|
|
51
|
+
// Preserve the resolver's first project selection and last named repo
|
|
52
|
+
// override behavior, including aliases in the same invocation.
|
|
53
|
+
project = option.value.trim()
|
|
54
|
+
}
|
|
55
|
+
index = option.nextIndex
|
|
56
|
+
}
|
|
57
|
+
return { project, repoPaths, remaining }
|
|
58
|
+
}
|