@gethmy/mcp 2.4.4 → 2.4.7
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/cli.js +56 -12
- package/dist/index.js +56 -12
- package/dist/lib/api-client.js +29 -1
- package/dist/lib/config.js +5 -1
- package/package.json +1 -1
- package/src/__tests__/memory-audit.test.ts +83 -1
- package/src/__tests__/remote-routing.test.ts +285 -0
- package/src/api-client.ts +40 -1
- package/src/memory-audit.ts +36 -16
- package/src/remote.ts +318 -56
package/dist/cli.js
CHANGED
|
@@ -5,25 +5,43 @@ var __getProtoOf = Object.getPrototypeOf;
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
function __accessProp(key) {
|
|
9
|
+
return this[key];
|
|
10
|
+
}
|
|
11
|
+
var __toESMCache_node;
|
|
12
|
+
var __toESMCache_esm;
|
|
8
13
|
var __toESM = (mod, isNodeMode, target) => {
|
|
14
|
+
var canCache = mod != null && typeof mod === "object";
|
|
15
|
+
if (canCache) {
|
|
16
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
17
|
+
var cached = cache.get(mod);
|
|
18
|
+
if (cached)
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
9
21
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
22
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
23
|
for (let key of __getOwnPropNames(mod))
|
|
12
24
|
if (!__hasOwnProp.call(to, key))
|
|
13
25
|
__defProp(to, key, {
|
|
14
|
-
get: (
|
|
26
|
+
get: __accessProp.bind(mod, key),
|
|
15
27
|
enumerable: true
|
|
16
28
|
});
|
|
29
|
+
if (canCache)
|
|
30
|
+
cache.set(mod, to);
|
|
17
31
|
return to;
|
|
18
32
|
};
|
|
19
33
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
34
|
+
var __returnValue = (v) => v;
|
|
35
|
+
function __exportSetter(name, newValue) {
|
|
36
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
37
|
+
}
|
|
20
38
|
var __export = (target, all) => {
|
|
21
39
|
for (var name in all)
|
|
22
40
|
__defProp(target, name, {
|
|
23
41
|
get: all[name],
|
|
24
42
|
enumerable: true,
|
|
25
43
|
configurable: true,
|
|
26
|
-
set: (
|
|
44
|
+
set: __exportSetter.bind(all, name)
|
|
27
45
|
});
|
|
28
46
|
};
|
|
29
47
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -8942,7 +8960,7 @@ var require_formats = __commonJS((exports) => {
|
|
|
8942
8960
|
}
|
|
8943
8961
|
var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
8944
8962
|
function getTime(strictTimeZone) {
|
|
8945
|
-
return function
|
|
8963
|
+
return function time3(str) {
|
|
8946
8964
|
const matches = TIME.exec(str);
|
|
8947
8965
|
if (!matches)
|
|
8948
8966
|
return false;
|
|
@@ -27052,16 +27070,31 @@ async function requestWithBearer(apiUrl, bearerToken, method, path, body) {
|
|
|
27052
27070
|
return result;
|
|
27053
27071
|
}
|
|
27054
27072
|
|
|
27073
|
+
class HarmonyUnauthorizedError extends Error {
|
|
27074
|
+
constructor(message = "Invalid or expired credentials") {
|
|
27075
|
+
super(message);
|
|
27076
|
+
this.name = "HarmonyUnauthorizedError";
|
|
27077
|
+
}
|
|
27078
|
+
}
|
|
27079
|
+
|
|
27055
27080
|
class HarmonyApiClient {
|
|
27056
27081
|
apiKey;
|
|
27057
27082
|
apiUrl;
|
|
27083
|
+
onUnauthorized;
|
|
27058
27084
|
constructor(options) {
|
|
27059
27085
|
this.apiKey = options?.apiKey ?? getApiKey();
|
|
27060
27086
|
this.apiUrl = options?.apiUrl ?? getApiUrl();
|
|
27087
|
+
this.onUnauthorized = options?.onUnauthorized;
|
|
27061
27088
|
}
|
|
27062
27089
|
getApiUrl() {
|
|
27063
27090
|
return this.apiUrl;
|
|
27064
27091
|
}
|
|
27092
|
+
setApiKey(apiKey) {
|
|
27093
|
+
this.apiKey = apiKey;
|
|
27094
|
+
}
|
|
27095
|
+
getApiKey() {
|
|
27096
|
+
return this.apiKey;
|
|
27097
|
+
}
|
|
27065
27098
|
async request(method, path, body, options) {
|
|
27066
27099
|
await requestSemaphore.acquire();
|
|
27067
27100
|
try {
|
|
@@ -27108,6 +27141,10 @@ class HarmonyApiClient {
|
|
|
27108
27141
|
}
|
|
27109
27142
|
if (!response.ok) {
|
|
27110
27143
|
const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
|
|
27144
|
+
if (response.status === 401) {
|
|
27145
|
+
this.onUnauthorized?.();
|
|
27146
|
+
throw new HarmonyUnauthorizedError(errorMsg);
|
|
27147
|
+
}
|
|
27111
27148
|
if (!isRetryableError(null, response.status)) {
|
|
27112
27149
|
throw new Error(errorMsg);
|
|
27113
27150
|
}
|
|
@@ -27157,6 +27194,10 @@ class HarmonyApiClient {
|
|
|
27157
27194
|
} catch {
|
|
27158
27195
|
errorMsg = text || `API error: ${response.status}`;
|
|
27159
27196
|
}
|
|
27197
|
+
if (response.status === 401) {
|
|
27198
|
+
this.onUnauthorized?.();
|
|
27199
|
+
throw new HarmonyUnauthorizedError(errorMsg);
|
|
27200
|
+
}
|
|
27160
27201
|
if (!isRetryableError(null, response.status)) {
|
|
27161
27202
|
throw new Error(errorMsg);
|
|
27162
27203
|
}
|
|
@@ -28088,23 +28129,25 @@ var BATCH_SIZE = 100;
|
|
|
28088
28129
|
var CONCURRENCY_LIMIT = 5;
|
|
28089
28130
|
var BOILERPLATE_PATTERNS = [
|
|
28090
28131
|
/^todo:?$/i,
|
|
28091
|
-
/^placeholder
|
|
28132
|
+
/^placeholder(\s+\d+|:)?$/i,
|
|
28092
28133
|
/^\.\.\.$/,
|
|
28093
|
-
/^untitled
|
|
28094
|
-
/^(note|memo|draft)\s
|
|
28134
|
+
/^untitled(\s+\d+|:)?$/i,
|
|
28135
|
+
/^(note|memo|draft)\s+\d+$/i,
|
|
28095
28136
|
/^task transition:/i
|
|
28096
28137
|
];
|
|
28097
|
-
function
|
|
28138
|
+
function isBoilerplateTitle(title) {
|
|
28098
28139
|
const t = title.trim();
|
|
28099
|
-
const c = content.trim();
|
|
28100
|
-
if (c.length === 0)
|
|
28101
|
-
return true;
|
|
28102
28140
|
for (const pat of BOILERPLATE_PATTERNS) {
|
|
28103
28141
|
if (pat.test(t))
|
|
28104
28142
|
return true;
|
|
28105
28143
|
}
|
|
28106
28144
|
return false;
|
|
28107
28145
|
}
|
|
28146
|
+
function isBoilerplate(title, content) {
|
|
28147
|
+
if (content.trim().length === 0)
|
|
28148
|
+
return true;
|
|
28149
|
+
return isBoilerplateTitle(title);
|
|
28150
|
+
}
|
|
28108
28151
|
function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraftAgeDays) {
|
|
28109
28152
|
const now = Date.now();
|
|
28110
28153
|
const ageDays = (now - Date.parse(entity.created_at)) / MS_PER_DAY;
|
|
@@ -28186,10 +28229,11 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraf
|
|
|
28186
28229
|
legacy = true;
|
|
28187
28230
|
legacyReasons.push("no graph presence");
|
|
28188
28231
|
}
|
|
28232
|
+
const boilerplateTitle = isBoilerplateTitle(entity.title);
|
|
28189
28233
|
let bucket;
|
|
28190
|
-
if (
|
|
28234
|
+
if (boilerplateTitle && deleteBelow > 0) {
|
|
28191
28235
|
bucket = "delete";
|
|
28192
|
-
reasons.push("boilerplate override");
|
|
28236
|
+
reasons.push("boilerplate title override");
|
|
28193
28237
|
} else if (score < deleteBelow)
|
|
28194
28238
|
bucket = "delete";
|
|
28195
28239
|
else if (score < archiveBelow)
|
package/dist/index.js
CHANGED
|
@@ -5,25 +5,43 @@ var __getProtoOf = Object.getPrototypeOf;
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
function __accessProp(key) {
|
|
9
|
+
return this[key];
|
|
10
|
+
}
|
|
11
|
+
var __toESMCache_node;
|
|
12
|
+
var __toESMCache_esm;
|
|
8
13
|
var __toESM = (mod, isNodeMode, target) => {
|
|
14
|
+
var canCache = mod != null && typeof mod === "object";
|
|
15
|
+
if (canCache) {
|
|
16
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
17
|
+
var cached = cache.get(mod);
|
|
18
|
+
if (cached)
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
9
21
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
22
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
23
|
for (let key of __getOwnPropNames(mod))
|
|
12
24
|
if (!__hasOwnProp.call(to, key))
|
|
13
25
|
__defProp(to, key, {
|
|
14
|
-
get: (
|
|
26
|
+
get: __accessProp.bind(mod, key),
|
|
15
27
|
enumerable: true
|
|
16
28
|
});
|
|
29
|
+
if (canCache)
|
|
30
|
+
cache.set(mod, to);
|
|
17
31
|
return to;
|
|
18
32
|
};
|
|
19
33
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
34
|
+
var __returnValue = (v) => v;
|
|
35
|
+
function __exportSetter(name, newValue) {
|
|
36
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
37
|
+
}
|
|
20
38
|
var __export = (target, all) => {
|
|
21
39
|
for (var name in all)
|
|
22
40
|
__defProp(target, name, {
|
|
23
41
|
get: all[name],
|
|
24
42
|
enumerable: true,
|
|
25
43
|
configurable: true,
|
|
26
|
-
set: (
|
|
44
|
+
set: __exportSetter.bind(all, name)
|
|
27
45
|
});
|
|
28
46
|
};
|
|
29
47
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -6849,7 +6867,7 @@ var require_formats = __commonJS((exports) => {
|
|
|
6849
6867
|
}
|
|
6850
6868
|
var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
6851
6869
|
function getTime(strictTimeZone) {
|
|
6852
|
-
return function
|
|
6870
|
+
return function time3(str) {
|
|
6853
6871
|
const matches = TIME.exec(str);
|
|
6854
6872
|
if (!matches)
|
|
6855
6873
|
return false;
|
|
@@ -24812,16 +24830,31 @@ async function requestWithBearer(apiUrl, bearerToken, method, path, body) {
|
|
|
24812
24830
|
return result;
|
|
24813
24831
|
}
|
|
24814
24832
|
|
|
24833
|
+
class HarmonyUnauthorizedError extends Error {
|
|
24834
|
+
constructor(message = "Invalid or expired credentials") {
|
|
24835
|
+
super(message);
|
|
24836
|
+
this.name = "HarmonyUnauthorizedError";
|
|
24837
|
+
}
|
|
24838
|
+
}
|
|
24839
|
+
|
|
24815
24840
|
class HarmonyApiClient {
|
|
24816
24841
|
apiKey;
|
|
24817
24842
|
apiUrl;
|
|
24843
|
+
onUnauthorized;
|
|
24818
24844
|
constructor(options) {
|
|
24819
24845
|
this.apiKey = options?.apiKey ?? getApiKey();
|
|
24820
24846
|
this.apiUrl = options?.apiUrl ?? getApiUrl();
|
|
24847
|
+
this.onUnauthorized = options?.onUnauthorized;
|
|
24821
24848
|
}
|
|
24822
24849
|
getApiUrl() {
|
|
24823
24850
|
return this.apiUrl;
|
|
24824
24851
|
}
|
|
24852
|
+
setApiKey(apiKey) {
|
|
24853
|
+
this.apiKey = apiKey;
|
|
24854
|
+
}
|
|
24855
|
+
getApiKey() {
|
|
24856
|
+
return this.apiKey;
|
|
24857
|
+
}
|
|
24825
24858
|
async request(method, path, body, options) {
|
|
24826
24859
|
await requestSemaphore.acquire();
|
|
24827
24860
|
try {
|
|
@@ -24868,6 +24901,10 @@ class HarmonyApiClient {
|
|
|
24868
24901
|
}
|
|
24869
24902
|
if (!response.ok) {
|
|
24870
24903
|
const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
|
|
24904
|
+
if (response.status === 401) {
|
|
24905
|
+
this.onUnauthorized?.();
|
|
24906
|
+
throw new HarmonyUnauthorizedError(errorMsg);
|
|
24907
|
+
}
|
|
24871
24908
|
if (!isRetryableError(null, response.status)) {
|
|
24872
24909
|
throw new Error(errorMsg);
|
|
24873
24910
|
}
|
|
@@ -24917,6 +24954,10 @@ class HarmonyApiClient {
|
|
|
24917
24954
|
} catch {
|
|
24918
24955
|
errorMsg = text || `API error: ${response.status}`;
|
|
24919
24956
|
}
|
|
24957
|
+
if (response.status === 401) {
|
|
24958
|
+
this.onUnauthorized?.();
|
|
24959
|
+
throw new HarmonyUnauthorizedError(errorMsg);
|
|
24960
|
+
}
|
|
24920
24961
|
if (!isRetryableError(null, response.status)) {
|
|
24921
24962
|
throw new Error(errorMsg);
|
|
24922
24963
|
}
|
|
@@ -25848,23 +25889,25 @@ var BATCH_SIZE = 100;
|
|
|
25848
25889
|
var CONCURRENCY_LIMIT = 5;
|
|
25849
25890
|
var BOILERPLATE_PATTERNS = [
|
|
25850
25891
|
/^todo:?$/i,
|
|
25851
|
-
/^placeholder
|
|
25892
|
+
/^placeholder(\s+\d+|:)?$/i,
|
|
25852
25893
|
/^\.\.\.$/,
|
|
25853
|
-
/^untitled
|
|
25854
|
-
/^(note|memo|draft)\s
|
|
25894
|
+
/^untitled(\s+\d+|:)?$/i,
|
|
25895
|
+
/^(note|memo|draft)\s+\d+$/i,
|
|
25855
25896
|
/^task transition:/i
|
|
25856
25897
|
];
|
|
25857
|
-
function
|
|
25898
|
+
function isBoilerplateTitle(title) {
|
|
25858
25899
|
const t = title.trim();
|
|
25859
|
-
const c = content.trim();
|
|
25860
|
-
if (c.length === 0)
|
|
25861
|
-
return true;
|
|
25862
25900
|
for (const pat of BOILERPLATE_PATTERNS) {
|
|
25863
25901
|
if (pat.test(t))
|
|
25864
25902
|
return true;
|
|
25865
25903
|
}
|
|
25866
25904
|
return false;
|
|
25867
25905
|
}
|
|
25906
|
+
function isBoilerplate(title, content) {
|
|
25907
|
+
if (content.trim().length === 0)
|
|
25908
|
+
return true;
|
|
25909
|
+
return isBoilerplateTitle(title);
|
|
25910
|
+
}
|
|
25868
25911
|
function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraftAgeDays) {
|
|
25869
25912
|
const now = Date.now();
|
|
25870
25913
|
const ageDays = (now - Date.parse(entity.created_at)) / MS_PER_DAY;
|
|
@@ -25946,10 +25989,11 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraf
|
|
|
25946
25989
|
legacy = true;
|
|
25947
25990
|
legacyReasons.push("no graph presence");
|
|
25948
25991
|
}
|
|
25992
|
+
const boilerplateTitle = isBoilerplateTitle(entity.title);
|
|
25949
25993
|
let bucket;
|
|
25950
|
-
if (
|
|
25994
|
+
if (boilerplateTitle && deleteBelow > 0) {
|
|
25951
25995
|
bucket = "delete";
|
|
25952
|
-
reasons.push("boilerplate override");
|
|
25996
|
+
reasons.push("boilerplate title override");
|
|
25953
25997
|
} else if (score < deleteBelow)
|
|
25954
25998
|
bucket = "delete";
|
|
25955
25999
|
else if (score < archiveBelow)
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
|
+
var __returnValue = (v) => v;
|
|
3
|
+
function __exportSetter(name, newValue) {
|
|
4
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
5
|
+
}
|
|
2
6
|
var __export = (target, all) => {
|
|
3
7
|
for (var name in all)
|
|
4
8
|
__defProp(target, name, {
|
|
5
9
|
get: all[name],
|
|
6
10
|
enumerable: true,
|
|
7
11
|
configurable: true,
|
|
8
|
-
set: (
|
|
12
|
+
set: __exportSetter.bind(all, name)
|
|
9
13
|
});
|
|
10
14
|
};
|
|
11
15
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -1570,16 +1574,31 @@ async function requestWithBearer(apiUrl, bearerToken, method, path, body) {
|
|
|
1570
1574
|
return result;
|
|
1571
1575
|
}
|
|
1572
1576
|
|
|
1577
|
+
class HarmonyUnauthorizedError extends Error {
|
|
1578
|
+
constructor(message = "Invalid or expired credentials") {
|
|
1579
|
+
super(message);
|
|
1580
|
+
this.name = "HarmonyUnauthorizedError";
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1573
1584
|
class HarmonyApiClient {
|
|
1574
1585
|
apiKey;
|
|
1575
1586
|
apiUrl;
|
|
1587
|
+
onUnauthorized;
|
|
1576
1588
|
constructor(options) {
|
|
1577
1589
|
this.apiKey = options?.apiKey ?? getApiKey();
|
|
1578
1590
|
this.apiUrl = options?.apiUrl ?? getApiUrl();
|
|
1591
|
+
this.onUnauthorized = options?.onUnauthorized;
|
|
1579
1592
|
}
|
|
1580
1593
|
getApiUrl() {
|
|
1581
1594
|
return this.apiUrl;
|
|
1582
1595
|
}
|
|
1596
|
+
setApiKey(apiKey) {
|
|
1597
|
+
this.apiKey = apiKey;
|
|
1598
|
+
}
|
|
1599
|
+
getApiKey() {
|
|
1600
|
+
return this.apiKey;
|
|
1601
|
+
}
|
|
1583
1602
|
async request(method, path, body, options) {
|
|
1584
1603
|
await requestSemaphore.acquire();
|
|
1585
1604
|
try {
|
|
@@ -1626,6 +1645,10 @@ class HarmonyApiClient {
|
|
|
1626
1645
|
}
|
|
1627
1646
|
if (!response.ok) {
|
|
1628
1647
|
const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
|
|
1648
|
+
if (response.status === 401) {
|
|
1649
|
+
this.onUnauthorized?.();
|
|
1650
|
+
throw new HarmonyUnauthorizedError(errorMsg);
|
|
1651
|
+
}
|
|
1629
1652
|
if (!isRetryableError(null, response.status)) {
|
|
1630
1653
|
throw new Error(errorMsg);
|
|
1631
1654
|
}
|
|
@@ -1675,6 +1698,10 @@ class HarmonyApiClient {
|
|
|
1675
1698
|
} catch {
|
|
1676
1699
|
errorMsg = text || `API error: ${response.status}`;
|
|
1677
1700
|
}
|
|
1701
|
+
if (response.status === 401) {
|
|
1702
|
+
this.onUnauthorized?.();
|
|
1703
|
+
throw new HarmonyUnauthorizedError(errorMsg);
|
|
1704
|
+
}
|
|
1678
1705
|
if (!isRetryableError(null, response.status)) {
|
|
1679
1706
|
throw new Error(errorMsg);
|
|
1680
1707
|
}
|
|
@@ -2136,5 +2163,6 @@ export {
|
|
|
2136
2163
|
resetClient,
|
|
2137
2164
|
requestWithBearer,
|
|
2138
2165
|
getClient,
|
|
2166
|
+
HarmonyUnauthorizedError,
|
|
2139
2167
|
HarmonyApiClient
|
|
2140
2168
|
};
|
package/dist/lib/config.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
|
+
var __returnValue = (v) => v;
|
|
3
|
+
function __exportSetter(name, newValue) {
|
|
4
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
5
|
+
}
|
|
2
6
|
var __export = (target, all) => {
|
|
3
7
|
for (var name in all)
|
|
4
8
|
__defProp(target, name, {
|
|
5
9
|
get: all[name],
|
|
6
10
|
enumerable: true,
|
|
7
11
|
configurable: true,
|
|
8
|
-
set: (
|
|
12
|
+
set: __exportSetter.bind(all, name)
|
|
9
13
|
});
|
|
10
14
|
};
|
|
11
15
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
package/package.json
CHANGED
|
@@ -312,7 +312,89 @@ describe("runMemoryAudit", () => {
|
|
|
312
312
|
});
|
|
313
313
|
expect(report.summary.delete).toBe(1);
|
|
314
314
|
expect(deletedIds).toContain("promoted-junk");
|
|
315
|
-
expect(report.lowest[0].reasons).toContain("boilerplate override");
|
|
315
|
+
expect(report.lowest[0].reasons).toContain("boilerplate title override");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("legitimate titles starting with boilerplate-prefix words are NOT deleted", async () => {
|
|
319
|
+
// Regression test for the over-broad regex bug. Pre-fix patterns matched
|
|
320
|
+
// any title starting with "Placeholder", "Untitled", "Note", etc. After
|
|
321
|
+
// tightening, only exact boilerplate forms (with optional digit suffix
|
|
322
|
+
// or colon) match — real titles survive.
|
|
323
|
+
const { client, deletedIds } = makeMockClient(
|
|
324
|
+
[
|
|
325
|
+
{
|
|
326
|
+
id: "legit-placeholder",
|
|
327
|
+
type: "pattern",
|
|
328
|
+
title: "Placeholder pattern in React Suspense",
|
|
329
|
+
content:
|
|
330
|
+
"Use React.Suspense with a fallback component as the placeholder pattern for streaming SSR.",
|
|
331
|
+
confidence: 0.9,
|
|
332
|
+
memory_tier: "reference",
|
|
333
|
+
access_count: 12,
|
|
334
|
+
last_accessed_at: daysAgo(1),
|
|
335
|
+
created_at: daysAgo(60),
|
|
336
|
+
tags: ["react", "ssr"],
|
|
337
|
+
embedding: [0.1],
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
id: "legit-untitled",
|
|
341
|
+
type: "context",
|
|
342
|
+
title: "UntitledMaster.fig — design source for the homepage",
|
|
343
|
+
content:
|
|
344
|
+
"Reference Figma file containing master components for landing page assets.",
|
|
345
|
+
confidence: 0.85,
|
|
346
|
+
memory_tier: "reference",
|
|
347
|
+
access_count: 8,
|
|
348
|
+
last_accessed_at: daysAgo(2),
|
|
349
|
+
created_at: daysAgo(45),
|
|
350
|
+
tags: ["design"],
|
|
351
|
+
embedding: [0.1],
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: "legit-note",
|
|
355
|
+
type: "context",
|
|
356
|
+
title: "Note: schema migration order matters",
|
|
357
|
+
content: "Always run 0042 before 0043 because of FK dependency.",
|
|
358
|
+
confidence: 0.8,
|
|
359
|
+
memory_tier: "reference",
|
|
360
|
+
access_count: 5,
|
|
361
|
+
last_accessed_at: daysAgo(3),
|
|
362
|
+
created_at: daysAgo(30),
|
|
363
|
+
tags: ["db"],
|
|
364
|
+
embedding: [0.1],
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
{ "legit-placeholder": 3, "legit-untitled": 2, "legit-note": 1 },
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const report = await runMemoryAudit(client, "ws-1", undefined, {
|
|
371
|
+
dryRun: false,
|
|
372
|
+
});
|
|
373
|
+
expect(deletedIds).toHaveLength(0);
|
|
374
|
+
expect(report.summary.delete).toBe(0);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("empty-content draft with real title is NOT delete-bucketed", async () => {
|
|
378
|
+
// Users sometimes save a draft with title only and fill content later.
|
|
379
|
+
// The override is title-only, so empty content alone must not delete.
|
|
380
|
+
const { client, deletedIds } = makeMockClient([
|
|
381
|
+
{
|
|
382
|
+
id: "draft-empty-body",
|
|
383
|
+
type: "decision",
|
|
384
|
+
title: "Decision: skip Q3 launch",
|
|
385
|
+
content: "",
|
|
386
|
+
confidence: 0.7,
|
|
387
|
+
memory_tier: "draft",
|
|
388
|
+
access_count: 1,
|
|
389
|
+
last_accessed_at: daysAgo(1),
|
|
390
|
+
created_at: daysAgo(2),
|
|
391
|
+
tags: ["q3"],
|
|
392
|
+
embedding: null,
|
|
393
|
+
},
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
await runMemoryAudit(client, "ws-1", undefined, { dryRun: false });
|
|
397
|
+
expect(deletedIds).not.toContain("draft-empty-body");
|
|
316
398
|
});
|
|
317
399
|
|
|
318
400
|
test("boilerplate override respects deleteBelow=0 escape hatch", async () => {
|