@gmickel/gno 2.7.1 → 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 +3 -2
- 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.1.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 +1 -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 +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/status.schema.json +15 -0
- 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/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 +1 -0
- package/src/core/knowledge-impact.ts +28 -0
- package/src/core/link-workspace.ts +324 -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 +22 -1
- 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 +3 -0
- package/src/mcp/tools/sessions.ts +33 -4
- package/src/mcp/tools/status.ts +3 -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/types.ts +6 -3
- package/src/serve/findings-pass.ts +1 -1
- package/src/serve/public/pages/GraphView.tsx +2 -0
- package/src/serve/routes/changes.ts +6 -1
- package/src/serve/routes/links.ts +13 -0
- package/src/serve/routes/sessions.ts +41 -53
- 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 +390 -231
- 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/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.1.zip.sha256 +0 -1
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Seed-scoped one-hop graph neighbor resolution for query-time expansion.
|
|
3
3
|
* Avoids collection-wide getGraph correlated link resolution.
|
|
4
4
|
*
|
|
5
|
+
* Resolution and scope are separate steps: links resolve with the shared
|
|
6
|
+
* workspace-aware resolver, then the collection allowlist is applied to the
|
|
7
|
+
* resolved source AND target of every edge before scoring or limits, so a
|
|
8
|
+
* cross-collection edge never widens the caller's scope.
|
|
9
|
+
*
|
|
5
10
|
* @module src/store/sqlite/graph-neighbors
|
|
6
11
|
*/
|
|
7
12
|
|
|
@@ -21,7 +26,16 @@ import {
|
|
|
21
26
|
mergeGraphEdgeAudit,
|
|
22
27
|
} from "../../core/graph-edge-confidence";
|
|
23
28
|
import { normalizeWikiName, stripWikiMdExt } from "../../core/links";
|
|
24
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
isTraversableResolution,
|
|
31
|
+
linkSourceIdentity,
|
|
32
|
+
resolveGraphLinkTargets,
|
|
33
|
+
} from "./graph-link-resolver";
|
|
34
|
+
import {
|
|
35
|
+
loadLinkWorkspaceMemberships,
|
|
36
|
+
workspaceKeyForDocument,
|
|
37
|
+
workspaceMemberCollections,
|
|
38
|
+
} from "./workspace-link-resolver";
|
|
25
39
|
|
|
26
40
|
const MAX_SEED_DOCUMENTS = 5;
|
|
27
41
|
const DEFAULT_EDGE_LIMIT = 10_000;
|
|
@@ -40,17 +54,23 @@ interface ResolvedEdgeRow {
|
|
|
40
54
|
link_type: "wiki" | "markdown";
|
|
41
55
|
match_rank: number | null;
|
|
42
56
|
match_count: number | null;
|
|
57
|
+
reason?: string;
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
interface RawLinkRow {
|
|
46
61
|
id: number;
|
|
47
62
|
source_docid: string;
|
|
48
63
|
source_collection: string;
|
|
64
|
+
source_rel_path: string;
|
|
49
65
|
target_ref_norm: string;
|
|
50
66
|
target_collection: string | null;
|
|
51
67
|
link_type: "wiki" | "markdown";
|
|
52
68
|
}
|
|
53
69
|
|
|
70
|
+
const RAW_LINK_COLUMNS = `dl.id, src.docid AS source_docid,
|
|
71
|
+
src.collection AS source_collection, src.rel_path AS source_rel_path,
|
|
72
|
+
dl.target_ref_norm, dl.target_collection, dl.link_type`;
|
|
73
|
+
|
|
54
74
|
const addWikiKeyVariants = (keySet: Set<string>, value: string): void => {
|
|
55
75
|
if (!value) {
|
|
56
76
|
return;
|
|
@@ -83,6 +103,33 @@ const matchesWikiKey = (targetRefNorm: string, keys: Set<string>): boolean => {
|
|
|
83
103
|
return false;
|
|
84
104
|
};
|
|
85
105
|
|
|
106
|
+
const escapeLike = (value: string): string =>
|
|
107
|
+
value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
108
|
+
|
|
109
|
+
/** Effective graph allowlist; undefined means unrestricted. */
|
|
110
|
+
export const graphCollectionAllowlist = (options: {
|
|
111
|
+
collection?: string;
|
|
112
|
+
collections?: string[];
|
|
113
|
+
}): Set<string> | undefined => {
|
|
114
|
+
if (options.collections) return new Set(options.collections);
|
|
115
|
+
if (options.collection) return new Set([options.collection]);
|
|
116
|
+
return undefined;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const inScopeClause = (
|
|
120
|
+
allowlist: Set<string> | undefined,
|
|
121
|
+
column: string
|
|
122
|
+
): { sql: string; params: string[] } =>
|
|
123
|
+
allowlist
|
|
124
|
+
? {
|
|
125
|
+
sql:
|
|
126
|
+
allowlist.size === 0
|
|
127
|
+
? "AND 0"
|
|
128
|
+
: `AND ${column} IN (${[...allowlist].map(() => "?").join(",")})`,
|
|
129
|
+
params: [...allowlist],
|
|
130
|
+
}
|
|
131
|
+
: { sql: "", params: [] };
|
|
132
|
+
|
|
86
133
|
const loadSeeds = (db: Database, seedDocumentIds: number[]): SeedDocRow[] => {
|
|
87
134
|
const uniqueIds = [...new Set(seedDocumentIds)]
|
|
88
135
|
.filter((id) => Number.isInteger(id) && id > 0)
|
|
@@ -104,11 +151,12 @@ const loadSeeds = (db: Database, seedDocumentIds: number[]): SeedDocRow[] => {
|
|
|
104
151
|
const collectIncomingCandidateLinks = (
|
|
105
152
|
db: Database,
|
|
106
153
|
seeds: SeedDocRow[],
|
|
107
|
-
|
|
154
|
+
allowlist: Set<string> | undefined
|
|
108
155
|
): { links: RawLinkRow[]; examinedLinkRows: number } => {
|
|
109
156
|
const linksById = new Map<number, RawLinkRow>();
|
|
110
157
|
let examinedLinkRows = 0;
|
|
111
158
|
const seedIdSet = new Set(seeds.map((seed) => seed.id));
|
|
159
|
+
const sourceScope = inScopeClause(allowlist, "src.collection");
|
|
112
160
|
|
|
113
161
|
const wikiKeysByCollection = new Map<string, Set<string>>();
|
|
114
162
|
for (const seed of seeds) {
|
|
@@ -124,9 +172,7 @@ const collectIncomingCandidateLinks = (
|
|
|
124
172
|
const collectionPlaceholders = targetCollections.map(() => "?").join(",");
|
|
125
173
|
const wikiRows = db
|
|
126
174
|
.query<RawLinkRow & { source_doc_id: number }, string[]>(
|
|
127
|
-
`SELECT dl.
|
|
128
|
-
src.collection AS source_collection, dl.target_ref_norm,
|
|
129
|
-
dl.target_collection, dl.link_type
|
|
175
|
+
`SELECT dl.source_doc_id, ${RAW_LINK_COLUMNS}
|
|
130
176
|
FROM doc_links dl
|
|
131
177
|
JOIN documents src ON src.id = dl.source_doc_id AND src.active = 1
|
|
132
178
|
WHERE dl.link_type = 'wiki'
|
|
@@ -135,13 +181,9 @@ const collectIncomingCandidateLinks = (
|
|
|
135
181
|
AND src.collection IN (${collectionPlaceholders}))
|
|
136
182
|
OR dl.target_collection IN (${collectionPlaceholders})
|
|
137
183
|
)
|
|
138
|
-
${
|
|
184
|
+
${sourceScope.sql}`
|
|
139
185
|
)
|
|
140
|
-
.all(
|
|
141
|
-
...targetCollections,
|
|
142
|
-
...targetCollections,
|
|
143
|
-
...(collection ? [collection] : [])
|
|
144
|
-
);
|
|
186
|
+
.all(...targetCollections, ...targetCollections, ...sourceScope.params);
|
|
145
187
|
examinedLinkRows += wikiRows.length;
|
|
146
188
|
|
|
147
189
|
for (const row of wikiRows) {
|
|
@@ -157,12 +199,69 @@ const collectIncomingCandidateLinks = (
|
|
|
157
199
|
}
|
|
158
200
|
}
|
|
159
201
|
|
|
202
|
+
// Workspace seeds: plain links from any member collection whose last path
|
|
203
|
+
// segment names the seed file. The resolver decides which really land on
|
|
204
|
+
// the seed; this only bounds the candidate rows.
|
|
205
|
+
const memberships = loadLinkWorkspaceMemberships(db);
|
|
206
|
+
const workspaceSeeds = seeds
|
|
207
|
+
.map((seed) => ({
|
|
208
|
+
seed,
|
|
209
|
+
wsKey: workspaceKeyForDocument(
|
|
210
|
+
memberships,
|
|
211
|
+
seed.collection,
|
|
212
|
+
seed.rel_path
|
|
213
|
+
),
|
|
214
|
+
}))
|
|
215
|
+
.filter(
|
|
216
|
+
(entry): entry is { seed: SeedDocRow; wsKey: string } =>
|
|
217
|
+
entry.wsKey !== null
|
|
218
|
+
);
|
|
219
|
+
if (workspaceSeeds.length > 0) {
|
|
220
|
+
const members = workspaceMemberCollections(
|
|
221
|
+
memberships,
|
|
222
|
+
new Set(workspaceSeeds.map(({ wsKey }) => wsKey))
|
|
223
|
+
);
|
|
224
|
+
const conditions: string[] = [];
|
|
225
|
+
const params: string[] = [];
|
|
226
|
+
for (const { seed } of workspaceSeeds) {
|
|
227
|
+
const base = stripWikiMdExt(
|
|
228
|
+
normalizeWikiName(seed.rel_path.split("/").pop() ?? seed.rel_path)
|
|
229
|
+
);
|
|
230
|
+
for (const value of [base, `${base}.md`]) {
|
|
231
|
+
conditions.push(
|
|
232
|
+
"dl.target_ref_norm = ? OR dl.target_ref_norm LIKE ? ESCAPE '\\'"
|
|
233
|
+
);
|
|
234
|
+
params.push(value, `%/${escapeLike(value)}`);
|
|
235
|
+
}
|
|
236
|
+
for (const key of wikiKeysForSeed(seed)) {
|
|
237
|
+
conditions.push("dl.target_ref_norm = ?");
|
|
238
|
+
params.push(key);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (members.length > 0 && conditions.length > 0) {
|
|
242
|
+
const rows = db
|
|
243
|
+
.query<RawLinkRow & { source_doc_id: number }, string[]>(
|
|
244
|
+
`SELECT dl.source_doc_id, ${RAW_LINK_COLUMNS}
|
|
245
|
+
FROM doc_links dl
|
|
246
|
+
JOIN documents src ON src.id = dl.source_doc_id AND src.active = 1
|
|
247
|
+
WHERE dl.link_type = 'wiki'
|
|
248
|
+
AND dl.target_collection IS NULL
|
|
249
|
+
AND src.collection IN (${members.map(() => "?").join(",")})
|
|
250
|
+
AND (${conditions.join(" OR ")})
|
|
251
|
+
${sourceScope.sql}`
|
|
252
|
+
)
|
|
253
|
+
.all(...members, ...params, ...sourceScope.params);
|
|
254
|
+
examinedLinkRows += rows.length;
|
|
255
|
+
for (const row of rows) {
|
|
256
|
+
if (!seedIdSet.has(row.source_doc_id)) linksById.set(row.id, row);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
160
261
|
for (const seed of seeds) {
|
|
161
262
|
const mdRows = db
|
|
162
263
|
.query<RawLinkRow & { source_doc_id: number }, string[]>(
|
|
163
|
-
`SELECT dl.
|
|
164
|
-
src.collection AS source_collection, dl.target_ref_norm,
|
|
165
|
-
dl.target_collection, dl.link_type
|
|
264
|
+
`SELECT dl.source_doc_id, ${RAW_LINK_COLUMNS}
|
|
166
265
|
FROM doc_links dl
|
|
167
266
|
JOIN documents src ON src.id = dl.source_doc_id AND src.active = 1
|
|
168
267
|
WHERE dl.link_type = 'markdown'
|
|
@@ -171,13 +270,13 @@ const collectIncomingCandidateLinks = (
|
|
|
171
270
|
(dl.target_collection IS NULL AND src.collection = ?)
|
|
172
271
|
OR dl.target_collection = ?
|
|
173
272
|
)
|
|
174
|
-
${
|
|
273
|
+
${sourceScope.sql}`
|
|
175
274
|
)
|
|
176
275
|
.all(
|
|
177
276
|
seed.rel_path,
|
|
178
277
|
seed.collection,
|
|
179
278
|
seed.collection,
|
|
180
|
-
...
|
|
279
|
+
...sourceScope.params
|
|
181
280
|
);
|
|
182
281
|
examinedLinkRows += mdRows.length;
|
|
183
282
|
for (const row of mdRows) {
|
|
@@ -193,29 +292,24 @@ const collectIncomingCandidateLinks = (
|
|
|
193
292
|
const loadRawLinksForSources = (
|
|
194
293
|
db: Database,
|
|
195
294
|
sourceIds: number[],
|
|
196
|
-
|
|
295
|
+
allowlist: Set<string> | undefined
|
|
197
296
|
): RawLinkRow[] => {
|
|
198
297
|
if (sourceIds.length === 0) {
|
|
199
298
|
return [];
|
|
200
299
|
}
|
|
201
300
|
const sourcePlaceholders = sourceIds.map(() => "?").join(",");
|
|
202
|
-
const
|
|
203
|
-
if (collection) {
|
|
204
|
-
params.push(collection);
|
|
205
|
-
}
|
|
301
|
+
const sourceScope = inScopeClause(allowlist, "src.collection");
|
|
206
302
|
return db
|
|
207
303
|
.query<RawLinkRow, (string | number)[]>(
|
|
208
|
-
`SELECT
|
|
209
|
-
src.collection AS source_collection,
|
|
210
|
-
dl.target_ref_norm, dl.target_collection, dl.link_type
|
|
304
|
+
`SELECT ${RAW_LINK_COLUMNS}
|
|
211
305
|
FROM documents src
|
|
212
306
|
JOIN doc_links dl ON dl.source_doc_id = src.id
|
|
213
307
|
WHERE src.active = 1
|
|
214
308
|
AND src.id IN (${sourcePlaceholders})
|
|
215
|
-
${
|
|
309
|
+
${sourceScope.sql}
|
|
216
310
|
ORDER BY src.id ASC, dl.id ASC`
|
|
217
311
|
)
|
|
218
|
-
.all(...params);
|
|
312
|
+
.all(...sourceIds, ...sourceScope.params);
|
|
219
313
|
};
|
|
220
314
|
|
|
221
315
|
const resolveRawEdges = (
|
|
@@ -223,36 +317,45 @@ const resolveRawEdges = (
|
|
|
223
317
|
rawRows: RawLinkRow[],
|
|
224
318
|
incomingLinkIds: Set<number>,
|
|
225
319
|
seedIds: Set<number>,
|
|
226
|
-
|
|
320
|
+
allowlist: Set<string> | undefined
|
|
227
321
|
): ResolvedEdgeRow[] => {
|
|
228
|
-
const inScopeRows = rawRows.filter(
|
|
229
|
-
(row) =>
|
|
230
|
-
!collection ||
|
|
231
|
-
(row.target_collection ?? row.source_collection) === collection
|
|
232
|
-
);
|
|
233
322
|
const resolvedTargets = resolveGraphLinkTargets(
|
|
234
323
|
db,
|
|
235
|
-
|
|
324
|
+
rawRows.map((row) => ({
|
|
236
325
|
targetRefNorm: row.target_ref_norm,
|
|
237
326
|
targetCollection: row.target_collection ?? row.source_collection,
|
|
238
327
|
linkType: row.link_type,
|
|
328
|
+
source: linkSourceIdentity(row),
|
|
239
329
|
}))
|
|
240
330
|
);
|
|
241
331
|
const rows: ResolvedEdgeRow[] = [];
|
|
242
|
-
for (const [index, rawRow] of
|
|
332
|
+
for (const [index, rawRow] of rawRows.entries()) {
|
|
243
333
|
const target = resolvedTargets[index];
|
|
244
334
|
if (
|
|
245
|
-
!target ||
|
|
335
|
+
!isTraversableResolution(target) ||
|
|
246
336
|
(incomingLinkIds.has(rawRow.id) && !seedIds.has(target.targetId))
|
|
247
337
|
) {
|
|
248
338
|
continue;
|
|
249
339
|
}
|
|
340
|
+
// Scope applies to the resolved identities, never the declared prefix.
|
|
341
|
+
if (
|
|
342
|
+
allowlist &&
|
|
343
|
+
(!allowlist.has(rawRow.source_collection) ||
|
|
344
|
+
!allowlist.has(
|
|
345
|
+
target.targetCollection ??
|
|
346
|
+
rawRow.target_collection ??
|
|
347
|
+
rawRow.source_collection
|
|
348
|
+
))
|
|
349
|
+
) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
250
352
|
rows.push({
|
|
251
353
|
source_docid: rawRow.source_docid,
|
|
252
354
|
target_docid: target.targetDocid,
|
|
253
355
|
link_type: rawRow.link_type,
|
|
254
356
|
match_rank: target.matchRank,
|
|
255
357
|
match_count: target.matchCount,
|
|
358
|
+
reason: target.reason,
|
|
256
359
|
});
|
|
257
360
|
}
|
|
258
361
|
return rows;
|
|
@@ -277,7 +380,8 @@ const toGraphLinks = (
|
|
|
277
380
|
const { confidence, audit } = classifyResolvedGraphEdge(
|
|
278
381
|
row.link_type,
|
|
279
382
|
row.match_rank,
|
|
280
|
-
row.match_count
|
|
383
|
+
row.match_count,
|
|
384
|
+
row.reason
|
|
281
385
|
);
|
|
282
386
|
const existing = edgeMap.get(key);
|
|
283
387
|
if (existing) {
|
|
@@ -326,7 +430,10 @@ export function queryGraphNeighborsForSeeds(
|
|
|
326
430
|
1,
|
|
327
431
|
Math.min(50_000, options.limitEdges ?? DEFAULT_EDGE_LIMIT)
|
|
328
432
|
);
|
|
329
|
-
const
|
|
433
|
+
const allowlist = graphCollectionAllowlist(options);
|
|
434
|
+
const seeds = loadSeeds(db, options.seedDocumentIds).filter(
|
|
435
|
+
(seed) => !allowlist || allowlist.has(seed.collection)
|
|
436
|
+
);
|
|
330
437
|
if (seeds.length === 0) {
|
|
331
438
|
return {
|
|
332
439
|
links: [],
|
|
@@ -339,9 +446,9 @@ export function queryGraphNeighborsForSeeds(
|
|
|
339
446
|
}
|
|
340
447
|
|
|
341
448
|
const seedIds = seeds.map((seed) => seed.id);
|
|
342
|
-
const outgoingLinks = loadRawLinksForSources(db, seedIds,
|
|
449
|
+
const outgoingLinks = loadRawLinksForSources(db, seedIds, allowlist);
|
|
343
450
|
const { links: incomingLinks, examinedLinkRows: incomingCandidates } =
|
|
344
|
-
collectIncomingCandidateLinks(db, seeds,
|
|
451
|
+
collectIncomingCandidateLinks(db, seeds, allowlist);
|
|
345
452
|
const incomingLinkIds = new Set(incomingLinks.map((link) => link.id));
|
|
346
453
|
const rawLinksById = new Map(
|
|
347
454
|
[...outgoingLinks, ...incomingLinks].map((link) => [link.id, link])
|
|
@@ -351,7 +458,7 @@ export function queryGraphNeighborsForSeeds(
|
|
|
351
458
|
[...rawLinksById.values()],
|
|
352
459
|
incomingLinkIds,
|
|
353
460
|
new Set(seedIds),
|
|
354
|
-
|
|
461
|
+
allowlist
|
|
355
462
|
);
|
|
356
463
|
|
|
357
464
|
const examinedLinkRows = outgoingLinks.length + incomingCandidates;
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
} from "../types";
|
|
9
9
|
|
|
10
10
|
import { buildWikiMatchExpression } from "../../core/graph-resolver";
|
|
11
|
-
import { normalizeWikiName } from "../../core/links";
|
|
11
|
+
import { normalizeWikiName, stripWikiMdExt } from "../../core/links";
|
|
12
12
|
|
|
13
13
|
const SNAPSHOT_COLUMNS = `document_id AS documentId, collection, rel_path AS relPath,
|
|
14
14
|
docid, uri, title, mirror_hash AS mirrorHash, source_hash AS sourceHash,
|
|
@@ -101,6 +101,11 @@ export function createGraphReferenceStore(db: Database): GraphReferenceStore {
|
|
|
101
101
|
),
|
|
102
102
|
wiki_rel: normalizeWikiName(d.relPath),
|
|
103
103
|
wiki_stem: normalizeWikiName(d.relPath.replace(/\.[^/.]+$/, "")),
|
|
104
|
+
// Workspace links name a file by its last path segment from any
|
|
105
|
+
// collection; every such referrer may re-resolve after this change.
|
|
106
|
+
wiki_base: stripWikiMdExt(
|
|
107
|
+
normalizeWikiName(d.relPath.split("/").pop() ?? d.relPath)
|
|
108
|
+
),
|
|
104
109
|
}));
|
|
105
110
|
return db
|
|
106
111
|
.query<{ sourceId: number }, [string]>(`
|
|
@@ -110,7 +115,8 @@ export function createGraphReferenceStore(db: Database): GraphReferenceStore {
|
|
|
110
115
|
json_extract(value, '$.title') AS title,
|
|
111
116
|
json_extract(value, '$.wiki_title') AS wiki_title,
|
|
112
117
|
json_extract(value, '$.wiki_rel') AS wiki_rel,
|
|
113
|
-
json_extract(value, '$.wiki_stem') AS wiki_stem
|
|
118
|
+
json_extract(value, '$.wiki_stem') AS wiki_stem,
|
|
119
|
+
json_extract(value, '$.wiki_base') AS wiki_base
|
|
114
120
|
FROM json_each(?)
|
|
115
121
|
)
|
|
116
122
|
SELECT DISTINCT dl.source_doc_id AS sourceId
|
|
@@ -121,6 +127,11 @@ export function createGraphReferenceStore(db: Database): GraphReferenceStore {
|
|
|
121
127
|
(dl.link_type = 'wiki' AND ${buildWikiMatchExpression("t", "dl.target_ref_norm")})
|
|
122
128
|
)) OR
|
|
123
129
|
(dl.link_type = 'wiki' AND dl.target_ref_norm IN (t.wiki_title, t.wiki_rel, t.wiki_stem))
|
|
130
|
+
OR (dl.link_type = 'wiki' AND dl.target_collection IS NULL AND (
|
|
131
|
+
dl.target_ref_norm IN (t.wiki_base, t.wiki_base || '.md')
|
|
132
|
+
OR substr(dl.target_ref_norm, -length(t.wiki_base) - 1) = '/' || t.wiki_base
|
|
133
|
+
OR substr(dl.target_ref_norm, -length(t.wiki_base) - 4) = '/' || t.wiki_base || '.md'
|
|
134
|
+
))
|
|
124
135
|
OR (dl.link_type = 'markdown' AND t.collection = src.collection AND dl.target_ref_norm = t.rel_path)
|
|
125
136
|
)
|
|
126
137
|
ORDER BY dl.source_doc_id
|