@hraness/kb 0.17.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/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/graph.ts
|
|
3
|
+
import { posix } from "path";
|
|
4
|
+
import {
|
|
5
|
+
isMap,
|
|
6
|
+
isNode,
|
|
7
|
+
isScalar,
|
|
8
|
+
isSeq,
|
|
9
|
+
LineCounter,
|
|
10
|
+
parseDocument
|
|
11
|
+
} from "yaml";
|
|
12
|
+
|
|
13
|
+
// src/portfolio-identity.ts
|
|
14
|
+
var MAX_PORTFOLIO_NAME_BYTES = 64;
|
|
15
|
+
var MAX_DOCUMENT_ID_BYTES = 128;
|
|
16
|
+
var namePattern = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/u;
|
|
17
|
+
var documentIdPattern = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/u;
|
|
18
|
+
var qualifiedUriPattern = /^kb:\/\/([a-z0-9][a-z0-9._-]{0,63})\/([a-z0-9][a-z0-9._-]{0,63})\/([a-z0-9][a-z0-9._-]{0,127})$/u;
|
|
19
|
+
function boundedAsciiName(value, label) {
|
|
20
|
+
if (typeof value !== "string" || !namePattern.test(value)) {
|
|
21
|
+
throw new TypeError(`${label} must be a canonical lowercase ASCII name using letters, digits, dots, underscores, or hyphens.`);
|
|
22
|
+
}
|
|
23
|
+
if (Buffer.byteLength(value, "utf8") > MAX_PORTFOLIO_NAME_BYTES) {
|
|
24
|
+
throw new RangeError(`${label} must be at most ${MAX_PORTFOLIO_NAME_BYTES} UTF-8 bytes.`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function portfolioVaultIdentity(owner, id) {
|
|
29
|
+
const checkedOwner = boundedAsciiName(owner, "Portfolio vault owner");
|
|
30
|
+
const checkedId = boundedAsciiName(id, "Portfolio vault ID");
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
owner: checkedOwner,
|
|
33
|
+
id: checkedId,
|
|
34
|
+
key: `${checkedOwner}/${checkedId}`
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function parseVaultKey(value) {
|
|
38
|
+
if (typeof value !== "string")
|
|
39
|
+
throw new TypeError("Portfolio vault key must be a string.");
|
|
40
|
+
const separator = value.indexOf("/");
|
|
41
|
+
if (separator < 1 || separator !== value.lastIndexOf("/")) {
|
|
42
|
+
throw new TypeError("Portfolio vault key must have the canonical owner/id form.");
|
|
43
|
+
}
|
|
44
|
+
const identity = portfolioVaultIdentity(value.slice(0, separator), value.slice(separator + 1));
|
|
45
|
+
if (identity.key !== value) {
|
|
46
|
+
throw new TypeError("Portfolio vault key must have the canonical owner/id form.");
|
|
47
|
+
}
|
|
48
|
+
return identity;
|
|
49
|
+
}
|
|
50
|
+
function parseDocumentId(value) {
|
|
51
|
+
if (typeof value !== "string" || !documentIdPattern.test(value)) {
|
|
52
|
+
throw new TypeError("document_id must be a canonical lowercase ASCII ID using letters, digits, dots, underscores, or hyphens.");
|
|
53
|
+
}
|
|
54
|
+
if (Buffer.byteLength(value, "utf8") > MAX_DOCUMENT_ID_BYTES) {
|
|
55
|
+
throw new RangeError(`document_id must be at most ${MAX_DOCUMENT_ID_BYTES} UTF-8 bytes.`);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function metadataValues(metadata, normalizedName) {
|
|
60
|
+
return Object.entries(metadata).filter(([name]) => name.normalize("NFC").toLocaleLowerCase("en-US") === normalizedName).map(([, value]) => value);
|
|
61
|
+
}
|
|
62
|
+
function documentIdState(metadata) {
|
|
63
|
+
const values = metadataValues(metadata, "document_id");
|
|
64
|
+
if (values.length === 0)
|
|
65
|
+
return Object.freeze({ kind: "missing" });
|
|
66
|
+
if (values.length !== 1) {
|
|
67
|
+
return Object.freeze({ kind: "invalid", value: Object.freeze([...values]) });
|
|
68
|
+
}
|
|
69
|
+
const value = values[0];
|
|
70
|
+
try {
|
|
71
|
+
return Object.freeze({ kind: "valid", documentId: parseDocumentId(value) });
|
|
72
|
+
} catch {
|
|
73
|
+
return Object.freeze({ kind: "invalid", value });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function formatQualifiedDocumentUri(vault, documentId) {
|
|
77
|
+
const checkedVault = portfolioVaultIdentity(vault.owner, vault.id);
|
|
78
|
+
const checkedDocumentId = parseDocumentId(documentId);
|
|
79
|
+
return `kb://${checkedVault.owner}/${checkedVault.id}/${checkedDocumentId}`;
|
|
80
|
+
}
|
|
81
|
+
function parseQualifiedDocumentUri(value) {
|
|
82
|
+
if (typeof value !== "string")
|
|
83
|
+
throw new TypeError("Qualified document URI must be a string.");
|
|
84
|
+
const match = qualifiedUriPattern.exec(value);
|
|
85
|
+
if (match === null) {
|
|
86
|
+
throw new TypeError("Qualified document URI must have canonical kb://owner/vault/document_id form.");
|
|
87
|
+
}
|
|
88
|
+
const vault = portfolioVaultIdentity(match[1], match[2]);
|
|
89
|
+
const documentId = parseDocumentId(match[3]);
|
|
90
|
+
const uri = formatQualifiedDocumentUri(vault, documentId);
|
|
91
|
+
if (uri !== value) {
|
|
92
|
+
throw new TypeError("Qualified document URI must be byte-canonical.");
|
|
93
|
+
}
|
|
94
|
+
return Object.freeze({ kind: "stable", stable: true, vault, documentId, uri });
|
|
95
|
+
}
|
|
96
|
+
function portfolioDocumentIdentity(vault, path, metadata) {
|
|
97
|
+
const checkedVault = portfolioVaultIdentity(vault.owner, vault.id);
|
|
98
|
+
const state = documentIdState(metadata);
|
|
99
|
+
if (state.kind === "valid") {
|
|
100
|
+
return Object.freeze({
|
|
101
|
+
kind: "stable",
|
|
102
|
+
stable: true,
|
|
103
|
+
vault: checkedVault,
|
|
104
|
+
documentId: state.documentId,
|
|
105
|
+
uri: formatQualifiedDocumentUri(checkedVault, state.documentId)
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
kind: "legacy-path",
|
|
110
|
+
stable: false,
|
|
111
|
+
vault: checkedVault,
|
|
112
|
+
path
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/graph.ts
|
|
117
|
+
var catalogStart = "<!-- kb:catalog:start -->";
|
|
118
|
+
var catalogEnd = "<!-- kb:catalog:end -->";
|
|
119
|
+
var MAX_ANALYZED_NOTES = 1e4;
|
|
120
|
+
var MAX_CONNECTION_OBSERVATIONS = 250000;
|
|
121
|
+
var MAX_MENTION_PAIRS = 1e6;
|
|
122
|
+
var MAX_MENTIONS = 50000;
|
|
123
|
+
|
|
124
|
+
class VaultAnalysisBudgetError extends RangeError {
|
|
125
|
+
kind;
|
|
126
|
+
limit;
|
|
127
|
+
constructor(kind, limit, message) {
|
|
128
|
+
super(message);
|
|
129
|
+
this.name = "VaultAnalysisBudgetError";
|
|
130
|
+
this.kind = kind;
|
|
131
|
+
this.limit = limit;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isMetadataObject(value) {
|
|
135
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
136
|
+
}
|
|
137
|
+
function parsedMetadataValue(value, ancestors) {
|
|
138
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
139
|
+
return { ok: true, value };
|
|
140
|
+
}
|
|
141
|
+
if (typeof value === "number") {
|
|
142
|
+
return Number.isFinite(value) && (!Number.isInteger(value) || Number.isSafeInteger(value)) ? { ok: true, value } : { ok: false };
|
|
143
|
+
}
|
|
144
|
+
if (typeof value !== "object" || ancestors.has(value))
|
|
145
|
+
return { ok: false };
|
|
146
|
+
ancestors.add(value);
|
|
147
|
+
try {
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
const parsed2 = [];
|
|
150
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
151
|
+
if (ownKeys.some((key) => typeof key !== "string" || key !== "length" && !/^(?:0|[1-9]\d*)$/u.test(key))) {
|
|
152
|
+
return { ok: false };
|
|
153
|
+
}
|
|
154
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
155
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
156
|
+
if (descriptor === undefined || !("value" in descriptor))
|
|
157
|
+
return { ok: false };
|
|
158
|
+
const result = parsedMetadataValue(descriptor.value, ancestors);
|
|
159
|
+
if (!result.ok)
|
|
160
|
+
return result;
|
|
161
|
+
parsed2.push(result.value);
|
|
162
|
+
}
|
|
163
|
+
return { ok: true, value: parsed2 };
|
|
164
|
+
}
|
|
165
|
+
const prototype = Object.getPrototypeOf(value);
|
|
166
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
167
|
+
return { ok: false };
|
|
168
|
+
const parsed = Object.create(null);
|
|
169
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
170
|
+
if (typeof key !== "string")
|
|
171
|
+
return { ok: false };
|
|
172
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
173
|
+
if (descriptor === undefined || !("value" in descriptor) || !descriptor.enumerable) {
|
|
174
|
+
return { ok: false };
|
|
175
|
+
}
|
|
176
|
+
const result = parsedMetadataValue(descriptor.value, ancestors);
|
|
177
|
+
if (!result.ok)
|
|
178
|
+
return result;
|
|
179
|
+
Object.defineProperty(parsed, key, {
|
|
180
|
+
configurable: false,
|
|
181
|
+
enumerable: true,
|
|
182
|
+
value: result.value,
|
|
183
|
+
writable: false
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return { ok: true, value: parsed };
|
|
187
|
+
} finally {
|
|
188
|
+
ancestors.delete(value);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function metadataValueFromUnknown(value) {
|
|
192
|
+
try {
|
|
193
|
+
const parsed = parsedMetadataValue(value, new WeakSet);
|
|
194
|
+
return parsed.ok ? parsed.value : undefined;
|
|
195
|
+
} catch {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function metadataObjectFromUnknown(value) {
|
|
200
|
+
if (value === null || Array.isArray(value) || typeof value !== "object")
|
|
201
|
+
return null;
|
|
202
|
+
const parsed = metadataValueFromUnknown(value);
|
|
203
|
+
return parsed !== undefined && isMetadataObject(parsed) ? parsed : null;
|
|
204
|
+
}
|
|
205
|
+
function emptyMetadata() {
|
|
206
|
+
return Object.create(null);
|
|
207
|
+
}
|
|
208
|
+
var relationPredicatePattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
|
|
209
|
+
var maxNoteIdLength = 2048;
|
|
210
|
+
function isCanonicalNoteId(value) {
|
|
211
|
+
if (value === "" || value.length > maxNoteIdLength || value !== value.trim() || value !== value.normalize("NFC") || value.includes("\\") || value.includes("\x00") || value.includes(`
|
|
212
|
+
`) || value.includes("\r") || value.startsWith("/") || value.endsWith("/") || value.toLocaleLowerCase("en-US").endsWith(".md")) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
const segments = value.split("/");
|
|
216
|
+
return !segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment.startsWith(".")) && posix.normalize(value) === value && posix.basename(`${value}.md`) !== "AGENTS.md";
|
|
217
|
+
}
|
|
218
|
+
function frontmatterSourceLine(lineCounter, node, fallback = 2) {
|
|
219
|
+
const offset = node?.range?.[0];
|
|
220
|
+
if (offset === undefined)
|
|
221
|
+
return fallback;
|
|
222
|
+
return lineCounter.linePos(offset).line + 1;
|
|
223
|
+
}
|
|
224
|
+
function malformedRelation(source, line, message, details = {}) {
|
|
225
|
+
return {
|
|
226
|
+
kind: "malformed",
|
|
227
|
+
source,
|
|
228
|
+
line,
|
|
229
|
+
...details,
|
|
230
|
+
message
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function relationTarget(node, predicate, source, lineCounter, fallbackLine) {
|
|
234
|
+
const line = frontmatterSourceLine(lineCounter, node, fallbackLine);
|
|
235
|
+
if (!isScalar(node) || typeof node.value !== "string") {
|
|
236
|
+
return malformedRelation(source, line, `Relation "${predicate}" targets must be non-empty strings.`, { predicate });
|
|
237
|
+
}
|
|
238
|
+
const target = node.value;
|
|
239
|
+
let qualified = false;
|
|
240
|
+
if (target.startsWith("kb://")) {
|
|
241
|
+
try {
|
|
242
|
+
parseQualifiedDocumentUri(target);
|
|
243
|
+
qualified = true;
|
|
244
|
+
} catch {
|
|
245
|
+
qualified = false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!isCanonicalNoteId(target) && !qualified) {
|
|
249
|
+
return malformedRelation(source, line, `Relation "${predicate}" target ${JSON.stringify(target)} must be an exact ` + "extensionless vault-root note ID or canonical kb:// document URI.", { predicate, target: node.value });
|
|
250
|
+
}
|
|
251
|
+
return { predicate, target, line };
|
|
252
|
+
}
|
|
253
|
+
function parsedRelations(contents, source, lineCounter) {
|
|
254
|
+
if (!isMap(contents))
|
|
255
|
+
return { relationDeclarations: [], relationIssues: [] };
|
|
256
|
+
const relationsPair = contents.items.find((pair) => isScalar(pair.key) && typeof pair.key.value === "string" && pair.key.value.toLocaleLowerCase("en-US") === "relations");
|
|
257
|
+
if (relationsPair === undefined) {
|
|
258
|
+
return { relationDeclarations: [], relationIssues: [] };
|
|
259
|
+
}
|
|
260
|
+
const relationsLine = frontmatterSourceLine(lineCounter, isScalar(relationsPair.key) ? relationsPair.key : null);
|
|
261
|
+
if (!isMap(relationsPair.value)) {
|
|
262
|
+
return {
|
|
263
|
+
relationDeclarations: [],
|
|
264
|
+
relationIssues: [
|
|
265
|
+
malformedRelation(source, relationsLine, "Relations must map strict lower-kebab predicates to a string or string list.")
|
|
266
|
+
]
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const relationDeclarations = [];
|
|
270
|
+
const relationIssues = [];
|
|
271
|
+
for (const pair of relationsPair.value.items) {
|
|
272
|
+
const predicateLine = frontmatterSourceLine(lineCounter, isNode(pair.key) ? pair.key : null, relationsLine);
|
|
273
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== "string") {
|
|
274
|
+
relationIssues.push(malformedRelation(source, predicateLine, "Relation predicates must be strict lower kebab-case strings."));
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const predicate = pair.key.value.normalize("NFC");
|
|
278
|
+
if (!relationPredicatePattern.test(predicate)) {
|
|
279
|
+
relationIssues.push(malformedRelation(source, predicateLine, `Invalid relation predicate "${predicate}"; use strict lower kebab-case.`, { predicate }));
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const nodes = isSeq(pair.value) ? pair.value.items : [pair.value];
|
|
283
|
+
for (const node of nodes) {
|
|
284
|
+
const parsed = relationTarget(isNode(node) ? node : null, predicate, source, lineCounter, predicateLine);
|
|
285
|
+
if ("kind" in parsed)
|
|
286
|
+
relationIssues.push(parsed);
|
|
287
|
+
else
|
|
288
|
+
relationDeclarations.push(parsed);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
relationDeclarations: relationDeclarations.toSorted((left, right) => left.predicate.localeCompare(right.predicate) || left.target.localeCompare(right.target) || left.line - right.line),
|
|
293
|
+
relationIssues: relationIssues.toSorted((left, right) => left.line - right.line || (left.predicate ?? "").localeCompare(right.predicate ?? "") || (left.target ?? "").localeCompare(right.target ?? ""))
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function parseMetadata(source, path) {
|
|
297
|
+
if (source.trim() === "") {
|
|
298
|
+
return {
|
|
299
|
+
metadata: emptyMetadata(),
|
|
300
|
+
relationDeclarations: [],
|
|
301
|
+
relationIssues: []
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
const lineCounter = new LineCounter;
|
|
306
|
+
const document = parseDocument(source, {
|
|
307
|
+
lineCounter,
|
|
308
|
+
schema: "core",
|
|
309
|
+
uniqueKeys: true
|
|
310
|
+
});
|
|
311
|
+
if (document.errors.length > 0) {
|
|
312
|
+
throw new Error("the YAML parser reported an error");
|
|
313
|
+
}
|
|
314
|
+
if (document.contents === null) {
|
|
315
|
+
return {
|
|
316
|
+
metadata: emptyMetadata(),
|
|
317
|
+
relationDeclarations: [],
|
|
318
|
+
relationIssues: []
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const parsed = document.toJS({ mapAsMap: false, maxAliasCount: 50 });
|
|
322
|
+
const metadata = metadataObjectFromUnknown(parsed);
|
|
323
|
+
if (metadata === null)
|
|
324
|
+
throw new Error("the YAML document is not a JSON-like object");
|
|
325
|
+
return {
|
|
326
|
+
metadata,
|
|
327
|
+
...parsedRelations(document.contents, path, lineCounter)
|
|
328
|
+
};
|
|
329
|
+
} catch (error) {
|
|
330
|
+
throw new Error(`Invalid YAML frontmatter in ${path}.`, { cause: error });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function metadataProperty(metadata, name) {
|
|
334
|
+
if (Object.hasOwn(metadata, name))
|
|
335
|
+
return metadata[name];
|
|
336
|
+
const lowerName = name.toLocaleLowerCase("en-US");
|
|
337
|
+
const matches = Object.keys(metadata).filter((key) => key.toLocaleLowerCase("en-US") === lowerName);
|
|
338
|
+
return matches.length === 1 ? metadata[matches[0] ?? ""] : undefined;
|
|
339
|
+
}
|
|
340
|
+
function normalizedTags(metadata) {
|
|
341
|
+
const value = metadataProperty(metadata, "tags");
|
|
342
|
+
const candidates = typeof value === "string" ? [value] : Array.isArray(value) ? value.filter((candidate) => typeof candidate === "string") : [];
|
|
343
|
+
const tags = [];
|
|
344
|
+
const seen = new Set;
|
|
345
|
+
for (const candidate of candidates) {
|
|
346
|
+
const tag = candidate.trim().replace(/^#+/u, "").normalize("NFC").toLocaleLowerCase("en-US");
|
|
347
|
+
if (tag === "" || seen.has(tag))
|
|
348
|
+
continue;
|
|
349
|
+
seen.add(tag);
|
|
350
|
+
tags.push(tag);
|
|
351
|
+
}
|
|
352
|
+
return tags;
|
|
353
|
+
}
|
|
354
|
+
var normalizeVaultPath = (path) => posix.normalize(path.replaceAll("\\", "/")).replace(/^\.\//, "");
|
|
355
|
+
var withoutMarkdownExtension = (path) => path.toLowerCase().endsWith(".md") ? path.slice(0, -3) : path;
|
|
356
|
+
function legacyPropertyValue(value) {
|
|
357
|
+
if (typeof value === "string")
|
|
358
|
+
return value;
|
|
359
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
360
|
+
return String(value);
|
|
361
|
+
if (value === null)
|
|
362
|
+
return "null";
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
function aliasesFromMetadata(metadata) {
|
|
366
|
+
const value = metadataProperty(metadata, "aliases");
|
|
367
|
+
if (typeof value === "string")
|
|
368
|
+
return value.trim() === "" ? [] : [value];
|
|
369
|
+
if (!Array.isArray(value))
|
|
370
|
+
return [];
|
|
371
|
+
return value.filter((candidate) => typeof candidate === "string" && candidate.trim() !== "");
|
|
372
|
+
}
|
|
373
|
+
function frontmatterOf(content, path) {
|
|
374
|
+
const lines = content.split(`
|
|
375
|
+
`);
|
|
376
|
+
if (lines[0]?.trim() !== "---") {
|
|
377
|
+
return {
|
|
378
|
+
values: new Map,
|
|
379
|
+
aliases: [],
|
|
380
|
+
tags: [],
|
|
381
|
+
metadata: emptyMetadata(),
|
|
382
|
+
relationDeclarations: [],
|
|
383
|
+
relationIssues: []
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
387
|
+
if (end === -1) {
|
|
388
|
+
throw new Error(`Invalid YAML frontmatter in ${path}: missing closing delimiter.`);
|
|
389
|
+
}
|
|
390
|
+
const parsed = parseMetadata(lines.slice(1, end).join(`
|
|
391
|
+
`), path);
|
|
392
|
+
const metadata = parsed.metadata;
|
|
393
|
+
const values = new Map;
|
|
394
|
+
const seenKeys = new Set;
|
|
395
|
+
for (const [authoredKey, typedValue] of Object.entries(metadata)) {
|
|
396
|
+
const key = authoredKey.toLocaleLowerCase("en-US");
|
|
397
|
+
if (seenKeys.has(key)) {
|
|
398
|
+
throw new Error(`Invalid YAML frontmatter in ${path}: keys must not differ only by case.`);
|
|
399
|
+
}
|
|
400
|
+
seenKeys.add(key);
|
|
401
|
+
if (key === "aliases")
|
|
402
|
+
continue;
|
|
403
|
+
const value = legacyPropertyValue(typedValue);
|
|
404
|
+
if (value !== undefined && value !== "")
|
|
405
|
+
values.set(key, value);
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
values,
|
|
409
|
+
aliases: aliasesFromMetadata(metadata),
|
|
410
|
+
tags: normalizedTags(metadata),
|
|
411
|
+
metadata,
|
|
412
|
+
relationDeclarations: parsed.relationDeclarations,
|
|
413
|
+
relationIssues: parsed.relationIssues
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function searchableMarkdown(content) {
|
|
417
|
+
const lines = content.split(`
|
|
418
|
+
`);
|
|
419
|
+
let inFrontmatter = lines[0]?.trim() === "---";
|
|
420
|
+
let inFence = null;
|
|
421
|
+
let inComment = false;
|
|
422
|
+
const blockMasked = lines.map((line, index) => {
|
|
423
|
+
if (inFrontmatter) {
|
|
424
|
+
if (index > 0 && line.trim() === "---")
|
|
425
|
+
inFrontmatter = false;
|
|
426
|
+
return "";
|
|
427
|
+
}
|
|
428
|
+
const fence = /^\s{0,3}(`{3,}|~{3,})/.exec(line);
|
|
429
|
+
if (inFence !== null) {
|
|
430
|
+
const delimiter = /^\s{0,3}(`{3,}|~{3,})[\t ]*$/.exec(line)?.[1];
|
|
431
|
+
if (delimiter !== undefined && delimiter[0] === inFence.character && delimiter.length >= inFence.length)
|
|
432
|
+
inFence = null;
|
|
433
|
+
return "";
|
|
434
|
+
}
|
|
435
|
+
const openingDelimiter = fence?.[1];
|
|
436
|
+
const openingRemainder = fence === null ? "" : line.slice(fence[0].length);
|
|
437
|
+
if (openingDelimiter !== undefined && (openingDelimiter[0] === "~" || !openingRemainder.includes("`"))) {
|
|
438
|
+
const delimiter = openingDelimiter;
|
|
439
|
+
inFence = { character: delimiter[0] ?? "`", length: delimiter.length };
|
|
440
|
+
return "";
|
|
441
|
+
}
|
|
442
|
+
if (/^(?: {4,}|\t)/u.test(line))
|
|
443
|
+
return "";
|
|
444
|
+
let output = line;
|
|
445
|
+
if (inComment) {
|
|
446
|
+
const close = output.indexOf("-->");
|
|
447
|
+
if (close === -1)
|
|
448
|
+
return "";
|
|
449
|
+
output = output.slice(close + 3);
|
|
450
|
+
inComment = false;
|
|
451
|
+
}
|
|
452
|
+
for (;; ) {
|
|
453
|
+
const open = output.indexOf("<!--");
|
|
454
|
+
if (open === -1)
|
|
455
|
+
break;
|
|
456
|
+
const close = output.indexOf("-->", open + 4);
|
|
457
|
+
if (close === -1) {
|
|
458
|
+
output = output.slice(0, open);
|
|
459
|
+
inComment = true;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
output = output.slice(0, open) + output.slice(close + 3);
|
|
463
|
+
}
|
|
464
|
+
return output;
|
|
465
|
+
}).join(`
|
|
466
|
+
`);
|
|
467
|
+
return maskInlineCodeBlocks(maskHtmlCodeBlocks(blockMasked));
|
|
468
|
+
}
|
|
469
|
+
function maskInlineCodeSpans(value) {
|
|
470
|
+
const runs = [];
|
|
471
|
+
for (let index = 0;index < value.length; ) {
|
|
472
|
+
if (value[index] !== "`") {
|
|
473
|
+
index += 1;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const start = index;
|
|
477
|
+
while (value[index] === "`")
|
|
478
|
+
index += 1;
|
|
479
|
+
let precedingBackslashes = 0;
|
|
480
|
+
for (let before = start - 1;before >= 0 && value[before] === "\\"; before -= 1) {
|
|
481
|
+
precedingBackslashes += 1;
|
|
482
|
+
}
|
|
483
|
+
if (precedingBackslashes % 2 === 1)
|
|
484
|
+
continue;
|
|
485
|
+
runs.push({ start, end: index, length: index - start });
|
|
486
|
+
}
|
|
487
|
+
const remaining = new Map;
|
|
488
|
+
for (const run of runs)
|
|
489
|
+
remaining.set(run.length, (remaining.get(run.length) ?? 0) + 1);
|
|
490
|
+
const output = [];
|
|
491
|
+
let cursor = 0;
|
|
492
|
+
let delimiterLength = null;
|
|
493
|
+
const appendSegment = (segment) => {
|
|
494
|
+
output.push(delimiterLength === null ? segment : segment.replace(/[^\n]/gu, ""));
|
|
495
|
+
};
|
|
496
|
+
for (const run of runs) {
|
|
497
|
+
appendSegment(value.slice(cursor, run.start));
|
|
498
|
+
remaining.set(run.length, (remaining.get(run.length) ?? 1) - 1);
|
|
499
|
+
if (delimiterLength === null) {
|
|
500
|
+
if ((remaining.get(run.length) ?? 0) > 0)
|
|
501
|
+
delimiterLength = run.length;
|
|
502
|
+
else
|
|
503
|
+
output.push(value.slice(run.start, run.end));
|
|
504
|
+
} else if (run.length === delimiterLength) {
|
|
505
|
+
delimiterLength = null;
|
|
506
|
+
}
|
|
507
|
+
cursor = run.end;
|
|
508
|
+
}
|
|
509
|
+
appendSegment(value.slice(cursor));
|
|
510
|
+
return output.join("");
|
|
511
|
+
}
|
|
512
|
+
function maskInlineCodeBlocks(value) {
|
|
513
|
+
const output = [];
|
|
514
|
+
let paragraph = [];
|
|
515
|
+
const flush = () => {
|
|
516
|
+
if (paragraph.length === 0)
|
|
517
|
+
return;
|
|
518
|
+
output.push(...maskInlineCodeSpans(paragraph.join(`
|
|
519
|
+
`)).split(`
|
|
520
|
+
`));
|
|
521
|
+
paragraph = [];
|
|
522
|
+
};
|
|
523
|
+
const startsIndependentBlock = (line) => /^\s{0,3}(?:#{1,6}(?:\s|$)|>|(?:[-+*]|\d+[.)])\s+)/u.test(line);
|
|
524
|
+
for (const line of value.split(`
|
|
525
|
+
`)) {
|
|
526
|
+
if (line.trim() === "") {
|
|
527
|
+
flush();
|
|
528
|
+
output.push("");
|
|
529
|
+
} else if (startsIndependentBlock(line)) {
|
|
530
|
+
flush();
|
|
531
|
+
output.push(maskInlineCodeSpans(line));
|
|
532
|
+
} else {
|
|
533
|
+
paragraph.push(line);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
flush();
|
|
537
|
+
return output.join(`
|
|
538
|
+
`);
|
|
539
|
+
}
|
|
540
|
+
function maskHtmlCodeBlocks(value) {
|
|
541
|
+
const tagPattern = /<\/?(pre|code|script|style)\b[^>]*>/giu;
|
|
542
|
+
const output = [];
|
|
543
|
+
let cursor = 0;
|
|
544
|
+
let activeTag = null;
|
|
545
|
+
let depth = 0;
|
|
546
|
+
const append = (segment) => {
|
|
547
|
+
output.push(activeTag === null ? segment : segment.replace(/[^\n]/gu, ""));
|
|
548
|
+
};
|
|
549
|
+
for (const match of value.matchAll(tagPattern)) {
|
|
550
|
+
const start = match.index ?? 0;
|
|
551
|
+
append(value.slice(cursor, start));
|
|
552
|
+
const tag = (match[1] ?? "").toLowerCase();
|
|
553
|
+
const closing = match[0].startsWith("</");
|
|
554
|
+
if (activeTag === null && !closing) {
|
|
555
|
+
activeTag = tag;
|
|
556
|
+
depth = 1;
|
|
557
|
+
} else if (activeTag === tag) {
|
|
558
|
+
if (closing)
|
|
559
|
+
depth -= 1;
|
|
560
|
+
else
|
|
561
|
+
depth += 1;
|
|
562
|
+
if (depth === 0)
|
|
563
|
+
activeTag = null;
|
|
564
|
+
}
|
|
565
|
+
output.push(match[0].replace(/[^\n]/gu, ""));
|
|
566
|
+
cursor = start + match[0].length;
|
|
567
|
+
}
|
|
568
|
+
append(value.slice(cursor));
|
|
569
|
+
return output.join("");
|
|
570
|
+
}
|
|
571
|
+
function markdownText(value) {
|
|
572
|
+
return value.replace(/!\[[^\]]*\]\([^)]*\)/g, "").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/\[\[([^|\]#]+)(?:#[^|\]]+)?(?:\|([^\]]+))?\]\]/g, (_whole, target, label) => label ?? posix.basename(target)).replace(/[*_~]/g, "").replace(/\s+/g, " ").trim();
|
|
573
|
+
}
|
|
574
|
+
function firstHeading(searchable) {
|
|
575
|
+
for (const line of searchable.split(`
|
|
576
|
+
`)) {
|
|
577
|
+
const heading = /^#\s+(.+?)\s*$/.exec(line);
|
|
578
|
+
if (heading !== null)
|
|
579
|
+
return markdownText(heading[1] ?? "");
|
|
580
|
+
}
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
function firstParagraph(searchable) {
|
|
584
|
+
const paragraphs = searchable.split(/\n\s*\n/);
|
|
585
|
+
for (const paragraph of paragraphs) {
|
|
586
|
+
const lines = paragraph.split(`
|
|
587
|
+
`).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#") && !line.startsWith("- ") && !line.startsWith("* ") && !line.startsWith("|") && !line.startsWith(">"));
|
|
588
|
+
const text = markdownText(lines.join(" "));
|
|
589
|
+
if (text !== "")
|
|
590
|
+
return text;
|
|
591
|
+
}
|
|
592
|
+
return "";
|
|
593
|
+
}
|
|
594
|
+
function concise(value, limit = 180) {
|
|
595
|
+
const text = markdownText(value);
|
|
596
|
+
if (text.length <= limit)
|
|
597
|
+
return text;
|
|
598
|
+
const prefix = text.slice(0, limit + 1);
|
|
599
|
+
const lastSpace = prefix.lastIndexOf(" ");
|
|
600
|
+
return (lastSpace >= Math.floor(limit * 0.65) ? prefix.slice(0, lastSpace) : text.slice(0, limit)).trimEnd() + "\u2026";
|
|
601
|
+
}
|
|
602
|
+
function wikiLinks(searchable) {
|
|
603
|
+
const links = [];
|
|
604
|
+
for (const match of searchable.matchAll(/(!?)\[\[([^\]\n]+)\]\]/g)) {
|
|
605
|
+
const offset = match.index ?? 0;
|
|
606
|
+
let precedingBackslashes = 0;
|
|
607
|
+
for (let before = offset - 1;before >= 0 && searchable[before] === "\\"; before -= 1) {
|
|
608
|
+
precedingBackslashes += 1;
|
|
609
|
+
}
|
|
610
|
+
if (precedingBackslashes % 2 === 1)
|
|
611
|
+
continue;
|
|
612
|
+
const inside = match[2] ?? "";
|
|
613
|
+
const separator = inside.indexOf("|");
|
|
614
|
+
const target = (separator === -1 ? inside : inside.slice(0, separator)).trim();
|
|
615
|
+
links.push({
|
|
616
|
+
target,
|
|
617
|
+
line: searchable.slice(0, offset).split(`
|
|
618
|
+
`).length,
|
|
619
|
+
embedded: (match[1] ?? "") === "!"
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
return links;
|
|
623
|
+
}
|
|
624
|
+
function parseNote(path, content) {
|
|
625
|
+
const notePath = normalizeVaultPath(path);
|
|
626
|
+
const metadata = frontmatterOf(content, notePath);
|
|
627
|
+
const searchable = searchableMarkdown(content);
|
|
628
|
+
const fallback = posix.basename(withoutMarkdownExtension(notePath)).replaceAll("-", " ");
|
|
629
|
+
const title = metadata.values.get("title") ?? firstHeading(searchable) ?? fallback;
|
|
630
|
+
const summary = concise(metadata.values.get("description") ?? firstParagraph(searchable));
|
|
631
|
+
return {
|
|
632
|
+
path: notePath,
|
|
633
|
+
id: withoutMarkdownExtension(notePath),
|
|
634
|
+
title,
|
|
635
|
+
aliases: metadata.aliases,
|
|
636
|
+
tags: metadata.tags,
|
|
637
|
+
properties: Object.fromEntries(metadata.values),
|
|
638
|
+
metadata: metadata.metadata,
|
|
639
|
+
content,
|
|
640
|
+
summary,
|
|
641
|
+
searchableText: searchable,
|
|
642
|
+
links: wikiLinks(searchable),
|
|
643
|
+
relationDeclarations: metadata.relationDeclarations,
|
|
644
|
+
relationIssues: metadata.relationIssues
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function decoded(value) {
|
|
648
|
+
try {
|
|
649
|
+
return decodeURIComponent(value);
|
|
650
|
+
} catch {
|
|
651
|
+
return value;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function lookupNote(notes, query) {
|
|
655
|
+
const trimmed = decoded(query.trim()).replaceAll("\\", "/").replace(/^\/+/, "");
|
|
656
|
+
if (trimmed === "")
|
|
657
|
+
return { kind: "missing", query };
|
|
658
|
+
const queryId = withoutMarkdownExtension(normalizeVaultPath(trimmed));
|
|
659
|
+
const exact = notes.find((note) => note.id === queryId);
|
|
660
|
+
if (exact !== undefined)
|
|
661
|
+
return { kind: "found", note: exact };
|
|
662
|
+
const lowerQueryId = queryId.toLocaleLowerCase("en-US");
|
|
663
|
+
const idMatches = notes.filter((note) => note.id.toLocaleLowerCase("en-US") === lowerQueryId);
|
|
664
|
+
const onlyIdMatch = idMatches.length === 1 ? idMatches[0] : undefined;
|
|
665
|
+
if (onlyIdMatch !== undefined)
|
|
666
|
+
return { kind: "found", note: onlyIdMatch };
|
|
667
|
+
if (idMatches.length > 1) {
|
|
668
|
+
return {
|
|
669
|
+
kind: "ambiguous",
|
|
670
|
+
query,
|
|
671
|
+
candidates: idMatches.toSorted((left, right) => left.path.localeCompare(right.path))
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
const lowerLabel = query.trim().toLocaleLowerCase("en-US");
|
|
675
|
+
const lowerBasename = posix.basename(queryId).toLocaleLowerCase("en-US");
|
|
676
|
+
const candidates = notes.filter((note) => {
|
|
677
|
+
const labels = [posix.basename(note.id), note.title, ...note.aliases].map((value) => value.toLocaleLowerCase("en-US"));
|
|
678
|
+
return labels.includes(lowerLabel) || labels.includes(lowerBasename);
|
|
679
|
+
}).toSorted((left, right) => left.path.localeCompare(right.path));
|
|
680
|
+
const onlyCandidate = candidates.length === 1 ? candidates[0] : undefined;
|
|
681
|
+
if (onlyCandidate !== undefined)
|
|
682
|
+
return { kind: "found", note: onlyCandidate };
|
|
683
|
+
if (candidates.length > 1)
|
|
684
|
+
return { kind: "ambiguous", query, candidates };
|
|
685
|
+
return { kind: "missing", query };
|
|
686
|
+
}
|
|
687
|
+
function resolveTarget(source, rawTarget, byId, byBasename) {
|
|
688
|
+
const targetWithoutAnchor = rawTarget.split("#", 1)[0]?.split("^", 1)[0]?.trim() ?? "";
|
|
689
|
+
if (targetWithoutAnchor === "")
|
|
690
|
+
return { kind: "note", id: source.id };
|
|
691
|
+
let target = decoded(targetWithoutAnchor).replaceAll("\\", "/");
|
|
692
|
+
const extension = posix.extname(target).toLowerCase();
|
|
693
|
+
if (extension !== "" && extension !== ".md")
|
|
694
|
+
return { kind: "attachment" };
|
|
695
|
+
target = withoutMarkdownExtension(target).replace(/^\//, "");
|
|
696
|
+
if (target.startsWith(".")) {
|
|
697
|
+
const relativeTarget = posix.normalize(posix.join(posix.dirname(source.id), target));
|
|
698
|
+
return byId.has(relativeTarget) ? { kind: "note", id: relativeTarget } : { kind: "broken" };
|
|
699
|
+
}
|
|
700
|
+
if (byId.has(target))
|
|
701
|
+
return { kind: "note", id: target };
|
|
702
|
+
if (target.includes("/"))
|
|
703
|
+
return { kind: "broken" };
|
|
704
|
+
const candidates = byBasename.get(target) ?? [];
|
|
705
|
+
if (candidates.length === 1)
|
|
706
|
+
return { kind: "note", id: candidates[0] ?? target };
|
|
707
|
+
if (candidates.length > 1)
|
|
708
|
+
return { kind: "ambiguous", candidates };
|
|
709
|
+
return { kind: "broken" };
|
|
710
|
+
}
|
|
711
|
+
var pairKey = (left, right) => left.localeCompare(right) <= 0 ? `${left}\x00${right}` : `${right}\x00${left}`;
|
|
712
|
+
function compareAuthoredRelations(left, right) {
|
|
713
|
+
return left.source.localeCompare(right.source) || left.predicate.localeCompare(right.predicate) || left.target.localeCompare(right.target) || left.provenance.line - right.provenance.line || left.provenance.authoredTarget.localeCompare(right.provenance.authoredTarget);
|
|
714
|
+
}
|
|
715
|
+
function compareExternalAuthoredRelations(left, right) {
|
|
716
|
+
return left.source.localeCompare(right.source) || left.predicate.localeCompare(right.predicate) || left.target.localeCompare(right.target) || left.provenance.line - right.provenance.line || left.provenance.authoredTarget.localeCompare(right.provenance.authoredTarget);
|
|
717
|
+
}
|
|
718
|
+
function compareRelationIssues(left, right) {
|
|
719
|
+
const predicateComparison = (left.predicate ?? "").localeCompare(right.predicate ?? "");
|
|
720
|
+
const targetComparison = (left.target ?? "").localeCompare(right.target ?? "");
|
|
721
|
+
return left.source.localeCompare(right.source) || left.line - right.line || left.kind.localeCompare(right.kind) || predicateComparison || targetComparison;
|
|
722
|
+
}
|
|
723
|
+
var wordCharacter = (value) => /[A-Za-z0-9]/.test(value);
|
|
724
|
+
function phraseOffset(lowerHaystack, lowerPhrase) {
|
|
725
|
+
let offset = lowerHaystack.indexOf(lowerPhrase);
|
|
726
|
+
while (offset !== -1) {
|
|
727
|
+
const before = offset === 0 ? "" : lowerHaystack[offset - 1] ?? "";
|
|
728
|
+
const afterIndex = offset + lowerPhrase.length;
|
|
729
|
+
const after = afterIndex >= lowerHaystack.length ? "" : lowerHaystack[afterIndex] ?? "";
|
|
730
|
+
const startsClean = !wordCharacter(lowerPhrase[0] ?? "") || !wordCharacter(before);
|
|
731
|
+
const endsClean = !wordCharacter(lowerPhrase.at(-1) ?? "") || !wordCharacter(after);
|
|
732
|
+
if (startsClean && endsClean)
|
|
733
|
+
return offset;
|
|
734
|
+
offset = lowerHaystack.indexOf(lowerPhrase, offset + 1);
|
|
735
|
+
}
|
|
736
|
+
return -1;
|
|
737
|
+
}
|
|
738
|
+
function candidatePhrases(note) {
|
|
739
|
+
const values = [note.title, ...note.aliases].map((value) => value.trim()).filter((value) => value.length >= 4 && /[A-Za-z]/.test(value));
|
|
740
|
+
return [...new Set(values)].sort((left, right) => right.length - left.length);
|
|
741
|
+
}
|
|
742
|
+
function uniquePhrasesByTarget(notes) {
|
|
743
|
+
const ownersByPhrase = new Map;
|
|
744
|
+
for (const note of notes) {
|
|
745
|
+
for (const phrase of candidatePhrases(note)) {
|
|
746
|
+
const lowerPhrase = phrase.toLocaleLowerCase("en-US");
|
|
747
|
+
const owners = ownersByPhrase.get(lowerPhrase) ?? new Map;
|
|
748
|
+
if (!owners.has(note.id))
|
|
749
|
+
owners.set(note.id, phrase);
|
|
750
|
+
ownersByPhrase.set(lowerPhrase, owners);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const phrasesByTarget = new Map;
|
|
754
|
+
for (const [lowerPhrase, owners] of ownersByPhrase) {
|
|
755
|
+
if (owners.size !== 1)
|
|
756
|
+
continue;
|
|
757
|
+
const owner = owners.entries().next().value;
|
|
758
|
+
if (owner === undefined)
|
|
759
|
+
continue;
|
|
760
|
+
const [targetId, phrase] = owner;
|
|
761
|
+
const phrases = phrasesByTarget.get(targetId) ?? [];
|
|
762
|
+
phrases.push({ phrase, lowerPhrase });
|
|
763
|
+
phrasesByTarget.set(targetId, phrases);
|
|
764
|
+
}
|
|
765
|
+
for (const phrases of phrasesByTarget.values()) {
|
|
766
|
+
phrases.sort((left, right) => right.lowerPhrase.length - left.lowerPhrase.length || left.lowerPhrase.localeCompare(right.lowerPhrase));
|
|
767
|
+
}
|
|
768
|
+
return phrasesByTarget;
|
|
769
|
+
}
|
|
770
|
+
function checkedAnalysisLimit(value, hardMaximum, option) {
|
|
771
|
+
const limit = value ?? hardMaximum;
|
|
772
|
+
if (!Number.isSafeInteger(limit) || limit < 0 || limit > hardMaximum) {
|
|
773
|
+
throw new RangeError(`${option} must be a safe integer from 0 through ${hardMaximum}.`);
|
|
774
|
+
}
|
|
775
|
+
return limit;
|
|
776
|
+
}
|
|
777
|
+
function analyzeVault(notes, options = {}) {
|
|
778
|
+
const maxNotes = checkedAnalysisLimit(options.maxNotes, MAX_ANALYZED_NOTES, "maxNotes");
|
|
779
|
+
const maxMentionPairs = checkedAnalysisLimit(options.maxMentionPairs, MAX_MENTION_PAIRS, "maxMentionPairs");
|
|
780
|
+
const maxMentions = checkedAnalysisLimit(options.maxMentions, MAX_MENTIONS, "maxMentions");
|
|
781
|
+
const maxConnectionObservations = checkedAnalysisLimit(options.maxConnectionObservations, MAX_CONNECTION_OBSERVATIONS, "maxConnectionObservations");
|
|
782
|
+
if (notes.length > maxNotes) {
|
|
783
|
+
throw new VaultAnalysisBudgetError("notes", maxNotes, `Vault analysis exceeds the ${maxNotes} note limit.`);
|
|
784
|
+
}
|
|
785
|
+
const catalogNoteId = withoutMarkdownExtension(normalizeVaultPath(options.catalogNoteId ?? "index"));
|
|
786
|
+
const byId = new Map(notes.map((note) => [note.id, note]));
|
|
787
|
+
const byBasename = new Map;
|
|
788
|
+
for (const note of notes) {
|
|
789
|
+
const basename = posix.basename(note.id);
|
|
790
|
+
const matches = byBasename.get(basename) ?? [];
|
|
791
|
+
matches.push(note.id);
|
|
792
|
+
byBasename.set(basename, matches);
|
|
793
|
+
}
|
|
794
|
+
for (const matches of byBasename.values())
|
|
795
|
+
matches.sort();
|
|
796
|
+
let connectionObservations = 0;
|
|
797
|
+
const observeConnection = () => {
|
|
798
|
+
if (connectionObservations >= maxConnectionObservations) {
|
|
799
|
+
throw new VaultAnalysisBudgetError("connection-observations", maxConnectionObservations, `Vault analysis exceeds the ${maxConnectionObservations} ` + "connection-observation limit.");
|
|
800
|
+
}
|
|
801
|
+
connectionObservations += 1;
|
|
802
|
+
};
|
|
803
|
+
const issues = [];
|
|
804
|
+
const relationIssues = [];
|
|
805
|
+
for (const note of notes) {
|
|
806
|
+
for (const issue of note.relationIssues ?? []) {
|
|
807
|
+
observeConnection();
|
|
808
|
+
relationIssues.push(issue);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const contextualLinks = [];
|
|
812
|
+
const edgeKeys = new Set;
|
|
813
|
+
for (const source of notes) {
|
|
814
|
+
for (const link of source.links) {
|
|
815
|
+
observeConnection();
|
|
816
|
+
const resolution = resolveTarget(source, link.target, byId, byBasename);
|
|
817
|
+
if (resolution.kind === "attachment")
|
|
818
|
+
continue;
|
|
819
|
+
if (resolution.kind === "broken") {
|
|
820
|
+
issues.push({ kind: "broken", source: source.path, line: link.line, target: link.target });
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (resolution.kind === "ambiguous") {
|
|
824
|
+
issues.push({
|
|
825
|
+
kind: "ambiguous",
|
|
826
|
+
source: source.path,
|
|
827
|
+
line: link.line,
|
|
828
|
+
target: link.target,
|
|
829
|
+
candidates: resolution.candidates
|
|
830
|
+
});
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (source.id === catalogNoteId || resolution.id === catalogNoteId || source.id === resolution.id)
|
|
834
|
+
continue;
|
|
835
|
+
const edgeKey = `${source.id}\x00${resolution.id}`;
|
|
836
|
+
if (edgeKeys.has(edgeKey))
|
|
837
|
+
continue;
|
|
838
|
+
edgeKeys.add(edgeKey);
|
|
839
|
+
contextualLinks.push({ source: source.path, target: `${resolution.id}.md`, line: link.line });
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const authoredRelations = [];
|
|
843
|
+
const externalAuthoredRelations = [];
|
|
844
|
+
const declarationKeys = new Set;
|
|
845
|
+
const relationEdgeKeys = new Set;
|
|
846
|
+
for (const source of notes) {
|
|
847
|
+
const declarations = [];
|
|
848
|
+
for (const declaration of source.relationDeclarations ?? []) {
|
|
849
|
+
observeConnection();
|
|
850
|
+
declarations.push(declaration);
|
|
851
|
+
}
|
|
852
|
+
declarations.sort((left, right) => left.line - right.line || left.predicate.localeCompare(right.predicate) || left.target.localeCompare(right.target));
|
|
853
|
+
for (const declaration of declarations) {
|
|
854
|
+
const declarationKey = `${source.id}\x00${declaration.predicate}\x00${declaration.target}`;
|
|
855
|
+
if (declarationKeys.has(declarationKey))
|
|
856
|
+
continue;
|
|
857
|
+
declarationKeys.add(declarationKey);
|
|
858
|
+
if (declaration.target.startsWith("kb://")) {
|
|
859
|
+
if (source.id !== catalogNoteId) {
|
|
860
|
+
externalAuthoredRelations.push({
|
|
861
|
+
source: source.id,
|
|
862
|
+
target: parseQualifiedDocumentUri(declaration.target).uri,
|
|
863
|
+
predicate: declaration.predicate,
|
|
864
|
+
provenance: {
|
|
865
|
+
kind: "frontmatter",
|
|
866
|
+
source: source.path,
|
|
867
|
+
line: declaration.line,
|
|
868
|
+
authoredTarget: declaration.target
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
const exactTarget = byId.get(declaration.target);
|
|
875
|
+
if (exactTarget === undefined) {
|
|
876
|
+
const basenameMatches = declaration.target.includes("/") ? [] : (byBasename.get(declaration.target) ?? []).filter((candidate) => candidate !== declaration.target);
|
|
877
|
+
if (basenameMatches.length > 0) {
|
|
878
|
+
relationIssues.push(malformedRelation(source.path, declaration.line, `Relation "${declaration.predicate}" target ` + `${JSON.stringify(declaration.target)} is only a basename; ` + "store the exact vault-root note ID.", {
|
|
879
|
+
predicate: declaration.predicate,
|
|
880
|
+
target: declaration.target
|
|
881
|
+
}));
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
relationIssues.push({
|
|
885
|
+
kind: "broken",
|
|
886
|
+
source: source.path,
|
|
887
|
+
line: declaration.line,
|
|
888
|
+
predicate: declaration.predicate,
|
|
889
|
+
target: declaration.target
|
|
890
|
+
});
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
if (source.id === catalogNoteId || exactTarget.id === catalogNoteId)
|
|
894
|
+
continue;
|
|
895
|
+
const edgeKey = `${source.id}\x00${declaration.predicate}\x00${exactTarget.id}`;
|
|
896
|
+
if (relationEdgeKeys.has(edgeKey))
|
|
897
|
+
continue;
|
|
898
|
+
relationEdgeKeys.add(edgeKey);
|
|
899
|
+
authoredRelations.push({
|
|
900
|
+
source: source.id,
|
|
901
|
+
target: exactTarget.id,
|
|
902
|
+
predicate: declaration.predicate,
|
|
903
|
+
provenance: {
|
|
904
|
+
kind: "frontmatter",
|
|
905
|
+
source: source.path,
|
|
906
|
+
line: declaration.line,
|
|
907
|
+
authoredTarget: declaration.target
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const sortedContextualLinks = contextualLinks.toSorted((left, right) => left.source.localeCompare(right.source) || left.target.localeCompare(right.target) || left.line - right.line);
|
|
913
|
+
const sortedAuthoredRelations = authoredRelations.toSorted(compareAuthoredRelations);
|
|
914
|
+
const sortedExternalAuthoredRelations = externalAuthoredRelations.toSorted(compareExternalAuthoredRelations);
|
|
915
|
+
const backlinks = sortedContextualLinks.toSorted((left, right) => left.target.localeCompare(right.target) || left.source.localeCompare(right.source) || left.line - right.line);
|
|
916
|
+
const contentNotes = notes.filter((note) => note.id !== catalogNoteId);
|
|
917
|
+
const connected = new Set;
|
|
918
|
+
const linkedPairs = new Set;
|
|
919
|
+
const inboundById = new Map;
|
|
920
|
+
const outboundById = new Map;
|
|
921
|
+
const inboundRelationsById = new Map;
|
|
922
|
+
const outboundRelationsById = new Map;
|
|
923
|
+
for (const link of sortedContextualLinks) {
|
|
924
|
+
const sourceId = withoutMarkdownExtension(link.source);
|
|
925
|
+
const targetId = withoutMarkdownExtension(link.target);
|
|
926
|
+
connected.add(sourceId);
|
|
927
|
+
connected.add(targetId);
|
|
928
|
+
linkedPairs.add(pairKey(sourceId, targetId));
|
|
929
|
+
const inbound = inboundById.get(targetId) ?? [];
|
|
930
|
+
inbound.push(link);
|
|
931
|
+
inboundById.set(targetId, inbound);
|
|
932
|
+
outboundById.set(sourceId, (outboundById.get(sourceId) ?? 0) + 1);
|
|
933
|
+
}
|
|
934
|
+
for (const relation of sortedAuthoredRelations) {
|
|
935
|
+
connected.add(relation.source);
|
|
936
|
+
connected.add(relation.target);
|
|
937
|
+
linkedPairs.add(pairKey(relation.source, relation.target));
|
|
938
|
+
const inbound = inboundRelationsById.get(relation.target) ?? [];
|
|
939
|
+
inbound.push(relation);
|
|
940
|
+
inboundRelationsById.set(relation.target, inbound);
|
|
941
|
+
outboundRelationsById.set(relation.source, (outboundRelationsById.get(relation.source) ?? 0) + 1);
|
|
942
|
+
}
|
|
943
|
+
for (const relation of sortedExternalAuthoredRelations) {
|
|
944
|
+
connected.add(relation.source);
|
|
945
|
+
outboundRelationsById.set(relation.source, (outboundRelationsById.get(relation.source) ?? 0) + 1);
|
|
946
|
+
}
|
|
947
|
+
const includeInSuggestions = options.includeInSuggestions ?? (() => true);
|
|
948
|
+
const suggestionNotes = contentNotes.filter(includeInSuggestions);
|
|
949
|
+
const scopedMentionNotes = options.mentionScope === undefined ? suggestionNotes : suggestionNotes.filter(options.mentionScope);
|
|
950
|
+
const scopedMentionIds = new Set(scopedMentionNotes.map((note) => note.id));
|
|
951
|
+
const phrasesByTarget = scopedMentionNotes.length === 0 ? new Map : uniquePhrasesByTarget(suggestionNotes);
|
|
952
|
+
const mentions = [];
|
|
953
|
+
let mentionPairs = 0;
|
|
954
|
+
for (const source of suggestionNotes) {
|
|
955
|
+
const targets = options.mentionScope === undefined || scopedMentionIds.has(source.id) ? suggestionNotes : scopedMentionNotes;
|
|
956
|
+
let lowerSearchableText;
|
|
957
|
+
for (const target of targets) {
|
|
958
|
+
if (source.id === target.id)
|
|
959
|
+
continue;
|
|
960
|
+
if (mentionPairs >= maxMentionPairs) {
|
|
961
|
+
throw new VaultAnalysisBudgetError("mention-pairs", maxMentionPairs, `Vault analysis exceeds the ${maxMentionPairs} mention-pair limit.`);
|
|
962
|
+
}
|
|
963
|
+
mentionPairs += 1;
|
|
964
|
+
if (linkedPairs.has(pairKey(source.id, target.id)))
|
|
965
|
+
continue;
|
|
966
|
+
lowerSearchableText ??= source.searchableText.toLocaleLowerCase("en-US");
|
|
967
|
+
for (const { phrase, lowerPhrase } of phrasesByTarget.get(target.id) ?? []) {
|
|
968
|
+
const offset = phraseOffset(lowerSearchableText, lowerPhrase);
|
|
969
|
+
if (offset === -1)
|
|
970
|
+
continue;
|
|
971
|
+
if (mentions.length >= maxMentions) {
|
|
972
|
+
throw new VaultAnalysisBudgetError("mentions", maxMentions, `Vault analysis exceeds the ${maxMentions} mention limit.`);
|
|
973
|
+
}
|
|
974
|
+
mentions.push({
|
|
975
|
+
source: source.path,
|
|
976
|
+
line: source.searchableText.slice(0, offset).split(`
|
|
977
|
+
`).length,
|
|
978
|
+
target: target.path,
|
|
979
|
+
phrase
|
|
980
|
+
});
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
noteCount: contentNotes.length,
|
|
987
|
+
contextualLinks: sortedContextualLinks,
|
|
988
|
+
backlinks,
|
|
989
|
+
authoredRelations: sortedAuthoredRelations,
|
|
990
|
+
externalAuthoredRelations: sortedExternalAuthoredRelations,
|
|
991
|
+
noteConnections: contentNotes.map((note) => ({
|
|
992
|
+
id: note.id,
|
|
993
|
+
path: note.path,
|
|
994
|
+
inboundContextualCount: inboundById.get(note.id)?.length ?? 0,
|
|
995
|
+
outboundContextualCount: outboundById.get(note.id) ?? 0,
|
|
996
|
+
backlinks: inboundById.get(note.id) ?? [],
|
|
997
|
+
inboundRelationCount: inboundRelationsById.get(note.id)?.length ?? 0,
|
|
998
|
+
outboundRelationCount: outboundRelationsById.get(note.id) ?? 0,
|
|
999
|
+
relationBacklinks: inboundRelationsById.get(note.id) ?? []
|
|
1000
|
+
})).toSorted((left, right) => left.path.localeCompare(right.path)),
|
|
1001
|
+
issues: issues.sort((left, right) => left.source.localeCompare(right.source) || left.line - right.line || left.target.localeCompare(right.target)),
|
|
1002
|
+
relationIssues: relationIssues.toSorted(compareRelationIssues),
|
|
1003
|
+
orphans: suggestionNotes.filter((note) => !connected.has(note.id)).map((note) => note.path).sort(),
|
|
1004
|
+
mentions: mentions.sort((left, right) => left.source.localeCompare(right.source) || left.line - right.line || left.target.localeCompare(right.target))
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
var sectionTitle = (directory) => directory.split("-").map((part) => part === "" ? "" : (part[0]?.toUpperCase() ?? "") + part.slice(1)).join(" ");
|
|
1008
|
+
function safeCatalogCharacter(character) {
|
|
1009
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
1010
|
+
return codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint === 1564 || codePoint === 8206 || codePoint === 8207 || codePoint >= 8232 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297 ? " " : character;
|
|
1011
|
+
}
|
|
1012
|
+
function safeCatalogText(value, limit) {
|
|
1013
|
+
return [...concise(value, limit)].map(safeCatalogCharacter).join("").replaceAll("<!--", "\u2039!--").replaceAll("-->", "--\u203A").replaceAll("|", " \u2014 ").replaceAll("]]", "]").replace(/\s+/gu, " ").trim();
|
|
1014
|
+
}
|
|
1015
|
+
function safeWikiTarget(value) {
|
|
1016
|
+
return value.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
1017
|
+
}
|
|
1018
|
+
function renderCatalog(notes, catalogNoteId = "index") {
|
|
1019
|
+
const normalizedCatalogNoteId = withoutMarkdownExtension(normalizeVaultPath(catalogNoteId));
|
|
1020
|
+
const groups = new Map;
|
|
1021
|
+
for (const note of notes.filter((candidate) => candidate.id !== normalizedCatalogNoteId)) {
|
|
1022
|
+
const directory = note.id.includes("/") ? note.id.split("/", 1)[0] ?? "Notes" : "Notes";
|
|
1023
|
+
const group = groups.get(directory) ?? [];
|
|
1024
|
+
group.push(note);
|
|
1025
|
+
groups.set(directory, group);
|
|
1026
|
+
}
|
|
1027
|
+
const lines = [catalogStart, "## Note catalog", ""];
|
|
1028
|
+
if (groups.size === 0) {
|
|
1029
|
+
lines.push("_No durable notes have been filed yet._", "", catalogEnd);
|
|
1030
|
+
return lines.join(`
|
|
1031
|
+
`);
|
|
1032
|
+
}
|
|
1033
|
+
for (const [directory, group] of [...groups].sort(([left], [right]) => left.localeCompare(right))) {
|
|
1034
|
+
lines.push(`### ${safeCatalogText(sectionTitle(directory), 120) || "Notes"}`, "");
|
|
1035
|
+
for (const note of group.sort((left, right) => left.title.localeCompare(right.title) || left.id.localeCompare(right.id))) {
|
|
1036
|
+
const details = [
|
|
1037
|
+
note.properties.type === "plan" && note.properties.status !== undefined ? `Status: ${safeCatalogText(note.properties.status, 60)}.` : "",
|
|
1038
|
+
safeCatalogText(note.summary, 180)
|
|
1039
|
+
].filter((value) => value !== "").join(" ");
|
|
1040
|
+
const suffix = details === "" ? "" : ` \u2014 ${details}`;
|
|
1041
|
+
const label = safeCatalogText(note.title, 240) || safeCatalogText(posix.basename(note.id), 240) || "Untitled";
|
|
1042
|
+
lines.push(`- [[${safeWikiTarget(note.id)}|${label}]]${suffix}`);
|
|
1043
|
+
}
|
|
1044
|
+
lines.push("");
|
|
1045
|
+
}
|
|
1046
|
+
lines.push(catalogEnd);
|
|
1047
|
+
return lines.join(`
|
|
1048
|
+
`);
|
|
1049
|
+
}
|
|
1050
|
+
function replaceCatalog(indexContent, catalog) {
|
|
1051
|
+
const start = indexContent.indexOf(catalogStart);
|
|
1052
|
+
const end = indexContent.indexOf(catalogEnd);
|
|
1053
|
+
if (start === -1 && end === -1)
|
|
1054
|
+
return indexContent.trimEnd() + `
|
|
1055
|
+
|
|
1056
|
+
` + catalog + `
|
|
1057
|
+
`;
|
|
1058
|
+
if (start === -1 || end === -1 || end < start) {
|
|
1059
|
+
throw new Error("kb/index.md has a malformed managed catalog boundary");
|
|
1060
|
+
}
|
|
1061
|
+
if (indexContent.indexOf(catalogStart, start + catalogStart.length) !== -1 || indexContent.indexOf(catalogEnd, end + catalogEnd.length) !== -1) {
|
|
1062
|
+
throw new Error("kb/index.md has duplicate managed catalog boundaries");
|
|
1063
|
+
}
|
|
1064
|
+
return indexContent.slice(0, start) + catalog + indexContent.slice(end + catalogEnd.length);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
export { MAX_PORTFOLIO_NAME_BYTES, MAX_DOCUMENT_ID_BYTES, portfolioVaultIdentity, parseVaultKey, parseDocumentId, documentIdState, formatQualifiedDocumentUri, parseQualifiedDocumentUri, portfolioDocumentIdentity, catalogStart, catalogEnd, MAX_ANALYZED_NOTES, MAX_CONNECTION_OBSERVATIONS, MAX_MENTION_PAIRS, MAX_MENTIONS, VaultAnalysisBudgetError, metadataValueFromUnknown, isCanonicalNoteId, normalizeVaultPath, searchableMarkdown, wikiLinks, parseNote, lookupNote, analyzeVault, renderCatalog, replaceCatalog };
|