@hraness/kb 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -113
- package/dist/authoring.js +2 -2
- package/dist/benchmark.js +3 -3
- package/dist/cli.js +16 -12
- package/dist/evaluation-builder.js +5 -5
- package/dist/evaluation-kb.js +5 -5
- package/dist/graph.js +3 -1
- package/dist/{index-zxdy5pby.js → index-5m2ydj5q.js} +2 -2
- package/dist/{index-cxfrakt7.js → index-ekpwvbra.js} +5 -2
- package/dist/{index-jsmvyyvf.js → index-ey46z1zf.js} +4 -4
- package/dist/{index-cv6fh7z5.js → index-gm9t95d9.js} +1 -1
- package/dist/{index-01jj6rbv.js → index-gxr0fctd.js} +3 -3
- package/dist/index-nd6nynv2.js +1162 -0
- package/dist/{index-s2gw5aw9.js → index-qwgsmtsz.js} +1 -1
- package/dist/{index-zzhgcwyt.js → index-vxmf14m1.js} +3 -3
- package/dist/{index-n5dd7r0v.js → index-xw9ac71d.js} +2 -2
- package/dist/{index-1vrd1rmn.js → index-ykvvkd77.js} +1 -1
- package/dist/index.js +30 -8
- package/dist/percolate.js +22 -2
- package/dist/portfolio.js +5 -5
- package/dist/sdk.js +4 -4
- package/dist/search.js +2 -2
- package/dist/semantic.js +3 -3
- package/dist/workflows/decision-context.js +5 -5
- package/dist/workflows/index.js +5 -5
- package/package.json +1 -1
- package/skills/kb/AGENTS.md +3 -0
- package/skills/kb/SKILL.md +38 -29
- package/skills/kb/agents/openai.yaml +2 -2
- package/skills/kb/references/companion-skills.md +96 -0
- package/skills/kb/references/customize.md +123 -0
- package/skills/kb/references/percolate.md +39 -7
- package/skills/kb/references/query.md +21 -0
- package/skills/kb/templates/companion-skill.template.md +57 -0
- package/src/authoring.ts +5 -3
- package/src/cli.ts +12 -7
- package/src/graph.ts +8 -1
- package/src/percolate.ts +1088 -17
- package/dist/index-dyqwejk5.js +0 -531
|
@@ -0,0 +1,1162 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
MAX_ANALYZED_NOTES,
|
|
4
|
+
MAX_MENTIONS,
|
|
5
|
+
isCanonicalNoteId,
|
|
6
|
+
isCanonicalRelationPredicate,
|
|
7
|
+
lookupNote
|
|
8
|
+
} from "./index-ekpwvbra.js";
|
|
9
|
+
|
|
10
|
+
// src/percolate.ts
|
|
11
|
+
import { createHash } from "crypto";
|
|
12
|
+
import { posix } from "path";
|
|
13
|
+
var DEFAULT_PERCOLATION_LIMIT = 100;
|
|
14
|
+
var MAX_PERCOLATION_LIMIT = 1000;
|
|
15
|
+
var DEFAULT_PERCOLATION_MIN_SUPPORT = 2;
|
|
16
|
+
var MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE = 100;
|
|
17
|
+
var MAX_PERCOLATION_NOTES = MAX_ANALYZED_NOTES;
|
|
18
|
+
var MAX_PERCOLATION_MENTION_PAIRS = 250000;
|
|
19
|
+
var MAX_PERCOLATION_MENTIONS = MAX_MENTIONS;
|
|
20
|
+
var MAX_SCOPED_PERCOLATION_MENTION_PAIRS = MAX_PERCOLATION_NOTES * 2;
|
|
21
|
+
var PERCOLATION_RESULT_SCHEMA_VERSION = 2;
|
|
22
|
+
var MAX_PERCOLATION_RESULT_NODES = 250000;
|
|
23
|
+
var MAX_PERCOLATION_RESULT_UTF8_BYTES = 16 * 1024 * 1024;
|
|
24
|
+
var MAX_PERCOLATION_TEXT_UTF8_BYTES = 64 * 1024;
|
|
25
|
+
var MAX_PERCOLATION_EVIDENCE = 250000;
|
|
26
|
+
var MAX_PERCOLATION_PAIR_OBSERVATIONS = MAX_PERCOLATION_MENTION_PAIRS;
|
|
27
|
+
function compareText(left, right) {
|
|
28
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
29
|
+
}
|
|
30
|
+
function pairKey(left, right) {
|
|
31
|
+
return compareText(left, right) <= 0 ? `${left}\x00${right}` : `${right}\x00${left}`;
|
|
32
|
+
}
|
|
33
|
+
function directedKey(source, target) {
|
|
34
|
+
return `${source}\x00${target}`;
|
|
35
|
+
}
|
|
36
|
+
function relationKey(source, predicate, target) {
|
|
37
|
+
return `${source}\x00${predicate}\x00${target}`;
|
|
38
|
+
}
|
|
39
|
+
function checkedLine(line, context) {
|
|
40
|
+
if (!Number.isSafeInteger(line) || line < 1) {
|
|
41
|
+
throw new TypeError(`${context} has an invalid evidence line.`);
|
|
42
|
+
}
|
|
43
|
+
return line;
|
|
44
|
+
}
|
|
45
|
+
function checkedOptions(options) {
|
|
46
|
+
const limit = options.limit ?? DEFAULT_PERCOLATION_LIMIT;
|
|
47
|
+
if (!Number.isSafeInteger(limit) || limit < 0 || limit > MAX_PERCOLATION_LIMIT) {
|
|
48
|
+
throw new RangeError(`Percolation limit must be a safe integer from 0 to ${MAX_PERCOLATION_LIMIT}.`);
|
|
49
|
+
}
|
|
50
|
+
const minSupport = options.minSupport ?? DEFAULT_PERCOLATION_MIN_SUPPORT;
|
|
51
|
+
if (!Number.isSafeInteger(minSupport) || minSupport < 1 || minSupport > MAX_PERCOLATION_EVIDENCE) {
|
|
52
|
+
throw new RangeError(`Percolation minimum support must be a safe integer from 1 to ${MAX_PERCOLATION_EVIDENCE}.`);
|
|
53
|
+
}
|
|
54
|
+
return { limit, minSupport };
|
|
55
|
+
}
|
|
56
|
+
function indexedContentNotes(notes, analysis) {
|
|
57
|
+
if (analysis.noteConnections.length > MAX_PERCOLATION_NOTES) {
|
|
58
|
+
throw new RangeError(`Percolation exceeds the ${MAX_PERCOLATION_NOTES} note limit.`);
|
|
59
|
+
}
|
|
60
|
+
const allById = new Map;
|
|
61
|
+
for (const note of notes) {
|
|
62
|
+
if (allById.has(note.id)) {
|
|
63
|
+
throw new Error(`Duplicate note identity in percolation: ${note.id}.`);
|
|
64
|
+
}
|
|
65
|
+
allById.set(note.id, note);
|
|
66
|
+
}
|
|
67
|
+
const byId = new Map;
|
|
68
|
+
const byPath = new Map;
|
|
69
|
+
for (const connection of analysis.noteConnections) {
|
|
70
|
+
if (byId.has(connection.id)) {
|
|
71
|
+
throw new Error(`Duplicate analyzed note identity: ${connection.id}.`);
|
|
72
|
+
}
|
|
73
|
+
const note = allById.get(connection.id);
|
|
74
|
+
if (note === undefined) {
|
|
75
|
+
throw new Error(`Analysis references missing note identity: ${connection.id}.`);
|
|
76
|
+
}
|
|
77
|
+
if (byPath.has(note.path)) {
|
|
78
|
+
throw new Error(`Duplicate note path in percolation: ${note.path}.`);
|
|
79
|
+
}
|
|
80
|
+
byId.set(note.id, note);
|
|
81
|
+
byPath.set(note.path, note);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
notes: [...byId.values()].toSorted((left, right) => compareText(left.id, right.id)),
|
|
85
|
+
byId,
|
|
86
|
+
byPath
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function conceptKey(value) {
|
|
90
|
+
return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/^#+/u, "").replace(/[^\p{Letter}\p{Number}]+/gu, "-").replace(/^-+|-+$/gu, "");
|
|
91
|
+
}
|
|
92
|
+
function isConcept(note) {
|
|
93
|
+
return note.properties.type?.normalize("NFC").toLocaleLowerCase("en-US") === "concept";
|
|
94
|
+
}
|
|
95
|
+
function conceptLabels(note) {
|
|
96
|
+
return [
|
|
97
|
+
note.title,
|
|
98
|
+
...note.aliases,
|
|
99
|
+
posix.basename(note.id)
|
|
100
|
+
].map(conceptKey).filter((value) => value !== "");
|
|
101
|
+
}
|
|
102
|
+
function naturalConceptId(tag) {
|
|
103
|
+
const slug = conceptKey(tag);
|
|
104
|
+
const digest = createHash("sha256").update(tag.normalize("NFC")).digest("hex");
|
|
105
|
+
if (slug === "")
|
|
106
|
+
return `notes/concept-${digest.slice(0, 16)}`;
|
|
107
|
+
if (slug.length <= 160)
|
|
108
|
+
return `notes/${slug}`;
|
|
109
|
+
let prefix = "";
|
|
110
|
+
let count = 0;
|
|
111
|
+
for (const character of slug) {
|
|
112
|
+
if (count >= 144)
|
|
113
|
+
break;
|
|
114
|
+
prefix += character;
|
|
115
|
+
count += 1;
|
|
116
|
+
}
|
|
117
|
+
return `notes/${prefix.replace(/-+$/u, "")}-${digest.slice(0, 12)}`;
|
|
118
|
+
}
|
|
119
|
+
function suggestedConceptId(tag, occupiedIds, reservedIds, nextSuffixByNaturalId) {
|
|
120
|
+
const natural = naturalConceptId(tag);
|
|
121
|
+
const foldedNatural = natural.toLocaleLowerCase("en-US");
|
|
122
|
+
const collidesWith = occupiedIds.get(foldedNatural) ?? null;
|
|
123
|
+
if (collidesWith === null && !reservedIds.has(foldedNatural)) {
|
|
124
|
+
reservedIds.add(foldedNatural);
|
|
125
|
+
return { id: natural, collidesWith: null };
|
|
126
|
+
}
|
|
127
|
+
const suffixed = `${natural}-concept`;
|
|
128
|
+
const foldedSuffixed = suffixed.toLocaleLowerCase("en-US");
|
|
129
|
+
if (!occupiedIds.has(foldedSuffixed) && !reservedIds.has(foldedSuffixed)) {
|
|
130
|
+
reservedIds.add(foldedSuffixed);
|
|
131
|
+
nextSuffixByNaturalId.set(foldedNatural, 2);
|
|
132
|
+
return { id: suffixed, collidesWith };
|
|
133
|
+
}
|
|
134
|
+
const nextSuffix = nextSuffixByNaturalId.get(foldedNatural) ?? 2;
|
|
135
|
+
for (let suffix = nextSuffix;suffix <= MAX_PERCOLATION_EVIDENCE + 2; suffix += 1) {
|
|
136
|
+
const candidate = `${suffixed}-${suffix}`;
|
|
137
|
+
const foldedCandidate = candidate.toLocaleLowerCase("en-US");
|
|
138
|
+
if (!occupiedIds.has(foldedCandidate) && !reservedIds.has(foldedCandidate)) {
|
|
139
|
+
reservedIds.add(foldedCandidate);
|
|
140
|
+
nextSuffixByNaturalId.set(foldedNatural, suffix + 1);
|
|
141
|
+
return { id: candidate, collidesWith };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw new RangeError("Percolation could not choose an unoccupied concept ID.");
|
|
145
|
+
}
|
|
146
|
+
function resolvedNoteFilter(notes, query) {
|
|
147
|
+
if (query === undefined)
|
|
148
|
+
return null;
|
|
149
|
+
const result = lookupNote(notes, query);
|
|
150
|
+
if (result.kind === "found")
|
|
151
|
+
return result.note.id;
|
|
152
|
+
if (result.kind === "ambiguous") {
|
|
153
|
+
throw new Error(`Percolation note is ambiguous: ${result.candidates.map((note) => note.id).join(", ")}.`);
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`Percolation note does not exist: ${query}.`);
|
|
156
|
+
}
|
|
157
|
+
function candidateInvolvesNote(candidate, note) {
|
|
158
|
+
if (note === null)
|
|
159
|
+
return true;
|
|
160
|
+
if (candidate.kind === "missing-concept") {
|
|
161
|
+
return candidate.evidence.some((evidence) => evidence.note === note);
|
|
162
|
+
}
|
|
163
|
+
return candidate.source === note || candidate.target === note;
|
|
164
|
+
}
|
|
165
|
+
function compareSharedEvidence(left, right) {
|
|
166
|
+
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);
|
|
167
|
+
}
|
|
168
|
+
function candidateIdentity(candidate) {
|
|
169
|
+
switch (candidate.kind) {
|
|
170
|
+
case "missing-concept":
|
|
171
|
+
return candidate.tag;
|
|
172
|
+
case "missing-relation":
|
|
173
|
+
return `${candidate.source}\x00${candidate.target}`;
|
|
174
|
+
case "unlinked-mention":
|
|
175
|
+
return `${candidate.source}\x00${candidate.target}`;
|
|
176
|
+
case "relation-hygiene":
|
|
177
|
+
return [
|
|
178
|
+
candidate.problem,
|
|
179
|
+
candidate.source,
|
|
180
|
+
candidate.predicate ?? "",
|
|
181
|
+
candidate.target ?? "",
|
|
182
|
+
candidate.message,
|
|
183
|
+
String(candidate.evidence[0]?.line ?? 0)
|
|
184
|
+
].join("\x00");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function historicalCandidateIdentity(candidate) {
|
|
188
|
+
switch (candidate.kind) {
|
|
189
|
+
case "missing-concept":
|
|
190
|
+
return candidate.tag;
|
|
191
|
+
case "missing-relation":
|
|
192
|
+
case "unlinked-mention":
|
|
193
|
+
return `${candidate.source}\x00${candidate.target}`;
|
|
194
|
+
case "relation-hygiene":
|
|
195
|
+
return [
|
|
196
|
+
candidate.problem,
|
|
197
|
+
candidate.source,
|
|
198
|
+
candidate.predicate ?? "",
|
|
199
|
+
candidate.target ?? ""
|
|
200
|
+
].join("\x00");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
var candidateKindRank = {
|
|
204
|
+
"relation-hygiene": 0,
|
|
205
|
+
"unlinked-mention": 1,
|
|
206
|
+
"missing-relation": 2,
|
|
207
|
+
"missing-concept": 3
|
|
208
|
+
};
|
|
209
|
+
function compareCandidates(left, right) {
|
|
210
|
+
return right.support - left.support || candidateKindRank[left.kind] - candidateKindRank[right.kind] || compareText(candidateIdentity(left), candidateIdentity(right));
|
|
211
|
+
}
|
|
212
|
+
function compareHistoricalCandidates(left, right) {
|
|
213
|
+
return right.support - left.support || candidateKindRank[left.kind] - candidateKindRank[right.kind] || compareText(historicalCandidateIdentity(left), historicalCandidateIdentity(right));
|
|
214
|
+
}
|
|
215
|
+
function relationEvidence(relation) {
|
|
216
|
+
return {
|
|
217
|
+
kind: "relation",
|
|
218
|
+
source: relation.source,
|
|
219
|
+
target: relation.target,
|
|
220
|
+
predicate: relation.predicate,
|
|
221
|
+
line: checkedLine(relation.provenance.line, `Authored relation ${relation.source} -> ${relation.target}`),
|
|
222
|
+
authoredTarget: relation.provenance.authoredTarget
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function issueEvidence(issue, source) {
|
|
226
|
+
const line = checkedLine(issue.line, `Relation issue in ${issue.source}`);
|
|
227
|
+
if (issue.kind === "malformed") {
|
|
228
|
+
return {
|
|
229
|
+
kind: "relation-issue",
|
|
230
|
+
issue: issue.kind,
|
|
231
|
+
source,
|
|
232
|
+
line,
|
|
233
|
+
predicate: issue.predicate ?? null,
|
|
234
|
+
target: issue.target ?? null,
|
|
235
|
+
candidates: [],
|
|
236
|
+
candidatesTruncated: false,
|
|
237
|
+
message: issue.message
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (issue.kind === "broken") {
|
|
241
|
+
return {
|
|
242
|
+
kind: "relation-issue",
|
|
243
|
+
issue: issue.kind,
|
|
244
|
+
source,
|
|
245
|
+
line,
|
|
246
|
+
predicate: issue.predicate,
|
|
247
|
+
target: issue.target,
|
|
248
|
+
candidates: [],
|
|
249
|
+
candidatesTruncated: false,
|
|
250
|
+
message: `Relationship target does not exist: ${issue.target}.`
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
kind: "relation-issue",
|
|
255
|
+
issue: issue.kind,
|
|
256
|
+
source,
|
|
257
|
+
line,
|
|
258
|
+
predicate: issue.predicate,
|
|
259
|
+
target: issue.target,
|
|
260
|
+
candidates: [...issue.candidates].toSorted(compareText).slice(0, MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE),
|
|
261
|
+
candidatesTruncated: issue.candidates.length > MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE,
|
|
262
|
+
message: `Relationship target is ambiguous: ${issue.target}.`
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function percolateVault(notes, analysis, options = {}) {
|
|
266
|
+
const { limit, minSupport } = checkedOptions(options);
|
|
267
|
+
const indexed = indexedContentNotes(notes, analysis);
|
|
268
|
+
const noteFilter = resolvedNoteFilter(indexed.notes, options.note);
|
|
269
|
+
const relations = analysis.authoredRelations ?? [];
|
|
270
|
+
const relationIssues = analysis.relationIssues ?? [];
|
|
271
|
+
let evidenceObservations = analysis.mentions.length + relations.length + relationIssues.length;
|
|
272
|
+
for (const issue of relationIssues) {
|
|
273
|
+
if (issue.kind !== "ambiguous")
|
|
274
|
+
continue;
|
|
275
|
+
evidenceObservations += issue.candidates.length;
|
|
276
|
+
if (evidenceObservations > MAX_PERCOLATION_EVIDENCE)
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
if (evidenceObservations > MAX_PERCOLATION_EVIDENCE) {
|
|
280
|
+
throw new RangeError(`Percolation exceeds the ${MAX_PERCOLATION_EVIDENCE} evidence limit.`);
|
|
281
|
+
}
|
|
282
|
+
const conceptIds = new Set(indexed.notes.filter(isConcept).map((note) => note.id));
|
|
283
|
+
const occupiedIds = new Map(indexed.notes.map((note) => [
|
|
284
|
+
note.id.toLocaleLowerCase("en-US"),
|
|
285
|
+
note.id
|
|
286
|
+
]));
|
|
287
|
+
const reservedConceptIds = new Set;
|
|
288
|
+
const nextConceptSuffixByNaturalId = new Map;
|
|
289
|
+
const nonConceptNotes = indexed.notes.filter((note) => !conceptIds.has(note.id));
|
|
290
|
+
const conceptLabelKeys = new Set(indexed.notes.filter((note) => conceptIds.has(note.id)).flatMap(conceptLabels));
|
|
291
|
+
const candidates = [];
|
|
292
|
+
const notesByTag = new Map;
|
|
293
|
+
let tagEvidenceCount = 0;
|
|
294
|
+
for (const note of nonConceptNotes) {
|
|
295
|
+
for (const tag of new Set(note.tags)) {
|
|
296
|
+
tagEvidenceCount += 1;
|
|
297
|
+
if (tagEvidenceCount > MAX_PERCOLATION_EVIDENCE) {
|
|
298
|
+
throw new RangeError(`Percolation exceeds the ${MAX_PERCOLATION_EVIDENCE} tag evidence limit.`);
|
|
299
|
+
}
|
|
300
|
+
const matches = notesByTag.get(tag) ?? [];
|
|
301
|
+
matches.push(note);
|
|
302
|
+
notesByTag.set(tag, matches);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const [tag, matchingNotes] of [...notesByTag].toSorted(([left], [right]) => compareText(left, right))) {
|
|
306
|
+
const sortedMatches = matchingNotes.toSorted((left, right) => (left.id === noteFilter ? -1 : 0) - (right.id === noteFilter ? -1 : 0) || compareText(left.id, right.id));
|
|
307
|
+
const evidence = sortedMatches.slice(0, MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE).map((note) => ({
|
|
308
|
+
kind: "tag",
|
|
309
|
+
note: note.id,
|
|
310
|
+
path: note.path,
|
|
311
|
+
tag
|
|
312
|
+
}));
|
|
313
|
+
if (matchingNotes.length >= Math.max(2, minSupport) && !conceptLabelKeys.has(conceptKey(tag)) && (noteFilter === null || matchingNotes.some((note) => note.id === noteFilter))) {
|
|
314
|
+
const suggestion = suggestedConceptId(tag, occupiedIds, reservedConceptIds, nextConceptSuffixByNaturalId);
|
|
315
|
+
candidates.push({
|
|
316
|
+
kind: "missing-concept",
|
|
317
|
+
tag,
|
|
318
|
+
suggestedId: suggestion.id,
|
|
319
|
+
collidesWith: suggestion.collidesWith,
|
|
320
|
+
support: matchingNotes.length,
|
|
321
|
+
evidenceTruncated: matchingNotes.length > MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE,
|
|
322
|
+
evidence
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const explicitPairs = new Set;
|
|
327
|
+
const noteConcepts = new Map;
|
|
328
|
+
const addConceptConnection = (noteId, conceptId) => {
|
|
329
|
+
if (conceptIds.has(noteId) || !conceptIds.has(conceptId))
|
|
330
|
+
return;
|
|
331
|
+
const concepts = noteConcepts.get(noteId) ?? new Set;
|
|
332
|
+
concepts.add(conceptId);
|
|
333
|
+
noteConcepts.set(noteId, concepts);
|
|
334
|
+
};
|
|
335
|
+
for (const link of analysis.contextualLinks) {
|
|
336
|
+
const source = indexed.byPath.get(link.source);
|
|
337
|
+
const target = indexed.byPath.get(link.target);
|
|
338
|
+
if (source === undefined || target === undefined) {
|
|
339
|
+
throw new Error(`Contextual link references an unknown percolation note: ${link.source} -> ${link.target}.`);
|
|
340
|
+
}
|
|
341
|
+
explicitPairs.add(pairKey(source.id, target.id));
|
|
342
|
+
addConceptConnection(source.id, target.id);
|
|
343
|
+
addConceptConnection(target.id, source.id);
|
|
344
|
+
}
|
|
345
|
+
for (const relation of relations) {
|
|
346
|
+
if (!indexed.byId.has(relation.source) || !indexed.byId.has(relation.target)) {
|
|
347
|
+
throw new Error(`Authored relation references an unknown percolation note: ${relation.source} -> ${relation.target}.`);
|
|
348
|
+
}
|
|
349
|
+
explicitPairs.add(pairKey(relation.source, relation.target));
|
|
350
|
+
addConceptConnection(relation.source, relation.target);
|
|
351
|
+
addConceptConnection(relation.target, relation.source);
|
|
352
|
+
}
|
|
353
|
+
const pairEvidence = new Map;
|
|
354
|
+
let pairObservations = 0;
|
|
355
|
+
const addPairEvidence = (left, right, evidence) => {
|
|
356
|
+
pairObservations += 1;
|
|
357
|
+
if (pairObservations > MAX_PERCOLATION_PAIR_OBSERVATIONS) {
|
|
358
|
+
throw new RangeError(`Percolation exceeds the ${MAX_PERCOLATION_PAIR_OBSERVATIONS} pair observation limit.`);
|
|
359
|
+
}
|
|
360
|
+
const key = pairKey(left.id, right.id);
|
|
361
|
+
if (explicitPairs.has(key))
|
|
362
|
+
return;
|
|
363
|
+
const accumulated = pairEvidence.get(key) ?? {
|
|
364
|
+
support: 0,
|
|
365
|
+
evidenceCount: 0,
|
|
366
|
+
evidence: []
|
|
367
|
+
};
|
|
368
|
+
accumulated.support += 1;
|
|
369
|
+
accumulated.evidenceCount += evidence.length;
|
|
370
|
+
for (const item of evidence) {
|
|
371
|
+
if (accumulated.evidence.length < MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE) {
|
|
372
|
+
accumulated.evidence.push(item);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
pairEvidence.set(key, accumulated);
|
|
376
|
+
};
|
|
377
|
+
const addPairsForGroup = (matchingNotes, evidenceFor) => {
|
|
378
|
+
const sortedNotes = matchingNotes.toSorted((left, right) => compareText(left.id, right.id));
|
|
379
|
+
if (noteFilter !== null) {
|
|
380
|
+
const scoped = sortedNotes.find((note) => note.id === noteFilter);
|
|
381
|
+
if (scoped === undefined)
|
|
382
|
+
return;
|
|
383
|
+
for (const other of sortedNotes) {
|
|
384
|
+
if (other.id === scoped.id)
|
|
385
|
+
continue;
|
|
386
|
+
const [left, right] = compareText(scoped.id, other.id) < 0 ? [scoped, other] : [other, scoped];
|
|
387
|
+
addPairEvidence(left, right, evidenceFor(left, right));
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
for (let leftIndex = 0;leftIndex < sortedNotes.length; leftIndex += 1) {
|
|
392
|
+
for (let rightIndex = leftIndex + 1;rightIndex < sortedNotes.length; rightIndex += 1) {
|
|
393
|
+
const left = sortedNotes[leftIndex];
|
|
394
|
+
const right = sortedNotes[rightIndex];
|
|
395
|
+
if (left === undefined || right === undefined)
|
|
396
|
+
continue;
|
|
397
|
+
addPairEvidence(left, right, evidenceFor(left, right));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
for (const [tag, matchingNotes] of [...notesByTag].toSorted(([left], [right]) => compareText(left, right))) {
|
|
402
|
+
addPairsForGroup(matchingNotes, (left, right) => [
|
|
403
|
+
{ kind: "shared-tag", note: left.id, path: left.path, tag },
|
|
404
|
+
{ kind: "shared-tag", note: right.id, path: right.path, tag }
|
|
405
|
+
]);
|
|
406
|
+
}
|
|
407
|
+
const notesByConcept = new Map;
|
|
408
|
+
for (const [noteId, connectedConcepts] of noteConcepts) {
|
|
409
|
+
const note = indexed.byId.get(noteId);
|
|
410
|
+
if (note === undefined)
|
|
411
|
+
continue;
|
|
412
|
+
for (const concept of connectedConcepts) {
|
|
413
|
+
const matches = notesByConcept.get(concept) ?? [];
|
|
414
|
+
matches.push(note);
|
|
415
|
+
notesByConcept.set(concept, matches);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
for (const [concept, matchingNotes] of [...notesByConcept].toSorted(([left], [right]) => compareText(left, right))) {
|
|
419
|
+
const conceptNote = indexed.byId.get(concept);
|
|
420
|
+
if (conceptNote === undefined)
|
|
421
|
+
continue;
|
|
422
|
+
addPairsForGroup(matchingNotes, (left, right) => [
|
|
423
|
+
{
|
|
424
|
+
kind: "shared-concept",
|
|
425
|
+
note: left.id,
|
|
426
|
+
path: left.path,
|
|
427
|
+
concept,
|
|
428
|
+
conceptPath: conceptNote.path
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
kind: "shared-concept",
|
|
432
|
+
note: right.id,
|
|
433
|
+
path: right.path,
|
|
434
|
+
concept,
|
|
435
|
+
conceptPath: conceptNote.path
|
|
436
|
+
}
|
|
437
|
+
]);
|
|
438
|
+
}
|
|
439
|
+
for (const [key, accumulated] of pairEvidence) {
|
|
440
|
+
const separator = key.indexOf("\x00");
|
|
441
|
+
const source = key.slice(0, separator);
|
|
442
|
+
const target = key.slice(separator + 1);
|
|
443
|
+
const evidence = accumulated.evidence.toSorted(compareSharedEvidence);
|
|
444
|
+
if (accumulated.support < minSupport)
|
|
445
|
+
continue;
|
|
446
|
+
candidates.push({
|
|
447
|
+
kind: "missing-relation",
|
|
448
|
+
source,
|
|
449
|
+
target,
|
|
450
|
+
predicate: { kind: "required" },
|
|
451
|
+
support: accumulated.support,
|
|
452
|
+
evidenceTruncated: accumulated.evidenceCount > MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE,
|
|
453
|
+
evidence
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
const mentionEvidenceByPair = new Map;
|
|
457
|
+
for (const mention of analysis.mentions) {
|
|
458
|
+
const source = indexed.byPath.get(mention.source);
|
|
459
|
+
const target = indexed.byPath.get(mention.target);
|
|
460
|
+
if (source === undefined || target === undefined) {
|
|
461
|
+
throw new Error(`Mention references an unknown percolation note: ${mention.source} -> ${mention.target}.`);
|
|
462
|
+
}
|
|
463
|
+
if (noteFilter !== null && source.id !== noteFilter && target.id !== noteFilter)
|
|
464
|
+
continue;
|
|
465
|
+
if (explicitPairs.has(pairKey(source.id, target.id)))
|
|
466
|
+
continue;
|
|
467
|
+
const key = directedKey(source.id, target.id);
|
|
468
|
+
const accumulated = mentionEvidenceByPair.get(key) ?? {
|
|
469
|
+
support: 0,
|
|
470
|
+
evidence: []
|
|
471
|
+
};
|
|
472
|
+
accumulated.support += 1;
|
|
473
|
+
if (accumulated.evidence.length < MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE)
|
|
474
|
+
accumulated.evidence.push({
|
|
475
|
+
kind: "mention",
|
|
476
|
+
source: source.id,
|
|
477
|
+
target: target.id,
|
|
478
|
+
line: checkedLine(mention.line, `Mention ${mention.source} -> ${mention.target}`),
|
|
479
|
+
phrase: mention.phrase
|
|
480
|
+
});
|
|
481
|
+
mentionEvidenceByPair.set(key, accumulated);
|
|
482
|
+
}
|
|
483
|
+
for (const [key, accumulated] of mentionEvidenceByPair) {
|
|
484
|
+
const separator = key.indexOf("\x00");
|
|
485
|
+
const source = key.slice(0, separator);
|
|
486
|
+
const target = key.slice(separator + 1);
|
|
487
|
+
const sortedEvidence = accumulated.evidence.toSorted((left, right) => left.line - right.line || compareText(left.phrase, right.phrase));
|
|
488
|
+
candidates.push({
|
|
489
|
+
kind: "unlinked-mention",
|
|
490
|
+
source,
|
|
491
|
+
target,
|
|
492
|
+
support: accumulated.support,
|
|
493
|
+
evidenceTruncated: accumulated.support > MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE,
|
|
494
|
+
evidence: sortedEvidence
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
const relationsByKey = new Map(relations.map((relation) => [
|
|
498
|
+
relationKey(relation.source, relation.predicate, relation.target),
|
|
499
|
+
relation
|
|
500
|
+
]));
|
|
501
|
+
for (const relation of relations) {
|
|
502
|
+
if (noteFilter !== null && relation.source !== noteFilter && relation.target !== noteFilter)
|
|
503
|
+
continue;
|
|
504
|
+
if (relation.source === relation.target) {
|
|
505
|
+
candidates.push({
|
|
506
|
+
kind: "relation-hygiene",
|
|
507
|
+
problem: "self-relation",
|
|
508
|
+
source: relation.source,
|
|
509
|
+
target: relation.target,
|
|
510
|
+
predicate: relation.predicate,
|
|
511
|
+
message: "Review an authored relationship whose source and target are the same note.",
|
|
512
|
+
support: 1,
|
|
513
|
+
evidenceTruncated: false,
|
|
514
|
+
evidence: [relationEvidence(relation)]
|
|
515
|
+
});
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (compareText(relation.source, relation.target) >= 0)
|
|
519
|
+
continue;
|
|
520
|
+
const reciprocal = relationsByKey.get(relationKey(relation.target, relation.predicate, relation.source));
|
|
521
|
+
if (reciprocal === undefined)
|
|
522
|
+
continue;
|
|
523
|
+
const evidence = [
|
|
524
|
+
relationEvidence(relation),
|
|
525
|
+
relationEvidence(reciprocal)
|
|
526
|
+
].toSorted((left, right) => compareText(left.source, right.source) || compareText(left.target, right.target));
|
|
527
|
+
candidates.push({
|
|
528
|
+
kind: "relation-hygiene",
|
|
529
|
+
problem: "reciprocal-relation",
|
|
530
|
+
source: relation.source,
|
|
531
|
+
target: relation.target,
|
|
532
|
+
predicate: relation.predicate,
|
|
533
|
+
message: "Review reciprocal assertions of the same directional predicate.",
|
|
534
|
+
support: evidence.length,
|
|
535
|
+
evidenceTruncated: false,
|
|
536
|
+
evidence
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
for (const issue of relationIssues) {
|
|
540
|
+
const sourceNote = indexed.byPath.get(issue.source);
|
|
541
|
+
if (sourceNote === undefined)
|
|
542
|
+
continue;
|
|
543
|
+
if (noteFilter !== null && sourceNote.id !== noteFilter)
|
|
544
|
+
continue;
|
|
545
|
+
const evidence = issueEvidence(issue, sourceNote.id);
|
|
546
|
+
const problem = issue.kind === "malformed" ? "malformed-relation" : issue.kind === "broken" ? "broken-relation" : "ambiguous-relation";
|
|
547
|
+
candidates.push({
|
|
548
|
+
kind: "relation-hygiene",
|
|
549
|
+
problem,
|
|
550
|
+
source: sourceNote.id,
|
|
551
|
+
target: evidence.target,
|
|
552
|
+
predicate: evidence.predicate,
|
|
553
|
+
message: evidence.message,
|
|
554
|
+
support: 1,
|
|
555
|
+
evidenceTruncated: evidence.candidatesTruncated,
|
|
556
|
+
evidence: [evidence]
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
if (candidates.length > MAX_PERCOLATION_EVIDENCE) {
|
|
560
|
+
throw new RangeError(`Percolation exceeds the ${MAX_PERCOLATION_EVIDENCE} candidate limit.`);
|
|
561
|
+
}
|
|
562
|
+
const uniqueCandidates = new Map;
|
|
563
|
+
for (const candidate of candidates) {
|
|
564
|
+
const identity = `${candidate.kind}\x00${candidateIdentity(candidate)}`;
|
|
565
|
+
if (!uniqueCandidates.has(identity))
|
|
566
|
+
uniqueCandidates.set(identity, candidate);
|
|
567
|
+
}
|
|
568
|
+
const sorted = [...uniqueCandidates.values()].filter((candidate) => candidateInvolvesNote(candidate, noteFilter)).toSorted(compareCandidates);
|
|
569
|
+
return parsePercolationResultV2({
|
|
570
|
+
schemaVersion: PERCOLATION_RESULT_SCHEMA_VERSION,
|
|
571
|
+
candidates: sorted.slice(0, limit),
|
|
572
|
+
truncated: sorted.length > limit
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
function countParseNode(budget, label) {
|
|
576
|
+
budget.nodes += 1;
|
|
577
|
+
if (budget.nodes > MAX_PERCOLATION_RESULT_NODES) {
|
|
578
|
+
throw new RangeError(`${label} exceeds the ${MAX_PERCOLATION_RESULT_NODES.toLocaleString("en-US")}-node percolation result limit.`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function dataRecord(value, label, budget) {
|
|
582
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
583
|
+
throw new TypeError(`${label} must be a plain data object.`);
|
|
584
|
+
}
|
|
585
|
+
const prototype = Object.getPrototypeOf(value);
|
|
586
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
587
|
+
throw new TypeError(`${label} must be a plain data object.`);
|
|
588
|
+
}
|
|
589
|
+
countParseNode(budget, label);
|
|
590
|
+
const output = Object.create(null);
|
|
591
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
592
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
593
|
+
if (typeof key !== "string") {
|
|
594
|
+
throw new TypeError(`${label} must not contain symbol fields.`);
|
|
595
|
+
}
|
|
596
|
+
const descriptor = descriptors[key];
|
|
597
|
+
if (descriptor === undefined || !("value" in descriptor) || !descriptor.enumerable) {
|
|
598
|
+
throw new TypeError(`${label}.${key} must be an enumerable data property.`);
|
|
599
|
+
}
|
|
600
|
+
Object.defineProperty(output, key, {
|
|
601
|
+
configurable: false,
|
|
602
|
+
enumerable: true,
|
|
603
|
+
value: descriptor.value,
|
|
604
|
+
writable: false
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return Object.freeze(output);
|
|
608
|
+
}
|
|
609
|
+
function exactKeys(record, keys, label) {
|
|
610
|
+
const actual = Reflect.ownKeys(record);
|
|
611
|
+
const expected = new Set(keys);
|
|
612
|
+
if (actual.length !== keys.length || actual.some((key) => typeof key !== "string" || !expected.has(key))) {
|
|
613
|
+
throw new TypeError(`${label} must contain exactly: ${keys.join(", ")}.`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function dataArray(value, label, maximum, budget) {
|
|
617
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
618
|
+
throw new TypeError(`${label} must be an ordinary array.`);
|
|
619
|
+
}
|
|
620
|
+
if (value.length > maximum) {
|
|
621
|
+
throw new RangeError(`${label} exceeds its ${maximum.toLocaleString("en-US")}-entry limit.`);
|
|
622
|
+
}
|
|
623
|
+
countParseNode(budget, label);
|
|
624
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
625
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
626
|
+
if (typeof key !== "string") {
|
|
627
|
+
throw new TypeError(`${label} must not contain symbol fields.`);
|
|
628
|
+
}
|
|
629
|
+
if (key === "length")
|
|
630
|
+
continue;
|
|
631
|
+
const index = Number(key);
|
|
632
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= value.length || String(index) !== key) {
|
|
633
|
+
throw new TypeError(`${label} contains a non-index property.`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const output = [];
|
|
637
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
638
|
+
const descriptor = descriptors[String(index)];
|
|
639
|
+
if (descriptor === undefined || !("value" in descriptor) || !descriptor.enumerable) {
|
|
640
|
+
throw new TypeError(`${label} must be a dense array of data properties.`);
|
|
641
|
+
}
|
|
642
|
+
output.push(descriptor.value);
|
|
643
|
+
}
|
|
644
|
+
return Object.freeze(output);
|
|
645
|
+
}
|
|
646
|
+
function hasUnpairedSurrogate(value) {
|
|
647
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
648
|
+
const code = value.charCodeAt(index);
|
|
649
|
+
if (code >= 55296 && code <= 56319) {
|
|
650
|
+
const next = value.charCodeAt(index + 1);
|
|
651
|
+
if (next < 56320 || next > 57343)
|
|
652
|
+
return true;
|
|
653
|
+
index += 1;
|
|
654
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
function parsedText(value, label, budget, options = {}) {
|
|
661
|
+
if (typeof value !== "string" || options.empty !== true && value === "" || hasUnpairedSurrogate(value)) {
|
|
662
|
+
throw new TypeError(`${label} must be a bounded Unicode string.`);
|
|
663
|
+
}
|
|
664
|
+
const bytes = new TextEncoder().encode(value).byteLength;
|
|
665
|
+
if (bytes > MAX_PERCOLATION_TEXT_UTF8_BYTES) {
|
|
666
|
+
throw new RangeError(`${label} exceeds its ${MAX_PERCOLATION_TEXT_UTF8_BYTES.toLocaleString("en-US")}-byte limit.`);
|
|
667
|
+
}
|
|
668
|
+
budget.utf8Bytes += bytes;
|
|
669
|
+
if (budget.utf8Bytes > MAX_PERCOLATION_RESULT_UTF8_BYTES) {
|
|
670
|
+
throw new RangeError(`Percolation result text exceeds ${MAX_PERCOLATION_RESULT_UTF8_BYTES.toLocaleString("en-US")} UTF-8 bytes.`);
|
|
671
|
+
}
|
|
672
|
+
return value;
|
|
673
|
+
}
|
|
674
|
+
function canonicalNote(value, label, budget) {
|
|
675
|
+
const parsed = parsedText(value, label, budget);
|
|
676
|
+
if (!isCanonicalNoteId(parsed)) {
|
|
677
|
+
throw new TypeError(`${label} must be a canonical note ID.`);
|
|
678
|
+
}
|
|
679
|
+
return parsed;
|
|
680
|
+
}
|
|
681
|
+
function canonicalMarkdownPath(value, label, budget) {
|
|
682
|
+
const parsed = parsedText(value, label, budget);
|
|
683
|
+
if (!parsed.endsWith(".md") || !isCanonicalNoteId(parsed.slice(0, -3))) {
|
|
684
|
+
throw new TypeError(`${label} must be a canonical vault Markdown path.`);
|
|
685
|
+
}
|
|
686
|
+
return parsed;
|
|
687
|
+
}
|
|
688
|
+
function canonicalPredicate(value, label, budget) {
|
|
689
|
+
const parsed = parsedText(value, label, budget);
|
|
690
|
+
if (!isCanonicalRelationPredicate(parsed)) {
|
|
691
|
+
throw new TypeError(`${label} must be a canonical relation predicate.`);
|
|
692
|
+
}
|
|
693
|
+
return parsed;
|
|
694
|
+
}
|
|
695
|
+
function nullableText(value, label, budget) {
|
|
696
|
+
return value === null ? null : parsedText(value, label, budget, { empty: true });
|
|
697
|
+
}
|
|
698
|
+
function parsedBoolean(value, label) {
|
|
699
|
+
if (typeof value !== "boolean")
|
|
700
|
+
throw new TypeError(`${label} must be a boolean.`);
|
|
701
|
+
return value;
|
|
702
|
+
}
|
|
703
|
+
function positiveSafeInteger(value, label, maximum = MAX_PERCOLATION_EVIDENCE) {
|
|
704
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
705
|
+
throw new TypeError(`${label} must be a positive bounded safe integer.`);
|
|
706
|
+
}
|
|
707
|
+
return value;
|
|
708
|
+
}
|
|
709
|
+
function parsedMinSupport(value, label) {
|
|
710
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < DEFAULT_PERCOLATION_MIN_SUPPORT || value > MAX_PERCOLATION_LIMIT) {
|
|
711
|
+
throw new TypeError(`${label} must be an integer from ${DEFAULT_PERCOLATION_MIN_SUPPORT} through ${MAX_PERCOLATION_LIMIT}.`);
|
|
712
|
+
}
|
|
713
|
+
return value;
|
|
714
|
+
}
|
|
715
|
+
function predicateDisposition(value, label, budget) {
|
|
716
|
+
const record = dataRecord(value, label, budget);
|
|
717
|
+
const kind = parsedText(record.kind, `${label}.kind`, budget);
|
|
718
|
+
if (kind === "required") {
|
|
719
|
+
exactKeys(record, ["kind"], label);
|
|
720
|
+
return Object.freeze({ kind: "required" });
|
|
721
|
+
}
|
|
722
|
+
throw new TypeError(`${label}.kind must be required.`);
|
|
723
|
+
}
|
|
724
|
+
function parsedMissingConceptEvidence(value, label, budget) {
|
|
725
|
+
const record = dataRecord(value, label, budget);
|
|
726
|
+
exactKeys(record, ["kind", "note", "path", "tag"], label);
|
|
727
|
+
if (parsedText(record.kind, `${label}.kind`, budget) !== "tag") {
|
|
728
|
+
throw new TypeError(`${label}.kind must be tag.`);
|
|
729
|
+
}
|
|
730
|
+
const note = canonicalNote(record.note, `${label}.note`, budget);
|
|
731
|
+
const path = canonicalMarkdownPath(record.path, `${label}.path`, budget);
|
|
732
|
+
if (path !== `${note}.md`)
|
|
733
|
+
throw new TypeError(`${label}.path must identify its note.`);
|
|
734
|
+
return Object.freeze({
|
|
735
|
+
kind: "tag",
|
|
736
|
+
note,
|
|
737
|
+
path,
|
|
738
|
+
tag: parsedText(record.tag, `${label}.tag`, budget)
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
function parsedSharedEvidence(value, label, budget) {
|
|
742
|
+
const record = dataRecord(value, label, budget);
|
|
743
|
+
const kind = parsedText(record.kind, `${label}.kind`, budget);
|
|
744
|
+
if (kind === "shared-tag") {
|
|
745
|
+
exactKeys(record, ["kind", "note", "path", "tag"], label);
|
|
746
|
+
const note = canonicalNote(record.note, `${label}.note`, budget);
|
|
747
|
+
const path = canonicalMarkdownPath(record.path, `${label}.path`, budget);
|
|
748
|
+
if (path !== `${note}.md`)
|
|
749
|
+
throw new TypeError(`${label}.path must identify its note.`);
|
|
750
|
+
return Object.freeze({
|
|
751
|
+
kind: "shared-tag",
|
|
752
|
+
note,
|
|
753
|
+
path,
|
|
754
|
+
tag: parsedText(record.tag, `${label}.tag`, budget)
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
if (kind === "shared-concept") {
|
|
758
|
+
exactKeys(record, ["kind", "note", "path", "concept", "conceptPath"], label);
|
|
759
|
+
const note = canonicalNote(record.note, `${label}.note`, budget);
|
|
760
|
+
const path = canonicalMarkdownPath(record.path, `${label}.path`, budget);
|
|
761
|
+
const concept = canonicalNote(record.concept, `${label}.concept`, budget);
|
|
762
|
+
const conceptPath = canonicalMarkdownPath(record.conceptPath, `${label}.conceptPath`, budget);
|
|
763
|
+
if (path !== `${note}.md`)
|
|
764
|
+
throw new TypeError(`${label}.path must identify its note.`);
|
|
765
|
+
if (conceptPath !== `${concept}.md`) {
|
|
766
|
+
throw new TypeError(`${label}.conceptPath must identify its concept.`);
|
|
767
|
+
}
|
|
768
|
+
return Object.freeze({
|
|
769
|
+
kind: "shared-concept",
|
|
770
|
+
note,
|
|
771
|
+
path,
|
|
772
|
+
concept,
|
|
773
|
+
conceptPath
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
throw new TypeError(`${label}.kind must be shared-tag or shared-concept.`);
|
|
777
|
+
}
|
|
778
|
+
function parsedMentionEvidence(value, label, budget) {
|
|
779
|
+
const record = dataRecord(value, label, budget);
|
|
780
|
+
exactKeys(record, ["kind", "source", "target", "line", "phrase"], label);
|
|
781
|
+
if (parsedText(record.kind, `${label}.kind`, budget) !== "mention") {
|
|
782
|
+
throw new TypeError(`${label}.kind must be mention.`);
|
|
783
|
+
}
|
|
784
|
+
return Object.freeze({
|
|
785
|
+
kind: "mention",
|
|
786
|
+
source: canonicalNote(record.source, `${label}.source`, budget),
|
|
787
|
+
target: canonicalNote(record.target, `${label}.target`, budget),
|
|
788
|
+
line: positiveSafeInteger(record.line, `${label}.line`),
|
|
789
|
+
phrase: parsedText(record.phrase, `${label}.phrase`, budget)
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
function parsedRelationEvidence(value, label, budget) {
|
|
793
|
+
const record = dataRecord(value, label, budget);
|
|
794
|
+
exactKeys(record, ["kind", "source", "target", "predicate", "line", "authoredTarget"], label);
|
|
795
|
+
if (parsedText(record.kind, `${label}.kind`, budget) !== "relation") {
|
|
796
|
+
throw new TypeError(`${label}.kind must be relation.`);
|
|
797
|
+
}
|
|
798
|
+
return Object.freeze({
|
|
799
|
+
kind: "relation",
|
|
800
|
+
source: canonicalNote(record.source, `${label}.source`, budget),
|
|
801
|
+
target: canonicalNote(record.target, `${label}.target`, budget),
|
|
802
|
+
predicate: canonicalPredicate(record.predicate, `${label}.predicate`, budget),
|
|
803
|
+
line: positiveSafeInteger(record.line, `${label}.line`),
|
|
804
|
+
authoredTarget: parsedText(record.authoredTarget, `${label}.authoredTarget`, budget)
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
function parsedRelationIssueEvidence(value, label, budget) {
|
|
808
|
+
const record = dataRecord(value, label, budget);
|
|
809
|
+
exactKeys(record, [
|
|
810
|
+
"kind",
|
|
811
|
+
"issue",
|
|
812
|
+
"source",
|
|
813
|
+
"line",
|
|
814
|
+
"predicate",
|
|
815
|
+
"target",
|
|
816
|
+
"candidates",
|
|
817
|
+
"candidatesTruncated",
|
|
818
|
+
"message"
|
|
819
|
+
], label);
|
|
820
|
+
if (parsedText(record.kind, `${label}.kind`, budget) !== "relation-issue") {
|
|
821
|
+
throw new TypeError(`${label}.kind must be relation-issue.`);
|
|
822
|
+
}
|
|
823
|
+
if (record.issue !== "malformed" && record.issue !== "broken" && record.issue !== "ambiguous") {
|
|
824
|
+
throw new TypeError(`${label}.issue is unsupported.`);
|
|
825
|
+
}
|
|
826
|
+
const issue = parsedText(record.issue, `${label}.issue`, budget);
|
|
827
|
+
const candidates = dataArray(record.candidates, `${label}.candidates`, MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE, budget).map((candidate, index) => canonicalNote(candidate, `${label}.candidates[${index}]`, budget));
|
|
828
|
+
for (let index = 0;index < candidates.length; index += 1) {
|
|
829
|
+
const previous = candidates[index - 1];
|
|
830
|
+
const candidate = candidates[index];
|
|
831
|
+
if (candidate === undefined)
|
|
832
|
+
continue;
|
|
833
|
+
if (previous !== undefined && compareText(previous, candidate) >= 0) {
|
|
834
|
+
throw new TypeError(`${label}.candidates must be sorted and unique.`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (issue !== "ambiguous" && candidates.length !== 0) {
|
|
838
|
+
throw new TypeError(`${label}.candidates are only valid for ambiguous issues.`);
|
|
839
|
+
}
|
|
840
|
+
if (issue === "ambiguous" && candidates.length < 2) {
|
|
841
|
+
throw new TypeError(`${label}.candidates must identify at least two ambiguous notes.`);
|
|
842
|
+
}
|
|
843
|
+
const candidatesTruncated = parsedBoolean(record.candidatesTruncated, `${label}.candidatesTruncated`);
|
|
844
|
+
if (issue !== "ambiguous" && candidatesTruncated || candidatesTruncated && candidates.length !== MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE) {
|
|
845
|
+
throw new TypeError(`${label}.candidatesTruncated is inconsistent.`);
|
|
846
|
+
}
|
|
847
|
+
const predicate = issue === "malformed" ? nullableText(record.predicate, `${label}.predicate`, budget) : canonicalPredicate(record.predicate, `${label}.predicate`, budget);
|
|
848
|
+
const target = issue === "malformed" ? nullableText(record.target, `${label}.target`, budget) : canonicalNote(record.target, `${label}.target`, budget);
|
|
849
|
+
return Object.freeze({
|
|
850
|
+
kind: "relation-issue",
|
|
851
|
+
issue,
|
|
852
|
+
source: canonicalNote(record.source, `${label}.source`, budget),
|
|
853
|
+
line: positiveSafeInteger(record.line, `${label}.line`),
|
|
854
|
+
predicate,
|
|
855
|
+
target,
|
|
856
|
+
candidates: Object.freeze(candidates),
|
|
857
|
+
candidatesTruncated,
|
|
858
|
+
message: parsedText(record.message, `${label}.message`, budget)
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
function evidenceArray(value, label, budget, parse) {
|
|
862
|
+
const input = dataArray(value, label, MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE, budget);
|
|
863
|
+
if (input.length === 0)
|
|
864
|
+
throw new TypeError(`${label} must not be empty.`);
|
|
865
|
+
const output = input.map((entry, index) => parse(entry, `${label}[${index}]`, budget));
|
|
866
|
+
const identities = new Set;
|
|
867
|
+
for (const entry of output) {
|
|
868
|
+
const identity = JSON.stringify(entry);
|
|
869
|
+
if (identities.has(identity))
|
|
870
|
+
throw new TypeError(`${label} must be unique.`);
|
|
871
|
+
identities.add(identity);
|
|
872
|
+
}
|
|
873
|
+
return Object.freeze(output);
|
|
874
|
+
}
|
|
875
|
+
function parsedRelationProblem(value, label, budget) {
|
|
876
|
+
const parsed = parsedText(value, label, budget);
|
|
877
|
+
if (parsed !== "self-relation" && parsed !== "reciprocal-relation" && parsed !== "malformed-relation" && parsed !== "broken-relation" && parsed !== "ambiguous-relation")
|
|
878
|
+
throw new TypeError(`${label} is unsupported.`);
|
|
879
|
+
return parsed;
|
|
880
|
+
}
|
|
881
|
+
function parsedCommonCandidate(record, label) {
|
|
882
|
+
return {
|
|
883
|
+
support: positiveSafeInteger(record.support, `${label}.support`),
|
|
884
|
+
evidenceTruncated: parsedBoolean(record.evidenceTruncated, `${label}.evidenceTruncated`)
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
function parseCandidate(value, label, budget, version) {
|
|
888
|
+
const record = dataRecord(value, label, budget);
|
|
889
|
+
const kind = parsedText(record.kind, `${label}.kind`, budget);
|
|
890
|
+
if (kind === "missing-concept") {
|
|
891
|
+
exactKeys(record, [
|
|
892
|
+
"kind",
|
|
893
|
+
"tag",
|
|
894
|
+
"suggestedId",
|
|
895
|
+
"collidesWith",
|
|
896
|
+
"support",
|
|
897
|
+
"evidenceTruncated",
|
|
898
|
+
"evidence"
|
|
899
|
+
], label);
|
|
900
|
+
const common = parsedCommonCandidate(record, label);
|
|
901
|
+
const tag = parsedText(record.tag, `${label}.tag`, budget);
|
|
902
|
+
const evidence = evidenceArray(record.evidence, `${label}.evidence`, budget, parsedMissingConceptEvidence);
|
|
903
|
+
if (evidence.some((entry) => entry.tag !== tag)) {
|
|
904
|
+
throw new TypeError(`${label}.evidence must support the candidate tag.`);
|
|
905
|
+
}
|
|
906
|
+
if (!common.evidenceTruncated && common.support !== evidence.length || common.evidenceTruncated && common.support <= evidence.length) {
|
|
907
|
+
throw new TypeError(`${label}.support does not match its bounded evidence.`);
|
|
908
|
+
}
|
|
909
|
+
return Object.freeze({
|
|
910
|
+
kind: "missing-concept",
|
|
911
|
+
tag,
|
|
912
|
+
suggestedId: canonicalNote(record.suggestedId, `${label}.suggestedId`, budget),
|
|
913
|
+
collidesWith: record.collidesWith === null ? null : canonicalNote(record.collidesWith, `${label}.collidesWith`, budget),
|
|
914
|
+
...common,
|
|
915
|
+
evidence
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
if (kind === "missing-relation") {
|
|
919
|
+
exactKeys(record, version === 1 ? [
|
|
920
|
+
"kind",
|
|
921
|
+
"source",
|
|
922
|
+
"target",
|
|
923
|
+
"suggestedPredicate",
|
|
924
|
+
"support",
|
|
925
|
+
"evidenceTruncated",
|
|
926
|
+
"evidence"
|
|
927
|
+
] : [
|
|
928
|
+
"kind",
|
|
929
|
+
"source",
|
|
930
|
+
"target",
|
|
931
|
+
"predicate",
|
|
932
|
+
"support",
|
|
933
|
+
"evidenceTruncated",
|
|
934
|
+
"evidence"
|
|
935
|
+
], label);
|
|
936
|
+
const source = canonicalNote(record.source, `${label}.source`, budget);
|
|
937
|
+
const target = canonicalNote(record.target, `${label}.target`, budget);
|
|
938
|
+
if (compareText(source, target) >= 0) {
|
|
939
|
+
throw new TypeError(`${label} endpoints must be an ordered, distinct pair.`);
|
|
940
|
+
}
|
|
941
|
+
const common = parsedCommonCandidate(record, label);
|
|
942
|
+
const evidence = evidenceArray(record.evidence, `${label}.evidence`, budget, parsedSharedEvidence);
|
|
943
|
+
if (evidence.some((entry) => entry.note !== source && entry.note !== target)) {
|
|
944
|
+
throw new TypeError(`${label}.evidence must belong to one of the unordered endpoints.`);
|
|
945
|
+
}
|
|
946
|
+
const signalEndpoints = new Map;
|
|
947
|
+
for (const entry of evidence) {
|
|
948
|
+
const signal = entry.kind === "shared-tag" ? `tag\x00${entry.tag}` : `concept\x00${entry.concept}`;
|
|
949
|
+
const endpoints = signalEndpoints.get(signal) ?? new Set;
|
|
950
|
+
endpoints.add(entry.note);
|
|
951
|
+
signalEndpoints.set(signal, endpoints);
|
|
952
|
+
}
|
|
953
|
+
if ([...signalEndpoints.values()].some((endpoints) => endpoints.size !== 2 || !endpoints.has(source) || !endpoints.has(target))) {
|
|
954
|
+
throw new TypeError(`${label}.evidence must pair both unordered endpoints per signal.`);
|
|
955
|
+
}
|
|
956
|
+
if (!common.evidenceTruncated && common.support !== signalEndpoints.size || common.evidenceTruncated && (evidence.length !== MAX_PERCOLATION_EVIDENCE_PER_CANDIDATE || common.support <= signalEndpoints.size)) {
|
|
957
|
+
throw new TypeError(`${label}.support does not match its bounded shared signals.`);
|
|
958
|
+
}
|
|
959
|
+
if (version === 1) {
|
|
960
|
+
if (parsedText(record.suggestedPredicate, `${label}.suggestedPredicate`, budget) !== "related-to") {
|
|
961
|
+
throw new TypeError(`${label}.suggestedPredicate must be related-to.`);
|
|
962
|
+
}
|
|
963
|
+
return Object.freeze({
|
|
964
|
+
kind: "missing-relation",
|
|
965
|
+
source,
|
|
966
|
+
target,
|
|
967
|
+
suggestedPredicate: "related-to",
|
|
968
|
+
...common,
|
|
969
|
+
evidence
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
return Object.freeze({
|
|
973
|
+
kind: "missing-relation",
|
|
974
|
+
source,
|
|
975
|
+
target,
|
|
976
|
+
predicate: predicateDisposition(record.predicate, `${label}.predicate`, budget),
|
|
977
|
+
...common,
|
|
978
|
+
evidence
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
if (kind === "unlinked-mention") {
|
|
982
|
+
exactKeys(record, ["kind", "source", "target", "support", "evidenceTruncated", "evidence"], label);
|
|
983
|
+
const source = canonicalNote(record.source, `${label}.source`, budget);
|
|
984
|
+
const target = canonicalNote(record.target, `${label}.target`, budget);
|
|
985
|
+
const common = parsedCommonCandidate(record, label);
|
|
986
|
+
const evidence = evidenceArray(record.evidence, `${label}.evidence`, budget, parsedMentionEvidence);
|
|
987
|
+
if (evidence.some((entry) => entry.source !== source || entry.target !== target)) {
|
|
988
|
+
throw new TypeError(`${label}.evidence must identify the candidate endpoints.`);
|
|
989
|
+
}
|
|
990
|
+
if (!common.evidenceTruncated && common.support !== evidence.length || common.evidenceTruncated && common.support <= evidence.length) {
|
|
991
|
+
throw new TypeError(`${label}.support does not match its bounded evidence.`);
|
|
992
|
+
}
|
|
993
|
+
return Object.freeze({
|
|
994
|
+
kind: "unlinked-mention",
|
|
995
|
+
source,
|
|
996
|
+
target,
|
|
997
|
+
...common,
|
|
998
|
+
evidence
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
if (kind === "relation-hygiene") {
|
|
1002
|
+
exactKeys(record, [
|
|
1003
|
+
"kind",
|
|
1004
|
+
"problem",
|
|
1005
|
+
"source",
|
|
1006
|
+
"target",
|
|
1007
|
+
"predicate",
|
|
1008
|
+
"message",
|
|
1009
|
+
"support",
|
|
1010
|
+
"evidenceTruncated",
|
|
1011
|
+
"evidence"
|
|
1012
|
+
], label);
|
|
1013
|
+
const problem = parsedRelationProblem(record.problem, `${label}.problem`, budget);
|
|
1014
|
+
const source = canonicalNote(record.source, `${label}.source`, budget);
|
|
1015
|
+
const target = problem === "malformed-relation" ? nullableText(record.target, `${label}.target`, budget) : record.target === null ? null : canonicalNote(record.target, `${label}.target`, budget);
|
|
1016
|
+
const predicate = problem === "malformed-relation" ? nullableText(record.predicate, `${label}.predicate`, budget) : record.predicate === null ? null : canonicalPredicate(record.predicate, `${label}.predicate`, budget);
|
|
1017
|
+
const common = parsedCommonCandidate(record, label);
|
|
1018
|
+
const relationProblem = problem === "self-relation" || problem === "reciprocal-relation";
|
|
1019
|
+
const evidence = relationProblem ? evidenceArray(record.evidence, `${label}.evidence`, budget, parsedRelationEvidence) : evidenceArray(record.evidence, `${label}.evidence`, budget, parsedRelationIssueEvidence);
|
|
1020
|
+
if (common.support !== evidence.length) {
|
|
1021
|
+
throw new TypeError(`${label}.support must equal its hygiene evidence count.`);
|
|
1022
|
+
}
|
|
1023
|
+
if (relationProblem) {
|
|
1024
|
+
const relations = evidence;
|
|
1025
|
+
if (target === null || predicate === null || common.evidenceTruncated || problem === "self-relation" && target !== source || problem === "reciprocal-relation" && (compareText(source, target) >= 0 || relations.length !== 2) || relations.some((entry) => entry.predicate !== predicate || (problem === "self-relation" ? entry.source !== source || entry.target !== target : !(entry.source === source && entry.target === target || entry.source === target && entry.target === source))))
|
|
1026
|
+
throw new TypeError(`${label}.evidence must identify the hygiene relation.`);
|
|
1027
|
+
} else {
|
|
1028
|
+
const issues = evidence;
|
|
1029
|
+
const expectedIssue = problem.slice(0, -"-relation".length);
|
|
1030
|
+
if (issues.some((entry) => entry.source !== source || entry.issue !== expectedIssue || entry.predicate !== predicate || entry.target !== target || entry.message !== record.message) || common.evidenceTruncated !== issues.some((entry) => entry.candidatesTruncated)) {
|
|
1031
|
+
throw new TypeError(`${label}.evidence must identify the hygiene issue.`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return Object.freeze({
|
|
1035
|
+
kind: "relation-hygiene",
|
|
1036
|
+
problem,
|
|
1037
|
+
source,
|
|
1038
|
+
target,
|
|
1039
|
+
predicate,
|
|
1040
|
+
message: parsedText(record.message, `${label}.message`, budget),
|
|
1041
|
+
...common,
|
|
1042
|
+
evidence
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
throw new TypeError(`${label}.kind is unsupported.`);
|
|
1046
|
+
}
|
|
1047
|
+
function parsedCandidates(value, label, budget, version) {
|
|
1048
|
+
const input = dataArray(value, label, MAX_PERCOLATION_LIMIT, budget);
|
|
1049
|
+
const output = input.map((entry, index) => parseCandidate(entry, `${label}[${index}]`, budget, version));
|
|
1050
|
+
if (version === 1) {
|
|
1051
|
+
const historical = output;
|
|
1052
|
+
for (let index = 1;index < historical.length; index += 1) {
|
|
1053
|
+
const previous = historical[index - 1];
|
|
1054
|
+
const candidate = historical[index];
|
|
1055
|
+
if (previous !== undefined && candidate !== undefined && compareHistoricalCandidates(previous, candidate) > 0) {
|
|
1056
|
+
throw new TypeError(`${label} must use historical percolation ordering.`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return Object.freeze([...historical]);
|
|
1060
|
+
}
|
|
1061
|
+
const identities = new Set;
|
|
1062
|
+
const suggestedConceptIds = new Set;
|
|
1063
|
+
for (let index = 0;index < output.length; index += 1) {
|
|
1064
|
+
const candidate = output[index];
|
|
1065
|
+
if (candidate === undefined)
|
|
1066
|
+
continue;
|
|
1067
|
+
const identity = `${candidate.kind}\x00${candidateIdentity(candidate)}`;
|
|
1068
|
+
if (identities.has(identity))
|
|
1069
|
+
throw new TypeError(`${label} must be unique.`);
|
|
1070
|
+
identities.add(identity);
|
|
1071
|
+
if (candidate.kind === "missing-concept") {
|
|
1072
|
+
if (suggestedConceptIds.has(candidate.suggestedId)) {
|
|
1073
|
+
throw new TypeError(`${label} must use unique suggested concept IDs.`);
|
|
1074
|
+
}
|
|
1075
|
+
suggestedConceptIds.add(candidate.suggestedId);
|
|
1076
|
+
}
|
|
1077
|
+
const previous = output[index - 1];
|
|
1078
|
+
if (previous !== undefined && compareCandidates(previous, candidate) > 0) {
|
|
1079
|
+
throw new TypeError(`${label} must use canonical percolation ordering.`);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return Object.freeze(output);
|
|
1083
|
+
}
|
|
1084
|
+
function parseResultFields(record, label, budget, version) {
|
|
1085
|
+
return {
|
|
1086
|
+
candidates: parsedCandidates(record.candidates, `${label}.candidates`, budget, version),
|
|
1087
|
+
truncated: parsedBoolean(record.truncated, `${label}.truncated`)
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
function parsePercolationResultV1(value) {
|
|
1091
|
+
const budget = { nodes: 0, utf8Bytes: 0 };
|
|
1092
|
+
const record = dataRecord(value, "percolation result v1", budget);
|
|
1093
|
+
exactKeys(record, ["candidates", "truncated"], "percolation result v1");
|
|
1094
|
+
const fields = parseResultFields(record, "percolation result v1", budget, 1);
|
|
1095
|
+
return Object.freeze({
|
|
1096
|
+
candidates: fields.candidates,
|
|
1097
|
+
truncated: fields.truncated
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
function parsePercolationResultV2(value) {
|
|
1101
|
+
const budget = { nodes: 0, utf8Bytes: 0 };
|
|
1102
|
+
const record = dataRecord(value, "percolation result v2", budget);
|
|
1103
|
+
exactKeys(record, ["schemaVersion", "candidates", "truncated"], "percolation result v2");
|
|
1104
|
+
if (record.schemaVersion !== PERCOLATION_RESULT_SCHEMA_VERSION) {
|
|
1105
|
+
throw new TypeError("percolation result v2.schemaVersion must be 2.");
|
|
1106
|
+
}
|
|
1107
|
+
const fields = parseResultFields(record, "percolation result v2", budget, 2);
|
|
1108
|
+
return Object.freeze({
|
|
1109
|
+
schemaVersion: PERCOLATION_RESULT_SCHEMA_VERSION,
|
|
1110
|
+
candidates: fields.candidates,
|
|
1111
|
+
truncated: fields.truncated
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
var parsePercolationResult = parsePercolationResultV2;
|
|
1115
|
+
function parsePercolationCliOutputV1(value) {
|
|
1116
|
+
const budget = { nodes: 0, utf8Bytes: 0 };
|
|
1117
|
+
const label = "percolation CLI output v1";
|
|
1118
|
+
const record = dataRecord(value, label, budget);
|
|
1119
|
+
exactKeys(record, ["root", "note", "minSupport", "candidates", "truncated"], label);
|
|
1120
|
+
const fields = parseResultFields(record, label, budget, 1);
|
|
1121
|
+
return Object.freeze({
|
|
1122
|
+
root: parsedText(record.root, `${label}.root`, budget),
|
|
1123
|
+
note: nullableText(record.note, `${label}.note`, budget),
|
|
1124
|
+
minSupport: parsedMinSupport(record.minSupport, `${label}.minSupport`),
|
|
1125
|
+
candidates: fields.candidates,
|
|
1126
|
+
truncated: fields.truncated
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
function parsePercolationCliOutputV2(value) {
|
|
1130
|
+
const budget = { nodes: 0, utf8Bytes: 0 };
|
|
1131
|
+
const label = "percolation CLI output v2";
|
|
1132
|
+
const record = dataRecord(value, label, budget);
|
|
1133
|
+
exactKeys(record, [
|
|
1134
|
+
"root",
|
|
1135
|
+
"note",
|
|
1136
|
+
"minSupport",
|
|
1137
|
+
"limit",
|
|
1138
|
+
"schemaVersion",
|
|
1139
|
+
"candidates",
|
|
1140
|
+
"truncated"
|
|
1141
|
+
], label);
|
|
1142
|
+
if (record.schemaVersion !== PERCOLATION_RESULT_SCHEMA_VERSION) {
|
|
1143
|
+
throw new TypeError(`${label}.schemaVersion must be 2.`);
|
|
1144
|
+
}
|
|
1145
|
+
const fields = parseResultFields(record, label, budget, 2);
|
|
1146
|
+
const limit = positiveSafeInteger(record.limit, `${label}.limit`, MAX_PERCOLATION_LIMIT);
|
|
1147
|
+
if (fields.candidates.length > limit || fields.truncated && fields.candidates.length !== limit) {
|
|
1148
|
+
throw new TypeError(`${label}.limit is inconsistent with its candidates.`);
|
|
1149
|
+
}
|
|
1150
|
+
return Object.freeze({
|
|
1151
|
+
root: parsedText(record.root, `${label}.root`, budget),
|
|
1152
|
+
note: nullableText(record.note, `${label}.note`, budget),
|
|
1153
|
+
minSupport: parsedMinSupport(record.minSupport, `${label}.minSupport`),
|
|
1154
|
+
limit,
|
|
1155
|
+
schemaVersion: PERCOLATION_RESULT_SCHEMA_VERSION,
|
|
1156
|
+
candidates: fields.candidates,
|
|
1157
|
+
truncated: fields.truncated
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
var parsePercolationCliOutput = parsePercolationCliOutputV2;
|
|
1161
|
+
|
|
1162
|
+
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, PERCOLATION_RESULT_SCHEMA_VERSION, MAX_PERCOLATION_RESULT_NODES, MAX_PERCOLATION_RESULT_UTF8_BYTES, MAX_PERCOLATION_TEXT_UTF8_BYTES, percolateVault, parsePercolationResultV1, parsePercolationResultV2, parsePercolationResult, parsePercolationCliOutputV1, parsePercolationCliOutputV2, parsePercolationCliOutput };
|