@mnstry/atelier 0.2.0-alpha.5 → 0.2.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +191 -0
- package/README.md +86 -25
- package/SECURITY.md +6 -1
- package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
- package/contracts/atelier-coauthor.v1.schema.json +163 -0
- package/contracts/atelier-decision-request.v1.schema.json +476 -0
- package/contracts/atelier-decision-result.v1.schema.json +500 -0
- package/contracts/atelier-guide.v1.schema.json +227 -0
- package/contracts/atelier-intake.v1.schema.json +151 -0
- package/contracts/atelier-migration.v2.schema.json +61 -0
- package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
- package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
- package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
- package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
- package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
- package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
- package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
- package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
- package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
- package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
- package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
- package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
- package/contracts/atelier-presentation.v1.schema.json +1293 -0
- package/contracts/atelier-repository-observation.v1.schema.json +163 -0
- package/contracts/atelier-review.v1.schema.json +1601 -0
- package/contracts/atelier-skill-steward.v1.schema.json +692 -0
- package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
- package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
- package/contracts/atelier-vault-publication.v1.schema.json +58 -0
- package/docs/assurance-controls.md +12 -2
- package/docs/atelier-runtime.md +13 -2
- package/docs/atelier-sync.md +172 -0
- package/docs/blocks/claims.md +28 -18
- package/docs/blocks/will-not-do.md +12 -3
- package/docs/coauthor-session.md +57 -0
- package/docs/connected-composition.md +75 -0
- package/docs/decisions.md +167 -0
- package/docs/exact-upgrades.md +159 -0
- package/docs/extension-pack-lifecycle.md +33 -0
- package/docs/guided-upgrades.md +99 -0
- package/docs/install.md +65 -9
- package/docs/intake-and-guides.md +65 -0
- package/docs/integration-and-review-plan.md +409 -0
- package/docs/integration-contract-decisions.md +22 -0
- package/docs/local-review.md +84 -0
- package/docs/local-services.md +227 -0
- package/docs/local-state.md +107 -0
- package/docs/obsidian-contract.md +687 -0
- package/docs/obsidian.md +387 -0
- package/docs/portable-presentation.md +328 -0
- package/docs/presentation-consumer-boundaries.md +75 -0
- package/docs/presentation-review-disposition.md +47 -0
- package/docs/project-command-inventory.md +38 -0
- package/docs/project-options.md +53 -0
- package/docs/release-engineering.md +99 -2
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +70 -2
- package/docs/vault-service.md +292 -0
- package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
- package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
- package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
- package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
- package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
- package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
- package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
- package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
- package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
- package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
- package/fixtures/atelier-intake/valid/source.v1.json +7 -0
- package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
- package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
- package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
- package/fixtures/atelier-review/valid/bound.v1.json +229 -0
- package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
- package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
- package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
- package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
- package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
- package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
- package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
- package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
- package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
- package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
- package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
- package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
- package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
- package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
- package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
- package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
- package/fixtures/decisions/request/valid/triage.v1.json +60 -0
- package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
- package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
- package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
- package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
- package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
- package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
- package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
- package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
- package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
- package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
- package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
- package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
- package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
- package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
- package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
- package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
- package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
- package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
- package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
- package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
- package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
- package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
- package/fixtures/obsidian/edits/workspace.json +65 -0
- package/fixtures/obsidian/materialization/expected-full.json +99 -0
- package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
- package/fixtures/obsidian/materialization/workspace.json +54 -0
- package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
- package/fixtures/ui/presentation/pane.v1.json +7 -0
- package/fixtures/ui/presentation/reference.v1.json +293 -0
- package/package.json +61 -6
- package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/claude/atelier-open/SKILL.md +5 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
- package/skills/claude/mnstry-readiness/SKILL.md +5 -0
- package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/codex/atelier-open/SKILL.md +5 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
- package/skills/codex/mnstry-readiness/SKILL.md +5 -0
- package/src/access/preview-enforcer.mjs +84 -0
- package/src/boundary/content-rules.mjs +9 -4
- package/src/boundary/policy.mjs +74 -42
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +61 -3
- package/src/coauthor/session.mjs +161 -0
- package/src/coauthor/store.mjs +173 -0
- package/src/collaboration/event-ledger.mjs +16 -33
- package/src/collaboration/inspection-bundle.mjs +223 -0
- package/src/collaboration/review-contracts.mjs +25 -0
- package/src/collaboration/review-store.mjs +305 -0
- package/src/commands/coauthor.mjs +26 -0
- package/src/commands/config.mjs +6 -1
- package/src/commands/extension-pack.mjs +15 -8
- package/src/commands/init.mjs +44 -5
- package/src/commands/obsidian.mjs +368 -0
- package/src/commands/review.mjs +147 -0
- package/src/commands/setup.mjs +51 -14
- package/src/commands/skills.mjs +199 -0
- package/src/commands/sync.mjs +107 -0
- package/src/composition/registry.mjs +27 -0
- package/src/composition/wire.mjs +122 -0
- package/src/contracts/corpus.mjs +35 -0
- package/src/decisions/contracts.d.mts +96 -0
- package/src/decisions/contracts.mjs +321 -0
- package/src/disclosure/content-scan.mjs +13 -0
- package/src/export/atelier-export-contract.mjs +16 -1
- package/src/extension-packs/lifecycle.mjs +154 -0
- package/src/graph/graph.mjs +64 -2
- package/src/graph/knowledge-graph.mjs +548 -24
- package/src/guides/contracts.mjs +54 -0
- package/src/index.mjs +29 -0
- package/src/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +50 -49
- package/src/project/durable-state.mjs +161 -0
- package/src/project/file-class.mjs +166 -0
- package/src/project/private-state.mjs +11 -1
- package/src/projection/obsidian/contracts.mjs +571 -0
- package/src/projection/obsidian/edits/align.mjs +169 -0
- package/src/projection/obsidian/edits/apply.mjs +889 -0
- package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
- package/src/projection/obsidian/edits/contribution.mjs +108 -0
- package/src/projection/obsidian/edits/index.mjs +19 -0
- package/src/projection/obsidian/edits/object-identity.mjs +61 -0
- package/src/projection/obsidian/edits/object-store.mjs +517 -0
- package/src/projection/obsidian/edits/observe.mjs +123 -0
- package/src/projection/obsidian/edits/policy.mjs +123 -0
- package/src/projection/obsidian/edits/regions.mjs +648 -0
- package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
- package/src/projection/obsidian/materialize/index.mjs +7 -0
- package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
- package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
- package/src/projection/obsidian/materialize/settings.mjs +57 -0
- package/src/projection/obsidian/materialize/stage.mjs +27 -0
- package/src/projection/obsidian/proposals/adapter.mjs +572 -0
- package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
- package/src/projection/obsidian/proposals/contribution.mjs +55 -0
- package/src/projection/obsidian/proposals/index.mjs +23 -0
- package/src/projection/obsidian/proposals/observation.mjs +216 -0
- package/src/projection/obsidian/proposals/queue.mjs +277 -0
- package/src/projection/obsidian/proposals/router.mjs +168 -0
- package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
- package/src/projection/obsidian/publication/exchange.mjs +117 -0
- package/src/projection/obsidian/publication/index.mjs +5 -0
- package/src/projection/obsidian/publication/publisher.mjs +588 -0
- package/src/projection/obsidian/publication/test-seam.mjs +16 -0
- package/src/projection/obsidian/publication/transport.mjs +139 -0
- package/src/projection/obsidian/recovery/index.mjs +6 -0
- package/src/projection/obsidian/recovery/journal.mjs +134 -0
- package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
- package/src/projection/obsidian/recovery/restart.mjs +316 -0
- package/src/projection/obsidian/recovery/store.mjs +234 -0
- package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
- package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
- package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
- package/src/projection/obsidian/selection-ui/index.mjs +12 -0
- package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
- package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
- package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
- package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
- package/src/projection/project.mjs +28 -19
- package/src/readiness-protocols/evidence.mjs +282 -0
- package/src/readiness-protocols/runtime.mjs +4 -3
- package/src/readiness-protocols/source-read.mjs +39 -0
- package/src/runtime/git-adapter.mjs +189 -0
- package/src/runtime/local-state.mjs +439 -0
- package/src/runtime/obsidian/app-capability.mjs +110 -0
- package/src/runtime/obsidian/app-production-seams.mjs +96 -0
- package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
- package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
- package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
- package/src/runtime/obsidian/contributions.mjs +27 -0
- package/src/runtime/obsidian/documents.mjs +37 -0
- package/src/runtime/obsidian/enablement.mjs +53 -0
- package/src/runtime/obsidian/engine.mjs +532 -0
- package/src/runtime/obsidian/errors.mjs +14 -0
- package/src/runtime/obsidian/extension-points.mjs +103 -0
- package/src/runtime/obsidian/index.mjs +34 -0
- package/src/runtime/obsidian/lifecycle.mjs +258 -0
- package/src/runtime/obsidian/machine-settings.mjs +260 -0
- package/src/runtime/obsidian/observation.mjs +158 -0
- package/src/runtime/obsidian/opening.mjs +243 -0
- package/src/runtime/obsidian/pending-edits.mjs +141 -0
- package/src/runtime/obsidian/pipeline.mjs +122 -0
- package/src/runtime/obsidian/private-lock.mjs +156 -0
- package/src/runtime/obsidian/process-identity.mjs +58 -0
- package/src/runtime/obsidian/service-client.mjs +66 -0
- package/src/runtime/obsidian/service-main.mjs +89 -0
- package/src/runtime/obsidian/service-record.mjs +169 -0
- package/src/runtime/obsidian/service-server.mjs +121 -0
- package/src/runtime/obsidian/service.mjs +216 -0
- package/src/runtime/obsidian/startup-adapters.mjs +87 -0
- package/src/runtime/obsidian/state-store.mjs +180 -0
- package/src/runtime/obsidian/sync-notice.mjs +33 -0
- package/src/runtime/obsidian/tick-loop.mjs +84 -0
- package/src/runtime/obsidian/watchers.mjs +36 -0
- package/src/runtime/repository-observation.mjs +491 -0
- package/src/runtime/supervisor.mjs +803 -0
- package/src/server/local-sidecar.mjs +44 -0
- package/src/server/server.mjs +1 -0
- package/src/skills/steward.mjs +780 -0
- package/src/ui/presentation/browser.mjs +278 -0
- package/src/ui/presentation/contract.mjs +116 -0
- package/src/ui/presentation/index.mjs +7 -0
- package/src/ui/presentation/native.mjs +153 -0
- package/src/ui/presentation/proof.mjs +57 -0
- package/src/ui/presentation/schema-check.mjs +30 -0
- package/src/ui/presentation/schema.generated.mjs +1371 -0
- package/src/ui/presentation/state.mjs +43 -0
- package/src/ui/presentation/styles.mjs +67 -0
- package/src/ui/presentation/tokens.mjs +74 -0
- package/src/ui/presentation/web.mjs +85 -0
- package/src/ui/review-page.mjs +32 -0
- package/src/upgrade/explanation.mjs +33 -0
- package/src/upgrade/provenance.mjs +338 -0
- package/src/upgrade/transaction-files.mjs +103 -0
- package/src/upgrade/transaction.mjs +475 -0
- package/src/upgrade/upgrade.mjs +63 -28
- package/src/vault/hosts.mjs +21 -0
- package/src/vault/identity.mjs +19 -0
- package/src/vault/index.mjs +7 -0
- package/src/vault/interface.mjs +11 -0
- package/src/vault/metadata.mjs +31 -0
- package/src/vault/privacy.mjs +148 -0
- package/src/vault/probe.mjs +89 -0
- package/src/vault/service.mjs +131 -0
- package/src/vault/source.mjs +45 -0
- package/src/vault/storage.mjs +23 -0
- package/templates/distribution-workspace/.claude/launch.json +6 -2
- package/templates/distribution-workspace/gitignore +1 -0
- package/templates/distribution-workspace/project/README.md +2 -2
- package/templates/external-project-workspace/README.md +45 -0
- package/templates/external-project-workspace/answers.example.json +4 -0
- package/templates/external-project-workspace/atelier.project.json +40 -0
- package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
- package/templates/external-project-workspace/gitignore +8 -0
- package/templates/external-project-workspace/pack-lifecycle.json +14 -0
- package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
- package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
- package/templates/external-project-workspace/repo-access.v1.json +9 -0
- package/templates/external-project-workspace/source/README.md +11 -0
- package/templates/launch.json +6 -2
- package/templates/private-domain-workspace/.claude/launch.json +6 -2
- package/templates/private-domain-workspace/domain/README.md +2 -2
- package/templates/private-domain-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/.claude/launch.json +6 -2
- package/templates/shared-project-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/project/README.md +2 -2
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Calls one `tick` again and again, never two at a time.
|
|
2
|
+
//
|
|
3
|
+
// The next tick is scheduled only when the previous one has settled, so ticks
|
|
4
|
+
// cannot overlap however long one takes. A tick that throws does not end the
|
|
5
|
+
// loop and does not change the next tick: the error is handed to `onOutcome`,
|
|
6
|
+
// and the delay before the next attempt doubles per consecutive failure up to
|
|
7
|
+
// a ceiling, so a disk that stays full is retried slowly instead of in a tight
|
|
8
|
+
// loop. One success returns the loop to its interval.
|
|
9
|
+
//
|
|
10
|
+
// `tickNow()` asks for a tick that starts after the request: while a tick is
|
|
11
|
+
// in flight the request is answered by one follow-up tick, shared by everyone
|
|
12
|
+
// who asked meanwhile.
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_TICK_INTERVAL_MS = 30 * 1000
|
|
15
|
+
export const DEFAULT_MAX_BACKOFF_MS = 15 * 60 * 1000
|
|
16
|
+
|
|
17
|
+
export const TICK_LOOP_PRIMITIVES = Object.freeze({
|
|
18
|
+
// Whether the loop goes on after a tick threw.
|
|
19
|
+
continuesAfterError: () => true,
|
|
20
|
+
delayAfter: ({ intervalMs, maxBackoffMs, consecutiveFailures }) => (consecutiveFailures === 0 ? intervalMs : Math.min(maxBackoffMs, intervalMs * 2 ** Math.min(consecutiveFailures, 20))),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export function createTickLoopForOracleTests({ tick, intervalMs = DEFAULT_TICK_INTERVAL_MS, maxBackoffMs = DEFAULT_MAX_BACKOFF_MS, setTimer = setTimeout, clearTimer = clearTimeout, onOutcome = () => {} }, primitives = TICK_LOOP_PRIMITIVES) {
|
|
24
|
+
if (typeof tick !== 'function') throw new TypeError('the tick loop needs a tick')
|
|
25
|
+
if (!Number.isInteger(intervalMs) || intervalMs < 1 || !Number.isInteger(maxBackoffMs) || maxBackoffMs < intervalMs) throw new TypeError('the tick loop needs a positive interval and a backoff ceiling no smaller than it')
|
|
26
|
+
const rules = { ...TICK_LOOP_PRIMITIVES, ...primitives }
|
|
27
|
+
let timer = null
|
|
28
|
+
let inFlight = null
|
|
29
|
+
let followUp = null // { promise, resolve } while somebody waits for a tick that starts after now
|
|
30
|
+
let started = false
|
|
31
|
+
let stopped = false
|
|
32
|
+
let consecutiveFailures = 0
|
|
33
|
+
let ticks = 0
|
|
34
|
+
|
|
35
|
+
function schedule() {
|
|
36
|
+
if (stopped || !started || timer !== null) return
|
|
37
|
+
timer = setTimer(() => { timer = null; void run() }, rules.delayAfter({ intervalMs, maxBackoffMs, consecutiveFailures }))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function once() {
|
|
41
|
+
let outcome
|
|
42
|
+
try { outcome = { ok: true, report: await tick() } } catch (error) { outcome = { ok: false, error } }
|
|
43
|
+
ticks += 1
|
|
44
|
+
consecutiveFailures = outcome.ok ? 0 : consecutiveFailures + 1
|
|
45
|
+
try { await onOutcome({ ...outcome, consecutiveFailures }) } catch { /* recording an outcome never ends the loop */ }
|
|
46
|
+
if (!outcome.ok && !rules.continuesAfterError()) stopped = true
|
|
47
|
+
return outcome
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function run() {
|
|
51
|
+
if (stopped) return Promise.resolve({ ok: false, stopped: true })
|
|
52
|
+
if (inFlight) return inFlight
|
|
53
|
+
if (timer !== null) { clearTimer(timer); timer = null }
|
|
54
|
+
inFlight = once().finally(() => {
|
|
55
|
+
inFlight = null
|
|
56
|
+
const waiting = followUp
|
|
57
|
+
followUp = null
|
|
58
|
+
if (waiting) run().then(waiting.resolve)
|
|
59
|
+
else schedule()
|
|
60
|
+
})
|
|
61
|
+
return inFlight
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
start() { if (!started && !stopped) { started = true; void run() } },
|
|
66
|
+
tickNow() {
|
|
67
|
+
if (stopped) return Promise.resolve({ ok: false, stopped: true })
|
|
68
|
+
if (!inFlight) return run()
|
|
69
|
+
if (!followUp) { let resolve; const promise = new Promise((settle) => { resolve = settle }); followUp = { promise, resolve } }
|
|
70
|
+
return followUp.promise
|
|
71
|
+
},
|
|
72
|
+
// Lets the tick in flight finish; starts no other.
|
|
73
|
+
async stop() {
|
|
74
|
+
stopped = true
|
|
75
|
+
if (timer !== null) { clearTimer(timer); timer = null }
|
|
76
|
+
if (inFlight) await inFlight.catch(() => {})
|
|
77
|
+
},
|
|
78
|
+
state: () => ({ started, stopped, ticking: inFlight !== null, ticks, consecutiveFailures, nextDelayMs: rules.delayAfter({ intervalMs, maxBackoffMs, consecutiveFailures }) }),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createTickLoop(options) {
|
|
83
|
+
return createTickLoopForOracleTests(options, TICK_LOOP_PRIMITIVES)
|
|
84
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
|
|
3
|
+
// Filesystem watchers, as hints only.
|
|
4
|
+
//
|
|
5
|
+
// factory({ roots: [{ id, path, recursive }], onEvent }) -> { close() }
|
|
6
|
+
// onEvent({ rootId, relative }) relative is a '/'-separated path under the
|
|
7
|
+
// root, or null for "something under it"
|
|
8
|
+
//
|
|
9
|
+
// A watcher may drop, repeat or delay events and may fail to start at all; the
|
|
10
|
+
// engine never depends on one. It only hashes a named file sooner than the
|
|
11
|
+
// next stat scan would have. Tests inject a factory they drive by hand.
|
|
12
|
+
|
|
13
|
+
export function createNullWatcherFactory() {
|
|
14
|
+
return () => ({ close() {} })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// fs.watch, recursive where the platform supports it. It does not keep the
|
|
18
|
+
// process alive, and a root that cannot be watched is reported, not thrown:
|
|
19
|
+
// digest reconciliation still covers it.
|
|
20
|
+
export function createFsWatcherFactory({ watch = fs.watch, onUnavailable = () => {} } = {}) {
|
|
21
|
+
return ({ roots, onEvent }) => {
|
|
22
|
+
const watchers = []
|
|
23
|
+
for (const root of roots) {
|
|
24
|
+
try {
|
|
25
|
+
const watcher = watch(root.path, { recursive: root.recursive !== false, persistent: false }, (_eventType, filename) => {
|
|
26
|
+
onEvent({ rootId: root.id, relative: typeof filename === 'string' && filename !== '' ? filename.split('\\').join('/') : null })
|
|
27
|
+
})
|
|
28
|
+
watcher.on('error', () => onUnavailable({ rootId: root.id }))
|
|
29
|
+
watchers.push(watcher)
|
|
30
|
+
} catch {
|
|
31
|
+
onUnavailable({ rootId: root.id })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { close() { for (const watcher of watchers.splice(0)) try { watcher.close() } catch { /* already closed */ } } }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { validateJsonSchema } from '../export/atelier-export-contract.mjs'
|
|
5
|
+
import { readRegularTextNoFollow } from '../project/private-state.mjs'
|
|
6
|
+
import {
|
|
7
|
+
classifyRemoteAuthentication,
|
|
8
|
+
gitText,
|
|
9
|
+
inspectGitEngine,
|
|
10
|
+
parseNullConfig,
|
|
11
|
+
runGit,
|
|
12
|
+
sanitizeRemoteUrl,
|
|
13
|
+
} from './git-adapter.mjs'
|
|
14
|
+
|
|
15
|
+
export const ATELIER_REPOSITORY_OBSERVATION_SCHEMA = 'atelier-repository-observation@v1'
|
|
16
|
+
export const ATELIER_REPOSITORY_OBSERVATION_MAX_ENTRIES = 4096
|
|
17
|
+
const OBSERVATION_CONTRACT = JSON.parse(fs.readFileSync(new URL('../../contracts/atelier-repository-observation.v1.schema.json', import.meta.url), 'utf8'))
|
|
18
|
+
|
|
19
|
+
const CLOUD_PATH_PATTERNS = [
|
|
20
|
+
{ code: 'provider-managed-onedrive', pattern: /(?:^|[\\/])OneDrive(?:\s*-[^\\/]+)?(?:[\\/]|$)/i },
|
|
21
|
+
{ code: 'provider-managed-dropbox', pattern: /(?:^|[\\/])Dropbox(?:[\\/]|$)/i },
|
|
22
|
+
{ code: 'provider-managed-google-drive', pattern: /(?:^|[\\/])Google Drive(?:[\\/]|$)/i },
|
|
23
|
+
{ code: 'provider-managed-icloud', pattern: /(?:^|[\\/])(?:iCloud Drive|Mobile Documents)(?:[\\/]|$)/i },
|
|
24
|
+
{ code: 'provider-managed-cloudstorage', pattern: /(?:^|[\\/])Library[\\/]CloudStorage(?:[\\/]|$)/i },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
function normalizeRelative(value) {
|
|
28
|
+
return String(value || '').replaceAll('\\', '/').replace(/^\.\//, '')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sha256(value) {
|
|
32
|
+
return crypto.createHash('sha256').update(value).digest('hex')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function firstLine(value) {
|
|
36
|
+
return String(value || '').split(/\r?\n/)[0].trim()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function optionalText(gitExecutable, repoRoot, args, { failures = null, label = args[0], allowMissing = false } = {}) {
|
|
40
|
+
const result = runGit(gitExecutable, repoRoot, args, { allowFailure: true })
|
|
41
|
+
if (result.ok) return result.stdout.trim()
|
|
42
|
+
if (!allowMissing && failures) failures.push(blocker('observation-evidence-unavailable', `required Git evidence is unavailable: ${label}`, { probe: label }))
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function boolConfig(config, key) {
|
|
47
|
+
const item = config.find((entry) => entry.key.toLowerCase() === key.toLowerCase())
|
|
48
|
+
return /^(?:true|yes|on|1)$/i.test(item?.value || '')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function safeConfigKey(key) {
|
|
52
|
+
const segments = String(key || '').split('.')
|
|
53
|
+
if (segments.length < 3) return String(key || '').toLowerCase()
|
|
54
|
+
const section = segments.shift().toLowerCase()
|
|
55
|
+
const variable = segments.pop().toLowerCase()
|
|
56
|
+
return `${section}.[subsection].${variable}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function configValues(config, pattern, { includeValue = false } = {}) {
|
|
60
|
+
return config.filter((entry) => pattern.test(entry.key)).map((entry) => includeValue ? { key: safeConfigKey(entry.key), value: entry.value } : { key: safeConfigKey(entry.key) })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function classifyFilesystemRoot(candidate, { platform = process.platform } = {}) {
|
|
64
|
+
const requested = String(candidate || '')
|
|
65
|
+
if (!requested) return { supported: false, code: 'path-missing', requested, resolved: null }
|
|
66
|
+
if (/^(?:\\\\|\/\/)/.test(requested)) {
|
|
67
|
+
return { supported: false, code: 'network-unc-root', requested, resolved: null }
|
|
68
|
+
}
|
|
69
|
+
if (platform === 'linux' && /^\/mnt\/[a-z](?:\/|$)/i.test(requested)) {
|
|
70
|
+
return { supported: false, code: 'wsl-cross-boundary-root', requested, resolved: null }
|
|
71
|
+
}
|
|
72
|
+
const cloud = CLOUD_PATH_PATTERNS.find(({ pattern }) => pattern.test(requested))
|
|
73
|
+
if (cloud) return { supported: false, code: cloud.code, requested, resolved: null }
|
|
74
|
+
|
|
75
|
+
let resolved
|
|
76
|
+
try {
|
|
77
|
+
resolved = fs.realpathSync.native(requested)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return { supported: false, code: 'path-unresolvable', requested, resolved: null, detail: error.code || error.message }
|
|
80
|
+
}
|
|
81
|
+
const resolvedCloud = CLOUD_PATH_PATTERNS.find(({ pattern }) => pattern.test(resolved))
|
|
82
|
+
if (resolvedCloud) return { supported: false, code: resolvedCloud.code, requested, resolved }
|
|
83
|
+
if (platform === 'darwin' && /^\/Volumes\//.test(resolved)) {
|
|
84
|
+
return { supported: false, code: 'external-volume-unclassified', requested, resolved }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let statfs = null
|
|
88
|
+
try {
|
|
89
|
+
const value = fs.statfsSync(resolved)
|
|
90
|
+
statfs = { type: String(value.type), blockSize: Number(value.bsize), blocks: Number(value.blocks) }
|
|
91
|
+
} catch {
|
|
92
|
+
// Filesystem metadata is useful evidence, but absence is not proof of a
|
|
93
|
+
// remote root. The lexical refusal boundary remains deterministic.
|
|
94
|
+
}
|
|
95
|
+
return { supported: true, code: 'local-filesystem', requested, resolved, statfs }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function resolveRepositoryRoot(candidate, gitExecutable) {
|
|
99
|
+
const filesystem = classifyFilesystemRoot(candidate)
|
|
100
|
+
if (!filesystem.supported) return { ok: false, filesystem, root: filesystem.resolved }
|
|
101
|
+
const rootResult = runGit(gitExecutable, filesystem.resolved, ['rev-parse', '--show-toplevel'], { allowFailure: true })
|
|
102
|
+
if (!rootResult.ok) return { ok: false, filesystem, root: null, error: 'path is not inside a Git worktree' }
|
|
103
|
+
let root
|
|
104
|
+
try {
|
|
105
|
+
root = fs.realpathSync.native(rootResult.stdout.trim())
|
|
106
|
+
} catch {
|
|
107
|
+
root = fs.realpathSync(rootResult.stdout.trim())
|
|
108
|
+
}
|
|
109
|
+
const rootFilesystem = classifyFilesystemRoot(root)
|
|
110
|
+
if (!rootFilesystem.supported) return { ok: false, filesystem: rootFilesystem, root }
|
|
111
|
+
return { ok: true, filesystem: rootFilesystem, root }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function parsePorcelainStatus(text) {
|
|
115
|
+
const records = String(text || '').split('\0')
|
|
116
|
+
const entries = []
|
|
117
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
118
|
+
const record = records[index]
|
|
119
|
+
if (!record) continue
|
|
120
|
+
const code = record.slice(0, 2)
|
|
121
|
+
const item = { code, path: normalizeRelative(record.slice(3)), originalPath: null }
|
|
122
|
+
if (code[0] === 'R' || code[0] === 'C') {
|
|
123
|
+
item.originalPath = normalizeRelative(records[index + 1] || '')
|
|
124
|
+
index += 1
|
|
125
|
+
}
|
|
126
|
+
entries.push(item)
|
|
127
|
+
}
|
|
128
|
+
return entries.sort((left, right) => left.path.localeCompare(right.path, 'en'))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function branchState(gitExecutable, root, failures) {
|
|
132
|
+
const branch = optionalText(gitExecutable, root, ['symbolic-ref', '--quiet', '--short', 'HEAD'], { failures, label: 'current branch', allowMissing: true })
|
|
133
|
+
const head = optionalText(gitExecutable, root, ['rev-parse', '--verify', 'HEAD'], { failures, label: 'HEAD identity' })
|
|
134
|
+
const upstream = optionalText(gitExecutable, root, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'], { failures, label: 'upstream identity', allowMissing: true })
|
|
135
|
+
let ahead = 0
|
|
136
|
+
let behind = 0
|
|
137
|
+
if (upstream) {
|
|
138
|
+
const counts = optionalText(gitExecutable, root, ['rev-list', '--left-right', '--count', `HEAD...${upstream}`], { failures, label: 'upstream divergence counts' })
|
|
139
|
+
const match = counts?.match(/^(\d+)\s+(\d+)$/)
|
|
140
|
+
if (match) {
|
|
141
|
+
ahead = Number(match[1])
|
|
142
|
+
behind = Number(match[2])
|
|
143
|
+
} else if (counts != null) {
|
|
144
|
+
failures.push(blocker('observation-evidence-invalid', 'required Git evidence is malformed: upstream divergence counts', { probe: 'upstream divergence counts' }))
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { branch, detached: !branch, head, upstream, ahead, behind }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function remoteState(gitExecutable, root, failures) {
|
|
151
|
+
const names = (optionalText(gitExecutable, root, ['remote'], { failures, label: 'remote names' }) || '').split(/\r?\n/).filter(Boolean).sort()
|
|
152
|
+
return names.map((name) => {
|
|
153
|
+
const url = optionalText(gitExecutable, root, ['remote', 'get-url', name], { failures, label: `remote URL for ${name}` })
|
|
154
|
+
const pushResult = runGit(gitExecutable, root, ['remote', 'get-url', '--push', '--all', name], { allowFailure: true })
|
|
155
|
+
if (!pushResult.ok) failures.push(blocker('observation-evidence-unavailable', `required Git evidence is unavailable: push destinations for ${name}`, { probe: `push destinations for ${name}` }))
|
|
156
|
+
const pushUrls = pushResult.ok ? pushResult.stdout.split(/\r?\n/).map((value) => value.trim()).filter(Boolean) : []
|
|
157
|
+
const pushUrl = pushUrls.length === 1 ? pushUrls[0] : null
|
|
158
|
+
return {
|
|
159
|
+
name,
|
|
160
|
+
url: sanitizeRemoteUrl(url),
|
|
161
|
+
identityDigest: sha256(sanitizeRemoteUrl(url)),
|
|
162
|
+
authentication: classifyRemoteAuthentication(url),
|
|
163
|
+
pushUrl: pushUrl == null ? null : sanitizeRemoteUrl(pushUrl),
|
|
164
|
+
pushIdentityDigest: pushUrl == null ? null : sha256(sanitizeRemoteUrl(pushUrl)),
|
|
165
|
+
pushAuthentication: pushUrl == null ? 'unknown' : classifyRemoteAuthentication(pushUrl),
|
|
166
|
+
pushTargetCount: pushUrls.length,
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function submoduleState(gitExecutable, root) {
|
|
172
|
+
if (!fs.existsSync(path.join(root, '.gitmodules'))) {
|
|
173
|
+
const staged = runGit(gitExecutable, root, ['ls-files', '--stage', '-z'], { allowFailure: true })
|
|
174
|
+
const gitlinks = staged.ok
|
|
175
|
+
? staged.stdout.split('\0').filter(Boolean).flatMap((record) => {
|
|
176
|
+
const match = record.match(/^160000 [0-9a-f]+ \d+\t(.+)$/u)
|
|
177
|
+
return match ? [{ marker: '?', value: match[1] }] : []
|
|
178
|
+
})
|
|
179
|
+
: []
|
|
180
|
+
if (gitlinks.length) {
|
|
181
|
+
return {
|
|
182
|
+
declared: true,
|
|
183
|
+
entries: gitlinks,
|
|
184
|
+
complete: false,
|
|
185
|
+
error: 'gitlink entries exist without .gitmodules',
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { declared: false, entries: [], complete: staged.ok, error: staged.ok ? null : 'gitlink inventory failed' }
|
|
189
|
+
}
|
|
190
|
+
const result = runGit(gitExecutable, root, ['submodule', 'status', '--recursive'], { allowFailure: true })
|
|
191
|
+
const entries = result.stdout.split(/\r?\n/).filter(Boolean).map((line) => ({
|
|
192
|
+
marker: line[0],
|
|
193
|
+
value: line.slice(1).trim(),
|
|
194
|
+
}))
|
|
195
|
+
const complete = result.ok && entries.every((entry) => entry.marker === ' ')
|
|
196
|
+
return { declared: true, entries, complete, error: result.ok ? null : firstLine(result.stderr) || 'submodule status failed' }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function lfsState(gitExecutable, root, config, gitDir, statusEntries, failures) {
|
|
200
|
+
const trackedAttributes = (optionalText(gitExecutable, root, ['ls-files', '--', '**/.gitattributes', '.gitattributes'], { failures, label: 'tracked attributes' }) || '')
|
|
201
|
+
.split(/\r?\n/)
|
|
202
|
+
.filter(Boolean)
|
|
203
|
+
const attributeFiles = trackedAttributes.filter((file) => fs.existsSync(path.join(root, file))).map((file) => ({ label: file, absolute: path.join(root, file) }))
|
|
204
|
+
for (const entry of statusEntries) {
|
|
205
|
+
if (path.basename(entry.path) !== '.gitattributes') continue
|
|
206
|
+
const absolute = path.resolve(root, entry.path)
|
|
207
|
+
const relative = path.relative(root, absolute)
|
|
208
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
209
|
+
failures.push(blocker('observation-evidence-unavailable', 'worktree attributes evidence escapes the repository root'))
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
if (fs.existsSync(absolute)) attributeFiles.push({ label: entry.path, absolute })
|
|
213
|
+
}
|
|
214
|
+
const infoAttributes = gitDir ? path.join(gitDir, 'info', 'attributes') : null
|
|
215
|
+
if (infoAttributes && fs.existsSync(infoAttributes)) attributeFiles.push({ label: '.git/info/attributes', absolute: infoAttributes })
|
|
216
|
+
for (const [variable, label] of [['GIT_ATTR_GLOBAL', 'global Git attributes'], ['GIT_ATTR_SYSTEM', 'system Git attributes']]) {
|
|
217
|
+
const absolute = optionalText(gitExecutable, root, ['var', variable], { failures, label: `${label} location` })
|
|
218
|
+
if (absolute && fs.existsSync(absolute)) attributeFiles.push({ label, absolute })
|
|
219
|
+
}
|
|
220
|
+
const externalAttributes = config.find((entry) => entry.key.toLowerCase() === 'core.attributesfile')
|
|
221
|
+
if (externalAttributes) {
|
|
222
|
+
failures.push(blocker('external-attributes-file-unclassified', 'core.attributesFile can change Git filter semantics and must be removed or classified', { key: externalAttributes.key }))
|
|
223
|
+
}
|
|
224
|
+
const uniqueAttributeFiles = [...new Map(attributeFiles.map((item) => [item.absolute, item])).values()]
|
|
225
|
+
const required = uniqueAttributeFiles.some(({ label, absolute }) => {
|
|
226
|
+
const stat = fs.lstatSync(absolute)
|
|
227
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
228
|
+
failures.push(blocker('observation-evidence-unavailable', 'Git attributes evidence is redirected or not a regular file', { path: label }))
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
return /filter\s*=\s*lfs|filter=lfs/i.test(readRegularTextNoFollow(absolute))
|
|
233
|
+
} catch {
|
|
234
|
+
failures.push(blocker('observation-evidence-unavailable', 'Git attributes evidence changed or could not be read safely', { path: label }))
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
const version = runGit(gitExecutable, root, ['lfs', 'version'], { allowFailure: true })
|
|
239
|
+
const labels = uniqueAttributeFiles.map((item) => item.label)
|
|
240
|
+
if (!required) return { required: false, available: version.ok, complete: !externalAttributes, attributeFiles: labels, error: externalAttributes ? 'external Git attributes are unclassified' : null }
|
|
241
|
+
if (!version.ok) return { required: true, available: false, complete: false, attributeFiles: labels, error: 'Git LFS is required but unavailable' }
|
|
242
|
+
const status = runGit(gitExecutable, root, ['lfs', 'fsck'], { allowFailure: true, timeout: 120_000 })
|
|
243
|
+
return {
|
|
244
|
+
required: true,
|
|
245
|
+
available: true,
|
|
246
|
+
complete: status.ok,
|
|
247
|
+
attributeFiles: labels,
|
|
248
|
+
error: status.ok ? null : firstLine(status.stderr) || 'Git LFS object verification failed',
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function repositoryFeatures(gitExecutable, root, config, failures) {
|
|
253
|
+
const gitDir = optionalText(gitExecutable, root, ['rev-parse', '--absolute-git-dir'], { failures, label: 'absolute Git directory' })
|
|
254
|
+
const commonDirRaw = optionalText(gitExecutable, root, ['rev-parse', '--git-common-dir'], { failures, label: 'Git common directory' })
|
|
255
|
+
const commonDir = commonDirRaw ? path.resolve(root, commonDirRaw) : null
|
|
256
|
+
const worktrees = (optionalText(gitExecutable, root, ['worktree', 'list', '--porcelain'], { failures, label: 'worktree inventory' }) || '')
|
|
257
|
+
.split(/\r?\n/)
|
|
258
|
+
.filter((line) => line.startsWith('worktree '))
|
|
259
|
+
.map((line) => line.slice('worktree '.length))
|
|
260
|
+
// Classify the filter identity before sanitizing its subsection. Once
|
|
261
|
+
// `filter.lfs.clean` becomes `filter.[subsection].clean`, a standard Git LFS
|
|
262
|
+
// installation is indistinguishable from an unreviewed custom filter. CI
|
|
263
|
+
// runners commonly install these keys globally, so the ordering matters.
|
|
264
|
+
const filters = configValues(config, /^filter\./i)
|
|
265
|
+
const customFilters = config
|
|
266
|
+
.filter((entry) => /^filter\./i.test(entry.key) && !/^filter\.lfs\./i.test(entry.key))
|
|
267
|
+
.map((entry) => ({ key: safeConfigKey(entry.key) }))
|
|
268
|
+
const partialClone = configValues(config, /^(?:extensions\.partialclone|remote\..*\.promisor)$/i)
|
|
269
|
+
const urlRewrites = configValues(config, /^url\..*\.(?:insteadof|pushinsteadof)$/i)
|
|
270
|
+
const sparse = boolConfig(config, 'core.sparsecheckout') || Boolean(gitDir && fs.existsSync(path.join(gitDir, 'info', 'sparse-checkout')))
|
|
271
|
+
const shallowRaw = optionalText(gitExecutable, root, ['rev-parse', '--is-shallow-repository'], { failures, label: 'shallow repository state' })
|
|
272
|
+
if (shallowRaw != null && !/^(?:true|false)$/.test(shallowRaw)) {
|
|
273
|
+
failures.push(blocker('observation-evidence-invalid', 'required Git evidence is malformed: shallow repository state', { probe: 'shallow repository state' }))
|
|
274
|
+
}
|
|
275
|
+
const indexFlagsResult = runGit(gitExecutable, root, ['ls-files', '-v', '-z'], { allowFailure: true })
|
|
276
|
+
if (!indexFlagsResult.ok) failures.push(blocker('observation-evidence-unavailable', 'required Git index visibility evidence is unavailable', { probe: 'index visibility flags' }))
|
|
277
|
+
const indexFlags = indexFlagsResult.ok
|
|
278
|
+
? indexFlagsResult.stdout.split('\0').filter(Boolean).reduce((counts, record) => {
|
|
279
|
+
const tag = record[0]
|
|
280
|
+
if (tag === 'S' || tag === 's') counts.skipWorktree += 1
|
|
281
|
+
if (/^[a-z]$/.test(tag)) counts.assumeUnchanged += 1
|
|
282
|
+
return counts
|
|
283
|
+
}, { assumeUnchanged: 0, skipWorktree: 0 })
|
|
284
|
+
: { assumeUnchanged: 0, skipWorktree: 0 }
|
|
285
|
+
return {
|
|
286
|
+
gitDir,
|
|
287
|
+
commonDir,
|
|
288
|
+
linkedWorktree: Boolean(gitDir && commonDir && path.resolve(gitDir) !== path.resolve(commonDir)),
|
|
289
|
+
worktrees,
|
|
290
|
+
sparseCheckout: sparse,
|
|
291
|
+
shallowRepository: shallowRaw === 'true',
|
|
292
|
+
indexFlags,
|
|
293
|
+
partialClone,
|
|
294
|
+
urlRewrites,
|
|
295
|
+
filters,
|
|
296
|
+
customFilters,
|
|
297
|
+
hooksPathConfigured: config.some((entry) => entry.key.toLowerCase() === 'core.hookspath'),
|
|
298
|
+
fileModeTrusted: boolConfig(config, 'core.filemode'),
|
|
299
|
+
symlinksTrusted: !config.some((entry) => entry.key.toLowerCase() === 'core.symlinks' && /^(?:false|no|off|0)$/i.test(entry.value || '')),
|
|
300
|
+
signing: {
|
|
301
|
+
required: boolConfig(config, 'commit.gpgsign'),
|
|
302
|
+
keyConfigured: config.some((entry) => entry.key.toLowerCase() === 'user.signingkey'),
|
|
303
|
+
formatConfigured: config.some((entry) => entry.key.toLowerCase() === 'gpg.format'),
|
|
304
|
+
},
|
|
305
|
+
proxy: configValues(config, /^https?\..*proxy$/i),
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function gitMode(stat, index, { fileModeTrusted, symlinksTrusted }) {
|
|
310
|
+
if (stat.isSymbolicLink()) return '120000'
|
|
311
|
+
if (!stat.isFile()) return null
|
|
312
|
+
const indexMode = index?.length === 1 && index[0].stage === 0 ? index[0].mode : null
|
|
313
|
+
if (!symlinksTrusted && indexMode === '120000') return '120000'
|
|
314
|
+
if (!fileModeTrusted && /^100(?:644|755)$/.test(indexMode || '')) return indexMode
|
|
315
|
+
return (stat.mode & 0o111) === 0 ? '100644' : '100755'
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function indexEntry(gitExecutable, root, itemPath, failures) {
|
|
319
|
+
const result = runGit(gitExecutable, root, ['--literal-pathspecs', 'ls-files', '--stage', '-z', '--', itemPath], { allowFailure: true })
|
|
320
|
+
if (!result.ok) {
|
|
321
|
+
failures.push(blocker('observation-evidence-unavailable', 'required Git index evidence is unavailable', { path: itemPath }))
|
|
322
|
+
return null
|
|
323
|
+
}
|
|
324
|
+
const records = result.stdout.split('\0').filter(Boolean)
|
|
325
|
+
if (!records.length) return null
|
|
326
|
+
const parsed = records.map((record) => {
|
|
327
|
+
const match = record.match(/^(\d{6}) ([0-9a-f]{40,64}) (\d)\t/)
|
|
328
|
+
if (!match) return null
|
|
329
|
+
return { mode: match[1], blob: match[2], stage: Number(match[3]) }
|
|
330
|
+
})
|
|
331
|
+
if (parsed.some((item) => !item)) {
|
|
332
|
+
failures.push(blocker('observation-evidence-invalid', 'required Git index evidence is malformed', { path: itemPath }))
|
|
333
|
+
return null
|
|
334
|
+
}
|
|
335
|
+
return parsed
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function worktreeEntry(gitExecutable, root, itemPath, index, features, failures) {
|
|
339
|
+
const absolute = path.join(root, itemPath)
|
|
340
|
+
let stat
|
|
341
|
+
try {
|
|
342
|
+
stat = fs.lstatSync(absolute)
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (error?.code === 'ENOENT') return null
|
|
345
|
+
failures.push(blocker('observation-evidence-unavailable', 'required worktree evidence is unavailable', { path: itemPath }))
|
|
346
|
+
return null
|
|
347
|
+
}
|
|
348
|
+
const mode = gitMode(stat, index, features)
|
|
349
|
+
if (!mode) {
|
|
350
|
+
failures.push(blocker('observation-evidence-invalid', 'changed worktree path is not a regular file or symlink', { path: itemPath }))
|
|
351
|
+
return null
|
|
352
|
+
}
|
|
353
|
+
const symlinkBytes = stat.isSymbolicLink() ? fs.readlinkSync(absolute, { encoding: 'buffer' }) : null
|
|
354
|
+
const result = symlinkBytes
|
|
355
|
+
? runGit(gitExecutable, root, ['hash-object', '--stdin'], { allowFailure: true, input: symlinkBytes })
|
|
356
|
+
: runGit(gitExecutable, root, ['--literal-pathspecs', 'hash-object', '--no-filters', '--', itemPath], { allowFailure: true })
|
|
357
|
+
const blob = result.ok ? result.stdout.trim() : null
|
|
358
|
+
if (!blob || !/^[0-9a-f]{40,64}$/.test(blob)) {
|
|
359
|
+
failures.push(blocker('observation-evidence-unavailable', 'required worktree blob evidence is unavailable', { path: itemPath }))
|
|
360
|
+
return null
|
|
361
|
+
}
|
|
362
|
+
const indexResult = symlinkBytes
|
|
363
|
+
? runGit(gitExecutable, root, ['hash-object', '--stdin'], { allowFailure: true, input: symlinkBytes })
|
|
364
|
+
: runGit(gitExecutable, root, ['--literal-pathspecs', 'hash-object', `--path=${itemPath}`, '--', itemPath], { allowFailure: true })
|
|
365
|
+
const indexBlob = indexResult.ok ? indexResult.stdout.trim() : null
|
|
366
|
+
if (!indexBlob || !/^[0-9a-f]{40,64}$/.test(indexBlob)) {
|
|
367
|
+
failures.push(blocker('observation-evidence-unavailable', 'required filtered blob evidence is unavailable', { path: itemPath }))
|
|
368
|
+
return null
|
|
369
|
+
}
|
|
370
|
+
return { mode, blob, indexBlob }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function statusFingerprints(gitExecutable, root, entries, features, failures) {
|
|
374
|
+
return entries.map((entry) => {
|
|
375
|
+
const index = indexEntry(gitExecutable, root, entry.path, failures)
|
|
376
|
+
const worktree = worktreeEntry(gitExecutable, root, entry.path, index, features, failures)
|
|
377
|
+
const originalIndex = entry.originalPath ? indexEntry(gitExecutable, root, entry.originalPath, failures) : null
|
|
378
|
+
return {
|
|
379
|
+
code: entry.code,
|
|
380
|
+
path: entry.path,
|
|
381
|
+
originalPath: entry.originalPath,
|
|
382
|
+
worktree,
|
|
383
|
+
index,
|
|
384
|
+
originalIndex,
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function blocker(code, message, details = {}) {
|
|
390
|
+
return { code, message, details }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function completenessFor({ filesystem, engine, bare, remotes, features, submodules, lfs, acquisitionFailures }) {
|
|
394
|
+
const blockers = [...acquisitionFailures]
|
|
395
|
+
const warnings = []
|
|
396
|
+
if (!filesystem.supported) blockers.push(blocker(filesystem.code, 'repository root is not on a supported local filesystem'))
|
|
397
|
+
if (!engine.supported) blockers.push(blocker('git-version-unsupported', `Git ${engine.version} is older than ${engine.minimum}`))
|
|
398
|
+
if (bare) blockers.push(blocker('bare-repository-unsupported', 'a working tree is required'))
|
|
399
|
+
if (features.sparseCheckout) blockers.push(blocker('sparse-checkout-unsupported', 'sparse workspaces are not complete observations'))
|
|
400
|
+
if (features.shallowRepository) blockers.push(blocker('shallow-repository-unsupported', 'shallow history is not a complete repository observation'))
|
|
401
|
+
if (features.indexFlags.assumeUnchanged || features.indexFlags.skipWorktree) blockers.push(blocker('index-visibility-flags-unsupported', 'assume-unchanged and skip-worktree index flags can hide authored changes from status', features.indexFlags))
|
|
402
|
+
if (features.partialClone.length) blockers.push(blocker('partial-clone-unsupported', 'partial clones may omit required Git objects', { config: features.partialClone }))
|
|
403
|
+
if (features.urlRewrites.length) blockers.push(blocker('url-rewrite-unclassified', 'Git URL rewrite rules make the execution destination ambiguous and must be removed before synchronization', { config: features.urlRewrites.map((entry) => entry.key) }))
|
|
404
|
+
if (features.hooksPathConfigured) blockers.push(blocker('custom-hooks-path-unclassified', 'core.hooksPath changes executable Git behavior and must be removed or classified before synchronization'))
|
|
405
|
+
if (!submodules.complete) blockers.push(blocker('submodules-incomplete', 'all declared submodules must be initialized and clean', { entries: submodules.entries }))
|
|
406
|
+
if (!lfs.complete) blockers.push(blocker('lfs-incomplete', lfs.error || 'Git LFS content is not complete'))
|
|
407
|
+
if (features.customFilters.length) blockers.push(blocker('custom-filter-unclassified', 'custom Git filters must be classified before synchronization', { filters: features.customFilters.map((entry) => entry.key) }))
|
|
408
|
+
for (const remote of remotes) {
|
|
409
|
+
if (remote.authentication === 'unknown') blockers.push(blocker('remote-authentication-unknown', `${remote.name} uses an unsupported remote URL shape`))
|
|
410
|
+
if (remote.pushTargetCount !== 1) blockers.push(blocker('remote-push-destination-ambiguous', `${remote.name} must resolve to exactly one push destination`, { count: remote.pushTargetCount }))
|
|
411
|
+
else if (remote.pushAuthentication === 'unknown') blockers.push(blocker('remote-push-authentication-unknown', `${remote.name} uses an unsupported push URL shape`))
|
|
412
|
+
}
|
|
413
|
+
if (!remotes.length) warnings.push(blocker('remote-missing', 'repository has no remotes; local commits are possible but synchronization is not'))
|
|
414
|
+
if (features.linkedWorktree) warnings.push(blocker('linked-worktree', 'repository is a linked worktree; the common Git directory is shared'))
|
|
415
|
+
if (features.proxy.length) warnings.push(blocker('git-proxy-configured', 'Git proxy configuration is present and must be preserved'))
|
|
416
|
+
return { complete: blockers.length === 0, blockers, warnings }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function observeRepository({ repoRoot, gitExecutable, observedAt = new Date().toISOString() }) {
|
|
420
|
+
const resolved = resolveRepositoryRoot(repoRoot, gitExecutable)
|
|
421
|
+
if (!resolved.ok) {
|
|
422
|
+
return {
|
|
423
|
+
schema: ATELIER_REPOSITORY_OBSERVATION_SCHEMA,
|
|
424
|
+
observedAt,
|
|
425
|
+
complete: false,
|
|
426
|
+
root: resolved.root,
|
|
427
|
+
filesystem: resolved.filesystem,
|
|
428
|
+
blockers: [blocker(resolved.filesystem?.code || 'repository-unresolved', resolved.error || 'repository could not be resolved')],
|
|
429
|
+
warnings: [],
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const root = resolved.root
|
|
433
|
+
const acquisitionFailures = []
|
|
434
|
+
const engine = inspectGitEngine(gitExecutable)
|
|
435
|
+
const bare = gitText(gitExecutable, root, ['rev-parse', '--is-bare-repository']) === 'true'
|
|
436
|
+
const configResult = runGit(gitExecutable, root, ['config', '--null', '--list'], { allowFailure: true })
|
|
437
|
+
const config = configResult.ok ? parseNullConfig(configResult.stdout) : []
|
|
438
|
+
if (!configResult.ok) acquisitionFailures.push(blocker('observation-evidence-unavailable', 'required Git evidence is unavailable: configuration inventory', { probe: 'configuration inventory' }))
|
|
439
|
+
const branch = branchState(gitExecutable, root, acquisitionFailures)
|
|
440
|
+
const statusResult = runGit(gitExecutable, root, ['status', '--porcelain=v1', '-z', '--untracked-files=all'])
|
|
441
|
+
const allEntries = parsePorcelainStatus(statusResult.stdout)
|
|
442
|
+
const entryCeilingExceeded = allEntries.length > ATELIER_REPOSITORY_OBSERVATION_MAX_ENTRIES
|
|
443
|
+
const entries = allEntries.slice(0, ATELIER_REPOSITORY_OBSERVATION_MAX_ENTRIES)
|
|
444
|
+
if (entryCeilingExceeded) {
|
|
445
|
+
acquisitionFailures.push(blocker('observation-change-set-oversized', 'repository change set exceeds the bounded observation entry ceiling', {
|
|
446
|
+
limitEntries: ATELIER_REPOSITORY_OBSERVATION_MAX_ENTRIES,
|
|
447
|
+
observedEntries: allEntries.length,
|
|
448
|
+
}))
|
|
449
|
+
}
|
|
450
|
+
const remotes = remoteState(gitExecutable, root, acquisitionFailures)
|
|
451
|
+
const features = repositoryFeatures(gitExecutable, root, config, acquisitionFailures)
|
|
452
|
+
const submodules = submoduleState(gitExecutable, root)
|
|
453
|
+
const lfs = lfsState(gitExecutable, root, config, features.gitDir, allEntries, acquisitionFailures)
|
|
454
|
+
const conflicts = allEntries.filter((entry) => entry.code.includes('U') || ['AA', 'DD'].includes(entry.code))
|
|
455
|
+
const staged = allEntries.filter((entry) => entry.code[0] && entry.code[0] !== '?' && entry.code[0] !== ' ')
|
|
456
|
+
const unstaged = allEntries.filter((entry) => entry.code[1] && entry.code[1] !== ' ')
|
|
457
|
+
const fingerprints = entryCeilingExceeded ? [] : statusFingerprints(gitExecutable, root, entries, features, acquisitionFailures)
|
|
458
|
+
const completeness = completenessFor({ filesystem: resolved.filesystem, engine, bare, remotes, features, submodules, lfs, acquisitionFailures })
|
|
459
|
+
const statusDigest = sha256(JSON.stringify({ head: branch.head, branch: branch.branch, rawStatusDigest: sha256(statusResult.stdout), fingerprints }))
|
|
460
|
+
return {
|
|
461
|
+
schema: ATELIER_REPOSITORY_OBSERVATION_SCHEMA,
|
|
462
|
+
observedAt,
|
|
463
|
+
complete: completeness.complete,
|
|
464
|
+
root,
|
|
465
|
+
filesystem: resolved.filesystem,
|
|
466
|
+
git: engine,
|
|
467
|
+
bare,
|
|
468
|
+
branch,
|
|
469
|
+
remotes,
|
|
470
|
+
features,
|
|
471
|
+
submodules,
|
|
472
|
+
lfs,
|
|
473
|
+
status: {
|
|
474
|
+
clean: allEntries.length === 0,
|
|
475
|
+
digest: statusDigest,
|
|
476
|
+
entries,
|
|
477
|
+
fingerprints,
|
|
478
|
+
stagedCount: staged.length,
|
|
479
|
+
unstagedCount: unstaged.length,
|
|
480
|
+
conflictCount: conflicts.length,
|
|
481
|
+
observedEntryCount: allEntries.length,
|
|
482
|
+
entriesTruncated: entryCeilingExceeded,
|
|
483
|
+
},
|
|
484
|
+
blockers: completeness.blockers,
|
|
485
|
+
warnings: completeness.warnings,
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function validateRepositoryObservation(doc) {
|
|
490
|
+
return validateJsonSchema(OBSERVATION_CONTRACT, doc)
|
|
491
|
+
}
|