@software-dlc/cli 0.157.2
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 +11 -0
- package/LICENSE +105 -0
- package/README.md +31 -0
- package/dist/checkout.d.ts +43 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/checkout.js +92 -0
- package/dist/checkout.js.map +1 -0
- package/dist/cli-arguments.d.ts +13 -0
- package/dist/cli-arguments.d.ts.map +1 -0
- package/dist/cli-arguments.js +67 -0
- package/dist/cli-arguments.js.map +1 -0
- package/dist/cli-branch-history.d.ts +48 -0
- package/dist/cli-branch-history.d.ts.map +1 -0
- package/dist/cli-branch-history.js +319 -0
- package/dist/cli-branch-history.js.map +1 -0
- package/dist/cli-branches.d.ts +22 -0
- package/dist/cli-branches.d.ts.map +1 -0
- package/dist/cli-branches.js +59 -0
- package/dist/cli-branches.js.map +1 -0
- package/dist/cli-deploy.d.ts +14 -0
- package/dist/cli-deploy.d.ts.map +1 -0
- package/dist/cli-deploy.js +164 -0
- package/dist/cli-deploy.js.map +1 -0
- package/dist/cli-health.d.ts +3 -0
- package/dist/cli-health.d.ts.map +1 -0
- package/dist/cli-health.js +73 -0
- package/dist/cli-health.js.map +1 -0
- package/dist/cli-help.d.ts +21 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +176 -0
- package/dist/cli-help.js.map +1 -0
- package/dist/cli-login.d.ts +76 -0
- package/dist/cli-login.d.ts.map +1 -0
- package/dist/cli-login.js +634 -0
- package/dist/cli-login.js.map +1 -0
- package/dist/cli-operation.d.ts +2 -0
- package/dist/cli-operation.d.ts.map +1 -0
- package/dist/cli-operation.js +66 -0
- package/dist/cli-operation.js.map +1 -0
- package/dist/cli-org.d.ts +12 -0
- package/dist/cli-org.d.ts.map +1 -0
- package/dist/cli-org.js +218 -0
- package/dist/cli-org.js.map +1 -0
- package/dist/cli-rebuild.d.ts +20 -0
- package/dist/cli-rebuild.d.ts.map +1 -0
- package/dist/cli-rebuild.js +29 -0
- package/dist/cli-rebuild.js.map +1 -0
- package/dist/cli-reset.d.ts +27 -0
- package/dist/cli-reset.d.ts.map +1 -0
- package/dist/cli-reset.js +44 -0
- package/dist/cli-reset.js.map +1 -0
- package/dist/cli-snapshot.d.ts +398 -0
- package/dist/cli-snapshot.d.ts.map +1 -0
- package/dist/cli-snapshot.js +45 -0
- package/dist/cli-snapshot.js.map +1 -0
- package/dist/cli-task-context.d.ts +3 -0
- package/dist/cli-task-context.d.ts.map +1 -0
- package/dist/cli-task-context.js +112 -0
- package/dist/cli-task-context.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1004 -0
- package/dist/cli.js.map +1 -0
- package/dist/graph-packages-lock.d.ts +169 -0
- package/dist/graph-packages-lock.d.ts.map +1 -0
- package/dist/graph-packages-lock.js +503 -0
- package/dist/graph-packages-lock.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/local/aggregate-preflight.d.ts +12 -0
- package/dist/local/aggregate-preflight.d.ts.map +1 -0
- package/dist/local/aggregate-preflight.js +22 -0
- package/dist/local/aggregate-preflight.js.map +1 -0
- package/dist/local/allow-mcp.d.ts +77 -0
- package/dist/local/allow-mcp.d.ts.map +1 -0
- package/dist/local/allow-mcp.js +394 -0
- package/dist/local/allow-mcp.js.map +1 -0
- package/dist/local/api-client.d.ts +15 -0
- package/dist/local/api-client.d.ts.map +1 -0
- package/dist/local/api-client.js +87 -0
- package/dist/local/api-client.js.map +1 -0
- package/dist/local/app-contract-readers.d.ts +229 -0
- package/dist/local/app-contract-readers.d.ts.map +1 -0
- package/dist/local/app-contract-readers.js +450 -0
- package/dist/local/app-contract-readers.js.map +1 -0
- package/dist/local/app-org-reader.d.ts +21 -0
- package/dist/local/app-org-reader.d.ts.map +1 -0
- package/dist/local/app-org-reader.js +66 -0
- package/dist/local/app-org-reader.js.map +1 -0
- package/dist/local/app-shell-readers.d.ts +96 -0
- package/dist/local/app-shell-readers.d.ts.map +1 -0
- package/dist/local/app-shell-readers.js +381 -0
- package/dist/local/app-shell-readers.js.map +1 -0
- package/dist/local/app-tasks-read.d.ts +202 -0
- package/dist/local/app-tasks-read.d.ts.map +1 -0
- package/dist/local/app-tasks-read.js +290 -0
- package/dist/local/app-tasks-read.js.map +1 -0
- package/dist/local/app-worktrees.d.ts +69 -0
- package/dist/local/app-worktrees.d.ts.map +1 -0
- package/dist/local/app-worktrees.js +554 -0
- package/dist/local/app-worktrees.js.map +1 -0
- package/dist/local/base-sync-manifest.d.ts +12 -0
- package/dist/local/base-sync-manifest.d.ts.map +1 -0
- package/dist/local/base-sync-manifest.js +57 -0
- package/dist/local/base-sync-manifest.js.map +1 -0
- package/dist/local/blast-radius.d.ts +191 -0
- package/dist/local/blast-radius.d.ts.map +1 -0
- package/dist/local/blast-radius.js +585 -0
- package/dist/local/blast-radius.js.map +1 -0
- package/dist/local/canonical.d.ts +2 -0
- package/dist/local/canonical.d.ts.map +1 -0
- package/dist/local/canonical.js +2 -0
- package/dist/local/canonical.js.map +1 -0
- package/dist/local/config.d.ts +22 -0
- package/dist/local/config.d.ts.map +1 -0
- package/dist/local/config.js +57 -0
- package/dist/local/config.js.map +1 -0
- package/dist/local/document-commands.d.ts +163 -0
- package/dist/local/document-commands.d.ts.map +1 -0
- package/dist/local/document-commands.js +620 -0
- package/dist/local/document-commands.js.map +1 -0
- package/dist/local/document-embeds.d.ts +21 -0
- package/dist/local/document-embeds.d.ts.map +1 -0
- package/dist/local/document-embeds.js +38 -0
- package/dist/local/document-embeds.js.map +1 -0
- package/dist/local/environment-context.d.ts +39 -0
- package/dist/local/environment-context.d.ts.map +1 -0
- package/dist/local/environment-context.js +193 -0
- package/dist/local/environment-context.js.map +1 -0
- package/dist/local/environment-exclude.d.ts +7 -0
- package/dist/local/environment-exclude.d.ts.map +1 -0
- package/dist/local/environment-exclude.js +82 -0
- package/dist/local/environment-exclude.js.map +1 -0
- package/dist/local/environment-sidecar.d.ts +126 -0
- package/dist/local/environment-sidecar.d.ts.map +1 -0
- package/dist/local/environment-sidecar.js +442 -0
- package/dist/local/environment-sidecar.js.map +1 -0
- package/dist/local/environment-switch.d.ts +22 -0
- package/dist/local/environment-switch.d.ts.map +1 -0
- package/dist/local/environment-switch.js +385 -0
- package/dist/local/environment-switch.js.map +1 -0
- package/dist/local/exact-remote-projection.d.ts +17 -0
- package/dist/local/exact-remote-projection.d.ts.map +1 -0
- package/dist/local/exact-remote-projection.js +53 -0
- package/dist/local/exact-remote-projection.js.map +1 -0
- package/dist/local/experimental/path-object-store.d.ts +66 -0
- package/dist/local/experimental/path-object-store.d.ts.map +1 -0
- package/dist/local/experimental/path-object-store.js +135 -0
- package/dist/local/experimental/path-object-store.js.map +1 -0
- package/dist/local/external-worktree-binding.d.ts +21 -0
- package/dist/local/external-worktree-binding.d.ts.map +1 -0
- package/dist/local/external-worktree-binding.js +56 -0
- package/dist/local/external-worktree-binding.js.map +1 -0
- package/dist/local/folder-members.d.ts +34 -0
- package/dist/local/folder-members.d.ts.map +1 -0
- package/dist/local/folder-members.js +168 -0
- package/dist/local/folder-members.js.map +1 -0
- package/dist/local/git-ref-authority.d.ts +11 -0
- package/dist/local/git-ref-authority.d.ts.map +1 -0
- package/dist/local/git-ref-authority.js +76 -0
- package/dist/local/git-ref-authority.js.map +1 -0
- package/dist/local/git.d.ts +88 -0
- package/dist/local/git.d.ts.map +1 -0
- package/dist/local/git.js +413 -0
- package/dist/local/git.js.map +1 -0
- package/dist/local/graph-read.d.ts +168 -0
- package/dist/local/graph-read.d.ts.map +1 -0
- package/dist/local/graph-read.js +360 -0
- package/dist/local/graph-read.js.map +1 -0
- package/dist/local/graph-sync.d.ts +39 -0
- package/dist/local/graph-sync.d.ts.map +1 -0
- package/dist/local/graph-sync.js +186 -0
- package/dist/local/graph-sync.js.map +1 -0
- package/dist/local/guide-commands.d.ts +79 -0
- package/dist/local/guide-commands.d.ts.map +1 -0
- package/dist/local/guide-commands.js +247 -0
- package/dist/local/guide-commands.js.map +1 -0
- package/dist/local/health-files.d.ts +24 -0
- package/dist/local/health-files.d.ts.map +1 -0
- package/dist/local/health-files.js +133 -0
- package/dist/local/health-files.js.map +1 -0
- package/dist/local/health-inspection.d.ts +12 -0
- package/dist/local/health-inspection.d.ts.map +1 -0
- package/dist/local/health-inspection.js +407 -0
- package/dist/local/health-inspection.js.map +1 -0
- package/dist/local/health-policy.d.ts +11 -0
- package/dist/local/health-policy.d.ts.map +1 -0
- package/dist/local/health-policy.js +257 -0
- package/dist/local/health-policy.js.map +1 -0
- package/dist/local/health-projection.d.ts +14 -0
- package/dist/local/health-projection.d.ts.map +1 -0
- package/dist/local/health-projection.js +102 -0
- package/dist/local/health-projection.js.map +1 -0
- package/dist/local/health-schema.d.ts +338 -0
- package/dist/local/health-schema.d.ts.map +1 -0
- package/dist/local/health-schema.js +213 -0
- package/dist/local/health-schema.js.map +1 -0
- package/dist/local/health-store.d.ts +61 -0
- package/dist/local/health-store.d.ts.map +1 -0
- package/dist/local/health-store.js +218 -0
- package/dist/local/health-store.js.map +1 -0
- package/dist/local/index.d.ts +66 -0
- package/dist/local/index.d.ts.map +1 -0
- package/dist/local/index.js +66 -0
- package/dist/local/index.js.map +1 -0
- package/dist/local/journey-commands.d.ts +65 -0
- package/dist/local/journey-commands.d.ts.map +1 -0
- package/dist/local/journey-commands.js +138 -0
- package/dist/local/journey-commands.js.map +1 -0
- package/dist/local/local-org-store.d.ts +46 -0
- package/dist/local/local-org-store.d.ts.map +1 -0
- package/dist/local/local-org-store.js +132 -0
- package/dist/local/local-org-store.js.map +1 -0
- package/dist/local/local-org.d.ts +389 -0
- package/dist/local/local-org.d.ts.map +1 -0
- package/dist/local/local-org.js +855 -0
- package/dist/local/local-org.js.map +1 -0
- package/dist/local/media-upload.d.ts +5 -0
- package/dist/local/media-upload.d.ts.map +1 -0
- package/dist/local/media-upload.js +41 -0
- package/dist/local/media-upload.js.map +1 -0
- package/dist/local/operation.d.ts +155 -0
- package/dist/local/operation.d.ts.map +1 -0
- package/dist/local/operation.js +340 -0
- package/dist/local/operation.js.map +1 -0
- package/dist/local/ownership-reader-schema.d.ts +117 -0
- package/dist/local/ownership-reader-schema.d.ts.map +1 -0
- package/dist/local/ownership-reader-schema.js +105 -0
- package/dist/local/ownership-reader-schema.js.map +1 -0
- package/dist/local/ownership-reader-worker.d.ts +2 -0
- package/dist/local/ownership-reader-worker.d.ts.map +1 -0
- package/dist/local/ownership-reader-worker.js +341 -0
- package/dist/local/ownership-reader-worker.js.map +1 -0
- package/dist/local/ownership-reader.d.ts +10 -0
- package/dist/local/ownership-reader.d.ts.map +1 -0
- package/dist/local/ownership-reader.js +93 -0
- package/dist/local/ownership-reader.js.map +1 -0
- package/dist/local/package-uses-check.d.ts +33 -0
- package/dist/local/package-uses-check.d.ts.map +1 -0
- package/dist/local/package-uses-check.js +62 -0
- package/dist/local/package-uses-check.js.map +1 -0
- package/dist/local/pending-recovery.d.ts +22 -0
- package/dist/local/pending-recovery.d.ts.map +1 -0
- package/dist/local/pending-recovery.js +171 -0
- package/dist/local/pending-recovery.js.map +1 -0
- package/dist/local/portable-commit-store.d.ts +28 -0
- package/dist/local/portable-commit-store.d.ts.map +1 -0
- package/dist/local/portable-commit-store.js +187 -0
- package/dist/local/portable-commit-store.js.map +1 -0
- package/dist/local/portable-commit.d.ts +109 -0
- package/dist/local/portable-commit.d.ts.map +1 -0
- package/dist/local/portable-commit.js +209 -0
- package/dist/local/portable-commit.js.map +1 -0
- package/dist/local/portable-remote.d.ts +432 -0
- package/dist/local/portable-remote.d.ts.map +1 -0
- package/dist/local/portable-remote.js +280 -0
- package/dist/local/portable-remote.js.map +1 -0
- package/dist/local/project-deleted.d.ts +4 -0
- package/dist/local/project-deleted.d.ts.map +1 -0
- package/dist/local/project-deleted.js +13 -0
- package/dist/local/project-deleted.js.map +1 -0
- package/dist/local/projection.d.ts +166 -0
- package/dist/local/projection.d.ts.map +1 -0
- package/dist/local/projection.js +709 -0
- package/dist/local/projection.js.map +1 -0
- package/dist/local/read-operation.d.ts +3 -0
- package/dist/local/read-operation.d.ts.map +1 -0
- package/dist/local/read-operation.js +19 -0
- package/dist/local/read-operation.js.map +1 -0
- package/dist/local/rebuild.d.ts +11 -0
- package/dist/local/rebuild.d.ts.map +1 -0
- package/dist/local/rebuild.js +42 -0
- package/dist/local/rebuild.js.map +1 -0
- package/dist/local/remote-base.d.ts +25 -0
- package/dist/local/remote-base.d.ts.map +1 -0
- package/dist/local/remote-base.js +68 -0
- package/dist/local/remote-base.js.map +1 -0
- package/dist/local/save-layout-worker.d.ts +19 -0
- package/dist/local/save-layout-worker.d.ts.map +1 -0
- package/dist/local/save-layout-worker.js +28 -0
- package/dist/local/save-layout-worker.js.map +1 -0
- package/dist/local/save-layout.d.ts +84 -0
- package/dist/local/save-layout.d.ts.map +1 -0
- package/dist/local/save-layout.js +221 -0
- package/dist/local/save-layout.js.map +1 -0
- package/dist/local/scope-impact.d.ts +31 -0
- package/dist/local/scope-impact.d.ts.map +1 -0
- package/dist/local/scope-impact.js +32 -0
- package/dist/local/scope-impact.js.map +1 -0
- package/dist/local/source-evidence/filesystem-worker.d.ts +2 -0
- package/dist/local/source-evidence/filesystem-worker.d.ts.map +1 -0
- package/dist/local/source-evidence/filesystem-worker.js +139 -0
- package/dist/local/source-evidence/filesystem-worker.js.map +1 -0
- package/dist/local/source-evidence/processes.d.ts +20 -0
- package/dist/local/source-evidence/processes.d.ts.map +1 -0
- package/dist/local/source-evidence/processes.js +133 -0
- package/dist/local/source-evidence/processes.js.map +1 -0
- package/dist/local/source-evidence/protocol.d.ts +52 -0
- package/dist/local/source-evidence/protocol.d.ts.map +1 -0
- package/dist/local/source-evidence/protocol.js +72 -0
- package/dist/local/source-evidence/protocol.js.map +1 -0
- package/dist/local/source-evidence/reader.d.ts +14 -0
- package/dist/local/source-evidence/reader.d.ts.map +1 -0
- package/dist/local/source-evidence/reader.js +308 -0
- package/dist/local/source-evidence/reader.js.map +1 -0
- package/dist/local/source-evidence/schema.d.ts +2504 -0
- package/dist/local/source-evidence/schema.d.ts.map +1 -0
- package/dist/local/source-evidence/schema.js +99 -0
- package/dist/local/source-evidence/schema.js.map +1 -0
- package/dist/local/source-policy/loader.d.ts +29 -0
- package/dist/local/source-policy/loader.d.ts.map +1 -0
- package/dist/local/source-policy/loader.js +209 -0
- package/dist/local/source-policy/loader.js.map +1 -0
- package/dist/local/source-policy/process.d.ts +7 -0
- package/dist/local/source-policy/process.d.ts.map +1 -0
- package/dist/local/source-policy/process.js +53 -0
- package/dist/local/source-policy/process.js.map +1 -0
- package/dist/local/source-policy/protocol.d.ts +27 -0
- package/dist/local/source-policy/protocol.d.ts.map +1 -0
- package/dist/local/source-policy/protocol.js +48 -0
- package/dist/local/source-policy/protocol.js.map +1 -0
- package/dist/local/source-policy/worker.d.ts +2 -0
- package/dist/local/source-policy/worker.d.ts.map +1 -0
- package/dist/local/source-policy/worker.js +100 -0
- package/dist/local/source-policy/worker.js.map +1 -0
- package/dist/local/state-directory.d.ts +17 -0
- package/dist/local/state-directory.d.ts.map +1 -0
- package/dist/local/state-directory.js +68 -0
- package/dist/local/state-directory.js.map +1 -0
- package/dist/local/store-database.d.ts +13 -0
- package/dist/local/store-database.d.ts.map +1 -0
- package/dist/local/store-database.js +187 -0
- package/dist/local/store-database.js.map +1 -0
- package/dist/local/store.d.ts +181 -0
- package/dist/local/store.d.ts.map +1 -0
- package/dist/local/store.js +585 -0
- package/dist/local/store.js.map +1 -0
- package/dist/local/sync.d.ts +47 -0
- package/dist/local/sync.d.ts.map +1 -0
- package/dist/local/sync.js +518 -0
- package/dist/local/sync.js.map +1 -0
- package/dist/local/task-context-inspection.d.ts +23 -0
- package/dist/local/task-context-inspection.d.ts.map +1 -0
- package/dist/local/task-context-inspection.js +165 -0
- package/dist/local/task-context-inspection.js.map +1 -0
- package/dist/local/task-context-observations.d.ts +9553 -0
- package/dist/local/task-context-observations.d.ts.map +1 -0
- package/dist/local/task-context-observations.js +232 -0
- package/dist/local/task-context-observations.js.map +1 -0
- package/dist/local/task-context-provider.d.ts +18 -0
- package/dist/local/task-context-provider.d.ts.map +1 -0
- package/dist/local/task-context-provider.js +231 -0
- package/dist/local/task-context-provider.js.map +1 -0
- package/dist/local/task-context-schema.d.ts +847 -0
- package/dist/local/task-context-schema.d.ts.map +1 -0
- package/dist/local/task-context-schema.js +305 -0
- package/dist/local/task-context-schema.js.map +1 -0
- package/dist/local/task-context-source.d.ts +16 -0
- package/dist/local/task-context-source.d.ts.map +1 -0
- package/dist/local/task-context-source.js +45 -0
- package/dist/local/task-context-source.js.map +1 -0
- package/dist/local/task-context.d.ts +14 -0
- package/dist/local/task-context.d.ts.map +1 -0
- package/dist/local/task-context.js +570 -0
- package/dist/local/task-context.js.map +1 -0
- package/dist/local/test-commands.d.ts +71 -0
- package/dist/local/test-commands.d.ts.map +1 -0
- package/dist/local/test-commands.js +292 -0
- package/dist/local/test-commands.js.map +1 -0
- package/dist/local/tree-save.d.ts +117 -0
- package/dist/local/tree-save.d.ts.map +1 -0
- package/dist/local/tree-save.js +615 -0
- package/dist/local/tree-save.js.map +1 -0
- package/dist/local/variants.d.ts +125 -0
- package/dist/local/variants.d.ts.map +1 -0
- package/dist/local/variants.js +267 -0
- package/dist/local/variants.js.map +1 -0
- package/dist/local/workspace-admission.d.ts +61 -0
- package/dist/local/workspace-admission.d.ts.map +1 -0
- package/dist/local/workspace-admission.js +112 -0
- package/dist/local/workspace-admission.js.map +1 -0
- package/dist/local/workspace-authority-guard.d.ts +18 -0
- package/dist/local/workspace-authority-guard.d.ts.map +1 -0
- package/dist/local/workspace-authority-guard.js +69 -0
- package/dist/local/workspace-authority-guard.js.map +1 -0
- package/dist/local/workspace-authority.d.ts +33 -0
- package/dist/local/workspace-authority.d.ts.map +1 -0
- package/dist/local/workspace-authority.js +33 -0
- package/dist/local/workspace-authority.js.map +1 -0
- package/dist/local/workspace-branch-runtime.d.ts +61 -0
- package/dist/local/workspace-branch-runtime.d.ts.map +1 -0
- package/dist/local/workspace-branch-runtime.js +216 -0
- package/dist/local/workspace-branch-runtime.js.map +1 -0
- package/dist/local/workspace-built-content.d.ts +137 -0
- package/dist/local/workspace-built-content.d.ts.map +1 -0
- package/dist/local/workspace-built-content.js +393 -0
- package/dist/local/workspace-built-content.js.map +1 -0
- package/dist/local/workspace-claim-status-read.d.ts +53 -0
- package/dist/local/workspace-claim-status-read.d.ts.map +1 -0
- package/dist/local/workspace-claim-status-read.js +118 -0
- package/dist/local/workspace-claim-status-read.js.map +1 -0
- package/dist/local/workspace-claim-status.d.ts +172 -0
- package/dist/local/workspace-claim-status.d.ts.map +1 -0
- package/dist/local/workspace-claim-status.js +721 -0
- package/dist/local/workspace-claim-status.js.map +1 -0
- package/dist/local/workspace-claim-views.d.ts +45 -0
- package/dist/local/workspace-claim-views.d.ts.map +1 -0
- package/dist/local/workspace-claim-views.js +45 -0
- package/dist/local/workspace-claim-views.js.map +1 -0
- package/dist/local/workspace-cli.d.ts +46 -0
- package/dist/local/workspace-cli.d.ts.map +1 -0
- package/dist/local/workspace-cli.js +1524 -0
- package/dist/local/workspace-cli.js.map +1 -0
- package/dist/local/workspace-commit-resolver.d.ts +19 -0
- package/dist/local/workspace-commit-resolver.d.ts.map +1 -0
- package/dist/local/workspace-commit-resolver.js +47 -0
- package/dist/local/workspace-commit-resolver.js.map +1 -0
- package/dist/local/workspace-decision-store.d.ts +184 -0
- package/dist/local/workspace-decision-store.d.ts.map +1 -0
- package/dist/local/workspace-decision-store.js +510 -0
- package/dist/local/workspace-decision-store.js.map +1 -0
- package/dist/local/workspace-environment-runtime.d.ts +107 -0
- package/dist/local/workspace-environment-runtime.d.ts.map +1 -0
- package/dist/local/workspace-environment-runtime.js +135 -0
- package/dist/local/workspace-environment-runtime.js.map +1 -0
- package/dist/local/workspace-evidence-drop.d.ts +21 -0
- package/dist/local/workspace-evidence-drop.d.ts.map +1 -0
- package/dist/local/workspace-evidence-drop.js +58 -0
- package/dist/local/workspace-evidence-drop.js.map +1 -0
- package/dist/local/workspace-evidence-sign-tool.d.ts +20 -0
- package/dist/local/workspace-evidence-sign-tool.d.ts.map +1 -0
- package/dist/local/workspace-evidence-sign-tool.js +85 -0
- package/dist/local/workspace-evidence-sign-tool.js.map +1 -0
- package/dist/local/workspace-evidence-status.d.ts +89 -0
- package/dist/local/workspace-evidence-status.d.ts.map +1 -0
- package/dist/local/workspace-evidence-status.js +141 -0
- package/dist/local/workspace-evidence-status.js.map +1 -0
- package/dist/local/workspace-evidence.d.ts +145 -0
- package/dist/local/workspace-evidence.d.ts.map +1 -0
- package/dist/local/workspace-evidence.js +347 -0
- package/dist/local/workspace-evidence.js.map +1 -0
- package/dist/local/workspace-graph-store.d.ts +32 -0
- package/dist/local/workspace-graph-store.d.ts.map +1 -0
- package/dist/local/workspace-graph-store.js +249 -0
- package/dist/local/workspace-graph-store.js.map +1 -0
- package/dist/local/workspace-handoff.d.ts +32 -0
- package/dist/local/workspace-handoff.d.ts.map +1 -0
- package/dist/local/workspace-handoff.js +144 -0
- package/dist/local/workspace-handoff.js.map +1 -0
- package/dist/local/workspace-history-journal.d.ts +70 -0
- package/dist/local/workspace-history-journal.d.ts.map +1 -0
- package/dist/local/workspace-history-journal.js +131 -0
- package/dist/local/workspace-history-journal.js.map +1 -0
- package/dist/local/workspace-history-store.d.ts +98 -0
- package/dist/local/workspace-history-store.d.ts.map +1 -0
- package/dist/local/workspace-history-store.js +632 -0
- package/dist/local/workspace-history-store.js.map +1 -0
- package/dist/local/workspace-history-types.d.ts +307 -0
- package/dist/local/workspace-history-types.d.ts.map +1 -0
- package/dist/local/workspace-history-types.js +2 -0
- package/dist/local/workspace-history-types.js.map +1 -0
- package/dist/local/workspace-integration-runtime.d.ts +48 -0
- package/dist/local/workspace-integration-runtime.d.ts.map +1 -0
- package/dist/local/workspace-integration-runtime.js +419 -0
- package/dist/local/workspace-integration-runtime.js.map +1 -0
- package/dist/local/workspace-lock.d.ts +7 -0
- package/dist/local/workspace-lock.d.ts.map +1 -0
- package/dist/local/workspace-lock.js +211 -0
- package/dist/local/workspace-lock.js.map +1 -0
- package/dist/local/workspace-merge.d.ts +77 -0
- package/dist/local/workspace-merge.d.ts.map +1 -0
- package/dist/local/workspace-merge.js +703 -0
- package/dist/local/workspace-merge.js.map +1 -0
- package/dist/local/workspace-mixed-history.d.ts +33 -0
- package/dist/local/workspace-mixed-history.d.ts.map +1 -0
- package/dist/local/workspace-mixed-history.js +107 -0
- package/dist/local/workspace-mixed-history.js.map +1 -0
- package/dist/local/workspace-overlap-allocation.d.ts +33 -0
- package/dist/local/workspace-overlap-allocation.d.ts.map +1 -0
- package/dist/local/workspace-overlap-allocation.js +69 -0
- package/dist/local/workspace-overlap-allocation.js.map +1 -0
- package/dist/local/workspace-overlap-impact.d.ts +46 -0
- package/dist/local/workspace-overlap-impact.d.ts.map +1 -0
- package/dist/local/workspace-overlap-impact.js +162 -0
- package/dist/local/workspace-overlap-impact.js.map +1 -0
- package/dist/local/workspace-overlap.d.ts +109 -0
- package/dist/local/workspace-overlap.d.ts.map +1 -0
- package/dist/local/workspace-overlap.js +327 -0
- package/dist/local/workspace-overlap.js.map +1 -0
- package/dist/local/workspace-paths.d.ts +4 -0
- package/dist/local/workspace-paths.d.ts.map +1 -0
- package/dist/local/workspace-paths.js +8 -0
- package/dist/local/workspace-paths.js.map +1 -0
- package/dist/local/workspace-pick-runtime.d.ts +70 -0
- package/dist/local/workspace-pick-runtime.d.ts.map +1 -0
- package/dist/local/workspace-pick-runtime.js +177 -0
- package/dist/local/workspace-pick-runtime.js.map +1 -0
- package/dist/local/workspace-pick-store.d.ts +33 -0
- package/dist/local/workspace-pick-store.d.ts.map +1 -0
- package/dist/local/workspace-pick-store.js +43 -0
- package/dist/local/workspace-pick-store.js.map +1 -0
- package/dist/local/workspace-pick.d.ts +73 -0
- package/dist/local/workspace-pick.d.ts.map +1 -0
- package/dist/local/workspace-pick.js +253 -0
- package/dist/local/workspace-pick.js.map +1 -0
- package/dist/local/workspace-portable-authoring.d.ts +43 -0
- package/dist/local/workspace-portable-authoring.d.ts.map +1 -0
- package/dist/local/workspace-portable-authoring.js +128 -0
- package/dist/local/workspace-portable-authoring.js.map +1 -0
- package/dist/local/workspace-prune.d.ts +63 -0
- package/dist/local/workspace-prune.d.ts.map +1 -0
- package/dist/local/workspace-prune.js +267 -0
- package/dist/local/workspace-prune.js.map +1 -0
- package/dist/local/workspace-remote-fetch.d.ts +82 -0
- package/dist/local/workspace-remote-fetch.d.ts.map +1 -0
- package/dist/local/workspace-remote-fetch.js +409 -0
- package/dist/local/workspace-remote-fetch.js.map +1 -0
- package/dist/local/workspace-remote-provider.d.ts +97 -0
- package/dist/local/workspace-remote-provider.d.ts.map +1 -0
- package/dist/local/workspace-remote-provider.js +400 -0
- package/dist/local/workspace-remote-provider.js.map +1 -0
- package/dist/local/workspace-remote-pull-request-journal.d.ts +49 -0
- package/dist/local/workspace-remote-pull-request-journal.d.ts.map +1 -0
- package/dist/local/workspace-remote-pull-request-journal.js +63 -0
- package/dist/local/workspace-remote-pull-request-journal.js.map +1 -0
- package/dist/local/workspace-remote-pull-request.d.ts +36 -0
- package/dist/local/workspace-remote-pull-request.d.ts.map +1 -0
- package/dist/local/workspace-remote-pull-request.js +158 -0
- package/dist/local/workspace-remote-pull-request.js.map +1 -0
- package/dist/local/workspace-remote-push.d.ts +88 -0
- package/dist/local/workspace-remote-push.d.ts.map +1 -0
- package/dist/local/workspace-remote-push.js +297 -0
- package/dist/local/workspace-remote-push.js.map +1 -0
- package/dist/local/workspace-remote-topic-journal.d.ts +56 -0
- package/dist/local/workspace-remote-topic-journal.d.ts.map +1 -0
- package/dist/local/workspace-remote-topic-journal.js +121 -0
- package/dist/local/workspace-remote-topic-journal.js.map +1 -0
- package/dist/local/workspace-remote-topic.d.ts +45 -0
- package/dist/local/workspace-remote-topic.d.ts.map +1 -0
- package/dist/local/workspace-remote-topic.js +204 -0
- package/dist/local/workspace-remote-topic.js.map +1 -0
- package/dist/local/workspace-remote-transfer.d.ts +76 -0
- package/dist/local/workspace-remote-transfer.d.ts.map +1 -0
- package/dist/local/workspace-remote-transfer.js +270 -0
- package/dist/local/workspace-remote-transfer.js.map +1 -0
- package/dist/local/workspace-resolution.d.ts +145 -0
- package/dist/local/workspace-resolution.d.ts.map +1 -0
- package/dist/local/workspace-resolution.js +479 -0
- package/dist/local/workspace-resolution.js.map +1 -0
- package/dist/local/workspace-resolver-provider.d.ts +72 -0
- package/dist/local/workspace-resolver-provider.d.ts.map +1 -0
- package/dist/local/workspace-resolver-provider.js +312 -0
- package/dist/local/workspace-resolver-provider.js.map +1 -0
- package/dist/local/workspace-runtime-handoff.d.ts +52 -0
- package/dist/local/workspace-runtime-handoff.d.ts.map +1 -0
- package/dist/local/workspace-runtime-handoff.js +237 -0
- package/dist/local/workspace-runtime-handoff.js.map +1 -0
- package/dist/local/workspace-runtime-lease.d.ts +26 -0
- package/dist/local/workspace-runtime-lease.d.ts.map +1 -0
- package/dist/local/workspace-runtime-lease.js +123 -0
- package/dist/local/workspace-runtime-lease.js.map +1 -0
- package/dist/local/workspace-runtime.d.ts +690 -0
- package/dist/local/workspace-runtime.d.ts.map +1 -0
- package/dist/local/workspace-runtime.js +2124 -0
- package/dist/local/workspace-runtime.js.map +1 -0
- package/dist/local/workspace-scope.d.ts +70 -0
- package/dist/local/workspace-scope.d.ts.map +1 -0
- package/dist/local/workspace-scope.js +206 -0
- package/dist/local/workspace-scope.js.map +1 -0
- package/dist/local/workspace-sources.d.ts +159 -0
- package/dist/local/workspace-sources.d.ts.map +1 -0
- package/dist/local/workspace-sources.js +570 -0
- package/dist/local/workspace-sources.js.map +1 -0
- package/dist/local/workspace-store-database.d.ts +5 -0
- package/dist/local/workspace-store-database.d.ts.map +1 -0
- package/dist/local/workspace-store-database.js +726 -0
- package/dist/local/workspace-store-database.js.map +1 -0
- package/dist/local/workspace-store.d.ts +261 -0
- package/dist/local/workspace-store.d.ts.map +1 -0
- package/dist/local/workspace-store.js +831 -0
- package/dist/local/workspace-store.js.map +1 -0
- package/dist/local/workspace-topology-runtime.d.ts +27 -0
- package/dist/local/workspace-topology-runtime.d.ts.map +1 -0
- package/dist/local/workspace-topology-runtime.js +396 -0
- package/dist/local/workspace-topology-runtime.js.map +1 -0
- package/dist/local/workspace-topology.d.ts +140 -0
- package/dist/local/workspace-topology.d.ts.map +1 -0
- package/dist/local/workspace-topology.js +359 -0
- package/dist/local/workspace-topology.js.map +1 -0
- package/dist/local/workspace-upstream-inspection.d.ts +60 -0
- package/dist/local/workspace-upstream-inspection.d.ts.map +1 -0
- package/dist/local/workspace-upstream-inspection.js +189 -0
- package/dist/local/workspace-upstream-inspection.js.map +1 -0
- package/dist/local/workspace-upstream-runtime.d.ts +91 -0
- package/dist/local/workspace-upstream-runtime.d.ts.map +1 -0
- package/dist/local/workspace-upstream-runtime.js +270 -0
- package/dist/local/workspace-upstream-runtime.js.map +1 -0
- package/dist/local/workspace-upstream-store.d.ts +51 -0
- package/dist/local/workspace-upstream-store.d.ts.map +1 -0
- package/dist/local/workspace-upstream-store.js +475 -0
- package/dist/local/workspace-upstream-store.js.map +1 -0
- package/dist/local/workspace-viewer-server.d.ts +136 -0
- package/dist/local/workspace-viewer-server.d.ts.map +1 -0
- package/dist/local/workspace-viewer-server.js +1097 -0
- package/dist/local/workspace-viewer-server.js.map +1 -0
- package/dist/local/workspace-viewer-snapshot.d.ts +49 -0
- package/dist/local/workspace-viewer-snapshot.d.ts.map +1 -0
- package/dist/local/workspace-viewer-snapshot.js +91 -0
- package/dist/local/workspace-viewer-snapshot.js.map +1 -0
- package/dist/local/worktree-projection.d.ts +76 -0
- package/dist/local/worktree-projection.d.ts.map +1 -0
- package/dist/local/worktree-projection.js +470 -0
- package/dist/local/worktree-projection.js.map +1 -0
- package/dist/mcp/client-identity.d.ts +8 -0
- package/dist/mcp/client-identity.d.ts.map +1 -0
- package/dist/mcp/client-identity.js +33 -0
- package/dist/mcp/client-identity.js.map +1 -0
- package/dist/mcp/graph-to-code.d.ts +53 -0
- package/dist/mcp/graph-to-code.d.ts.map +1 -0
- package/dist/mcp/graph-to-code.js +1624 -0
- package/dist/mcp/graph-to-code.js.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/stdio/graph-to-code.d.ts +3 -0
- package/dist/mcp/stdio/graph-to-code.d.ts.map +1 -0
- package/dist/mcp/stdio/graph-to-code.js +55 -0
- package/dist/mcp/stdio/graph-to-code.js.map +1 -0
- package/dist/mcp/stdio/worktree.d.ts +3 -0
- package/dist/mcp/stdio/worktree.d.ts.map +1 -0
- package/dist/mcp/stdio/worktree.js +67 -0
- package/dist/mcp/stdio/worktree.js.map +1 -0
- package/dist/mcp/worktree-claim-status.d.ts +162 -0
- package/dist/mcp/worktree-claim-status.d.ts.map +1 -0
- package/dist/mcp/worktree-claim-status.js +113 -0
- package/dist/mcp/worktree-claim-status.js.map +1 -0
- package/dist/mcp/worktree-dispatch.d.ts +13 -0
- package/dist/mcp/worktree-dispatch.d.ts.map +1 -0
- package/dist/mcp/worktree-dispatch.js +52 -0
- package/dist/mcp/worktree-dispatch.js.map +1 -0
- package/dist/mcp/worktree-errors.d.ts +15 -0
- package/dist/mcp/worktree-errors.d.ts.map +1 -0
- package/dist/mcp/worktree-errors.js +17 -0
- package/dist/mcp/worktree-errors.js.map +1 -0
- package/dist/mcp/worktree-health.d.ts +12 -0
- package/dist/mcp/worktree-health.d.ts.map +1 -0
- package/dist/mcp/worktree-health.js +32 -0
- package/dist/mcp/worktree-health.js.map +1 -0
- package/dist/mcp/worktree-results.d.ts +15 -0
- package/dist/mcp/worktree-results.d.ts.map +1 -0
- package/dist/mcp/worktree-results.js +12 -0
- package/dist/mcp/worktree-results.js.map +1 -0
- package/dist/mcp/worktree-task-context.d.ts +57 -0
- package/dist/mcp/worktree-task-context.d.ts.map +1 -0
- package/dist/mcp/worktree-task-context.js +45 -0
- package/dist/mcp/worktree-task-context.js.map +1 -0
- package/dist/mcp/worktree-tools.d.ts +2648 -0
- package/dist/mcp/worktree-tools.d.ts.map +1 -0
- package/dist/mcp/worktree-tools.js +192 -0
- package/dist/mcp/worktree-tools.js.map +1 -0
- package/dist/mcp/worktree.d.ts +33 -0
- package/dist/mcp/worktree.d.ts.map +1 -0
- package/dist/mcp/worktree.js +551 -0
- package/dist/mcp/worktree.js.map +1 -0
- package/dist/own-package.d.ts +13 -0
- package/dist/own-package.d.ts.map +1 -0
- package/dist/own-package.js +32 -0
- package/dist/own-package.js.map +1 -0
- package/dist/packages-browse.d.ts +107 -0
- package/dist/packages-browse.d.ts.map +1 -0
- package/dist/packages-browse.js +646 -0
- package/dist/packages-browse.js.map +1 -0
- package/dist/packages-cli.d.ts +8 -0
- package/dist/packages-cli.d.ts.map +1 -0
- package/dist/packages-cli.js +184 -0
- package/dist/packages-cli.js.map +1 -0
- package/dist/packages-uses.d.ts +46 -0
- package/dist/packages-uses.d.ts.map +1 -0
- package/dist/packages-uses.js +737 -0
- package/dist/packages-uses.js.map +1 -0
- package/dist/portable.d.ts +7 -0
- package/dist/portable.d.ts.map +1 -0
- package/dist/portable.js +14 -0
- package/dist/portable.js.map +1 -0
- package/dist/quiet-warnings.d.ts +3 -0
- package/dist/quiet-warnings.d.ts.map +1 -0
- package/dist/quiet-warnings.js +21 -0
- package/dist/quiet-warnings.js.map +1 -0
- package/dist/view-seam.d.ts +62 -0
- package/dist/view-seam.d.ts.map +1 -0
- package/dist/view-seam.js +70 -0
- package/dist/view-seam.js.map +1 -0
- package/generated/graph-packages/index.json +5 -0
- package/graph-packages.lock.json +4 -0
- package/package.json +81 -0
- package/viewer/assets/index-BZKGDARF.css +1 -0
- package/viewer/assets/index-DWu0CbZZ.js +272 -0
- package/viewer/index.html +13 -0
|
@@ -0,0 +1,2124 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { getLibraryOriginWarnings } from '@software-dlc/graph/validator';
|
|
5
|
+
import { composeGraphProjection, diffProjectionGraphs, extendScopedProjection, graphHasProjectionEntity, hasProjectionManifest, isScopedProjection, materializeGraphProjection, projectionDiffHasChanges, projectionHasManagedContent, projectionScopeOfTaskScope, recordScopedProjectionFiles, removeEmptyGraphProjection, removeProjectionEntity, validateProjectionGraph, writeJsonAtomically } from './projection.js';
|
|
6
|
+
import { heldPackages, packageUseCommitIssues } from './package-uses-check.js';
|
|
7
|
+
import { introducedBrokenIssues, introducedValidationWarnings } from './workspace-merge.js';
|
|
8
|
+
import { planEvidenceSign, signedMeta } from './workspace-evidence.js';
|
|
9
|
+
import { evidenceWith, ReferenceLimitError, sourceIndex, sourceReference } from './workspace-sources.js';
|
|
10
|
+
import { evidenceStatus } from './workspace-evidence-status.js';
|
|
11
|
+
import { droppedEvidenceReferences, evidenceDropWarning } from './workspace-evidence-drop.js';
|
|
12
|
+
import { assertLocalWorkspacePathsIgnored, createDetachedGitWorktree, discoverGitRemoteIdentity, gitRemoteNames, discoverGitWorktree, removeGitWorktree, namingRemote, repositoryNames, trackedRemoteName } from './git.js';
|
|
13
|
+
import { LocalWorkspaceGraphStore, localGraphRootHash, resolveLocalRepositoryStateDirectory } from './workspace-graph-store.js';
|
|
14
|
+
import { blastRadius } from './blast-radius.js';
|
|
15
|
+
import { findGraphEntities, showGraphEntities } from './graph-read.js';
|
|
16
|
+
import { readFolderMembers, repositoryTaskScope } from './folder-members.js';
|
|
17
|
+
import { variantMemberFormIds, variantSyncIds, variantsReport } from './variants.js';
|
|
18
|
+
import { withWorkspaceOperationLock } from './workspace-lock.js';
|
|
19
|
+
import { allocationUnits, compareLocalWorkspacePlans } from './workspace-overlap.js';
|
|
20
|
+
import { allocationUnitKey, combineValidationUnits, firstOwnChanges } from './workspace-overlap-allocation.js';
|
|
21
|
+
import { LocalWorkspaceCommitResolver } from './workspace-commit-resolver.js';
|
|
22
|
+
import { LocalWorkspaceAuthorityGuard } from './workspace-authority-guard.js';
|
|
23
|
+
import { readBuiltView as readBuiltGraph } from './workspace-built-content.js';
|
|
24
|
+
import { listClaims } from './workspace-claim-views.js';
|
|
25
|
+
import { claimStatus, gitClaimStatusReader } from './workspace-claim-status.js';
|
|
26
|
+
import { workspaceControlDirectory as controlDirectory, workspaceProjectionDirectory as projectionDirectory } from './workspace-paths.js';
|
|
27
|
+
import { LocalWorkspaceStore } from './workspace-store.js';
|
|
28
|
+
import { LocalWorkspaceHandoffRuntime } from './workspace-runtime-handoff.js';
|
|
29
|
+
import { LocalWorkspacePruner } from './workspace-prune.js';
|
|
30
|
+
import { inspectLocalWorkspaceAuthority } from './workspace-authority.js';
|
|
31
|
+
import { LocalWorkspaceResolverProviderStore } from './workspace-resolver-provider.js';
|
|
32
|
+
import { LocalWorkspaceRemoteProviderStore } from './workspace-remote-provider.js';
|
|
33
|
+
import { LocalWorkspaceRemoteFetcher } from './workspace-remote-fetch.js';
|
|
34
|
+
import { LocalWorkspaceBranchRuntime } from './workspace-branch-runtime.js';
|
|
35
|
+
import { LocalWorkspaceIntegrationRuntime } from './workspace-integration-runtime.js';
|
|
36
|
+
import { LocalWorkspacePickRuntime } from './workspace-pick-runtime.js';
|
|
37
|
+
import { LocalWorkspaceEnvironmentRuntime } from './workspace-environment-runtime.js';
|
|
38
|
+
import { LocalWorkspaceUpstreamRuntime } from './workspace-upstream-runtime.js';
|
|
39
|
+
import { LocalWorkspaceRemotePusher } from './workspace-remote-push.js';
|
|
40
|
+
import { LocalWorkspaceRemoteTopicPublisher } from './workspace-remote-topic.js';
|
|
41
|
+
import { LocalWorkspaceRemotePullRequestCreator } from './workspace-remote-pull-request.js';
|
|
42
|
+
import { LocalWorkspacePortableAuthoring } from './workspace-portable-authoring.js';
|
|
43
|
+
import { assertGraphChangesWithinTaskScope, assertNodeTypeRulesOnChangedNodes, assertNoRetiredKeysOnChangedNodes, graphChangeCoordinates, ruleCovers, local_workspace_task_scope_schema } from './workspace-scope.js';
|
|
44
|
+
import { assertIntroducedVocabularyAdmitted } from './workspace-admission.js';
|
|
45
|
+
import { canonicalizePortableGraph, stringifyCanonicalGraph } from './canonical.js';
|
|
46
|
+
import { introducedDanglingDocumentEmbeds } from './document-embeds.js';
|
|
47
|
+
import { readWorkspaceTopology, removeWorkspaceTopology, topologyBindingMatch, writeWorkspaceTopology } from './workspace-topology.js';
|
|
48
|
+
import { registerWorkspaceRuntimeLease, releaseWorkspaceRuntimeLease, withGraphRepositoryTopologyLock } from './workspace-runtime-lease.js';
|
|
49
|
+
/**
|
|
50
|
+
* When init and attach began binding the naming remote: e33d186f reached
|
|
51
|
+
* origin/main in the production merge 866a9426cd. A binding made on origin
|
|
52
|
+
* since by a CLI from before that release is not told apart and keeps origin.
|
|
53
|
+
*/
|
|
54
|
+
const NAMING_REMOTE_RULE_AT = '2026-10-28T21:14:06.000Z';
|
|
55
|
+
/**
|
|
56
|
+
* The node types a graph starts from, outside in (the local-graph-start
|
|
57
|
+
* skill): one is missing when no node of it exists. Complete graph validation
|
|
58
|
+
* does not ask for them, so a commit only warns.
|
|
59
|
+
*/
|
|
60
|
+
const STARTING_NODE_TYPES = [
|
|
61
|
+
['system', 'the product'],
|
|
62
|
+
['actor', 'the people and outside systems that use it']
|
|
63
|
+
];
|
|
64
|
+
/** An outside system, or one nested in another node, is not the product. */
|
|
65
|
+
function startsTheGraph(node) {
|
|
66
|
+
if (node.type !== 'system')
|
|
67
|
+
return true;
|
|
68
|
+
const system = node.data?.data
|
|
69
|
+
?.system;
|
|
70
|
+
return !node.parentId && system?.external !== true;
|
|
71
|
+
}
|
|
72
|
+
function graphShapeWarnings(graph) {
|
|
73
|
+
const types = new Set(graph.nodes.filter(startsTheGraph).map((node) => node.type));
|
|
74
|
+
return STARTING_NODE_TYPES.filter(([type]) => !types.has(type)).map(([type, what]) => `The graph has no ${type} node for ${what}.`);
|
|
75
|
+
}
|
|
76
|
+
function brokenValidationMessages(graph) {
|
|
77
|
+
return validateProjectionGraph(graph)
|
|
78
|
+
.issues.filter((issue) => issue.status === 'broken')
|
|
79
|
+
.map((issue) => `${issue.code}: ${issue.message}`);
|
|
80
|
+
}
|
|
81
|
+
export class LocalWorkspaceRuntime {
|
|
82
|
+
git;
|
|
83
|
+
state_dir;
|
|
84
|
+
store;
|
|
85
|
+
graphs;
|
|
86
|
+
topology;
|
|
87
|
+
runtime_lease;
|
|
88
|
+
closed = false;
|
|
89
|
+
resolver_store;
|
|
90
|
+
remote_store;
|
|
91
|
+
remote_fetcher;
|
|
92
|
+
remote_pusher;
|
|
93
|
+
remote_topic_publisher;
|
|
94
|
+
remote_pull_request_creator;
|
|
95
|
+
portable_authoring;
|
|
96
|
+
commits;
|
|
97
|
+
authority;
|
|
98
|
+
handoff_runtime;
|
|
99
|
+
pruner;
|
|
100
|
+
branch_runtime;
|
|
101
|
+
integration_runtime;
|
|
102
|
+
pick_runtime;
|
|
103
|
+
upstream_runtime;
|
|
104
|
+
environment_runtime;
|
|
105
|
+
materialize_projection;
|
|
106
|
+
constructor(input) {
|
|
107
|
+
this.git = input.git;
|
|
108
|
+
this.state_dir = input.state_dir;
|
|
109
|
+
this.store = input.store;
|
|
110
|
+
this.topology = input.topology;
|
|
111
|
+
this.runtime_lease = input.runtime_lease;
|
|
112
|
+
this.materialize_projection =
|
|
113
|
+
input.materialize_projection ?? materializeGraphProjection;
|
|
114
|
+
this.graphs = new LocalWorkspaceGraphStore(input.state_dir);
|
|
115
|
+
this.commits = new LocalWorkspaceCommitResolver(input.store, this.graphs);
|
|
116
|
+
this.authority = new LocalWorkspaceAuthorityGuard(input.git.repository_root, input.git.git_common_dir, input.store);
|
|
117
|
+
this.branch_runtime = new LocalWorkspaceBranchRuntime(input.git, input.store, this.authority, this.commits, this.materialize_projection);
|
|
118
|
+
this.integration_runtime = new LocalWorkspaceIntegrationRuntime(input.store, this.graphs, this.authority, this.commits, this.materialize_projection);
|
|
119
|
+
this.pick_runtime = new LocalWorkspacePickRuntime(input.store, this.graphs, this.authority, this.commits, this.materialize_projection);
|
|
120
|
+
this.upstream_runtime = new LocalWorkspaceUpstreamRuntime(input.store, this.authority, this.commits, this.materialize_projection);
|
|
121
|
+
this.resolver_store = new LocalWorkspaceResolverProviderStore(input.state_dir);
|
|
122
|
+
this.remote_store = new LocalWorkspaceRemoteProviderStore(input.state_dir);
|
|
123
|
+
this.remote_fetcher = new LocalWorkspaceRemoteFetcher(this.remote_store, input.store.remote_tracking, input.store.portable_commits, this.graphs);
|
|
124
|
+
this.remote_pusher = new LocalWorkspaceRemotePusher(this.remote_store, input.store.remote_push, input.store.portable_commits, this.graphs, input.state_dir);
|
|
125
|
+
this.remote_topic_publisher = new LocalWorkspaceRemoteTopicPublisher(this.remote_store, input.store.remote_topic, input.store.portable_commits, this.graphs, input.state_dir);
|
|
126
|
+
this.remote_pull_request_creator =
|
|
127
|
+
new LocalWorkspaceRemotePullRequestCreator(this.remote_store, input.store.remote_pull_request, input.state_dir);
|
|
128
|
+
this.portable_authoring = new LocalWorkspacePortableAuthoring(input.store.portable_commits, input.store.remote_tracking);
|
|
129
|
+
this.environment_runtime = new LocalWorkspaceEnvironmentRuntime(input.store, this.authority, this.resolver_store, this.remote_store, this.remote_fetcher, this.remote_pusher, this.remote_topic_publisher, this.remote_pull_request_creator, this.portable_authoring);
|
|
130
|
+
this.handoff_runtime = new LocalWorkspaceHandoffRuntime({
|
|
131
|
+
repository_root: input.git.repository_root,
|
|
132
|
+
git_common_dir: input.git.git_common_dir,
|
|
133
|
+
store: input.store,
|
|
134
|
+
graphs: this.graphs,
|
|
135
|
+
materialize_projection: this.materialize_projection
|
|
136
|
+
});
|
|
137
|
+
this.pruner = new LocalWorkspacePruner({
|
|
138
|
+
repository_root: input.git.repository_root,
|
|
139
|
+
current_worktree_path: input.git.repository_root,
|
|
140
|
+
store: input.store,
|
|
141
|
+
graphs: this.graphs,
|
|
142
|
+
assert_actor_authority: async (workspace_id) => {
|
|
143
|
+
await this.authority.requireCurrent(workspace_id);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
static async open(input = {}) {
|
|
148
|
+
const git = await discoverGitWorktree(input.cwd);
|
|
149
|
+
const initial_topology = await readWorkspaceTopology(git.repository_root);
|
|
150
|
+
const open_runtime = async (topology) => {
|
|
151
|
+
if (topology &&
|
|
152
|
+
input.state_dir &&
|
|
153
|
+
resolve(input.state_dir) !== topology.state_dir) {
|
|
154
|
+
throw new Error('Explicit state directory conflicts with checkout topology.');
|
|
155
|
+
}
|
|
156
|
+
if (topology &&
|
|
157
|
+
input.graph_repository_id &&
|
|
158
|
+
input.graph_repository_id !== topology.graph_repository_id) {
|
|
159
|
+
throw new Error('Explicit graph repository identity conflicts with checkout topology.');
|
|
160
|
+
}
|
|
161
|
+
const state_dir = resolveLocalRepositoryStateDirectory({
|
|
162
|
+
git_common_dir: git.git_common_dir,
|
|
163
|
+
override: input.state_dir ?? topology?.state_dir
|
|
164
|
+
});
|
|
165
|
+
const store = await LocalWorkspaceStore.open({
|
|
166
|
+
state_dir,
|
|
167
|
+
git_common_dir: git.git_common_dir,
|
|
168
|
+
expected_repository_id: input.graph_repository_id ?? topology?.graph_repository_id
|
|
169
|
+
});
|
|
170
|
+
try {
|
|
171
|
+
if (topology) {
|
|
172
|
+
const workspace = store.getWorkspaceByGitWorktreePath(git.repository_root);
|
|
173
|
+
if (!workspace ||
|
|
174
|
+
workspace.status !== 'active' ||
|
|
175
|
+
workspace.id !== topology.workspace_id) {
|
|
176
|
+
throw new Error('Checkout topology does not identify its active graph workspace.');
|
|
177
|
+
}
|
|
178
|
+
const binding = store.getSourceRepositoryBinding(git.git_common_dir);
|
|
179
|
+
const match = topologyBindingMatch({ topology, binding });
|
|
180
|
+
if (match === 'mismatch') {
|
|
181
|
+
throw new Error('Checkout topology does not match its source repository binding.');
|
|
182
|
+
}
|
|
183
|
+
if (match === 'moved') {
|
|
184
|
+
// The binding moved to upstream; this checkout catches up
|
|
185
|
+
topology = {
|
|
186
|
+
...topology,
|
|
187
|
+
source_remote_identity: binding.remote_identity
|
|
188
|
+
};
|
|
189
|
+
await writeWorkspaceTopology({
|
|
190
|
+
repository_root: git.repository_root,
|
|
191
|
+
topology
|
|
192
|
+
}).catch(() => undefined);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const runtime_lease = await registerWorkspaceRuntimeLease({
|
|
196
|
+
graph_repository_id: store.repository.repository_id,
|
|
197
|
+
state_dir,
|
|
198
|
+
repository_root: git.repository_root
|
|
199
|
+
});
|
|
200
|
+
try {
|
|
201
|
+
return new LocalWorkspaceRuntime({
|
|
202
|
+
git,
|
|
203
|
+
state_dir,
|
|
204
|
+
store,
|
|
205
|
+
topology,
|
|
206
|
+
runtime_lease,
|
|
207
|
+
materialize_projection: input.materialize_projection
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
releaseWorkspaceRuntimeLease(runtime_lease);
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
store.close();
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const expected_repository_id = input.graph_repository_id ?? initial_topology?.graph_repository_id;
|
|
221
|
+
if (expected_repository_id) {
|
|
222
|
+
return withGraphRepositoryTopologyLock({
|
|
223
|
+
graph_repository_id: expected_repository_id,
|
|
224
|
+
operation: 'open-runtime',
|
|
225
|
+
run: async () => {
|
|
226
|
+
const topology = await readWorkspaceTopology(git.repository_root);
|
|
227
|
+
if (initial_topology &&
|
|
228
|
+
topology?.graph_repository_id !==
|
|
229
|
+
initial_topology.graph_repository_id) {
|
|
230
|
+
throw new Error('Checkout topology changed while opening runtime.');
|
|
231
|
+
}
|
|
232
|
+
return open_runtime(topology);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const runtime = await open_runtime(null);
|
|
237
|
+
return runtime;
|
|
238
|
+
}
|
|
239
|
+
close() {
|
|
240
|
+
if (this.closed)
|
|
241
|
+
return;
|
|
242
|
+
this.closed = true;
|
|
243
|
+
try {
|
|
244
|
+
this.store.close();
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
releaseWorkspaceRuntimeLease(this.runtime_lease);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async initializeCurrentWorkspace(input = {}) {
|
|
251
|
+
await assertLocalWorkspacePathsIgnored(this.git.repository_root);
|
|
252
|
+
const existing = this.store.getWorkspaceByGitWorktreePath(this.git.repository_root);
|
|
253
|
+
if (existing) {
|
|
254
|
+
if (existing.status !== 'active') {
|
|
255
|
+
throw new Error('The current Git worktree is registered as closed.');
|
|
256
|
+
}
|
|
257
|
+
await this.persistCurrentTopology(existing, input.storage_mode);
|
|
258
|
+
return this.getWorkspaceStatus(existing.id);
|
|
259
|
+
}
|
|
260
|
+
const projection_dir = join(this.git.repository_root, 'sdlc');
|
|
261
|
+
// A repository that never had a hosted checkout has no projection yet:
|
|
262
|
+
// start its local graph empty, so local-only use needs no account. A
|
|
263
|
+
// failed init removes that empty projection again, so the worktree is
|
|
264
|
+
// left as it was found.
|
|
265
|
+
const starts_empty = !(await projectionHasManagedContent(projection_dir));
|
|
266
|
+
try {
|
|
267
|
+
if (starts_empty) {
|
|
268
|
+
await materializeGraphProjection({
|
|
269
|
+
projection_dir,
|
|
270
|
+
graph: {
|
|
271
|
+
nodes: [],
|
|
272
|
+
edges: [],
|
|
273
|
+
tags: [],
|
|
274
|
+
paths: [],
|
|
275
|
+
path_relations: []
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return await this.registerProjectionAsWorkspace(projection_dir, input);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
if (starts_empty)
|
|
283
|
+
await removeEmptyGraphProjection(projection_dir);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async registerProjectionAsWorkspace(projection_dir, input) {
|
|
288
|
+
const { graph } = await composeGraphProjection(projection_dir);
|
|
289
|
+
const issues = brokenValidationMessages(graph);
|
|
290
|
+
if (issues.length > 0) {
|
|
291
|
+
throw new Error(`Cannot initialize invalid projection: ${issues.join('; ')}`);
|
|
292
|
+
}
|
|
293
|
+
const root_hash = await this.graphs.writeGraph(graph);
|
|
294
|
+
if (input.source_repository_association_id) {
|
|
295
|
+
await this.bindCurrentSourceRepository({
|
|
296
|
+
source_repository_association_id: input.source_repository_association_id,
|
|
297
|
+
remote_name: input.source_remote_name
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const workspace = this.store.registerForegroundWorkspace({
|
|
301
|
+
actor_kind: input.actor_kind ?? 'human',
|
|
302
|
+
git_worktree_path: this.git.repository_root,
|
|
303
|
+
git_dir: this.git.git_dir,
|
|
304
|
+
git_common_dir: this.git.git_common_dir,
|
|
305
|
+
git_head_commit_id: this.git.head,
|
|
306
|
+
sdlc_head_commit_id: root_hash,
|
|
307
|
+
attached_ref: this.git.ref
|
|
308
|
+
});
|
|
309
|
+
try {
|
|
310
|
+
await mkdir(controlDirectory(workspace), {
|
|
311
|
+
recursive: true,
|
|
312
|
+
mode: 0o700
|
|
313
|
+
});
|
|
314
|
+
const status = await this.getWorkspaceStatus(workspace.id);
|
|
315
|
+
await this.persistCurrentTopology(workspace, input.storage_mode);
|
|
316
|
+
return status;
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
this.store.closeWorkspace({
|
|
320
|
+
actor_workspace_id: workspace.id,
|
|
321
|
+
workspace_id: workspace.id,
|
|
322
|
+
expected_head_commit_id: workspace.head_sdlc_commit_id
|
|
323
|
+
});
|
|
324
|
+
const binding = this.store.getSourceRepositoryBinding(this.git.git_common_dir);
|
|
325
|
+
if (binding) {
|
|
326
|
+
this.store.unbindSourceRepository({
|
|
327
|
+
git_common_dir: this.git.git_common_dir,
|
|
328
|
+
expected_source_repository_association_id: binding.source_repository_association_id
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async persistCurrentTopology(workspace, storage_mode, repository_root = this.git.repository_root, git_common_dir = this.git.git_common_dir) {
|
|
335
|
+
const binding = this.store.getSourceRepositoryBinding(git_common_dir);
|
|
336
|
+
const default_state_dir = resolveLocalRepositoryStateDirectory({
|
|
337
|
+
git_common_dir
|
|
338
|
+
});
|
|
339
|
+
const now = new Date().toISOString();
|
|
340
|
+
const written = await writeWorkspaceTopology({
|
|
341
|
+
repository_root,
|
|
342
|
+
topology: {
|
|
343
|
+
version: 1,
|
|
344
|
+
graph_repository_id: this.store.repository.repository_id,
|
|
345
|
+
workspace_id: workspace.id,
|
|
346
|
+
state_dir: this.state_dir,
|
|
347
|
+
storage_mode: storage_mode ??
|
|
348
|
+
(this.state_dir === default_state_dir
|
|
349
|
+
? 'self_contained'
|
|
350
|
+
: 'machine_shared'),
|
|
351
|
+
source_repository_association_id: binding?.source_repository_association_id ?? null,
|
|
352
|
+
source_remote_identity: binding?.remote_identity ?? null,
|
|
353
|
+
created_at: now,
|
|
354
|
+
updated_at: now
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
if (repository_root === this.git.repository_root) {
|
|
358
|
+
this.topology = written.topology;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async bindCurrentSourceRepository(input) {
|
|
362
|
+
const remote = await discoverGitRemoteIdentity(this.git.repository_root, input.remote_name ??
|
|
363
|
+
this.store.getSourceRepositoryBinding(this.git.git_common_dir)
|
|
364
|
+
?.remote_name ??
|
|
365
|
+
(await this.defaultSourceRemote(input.source_repository_association_id)));
|
|
366
|
+
return this.store.bindSourceRepository({
|
|
367
|
+
git_common_dir: this.git.git_common_dir,
|
|
368
|
+
source_repository_association_id: input.source_repository_association_id,
|
|
369
|
+
remote_name: remote.remote_name,
|
|
370
|
+
remote_url: remote.remote_url,
|
|
371
|
+
remote_identity: remote.repository_identity
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The remote a new binding takes when none is named: the one the
|
|
376
|
+
* association is bound to from another clone, when this repository has a
|
|
377
|
+
* remote of that identity; else the remote its references are named from
|
|
378
|
+
* (namingRemote), else origin. A binding the repository already has keeps
|
|
379
|
+
* its own remote (bindCurrentSourceRepository).
|
|
380
|
+
*/
|
|
381
|
+
async defaultSourceRemote(source_repository_association_id) {
|
|
382
|
+
const root = this.git.repository_root;
|
|
383
|
+
const naming = (await namingRemote(root)) ?? 'origin';
|
|
384
|
+
const bound = this.store.getSourceAssociationBinding(source_repository_association_id);
|
|
385
|
+
if (!bound)
|
|
386
|
+
return naming;
|
|
387
|
+
for (const name of new Set([
|
|
388
|
+
naming,
|
|
389
|
+
bound.remote_name,
|
|
390
|
+
...(await gitRemoteNames(root))
|
|
391
|
+
])) {
|
|
392
|
+
const identity = await discoverGitRemoteIdentity(root, name).catch(() => null);
|
|
393
|
+
if (identity?.repository_identity === bound.remote_identity)
|
|
394
|
+
return name;
|
|
395
|
+
}
|
|
396
|
+
return naming;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* A binding made before init and attach bound the naming remote (2026-10-28)
|
|
400
|
+
* may name a fork's origin while its references name upstream: this moves
|
|
401
|
+
* it to upstream, with the topology of every attached checkout of the
|
|
402
|
+
* repository, so a workspace commit's remote identity names the repository
|
|
403
|
+
* its references name. Only origin gives way, only to upstream, as in
|
|
404
|
+
* repositoryNames, and only for a binding made before that rule, so a
|
|
405
|
+
* remote chosen since is kept. Only the binding row moves: a checkout's
|
|
406
|
+
* topology still naming origin matches it as moved, and opening the
|
|
407
|
+
* checkout rewrites it. Returns the identities it moved between, or null.
|
|
408
|
+
*/
|
|
409
|
+
async realignForkBinding(repository_root = this.git.repository_root, git_common_dir = this.git.git_common_dir) {
|
|
410
|
+
const binding = this.store.getSourceRepositoryBinding(git_common_dir);
|
|
411
|
+
if (binding?.remote_name !== 'origin')
|
|
412
|
+
return null;
|
|
413
|
+
if (binding.created_at >= NAMING_REMOTE_RULE_AT)
|
|
414
|
+
return null;
|
|
415
|
+
if (!(await gitRemoteNames(repository_root)).includes('upstream')) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
if ((await namingRemote(repository_root)) !== 'upstream')
|
|
419
|
+
return null;
|
|
420
|
+
const origin = await discoverGitRemoteIdentity(repository_root, 'origin');
|
|
421
|
+
// A changed origin is the binding mismatch, reported where it is checked
|
|
422
|
+
if (origin.repository_identity !== binding.remote_identity)
|
|
423
|
+
return null;
|
|
424
|
+
const upstream = await discoverGitRemoteIdentity(repository_root, 'upstream');
|
|
425
|
+
if (upstream.repository_identity === binding.remote_identity)
|
|
426
|
+
return null;
|
|
427
|
+
// One store transaction: every checkout's topology still naming origin is
|
|
428
|
+
// read as moved (topologyBindingMatch), and rewritten when it is opened
|
|
429
|
+
const moved = this.store.rebindSourceRepositoryRemote({
|
|
430
|
+
git_common_dir,
|
|
431
|
+
expected_remote_name: 'origin',
|
|
432
|
+
remote_name: upstream.remote_name,
|
|
433
|
+
remote_url: upstream.remote_url,
|
|
434
|
+
remote_identity: upstream.repository_identity
|
|
435
|
+
});
|
|
436
|
+
if (!moved)
|
|
437
|
+
return null;
|
|
438
|
+
if (this.topology && repository_root === this.git.repository_root) {
|
|
439
|
+
this.topology = {
|
|
440
|
+
...this.topology,
|
|
441
|
+
source_remote_identity: moved.remote_identity
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
return { from: binding.remote_identity, to: moved.remote_identity };
|
|
445
|
+
}
|
|
446
|
+
async attachCurrentSourceWorkspace(input) {
|
|
447
|
+
await assertLocalWorkspacePathsIgnored(this.git.repository_root);
|
|
448
|
+
if (this.store.getWorkspaceByGitWorktreePath(this.git.repository_root)) {
|
|
449
|
+
throw new Error('The current Git worktree is already registered.');
|
|
450
|
+
}
|
|
451
|
+
const projection_dir = join(this.git.repository_root, 'sdlc');
|
|
452
|
+
try {
|
|
453
|
+
await stat(projection_dir);
|
|
454
|
+
throw new Error('Refusing to attach over an existing local graph projection.');
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
if (!(error instanceof Error) ||
|
|
458
|
+
!('code' in error) ||
|
|
459
|
+
error.code !== 'ENOENT') {
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const existing_binding = this.store.getSourceRepositoryBinding(this.git.git_common_dir);
|
|
464
|
+
await this.bindCurrentSourceRepository({
|
|
465
|
+
source_repository_association_id: input.source_repository_association_id,
|
|
466
|
+
remote_name: input.source_remote_name
|
|
467
|
+
});
|
|
468
|
+
const workspace = this.store.forkWorkspace({
|
|
469
|
+
actor_workspace_id: input.source_workspace_id,
|
|
470
|
+
expected_source_sdlc_head_commit_id: input.expected_source_sdlc_head_commit_id,
|
|
471
|
+
git_worktree_path: this.git.repository_root,
|
|
472
|
+
git_dir: this.git.git_dir,
|
|
473
|
+
git_common_dir: this.git.git_common_dir,
|
|
474
|
+
git_head_commit_id: this.git.head,
|
|
475
|
+
actor_kind: input.actor_kind ?? 'human',
|
|
476
|
+
task_scope: input.task_scope
|
|
477
|
+
});
|
|
478
|
+
try {
|
|
479
|
+
const graph = await this.readCommitGraph(workspace.head_sdlc_commit_id);
|
|
480
|
+
await this.materialize_projection({
|
|
481
|
+
projection_dir,
|
|
482
|
+
graph,
|
|
483
|
+
...(input.full_projection
|
|
484
|
+
? {}
|
|
485
|
+
: { scope: projectionScopeOfTaskScope(input.task_scope) })
|
|
486
|
+
});
|
|
487
|
+
await mkdir(controlDirectory(workspace), {
|
|
488
|
+
recursive: true,
|
|
489
|
+
mode: 0o700
|
|
490
|
+
});
|
|
491
|
+
const status = await this.getWorkspaceStatus(workspace.id);
|
|
492
|
+
await this.persistCurrentTopology(workspace, input.storage_mode);
|
|
493
|
+
return status;
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
this.store.closeWorkspace({
|
|
497
|
+
actor_workspace_id: input.source_workspace_id,
|
|
498
|
+
workspace_id: workspace.id,
|
|
499
|
+
expected_head_commit_id: workspace.head_sdlc_commit_id
|
|
500
|
+
});
|
|
501
|
+
this.store.removeClosedWorkspaceRegistration({
|
|
502
|
+
workspace_id: workspace.id,
|
|
503
|
+
source_workspace_id: input.source_workspace_id,
|
|
504
|
+
git_worktree_path: this.git.repository_root
|
|
505
|
+
});
|
|
506
|
+
if (!existing_binding) {
|
|
507
|
+
this.store.unbindSourceRepository({
|
|
508
|
+
git_common_dir: this.git.git_common_dir,
|
|
509
|
+
expected_source_repository_association_id: input.source_repository_association_id
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
await rm(projection_dir, { recursive: true, force: true });
|
|
513
|
+
await rm(controlDirectory(workspace), { recursive: true, force: true });
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Attaches every other repository of the folder whose home this workspace's
|
|
519
|
+
* checkout belongs to (run in its plan coordinator), each from this
|
|
520
|
+
* workspace's head with its whole-repository scope (repositoryTaskScope).
|
|
521
|
+
* A member that cannot attach now (already attached, no node cites it yet,
|
|
522
|
+
* or attach refuses) is skipped with its reason, so a rerun picks it up.
|
|
523
|
+
*/
|
|
524
|
+
async attachFolderMembers(input = {}) {
|
|
525
|
+
const source = this.currentActiveWorkspace();
|
|
526
|
+
if (source.source_workspace_id !== null) {
|
|
527
|
+
// Every worktree of the home reads the folder; members attach to the coordinator, never a task
|
|
528
|
+
throw new Error(`Run attach --all-members in the plan coordinator: workspace ${source.id} is a child task workspace.`);
|
|
529
|
+
}
|
|
530
|
+
const folder = await readFolderMembers(this.git.git_common_dir);
|
|
531
|
+
const graph = await this.readCommitGraph(source.head_sdlc_commit_id);
|
|
532
|
+
const result = {
|
|
533
|
+
version: 1,
|
|
534
|
+
folder: folder.folder,
|
|
535
|
+
source_workspace_id: source.id,
|
|
536
|
+
head_revision: source.head_sdlc_commit_id,
|
|
537
|
+
attached: [],
|
|
538
|
+
skipped: [...folder.skipped],
|
|
539
|
+
behind: [],
|
|
540
|
+
rebound: []
|
|
541
|
+
};
|
|
542
|
+
for (const member of folder.members) {
|
|
543
|
+
const skip = (reason) => result.skipped.push({ ...member, reason });
|
|
544
|
+
let runtime;
|
|
545
|
+
try {
|
|
546
|
+
runtime = await LocalWorkspaceRuntime.open({
|
|
547
|
+
cwd: member.path,
|
|
548
|
+
state_dir: this.state_dir,
|
|
549
|
+
graph_repository_id: this.store.repository.repository_id
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
skip(error.message);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
const attached = runtime.store.getWorkspaceByGitWorktreePath(runtime.git.repository_root);
|
|
558
|
+
if (attached?.status === 'active') {
|
|
559
|
+
let reason = `already attached as workspace ${attached.id}`;
|
|
560
|
+
try {
|
|
561
|
+
const realigned = await runtime.realignForkBinding();
|
|
562
|
+
if (realigned)
|
|
563
|
+
result.rebound.push({ ...member, ...realigned });
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
reason += `; its binding was not moved to upstream: ${error.message}`;
|
|
567
|
+
}
|
|
568
|
+
skip(reason);
|
|
569
|
+
// Attached from an older head: name the reconcile that catches it up
|
|
570
|
+
if (attached.source_workspace_id === source.id &&
|
|
571
|
+
attached.head_sdlc_commit_id !== source.head_sdlc_commit_id &&
|
|
572
|
+
!runtime.store.history.isAncestor(source.head_sdlc_commit_id, attached.head_sdlc_commit_id)) {
|
|
573
|
+
result.behind.push({
|
|
574
|
+
...member,
|
|
575
|
+
workspace_id: attached.id,
|
|
576
|
+
head_revision: attached.head_sdlc_commit_id,
|
|
577
|
+
catch_up: `sdlc integrate reconcile --source ${source.id}`
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
const names = await repositoryNames(runtime.git.repository_root);
|
|
583
|
+
if (names.length === 0) {
|
|
584
|
+
skip('no remote names its repository, so no source reference can cite it');
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
const task_scope = repositoryTaskScope(graph, names);
|
|
588
|
+
if (!task_scope) {
|
|
589
|
+
skip(`no node cites ${names.join(' or ')} yet; run attach --all-members again once one does`);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const status = await runtime.attachCurrentSourceWorkspace({
|
|
593
|
+
source_workspace_id: source.id,
|
|
594
|
+
expected_source_sdlc_head_commit_id: source.head_sdlc_commit_id,
|
|
595
|
+
source_repository_association_id: member.name,
|
|
596
|
+
actor_kind: input.actor_kind,
|
|
597
|
+
storage_mode: input.storage_mode,
|
|
598
|
+
task_scope,
|
|
599
|
+
full_projection: input.full_projection
|
|
600
|
+
});
|
|
601
|
+
result.attached.push({
|
|
602
|
+
...member,
|
|
603
|
+
workspace_id: status.workspace.id,
|
|
604
|
+
projection_dir: status.projection_dir,
|
|
605
|
+
nodes: task_scope.rules.filter((rule) => rule.entity_kind === 'node')
|
|
606
|
+
.length,
|
|
607
|
+
edges: task_scope.rules.filter((rule) => rule.entity_kind === 'edge')
|
|
608
|
+
.length
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
skip(error.message);
|
|
613
|
+
}
|
|
614
|
+
finally {
|
|
615
|
+
runtime.close();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
async getMultiRepositoryContext() {
|
|
621
|
+
const workspace = this.currentActiveWorkspace();
|
|
622
|
+
if (!workspace) {
|
|
623
|
+
throw new Error('The current Git worktree is not an active local workspace.');
|
|
624
|
+
}
|
|
625
|
+
const { git } = await this.authority.requireCurrent(workspace.id);
|
|
626
|
+
return {
|
|
627
|
+
version: 1,
|
|
628
|
+
graph_repository: {
|
|
629
|
+
repository_id: this.store.repository.repository_id,
|
|
630
|
+
state_dir: this.state_dir
|
|
631
|
+
},
|
|
632
|
+
source_repository: {
|
|
633
|
+
binding: this.store.getSourceRepositoryBinding(git.git_common_dir),
|
|
634
|
+
git_ref: git.ref,
|
|
635
|
+
git_head: git.head,
|
|
636
|
+
git_worktree_path: git.repository_root
|
|
637
|
+
},
|
|
638
|
+
graph_workspace: {
|
|
639
|
+
workspace_id: workspace.id,
|
|
640
|
+
source_workspace_id: workspace.source_workspace_id,
|
|
641
|
+
sdlc_branch: workspace.attached_ref,
|
|
642
|
+
fork_revision: workspace.fork_sdlc_commit_id,
|
|
643
|
+
head_revision: workspace.head_sdlc_commit_id,
|
|
644
|
+
projection_dir: projectionDirectory(workspace)
|
|
645
|
+
},
|
|
646
|
+
topology: {
|
|
647
|
+
topology_path: join(this.git.repository_root, '.sdlc', 'topology-v1.json'),
|
|
648
|
+
storage_mode: this.topology?.storage_mode ??
|
|
649
|
+
(this.state_dir ===
|
|
650
|
+
resolveLocalRepositoryStateDirectory({
|
|
651
|
+
git_common_dir: this.git.git_common_dir
|
|
652
|
+
})
|
|
653
|
+
? 'self_contained'
|
|
654
|
+
: 'machine_shared')
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
async detachCurrentSourceWorkspace() {
|
|
659
|
+
const workspace = this.currentActiveWorkspace();
|
|
660
|
+
if (!workspace || !this.topology) {
|
|
661
|
+
throw new Error('The current checkout is not attached.');
|
|
662
|
+
}
|
|
663
|
+
if (workspace.actor_kind !== 'human') {
|
|
664
|
+
throw new Error('Only a human-owned workspace may detach itself.');
|
|
665
|
+
}
|
|
666
|
+
await this.authority.requireCurrent(workspace.id);
|
|
667
|
+
const topology = this.topology;
|
|
668
|
+
// Under the operation lock: detach may rewrite the projection, so no
|
|
669
|
+
// commit or edit command may run between its clean check and the close.
|
|
670
|
+
const { closed, topology_path } = await withWorkspaceOperationLock({
|
|
671
|
+
control_dir: controlDirectory(workspace),
|
|
672
|
+
operation: 'detach',
|
|
673
|
+
run: async () => {
|
|
674
|
+
const current = this.requireWorkspace(workspace.id);
|
|
675
|
+
await this.assertProjectionClean(current);
|
|
676
|
+
// The preserved projection must read without its workspace: a scoped
|
|
677
|
+
// one becomes the whole head graph, which it equals once clean.
|
|
678
|
+
if (await isScopedProjection(projectionDirectory(current))) {
|
|
679
|
+
await this.materialize_projection({
|
|
680
|
+
projection_dir: projectionDirectory(current),
|
|
681
|
+
graph: await this.readCommitGraph(current.head_sdlc_commit_id),
|
|
682
|
+
full: true
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
const topology_path = await removeWorkspaceTopology({
|
|
686
|
+
repository_root: this.git.repository_root,
|
|
687
|
+
graph_repository_id: topology.graph_repository_id,
|
|
688
|
+
workspace_id: topology.workspace_id
|
|
689
|
+
});
|
|
690
|
+
try {
|
|
691
|
+
return {
|
|
692
|
+
topology_path,
|
|
693
|
+
closed: this.store.closeWorkspace({
|
|
694
|
+
actor_workspace_id: current.id,
|
|
695
|
+
workspace_id: current.id,
|
|
696
|
+
expected_head_commit_id: current.head_sdlc_commit_id
|
|
697
|
+
})
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
await writeWorkspaceTopology({
|
|
702
|
+
repository_root: this.git.repository_root,
|
|
703
|
+
topology
|
|
704
|
+
});
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
this.topology = null;
|
|
710
|
+
const binding = this.store.getSourceRepositoryBinding(this.git.git_common_dir);
|
|
711
|
+
const unbound = binding
|
|
712
|
+
? this.store.unbindSourceRepository({
|
|
713
|
+
git_common_dir: this.git.git_common_dir,
|
|
714
|
+
expected_source_repository_association_id: binding.source_repository_association_id
|
|
715
|
+
})
|
|
716
|
+
: null;
|
|
717
|
+
return {
|
|
718
|
+
version: 1,
|
|
719
|
+
status: 'detached',
|
|
720
|
+
workspace: closed,
|
|
721
|
+
topology_path,
|
|
722
|
+
projection_dir: projectionDirectory(workspace),
|
|
723
|
+
projection_preserved: true,
|
|
724
|
+
source_binding_removed: unbound?.removed ?? false
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
async createChildWorkspace(input) {
|
|
728
|
+
const { workspace: source, git } = await this.authority.requireCurrent(input.actor_workspace_id);
|
|
729
|
+
return withWorkspaceOperationLock({
|
|
730
|
+
control_dir: controlDirectory(source),
|
|
731
|
+
operation: 'create-child',
|
|
732
|
+
run: async () => {
|
|
733
|
+
const current_source = this.requireWorkspace(source.id);
|
|
734
|
+
await this.assertProjectionClean(current_source);
|
|
735
|
+
const target_path = resolve(input.target_path);
|
|
736
|
+
let created_git = null;
|
|
737
|
+
let created_workspace = null;
|
|
738
|
+
try {
|
|
739
|
+
created_git = await createDetachedGitWorktree({
|
|
740
|
+
source_cwd: current_source.git_worktree_path,
|
|
741
|
+
target_path,
|
|
742
|
+
commit_id: git.head
|
|
743
|
+
});
|
|
744
|
+
await assertLocalWorkspacePathsIgnored(created_git.repository_root);
|
|
745
|
+
const child = this.store.forkWorkspace({
|
|
746
|
+
actor_workspace_id: current_source.id,
|
|
747
|
+
expected_source_sdlc_head_commit_id: current_source.head_sdlc_commit_id,
|
|
748
|
+
git_worktree_path: created_git.repository_root,
|
|
749
|
+
git_dir: created_git.git_dir,
|
|
750
|
+
git_common_dir: created_git.git_common_dir,
|
|
751
|
+
git_head_commit_id: created_git.head,
|
|
752
|
+
actor_kind: input.actor_kind ?? 'ai',
|
|
753
|
+
task_scope: input.task_scope
|
|
754
|
+
});
|
|
755
|
+
created_workspace = child;
|
|
756
|
+
const graph = await this.readCommitGraph(child.head_sdlc_commit_id);
|
|
757
|
+
await this.materialize_projection({
|
|
758
|
+
projection_dir: projectionDirectory(child),
|
|
759
|
+
graph
|
|
760
|
+
});
|
|
761
|
+
await mkdir(controlDirectory(child), {
|
|
762
|
+
recursive: true,
|
|
763
|
+
mode: 0o700
|
|
764
|
+
});
|
|
765
|
+
await this.persistCurrentTopology(child, this.topology?.storage_mode, created_git.repository_root, created_git.git_common_dir);
|
|
766
|
+
return this.getWorkspaceStatus(child.id);
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
if (created_workspace) {
|
|
770
|
+
try {
|
|
771
|
+
this.store.closeWorkspace({
|
|
772
|
+
actor_workspace_id: current_source.id,
|
|
773
|
+
workspace_id: created_workspace.id,
|
|
774
|
+
expected_head_commit_id: created_workspace.head_sdlc_commit_id
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
// Preserve the creation failure; the record remains inspectable.
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (created_git) {
|
|
782
|
+
await removeGitWorktree({
|
|
783
|
+
repository_cwd: this.git.repository_root,
|
|
784
|
+
target_path: created_git.repository_root,
|
|
785
|
+
force: true
|
|
786
|
+
}).catch(() => undefined);
|
|
787
|
+
}
|
|
788
|
+
throw error;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
listWorkspaces() {
|
|
794
|
+
return this.store.listWorkspaces();
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Replaces a child workspace's declared task scope, for example when a plan
|
|
798
|
+
* grows. The new scope must still cover the workspace's own changes, which
|
|
799
|
+
* are measured from its merge base with its source.
|
|
800
|
+
*/
|
|
801
|
+
async replaceTaskScope(input) {
|
|
802
|
+
const { workspace: actor } = await this.authority.requireCurrent(input.actor_workspace_id);
|
|
803
|
+
const workspace = this.requireWorkspace(input.workspace_id);
|
|
804
|
+
const task_scope = local_workspace_task_scope_schema.parse(input.task_scope);
|
|
805
|
+
return withWorkspaceOperationLock({
|
|
806
|
+
control_dir: controlDirectory(workspace),
|
|
807
|
+
operation: 'scope',
|
|
808
|
+
run: async () => {
|
|
809
|
+
const current = this.requireWorkspace(workspace.id);
|
|
810
|
+
const source = current.source_workspace_id
|
|
811
|
+
? this.requireWorkspace(current.source_workspace_id)
|
|
812
|
+
: null;
|
|
813
|
+
let base_commit_id = current.fork_sdlc_commit_id;
|
|
814
|
+
if (source) {
|
|
815
|
+
try {
|
|
816
|
+
base_commit_id = this.store.history.findMergeBase(current.head_sdlc_commit_id, source.head_sdlc_commit_id);
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
// Keep the fork commit when the heads share no local ancestor.
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
assertGraphChangesWithinTaskScope({
|
|
823
|
+
base_graph: await this.readCommitGraph(base_commit_id),
|
|
824
|
+
current_graph: await this.readCommitGraph(current.head_sdlc_commit_id),
|
|
825
|
+
task_scope
|
|
826
|
+
});
|
|
827
|
+
const replaced = this.store.replaceWorkspaceTaskScope({
|
|
828
|
+
actor_workspace_id: actor.id,
|
|
829
|
+
workspace_id: current.id,
|
|
830
|
+
expected_head_commit_id: current.head_sdlc_commit_id,
|
|
831
|
+
task_scope
|
|
832
|
+
});
|
|
833
|
+
// A scoped projection gains files for the entities the scope adds.
|
|
834
|
+
const projection_dir = projectionDirectory(current);
|
|
835
|
+
if (await isScopedProjection(projection_dir)) {
|
|
836
|
+
await extendScopedProjection({
|
|
837
|
+
projection_dir,
|
|
838
|
+
graph: await this.readCommitGraph(current.head_sdlc_commit_id),
|
|
839
|
+
scope: projectionScopeOfTaskScope(task_scope)
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return { version: 1, workspace_id: current.id, ...replaced };
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Removes one entity file from the current workspace's projection and, when
|
|
848
|
+
* the head has that entity, records the deletion for the next commit, like
|
|
849
|
+
* hosted `sdlc rm`. The commit still enforces the task scope.
|
|
850
|
+
*/
|
|
851
|
+
async removeEntity(input) {
|
|
852
|
+
const { workspace } = await this.authority.requireCurrent(input.actor_workspace_id);
|
|
853
|
+
return withWorkspaceOperationLock({
|
|
854
|
+
control_dir: controlDirectory(workspace),
|
|
855
|
+
operation: 'remove',
|
|
856
|
+
run: async () => {
|
|
857
|
+
const current = this.requireWorkspace(workspace.id);
|
|
858
|
+
const head_graph = await this.readCommitGraph(current.head_sdlc_commit_id);
|
|
859
|
+
const removed = await removeProjectionEntity({
|
|
860
|
+
projection_dir: projectionDirectory(current),
|
|
861
|
+
relative_path: input.relative_path,
|
|
862
|
+
allow_missing: (entry) => graphHasProjectionEntity(head_graph, entry)
|
|
863
|
+
});
|
|
864
|
+
const deletion_recorded = graphHasProjectionEntity(head_graph, removed);
|
|
865
|
+
if (deletion_recorded) {
|
|
866
|
+
this.store.recordWorkspaceDeletion({
|
|
867
|
+
workspace_id: current.id,
|
|
868
|
+
entity_kind: removed.entity_kind,
|
|
869
|
+
entity_id: removed.entity_id
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
version: 1,
|
|
874
|
+
workspace_id: current.id,
|
|
875
|
+
relative_path: removed.relative_path,
|
|
876
|
+
deletion_recorded
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Runs a `sdlc workspace doc` command against the current workspace's
|
|
883
|
+
* projection, under the same authority check and operation lock as `rm`
|
|
884
|
+
* and `commit`, so it never edits a projection another operation is using.
|
|
885
|
+
*/
|
|
886
|
+
async withDocumentProjection(input) {
|
|
887
|
+
const { workspace } = await this.authority.requireCurrent(input.actor_workspace_id);
|
|
888
|
+
return withWorkspaceOperationLock({
|
|
889
|
+
control_dir: controlDirectory(workspace),
|
|
890
|
+
operation: `doc:${input.operation}`,
|
|
891
|
+
run: async () => {
|
|
892
|
+
const projection_dir = projectionDirectory(this.requireWorkspace(workspace.id));
|
|
893
|
+
// These commands read and write entity files directly, so they need
|
|
894
|
+
// every entity on disk.
|
|
895
|
+
if (await isScopedProjection(projection_dir)) {
|
|
896
|
+
throw new Error('This projection holds only its task scope (workspace attach); run doc, guide, journey and test commands in a workspace with the whole graph, such as a plan workspace from sdlc workspace create, or attach with --full-projection.');
|
|
897
|
+
}
|
|
898
|
+
return input.run(projection_dir);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Writes a workspace commit's graph as a canonical portable graph file, the
|
|
904
|
+
* format `sdlc save --graph-file` reads. Defaults to the workspace head.
|
|
905
|
+
*/
|
|
906
|
+
async exportWorkspaceGraph(input) {
|
|
907
|
+
const workspace = this.requireWorkspace(input.workspace_id);
|
|
908
|
+
const view = input.view ?? 'planned';
|
|
909
|
+
const commit_id = input.commit_id ?? workspace.head_sdlc_commit_id;
|
|
910
|
+
if (commit_id !== workspace.head_sdlc_commit_id &&
|
|
911
|
+
!this.store.history.isAncestor(commit_id, workspace.head_sdlc_commit_id)) {
|
|
912
|
+
throw new Error('Export commit is not in the workspace history.');
|
|
913
|
+
}
|
|
914
|
+
if (view === 'built' && commit_id !== workspace.head_sdlc_commit_id) {
|
|
915
|
+
throw new Error('The built view reads the workspace head only.');
|
|
916
|
+
}
|
|
917
|
+
const built = view === 'built'
|
|
918
|
+
? await this.integration_runtime.readBuiltView({
|
|
919
|
+
coordinator_workspace_id: workspace.id
|
|
920
|
+
})
|
|
921
|
+
: null;
|
|
922
|
+
const graph = built?.graph ?? (await this.readCommitGraph(commit_id));
|
|
923
|
+
const output_path = resolve(input.output_path);
|
|
924
|
+
const temporary_path = `${output_path}.tmp-${randomUUID()}`;
|
|
925
|
+
try {
|
|
926
|
+
await writeFile(temporary_path, `${stringifyCanonicalGraph(canonicalizePortableGraph(graph))}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
927
|
+
await rename(temporary_path, output_path);
|
|
928
|
+
}
|
|
929
|
+
catch (error) {
|
|
930
|
+
await rm(temporary_path, { force: true });
|
|
931
|
+
throw error;
|
|
932
|
+
}
|
|
933
|
+
return {
|
|
934
|
+
version: 1,
|
|
935
|
+
view,
|
|
936
|
+
workspace_id: workspace.id,
|
|
937
|
+
commit_id,
|
|
938
|
+
root_hash: built
|
|
939
|
+
? localGraphRootHash(canonicalizePortableGraph(built.graph))
|
|
940
|
+
: this.commitRoot(commit_id),
|
|
941
|
+
output_path,
|
|
942
|
+
...(built
|
|
943
|
+
? {
|
|
944
|
+
marked_for_removal: built.marked_for_removal,
|
|
945
|
+
left_out_orphans: built.left_out_orphans,
|
|
946
|
+
validation: built.validation
|
|
947
|
+
}
|
|
948
|
+
: {})
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
/** The workspace's pending claims: merged under a gate and not landed. */
|
|
952
|
+
listPendingClaims(input) {
|
|
953
|
+
const workspace = this.requireWorkspace(input.workspace_id);
|
|
954
|
+
return listClaims({
|
|
955
|
+
workspace_id: workspace.id,
|
|
956
|
+
pending: this.store.built_content.listPendingClaims(workspace.id)
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Each node's claim status in a view (`sdlc workspace status`), read from a
|
|
961
|
+
* coordinator's committed head and its pending claims. A plan workspace (one
|
|
962
|
+
* with no active plans under it) reads its coordinator, whose head is what
|
|
963
|
+
* the plans merged into. Source
|
|
964
|
+
* references are checked against `commit` in this checkout's repository.
|
|
965
|
+
* Read-only.
|
|
966
|
+
*/
|
|
967
|
+
async workspaceClaimStatus(input) {
|
|
968
|
+
const asked = this.requireWorkspace(input.workspace_id);
|
|
969
|
+
const workspace = this.claimReadWorkspace(asked);
|
|
970
|
+
const result = await claimStatus({
|
|
971
|
+
graph: await this.readCommitGraph(workspace.head_sdlc_commit_id),
|
|
972
|
+
pending: this.store.built_content.listPendingClaims(workspace.id),
|
|
973
|
+
workspace_id: workspace.id,
|
|
974
|
+
graph_commit_id: workspace.head_sdlc_commit_id,
|
|
975
|
+
git: gitClaimStatusReader(this.git.repository_root),
|
|
976
|
+
// Checked against main by default, as claim_status.py is: a plan's or the
|
|
977
|
+
// coordinator's HEAD is the planning branch, not the code.
|
|
978
|
+
commit: input.commit ?? 'main',
|
|
979
|
+
environments: input.environments ?? [],
|
|
980
|
+
cites_code: input.cites_code ?? [],
|
|
981
|
+
view: input.view,
|
|
982
|
+
nodes: input.nodes,
|
|
983
|
+
status: input.status,
|
|
984
|
+
stale_only: input.stale_only,
|
|
985
|
+
behind_only: input.behind_only
|
|
986
|
+
});
|
|
987
|
+
return workspace.id === asked.id
|
|
988
|
+
? result
|
|
989
|
+
: { ...result, asked_workspace_id: asked.id };
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* The workspace a claim read answers from: a coordinator has active plans
|
|
993
|
+
* under it and reads itself; a plan reads the coordinator it merges into.
|
|
994
|
+
*/
|
|
995
|
+
claimReadWorkspace(asked) {
|
|
996
|
+
const coordinates = this.listWorkspaces().some((other) => other.source_workspace_id === asked.id && other.status === 'active');
|
|
997
|
+
return asked.source_workspace_id && !coordinates
|
|
998
|
+
? this.requireWorkspace(asked.source_workspace_id)
|
|
999
|
+
: asked;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* `sdlc variants`: each Variant Set in the committed graph with its gap and
|
|
1003
|
+
* its sync entities' claim status (docs/specs/graph-variants.md, P3a), and
|
|
1004
|
+
* with an environment, its members' forms by claim status. It reads the
|
|
1005
|
+
* graph claim status reads, so a plan reads its coordinator. Read-only.
|
|
1006
|
+
*/
|
|
1007
|
+
async workspaceVariants(input) {
|
|
1008
|
+
const asked = this.requireWorkspace(input.workspace_id);
|
|
1009
|
+
const workspace = this.claimReadWorkspace(asked);
|
|
1010
|
+
const graph = await this.readCommitGraph(workspace.head_sdlc_commit_id);
|
|
1011
|
+
const environments = input.environments ?? [];
|
|
1012
|
+
const read = (view, nodes) => claimStatus({
|
|
1013
|
+
graph,
|
|
1014
|
+
pending: this.store.built_content.listPendingClaims(workspace.id),
|
|
1015
|
+
workspace_id: workspace.id,
|
|
1016
|
+
graph_commit_id: workspace.head_sdlc_commit_id,
|
|
1017
|
+
git: gitClaimStatusReader(this.git.repository_root),
|
|
1018
|
+
commit: input.commit ?? 'main',
|
|
1019
|
+
environments,
|
|
1020
|
+
view,
|
|
1021
|
+
cites_code: [],
|
|
1022
|
+
nodes
|
|
1023
|
+
});
|
|
1024
|
+
const claims = new Map();
|
|
1025
|
+
let form_claims;
|
|
1026
|
+
let claim_error;
|
|
1027
|
+
try {
|
|
1028
|
+
const sync_ids = variantSyncIds(graph);
|
|
1029
|
+
if (sync_ids.length) {
|
|
1030
|
+
for (const claim of (await read('planned', sync_ids)).claims ?? []) {
|
|
1031
|
+
claims.set(claim.node, {
|
|
1032
|
+
status: claim.status,
|
|
1033
|
+
environment: claim.environment
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
// Only an environment can read a form as released, so members are read
|
|
1038
|
+
// only with one, and as built: a plan's pending gate claim on a form, or
|
|
1039
|
+
// on a node realizing it, must not hide a release.
|
|
1040
|
+
const form_ids = environments.length ? variantMemberFormIds(graph) : [];
|
|
1041
|
+
if (form_ids.length) {
|
|
1042
|
+
const built = await read('built', form_ids);
|
|
1043
|
+
const unresolved = built.environments.filter((environment) => !environment.resolved);
|
|
1044
|
+
if (unresolved.length) {
|
|
1045
|
+
// Nothing reads released against a ref that does not resolve, which
|
|
1046
|
+
// would make every live member look contradicted.
|
|
1047
|
+
claim_error = `environment ${unresolved
|
|
1048
|
+
.map((environment) => `${environment.key}=${environment.ref}`)
|
|
1049
|
+
.join(', ')} does not resolve, so members are not checked`;
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
form_claims = new Map((built.claims ?? []).map((claim) => [
|
|
1053
|
+
claim.node,
|
|
1054
|
+
{ status: claim.status, environment: claim.environment }
|
|
1055
|
+
]));
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
catch (error) {
|
|
1060
|
+
// The gap stands without claim status, for example where main is no ref.
|
|
1061
|
+
claim_error = error instanceof Error ? error.message : String(error);
|
|
1062
|
+
form_claims = undefined;
|
|
1063
|
+
}
|
|
1064
|
+
return {
|
|
1065
|
+
...variantsReport({ graph, claims, form_claims }),
|
|
1066
|
+
...(claim_error ? { claim_error } : {}),
|
|
1067
|
+
workspace_id: workspace.id,
|
|
1068
|
+
...(workspace.id === asked.id ? {} : { asked_workspace_id: asked.id })
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Claim freshness of the nodes and edges with evidence in the current
|
|
1073
|
+
* workspace's projection (`sdlc workspace evidence status`): what sign
|
|
1074
|
+
* writes, uncommitted edits included. Read-only.
|
|
1075
|
+
*/
|
|
1076
|
+
async workspaceEvidenceStatus(input) {
|
|
1077
|
+
const { workspace } = await this.authority.requireCurrent(input.actor_workspace_id);
|
|
1078
|
+
const current = this.requireWorkspace(workspace.id);
|
|
1079
|
+
const { graph } = await composeGraphProjection(projectionDirectory(current), undefined, { base: () => this.readCommitGraph(current.head_sdlc_commit_id) });
|
|
1080
|
+
return evidenceStatus({
|
|
1081
|
+
graph,
|
|
1082
|
+
workspace_id: workspace.id,
|
|
1083
|
+
nodes: input.nodes,
|
|
1084
|
+
edges: input.edges,
|
|
1085
|
+
behind_only: input.behind_only
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* `sdlc workspace evidence sign`: pins the selected entities' references to
|
|
1090
|
+
* their current object hash in the projection, after verifying each code
|
|
1091
|
+
* reference it signs at `commit` (default HEAD). Writes nothing when any
|
|
1092
|
+
* does not hold. `changed` selects every node and edge the plan changed
|
|
1093
|
+
* since its merge base with the coordinator. A signed entity outside a
|
|
1094
|
+
* plan's task scope widens the scope to its meta. Commits nothing.
|
|
1095
|
+
* `repository` is the Git repository whose code the references are
|
|
1096
|
+
* verified in (default this workspace's own), so a graph that cites
|
|
1097
|
+
* another repository signs that repository's references against its code.
|
|
1098
|
+
*/
|
|
1099
|
+
async signEvidence(input) {
|
|
1100
|
+
const { workspace } = await this.authority.requireCurrent(input.actor_workspace_id);
|
|
1101
|
+
return withWorkspaceOperationLock({
|
|
1102
|
+
control_dir: controlDirectory(workspace),
|
|
1103
|
+
operation: 'evidence-sign',
|
|
1104
|
+
run: () => this.signEvidenceLocked(workspace.id, input)
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
/** signEvidence's work, for a caller already holding the workspace operation lock. */
|
|
1108
|
+
async signEvidenceLocked(workspace_id, input) {
|
|
1109
|
+
const current = this.requireWorkspace(workspace_id);
|
|
1110
|
+
const projection_dir = projectionDirectory(current);
|
|
1111
|
+
const { graph } = await composeGraphProjection(projection_dir, undefined, {
|
|
1112
|
+
base: () => this.readCommitGraph(current.head_sdlc_commit_id)
|
|
1113
|
+
});
|
|
1114
|
+
const repository = input.repository
|
|
1115
|
+
? resolve(input.repository)
|
|
1116
|
+
: this.git.repository_root;
|
|
1117
|
+
const git = gitClaimStatusReader(repository);
|
|
1118
|
+
const commit = await git.resolve(input.commit ?? 'HEAD');
|
|
1119
|
+
if (commit === null) {
|
|
1120
|
+
throw new Error(input.repository
|
|
1121
|
+
? `${input.commit ?? 'HEAD'} is not a commit in ${repository}.`
|
|
1122
|
+
: `${input.commit ?? 'HEAD'} is not a commit in this repository.`);
|
|
1123
|
+
}
|
|
1124
|
+
const repository_names = await repositoryNames(repository, commit);
|
|
1125
|
+
const nodes = [...(input.nodes ?? [])];
|
|
1126
|
+
const edges = [...(input.edges ?? [])];
|
|
1127
|
+
if (input.changed) {
|
|
1128
|
+
if (!current.source_workspace_id) {
|
|
1129
|
+
throw new Error('sign --changed reads what a plan changed, and this workspace has no coordinator.');
|
|
1130
|
+
}
|
|
1131
|
+
const source = this.requireWorkspace(current.source_workspace_id);
|
|
1132
|
+
let base_commit_id = current.fork_sdlc_commit_id;
|
|
1133
|
+
try {
|
|
1134
|
+
base_commit_id = this.store.history.findMergeBase(current.head_sdlc_commit_id, source.head_sdlc_commit_id);
|
|
1135
|
+
}
|
|
1136
|
+
catch {
|
|
1137
|
+
// Keep the fork commit when the heads share no local ancestor.
|
|
1138
|
+
}
|
|
1139
|
+
const present = {
|
|
1140
|
+
node: new Set(graph.nodes.map((node) => node.id)),
|
|
1141
|
+
edge: new Set(graph.edges.map((edge) => edge.id))
|
|
1142
|
+
};
|
|
1143
|
+
for (const change of graphChangeCoordinates({
|
|
1144
|
+
base_graph: await this.readCommitGraph(base_commit_id),
|
|
1145
|
+
current_graph: graph
|
|
1146
|
+
})) {
|
|
1147
|
+
if (change.entity_kind === 'node' && present.node.has(change.entity_id))
|
|
1148
|
+
nodes.push(change.entity_id);
|
|
1149
|
+
if (change.entity_kind === 'edge' && present.edge.has(change.entity_id))
|
|
1150
|
+
edges.push(change.entity_id);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
const plan = await planEvidenceSign({
|
|
1154
|
+
graph,
|
|
1155
|
+
git,
|
|
1156
|
+
commit,
|
|
1157
|
+
now: (input.now ?? new Date()).toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
|
1158
|
+
nodes: [...new Set(nodes)],
|
|
1159
|
+
edges: [...new Set(edges)],
|
|
1160
|
+
references: input.references,
|
|
1161
|
+
repository_names
|
|
1162
|
+
});
|
|
1163
|
+
const result = {
|
|
1164
|
+
version: 1,
|
|
1165
|
+
workspace_id: current.id,
|
|
1166
|
+
...plan
|
|
1167
|
+
};
|
|
1168
|
+
if (plan.status !== 'signed')
|
|
1169
|
+
return result;
|
|
1170
|
+
// A scoped projection first gains the file of each entity it signs.
|
|
1171
|
+
if (await isScopedProjection(projection_dir)) {
|
|
1172
|
+
await extendScopedProjection({
|
|
1173
|
+
projection_dir,
|
|
1174
|
+
graph,
|
|
1175
|
+
scope: projectionScopeOfTaskScope({
|
|
1176
|
+
rules: plan.changes.map((change) => ({
|
|
1177
|
+
entity_kind: change.kind,
|
|
1178
|
+
entity_id: change.id
|
|
1179
|
+
}))
|
|
1180
|
+
})
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
// Widen the scope next: a failure while writing then leaves signed
|
|
1184
|
+
// entities the scope already covers, never ones it refuses at commit.
|
|
1185
|
+
const scope = this.store.getWorkspaceTaskScope(current.id);
|
|
1186
|
+
const META = '/data/meta';
|
|
1187
|
+
const widened = scope
|
|
1188
|
+
? plan.changes
|
|
1189
|
+
.map((change) => ({
|
|
1190
|
+
entity_kind: change.kind,
|
|
1191
|
+
entity_id: change.id,
|
|
1192
|
+
field_path: META
|
|
1193
|
+
}))
|
|
1194
|
+
.filter((candidate) => !scope.rules.some((rule) => ruleCovers(rule, candidate)))
|
|
1195
|
+
: [];
|
|
1196
|
+
if (scope && widened.length) {
|
|
1197
|
+
this.store.replaceWorkspaceTaskScope({
|
|
1198
|
+
actor_workspace_id: current.id,
|
|
1199
|
+
workspace_id: current.id,
|
|
1200
|
+
expected_head_commit_id: current.head_sdlc_commit_id,
|
|
1201
|
+
task_scope: { ...scope, rules: [...scope.rules, ...widened] }
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
for (const change of plan.changes) {
|
|
1205
|
+
const path = join(projection_dir, change.kind === 'node' ? 'nodes' : 'edges', `${change.id}.json`);
|
|
1206
|
+
const entity = JSON.parse(await readFile(path, 'utf8'));
|
|
1207
|
+
entity.data = {
|
|
1208
|
+
...entity.data,
|
|
1209
|
+
meta: signedMeta(entity.data?.meta, change.evidence)
|
|
1210
|
+
};
|
|
1211
|
+
await writeJsonAtomically(path, entity);
|
|
1212
|
+
}
|
|
1213
|
+
return {
|
|
1214
|
+
...result,
|
|
1215
|
+
widened_scope: widened.map((rule) => ({
|
|
1216
|
+
kind: rule.entity_kind,
|
|
1217
|
+
id: rule.entity_id
|
|
1218
|
+
}))
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* `sdlc workspace evidence record`: writes each node's source references
|
|
1223
|
+
* (already verified at `commit` by the caller) into the current workspace's
|
|
1224
|
+
* projection, as sources.py record does, each replacing a reference of the
|
|
1225
|
+
* same identity under any name the repository goes by. With `sign`, signs
|
|
1226
|
+
* exactly those references under the same lock, and restores the node
|
|
1227
|
+
* files when the sign fails or is refused, so nothing is left recorded
|
|
1228
|
+
* unsigned. Refuses, before writing anything, a node with no file in the
|
|
1229
|
+
* projection or holding legacy source_evidence. A node whose references
|
|
1230
|
+
* would pass 100 is left out and returned under over_limit with the count
|
|
1231
|
+
* it would have (sources.py record). Commits nothing.
|
|
1232
|
+
*/
|
|
1233
|
+
async recordSourceReferences(input) {
|
|
1234
|
+
const { workspace } = await this.authority.requireCurrent(input.actor_workspace_id);
|
|
1235
|
+
const repository = resolve(input.repository);
|
|
1236
|
+
return withWorkspaceOperationLock({
|
|
1237
|
+
control_dir: controlDirectory(workspace),
|
|
1238
|
+
operation: 'evidence-record',
|
|
1239
|
+
run: async () => {
|
|
1240
|
+
const current = this.requireWorkspace(workspace.id);
|
|
1241
|
+
const projection_dir = projectionDirectory(current);
|
|
1242
|
+
const names = await repositoryNames(repository, input.commit);
|
|
1243
|
+
if (!names.length)
|
|
1244
|
+
throw new Error(`the ${await trackedRemoteName(repository)} remote names no <owner>/<name>, so references have no repository identity`);
|
|
1245
|
+
const observed_at = (input.now ?? new Date())
|
|
1246
|
+
.toISOString()
|
|
1247
|
+
.replace(/\.\d{3}Z$/, 'Z');
|
|
1248
|
+
const updates = [];
|
|
1249
|
+
const ids = [];
|
|
1250
|
+
const over_limit = {};
|
|
1251
|
+
for (const [node_id, specs] of Object.entries(input.nodes)) {
|
|
1252
|
+
const path = join(projection_dir, 'nodes', `${node_id}.json`);
|
|
1253
|
+
let node;
|
|
1254
|
+
try {
|
|
1255
|
+
node = JSON.parse(await readFile(path, 'utf8'));
|
|
1256
|
+
}
|
|
1257
|
+
catch {
|
|
1258
|
+
throw new Error(`node ${node_id} is not in the workspace's projection`);
|
|
1259
|
+
}
|
|
1260
|
+
const data = { ...(node.data ?? {}) };
|
|
1261
|
+
const meta = { ...(data.meta ?? {}) };
|
|
1262
|
+
// As sources.py tests it: an empty legacy block holds nothing to convert.
|
|
1263
|
+
const legacy = meta.source_evidence;
|
|
1264
|
+
if (legacy &&
|
|
1265
|
+
(typeof legacy !== 'object' || Object.keys(legacy).length > 0))
|
|
1266
|
+
throw new Error(`node ${node_id} holds legacy source_evidence; convert it to meta.evidence (the current form) before recording on it`);
|
|
1267
|
+
const references = specs.map((spec) => sourceReference(node_id, spec, input.commit, names[0], observed_at));
|
|
1268
|
+
try {
|
|
1269
|
+
meta.evidence = evidenceWith(meta, references, names);
|
|
1270
|
+
}
|
|
1271
|
+
catch (error) {
|
|
1272
|
+
if (!(error instanceof ReferenceLimitError))
|
|
1273
|
+
throw error;
|
|
1274
|
+
over_limit[node_id] = error.count;
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
ids.push(...references.map((ref) => String(ref.reference_id)));
|
|
1278
|
+
updates.push({ path, node: { ...node, data: { ...data, meta } } });
|
|
1279
|
+
}
|
|
1280
|
+
const recorded = Object.keys(input.nodes).filter((node_id) => !(node_id in over_limit));
|
|
1281
|
+
const before = new Map();
|
|
1282
|
+
for (const { path } of updates)
|
|
1283
|
+
before.set(path, await readFile(path));
|
|
1284
|
+
const restore = async () => {
|
|
1285
|
+
for (const [path, content] of before)
|
|
1286
|
+
await writeFile(path, content);
|
|
1287
|
+
};
|
|
1288
|
+
try {
|
|
1289
|
+
for (const { path, node } of updates)
|
|
1290
|
+
await writeJsonAtomically(path, node);
|
|
1291
|
+
if (!input.sign || !updates.length)
|
|
1292
|
+
return {
|
|
1293
|
+
workspace_id: current.id,
|
|
1294
|
+
repository: names[0],
|
|
1295
|
+
recorded,
|
|
1296
|
+
over_limit
|
|
1297
|
+
};
|
|
1298
|
+
const signed = await this.signEvidenceLocked(current.id, {
|
|
1299
|
+
references: ids,
|
|
1300
|
+
commit: input.commit,
|
|
1301
|
+
repository,
|
|
1302
|
+
now: input.now
|
|
1303
|
+
});
|
|
1304
|
+
if (signed.status === 'refused')
|
|
1305
|
+
throw new Error(`signing the recorded references was refused: ${signed.failures
|
|
1306
|
+
.map((f) => `${f.kind} ${f.id}: ${f.problem}`)
|
|
1307
|
+
.join('; ') || 'no failure named'}`);
|
|
1308
|
+
return {
|
|
1309
|
+
workspace_id: current.id,
|
|
1310
|
+
repository: names[0],
|
|
1311
|
+
recorded,
|
|
1312
|
+
over_limit,
|
|
1313
|
+
signed
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
catch (error) {
|
|
1317
|
+
await restore();
|
|
1318
|
+
throw error;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* The source references of a workspace's graph as built (`sdlc workspace
|
|
1325
|
+
* evidence lookup|check|coverage`): its projection as it stands, so
|
|
1326
|
+
* references just recorded count before a commit, with each pending claim
|
|
1327
|
+
* at its built content, as sources.py index reads it with
|
|
1328
|
+
* owners.built_claims. Only nodes with a pending claim come from the built
|
|
1329
|
+
* view: one left out there as an orphan, under a parent never built, still
|
|
1330
|
+
* names its files.
|
|
1331
|
+
*/
|
|
1332
|
+
async workspaceSourceIndex(input) {
|
|
1333
|
+
const workspace = this.requireWorkspace(input.workspace_id);
|
|
1334
|
+
const { graph } = await composeGraphProjection(projectionDirectory(workspace), undefined, { base: () => this.readCommitGraph(workspace.head_sdlc_commit_id) });
|
|
1335
|
+
const pending = this.store.built_content.listPendingClaimContent(workspace.id);
|
|
1336
|
+
const built = await readBuiltGraph({
|
|
1337
|
+
graph,
|
|
1338
|
+
pending,
|
|
1339
|
+
readGraph: (commit_id) => this.readCommitGraph(commit_id)
|
|
1340
|
+
});
|
|
1341
|
+
const claimed = new Set(pending
|
|
1342
|
+
.filter((claim) => claim.entity_kind === 'node')
|
|
1343
|
+
.map((claim) => claim.entity_id));
|
|
1344
|
+
const nodes = [
|
|
1345
|
+
...graph.nodes.filter((node) => !claimed.has(node.id)),
|
|
1346
|
+
...built.graph.nodes.filter((node) => claimed.has(node.id))
|
|
1347
|
+
];
|
|
1348
|
+
return { workspace_id: workspace.id, ...sourceIndex({ ...graph, nodes }) };
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* What changed repository files reach in a workspace's committed head graph
|
|
1352
|
+
* (`sdlc workspace blast-radius`). Read-only: it answers from the head, so
|
|
1353
|
+
* uncommitted edits never change the answer. The planned view composes the
|
|
1354
|
+
* projection only to say when it differs (`projection_dirty` and a warning);
|
|
1355
|
+
* the built view, read by readiness on every landing, skips that compose.
|
|
1356
|
+
*/
|
|
1357
|
+
async workspaceBlastRadius(input) {
|
|
1358
|
+
const workspace = this.requireWorkspace(input.workspace_id);
|
|
1359
|
+
const commit_id = workspace.head_sdlc_commit_id;
|
|
1360
|
+
const view = input.view ?? 'planned';
|
|
1361
|
+
const built = view === 'built'
|
|
1362
|
+
? await this.integration_runtime.readBuiltView({
|
|
1363
|
+
coordinator_workspace_id: workspace.id
|
|
1364
|
+
})
|
|
1365
|
+
: null;
|
|
1366
|
+
const head = built ? null : await this.readCommitGraph(commit_id);
|
|
1367
|
+
const dirty = head ? await this.projectionDirty(workspace, head) : null;
|
|
1368
|
+
const result = await blastRadius({
|
|
1369
|
+
graph: built?.graph ?? head,
|
|
1370
|
+
files: input.files,
|
|
1371
|
+
// An unqualified changed file belongs to the repository read here.
|
|
1372
|
+
repository_names: await repositoryNames(this.git.repository_root),
|
|
1373
|
+
workspace_id: workspace.id,
|
|
1374
|
+
commit_id,
|
|
1375
|
+
view,
|
|
1376
|
+
...(built
|
|
1377
|
+
? {
|
|
1378
|
+
marked_for_removal: built.marked_for_removal,
|
|
1379
|
+
validation: built.validation
|
|
1380
|
+
}
|
|
1381
|
+
: {})
|
|
1382
|
+
});
|
|
1383
|
+
if (!dirty)
|
|
1384
|
+
return result;
|
|
1385
|
+
return {
|
|
1386
|
+
...result,
|
|
1387
|
+
...(dirty.dirty === null ? {} : { projection_dirty: dirty.dirty }),
|
|
1388
|
+
...(dirty.warning ? { warnings: [dirty.warning] } : {})
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
/** `sdlc workspace find`: nodes ranked by the words they match (graph-read.ts). */
|
|
1392
|
+
async workspaceFind(input) {
|
|
1393
|
+
const read = await this.readGraphForView(input.workspace_id, input.view, 'find');
|
|
1394
|
+
const found = findGraphEntities({
|
|
1395
|
+
graph: read.graph,
|
|
1396
|
+
terms: input.terms,
|
|
1397
|
+
types: input.types,
|
|
1398
|
+
origin: input.origin,
|
|
1399
|
+
limit: input.limit,
|
|
1400
|
+
view: read.view,
|
|
1401
|
+
workspace_id: read.workspace_id,
|
|
1402
|
+
commit_id: read.commit_id,
|
|
1403
|
+
removed: read.removed
|
|
1404
|
+
});
|
|
1405
|
+
const warnings = [...(read.warnings ?? []), ...(found.warnings ?? [])];
|
|
1406
|
+
return {
|
|
1407
|
+
...found,
|
|
1408
|
+
...read.extra,
|
|
1409
|
+
...(warnings.length > 0 ? { warnings } : {})
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
/** `sdlc workspace show`: nodes and edges read compactly with their relations (graph-read.ts). */
|
|
1413
|
+
async workspaceShow(input) {
|
|
1414
|
+
const read = await this.readGraphForView(input.workspace_id, input.view, 'show');
|
|
1415
|
+
return {
|
|
1416
|
+
...showGraphEntities({
|
|
1417
|
+
graph: read.graph,
|
|
1418
|
+
ids: input.ids,
|
|
1419
|
+
budget: input.budget,
|
|
1420
|
+
view: read.view,
|
|
1421
|
+
workspace_id: read.workspace_id,
|
|
1422
|
+
commit_id: read.commit_id,
|
|
1423
|
+
removed: read.removed
|
|
1424
|
+
}),
|
|
1425
|
+
...read.extra,
|
|
1426
|
+
...(read.warnings ? { warnings: read.warnings } : {})
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* The graph a read command answers from: the committed head, or the built
|
|
1431
|
+
* view, as blast radius reads it, with what blast radius reports beside it.
|
|
1432
|
+
*/
|
|
1433
|
+
async readGraphForView(workspace_id, requested, command) {
|
|
1434
|
+
const workspace = this.requireWorkspace(workspace_id);
|
|
1435
|
+
const commit_id = workspace.head_sdlc_commit_id;
|
|
1436
|
+
const view = requested ?? 'planned';
|
|
1437
|
+
if (view === 'built') {
|
|
1438
|
+
const built = await this.integration_runtime.readBuiltView({
|
|
1439
|
+
coordinator_workspace_id: workspace.id
|
|
1440
|
+
});
|
|
1441
|
+
return {
|
|
1442
|
+
graph: built.graph,
|
|
1443
|
+
view,
|
|
1444
|
+
workspace_id: workspace.id,
|
|
1445
|
+
commit_id,
|
|
1446
|
+
removed: new Set(built.marked_for_removal.map((entity) => entity.entity_id)),
|
|
1447
|
+
extra: {
|
|
1448
|
+
marked_for_removal: built.marked_for_removal,
|
|
1449
|
+
validation: built.validation
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
const graph = await this.readCommitGraph(commit_id);
|
|
1454
|
+
const dirty = await this.projectionDirty(workspace, graph, command);
|
|
1455
|
+
return {
|
|
1456
|
+
graph,
|
|
1457
|
+
view,
|
|
1458
|
+
workspace_id: workspace.id,
|
|
1459
|
+
commit_id,
|
|
1460
|
+
extra: dirty.dirty === null ? {} : { projection_dirty: dirty.dirty },
|
|
1461
|
+
...(dirty.warning ? { warnings: [dirty.warning] } : {})
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
/** Whether a workspace's projection differs from its committed head graph, with the warning blast radius gives. */
|
|
1465
|
+
async projectionDirty(workspace, head, command = 'blast-radius') {
|
|
1466
|
+
let projection;
|
|
1467
|
+
try {
|
|
1468
|
+
({ graph: projection } = await composeGraphProjection(projectionDirectory(workspace), undefined, { base: async () => head }));
|
|
1469
|
+
}
|
|
1470
|
+
catch (error) {
|
|
1471
|
+
return {
|
|
1472
|
+
dirty: null,
|
|
1473
|
+
warning: `The workspace projection could not be compared with its committed head (${error instanceof Error ? error.message : String(error)}); this answer reads the committed head.`
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
if (localGraphRootHash(projection) ===
|
|
1477
|
+
this.commitRoot(workspace.head_sdlc_commit_id)) {
|
|
1478
|
+
return { dirty: false, warning: null };
|
|
1479
|
+
}
|
|
1480
|
+
// commit takes the workspace of the directory it runs in, so name another one's worktree.
|
|
1481
|
+
const where = workspace.git_worktree_path === this.git.repository_root
|
|
1482
|
+
? ''
|
|
1483
|
+
: ` in ${workspace.git_worktree_path}`;
|
|
1484
|
+
return {
|
|
1485
|
+
dirty: true,
|
|
1486
|
+
warning: `The workspace projection has uncommitted changes, such as source references sources record just wrote; this answer reads the committed head. Run sdlc workspace commit --message "<what changed>"${where}, then run ${command} again.`
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Compares the committed plans of a coordinator's active child workspaces
|
|
1491
|
+
* pairwise. Read-only: it never locks, commits, or moves a workspace.
|
|
1492
|
+
*/
|
|
1493
|
+
async compareChildWorkspacePlans(input) {
|
|
1494
|
+
const coordinator = this.requireWorkspace(input.coordinator_workspace_id);
|
|
1495
|
+
const coordinator_commit_id = coordinator.head_sdlc_commit_id;
|
|
1496
|
+
const children = this.store
|
|
1497
|
+
.listChildWorkspaces(coordinator.id)
|
|
1498
|
+
.filter((workspace) => workspace.status === 'active');
|
|
1499
|
+
const graphs = new Map();
|
|
1500
|
+
const readGraph = (commit_id) => {
|
|
1501
|
+
let graph = graphs.get(commit_id);
|
|
1502
|
+
if (!graph) {
|
|
1503
|
+
graph = this.readCommitGraph(commit_id);
|
|
1504
|
+
graphs.set(commit_id, graph);
|
|
1505
|
+
}
|
|
1506
|
+
return graph;
|
|
1507
|
+
};
|
|
1508
|
+
const coordinator_graph = await readGraph(coordinator_commit_id);
|
|
1509
|
+
const plans = [];
|
|
1510
|
+
const projection_dirty = new Map();
|
|
1511
|
+
for (const child of children) {
|
|
1512
|
+
let base_commit_id;
|
|
1513
|
+
try {
|
|
1514
|
+
// Integration and reconciliation record merge parents, so the merge
|
|
1515
|
+
// base excludes work this plan already landed or pulled in.
|
|
1516
|
+
base_commit_id = this.store.history.findMergeBase(child.head_sdlc_commit_id, coordinator_commit_id);
|
|
1517
|
+
}
|
|
1518
|
+
catch {
|
|
1519
|
+
base_commit_id = child.fork_sdlc_commit_id;
|
|
1520
|
+
}
|
|
1521
|
+
// A plan with no commits of its own (it landed, or was never edited)
|
|
1522
|
+
// compares as the coordinator head: neither of its graphs nor its
|
|
1523
|
+
// projection is read, and whether it has uncommitted edits is left
|
|
1524
|
+
// unknown. Most open plans are such plans, and composing each one's
|
|
1525
|
+
// projection (thousands of files, then a root hash) cost most of a run
|
|
1526
|
+
// once they numbered over a hundred (iss-130).
|
|
1527
|
+
const held = base_commit_id === child.head_sdlc_commit_id;
|
|
1528
|
+
const [base_graph, head_graph] = held
|
|
1529
|
+
? [coordinator_graph, coordinator_graph]
|
|
1530
|
+
: await Promise.all([
|
|
1531
|
+
readGraph(base_commit_id),
|
|
1532
|
+
readGraph(child.head_sdlc_commit_id)
|
|
1533
|
+
]);
|
|
1534
|
+
plans.push({
|
|
1535
|
+
workspace_id: child.id,
|
|
1536
|
+
base_commit_id,
|
|
1537
|
+
head_commit_id: child.head_sdlc_commit_id,
|
|
1538
|
+
base_graph,
|
|
1539
|
+
head_graph
|
|
1540
|
+
});
|
|
1541
|
+
if (held) {
|
|
1542
|
+
projection_dirty.set(child.id, null);
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
try {
|
|
1546
|
+
// Root hashes alone say whether the projection moved: no graph is read.
|
|
1547
|
+
const { graph } = await composeGraphProjection(projectionDirectory(child), undefined, { base: async () => head_graph });
|
|
1548
|
+
projection_dirty.set(child.id, this.commitRoot(child.head_sdlc_commit_id) !==
|
|
1549
|
+
localGraphRootHash(graph));
|
|
1550
|
+
}
|
|
1551
|
+
catch {
|
|
1552
|
+
// A missing or unreadable projection does not change the committed plan.
|
|
1553
|
+
projection_dirty.set(child.id, null);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
const comparison = compareLocalWorkspacePlans({
|
|
1557
|
+
coordinator_commit_id,
|
|
1558
|
+
coordinator_graph,
|
|
1559
|
+
plans
|
|
1560
|
+
});
|
|
1561
|
+
const states = new Map(comparison.plans.map((state) => [state.workspace_id, state]));
|
|
1562
|
+
let coordinator_ancestors = null;
|
|
1563
|
+
for (const pair of comparison.pairs) {
|
|
1564
|
+
const units = allocationUnits(pair);
|
|
1565
|
+
if (units.length === 0)
|
|
1566
|
+
continue;
|
|
1567
|
+
coordinator_ancestors ??= this.store.history.ancestorIds(coordinator_commit_id);
|
|
1568
|
+
const reachable = coordinator_ancestors;
|
|
1569
|
+
const first_changed = await Promise.all(pair.workspace_ids.map((workspace_id) => firstOwnChanges({
|
|
1570
|
+
head_commit_id: plans.find((plan) => plan.workspace_id === workspace_id)
|
|
1571
|
+
?.head_commit_id ?? '',
|
|
1572
|
+
units,
|
|
1573
|
+
getCommit: (commit_id) => this.store.history.getCommit(commit_id),
|
|
1574
|
+
reachableFromCoordinator: (commit_id) => reachable.has(commit_id),
|
|
1575
|
+
readGraph
|
|
1576
|
+
})));
|
|
1577
|
+
const timed = units.map((unit) => ({
|
|
1578
|
+
...unit,
|
|
1579
|
+
first_changed_at: Object.fromEntries(pair.workspace_ids.map((workspace_id, index) => [
|
|
1580
|
+
workspace_id,
|
|
1581
|
+
first_changed[index]?.get(allocationUnitKey(unit)) ?? null
|
|
1582
|
+
]))
|
|
1583
|
+
}));
|
|
1584
|
+
// A block only by validation is one unit: the combination of the
|
|
1585
|
+
// entities fails, so one plan owns it, by its earliest change to any.
|
|
1586
|
+
pair.allocation =
|
|
1587
|
+
pair.conflicts.length > 0 ? timed : [combineValidationUnits(timed)];
|
|
1588
|
+
}
|
|
1589
|
+
return {
|
|
1590
|
+
version: 1,
|
|
1591
|
+
coordinator_workspace_id: coordinator.id,
|
|
1592
|
+
coordinator_head_commit_id: coordinator_commit_id,
|
|
1593
|
+
verdict: comparison.verdict,
|
|
1594
|
+
stale_workspace_ids: comparison.plans
|
|
1595
|
+
.filter((state) => state.status === 'stale')
|
|
1596
|
+
.map((state) => state.workspace_id),
|
|
1597
|
+
workspaces: plans.map((plan) => {
|
|
1598
|
+
const child = children.find((workspace) => workspace.id === plan.workspace_id);
|
|
1599
|
+
const state = states.get(plan.workspace_id);
|
|
1600
|
+
if (!child || !state) {
|
|
1601
|
+
throw new Error('Workspace plan comparison lost a child workspace.');
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
...state,
|
|
1605
|
+
git_worktree_path: child.git_worktree_path,
|
|
1606
|
+
base_commit_id: plan.base_commit_id,
|
|
1607
|
+
head_commit_id: plan.head_commit_id,
|
|
1608
|
+
projection_dirty: projection_dirty.get(child.id) ?? null
|
|
1609
|
+
};
|
|
1610
|
+
}),
|
|
1611
|
+
compared_pairs: comparison.compared_pairs,
|
|
1612
|
+
pairs: comparison.pairs
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
inspectAuthority() {
|
|
1616
|
+
return inspectLocalWorkspaceAuthority({
|
|
1617
|
+
workspace: this.currentActiveWorkspace(),
|
|
1618
|
+
git: this.git
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
resolverStatus() {
|
|
1622
|
+
return this.environment_runtime.resolverStatus();
|
|
1623
|
+
}
|
|
1624
|
+
async configureResolver(input) {
|
|
1625
|
+
return this.environment_runtime.configureResolver(input);
|
|
1626
|
+
}
|
|
1627
|
+
async configureResolverFromFile(input) {
|
|
1628
|
+
return this.environment_runtime.configureResolverFromFile(input);
|
|
1629
|
+
}
|
|
1630
|
+
async clearResolver(input) {
|
|
1631
|
+
return this.environment_runtime.clearResolver(input);
|
|
1632
|
+
}
|
|
1633
|
+
async loadResolver(input) {
|
|
1634
|
+
return this.environment_runtime.loadResolver(input);
|
|
1635
|
+
}
|
|
1636
|
+
listRemotes() {
|
|
1637
|
+
return this.environment_runtime.listRemotes();
|
|
1638
|
+
}
|
|
1639
|
+
inspectRemote(name) {
|
|
1640
|
+
return this.environment_runtime.inspectRemote(name);
|
|
1641
|
+
}
|
|
1642
|
+
async setRemote(input) {
|
|
1643
|
+
return this.environment_runtime.setRemote(input);
|
|
1644
|
+
}
|
|
1645
|
+
async removeRemote(input) {
|
|
1646
|
+
return this.environment_runtime.removeRemote(input);
|
|
1647
|
+
}
|
|
1648
|
+
async discoverRemote(input) {
|
|
1649
|
+
return this.environment_runtime.discoverRemote(input);
|
|
1650
|
+
}
|
|
1651
|
+
listRemoteTrackingRefs(name) {
|
|
1652
|
+
return this.environment_runtime.listRemoteTrackingRefs(name);
|
|
1653
|
+
}
|
|
1654
|
+
listLocalBranches() {
|
|
1655
|
+
return this.branch_runtime.list();
|
|
1656
|
+
}
|
|
1657
|
+
async createLocalBranchFromTrackingRef(input) {
|
|
1658
|
+
return this.branch_runtime.createFromTrackingRef(input);
|
|
1659
|
+
}
|
|
1660
|
+
async selectLocalBranch(input) {
|
|
1661
|
+
return this.branch_runtime.select(input);
|
|
1662
|
+
}
|
|
1663
|
+
async integrateBranchUpstream(input) {
|
|
1664
|
+
return this.upstream_runtime.integrate(input);
|
|
1665
|
+
}
|
|
1666
|
+
listBranchIntegrationAttempts(local_ref) {
|
|
1667
|
+
return this.upstream_runtime.listAttempts(local_ref);
|
|
1668
|
+
}
|
|
1669
|
+
listBranchIntegrationResolutionAttempts(attempt_id) {
|
|
1670
|
+
return this.upstream_runtime.listResolutionAttempts(attempt_id);
|
|
1671
|
+
}
|
|
1672
|
+
listBranchIntegrationRevertAttempts(local_ref) {
|
|
1673
|
+
return this.upstream_runtime.listRevertAttempts(local_ref);
|
|
1674
|
+
}
|
|
1675
|
+
inspectBranchUpstream(input) {
|
|
1676
|
+
return this.upstream_runtime.inspect(input);
|
|
1677
|
+
}
|
|
1678
|
+
revertBranchUpstream(input) {
|
|
1679
|
+
return this.upstream_runtime.revert(input);
|
|
1680
|
+
}
|
|
1681
|
+
async fetchRemote(input) {
|
|
1682
|
+
return this.environment_runtime.fetchRemote(input);
|
|
1683
|
+
}
|
|
1684
|
+
listRemotePushJournals(name) {
|
|
1685
|
+
return this.environment_runtime.listRemotePushJournals(name);
|
|
1686
|
+
}
|
|
1687
|
+
listRemoteTopicJournals(name) {
|
|
1688
|
+
return this.environment_runtime.listRemoteTopicJournals(name);
|
|
1689
|
+
}
|
|
1690
|
+
listRemotePullRequestJournals(name) {
|
|
1691
|
+
return this.environment_runtime.listRemotePullRequestJournals(name);
|
|
1692
|
+
}
|
|
1693
|
+
async planPortableHistory(input) {
|
|
1694
|
+
return this.environment_runtime.planPortableHistory(input);
|
|
1695
|
+
}
|
|
1696
|
+
async convertPortableHistory(input) {
|
|
1697
|
+
return this.environment_runtime.convertPortableHistory(input);
|
|
1698
|
+
}
|
|
1699
|
+
async pushRemote(input) {
|
|
1700
|
+
return this.environment_runtime.pushRemote(input);
|
|
1701
|
+
}
|
|
1702
|
+
async publishRemoteTopic(input) {
|
|
1703
|
+
return this.environment_runtime.publishRemoteTopic(input);
|
|
1704
|
+
}
|
|
1705
|
+
async createRemotePullRequest(input) {
|
|
1706
|
+
return this.environment_runtime.createRemotePullRequest(input);
|
|
1707
|
+
}
|
|
1708
|
+
async getWorkspaceStatus(workspace_id) {
|
|
1709
|
+
const workspace = this.requireWorkspace(workspace_id);
|
|
1710
|
+
const { graph } = await composeGraphProjection(projectionDirectory(workspace), undefined, { base: () => this.readCommitGraph(workspace.head_sdlc_commit_id) });
|
|
1711
|
+
const projection_root_hash = localGraphRootHash(graph);
|
|
1712
|
+
const head_root_hash = this.commitRoot(workspace.head_sdlc_commit_id);
|
|
1713
|
+
await this.graphs.readGraph(head_root_hash);
|
|
1714
|
+
const source = workspace.source_workspace_id
|
|
1715
|
+
? this.store.getWorkspace(workspace.source_workspace_id)
|
|
1716
|
+
: null;
|
|
1717
|
+
return {
|
|
1718
|
+
version: 1,
|
|
1719
|
+
workspace,
|
|
1720
|
+
projection_dir: projectionDirectory(workspace),
|
|
1721
|
+
control_dir: controlDirectory(workspace),
|
|
1722
|
+
head_root_hash,
|
|
1723
|
+
projection_root_hash,
|
|
1724
|
+
projection_dirty: head_root_hash !== projection_root_hash,
|
|
1725
|
+
task_scope: this.store.getWorkspaceTaskScope(workspace.id),
|
|
1726
|
+
...(source
|
|
1727
|
+
? {
|
|
1728
|
+
source: {
|
|
1729
|
+
workspace_id: source.id,
|
|
1730
|
+
head_revision: source.head_sdlc_commit_id,
|
|
1731
|
+
behind: source.head_sdlc_commit_id !== workspace.head_sdlc_commit_id &&
|
|
1732
|
+
!this.store.history.isAncestor(source.head_sdlc_commit_id, workspace.head_sdlc_commit_id)
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
: {})
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
async commitCurrentProjection(input) {
|
|
1739
|
+
const { workspace, git } = await this.authority.requireCurrent(input.actor_workspace_id);
|
|
1740
|
+
// Before the commit reads the binding for its source context; a move that
|
|
1741
|
+
// fails changes nothing, and the commit keeps the binding it has
|
|
1742
|
+
await this.realignForkBinding(git.repository_root, git.git_common_dir).catch(() => null);
|
|
1743
|
+
return withWorkspaceOperationLock({
|
|
1744
|
+
control_dir: controlDirectory(workspace),
|
|
1745
|
+
operation: 'commit',
|
|
1746
|
+
run: async () => {
|
|
1747
|
+
const current_workspace = this.requireWorkspace(workspace.id);
|
|
1748
|
+
const base_graph = await this.readCommitGraph(current_workspace.head_sdlc_commit_id);
|
|
1749
|
+
const { graph } = await composeGraphProjection(projectionDirectory(current_workspace), undefined, { base: async () => base_graph });
|
|
1750
|
+
// A node parent the head already had is not this commit's to fix.
|
|
1751
|
+
const graph_issues = validateProjectionGraph(graph, {
|
|
1752
|
+
node_parents: true
|
|
1753
|
+
}).issues;
|
|
1754
|
+
const issues = introducedBrokenIssues(graph, [base_graph], graph_issues).map((issue) => `${issue.code}: ${issue.message}`);
|
|
1755
|
+
if (issues.length > 0) {
|
|
1756
|
+
throw new Error(`Cannot commit invalid projection: ${issues.join('; ')}`);
|
|
1757
|
+
}
|
|
1758
|
+
const diff = diffProjectionGraphs({
|
|
1759
|
+
base_graph,
|
|
1760
|
+
current_graph: graph,
|
|
1761
|
+
deletion_intents: this.store.listWorkspaceDeletions(current_workspace.id)
|
|
1762
|
+
});
|
|
1763
|
+
if (diff.missing_unrecorded.length > 0) {
|
|
1764
|
+
throw new Error(`Projection has unrecorded deletions: ${diff.missing_unrecorded
|
|
1765
|
+
.map((entry) => entry.relative_path)
|
|
1766
|
+
.join(', ')}; record them with sdlc workspace rm.`);
|
|
1767
|
+
}
|
|
1768
|
+
if (!projectionDiffHasChanges(diff)) {
|
|
1769
|
+
// Intents for entities that are still present no longer apply.
|
|
1770
|
+
this.store.clearWorkspaceDeletions(current_workspace.id);
|
|
1771
|
+
return {
|
|
1772
|
+
version: 1,
|
|
1773
|
+
status: 'unchanged',
|
|
1774
|
+
workspace: current_workspace,
|
|
1775
|
+
root_hash: this.commitRoot(current_workspace.head_sdlc_commit_id)
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
assertGraphChangesWithinTaskScope({
|
|
1779
|
+
base_graph,
|
|
1780
|
+
current_graph: graph,
|
|
1781
|
+
task_scope: this.store.getWorkspaceTaskScope(current_workspace.id)
|
|
1782
|
+
});
|
|
1783
|
+
assertNoRetiredKeysOnChangedNodes({ base_graph, current_graph: graph });
|
|
1784
|
+
assertNodeTypeRulesOnChangedNodes({ base_graph, current_graph: graph });
|
|
1785
|
+
assertIntroducedVocabularyAdmitted({
|
|
1786
|
+
base_graph,
|
|
1787
|
+
graph,
|
|
1788
|
+
admission: this.store.admissions.get()
|
|
1789
|
+
});
|
|
1790
|
+
const use_issues = packageUseCommitIssues({
|
|
1791
|
+
base_graph,
|
|
1792
|
+
graph,
|
|
1793
|
+
source: () => heldPackages()
|
|
1794
|
+
});
|
|
1795
|
+
if (use_issues.length > 0) {
|
|
1796
|
+
throw new Error(`Cannot commit package uses that do not resolve: ${use_issues.join('; ')}`);
|
|
1797
|
+
}
|
|
1798
|
+
const embed_warnings = introducedDanglingDocumentEmbeds({
|
|
1799
|
+
base_graph,
|
|
1800
|
+
graph
|
|
1801
|
+
});
|
|
1802
|
+
const library_warnings = getLibraryOriginWarnings(graph, {
|
|
1803
|
+
node_ids: new Set([...diff.added, ...diff.modified]
|
|
1804
|
+
.filter((entry) => entry.entity_kind === 'node')
|
|
1805
|
+
.map((entry) => entry.entity_id))
|
|
1806
|
+
});
|
|
1807
|
+
const evidence_drop_warning = evidenceDropWarning(droppedEvidenceReferences({ base_graph, graph }));
|
|
1808
|
+
// What a hosted save of this graph would warn of, less what the head had.
|
|
1809
|
+
const validation_warnings = introducedValidationWarnings(graph, base_graph, graph_issues);
|
|
1810
|
+
const root_hash = await this.graphs.writeGraph(graph);
|
|
1811
|
+
const binding = this.store.getSourceRepositoryBinding(git.git_common_dir);
|
|
1812
|
+
const commit = this.store.history.commitWorkspace({
|
|
1813
|
+
actor_workspace_id: current_workspace.id,
|
|
1814
|
+
expected_head_commit_id: current_workspace.head_sdlc_commit_id,
|
|
1815
|
+
root_hash,
|
|
1816
|
+
message: input.message,
|
|
1817
|
+
validation_evidence: [
|
|
1818
|
+
'complete graph validation passed',
|
|
1819
|
+
...(embed_warnings.length > 0
|
|
1820
|
+
? [
|
|
1821
|
+
`Document embeds reference missing entities: ${embed_warnings
|
|
1822
|
+
.map((issue) => `${issue.node_id}#${issue.block_id} -> ${issue.entity_kind} ${issue.entity_id}`)
|
|
1823
|
+
.join(', ')}`
|
|
1824
|
+
]
|
|
1825
|
+
: []),
|
|
1826
|
+
...library_warnings.map((warning) => warning.message),
|
|
1827
|
+
...(evidence_drop_warning ? [evidence_drop_warning] : []),
|
|
1828
|
+
...validation_warnings
|
|
1829
|
+
],
|
|
1830
|
+
source_context: binding
|
|
1831
|
+
? {
|
|
1832
|
+
source_repository_association_id: binding.source_repository_association_id,
|
|
1833
|
+
remote_identity: binding.remote_identity,
|
|
1834
|
+
git_ref: git.ref,
|
|
1835
|
+
git_head: git.head,
|
|
1836
|
+
evidence_scope: input.evidence_scope ?? {
|
|
1837
|
+
include_paths: [],
|
|
1838
|
+
exclude_paths: []
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
: undefined
|
|
1842
|
+
});
|
|
1843
|
+
this.store.clearWorkspaceDeletions(current_workspace.id);
|
|
1844
|
+
await recordScopedProjectionFiles(projectionDirectory(current_workspace));
|
|
1845
|
+
const source_context = binding
|
|
1846
|
+
? this.store.getWorkspaceCommitSourceContext(commit.id)
|
|
1847
|
+
: null;
|
|
1848
|
+
const warnings = [
|
|
1849
|
+
...commit.validation_evidence.slice(1),
|
|
1850
|
+
...graphShapeWarnings(graph)
|
|
1851
|
+
];
|
|
1852
|
+
return {
|
|
1853
|
+
version: 1,
|
|
1854
|
+
status: 'committed',
|
|
1855
|
+
validation: {
|
|
1856
|
+
status: warnings.length > 0 ? 'passed_with_warnings' : 'passed',
|
|
1857
|
+
warnings
|
|
1858
|
+
},
|
|
1859
|
+
workspace: this.requireWorkspace(current_workspace.id),
|
|
1860
|
+
commit,
|
|
1861
|
+
source_context,
|
|
1862
|
+
...(embed_warnings.length > 0
|
|
1863
|
+
? { document_embed_warnings: embed_warnings }
|
|
1864
|
+
: {}),
|
|
1865
|
+
...(library_warnings.length > 0
|
|
1866
|
+
? { library_origin_warnings: library_warnings }
|
|
1867
|
+
: {})
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
async enqueueChildCommit(input) {
|
|
1873
|
+
return this.integration_runtime.enqueue(input);
|
|
1874
|
+
}
|
|
1875
|
+
async readBuiltView(input) {
|
|
1876
|
+
return this.integration_runtime.readBuiltView(input);
|
|
1877
|
+
}
|
|
1878
|
+
async integrateNext(input) {
|
|
1879
|
+
return this.integration_runtime.integrate(input);
|
|
1880
|
+
}
|
|
1881
|
+
async reconcileSource(input) {
|
|
1882
|
+
return this.integration_runtime.reconcile(input);
|
|
1883
|
+
}
|
|
1884
|
+
prepareHandoff(input) {
|
|
1885
|
+
return this.handoff_runtime.prepare({
|
|
1886
|
+
...input,
|
|
1887
|
+
reconcile_source: (reconciliation_input) => this.reconcileSource(reconciliation_input)
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
listHandoffs(actor_workspace_id) {
|
|
1891
|
+
return this.handoff_runtime.list(actor_workspace_id);
|
|
1892
|
+
}
|
|
1893
|
+
inspectHandoff(input) {
|
|
1894
|
+
return this.handoff_runtime.inspect(input);
|
|
1895
|
+
}
|
|
1896
|
+
applyHandoff(input) {
|
|
1897
|
+
return this.handoff_runtime.apply(input);
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* `sdlc workspace decision reopen`: reopens the Decision and, with
|
|
1901
|
+
* `revive_workspace_id`, checks out a new plan workspace at `target_path`
|
|
1902
|
+
* that starts at that closed candidate's last head and takes its place.
|
|
1903
|
+
*/
|
|
1904
|
+
async reopenDecision(input) {
|
|
1905
|
+
await this.decisionActor();
|
|
1906
|
+
if (!input.revive_workspace_id) {
|
|
1907
|
+
const { decision } = this.store.reopenDecision(input);
|
|
1908
|
+
return { version: 1, decision, workspace: null };
|
|
1909
|
+
}
|
|
1910
|
+
if (!input.target_path) {
|
|
1911
|
+
throw new Error('Reopening a closed candidate needs --path for its new workspace.');
|
|
1912
|
+
}
|
|
1913
|
+
const closed = this.requireWorkspace(input.revive_workspace_id);
|
|
1914
|
+
if (closed.status !== 'closed' || !closed.source_workspace_id) {
|
|
1915
|
+
throw new Error(`Workspace ${closed.id} is not a closed plan workspace.`);
|
|
1916
|
+
}
|
|
1917
|
+
const coordinator = this.requireWorkspace(closed.source_workspace_id);
|
|
1918
|
+
return withWorkspaceOperationLock({
|
|
1919
|
+
control_dir: controlDirectory(coordinator),
|
|
1920
|
+
operation: 'decision-reopen',
|
|
1921
|
+
run: async () => {
|
|
1922
|
+
const target_path = resolve(input.target_path);
|
|
1923
|
+
const created_git = await createDetachedGitWorktree({
|
|
1924
|
+
source_cwd: coordinator.git_worktree_path,
|
|
1925
|
+
target_path,
|
|
1926
|
+
commit_id: closed.fork_git_commit_id
|
|
1927
|
+
});
|
|
1928
|
+
// Everything in the new checkout is written before the one store
|
|
1929
|
+
// transaction, so any failure removes the checkout and leaves the
|
|
1930
|
+
// Decision as it was.
|
|
1931
|
+
const id = randomUUID();
|
|
1932
|
+
let reopened;
|
|
1933
|
+
try {
|
|
1934
|
+
await assertLocalWorkspacePathsIgnored(created_git.repository_root);
|
|
1935
|
+
await this.materialize_projection({
|
|
1936
|
+
projection_dir: join(created_git.repository_root, 'sdlc'),
|
|
1937
|
+
graph: await this.readCommitGraph(closed.head_sdlc_commit_id)
|
|
1938
|
+
});
|
|
1939
|
+
await mkdir(join(created_git.repository_root, '.sdlc', 'state'), {
|
|
1940
|
+
recursive: true,
|
|
1941
|
+
mode: 0o700
|
|
1942
|
+
});
|
|
1943
|
+
await this.persistCurrentTopology({ id }, this.topology?.storage_mode, created_git.repository_root, created_git.git_common_dir);
|
|
1944
|
+
reopened = this.store.reopenDecision({
|
|
1945
|
+
decision_id: input.decision_id,
|
|
1946
|
+
reason: input.reason,
|
|
1947
|
+
revive: {
|
|
1948
|
+
id,
|
|
1949
|
+
workspace_id: closed.id,
|
|
1950
|
+
git_worktree_path: created_git.repository_root,
|
|
1951
|
+
git_dir: created_git.git_dir,
|
|
1952
|
+
git_common_dir: created_git.git_common_dir,
|
|
1953
|
+
git_head_commit_id: created_git.head
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
catch (error) {
|
|
1958
|
+
await removeGitWorktree({
|
|
1959
|
+
repository_cwd: this.git.repository_root,
|
|
1960
|
+
target_path: created_git.repository_root,
|
|
1961
|
+
force: true
|
|
1962
|
+
}).catch(() => undefined);
|
|
1963
|
+
throw error;
|
|
1964
|
+
}
|
|
1965
|
+
return {
|
|
1966
|
+
version: 1,
|
|
1967
|
+
decision: reopened.decision,
|
|
1968
|
+
workspace: await this.getWorkspaceStatus(reopened.workspace.id)
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* `sdlc workspace pick`: copies chosen entities from another workspace's
|
|
1975
|
+
* commit into the acting workspace as one commit with a receipt.
|
|
1976
|
+
*/
|
|
1977
|
+
pickEntities(input) {
|
|
1978
|
+
return this.pick_runtime.pick(input);
|
|
1979
|
+
}
|
|
1980
|
+
/** The current workspace, authorized to run `sdlc workspace decision`. */
|
|
1981
|
+
async decisionActor() {
|
|
1982
|
+
const { workspace } = await this.authority.requireCurrent(this.currentActiveWorkspace().id);
|
|
1983
|
+
return workspace;
|
|
1984
|
+
}
|
|
1985
|
+
async closeWorkspace(input) {
|
|
1986
|
+
const { workspace: actor } = await this.authority.requireCurrent(input.actor_workspace_id);
|
|
1987
|
+
const workspace = this.requireWorkspace(input.workspace_id);
|
|
1988
|
+
return withWorkspaceOperationLock({
|
|
1989
|
+
control_dir: controlDirectory(workspace),
|
|
1990
|
+
operation: 'close',
|
|
1991
|
+
run: async () => {
|
|
1992
|
+
const current_workspace = this.requireWorkspace(workspace.id);
|
|
1993
|
+
const status = await this.getWorkspaceStatus(current_workspace.id);
|
|
1994
|
+
if (status.projection_dirty) {
|
|
1995
|
+
throw new Error('Workspace projection has uncommitted changes; commit them before closing.');
|
|
1996
|
+
}
|
|
1997
|
+
return this.store.closeWorkspace({
|
|
1998
|
+
actor_workspace_id: actor.id,
|
|
1999
|
+
workspace_id: current_workspace.id,
|
|
2000
|
+
expected_head_commit_id: current_workspace.head_sdlc_commit_id
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
async planPrune(input = {}) {
|
|
2006
|
+
const current = this.currentActiveWorkspace();
|
|
2007
|
+
await this.authority.requireCurrent(current.id);
|
|
2008
|
+
return this.pruner.plan({
|
|
2009
|
+
actor_workspace_id: current.id,
|
|
2010
|
+
...input
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
async applyPrune(input) {
|
|
2014
|
+
const current = this.currentActiveWorkspace();
|
|
2015
|
+
await this.authority.requireCurrent(current.id);
|
|
2016
|
+
return this.pruner.apply({
|
|
2017
|
+
actor_workspace_id: current.id,
|
|
2018
|
+
...input
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
async recoverCurrentProjection(input) {
|
|
2022
|
+
const { workspace } = await this.authority.requireCurrent(input.actor_workspace_id);
|
|
2023
|
+
if (!input.expected_projection_root_hash &&
|
|
2024
|
+
!input.discard_unreadable_projection) {
|
|
2025
|
+
throw new Error('Recovery requires --expected-projection-root unless unreadable projection discard is explicit.');
|
|
2026
|
+
}
|
|
2027
|
+
return withWorkspaceOperationLock({
|
|
2028
|
+
control_dir: controlDirectory(workspace),
|
|
2029
|
+
operation: 'recover',
|
|
2030
|
+
run: async () => {
|
|
2031
|
+
const current_workspace = this.requireWorkspace(workspace.id);
|
|
2032
|
+
if (current_workspace.head_sdlc_commit_id !==
|
|
2033
|
+
input.expected_head_commit_id) {
|
|
2034
|
+
throw new Error('Workspace head is stale; refusing projection recovery.');
|
|
2035
|
+
}
|
|
2036
|
+
const head_graph = await this.readCommitGraph(current_workspace.head_sdlc_commit_id);
|
|
2037
|
+
const head_root_hash = this.commitRoot(current_workspace.head_sdlc_commit_id);
|
|
2038
|
+
let previous_projection_root_hash = null;
|
|
2039
|
+
let discarded_unreadable_projection = false;
|
|
2040
|
+
let readable_projection = null;
|
|
2041
|
+
try {
|
|
2042
|
+
const { graph } = await composeGraphProjection(projectionDirectory(current_workspace), undefined, { base: async () => head_graph });
|
|
2043
|
+
readable_projection = graph;
|
|
2044
|
+
}
|
|
2045
|
+
catch (error) {
|
|
2046
|
+
if (!input.discard_unreadable_projection)
|
|
2047
|
+
throw error;
|
|
2048
|
+
discarded_unreadable_projection = true;
|
|
2049
|
+
}
|
|
2050
|
+
if (readable_projection) {
|
|
2051
|
+
if (!input.expected_projection_root_hash) {
|
|
2052
|
+
throw new Error('Readable projection recovery requires its expected root.');
|
|
2053
|
+
}
|
|
2054
|
+
previous_projection_root_hash =
|
|
2055
|
+
localGraphRootHash(readable_projection);
|
|
2056
|
+
if (previous_projection_root_hash !==
|
|
2057
|
+
input.expected_projection_root_hash) {
|
|
2058
|
+
throw new Error('Projection root is stale; refusing projection recovery.');
|
|
2059
|
+
}
|
|
2060
|
+
await this.graphs.writeGraph(readable_projection);
|
|
2061
|
+
}
|
|
2062
|
+
if (previous_projection_root_hash === head_root_hash) {
|
|
2063
|
+
return {
|
|
2064
|
+
version: 1,
|
|
2065
|
+
status: 'unchanged',
|
|
2066
|
+
workspace: current_workspace,
|
|
2067
|
+
head_root_hash,
|
|
2068
|
+
previous_projection_root_hash,
|
|
2069
|
+
discarded_unreadable_projection
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
// A discarded projection.json cannot name its scope: the task scope does.
|
|
2073
|
+
const rescope = discarded_unreadable_projection &&
|
|
2074
|
+
(await hasProjectionManifest(projectionDirectory(current_workspace)));
|
|
2075
|
+
const task_scope = this.store.getWorkspaceTaskScope(current_workspace.id);
|
|
2076
|
+
await this.materialize_projection({
|
|
2077
|
+
projection_dir: projectionDirectory(current_workspace),
|
|
2078
|
+
graph: head_graph,
|
|
2079
|
+
...(rescope && task_scope
|
|
2080
|
+
? { scope: projectionScopeOfTaskScope(task_scope) }
|
|
2081
|
+
: rescope
|
|
2082
|
+
? { full: true }
|
|
2083
|
+
: {})
|
|
2084
|
+
});
|
|
2085
|
+
// The head projection restores every entity a deletion intent named.
|
|
2086
|
+
this.store.clearWorkspaceDeletions(current_workspace.id);
|
|
2087
|
+
return {
|
|
2088
|
+
version: 1,
|
|
2089
|
+
status: 'recovered',
|
|
2090
|
+
workspace: current_workspace,
|
|
2091
|
+
head_root_hash,
|
|
2092
|
+
previous_projection_root_hash,
|
|
2093
|
+
discarded_unreadable_projection
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
requireWorkspace(workspace_id) {
|
|
2099
|
+
const workspace = this.store.getWorkspace(workspace_id);
|
|
2100
|
+
if (!workspace)
|
|
2101
|
+
throw new Error('Local workspace was not found.');
|
|
2102
|
+
return workspace;
|
|
2103
|
+
}
|
|
2104
|
+
currentActiveWorkspace() {
|
|
2105
|
+
const workspace = this.store.getWorkspaceByGitWorktreePath(this.git.repository_root);
|
|
2106
|
+
if (!workspace || workspace.status !== 'active') {
|
|
2107
|
+
throw new Error('The current Git worktree is not an active workspace.');
|
|
2108
|
+
}
|
|
2109
|
+
return workspace;
|
|
2110
|
+
}
|
|
2111
|
+
commitRoot(commit_id) {
|
|
2112
|
+
return this.commits.root(commit_id);
|
|
2113
|
+
}
|
|
2114
|
+
async readCommitGraph(commit_id) {
|
|
2115
|
+
return this.commits.readGraph(commit_id);
|
|
2116
|
+
}
|
|
2117
|
+
async assertProjectionClean(workspace) {
|
|
2118
|
+
const status = await this.getWorkspaceStatus(workspace.id);
|
|
2119
|
+
if (status.projection_dirty) {
|
|
2120
|
+
throw new Error('Workspace projection has uncommitted changes; commit them before this operation.');
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
//# sourceMappingURL=workspace-runtime.js.map
|