@gmickel/gno 2.7.1 → 2.8.1
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 +3 -2
- package/assets/skill/SKILL.md +11 -1
- package/assets/skill/cli-reference.md +8 -1
- package/assets/skill/examples.md +2 -1
- package/assets/skill/mcp-reference.md +3 -1
- package/assets/skill/recipes/memory-scoped-recall.md +10 -5
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.7.1.zip → gno-browser-clipper-v2.8.1.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.1.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +86 -9
- package/spec/db/schema.sql +0 -1
- package/spec/mcp.md +26 -7
- package/spec/output-schemas/audit-report.schema.json +18 -4
- package/spec/output-schemas/backlinks.schema.json +4 -0
- package/spec/output-schemas/collection-list.schema.json +13 -0
- package/spec/output-schemas/graph.schema.json +2 -0
- package/spec/output-schemas/links-list.schema.json +4 -0
- package/spec/output-schemas/memory-recall.schema.json +1 -1
- package/spec/output-schemas/status.schema.json +18 -3
- package/src/cli/commands/audit.ts +23 -4
- package/src/cli/commands/collection/list.ts +39 -5
- package/src/cli/commands/embed.ts +3 -3
- package/src/cli/commands/graph.ts +3 -1
- package/src/cli/commands/links.ts +61 -180
- package/src/cli/commands/shared.ts +7 -0
- package/src/cli/commands/status.ts +6 -0
- package/src/cli/program.ts +12 -2
- package/src/config/loader.ts +43 -0
- package/src/config/types.ts +8 -0
- package/src/core/audit-contract.ts +16 -4
- package/src/core/audit-freshness.ts +11 -1
- package/src/core/audit-links.ts +197 -25
- package/src/core/audit-outside-index.ts +215 -0
- package/src/core/audit-provenance.ts +11 -4
- package/src/core/audit-workspace.ts +30 -9
- package/src/core/audit.ts +76 -16
- package/src/core/context-compiler.ts +3 -0
- package/src/core/context-evidence.ts +11 -0
- package/src/core/graph-edge-confidence.ts +23 -1
- package/src/core/host-paths.ts +1 -0
- package/src/core/knowledge-impact.ts +28 -0
- package/src/core/link-inventory-markdown.ts +2 -3
- package/src/core/link-workspace.ts +324 -0
- package/src/core/links.ts +40 -17
- package/src/core/memory-recall.ts +254 -15
- package/src/core/memory-types.ts +12 -0
- package/src/core/memory.ts +2 -0
- package/src/core/retrieval-replay-candidate.ts +6 -0
- package/src/core/retrieval-trace-request.ts +3 -0
- package/src/index.ts +14 -1
- package/src/ingestion/graph-reconciliation.ts +77 -15
- package/src/ingestion/source-availability/darwin-path.ts +9 -3
- package/src/ingestion/sync.ts +27 -4
- package/src/ingestion/types.ts +14 -0
- package/src/llm/inference-scope.ts +4 -3
- package/src/mcp/http-egress.ts +42 -3
- package/src/mcp/tools/audit.ts +11 -2
- package/src/mcp/tools/changes.ts +1 -0
- package/src/mcp/tools/links.ts +74 -93
- package/src/mcp/tools/sessions.ts +33 -4
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/expansion.ts +19 -31
- package/src/pipeline/graph-retrieval.ts +22 -2
- package/src/pipeline/hybrid.ts +1 -1
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +10 -3
- package/src/sdk/client.ts +1 -0
- package/src/serve/findings-pass.ts +1 -1
- package/src/serve/public/components/editor/MarkdownPreview.tsx +5 -3
- package/src/serve/public/pages/GraphView.tsx +2 -0
- package/src/serve/routes/changes.ts +6 -1
- package/src/serve/routes/graph.ts +3 -1
- package/src/serve/routes/links.ts +45 -50
- package/src/serve/routes/sessions.ts +41 -53
- package/src/serve/server.ts +2 -1
- package/src/serve/status.ts +1 -0
- package/src/sessions/config-refresh.ts +111 -0
- package/src/store/migrations/033-drop-documents-active-index.ts +30 -0
- package/src/store/migrations/034-collection-link-workspace.ts +47 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +477 -337
- package/src/store/sqlite/eligibility.ts +8 -2
- package/src/store/sqlite/graph-link-resolver.ts +259 -5
- package/src/store/sqlite/graph-neighbors.ts +147 -40
- package/src/store/sqlite/graph-reference-state.ts +13 -2
- package/src/store/sqlite/graph-similarity.ts +96 -0
- package/src/store/sqlite/workspace-link-resolver.ts +742 -0
- package/src/store/types.ts +64 -5
- package/src/store/vector/stats.ts +1 -1
- package/src/store/vector/status.ts +27 -0
- package/src/store/vector/stored-vectors.ts +158 -0
- package/src/store/vector/types.ts +6 -0
- package/src/store/vector/variant-search.ts +30 -14
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.1.zip.sha256 +0 -1
|
@@ -76,6 +76,12 @@ export function buildEligibleDocumentQuery(
|
|
|
76
76
|
params.push(`%${options.author.toLowerCase()}%`);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// Without planner statistics a collection filter can outrank the hash
|
|
80
|
+
// index; a hash allowlist is always the selective predicate.
|
|
81
|
+
const hashPin =
|
|
82
|
+
options.allowedMirrorHashes === undefined
|
|
83
|
+
? ""
|
|
84
|
+
: " INDEXED BY idx_documents_mirror_hash";
|
|
79
85
|
if (options.allowedMirrorHashes !== undefined) {
|
|
80
86
|
conditions.push("d.mirror_hash IN (SELECT value FROM json_each(?))");
|
|
81
87
|
params.push(JSON.stringify(options.allowedMirrorHashes));
|
|
@@ -128,7 +134,7 @@ export function buildEligibleDocumentQuery(
|
|
|
128
134
|
(string | number)[]
|
|
129
135
|
>(`
|
|
130
136
|
SELECT d.id, d.title, d.author, d.content_type, d.categories, COALESCE(d.record_source_path, d.rel_path) AS path, cc.text
|
|
131
|
-
FROM documents d LEFT JOIN content_chunks cc ON cc.mirror_hash = d.mirror_hash
|
|
137
|
+
FROM documents d${hashPin} LEFT JOIN content_chunks cc ON cc.mirror_hash = d.mirror_hash
|
|
132
138
|
WHERE ${conditions.join(" AND ")}
|
|
133
139
|
`)
|
|
134
140
|
.iterate(...params);
|
|
@@ -180,7 +186,7 @@ export function buildEligibleDocumentQuery(
|
|
|
180
186
|
params.push(JSON.stringify([...denied]));
|
|
181
187
|
}
|
|
182
188
|
return {
|
|
183
|
-
sql: `SELECT d.id, d.mirror_hash FROM documents d WHERE ${conditions.join(" AND ")}`,
|
|
189
|
+
sql: `SELECT d.id, d.mirror_hash FROM documents d${hashPin} WHERE ${conditions.join(" AND ")}`,
|
|
184
190
|
params,
|
|
185
191
|
};
|
|
186
192
|
}
|
|
@@ -6,22 +6,82 @@
|
|
|
6
6
|
|
|
7
7
|
import type { Database } from "bun:sqlite";
|
|
8
8
|
|
|
9
|
+
import type { WorkspaceResolutionReason } from "./workspace-link-resolver";
|
|
10
|
+
|
|
9
11
|
import { stripWikiMdExt } from "../../core/links";
|
|
10
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
GRAPH_LINK_BULK_MAX_DOCUMENTS,
|
|
14
|
+
resolveGraphLinkTargetsBulk,
|
|
15
|
+
} from "./graph-link-bulk-resolver";
|
|
16
|
+
import {
|
|
17
|
+
loadLinkWorkspaceMemberships,
|
|
18
|
+
resolveWorkspaceTargets,
|
|
19
|
+
workspaceKeyForDocument,
|
|
20
|
+
workspaceMemberCollections,
|
|
21
|
+
type WorkspaceTargetInput,
|
|
22
|
+
} from "./workspace-link-resolver";
|
|
23
|
+
|
|
24
|
+
export interface GraphLinkSourceIdentity {
|
|
25
|
+
collection: string;
|
|
26
|
+
relPath: string;
|
|
27
|
+
/** The link carried an explicit `collection:` prefix. */
|
|
28
|
+
explicit: boolean;
|
|
29
|
+
}
|
|
11
30
|
|
|
12
31
|
export interface GraphLinkTarget {
|
|
13
32
|
targetRefNorm: string;
|
|
33
|
+
/** Explicit prefix collection, else the source collection. */
|
|
14
34
|
targetCollection: string;
|
|
15
35
|
linkType: "wiki" | "markdown";
|
|
36
|
+
/**
|
|
37
|
+
* Source document identity. Plain wiki links from a document inside a link
|
|
38
|
+
* workspace resolve across the workspace; without it (or for explicit,
|
|
39
|
+
* markdown and non-workspace links) resolution stays collection-scoped.
|
|
40
|
+
*/
|
|
41
|
+
source?: GraphLinkSourceIdentity;
|
|
16
42
|
}
|
|
17
43
|
|
|
44
|
+
/** Where a resolved link landed relative to its source collection. */
|
|
45
|
+
export type LinkResolutionScope =
|
|
46
|
+
| "same-collection"
|
|
47
|
+
| "cross-collection"
|
|
48
|
+
| "explicit-collection";
|
|
49
|
+
|
|
18
50
|
export interface ResolvedGraphLinkTarget {
|
|
19
51
|
targetId: number;
|
|
20
52
|
targetDocid: string;
|
|
21
53
|
matchRank: number;
|
|
22
54
|
matchCount: number;
|
|
55
|
+
/** Collection of the resolved target document. */
|
|
56
|
+
targetCollection?: string;
|
|
57
|
+
scope?: LinkResolutionScope;
|
|
58
|
+
/** Workspace resolution reason; absent for collection-scoped resolution. */
|
|
59
|
+
reason?: WorkspaceResolutionReason;
|
|
60
|
+
/** False for a tied workspace link: audit evidence only, never an edge. */
|
|
61
|
+
traversable?: boolean;
|
|
62
|
+
/** Tied candidates of a non-traversable link, canonical path order. */
|
|
63
|
+
candidates?: Array<{ id: number; docid: string; collection: string }>;
|
|
23
64
|
}
|
|
24
65
|
|
|
66
|
+
/** Resolved and allowed to become a graph edge. */
|
|
67
|
+
export const isTraversableResolution = (
|
|
68
|
+
resolution: ResolvedGraphLinkTarget | null | undefined
|
|
69
|
+
): resolution is ResolvedGraphLinkTarget =>
|
|
70
|
+
resolution !== null &&
|
|
71
|
+
resolution !== undefined &&
|
|
72
|
+
resolution.traversable !== false;
|
|
73
|
+
|
|
74
|
+
/** Source identity for a raw doc_links row. */
|
|
75
|
+
export const linkSourceIdentity = (row: {
|
|
76
|
+
source_collection: string;
|
|
77
|
+
source_rel_path: string;
|
|
78
|
+
target_collection: string | null;
|
|
79
|
+
}): GraphLinkSourceIdentity => ({
|
|
80
|
+
collection: row.source_collection,
|
|
81
|
+
relPath: row.source_rel_path,
|
|
82
|
+
explicit: row.target_collection !== null && row.target_collection !== "",
|
|
83
|
+
});
|
|
84
|
+
|
|
25
85
|
const MAX_SQL_PARAMS = 900;
|
|
26
86
|
const BULK_RESOLUTION_THRESHOLD = 128;
|
|
27
87
|
export const AUDIT_LINK_SNAPSHOT_MAX_DOCUMENTS = 50_000;
|
|
@@ -48,19 +108,33 @@ export interface AuditLinkSnapshotLink {
|
|
|
48
108
|
targetRefNorm: string;
|
|
49
109
|
targetAnchor: string | null;
|
|
50
110
|
targetCollection: string;
|
|
111
|
+
/** The link carried an explicit `collection:` prefix. */
|
|
112
|
+
explicitCollection?: boolean;
|
|
51
113
|
linkType: "wiki" | "markdown";
|
|
52
114
|
startLine: number;
|
|
53
115
|
startCol: number;
|
|
54
116
|
endLine: number;
|
|
55
117
|
endCol: number;
|
|
56
118
|
resolved: ResolvedGraphLinkTarget | null;
|
|
119
|
+
/**
|
|
120
|
+
* Unresolved workspace wiki link whose target exists as a file in the
|
|
121
|
+
* workspace but is not an indexed document. Existence only: no edge.
|
|
122
|
+
*/
|
|
123
|
+
outsideIndex?: boolean;
|
|
57
124
|
}
|
|
58
125
|
|
|
59
126
|
export interface AuditLinkSnapshot {
|
|
60
127
|
documents: AuditLinkSnapshotDocument[];
|
|
61
128
|
/** Optional finding scope when documents also include graph-wide evidence. */
|
|
62
129
|
auditedDocumentIds?: number[];
|
|
130
|
+
/**
|
|
131
|
+
* Requested collection scope of a scoped audit. Connectivity still uses
|
|
132
|
+
* the whole graph, but candidate identities outside it are withheld.
|
|
133
|
+
*/
|
|
134
|
+
scopeCollections?: string[];
|
|
63
135
|
links: AuditLinkSnapshotLink[];
|
|
136
|
+
/** Why some workspace file listings were incomplete, when they were. */
|
|
137
|
+
outsideIndexDiagnostic?: string;
|
|
64
138
|
totals: { documents: number; links: number };
|
|
65
139
|
truncated: { documents: boolean; links: boolean };
|
|
66
140
|
metrics: {
|
|
@@ -265,10 +339,179 @@ export const resolveGraphLinkTargetsSql = (
|
|
|
265
339
|
return results;
|
|
266
340
|
};
|
|
267
341
|
|
|
268
|
-
|
|
342
|
+
export type GraphLinkResolutionStrategy = "auto" | "sql" | "bulk";
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Resolve all targets with one contract for every graph consumer: plain wiki
|
|
346
|
+
* links inside a link workspace use workspace ranking; explicit, markdown and
|
|
347
|
+
* non-workspace links keep collection-scoped resolution. `strategy` forces
|
|
348
|
+
* the SQL or bulk candidate path (parity tests); results are identical.
|
|
349
|
+
*/
|
|
269
350
|
export function resolveGraphLinkTargets(
|
|
270
351
|
db: Database,
|
|
271
|
-
targets: GraphLinkTarget[]
|
|
352
|
+
targets: GraphLinkTarget[],
|
|
353
|
+
strategy: GraphLinkResolutionStrategy = "auto"
|
|
354
|
+
): Array<ResolvedGraphLinkTarget | null> {
|
|
355
|
+
const results: Array<ResolvedGraphLinkTarget | null> = Array.from(
|
|
356
|
+
{ length: targets.length },
|
|
357
|
+
() => null
|
|
358
|
+
);
|
|
359
|
+
const memberships = targets.some(
|
|
360
|
+
(target) =>
|
|
361
|
+
target.linkType === "wiki" && target.source && !target.source.explicit
|
|
362
|
+
)
|
|
363
|
+
? loadLinkWorkspaceMemberships(db)
|
|
364
|
+
: new Map();
|
|
365
|
+
const workspaceInputs: WorkspaceTargetInput[] = [];
|
|
366
|
+
const workspaceIndexes: number[] = [];
|
|
367
|
+
const legacyTargets: GraphLinkTarget[] = [];
|
|
368
|
+
const legacyIndexes: number[] = [];
|
|
369
|
+
for (const [index, target] of targets.entries()) {
|
|
370
|
+
const source = target.source;
|
|
371
|
+
const wsKey =
|
|
372
|
+
target.linkType === "wiki" && source && !source.explicit
|
|
373
|
+
? workspaceKeyForDocument(
|
|
374
|
+
memberships,
|
|
375
|
+
source.collection,
|
|
376
|
+
source.relPath
|
|
377
|
+
)
|
|
378
|
+
: null;
|
|
379
|
+
if (wsKey !== null && source) {
|
|
380
|
+
workspaceInputs.push({
|
|
381
|
+
targetRefNorm: target.targetRefNorm,
|
|
382
|
+
source: { collection: source.collection, relPath: source.relPath },
|
|
383
|
+
wsKey,
|
|
384
|
+
});
|
|
385
|
+
workspaceIndexes.push(index);
|
|
386
|
+
} else {
|
|
387
|
+
legacyTargets.push(target);
|
|
388
|
+
legacyIndexes.push(index);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const legacyResults = resolveCollectionScopedTargets(
|
|
393
|
+
db,
|
|
394
|
+
legacyTargets,
|
|
395
|
+
strategy
|
|
396
|
+
);
|
|
397
|
+
// Results are shared per unique resolution (as the resolvers dedupe
|
|
398
|
+
// targets) so large inventories do not allocate one object per link.
|
|
399
|
+
const legacyShared = new Map<
|
|
400
|
+
ResolvedGraphLinkTarget,
|
|
401
|
+
Map<string, ResolvedGraphLinkTarget>
|
|
402
|
+
>();
|
|
403
|
+
for (const [position, index] of legacyIndexes.entries()) {
|
|
404
|
+
const resolution = legacyResults[position] ?? null;
|
|
405
|
+
if (!resolution) continue;
|
|
406
|
+
const target = targets[index]!;
|
|
407
|
+
const scope = target.source
|
|
408
|
+
? target.source.explicit
|
|
409
|
+
? ("explicit-collection" as const)
|
|
410
|
+
: ("same-collection" as const)
|
|
411
|
+
: undefined;
|
|
412
|
+
const variantKey = `${target.targetCollection}\0${scope ?? ""}`;
|
|
413
|
+
let variants = legacyShared.get(resolution);
|
|
414
|
+
if (!variants) {
|
|
415
|
+
variants = new Map();
|
|
416
|
+
legacyShared.set(resolution, variants);
|
|
417
|
+
}
|
|
418
|
+
let shared = variants.get(variantKey);
|
|
419
|
+
if (!shared) {
|
|
420
|
+
shared = {
|
|
421
|
+
...resolution,
|
|
422
|
+
targetCollection: target.targetCollection,
|
|
423
|
+
...(scope ? { scope } : {}),
|
|
424
|
+
};
|
|
425
|
+
variants.set(variantKey, shared);
|
|
426
|
+
}
|
|
427
|
+
results[index] = shared;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (workspaceInputs.length > 0) {
|
|
431
|
+
// Only whether the unique count passes the threshold matters.
|
|
432
|
+
const unique = new Set<string>();
|
|
433
|
+
for (const input of workspaceInputs) {
|
|
434
|
+
unique.add(
|
|
435
|
+
`${input.wsKey}\0${input.source.collection}\0${input.source.relPath}\0${input.targetRefNorm}`
|
|
436
|
+
);
|
|
437
|
+
if (unique.size > BULK_RESOLUTION_THRESHOLD) break;
|
|
438
|
+
}
|
|
439
|
+
const uniqueCount = unique.size;
|
|
440
|
+
const bulk =
|
|
441
|
+
strategy === "bulk" ||
|
|
442
|
+
(strategy === "auto" &&
|
|
443
|
+
uniqueCount > BULK_RESOLUTION_THRESHOLD &&
|
|
444
|
+
countActiveDocuments(
|
|
445
|
+
db,
|
|
446
|
+
workspaceMemberCollections(
|
|
447
|
+
memberships,
|
|
448
|
+
new Set(workspaceInputs.map((input) => input.wsKey))
|
|
449
|
+
)
|
|
450
|
+
) <= GRAPH_LINK_BULK_MAX_DOCUMENTS);
|
|
451
|
+
const workspaceResults = resolveWorkspaceTargets(
|
|
452
|
+
db,
|
|
453
|
+
memberships,
|
|
454
|
+
workspaceInputs,
|
|
455
|
+
{ bulk }
|
|
456
|
+
);
|
|
457
|
+
const workspaceShared = new Map<object, ResolvedGraphLinkTarget>();
|
|
458
|
+
for (const [position, index] of workspaceIndexes.entries()) {
|
|
459
|
+
const resolution = workspaceResults[position] ?? null;
|
|
460
|
+
const source = targets[index]!.source!;
|
|
461
|
+
// One resolution object per (workspace, source collection, folder,
|
|
462
|
+
// target), so its scope is fixed and the converted result is shared.
|
|
463
|
+
const cached = resolution ? workspaceShared.get(resolution) : undefined;
|
|
464
|
+
if (cached) {
|
|
465
|
+
results[index] = cached;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
results[index] = resolution
|
|
469
|
+
? {
|
|
470
|
+
targetId: resolution.target.id,
|
|
471
|
+
targetDocid: resolution.target.docid,
|
|
472
|
+
matchRank: resolution.titleRank ?? 0,
|
|
473
|
+
matchCount: resolution.traversable ? 1 : resolution.tied.length,
|
|
474
|
+
targetCollection: resolution.target.collection,
|
|
475
|
+
scope:
|
|
476
|
+
resolution.target.collection === source.collection
|
|
477
|
+
? "same-collection"
|
|
478
|
+
: "cross-collection",
|
|
479
|
+
reason: resolution.reason,
|
|
480
|
+
traversable: resolution.traversable,
|
|
481
|
+
...(resolution.traversable
|
|
482
|
+
? {}
|
|
483
|
+
: {
|
|
484
|
+
candidates: resolution.tied.map(
|
|
485
|
+
({ id, docid, collection }) => ({ id, docid, collection })
|
|
486
|
+
),
|
|
487
|
+
}),
|
|
488
|
+
}
|
|
489
|
+
: null;
|
|
490
|
+
if (resolution && results[index]) {
|
|
491
|
+
workspaceShared.set(resolution, results[index]!);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return results;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const countActiveDocuments = (
|
|
499
|
+
db: Database,
|
|
500
|
+
collections: readonly string[]
|
|
501
|
+
): number =>
|
|
502
|
+
collections.length === 0
|
|
503
|
+
? 0
|
|
504
|
+
: (db
|
|
505
|
+
.query<{ count: number }, string[]>(
|
|
506
|
+
`SELECT COUNT(*) AS count FROM documents WHERE active = 1 AND collection IN (${collections.map(() => "?").join(",")})`
|
|
507
|
+
)
|
|
508
|
+
.get(...collections)?.count ?? 0);
|
|
509
|
+
|
|
510
|
+
/** Collection-scoped (pre-workspace) resolution, deduplicated per target. */
|
|
511
|
+
function resolveCollectionScopedTargets(
|
|
512
|
+
db: Database,
|
|
513
|
+
targets: GraphLinkTarget[],
|
|
514
|
+
strategy: GraphLinkResolutionStrategy
|
|
272
515
|
): Array<ResolvedGraphLinkTarget | null> {
|
|
273
516
|
const uniqueTargets: GraphLinkTarget[] = [];
|
|
274
517
|
const uniqueIndexByKey = new Map<string, number>();
|
|
@@ -289,7 +532,10 @@ export function resolveGraphLinkTargets(
|
|
|
289
532
|
originalToUniqueIndex.push(uniqueIndex);
|
|
290
533
|
}
|
|
291
534
|
|
|
292
|
-
if (
|
|
535
|
+
if (
|
|
536
|
+
strategy === "bulk" ||
|
|
537
|
+
(strategy === "auto" && uniqueTargets.length > BULK_RESOLUTION_THRESHOLD)
|
|
538
|
+
) {
|
|
293
539
|
const bulkResults = resolveGraphLinkTargetsBulk(db, uniqueTargets);
|
|
294
540
|
if (bulkResults) {
|
|
295
541
|
return originalToUniqueIndex.map(
|
|
@@ -396,6 +642,7 @@ export function captureAuditLinkSnapshot(
|
|
|
396
642
|
source_uri: string;
|
|
397
643
|
source_collection: string;
|
|
398
644
|
source_rel_path: string;
|
|
645
|
+
source_doc_rel_path: string;
|
|
399
646
|
target_ref: string;
|
|
400
647
|
target_ref_norm: string;
|
|
401
648
|
target_anchor: string | null;
|
|
@@ -410,7 +657,8 @@ export function captureAuditLinkSnapshot(
|
|
|
410
657
|
>(
|
|
411
658
|
`SELECT d.id AS source_id, d.docid AS source_docid,
|
|
412
659
|
d.uri AS source_uri, d.collection AS source_collection,
|
|
413
|
-
COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) AS source_rel_path,
|
|
660
|
+
COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) AS source_rel_path,
|
|
661
|
+
d.rel_path AS source_doc_rel_path, dl.target_ref,
|
|
414
662
|
dl.target_ref_norm, dl.target_anchor, dl.target_collection,
|
|
415
663
|
dl.link_type, dl.start_line, dl.start_col,
|
|
416
664
|
dl.end_line, dl.end_col
|
|
@@ -425,6 +673,11 @@ export function captureAuditLinkSnapshot(
|
|
|
425
673
|
targetRefNorm: row.target_ref_norm,
|
|
426
674
|
targetCollection: row.target_collection ?? row.source_collection,
|
|
427
675
|
linkType: row.link_type,
|
|
676
|
+
source: linkSourceIdentity({
|
|
677
|
+
source_collection: row.source_collection,
|
|
678
|
+
source_rel_path: row.source_doc_rel_path,
|
|
679
|
+
target_collection: row.target_collection,
|
|
680
|
+
}),
|
|
428
681
|
}));
|
|
429
682
|
const resolutions = resolveGraphLinkTargets(db, targets);
|
|
430
683
|
const uniqueTargetsResolved = new Set(
|
|
@@ -458,6 +711,7 @@ export function captureAuditLinkSnapshot(
|
|
|
458
711
|
targetRefNorm: row.target_ref_norm,
|
|
459
712
|
targetAnchor: row.target_anchor,
|
|
460
713
|
targetCollection: row.target_collection ?? row.source_collection,
|
|
714
|
+
explicitCollection: row.target_collection !== null,
|
|
461
715
|
linkType: row.link_type,
|
|
462
716
|
startLine: row.start_line,
|
|
463
717
|
startCol: row.start_col,
|