@gmickel/gno 2.7.0 → 2.8.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 +26 -30
- package/assets/skill/SKILL.md +8 -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/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.7.0.zip → gno-browser-clipper-v2.8.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +48 -6
- package/spec/db/schema.sql +0 -1
- package/spec/mcp.md +18 -3
- 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 +15 -2
- package/spec/output-schemas/graph.schema.json +2 -0
- package/spec/output-schemas/links-list.schema.json +4 -0
- package/spec/output-schemas/status.schema.json +27 -16
- package/src/cli/commands/ask.ts +31 -12
- 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/links.ts +34 -131
- package/src/cli/commands/ls.ts +6 -1
- package/src/cli/commands/shared.ts +7 -0
- package/src/cli/commands/status.ts +5 -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 +145 -25
- package/src/core/audit-provenance.ts +11 -4
- package/src/core/audit-workspace.ts +19 -4
- package/src/core/audit.ts +67 -15
- 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 +24 -5
- package/src/core/knowledge-impact.ts +28 -0
- package/src/core/link-workspace.ts +324 -0
- package/src/core/request-receipts.ts +63 -9
- package/src/core/retrieval-replay-candidate.ts +6 -0
- package/src/core/retrieval-trace-request.ts +3 -0
- package/src/core/windows-private-path.ts +136 -1
- package/src/embed/backlog.ts +22 -10
- package/src/embed/variant-backlog.ts +34 -9
- 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 +22 -1
- package/src/ingestion/types.ts +14 -0
- package/src/llm/inference-scope.ts +19 -0
- 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 +3 -0
- package/src/mcp/tools/sessions.ts +33 -4
- package/src/mcp/tools/status.ts +22 -6
- 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/types.ts +6 -3
- package/src/serve/embed-scheduler.ts +2 -2
- package/src/serve/findings-pass.ts +1 -1
- package/src/serve/host-path-redaction.ts +51 -14
- package/src/serve/public/components/BootstrapStatus.tsx +5 -3
- package/src/serve/public/components/CaptureModal.tsx +1 -1
- package/src/serve/public/components/CollectionModelDialog.tsx +16 -13
- package/src/serve/public/components/CollectionsEmptyState.tsx +5 -3
- package/src/serve/public/components/FirstRunWizard.tsx +4 -2
- package/src/serve/public/components/sessions/SourcesPanel.tsx +77 -60
- package/src/serve/public/pages/Collections.tsx +16 -13
- package/src/serve/public/pages/Connectors.tsx +7 -4
- package/src/serve/public/pages/Dashboard.tsx +8 -6
- package/src/serve/public/pages/GraphView.tsx +2 -0
- package/src/serve/resident-runtime.ts +7 -0
- package/src/serve/routes/changes.ts +6 -1
- package/src/serve/routes/links.ts +13 -0
- package/src/serve/routes/sessions.ts +81 -37
- package/src/serve/server.ts +5 -3
- package/src/serve/status-model.ts +10 -6
- package/src/serve/status.ts +2 -7
- 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 +392 -233
- package/src/store/sqlite/change-journal-store.ts +1 -1
- package/src/store/sqlite/eligibility.ts +8 -2
- package/src/store/sqlite/graph-link-resolver.ts +252 -5
- package/src/store/sqlite/graph-neighbors.ts +147 -40
- package/src/store/sqlite/graph-reference-state.ts +13 -2
- package/src/store/sqlite/legacy-vector-ownership.ts +2 -1
- package/src/store/sqlite/workspace-link-resolver.ts +654 -0
- package/src/store/types.ts +49 -3
- package/src/store/vector/stats.ts +1 -1
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +0 -1
|
@@ -180,7 +180,7 @@ const enforceInTransaction = (
|
|
|
180
180
|
db
|
|
181
181
|
.query<{ sequence: number | null }, [number]>(
|
|
182
182
|
`SELECT MAX(sequence) AS sequence
|
|
183
|
-
FROM document_changes
|
|
183
|
+
FROM document_changes INDEXED BY idx_document_changes_retention
|
|
184
184
|
WHERE observed_at_ms <= ?`
|
|
185
185
|
)
|
|
186
186
|
.get(ageBoundary)?.sequence ?? state.retention_floor;
|
|
@@ -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,6 +108,8 @@ 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;
|
|
@@ -60,6 +122,11 @@ export interface AuditLinkSnapshot {
|
|
|
60
122
|
documents: AuditLinkSnapshotDocument[];
|
|
61
123
|
/** Optional finding scope when documents also include graph-wide evidence. */
|
|
62
124
|
auditedDocumentIds?: number[];
|
|
125
|
+
/**
|
|
126
|
+
* Requested collection scope of a scoped audit. Connectivity still uses
|
|
127
|
+
* the whole graph, but candidate identities outside it are withheld.
|
|
128
|
+
*/
|
|
129
|
+
scopeCollections?: string[];
|
|
63
130
|
links: AuditLinkSnapshotLink[];
|
|
64
131
|
totals: { documents: number; links: number };
|
|
65
132
|
truncated: { documents: boolean; links: boolean };
|
|
@@ -265,10 +332,179 @@ export const resolveGraphLinkTargetsSql = (
|
|
|
265
332
|
return results;
|
|
266
333
|
};
|
|
267
334
|
|
|
268
|
-
|
|
335
|
+
export type GraphLinkResolutionStrategy = "auto" | "sql" | "bulk";
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Resolve all targets with one contract for every graph consumer: plain wiki
|
|
339
|
+
* links inside a link workspace use workspace ranking; explicit, markdown and
|
|
340
|
+
* non-workspace links keep collection-scoped resolution. `strategy` forces
|
|
341
|
+
* the SQL or bulk candidate path (parity tests); results are identical.
|
|
342
|
+
*/
|
|
269
343
|
export function resolveGraphLinkTargets(
|
|
270
344
|
db: Database,
|
|
271
|
-
targets: GraphLinkTarget[]
|
|
345
|
+
targets: GraphLinkTarget[],
|
|
346
|
+
strategy: GraphLinkResolutionStrategy = "auto"
|
|
347
|
+
): Array<ResolvedGraphLinkTarget | null> {
|
|
348
|
+
const results: Array<ResolvedGraphLinkTarget | null> = Array.from(
|
|
349
|
+
{ length: targets.length },
|
|
350
|
+
() => null
|
|
351
|
+
);
|
|
352
|
+
const memberships = targets.some(
|
|
353
|
+
(target) =>
|
|
354
|
+
target.linkType === "wiki" && target.source && !target.source.explicit
|
|
355
|
+
)
|
|
356
|
+
? loadLinkWorkspaceMemberships(db)
|
|
357
|
+
: new Map();
|
|
358
|
+
const workspaceInputs: WorkspaceTargetInput[] = [];
|
|
359
|
+
const workspaceIndexes: number[] = [];
|
|
360
|
+
const legacyTargets: GraphLinkTarget[] = [];
|
|
361
|
+
const legacyIndexes: number[] = [];
|
|
362
|
+
for (const [index, target] of targets.entries()) {
|
|
363
|
+
const source = target.source;
|
|
364
|
+
const wsKey =
|
|
365
|
+
target.linkType === "wiki" && source && !source.explicit
|
|
366
|
+
? workspaceKeyForDocument(
|
|
367
|
+
memberships,
|
|
368
|
+
source.collection,
|
|
369
|
+
source.relPath
|
|
370
|
+
)
|
|
371
|
+
: null;
|
|
372
|
+
if (wsKey !== null && source) {
|
|
373
|
+
workspaceInputs.push({
|
|
374
|
+
targetRefNorm: target.targetRefNorm,
|
|
375
|
+
source: { collection: source.collection, relPath: source.relPath },
|
|
376
|
+
wsKey,
|
|
377
|
+
});
|
|
378
|
+
workspaceIndexes.push(index);
|
|
379
|
+
} else {
|
|
380
|
+
legacyTargets.push(target);
|
|
381
|
+
legacyIndexes.push(index);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const legacyResults = resolveCollectionScopedTargets(
|
|
386
|
+
db,
|
|
387
|
+
legacyTargets,
|
|
388
|
+
strategy
|
|
389
|
+
);
|
|
390
|
+
// Results are shared per unique resolution (as the resolvers dedupe
|
|
391
|
+
// targets) so large inventories do not allocate one object per link.
|
|
392
|
+
const legacyShared = new Map<
|
|
393
|
+
ResolvedGraphLinkTarget,
|
|
394
|
+
Map<string, ResolvedGraphLinkTarget>
|
|
395
|
+
>();
|
|
396
|
+
for (const [position, index] of legacyIndexes.entries()) {
|
|
397
|
+
const resolution = legacyResults[position] ?? null;
|
|
398
|
+
if (!resolution) continue;
|
|
399
|
+
const target = targets[index]!;
|
|
400
|
+
const scope = target.source
|
|
401
|
+
? target.source.explicit
|
|
402
|
+
? ("explicit-collection" as const)
|
|
403
|
+
: ("same-collection" as const)
|
|
404
|
+
: undefined;
|
|
405
|
+
const variantKey = `${target.targetCollection}\0${scope ?? ""}`;
|
|
406
|
+
let variants = legacyShared.get(resolution);
|
|
407
|
+
if (!variants) {
|
|
408
|
+
variants = new Map();
|
|
409
|
+
legacyShared.set(resolution, variants);
|
|
410
|
+
}
|
|
411
|
+
let shared = variants.get(variantKey);
|
|
412
|
+
if (!shared) {
|
|
413
|
+
shared = {
|
|
414
|
+
...resolution,
|
|
415
|
+
targetCollection: target.targetCollection,
|
|
416
|
+
...(scope ? { scope } : {}),
|
|
417
|
+
};
|
|
418
|
+
variants.set(variantKey, shared);
|
|
419
|
+
}
|
|
420
|
+
results[index] = shared;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (workspaceInputs.length > 0) {
|
|
424
|
+
// Only whether the unique count passes the threshold matters.
|
|
425
|
+
const unique = new Set<string>();
|
|
426
|
+
for (const input of workspaceInputs) {
|
|
427
|
+
unique.add(
|
|
428
|
+
`${input.wsKey}\0${input.source.collection}\0${input.source.relPath}\0${input.targetRefNorm}`
|
|
429
|
+
);
|
|
430
|
+
if (unique.size > BULK_RESOLUTION_THRESHOLD) break;
|
|
431
|
+
}
|
|
432
|
+
const uniqueCount = unique.size;
|
|
433
|
+
const bulk =
|
|
434
|
+
strategy === "bulk" ||
|
|
435
|
+
(strategy === "auto" &&
|
|
436
|
+
uniqueCount > BULK_RESOLUTION_THRESHOLD &&
|
|
437
|
+
countActiveDocuments(
|
|
438
|
+
db,
|
|
439
|
+
workspaceMemberCollections(
|
|
440
|
+
memberships,
|
|
441
|
+
new Set(workspaceInputs.map((input) => input.wsKey))
|
|
442
|
+
)
|
|
443
|
+
) <= GRAPH_LINK_BULK_MAX_DOCUMENTS);
|
|
444
|
+
const workspaceResults = resolveWorkspaceTargets(
|
|
445
|
+
db,
|
|
446
|
+
memberships,
|
|
447
|
+
workspaceInputs,
|
|
448
|
+
{ bulk }
|
|
449
|
+
);
|
|
450
|
+
const workspaceShared = new Map<object, ResolvedGraphLinkTarget>();
|
|
451
|
+
for (const [position, index] of workspaceIndexes.entries()) {
|
|
452
|
+
const resolution = workspaceResults[position] ?? null;
|
|
453
|
+
const source = targets[index]!.source!;
|
|
454
|
+
// One resolution object per (workspace, source collection, folder,
|
|
455
|
+
// target), so its scope is fixed and the converted result is shared.
|
|
456
|
+
const cached = resolution ? workspaceShared.get(resolution) : undefined;
|
|
457
|
+
if (cached) {
|
|
458
|
+
results[index] = cached;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
results[index] = resolution
|
|
462
|
+
? {
|
|
463
|
+
targetId: resolution.target.id,
|
|
464
|
+
targetDocid: resolution.target.docid,
|
|
465
|
+
matchRank: resolution.titleRank ?? 0,
|
|
466
|
+
matchCount: resolution.traversable ? 1 : resolution.tied.length,
|
|
467
|
+
targetCollection: resolution.target.collection,
|
|
468
|
+
scope:
|
|
469
|
+
resolution.target.collection === source.collection
|
|
470
|
+
? "same-collection"
|
|
471
|
+
: "cross-collection",
|
|
472
|
+
reason: resolution.reason,
|
|
473
|
+
traversable: resolution.traversable,
|
|
474
|
+
...(resolution.traversable
|
|
475
|
+
? {}
|
|
476
|
+
: {
|
|
477
|
+
candidates: resolution.tied.map(
|
|
478
|
+
({ id, docid, collection }) => ({ id, docid, collection })
|
|
479
|
+
),
|
|
480
|
+
}),
|
|
481
|
+
}
|
|
482
|
+
: null;
|
|
483
|
+
if (resolution && results[index]) {
|
|
484
|
+
workspaceShared.set(resolution, results[index]!);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return results;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const countActiveDocuments = (
|
|
492
|
+
db: Database,
|
|
493
|
+
collections: readonly string[]
|
|
494
|
+
): number =>
|
|
495
|
+
collections.length === 0
|
|
496
|
+
? 0
|
|
497
|
+
: (db
|
|
498
|
+
.query<{ count: number }, string[]>(
|
|
499
|
+
`SELECT COUNT(*) AS count FROM documents WHERE active = 1 AND collection IN (${collections.map(() => "?").join(",")})`
|
|
500
|
+
)
|
|
501
|
+
.get(...collections)?.count ?? 0);
|
|
502
|
+
|
|
503
|
+
/** Collection-scoped (pre-workspace) resolution, deduplicated per target. */
|
|
504
|
+
function resolveCollectionScopedTargets(
|
|
505
|
+
db: Database,
|
|
506
|
+
targets: GraphLinkTarget[],
|
|
507
|
+
strategy: GraphLinkResolutionStrategy
|
|
272
508
|
): Array<ResolvedGraphLinkTarget | null> {
|
|
273
509
|
const uniqueTargets: GraphLinkTarget[] = [];
|
|
274
510
|
const uniqueIndexByKey = new Map<string, number>();
|
|
@@ -289,7 +525,10 @@ export function resolveGraphLinkTargets(
|
|
|
289
525
|
originalToUniqueIndex.push(uniqueIndex);
|
|
290
526
|
}
|
|
291
527
|
|
|
292
|
-
if (
|
|
528
|
+
if (
|
|
529
|
+
strategy === "bulk" ||
|
|
530
|
+
(strategy === "auto" && uniqueTargets.length > BULK_RESOLUTION_THRESHOLD)
|
|
531
|
+
) {
|
|
293
532
|
const bulkResults = resolveGraphLinkTargetsBulk(db, uniqueTargets);
|
|
294
533
|
if (bulkResults) {
|
|
295
534
|
return originalToUniqueIndex.map(
|
|
@@ -396,6 +635,7 @@ export function captureAuditLinkSnapshot(
|
|
|
396
635
|
source_uri: string;
|
|
397
636
|
source_collection: string;
|
|
398
637
|
source_rel_path: string;
|
|
638
|
+
source_doc_rel_path: string;
|
|
399
639
|
target_ref: string;
|
|
400
640
|
target_ref_norm: string;
|
|
401
641
|
target_anchor: string | null;
|
|
@@ -410,7 +650,8 @@ export function captureAuditLinkSnapshot(
|
|
|
410
650
|
>(
|
|
411
651
|
`SELECT d.id AS source_id, d.docid AS source_docid,
|
|
412
652
|
d.uri AS source_uri, d.collection AS source_collection,
|
|
413
|
-
COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) AS source_rel_path,
|
|
653
|
+
COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) AS source_rel_path,
|
|
654
|
+
d.rel_path AS source_doc_rel_path, dl.target_ref,
|
|
414
655
|
dl.target_ref_norm, dl.target_anchor, dl.target_collection,
|
|
415
656
|
dl.link_type, dl.start_line, dl.start_col,
|
|
416
657
|
dl.end_line, dl.end_col
|
|
@@ -425,6 +666,11 @@ export function captureAuditLinkSnapshot(
|
|
|
425
666
|
targetRefNorm: row.target_ref_norm,
|
|
426
667
|
targetCollection: row.target_collection ?? row.source_collection,
|
|
427
668
|
linkType: row.link_type,
|
|
669
|
+
source: linkSourceIdentity({
|
|
670
|
+
source_collection: row.source_collection,
|
|
671
|
+
source_rel_path: row.source_doc_rel_path,
|
|
672
|
+
target_collection: row.target_collection,
|
|
673
|
+
}),
|
|
428
674
|
}));
|
|
429
675
|
const resolutions = resolveGraphLinkTargets(db, targets);
|
|
430
676
|
const uniqueTargetsResolved = new Set(
|
|
@@ -458,6 +704,7 @@ export function captureAuditLinkSnapshot(
|
|
|
458
704
|
targetRefNorm: row.target_ref_norm,
|
|
459
705
|
targetAnchor: row.target_anchor,
|
|
460
706
|
targetCollection: row.target_collection ?? row.source_collection,
|
|
707
|
+
explicitCollection: row.target_collection !== null,
|
|
461
708
|
linkType: row.link_type,
|
|
462
709
|
startLine: row.start_line,
|
|
463
710
|
startCol: row.start_col,
|