@oscharko-dev/keiko-tools 0.2.7 → 0.2.9

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.
Files changed (50) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/git-commit-intent-node.d.ts +7 -0
  3. package/dist/git-commit-intent-node.d.ts.map +1 -0
  4. package/dist/git-commit-intent-node.js +42 -0
  5. package/dist/git-merge-gateway.d.ts +95 -0
  6. package/dist/git-merge-gateway.d.ts.map +1 -0
  7. package/dist/git-merge-gateway.js +536 -0
  8. package/dist/git-merge-node.d.ts +17 -0
  9. package/dist/git-merge-node.d.ts.map +1 -0
  10. package/dist/git-merge-node.js +243 -0
  11. package/dist/git-mutation-adapter.d.ts +51 -0
  12. package/dist/git-mutation-adapter.d.ts.map +1 -0
  13. package/dist/git-mutation-adapter.js +207 -0
  14. package/dist/git-mutation-evidence.d.ts +23 -0
  15. package/dist/git-mutation-evidence.d.ts.map +1 -0
  16. package/dist/git-mutation-evidence.js +283 -0
  17. package/dist/git-mutation-node.d.ts +22 -0
  18. package/dist/git-mutation-node.d.ts.map +1 -0
  19. package/dist/git-mutation-node.js +145 -0
  20. package/dist/git-mutation-orchestrator.d.ts +92 -0
  21. package/dist/git-mutation-orchestrator.d.ts.map +1 -0
  22. package/dist/git-mutation-orchestrator.js +321 -0
  23. package/dist/git-mutation-preflight.d.ts +35 -0
  24. package/dist/git-mutation-preflight.d.ts.map +1 -0
  25. package/dist/git-mutation-preflight.js +226 -0
  26. package/dist/git-mutation-taxonomy.d.ts +15 -0
  27. package/dist/git-mutation-taxonomy.d.ts.map +1 -0
  28. package/dist/git-mutation-taxonomy.js +102 -0
  29. package/dist/git-pr-gateway.d.ts +101 -0
  30. package/dist/git-pr-gateway.d.ts.map +1 -0
  31. package/dist/git-pr-gateway.js +487 -0
  32. package/dist/git-pr-node.d.ts +17 -0
  33. package/dist/git-pr-node.d.ts.map +1 -0
  34. package/dist/git-pr-node.js +173 -0
  35. package/dist/git-publish-gateway.d.ts +72 -0
  36. package/dist/git-publish-gateway.d.ts.map +1 -0
  37. package/dist/git-publish-gateway.js +423 -0
  38. package/dist/git-publish-node.d.ts +17 -0
  39. package/dist/git-publish-node.d.ts.map +1 -0
  40. package/dist/git-publish-node.js +107 -0
  41. package/dist/git-worktree-adapter.d.ts +78 -0
  42. package/dist/git-worktree-adapter.d.ts.map +1 -0
  43. package/dist/git-worktree-adapter.js +300 -0
  44. package/dist/git-worktree-snapshot-node.d.ts +31 -0
  45. package/dist/git-worktree-snapshot-node.d.ts.map +1 -0
  46. package/dist/git-worktree-snapshot-node.js +189 -0
  47. package/dist/index.d.ts +9 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +37 -0
  50. package/package.json +9 -5
