hadara 0.3.2 → 0.3.3-rc.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.
Files changed (51) hide show
  1. package/README.md +38 -23
  2. package/dist/cli/context.js +110 -0
  3. package/dist/cli/help.js +6 -7
  4. package/dist/cli/init.js +56 -58
  5. package/dist/cli/main.js +12 -0
  6. package/dist/cli/session.js +37 -0
  7. package/dist/cli/task.js +52 -0
  8. package/dist/context/code-graph-extractor.js +123 -0
  9. package/dist/context/code-index.js +1154 -0
  10. package/dist/context/context-cache-store.js +925 -0
  11. package/dist/context/context-graph-builder.js +341 -0
  12. package/dist/context/context-graph.js +42 -0
  13. package/dist/context/context-pack.js +457 -0
  14. package/dist/context/context-slice-boundary.js +37 -0
  15. package/dist/context/context-slice.js +487 -0
  16. package/dist/context/document-extractors.js +343 -0
  17. package/dist/context/evidence-extractors.js +179 -0
  18. package/dist/context/extractor-contract.js +166 -0
  19. package/dist/context/registry-extractors.js +177 -0
  20. package/dist/context/release-extractors.js +175 -0
  21. package/dist/context/session-start.js +297 -0
  22. package/dist/context/source-manifest.js +566 -0
  23. package/dist/context/state-projection.js +196 -0
  24. package/dist/context/task-extractors.js +168 -0
  25. package/dist/core/schema.js +26 -0
  26. package/dist/harness/validate.js +7 -8
  27. package/dist/schemas/code-index.schema.json +173 -0
  28. package/dist/schemas/context-cache-record.schema.json +56 -0
  29. package/dist/schemas/context-cache-status.schema.json +167 -0
  30. package/dist/schemas/context-cache-warm.schema.json +222 -0
  31. package/dist/schemas/context-graph.schema.json +286 -0
  32. package/dist/schemas/context-pack.schema.json +246 -0
  33. package/dist/schemas/context-slice.schema.json +94 -0
  34. package/dist/schemas/context-source-manifest.schema.json +147 -0
  35. package/dist/schemas/schema-index.json +91 -0
  36. package/dist/schemas/session-start.schema.json +158 -0
  37. package/dist/schemas/task-close-repair-plan.schema.json +67 -0
  38. package/dist/schemas/task-context.schema.json +125 -0
  39. package/dist/schemas/task-finalize.schema.json +98 -0
  40. package/dist/schemas/task-lifecycle.schema.json +84 -0
  41. package/dist/services/capability-registry.js +266 -9
  42. package/dist/services/lifecycle-guide.js +23 -29
  43. package/dist/services/protocol-consistency.js +6 -8
  44. package/dist/task/acceptance.js +171 -0
  45. package/dist/task/task-close-repair-plan.js +190 -0
  46. package/dist/task/task-close.js +34 -35
  47. package/dist/task/task-finalize.js +377 -0
  48. package/dist/task/task-lifecycle.js +210 -0
  49. package/dist/task/task-next.js +10 -1
  50. package/dist/task/task-ready.js +4 -0
  51. package/package.json +1 -1
