@gmickel/gno 1.31.0 → 1.32.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.
- package/README.md +5 -4
- package/assets/skill/SKILL.md +23 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.31.0.zip → gno-browser-clipper-v1.32.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +19 -0
- package/spec/db/schema.sql +55 -0
- package/spec/mcp.md +114 -11
- package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
- package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
- package/src/core/document-capabilities.ts +13 -0
- package/src/core/file-ops.ts +129 -1
- package/src/core/file-refactor-adapter.ts +329 -0
- package/src/core/file-refactor-apply-edits.ts +61 -0
- package/src/core/file-refactor-apply-fs.ts +512 -0
- package/src/core/file-refactor-apply-safety.ts +340 -0
- package/src/core/file-refactor-apply-validate.ts +401 -0
- package/src/core/file-refactor-contract.ts +486 -0
- package/src/core/file-refactor-destination.ts +123 -0
- package/src/core/file-refactor-from-snapshot.ts +148 -0
- package/src/core/file-refactor-journal-port.ts +150 -0
- package/src/core/file-refactor-journal.ts +347 -0
- package/src/core/file-refactor-paths.ts +60 -0
- package/src/core/file-refactor-plan-classify.ts +208 -0
- package/src/core/file-refactor-plan-validate.ts +169 -0
- package/src/core/file-refactor-planner-types.ts +62 -0
- package/src/core/file-refactor-planner.ts +423 -0
- package/src/core/file-refactor-resolve.ts +280 -0
- package/src/core/file-refactor-service.ts +468 -0
- package/src/core/file-refactors.ts +84 -56
- package/src/core/link-destination-parse.ts +275 -0
- package/src/core/link-inventory-markdown.ts +454 -0
- package/src/core/link-inventory-opaque.ts +244 -0
- package/src/core/link-inventory-types.ts +47 -0
- package/src/core/link-inventory.ts +182 -0
- package/src/core/link-relevance.ts +150 -0
- package/src/mcp/tools/index.ts +18 -17
- package/src/mcp/tools/workspace-write.ts +215 -97
- package/src/sdk/client.ts +167 -115
- package/src/sdk/index.ts +7 -0
- package/src/sdk/types.ts +32 -2
- package/src/serve/file-refactor-http.ts +239 -0
- package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/DocView.tsx +176 -41
- package/src/serve/routes/api.ts +191 -104
- package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +452 -0
- package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
- package/src/store/types.ts +84 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.31.0.zip.sha256 +0 -1
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wiki / markdown / reference-definition inventory scanners.
|
|
3
|
+
*
|
|
4
|
+
* @module src/core/link-inventory-markdown
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ExcludedRange } from "../ingestion/strip";
|
|
8
|
+
import type { LinkInventoryToken } from "./link-inventory-types";
|
|
9
|
+
|
|
10
|
+
import { offsetToPosition } from "../ingestion/position";
|
|
11
|
+
import { rangeIntersectsExcluded } from "../ingestion/strip";
|
|
12
|
+
import {
|
|
13
|
+
detectEncodingStyle,
|
|
14
|
+
parseParenthesizedDestination,
|
|
15
|
+
splitDestinationPath,
|
|
16
|
+
stripAngleBracketDestination,
|
|
17
|
+
unescapeCommonMarkDestination,
|
|
18
|
+
} from "./link-destination-parse";
|
|
19
|
+
import {
|
|
20
|
+
codeContextReason,
|
|
21
|
+
findExcludedKind,
|
|
22
|
+
makeOpaqueToken,
|
|
23
|
+
pushInventoryToken,
|
|
24
|
+
} from "./link-inventory-opaque";
|
|
25
|
+
import { isRelevantDestination } from "./link-relevance";
|
|
26
|
+
import { parseTargetParts } from "./links";
|
|
27
|
+
|
|
28
|
+
const EXTERNAL_URL_REGEX = /^[a-z][a-z0-9+.-]*:/i;
|
|
29
|
+
|
|
30
|
+
export function inventoryWikiLinks(
|
|
31
|
+
markdown: string,
|
|
32
|
+
lineOffsets: number[],
|
|
33
|
+
sourceKeys: ReadonlySet<string>,
|
|
34
|
+
excluded: ExcludedRange[],
|
|
35
|
+
tokens: LinkInventoryToken[],
|
|
36
|
+
truncated: { value: boolean },
|
|
37
|
+
consumed: Set<number>
|
|
38
|
+
): boolean {
|
|
39
|
+
const wikiRegex = /\[\[([^\]\n]*?)\]\]/g;
|
|
40
|
+
let match: RegExpExecArray | null;
|
|
41
|
+
while ((match = wikiRegex.exec(markdown)) !== null) {
|
|
42
|
+
const startOffset = match.index;
|
|
43
|
+
const endOffset = startOffset + match[0].length;
|
|
44
|
+
if (consumed.has(startOffset)) continue;
|
|
45
|
+
if (startOffset > 0 && markdown[startOffset - 1] === "!") continue;
|
|
46
|
+
if (
|
|
47
|
+
markdown.slice(Math.max(0, startOffset - 2), startOffset) === "](" &&
|
|
48
|
+
markdown.slice(endOffset, endOffset + 1) === ")"
|
|
49
|
+
) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const content = match[1] ?? "";
|
|
54
|
+
const pipeIndex = content.indexOf("|");
|
|
55
|
+
const targetPart = pipeIndex >= 0 ? content.slice(0, pipeIndex) : content;
|
|
56
|
+
const trimmedTarget = targetPart.trim();
|
|
57
|
+
if (!trimmedTarget) continue;
|
|
58
|
+
const leadingWs = targetPart.length - targetPart.trimStart().length;
|
|
59
|
+
const parts = parseTargetParts(trimmedTarget);
|
|
60
|
+
if (!parts.ref || !isRelevantDestination(parts.ref, sourceKeys)) continue;
|
|
61
|
+
|
|
62
|
+
const destStart = startOffset + 2 + leadingWs;
|
|
63
|
+
const destEnd = destStart + parts.ref.length;
|
|
64
|
+
const codeReason = codeContextReason(
|
|
65
|
+
findExcludedKind(startOffset, endOffset, excluded)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (codeReason) {
|
|
69
|
+
if (
|
|
70
|
+
!pushInventoryToken(
|
|
71
|
+
tokens,
|
|
72
|
+
truncated,
|
|
73
|
+
makeOpaqueToken(
|
|
74
|
+
markdown,
|
|
75
|
+
lineOffsets,
|
|
76
|
+
"opaque",
|
|
77
|
+
"unchanged",
|
|
78
|
+
codeReason,
|
|
79
|
+
startOffset,
|
|
80
|
+
endOffset,
|
|
81
|
+
destStart,
|
|
82
|
+
destEnd
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
for (let i = startOffset; i < endOffset; i += 1) consumed.add(i);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (findExcludedKind(startOffset, endOffset, excluded)) continue;
|
|
92
|
+
if (
|
|
93
|
+
EXTERNAL_URL_REGEX.test(trimmedTarget) ||
|
|
94
|
+
trimmedTarget.startsWith("//")
|
|
95
|
+
) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const startPos = offsetToPosition(startOffset, lineOffsets);
|
|
100
|
+
const endPos = offsetToPosition(endOffset, lineOffsets);
|
|
101
|
+
if (
|
|
102
|
+
!pushInventoryToken(tokens, truncated, {
|
|
103
|
+
kind: "wiki",
|
|
104
|
+
raw: match[0],
|
|
105
|
+
originalDestination: parts.ref,
|
|
106
|
+
destinationStart: destStart,
|
|
107
|
+
destinationEnd: destEnd,
|
|
108
|
+
startOffset,
|
|
109
|
+
endOffset,
|
|
110
|
+
startLine: startPos.line,
|
|
111
|
+
startCol: startPos.col,
|
|
112
|
+
endLine: endPos.line,
|
|
113
|
+
endCol: endPos.col,
|
|
114
|
+
targetRef: parts.ref,
|
|
115
|
+
targetAnchor: parts.anchor,
|
|
116
|
+
targetCollection: parts.collection,
|
|
117
|
+
hadLeadingDotSlash: parts.ref.startsWith("./"),
|
|
118
|
+
encodingStyle: detectEncodingStyle(parts.ref),
|
|
119
|
+
})
|
|
120
|
+
) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
for (let i = startOffset; i < endOffset; i += 1) consumed.add(i);
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function inventoryReferenceDefinitions(
|
|
129
|
+
markdown: string,
|
|
130
|
+
lineOffsets: number[],
|
|
131
|
+
sourceKeys: ReadonlySet<string>,
|
|
132
|
+
excluded: ExcludedRange[],
|
|
133
|
+
tokens: LinkInventoryToken[],
|
|
134
|
+
truncated: { value: boolean },
|
|
135
|
+
consumed: Set<number>
|
|
136
|
+
): boolean {
|
|
137
|
+
let lineStart = 0;
|
|
138
|
+
while (lineStart <= markdown.length) {
|
|
139
|
+
const lineEndIdx = markdown.indexOf("\n", lineStart);
|
|
140
|
+
const lineEnd = lineEndIdx >= 0 ? lineEndIdx : markdown.length;
|
|
141
|
+
const line = markdown.slice(lineStart, lineEnd);
|
|
142
|
+
const defMatch = /^ {0,3}\[([^\]]+)\]:\s*/.exec(line);
|
|
143
|
+
if (defMatch) {
|
|
144
|
+
const startOffset = lineStart;
|
|
145
|
+
const endOffset = lineEnd;
|
|
146
|
+
if (!consumed.has(startOffset)) {
|
|
147
|
+
let cursor = defMatch[0].length;
|
|
148
|
+
while (cursor < line.length && /[ \t]/.test(line[cursor] ?? "")) {
|
|
149
|
+
cursor += 1;
|
|
150
|
+
}
|
|
151
|
+
let destRaw = "";
|
|
152
|
+
let pathStartInLine = cursor;
|
|
153
|
+
let angleBrackets = false;
|
|
154
|
+
|
|
155
|
+
if (line[cursor] === "<") {
|
|
156
|
+
const close = line.indexOf(">", cursor + 1);
|
|
157
|
+
if (close >= 0) {
|
|
158
|
+
angleBrackets = true;
|
|
159
|
+
destRaw = line.slice(cursor, close + 1);
|
|
160
|
+
cursor = close + 1;
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
let i = cursor;
|
|
164
|
+
while (i < line.length) {
|
|
165
|
+
const ch = line[i];
|
|
166
|
+
if (ch === "\\" && i + 1 < line.length) {
|
|
167
|
+
i += 2;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (ch === " " || ch === "\t") break;
|
|
171
|
+
i += 1;
|
|
172
|
+
}
|
|
173
|
+
destRaw = line.slice(cursor, i);
|
|
174
|
+
cursor = i;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (destRaw) {
|
|
178
|
+
const angled = stripAngleBracketDestination(destRaw);
|
|
179
|
+
const destParts = splitDestinationPath(
|
|
180
|
+
unescapeCommonMarkDestination(angled.path)
|
|
181
|
+
);
|
|
182
|
+
if (destParts.path) {
|
|
183
|
+
let pathStart = startOffset + pathStartInLine;
|
|
184
|
+
let pathEnd = pathStart + destRaw.length;
|
|
185
|
+
if (angleBrackets) {
|
|
186
|
+
pathStart += 1;
|
|
187
|
+
const innerSplit = splitDestinationPath(angled.path);
|
|
188
|
+
pathEnd = pathStart + innerSplit.path.length;
|
|
189
|
+
} else {
|
|
190
|
+
const rawSplit = splitDestinationPath(destRaw);
|
|
191
|
+
pathEnd = pathStart + rawSplit.path.length;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const pathForRelevance = angleBrackets
|
|
195
|
+
? destParts.path
|
|
196
|
+
: splitDestinationPath(destRaw).path;
|
|
197
|
+
const originalDestination = markdown.slice(pathStart, pathEnd);
|
|
198
|
+
const codeReason = codeContextReason(
|
|
199
|
+
findExcludedKind(startOffset, endOffset, excluded)
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (
|
|
203
|
+
codeReason &&
|
|
204
|
+
isRelevantDestination(pathForRelevance, sourceKeys)
|
|
205
|
+
) {
|
|
206
|
+
if (
|
|
207
|
+
!pushInventoryToken(
|
|
208
|
+
tokens,
|
|
209
|
+
truncated,
|
|
210
|
+
makeOpaqueToken(
|
|
211
|
+
markdown,
|
|
212
|
+
lineOffsets,
|
|
213
|
+
"opaque",
|
|
214
|
+
"unchanged",
|
|
215
|
+
codeReason,
|
|
216
|
+
startOffset,
|
|
217
|
+
endOffset,
|
|
218
|
+
pathStart,
|
|
219
|
+
pathEnd
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
} else if (
|
|
226
|
+
!rangeIntersectsExcluded(startOffset, endOffset, excluded)
|
|
227
|
+
) {
|
|
228
|
+
if (EXTERNAL_URL_REGEX.test(destParts.path)) {
|
|
229
|
+
if (isRelevantDestination(pathForRelevance, sourceKeys)) {
|
|
230
|
+
if (
|
|
231
|
+
!pushInventoryToken(
|
|
232
|
+
tokens,
|
|
233
|
+
truncated,
|
|
234
|
+
makeOpaqueToken(
|
|
235
|
+
markdown,
|
|
236
|
+
lineOffsets,
|
|
237
|
+
"markdown_definition",
|
|
238
|
+
"unchanged",
|
|
239
|
+
"external_destination",
|
|
240
|
+
startOffset,
|
|
241
|
+
endOffset,
|
|
242
|
+
pathStart,
|
|
243
|
+
pathEnd
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} else if (isRelevantDestination(pathForRelevance, sourceKeys)) {
|
|
251
|
+
const startPos = offsetToPosition(startOffset, lineOffsets);
|
|
252
|
+
const endPos = offsetToPosition(endOffset, lineOffsets);
|
|
253
|
+
if (
|
|
254
|
+
!pushInventoryToken(tokens, truncated, {
|
|
255
|
+
kind: "markdown_definition",
|
|
256
|
+
reasonCode: "reference_definition_site",
|
|
257
|
+
raw: markdown.slice(startOffset, endOffset),
|
|
258
|
+
originalDestination,
|
|
259
|
+
destinationStart: pathStart,
|
|
260
|
+
destinationEnd: pathEnd,
|
|
261
|
+
startOffset,
|
|
262
|
+
endOffset,
|
|
263
|
+
startLine: startPos.line,
|
|
264
|
+
startCol: startPos.col,
|
|
265
|
+
endLine: endPos.line,
|
|
266
|
+
endCol: endPos.col,
|
|
267
|
+
targetRef: originalDestination,
|
|
268
|
+
targetAnchor: destParts.anchor,
|
|
269
|
+
targetQuery: destParts.query,
|
|
270
|
+
hadLeadingDotSlash: destParts.path.startsWith("./"),
|
|
271
|
+
encodingStyle: detectEncodingStyle(originalDestination),
|
|
272
|
+
})
|
|
273
|
+
) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (lineEndIdx < 0) break;
|
|
284
|
+
lineStart = lineEndIdx + 1;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function inventoryInlineMarkdown(
|
|
290
|
+
markdown: string,
|
|
291
|
+
lineOffsets: number[],
|
|
292
|
+
sourceKeys: ReadonlySet<string>,
|
|
293
|
+
excluded: ExcludedRange[],
|
|
294
|
+
tokens: LinkInventoryToken[],
|
|
295
|
+
truncated: { value: boolean },
|
|
296
|
+
consumed: Set<number>
|
|
297
|
+
): boolean {
|
|
298
|
+
let searchFrom = 0;
|
|
299
|
+
while (searchFrom < markdown.length) {
|
|
300
|
+
const labelOpen = markdown.indexOf("[", searchFrom);
|
|
301
|
+
if (labelOpen < 0) break;
|
|
302
|
+
if (labelOpen > 0 && markdown[labelOpen - 1] === "!") {
|
|
303
|
+
searchFrom = labelOpen + 1;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const labelClose = markdown.indexOf("]", labelOpen + 1);
|
|
307
|
+
if (labelClose < 0) break;
|
|
308
|
+
if (markdown[labelClose + 1] !== "(") {
|
|
309
|
+
searchFrom = labelOpen + 1;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const parsed = parseParenthesizedDestination(markdown, labelClose + 1);
|
|
313
|
+
if (!parsed) {
|
|
314
|
+
searchFrom = labelOpen + 1;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const startOffset = labelOpen;
|
|
318
|
+
const endOffset = parsed.closeParenOffset + 1;
|
|
319
|
+
searchFrom = endOffset;
|
|
320
|
+
if (consumed.has(startOffset)) continue;
|
|
321
|
+
|
|
322
|
+
const destRaw = parsed.destinationRaw.trim();
|
|
323
|
+
if (destRaw.startsWith("[[") && destRaw.endsWith("]]")) continue;
|
|
324
|
+
|
|
325
|
+
const angled = stripAngleBracketDestination(destRaw);
|
|
326
|
+
const unescapedPath = unescapeCommonMarkDestination(angled.path);
|
|
327
|
+
const destParts = splitDestinationPath(unescapedPath);
|
|
328
|
+
const pathToken = destParts.path;
|
|
329
|
+
if (!pathToken) continue;
|
|
330
|
+
|
|
331
|
+
const leading =
|
|
332
|
+
parsed.destinationRaw.length - parsed.destinationRaw.trimStart().length;
|
|
333
|
+
let destStart = parsed.destinationStart + leading;
|
|
334
|
+
let destEnd = destStart + destRaw.length;
|
|
335
|
+
if (angled.angleBrackets) {
|
|
336
|
+
destStart += 1;
|
|
337
|
+
const innerSplit = splitDestinationPath(angled.path);
|
|
338
|
+
destEnd = destStart + innerSplit.path.length;
|
|
339
|
+
} else {
|
|
340
|
+
const rawSplit = splitDestinationPath(destRaw);
|
|
341
|
+
destEnd = destStart + rawSplit.path.length;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const originalDestination = markdown.slice(destStart, destEnd);
|
|
345
|
+
if (
|
|
346
|
+
!isRelevantDestination(originalDestination, sourceKeys) &&
|
|
347
|
+
!isRelevantDestination(pathToken, sourceKeys)
|
|
348
|
+
) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const excludedKind = findExcludedKind(startOffset, endOffset, excluded);
|
|
353
|
+
const codeReason = codeContextReason(excludedKind);
|
|
354
|
+
if (codeReason) {
|
|
355
|
+
if (
|
|
356
|
+
!pushInventoryToken(
|
|
357
|
+
tokens,
|
|
358
|
+
truncated,
|
|
359
|
+
makeOpaqueToken(
|
|
360
|
+
markdown,
|
|
361
|
+
lineOffsets,
|
|
362
|
+
"opaque",
|
|
363
|
+
"unchanged",
|
|
364
|
+
codeReason,
|
|
365
|
+
startOffset,
|
|
366
|
+
endOffset,
|
|
367
|
+
destStart,
|
|
368
|
+
destEnd
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
for (let i = startOffset; i < endOffset; i += 1) consumed.add(i);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (excludedKind) continue;
|
|
378
|
+
|
|
379
|
+
if (EXTERNAL_URL_REGEX.test(pathToken) || pathToken.startsWith("//")) {
|
|
380
|
+
if (
|
|
381
|
+
!pushInventoryToken(
|
|
382
|
+
tokens,
|
|
383
|
+
truncated,
|
|
384
|
+
makeOpaqueToken(
|
|
385
|
+
markdown,
|
|
386
|
+
lineOffsets,
|
|
387
|
+
"markdown",
|
|
388
|
+
"unchanged",
|
|
389
|
+
"external_destination",
|
|
390
|
+
startOffset,
|
|
391
|
+
endOffset,
|
|
392
|
+
destStart,
|
|
393
|
+
destEnd
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const parts = parseTargetParts(pathToken);
|
|
403
|
+
if (parts.collection) {
|
|
404
|
+
if (
|
|
405
|
+
!pushInventoryToken(
|
|
406
|
+
tokens,
|
|
407
|
+
truncated,
|
|
408
|
+
makeOpaqueToken(
|
|
409
|
+
markdown,
|
|
410
|
+
lineOffsets,
|
|
411
|
+
"markdown",
|
|
412
|
+
"unsupported",
|
|
413
|
+
"cross_collection_unsupported",
|
|
414
|
+
startOffset,
|
|
415
|
+
endOffset,
|
|
416
|
+
destStart,
|
|
417
|
+
destEnd
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const startPos = offsetToPosition(startOffset, lineOffsets);
|
|
427
|
+
const endPos = offsetToPosition(endOffset, lineOffsets);
|
|
428
|
+
if (
|
|
429
|
+
!pushInventoryToken(tokens, truncated, {
|
|
430
|
+
kind: "markdown",
|
|
431
|
+
raw: markdown.slice(startOffset, endOffset),
|
|
432
|
+
originalDestination,
|
|
433
|
+
destinationStart: destStart,
|
|
434
|
+
destinationEnd: destEnd,
|
|
435
|
+
startOffset,
|
|
436
|
+
endOffset,
|
|
437
|
+
startLine: startPos.line,
|
|
438
|
+
startCol: startPos.col,
|
|
439
|
+
endLine: endPos.line,
|
|
440
|
+
endCol: endPos.col,
|
|
441
|
+
targetRef: originalDestination,
|
|
442
|
+
targetAnchor: destParts.anchor ?? parts.anchor,
|
|
443
|
+
targetQuery: destParts.query,
|
|
444
|
+
targetCollection: parts.collection,
|
|
445
|
+
hadLeadingDotSlash: pathToken.startsWith("./"),
|
|
446
|
+
encodingStyle: detectEncodingStyle(originalDestination),
|
|
447
|
+
})
|
|
448
|
+
) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
for (let i = startOffset; i < endOffset; i += 1) consumed.add(i);
|
|
452
|
+
}
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opaque / unsupported inventory scanners for embeds, HTML, and malformed wiki.
|
|
3
|
+
*
|
|
4
|
+
* @module src/core/link-inventory-opaque
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ExcludedRange } from "../ingestion/strip";
|
|
8
|
+
import type {
|
|
9
|
+
FileRefactorReasonCode,
|
|
10
|
+
FileRefactorReferenceClassification,
|
|
11
|
+
FileRefactorReferenceKind,
|
|
12
|
+
} from "./file-refactor-contract";
|
|
13
|
+
import type { LinkInventoryToken } from "./link-inventory-types";
|
|
14
|
+
|
|
15
|
+
import { buildLineOffsets, offsetToPosition } from "../ingestion/position";
|
|
16
|
+
import { detectEncodingStyle } from "./link-destination-parse";
|
|
17
|
+
import { LINK_INVENTORY_CAPS } from "./link-inventory-types";
|
|
18
|
+
import { isRelevantDestination } from "./link-relevance";
|
|
19
|
+
|
|
20
|
+
const HTML_HREF_REGEX = /<a\b[^>]*\bhref\s*=\s*(["'])([^"']*)\1[^>]*>/gi;
|
|
21
|
+
|
|
22
|
+
export function pushInventoryToken(
|
|
23
|
+
tokens: LinkInventoryToken[],
|
|
24
|
+
truncated: { value: boolean },
|
|
25
|
+
token: LinkInventoryToken
|
|
26
|
+
): boolean {
|
|
27
|
+
if (tokens.length >= LINK_INVENTORY_CAPS.maxTokensPerDocument) {
|
|
28
|
+
truncated.value = true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
tokens.push(token);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function makeOpaqueToken(
|
|
36
|
+
markdown: string,
|
|
37
|
+
lineOffsets: number[],
|
|
38
|
+
kind: FileRefactorReferenceKind,
|
|
39
|
+
classification: FileRefactorReferenceClassification,
|
|
40
|
+
reasonCode: FileRefactorReasonCode,
|
|
41
|
+
startOffset: number,
|
|
42
|
+
endOffset: number,
|
|
43
|
+
destinationStart: number,
|
|
44
|
+
destinationEnd: number
|
|
45
|
+
): LinkInventoryToken {
|
|
46
|
+
const startPos = offsetToPosition(startOffset, lineOffsets);
|
|
47
|
+
const endPos = offsetToPosition(endOffset, lineOffsets);
|
|
48
|
+
const originalDestination = markdown.slice(destinationStart, destinationEnd);
|
|
49
|
+
return {
|
|
50
|
+
kind,
|
|
51
|
+
classification,
|
|
52
|
+
reasonCode,
|
|
53
|
+
raw: markdown.slice(startOffset, endOffset),
|
|
54
|
+
originalDestination,
|
|
55
|
+
destinationStart,
|
|
56
|
+
destinationEnd,
|
|
57
|
+
startOffset,
|
|
58
|
+
endOffset,
|
|
59
|
+
startLine: startPos.line,
|
|
60
|
+
startCol: startPos.col,
|
|
61
|
+
endLine: endPos.line,
|
|
62
|
+
endCol: endPos.col,
|
|
63
|
+
targetRef: originalDestination,
|
|
64
|
+
hadLeadingDotSlash: originalDestination.startsWith("./"),
|
|
65
|
+
encodingStyle: detectEncodingStyle(originalDestination),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function findExcludedKind(
|
|
70
|
+
start: number,
|
|
71
|
+
end: number,
|
|
72
|
+
excluded: ExcludedRange[]
|
|
73
|
+
): ExcludedRange["kind"] | null {
|
|
74
|
+
for (const range of excluded) {
|
|
75
|
+
if (start < range.end && end > range.start) return range.kind;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Map strip exclusion kind → refactor reason for code-context opaque tokens. */
|
|
81
|
+
export function codeContextReason(
|
|
82
|
+
kind: ExcludedRange["kind"] | null
|
|
83
|
+
): "code_fence_context" | "inline_code_context" | null {
|
|
84
|
+
if (kind === "fenced_code") return "code_fence_context";
|
|
85
|
+
if (kind === "inline_code") return "inline_code_context";
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function inventoryEmbeds(
|
|
90
|
+
markdown: string,
|
|
91
|
+
lineOffsets: number[],
|
|
92
|
+
sourceKeys: ReadonlySet<string>,
|
|
93
|
+
excluded: ExcludedRange[],
|
|
94
|
+
tokens: LinkInventoryToken[],
|
|
95
|
+
truncated: { value: boolean },
|
|
96
|
+
consumed: Set<number>
|
|
97
|
+
): boolean {
|
|
98
|
+
const embedRegex = /!\[\[([^\]]*)\]\]/g;
|
|
99
|
+
let match: RegExpExecArray | null;
|
|
100
|
+
while ((match = embedRegex.exec(markdown)) !== null) {
|
|
101
|
+
const startOffset = match.index;
|
|
102
|
+
const endOffset = startOffset + match[0].length;
|
|
103
|
+
const inner = match[1] ?? "";
|
|
104
|
+
const pipe = inner.indexOf("|");
|
|
105
|
+
const hash = inner.indexOf("#");
|
|
106
|
+
let destEndInInner = inner.length;
|
|
107
|
+
if (pipe >= 0) destEndInInner = Math.min(destEndInInner, pipe);
|
|
108
|
+
if (hash >= 0) destEndInInner = Math.min(destEndInInner, hash);
|
|
109
|
+
const destStart = startOffset + 3;
|
|
110
|
+
const destEnd = destStart + destEndInInner;
|
|
111
|
+
if (
|
|
112
|
+
!isRelevantDestination(markdown.slice(destStart, destEnd), sourceKeys)
|
|
113
|
+
) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
for (let i = startOffset; i < endOffset; i += 1) consumed.add(i);
|
|
117
|
+
const codeReason = codeContextReason(
|
|
118
|
+
findExcludedKind(startOffset, endOffset, excluded)
|
|
119
|
+
);
|
|
120
|
+
if (
|
|
121
|
+
!pushInventoryToken(
|
|
122
|
+
tokens,
|
|
123
|
+
truncated,
|
|
124
|
+
makeOpaqueToken(
|
|
125
|
+
markdown,
|
|
126
|
+
lineOffsets,
|
|
127
|
+
"opaque",
|
|
128
|
+
codeReason ? "unchanged" : "unsupported",
|
|
129
|
+
codeReason ?? "unsupported_syntax",
|
|
130
|
+
startOffset,
|
|
131
|
+
endOffset,
|
|
132
|
+
destStart,
|
|
133
|
+
destEnd
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function inventoryHtmlHrefs(
|
|
144
|
+
markdown: string,
|
|
145
|
+
lineOffsets: number[],
|
|
146
|
+
sourceKeys: ReadonlySet<string>,
|
|
147
|
+
excluded: ExcludedRange[],
|
|
148
|
+
tokens: LinkInventoryToken[],
|
|
149
|
+
truncated: { value: boolean },
|
|
150
|
+
consumed: Set<number>
|
|
151
|
+
): boolean {
|
|
152
|
+
HTML_HREF_REGEX.lastIndex = 0;
|
|
153
|
+
let match: RegExpExecArray | null;
|
|
154
|
+
while ((match = HTML_HREF_REGEX.exec(markdown)) !== null) {
|
|
155
|
+
const startOffset = match.index;
|
|
156
|
+
const endOffset = startOffset + match[0].length;
|
|
157
|
+
const href = match[2] ?? "";
|
|
158
|
+
const hrefStartInMatch = match[0].indexOf(href);
|
|
159
|
+
if (hrefStartInMatch < 0) continue;
|
|
160
|
+
if (!isRelevantDestination(href, sourceKeys)) continue;
|
|
161
|
+
const destStart = startOffset + hrefStartInMatch;
|
|
162
|
+
const destEnd = destStart + href.length;
|
|
163
|
+
for (let i = startOffset; i < endOffset; i += 1) consumed.add(i);
|
|
164
|
+
const codeReason = codeContextReason(
|
|
165
|
+
findExcludedKind(startOffset, endOffset, excluded)
|
|
166
|
+
);
|
|
167
|
+
if (
|
|
168
|
+
!pushInventoryToken(
|
|
169
|
+
tokens,
|
|
170
|
+
truncated,
|
|
171
|
+
makeOpaqueToken(
|
|
172
|
+
markdown,
|
|
173
|
+
lineOffsets,
|
|
174
|
+
"opaque",
|
|
175
|
+
codeReason ? "unchanged" : "unsupported",
|
|
176
|
+
codeReason ?? "html_context",
|
|
177
|
+
startOffset,
|
|
178
|
+
endOffset,
|
|
179
|
+
destStart,
|
|
180
|
+
destEnd
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function inventoryMalformedWiki(
|
|
191
|
+
markdown: string,
|
|
192
|
+
lineOffsets: number[],
|
|
193
|
+
sourceKeys: ReadonlySet<string>,
|
|
194
|
+
excluded: ExcludedRange[],
|
|
195
|
+
tokens: LinkInventoryToken[],
|
|
196
|
+
truncated: { value: boolean },
|
|
197
|
+
consumed: Set<number>
|
|
198
|
+
): boolean {
|
|
199
|
+
const openWikiRegex = /\[\[/g;
|
|
200
|
+
let match: RegExpExecArray | null;
|
|
201
|
+
while ((match = openWikiRegex.exec(markdown)) !== null) {
|
|
202
|
+
const startOffset = match.index;
|
|
203
|
+
if (consumed.has(startOffset)) continue;
|
|
204
|
+
if (startOffset > 0 && markdown[startOffset - 1] === "!") continue;
|
|
205
|
+
const close = markdown.indexOf("]]", startOffset + 2);
|
|
206
|
+
const nextOpen = markdown.indexOf("[[", startOffset + 2);
|
|
207
|
+
if (findExcludedKind(startOffset, startOffset + 2, excluded)) continue;
|
|
208
|
+
if (close < 0 || (nextOpen >= 0 && nextOpen < close)) {
|
|
209
|
+
const lineEnd = markdown.indexOf("\n", startOffset);
|
|
210
|
+
const endOffset = lineEnd >= 0 ? lineEnd : markdown.length;
|
|
211
|
+
const slice = markdown.slice(startOffset, endOffset);
|
|
212
|
+
const pipe = slice.indexOf("|");
|
|
213
|
+
const destEnd =
|
|
214
|
+
startOffset + (pipe >= 0 ? pipe : Math.min(slice.length, 64));
|
|
215
|
+
const destStart = startOffset + 2;
|
|
216
|
+
const dest = markdown.slice(destStart, destEnd);
|
|
217
|
+
if (!dest || !isRelevantDestination(dest, sourceKeys)) continue;
|
|
218
|
+
for (let i = startOffset; i < endOffset; i += 1) consumed.add(i);
|
|
219
|
+
if (
|
|
220
|
+
!pushInventoryToken(
|
|
221
|
+
tokens,
|
|
222
|
+
truncated,
|
|
223
|
+
makeOpaqueToken(
|
|
224
|
+
markdown,
|
|
225
|
+
lineOffsets,
|
|
226
|
+
"opaque",
|
|
227
|
+
"malformed",
|
|
228
|
+
"malformed_syntax",
|
|
229
|
+
startOffset,
|
|
230
|
+
endOffset,
|
|
231
|
+
destStart,
|
|
232
|
+
Math.max(destStart, destEnd)
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Re-export for callers that need line offsets without importing position. */
|
|
244
|
+
export { buildLineOffsets };
|