oh-my-second-brain 0.1.9 → 0.2.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/CHANGELOG.md +89 -0
- package/adapters/claude-code/.claude-plugin/plugin.json +1 -1
- package/adapters/claude-code/skills/link/SKILL.md +87 -0
- package/adapters/codex/.codex-plugin/plugin.json +1 -1
- package/adapters/codex/skills/oms-link/SKILL.md +35 -0
- package/adapters/hermes/README.md +2 -0
- package/adapters/hermes/manifest.json +1 -1
- package/adapters/hermes/skills/link/SKILL.md +18 -0
- package/core/ontology/concepts/term.yaml +12 -0
- package/core/ontology/taxonomy.yaml +3 -0
- package/core/skills/link/SKILL.md +130 -0
- package/dist/capture/safe.d.ts +32 -2
- package/dist/capture/safe.js +223 -37
- package/dist/capture/safe.js.map +1 -1
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.js +6 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/global-writeback.d.ts +31 -0
- package/dist/cli/global-writeback.js +73 -0
- package/dist/cli/global-writeback.js.map +1 -0
- package/dist/cli/host-commands.d.ts +30 -0
- package/dist/cli/host-commands.js +89 -0
- package/dist/cli/host-commands.js.map +1 -0
- package/dist/cli/linkify.d.ts +23 -0
- package/dist/cli/linkify.js +118 -0
- package/dist/cli/linkify.js.map +1 -0
- package/dist/cli/oms.d.ts +1 -0
- package/dist/cli/oms.js +52 -66
- package/dist/cli/oms.js.map +1 -1
- package/dist/cli/usage.js +11 -1
- package/dist/cli/usage.js.map +1 -1
- package/dist/conventions/write-protocol.d.ts +39 -0
- package/dist/conventions/write-protocol.js +44 -0
- package/dist/conventions/write-protocol.js.map +1 -0
- package/dist/core/runtime/assets.d.ts +6 -0
- package/dist/core/runtime/assets.js +27 -0
- package/dist/core/runtime/assets.js.map +1 -1
- package/dist/engine/graph/resolver.d.ts +17 -0
- package/dist/engine/graph/resolver.js +46 -11
- package/dist/engine/graph/resolver.js.map +1 -1
- package/dist/engine/linkify/apply.d.ts +56 -0
- package/dist/engine/linkify/apply.js +65 -0
- package/dist/engine/linkify/apply.js.map +1 -0
- package/dist/engine/linkify/josa.d.ts +58 -0
- package/dist/engine/linkify/josa.js +133 -0
- package/dist/engine/linkify/josa.js.map +1 -0
- package/dist/engine/linkify/mask.d.ts +29 -0
- package/dist/engine/linkify/mask.js +173 -0
- package/dist/engine/linkify/mask.js.map +1 -0
- package/dist/engine/linkify/suggest.d.ts +52 -0
- package/dist/engine/linkify/suggest.js +181 -0
- package/dist/engine/linkify/suggest.js.map +1 -0
- package/dist/engine/linkify/types.d.ts +68 -0
- package/dist/engine/linkify/types.js +12 -0
- package/dist/engine/linkify/types.js.map +1 -0
- package/dist/harness/surface-registry.d.ts +1 -0
- package/dist/harness/surface-registry.js +27 -0
- package/dist/harness/surface-registry.js.map +1 -1
- package/dist/harness/validation.js +30 -1
- package/dist/harness/validation.js.map +1 -1
- package/dist/install/claude-marketplace.d.ts +44 -0
- package/dist/install/claude-marketplace.js +89 -0
- package/dist/install/claude-marketplace.js.map +1 -0
- package/dist/install/claude.js +14 -7
- package/dist/install/claude.js.map +1 -1
- package/dist/install/codex.d.ts +15 -0
- package/dist/install/codex.js +15 -0
- package/dist/install/codex.js.map +1 -1
- package/dist/install/common.d.ts +11 -2
- package/dist/install/common.js +19 -12
- package/dist/install/common.js.map +1 -1
- package/dist/install/hermes.js +15 -15
- package/dist/install/hermes.js.map +1 -1
- package/dist/install/hosts.js +31 -15
- package/dist/install/hosts.js.map +1 -1
- package/dist/link/global-config.d.ts +22 -0
- package/dist/link/global-config.js +69 -0
- package/dist/link/global-config.js.map +1 -0
- package/dist/link/link.d.ts +6 -3
- package/dist/link/link.js +12 -2
- package/dist/link/link.js.map +1 -1
- package/dist/mcp/link-tools.d.ts +81 -0
- package/dist/mcp/link-tools.js +157 -0
- package/dist/mcp/link-tools.js.map +1 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +107 -8
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/update-notice.d.ts +51 -0
- package/dist/mcp/update-notice.js +196 -0
- package/dist/mcp/update-notice.js.map +1 -0
- package/dist/update/update.d.ts +12 -0
- package/dist/update/update.js +2 -2
- package/dist/update/update.js.map +1 -1
- package/docs/install.md +38 -2
- package/docs/release.md +109 -17
- package/package.json +4 -2
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Body masking: split a note body into regions the linkifier may rewrite and
|
|
3
|
+
* regions it must leave byte-for-byte alone.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: inserting `[[…]]` into a code fence, a URL, an existing
|
|
6
|
+
* wikilink, or a heading corrupts the note. The span detector therefore never
|
|
7
|
+
* sees the raw body — it sees only the FREE spans this module returns.
|
|
8
|
+
*
|
|
9
|
+
* Design: one collector pass per construct family, then a sort-and-merge into
|
|
10
|
+
* disjoint ascending ranges, then the complement. Merging (rather than
|
|
11
|
+
* precedence rules) is what makes overlapping constructs — a URL inside a
|
|
12
|
+
* markdown link target, a wikilink inside a heading — safe: the union is
|
|
13
|
+
* protected and nothing is double-counted. Malformed input degrades toward MORE
|
|
14
|
+
* protection (an unterminated fence protects to end of body), never less.
|
|
15
|
+
*
|
|
16
|
+
* Pure module: no I/O, no state. Offsets are UTF-16 indices into the input.
|
|
17
|
+
*/
|
|
18
|
+
import type { MaskResult } from "./types.js";
|
|
19
|
+
/**
|
|
20
|
+
* Partition `body` into protected and free spans.
|
|
21
|
+
*
|
|
22
|
+
* Together the two lists tile `[0, body.length)` exactly once, ascending, with
|
|
23
|
+
* no zero-length entries. Callers scan `freeSpans` and never touch the rest.
|
|
24
|
+
*/
|
|
25
|
+
export declare function maskBody(body: string): MaskResult;
|
|
26
|
+
/** True when `offset` falls inside any protected span. */
|
|
27
|
+
export declare function isProtected(mask: MaskResult, offset: number): boolean;
|
|
28
|
+
/** The body slices the span detector is allowed to scan, in document order. */
|
|
29
|
+
export declare function freeText(body: string, mask: MaskResult): string[];
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Body masking: split a note body into regions the linkifier may rewrite and
|
|
3
|
+
* regions it must leave byte-for-byte alone.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: inserting `[[…]]` into a code fence, a URL, an existing
|
|
6
|
+
* wikilink, or a heading corrupts the note. The span detector therefore never
|
|
7
|
+
* sees the raw body — it sees only the FREE spans this module returns.
|
|
8
|
+
*
|
|
9
|
+
* Design: one collector pass per construct family, then a sort-and-merge into
|
|
10
|
+
* disjoint ascending ranges, then the complement. Merging (rather than
|
|
11
|
+
* precedence rules) is what makes overlapping constructs — a URL inside a
|
|
12
|
+
* markdown link target, a wikilink inside a heading — safe: the union is
|
|
13
|
+
* protected and nothing is double-counted. Malformed input degrades toward MORE
|
|
14
|
+
* protection (an unterminated fence protects to end of body), never less.
|
|
15
|
+
*
|
|
16
|
+
* Pure module: no I/O, no state. Offsets are UTF-16 indices into the input.
|
|
17
|
+
*/
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Collectors — each returns raw (possibly overlapping) protected ranges
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/** Leading `---\n … \n---` YAML block only; a mid-body `---` is a thematic break. */
|
|
22
|
+
function frontmatterSpan(body) {
|
|
23
|
+
if (!body.startsWith("---\n") && !body.startsWith("---\r\n"))
|
|
24
|
+
return [];
|
|
25
|
+
const close = /^---[ \t]*\r?$/m.exec(body.slice(3));
|
|
26
|
+
if (!close || close.index === undefined)
|
|
27
|
+
return [];
|
|
28
|
+
return [{ start: 0, end: 3 + close.index + close[0].length, kind: "frontmatter" }];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Fenced code blocks (``` or ~~~), including the fence lines themselves.
|
|
32
|
+
* An unterminated fence protects everything to the end of the body.
|
|
33
|
+
*/
|
|
34
|
+
function fencedCodeSpans(body) {
|
|
35
|
+
const spans = [];
|
|
36
|
+
const fence = /^[ \t]*(`{3,}|~{3,})[^\n]*$/gm;
|
|
37
|
+
let match;
|
|
38
|
+
while ((match = fence.exec(body)) !== null) {
|
|
39
|
+
const marker = match[1];
|
|
40
|
+
if (marker === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
const opening = match.index;
|
|
43
|
+
const closer = new RegExp(`^[ \\t]*${marker[0] === "\`" ? "`" : "~"}{${String(marker.length)},}[ \\t]*$`, "m");
|
|
44
|
+
const rest = body.slice(fence.lastIndex);
|
|
45
|
+
const closeMatch = closer.exec(rest);
|
|
46
|
+
const end = closeMatch && closeMatch.index !== undefined
|
|
47
|
+
? fence.lastIndex + closeMatch.index + closeMatch[0].length
|
|
48
|
+
: body.length;
|
|
49
|
+
spans.push({ start: opening, end, kind: "fenced-code" });
|
|
50
|
+
fence.lastIndex = end;
|
|
51
|
+
}
|
|
52
|
+
return spans;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Inline code spans: a run of N backticks closed by another run of exactly N
|
|
56
|
+
* on the same logical block. An unclosed run protects nothing extra — the
|
|
57
|
+
* fenced-code pass already covers the block-level case.
|
|
58
|
+
*/
|
|
59
|
+
function inlineCodeSpans(body) {
|
|
60
|
+
const spans = [];
|
|
61
|
+
const run = /`+/g;
|
|
62
|
+
let match;
|
|
63
|
+
while ((match = run.exec(body)) !== null) {
|
|
64
|
+
const ticks = match[0];
|
|
65
|
+
const closeIndex = body.indexOf(ticks, match.index + ticks.length);
|
|
66
|
+
if (closeIndex < 0)
|
|
67
|
+
continue;
|
|
68
|
+
const inner = body.slice(match.index + ticks.length, closeIndex);
|
|
69
|
+
if (inner.includes("\n\n"))
|
|
70
|
+
continue;
|
|
71
|
+
const end = closeIndex + ticks.length;
|
|
72
|
+
spans.push({ start: match.index, end, kind: "inline-code" });
|
|
73
|
+
run.lastIndex = end;
|
|
74
|
+
}
|
|
75
|
+
return spans;
|
|
76
|
+
}
|
|
77
|
+
/** Regex-driven collectors that need no cursor bookkeeping. */
|
|
78
|
+
const SIMPLE_PATTERNS = [
|
|
79
|
+
// image embeds first so their `!` prefix is inside the span, not left free
|
|
80
|
+
{ kind: "image-embed", pattern: /!\[\[[^\]\n]*\]\]/g },
|
|
81
|
+
{ kind: "image-embed", pattern: /!\[[^\]\n]*\]\([^)\n]*\)/g },
|
|
82
|
+
{ kind: "wikilink", pattern: /\[\[[^\]\n]*\]\]/g },
|
|
83
|
+
{ kind: "markdown-link", pattern: /\[[^\]\n]*\]\([^)\n]*\)/g },
|
|
84
|
+
{ kind: "html", pattern: /<\/?[A-Za-z][^<>\n]*>/g },
|
|
85
|
+
{ kind: "heading", pattern: /^[ \t]{0,3}#{1,6}[ \t][^\n]*$/gm },
|
|
86
|
+
{ kind: "heading", pattern: /^[^\n]+\r?\n[ \t]{0,3}(?:=+|-{2,})[ \t]*$/gm },
|
|
87
|
+
{ kind: "url", pattern: /\b(?:https?|ftp|obsidian|mailto):[^\s<>()[\]]+/g },
|
|
88
|
+
{ kind: "block-id", pattern: /(?<=\s)\^[A-Za-z0-9-]+[ \t]*$/gm },
|
|
89
|
+
{ kind: "tag", pattern: /(?<=^|\s)#[\p{L}\p{N}][\p{L}\p{N}/_-]*/gu },
|
|
90
|
+
];
|
|
91
|
+
function simpleSpans(body) {
|
|
92
|
+
const spans = [];
|
|
93
|
+
for (const { kind, pattern } of SIMPLE_PATTERNS) {
|
|
94
|
+
const scanner = new RegExp(pattern.source, pattern.flags);
|
|
95
|
+
let match;
|
|
96
|
+
while ((match = scanner.exec(body)) !== null) {
|
|
97
|
+
if (match[0].length < 1) {
|
|
98
|
+
scanner.lastIndex += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
spans.push({ start: match.index, end: match.index + match[0].length, kind });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return spans;
|
|
105
|
+
}
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Merge + complement
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
/**
|
|
110
|
+
* Sort by start (earlier wins, longer wins on tie) and fuse overlaps into one
|
|
111
|
+
* span carrying the first contributor's kind — the union is what matters to the
|
|
112
|
+
* detector; the kind is reviewer-facing provenance.
|
|
113
|
+
*/
|
|
114
|
+
function mergeSpans(raw) {
|
|
115
|
+
const sorted = raw
|
|
116
|
+
.filter((s) => s.end > s.start)
|
|
117
|
+
.slice()
|
|
118
|
+
.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
119
|
+
const merged = [];
|
|
120
|
+
for (const span of sorted) {
|
|
121
|
+
const last = merged[merged.length - 1];
|
|
122
|
+
if (last !== undefined && span.start <= last.end) {
|
|
123
|
+
if (span.end > last.end) {
|
|
124
|
+
merged[merged.length - 1] = { start: last.start, end: span.end, kind: last.kind };
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
merged.push(span);
|
|
129
|
+
}
|
|
130
|
+
return merged;
|
|
131
|
+
}
|
|
132
|
+
/** The complement of `protectedSpans` within `[0, length)`. */
|
|
133
|
+
function complement(protectedSpans, length) {
|
|
134
|
+
const free = [];
|
|
135
|
+
let cursor = 0;
|
|
136
|
+
for (const span of protectedSpans) {
|
|
137
|
+
if (span.start > cursor)
|
|
138
|
+
free.push({ start: cursor, end: span.start });
|
|
139
|
+
cursor = span.end;
|
|
140
|
+
}
|
|
141
|
+
if (cursor < length)
|
|
142
|
+
free.push({ start: cursor, end: length });
|
|
143
|
+
return free;
|
|
144
|
+
}
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Public API
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
/**
|
|
149
|
+
* Partition `body` into protected and free spans.
|
|
150
|
+
*
|
|
151
|
+
* Together the two lists tile `[0, body.length)` exactly once, ascending, with
|
|
152
|
+
* no zero-length entries. Callers scan `freeSpans` and never touch the rest.
|
|
153
|
+
*/
|
|
154
|
+
export function maskBody(body) {
|
|
155
|
+
if (body.length < 1)
|
|
156
|
+
return { protectedSpans: [], freeSpans: [] };
|
|
157
|
+
const protectedSpans = mergeSpans([
|
|
158
|
+
...frontmatterSpan(body),
|
|
159
|
+
...fencedCodeSpans(body),
|
|
160
|
+
...inlineCodeSpans(body),
|
|
161
|
+
...simpleSpans(body),
|
|
162
|
+
]);
|
|
163
|
+
return { protectedSpans, freeSpans: complement(protectedSpans, body.length) };
|
|
164
|
+
}
|
|
165
|
+
/** True when `offset` falls inside any protected span. */
|
|
166
|
+
export function isProtected(mask, offset) {
|
|
167
|
+
return mask.protectedSpans.some((span) => span.start <= offset && offset < span.end);
|
|
168
|
+
}
|
|
169
|
+
/** The body slices the span detector is allowed to scan, in document order. */
|
|
170
|
+
export function freeText(body, mask) {
|
|
171
|
+
return mask.freeSpans.map((span) => body.slice(span.start, span.end));
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=mask.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mask.js","sourceRoot":"","sources":["../../../src/engine/linkify/mask.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Span detection: propose `[[wikilink]]` insertions for a note body.
|
|
3
|
+
*
|
|
4
|
+
* The pipeline is surface-anchored by construction. A candidate exists ONLY
|
|
5
|
+
* because a term's basename or frontmatter alias literally appears in a FREE
|
|
6
|
+
* region of the body (see mask.ts). Retrieval signals never create candidates:
|
|
7
|
+
* an injected lexical ranker may only reorder what the surface already found,
|
|
8
|
+
* and an injected vector predicate may only veto. This is what keeps the
|
|
9
|
+
* feature from hallucinating links into a user's vault.
|
|
10
|
+
*
|
|
11
|
+
* Two rules encode judgement the machine must not make on its own:
|
|
12
|
+
* - FIRST occurrence per target note only — a term linked ten times is noise.
|
|
13
|
+
* - Ambiguity (2+ notes claiming one span) is REPORTED, never resolved. The
|
|
14
|
+
* resolver's shortest-path tie-break is right for reading an existing link
|
|
15
|
+
* and wrong for creating a new one.
|
|
16
|
+
*
|
|
17
|
+
* Pure given its injected dependencies: no I/O, no globals, no clock.
|
|
18
|
+
*/
|
|
19
|
+
import type { LinkCandidate, TermNote } from "./types.js";
|
|
20
|
+
/** The ontology concept whose notes are the default link universe. */
|
|
21
|
+
export declare const TERM_CONCEPT = "term";
|
|
22
|
+
/** Minimal shape of `EngineStore.queryLex` results — only what ranking needs. */
|
|
23
|
+
export interface LexHit {
|
|
24
|
+
readonly docPath: string;
|
|
25
|
+
readonly score: number;
|
|
26
|
+
}
|
|
27
|
+
/** Options for {@link suggestLinks}; every dependency is optional and injected. */
|
|
28
|
+
export interface SuggestOptions {
|
|
29
|
+
/** Vault-relative path of the note being linkified; never linked to itself. */
|
|
30
|
+
readonly notePath: string;
|
|
31
|
+
/** Optional lexical re-ranker. Reorders surface-anchored candidates only. */
|
|
32
|
+
readonly queryLex?: (text: string, k: number) => readonly LexHit[];
|
|
33
|
+
/** Optional semantic veto, honoured only when `embeddingConfigured` is true. */
|
|
34
|
+
readonly vectorVeto?: (candidate: LinkCandidate) => boolean;
|
|
35
|
+
/** ADR-007 gate; when false the vector veto is inert. Defaults to true. */
|
|
36
|
+
readonly embeddingConfigured?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Restrict a candidate set to notes bound to the `term` concept.
|
|
40
|
+
*
|
|
41
|
+
* Offered as a helper rather than applied inside {@link suggestLinks} so the
|
|
42
|
+
* caller owns the universe decision (a vault may want a wider set).
|
|
43
|
+
*/
|
|
44
|
+
export declare function termBoundNotes(notes: readonly TermNote[]): TermNote[];
|
|
45
|
+
/**
|
|
46
|
+
* Propose link candidates for `body` against `notes`.
|
|
47
|
+
*
|
|
48
|
+
* Candidates are returned ranked (highest confidence first, earliest offset
|
|
49
|
+
* breaking ties) and are guaranteed to be non-overlapping, first-occurrence
|
|
50
|
+
* only, outside every protected region, and never self-referential.
|
|
51
|
+
*/
|
|
52
|
+
export declare function suggestLinks(body: string, notes: readonly TermNote[], options: SuggestOptions): LinkCandidate[];
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Span detection: propose `[[wikilink]]` insertions for a note body.
|
|
3
|
+
*
|
|
4
|
+
* The pipeline is surface-anchored by construction. A candidate exists ONLY
|
|
5
|
+
* because a term's basename or frontmatter alias literally appears in a FREE
|
|
6
|
+
* region of the body (see mask.ts). Retrieval signals never create candidates:
|
|
7
|
+
* an injected lexical ranker may only reorder what the surface already found,
|
|
8
|
+
* and an injected vector predicate may only veto. This is what keeps the
|
|
9
|
+
* feature from hallucinating links into a user's vault.
|
|
10
|
+
*
|
|
11
|
+
* Two rules encode judgement the machine must not make on its own:
|
|
12
|
+
* - FIRST occurrence per target note only — a term linked ten times is noise.
|
|
13
|
+
* - Ambiguity (2+ notes claiming one span) is REPORTED, never resolved. The
|
|
14
|
+
* resolver's shortest-path tie-break is right for reading an existing link
|
|
15
|
+
* and wrong for creating a new one.
|
|
16
|
+
*
|
|
17
|
+
* Pure given its injected dependencies: no I/O, no globals, no clock.
|
|
18
|
+
*/
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { matchTermInToken } from "./josa.js";
|
|
21
|
+
import { maskBody } from "./mask.js";
|
|
22
|
+
/** The ontology concept whose notes are the default link universe. */
|
|
23
|
+
export const TERM_CONCEPT = "term";
|
|
24
|
+
const BASENAME_CONFIDENCE = 0.9;
|
|
25
|
+
const ALIAS_CONFIDENCE = 0.6;
|
|
26
|
+
const TOKEN_PATTERN = /[\p{L}\p{N}][\p{L}\p{N}_'-]*/gu;
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Candidate universe
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Restrict a candidate set to notes bound to the `term` concept.
|
|
32
|
+
*
|
|
33
|
+
* Offered as a helper rather than applied inside {@link suggestLinks} so the
|
|
34
|
+
* caller owns the universe decision (a vault may want a wider set).
|
|
35
|
+
*/
|
|
36
|
+
export function termBoundNotes(notes) {
|
|
37
|
+
return notes.filter((note) => note.concept === TERM_CONCEPT);
|
|
38
|
+
}
|
|
39
|
+
/** Basename + alias surfaces of a note, longest first so the greediest wins. */
|
|
40
|
+
function surfacesOf(note) {
|
|
41
|
+
const basename = path.basename(note.path, ".md");
|
|
42
|
+
const surfaces = [{ note, text: basename, source: "basename" }];
|
|
43
|
+
for (const alias of note.aliases) {
|
|
44
|
+
const trimmed = alias.trim();
|
|
45
|
+
if (trimmed.length > 0)
|
|
46
|
+
surfaces.push({ note, text: trimmed, source: "alias" });
|
|
47
|
+
}
|
|
48
|
+
return surfaces.sort((a, b) => b.text.length - a.text.length);
|
|
49
|
+
}
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Detection over free spans
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
/**
|
|
54
|
+
* Locate every surface hit inside one free span, via the josa-aware matcher.
|
|
55
|
+
*
|
|
56
|
+
* At most one hit per NOTE per token (its longest matching surface), but every
|
|
57
|
+
* note that claims the token is recorded — that is precisely the signal the
|
|
58
|
+
* ambiguity check downstream needs.
|
|
59
|
+
*/
|
|
60
|
+
function hitsInSpan(body, span, surfaces) {
|
|
61
|
+
const text = body.slice(span.start, span.end);
|
|
62
|
+
const hits = [];
|
|
63
|
+
for (const match of text.matchAll(TOKEN_PATTERN)) {
|
|
64
|
+
const token = match[0];
|
|
65
|
+
const tokenStart = match.index;
|
|
66
|
+
if (tokenStart === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
const claimed = new Set();
|
|
69
|
+
for (const surface of surfaces) {
|
|
70
|
+
if (claimed.has(surface.note.path))
|
|
71
|
+
continue;
|
|
72
|
+
const result = matchTermInToken(surface.text, token);
|
|
73
|
+
if (result.kind === "no_match")
|
|
74
|
+
continue;
|
|
75
|
+
claimed.add(surface.note.path);
|
|
76
|
+
hits.push({
|
|
77
|
+
start: span.start + tokenStart + result.stemStart,
|
|
78
|
+
end: span.start + tokenStart + result.stemEnd,
|
|
79
|
+
matchedText: token.slice(result.stemStart, result.stemEnd),
|
|
80
|
+
surface,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return hits;
|
|
85
|
+
}
|
|
86
|
+
/** Render the replacement, aliasing whenever the surface differs from the target. */
|
|
87
|
+
function render(matchedText, note) {
|
|
88
|
+
const basename = path.basename(note.path, ".md");
|
|
89
|
+
return matchedText === basename ? `[[${basename}]]` : `[[${basename}|${matchedText}]]`;
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Grouping, ranking
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
/** Fold hits sharing one offset span into a single (possibly ambiguous) candidate. */
|
|
95
|
+
function groupBySpan(hits) {
|
|
96
|
+
const bySpan = new Map();
|
|
97
|
+
for (const hit of hits) {
|
|
98
|
+
const key = `${String(hit.start)}:${String(hit.end)}`;
|
|
99
|
+
const bucket = bySpan.get(key) ?? [];
|
|
100
|
+
bucket.push(hit);
|
|
101
|
+
bySpan.set(key, bucket);
|
|
102
|
+
}
|
|
103
|
+
return [...bySpan.values()].map((bucket) => {
|
|
104
|
+
const rivalPaths = [...new Set(bucket.map((hit) => hit.surface.note.path))];
|
|
105
|
+
const best = bucket.reduce((a, b) => (confidenceOf(a) >= confidenceOf(b) ? a : b));
|
|
106
|
+
return {
|
|
107
|
+
startOffset: best.start,
|
|
108
|
+
endOffset: best.end,
|
|
109
|
+
matchedText: best.matchedText,
|
|
110
|
+
targetPath: best.surface.note.path,
|
|
111
|
+
renderedReplacement: render(best.matchedText, best.surface.note),
|
|
112
|
+
source: best.surface.source,
|
|
113
|
+
confidence: confidenceOf(best),
|
|
114
|
+
ambiguous: rivalPaths.length > 1,
|
|
115
|
+
rivalPaths,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function confidenceOf(hit) {
|
|
120
|
+
return hit.surface.source === "basename" ? BASENAME_CONFIDENCE : ALIAS_CONFIDENCE;
|
|
121
|
+
}
|
|
122
|
+
/** Keep the earliest candidate per target note; later mentions are noise. */
|
|
123
|
+
function firstPerTarget(candidates) {
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
return candidates
|
|
126
|
+
.slice()
|
|
127
|
+
.sort((a, b) => a.startOffset - b.startOffset)
|
|
128
|
+
.filter((candidate) => {
|
|
129
|
+
if (seen.has(candidate.targetPath))
|
|
130
|
+
return false;
|
|
131
|
+
seen.add(candidate.targetPath);
|
|
132
|
+
return true;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Additive lexical boost, normalised to (0, 0.1]: strong enough to reorder ties
|
|
137
|
+
* between candidates of the same source class, too small to lift an alias hit
|
|
138
|
+
* above an exact basename hit.
|
|
139
|
+
*/
|
|
140
|
+
function lexBoosts(body, options) {
|
|
141
|
+
const lex = options.queryLex;
|
|
142
|
+
if (lex === undefined)
|
|
143
|
+
return new Map();
|
|
144
|
+
const hits = lex(body, 20);
|
|
145
|
+
const top = Math.max(...hits.map((hit) => hit.score), 1);
|
|
146
|
+
return new Map(hits.map((hit) => [hit.docPath, (hit.score / top) * 0.1]));
|
|
147
|
+
}
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Public API
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
/**
|
|
152
|
+
* Propose link candidates for `body` against `notes`.
|
|
153
|
+
*
|
|
154
|
+
* Candidates are returned ranked (highest confidence first, earliest offset
|
|
155
|
+
* breaking ties) and are guaranteed to be non-overlapping, first-occurrence
|
|
156
|
+
* only, outside every protected region, and never self-referential.
|
|
157
|
+
*/
|
|
158
|
+
export function suggestLinks(body, notes, options) {
|
|
159
|
+
if (body.length < 1)
|
|
160
|
+
return [];
|
|
161
|
+
const surfaces = notes
|
|
162
|
+
.filter((note) => note.path !== options.notePath)
|
|
163
|
+
.flatMap(surfacesOf)
|
|
164
|
+
.sort((a, b) => b.text.length - a.text.length);
|
|
165
|
+
if (surfaces.length < 1)
|
|
166
|
+
return [];
|
|
167
|
+
const mask = maskBody(body);
|
|
168
|
+
const hits = mask.freeSpans.flatMap((span) => hitsInSpan(body, span, surfaces));
|
|
169
|
+
const candidates = firstPerTarget(groupBySpan(hits));
|
|
170
|
+
const boosts = lexBoosts(body, options);
|
|
171
|
+
const gateOpen = options.embeddingConfigured ?? true;
|
|
172
|
+
const veto = gateOpen ? options.vectorVeto : undefined;
|
|
173
|
+
return candidates
|
|
174
|
+
.map((candidate) => ({
|
|
175
|
+
...candidate,
|
|
176
|
+
confidence: candidate.confidence + (boosts.get(candidate.targetPath) ?? 0),
|
|
177
|
+
}))
|
|
178
|
+
.filter((candidate) => veto === undefined || !veto(candidate))
|
|
179
|
+
.sort((a, b) => b.confidence - a.confidence || a.startOffset - b.startOffset);
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=suggest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suggest.js","sourceRoot":"","sources":["../../../src/engine/linkify/suggest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared value types for the linkify core (mask → suggest → apply).
|
|
3
|
+
*
|
|
4
|
+
* Kept in one place because all three stages speak the same coordinate system:
|
|
5
|
+
* half-open UTF-16 offset ranges into the ORIGINAL note body string. Every
|
|
6
|
+
* offset any linkify module emits or consumes indexes that same string, so a
|
|
7
|
+
* caller can splice with `body.slice(start, end)` without a second pass.
|
|
8
|
+
*
|
|
9
|
+
* Pure types only: no I/O, no runtime dependencies.
|
|
10
|
+
*/
|
|
11
|
+
/** Why a body region is off-limits to the span detector. */
|
|
12
|
+
export type ProtectedKind = "frontmatter" | "fenced-code" | "inline-code" | "wikilink" | "markdown-link" | "image-embed" | "html" | "heading" | "url" | "block-id" | "tag";
|
|
13
|
+
/** A half-open `[start, end)` range of UTF-16 offsets into the note body. */
|
|
14
|
+
export interface Span {
|
|
15
|
+
readonly start: number;
|
|
16
|
+
readonly end: number;
|
|
17
|
+
}
|
|
18
|
+
/** A protected range plus the syntax that claimed it. */
|
|
19
|
+
export interface ProtectedSpan extends Span {
|
|
20
|
+
readonly kind: ProtectedKind;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The body partitioned into regions the span detector must not touch
|
|
24
|
+
* (`protectedSpans`) and the complement it may scan (`freeSpans`).
|
|
25
|
+
*
|
|
26
|
+
* Both lists are sorted by `start`, contain no zero-length entries, and never
|
|
27
|
+
* overlap each other; together they tile the whole body exactly once.
|
|
28
|
+
*/
|
|
29
|
+
export interface MaskResult {
|
|
30
|
+
readonly protectedSpans: readonly ProtectedSpan[];
|
|
31
|
+
readonly freeSpans: readonly Span[];
|
|
32
|
+
}
|
|
33
|
+
/** Which surface form of a note a candidate span matched. */
|
|
34
|
+
export type MatchSource = "basename" | "alias";
|
|
35
|
+
/**
|
|
36
|
+
* One proposed link: the body range to replace and the text to replace it with.
|
|
37
|
+
*
|
|
38
|
+
* `renderedReplacement` is the finished wikilink for the STEM only. A trailing
|
|
39
|
+
* Korean particle is left OUTSIDE both the span and the link — `아타락시아를`
|
|
40
|
+
* yields the range covering `아타락시아` and the replacement
|
|
41
|
+
* `[[Ataraxia|아타락시아]]`, so splicing keeps `를` in the prose. Apply never
|
|
42
|
+
* re-derives the text.
|
|
43
|
+
*/
|
|
44
|
+
export interface LinkCandidate {
|
|
45
|
+
readonly startOffset: number;
|
|
46
|
+
readonly endOffset: number;
|
|
47
|
+
/** Exact body slice `[startOffset, endOffset)` this candidate replaces. */
|
|
48
|
+
readonly matchedText: string;
|
|
49
|
+
/** Vault-relative path of the note the link points at. */
|
|
50
|
+
readonly targetPath: string;
|
|
51
|
+
readonly renderedReplacement: string;
|
|
52
|
+
readonly source: MatchSource;
|
|
53
|
+
/** Higher is better; ranking is total and deterministic. */
|
|
54
|
+
readonly confidence: number;
|
|
55
|
+
/** True when 2+ distinct notes claim this span — the caller must disambiguate. */
|
|
56
|
+
readonly ambiguous: boolean;
|
|
57
|
+
/** Every note path that claimed the span (length ≥ 2 exactly when ambiguous). */
|
|
58
|
+
readonly rivalPaths: readonly string[];
|
|
59
|
+
}
|
|
60
|
+
/** A link-target note: its vault path, concept binding, and surface forms. */
|
|
61
|
+
export interface TermNote {
|
|
62
|
+
/** Vault-relative path (with .md). */
|
|
63
|
+
readonly path: string;
|
|
64
|
+
/** Ontology concept this note is bound to, or null when unbound. */
|
|
65
|
+
readonly concept: string | null;
|
|
66
|
+
/** Frontmatter `aliases`, already flattened to trimmed strings. */
|
|
67
|
+
readonly aliases: readonly string[];
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared value types for the linkify core (mask → suggest → apply).
|
|
3
|
+
*
|
|
4
|
+
* Kept in one place because all three stages speak the same coordinate system:
|
|
5
|
+
* half-open UTF-16 offset ranges into the ORIGINAL note body string. Every
|
|
6
|
+
* offset any linkify module emits or consumes indexes that same string, so a
|
|
7
|
+
* caller can splice with `body.slice(start, end)` without a second pass.
|
|
8
|
+
*
|
|
9
|
+
* Pure types only: no I/O, no runtime dependencies.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/engine/linkify/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
|
@@ -41,6 +41,7 @@ export interface HarnessRuntimeAssetRoot {
|
|
|
41
41
|
}
|
|
42
42
|
export interface HarnessPackageAssetSurface {
|
|
43
43
|
readonly npmFiles: readonly string[];
|
|
44
|
+
readonly rootShippedFiles: readonly string[];
|
|
44
45
|
readonly runtimeAssetRoots: readonly HarnessRuntimeAssetRoot[];
|
|
45
46
|
readonly releaseRequiredPaths: readonly string[];
|
|
46
47
|
}
|
|
@@ -5,6 +5,7 @@ export const harnessSurfaceRegistry = {
|
|
|
5
5
|
{ name: "audit", owner: "cli", stability: "stable" },
|
|
6
6
|
{ name: "lint", owner: "cli", stability: "stable" },
|
|
7
7
|
{ name: "link", owner: "cli", stability: "experimental" },
|
|
8
|
+
{ name: "linkify", owner: "cli", stability: "experimental" },
|
|
8
9
|
{ name: "install", owner: "install", stability: "stable" },
|
|
9
10
|
{ name: "uninstall", owner: "install", stability: "stable" },
|
|
10
11
|
{ name: "update", owner: "install", stability: "stable" },
|
|
@@ -30,6 +31,7 @@ export const harnessSurfaceRegistry = {
|
|
|
30
31
|
"define",
|
|
31
32
|
"distill",
|
|
32
33
|
"doctor",
|
|
34
|
+
"link",
|
|
33
35
|
"retrieve",
|
|
34
36
|
"setup",
|
|
35
37
|
"uninstall",
|
|
@@ -220,6 +222,24 @@ export const harnessSurfaceRegistry = {
|
|
|
220
222
|
openWorld: false,
|
|
221
223
|
stability: "stable",
|
|
222
224
|
},
|
|
225
|
+
{
|
|
226
|
+
name: "oms_link_suggest",
|
|
227
|
+
owner: "retrieval",
|
|
228
|
+
posture: "read",
|
|
229
|
+
destructive: false,
|
|
230
|
+
idempotent: true,
|
|
231
|
+
openWorld: false,
|
|
232
|
+
stability: "experimental",
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "oms_link_apply",
|
|
236
|
+
owner: "capture",
|
|
237
|
+
posture: "write",
|
|
238
|
+
destructive: true,
|
|
239
|
+
idempotent: false,
|
|
240
|
+
openWorld: false,
|
|
241
|
+
stability: "experimental",
|
|
242
|
+
},
|
|
223
243
|
{
|
|
224
244
|
name: "write",
|
|
225
245
|
owner: "capture",
|
|
@@ -239,6 +259,7 @@ export const harnessSurfaceRegistry = {
|
|
|
239
259
|
"define",
|
|
240
260
|
"distill",
|
|
241
261
|
"doctor",
|
|
262
|
+
"link",
|
|
242
263
|
"retrieve",
|
|
243
264
|
"setup",
|
|
244
265
|
"uninstall",
|
|
@@ -264,6 +285,7 @@ export const harnessSurfaceRegistry = {
|
|
|
264
285
|
"oms-distill",
|
|
265
286
|
"oms-doctor",
|
|
266
287
|
"oms-install",
|
|
288
|
+
"oms-link",
|
|
267
289
|
"oms-retrieve",
|
|
268
290
|
"oms-setup",
|
|
269
291
|
"oms-uninstall",
|
|
@@ -287,6 +309,7 @@ export const harnessSurfaceRegistry = {
|
|
|
287
309
|
"distill",
|
|
288
310
|
"doctor",
|
|
289
311
|
"install",
|
|
312
|
+
"link",
|
|
290
313
|
"retrieve",
|
|
291
314
|
"setup",
|
|
292
315
|
"uninstall",
|
|
@@ -327,6 +350,10 @@ export const harnessSurfaceRegistry = {
|
|
|
327
350
|
"docs/release.md",
|
|
328
351
|
"scripts/install.sh",
|
|
329
352
|
"scripts/uninstall.sh",
|
|
353
|
+
"CHANGELOG.md",
|
|
354
|
+
],
|
|
355
|
+
rootShippedFiles: [
|
|
356
|
+
"CHANGELOG.md",
|
|
330
357
|
],
|
|
331
358
|
runtimeAssetRoots: [
|
|
332
359
|
{ id: "ontology", path: "core/ontology", owner: "core" },
|