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.
- package/CHANGELOG.md +37 -0
- package/README.md +45 -0
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +34 -19
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/decision.d.ts +8 -0
- package/dist/commands/decision.d.ts.map +1 -0
- package/dist/commands/decision.js +112 -0
- package/dist/commands/decision.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +4 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/module.d.ts +8 -0
- package/dist/commands/module.d.ts.map +1 -0
- package/dist/commands/module.js +108 -0
- package/dist/commands/module.js.map +1 -0
- package/dist/commands/new.js +1 -1
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/rebuild.d.ts.map +1 -1
- package/dist/commands/rebuild.js +57 -2
- package/dist/commands/rebuild.js.map +1 -1
- package/dist/commands/recall.d.ts +4 -1
- package/dist/commands/recall.d.ts.map +1 -1
- package/dist/commands/recall.js +14 -128
- package/dist/commands/recall.js.map +1 -1
- package/dist/commands/relations.d.ts +46 -0
- package/dist/commands/relations.d.ts.map +1 -0
- package/dist/commands/relations.js +166 -0
- package/dist/commands/relations.js.map +1 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +154 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +6 -1
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/update.d.ts +18 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +191 -46
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +377 -280
- package/dist/commands/verify.js.map +1 -1
- package/dist/core/capture.d.ts.map +1 -1
- package/dist/core/capture.js +72 -55
- package/dist/core/capture.js.map +1 -1
- package/dist/core/consistency.d.ts.map +1 -1
- package/dist/core/consistency.js +2 -0
- package/dist/core/consistency.js.map +1 -1
- package/dist/core/decisionInfer.d.ts +27 -0
- package/dist/core/decisionInfer.d.ts.map +1 -0
- package/dist/core/decisionInfer.js +190 -0
- package/dist/core/decisionInfer.js.map +1 -0
- package/dist/core/format.js +134 -11
- package/dist/core/format.js.map +1 -1
- package/dist/core/fs.d.ts +27 -0
- package/dist/core/fs.d.ts.map +1 -1
- package/dist/core/fs.js +72 -0
- package/dist/core/fs.js.map +1 -1
- package/dist/core/moduleInfer.d.ts +13 -0
- package/dist/core/moduleInfer.d.ts.map +1 -0
- package/dist/core/moduleInfer.js +252 -0
- package/dist/core/moduleInfer.js.map +1 -0
- package/dist/core/next.d.ts +30 -0
- package/dist/core/next.d.ts.map +1 -0
- package/dist/core/next.js +193 -0
- package/dist/core/next.js.map +1 -0
- package/dist/core/query/context.d.ts.map +1 -1
- package/dist/core/query/context.js +35 -0
- package/dist/core/query/context.js.map +1 -1
- package/dist/core/query/recall.d.ts +28 -0
- package/dist/core/query/recall.d.ts.map +1 -1
- package/dist/core/query/recall.js +131 -25
- package/dist/core/query/recall.js.map +1 -1
- package/dist/core/query/status.d.ts +5 -1
- package/dist/core/query/status.d.ts.map +1 -1
- package/dist/core/query/status.js +143 -3
- package/dist/core/query/status.js.map +1 -1
- package/dist/core/state.d.ts +6 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/state.js +147 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/symbols.d.ts +2 -0
- package/dist/core/symbols.d.ts.map +1 -0
- package/dist/core/symbols.js +147 -0
- package/dist/core/symbols.js.map +1 -0
- package/dist/core/traceParse.d.ts +14 -0
- package/dist/core/traceParse.d.ts.map +1 -0
- package/dist/core/traceParse.js +80 -0
- package/dist/core/traceParse.js.map +1 -0
- package/dist/core/traceSummary.d.ts +28 -0
- package/dist/core/traceSummary.d.ts.map +1 -0
- package/dist/core/traceSummary.js +265 -0
- package/dist/core/traceSummary.js.map +1 -0
- package/dist/index.js +67 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -2
- package/skills/pmem/SKILL.md +17 -1
package/dist/commands/verify.js
CHANGED
|
@@ -61,341 +61,438 @@ function verifyCommand(options) {
|
|
|
61
61
|
fix: 'Run: pmem init',
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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: '
|
|
120
|
-
message: `
|
|
121
|
-
fix: '
|
|
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: '
|
|
132
|
-
message:
|
|
133
|
-
fix: 'Run: pmem
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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: '
|
|
147
|
-
type: '
|
|
148
|
-
message:
|
|
149
|
-
fix: '
|
|
166
|
+
severity: 'warning',
|
|
167
|
+
type: 'missing_database',
|
|
168
|
+
message: '.pmem/pmem.db not found.',
|
|
169
|
+
fix: 'Run: pmem rebuild',
|
|
150
170
|
});
|
|
151
171
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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 (
|
|
198
|
+
else if (currentSchema < '0.3') {
|
|
192
199
|
issues.push({
|
|
193
200
|
severity: 'warning',
|
|
194
|
-
type: '
|
|
195
|
-
message:
|
|
196
|
-
fix: 'Run: pmem
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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: '
|
|
211
|
-
message: `Card "${card.id}"
|
|
212
|
-
fix:
|
|
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
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
241
|
+
// 5. Orphan edges — edges 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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
//
|
|
279
|
-
|
|
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:
|
|
285
|
-
message:
|
|
286
|
-
fix:
|
|
287
|
-
|
|
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
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
console.log(`
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
i.type === '
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
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);
|