@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.
Files changed (200) hide show
  1. package/DISCLOSURE +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/agent-context.js +35 -0
  5. package/dist/agent-guide-audit.js +18 -0
  6. package/dist/attachments.js +19 -0
  7. package/dist/authoring.js +33 -0
  8. package/dist/benchmark.js +19 -0
  9. package/dist/capture.js +49 -0
  10. package/dist/cli.js +3646 -0
  11. package/dist/clip/acquire.js +57 -0
  12. package/dist/clip/args.js +21 -0
  13. package/dist/clip/bounded-byte-buffer.js +9 -0
  14. package/dist/clip/browser-profiles.js +17 -0
  15. package/dist/clip/bundle-reader.js +11 -0
  16. package/dist/clip/cli.js +27 -0
  17. package/dist/clip/cookies.js +21 -0
  18. package/dist/clip/defuddle-worker.js +84 -0
  19. package/dist/clip/doctor.js +32 -0
  20. package/dist/clip/jobs.js +784 -0
  21. package/dist/clip/network-proxy.js +9 -0
  22. package/dist/clip/network.js +28 -0
  23. package/dist/clip/persist.js +30 -0
  24. package/dist/clip/refresh.js +11 -0
  25. package/dist/clip/terminal.js +11 -0
  26. package/dist/evaluation-builder.js +12238 -0
  27. package/dist/evaluation-kb.js +26 -0
  28. package/dist/evaluation.js +35 -0
  29. package/dist/git.js +39 -0
  30. package/dist/graph.js +39 -0
  31. package/dist/index-01jj6rbv.js +1103 -0
  32. package/dist/index-06c9ctr6.js +738 -0
  33. package/dist/index-0kavxzqj.js +2480 -0
  34. package/dist/index-1gwbassd.js +743 -0
  35. package/dist/index-1n418kb9.js +827 -0
  36. package/dist/index-1vrd1rmn.js +56 -0
  37. package/dist/index-1xxnjn0d.js +171 -0
  38. package/dist/index-3rm7cz6h.js +373 -0
  39. package/dist/index-3v2z4f0q.js +328 -0
  40. package/dist/index-48pz4jpc.js +392 -0
  41. package/dist/index-4j3tt0c3.js +234 -0
  42. package/dist/index-5n05se68.js +307 -0
  43. package/dist/index-5vwpzb5a.js +764 -0
  44. package/dist/index-6g2pv9d2.js +370 -0
  45. package/dist/index-7fzc32gf.js +712 -0
  46. package/dist/index-84x0vjjp.js +432 -0
  47. package/dist/index-adx6khj5.js +488 -0
  48. package/dist/index-b88v3vtm.js +805 -0
  49. package/dist/index-cv6fh7z5.js +462 -0
  50. package/dist/index-cxfrakt7.js +1067 -0
  51. package/dist/index-d13v9ckt.js +150 -0
  52. package/dist/index-dycjbhw2.js +1943 -0
  53. package/dist/index-dyqwejk5.js +531 -0
  54. package/dist/index-e5fbsywq.js +658 -0
  55. package/dist/index-f984hw45.js +927 -0
  56. package/dist/index-gh719d91.js +63 -0
  57. package/dist/index-hgve9rh2.js +2046 -0
  58. package/dist/index-hya40gb2.js +474 -0
  59. package/dist/index-j4zgmzjr.js +152 -0
  60. package/dist/index-jsmvyyvf.js +1166 -0
  61. package/dist/index-kh0pwvdd.js +1974 -0
  62. package/dist/index-mqx4nd6v.js +119 -0
  63. package/dist/index-mxxxytys.js +1254 -0
  64. package/dist/index-n5dd7r0v.js +711 -0
  65. package/dist/index-npg9z1a4.js +431 -0
  66. package/dist/index-pj501bh1.js +140 -0
  67. package/dist/index-qry4vhxk.js +1041 -0
  68. package/dist/index-s2gw5aw9.js +199 -0
  69. package/dist/index-vvdj6kca.js +70 -0
  70. package/dist/index-w2zc0vwa.js +571 -0
  71. package/dist/index-x3fthpsc.js +585 -0
  72. package/dist/index-zr53sf63.js +44 -0
  73. package/dist/index-zxdy5pby.js +2728 -0
  74. package/dist/index-zzhgcwyt.js +966 -0
  75. package/dist/index.js +453 -0
  76. package/dist/navigation.js +13 -0
  77. package/dist/pdf.js +85 -0
  78. package/dist/percolate.js +24 -0
  79. package/dist/portfolio.js +77 -0
  80. package/dist/query.js +29 -0
  81. package/dist/repository-memory.js +60 -0
  82. package/dist/sdk.js +36 -0
  83. package/dist/search-rules.js +34 -0
  84. package/dist/search.js +22 -0
  85. package/dist/semantic.js +39 -0
  86. package/dist/source-inbox.js +17 -0
  87. package/dist/untrusted-content.js +32 -0
  88. package/dist/url-intelligence.js +74 -0
  89. package/dist/workflow.js +23 -0
  90. package/dist/workflows/decision-context.js +20 -0
  91. package/dist/workflows/explain-change.js +8 -0
  92. package/dist/workflows/index.js +28 -0
  93. package/dist/workflows/plan-radar.js +10 -0
  94. package/package.json +409 -0
  95. package/skills/kb/AGENTS.md +19 -0
  96. package/skills/kb/SKILL.md +82 -0
  97. package/skills/kb/agents/openai.yaml +4 -0
  98. package/skills/kb/references/pdf-review.md +51 -0
  99. package/skills/kb/references/percolate.md +144 -0
  100. package/skills/kb/references/plan-structure.md +105 -0
  101. package/skills/kb/references/plan.md +115 -0
  102. package/skills/kb/references/query.md +231 -0
  103. package/skills/kb/references/refresh.md +133 -0
  104. package/skills/kb/references/save-pdf.md +125 -0
  105. package/skills/kb/references/save-url.md +178 -0
  106. package/skills/kb/references/url-authentication.md +75 -0
  107. package/skills/kb/references/url-platforms.md +25 -0
  108. package/src/agent-context.ts +1241 -0
  109. package/src/agent-guide-audit.ts +750 -0
  110. package/src/attachments.ts +723 -0
  111. package/src/authoring.ts +1725 -0
  112. package/src/benchmark.ts +362 -0
  113. package/src/capture.ts +68 -0
  114. package/src/cli.ts +3660 -0
  115. package/src/clip/acquire.ts +1273 -0
  116. package/src/clip/archive-today.ts +402 -0
  117. package/src/clip/args.ts +423 -0
  118. package/src/clip/assets.ts +262 -0
  119. package/src/clip/bounded-byte-buffer.ts +75 -0
  120. package/src/clip/browser-profiles.ts +372 -0
  121. package/src/clip/bundle-reader.ts +608 -0
  122. package/src/clip/capture.ts +1054 -0
  123. package/src/clip/cli.ts +181 -0
  124. package/src/clip/cookies.ts +512 -0
  125. package/src/clip/doctor.ts +1110 -0
  126. package/src/clip/extract.ts +868 -0
  127. package/src/clip/jobs.ts +959 -0
  128. package/src/clip/lib.ts +861 -0
  129. package/src/clip/media.ts +1474 -0
  130. package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
  131. package/src/clip/metadata-search-tool/Cargo.toml +18 -0
  132. package/src/clip/metadata-search-tool/runner.ts +685 -0
  133. package/src/clip/metadata-search-tool/src/main.rs +389 -0
  134. package/src/clip/metadata-search.ts +687 -0
  135. package/src/clip/network-proxy.ts +627 -0
  136. package/src/clip/network.ts +950 -0
  137. package/src/clip/package-root.ts +54 -0
  138. package/src/clip/persist.ts +1472 -0
  139. package/src/clip/platforms.ts +1824 -0
  140. package/src/clip/refresh.ts +197 -0
  141. package/src/clip/structured.ts +432 -0
  142. package/src/clip/terminal.ts +197 -0
  143. package/src/clip/url-intelligence.ts +581 -0
  144. package/src/clip/url-metadata-backfill.ts +616 -0
  145. package/src/clip/url-metadata-cli.ts +251 -0
  146. package/src/clip/url-metadata.ts +1397 -0
  147. package/src/evaluation-analysis-v2.ts +1440 -0
  148. package/src/evaluation-builder.ts +2539 -0
  149. package/src/evaluation-corpus-authoring.ts +2052 -0
  150. package/src/evaluation-evidence.ts +1147 -0
  151. package/src/evaluation-execution-v2.ts +426 -0
  152. package/src/evaluation-implementation.ts +131 -0
  153. package/src/evaluation-kb-closure.ts +1430 -0
  154. package/src/evaluation-kb-runner-v2.ts +735 -0
  155. package/src/evaluation-kb-v2.ts +2567 -0
  156. package/src/evaluation-kb.ts +1018 -0
  157. package/src/evaluation-measurement-v2.ts +1462 -0
  158. package/src/evaluation-packing-v2.ts +452 -0
  159. package/src/evaluation-redaction.ts +28 -0
  160. package/src/evaluation-v2.ts +3857 -0
  161. package/src/evaluation.ts +1220 -0
  162. package/src/git.ts +1276 -0
  163. package/src/graph.ts +1487 -0
  164. package/src/index.ts +20 -0
  165. package/src/init.ts +123 -0
  166. package/src/navigation.ts +277 -0
  167. package/src/note-lock.ts +529 -0
  168. package/src/pdf/args.ts +202 -0
  169. package/src/pdf/capture.ts +298 -0
  170. package/src/pdf/cli.ts +159 -0
  171. package/src/pdf/extract.ts +303 -0
  172. package/src/pdf/layout.ts +618 -0
  173. package/src/pdf/markdown.ts +154 -0
  174. package/src/pdf/model.ts +234 -0
  175. package/src/pdf/ocr.ts +190 -0
  176. package/src/pdf/persist.ts +314 -0
  177. package/src/pdf/source.ts +135 -0
  178. package/src/pdf/tools.ts +146 -0
  179. package/src/pdf.ts +92 -0
  180. package/src/percolate.ts +877 -0
  181. package/src/portfolio-audit.ts +574 -0
  182. package/src/portfolio-identity.ts +162 -0
  183. package/src/portfolio-registry.ts +473 -0
  184. package/src/portfolio.ts +765 -0
  185. package/src/query.ts +646 -0
  186. package/src/repository-memory.ts +1150 -0
  187. package/src/sdk.ts +1452 -0
  188. package/src/search-rules.ts +712 -0
  189. package/src/search.ts +710 -0
  190. package/src/semantic-runtime.ts +1139 -0
  191. package/src/semantic.ts +2480 -0
  192. package/src/source-inbox.ts +232 -0
  193. package/src/untrusted-content.ts +401 -0
  194. package/src/url-intelligence.ts +104 -0
  195. package/src/vault.ts +636 -0
  196. package/src/workflow.ts +653 -0
  197. package/src/workflows/decision-context.ts +81 -0
  198. package/src/workflows/explain-change.ts +73 -0
  199. package/src/workflows/index.ts +3 -0
  200. package/src/workflows/plan-radar.ts +101 -0
