@theglitchking/semantic-memory 1.6.1 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +81 -0
  4. package/LICENSES/Apache-2.0.txt +202 -0
  5. package/README.md +19 -2
  6. package/THIRD_PARTY_NOTICES.md +17 -0
  7. package/commands/jev-mode.md +18 -0
  8. package/commands/skill-loop.md +23 -0
  9. package/dist/chunk-2BFG4IBC.js +295 -0
  10. package/dist/chunk-2BFG4IBC.js.map +1 -0
  11. package/dist/chunk-2OMDSTSS.js +118 -0
  12. package/dist/chunk-2OMDSTSS.js.map +1 -0
  13. package/dist/{chunk-TMMTOFHI.js → chunk-34VN47IW.js} +2 -2
  14. package/dist/chunk-3ML5ESXI.js +224 -0
  15. package/dist/chunk-3ML5ESXI.js.map +1 -0
  16. package/dist/{chunk-ZLIHQZ4C.js → chunk-6TIKWOY4.js} +5 -5
  17. package/dist/{chunk-KSH2QO6V.js → chunk-7UAVDOI6.js} +5 -5
  18. package/dist/chunk-ACZKDSMP.js +123 -0
  19. package/dist/chunk-ACZKDSMP.js.map +1 -0
  20. package/dist/{chunk-PGVIBUL3.js → chunk-AIXQFTSB.js} +5 -5
  21. package/dist/chunk-BSXIGPDY.js +137 -0
  22. package/dist/chunk-BSXIGPDY.js.map +1 -0
  23. package/dist/{chunk-HZ7B5KHY.js → chunk-EXAUVIGW.js} +4 -4
  24. package/dist/chunk-GYR7U6KA.js +80 -0
  25. package/dist/chunk-GYR7U6KA.js.map +1 -0
  26. package/dist/chunk-M2PC64DB.js +195 -0
  27. package/dist/chunk-M2PC64DB.js.map +1 -0
  28. package/dist/chunk-MTLYF357.js +130 -0
  29. package/dist/chunk-MTLYF357.js.map +1 -0
  30. package/dist/chunk-N77YCMSX.js +113 -0
  31. package/dist/chunk-N77YCMSX.js.map +1 -0
  32. package/dist/chunk-PJXYCCI2.js +177 -0
  33. package/dist/chunk-PJXYCCI2.js.map +1 -0
  34. package/dist/chunk-PMOZKUEM.js +170 -0
  35. package/dist/chunk-PMOZKUEM.js.map +1 -0
  36. package/dist/chunk-R6GNP7DS.js +87 -0
  37. package/dist/chunk-R6GNP7DS.js.map +1 -0
  38. package/dist/chunk-TKHWVISV.js +71 -0
  39. package/dist/chunk-TKHWVISV.js.map +1 -0
  40. package/dist/chunk-V6PSK4Y4.js +186 -0
  41. package/dist/chunk-V6PSK4Y4.js.map +1 -0
  42. package/dist/{chunk-GYBUBJYM.js → chunk-VBLCUWT4.js} +17 -8
  43. package/dist/chunk-VBLCUWT4.js.map +1 -0
  44. package/dist/chunk-VQCNWRTL.js +121 -0
  45. package/dist/chunk-VQCNWRTL.js.map +1 -0
  46. package/dist/{chunk-HUG7E25D.js → chunk-Y7H76Q7F.js} +1 -1
  47. package/dist/chunk-Y7H76Q7F.js.map +1 -0
  48. package/dist/chunk-ZSWI3Y4Y.js +232 -0
  49. package/dist/chunk-ZSWI3Y4Y.js.map +1 -0
  50. package/dist/cli/index.js +381 -52
  51. package/dist/cli/index.js.map +1 -1
  52. package/dist/core/features.d.ts +38 -0
  53. package/dist/core/features.js +28 -0
  54. package/dist/core/index.d.ts +285 -2
  55. package/dist/core/index.js +170 -21
  56. package/dist/core/index.js.map +1 -1
  57. package/dist/core/jev.d.ts +35 -0
  58. package/dist/core/jev.js +20 -0
  59. package/dist/core/skill-curator.d.ts +1 -0
  60. package/dist/core/skill-curator.js +41 -0
  61. package/dist/core/skill-evidence.d.ts +25 -0
  62. package/dist/core/skill-evidence.js +23 -0
  63. package/dist/core/skill-queue.d.ts +46 -0
  64. package/dist/core/skill-queue.js +42 -0
  65. package/dist/core/skill-review.d.ts +3 -0
  66. package/dist/core/skill-review.js +29 -0
  67. package/dist/core/skill-worker.d.ts +21 -0
  68. package/dist/core/skill-worker.js +70 -0
  69. package/dist/core/skill-worker.js.map +1 -0
  70. package/dist/{dossier-EN26WDKC.js → dossier-RXBLQOXC.js} +3 -3
  71. package/dist/failure-eval-IUJO47UC.js +18 -0
  72. package/dist/failure-eval-IUJO47UC.js.map +1 -0
  73. package/dist/{healthcheck-53OVXUQQ.js → healthcheck-S7D6YNTA.js} +7 -7
  74. package/dist/healthcheck-S7D6YNTA.js.map +1 -0
  75. package/dist/{lexicon-XNA5IFIA.js → lexicon-SEJSLBHO.js} +4 -4
  76. package/dist/lexicon-SEJSLBHO.js.map +1 -0
  77. package/dist/{lint-O4RQ47NX.js → lint-3JNLRM5U.js} +6 -6
  78. package/dist/lint-3JNLRM5U.js.map +1 -0
  79. package/dist/mcp/server.js +19 -17
  80. package/dist/mcp/server.js.map +1 -1
  81. package/dist/{profile-SIWAVD7O.js → profile-SNULTJ27.js} +4 -4
  82. package/dist/profile-SNULTJ27.js.map +1 -0
  83. package/dist/{schema-UJNYYRZZ.js → schema-RDYUK6CC.js} +2 -2
  84. package/dist/schema-RDYUK6CC.js.map +1 -0
  85. package/dist/skill-curator-D7HHZaUI.d.ts +71 -0
  86. package/dist/skill-land-EHXZW47F.js +31 -0
  87. package/dist/skill-land-EHXZW47F.js.map +1 -0
  88. package/dist/skill-ledger-AYXVNFX7.js +18 -0
  89. package/dist/skill-ledger-AYXVNFX7.js.map +1 -0
  90. package/dist/skill-merge-HBZBALZX.js +24 -0
  91. package/dist/skill-merge-HBZBALZX.js.map +1 -0
  92. package/dist/skill-review-BNvHipn9.d.ts +182 -0
  93. package/dist/skill-schema-RVB3EHEF.js +30 -0
  94. package/dist/skill-schema-RVB3EHEF.js.map +1 -0
  95. package/hooks/hooks.json +26 -1
  96. package/hooks/jev-bridge.py +109 -0
  97. package/hooks/session-start.js +5 -0
  98. package/hooks/skill-loop.js +65 -0
  99. package/hooks/vault-context.js +104 -6
  100. package/package.json +4 -2
  101. package/dist/chunk-5F5ZIAW2.js +0 -79
  102. package/dist/chunk-5F5ZIAW2.js.map +0 -1
  103. package/dist/chunk-GYBUBJYM.js.map +0 -1
  104. package/dist/chunk-HUG7E25D.js.map +0 -1
  105. package/dist/failure-eval-37JRNTIW.js +0 -10
  106. /package/dist/{chunk-TMMTOFHI.js.map → chunk-34VN47IW.js.map} +0 -0
  107. /package/dist/{chunk-ZLIHQZ4C.js.map → chunk-6TIKWOY4.js.map} +0 -0
  108. /package/dist/{chunk-KSH2QO6V.js.map → chunk-7UAVDOI6.js.map} +0 -0
  109. /package/dist/{chunk-PGVIBUL3.js.map → chunk-AIXQFTSB.js.map} +0 -0
  110. /package/dist/{chunk-HZ7B5KHY.js.map → chunk-EXAUVIGW.js.map} +0 -0
  111. /package/dist/{dossier-EN26WDKC.js.map → core/features.js.map} +0 -0
  112. /package/dist/{failure-eval-37JRNTIW.js.map → core/jev.js.map} +0 -0
  113. /package/dist/{healthcheck-53OVXUQQ.js.map → core/skill-curator.js.map} +0 -0
  114. /package/dist/{lexicon-XNA5IFIA.js.map → core/skill-evidence.js.map} +0 -0
  115. /package/dist/{lint-O4RQ47NX.js.map → core/skill-queue.js.map} +0 -0
  116. /package/dist/{profile-SIWAVD7O.js.map → core/skill-review.js.map} +0 -0
  117. /package/dist/{schema-UJNYYRZZ.js.map → dossier-RXBLQOXC.js.map} +0 -0