@@ -0,0 +1,283 @@
1
+ // Governed Git mutation EVIDENCE builder (Issue #474, Epic #470). Projects the #472 kernel's
2
+ // GitMutationLifecycleResult into the content-free, redacted-by-construction GitDeliveryEvidenceRecord
3
+ // the keiko-contracts evidence atom defines. This is the producer the orchestrator's idempotency
4
+ // journal comment anticipates ("a durable journal — the evidence ledger, #474"), except it records
5
+ // EVERY terminal outcome (succeeded, blocked, rejected, failed, recovery-required, approval-required),
6
+ // not only succeeded retries.
7
+ //
8
+ // PURE: a deterministic function of (lifecycle result, content-free snapshot, correlation, injected
9
+ // deps). No IO, no clock, no randomness — the clock and the hash are injected. Hashing uses the
10
+ // shared keiko-security sha256Hex so remote identifiers, the provider external id, and the repository
11
+ // identity become opaque hashes (AC2) rather than raw artifacts. Local branch names are retained as
12
+ // content-free repository context, consistent with the #473 action-sheet projection.
13
+ //
14
+ // The kernel's lifecycle-phase and outcome vocabularies are mapped onto the contract's self-contained
15
+ // mirrors with exhaustive switches/tables, so a divergence between the kernel and the contract is a
16
+ // compile error here rather than a silent audit gap.
17
+ import { GIT_DELIVERY_EVIDENCE_SCHEMA_VERSION, GIT_DELIVERY_RISK_CLASS_SEVERITY, gitDeliveryRecoveryDispositionForBlockReason, gitDeliveryRecoveryDispositionForExecutionError, gitDeliveryRiskClassForInputs, } from "@oscharko-dev/keiko-contracts";
18
+ import { sha256Hex } from "@oscharko-dev/keiko-security";
19
+ // ─── Phase + outcome-class mapping (exhaustive) ────────────────────────────────────────────────
20
+ function mapLifecyclePhase(phase) {
21
+ switch (phase) {
22
+ case "resolve":
23
+ return "resolve";
24
+ case "preflight":
25
+ return "preflight";
26
+ case "preview":
27
+ return "preview";
28
+ case "policy":
29
+ return "policy";
30
+ case "execute":
31
+ return "execute";
32
+ case "result":
33
+ return "result";
34
+ default:
35
+ return assertNever(phase);
36
+ }
37
+ }
38
+ function outcomeClassFor(outcome) {
39
+ switch (outcome.status) {
40
+ case "succeeded":
41
+ return "succeeded";
42
+ case "approval-required":
43
+ return "approval-required";
44
+ case "blocked":
45
+ return "blocked";
46
+ case "recovery-required":
47
+ return "recovery-required";
48
+ case "failed":
49
+ // A provider rejection is a "rejected" attempt; every other failure (network, timeout,
50
+ // internal) is a transient "failed" attempt.
51
+ return outcome.executionResult.errorCode === "provider-rejected" ? "rejected" : "failed";
52
+ default:
53
+ return assertNever(outcome);
54
+ }
55
+ }
56
+ // ─── Recovery-metadata derivation (AC3) ─────────────────────────────────────────────────────────
57
+ // Action hints REUSE the #473 GitDeliveryRecoveryActionHint vocabulary. Every table is exhaustive
58
+ // over its closed contract/kernel vocabulary, so a new code forces an explicit classification here.
59
+ const ACTION_HINT_BY_BLOCK_REASON = {
60
+ "policy-pack-blocked": "adjust-policy-target",
61
+ "protected-branch": "adjust-policy-target",
62
+ "provider-capability-absent": "adjust-policy-target",
63
+ "approval-expired": "request-approval",
64
+ "risk-class-ceiling": "adjust-policy-target",
65
+ "no-applicable-rule": "adjust-policy-target",
66
+ };
67
+ const ACTION_HINT_BY_EXECUTION_ERROR = {
68
+ // A provider rejection is most often a diverged/non-fast-forward ref; resolving the divergence is
69
+ // the operator's fix. The provider execution slices (#477/#478) may refine this.
70
+ "provider-rejected": "resolve-conflicts",
71
+ "network-failure": "wait-for-provider",
72
+ conflict: "resolve-conflicts",
73
+ "precondition-failed": "resolve-conflicts",
74
+ timeout: "retry",
75
+ "internal-error": "retry",
76
+ };
77
+ const ACTION_HINT_BY_PREFLIGHT_FINDING = {
78
+ "detached-head": "recover-via-strategy",
79
+ "branch-already-exists": "retry",
80
+ "base-branch-missing": "retry",
81
+ "switch-target-missing": "retry",
82
+ "no-changes-to-stage": "stage-changes",
83
+ "nothing-staged-to-unstage": "stage-changes",
84
+ "nothing-staged-to-commit": "stage-changes",
85
+ "untracked-files-impacted": "stage-changes",
86
+ "no-upstream-configured": "configure-upstream",
87
+ "nothing-to-push": "retry",
88
+ "non-fast-forward": "resolve-conflicts",
89
+ "remote-alias-missing": "configure-upstream",
90
+ "remote-unreachable": "wait-for-provider",
91
+ "operation-in-progress": "abort-in-progress-operation",
92
+ "no-operation-to-abort": "retry",
93
+ "recovery-target-unset": "recover-via-strategy",
94
+ "dirty-worktree-impacts-recovery": "recover-via-strategy",
95
+ };
96
+ function recoveryForBlockReason(reason) {
97
+ return {
98
+ disposition: gitDeliveryRecoveryDispositionForBlockReason(reason),
99
+ actionHint: ACTION_HINT_BY_BLOCK_REASON[reason],
100
+ blockReason: reason,
101
+ };
102
+ }
103
+ function recoveryForPreflight(findings) {
104
+ // A blocked-by-preflight attempt is user-fixable: the operator changes the named repository
105
+ // condition. The dominant (first, deterministic order) blocking finding names the action.
106
+ const code = findings[0]?.code;
107
+ return {
108
+ disposition: "user-fixable",
109
+ ...(code !== undefined ? { actionHint: ACTION_HINT_BY_PREFLIGHT_FINDING[code] } : {}),
110
+ };
111
+ }
112
+ function recoveryForExecution(result) {
113
+ const code = result.errorCode ?? "internal-error";
114
+ return {
115
+ disposition: gitDeliveryRecoveryDispositionForExecutionError(code),
116
+ actionHint: ACTION_HINT_BY_EXECUTION_ERROR[code],
117
+ ...(result.errorCode !== undefined ? { executionErrorCode: result.errorCode } : {}),
118
+ };
119
+ }
120
+ function recoveryForRecoveryRequired(result, inputs) {
121
+ const strategy = inputs.kind === "recovery" ? inputs.recoveryStrategyHint : undefined;
122
+ return {
123
+ disposition: "user-fixable",
124
+ actionHint: "recover-via-strategy",
125
+ ...(result.errorCode !== undefined ? { executionErrorCode: result.errorCode } : {}),
126
+ ...(strategy !== undefined ? { suggestedRecoveryStrategy: strategy } : {}),
127
+ };
128
+ }
129
+ function recoveryFor(outcome, inputs) {
130
+ switch (outcome.status) {
131
+ case "succeeded":
132
+ return { disposition: "none" };
133
+ case "approval-required":
134
+ return { disposition: "user-fixable", actionHint: "request-approval" };
135
+ case "blocked":
136
+ return outcome.category === "policy-block"
137
+ ? recoveryForBlockReason(outcome.blockReason)
138
+ : recoveryForPreflight(outcome.findings);
139
+ case "failed":
140
+ return recoveryForExecution(outcome.executionResult);
141
+ case "recovery-required":
142
+ return recoveryForRecoveryRequired(outcome.executionResult, inputs);
143
+ default:
144
+ return assertNever(outcome);
145
+ }
146
+ }
147
+ // ─── Section projections ─────────────────────────────────────────────────────────────────────
148
+ // Branch names are echoed raw into the audit record (they are git refs, not secrets). Strip
149
+ // bidirectional/zero-width/BOM format characters so a crafted ref cannot visually spoof which branch
150
+ // an audit row refers to when the export is rendered. Mirrors the action-sheet route's boundary
151
+ // scanner; git ref rules already bar C0/C1 controls and spaces, so only the format-char set is removed.
152
+ const UNSAFE_FORMAT_CHARS = new RegExp("[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF]", "gu");
153
+ function sanitizeRef(value) {
154
+ return value.replace(UNSAFE_FORMAT_CHARS, "");
155
+ }
156
+ function approvalFor(approval) {
157
+ if (!approval.required) {
158
+ return { required: false };
159
+ }
160
+ return {
161
+ required: true,
162
+ approvalTokenHash: approval.approvalTokenHash,
163
+ approvedByUserId: approval.approvedByUserId,
164
+ approvedAtMs: approval.approvedAtMs,
165
+ ...(approval.expiresAtMs !== undefined ? { expiresAtMs: approval.expiresAtMs } : {}),
166
+ };
167
+ }
168
+ function previewFor(preview) {
169
+ if (preview === undefined) {
170
+ return undefined;
171
+ }
172
+ return {
173
+ ...(preview.affectedBranchName !== undefined
174
+ ? { affectedBranchName: sanitizeRef(preview.affectedBranchName) }
175
+ : {}),
176
+ ...(preview.estimatedFileCount !== undefined
177
+ ? { estimatedFileCount: preview.estimatedFileCount }
178
+ : {}),
179
+ ...(preview.estimatedBytesDelta !== undefined
180
+ ? { estimatedBytesDelta: preview.estimatedBytesDelta }
181
+ : {}),
182
+ wouldCreateRemoteBranch: preview.wouldCreateRemoteBranch,
183
+ wouldTriggerChecks: preview.wouldTriggerChecks,
184
+ };
185
+ }
186
+ function executionFor(result, hash) {
187
+ if (result === undefined) {
188
+ return undefined;
189
+ }
190
+ return {
191
+ outcome: result.outcome,
192
+ durationMs: result.durationMs,
193
+ ...(result.errorCode !== undefined ? { errorCode: result.errorCode } : {}),
194
+ ...(result.partialDetail !== undefined
195
+ ? {
196
+ attemptedUnitCount: result.partialDetail.attemptedUnitCount,
197
+ succeededUnitCount: result.partialDetail.succeededUnitCount,
198
+ }
199
+ : {}),
200
+ ...(result.externalId !== undefined ? { externalIdHash: hash(result.externalId) } : {}),
201
+ };
202
+ }
203
+ function repoContextFor(inputs, snapshot, repoId, hash) {
204
+ const target = inputs.kind === "branch-create" ? inputs.branchName : snapshot.currentBranchName;
205
+ return {
206
+ ...(repoId !== undefined ? { repoIdHash: hash(repoId) } : {}),
207
+ ...(target !== undefined ? { targetBranchName: sanitizeRef(target) } : {}),
208
+ headDetached: snapshot.headDetached,
209
+ stagedFileCount: snapshot.stagedFileCount,
210
+ unstagedFileCount: snapshot.unstagedFileCount,
211
+ untrackedFileCount: snapshot.untrackedFileCount,
212
+ ...(inputs.kind === "push"
213
+ ? { remoteRefHash: hash(`${inputs.remoteAlias}/${inputs.remoteBranchName}`) }
214
+ : {}),
215
+ };
216
+ }
217
+ function effectiveBlockReason(envelope, outcome) {
218
+ if (outcome.status === "blocked" && outcome.category === "policy-block") {
219
+ return outcome.blockReason;
220
+ }
221
+ // The kernel evaluates policy eagerly (prepareLifecycle) even when preflight halts the lifecycle
222
+ // first, so a preflight-blocked envelope can still carry a "blocked" policyDecision that was never
223
+ // the terminal enforcement. Recording that policy reason here would contradict the preflight
224
+ // disposition, so a preflight-blocked attempt (the only remaining "blocked" category) carries no
225
+ // policy block reason — its cause is a preflight finding, reflected in recovery.actionHint and
226
+ // phaseReached === "preflight".
227
+ if (outcome.status === "blocked") {
228
+ return undefined;
229
+ }
230
+ return envelope.policyDecision.outcome === "blocked" ? envelope.policyDecision.reason : undefined;
231
+ }
232
+ function effectiveApprovers(envelope, outcome) {
233
+ if (envelope.policyDecision.outcome === "approval-gated") {
234
+ return envelope.policyDecision.requiredApprovers;
235
+ }
236
+ return outcome.status === "approval-required" ? outcome.requiredApprovers : undefined;
237
+ }
238
+ function policyFieldsFor(envelope, outcome) {
239
+ const blockReason = effectiveBlockReason(envelope, outcome);
240
+ const requiredApprovers = effectiveApprovers(envelope, outcome);
241
+ return {
242
+ ...(blockReason !== undefined ? { blockReason } : {}),
243
+ ...(requiredApprovers !== undefined ? { requiredApprovers } : {}),
244
+ };
245
+ }
246
+ // ─── Entry point ─────────────────────────────────────────────────────────────────────────────
247
+ export function buildGitDeliveryEvidenceRecord(input, deps) {
248
+ const hash = deps.hash ?? sha256Hex;
249
+ const { envelope, outcome, phaseReached } = input.result;
250
+ const inputs = envelope.resolvedInputs;
251
+ const riskClass = gitDeliveryRiskClassForInputs(inputs);
252
+ const workflowRunIdHash = hash(input.workflowRunId);
253
+ const evidenceId = `gde-${hash(`${workflowRunIdHash}:${envelope.actionId}`).slice(0, 40)}`;
254
+ const correlation = {
255
+ workflowRunIdHash,
256
+ actionId: envelope.actionId,
257
+ ...(input.attemptSequence !== undefined ? { attemptSequence: input.attemptSequence } : {}),
258
+ };
259
+ const preview = previewFor(envelope.preview);
260
+ const execution = executionFor(envelope.executionResult, hash);
261
+ return {
262
+ schemaVersion: GIT_DELIVERY_EVIDENCE_SCHEMA_VERSION,
263
+ evidenceId,
264
+ actionKind: envelope.kind,
265
+ riskClass,
266
+ riskSeverity: GIT_DELIVERY_RISK_CLASS_SEVERITY[riskClass],
267
+ outcomeClass: outcomeClassFor(outcome),
268
+ phaseReached: mapLifecyclePhase(phaseReached),
269
+ policyOutcome: envelope.policyDecision.outcome,
270
+ ...policyFieldsFor(envelope, outcome),
271
+ correlation,
272
+ approval: approvalFor(envelope.approvalRequirement),
273
+ ...(preview !== undefined ? { preview } : {}),
274
+ ...(execution !== undefined ? { execution } : {}),
275
+ repoContext: repoContextFor(inputs, input.snapshot, input.repoId, hash),
276
+ recovery: recoveryFor(outcome, inputs),
277
+ recordedAtMs: deps.now(),
278
+ ...(input.evidenceRef !== undefined ? { evidenceRef: input.evidenceRef } : {}),
279
+ };
280
+ }
281
+ function assertNever(value) {
282
+ throw new Error(`unhandled git mutation evidence variant: ${JSON.stringify(value)}`);
283
+ }
@@ -0,0 +1,22 @@
1
+ import type { WorkspaceInfo } from "@oscharko-dev/keiko-workspace";
2
+ import { type GitLocalMutationAdapter } from "./git-mutation-adapter.js";
3
+ import { type ExecutableResolver, type HomeProvider, type SpawnFn } from "./exec.js";
4
+ import { type SandboxPolicy } from "./types.js";
5
+ export interface NodeGitMutationAdapterDeps {
6
+ readonly workspace: WorkspaceInfo;
7
+ readonly processEnv?: NodeJS.ProcessEnv | undefined;
8
+ readonly now?: (() => number) | undefined;
9
+ readonly spawn?: SpawnFn | undefined;
10
+ readonly policy?: SandboxPolicy | undefined;
11
+ readonly resolveExecutable?: ExecutableResolver | undefined;
12
+ readonly home?: HomeProvider | undefined;
13
+ readonly signal?: AbortSignal | undefined;
14
+ readonly timeoutMs?: number | undefined;
15
+ }
16
+ export declare function createNodeGitMutationAdapter(deps: NodeGitMutationAdapterDeps): GitLocalMutationAdapter;
17
+ export { GIT_WORKTREE_READ_COMMAND_RULES, GitWorktreeReadError, readGitWorktreeSnapshot, readStagedPaths, type NodeGitWorktreeReaderDeps, } from "./git-worktree-snapshot-node.js";
18
+ export { buildAddExistingBranchArgv, buildAddWorktreeArgv, buildListWorktreesArgv, buildLocalBranchExistsArgv, buildPruneWorktreesArgv, buildRefResolvesArgv, buildRemoveWorktreeArgv, buildShowToplevelArgv, buildWorktreeStatusArgv, createNodeGitWorktreeAdapter, GIT_WORKTREE_COMMAND_RULES, GitWorktreeOperandError, GitWorktreeSpawnError, isSafeGitRefName, isSafeWorktreePathOperand, parseWorktreeListPorcelain, type AddExistingBranchOperands, type AddWorktreeOperands, type GitWorktreeAdapter, type NodeGitWorktreeAdapterDeps, type RemoveWorktreeOperands, type WorktreeListEntry, type WorktreeOperationResult, type WorktreeStatusResult, } from "./git-worktree-adapter.js";
19
+ export { createNodeGitPublishAdapter, type NodeGitPublishAdapterDeps } from "./git-publish-node.js";
20
+ export { createNodeGitPullRequestAdapter, type NodeGitPullRequestAdapterDeps, } from "./git-pr-node.js";
21
+ export { createNodeGitMergeAdapter, type NodeGitMergeAdapterDeps } from "./git-merge-node.js";
22
+ //# sourceMappingURL=git-mutation-node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-mutation-node.d.ts","sourceRoot":"","sources":["../src/git-mutation-node.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAKnE,OAAO,EASL,KAAK,uBAAuB,EAE7B,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,YAAY,EAEjB,KAAK,OAAO,EACb,MAAM,WAAW,CAAC;AACnB,OAAO,EAA8C,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5F,MAAM,WAAW,0BAA0B;IAEzC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,UAAU,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAErC,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,iBAAiB,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;IAC5D,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IAEzC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AA4HD,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,0BAA0B,GAC/B,uBAAuB,CAWzB;AAKD,OAAO,EACL,+BAA+B,EAC/B,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,KAAK,yBAAyB,GAC/B,MAAM,iCAAiC,CAAC;AAQzC,OAAO,EACL,0BAA0B,EAC1B,oBAAoB,EACpB,sBAAsB,EACtB,0BAA0B,EAC1B,uBAAuB,EACvB,oBAAoB,EACpB,uBAAuB,EACvB,qBAAqB,EACrB,uBAAuB,EACvB,4BAA4B,EAC5B,0BAA0B,EAC1B,uBAAuB,EACvB,qBAAqB,EACrB,gBAAgB,EAChB,yBAAyB,EACzB,0BAA0B,EAC1B,KAAK,yBAAyB,EAC9B,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,GAC1B,MAAM,2BAA2B,CAAC;AAKnC,OAAO,EAAE,2BAA2B,EAAE,KAAK,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAMpG,OAAO,EACL,+BAA+B,EAC/B,KAAK,6BAA6B,GACnC,MAAM,kBAAkB,CAAC;AAM1B,OAAO,EAAE,yBAAyB,EAAE,KAAK,uBAAuB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,145 @@
1
+ // Node implementation of the narrow local Git mutation adapter (Issue #472, Epic #470) — AC3.
2
+ //
3
+ // This is the ONLY place governed local Git writes actually execute. Each typed adapter method
4
+ // builds a fixed argv plan from the pure builders (git-mutation-adapter.ts) and runs it through the
5
+ // single keiko-tools no-shell spawn boundary (`runCommand`, exec.ts) with a dedicated mutation
6
+ // allowlist. There is no method that accepts an arbitrary command string, and no parallel
7
+ // child_process path: the deny-by-default allowlist, env isolation, redaction, and cancellation of
8
+ // the shared boundary apply to every governed mutation exactly as they do to every other tool.
9
+ //
10
+ // Lives on the `./internal/git-mutation` subpath (mirroring `./internal/exec` and `./internal/writer`)
11
+ // because it carries the Node execution effect; the pure port, builders, and rules it implements are
12
+ // re-exported from the package's main barrel.
13
+ import { GIT_DELIVERY_SCHEMA_VERSION, } from "@oscharko-dev/keiko-contracts";
14
+ import { buildAbortArgv, buildBranchCreateArgv, buildBranchSwitchArgv, buildCommitArgv, buildRecoveryArgv, buildStageArgv, buildUnstageArgv, GIT_MUTATION_COMMAND_RULES, } from "./git-mutation-adapter.js";
15
+ import { CommandCancelledError, CommandTimeoutError } from "./errors.js";
16
+ import { nodeSpawnFn, runCommand, } from "./exec.js";
17
+ import { DEFAULT_SANDBOX_POLICY } from "./types.js";
18
+ function executionResult(outcome, durationMs, extra) {
19
+ return {
20
+ schemaVersion: GIT_DELIVERY_SCHEMA_VERSION,
21
+ outcome,
22
+ durationMs: Math.max(0, Math.trunc(durationMs)),
23
+ ...extra,
24
+ };
25
+ }
26
+ // A non-zero git exit at execution time means a precondition that the preflight snapshot did not
27
+ // capture failed against the live repository (a time-of-check/time-of-use gap) — classified as
28
+ // `precondition-failed`, which the taxonomy routes to recovery-required. When part of a multi-step
29
+ // plan already partially applied, the result is `partial` with the attempted/succeeded counts.
30
+ function failureFromExit(durationMs, stepIndex) {
31
+ if (stepIndex > 0) {
32
+ return executionResult("partial", durationMs, {
33
+ errorCode: "precondition-failed",
34
+ partialDetail: { attemptedUnitCount: stepIndex + 1, succeededUnitCount: stepIndex },
35
+ });
36
+ }
37
+ return executionResult("failed", durationMs, { errorCode: "precondition-failed" });
38
+ }
39
+ function failureFromThrow(error, durationMs, stepIndex) {
40
+ const partial = stepIndex > 0
41
+ ? { partialDetail: { attemptedUnitCount: stepIndex + 1, succeededUnitCount: stepIndex } }
42
+ : {};
43
+ if (error instanceof CommandTimeoutError) {
44
+ return executionResult(stepIndex > 0 ? "partial" : "failed", durationMs, {
45
+ errorCode: "timeout",
46
+ ...partial,
47
+ });
48
+ }
49
+ if (error instanceof CommandCancelledError) {
50
+ return executionResult("aborted", durationMs, partial);
51
+ }
52
+ // A denied command (our own argv hit the allowlist), an argv-construction fault, or any other
53
+ // throw is an internal kernel error — it never means the user's repository is at fault.
54
+ return executionResult(stepIndex > 0 ? "partial" : "failed", durationMs, {
55
+ errorCode: "internal-error",
56
+ ...partial,
57
+ });
58
+ }
59
+ function runOne(ctx, argv) {
60
+ return runCommand({ command: "git", args: argv, cwd: undefined, timeoutMs: ctx.timeoutMs, signal: ctx.signal }, ctx.runDeps);
61
+ }
62
+ async function runPlan(ctx, plan) {
63
+ let totalDuration = 0;
64
+ for (const [stepIndex, argv] of plan.entries()) {
65
+ let result;
66
+ try {
67
+ result = await runOne(ctx, argv);
68
+ }
69
+ catch (error) {
70
+ return failureFromThrow(error, totalDuration, stepIndex);
71
+ }
72
+ totalDuration += result.durationMs;
73
+ if (result.exitCode !== 0) {
74
+ return failureFromExit(totalDuration, stepIndex);
75
+ }
76
+ }
77
+ return executionResult("succeeded", totalDuration);
78
+ }
79
+ // Builds the argv plan, then runs it. A builder throw (invalid operand) is an internal error that
80
+ // never reaches a spawn.
81
+ async function execPlan(ctx, build) {
82
+ let plan;
83
+ try {
84
+ plan = build();
85
+ }
86
+ catch {
87
+ return executionResult("failed", 0, { errorCode: "internal-error" });
88
+ }
89
+ return runPlan(ctx, plan);
90
+ }
91
+ function buildRunContext(deps) {
92
+ return {
93
+ runDeps: {
94
+ workspace: deps.workspace,
95
+ policy: deps.policy ?? DEFAULT_SANDBOX_POLICY,
96
+ commandRules: GIT_MUTATION_COMMAND_RULES,
97
+ spawn: deps.spawn ?? nodeSpawnFn,
98
+ processEnv: deps.processEnv ?? process.env,
99
+ now: deps.now ?? Date.now,
100
+ ...(deps.resolveExecutable !== undefined
101
+ ? { resolveExecutable: deps.resolveExecutable }
102
+ : {}),
103
+ ...(deps.home !== undefined ? { home: deps.home } : {}),
104
+ },
105
+ signal: deps.signal ?? new AbortController().signal,
106
+ timeoutMs: deps.timeoutMs,
107
+ };
108
+ }
109
+ export function createNodeGitMutationAdapter(deps) {
110
+ const ctx = buildRunContext(deps);
111
+ return {
112
+ createBranch: (req) => execPlan(ctx, () => buildBranchCreateArgv(req)),
113
+ switchBranch: (req) => execPlan(ctx, () => buildBranchSwitchArgv(req)),
114
+ stage: (req) => execPlan(ctx, () => buildStageArgv(req)),
115
+ unstage: (req) => execPlan(ctx, () => buildUnstageArgv(req)),
116
+ commit: (req) => execPlan(ctx, () => buildCommitArgv(req)),
117
+ abort: (req) => execPlan(ctx, () => buildAbortArgv(req)),
118
+ recover: (req) => execPlan(ctx, () => buildRecoveryArgv(req)),
119
+ };
120
+ }
121
+ // The read-only worktree snapshot reader (Issue #475) carries the same Node spawn effect as this
122
+ // adapter, so it is exposed on the SAME `./internal/git-mutation` subpath rather than the pure barrel.
123
+ // Its inspection allowlist is structurally separate from the mutation rules.
124
+ export { GIT_WORKTREE_READ_COMMAND_RULES, GitWorktreeReadError, readGitWorktreeSnapshot, readStagedPaths, } from "./git-worktree-snapshot-node.js";
125
+ // The narrow managed-worktree lifecycle adapter (Issue #445, Epic #443) carries the same Node spawn
126
+ // effect through the same governed `runCommand` boundary with its OWN dedicated allowlist
127
+ // (`GIT_WORKTREE_COMMAND_RULES`: worktree/rev-parse/show-ref only — structurally separate from the
128
+ // mutation, read-inspection, publish, PR, and merge rule sets). It is exposed on the SAME
129
+ // `./internal/git-mutation` subpath. The pure argv builders, operand validators, and porcelain parser
130
+ // are re-exported alongside the node factory so the server can pre-validate operands without spawning.
131
+ export { buildAddExistingBranchArgv, buildAddWorktreeArgv, buildListWorktreesArgv, buildLocalBranchExistsArgv, buildPruneWorktreesArgv, buildRefResolvesArgv, buildRemoveWorktreeArgv, buildShowToplevelArgv, buildWorktreeStatusArgv, createNodeGitWorktreeAdapter, GIT_WORKTREE_COMMAND_RULES, GitWorktreeOperandError, GitWorktreeSpawnError, isSafeGitRefName, isSafeWorktreePathOperand, parseWorktreeListPorcelain, } from "./git-worktree-adapter.js";
132
+ // The Node remote publish executor (Issue #476) carries the same Node spawn effect and a DEDICATED
133
+ // push allowlist; it is exposed on the SAME `./internal/git-mutation` subpath. Its allowlist is
134
+ // structurally separate from both the mutation and the read-only inspection rules.
135
+ export { createNodeGitPublishAdapter } from "./git-publish-node.js";
136
+ // The Node GitHub pull request executor (Issue #477) shells `gh api` through the same spawn boundary
137
+ // with its OWN dedicated PR allowlist (create / update / draft-toggle GraphQL — no merge, no delete);
138
+ // it is exposed on the SAME `./internal/git-mutation` subpath. The GitHub token is read by gh itself,
139
+ // never by Keiko.
140
+ export { createNodeGitPullRequestAdapter, } from "./git-pr-node.js";
141
+ // The Node governed merge executor (Issue #478) shells `gh api` through the same spawn boundary with its
142
+ // OWN dedicated merge allowlist (the merge PUT, the readiness GETs, and the guarded branch DELETE — no
143
+ // generic exec); it is exposed on the SAME `./internal/git-mutation` subpath. The GitHub token is read by
144
+ // gh itself, never by Keiko.
145
+ export { createNodeGitMergeAdapter } from "./git-merge-node.js";
@@ -0,0 +1,92 @@
1
+ import type { GitDeliveryAbortableOperation, GitDeliveryActionEnvelope, GitDeliveryApprovalRequirement, GitDeliveryBlockReason, GitDeliveryExecutionResult, GitDeliveryOrgPolicyPack, GitDeliveryProviderCapability, GitDeliveryRecoveryStrategyHint, GitDeliveryRepoPolicyPack } from "@oscharko-dev/keiko-contracts";
2
+ import type { GitLocalMutationAdapter } from "./git-mutation-adapter.js";
3
+ import type { GitPreflightFinding, GitPreflightReport, GitWorktreeSnapshot } from "./git-mutation-preflight.js";
4
+ import type { GitMutationFailureCategory, GitMutationLifecyclePhase } from "./git-mutation-taxonomy.js";
5
+ export interface GitBranchCreateCommand {
6
+ readonly kind: "branch-create";
7
+ readonly branchName: string;
8
+ readonly baseBranchName: string;
9
+ readonly startPointRefHash: string;
10
+ }
11
+ export interface GitBranchSwitchCommand {
12
+ readonly kind: "branch-switch";
13
+ readonly branchName: string;
14
+ }
15
+ export interface GitStageCommand {
16
+ readonly kind: "stage";
17
+ readonly pathspecs: readonly string[];
18
+ readonly includeUntracked: boolean;
19
+ }
20
+ export interface GitUnstageCommand {
21
+ readonly kind: "unstage";
22
+ readonly pathspecs: readonly string[];
23
+ }
24
+ export interface GitCommitCommand {
25
+ readonly kind: "commit";
26
+ readonly message: string;
27
+ readonly allowEmpty: boolean;
28
+ }
29
+ export interface GitAbortCommand {
30
+ readonly kind: "abort";
31
+ readonly operationToAbort: GitDeliveryAbortableOperation;
32
+ readonly preserveIndexChanges: boolean;
33
+ }
34
+ export interface GitRecoveryCommand {
35
+ readonly kind: "recovery";
36
+ readonly recoveryStrategyHint: GitDeliveryRecoveryStrategyHint;
37
+ readonly targetRefHash: string;
38
+ readonly affectedPathspecs: readonly string[];
39
+ }
40
+ export type GitMutationCommand = GitBranchCreateCommand | GitBranchSwitchCommand | GitStageCommand | GitUnstageCommand | GitCommitCommand | GitAbortCommand | GitRecoveryCommand;
41
+ export interface GitMutationRequest {
42
+ readonly command: GitMutationCommand;
43
+ readonly approval: GitDeliveryApprovalRequirement;
44
+ readonly idempotencyKey?: string | undefined;
45
+ }
46
+ export interface GitMutationJournal {
47
+ lookup(idempotencyKey: string): GitMutationLifecycleResult | undefined;
48
+ record(idempotencyKey: string, result: GitMutationLifecycleResult): void;
49
+ }
50
+ export interface GitMutationOrchestratorDeps {
51
+ readonly adapter: GitLocalMutationAdapter;
52
+ readonly snapshot: GitWorktreeSnapshot;
53
+ readonly orgPolicyPack?: GitDeliveryOrgPolicyPack | undefined;
54
+ readonly repoPolicyPack?: GitDeliveryRepoPolicyPack | undefined;
55
+ readonly activeProviderCapabilities?: readonly GitDeliveryProviderCapability[] | undefined;
56
+ readonly now: () => number;
57
+ readonly newActionId: () => string;
58
+ readonly journal?: GitMutationJournal | undefined;
59
+ }
60
+ export type GitMutationOutcome = {
61
+ readonly status: "succeeded";
62
+ readonly executionResult: GitDeliveryExecutionResult;
63
+ } | {
64
+ readonly status: "approval-required";
65
+ readonly requiredApprovers: readonly string[];
66
+ } | {
67
+ readonly status: "blocked";
68
+ readonly category: "policy-block";
69
+ readonly blockReason: GitDeliveryBlockReason;
70
+ } | {
71
+ readonly status: "blocked";
72
+ readonly category: "preflight-block";
73
+ readonly findings: readonly GitPreflightFinding[];
74
+ } | {
75
+ readonly status: "failed";
76
+ readonly category: "execution-failure" | "provider-failure";
77
+ readonly executionResult: GitDeliveryExecutionResult;
78
+ } | {
79
+ readonly status: "recovery-required";
80
+ readonly category: "recovery-required";
81
+ readonly executionResult: GitDeliveryExecutionResult;
82
+ };
83
+ export interface GitMutationLifecycleResult {
84
+ readonly envelope: GitDeliveryActionEnvelope;
85
+ readonly outcome: GitMutationOutcome;
86
+ readonly phaseReached: GitMutationLifecyclePhase;
87
+ readonly preflight: GitPreflightReport;
88
+ }
89
+ export declare function gitMutationOutcomeFailureCategory(outcome: GitMutationOutcome): GitMutationFailureCategory | undefined;
90
+ export declare function runGitMutation(request: GitMutationRequest, deps: GitMutationOrchestratorDeps): Promise<GitMutationLifecycleResult>;
91
+ export declare function createInMemoryGitMutationJournal(): GitMutationJournal;
92
+ //# sourceMappingURL=git-mutation-orchestrator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-mutation-orchestrator.d.ts","sourceRoot":"","sources":["../src/git-mutation-orchestrator.ts"],"names":[],"mappings":"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"}