@plurnk/plurnk-schemes 0.3.0 → 0.4.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.
@@ -10,312 +10,314 @@
10
10
  // <-1> as defined sentinels. Other negatives (<-2>, <-3>) are not
11
11
  // specified and rejected as 416. Within a range, -1 as the M endpoint
12
12
  // means "include through the last line" (so <1,-1> is whole content).
13
- const splitLines = (content) => {
14
- const trailingNewline = content.endsWith("\n");
15
- if (content === "")
16
- return { lines: [], trailingNewline: false };
17
- const lines = content.split("\n");
18
- if (trailingNewline)
19
- lines.pop();
20
- return { lines, trailingNewline };
21
- };
22
- const normalize = (marker, totalLines) => {
23
- const { first, last } = marker;
24
- if (last === null) {
25
- if (first === 0)
26
- return { kind: "before-first", start: 0, end: 0 };
27
- if (first === -1)
28
- return { kind: "after-last", start: 0, end: 0 };
29
- if (first > 0 && first <= totalLines)
30
- return { kind: "range", start: first, end: first };
31
- return { error: `line ${first} out of range (1..${totalLines})` };
13
+ export default class Slicer {
14
+ static #splitLines(content) {
15
+ const trailingNewline = content.endsWith("\n");
16
+ if (content === "")
17
+ return { lines: [], trailingNewline: false };
18
+ const lines = content.split("\n");
19
+ if (trailingNewline)
20
+ lines.pop();
21
+ return { lines, trailingNewline };
32
22
  }
33
- let n = first;
34
- let m = last;
35
- if (n === 0)
36
- n = 1;
37
- if (m === -1)
38
- m = totalLines;
39
- if (n < 1 || n > totalLines)
40
- return { error: `range start ${first} out of range (1..${totalLines})` };
41
- if (m < 1 || m > totalLines)
42
- return { error: `range end ${last} out of range (1..${totalLines})` };
43
- if (n > m)
44
- return { error: `range start ${first} > end ${last}` };
45
- return { kind: "range", start: n, end: m };
46
- };
47
- // READ a line range. Returns the raw selected lines (no `N:\t` prefix)
48
- // plus the 1-indexed position of the first selected line. The render
49
- // layer adds `N:\t` per plurnk.md ("READ output prefixes every line with
50
- // line numbers, N:\t") starting from `startLine` — keeps numbering as a
51
- // presentation concern, prevents double-prefixing when the same content
52
- // passes through the log render.
53
- //
54
- // Sentinel positions <0> and <-1> select no content (they're insertion
55
- // points, not lines) → status 200 with empty text.
56
- export const sliceLines = (content, marker) => {
57
- const { lines } = splitLines(content);
58
- const norm = normalize(marker, lines.length);
59
- if ("error" in norm)
60
- return { status: 416, error: norm.error };
61
- if (norm.kind !== "range")
62
- return { status: 200, text: "", startLine: undefined };
63
- const selected = lines.slice(norm.start - 1, norm.end);
64
- return { status: 200, text: selected.join("\n"), startLine: norm.start };
65
- };
66
- // Structural `<L>` slice for JSON sources (plurnk-grammar 0.13.0).
67
- // "On structured entries, <L> addresses item index, not line number."
68
- // Every JSON value becomes a list of top-level items:
69
- // array `[a, b, c]` → items are the array elements
70
- // object `{k1: v1, ...}` → items are key-value pairs (as single-key objects)
71
- // scalar `"hello"` / 42 → item is the scalar itself (length-1 list)
72
- // `<L>` indexes into that list (1-indexed). Result is always a JSON array.
73
- // Sentinels `<0>` / `<-1>` are insertion points — empty `[]` for READ.
74
- // Out-of-range positions return 416. Matches the uniform "always JSON
75
- // array out" shape we settled for matcher results.
76
- const jsonValueToItems = (parsed) => {
77
- if (Array.isArray(parsed))
78
- return parsed;
79
- if (parsed !== null && typeof parsed === "object") {
80
- // Object items are single-key {key: value} wrappers, in insertion
81
- // order. Object.entries preserves spec-guaranteed iteration order
82
- // for string keys.
83
- return Object.entries(parsed).map(([k, v]) => ({ [k]: v }));
84
- }
85
- // Scalar (string, number, boolean, null): a length-1 list of itself.
86
- return [parsed];
87
- };
88
- export const sliceJsonItems = (content, marker) => {
89
- let parsed;
90
- try {
91
- parsed = JSON.parse(content);
92
- }
93
- catch (err) {
94
- return { status: 400, error: `malformed JSON: ${err instanceof Error ? err.message : String(err)}` };
95
- }
96
- const items = jsonValueToItems(parsed);
97
- const total = items.length;
98
- const { first, last } = marker;
99
- if (last === null) {
100
- if (first === 0 || first === -1)
101
- return { status: 200, body: "[]" };
102
- if (first > 0 && first <= total)
103
- return { status: 200, body: JSON.stringify([items[first - 1]], null, 2) };
104
- return { status: 416, error: `item ${first} out of range (1..${total})` };
105
- }
106
- let n = first;
107
- let m = last;
108
- if (n === 0)
109
- n = 1;
110
- if (m === -1)
111
- m = total;
112
- if (n < 1 || n > total)
113
- return { status: 416, error: `range start ${first} out of range (1..${total})` };
114
- if (m < 1 || m > total)
115
- return { status: 416, error: `range end ${last} out of range (1..${total})` };
116
- if (n > m)
117
- return { status: 416, error: `range start ${first} > end ${last}` };
118
- return { status: 200, body: JSON.stringify(items.slice(n - 1, m), null, 2) };
119
- };
120
- // Structural `<L>` EDIT for JSON sources (plurnk-grammar 0.13.0/0.14.0).
121
- // Source-shape rules (matches sliceJsonItems' item definition):
122
- // array → items are elements
123
- // object → items are key-value pairs (single-key fragments)
124
- // scalar → length-1 list of itself; grow markers (<0>,<-1>) reject
125
- //
126
- // Body shape (Resolution B):
127
- // body parses as JSON array → those are the items to splice in
128
- // body parses as non-array JSON → single item to splice in
129
- // empty body → delete the selection
130
- // body fails JSON parse → 400 (path-extension declares intent; honor it)
131
- //
132
- // Marker semantics (parallel to line-EDIT):
133
- // <N> replace item N with body item(s)
134
- // <N,M> replace items N..M with body item(s)
135
- // <0> prepend body item(s)
136
- // <-1> append body item(s)
137
- // <1,-1> replace whole top-level with body item(s); empty body clears
138
- // Empty body on a sentinel insertion (<0> or <-1>) → no-op.
139
- const itemsFromBody = (body) => {
140
- if (body === "")
141
- return { items: [] }; // empty body = delete
142
- let parsed;
143
- try {
144
- parsed = JSON.parse(body);
145
- }
146
- catch (err) {
147
- return { error: `malformed JSON body: ${err instanceof Error ? err.message : String(err)}` };
148
- }
149
- if (Array.isArray(parsed))
150
- return { items: parsed };
151
- return { items: [parsed] };
152
- };
153
- const applyJsonArrayEdit = (source, marker, items) => {
154
- const total = source.length;
155
- const { first, last } = marker;
156
- let result;
157
- if (last === null) {
158
- if (first === 0)
159
- result = [...items, ...source];
160
- else if (first === -1)
161
- result = [...source, ...items];
162
- else if (first > 0 && first <= total)
163
- result = [...source.slice(0, first - 1), ...items, ...source.slice(first)];
164
- else
165
- return { status: 416, error: `position ${first} out of range (1..${total})` };
166
- }
167
- else {
23
+ static #normalize(marker, totalLines) {
24
+ const { first, last } = marker;
25
+ if (last === null) {
26
+ if (first === 0)
27
+ return { kind: "before-first", start: 0, end: 0 };
28
+ if (first === -1)
29
+ return { kind: "after-last", start: 0, end: 0 };
30
+ if (first > 0 && first <= totalLines)
31
+ return { kind: "range", start: first, end: first };
32
+ return { error: `line ${first} out of range (1..${totalLines})` };
33
+ }
168
34
  let n = first;
169
35
  let m = last;
170
36
  if (n === 0)
171
37
  n = 1;
172
38
  if (m === -1)
173
- m = total;
174
- if (total === 0 && (first !== 1 || last !== -1))
175
- return { status: 416, error: `range on empty array` };
176
- if (n < 1 || (total > 0 && n > total))
177
- return { status: 416, error: `range start ${first} out of range (1..${total})` };
178
- if (m < 1 || (total > 0 && m > total))
179
- return { status: 416, error: `range end ${last} out of range (1..${total})` };
39
+ m = totalLines;
40
+ if (n < 1 || n > totalLines)
41
+ return { error: `range start ${first} out of range (1..${totalLines})` };
42
+ if (m < 1 || m > totalLines)
43
+ return { error: `range end ${last} out of range (1..${totalLines})` };
180
44
  if (n > m)
181
- return { status: 416, error: `range start ${first} > end ${last}` };
182
- result = [...source.slice(0, n - 1), ...items, ...source.slice(m)];
45
+ return { error: `range start ${first} > end ${last}` };
46
+ return { kind: "range", start: n, end: m };
183
47
  }
184
- return { status: 200, result: JSON.stringify(result, null, 2) };
185
- };
186
- const applyJsonObjectEdit = (source, marker, items) => {
187
- // Object items are key-value pairs. Body items must be objects;
188
- // each object's entries become kv-pairs to splice in. Items that
189
- // aren't single objects 400 (model used wrong body shape for an
190
- // object source).
191
- const bodyEntries = [];
192
- for (const item of items) {
193
- if (item === null || typeof item !== "object" || Array.isArray(item)) {
194
- return { status: 400, error: "object source requires body items to be JSON objects (key-value pairs)" };
195
- }
196
- bodyEntries.push(...Object.entries(item));
48
+ // READ a line range. Returns the raw selected lines (no `N:\t` prefix)
49
+ // plus the 1-indexed position of the first selected line. The render
50
+ // layer adds `N:\t` per plurnk.md ("READ output prefixes every line with
51
+ // line numbers, N:\t") starting from `startLine` keeps numbering as a
52
+ // presentation concern, prevents double-prefixing when the same content
53
+ // passes through the log render.
54
+ //
55
+ // Sentinel positions <0> and <-1> select no content (they're insertion
56
+ // points, not lines) → status 200 with empty text.
57
+ static lines(content, marker) {
58
+ const { lines } = Slicer.#splitLines(content);
59
+ const norm = Slicer.#normalize(marker, lines.length);
60
+ if ("error" in norm)
61
+ return { status: 416, error: norm.error };
62
+ if (norm.kind !== "range")
63
+ return { status: 200, text: "", startLine: undefined };
64
+ const selected = lines.slice(norm.start - 1, norm.end);
65
+ return { status: 200, text: selected.join("\n"), startLine: norm.start };
197
66
  }
198
- const entries = Object.entries(source);
199
- const total = entries.length;
200
- const { first, last } = marker;
201
- let result;
202
- if (last === null) {
203
- if (first === 0)
204
- result = [...bodyEntries, ...entries];
205
- else if (first === -1)
206
- result = [...entries, ...bodyEntries];
207
- else if (first > 0 && first <= total)
208
- result = [...entries.slice(0, first - 1), ...bodyEntries, ...entries.slice(first)];
209
- else
210
- return { status: 416, error: `position ${first} out of range (1..${total})` };
67
+ // Structural `<L>` slice for JSON sources (plurnk-grammar 0.13.0).
68
+ // "On structured entries, <L> addresses item index, not line number."
69
+ // Every JSON value becomes a list of top-level items:
70
+ // array `[a, b, c]` → items are the array elements
71
+ // object `{k1: v1, ...}` → items are key-value pairs (as single-key objects)
72
+ // scalar `"hello"` / 42 → item is the scalar itself (length-1 list)
73
+ // `<L>` indexes into that list (1-indexed). Result is always a JSON array.
74
+ // Sentinels `<0>` / `<-1>` are insertion points — empty `[]` for READ.
75
+ // Out-of-range positions return 416. Matches the uniform "always JSON
76
+ // array out" shape we settled for matcher results.
77
+ static #jsonValueToItems(parsed) {
78
+ if (Array.isArray(parsed))
79
+ return parsed;
80
+ if (parsed !== null && typeof parsed === "object") {
81
+ // Object items are single-key {key: value} wrappers, in insertion
82
+ // order. Object.entries preserves spec-guaranteed iteration order
83
+ // for string keys.
84
+ return Object.entries(parsed).map(([k, v]) => ({ [k]: v }));
85
+ }
86
+ // Scalar (string, number, boolean, null): a length-1 list of itself.
87
+ return [parsed];
211
88
  }
212
- else {
89
+ static jsonItems(content, marker) {
90
+ let parsed;
91
+ try {
92
+ parsed = JSON.parse(content);
93
+ }
94
+ catch (err) {
95
+ return { status: 400, error: `malformed JSON: ${err instanceof Error ? err.message : String(err)}` };
96
+ }
97
+ const items = Slicer.#jsonValueToItems(parsed);
98
+ const total = items.length;
99
+ const { first, last } = marker;
100
+ if (last === null) {
101
+ if (first === 0 || first === -1)
102
+ return { status: 200, body: "[]" };
103
+ if (first > 0 && first <= total)
104
+ return { status: 200, body: JSON.stringify([items[first - 1]], null, 2) };
105
+ return { status: 416, error: `item ${first} out of range (1..${total})` };
106
+ }
213
107
  let n = first;
214
108
  let m = last;
215
109
  if (n === 0)
216
110
  n = 1;
217
111
  if (m === -1)
218
112
  m = total;
219
- if (total === 0 && (first !== 1 || last !== -1))
220
- return { status: 416, error: `range on empty object` };
221
- if (n < 1 || (total > 0 && n > total))
113
+ if (n < 1 || n > total)
222
114
  return { status: 416, error: `range start ${first} out of range (1..${total})` };
223
- if (m < 1 || (total > 0 && m > total))
115
+ if (m < 1 || m > total)
224
116
  return { status: 416, error: `range end ${last} out of range (1..${total})` };
225
117
  if (n > m)
226
118
  return { status: 416, error: `range start ${first} > end ${last}` };
227
- result = [...entries.slice(0, n - 1), ...bodyEntries, ...entries.slice(m)];
228
- }
229
- return { status: 200, result: JSON.stringify(Object.fromEntries(result), null, 2) };
230
- };
231
- const applyJsonScalarEdit = (source, marker, items) => {
232
- // Scalar source is a length-1 list of itself. Only `<1>` replace
233
- // works cleanly; grow markers (<0>,<-1>) and ranges that imply
234
- // growth/delete would require type promotion (scalar → array),
235
- // which is the kind of implicit magic that bites later. Reject.
236
- const { first, last } = marker;
237
- if (last === null && first === 1) {
238
- if (items.length === 0)
239
- return { status: 200, result: "null" }; // delete the scalar
240
- if (items.length === 1)
241
- return { status: 200, result: JSON.stringify(items[0], null, 2) };
242
- return { status: 400, error: "scalar source: <1> body must produce 0 or 1 items (no implicit promotion to array)" };
119
+ return { status: 200, body: JSON.stringify(items.slice(n - 1, m), null, 2) };
243
120
  }
244
- if (last === -1 && first === 1) {
245
- // <1,-1> = whole content. Same constraints as <1> for scalars.
246
- if (items.length === 0)
247
- return { status: 200, result: "null" };
248
- if (items.length === 1)
249
- return { status: 200, result: JSON.stringify(items[0], null, 2) };
250
- return { status: 400, error: "scalar source: <1,-1> body must produce 0 or 1 items (no implicit promotion to array)" };
121
+ // Structural `<L>` EDIT for JSON sources (plurnk-grammar 0.13.0/0.14.0).
122
+ // Source-shape rules (matches jsonItems' item definition):
123
+ // array → items are elements
124
+ // object items are key-value pairs (single-key fragments)
125
+ // scalar length-1 list of itself; grow markers (<0>,<-1>) reject
126
+ //
127
+ // Body shape (Resolution B):
128
+ // body parses as JSON array → those are the items to splice in
129
+ // body parses as non-array JSON → single item to splice in
130
+ // empty body → delete the selection
131
+ // body fails JSON parse → 400 (path-extension declares intent; honor it)
132
+ //
133
+ // Marker semantics (parallel to line-EDIT):
134
+ // <N> replace item N with body item(s)
135
+ // <N,M> replace items N..M with body item(s)
136
+ // <0> prepend body item(s)
137
+ // <-1> append body item(s)
138
+ // <1,-1> replace whole top-level with body item(s); empty body clears
139
+ // Empty body on a sentinel insertion (<0> or <-1>) → no-op.
140
+ static #itemsFromBody(body) {
141
+ if (body === "")
142
+ return { items: [] }; // empty body = delete
143
+ let parsed;
144
+ try {
145
+ parsed = JSON.parse(body);
146
+ }
147
+ catch (err) {
148
+ return { error: `malformed JSON body: ${err instanceof Error ? err.message : String(err)}` };
149
+ }
150
+ if (Array.isArray(parsed))
151
+ return { items: parsed };
152
+ return { items: [parsed] };
251
153
  }
252
- return { status: 400, error: "scalar JSON source: only <1> or <1,-1> markers supported (no implicit promotion to array via grow markers)" };
253
- };
254
- export const applyJsonItemEdit = (content, marker, body) => {
255
- let parsed;
256
- try {
257
- parsed = JSON.parse(content);
154
+ static #applyJsonArrayEdit(source, marker, items) {
155
+ const total = source.length;
156
+ const { first, last } = marker;
157
+ let result;
158
+ if (last === null) {
159
+ if (first === 0)
160
+ result = [...items, ...source];
161
+ else if (first === -1)
162
+ result = [...source, ...items];
163
+ else if (first > 0 && first <= total)
164
+ result = [...source.slice(0, first - 1), ...items, ...source.slice(first)];
165
+ else
166
+ return { status: 416, error: `position ${first} out of range (1..${total})` };
167
+ }
168
+ else {
169
+ let n = first;
170
+ let m = last;
171
+ if (n === 0)
172
+ n = 1;
173
+ if (m === -1)
174
+ m = total;
175
+ if (total === 0 && (first !== 1 || last !== -1))
176
+ return { status: 416, error: `range on empty array` };
177
+ if (n < 1 || (total > 0 && n > total))
178
+ return { status: 416, error: `range start ${first} out of range (1..${total})` };
179
+ if (m < 1 || (total > 0 && m > total))
180
+ return { status: 416, error: `range end ${last} out of range (1..${total})` };
181
+ if (n > m)
182
+ return { status: 416, error: `range start ${first} > end ${last}` };
183
+ result = [...source.slice(0, n - 1), ...items, ...source.slice(m)];
184
+ }
185
+ return { status: 200, result: JSON.stringify(result, null, 2) };
258
186
  }
259
- catch (err) {
260
- return { status: 400, error: `malformed JSON source: ${err instanceof Error ? err.message : String(err)}` };
187
+ static #applyJsonObjectEdit(source, marker, items) {
188
+ // Object items are key-value pairs. Body items must be objects;
189
+ // each object's entries become kv-pairs to splice in. Items that
190
+ // aren't single objects → 400 (model used wrong body shape for an
191
+ // object source).
192
+ const bodyEntries = [];
193
+ for (const item of items) {
194
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
195
+ return { status: 400, error: "object source requires body items to be JSON objects (key-value pairs)" };
196
+ }
197
+ bodyEntries.push(...Object.entries(item));
198
+ }
199
+ const entries = Object.entries(source);
200
+ const total = entries.length;
201
+ const { first, last } = marker;
202
+ let result;
203
+ if (last === null) {
204
+ if (first === 0)
205
+ result = [...bodyEntries, ...entries];
206
+ else if (first === -1)
207
+ result = [...entries, ...bodyEntries];
208
+ else if (first > 0 && first <= total)
209
+ result = [...entries.slice(0, first - 1), ...bodyEntries, ...entries.slice(first)];
210
+ else
211
+ return { status: 416, error: `position ${first} out of range (1..${total})` };
212
+ }
213
+ else {
214
+ let n = first;
215
+ let m = last;
216
+ if (n === 0)
217
+ n = 1;
218
+ if (m === -1)
219
+ m = total;
220
+ if (total === 0 && (first !== 1 || last !== -1))
221
+ return { status: 416, error: `range on empty object` };
222
+ if (n < 1 || (total > 0 && n > total))
223
+ return { status: 416, error: `range start ${first} out of range (1..${total})` };
224
+ if (m < 1 || (total > 0 && m > total))
225
+ return { status: 416, error: `range end ${last} out of range (1..${total})` };
226
+ if (n > m)
227
+ return { status: 416, error: `range start ${first} > end ${last}` };
228
+ result = [...entries.slice(0, n - 1), ...bodyEntries, ...entries.slice(m)];
229
+ }
230
+ return { status: 200, result: JSON.stringify(Object.fromEntries(result), null, 2) };
261
231
  }
262
- const bodyResult = itemsFromBody(body);
263
- if ("error" in bodyResult)
264
- return { status: 400, error: bodyResult.error };
265
- const items = bodyResult.items;
266
- // Empty-body sentinel insertion is a no-op (model accidentally
267
- // emitted no items at an insertion point).
268
- if (items.length === 0 && marker.last === null && (marker.first === 0 || marker.first === -1)) {
269
- return { status: 200, result: content };
232
+ static #applyJsonScalarEdit(source, marker, items) {
233
+ // Scalar source is a length-1 list of itself. Only `<1>` replace
234
+ // works cleanly; grow markers (<0>,<-1>) and ranges that imply
235
+ // growth/delete would require type promotion (scalar → array),
236
+ // which is the kind of implicit magic that bites later. Reject.
237
+ const { first, last } = marker;
238
+ if (last === null && first === 1) {
239
+ if (items.length === 0)
240
+ return { status: 200, result: "null" }; // delete the scalar
241
+ if (items.length === 1)
242
+ return { status: 200, result: JSON.stringify(items[0], null, 2) };
243
+ return { status: 400, error: "scalar source: <1> body must produce 0 or 1 items (no implicit promotion to array)" };
244
+ }
245
+ if (last === -1 && first === 1) {
246
+ // <1,-1> = whole content. Same constraints as <1> for scalars.
247
+ if (items.length === 0)
248
+ return { status: 200, result: "null" };
249
+ if (items.length === 1)
250
+ return { status: 200, result: JSON.stringify(items[0], null, 2) };
251
+ return { status: 400, error: "scalar source: <1,-1> body must produce 0 or 1 items (no implicit promotion to array)" };
252
+ }
253
+ return { status: 400, error: "scalar JSON source: only <1> or <1,-1> markers supported (no implicit promotion to array via grow markers)" };
270
254
  }
271
- if (Array.isArray(parsed))
272
- return applyJsonArrayEdit(parsed, marker, items);
273
- if (parsed !== null && typeof parsed === "object")
274
- return applyJsonObjectEdit(parsed, marker, items);
275
- return applyJsonScalarEdit(parsed, marker, items);
276
- };
277
- // COPY-style raw line slice. Returns the selected lines verbatim (no line-
278
- // number prefix), trailing newline appended if any lines were selected.
279
- // Used for COPY/MOVE `<L>` per SPEC.md §16.9 (source range, symmetric
280
- // with READ but without the READ-output prefix that's a render concern,
281
- // not a data concern).
282
- export const sliceLinesRaw = (content, marker) => {
283
- const { lines } = splitLines(content);
284
- const norm = normalize(marker, lines.length);
285
- if ("error" in norm)
286
- return { status: 416, error: norm.error };
287
- if (norm.kind !== "range")
288
- return { status: 200, text: "" };
289
- const selected = lines.slice(norm.start - 1, norm.end);
290
- const result = selected.length > 0 ? `${selected.join("\n")}\n` : "";
291
- return { status: 200, text: result };
292
- };
293
- // EDIT applies body at the marker position:
294
- // <0> prepend body before line 1
295
- // <-1> append body after the last line
296
- // <N> replace line N with body
297
- // <N,M> replace lines N..M with body
298
- // <1,-1> whole content (replace everything); empty body clears.
299
- // Empty body with <N>/<N,M> deletes those lines.
300
- export const applyLineMarkerEdit = (content, marker, body) => {
301
- const { lines, trailingNewline } = splitLines(content);
302
- const norm = normalize(marker, lines.length);
303
- if ("error" in norm)
304
- return { status: 416, error: norm.error };
305
- const bodyLines = splitLines(body).lines;
306
- let newLines;
307
- if (norm.kind === "before-first") {
308
- newLines = [...bodyLines, ...lines];
255
+ static jsonItemEdit(content, marker, body) {
256
+ let parsed;
257
+ try {
258
+ parsed = JSON.parse(content);
259
+ }
260
+ catch (err) {
261
+ return { status: 400, error: `malformed JSON source: ${err instanceof Error ? err.message : String(err)}` };
262
+ }
263
+ const bodyResult = Slicer.#itemsFromBody(body);
264
+ if ("error" in bodyResult)
265
+ return { status: 400, error: bodyResult.error };
266
+ const items = bodyResult.items;
267
+ // Empty-body sentinel insertion is a no-op (model accidentally
268
+ // emitted no items at an insertion point).
269
+ if (items.length === 0 && marker.last === null && (marker.first === 0 || marker.first === -1)) {
270
+ return { status: 200, result: content };
271
+ }
272
+ if (Array.isArray(parsed))
273
+ return Slicer.#applyJsonArrayEdit(parsed, marker, items);
274
+ if (parsed !== null && typeof parsed === "object")
275
+ return Slicer.#applyJsonObjectEdit(parsed, marker, items);
276
+ return Slicer.#applyJsonScalarEdit(parsed, marker, items);
309
277
  }
310
- else if (norm.kind === "after-last") {
311
- newLines = [...lines, ...bodyLines];
278
+ // COPY-style raw line slice. Returns the selected lines verbatim (no line-
279
+ // number prefix), trailing newline appended if any lines were selected.
280
+ // Used for COPY/MOVE `<L>` per SPEC.md §16.9 (source range, symmetric
281
+ // with READ but without the READ-output prefix that's a render concern,
282
+ // not a data concern).
283
+ static linesRaw(content, marker) {
284
+ const { lines } = Slicer.#splitLines(content);
285
+ const norm = Slicer.#normalize(marker, lines.length);
286
+ if ("error" in norm)
287
+ return { status: 416, error: norm.error };
288
+ if (norm.kind !== "range")
289
+ return { status: 200, text: "" };
290
+ const selected = lines.slice(norm.start - 1, norm.end);
291
+ const result = selected.length > 0 ? `${selected.join("\n")}\n` : "";
292
+ return { status: 200, text: result };
312
293
  }
313
- else {
314
- newLines = [...lines.slice(0, norm.start - 1), ...bodyLines, ...lines.slice(norm.end)];
294
+ // EDIT applies body at the marker position:
295
+ // <0> prepend body before line 1
296
+ // <-1> append body after the last line
297
+ // <N> replace line N with body
298
+ // <N,M> replace lines N..M with body
299
+ // <1,-1> whole content (replace everything); empty body clears.
300
+ // Empty body with <N>/<N,M> deletes those lines.
301
+ static lineMarkerEdit(content, marker, body) {
302
+ const { lines, trailingNewline } = Slicer.#splitLines(content);
303
+ const norm = Slicer.#normalize(marker, lines.length);
304
+ if ("error" in norm)
305
+ return { status: 416, error: norm.error };
306
+ const bodyLines = Slicer.#splitLines(body).lines;
307
+ let newLines;
308
+ if (norm.kind === "before-first") {
309
+ newLines = [...bodyLines, ...lines];
310
+ }
311
+ else if (norm.kind === "after-last") {
312
+ newLines = [...lines, ...bodyLines];
313
+ }
314
+ else {
315
+ newLines = [...lines.slice(0, norm.start - 1), ...bodyLines, ...lines.slice(norm.end)];
316
+ }
317
+ let result = newLines.join("\n");
318
+ if (newLines.length > 0 && trailingNewline)
319
+ result += "\n";
320
+ return { status: 200, result };
315
321
  }
316
- let result = newLines.join("\n");
317
- if (newLines.length > 0 && trailingNewline)
318
- result += "\n";
319
- return { status: 200, result };
320
- };
322
+ }
321
323
  //# sourceMappingURL=line-marker.js.map