@gitkraken/compose-tools 0.4.0 → 0.6.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/errors/task.d.ts +11 -1
  3. package/dist/errors/task.js +1 -1
  4. package/dist/git/commit-chain.js +3 -4
  5. package/dist/git/reparent.js +3 -4
  6. package/dist/git/safety-range.d.ts +14 -2
  7. package/dist/git/safety-range.js +4 -5
  8. package/dist/git/utils/conventions.d.ts +7 -0
  9. package/dist/git/utils/conventions.js +8 -9
  10. package/dist/git/utils/refs.d.ts +18 -0
  11. package/dist/git/utils/refs.js +2 -0
  12. package/dist/index.d.ts +6 -3
  13. package/dist/index.js +1 -1
  14. package/dist/tasks/analyze/analyze.js +1 -1
  15. package/dist/tasks/analyze/prompt.d.ts +4 -1
  16. package/dist/tasks/analyze/prompt.js +5 -5
  17. package/dist/tasks/analyze/schema.d.ts +127 -36
  18. package/dist/tasks/analyze/schema.js +1 -1
  19. package/dist/tasks/analyze/types.d.ts +8 -0
  20. package/dist/tasks/analyze/validator.js +2 -2
  21. package/dist/tasks/compose-group/compose-group.js +1 -1
  22. package/dist/tasks/compose-group/prompt.d.ts +4 -1
  23. package/dist/tasks/compose-group/prompt.js +23 -29
  24. package/dist/tasks/compose-group/types.d.ts +5 -0
  25. package/dist/tasks/group/group.js +1 -1
  26. package/dist/tasks/group/prompt.d.ts +4 -1
  27. package/dist/tasks/group/prompt.js +15 -21
  28. package/dist/tasks/group/schema.d.ts +89 -17
  29. package/dist/tasks/group/schema.js +1 -1
  30. package/dist/tasks/group/types.d.ts +8 -0
  31. package/dist/tasks/group/validator.js +6 -6
  32. package/dist/tasks/order/prompt.js +5 -5
  33. package/dist/tasks/order/schema.d.ts +35 -10
  34. package/dist/tasks/order/schema.js +1 -1
  35. package/dist/tasks/order/validator.js +2 -2
  36. package/dist/tasks/partition/prompt.js +9 -9
  37. package/dist/tasks/partition/schema.d.ts +46 -12
  38. package/dist/tasks/partition/schema.js +1 -1
  39. package/dist/tasks/partition/validator.js +2 -2
  40. package/dist/tasks/quick-compose/prompt.js +8 -9
  41. package/dist/tasks/quick-compose/schema.d.ts +142 -25
  42. package/dist/tasks/quick-compose/schema.js +1 -1
  43. package/dist/tasks/quick-compose/validator.js +5 -5
  44. package/dist/tasks/refine/refine.js +1 -1
  45. package/dist/tasks/refine/types.d.ts +14 -0
  46. package/dist/tasks/refine/validator.d.ts +5 -1
  47. package/dist/tasks/refine/validator.js +1 -1
  48. package/dist/tasks/utils/budget.d.ts +40 -0
  49. package/dist/tasks/utils/budget.js +1 -0
  50. package/dist/tasks/utils/json-schema.d.ts +61 -0
  51. package/dist/tasks/utils/json-schema.js +1 -0
  52. package/dist/tasks/utils/noise.d.ts +21 -0
  53. package/dist/tasks/utils/noise.js +2 -0
  54. package/dist/tasks/utils/redact.d.ts +7 -1
  55. package/dist/tasks/utils/redact.js +1 -1
  56. package/dist/workflows/compose.d.ts +47 -2
  57. package/dist/workflows/compose.js +3 -3
  58. package/dist/workflows/refine.d.ts +9 -1
  59. package/dist/workflows/refine.js +1 -1
  60. package/dist/workflows/split.js +3 -3
  61. package/dist/workflows/types.d.ts +14 -1
  62. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.0] - 2026-07-24
10
+
11
+ ### Added
12
+
13
+ - Adds `inputTokenBudget` on `ComposeInput`, `ComposePlanInput`, and the hunk-bearing task inputs (`AnalyzeInput`, `GroupInput`, `ComposeGroupInput`) — a ceiling on estimated input tokens for each hunk-bearing prompt, defaulting to the exported `DEFAULT_INPUT_TOKEN_BUDGET` (180k). When a prompt would exceed the budget, hunks are progressively condensed — biggest-first — to metadata only (index, file name, hunk header, line counts); a diff that cannot fit even fully condensed throws the new `DiffTooLargeError` (exported), which carries `estimatedTokens` and `budgetTokens` so consumers can surface an "exclude generated files or commit in parts" hint. New `hunksCondensedCount` fields on `ComposeResult` / `ComposePlanResult` (and on `AnalyzeOutput` / `GroupOutput`) report how many hunks were condensed — non-zero means the AI reasoned over less than the full diff and grouping quality may degrade.
14
+ - Exports `isNoiseHunk` and `withDefaultNoiseRedaction` — the built-in noise detector behind the new on-by-default redaction (see Changed). Flags dependency lockfiles by basename (`package-lock.json`, `pnpm-lock.yaml`, `cargo.lock`, ...), vendored/build path segments (`node_modules`, `vendor`, `dist`, `__generated__`, ...), minified/source-map/generated filename patterns, codegen body markers, and binary hunks. `withDefaultNoiseRedaction` combines the detector with an optional consumer predicate for reuse in custom task-level flows.
15
+
16
+ ### Changed
17
+
18
+ - AI prompts now redact noise-hunk bodies by default — the built-in noise detector is combined (OR) with any consumer `redactHunkContent`, so a caller predicate extends the default rather than replacing it, and matching hunks reach the AI as metadata plus a placeholder instead of full patch text. Prevents pathological diffs (lockfile- or generated-content-dominated) from sending megabytes of zero-signal content verbatim. Opt out with the new `disableDefaultNoiseReduction` on `ComposeInput` / `ComposePlanInput`; audit what was masked via the new `hunksRedactedCount` on `ComposeResult` / `ComposePlanResult` (undefined when no redaction predicate ran).
19
+ - Removed `zod` as a runtime dependency. Task output schemas are now hand-written JSON Schema literals validated by a small internal validator; the JSON Schema text embedded in prompts is byte-identical to what `zod` previously emitted (snapshot-enforced). No public API change.
20
+
21
+ ### Fixed
22
+
23
+ - Commit-convention detection no longer degrades generated commit messages to bare subject lines. Detection previously sampled only commit subjects, so it never learned whether a repository writes descriptive bodies, and the detected conventions then replaced the default full-message instruction in the `group` / `compose-group` / `quick-compose` prompts. Convention gathering now samples commit bodies as well and records a `bodyStyle` on `CommitConventions`; the prompt builders always keep the summary-and-body structure and let detected conventions augment it rather than replace it. Full messages are the default and collapse to a single subject line only when the repository's history clearly favors one-liners.
24
+
25
+ ## [0.5.0] - 2026-07-16
26
+
27
+ ### Added
28
+
29
+ - Adds `overrideSafetyFailures` on `ComposeInput`, `ComposePlanInput`, and `ApplyComposePlanInput` — an optional object for opting out of individual safety checks. Its only flag, `interiorRefs`, lets a range rewrite proceed even when branches or tags point at commits inside the rewritten range (those refs are left in place and diverge from the recomposed history rather than being blocked). The interior-fork check still always runs. Type export: `OverrideSafetyFailures`.
30
+ - Exports `detectInteriorRefs` — reports the branches, tags, or detached HEAD pointing at commits inside the range a rewrite of a source would replace, so consumers can warn before composing with the `interiorRefs` override. Type export: `InteriorRefViolation`.
31
+
32
+ ### Fixed
33
+
34
+ - `refinePlan` now repairs locked-commit ordering the model may have shuffled during a refine.
35
+ - The rewrite-safety message no longer says interior refs would be "orphaned" — they keep their commits and simply don't follow the recomposed history.
36
+ - Interior-ref detection and rewrite-time ref moves now resolve annotated tags to their target commit (via `%(*objectname)`) instead of comparing the tag object's SHA and missing them.
37
+
9
38
  ## [0.4.0] - 2026-06-26
10
39
 
11
40
  ### Added
@@ -2,5 +2,15 @@
2
2
  declare class TaskInputError extends Error {
3
3
  constructor(message: string);
4
4
  }
5
+ /**
6
+ * Thrown when a diff is too large to fit the input-token budget even after every hunk
7
+ * has been reduced to metadata-only. Consumers should surface a "commit in parts /
8
+ * exclude generated files" hint. Carries the estimate and the budget it exceeded.
9
+ */
10
+ declare class DiffTooLargeError extends Error {
11
+ readonly estimatedTokens: number;
12
+ readonly budgetTokens: number;
13
+ constructor(estimatedTokens: number, budgetTokens: number);
14
+ }
5
15
  //#endregion
