@deepseek-ai/dsh-fs-patch 0.0.0-stage → 0.2.2-alpha.3
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 +235 -0
- package/README.i18n.yaml +40 -0
- package/README.md +111 -2
- package/README.zh.md +112 -0
- package/lib/index.js +275 -0
- package/lib/types/apply.d.ts +16 -0
- package/lib/types/index.d.ts +5 -0
- package/lib/types/parse.d.ts +20 -0
- package/lib/types/types.d.ts +52 -0
- package/package.json +32 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
DeepSeek contributions are licensed under MIT below.
|
|
2
|
+
|
|
3
|
+
This package adapts patch parsing and text updates from OpenAI Codex
|
|
4
|
+
rust-v0.158.0 (codex-rs/apply-patch/src/parser.rs, streaming_parser.rs,
|
|
5
|
+
seek_sequence.rs, and file_update.rs), with text_file.rs as a reference
|
|
6
|
+
for the line-ending representations. Test fixtures also draw from
|
|
7
|
+
file_update_tests.rs and lib.rs in that directory.
|
|
8
|
+
Copyright 2025 OpenAI. Those portions remain under Apache License 2.0 below.
|
|
9
|
+
Modifications: pure TypeScript batch API, original-input line locations,
|
|
10
|
+
serializable records, explicit rejection of environment-routing headers,
|
|
11
|
+
and a filesystem-independent NormalizeToLf updater with structured failures.
|
|
12
|
+
Selected parser and text tests are adapted from the same Apache-licensed sources.
|
|
13
|
+
|
|
14
|
+
MIT License
|
|
15
|
+
|
|
16
|
+
Copyright (c) 2026 DeepSeek
|
|
17
|
+
|
|
18
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
19
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
20
|
+
in the Software without restriction, including without limitation the rights
|
|
21
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
22
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
23
|
+
furnished to do so, subject to the following conditions:
|
|
24
|
+
|
|
25
|
+
The above copyright notice and this permission notice shall be included in all
|
|
26
|
+
copies or substantial portions of the Software.
|
|
27
|
+
|
|
28
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
29
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
30
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
31
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
32
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
33
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
34
|
+
SOFTWARE.
|
|
35
|
+
Apache License
|
|
36
|
+
Version 2.0, January 2004
|
|
37
|
+
http://www.apache.org/licenses/
|
|
38
|
+
|
|
39
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
40
|
+
|
|
41
|
+
1. Definitions.
|
|
42
|
+
|
|
43
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
44
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
45
|
+
|
|
46
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
47
|
+
the copyright owner that is granting the License.
|
|
48
|
+
|
|
49
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
50
|
+
other entities that control, are controlled by, or are under common
|
|
51
|
+
control with that entity. For the purposes of this definition,
|
|
52
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
53
|
+
direction or management of such entity, whether by contract or
|
|
54
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
55
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
56
|
+
|
|
57
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
58
|
+
exercising permissions granted by this License.
|
|
59
|
+
|
|
60
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
61
|
+
including but not limited to software source code, documentation
|
|
62
|
+
source, and configuration files.
|
|
63
|
+
|
|
64
|
+
"Object" form shall mean any form resulting from mechanical
|
|
65
|
+
transformation or translation of a Source form, including but
|
|
66
|
+
not limited to compiled object code, generated documentation,
|
|
67
|
+
and conversions to other media types.
|
|
68
|
+
|
|
69
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
70
|
+
Object form, made available under the License, as indicated by a
|
|
71
|
+
copyright notice that is included in or attached to the work
|
|
72
|
+
(an example is provided in the Appendix below).
|
|
73
|
+
|
|
74
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
75
|
+
form, that is based on (or derived from) the Work and for which the
|
|
76
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
77
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
78
|
+
of this License, Derivative Works shall not include works that remain
|
|
79
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
80
|
+
the Work and Derivative Works thereof.
|
|
81
|
+
|
|
82
|
+
"Contribution" shall mean any work of authorship, including
|
|
83
|
+
the original version of the Work and any modifications or additions
|
|
84
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
85
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
86
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
87
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
88
|
+
means any form of electronic, verbal, or written communication sent
|
|
89
|
+
to the Licensor or its representatives, including but not limited to
|
|
90
|
+
communication on electronic mailing lists, source code control systems,
|
|
91
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
92
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
93
|
+
excluding communication that is conspicuously marked or otherwise
|
|
94
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
95
|
+
|
|
96
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
97
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
98
|
+
subsequently incorporated within the Work.
|
|
99
|
+
|
|
100
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
101
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
102
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
103
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
104
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
105
|
+
Work and such Derivative Works in Source or Object form.
|
|
106
|
+
|
|
107
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
108
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
109
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
110
|
+
(except as stated in this section) patent license to make, have made,
|
|
111
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
112
|
+
where such license applies only to those patent claims licensable
|
|
113
|
+
by such Contributor that are necessarily infringed by their
|
|
114
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
115
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
116
|
+
institute patent litigation against any entity (including a
|
|
117
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
118
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
119
|
+
or contributory patent infringement, then any patent licenses
|
|
120
|
+
granted to You under this License for that Work shall terminate
|
|
121
|
+
as of the date such litigation is filed.
|
|
122
|
+
|
|
123
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
124
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
125
|
+
modifications, and in Source or Object form, provided that You
|
|
126
|
+
meet the following conditions:
|
|
127
|
+
|
|
128
|
+
(a) You must give any other recipients of the Work or
|
|
129
|
+
Derivative Works a copy of this License; and
|
|
130
|
+
|
|
131
|
+
(b) You must cause any modified files to carry prominent notices
|
|
132
|
+
stating that You changed the files; and
|
|
133
|
+
|
|
134
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
135
|
+
that You distribute, all copyright, patent, trademark, and
|
|
136
|
+
attribution notices from the Source form of the Work,
|
|
137
|
+
excluding those notices that do not pertain to any part of
|
|
138
|
+
the Derivative Works; and
|
|
139
|
+
|
|
140
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
141
|
+
distribution, then any Derivative Works that You distribute must
|
|
142
|
+
include a readable copy of the attribution notices contained
|
|
143
|
+
within such NOTICE file, excluding those notices that do not
|
|
144
|
+
pertain to any part of the Derivative Works, in at least one
|
|
145
|
+
of the following places: within a NOTICE text file distributed
|
|
146
|
+
as part of the Derivative Works; within the Source form or
|
|
147
|
+
documentation, if provided along with the Derivative Works; or,
|
|
148
|
+
within a display generated by the Derivative Works, if and
|
|
149
|
+
wherever such third-party notices normally appear. The contents
|
|
150
|
+
of the NOTICE file are for informational purposes only and
|
|
151
|
+
do not modify the License. You may add Your own attribution
|
|
152
|
+
notices within Derivative Works that You distribute, alongside
|
|
153
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
154
|
+
that such additional attribution notices cannot be construed
|
|
155
|
+
as modifying the License.
|
|
156
|
+
|
|
157
|
+
You may add Your own copyright statement to Your modifications and
|
|
158
|
+
may provide additional or different license terms and conditions
|
|
159
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
160
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
161
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
162
|
+
the conditions stated in this License.
|
|
163
|
+
|
|
164
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
165
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
166
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
167
|
+
this License, without any additional terms or conditions.
|
|
168
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
169
|
+
the terms of any separate license agreement you may have executed
|
|
170
|
+
with Licensor regarding such Contributions.
|
|
171
|
+
|
|
172
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
173
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
174
|
+
except as required for reasonable and customary use in describing the
|
|
175
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
176
|
+
|
|
177
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
178
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
179
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
180
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
181
|
+
implied, including, without limitation, any warranties or conditions
|
|
182
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
183
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
184
|
+
appropriateness of using or redistributing the Work and assume any
|
|
185
|
+
risks associated with Your exercise of permissions under this License.
|
|
186
|
+
|
|
187
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
188
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
189
|
+
unless required by applicable law (such as deliberate and grossly
|
|
190
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
191
|
+
liable to You for damages, including any direct, indirect, special,
|
|
192
|
+
incidental, or consequential damages of any character arising as a
|
|
193
|
+
result of this License or out of the use or inability to use the
|
|
194
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
195
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
196
|
+
other commercial damages or losses), even if such Contributor
|
|
197
|
+
has been advised of the possibility of such damages.
|
|
198
|
+
|
|
199
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
200
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
201
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
202
|
+
or other liability obligations and/or rights consistent with this
|
|
203
|
+
License. However, in accepting such obligations, You may act only
|
|
204
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
205
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
206
|
+
defend, and hold each Contributor harmless for any liability
|
|
207
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
208
|
+
of your accepting any such warranty or additional liability.
|
|
209
|
+
|
|
210
|
+
END OF TERMS AND CONDITIONS
|
|
211
|
+
|
|
212
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
213
|
+
|
|
214
|
+
To apply the Apache License to your work, attach the following
|
|
215
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
216
|
+
replaced with your own identifying information. (Don't include
|
|
217
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
218
|
+
comment syntax for the file format. We also recommend that a
|
|
219
|
+
file or class name and description of purpose be included on the
|
|
220
|
+
same "printed page" as the copyright notice for easier
|
|
221
|
+
identification within third-party archives.
|
|
222
|
+
|
|
223
|
+
Copyright 2025 OpenAI
|
|
224
|
+
|
|
225
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
226
|
+
you may not use this file except in compliance with the License.
|
|
227
|
+
You may obtain a copy of the License at
|
|
228
|
+
|
|
229
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
230
|
+
|
|
231
|
+
Unless required by applicable law or agreed to in writing, software
|
|
232
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
233
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
234
|
+
See the License for the specific language governing permissions and
|
|
235
|
+
limitations under the License.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/fs/fs-patch/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 80da396fec6cc3d9
|
|
7
|
+
zh: 35d4d8c713d9450c
|
|
8
|
+
/dsh-fs-patch:
|
|
9
|
+
en: ad2970c594c3b669
|
|
10
|
+
zh: fee247cad74b6191
|
|
11
|
+
/dsh-fs-patch/summary:
|
|
12
|
+
en: 614121759671f168
|
|
13
|
+
zh: 7cea460bb8940793
|
|
14
|
+
/dsh-fs-patch/table-of-contents:
|
|
15
|
+
en: 170f124543e5647a
|
|
16
|
+
zh: c30de483a11a1c09
|
|
17
|
+
/dsh-fs-patch/use-this-package:
|
|
18
|
+
en: 142b3b023512fc72
|
|
19
|
+
zh: bcfa70b710947eaf
|
|
20
|
+
/dsh-fs-patch/compatibility:
|
|
21
|
+
en: 70da6aede0649b32
|
|
22
|
+
zh: 2e3ed2964ae0461a
|
|
23
|
+
/dsh-fs-patch/compatibility/parsing:
|
|
24
|
+
en: 06644a9c38f7d569
|
|
25
|
+
zh: 91ca563d4e789087
|
|
26
|
+
/dsh-fs-patch/compatibility/matching-and-text-updates:
|
|
27
|
+
en: fcd630c121472565
|
|
28
|
+
zh: 35cc062e2f76b195
|
|
29
|
+
/dsh-fs-patch/understand-the-implementation:
|
|
30
|
+
en: 2ded4719a85a20bc
|
|
31
|
+
zh: e04f2614167a6c1c
|
|
32
|
+
/dsh-fs-patch/further-exploration:
|
|
33
|
+
en: d7d750bd71805956
|
|
34
|
+
zh: 8d33c76f5895d286
|
|
35
|
+
/dsh-fs-patch/known-limitations-and-deferred-work:
|
|
36
|
+
en: b18ead2791224bc2
|
|
37
|
+
zh: a4a8ab319c072445
|
|
38
|
+
/dsh-fs-patch/known-limitations-and-deferred-work/dev-note:
|
|
39
|
+
en: e67f633dc6fe7773
|
|
40
|
+
zh: e88aeb617ffb74ea
|
package/README.md
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "Parse Codex-style patches and apply hunks to text without filesystem I/O."
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# dsh-fs-patch
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
Parse a complete Codex-style patch into ordered file operations with `parsePatch`, then apply update hunks to LF-normalized text with `applyHunks`. Both functions work on caller-supplied strings without filesystem access. Syntax errors throw with the original input line; matching returns new text or a structured failure.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Compatibility](#compatibility)
|
|
18
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
19
|
+
- [Further Exploration](#further-exploration)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
<a id="use-this-package"></a>
|
|
24
|
+
## Use this package
|
|
25
|
+
|
|
26
|
+
Operations and hunks contain only JSON-serializable values, and parsing retains repeated paths in input order as unresolved strings.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { applyHunks, parsePatch } from '@deepseek-ai/dsh-fs-patch'
|
|
30
|
+
|
|
31
|
+
const operations = parsePatch('*** Begin Patch\n*** Update File: hello.txt\n-hello\n+hi\n*** End Patch')
|
|
32
|
+
const operation = operations[0]
|
|
33
|
+
if (operation?.kind === 'update') {
|
|
34
|
+
const result = applyHunks('hello\n', operation.hunks) // { ok: true, after: 'hi\n' }
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Malformed input throws `PatchParseError` with a one-based `lineNumber` and a corrective `reason`. Locations count leading blank lines and heredoc wrappers in the original input.
|
|
39
|
+
|
|
40
|
+
`applyHunks` returns `{ ok: true, after }` or `{ ok: false, failure }` without mutating its inputs. A match failure reports the hunk's patch line, the `context` or `oldLines` stage, the sequential search cursor, and the original unmatched pattern; a trailing-empty-line retry still reports the original pattern. Returning rather than throwing lets callers collect match failures across file operations.
|
|
41
|
+
|
|
42
|
+
<a id="compatibility"></a>
|
|
43
|
+
## Compatibility
|
|
44
|
+
|
|
45
|
+
The reference is [OpenAI Codex rust-v0.158.0](https://github.com/openai/codex/tree/rust-v0.158.0/codex-rs/apply-patch/src): `parser.rs` and `streaming_parser.rs` for syntax, and `seek_sequence.rs` and `file_update.rs` for matching and `NormalizeToLf` reconstruction. `text_file.rs` documents the separate line-preserving representation. The [parser tests](tests/parse.spec.ts) and [text tests](tests/apply.spec.ts) adapt selected upstream cases, including `file_update_tests.rs` and `lib.rs`. The [LICENSE](LICENSE) retains OpenAI attribution and Apache-2.0 terms alongside the MIT license for DeepSeek contributions.
|
|
46
|
+
|
|
47
|
+
### Parsing
|
|
48
|
+
|
|
49
|
+
| Input | Parsed result |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Begin/End Patch; Add/Delete/Update File headers | Ordered operations; an empty patch and an empty added file are accepted. |
|
|
52
|
+
| Added file lines prefixed by `+` | Prefix removed; each line contributes a final LF. |
|
|
53
|
+
| Update lines prefixed by space, `+`, or `-` | Context, insertion, or deletion; a bare empty line is empty context. |
|
|
54
|
+
| First update without `@@`; later `@@` or `@@ text` | Ordered hunks with an optional context anchor; empty hunks are rejected. |
|
|
55
|
+
| Move to immediately after Update File | Destination on the update; a move without a nonempty hunk is rejected. |
|
|
56
|
+
| End of File after a hunk | EOF flag; following blank lines are ignored and further content requires a new hunk or file header. |
|
|
57
|
+
| LF/CRLF, outer Unicode whitespace, indented headers | Codex trimming rules use Unicode `White_Space`, including U+0085 and excluding BOM (U+FEFF). Indented markers inside updates remain context. Content whitespace is retained. |
|
|
58
|
+
| Literal `<<EOF`, `<<'EOF'`, or `<<"EOF"` wrappers | Accepted without shell evaluation; the closing line must end in `EOF`, as in the reference. |
|
|
59
|
+
| Repeated paths, relative/absolute paths, Unicode text | Retained without path resolution or Unicode normalization. |
|
|
60
|
+
| Environment ID header | Dedicated unsupported-header error before the first operation; ordinary syntax errors elsewhere. |
|
|
61
|
+
| Unified diff, shell commands, malformed markers | Rejected with the original line number; diagnostic wording differs from Codex. |
|
|
62
|
+
|
|
63
|
+
EOF before any hunk has no effect, matching the reference; an update still requires a nonempty hunk. The reference also accepts a redundant final End Patch after an earlier End Patch and intervening whitespace.
|
|
64
|
+
|
|
65
|
+
### Matching and text updates
|
|
66
|
+
|
|
67
|
+
| Input or rule | Result |
|
|
68
|
+
|---|---|
|
|
69
|
+
| Four matching passes | Exact, trailing `White_Space` trimmed on both sides, both ends trimmed, then trimmed Unicode punctuation mapped to ASCII. Each pass searches all candidates before the next pass. |
|
|
70
|
+
| Unicode punctuation | U+2010–2015 and U+2212 map to `-`; U+2018–201B to single quotes; U+201C–201F to double quotes; U+00A0, U+2002–200A, U+202F, U+205F, and U+3000 to space. No NFC/NFKC normalization. |
|
|
71
|
+
| `@@ text` and repeated content | Find the first matching anchor from the cursor, then search after it. Old lines use the first match from that cursor, which advances to the match end. All searches use the original text. |
|
|
72
|
+
| EOF hunk | Search only at `lineCount - patternLength`, even before the cursor. The pinned implementation does not fall back to earlier matches, despite its search-function comment. |
|
|
73
|
+
| Empty old lines | Insert at the original file's end, before its final empty source line when present. The insertion does not advance the cursor; an anchor must still match. Same-position insertions retain patch order. |
|
|
74
|
+
| Unmatched pattern ending in an empty line | Retry without that last line; also remove the last new line if it is empty. An empty retry pattern matches the cursor. |
|
|
75
|
+
| Reconstruction | Remove one terminal empty split element before matching; apply stably sorted replacements in reverse order. Add a terminal empty element unless one exists, then join with LF. Empty output stays empty; nonempty output ends in LF. |
|
|
76
|
+
| Context lines | Replace the complete matched region from patch text, including tolerated context differences. |
|
|
77
|
+
| No hunks | Still perform the same reconstruction, so a missing terminal LF is added and one terminal empty line can be consumed. |
|
|
78
|
+
|
|
79
|
+
The intentional differences from Codex are ownership and result reporting: the DSH provider preserves line-ending style, so the matcher sees only LF-normalized text and `PreserveLineEndings` is not implemented. CR is an ordinary character: callers must normalize CRLF first and restore the chosen style afterward. Match failures return structured data rather than Codex's path-bearing errors, so the caller owns file identification and user-facing diagnostics.
|
|
80
|
+
|
|
81
|
+
<a id="understand-the-implementation"></a>
|
|
82
|
+
## Understand the implementation
|
|
83
|
+
|
|
84
|
+
<details>
|
|
85
|
+
<summary>Implementation internals — click to expand</summary>
|
|
86
|
+
|
|
87
|
+
The [parser](src/parse.ts) builds syntax records without a Session or provider dependency. The [text updater](src/apply.ts) locates every hunk against the original text before constructing a complete result. The [types](src/types.ts) carry syntax locations and match failures; the [filesystem reference](../../../docs/subsystems/filesystem.md#patch-syntax-records) owns the public type definitions.
|
|
88
|
+
|
|
89
|
+
</details>
|
|
90
|
+
|
|
91
|
+
<a id="further-exploration"></a>
|
|
92
|
+
## Further Exploration
|
|
93
|
+
|
|
94
|
+
- [Filesystem package group](../README.md) — providers, policies, and file tools.
|
|
95
|
+
- [Apply patch file editing](../../../.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.md) — decision record.
|
|
96
|
+
|
|
97
|
+
## Known Limitations and Deferred Work
|
|
98
|
+
|
|
99
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
100
|
+
|
|
101
|
+
- Text updates implement only `NormalizeToLf`; callers own line-ending conversion.
|
|
102
|
+
- The public API accepts complete strings, not streaming deltas.
|
|
103
|
+
|
|
104
|
+
<a id="dev-note"></a>
|
|
105
|
+
### Dev Note
|
|
106
|
+
|
|
107
|
+
<details>
|
|
108
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
109
|
+
|
|
110
|
+
None.
|
|
111
|
+
|
|
112
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "解析 Codex 风格补丁并将 hunk 应用于文本,不执行文件系统 I/O。"
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# dsh-fs-patch
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
使用 `parsePatch` 将完整的 Codex 风格补丁解析为有序文件操作,再用 `applyHunks` 将更新 hunk 应用于 LF 规范化文本。两个函数只处理调用方传入的字符串,不访问文件系统。语法错误抛出并标明原始输入行号;匹配返回新文本或结构化失败。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用此包](#use-this-package)
|
|
17
|
+
- [兼容性](#compatibility)
|
|
18
|
+
- [理解实现](#understand-the-implementation)
|
|
19
|
+
- [进一步探索](#further-exploration)
|
|
20
|
+
- [已知限制与后续工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
<a id="use-this-package"></a>
|
|
24
|
+
## 使用此包
|
|
25
|
+
|
|
26
|
+
操作和 hunk(修改块)只包含可 JSON 序列化的值;解析按输入顺序保留重复路径,路径保持为未解析的字符串。
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { applyHunks, parsePatch } from '@deepseek-ai/dsh-fs-patch'
|
|
30
|
+
|
|
31
|
+
const operations = parsePatch('*** Begin Patch\n*** Update File: hello.txt\n-hello\n+hi\n*** End Patch')
|
|
32
|
+
const operation = operations[0]
|
|
33
|
+
if (operation?.kind === 'update') {
|
|
34
|
+
const result = applyHunks('hello\n', operation.hunks) // { ok: true, after: 'hi\n' }
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
非法输入抛出 `PatchParseError`,包含从 1 开始的 `lineNumber` 和说明如何修正的 `reason`。位置计入原始输入中的前导空行和 heredoc 包装。
|
|
39
|
+
|
|
40
|
+
`applyHunks` 返回 `{ ok: true, after }` 或 `{ ok: false, failure }`,不修改输入。匹配失败报告 hunk 在补丁中的行号、`context` 或 `oldLines` 阶段、顺序搜索游标和原始未匹配模式;移除末尾空行重试后仍报告原始模式。以返回值而非异常报告失败,便于调用方收集多个文件操作的匹配失败。
|
|
41
|
+
|
|
42
|
+
<a id="compatibility"></a>
|
|
43
|
+
## 兼容性
|
|
44
|
+
|
|
45
|
+
参照实现为 [OpenAI Codex rust-v0.158.0](https://github.com/openai/codex/tree/rust-v0.158.0/codex-rs/apply-patch/src):语法参照 `parser.rs` 和 `streaming_parser.rs`,匹配及 `NormalizeToLf` 重建参照 `seek_sequence.rs` 和 `file_update.rs`。`text_file.rs` 说明独立的换行保留表示。[解析器测试](tests/parse.spec.ts) 与[文本测试](tests/apply.spec.ts) 改编选定的上游用例,包括 `file_update_tests.rs` 和 `lib.rs`。[LICENSE](LICENSE) 在 DeepSeek 贡献的 MIT 许可证之外,保留 OpenAI 署名与 Apache-2.0 条款。
|
|
46
|
+
|
|
47
|
+
### 解析
|
|
48
|
+
|
|
49
|
+
| 输入 | 解析结果 |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Begin/End Patch;Add/Delete/Update File 头 | 有序操作;接受空补丁和空新增文件。 |
|
|
52
|
+
| 新增文件中以 `+` 为前缀的行 | 去除前缀;每行都附带末尾 LF。 |
|
|
53
|
+
| 更新中以空格、`+` 或 `-` 为前缀的行 | 上下文、插入或删除;没有前缀的空行视为空上下文。 |
|
|
54
|
+
| 首个更新省略 `@@`;后续使用 `@@` 或 `@@ text` | 带可选上下文锚点的有序 hunk;拒绝空 hunk。 |
|
|
55
|
+
| 紧跟 Update File 的 Move to | 更新操作携带目标路径;没有非空 hunk 的移动被拒绝。 |
|
|
56
|
+
| hunk 后的 End of File | 设置 EOF 标志;忽略后续空白行,更多内容需要新的 hunk 或文件头。 |
|
|
57
|
+
| LF/CRLF、外围 Unicode 空白、缩进的文件头 | Codex 裁剪规则使用 Unicode `White_Space`,包含 U+0085,不包含 BOM(U+FEFF)。更新内部缩进的标记仍作为上下文。保留内容空白。 |
|
|
58
|
+
| 字面量 `<<EOF`、`<<'EOF'` 或 `<<"EOF"` 包装 | 接受但不执行 shell;与参照实现相同,结束行必须以 `EOF` 结尾。 |
|
|
59
|
+
| 重复路径、相对/绝对路径、Unicode 文本 | 原样保留,不解析路径或进行 Unicode 归一化。 |
|
|
60
|
+
| Environment ID 头 | 首个操作前给出专用的不支持错误;其他位置给出普通语法错误。 |
|
|
61
|
+
| Unified diff、shell 命令、非法标记 | 拒绝并给出原始行号;诊断措辞与 Codex 不同。 |
|
|
62
|
+
|
|
63
|
+
与参照实现相同,任何 hunk 之前的 EOF 没有效果;更新仍然需要非空 hunk。参照实现也接受在较早的 End Patch 和中间空白之后重复出现的最终 End Patch。
|
|
64
|
+
|
|
65
|
+
### 匹配与文本更新
|
|
66
|
+
|
|
67
|
+
| 输入或规则 | 结果 |
|
|
68
|
+
|---|---|
|
|
69
|
+
| 四级匹配 | 精确匹配、两侧去尾部 `White_Space`、两侧去首尾空白,再将去首尾空白后的 Unicode 标点映射为 ASCII。每一级遍历所有候选后才进入下一级。 |
|
|
70
|
+
| Unicode 标点 | U+2010–2015 和 U+2212 映射为 `-`;U+2018–201B 映射为单引号;U+201C–201F 映射为双引号;U+00A0、U+2002–200A、U+202F、U+205F、U+3000 映射为空格。不做 NFC/NFKC 归一化。 |
|
|
71
|
+
| `@@ text` 与重复内容 | 从游标起找第一个匹配锚点,再从其后一行搜索。旧行取游标后的首个匹配,随后游标推进到匹配末尾。所有搜索都基于原文本。 |
|
|
72
|
+
| EOF hunk | 只从 `lineCount - patternLength` 搜索,即使该位置在游标之前。固定版本的实现不会回退到更早的匹配位置,尽管搜索函数的注释如此描述。 |
|
|
73
|
+
| 旧行为空 | 在原文件末尾插入;若末尾有空源行,则插在该行之前。插入不推进游标;存在锚点时仍须匹配。同位置插入保留补丁顺序。 |
|
|
74
|
+
| 未匹配且末行为空的模式 | 去掉最后一行重试;新行末尾若为空也去掉。重试模式为空时匹配游标位置。 |
|
|
75
|
+
| 文本重建 | 匹配前去掉按 LF 分割所得的一个末尾空元素;按起点稳定排序后反序应用替换。如果末尾没有空元素则补一个,再用 LF 连接。空结果保持空;非空结果以 LF 结尾。 |
|
|
76
|
+
| 上下文行 | 用补丁文本替换整个匹配区间,包括容错匹配的上下文差异。 |
|
|
77
|
+
| 没有 hunk | 仍执行相同的重建,因此会补齐缺少的末尾 LF,也可能消耗一个末尾空行。 |
|
|
78
|
+
|
|
79
|
+
与 Codex 的刻意差异在于职责和结果报告:行尾风格由 DSH 提供方保留,因此匹配器只接收 LF 规范化文本,不实现 `PreserveLineEndings`。CR 是普通字符:调用方必须先规范化 CRLF,之后还原选定的行尾风格。匹配失败返回结构化数据而非 Codex 带路径的错误,因此调用方负责文件标识和面向用户的诊断。
|
|
80
|
+
|
|
81
|
+
<a id="understand-the-implementation"></a>
|
|
82
|
+
## 理解实现
|
|
83
|
+
|
|
84
|
+
<details>
|
|
85
|
+
<summary>实现内部机制——点击展开</summary>
|
|
86
|
+
|
|
87
|
+
[解析器](src/parse.ts) 构建语法记录,不依赖 Session 或提供方。[文本更新器](src/apply.ts) 先在原文本中定位每个 hunk,再构建完整结果。[类型](src/types.ts) 携带语法位置与匹配失败;[文件系统参考](../../../docs/subsystems/filesystem.zh.md#patch-syntax-records) 维护公开类型定义。
|
|
88
|
+
|
|
89
|
+
</details>
|
|
90
|
+
|
|
91
|
+
<a id="further-exploration"></a>
|
|
92
|
+
## 进一步探索
|
|
93
|
+
|
|
94
|
+
- [文件系统包组](../README.zh.md)——提供方、策略和文件工具。
|
|
95
|
+
- [Apply patch 文件编辑](../../../.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.zh.md)——决策记录。
|
|
96
|
+
|
|
97
|
+
## 已知限制与后续工作
|
|
98
|
+
|
|
99
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
100
|
+
|
|
101
|
+
- 文本更新只实现 `NormalizeToLf`;调用方负责行尾转换。
|
|
102
|
+
- 公开 API 接受完整字符串,不接收流式增量。
|
|
103
|
+
|
|
104
|
+
<a id="dev-note"></a>
|
|
105
|
+
### 开发备注
|
|
106
|
+
|
|
107
|
+
<details>
|
|
108
|
+
<summary>维护者工作上下文——点击展开</summary>
|
|
109
|
+
|
|
110
|
+
无。
|
|
111
|
+
|
|
112
|
+
</details>
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
//#region lib/types/parse.js
|
|
2
|
+
/**
|
|
3
|
+
* Batch parser following OpenAI Codex rust-v0.158.0 parser.rs and streaming_parser.rs.
|
|
4
|
+
* Adapted into a pure TypeScript parser with original-input diagnostics and no environment routing.
|
|
5
|
+
* Copyright 2025 OpenAI. Apache-2.0 portions and modification notice: ../LICENSE.
|
|
6
|
+
*/
|
|
7
|
+
const BEGIN = "*** Begin Patch";
|
|
8
|
+
const END = "*** End Patch";
|
|
9
|
+
const EOF = "*** End of File";
|
|
10
|
+
const MOVE = "*** Move to: ";
|
|
11
|
+
const ADD = "*** Add File: ";
|
|
12
|
+
const DELETE = "*** Delete File: ";
|
|
13
|
+
const UPDATE = "*** Update File: ";
|
|
14
|
+
/** Rust uses Unicode White_Space (all in the BMP); JavaScript trim also removes BOM. */
|
|
15
|
+
const WHITE_SPACE$1 = /\p{White_Space}/u;
|
|
16
|
+
/** Syntax failure with a one-based line in the original input, including any wrapper. */
|
|
17
|
+
var PatchParseError = class extends Error {
|
|
18
|
+
lineNumber;
|
|
19
|
+
reason;
|
|
20
|
+
constructor(lineNumber, reason) {
|
|
21
|
+
super(`Invalid patch at line ${lineNumber}: ${reason}`);
|
|
22
|
+
this.lineNumber = lineNumber;
|
|
23
|
+
this.reason = reason;
|
|
24
|
+
this.name = "PatchParseError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/** Scan the ends to avoid regex backtracking over long interior whitespace. */
|
|
28
|
+
function trim(value) {
|
|
29
|
+
let start = 0;
|
|
30
|
+
while (start < value.length && WHITE_SPACE$1.test(value.charAt(start))) start++;
|
|
31
|
+
return trimEnd(value.slice(start));
|
|
32
|
+
}
|
|
33
|
+
function trimEnd(value) {
|
|
34
|
+
let end = value.length;
|
|
35
|
+
while (end > 0 && WHITE_SPACE$1.test(value.charAt(end - 1))) end--;
|
|
36
|
+
return value.slice(0, end);
|
|
37
|
+
}
|
|
38
|
+
function patchLines(patch) {
|
|
39
|
+
const offset = patch.slice(0, patch.length - patch.replace(/^\p{White_Space}+/u, "").length).split("\n").length - 1;
|
|
40
|
+
let lines = trim(patch).split(/\r?\n/u).map((text, index) => ({
|
|
41
|
+
text,
|
|
42
|
+
number: offset + index + 1
|
|
43
|
+
}));
|
|
44
|
+
const first = lines[0];
|
|
45
|
+
const last = lines.at(-1);
|
|
46
|
+
if ([
|
|
47
|
+
"<<EOF",
|
|
48
|
+
"<<'EOF'",
|
|
49
|
+
"<<\"EOF\""
|
|
50
|
+
].includes(first.text) && last.text.endsWith("EOF") && lines.length >= 4) lines = lines.slice(1, -1);
|
|
51
|
+
const start = lines[0];
|
|
52
|
+
const end = lines.at(-1);
|
|
53
|
+
if (trim(start.text) !== BEGIN) throw new PatchParseError(start.number, `Expected '${BEGIN}'`);
|
|
54
|
+
if (trim(end.text) !== END) throw new PatchParseError(end.number, `Expected '${END}'`);
|
|
55
|
+
return lines.map((line, index) => index < lines.length - 1 ? {
|
|
56
|
+
...line,
|
|
57
|
+
text: line.text.replace(/\r$/u, "")
|
|
58
|
+
} : line);
|
|
59
|
+
}
|
|
60
|
+
function emptyHunk(hunk) {
|
|
61
|
+
return hunk.oldLines.length === 0 && hunk.newLines.length === 0;
|
|
62
|
+
}
|
|
63
|
+
function ensureUpdate(operation, lineNumber) {
|
|
64
|
+
if (operation?.kind !== "update") return;
|
|
65
|
+
const last = operation.hunks.at(-1);
|
|
66
|
+
if (last === void 0) throw new PatchParseError(operation.lineNumber, `Update for '${operation.path}' requires at least one hunk`);
|
|
67
|
+
if (emptyHunk(last)) throw new PatchParseError(lineNumber, "Update hunk must contain context, added, or removed lines");
|
|
68
|
+
}
|
|
69
|
+
function newHunk(lineNumber) {
|
|
70
|
+
return {
|
|
71
|
+
lineNumber,
|
|
72
|
+
oldLines: [],
|
|
73
|
+
newLines: [],
|
|
74
|
+
isEndOfFile: false
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function readUpdate(operation, line) {
|
|
78
|
+
const text = trimEnd(line.text);
|
|
79
|
+
const previous = operation.hunks.at(-1);
|
|
80
|
+
const marker = text === "@@" || text.startsWith("@@ ");
|
|
81
|
+
if (previous?.isEndOfFile) {
|
|
82
|
+
if (text === "") return;
|
|
83
|
+
if (!marker) throw new PatchParseError(line.number, "Expected '@@' after '*** End of File'");
|
|
84
|
+
}
|
|
85
|
+
if (previous === void 0 && operation.moveTo === void 0 && text.startsWith(MOVE)) {
|
|
86
|
+
operation.moveTo = text.slice(13);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (marker) {
|
|
90
|
+
if (previous !== void 0 && emptyHunk(previous)) throw new PatchParseError(line.number, "Update hunk must contain context, added, or removed lines");
|
|
91
|
+
const hunk = newHunk(line.number);
|
|
92
|
+
if (text.startsWith("@@ ")) hunk.context = text.slice(3);
|
|
93
|
+
operation.hunks.push(hunk);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (text === EOF) {
|
|
97
|
+
if (previous !== void 0) {
|
|
98
|
+
if (emptyHunk(previous)) throw new PatchParseError(line.number, "Update hunk must contain context, added, or removed lines");
|
|
99
|
+
previous.isEndOfFile = true;
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const prefix = line.text[0];
|
|
104
|
+
if (line.text !== "" && prefix !== " " && prefix !== "+" && prefix !== "-") throw new PatchParseError(line.number, "Expected a context (' '), added ('+'), removed ('-') line, or '@@'");
|
|
105
|
+
const hunk = previous ?? newHunk(line.number);
|
|
106
|
+
if (previous === void 0) operation.hunks.push(hunk);
|
|
107
|
+
const content = line.text.slice(1);
|
|
108
|
+
if (prefix === "+") hunk.newLines.push(content);
|
|
109
|
+
else if (prefix === "-") hunk.oldLines.push(content);
|
|
110
|
+
else {
|
|
111
|
+
hunk.oldLines.push(content);
|
|
112
|
+
hunk.newLines.push(content);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parse a complete Codex-style patch without reading or modifying files.
|
|
117
|
+
* @param patch - patch text, optionally wrapped in a literal EOF heredoc.
|
|
118
|
+
* @returns ordered, JSON-serializable operations; an empty patch returns an empty array.
|
|
119
|
+
* @throws PatchParseError for malformed syntax or an unsupported Environment ID header.
|
|
120
|
+
*/
|
|
121
|
+
function parsePatch(patch) {
|
|
122
|
+
const lines = patchLines(patch);
|
|
123
|
+
const operations = [];
|
|
124
|
+
let ended = false;
|
|
125
|
+
for (const line of lines.slice(1)) {
|
|
126
|
+
const operation = operations.at(-1);
|
|
127
|
+
const header = operation?.kind === "update" && line !== lines.at(-1) ? trimEnd(line.text) : trim(line.text);
|
|
128
|
+
if (ended && line !== lines.at(-1)) {
|
|
129
|
+
if (trim(line.text) !== "") throw new PatchParseError(line.number, `Unexpected content after '${END}'`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (header === END) {
|
|
133
|
+
ensureUpdate(operation, line.number);
|
|
134
|
+
ended = true;
|
|
135
|
+
} else if (header.startsWith(ADD)) {
|
|
136
|
+
ensureUpdate(operation, line.number);
|
|
137
|
+
operations.push({
|
|
138
|
+
kind: "add",
|
|
139
|
+
path: header.slice(14),
|
|
140
|
+
lineNumber: line.number,
|
|
141
|
+
content: ""
|
|
142
|
+
});
|
|
143
|
+
} else if (header.startsWith(DELETE)) {
|
|
144
|
+
ensureUpdate(operation, line.number);
|
|
145
|
+
operations.push({
|
|
146
|
+
kind: "delete",
|
|
147
|
+
path: header.slice(17),
|
|
148
|
+
lineNumber: line.number
|
|
149
|
+
});
|
|
150
|
+
} else if (header.startsWith(UPDATE)) {
|
|
151
|
+
ensureUpdate(operation, line.number);
|
|
152
|
+
operations.push({
|
|
153
|
+
kind: "update",
|
|
154
|
+
path: header.slice(17),
|
|
155
|
+
lineNumber: line.number,
|
|
156
|
+
hunks: []
|
|
157
|
+
});
|
|
158
|
+
} else if (operations.length === 0 && header.startsWith("*** Environment ID:")) throw new PatchParseError(line.number, "Environment ID headers are unsupported; the caller selects the filesystem provider");
|
|
159
|
+
else if (operation?.kind === "add" && line.text.startsWith("+")) operation.content += `${line.text.slice(1)}\n`;
|
|
160
|
+
else if (operation?.kind === "update") readUpdate(operation, line);
|
|
161
|
+
else throw new PatchParseError(line.number, "Expected an Add File, Delete File, or Update File header; added content must start with +");
|
|
162
|
+
}
|
|
163
|
+
return operations;
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region lib/types/apply.js
|
|
167
|
+
/**
|
|
168
|
+
* Pure text updates following OpenAI Codex rust-v0.158.0 NormalizeToLf behavior.
|
|
169
|
+
* Adapted from seek_sequence.rs and file_update.rs with structured match failures.
|
|
170
|
+
* Copyright 2025 OpenAI. Apache-2.0 portions and modification notice: ../LICENSE.
|
|
171
|
+
*/
|
|
172
|
+
const WHITE_SPACE = /\p{White_Space}/u;
|
|
173
|
+
function trimWhitespace(value, leading) {
|
|
174
|
+
let start = 0;
|
|
175
|
+
let end = value.length;
|
|
176
|
+
if (leading) while (start < end && WHITE_SPACE.test(value.charAt(start))) start++;
|
|
177
|
+
while (end > start && WHITE_SPACE.test(value.charAt(end - 1))) end--;
|
|
178
|
+
return value.slice(start, end);
|
|
179
|
+
}
|
|
180
|
+
function normalizePunctuation(value) {
|
|
181
|
+
return trimWhitespace(value, true).replace(/[\u2010-\u2015\u2212]/gu, "-").replace(/[\u2018-\u201B]/gu, "'").replace(/[\u201C-\u201F]/gu, "\"").replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/gu, " ");
|
|
182
|
+
}
|
|
183
|
+
const MATCH_PASSES = [
|
|
184
|
+
(value) => value,
|
|
185
|
+
(value) => trimWhitespace(value, false),
|
|
186
|
+
(value) => trimWhitespace(value, true),
|
|
187
|
+
normalizePunctuation
|
|
188
|
+
];
|
|
189
|
+
/** NormalizeToLf checks only the tail candidate for EOF, even before the cursor. */
|
|
190
|
+
function seekSequence(lines, pattern, start, eof) {
|
|
191
|
+
if (pattern.length === 0) return start;
|
|
192
|
+
if (pattern.length > lines.length) return void 0;
|
|
193
|
+
const last = lines.length - pattern.length;
|
|
194
|
+
const first = eof ? last : start;
|
|
195
|
+
for (const normalize of MATCH_PASSES) {
|
|
196
|
+
const expected = pattern.map(normalize);
|
|
197
|
+
for (let index = first; index <= last; index++) if (expected.every((line, offset) => normalize(lines[index + offset]) === line)) return index;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Apply parsed hunks to LF-normalized text without I/O or input mutation.
|
|
202
|
+
* All searches use the original text; a failed search returns no partial text.
|
|
203
|
+
* Successful nonempty output ends in LF, including initially unterminated text.
|
|
204
|
+
* @param before - text normalized by the caller; CR characters receive no line-ending handling.
|
|
205
|
+
* @param hunks - ordered hunks from parsePatch; EOF searches follow NormalizeToLf tail semantics.
|
|
206
|
+
* @returns new text or the first unmatched hunk, without throwing for a match failure.
|
|
207
|
+
*/
|
|
208
|
+
function applyHunks(before, hunks) {
|
|
209
|
+
const original = before.split("\n");
|
|
210
|
+
if (original.at(-1) === "") original.pop();
|
|
211
|
+
const replacements = [];
|
|
212
|
+
let lineIndex = 0;
|
|
213
|
+
for (const hunk of hunks) {
|
|
214
|
+
if (hunk.context !== void 0) {
|
|
215
|
+
const pattern = [hunk.context];
|
|
216
|
+
const found = seekSequence(original, pattern, lineIndex, false);
|
|
217
|
+
if (found === void 0) return {
|
|
218
|
+
ok: false,
|
|
219
|
+
failure: {
|
|
220
|
+
hunkLineNumber: hunk.lineNumber,
|
|
221
|
+
stage: "context",
|
|
222
|
+
searchStart: lineIndex,
|
|
223
|
+
pattern
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
lineIndex = found + 1;
|
|
227
|
+
}
|
|
228
|
+
if (hunk.oldLines.length === 0) {
|
|
229
|
+
const start = original.at(-1) === "" ? original.length - 1 : original.length;
|
|
230
|
+
replacements.push({
|
|
231
|
+
start,
|
|
232
|
+
oldLength: 0,
|
|
233
|
+
newLines: hunk.newLines
|
|
234
|
+
});
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
let pattern = hunk.oldLines;
|
|
238
|
+
let newLines = hunk.newLines;
|
|
239
|
+
let found = seekSequence(original, pattern, lineIndex, hunk.isEndOfFile);
|
|
240
|
+
if (found === void 0 && pattern.at(-1) === "") {
|
|
241
|
+
pattern = pattern.slice(0, -1);
|
|
242
|
+
if (newLines.at(-1) === "") newLines = newLines.slice(0, -1);
|
|
243
|
+
found = seekSequence(original, pattern, lineIndex, hunk.isEndOfFile);
|
|
244
|
+
}
|
|
245
|
+
if (found === void 0) return {
|
|
246
|
+
ok: false,
|
|
247
|
+
failure: {
|
|
248
|
+
hunkLineNumber: hunk.lineNumber,
|
|
249
|
+
stage: "oldLines",
|
|
250
|
+
searchStart: lineIndex,
|
|
251
|
+
pattern: [...hunk.oldLines]
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
replacements.push({
|
|
255
|
+
start: found,
|
|
256
|
+
oldLength: pattern.length,
|
|
257
|
+
newLines
|
|
258
|
+
});
|
|
259
|
+
lineIndex = found + pattern.length;
|
|
260
|
+
}
|
|
261
|
+
replacements.sort((left, right) => left.start - right.start);
|
|
262
|
+
let lines = original;
|
|
263
|
+
for (const replacement of replacements.reverse()) lines = [
|
|
264
|
+
...lines.slice(0, replacement.start),
|
|
265
|
+
...replacement.newLines,
|
|
266
|
+
...lines.slice(replacement.start + replacement.oldLength)
|
|
267
|
+
];
|
|
268
|
+
if (lines.at(-1) !== "") lines.push("");
|
|
269
|
+
return {
|
|
270
|
+
ok: true,
|
|
271
|
+
after: lines.join("\n")
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
export { PatchParseError, applyHunks, parsePatch };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure text updates following OpenAI Codex rust-v0.158.0 NormalizeToLf behavior.
|
|
3
|
+
* Adapted from seek_sequence.rs and file_update.rs with structured match failures.
|
|
4
|
+
* Copyright 2025 OpenAI. Apache-2.0 portions and modification notice: ../LICENSE.
|
|
5
|
+
*/
|
|
6
|
+
import type { ApplyHunksResult, PatchHunk } from './types.ts';
|
|
7
|
+
/**
|
|
8
|
+
* Apply parsed hunks to LF-normalized text without I/O or input mutation.
|
|
9
|
+
* All searches use the original text; a failed search returns no partial text.
|
|
10
|
+
* Successful nonempty output ends in LF, including initially unterminated text.
|
|
11
|
+
* @param before - text normalized by the caller; CR characters receive no line-ending handling.
|
|
12
|
+
* @param hunks - ordered hunks from parsePatch; EOF searches follow NormalizeToLf tail semantics.
|
|
13
|
+
* @returns new text or the first unmatched hunk, without throwing for a match failure.
|
|
14
|
+
*/
|
|
15
|
+
export declare function applyHunks(before: string, hunks: readonly PatchHunk[]): ApplyHunksResult;
|
|
16
|
+
//# sourceMappingURL=apply.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Pure patch parsing and text application, independent of filesystem providers. */
|
|
2
|
+
export { parsePatch, PatchParseError } from './parse.ts';
|
|
3
|
+
export { applyHunks } from './apply.ts';
|
|
4
|
+
export type { ApplyHunksResult, HunkApplyFailure, PatchHunk, PatchOperation } from './types.ts';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch parser following OpenAI Codex rust-v0.158.0 parser.rs and streaming_parser.rs.
|
|
3
|
+
* Adapted into a pure TypeScript parser with original-input diagnostics and no environment routing.
|
|
4
|
+
* Copyright 2025 OpenAI. Apache-2.0 portions and modification notice: ../LICENSE.
|
|
5
|
+
*/
|
|
6
|
+
import type { PatchOperation } from './types.ts';
|
|
7
|
+
/** Syntax failure with a one-based line in the original input, including any wrapper. */
|
|
8
|
+
export declare class PatchParseError extends Error {
|
|
9
|
+
readonly lineNumber: number;
|
|
10
|
+
readonly reason: string;
|
|
11
|
+
constructor(lineNumber: number, reason: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parse a complete Codex-style patch without reading or modifying files.
|
|
15
|
+
* @param patch - patch text, optionally wrapped in a literal EOF heredoc.
|
|
16
|
+
* @returns ordered, JSON-serializable operations; an empty patch returns an empty array.
|
|
17
|
+
* @throws PatchParseError for malformed syntax or an unsupported Environment ID header.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parsePatch(patch: string): PatchOperation[];
|
|
20
|
+
//# sourceMappingURL=parse.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Serializable patch syntax and text-application results; paths remain unresolved. */
|
|
2
|
+
/** One contiguous update. */
|
|
3
|
+
export interface PatchHunk {
|
|
4
|
+
/** One-based input line containing the marker or first implicit hunk line. */
|
|
5
|
+
lineNumber: number;
|
|
6
|
+
/** Text following `@@ `, used as a search anchor by the text matcher. */
|
|
7
|
+
context?: string;
|
|
8
|
+
/** Context and removed lines, without patch prefixes or line endings. */
|
|
9
|
+
oldLines: string[];
|
|
10
|
+
/** Context and added lines, without patch prefixes or line endings. */
|
|
11
|
+
newLines: string[];
|
|
12
|
+
/** Whether `*** End of File` follows this hunk. */
|
|
13
|
+
isEndOfFile: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Ordered file operation; a move is an update with a destination path. */
|
|
16
|
+
export type PatchOperation = {
|
|
17
|
+
kind: 'add';
|
|
18
|
+
path: string;
|
|
19
|
+
lineNumber: number;
|
|
20
|
+
/** Added lines joined with LF, including a final LF for every added line. */
|
|
21
|
+
content: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'delete';
|
|
24
|
+
path: string;
|
|
25
|
+
lineNumber: number;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'update';
|
|
28
|
+
path: string;
|
|
29
|
+
lineNumber: number;
|
|
30
|
+
moveTo?: string;
|
|
31
|
+
hunks: PatchHunk[];
|
|
32
|
+
};
|
|
33
|
+
/** The first hunk whose anchor or old lines could not be located in the original text. */
|
|
34
|
+
export interface HunkApplyFailure {
|
|
35
|
+
/** One-based line of the failing hunk in the patch input. */
|
|
36
|
+
hunkLineNumber: number;
|
|
37
|
+
/** The failed anchor or old-content search. */
|
|
38
|
+
stage: 'context' | 'oldLines';
|
|
39
|
+
/** Zero-based sequential search cursor; EOF searches override it with the tail candidate. */
|
|
40
|
+
searchStart: number;
|
|
41
|
+
/** Original anchor or old lines, including any trailing empty line retried without its sentinel. */
|
|
42
|
+
pattern: string[];
|
|
43
|
+
}
|
|
44
|
+
/** Complete new text or a match failure; failures contain no partially updated text. */
|
|
45
|
+
export type ApplyHunksResult = {
|
|
46
|
+
ok: true;
|
|
47
|
+
after: string;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
failure: HunkApplyFailure;
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-fs-patch",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "Pure Codex-style patch parser and LF text updater with structured match failures",
|
|
4
|
+
"version": "0.2.2-alpha.3",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/fs/fs-patch"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./src/*": "./src/*",
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"lib/index.js",
|
|
26
|
+
"lib/types/**/*.d.ts"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@deepseek-ai/cordis": "~4.0.6-alpha.2"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@deepseek-ai/cordis": "~4.0.6-alpha.2"
|
|
34
|
+
}
|
|
6
35
|
}
|