@neabyte/v4a-diff 0.1.0 → 0.2.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/README.md +50 -5
- package/dist/index.cjs +7 -7
- package/dist/index.d.cts +28 -8
- package/dist/index.d.mts +28 -8
- package/dist/index.d.ts +28 -8
- package/dist/index.mjs +7 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ V4A is a context-anchored diff format designed for LLM tool calling. Instead of
|
|
|
18
18
|
|
|
19
19
|
## Features
|
|
20
20
|
|
|
21
|
+
- **CRUD modes** - Update, create, delete, and move files in a single API.
|
|
21
22
|
- **Token efficient** - Returns only changed lines, not the entire file.
|
|
22
23
|
- **Instant rollback** - Original source included in result, no manual tracking.
|
|
23
24
|
- **Structured diff** - Line-by-line add/delete/equal metadata with line numbers.
|
|
@@ -55,6 +56,9 @@ Or via [esm.sh](https://esm.sh):
|
|
|
55
56
|
|
|
56
57
|
## Usage
|
|
57
58
|
|
|
59
|
+
> [!WARNING]
|
|
60
|
+
> This is a pure text-to-text API with no filesystem I/O, designed to run in any environment including browsers, CLI, and terminal applications. Filesystem operations such as reading, writing, and deleting files are left to the consumer.
|
|
61
|
+
|
|
58
62
|
```ts
|
|
59
63
|
import V4A from '@neabyte/v4a-diff'
|
|
60
64
|
|
|
@@ -104,6 +108,37 @@ const result = V4A.apply(
|
|
|
104
108
|
// result.text === 'export const PORT = 8080\nexport const HOST = "localhost"'
|
|
105
109
|
```
|
|
106
110
|
|
|
111
|
+
### Delete a file
|
|
112
|
+
|
|
113
|
+
> [!NOTE]
|
|
114
|
+
> Source is needed to produce the structured diff output.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const result = V4A.apply('const x = 1\nconst y = 2', '*** Delete File: /src/config.ts', 'delete')
|
|
118
|
+
// result.text === ''
|
|
119
|
+
// result.source === 'const x = 1\nconst y = 2'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Move a file (apply diff at new path)
|
|
123
|
+
|
|
124
|
+
> [!NOTE]
|
|
125
|
+
> Source is needed to produce the structured diff output.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// Move with edits
|
|
129
|
+
const result = V4A.apply(
|
|
130
|
+
'const x = 1',
|
|
131
|
+
'*** Move to: /src/new-path.ts\n@@\n-const x = 1\n+const x = 2',
|
|
132
|
+
'move'
|
|
133
|
+
)
|
|
134
|
+
// result.text === 'const x = 2'
|
|
135
|
+
|
|
136
|
+
// Move without edits (pure rename)
|
|
137
|
+
const renamed = V4A.apply('const x = 1\nconst y = 2', '*** Move to: /src/new-path.ts', 'move')
|
|
138
|
+
// renamed.text === 'const x = 1\nconst y = 2'
|
|
139
|
+
// renamed.diff lines are all { type: 'equal' }
|
|
140
|
+
```
|
|
141
|
+
|
|
107
142
|
### Multi-hunk edits
|
|
108
143
|
|
|
109
144
|
```ts
|
|
@@ -117,11 +152,11 @@ const result = V4A.apply(
|
|
|
117
152
|
|
|
118
153
|
### `V4A.apply(sourceText, diffText, mode?)`
|
|
119
154
|
|
|
120
|
-
| Parameter | Type
|
|
121
|
-
| ------------ |
|
|
122
|
-
| `sourceText` | `string`
|
|
123
|
-
| `diffText` | `string`
|
|
124
|
-
| `mode` | `
|
|
155
|
+
| Parameter | Type | Description |
|
|
156
|
+
| ------------ | --------------- | --------------------------------------------------------- |
|
|
157
|
+
| `sourceText` | `string` | Original file content |
|
|
158
|
+
| `diffText` | `string` | V4A format diff string |
|
|
159
|
+
| `mode` | `ApplyDiffMode` | `'update'` (default), `'create'`, `'delete'`, or `'move'` |
|
|
125
160
|
|
|
126
161
|
**Returns:** `ApplyDiffResult`
|
|
127
162
|
|
|
@@ -152,10 +187,20 @@ type DiffLine = {
|
|
|
152
187
|
*** End Patch
|
|
153
188
|
```
|
|
154
189
|
|
|
190
|
+
Other file operation headers:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
*** Add File: {path}
|
|
194
|
+
*** Delete File: {path}
|
|
195
|
+
*** Move to: {new_path}
|
|
196
|
+
```
|
|
197
|
+
|
|
155
198
|
- `@@` followed by text anchors to a matching line in the source file.
|
|
156
199
|
- A bare `@@` alone means "start of file."
|
|
157
200
|
- Every line in a hunk must start with ``(space),`-`, or `+`.
|
|
158
201
|
- `*** Add File:` with `+` prefixed lines creates new files.
|
|
202
|
+
- `*** Delete File:` marks a file for deletion.
|
|
203
|
+
- `*** Move to:` renames/moves a file while applying edits.
|
|
159
204
|
|
|
160
205
|
## LLM Tool Schemas
|
|
161
206
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"use strict";class
|
|
2
|
-
`)}static parseUpdateDiff(e,
|
|
3
|
-
`),i=[];let c=0;for(;!this.isDone(
|
|
4
|
-
`)
|
|
5
|
-
`),
|
|
6
|
-
`),r=[],i=[];let c=0,
|
|
7
|
-
`),diff:i,source:e}}static
|
|
1
|
+
"use strict";class s{static anchorStrategies=[{mapFn:e=>e,fuzzScore:0},{mapFn:e=>e.trim(),fuzzScore:1},{mapFn:this.normalizeUnicode.bind(this),fuzzScore:10}];static contextStrategies=[{mapFn:e=>e,fuzzScore:0},{mapFn:e=>e.trimEnd(),fuzzScore:1},{mapFn:e=>e.trim(),fuzzScore:100},{mapFn:this.normalizeUnicode.bind(this),fuzzScore:1e3}];static unicodeReplacements={"\u2010":"-","\u2011":"-","\u2012":"-","\u2013":"-","\u2014":"-","\u2015":"-","\u2212":"-","\u2018":"'","\u2019":"'","\u201A":"'","\u201B":"'","\u201C":'"',"\u201D":'"',"\u201E":'"',"\u201F":'"',"\xA0":" ","\u2002":" ","\u2003":" ","\u2004":" ","\u2005":" ","\u2006":" ","\u2007":" ","\u2008":" ","\u2009":" ","\u200A":" ","\u202F":" ","\u205F":" ","\u3000":" "};static unicodePattern=new RegExp(`[${Object.keys(this.unicodeReplacements).join("")}]`,"g");static findContext(e,n,t,r){if(r){const i=this.findContextCore(e,n,Math.max(0,e.length-n.length));if(i.matchedIndex!==-1)return i;const c=this.findContextCore(e,n,t);return{matchedIndex:c.matchedIndex,fuzzScore:c.fuzzScore+1e4}}return this.findContextCore(e,n,t)}static resolveAnchor(e,n,t,r){for(const i of this.anchorStrategies){const c=i.mapFn(e);let l=!1;for(let a=0;a<t;a+=1)if(i.mapFn(n[a])===c){l=!0;break}if(l)return t;const o=this.searchLines(n,e,t,i.mapFn);if(o!==-1)return r.fuzzScore+=i.fuzzScore,o}return t}static findContextCore(e,n,t){if(!n.length)return{matchedIndex:t,fuzzScore:0};for(const r of this.contextStrategies)for(let i=t;i<e.length;i+=1)if(this.sliceEquals(e,n,i,r.mapFn))return{matchedIndex:i,fuzzScore:r.fuzzScore};return{matchedIndex:-1,fuzzScore:0}}static normalizeUnicode(e){return e.trim().replace(this.unicodePattern,n=>this.unicodeReplacements[n])}static searchLines(e,n,t,r){const i=r(n);for(let c=t;c<e.length;c+=1)if(r(e[c])===i)return c;return-1}static sliceEquals(e,n,t,r){if(t+n.length>e.length)return!1;for(let i=0;i<n.length;i+=1)if(r(e[t+i])!==r(n[i]))return!1;return!0}}class p{static endFile="*** End of File";static prefixToMode={"+":"add","-":"delete"," ":"keep"};static terminators=["*** End Patch","*** Update File:","*** Delete File:","*** Add File:","*** Move to:"];static normalizeDiffLines(e){const n=e.split(/\r?\n/);return n.at(-1)===""&&n.pop(),n}static parseCreateDiff(e){const n=this.createState(e),t=[];for(;!this.isDone(n,!1);){const r=n.lines[n.currentIndex];if(n.currentIndex+=1,!r.startsWith("+"))throw new SyntaxError(`line ${n.currentIndex} expected '+' prefix but got "${r}"`);t.push(r.slice(1))}return t.join(`
|
|
2
|
+
`)}static parseUpdateDiff(e,n){const t=this.createState(e),r=n.split(`
|
|
3
|
+
`),i=[];let c=0;for(;!this.isDone(t,!0);){const l=this.consumePrefix(t,"@@ "),o=!l&&t.lines[t.currentIndex]==="@@";if(o&&(t.currentIndex+=1),!l&&!o&&c!==0)throw new SyntaxError(`line ${t.currentIndex+1} unexpected line "${t.lines[t.currentIndex]}"`);l?.trim()&&(c=s.resolveAnchor(l,r,c,t));const a=this.readSection(t.lines,t.currentIndex),h=s.findContext(r,a.contextLines,c,a.isEndOfFile);if(h.matchedIndex===-1){const d=a.isEndOfFile?"EOF context":"context";throw new SyntaxError(`line ${t.currentIndex+1} unmatched ${d} at source line ${c+1} "${a.contextLines[0]??""}"`)}t.fuzzScore+=h.fuzzScore;for(const d of a.diffChunks)i.push({...d,sourceIndex:d.sourceIndex+h.matchedIndex});c=h.matchedIndex+a.contextLines.length,t.currentIndex=a.endIndex}return{diffChunks:i,fuzzScore:t.fuzzScore}}static consumePrefix(e,n){return e.lines[e.currentIndex]?.startsWith(n)?(e.currentIndex+=1,e.lines[e.currentIndex-1].slice(n.length)):""}static createState(e){return{lines:[...e,this.terminators[0]],currentIndex:0,fuzzScore:0}}static isDone(e,n){return e.currentIndex>=e.lines.length||this.isTerminator(e.lines[e.currentIndex],n)}static isSectionBoundary(e){return e.startsWith("@@")||this.isTerminator(e,!0)}static isTerminator(e,n){return this.terminators.some(t=>e.startsWith(t))?!0:n&&e.startsWith(this.endFile)}static readSection(e,n){const t=[];let r=[],i=[];const c=[];let l="keep",o=n;for(;o<e.length;){const a=e[o];if(this.isSectionBoundary(a)||a==="***")break;if(a.startsWith("***"))throw new SyntaxError(`line ${o+1} unexpected marker "${a}"`);o+=1;const h=l,d=a||" ",m=this.prefixToMode[d[0]];if(!m)throw new SyntaxError(`line ${o} unexpected line prefix "${d}"`);l=m;const f=d.slice(1);l==="keep"&&h!==l&&(r.length||i.length)&&(c.push({sourceIndex:t.length-r.length,deletedLines:r,insertedLines:i}),r=[],i=[]),l==="delete"?(r.push(f),t.push(f)):l==="add"?i.push(f):t.push(f)}if((r.length||i.length)&&c.push({sourceIndex:t.length-r.length,deletedLines:r,insertedLines:i}),o<e.length&&e[o]===this.endFile)return{contextLines:t,diffChunks:c,endIndex:o+1,isEndOfFile:!0};if(o===n)throw new SyntaxError(`line ${o+1} empty section, expected content but got "${e[o]}"`);return{contextLines:t,diffChunks:c,endIndex:o,isEndOfFile:!1}}}class u{static envelopeExact=new Set(["*** Begin Patch","*** End Patch","\"]);static envelopePrefixes=["*** Add File:","*** Delete File:","*** Move to:","*** Update File:","--- a/","--- a\\","+++ b/","+++ b\\"];static apply(e,n,t="update"){if(t==="delete")return this.buildResult(e,"",e.split(`
|
|
4
|
+
`),"delete");const r=this.stripLeadingEmpty(this.stripEnvelope(p.normalizeDiffLines(n)));if(t==="create"){const i=p.parseCreateDiff(r);return this.buildResult(e,i,i.split(`
|
|
5
|
+
`),"add")}return this.applyChunks(e,p.parseUpdateDiff(r,e).diffChunks)}static applyChunks(e,n){const t=e.split(`
|
|
6
|
+
`),r=[],i=[];let c=0,l=1;for(const o of n){if(o.sourceIndex>t.length)throw new RangeError(`chunk targets source line ${o.sourceIndex+1} but file only has ${t.length} lines`);if(c>o.sourceIndex)throw new RangeError(`overlapping chunk at source line ${o.sourceIndex+1} but cursor already at line ${c+1}`);for(let a=c;a<o.sourceIndex;a+=1)r.push(t[a]),i.push({type:"equal",value:t[a],oldLine:a+1,newLine:l}),l+=1;c=o.sourceIndex;for(const a of o.deletedLines)i.push({type:"delete",value:a,oldLine:c+1,newLine:null}),c+=1;for(const a of o.insertedLines)r.push(a),i.push({type:"add",value:a,oldLine:null,newLine:l}),l+=1}for(let o=c;o<t.length;o+=1)r.push(t[o]),i.push({type:"equal",value:t[o],oldLine:o+1,newLine:l}),l+=1;return{text:r.join(`
|
|
7
|
+
`),diff:i,source:e}}static buildResult(e,n,t,r){return{text:n,source:e,diff:t.map((i,c)=>({type:r,value:i,oldLine:r==="delete"?c+1:null,newLine:r==="add"?c+1:null}))}}static stripEnvelope(e){return e.filter(n=>{const t=n.trim();return!this.envelopeExact.has(t)&&!this.envelopePrefixes.some(r=>t.startsWith(r))})}static stripLeadingEmpty(e){let n=0;for(;n<e.length&&e[n]==="";)n+=1;return n>0?e.slice(n):e}}module.exports=u;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Diff application mode selector */
|
|
2
|
-
type ApplyDiffMode = '
|
|
2
|
+
type ApplyDiffMode = 'create' | 'delete' | 'move' | 'update';
|
|
3
3
|
/**
|
|
4
4
|
* Result from applying a diff.
|
|
5
5
|
* @description Contains patched text, structured diff, and source.
|
|
@@ -40,7 +40,7 @@ type DiffChunk = {
|
|
|
40
40
|
*/
|
|
41
41
|
type DiffLine = {
|
|
42
42
|
/** Operation type for this line */
|
|
43
|
-
type:
|
|
43
|
+
type: DiffLineType;
|
|
44
44
|
/** Line content without prefix */
|
|
45
45
|
value: string;
|
|
46
46
|
/** Source line number or null */
|
|
@@ -48,18 +48,24 @@ type DiffLine = {
|
|
|
48
48
|
/** Result line number or null */
|
|
49
49
|
newLine: number | null;
|
|
50
50
|
};
|
|
51
|
+
/** All diff line operation types */
|
|
52
|
+
type DiffLineType = DiffMutationType | 'equal';
|
|
53
|
+
/** Diff line type for mutation operations */
|
|
54
|
+
type DiffMutationType = 'add' | 'delete';
|
|
51
55
|
/**
|
|
52
56
|
* Fuzzy matching strategy with penalty.
|
|
53
57
|
* @description Pairs a line transform with fuzz score.
|
|
54
58
|
*/
|
|
55
59
|
type FuzzStrategy = {
|
|
56
60
|
/** Line transformation function */
|
|
57
|
-
mapFn:
|
|
61
|
+
mapFn: LineTransformFn;
|
|
58
62
|
/** Penalty score for this strategy */
|
|
59
63
|
fuzzScore: number;
|
|
60
64
|
};
|
|
61
65
|
/** Hunk line operation mode */
|
|
62
|
-
type HunkLineMode =
|
|
66
|
+
type HunkLineMode = DiffMutationType | 'keep';
|
|
67
|
+
/** Line transformation function signature */
|
|
68
|
+
type LineTransformFn = (line: string) => string;
|
|
63
69
|
/**
|
|
64
70
|
* Parsed update diff result.
|
|
65
71
|
* @description Contains diff chunks and total fuzz score.
|
|
@@ -102,12 +108,16 @@ type SectionResult = {
|
|
|
102
108
|
* @description Applies V4A patches and produces structured diff output.
|
|
103
109
|
*/
|
|
104
110
|
declare class V4A {
|
|
111
|
+
/** Exact envelope lines to strip */
|
|
112
|
+
private static readonly envelopeExact;
|
|
113
|
+
/** Prefix-based envelope markers to strip */
|
|
114
|
+
private static readonly envelopePrefixes;
|
|
105
115
|
/**
|
|
106
|
-
* Apply
|
|
107
|
-
* @description Parses and applies diff to
|
|
116
|
+
* Apply V4A diff to source.
|
|
117
|
+
* @description Parses and applies diff to produce patched output.
|
|
108
118
|
* @param sourceText - Original source file text
|
|
109
119
|
* @param diffText - V4A format diff string
|
|
110
|
-
* @param mode -
|
|
120
|
+
* @param mode - Operation mode: update, create, move, or delete
|
|
111
121
|
* @returns Result with patched text and diff lines
|
|
112
122
|
*/
|
|
113
123
|
static apply(sourceText: string, diffText: string, mode?: ApplyDiffMode): ApplyDiffResult;
|
|
@@ -120,6 +130,16 @@ declare class V4A {
|
|
|
120
130
|
* @throws RangeError on out-of-bounds or overlapping chunks
|
|
121
131
|
*/
|
|
122
132
|
private static applyChunks;
|
|
133
|
+
/**
|
|
134
|
+
* Build result for uniform operations.
|
|
135
|
+
* @description Maps lines to DiffLine entries for single-type operations.
|
|
136
|
+
* @param sourceText - Original source file text
|
|
137
|
+
* @param resultText - Patched output text
|
|
138
|
+
* @param lines - Split result lines array
|
|
139
|
+
* @param type - Diff line mutation type
|
|
140
|
+
* @returns Structured diff result with text and source
|
|
141
|
+
*/
|
|
142
|
+
private static buildResult;
|
|
123
143
|
/**
|
|
124
144
|
* Strip patch envelope markers from lines.
|
|
125
145
|
* @description Removes Begin/End Patch, file headers, and git markers.
|
|
@@ -137,4 +157,4 @@ declare class V4A {
|
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
export = V4A;
|
|
140
|
-
export type { ApplyDiffMode, ApplyDiffResult, ContextMatch, DiffChunk, DiffLine, FuzzStrategy, HunkLineMode, ParsedUpdate, ParserState, SectionResult };
|
|
160
|
+
export type { ApplyDiffMode, ApplyDiffResult, ContextMatch, DiffChunk, DiffLine, DiffLineType, DiffMutationType, FuzzStrategy, HunkLineMode, LineTransformFn, ParsedUpdate, ParserState, SectionResult };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Diff application mode selector */
|
|
2
|
-
type ApplyDiffMode = '
|
|
2
|
+
type ApplyDiffMode = 'create' | 'delete' | 'move' | 'update';
|
|
3
3
|
/**
|
|
4
4
|
* Result from applying a diff.
|
|
5
5
|
* @description Contains patched text, structured diff, and source.
|
|
@@ -40,7 +40,7 @@ type DiffChunk = {
|
|
|
40
40
|
*/
|
|
41
41
|
type DiffLine = {
|
|
42
42
|
/** Operation type for this line */
|
|
43
|
-
type:
|
|
43
|
+
type: DiffLineType;
|
|
44
44
|
/** Line content without prefix */
|
|
45
45
|
value: string;
|
|
46
46
|
/** Source line number or null */
|
|
@@ -48,18 +48,24 @@ type DiffLine = {
|
|
|
48
48
|
/** Result line number or null */
|
|
49
49
|
newLine: number | null;
|
|
50
50
|
};
|
|
51
|
+
/** All diff line operation types */
|
|
52
|
+
type DiffLineType = DiffMutationType | 'equal';
|
|
53
|
+
/** Diff line type for mutation operations */
|
|
54
|
+
type DiffMutationType = 'add' | 'delete';
|
|
51
55
|
/**
|
|
52
56
|
* Fuzzy matching strategy with penalty.
|
|
53
57
|
* @description Pairs a line transform with fuzz score.
|
|
54
58
|
*/
|
|
55
59
|
type FuzzStrategy = {
|
|
56
60
|
/** Line transformation function */
|
|
57
|
-
mapFn:
|
|
61
|
+
mapFn: LineTransformFn;
|
|
58
62
|
/** Penalty score for this strategy */
|
|
59
63
|
fuzzScore: number;
|
|
60
64
|
};
|
|
61
65
|
/** Hunk line operation mode */
|
|
62
|
-
type HunkLineMode =
|
|
66
|
+
type HunkLineMode = DiffMutationType | 'keep';
|
|
67
|
+
/** Line transformation function signature */
|
|
68
|
+
type LineTransformFn = (line: string) => string;
|
|
63
69
|
/**
|
|
64
70
|
* Parsed update diff result.
|
|
65
71
|
* @description Contains diff chunks and total fuzz score.
|
|
@@ -102,12 +108,16 @@ type SectionResult = {
|
|
|
102
108
|
* @description Applies V4A patches and produces structured diff output.
|
|
103
109
|
*/
|
|
104
110
|
declare class V4A {
|
|
111
|
+
/** Exact envelope lines to strip */
|
|
112
|
+
private static readonly envelopeExact;
|
|
113
|
+
/** Prefix-based envelope markers to strip */
|
|
114
|
+
private static readonly envelopePrefixes;
|
|
105
115
|
/**
|
|
106
|
-
* Apply
|
|
107
|
-
* @description Parses and applies diff to
|
|
116
|
+
* Apply V4A diff to source.
|
|
117
|
+
* @description Parses and applies diff to produce patched output.
|
|
108
118
|
* @param sourceText - Original source file text
|
|
109
119
|
* @param diffText - V4A format diff string
|
|
110
|
-
* @param mode -
|
|
120
|
+
* @param mode - Operation mode: update, create, move, or delete
|
|
111
121
|
* @returns Result with patched text and diff lines
|
|
112
122
|
*/
|
|
113
123
|
static apply(sourceText: string, diffText: string, mode?: ApplyDiffMode): ApplyDiffResult;
|
|
@@ -120,6 +130,16 @@ declare class V4A {
|
|
|
120
130
|
* @throws RangeError on out-of-bounds or overlapping chunks
|
|
121
131
|
*/
|
|
122
132
|
private static applyChunks;
|
|
133
|
+
/**
|
|
134
|
+
* Build result for uniform operations.
|
|
135
|
+
* @description Maps lines to DiffLine entries for single-type operations.
|
|
136
|
+
* @param sourceText - Original source file text
|
|
137
|
+
* @param resultText - Patched output text
|
|
138
|
+
* @param lines - Split result lines array
|
|
139
|
+
* @param type - Diff line mutation type
|
|
140
|
+
* @returns Structured diff result with text and source
|
|
141
|
+
*/
|
|
142
|
+
private static buildResult;
|
|
123
143
|
/**
|
|
124
144
|
* Strip patch envelope markers from lines.
|
|
125
145
|
* @description Removes Begin/End Patch, file headers, and git markers.
|
|
@@ -137,4 +157,4 @@ declare class V4A {
|
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
export { V4A as default };
|
|
140
|
-
export type { ApplyDiffMode, ApplyDiffResult, ContextMatch, DiffChunk, DiffLine, FuzzStrategy, HunkLineMode, ParsedUpdate, ParserState, SectionResult };
|
|
160
|
+
export type { ApplyDiffMode, ApplyDiffResult, ContextMatch, DiffChunk, DiffLine, DiffLineType, DiffMutationType, FuzzStrategy, HunkLineMode, LineTransformFn, ParsedUpdate, ParserState, SectionResult };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Diff application mode selector */
|
|
2
|
-
type ApplyDiffMode = '
|
|
2
|
+
type ApplyDiffMode = 'create' | 'delete' | 'move' | 'update';
|
|
3
3
|
/**
|
|
4
4
|
* Result from applying a diff.
|
|
5
5
|
* @description Contains patched text, structured diff, and source.
|
|
@@ -40,7 +40,7 @@ type DiffChunk = {
|
|
|
40
40
|
*/
|
|
41
41
|
type DiffLine = {
|
|
42
42
|
/** Operation type for this line */
|
|
43
|
-
type:
|
|
43
|
+
type: DiffLineType;
|
|
44
44
|
/** Line content without prefix */
|
|
45
45
|
value: string;
|
|
46
46
|
/** Source line number or null */
|
|
@@ -48,18 +48,24 @@ type DiffLine = {
|
|
|
48
48
|
/** Result line number or null */
|
|
49
49
|
newLine: number | null;
|
|
50
50
|
};
|
|
51
|
+
/** All diff line operation types */
|
|
52
|
+
type DiffLineType = DiffMutationType | 'equal';
|
|
53
|
+
/** Diff line type for mutation operations */
|
|
54
|
+
type DiffMutationType = 'add' | 'delete';
|
|
51
55
|
/**
|
|
52
56
|
* Fuzzy matching strategy with penalty.
|
|
53
57
|
* @description Pairs a line transform with fuzz score.
|
|
54
58
|
*/
|
|
55
59
|
type FuzzStrategy = {
|
|
56
60
|
/** Line transformation function */
|
|
57
|
-
mapFn:
|
|
61
|
+
mapFn: LineTransformFn;
|
|
58
62
|
/** Penalty score for this strategy */
|
|
59
63
|
fuzzScore: number;
|
|
60
64
|
};
|
|
61
65
|
/** Hunk line operation mode */
|
|
62
|
-
type HunkLineMode =
|
|
66
|
+
type HunkLineMode = DiffMutationType | 'keep';
|
|
67
|
+
/** Line transformation function signature */
|
|
68
|
+
type LineTransformFn = (line: string) => string;
|
|
63
69
|
/**
|
|
64
70
|
* Parsed update diff result.
|
|
65
71
|
* @description Contains diff chunks and total fuzz score.
|
|
@@ -102,12 +108,16 @@ type SectionResult = {
|
|
|
102
108
|
* @description Applies V4A patches and produces structured diff output.
|
|
103
109
|
*/
|
|
104
110
|
declare class V4A {
|
|
111
|
+
/** Exact envelope lines to strip */
|
|
112
|
+
private static readonly envelopeExact;
|
|
113
|
+
/** Prefix-based envelope markers to strip */
|
|
114
|
+
private static readonly envelopePrefixes;
|
|
105
115
|
/**
|
|
106
|
-
* Apply
|
|
107
|
-
* @description Parses and applies diff to
|
|
116
|
+
* Apply V4A diff to source.
|
|
117
|
+
* @description Parses and applies diff to produce patched output.
|
|
108
118
|
* @param sourceText - Original source file text
|
|
109
119
|
* @param diffText - V4A format diff string
|
|
110
|
-
* @param mode -
|
|
120
|
+
* @param mode - Operation mode: update, create, move, or delete
|
|
111
121
|
* @returns Result with patched text and diff lines
|
|
112
122
|
*/
|
|
113
123
|
static apply(sourceText: string, diffText: string, mode?: ApplyDiffMode): ApplyDiffResult;
|
|
@@ -120,6 +130,16 @@ declare class V4A {
|
|
|
120
130
|
* @throws RangeError on out-of-bounds or overlapping chunks
|
|
121
131
|
*/
|
|
122
132
|
private static applyChunks;
|
|
133
|
+
/**
|
|
134
|
+
* Build result for uniform operations.
|
|
135
|
+
* @description Maps lines to DiffLine entries for single-type operations.
|
|
136
|
+
* @param sourceText - Original source file text
|
|
137
|
+
* @param resultText - Patched output text
|
|
138
|
+
* @param lines - Split result lines array
|
|
139
|
+
* @param type - Diff line mutation type
|
|
140
|
+
* @returns Structured diff result with text and source
|
|
141
|
+
*/
|
|
142
|
+
private static buildResult;
|
|
123
143
|
/**
|
|
124
144
|
* Strip patch envelope markers from lines.
|
|
125
145
|
* @description Removes Begin/End Patch, file headers, and git markers.
|
|
@@ -137,4 +157,4 @@ declare class V4A {
|
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
export = V4A;
|
|
140
|
-
export type { ApplyDiffMode, ApplyDiffResult, ContextMatch, DiffChunk, DiffLine, FuzzStrategy, HunkLineMode, ParsedUpdate, ParserState, SectionResult };
|
|
160
|
+
export type { ApplyDiffMode, ApplyDiffResult, ContextMatch, DiffChunk, DiffLine, DiffLineType, DiffMutationType, FuzzStrategy, HunkLineMode, LineTransformFn, ParsedUpdate, ParserState, SectionResult };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
class x{static anchorStrategies=[{mapFn:e=>e,fuzzScore:0},{mapFn:e=>e.trim(),fuzzScore:1},{mapFn:this.normalizeUnicode.bind(this),fuzzScore:10}];static contextStrategies=[{mapFn:e=>e,fuzzScore:0},{mapFn:e=>e.trimEnd(),fuzzScore:1},{mapFn:e=>e.trim(),fuzzScore:100},{mapFn:this.normalizeUnicode.bind(this),fuzzScore:1e3}];static
|
|
2
|
-
`)}static parseUpdateDiff(e,
|
|
3
|
-
`),i=[];let o=0;for(;!this.isDone(
|
|
4
|
-
`)
|
|
5
|
-
`),
|
|
6
|
-
`),r=[],i=[];let o=0,
|
|
7
|
-
`),diff:i,source:e}}static
|
|
1
|
+
class x{static anchorStrategies=[{mapFn:e=>e,fuzzScore:0},{mapFn:e=>e.trim(),fuzzScore:1},{mapFn:this.normalizeUnicode.bind(this),fuzzScore:10}];static contextStrategies=[{mapFn:e=>e,fuzzScore:0},{mapFn:e=>e.trimEnd(),fuzzScore:1},{mapFn:e=>e.trim(),fuzzScore:100},{mapFn:this.normalizeUnicode.bind(this),fuzzScore:1e3}];static unicodeReplacements={"\u2010":"-","\u2011":"-","\u2012":"-","\u2013":"-","\u2014":"-","\u2015":"-","\u2212":"-","\u2018":"'","\u2019":"'","\u201A":"'","\u201B":"'","\u201C":'"',"\u201D":'"',"\u201E":'"',"\u201F":'"',"\xA0":" ","\u2002":" ","\u2003":" ","\u2004":" ","\u2005":" ","\u2006":" ","\u2007":" ","\u2008":" ","\u2009":" ","\u200A":" ","\u202F":" ","\u205F":" ","\u3000":" "};static unicodePattern=new RegExp(`[${Object.keys(this.unicodeReplacements).join("")}]`,"g");static findContext(e,n,t,r){if(r){const i=this.findContextCore(e,n,Math.max(0,e.length-n.length));if(i.matchedIndex!==-1)return i;const o=this.findContextCore(e,n,t);return{matchedIndex:o.matchedIndex,fuzzScore:o.fuzzScore+1e4}}return this.findContextCore(e,n,t)}static resolveAnchor(e,n,t,r){for(const i of this.anchorStrategies){const o=i.mapFn(e);let c=!1;for(let u=0;u<t;u+=1)if(i.mapFn(n[u])===o){c=!0;break}if(c)return t;const s=this.searchLines(n,e,t,i.mapFn);if(s!==-1)return r.fuzzScore+=i.fuzzScore,s}return t}static findContextCore(e,n,t){if(!n.length)return{matchedIndex:t,fuzzScore:0};for(const r of this.contextStrategies)for(let i=t;i<e.length;i+=1)if(this.sliceEquals(e,n,i,r.mapFn))return{matchedIndex:i,fuzzScore:r.fuzzScore};return{matchedIndex:-1,fuzzScore:0}}static normalizeUnicode(e){return e.trim().replace(this.unicodePattern,n=>this.unicodeReplacements[n])}static searchLines(e,n,t,r){const i=r(n);for(let o=t;o<e.length;o+=1)if(r(e[o])===i)return o;return-1}static sliceEquals(e,n,t,r){if(t+n.length>e.length)return!1;for(let i=0;i<n.length;i+=1)if(r(e[t+i])!==r(n[i]))return!1;return!0}}class h{static endFile="*** End of File";static prefixToMode={"+":"add","-":"delete"," ":"keep"};static terminators=["*** End Patch","*** Update File:","*** Delete File:","*** Add File:","*** Move to:"];static normalizeDiffLines(e){const n=e.split(/\r?\n/);return n.at(-1)===""&&n.pop(),n}static parseCreateDiff(e){const n=this.createState(e),t=[];for(;!this.isDone(n,!1);){const r=n.lines[n.currentIndex];if(n.currentIndex+=1,!r.startsWith("+"))throw new SyntaxError(`line ${n.currentIndex} expected '+' prefix but got "${r}"`);t.push(r.slice(1))}return t.join(`
|
|
2
|
+
`)}static parseUpdateDiff(e,n){const t=this.createState(e),r=n.split(`
|
|
3
|
+
`),i=[];let o=0;for(;!this.isDone(t,!0);){const c=this.consumePrefix(t,"@@ "),s=!c&&t.lines[t.currentIndex]==="@@";if(s&&(t.currentIndex+=1),!c&&!s&&o!==0)throw new SyntaxError(`line ${t.currentIndex+1} unexpected line "${t.lines[t.currentIndex]}"`);c?.trim()&&(o=x.resolveAnchor(c,r,o,t));const u=this.readSection(t.lines,t.currentIndex),l=x.findContext(r,u.contextLines,o,u.isEndOfFile);if(l.matchedIndex===-1){const a=u.isEndOfFile?"EOF context":"context";throw new SyntaxError(`line ${t.currentIndex+1} unmatched ${a} at source line ${o+1} "${u.contextLines[0]??""}"`)}t.fuzzScore+=l.fuzzScore;for(const a of u.diffChunks)i.push({...a,sourceIndex:a.sourceIndex+l.matchedIndex});o=l.matchedIndex+u.contextLines.length,t.currentIndex=u.endIndex}return{diffChunks:i,fuzzScore:t.fuzzScore}}static consumePrefix(e,n){return e.lines[e.currentIndex]?.startsWith(n)?(e.currentIndex+=1,e.lines[e.currentIndex-1].slice(n.length)):""}static createState(e){return{lines:[...e,this.terminators[0]],currentIndex:0,fuzzScore:0}}static isDone(e,n){return e.currentIndex>=e.lines.length||this.isTerminator(e.lines[e.currentIndex],n)}static isSectionBoundary(e){return e.startsWith("@@")||this.isTerminator(e,!0)}static isTerminator(e,n){return this.terminators.some(t=>e.startsWith(t))?!0:n&&e.startsWith(this.endFile)}static readSection(e,n){const t=[];let r=[],i=[];const o=[];let c="keep",s=n;for(;s<e.length;){const u=e[s];if(this.isSectionBoundary(u)||u==="***")break;if(u.startsWith("***"))throw new SyntaxError(`line ${s+1} unexpected marker "${u}"`);s+=1;const l=c,a=u||" ",p=this.prefixToMode[a[0]];if(!p)throw new SyntaxError(`line ${s} unexpected line prefix "${a}"`);c=p;const d=a.slice(1);c==="keep"&&l!==c&&(r.length||i.length)&&(o.push({sourceIndex:t.length-r.length,deletedLines:r,insertedLines:i}),r=[],i=[]),c==="delete"?(r.push(d),t.push(d)):c==="add"?i.push(d):t.push(d)}if((r.length||i.length)&&o.push({sourceIndex:t.length-r.length,deletedLines:r,insertedLines:i}),s<e.length&&e[s]===this.endFile)return{contextLines:t,diffChunks:o,endIndex:s+1,isEndOfFile:!0};if(s===n)throw new SyntaxError(`line ${s+1} empty section, expected content but got "${e[s]}"`);return{contextLines:t,diffChunks:o,endIndex:s,isEndOfFile:!1}}}class m{static envelopeExact=new Set(["*** Begin Patch","*** End Patch","\"]);static envelopePrefixes=["*** Add File:","*** Delete File:","*** Move to:","*** Update File:","--- a/","--- a\\","+++ b/","+++ b\\"];static apply(e,n,t="update"){if(t==="delete")return this.buildResult(e,"",e.split(`
|
|
4
|
+
`),"delete");const r=this.stripLeadingEmpty(this.stripEnvelope(h.normalizeDiffLines(n)));if(t==="create"){const i=h.parseCreateDiff(r);return this.buildResult(e,i,i.split(`
|
|
5
|
+
`),"add")}return this.applyChunks(e,h.parseUpdateDiff(r,e).diffChunks)}static applyChunks(e,n){const t=e.split(`
|
|
6
|
+
`),r=[],i=[];let o=0,c=1;for(const s of n){if(s.sourceIndex>t.length)throw new RangeError(`chunk targets source line ${s.sourceIndex+1} but file only has ${t.length} lines`);if(o>s.sourceIndex)throw new RangeError(`overlapping chunk at source line ${s.sourceIndex+1} but cursor already at line ${o+1}`);for(let u=o;u<s.sourceIndex;u+=1)r.push(t[u]),i.push({type:"equal",value:t[u],oldLine:u+1,newLine:c}),c+=1;o=s.sourceIndex;for(const u of s.deletedLines)i.push({type:"delete",value:u,oldLine:o+1,newLine:null}),o+=1;for(const u of s.insertedLines)r.push(u),i.push({type:"add",value:u,oldLine:null,newLine:c}),c+=1}for(let s=o;s<t.length;s+=1)r.push(t[s]),i.push({type:"equal",value:t[s],oldLine:s+1,newLine:c}),c+=1;return{text:r.join(`
|
|
7
|
+
`),diff:i,source:e}}static buildResult(e,n,t,r){return{text:n,source:e,diff:t.map((i,o)=>({type:r,value:i,oldLine:r==="delete"?o+1:null,newLine:r==="add"?o+1:null}))}}static stripEnvelope(e){return e.filter(n=>{const t=n.trim();return!this.envelopeExact.has(t)&&!this.envelopePrefixes.some(r=>t.startsWith(r))})}static stripLeadingEmpty(e){let n=0;for(;n<e.length&&e[n]==="";)n+=1;return n>0?e.slice(n):e}}export{m as default};
|