pmem-ai 0.7.4 → 0.7.6

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 (100) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +45 -0
  3. package/dist/commands/context.d.ts.map +1 -1
  4. package/dist/commands/context.js +34 -19
  5. package/dist/commands/context.js.map +1 -1
  6. package/dist/commands/decision.d.ts +8 -0
  7. package/dist/commands/decision.d.ts.map +1 -0
  8. package/dist/commands/decision.js +112 -0
  9. package/dist/commands/decision.js.map +1 -0
  10. package/dist/commands/init.d.ts.map +1 -1
  11. package/dist/commands/init.js +4 -0
  12. package/dist/commands/init.js.map +1 -1
  13. package/dist/commands/module.d.ts +8 -0
  14. package/dist/commands/module.d.ts.map +1 -0
  15. package/dist/commands/module.js +108 -0
  16. package/dist/commands/module.js.map +1 -0
  17. package/dist/commands/new.js +1 -1
  18. package/dist/commands/new.js.map +1 -1
  19. package/dist/commands/rebuild.d.ts.map +1 -1
  20. package/dist/commands/rebuild.js +57 -2
  21. package/dist/commands/rebuild.js.map +1 -1
  22. package/dist/commands/recall.d.ts +4 -1
  23. package/dist/commands/recall.d.ts.map +1 -1
  24. package/dist/commands/recall.js +14 -128
  25. package/dist/commands/recall.js.map +1 -1
  26. package/dist/commands/relations.d.ts +46 -0
  27. package/dist/commands/relations.d.ts.map +1 -0
  28. package/dist/commands/relations.js +166 -0
  29. package/dist/commands/relations.js.map +1 -0
  30. package/dist/commands/status.d.ts.map +1 -1
  31. package/dist/commands/status.js +154 -4
  32. package/dist/commands/status.js.map +1 -1
  33. package/dist/commands/sync.d.ts.map +1 -1
  34. package/dist/commands/sync.js +6 -1
  35. package/dist/commands/sync.js.map +1 -1
  36. package/dist/commands/update.d.ts +18 -0
  37. package/dist/commands/update.d.ts.map +1 -1
  38. package/dist/commands/update.js +191 -46
  39. package/dist/commands/update.js.map +1 -1
  40. package/dist/commands/verify.d.ts.map +1 -1
  41. package/dist/commands/verify.js +377 -280
  42. package/dist/commands/verify.js.map +1 -1
  43. package/dist/core/capture.d.ts.map +1 -1
  44. package/dist/core/capture.js +72 -55
  45. package/dist/core/capture.js.map +1 -1
  46. package/dist/core/consistency.d.ts.map +1 -1
  47. package/dist/core/consistency.js +2 -0
  48. package/dist/core/consistency.js.map +1 -1
  49. package/dist/core/decisionInfer.d.ts +27 -0
  50. package/dist/core/decisionInfer.d.ts.map +1 -0
  51. package/dist/core/decisionInfer.js +190 -0
  52. package/dist/core/decisionInfer.js.map +1 -0
  53. package/dist/core/format.js +134 -11
  54. package/dist/core/format.js.map +1 -1
  55. package/dist/core/fs.d.ts +27 -0
  56. package/dist/core/fs.d.ts.map +1 -1
  57. package/dist/core/fs.js +72 -0
  58. package/dist/core/fs.js.map +1 -1
  59. package/dist/core/moduleInfer.d.ts +13 -0
  60. package/dist/core/moduleInfer.d.ts.map +1 -0
  61. package/dist/core/moduleInfer.js +252 -0
  62. package/dist/core/moduleInfer.js.map +1 -0
  63. package/dist/core/next.d.ts +30 -0
  64. package/dist/core/next.d.ts.map +1 -0
  65. package/dist/core/next.js +193 -0
  66. package/dist/core/next.js.map +1 -0
  67. package/dist/core/query/context.d.ts.map +1 -1
  68. package/dist/core/query/context.js +35 -0
  69. package/dist/core/query/context.js.map +1 -1
  70. package/dist/core/query/recall.d.ts +28 -0
  71. package/dist/core/query/recall.d.ts.map +1 -1
  72. package/dist/core/query/recall.js +131 -25
  73. package/dist/core/query/recall.js.map +1 -1
  74. package/dist/core/query/status.d.ts +5 -1
  75. package/dist/core/query/status.d.ts.map +1 -1
  76. package/dist/core/query/status.js +143 -3
  77. package/dist/core/query/status.js.map +1 -1
  78. package/dist/core/state.d.ts +6 -0
  79. package/dist/core/state.d.ts.map +1 -0
  80. package/dist/core/state.js +147 -0
  81. package/dist/core/state.js.map +1 -0
  82. package/dist/core/symbols.d.ts +2 -0
  83. package/dist/core/symbols.d.ts.map +1 -0
  84. package/dist/core/symbols.js +147 -0
  85. package/dist/core/symbols.js.map +1 -0
  86. package/dist/core/traceParse.d.ts +14 -0
  87. package/dist/core/traceParse.d.ts.map +1 -0
  88. package/dist/core/traceParse.js +80 -0
  89. package/dist/core/traceParse.js.map +1 -0
  90. package/dist/core/traceSummary.d.ts +28 -0
  91. package/dist/core/traceSummary.d.ts.map +1 -0
  92. package/dist/core/traceSummary.js +265 -0
  93. package/dist/core/traceSummary.js.map +1 -0
  94. package/dist/index.js +67 -2
  95. package/dist/index.js.map +1 -1
  96. package/dist/mcp/server.js +2 -2
  97. package/dist/types.d.ts +69 -0
  98. package/dist/types.d.ts.map +1 -1
  99. package/package.json +4 -2
  100. package/skills/pmem/SKILL.md +17 -1
