nexusmem 0.7.0 → 0.9.0
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 +60 -1
- package/README.md +76 -28
- package/dist/cli/index.js +524 -194
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import pc21 from "picocolors";
|
|
6
6
|
|
|
7
7
|
// src/config/workspace.ts
|
|
8
8
|
import { existsSync } from "fs";
|
|
@@ -174,7 +174,20 @@ var ConfigSchema = z.object({
|
|
|
174
174
|
limits: z.object({
|
|
175
175
|
maxFilesPerNode: z.number().int().positive().default(40),
|
|
176
176
|
maxBodyChars: z.number().int().positive().default(4e3)
|
|
177
|
-
}).default({ maxFilesPerNode: 40, maxBodyChars: 4e3 })
|
|
177
|
+
}).default({ maxFilesPerNode: 40, maxBodyChars: 4e3 }),
|
|
178
|
+
/**
|
|
179
|
+
* Automatic contradiction checking during sync. On by default -- unlike the
|
|
180
|
+
* opt-in transcript sources, this reads nothing new, writes only suggestions
|
|
181
|
+
* (never `supersedes`), and stays affordable by construction: at most
|
|
182
|
+
* `maxPerSync` new SLM judgments per run, judged pairs memoized and never
|
|
183
|
+
* re-asked, and an unreachable model degrades to skipping quietly.
|
|
184
|
+
*/
|
|
185
|
+
contradictions: z.object({
|
|
186
|
+
autoCheck: z.boolean().default(true),
|
|
187
|
+
maxPerSync: z.number().int().nonnegative().default(3),
|
|
188
|
+
/** Ollama model tag. Must be pulled locally, same as `sources.session.model`. */
|
|
189
|
+
model: z.string().default(DEFAULT_SLM_MODEL)
|
|
190
|
+
}).default({ autoCheck: true, maxPerSync: 3, model: DEFAULT_SLM_MODEL })
|
|
178
191
|
});
|
|
179
192
|
function defaultConfig(projectId) {
|
|
180
193
|
return ConfigSchema.parse({ version: 1, projectId });
|
|
@@ -277,6 +290,7 @@ function crashStatus(code, signal) {
|
|
|
277
290
|
return null;
|
|
278
291
|
}
|
|
279
292
|
var TRANSIENT_SPAWN_CODES = /* @__PURE__ */ new Set(["EAGAIN", "EPERM", "EACCES", "EMFILE", "ENFILE", "ENOMEM", "EBUSY", "ETXTBSY"]);
|
|
293
|
+
var GIT_LAUNCH_FAILURE = /error launching git/i;
|
|
280
294
|
function toSpawnError(err, cwd, args) {
|
|
281
295
|
const code = err?.code;
|
|
282
296
|
if (code === "ENOENT") {
|
|
@@ -353,6 +367,15 @@ async function* runGitOnce(cwd, args, opts) {
|
|
|
353
367
|
}
|
|
354
368
|
if (code !== 0) {
|
|
355
369
|
const trimmed = stderr.trim();
|
|
370
|
+
if (GIT_LAUNCH_FAILURE.test(trimmed)) {
|
|
371
|
+
throw new GitSpawnError(
|
|
372
|
+
`Could not start git (${trimmed.split("\n")[0]}) in ${cwd}. This is usually transient on Windows -- retrying the same command often succeeds.`,
|
|
373
|
+
fullArgs,
|
|
374
|
+
void 0,
|
|
375
|
+
true,
|
|
376
|
+
null
|
|
377
|
+
);
|
|
378
|
+
}
|
|
356
379
|
const detail = trimmed.split("\n")[0];
|
|
357
380
|
throw new GitError(
|
|
358
381
|
`git ${args.join(" ")} exited with code ${code}${detail ? `: ${detail}` : ""}`,
|
|
@@ -478,6 +501,9 @@ function renderHookSnippet(logPath) {
|
|
|
478
501
|
"$global:__ssd_last_history_id = -1",
|
|
479
502
|
`$global:__ssd_log_path = ${toPowerShellLiteral(logPath)}`,
|
|
480
503
|
"function global:prompt {",
|
|
504
|
+
// Must be first: Get-History (or anything else) below would overwrite $?.
|
|
505
|
+
" $__ssd_ok = $?",
|
|
506
|
+
" $__ssd_exit = $LASTEXITCODE",
|
|
481
507
|
" $__ssd_h = Get-History -Count 1 -ErrorAction SilentlyContinue",
|
|
482
508
|
" if ($__ssd_h -and $__ssd_h.Id -ne $global:__ssd_last_history_id) {",
|
|
483
509
|
" $global:__ssd_last_history_id = $__ssd_h.Id",
|
|
@@ -485,7 +511,8 @@ function renderHookSnippet(logPath) {
|
|
|
485
511
|
" $__ssd_entry = [ordered]@{",
|
|
486
512
|
' ts = (Get-Date).ToString("o")',
|
|
487
513
|
" cwd = (Get-Location).Path",
|
|
488
|
-
|
|
514
|
+
// $LASTEXITCODE alone misses cmdlet failures and goes stale after them; $? catches both.
|
|
515
|
+
" exitCode = if ($__ssd_ok) { 0 } elseif ($__ssd_exit) { $__ssd_exit } else { 1 }",
|
|
489
516
|
" durationMs = [int](($__ssd_h.EndExecutionTime - $__ssd_h.StartExecutionTime).TotalMilliseconds)",
|
|
490
517
|
" command = $__ssd_h.CommandLine",
|
|
491
518
|
" }",
|
|
@@ -944,13 +971,41 @@ UPDATE nodes SET provenance = 'observed' WHERE kind IN ('git_commit', 'code_diff
|
|
|
944
971
|
|
|
945
972
|
CREATE INDEX idx_nodes_supersedes ON nodes (supersedes) WHERE supersedes IS NOT NULL;
|
|
946
973
|
`;
|
|
974
|
+
var V7 = `
|
|
975
|
+
UPDATE nodes SET provenance = 'authored' WHERE kind IN ('doc_section', 'note');
|
|
976
|
+
UPDATE nodes SET provenance = 'recorded' WHERE kind = 'conversation_turn';
|
|
977
|
+
UPDATE nodes SET provenance = 'derived' WHERE kind = 'session_summary';
|
|
978
|
+
UPDATE nodes SET provenance = 'recorded' WHERE provenance = 'inferred';
|
|
979
|
+
`;
|
|
980
|
+
var V8 = `
|
|
981
|
+
CREATE TABLE contradiction_checks (
|
|
982
|
+
candidate_id TEXT NOT NULL REFERENCES nodes (id) ON DELETE CASCADE,
|
|
983
|
+
against_id TEXT NOT NULL REFERENCES nodes (id) ON DELETE CASCADE,
|
|
984
|
+
contradicts INTEGER NOT NULL,
|
|
985
|
+
reason TEXT,
|
|
986
|
+
model TEXT NOT NULL,
|
|
987
|
+
checked_at INTEGER NOT NULL,
|
|
988
|
+
PRIMARY KEY (candidate_id, against_id)
|
|
989
|
+
);
|
|
990
|
+
`;
|
|
991
|
+
var V9 = `
|
|
992
|
+
ALTER TABLE contradiction_checks ADD COLUMN dismissed INTEGER NOT NULL DEFAULT 0;
|
|
993
|
+
`;
|
|
994
|
+
var V10 = `
|
|
995
|
+
ALTER TABLE nodes ADD COLUMN trust_state TEXT NOT NULL DEFAULT 'candidate';
|
|
996
|
+
CREATE INDEX idx_nodes_trust_state ON nodes (project_id, trust_state) WHERE trust_state != 'candidate';
|
|
997
|
+
`;
|
|
947
998
|
var MIGRATIONS = [
|
|
948
999
|
{ version: 1, up: (db) => db.exec(V1) },
|
|
949
1000
|
{ version: 2, up: (db) => db.exec(V2) },
|
|
950
1001
|
{ version: 3, up: (db) => db.exec(V3) },
|
|
951
1002
|
{ version: 4, up: (db) => db.exec(V4) },
|
|
952
1003
|
{ version: 5, up: (db) => db.exec(V5) },
|
|
953
|
-
{ version: 6, up: (db) => db.exec(V6) }
|
|
1004
|
+
{ version: 6, up: (db) => db.exec(V6) },
|
|
1005
|
+
{ version: 7, up: (db) => db.exec(V7) },
|
|
1006
|
+
{ version: 8, up: (db) => db.exec(V8) },
|
|
1007
|
+
{ version: 9, up: (db) => db.exec(V9) },
|
|
1008
|
+
{ version: 10, up: (db) => db.exec(V10) }
|
|
954
1009
|
];
|
|
955
1010
|
var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
956
1011
|
function currentSchemaVersion(db) {
|
|
@@ -968,6 +1023,36 @@ function migrate(db) {
|
|
|
968
1023
|
return { from, to: currentSchemaVersion(db) };
|
|
969
1024
|
}
|
|
970
1025
|
|
|
1026
|
+
// src/store/audit.ts
|
|
1027
|
+
function recordMutationAudit(db, input) {
|
|
1028
|
+
return Number(
|
|
1029
|
+
db.prepare(
|
|
1030
|
+
`INSERT INTO mutation_audit (action, project_id, detail, affected_count, succeeded, error, started_at, finished_at)
|
|
1031
|
+
VALUES (@action, @projectId, @detail, @affectedCount, @succeeded, @error, @startedAt, @finishedAt)`
|
|
1032
|
+
).run({
|
|
1033
|
+
action: input.action,
|
|
1034
|
+
projectId: input.projectId,
|
|
1035
|
+
detail: JSON.stringify(input.detail),
|
|
1036
|
+
affectedCount: input.affectedCount,
|
|
1037
|
+
succeeded: input.succeeded ? 1 : 0,
|
|
1038
|
+
error: input.error ?? null,
|
|
1039
|
+
startedAt: input.startedAt,
|
|
1040
|
+
finishedAt: input.finishedAt
|
|
1041
|
+
}).lastInsertRowid
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
function listMutationAudit(db, projectId, opts = {}) {
|
|
1045
|
+
const rows = db.prepare(
|
|
1046
|
+
`SELECT id, action, project_id AS projectId, detail, affected_count AS affectedCount,
|
|
1047
|
+
succeeded, error, started_at AS startedAt, finished_at AS finishedAt
|
|
1048
|
+
FROM mutation_audit
|
|
1049
|
+
WHERE project_id = @projectId
|
|
1050
|
+
ORDER BY started_at DESC
|
|
1051
|
+
LIMIT @limit`
|
|
1052
|
+
).all({ projectId, limit: opts.limit ?? 50 });
|
|
1053
|
+
return rows.map((r) => ({ ...r, succeeded: r.succeeded === 1 }));
|
|
1054
|
+
}
|
|
1055
|
+
|
|
971
1056
|
// src/store/projects.ts
|
|
972
1057
|
function upsertProject(db, project) {
|
|
973
1058
|
db.prepare(
|
|
@@ -1012,11 +1097,13 @@ function defaultProvenanceForKind(kind) {
|
|
|
1012
1097
|
case "code_diff":
|
|
1013
1098
|
case "shell_command":
|
|
1014
1099
|
return "observed";
|
|
1015
|
-
case "conversation_turn":
|
|
1016
|
-
case "session_summary":
|
|
1017
1100
|
case "doc_section":
|
|
1018
1101
|
case "note":
|
|
1019
|
-
return "
|
|
1102
|
+
return "authored";
|
|
1103
|
+
case "conversation_turn":
|
|
1104
|
+
return "recorded";
|
|
1105
|
+
case "session_summary":
|
|
1106
|
+
return "derived";
|
|
1020
1107
|
}
|
|
1021
1108
|
}
|
|
1022
1109
|
|
|
@@ -1118,7 +1205,7 @@ function clearProject(db, projectId) {
|
|
|
1118
1205
|
function getNodesByIds(db, ids) {
|
|
1119
1206
|
if (ids.length === 0) return [];
|
|
1120
1207
|
return db.prepare(
|
|
1121
|
-
`SELECT id, kind, project_id AS projectId, ts, title, body, signal, provenance
|
|
1208
|
+
`SELECT id, kind, project_id AS projectId, ts, title, body, signal, provenance, trust_state AS trustState
|
|
1122
1209
|
FROM nodes WHERE id IN (SELECT value FROM json_each(?))`
|
|
1123
1210
|
).all(JSON.stringify(ids));
|
|
1124
1211
|
}
|
|
@@ -1164,6 +1251,9 @@ function getSupersededIds(db, projectId) {
|
|
|
1164
1251
|
function setSupersedes(db, newNodeId, staleNodeId) {
|
|
1165
1252
|
db.prepare("UPDATE nodes SET supersedes = ? WHERE id = ?").run(staleNodeId, newNodeId);
|
|
1166
1253
|
}
|
|
1254
|
+
function setTrustState(db, nodeId, state) {
|
|
1255
|
+
return db.prepare("UPDATE nodes SET trust_state = ? WHERE id = ?").run(state, nodeId).changes > 0;
|
|
1256
|
+
}
|
|
1167
1257
|
function listStaleCandidates(db, projectId, opts = {}) {
|
|
1168
1258
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
1169
1259
|
const minAgeDays = opts.minAgeDays ?? 45;
|
|
@@ -1171,7 +1261,7 @@ function listStaleCandidates(db, projectId, opts = {}) {
|
|
|
1171
1261
|
const rows = db.prepare(
|
|
1172
1262
|
`SELECT id, kind, ts, ts_epoch AS tsEpoch, source, title
|
|
1173
1263
|
FROM nodes
|
|
1174
|
-
WHERE project_id = @projectId AND provenance
|
|
1264
|
+
WHERE project_id = @projectId AND provenance != 'observed' AND ts_epoch < @cutoff
|
|
1175
1265
|
AND id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)
|
|
1176
1266
|
ORDER BY ts_epoch ASC
|
|
1177
1267
|
LIMIT @limit`
|
|
@@ -1191,7 +1281,7 @@ function countStaleCandidates(db, projectId, opts = {}) {
|
|
|
1191
1281
|
const cutoff = now.getTime() - minAgeDays * 864e5;
|
|
1192
1282
|
const row = db.prepare(
|
|
1193
1283
|
`SELECT COUNT(*) AS count FROM nodes
|
|
1194
|
-
WHERE project_id = @projectId AND provenance
|
|
1284
|
+
WHERE project_id = @projectId AND provenance != 'observed' AND ts_epoch < @cutoff
|
|
1195
1285
|
AND id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)`
|
|
1196
1286
|
).get({ projectId, cutoff });
|
|
1197
1287
|
return row.count;
|
|
@@ -1372,22 +1462,29 @@ function countNodesNeedingEmbedding(db, projectId) {
|
|
|
1372
1462
|
).get(projectId);
|
|
1373
1463
|
return row.n;
|
|
1374
1464
|
}
|
|
1465
|
+
function getEmbedding(db, nodeId) {
|
|
1466
|
+
const row = db.prepare("SELECT v.embedding AS embedding FROM nodes_vec v JOIN nodes n ON n.rowid = v.rowid WHERE n.id = ?").get(nodeId);
|
|
1467
|
+
if (!row) return null;
|
|
1468
|
+
return new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
|
|
1469
|
+
}
|
|
1375
1470
|
function upsertEmbedding(db, rowid, embedding) {
|
|
1376
1471
|
db.prepare("INSERT OR REPLACE INTO nodes_vec (rowid, embedding) VALUES (?, ?)").run(BigInt(rowid), embedding);
|
|
1377
1472
|
}
|
|
1378
1473
|
function dropAllEmbeddings(db) {
|
|
1379
1474
|
return db.prepare("DELETE FROM nodes_vec").run().changes;
|
|
1380
1475
|
}
|
|
1381
|
-
function vectorSearch(db, projectId, embedding, limit = 20) {
|
|
1476
|
+
function vectorSearch(db, projectId, embedding, limit = 20, opts = {}) {
|
|
1382
1477
|
const overfetch = Math.max(limit * 8, 50);
|
|
1478
|
+
const asOfEpoch = opts.asOfEpoch ?? null;
|
|
1383
1479
|
return db.prepare(
|
|
1384
|
-
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance, v.distance AS distance
|
|
1480
|
+
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance, n.trust_state AS trustState, v.distance AS distance
|
|
1385
1481
|
FROM nodes_vec v
|
|
1386
1482
|
JOIN nodes n ON n.rowid = v.rowid
|
|
1387
1483
|
WHERE v.embedding MATCH ? AND k = ? AND n.project_id = ?
|
|
1484
|
+
AND (? IS NULL OR n.created_at <= ?)
|
|
1388
1485
|
ORDER BY v.distance
|
|
1389
1486
|
LIMIT ?`
|
|
1390
|
-
).all(embedding, overfetch, projectId, limit);
|
|
1487
|
+
).all(embedding, overfetch, projectId, asOfEpoch, asOfEpoch, limit);
|
|
1391
1488
|
}
|
|
1392
1489
|
|
|
1393
1490
|
// src/store/fts.ts
|
|
@@ -1406,18 +1503,20 @@ function toMatchQuery(input) {
|
|
|
1406
1503
|
}
|
|
1407
1504
|
|
|
1408
1505
|
// src/store/search.ts
|
|
1409
|
-
function search(db, projectId, query, limit = 20) {
|
|
1506
|
+
function search(db, projectId, query, limit = 20, opts = {}) {
|
|
1410
1507
|
const match = toMatchQuery(query);
|
|
1411
1508
|
if (!match) return [];
|
|
1509
|
+
const asOfEpoch = opts.asOfEpoch ?? null;
|
|
1412
1510
|
const rows = db.prepare(
|
|
1413
|
-
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance,
|
|
1511
|
+
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance, n.trust_state AS trustState,
|
|
1414
1512
|
bm25(nodes_fts, 10.0, 1.0) AS rank
|
|
1415
1513
|
FROM nodes_fts
|
|
1416
1514
|
JOIN nodes n ON n.rowid = nodes_fts.rowid
|
|
1417
1515
|
WHERE nodes_fts MATCH ? AND n.project_id = ?
|
|
1516
|
+
AND (? IS NULL OR n.created_at <= ?)
|
|
1418
1517
|
ORDER BY rank
|
|
1419
1518
|
LIMIT ?`
|
|
1420
|
-
).all(match, projectId, limit);
|
|
1519
|
+
).all(match, projectId, asOfEpoch, asOfEpoch, limit);
|
|
1421
1520
|
return rows;
|
|
1422
1521
|
}
|
|
1423
1522
|
function stats(db, projectId) {
|
|
@@ -1449,6 +1548,57 @@ function setMeta(db, key, value) {
|
|
|
1449
1548
|
);
|
|
1450
1549
|
}
|
|
1451
1550
|
|
|
1551
|
+
// src/store/contradictions.ts
|
|
1552
|
+
function recordContradictionCheck(db, input) {
|
|
1553
|
+
db.prepare(
|
|
1554
|
+
`INSERT OR REPLACE INTO contradiction_checks (candidate_id, against_id, contradicts, reason, model, checked_at)
|
|
1555
|
+
VALUES (@candidateId, @againstId, @contradicts, @reason, @model, @now)`
|
|
1556
|
+
).run({
|
|
1557
|
+
candidateId: input.candidateId,
|
|
1558
|
+
againstId: input.againstId,
|
|
1559
|
+
contradicts: input.contradicts ? 1 : 0,
|
|
1560
|
+
reason: input.reason,
|
|
1561
|
+
model: input.model,
|
|
1562
|
+
now: Date.now()
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
function hasContradictionCheck(db, candidateId, againstId) {
|
|
1566
|
+
const row = db.prepare("SELECT 1 FROM contradiction_checks WHERE candidate_id = ? AND against_id = ?").get(candidateId, againstId);
|
|
1567
|
+
return row !== void 0;
|
|
1568
|
+
}
|
|
1569
|
+
function listContradictionSuggestions(db, projectId, opts = {}) {
|
|
1570
|
+
return db.prepare(
|
|
1571
|
+
`SELECT c.candidate_id AS candidateId, n.title AS candidateTitle,
|
|
1572
|
+
c.against_id AS againstId, a.title AS againstTitle,
|
|
1573
|
+
c.reason AS reason, c.checked_at AS checkedAt
|
|
1574
|
+
FROM contradiction_checks c
|
|
1575
|
+
JOIN nodes n ON n.id = c.candidate_id
|
|
1576
|
+
JOIN nodes a ON a.id = c.against_id
|
|
1577
|
+
WHERE n.project_id = @projectId AND c.contradicts = 1 AND c.dismissed = 0
|
|
1578
|
+
AND c.candidate_id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)
|
|
1579
|
+
ORDER BY c.checked_at DESC
|
|
1580
|
+
LIMIT @limit`
|
|
1581
|
+
).all({ projectId, limit: opts.limit ?? 50 });
|
|
1582
|
+
}
|
|
1583
|
+
function countContradictionSuggestions(db, projectId) {
|
|
1584
|
+
const row = db.prepare(
|
|
1585
|
+
`SELECT COUNT(*) AS count
|
|
1586
|
+
FROM contradiction_checks c
|
|
1587
|
+
JOIN nodes n ON n.id = c.candidate_id
|
|
1588
|
+
WHERE n.project_id = @projectId AND c.contradicts = 1 AND c.dismissed = 0
|
|
1589
|
+
AND c.candidate_id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)`
|
|
1590
|
+
).get({ projectId });
|
|
1591
|
+
return row.count;
|
|
1592
|
+
}
|
|
1593
|
+
function dismissContradictionSuggestion(db, projectId, candidateId) {
|
|
1594
|
+
return db.prepare(
|
|
1595
|
+
`UPDATE contradiction_checks
|
|
1596
|
+
SET dismissed = 1
|
|
1597
|
+
WHERE candidate_id = @candidateId AND contradicts = 1 AND dismissed = 0
|
|
1598
|
+
AND candidate_id IN (SELECT id FROM nodes WHERE project_id = @projectId)`
|
|
1599
|
+
).run({ projectId, candidateId }).changes;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1452
1602
|
// src/store/store.ts
|
|
1453
1603
|
var MemoryStore = class _MemoryStore {
|
|
1454
1604
|
constructor(db) {
|
|
@@ -1608,6 +1758,10 @@ var MemoryStore = class _MemoryStore {
|
|
|
1608
1758
|
upsertEmbedding(rowid, embedding) {
|
|
1609
1759
|
upsertEmbedding(this.db, rowid, embedding);
|
|
1610
1760
|
}
|
|
1761
|
+
/** The stored vector for one node, or null if it has not been embedded yet. */
|
|
1762
|
+
getEmbedding(nodeId) {
|
|
1763
|
+
return getEmbedding(this.db, nodeId);
|
|
1764
|
+
}
|
|
1611
1765
|
dropAllEmbeddings() {
|
|
1612
1766
|
return dropAllEmbeddings(this.db);
|
|
1613
1767
|
}
|
|
@@ -1617,14 +1771,14 @@ var MemoryStore = class _MemoryStore {
|
|
|
1617
1771
|
setMeta(key, value) {
|
|
1618
1772
|
setMeta(this.db, key, value);
|
|
1619
1773
|
}
|
|
1620
|
-
vectorSearch(projectId, embedding, limit = 20) {
|
|
1621
|
-
return vectorSearch(this.db, projectId, embedding, limit);
|
|
1774
|
+
vectorSearch(projectId, embedding, limit = 20, opts = {}) {
|
|
1775
|
+
return vectorSearch(this.db, projectId, embedding, limit, opts);
|
|
1622
1776
|
}
|
|
1623
1777
|
stats(projectId) {
|
|
1624
1778
|
return stats(this.db, projectId);
|
|
1625
1779
|
}
|
|
1626
|
-
search(projectId, query, limit = 20) {
|
|
1627
|
-
return search(this.db, projectId, query, limit);
|
|
1780
|
+
search(projectId, query, limit = 20, opts = {}) {
|
|
1781
|
+
return search(this.db, projectId, query, limit, opts);
|
|
1628
1782
|
}
|
|
1629
1783
|
/** The project a node belongs to, or null if no node has this id. Used by `mark-stale` to validate both ids. */
|
|
1630
1784
|
getNodeProjectId(id) {
|
|
@@ -1638,7 +1792,11 @@ var MemoryStore = class _MemoryStore {
|
|
|
1638
1792
|
setSupersedes(newNodeId, staleNodeId) {
|
|
1639
1793
|
setSupersedes(this.db, newNodeId, staleNodeId);
|
|
1640
1794
|
}
|
|
1641
|
-
/**
|
|
1795
|
+
/** Record a human's verdict on one node -- the write behind `nexusmem review`. Caller validates project ownership first. */
|
|
1796
|
+
setTrustState(nodeId, state) {
|
|
1797
|
+
return setTrustState(this.db, nodeId, state);
|
|
1798
|
+
}
|
|
1799
|
+
/** Aging non-`observed` nodes nothing supersedes yet -- candidates for `nexusmem mark-stale`, not auto-applied. */
|
|
1642
1800
|
listStaleCandidates(projectId, opts = {}) {
|
|
1643
1801
|
return listStaleCandidates(this.db, projectId, opts);
|
|
1644
1802
|
}
|
|
@@ -1646,6 +1804,32 @@ var MemoryStore = class _MemoryStore {
|
|
|
1646
1804
|
countStaleCandidates(projectId, opts = {}) {
|
|
1647
1805
|
return countStaleCandidates(this.db, projectId, opts);
|
|
1648
1806
|
}
|
|
1807
|
+
/** Memoize one SLM contradiction judgment (either verdict). Suggest-only: never writes `supersedes`. */
|
|
1808
|
+
recordContradictionCheck(input) {
|
|
1809
|
+
recordContradictionCheck(this.db, input);
|
|
1810
|
+
}
|
|
1811
|
+
hasContradictionCheck(candidateId, againstId) {
|
|
1812
|
+
return hasContradictionCheck(this.db, candidateId, againstId);
|
|
1813
|
+
}
|
|
1814
|
+
/** Open YES verdicts awaiting a human's `mark-stale`, newest judgment first. */
|
|
1815
|
+
listContradictionSuggestions(projectId, opts = {}) {
|
|
1816
|
+
return listContradictionSuggestions(this.db, projectId, opts);
|
|
1817
|
+
}
|
|
1818
|
+
countContradictionSuggestions(projectId) {
|
|
1819
|
+
return countContradictionSuggestions(this.db, projectId);
|
|
1820
|
+
}
|
|
1821
|
+
/** Reject every open suggestion for this candidate; returns how many were actually dismissed. */
|
|
1822
|
+
dismissContradictionSuggestion(projectId, candidateId) {
|
|
1823
|
+
return dismissContradictionSuggestion(this.db, projectId, candidateId);
|
|
1824
|
+
}
|
|
1825
|
+
/** Record one `mutation_audit` row for a coarse/destructive operation outside `forget` (currently: `--prune-source`/`--prune-stale-shell`). */
|
|
1826
|
+
recordMutationAudit(input) {
|
|
1827
|
+
return recordMutationAudit(this.db, input);
|
|
1828
|
+
}
|
|
1829
|
+
/** Newest-first `mutation_audit` rows for this project -- every `forget` and `--prune-source` run, whether or not anything matched. */
|
|
1830
|
+
listMutationAudit(projectId, opts = {}) {
|
|
1831
|
+
return listMutationAudit(this.db, projectId, opts);
|
|
1832
|
+
}
|
|
1649
1833
|
/** Escape hatch for tests and future modules. */
|
|
1650
1834
|
get raw() {
|
|
1651
1835
|
return this.db;
|
|
@@ -2295,6 +2479,7 @@ function packContext(ranked, tokensBudget, opts = {}) {
|
|
|
2295
2479
|
summary,
|
|
2296
2480
|
tokens,
|
|
2297
2481
|
provenance: hit.provenance,
|
|
2482
|
+
trustState: hit.trustState,
|
|
2298
2483
|
...hit.project ? { project: hit.project } : {}
|
|
2299
2484
|
});
|
|
2300
2485
|
tokensUsed += tokens;
|
|
@@ -2308,7 +2493,8 @@ function renderContextBlock(query, result) {
|
|
|
2308
2493
|
for (const node of result.nodes) {
|
|
2309
2494
|
const project = node.project ? `[${node.project}] ` : "";
|
|
2310
2495
|
const provenance = `[${node.provenance}] `;
|
|
2311
|
-
|
|
2496
|
+
const trust = node.trustState !== "candidate" ? `[${node.trustState}] ` : "";
|
|
2497
|
+
lines.push(`- ${node.ts.slice(0, 10)} ${provenance}${trust}${project}${node.title}`);
|
|
2312
2498
|
if (node.summary && node.summary !== node.title) {
|
|
2313
2499
|
if (node.kind === "code_diff") {
|
|
2314
2500
|
for (const line of node.summary.split("\n")) lines.push(` ${line}`);
|
|
@@ -2446,6 +2632,7 @@ function mergeSearchAndVectorHits(bm25Hits, vectorHits) {
|
|
|
2446
2632
|
body: hit.body,
|
|
2447
2633
|
signal: hit.signal,
|
|
2448
2634
|
provenance: hit.provenance,
|
|
2635
|
+
trustState: hit.trustState,
|
|
2449
2636
|
rank: 0
|
|
2450
2637
|
});
|
|
2451
2638
|
}
|
|
@@ -2458,8 +2645,14 @@ var SIGNAL_FLOOR = 0.2;
|
|
|
2458
2645
|
var RECENCY_FLOOR = 0.3;
|
|
2459
2646
|
var DEFAULT_HALF_LIFE_DAYS = 30;
|
|
2460
2647
|
var MS_PER_DAY = 864e5;
|
|
2461
|
-
var
|
|
2648
|
+
var HALF_LIFE_RATIO = {
|
|
2649
|
+
observed: 1,
|
|
2650
|
+
authored: 0.75,
|
|
2651
|
+
recorded: 0.5,
|
|
2652
|
+
derived: 0.35
|
|
2653
|
+
};
|
|
2462
2654
|
var SUPERSEDED_PENALTY = 0.5;
|
|
2655
|
+
var REJECTED_TRUST_PENALTY = 0.3;
|
|
2463
2656
|
var MAX_PRIOR_OVERTURN = 2;
|
|
2464
2657
|
var PRIOR_COUNT = 2;
|
|
2465
2658
|
var PER_PRIOR_OVERTURN = MAX_PRIOR_OVERTURN ** (1 / PRIOR_COUNT);
|
|
@@ -2493,17 +2686,18 @@ function ageDaysOf(ts, now) {
|
|
|
2493
2686
|
function rankHits(hits, opts = {}) {
|
|
2494
2687
|
if (hits.length === 0) return [];
|
|
2495
2688
|
const halfLife = opts.halfLifeDays ?? DEFAULT_HALF_LIFE_DAYS;
|
|
2496
|
-
const
|
|
2689
|
+
const ratios = { ...HALF_LIFE_RATIO, ...opts.halfLifeRatios };
|
|
2497
2690
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
2498
2691
|
const relevances = opts.relevanceScores ? normalizeExternalRelevance(hits, opts.relevanceScores) : normalizeRelevance(hits);
|
|
2499
2692
|
const ranked = hits.map((hit, i) => {
|
|
2500
2693
|
const relevance = relevances[i] ?? RELEVANCE_FLOOR;
|
|
2501
2694
|
const signalWeight = SIGNAL_FLOOR + (1 - SIGNAL_FLOOR) * hit.signal;
|
|
2502
2695
|
const ageDays = ageDaysOf(hit.ts, now);
|
|
2503
|
-
const effectiveHalfLife = hit.provenance
|
|
2696
|
+
const effectiveHalfLife = halfLife * (ratios[hit.provenance] ?? 1);
|
|
2504
2697
|
const recencyFactor = RECENCY_FLOOR + (1 - RECENCY_FLOOR) * 2 ** (-ageDays / effectiveHalfLife);
|
|
2505
2698
|
const rawScore = relevance * signalWeight ** SIGNAL_EXPONENT * recencyFactor ** RECENCY_EXPONENT;
|
|
2506
|
-
const
|
|
2699
|
+
const supersededScore = opts.supersededIds?.has(hit.id) ? rawScore * SUPERSEDED_PENALTY : rawScore;
|
|
2700
|
+
const score = hit.trustState === "rejected" ? supersededScore * REJECTED_TRUST_PENALTY : supersededScore;
|
|
2507
2701
|
return { ...hit, relevance, signalWeight, ageDays, recencyFactor, score };
|
|
2508
2702
|
});
|
|
2509
2703
|
return ranked.sort((a, b) => b.score - a.score);
|
|
@@ -2533,6 +2727,7 @@ function pullLinkedResolutions(resolveStore, ranked) {
|
|
|
2533
2727
|
body: resolution.body,
|
|
2534
2728
|
signal: resolution.signal,
|
|
2535
2729
|
provenance: resolution.provenance,
|
|
2730
|
+
trustState: resolution.trustState,
|
|
2536
2731
|
rank: 0,
|
|
2537
2732
|
// no bm25/vector rank of its own -- never read again past this point
|
|
2538
2733
|
relevance: hit.relevance,
|
|
@@ -2557,8 +2752,8 @@ async function runCrossProjectQuery(sources, query, opts) {
|
|
|
2557
2752
|
const supersededIds = /* @__PURE__ */ new Set();
|
|
2558
2753
|
for (const source of sources) {
|
|
2559
2754
|
const label = (hit) => ({ ...hit, project: source.label });
|
|
2560
|
-
const bm25Hits = source.store.search(source.projectId, query, opts.candidates).map(label);
|
|
2561
|
-
const vectorHits = queryVector ? source.store.vectorSearch(source.projectId, queryVector, opts.candidates) : [];
|
|
2755
|
+
const bm25Hits = source.store.search(source.projectId, query, opts.candidates, { asOfEpoch: opts.asOfEpoch }).map(label);
|
|
2756
|
+
const vectorHits = queryVector ? source.store.vectorSearch(source.projectId, queryVector, opts.candidates, { asOfEpoch: opts.asOfEpoch }) : [];
|
|
2562
2757
|
bm25Count += bm25Hits.length;
|
|
2563
2758
|
vectorCount += vectorHits.length;
|
|
2564
2759
|
perProject.push({ label: source.label, bm25: bm25Hits.length, vector: vectorHits.length });
|
|
@@ -2579,11 +2774,11 @@ async function runCrossProjectQuery(sources, query, opts) {
|
|
|
2579
2774
|
return { bm25Count, vectorCount, hits, packed, perProject };
|
|
2580
2775
|
}
|
|
2581
2776
|
async function runHybridQuery(store, projectId, query, opts) {
|
|
2582
|
-
const bm25Hits = store.search(projectId, query, opts.candidates);
|
|
2777
|
+
const bm25Hits = store.search(projectId, query, opts.candidates, { asOfEpoch: opts.asOfEpoch });
|
|
2583
2778
|
let vectorHits = [];
|
|
2584
2779
|
if (opts.embeddingProvider) {
|
|
2585
2780
|
const queryVector = await opts.embeddingProvider.embed(query);
|
|
2586
|
-
if (queryVector) vectorHits = store.vectorSearch(projectId, queryVector, opts.candidates);
|
|
2781
|
+
if (queryVector) vectorHits = store.vectorSearch(projectId, queryVector, opts.candidates, { asOfEpoch: opts.asOfEpoch });
|
|
2587
2782
|
}
|
|
2588
2783
|
const hits = vectorHits.length > 0 ? mergeSearchAndVectorHits(bm25Hits, vectorHits) : bm25Hits;
|
|
2589
2784
|
const relevanceScores = vectorHits.length > 0 ? reciprocalRankFusion([bm25Hits, vectorHits]) : void 0;
|
|
@@ -2830,8 +3025,8 @@ function toMemoryNodes(turn, projectId, opts = {}) {
|
|
|
2830
3025
|
files: extractMentionedFiles(`${userRedacted.text}
|
|
2831
3026
|
${chunk2.text}`),
|
|
2832
3027
|
signal: scoreConversationTurn(userRedacted.text, chunk2.text),
|
|
2833
|
-
provenance: "
|
|
2834
|
-
// discourse about what happened, not the event itself
|
|
3028
|
+
provenance: "recorded",
|
|
3029
|
+
// verbatim discourse about what happened, not the event itself
|
|
2835
3030
|
meta: {
|
|
2836
3031
|
cwd: turn.cwd,
|
|
2837
3032
|
source: turn.source,
|
|
@@ -3403,8 +3598,8 @@ function toMemoryNodes3(file, projectId, opts = {}) {
|
|
|
3403
3598
|
body: truncate(chunk2.text, maxBody),
|
|
3404
3599
|
files: [{ path: file.path, insertions: null, deletions: null, binary: false }],
|
|
3405
3600
|
signal: scoreDocSection(file.path, chunk2.heading, chunk2.text),
|
|
3406
|
-
provenance: "
|
|
3407
|
-
// a written claim
|
|
3601
|
+
provenance: "authored",
|
|
3602
|
+
// a human's own written claim -- deliberate, but can still go stale
|
|
3408
3603
|
meta: {
|
|
3409
3604
|
path: file.path,
|
|
3410
3605
|
heading: chunk2.heading,
|
|
@@ -3566,7 +3761,7 @@ ${summary.body}`;
|
|
|
3566
3761
|
files: extractMentionedFiles(session.turns.map((t) => `${t.userText}
|
|
3567
3762
|
${t.assistantText}`).join("\n")),
|
|
3568
3763
|
signal: scoreSession(session.turns.length),
|
|
3569
|
-
provenance: "
|
|
3764
|
+
provenance: "derived",
|
|
3570
3765
|
// a model's distillation, not a directly observed event
|
|
3571
3766
|
meta: {
|
|
3572
3767
|
sessionKey: session.sessionKey,
|
|
@@ -3820,6 +4015,97 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
3820
4015
|
return { files, unreadable };
|
|
3821
4016
|
}
|
|
3822
4017
|
|
|
4018
|
+
// src/slm/contradiction.ts
|
|
4019
|
+
var MAX_BODY_CHARS = 1500;
|
|
4020
|
+
var MAX_REASON_CHARS = 200;
|
|
4021
|
+
var CONTRADICTION_INSTRUCTIONS = `You are checking whether a NEWER memory replaces or contradicts an OLDER one, for an AI coding assistant's memory index.
|
|
4022
|
+
|
|
4023
|
+
Answer in exactly this shape:
|
|
4024
|
+
VERDICT: YES or NO
|
|
4025
|
+
REASON: <one line, under 20 words>
|
|
4026
|
+
|
|
4027
|
+
Say YES only if the NEWER memory states something that makes the OLDER one factually wrong or obsolete -- a decision reversed, a bug fixed, a plan abandoned. Say NO if they are about different things, or the newer one only adds detail without contradicting the older one. When unsure, say NO.`;
|
|
4028
|
+
function buildContradictionPrompt(older, newer) {
|
|
4029
|
+
const body = [
|
|
4030
|
+
`OLDER (${older.title}):`,
|
|
4031
|
+
truncate(older.body, MAX_BODY_CHARS),
|
|
4032
|
+
"",
|
|
4033
|
+
`NEWER (${newer.title}):`,
|
|
4034
|
+
truncate(newer.body, MAX_BODY_CHARS)
|
|
4035
|
+
].join("\n");
|
|
4036
|
+
return `${CONTRADICTION_INSTRUCTIONS}
|
|
4037
|
+
|
|
4038
|
+
---
|
|
4039
|
+
|
|
4040
|
+
${body}
|
|
4041
|
+
|
|
4042
|
+
---
|
|
4043
|
+
|
|
4044
|
+
Answer:`;
|
|
4045
|
+
}
|
|
4046
|
+
function parseContradictionVerdict(raw) {
|
|
4047
|
+
const lines = raw.trim().split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
4048
|
+
const verdictLine = lines.find((l) => /^VERDICT:/i.test(l));
|
|
4049
|
+
if (!verdictLine) return null;
|
|
4050
|
+
const verdict = /^VERDICT:\s*(YES|NO)\b/i.exec(verdictLine);
|
|
4051
|
+
if (!verdict) return null;
|
|
4052
|
+
const reasonLine = lines.find((l) => /^REASON:/i.test(l));
|
|
4053
|
+
const reason = reasonLine ? reasonLine.replace(/^REASON:\s*/i, "").trim() : "";
|
|
4054
|
+
return {
|
|
4055
|
+
contradicts: verdict[1].toUpperCase() === "YES",
|
|
4056
|
+
reason: truncate(reason, MAX_REASON_CHARS)
|
|
4057
|
+
};
|
|
4058
|
+
}
|
|
4059
|
+
|
|
4060
|
+
// src/retrieval/contradiction.ts
|
|
4061
|
+
var DEFAULT_LIMIT = 10;
|
|
4062
|
+
var DEFAULT_NEIGHBOR_LIMIT = 25;
|
|
4063
|
+
async function checkContradictions(store, embeddingProvider, slmProvider, projectId, candidates, opts = {}) {
|
|
4064
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
4065
|
+
const neighborLimit = opts.neighborLimit ?? DEFAULT_NEIGHBOR_LIMIT;
|
|
4066
|
+
const model = opts.model ?? DEFAULT_SLM_MODEL;
|
|
4067
|
+
const suggestions = [];
|
|
4068
|
+
let judgments = 0;
|
|
4069
|
+
let consecutiveNullReplies = 0;
|
|
4070
|
+
for (const candidate of candidates.slice(0, limit)) {
|
|
4071
|
+
if (opts.maxJudgments !== void 0 && judgments >= opts.maxJudgments) break;
|
|
4072
|
+
const full = store.getNodesByIds([candidate.id])[0];
|
|
4073
|
+
if (!full) continue;
|
|
4074
|
+
const embedding = store.getEmbedding(candidate.id) ?? await embeddingProvider.embed(`${full.title}
|
|
4075
|
+
${full.body}`);
|
|
4076
|
+
if (!embedding) continue;
|
|
4077
|
+
const candidateEpoch = Date.parse(candidate.ts);
|
|
4078
|
+
const nearest = store.vectorSearch(projectId, embedding, neighborLimit + 1).find((hit) => hit.id !== candidate.id && Date.parse(hit.ts) > candidateEpoch);
|
|
4079
|
+
if (!nearest) continue;
|
|
4080
|
+
if (store.hasContradictionCheck(candidate.id, nearest.id)) continue;
|
|
4081
|
+
const reply = await slmProvider.complete(buildContradictionPrompt(full, nearest));
|
|
4082
|
+
if (!reply) {
|
|
4083
|
+
consecutiveNullReplies += 1;
|
|
4084
|
+
if (consecutiveNullReplies >= 2) break;
|
|
4085
|
+
continue;
|
|
4086
|
+
}
|
|
4087
|
+
consecutiveNullReplies = 0;
|
|
4088
|
+
const verdict = parseContradictionVerdict(reply);
|
|
4089
|
+
if (!verdict) continue;
|
|
4090
|
+
judgments += 1;
|
|
4091
|
+
store.recordContradictionCheck({
|
|
4092
|
+
candidateId: candidate.id,
|
|
4093
|
+
againstId: nearest.id,
|
|
4094
|
+
contradicts: verdict.contradicts,
|
|
4095
|
+
reason: verdict.contradicts ? verdict.reason : null,
|
|
4096
|
+
model
|
|
4097
|
+
});
|
|
4098
|
+
if (!verdict.contradicts) continue;
|
|
4099
|
+
suggestions.push({
|
|
4100
|
+
candidateId: candidate.id,
|
|
4101
|
+
againstId: nearest.id,
|
|
4102
|
+
againstTitle: nearest.title,
|
|
4103
|
+
reason: verdict.reason
|
|
4104
|
+
});
|
|
4105
|
+
}
|
|
4106
|
+
return suggestions;
|
|
4107
|
+
}
|
|
4108
|
+
|
|
3823
4109
|
// src/shell/detect.ts
|
|
3824
4110
|
import { existsSync as existsSync4 } from "fs";
|
|
3825
4111
|
import { readFile as readFile9, stat as stat2 } from "fs/promises";
|
|
@@ -4922,13 +5208,38 @@ function runPruneSources(store, projectId, otherProjectIds, sources, yes, out) {
|
|
|
4922
5208
|
);
|
|
4923
5209
|
return 0;
|
|
4924
5210
|
}
|
|
5211
|
+
const startedAt = Date.now();
|
|
4925
5212
|
let removed = 0;
|
|
4926
5213
|
for (const { source, id } of counts) removed += store.pruneSourceNodes(id, source, []);
|
|
5214
|
+
store.recordMutationAudit({
|
|
5215
|
+
action: "prune_source",
|
|
5216
|
+
projectId,
|
|
5217
|
+
detail: { sources, scopeProjectIds: scopeIds },
|
|
5218
|
+
affectedCount: removed,
|
|
5219
|
+
succeeded: true,
|
|
5220
|
+
startedAt,
|
|
5221
|
+
finishedAt: Date.now()
|
|
5222
|
+
});
|
|
4927
5223
|
const identityPart = otherProjectIds.length > 0 ? `, ${scopeIds.length} project identit${scopeIds.length === 1 ? "y" : "ies"}` : "";
|
|
4928
5224
|
out(`${pc7.green("pruned")} ${removed} node(s) across ${sources.length} source(s)${identityPart}
|
|
4929
5225
|
`);
|
|
4930
5226
|
return 0;
|
|
4931
5227
|
}
|
|
5228
|
+
async function runAutoContradictionCheck(store, config, projectId, providers) {
|
|
5229
|
+
if (!config.contradictions.autoCheck) return "";
|
|
5230
|
+
const candidates = store.listStaleCandidates(projectId);
|
|
5231
|
+
if (candidates.length === 0) return "";
|
|
5232
|
+
const fresh = await checkContradictions(store, providers.embedder, providers.slm, projectId, candidates, {
|
|
5233
|
+
limit: candidates.length,
|
|
5234
|
+
maxJudgments: config.contradictions.maxPerSync,
|
|
5235
|
+
model: config.contradictions.model
|
|
5236
|
+
});
|
|
5237
|
+
const open = store.countContradictionSuggestions(projectId);
|
|
5238
|
+
if (fresh.length === 0 && open === 0) return "";
|
|
5239
|
+
const freshPart = fresh.length > 0 ? pc7.yellow(`${fresh.length} new`) : `${fresh.length} new`;
|
|
5240
|
+
return ` ${pc7.dim("contradictions:")} ${freshPart}${pc7.dim(`, ${open} open suggestion(s) -- run`)} ${pc7.bold("nexusmem stale")} ${pc7.dim("for detail")}
|
|
5241
|
+
`;
|
|
5242
|
+
}
|
|
4932
5243
|
async function runSync(opts) {
|
|
4933
5244
|
const { repo, ws, projectId, config } = await loadContext(opts.cwd);
|
|
4934
5245
|
const log = (line) => {
|
|
@@ -4982,6 +5293,7 @@ async function runSync(opts) {
|
|
|
4982
5293
|
const docs = await syncDocs(store, projectId, repo.root, config, log);
|
|
4983
5294
|
const structure = await syncStructure(store, projectId, repo.root, config, log);
|
|
4984
5295
|
let embedLine = "";
|
|
5296
|
+
let embeddingAvailable = false;
|
|
4985
5297
|
if (!opts.noEmbed) {
|
|
4986
5298
|
let lastLogged = 0;
|
|
4987
5299
|
const result = await embedPendingNodes(store, new OllamaEmbeddingProvider(), projectId, {
|
|
@@ -4993,6 +5305,7 @@ async function runSync(opts) {
|
|
|
4993
5305
|
log(` ${pc7.dim(`vector: ${attempted}/${total} embedded`)}`);
|
|
4994
5306
|
}
|
|
4995
5307
|
});
|
|
5308
|
+
embeddingAvailable = !result.providerUnavailable;
|
|
4996
5309
|
if (result.embedded > 0) {
|
|
4997
5310
|
const skippedPart = result.skipped > 0 ? pc7.dim(`, ${result.skipped} skipped`) : "";
|
|
4998
5311
|
const remainingPart = result.remaining > 0 ? pc7.yellow(`, ${result.remaining} still pending`) : "";
|
|
@@ -5002,6 +5315,10 @@ async function runSync(opts) {
|
|
|
5002
5315
|
log(`${pc7.dim("vector")} embedding provider unavailable (is Ollama running with nomic-embed-text pulled?) -- BM25-only for now`);
|
|
5003
5316
|
}
|
|
5004
5317
|
}
|
|
5318
|
+
const contradictionLine = !opts.noEmbed && embeddingAvailable ? await runAutoContradictionCheck(store, config, projectId, {
|
|
5319
|
+
embedder: new OllamaEmbeddingProvider(),
|
|
5320
|
+
slm: new OllamaChatProvider({ model: config.contradictions.model })
|
|
5321
|
+
}) : "";
|
|
5005
5322
|
let linkLine = "";
|
|
5006
5323
|
if (opts.linkFailures) {
|
|
5007
5324
|
const linkStats = correlateFailures(store, projectId);
|
|
@@ -5030,7 +5347,7 @@ async function runSync(opts) {
|
|
|
5030
5347
|
` ${pc7.green(`+${totals.inserted} new`)} ${pc7.yellow(`~${totals.updated} updated`)} ${pc7.dim(`=${totals.unchanged} unchanged`)}${deniedPart}`,
|
|
5031
5348
|
` ${pc7.dim(`${stats2.total} node(s) total across ${stats2.distinctFiles} file path(s)`)}`,
|
|
5032
5349
|
""
|
|
5033
|
-
].join("\n") + embedLine + linkLine
|
|
5350
|
+
].join("\n") + embedLine + linkLine + contradictionLine
|
|
5034
5351
|
);
|
|
5035
5352
|
return 0;
|
|
5036
5353
|
} finally {
|
|
@@ -5045,10 +5362,17 @@ async function searchMemory(input) {
|
|
|
5045
5362
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5046
5363
|
const budget = input.budget ?? 2e3;
|
|
5047
5364
|
const candidates = input.candidates ?? 30;
|
|
5365
|
+
let asOfEpoch;
|
|
5366
|
+
if (input.asOf) {
|
|
5367
|
+
const parsed = Date.parse(input.asOf);
|
|
5368
|
+
if (Number.isNaN(parsed)) throw new Error(`asOf "${input.asOf}" is not a parseable date`);
|
|
5369
|
+
asOfEpoch = parsed;
|
|
5370
|
+
}
|
|
5048
5371
|
const queryOpts = {
|
|
5049
5372
|
budget,
|
|
5050
5373
|
candidates,
|
|
5051
|
-
embeddingProvider: input.noVector ? null : new OllamaEmbeddingProvider()
|
|
5374
|
+
embeddingProvider: input.noVector ? null : new OllamaEmbeddingProvider(),
|
|
5375
|
+
asOfEpoch
|
|
5052
5376
|
};
|
|
5053
5377
|
if (input.allProjects) {
|
|
5054
5378
|
const opened = await openAllProjectSources({ projectId, root: repo.root, dbPath: ws.dbPath });
|
|
@@ -5144,11 +5468,14 @@ function createServer() {
|
|
|
5144
5468
|
budget: z4.number().int().positive().optional().describe("Max tokens in the returned context block. Default 2000."),
|
|
5145
5469
|
allProjects: z4.boolean().optional().describe(
|
|
5146
5470
|
"Search every repository NexusMem has been run in on this machine, not just projectRoot. Use when the answer may live in a different project (a pattern solved elsewhere, a tool that failed the same way before). Each result is tagged with its repository."
|
|
5471
|
+
),
|
|
5472
|
+
asOf: z4.string().optional().describe(
|
|
5473
|
+
'ISO-8601 date/time. Restricts results to nodes recorded at or before this instant -- "what did memory hold as of then", not "what happened then". Omit for the normal, unrestricted read.'
|
|
5147
5474
|
)
|
|
5148
5475
|
}
|
|
5149
5476
|
},
|
|
5150
|
-
async ({ projectRoot, query, budget, allProjects }) => {
|
|
5151
|
-
const result = await searchMemory({ projectRoot, query, budget, allProjects });
|
|
5477
|
+
async ({ projectRoot, query, budget, allProjects, asOf }) => {
|
|
5478
|
+
const result = await searchMemory({ projectRoot, query, budget, allProjects, asOf });
|
|
5152
5479
|
return {
|
|
5153
5480
|
content: [{ type: "text", text: result.text }],
|
|
5154
5481
|
structuredContent: {
|
|
@@ -5333,8 +5660,20 @@ ${pc8.dim("-".repeat(40))}
|
|
|
5333
5660
|
|
|
5334
5661
|
// src/cli/commands/query.ts
|
|
5335
5662
|
import pc9 from "picocolors";
|
|
5663
|
+
var QueryError = class extends Error {
|
|
5664
|
+
constructor(message) {
|
|
5665
|
+
super(message);
|
|
5666
|
+
this.name = "QueryError";
|
|
5667
|
+
}
|
|
5668
|
+
};
|
|
5336
5669
|
async function runQuery(opts) {
|
|
5337
5670
|
const { repo, ws, projectId } = await loadContext(opts.cwd);
|
|
5671
|
+
let asOfEpoch;
|
|
5672
|
+
if (opts.asOf) {
|
|
5673
|
+
const parsed = Date.parse(opts.asOf);
|
|
5674
|
+
if (Number.isNaN(parsed)) throw new QueryError(`--as-of "${opts.asOf}" is not a parseable date`);
|
|
5675
|
+
asOfEpoch = parsed;
|
|
5676
|
+
}
|
|
5338
5677
|
const opened = opts.allProjects ? await openAllProjectSources({ projectId, root: repo.root, dbPath: ws.dbPath }) : null;
|
|
5339
5678
|
let store = null;
|
|
5340
5679
|
try {
|
|
@@ -5342,7 +5681,8 @@ async function runQuery(opts) {
|
|
|
5342
5681
|
budget: opts.budget,
|
|
5343
5682
|
candidates: opts.candidates,
|
|
5344
5683
|
halfLifeDays: opts.halfLifeDays,
|
|
5345
|
-
embeddingProvider: opts.noVector ? null : new OllamaEmbeddingProvider()
|
|
5684
|
+
embeddingProvider: opts.noVector ? null : new OllamaEmbeddingProvider(),
|
|
5685
|
+
asOfEpoch
|
|
5346
5686
|
};
|
|
5347
5687
|
let result;
|
|
5348
5688
|
if (opened) {
|
|
@@ -5352,6 +5692,10 @@ async function runQuery(opts) {
|
|
|
5352
5692
|
result = await runHybridQuery(store, projectId, opts.query, queryOpts);
|
|
5353
5693
|
}
|
|
5354
5694
|
const { bm25Count, vectorCount, hits, packed } = result;
|
|
5695
|
+
if (asOfEpoch !== void 0 && !opts.json) {
|
|
5696
|
+
process.stderr.write(`${pc9.dim("as of ")} ${new Date(asOfEpoch).toISOString()} -- excludes anything recorded after
|
|
5697
|
+
`);
|
|
5698
|
+
}
|
|
5355
5699
|
if (opened && !opts.json) {
|
|
5356
5700
|
const searched = opened.sources.map((s) => s.label).join(", ");
|
|
5357
5701
|
process.stderr.write(`${pc9.dim("scope ")} ${opened.sources.length} project(s): ${searched}
|
|
@@ -5413,11 +5757,45 @@ async function runQuery(opts) {
|
|
|
5413
5757
|
}
|
|
5414
5758
|
}
|
|
5415
5759
|
|
|
5760
|
+
// src/cli/commands/review.ts
|
|
5761
|
+
import pc10 from "picocolors";
|
|
5762
|
+
var ReviewError = class extends Error {
|
|
5763
|
+
constructor(message) {
|
|
5764
|
+
super(message);
|
|
5765
|
+
this.name = "ReviewError";
|
|
5766
|
+
}
|
|
5767
|
+
};
|
|
5768
|
+
async function runReview(opts) {
|
|
5769
|
+
const { projectId, ws } = await loadContext(opts.cwd);
|
|
5770
|
+
const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
|
|
5771
|
+
const store = MemoryStore.open(ws.dbPath);
|
|
5772
|
+
try {
|
|
5773
|
+
const owner = store.getNodeProjectId(opts.nodeId);
|
|
5774
|
+
if (owner === null) {
|
|
5775
|
+
throw new ReviewError(`no node found with id ${opts.nodeId}`);
|
|
5776
|
+
}
|
|
5777
|
+
if (owner !== projectId) {
|
|
5778
|
+
throw new ReviewError("node must belong to the current project");
|
|
5779
|
+
}
|
|
5780
|
+
store.setTrustState(opts.nodeId, opts.verdict);
|
|
5781
|
+
const verb = opts.verdict === "verified" ? "verified" : "rejected";
|
|
5782
|
+
out(
|
|
5783
|
+
`${pc10.green(verb)} ${opts.nodeId}
|
|
5784
|
+
` + (opts.verdict === "rejected" ? `${pc10.dim("down-weighted in ranking")} -- the node stays queryable, just ranked lower
|
|
5785
|
+
` : `${pc10.dim("labeled only")} -- verifying does not change ranking
|
|
5786
|
+
`)
|
|
5787
|
+
);
|
|
5788
|
+
return 0;
|
|
5789
|
+
} finally {
|
|
5790
|
+
store.close();
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5416
5794
|
// src/cli/commands/scan-conversation.ts
|
|
5417
|
-
import
|
|
5795
|
+
import pc12 from "picocolors";
|
|
5418
5796
|
|
|
5419
5797
|
// src/cli/format.ts
|
|
5420
|
-
import
|
|
5798
|
+
import pc11 from "picocolors";
|
|
5421
5799
|
var GIT_SIGNAL_BANDS = { high: 0.7, medium: 0.45 };
|
|
5422
5800
|
var SHELL_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
|
|
5423
5801
|
var CONVERSATION_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
@@ -5429,9 +5807,9 @@ function signalBand(signal, bands) {
|
|
|
5429
5807
|
return "low";
|
|
5430
5808
|
}
|
|
5431
5809
|
var BAND_COLOR = {
|
|
5432
|
-
high:
|
|
5433
|
-
medium:
|
|
5434
|
-
low:
|
|
5810
|
+
high: pc11.green,
|
|
5811
|
+
medium: pc11.yellow,
|
|
5812
|
+
low: pc11.dim
|
|
5435
5813
|
};
|
|
5436
5814
|
function formatSignal(signal, bands) {
|
|
5437
5815
|
return BAND_COLOR[signalBand(signal, bands)](signal.toFixed(2));
|
|
@@ -5440,7 +5818,7 @@ function approxTotalTokens(nodes) {
|
|
|
5440
5818
|
return nodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
5441
5819
|
}
|
|
5442
5820
|
function summarize2(nodes) {
|
|
5443
|
-
if (nodes.length === 0) return
|
|
5821
|
+
if (nodes.length === 0) return pc11.yellow("no commits matched");
|
|
5444
5822
|
const timestamps = nodes.map((n) => n.ts).sort();
|
|
5445
5823
|
const avgSignal = nodes.reduce((n, x) => n + x.signal, 0) / nodes.length;
|
|
5446
5824
|
const totalTokens = approxTotalTokens(nodes);
|
|
@@ -5450,7 +5828,7 @@ function summarize2(nodes) {
|
|
|
5450
5828
|
}
|
|
5451
5829
|
const hottest = [...fileHits.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([path, count]) => ` ${String(count).padStart(3)}x ${path}`);
|
|
5452
5830
|
return [
|
|
5453
|
-
`${
|
|
5831
|
+
`${pc11.bold(String(nodes.length))} nodes ${pc11.dim(`${timestamps[0]?.slice(0, 10)} .. ${timestamps.at(-1)?.slice(0, 10)}`)}`,
|
|
5454
5832
|
` avg signal ${avgSignal.toFixed(3)} ~${totalTokens.toLocaleString()} tokens if sent raw`,
|
|
5455
5833
|
hottest.length ? ` hottest files:
|
|
5456
5834
|
${hottest.join("\n")}` : ""
|
|
@@ -5464,9 +5842,9 @@ async function runScanConversation(opts) {
|
|
|
5464
5842
|
const files = await listTranscriptFiles(repo.root);
|
|
5465
5843
|
if (!opts.json) {
|
|
5466
5844
|
process.stderr.write(
|
|
5467
|
-
files.length ? `${
|
|
5845
|
+
files.length ? `${pc12.dim("transcripts")} ${files.length} file(s) in ${claudeProjectTranscriptDir(repo.root)}
|
|
5468
5846
|
|
|
5469
|
-
` : `${
|
|
5847
|
+
` : `${pc12.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
|
|
5470
5848
|
`
|
|
5471
5849
|
);
|
|
5472
5850
|
}
|
|
@@ -5483,7 +5861,7 @@ async function runScanConversation(opts) {
|
|
|
5483
5861
|
const approxTotal = approxTotalTokens(nodes);
|
|
5484
5862
|
process.stderr.write(
|
|
5485
5863
|
`
|
|
5486
|
-
${
|
|
5864
|
+
${pc12.bold(String(nodes.length))} of ${turns.length} exchange(s) above threshold ${pc12.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}` + (redactedTotal > 0 ? ` ${pc12.yellow(`${redactedTotal} secret-like value(s) redacted`)}` : "") + "\n"
|
|
5487
5865
|
);
|
|
5488
5866
|
return 0;
|
|
5489
5867
|
}
|
|
@@ -5492,7 +5870,7 @@ function formatNode(node) {
|
|
|
5492
5870
|
}
|
|
5493
5871
|
|
|
5494
5872
|
// src/cli/commands/scan-diff.ts
|
|
5495
|
-
import
|
|
5873
|
+
import pc13 from "picocolors";
|
|
5496
5874
|
var DEFAULT_SCAN_COMMITS = 50;
|
|
5497
5875
|
async function runScanDiff(opts) {
|
|
5498
5876
|
const repo = await readRepoInfo(opts.cwd);
|
|
@@ -5500,9 +5878,9 @@ async function runScanDiff(opts) {
|
|
|
5500
5878
|
if (!opts.json) {
|
|
5501
5879
|
process.stderr.write(
|
|
5502
5880
|
[
|
|
5503
|
-
`${
|
|
5504
|
-
`${
|
|
5505
|
-
`${
|
|
5881
|
+
`${pc13.dim("repo ")} ${repo.root}`,
|
|
5882
|
+
`${pc13.dim("branch ")} ${repo.branch ?? pc13.yellow("(detached)")}`,
|
|
5883
|
+
`${pc13.dim("project")} ${pc13.cyan(projectId)}`,
|
|
5506
5884
|
""
|
|
5507
5885
|
].join("\n")
|
|
5508
5886
|
);
|
|
@@ -5532,28 +5910,28 @@ function formatNode2(node) {
|
|
|
5532
5910
|
const churn = `+${node.meta.insertions ?? 0}/-${node.meta.deletions ?? 0}`;
|
|
5533
5911
|
return [
|
|
5534
5912
|
formatSignal(node.signal, DIFF_SIGNAL_BANDS),
|
|
5535
|
-
|
|
5536
|
-
|
|
5913
|
+
pc13.dim(node.ts.slice(0, 10)),
|
|
5914
|
+
pc13.magenta(sha),
|
|
5537
5915
|
String(node.meta.path ?? ""),
|
|
5538
|
-
|
|
5916
|
+
pc13.dim(`(${churn}, ${node.meta.hunkCount ?? 0} hunk(s))`)
|
|
5539
5917
|
].join(" ");
|
|
5540
5918
|
}
|
|
5541
5919
|
|
|
5542
5920
|
// src/cli/commands/scan-docs.ts
|
|
5543
|
-
import
|
|
5921
|
+
import pc14 from "picocolors";
|
|
5544
5922
|
async function runScanDocs(opts) {
|
|
5545
5923
|
const repo = await readRepoInfo(opts.cwd);
|
|
5546
5924
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5547
5925
|
const { files, unreadable } = await readDocFiles(repo.root);
|
|
5548
5926
|
if (!opts.json) {
|
|
5549
5927
|
process.stderr.write(
|
|
5550
|
-
files.length ? `${
|
|
5928
|
+
files.length ? `${pc14.dim("tracked .md files")} ${files.map((f) => f.path).join(", ")}
|
|
5551
5929
|
|
|
5552
|
-
` : `${
|
|
5930
|
+
` : `${pc14.yellow("no tracked .md files found")}
|
|
5553
5931
|
`
|
|
5554
5932
|
);
|
|
5555
5933
|
if (unreadable.length > 0) {
|
|
5556
|
-
process.stderr.write(`${
|
|
5934
|
+
process.stderr.write(`${pc14.yellow("unreadable")} ${unreadable.join(", ")}
|
|
5557
5935
|
|
|
5558
5936
|
`);
|
|
5559
5937
|
}
|
|
@@ -5569,7 +5947,7 @@ async function runScanDocs(opts) {
|
|
|
5569
5947
|
const approxTotal = approxTotalTokens(nodes);
|
|
5570
5948
|
process.stderr.write(
|
|
5571
5949
|
`
|
|
5572
|
-
${
|
|
5950
|
+
${pc14.bold(String(nodes.length))} section(s) from ${files.length} file(s) above threshold ${pc14.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
5573
5951
|
`
|
|
5574
5952
|
);
|
|
5575
5953
|
return 0;
|
|
@@ -5579,17 +5957,17 @@ function formatNode3(node) {
|
|
|
5579
5957
|
}
|
|
5580
5958
|
|
|
5581
5959
|
// src/cli/commands/scan-git.ts
|
|
5582
|
-
import
|
|
5960
|
+
import pc15 from "picocolors";
|
|
5583
5961
|
async function runScanGit(opts) {
|
|
5584
5962
|
const repo = await readRepoInfo(opts.cwd);
|
|
5585
5963
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5586
5964
|
if (!opts.json) {
|
|
5587
5965
|
process.stderr.write(
|
|
5588
5966
|
[
|
|
5589
|
-
`${
|
|
5590
|
-
`${
|
|
5591
|
-
`${
|
|
5592
|
-
`${
|
|
5967
|
+
`${pc15.dim("repo ")} ${repo.root}`,
|
|
5968
|
+
`${pc15.dim("branch ")} ${repo.branch ?? pc15.yellow("(detached)")}`,
|
|
5969
|
+
`${pc15.dim("origin ")} ${repo.originUrl ?? pc15.dim("(none)")}`,
|
|
5970
|
+
`${pc15.dim("project")} ${pc15.cyan(projectId)}`,
|
|
5593
5971
|
""
|
|
5594
5972
|
].join("\n")
|
|
5595
5973
|
);
|
|
@@ -5623,21 +6001,21 @@ function formatNode4(node) {
|
|
|
5623
6001
|
const churn = `+${node.meta.insertions ?? 0}/-${node.meta.deletions ?? 0}`;
|
|
5624
6002
|
return [
|
|
5625
6003
|
formatSignal(node.signal, GIT_SIGNAL_BANDS),
|
|
5626
|
-
|
|
5627
|
-
|
|
6004
|
+
pc15.dim(date),
|
|
6005
|
+
pc15.magenta(sha),
|
|
5628
6006
|
node.title,
|
|
5629
|
-
|
|
6007
|
+
pc15.dim(`(${files} file${files === 1 ? "" : "s"}, ${churn})`)
|
|
5630
6008
|
].join(" ");
|
|
5631
6009
|
}
|
|
5632
6010
|
|
|
5633
6011
|
// src/cli/commands/scan-session.ts
|
|
5634
|
-
import
|
|
6012
|
+
import pc16 from "picocolors";
|
|
5635
6013
|
async function runScanSession(opts) {
|
|
5636
6014
|
const repo = await readRepoInfo(opts.cwd);
|
|
5637
6015
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5638
6016
|
const turns = await collectClaudeCodeTranscripts(repo.root);
|
|
5639
6017
|
if (turns.length === 0) {
|
|
5640
|
-
process.stderr.write(`${
|
|
6018
|
+
process.stderr.write(`${pc16.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
|
|
5641
6019
|
`);
|
|
5642
6020
|
return 0;
|
|
5643
6021
|
}
|
|
@@ -5645,7 +6023,7 @@ async function runScanSession(opts) {
|
|
|
5645
6023
|
const settled = selectSettledSessions(sessions, opts.settleMinutes);
|
|
5646
6024
|
if (!opts.json) {
|
|
5647
6025
|
process.stderr.write(
|
|
5648
|
-
`${
|
|
6026
|
+
`${pc16.dim("sessions")} ${sessions.length} found, ${settled.length} settled (quiet ${opts.settleMinutes}m+)
|
|
5649
6027
|
|
|
5650
6028
|
`
|
|
5651
6029
|
);
|
|
@@ -5671,7 +6049,7 @@ async function runScanSession(opts) {
|
|
|
5671
6049
|
}
|
|
5672
6050
|
for (const preview of previews) {
|
|
5673
6051
|
process.stdout.write(
|
|
5674
|
-
`${
|
|
6052
|
+
`${pc16.bold(preview.sessionKey)} ${preview.startedAt.slice(0, 16).replace("T", " ")} ${preview.includedTurns}/${preview.turns} turn(s), ${preview.promptChars} chars
|
|
5675
6053
|
${preview.prompt}
|
|
5676
6054
|
|
|
5677
6055
|
`
|
|
@@ -5683,7 +6061,7 @@ ${preview.prompt}
|
|
|
5683
6061
|
settleMinutes: opts.settleMinutes,
|
|
5684
6062
|
maxSessions: opts.maxSessions,
|
|
5685
6063
|
onProgress: (done, total) => {
|
|
5686
|
-
if (!opts.json) process.stderr.write(` ${
|
|
6064
|
+
if (!opts.json) process.stderr.write(` ${pc16.dim(`summarizing ${done}/${total}`)}
|
|
5687
6065
|
`);
|
|
5688
6066
|
}
|
|
5689
6067
|
});
|
|
@@ -5693,21 +6071,21 @@ ${preview.prompt}
|
|
|
5693
6071
|
return 0;
|
|
5694
6072
|
}
|
|
5695
6073
|
for (const node of result.nodes) {
|
|
5696
|
-
process.stdout.write(`${
|
|
5697
|
-
${
|
|
6074
|
+
process.stdout.write(`${pc16.bold(node.title)}
|
|
6075
|
+
${pc16.dim(node.ts.slice(0, 16).replace("T", " "))}
|
|
5698
6076
|
${node.body}
|
|
5699
6077
|
|
|
5700
6078
|
`);
|
|
5701
6079
|
}
|
|
5702
6080
|
if (result.providerUnavailable) {
|
|
5703
6081
|
process.stderr.write(
|
|
5704
|
-
`${
|
|
6082
|
+
`${pc16.yellow("model unavailable")} -- is Ollama running with \`${opts.model}\` pulled? (\`ollama pull ${opts.model}\`)
|
|
5705
6083
|
`
|
|
5706
6084
|
);
|
|
5707
6085
|
return 0;
|
|
5708
6086
|
}
|
|
5709
6087
|
process.stderr.write(
|
|
5710
|
-
`${
|
|
6088
|
+
`${pc16.bold(String(result.nodes.length))} summarized` + (result.failed > 0 ? `, ${pc16.yellow(`${result.failed} failed`)}` : "") + ` ${pc16.dim(`(model ${opts.model})`)}
|
|
5711
6089
|
`
|
|
5712
6090
|
);
|
|
5713
6091
|
return 0;
|
|
@@ -5715,16 +6093,16 @@ ${node.body}
|
|
|
5715
6093
|
var SCAN_SESSION_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
5716
6094
|
|
|
5717
6095
|
// src/cli/commands/scan-shell.ts
|
|
5718
|
-
import
|
|
6096
|
+
import pc17 from "picocolors";
|
|
5719
6097
|
async function runScanShell(opts) {
|
|
5720
6098
|
const repo = await readRepoInfo(opts.cwd);
|
|
5721
6099
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5722
6100
|
const results = await collectAvailableShellHistory({ tailLines: opts.tailLines, repoRoot: repo.root });
|
|
5723
6101
|
if (!opts.json) {
|
|
5724
6102
|
process.stderr.write(
|
|
5725
|
-
results.length ? `${
|
|
6103
|
+
results.length ? `${pc17.dim("sources found")} ${results.map((r) => r.name).join(", ")}
|
|
5726
6104
|
|
|
5727
|
-
` : `${
|
|
6105
|
+
` : `${pc17.yellow("no shell history source found on this machine")}
|
|
5728
6106
|
`
|
|
5729
6107
|
);
|
|
5730
6108
|
}
|
|
@@ -5733,7 +6111,7 @@ async function runScanShell(opts) {
|
|
|
5733
6111
|
const nodes = collectShellHistory(result.entries, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
5734
6112
|
allNodes.push(...nodes);
|
|
5735
6113
|
if (!opts.json) {
|
|
5736
|
-
process.stdout.write(`${
|
|
6114
|
+
process.stdout.write(`${pc17.bold(`shell:${result.name}`)} ${pc17.dim(`(${nodes.length} of ${result.entries.length} above threshold)`)}
|
|
5737
6115
|
`);
|
|
5738
6116
|
for (const node of nodes) process.stdout.write(`${formatNode5(node)}
|
|
5739
6117
|
`);
|
|
@@ -5746,19 +6124,19 @@ async function runScanShell(opts) {
|
|
|
5746
6124
|
return 0;
|
|
5747
6125
|
}
|
|
5748
6126
|
const approxTotal = approxTotalTokens(allNodes);
|
|
5749
|
-
process.stderr.write(`${
|
|
6127
|
+
process.stderr.write(`${pc17.bold(String(allNodes.length))} node(s) total ${pc17.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
5750
6128
|
`);
|
|
5751
6129
|
return 0;
|
|
5752
6130
|
}
|
|
5753
6131
|
function formatNode5(node) {
|
|
5754
|
-
const approx = node.meta.tsApprox ?
|
|
6132
|
+
const approx = node.meta.tsApprox ? pc17.dim("~") : " ";
|
|
5755
6133
|
const exit = node.meta.exitCode;
|
|
5756
|
-
const exitLabel = typeof exit === "number" && exit !== 0 ?
|
|
6134
|
+
const exitLabel = typeof exit === "number" && exit !== 0 ? pc17.red(`exit ${exit}`) : "";
|
|
5757
6135
|
return [formatSignal(node.signal, SHELL_SIGNAL_BANDS), approx + node.ts.slice(0, 16).replace("T", " "), node.title, exitLabel].filter(Boolean).join(" ");
|
|
5758
6136
|
}
|
|
5759
6137
|
|
|
5760
6138
|
// src/cli/commands/scan-structure.ts
|
|
5761
|
-
import
|
|
6139
|
+
import pc18 from "picocolors";
|
|
5762
6140
|
var TRACKED_EXTENSIONS = TRACKED_PATHSPECS.map((p) => p.replace("*", "")).join("/");
|
|
5763
6141
|
async function runScanStructure(opts) {
|
|
5764
6142
|
const repo = await readRepoInfo(opts.cwd);
|
|
@@ -5769,107 +6147,42 @@ async function runScanStructure(opts) {
|
|
|
5769
6147
|
return 0;
|
|
5770
6148
|
}
|
|
5771
6149
|
if (unreadable.length > 0) {
|
|
5772
|
-
process.stderr.write(`${
|
|
6150
|
+
process.stderr.write(`${pc18.yellow("unreadable")} ${unreadable.join(", ")}
|
|
5773
6151
|
|
|
5774
6152
|
`);
|
|
5775
6153
|
}
|
|
5776
6154
|
for (const edge of edges) {
|
|
5777
|
-
process.stdout.write(`${edge.fromPath} ${
|
|
6155
|
+
process.stdout.write(`${edge.fromPath} ${pc18.dim("->")} ${edge.toPath}
|
|
5778
6156
|
`);
|
|
5779
6157
|
}
|
|
5780
6158
|
process.stderr.write(
|
|
5781
6159
|
`
|
|
5782
|
-
${
|
|
6160
|
+
${pc18.bold(String(edges.length))} edge(s) from ${filesScanned} tracked ${TRACKED_EXTENSIONS} file(s)
|
|
5783
6161
|
`
|
|
5784
6162
|
);
|
|
5785
6163
|
return 0;
|
|
5786
6164
|
}
|
|
5787
6165
|
|
|
5788
6166
|
// src/cli/commands/stale.ts
|
|
5789
|
-
import
|
|
5790
|
-
|
|
5791
|
-
// src/slm/contradiction.ts
|
|
5792
|
-
var MAX_BODY_CHARS = 1500;
|
|
5793
|
-
var MAX_REASON_CHARS = 200;
|
|
5794
|
-
var CONTRADICTION_INSTRUCTIONS = `You are checking whether a NEWER memory replaces or contradicts an OLDER one, for an AI coding assistant's memory index.
|
|
5795
|
-
|
|
5796
|
-
Answer in exactly this shape:
|
|
5797
|
-
VERDICT: YES or NO
|
|
5798
|
-
REASON: <one line, under 20 words>
|
|
5799
|
-
|
|
5800
|
-
Say YES only if the NEWER memory states something that makes the OLDER one factually wrong or obsolete -- a decision reversed, a bug fixed, a plan abandoned. Say NO if they are about different things, or the newer one only adds detail without contradicting the older one. When unsure, say NO.`;
|
|
5801
|
-
function buildContradictionPrompt(older, newer) {
|
|
5802
|
-
const body = [
|
|
5803
|
-
`OLDER (${older.title}):`,
|
|
5804
|
-
truncate(older.body, MAX_BODY_CHARS),
|
|
5805
|
-
"",
|
|
5806
|
-
`NEWER (${newer.title}):`,
|
|
5807
|
-
truncate(newer.body, MAX_BODY_CHARS)
|
|
5808
|
-
].join("\n");
|
|
5809
|
-
return `${CONTRADICTION_INSTRUCTIONS}
|
|
5810
|
-
|
|
5811
|
-
---
|
|
5812
|
-
|
|
5813
|
-
${body}
|
|
5814
|
-
|
|
5815
|
-
---
|
|
5816
|
-
|
|
5817
|
-
Answer:`;
|
|
5818
|
-
}
|
|
5819
|
-
function parseContradictionVerdict(raw) {
|
|
5820
|
-
const lines = raw.trim().split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
5821
|
-
const verdictLine = lines.find((l) => /^VERDICT:/i.test(l));
|
|
5822
|
-
if (!verdictLine) return null;
|
|
5823
|
-
const verdict = /^VERDICT:\s*(YES|NO)\b/i.exec(verdictLine);
|
|
5824
|
-
if (!verdict) return null;
|
|
5825
|
-
const reasonLine = lines.find((l) => /^REASON:/i.test(l));
|
|
5826
|
-
const reason = reasonLine ? reasonLine.replace(/^REASON:\s*/i, "").trim() : "";
|
|
5827
|
-
return {
|
|
5828
|
-
contradicts: verdict[1].toUpperCase() === "YES",
|
|
5829
|
-
reason: truncate(reason, MAX_REASON_CHARS)
|
|
5830
|
-
};
|
|
5831
|
-
}
|
|
5832
|
-
|
|
5833
|
-
// src/retrieval/contradiction.ts
|
|
5834
|
-
var DEFAULT_LIMIT = 10;
|
|
5835
|
-
var DEFAULT_NEIGHBOR_LIMIT = 25;
|
|
5836
|
-
async function checkContradictions(store, embeddingProvider, slmProvider, projectId, candidates, opts = {}) {
|
|
5837
|
-
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
5838
|
-
const neighborLimit = opts.neighborLimit ?? DEFAULT_NEIGHBOR_LIMIT;
|
|
5839
|
-
const suggestions = [];
|
|
5840
|
-
for (const candidate of candidates.slice(0, limit)) {
|
|
5841
|
-
const full = store.getNodesByIds([candidate.id])[0];
|
|
5842
|
-
if (!full) continue;
|
|
5843
|
-
const embedding = await embeddingProvider.embed(`${full.title}
|
|
5844
|
-
${full.body}`);
|
|
5845
|
-
if (!embedding) continue;
|
|
5846
|
-
const candidateEpoch = Date.parse(candidate.ts);
|
|
5847
|
-
const nearest = store.vectorSearch(projectId, embedding, neighborLimit + 1).find((hit) => hit.id !== candidate.id && Date.parse(hit.ts) > candidateEpoch);
|
|
5848
|
-
if (!nearest) continue;
|
|
5849
|
-
const reply = await slmProvider.complete(buildContradictionPrompt(full, nearest));
|
|
5850
|
-
if (!reply) continue;
|
|
5851
|
-
const verdict = parseContradictionVerdict(reply);
|
|
5852
|
-
if (!verdict?.contradicts) continue;
|
|
5853
|
-
suggestions.push({
|
|
5854
|
-
candidateId: candidate.id,
|
|
5855
|
-
againstId: nearest.id,
|
|
5856
|
-
againstTitle: nearest.title,
|
|
5857
|
-
reason: verdict.reason
|
|
5858
|
-
});
|
|
5859
|
-
}
|
|
5860
|
-
return suggestions;
|
|
5861
|
-
}
|
|
5862
|
-
|
|
5863
|
-
// src/cli/commands/stale.ts
|
|
6167
|
+
import pc19 from "picocolors";
|
|
5864
6168
|
var STALE_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
5865
6169
|
async function runStale(opts) {
|
|
5866
6170
|
const { projectId, ws } = await loadContext(opts.cwd);
|
|
5867
6171
|
const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
|
|
5868
6172
|
const store = MemoryStore.open(ws.dbPath);
|
|
5869
6173
|
try {
|
|
6174
|
+
if (opts.dismiss) {
|
|
6175
|
+
const dismissed = store.dismissContradictionSuggestion(projectId, opts.dismiss);
|
|
6176
|
+
out(
|
|
6177
|
+
dismissed > 0 ? `${pc19.green("dismissed")} the contradiction suggestion for ${opts.dismiss} -- it will not resurface
|
|
6178
|
+
` : `${pc19.dim("no open contradiction suggestion")} for ${opts.dismiss} -- nothing to dismiss
|
|
6179
|
+
`
|
|
6180
|
+
);
|
|
6181
|
+
return 0;
|
|
6182
|
+
}
|
|
5870
6183
|
const candidates = store.listStaleCandidates(projectId, { minAgeDays: opts.minAgeDays, limit: opts.limit });
|
|
5871
6184
|
if (candidates.length === 0) {
|
|
5872
|
-
out(`${
|
|
6185
|
+
out(`${pc19.dim("no stale candidates")} -- no unconfirmed node older than the threshold lacks a successor
|
|
5873
6186
|
`);
|
|
5874
6187
|
return 0;
|
|
5875
6188
|
}
|
|
@@ -5880,21 +6193,26 @@ async function runStale(opts) {
|
|
|
5880
6193
|
new OllamaEmbeddingProvider(),
|
|
5881
6194
|
new OllamaChatProvider({ model: opts.model ?? DEFAULT_SLM_MODEL }),
|
|
5882
6195
|
projectId,
|
|
5883
|
-
candidates
|
|
6196
|
+
candidates,
|
|
6197
|
+
{ model: opts.model ?? DEFAULT_SLM_MODEL }
|
|
5884
6198
|
);
|
|
5885
6199
|
}
|
|
5886
|
-
const byCandidateId = new Map(
|
|
6200
|
+
const byCandidateId = /* @__PURE__ */ new Map();
|
|
6201
|
+
for (const s of store.listContradictionSuggestions(projectId)) {
|
|
6202
|
+
byCandidateId.set(s.candidateId, { againstId: s.againstId, againstTitle: s.againstTitle, reason: s.reason ?? "" });
|
|
6203
|
+
}
|
|
6204
|
+
for (const s of suggestions) byCandidateId.set(s.candidateId, s);
|
|
5887
6205
|
out(
|
|
5888
6206
|
[
|
|
5889
|
-
`${
|
|
6207
|
+
`${pc19.bold(String(candidates.length))} stale candidate(s) -- oldest first, none of these were changed:`,
|
|
5890
6208
|
...candidates.map((c) => {
|
|
5891
|
-
const line = ` ${
|
|
6209
|
+
const line = ` ${pc19.dim(c.id)} ${pc19.yellow(`${c.ageDays}d old`)} [${c.kind}] ${c.title}`;
|
|
5892
6210
|
const hit = byCandidateId.get(c.id);
|
|
5893
6211
|
return hit ? `${line}
|
|
5894
|
-
${
|
|
6212
|
+
${pc19.red("likely superseded by")} ${pc19.dim(hit.againstId)} ${hit.againstTitle} -- ${hit.reason}` : line;
|
|
5895
6213
|
}),
|
|
5896
6214
|
"",
|
|
5897
|
-
`run ${
|
|
6215
|
+
`run ${pc19.bold("nexusmem mark-stale <id> --supersedes <newId>")} on any that are actually wrong`
|
|
5898
6216
|
].join("\n").concat("\n")
|
|
5899
6217
|
);
|
|
5900
6218
|
return 0;
|
|
@@ -5906,7 +6224,7 @@ async function runStale(opts) {
|
|
|
5906
6224
|
// src/cli/commands/status.ts
|
|
5907
6225
|
import { basename as basename4 } from "path";
|
|
5908
6226
|
import { statSync } from "fs";
|
|
5909
|
-
import
|
|
6227
|
+
import pc20 from "picocolors";
|
|
5910
6228
|
function daySpan(oldest, newest) {
|
|
5911
6229
|
const oldestDay = Date.parse(oldest.slice(0, 10));
|
|
5912
6230
|
const newestDay = Date.parse(newest.slice(0, 10));
|
|
@@ -5963,35 +6281,37 @@ async function runStatus(opts) {
|
|
|
5963
6281
|
const otherProjectNodes = store.countProjectNodes(otherProjectIds);
|
|
5964
6282
|
const structure = store.fileEdgeStats(projectId);
|
|
5965
6283
|
const staleCount = store.countStaleCandidates(projectId);
|
|
6284
|
+
const flaggedCount = store.countContradictionSuggestions(projectId);
|
|
5966
6285
|
const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
|
|
5967
6286
|
const kinds = Object.entries(stats2.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
|
|
5968
|
-
const staleProjectWarning = otherProjectIds.length ? `${
|
|
6287
|
+
const staleProjectWarning = otherProjectIds.length ? `${pc20.yellow("stale ")} ${otherProjectIds.length} prior project ${otherProjectIds.length === 1 ? "identity holds" : "identities hold"} ${otherProjectNodes} node(s) \u2014 run ${pc20.bold(
|
|
5969
6288
|
"nexusmem sync --prune-source <name>"
|
|
5970
6289
|
)} to remove stale source data` : "";
|
|
5971
6290
|
out(
|
|
5972
6291
|
[
|
|
5973
|
-
`${
|
|
5974
|
-
`${
|
|
5975
|
-
`${
|
|
5976
|
-
`${
|
|
5977
|
-
`${
|
|
6292
|
+
`${pc20.dim("repo ")} ${repo.root}`,
|
|
6293
|
+
`${pc20.dim("branch ")} ${repo.branch ?? pc20.yellow("(detached)")}`,
|
|
6294
|
+
`${pc20.dim("project ")} ${pc20.cyan(projectId)}`,
|
|
6295
|
+
`${pc20.dim("schema ")} v${schema}${schema === LATEST_SCHEMA_VERSION ? "" : pc20.yellow(` (latest is v${LATEST_SCHEMA_VERSION})`)}`,
|
|
6296
|
+
`${pc20.dim("database")} ${ws.dbPath} ${pc20.dim(`(${humanBytes(dbBytes)})`)}`,
|
|
5978
6297
|
staleProjectWarning,
|
|
5979
6298
|
"",
|
|
5980
|
-
`${
|
|
6299
|
+
`${pc20.bold(String(stats2.total))} node(s)${stats2.total ? ` ${pc20.dim(`${stats2.oldest?.slice(0, 10)} .. ${stats2.newest?.slice(0, 10)}`)}` : ""}`,
|
|
5981
6300
|
...kinds,
|
|
5982
|
-
stats2.total ? ` ${
|
|
6301
|
+
stats2.total ? ` ${pc20.dim(`${stats2.distinctFiles} distinct file path(s)`)}` : "",
|
|
5983
6302
|
"",
|
|
5984
|
-
sources.length ?
|
|
6303
|
+
sources.length ? pc20.dim("sources") : pc20.yellow("no sources synced yet"),
|
|
5985
6304
|
...sources.map((s) => {
|
|
5986
6305
|
const when = s.lastRunAt ? new Date(s.lastRunAt).toISOString().slice(0, 16).replace("T", " ") : "never";
|
|
5987
6306
|
const cursorLabel = s.source === "git" ? s.cursor?.slice(0, 7) ?? "-" : s.cursor ?? "-";
|
|
5988
|
-
return ` ${s.source.padEnd(14)} ${
|
|
6307
|
+
return ` ${s.source.padEnd(14)} ${pc20.dim(`last run ${when}`)} ${pc20.dim(`cursor ${cursorLabel}`)}`;
|
|
5989
6308
|
}),
|
|
5990
6309
|
"",
|
|
5991
|
-
gitCursor && gitCursor !== repo.head ? `${
|
|
5992
|
-
chains.failuresTotal ? `${
|
|
5993
|
-
structure.edges ? `${
|
|
5994
|
-
staleCount ? `${
|
|
6310
|
+
gitCursor && gitCursor !== repo.head ? `${pc20.yellow("git behind HEAD")} \u2014 run ${pc20.bold("nexusmem sync")}` : "",
|
|
6311
|
+
chains.failuresTotal ? `${pc20.dim("chains ")} ${pc20.bold(String(chains.resolvedTotal))}/${chains.failuresTotal} failure(s) resolved ${pc20.dim(`(${chains.resolvedByRetry} retry, ${chains.resolvedByDiscussion} discussion)`)}${chains.resolvedTotal < chains.failuresTotal ? ` \u2014 run ${pc20.bold("nexusmem sync --link-failures")} to link more` : ""}` : "",
|
|
6312
|
+
structure.edges ? `${pc20.dim("structure")} ${pc20.bold(String(structure.edges))} import edge(s) across ${structure.files} file(s)` : "",
|
|
6313
|
+
staleCount ? `${pc20.dim("aging ")} ${pc20.bold(String(staleCount))} unconfirmed node(s) worth a look \u2014 run ${pc20.bold("nexusmem stale")}` : "",
|
|
6314
|
+
flaggedCount ? `${pc20.dim("flagged ")} ${pc20.bold(String(flaggedCount))} likely-superseded node(s) awaiting review \u2014 run ${pc20.bold("nexusmem stale")} for detail` : ""
|
|
5995
6315
|
].filter((line) => line !== "").join("\n").concat("\n")
|
|
5996
6316
|
);
|
|
5997
6317
|
return 0;
|
|
@@ -6006,7 +6326,7 @@ function isExpected(err) {
|
|
|
6006
6326
|
// the user fixes, not stack traces they debug.
|
|
6007
6327
|
err instanceof GitSpawnError || // Survived every retry, so git is genuinely unstable on this machine
|
|
6008
6328
|
// (antivirus, a bad install). Actionable, and not our stack to print.
|
|
6009
|
-
err instanceof GitCrashError || err instanceof ConfigError || err instanceof ProfileNotFoundError || err instanceof ForeignGitHookError || err instanceof DenyListError || err instanceof MarkStaleError;
|
|
6329
|
+
err instanceof GitCrashError || err instanceof ConfigError || err instanceof ProfileNotFoundError || err instanceof ForeignGitHookError || err instanceof DenyListError || err instanceof MarkStaleError || err instanceof QueryError || err instanceof ReviewError;
|
|
6010
6330
|
}
|
|
6011
6331
|
function guard(run) {
|
|
6012
6332
|
return async () => {
|
|
@@ -6014,7 +6334,7 @@ function guard(run) {
|
|
|
6014
6334
|
process.exitCode = await run();
|
|
6015
6335
|
} catch (err) {
|
|
6016
6336
|
if (isExpected(err)) {
|
|
6017
|
-
process.stderr.write(`${
|
|
6337
|
+
process.stderr.write(`${pc21.red("error")} ${err.message}
|
|
6018
6338
|
`);
|
|
6019
6339
|
process.exitCode = 1;
|
|
6020
6340
|
return;
|
|
@@ -6077,7 +6397,7 @@ program.command("hook").description("Manage the opt-in PowerShell hook that logs
|
|
|
6077
6397
|
)
|
|
6078
6398
|
);
|
|
6079
6399
|
program.command("status").description("Show what is currently remembered for this repository").option("-C, --cwd <path>", "repository path", process.cwd()).option("--share", "print a plain-text summary formatted for sharing, e.g. on X or Reddit").action((options) => guard(() => runStatus({ cwd: options.cwd, share: options.share }))());
|
|
6080
|
-
program.command("query").description("Search remembered history and print a token-budgeted context block").argument("<text>", "free-text query").option("-C, --cwd <path>", "repository path", process.cwd()).option("-b, --budget <tokens>", "max tokens in the packed context", (v) => Number.parseInt(v, 10), 2e3).option("-n, --candidates <count>", "how many search hits to rank before packing", (v) => Number.parseInt(v, 10), 30).option("--half-life <days>", "days for a node's recency weight to halve", (v) => Number.parseFloat(v)).option("--no-vector", "BM25 only -- skip embedding the query and vector search").option("-a, --all-projects", "search every registered repository, not just this one", false).option("--json", "emit the packed result as JSON on stdout", false).action(
|
|
6400
|
+
program.command("query").description("Search remembered history and print a token-budgeted context block").argument("<text>", "free-text query").option("-C, --cwd <path>", "repository path", process.cwd()).option("-b, --budget <tokens>", "max tokens in the packed context", (v) => Number.parseInt(v, 10), 2e3).option("-n, --candidates <count>", "how many search hits to rank before packing", (v) => Number.parseInt(v, 10), 30).option("--half-life <days>", "days for a node's recency weight to halve", (v) => Number.parseFloat(v)).option("--no-vector", "BM25 only -- skip embedding the query and vector search").option("-a, --all-projects", "search every registered repository, not just this one", false).option("--as-of <date>", 'bi-temporal read: only nodes recorded at or before this date -- "what did the store hold then", not "what happened then"').option("--json", "emit the packed result as JSON on stdout", false).action(
|
|
6081
6401
|
(text, options) => guard(
|
|
6082
6402
|
() => runQuery({
|
|
6083
6403
|
cwd: options.cwd,
|
|
@@ -6087,6 +6407,7 @@ program.command("query").description("Search remembered history and print a toke
|
|
|
6087
6407
|
halfLifeDays: options.halfLife,
|
|
6088
6408
|
noVector: !options.vector,
|
|
6089
6409
|
allProjects: options.allProjects,
|
|
6410
|
+
asOf: options.asOf,
|
|
6090
6411
|
json: options.json
|
|
6091
6412
|
})
|
|
6092
6413
|
)()
|
|
@@ -6113,20 +6434,29 @@ program.command("mark-stale").description(
|
|
|
6113
6434
|
).argument("<nodeId>", "id of the node to mark stale").requiredOption("--supersedes <newNodeId>", "id of the node that supersedes it").option("-C, --cwd <path>", "repository path", process.cwd()).action(
|
|
6114
6435
|
(nodeId, options) => guard(() => runMarkStale({ cwd: options.cwd, nodeId, supersedesId: options.supersedes }))()
|
|
6115
6436
|
);
|
|
6116
|
-
program.command("stale").description("List
|
|
6437
|
+
program.command("stale").description("List unconfirmed (non-observed) nodes old enough to be worth double-checking (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--min-age-days <days>", "only nodes at least this old", (v) => Number.parseFloat(v)).option("-n, --limit <count>", "stop after N candidates", (v) => Number.parseInt(v, 10)).option(
|
|
6117
6438
|
"--check-contradictions",
|
|
6118
6439
|
"ask the local SLM whether a similar newer node actually contradicts each candidate (needs Ollama)"
|
|
6119
|
-
).option("--model <name>", "Ollama chat model for --check-contradictions", STALE_DEFAULT_MODEL).action(
|
|
6440
|
+
).option("--model <name>", "Ollama chat model for --check-contradictions", STALE_DEFAULT_MODEL).option("--dismiss <candidateId>", "reject the open contradiction suggestion for this node id so it stops resurfacing").action(
|
|
6120
6441
|
(options) => guard(
|
|
6121
6442
|
() => runStale({
|
|
6122
6443
|
cwd: options.cwd,
|
|
6123
6444
|
minAgeDays: options.minAgeDays,
|
|
6124
6445
|
limit: options.limit,
|
|
6125
6446
|
checkContradictions: options.checkContradictions,
|
|
6126
|
-
model: options.model
|
|
6447
|
+
model: options.model,
|
|
6448
|
+
dismiss: options.dismiss
|
|
6127
6449
|
})
|
|
6128
6450
|
)()
|
|
6129
6451
|
);
|
|
6452
|
+
program.command("review").description("Record a human verdict on one node: --verify or --reject (a rejected node is down-weighted in ranking, never deleted)").argument("<nodeId>", "id of the node being reviewed").option("-C, --cwd <path>", "repository path", process.cwd()).option("--verify", "mark the node verified (label only, no ranking change)", false).option("--reject", "mark the node rejected (down-weighted in ranking, still queryable)", false).action(
|
|
6453
|
+
(nodeId, options) => guard(async () => {
|
|
6454
|
+
if (options.verify === options.reject) {
|
|
6455
|
+
throw new ReviewError("pass exactly one of --verify or --reject");
|
|
6456
|
+
}
|
|
6457
|
+
return runReview({ cwd: options.cwd, nodeId, verdict: options.verify ? "verified" : "rejected" });
|
|
6458
|
+
})()
|
|
6459
|
+
);
|
|
6130
6460
|
program.command("projects").description("List the repositories `query --all-projects` would search").option("--prune", "forget registered projects whose database is no longer on disk", false).option("--json", "emit the registry as JSON on stdout", false).action((options) => guard(() => runProjects({ prune: options.prune, json: options.json }))());
|
|
6131
6461
|
program.command("scan-git").description("Preview the MemoryNodes git history would produce (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--since <date>", "only commits newer than this git date expression, e.g. 90.days.ago").option("-n, --limit <count>", "stop after N commits", (v) => Number.parseInt(v, 10)).option("--no-merges", "skip merge commits").option("--min-signal <score>", "drop nodes below this signal", (v) => Number.parseFloat(v), 0).option("--json", "emit MemoryNodes as JSON on stdout", false).action(
|
|
6132
6462
|
(options) => guard(
|
|
@@ -6187,7 +6517,7 @@ program.command("scan-structure").description("Preview the JS/TS/Python/Go/Rust/
|
|
|
6187
6517
|
program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
|
|
6188
6518
|
program.parseAsync(process.argv).catch((err) => {
|
|
6189
6519
|
const message = err instanceof Error ? err.message : String(err);
|
|
6190
|
-
process.stderr.write(`${
|
|
6520
|
+
process.stderr.write(`${pc21.red("error")} ${message}
|
|
6191
6521
|
`);
|
|
6192
6522
|
process.exitCode = 1;
|
|
6193
6523
|
});
|