@@ -0,0 +1,531 @@
1
+ // @bun
2
+ import {
3
+ MAX_ANALYZED_NOTES,
4
+ MAX_MENTIONS,
5
+ lookupNote
6
+ } from "./index-cxfrakt7.js";
7
+
8
+ // src/percolate.ts
9
+ import { createHash } from "crypto";
10
+ import { posix } from "path";
11
+ var DEFAULT_PERCOLATION_LIMIT = 100;
12
+ var MAX_PERCOLATION_LIMIT = 1000;
13
+ var DEFAULT_PERCOLATION_MIN_SUPPORT = 2;
14
+ var MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE = 100;
15
+ var MAX_PERCOLATION_NOTES = MAX_ANALYZED_NOTES;
16
+ var MAX_PERCOLATION_MENTION_PAIRS = 250000;
17
+ var MAX_PERCOLATION_MENTIONS = MAX_MENTIONS;
18
+ var MAX_SCOPED_PERCOLATION_MENTION_PAIRS = MAX_PERCOLATION_NOTES * 2;
19
+ var MAX_PERCOLATION_EVIDENCE = 250000;
20
+ var MAX_PERCOLATION_PAIR_OBSERVATIONS = MAX_PERCOLATION_MENTION_PAIRS;
21
+ function compareText(left, right) {
22
+ return left < right ? -1 : left > right ? 1 : 0;
23
+ }
24
+ function pairKey(left, right) {
25
+ return compareText(left, right) <= 0 ? `${left}\x00${right}` : `${right}\x00${left}`;
26
+ }
27
+ function directedKey(source, target) {
28
+ return `${source}\x00${target}`;
29
+ }
30
+ function relationKey(source, predicate, target) {
31
+ return `${source}\x00${predicate}\x00${target}`;
32
+ }
33
+ function checkedLine(line, context) {
34
+ if (!Number.isSafeInteger(line) || line < 1) {
35
+ throw new TypeError(`${context} has an invalid evidence line.`);
36
+ }
37
+ return line;
38
+ }
39
+ function checkedOptions(options) {
40
+ const limit = options.limit ?? DEFAULT_PERCOLATION_LIMIT;
41
+ if (!Number.isSafeInteger(limit) || limit < 0 || limit > MAX_PERCOLATION_LIMIT) {
42
+ throw new RangeError(`Percolation limit must be a safe integer from 0 to ${MAX_PERCOLATION_LIMIT}.`);
43
+ }
44
+ const minSupport = options.minSupport ?? DEFAULT_PERCOLATION_MIN_SUPPORT;
45
+ if (!Number.isSafeInteger(minSupport) || minSupport < 1 || minSupport > MAX_PERCOLATION_EVIDENCE) {
46
+ throw new RangeError(`Percolation minimum support must be a safe integer from 1 to ${MAX_PERCOLATION_EVIDENCE}.`);
47
+ }
48
+ return { limit, minSupport };
49
+ }
50
+ function indexedContentNotes(notes, analysis) {
51
+ if (analysis.noteConnections.length > MAX_PERCOLATION_NOTES) {
52
+ throw new RangeError(`Percolation exceeds the ${MAX_PERCOLATION_NOTES} note limit.`);
53
+ }
54
+ const allById = new Map;
55
+ for (const note of notes) {
56
+ if (allById.has(note.id)) {
57
+ throw new Error(`Duplicate note identity in percolation: ${note.id}.`);
58
+ }
59
+ allById.set(note.id, note);
60
+ }
61
+ const byId = new Map;
62
+ const byPath = new Map;
63
+ for (const connection of analysis.noteConnections) {
64
+ if (byId.has(connection.id)) {
65
+ throw new Error(`Duplicate analyzed note identity: ${connection.id}.`);
66
+ }
67
+ const note = allById.get(connection.id);
68
+ if (note === undefined) {
69
+ throw new Error(`Analysis references missing note identity: ${connection.id}.`);
70
+ }
71
+ if (byPath.has(note.path)) {
72
+ throw new Error(`Duplicate note path in percolation: ${note.path}.`);
73
+ }
74
+ byId.set(note.id, note);
75
+ byPath.set(note.path, note);
76
+ }
77
+ return {
78
+ notes: [...byId.values()].toSorted((left, right) => compareText(left.id, right.id)),
79
+ byId,
80
+ byPath
81
+ };
82
+ }
83
+ function conceptKey(value) {
84
+ return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/^#+/u, "").replace(/[^\p{Letter}\p{Number}]+/gu, "-").replace(/^-+|-+$/gu, "");
85
+ }
86
+ function isConcept(note) {
87
+ return note.properties.type?.normalize("NFC").toLocaleLowerCase("en-US") === "concept";
88
+ }
89
+ function conceptLabels(note) {
90
+ return [
91
+ note.title,
92
+ ...note.aliases,
93
+ posix.basename(note.id)
94
+ ].map(conceptKey).filter((value) => value !== "");
95
+ }
96
+ function naturalConceptId(tag) {
97
+ const slug = conceptKey(tag);
98
+ const digest = createHash("sha256").update(tag.normalize("NFC")).digest("hex");
99
+ if (slug === "")
100
+ return `notes/concept-${digest.slice(0, 16)}`;
101
+ if (slug.length <= 160)
102
+ return `notes/${slug}`;
103
+ let prefix = "";
104
+ let count = 0;
105
+ for (const character of slug) {
106
+ if (count >= 144)
107
+ break;
108
+ prefix += character;
109
+ count += 1;
110
+ }
111
+ return `notes/${prefix.replace(/-+$/u, "")}-${digest.slice(0, 12)}`;
112
+ }
113
+ function suggestedConceptId(tag, occupiedIds) {
114
+ const natural = naturalConceptId(tag);
115
+ const foldedNatural = natural.toLocaleLowerCase("en-US");
116
+ const collidesWith = occupiedIds.get(foldedNatural) ?? null;
117
+ if (collidesWith === null)
118
+ return { id: natural, collidesWith: null };
119
+ const suffixed = `${natural}-concept`;
120
+ if (!occupiedIds.has(suffixed.toLocaleLowerCase("en-US"))) {
121
+ return { id: suffixed, collidesWith };
122
+ }
123
+ for (let suffix = 2;suffix <= MAX_PERCOLATION_NOTES + 2; suffix += 1) {
124
+ const candidate = `${suffixed}-${suffix}`;
125
+ if (!occupiedIds.has(candidate.toLocaleLowerCase("en-US"))) {
126
+ return { id: candidate, collidesWith };
127
+ }
128
+ }
129
+ throw new RangeError("Percolation could not choose an unoccupied concept ID.");
130
+ }
131
+ function resolvedNoteFilter(notes, query) {
132
+ if (query === undefined)
133
+ return null;
134
+ const result = lookupNote(notes, query);
135
+ if (result.kind === "found")
136
+ return result.note.id;
137
+ if (result.kind === "ambiguous") {
138
+ throw new Error(`Percolation note is ambiguous: ${result.candidates.map((note) => note.id).join(", ")}.`);
139
+ }
140
+ throw new Error(`Percolation note does not exist: ${query}.`);
141
+ }
142
+ function candidateInvolvesNote(candidate, note) {
143
+ if (note === null)
144
+ return true;
145
+ if (candidate.kind === "missing-concept") {
146
+ return candidate.evidence.some((evidence) => evidence.note === note);
147
+ }
148
+ return candidate.source === note || candidate.target === note;
149
+ }
150
+ function compareSharedEvidence(left, right) {
151
+ return compareText(left.kind, right.kind) || compareText(left.kind === "shared-tag" ? left.tag : left.concept, right.kind === "shared-tag" ? right.tag : right.concept) || compareText(left.note, right.note);
152
+ }
153
+ function candidateIdentity(candidate) {
154
+ switch (candidate.kind) {
155
+ case "missing-concept":
156
+ return candidate.tag;
157
+ case "missing-relation":
158
+ return `${candidate.source}\x00${candidate.target}`;
159
+ case "unlinked-mention":
160
+ return `${candidate.source}\x00${candidate.target}`;
161
+ case "relation-hygiene":
162
+ return [
163
+ candidate.problem,
164
+ candidate.source,
165
+ candidate.predicate ?? "",
166
+ candidate.target ?? ""
167
+ ].join("\x00");
168
+ }
169
+ }
170
+ var candidateKindRank = {
171
+ "relation-hygiene": 0,
172
+ "unlinked-mention": 1,
173
+ "missing-relation": 2,
174
+ "missing-concept": 3
175
+ };
176
+ function compareCandidates(left, right) {
177
+ return right.support - left.support || candidateKindRank[left.kind] - candidateKindRank[right.kind] || compareText(candidateIdentity(left), candidateIdentity(right));
178
+ }
179
+ function relationEvidence(relation) {
180
+ return {
181
+ kind: "relation",
182
+ source: relation.source,
183
+ target: relation.target,
184
+ predicate: relation.predicate,
185
+ line: checkedLine(relation.provenance.line, `Authored relation ${relation.source} -> ${relation.target}`),
186
+ authoredTarget: relation.provenance.authoredTarget
187
+ };
188
+ }
189
+ function issueEvidence(issue, source) {
190
+ const line = checkedLine(issue.line, `Relation issue in ${issue.source}`);
191
+ if (issue.kind === "malformed") {
192
+ return {
193
+ kind: "relation-issue",
194
+ issue: issue.kind,
195
+ source,
196
+ line,
197
+ predicate: issue.predicate ?? null,
198
+ target: issue.target ?? null,
199
+ candidates: [],
200
+ candidatesTruncated: false,
201
+ message: issue.message
202
+ };
203
+ }
204
+ if (issue.kind === "broken") {
205
+ return {
206
+ kind: "relation-issue",
207
+ issue: issue.kind,
208
+ source,
209
+ line,
210
+ predicate: issue.predicate,
211
+ target: issue.target,
212
+ candidates: [],
213
+ candidatesTruncated: false,
214
+ message: `Relationship target does not exist: ${issue.target}.`
215
+ };
216
+ }
217
+ return {
218
+ kind: "relation-issue",
219
+ issue: issue.kind,
220
+ source,
221
+ line,
222
+ predicate: issue.predicate,
223
+ target: issue.target,
224
+ candidates: [...issue.candidates].toSorted(compareText).slice(0, MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE),
225
+ candidatesTruncated: issue.candidates.length > MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE,
226
+ message: `Relationship target is ambiguous: ${issue.target}.`
227
+ };
228
+ }
229
+ function percolateVault(notes, analysis, options = {}) {
230
+ const { limit, minSupport } = checkedOptions(options);
231
+ const indexed = indexedContentNotes(notes, analysis);
232
+ const noteFilter = resolvedNoteFilter(indexed.notes, options.note);
233
+ const relations = analysis.authoredRelations ?? [];
234
+ const relationIssues = analysis.relationIssues ?? [];
235
+ let evidenceObservations = analysis.mentions.length + relations.length + relationIssues.length;
236
+ for (const issue of relationIssues) {
237
+ if (issue.kind !== "ambiguous")
238
+ continue;
239
+ evidenceObservations += issue.candidates.length;
240
+ if (evidenceObservations > MAX_PERCOLATION_EVIDENCE)
241
+ break;
242
+ }
243
+ if (evidenceObservations > MAX_PERCOLATION_EVIDENCE) {
244
+ throw new RangeError(`Percolation exceeds the ${MAX_PERCOLATION_EVIDENCE} evidence limit.`);
245
+ }
246
+ const conceptIds = new Set(indexed.notes.filter(isConcept).map((note) => note.id));
247
+ const occupiedIds = new Map(indexed.notes.map((note) => [
248
+ note.id.toLocaleLowerCase("en-US"),
249
+ note.id
250
+ ]));
251
+ const nonConceptNotes = indexed.notes.filter((note) => !conceptIds.has(note.id));
252
+ const conceptLabelKeys = new Set(indexed.notes.filter((note) => conceptIds.has(note.id)).flatMap(conceptLabels));
253
+ const candidates = [];
254
+ const notesByTag = new Map;
255
+ let tagEvidenceCount = 0;
256
+ for (const note of nonConceptNotes) {
257
+ for (const tag of new Set(note.tags)) {
258
+ tagEvidenceCount += 1;
259
+ if (tagEvidenceCount > MAX_PERCOLATION_EVIDENCE) {
260
+ throw new RangeError(`Percolation exceeds the ${MAX_PERCOLATION_EVIDENCE} tag evidence limit.`);
261
+ }
262
+ const matches = notesByTag.get(tag) ?? [];
263
+ matches.push(note);
264
+ notesByTag.set(tag, matches);
265
+ }
266
+ }
267
+ for (const [tag, matchingNotes] of [...notesByTag].toSorted(([left], [right]) => compareText(left, right))) {
268
+ const sortedMatches = matchingNotes.toSorted((left, right) => (left.id === noteFilter ? -1 : 0) - (right.id === noteFilter ? -1 : 0) || compareText(left.id, right.id));
269
+ const evidence = sortedMatches.slice(0, MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE).map((note) => ({
270
+ kind: "tag",
271
+ note: note.id,
272
+ path: note.path,
273
+ tag
274
+ }));
275
+ if (matchingNotes.length >= Math.max(2, minSupport) && !conceptLabelKeys.has(conceptKey(tag)) && (noteFilter === null || matchingNotes.some((note) => note.id === noteFilter))) {
276
+ const suggestion = suggestedConceptId(tag, occupiedIds);
277
+ candidates.push({
278
+ kind: "missing-concept",
279
+ tag,
280
+ suggestedId: suggestion.id,
281
+ collidesWith: suggestion.collidesWith,
282
+ support: matchingNotes.length,
283
+ evidenceTruncated: matchingNotes.length > MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE,
284
+ evidence
285
+ });
286
+ }
287
+ }
288
+ const explicitPairs = new Set;
289
+ const noteConcepts = new Map;
290
+ const addConceptConnection = (noteId, conceptId) => {
291
+ if (conceptIds.has(noteId) || !conceptIds.has(conceptId))
292
+ return;
293
+ const concepts = noteConcepts.get(noteId) ?? new Set;
294
+ concepts.add(conceptId);
295
+ noteConcepts.set(noteId, concepts);
296
+ };
297
+ for (const link of analysis.contextualLinks) {
298
+ const source = indexed.byPath.get(link.source);
299
+ const target = indexed.byPath.get(link.target);
300
+ if (source === undefined || target === undefined) {
301
+ throw new Error(`Contextual link references an unknown percolation note: ${link.source} -> ${link.target}.`);
302
+ }
303
+ explicitPairs.add(pairKey(source.id, target.id));
304
+ addConceptConnection(source.id, target.id);
305
+ addConceptConnection(target.id, source.id);
306
+ }
307
+ for (const relation of relations) {
308
+ if (!indexed.byId.has(relation.source) || !indexed.byId.has(relation.target)) {
309
+ throw new Error(`Authored relation references an unknown percolation note: ${relation.source} -> ${relation.target}.`);
310
+ }
311
+ explicitPairs.add(pairKey(relation.source, relation.target));
312
+ addConceptConnection(relation.source, relation.target);
313
+ addConceptConnection(relation.target, relation.source);
314
+ }
315
+ const pairEvidence = new Map;
316
+ let pairObservations = 0;
317
+ const addPairEvidence = (left, right, evidence) => {
318
+ pairObservations += 1;
319
+ if (pairObservations > MAX_PERCOLATION_PAIR_OBSERVATIONS) {
320
+ throw new RangeError(`Percolation exceeds the ${MAX_PERCOLATION_PAIR_OBSERVATIONS} pair observation limit.`);
321
+ }
322
+ const key = pairKey(left.id, right.id);
323
+ if (explicitPairs.has(key))
324
+ return;
325
+ const accumulated = pairEvidence.get(key) ?? {
326
+ support: 0,
327
+ evidenceCount: 0,
328
+ evidence: []
329
+ };
330
+ accumulated.support += 1;
331
+ accumulated.evidenceCount += evidence.length;
332
+ for (const item of evidence) {
333
+ if (accumulated.evidence.length < MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE) {
334
+ accumulated.evidence.push(item);
335
+ }
336
+ }
337
+ pairEvidence.set(key, accumulated);
338
+ };
339
+ const addPairsForGroup = (matchingNotes, evidenceFor) => {
340
+ const sortedNotes = matchingNotes.toSorted((left, right) => compareText(left.id, right.id));
341
+ if (noteFilter !== null) {
342
+ const scoped = sortedNotes.find((note) => note.id === noteFilter);
343
+ if (scoped === undefined)
344
+ return;
345
+ for (const other of sortedNotes) {
346
+ if (other.id === scoped.id)
347
+ continue;
348
+ const [left, right] = compareText(scoped.id, other.id) < 0 ? [scoped, other] : [other, scoped];
349
+ addPairEvidence(left, right, evidenceFor(left, right));
350
+ }
351
+ return;
352
+ }
353
+ for (let leftIndex = 0;leftIndex < sortedNotes.length; leftIndex += 1) {
354
+ for (let rightIndex = leftIndex + 1;rightIndex < sortedNotes.length; rightIndex += 1) {
355
+ const left = sortedNotes[leftIndex];
356
+ const right = sortedNotes[rightIndex];
357
+ if (left === undefined || right === undefined)
358
+ continue;
359
+ addPairEvidence(left, right, evidenceFor(left, right));
360
+ }
361
+ }
362
+ };
363
+ for (const [tag, matchingNotes] of [...notesByTag].toSorted(([left], [right]) => compareText(left, right))) {
364
+ addPairsForGroup(matchingNotes, (left, right) => [
365
+ { kind: "shared-tag", note: left.id, path: left.path, tag },
366
+ { kind: "shared-tag", note: right.id, path: right.path, tag }
367
+ ]);
368
+ }
369
+ const notesByConcept = new Map;
370
+ for (const [noteId, connectedConcepts] of noteConcepts) {
371
+ const note = indexed.byId.get(noteId);
372
+ if (note === undefined)
373
+ continue;
374
+ for (const concept of connectedConcepts) {
375
+ const matches = notesByConcept.get(concept) ?? [];
376
+ matches.push(note);
377
+ notesByConcept.set(concept, matches);
378
+ }
379
+ }
380
+ for (const [concept, matchingNotes] of [...notesByConcept].toSorted(([left], [right]) => compareText(left, right))) {
381
+ const conceptNote = indexed.byId.get(concept);
382
+ if (conceptNote === undefined)
383
+ continue;
384
+ addPairsForGroup(matchingNotes, (left, right) => [
385
+ {
386
+ kind: "shared-concept",
387
+ note: left.id,
388
+ path: left.path,
389
+ concept,
390
+ conceptPath: conceptNote.path
391
+ },
392
+ {
393
+ kind: "shared-concept",
394
+ note: right.id,
395
+ path: right.path,
396
+ concept,
397
+ conceptPath: conceptNote.path
398
+ }
399
+ ]);
400
+ }
401
+ for (const [key, accumulated] of pairEvidence) {
402
+ const separator = key.indexOf("\x00");
403
+ const source = key.slice(0, separator);
404
+ const target = key.slice(separator + 1);
405
+ const evidence = accumulated.evidence.toSorted(compareSharedEvidence);
406
+ if (accumulated.support < minSupport)
407
+ continue;
408
+ candidates.push({
409
+ kind: "missing-relation",
410
+ source,
411
+ target,
412
+ suggestedPredicate: "related-to",
413
+ support: accumulated.support,
414
+ evidenceTruncated: accumulated.evidenceCount > MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE,
415
+ evidence
416
+ });
417
+ }
418
+ const mentionEvidenceByPair = new Map;
419
+ for (const mention of analysis.mentions) {
420
+ const source = indexed.byPath.get(mention.source);
421
+ const target = indexed.byPath.get(mention.target);
422
+ if (source === undefined || target === undefined) {
423
+ throw new Error(`Mention references an unknown percolation note: ${mention.source} -> ${mention.target}.`);
424
+ }
425
+ if (noteFilter !== null && source.id !== noteFilter && target.id !== noteFilter)
426
+ continue;
427
+ if (explicitPairs.has(pairKey(source.id, target.id)))
428
+ continue;
429
+ const key = directedKey(source.id, target.id);
430
+ const accumulated = mentionEvidenceByPair.get(key) ?? {
431
+ support: 0,
432
+ evidence: []
433
+ };
434
+ accumulated.support += 1;
435
+ if (accumulated.evidence.length < MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE)
436
+ accumulated.evidence.push({
437
+ kind: "mention",
438
+ source: source.id,
439
+ target: target.id,
440
+ line: checkedLine(mention.line, `Mention ${mention.source} -> ${mention.target}`),
441
+ phrase: mention.phrase
442
+ });
443
+ mentionEvidenceByPair.set(key, accumulated);
444
+ }
445
+ for (const [key, accumulated] of mentionEvidenceByPair) {
446
+ const separator = key.indexOf("\x00");
447
+ const source = key.slice(0, separator);
448
+ const target = key.slice(separator + 1);
449
+ const sortedEvidence = accumulated.evidence.toSorted((left, right) => left.line - right.line || compareText(left.phrase, right.phrase));
450
+ candidates.push({
451
+ kind: "unlinked-mention",
452
+ source,
453
+ target,
454
+ support: accumulated.support,
455
+ evidenceTruncated: accumulated.support > MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE,
456
+ evidence: sortedEvidence
457
+ });
458
+ }
459
+ const relationsByKey = new Map(relations.map((relation) => [
460
+ relationKey(relation.source, relation.predicate, relation.target),
461
+ relation
462
+ ]));
463
+ for (const relation of relations) {
464
+ if (noteFilter !== null && relation.source !== noteFilter && relation.target !== noteFilter)
465
+ continue;
466
+ if (relation.source === relation.target) {
467
+ candidates.push({
468
+ kind: "relation-hygiene",
469
+ problem: "self-relation",
470
+ source: relation.source,
471
+ target: relation.target,
472
+ predicate: relation.predicate,
473
+ message: "Review an authored relationship whose source and target are the same note.",
474
+ support: 1,
475
+ evidenceTruncated: false,
476
+ evidence: [relationEvidence(relation)]
477
+ });
478
+ continue;
479
+ }
480
+ if (compareText(relation.source, relation.target) >= 0)
481
+ continue;
482
+ const reciprocal = relationsByKey.get(relationKey(relation.target, relation.predicate, relation.source));
483
+ if (reciprocal === undefined)
484
+ continue;
485
+ const evidence = [
486
+ relationEvidence(relation),
487
+ relationEvidence(reciprocal)
488
+ ].toSorted((left, right) => compareText(left.source, right.source) || compareText(left.target, right.target));
489
+ candidates.push({
490
+ kind: "relation-hygiene",
491
+ problem: "reciprocal-relation",
492
+ source: relation.source,
493
+ target: relation.target,
494
+ predicate: relation.predicate,
495
+ message: "Review reciprocal assertions of the same directional predicate.",
496
+ support: evidence.length,
497
+ evidenceTruncated: false,
498
+ evidence
499
+ });
500
+ }
501
+ for (const issue of relationIssues) {
502
+ const sourceNote = indexed.byPath.get(issue.source);
503
+ if (sourceNote === undefined)
504
+ continue;
505
+ if (noteFilter !== null && sourceNote.id !== noteFilter)
506
+ continue;
507
+ const evidence = issueEvidence(issue, sourceNote.id);
508
+ const problem = issue.kind === "malformed" ? "malformed-relation" : issue.kind === "broken" ? "broken-relation" : "ambiguous-relation";
509
+ candidates.push({
510
+ kind: "relation-hygiene",
511
+ problem,
512
+ source: sourceNote.id,
513
+ target: evidence.target,
514
+ predicate: evidence.predicate,
515
+ message: evidence.message,
516
+ support: 1,
517
+ evidenceTruncated: evidence.candidatesTruncated,
518
+ evidence: [evidence]
519
+ });
520
+ }
521
+ if (candidates.length > MAX_PERCOLATION_EVIDENCE) {
522
+ throw new RangeError(`Percolation exceeds the ${MAX_PERCOLATION_EVIDENCE} candidate limit.`);
523
+ }
524
+ const sorted = candidates.filter((candidate) => candidateInvolvesNote(candidate, noteFilter)).toSorted(compareCandidates);
525
+ return {
526
+ candidates: sorted.slice(0, limit),
527
+ truncated: sorted.length > limit
528
+ };
529
+ }
530
+
531
+ export { DEFAULT_PERCOLATION_LIMIT, MAX_PERCOLATION_LIMIT, DEFAULT_PERCOLATION_MIN_SUPPORT, MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE, MAX_PERCOLATION_NOTES, MAX_PERCOLATION_MENTION_PAIRS, MAX_PERCOLATION_MENTIONS, MAX_SCOPED_PERCOLATION_MENTION_PAIRS, percolateVault };