@@ -61,341 +61,438 @@ function verifyCommand(options) {
61
61
  fix: 'Run: pmem init',
62
62
  });
63
63
  }
64
- // 2. Check SQLite DB exists
65
- const dbPath = path.join(pmemPath, 'pmem.db');
66
- const dbExists = (0, fs_1.fileExists)(dbPath);
67
- let db = null;
68
- if (!dbExists) {
69
- issues.push({
70
- severity: 'warning',
71
- type: 'missing_database',
72
- message: '.pmem/pmem.db not found.',
73
- fix: 'Run: pmem rebuild',
74
- });
75
- }
76
- else {
77
- try {
78
- db = (0, db_1.openDatabase)(pmemPath);
79
- (0, db_1.createSchema)(db);
80
- }
81
- catch (err) {
82
- issues.push({
83
- severity: 'error',
84
- type: 'corrupt_database',
85
- message: err?.message || '.pmem/pmem.db is corrupted.',
86
- fix: 'Back up the file if needed, then run: pmem rebuild --full',
87
- });
88
- db = null;
89
- }
90
- }
91
- // 2b. Lock status check
64
+ // 2b. Lock status check (read-only)
65
+ //
66
+ // v0.7.6 FIX-1 (issue #9): restructured the lock block. The old code
67
+ // emitted an `active_lock` warning here based purely on lock presence.
68
+ // The new flow instead tries to *acquire* the lock with a short timeout
69
+ // (see below) — if it cannot, another process is rebuilding right now
70
+ // and we defer stale-index checks instead of producing a transient
71
+ // `stale_index` warning. The `stale_lock` / `stale_lock_cleaned`
72
+ // classification still exists so users can still detect + clean
73
+ // crashed pmem processes that left a lock behind.
92
74
  const lockPath = path.join(pmemPath, '.lock');
93
75
  const lockStatus = (0, fs_1.getLockStatus)(lockPath);
