@gitkraken/compose-tools 0.8.0 → 0.9.0
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 +28 -0
- package/README.md +8 -4
- package/dist/ai/types.d.ts +46 -1
- package/dist/context.d.ts +66 -0
- package/dist/context.js +1 -0
- package/dist/git/apply-target.js +1 -1
- package/dist/git/commit-chain.d.ts +14 -5
- package/dist/git/commit-chain.js +7 -7
- package/dist/git/diff-collector.d.ts +26 -12
- package/dist/git/diff-collector.js +4 -4
- package/dist/git/index.js +1 -1
- package/dist/git/safety.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -1
- package/dist/tasks/ai-loop.d.ts +10 -1
- package/dist/tasks/ai-loop.js +1 -1
- package/dist/tasks/analyze/analyze.js +1 -1
- package/dist/tasks/analyze/prompt.d.ts +6 -2
- package/dist/tasks/analyze/prompt.js +5 -5
- package/dist/tasks/analyze/types.d.ts +11 -0
- package/dist/tasks/compose-group/compose-group.js +1 -1
- package/dist/tasks/compose-group/prompt.d.ts +6 -1
- package/dist/tasks/compose-group/prompt.js +31 -31
- package/dist/tasks/compose-group/types.d.ts +19 -11
- package/dist/tasks/group/group.js +1 -1
- package/dist/tasks/group/prompt.d.ts +7 -1
- package/dist/tasks/group/prompt.js +21 -21
- package/dist/tasks/group/types.d.ts +22 -1
- package/dist/tasks/group/validator.d.ts +10 -2
- package/dist/tasks/group/validator.js +5 -5
- package/dist/tasks/quick-compose/prompt.d.ts +3 -1
- package/dist/tasks/quick-compose/prompt.js +11 -11
- package/dist/tasks/quick-compose/quick-compose.js +1 -1
- package/dist/tasks/quick-compose/types.d.ts +7 -0
- package/dist/tasks/refine/refine.js +1 -1
- package/dist/tasks/refine/types.d.ts +6 -1
- package/dist/tasks/refine/validator.js +1 -1
- package/dist/tasks/utils/analysis-prompt.d.ts +14 -0
- package/dist/tasks/utils/analysis-prompt.js +5 -0
- package/dist/tasks/utils/authoring-context.d.ts +47 -0
- package/dist/tasks/utils/authoring-context.js +9 -0
- package/dist/tasks/utils/hunk-summaries.d.ts +19 -0
- package/dist/tasks/utils/hunk-summaries.js +5 -0
- package/dist/workflows/author.d.ts +101 -0
- package/dist/workflows/author.js +16 -0
- package/dist/workflows/compose.d.ts +89 -1
- package/dist/workflows/compose.js +3 -3
- package/dist/workflows/refine.js +1 -1
- package/dist/workflows/split.js +2 -2
- package/dist/workflows/types.d.ts +31 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.0] - 2026-10-05
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Adds `AuthoringContext` — a structured, optional description of what a change's author knows that the diff cannot show: intent, issue (url/key/title/body), pull request, spec, notes, per-file edit rationales, ordering dependencies, and conventions. `ComposeContext` now extends it, so every compose entry point accepts the full shape; the flat `issueUrl`/`pullRequestUrl` fields are deprecated in favor of `issue.url`/`pullRequest.url` and remain honored (the structured field wins when both are set). Everything beyond the URLs is rendered into prompts as escaped, per-field-capped, tagged data behind an explicit data-not-instructions notice, so tracker text cannot act as a prompt injection and one verbose issue body cannot crowd the hunks out of the token budget; the URL fields themselves are emitted as plain context lines, forced to a single line and length-capped. Supplying nothing leaves every prompt byte-identical to before. Type exports: `AuthoringContext`, `AuthoringDependency`, `AuthoringEdit`, `AuthoringIssue`, `AuthoringNote`, `AuthoringPullRequest`.
|
|
14
|
+
- Adds `collectComposeHunks` and `buildAuthoredComposePlan`, the two halves of an authored-plan flow in which the caller writes the commit plan and no model is called at any point. `collectComposeHunks` runs the collection phase alone and returns the hunk index map, the safety snapshot, and the resolved source/target — the parts a caller cannot derive itself. `buildAuthoredComposePlan` validates a caller-written commit list against the collected indices (each index claimed exactly once; unknown, duplicate, and unassigned indices are rejected with the offending files named in the error) and shapes it into a `ComposePlan` for `applyComposePlan`. Built for consumers whose planner is the coding agent that made the changes: the agent already holds the full context of its own edits, so the grouping decision can come from it directly instead of from a model prompt carrying the diff. The produced plan is a single branch group — multi-branch splitting stays with the model-driven flow. Type exports: `CollectComposeHunksInput`, `CollectComposeHunksResult`, `AuthoredCommit`, `BuildAuthoredComposePlanInput`.
|
|
15
|
+
- Adds `analysis` and `condenseHunks` to `compose` and `composePlan` — inputs that let a caller who already knows the change inform the composing model instead of paying to have it inferred. `analysis` supplies the breakdown the `analyze` phase exists to derive from the diff; at `depth: 'deep'` that phase is skipped entirely (still reported, with zero attempts) and the grouping model is no longer asked to produce its own. `condenseHunks` mode sends hunk *metadata only* — index, file name, hunk header, line counts — omitting every patch body from the prompts, for the situation where the answering model already has the change in front of it (an agent composing edits it made itself). Supplying neither leaves every prompt byte-identical to before. `analysis` cannot be combined with a resumed `session` — continuation prompts never carry it — and the combination is rejected with `ComposeWorkflowInputError`.
|
|
16
|
+
- Adds `hunkSummaries` — one-line, caller-supplied summaries of what individual hunks do, keyed by hunk index — rendered into the analyze and grouping prompts as an escaped `<hunk-summaries>` block (at `depth: 'deep'` they enter at the analyze phase and the conversation carries them forward). Built for `condenseHunks` mode: with patch bodies absent, headers say where a change is but not what it does, and grouping suffers most where related hunks span files (an export in one file, its import in another). Summaries restore that signal at a fraction of the diff's size, carry no concern structure (so, unlike a supplied `analysis`, they cannot pre-bias the grouping), are capped at 200 characters each, and drop entries for indices that were not collected. Sparse maps are expected — summarize the hunks whose headers don't speak for themselves.
|
|
17
|
+
- Adds `condensed` to `collectComposeHunks` — the returned hunks come back with empty patch bodies (binary payloads dropped), index map only, while collection and the safety snapshot still compute from the real content. For consumers relaying the result to the agent that authored the changes: masking becomes the library's guarantee instead of something every consumer must remember, so the author never pays to read its own diff.
|
|
18
|
+
- Adds `buildAuthoredPlanInstructions` — the agent-facing rules for authoring a plan (the decision JSON shape, the every-index-exactly-once rules, ordering, the hunk-boundary caveat), shipped from the library so consumers embed a function call instead of maintaining prose that can drift from the validator.
|
|
19
|
+
- Adds `SESSION-FLOWS.md` — consumer documentation of the four session interaction flows (detached model informed/uninformed, calling agent as composer for its own or unfamiliar changes), which functions each flow uses, and what the consumer owns (wiring, routing, the collect-to-apply binding).
|
|
20
|
+
|
|
21
|
+
## [0.8.1] - 2026-08-05
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- `commit-range` + `includeWorkdir` now **preserves** a workdir layer it was told not to compose, instead of rejecting the input. A layer excluded via `includeStaged`/`includeUnstaged` is captured as a patch immediately before the apply's worktree reset and re-applied to the worktree afterward — so `{ includeStaged: true, includeUnstaged: false }` composes the staged changes together with the range and leaves the unstaged work sitting in the worktree, which is what those flags read as meaning. This reverses the `ComposeWorkflowInputError` raised for that combination: the rejection was right about the danger (an excluded layer is absent from the collected input, therefore absent from `leftoverPatch`, and unprotected by a safety stash — none is taken when `includeWorkdir` is set — so the reset destroyed it) but resolved it by refusing the operation rather than by protecting the layer. Excluded content survives; its position does not — an excluded staged layer comes back as unstaged changes. Untracked files are unaffected, as before: the reset leaves them alone. Layer exclusion stays deliberately distinct from `hunkFilter` and partial apply, which drop specific changes the caller singled out of the composed input; an excluded layer was never part of that input at all.
|
|
26
|
+
- One `includeWorkdir` combination is still rejected, now narrowly: `includeStaged: false` together with `includeUnstaged: true` while staged changes exist. Unstaged changes are diffed against the index and therefore already presuppose the staged content, so once the two touch the same lines there is no coherent way to replay the excluded staged layer over a result composed from the unstaged one. `ComposeWorkflowInputError` names the fix (include the layer, or commit/stash it). Every other combination now composes.
|
|
27
|
+
- Drift in an excluded layer no longer fails the pre-apply safety check. `verifySnapshot` previously reported `Excluded ... changes appeared since the AI analysis` when a change appeared in an excluded layer between collection and apply; the capture now happens immediately before the reset, so whatever the layer holds at that moment is what gets restored and a late change no longer needs to block the apply.
|
|
28
|
+
- Documented that **every `includeWorkdir` layer defaults to `true`**, in both the `ComposeSource` type and the README. `{ includeStaged: true }` reads like an allowlist but composes the entire working directory, unstaged and untracked changes included; keeping a layer out requires naming it with `false`.
|
|
29
|
+
|
|
9
30
|
## [0.8.0] - 2026-08-03
|
|
10
31
|
|
|
11
32
|
### Added
|
|
@@ -13,6 +34,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this
|
|
|
13
34
|
- Adds `stashNotRestored` on `ComposeResult`, `ApplyComposePlanResult`, and the apply-layer results — reported when the post-apply safety-stash restore was skipped or failed without conflicts (reasons: `stash-ref-not-found`, `untracked-restore-failed`, `pop-failed`). Previously a git-aborted pop and a missing stash ref were indistinguishable from a clean restore (or misreported as a conflict with no files). The stash entry is now left in place in these cases instead of being dropped, so the WIP it holds stays recoverable. Type export: `StashNotRestored`.
|
|
14
35
|
- Adds optional `diffFilter` and `noRenames` fields to `DiffTreeOptions` (mapping to `git diff`'s `--diff-filter` and `--no-renames`), so a consumer-supplied `diffTree` port can honor both. Used internally to compare a stash's untracked payload against the working tree without rename detection folding a missing file into a rename.
|
|
15
36
|
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- A hunk index claimed by more than one commit is now repaired deterministically instead of costing a retry. The response validator for `group` / `composeGroup` / `refinePlan` keeps the index in one commit and drops the other claims, then accepts the response. The winner is the locked claimant when one exists, otherwise the first claim in the model's **response** order — not necessarily first in the final chain, since the `order` phase and `restoreLockedOrdering` can both move it later. Dropping a later claim removes a wrong extra assignment rather than guessing intent — the hunk is still assigned somewhere. Previously the whole prompt was re-sent, which on a large diff meant re-transmitting every hunk to correct one repeated number; over four eval runs duplicate hunks accounted for 12 of 29 outright `VALIDATION_EXHAUSTED` failures. Repairs are reported, never silent: via `repairs` on the `onAfterModelCall` info object, on `GroupOutput`, on `RefineOutput`, on `RefinePlanResult.phases[]`, and on the matching `PhaseDetail` in `ComposeResult.phases`. Type exports: `ValidationRepair`, `DuplicateHunksRepair`, `RepairedHunkClaim`, `RepairScope`. "Reported, never silent" describes this repair, not validator corrections generally: refine's locked-content substitution and `restoreLockedOrdering` remain silent, and one of them rewrites a commit message the user will see. Two cases deliberately fall back to a retry instead of being repaired: claims spanning **two branches in branches mode** (branch chains are built in parallel from a common base there, so splitting adjacent hunks across them risks a merge conflict raised as a raw exec error; in commits mode the branches are never built as parallel chains, so the same contest is repaired like any other), and commits whose every hunk was a duplicate claim (repairing those would silently discard a commit the model intended, and a zero-hunk commit throws `Empty patch for commit` at apply time). Responses that are wrong in any other way still fail exactly as before. Repairing genuinely missing hunks is deliberately not covered, and neither is `quickCompose`, whose structurally identical duplicate check still rejects outright.
|
|
40
|
+
- **Commit messages are not reconciled with a repair.** A commit that loses a hunk keeps prose describing it, and the winner gains a hunk its message never mentions. The index math does not guess intent, but the resulting message can be wrong in the user's permanent history.
|
|
41
|
+
- `onAfterModelCall` now fires after the response validator runs rather than immediately after `model.generate` returns, so it can carry that call's `repairs`. `repairs` is populated only when the response was accepted. Its `durationMs` still measures only the model call, and the hook still fires when the validator throws. An exception thrown by the hook is now swallowed: an observability callback must not be able to abort a compose or mask the validator's own error.
|
|
42
|
+
- `ComposeGroupOutput` is now an alias of `GroupOutput` rather than a hand-written structural clone that had already drifted (it was missing `hunksCondensedCount`, and would have missed `repairs`).
|
|
43
|
+
|
|
16
44
|
### Fixed
|
|
17
45
|
|
|
18
46
|
- Composing with a `hunkFilter` that drops a non-trailing hunk no longer fails validation (or worse, tempts the model into renumbering hunks). The filter contract preserves the surviving hunks' original `index` values, but the prompts and all response validators assumed indices form a dense `0..N-1` range derived from the hunk count — so a correct response referencing the real indices was rejected, retried until `VALIDATION_EXHAUSTED`, and the retry feedback actively steered the model toward renumbering, which would have validated cleanly and resolved to the wrong hunks. Validation (analyze, group / compose-group, quick-compose, refine) now checks against the actual set of surviving hunk indices, prompts instruct the model to use the `index` values exactly as shown, and retry feedback lists the real indices. The fresh `compose()` apply path also now resolves plan indices against the filtered hunk set (matching `applyComposePlan`), so a plan referencing a filter-dropped index fails loudly instead of committing excluded content. Internal `refine` task input changed from `hunkCount: number` to `hunkIndices: ReadonlySet<number>` (not part of the public API).
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
AI-powered git change organization. Takes a messy working directory or a branch full of interleaved commits and restructures them into clean, atomic commits on well-named branches — ready for code review.
|
|
4
4
|
|
|
5
|
+
Using compose from an MCP server or CLI that a coding agent drives? [SESSION-FLOWS.md](./SESSION-FLOWS.md) documents the four supported interaction flows and how to wire each one.
|
|
6
|
+
|
|
5
7
|
## What it does
|
|
6
8
|
|
|
7
9
|
`compose()` collects your changes, sends the diffs through an AI pipeline that understands the logical structure of code changes, and produces clean commits grouped by concern. Depending on configuration, it can:
|
|
@@ -183,7 +185,7 @@ See [Undo](#undo) below.
|
|
|
183
185
|
|---|---|
|
|
184
186
|
| `{ type: 'workdir', stagedOnly?, noUntracked? }` | Collects staged, unstaged, and untracked changes. `stagedOnly: true` limits to the index; `noUntracked: true` skips untracked files. |
|
|
185
187
|
| `{ type: 'branch', name, mergeTarget? }` | Collects `mergeBase(mergeTarget, name) → name` as one combined diff. `mergeTarget` defaults to `'main'`. |
|
|
186
|
-
| `{ type: 'commit-range', branch, from, to, includeWorkdir? }` | Collects a contiguous range on `branch` as a single combined diff. Both `from` and `to` are **inclusive** — they name the first and last commits being rewritten. For a single-commit recompose: `from = sha, to = sha`. Per-hunk authorship is attributed via a line-range-overlap heuristic against per-commit history. Pass `includeWorkdir: { includeStaged?, includeUnstaged?, includeUntracked? }` to combine the range with the user's working changes in one composed input. When set, `to` must equal the current branch tip
|
|
188
|
+
| `{ type: 'commit-range', branch, from, to, includeWorkdir? }` | Collects a contiguous range on `branch` as a single combined diff. Both `from` and `to` are **inclusive** — they name the first and last commits being rewritten. For a single-commit recompose: `from = sha, to = sha`. Per-hunk authorship is attributed via a line-range-overlap heuristic against per-commit history. Pass `includeWorkdir: { includeStaged?, includeUnstaged?, includeUntracked? }` to combine the range with the user's working changes in one composed input; **each layer defaults to `true`**, so `{ includeStaged: true }` composes the whole working directory — name a layer with `false` to keep it out. When set, `to` must equal the current branch tip and HEAD must be non-detached. A layer excluded via `includeStaged`/`includeUnstaged` is preserved rather than composed: its content is captured before the apply resets the worktree and re-applied afterward as unstaged changes. Excluding staged while composing unstaged is rejected when staged changes exist — unstaged changes are diffed against the index, so the excluded staged layer has nowhere coherent to be replayed. |
|
|
187
189
|
|
|
188
190
|
## Targets
|
|
189
191
|
|
|
@@ -259,8 +261,10 @@ onBeforeModelCall: ({ phase, attempt, maxAttempts, callId }) => {
|
|
|
259
261
|
// callId: stable UUID — correlates with onAfterModelCall and with result.feedbackTargetCallId
|
|
260
262
|
},
|
|
261
263
|
|
|
262
|
-
onAfterModelCall: ({ phase, attempt, callId, success, durationMs, usage, error, promptVersion }) => {
|
|
263
|
-
// Fires on both success and error
|
|
264
|
+
onAfterModelCall: ({ phase, attempt, callId, success, durationMs, usage, error, promptVersion, repairs }) => {
|
|
265
|
+
// Fires on both success and error, after the response validator runs (durationMs still
|
|
266
|
+
// measures only the model call). `repairs` lists deterministic fixes applied to an accepted
|
|
267
|
+
// response — see ValidationRepair. An exception thrown here is swallowed.
|
|
264
268
|
},
|
|
265
269
|
```
|
|
266
270
|
|
|
@@ -451,7 +455,7 @@ Every top-level entry point returns an object with these common fields:
|
|
|
451
455
|
| `snapshot` | all paths | Safety hash fingerprint of the collection — carry it to apply time |
|
|
452
456
|
| `source` | all paths | `{ type, hunks, commitMessages?, branchName?, baseBranch? }` — the hunks the AI operated on |
|
|
453
457
|
| `session` | all paths | Pass to a subsequent call to resume |
|
|
454
|
-
| `phases` | all paths | `PhaseDetail[]` with `{ name, durationMs, attempts, usage?, modelId?, promptVersion?, callIds? }` per phase |
|
|
458
|
+
| `phases` | all paths | `PhaseDetail[]` with `{ name, durationMs, attempts, usage?, modelId?, promptVersion?, callIds?, repairs? }` per phase |
|
|
455
459
|
| `usage` | all paths | Aggregated `{ inputTokens, outputTokens, ... }` across all AI calls |
|
|
456
460
|
| `diffStats` | sugar / plan | `{ fileCount, hunkCount, addedLines, removedLines }` |
|
|
457
461
|
| `feedbackTargetCallId` | when AI ran | callId of the last validated model response — stamp this on thumbs up/down feedback events |
|
package/dist/ai/types.d.ts
CHANGED
|
@@ -1,4 +1,44 @@
|
|
|
1
1
|
//#region src/ai/types.d.ts
|
|
2
|
+
/** Which commits contested a hunk index, and which one kept it. */
|
|
3
|
+
interface RepairedHunkClaim {
|
|
4
|
+
hunkIndex: number;
|
|
5
|
+
/** Commit that kept the hunk. `branchId/commitId` in branches mode, bare commit id in commits mode. */
|
|
6
|
+
keptIn: string;
|
|
7
|
+
/** Commits the claim was dropped from, in the order they appeared in the response. */
|
|
8
|
+
droppedFrom: string[];
|
|
9
|
+
/** Where THIS claim's contest lived; sibling claims in one repair can differ. */
|
|
10
|
+
scope: RepairScope;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Where a contested claim came from. Nothing emits `across-branches` today: in branches mode those
|
|
14
|
+
* contests are rejected rather than repaired (no claim to put a scope on), and in commits mode a
|
|
15
|
+
* multi-branch response's contests are `across-commits`. The member exists so a future repair
|
|
16
|
+
* covering the branches-mode case does not have to widen the union.
|
|
17
|
+
*/
|
|
18
|
+
type RepairScope = 'within-commit' | 'across-commits' | 'across-branches';
|
|
19
|
+
/**
|
|
20
|
+
* A hunk index claimed more than once was kept in a single commit and dropped from the others.
|
|
21
|
+
* The winner is the locked claimant when one exists, otherwise the first claim in the model's
|
|
22
|
+
* response order — which is not necessarily first in the final chain, since the `order` phase and
|
|
23
|
+
* `restoreLockedOrdering` can both place it later.
|
|
24
|
+
*/
|
|
25
|
+
interface DuplicateHunksRepair {
|
|
26
|
+
kind: 'duplicate-hunks';
|
|
27
|
+
/** The hunk indices whose extra claims were dropped, ascending. */
|
|
28
|
+
hunkIndices: number[];
|
|
29
|
+
claims: RepairedHunkClaim[];
|
|
30
|
+
/** Unstable log string. Read `claims` for anything programmatic; this shape is free to change. */
|
|
31
|
+
detail: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A deterministic fix a response validator applied to an AI response that would otherwise have
|
|
35
|
+
* been rejected, sparing a retry. A repaired response is treated as valid — repairs are reported
|
|
36
|
+
* for observability, not as a failure signal.
|
|
37
|
+
*
|
|
38
|
+
* This covers duplicate-hunk repair only. It is not an exhaustive record of every correction a
|
|
39
|
+
* validator makes: refine's locked-content substitution and `restoreLockedOrdering` stay silent.
|
|
40
|
+
*/
|
|
41
|
+
type ValidationRepair = DuplicateHunksRepair;
|
|
2
42
|
interface TokenUsage {
|
|
3
43
|
inputTokens: number;
|
|
4
44
|
outputTokens: number;
|
|
@@ -20,6 +60,11 @@ interface PhaseDetail {
|
|
|
20
60
|
promptVersion?: string;
|
|
21
61
|
/** Stable call IDs for each model.generate invocation in this phase (one per attempt). */
|
|
22
62
|
callIds?: string[];
|
|
63
|
+
/**
|
|
64
|
+
* Deterministic repairs applied to this phase's accepted response instead of spending a retry.
|
|
65
|
+
* Undefined when the response needed no repair.
|
|
66
|
+
*/
|
|
67
|
+
repairs?: ValidationRepair[];
|
|
23
68
|
}
|
|
24
69
|
interface ProgressEvent {
|
|
25
70
|
type: 'attempt_start' | 'validation_failed';
|
|
@@ -28,4 +73,4 @@ interface ProgressEvent {
|
|
|
28
73
|
errors?: string[];
|
|
29
74
|
}
|
|
30
75
|
//#endregion
|
|
31
|
-
export { PhaseDetail, ProgressEvent, TokenUsage, addUsage, emptyUsage };
|
|
76
|
+
export { DuplicateHunksRepair, PhaseDetail, ProgressEvent, RepairScope, RepairedHunkClaim, TokenUsage, ValidationRepair, addUsage, emptyUsage };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//#region src/context.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What the author of a change knows that the diff cannot show: the goal, the ticket, the reasoning
|
|
4
|
+
* behind individual edits, and the order things had to happen in.
|
|
5
|
+
*
|
|
6
|
+
* Every field is optional and every field is a hint. A consumer must still behave correctly when it
|
|
7
|
+
* is absent or empty, and must still validate whatever a model produces from it.
|
|
8
|
+
*
|
|
9
|
+
* The producer is deliberately unspecified. A coding agent writes it from its own session, a host
|
|
10
|
+
* application generates it from a task record, or a person writes it by hand — the consuming
|
|
11
|
+
* library cannot tell the difference and does not need to.
|
|
12
|
+
*
|
|
13
|
+
* **Treat every string here as untrusted.** Issue and pull-request bodies are third-party text on
|
|
14
|
+
* its way into a prompt, so a consumer rendering them must escape them and label them as data.
|
|
15
|
+
*/
|
|
16
|
+
interface AuthoringContext {
|
|
17
|
+
/** Why this work exists, in the author's own words. */
|
|
18
|
+
intent?: string;
|
|
19
|
+
issue?: AuthoringIssue;
|
|
20
|
+
pullRequest?: AuthoringPullRequest;
|
|
21
|
+
/** The design or plan the author worked from, when there was a written one. */
|
|
22
|
+
spec?: string;
|
|
23
|
+
/** Decisions and findings recorded during the work. */
|
|
24
|
+
notes?: AuthoringNote[];
|
|
25
|
+
/** Per-file rationale: why this path was touched at all. */
|
|
26
|
+
edits?: AuthoringEdit[];
|
|
27
|
+
/** Ordering constraints the author already knows. */
|
|
28
|
+
dependencies?: AuthoringDependency[];
|
|
29
|
+
/** Repository rules the author was already following. */
|
|
30
|
+
conventions?: string;
|
|
31
|
+
}
|
|
32
|
+
interface AuthoringIssue {
|
|
33
|
+
url?: string;
|
|
34
|
+
/** Tracker-native identifier, e.g. `AGP-430` or `#1234`. */
|
|
35
|
+
key?: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
body?: string;
|
|
38
|
+
}
|
|
39
|
+
interface AuthoringPullRequest {
|
|
40
|
+
url?: string;
|
|
41
|
+
title?: string;
|
|
42
|
+
body?: string;
|
|
43
|
+
}
|
|
44
|
+
interface AuthoringNote {
|
|
45
|
+
title?: string;
|
|
46
|
+
content: string;
|
|
47
|
+
}
|
|
48
|
+
interface AuthoringEdit {
|
|
49
|
+
path: string;
|
|
50
|
+
rationale: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `before` must land ahead of `after`. Each names either a repository path or a free-form concern
|
|
54
|
+
* label; a consumer resolves them against whatever it is grouping and ignores what it cannot match.
|
|
55
|
+
*
|
|
56
|
+
* Deliberately looser than the analyzer's `ConcernDependency`, which relates two concern ids inside
|
|
57
|
+
* one `AnalysisResult`. This shape has no analysis to reference — the author states orderings in
|
|
58
|
+
* its own words before any concern structure exists, and the ends here are prose, not identifiers.
|
|
59
|
+
*/
|
|
60
|
+
interface AuthoringDependency {
|
|
61
|
+
before: string;
|
|
62
|
+
after: string;
|
|
63
|
+
why?: string;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { AuthoringContext, AuthoringDependency, AuthoringEdit, AuthoringIssue, AuthoringNote, AuthoringPullRequest };
|
package/dist/context.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
package/dist/git/apply-target.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{GitError as e}from"../errors/git.js";import{applyPatchToWorkdir as t,checkout as n,cleanWorkdir as r,currentBranch as i,deleteBranch as a,discardWorkdirChanges as o,reset as s,revParse as c,revParseVerify as l,updateRef as u}from"./port-dispatch.js";import{
|
|
1
|
+
import{GitError as e}from"../errors/git.js";import{applyPatchToWorkdir as t,checkout as n,cleanWorkdir as r,currentBranch as i,deleteBranch as a,discardWorkdirChanges as o,reset as s,revParse as c,revParseVerify as l,updateRef as u}from"./port-dispatch.js";import{resolveWorkdirLayers as d}from"./diff-collector.js";import{applyBranchStructure as f}from"./apply-branches.js";import{reparentDescendants as p}from"./reparent.js";import{applyCommitChain as m,applyRangeRewrite as h,buildCommitChain as g,moveRefsAtSha as _}from"./commit-chain.js";import{verifyMultiBranchDiff as v,verifyResultingDiff as y}from"./safety.js";async function b(t){let{git:n,source:r,target:i,branchPlans:a,allOrderedCommits:o,hunks:s,snapshot:l,collection:u,signing:p,authorAttribution:_,stashLabel:v,leftoverPatch:b}=t,C=r.type===`commit-range`&&r.includeWorkdir!=null?d(r.includeWorkdir):void 0;if(i.type===`rewrite-range`){if(!u.branchName||!u.branchSha)throw new e(`OPERATION_FAILED`,`rewrite-range target requires a source that records branchName and branchSha (branch or commit-range).`);let t;try{t=await c(n,`${i.rangeFrom}^`)}catch{throw new e(`OPERATION_FAILED`,`rewrite-range target's rangeFrom (${i.rangeFrom.slice(0,8)}) has no parent (root commit) — rewriting the root commit is not currently supported.`)}let{headSha:r,shaMap:a}=await g({git:n,baseSha:t,commits:o,hunks:s,signing:p,authorAttribution:_}),d=await y(n,t,r,l);if(!d.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${d.error}`);let f=await h({git:n,branch:i.branch,rangeFrom:i.rangeFrom,rangeTo:i.rangeTo,newHeadSha:r,originalBranchSha:u.branchSha,stashLabel:v,leftoverPatch:b,includeWorkdir:C});return{commitShas:Object.fromEntries(a),mutations:D(f.refMovements),stashConflict:f.stashConflict,stashNotRestored:f.stashNotRestored}}if(u.branchName&&!u.mergeBaseSha)throw new e(`OPERATION_FAILED`,`Branch source is missing mergeBaseSha — cannot determine base commit`);let w=u.mergeBaseSha??await c(n,`HEAD`),E=k(r,i);if(i.type===`auto`&&a.length>1)return x(n,w,a,s,l,u,E,p,_);if(i.type===`auto`&&a.length===1&&a[0].partitioning)return S(n,w,a[0],s,l,u,E,p,_,v,C);let{headSha:j,shaMap:M}=await g({git:n,baseSha:w,commits:o,hunks:s,signing:p,authorAttribution:_}),N=Object.fromEntries(M),P=await y(n,w,j,l);if(!P.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${P.error}`);if(i.type===`branch`)return A({git:n,source:r,target:i,newHeadSha:j,collection:u,stashLabel:v,leftoverPatch:b,commitShas:N});if(i.type===`auto`){let e=a[0]?.branchGroup.name||u.branchName;if(e){let t=!E&&u.branchName&&u.branchSha?{originalBranchName:u.branchName,originalBranchSha:u.branchSha}:{},r={branchName:e,title:a[0]?.branchGroup.title??``,description:a[0]?.branchGroup.description??``,commitIds:o.map(e=>e.id)};await f({git:n,branches:[{partitions:[r],tipSha:j}],...t});let i=O([r],N,t.originalBranchName,t.originalBranchSha);return{partitions:[r],commitShas:N,mutations:i}}let t=await m({git:n,newHeadSha:j,stashLabel:v,includeWorkdir:C});return{commitShas:N,mutations:T(),stashConflict:t.stashConflict,stashNotRestored:t.stashNotRestored}}let F=await m({git:n,newHeadSha:j,stashLabel:v,leftoverPatch:b,includeWorkdir:C});return{commitShas:N,mutations:T(),stashConflict:F.stashConflict,stashNotRestored:F.stashNotRestored}}async function x(t,n,i,a,s,c,l,u,d){let p=[],m=[],h=[],_=[],y={};for(let e of i){let r=C(e),{headSha:i,shaMap:o}=await g({git:t,baseSha:n,commits:r,hunks:a,signing:u,authorAttribution:d});h.push(i);for(let[e,t]of o)y[e]=t;if(e.partitioning&&e.partitioning.partitions.length>1){let t=w(e.partitioning.partitions,o);p.push(...t),m.push(t),_.push({partitions:t,tipSha:i})}else{let t={branchName:e.branchGroup.name,title:e.branchGroup.title,description:e.branchGroup.description,commitIds:r.map(e=>o.get(e.id)??e.id)};p.push(t),m.push([t]),_.push({partitions:[t],tipSha:i})}}let b=await v(t,n,h,s);if(!b.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${b.error}`);let x=!l&&c.branchName&&c.branchSha?{originalBranchName:c.branchName,originalBranchSha:c.branchSha}:{};return await f({git:t,branches:_,...x}),!c.branchName&&!l&&(await o(t),c.untrackedIncluded&&await r(t)),{partitions:p,partitionGroups:m,commitShas:y,mutations:O(p,y,x.originalBranchName,x.originalBranchSha)}}async function S(t,n,i,a,s,c,l,u,d,p,h){let{headSha:_,shaMap:v}=await g({git:t,baseSha:n,commits:C(i),hunks:a,signing:u,authorAttribution:d}),b=await y(t,n,_,s);if(!b.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${b.error}`);let x=w(i.partitioning.partitions,v),S=Object.fromEntries(v);if(x.length>1){let e=!l&&c.branchName&&c.branchSha?{originalBranchName:c.branchName,originalBranchSha:c.branchSha}:{};return await f({git:t,branches:[{partitions:x,tipSha:_}],...e}),!c.branchName&&!l&&(await o(t),c.untrackedIncluded&&await r(t,{force:!0,directories:!0})),{partitions:x,commitShas:S,mutations:O(x,S,e.originalBranchName,e.originalBranchSha)}}let E=await m({git:t,newHeadSha:_,stashLabel:p,includeWorkdir:h});return{partitions:x,commitShas:S,mutations:T(),stashConflict:E.stashConflict,stashNotRestored:E.stashNotRestored}}function C(t){return t.orderedCommitIds.map(n=>{let r=t.branchGroup.commits.find(e=>e.id===n);if(!r)throw new e(`OPERATION_FAILED`,`Commit ${n} not found in branch '${t.branchGroup.id}'`);return r})}function w(t,n){return t.map(t=>({...t,commitIds:t.commitIds.map(t=>{let r=n.get(t);if(!r)throw new e(`OPERATION_FAILED`,`No SHA found for commit ID '${t}'`);return r})}))}function T(){return{createdRefs:[],deletedRefs:[],movedRefs:[],composedBranchCount:0}}function E(t){if(t.startsWith(`refs/heads/`))return{kind:`branch`,name:t.slice(11)};if(t.startsWith(`refs/tags/`))return{kind:`tag`,name:t.slice(10)};throw new e(`OPERATION_FAILED`,`Unexpected refname '${t}' in apply mutations — only refs/heads/* and refs/tags/* are tracked.`)}function D(e){let t=[];for(let n of e){let{kind:e,name:r}=E(n.refname);t.push({kind:e,name:r,previousSha:n.previousSha,newSha:n.newSha})}return{createdRefs:[],deletedRefs:[],movedRefs:t,composedBranchCount:1}}function O(e,t,n,r){let i=e.map(e=>{let n=e.commitIds[e.commitIds.length-1];return{kind:`branch`,name:e.branchName,sha:t[n]??n}}),a=[];return n&&r&&a.push({kind:`branch`,name:n,sha:r}),{createdRefs:i,deletedRefs:a,movedRefs:[],composedBranchCount:i.length}}function k(e,t){return t.type!==`branch`&&t.type!==`auto`||e.type===`commit-range`?!0:t.preserveOriginal===!0}async function A(r){let{git:o,source:d,target:f,newHeadSha:m,collection:h,leftoverPatch:g,commitShas:v}=r,y=k(d,f),b=d.type===`branch`&&!y&&h.branchName!=null&&h.branchSha!=null;if(b&&f.name===h.branchName)throw new e(`OPERATION_FAILED`,`target 'branch' with name '${f.name}' conflicts with the source branch (preserveOriginal=false). Use target 'rewrite-range' to rewrite the branch in place under its existing name.`);let x=await l(o,`refs/heads/${f.name}`),S=x?{createdRefs:[],deletedRefs:[],movedRefs:[{kind:`branch`,name:f.name,previousSha:x,newSha:m}],composedBranchCount:1}:{createdRefs:[{kind:`branch`,name:f.name,sha:m}],deletedRefs:[],movedRefs:[],composedBranchCount:1},C=await i(o),w=await c(o,`HEAD`);if(await u(o,`refs/heads/${f.name}`,m),b){let e=h.branchSha,r=h.branchName,i=await p({git:o,oldToSha:e,newTopSha:m,sourceBranchName:void 0});for(let e of i.reparentedRefs){let{kind:t,name:n}=E(e.refname);S.movedRefs.push({kind:t,name:n,previousSha:e.oldSha,newSha:e.newSha})}let c=await _(o,e,m,`refs/heads/${r}`);for(let e of c){let{kind:t,name:n}=E(e.refname);S.movedRefs.push({kind:t,name:n,previousSha:e.previousSha,newSha:e.newSha})}C===r&&(await n(o,f.name),await s(o,m,`hard`),g&&await t(o,g)),await a(o,r,{force:!0}),S.deletedRefs.push({kind:`branch`,name:r,sha:e})}else d.type===`workdir`&&!y&&(await s(o,w,`hard`),g&&await t(o,g));return{commitShas:v,mutations:S}}export{b as applyToTarget};
|
|
@@ -21,6 +21,12 @@ interface BuildCommitChainResult {
|
|
|
21
21
|
shaMap: Map<string, string>;
|
|
22
22
|
}
|
|
23
23
|
declare function buildCommitChain(params: BuildCommitChainParams): Promise<BuildCommitChainResult>;
|
|
24
|
+
/** Resolved `ComposeSource.includeWorkdir` layer flags — every layer decided, no defaulting left. */
|
|
25
|
+
type WorkdirLayers = {
|
|
26
|
+
includeStaged: boolean;
|
|
27
|
+
includeUnstaged: boolean;
|
|
28
|
+
includeUntracked: boolean;
|
|
29
|
+
};
|
|
24
30
|
interface ApplyCommitChainParams {
|
|
25
31
|
git: ComposeGitPort;
|
|
26
32
|
newHeadSha: string;
|
|
@@ -32,6 +38,8 @@ interface ApplyCommitChainParams {
|
|
|
32
38
|
* unstaged WIP, so the user's unselected content isn't lost when a destructive apply rewrites
|
|
33
39
|
* the source. */
|
|
34
40
|
leftoverPatch?: string;
|
|
41
|
+
/** See {@link ApplyRangeRewriteParams.includeWorkdir} — same contract on this apply path. */
|
|
42
|
+
includeWorkdir?: WorkdirLayers;
|
|
35
43
|
}
|
|
36
44
|
interface ApplyChainResult {
|
|
37
45
|
stashConflict?: StashConflict;
|
|
@@ -52,10 +60,11 @@ interface ApplyRangeRewriteParams {
|
|
|
52
60
|
originalBranchSha: string;
|
|
53
61
|
stashLabel?: string;
|
|
54
62
|
leftoverPatch?: string;
|
|
55
|
-
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
63
|
+
/** Resolved `includeWorkdir` layer flags when the source folded WIP into the input. When set, no
|
|
64
|
+
* safety stash is taken — the WIP is part of the source, and leftover hunks are materialized via
|
|
65
|
+
* {@link leftoverPatch} — and any layer the caller excluded is captured before the worktree reset
|
|
66
|
+
* and re-applied after it. When undefined, a safety stash protects unrelated dirty WIP. */
|
|
67
|
+
includeWorkdir?: WorkdirLayers;
|
|
59
68
|
}
|
|
60
69
|
interface ApplyRangeRewriteResult {
|
|
61
70
|
/** Final source-branch tip SHA after descendant re-parenting and any ref-at-tip moves. */
|
|
@@ -94,4 +103,4 @@ declare function moveRefsAtSha(git: ComposeGitPort, oldSha: string, newSha: stri
|
|
|
94
103
|
newSha: string;
|
|
95
104
|
}[]>;
|
|
96
105
|
//#endregion
|
|
97
|
-
export { ApplyChainResult, ApplyCommitChainParams, ApplyRangeRewriteParams, ApplyRangeRewriteResult, AuthorAttributionStrategy, BuildCommitChainParams, BuildCommitChainResult, applyCommitChain, applyRangeRewrite, buildCommitChain, moveRefsAtSha };
|
|
106
|
+
export { ApplyChainResult, ApplyCommitChainParams, ApplyRangeRewriteParams, ApplyRangeRewriteResult, AuthorAttributionStrategy, BuildCommitChainParams, BuildCommitChainResult, WorkdirLayers, applyCommitChain, applyRangeRewrite, buildCommitChain, moveRefsAtSha };
|
package/dist/git/commit-chain.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{GitError as e}from"../errors/git.js";import{applyPatchToIndex as t,applyPatchToWorkdir as n,checkout as r,commitTree as i,currentBranch as a,diffTree as o,localeNeutralEnv as s,readTree as c,reset as l,revParse as u,revParseVerify as d,stageAll as f,stashDrop as p,stashPop as m,stashPush as h,statusPorcelain as g,updateRef as _,writeTree as v}from"./port-dispatch.js";import{listRefsWithPeeledSha as y}from"./utils/refs.js";import{
|
|
2
|
-
`)[0]}': ${r}`,{cause:t})}for(let e of a)e.originalFileName&&e.diffHeader.includes(`rename from`)&&m.add(e.diffHeader);let g=await v(r,{indexEnv:h}),_=
|
|
3
|
-
`).some(e=>e.startsWith(`?? `));await h(e,{message:t,includeUntracked:!0});try{let t=(await u(e,`stash@{0}`)).trim();return t.length===0?null:(r||=await
|
|
4
|
-
`)){let n=e.indexOf(` `);if(!(n<0)&&e.slice(0,n)===t){let t=e.slice(n+1).trim();return t.length>0?t:null}}return null}async function
|
|
5
|
-
`))/^(UU|AA|DD|AU|UA|UD|DU) /.test(e)&&n.push(e.slice(3).trim());return n}catch{return[]}}function
|
|
6
|
-
`);return`${e.trimEnd()}\n\n${n}\n`}function
|
|
1
|
+
import{GitError as e}from"../errors/git.js";import{applyPatchToIndex as t,applyPatchToWorkdir as n,checkout as r,commitTree as i,currentBranch as a,diffTree as o,localeNeutralEnv as s,readTree as c,reset as l,revParse as u,revParseVerify as d,stageAll as f,stashDrop as p,stashPop as m,stashPush as h,statusPorcelain as g,updateRef as _,writeTree as v}from"./port-dispatch.js";import{listRefsWithPeeledSha as y}from"./utils/refs.js";import{captureExcludedLayersPatch as b}from"./diff-collector.js";import{reparentDescendants as x}from"./reparent.js";import{buildPatchForCommit as S}from"./utils/patch-builder.js";import{randomUUID as C}from"node:crypto";import*as w from"node:fs";import*as T from"node:os";import*as E from"node:path";async function D(n){let{git:r,baseSha:a,commits:o,hunks:s,signing:l,authorAttribution:u=`plurality`}=n,d=E.join(T.tmpdir(),`compose-idx-${C()}`),f=a,p=new Map,m=new Set;try{for(let n of o){let a=n.hunkIndices.map(t=>{let n=s.find(e=>e.index===t);if(!n)throw new e(`OPERATION_FAILED`,`Hunk index ${t} not found`);return n}),o=S(a.map(e=>m.has(e.diffHeader)?{...e,diffHeader:q(e.diffHeader,e.fileName)}:e));if(!o.trim())throw new e(`OPERATION_FAILED`,`Empty patch for commit: ${n.message}`);let h={GIT_INDEX_FILE:d};await c(r,f,{indexEnv:h});try{await t(r,o,{indexEnv:h})}catch(t){let r=t instanceof Error?t.message:String(t);throw new e(`OPERATION_FAILED`,`git apply failed for commit '${n.message.split(`
|
|
2
|
+
`)[0]}': ${r}`,{cause:t})}for(let e of a)e.originalFileName&&e.diffHeader.includes(`rename from`)&&m.add(e.diffHeader);let g=await v(r,{indexEnv:h}),_=U(a,u),y=G(n.message,_.coAuthors),b=await i(r,g,[f],y,{signing:l,author:_.author});p.set(n.id,b),f=b}return{headSha:f,shaMap:p}}finally{K(d)}}async function O(e,t,r){t&&await n(e,t),r&&await n(e,r)}async function k(e){let{git:t,newHeadSha:n,branchName:i,stashLabel:a,leftoverPatch:o,includeWorkdir:s}=e,c=a??M,d=s==null?await N(t,c):null,f=s==null?void 0:await b(t,await u(t,`HEAD`),s);try{i?await r(t,i,{createBranch:!0,startPoint:n}):await l(t,n,`hard`),await O(t,o,f)}catch(e){throw d!=null&&await L(t,d),e}if(d!=null){let e=await L(t,d);if(e.kind===`conflicted`)return{stashConflict:{stashLabel:c,conflictedFiles:e.conflictedFiles}};if(e.kind===`not-restored`)return{stashNotRestored:{stashLabel:c,stashSha:d.sha,reason:e.reason}}}return{}}async function A(e){let{git:t,branch:n,rangeTo:i,newHeadSha:o,originalBranchSha:s,stashLabel:c,leftoverPatch:d,includeWorkdir:f}=e,p=await a(t)===n,m=c??M,h=p&&f==null?await N(t,m):null,g=p&&f!=null?await b(t,await u(t,`HEAD`),f):void 0,v,y,S=o,C=[];try{let e=await x({git:t,oldToSha:i,newTopSha:o,sourceBranchName:n});for(let t of e.reparentedRefs)C.push({refname:t.refname,previousSha:t.oldSha,newSha:t.newSha});e.sourceBranchNewSha==null?(await _(t,`refs/heads/${n}`,o),C.push({refname:`refs/heads/${n}`,previousSha:s,newSha:o}),S=o):S=e.sourceBranchNewSha;let a=await j(t,i,o,`refs/heads/${n}`);C.push(...a),p&&(await r(t,n),await l(t,S,`hard`),await O(t,d,g))}catch(e){throw h!=null&&await L(t,h),e}if(h!=null){let e=await L(t,h);e.kind===`conflicted`?v={stashLabel:m,conflictedFiles:e.conflictedFiles}:e.kind===`not-restored`&&(y={stashLabel:m,stashSha:h.sha,reason:e.reason})}return{finalTipSha:S,refMovements:C,stashConflict:v,stashNotRestored:y}}async function j(e,t,n,r){let i=[];for(let{refname:a,sha:o}of await y(e,{pointsAt:t}))a!==r&&(!a.startsWith(`refs/heads/`)&&!a.startsWith(`refs/tags/`)||o===t&&(await _(e,a,n),i.push({refname:a,previousSha:t,newSha:n})));return i}const M=`compose backup`;async function N(e,t){let n=await g(e);if(!n.trim())return null;let r=n.split(`
|
|
3
|
+
`).some(e=>e.startsWith(`?? `));await h(e,{message:t,includeUntracked:!0});try{let t=(await u(e,`stash@{0}`)).trim();return t.length===0?null:(r||=await F(e,t),{sha:t,hasUntracked:r})}catch{return null}}const P=new Set([`4b825dc642cb6eb9a060e54bf8d69288fbee4904`,`6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321`]);async function F(e,t){try{let n=await d(e,`${t}^3^{tree}`);return n==null?!0:!P.has(n)}catch{return!0}}async function I(e,t){if(!e.exec)return null;let n;try{n=await e.exec([`stash`,`list`,`--format=%H %gd`],{env:s})}catch{return null}for(let e of n.split(`
|
|
4
|
+
`)){let n=e.indexOf(` `);if(!(n<0)&&e.slice(0,n)===t){let t=e.slice(n+1).trim();return t.length>0?t:null}}return null}async function L(e,t){if(!t.hasUntracked)try{let n=(await u(e,`${t.sha}^{tree}`)).trim(),r=await z(e);if(n.length>0&&n===r){let n=await I(e,t.sha);if(n!=null)try{await p(e,n)}catch{}return{kind:`restored`}}}catch{}let n=await I(e,t.sha);if(n==null)return{kind:`not-restored`,reason:`stash-ref-not-found`};let r;try{return await m(e,{withIndex:!0,ref:n}),{kind:`restored`}}catch(e){r=e}let i=await H(e);if(i.length>0)return{kind:`conflicted`,conflictedFiles:i};if(B(r)){if(await R(e,t.sha)){let n=await I(e,t.sha);if(n!=null)try{await p(e,n)}catch{}return{kind:`restored`}}return{kind:`not-restored`,reason:`untracked-restore-failed`}}return{kind:`not-restored`,reason:`pop-failed`}}async function R(e,t){try{let n=await d(e,`${t}^3^{tree}`);return n==null?!1:(await o(e,n,await z(e),{nameOnly:!0,binary:!1,diffFilter:`DMT`,noRenames:!0})).trim().length===0}catch{return!1}}async function z(e){let t=E.join(T.tmpdir(),`compose-equiv-${C()}`);try{let n={GIT_INDEX_FILE:t};return await c(e,`HEAD`,{indexEnv:n}),await f(e,{indexEnv:n}),(await v(e,{indexEnv:n})).trim()}finally{K(t)}}function B(e){let t=V(e);return t.includes(`already exists, no checkout`)||t.includes(`could not restore untracked files from stash`)}function V(e){if(e==null)return``;if(e instanceof Error){let t=e.stderr;return t!=null&&t.length>0?`${e.message}\n${t}`:e.message}return String(e)}async function H(e){try{let t=await g(e),n=[];for(let e of t.split(`
|
|
5
|
+
`))/^(UU|AA|DD|AU|UA|UD|DU) /.test(e)&&n.push(e.slice(3).trim());return n}catch{return[]}}function U(e,t){let n=e.filter(e=>e.author!=null);if(n.length===0)return{author:void 0,coAuthors:[]};let r;if(typeof t==`function`)r=t(e);else if(t===`first-hunk`)r=n[0].author;else{let e=new Map;for(let t of n){let n=W(t.author),r=e.get(n);r?r.count++:e.set(n,{identity:t.author,count:1})}let t=0;for(let{identity:n,count:i}of e.values())i>t&&(r=n,t=i)}let i=new Map;for(let t of e){t.author&&i.set(W(t.author),t.author);for(let e of t.coAuthors??[])i.set(W(e),e)}let a=r?W(r):null,o=[];for(let[e,t]of i)e!==a&&o.push(t);return{author:r,coAuthors:o}}function W(e){return`${e.name.toLowerCase()}\0${e.email.toLowerCase()}`}function G(e,t){if(t.length===0)return e;let n=t.map(e=>`Co-authored-by: ${e.name} <${e.email}>`).join(`
|
|
6
|
+
`);return`${e.trimEnd()}\n\n${n}\n`}function K(e){try{w.unlinkSync(e)}catch{}}function q(e,t){let n=e.split(`
|
|
7
7
|
`),r=[];for(let e of n)e.startsWith(`diff --git `)?r.push(`diff --git a/${t} b/${t}`):e.startsWith(`similarity index`)||e.startsWith(`rename from`)||e.startsWith(`rename to`)||(e.startsWith(`--- a/`)?r.push(`--- a/${t}`):r.push(e));return r.join(`
|
|
8
|
-
`)}export{
|
|
8
|
+
`)}export{k as applyCommitChain,A as applyRangeRewrite,D as buildCommitChain,j as moveRefsAtSha};
|
|
@@ -87,10 +87,10 @@ declare function collectCombinedDiffRange(git: ComposeGitPort, parentOfFromSha:
|
|
|
87
87
|
* snapshot hashes that same combined diff — the bytes the AI sees match the bytes the
|
|
88
88
|
* apply phase verifies against.
|
|
89
89
|
*
|
|
90
|
-
* Requires `to` to equal both the branch tip and HEAD, HEAD to be non-detached
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
90
|
+
* Requires `to` to equal both the branch tip and HEAD, and HEAD to be non-detached — mixing WIP
|
|
91
|
+
* with a range only makes sense when the user is actively on the branch tip being recomposed.
|
|
92
|
+
* A layer the caller excluded stays out of the collected input entirely; preserving its content
|
|
93
|
+
* across the apply's worktree reset is the apply path's job, via {@link captureExcludedLayersPatch}.
|
|
94
94
|
*/
|
|
95
95
|
declare function collectCommitRangeWithWorkdir(git: ComposeGitPort, branchName: string, from: string, to: string, options: {
|
|
96
96
|
includeStaged: boolean;
|
|
@@ -110,17 +110,31 @@ declare function collectWorkdirPortionForRange(git: ComposeGitPort, headSha: str
|
|
|
110
110
|
}): Promise<{
|
|
111
111
|
finalTreeSha: string;
|
|
112
112
|
}>;
|
|
113
|
+
/** Resolve `ComposeSource.includeWorkdir`'s optional per-layer flags, each of which defaults to `true`. */
|
|
114
|
+
declare function resolveWorkdirLayers(includeWorkdir: {
|
|
115
|
+
includeStaged?: boolean;
|
|
116
|
+
includeUnstaged?: boolean;
|
|
117
|
+
includeUntracked?: boolean;
|
|
118
|
+
}): {
|
|
119
|
+
includeStaged: boolean;
|
|
120
|
+
includeUnstaged: boolean;
|
|
121
|
+
includeUntracked: boolean;
|
|
122
|
+
};
|
|
113
123
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
124
|
+
* Capture the content of the workdir layers the caller excluded from a `commit-range +
|
|
125
|
+
* includeWorkdir` compose, as one patch. Returns `undefined` when both tracked layers are
|
|
126
|
+
* included, or when the excluded ones hold nothing.
|
|
127
|
+
*
|
|
128
|
+
* An excluded layer is deliberately absent from the collected input, so it is also absent from the
|
|
129
|
+
* plan and from the leftover patch — and a range rewrite hard-resets the worktree to the new tip.
|
|
130
|
+
* This patch is the only remaining record of that content, so the apply path takes it before
|
|
131
|
+
* mutating anything and re-applies it after the reset. Untracked files are not included: the reset
|
|
132
|
+
* leaves them alone.
|
|
119
133
|
*/
|
|
120
|
-
declare function
|
|
134
|
+
declare function captureExcludedLayersPatch(git: ComposeGitPort, headSha: string, options: {
|
|
121
135
|
includeStaged: boolean;
|
|
122
136
|
includeUnstaged: boolean;
|
|
123
|
-
}): Promise<
|
|
137
|
+
}): Promise<string | undefined>;
|
|
124
138
|
/**
|
|
125
139
|
* Compute the tree SHA of the worktree's tracked content, working against a temp copy of the
|
|
126
140
|
* real index so the consumer's staged state is never modified.
|
|
@@ -144,4 +158,4 @@ declare function collectSourceHashes(git: ComposeGitPort, options: {
|
|
|
144
158
|
includeUntracked: boolean;
|
|
145
159
|
}): Promise<SafetySnapshot['sourceHashes']>;
|
|
146
160
|
//#endregion
|
|
147
|
-
export { BranchCommitInfo, BranchDiffResult, CollectionResult, SplitCollectionResult, WorkingDiffOptions, WorkingDiffResult, collectBranchDiffs, collectCombinedDiffRange, collectCommitRangeDiffs, collectCommitRangeWithWorkdir, collectCommitsForSplit, collectFromSource, collectSourceHashes, collectWorkdirPortionForRange, collectWorkingDiffs, computeTrackedWorktreeTreeSha,
|
|
161
|
+
export { BranchCommitInfo, BranchDiffResult, CollectionResult, SplitCollectionResult, WorkingDiffOptions, WorkingDiffResult, captureExcludedLayersPatch, collectBranchDiffs, collectCombinedDiffRange, collectCommitRangeDiffs, collectCommitRangeWithWorkdir, collectCommitsForSplit, collectFromSource, collectSourceHashes, collectWorkdirPortionForRange, collectWorkingDiffs, computeTrackedWorktreeTreeSha, resolveWorkdirLayers };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import{GitError as e}from"../errors/git.js";import{ComposeWorkflowInputError as t}from"../errors/workflow.js";import{applyPatchToIndex as n,createBranch as r,currentBranch as i,diffTree as a,gitDir as o,listUntracked as s,log as c,mergeBase as l,mergeBaseIsAncestor as u,readTree as d,revParse as f,revParseVerify as p,stageAll as m,writeTree as h}from"./port-dispatch.js";import{parseUnifiedDiff as g}from"./utils/hunk-parser.js";import{getAuthorAndCoAuthorsForCombinedDiffHunk as _}from"./utils/authorship.js";import{sha256 as v}from"./utils/crypto.js";import{withUntrackedInTempIndex as y}from"./utils/temp-index.js";import{randomUUID as b}from"node:crypto";import{copyFileSync as x,unlinkSync as S}from"node:fs";import{tmpdir as C}from"node:os";import{join as w}from"node:path";async function T(e,t){if(t.type===`workdir`){let n=!t.stagedOnly&&!t.noUntracked;return{...await E(e,{includeUnstaged:!t.stagedOnly,includeUntracked:n}),untrackedIncluded:n}}if(t.type===`commit-range`){if(t.includeWorkdir){let n=t.includeWorkdir.includeStaged??!0,r=t.includeWorkdir.includeUnstaged??!0,i=t.includeWorkdir.includeUntracked??!0,a=await L(e,t.branch,t.from,t.to,{includeStaged:n,includeUnstaged:r,includeUntracked:i});return{hunks:a.hunks,snapshot:a.snapshot,combinedDiff:a.combinedDiff,commitMessages:a.commits.map(e=>e.message),commitOrder:a.commits.map(e=>e.sha),branchName:t.branch,baseBranch:void 0,branchSha:a.snapshot.branchSha,mergeBaseSha:a.snapshot.baseSha,untrackedIncluded:i}}let n=await M(e,t.branch,t.from,t.to);return{hunks:n.hunks,snapshot:n.snapshot,combinedDiff:n.combinedDiff,commitMessages:n.commits.map(e=>e.message),commitOrder:n.commits.map(e=>e.sha),branchName:t.branch,baseBranch:void 0,branchSha:n.snapshot.branchSha,mergeBaseSha:n.snapshot.baseSha,untrackedIncluded:!1}}let n=t.mergeTarget??`main`,r=await j(e,t.name,n);return{hunks:r.hunks,snapshot:r.snapshot,combinedDiff:r.combinedDiff,commitMessages:r.commits.map(e=>e.message),commitOrder:r.commits.map(e=>e.sha),branchName:t.name,baseBranch:n,branchSha:r.snapshot.branchSha,mergeBaseSha:r.snapshot.baseSha,untrackedIncluded:!1}}async function E(e,t){let n=t.includeUnstaged||t.includeUntracked;return t.includeUntracked?k(e,n):D(e,n)}async function D(e,t){let n=await f(e,`HEAD`),r=await O(e,n,t);return{hunks:g(r),snapshot:
|
|
1
|
+
import{GitError as e}from"../errors/git.js";import{ComposeWorkflowInputError as t}from"../errors/workflow.js";import{applyPatchToIndex as n,createBranch as r,currentBranch as i,diffTree as a,gitDir as o,listUntracked as s,log as c,mergeBase as l,mergeBaseIsAncestor as u,readTree as d,revParse as f,revParseVerify as p,stageAll as m,writeTree as h}from"./port-dispatch.js";import{parseUnifiedDiff as g}from"./utils/hunk-parser.js";import{getAuthorAndCoAuthorsForCombinedDiffHunk as _}from"./utils/authorship.js";import{sha256 as v}from"./utils/crypto.js";import{withUntrackedInTempIndex as y}from"./utils/temp-index.js";import{randomUUID as b}from"node:crypto";import{copyFileSync as x,unlinkSync as S}from"node:fs";import{tmpdir as C}from"node:os";import{join as w}from"node:path";async function T(e,t){if(t.type===`workdir`){let n=!t.stagedOnly&&!t.noUntracked;return{...await E(e,{includeUnstaged:!t.stagedOnly,includeUntracked:n}),untrackedIncluded:n}}if(t.type===`commit-range`){if(t.includeWorkdir){let n=t.includeWorkdir.includeStaged??!0,r=t.includeWorkdir.includeUnstaged??!0,i=t.includeWorkdir.includeUntracked??!0,a=await L(e,t.branch,t.from,t.to,{includeStaged:n,includeUnstaged:r,includeUntracked:i});return{hunks:a.hunks,snapshot:a.snapshot,combinedDiff:a.combinedDiff,commitMessages:a.commits.map(e=>e.message),commitOrder:a.commits.map(e=>e.sha),branchName:t.branch,baseBranch:void 0,branchSha:a.snapshot.branchSha,mergeBaseSha:a.snapshot.baseSha,untrackedIncluded:i}}let n=await M(e,t.branch,t.from,t.to);return{hunks:n.hunks,snapshot:n.snapshot,combinedDiff:n.combinedDiff,commitMessages:n.commits.map(e=>e.message),commitOrder:n.commits.map(e=>e.sha),branchName:t.branch,baseBranch:void 0,branchSha:n.snapshot.branchSha,mergeBaseSha:n.snapshot.baseSha,untrackedIncluded:!1}}let n=t.mergeTarget??`main`,r=await j(e,t.name,n);return{hunks:r.hunks,snapshot:r.snapshot,combinedDiff:r.combinedDiff,commitMessages:r.commits.map(e=>e.message),commitOrder:r.commits.map(e=>e.sha),branchName:t.name,baseBranch:n,branchSha:r.snapshot.branchSha,mergeBaseSha:r.snapshot.baseSha,untrackedIncluded:!1}}async function E(e,t){let n=t.includeUnstaged||t.includeUntracked;return t.includeUntracked?k(e,n):D(e,n)}async function D(e,t){let n=await f(e,`HEAD`),r=await O(e,n,t);return{hunks:g(r),snapshot:W(r,n,void 0,void 0,await G(e,{includeUnstaged:t,includeUntracked:!1})),combinedDiff:r}}async function O(e,t,n){return n?a(e,t,await V(e)):a(e,t,await h(e))}async function k(e,t){return y(e,async n=>{await A(e,n,t);let r=await h(e,{indexEnv:n}),i=await f(e,`HEAD`),o=await a(e,i,r);return{hunks:g(o),snapshot:W(o,i,void 0,void 0,await G(e,{includeUnstaged:t,includeUntracked:!0})),combinedDiff:o}})}async function A(e,t,n){n&&await m(e,{trackedOnly:!0,indexEnv:t}),await m(e,{indexEnv:t})}async function j(e,t,n){let r=await U(e,t);return P(e,await l(e,await U(e,n),r),await f(e,r))}async function M(e,t,n,r){await U(e,t);let i=await f(e,n),a=await f(e,r);return P(e,await N(e,i),a)}async function N(e,n){try{return await f(e,`${n}^`)}catch{throw new t(`Cannot recompose commit ${n.slice(0,8)} — it has no parent (root commit). Recomposing the root commit is not currently supported.`)}}async function P(n,r,i){if(!await u(n,r,i))throw new t(`Cannot collect range ${r.slice(0,8)}..${i.slice(0,8)}: '${r.slice(0,8)}' is not an ancestor of '${i.slice(0,8)}'.`);let o=await a(n,r,i),s=g(o),l=I(await c(n,{format:`%H%n%an%n%ae%n%aI%n%B%n---COMMIT-END---`,range:`${r}..${i}`,reverse:!0}));if(l.length===0)throw new e(`NO_CHANGES`,`No commits in range '${r.slice(0,8)}..${i.slice(0,8)}'.`);let d=await F(n,l);for(let e of s){let{author:t,coAuthors:n}=_(d,e);t!=null&&(e.author=t),n.length>0&&(e.coAuthors=n)}let p=W(o,await f(n,`HEAD`),i,r);return{hunks:s,commits:l.map(e=>({sha:e.sha,message:e.message})),snapshot:p,combinedDiff:o}}async function F(e,t){let n=[];for(let r of t){let t;try{t=await a(e,`${r.sha}^`,r.sha)}catch{continue}let i=g(t,0);for(let e of i)e.author={name:r.authorName,email:r.authorEmail,date:r.authorDate};n.push(...i)}return n}function I(e){let t=[],n=e.split(`---COMMIT-END---`).filter(e=>e.trim());for(let e of n){let n=e.replace(/^\n+/,``).split(`
|
|
2
2
|
`);if(n.length<5)continue;let r=n[0],i=n[1],a=n[2],o=n[3],s=n.slice(4).join(`
|
|
3
|
-
`).trimEnd(),c=o?new Date(o):void 0,l=c&&!Number.isNaN(c.getTime())?c:void 0;t.push({sha:r,authorName:i,authorEmail:a,authorDate:l,message:s})}return t}async function L(e,n,r,o,s){let l=await f(e,await
|
|
3
|
+
`).trimEnd(),c=o?new Date(o):void 0,l=c&&!Number.isNaN(c.getTime())?c:void 0;t.push({sha:r,authorName:i,authorEmail:a,authorDate:l,message:s})}return t}async function L(e,n,r,o,s){let l=await f(e,await U(e,n)),d=await f(e,r),p=await f(e,o),m=await f(e,`HEAD`);if(p!==l)throw new t(`commit-range with includeWorkdir requires 'to' to equal branch tip. Branch '${n}' tip is ${l}; got 'to'=${p}.`);if(m!==p)throw new t(`commit-range with includeWorkdir requires HEAD to equal 'to'. HEAD=${m}, 'to'=${p}.`);if(await i(e)==null)throw new t(`commit-range with includeWorkdir requires HEAD to be on a branch (non-detached). Check out the branch holding the range before composing.`);if(!s.includeStaged&&s.includeUnstaged&&(await a(e,m,await h(e))).trim())throw new t(`Cannot exclude staged changes while composing unstaged changes: unstaged changes are diffed against the index, so the excluded staged layer cannot be replayed over the composed result. Include the staged layer, or commit/stash it before composing.`);let v=await N(e,d),{finalTreeSha:y}=await R(e,m,s),b=await a(e,v,y);if(!await u(e,v,p))throw new t(`Cannot collect range ${v.slice(0,8)}..${p.slice(0,8)}: '${v.slice(0,8)}' is not an ancestor of '${p.slice(0,8)}'.`);let x=g(b),S=I(await c(e,{format:`%H%n%an%n%ae%n%aI%n%B%n---COMMIT-END---`,range:`${v}..${p}`,reverse:!0})),C=await F(e,S);for(let e of x){let{author:t,coAuthors:n}=_(C,e);t!=null&&(e.author=t),n.length>0&&(e.coAuthors=n)}let w=W(b,m,p,v,await G(e,s));return{hunks:x,commits:S.map(e=>({sha:e.sha,message:e.message})),snapshot:w,combinedDiff:b}}async function R(e,t,r){let i=w(C(),`compose-workdir-${b()}`),o={GIT_INDEX_FILE:i};try{if(await d(e,t,{indexEnv:o}),r.includeStaged){let r=await a(e,t,await h(e));r.trim()&&await n(e,r,{indexEnv:o})}if(r.includeUnstaged){let t=await a(e,await h(e),await V(e));t.trim()&&await n(e,t,{indexEnv:o})}if(r.includeUntracked){let t=await s(e);t.length>0&&(await m(e,{intentToAdd:!0,paths:t,indexEnv:o}),await m(e,{paths:t,indexEnv:o}))}return{finalTreeSha:await h(e,{indexEnv:o})}}finally{try{S(i)}catch{}}}function z(e){return{includeStaged:e.includeStaged??!0,includeUnstaged:e.includeUnstaged??!0,includeUntracked:e.includeUntracked??!0}}async function B(e,t,n){if(n.includeStaged&&n.includeUnstaged)return;let r;return r=!n.includeStaged&&!n.includeUnstaged?await a(e,t,await V(e)):n.includeStaged?await a(e,await h(e),await V(e)):await a(e,t,await h(e)),r.trim()?r:void 0}async function V(e){let t=w(C(),`compose-wt-tree-${b()}`),n={GIT_INDEX_FILE:t};try{return x(w(await o(e),`index`),t),await m(e,{trackedOnly:!0,indexEnv:n}),await h(e,{indexEnv:n})}finally{try{S(t)}catch{}}}async function H(e,t,n){let r=await U(e,t),i=await l(e,await U(e,n),r),o=await f(e,r),s=(await c(e,{format:`%H`,range:`${i}..${r}`,reverse:!0})).trim().split(`
|
|
4
4
|
`).filter(Boolean),u=[];for(let t of s){let[n,r,i]=await Promise.all([c(e,{format:`%B`,sha:t}).then(e=>e.trim()),a(e,`${t}^`,t),a(e,`${t}^`,t,{binary:!1,nameOnly:!0})]),o=i.trim().split(`
|
|
5
|
-
`).filter(Boolean);u.push({sha:t,message:n,diff:r,filesChanged:o})}return{commits:u,mergeBaseSha:i,branchSha:o}}async function
|
|
5
|
+
`).filter(Boolean);u.push({sha:t,message:n,diff:r,filesChanged:o})}return{commits:u,mergeBaseSha:i,branchSha:o}}async function U(t,n){if(await p(t,`refs/heads/${n}`)||await p(t,n))return n;if(await p(t,`origin/${n}`))return await r(t,n,`origin/${n}`),n;throw new e(`BRANCH_NOT_FOUND`,`Branch '${n}' not found locally or on origin. Run 'git fetch' if the branch exists on the remote.`)}function W(e,t,n,r,i){return{diffHash:v(e),headSha:t,branchSha:n,baseSha:r,sourceHashes:i}}async function G(e,t){if(!e.exec)return;let n=t.includeStaged??!0,r={};if(n)try{r.staged=v(await e.exec([`diff`,`--cached`,`--binary`,`HEAD`]))}catch{}if(t.includeUnstaged)try{r.unstaged=v(await e.exec([`diff`,`--binary`]))}catch{}if(t.includeUntracked)try{let t=(await e.exec([`ls-files`,`--others`,`--exclude-standard`])).trim().split(`
|
|
6
6
|
`).filter(Boolean);if(t.length>0){let n=[];for(let r of t)try{let t=await e.exec([`hash-object`,`--`,r]);n.push(`${r}:${t.trim()}`)}catch{}r.untracked=v(n.join(`
|
|
7
|
-
`))}else r.untracked=v(``)}catch{}return Object.keys(r).length>0?r:void 0}export{j as collectBranchDiffs,P as collectCombinedDiffRange,M as collectCommitRangeDiffs,L as collectCommitRangeWithWorkdir,
|
|
7
|
+
`))}else r.untracked=v(``)}catch{}return Object.keys(r).length>0?r:void 0}export{B as captureExcludedLayersPatch,j as collectBranchDiffs,P as collectCombinedDiffRange,M as collectCommitRangeDiffs,L as collectCommitRangeWithWorkdir,H as collectCommitsForSplit,T as collectFromSource,G as collectSourceHashes,R as collectWorkdirPortionForRange,E as collectWorkingDiffs,V as computeTrackedWorktreeTreeSha,z as resolveWorkdirLayers};
|
package/dist/git/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{collectBranchDiffs as e,collectCommitsForSplit as t,collectFromSource as n,collectWorkingDiffs as r}from"./diff-collector.js";import{applyBranchStructure as i,createStackedBranches as a}from"./apply-branches.js";import{applyCommitChain as o,buildCommitChain as s}from"./commit-chain.js";import{verifyMultiBranchDiff as c,verifyResultingDiff as l,verifySnapshot as u}from"./safety.js";import{applyToTarget as d}from"./apply-target.js";export{i as applyBranchStructure,o as applyCommitChain,d as applyToTarget,s as buildCommitChain,e as collectBranchDiffs,t as collectCommitsForSplit,n as collectFromSource,r as collectWorkingDiffs,a as createStackedBranches,c as verifyMultiBranchDiff,l as verifyResultingDiff,u as verifySnapshot};
|
package/dist/git/safety.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{commitTree as e,diffTree as t,mergeTree as n,revParse as r,stageAll as i,writeTree as a}from"./port-dispatch.js";import{sha256 as o}from"./utils/crypto.js";import{withUntrackedInTempIndex as s}from"./utils/temp-index.js";import{collectSourceHashes as c,collectWorkdirPortionForRange as l,computeTrackedWorktreeTreeSha as u
|
|
1
|
+
import{commitTree as e,diffTree as t,mergeTree as n,revParse as r,stageAll as i,writeTree as a}from"./port-dispatch.js";import{sha256 as o}from"./utils/crypto.js";import{withUntrackedInTempIndex as s}from"./utils/temp-index.js";import{collectSourceHashes as c,collectWorkdirPortionForRange as l,computeTrackedWorktreeTreeSha as u}from"./diff-collector.js";async function d(e,t,n){return t.branchSha&&t.baseSha?n.includeStaged!=null||n.includeUnstaged||n.includeUntracked?p(e,t,n):h(e,t):f(e,t,n)}async function f(e,n,c){let l=await r(e,`HEAD`);if(l!==n.headSha)return{valid:!1,error:`HEAD changed from ${n.headSha} to ${l}`};let d;if(d=c.includeUntracked?await s(e,async n=>(c.includeUnstaged&&await i(e,{trackedOnly:!0,indexEnv:n}),await i(e,{indexEnv:n}),t(e,l,await a(e,{indexEnv:n})))):c.includeUnstaged?await t(e,l,await u(e)):await t(e,l,await a(e)),o(d)!==n.diffHash){let t=await m(e,n,c);return t.length>0?{valid:!1,error:`Working directory ${t.join(` and `)} ${t.length===1?`has`:`have`} changed since the AI analysis`}:n.sourceHashes==null?{valid:!1,error:`Working directory changes have been modified since the AI analysis`}:{valid:!0}}return{valid:!0}}async function p(e,n,i){if(await r(e,n.branchSha)!==n.branchSha)return{valid:!1,error:`Branch has been modified since the AI analysis`};let a=await r(e,`HEAD`);if(a!==n.headSha)return{valid:!1,error:`HEAD changed from ${n.headSha} to ${a}`};let{finalTreeSha:s}=await l(e,a,{includeStaged:i.includeStaged??!0,includeUnstaged:i.includeUnstaged,includeUntracked:i.includeUntracked});if(o(await t(e,n.baseSha,s))!==n.diffHash){let t=await m(e,n,i);return t.length>0?{valid:!1,error:`Working directory ${t.join(` and `)} ${t.length===1?`has`:`have`} changed since the AI analysis`}:n.sourceHashes==null?{valid:!1,error:`Working directory or branch state has drifted since the AI analysis`}:{valid:!0}}return{valid:!0}}async function m(e,t,n){if(!t.sourceHashes)return[];let r=await c(e,{includeStaged:n.includeStaged,includeUnstaged:n.includeUnstaged,includeUntracked:n.includeUntracked});if(!r)return[];let i=[];return t.sourceHashes.staged!=null&&r.staged!==t.sourceHashes.staged&&i.push(`staged changes`),t.sourceHashes.unstaged!=null&&r.unstaged!==t.sourceHashes.unstaged&&i.push(`unstaged changes`),t.sourceHashes.untracked!=null&&r.untracked!==t.sourceHashes.untracked&&i.push(`untracked files`),i}async function h(e,n){return await r(e,n.branchSha)===n.branchSha?o(await t(e,n.baseSha,n.branchSha))===n.diffHash?{valid:!0}:{valid:!1,error:`Branch diff has changed since the AI analysis`}:{valid:!1,error:`Branch has been modified since the AI analysis`}}async function g(e,n,r,i){return o(await t(e,n,r))===i.diffHash?{valid:!0}:{valid:!1,error:`Output diff does not match input — the commit chain does not reproduce the original changes`}}async function _(r,i,a,s){if(a.length===1)return g(r,i,a[0],s);let c=a[0];for(let t=1;t<a.length;t++)c=await e(r,await n(r,c,a[t]),[c,a[t]],`verify merge`);return o(await t(r,i,c))===s.diffHash?{valid:!0}:{valid:!1,error:`Combined diff of all branches does not match the original changes`}}export{_ as verifyMultiBranchDiff,g as verifyResultingDiff,d as verifySnapshot};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { PhaseDetail, ProgressEvent, TokenUsage } from "./ai/types.js";
|
|
1
|
+
import { DuplicateHunksRepair, PhaseDetail, ProgressEvent, RepairScope, RepairedHunkClaim, TokenUsage, ValidationRepair } from "./ai/types.js";
|
|
2
|
+
import { AuthoringContext, AuthoringDependency, AuthoringEdit, AuthoringIssue, AuthoringNote, AuthoringPullRequest } from "./context.js";
|
|
2
3
|
import { AIError, AIErrorCode } from "./errors/ai.js";
|
|
3
4
|
import { GitError, GitErrorCode } from "./errors/git.js";
|
|
4
5
|
import { DiffTooLargeError, TaskInputError } from "./errors/task.js";
|
|
@@ -29,8 +30,9 @@ import { quickCompose } from "./tasks/quick-compose/quick-compose.js";
|
|
|
29
30
|
import { DEFAULT_INPUT_TOKEN_BUDGET } from "./tasks/utils/budget.js";
|
|
30
31
|
import { isNoiseHunk, withDefaultNoiseRedaction } from "./tasks/utils/noise.js";
|
|
31
32
|
import { ApplyComposePlanInput, ApplyComposePlanResult, ComposeApplyPlan, ComposeDepth, ComposeDiffStats, ComposeInput, ComposePlan, ComposePlanInput, ComposePlanResult, ComposeProgressEvent, ComposeResult, ComposeStrategy, applyComposePlan, compose, composePlan, defaultPhaseMessages } from "./workflows/compose.js";
|
|
33
|
+
import { AuthoredCommit, BuildAuthoredComposePlanInput, CollectComposeHunksInput, CollectComposeHunksResult, buildAuthoredComposePlan, buildAuthoredPlanInstructions, collectComposeHunks } from "./workflows/author.js";
|
|
32
34
|
import { RefinePlanInput, RefinePlanResult, RefineProgressEvent, refinePlan } from "./workflows/refine.js";
|
|
33
35
|
import { SplitInput, SplitPlan, SplitProgressEvent, SplitResult, split } from "./workflows/split.js";
|
|
34
36
|
import { UndoComposeInput, ValidateUndoComposeInput, listUndoIds, undoCompose, validateUndoCompose } from "./workflows/undo.js";
|
|
35
37
|
import { UndoForceOptions } from "@gitkraken/shared-tools";
|
|
36
|
-
export { AIError, type AIErrorCode, type AfterModelCallInfo, type AnalysisResult, type AnalyzeInput, type AnalyzeOutput, type ApplyComposePlanInput, type ApplyComposePlanResult, type ApplyPatchOptions, type BeforeModelCallInfo, type BeforePromptInfo, type BranchGroup, type BranchOrdering, type BranchPartition, type CheckoutOptions, type CleanOptions, type CommitDetail, type CommitSuggestion, type CommitTreeOptions, type ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, type ComposeDepth, type ComposeDiffStats, type ComposeGitOps, type ComposeGitPort, ComposeGitPortMissingOpError, type ComposeGroupInput, type ComposeGroupOutput, type ComposeHunk, type ComposeInput, type ComposePlan, type ComposePlanInput, type ComposePlanResult, type ComposeProgressEvent, type ComposeResult, type ComposeSession, type ComposeSource, type ComposeStrategy, type ComposeTarget, type ComposeTaskName, ComposeWorkflowError, type ComposeWorkflowErrorCode, ComposeWorkflowInputError, type Concern, type ConcernDependency, type ConventionDescriptor, DEFAULT_INPUT_TOKEN_BUDGET, type DeleteBranchOptions, DiffTooLargeError, type DiffTreeOptions, type ForEachRefOptions, type GitCommitIdentity, GitError, type GitErrorCode, type GroupInput, type GroupOutput, type GroupingResult, type GroupingStrategy, type HunkAnnotation, type HunkOrderingConstraint, type IndexScopedOptions, type InteriorRefViolation, type LogOptions, type OnAfterModelCall, type OnBeforeModelCall, type OnBeforePrompt, type OpOptions, type OrderInput, type OrderOutput, type OrderingResult, type OverrideSafetyFailures, type PartitionInput, type PartitionOutput, type PartitionResult, type PartitionStrategy, type PhaseDetail, type ProgressEvent, type PromptOverride, type QuickComposeInput, type QuickComposeOutput, type QuickComposeResult, REDACTED_HUNK_CONTENT, type RedactHunkContent, type RefinePlanInput, type RefinePlanResult, type RefineProgressEvent, type SigningConfig, type SplitInput, type SplitPlan, type SplitProgressEvent, type SplitResult, type StageAllOptions, type StashConflict, type StashNotRestored, type StashPushOptions, TaskInputError, type TokenUsage, type UndoComposeInput, type UndoForceOptions, type UpdateRefOptions, type ValidateUndoComposeInput, analyze, applyComposePlan, compose, composeGroup, composePlan, defaultPhaseMessages, detectInteriorRefs, estimatedCharactersPerToken, group, isNoiseHunk, listUndoIds, order, parseUnifiedDiff, partition, quickCompose, refinePlan, split, undoCompose, validateUndoCompose, withDefaultNoiseRedaction };
|
|
38
|
+
export { AIError, type AIErrorCode, type AfterModelCallInfo, type AnalysisResult, type AnalyzeInput, type AnalyzeOutput, type ApplyComposePlanInput, type ApplyComposePlanResult, type ApplyPatchOptions, type AuthoredCommit, type AuthoringContext, type AuthoringDependency, type AuthoringEdit, type AuthoringIssue, type AuthoringNote, type AuthoringPullRequest, type BeforeModelCallInfo, type BeforePromptInfo, type BranchGroup, type BranchOrdering, type BranchPartition, type BuildAuthoredComposePlanInput, type CheckoutOptions, type CleanOptions, type CollectComposeHunksInput, type CollectComposeHunksResult, type CommitDetail, type CommitSuggestion, type CommitTreeOptions, type ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, type ComposeDepth, type ComposeDiffStats, type ComposeGitOps, type ComposeGitPort, ComposeGitPortMissingOpError, type ComposeGroupInput, type ComposeGroupOutput, type ComposeHunk, type ComposeInput, type ComposePlan, type ComposePlanInput, type ComposePlanResult, type ComposeProgressEvent, type ComposeResult, type ComposeSession, type ComposeSource, type ComposeStrategy, type ComposeTarget, type ComposeTaskName, ComposeWorkflowError, type ComposeWorkflowErrorCode, ComposeWorkflowInputError, type Concern, type ConcernDependency, type ConventionDescriptor, DEFAULT_INPUT_TOKEN_BUDGET, type DeleteBranchOptions, DiffTooLargeError, type DiffTreeOptions, type DuplicateHunksRepair, type ForEachRefOptions, type GitCommitIdentity, GitError, type GitErrorCode, type GroupInput, type GroupOutput, type GroupingResult, type GroupingStrategy, type HunkAnnotation, type HunkOrderingConstraint, type IndexScopedOptions, type InteriorRefViolation, type LogOptions, type OnAfterModelCall, type OnBeforeModelCall, type OnBeforePrompt, type OpOptions, type OrderInput, type OrderOutput, type OrderingResult, type OverrideSafetyFailures, type PartitionInput, type PartitionOutput, type PartitionResult, type PartitionStrategy, type PhaseDetail, type ProgressEvent, type PromptOverride, type QuickComposeInput, type QuickComposeOutput, type QuickComposeResult, REDACTED_HUNK_CONTENT, type RedactHunkContent, type RefinePlanInput, type RefinePlanResult, type RefineProgressEvent, type RepairScope, type RepairedHunkClaim, type SigningConfig, type SplitInput, type SplitPlan, type SplitProgressEvent, type SplitResult, type StageAllOptions, type StashConflict, type StashNotRestored, type StashPushOptions, TaskInputError, type TokenUsage, type UndoComposeInput, type UndoForceOptions, type UpdateRefOptions, type ValidateUndoComposeInput, type ValidationRepair, analyze, applyComposePlan, buildAuthoredComposePlan, buildAuthoredPlanInstructions, collectComposeHunks, compose, composeGroup, composePlan, defaultPhaseMessages, detectInteriorRefs, estimatedCharactersPerToken, group, isNoiseHunk, listUndoIds, order, parseUnifiedDiff, partition, quickCompose, refinePlan, split, undoCompose, validateUndoCompose, withDefaultNoiseRedaction };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AIError as e}from"./errors/ai.js";import{GitError as t}from"./errors/git.js";import{DiffTooLargeError as n,TaskInputError as r}from"./errors/task.js";import{ComposeWorkflowError as i,ComposeWorkflowInputError as a}from"./errors/workflow.js";import{ComposeGitPortMissingOpError as o}from"./ports/git.js";import{detectInteriorRefs as s}from"./git/safety-range.js";import{parseUnifiedDiff as c}from"./git/utils/hunk-parser.js";import{estimatedCharactersPerToken as l}from"./tasks/ai-loop.js";import{REDACTED_HUNK_CONTENT as u}from"./tasks/utils/redact.js";import{DEFAULT_INPUT_TOKEN_BUDGET as d}from"./tasks/utils/budget.js";import{analyze as f}from"./tasks/analyze/analyze.js";import{composeGroup as p}from"./tasks/compose-group/compose-group.js";import{group as m}from"./tasks/group/group.js";import{order as h}from"./tasks/order/order.js";import{partition as g}from"./tasks/partition/partition.js";import{quickCompose as _}from"./tasks/quick-compose/quick-compose.js";import{isNoiseHunk as v,withDefaultNoiseRedaction as y}from"./tasks/utils/noise.js";import{applyComposePlan as b,compose as x,composePlan as S,defaultPhaseMessages as C}from"./workflows/compose.js";import{
|
|
1
|
+
import{AIError as e}from"./errors/ai.js";import{GitError as t}from"./errors/git.js";import{DiffTooLargeError as n,TaskInputError as r}from"./errors/task.js";import{ComposeWorkflowError as i,ComposeWorkflowInputError as a}from"./errors/workflow.js";import{ComposeGitPortMissingOpError as o}from"./ports/git.js";import{detectInteriorRefs as s}from"./git/safety-range.js";import{parseUnifiedDiff as c}from"./git/utils/hunk-parser.js";import{estimatedCharactersPerToken as l}from"./tasks/ai-loop.js";import{REDACTED_HUNK_CONTENT as u}from"./tasks/utils/redact.js";import{DEFAULT_INPUT_TOKEN_BUDGET as d}from"./tasks/utils/budget.js";import{analyze as f}from"./tasks/analyze/analyze.js";import{composeGroup as p}from"./tasks/compose-group/compose-group.js";import{group as m}from"./tasks/group/group.js";import{order as h}from"./tasks/order/order.js";import{partition as g}from"./tasks/partition/partition.js";import{quickCompose as _}from"./tasks/quick-compose/quick-compose.js";import{isNoiseHunk as v,withDefaultNoiseRedaction as y}from"./tasks/utils/noise.js";import{applyComposePlan as b,compose as x,composePlan as S,defaultPhaseMessages as C}from"./workflows/compose.js";import{buildAuthoredComposePlan as w,buildAuthoredPlanInstructions as T,collectComposeHunks as E}from"./workflows/author.js";import{refinePlan as D}from"./workflows/refine.js";import{split as O}from"./workflows/split.js";import{listUndoIds as k,undoCompose as A,validateUndoCompose as j}from"./workflows/undo.js";export{e as AIError,o as ComposeGitPortMissingOpError,i as ComposeWorkflowError,a as ComposeWorkflowInputError,d as DEFAULT_INPUT_TOKEN_BUDGET,n as DiffTooLargeError,t as GitError,u as REDACTED_HUNK_CONTENT,r as TaskInputError,f as analyze,b as applyComposePlan,w as buildAuthoredComposePlan,T as buildAuthoredPlanInstructions,E as collectComposeHunks,x as compose,p as composeGroup,S as composePlan,C as defaultPhaseMessages,s as detectInteriorRefs,l as estimatedCharactersPerToken,m as group,v as isNoiseHunk,k as listUndoIds,h as order,c as parseUnifiedDiff,g as partition,_ as quickCompose,D as refinePlan,O as split,A as undoCompose,j as validateUndoCompose,y as withDefaultNoiseRedaction};
|
package/dist/tasks/ai-loop.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AiSession } from "../ai/session.js";
|
|
2
|
-
import { ProgressEvent, TokenUsage } from "../ai/types.js";
|
|
2
|
+
import { ProgressEvent, TokenUsage, ValidationRepair } from "../ai/types.js";
|
|
3
3
|
import { AiModelPort } from "@gitkraken/shared-tools";
|
|
4
4
|
|
|
5
5
|
//#region src/tasks/ai-loop.d.ts
|
|
@@ -30,6 +30,11 @@ interface AfterModelCallInfo {
|
|
|
30
30
|
};
|
|
31
31
|
/** Identifier of the prompt template used for this call, when a consumer override supplied one. */
|
|
32
32
|
promptVersion?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Deterministic repairs applied to this response so it could be accepted instead of retried.
|
|
35
|
+
* Undefined when the response needed no repair (or when the call itself failed).
|
|
36
|
+
*/
|
|
37
|
+
repairs?: ValidationRepair[];
|
|
33
38
|
}
|
|
34
39
|
type OnBeforeModelCall = (info: BeforeModelCallInfo) => void;
|
|
35
40
|
type OnAfterModelCall = (info: AfterModelCallInfo) => void;
|
|
@@ -77,6 +82,8 @@ interface ValidationResult<T> {
|
|
|
77
82
|
result?: T;
|
|
78
83
|
errors?: string[];
|
|
79
84
|
retryFeedback?: string;
|
|
85
|
+
/** Set (non-empty) only when the validator repaired the response to make it valid. */
|
|
86
|
+
repairs?: ValidationRepair[];
|
|
80
87
|
}
|
|
81
88
|
interface AiLoopResult<T> {
|
|
82
89
|
result: T;
|
|
@@ -86,6 +93,8 @@ interface AiLoopResult<T> {
|
|
|
86
93
|
durationMs: number;
|
|
87
94
|
/** Stable call IDs for each model.generate invocation this loop made, in order. */
|
|
88
95
|
callIds: string[];
|
|
96
|
+
/** Repairs applied to the accepted response. Undefined when it needed no repair. */
|
|
97
|
+
repairs?: ValidationRepair[];
|
|
89
98
|
}
|
|
90
99
|
declare function runAiLoop<T>(params: AiLoopParams<T>): Promise<AiLoopResult<T>>;
|
|
91
100
|
declare function extractOutputTag(text: string): string | null;
|