@shapeshift-labs/frontier-lang-compiler 0.2.100 → 0.2.101
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/dist/declarations/bidirectional-target-change-evidence.d.ts +299 -0
- package/dist/declarations/bidirectional-target-change.d.ts +19 -120
- package/dist/declarations/native-project-admission.d.ts +43 -22
- package/dist/declarations/semantic-edit-replay-diagnostics.d.ts +24 -0
- package/dist/declarations/semantic-edit-script.d.ts +20 -15
- package/dist/declarations/semantic-lineage.d.ts +3 -21
- package/dist/declarations/semantic-merge-candidates.d.ts +39 -0
- package/dist/declarations/semantic-sidecar-admission.d.ts +14 -0
- package/dist/declarations/semantic-sidecar.d.ts +12 -14
- package/dist/internal/index-impl/bidirectionalTargetRoundtripEvidence.js +200 -0
- package/dist/internal/index-impl/createBidirectionalTargetChangeRecord.js +62 -17
- package/dist/internal/index-impl/createNativeSourcePreservation.js +16 -1
- package/dist/internal/index-impl/createProjectImportAdmissionRecord.js +151 -1
- package/dist/internal/index-impl/createSemanticImportSidecar.js +5 -0
- package/dist/internal/index-impl/createSemanticImportSidecarAdmission.js +29 -11
- package/dist/internal/index-impl/nativeChangeProjectionEndpoint.js +56 -16
- package/dist/internal/index-impl/projectImportAdmissionMergeScore.js +26 -74
- package/dist/internal/index-impl/projectImportAdmissionProjectionCoverage.js +74 -0
- package/dist/internal/index-impl/projectSemanticEditScriptToSource.js +39 -13
- package/dist/internal/index-impl/replaySemanticEditProjection.js +65 -23
- package/dist/internal/index-impl/semanticEditInsertionAnchors.js +8 -5
- package/dist/internal/index-impl/semanticEditReplayDiagnostics.js +167 -0
- package/dist/internal/index-impl/semanticEditSourceRanges.js +94 -15
- package/dist/internal/index-impl/semanticHistoryLineageResolution.js +21 -2
- package/dist/internal/index-impl/semanticLineageHashEvidence.js +97 -0
- package/dist/internal/index-impl/semanticLineageInferenceMatching.js +8 -0
- package/dist/internal/index-impl/semanticLineageResolutionRecords.js +18 -1
- package/dist/internal/index-impl/semanticMergeCandidateRecords.js +22 -2
- package/dist/internal/index-impl/semanticMergeCandidateScoreFacets.js +221 -0
- package/dist/internal/index-impl/semanticPatchBundleOverlaps.js +23 -1
- package/dist/internal/index-impl/sourcePreservationFromProjectionContext.js +9 -2
- package/dist/native-import-language-profiles.js +10 -2
- package/dist/native-region-scanner-js-helpers.js +8 -2
- package/dist/native-region-scanner-js-imports.js +7 -0
- package/dist/native-region-scanner-js.js +4 -4
- package/dist/native-region-scanner.js +2 -1
- package/dist/semantic-import-regions.js +6 -4
- package/dist/semantic-import-sidecar-admission-types.d.ts +14 -0
- package/dist/semantic-import-sidecar-entry.js +151 -7
- package/dist/semantic-import-sidecar-types.d.ts +18 -13
- package/dist/semantic-import-source-preservation-utils.js +55 -0
- package/dist/semantic-import-source-preservation.js +98 -3
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { idFragment, normalizeNativeLanguageId, uniqueStrings } from '../../nati
|
|
|
3
3
|
import { createSemanticImportSidecar } from './createSemanticImportSidecar.js';
|
|
4
4
|
import { mapDiffSymbols } from './mapDiffSymbols.js';
|
|
5
5
|
import { normalizeNativeDiffImport } from './normalizeNativeDiffImport.js';
|
|
6
|
+
import { replayDiagnostics, replayEditDiagnostics, replayEditsWithOverlapDiagnostics } from './semanticEditReplayDiagnostics.js';
|
|
6
7
|
import { afterLineOffset, bodyContentRange, spanOffsets } from './semanticEditSourceRanges.js';
|
|
7
8
|
|
|
8
9
|
export function replaySemanticEditProjection(input = {}) {
|
|
@@ -17,11 +18,20 @@ export function replaySemanticEditProjection(input = {}) {
|
|
|
17
18
|
const currentSymbols = currentSourceText && isJavaScriptLike(language)
|
|
18
19
|
? currentSymbolIndex({ currentSourceText, sourcePath, language, parser: input.parser })
|
|
19
20
|
: [];
|
|
20
|
-
const
|
|
21
|
+
const replayedEdits = projection.status === 'projected' && typeof currentSourceText === 'string'
|
|
21
22
|
? (projection.edits ?? []).map((edit, index) => replayProjectionEdit(projectionEditWithOrder(edit, index), { currentSourceText, currentSymbols }))
|
|
22
23
|
: [];
|
|
24
|
+
const edits = replayEditsWithOverlapDiagnostics(replayedEdits);
|
|
23
25
|
const status = replayStatus(reasonCodes, edits, projection);
|
|
24
26
|
const outputSourceText = replayOutputSource(status, currentSourceText, edits);
|
|
27
|
+
const diagnostics = replayDiagnostics({
|
|
28
|
+
status,
|
|
29
|
+
reasonCodes,
|
|
30
|
+
edits,
|
|
31
|
+
sourcePath,
|
|
32
|
+
currentHash,
|
|
33
|
+
expectedCurrentHash: input.currentSourceHash
|
|
34
|
+
});
|
|
25
35
|
const core = {
|
|
26
36
|
kind: 'frontier.lang.semanticEditReplay',
|
|
27
37
|
version: 1,
|
|
@@ -38,6 +48,7 @@ export function replaySemanticEditProjection(input = {}) {
|
|
|
38
48
|
edits,
|
|
39
49
|
appliedOperations: edits.filter((edit) => edit.status === 'applied').map((edit) => edit.operationId).filter(Boolean),
|
|
40
50
|
skippedOperations: edits.filter((edit) => edit.status !== 'applied').map((edit) => edit.operationId).filter(Boolean),
|
|
51
|
+
diagnostics,
|
|
41
52
|
admission: replayAdmission(status, reasonCodes, edits),
|
|
42
53
|
outputSourceText,
|
|
43
54
|
summary: replaySummary(edits, reasonCodes),
|
|
@@ -52,8 +63,8 @@ export function replaySemanticEditProjection(input = {}) {
|
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
function replayProjectionEdit(edit, context) {
|
|
55
|
-
if (edit.status === 'already-applied') return replayEditRecord(edit, 'already-applied', undefined, ['projection-edit-already-applied']);
|
|
56
|
-
if (typeof edit.replacementText !== 'string') return replayEditRecord(edit, 'blocked', undefined, ['missing-replacement-text']);
|
|
66
|
+
if (edit.status === 'already-applied') return replayEditRecord(edit, 'already-applied', undefined, ['projection-edit-already-applied'], context.currentSourceText);
|
|
67
|
+
if (typeof edit.replacementText !== 'string') return replayEditRecord(edit, 'blocked', undefined, ['missing-replacement-text'], context.currentSourceText);
|
|
57
68
|
if (edit.editKind === 'insert') return replayInsertionEdit(edit, context);
|
|
58
69
|
const headRange = { start: edit.headStart, end: edit.headEnd };
|
|
59
70
|
const offset = checkRange(edit, headRange, context.currentSourceText, 'head-offset');
|
|
@@ -61,34 +72,34 @@ function replayProjectionEdit(edit, context) {
|
|
|
61
72
|
const spanRange = currentSymbolEditRange(edit, spanOffsets(context.currentSourceText, symbol?.sourceSpan), context.currentSourceText);
|
|
62
73
|
if (symbol && spanRange && !sameRange(headRange, spanRange)) {
|
|
63
74
|
const moved = checkRange(edit, spanRange, context.currentSourceText, currentSymbolRangeLabel(edit));
|
|
64
|
-
if (moved) return replayEditRecord(edit, moved.status, moved.range, [moved.reason, 'offset-reanchored-by-symbol']);
|
|
75
|
+
if (moved) return replayEditRecord(edit, moved.status, moved.range, [moved.reason, 'offset-reanchored-by-symbol'], context.currentSourceText);
|
|
65
76
|
if (edit.editKind === 'delete' && offset && rangesOverlap(headRange, spanRange)) {
|
|
66
|
-
return replayEditRecord(edit, offset.status, offset.range, [offset.reason]);
|
|
77
|
+
return replayEditRecord(edit, offset.status, offset.range, [offset.reason], context.currentSourceText);
|
|
67
78
|
}
|
|
68
|
-
return replayEditRecord(edit, 'conflict', spanRange, [`${currentSymbolRangeLabel(edit)}-content-mismatch`]);
|
|
79
|
+
return replayEditRecord(edit, 'conflict', spanRange, [`${currentSymbolRangeLabel(edit)}-content-mismatch`], context.currentSourceText);
|
|
69
80
|
}
|
|
70
|
-
if (offset) return replayEditRecord(edit, offset.status, offset.range, [offset.reason]);
|
|
81
|
+
if (offset) return replayEditRecord(edit, offset.status, offset.range, [offset.reason], context.currentSourceText);
|
|
71
82
|
const anchored = checkRange(edit, spanRange, context.currentSourceText, currentSymbolRangeLabel(edit));
|
|
72
|
-
if (anchored) return replayEditRecord(edit, anchored.status, anchored.range, [anchored.reason, 'offset-reanchored-by-symbol']);
|
|
83
|
+
if (anchored) return replayEditRecord(edit, anchored.status, anchored.range, [anchored.reason, 'offset-reanchored-by-symbol'], context.currentSourceText);
|
|
73
84
|
return replayEditRecord(edit, symbol ? 'conflict' : 'stale', spanRange, [
|
|
74
85
|
symbol ? `${currentSymbolRangeLabel(edit)}-content-mismatch` : 'current-symbol-anchor-missing'
|
|
75
|
-
]);
|
|
86
|
+
], context.currentSourceText);
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
function replayInsertionEdit(edit, context) {
|
|
79
90
|
const inserted = findCurrentSymbol(edit, context.currentSymbols);
|
|
80
91
|
const insertedRange = spanOffsets(context.currentSourceText, inserted?.sourceSpan);
|
|
81
92
|
const already = checkRange(edit, insertedRange, context.currentSourceText, 'current-inserted-symbol');
|
|
82
|
-
if (already?.status === 'already-applied') return replayEditRecord(edit, 'already-applied', already.range, [already.reason]);
|
|
93
|
+
if (already?.status === 'already-applied') return replayEditRecord(edit, 'already-applied', already.range, [already.reason], context.currentSourceText);
|
|
83
94
|
if (inserted && insertedRange) {
|
|
84
|
-
return replayEditRecord(edit, 'conflict', insertedRange, ['current-inserted-symbol-content-mismatch']);
|
|
95
|
+
return replayEditRecord(edit, 'conflict', insertedRange, ['current-inserted-symbol-content-mismatch'], context.currentSourceText);
|
|
85
96
|
}
|
|
86
|
-
const anchor =
|
|
87
|
-
const range = insertionRange(edit, anchor, context.currentSourceText);
|
|
88
|
-
if (range) return replayEditRecord(edit, 'applied', range, [anchor ? 'current-insertion-anchor' : `current-${edit.insertionMode}`]);
|
|
97
|
+
const anchor = findInsertionAnchor(edit, context.currentSymbols);
|
|
98
|
+
const range = insertionRange(edit, anchor?.candidate, anchor?.symbol, context.currentSourceText);
|
|
99
|
+
if (range) return replayEditRecord(edit, 'applied', range, [anchor ? 'current-insertion-anchor' : `current-${edit.insertionMode}`], context.currentSourceText);
|
|
89
100
|
return replayEditRecord(edit, anchor ? 'conflict' : 'stale', undefined, [
|
|
90
101
|
anchor ? 'current-insertion-anchor-unusable' : 'current-insertion-anchor-missing'
|
|
91
|
-
]);
|
|
102
|
+
], context.currentSourceText);
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
function checkRange(edit, range, sourceText, label) {
|
|
@@ -102,7 +113,8 @@ function checkRange(edit, range, sourceText, label) {
|
|
|
102
113
|
return undefined;
|
|
103
114
|
}
|
|
104
115
|
|
|
105
|
-
function replayEditRecord(edit, status, range, reasonCodes) {
|
|
116
|
+
function replayEditRecord(edit, status, range, reasonCodes, sourceText) {
|
|
117
|
+
const normalizedReasonCodes = reasonList(reasonCodes);
|
|
106
118
|
return compactRecord({
|
|
107
119
|
operationId: edit.operationId,
|
|
108
120
|
semanticKey: edit.semanticKey,
|
|
@@ -120,7 +132,8 @@ function replayEditRecord(edit, status, range, reasonCodes) {
|
|
|
120
132
|
end: range?.end,
|
|
121
133
|
replacementBytes: edit.replacementBytes,
|
|
122
134
|
replacementText: edit.replacementText,
|
|
123
|
-
reasonCodes:
|
|
135
|
+
reasonCodes: normalizedReasonCodes,
|
|
136
|
+
diagnostics: replayEditDiagnostics(edit, status, range, normalizedReasonCodes, sourceText)
|
|
124
137
|
});
|
|
125
138
|
}
|
|
126
139
|
|
|
@@ -146,18 +159,47 @@ function findCurrentSymbol(edit, symbols) {
|
|
|
146
159
|
return symbols.find((symbol) => symbol.name === name && (!kind || symbol.kind === kind));
|
|
147
160
|
}
|
|
148
161
|
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
function findInsertionAnchor(edit, symbols) {
|
|
163
|
+
for (const candidate of insertionAnchorCandidates(edit)) {
|
|
164
|
+
const symbol = findInsertionAnchorSymbol(candidate, symbols);
|
|
165
|
+
if (symbol) return { candidate, symbol };
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function findInsertionAnchorSymbol(candidate, symbols) {
|
|
171
|
+
const keys = [candidate.anchorKey, candidate.anchorSymbolId].filter(Boolean);
|
|
172
|
+
return symbols.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && keys.includes(key)))
|
|
173
|
+
?? symbols.find((symbol) => symbol.name === candidate.anchorSymbolName && (!candidate.anchorSymbolKind || symbol.kind === candidate.anchorSymbolKind));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function insertionAnchorCandidates(edit) {
|
|
177
|
+
const primary = {
|
|
178
|
+
mode: edit.insertionMode,
|
|
179
|
+
anchorKey: edit.insertionAnchorKey,
|
|
180
|
+
anchorSymbolName: edit.insertionAnchorSymbolName,
|
|
181
|
+
anchorSymbolKind: edit.insertionAnchorSymbolKind
|
|
182
|
+
};
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
const result = [];
|
|
185
|
+
for (const candidate of [primary, ...(Array.isArray(edit.insertionAnchorCandidates) ? edit.insertionAnchorCandidates : [])]) {
|
|
186
|
+
if (!candidate || (candidate.mode !== 'before' && candidate.mode !== 'after')) continue;
|
|
187
|
+
const key = [candidate.mode, candidate.anchorKey, candidate.anchorSymbolId, candidate.anchorSymbolName, candidate.anchorSymbolKind].join('\0');
|
|
188
|
+
if (seen.has(key)) continue;
|
|
189
|
+
seen.add(key);
|
|
190
|
+
result.push(candidate);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
152
193
|
}
|
|
153
194
|
|
|
154
|
-
function insertionRange(edit, anchor, sourceText) {
|
|
195
|
+
function insertionRange(edit, candidate, anchor, sourceText) {
|
|
155
196
|
if (edit.insertionMode === 'file-start') return { start: 0, end: 0 };
|
|
156
197
|
if (edit.insertionMode === 'file-end') return { start: sourceText.length, end: sourceText.length };
|
|
198
|
+
const mode = candidate?.mode ?? edit.insertionMode;
|
|
157
199
|
const anchorRange = spanOffsets(sourceText, anchor?.sourceSpan);
|
|
158
200
|
if (!anchorRange) return undefined;
|
|
159
|
-
if (
|
|
160
|
-
if (
|
|
201
|
+
if (mode === 'before') return { start: anchorRange.start, end: anchorRange.start };
|
|
202
|
+
if (mode === 'after') {
|
|
161
203
|
return { start: afterLineOffset(sourceText, anchorRange.end), end: afterLineOffset(sourceText, anchorRange.end) };
|
|
162
204
|
}
|
|
163
205
|
return undefined;
|
|
@@ -7,13 +7,16 @@ export function semanticEditInsertionAnchor(region, context) {
|
|
|
7
7
|
.filter((symbol) => hasSymbol(context.baseSymbols, symbol));
|
|
8
8
|
const before = nearestBefore(workers, workerSymbol);
|
|
9
9
|
const after = nearestAfter(workers, workerSymbol);
|
|
10
|
-
const
|
|
11
|
-
? insertionFromSymbol('after', before, context, 'nearest-previous-base-symbol')
|
|
12
|
-
:
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
const anchorCandidates = [
|
|
11
|
+
before ? insertionFromSymbol('after', before, context, 'nearest-previous-base-symbol') : undefined,
|
|
12
|
+
after ? insertionFromSymbol('before', after, context, 'nearest-next-base-symbol') : undefined
|
|
13
|
+
].filter(Boolean);
|
|
14
|
+
const anchor = anchorCandidates.find((candidate) => candidate.headSpan)
|
|
15
|
+
?? anchorCandidates[0]
|
|
16
|
+
?? fallbackInsertion(region, context, 'no-neighbor-base-symbol');
|
|
15
17
|
return compactRecord({
|
|
16
18
|
...anchor,
|
|
19
|
+
anchorCandidates,
|
|
17
20
|
insertedSymbolId: workerSymbol.id,
|
|
18
21
|
insertedSymbolName: workerSymbol.name,
|
|
19
22
|
insertedSymbolKind: workerSymbol.kind,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
|
+
import { uniqueStrings } from '../../native-import-utils.js';
|
|
3
|
+
|
|
4
|
+
export function replayEditDiagnostics(edit, status, range, reasonCodes, sourceText) {
|
|
5
|
+
if (status === 'applied' || status === 'already-applied') return [];
|
|
6
|
+
return reasonCodes.map((code) => replayDiagnostic(code, {
|
|
7
|
+
scope: 'edit',
|
|
8
|
+
status,
|
|
9
|
+
operationId: edit.operationId,
|
|
10
|
+
sourcePath: edit.targetSourcePath ?? edit.sourcePath,
|
|
11
|
+
symbolName: edit.targetSymbolName ?? edit.symbolName,
|
|
12
|
+
symbolKind: edit.targetSymbolKind ?? edit.symbolKind,
|
|
13
|
+
editKind: edit.editKind,
|
|
14
|
+
start: range?.start,
|
|
15
|
+
end: range?.end,
|
|
16
|
+
expectedHash: replayDiagnosticExpectedHash(code, edit),
|
|
17
|
+
actualHash: replayDiagnosticActualHash(range, sourceText),
|
|
18
|
+
replacementHash: edit.replacementTextHash ?? edit.replacementSpanTextHash
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function replayEditsWithOverlapDiagnostics(edits) {
|
|
23
|
+
const overlapDiagnostics = new Map();
|
|
24
|
+
const ordered = edits
|
|
25
|
+
.filter((edit) => edit.status === 'applied' && hasNumericReplayRange(edit))
|
|
26
|
+
.sort((left, right) => left.start - right.start || left.end - right.end || (left.editOrder ?? 0) - (right.editOrder ?? 0));
|
|
27
|
+
for (let leftIndex = 0; leftIndex < ordered.length; leftIndex += 1) {
|
|
28
|
+
for (let rightIndex = leftIndex + 1; rightIndex < ordered.length; rightIndex += 1) {
|
|
29
|
+
const left = ordered[leftIndex];
|
|
30
|
+
const right = ordered[rightIndex];
|
|
31
|
+
if (!rangesOverlap(left, right)) continue;
|
|
32
|
+
const operationIds = [left.operationId, right.operationId].filter(Boolean);
|
|
33
|
+
const fallbackId = `${left.editOrder ?? leftIndex}:${right.editOrder ?? rightIndex}`;
|
|
34
|
+
const code = `replay-edit-overlap:${operationIds.join(':') || fallbackId}`;
|
|
35
|
+
appendOverlapDiagnostic(overlapDiagnostics, left, code, operationIds);
|
|
36
|
+
appendOverlapDiagnostic(overlapDiagnostics, right, code, operationIds);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!overlapDiagnostics.size) return edits;
|
|
40
|
+
return edits.map((edit) => {
|
|
41
|
+
const diagnostics = overlapDiagnostics.get(edit);
|
|
42
|
+
if (!diagnostics) return edit;
|
|
43
|
+
return diagnostics.reduce((record, diagnostic) => appendReplayEditDiagnostic(record, diagnostic), edit);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function replayDiagnostics(input) {
|
|
48
|
+
return uniqueDiagnostics([
|
|
49
|
+
...reasonList(input.reasonCodes).map((code) => replayDiagnostic(code, {
|
|
50
|
+
scope: 'replay',
|
|
51
|
+
status: input.status,
|
|
52
|
+
sourcePath: input.sourcePath,
|
|
53
|
+
expectedHash: code === 'current-source-hash-mismatch' ? input.expectedCurrentHash : undefined,
|
|
54
|
+
actualHash: code === 'current-source-hash-mismatch' ? input.currentHash : undefined
|
|
55
|
+
})),
|
|
56
|
+
...input.edits.flatMap((edit) => edit.diagnostics ?? [])
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function appendOverlapDiagnostic(overlapDiagnostics, edit, code, operationIds) {
|
|
61
|
+
const diagnostics = overlapDiagnostics.get(edit) ?? [];
|
|
62
|
+
diagnostics.push(replayDiagnostic(code, {
|
|
63
|
+
scope: 'edit',
|
|
64
|
+
status: 'conflict',
|
|
65
|
+
operationId: edit.operationId,
|
|
66
|
+
sourcePath: edit.sourcePath,
|
|
67
|
+
symbolName: edit.symbolName,
|
|
68
|
+
symbolKind: edit.symbolKind,
|
|
69
|
+
editKind: edit.editKind,
|
|
70
|
+
start: edit.start,
|
|
71
|
+
end: edit.end,
|
|
72
|
+
overlapOperationIds: operationIds
|
|
73
|
+
}));
|
|
74
|
+
overlapDiagnostics.set(edit, diagnostics);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function appendReplayEditDiagnostic(edit, diagnostic) {
|
|
78
|
+
return compactRecord({
|
|
79
|
+
...edit,
|
|
80
|
+
status: edit.status === 'applied' ? 'conflict' : edit.status,
|
|
81
|
+
reasonCodes: reasonList([...(edit.reasonCodes ?? []), diagnostic.code]),
|
|
82
|
+
diagnostics: uniqueDiagnostics([...(edit.diagnostics ?? []), diagnostic])
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function replayDiagnostic(code, context) {
|
|
87
|
+
const category = replayDiagnosticCategory(code, context.status);
|
|
88
|
+
return compactRecord({
|
|
89
|
+
code,
|
|
90
|
+
category,
|
|
91
|
+
severity: replayDiagnosticSeverity(code, category, context.status),
|
|
92
|
+
scope: context.scope,
|
|
93
|
+
status: context.status,
|
|
94
|
+
operationId: context.operationId,
|
|
95
|
+
sourcePath: context.sourcePath,
|
|
96
|
+
symbolName: context.symbolName,
|
|
97
|
+
symbolKind: context.symbolKind,
|
|
98
|
+
editKind: context.editKind,
|
|
99
|
+
start: context.start,
|
|
100
|
+
end: context.end,
|
|
101
|
+
expectedHash: context.expectedHash,
|
|
102
|
+
actualHash: context.actualHash,
|
|
103
|
+
replacementHash: context.replacementHash,
|
|
104
|
+
overlapOperationIds: context.overlapOperationIds
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function replayDiagnosticCategory(code, status) {
|
|
109
|
+
if (code.includes('overlap')) return 'overlap';
|
|
110
|
+
if (code.startsWith('missing-current-source') || code.startsWith('missing-head-source') || code.startsWith('missing-worker-source')) return 'missing-source';
|
|
111
|
+
if (code === 'current-symbol-anchor-missing' || code.includes('anchor-missing') || code.includes('anchor-unusable')) return 'stale-anchor';
|
|
112
|
+
if (code.includes('content-mismatch') || code.includes('hash-mismatch') || code.includes('span-not-resolvable') || code.startsWith('projection-not-') || code === 'missing-replacement-text') return 'projection-mismatch';
|
|
113
|
+
if (code.includes('reanchored')) return 'reanchored';
|
|
114
|
+
if (code.includes('matches-')) return 'matched-source';
|
|
115
|
+
if (status === 'stale') return 'stale-anchor';
|
|
116
|
+
if (status === 'conflict' || status === 'blocked') return 'projection-mismatch';
|
|
117
|
+
return 'replay';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function replayDiagnosticSeverity(code, category, status) {
|
|
121
|
+
if (code === 'current-source-hash-mismatch' && (status === 'accepted-clean' || status === 'already-applied')) return 'warning';
|
|
122
|
+
if (category === 'matched-source' || category === 'reanchored' || status === 'applied' || status === 'already-applied') return 'info';
|
|
123
|
+
if (category === 'overlap' || category === 'missing-source' || category === 'stale-anchor' || category === 'projection-mismatch') return 'error';
|
|
124
|
+
return status === 'accepted-clean' ? 'info' : 'warning';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function replayDiagnosticExpectedHash(code, edit) {
|
|
128
|
+
if (code.includes('matches-replacement')) return edit.replacementTextHash ?? edit.replacementSpanTextHash;
|
|
129
|
+
if (code.includes('content-mismatch') || code.includes('matches-deleted')) return edit.deletedTextHash ?? edit.anchorDeletedTextHash;
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function replayDiagnosticActualHash(range, sourceText) {
|
|
134
|
+
if (!range || typeof sourceText !== 'string') return undefined;
|
|
135
|
+
return hashSemanticValue(sourceText.slice(range.start, range.end));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function uniqueDiagnostics(diagnostics) {
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
const result = [];
|
|
141
|
+
for (const diagnostic of diagnostics.filter(Boolean)) {
|
|
142
|
+
const key = [
|
|
143
|
+
diagnostic.scope,
|
|
144
|
+
diagnostic.operationId,
|
|
145
|
+
diagnostic.status,
|
|
146
|
+
diagnostic.code,
|
|
147
|
+
diagnostic.start,
|
|
148
|
+
diagnostic.end,
|
|
149
|
+
(diagnostic.overlapOperationIds ?? []).join('|')
|
|
150
|
+
].join(':');
|
|
151
|
+
if (seen.has(key)) continue;
|
|
152
|
+
seen.add(key);
|
|
153
|
+
result.push(diagnostic);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function rangesOverlap(left, right) {
|
|
159
|
+
return Boolean(left && right && left.start < right.end && right.start < left.end);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function hasNumericReplayRange(edit) {
|
|
163
|
+
return typeof edit.start === 'number' && typeof edit.end === 'number';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function reasonList(values) { return uniqueStrings((values ?? []).filter(Boolean)); }
|
|
167
|
+
function compactRecord(value) { return Object.fromEntries(Object.entries(value ?? {}).filter(([, entry]) => entry !== undefined && (!Array.isArray(entry) || entry.length > 0))); }
|
|
@@ -24,7 +24,7 @@ export function spanOffsets(sourceText, span) {
|
|
|
24
24
|
const endLineStart = lineStarts[endLine - 1];
|
|
25
25
|
if (start === undefined || endLineStart === undefined) return undefined;
|
|
26
26
|
const startColumn = Math.max(1, span.startColumn ?? 1) - 1;
|
|
27
|
-
const lineEnd =
|
|
27
|
+
const lineEnd = lineContentEndOffset(sourceText, lineStarts[endLine]);
|
|
28
28
|
const endColumn = span.endColumn === undefined ? lineEnd - endLineStart : Math.max(1, span.endColumn) - 1;
|
|
29
29
|
return { start: start + startColumn, end: endLineStart + endColumn };
|
|
30
30
|
}
|
|
@@ -49,36 +49,41 @@ export function bodyContentRange(sourceText, range) {
|
|
|
49
49
|
return pair ? { start: pair.open + 1, end: pair.close } : undefined;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export function insertionOffset(sourceText, insertion) {
|
|
52
|
+
export function insertionOffset(sourceText, insertion, context = {}) {
|
|
53
53
|
if (typeof sourceText !== 'string') return { ok: false, reasonCodes: ['missing-head-source-text'] };
|
|
54
54
|
const mode = insertion?.mode;
|
|
55
55
|
if (mode === 'file-start') return { ok: true, offset: 0 };
|
|
56
56
|
if (mode === 'file-end') return { ok: true, offset: sourceText.length };
|
|
57
|
-
const
|
|
58
|
-
if (!range) return { ok: false, reasonCodes: ['insertion-anchor-not-resolvable'] };
|
|
59
|
-
if (mode === 'before') return { ok: true, offset: range.start };
|
|
60
|
-
if (mode === 'after') return { ok: true, offset: afterLineOffset(sourceText, range.end) };
|
|
57
|
+
const resolved = insertionAnchorResolution(sourceText, insertion, context);
|
|
58
|
+
if (!resolved?.range) return { ok: false, reasonCodes: ['insertion-anchor-not-resolvable'] };
|
|
59
|
+
if (resolved.mode === 'before') return { ok: true, offset: resolved.range.start };
|
|
60
|
+
if (resolved.mode === 'after') return { ok: true, offset: afterLineOffset(sourceText, resolved.range.end) };
|
|
61
61
|
return { ok: false, reasonCodes: ['insertion-mode-unsupported'] };
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function removalRange(sourceText, span) {
|
|
65
65
|
const range = { ...span };
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const next = lineBreakEndOffset(sourceText, range.end);
|
|
67
|
+
if (next !== range.end) range.end = next;
|
|
68
|
+
else {
|
|
69
|
+
const previous = previousLineBreakStartOffset(sourceText, range.start);
|
|
70
|
+
if (previous !== range.start) range.start = previous;
|
|
71
|
+
}
|
|
68
72
|
return range;
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
export function insertionReplacement(text, sourceText, offset) {
|
|
72
76
|
let replacement = String(text ?? '');
|
|
73
|
-
|
|
74
|
-
if (offset
|
|
75
|
-
if (offset
|
|
76
|
-
if (offset === sourceText.length && !
|
|
77
|
+
const newline = sourceLineEnding(sourceText);
|
|
78
|
+
if (offset > 0 && !isLineBreak(sourceText[offset - 1])) replacement = `${newline}${replacement}`;
|
|
79
|
+
if (offset < sourceText.length && !endsWithLineBreak(replacement)) replacement += newline;
|
|
80
|
+
if (offset === sourceText.length && sourceText && !endsWithLineBreak(sourceText)) replacement = `${newline}${replacement}`;
|
|
81
|
+
if (offset === sourceText.length && !endsWithLineBreak(replacement)) replacement += newline;
|
|
77
82
|
return replacement;
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
export function afterLineOffset(sourceText, offset) {
|
|
81
|
-
return sourceText
|
|
86
|
+
return lineBreakEndOffset(sourceText, offset);
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
function isProjectionCoverableContainer(operation) {
|
|
@@ -112,15 +117,89 @@ function containedRange(inner, outer) {
|
|
|
112
117
|
|
|
113
118
|
function insertionRemovalRange(sourceText, span, container) {
|
|
114
119
|
const range = { ...span };
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
const next = lineBreakEndOffset(sourceText, range.end);
|
|
121
|
+
if (next !== range.end && next <= container.end) range.end = next;
|
|
122
|
+
else {
|
|
123
|
+
const previous = previousLineBreakStartOffset(sourceText, range.start);
|
|
124
|
+
if (previous !== range.start && previous >= container.start) range.start = previous;
|
|
125
|
+
}
|
|
117
126
|
return range;
|
|
118
127
|
}
|
|
119
128
|
|
|
129
|
+
function lineContentEndOffset(sourceText, nextLineStart) {
|
|
130
|
+
if (nextLineStart === undefined) return sourceText.length;
|
|
131
|
+
const lineBreakStart = sourceText[nextLineStart - 2] === '\r' ? nextLineStart - 2 : nextLineStart - 1;
|
|
132
|
+
return Math.max(0, lineBreakStart);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function lineBreakEndOffset(sourceText, offset) {
|
|
136
|
+
if (sourceText[offset] === '\r' && sourceText[offset + 1] === '\n') return offset + 2;
|
|
137
|
+
if (isLineBreak(sourceText[offset])) return offset + 1;
|
|
138
|
+
return offset;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function previousLineBreakStartOffset(sourceText, offset) {
|
|
142
|
+
if (sourceText[offset - 1] === '\n') return sourceText[offset - 2] === '\r' ? offset - 2 : offset - 1;
|
|
143
|
+
if (sourceText[offset - 1] === '\r') return offset - 1;
|
|
144
|
+
return offset;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function sourceLineEnding(sourceText) {
|
|
148
|
+
if (sourceText.includes('\r\n')) return '\r\n';
|
|
149
|
+
return sourceText.includes('\r') ? '\r' : '\n';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function endsWithLineBreak(value) {
|
|
153
|
+
return isLineBreak(value[value.length - 1]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isLineBreak(char) {
|
|
157
|
+
return char === '\n' || char === '\r';
|
|
158
|
+
}
|
|
159
|
+
|
|
120
160
|
function isBodyReplacement(operation) {
|
|
121
161
|
return operation.changeKind === 'modified' && (operation.kind === 'replaceBody' || operation.anchor?.regionKind === 'body');
|
|
122
162
|
}
|
|
123
163
|
|
|
164
|
+
function insertionAnchorResolution(sourceText, insertion, context) {
|
|
165
|
+
const candidates = insertionAnchorCandidates(insertion);
|
|
166
|
+
for (const candidate of candidates) {
|
|
167
|
+
const symbol = insertionAnchorSymbol(candidate, context.symbols);
|
|
168
|
+
const range = spanOffsets(sourceText, symbol?.sourceSpan);
|
|
169
|
+
if (range) return { mode: candidate.mode, range };
|
|
170
|
+
}
|
|
171
|
+
for (const candidate of candidates) {
|
|
172
|
+
const range = spanOffsets(sourceText, candidate.headSpan);
|
|
173
|
+
if (range) return { mode: candidate.mode, range };
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function insertionAnchorCandidates(insertion) {
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
const result = [];
|
|
181
|
+
for (const candidate of [insertion, ...(Array.isArray(insertion?.anchorCandidates) ? insertion.anchorCandidates : [])]) {
|
|
182
|
+
if (!candidate || (candidate.mode !== 'before' && candidate.mode !== 'after')) continue;
|
|
183
|
+
const key = [candidate.mode, candidate.anchorKey, candidate.anchorSymbolId, candidate.anchorSymbolName, candidate.anchorSymbolKind].join('\0');
|
|
184
|
+
if (seen.has(key)) continue;
|
|
185
|
+
seen.add(key);
|
|
186
|
+
result.push(candidate);
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function insertionAnchorSymbol(candidate, symbols) {
|
|
192
|
+
const symbolList = Array.isArray(symbols)
|
|
193
|
+
? symbols
|
|
194
|
+
: symbols?.values
|
|
195
|
+
? [...symbols.values()]
|
|
196
|
+
: [];
|
|
197
|
+
const keys = [candidate.anchorKey, candidate.anchorSymbolId].filter(Boolean);
|
|
198
|
+
const exact = symbolList.find((symbol) => [symbol.ownershipKey, symbol.key, symbol.id].some((key) => key && keys.includes(key)));
|
|
199
|
+
if (exact) return exact;
|
|
200
|
+
return symbolList.find((symbol) => symbol.name === candidate.anchorSymbolName && (!candidate.anchorSymbolKind || symbol.kind === candidate.anchorSymbolKind));
|
|
201
|
+
}
|
|
202
|
+
|
|
124
203
|
function trailingBodyCloseOffset(sourceText, range) {
|
|
125
204
|
if (typeof sourceText !== 'string' || !range) return undefined;
|
|
126
205
|
let index = range.end - 1;
|
|
@@ -137,12 +137,17 @@ function resolveIndexKeys(values, resolutions, options) {
|
|
|
137
137
|
const key = String(value);
|
|
138
138
|
const resolution = resolutions.get(key);
|
|
139
139
|
if (!resolution) return [key];
|
|
140
|
+
const current = resolution.currentAnchors.map((anchor) => anchor.key).filter(Boolean);
|
|
140
141
|
if (resolution.status === 'deleted' && options.keepDeletedAnchors !== true) return [];
|
|
141
142
|
if (resolution.status === 'cycle' && options.keepBlockedAnchors !== true) return [];
|
|
142
143
|
if (resolution.status === 'max-depth' && options.keepBlockedAnchors !== true) return [];
|
|
143
144
|
if (resolution.status === 'not-found' && options.keepUnresolvedAnchors !== true) return [];
|
|
145
|
+
if (resolution.status === 'ambiguous' && resolutionHasInactiveTerminal(resolution)) {
|
|
146
|
+
if (options.keepCandidateAnchors === true) return current.length ? current : [key];
|
|
147
|
+
if (options.keepDeletedAnchors === true || options.keepInactiveAnchors === true) return [key];
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
144
150
|
if (resolution.status === 'ambiguous' && options.keepCandidateAnchors === false) return [];
|
|
145
|
-
const current = resolution.currentAnchors.map((anchor) => anchor.key).filter(Boolean);
|
|
146
151
|
return current.length ? current : [key];
|
|
147
152
|
}));
|
|
148
153
|
}
|
|
@@ -193,7 +198,10 @@ function createAnchorInventory(resolutions) {
|
|
|
193
198
|
const current = resolution.currentAnchors.map((anchor) => anchorEntry(anchor, resolution));
|
|
194
199
|
if (resolution.status === 'ambiguous') {
|
|
195
200
|
inventory.candidate.push(...current);
|
|
196
|
-
if (start)
|
|
201
|
+
if (start) {
|
|
202
|
+
inventory.inactive.push(start);
|
|
203
|
+
if (resolutionHasDeletedTerminal(resolution)) inventory.deleted.push(start);
|
|
204
|
+
}
|
|
197
205
|
continue;
|
|
198
206
|
}
|
|
199
207
|
if (resolution.status === 'deleted') {
|
|
@@ -254,6 +262,17 @@ function anchorEntry(anchor, resolution) {
|
|
|
254
262
|
});
|
|
255
263
|
}
|
|
256
264
|
|
|
265
|
+
function resolutionHasInactiveTerminal(resolution) {
|
|
266
|
+
return array(resolution.terminalEventIds).length > 0
|
|
267
|
+
|| resolutionHasDeletedTerminal(resolution)
|
|
268
|
+
|| array(resolution.reasonCodes).some((code) => code === 'lineage-event-without-target-anchor' || code === 'inactive-anchor-has-active-candidates');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function resolutionHasDeletedTerminal(resolution) {
|
|
272
|
+
return array(resolution.lineageEventKinds).includes('deleted')
|
|
273
|
+
|| array(resolution.reasonCodes).includes('anchor-deleted');
|
|
274
|
+
}
|
|
275
|
+
|
|
257
276
|
function queryAnchor(resolution) {
|
|
258
277
|
return compactRecord({
|
|
259
278
|
key: resolution.query?.anchorKey,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { uniqueStrings } from '../../native-import-utils.js';
|
|
2
|
+
|
|
3
|
+
export function addIdentityHashEvidence(before, after, add, note) {
|
|
4
|
+
const matches = matchingIdentityHashReasons(before, after);
|
|
5
|
+
if (matches.length === 0) return;
|
|
6
|
+
if (!compatibleLineageSurface(before, after)) {
|
|
7
|
+
note('identity-hash-match-surface-mismatch');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const primary = matches.includes('semantic-identity-hash-match')
|
|
11
|
+
? 'semantic-identity-hash-match'
|
|
12
|
+
: matches.includes('source-identity-hash-match')
|
|
13
|
+
? 'source-identity-hash-match'
|
|
14
|
+
: 'identity-hash-match';
|
|
15
|
+
add(0.62, primary);
|
|
16
|
+
for (const reason of matches) {
|
|
17
|
+
if (reason !== primary) note(reason);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function addSourceHashEvidence(before, after, add, note, reasons, sameSymbolSurface) {
|
|
22
|
+
const beforeHash = firstString(before.anchor.sourceHash, before.sourceHash);
|
|
23
|
+
const afterHash = firstString(after.anchor.sourceHash, after.sourceHash);
|
|
24
|
+
if (!beforeHash || !afterHash) return;
|
|
25
|
+
if (beforeHash !== afterHash) {
|
|
26
|
+
note('source-hash-changed');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!hasSourceHashSupport(before, after, reasons, sameSymbolSurface)) {
|
|
30
|
+
note('source-hash-match-without-lineage-support');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
add(0.04, 'source-hash-match');
|
|
34
|
+
if (before.anchor.sourcePath && after.anchor.sourcePath && before.anchor.sourcePath !== after.anchor.sourcePath) {
|
|
35
|
+
note('source-hash-preserved-across-path');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function hashEvidenceSummary(reasons) {
|
|
40
|
+
return {
|
|
41
|
+
semanticIdentityHashMatch: reasons.includes('semantic-identity-hash-match'),
|
|
42
|
+
sourceIdentityHashMatch: reasons.includes('source-identity-hash-match'),
|
|
43
|
+
identityHashMatch: reasons.includes('identity-hash-match'),
|
|
44
|
+
sourceHashMatch: reasons.includes('source-hash-match'),
|
|
45
|
+
signatureHashMatch: reasons.includes('signature-hash-match'),
|
|
46
|
+
bodyHashMatch: reasons.includes('body-hash-match')
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function matchingIdentityHashReasons(before, after) {
|
|
51
|
+
const beforeHashes = identityHashEntries(before);
|
|
52
|
+
const afterHashes = new Map(identityHashEntries(after).map((entry) => [entry.value, entry.reason]));
|
|
53
|
+
const reasons = [];
|
|
54
|
+
for (const entry of beforeHashes) {
|
|
55
|
+
const afterReason = afterHashes.get(entry.value);
|
|
56
|
+
if (!afterReason) continue;
|
|
57
|
+
reasons.push(identityHashMatchReason(entry.reason, afterReason));
|
|
58
|
+
}
|
|
59
|
+
return uniqueStrings(reasons);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function identityHashEntries(symbol) {
|
|
63
|
+
return [
|
|
64
|
+
{ reason: 'semantic-identity-hash-match', value: firstString(symbol.semanticIdentityHash, symbol.anchor.metadata?.semanticIdentityHash) },
|
|
65
|
+
{ reason: 'source-identity-hash-match', value: firstString(symbol.sourceIdentityHash, symbol.anchor.metadata?.sourceIdentityHash) },
|
|
66
|
+
{ reason: 'identity-hash-match', value: firstString(symbol.identityHash, symbol.anchor.metadata?.identityHash) }
|
|
67
|
+
].filter((entry) => entry.value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function identityHashMatchReason(beforeReason, afterReason) {
|
|
71
|
+
if (beforeReason === afterReason) return beforeReason;
|
|
72
|
+
if (beforeReason === 'semantic-identity-hash-match' || afterReason === 'semantic-identity-hash-match') return 'semantic-identity-hash-match';
|
|
73
|
+
if (beforeReason === 'source-identity-hash-match' || afterReason === 'source-identity-hash-match') return 'source-identity-hash-match';
|
|
74
|
+
return 'identity-hash-match';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function compatibleLineageSurface(before, after) {
|
|
78
|
+
return (!before.language || !after.language || before.language === after.language)
|
|
79
|
+
&& (!before.kind || !after.kind || before.kind === after.kind)
|
|
80
|
+
&& (!before.anchor.kind || !after.anchor.kind || before.anchor.kind === after.anchor.kind);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hasSourceHashSupport(before, after, reasons, sameSymbolSurface) {
|
|
84
|
+
return reasons.some((reason) => [
|
|
85
|
+
'semantic-identity-hash-match',
|
|
86
|
+
'source-identity-hash-match',
|
|
87
|
+
'identity-hash-match',
|
|
88
|
+
'signature-hash-match',
|
|
89
|
+
'body-hash-match',
|
|
90
|
+
'symbol-name-match'
|
|
91
|
+
].includes(reason))
|
|
92
|
+
|| sameSymbolSurface(before, after);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function firstString(...values) {
|
|
96
|
+
return values.map((value) => value === undefined || value === null ? '' : String(value)).find(Boolean);
|
|
97
|
+
}
|