@velvetmonkey/flywheel-memory 2.0.11 → 2.0.13
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/dist/index.js +256 -27
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2731,6 +2731,15 @@ import {
|
|
|
2731
2731
|
// src/core/write/wikilinkFeedback.ts
|
|
2732
2732
|
var MIN_FEEDBACK_COUNT = 10;
|
|
2733
2733
|
var SUPPRESSION_THRESHOLD = 0.3;
|
|
2734
|
+
var FEEDBACK_BOOST_MIN_SAMPLES = 5;
|
|
2735
|
+
var FOLDER_SUPPRESSION_MIN_COUNT = 5;
|
|
2736
|
+
var FEEDBACK_BOOST_TIERS = [
|
|
2737
|
+
{ minAccuracy: 0.95, minSamples: 20, boost: 5 },
|
|
2738
|
+
{ minAccuracy: 0.8, minSamples: 5, boost: 2 },
|
|
2739
|
+
{ minAccuracy: 0.6, minSamples: 5, boost: 0 },
|
|
2740
|
+
{ minAccuracy: 0.4, minSamples: 5, boost: -2 },
|
|
2741
|
+
{ minAccuracy: 0, minSamples: 5, boost: -4 }
|
|
2742
|
+
];
|
|
2734
2743
|
function recordFeedback(stateDb2, entity, context, notePath, correct) {
|
|
2735
2744
|
stateDb2.db.prepare(
|
|
2736
2745
|
"INSERT INTO wikilink_feedback (entity, context, note_path, correct) VALUES (?, ?, ?, ?)"
|
|
@@ -2814,11 +2823,31 @@ function updateSuppressionList(stateDb2) {
|
|
|
2814
2823
|
transaction();
|
|
2815
2824
|
return updated;
|
|
2816
2825
|
}
|
|
2817
|
-
function isSuppressed(stateDb2, entity) {
|
|
2826
|
+
function isSuppressed(stateDb2, entity, folder) {
|
|
2818
2827
|
const row = stateDb2.db.prepare(
|
|
2819
2828
|
"SELECT entity FROM wikilink_suppressions WHERE entity = ?"
|
|
2820
2829
|
).get(entity);
|
|
2821
|
-
return
|
|
2830
|
+
if (row) return true;
|
|
2831
|
+
if (folder !== void 0) {
|
|
2832
|
+
const folderStats = stateDb2.db.prepare(`
|
|
2833
|
+
SELECT
|
|
2834
|
+
COUNT(*) as total,
|
|
2835
|
+
SUM(CASE WHEN correct = 0 THEN 1 ELSE 0 END) as false_positives
|
|
2836
|
+
FROM wikilink_feedback
|
|
2837
|
+
WHERE entity = ? AND (
|
|
2838
|
+
CASE WHEN ? = '' THEN note_path NOT LIKE '%/%'
|
|
2839
|
+
ELSE note_path LIKE ? || '/%'
|
|
2840
|
+
END
|
|
2841
|
+
)
|
|
2842
|
+
`).get(entity, folder, folder);
|
|
2843
|
+
if (folderStats && folderStats.total >= FOLDER_SUPPRESSION_MIN_COUNT) {
|
|
2844
|
+
const fpRate = folderStats.false_positives / folderStats.total;
|
|
2845
|
+
if (fpRate >= SUPPRESSION_THRESHOLD) {
|
|
2846
|
+
return true;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
return false;
|
|
2822
2851
|
}
|
|
2823
2852
|
function getSuppressedCount(stateDb2) {
|
|
2824
2853
|
const row = stateDb2.db.prepare(
|
|
@@ -2826,6 +2855,109 @@ function getSuppressedCount(stateDb2) {
|
|
|
2826
2855
|
).get();
|
|
2827
2856
|
return row.count;
|
|
2828
2857
|
}
|
|
2858
|
+
function computeBoostFromAccuracy(accuracy, sampleCount) {
|
|
2859
|
+
if (sampleCount < FEEDBACK_BOOST_MIN_SAMPLES) return 0;
|
|
2860
|
+
for (const tier of FEEDBACK_BOOST_TIERS) {
|
|
2861
|
+
if (accuracy >= tier.minAccuracy && sampleCount >= tier.minSamples) {
|
|
2862
|
+
return tier.boost;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
return 0;
|
|
2866
|
+
}
|
|
2867
|
+
function getAllFeedbackBoosts(stateDb2, folder) {
|
|
2868
|
+
const globalRows = stateDb2.db.prepare(`
|
|
2869
|
+
SELECT
|
|
2870
|
+
entity,
|
|
2871
|
+
COUNT(*) as total,
|
|
2872
|
+
SUM(CASE WHEN correct = 1 THEN 1 ELSE 0 END) as correct_count
|
|
2873
|
+
FROM wikilink_feedback
|
|
2874
|
+
GROUP BY entity
|
|
2875
|
+
HAVING total >= ?
|
|
2876
|
+
`).all(FEEDBACK_BOOST_MIN_SAMPLES);
|
|
2877
|
+
let folderStats = null;
|
|
2878
|
+
if (folder !== void 0) {
|
|
2879
|
+
const folderRows = stateDb2.db.prepare(`
|
|
2880
|
+
SELECT
|
|
2881
|
+
entity,
|
|
2882
|
+
COUNT(*) as total,
|
|
2883
|
+
SUM(CASE WHEN correct = 1 THEN 1 ELSE 0 END) as correct_count
|
|
2884
|
+
FROM wikilink_feedback
|
|
2885
|
+
WHERE (
|
|
2886
|
+
CASE WHEN ? = '' THEN note_path NOT LIKE '%/%'
|
|
2887
|
+
ELSE note_path LIKE ? || '/%'
|
|
2888
|
+
END
|
|
2889
|
+
)
|
|
2890
|
+
GROUP BY entity
|
|
2891
|
+
HAVING total >= ?
|
|
2892
|
+
`).all(folder, folder, FEEDBACK_BOOST_MIN_SAMPLES);
|
|
2893
|
+
folderStats = /* @__PURE__ */ new Map();
|
|
2894
|
+
for (const row of folderRows) {
|
|
2895
|
+
folderStats.set(row.entity, {
|
|
2896
|
+
accuracy: row.correct_count / row.total,
|
|
2897
|
+
count: row.total
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
2902
|
+
for (const row of globalRows) {
|
|
2903
|
+
let accuracy;
|
|
2904
|
+
let sampleCount;
|
|
2905
|
+
const fs25 = folderStats?.get(row.entity);
|
|
2906
|
+
if (fs25 && fs25.count >= FEEDBACK_BOOST_MIN_SAMPLES) {
|
|
2907
|
+
accuracy = fs25.accuracy;
|
|
2908
|
+
sampleCount = fs25.count;
|
|
2909
|
+
} else {
|
|
2910
|
+
accuracy = row.correct_count / row.total;
|
|
2911
|
+
sampleCount = row.total;
|
|
2912
|
+
}
|
|
2913
|
+
const boost = computeBoostFromAccuracy(accuracy, sampleCount);
|
|
2914
|
+
if (boost !== 0) {
|
|
2915
|
+
boosts.set(row.entity, boost);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
return boosts;
|
|
2919
|
+
}
|
|
2920
|
+
function trackWikilinkApplications(stateDb2, notePath, entities) {
|
|
2921
|
+
const upsert = stateDb2.db.prepare(`
|
|
2922
|
+
INSERT INTO wikilink_applications (entity, note_path, applied_at, status)
|
|
2923
|
+
VALUES (?, ?, datetime('now'), 'applied')
|
|
2924
|
+
ON CONFLICT(entity, note_path) DO UPDATE SET
|
|
2925
|
+
applied_at = datetime('now'),
|
|
2926
|
+
status = 'applied'
|
|
2927
|
+
`);
|
|
2928
|
+
const transaction = stateDb2.db.transaction(() => {
|
|
2929
|
+
for (const entity of entities) {
|
|
2930
|
+
upsert.run(entity.toLowerCase(), notePath);
|
|
2931
|
+
}
|
|
2932
|
+
});
|
|
2933
|
+
transaction();
|
|
2934
|
+
}
|
|
2935
|
+
function getTrackedApplications(stateDb2, notePath) {
|
|
2936
|
+
const rows = stateDb2.db.prepare(
|
|
2937
|
+
`SELECT entity FROM wikilink_applications WHERE note_path = ? AND status = 'applied'`
|
|
2938
|
+
).all(notePath);
|
|
2939
|
+
return rows.map((r) => r.entity);
|
|
2940
|
+
}
|
|
2941
|
+
function processImplicitFeedback(stateDb2, notePath, currentContent) {
|
|
2942
|
+
const tracked = getTrackedApplications(stateDb2, notePath);
|
|
2943
|
+
if (tracked.length === 0) return [];
|
|
2944
|
+
const currentLinks = extractLinkedEntities(currentContent);
|
|
2945
|
+
const removed = [];
|
|
2946
|
+
const markRemoved = stateDb2.db.prepare(
|
|
2947
|
+
`UPDATE wikilink_applications SET status = 'removed' WHERE entity = ? AND note_path = ?`
|
|
2948
|
+
);
|
|
2949
|
+
const transaction = stateDb2.db.transaction(() => {
|
|
2950
|
+
for (const entity of tracked) {
|
|
2951
|
+
if (!currentLinks.has(entity)) {
|
|
2952
|
+
recordFeedback(stateDb2, entity, "implicit:removed", notePath, false);
|
|
2953
|
+
markRemoved.run(entity, notePath);
|
|
2954
|
+
removed.push(entity);
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
});
|
|
2958
|
+
transaction();
|
|
2959
|
+
return removed;
|
|
2960
|
+
}
|
|
2829
2961
|
|
|
2830
2962
|
// src/core/write/git.ts
|
|
2831
2963
|
import { simpleGit, CheckRepoActions } from "simple-git";
|
|
@@ -4174,6 +4306,9 @@ function setWriteStateDb(stateDb2) {
|
|
|
4174
4306
|
setHintsStateDb(stateDb2);
|
|
4175
4307
|
setRecencyStateDb(stateDb2);
|
|
4176
4308
|
}
|
|
4309
|
+
function getWriteStateDb() {
|
|
4310
|
+
return moduleStateDb4;
|
|
4311
|
+
}
|
|
4177
4312
|
var entityIndex = null;
|
|
4178
4313
|
var indexReady = false;
|
|
4179
4314
|
var indexError2 = null;
|
|
@@ -4320,9 +4455,10 @@ function processWikilinks(content, notePath) {
|
|
|
4320
4455
|
let entities = getAllEntities(entityIndex);
|
|
4321
4456
|
console.error(`[Flywheel:DEBUG] Processing wikilinks with ${entities.length} entities`);
|
|
4322
4457
|
if (moduleStateDb4) {
|
|
4458
|
+
const folder = notePath ? notePath.split("/")[0] : void 0;
|
|
4323
4459
|
entities = entities.filter((e) => {
|
|
4324
4460
|
const name = getEntityName2(e);
|
|
4325
|
-
return !isSuppressed(moduleStateDb4, name);
|
|
4461
|
+
return !isSuppressed(moduleStateDb4, name, folder);
|
|
4326
4462
|
});
|
|
4327
4463
|
}
|
|
4328
4464
|
const sortedEntities = sortEntitiesByPriority(entities, notePath);
|
|
@@ -4346,6 +4482,9 @@ function maybeApplyWikilinks(content, skipWikilinks, notePath) {
|
|
|
4346
4482
|
checkAndRefreshIfStale();
|
|
4347
4483
|
const result = processWikilinks(content, notePath);
|
|
4348
4484
|
if (result.linksAdded > 0) {
|
|
4485
|
+
if (moduleStateDb4 && notePath) {
|
|
4486
|
+
trackWikilinkApplications(moduleStateDb4, notePath, result.linkedEntities);
|
|
4487
|
+
}
|
|
4349
4488
|
return {
|
|
4350
4489
|
content: result.content,
|
|
4351
4490
|
wikilinkInfo: `Applied ${result.linksAdded} wikilink(s): ${result.linkedEntities.join(", ")}`
|
|
@@ -4663,7 +4802,8 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
4663
4802
|
maxSuggestions = 3,
|
|
4664
4803
|
excludeLinked = true,
|
|
4665
4804
|
strictness = DEFAULT_STRICTNESS,
|
|
4666
|
-
notePath
|
|
4805
|
+
notePath,
|
|
4806
|
+
detail = false
|
|
4667
4807
|
} = options;
|
|
4668
4808
|
const config = STRICTNESS_CONFIGS[strictness];
|
|
4669
4809
|
const adaptiveMinScore = getAdaptiveMinScore(content.length, config.minSuggestionScore);
|
|
@@ -4697,6 +4837,8 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
4697
4837
|
return emptyResult;
|
|
4698
4838
|
}
|
|
4699
4839
|
const linkedEntities = excludeLinked ? extractLinkedEntities(content) : /* @__PURE__ */ new Set();
|
|
4840
|
+
const noteFolder = notePath ? notePath.split("/")[0] : void 0;
|
|
4841
|
+
const feedbackBoosts = moduleStateDb4 ? getAllFeedbackBoosts(moduleStateDb4, noteFolder) : /* @__PURE__ */ new Map();
|
|
4700
4842
|
const scoredEntities = [];
|
|
4701
4843
|
const directlyMatchedEntities = /* @__PURE__ */ new Set();
|
|
4702
4844
|
const entitiesWithContentMatch = /* @__PURE__ */ new Set();
|
|
@@ -4717,20 +4859,38 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
4717
4859
|
if (contentScore > 0) {
|
|
4718
4860
|
entitiesWithContentMatch.add(entityName);
|
|
4719
4861
|
}
|
|
4720
|
-
|
|
4721
|
-
score +=
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4862
|
+
const layerTypeBoost = TYPE_BOOST[category] || 0;
|
|
4863
|
+
score += layerTypeBoost;
|
|
4864
|
+
const layerContextBoost = contextBoosts[category] || 0;
|
|
4865
|
+
score += layerContextBoost;
|
|
4866
|
+
const layerRecencyBoost = recencyIndex ? getRecencyBoost(entityName, recencyIndex) : 0;
|
|
4867
|
+
score += layerRecencyBoost;
|
|
4868
|
+
const layerCrossFolderBoost = notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
|
|
4869
|
+
score += layerCrossFolderBoost;
|
|
4870
|
+
const layerHubBoost = getHubBoost(entity);
|
|
4871
|
+
score += layerHubBoost;
|
|
4872
|
+
const layerFeedbackAdj = feedbackBoosts.get(entityName) ?? 0;
|
|
4873
|
+
score += layerFeedbackAdj;
|
|
4729
4874
|
if (score > 0) {
|
|
4730
4875
|
directlyMatchedEntities.add(entityName);
|
|
4731
4876
|
}
|
|
4732
4877
|
if (score >= adaptiveMinScore) {
|
|
4733
|
-
scoredEntities.push({
|
|
4878
|
+
scoredEntities.push({
|
|
4879
|
+
name: entityName,
|
|
4880
|
+
path: entity.path || "",
|
|
4881
|
+
score,
|
|
4882
|
+
category,
|
|
4883
|
+
breakdown: {
|
|
4884
|
+
contentMatch: contentScore,
|
|
4885
|
+
cooccurrenceBoost: 0,
|
|
4886
|
+
typeBoost: layerTypeBoost,
|
|
4887
|
+
contextBoost: layerContextBoost,
|
|
4888
|
+
recencyBoost: layerRecencyBoost,
|
|
4889
|
+
crossFolderBoost: layerCrossFolderBoost,
|
|
4890
|
+
hubBoost: layerHubBoost,
|
|
4891
|
+
feedbackAdjustment: layerFeedbackAdj
|
|
4892
|
+
}
|
|
4893
|
+
});
|
|
4734
4894
|
}
|
|
4735
4895
|
}
|
|
4736
4896
|
if (cooccurrenceIndex && directlyMatchedEntities.size > 0) {
|
|
@@ -4745,6 +4905,7 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
4745
4905
|
const existing = scoredEntities.find((e) => e.name === entityName);
|
|
4746
4906
|
if (existing) {
|
|
4747
4907
|
existing.score += boost;
|
|
4908
|
+
existing.breakdown.cooccurrenceBoost += boost;
|
|
4748
4909
|
} else {
|
|
4749
4910
|
const entityTokens = tokenize(entityName);
|
|
4750
4911
|
const hasContentOverlap = entityTokens.some(
|
|
@@ -4759,9 +4920,25 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
4759
4920
|
const recencyBoostVal = recencyIndex ? getRecencyBoost(entityName, recencyIndex) : 0;
|
|
4760
4921
|
const crossFolderBoost = notePath && entity.path ? getCrossFolderBoost(entity.path, notePath) : 0;
|
|
4761
4922
|
const hubBoost = getHubBoost(entity);
|
|
4762
|
-
const
|
|
4923
|
+
const feedbackAdj = feedbackBoosts.get(entityName) ?? 0;
|
|
4924
|
+
const totalBoost = boost + typeBoost + contextBoost + recencyBoostVal + crossFolderBoost + hubBoost + feedbackAdj;
|
|
4763
4925
|
if (totalBoost >= adaptiveMinScore) {
|
|
4764
|
-
scoredEntities.push({
|
|
4926
|
+
scoredEntities.push({
|
|
4927
|
+
name: entityName,
|
|
4928
|
+
path: entity.path || "",
|
|
4929
|
+
score: totalBoost,
|
|
4930
|
+
category,
|
|
4931
|
+
breakdown: {
|
|
4932
|
+
contentMatch: 0,
|
|
4933
|
+
cooccurrenceBoost: boost,
|
|
4934
|
+
typeBoost,
|
|
4935
|
+
contextBoost,
|
|
4936
|
+
recencyBoost: recencyBoostVal,
|
|
4937
|
+
crossFolderBoost,
|
|
4938
|
+
hubBoost,
|
|
4939
|
+
feedbackAdjustment: feedbackAdj
|
|
4940
|
+
}
|
|
4941
|
+
});
|
|
4765
4942
|
}
|
|
4766
4943
|
}
|
|
4767
4944
|
}
|
|
@@ -4782,15 +4959,34 @@ function suggestRelatedLinks(content, options = {}) {
|
|
|
4782
4959
|
}
|
|
4783
4960
|
return 0;
|
|
4784
4961
|
});
|
|
4785
|
-
const
|
|
4962
|
+
const topEntries = relevantEntities.slice(0, maxSuggestions);
|
|
4963
|
+
const topSuggestions = topEntries.map((e) => e.name);
|
|
4786
4964
|
if (topSuggestions.length === 0) {
|
|
4787
4965
|
return emptyResult;
|
|
4788
4966
|
}
|
|
4789
4967
|
const suffix = "\u2192 " + topSuggestions.map((name) => `[[${name}]]`).join(", ");
|
|
4790
|
-
|
|
4968
|
+
const result = {
|
|
4791
4969
|
suggestions: topSuggestions,
|
|
4792
4970
|
suffix
|
|
4793
4971
|
};
|
|
4972
|
+
if (detail) {
|
|
4973
|
+
const feedbackStats = moduleStateDb4 ? getEntityStats(moduleStateDb4) : [];
|
|
4974
|
+
const feedbackMap = new Map(feedbackStats.map((s) => [s.entity, s]));
|
|
4975
|
+
result.detailed = topEntries.map((e) => {
|
|
4976
|
+
const fb = feedbackMap.get(e.name);
|
|
4977
|
+
const confidence = e.score >= 20 ? "high" : e.score >= 12 ? "medium" : "low";
|
|
4978
|
+
return {
|
|
4979
|
+
entity: e.name,
|
|
4980
|
+
path: e.path,
|
|
4981
|
+
totalScore: e.score,
|
|
4982
|
+
breakdown: e.breakdown,
|
|
4983
|
+
confidence,
|
|
4984
|
+
feedbackCount: fb?.total ?? 0,
|
|
4985
|
+
accuracy: fb ? fb.accuracy : void 0
|
|
4986
|
+
};
|
|
4987
|
+
});
|
|
4988
|
+
}
|
|
4989
|
+
return result;
|
|
4794
4990
|
}
|
|
4795
4991
|
function detectAliasCollisions(noteName, aliases = []) {
|
|
4796
4992
|
if (!moduleStateDb4) return [];
|
|
@@ -5616,11 +5812,12 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
5616
5812
|
inputSchema: {
|
|
5617
5813
|
text: z2.string().describe("The text to analyze for potential wikilinks"),
|
|
5618
5814
|
limit: z2.coerce.number().default(50).describe("Maximum number of suggestions to return"),
|
|
5619
|
-
offset: z2.coerce.number().default(0).describe("Number of suggestions to skip (for pagination)")
|
|
5815
|
+
offset: z2.coerce.number().default(0).describe("Number of suggestions to skip (for pagination)"),
|
|
5816
|
+
detail: z2.boolean().default(false).describe("Include per-layer score breakdown for each suggestion")
|
|
5620
5817
|
},
|
|
5621
5818
|
outputSchema: SuggestWikilinksOutputSchema
|
|
5622
5819
|
},
|
|
5623
|
-
async ({ text, limit: requestedLimit, offset }) => {
|
|
5820
|
+
async ({ text, limit: requestedLimit, offset, detail }) => {
|
|
5624
5821
|
const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
|
|
5625
5822
|
const index = getIndex();
|
|
5626
5823
|
const allMatches = findEntityMatches(text, index.entities);
|
|
@@ -5631,6 +5828,16 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
5631
5828
|
returned_count: matches.length,
|
|
5632
5829
|
suggestions: matches
|
|
5633
5830
|
};
|
|
5831
|
+
if (detail) {
|
|
5832
|
+
const scored = suggestRelatedLinks(text, {
|
|
5833
|
+
detail: true,
|
|
5834
|
+
maxSuggestions: limit,
|
|
5835
|
+
strictness: "balanced"
|
|
5836
|
+
});
|
|
5837
|
+
if (scored.detailed) {
|
|
5838
|
+
output.scored_suggestions = scored.detailed;
|
|
5839
|
+
}
|
|
5840
|
+
}
|
|
5634
5841
|
return {
|
|
5635
5842
|
content: [
|
|
5636
5843
|
{
|
|
@@ -9103,6 +9310,10 @@ async function withVaultFile(options, operation) {
|
|
|
9103
9310
|
return formatMcpResult(existsError);
|
|
9104
9311
|
}
|
|
9105
9312
|
const { content, frontmatter, lineEnding } = await readVaultFile(vaultPath2, notePath);
|
|
9313
|
+
const writeStateDb = getWriteStateDb();
|
|
9314
|
+
if (writeStateDb) {
|
|
9315
|
+
processImplicitFeedback(writeStateDb, notePath, content);
|
|
9316
|
+
}
|
|
9106
9317
|
let sectionBoundary;
|
|
9107
9318
|
if (section) {
|
|
9108
9319
|
const sectionResult = ensureSectionExists(content, section, notePath);
|
|
@@ -12013,9 +12224,12 @@ var ALL_METRICS = [
|
|
|
12013
12224
|
"entity_count",
|
|
12014
12225
|
"avg_links_per_note",
|
|
12015
12226
|
"link_density",
|
|
12016
|
-
"connected_ratio"
|
|
12227
|
+
"connected_ratio",
|
|
12228
|
+
"wikilink_accuracy",
|
|
12229
|
+
"wikilink_feedback_volume",
|
|
12230
|
+
"wikilink_suppressed_count"
|
|
12017
12231
|
];
|
|
12018
|
-
function computeMetrics(index) {
|
|
12232
|
+
function computeMetrics(index, stateDb2) {
|
|
12019
12233
|
const noteCount = index.notes.size;
|
|
12020
12234
|
let linkCount = 0;
|
|
12021
12235
|
for (const note of index.notes.values()) {
|
|
@@ -12050,6 +12264,18 @@ function computeMetrics(index) {
|
|
|
12050
12264
|
const possibleLinks = noteCount * (noteCount - 1);
|
|
12051
12265
|
const linkDensity = possibleLinks > 0 ? linkCount / possibleLinks : 0;
|
|
12052
12266
|
const connectedRatio = noteCount > 0 ? connectedNotes.size / noteCount : 0;
|
|
12267
|
+
let wikilinkAccuracy = 0;
|
|
12268
|
+
let wikilinkFeedbackVolume = 0;
|
|
12269
|
+
let wikilinkSuppressedCount = 0;
|
|
12270
|
+
if (stateDb2) {
|
|
12271
|
+
const entityStatsList = getEntityStats(stateDb2);
|
|
12272
|
+
wikilinkFeedbackVolume = entityStatsList.reduce((sum, s) => sum + s.total, 0);
|
|
12273
|
+
if (wikilinkFeedbackVolume > 0) {
|
|
12274
|
+
const totalCorrect = entityStatsList.reduce((sum, s) => sum + s.correct, 0);
|
|
12275
|
+
wikilinkAccuracy = Math.round(totalCorrect / wikilinkFeedbackVolume * 1e3) / 1e3;
|
|
12276
|
+
}
|
|
12277
|
+
wikilinkSuppressedCount = getSuppressedCount(stateDb2);
|
|
12278
|
+
}
|
|
12053
12279
|
return {
|
|
12054
12280
|
note_count: noteCount,
|
|
12055
12281
|
link_count: linkCount,
|
|
@@ -12058,7 +12284,10 @@ function computeMetrics(index) {
|
|
|
12058
12284
|
entity_count: entityCount,
|
|
12059
12285
|
avg_links_per_note: Math.round(avgLinksPerNote * 100) / 100,
|
|
12060
12286
|
link_density: Math.round(linkDensity * 1e4) / 1e4,
|
|
12061
|
-
connected_ratio: Math.round(connectedRatio * 1e3) / 1e3
|
|
12287
|
+
connected_ratio: Math.round(connectedRatio * 1e3) / 1e3,
|
|
12288
|
+
wikilink_accuracy: wikilinkAccuracy,
|
|
12289
|
+
wikilink_feedback_volume: wikilinkFeedbackVolume,
|
|
12290
|
+
wikilink_suppressed_count: wikilinkSuppressedCount
|
|
12062
12291
|
};
|
|
12063
12292
|
}
|
|
12064
12293
|
function recordMetrics(stateDb2, metrics) {
|
|
@@ -12148,7 +12377,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
12148
12377
|
"vault_growth",
|
|
12149
12378
|
{
|
|
12150
12379
|
title: "Vault Growth",
|
|
12151
|
-
description: 'Track vault growth over time. Modes: "current" (live snapshot), "history" (time series), "trends" (deltas vs N days ago). Tracks
|
|
12380
|
+
description: 'Track vault growth over time. Modes: "current" (live snapshot), "history" (time series), "trends" (deltas vs N days ago). Tracks 11 metrics: note_count, link_count, orphan_count, tag_count, entity_count, avg_links_per_note, link_density, connected_ratio, wikilink_accuracy, wikilink_feedback_volume, wikilink_suppressed_count.',
|
|
12152
12381
|
inputSchema: {
|
|
12153
12382
|
mode: z21.enum(["current", "history", "trends"]).describe("Query mode: current snapshot, historical time series, or trend analysis"),
|
|
12154
12383
|
metric: z21.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
|
|
@@ -12162,7 +12391,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
12162
12391
|
let result;
|
|
12163
12392
|
switch (mode) {
|
|
12164
12393
|
case "current": {
|
|
12165
|
-
const metrics = computeMetrics(index);
|
|
12394
|
+
const metrics = computeMetrics(index, stateDb2 ?? void 0);
|
|
12166
12395
|
result = {
|
|
12167
12396
|
mode: "current",
|
|
12168
12397
|
metrics,
|
|
@@ -12189,7 +12418,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
12189
12418
|
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available for trend analysis" }) }]
|
|
12190
12419
|
};
|
|
12191
12420
|
}
|
|
12192
|
-
const currentMetrics = computeMetrics(index);
|
|
12421
|
+
const currentMetrics = computeMetrics(index, stateDb2);
|
|
12193
12422
|
const trends = computeTrends(stateDb2, currentMetrics, daysBack);
|
|
12194
12423
|
result = {
|
|
12195
12424
|
mode: "trends",
|
|
@@ -12618,7 +12847,7 @@ async function runPostIndexWork(index) {
|
|
|
12618
12847
|
await exportHubScores(index, stateDb);
|
|
12619
12848
|
if (stateDb) {
|
|
12620
12849
|
try {
|
|
12621
|
-
const metrics = computeMetrics(index);
|
|
12850
|
+
const metrics = computeMetrics(index, stateDb);
|
|
12622
12851
|
recordMetrics(stateDb, metrics);
|
|
12623
12852
|
purgeOldMetrics(stateDb, 90);
|
|
12624
12853
|
console.error("[Memory] Growth metrics recorded");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.13",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. 36 tools for search, backlinks, graph queries, and mutations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
53
|
-
"@velvetmonkey/vault-core": "^2.0.
|
|
53
|
+
"@velvetmonkey/vault-core": "^2.0.13",
|
|
54
54
|
"better-sqlite3": "^11.0.0",
|
|
55
55
|
"chokidar": "^4.0.0",
|
|
56
56
|
"gray-matter": "^4.0.3",
|