@@ -0,0 +1,487 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CONTEXT_SLICE_COMMAND = exports.CONTEXT_SLICE_SCHEMA_ID = void 0;
7
+ exports.buildContextSliceReport = buildContextSliceReport;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const context_pack_1 = require("./context-pack");
11
+ const code_index_1 = require("./code-index");
12
+ const extractor_contract_1 = require("./extractor-contract");
13
+ const context_slice_boundary_1 = require("./context-slice-boundary");
14
+ const managed_sections_1 = require("../services/managed-sections");
15
+ exports.CONTEXT_SLICE_SCHEMA_ID = 'hadara.contextSlice.v1';
16
+ exports.CONTEXT_SLICE_COMMAND = 'context.slice';
17
+ const MAX_FILE_BYTES = 2 * 1024 * 1024;
18
+ const MAX_RANGE_LINES = 300;
19
+ const MAX_TAIL_LINES = 500;
20
+ const MAX_KEYWORD_WINDOWS = 3;
21
+ const MAX_SLICE_BYTES = 512 * 1024;
22
+ function buildContextSliceReport(input) {
23
+ const generatedAt = input.generatedAt ?? new Date().toISOString();
24
+ const requestedStrategy = inferStrategy(input);
25
+ const issues = [];
26
+ const resolvedInput = requestedStrategy === 'context-candidate'
27
+ ? resolveContextCandidateInput(input, generatedAt, issues)
28
+ : input;
29
+ const effectiveStrategy = resolvedInput ? inferStrategy(resolvedInput) : requestedStrategy;
30
+ const pathResult = resolvedInput
31
+ ? resolveContextSlicePath(input.projectRoot, resolvedInput.path)
32
+ : { path: '', absolutePath: '' };
33
+ if (pathResult.issue)
34
+ issues.push(pathResult.issue);
35
+ const readResult = !resolvedInput || pathResult.issue ? null : readContextSliceSource(pathResult.path, pathResult.absolutePath);
36
+ if (readResult?.issue)
37
+ issues.push(readResult.issue);
38
+ const source = readResult && !readResult.issue ? readResult.source : null;
39
+ let slices = [];
40
+ if (source && issues.every((issue) => issue.severity !== 'error')) {
41
+ slices = buildSlicesForStrategy(source, effectiveStrategy, resolvedInput ?? input, issues);
42
+ }
43
+ const { slices: boundedSlices, summary } = enforceSlicePayloadBudget(slices, issues);
44
+ return {
45
+ schemaVersion: exports.CONTEXT_SLICE_SCHEMA_ID,
46
+ command: exports.CONTEXT_SLICE_COMMAND,
47
+ ok: issues.every((issue) => issue.severity !== 'error'),
48
+ generatedAt,
49
+ projectRoot: input.projectRoot,
50
+ path: source?.path ?? pathResult.path,
51
+ sourceHash: source?.sourceHash ?? 'sha256:unavailable',
52
+ lineCount: source?.lines.length ?? 0,
53
+ strategy: requestedStrategy,
54
+ slices: boundedSlices,
55
+ summary,
56
+ issues
57
+ };
58
+ }
59
+ function inferStrategy(input) {
60
+ if (input.candidateId !== undefined || input.taskId !== undefined)
61
+ return 'context-candidate';
62
+ if (input.symbol !== undefined)
63
+ return 'symbol-neighborhood';
64
+ if (input.keyword !== undefined)
65
+ return 'keyword-window';
66
+ if (input.tail !== undefined)
67
+ return 'tail-window';
68
+ if (input.managedSection !== undefined)
69
+ return 'managed-section';
70
+ if (input.from !== undefined || input.to !== undefined)
71
+ return 'explicit-range';
72
+ return 'explicit-range';
73
+ }
74
+ function resolveContextSlicePath(projectRoot, inputPath) {
75
+ const normalized = (0, context_slice_boundary_1.normalizeContextSliceInputPath)(inputPath);
76
+ const root = node_path_1.default.resolve(projectRoot);
77
+ const absolutePath = normalized ? node_path_1.default.resolve(root, normalized) : root;
78
+ if (!normalized || node_path_1.default.isAbsolute(inputPath ?? '') || normalized.split('/').includes('..') || absolutePath !== root && !absolutePath.startsWith(`${root}${node_path_1.default.sep}`)) {
79
+ return {
80
+ path: normalized,
81
+ absolutePath,
82
+ issue: {
83
+ severity: 'error',
84
+ code: 'CONTEXT_SLICE_OUTSIDE_PROJECT',
85
+ path: normalized,
86
+ message: `${inputPath ?? ''} is not a project-relative path inside the project root.`,
87
+ fixHint: 'Pass --path with a project-relative file path.'
88
+ }
89
+ };
90
+ }
91
+ if ((0, context_slice_boundary_1.isDeniedContextSlicePath)(normalized)) {
92
+ return {
93
+ path: normalized,
94
+ absolutePath,
95
+ issue: {
96
+ severity: 'error',
97
+ code: 'CONTEXT_SLICE_OUTSIDE_PROJECT',
98
+ path: normalized,
99
+ message: `${normalized} is outside the supported public project read boundary.`,
100
+ fixHint: 'Choose a tracked project file, task file, or public docs file.'
101
+ }
102
+ };
103
+ }
104
+ return { path: normalized, absolutePath };
105
+ }
106
+ function readContextSliceSource(filePath, absolutePath) {
107
+ if (!node_fs_1.default.existsSync(absolutePath) || !node_fs_1.default.statSync(absolutePath).isFile()) {
108
+ return {
109
+ issue: {
110
+ severity: 'error',
111
+ code: 'CONTEXT_SLICE_FILE_NOT_FOUND',
112
+ path: filePath,
113
+ message: `Context slice source file not found: ${filePath}`
114
+ }
115
+ };
116
+ }
117
+ const stat = node_fs_1.default.statSync(absolutePath);
118
+ if (stat.size > MAX_FILE_BYTES) {
119
+ return {
120
+ issue: {
121
+ severity: 'error',
122
+ code: 'CONTEXT_SLICE_TOO_LARGE',
123
+ path: filePath,
124
+ message: `${filePath} is ${stat.size} bytes, above the ${MAX_FILE_BYTES} byte context-slice file budget.`,
125
+ fixHint: 'Use a smaller source file or add a narrower indexed slice surface.'
126
+ }
127
+ };
128
+ }
129
+ const buffer = node_fs_1.default.readFileSync(absolutePath);
130
+ if (buffer.includes(0)) {
131
+ return {
132
+ issue: {
133
+ severity: 'error',
134
+ code: 'CONTEXT_SLICE_BINARY_FILE',
135
+ path: filePath,
136
+ message: `Context slice refused binary-looking file: ${filePath}`
137
+ }
138
+ };
139
+ }
140
+ const text = buffer.toString('utf8');
141
+ return {
142
+ source: {
143
+ path: filePath,
144
+ absolutePath,
145
+ text,
146
+ sourceHash: (0, extractor_contract_1.hashContextGraphText)(text),
147
+ lines: splitLinesPreserve(text)
148
+ }
149
+ };
150
+ }
151
+ function buildSlicesForStrategy(source, strategy, input, issues) {
152
+ if (strategy === 'explicit-range')
153
+ return explicitRangeSlices(source, input, issues);
154
+ if (strategy === 'symbol-neighborhood')
155
+ return symbolNeighborhoodSlices(source, input, issues);
156
+ if (strategy === 'tail-window')
157
+ return tailSlices(source, input, issues);
158
+ if (strategy === 'keyword-window')
159
+ return keywordSlices(source, input, issues);
160
+ if (strategy === 'managed-section')
161
+ return managedSectionSlices(source, input, issues);
162
+ issues.push({
163
+ severity: 'error',
164
+ code: 'CONTEXT_SLICE_UNSUPPORTED_STRATEGY',
165
+ path: source.path,
166
+ message: `Context slice strategy is not implemented in this C4 core: ${strategy}`
167
+ });
168
+ return [];
169
+ }
170
+ function explicitRangeSlices(source, input, issues) {
171
+ if (input.from === undefined || input.to === undefined || input.from < 1 || input.to < input.from || input.from > source.lines.length) {
172
+ issues.push({
173
+ severity: 'error',
174
+ code: 'CONTEXT_SLICE_RANGE_INVALID',
175
+ path: source.path,
176
+ message: `Invalid explicit range for ${source.path}: from=${input.from ?? 'missing'} to=${input.to ?? 'missing'}`
177
+ });
178
+ return [];
179
+ }
180
+ let endLine = input.to;
181
+ if (endLine > source.lines.length) {
182
+ endLine = source.lines.length;
183
+ issues.push({
184
+ severity: 'warning',
185
+ code: 'CONTEXT_SLICE_RANGE_CLAMPED',
186
+ path: source.path,
187
+ message: `Range end was clamped to file line count ${source.lines.length}.`
188
+ });
189
+ }
190
+ const range = clampRangeLineBudget(source.path, { startLine: input.from, endLine }, MAX_RANGE_LINES, issues);
191
+ return [createSlice(source, 'explicit-range', range, 'Explicit line range requested by caller.')];
192
+ }
193
+ function symbolNeighborhoodSlices(source, input, issues) {
194
+ const symbol = input.symbol ?? '';
195
+ if (!symbol) {
196
+ issues.push({
197
+ severity: 'error',
198
+ code: 'CONTEXT_SLICE_RANGE_INVALID',
199
+ path: source.path,
200
+ message: 'Symbol slice requires --symbol <name>.'
201
+ });
202
+ return [];
203
+ }
204
+ const references = (0, code_index_1.extractCodeFileReferences)({
205
+ projectRoot: input.projectRoot,
206
+ path: source.path,
207
+ content: source.text
208
+ });
209
+ issues.push(...references.issues.map((issue) => ({
210
+ severity: issue.severity,
211
+ code: 'CONTEXT_SLICE_DEGRADED',
212
+ path: issue.path ?? source.path,
213
+ message: issue.message,
214
+ ...(issue.fixHint ? { fixHint: issue.fixHint } : {})
215
+ })));
216
+ const match = references.exports.find((candidate) => candidate.name === symbol);
217
+ if (!match) {
218
+ issues.push({
219
+ severity: 'error',
220
+ code: 'CONTEXT_SLICE_SYMBOL_NOT_FOUND',
221
+ path: source.path,
222
+ message: `Symbol not found in ${source.path}: ${symbol}`,
223
+ fixHint: 'Use a symbol exported by the target file or pass an explicit --from/--to range.'
224
+ });
225
+ return [];
226
+ }
227
+ const window = Math.max(0, input.window ?? 40);
228
+ const range = clampRangeLineBudget(source.path, {
229
+ startLine: Math.max(1, match.line - window),
230
+ endLine: Math.min(source.lines.length, match.line + window)
231
+ }, MAX_RANGE_LINES, issues);
232
+ return [createSlice(source, 'symbol-neighborhood', range, `Symbol neighborhood for ${symbol}.`, 'derived')];
233
+ }
234
+ function tailSlices(source, input, issues) {
235
+ const requested = input.tail ?? 0;
236
+ if (requested < 1) {
237
+ issues.push({
238
+ severity: 'error',
239
+ code: 'CONTEXT_SLICE_RANGE_INVALID',
240
+ path: source.path,
241
+ message: `Invalid tail line count for ${source.path}: ${requested}`
242
+ });
243
+ return [];
244
+ }
245
+ const lineBudget = Math.min(requested, MAX_TAIL_LINES);
246
+ if (requested > MAX_TAIL_LINES) {
247
+ issues.push({
248
+ severity: 'warning',
249
+ code: 'CONTEXT_SLICE_RANGE_CLAMPED',
250
+ path: source.path,
251
+ message: `Tail window was clamped from ${requested} to ${MAX_TAIL_LINES} lines.`
252
+ });
253
+ }
254
+ const startLine = Math.max(1, source.lines.length - lineBudget + 1);
255
+ return [createSlice(source, 'tail-window', { startLine, endLine: source.lines.length }, `Last ${lineBudget} lines requested by caller.`)];
256
+ }
257
+ function keywordSlices(source, input, issues) {
258
+ const keyword = input.keyword ?? '';
259
+ const window = Math.max(0, input.window ?? 40);
260
+ if (!keyword) {
261
+ issues.push({
262
+ severity: 'error',
263
+ code: 'CONTEXT_SLICE_RANGE_INVALID',
264
+ path: source.path,
265
+ message: 'Keyword slice requires --keyword <text>.'
266
+ });
267
+ return [];
268
+ }
269
+ const matches = [];
270
+ for (let index = 0; index < source.lines.length; index += 1) {
271
+ if (source.lines[index].includes(keyword))
272
+ matches.push(index + 1);
273
+ }
274
+ if (matches.length === 0) {
275
+ issues.push({
276
+ severity: 'warning',
277
+ code: 'CONTEXT_SLICE_KEYWORD_NOT_FOUND',
278
+ path: source.path,
279
+ message: `Keyword not found in ${source.path}: ${keyword}`
280
+ });
281
+ return [];
282
+ }
283
+ const selectedMatches = matches.slice(0, MAX_KEYWORD_WINDOWS);
284
+ if (matches.length > selectedMatches.length) {
285
+ issues.push({
286
+ severity: 'warning',
287
+ code: 'CONTEXT_SLICE_TOO_LARGE',
288
+ path: source.path,
289
+ message: `Keyword matched ${matches.length} lines; returned the first ${MAX_KEYWORD_WINDOWS} bounded windows.`
290
+ });
291
+ }
292
+ const mergedRanges = mergeRanges(selectedMatches.map((line) => ({
293
+ startLine: Math.max(1, line - window),
294
+ endLine: Math.min(source.lines.length, line + window)
295
+ })));
296
+ return mergedRanges.map((range, index) => createSlice(source, 'keyword-window', range, `Keyword window ${index + 1} for ${JSON.stringify(keyword)}.`));
297
+ }
298
+ function managedSectionSlices(source, input, issues) {
299
+ const sectionId = input.managedSection ?? '';
300
+ if (!sectionId) {
301
+ issues.push({
302
+ severity: 'error',
303
+ code: 'CONTEXT_SLICE_RANGE_INVALID',
304
+ path: source.path,
305
+ message: 'Managed section slice requires --managed-section <section-id>.'
306
+ });
307
+ return [];
308
+ }
309
+ const parsed = (0, managed_sections_1.parseManagedSections)(source.text, source.path);
310
+ issues.push(...parsed.issues.map((issue) => ({
311
+ severity: issue.severity,
312
+ code: 'CONTEXT_SLICE_DEGRADED',
313
+ path: source.path,
314
+ message: issue.message
315
+ })));
316
+ const section = parsed.sections.find((candidate) => candidate.id === sectionId);
317
+ if (!section) {
318
+ issues.push({
319
+ severity: 'error',
320
+ code: 'CONTEXT_SLICE_DEGRADED',
321
+ path: source.path,
322
+ message: `Managed section not found in ${source.path}: ${sectionId}`,
323
+ fixHint: 'Run hadara docs managed list --json to discover available section ids.'
324
+ });
325
+ return [];
326
+ }
327
+ return [createSlice(source, 'managed-section', { startLine: section.startLine, endLine: section.endLine }, `Managed section ${sectionId} requested by caller.`)];
328
+ }
329
+ function createSlice(source, strategy, range, reason, confidence = 'explicit') {
330
+ return {
331
+ id: `${source.path}:${range.startLine}-${range.endLine}`,
332
+ path: source.path,
333
+ strategy,
334
+ startLine: range.startLine,
335
+ endLine: range.endLine,
336
+ text: source.lines.slice(range.startLine - 1, range.endLine).join(''),
337
+ sourceHash: source.sourceHash,
338
+ reason,
339
+ confidence
340
+ };
341
+ }
342
+ function resolveContextCandidateInput(input, generatedAt, issues) {
343
+ if (!input.taskId || !input.candidateId) {
344
+ issues.push({
345
+ severity: 'error',
346
+ code: 'CONTEXT_SLICE_CANDIDATE_NOT_FOUND',
347
+ message: 'Context candidate slicing requires --task <task-id> and --candidate <candidate-id>.',
348
+ fixHint: 'Run hadara context pack --task <task-id> --json and pass one returned sliceCandidates[].id.'
349
+ });
350
+ return null;
351
+ }
352
+ const pack = input.contextPackReport ?? (0, context_pack_1.buildContextPackReport)({
353
+ projectRoot: input.projectRoot,
354
+ generatedAt,
355
+ taskId: input.taskId,
356
+ includeCode: input.includeCode
357
+ });
358
+ for (const issue of pack.issues) {
359
+ if (issue.severity === 'error') {
360
+ issues.push({
361
+ severity: 'error',
362
+ code: 'CONTEXT_SLICE_DEGRADED',
363
+ path: issue.path,
364
+ message: `Context pack could not resolve candidate ${input.candidateId}: ${issue.message}`,
365
+ ...(issue.fixHint ? { fixHint: issue.fixHint } : {})
366
+ });
367
+ }
368
+ }
369
+ if (issues.some((issue) => issue.severity === 'error'))
370
+ return null;
371
+ const candidate = pack.sliceCandidates.find((item) => item.id === input.candidateId);
372
+ if (!candidate) {
373
+ issues.push({
374
+ severity: 'error',
375
+ code: 'CONTEXT_SLICE_CANDIDATE_NOT_FOUND',
376
+ message: `Context slice candidate not found for task ${input.taskId}: ${input.candidateId}`,
377
+ fixHint: 'Refresh the context pack for the same task/options and pass an exact sliceCandidates[].id.'
378
+ });
379
+ return null;
380
+ }
381
+ return inputFromCandidate(input, candidate);
382
+ }
383
+ function inputFromCandidate(input, candidate) {
384
+ const base = {
385
+ projectRoot: input.projectRoot,
386
+ generatedAt: input.generatedAt,
387
+ path: candidate.path,
388
+ window: input.window
389
+ };
390
+ if (candidate.strategy === 'symbol-neighborhood') {
391
+ return {
392
+ ...base,
393
+ symbol: candidate.symbol ?? symbolFromCandidateId(candidate.id)
394
+ };
395
+ }
396
+ if (candidate.strategy === 'managed-section') {
397
+ return {
398
+ ...base,
399
+ managedSection: candidate.managedSection ?? sectionFromCandidateId(candidate.id)
400
+ };
401
+ }
402
+ if (candidate.strategy === 'explicit-range') {
403
+ const startLine = candidate.lineStart ?? 1;
404
+ return {
405
+ ...base,
406
+ from: startLine,
407
+ to: candidate.lineEnd ?? Math.max(startLine, startLine + 80)
408
+ };
409
+ }
410
+ return base;
411
+ }
412
+ function symbolFromCandidateId(candidateId) {
413
+ const marker = '#';
414
+ const index = candidateId.lastIndexOf(marker);
415
+ return index >= 0 ? candidateId.slice(index + marker.length) : undefined;
416
+ }
417
+ function sectionFromCandidateId(candidateId) {
418
+ return symbolFromCandidateId(candidateId);
419
+ }
420
+ function clampRangeLineBudget(filePath, range, maxLines, issues) {
421
+ const lineCount = range.endLine - range.startLine + 1;
422
+ if (lineCount <= maxLines)
423
+ return range;
424
+ issues.push({
425
+ severity: 'warning',
426
+ code: 'CONTEXT_SLICE_TOO_LARGE',
427
+ path: filePath,
428
+ message: `Context slice range was truncated from ${lineCount} to ${maxLines} lines.`
429
+ });
430
+ return { startLine: range.startLine, endLine: range.startLine + maxLines - 1 };
431
+ }
432
+ function enforceSlicePayloadBudget(slices, issues) {
433
+ let totalBytes = 0;
434
+ let totalLines = 0;
435
+ for (const slice of slices) {
436
+ totalBytes += Buffer.byteLength(slice.text, 'utf8');
437
+ totalLines += slice.endLine - slice.startLine + 1;
438
+ }
439
+ if (totalBytes > MAX_SLICE_BYTES) {
440
+ issues.push({
441
+ severity: 'error',
442
+ code: 'CONTEXT_SLICE_TOO_LARGE',
443
+ message: `Requested slice payload is ${totalBytes} bytes, above the ${MAX_SLICE_BYTES} byte budget.`,
444
+ fixHint: 'Use narrower --from/--to, --tail, or --window options.'
445
+ });
446
+ return {
447
+ slices: [],
448
+ summary: {
449
+ sliceCount: 0,
450
+ totalLines: 0,
451
+ totalBytes: 0,
452
+ truncated: true
453
+ }
454
+ };
455
+ }
456
+ return {
457
+ slices,
458
+ summary: {
459
+ sliceCount: slices.length,
460
+ totalLines,
461
+ totalBytes,
462
+ truncated: issues.some((issue) => issue.code === 'CONTEXT_SLICE_RANGE_CLAMPED' || issue.code === 'CONTEXT_SLICE_TOO_LARGE')
463
+ }
464
+ };
465
+ }
466
+ function mergeRanges(ranges) {
467
+ const sorted = [...ranges].sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
468
+ const merged = [];
469
+ for (const range of sorted) {
470
+ const previous = merged[merged.length - 1];
471
+ if (!previous || range.startLine > previous.endLine + 1) {
472
+ merged.push({ ...range });
473
+ }
474
+ else {
475
+ previous.endLine = Math.max(previous.endLine, range.endLine);
476
+ }
477
+ }
478
+ return merged;
479
+ }
480
+ function splitLinesPreserve(text) {
481
+ if (!text)
482
+ return [];
483
+ const lines = text.match(/.*(?:\r?\n|$)/g) ?? [];
484
+ if (lines[lines.length - 1] === '')
485
+ lines.pop();
486
+ return lines;
487
+ }