@@ -0,0 +1,186 @@
1
+ import {
2
+ applyChange,
3
+ archiveDir,
4
+ readLedger
5
+ } from "./chunk-TKHWVISV.js";
6
+ import {
7
+ indexSkill
8
+ } from "./chunk-N77YCMSX.js";
9
+ import {
10
+ parseVerify
11
+ } from "./chunk-MTLYF357.js";
12
+ import {
13
+ findSkills,
14
+ parseSkill,
15
+ renderSkill
16
+ } from "./chunk-ZSWI3Y4Y.js";
17
+ import {
18
+ readEvidence,
19
+ stateDir
20
+ } from "./chunk-R6GNP7DS.js";
21
+
22
+ // src/core/skill-curator.ts
23
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from "fs";
24
+ import { join } from "path";
25
+ import { spawnSync } from "child_process";
26
+ var STALE_DAYS = 14;
27
+ var ARCHIVE_DAYS = 30;
28
+ var CURATOR_INTERVAL_DAYS = 7;
29
+ var IDLE_HOURS = 2;
30
+ var DAY = 864e5;
31
+ var curatorStatePath = (repo) => join(stateDir(repo), "curator.json");
32
+ function readState(repo) {
33
+ try {
34
+ return JSON.parse(readFileSync(curatorStatePath(repo), "utf8"));
35
+ } catch {
36
+ return {};
37
+ }
38
+ }
39
+ function writeState(repo, s) {
40
+ mkdirSync(stateDir(repo), { recursive: true });
41
+ writeFileSync(curatorStatePath(repo), JSON.stringify(s, null, 1));
42
+ }
43
+ function lastUsed(repo) {
44
+ const m = /* @__PURE__ */ new Map();
45
+ for (const u of readEvidence(repo, "usage")) {
46
+ const t = Date.parse(String(u.ts));
47
+ const name = String(u.skill).split(":").pop();
48
+ if (Number.isFinite(t) && t > (m.get(name) ?? 0)) m.set(name, t);
49
+ }
50
+ return m;
51
+ }
52
+ function landedAt(s, ledger) {
53
+ const e = ledger.filter((x) => x.skill === s.skill.name && ["land", "patch", "restore", "merge", "promote"].includes(x.action)).pop();
54
+ return e ? Date.parse(e.ts) : statSync(s.path).mtimeMs;
55
+ }
56
+ function plan(repo, now = Date.now(), env = process.env) {
57
+ const all = findSkills(repo, { home: env.HOME, global: false });
58
+ const learned = all.filter((s) => s.learned && s.skill.meta);
59
+ const used = lastUsed(repo);
60
+ const ledger = readLedger(repo);
61
+ const referenced = new Set(learned.flatMap((s) => [...s.skill.meta.supersedes ?? [], ...s.skill.meta.cites].map((x) => x.split("/").pop().replace(/\.md$/, ""))));
62
+ for (const s of all) for (const o of learned) if (s !== o && s.skill.body.includes(`/${o.skill.name}`)) referenced.add(o.skill.name);
63
+ const out = [];
64
+ for (const s of learned) {
65
+ const name = s.skill.name;
66
+ const meta = s.skill.meta;
67
+ const base = { skill: name, path: s.path };
68
+ if (meta.pinned) {
69
+ out.push({ ...base, action: "skip", why: "pinned" });
70
+ continue;
71
+ }
72
+ if (referenced.has(name)) {
73
+ out.push({ ...base, action: "skip", why: "cited by another skill" });
74
+ continue;
75
+ }
76
+ const last = used.get(name);
77
+ const since = last ?? landedAt(s, ledger);
78
+ const idle = (now - since) / DAY;
79
+ if (last === void 0 && idle < STALE_DAYS) {
80
+ out.push({ ...base, action: "skip", why: `never used, landed ${idle.toFixed(0)} d ago` });
81
+ continue;
82
+ }
83
+ if (idle >= ARCHIVE_DAYS) out.push({ ...base, action: "archive", why: `unused ${Math.floor(idle)} days` });
84
+ else if (idle >= STALE_DAYS && meta.status === "active") out.push({ ...base, action: "stale", why: `unused ${Math.floor(idle)} days` });
85
+ else if (idle < STALE_DAYS && meta.status === "stale") out.push({ ...base, action: "reactivate", why: `used ${Math.floor(idle)} days ago` });
86
+ else out.push({ ...base, action: "keep", why: `${meta.status}, ${last === void 0 ? "landed" : "used"} ${Math.floor(idle)} days ago` });
87
+ }
88
+ return out;
89
+ }
90
+ var withStatus = (raw, status) => {
91
+ const s = parseSkill(raw);
92
+ return renderSkill({ name: s.name, description: s.description, meta: { ...s.meta, status }, body: s.body });
93
+ };
94
+ function curate(repo, opts = {}) {
95
+ const env = opts.env ?? process.env;
96
+ const now = opts.now ?? Date.now();
97
+ const actions = plan(repo, now, env);
98
+ const ids = [];
99
+ if (!opts.dryRun) {
100
+ for (const a of actions) {
101
+ if (a.action === "skip" || a.action === "keep") continue;
102
+ const raw = readFileSync(a.path, "utf8");
103
+ if (a.action === "archive") {
104
+ const dest = join(archiveDir(repo), a.skill, "SKILL.md");
105
+ ids.push(applyChange(repo, { action: "archive", actor: "curator", skill: a.skill, why: a.why }, [
106
+ { path: dest, content: withStatus(raw, "archived") },
107
+ { path: a.path, content: null }
108
+ ]).id);
109
+ indexSkill({ repo, name: a.skill, path: a.path, status: "archived" }, env);
110
+ } else {
111
+ const status = a.action === "stale" ? "stale" : "active";
112
+ ids.push(applyChange(repo, { action: a.action === "stale" ? "stale" : "restore", actor: "curator", skill: a.skill, why: a.why }, [{ path: a.path, content: withStatus(raw, status) }]).id);
113
+ }
114
+ }
115
+ const st = readState(repo);
116
+ writeState(repo, { ...st, last_run: new Date(now).toISOString(), last_report: actions.filter((a) => a.action !== "skip" && a.action !== "keep").map((a) => `${a.action} ${a.skill}`).join(", ") || "nothing to do" });
117
+ }
118
+ return { actions, ledger: ids };
119
+ }
120
+ function curatorDue(repo, now = Date.now()) {
121
+ const st = readState(repo);
122
+ if (!st.first_seen) {
123
+ writeState(repo, { ...st, first_seen: new Date(now).toISOString() });
124
+ return false;
125
+ }
126
+ if (now - Date.parse(st.first_seen) < CURATOR_INTERVAL_DAYS * DAY) return false;
127
+ if (st.last_run && now - Date.parse(st.last_run) < CURATOR_INTERVAL_DAYS * DAY) return false;
128
+ let last = 0;
129
+ for (const f of ["skill-usage.jsonl", "failures.jsonl", "corrections.jsonl"]) {
130
+ try {
131
+ last = Math.max(last, statSync(join(stateDir(repo), f)).mtimeMs);
132
+ } catch {
133
+ }
134
+ }
135
+ return now - last >= IDLE_HOURS * 36e5;
136
+ }
137
+ function restore(repo, name) {
138
+ const archived = join(archiveDir(repo), name, "SKILL.md");
139
+ if (!existsSync(archived)) throw new Error(`no archived skill "${name}" (archived: ${listArchived(repo).join(", ") || "none"})`);
140
+ const from = readLedger(repo).filter((e) => e.action === "archive" && e.skill === name).pop();
141
+ const original = from?.files.find((f) => f.after === null)?.path ?? join(repo, ".claude", "skills", name, "SKILL.md");
142
+ if (existsSync(original)) throw new Error(`${original} exists already`);
143
+ return applyChange(repo, { action: "restore", actor: "user", skill: name, why: "restored from skills-archive" }, [
144
+ { path: original, content: withStatus(readFileSync(archived, "utf8"), "active") },
145
+ { path: archived, content: null }
146
+ ]);
147
+ }
148
+ function listArchived(repo) {
149
+ try {
150
+ return readdirSync(archiveDir(repo)).filter((d) => existsSync(join(archiveDir(repo), d, "SKILL.md")));
151
+ } catch {
152
+ return [];
153
+ }
154
+ }
155
+ function verifySkills(repo, opts = {}) {
156
+ const out = [];
157
+ for (const s of findSkills(repo, { home: opts.env?.HOME, global: false }).filter((x) => x.learned)) {
158
+ const v = parseVerify(s.skill.body);
159
+ if (v.kind !== "command") {
160
+ out.push({ skill: s.skill.name, result: v.kind === "prose" ? "prose" : "none", detail: v.kind === "prose" ? v.text.slice(0, 120) : "no ## Verify" });
161
+ continue;
162
+ }
163
+ const r = spawnSync("bash", ["-c", v.command], { cwd: repo, timeout: opts.timeoutMs ?? 12e4, encoding: "utf8", env: opts.env ?? process.env });
164
+ out.push({ skill: s.skill.name, result: r.status === 0 ? "pass" : "fail", detail: r.status === 0 ? v.command : `${v.command} \u2192 exit ${r.status}: ${(r.stderr || r.stdout || "").trim().slice(0, 200)}` });
165
+ }
166
+ const st = readState(repo);
167
+ writeState(repo, { ...st, flagged: out.filter((x) => x.result === "fail").map((x) => ({ name: x.skill, reason: x.detail, at: (/* @__PURE__ */ new Date()).toISOString() })) });
168
+ return out;
169
+ }
170
+ var curatorState = readState;
171
+
172
+ export {
173
+ STALE_DAYS,
174
+ ARCHIVE_DAYS,
175
+ CURATOR_INTERVAL_DAYS,
176
+ IDLE_HOURS,
177
+ curatorStatePath,
178
+ plan,
179
+ curate,
180
+ curatorDue,
181
+ restore,
182
+ listArchived,
183
+ verifySkills,
184
+ curatorState
185
+ };
186
+ //# sourceMappingURL=chunk-V6PSK4Y4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/skill-curator.ts"],"sourcesContent":["/**\n * The skill loop's curator (v1.7): keeps learned skills from piling up. Deterministic — no model call:\n *\n * - a learned skill unused for 14 days becomes `stale`; for 30 days it is archived to\n * .claude/.semantic-memory/skills-archive/<name>/ (outside .claude/skills/, so Claude Code stops loading it)\n * - a stale skill that is used again goes back to `active`\n * - skipped: pinned skills, skills another skill cites or supersedes, never-used skills younger than 14 days,\n * and every skill the loop did not write\n *\n * Runs idle-triggered from SessionStart (every 7 days, when the repo has been quiet >= 2 h, never in the first\n * 7 days after the loop first saw the repo) or by hand. Every change is a ledger entry; `restore` brings an\n * archived skill back. Merging is separate and opt-in (skill-merge.ts).\n */\nimport { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { readEvidence, stateDir } from \"./skill-evidence.js\";\nimport { applyChange, archiveDir, readLedger, type LedgerEntry } from \"./skill-ledger.js\";\nimport { findSkills, parseSkill, renderSkill, type SkillFile } from \"./skill-schema.js\";\nimport { indexSkill } from \"./skill-scope.js\";\nimport { parseVerify } from \"./skill-eval/task.js\";\n\nexport const STALE_DAYS = 14;\nexport const ARCHIVE_DAYS = 30;\nexport const CURATOR_INTERVAL_DAYS = 7;\nexport const IDLE_HOURS = 2;\nconst DAY = 86_400_000;\n\nexport const curatorStatePath = (repo: string) => join(stateDir(repo), \"curator.json\");\n\ninterface CuratorState {\n first_seen?: string;\n last_run?: string;\n last_report?: string;\n flagged?: { name: string; reason: string; at: string }[];\n}\n\nfunction readState(repo: string): CuratorState {\n try {\n return JSON.parse(readFileSync(curatorStatePath(repo), \"utf8\"));\n } catch {\n return {};\n }\n}\nfunction writeState(repo: string, s: CuratorState) {\n mkdirSync(stateDir(repo), { recursive: true });\n writeFileSync(curatorStatePath(repo), JSON.stringify(s, null, 1));\n}\n\nexport interface CuratorAction {\n skill: string;\n path: string;\n action: \"stale\" | \"archive\" | \"reactivate\" | \"keep\" | \"skip\";\n why: string;\n}\n\n/** When the skill was last used here (ms), from skill-usage.jsonl; null if never. */\nfunction lastUsed(repo: string): Map<string, number> {\n const m = new Map<string, number>();\n for (const u of readEvidence(repo, \"usage\")) {\n const t = Date.parse(String(u.ts));\n const name = String(u.skill).split(\":\").pop()!;\n if (Number.isFinite(t) && t > (m.get(name) ?? 0)) m.set(name, t);\n }\n return m;\n}\n\n/** When the skill landed here (ms): its ledger entry, else the file's mtime. */\nfunction landedAt(s: SkillFile, ledger: LedgerEntry[]): number {\n const e = ledger.filter((x) => x.skill === s.skill.name && [\"land\", \"patch\", \"restore\", \"merge\", \"promote\"].includes(x.action)).pop();\n return e ? Date.parse(e.ts) : statSync(s.path).mtimeMs;\n}\n\nexport function plan(repo: string, now = Date.now(), env = process.env): CuratorAction[] {\n const all = findSkills(repo, { home: env.HOME, global: false });\n const learned = all.filter((s) => s.learned && s.skill.meta);\n const used = lastUsed(repo);\n const ledger = readLedger(repo);\n // a skill another one cites or supersedes is still load-bearing\n const referenced = new Set(learned.flatMap((s) => [...(s.skill.meta!.supersedes ?? []), ...s.skill.meta!.cites].map((x) => x.split(\"/\").pop()!.replace(/\\.md$/, \"\"))));\n for (const s of all) for (const o of learned) if (s !== o && s.skill.body.includes(`/${o.skill.name}`)) referenced.add(o.skill.name);\n const out: CuratorAction[] = [];\n for (const s of learned) {\n const name = s.skill.name;\n const meta = s.skill.meta!;\n const base = { skill: name, path: s.path };\n if (meta.pinned) { out.push({ ...base, action: \"skip\", why: \"pinned\" }); continue; }\n if (referenced.has(name)) { out.push({ ...base, action: \"skip\", why: \"cited by another skill\" }); continue; }\n const last = used.get(name);\n const since = last ?? landedAt(s, ledger);\n const idle = (now - since) / DAY;\n if (last === undefined && idle < STALE_DAYS) { out.push({ ...base, action: \"skip\", why: `never used, landed ${idle.toFixed(0)} d ago` }); continue; }\n if (idle >= ARCHIVE_DAYS) out.push({ ...base, action: \"archive\", why: `unused ${Math.floor(idle)} days` });\n else if (idle >= STALE_DAYS && meta.status === \"active\") out.push({ ...base, action: \"stale\", why: `unused ${Math.floor(idle)} days` });\n else if (idle < STALE_DAYS && meta.status === \"stale\") out.push({ ...base, action: \"reactivate\", why: `used ${Math.floor(idle)} days ago` });\n else out.push({ ...base, action: \"keep\", why: `${meta.status}, ${last === undefined ? \"landed\" : \"used\"} ${Math.floor(idle)} days ago` });\n }\n return out;\n}\n\nconst withStatus = (raw: string, status: \"active\" | \"stale\" | \"archived\") => {\n const s = parseSkill(raw);\n return renderSkill({ name: s.name, description: s.description, meta: { ...s.meta!, status }, body: s.body });\n};\n\n/** Run the curator. Dry run: report only. Returns the actions (and the ledger ids of the changes made). */\nexport function curate(repo: string, opts: { dryRun?: boolean; now?: number; env?: NodeJS.ProcessEnv } = {}): { actions: CuratorAction[]; ledger: string[] } {\n const env = opts.env ?? process.env;\n const now = opts.now ?? Date.now();\n const actions = plan(repo, now, env);\n const ids: string[] = [];\n if (!opts.dryRun) {\n for (const a of actions) {\n if (a.action === \"skip\" || a.action === \"keep\") continue;\n const raw = readFileSync(a.path, \"utf8\");\n if (a.action === \"archive\") {\n const dest = join(archiveDir(repo), a.skill, \"SKILL.md\");\n ids.push(applyChange(repo, { action: \"archive\", actor: \"curator\", skill: a.skill, why: a.why }, [\n { path: dest, content: withStatus(raw, \"archived\") },\n { path: a.path, content: null },\n ]).id);\n indexSkill({ repo, name: a.skill, path: a.path, status: \"archived\" }, env);\n } else {\n const status = a.action === \"stale\" ? \"stale\" : \"active\";\n ids.push(applyChange(repo, { action: a.action === \"stale\" ? \"stale\" : \"restore\", actor: \"curator\", skill: a.skill, why: a.why }, [{ path: a.path, content: withStatus(raw, status) }]).id);\n }\n }\n const st = readState(repo);\n writeState(repo, { ...st, last_run: new Date(now).toISOString(), last_report: actions.filter((a) => a.action !== \"skip\" && a.action !== \"keep\").map((a) => `${a.action} ${a.skill}`).join(\", \") || \"nothing to do\" });\n }\n return { actions, ledger: ids };\n}\n\n/** Whether SessionStart should start the curator now (and records first sight of the repo). */\nexport function curatorDue(repo: string, now = Date.now()): boolean {\n const st = readState(repo);\n if (!st.first_seen) {\n writeState(repo, { ...st, first_seen: new Date(now).toISOString() });\n return false;\n }\n if (now - Date.parse(st.first_seen) < CURATOR_INTERVAL_DAYS * DAY) return false;\n if (st.last_run && now - Date.parse(st.last_run) < CURATOR_INTERVAL_DAYS * DAY) return false;\n // quiet: the last thing the loop recorded here is >= 2 h old (the session starting now is not activity yet)\n let last = 0;\n for (const f of [\"skill-usage.jsonl\", \"failures.jsonl\", \"corrections.jsonl\"]) {\n try {\n last = Math.max(last, statSync(join(stateDir(repo), f)).mtimeMs);\n } catch {}\n }\n return now - last >= IDLE_HOURS * 3_600_000;\n}\n\n/** Bring an archived skill back where it was, active again. */\nexport function restore(repo: string, name: string): LedgerEntry {\n const archived = join(archiveDir(repo), name, \"SKILL.md\");\n if (!existsSync(archived)) throw new Error(`no archived skill \"${name}\" (archived: ${listArchived(repo).join(\", \") || \"none\"})`);\n const from = readLedger(repo).filter((e) => e.action === \"archive\" && e.skill === name).pop();\n const original = from?.files.find((f) => f.after === null)?.path ?? join(repo, \".claude\", \"skills\", name, \"SKILL.md\");\n if (existsSync(original)) throw new Error(`${original} exists already`);\n return applyChange(repo, { action: \"restore\", actor: \"user\", skill: name, why: \"restored from skills-archive\" }, [\n { path: original, content: withStatus(readFileSync(archived, \"utf8\"), \"active\") },\n { path: archived, content: null },\n ]);\n}\n\nexport function listArchived(repo: string): string[] {\n try {\n return readdirSync(archiveDir(repo)).filter((d) => existsSync(join(archiveDir(repo), d, \"SKILL.md\")));\n } catch {\n return [];\n }\n}\n\n/** Run each landed learned skill's Verify command (they were approved by a person); flag failures, archive nothing. */\nexport function verifySkills(repo: string, opts: { timeoutMs?: number; env?: NodeJS.ProcessEnv } = {}): { skill: string; result: \"pass\" | \"fail\" | \"prose\" | \"none\"; detail: string }[] {\n const out: { skill: string; result: \"pass\" | \"fail\" | \"prose\" | \"none\"; detail: string }[] = [];\n for (const s of findSkills(repo, { home: opts.env?.HOME, global: false }).filter((x) => x.learned)) {\n const v = parseVerify(s.skill.body);\n if (v.kind !== \"command\") {\n out.push({ skill: s.skill.name, result: v.kind === \"prose\" ? \"prose\" : \"none\", detail: v.kind === \"prose\" ? v.text.slice(0, 120) : \"no ## Verify\" });\n continue;\n }\n const r = spawnSync(\"bash\", [\"-c\", v.command], { cwd: repo, timeout: opts.timeoutMs ?? 120_000, encoding: \"utf8\", env: opts.env ?? process.env });\n out.push({ skill: s.skill.name, result: r.status === 0 ? \"pass\" : \"fail\", detail: r.status === 0 ? v.command : `${v.command} → exit ${r.status}: ${(r.stderr || r.stdout || \"\").trim().slice(0, 200)}` });\n }\n const st = readState(repo);\n writeState(repo, { ...st, flagged: out.filter((x) => x.result === \"fail\").map((x) => ({ name: x.skill, reason: x.detail, at: new Date().toISOString() })) });\n return out;\n}\n\nexport const curatorState = readState;\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  loadSchema,
3
3
  validateNote