6
- export { TaskInputError };
16
+ export { DiffTooLargeError, TaskInputError };
@@ -1 +1 @@
1
- var e=class extends Error{constructor(e){super(e),this.name=`TaskInputError`}};export{e as TaskInputError};
1
+ var e=class extends Error{constructor(e){super(e),this.name=`TaskInputError`}},t=class extends Error{estimatedTokens;budgetTokens;constructor(e,t){super(`Diff too large to compose: an estimated ${e} input tokens exceeds the ${t}-token budget even after reducing every hunk to metadata. Exclude generated files or commit in parts.`),this.name=`DiffTooLargeError`,this.estimatedTokens=e,this.budgetTokens=t}};export{t as DiffTooLargeError,e as TaskInputError};
@@ -1,7 +1,6 @@
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,forEachRef as o,localeNeutralEnv as s,readTree as c,reset as l,revParse as u,stageAll as d,stashDrop as f,stashPop as p,stashPush as m,statusPorcelain as h,updateRef as g,writeTree as _}from"./port-dispatch.js";import{reparentDescendants as v}from"./reparent.js";import{buildPatchForCommit as y}from"./utils/patch-builder.js";import{randomUUID as b}from"node:crypto";import*as x from"node:fs";import*as S from"node:os";import*as C from"node:path";async function w(n){let{git:r,baseSha:a,commits:o,hunks:s,signing:l,authorAttribution:u=`plurality`}=n,d=C.join(S.tmpdir(),`compose-idx-${b()}`),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=y(a.map(e=>m.has(e.diffHeader)?{...e,diffHeader:B(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 _(r,{indexEnv:h}),v=I(a,u),b=R(n.message,v.coAuthors),x=await i(r,g,[f],b,{signing:l,author:v.author});p.set(n.id,x),f=x}return{headSha:f,shaMap:p}}finally{z(d)}}async function T(e){let{git:t,newHeadSha:i,branchName:a,stashLabel:o,leftoverPatch:s}=e,c=o??O,u=await k(t,c);try{a?await r(t,a,{createBranch:!0,startPoint:i}):await l(t,i,`hard`),s&&await n(t,s)}catch(e){throw u!=null&&await j(t,u,c),e}if(u!=null){let e=await j(t,u,c);if(e.conflicted)return{stashConflict:{stashLabel:c,conflictedFiles:e.conflictedFiles}}}return{}}async function E(e){let{git:t,branch:i,rangeTo:o,newHeadSha:s,originalBranchSha:c,stashLabel:u,leftoverPatch:d,includeWorkdirActive:f}=e,p=await a(t)===i,m=u??O,h=p&&!f?await k(t,m):null,_,y=s,b=[];try{let e=await v({git:t,oldToSha:o,newTopSha:s,sourceBranchName:i});for(let t of e.reparentedRefs)b.push({refname:t.refname,previousSha:t.oldSha,newSha:t.newSha});e.sourceBranchNewSha==null?(await g(t,`refs/heads/${i}`,s),b.push({refname:`refs/heads/${i}`,previousSha:c,newSha:s}),y=s):y=e.sourceBranchNewSha;let a=await D(t,o,s,`refs/heads/${i}`);b.push(...a),p&&(await r(t,i),await l(t,y,`hard`),h==null&&d&&await n(t,d))}catch(e){throw h!=null&&await j(t,h,m),e}if(h!=null){let e=await j(t,h,m);e.conflicted&&(_={stashLabel:m,conflictedFiles:e.conflictedFiles})}return{finalTipSha:y,refMovements:b,stashConflict:_}}async function D(e,t,n,r){let i=await o(e,{format:`%(refname) %(objectname)`,pointsAt:t}),a=[];for(let o of i.split(`
3
- `)){let i=o.trim();if(!i)continue;let s=i.indexOf(` `);if(s<0)continue;let c=i.slice(0,s),l=i.slice(s+1).trim();c!==r&&(!c.startsWith(`refs/heads/`)&&!c.startsWith(`refs/tags/`)||l===t&&(await g(e,c,n),a.push({refname:c,previousSha:t,newSha:n})))}return a}const O=`compose backup`;async function k(e,t){if(!(await h(e)).trim())return null;await m(e,{message:t,includeUntracked:!0});try{let t=(await u(e,`stash@{0}`)).trim();return t.length>0?t:null}catch{return null}}async function A(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 j(e,t,n){try{let n=(await u(e,`${t}^{tree}`)).trim(),r=await M(e);if(n.length>0&&n===r){let n=await A(e,t);if(n!=null)try{await f(e,n)}catch{}return{conflicted:!1,conflictedFiles:[]}}}catch{}let r=await A(e,t);if(r==null)return{conflicted:!0,conflictedFiles:[]};let i;try{return await p(e,{withIndex:!0,ref:r}),{conflicted:!1,conflictedFiles:[]}}catch(e){i=e}let a=await F(e);if(a.length>0)return{conflicted:!0,conflictedFiles:a};if(N(i)){let n=await A(e,t);if(n!=null)try{await f(e,n)}catch{}return{conflicted:!1,conflictedFiles:[]}}return{conflicted:!0,conflictedFiles:[]}}async function M(e){let t=C.join(S.tmpdir(),`compose-equiv-${b()}`);try{let n={GIT_INDEX_FILE:t};return await c(e,`HEAD`,{indexEnv:n}),await d(e,{indexEnv:n}),(await _(e,{indexEnv:n})).trim()}finally{z(t)}}function N(e){let t=P(e);return t.includes(`already exists, no checkout`)||t.includes(`could not restore untracked files from stash`)}function P(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 F(e){try{let t=await h(e),n=[];for(let e of t.split(`
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,localeNeutralEnv as o,readTree as s,reset as c,revParse as l,stageAll as u,stashDrop as d,stashPop as f,stashPush as p,statusPorcelain as m,updateRef as h,writeTree as g}from"./port-dispatch.js";import{listRefsWithPeeledSha as _}from"./utils/refs.js";import{reparentDescendants as v}from"./reparent.js";import{buildPatchForCommit as y}from"./utils/patch-builder.js";import{randomUUID as b}from"node:crypto";import*as x from"node:fs";import*as S from"node:os";import*as C from"node:path";async function w(n){let{git:r,baseSha:a,commits:o,hunks:c,signing:l,authorAttribution:u=`plurality`}=n,d=C.join(S.tmpdir(),`compose-idx-${b()}`),f=a,p=new Map,m=new Set;try{for(let n of o){let a=n.hunkIndices.map(t=>{let n=c.find(e=>e.index===t);if(!n)throw new e(`OPERATION_FAILED`,`Hunk index ${t} not found`);return n}),o=y(a.map(e=>m.has(e.diffHeader)?{...e,diffHeader:B(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 s(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 _=await g(r,{indexEnv:h}),v=I(a,u),b=R(n.message,v.coAuthors),x=await i(r,_,[f],b,{signing:l,author:v.author});p.set(n.id,x),f=x}return{headSha:f,shaMap:p}}finally{z(d)}}async function T(e){let{git:t,newHeadSha:i,branchName:a,stashLabel:o,leftoverPatch:s}=e,l=o??O,u=await k(t,l);try{a?await r(t,a,{createBranch:!0,startPoint:i}):await c(t,i,`hard`),s&&await n(t,s)}catch(e){throw u!=null&&await j(t,u,l),e}if(u!=null){let e=await j(t,u,l);if(e.conflicted)return{stashConflict:{stashLabel:l,conflictedFiles:e.conflictedFiles}}}return{}}async function E(e){let{git:t,branch:i,rangeTo:o,newHeadSha:s,originalBranchSha:l,stashLabel:u,leftoverPatch:d,includeWorkdirActive:f}=e,p=await a(t)===i,m=u??O,g=p&&!f?await k(t,m):null,_,y=s,b=[];try{let e=await v({git:t,oldToSha:o,newTopSha:s,sourceBranchName:i});for(let t of e.reparentedRefs)b.push({refname:t.refname,previousSha:t.oldSha,newSha:t.newSha});e.sourceBranchNewSha==null?(await h(t,`refs/heads/${i}`,s),b.push({refname:`refs/heads/${i}`,previousSha:l,newSha:s}),y=s):y=e.sourceBranchNewSha;let a=await D(t,o,s,`refs/heads/${i}`);b.push(...a),p&&(await r(t,i),await c(t,y,`hard`),g==null&&d&&await n(t,d))}catch(e){throw g!=null&&await j(t,g,m),e}if(g!=null){let e=await j(t,g,m);e.conflicted&&(_={stashLabel:m,conflictedFiles:e.conflictedFiles})}return{finalTipSha:y,refMovements:b,stashConflict:_}}async function D(e,t,n,r){let i=[];for(let{refname:a,sha:o}of await _(e,{pointsAt:t}))a!==r&&(!a.startsWith(`refs/heads/`)&&!a.startsWith(`refs/tags/`)||o===t&&(await h(e,a,n),i.push({refname:a,previousSha:t,newSha:n})));return i}const O=`compose backup`;async function k(e,t){if(!(await m(e)).trim())return null;await p(e,{message:t,includeUntracked:!0});try{let t=(await l(e,`stash@{0}`)).trim();return t.length>0?t:null}catch{return null}}async function A(e,t){if(!e.exec)return null;let n;try{n=await e.exec([`stash`,`list`,`--format=%H %gd`],{env:o})}catch{return null}for(let e of n.split(`
3
+ `)){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 j(e,t,n){try{let n=(await l(e,`${t}^{tree}`)).trim(),r=await M(e);if(n.length>0&&n===r){let n=await A(e,t);if(n!=null)try{await d(e,n)}catch{}return{conflicted:!1,conflictedFiles:[]}}}catch{}let r=await A(e,t);if(r==null)return{conflicted:!0,conflictedFiles:[]};let i;try{return await f(e,{withIndex:!0,ref:r}),{conflicted:!1,conflictedFiles:[]}}catch(e){i=e}let a=await F(e);if(a.length>0)return{conflicted:!0,conflictedFiles:a};if(N(i)){let n=await A(e,t);if(n!=null)try{await d(e,n)}catch{}return{conflicted:!1,conflictedFiles:[]}}return{conflicted:!0,conflictedFiles:[]}}async function M(e){let t=C.join(S.tmpdir(),`compose-equiv-${b()}`);try{let n={GIT_INDEX_FILE:t};return await s(e,`HEAD`,{indexEnv:n}),await u(e,{indexEnv:n}),(await g(e,{indexEnv:n})).trim()}finally{z(t)}}function N(e){let t=P(e);return t.includes(`already exists, no checkout`)||t.includes(`could not restore untracked files from stash`)}function P(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 F(e){try{let t=await m(e),n=[];for(let e of t.split(`
5
4
  `))/^(UU|AA|DD|AU|UA|UD|DU) /.test(e)&&n.push(e.slice(3).trim());return n}catch{return[]}}function I(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=L(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(L(t.author),t.author);for(let e of t.coAuthors??[])i.set(L(e),e)}let a=r?L(r):null,o=[];for(let[e,t]of i)e!==a&&o.push(t);return{author:r,coAuthors:o}}function L(e){return`${e.name.toLowerCase()}\0${e.email.toLowerCase()}`}function R(e,t){if(t.length===0)return e;let n=t.map(e=>`Co-authored-by: ${e.name} <${e.email}>`).join(`
6
5
  `);return`${e.trimEnd()}\n\n${n}\n`}function z(e){try{x.unlinkSync(e)}catch{}}function B(e,t){let n=e.split(`
7
6
  `),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(`
@@ -1,5 +1,4 @@
1
- import{GitError as e}from"../errors/git.js";import{commitTree as t,forEachRef as n,log as r,mergeBaseIsAncestor as i,updateRef as a}from"./port-dispatch.js";async function o(n){let{git:r,oldToSha:i,newTopSha:o,sourceBranchName:u,signing:d}=n,f=await s(r,i);if(f.length===0)return{reparentedRefs:[]};let p=await c(r,i,f.map(e=>e.sha)),m=new Map;m.set(i,o);for(let e of p){if(m.has(e))continue;let n=await l(r,e),i=n.parents.map(e=>m.get(e)??e),a=await t(r,n.tree,i,n.message,{author:n.author,signing:d});m.set(e,a)}let h=u?`refs/heads/${u}`:void 0,g,_=[];for(let t of f){let n=m.get(t.sha);if(n==null)throw new e(`OPERATION_FAILED`,`Internal error during descendant re-parenting: ref ${t.refname} at ${t.sha.slice(0,8)} did not appear in the downstream walk.`);await a(r,t.refname,n),_.push({refname:t.refname,oldSha:t.sha,newSha:n}),h!=null&&t.refname===h&&(g=n)}return{reparentedRefs:_,sourceBranchNewSha:g}}async function s(e,t){let r=await n(e,{format:`%(refname) %(objectname)`}),a=[];for(let n of r.split(`
2
- `)){let r=n.trim();if(!r)continue;let o=r.indexOf(` `);if(o<0)continue;let s=r.slice(0,o),c=r.slice(o+1).trim();!s.startsWith(`refs/heads/`)&&!s.startsWith(`refs/tags/`)||c!==t&&await i(e,t,c)&&a.push({refname:s,sha:c})}return a}async function c(e,t,n){return(await r(e,{format:`%H`,range:`${n.join(` `)} ^${t}`,reverse:!0,topoOrder:!0})).trim().split(`
3
- `).filter(Boolean)}async function l(t,n){let i=(await r(t,{format:`%T%n%P%n%an%n%ae%n%aI%n%B`,sha:n})).split(`
4
- `);if(i.length<5)throw new e(`OPERATION_FAILED`,`Unable to read commit info for ${n.slice(0,8)}`);let a=i[0].trim(),o=i[1].trim(),s=o?o.split(/\s+/):[],c=i[2],l=i[3],u=i[4],d=i.slice(5).join(`
1
+ import{GitError as e}from"../errors/git.js";import{commitTree as t,log as n,mergeBaseIsAncestor as r,updateRef as i}from"./port-dispatch.js";import{listRefsWithPeeledSha as a}from"./utils/refs.js";async function o(n){let{git:r,oldToSha:a,newTopSha:o,sourceBranchName:u,signing:d}=n,f=await s(r,a);if(f.length===0)return{reparentedRefs:[]};let p=await c(r,a,f.map(e=>e.sha)),m=new Map;m.set(a,o);for(let e of p){if(m.has(e))continue;let n=await l(r,e),i=n.parents.map(e=>m.get(e)??e),a=await t(r,n.tree,i,n.message,{author:n.author,signing:d});m.set(e,a)}let h=u?`refs/heads/${u}`:void 0,g,_=[];for(let t of f){let n=m.get(t.sha);if(n==null)throw new e(`OPERATION_FAILED`,`Internal error during descendant re-parenting: ref ${t.refname} at ${t.sha.slice(0,8)} did not appear in the downstream walk.`);await i(r,t.refname,n),_.push({refname:t.refname,oldSha:t.sha,newSha:n}),h!=null&&t.refname===h&&(g=n)}return{reparentedRefs:_,sourceBranchNewSha:g}}async function s(e,t){let n=[];for(let{refname:i,sha:o}of await a(e))!i.startsWith(`refs/heads/`)&&!i.startsWith(`refs/tags/`)||o!==t&&await r(e,t,o)&&n.push({refname:i,sha:o});return n}async function c(e,t,r){return(await n(e,{format:`%H`,range:`${r.join(` `)} ^${t}`,reverse:!0,topoOrder:!0})).trim().split(`
2
+ `).filter(Boolean)}async function l(t,r){let i=(await n(t,{format:`%T%n%P%n%an%n%ae%n%aI%n%B`,sha:r})).split(`
3
+ `);if(i.length<5)throw new e(`OPERATION_FAILED`,`Unable to read commit info for ${r.slice(0,8)}`);let a=i[0].trim(),o=i[1].trim(),s=o?o.split(/\s+/):[],c=i[2],l=i[3],u=i[4],d=i.slice(5).join(`
5
4
  `).trimEnd(),f=u?new Date(u):void 0;return{tree:a,parents:s,author:{name:c,email:l,date:f&&!Number.isNaN(f.getTime())?f:void 0},message:d}}export{o as reparentDescendants};
@@ -1,5 +1,5 @@
1
1
  import { ComposeGitPort } from "../ports/git.js";
2
- import { ComposeSource, ComposeTarget } from "../workflows/types.js";
2
+ import { ComposeSource, ComposeTarget, OverrideSafetyFailures } from "../workflows/types.js";
3
3
 
4
4
  //#region src/git/safety-range.d.ts
5
5
  interface InteriorForkViolation {
@@ -19,6 +19,8 @@ interface ValidateRewriteRangeSafetyParams {
19
19
  git: ComposeGitPort;
20
20
  source: ComposeSource;
21
21
  target: ComposeTarget;
22
+ /** Opt-out switches for individual checks. When `interiorRefs` is set, the interior-ref check is skipped. */
23
+ overrides?: OverrideSafetyFailures;
22
24
  }
23
25
  /**
24
26
  * Run the interior-fork and interior-ref checks against the source range.
@@ -27,7 +29,17 @@ interface ValidateRewriteRangeSafetyParams {
27
29
  *
28
30
  * No-op when the target doesn't rewrite history (`head`, `branch` with
29
31
  * `preserveOriginal: true`, `auto` with `preserveOriginal: true`).
32
+ *
33
+ * `overrides.interiorRefs` suppresses the interior-ref check only; the
34
+ * interior-fork check always runs (its violations can truly orphan commits).
30
35
  */
31
36
  declare function validateRewriteRangeSafety(params: ValidateRewriteRangeSafetyParams): Promise<void>;
37
+ /**
38
+ * Report the refs (branches, tags, or detached HEAD) pointing at commits inside the range a rewrite
39
+ * of `source` would replace — the interior-ref violations {@link validateRewriteRangeSafety} would
40
+ * raise, returned instead of thrown so a consumer can warn before composing with the `interiorRefs`
41
+ * override set. Empty when the source carries no range (e.g. workdir).
42
+ */
43
+ declare function detectInteriorRefs(git: ComposeGitPort, source: ComposeSource): Promise<InteriorRefViolation[]>;
32
44
  //#endregion
33
- export { InteriorForkViolation, InteriorRefViolation, RewriteSafetyViolation, ValidateRewriteRangeSafetyParams, validateRewriteRangeSafety };
45
+ export { InteriorForkViolation, InteriorRefViolation, RewriteSafetyViolation, ValidateRewriteRangeSafetyParams, detectInteriorRefs, validateRewriteRangeSafety };
@@ -1,5 +1,4 @@
1
- import{ComposeWorkflowInputError as e}from"../errors/workflow.js";import{currentBranch as t,forEachRef as n,log as r,mergeBase as i,mergeBaseIsAncestor as a,revListChildren as o,revParse as s}from"./port-dispatch.js";async function c(t){let{git:n,source:r,target:i}=t;if(!l(r,i))return;let o=await u(n,r);if(o==null)return;let{sourceBranchName:s,parentOfFromSha:c,toSha:m}=o;if(!await a(n,c,m))return;let h=[],g=await d(n,c,m);h.push(...g);let _=await f(n,c,m,s);if(h.push(..._),h.length>0)throw new e(p(h))}function l(e,t){return t.type===`rewrite-range`?!0:(t.type===`branch`||t.type===`auto`)&&e.type===`branch`?!t.preserveOriginal:!1}async function u(t,n){if(n.type===`workdir`)return null;if(n.type===`branch`){let e=n.mergeTarget??`main`,r=await s(t,n.name),a=await i(t,await s(t,e),r);return{sourceBranchName:n.name,parentOfFromSha:a,toSha:r}}let r=await s(t,n.from),a=await s(t,n.to),o;try{o=await s(t,`${r}^`)}catch{throw new e(`Cannot rewrite commit ${r.slice(0,8)} — it has no parent (root commit). Rewriting the root commit is not currently supported.`)}return{sourceBranchName:n.branch,parentOfFromSha:o,toSha:a}}async function d(e,t,n){let i=await r(e,{format:`%H`,range:`${t}..${n}`}),a=new Set(i.trim().split(`
2
- `).filter(Boolean)),s=await o(e,{range:`${t}..${n}`}),c=[];for(let e of s){if(e.sha===n||!a.has(e.sha))continue;if(e.children.length!==1){c.push({kind:`interior-fork`,sha:e.sha,children:e.children,reason:`multiple-children`});continue}let t=e.children[0];a.has(t)||c.push({kind:`interior-fork`,sha:e.sha,children:e.children,reason:`child-outside-range`})}return c}async function f(e,i,a,o){let c=await r(e,{format:`%H`,range:`${i}..${a}`}),l=new Set(c.trim().split(`
3
- `).filter(Boolean));l.delete(a);let u=await n(e,{format:`%(refname) %(objectname)`}),d=`refs/heads/${o}`,f=[];for(let e of u.split(`
4
- `)){let t=e.trim();if(!t)continue;let n=t.indexOf(` `);if(n<0)continue;let r=t.slice(0,n),i=t.slice(n+1).trim();r!==d&&(!r.startsWith(`refs/heads/`)&&!r.startsWith(`refs/tags/`)||l.has(i)&&f.push({kind:`interior-ref`,refname:r,sha:i}))}if(await t(e)==null){let t=await s(e,`HEAD`);l.has(t)&&f.push({kind:`interior-ref`,refname:`HEAD (detached)`,sha:t})}return f}function p(e){let t=[`Cannot safely rewrite the source range — applying the rewrite would orphan refs or lose history outside the range.`,``],n=e.filter(e=>e.kind===`interior-fork`),r=e.filter(e=>e.kind===`interior-ref`);if(n.length>0){t.push(`Interior fork(s) detected — every commit in the rewritten range (except the tip) must have exactly one child within the range:`);for(let e of n){let n=e.children.length>0?e.children.map(e=>e.slice(0,8)).join(`, `):`(none)`,r=e.reason===`multiple-children`?`has ${e.children.length} children`:`has a child that is outside the rewritten range`;t.push(` - ${e.sha.slice(0,8)}: ${r} (${n})`)}t.push(``)}if(r.length>0){t.push(`Refs pointing at interior commits — these would be orphaned by the rewrite:`);for(let e of r)t.push(` - ${e.refname} → ${e.sha.slice(0,8)}`);t.push(``),t.push(`To proceed: delete the offending refs (or move them outside the range), or check out a stable ref if HEAD is detached at an interior commit, then re-run.`)}return t.join(`
5
- `)}export{c as validateRewriteRangeSafety};
1
+ import{ComposeWorkflowInputError as e}from"../errors/workflow.js";import{currentBranch as t,log as n,mergeBase as r,mergeBaseIsAncestor as i,revListChildren as a,revParse as o}from"./port-dispatch.js";import{listRefsWithPeeledSha as s}from"./utils/refs.js";async function c(t){let{git:n,source:r,target:a,overrides:o}=t;if(!u(r,a))return;let s=await d(n,r);if(s==null)return;let{sourceBranchName:c,parentOfFromSha:l,toSha:h}=s;if(!await i(n,l,h))return;let g=[],_=await f(n,l,h);if(g.push(..._),!o?.interiorRefs){let e=await p(n,l,h,c);g.push(...e)}if(g.length>0)throw new e(m(g))}async function l(e,t){let n=await d(e,t);return n==null?[]:p(e,n.parentOfFromSha,n.toSha,n.sourceBranchName)}function u(e,t){return t.type===`rewrite-range`?!0:(t.type===`branch`||t.type===`auto`)&&e.type===`branch`?!t.preserveOriginal:!1}async function d(t,n){if(n.type===`workdir`)return null;if(n.type===`branch`){let e=n.mergeTarget??`main`,i=await o(t,n.name),a=await r(t,await o(t,e),i);return{sourceBranchName:n.name,parentOfFromSha:a,toSha:i}}let i=await o(t,n.from),a=await o(t,n.to),s;try{s=await o(t,`${i}^`)}catch{throw new e(`Cannot rewrite commit ${i.slice(0,8)} — it has no parent (root commit). Rewriting the root commit is not currently supported.`)}return{sourceBranchName:n.branch,parentOfFromSha:s,toSha:a}}async function f(e,t,r){let i=await n(e,{format:`%H`,range:`${t}..${r}`}),o=new Set(i.trim().split(`
2
+ `).filter(Boolean)),s=await a(e,{range:`${t}..${r}`}),c=[];for(let e of s){if(e.sha===r||!o.has(e.sha))continue;if(e.children.length!==1){c.push({kind:`interior-fork`,sha:e.sha,children:e.children,reason:`multiple-children`});continue}let t=e.children[0];o.has(t)||c.push({kind:`interior-fork`,sha:e.sha,children:e.children,reason:`child-outside-range`})}return c}async function p(e,r,i,a){let c=await n(e,{format:`%H`,range:`${r}..${i}`}),l=new Set(c.trim().split(`
3
+ `).filter(Boolean));l.delete(i);let u=`refs/heads/${a}`,d=[];for(let{refname:t,sha:n}of await s(e))t!==u&&(!t.startsWith(`refs/heads/`)&&!t.startsWith(`refs/tags/`)||l.has(n)&&d.push({kind:`interior-ref`,refname:t,sha:n}));if(await t(e)==null){let t=await o(e,`HEAD`);l.has(t)&&d.push({kind:`interior-ref`,refname:`HEAD (detached)`,sha:t})}return d}function m(e){let t=[`Cannot safely rewrite the source range — applying the rewrite would orphan refs or lose history outside the range.`,``],n=e.filter(e=>e.kind===`interior-fork`),r=e.filter(e=>e.kind===`interior-ref`);if(n.length>0){t.push(`Interior fork(s) detected — every commit in the rewritten range (except the tip) must have exactly one child within the range:`);for(let e of n){let n=e.children.length>0?e.children.map(e=>e.slice(0,8)).join(`, `):`(none)`,r=e.reason===`multiple-children`?`has ${e.children.length} children`:`has a child that is outside the rewritten range`;t.push(` - ${e.sha.slice(0,8)}: ${r} (${n})`)}t.push(``)}if(r.length>0){t.push(`Refs pointing at interior commits — these would have deviating history and would not follow the new commits after the rewrite:`);for(let e of r)t.push(` - ${e.refname} → ${e.sha.slice(0,8)}`);t.push(``),t.push(`To proceed: delete the offending refs (or move them outside the range), or check out a stable ref if HEAD is detached at an interior commit, then re-run.`)}return t.join(`
4
+ `)}export{l as detectInteriorRefs,c as validateRewriteRangeSafety};
@@ -7,6 +7,13 @@ interface CommitConventions {
7
7
  scopeStyle?: 'parenthesized' | 'none';
8
8
  casing?: 'lowercase' | 'capitalized';
9
9
  issueRefPattern?: string;
10
+ /**
11
+ * Whether the repository's commits typically carry a descriptive body.
12
+ * `'detailed'` — most commits include a body; `'minimal'` — commits are
13
+ * almost always a lone subject line. `undefined` when the sample is too
14
+ * small or mixed to call, which callers treat as "write full messages".
15
+ */
16
+ bodyStyle?: 'detailed' | 'minimal';
10
17
  samples: string[];
11
18
  }
12
19
  interface BranchConventions {
@@ -1,10 +1,9 @@
1
- import{forEachRef as e,getConfig as t,log as n,showFile as r}from"../port-dispatch.js";const i=/^(feat|fix|refactor|chore|docs|style|test|perf|ci|build|revert)(\(.+?\))?!?:\s/,a=[{re:/\(#\d+\)/,label:`(#NNN)`},{re:/#\d+/,label:`#NNN`},{re:/[A-Z]{2,}-\d+/,label:`PROJ-NNN`}],o=/^(feat|fix|chore|feature|hotfix|bugfix|refactor|release|support)\//,s=/^(\d+)-|^([A-Z]{2,}-\d+)-/,c=new Set([`main`,`master`,`develop`,`development`,`staging`,`production`]);async function l(e){let{git:t,commitLimit:n=30,branchLimit:r=15}=e,i=[],a=e.author??await T(t),[o,s,c]=await Promise.all([u(t,i),f(t,n,a),p(t,r)]),l=m(s,o),d=h(c);return{commits:l,branches:d,sources:i,promptFragment:b(l,d)}}async function u(e,t){let n={hasConventionalCommitsDep:!1,hasCommitizen:!1},r=await C(e,`package.json`);if(r)try{let e=JSON.parse(r),i={...e.dependencies??{},...e.devDependencies??{}};i[`@commitlint/config-conventional`]&&(n.hasConventionalCommitsDep=!0,t.push(`package.json (@commitlint/config-conventional)`)),(e.config?.commitizen||i.commitizen)&&(n.hasCommitizen=!0,t.push(`package.json (commitizen)`))}catch{}for(let r of[`.commitlintrc.json`,`.commitlintrc.yml`,`.commitlintrc.yaml`]){let i=await C(e,r);if(i){t.push(r);let e=d(i);e.length>0&&(n.commitlintTypes=e);break}}for(let r of[`commitlint.config.js`,`commitlint.config.cjs`,`commitlint.config.mjs`])if(await w(e,r)){t.push(r),n.commitlintTypes||(n.hasConventionalCommitsDep=!0);break}let i=await C(e,`.gitmessage`);return i&&(n.gitMessageTemplate=i.trim(),t.push(`.gitmessage`)),n}function d(e){try{let t=JSON.parse(e)?.rules?.[`type-enum`];if(Array.isArray(t)){let e=t.find(e=>Array.isArray(e));if(Array.isArray(e))return e.filter(e=>typeof e==`string`)}}catch{let t=e.match(/type-enum[^[]*\[([^\]]+)\]/);if(t)return t[1].split(`,`).map(e=>e.trim().replace(/['"]/g,``)).filter(Boolean)}return[]}async function f(e,t,r){try{let i=(await n(e,{format:`%s`,noMerges:!0,limit:t,author:r})).trim().split(`
2
- `).filter(Boolean);return r&&i.length<5?(await n(e,{format:`%s`,noMerges:!0,limit:t})).trim().split(`
3
- `).filter(Boolean):i}catch{return[]}}async function p(t,n){try{return(await e(t,{format:`%(refname:short)`,prefix:`refs/heads/`,sort:`-committerdate`,limit:n+5})).trim().split(`
4
- `).filter(Boolean).filter(e=>!c.has(e)&&!e.startsWith(`release/`)).slice(0,n)}catch{return[]}}function m(e,t){if(e.length===0)return{format:t.hasConventionalCommitsDep||t.hasCommitizen?`conventional-commits`:`unknown`,prefixes:t.commitlintTypes,samples:[]};let n=e.filter(e=>i.test(e)),r=n.length/e.length>.6||t.hasConventionalCommitsDep||t.hasCommitizen,a,o;if(r){let e=new Set,r=0;for(let t of n){let n=t.match(i);n&&(e.add(n[1]),n[2]&&r++)}a=t.commitlintTypes??[...e].sort(),o=r/Math.max(n.length,1)>.3?`parenthesized`:`none`}let s=g(e,r),c=_(e),l=v(e,r?a:void 0);return{format:r?`conventional-commits`:`plain`,prefixes:a,scopeStyle:o,casing:s,issueRefPattern:c,samples:l}}function h(e){if(e.length===0)return{format:`unknown`,samples:[]};let t=e.filter(e=>o.test(e)),n=e.filter(e=>s.test(e));if(t.length/e.length>.5){let n=new Set;for(let e of t){let t=e.match(o);t&&n.add(t[1])}return{format:`prefixed`,prefixes:[...n].sort(),separator:`/`,samples:y(e,[...n])}}return n.length/e.length>.5?{format:`issue-number`,separator:`-`,samples:e.slice(0,5)}:{format:e.length>=3?`plain`:`unknown`,samples:e.slice(0,5)}}function g(e,t){let n=0,r=0;for(let a of e){let e=(t?a.replace(i,``):a).trim()[0];e&&(e===e.toUpperCase()&&e!==e.toLowerCase()?r++:n++)}if(!(n+r<3)){if(n/(n+r)>.7)return`lowercase`;if(r/(n+r)>.7)return`capitalized`}}function _(e){for(let{re:t,label:n}of a)if(e.filter(e=>t.test(e)).length/e.length>.2)return n}function v(e,t){if(!t||t.length===0)return e.slice(0,5);let n=[],r=new Set;for(let i of t){let t=e.find(e=>e.startsWith(`${i}(`)||e.startsWith(`${i}:`));if(t&&!r.has(t)&&(n.push(t),r.add(t),n.length>=5))break}for(let t of e){if(n.length>=5)break;r.has(t)||(n.push(t),r.add(t))}return n}function y(e,t){let n=[],r=new Set;for(let i of t){let t=e.find(e=>e.startsWith(`${i}/`));if(t&&!r.has(t)&&(n.push(t),r.add(t),n.length>=5))break}for(let t of e){if(n.length>=5)break;r.has(t)||(n.push(t),r.add(t))}return n}function b(e,t){let n=[`## Repository conventions
5
- `];return n.push(x(e)),n.push(S(t)),n.push(`Match these conventions in your output.`),n.join(`
6
- `)}function x(e){if(e.format===`unknown`&&e.samples.length===0)return`Commit messages: No clear convention detected. Use clear, concise messages.
7
- `;let t=[];if(e.format===`conventional-commits`){let n=e.scopeStyle===`parenthesized`?` with scope in parentheses`:``;t.push(`Commit messages follow Conventional Commits format${n}: type${e.scopeStyle===`parenthesized`?`(scope)`:``}: description`),e.prefixes?.length&&t.push(`Allowed types: ${e.prefixes.join(`, `)}`)}else t.push(`Commit messages use plain English format.`);if(e.casing&&t.push(`${e.casing===`capitalized`?`Capitalized`:`Lowercase`} first word after any prefix.`),e.issueRefPattern&&t.push(`Issues often referenced as ${e.issueRefPattern}.`),e.samples.length>0){t.push(`Examples:`);for(let n of e.samples)t.push(`- ${n}`)}return`${t.join(`
8
- `)}\n`}function S(e){if(e.format===`unknown`&&e.samples.length===0)return`Branch names: No clear convention detected. Use descriptive kebab-case names.
1
+ import{forEachRef as e,getConfig as t,log as n,showFile as r}from"../port-dispatch.js";const i=/^(feat|fix|refactor|chore|docs|style|test|perf|ci|build|revert)(\(.+?\))?!?:\s/,a=[{re:/\(#\d+\)/,label:`(#NNN)`},{re:/#\d+/,label:`#NNN`},{re:/[A-Z]{2,}-\d+/,label:`PROJ-NNN`}],o=/^(feat|fix|chore|feature|hotfix|bugfix|refactor|release|support)\//,s=/^(\d+)-|^([A-Z]{2,}-\d+)-/,c=new Set([`main`,`master`,`develop`,`development`,`staging`,`production`]),l=`%x1e%s%x1f%b`,u=/^[A-Za-z][A-Za-z-]*:\s/;async function d(e){let{git:t,commitLimit:n=30,branchLimit:r=15}=e,i=[],a=e.author??await A(t),[o,s,c]=await Promise.all([f(t,i),m(t,n,a),v(t,r)]),l=y(s,o),u=b(c);return{commits:l,branches:u,sources:i,promptFragment:T(l,u)}}async function f(e,t){let n={hasConventionalCommitsDep:!1,hasCommitizen:!1},r=await O(e,`package.json`);if(r)try{let e=JSON.parse(r),i={...e.dependencies??{},...e.devDependencies??{}};i[`@commitlint/config-conventional`]&&(n.hasConventionalCommitsDep=!0,t.push(`package.json (@commitlint/config-conventional)`)),(e.config?.commitizen||i.commitizen)&&(n.hasCommitizen=!0,t.push(`package.json (commitizen)`))}catch{}for(let r of[`.commitlintrc.json`,`.commitlintrc.yml`,`.commitlintrc.yaml`]){let i=await O(e,r);if(i){t.push(r);let e=p(i);e.length>0&&(n.commitlintTypes=e);break}}for(let r of[`commitlint.config.js`,`commitlint.config.cjs`,`commitlint.config.mjs`])if(await k(e,r)){t.push(r),n.commitlintTypes||(n.hasConventionalCommitsDep=!0);break}let i=await O(e,`.gitmessage`);return i&&(n.gitMessageTemplate=i.trim(),t.push(`.gitmessage`)),n}function p(e){try{let t=JSON.parse(e)?.rules?.[`type-enum`];if(Array.isArray(t)){let e=t.find(e=>Array.isArray(e));if(Array.isArray(e))return e.filter(e=>typeof e==`string`)}}catch{let t=e.match(/type-enum[^[]*\[([^\]]+)\]/);if(t)return t[1].split(`,`).map(e=>e.trim().replace(/['"]/g,``)).filter(Boolean)}return[]}async function m(e,t,r){try{let i=h(await n(e,{format:l,noMerges:!0,limit:t,author:r}));return r&&i.length<5?h(await n(e,{format:l,noMerges:!0,limit:t})):i}catch{return[]}}function h(e){return e.split(``).filter(e=>e.trim().length>0).map(e=>{let t=e.indexOf(``);return t===-1?{subject:e.trim(),body:``}:{subject:e.slice(0,t).trim(),body:e.slice(t+1).trim()}})}function g(e){if(e.length<5)return;let t=e.filter(e=>_(e.body)).length/e.length;if(t>=.3)return`detailed`;if(t<=.1)return`minimal`}function _(e){return e?e.split(`
2
+ `).map(e=>e.trim()).filter(Boolean).some(e=>!u.test(e)):!1}async function v(t,n){try{return(await e(t,{format:`%(refname:short)`,prefix:`refs/heads/`,sort:`-committerdate`,limit:n+5})).trim().split(`
3
+ `).filter(Boolean).filter(e=>!c.has(e)&&!e.startsWith(`release/`)).slice(0,n)}catch{return[]}}function y(e,t){let n=e.map(e=>e.subject);if(n.length===0)return{format:t.hasConventionalCommitsDep||t.hasCommitizen?`conventional-commits`:`unknown`,prefixes:t.commitlintTypes,samples:[]};let r=n.filter(e=>i.test(e)),a=r.length/n.length>.6||t.hasConventionalCommitsDep||t.hasCommitizen,o,s;if(a){let e=new Set,n=0;for(let t of r){let r=t.match(i);r&&(e.add(r[1]),r[2]&&n++)}o=t.commitlintTypes??[...e].sort(),s=n/Math.max(r.length,1)>.3?`parenthesized`:`none`}let c=x(n,a),l=S(n),u=g(e),d=C(n,a?o:void 0);return{format:a?`conventional-commits`:`plain`,prefixes:o,scopeStyle:s,casing:c,issueRefPattern:l,bodyStyle:u,samples:d}}function b(e){if(e.length===0)return{format:`unknown`,samples:[]};let t=e.filter(e=>o.test(e)),n=e.filter(e=>s.test(e));if(t.length/e.length>.5){let n=new Set;for(let e of t){let t=e.match(o);t&&n.add(t[1])}return{format:`prefixed`,prefixes:[...n].sort(),separator:`/`,samples:w(e,[...n])}}return n.length/e.length>.5?{format:`issue-number`,separator:`-`,samples:e.slice(0,5)}:{format:e.length>=3?`plain`:`unknown`,samples:e.slice(0,5)}}function x(e,t){let n=0,r=0;for(let a of e){let e=(t?a.replace(i,``):a).trim()[0];e&&(e===e.toUpperCase()&&e!==e.toLowerCase()?r++:n++)}if(!(n+r<3)){if(n/(n+r)>.7)return`lowercase`;if(r/(n+r)>.7)return`capitalized`}}function S(e){for(let{re:t,label:n}of a)if(e.filter(e=>t.test(e)).length/e.length>.2)return n}function C(e,t){if(!t||t.length===0)return e.slice(0,5);let n=[],r=new Set;for(let i of t){let t=e.find(e=>e.startsWith(`${i}(`)||e.startsWith(`${i}:`));if(t&&!r.has(t)&&(n.push(t),r.add(t),n.length>=5))break}for(let t of e){if(n.length>=5)break;r.has(t)||(n.push(t),r.add(t))}return n}function w(e,t){let n=[],r=new Set;for(let i of t){let t=e.find(e=>e.startsWith(`${i}/`));if(t&&!r.has(t)&&(n.push(t),r.add(t),n.length>=5))break}for(let t of e){if(n.length>=5)break;r.has(t)||(n.push(t),r.add(t))}return n}function T(e,t){let n=[`## Repository conventions
4
+ `];return n.push(E(e)),n.push(D(t)),n.push(`Match these conventions in your output.`),n.join(`
5
+ `)}function E(e){if(e.format===`unknown`&&e.samples.length===0)return`Commit messages: No clear convention detected. Use clear, concise messages.
6
+ `;let t=[];if(e.format===`conventional-commits`){let n=e.scopeStyle===`parenthesized`?` with scope in parentheses`:``;t.push(`Commit messages follow Conventional Commits format${n}: type${e.scopeStyle===`parenthesized`?`(scope)`:``}: description`),e.prefixes?.length&&t.push(`Allowed types: ${e.prefixes.join(`, `)}`)}else t.push(`Commit messages use plain English format.`);if(e.casing&&t.push(`${e.casing===`capitalized`?`Capitalized`:`Lowercase`} first word after any prefix.`),e.issueRefPattern&&t.push(`Issues often referenced as ${e.issueRefPattern}.`),e.samples.length>0){t.push(`Example subject lines:`);for(let n of e.samples)t.push(`- ${n}`)}return`${t.join(`
7
+ `)}\n`}function D(e){if(e.format===`unknown`&&e.samples.length===0)return`Branch names: No clear convention detected. Use descriptive kebab-case names.
9
8
  `;let t=[];if(e.format===`prefixed`){let n=e.separator??`/`;t.push(`Branch names use type${n}kebab-case-slug format.`),e.prefixes?.length&&t.push(`Common prefixes: ${e.prefixes.join(`, `)}`)}else e.format===`issue-number`?t.push(`Branch names start with an issue number followed by a kebab-case description.`):t.push(`Branch names use descriptive kebab-case format.`);if(e.samples.length>0){t.push(`Examples:`);for(let n of e.samples)t.push(`- ${n}`)}return`${t.join(`
10
- `)}\n`}async function C(e,t){try{return await r(e,`HEAD`,t)}catch{return null}}async function w(e,t){try{return await r(e,`HEAD`,t),!0}catch{return!1}}async function T(e){let n=await t(e,`user.email`);if(n)return n;let r=await t(e,`user.name`);if(r)return r}export{l as extractConventions};
9
+ `)}\n`}async function O(e,t){try{return await r(e,`HEAD`,t)}catch{return null}}async function k(e,t){try{return await r(e,`HEAD`,t),!0}catch{return!1}}async function A(e){let n=await t(e,`user.email`);if(n)return n;let r=await t(e,`user.name`);if(r)return r}export{d as extractConventions};
@@ -0,0 +1,18 @@
1
+ import { ComposeGitPort } from "../../ports/git.js";
2
+
3
+ //#region src/git/utils/refs.d.ts
4
+ /**
5
+ * List refs as `{ refname, sha }`, resolving each to the commit it ultimately points at.
6
+ * `%(*objectname)` is the peeled target of an annotated tag (the commit); it's empty for lightweight
7
+ * tags and branches, where `%(objectname)` is already the commit. Reading `%(objectname)` alone
8
+ * yields the tag *object's* SHA for annotated tags, which never matches a commit — so any caller
9
+ * comparing refs against commits must peel. Pass `pointsAt` to filter to refs at one object.
10
+ */
11
+ declare function listRefsWithPeeledSha(git: ComposeGitPort, opts?: {
12
+ pointsAt?: string;
13
+ }): Promise<{
14
+ refname: string;
15
+ sha: string;
16
+ }[]>;
17
+ //#endregion
18
+ export { listRefsWithPeeledSha };
@@ -0,0 +1,2 @@
1
+ import{forEachRef as e}from"../port-dispatch.js";async function t(t,n){let r=await e(t,{format:`%(refname) %(objectname) %(*objectname)`,pointsAt:n?.pointsAt}),i=[];for(let e of r.split(`
2
+ `)){let t=e.trim().split(/\s+/).filter(Boolean);t.length<2||i.push({refname:t[0],sha:t[2]??t[1]})}return i}export{t as listRefsWithPeeledSha};
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { PhaseDetail, ProgressEvent, TokenUsage } from "./ai/types.js";
2
2
  import { AIError, AIErrorCode } from "./errors/ai.js";
3
3
  import { GitError, GitErrorCode } from "./errors/git.js";
4
- import { TaskInputError } from "./errors/task.js";
4
+ import { DiffTooLargeError, TaskInputError } from "./errors/task.js";
5
5
  import { ComposeWorkflowError, ComposeWorkflowErrorCode, ComposeWorkflowInputError } from "./errors/workflow.js";
6
6
  import { ApplyPatchOptions, CheckoutOptions, CleanOptions, CommitTreeOptions, ComposeGitOps, ComposeGitPort, ComposeGitPortMissingOpError, DeleteBranchOptions, DiffTreeOptions, ForEachRefOptions, GitCommitIdentity, IndexScopedOptions, LogOptions, OpOptions, SigningConfig, StageAllOptions, StashPushOptions, UpdateRefOptions } from "./ports/git.js";
7
7
  import { BranchGroup, BranchOrdering, BranchPartition, CommitDetail, CommitSuggestion, ComposeHunk, ComposeTaskName, PromptOverride } from "./tasks/types.js";
@@ -14,9 +14,10 @@ import { OrderInput, OrderOutput, OrderingResult } from "./tasks/order/types.js"
14
14
  import { QuickComposeInput, QuickComposeOutput, QuickComposeResult } from "./tasks/quick-compose/types.js";
15
15
  import { ComposeSession } from "./tasks/session.js";
16
16
  import { PartitionInput, PartitionOutput, PartitionResult, PartitionStrategy } from "./tasks/partition/types.js";
17
- import { ComposeContext, ComposeSource, ComposeTarget } from "./workflows/types.js";
17
+ import { ComposeContext, ComposeSource, ComposeTarget, OverrideSafetyFailures } from "./workflows/types.js";
18
18
  import { StashConflict } from "./git/types.js";
19
19
  import { ComposeBranchPlan } from "./git/apply-target.js";
20
+ import { InteriorRefViolation, detectInteriorRefs } from "./git/safety-range.js";
20
21
  import { analyze } from "./tasks/analyze/analyze.js";
21
22
  import { ComposeGroupInput, ComposeGroupOutput } from "./tasks/compose-group/types.js";
22
23
  import { composeGroup } from "./tasks/compose-group/compose-group.js";
@@ -24,9 +25,11 @@ import { group } from "./tasks/group/group.js";
24
25
  import { order } from "./tasks/order/order.js";
25
26
  import { partition } from "./tasks/partition/partition.js";
26
27
  import { quickCompose } from "./tasks/quick-compose/quick-compose.js";
28
+ import { DEFAULT_INPUT_TOKEN_BUDGET } from "./tasks/utils/budget.js";
29
+ import { isNoiseHunk, withDefaultNoiseRedaction } from "./tasks/utils/noise.js";
27
30
  import { ApplyComposePlanInput, ApplyComposePlanResult, ComposeApplyPlan, ComposeDepth, ComposeDiffStats, ComposeInput, ComposePlan, ComposePlanInput, ComposePlanResult, ComposeProgressEvent, ComposeResult, ComposeStrategy, applyComposePlan, compose, composePlan, defaultPhaseMessages } from "./workflows/compose.js";
28
31
  import { RefinePlanInput, RefinePlanResult, RefineProgressEvent, refinePlan } from "./workflows/refine.js";
29
32
  import { SplitInput, SplitPlan, SplitProgressEvent, SplitResult, split } from "./workflows/split.js";
30
33
  import { UndoComposeInput, ValidateUndoComposeInput, listUndoIds, undoCompose, validateUndoCompose } from "./workflows/undo.js";
31
34
  import { UndoForceOptions } from "@gitkraken/shared-tools";
32
- 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, type DeleteBranchOptions, type DiffTreeOptions, type ForEachRefOptions, type GitCommitIdentity, GitError, type GitErrorCode, type GroupInput, type GroupOutput, type GroupingResult, type GroupingStrategy, type HunkAnnotation, type HunkOrderingConstraint, type IndexScopedOptions, type LogOptions, type OnAfterModelCall, type OnBeforeModelCall, type OnBeforePrompt, type OpOptions, type OrderInput, type OrderOutput, type OrderingResult, 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 StashPushOptions, TaskInputError, type TokenUsage, type UndoComposeInput, type UndoForceOptions, type UpdateRefOptions, type ValidateUndoComposeInput, analyze, applyComposePlan, compose, composeGroup, composePlan, defaultPhaseMessages, estimatedCharactersPerToken, group, listUndoIds, order, partition, quickCompose, refinePlan, split, undoCompose, validateUndoCompose };
35
+ 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 StashPushOptions, TaskInputError, type TokenUsage, type UndoComposeInput, type UndoForceOptions, type UpdateRefOptions, type ValidateUndoComposeInput, analyze, applyComposePlan, compose, composeGroup, composePlan, defaultPhaseMessages, detectInteriorRefs, estimatedCharactersPerToken, group, isNoiseHunk, listUndoIds, order, 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{TaskInputError as n}from"./errors/task.js";import{ComposeWorkflowError as r,ComposeWorkflowInputError as i}from"./errors/workflow.js";import{ComposeGitPortMissingOpError as a}from"./ports/git.js";import{estimatedCharactersPerToken as o}from"./tasks/ai-loop.js";import{REDACTED_HUNK_CONTENT as s}from"./tasks/utils/redact.js";import{analyze as c}from"./tasks/analyze/analyze.js";import{composeGroup as l}from"./tasks/compose-group/compose-group.js";import{group as u}from"./tasks/group/group.js";import{order as d}from"./tasks/order/order.js";import{partition as f}from"./tasks/partition/partition.js";import{quickCompose as p}from"./tasks/quick-compose/quick-compose.js";import{applyComposePlan as m,compose as h,composePlan as g,defaultPhaseMessages as _}from"./workflows/compose.js";import{refinePlan as v}from"./workflows/refine.js";import{split as y}from"./workflows/split.js";import{listUndoIds as b,undoCompose as x,validateUndoCompose as S}from"./workflows/undo.js";export{e as AIError,a as ComposeGitPortMissingOpError,r as ComposeWorkflowError,i as ComposeWorkflowInputError,t as GitError,s as REDACTED_HUNK_CONTENT,n as TaskInputError,c as analyze,m as applyComposePlan,h as compose,l as composeGroup,g as composePlan,_ as defaultPhaseMessages,o as estimatedCharactersPerToken,u as group,b as listUndoIds,d as order,f as partition,p as quickCompose,v as refinePlan,y as split,x as undoCompose,S as validateUndoCompose};
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{estimatedCharactersPerToken as c}from"./tasks/ai-loop.js";import{REDACTED_HUNK_CONTENT as l}from"./tasks/utils/redact.js";import{DEFAULT_INPUT_TOKEN_BUDGET as u}from"./tasks/utils/budget.js";import{analyze as d}from"./tasks/analyze/analyze.js";import{composeGroup as f}from"./tasks/compose-group/compose-group.js";import{group as p}from"./tasks/group/group.js";import{order as m}from"./tasks/order/order.js";import{partition as h}from"./tasks/partition/partition.js";import{quickCompose as g}from"./tasks/quick-compose/quick-compose.js";import{isNoiseHunk as _,withDefaultNoiseRedaction as v}from"./tasks/utils/noise.js";import{applyComposePlan as y,compose as b,composePlan as x,defaultPhaseMessages as S}from"./workflows/compose.js";import{refinePlan as C}from"./workflows/refine.js";import{split as w}from"./workflows/split.js";import{listUndoIds as T,undoCompose as E,validateUndoCompose as D}from"./workflows/undo.js";export{e as AIError,o as ComposeGitPortMissingOpError,i as ComposeWorkflowError,a as ComposeWorkflowInputError,u as DEFAULT_INPUT_TOKEN_BUDGET,n as DiffTooLargeError,t as GitError,l as REDACTED_HUNK_CONTENT,r as TaskInputError,d as analyze,y as applyComposePlan,b as compose,f as composeGroup,x as composePlan,S as defaultPhaseMessages,s as detectInteriorRefs,c as estimatedCharactersPerToken,p as group,_ as isNoiseHunk,T as listUndoIds,m as order,h as partition,g as quickCompose,C as refinePlan,w as split,E as undoCompose,D as validateUndoCompose,v as withDefaultNoiseRedaction};
@@ -1 +1 @@
1
- import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildAnalyzeSystemPrompt as r,buildAnalyzeUserPrompt as i}from"./prompt.js";import{validateAnalyzeResponse as a}from"./validator.js";async function o(o){let s=o.hunks;if(!s||s.length===0)throw new e(`No hunks provided: supply 'hunks'`);if(o.session?.completedSteps.analyze)return{result:o.session.completedSteps.analyze,session:o.session,attempts:0,durationMs:0,callIds:[]};let c=n(i({hunks:s,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:o.issueUrl,pullRequestUrl:o.pullRequestUrl,instructions:o.instructions,redactHunkContent:o.redactHunkContent}),o.promptOverride),l=s.length,u=await t({model:o.model,systemPrompt:o.session?void 0:o.promptOverride?.system??r(),userPrompt:c,session:o.session,maxRetries:o.maxRetries,maxTokens:o.maxTokens,onProgress:o.onProgress,cancellation:o.cancellation,onBeforeModelCall:o.onBeforeModelCall,onAfterModelCall:o.onAfterModelCall,onBeforePrompt:o.onBeforePrompt,validate:e=>a(e,l),taskName:`analyze`}),d=o.session?.completedSteps??{},f={...u.session,completedSteps:{...d,analyze:u.result}};return{result:u.result,session:f,usage:u.usage,attempts:u.attempts,durationMs:u.durationMs,callIds:u.callIds}}export{o as analyze};
1
+ import{TaskInputError as e}from"../../errors/task.js";import{runAiLoop as t}from"../ai-loop.js";import{applyUserPromptOverride as n}from"../types.js";import{buildAnalyzeSystemPrompt as r,buildAnalyzeUserPrompt as i}from"./prompt.js";import{validateAnalyzeResponse as a}from"./validator.js";async function o(o){let s=o.hunks;if(!s||s.length===0)throw new e(`No hunks provided: supply 'hunks'`);if(o.session?.completedSteps.analyze)return{result:o.session.completedSteps.analyze,session:o.session,attempts:0,durationMs:0,callIds:[]};let c=o.session?void 0:o.promptOverride?.system??r(),l,u=n(i({hunks:s,commitMessages:o.commitMessages,commitOrder:o.commitOrder,branchName:o.branchName,baseBranch:o.baseBranch,issueUrl:o.issueUrl,pullRequestUrl:o.pullRequestUrl,instructions:o.instructions,redactHunkContent:o.redactHunkContent,inputTokenBudget:o.inputTokenBudget,systemPromptChars:c?.length,onBudgetReduction:e=>{l=e}}),o.promptOverride),d=s.length,f=await t({model:o.model,systemPrompt:c,userPrompt:u,session:o.session,maxRetries:o.maxRetries,maxTokens:o.maxTokens,onProgress:o.onProgress,cancellation:o.cancellation,onBeforeModelCall:o.onBeforeModelCall,onAfterModelCall:o.onAfterModelCall,onBeforePrompt:o.onBeforePrompt,validate:e=>a(e,d),taskName:`analyze`}),p=o.session?.completedSteps??{},m={...f.session,completedSteps:{...p,analyze:f.result}};return{result:f.result,session:m,usage:f.usage,attempts:f.attempts,durationMs:f.durationMs,callIds:f.callIds,hunksCondensedCount:l}}export{o as analyze};
@@ -12,7 +12,10 @@ declare function buildAnalyzeUserPrompt(params: {
12
12
  issueUrl?: string;
13
13
  pullRequestUrl?: string;
14
14
  instructions?: string;
15
- redactHunkContent?: RedactHunkContent;
15
+ redactHunkContent?: RedactHunkContent; /** Input-token ceiling that triggers progressive hunk reduction. Defaults to the shared budget. */
16
+ inputTokenBudget?: number; /** Char count of the system prompt, folded into the budget estimate. Defaults to 0. */
17
+ systemPromptChars?: number; /** Reports how many hunks were condensed for budget (full to condensed), for consumer telemetry. */
18
+ onBudgetReduction?: (hunksCondensedForBudget: number) => void;
16
19
  }): string;
17
20
  //#endregion
18
21
  export { buildAnalyzeSystemPrompt, buildAnalyzeUserPrompt };
@@ -1,4 +1,4 @@
1
- import{hunkContentForPrompt as e}from"../utils/redact.js";import{analysisResultSchema as t}from"./schema.js";import{z as n}from"zod";const r=JSON.stringify(n.toJSONSchema(t),null,2);function i(){return`You are an advanced AI code analysis assistant. Your task is to deeply analyze a set of code changes (provided as diff hunks) and produce a structured understanding of what the changes accomplish, what concerns they address, and how hunks relate to each other.
1
+ import{assembleHunkPrompt as e}from"../utils/budget.js";import{analysisResultJsonSchema as t}from"./schema.js";const n=JSON.stringify(t,null,2);function r(){return`You are an advanced AI code analysis assistant. Your task is to deeply analyze a set of code changes (provided as diff hunks) and produce a structured understanding of what the changes accomplish, what concerns they address, and how hunks relate to each other.
2
2
 
3
3
  Your analysis must produce:
4
4
  1. **Title** — a short, PR-style title for the changes (under 72 characters, imperative mood, e.g. "Add user authentication and session management"). This should capture the essence of what the changes accomplish.
@@ -25,10 +25,10 @@ Rules:
25
25
  Output your analysis as JSON inside a <output> tag matching this schema:
26
26
 
27
27
  \`\`\`json
28
- ${r}
28
+ ${n}
29
29
  \`\`\`
30
30
 
31
- Return only the <output> tag and no other text.`}function a(t){let{hunks:n,commitMessages:r,commitOrder:i,branchName:a,baseBranch:o,issueUrl:s,pullRequestUrl:c,instructions:l,redactHunkContent:u}=t,d=`Analyze the following code changes:\n\n<hunks>\n${JSON.stringify(n.map(t=>({index:t.index,fileName:t.fileName,hunkHeader:t.hunkHeader,content:e(t,u),...t.isBinary?{isBinary:!0}:{},...t.isRename?{isRename:!0}:{},...t.originalFileName?{originalFileName:t.originalFileName}:{}})))}\n</hunks>`,f=[];if(a&&f.push(`Branch: ${a}`),o&&f.push(`Base branch: ${o}`),s&&f.push(`Issue: ${s}`),c&&f.push(`Pull request: ${c}`),f.length>0&&(d+=`\n\n<context>\n${f.join(`
32
- `)}\n</context>`),r?.length){let e=r.map((e,t)=>({index:t,sha:i?.[t],message:e}));d+=`\n\n<commit_messages>\n${JSON.stringify(e)}\n</commit_messages>`}return l&&(d+=`\n\nCarefully follow these additional instructions, but do not deviate from the output structure:\n${l}`),d+=`
31
+ Return only the <output> tag and no other text.`}function i(t){let{hunks:n,commitMessages:r,commitOrder:i,branchName:a,baseBranch:o,issueUrl:s,pullRequestUrl:c,instructions:l,redactHunkContent:u,inputTokenBudget:d,systemPromptChars:f,onBudgetReduction:p}=t,{prompt:m,hunksCondensedForBudget:h}=e({hunks:n,assemble:e=>{let t=`Analyze the following code changes:\n\n<hunks>\n${e}\n</hunks>`,n=[];if(a&&n.push(`Branch: ${a}`),o&&n.push(`Base branch: ${o}`),s&&n.push(`Issue: ${s}`),c&&n.push(`Pull request: ${c}`),n.length>0&&(t+=`\n\n<context>\n${n.join(`
32
+ `)}\n</context>`),r?.length){let e=r.map((e,t)=>({index:t,sha:i?.[t],message:e}));t+=`\n\n<commit_messages>\n${JSON.stringify(e)}\n</commit_messages>`}return l&&(t+=`\n\nCarefully follow these additional instructions, but do not deviate from the output structure:\n${l}`),t+=`
33
33
 
34
- Now analyze the changes and return your structured analysis. Return only the <output> tag and no other text.`,d}export{i as buildAnalyzeSystemPrompt,a as buildAnalyzeUserPrompt};
34
+ Now analyze the changes and return your structured analysis. Return only the <output> tag and no other text.`,t},redactHunkContent:u,inputTokenBudget:d,systemPromptChars:f});return p?.(h),m}export{r as buildAnalyzeSystemPrompt,i as buildAnalyzeUserPrompt};
@@ -1,38 +1,129 @@
1
- import { z } from "zod";
2
-
3
1
  //#region src/tasks/analyze/schema.d.ts
4
- declare const analysisResultSchema: z.ZodObject<{
5
- title: z.ZodString;
6
- summary: z.ZodString;
7
- concerns: z.ZodArray<z.ZodObject<{
8
- id: z.ZodString;
9
- name: z.ZodString;
10
- description: z.ZodString;
11
- hunkIndices: z.ZodArray<z.ZodNumber>;
12
- }, z.core.$strip>>;
13
- dependencies: z.ZodDefault<z.ZodArray<z.ZodObject<{
14
- from: z.ZodString;
15
- to: z.ZodString;
16
- reason: z.ZodString;
17
- }, z.core.$strip>>>;
18
- hunkAnnotations: z.ZodArray<z.ZodObject<{
19
- hunkIndex: z.ZodNumber;
20
- concernId: z.ZodString;
21
- purpose: z.ZodString;
22
- }, z.core.$strip>>;
23
- orderingConstraints: z.ZodDefault<z.ZodArray<z.ZodObject<{
24
- provider: z.ZodNumber;
25
- consumer: z.ZodNumber;
26
- kind: z.ZodEnum<{
27
- "file-reference": "file-reference";
28
- import: "import";
29
- "function-call": "function-call";
30
- "type-reference": "type-reference";
31
- "config-reference": "config-reference";
32
- other: "other";
33
- }>;
34
- reason: z.ZodString;
35
- }, z.core.$strip>>>;
36
- }, z.core.$strip>;
2
+ declare const analysisResultJsonSchema: {
3
+ readonly $schema: "https://json-schema.org/draft/2020-12/schema";
4
+ readonly type: "object";
5
+ readonly properties: {
6
+ readonly title: {
7
+ readonly type: "string";
8
+ readonly minLength: 1;
9
+ readonly maxLength: 72;
10
+ readonly description: "Short PR-style title for the changes (under 72 chars, imperative mood, e.g. 'Add user authentication and session management')";
11
+ };
12
+ readonly summary: {
13
+ readonly type: "string";
14
+ readonly minLength: 1;
15
+ readonly description: "High-level summary of all changes";
16
+ };
17
+ readonly concerns: {
18
+ readonly minItems: 1;
19
+ readonly type: "array";
20
+ readonly items: {
21
+ readonly type: "object";
22
+ readonly properties: {
23
+ readonly id: {
24
+ readonly type: "string";
25
+ readonly description: "Unique concern ID (e.g. c1, c2)";
26
+ };
27
+ readonly name: {
28
+ readonly type: "string";
29
+ readonly description: "Short concern name";
30
+ };
31
+ readonly description: {
32
+ readonly type: "string";
33
+ readonly description: "What this concern accomplishes";
34
+ };
35
+ readonly hunkIndices: {
36
+ readonly type: "array";
37
+ readonly items: {
38
+ readonly type: "number";
39
+ };
40
+ readonly description: "Indices of hunks belonging to this concern";
41
+ };
42
+ };
43
+ readonly required: readonly ["id", "name", "description", "hunkIndices"];
44
+ readonly additionalProperties: false;
45
+ };
46
+ readonly description: "Distinct themes, features, or purposes within the changes";
47
+ };
48
+ readonly dependencies: {
49
+ readonly default: readonly [];
50
+ readonly description: "Relationships between concerns";
51
+ readonly type: "array";
52
+ readonly items: {
53
+ readonly type: "object";
54
+ readonly properties: {
55
+ readonly from: {
56
+ readonly type: "string";
57
+ readonly description: "Concern ID";
58
+ };
59
+ readonly to: {
60
+ readonly type: "string";
61
+ readonly description: "Concern ID that 'from' depends on";
62
+ };
63
+ readonly reason: {
64
+ readonly type: "string";
65
+ readonly description: "Why the dependency exists";
66
+ };
67
+ };
68
+ readonly required: readonly ["from", "to", "reason"];
69
+ readonly additionalProperties: false;
70
+ };
71
+ };
72
+ readonly hunkAnnotations: {
73
+ readonly type: "array";
74
+ readonly items: {
75
+ readonly type: "object";
76
+ readonly properties: {
77
+ readonly hunkIndex: {
78
+ readonly type: "number";
79
+ readonly description: "Index of the hunk";
80
+ };
81
+ readonly concernId: {
82
+ readonly type: "string";
83
+ readonly description: "Concern this hunk belongs to";
84
+ };
85
+ readonly purpose: {
86
+ readonly type: "string";
87
+ readonly description: "What this hunk does in context of its concern";
88
+ };
89
+ };
90
+ readonly required: readonly ["hunkIndex", "concernId", "purpose"];
91
+ readonly additionalProperties: false;
92
+ };
93
+ readonly description: "Per-hunk concern assignment and purpose";
94
+ };
95
+ readonly orderingConstraints: {
96
+ readonly default: readonly [];
97
+ readonly description: "Provider/consumer ordering between hunks";
98
+ readonly type: "array";
99
+ readonly items: {
100
+ readonly type: "object";
101
+ readonly properties: {
102
+ readonly provider: {
103
+ readonly type: "number";
104
+ readonly description: "Hunk index that defines/creates something";
105
+ };
106
+ readonly consumer: {
107
+ readonly type: "number";
108
+ readonly description: "Hunk index that references/uses it";
109
+ };
110
+ readonly kind: {
111
+ readonly type: "string";
112
+ readonly enum: readonly ["file-reference", "import", "function-call", "type-reference", "config-reference", "other"];
113
+ readonly description: "Type of provider/consumer relationship";
114
+ };
115
+ readonly reason: {
116
+ readonly type: "string";
117
+ readonly description: "Brief explanation of the relationship";
118
+ };
119
+ };
120
+ readonly required: readonly ["provider", "consumer", "kind", "reason"];
121
+ readonly additionalProperties: false;
122
+ };
123
+ };
124
+ };
125
+ readonly required: readonly ["title", "summary", "concerns", "dependencies", "hunkAnnotations", "orderingConstraints"];
126
+ readonly additionalProperties: false;
127
+ };
37
128
  //#endregion
38
- export { analysisResultSchema };
129
+ export { analysisResultJsonSchema };
@@ -1 +1 @@
1
- import{z as e}from"zod";const t=e.enum([`file-reference`,`import`,`function-call`,`type-reference`,`config-reference`,`other`]),n=e.object({title:e.string().min(1).max(72).describe(`Short PR-style title for the changes (under 72 chars, imperative mood, e.g. 'Add user authentication and session management')`),summary:e.string().min(1).describe(`High-level summary of all changes`),concerns:e.array(e.object({id:e.string().describe(`Unique concern ID (e.g. c1, c2)`),name:e.string().describe(`Short concern name`),description:e.string().describe(`What this concern accomplishes`),hunkIndices:e.array(e.number()).describe(`Indices of hunks belonging to this concern`)})).min(1).describe(`Distinct themes, features, or purposes within the changes`),dependencies:e.array(e.object({from:e.string().describe(`Concern ID`),to:e.string().describe(`Concern ID that 'from' depends on`),reason:e.string().describe(`Why the dependency exists`)})).default([]).describe(`Relationships between concerns`),hunkAnnotations:e.array(e.object({hunkIndex:e.number().describe(`Index of the hunk`),concernId:e.string().describe(`Concern this hunk belongs to`),purpose:e.string().describe(`What this hunk does in context of its concern`)})).describe(`Per-hunk concern assignment and purpose`),orderingConstraints:e.array(e.object({provider:e.number().describe(`Hunk index that defines/creates something`),consumer:e.number().describe(`Hunk index that references/uses it`),kind:t.describe(`Type of provider/consumer relationship`),reason:e.string().describe(`Brief explanation of the relationship`)})).default([]).describe(`Provider/consumer ordering between hunks`)});export{n as analysisResultSchema};
1
+ const e={$schema:`https://json-schema.org/draft/2020-12/schema`,type:`object`,properties:{title:{type:`string`,minLength:1,maxLength:72,description:`Short PR-style title for the changes (under 72 chars, imperative mood, e.g. 'Add user authentication and session management')`},summary:{type:`string`,minLength:1,description:`High-level summary of all changes`},concerns:{minItems:1,type:`array`,items:{type:`object`,properties:{id:{type:`string`,description:`Unique concern ID (e.g. c1, c2)`},name:{type:`string`,description:`Short concern name`},description:{type:`string`,description:`What this concern accomplishes`},hunkIndices:{type:`array`,items:{type:`number`},description:`Indices of hunks belonging to this concern`}},required:[`id`,`name`,`description`,`hunkIndices`],additionalProperties:!1},description:`Distinct themes, features, or purposes within the changes`},dependencies:{default:[],description:`Relationships between concerns`,type:`array`,items:{type:`object`,properties:{from:{type:`string`,description:`Concern ID`},to:{type:`string`,description:`Concern ID that 'from' depends on`},reason:{type:`string`,description:`Why the dependency exists`}},required:[`from`,`to`,`reason`],additionalProperties:!1}},hunkAnnotations:{type:`array`,items:{type:`object`,properties:{hunkIndex:{type:`number`,description:`Index of the hunk`},concernId:{type:`string`,description:`Concern this hunk belongs to`},purpose:{type:`string`,description:`What this hunk does in context of its concern`}},required:[`hunkIndex`,`concernId`,`purpose`],additionalProperties:!1},description:`Per-hunk concern assignment and purpose`},orderingConstraints:{default:[],description:`Provider/consumer ordering between hunks`,type:`array`,items:{type:`object`,properties:{provider:{type:`number`,description:`Hunk index that defines/creates something`},consumer:{type:`number`,description:`Hunk index that references/uses it`},kind:{type:`string`,enum:[`file-reference`,`import`,`function-call`,`type-reference`,`config-reference`,`other`],description:`Type of provider/consumer relationship`},reason:{type:`string`,description:`Brief explanation of the relationship`}},required:[`provider`,`consumer`,`kind`,`reason`],additionalProperties:!1}}},required:[`title`,`summary`,`concerns`,`dependencies`,`hunkAnnotations`,`orderingConstraints`],additionalProperties:!1};export{e as analysisResultJsonSchema};
@@ -17,6 +17,12 @@ interface AnalyzeInput {
17
17
  instructions?: string;
18
18
  /** See {@link RedactHunkContent}. */
19
19
  redactHunkContent?: RedactHunkContent;
20
+ /**
21
+ * Input-token ceiling for the hunk prompt. When exceeded, hunks are progressively reduced
22
+ * (biggest-first) to metadata; a diff that can't fit even condensed throws `DiffTooLargeError`.
23
+ * Defaults to `DEFAULT_INPUT_TOKEN_BUDGET`.
24
+ */
25
+ inputTokenBudget?: number;
20
26
  model: AiModelPort;
21
27
  session?: ComposeSession;
22
28
  maxRetries?: number;
@@ -69,6 +75,8 @@ interface AnalyzeOutput {
69
75
  attempts: number;
70
76
  durationMs: number;
71
77
  callIds: string[];
78
+ /** Hunks condensed to metadata to fit the input-token budget. Undefined when the AI call was skipped. */
79
+ hunksCondensedCount?: number;
72
80
  }
73
81
  //#endregion
74
82
  export { AnalysisResult, AnalyzeInput, AnalyzeOutput, Concern, ConcernDependency, HunkAnnotation, HunkOrderingConstraint };
@@ -1,4 +1,4 @@
1
- import{extractOutputTag as e}from"../ai-loop.js";import{analysisResultSchema as t}from"./schema.js";function n(n,a){let o=null;try{if(o=e(n),!o)return r([`No <output> tag found in response`],i());let s=JSON.parse(o),c=t.safeParse(s);if(!c.success)return r(c.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),i());let l=c.data,u=new Set(l.hunkAnnotations.map(e=>e.hunkIndex)),d=[...new Set(Array.from({length:a},(e,t)=>t))].filter(e=>!u.has(e));return d.length>0?r([`Missing annotations for hunks: ${d.join(`, `)}`],`Your response is missing annotations for some hunks. Every hunk must be annotated.\n\nMissing: ${d.join(`, `)}\n\nPlease provide a corrected response.`):{valid:!0,result:{title:l.title,summary:l.summary,concerns:l.concerns,dependencies:l.dependencies,hunkAnnotations:l.hunkAnnotations,orderingConstraints:l.orderingConstraints}}}catch(e){let t=e instanceof Error?e.message:String(e),a=(o??n).slice(-300);return r([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${a}`],i())}}function r(e,t){return{valid:!1,errors:e,retryFeedback:t}}function i(){return`Your response has an invalid structure. Please return JSON inside <output> tags with this structure:
1
+ import{extractOutputTag as e}from"../ai-loop.js";import{analysisResultJsonSchema as t}from"./schema.js";import{validateJsonSchema as n}from"../utils/json-schema.js";function r(r,o){let s=null;try{if(s=e(r),!s)return i([`No <output> tag found in response`],a());let c=n(t,JSON.parse(s));if(!c.success)return i(c.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),a());let l=c.data,u=new Set(l.hunkAnnotations.map(e=>e.hunkIndex)),d=[...new Set(Array.from({length:o},(e,t)=>t))].filter(e=>!u.has(e));return d.length>0?i([`Missing annotations for hunks: ${d.join(`, `)}`],`Your response is missing annotations for some hunks. Every hunk must be annotated.\n\nMissing: ${d.join(`, `)}\n\nPlease provide a corrected response.`):{valid:!0,result:{title:l.title,summary:l.summary,concerns:l.concerns,dependencies:l.dependencies,hunkAnnotations:l.hunkAnnotations,orderingConstraints:l.orderingConstraints}}}catch(e){let t=e instanceof Error?e.message:String(e),n=(s??r).slice(-300);return i([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],a())}}function i(e,t){return{valid:!1,errors:e,retryFeedback:t}}function a(){return`Your response has an invalid structure. Please return JSON inside <output> tags with this structure:
2
2
  <output>
3
3
  {
4
4
  "title": "Short PR-style title (under 72 chars)",
@@ -10,4 +10,4 @@ import{extractOutputTag as e}from"../ai-loop.js";import{analysisResultSchema as
10
10
  }
11
11
  </output>
12
12
 
13
- Return only the <output> tag and no other text.`}export{n as validateAnalyzeResponse};
13
+ Return only the <output> tag and no other text.`}export{r as validateAnalyzeResponse};