@hraness/wordcell 0.22.4 → 0.22.5
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 +20 -17
- package/dist/authoring.js +2 -2
- package/dist/{authority-06eqhqam.js → authority-3ga47syb.js} +2 -2
- package/dist/benchmark.js +3 -3
- package/dist/cli.js +15 -15
- package/dist/evaluation-builder.js +10 -10
- package/dist/evaluation-kb.js +10 -10
- package/dist/graph-authority.js +4 -4
- package/dist/graph-percolation.js +6 -6
- package/dist/graph.js +3 -1
- package/dist/{index-1k595z6v.js → index-1r5rnkfr.js} +2 -2
- package/dist/{index-q7tbg07z.js → index-42tb8qqp.js} +7 -7
- package/dist/{index-9h7zh0wt.js → index-8bgnhjgq.js} +17 -16
- package/dist/{index-233z9wmn.js → index-8v6k9h4r.js} +13 -5
- package/dist/{index-vnybgywh.js → index-aer0jdrq.js} +1 -1
- package/dist/{index-xv80vzfp.js → index-f75zmmdr.js} +4 -4
- package/dist/{index-ecqpgr5y.js → index-f7fnww7a.js} +1 -1
- package/dist/{index-djrcc1yf.js → index-gtwqye5a.js} +4 -4
- package/dist/{index-bqtqeak3.js → index-h2qn3y3f.js} +4 -4
- package/dist/{index-rr9kzq4n.js → index-n57tewfr.js} +2 -2
- package/dist/{index-76dbn6pe.js → index-n9kwenpt.js} +1 -1
- package/dist/{index-n386evxj.js → index-p96kb6t2.js} +1 -1
- package/dist/{index-tf4wzjew.js → index-pz2b2x0y.js} +1 -1
- package/dist/{index-jk1sgx36.js → index-qdb8f8va.js} +1 -1
- package/dist/{index-pnt9wr59.js → index-xzvcw9ga.js} +2 -2
- package/dist/{index-qbssx940.js → index-zy7an84p.js} +223 -8
- package/dist/index.js +20 -14
- package/dist/percolate.js +2 -2
- package/dist/portfolio.js +10 -10
- package/dist/publish.js +3 -3
- package/dist/sdk.js +9 -9
- package/dist/search.js +2 -2
- package/dist/semantic.js +4 -4
- package/dist/{support-m3t917ev.js → support-jf9v10j7.js} +14 -14
- package/dist/workflows/decision-context.js +10 -10
- package/dist/workflows/index.js +10 -10
- package/package.json +5 -4
- package/skills/wordcell/SKILL.md +1 -1
- package/skills/wordcell/references/plan-structure.md +1 -1
- package/skills/wordcell/references/url-platforms.md +1 -1
- package/src/cli-program.ts +4 -2
- package/src/graph.ts +79 -9
- package/src/mention-index.ts +194 -0
- package/src/vault.ts +26 -2
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import {
|
|
3
3
|
parseArguments as parseArguments2,
|
|
4
4
|
parseUrlMetadataArguments
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-8bgnhjgq.js";
|
|
6
6
|
import"./index-bcknqxrq.js";
|
|
7
7
|
import {
|
|
8
8
|
parsePdfArguments
|
|
9
9
|
} from "./index-054mb7d3.js";
|
|
10
|
-
import"./index-
|
|
10
|
+
import"./index-h2qn3y3f.js";
|
|
11
11
|
import"./index-j4zgmzjr.js";
|
|
12
12
|
import"./index-de2w8crk.js";
|
|
13
13
|
import"./index-6jcz0m1c.js";
|
|
@@ -28,22 +28,22 @@ import"./index-mxxxytys.js";
|
|
|
28
28
|
import"./index-23z4zxgg.js";
|
|
29
29
|
import"./index-pj501bh1.js";
|
|
30
30
|
import"./index-9rf81m0p.js";
|
|
31
|
-
import"./index-
|
|
32
|
-
import"./index-
|
|
31
|
+
import"./index-1r5rnkfr.js";
|
|
32
|
+
import"./index-42tb8qqp.js";
|
|
33
33
|
import"./index-adx6khj5.js";
|
|
34
|
-
import"./index-
|
|
34
|
+
import"./index-n57tewfr.js";
|
|
35
35
|
import"./index-4j3tt0c3.js";
|
|
36
36
|
import"./index-j70m75wd.js";
|
|
37
37
|
import"./index-b88v3vtm.js";
|
|
38
|
-
import"./index-
|
|
39
|
-
import"./index-
|
|
40
|
-
import"./index-
|
|
41
|
-
import"./index-
|
|
38
|
+
import"./index-gtwqye5a.js";
|
|
39
|
+
import"./index-qdb8f8va.js";
|
|
40
|
+
import"./index-f75zmmdr.js";
|
|
41
|
+
import"./index-pz2b2x0y.js";
|
|
42
42
|
import"./index-1gwbassd.js";
|
|
43
|
-
import"./index-
|
|
43
|
+
import"./index-aer0jdrq.js";
|
|
44
44
|
import"./index-1xxnjn0d.js";
|
|
45
|
-
import"./index-
|
|
46
|
-
import"./index-
|
|
45
|
+
import"./index-xzvcw9ga.js";
|
|
46
|
+
import"./index-8v6k9h4r.js";
|
|
47
47
|
import"./index-d13v9ckt.js";
|
|
48
48
|
import"./index-48pz4jpc.js";
|
|
49
49
|
import"./index-06c9ctr6.js";
|
|
@@ -53,9 +53,9 @@ import"./index-23m6bbjt.js";
|
|
|
53
53
|
import"./index-hya40gb2.js";
|
|
54
54
|
import"./index-5vwpzb5a.js";
|
|
55
55
|
import"./index-x3fthpsc.js";
|
|
56
|
-
import"./index-
|
|
56
|
+
import"./index-f7fnww7a.js";
|
|
57
57
|
import"./index-3rm7cz6h.js";
|
|
58
|
-
import"./index-
|
|
58
|
+
import"./index-zy7an84p.js";
|
|
59
59
|
import"./index-z1w83f81.js";
|
|
60
60
|
|
|
61
61
|
// src/support.ts
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
decisionContextWorkflow
|
|
4
|
-
} from "../index-
|
|
4
|
+
} from "../index-n9kwenpt.js";
|
|
5
5
|
import"../index-h170byqw.js";
|
|
6
|
-
import"../index-
|
|
6
|
+
import"../index-42tb8qqp.js";
|
|
7
7
|
import"../index-adx6khj5.js";
|
|
8
|
-
import"../index-
|
|
8
|
+
import"../index-n57tewfr.js";
|
|
9
9
|
import"../index-4j3tt0c3.js";
|
|
10
10
|
import"../index-j70m75wd.js";
|
|
11
|
-
import"../index-
|
|
12
|
-
import"../index-
|
|
13
|
-
import"../index-
|
|
14
|
-
import"../index-
|
|
11
|
+
import"../index-gtwqye5a.js";
|
|
12
|
+
import"../index-qdb8f8va.js";
|
|
13
|
+
import"../index-f75zmmdr.js";
|
|
14
|
+
import"../index-pz2b2x0y.js";
|
|
15
15
|
import"../index-1gwbassd.js";
|
|
16
|
-
import"../index-
|
|
16
|
+
import"../index-aer0jdrq.js";
|
|
17
17
|
import"../index-1xxnjn0d.js";
|
|
18
|
-
import"../index-
|
|
18
|
+
import"../index-8v6k9h4r.js";
|
|
19
19
|
import"../index-d13v9ckt.js";
|
|
20
20
|
import"../index-48pz4jpc.js";
|
|
21
21
|
import"../index-06c9ctr6.js";
|
|
22
22
|
import"../index-5vwpzb5a.js";
|
|
23
23
|
import"../index-3rm7cz6h.js";
|
|
24
|
-
import"../index-
|
|
24
|
+
import"../index-zy7an84p.js";
|
|
25
25
|
import"../index-z1w83f81.js";
|
|
26
26
|
export {
|
|
27
27
|
decisionContextWorkflow
|
package/dist/workflows/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
decisionContextWorkflow
|
|
4
|
-
} from "../index-
|
|
4
|
+
} from "../index-n9kwenpt.js";
|
|
5
5
|
import {
|
|
6
6
|
explainChangeWorkflow
|
|
7
7
|
} from "../index-e4fagfjz.js";
|
|
@@ -9,25 +9,25 @@ import {
|
|
|
9
9
|
planRadarWorkflow
|
|
10
10
|
} from "../index-rdd5f7jj.js";
|
|
11
11
|
import"../index-h170byqw.js";
|
|
12
|
-
import"../index-
|
|
12
|
+
import"../index-42tb8qqp.js";
|
|
13
13
|
import"../index-adx6khj5.js";
|
|
14
|
-
import"../index-
|
|
14
|
+
import"../index-n57tewfr.js";
|
|
15
15
|
import"../index-4j3tt0c3.js";
|
|
16
16
|
import"../index-j70m75wd.js";
|
|
17
|
-
import"../index-
|
|
18
|
-
import"../index-
|
|
19
|
-
import"../index-
|
|
20
|
-
import"../index-
|
|
17
|
+
import"../index-gtwqye5a.js";
|
|
18
|
+
import"../index-qdb8f8va.js";
|
|
19
|
+
import"../index-f75zmmdr.js";
|
|
20
|
+
import"../index-pz2b2x0y.js";
|
|
21
21
|
import"../index-1gwbassd.js";
|
|
22
|
-
import"../index-
|
|
22
|
+
import"../index-aer0jdrq.js";
|
|
23
23
|
import"../index-1xxnjn0d.js";
|
|
24
|
-
import"../index-
|
|
24
|
+
import"../index-8v6k9h4r.js";
|
|
25
25
|
import"../index-d13v9ckt.js";
|
|
26
26
|
import"../index-48pz4jpc.js";
|
|
27
27
|
import"../index-06c9ctr6.js";
|
|
28
28
|
import"../index-5vwpzb5a.js";
|
|
29
29
|
import"../index-3rm7cz6h.js";
|
|
30
|
-
import"../index-
|
|
30
|
+
import"../index-zy7an84p.js";
|
|
31
31
|
import"../index-z1w83f81.js";
|
|
32
32
|
export {
|
|
33
33
|
planRadarWorkflow,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/wordcell",
|
|
3
|
-
"version": "0.22.
|
|
4
|
-
"description": "A local Markdown knowledge base with superpowers. Save decisions, sources, and plans as files you own, then
|
|
3
|
+
"version": "0.22.5",
|
|
4
|
+
"description": "A local Markdown knowledge base with superpowers. Save decisions, sources, and plans as files you own, then find them by exact words or, with an optional local model, by meaning.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
|
@@ -381,6 +381,7 @@
|
|
|
381
381
|
"src/evaluation-v2.ts",
|
|
382
382
|
"src/evaluation.ts",
|
|
383
383
|
"src/graph.ts",
|
|
384
|
+
"src/mention-index.ts",
|
|
384
385
|
"src/index.ts",
|
|
385
386
|
"src/init.ts",
|
|
386
387
|
"src/navigation.ts",
|
|
@@ -479,8 +480,8 @@
|
|
|
479
480
|
"check": "bun run check:worker && bun run check:effect && bun run check:workflows && bun run check:portfolio-inventory && bun run check:installed-command-docs && bun run typecheck && bun run build && bun run test:package && bun run test && bun run url-metadata:tool:check",
|
|
480
481
|
"prepack": "bun run check",
|
|
481
482
|
"check:effect": "bun run scripts/check-effect-policy.ts",
|
|
482
|
-
"kb:search": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.22.
|
|
483
|
-
"kb:search:local": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.22.
|
|
483
|
+
"kb:search": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.22.5/hraness-wordcell-0.22.5.tgz wordcell search --root kb --mode exact --rerank typesafe --rerank-limit 25",
|
|
484
|
+
"kb:search:local": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.22.5/hraness-wordcell-0.22.5.tgz wordcell search --root kb --mode exact",
|
|
484
485
|
"check:worker": "tsc -p worker/tsconfig.json --noEmit"
|
|
485
486
|
},
|
|
486
487
|
"dependencies": {
|
package/skills/wordcell/SKILL.md
CHANGED
|
@@ -56,7 +56,7 @@ missing:
|
|
|
56
56
|
```sh
|
|
57
57
|
command -v wordcell >/dev/null 2>&1 || {
|
|
58
58
|
command -v bun >/dev/null 2>&1 || exit 1
|
|
59
|
-
bun add --global --ignore-scripts https://github.com/hraness/wordcell/releases/download/v0.22.
|
|
59
|
+
bun add --global --ignore-scripts https://github.com/hraness/wordcell/releases/download/v0.22.5/hraness-wordcell-0.22.5.tgz
|
|
60
60
|
}
|
|
61
61
|
wordcell --help
|
|
62
62
|
```
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Use the strongest available read route, then describe exactly what it retained. Every route is bounded by item count, tree depth, time, HTML bytes, per-asset bytes, and aggregate asset bytes.
|
|
4
4
|
|
|
5
|
-
| Surface | Preferred route | Conversation behavior |
|
|
5
|
+
| Surface | Preferred route | Conversation behavior | Limit |
|
|
6
6
|
| --- | --- | --- | --- |
|
|
7
7
|
| Generic articles and papers | Structured data or HTTP + Defuddle; rendered browser fallback | Captures the page body and any comments present in that representation | JavaScript-only or unusual layouts can remain partial |
|
|
8
8
|
| X | Defuddle X extraction plus rendered text; public profile metadata as a separate reviewed surface; current tab or profile for signed-in views | Preserves the complete visible root post or long-form article, quote context, visible metrics, localized post media, and loaded replies while keeping author, continuation, and reply boundaries distinct | Virtualized or unloaded replies and missing trustworthy item trees remain partial; profile bios and links are never inferred from post prose |
|
package/src/cli-program.ts
CHANGED
|
@@ -162,7 +162,9 @@ import { MAX_RERANK_CANDIDATES, type SearchReranker } from "./rerank.js";
|
|
|
162
162
|
import { createCliTypeSafeReranker } from "./rerank-credentials.js";
|
|
163
163
|
import {
|
|
164
164
|
refreshVault,
|
|
165
|
+
refreshVaultComplete,
|
|
165
166
|
scanVault,
|
|
167
|
+
scanVaultComplete,
|
|
166
168
|
type ScanVaultOptions,
|
|
167
169
|
type VaultSnapshot,
|
|
168
170
|
} from "./vault.js";
|
|
@@ -3871,8 +3873,8 @@ async function runVault(
|
|
|
3871
3873
|
dependencies: CliDependencies,
|
|
3872
3874
|
): Promise<number> {
|
|
3873
3875
|
const snapshot = command.kind === "refresh"
|
|
3874
|
-
? await (dependencies.refreshVault ??
|
|
3875
|
-
: await (dependencies.scanVault ?? scanVault)(command.root, command.options);
|
|
3876
|
+
? await (dependencies.refreshVault ?? refreshVaultComplete)(command.root, command.options)
|
|
3877
|
+
: await (dependencies.scanVault ?? (command.kind === "check" ? scanVaultComplete : scanVault))(command.root, command.options);
|
|
3876
3878
|
|
|
3877
3879
|
if (command.kind === "refresh" || command.kind === "check") {
|
|
3878
3880
|
const noCatalog = command.kind === "check" && command.noCatalog === true;
|
package/src/graph.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
type Node,
|
|
10
10
|
} from "yaml";
|
|
11
11
|
import { parseQualifiedDocumentUri, type QualifiedDocumentUri } from "./portfolio-identity.js";
|
|
12
|
+
import { cleanMentionBoundary, MentionIndexBudgetError, MentionMatcher, preflightMentionInput, type MentionIndexBudgetKind, type MentionIndexLimits } from "./mention-index.js";
|
|
13
|
+
export type { MentionIndexLimits } from "./mention-index.js";
|
|
12
14
|
|
|
13
15
|
export const catalogStart = "<!-- kb:catalog:start -->";
|
|
14
16
|
export const catalogEnd = "<!-- kb:catalog:end -->";
|
|
@@ -22,7 +24,8 @@ export type VaultAnalysisBudgetKind =
|
|
|
22
24
|
| "notes"
|
|
23
25
|
| "connection-observations"
|
|
24
26
|
| "mention-pairs"
|
|
25
|
-
| "mentions"
|
|
27
|
+
| "mentions"
|
|
28
|
+
| MentionIndexBudgetKind;
|
|
26
29
|
|
|
27
30
|
/** A stable failure for callers that need to distinguish bounded graph work. */
|
|
28
31
|
export class VaultAnalysisBudgetError extends RangeError {
|
|
@@ -214,6 +217,11 @@ export type AnalyzeVaultOptions = {
|
|
|
214
217
|
catalogNoteId?: string;
|
|
215
218
|
};
|
|
216
219
|
|
|
220
|
+
/** Complete discovery indexes phrases instead of enumerating every note pair. */
|
|
221
|
+
export type CompleteAnalyzeVaultOptions = AnalyzeVaultOptions & {
|
|
222
|
+
readonly mentionIndexLimits?: MentionIndexLimits;
|
|
223
|
+
};
|
|
224
|
+
|
|
217
225
|
export type VaultAnalysis = {
|
|
218
226
|
noteCount: number;
|
|
219
227
|
contextualLinks: readonly ResolvedLink[];
|
|
@@ -1015,17 +1023,10 @@ function compareRelationIssues(left: RelationIssue, right: RelationIssue): numbe
|
|
|
1015
1023
|
|| targetComparison;
|
|
1016
1024
|
}
|
|
1017
1025
|
|
|
1018
|
-
const wordCharacter = (value: string): boolean => /[A-Za-z0-9]/.test(value);
|
|
1019
|
-
|
|
1020
1026
|
function phraseOffset(lowerHaystack: string, lowerPhrase: string): number {
|
|
1021
1027
|
let offset = lowerHaystack.indexOf(lowerPhrase);
|
|
1022
1028
|
while (offset !== -1) {
|
|
1023
|
-
|
|
1024
|
-
const afterIndex = offset + lowerPhrase.length;
|
|
1025
|
-
const after = afterIndex >= lowerHaystack.length ? "" : lowerHaystack[afterIndex] ?? "";
|
|
1026
|
-
const startsClean = !wordCharacter(lowerPhrase[0] ?? "") || !wordCharacter(before);
|
|
1027
|
-
const endsClean = !wordCharacter(lowerPhrase.at(-1) ?? "") || !wordCharacter(after);
|
|
1028
|
-
if (startsClean && endsClean) return offset;
|
|
1029
|
+
if (cleanMentionBoundary(lowerHaystack, lowerPhrase, offset)) return offset;
|
|
1029
1030
|
offset = lowerHaystack.indexOf(lowerPhrase, offset + 1);
|
|
1030
1031
|
}
|
|
1031
1032
|
return -1;
|
|
@@ -1093,6 +1094,33 @@ function checkedAnalysisLimit(
|
|
|
1093
1094
|
export function analyzeVault(
|
|
1094
1095
|
notes: readonly Note[],
|
|
1095
1096
|
options: AnalyzeVaultOptions = {},
|
|
1097
|
+
): VaultAnalysis {
|
|
1098
|
+
return analyzeVaultInternal(notes, options);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Return the same complete graph using a finite phrase index. maxMentionPairs
|
|
1103
|
+
* counts matching source/target candidates; index work and repeated matches
|
|
1104
|
+
* have separate aggregate limits. Exhaustion always throws, never truncates.
|
|
1105
|
+
*/
|
|
1106
|
+
export function analyzeVaultComplete(
|
|
1107
|
+
notes: readonly Note[],
|
|
1108
|
+
options: CompleteAnalyzeVaultOptions = {},
|
|
1109
|
+
): VaultAnalysis {
|
|
1110
|
+
try {
|
|
1111
|
+
return analyzeVaultInternal(notes, options, options.mentionIndexLimits ?? {});
|
|
1112
|
+
} catch (error: unknown) {
|
|
1113
|
+
if (error instanceof MentionIndexBudgetError) {
|
|
1114
|
+
throw new VaultAnalysisBudgetError(error.kind, error.limit, error.message);
|
|
1115
|
+
}
|
|
1116
|
+
throw error;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function analyzeVaultInternal(
|
|
1121
|
+
notes: readonly Note[],
|
|
1122
|
+
options: AnalyzeVaultOptions,
|
|
1123
|
+
mentionIndexLimits?: MentionIndexLimits,
|
|
1096
1124
|
): VaultAnalysis {
|
|
1097
1125
|
const maxNotes = checkedAnalysisLimit(
|
|
1098
1126
|
options.maxNotes,
|
|
@@ -1121,6 +1149,13 @@ export function analyzeVault(
|
|
|
1121
1149
|
`Vault analysis exceeds the ${maxNotes} note limit.`,
|
|
1122
1150
|
);
|
|
1123
1151
|
}
|
|
1152
|
+
const mentionInput = mentionIndexLimits === undefined ? undefined : preflightMentionInput((function* () {
|
|
1153
|
+
for (const note of notes) {
|
|
1154
|
+
yield note.searchableText;
|
|
1155
|
+
yield note.title;
|
|
1156
|
+
yield* note.aliases;
|
|
1157
|
+
}
|
|
1158
|
+
})(), mentionIndexLimits);
|
|
1124
1159
|
const catalogNoteId = withoutMarkdownExtension(
|
|
1125
1160
|
normalizeVaultPath(options.catalogNoteId ?? "index"),
|
|
1126
1161
|
);
|
|
@@ -1332,7 +1367,42 @@ export function analyzeVault(
|
|
|
1332
1367
|
: uniquePhrasesByTarget(suggestionNotes);
|
|
1333
1368
|
const mentions: MentionCandidate[] = [];
|
|
1334
1369
|
let mentionPairs = 0;
|
|
1370
|
+
const matcher = mentionIndexLimits === undefined ? undefined : new MentionMatcher(
|
|
1371
|
+
[...phrasesByTarget].flatMap(([targetId, phrases]) => phrases.map((phrase, rank) => ({ ...phrase, targetId, rank }))),
|
|
1372
|
+
mentionIndexLimits,
|
|
1373
|
+
mentionInput!.rawInputCodeUnits,
|
|
1374
|
+
mentionInput!.work,
|
|
1375
|
+
);
|
|
1376
|
+
const targetsById = new Map<string, { readonly note: Note; readonly order: number }[]>();
|
|
1377
|
+
if (matcher !== undefined) suggestionNotes.forEach((note, order) => {
|
|
1378
|
+
const targets = targetsById.get(note.id) ?? [];
|
|
1379
|
+
targets.push({ note, order });
|
|
1380
|
+
targetsById.set(note.id, targets);
|
|
1381
|
+
});
|
|
1335
1382
|
for (const source of suggestionNotes) {
|
|
1383
|
+
if (matcher !== undefined) {
|
|
1384
|
+
if (scopedMentionNotes.length === 0) continue;
|
|
1385
|
+
const found = matcher.scan(source.searchableText.toLocaleLowerCase("en-US"));
|
|
1386
|
+
const targets = [...found].flatMap(([id, match]) => (targetsById.get(id) ?? []).map((target) => ({ ...target, match })))
|
|
1387
|
+
.filter(({ note }) => source.id !== note.id && (options.mentionScope === undefined
|
|
1388
|
+
|| scopedMentionIds.has(source.id) || scopedMentionIds.has(note.id)))
|
|
1389
|
+
.sort((left, right) => left.order - right.order);
|
|
1390
|
+
let newlineOffsets: readonly number[] | undefined;
|
|
1391
|
+
for (const { note: target, match } of targets) {
|
|
1392
|
+
if (mentionPairs >= maxMentionPairs) throw new VaultAnalysisBudgetError(
|
|
1393
|
+
"mention-pairs", maxMentionPairs, `Vault analysis exceeds the ${maxMentionPairs} mention-pair limit.`,
|
|
1394
|
+
);
|
|
1395
|
+
mentionPairs += 1;
|
|
1396
|
+
if (linkedPairs.has(pairKey(source.id, target.id))) continue;
|
|
1397
|
+
if (mentions.length >= maxMentions) throw new VaultAnalysisBudgetError(
|
|
1398
|
+
"mentions", maxMentions, `Vault analysis exceeds the ${maxMentions} mention limit.`,
|
|
1399
|
+
);
|
|
1400
|
+
newlineOffsets ??= matcher.newlineOffsets(source.searchableText);
|
|
1401
|
+
mentions.push({ source: source.path, target: target.path, phrase: match.pattern.phrase,
|
|
1402
|
+
line: matcher.lineAt(newlineOffsets, match.offset) });
|
|
1403
|
+
}
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1336
1406
|
const targets = options.mentionScope === undefined
|
|
1337
1407
|
|| scopedMentionIds.has(source.id)
|
|
1338
1408
|
? suggestionNotes
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/** Finite UTF-16 multi-pattern matching for complete vault analysis. */
|
|
2
|
+
export type MentionIndexLimits = {
|
|
3
|
+
readonly maxNodes?: number;
|
|
4
|
+
readonly maxWork?: number;
|
|
5
|
+
readonly maxMatches?: number;
|
|
6
|
+
readonly maxInputCodeUnits?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const MENTION_INDEX_LIMITS = {
|
|
10
|
+
maxNodes: 262_144,
|
|
11
|
+
maxWork: 64 * 1_024 * 1_024,
|
|
12
|
+
maxMatches: 1_000_000,
|
|
13
|
+
maxInputCodeUnits: 256 * 1_024 * 1_024,
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export type MentionIndexBudgetKind =
|
|
17
|
+
| "mention-index-nodes"
|
|
18
|
+
| "mention-index-work"
|
|
19
|
+
| "mention-matches"
|
|
20
|
+
| "mention-input-code-units";
|
|
21
|
+
|
|
22
|
+
export class MentionIndexBudgetError extends RangeError {
|
|
23
|
+
readonly kind: MentionIndexBudgetKind;
|
|
24
|
+
readonly limit: number;
|
|
25
|
+
|
|
26
|
+
constructor(kind: MentionIndexBudgetKind, limit: number) {
|
|
27
|
+
super(`Vault analysis exceeds the ${limit} ${kind} limit.`);
|
|
28
|
+
this.kind = kind;
|
|
29
|
+
this.limit = limit;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function checkedLimits(options: MentionIndexLimits): Record<keyof MentionIndexLimits, number> {
|
|
34
|
+
const limits: Record<keyof MentionIndexLimits, number> = { ...MENTION_INDEX_LIMITS };
|
|
35
|
+
for (const name of Object.keys(MENTION_INDEX_LIMITS) as (keyof MentionIndexLimits)[]) {
|
|
36
|
+
const value = options[name] ?? MENTION_INDEX_LIMITS[name];
|
|
37
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MENTION_INDEX_LIMITS[name]) {
|
|
38
|
+
throw new RangeError(`${name} must be a safe integer from 0 through ${MENTION_INDEX_LIMITS[name]}.`);
|
|
39
|
+
}
|
|
40
|
+
limits[name] = value;
|
|
41
|
+
}
|
|
42
|
+
return limits;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Admit original strings before trimming, normalizing, or indexing any phrase. */
|
|
46
|
+
export function preflightMentionInput(inputs: Iterable<string>, options: MentionIndexLimits): { rawInputCodeUnits: number; work: number } {
|
|
47
|
+
const limits = checkedLimits(options);
|
|
48
|
+
let rawInputCodeUnits = 0, work = 0;
|
|
49
|
+
for (const input of inputs) {
|
|
50
|
+
rawInputCodeUnits += input.length;
|
|
51
|
+
if (rawInputCodeUnits > limits.maxInputCodeUnits) throw new MentionIndexBudgetError("mention-input-code-units", limits.maxInputCodeUnits);
|
|
52
|
+
work += input.length + 1;
|
|
53
|
+
if (work > limits.maxWork) throw new MentionIndexBudgetError("mention-index-work", limits.maxWork);
|
|
54
|
+
}
|
|
55
|
+
return { rawInputCodeUnits, work };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type MentionPattern = {
|
|
59
|
+
readonly targetId: string;
|
|
60
|
+
readonly phrase: string;
|
|
61
|
+
readonly lowerPhrase: string;
|
|
62
|
+
readonly rank: number;
|
|
63
|
+
};
|
|
64
|
+
export type IndexedMention = { readonly pattern: MentionPattern; readonly offset: number };
|
|
65
|
+
type TrieNode = {
|
|
66
|
+
readonly next: Map<number, number>;
|
|
67
|
+
failure: number;
|
|
68
|
+
output: number;
|
|
69
|
+
pattern: MentionPattern | undefined;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The legacy matcher uses ASCII word boundaries, even for Unicode phrases. */
|
|
73
|
+
export function cleanMentionBoundary(text: string, phrase: string, offset: number): boolean {
|
|
74
|
+
const word = (value: string): boolean => /[A-Za-z0-9]/.test(value);
|
|
75
|
+
return (!word(phrase[0] ?? "") || !word(text[offset - 1] ?? ""))
|
|
76
|
+
&& (!word(phrase.at(-1) ?? "") || !word(text[offset + phrase.length] ?? ""));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class MentionMatcher {
|
|
80
|
+
private readonly limits: Record<keyof MentionIndexLimits, number>;
|
|
81
|
+
private readonly nodes: TrieNode[] = [];
|
|
82
|
+
private work = 0;
|
|
83
|
+
private matches = 0;
|
|
84
|
+
private input = 0;
|
|
85
|
+
|
|
86
|
+
constructor(patterns: readonly MentionPattern[], options: MentionIndexLimits = {}, rawInputCodeUnits = 0, preflightWork = 0) {
|
|
87
|
+
this.limits = checkedLimits(options);
|
|
88
|
+
if (!Number.isSafeInteger(preflightWork) || preflightWork < 0) throw new RangeError("Invalid mention preflight work.");
|
|
89
|
+
if (preflightWork > this.limits.maxWork) this.fail("mention-index-work", this.limits.maxWork);
|
|
90
|
+
this.work = preflightWork;
|
|
91
|
+
if (!Number.isSafeInteger(rawInputCodeUnits) || rawInputCodeUnits < 0) throw new RangeError("Invalid mention input length.");
|
|
92
|
+
if (rawInputCodeUnits > this.limits.maxInputCodeUnits) this.fail("mention-input-code-units", this.limits.maxInputCodeUnits);
|
|
93
|
+
this.node();
|
|
94
|
+
for (const pattern of patterns) {
|
|
95
|
+
this.inputUnits(pattern.lowerPhrase.length);
|
|
96
|
+
let state = 0;
|
|
97
|
+
for (let index = 0; index < pattern.lowerPhrase.length; index += 1) {
|
|
98
|
+
this.step();
|
|
99
|
+
const character = pattern.lowerPhrase.charCodeAt(index);
|
|
100
|
+
let next = this.nodes[state]!.next.get(character);
|
|
101
|
+
if (next === undefined) {
|
|
102
|
+
next = this.node();
|
|
103
|
+
this.nodes[state]!.next.set(character, next);
|
|
104
|
+
}
|
|
105
|
+
state = next;
|
|
106
|
+
}
|
|
107
|
+
if (pattern.lowerPhrase.length === 0 || this.nodes[state]!.pattern !== undefined) {
|
|
108
|
+
throw new TypeError("Mention index requires nonempty, uniquely owned normalized phrases.");
|
|
109
|
+
}
|
|
110
|
+
this.nodes[state]!.pattern = pattern;
|
|
111
|
+
}
|
|
112
|
+
const queue = [...this.nodes[0]!.next.values()];
|
|
113
|
+
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
114
|
+
const state = queue[cursor]!;
|
|
115
|
+
for (const [character, child] of this.nodes[state]!.next) {
|
|
116
|
+
this.step();
|
|
117
|
+
let fallback = this.nodes[state]!.failure;
|
|
118
|
+
while (fallback !== 0 && !this.nodes[fallback]!.next.has(character)) {
|
|
119
|
+
this.step();
|
|
120
|
+
fallback = this.nodes[fallback]!.failure;
|
|
121
|
+
}
|
|
122
|
+
const failure = this.nodes[fallback]!.next.get(character) ?? 0;
|
|
123
|
+
this.nodes[child]!.failure = failure;
|
|
124
|
+
this.nodes[child]!.output = this.nodes[failure]!.pattern === undefined
|
|
125
|
+
? this.nodes[failure]!.output : failure;
|
|
126
|
+
queue.push(child);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private fail(kind: MentionIndexBudgetKind, limit: number): never { throw new MentionIndexBudgetError(kind, limit); }
|
|
132
|
+
private step(): void {
|
|
133
|
+
if (this.work >= this.limits.maxWork) this.fail("mention-index-work", this.limits.maxWork);
|
|
134
|
+
this.work += 1;
|
|
135
|
+
}
|
|
136
|
+
private node(): number {
|
|
137
|
+
if (this.nodes.length >= this.limits.maxNodes) this.fail("mention-index-nodes", this.limits.maxNodes);
|
|
138
|
+
this.nodes.push({ next: new Map(), failure: 0, output: -1, pattern: undefined });
|
|
139
|
+
return this.nodes.length - 1;
|
|
140
|
+
}
|
|
141
|
+
private inputUnits(count: number): void {
|
|
142
|
+
this.input += count;
|
|
143
|
+
if (this.input > this.limits.maxInputCodeUnits) this.fail("mention-input-code-units", this.limits.maxInputCodeUnits);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
newlineOffsets(originalText: string): readonly number[] {
|
|
147
|
+
const offsets: number[] = [];
|
|
148
|
+
for (let index = 0; index < originalText.length; index += 1) {
|
|
149
|
+
this.step();
|
|
150
|
+
if (originalText.charCodeAt(index) === 10) offsets.push(index);
|
|
151
|
+
}
|
|
152
|
+
return offsets;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
lineAt(offsets: readonly number[], offset: number): number {
|
|
156
|
+
let low = 0, high = offsets.length;
|
|
157
|
+
while (low < high) {
|
|
158
|
+
this.step();
|
|
159
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
160
|
+
if (offsets[middle]! < offset) low = middle + 1;
|
|
161
|
+
else high = middle;
|
|
162
|
+
}
|
|
163
|
+
return low + 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
scan(lowerText: string): ReadonlyMap<string, IndexedMention> {
|
|
167
|
+
this.inputUnits(lowerText.length);
|
|
168
|
+
const result = new Map<string, IndexedMention>();
|
|
169
|
+
let state = 0;
|
|
170
|
+
for (let index = 0; index < lowerText.length; index += 1) {
|
|
171
|
+
this.step();
|
|
172
|
+
const character = lowerText.charCodeAt(index);
|
|
173
|
+
while (state !== 0 && !this.nodes[state]!.next.has(character)) {
|
|
174
|
+
this.step();
|
|
175
|
+
state = this.nodes[state]!.failure;
|
|
176
|
+
}
|
|
177
|
+
state = this.nodes[state]!.next.get(character) ?? 0;
|
|
178
|
+
let output = this.nodes[state]!.pattern === undefined ? this.nodes[state]!.output : state;
|
|
179
|
+
while (output !== -1) {
|
|
180
|
+
if (this.matches >= this.limits.maxMatches) this.fail("mention-matches", this.limits.maxMatches);
|
|
181
|
+
this.matches += 1; // Count overlapping/repeated outputs before deduplication or boundary filtering.
|
|
182
|
+
const pattern = this.nodes[output]!.pattern!;
|
|
183
|
+
const offset = index + 1 - pattern.lowerPhrase.length;
|
|
184
|
+
const existing = result.get(pattern.targetId);
|
|
185
|
+
if (cleanMentionBoundary(lowerText, pattern.lowerPhrase, offset)
|
|
186
|
+
&& (existing === undefined || pattern.rank < existing.pattern.rank)) {
|
|
187
|
+
result.set(pattern.targetId, { pattern, offset });
|
|
188
|
+
}
|
|
189
|
+
output = this.nodes[output]!.output;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
}
|
package/src/vault.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
analyzeVault,
|
|
16
|
+
analyzeVaultComplete,
|
|
16
17
|
isCanonicalNoteId,
|
|
17
18
|
lookupNote,
|
|
18
19
|
MAX_ANALYZED_NOTES,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
renderCatalog,
|
|
22
23
|
replaceCatalog,
|
|
23
24
|
type AnalyzeVaultOptions,
|
|
25
|
+
type CompleteAnalyzeVaultOptions,
|
|
24
26
|
type Note,
|
|
25
27
|
type VaultAnalysis,
|
|
26
28
|
} from "./graph.js";
|
|
@@ -90,6 +92,8 @@ export type ScanVaultOptions = Omit<AnalyzeVaultOptions, "mentionScope"> & {
|
|
|
90
92
|
readonly mentionScope?: string | false;
|
|
91
93
|
};
|
|
92
94
|
|
|
95
|
+
export type CompleteScanVaultOptions = ScanVaultOptions & Pick<CompleteAnalyzeVaultOptions, "mentionIndexLimits">;
|
|
96
|
+
|
|
93
97
|
export async function markdownFiles(
|
|
94
98
|
directory: string,
|
|
95
99
|
ignoredDirectories: ReadonlySet<string> = defaultIgnoredDirectories,
|
|
@@ -520,8 +524,9 @@ async function atomicReplace(
|
|
|
520
524
|
|
|
521
525
|
async function snapshot(
|
|
522
526
|
rootInput: string,
|
|
523
|
-
options:
|
|
527
|
+
options: CompleteScanVaultOptions,
|
|
524
528
|
writeIndex: boolean,
|
|
529
|
+
completeMentions = false,
|
|
525
530
|
): Promise<VaultSnapshot> {
|
|
526
531
|
const requestedRoot = resolve(rootInput);
|
|
527
532
|
const root = await realpath(requestedRoot);
|
|
@@ -613,7 +618,7 @@ async function snapshot(
|
|
|
613
618
|
catalogMode,
|
|
614
619
|
index,
|
|
615
620
|
notes,
|
|
616
|
-
analysis: analyzeVault(notes, {
|
|
621
|
+
analysis: (completeMentions ? analyzeVaultComplete : analyzeVault)(notes, {
|
|
617
622
|
catalogNoteId,
|
|
618
623
|
...(options.includeInSuggestions === undefined
|
|
619
624
|
? {}
|
|
@@ -631,6 +636,9 @@ async function snapshot(
|
|
|
631
636
|
...(options.maxMentions === undefined
|
|
632
637
|
? {}
|
|
633
638
|
: { maxMentions: options.maxMentions }),
|
|
639
|
+
...(!completeMentions || options.mentionIndexLimits === undefined
|
|
640
|
+
? {}
|
|
641
|
+
: { mentionIndexLimits: options.mentionIndexLimits }),
|
|
634
642
|
}),
|
|
635
643
|
};
|
|
636
644
|
}
|
|
@@ -648,3 +656,19 @@ export async function refreshVault(
|
|
|
648
656
|
): Promise<VaultSnapshot> {
|
|
649
657
|
return snapshot(root, options, true);
|
|
650
658
|
}
|
|
659
|
+
|
|
660
|
+
/** Read every admitted note and compute the full graph through the phrase index. */
|
|
661
|
+
export async function scanVaultComplete(
|
|
662
|
+
root = ".",
|
|
663
|
+
options: CompleteScanVaultOptions = {},
|
|
664
|
+
): Promise<VaultSnapshot> {
|
|
665
|
+
return snapshot(root, options, false, true);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Refresh the configured catalog while retaining complete indexed analysis. */
|
|
669
|
+
export async function refreshVaultComplete(
|
|
670
|
+
root = ".",
|
|
671
|
+
options: CompleteScanVaultOptions = {},
|
|
672
|
+
): Promise<VaultSnapshot> {
|
|
673
|
+
return snapshot(root, options, true, true);
|
|
674
|
+
}
|