@neabyte/v4a-diff 0.1.0 → 0.2.1

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 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 | Description |
121
- | ------------ | ----------------------- | -------------------------------------------------------------------- |
122
- | `sourceText` | `string` | Original file content |
123
- | `diffText` | `string` | V4A format diff string |
124
- | `mode` | `'default' \| 'create'` | `default` updates existing text, `create` builds from `+` lines only |
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,9 @@
1
- "use strict";class f{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 unicodeMap=new Map([[8208,"-"],[8209,"-"],[8210,"-"],[8211,"-"],[8212,"-"],[8213,"-"],[8722,"-"],[8216,"'"],[8217,"'"],[8218,"'"],[8219,"'"],[8220,'"'],[8221,'"'],[8222,'"'],[8223,'"'],[160," "],[8194," "],[8195," "],[8196," "],[8197," "],[8198," "],[8199," "],[8200," "],[8201," "],[8202," "],[8239," "],[8287," "],[12288," "]]);static findContext(e,t,n,r){if(r){const i=this.findContextCore(e,t,Math.max(0,e.length-t.length));if(i.matchedIndex!==-1)return i;const c=this.findContextCore(e,t,n);return{matchedIndex:c.matchedIndex,fuzzScore:c.fuzzScore+1e4}}return this.findContextCore(e,t,n)}static resolveAnchor(e,t,n,r){for(const i of this.anchorStrategies){const c=i.mapFn(e);let u=!1;for(let a=0;a<n;a+=1)if(i.mapFn(t[a])===c){u=!0;break}if(u)return n;const s=this.searchLines(t,e,n,i.mapFn);if(s!==-1)return r.fuzzScore+=i.fuzzScore,s}return n}static findContextCore(e,t,n){if(!t.length)return{matchedIndex:n,fuzzScore:0};for(const r of this.contextStrategies)for(let i=n;i<e.length;i+=1)if(this.sliceEquals(e,t,i,r.mapFn))return{matchedIndex:i,fuzzScore:r.fuzzScore};return{matchedIndex:-1,fuzzScore:0}}static normalizeUnicode(e){const t=e.trim(),n=[];for(let r=0;r<t.length;r+=1)n.push(this.unicodeMap.get(t.charCodeAt(r))??t[r]);return n.join("")}static searchLines(e,t,n,r){const i=r(t);for(let c=n;c<e.length;c+=1)if(r(e[c])===i)return c;return-1}static sliceEquals(e,t,n,r){if(n+t.length>e.length)return!1;for(let i=0;i<t.length;i+=1)if(r(e[n+i])!==r(t[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:"];static normalizeDiffLines(e){const t=e.split(/\r?\n/);return t.at(-1)===""&&t.pop(),t}static parseCreateDiff(e){const t=this.createState(e),n=[];for(;!this.isDone(t,!1);){const r=t.lines[t.currentIndex];if(t.currentIndex+=1,!r.startsWith("+"))throw new SyntaxError(`expected '+' prefix but got "${r}"`);n.push(r.slice(1))}return n.join(`
2
- `)}static parseUpdateDiff(e,t){const n=this.createState(e),r=t.split(`
3
- `),i=[];let c=0;for(;!this.isDone(n,!0);){const u=this.consumePrefix(n,"@@ "),s=!u&&n.lines[n.currentIndex]==="@@";if(s&&(n.currentIndex+=1),!u&&!s&&c!==0)throw new SyntaxError(`unexpected line "${n.lines[n.currentIndex]}"`);u?.trim()&&(c=f.resolveAnchor(u,r,c,n));const a=this.readSection(n.lines,n.currentIndex),l=f.findContext(r,a.contextLines,c,a.isEndOfFile);if(l.matchedIndex===-1){const d=a.isEndOfFile?"EOF context":"context";throw new SyntaxError(`unmatched ${d} at cursor ${c} "${a.contextLines.join(`
4
- `)}"`)}n.fuzzScore+=l.fuzzScore;for(const d of a.diffChunks)i.push({...d,sourceIndex:d.sourceIndex+l.matchedIndex});c=l.matchedIndex+a.contextLines.length,n.currentIndex=a.endIndex}return{diffChunks:i,fuzzScore:n.fuzzScore}}static consumePrefix(e,t){return e.lines[e.currentIndex]?.startsWith(t)?(e.currentIndex+=1,e.lines[e.currentIndex-1].slice(t.length)):""}static createState(e){return{lines:[...e,this.terminators[0]],currentIndex:0,fuzzScore:0}}static isDone(e,t){return e.currentIndex>=e.lines.length||this.isTerminator(e.lines[e.currentIndex],t)}static isSectionBoundary(e){return e.startsWith("@@")||this.isTerminator(e,!0)}static isTerminator(e,t){return this.terminators.some(n=>e.startsWith(n))?!0:t&&e.startsWith(this.endFile)}static readSection(e,t){const n=[];let r=[],i=[];const c=[];let u="keep",s=t;for(;s<e.length;){const a=e[s];if(this.isSectionBoundary(a)||a==="***")break;if(a.startsWith("***"))throw new SyntaxError(`unexpected marker "${a}"`);s+=1;const l=u,d=a||" ",m=this.prefixToMode[d[0]];if(!m)throw new SyntaxError(`unexpected line prefix "${d}"`);u=m;const h=d.slice(1);u==="keep"&&l!==u&&(r.length||i.length)&&(c.push({sourceIndex:n.length-r.length,deletedLines:r,insertedLines:i}),r=[],i=[]),u==="delete"?(r.push(h),n.push(h)):u==="add"?i.push(h):n.push(h)}if((r.length||i.length)&&c.push({sourceIndex:n.length-r.length,deletedLines:r,insertedLines:i}),s<e.length&&e[s]===this.endFile)return{contextLines:n,diffChunks:c,endIndex:s+1,isEndOfFile:!0};if(s===t)throw new SyntaxError(`empty section at index ${s} "${e[s]}"`);return{contextLines:n,diffChunks:c,endIndex:s,isEndOfFile:!1}}}class o{static apply(e,t,n="default"){const r=this.stripLeadingEmpty(this.stripEnvelope(p.normalizeDiffLines(t)));if(n==="create"){const i=p.parseCreateDiff(r),c=i.split(`
5
- `),u=[];for(let s=0;s<c.length;s+=1)u.push({type:"add",value:c[s],oldLine:null,newLine:s+1});return{text:i,diff:u,source:e}}return this.applyChunks(e,p.parseUpdateDiff(r,e).diffChunks)}static applyChunks(e,t){const n=e.split(`
6
- `),r=[],i=[];let c=0,u=1;for(const s of t){if(s.sourceIndex>n.length)throw new RangeError(`chunk sourceIndex ${s.sourceIndex} exceeds input length ${n.length}`);if(c>s.sourceIndex)throw new RangeError(`overlapping chunk at ${s.sourceIndex} cursor already at ${c}`);for(let a=c;a<s.sourceIndex;a+=1)r.push(n[a]),i.push({type:"equal",value:n[a],oldLine:a+1,newLine:u}),u+=1;c=s.sourceIndex;for(const a of s.deletedLines)i.push({type:"delete",value:a,oldLine:c+1,newLine:null}),c+=1;for(const a of s.insertedLines)r.push(a),i.push({type:"add",value:a,oldLine:null,newLine:u}),u+=1}for(let s=c;s<n.length;s+=1)r.push(n[s]),i.push({type:"equal",value:n[s],oldLine:s+1,newLine:u}),u+=1;return{text:r.join(`
7
- `),diff:i,source:e}}static stripEnvelope(e){return e.filter(t=>!(t==="*** Begin Patch"||t==="*** End Patch"||t.startsWith("*** Update File:")||t.startsWith("*** Add File:")||t.startsWith("--- a/")||t.startsWith("+++ b/")||t.startsWith("--- a\\")||t.startsWith("+++ b\\")||t==="\"))}static stripLeadingEmpty(e){let t=0;for(;t<e.length&&e[t]==="";)t+=1;return t>0?e.slice(t):e}}module.exports=o;
1
+ "use strict";class c{static unicodeReplacements={"\u2010":"-","\u2011":"-","\u2012":"-","\u2013":"-","\u2014":"-","\u2015":"-","\u2212":"-","\uFF0D":"-","\u2018":"'","\u2019":"'","\u201A":"'","\u201B":"'",\u02BC:"'","\uFF07":"'","\u201C":'"',"\u201D":'"',"\u201E":'"',"\u201F":'"',"\uFF02":'"',"\u2026":"...","\xA0":" ","\u2002":" ","\u2003":" ","\u2004":" ","\u2005":" ","\u2006":" ","\u2007":" ","\u2008":" ","\u2009":" ","\u200A":" ","\u202F":" ","\u205F":" ","\u3000":" "};static unicodePattern=new RegExp(`[${Object.keys(this.unicodeReplacements).join("")}]`,"g");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:this.collapseSpace.bind(this),fuzzScore:50},{mapFn:e=>e.trim(),fuzzScore:100},{mapFn:this.normalizeUnicode.bind(this),fuzzScore:1e3}];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 l=!1;for(let a=0;a<t;a+=1)if(i.mapFn(n[a])===o){l=!0;break}if(l)return t;const s=this.searchLines(n,e,t,i.mapFn);if(s!==-1)return r.fuzzScore+=i.fuzzScore,s}for(const i of this.anchorStrategies){const o=this.searchLines(n,e,t,i.mapFn,!0);if(o!==-1)return r.fuzzScore+=i.fuzzScore+5,o}return t}static collapseSpace(e){return e.replace(/\s+/g," ").trim()}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,i=!1){const o=r(n);if(i&&o.length===0)return-1;for(let l=t;l<e.length;l+=1){const s=r(e[l]);if(i?s.startsWith(o):s===o)return l}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 o=0;for(;!this.isDone(t,!0);){const l=this.consumePrefix(t,"@@ "),s=!l&&t.lines[t.currentIndex]==="@@";if(s&&(t.currentIndex+=1),!l&&!s&&o!==0)throw new SyntaxError(`line ${t.currentIndex+1} unexpected line "${t.lines[t.currentIndex]}"`);l?.trim()&&(o=c.resolveAnchor(l,r,o,t));const a=this.readSection(t.lines,t.currentIndex),h=c.findContext(r,a.contextLines,o,a.isEndOfFile);if(h.matchedIndex===-1){const d=a.isEndOfFile?"EOF context":"context",x=a.contextLines[0]??"",f=r[o]??"(end of file)";throw new SyntaxError(`line ${t.currentIndex+1} unmatched ${d} at source line ${o+1} expected "${x}" but found "${f}"`)}t.fuzzScore+=h.fuzzScore;for(const d of a.diffChunks)i.push({...d,sourceIndex:d.sourceIndex+h.matchedIndex});o=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 o=[];let l="keep",s=n;for(;s<e.length;){const a=e[s];if(this.isSectionBoundary(a)||a==="***")break;if(a.startsWith("***"))throw new SyntaxError(`line ${s+1} unexpected marker "${a}"`);s+=1;const h=l,d=a||" ",x=this.prefixToMode[d[0]];if(!x)throw new SyntaxError(`line ${s} unexpected line prefix "${d}"`);l=x;const f=d.slice(1);l==="keep"&&h!==l&&(r.length||i.length)&&(o.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)&&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 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(e=(e??"").replaceAll(`\r
4
+ `,`
5
+ `),n=n??"",t==="delete")return this.buildResult(e,"",e.split(`
6
+ `),"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(`
7
+ `),"add")}return this.applyChunks(e,p.parseUpdateDiff(r,e).diffChunks)}static applyChunks(e,n){const t=e.split(`
8
+ `),r=[],i=[];let o=0,l=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 a=o;a<s.sourceIndex;a+=1)r.push(t[a]),i.push({type:"equal",value:t[a],oldLine:a+1,newLine:l}),l+=1;o=s.sourceIndex;for(const a of s.deletedLines)i.push({type:"delete",value:a,oldLine:o+1,newLine:null}),o+=1;for(const a of s.insertedLines)r.push(a),i.push({type:"add",value:a,oldLine:null,newLine:l}),l+=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:l}),l+=1;return{text:r.join(`
9
+ `),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}}module.exports=u;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Diff application mode selector */
2
- type ApplyDiffMode = 'default' | 'create';
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: 'add' | 'delete' | 'equal';
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: (line: string) => string;
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 = 'keep' | 'add' | 'delete';
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 a V4A diff patch.
107
- * @description Parses and applies diff to source text.
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 - Apply mode: default or create
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 = 'default' | 'create';
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: 'add' | 'delete' | 'equal';
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: (line: string) => string;
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 = 'keep' | 'add' | 'delete';
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 a V4A diff patch.
107
- * @description Parses and applies diff to source text.
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 - Apply mode: default or create
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 = 'default' | 'create';
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: 'add' | 'delete' | 'equal';
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: (line: string) => string;
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 = 'keep' | 'add' | 'delete';
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 a V4A diff patch.
107
- * @description Parses and applies diff to source text.
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 - Apply mode: default or create
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,9 @@
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 unicodeMap=new Map([[8208,"-"],[8209,"-"],[8210,"-"],[8211,"-"],[8212,"-"],[8213,"-"],[8722,"-"],[8216,"'"],[8217,"'"],[8218,"'"],[8219,"'"],[8220,'"'],[8221,'"'],[8222,'"'],[8223,'"'],[160," "],[8194," "],[8195," "],[8196," "],[8197," "],[8198," "],[8199," "],[8200," "],[8201," "],[8202," "],[8239," "],[8287," "],[12288," "]]);static findContext(e,t,n,r){if(r){const i=this.findContextCore(e,t,Math.max(0,e.length-t.length));if(i.matchedIndex!==-1)return i;const o=this.findContextCore(e,t,n);return{matchedIndex:o.matchedIndex,fuzzScore:o.fuzzScore+1e4}}return this.findContextCore(e,t,n)}static resolveAnchor(e,t,n,r){for(const i of this.anchorStrategies){const o=i.mapFn(e);let a=!1;for(let c=0;c<n;c+=1)if(i.mapFn(t[c])===o){a=!0;break}if(a)return n;const s=this.searchLines(t,e,n,i.mapFn);if(s!==-1)return r.fuzzScore+=i.fuzzScore,s}return n}static findContextCore(e,t,n){if(!t.length)return{matchedIndex:n,fuzzScore:0};for(const r of this.contextStrategies)for(let i=n;i<e.length;i+=1)if(this.sliceEquals(e,t,i,r.mapFn))return{matchedIndex:i,fuzzScore:r.fuzzScore};return{matchedIndex:-1,fuzzScore:0}}static normalizeUnicode(e){const t=e.trim(),n=[];for(let r=0;r<t.length;r+=1)n.push(this.unicodeMap.get(t.charCodeAt(r))??t[r]);return n.join("")}static searchLines(e,t,n,r){const i=r(t);for(let o=n;o<e.length;o+=1)if(r(e[o])===i)return o;return-1}static sliceEquals(e,t,n,r){if(n+t.length>e.length)return!1;for(let i=0;i<t.length;i+=1)if(r(e[n+i])!==r(t[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:"];static normalizeDiffLines(e){const t=e.split(/\r?\n/);return t.at(-1)===""&&t.pop(),t}static parseCreateDiff(e){const t=this.createState(e),n=[];for(;!this.isDone(t,!1);){const r=t.lines[t.currentIndex];if(t.currentIndex+=1,!r.startsWith("+"))throw new SyntaxError(`expected '+' prefix but got "${r}"`);n.push(r.slice(1))}return n.join(`
2
- `)}static parseUpdateDiff(e,t){const n=this.createState(e),r=t.split(`
3
- `),i=[];let o=0;for(;!this.isDone(n,!0);){const a=this.consumePrefix(n,"@@ "),s=!a&&n.lines[n.currentIndex]==="@@";if(s&&(n.currentIndex+=1),!a&&!s&&o!==0)throw new SyntaxError(`unexpected line "${n.lines[n.currentIndex]}"`);a?.trim()&&(o=x.resolveAnchor(a,r,o,n));const c=this.readSection(n.lines,n.currentIndex),d=x.findContext(r,c.contextLines,o,c.isEndOfFile);if(d.matchedIndex===-1){const u=c.isEndOfFile?"EOF context":"context";throw new SyntaxError(`unmatched ${u} at cursor ${o} "${c.contextLines.join(`
4
- `)}"`)}n.fuzzScore+=d.fuzzScore;for(const u of c.diffChunks)i.push({...u,sourceIndex:u.sourceIndex+d.matchedIndex});o=d.matchedIndex+c.contextLines.length,n.currentIndex=c.endIndex}return{diffChunks:i,fuzzScore:n.fuzzScore}}static consumePrefix(e,t){return e.lines[e.currentIndex]?.startsWith(t)?(e.currentIndex+=1,e.lines[e.currentIndex-1].slice(t.length)):""}static createState(e){return{lines:[...e,this.terminators[0]],currentIndex:0,fuzzScore:0}}static isDone(e,t){return e.currentIndex>=e.lines.length||this.isTerminator(e.lines[e.currentIndex],t)}static isSectionBoundary(e){return e.startsWith("@@")||this.isTerminator(e,!0)}static isTerminator(e,t){return this.terminators.some(n=>e.startsWith(n))?!0:t&&e.startsWith(this.endFile)}static readSection(e,t){const n=[];let r=[],i=[];const o=[];let a="keep",s=t;for(;s<e.length;){const c=e[s];if(this.isSectionBoundary(c)||c==="***")break;if(c.startsWith("***"))throw new SyntaxError(`unexpected marker "${c}"`);s+=1;const d=a,u=c||" ",p=this.prefixToMode[u[0]];if(!p)throw new SyntaxError(`unexpected line prefix "${u}"`);a=p;const l=u.slice(1);a==="keep"&&d!==a&&(r.length||i.length)&&(o.push({sourceIndex:n.length-r.length,deletedLines:r,insertedLines:i}),r=[],i=[]),a==="delete"?(r.push(l),n.push(l)):a==="add"?i.push(l):n.push(l)}if((r.length||i.length)&&o.push({sourceIndex:n.length-r.length,deletedLines:r,insertedLines:i}),s<e.length&&e[s]===this.endFile)return{contextLines:n,diffChunks:o,endIndex:s+1,isEndOfFile:!0};if(s===t)throw new SyntaxError(`empty section at index ${s} "${e[s]}"`);return{contextLines:n,diffChunks:o,endIndex:s,isEndOfFile:!1}}}class m{static apply(e,t,n="default"){const r=this.stripLeadingEmpty(this.stripEnvelope(h.normalizeDiffLines(t)));if(n==="create"){const i=h.parseCreateDiff(r),o=i.split(`
5
- `),a=[];for(let s=0;s<o.length;s+=1)a.push({type:"add",value:o[s],oldLine:null,newLine:s+1});return{text:i,diff:a,source:e}}return this.applyChunks(e,h.parseUpdateDiff(r,e).diffChunks)}static applyChunks(e,t){const n=e.split(`
6
- `),r=[],i=[];let o=0,a=1;for(const s of t){if(s.sourceIndex>n.length)throw new RangeError(`chunk sourceIndex ${s.sourceIndex} exceeds input length ${n.length}`);if(o>s.sourceIndex)throw new RangeError(`overlapping chunk at ${s.sourceIndex} cursor already at ${o}`);for(let c=o;c<s.sourceIndex;c+=1)r.push(n[c]),i.push({type:"equal",value:n[c],oldLine:c+1,newLine:a}),a+=1;o=s.sourceIndex;for(const c of s.deletedLines)i.push({type:"delete",value:c,oldLine:o+1,newLine:null}),o+=1;for(const c of s.insertedLines)r.push(c),i.push({type:"add",value:c,oldLine:null,newLine:a}),a+=1}for(let s=o;s<n.length;s+=1)r.push(n[s]),i.push({type:"equal",value:n[s],oldLine:s+1,newLine:a}),a+=1;return{text:r.join(`
7
- `),diff:i,source:e}}static stripEnvelope(e){return e.filter(t=>!(t==="*** Begin Patch"||t==="*** End Patch"||t.startsWith("*** Update File:")||t.startsWith("*** Add File:")||t.startsWith("--- a/")||t.startsWith("+++ b/")||t.startsWith("--- a\\")||t.startsWith("+++ b\\")||t==="\"))}static stripLeadingEmpty(e){let t=0;for(;t<e.length&&e[t]==="";)t+=1;return t>0?e.slice(t):e}}export{m as default};
1
+ class x{static unicodeReplacements={"\u2010":"-","\u2011":"-","\u2012":"-","\u2013":"-","\u2014":"-","\u2015":"-","\u2212":"-","\uFF0D":"-","\u2018":"'","\u2019":"'","\u201A":"'","\u201B":"'",\u02BC:"'","\uFF07":"'","\u201C":'"',"\u201D":'"',"\u201E":'"',"\u201F":'"',"\uFF02":'"',"\u2026":"...","\xA0":" ","\u2002":" ","\u2003":" ","\u2004":" ","\u2005":" ","\u2006":" ","\u2007":" ","\u2008":" ","\u2009":" ","\u200A":" ","\u202F":" ","\u205F":" ","\u3000":" "};static unicodePattern=new RegExp(`[${Object.keys(this.unicodeReplacements).join("")}]`,"g");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:this.collapseSpace.bind(this),fuzzScore:50},{mapFn:e=>e.trim(),fuzzScore:100},{mapFn:this.normalizeUnicode.bind(this),fuzzScore:1e3}];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}for(const i of this.anchorStrategies){const o=this.searchLines(n,e,t,i.mapFn,!0);if(o!==-1)return r.fuzzScore+=i.fuzzScore+5,o}return t}static collapseSpace(e){return e.replace(/\s+/g," ").trim()}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,i=!1){const o=r(n);if(i&&o.length===0)return-1;for(let c=t;c<e.length;c+=1){const s=r(e[c]);if(i?s.startsWith(o):s===o)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 f{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",h=u.contextLines[0]??"",d=r[o]??"(end of file)";throw new SyntaxError(`line ${t.currentIndex+1} unmatched ${a} at source line ${o+1} expected "${h}" but found "${d}"`)}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||" ",h=this.prefixToMode[a[0]];if(!h)throw new SyntaxError(`line ${s} unexpected line prefix "${a}"`);c=h;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(e=(e??"").replaceAll(`\r
4
+ `,`
5
+ `),n=n??"",t==="delete")return this.buildResult(e,"",e.split(`
6
+ `),"delete");const r=this.stripLeadingEmpty(this.stripEnvelope(f.normalizeDiffLines(n)));if(t==="create"){const i=f.parseCreateDiff(r);return this.buildResult(e,i,i.split(`
7
+ `),"add")}return this.applyChunks(e,f.parseUpdateDiff(r,e).diffChunks)}static applyChunks(e,n){const t=e.split(`
8
+ `),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(`
9
+ `),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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neabyte/v4a-diff",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Apply context-anchored file patches from LLM tool calls",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",