@gmickel/gno 1.33.0 → 1.34.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 +7 -2
- package/assets/skill/SKILL.md +19 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.33.0.zip → gno-browser-clipper-v1.34.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.34.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +44 -0
- package/spec/mcp.md +21 -0
- package/spec/output-schemas/audit-report.schema.json +284 -0
- package/src/cli/commands/audit.ts +231 -0
- package/src/cli/errors.ts +9 -2
- package/src/cli/program.ts +112 -0
- package/src/core/audit-contract.ts +296 -0
- package/src/core/audit-freshness.ts +233 -0
- package/src/core/audit-links.ts +222 -0
- package/src/core/audit-provenance.ts +154 -0
- package/src/core/audit-report.ts +318 -0
- package/src/core/audit-workspace.ts +678 -0
- package/src/core/audit.ts +569 -0
- package/src/core/capture.ts +196 -3
- package/src/core/document-capabilities.ts +9 -8
- package/src/core/record-metadata.ts +33 -0
- package/src/mcp/http-egress.ts +8 -0
- package/src/mcp/tools/audit.ts +97 -0
- package/src/mcp/tools/index.ts +13 -0
- package/src/store/sqlite/adapter.ts +82 -0
- package/src/store/sqlite/graph-link-bulk-resolver.ts +191 -0
- package/src/store/sqlite/graph-link-resolver.ts +241 -2
- package/browser-extension/artifacts/gno-browser-clipper-v1.33.0.zip.sha256 +0 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/** Linear-time bulk target resolution for large graph-link inventories. */
|
|
2
|
+
|
|
3
|
+
import type { Database } from "bun:sqlite";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
GraphLinkTarget,
|
|
7
|
+
ResolvedGraphLinkTarget,
|
|
8
|
+
} from "./graph-link-resolver";
|
|
9
|
+
|
|
10
|
+
import { stripWikiMdExt } from "../../core/links";
|
|
11
|
+
|
|
12
|
+
interface IndexedDocument {
|
|
13
|
+
id: number;
|
|
14
|
+
docid: string;
|
|
15
|
+
collection: string;
|
|
16
|
+
titleNorm: string | null;
|
|
17
|
+
relNorm: string;
|
|
18
|
+
relRaw: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Lookup = Map<string, IndexedDocument[]>;
|
|
22
|
+
|
|
23
|
+
export const GRAPH_LINK_BULK_MAX_DOCUMENTS = 100_000;
|
|
24
|
+
|
|
25
|
+
const lookupKey = (collection: string, value: string): string =>
|
|
26
|
+
`${collection}\0${value}`;
|
|
27
|
+
|
|
28
|
+
const suffixes = (value: string): string[] => {
|
|
29
|
+
const output = [value];
|
|
30
|
+
let separator = value.indexOf("/");
|
|
31
|
+
while (separator >= 0) {
|
|
32
|
+
const suffix = value.slice(separator + 1);
|
|
33
|
+
if (suffix) output.push(suffix);
|
|
34
|
+
separator = value.indexOf("/", separator + 1);
|
|
35
|
+
}
|
|
36
|
+
return output;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const appendLookup = (
|
|
40
|
+
lookup: Lookup,
|
|
41
|
+
collection: string,
|
|
42
|
+
value: string,
|
|
43
|
+
document: IndexedDocument
|
|
44
|
+
): void => {
|
|
45
|
+
const key = lookupKey(collection, value);
|
|
46
|
+
const documents = lookup.get(key) ?? [];
|
|
47
|
+
documents.push(document);
|
|
48
|
+
lookup.set(key, documents);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const candidatesFor = (
|
|
52
|
+
lookup: Lookup,
|
|
53
|
+
collection: string,
|
|
54
|
+
values: readonly string[]
|
|
55
|
+
): IndexedDocument[] => {
|
|
56
|
+
const byId = new Map<number, IndexedDocument>();
|
|
57
|
+
for (const value of values) {
|
|
58
|
+
for (const document of lookup.get(lookupKey(collection, value)) ?? []) {
|
|
59
|
+
byId.set(document.id, document);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return [...byId.values()].sort((left, right) => left.id - right.id);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const resolved = (
|
|
66
|
+
documents: readonly IndexedDocument[],
|
|
67
|
+
matchRank: number
|
|
68
|
+
): ResolvedGraphLinkTarget | null => {
|
|
69
|
+
const first = documents[0];
|
|
70
|
+
if (!first) return null;
|
|
71
|
+
return {
|
|
72
|
+
targetId: first.id,
|
|
73
|
+
targetDocid: first.docid,
|
|
74
|
+
matchRank,
|
|
75
|
+
matchCount: documents.length,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a large target set with lookup tables equivalent to the ranked SQL
|
|
81
|
+
* resolver. SQLite still performs lower()/trim(), preserving its normalization
|
|
82
|
+
* semantics; resolution then scales with documents plus path segments.
|
|
83
|
+
*/
|
|
84
|
+
export const resolveGraphLinkTargetsBulk = (
|
|
85
|
+
db: Database,
|
|
86
|
+
targets: readonly GraphLinkTarget[],
|
|
87
|
+
maxDocuments = GRAPH_LINK_BULK_MAX_DOCUMENTS
|
|
88
|
+
): Array<ResolvedGraphLinkTarget | null> | null => {
|
|
89
|
+
const boundedMaxDocuments = Math.max(
|
|
90
|
+
1,
|
|
91
|
+
Math.min(GRAPH_LINK_BULK_MAX_DOCUMENTS, maxDocuments)
|
|
92
|
+
);
|
|
93
|
+
const rows = db
|
|
94
|
+
.query<
|
|
95
|
+
{
|
|
96
|
+
id: number;
|
|
97
|
+
docid: string;
|
|
98
|
+
collection: string;
|
|
99
|
+
title_norm: string | null;
|
|
100
|
+
rel_norm: string;
|
|
101
|
+
rel_raw: string;
|
|
102
|
+
},
|
|
103
|
+
[number]
|
|
104
|
+
>(
|
|
105
|
+
`SELECT id, docid, collection, lower(trim(title)) AS title_norm,
|
|
106
|
+
lower(rel_path) AS rel_norm, rel_path AS rel_raw
|
|
107
|
+
FROM documents
|
|
108
|
+
WHERE active = 1
|
|
109
|
+
ORDER BY id
|
|
110
|
+
LIMIT ?`
|
|
111
|
+
)
|
|
112
|
+
.all(boundedMaxDocuments + 1);
|
|
113
|
+
// The fast lookup-table path is intentionally bounded. Callers fall back to
|
|
114
|
+
// set-oriented SQL batches when the active index exceeds this memory cap.
|
|
115
|
+
if (rows.length > boundedMaxDocuments) return null;
|
|
116
|
+
const titleExact: Lookup = new Map();
|
|
117
|
+
const relExact: Lookup = new Map();
|
|
118
|
+
const relExactRaw: Lookup = new Map();
|
|
119
|
+
const relSuffix: Lookup = new Map();
|
|
120
|
+
for (const row of rows) {
|
|
121
|
+
const document: IndexedDocument = {
|
|
122
|
+
id: row.id,
|
|
123
|
+
docid: row.docid,
|
|
124
|
+
collection: row.collection,
|
|
125
|
+
titleNorm: row.title_norm,
|
|
126
|
+
relNorm: row.rel_norm,
|
|
127
|
+
relRaw: row.rel_raw,
|
|
128
|
+
};
|
|
129
|
+
if (document.titleNorm !== null) {
|
|
130
|
+
appendLookup(titleExact, row.collection, document.titleNorm, document);
|
|
131
|
+
}
|
|
132
|
+
appendLookup(relExact, row.collection, document.relNorm, document);
|
|
133
|
+
appendLookup(relExactRaw, row.collection, document.relRaw, document);
|
|
134
|
+
for (const suffix of suffixes(document.relNorm)) {
|
|
135
|
+
appendLookup(relSuffix, row.collection, suffix, document);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const cache = new Map<string, ResolvedGraphLinkTarget | null>();
|
|
140
|
+
return targets.map((target) => {
|
|
141
|
+
const cacheKey = JSON.stringify([
|
|
142
|
+
target.linkType,
|
|
143
|
+
target.targetCollection,
|
|
144
|
+
target.targetRefNorm,
|
|
145
|
+
]);
|
|
146
|
+
if (cache.has(cacheKey)) return cache.get(cacheKey) ?? null;
|
|
147
|
+
let result: ResolvedGraphLinkTarget | null;
|
|
148
|
+
if (target.linkType === "markdown") {
|
|
149
|
+
result = resolved(
|
|
150
|
+
candidatesFor(relExactRaw, target.targetCollection, [
|
|
151
|
+
target.targetRefNorm,
|
|
152
|
+
]),
|
|
153
|
+
5
|
|
154
|
+
);
|
|
155
|
+
} else {
|
|
156
|
+
const baseRef = stripWikiMdExt(target.targetRefNorm);
|
|
157
|
+
const baseRefMd = `${baseRef}.md`;
|
|
158
|
+
const rankedCandidates: Array<[number, Lookup, string[]]> = [
|
|
159
|
+
[1, titleExact, [baseRef]],
|
|
160
|
+
[2, titleExact, [baseRefMd]],
|
|
161
|
+
[3, titleExact, suffixes(baseRef)],
|
|
162
|
+
[
|
|
163
|
+
4,
|
|
164
|
+
titleExact,
|
|
165
|
+
suffixes(baseRefMd)
|
|
166
|
+
.filter((value) => value.endsWith(".md"))
|
|
167
|
+
.map((value) => value.slice(0, -3)),
|
|
168
|
+
],
|
|
169
|
+
[5, relExact, [baseRef]],
|
|
170
|
+
[6, relExact, [baseRefMd]],
|
|
171
|
+
[7, relSuffix, [baseRefMd]],
|
|
172
|
+
[8, relSuffix, [baseRef]],
|
|
173
|
+
[9, relExact, suffixes(baseRefMd)],
|
|
174
|
+
[10, relExact, suffixes(baseRef)],
|
|
175
|
+
];
|
|
176
|
+
result = null;
|
|
177
|
+
for (const [rank, lookup, values] of rankedCandidates) {
|
|
178
|
+
const documents = candidatesFor(
|
|
179
|
+
lookup,
|
|
180
|
+
target.targetCollection,
|
|
181
|
+
values
|
|
182
|
+
);
|
|
183
|
+
if (documents.length === 0) continue;
|
|
184
|
+
result = resolved(documents, rank);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
cache.set(cacheKey, result);
|
|
189
|
+
return result;
|
|
190
|
+
});
|
|
191
|
+
};
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { Database } from "bun:sqlite";
|
|
8
8
|
|
|
9
9
|
import { stripWikiMdExt } from "../../core/links";
|
|
10
|
+
import { resolveGraphLinkTargetsBulk } from "./graph-link-bulk-resolver";
|
|
10
11
|
|
|
11
12
|
export interface GraphLinkTarget {
|
|
12
13
|
targetRefNorm: string;
|
|
@@ -22,6 +23,60 @@ export interface ResolvedGraphLinkTarget {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const MAX_SQL_PARAMS = 900;
|
|
26
|
+
const BULK_RESOLUTION_THRESHOLD = 128;
|
|
27
|
+
export const AUDIT_LINK_SNAPSHOT_MAX_DOCUMENTS = 50_000;
|
|
28
|
+
export const AUDIT_LINK_SNAPSHOT_MAX_LINKS = 50_000;
|
|
29
|
+
|
|
30
|
+
export interface AuditLinkSnapshotDocument {
|
|
31
|
+
id: number;
|
|
32
|
+
docid: string;
|
|
33
|
+
uri: string;
|
|
34
|
+
collection: string;
|
|
35
|
+
relPath: string;
|
|
36
|
+
recordSourcePath: string | null;
|
|
37
|
+
title: string | null;
|
|
38
|
+
mirrorHash: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AuditLinkSnapshotLink {
|
|
42
|
+
sourceId: number;
|
|
43
|
+
sourceDocid: string;
|
|
44
|
+
sourceUri: string;
|
|
45
|
+
sourceCollection: string;
|
|
46
|
+
sourceRelPath: string;
|
|
47
|
+
targetRef: string;
|
|
48
|
+
targetRefNorm: string;
|
|
49
|
+
targetAnchor: string | null;
|
|
50
|
+
targetCollection: string;
|
|
51
|
+
linkType: "wiki" | "markdown";
|
|
52
|
+
startLine: number;
|
|
53
|
+
startCol: number;
|
|
54
|
+
endLine: number;
|
|
55
|
+
endCol: number;
|
|
56
|
+
resolved: ResolvedGraphLinkTarget | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface AuditLinkSnapshot {
|
|
60
|
+
documents: AuditLinkSnapshotDocument[];
|
|
61
|
+
/** Optional finding scope when documents also include graph-wide evidence. */
|
|
62
|
+
auditedDocumentIds?: number[];
|
|
63
|
+
links: AuditLinkSnapshotLink[];
|
|
64
|
+
totals: { documents: number; links: number };
|
|
65
|
+
truncated: { documents: boolean; links: boolean };
|
|
66
|
+
metrics: {
|
|
67
|
+
documentRowsExamined: number;
|
|
68
|
+
linkRowsExamined: number;
|
|
69
|
+
uniqueTargetsResolved: number;
|
|
70
|
+
batchedResolution: true;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface AuditLinkSnapshotOptions {
|
|
75
|
+
collections?: readonly string[];
|
|
76
|
+
pathPrefixes?: readonly string[];
|
|
77
|
+
maxDocuments?: number;
|
|
78
|
+
maxLinks?: number;
|
|
79
|
+
}
|
|
25
80
|
|
|
26
81
|
const chunkArray = <T>(items: T[], chunkSize: number): T[][] => {
|
|
27
82
|
const chunks: T[][] = [];
|
|
@@ -36,7 +91,7 @@ const suffixMatch = (targetExpr: string, valueExpr: string): string =>
|
|
|
36
91
|
AND (length(${targetExpr}) = length(${valueExpr})
|
|
37
92
|
OR substr(${targetExpr}, -length(${valueExpr}) - 1, 1) = '/'))`;
|
|
38
93
|
|
|
39
|
-
const
|
|
94
|
+
export const resolveGraphLinkTargetsSql = (
|
|
40
95
|
db: Database,
|
|
41
96
|
targets: GraphLinkTarget[]
|
|
42
97
|
): Array<ResolvedGraphLinkTarget | null> => {
|
|
@@ -234,8 +289,192 @@ export function resolveGraphLinkTargets(
|
|
|
234
289
|
originalToUniqueIndex.push(uniqueIndex);
|
|
235
290
|
}
|
|
236
291
|
|
|
237
|
-
|
|
292
|
+
if (uniqueTargets.length > BULK_RESOLUTION_THRESHOLD) {
|
|
293
|
+
const bulkResults = resolveGraphLinkTargetsBulk(db, uniqueTargets);
|
|
294
|
+
if (bulkResults) {
|
|
295
|
+
return originalToUniqueIndex.map(
|
|
296
|
+
(uniqueIndex) => bulkResults[uniqueIndex] ?? null
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const uniqueResults = resolveGraphLinkTargetsSql(db, uniqueTargets);
|
|
238
302
|
return originalToUniqueIndex.map(
|
|
239
303
|
(uniqueIndex) => uniqueResults[uniqueIndex] ?? null
|
|
240
304
|
);
|
|
241
305
|
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Capture one bounded, read-only link inventory using set-oriented SQL and the
|
|
309
|
+
* same target resolver as graph expansion. No query is issued per finding.
|
|
310
|
+
*/
|
|
311
|
+
export function captureAuditLinkSnapshot(
|
|
312
|
+
db: Database,
|
|
313
|
+
options: AuditLinkSnapshotOptions = {}
|
|
314
|
+
): AuditLinkSnapshot {
|
|
315
|
+
const maxDocuments = Math.max(
|
|
316
|
+
1,
|
|
317
|
+
Math.min(
|
|
318
|
+
AUDIT_LINK_SNAPSHOT_MAX_DOCUMENTS,
|
|
319
|
+
options.maxDocuments ?? AUDIT_LINK_SNAPSHOT_MAX_DOCUMENTS
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
const maxLinks = Math.max(
|
|
323
|
+
1,
|
|
324
|
+
Math.min(
|
|
325
|
+
AUDIT_LINK_SNAPSHOT_MAX_LINKS,
|
|
326
|
+
options.maxLinks ?? AUDIT_LINK_SNAPSHOT_MAX_LINKS
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
const conditions = ["d.active = 1"];
|
|
330
|
+
const params: string[] = [];
|
|
331
|
+
const collections = [...new Set(options.collections ?? [])]
|
|
332
|
+
.map((value) => value.normalize("NFC").trim())
|
|
333
|
+
.filter(Boolean)
|
|
334
|
+
.sort();
|
|
335
|
+
if (collections.length > 0) {
|
|
336
|
+
conditions.push(
|
|
337
|
+
`d.collection IN (${collections.map(() => "?").join(",")})`
|
|
338
|
+
);
|
|
339
|
+
params.push(...collections);
|
|
340
|
+
}
|
|
341
|
+
const prefixes = [...new Set(options.pathPrefixes ?? [])]
|
|
342
|
+
.map((value) => value.normalize("NFC").trim().replace(/^\/+/, ""))
|
|
343
|
+
.filter(Boolean)
|
|
344
|
+
.sort();
|
|
345
|
+
if (prefixes.length > 0) {
|
|
346
|
+
conditions.push(
|
|
347
|
+
`(${prefixes.map(() => "COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) = ? OR COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) LIKE ? ESCAPE '\\'").join(" OR ")})`
|
|
348
|
+
);
|
|
349
|
+
for (const prefix of prefixes) {
|
|
350
|
+
const escaped = prefix
|
|
351
|
+
.replaceAll("\\", "\\\\")
|
|
352
|
+
.replaceAll("%", "\\%")
|
|
353
|
+
.replaceAll("_", "\\_");
|
|
354
|
+
params.push(prefix, `${escaped}/%`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const where = conditions.join(" AND ");
|
|
358
|
+
const totalDocuments =
|
|
359
|
+
db
|
|
360
|
+
.query<{ count: number }, string[]>(
|
|
361
|
+
`SELECT COUNT(*) AS count FROM documents d WHERE ${where}`
|
|
362
|
+
)
|
|
363
|
+
.get(...params)?.count ?? 0;
|
|
364
|
+
const documentRows = db
|
|
365
|
+
.query<
|
|
366
|
+
{
|
|
367
|
+
id: number;
|
|
368
|
+
docid: string;
|
|
369
|
+
uri: string;
|
|
370
|
+
collection: string;
|
|
371
|
+
rel_path: string;
|
|
372
|
+
record_source_path: string | null;
|
|
373
|
+
title: string | null;
|
|
374
|
+
mirror_hash: string | null;
|
|
375
|
+
},
|
|
376
|
+
(string | number)[]
|
|
377
|
+
>(
|
|
378
|
+
`SELECT d.id, d.docid, d.uri, d.collection, d.rel_path, d.record_source_path, d.title, d.mirror_hash
|
|
379
|
+
FROM documents d WHERE ${where} ORDER BY d.id LIMIT ?`
|
|
380
|
+
)
|
|
381
|
+
.all(...params, maxDocuments);
|
|
382
|
+
const totalLinks =
|
|
383
|
+
db
|
|
384
|
+
.query<{ count: number }, string[]>(
|
|
385
|
+
`SELECT COUNT(*) AS count
|
|
386
|
+
FROM doc_links dl
|
|
387
|
+
JOIN documents d ON d.id = dl.source_doc_id
|
|
388
|
+
WHERE ${where} AND dl.source = 'parsed'`
|
|
389
|
+
)
|
|
390
|
+
.get(...params)?.count ?? 0;
|
|
391
|
+
const rawLinks = db
|
|
392
|
+
.query<
|
|
393
|
+
{
|
|
394
|
+
source_id: number;
|
|
395
|
+
source_docid: string;
|
|
396
|
+
source_uri: string;
|
|
397
|
+
source_collection: string;
|
|
398
|
+
source_rel_path: string;
|
|
399
|
+
target_ref: string;
|
|
400
|
+
target_ref_norm: string;
|
|
401
|
+
target_anchor: string | null;
|
|
402
|
+
target_collection: string | null;
|
|
403
|
+
link_type: "wiki" | "markdown";
|
|
404
|
+
start_line: number;
|
|
405
|
+
start_col: number;
|
|
406
|
+
end_line: number;
|
|
407
|
+
end_col: number;
|
|
408
|
+
},
|
|
409
|
+
(string | number)[]
|
|
410
|
+
>(
|
|
411
|
+
`SELECT d.id AS source_id, d.docid AS source_docid,
|
|
412
|
+
d.uri AS source_uri, d.collection AS source_collection,
|
|
413
|
+
COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) AS source_rel_path, dl.target_ref,
|
|
414
|
+
dl.target_ref_norm, dl.target_anchor, dl.target_collection,
|
|
415
|
+
dl.link_type, dl.start_line, dl.start_col,
|
|
416
|
+
dl.end_line, dl.end_col
|
|
417
|
+
FROM doc_links dl
|
|
418
|
+
JOIN documents d ON d.id = dl.source_doc_id
|
|
419
|
+
WHERE ${where} AND dl.source = 'parsed'
|
|
420
|
+
ORDER BY d.id, dl.start_line, dl.start_col, dl.id
|
|
421
|
+
LIMIT ?`
|
|
422
|
+
)
|
|
423
|
+
.all(...params, maxLinks);
|
|
424
|
+
const targets = rawLinks.map((row) => ({
|
|
425
|
+
targetRefNorm: row.target_ref_norm,
|
|
426
|
+
targetCollection: row.target_collection ?? row.source_collection,
|
|
427
|
+
linkType: row.link_type,
|
|
428
|
+
}));
|
|
429
|
+
const resolutions = resolveGraphLinkTargets(db, targets);
|
|
430
|
+
const uniqueTargetsResolved = new Set(
|
|
431
|
+
targets.map((target) =>
|
|
432
|
+
JSON.stringify([
|
|
433
|
+
target.linkType,
|
|
434
|
+
target.targetCollection,
|
|
435
|
+
target.targetRefNorm,
|
|
436
|
+
])
|
|
437
|
+
)
|
|
438
|
+
).size;
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
documents: documentRows.map((row) => ({
|
|
442
|
+
id: row.id,
|
|
443
|
+
docid: row.docid,
|
|
444
|
+
uri: row.uri,
|
|
445
|
+
collection: row.collection,
|
|
446
|
+
relPath: row.rel_path,
|
|
447
|
+
recordSourcePath: row.record_source_path,
|
|
448
|
+
title: row.title,
|
|
449
|
+
mirrorHash: row.mirror_hash,
|
|
450
|
+
})),
|
|
451
|
+
links: rawLinks.map((row, index) => ({
|
|
452
|
+
sourceId: row.source_id,
|
|
453
|
+
sourceDocid: row.source_docid,
|
|
454
|
+
sourceUri: row.source_uri,
|
|
455
|
+
sourceCollection: row.source_collection,
|
|
456
|
+
sourceRelPath: row.source_rel_path,
|
|
457
|
+
targetRef: row.target_ref,
|
|
458
|
+
targetRefNorm: row.target_ref_norm,
|
|
459
|
+
targetAnchor: row.target_anchor,
|
|
460
|
+
targetCollection: row.target_collection ?? row.source_collection,
|
|
461
|
+
linkType: row.link_type,
|
|
462
|
+
startLine: row.start_line,
|
|
463
|
+
startCol: row.start_col,
|
|
464
|
+
endLine: row.end_line,
|
|
465
|
+
endCol: row.end_col,
|
|
466
|
+
resolved: resolutions[index] ?? null,
|
|
467
|
+
})),
|
|
468
|
+
totals: { documents: totalDocuments, links: totalLinks },
|
|
469
|
+
truncated: {
|
|
470
|
+
documents: totalDocuments > documentRows.length,
|
|
471
|
+
links: totalLinks > rawLinks.length,
|
|
472
|
+
},
|
|
473
|
+
metrics: {
|
|
474
|
+
documentRowsExamined: documentRows.length,
|
|
475
|
+
linkRowsExamined: rawLinks.length,
|
|
476
|
+
uniqueTargetsResolved,
|
|
477
|
+
batchedResolution: true,
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
c398c09680951afa0b5d436f3b92cf3dfb6e49427b9df83830ce899d3b03d7e4 gno-browser-clipper-v1.33.0.zip
|