@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.
- package/CHANGELOG.md +29 -0
- package/dist/errors/task.d.ts +11 -1
- package/dist/errors/task.js +1 -1
- package/dist/git/commit-chain.js +3 -4
- package/dist/git/reparent.js +3 -4
- package/dist/git/safety-range.d.ts +14 -2
- package/dist/git/safety-range.js +4 -5
- package/dist/git/utils/conventions.d.ts +7 -0
- package/dist/git/utils/conventions.js +8 -9
- package/dist/git/utils/refs.d.ts +18 -0
- package/dist/git/utils/refs.js +2 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +1 -1
- package/dist/tasks/analyze/analyze.js +1 -1
- package/dist/tasks/analyze/prompt.d.ts +4 -1
- package/dist/tasks/analyze/prompt.js +5 -5
- package/dist/tasks/analyze/schema.d.ts +127 -36
- package/dist/tasks/analyze/schema.js +1 -1
- package/dist/tasks/analyze/types.d.ts +8 -0
- package/dist/tasks/analyze/validator.js +2 -2
- package/dist/tasks/compose-group/compose-group.js +1 -1
- package/dist/tasks/compose-group/prompt.d.ts +4 -1
- package/dist/tasks/compose-group/prompt.js +23 -29
- package/dist/tasks/compose-group/types.d.ts +5 -0
- package/dist/tasks/group/group.js +1 -1
- package/dist/tasks/group/prompt.d.ts +4 -1
- package/dist/tasks/group/prompt.js +15 -21
- package/dist/tasks/group/schema.d.ts +89 -17
- package/dist/tasks/group/schema.js +1 -1
- package/dist/tasks/group/types.d.ts +8 -0
- package/dist/tasks/group/validator.js +6 -6
- package/dist/tasks/order/prompt.js +5 -5
- package/dist/tasks/order/schema.d.ts +35 -10
- package/dist/tasks/order/schema.js +1 -1
- package/dist/tasks/order/validator.js +2 -2
- package/dist/tasks/partition/prompt.js +9 -9
- package/dist/tasks/partition/schema.d.ts +46 -12
- package/dist/tasks/partition/schema.js +1 -1
- package/dist/tasks/partition/validator.js +2 -2
- package/dist/tasks/quick-compose/prompt.js +8 -9
- package/dist/tasks/quick-compose/schema.d.ts +142 -25
- package/dist/tasks/quick-compose/schema.js +1 -1
- package/dist/tasks/quick-compose/validator.js +5 -5
- package/dist/tasks/refine/refine.js +1 -1
- package/dist/tasks/refine/types.d.ts +14 -0
- package/dist/tasks/refine/validator.d.ts +5 -1
- package/dist/tasks/refine/validator.js +1 -1
- package/dist/tasks/utils/budget.d.ts +40 -0
- package/dist/tasks/utils/budget.js +1 -0
- package/dist/tasks/utils/json-schema.d.ts +61 -0
- package/dist/tasks/utils/json-schema.js +1 -0
- package/dist/tasks/utils/noise.d.ts +21 -0
- package/dist/tasks/utils/noise.js +2 -0
- package/dist/tasks/utils/redact.d.ts +7 -1
- package/dist/tasks/utils/redact.js +1 -1
- package/dist/workflows/compose.d.ts +47 -2
- package/dist/workflows/compose.js +3 -3
- package/dist/workflows/refine.d.ts +9 -1
- package/dist/workflows/refine.js +1 -1
- package/dist/workflows/split.js +3 -3
- package/dist/workflows/types.d.ts +14 -1
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{partitionResultJsonSchema as e}from"./schema.js";const t=JSON.stringify(e,null,2);function n(e,n){let i=e?.branches&&e.branches.format!==`unknown`?`For each sub-branch, name it following the repository's branch naming conventions.\n\n${e.promptFragment}`:`For each sub-branch, provide a branch name derived from the source branch name (e.g., "feature/part-1-description").`;return`You are an advanced AI programming assistant tasked with determining whether an ordered list of commits on a single branch should be subdivided into stacked branches for a pull request workflow.
|
|
2
2
|
|
|
3
3
|
Your goal is to examine the commits and decide whether they form one cohesive unit or whether there are natural subdivision points that would benefit from stacked PRs.
|
|
4
4
|
|
|
@@ -9,9 +9,9 @@ Rules:
|
|
|
9
9
|
4. A single sub-branch (no subdivision) is a perfectly valid and common outcome
|
|
10
10
|
5. A single-commit sub-branch is valid if that commit is independently mergeable
|
|
11
11
|
|
|
12
|
-
${
|
|
12
|
+
${r(n?.splitting)}
|
|
13
13
|
|
|
14
|
-
${
|
|
14
|
+
${i}
|
|
15
15
|
- A short PR-style title (under 72 characters, imperative mood)
|
|
16
16
|
- A description of what the sub-branch accomplishes
|
|
17
17
|
- The commit IDs it contains (in order)
|
|
@@ -21,10 +21,10 @@ ${r}
|
|
|
21
21
|
Output your partition as JSON inside a <output> tag matching this schema:
|
|
22
22
|
|
|
23
23
|
\`\`\`json
|
|
24
|
-
${
|
|
24
|
+
${t}
|
|
25
25
|
\`\`\`
|
|
26
26
|
|
|
27
|
-
Return only the <output> tag and no other text.`}function
|
|
27
|
+
Return only the <output> tag and no other text.`}function r(e){let t=`Consider merge-order preference: even if sub-groups compile independently, keep them together if merging one without the other would be incomplete or confusing to reviewers.`;return e===`conservative`?`Keep commits as a single branch unless the sub-groups address completely different concerns with no merge-order preference between them.
|
|
28
28
|
${t}
|
|
29
29
|
Changes addressing the same pattern across different files should stay together. When in doubt, do not partition.`:e===`aggressive`?`Subdivide whenever there is a clear concern boundary between contiguous groups of commits. Each sub-group should be as focused as possible.
|
|
30
30
|
${t}
|
|
@@ -34,10 +34,10 @@ Do NOT partition just because commits are distinct. A branch with a refactor com
|
|
|
34
34
|
|
|
35
35
|
Changes addressing the same pattern across different files should stay together. For example, applying a similar fix to multiple functions belongs in one branch, not split across multiple stacked PRs.
|
|
36
36
|
|
|
37
|
-
${t}`}function
|
|
37
|
+
${t}`}function i(e){let{commits:t,orderedCommitIds:n,sourceBranchName:r,baseBranch:i,instructions:a}=e,o=n.map(e=>{let n=t.find(t=>t.id===e);return n?{id:n.id,message:n.message,explanation:n.explanation}:{id:e,message:`unknown`,explanation:``}}),s=`Determine whether these ordered commits should be subdivided into stacked branches:\n\n<commits>\n${JSON.stringify(o)}\n</commits>`,c=[];return r&&c.push(`Source branch: ${r}`),i&&c.push(`Base branch: ${i}`),c.length>0&&(s+=`\n\n<context>\n${c.join(`
|
|
38
38
|
`)}\n</context>`),a&&(s+=`\n\nAdditional instructions:\n${a}`),s+=`
|
|
39
39
|
|
|
40
|
-
Return the partition. Return only the <output> tag and no other text.`,s}function
|
|
40
|
+
Return the partition. Return only the <output> tag and no other text.`,s}function a(e){let t=o(e.strategy);return e.sourceBranchName&&(t+=`\n\nSource branch: ${e.sourceBranchName}`),e.baseBranch&&(t+=`\nBase branch: ${e.baseBranch}`),e.conventions?.promptFragment&&(t+=`\n\n${e.conventions.promptFragment}`),e.instructions&&(t+=`\n\nAdditional instructions:\n${e.instructions}`),t+=`
|
|
41
41
|
|
|
42
42
|
Return the partition as JSON inside <output> tags:
|
|
43
43
|
<output>
|
|
@@ -49,7 +49,7 @@ Return the partition as JSON inside <output> tags:
|
|
|
49
49
|
}
|
|
50
50
|
</output>
|
|
51
51
|
|
|
52
|
-
Return only the <output> tag and no other text.`,t}function
|
|
52
|
+
Return only the <output> tag and no other text.`,t}function o(e){let t=e?.splitting??`balanced`,n=`Based on the ordered commits for this branch, determine whether they should be subdivided into stacked sub-branches. Each sub-branch must be a contiguous range of commits.`,r=`Consider merge-order preference: even if sub-groups compile independently, keep them together if merging one without the other would be incomplete or confusing.`;return t===`conservative`?`${n}
|
|
53
53
|
|
|
54
54
|
Keep as a single branch unless sub-groups address completely different concerns with no merge-order preference. ${r}
|
|
55
55
|
When in doubt, do not partition.`:t===`aggressive`?`${n}
|
|
@@ -58,4 +58,4 @@ Subdivide whenever there is a clear concern boundary. Each stacked PR should tel
|
|
|
58
58
|
|
|
59
59
|
Only subdivide when the sub-groups are independently reviewable AND independently mergeable, and a reviewer would genuinely benefit from separate PRs. Do NOT partition just because commits are distinct — a refactor followed by a feature using it is a coherent single PR.
|
|
60
60
|
|
|
61
|
-
${r}`}export{
|
|
61
|
+
${r}`}export{a as buildPartitionContinuationPrompt,n as buildPartitionSystemPrompt,i as buildPartitionUserPrompt};
|
|
@@ -1,14 +1,48 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
1
|
//#region src/tasks/partition/schema.d.ts
|
|
4
|
-
declare const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
declare const partitionResultJsonSchema: {
|
|
3
|
+
readonly $schema: "https://json-schema.org/draft/2020-12/schema";
|
|
4
|
+
readonly type: "object";
|
|
5
|
+
readonly properties: {
|
|
6
|
+
readonly partitions: {
|
|
7
|
+
readonly minItems: 1;
|
|
8
|
+
readonly type: "array";
|
|
9
|
+
readonly items: {
|
|
10
|
+
readonly type: "object";
|
|
11
|
+
readonly properties: {
|
|
12
|
+
readonly branchName: {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
readonly minLength: 1;
|
|
15
|
+
readonly description: "Valid git branch name";
|
|
16
|
+
};
|
|
17
|
+
readonly title: {
|
|
18
|
+
readonly type: "string";
|
|
19
|
+
readonly description: "Short PR-style title for this sub-branch (under 72 chars, imperative mood, e.g. 'Refactor authentication middleware')";
|
|
20
|
+
};
|
|
21
|
+
readonly description: {
|
|
22
|
+
readonly type: "string";
|
|
23
|
+
readonly description: "What this sub-branch accomplishes";
|
|
24
|
+
};
|
|
25
|
+
readonly commitIds: {
|
|
26
|
+
readonly minItems: 1;
|
|
27
|
+
readonly type: "array";
|
|
28
|
+
readonly items: {
|
|
29
|
+
readonly type: "string";
|
|
30
|
+
};
|
|
31
|
+
readonly description: "Commit IDs in this partition (contiguous range)";
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
readonly required: readonly ["branchName", "title", "description", "commitIds"];
|
|
35
|
+
readonly additionalProperties: false;
|
|
36
|
+
};
|
|
37
|
+
readonly description: "Contiguous partitions of commits into stacked branches";
|
|
38
|
+
};
|
|
39
|
+
readonly rationale: {
|
|
40
|
+
readonly type: "string";
|
|
41
|
+
readonly description: "Explanation of why this partition was chosen";
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
readonly required: readonly ["partitions", "rationale"];
|
|
45
|
+
readonly additionalProperties: false;
|
|
46
|
+
};
|
|
13
47
|
//#endregion
|
|
14
|
-
export {
|
|
48
|
+
export { partitionResultJsonSchema };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
const e={$schema:`https://json-schema.org/draft/2020-12/schema`,type:`object`,properties:{partitions:{minItems:1,type:`array`,items:{type:`object`,properties:{branchName:{type:`string`,minLength:1,description:`Valid git branch name`},title:{type:`string`,description:`Short PR-style title for this sub-branch (under 72 chars, imperative mood, e.g. 'Refactor authentication middleware')`},description:{type:`string`,description:`What this sub-branch accomplishes`},commitIds:{minItems:1,type:`array`,items:{type:`string`},description:`Commit IDs in this partition (contiguous range)`}},required:[`branchName`,`title`,`description`,`commitIds`],additionalProperties:!1},description:`Contiguous partitions of commits into stacked branches`},rationale:{type:`string`,description:`Explanation of why this partition was chosen`}},required:[`partitions`,`rationale`],additionalProperties:!1};export{e as partitionResultJsonSchema};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{extractOutputTag as e}from"../ai-loop.js";import{
|
|
1
|
+
import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}from"../utils/json-schema.js";import{partitionResultJsonSchema as n}from"./schema.js";function r(r,c,l){let u=null;try{if(u=e(r),!u)return o([`No <output> tag found in response`],s());let d=t(n,JSON.parse(u));if(!d.success)return o(d.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),s());let f=d.data,p=[],m=[],h=new Set;for(let e=0;e<f.partitions.length;e++){let t=f.partitions[e];if(!a(t.branchName))return o([`Partition ${e}: '${t.branchName}' is not a valid git branch name`],`The branch name '${t.branchName}' is invalid. Git branch names cannot contain spaces, '..', '~', '^', ':', '\\', or end with '.lock'. Please provide a valid name.`);if(h.has(t.branchName))return o([`Duplicate partition branch name: '${t.branchName}'`],`Multiple partitions use the branch name '${t.branchName}'. Each partition must have a unique branch name. Please rename one of them.`);if(h.add(t.branchName),l?.has(t.branchName))return o([`Branch name '${t.branchName}' already exists in the repository`],`The branch name '${t.branchName}' already exists in the repository. Please choose a different name for partition ${e}.`);for(let e of t.commitIds)m.push(e);p.push({branchName:t.branchName,title:t.title,description:t.description,commitIds:t.commitIds})}let g=new Set,_=[];for(let e of m)g.has(e)&&_.push(e),g.add(e);if(_.length>0)return o([`Duplicate commit IDs: ${_.join(`, `)}`],`Some commits appear in multiple partitions. Each must be in exactly one.\n\nDuplicates: ${_.join(`, `)}`);let v=new Set(c),y=c.filter(e=>!g.has(e)),b=m.filter(e=>!v.has(e));return y.length>0?o([`Missing commits: ${y.join(`, `)}`],`Some commits are not assigned to any partition.\n\nMissing: ${y.join(`, `)}`):b.length>0?o([`Unknown commits: ${b.join(`, `)}`],`Some commit IDs don't match any known commit.\n\nUnknown: ${b.join(`, `)}`):i(p,c)?{valid:!0,result:{partitions:p,rationale:f.rationale}}:o([`Partitions are not contiguous`],`Each partition must contain a contiguous range of commits in the provided order. Please regroup into contiguous ranges.`)}catch(e){let t=e instanceof Error?e.message:String(e),n=(u??r).slice(-300);return o([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],s())}}function i(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function a(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function o(e,t){return{valid:!1,errors:e,retryFeedback:t}}function s(){return`Your response has an invalid structure. Return JSON inside <output> tags:
|
|
2
2
|
<output>
|
|
3
3
|
{
|
|
4
4
|
"partitions": [
|
|
@@ -8,4 +8,4 @@ import{extractOutputTag as e}from"../ai-loop.js";import{partitionResultSchema as
|
|
|
8
8
|
}
|
|
9
9
|
</output>
|
|
10
10
|
|
|
11
|
-
Return only the <output> tag and no other text.`}export{
|
|
11
|
+
Return only the <output> tag and no other text.`}export{r as validatePartitionResponse};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import{hunkContentForPrompt as e}from"../utils/redact.js";import{
|
|
2
|
-
Each commit message must contain both a summary and a description, separated by a blank line:
|
|
1
|
+
import{hunkContentForPrompt as e}from"../utils/redact.js";import{quickComposeResultJsonSchema as t}from"./schema.js";const n=JSON.stringify(t,null,2);function r(e,t=`branches`,r,i=!1){let c=`Write commit messages:\n${e?.commits.bodyStyle===`minimal`?`Each commit message is a single subject line with no body: concise, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves"). This repository's history overwhelmingly uses one-line commit messages, so do not add a description.`:`Each commit message must contain both a summary and a description, separated by a blank line:
|
|
3
2
|
- **Summary** (first line): concise, 50 chars max, third-person singular present tense ("Fixes", "Adds", "Improves"). Stands alone as the commit subject.
|
|
4
3
|
- **Blank line**: mandatory separator.
|
|
5
|
-
- **Description** (body): one or more lines explaining *why* the change was made, not *what* it does. Focus on motivation, trade-offs, or context a reviewer needs. For non-trivial changes, always include a description
|
|
4
|
+
- **Description** (body): one or more lines explaining *why* the change was made, not *what* it does. Focus on motivation, trade-offs, or context a reviewer needs. For non-trivial changes, always include a description.`}${e?.promptFragment?`\n\n${e.promptFragment}`:``}`,l=a(r?.commitGranularity),u=t===`branches`?o(r?.branchSplitting,e):``,d=i?s():``;return`You are a fast AI code organizer. Given a set of code diff hunks, organize them into well-structured commits${t===`branches`?` and branches`:``} in a single pass.
|
|
6
5
|
|
|
7
6
|
## Your task
|
|
8
7
|
|
|
@@ -13,7 +12,7 @@ ${t===`branches`?`4. Assign commits to branches — each branch is a cohesive un
|
|
|
13
12
|
${i?`5. If a branch should be split into stacked sub-branches for review, provide partitions.`:``}
|
|
14
13
|
|
|
15
14
|
## Commit style
|
|
16
|
-
${
|
|
15
|
+
${c}
|
|
17
16
|
${l}
|
|
18
17
|
${u}
|
|
19
18
|
${d}
|
|
@@ -36,21 +35,21 @@ Before producing the \`<output>\` tag, do a self-check: count the total number o
|
|
|
36
35
|
Output as JSON inside a <output> tag matching this schema:
|
|
37
36
|
|
|
38
37
|
\`\`\`json
|
|
39
|
-
${
|
|
38
|
+
${n}
|
|
40
39
|
\`\`\`
|
|
41
40
|
|
|
42
|
-
Return only the <output> tag and no other text.`}function
|
|
41
|
+
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}=t,d=`Organize these code hunks into commits:
|
|
43
42
|
|
|
44
43
|
<hunks>
|
|
45
44
|
${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}:{}})))}
|
|
46
45
|
</hunks>`,f=[];return a&&f.push(`Branch: ${a}`),o&&f.push(`Base: ${o}`),s&&f.push(`Issue: ${s}`),c&&f.push(`PR: ${c}`),f.length>0&&(d+=`\n\nContext:\n${f.join(`
|
|
47
46
|
`)}`),r?.length&&(d+=`\n\nOriginal commit messages (match their style):\n<commit_messages>\n${JSON.stringify(r)}\n</commit_messages>`),i?.length&&(d+=`\n\nOriginal commit order:\n${JSON.stringify(i)}`),l&&(d+=`\n\nAdditional instructions:\n${l}`),d+=`
|
|
48
47
|
|
|
49
|
-
Return only the <output> tag and no other text.`,d}function
|
|
48
|
+
Return only the <output> tag and no other text.`,d}function a(e){return e===`consolidated`?`
|
|
50
49
|
Prefer fewer, larger commits. Combine related changes into a single commit.`:e===`granular`?`
|
|
51
|
-
Prefer more, smaller commits. Each distinct change gets its own commit.`:``}function
|
|
50
|
+
Prefer more, smaller commits. Each distinct change gets its own commit.`:``}function o(e,t){let n=`\n## Branch guidelines\n- Each branch has a descriptive name, a short PR title (under 72 chars), and at least one commit.\n- ${t?.promptFragment?`Follow the repository's branch naming conventions.`:`Give each branch a descriptive name.`}`;return e===`conservative`?`${n}\n- Default to a single branch. Only split when concerns are completely unrelated.`:e===`aggressive`?`${n}\n- Create separate branches for each distinct concern. Maximize focus per PR.`:`${n}\n- Split when concerns are genuinely independent. Keep together when they form a coherent story.`}function s(){return`
|
|
52
51
|
## Partition guidelines (stacked sub-branches)
|
|
53
52
|
- If a branch has multiple distinct concerns, split it into partitions for stacked review.
|
|
54
53
|
- Each partition is a contiguous range of commits from orderedCommitIds.
|
|
55
54
|
- Each partition gets a unique, valid git branch name.
|
|
56
|
-
- Single partition (no split) is a valid outcome.`}export{
|
|
55
|
+
- Single partition (no split) is a valid outcome.`}export{r as buildQuickComposeSystemPrompt,i as buildQuickComposeUserPrompt};
|
|
@@ -1,27 +1,144 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
1
|
//#region src/tasks/quick-compose/schema.d.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
2
|
+
/** Shape of the LLM JSON output before the validator maps it to public types (`hunks` → `hunkIndices`). */
|
|
3
|
+
interface QuickComposeWire {
|
|
4
|
+
branches: QuickComposeWireBranch[];
|
|
5
|
+
rationale: string;
|
|
6
|
+
}
|
|
7
|
+
interface QuickComposeWireBranch {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
commits: QuickComposeWireCommit[];
|
|
13
|
+
orderedCommitIds: string[];
|
|
14
|
+
partitions?: QuickComposeWirePartition[];
|
|
15
|
+
}
|
|
16
|
+
interface QuickComposeWireCommit {
|
|
17
|
+
id: string;
|
|
18
|
+
message: string;
|
|
19
|
+
explanation: string;
|
|
20
|
+
hunks: number[];
|
|
21
|
+
}
|
|
22
|
+
interface QuickComposeWirePartition {
|
|
23
|
+
branchName: string;
|
|
24
|
+
title: string;
|
|
25
|
+
description: string;
|
|
26
|
+
commitIds: string[];
|
|
27
|
+
}
|
|
28
|
+
declare const quickComposeResultJsonSchema: {
|
|
29
|
+
readonly $schema: "https://json-schema.org/draft/2020-12/schema";
|
|
30
|
+
readonly type: "object";
|
|
31
|
+
readonly properties: {
|
|
32
|
+
readonly branches: {
|
|
33
|
+
readonly minItems: 1;
|
|
34
|
+
readonly type: "array";
|
|
35
|
+
readonly items: {
|
|
36
|
+
readonly type: "object";
|
|
37
|
+
readonly properties: {
|
|
38
|
+
readonly id: {
|
|
39
|
+
readonly type: "string";
|
|
40
|
+
readonly minLength: 1;
|
|
41
|
+
readonly description: "Unique branch ID";
|
|
42
|
+
};
|
|
43
|
+
readonly name: {
|
|
44
|
+
readonly type: "string";
|
|
45
|
+
readonly description: "Branch name (empty string for commits-only mode)";
|
|
46
|
+
};
|
|
47
|
+
readonly title: {
|
|
48
|
+
readonly type: "string";
|
|
49
|
+
readonly description: "Short PR-style title (under 72 chars, imperative mood)";
|
|
50
|
+
};
|
|
51
|
+
readonly description: {
|
|
52
|
+
readonly type: "string";
|
|
53
|
+
readonly description: "What this branch accomplishes";
|
|
54
|
+
};
|
|
55
|
+
readonly commits: {
|
|
56
|
+
readonly minItems: 1;
|
|
57
|
+
readonly type: "array";
|
|
58
|
+
readonly items: {
|
|
59
|
+
readonly type: "object";
|
|
60
|
+
readonly properties: {
|
|
61
|
+
readonly id: {
|
|
62
|
+
readonly type: "string";
|
|
63
|
+
readonly description: "Unique commit ID (e.g. commit-1)";
|
|
64
|
+
};
|
|
65
|
+
readonly message: {
|
|
66
|
+
readonly type: "string";
|
|
67
|
+
readonly description: "Commit message";
|
|
68
|
+
};
|
|
69
|
+
readonly explanation: {
|
|
70
|
+
readonly type: "string";
|
|
71
|
+
readonly description: "Detailed explanation of the commit";
|
|
72
|
+
};
|
|
73
|
+
readonly hunks: {
|
|
74
|
+
readonly type: "array";
|
|
75
|
+
readonly items: {
|
|
76
|
+
readonly type: "integer";
|
|
77
|
+
readonly minimum: -9007199254740991;
|
|
78
|
+
readonly maximum: 9007199254740991;
|
|
79
|
+
readonly description: "Hunk index";
|
|
80
|
+
};
|
|
81
|
+
readonly description: "Hunk indices included in this commit";
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
readonly required: readonly ["id", "message", "explanation", "hunks"];
|
|
85
|
+
readonly additionalProperties: false;
|
|
86
|
+
};
|
|
87
|
+
readonly description: "Commits in this branch";
|
|
88
|
+
};
|
|
89
|
+
readonly orderedCommitIds: {
|
|
90
|
+
readonly minItems: 1;
|
|
91
|
+
readonly type: "array";
|
|
92
|
+
readonly items: {
|
|
93
|
+
readonly type: "string";
|
|
94
|
+
};
|
|
95
|
+
readonly description: "Commit IDs in optimal order";
|
|
96
|
+
};
|
|
97
|
+
readonly partitions: {
|
|
98
|
+
readonly description: "Stacked sub-branches (only for multi-branch mode)";
|
|
99
|
+
readonly type: "array";
|
|
100
|
+
readonly items: {
|
|
101
|
+
readonly type: "object";
|
|
102
|
+
readonly properties: {
|
|
103
|
+
readonly branchName: {
|
|
104
|
+
readonly type: "string";
|
|
105
|
+
readonly minLength: 1;
|
|
106
|
+
readonly description: "Git branch name for this partition";
|
|
107
|
+
};
|
|
108
|
+
readonly title: {
|
|
109
|
+
readonly type: "string";
|
|
110
|
+
readonly description: "Short PR-style title";
|
|
111
|
+
};
|
|
112
|
+
readonly description: {
|
|
113
|
+
readonly type: "string";
|
|
114
|
+
readonly description: "What this partition accomplishes";
|
|
115
|
+
};
|
|
116
|
+
readonly commitIds: {
|
|
117
|
+
readonly minItems: 1;
|
|
118
|
+
readonly type: "array";
|
|
119
|
+
readonly items: {
|
|
120
|
+
readonly type: "string";
|
|
121
|
+
};
|
|
122
|
+
readonly description: "Ordered commit IDs in this partition";
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
readonly required: readonly ["branchName", "title", "description", "commitIds"];
|
|
126
|
+
readonly additionalProperties: false;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
readonly required: readonly ["id", "name", "title", "description", "commits", "orderedCommitIds"];
|
|
131
|
+
readonly additionalProperties: false;
|
|
132
|
+
};
|
|
133
|
+
readonly description: "Branches with grouped, ordered commits";
|
|
134
|
+
};
|
|
135
|
+
readonly rationale: {
|
|
136
|
+
readonly type: "string";
|
|
137
|
+
readonly description: "Brief explanation of the grouping and ordering rationale";
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
readonly required: readonly ["branches", "rationale"];
|
|
141
|
+
readonly additionalProperties: false;
|
|
142
|
+
};
|
|
26
143
|
//#endregion
|
|
27
|
-
export {
|
|
144
|
+
export { QuickComposeWire, QuickComposeWireBranch, QuickComposeWireCommit, QuickComposeWirePartition, quickComposeResultJsonSchema };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
const e={$schema:`https://json-schema.org/draft/2020-12/schema`,type:`object`,properties:{branches:{minItems:1,type:`array`,items:{type:`object`,properties:{id:{type:`string`,minLength:1,description:`Unique branch ID`},name:{type:`string`,description:`Branch name (empty string for commits-only mode)`},title:{type:`string`,description:`Short PR-style title (under 72 chars, imperative mood)`},description:{type:`string`,description:`What this branch accomplishes`},commits:{minItems:1,type:`array`,items:{type:`object`,properties:{id:{type:`string`,description:`Unique commit ID (e.g. commit-1)`},message:{type:`string`,description:`Commit message`},explanation:{type:`string`,description:`Detailed explanation of the commit`},hunks:{type:`array`,items:{type:`integer`,minimum:-9007199254740991,maximum:9007199254740991,description:`Hunk index`},description:`Hunk indices included in this commit`}},required:[`id`,`message`,`explanation`,`hunks`],additionalProperties:!1},description:`Commits in this branch`},orderedCommitIds:{minItems:1,type:`array`,items:{type:`string`},description:`Commit IDs in optimal order`},partitions:{description:`Stacked sub-branches (only for multi-branch mode)`,type:`array`,items:{type:`object`,properties:{branchName:{type:`string`,minLength:1,description:`Git branch name for this partition`},title:{type:`string`,description:`Short PR-style title`},description:{type:`string`,description:`What this partition accomplishes`},commitIds:{minItems:1,type:`array`,items:{type:`string`},description:`Ordered commit IDs in this partition`}},required:[`branchName`,`title`,`description`,`commitIds`],additionalProperties:!1}}},required:[`id`,`name`,`title`,`description`,`commits`,`orderedCommitIds`],additionalProperties:!1},description:`Branches with grouped, ordered commits`},rationale:{type:`string`,description:`Brief explanation of the grouping and ordering rationale`}},required:[`branches`,`rationale`],additionalProperties:!1};export{e as quickComposeResultJsonSchema};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import{extractOutputTag as e}from"../ai-loop.js";import{
|
|
1
|
+
import{extractOutputTag as e}from"../ai-loop.js";import{validateJsonSchema as t}from"../utils/json-schema.js";import{quickComposeResultJsonSchema as n}from"./schema.js";function r(r,c,l=`branches`,u=!1,d){let f=null;try{if(f=e(r),!f)return o([`No <output> tag found in response`],s(l,u));let p=t(n,JSON.parse(f));if(!p.success)return o(p.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`),s(l,u));let m=p.data,h=new Set,g=new Set,_=new Set,v=[],y=new Map,b=new Set,x=[],S=[],C=[];for(let e of m.branches){if(h.has(e.id))return o([`Duplicate branch ID: '${e.id}'`],s(l,u));if(h.add(e.id),l===`branches`&&e.name.length===0)return o([`Branch '${e.id}': missing or empty 'name'`],s(l,u));if(l===`branches`&&e.name.length>0){if(g.has(e.name))return o([`Duplicate branch name: '${e.name}'`],`Multiple branches use the name '${e.name}'. Each branch must have a unique name. Please rename one of them.`);if(g.add(e.name),d?.has(e.name))return o([`Branch name '${e.name}' already exists in the repository`],`The branch name '${e.name}' already exists in the repository. Please choose a different name for branch '${e.id}'.`)}let t=[],n=new Set;for(let r of e.commits){b.has(r.id)&&x.push(r.id),b.add(r.id),n.add(r.id);let i=[],a=l===`branches`?`${e.id}/${r.id}`:r.id;for(let e of r.hunks){_.has(e)&&v.push(e),_.add(e),i.push(e);let t=y.get(e);t?t.push(a):y.set(e,[a])}i.length===0&&S.push(r.id),t.push({id:r.id,message:r.message,explanation:r.explanation,hunkIndices:i})}let r=new Set(e.orderedCommitIds);if(r.size!==e.orderedCommitIds.length)return o([`Branch '${e.id}': orderedCommitIds contains duplicates`],`The orderedCommitIds list for branch '${e.id}' contains duplicates. Each commit ID must appear exactly once.`);let c=[...n].filter(e=>!r.has(e)),f=e.orderedCommitIds.filter(e=>!n.has(e));if(c.length>0)return o([`Branch '${e.id}': orderedCommitIds is missing commits: ${c.join(`, `)}`],`The orderedCommitIds for branch '${e.id}' must include every commit. Missing: ${c.join(`, `)}`);if(f.length>0)return o([`Branch '${e.id}': orderedCommitIds references unknown commits: ${f.join(`, `)}`],`The orderedCommitIds for branch '${e.id}' references commits that don't exist. Unknown: ${f.join(`, `)}`);let p=e.partitions?[...e.partitions]:void 0;if(p){let t=new Set,n=[];for(let r=0;r<p.length;r++){let i=p[r];if(!a(i.branchName))return o([`Branch '${e.id}', partition ${r}: '${i.branchName}' is not a valid git branch name`],`The branch name '${i.branchName}' is invalid. Git branch names cannot contain spaces, '..', '~', '^', ':', '\\', or end with '.lock'. Please provide a valid name.`);if(t.has(i.branchName))return o([`Branch '${e.id}': duplicate partition name '${i.branchName}'`],`Multiple partitions use the branch name '${i.branchName}'. Each must be unique.`);if(t.add(i.branchName),d?.has(i.branchName))return o([`Partition branch name '${i.branchName}' already exists in the repository`],`The branch name '${i.branchName}' already exists in the repository. Please choose a different name.`);for(let e of i.commitIds)n.push(e)}let s=new Set(n),c=e.orderedCommitIds.filter(e=>!s.has(e)),l=n.filter(e=>!r.has(e));if(c.length>0)return o([`Branch '${e.id}': partition is missing commits: ${c.join(`, `)}`],`All commits must be assigned to a partition. Missing: ${c.join(`, `)}`);if(l.length>0)return o([`Branch '${e.id}': partition references unknown commits: ${l.join(`, `)}`],`Partition references unknown commits: ${l.join(`, `)}`);if(!i(p,e.orderedCommitIds))return o([`Branch '${e.id}': partitions are not contiguous`],`Each partition must contain a contiguous range of commits in the ordered list. Please regroup into contiguous ranges.`)}C.push({id:e.id,name:e.name,title:e.title,description:e.description,commits:t,orderedCommitIds:e.orderedCommitIds,partitions:p})}if(x.length>0)return o([`Duplicate commit IDs: ${x.join(`, `)}`],`Some commit IDs appear multiple times. Each must be unique.\n\nDuplicates: ${x.join(`, `)}`);if(S.length>0)return o([`Empty commits (no hunks assigned): ${S.join(`, `)}`],`Every commit must contain at least one hunk. These commits have an empty \`hunks\` array: ${S.join(`, `)}.
|
|
2
2
|
|
|
3
3
|
Either assign at least one hunk to each of these commits, or remove them entirely. If a commit's purpose is already covered by another commit, remove the duplicate. If a commit is supposed to cover specific hunks, assign those hunk indices to it. Every hunk index still needs to appear in exactly one commit.`);if(v.length>0){let e=[...new Set(v)].sort((e,t)=>e-t),t=e.map(e=>` hunk ${e} → ${(y.get(e)??[]).join(`, `)}`).join(`
|
|
4
|
-
`),n=[...y.values()].reduce((e,t)=>e+t.length,0);return
|
|
4
|
+
`),n=[...y.values()].reduce((e,t)=>e+t.length,0);return o([`Duplicate hunks: ${e.join(`, `)}`],`Some hunks appear in multiple commits. Each hunk can only be used once.
|
|
5
5
|
|
|
6
|
-
Your response listed ${n} hunk references across ${y.size} unique indices. Expected: ${
|
|
6
|
+
Your response listed ${n} hunk references across ${y.size} unique indices. Expected: ${c} references across ${c} unique indices.
|
|
7
7
|
|
|
8
8
|
Duplicates (each hunk index → the commits it landed in):
|
|
9
9
|
${t}
|
|
10
10
|
|
|
11
|
-
To fix: for each duplicated hunk index, decide which one commit it truly belongs in and remove it from the others. Do not split a single hunk across two commits — git only knows how to apply a hunk once.`)}let w=new Set(Array.from({length:
|
|
11
|
+
To fix: for each duplicated hunk index, decide which one commit it truly belongs in and remove it from the others. Do not split a single hunk across two commits — git only knows how to apply a hunk once.`)}let w=new Set(Array.from({length:c},(e,t)=>t)),T=[...w].filter(e=>!_.has(e)),E=[..._].filter(e=>!w.has(e));return T.length>0?o([`Missing hunks: ${T.join(`, `)}`],`Some hunks are not assigned to any commit. All hunks must be included.\n\nMissing: ${T.join(`, `)}`):E.length>0?o([`Extra hunks: ${E.join(`, `)}`],`Some hunks in your response don't exist in the input.\n\nExtra: ${E.join(`, `)}`):{valid:!0,result:{branches:C,rationale:m.rationale}}}catch(e){let t=e instanceof Error?e.message:String(e),n=(f??r).slice(-300);return o([`Failed to parse response as valid JSON: ${t}`,`Response tail: ${n}`],s(l,u))}}function i(e,t){let n=new Map(t.map((e,t)=>[e,t])),r=0;for(let t of e)for(let e of t.commitIds){let t=n.get(e);if(t===void 0||t!==r)return!1;r++}return r===t.length}function a(e){return e.startsWith(`-`)||e.startsWith(`.`)||e.endsWith(`.lock`)||e.endsWith(`.`)||e.endsWith(`/`)||e.includes(`..`)||e.includes(`~`)||e.includes(`^`)||e.includes(`:`)||e.includes(`\\`)||e.includes(` `)||e.includes(`?`)||e.includes(`*`)||e.includes(`[`)||e.includes(`@{`)?!1:e.length>0}function o(e,t){return{valid:!1,errors:e,retryFeedback:t}}function s(e,t){return`Your response has an invalid structure. Please return JSON inside <output> tags:
|
|
12
12
|
<output>
|
|
13
13
|
{
|
|
14
14
|
"branches": [
|
|
@@ -30,4 +30,4 @@ To fix: for each duplicated hunk index, decide which one commit it truly belongs
|
|
|
30
30
|
}
|
|
31
31
|
</output>
|
|
32
32
|
|
|
33
|
-
Return only the <output> tag and no other text.`}export{
|
|
33
|
+
Return only the <output> tag and no other text.`}export{r as validateQuickComposeResponse};
|
|
@@ -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{buildRefinementPrompt as r}from"./prompt.js";import{validateRefineResponse as i}from"./validator.js";async function a(a){if(a.session.messages.length===0)throw new e(`refine: session must contain prior messages from a previous compose call.`);if(a.hunkCount<=0)throw new e(`refine: hunkCount must be a positive integer.`);let o=a.mode??`branches`,s=new Map;for(let e of a.lockedCommits??[])s.set(e.id,e);let c=n(r({instructions:a.instructions,lockedCommits:a.lockedCommits??[],clientContext:a.clientContext,hunkCount:a.hunkCount,mode:o}),a.promptOverride),l=await t({model:a.model,systemPrompt:void 0,userPrompt:c,session:a.session,maxRetries:a.maxRetries,maxTokens:a.maxTokens,onProgress:a.onProgress,cancellation:a.cancellation,onBeforeModelCall:a.onBeforeModelCall,onAfterModelCall:a.onAfterModelCall,onBeforePrompt:a.onBeforePrompt,validate:e=>i(e,{hunkCount:a.hunkCount,mode:o,lockedCommits:s}),taskName:`refine`}),u={...l.session,completedSteps:a.session.completedSteps};return{result:l.result,session:u,usage:l.usage,attempts:l.attempts,durationMs:l.durationMs,callIds:l.callIds}}export{a as refine};
|
|
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{buildRefinementPrompt as r}from"./prompt.js";import{validateRefineResponse as i}from"./validator.js";async function a(a){if(a.session.messages.length===0)throw new e(`refine: session must contain prior messages from a previous compose call.`);if(a.hunkCount<=0)throw new e(`refine: hunkCount must be a positive integer.`);let o=a.mode??`branches`,s=new Map;for(let e of a.lockedCommits??[])s.set(e.id,e);let c=n(r({instructions:a.instructions,lockedCommits:a.lockedCommits??[],clientContext:a.clientContext,hunkCount:a.hunkCount,mode:o}),a.promptOverride),l=await t({model:a.model,systemPrompt:void 0,userPrompt:c,session:a.session,maxRetries:a.maxRetries,maxTokens:a.maxTokens,onProgress:a.onProgress,cancellation:a.cancellation,onBeforeModelCall:a.onBeforeModelCall,onAfterModelCall:a.onAfterModelCall,onBeforePrompt:a.onBeforePrompt,validate:e=>i(e,{hunkCount:a.hunkCount,mode:o,lockedCommits:s,lockedRuns:a.lockedRuns}),taskName:`refine`}),u={...l.session,completedSteps:a.session.completedSteps};return{result:l.result,session:u,usage:l.usage,attempts:l.attempts,durationMs:l.durationMs,callIds:l.callIds}}export{a as refine};
|
|
@@ -25,6 +25,20 @@ interface RefineInput {
|
|
|
25
25
|
* prompt and enforces their presence + substitutes their content (id, message,
|
|
26
26
|
* explanation, hunkIndices) into the AI's response after validation. */
|
|
27
27
|
lockedCommits?: readonly CommitSuggestion[];
|
|
28
|
+
/**
|
|
29
|
+
* The locked commits partitioned into the maximal runs of *consecutive* locked commits
|
|
30
|
+
* (no commit between them) within each branch of the prior plan, in that plan's original
|
|
31
|
+
* order — one entry per run, singletons included. Flattening yields the locked commits'
|
|
32
|
+
* original relative order.
|
|
33
|
+
*
|
|
34
|
+
* The validator uses this to undo AI reshuffling in the post-substitution cleanup: it
|
|
35
|
+
* restores the locked commits' relative order within each output branch and pulls each
|
|
36
|
+
* multi-commit run back together contiguously. Adjacency can't be derived from
|
|
37
|
+
* `lockedCommits` alone (which carries no positional context), so callers driving the task
|
|
38
|
+
* directly must supply it; the workflow computes it from the prior plan. Omit for no
|
|
39
|
+
* reordering.
|
|
40
|
+
*/
|
|
41
|
+
lockedRuns?: readonly (readonly string[])[];
|
|
28
42
|
/** Per-call user instructions appended to the refinement prompt. */
|
|
29
43
|
instructions?: string;
|
|
30
44
|
/** Free-text consumer-supplied context (e.g. UI-ordering notes) injected verbatim into
|
|
@@ -7,13 +7,17 @@ interface ValidateRefineParams {
|
|
|
7
7
|
hunkCount: number;
|
|
8
8
|
mode: 'commits' | 'branches';
|
|
9
9
|
lockedCommits: Map<string, CommitSuggestion>;
|
|
10
|
+
/** Maximal runs of consecutive locked commits per prior-plan branch, in original order.
|
|
11
|
+
* See {@link import('./types.js').RefineInput.lockedRuns}. */
|
|
12
|
+
lockedRuns?: readonly (readonly string[])[];
|
|
10
13
|
}
|
|
11
14
|
/**
|
|
12
15
|
* Two-stage validation for the refine task:
|
|
13
16
|
*
|
|
14
17
|
* 1. Base structural validation via `validateGroupResponse` — same invariants the rest of
|
|
15
18
|
* the library enforces (hunk index uniqueness + coverage, non-empty commits, unique ids).
|
|
16
|
-
* 2. Locked-commits check + content substitution + post-substitution invariant check
|
|
19
|
+
* 2. Locked-commits check + content substitution + post-substitution invariant check +
|
|
20
|
+
* locked-order restoration (see {@link restoreLockedOrdering}).
|
|
17
21
|
*
|
|
18
22
|
* The substitution step is what makes refine resilient to minor AI drift: when the AI emits
|
|
19
23
|
* a commit with a locked id but slightly different message text, we silently restore the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateGroupResponse as e}from"../group/validator.js";function t(t,r){let i=e(t,r.hunkCount,r.mode);if(!i.valid||i.result===void 0)return i;let
|
|
1
|
+
import{validateGroupResponse as e}from"../group/validator.js";function t(t,r){let i=e(t,r.hunkCount,r.mode);if(!i.valid||i.result===void 0)return i;let o=i.result;if(r.lockedCommits.size===0)return i;let s=new Map;for(let e of o.branches)for(let t=0;t<e.commits.length;t++)s.set(e.commits[t].id,{branch:e,commitIndex:t});let c=[];for(let e of r.lockedCommits.keys())s.has(e)||c.push(e);if(c.length>0){let e=c.map(e=>`'${e}'`).join(`, `);return{valid:!1,errors:[`Missing locked commits: ${e}`],retryFeedback:`Your response is missing the following LOCKED commits, which must always appear in the output: ${e}. Re-emit the plan with these commits present (preserving their id, message, explanation, and hunks exactly).`}}for(let[e,t]of r.lockedCommits){let n=s.get(e);if(n==null)continue;let r=n.branch.commits[n.commitIndex];n.branch.commits[n.commitIndex]={...r,id:t.id,message:t.message,explanation:t.explanation,hunkIndices:[...t.hunkIndices]}}let l=a(o,r.hunkCount);return l==null?(n(o,r.lockedRuns),{valid:!0,result:o}):{valid:!1,errors:[l],retryFeedback:`After restoring the locked commits' original hunk assignments, the resulting plan is structurally invalid: ${l}. This means you assigned hunks belonging to a locked commit to a different commit. Locked commits' hunks must remain exclusive to them — do not move those hunks elsewhere.`}}function n(e,t){if(t==null||t.length===0)return;let n=t.flat(),a=new Map(n.map((e,t)=>[e,t])),o=new Set(n);for(let t of e.branches)r(t,o,a);for(let n of t)n.length>=2&&i(e,n)}function r(e,t,n){let r=[],i=[];for(let n=0;n<e.commits.length;n++)t.has(e.commits[n].id)&&(r.push(n),i.push(e.commits[n]));if(!(i.length<2)){i.sort((e,t)=>(n.get(e.id)??0)-(n.get(t.id)??0));for(let t=0;t<r.length;t++)e.commits[r[t]]=i[t]}}function i(e,t){let n=new Set(t),r=e.branches.filter(e=>e.commits.some(e=>n.has(e.id)));if(r.length!==1)return;let i=r[0],a=[];for(let e of t){let t=i.commits.find(t=>t.id===e);t!=null&&a.push(t)}if(a.length<2)return;let o=Math.min(...a.map(e=>i.commits.indexOf(e))),s=i.commits.slice(0,o).filter(e=>!n.has(e.id)).length,c=i.commits.filter(e=>!n.has(e.id));c.splice(s,0,...a),i.commits=c}function a(e,t){let n=new Set,r=[],i=[],a=new Set,o=[];for(let t of e.branches)for(let e of t.commits){if(a.has(e.id)&&o.push(e.id),a.add(e.id),e.hunkIndices.length===0){i.push(e.id);continue}for(let t of e.hunkIndices)n.has(t)&&r.push(t),n.add(t)}if(o.length>0)return`duplicate commit ids after substitution: ${o.join(`, `)}`;if(i.length>0)return`empty commits after substitution (no hunks assigned): ${i.join(`, `)}`;if(r.length>0)return`hunk indices appear in more than one commit: ${[...new Set(r)].join(`, `)}`;let s=[];for(let e=0;e<t;e++)n.has(e)||s.push(e);if(s.length>0)return`hunk indices missing from the plan: ${s.join(`, `)}`;let c=[...n].filter(e=>e>=t||e<0);if(c.length>0)return`hunk indices outside the valid range 0..${String(t-1)}: ${c.join(`, `)}`}export{t as validateRefineResponse};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ComposeHunk } from "../types.js";
|
|
2
|
+
import { RedactHunkContent } from "./redact.js";
|
|
3
|
+
|
|
4
|
+
//#region src/tasks/utils/budget.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Ceiling on estimated input tokens for a single hunk-bearing prompt.
|
|
7
|
+
*
|
|
8
|
+
* Production p99 for the composer sits around 150k input tokens; pathological diffs
|
|
9
|
+
* (lockfile-dominated) reach 700k-1.7M. 180k leaves headroom above normal usage while
|
|
10
|
+
* still forcing degradation on the pathological tail, and stays comfortably inside the
|
|
11
|
+
* 200k context window of the models the composer targets.
|
|
12
|
+
*/
|
|
13
|
+
declare const DEFAULT_INPUT_TOKEN_BUDGET = 180000;
|
|
14
|
+
/** Rough char→token estimate shared with `runAiLoop`'s prompt-gate heuristic. */
|
|
15
|
+
declare function estimateTokens(chars: number): number;
|
|
16
|
+
interface AssembledHunkPrompt {
|
|
17
|
+
prompt: string;
|
|
18
|
+
/**
|
|
19
|
+
* Hunks that lost their real body to budget pressure (full to condensed). Non-zero means
|
|
20
|
+
* content-dependent reasoning (analyze/group dependency detection) saw less than the full diff.
|
|
21
|
+
*/
|
|
22
|
+
hunksCondensedForBudget: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Serializes `hunks` into the `<hunks>` JSON block and assembles the full prompt via
|
|
26
|
+
* `assemble`, keeping estimated input tokens under `inputTokenBudget`. Noise hunks (per
|
|
27
|
+
* `redactHunkContent`) start redacted, `condensed` forces metadata-only, else full; over
|
|
28
|
+
* budget, hunks are condensed biggest-first, throwing {@link DiffTooLargeError} if they still
|
|
29
|
+
* cannot fit. `assemble` is called with `''` to measure fixed overhead, so it must be pure.
|
|
30
|
+
*/
|
|
31
|
+
declare function assembleHunkPrompt(params: {
|
|
32
|
+
hunks: ComposeHunk[];
|
|
33
|
+
assemble: (hunksJson: string) => string;
|
|
34
|
+
condensed?: boolean;
|
|
35
|
+
redactHunkContent?: RedactHunkContent;
|
|
36
|
+
inputTokenBudget?: number;
|
|
37
|
+
systemPromptChars?: number;
|
|
38
|
+
}): AssembledHunkPrompt;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { AssembledHunkPrompt, DEFAULT_INPUT_TOKEN_BUDGET, assembleHunkPrompt, estimateTokens };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{DiffTooLargeError as e}from"../../errors/task.js";import{estimatedCharactersPerToken as t}from"../ai-loop.js";import{REDACTED_HUNK_CONTENT as n}from"./redact.js";const r=18e4;function i(e){return Math.ceil(e/t)}function a(e,t){let r={...e.isBinary?{isBinary:!0}:{},...e.isRename?{isRename:!0}:{},...e.originalFileName?{originalFileName:e.originalFileName}:{}};return t===`condensed`?{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,additions:e.additions,deletions:e.deletions,...r}:t===`redacted`?{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,additions:e.additions,deletions:e.deletions,content:n,...r}:{index:e.index,fileName:e.fileName,hunkHeader:e.hunkHeader,content:e.content,...r}}function o(e,t){return JSON.stringify(a(e,t)).length}function s(e,t,n,r){let a=new Map(t),s=2+Math.max(0,e.length-1),c=0,l=e.map(e=>{let t=a.get(e.index),n=o(e,`condensed`),r=t===`condensed`?n:o(e,t);return c+=r,{hunk:e,currentLen:r,condensedLen:n}}),u=()=>i(r+s+c),d=()=>u()>n;if(d()){let e=l.map(e=>({hunk:e.hunk,savings:e.currentLen-e.condensedLen})).filter(e=>e.savings>0).sort((e,t)=>t.savings-e.savings);for(let{hunk:t,savings:n}of e){if(!d())break;c-=n,a.set(t.index,`condensed`)}}return{modes:a,estimatedTokens:u(),tooLarge:d()}}function c(t){let{hunks:n,assemble:i,condensed:o,redactHunkContent:c,inputTokenBudget:l=r,systemPromptChars:u=0}=t,d=new Map(n.map(e=>o?[e.index,`condensed`]:c?.(e)?[e.index,`redacted`]:[e.index,`full`])),{modes:f,estimatedTokens:p,tooLarge:m}=s(n,d,l,i(``).length+u);if(m)throw new e(p,l);let h=0;for(let e of n)d.get(e.index)===`full`&&f.get(e.index)===`condensed`&&h++;return{prompt:i(JSON.stringify(n.map(e=>a(e,f.get(e.index))))),hunksCondensedForBudget:h}}export{r as DEFAULT_INPUT_TOKEN_BUDGET,c as assembleHunkPrompt,i as estimateTokens};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/tasks/utils/json-schema.d.ts
|
|
2
|
+
interface SchemaIssue {
|
|
3
|
+
path: (string | number)[];
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
type SchemaResult<T> = {
|
|
7
|
+
success: true;
|
|
8
|
+
data: T;
|
|
9
|
+
} | {
|
|
10
|
+
success: false;
|
|
11
|
+
issues: SchemaIssue[];
|
|
12
|
+
};
|
|
13
|
+
interface SchemaAnnotations {
|
|
14
|
+
$schema?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
}
|
|
17
|
+
interface ObjectSchemaDef extends SchemaAnnotations {
|
|
18
|
+
type: 'object';
|
|
19
|
+
properties: Readonly<Record<string, JsonSchemaDef>>;
|
|
20
|
+
required: readonly string[];
|
|
21
|
+
additionalProperties: false;
|
|
22
|
+
}
|
|
23
|
+
interface ArraySchemaDef extends SchemaAnnotations {
|
|
24
|
+
type: 'array';
|
|
25
|
+
items: JsonSchemaDef;
|
|
26
|
+
minItems?: number;
|
|
27
|
+
default?: readonly unknown[];
|
|
28
|
+
}
|
|
29
|
+
interface StringSchemaDef extends SchemaAnnotations {
|
|
30
|
+
type: 'string';
|
|
31
|
+
minLength?: number;
|
|
32
|
+
maxLength?: number;
|
|
33
|
+
enum?: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
interface NumberSchemaDef extends SchemaAnnotations {
|
|
36
|
+
type: 'number' | 'integer';
|
|
37
|
+
minimum?: number;
|
|
38
|
+
maximum?: number;
|
|
39
|
+
}
|
|
40
|
+
interface BooleanSchemaDef extends SchemaAnnotations {
|
|
41
|
+
type: 'boolean';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The JSON-Schema subset used by the task output schemas. Deliberately closed: no `anyOf`,
|
|
45
|
+
* `const`, `$ref`, `pattern`, `format`, record types, or type arrays — widen only when a task
|
|
46
|
+
* schema actually needs more, and keep prompt snapshots byte-stable when doing so.
|
|
47
|
+
*/
|
|
48
|
+
type JsonSchemaDef = ObjectSchemaDef | ArraySchemaDef | StringSchemaDef | NumberSchemaDef | BooleanSchemaDef;
|
|
49
|
+
/**
|
|
50
|
+
* Validates `value` against `schema` with the same observable semantics the previous zod
|
|
51
|
+
* schemas had: every issue is collected in one pass, unknown object keys are silently
|
|
52
|
+
* stripped, a `required` property whose subschema carries `default` is filled with a fresh
|
|
53
|
+
* clone of that default when absent, and issue messages mirror zod v4 wording so telemetry
|
|
54
|
+
* and error logs stay comparable across the migration. One deliberate divergence: on a type
|
|
55
|
+
* mismatch zod could add a second size-constraint issue keyed off the wrong-typed value's
|
|
56
|
+
* `.length`; this validator reports only the type issue (same failure outcome, one issue
|
|
57
|
+
* fewer). On success `data` is a rebuilt tree, never the input by reference.
|
|
58
|
+
*/
|
|
59
|
+
declare function validateJsonSchema<T>(schema: JsonSchemaDef, value: unknown): SchemaResult<T>;
|
|
60
|
+
//#endregion
|
|
61
|
+
export { ArraySchemaDef, BooleanSchemaDef, JsonSchemaDef, NumberSchemaDef, ObjectSchemaDef, SchemaIssue, SchemaResult, StringSchemaDef, validateJsonSchema };
|