@neabyte/v4a-diff 0.1.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/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +140 -0
- package/dist/index.d.mts +140 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.mjs +7 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NeaByteLab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<div align='center'>
|
|
2
|
+
|
|
3
|
+
# V4A Diff
|
|
4
|
+
|
|
5
|
+
Context-anchored diff engine for LLM-powered file editing
|
|
6
|
+
|
|
7
|
+
[](https://deno.com) [](https://nodejs.org) [](https://bun.sh) [](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
|
|
8
|
+
|
|
9
|
+
[](https://github.com/NeaByteLab/V4A-Diff) [](https://www.npmjs.org/package/@neabyte/v4a-diff) [](https://jsr.io/@neabyte/v4a-diff) [](LICENSE)
|
|
10
|
+
|
|
11
|
+
<img src="./assets/preview.webp" alt="V4A Diff Preview" width="100%">
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
## What is V4A?
|
|
16
|
+
|
|
17
|
+
V4A is a context-anchored diff format designed for LLM tool calling. Instead of line numbers, hunks use `@@` text anchors to locate edits - making diffs resilient to file changes between turns. This package parses V4A diffs, applies them to source text, and returns both the patched result and structured diff metadata with line numbers.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Token efficient** - Returns only changed lines, not the entire file.
|
|
22
|
+
- **Instant rollback** - Original source included in result, no manual tracking.
|
|
23
|
+
- **Structured diff** - Line-by-line add/delete/equal metadata with line numbers.
|
|
24
|
+
- **Smart matching** - Fuzzy context resolution with whitespace and Unicode tolerance.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
**Deno (JSR):**
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
deno add jsr:@neabyte/v4a-diff
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**npm:**
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @neabyte/v4a-diff
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**CDN (jsDelivr/esm.sh):**
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<script type="module">
|
|
44
|
+
import V4A from 'https://cdn.jsdelivr.net/npm/@neabyte/v4a-diff/dist/index.mjs'
|
|
45
|
+
</script>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or via [esm.sh](https://esm.sh):
|
|
49
|
+
|
|
50
|
+
```html
|
|
51
|
+
<script type="module">
|
|
52
|
+
import V4A from 'https://esm.sh/@neabyte/v4a-diff'
|
|
53
|
+
</script>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import V4A from '@neabyte/v4a-diff'
|
|
60
|
+
|
|
61
|
+
// Update an existing file
|
|
62
|
+
const result = V4A.apply(
|
|
63
|
+
'function add(a, b) {\n return a - b\n}',
|
|
64
|
+
'@@\n function add(a, b) {\n- return a - b\n+ return a + b\n }'
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
console.log(result.source)
|
|
68
|
+
// function add(a, b) {
|
|
69
|
+
// return a - b
|
|
70
|
+
// }
|
|
71
|
+
|
|
72
|
+
console.log(result.text)
|
|
73
|
+
// function add(a, b) {
|
|
74
|
+
// return a + b
|
|
75
|
+
// }
|
|
76
|
+
|
|
77
|
+
console.log(result.diff)
|
|
78
|
+
// [
|
|
79
|
+
// { type: 'equal', value: 'function add(a, b) {', oldLine: 1, newLine: 1 },
|
|
80
|
+
// { type: 'delete', value: ' return a - b', oldLine: 2, newLine: null },
|
|
81
|
+
// { type: 'add', value: ' return a + b', oldLine: null, newLine: 2 },
|
|
82
|
+
// { type: 'equal', value: '}', oldLine: 3, newLine: 3 }
|
|
83
|
+
// ]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### With `*** Begin Patch` envelope
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const result = V4A.apply(
|
|
90
|
+
'const x = 1',
|
|
91
|
+
'*** Begin Patch\n*** Update File: /src/config.ts\n@@\n-const x = 1\n+const x = 2\n*** End Patch'
|
|
92
|
+
)
|
|
93
|
+
// result.text === 'const x = 2'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Create a new file
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const result = V4A.apply(
|
|
100
|
+
'',
|
|
101
|
+
'+export const PORT = 8080\n+export const HOST = "localhost"',
|
|
102
|
+
'create'
|
|
103
|
+
)
|
|
104
|
+
// result.text === 'export const PORT = 8080\nexport const HOST = "localhost"'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Multi-hunk edits
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const result = V4A.apply(
|
|
111
|
+
'function add(a, b) {\n return a - b\n}\nfunction sub(a, b) {\n return a + b\n}',
|
|
112
|
+
'@@ function add(\n function add(a, b) {\n- return a - b\n+ return a + b\n }\n@@ function sub(\n function sub(a, b) {\n- return a + b\n+ return a - b\n }'
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API
|
|
117
|
+
|
|
118
|
+
### `V4A.apply(sourceText, diffText, mode?)`
|
|
119
|
+
|
|
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 |
|
|
125
|
+
|
|
126
|
+
**Returns:** `ApplyDiffResult`
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
type ApplyDiffResult = {
|
|
130
|
+
text: string // Patched output text
|
|
131
|
+
diff: DiffLine[] // Structured line-by-line diff
|
|
132
|
+
source: string // Original source text for rollback
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
type DiffLine = {
|
|
136
|
+
type: 'add' | 'delete' | 'equal'
|
|
137
|
+
value: string // Line content
|
|
138
|
+
oldLine: number | null // Source line number (null for adds)
|
|
139
|
+
newLine: number | null // Result line number (null for deletes)
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## V4A Diff Format
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
*** Begin Patch
|
|
147
|
+
*** Update File: {path}
|
|
148
|
+
@@ {anchor_text}
|
|
149
|
+
context line (space prefix, exact copy from file)
|
|
150
|
+
-removed line (must match file exactly)
|
|
151
|
+
+added line
|
|
152
|
+
*** End Patch
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
- `@@` followed by text anchors to a matching line in the source file.
|
|
156
|
+
- A bare `@@` alone means "start of file."
|
|
157
|
+
- Every line in a hunk must start with ``(space),`-`, or `+`.
|
|
158
|
+
- `*** Add File:` with `+` prefixed lines creates new files.
|
|
159
|
+
|
|
160
|
+
## LLM Tool Schemas
|
|
161
|
+
|
|
162
|
+
Pre-built schemas for tool calling live in [`schema/`](schema/README.md):
|
|
163
|
+
|
|
164
|
+
- [`schema/openai.json`](schema/openai.json) - OpenAI function calling format
|
|
165
|
+
- [`schema/anthropic.json`](schema/anthropic.json) - Anthropic tool use format
|
|
166
|
+
|
|
167
|
+
## Build
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm run build
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Testing
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
deno task check
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
deno task test
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Acknowledgements
|
|
184
|
+
|
|
185
|
+
This is a clean-room TypeScript reimplementation of the V4A context-anchored diff format, with additional features including fuzz matching, envelope stripping, and structured diff output.
|
|
186
|
+
|
|
187
|
+
Based on the V4A format specification and reference implementations by OpenAI:
|
|
188
|
+
|
|
189
|
+
- [Apply Patch Tool Documentation](https://developers.openai.com/api/docs/guides/tools-apply-patch)
|
|
190
|
+
- [Rust Reference (codex apply-patch)](https://github.com/openai/codex/tree/main/codex-rs/apply-patch)
|
|
191
|
+
- [TypeScript Reference (openai-agents-js)](https://github.com/openai/openai-agents-js/blob/main/packages/agents-core/src/utils/applyDiff.ts)
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for details.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
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;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** Diff application mode selector */
|
|
2
|
+
type ApplyDiffMode = 'default' | 'create';
|
|
3
|
+
/**
|
|
4
|
+
* Result from applying a diff.
|
|
5
|
+
* @description Contains patched text, structured diff, and source.
|
|
6
|
+
*/
|
|
7
|
+
type ApplyDiffResult = {
|
|
8
|
+
/** Patched output text */
|
|
9
|
+
text: string;
|
|
10
|
+
/** Structured line-by-line diff entries */
|
|
11
|
+
diff: DiffLine[];
|
|
12
|
+
/** Original source text before patching */
|
|
13
|
+
source: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Fuzzy context match result.
|
|
17
|
+
* @description Holds matched line index and fuzz penalty.
|
|
18
|
+
*/
|
|
19
|
+
type ContextMatch = {
|
|
20
|
+
/** Matched source line index */
|
|
21
|
+
matchedIndex: number;
|
|
22
|
+
/** Accumulated fuzz penalty score */
|
|
23
|
+
fuzzScore: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Single chunk of diff operations.
|
|
27
|
+
* @description Groups deleted and inserted lines at source position.
|
|
28
|
+
*/
|
|
29
|
+
type DiffChunk = {
|
|
30
|
+
/** Starting index in source lines */
|
|
31
|
+
sourceIndex: number;
|
|
32
|
+
/** Lines removed from source */
|
|
33
|
+
deletedLines: string[];
|
|
34
|
+
/** Lines added to output */
|
|
35
|
+
insertedLines: string[];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Single line in structured diff.
|
|
39
|
+
* @description Represents one add, delete, or equal line entry.
|
|
40
|
+
*/
|
|
41
|
+
type DiffLine = {
|
|
42
|
+
/** Operation type for this line */
|
|
43
|
+
type: 'add' | 'delete' | 'equal';
|
|
44
|
+
/** Line content without prefix */
|
|
45
|
+
value: string;
|
|
46
|
+
/** Source line number or null */
|
|
47
|
+
oldLine: number | null;
|
|
48
|
+
/** Result line number or null */
|
|
49
|
+
newLine: number | null;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Fuzzy matching strategy with penalty.
|
|
53
|
+
* @description Pairs a line transform with fuzz score.
|
|
54
|
+
*/
|
|
55
|
+
type FuzzStrategy = {
|
|
56
|
+
/** Line transformation function */
|
|
57
|
+
mapFn: (line: string) => string;
|
|
58
|
+
/** Penalty score for this strategy */
|
|
59
|
+
fuzzScore: number;
|
|
60
|
+
};
|
|
61
|
+
/** Hunk line operation mode */
|
|
62
|
+
type HunkLineMode = 'keep' | 'add' | 'delete';
|
|
63
|
+
/**
|
|
64
|
+
* Parsed update diff result.
|
|
65
|
+
* @description Contains diff chunks and total fuzz score.
|
|
66
|
+
*/
|
|
67
|
+
type ParsedUpdate = {
|
|
68
|
+
/** Ordered list of diff chunks */
|
|
69
|
+
diffChunks: DiffChunk[];
|
|
70
|
+
/** Total accumulated fuzz score */
|
|
71
|
+
fuzzScore: number;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Mutable parser state during processing.
|
|
75
|
+
* @description Tracks lines, cursor position, and fuzz score.
|
|
76
|
+
*/
|
|
77
|
+
type ParserState = {
|
|
78
|
+
/** All diff lines being parsed */
|
|
79
|
+
lines: string[];
|
|
80
|
+
/** Current cursor position */
|
|
81
|
+
currentIndex: number;
|
|
82
|
+
/** Running fuzz penalty total */
|
|
83
|
+
fuzzScore: number;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Parsed section with context and chunks.
|
|
87
|
+
* @description Groups context lines, chunks, and boundary info.
|
|
88
|
+
*/
|
|
89
|
+
type SectionResult = {
|
|
90
|
+
/** Context lines from source */
|
|
91
|
+
contextLines: string[];
|
|
92
|
+
/** Diff chunks in this section */
|
|
93
|
+
diffChunks: DiffChunk[];
|
|
94
|
+
/** Line index after section end */
|
|
95
|
+
endIndex: number;
|
|
96
|
+
/** True when section ends at EOF */
|
|
97
|
+
isEndOfFile: boolean;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* V4A context-anchored diff applicator.
|
|
102
|
+
* @description Applies V4A patches and produces structured diff output.
|
|
103
|
+
*/
|
|
104
|
+
declare class V4A {
|
|
105
|
+
/**
|
|
106
|
+
* Apply a V4A diff patch.
|
|
107
|
+
* @description Parses and applies diff to source text.
|
|
108
|
+
* @param sourceText - Original source file text
|
|
109
|
+
* @param diffText - V4A format diff string
|
|
110
|
+
* @param mode - Apply mode: default or create
|
|
111
|
+
* @returns Result with patched text and diff lines
|
|
112
|
+
*/
|
|
113
|
+
static apply(sourceText: string, diffText: string, mode?: ApplyDiffMode): ApplyDiffResult;
|
|
114
|
+
/**
|
|
115
|
+
* Apply diff chunks to source text.
|
|
116
|
+
* @description Builds output text and structured diff from chunks.
|
|
117
|
+
* @param sourceText - Original source file text
|
|
118
|
+
* @param diffChunks - Parsed diff chunks to apply
|
|
119
|
+
* @returns Result with patched text and diff lines
|
|
120
|
+
* @throws RangeError on out-of-bounds or overlapping chunks
|
|
121
|
+
*/
|
|
122
|
+
private static applyChunks;
|
|
123
|
+
/**
|
|
124
|
+
* Strip patch envelope markers from lines.
|
|
125
|
+
* @description Removes Begin/End Patch, file headers, and git markers.
|
|
126
|
+
* @param diffLines - Raw diff lines to filter
|
|
127
|
+
* @returns Lines without envelope markers
|
|
128
|
+
*/
|
|
129
|
+
private static stripEnvelope;
|
|
130
|
+
/**
|
|
131
|
+
* Strip leading empty lines from diff.
|
|
132
|
+
* @description Skips empty lines at start of array.
|
|
133
|
+
* @param diffLines - Lines to trim
|
|
134
|
+
* @returns Lines without leading empties
|
|
135
|
+
*/
|
|
136
|
+
private static stripLeadingEmpty;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export = V4A;
|
|
140
|
+
export type { ApplyDiffMode, ApplyDiffResult, ContextMatch, DiffChunk, DiffLine, FuzzStrategy, HunkLineMode, ParsedUpdate, ParserState, SectionResult };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** Diff application mode selector */
|
|
2
|
+
type ApplyDiffMode = 'default' | 'create';
|
|
3
|
+
/**
|
|
4
|
+
* Result from applying a diff.
|
|
5
|
+
* @description Contains patched text, structured diff, and source.
|
|
6
|
+
*/
|
|
7
|
+
type ApplyDiffResult = {
|
|
8
|
+
/** Patched output text */
|
|
9
|
+
text: string;
|
|
10
|
+
/** Structured line-by-line diff entries */
|
|
11
|
+
diff: DiffLine[];
|
|
12
|
+
/** Original source text before patching */
|
|
13
|
+
source: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Fuzzy context match result.
|
|
17
|
+
* @description Holds matched line index and fuzz penalty.
|
|
18
|
+
*/
|
|
19
|
+
type ContextMatch = {
|
|
20
|
+
/** Matched source line index */
|
|
21
|
+
matchedIndex: number;
|
|
22
|
+
/** Accumulated fuzz penalty score */
|
|
23
|
+
fuzzScore: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Single chunk of diff operations.
|
|
27
|
+
* @description Groups deleted and inserted lines at source position.
|
|
28
|
+
*/
|
|
29
|
+
type DiffChunk = {
|
|
30
|
+
/** Starting index in source lines */
|
|
31
|
+
sourceIndex: number;
|
|
32
|
+
/** Lines removed from source */
|
|
33
|
+
deletedLines: string[];
|
|
34
|
+
/** Lines added to output */
|
|
35
|
+
insertedLines: string[];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Single line in structured diff.
|
|
39
|
+
* @description Represents one add, delete, or equal line entry.
|
|
40
|
+
*/
|
|
41
|
+
type DiffLine = {
|
|
42
|
+
/** Operation type for this line */
|
|
43
|
+
type: 'add' | 'delete' | 'equal';
|
|
44
|
+
/** Line content without prefix */
|
|
45
|
+
value: string;
|
|
46
|
+
/** Source line number or null */
|
|
47
|
+
oldLine: number | null;
|
|
48
|
+
/** Result line number or null */
|
|
49
|
+
newLine: number | null;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Fuzzy matching strategy with penalty.
|
|
53
|
+
* @description Pairs a line transform with fuzz score.
|
|
54
|
+
*/
|
|
55
|
+
type FuzzStrategy = {
|
|
56
|
+
/** Line transformation function */
|
|
57
|
+
mapFn: (line: string) => string;
|
|
58
|
+
/** Penalty score for this strategy */
|
|
59
|
+
fuzzScore: number;
|
|
60
|
+
};
|
|
61
|
+
/** Hunk line operation mode */
|
|
62
|
+
type HunkLineMode = 'keep' | 'add' | 'delete';
|
|
63
|
+
/**
|
|
64
|
+
* Parsed update diff result.
|
|
65
|
+
* @description Contains diff chunks and total fuzz score.
|
|
66
|
+
*/
|
|
67
|
+
type ParsedUpdate = {
|
|
68
|
+
/** Ordered list of diff chunks */
|
|
69
|
+
diffChunks: DiffChunk[];
|
|
70
|
+
/** Total accumulated fuzz score */
|
|
71
|
+
fuzzScore: number;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Mutable parser state during processing.
|
|
75
|
+
* @description Tracks lines, cursor position, and fuzz score.
|
|
76
|
+
*/
|
|
77
|
+
type ParserState = {
|
|
78
|
+
/** All diff lines being parsed */
|
|
79
|
+
lines: string[];
|
|
80
|
+
/** Current cursor position */
|
|
81
|
+
currentIndex: number;
|
|
82
|
+
/** Running fuzz penalty total */
|
|
83
|
+
fuzzScore: number;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Parsed section with context and chunks.
|
|
87
|
+
* @description Groups context lines, chunks, and boundary info.
|
|
88
|
+
*/
|
|
89
|
+
type SectionResult = {
|
|
90
|
+
/** Context lines from source */
|
|
91
|
+
contextLines: string[];
|
|
92
|
+
/** Diff chunks in this section */
|
|
93
|
+
diffChunks: DiffChunk[];
|
|
94
|
+
/** Line index after section end */
|
|
95
|
+
endIndex: number;
|
|
96
|
+
/** True when section ends at EOF */
|
|
97
|
+
isEndOfFile: boolean;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* V4A context-anchored diff applicator.
|
|
102
|
+
* @description Applies V4A patches and produces structured diff output.
|
|
103
|
+
*/
|
|
104
|
+
declare class V4A {
|
|
105
|
+
/**
|
|
106
|
+
* Apply a V4A diff patch.
|
|
107
|
+
* @description Parses and applies diff to source text.
|
|
108
|
+
* @param sourceText - Original source file text
|
|
109
|
+
* @param diffText - V4A format diff string
|
|
110
|
+
* @param mode - Apply mode: default or create
|
|
111
|
+
* @returns Result with patched text and diff lines
|
|
112
|
+
*/
|
|
113
|
+
static apply(sourceText: string, diffText: string, mode?: ApplyDiffMode): ApplyDiffResult;
|
|
114
|
+
/**
|
|
115
|
+
* Apply diff chunks to source text.
|
|
116
|
+
* @description Builds output text and structured diff from chunks.
|
|
117
|
+
* @param sourceText - Original source file text
|
|
118
|
+
* @param diffChunks - Parsed diff chunks to apply
|
|
119
|
+
* @returns Result with patched text and diff lines
|
|
120
|
+
* @throws RangeError on out-of-bounds or overlapping chunks
|
|
121
|
+
*/
|
|
122
|
+
private static applyChunks;
|
|
123
|
+
/**
|
|
124
|
+
* Strip patch envelope markers from lines.
|
|
125
|
+
* @description Removes Begin/End Patch, file headers, and git markers.
|
|
126
|
+
* @param diffLines - Raw diff lines to filter
|
|
127
|
+
* @returns Lines without envelope markers
|
|
128
|
+
*/
|
|
129
|
+
private static stripEnvelope;
|
|
130
|
+
/**
|
|
131
|
+
* Strip leading empty lines from diff.
|
|
132
|
+
* @description Skips empty lines at start of array.
|
|
133
|
+
* @param diffLines - Lines to trim
|
|
134
|
+
* @returns Lines without leading empties
|
|
135
|
+
*/
|
|
136
|
+
private static stripLeadingEmpty;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { V4A as default };
|
|
140
|
+
export type { ApplyDiffMode, ApplyDiffResult, ContextMatch, DiffChunk, DiffLine, FuzzStrategy, HunkLineMode, ParsedUpdate, ParserState, SectionResult };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** Diff application mode selector */
|
|
2
|
+
type ApplyDiffMode = 'default' | 'create';
|
|
3
|
+
/**
|
|
4
|
+
* Result from applying a diff.
|
|
5
|
+
* @description Contains patched text, structured diff, and source.
|
|
6
|
+
*/
|
|
7
|
+
type ApplyDiffResult = {
|
|
8
|
+
/** Patched output text */
|
|
9
|
+
text: string;
|
|
10
|
+
/** Structured line-by-line diff entries */
|
|
11
|
+
diff: DiffLine[];
|
|
12
|
+
/** Original source text before patching */
|
|
13
|
+
source: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Fuzzy context match result.
|
|
17
|
+
* @description Holds matched line index and fuzz penalty.
|
|
18
|
+
*/
|
|
19
|
+
type ContextMatch = {
|
|
20
|
+
/** Matched source line index */
|
|
21
|
+
matchedIndex: number;
|
|
22
|
+
/** Accumulated fuzz penalty score */
|
|
23
|
+
fuzzScore: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Single chunk of diff operations.
|
|
27
|
+
* @description Groups deleted and inserted lines at source position.
|
|
28
|
+
*/
|
|
29
|
+
type DiffChunk = {
|
|
30
|
+
/** Starting index in source lines */
|
|
31
|
+
sourceIndex: number;
|
|
32
|
+
/** Lines removed from source */
|
|
33
|
+
deletedLines: string[];
|
|
34
|
+
/** Lines added to output */
|
|
35
|
+
insertedLines: string[];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Single line in structured diff.
|
|
39
|
+
* @description Represents one add, delete, or equal line entry.
|
|
40
|
+
*/
|
|
41
|
+
type DiffLine = {
|
|
42
|
+
/** Operation type for this line */
|
|
43
|
+
type: 'add' | 'delete' | 'equal';
|
|
44
|
+
/** Line content without prefix */
|
|
45
|
+
value: string;
|
|
46
|
+
/** Source line number or null */
|
|
47
|
+
oldLine: number | null;
|
|
48
|
+
/** Result line number or null */
|
|
49
|
+
newLine: number | null;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Fuzzy matching strategy with penalty.
|
|
53
|
+
* @description Pairs a line transform with fuzz score.
|
|
54
|
+
*/
|
|
55
|
+
type FuzzStrategy = {
|
|
56
|
+
/** Line transformation function */
|
|
57
|
+
mapFn: (line: string) => string;
|
|
58
|
+
/** Penalty score for this strategy */
|
|
59
|
+
fuzzScore: number;
|
|
60
|
+
};
|
|
61
|
+
/** Hunk line operation mode */
|
|
62
|
+
type HunkLineMode = 'keep' | 'add' | 'delete';
|
|
63
|
+
/**
|
|
64
|
+
* Parsed update diff result.
|
|
65
|
+
* @description Contains diff chunks and total fuzz score.
|
|
66
|
+
*/
|
|
67
|
+
type ParsedUpdate = {
|
|
68
|
+
/** Ordered list of diff chunks */
|
|
69
|
+
diffChunks: DiffChunk[];
|
|
70
|
+
/** Total accumulated fuzz score */
|
|
71
|
+
fuzzScore: number;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Mutable parser state during processing.
|
|
75
|
+
* @description Tracks lines, cursor position, and fuzz score.
|
|
76
|
+
*/
|
|
77
|
+
type ParserState = {
|
|
78
|
+
/** All diff lines being parsed */
|
|
79
|
+
lines: string[];
|
|
80
|
+
/** Current cursor position */
|
|
81
|
+
currentIndex: number;
|
|
82
|
+
/** Running fuzz penalty total */
|
|
83
|
+
fuzzScore: number;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Parsed section with context and chunks.
|
|
87
|
+
* @description Groups context lines, chunks, and boundary info.
|
|
88
|
+
*/
|
|
89
|
+
type SectionResult = {
|
|
90
|
+
/** Context lines from source */
|
|
91
|
+
contextLines: string[];
|
|
92
|
+
/** Diff chunks in this section */
|
|
93
|
+
diffChunks: DiffChunk[];
|
|
94
|
+
/** Line index after section end */
|
|
95
|
+
endIndex: number;
|
|
96
|
+
/** True when section ends at EOF */
|
|
97
|
+
isEndOfFile: boolean;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* V4A context-anchored diff applicator.
|
|
102
|
+
* @description Applies V4A patches and produces structured diff output.
|
|
103
|
+
*/
|
|
104
|
+
declare class V4A {
|
|
105
|
+
/**
|
|
106
|
+
* Apply a V4A diff patch.
|
|
107
|
+
* @description Parses and applies diff to source text.
|
|
108
|
+
* @param sourceText - Original source file text
|
|
109
|
+
* @param diffText - V4A format diff string
|
|
110
|
+
* @param mode - Apply mode: default or create
|
|
111
|
+
* @returns Result with patched text and diff lines
|
|
112
|
+
*/
|
|
113
|
+
static apply(sourceText: string, diffText: string, mode?: ApplyDiffMode): ApplyDiffResult;
|
|
114
|
+
/**
|
|
115
|
+
* Apply diff chunks to source text.
|
|
116
|
+
* @description Builds output text and structured diff from chunks.
|
|
117
|
+
* @param sourceText - Original source file text
|
|
118
|
+
* @param diffChunks - Parsed diff chunks to apply
|
|
119
|
+
* @returns Result with patched text and diff lines
|
|
120
|
+
* @throws RangeError on out-of-bounds or overlapping chunks
|
|
121
|
+
*/
|
|
122
|
+
private static applyChunks;
|
|
123
|
+
/**
|
|
124
|
+
* Strip patch envelope markers from lines.
|
|
125
|
+
* @description Removes Begin/End Patch, file headers, and git markers.
|
|
126
|
+
* @param diffLines - Raw diff lines to filter
|
|
127
|
+
* @returns Lines without envelope markers
|
|
128
|
+
*/
|
|
129
|
+
private static stripEnvelope;
|
|
130
|
+
/**
|
|
131
|
+
* Strip leading empty lines from diff.
|
|
132
|
+
* @description Skips empty lines at start of array.
|
|
133
|
+
* @param diffLines - Lines to trim
|
|
134
|
+
* @returns Lines without leading empties
|
|
135
|
+
*/
|
|
136
|
+
private static stripLeadingEmpty;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export = V4A;
|
|
140
|
+
export type { ApplyDiffMode, ApplyDiffResult, ContextMatch, DiffChunk, DiffLine, FuzzStrategy, HunkLineMode, ParsedUpdate, ParserState, SectionResult };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +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 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};
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neabyte/v4a-diff",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Apply context-anchored file patches from LLM tool calls",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.mjs",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"require": "./dist/index.cjs",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "unbuild --minify",
|
|
18
|
+
"clean": "rm -rf dist",
|
|
19
|
+
"prepublishOnly": "rm -rf dist && npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"anthropic",
|
|
23
|
+
"code-editing",
|
|
24
|
+
"context-anchored-diff",
|
|
25
|
+
"deno",
|
|
26
|
+
"diff-engine",
|
|
27
|
+
"diff-parser",
|
|
28
|
+
"esm",
|
|
29
|
+
"fuzz-matching",
|
|
30
|
+
"jsr",
|
|
31
|
+
"llm-tools",
|
|
32
|
+
"npm-package",
|
|
33
|
+
"openai",
|
|
34
|
+
"patch-applier",
|
|
35
|
+
"structured-diff",
|
|
36
|
+
"text-patching",
|
|
37
|
+
"tool-calling",
|
|
38
|
+
"typescript",
|
|
39
|
+
"unicode-normalization",
|
|
40
|
+
"v4a-diff",
|
|
41
|
+
"zero-dependencies"
|
|
42
|
+
],
|
|
43
|
+
"dependencies": {},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"unbuild": "^3.6.1"
|
|
46
|
+
},
|
|
47
|
+
"author": {
|
|
48
|
+
"name": "NeaByteLab",
|
|
49
|
+
"email": "me@neabyte.com",
|
|
50
|
+
"url": "https://github.com/NeaByteLab"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/NeaByteLab/V4A-Diff.git"
|
|
56
|
+
},
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/NeaByteLab/V4A-Diff/issues"
|
|
59
|
+
},
|
|
60
|
+
"homepage": "https://github.com/NeaByteLab/V4A-Diff#readme",
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=24.0.0"
|
|
63
|
+
},
|
|
64
|
+
"files": [
|
|
65
|
+
"dist/**/*",
|
|
66
|
+
"README.md",
|
|
67
|
+
"LICENSE"
|
|
68
|
+
],
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"access": "public"
|
|
71
|
+
}
|
|
72
|
+
}
|