@molecule/api-ai-tools 1.9.6 → 1.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -9
- package/dist/command-position.d.ts +36 -0
- package/dist/command-position.d.ts.map +1 -1
- package/dist/command-position.js +192 -19
- package/dist/cut-run.d.ts +8 -0
- package/dist/cut-run.d.ts.map +1 -1
- package/dist/cut-run.js +20 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/line-diff.d.ts +34 -0
- package/dist/line-diff.d.ts.map +1 -0
- package/dist/line-diff.js +150 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +70 -22
- package/dist/trace-artifacts.d.ts +57 -1
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +381 -43
- package/dist/utilities.d.ts +55 -5
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +150 -15
- package/package.json +1 -1
package/dist/command-position.js
CHANGED
|
@@ -541,6 +541,26 @@ function splitSegments(command) {
|
|
|
541
541
|
let buffer = '';
|
|
542
542
|
/** The offset in the command of the current buffer's first character. */
|
|
543
543
|
let bufferStart = 0;
|
|
544
|
+
/** How many `( … )` / `{ … }` groups are open at the current position. */
|
|
545
|
+
let groupDepth = 0;
|
|
546
|
+
/** The depth each character of `buffer` was written at, aligned with it (-1 when quoted). */
|
|
547
|
+
let depths = [];
|
|
548
|
+
/**
|
|
549
|
+
* Append shell text to the statement being read, recording the depth it was written at.
|
|
550
|
+
* Text inside a quote or a command substitution is recorded as -1: a `>` there is a literal
|
|
551
|
+
* character or a nested command's business, never the outer command's redirect.
|
|
552
|
+
*/
|
|
553
|
+
const put = (text, depth = groupDepth) => {
|
|
554
|
+
for (let n = 0; n < text.length; n++)
|
|
555
|
+
depths.push(depth);
|
|
556
|
+
buffer += text;
|
|
557
|
+
};
|
|
558
|
+
/** Close the statement being read at `after`. */
|
|
559
|
+
const end = (after) => {
|
|
560
|
+
parts.push({ text: buffer, start: bufferStart, after, depths });
|
|
561
|
+
buffer = '';
|
|
562
|
+
depths = [];
|
|
563
|
+
};
|
|
544
564
|
/** The nestings this position sits inside, outermost first. */
|
|
545
565
|
const stack = [];
|
|
546
566
|
for (let i = 0; i < source.length; i++) {
|
|
@@ -550,7 +570,7 @@ function splitSegments(command) {
|
|
|
550
570
|
// A single-quoted string expands nothing at all: no escape, no substitution, and no quote
|
|
551
571
|
// but its own ends it.
|
|
552
572
|
if (top === 'single') {
|
|
553
|
-
|
|
573
|
+
put(char, -1);
|
|
554
574
|
if (char === "'")
|
|
555
575
|
stack.pop();
|
|
556
576
|
continue;
|
|
@@ -559,7 +579,7 @@ function splitSegments(command) {
|
|
|
559
579
|
// inside single quotes (handled above). The `(` is consumed here so the depth is counted once.
|
|
560
580
|
if (char === '$' && next === '(') {
|
|
561
581
|
stack.push('sub');
|
|
562
|
-
|
|
582
|
+
put('$(', -1);
|
|
563
583
|
i += 1;
|
|
564
584
|
continue;
|
|
565
585
|
}
|
|
@@ -569,21 +589,21 @@ function splitSegments(command) {
|
|
|
569
589
|
if (char === '"')
|
|
570
590
|
stack.pop();
|
|
571
591
|
else if (char === '\\' && next !== undefined) {
|
|
572
|
-
|
|
592
|
+
put(char + next, -1);
|
|
573
593
|
i += 1;
|
|
574
594
|
continue;
|
|
575
595
|
}
|
|
576
|
-
|
|
596
|
+
put(char, -1);
|
|
577
597
|
continue;
|
|
578
598
|
}
|
|
579
599
|
if (char === '"') {
|
|
580
600
|
stack.push('double');
|
|
581
|
-
|
|
601
|
+
put(char, -1);
|
|
582
602
|
continue;
|
|
583
603
|
}
|
|
584
604
|
if (char === "'") {
|
|
585
605
|
stack.push('single');
|
|
586
|
-
|
|
606
|
+
put(char, -1);
|
|
587
607
|
continue;
|
|
588
608
|
}
|
|
589
609
|
// Inside a substitution or a subshell of one: its own parentheses nest, its `|`/`;`/`&&`
|
|
@@ -593,41 +613,53 @@ function splitSegments(command) {
|
|
|
593
613
|
stack.push('sub');
|
|
594
614
|
else if (char === ')')
|
|
595
615
|
stack.pop();
|
|
596
|
-
|
|
616
|
+
put(char, -1);
|
|
597
617
|
continue;
|
|
598
618
|
}
|
|
599
619
|
if (char === '\\' && next !== undefined) {
|
|
600
|
-
|
|
620
|
+
// An escaped character is a literal (`\>` writes no file).
|
|
621
|
+
put(char + next, -1);
|
|
601
622
|
i += 1;
|
|
602
623
|
continue;
|
|
603
624
|
}
|
|
625
|
+
// Grouping belongs to the shell, not to a word: a statement written inside `( … )` or
|
|
626
|
+
// `{ … }` writes through that group, so the depth travels with every character a caller
|
|
627
|
+
// reading a redirect may ask about.
|
|
628
|
+
if (char === '(' || char === '{') {
|
|
629
|
+
put(char);
|
|
630
|
+
groupDepth += 1;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (char === ')' || char === '}') {
|
|
634
|
+
groupDepth = Math.max(0, groupDepth - 1);
|
|
635
|
+
put(char);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
604
638
|
if ((char === '&' && next === '&') || (char === '|' && next === '|')) {
|
|
605
|
-
|
|
606
|
-
buffer = '';
|
|
639
|
+
end(char + next);
|
|
607
640
|
i += 1;
|
|
608
641
|
bufferStart = i + 1;
|
|
609
642
|
continue;
|
|
610
643
|
}
|
|
611
644
|
if (char === ';' || char === '|' || char === '\n') {
|
|
612
|
-
|
|
613
|
-
buffer = '';
|
|
645
|
+
end(char);
|
|
614
646
|
bufferStart = i + 1;
|
|
615
647
|
continue;
|
|
616
648
|
}
|
|
617
649
|
if (char === '&' && source[i - 1] !== '>' && next !== '>') {
|
|
618
|
-
|
|
619
|
-
buffer = '';
|
|
650
|
+
end('&');
|
|
620
651
|
bufferStart = i + 1;
|
|
621
652
|
continue;
|
|
622
653
|
}
|
|
623
|
-
|
|
654
|
+
put(char);
|
|
624
655
|
}
|
|
625
|
-
|
|
656
|
+
end('');
|
|
626
657
|
const out = [];
|
|
627
658
|
// The separator of the last part, kept across the empty ones so a statement's own separator
|
|
628
659
|
// survives the blank lines around it (`a &&\n b` and a heredoc's blanked body alike).
|
|
629
660
|
let lastAfter = null;
|
|
630
661
|
for (const part of parts) {
|
|
662
|
+
const trimStart = part.text.length - part.text.trimStart().length;
|
|
631
663
|
const text = part.text.trim();
|
|
632
664
|
if (text === '') {
|
|
633
665
|
lastAfter = part.after;
|
|
@@ -635,14 +667,63 @@ function splitSegments(command) {
|
|
|
635
667
|
}
|
|
636
668
|
out.push({
|
|
637
669
|
text,
|
|
638
|
-
start: part.start +
|
|
670
|
+
start: part.start + trimStart,
|
|
639
671
|
after: part.after,
|
|
640
672
|
before: lastAfter ?? '',
|
|
673
|
+
depths: part.depths.slice(trimStart),
|
|
641
674
|
});
|
|
642
675
|
lastAfter = part.after;
|
|
643
676
|
}
|
|
644
677
|
return out;
|
|
645
678
|
}
|
|
679
|
+
/** A redirect's target word: a quoted one keeps its spaces (`tee 'out file.txt'`). */
|
|
680
|
+
const REDIRECT_TARGET_WORD = String.raw `(?:'[^']*'|"[^"]*"|[^\s;&|]+)`;
|
|
681
|
+
/**
|
|
682
|
+
* A `>`, `>>` or `&>` redirect written at a statement's own level, with the word it writes to.
|
|
683
|
+
* The lookbehind is what keeps `2>&1` out: the `>` of a descriptor redirect follows a digit.
|
|
684
|
+
*/
|
|
685
|
+
const REDIRECT_AND_TARGET = new RegExp(String.raw `(?<![0-9&>])(?:&>|1?>>?)\s*(${REDIRECT_TARGET_WORD})`, 'g');
|
|
686
|
+
/** A word with one layer of MATCHING shell quotes removed (`'out file.txt'` → `out file.txt`). */
|
|
687
|
+
function unquotedWord(word) {
|
|
688
|
+
return word.replace(/^(['"])(.*)\1$/, '$2');
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* The file a statement sends its own output to, when it redirects it at the command's own level.
|
|
692
|
+
*
|
|
693
|
+
* Three shapes are deliberately not one: a redirect inside a `( … )` / `{ … }` group (it writes
|
|
694
|
+
* through that group, not through the command's own shell), a statement that feeds a heredoc's
|
|
695
|
+
* file (`cat > cfg.ts <<'EOF'` writes a config), and `/dev/null` or a descriptor (`2>&1`).
|
|
696
|
+
*
|
|
697
|
+
* @param text - The statement's text, as the tokenizer split it.
|
|
698
|
+
* @param depths - The depth each character of `text` was written at: the open-group count, 0 at
|
|
699
|
+
* the command's own level, -1 inside a quote or a command substitution.
|
|
700
|
+
* @param program - The word in command position (`tee` writes through its own file argument).
|
|
701
|
+
* @param args - The statement's words after the program.
|
|
702
|
+
* @returns The target with one layer of quotes removed, or null.
|
|
703
|
+
*/
|
|
704
|
+
function redirectTargetOf(text, depths, program, args) {
|
|
705
|
+
// A heredoc's own file is a config the command WRITES, never where the run's output goes
|
|
706
|
+
// (X1 flash-m44).
|
|
707
|
+
if (/(?:^|\s)<<(?![<])-?/.test(text))
|
|
708
|
+
return null;
|
|
709
|
+
// `| tee file` is a statement of its own: the pipe is a statement separator here.
|
|
710
|
+
if (program === 'tee') {
|
|
711
|
+
const target = args.find((arg) => !arg.startsWith('-'));
|
|
712
|
+
return target === undefined ? null : unquotedWord(target);
|
|
713
|
+
}
|
|
714
|
+
let last = null;
|
|
715
|
+
for (const match of text.matchAll(REDIRECT_AND_TARGET)) {
|
|
716
|
+
const word = unquotedWord(match[1] ?? '');
|
|
717
|
+
// Only the command's own level counts: not a group's redirect (depth > 0), and not one
|
|
718
|
+
// written inside a quote or a command substitution (depth -1).
|
|
719
|
+
if ((depths[match.index ?? 0] ?? 0) !== 0)
|
|
720
|
+
continue;
|
|
721
|
+
if (word === '/dev/null' || word.startsWith('&'))
|
|
722
|
+
continue;
|
|
723
|
+
last = word;
|
|
724
|
+
}
|
|
725
|
+
return last;
|
|
726
|
+
}
|
|
646
727
|
/**
|
|
647
728
|
* One segment's words: quotes kept whole, grouping parentheses dropped, and a `(` written
|
|
648
729
|
* against the first word taken off it (`(npx playwright test)`).
|
|
@@ -912,6 +993,9 @@ function locatedSegmentsOf(command, depth) {
|
|
|
912
993
|
assignmentOffset: part.start + leading.end,
|
|
913
994
|
programOffset: words[start.at] === undefined ? null : part.start + (wordOffset(part.text, start.at) ?? 0),
|
|
914
995
|
};
|
|
996
|
+
const program = words[start.at] === undefined ? '' : programName(words[start.at] ?? '');
|
|
997
|
+
const args = words.slice(start.at + 1);
|
|
998
|
+
const redirectTarget = redirectTargetOf(part.text, part.depths, program, args);
|
|
915
999
|
if (start.nested !== null) {
|
|
916
1000
|
const inner = locatedSegmentsOf(start.nested, depth + 1);
|
|
917
1001
|
for (let i = 0; i < inner.length; i += 1) {
|
|
@@ -927,6 +1011,12 @@ function locatedSegmentsOf(command, depth) {
|
|
|
927
1011
|
end: null,
|
|
928
1012
|
assignmentOffset: null,
|
|
929
1013
|
programOffset: null,
|
|
1014
|
+
// `sh -c '…' &` backgrounds the command inside the string too.
|
|
1015
|
+
terminator: part.after === '&' ? '&' : entry.terminator,
|
|
1016
|
+
// A redirect written OUTSIDE the string (`sh -c '…' > run.log`) carries the nested
|
|
1017
|
+
// command's output, so the first nested statement holds it — unless that statement
|
|
1018
|
+
// redirects on its own, which takes the output away from the outer file.
|
|
1019
|
+
redirectTarget: i === 0 && entry.redirectTarget === null ? redirectTarget : entry.redirectTarget,
|
|
930
1020
|
separator: i === 0 ? part.before : entry.separator,
|
|
931
1021
|
});
|
|
932
1022
|
}
|
|
@@ -943,11 +1033,11 @@ function locatedSegmentsOf(command, depth) {
|
|
|
943
1033
|
...placed,
|
|
944
1034
|
programOffset: null,
|
|
945
1035
|
separator: part.before,
|
|
1036
|
+
terminator: part.after,
|
|
1037
|
+
redirectTarget,
|
|
946
1038
|
});
|
|
947
1039
|
continue;
|
|
948
1040
|
}
|
|
949
|
-
const program = programName(word);
|
|
950
|
-
const args = words.slice(start.at + 1);
|
|
951
1041
|
out.push({
|
|
952
1042
|
segment: {
|
|
953
1043
|
program,
|
|
@@ -958,6 +1048,8 @@ function locatedSegmentsOf(command, depth) {
|
|
|
958
1048
|
assignments: leading.words,
|
|
959
1049
|
...placed,
|
|
960
1050
|
separator: part.before,
|
|
1051
|
+
terminator: part.after,
|
|
1052
|
+
redirectTarget,
|
|
961
1053
|
});
|
|
962
1054
|
}
|
|
963
1055
|
return out;
|
|
@@ -984,6 +1076,87 @@ export function commandTimeoutSeconds(command) {
|
|
|
984
1076
|
}
|
|
985
1077
|
return bound;
|
|
986
1078
|
}
|
|
1079
|
+
/**
|
|
1080
|
+
* The command with the `timeout <N>` wrapper lifted off the statements `which` selects, and the
|
|
1081
|
+
* largest bound that was lifted.
|
|
1082
|
+
*
|
|
1083
|
+
* The platform moves a run to the background precisely because its own bound cannot fit this
|
|
1084
|
+
* tool's ceiling, and a background task has no ceiling — its wait is bounded by `wait_for_task`
|
|
1085
|
+
* and the turn's deadline. The `timeout N` an executor wrote to fit the ceiling therefore keeps
|
|
1086
|
+
* killing the run at N in the background, where nothing needed it: X1 xr-molecule-flash-m143
|
|
1087
|
+
* (+24:45) wrote `timeout 280` for a 31-test walk, the tool backgrounded it, and the prefix stopped
|
|
1088
|
+
* the walk with no tally — then the cut sentence told the executor to "run it in the background",
|
|
1089
|
+
* which is where it already was.
|
|
1090
|
+
*
|
|
1091
|
+
* The wrapper is found by the same parse every other command reader uses ({@link commandStart}):
|
|
1092
|
+
* `timeout` in an argument bounds nothing, and the wrapper's own flags (`-k`/`--kill-after`,
|
|
1093
|
+
* `-s`/`--signal`) are stepped over, so `timeout -k 5 280 …` is `280`.
|
|
1094
|
+
*
|
|
1095
|
+
* @param command - The shell command.
|
|
1096
|
+
* @param which - The statements to lift the wrapper from (a suite run, for the background path).
|
|
1097
|
+
* @returns The rewritten command, and the largest lifted bound (null when nothing was lifted).
|
|
1098
|
+
*/
|
|
1099
|
+
export function withoutTimeoutWrapper(command, which) {
|
|
1100
|
+
if (typeof command !== 'string' || !command.trim())
|
|
1101
|
+
return { command, seconds: null };
|
|
1102
|
+
/** The spans to remove, as offsets in the command — applied back to front. */
|
|
1103
|
+
const removals = [];
|
|
1104
|
+
let seconds = null;
|
|
1105
|
+
for (const located of locatedSegmentsOf(command, 0)) {
|
|
1106
|
+
const segment = located.segment;
|
|
1107
|
+
if (segment === null || segment.timeoutSeconds === null)
|
|
1108
|
+
continue;
|
|
1109
|
+
// A statement inside a nested `sh -c '…'` has no offsets here: its positions are positions in
|
|
1110
|
+
// that string, so nothing may be spliced at them.
|
|
1111
|
+
if (located.start === null || located.programOffset === null)
|
|
1112
|
+
continue;
|
|
1113
|
+
if (!which(segment))
|
|
1114
|
+
continue;
|
|
1115
|
+
// Every word in front of the program: the assignments and the wrappers. The duration belongs
|
|
1116
|
+
// to the wrapper, so the walk has to see the words, not just the parse's total bound.
|
|
1117
|
+
const prefix = command.slice(located.start, located.programOffset);
|
|
1118
|
+
const words = [];
|
|
1119
|
+
const at = new RegExp(WORD.source, 'g');
|
|
1120
|
+
for (let match = at.exec(prefix); match !== null; match = at.exec(prefix)) {
|
|
1121
|
+
words.push({
|
|
1122
|
+
text: match[0],
|
|
1123
|
+
start: located.start + match.index,
|
|
1124
|
+
end: located.start + match.index + match[0].length,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
for (let i = 0; i < words.length; i += 1) {
|
|
1128
|
+
const word = words[i];
|
|
1129
|
+
if (word === undefined || programName(word.text) !== 'timeout')
|
|
1130
|
+
continue;
|
|
1131
|
+
// The wrapper's own flags, then the duration — the same walk {@link commandStart} makes.
|
|
1132
|
+
const withValue = WRAPPER_VALUE_FLAGS.timeout ?? [];
|
|
1133
|
+
let next = i + 1;
|
|
1134
|
+
while (next < words.length && (words[next]?.text ?? '').startsWith('-')) {
|
|
1135
|
+
const flag = unquoted(words[next]?.text ?? '');
|
|
1136
|
+
next += !flag.includes('=') && withValue.includes(flag) ? 2 : 1;
|
|
1137
|
+
}
|
|
1138
|
+
const duration = words[next];
|
|
1139
|
+
const bound = duration === undefined ? null : durationSeconds(unquoted(duration.text));
|
|
1140
|
+
if (duration === undefined || bound === null)
|
|
1141
|
+
continue;
|
|
1142
|
+
seconds = seconds === null ? bound : Math.max(seconds, bound);
|
|
1143
|
+
// The whitespace between the duration and the program goes too, so the command does not
|
|
1144
|
+
// carry a double space where the wrapper stood.
|
|
1145
|
+
let end = duration.end;
|
|
1146
|
+
while (end < command.length && /\s/.test(command[end] ?? ''))
|
|
1147
|
+
end += 1;
|
|
1148
|
+
removals.push({ start: word.start, end });
|
|
1149
|
+
i = next;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (removals.length === 0)
|
|
1153
|
+
return { command, seconds: null };
|
|
1154
|
+
let out = command;
|
|
1155
|
+
for (const removal of removals.sort((a, b) => b.start - a.start)) {
|
|
1156
|
+
out = out.slice(0, removal.start) + out.slice(removal.end);
|
|
1157
|
+
}
|
|
1158
|
+
return { command: out, seconds };
|
|
1159
|
+
}
|
|
987
1160
|
/**
|
|
988
1161
|
* Whether any segment of the command runs a program whose command-position text matches
|
|
989
1162
|
* `pattern` — the one question a runner-kind table asks.
|
package/dist/cut-run.d.ts
CHANGED
|
@@ -134,6 +134,14 @@ export interface CutRun {
|
|
|
134
134
|
seconds: number;
|
|
135
135
|
/** Whether this tool's budget stopped it or the `timeout` the command carried. */
|
|
136
136
|
stoppedBy: CutBy;
|
|
137
|
+
/**
|
|
138
|
+
* Whether this was a detached background task, read with no budget of this tool's own
|
|
139
|
+
* ({@link commandCutRun}'s `budgetSeconds` is 0). A background run is bounded by what its own
|
|
140
|
+
* command carries — so a `command-timeout` cut there is that prefix killing the walk, and the
|
|
141
|
+
* executor must be told to drop the prefix, never to start the run in the background it is
|
|
142
|
+
* already in (X1 xr-molecule-flash-m143 +24:45).
|
|
143
|
+
*/
|
|
144
|
+
background: boolean;
|
|
137
145
|
/**
|
|
138
146
|
* The exit code the run's own process reported, or null when the code in the result belongs to
|
|
139
147
|
* the pipeline's filter (`… | tail`) rather than to the run — the case that read as success.
|
package/dist/cut-run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cut-run.d.ts","sourceRoot":"","sources":["../src/cut-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAKH;;;GAGG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAE5C,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,aAAa,CAAA;AAE1D;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAGhE;AAiBD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAkDD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA4B9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAKD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GACb,MAAM,GAAG,IAAI,CAIf;AAED,0BAA0B;AAC1B,MAAM,MAAM,KAAK;AACf,sEAAsE;AACpE,aAAa;AACf,oDAAoD;GAClD,iBAAiB;AACnB;;;;GAIG;GACD,cAAc,CAAA;AAElB,wDAAwD;AACxD,MAAM,WAAW,MAAM;IACrB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAA;IACf,kFAAkF;IAClF,SAAS,EAAE,KAAK,CAAA;IAChB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,qCAAqC;IACrC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,yEAAyE;IACzE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAsDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,aAAa,SAAI,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"cut-run.d.ts","sourceRoot":"","sources":["../src/cut-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAKH;;;GAGG;AACH,eAAO,MAAM,aAAa,kBAAkB,CAAA;AAE5C,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,aAAa,CAAA;AAE1D;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAGhE;AAiBD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAA;CAClB;AAkDD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA4B9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEhD;AAKD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,GACb,MAAM,GAAG,IAAI,CAIf;AAED,0BAA0B;AAC1B,MAAM,MAAM,KAAK;AACf,sEAAsE;AACpE,aAAa;AACf,oDAAoD;GAClD,iBAAiB;AACnB;;;;GAIG;GACD,cAAc,CAAA;AAElB,wDAAwD;AACxD,MAAM,WAAW,MAAM;IACrB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAA;IACf,kFAAkF;IAClF,SAAS,EAAE,KAAK,CAAA;IAChB;;;;;;OAMG;IACH,UAAU,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB;;;OAGG;IACH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,qCAAqC;IACrC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,yEAAyE;IACzE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAsDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,aAAa,SAAI,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAuFf;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAiB1D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAiClD"}
|
package/dist/cut-run.js
CHANGED
|
@@ -296,6 +296,9 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0, raw
|
|
|
296
296
|
// tool budget at all (a reader re-reading a background run) the command's own bound is the only
|
|
297
297
|
// one there is.
|
|
298
298
|
const ownBoundStops = budgetSeconds === 0 || (declared !== null && declared < budgetSeconds);
|
|
299
|
+
// No budget of this tool's in force is what a background reader is: the task was detached, and
|
|
300
|
+
// nothing bounded it but what its own command carries.
|
|
301
|
+
const background = budgetSeconds === 0;
|
|
299
302
|
// `timeout`'s own 124 — the code the backend's wrapper returns when the TOOL's budget fires,
|
|
300
303
|
// whatever the pipeline ended with (`backends/sandbox.ts`). Which of the two bounds fired is the
|
|
301
304
|
// clock's to say: a run killed at the tool's budget reports at least the budget, and one killed
|
|
@@ -308,6 +311,7 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0, raw
|
|
|
308
311
|
return {
|
|
309
312
|
seconds: ownFired ? declared : budgetSeconds,
|
|
310
313
|
stoppedBy: ownFired ? 'command-timeout' : 'tool-budget',
|
|
314
|
+
background,
|
|
311
315
|
exitCode,
|
|
312
316
|
...progress,
|
|
313
317
|
doing,
|
|
@@ -317,6 +321,7 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0, raw
|
|
|
317
321
|
return {
|
|
318
322
|
seconds: declared,
|
|
319
323
|
stoppedBy: 'command-timeout',
|
|
324
|
+
background,
|
|
320
325
|
// A code the filter produced is not the run's: `… | tail` handed back 0 forever (m93).
|
|
321
326
|
exitCode: withheld || exitCode === 0 ? null : exitCode,
|
|
322
327
|
...progress,
|
|
@@ -329,6 +334,7 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0, raw
|
|
|
329
334
|
return {
|
|
330
335
|
seconds: budgetSeconds,
|
|
331
336
|
stoppedBy: 'tool-budget',
|
|
337
|
+
background,
|
|
332
338
|
exitCode: exitCode === 0 ? null : exitCode,
|
|
333
339
|
...progress,
|
|
334
340
|
doing,
|
|
@@ -344,6 +350,7 @@ export function commandCutRun(command, result, elapsedMs, budgetSeconds = 0, raw
|
|
|
344
350
|
return {
|
|
345
351
|
seconds: usedBudget ? budgetSeconds : Math.max(1, Math.round(elapsedMs / 1000)),
|
|
346
352
|
stoppedBy: usedBudget ? 'tool-budget' : 'process-stop',
|
|
353
|
+
background,
|
|
347
354
|
exitCode: withheld || exitCode === 0 ? null : exitCode,
|
|
348
355
|
...progress,
|
|
349
356
|
doing,
|
|
@@ -377,6 +384,7 @@ export function cutRunFacts(result) {
|
|
|
377
384
|
return {
|
|
378
385
|
seconds: c.seconds,
|
|
379
386
|
stoppedBy: c.stoppedBy,
|
|
387
|
+
background: c.background === true,
|
|
380
388
|
exitCode: typeof c.exitCode === 'number' ? c.exitCode : null,
|
|
381
389
|
ran: typeof c.ran === 'number' ? c.ran : null,
|
|
382
390
|
total: typeof c.total === 'number' ? c.total : null,
|
|
@@ -401,9 +409,19 @@ export function cutRunSentence(run) {
|
|
|
401
409
|
const by = run.stoppedBy === 'tool-budget'
|
|
402
410
|
? "this tool's limit"
|
|
403
411
|
: run.stoppedBy === 'command-timeout'
|
|
404
|
-
?
|
|
412
|
+
? run.background
|
|
413
|
+
? `the command's own \`timeout ${run.seconds}\`, which killed it at ${run.seconds} s`
|
|
414
|
+
: `its own \`timeout ${run.seconds}\``
|
|
405
415
|
: 'a process kill outside this tool (no `timeout` of its own and no limit of this tool had fired)';
|
|
406
|
-
|
|
416
|
+
// What the reader must do next. A run that was ALREADY a background task has nothing to gain
|
|
417
|
+
// from being sent to the background again — when its own `timeout` is what stopped it, that
|
|
418
|
+
// prefix is the thing to drop (X1 xr-molecule-flash-m143 +24:45: three cut sentences in one run
|
|
419
|
+
// told a background walk to "run it in the background", which is where it already was).
|
|
420
|
+
const background = run.background
|
|
421
|
+
? run.stoppedBy === 'command-timeout'
|
|
422
|
+
? `drop the \`timeout\` prefix and send it again — in the background a run is bounded by \`wait_for_task\` and the turn's deadline, not by this tool's ceiling, so the bound you wrote is what killed it`
|
|
423
|
+
: 'send it again when you are ready'
|
|
424
|
+
: 'run it in the background (run_in_background + wait_for_task)';
|
|
407
425
|
const missing = run.ran !== null && run.total !== null ? run.total - run.ran : null;
|
|
408
426
|
const lead = missing !== null
|
|
409
427
|
? `Ran ${run.ran} of ${run.total} ${run.noun} in ${run.seconds} s and was stopped by ${by} — the ${missing} it never reached ${missing === 1 ? 'has' : 'have'} not run; ${background}`
|
package/dist/index.d.ts
CHANGED
|
@@ -136,6 +136,7 @@ export * from './command-position.js';
|
|
|
136
136
|
export * from './cut-run.js';
|
|
137
137
|
export * from './failure-kind.js';
|
|
138
138
|
export * from './input-normalizer.js';
|
|
139
|
+
export * from './line-diff.js';
|
|
139
140
|
export * from './no-test-run.js';
|
|
140
141
|
export * from './pipeline-exit-note.js';
|
|
141
142
|
export * from './schemas.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkIG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkIG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -136,6 +136,7 @@ export * from './command-position.js';
|
|
|
136
136
|
export * from './cut-run.js';
|
|
137
137
|
export * from './failure-kind.js';
|
|
138
138
|
export * from './input-normalizer.js';
|
|
139
|
+
export * from './line-diff.js';
|
|
139
140
|
export * from './no-test-run.js';
|
|
140
141
|
export * from './pipeline-exit-note.js';
|
|
141
142
|
export * from './schemas.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real added/removed LINE counts for the `diff` a file-writing tool reports.
|
|
3
|
+
*
|
|
4
|
+
* The count is a diff, not an arithmetic delta: replacing one line with another
|
|
5
|
+
* leaves the line count unchanged, so `new.length - old.length` is 0 for the
|
|
6
|
+
* edit that changed the file — a result that reads to the model as "your write
|
|
7
|
+
* did nothing" (X0 xr-molecule-flash-m141 +09:56: `write_file` rewrote
|
|
8
|
+
* `ArticlePreview.tsx`'s favorite label and reported
|
|
9
|
+
* `"linesAdded":0,"linesRemoved":0`). The lines an edit actually changed are
|
|
10
|
+
* found with the same longest-common-subsequence line diff the IDE's tool cards
|
|
11
|
+
* use (`@molecule/app-ide-react`'s `diffLineCount`); it is mirrored here because
|
|
12
|
+
* the API and App halves of the workspace cannot import each other (AGENTS.md
|
|
13
|
+
* Rule 11, enforced by ESLint). Keep the two in step if either changes.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Count truly added/removed lines between two line arrays using LCS.
|
|
19
|
+
*
|
|
20
|
+
* Bounded by construction, so a whole-file edit cannot allocate a quadratic
|
|
21
|
+
* table here: the longest common prefix and suffix are trimmed first (exact —
|
|
22
|
+
* those lines belong to every optimal diff, and on the usual small edit they
|
|
23
|
+
* shrink the table to the edited span), and what is left is capped (see
|
|
24
|
+
* {@link lcsLength}).
|
|
25
|
+
*
|
|
26
|
+
* @param oldLines - The original lines array.
|
|
27
|
+
* @param newLines - The modified lines array.
|
|
28
|
+
* @returns An object with the number of added and removed lines.
|
|
29
|
+
*/
|
|
30
|
+
export declare function diffLineCount(oldLines: readonly string[], newLines: readonly string[]): {
|
|
31
|
+
added: number;
|
|
32
|
+
removed: number;
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=line-diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"line-diff.d.ts","sourceRoot":"","sources":["../src/line-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAmGH;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAuBpC"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real added/removed LINE counts for the `diff` a file-writing tool reports.
|
|
3
|
+
*
|
|
4
|
+
* The count is a diff, not an arithmetic delta: replacing one line with another
|
|
5
|
+
* leaves the line count unchanged, so `new.length - old.length` is 0 for the
|
|
6
|
+
* edit that changed the file — a result that reads to the model as "your write
|
|
7
|
+
* did nothing" (X0 xr-molecule-flash-m141 +09:56: `write_file` rewrote
|
|
8
|
+
* `ArticlePreview.tsx`'s favorite label and reported
|
|
9
|
+
* `"linesAdded":0,"linesRemoved":0`). The lines an edit actually changed are
|
|
10
|
+
* found with the same longest-common-subsequence line diff the IDE's tool cards
|
|
11
|
+
* use (`@molecule/app-ide-react`'s `diffLineCount`); it is mirrored here because
|
|
12
|
+
* the API and App halves of the workspace cannot import each other (AGENTS.md
|
|
13
|
+
* Rule 11, enforced by ESLint). Keep the two in step if either changes.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Largest LCS table the exact diff may build, in cells (old × new). A whole-file
|
|
19
|
+
* edit can be thousands of lines per side, i.e. tens of millions of cells —
|
|
20
|
+
* gigabytes of heap for a summary that only needs two numbers. Above this cap
|
|
21
|
+
* the answer comes from {@link orderedCommonLines}, a provable, order-respecting
|
|
22
|
+
* common subsequence in O(n log n); below it the result is exact. 1 << 22 cells
|
|
23
|
+
* is a few milliseconds, and the two-row table keeps memory at O(min(old, new)).
|
|
24
|
+
*/
|
|
25
|
+
const MAX_DIFF_CELLS = 1 << 22;
|
|
26
|
+
/**
|
|
27
|
+
* Longest common subsequence length of two line arrays, in bounded time and
|
|
28
|
+
* memory: two rows of the recurrence instead of the whole table, and — once the
|
|
29
|
+
* table would exceed {@link MAX_DIFF_CELLS} — the order-respecting lower bound
|
|
30
|
+
* from {@link orderedCommonLines}.
|
|
31
|
+
*
|
|
32
|
+
* @param a - The first line array.
|
|
33
|
+
* @param b - The second line array.
|
|
34
|
+
* @returns The common subsequence length (a lower bound for the capped inputs).
|
|
35
|
+
*/
|
|
36
|
+
function lcsLength(a, b) {
|
|
37
|
+
if (a.length === 0 || b.length === 0)
|
|
38
|
+
return 0;
|
|
39
|
+
if (a.length * b.length > MAX_DIFF_CELLS)
|
|
40
|
+
return orderedCommonLines(a, b);
|
|
41
|
+
// The recurrence needs only the previous row, and a row is `cols` wide — so
|
|
42
|
+
// the SHORTER array belongs on the `cols` axis, which bounds every row by
|
|
43
|
+
// min(a, b) and is what the memory claim above rests on. The recurrence is
|
|
44
|
+
// symmetric, so the length it returns is the same either way.
|
|
45
|
+
let rows = a;
|
|
46
|
+
let cols = b;
|
|
47
|
+
if (cols.length > rows.length) {
|
|
48
|
+
rows = b;
|
|
49
|
+
cols = a;
|
|
50
|
+
}
|
|
51
|
+
let previous = new Array(cols.length + 1).fill(0);
|
|
52
|
+
let current = new Array(cols.length + 1).fill(0);
|
|
53
|
+
for (let i = 1; i <= rows.length; i++) {
|
|
54
|
+
const line = rows[i - 1];
|
|
55
|
+
for (let j = 1; j <= cols.length; j++) {
|
|
56
|
+
if (line === cols[j - 1]) {
|
|
57
|
+
current[j] = previous[j - 1] + 1;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const above = previous[j];
|
|
61
|
+
const left = current[j - 1];
|
|
62
|
+
current[j] = above >= left ? above : left;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const swap = previous;
|
|
66
|
+
previous = current;
|
|
67
|
+
current = swap;
|
|
68
|
+
}
|
|
69
|
+
return previous[cols.length];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* How many lines the two arrays share IN ORDER, for inputs too large for the
|
|
73
|
+
* exact table: the longest run of lines that appear in the same relative order
|
|
74
|
+
* on both sides, found among the lines that occur exactly once in `a` (their
|
|
75
|
+
* position is unambiguous) by reducing their `b` positions to a longest
|
|
76
|
+
* strictly increasing run — the standard patience LIS, O(n log n).
|
|
77
|
+
*
|
|
78
|
+
* Order is the whole point. Counting the lines as a multiset (order thrown
|
|
79
|
+
* away) is only an UPPER bound on the LCS, so it under-reports changes: a
|
|
80
|
+
* 3000-line file whose two halves were swapped shares every line, and the
|
|
81
|
+
* multiset count would report the replacement as `+0 −0` — a result claiming a
|
|
82
|
+
* real edit changed nothing. Every line counted here is a real in-order match,
|
|
83
|
+
* so the answer is a LOWER bound on the LCS and the change count it feeds can
|
|
84
|
+
* only ever be over-stated, never denied.
|
|
85
|
+
*
|
|
86
|
+
* @param a - The first line array.
|
|
87
|
+
* @param b - The second line array.
|
|
88
|
+
* @returns A common subsequence length.
|
|
89
|
+
*/
|
|
90
|
+
function orderedCommonLines(a, b) {
|
|
91
|
+
const occurrences = new Map();
|
|
92
|
+
for (const line of a)
|
|
93
|
+
occurrences.set(line, (occurrences.get(line) ?? 0) + 1);
|
|
94
|
+
const position = new Map();
|
|
95
|
+
a.forEach((line, index) => {
|
|
96
|
+
if (occurrences.get(line) === 1)
|
|
97
|
+
position.set(line, index);
|
|
98
|
+
});
|
|
99
|
+
// Patience: `tails[i]` is the smallest `b`-position that ends an increasing
|
|
100
|
+
// run of length `i + 1`.
|
|
101
|
+
const tails = [];
|
|
102
|
+
for (const line of b) {
|
|
103
|
+
const at = position.get(line);
|
|
104
|
+
if (at === undefined)
|
|
105
|
+
continue;
|
|
106
|
+
let lo = 0;
|
|
107
|
+
let hi = tails.length;
|
|
108
|
+
while (lo < hi) {
|
|
109
|
+
const mid = (lo + hi) >> 1;
|
|
110
|
+
if (tails[mid] < at)
|
|
111
|
+
lo = mid + 1;
|
|
112
|
+
else
|
|
113
|
+
hi = mid;
|
|
114
|
+
}
|
|
115
|
+
tails[lo] = at;
|
|
116
|
+
}
|
|
117
|
+
return tails.length;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Count truly added/removed lines between two line arrays using LCS.
|
|
121
|
+
*
|
|
122
|
+
* Bounded by construction, so a whole-file edit cannot allocate a quadratic
|
|
123
|
+
* table here: the longest common prefix and suffix are trimmed first (exact —
|
|
124
|
+
* those lines belong to every optimal diff, and on the usual small edit they
|
|
125
|
+
* shrink the table to the edited span), and what is left is capped (see
|
|
126
|
+
* {@link lcsLength}).
|
|
127
|
+
*
|
|
128
|
+
* @param oldLines - The original lines array.
|
|
129
|
+
* @param newLines - The modified lines array.
|
|
130
|
+
* @returns An object with the number of added and removed lines.
|
|
131
|
+
*/
|
|
132
|
+
export function diffLineCount(oldLines, newLines) {
|
|
133
|
+
const oldLength = oldLines.length;
|
|
134
|
+
const newLength = newLines.length;
|
|
135
|
+
const shared = Math.min(oldLength, newLength);
|
|
136
|
+
// Longest common prefix / suffix. `suffix` is bounded by `shared - prefix`, so
|
|
137
|
+
// the two trimmed runs can never overlap each other.
|
|
138
|
+
let prefix = 0;
|
|
139
|
+
while (prefix < shared && oldLines[prefix] === newLines[prefix])
|
|
140
|
+
prefix++;
|
|
141
|
+
let suffix = 0;
|
|
142
|
+
while (suffix < shared - prefix &&
|
|
143
|
+
oldLines[oldLength - 1 - suffix] === newLines[newLength - 1 - suffix]) {
|
|
144
|
+
suffix++;
|
|
145
|
+
}
|
|
146
|
+
const common = prefix +
|
|
147
|
+
suffix +
|
|
148
|
+
lcsLength(oldLines.slice(prefix, oldLength - suffix), newLines.slice(prefix, newLength - suffix));
|
|
149
|
+
return { added: newLength - common, removed: oldLength - common };
|
|
150
|
+
}
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAkJ9C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAahG;AAkOD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAqEnE;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAEA,CAAA;AAE9B;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,QAI6C,CAAA;AAmBlF;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,8FAA8F;IAC9F,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;AAoBpF;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,CAAC,GACxE,qBAAqB,CAkBvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,qBAAqB,GAC5B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiCxB;AA8ED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAk9ExF"}
|