@gmickel/gno 1.33.0 → 1.34.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 +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.1.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.34.1.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +46 -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/commands/models/pull.ts +32 -3
- 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 +138 -91
- 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
|
@@ -128,11 +128,7 @@ import {
|
|
|
128
128
|
classifyResolvedGraphEdge,
|
|
129
129
|
mergeGraphEdgeAudit,
|
|
130
130
|
} from "../../core/graph-edge-confidence";
|
|
131
|
-
import {
|
|
132
|
-
buildWikiBestMatchSubquery,
|
|
133
|
-
buildWikiBestRankMatchCountSubquery,
|
|
134
|
-
buildWikiBestRankSubquery,
|
|
135
|
-
} from "../../core/graph-resolver";
|
|
131
|
+
import { buildWikiBestMatchSubquery } from "../../core/graph-resolver";
|
|
136
132
|
import { buildContentPrefilterNeedles } from "../../core/link-relevance";
|
|
137
133
|
import { normalizeWikiName, stripWikiMdExt } from "../../core/links";
|
|
138
134
|
import {
|
|
@@ -179,6 +175,7 @@ import {
|
|
|
179
175
|
getLatestFileRefactorReceiptByPlanDigest as getStoredLatestFileRefactorReceiptByPlanDigest,
|
|
180
176
|
} from "./file-refactor-journal-store";
|
|
181
177
|
import { loadFts5Snowball } from "./fts5-snowball";
|
|
178
|
+
import { resolveGraphLinkTargets } from "./graph-link-resolver";
|
|
182
179
|
import { queryGraphNeighborsForSeeds } from "./graph-neighbors";
|
|
183
180
|
import {
|
|
184
181
|
appendExportManifest as appendStoredTraceExportManifest,
|
|
@@ -515,6 +512,28 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
515
512
|
}
|
|
516
513
|
}
|
|
517
514
|
|
|
515
|
+
/** Open an existing index with SQLite enforced query-only semantics. */
|
|
516
|
+
openReadOnly(dbPath: string): StoreResult<void> {
|
|
517
|
+
try {
|
|
518
|
+
this.db = new Database(dbPath, { readonly: true, strict: true });
|
|
519
|
+
this.dbPath = dbPath;
|
|
520
|
+
this.db.exec("PRAGMA query_only = ON");
|
|
521
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
522
|
+
this.contextGeneration += 1;
|
|
523
|
+
return ok(undefined);
|
|
524
|
+
} catch (cause) {
|
|
525
|
+
this.db?.close();
|
|
526
|
+
this.db = null;
|
|
527
|
+
return err(
|
|
528
|
+
"CONNECTION_FAILED",
|
|
529
|
+
cause instanceof Error
|
|
530
|
+
? cause.message
|
|
531
|
+
: "Failed to open database read-only",
|
|
532
|
+
cause
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
518
537
|
async close(): Promise<void> {
|
|
519
538
|
if (this.db) {
|
|
520
539
|
this.db.close();
|
|
@@ -1559,6 +1578,66 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1559
1578
|
}
|
|
1560
1579
|
}
|
|
1561
1580
|
|
|
1581
|
+
async listDocumentsForAudit(options: {
|
|
1582
|
+
collections: readonly string[];
|
|
1583
|
+
pathPrefixes: readonly string[];
|
|
1584
|
+
tags: readonly string[];
|
|
1585
|
+
limit: number;
|
|
1586
|
+
}): Promise<StoreResult<{ documents: DocumentRow[]; total: number }>> {
|
|
1587
|
+
try {
|
|
1588
|
+
const db = this.ensureOpen();
|
|
1589
|
+
const conditions = ["d.active = 1"];
|
|
1590
|
+
const params: (string | number)[] = [];
|
|
1591
|
+
if (options.collections.length > 0) {
|
|
1592
|
+
conditions.push("d.collection IN (SELECT value FROM json_each(?))");
|
|
1593
|
+
params.push(JSON.stringify(options.collections));
|
|
1594
|
+
}
|
|
1595
|
+
if (options.pathPrefixes.length > 0) {
|
|
1596
|
+
conditions.push(`EXISTS (
|
|
1597
|
+
SELECT 1 FROM json_each(?) prefix
|
|
1598
|
+
WHERE COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) = prefix.value
|
|
1599
|
+
OR (substr(COALESCE(NULLIF(d.record_source_path, ''), d.rel_path), 1, length(prefix.value)) = prefix.value
|
|
1600
|
+
AND substr(COALESCE(NULLIF(d.record_source_path, ''), d.rel_path), length(prefix.value) + 1, 1) = '/')
|
|
1601
|
+
)`);
|
|
1602
|
+
params.push(JSON.stringify(options.pathPrefixes));
|
|
1603
|
+
}
|
|
1604
|
+
if (options.tags.length > 0) {
|
|
1605
|
+
conditions.push(`NOT EXISTS (
|
|
1606
|
+
SELECT 1 FROM json_each(?) requested_tag
|
|
1607
|
+
WHERE NOT EXISTS (
|
|
1608
|
+
SELECT 1 FROM doc_tags dt
|
|
1609
|
+
WHERE dt.document_id = d.id AND dt.tag = requested_tag.value
|
|
1610
|
+
)
|
|
1611
|
+
)`);
|
|
1612
|
+
params.push(JSON.stringify(options.tags));
|
|
1613
|
+
}
|
|
1614
|
+
const where = conditions.join(" AND ");
|
|
1615
|
+
const total =
|
|
1616
|
+
db
|
|
1617
|
+
.query<{ count: number }, (string | number)[]>(
|
|
1618
|
+
`SELECT COUNT(*) AS count FROM documents d WHERE ${where}`
|
|
1619
|
+
)
|
|
1620
|
+
.get(...params)?.count ?? 0;
|
|
1621
|
+
const rows = db
|
|
1622
|
+
.query<DbDocumentRow, (string | number)[]>(
|
|
1623
|
+
`SELECT d.* FROM documents d
|
|
1624
|
+
WHERE ${where}
|
|
1625
|
+
ORDER BY d.id
|
|
1626
|
+
LIMIT ?`
|
|
1627
|
+
)
|
|
1628
|
+
.all(...params, options.limit);
|
|
1629
|
+
return ok({ documents: rows.map(mapDocumentRow), total });
|
|
1630
|
+
} catch (cause) {
|
|
1631
|
+
return err(
|
|
1632
|
+
"QUERY_FAILED",
|
|
1633
|
+
cause instanceof Error
|
|
1634
|
+
? cause.message
|
|
1635
|
+
: "Failed to select bounded audit documents",
|
|
1636
|
+
cause
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1562
1641
|
async listRecordDocuments(
|
|
1563
1642
|
collection: string,
|
|
1564
1643
|
sourcePath: string
|
|
@@ -4675,9 +4754,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
4675
4754
|
match_count: number | null;
|
|
4676
4755
|
}
|
|
4677
4756
|
|
|
4678
|
-
interface
|
|
4757
|
+
interface GraphLinkResolutionRow {
|
|
4758
|
+
source_id: number;
|
|
4759
|
+
source_docid: string;
|
|
4760
|
+
source_collection: string;
|
|
4761
|
+
target_ref_norm: string;
|
|
4762
|
+
target_collection: string | null;
|
|
4679
4763
|
link_type: "wiki" | "markdown";
|
|
4680
|
-
unresolved: number;
|
|
4681
4764
|
}
|
|
4682
4765
|
|
|
4683
4766
|
interface NodeMetaRow {
|
|
@@ -4689,104 +4772,68 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
4689
4772
|
rel_path: string;
|
|
4690
4773
|
}
|
|
4691
4774
|
|
|
4692
|
-
const
|
|
4693
|
-
let
|
|
4775
|
+
const linkParams: string[] = [];
|
|
4776
|
+
let sourceCollectionClause = "";
|
|
4694
4777
|
if (collection) {
|
|
4695
|
-
|
|
4696
|
-
|
|
4778
|
+
sourceCollectionClause = "AND src.collection = ?";
|
|
4779
|
+
linkParams.push(collection);
|
|
4697
4780
|
}
|
|
4698
4781
|
|
|
4699
|
-
const
|
|
4782
|
+
const graphLinkRows = db
|
|
4783
|
+
.query<GraphLinkResolutionRow, string[]>(
|
|
4784
|
+
`
|
|
4700
4785
|
SELECT
|
|
4701
4786
|
src.id as source_id,
|
|
4702
4787
|
src.docid as source_docid,
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
dl.
|
|
4706
|
-
|
|
4707
|
-
WHEN 'wiki' THEN (${buildWikiBestRankSubquery(
|
|
4708
|
-
"COALESCE(dl.target_collection, src.collection)",
|
|
4709
|
-
"dl.target_ref_norm"
|
|
4710
|
-
)})
|
|
4711
|
-
WHEN 'markdown' THEN 5
|
|
4712
|
-
END as match_rank,
|
|
4713
|
-
CASE dl.link_type
|
|
4714
|
-
WHEN 'wiki' THEN (${buildWikiBestRankMatchCountSubquery(
|
|
4715
|
-
"COALESCE(dl.target_collection, src.collection)",
|
|
4716
|
-
"dl.target_ref_norm"
|
|
4717
|
-
)})
|
|
4718
|
-
WHEN 'markdown' THEN 1
|
|
4719
|
-
END as match_count
|
|
4788
|
+
src.collection as source_collection,
|
|
4789
|
+
dl.target_ref_norm,
|
|
4790
|
+
dl.target_collection,
|
|
4791
|
+
dl.link_type
|
|
4720
4792
|
FROM documents src
|
|
4721
4793
|
JOIN doc_links dl ON dl.source_doc_id = src.id
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
)})
|
|
4727
|
-
WHEN 'markdown' THEN (
|
|
4728
|
-
SELECT md.id FROM documents md
|
|
4729
|
-
WHERE md.active = 1
|
|
4730
|
-
AND md.collection = COALESCE(dl.target_collection, src.collection)
|
|
4731
|
-
AND md.rel_path = dl.target_ref_norm
|
|
4732
|
-
ORDER BY md.id LIMIT 1
|
|
4733
|
-
)
|
|
4734
|
-
END
|
|
4735
|
-
WHERE src.active = 1 AND tgt.active = 1
|
|
4736
|
-
${edgeCollectionClause}
|
|
4737
|
-
ORDER BY src.id ASC, tgt.id ASC, dl.link_type ASC
|
|
4738
|
-
`;
|
|
4739
|
-
|
|
4740
|
-
const resolvedEdgeRows = db
|
|
4741
|
-
.query<ResolvedEdgeRow, string[]>(resolvedEdgeQuery)
|
|
4742
|
-
.all(...edgeParams);
|
|
4743
|
-
|
|
4744
|
-
const unresolvedParams: string[] = [];
|
|
4745
|
-
let unresolvedCollectionClause = "";
|
|
4746
|
-
if (collection) {
|
|
4747
|
-
unresolvedCollectionClause = "AND src.collection = ?";
|
|
4748
|
-
unresolvedParams.push(collection);
|
|
4749
|
-
}
|
|
4750
|
-
const unresolvedQuery = `
|
|
4751
|
-
SELECT
|
|
4752
|
-
link_type,
|
|
4753
|
-
COUNT(*) as unresolved
|
|
4754
|
-
FROM (
|
|
4755
|
-
SELECT
|
|
4756
|
-
dl.link_type,
|
|
4757
|
-
CASE dl.link_type
|
|
4758
|
-
WHEN 'wiki' THEN (
|
|
4759
|
-
${buildWikiBestMatchSubquery(
|
|
4760
|
-
"COALESCE(dl.target_collection, src.collection)",
|
|
4761
|
-
"dl.target_ref_norm"
|
|
4762
|
-
)}
|
|
4763
|
-
)
|
|
4764
|
-
WHEN 'markdown' THEN (
|
|
4765
|
-
SELECT t.id FROM documents t
|
|
4766
|
-
WHERE t.active = 1
|
|
4767
|
-
AND t.collection = COALESCE(dl.target_collection, src.collection)
|
|
4768
|
-
AND t.rel_path = dl.target_ref_norm
|
|
4769
|
-
ORDER BY t.id LIMIT 1
|
|
4770
|
-
)
|
|
4771
|
-
END as target_id
|
|
4772
|
-
FROM documents src
|
|
4773
|
-
JOIN doc_links dl ON dl.source_doc_id = src.id
|
|
4774
|
-
WHERE src.active = 1
|
|
4775
|
-
${unresolvedCollectionClause}
|
|
4794
|
+
WHERE src.active = 1
|
|
4795
|
+
${sourceCollectionClause}
|
|
4796
|
+
ORDER BY src.id ASC, dl.id ASC
|
|
4797
|
+
`
|
|
4776
4798
|
)
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4799
|
+
.all(...linkParams);
|
|
4800
|
+
const resolutions = resolveGraphLinkTargets(
|
|
4801
|
+
db,
|
|
4802
|
+
graphLinkRows.map((row) => ({
|
|
4803
|
+
targetRefNorm: row.target_ref_norm,
|
|
4804
|
+
targetCollection: row.target_collection ?? row.source_collection,
|
|
4805
|
+
linkType: row.link_type,
|
|
4806
|
+
}))
|
|
4807
|
+
);
|
|
4808
|
+
const resolvedEdgeRows: ResolvedEdgeRow[] = [];
|
|
4783
4809
|
const unresolvedByType: Record<"wiki" | "markdown", number> = {
|
|
4784
4810
|
wiki: 0,
|
|
4785
4811
|
markdown: 0,
|
|
4786
4812
|
};
|
|
4787
|
-
for (const row of
|
|
4788
|
-
|
|
4813
|
+
for (const [index, row] of graphLinkRows.entries()) {
|
|
4814
|
+
const resolution = resolutions[index];
|
|
4815
|
+
if (!resolution) {
|
|
4816
|
+
unresolvedByType[row.link_type] += 1;
|
|
4817
|
+
continue;
|
|
4818
|
+
}
|
|
4819
|
+
const targetCollection = row.target_collection ?? row.source_collection;
|
|
4820
|
+
if (collection && targetCollection !== collection) continue;
|
|
4821
|
+
resolvedEdgeRows.push({
|
|
4822
|
+
source_id: row.source_id,
|
|
4823
|
+
source_docid: row.source_docid,
|
|
4824
|
+
target_id: resolution.targetId,
|
|
4825
|
+
target_docid: resolution.targetDocid,
|
|
4826
|
+
link_type: row.link_type,
|
|
4827
|
+
match_rank: resolution.matchRank,
|
|
4828
|
+
match_count: resolution.matchCount,
|
|
4829
|
+
});
|
|
4789
4830
|
}
|
|
4831
|
+
resolvedEdgeRows.sort(
|
|
4832
|
+
(left, right) =>
|
|
4833
|
+
left.source_id - right.source_id ||
|
|
4834
|
+
left.target_id - right.target_id ||
|
|
4835
|
+
left.link_type.localeCompare(right.link_type)
|
|
4836
|
+
);
|
|
4790
4837
|
const totalEdgesUnresolved =
|
|
4791
4838
|
unresolvedByType.wiki + unresolvedByType.markdown;
|
|
4792
4839
|
|
|
@@ -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
|
+
};
|