@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
package/src/project/config.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
|
|
5
|
+
import { parseProjectOptions } from '../cli/project-options.mjs'
|
|
4
6
|
|
|
5
7
|
export const PROJECT_CONFIG_ARG_PREFIX = '--project-config='
|
|
6
8
|
export const PROJECT_CONFIG_ENV = 'MNSTRY_ATELIER_PROJECT_CONFIG'
|
|
@@ -17,6 +19,16 @@ export const LOCAL_OVERLAY_FILES = ['atelier.local.json', 'atelier.workspace.loc
|
|
|
17
19
|
// a way to silence checks on a repo you do manage.
|
|
18
20
|
export const EXTERNAL_REPO_KIND = 'external'
|
|
19
21
|
|
|
22
|
+
export class AtelierDiagnosticError extends Error {
|
|
23
|
+
constructor(code, message, { hint = null, exitCode = 2, cause = null } = {}) {
|
|
24
|
+
super(message, cause ? { cause } : undefined)
|
|
25
|
+
this.name = 'AtelierDiagnosticError'
|
|
26
|
+
this.code = code
|
|
27
|
+
this.hint = hint
|
|
28
|
+
this.exitCode = exitCode
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
export const isExternalRepo = (repo) => firstString(repo?.kind) === EXTERNAL_REPO_KIND
|
|
21
33
|
|
|
22
34
|
// Kept here rather than imported from repo-identity.mjs so config validation stays
|
|
@@ -59,10 +71,19 @@ export function resolvePathValue(value, baseDir) {
|
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
export function readJson(file) {
|
|
74
|
+
if (!fs.existsSync(file)) {
|
|
75
|
+
const graphArtifact = path.basename(file) === 'knowledge.graph.json'
|
|
76
|
+
throw new AtelierDiagnosticError('artifact-missing', `required JSON artifact not found: ${file}`, {
|
|
77
|
+
hint: graphArtifact ? 'Run atelier graph with this project config, then retry.' : 'Create or regenerate the named artifact, then retry.',
|
|
78
|
+
})
|
|
79
|
+
}
|
|
62
80
|
try {
|
|
63
81
|
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
64
82
|
} catch (error) {
|
|
65
|
-
throw new
|
|
83
|
+
throw new AtelierDiagnosticError('json-invalid', `JSON is malformed at ${file}: ${error.message}`, {
|
|
84
|
+
hint: 'Repair or regenerate the named JSON file, then retry.',
|
|
85
|
+
cause: error,
|
|
86
|
+
})
|
|
66
87
|
}
|
|
67
88
|
}
|
|
68
89
|
|
|
@@ -75,14 +96,7 @@ export function writeJson(file, value) {
|
|
|
75
96
|
}
|
|
76
97
|
|
|
77
98
|
export function projectConfigArg(argv = process.argv.slice(2), prefix = PROJECT_CONFIG_ARG_PREFIX) {
|
|
78
|
-
|
|
79
|
-
const arg = argv[index]
|
|
80
|
-
if (typeof arg !== 'string') continue
|
|
81
|
-
if (arg.startsWith(prefix)) return arg.slice(prefix.length).trim() || null
|
|
82
|
-
if ((arg === '--project-config' || arg === '--project') && argv[index + 1]) return String(argv[index + 1]).trim() || null
|
|
83
|
-
if (arg.startsWith('--project=')) return arg.slice('--project='.length).trim() || null
|
|
84
|
-
}
|
|
85
|
-
return null
|
|
99
|
+
return parseProjectOptions(argv, prefix).project
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
export function stripProjectConfigArgs(argv = [], prefix = PROJECT_CONFIG_ARG_PREFIX) {
|
|
@@ -90,9 +104,30 @@ export function stripProjectConfigArgs(argv = [], prefix = PROJECT_CONFIG_ARG_PR
|
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
export function readProjectConfig(configPath) {
|
|
93
|
-
|
|
107
|
+
let doc
|
|
108
|
+
try {
|
|
109
|
+
doc = readJson(configPath)
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof AtelierDiagnosticError) {
|
|
112
|
+
throw new AtelierDiagnosticError(
|
|
113
|
+
error.code === 'artifact-missing' ? 'project-config-missing' : 'project-config-json-invalid',
|
|
114
|
+
error.code === 'artifact-missing'
|
|
115
|
+
? `atelier project config not found: ${configPath}`
|
|
116
|
+
: `atelier project config is malformed: ${configPath}`,
|
|
117
|
+
{
|
|
118
|
+
hint: error.code === 'artifact-missing'
|
|
119
|
+
? 'Pass --project PATH to a tracked atelier.project.json file.'
|
|
120
|
+
: 'Repair the JSON syntax, then run atelier config check with the same --project path.',
|
|
121
|
+
cause: error,
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
throw error
|
|
126
|
+
}
|
|
94
127
|
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
95
|
-
throw new
|
|
128
|
+
throw new AtelierDiagnosticError('project-config-shape-invalid', `Atelier project config must be a JSON object: ${configPath}`, {
|
|
129
|
+
hint: 'Repair the document shape, then run atelier config check with the same --project path.',
|
|
130
|
+
})
|
|
96
131
|
}
|
|
97
132
|
return doc
|
|
98
133
|
}
|
|
@@ -130,22 +165,7 @@ export function readLocalOverlay({ configDir, env = process.env, cwd = process.c
|
|
|
130
165
|
}
|
|
131
166
|
|
|
132
167
|
export function parseRepoPathOverrides(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
133
|
-
|
|
134
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
135
|
-
const arg = argv[index]
|
|
136
|
-
let value = null
|
|
137
|
-
if (arg === '--repo-path' && argv[index + 1]) {
|
|
138
|
-
value = argv[index + 1]
|
|
139
|
-
index += 1
|
|
140
|
-
} else if (typeof arg === 'string' && arg.startsWith('--repo-path=')) {
|
|
141
|
-
value = arg.slice('--repo-path='.length)
|
|
142
|
-
}
|
|
143
|
-
if (!value || !value.includes('=')) continue
|
|
144
|
-
const [name, ...rest] = value.split('=')
|
|
145
|
-
const repoPath = rest.join('=')
|
|
146
|
-
if (firstString(name) && firstString(repoPath)) out.set(name.trim(), resolvePathValue(repoPath, cwd))
|
|
147
|
-
}
|
|
148
|
-
return out
|
|
168
|
+
return new Map([...parseProjectOptions(argv).repoPaths].map(([name, value]) => [name, resolvePathValue(value, cwd)]))
|
|
149
169
|
}
|
|
150
170
|
|
|
151
171
|
function overlayRepoPath(overlay, repoName) {
|
|
@@ -155,25 +175,25 @@ function overlayRepoPath(overlay, repoName) {
|
|
|
155
175
|
return firstString(direct, repo.path, repo.localPath)
|
|
156
176
|
}
|
|
157
177
|
|
|
158
|
-
function resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay, cliRepoPaths }) {
|
|
178
|
+
function resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay, cliRepoPaths, gitExecutable, env }) {
|
|
159
179
|
if (repoName && cliRepoPaths.has(repoName)) return cliRepoPaths.get(repoName)
|
|
160
180
|
const overlayPath = overlayRepoPath(overlay, repoName)
|
|
161
181
|
const fromOverlay = resolvePathValue(overlayPath, configDir)
|
|
162
182
|
if (fromOverlay) return fromOverlay
|
|
163
183
|
const fromConfig = resolvePathValue(repo.path, configDir) || resolvePathValue(repo.path, workspaceRoot)
|
|
164
184
|
if (fromConfig) return fromConfig
|
|
165
|
-
return discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot })
|
|
185
|
+
return discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot, gitExecutable, env })
|
|
166
186
|
}
|
|
167
187
|
|
|
168
|
-
function repoPathSource({ repo, repoName, overlay, cliRepoPaths, workspaceRoot, configDir }) {
|
|
188
|
+
function repoPathSource({ repo, repoName, overlay, cliRepoPaths, workspaceRoot, configDir, gitExecutable, env }) {
|
|
169
189
|
if (repoName && cliRepoPaths.has(repoName)) return 'cli'
|
|
170
190
|
if (overlayRepoPath(overlay, repoName)) return 'local-overlay'
|
|
171
191
|
if (firstString(repo.path)) return 'tracked-config'
|
|
172
|
-
if (discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot })) return 'sibling-discovery'
|
|
192
|
+
if (discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot, gitExecutable, env })) return 'sibling-discovery'
|
|
173
193
|
return null
|
|
174
194
|
}
|
|
175
195
|
|
|
176
|
-
function discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot }) {
|
|
196
|
+
function discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot, gitExecutable = 'git', env = process.env }) {
|
|
177
197
|
if (!repoName) return null
|
|
178
198
|
const candidates = [
|
|
179
199
|
path.join(workspaceRoot, repoName),
|
|
@@ -182,14 +202,14 @@ function discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot }) {
|
|
|
182
202
|
]
|
|
183
203
|
for (const candidate of candidates) {
|
|
184
204
|
if (!fs.existsSync(candidate)) continue
|
|
185
|
-
if (repo.remote && !gitRemoteMatches(candidate, repo.remote)) continue
|
|
205
|
+
if (repo.remote && !gitRemoteMatches(candidate, repo.remote, { gitExecutable, env })) continue
|
|
186
206
|
return path.resolve(candidate)
|
|
187
207
|
}
|
|
188
208
|
return null
|
|
189
209
|
}
|
|
190
210
|
|
|
191
|
-
function gitRemoteMatches(repoPath, expected) {
|
|
192
|
-
const result = spawnSync(
|
|
211
|
+
function gitRemoteMatches(repoPath, expected, { gitExecutable = 'git', env = process.env } = {}) {
|
|
212
|
+
const result = spawnSync(gitExecutable, ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
|
|
193
213
|
if (result.status !== 0) return false
|
|
194
214
|
const actual = normalizeRemote(result.stdout.trim())
|
|
195
215
|
return actual === normalizeRemote(expected)
|
|
@@ -203,9 +223,9 @@ function normalizeRemote(value) {
|
|
|
203
223
|
.toLowerCase()
|
|
204
224
|
}
|
|
205
225
|
|
|
206
|
-
export function gitRemoteUrl(repoPath) {
|
|
226
|
+
export function gitRemoteUrl(repoPath, { gitExecutable = 'git', env = process.env } = {}) {
|
|
207
227
|
if (!repoPath || !fs.existsSync(repoPath)) return null
|
|
208
|
-
const result = spawnSync(
|
|
228
|
+
const result = spawnSync(gitExecutable, ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
|
|
209
229
|
return result.status === 0 ? firstString(result.stdout) : null
|
|
210
230
|
}
|
|
211
231
|
|
|
@@ -228,7 +248,9 @@ export function resolveProjectConfig({
|
|
|
228
248
|
env = process.env,
|
|
229
249
|
cwd = process.cwd(),
|
|
230
250
|
configArgPrefix = PROJECT_CONFIG_ARG_PREFIX,
|
|
251
|
+
gitExecutable = 'git',
|
|
231
252
|
configEnv = PROJECT_CONFIG_ENV,
|
|
253
|
+
writeLocalState = true,
|
|
232
254
|
defaults = {},
|
|
233
255
|
} = {}) {
|
|
234
256
|
const argPath = projectConfigArg(argv, configArgPrefix)
|
|
@@ -253,7 +275,9 @@ export function resolveProjectConfig({
|
|
|
253
275
|
missingConfigPath = configPath
|
|
254
276
|
configPath = null
|
|
255
277
|
} else {
|
|
256
|
-
throw new
|
|
278
|
+
throw new AtelierDiagnosticError('project-config-missing', `atelier project config not found: ${configPath || requestedPath}`, {
|
|
279
|
+
hint: 'Pass --project PATH to a tracked atelier.project.json file.',
|
|
280
|
+
})
|
|
257
281
|
}
|
|
258
282
|
}
|
|
259
283
|
|
|
@@ -331,7 +355,7 @@ export function resolveProjectConfig({
|
|
|
331
355
|
localOverlay,
|
|
332
356
|
repos: (Array.isArray(config.repos) ? config.repos : []).map((repo) => {
|
|
333
357
|
const repoName = firstString(repo.name) || (repo.path ? path.basename(repo.path) : null)
|
|
334
|
-
const repoPath = resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay: localOverlay.overlay, cliRepoPaths })
|
|
358
|
+
const repoPath = resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay: localOverlay.overlay, cliRepoPaths, gitExecutable, env })
|
|
335
359
|
const external = isExternalRepo(repo)
|
|
336
360
|
return {
|
|
337
361
|
...repo,
|
|
@@ -339,23 +363,25 @@ export function resolveProjectConfig({
|
|
|
339
363
|
path: repoPath,
|
|
340
364
|
external,
|
|
341
365
|
readBoundary: external ? null : firstString(repo.readBoundary) || 'team',
|
|
342
|
-
pathSource: repoPath ? repoPathSource({ repo, repoName, overlay: localOverlay.overlay, cliRepoPaths, workspaceRoot, configDir }) : null,
|
|
366
|
+
pathSource: repoPath ? repoPathSource({ repo, repoName, overlay: localOverlay.overlay, cliRepoPaths, workspaceRoot, configDir, gitExecutable, env }) : null,
|
|
343
367
|
}
|
|
344
368
|
}),
|
|
345
369
|
}
|
|
346
|
-
resolved.localState = ensureLocalState(resolved, { write:
|
|
370
|
+
resolved.localState = ensureLocalState(resolved, { write: writeLocalState, gitExecutable, env })
|
|
347
371
|
return resolved
|
|
348
372
|
}
|
|
349
373
|
|
|
350
|
-
export function commandProject({ argv = process.argv.slice(2), env = process.env, cwd = process.cwd() } = {}) {
|
|
351
|
-
const project = resolveProjectConfig({ argv, env, cwd })
|
|
374
|
+
export function commandProject({ argv = process.argv.slice(2), env = process.env, cwd = process.cwd(), gitExecutable = 'git', writeLocalState = true } = {}) {
|
|
375
|
+
const project = resolveProjectConfig({ argv, env, cwd, gitExecutable, writeLocalState })
|
|
352
376
|
// Fail closed at CLI entry, but only when a real config file was loaded; the
|
|
353
377
|
// defaults/no-file path resolves with an empty config that would spuriously
|
|
354
378
|
// fail document validation.
|
|
355
379
|
if (project.configPath != null) {
|
|
356
380
|
const configErrors = validateProjectConfigDoc(project.config)
|
|
357
381
|
if (configErrors.length) {
|
|
358
|
-
throw new
|
|
382
|
+
throw new AtelierDiagnosticError('project-config-contract-invalid', `invalid atelier project config at ${project.configPath}:\n${configErrors.join('\n')}`, {
|
|
383
|
+
hint: 'Fix the listed fields, then run atelier config check with the same --project path.',
|
|
384
|
+
})
|
|
359
385
|
}
|
|
360
386
|
}
|
|
361
387
|
if (project.schema !== PROJECT_CONFIG_SCHEMA) {
|
|
@@ -375,21 +401,36 @@ export function localStateRoot(projectOrDir) {
|
|
|
375
401
|
return path.join(configDir || process.cwd(), LOCAL_STATE_DIR)
|
|
376
402
|
}
|
|
377
403
|
|
|
378
|
-
function gitRootFor(dir) {
|
|
379
|
-
const result = spawnSync(
|
|
404
|
+
function gitRootFor(dir, { gitExecutable = 'git', env = process.env } = {}) {
|
|
405
|
+
const result = spawnSync(gitExecutable, ['-C', dir, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
|
|
380
406
|
return result.status === 0 ? result.stdout.trim() : null
|
|
381
407
|
}
|
|
382
408
|
|
|
383
|
-
function
|
|
384
|
-
const result = spawnSync(
|
|
409
|
+
function gitPrefixFor(dir, { gitExecutable = 'git', env = process.env } = {}) {
|
|
410
|
+
const result = spawnSync(gitExecutable, ['-C', dir, 'rev-parse', '--show-prefix'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
|
|
411
|
+
return result.status === 0 ? result.stdout.trim() : null
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function isIgnoredByGit(gitRoot, rel, { gitExecutable = 'git', env = process.env } = {}) {
|
|
415
|
+
const result = spawnSync(gitExecutable, ['-C', gitRoot, 'check-ignore', '-q', rel], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
|
|
385
416
|
return result.status === 0
|
|
386
417
|
}
|
|
387
418
|
|
|
388
|
-
export function ensureLocalState(project, { write = false } = {}) {
|
|
419
|
+
export function ensureLocalState(project, { write = false, gitExecutable = 'git', env = process.env } = {}) {
|
|
389
420
|
const root = localStateRoot(project)
|
|
390
|
-
const gitRoot = gitRootFor(project.configDir)
|
|
391
|
-
|
|
392
|
-
|
|
421
|
+
const gitRoot = gitRootFor(project.configDir, { gitExecutable, env })
|
|
422
|
+
// Git owns the repository-relative spelling. Deriving it by comparing
|
|
423
|
+
// filesystem paths breaks when Windows exposes the cwd through an 8.3 short
|
|
424
|
+
// name (RUNNER~1) but Git reports the same root through its long name.
|
|
425
|
+
const gitPrefix = gitRoot ? gitPrefixFor(project.configDir, { gitExecutable, env }) : null
|
|
426
|
+
const rel = gitRoot && gitPrefix !== null ? `${gitPrefix}${LOCAL_STATE_DIR}` : LOCAL_STATE_DIR
|
|
427
|
+
// A directory-only ignore rule such as `.atelier-local/` is not evaluated
|
|
428
|
+
// consistently by Git for an absent directory on every host. Probe a
|
|
429
|
+
// hypothetical child as well: it proves the directory rule before Atelier
|
|
430
|
+
// creates any local state, including on Git for Windows.
|
|
431
|
+
const ignored = gitRoot && gitPrefix !== null
|
|
432
|
+
? isIgnoredByGit(gitRoot, `${rel}/.atelier-ignore-probe`, { gitExecutable, env }) || isIgnoredByGit(gitRoot, `${rel}/`, { gitExecutable, env }) || isIgnoredByGit(gitRoot, rel, { gitExecutable, env })
|
|
433
|
+
: gitRoot === null
|
|
393
434
|
const report = {
|
|
394
435
|
root,
|
|
395
436
|
ignored,
|
|
@@ -443,6 +484,19 @@ function extErrors(value, label) {
|
|
|
443
484
|
.map(([key]) => `${label}.${key} must be an object; ext members are namespaced objects`)
|
|
444
485
|
}
|
|
445
486
|
|
|
487
|
+
// Hands one namespaced ext member to its structured consumer, uninterpreted.
|
|
488
|
+
// The project validator above deliberately does not look inside a member, so
|
|
489
|
+
// the consumer validates it against its own closed contract and fails closed.
|
|
490
|
+
// `present` separates "no such member" (the consumer's feature is simply not
|
|
491
|
+
// configured) from a member that exists with any value at all, including a
|
|
492
|
+
// malformed one, which the consumer must refuse rather than ignore.
|
|
493
|
+
export function projectExtMember(project, key) {
|
|
494
|
+
const ext = project?.config?.ext
|
|
495
|
+
if (ext == null) return { present: false, value: undefined }
|
|
496
|
+
if (typeof ext !== 'object' || Array.isArray(ext)) return { present: true, value: undefined }
|
|
497
|
+
return Object.hasOwn(ext, key) ? { present: true, value: ext[key] } : { present: false, value: undefined }
|
|
498
|
+
}
|
|
499
|
+
|
|
446
500
|
export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {}) {
|
|
447
501
|
const errors = []
|
|
448
502
|
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return ['project config must be a JSON object']
|
|
@@ -504,6 +558,7 @@ export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {})
|
|
|
504
558
|
}
|
|
505
559
|
if (alignment.appRepo != null && !firstString(alignment.appRepo)) errors.push('alignment.appRepo must be a non-empty string')
|
|
506
560
|
|
|
561
|
+
const repoNames = new Set()
|
|
507
562
|
for (const [index, repo] of (Array.isArray(doc.repos) ? doc.repos : []).entries()) {
|
|
508
563
|
if (!repo || typeof repo !== 'object' || Array.isArray(repo)) {
|
|
509
564
|
errors.push(`repos[${index}] must be an object`)
|
|
@@ -512,6 +567,11 @@ export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {})
|
|
|
512
567
|
errors.push(...unknownKeyErrors(repo, `repos[${index}]`, new Set(['name', 'path', 'readBoundary', 'role', 'kind', 'remote', 'identity', 'aliases', 'required', 'ext'])))
|
|
513
568
|
errors.push(...extErrors(repo.ext, `repos[${index}].ext`))
|
|
514
569
|
if (!firstString(repo.name)) errors.push(`repos[${index}].name is required`)
|
|
570
|
+
else {
|
|
571
|
+
const key = repo.name.toLowerCase()
|
|
572
|
+
if (repoNames.has(key)) errors.push(`repos[${index}].name duplicates another repository name`)
|
|
573
|
+
repoNames.add(key)
|
|
574
|
+
}
|
|
515
575
|
if (repo.identity != null) {
|
|
516
576
|
const identity = asObject(repo.identity)
|
|
517
577
|
errors.push(...unknownKeyErrors(identity, `repos[${index}].identity`, new Set(['provider', 'id', 'ext'])))
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
4
|
+
import { hostname } from 'node:os';
|
|
5
|
+
import { ensureContainedPrivateDirectory, openRegularFileNoFollow, readRegularTextNoFollow, syncPrivateDirectory } from './private-state.mjs';
|
|
6
|
+
|
|
7
|
+
const host = createHash('sha256').update(hostname()).digest('hex');
|
|
8
|
+
const pendingPattern = /^\.atelier-write-[a-f0-9-]{36}\.tmp$/;
|
|
9
|
+
export const isPendingPrivateWrite = name => pendingPattern.test(name);
|
|
10
|
+
|
|
11
|
+
export { syncPrivateDirectory };
|
|
12
|
+
|
|
13
|
+
// Publish a complete immutable file with an atomic, non-overwriting hard link.
|
|
14
|
+
// A process interruption can leave a private staging file, never a partial final.
|
|
15
|
+
export function publishPrivateFile(file, bytes) {
|
|
16
|
+
const directory = path.dirname(file);
|
|
17
|
+
const pending = path.join(directory, `.atelier-write-${randomUUID()}.tmp`);
|
|
18
|
+
let fd;
|
|
19
|
+
try {
|
|
20
|
+
fd = openRegularFileNoFollow(pending, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
|
|
21
|
+
fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); fs.closeSync(fd); fd = undefined;
|
|
22
|
+
try { fs.linkSync(pending, file); }
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code !== 'EEXIST') throw error;
|
|
25
|
+
const existing = openRegularFileNoFollow(file);
|
|
26
|
+
try { if (!fs.readFileSync(existing).equals(Buffer.from(bytes))) throw error; }
|
|
27
|
+
finally { fs.closeSync(existing); }
|
|
28
|
+
}
|
|
29
|
+
syncPrivateDirectory(directory);
|
|
30
|
+
const actual = openRegularFileNoFollow(file);
|
|
31
|
+
try { if (!fs.readFileSync(actual).equals(Buffer.from(bytes))) throw new Error('published file readback mismatch'); }
|
|
32
|
+
finally { fs.closeSync(actual); }
|
|
33
|
+
} finally {
|
|
34
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
35
|
+
try { fs.unlinkSync(pending); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function unavailable(diagnostic = { reason: 'ownership changed' }) {
|
|
40
|
+
return Object.assign(new Error(`EEXIST: private state is locked (${diagnostic.reason}${diagnostic.file ? `: ${diagnostic.file}` : ''}); inspectPrivateLock and preserve ownership records before offline recovery`), { code: 'EEXIST', diagnostic });
|
|
41
|
+
}
|
|
42
|
+
function ownerState(record, legacy = false) {
|
|
43
|
+
const pid = typeof record === 'number' ? record : record?.pid;
|
|
44
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return 'invalid owner';
|
|
45
|
+
if (!legacy && record?.host !== host) return 'host identity differs';
|
|
46
|
+
try { process.kill(pid, 0); return 'process exists'; }
|
|
47
|
+
catch (error) { return error.code === 'ESRCH' ? 'absent' : 'process identity unavailable'; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Read-only diagnosis. Older tickets were superseded by a successful successor;
|
|
51
|
+
// only the newest ticket owns this lock. A release marker prevents a completed
|
|
52
|
+
// successor from disappearing and resurrecting an obsolete owner on PID reuse.
|
|
53
|
+
export function inspectPrivateLock(lockPath) {
|
|
54
|
+
const directory = `${lockPath}.owners`;
|
|
55
|
+
let names;
|
|
56
|
+
try {
|
|
57
|
+
const stat = fs.lstatSync(directory);
|
|
58
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return { available: false, reason: 'unsafe ownership directory' };
|
|
59
|
+
names = fs.readdirSync(directory).filter(name => !isPendingPrivateWrite(name)).sort();
|
|
60
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; names = []; }
|
|
61
|
+
const unexpected = names.find(name => !/^\d{12}\.(json|released)$/.test(name));
|
|
62
|
+
if (unexpected) return { available: false, reason: 'unknown ownership file', file: unexpected };
|
|
63
|
+
const nameSet = new Set(names);
|
|
64
|
+
const tickets = names.filter(name => name.endsWith('.json'));
|
|
65
|
+
const orphan = names.find(name => name.endsWith('.released') && !nameSet.has(name.replace(/\.released$/, '.json')));
|
|
66
|
+
if (orphan) return { available: false, reason: 'release without owner', file: orphan };
|
|
67
|
+
let legacy, legacyDigest = null;
|
|
68
|
+
try {
|
|
69
|
+
const bytes = readRegularTextNoFollow(lockPath);
|
|
70
|
+
legacy = JSON.parse(bytes); legacyDigest = createHash('sha256').update(bytes).digest('hex');
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error.code !== 'ENOENT') return { available: false, reason: 'unreadable legacy owner', file: path.basename(lockPath) };
|
|
73
|
+
}
|
|
74
|
+
if (tickets.length) {
|
|
75
|
+
const file = tickets.at(-1), generation = Number(file.slice(0, 12));
|
|
76
|
+
let body, record;
|
|
77
|
+
try { body = readRegularTextNoFollow(path.join(directory, file)); record = JSON.parse(body); }
|
|
78
|
+
catch { return { available: false, reason: 'unreadable owner', file }; }
|
|
79
|
+
if (!Number.isSafeInteger(record?.pid) || record.pid <= 0 || !/^[a-f0-9]{64}$/.test(record?.host) || !/^[a-f0-9-]{36}$/.test(record?.nonce)) return { available: false, reason: 'invalid owner', file };
|
|
80
|
+
if (legacyDigest !== null && record.legacyDigest !== legacyDigest) {
|
|
81
|
+
const reason = ownerState(legacy, true);
|
|
82
|
+
if (reason !== 'absent') return { available: false, reason: `legacy ${reason}`, file: path.basename(lockPath) };
|
|
83
|
+
}
|
|
84
|
+
const marker = file.replace(/\.json$/, '.released');
|
|
85
|
+
if (names.includes(marker)) {
|
|
86
|
+
try {
|
|
87
|
+
if (readRegularTextNoFollow(path.join(directory, marker)) !== body) return { available: false, reason: 'release identity differs', file: marker };
|
|
88
|
+
} catch { return { available: false, reason: 'unreadable release', file: marker }; }
|
|
89
|
+
return { available: true, reason: 'released', generation, file, legacyDigest };
|
|
90
|
+
}
|
|
91
|
+
const reason = ownerState(record);
|
|
92
|
+
return { available: reason === 'absent', reason, generation, file, legacyDigest };
|
|
93
|
+
}
|
|
94
|
+
if (legacyDigest === null) return { available: true, reason: 'unused', generation: 0, legacyDigest };
|
|
95
|
+
const reason = ownerState(legacy, true);
|
|
96
|
+
return { available: reason === 'absent', reason, generation: 0, file: path.basename(lockPath), legacyDigest };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function acquirePrivateLock(lockPath) {
|
|
100
|
+
const parent = path.dirname(lockPath);
|
|
101
|
+
const directory = ensureContainedPrivateDirectory({ workspaceRoot: parent, directory: `${lockPath}.owners`, label: 'private lock' });
|
|
102
|
+
let ticket, body;
|
|
103
|
+
try {
|
|
104
|
+
const status = inspectPrivateLock(lockPath);
|
|
105
|
+
if (!status.available) throw unavailable(status);
|
|
106
|
+
const generation = status.generation;
|
|
107
|
+
if (generation >= 999999999999) throw unavailable({ reason: 'ownership generation exhausted' });
|
|
108
|
+
ticket = path.join(directory, `${String(generation + 1).padStart(12, '0')}.json`);
|
|
109
|
+
body = JSON.stringify({ pid: process.pid, host, nonce: randomUUID(), legacyDigest: status.legacyDigest });
|
|
110
|
+
publishPrivateFile(ticket, body);
|
|
111
|
+
let released = false;
|
|
112
|
+
return () => {
|
|
113
|
+
if (released) return;
|
|
114
|
+
if (readRegularTextNoFollow(ticket) !== body) throw unavailable();
|
|
115
|
+
publishPrivateFile(ticket.replace(/\.json$/, '.released'), body);
|
|
116
|
+
released = true;
|
|
117
|
+
};
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// No operation has started: clean up only this exact attempted owner.
|
|
120
|
+
if (ticket && body) {
|
|
121
|
+
try { if (readRegularTextNoFollow(ticket) === body) fs.unlinkSync(ticket); }
|
|
122
|
+
catch { /* Preserve uncertain ownership for offline diagnosis. */ }
|
|
123
|
+
}
|
|
124
|
+
if (error.code === 'EEXIST' && !error.diagnostic) throw unavailable(); throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function withPrivateLock(lockPath, operation) {
|
|
129
|
+
const release = acquirePrivateLock(lockPath);
|
|
130
|
+
try { return operation(); } finally { release(); }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Revalidate file identities on every read; unchanged prefixes reuse their
|
|
134
|
+
// verified result. A changed existing file forces replay, a missing tail is
|
|
135
|
+
// refused for the lifetime of this reader. Startup always verifies all history.
|
|
136
|
+
export function createVerifiedFileSequence({ directory, initial, apply, ignoreFiles = [] }) {
|
|
137
|
+
let names = [], identities = [], value = initial();
|
|
138
|
+
const identity = file => {
|
|
139
|
+
const s = fs.lstatSync(file, { bigint: true });
|
|
140
|
+
if (!s.isFile() || s.isSymbolicLink()) throw new Error('non-regular history refused');
|
|
141
|
+
return [s.dev, s.ino, s.size, s.mtimeNs, s.ctimeNs].join(':');
|
|
142
|
+
};
|
|
143
|
+
return () => {
|
|
144
|
+
const listed = fs.readdirSync(directory).filter(n => !isPendingPrivateWrite(n)).sort();
|
|
145
|
+
// Explicit adapter metadata exceptions must still be regular files. Never
|
|
146
|
+
// hide unknown filenames or malformed committed records behind a glob.
|
|
147
|
+
for (const name of listed.filter(n => ignoreFiles.includes(n))) identity(path.join(directory, name));
|
|
148
|
+
const next = listed.filter(n => !ignoreFiles.includes(n));
|
|
149
|
+
if (next.length < names.length) throw new Error('history shortened; preserve and inspect');
|
|
150
|
+
const ids = next.map(n => identity(path.join(directory, n)));
|
|
151
|
+
const reuse = names.every((n, i) => n === next[i] && identities[i] === ids[i]);
|
|
152
|
+
let result = reuse ? value : initial();
|
|
153
|
+
for (let i = reuse ? names.length : 0; i < next.length; i++) {
|
|
154
|
+
const file = path.join(directory, next[i]);
|
|
155
|
+
result = apply(readRegularTextNoFollow(file), result, i + 1, next[i]);
|
|
156
|
+
if (identity(file) !== ids[i]) throw new Error('history changed during verification');
|
|
157
|
+
}
|
|
158
|
+
names = next; identities = ids; value = result;
|
|
159
|
+
return structuredClone(result);
|
|
160
|
+
};
|
|
161
|
+
}
|