@agfpd/iapeer-memory-core 0.2.9 → 0.3.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/package.json +1 -1
- package/src/archive.ts +1 -1
- package/src/config.ts +7 -10
- package/src/db.ts +2 -2
- package/src/embedding.ts +1 -1
- package/src/frontmatter-fill.ts +20 -55
- package/src/graph.ts +1 -1
- package/src/human-edit-detect.ts +42 -47
- package/src/index-render.ts +1 -1
- package/src/index.ts +17 -0
- package/src/indexer.ts +1 -1
- package/src/mcp-tools.ts +17 -13
- package/src/memoryd.ts +156 -198
- package/src/mode.ts +76 -0
- package/src/permanent-detect.ts +4 -82
- package/src/search.ts +34 -10
- package/src/silent-edit-detect.ts +11 -20
- package/src/taxonomy.ts +5 -15
- package/src/utils.ts +1 -1
package/src/mode.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lean §7/§7.1 — mode + per-role proactivity resolution.
|
|
3
|
+
*
|
|
4
|
+
* The toggle gates the curation OVERLAY's PROACTIVITY (notifier triggers /
|
|
5
|
+
* memoryd curation emits), NOT the existence of anything: the role peers
|
|
6
|
+
* (Index/Scriber/DreamWeaver) are ALWAYS provisioned and callable on-demand in
|
|
7
|
+
* either mode, and memoryd (страж detector + archive + projection render +
|
|
8
|
+
* /dedup) ALWAYS runs (its watcher trigger launches it — base infra, never
|
|
9
|
+
* gated). What the mode gates is whether the curation pipeline FIRES by itself.
|
|
10
|
+
*
|
|
11
|
+
* Default resolution: `IAPEER_MEMORY_MODE` ABSENT → `curated` — a host
|
|
12
|
+
* provisioned before this feature ran curated, and must never be silently
|
|
13
|
+
* flipped to lean (§10.3). A NEW install's `init` WRITES the key (default
|
|
14
|
+
* `lean`), so new hosts read lean. Per-role env overrides the preset:
|
|
15
|
+
* `IAPEER_MEMORY_PROACTIVE_{INDEX,SCRIBER,DREAMWEAVER}` = on|off (independent
|
|
16
|
+
* toggles over the preset, §7).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type MemoryMode = "lean" | "curated";
|
|
20
|
+
export type RoleSet = { index: boolean; scriber: boolean; dreamweaver: boolean };
|
|
21
|
+
|
|
22
|
+
const TRUE_TOKENS = new Set(["1", "on", "true", "yes", "y"]);
|
|
23
|
+
const FALSE_TOKENS = new Set(["0", "off", "false", "no", "n"]);
|
|
24
|
+
|
|
25
|
+
function toggle(raw: string | undefined, preset: boolean): boolean {
|
|
26
|
+
const v = (raw ?? "").trim().toLowerCase();
|
|
27
|
+
if (TRUE_TOKENS.has(v)) return true;
|
|
28
|
+
if (FALSE_TOKENS.has(v)) return false;
|
|
29
|
+
return preset; // unset / unrecognised → follow the mode preset
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveMode(env: Record<string, string | undefined> = process.env): {
|
|
33
|
+
mode: MemoryMode;
|
|
34
|
+
roles: RoleSet;
|
|
35
|
+
} {
|
|
36
|
+
const raw = (env.IAPEER_MEMORY_MODE ?? "").trim().toLowerCase();
|
|
37
|
+
// absent / "curated" / anything-not-"lean" → curated (legacy preservation).
|
|
38
|
+
const mode: MemoryMode = raw === "lean" ? "lean" : "curated";
|
|
39
|
+
const preset = mode === "curated";
|
|
40
|
+
return {
|
|
41
|
+
mode,
|
|
42
|
+
roles: {
|
|
43
|
+
index: toggle(env.IAPEER_MEMORY_PROACTIVE_INDEX, preset),
|
|
44
|
+
scriber: toggle(env.IAPEER_MEMORY_PROACTIVE_SCRIBER, preset),
|
|
45
|
+
dreamweaver: toggle(env.IAPEER_MEMORY_PROACTIVE_DREAMWEAVER, preset),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Curation proactivity derived from the role set (§7.1 matrix). The memoryd
|
|
52
|
+
* WATCHER trigger is deliberately NOT here — it ALWAYS registers (it launches
|
|
53
|
+
* memoryd; the base needs memoryd in both modes). This is exactly what the
|
|
54
|
+
* toggle gates ON TOP of the always-on watcher.
|
|
55
|
+
*/
|
|
56
|
+
export type CurationPlan = {
|
|
57
|
+
/** memoryd emits the curation event (CURATOR_TICK) — ONLY when a proactive
|
|
58
|
+
* receiver exists (scriber ∥ index). Full-lean → suppressed (the watcher
|
|
59
|
+
* then forwards nothing). */
|
|
60
|
+
emit: boolean;
|
|
61
|
+
/** The watcher's forward target, §7.1 conditional: scriber → else index →
|
|
62
|
+
* else null (no proactive receiver; placeholder, nothing is emitted). */
|
|
63
|
+
eventTarget: string | null;
|
|
64
|
+
/** Register the weekly dream-tick timer (→ dreamweaver) — decoupled from
|
|
65
|
+
* the Index (§7.1 key #2); only if dreamweaver is proactive. */
|
|
66
|
+
dream: boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function curationPlan(roles: RoleSet): CurationPlan {
|
|
70
|
+
const eventTarget = roles.scriber ? "scriber" : roles.index ? "index" : null;
|
|
71
|
+
return {
|
|
72
|
+
emit: eventTarget !== null,
|
|
73
|
+
eventTarget,
|
|
74
|
+
dream: roles.dreamweaver,
|
|
75
|
+
};
|
|
76
|
+
}
|
package/src/permanent-detect.ts
CHANGED
|
@@ -75,85 +75,7 @@ export function diffSnapshots(prev: VaultSnapshot, next: VaultSnapshot): string[
|
|
|
75
75
|
return changed.sort();
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* One detection pass: diff the current vault state against the previous
|
|
86
|
-
* snapshot. Returns the coalesced event (or null when nothing changed)
|
|
87
|
-
* plus the snapshot to carry into the next pass.
|
|
88
|
-
*/
|
|
89
|
-
export function detectPermanentChanges(opts: {
|
|
90
|
-
vault: string;
|
|
91
|
-
taxonomy: TaxonomyPreset;
|
|
92
|
-
prev: VaultSnapshot;
|
|
93
|
-
}): { event: PermanentChangedEvent | null; next: VaultSnapshot } {
|
|
94
|
-
const next = snapshotVault(opts.vault, opts.taxonomy);
|
|
95
|
-
const paths = diffSnapshots(opts.prev, next);
|
|
96
|
-
return {
|
|
97
|
-
event: paths.length ? { kind: "PERMANENT_CHANGED", paths } : null,
|
|
98
|
-
next,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Render the event as memoryd stdout signal lines (one per path — the
|
|
104
|
-
* notifier forwards each line verbatim; the Index batches the visible
|
|
105
|
-
* burst in one turn, and the coalescing above already bounds the burst to
|
|
106
|
-
* one pass).
|
|
107
|
-
*/
|
|
108
|
-
export function formatEventLines(event: PermanentChangedEvent): string[] {
|
|
109
|
-
return event.paths.map((p) => `PERMANENT_CHANGED: ${p}`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Inbox snapshot: draft basename → smart hash (top-level `.md` only —
|
|
114
|
-
* drafts live flat). Same smart-hash mechanics as the permanent snapshot:
|
|
115
|
-
* service-field re-stamps are invisible, content edits are events.
|
|
116
|
-
*
|
|
117
|
-
* WHY hash-diff and not a name baseline (B-приёмка 10.06, boris): the
|
|
118
|
-
* original name-set baseline made a draft INVISIBLE FOREVER once it
|
|
119
|
-
* survived a memoryd restart, and silently broke the reject→fix→re-review
|
|
120
|
-
* cycle (an author's fix never re-entered the pipeline). The reference
|
|
121
|
-
* semantics «the Index catches pre-existing drafts up at its own start»
|
|
122
|
-
* died with the inversion — the Index no longer scans the inbox; the
|
|
123
|
-
* fail-open sweep covers pre-existing UNCHANGED drafts, the hash-diff
|
|
124
|
-
* covers everything else.
|
|
125
|
-
*/
|
|
126
|
-
export function snapshotFlatFolder(dir: string): VaultSnapshot {
|
|
127
|
-
const snapshot: VaultSnapshot = new Map();
|
|
128
|
-
let entries: fs.Dirent[];
|
|
129
|
-
try {
|
|
130
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
131
|
-
} catch {
|
|
132
|
-
return snapshot;
|
|
133
|
-
}
|
|
134
|
-
for (const e of entries) {
|
|
135
|
-
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
136
|
-
const h = hashFile(path.join(dir, e.name));
|
|
137
|
-
if (h) snapshot.set(e.name, h);
|
|
138
|
-
}
|
|
139
|
-
return snapshot;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function snapshotInbox(vault: string, taxonomy: TaxonomyPreset): VaultSnapshot {
|
|
143
|
-
return snapshotFlatFolder(path.join(vault, taxonomy.folders.inbox));
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* One inbox detection pass: new or semantically changed drafts since the
|
|
148
|
-
* previous snapshot (deletions ignored — a placement move by the Index).
|
|
149
|
-
* Scans the WHOLE inbox folder, independent of which fs.watch path
|
|
150
|
-
* triggered the flush — no dependency on watch filename fidelity.
|
|
151
|
-
*/
|
|
152
|
-
export function detectInboxChanges(opts: {
|
|
153
|
-
vault: string;
|
|
154
|
-
taxonomy: TaxonomyPreset;
|
|
155
|
-
prev: VaultSnapshot;
|
|
156
|
-
}): { names: string[]; next: VaultSnapshot } {
|
|
157
|
-
const next = snapshotInbox(opts.vault, opts.taxonomy);
|
|
158
|
-
return { names: diffSnapshots(opts.prev, next), next };
|
|
159
|
-
}
|
|
78
|
+
// The daemon coalesces a detection pass into ONE batch event (CURATOR_TICK)
|
|
79
|
+
// by diffing `snapshotVault` against the carried baseline directly — see
|
|
80
|
+
// `runCuratorTick` in memoryd.ts. The earlier per-line event helper was
|
|
81
|
+
// vestigial and removed with the inbox pipeline.
|
package/src/search.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { escapeFtsQuery, queryTokens } from "./utils.js";
|
|
|
22
22
|
import { agentMemoryFolderMarker, statusGroup } from "./taxonomy.js";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Per-step pipeline status, прокидывается наверх в response
|
|
25
|
+
* Per-step pipeline status, прокидывается наверх в response memory_search,
|
|
26
26
|
* чтобы агент-вызыватель видел в каком режиме отработал поиск (полный
|
|
27
27
|
* пайплайн / BM25-only из-за деградации эмбеддингов / без reranker).
|
|
28
28
|
*
|
|
@@ -62,7 +62,7 @@ const SNIPPET_LEAD_CHARS = 40;
|
|
|
62
62
|
const EMPTY_NOTE_SNIPPET = "(пустой каркас — в заметке нет содержимого)";
|
|
63
63
|
// How many top-degree related notes to surface per result. Was unbounded —
|
|
64
64
|
// hub notes (e.g. a project Plan note) inflated payload to ~10KB on a 6-result
|
|
65
|
-
// search. Agents can call
|
|
65
|
+
// search. Agents can call memory_related for the full neighborhood.
|
|
66
66
|
const RELATED_LIMIT = 3;
|
|
67
67
|
|
|
68
68
|
// --- Frontmatter status categories & ranking coefficients ---
|
|
@@ -257,7 +257,18 @@ export async function runVaultSearch(params: {
|
|
|
257
257
|
|
|
258
258
|
export type DedupMatch = { path: string; title: string; similarity: number };
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
/** Dup band lower bound — raw cosine ≥ this ⇒ «possible duplicate» (§3a).
|
|
261
|
+
* 0.78 confirmed by Артур (15.06) on live TEI calibration: paraphrase
|
|
262
|
+
* 0.81–0.88 caught, related-but-different ~0.72 excluded; 0.82 clipped heavy
|
|
263
|
+
* paraphrase at 0.815. Env-overridable (`IAPEER_MEMORY_DEDUP_THRESHOLD`). */
|
|
264
|
+
export const DEFAULT_DEDUP_THRESHOLD = 0.78;
|
|
265
|
+
|
|
266
|
+
/** Link-hint band lower bound — cosine in [this, DEDUP_THRESHOLD) ⇒
|
|
267
|
+
* «semantically close, maybe link» (§3b, Артур 15.06). HONEST framing: catches
|
|
268
|
+
* semantically SIMILAR notes, NOT complementary ones (a real link at cosine
|
|
269
|
+
* ~0.54 is missed) — additive to the Index's link saturation (§8), not a
|
|
270
|
+
* replacement. Env-overridable. */
|
|
271
|
+
export const DEFAULT_LINK_HINT_THRESHOLD = 0.68;
|
|
261
272
|
|
|
262
273
|
/**
|
|
263
274
|
* Dedup hint (lean §3a): given the content of a CANON note being written,
|
|
@@ -272,14 +283,21 @@ export const DEFAULT_DEDUP_THRESHOLD = 0.82;
|
|
|
272
283
|
export async function runDedup(
|
|
273
284
|
db: CoreDb,
|
|
274
285
|
config: CoreConfig,
|
|
275
|
-
params: { content: string; threshold?: number; limit?: number },
|
|
286
|
+
params: { content: string; threshold?: number; limit?: number; linkThreshold?: number },
|
|
276
287
|
): Promise<{ enabled: boolean; matches: DedupMatch[] }> {
|
|
277
288
|
if (!config.embedding) return { enabled: false, matches: [] };
|
|
278
289
|
const threshold = params.threshold ?? DEFAULT_DEDUP_THRESHOLD;
|
|
290
|
+
// §3b: when a link-band lower bound is given, `threshold` is the DUP-band
|
|
291
|
+
// bound and `linkThreshold` the LINK-band bound; each band gets its OWN cap so
|
|
292
|
+
// a burst of ≥`limit` dup matches never starves the link band (both are
|
|
293
|
+
// returned, the caller re-classifies by similarity). Without it → legacy
|
|
294
|
+
// single-band behaviour (identical to before).
|
|
295
|
+
const linkThreshold = params.linkThreshold;
|
|
296
|
+
const lower = linkThreshold !== undefined ? Math.min(linkThreshold, threshold) : threshold;
|
|
279
297
|
const limit = Math.max(1, params.limit ?? 5);
|
|
280
298
|
const q = await embedQuery(params.content, config.embedding);
|
|
281
299
|
if (!q.vector) return { enabled: true, matches: [] }; // embed failed / circuit-open → silent
|
|
282
|
-
const raw = vectorSearch(db, q.vector, limit *
|
|
300
|
+
const raw = vectorSearch(db, q.vector, limit * 6); // headroom: canon filter + two bands
|
|
283
301
|
const f = config.taxonomy.folders;
|
|
284
302
|
const canonHeads = new Set([
|
|
285
303
|
f.knowledge,
|
|
@@ -289,16 +307,22 @@ export async function runDedup(
|
|
|
289
307
|
f.lists,
|
|
290
308
|
f.archive,
|
|
291
309
|
]);
|
|
292
|
-
const
|
|
310
|
+
const dup: DedupMatch[] = [];
|
|
311
|
+
const link: DedupMatch[] = [];
|
|
293
312
|
for (const r of raw) {
|
|
294
|
-
if (r.score <
|
|
313
|
+
if (r.score < lower) break; // raw is cosine-descending → below the lowest band, done
|
|
295
314
|
const head = r.path.split("/")[0];
|
|
296
315
|
if (!canonHeads.has(head)) continue; // canon (+archive) only
|
|
297
316
|
const base = r.path.split("/").pop() ?? r.path;
|
|
298
|
-
|
|
299
|
-
if (
|
|
317
|
+
const m: DedupMatch = { path: r.path, title: base.replace(/\.md$/, ""), similarity: r.score };
|
|
318
|
+
if (r.score >= threshold) {
|
|
319
|
+
if (dup.length < limit) dup.push(m);
|
|
320
|
+
} else if (linkThreshold !== undefined && r.score >= linkThreshold) {
|
|
321
|
+
if (link.length < limit) link.push(m);
|
|
322
|
+
}
|
|
323
|
+
if (dup.length >= limit && (linkThreshold === undefined || link.length >= limit)) break;
|
|
300
324
|
}
|
|
301
|
-
return { enabled: true, matches };
|
|
325
|
+
return { enabled: true, matches: [...dup, ...link] };
|
|
302
326
|
}
|
|
303
327
|
|
|
304
328
|
// --- Vector search ---
|
|
@@ -14,19 +14,11 @@
|
|
|
14
14
|
* our response re-stamp echo-safe BY CONSTRUCTION: a re-stamp touches only
|
|
15
15
|
* service fields, so it never re-triggers the detector or the batch diffs.
|
|
16
16
|
*
|
|
17
|
-
* ZONED rule (the humanEditPass intersection, §3a of the design):
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* inbox — UNCONDITIONAL: humanEditPass does not cover the agent inbox at
|
|
23
|
-
* all, human edits there are marginal, and the curator-masquerade
|
|
24
|
-
* (memoryd's 822-suppress) plus the silent author edit are both closed
|
|
25
|
-
* by one branch. PRECONDITION (the 11.06 false-positive lesson, boris's
|
|
26
|
-
* Write+Edit repro): the rule is only a discriminator because fillInbox
|
|
27
|
-
* UPSERTS the stamp pair on every hook edit — before that fix the pair
|
|
28
|
-
* was identically (null, null) for author drafts, «stamp unmoved» held
|
|
29
|
-
* vacuously and every second legitimate edit re-stamped as unstamped.
|
|
17
|
+
* ZONED rule (the humanEditPass intersection, §3a of the design): the canon
|
|
18
|
+
* folders (five typed + project notes + agent memory) — ONLY the fresh-window
|
|
19
|
+
* case (stamp ≤ FRESH_EDIT_WINDOW_S): that is exactly the echo-window swallow;
|
|
20
|
+
* STALE cases stay with humanEditPass (human attribution — the Obsidian main
|
|
21
|
+
* case; a wider rule would regress «Артур правит кураторскую заметку»).
|
|
30
22
|
*
|
|
31
23
|
* Attribution token: `unstamped` — NEUTRAL on purpose (the mechanism
|
|
32
24
|
* cannot tell a Bash agent from a human outside Obsidian; a false «agent»
|
|
@@ -49,7 +41,7 @@ import { smartHash } from "./smart-hash.js";
|
|
|
49
41
|
|
|
50
42
|
export const UNSTAMPED_TOKEN = "unstamped";
|
|
51
43
|
|
|
52
|
-
export type SilentZone = "permanent"
|
|
44
|
+
export type SilentZone = "permanent";
|
|
53
45
|
|
|
54
46
|
/** The per-file baseline record (design §4): the SEMANTIC hash (the BASE
|
|
55
47
|
* precondition — without it an iCloud mtime-echo inside the fresh window
|
|
@@ -87,18 +79,17 @@ export type DecideSilentInput = {
|
|
|
87
79
|
};
|
|
88
80
|
|
|
89
81
|
/**
|
|
90
|
-
* BASE: the semantic hash MOVED ∧ the stamp pair did not move verbatim
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* silent
|
|
82
|
+
* BASE: the semantic hash MOVED ∧ the stamp pair did not move verbatim,
|
|
83
|
+
* and only when the standing stamp is FRESH (the echo-window swallow). A
|
|
84
|
+
* service-only change (hook echo, our own re-stamp) keeps the semantic hash
|
|
85
|
+
* still → never silent; an mtime-only event keeps content identical → never
|
|
86
|
+
* silent.
|
|
95
87
|
*/
|
|
96
88
|
export function isSilentEdit(input: DecideSilentInput): boolean {
|
|
97
89
|
if (input.prev.hash === input.curr.hash) return false; // BASE: no semantic move
|
|
98
90
|
const stampUnmoved =
|
|
99
91
|
input.prev.updated === input.curr.updated && input.prev.leb === input.curr.leb;
|
|
100
92
|
if (!stampUnmoved) return false;
|
|
101
|
-
if (input.zone === "inbox") return true;
|
|
102
93
|
const windowS = input.freshEditWindowS ?? DEFAULT_FRESH_EDIT_WINDOW_S;
|
|
103
94
|
const editAt = parseUpdated(input.curr.updated);
|
|
104
95
|
return editAt !== null && (input.nowMs - editAt) / 1000 < windowS;
|
package/src/taxonomy.ts
CHANGED
|
@@ -22,8 +22,6 @@ export type LocaleId = "en" | "ru";
|
|
|
22
22
|
export type StatusGroup = "active" | "pending" | "stale";
|
|
23
23
|
|
|
24
24
|
export type TaxonomyFolders = {
|
|
25
|
-
inbox: string;
|
|
26
|
-
inboxHuman: string;
|
|
27
25
|
knowledge: string;
|
|
28
26
|
decisions: string;
|
|
29
27
|
projects: string;
|
|
@@ -53,7 +51,7 @@ export type TaxonomySubtypes = {
|
|
|
53
51
|
|
|
54
52
|
/** Individual status tokens the WRITER side must emit (hook fills). */
|
|
55
53
|
export type TaxonomyStatusTokens = {
|
|
56
|
-
/**
|
|
54
|
+
/** The draft status token (pending group) — author-settable WIP marker. */
|
|
57
55
|
draft: string;
|
|
58
56
|
/** Initial status of a new agent-memory note. */
|
|
59
57
|
current: string;
|
|
@@ -149,8 +147,6 @@ export type TaxonomyPreset = {
|
|
|
149
147
|
export const TAXONOMY_EN: TaxonomyPreset = {
|
|
150
148
|
locale: "en",
|
|
151
149
|
folders: {
|
|
152
|
-
inbox: "00_Inbox",
|
|
153
|
-
inboxHuman: "00_Inbox_Human",
|
|
154
150
|
knowledge: "01_Knowledge",
|
|
155
151
|
decisions: "02_Decisions",
|
|
156
152
|
projects: "03_Projects",
|
|
@@ -232,8 +228,6 @@ export const TAXONOMY_EN: TaxonomyPreset = {
|
|
|
232
228
|
export const TAXONOMY_RU: TaxonomyPreset = {
|
|
233
229
|
locale: "ru",
|
|
234
230
|
folders: {
|
|
235
|
-
inbox: "00_Входящие",
|
|
236
|
-
inboxHuman: "00_Входящие_человек",
|
|
237
231
|
knowledge: "01_Знания",
|
|
238
232
|
decisions: "02_Решения",
|
|
239
233
|
projects: "03_Проекты",
|
|
@@ -396,16 +390,12 @@ export function genreForFolder(
|
|
|
396
390
|
}
|
|
397
391
|
|
|
398
392
|
/**
|
|
399
|
-
* Default search-index exclusions:
|
|
400
|
-
*
|
|
401
|
-
*
|
|
393
|
+
* Default search-index exclusions: the system folder (templates/dictionary,
|
|
394
|
+
* not content). The archive is NOT excluded — it stays searchable with the
|
|
395
|
+
* stale boost. (Inbox folders are gone — authors write straight into canon.)
|
|
402
396
|
*/
|
|
403
397
|
export function defaultExcludeFolders(taxonomy: TaxonomyPreset): string[] {
|
|
404
|
-
return [
|
|
405
|
-
taxonomy.folders.inbox,
|
|
406
|
-
taxonomy.folders.inboxHuman,
|
|
407
|
-
taxonomy.folders.system,
|
|
408
|
-
];
|
|
398
|
+
return [taxonomy.folders.system];
|
|
409
399
|
}
|
|
410
400
|
|
|
411
401
|
/**
|
package/src/utils.ts
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
// Hash the FULL file content. Was a 4096-char prefix hash — that skipped
|
|
5
5
|
// reindexing whenever an append-only note (План / Фаза / Список — a
|
|
6
6
|
// first-class vault genre that grows downward) changed below the prefix
|
|
7
|
-
// window, leaving
|
|
7
|
+
// window, leaving memory_search / embeddings on the stale tail. Markdown is
|
|
8
8
|
// cheap; correctness over the micro-optimisation. If profiling ever demands
|
|
9
9
|
// it, gate a full hash behind size+mtime, never trust a prefix as the source
|
|
10
10
|
// of truth.
|