4
- } from "./chunk-HUG7E25D.js";
4
+ } from "./chunk-Y7H76Q7F.js";
5
5
  import {
6
6
  computeDecay,
7
7
  loadDecayConfig,
@@ -13,21 +13,21 @@ import {
13
13
  import {
14
14
  compileLexicon,
15
15
  findAliasConflicts
16
- } from "./chunk-KSH2QO6V.js";
16
+ } from "./chunk-7UAVDOI6.js";
17
17
  import {
18
18
  deriveProjectRoot
19
19
  } from "./chunk-EYVFC745.js";
20
- import {
21
- findSecrets
22
- } from "./chunk-H2BES6FU.js";
23
20
  import {
24
21
  matter_default
25
22
  } from "./chunk-J7LYDRML.js";
23
+ import {
24
+ findSecrets
25
+ } from "./chunk-H2BES6FU.js";
26
26
 
27
27
  // src/core/lint.ts
28
28
  import { readFile } from "fs/promises";
29
29
  import { existsSync } from "fs";
30
- import { basename, join } from "path";
30
+ import { basename, dirname, join, relative } from "path";
31
31
  import { glob } from "glob";
32
32
  async function lintVault(notesPath, opts = {}) {
33
33
  const schema = opts.schemaOverride ?? await loadSchema(notesPath);
@@ -78,6 +78,14 @@ async function lintVault(notesPath, opts = {}) {
78
78
  if (opts.checkDecoys) {
79
79
  findings.push(...await findDecoys(notesPath, rawByPath));
80
80
  }
81
+ if (opts.checkSkills) {
82
+ const { lintSkills } = await import("./skill-schema-RVB3EHEF.js");
83
+ const derived = deriveProjectRoot(notesPath);
84
+ const root = opts.projectRoot ?? (basename(derived) === ".documentation" ? dirname(derived) : derived);
85
+ for (const f of lintSkills(root).findings) {
86
+ findings.push({ path: relative(root, f.path), rule: "skills", severity: f.severity, message: `${f.rule}: ${f.message}` });
87
+ }
88
+ }
81
89
  const byRule = {
82
90
  schema_violations: findings.filter((f) => f.rule === "schema_violations"),
83
91
  missing_provenance: findings.filter((f) => f.rule === "missing_provenance"),
@@ -87,7 +95,8 @@ async function lintVault(notesPath, opts = {}) {
87
95
  decay_candidates: findings.filter((f) => f.rule === "decay_candidates"),
88
96
  alias_conflicts: findings.filter((f) => f.rule === "alias_conflicts"),
89
97
  decoys: findings.filter((f) => f.rule === "decoys"),
90
- ...opts.checkSecrets ? { secrets: findings.filter((f) => f.rule === "secrets") } : {}
98
+ ...opts.checkSecrets ? { secrets: findings.filter((f) => f.rule === "secrets") } : {},
99
+ ...opts.checkSkills ? { skills: findings.filter((f) => f.rule === "skills") } : {}
91
100
  };
92
101
  const errors = findings.filter((f) => f.severity === "error").length;
93
102
  const warnings = findings.filter((f) => f.severity === "warn").length;
@@ -285,4 +294,4 @@ export {
285
294
  lintVault,
286
295
  formatLintReport
287
296
  };
288
- //# sourceMappingURL=chunk-GYBUBJYM.js.map
297
+ //# sourceMappingURL=chunk-VBLCUWT4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/lint.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { basename, dirname, join, relative } from \"node:path\";\nimport { glob } from \"glob\";\nimport matter from \"./matter.js\";\nimport { loadSchema, validateNote, type LintFinding, type VaultSchema } from \"./schema.js\";\nimport { deriveProjectRoot } from \"./session.js\";\nimport { computeDecay, loadDecayConfig, normalizeVerifiedDate } from \"./decay.js\";\nimport { readSelectionLog } from \"./telemetry.js\";\nimport { compileLexicon, findAliasConflicts } from \"./lexicon.js\";\nimport { findSecrets } from \"./redact.js\";\n\nexport interface LintReport {\n schemaPath: string;\n findings: LintFinding[];\n byRule: {\n schema_violations: LintFinding[];\n missing_provenance: LintFinding[];\n stale: LintFinding[];\n broken_links: LintFinding[];\n code_symbols: LintFinding[];\n decay_candidates: LintFinding[];\n alias_conflicts: LintFinding[];\n decoys: LintFinding[];\n /** only present when the opt-in secrets check ran */\n secrets?: LintFinding[];\n /** only present when the opt-in skills check ran */\n skills?: LintFinding[];\n };\n counts: {\n errors: number;\n warnings: number;\n filesLinted: number;\n };\n}\n\nexport async function lintVault(\n notesPath: string,\n opts: {\n pathGlob?: string;\n schemaOverride?: VaultSchema;\n todayIso?: string;\n /**\n * When true, run the code-symbol drift check: flag inline-code file-path\n * references in notes that no longer exist under the project root. Opt-in\n * (off by default) so the standard report and the healthcheck slow tier stay\n * quiet and byte-stable. Fails open when the project root is not a code repo.\n */\n checkCodeSymbols?: boolean;\n /** Override the project root for code-symbol resolution (default: derived from notesPath). */\n projectRoot?: string;\n /**\n * When true, run the decay-candidates check: cross-reference the selection\n * log (notes that appeared in recent search results) against each note's\n * current decay multiplier, and flag notes that are frequently retrieved yet\n * heavily decayed (\"relevant but going stale — verify or revise\"). Opt-in;\n * a silent no-op when no selection log exists.\n */\n checkDecayCandidates?: boolean;\n /**\n * When true, run the alias-conflict check: flag lexicon phrases that map to\n * more than one canonical target (a wrong binding misroutes queries). Opt-in;\n * no-op when the vault has no lexicon.\n */\n checkAliasConflicts?: boolean;\n /**\n * When true, run the decoys check: flag notes that were retrieved often (per\n * the selection log) but NEVER cited in an answer. Surfaced for a human to\n * judge — a decoy might be genuine noise OR a note the answers were wrong to\n * skip; usage-feedback ranking (v1.5) never auto-down-ranks on this ambiguous\n * signal (decision Q5). Opt-in; a no-op with no selection log.\n */\n checkDecoys?: boolean;\n /**\n * When true, run the secrets check (v1.5.3): flag notes that contain a secret\n * (API key, token, password, private key — the redactor's precise set). Reports\n * path, line and kind, never the value; nothing is rewritten. Opt-in, so the\n * default report stays byte-stable. A real secret must be rotated: it is in\n * git history if the note was ever committed.\n */\n checkSecrets?: boolean;\n /** When true, lint the skill loop's learned skills this repo loads (v1.7): format, evidence, cites, clashes. Opt-in. */\n checkSkills?: boolean;\n } = {}\n): Promise<LintReport> {\n const schema = opts.schemaOverride ?? (await loadSchema(notesPath));\n const files = await glob(opts.pathGlob ?? \"**/*.md\", {\n cwd: notesPath,\n ignore: [\"node_modules/**\", \".semantic-sidekick-index/**\", \"vault.schema.yml\"],\n });\n\n const findings: LintFinding[] = [];\n // Build known-note set first, to make broken-link detection O(1) per link.\n const knownNames = new Set<string>();\n const rawByPath = new Map<string, string>();\n for (const rel of files) {\n try {\n const raw = await readFile(join(notesPath, rel), \"utf-8\");\n rawByPath.set(rel, raw);\n // Wikilink names match on basename-without-ext (obsidian-style)\n knownNames.add(basename(rel).replace(/\\.md$/, \"\"));\n } catch {\n /* skip */\n }\n }\n\n // Code-symbol drift is opt-in and only meaningful against a real code repo.\n const codeRoot = opts.checkCodeSymbols\n ? resolveCodeRoot(opts.projectRoot ?? deriveProjectRoot(notesPath))\n : null;\n\n for (const [rel, raw] of rawByPath) {\n findings.push(...validateNote(rel, raw, schema, { todayIso: opts.todayIso }));\n findings.push(...findBrokenLinks(rel, raw, knownNames));\n if (codeRoot) findings.push(...findCodeSymbolDrift(rel, raw, codeRoot));\n if (opts.checkSecrets) {\n for (const s of findSecrets(raw)) {\n findings.push({\n path: rel,\n rule: \"secrets\",\n severity: \"error\",\n message: `line ${s.line}: ${s.kind} — rotate it if it is real, then remove it from the note (redaction does not rewrite git history)`,\n });\n }\n }\n }\n\n if (opts.checkDecayCandidates) {\n findings.push(...(await findDecayCandidates(notesPath, rawByPath, opts.todayIso)));\n }\n\n if (opts.checkAliasConflicts) {\n for (const c of findAliasConflicts(await compileLexicon(notesPath))) {\n findings.push({\n path: \"lexicon\",\n rule: \"alias_conflicts\",\n severity: \"warn\",\n message: `phrase \"${c.phrase}\" maps to ${c.canonicals.length} targets: ${c.canonicals.join(\", \")} — pick one`,\n });\n }\n }\n\n if (opts.checkDecoys) {\n findings.push(...(await findDecoys(notesPath, rawByPath)));\n }\n\n if (opts.checkSkills) {\n const { lintSkills } = await import(\"./skill-schema.js\");\n // .claude/.vault → the repo; a docs corpus (.documentation) sits at the repo root too\n const derived = deriveProjectRoot(notesPath);\n const root = opts.projectRoot ?? (basename(derived) === \".documentation\" ? dirname(derived) : derived);\n for (const f of lintSkills(root).findings) {\n findings.push({ path: relative(root, f.path), rule: \"skills\", severity: f.severity, message: `${f.rule}: ${f.message}` });\n }\n }\n\n const byRule = {\n schema_violations: findings.filter((f) => f.rule === \"schema_violations\"),\n missing_provenance: findings.filter((f) => f.rule === \"missing_provenance\"),\n stale: findings.filter((f) => f.rule === \"stale\"),\n broken_links: findings.filter((f) => f.rule === \"broken_links\"),\n code_symbols: findings.filter((f) => f.rule === \"code_symbols\"),\n decay_candidates: findings.filter((f) => f.rule === \"decay_candidates\"),\n alias_conflicts: findings.filter((f) => f.rule === \"alias_conflicts\"),\n decoys: findings.filter((f) => f.rule === \"decoys\"),\n ...(opts.checkSecrets ? { secrets: findings.filter((f) => f.rule === \"secrets\") } : {}),\n ...(opts.checkSkills ? { skills: findings.filter((f) => f.rule === \"skills\") } : {}),\n };\n const errors = findings.filter((f) => f.severity === \"error\").length;\n const warnings = findings.filter((f) => f.severity === \"warn\").length;\n\n return {\n schemaPath: join(notesPath, \"vault.schema.yml\"),\n findings,\n byRule,\n counts: { errors, warnings, filesLinted: files.length },\n };\n}\n\nfunction findBrokenLinks(\n relPath: string,\n rawFileContent: string,\n knownNames: Set<string>\n): LintFinding[] {\n const { content } = matter(rawFileContent);\n const findings: LintFinding[] = [];\n // Match [[target]] and [[target|alias]] — skip fenced code and inline code.\n const stripped = content\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`[^`]+`/g, \"\");\n const re = /\\[\\[([^\\]\\|]+)(?:\\|[^\\]]+)?\\]\\]/g;\n const seen = new Set<string>();\n let m: RegExpExecArray | null;\n while ((m = re.exec(stripped)) !== null) {\n const target = m[1].trim().split(\"#\")[0].split(\"/\").pop()!.replace(/\\.md$/, \"\");\n if (seen.has(target)) continue;\n seen.add(target);\n if (!knownNames.has(target)) {\n findings.push({\n path: relPath,\n rule: \"broken_links\",\n severity: \"warn\",\n message: `wikilink [[${target}]] has no matching note`,\n });\n }\n }\n return findings;\n}\n\n/** File extensions we treat as \"this looks like a source/doc file reference\". */\nconst CODE_EXTENSIONS = new Set([\n \"ts\", \"tsx\", \"js\", \"jsx\", \"mjs\", \"cjs\", \"py\", \"rb\", \"go\", \"rs\", \"java\", \"kt\",\n \"c\", \"h\", \"cpp\", \"hpp\", \"cs\", \"php\", \"swift\", \"scala\", \"sh\", \"bash\", \"sql\",\n \"json\", \"yaml\", \"yml\", \"toml\", \"css\", \"scss\", \"html\", \"vue\", \"svelte\", \"md\",\n]);\n\n/**\n * Decide whether `root` is a real code repository worth checking paths against.\n * Fails open (returns null) for non-code roots so the check is a silent no-op\n * outside a project — e.g. a standalone vault, or when babel-fish isn't present.\n */\nfunction resolveCodeRoot(root: string): string | null {\n if (!root || !existsSync(root)) return null;\n const markers = [\"package.json\", \".git\", \"src\", \"lib\", \"pyproject.toml\", \"go.mod\", \"Cargo.toml\"];\n return markers.some((m) => existsSync(join(root, m))) ? root : null;\n}\n\n/**\n * Code-symbol drift: scan a note's inline-code spans for repo-relative file-path\n * references and flag ones whose first path segment IS a real directory in the repo\n * but whose full path no longer exists — i.e. a stale reference to a moved/deleted\n * file. Anchoring on an existing first segment keeps false positives low: paths\n * belonging to other repos (whose top-level dir isn't present here) are skipped.\n *\n * Scope note: this validates *path* references. Fine-grained symbol-name checking\n * (function/class identifiers) needs a real symbol index and is planned for the\n * v1.4 lexicon arc, which will extend this same `code_symbols` rule.\n */\nfunction findCodeSymbolDrift(relPath: string, rawFileContent: string, codeRoot: string): LintFinding[] {\n const { content } = matter(rawFileContent);\n // Inline-code spans only (single backtick). Fenced blocks are illustrative and\n // over-match. Strip fenced blocks first so their backticks don't confuse the span regex.\n const withoutFences = content.replace(/```[\\s\\S]*?```/g, \"\");\n const findings: LintFinding[] = [];\n const seen = new Set<string>();\n const re = /`([^`\\n]+)`/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(withoutFences)) !== null) {\n let token = m[1].trim();\n if (!looksLikeRepoPath(token)) continue;\n token = token.replace(/^\\.\\//, \"\");\n if (seen.has(token)) continue;\n seen.add(token);\n const firstSeg = token.split(\"/\")[0];\n // Only judge paths anchored in a directory that actually exists in this repo.\n if (!existsSync(join(codeRoot, firstSeg))) continue;\n if (!existsSync(join(codeRoot, token))) {\n findings.push({\n path: relPath,\n rule: \"code_symbols\",\n severity: \"warn\",\n message: `inline code path \\`${token}\\` does not exist under the project root (stale reference?)`,\n });\n }\n }\n return findings;\n}\n\nfunction looksLikeRepoPath(token: string): boolean {\n if (!token.includes(\"/\")) return false;\n if (/\\s/.test(token)) return false; // commands, prose\n if (token.includes(\"://\")) return false; // URLs\n if (/[*?\\[\\]{}()<>|]/.test(token)) return false; // globs / shell / placeholders\n if (token.startsWith(\"/\") || token.startsWith(\"~\")) return false; // absolute / home\n const normalized = token.replace(/^\\.\\//, \"\");\n if (normalized.startsWith(\"..\")) return false; // escapes the repo\n const last = normalized.split(\"/\").pop() ?? \"\";\n const dot = last.lastIndexOf(\".\");\n if (dot <= 0) return false; // no extension → likely a dir or a symbol like a/b\n const ext = last.slice(dot + 1).toLowerCase();\n return CODE_EXTENSIONS.has(ext);\n}\n\n/** Multiplier at or below which a frequently-retrieved note is worth surfacing. */\nconst DECAY_CANDIDATE_THRESHOLD = 0.5;\n\n/**\n * Decay-candidates: notes that keep showing up in search results but have decayed.\n * Reads the selection log (search events record the paths each query returned),\n * computes each retrieved note's current decay multiplier from its frontmatter,\n * and flags those at/below the threshold — \"this keeps being relevant but is\n * going stale; verify or revise.\" Index-free (no embedder); a no-op when there's\n * no selection log yet.\n */\nasync function findDecayCandidates(\n notesPath: string,\n rawByPath: Map<string, string>,\n todayIso?: string\n): Promise<LintFinding[]> {\n const events = await readSelectionLog(notesPath);\n if (events.length === 0) return [];\n\n // Count how often each path was retrieved (appeared in a search's results).\n const retrieved = new Map<string, number>();\n for (const e of events) {\n if (e.kind === \"search\") {\n for (const r of e.results) retrieved.set(r.path, (retrieved.get(r.path) ?? 0) + 1);\n }\n }\n if (retrieved.size === 0) return [];\n\n const config = loadDecayConfig(notesPath);\n if (!config.enabled) return [];\n const nowMs = todayIso ? Date.parse(todayIso) : undefined;\n\n const findings: LintFinding[] = [];\n for (const [rel, count] of retrieved) {\n const raw = rawByPath.get(rel);\n if (!raw) continue; // retrieved path no longer in the vault — skip\n const { data: fm } = matter(raw);\n const decay = computeDecay({\n type: typeof fm.type === \"string\" ? fm.type : undefined,\n last_verified: normalizeVerifiedDate(fm.last_verified),\n evergreen: fm.evergreen === true,\n config,\n nowMs,\n });\n if (decay.multiplier <= DECAY_CANDIDATE_THRESHOLD) {\n findings.push({\n path: rel,\n rule: \"decay_candidates\",\n severity: \"warn\",\n message: `retrieved ${count}× recently but decayed to ${decay.multiplier.toFixed(2)} (age ${Math.round(decay.age_days)}d) — verify_note or revise`,\n });\n }\n }\n // Most-retrieved first — the ones worth re-verifying soonest.\n findings.sort((a, b) => (retrieved.get(b.path) ?? 0) - (retrieved.get(a.path) ?? 0));\n return findings;\n}\n\n/** A note must be retrieved at least this many times before \"never cited\" is signal. */\nconst DECOY_MIN_RETRIEVALS = 3;\n\n/**\n * Decoys (v1.5 Phase 9): notes retrieved ≥ DECOY_MIN_RETRIEVALS times that were\n * NEVER cited. Surfaced only — usage-feedback ranking never auto-down-ranks on\n * this ambiguous signal (a decoy might be genuine noise, or a note the answers\n * were wrong to skip). A human decides; the lint just points.\n */\nasync function findDecoys(notesPath: string, rawByPath: Map<string, string>): Promise<LintFinding[]> {\n const events = await readSelectionLog(notesPath);\n if (events.length === 0) return [];\n\n const retrieved = new Map<string, number>();\n const cited = new Set<string>();\n for (const e of events) {\n if (e.kind === \"search\") {\n for (const r of e.results) retrieved.set(r.path, (retrieved.get(r.path) ?? 0) + 1);\n } else if (e.kind === \"selection\") {\n cited.add(e.note_path);\n }\n }\n\n const findings: LintFinding[] = [];\n for (const [rel, count] of retrieved) {\n if (count < DECOY_MIN_RETRIEVALS) continue;\n if (cited.has(rel)) continue;\n if (!rawByPath.has(rel)) continue; // retrieved path no longer in the vault — skip\n findings.push({\n path: rel,\n rule: \"decoys\",\n severity: \"warn\",\n message: `retrieved ${count}× but never cited — decoy noise, or a note answers wrongly skip? (surfaced, not auto-down-ranked)`,\n });\n }\n findings.sort((a, b) => (retrieved.get(b.path) ?? 0) - (retrieved.get(a.path) ?? 0));\n return findings;\n}\n\nexport function formatLintReport(report: LintReport, opts: { rule?: keyof LintReport[\"byRule\"] } = {}): string {\n const target = (opts.rule ? report.byRule[opts.rule] : report.findings) ?? [];\n if (target.length === 0) {\n return `clean: 0 findings across ${report.counts.filesLinted} files`;\n }\n const lines = [\n `${target.length} finding${target.length === 1 ? \"\" : \"s\"} (errors=${report.counts.errors}, warnings=${report.counts.warnings}, files=${report.counts.filesLinted})`,\n \"\",\n ];\n for (const f of target) {\n lines.push(`[${f.severity}] ${f.rule}: ${f.path} — ${f.message}`);\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,121 @@
1
+ // src/core/features.ts
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join, dirname, delimiter } from "path";
5
+ import YAML from "yaml";
6
+ var ENV = { skill_loop: "SEMANTIC_MEMORY_SKILL_LOOP", jev: "SEMANTIC_MEMORY_JEV" };
7
+ var truthy = (v) => v === true || v === 1 || typeof v === "string" && /^(1|on|true|yes)$/i.test(v.trim());
8
+ var falsy = (v) => v === false || v === 0 || typeof v === "string" && /^(0|off|false|no)$/i.test(v.trim());
9
+ var repoConfigPath = (projectRoot) => join(projectRoot, ".claude", ".semantic-memory", "config");
10
+ function readRepoConfig(projectRoot) {
11
+ try {
12
+ const data = YAML.parse(readFileSync(repoConfigPath(projectRoot), "utf8"));
13
+ return data && typeof data === "object" ? data : {};
14
+ } catch {
15
+ return {};
16
+ }
17
+ }
18
+ var jevLoopDir = (env = process.env) => env.JEV_LOOP_DIR || join(env.HOME || homedir(), ".claude", "jev-loop");
19
+ var claudeSettingsPath = (env = process.env) => env.SEMANTIC_MEMORY_CLAUDE_SETTINGS || join(env.HOME || homedir(), ".claude", "settings.json");
20
+ function onPath(bin, env = process.env) {
21
+ return (env.PATH || "").split(delimiter).some((d) => d && existsSync(join(d, bin)));
22
+ }
23
+ function jevAvailable(env = process.env) {
24
+ const dir = jevLoopDir(env);
25
+ if (!existsSync(join(dir, "router.py"))) return { available: false, reason: `jev-loop not found at ${dir} (router.py missing; set JEV_LOOP_DIR)` };
26
+ if (!env.TYPESAFE_API_KEY) {
27
+ let inSettings = false;
28
+ try {
29
+ inSettings = Boolean(JSON.parse(readFileSync(claudeSettingsPath(env), "utf8"))?.env?.TYPESAFE_API_KEY);
30
+ } catch {
31
+ }
32
+ if (!inSettings) return { available: false, reason: "TYPESAFE_API_KEY is not set (env or ~/.claude/settings.json env)" };
33
+ }
34
+ if (!onPath("python3", env)) return { available: false, reason: "python3 not found on PATH" };
35
+ return { available: true };
36
+ }
37
+ function featureState(feature, projectRoot, env = process.env) {
38
+ let on = false;
39
+ let why = "default";
40
+ const raw = env[ENV[feature]];
41
+ if (raw !== void 0 && raw !== "") {
42
+ on = truthy(raw);
43
+ why = "env";
44
+ }
45
+ if (feature === "skill_loop" && projectRoot) {
46
+ const repo = readRepoConfig(projectRoot).skill_loop;
47
+ if (truthy(repo) || falsy(repo)) {
48
+ on = truthy(repo);
49
+ why = "repo";
50
+ }
51
+ }
52
+ if (feature === "jev" && on) {
53
+ const a = jevAvailable(env);
54
+ return { feature, on, why, ...a };
55
+ }
56
+ return { feature, on, why, available: true };
57
+ }
58
+ var featureOn = (feature, projectRoot, env = process.env) => {
59
+ const s = featureState(feature, projectRoot, env);
60
+ return s.on && s.available;
61
+ };
62
+ function setSettingsEnv(key, value, env = process.env) {
63
+ const path = claudeSettingsPath(env);
64
+ let data = {};
65
+ let raw;
66
+ if (existsSync(path)) {
67
+ raw = readFileSync(path, "utf8");
68
+ try {
69
+ data = raw.trim() ? JSON.parse(raw) : {};
70
+ } catch (e) {
71
+ throw new Error(`${path} is not valid JSON (${e.message}); not changed`);
72
+ }
73
+ if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error(`${path} is not a JSON object; not changed`);
74
+ }
75
+ if (data.env !== void 0 && (typeof data.env !== "object" || data.env === null || Array.isArray(data.env))) {
76
+ throw new Error(`${path}: "env" is not an object; not changed`);
77
+ }
78
+ const nextEnv = { ...data.env ?? {} };
79
+ if (value === void 0) delete nextEnv[key];
80
+ else nextEnv[key] = value;
81
+ data.env = nextEnv;
82
+ mkdirSync(dirname(path), { recursive: true });
83
+ let backup;
84
+ if (raw !== void 0) {
85
+ backup = `${path}.bak`;
86
+ copyFileSync(path, backup);
87
+ }
88
+ const indent = raw?.match(/^[{\[]\s*\n([ \t]+)/)?.[1] ?? " ";
89
+ writeFileSync(path, JSON.stringify(data, null, indent) + "\n");
90
+ return { path, backup };
91
+ }
92
+ function setRepoConfig(projectRoot, key, value) {
93
+ const path = repoConfigPath(projectRoot);
94
+ const data = readRepoConfig(projectRoot);
95
+ if (value === void 0) delete data[key];
96
+ else data[key] = value;
97
+ mkdirSync(dirname(path), { recursive: true });
98
+ writeFileSync(path, Object.keys(data).length ? YAML.stringify(data) : "");
99
+ return path;
100
+ }
101
+ function describeFeature(s) {
102
+ const name = s.feature === "skill_loop" ? "skill loop" : "jev mode";
103
+ const src = s.why === "default" ? "default" : s.why === "env" ? `env ${ENV[s.feature]}` : "repo override in .claude/.semantic-memory/config";
104
+ if (s.on && !s.available) return `${name}: on but unavailable (${s.reason}) [${src}]`;
105
+ return `${name}: ${s.on ? "on" : "off"} [${src}]`;
106
+ }
107
+
108
+ export {
109
+ ENV,
110
+ repoConfigPath,
111
+ readRepoConfig,
112
+ jevLoopDir,
113
+ claudeSettingsPath,
114
+ jevAvailable,
115
+ featureState,
116
+ featureOn,
117
+ setSettingsEnv,
118
+ setRepoConfig,
119
+ describeFeature
120
+ };
121
+ //# sourceMappingURL=chunk-VQCNWRTL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/features.ts"],"sourcesContent":["/**\n * The v1.7 feature switches, both off by default (v1.7):\n *\n * - skill loop (`SEMANTIC_MEMORY_SKILL_LOOP=1`): learn procedures as skills between sessions. A repo can\n * override the global switch with `skill_loop: on | off` in `<repo>/.claude/.semantic-memory/config`.\n * - jev mode (`SEMANTIC_MEMORY_JEV=1`): answer closed-set decisions with Jev through jev-loop. Env only.\n *\n * Standalone (no embedder, no vault): hooks import it from dist/core/features.js on every event, so it\n * must stay cheap — a few stat/read calls.\n */\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, dirname, delimiter } from \"node:path\";\nimport YAML from \"yaml\";\n\nexport type Feature = \"skill_loop\" | \"jev\";\n\nexport interface FeatureState {\n feature: Feature;\n on: boolean;\n /** Where the on/off came from. */\n why: \"default\" | \"env\" | \"repo\";\n /** jev only: whether jev-loop, the key and python are present. Always true for the skill loop. */\n available: boolean;\n /** Why it is unavailable, when it is on but cannot run. */\n reason?: string;\n}\n\nexport const ENV: Record<Feature, string> = { skill_loop: \"SEMANTIC_MEMORY_SKILL_LOOP\", jev: \"SEMANTIC_MEMORY_JEV\" };\n\nconst truthy = (v: unknown) => v === true || v === 1 || (typeof v === \"string\" && /^(1|on|true|yes)$/i.test(v.trim()));\nconst falsy = (v: unknown) => v === false || v === 0 || (typeof v === \"string\" && /^(0|off|false|no)$/i.test(v.trim()));\n\nexport const repoConfigPath = (projectRoot: string) => join(projectRoot, \".claude\", \".semantic-memory\", \"config\");\n\n/** `<repo>/.claude/.semantic-memory/config` as an object ({} when missing or unparseable). */\nexport function readRepoConfig(projectRoot: string): Record<string, unknown> {\n try {\n const data = YAML.parse(readFileSync(repoConfigPath(projectRoot), \"utf8\"));\n return data && typeof data === \"object\" ? data : {};\n } catch {\n return {};\n }\n}\n\nexport const jevLoopDir = (env = process.env) => env.JEV_LOOP_DIR || join(env.HOME || homedir(), \".claude\", \"jev-loop\");\nexport const claudeSettingsPath = (env = process.env) =>\n env.SEMANTIC_MEMORY_CLAUDE_SETTINGS || join(env.HOME || homedir(), \".claude\", \"settings.json\");\n\nfunction onPath(bin: string, env = process.env): boolean {\n return (env.PATH || \"\").split(delimiter).some((d) => d && existsSync(join(d, bin)));\n}\n\n/** Whether jev mode could run: jev-loop's router, the TypeSafe key (env or settings, never read out), python3. */\nexport function jevAvailable(env = process.env): { available: boolean; reason?: string } {\n const dir = jevLoopDir(env);\n if (!existsSync(join(dir, \"router.py\"))) return { available: false, reason: `jev-loop not found at ${dir} (router.py missing; set JEV_LOOP_DIR)` };\n if (!env.TYPESAFE_API_KEY) {\n let inSettings = false;\n try {\n inSettings = Boolean(JSON.parse(readFileSync(claudeSettingsPath(env), \"utf8\"))?.env?.TYPESAFE_API_KEY);\n } catch {}\n if (!inSettings) return { available: false, reason: \"TYPESAFE_API_KEY is not set (env or ~/.claude/settings.json env)\" };\n }\n if (!onPath(\"python3\", env)) return { available: false, reason: \"python3 not found on PATH\" };\n return { available: true };\n}\n\nexport function featureState(feature: Feature, projectRoot?: string, env = process.env): FeatureState {\n let on = false;\n let why: FeatureState[\"why\"] = \"default\";\n const raw = env[ENV[feature]];\n if (raw !== undefined && raw !== \"\") {\n on = truthy(raw);\n why = \"env\";\n }\n if (feature === \"skill_loop\" && projectRoot) {\n const repo = readRepoConfig(projectRoot).skill_loop;\n if (truthy(repo) || falsy(repo)) {\n on = truthy(repo);\n why = \"repo\";\n }\n }\n if (feature === \"jev\" && on) {\n const a = jevAvailable(env);\n return { feature, on, why, ...a };\n }\n return { feature, on, why, available: true };\n}\n\n/** On and able to run. The check every hook path makes first. */\nexport const featureOn = (feature: Feature, projectRoot?: string, env = process.env) => {\n const s = featureState(feature, projectRoot, env);\n return s.on && s.available;\n};\n\n/**\n * Set (true / false) or clear (undefined) one env key in ~/.claude/settings.json, touching nothing else.\n * Keeps a `.bak` of the previous file. Refuses (throws, writes nothing) when the file is not valid JSON.\n */\nexport function setSettingsEnv(key: string, value: string | undefined, env = process.env): { path: string; backup?: string } {\n const path = claudeSettingsPath(env);\n let data: Record<string, any> = {};\n let raw: string | undefined;\n if (existsSync(path)) {\n raw = readFileSync(path, \"utf8\");\n try {\n data = raw.trim() ? JSON.parse(raw) : {};\n } catch (e) {\n throw new Error(`${path} is not valid JSON (${(e as Error).message}); not changed`);\n }\n if (!data || typeof data !== \"object\" || Array.isArray(data)) throw new Error(`${path} is not a JSON object; not changed`);\n }\n if (data.env !== undefined && (typeof data.env !== \"object\" || data.env === null || Array.isArray(data.env))) {\n throw new Error(`${path}: \"env\" is not an object; not changed`);\n }\n const nextEnv = { ...(data.env ?? {}) };\n if (value === undefined) delete nextEnv[key];\n else nextEnv[key] = value;\n data.env = nextEnv;\n mkdirSync(dirname(path), { recursive: true });\n let backup: string | undefined;\n if (raw !== undefined) {\n backup = `${path}.bak`;\n copyFileSync(path, backup);\n }\n const indent = raw?.match(/^[{\\[]\\s*\\n([ \\t]+)/)?.[1] ?? \" \";\n writeFileSync(path, JSON.stringify(data, null, indent) + \"\\n\");\n return { path, backup };\n}\n\n/** Set or clear `<key>: <value>` in the repo config, keeping the other keys. */\nexport function setRepoConfig(projectRoot: string, key: string, value: string | boolean | undefined): string {\n const path = repoConfigPath(projectRoot);\n const data = readRepoConfig(projectRoot);\n if (value === undefined) delete data[key];\n else data[key] = value;\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, Object.keys(data).length ? YAML.stringify(data) : \"\");\n return path;\n}\n\nexport function describeFeature(s: FeatureState): string {\n const name = s.feature === \"skill_loop\" ? \"skill loop\" : \"jev mode\";\n const src = s.why === \"default\" ? \"default\" : s.why === \"env\" ? `env ${ENV[s.feature]}` : \"repo override in .claude/.semantic-memory/config\";\n if (s.on && !s.available) return `${name}: on but unavailable (${s.reason}) [${src}]`;\n return `${name}: ${s.on ? \"on\" : \"off\"} [${src}]`;\n}\n"],"mappings":"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","names":[]}
@@ -248,4 +248,4 @@ export {
248
248
  installDefaultSchema,
249
249
  validateNote
250
250
  };
251
- //# sourceMappingURL=chunk-HUG7E25D.js.map
251
+ //# sourceMappingURL=chunk-Y7H76Q7F.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/schema.ts","../src/core/schema-default.ts"],"sourcesContent":["import { readFile, writeFile, access } from \"node:fs/promises\";\nimport { constants } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport matter from \"./matter.js\";\nimport { DEFAULT_SCHEMA_YAML } from \"./schema-default.js\";\n\nexport interface VaultSchema {\n version: number;\n provenance_fields: string[];\n status_enum: string[];\n confidence_enum: string[];\n severity_enum: string[];\n types: Record<string, { description?: string; required: string[] }>;\n lint: {\n missing_provenance: { applies_to: string[]; severity: \"warn\" | \"error\" };\n stale: { max_age_days: number; severity: \"warn\" | \"error\" };\n schema_violations: { severity: \"warn\" | \"error\" };\n };\n}\n\nexport type Severity = \"warn\" | \"error\";\n\nexport interface LintFinding {\n path: string;\n rule: \"missing_provenance\" | \"stale\" | \"schema_violations\" | \"broken_links\" | \"code_symbols\" | \"decay_candidates\" | \"alias_conflicts\" | \"decoys\" | \"secrets\" | \"skills\";\n severity: Severity;\n message: string;\n}\n\nexport async function loadSchema(vaultPath: string): Promise<VaultSchema> {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n let raw: string;\n try {\n await access(schemaPath, constants.R_OK);\n raw = await readFile(schemaPath, \"utf-8\");\n } catch {\n raw = DEFAULT_SCHEMA_YAML;\n }\n const parsed = parseYaml(raw) as VaultSchema;\n return parsed;\n}\n\n/**\n * Bootstrap the schema file in a vault by writing the default if none exists.\n * Used by the `--install-schema` CLI flag. Returns the path that was (or would be) written.\n */\nexport async function installDefaultSchema(vaultPath: string, force = false): Promise<{ written: boolean; path: string }> {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n if (!force) {\n try {\n await access(schemaPath, constants.F_OK);\n return { written: false, path: schemaPath };\n } catch {\n // not present → write it\n }\n }\n await writeFile(schemaPath, DEFAULT_SCHEMA_YAML, \"utf-8\");\n return { written: true, path: schemaPath };\n}\n\n/**\n * Validate a single note's frontmatter + content against the schema.\n * Returns findings (possibly empty). Does NOT throw — lint never throws.\n */\nexport function validateNote(\n relativePath: string,\n rawFileContent: string,\n schema: VaultSchema,\n opts: { todayIso?: string } = {}\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const { data: fm } = matter(rawFileContent);\n\n const typeName = typeof fm.type === \"string\" ? fm.type : \"note\";\n const typeDef = schema.types[typeName];\n\n if (!typeDef) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `unknown type \"${typeName}\" — allowed: ${Object.keys(schema.types).join(\", \")}`,\n });\n return findings; // can't validate required fields if type is unknown\n }\n\n // Required field check\n for (const field of typeDef.required) {\n if (fm[field] === undefined || fm[field] === null || fm[field] === \"\") {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `missing required field \"${field}\" for type \"${typeName}\"`,\n });\n }\n }\n\n // Enum checks\n if (fm.status !== undefined && !schema.status_enum.includes(String(fm.status))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `status \"${fm.status}\" not in enum [${schema.status_enum.join(\", \")}]`,\n });\n }\n if (fm.confidence !== undefined && !schema.confidence_enum.includes(String(fm.confidence))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `confidence \"${fm.confidence}\" not in enum [${schema.confidence_enum.join(\", \")}]`,\n });\n }\n if (typeName === \"gotcha\" && fm.severity !== undefined && !schema.severity_enum.includes(String(fm.severity))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `severity \"${fm.severity}\" not in enum [${schema.severity_enum.join(\", \")}]`,\n });\n }\n\n // Missing provenance\n if (schema.lint.missing_provenance.applies_to.includes(typeName)) {\n const hasSources = Array.isArray(fm.sources) ? fm.sources.length > 0 : typeof fm.sources === \"string\" && fm.sources.length > 0;\n const hasDerived = Array.isArray(fm.derived_from) ? fm.derived_from.length > 0 : typeof fm.derived_from === \"string\" && fm.derived_from.length > 0;\n if (!hasSources && !hasDerived) {\n findings.push({\n path: relativePath,\n rule: \"missing_provenance\",\n severity: schema.lint.missing_provenance.severity,\n message: `no sources or derived_from on ${typeName}`,\n });\n }\n }\n\n // Stale\n if (schema.lint.stale.max_age_days > 0 && fm.last_verified) {\n const today = opts.todayIso ?? new Date().toISOString().slice(0, 10);\n const verified = String(fm.last_verified);\n const ageDays = dayDiff(today, verified);\n if (ageDays !== null && ageDays > schema.lint.stale.max_age_days) {\n findings.push({\n path: relativePath,\n rule: \"stale\",\n severity: schema.lint.stale.severity,\n message: `last_verified ${verified} is ${ageDays} days old (limit ${schema.lint.stale.max_age_days})`,\n });\n }\n }\n\n return findings;\n}\n\nfunction dayDiff(todayIso: string, thenIso: string): number | null {\n const a = Date.parse(todayIso);\n const b = Date.parse(thenIso);\n if (Number.isNaN(a) || Number.isNaN(b)) return null;\n return Math.round((a - b) / 86400000);\n}\n","// Default vault schema shipped with semantic-sidekick.\n// Intentionally minimal — the plan is to grow from pain, not preempt it.\n// Users can override with a vault.schema.yml at the vault root.\n\nexport const DEFAULT_SCHEMA_YAML = `# semantic-sidekick vault schema (minimal starter).\n# Override by placing a vault.schema.yml at the vault root.\n\nversion: 1\n\n# Provenance fields — apply to all types, enforced by lint.find_missing_provenance.\n# sources: URLs or filesystem paths the note was derived from\n# derived_from: wikilinks to other notes this one synthesizes\n# last_verified: ISO date this note was last confirmed true (YYYY-MM-DD).\n# Set at creation and by verify_note; NOT bumped on ordinary edits.\n# Drives confidence decay (see the decay: block below).\n# evergreen: true → the note is pinned against decay while last_verified is\n# within 365 days; after that it decays normally (re-affirm to reset).\n# status: one of: draft | active | stale | deprecated | archived\n# confidence: one of: high | medium | low\nprovenance_fields:\n - sources\n - derived_from\n - last_verified\n - status\n - confidence\n\n# Allowed status values (enforced by schema_violations lint).\nstatus_enum: [draft, active, stale, deprecated, archived]\nconfidence_enum: [high, medium, low]\n\n# Note types. Each type declares its required frontmatter.\n# Any field not listed as required is allowed but not enforced.\ntypes:\n note:\n description: Generic note — default type when \\`type\\` frontmatter is absent\n required: [title, status]\n\n decision:\n description: Architectural or process decision with dated rationale\n required: [title, status, decided_on, decision_maker]\n\n gotcha:\n description: Compressed lesson from a past incident/bug/surprise\n required: [title, status, last_verified, severity]\n\n source:\n description: External reference (paper, blog post, doc) used as provenance\n required: [title, status, source]\n\n alias:\n description: Learned human→artifact vocabulary entry (v1.4 lexicon). Maps the human's phrasing to a concrete path/symbol.\n required: [canonical]\n\n dossier:\n description: Entity-centric living note (v1.5). One per critical component — purpose, failure modes, knobs, incident log, current state. Accretes in place.\n required: [title, status, entity]\n\n profile:\n description: Speaker profile (v1.5). Models how THIS human communicates — severity calibration, chronic omissions, verbosity, shorthand. One human, one profile.\n required: [title, status]\n\n# Severity values for gotcha type.\nseverity_enum: [low, medium, high, critical]\n\n# Lint policy. Each rule has a default severity that pre-commit/CI consumes.\nlint:\n missing_provenance:\n # Triggers when a note type in this list has no sources AND no derived_from.\n applies_to: [note, decision, gotcha]\n severity: warn\n stale:\n # Triggers when last_verified is older than this many days (0 = disabled).\n max_age_days: 180\n severity: warn\n schema_violations:\n # Triggers on missing required fields, unknown type, or enum mismatch.\n severity: error\n\n# Confidence decay (v1.3). Down-weights search results by time since last_verified.\n# Composes multiplicatively with load_priority — it never removes notes, only ranks\n# them lower (floored). Set enabled: false for byte-identical pre-v1.3 ranking.\ndecay:\n enabled: true\n default_half_life_days: 365 # multiplier = 0.5 ^ (age_days / half_life)\n per_type:\n decision: 365\n note: 365\n gotcha: 180\n source: null # null = never decays\n proposal: 14\n floor: 0.1 # notes never drop below this multiplier\n hotness_boost:\n enabled: false # extend half-life for heavily-backlinked notes (opt-in)\n cap: 2.0\n\n# Path-class ranking (v1.4). Down-weights whole regions of the vault by path glob\n# (composes multiplicatively with decay + load_priority). First matching rule wins.\n# The default down-weights archived notes so retired docs stop dominating results.\npath_class:\n enabled: true\n rules:\n - glob: \"archive/**\"\n multiplier: 0.3\n\n# Usage-feedback ranking (v1.5). Boosts notes that get CITED after retrieval (per\n# the local selection log) so load-bearing knowledge floats up. Bounded by cap to\n# stop a feedback runaway; composes multiplicatively with decay + path_class +\n# load_priority. A no-op until the selection log has citations. The complementary\n# signal (retrieved-but-never-cited \"decoys\") is surfaced by lint_vault, NEVER\n# auto-down-ranked. Set enabled: false for byte-identical pre-v1.5 ranking.\nusage_boost:\n enabled: true\n cap: 1.5 # max multiplier a note can earn from citations\n per_citation: 0.1 # multiplier += per_citation × citation_count, then capped\n\n# Token-frugal tool surface (v1.4). When enabled, the server registers a reduced\n# tool set in modes that don't need the full surface (outage-silence → search/read\n# only). DEFAULT OFF — mid-session mode switches don't re-register, so opt in and\n# dogfood before relying on it.\ntools:\n conditional: false\n\n# Selection-logging telemetry (v1.3.1). Records what search returned and which\n# note the answer used, to a LOCAL append-only JSONL at\n# .claude/.semantic-memory/selection.jsonl (gitignored). Never leaves the machine.\n# Feeds the decay_candidates lint and (later) usage-feedback ranking. Set\n# enabled: false to disable all logging.\ntelemetry:\n enabled: true\n`;\n"],"mappings":"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","names":[]}