94
- if (lockStatus.exists) {
95
- if (lockStatus.stale) {
96
- const ageSec = lockStatus.age !== null ? Math.round(lockStatus.age / 1000) : '?';
97
- if (options.fixLocks) {
98
- (0, fs_1.breakLock)(lockPath);
99
- issues.push({
100
- severity: 'warning',
101
- type: 'stale_lock_cleaned',
102
- message: `Stale lock at .pmem/.lock (age: ${ageSec}s) was cleaned.`,
103
- fix: 'Lock has been removed. You can now run pmem commands.',
104
- });
105
- }
106
- else {
107
- issues.push({
108
- severity: 'warning',
109
- type: 'stale_lock',
110
- message: `Stale lock detected at .pmem/.lock (age: ${ageSec}s).`,
111
- fix: 'Run: pmem verify --fix-locks (to clean stale lock)\n Or: pmem doctor (to diagnose lock status)',
112
- });
113
- }
114
- }
115
- else if (lockStatus.age !== null) {
116
- const ageSec = Math.round(lockStatus.age / 1000);
76
+ if (lockStatus.exists && lockStatus.stale) {
77
+ const ageSec = lockStatus.age !== null ? Math.round(lockStatus.age / 1000) : '?';
78
+ if (options.fixLocks) {
79
+ (0, fs_1.breakLock)(lockPath);
117
80
  issues.push({
118
81
  severity: 'warning',
119
- type: 'active_lock',
120
- message: `Active lock at .pmem/.lock (age: ${ageSec}s). Another pmem process may be running.`,
121
- fix: 'Wait for the other process to finish. If no other process is running, run: pmem verify --fix-locks',
82
+ type: 'stale_lock_cleaned',
83
+ message: `Stale lock at .pmem/.lock (age: ${ageSec}s) was cleaned.`,
84
+ fix: 'Lock has been removed. You can now run pmem commands.',
122
85
  });
123
86
  }
124
- }
125
- if (manifest) {
126
- // 3. Check schema version
127
- const currentSchema = manifest.pmem?.schema_version;
128
- if (!currentSchema) {
87
+ else {
129
88
  issues.push({
130
89
  severity: 'warning',
131
- type: 'missing_schema_version',
132
- message: 'Manifest is missing pmem.schema_version.',
133
- fix: 'Run: pmem migrate --to 0.3',
90
+ type: 'stale_lock',
91
+ message: `Stale lock detected at .pmem/.lock (age: ${ageSec}s).`,
92
+ fix: 'Run: pmem verify --fix-locks (to clean stale lock)\n Or: pmem doctor (to diagnose lock status)',
134
93
  });
135
94
  }
136
- else if (currentSchema < '0.3') {
137
- issues.push({
138
- severity: 'warning',
139
- type: 'old_schema_version',
140
- message: `Project schema version is ${currentSchema}. Current CLI supports 0.3.`,
141
- fix: 'Run: pmem migrate --to 0.3',
142
- });
95
+ }
96
+ // v0.7.6 FIX-1 (issue #9): try to acquire the lock with a short timeout
97
+ // so a concurrent `pmem rebuild` cannot tear the SQLite index out from
98
+ // under us while we read it (which would produce a transient `stale_index`
99
+ // warning that disappears on the next verify). If we cannot acquire the
100
+ // lock, surface a single info-level `active_lock` note and skip the
101
+ // freshness checks entirely.
102
+ const lockAcquired = (0, fs_1.acquireLock)(lockPath, 500);
103
+ if (!lockAcquired) {
104
+ const ageSec = lockStatus.age !== null ? Math.round(lockStatus.age / 1000) : '?';
105
+ issues.push({
106
+ severity: 'info',
107
+ type: 'active_lock',
108
+ message: `Active lock at .pmem/.lock (age: ${ageSec}s). Another pmem process is running — deferring index freshness checks.`,
109
+ fix: 'Wait for the other pmem process to finish, then re-run: pmem verify',
110
+ });
111
+ const errors = issues.filter(i => i.severity === 'error');
112
+ const warnings = issues.filter(i => i.severity === 'warning');
113
+ const infos = issues.filter(i => i.severity === 'info');
114
+ const passed = errors.length === 0;
115
+ const score = Math.max(0, 100 - errors.length * 30 - warnings.length * 5);
116
+ const result = { passed, score, issues };
117
+ if (passed && warnings.length === 0) {
118
+ console.log(`Memory Verify Result: clean (index checks deferred).`);
119
+ console.log(`Score: ${score}/100`);
120
+ if (infos.length > 0) {
121
+ console.log('');
122
+ console.log('Informational Notes:');
123
+ for (const issue of infos) {
124
+ console.log(`ℹ [${issue.type}] ${issue.message}`);
125
+ }
126
+ }
127
+ return;
128
+ }
129
+ console.log(`Memory Verify Result: ${passed ? 'Warnings found' : 'Failed'}`);
130
+ console.log(`Score: ${score}/100`);
131
+ console.log('');
132
+ for (const issue of issues) {
133
+ let icon = 'ℹ';
134
+ if (issue.severity === 'error')
135
+ icon = '✗';
136
+ else if (issue.severity === 'warning')
137
+ icon = '⚠';
138
+ console.log(`${icon} [${issue.type}] ${issue.message}`);
139
+ console.log(` Fix: ${issue.fix}`);
140
+ console.log('');
143
141
  }
144
- else if (currentSchema > '0.3') {
142
+ const hasErrors = issues.some(i => i.severity === 'error');
143
+ if (hasErrors) {
144
+ if (options.noExit)
145
+ return;
146
+ process.exit(2);
147
+ }
148
+ if (options.noExit)
149
+ return;
150
+ process.exit(0);
151
+ }
152
+ // v0.7.6 FIX-1 (issue #9): wrap the bulk of verify (manifest/schema/hash/
153
+ // policy checks + auto-fix) in a try/finally so the lock acquired above
154
+ // is always released — even on a thrown error or an auto-fix subprocess
155
+ // exit. The early-return `active_lock` branch above already bails before
156
+ // reaching this block, so it does not need its own release.
157
+ try {
158
+ // 2. Check SQLite DB exists (v0.7.6 FIX-1: moved here from before lock
159
+ // acquisition so the active_lock fast path never sees a transient
160
+ // missing_database warning when rebuild is busy creating the index).
161
+ const dbPath = path.join(pmemPath, 'pmem.db');
162
+ const dbExists = (0, fs_1.fileExists)(dbPath);
163
+ let db = null;
164
+ if (!dbExists) {
145
165
  issues.push({
146
- severity: 'error',
147
- type: 'newer_schema_version',
148
- message: `Project schema version is ${currentSchema}. Current CLI only supports up to 0.3.`,
149
- fix: 'Please upgrade pmem CLI to a newer version.',
166
+ severity: 'warning',
167
+ type: 'missing_database',
168
+ message: '.pmem/pmem.db not found.',
169
+ fix: 'Run: pmem rebuild',
150
170
  });
151
171
  }
152
- if (db) {
153
- const cards = db.prepare('SELECT * FROM cards WHERE is_deleted = 0').all();
154
- // 4. Hash consistency — compare DB file_hash against actual .md file content
155
- for (const card of cards) {
156
- const cardFilePath = path.join(cwd, card.file_path);
157
- if (!(0, fs_1.fileExists)(cardFilePath)) {
158
- issues.push({
159
- severity: 'warning',
160
- type: 'missing_card_file',
161
- message: `Card "${card.id}" references missing file: ${card.file_path}`,
162
- fix: 'Run: pmem rebuild',
163
- });
164
- continue;
165
- }
166
- const content = (0, fs_1.readFile)(cardFilePath);
167
- if (!content)
168
- continue;
169
- const currentFileHash = (0, hash_1.computeHash)(content);
170
- if (currentFileHash !== card.file_hash) {
171
- issues.push({
172
- severity: 'warning',
173
- type: 'stale_index',
174
- message: `Card "${card.id}" file hash mismatch (stored: ${card.file_hash}, current: ${currentFileHash}).`,
175
- fix: 'Run: pmem rebuild',
176
- });
177
- }
172
+ else {
173
+ try {
174
+ db = (0, db_1.openDatabase)(pmemPath);
175
+ (0, db_1.createSchema)(db);
178
176
  }
179
- // 5. Orphan edges — edges referencing non-existent card IDs
180
- const orphanFrom = db.prepare('SELECT e.* FROM edges e LEFT JOIN cards c ON e.from_id = c.id WHERE c.id IS NULL').all();
181
- const orphanTo = db.prepare('SELECT e.* FROM edges e LEFT JOIN cards c ON e.to_id = c.id WHERE c.id IS NULL').all();
182
- const orphanEdgeSet = new Map();
183
- for (const e of orphanFrom) {
184
- if (e.id !== undefined)
185
- orphanEdgeSet.set(e.id, e);
177
+ catch (err) {
178
+ issues.push({
179
+ severity: 'error',
180
+ type: 'corrupt_database',
181
+ message: err?.message || '.pmem/pmem.db is corrupted.',
182
+ fix: 'Back up the file if needed, then run: pmem rebuild --full',
183
+ });
184
+ db = null;
186
185
  }
187
- for (const e of orphanTo) {
188
- if (e.id !== undefined && !orphanEdgeSet.has(e.id))
189
- orphanEdgeSet.set(e.id, e);
186
+ }
187
+ if (manifest) {
188
+ // 3. Check schema version
189
+ const currentSchema = manifest.pmem?.schema_version;
190
+ if (!currentSchema) {
191
+ issues.push({
192
+ severity: 'warning',
193
+ type: 'missing_schema_version',
194
+ message: 'Manifest is missing pmem.schema_version.',
195
+ fix: 'Run: pmem migrate --to 0.3',
196
+ });
190
197
  }
191
- if (orphanEdgeSet.size > 0) {
198
+ else if (currentSchema < '0.3') {
192
199
  issues.push({
193
200
  severity: 'warning',
194
- type: 'orphan_edges',
195
- message: `${orphanEdgeSet.size} edge(s) reference non-existent card IDs.`,
196
- fix: 'Run: pmem rebuild',
201
+ type: 'old_schema_version',
202
+ message: `Project schema version is ${currentSchema}. Current CLI supports 0.3.`,
203
+ fix: 'Run: pmem migrate --to 0.3',
204
+ });
205
+ }
206
+ else if (currentSchema > '0.3') {
207
+ issues.push({
208
+ severity: 'error',
209
+ type: 'newer_schema_version',
210
+ message: `Project schema version is ${currentSchema}. Current CLI only supports up to 0.3.`,
211
+ fix: 'Please upgrade pmem CLI to a newer version.',
197
212
  });
198
213
  }
199
- // 6. Card policy checks
200
- if (manifest.card_policy) {
201
- const policy = manifest.card_policy;
202
- // 6a. ID naming pattern — v0.7.0: render {types} placeholder if present
203
- const config = (0, manifest_1.resolveConfig)(manifest);
204
- const renderedPattern = (0, manifest_1.renderIdPattern)(policy.id_pattern, config.card_types);
205
- const idRegex = new RegExp(renderedPattern);
214
+ if (db) {
215
+ const cards = db.prepare('SELECT * FROM cards WHERE is_deleted = 0').all();
216
+ // 4. Hash consistency — compare DB file_hash against actual .md file content
206
217
  for (const card of cards) {
207
- if (!idRegex.test(card.id)) {
218
+ const cardFilePath = path.join(cwd, card.file_path);
219
+ if (!(0, fs_1.fileExists)(cardFilePath)) {
208
220
  issues.push({
209
221
  severity: 'warning',
210
- type: 'card_id_violation',
211
- message: `Card "${card.id}" does not match naming pattern.`,
212
- fix: `Rename card ID to match: ${renderedPattern}`,
222
+ type: 'missing_card_file',
223
+ message: `Card "${card.id}" references missing file: ${card.file_path}`,
224
+ fix: 'Run: pmem rebuild',
225
+ });
226
+ continue;
227
+ }
228
+ const content = (0, fs_1.readFile)(cardFilePath);
229
+ if (!content)
230
+ continue;
231
+ const currentFileHash = (0, hash_1.computeHash)(content);
232
+ if (currentFileHash !== card.file_hash) {
233
+ issues.push({
234
+ severity: 'warning',
235
+ type: 'stale_index',
236
+ message: `Card "${card.id}" file hash mismatch (stored: ${card.file_hash}, current: ${currentFileHash}).`,
237
+ fix: 'Run: pmem rebuild',
213
238
  });
214
239
  }
215
240
  }
216
- // 6b. Token count limits read files and estimate tokens
217
- for (const card of cards) {
218
- const filePath = path.join(cwd, card.file_path);
219
- const content = (0, fs_1.readFile)(filePath);
220
- if (content) {
221
- const estimatedTokens = (0, hash_1.tokenCount)(content);
222
- const maxForType = policy.max_tokens[card.type];
223
- if (maxForType && estimatedTokens > maxForType) {
224
- // Check if relaxed locally (frontmatter or manifest list)
225
- const isLocalRelaxed = (() => {
226
- const parsed = (0, yaml_1.parseFrontmatter)(content);
227
- if (parsed?.data) {
228
- if (parsed.data.relaxed === true || parsed.data.token_policy === 'relaxed') {
241
+ // 5. Orphan edgesedges referencing non-existent card IDs
242
+ const orphanFrom = db.prepare('SELECT e.* FROM edges e LEFT JOIN cards c ON e.from_id = c.id WHERE c.id IS NULL').all();
243
+ const orphanTo = db.prepare('SELECT e.* FROM edges e LEFT JOIN cards c ON e.to_id = c.id WHERE c.id IS NULL').all();
244
+ const orphanEdgeSet = new Map();
245
+ for (const e of orphanFrom) {
246
+ if (e.id !== undefined)
247
+ orphanEdgeSet.set(e.id, e);
248
+ }
249
+ for (const e of orphanTo) {
250
+ if (e.id !== undefined && !orphanEdgeSet.has(e.id))
251
+ orphanEdgeSet.set(e.id, e);
252
+ }
253
+ if (orphanEdgeSet.size > 0) {
254
+ issues.push({
255
+ severity: 'warning',
256
+ type: 'orphan_edges',
257
+ message: `${orphanEdgeSet.size} edge(s) reference non-existent card IDs.`,
258
+ fix: 'Run: pmem rebuild',
259
+ });
260
+ }
261
+ // 6. Card policy checks
262
+ if (manifest.card_policy) {
263
+ const policy = manifest.card_policy;
264
+ // 6a. ID naming pattern — v0.7.0: render {types} placeholder if present
265
+ const config = (0, manifest_1.resolveConfig)(manifest);
266
+ const renderedPattern = (0, manifest_1.renderIdPattern)(policy.id_pattern, config.card_types);
267
+ const idRegex = new RegExp(renderedPattern);
268
+ for (const card of cards) {
269
+ if (!idRegex.test(card.id)) {
270
+ issues.push({
271
+ severity: 'warning',
272
+ type: 'card_id_violation',
273
+ message: `Card "${card.id}" does not match naming pattern.`,
274
+ fix: `Rename card ID to match: ${renderedPattern}`,
275
+ });
276
+ }
277
+ }
278
+ // 6b. Token count limits — read files and estimate tokens
279
+ for (const card of cards) {
280
+ const filePath = path.join(cwd, card.file_path);
281
+ const content = (0, fs_1.readFile)(filePath);
282
+ if (content) {
283
+ const estimatedTokens = (0, hash_1.tokenCount)(content);
284
+ const maxForType = policy.max_tokens[card.type];
285
+ if (maxForType && estimatedTokens > maxForType) {
286
+ // Check if relaxed locally (frontmatter or manifest list)
287
+ const isLocalRelaxed = (() => {
288
+ const parsed = (0, yaml_1.parseFrontmatter)(content);
289
+ if (parsed?.data) {
290
+ if (parsed.data.relaxed === true || parsed.data.token_policy === 'relaxed') {
291
+ return true;
292
+ }
293
+ }
294
+ const relaxedCards = policy.relaxed_cards;
295
+ if (Array.isArray(relaxedCards) && relaxedCards.includes(card.id)) {
229
296
  return true;
230
297
  }
298
+ return false;
299
+ })();
300
+ const isRelaxed = !!(options.relaxed || isLocalRelaxed);
301
+ if (isRelaxed) {
302
+ issues.push({
303
+ severity: 'info',
304
+ type: 'card_too_large_relaxed',
305
+ message: `Card "${card.id}" (~${estimatedTokens} tokens) exceeds the normal limit of ${maxForType} tokens for type "${card.type}". (Relaxed/suppressed warning).`,
306
+ fix: `To restore normal limits, remove '--relaxed' option, delete 'relaxed: true'/'token_policy: relaxed' from card frontmatter, or remove the card ID from 'relaxed_cards' in .pmem/manifest.yml.`,
307
+ card_id: card.id,
308
+ });
231
309
  }
232
- const relaxedCards = policy.relaxed_cards;
233
- if (Array.isArray(relaxedCards) && relaxedCards.includes(card.id)) {
234
- return true;
310
+ else {
311
+ issues.push({
312
+ severity: 'warning',
313
+ type: 'card_too_large',
314
+ message: `Card "${card.id}" is ~${estimatedTokens} tokens, exceeding the max configured limit of ${maxForType} tokens for card type "${card.type}" by ${estimatedTokens - maxForType} tokens.`,
315
+ fix: `Consider splitting this card, or raise the limit in .pmem/manifest.yml (card_policy -> max_tokens -> ${card.type}).\n` +
316
+ ` To suppress this warning locally, add 'token_policy: relaxed' or 'relaxed: true' to the card's frontmatter, or add the card ID to 'relaxed_cards' in .pmem/manifest.yml.\n` +
317
+ ` To temporarily relax all limits, run: pmem verify --relaxed`,
318
+ card_id: card.id,
319
+ });
235
320
  }
236
- return false;
237
- })();
238
- const isRelaxed = !!(options.relaxed || isLocalRelaxed);
239
- if (isRelaxed) {
240
- issues.push({
241
- severity: 'info',
242
- type: 'card_too_large_relaxed',
243
- message: `Card "${card.id}" (~${estimatedTokens} tokens) exceeds the normal limit of ${maxForType} tokens for type "${card.type}". (Relaxed/suppressed warning).`,
244
- fix: `To restore normal limits, remove '--relaxed' option, delete 'relaxed: true'/'token_policy: relaxed' from card frontmatter, or remove the card ID from 'relaxed_cards' in .pmem/manifest.yml.`,
245
- card_id: card.id,
246
- });
247
- }
248
- else {
249
- issues.push({
250
- severity: 'warning',
251
- type: 'card_too_large',
252
- message: `Card "${card.id}" is ~${estimatedTokens} tokens, exceeding the max configured limit of ${maxForType} tokens for card type "${card.type}" by ${estimatedTokens - maxForType} tokens.`,
253
- fix: `Consider splitting this card, or raise the limit in .pmem/manifest.yml (card_policy -> max_tokens -> ${card.type}).\n` +
254
- ` To suppress this warning locally, add 'token_policy: relaxed' or 'relaxed: true' to the card's frontmatter, or add the card ID to 'relaxed_cards' in .pmem/manifest.yml.\n` +
255
- ` To temporarily relax all limits, run: pmem verify --relaxed`,
256
- card_id: card.id,
257
- });
258
321
  }
259
322
  }
260
323
  }
261
- }
262
- // 6c. Relation count threshold
263
- for (const card of cards) {
264
- const { count: relatedEdgeCount } = db.prepare('SELECT COUNT(*) as count FROM edges WHERE from_id = ? OR to_id = ?').get(card.id, card.id);
265
- // Use per-card-type threshold if defined, otherwise fall back to global
266
- const threshold = policy.warn_when_related_count_gt_by_type?.[card.type]
267
- ?? policy.warn_when_related_count_gt;
268
- if (relatedEdgeCount > threshold) {
269
- issues.push({
270
- severity: 'warning',
271
- type: 'too_many_relations',
272
- message: `Card "${card.id}" has ${relatedEdgeCount} relations (threshold: ${threshold} for type "${card.type}").`,
273
- fix: 'Review whether all relations are necessary.',
274
- });
324
+ // 6c. Relation count threshold
325
+ for (const card of cards) {
326
+ const { count: relatedEdgeCount } = db.prepare('SELECT COUNT(*) as count FROM edges WHERE from_id = ? OR to_id = ?').get(card.id, card.id);
327
+ // Use per-card-type threshold if defined, otherwise fall back to global
328
+ const threshold = policy.warn_when_related_count_gt_by_type?.[card.type]
329
+ ?? policy.warn_when_related_count_gt;
330
+ if (relatedEdgeCount > threshold) {
331
+ // v0.7.6 (issue #10): fetch up to 10 lowest-confidence edges so the
332
+ // agent can see which relations contribute to the count and which
333
+ // are safe to prune. Sort ASC so lowest-confidence (best pruning
334
+ // candidates) appear first.
335
+ const topEdgesRaw = db.prepare(`SELECT from_id, to_id, type, source, confidence
336
+ FROM edges
337
+ WHERE from_id = ? OR to_id = ?
338
+ ORDER BY confidence ASC
339
+ LIMIT 10`).all(card.id, card.id);
340
+ const topEdges = topEdgesRaw.map(e => ({
341
+ from_id: e.from_id,
342
+ to_id: e.to_id,
343
+ type: e.type,
344
+ source: e.source,
345
+ confidence: e.confidence,
346
+ }));
347
+ const pruningCandidates = topEdges.filter(e => e.source === 'inferred' || e.confidence < 0.5);
348
+ issues.push({
349
+ severity: 'warning',
350
+ type: 'too_many_relations',
351
+ message: `Card "${card.id}" has ${relatedEdgeCount} relations (threshold: ${threshold} for type "${card.type}").`,
352
+ fix: `Run: pmem relations ${card.id} --format json to inspect.`,
353
+ card_id: card.id,
354
+ relation_count: relatedEdgeCount,
355
+ threshold,
356
+ top_edges: topEdges,
357
+ pruning_candidates: pruningCandidates,
358
+ });
359
+ }
275
360
  }
276
361
  }
362
+ // 9. Stale memory: source files newer than card update time
363
+ // Uses shared consistency check to stay aligned with update --suggest
364
+ const staleMemoryIssues = (0, consistency_1.checkStaleMemory)(pmemPath);
365
+ for (const ci of staleMemoryIssues) {
366
+ issues.push({
367
+ severity: 'warning',
368
+ type: ci.type,
369
+ message: ci.message,
370
+ fix: ci.card_id ? `Run: pmem update --confirm to update ${ci.card_id}.` : 'Run: pmem rebuild',
371
+ card_id: ci.card_id,
372
+ });
373
+ }
277
374
  }
278
- // 9. Stale memory: source files newer than card update time
279
- // Uses shared consistency check to stay aligned with update --suggest
280
- const staleMemoryIssues = (0, consistency_1.checkStaleMemory)(pmemPath);
281
- for (const ci of staleMemoryIssues) {
375
+ // 7. Check AGENTS.md exists
376
+ if (!(0, fs_1.fileExists)(path.join(cwd, 'AGENTS.md'))) {
282
377
  issues.push({
283
378
  severity: 'warning',
284
- type: ci.type,
285
- message: ci.message,
286
- fix: ci.card_id ? `Run: pmem update --confirm to update ${ci.card_id}.` : 'Run: pmem rebuild',
287
- card_id: ci.card_id,
379
+ type: 'missing_agents',
380
+ message: 'AGENTS.md not found in project root.',
381
+ fix: 'Run: pmem init',
382
+ });
383
+ }
384
+ // 8. Check memory_status.dirty
385
+ if (manifest.memory_status?.dirty) {
386
+ issues.push({
387
+ severity: 'warning',
388
+ type: 'memory_dirty',
389
+ message: `Memory is marked dirty since ${manifest.memory_status.dirty_since || 'unknown'}. Reason: ${manifest.memory_status.dirty_reason || 'unknown'}.`,
390
+ fix: 'Run: pmem update --auto (to detect changes) or pmem update --confirm (to record updates).',
288
391
  });
289
392
  }
290
393
  }
291
- // 7. Check AGENTS.md exists
292
- if (!(0, fs_1.fileExists)(path.join(cwd, 'AGENTS.md'))) {
293
- issues.push({
294
- severity: 'warning',
295
- type: 'missing_agents',
296
- message: 'AGENTS.md not found in project root.',
297
- fix: 'Run: pmem init',
298
- });
299
- }
300
- // 8. Check memory_status.dirty
301
- if (manifest.memory_status?.dirty) {
302
- issues.push({
303
- severity: 'warning',
304
- type: 'memory_dirty',
305
- message: `Memory is marked dirty since ${manifest.memory_status.dirty_since || 'unknown'}. Reason: ${manifest.memory_status.dirty_reason || 'unknown'}.`,
306
- fix: 'Run: pmem update --auto (to detect changes) or pmem update --confirm (to record updates).',
307
- });
308
- }
309
- }
310
- // Build result
311
- const errors = issues.filter(i => i.severity === 'error');
312
- const warnings = issues.filter(i => i.severity === 'warning');
313
- const infos = issues.filter(i => i.severity === 'info');
314
- const passed = errors.length === 0;
315
- const score = Math.max(0, 100 - errors.length * 30 - warnings.length * 5);
316
- const result = { passed, score, issues };
317
- // Output
318
- if (passed && warnings.length === 0) {
319
- console.log(`✓ Memory verification passed.`);
320
- console.log(` Score: ${score}/100`);
321
- if (infos.length > 0) {
322
- console.log('');
323
- console.log('Informational Notes:');
324
- for (const issue of infos) {
325
- console.log(`ℹ [${issue.type}] ${issue.message}`);
394
+ // Build result
395
+ const errors = issues.filter(i => i.severity === 'error');
396
+ const warnings = issues.filter(i => i.severity === 'warning');
397
+ const infos = issues.filter(i => i.severity === 'info');
398
+ const passed = errors.length === 0;
399
+ const score = Math.max(0, 100 - errors.length * 30 - warnings.length * 5);
400
+ const result = { passed, score, issues };
401
+ // Output
402
+ if (passed && warnings.length === 0) {
403
+ console.log(`✓ Memory verification passed.`);
404
+ console.log(` Score: ${score}/100`);
405
+ if (infos.length > 0) {
406
+ console.log('');
407
+ console.log('Informational Notes:');
408
+ for (const issue of infos) {
409
+ console.log(`ℹ [${issue.type}] ${issue.message}`);
410
+ }
326
411
  }
412
+ return;
327
413
  }
328
- return;
329
- }
330
- console.log(`Memory Verify Result: ${passed ? 'Warnings found' : 'Failed'}`);
331
- console.log(`Score: ${score}/100`);
332
- console.log('');
333
- for (const issue of issues) {
334
- let icon = 'ℹ';
335
- if (issue.severity === 'error')
336
- icon = '✗';
337
- else if (issue.severity === 'warning')
338
- icon = '⚠';
339
- console.log(`${icon} [${issue.type}] ${issue.message}`);
340
- console.log(` Fix: ${issue.fix}`);
414
+ console.log(`Memory Verify Result: ${passed ? 'Warnings found' : 'Failed'}`);
415
+ console.log(`Score: ${score}/100`);
341
416
  console.log('');
342
- }
343
- // --fix-stale: refresh stale_memory cards by bumping last_verified timestamps.
344
- // This is separate from --fix so agents can choose between "repair structural
345
- // index state" and "also acknowledge that source-file changes are reviewed."
346
- if (options.fixStale) {
347
- const staleIssues = issues.filter(i => i.type === 'stale_memory');
348
- if (staleIssues.length > 0 && db) {
349
- console.log(`Auto-fixing ${staleIssues.length} stale memory card(s)...`);
350
- for (const issue of staleIssues) {
351
- if (issue.card_id) {
352
- const card = db.prepare('SELECT file_path FROM cards WHERE id = ?').get(issue.card_id);
353
- if (card) {
354
- const cardFilePath = path.join(cwd, card.file_path);
355
- if ((0, fs_1.fileExists)(cardFilePath)) {
356
- updateFrontmatterTimestamp(cardFilePath, 'last_verified');
357
- console.log(` Updated last_verified timestamp for card: ${issue.card_id}`);
417
+ for (const issue of issues) {
418
+ let icon = 'ℹ';
419
+ if (issue.severity === 'error')
420
+ icon = '✗';
421
+ else if (issue.severity === 'warning')
422
+ icon = '';
423
+ console.log(`${icon} [${issue.type}] ${issue.message}`);
424
+ console.log(` Fix: ${issue.fix}`);
425
+ console.log('');
426
+ }
427
+ // --fix-stale: refresh stale_memory cards by bumping last_verified timestamps.
428
+ // This is separate from --fix so agents can choose between "repair structural
429
+ // index state" and "also acknowledge that source-file changes are reviewed."
430
+ if (options.fixStale) {
431
+ const staleIssues = issues.filter(i => i.type === 'stale_memory');
432
+ if (staleIssues.length > 0 && db) {
433
+ console.log(`Auto-fixing ${staleIssues.length} stale memory card(s)...`);
434
+ for (const issue of staleIssues) {
435
+ if (issue.card_id) {
436
+ const card = db.prepare('SELECT file_path FROM cards WHERE id = ?').get(issue.card_id);
437
+ if (card) {
438
+ const cardFilePath = path.join(cwd, card.file_path);
439
+ if ((0, fs_1.fileExists)(cardFilePath)) {
440
+ updateFrontmatterTimestamp(cardFilePath, 'last_verified');
441
+ console.log(` Updated last_verified timestamp for card: ${issue.card_id}`);
442
+ }
358
443
  }
359
444
  }
360
445
  }
446
+ console.log('Rebuilding indexes for updated cards...');
447
+ (0, rebuild_1.rebuildCommand)();
448
+ }
449
+ // Also fix structural index issues (stale_index, etc.) when --fix-stale is used
450
+ const fixableIssue = issues.find(i => i.type === 'stale_index' ||
451
+ i.type === 'missing_database' ||
452
+ i.type === 'missing_card_file' ||
453
+ i.type === 'orphan_edges');
454
+ if (fixableIssue && staleIssues.length === 0) {
455
+ console.log('Auto-fixing: rebuilding indexes...');
456
+ (0, rebuild_1.rebuildCommand)();
361
457
  }
362
- console.log('Rebuilding indexes for updated cards...');
363
- (0, rebuild_1.rebuildCommand)();
364
458
  }
365
- // Also fix structural index issues (stale_index, etc.) when --fix-stale is used
366
- const fixableIssue = issues.find(i => i.type === 'stale_index' ||
367
- i.type === 'missing_database' ||
368
- i.type === 'missing_card_file' ||
369
- i.type === 'orphan_edges');
370
- if (fixableIssue && staleIssues.length === 0) {
371
- console.log('Auto-fixing: rebuilding indexes...');
372
- (0, rebuild_1.rebuildCommand)();
459
+ // --fix: repair structural index state only (stale_index, missing db, etc.)
460
+ // Does NOT touch stale_memory use --fix-stale for that.
461
+ if (options.fix && !options.fixStale) {
462
+ const fixableIssue = issues.find(i => i.type === 'stale_index' ||
463
+ i.type === 'missing_database' ||
464
+ i.type === 'missing_card_file' ||
465
+ i.type === 'orphan_edges');
466
+ if (fixableIssue) {
467
+ console.log('Auto-fixing: rebuilding indexes...');
468
+ (0, rebuild_1.rebuildCommand)();
469
+ }
373
470
  }
374
- }
375
- // --fix: repair structural index state only (stale_index, missing db, etc.)
376
- // Does NOT touch stale_memory — use --fix-stale for that.
377
- if (options.fix && !options.fixStale) {
378
- const fixableIssue = issues.find(i => i.type === 'stale_index' ||
379
- i.type === 'missing_database' ||
380
- i.type === 'missing_card_file' ||
381
- i.type === 'orphan_edges');
382
- if (fixableIssue) {
383
- console.log('Auto-fixing: rebuilding indexes...');
384
- (0, rebuild_1.rebuildCommand)();
471
+ // --fix-locks cleans stale locks during the check pass above,
472
+ // but if a stale lock was found and not cleaned (e.g., --fix-locks not passed),
473
+ // we provide guidance here.
474
+ const hasErrors = issues.some(i => i.severity === 'error');
475
+ if (hasErrors) {
476
+ if (options.noExit)
477
+ return;
478
+ (0, fs_1.releaseLock)(lockPath);
479
+ process.exit(2);
385
480
  }
386
- }
387
- // --fix-locks cleans stale locks during the check pass above,
388
- // but if a stale lock was found and not cleaned (e.g., --fix-locks not passed),
389
- // we provide guidance here.
390
- const hasErrors = issues.some(i => i.severity === 'error');
391
- if (hasErrors) {
392
481
  if (options.noExit)
393
482
  return;
394
- process.exit(2);
483
+ (0, fs_1.releaseLock)(lockPath);
484
+ process.exit(0);
485
+ }
486
+ finally {
487
+ // If we got here via an exception thrown during verify (e.g. a SQL
488
+ // error from createSchema), still release the lock. The explicit
489
+ // `releaseLock` calls above cover the normal exit paths since
490
+ // `process.exit` does NOT run `finally` blocks.
491
+ try {
492
+ (0, fs_1.releaseLock)(lockPath);
493
+ }
494
+ catch { /* ignore */ }
395
495
  }
396
- if (options.noExit)
397
- return;
398
- process.exit(0);
399
496
  }
400
497
  function updateFrontmatterTimestamp(filePath, field) {
401
498
  const content = (0, fs_1.readFile)(filePath);