nexusmem 0.6.0 → 0.8.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 +56 -1
- package/README.md +39 -17
- package/dist/cli/index.js +464 -29
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -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 });
|
|
@@ -944,13 +957,32 @@ UPDATE nodes SET provenance = 'observed' WHERE kind IN ('git_commit', 'code_diff
|
|
|
944
957
|
|
|
945
958
|
CREATE INDEX idx_nodes_supersedes ON nodes (supersedes) WHERE supersedes IS NOT NULL;
|
|
946
959
|
`;
|
|
960
|
+
var V7 = `
|
|
961
|
+
UPDATE nodes SET provenance = 'authored' WHERE kind IN ('doc_section', 'note');
|
|
962
|
+
UPDATE nodes SET provenance = 'recorded' WHERE kind = 'conversation_turn';
|
|
963
|
+
UPDATE nodes SET provenance = 'derived' WHERE kind = 'session_summary';
|
|
964
|
+
UPDATE nodes SET provenance = 'recorded' WHERE provenance = 'inferred';
|
|
965
|
+
`;
|
|
966
|
+
var V8 = `
|
|
967
|
+
CREATE TABLE contradiction_checks (
|
|
968
|
+
candidate_id TEXT NOT NULL REFERENCES nodes (id) ON DELETE CASCADE,
|
|
969
|
+
against_id TEXT NOT NULL REFERENCES nodes (id) ON DELETE CASCADE,
|
|
970
|
+
contradicts INTEGER NOT NULL,
|
|
971
|
+
reason TEXT,
|
|
972
|
+
model TEXT NOT NULL,
|
|
973
|
+
checked_at INTEGER NOT NULL,
|
|
974
|
+
PRIMARY KEY (candidate_id, against_id)
|
|
975
|
+
);
|
|
976
|
+
`;
|
|
947
977
|
var MIGRATIONS = [
|
|
948
978
|
{ version: 1, up: (db) => db.exec(V1) },
|
|
949
979
|
{ version: 2, up: (db) => db.exec(V2) },
|
|
950
980
|
{ version: 3, up: (db) => db.exec(V3) },
|
|
951
981
|
{ version: 4, up: (db) => db.exec(V4) },
|
|
952
982
|
{ version: 5, up: (db) => db.exec(V5) },
|
|
953
|
-
{ version: 6, up: (db) => db.exec(V6) }
|
|
983
|
+
{ version: 6, up: (db) => db.exec(V6) },
|
|
984
|
+
{ version: 7, up: (db) => db.exec(V7) },
|
|
985
|
+
{ version: 8, up: (db) => db.exec(V8) }
|
|
954
986
|
];
|
|
955
987
|
var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
956
988
|
function currentSchemaVersion(db) {
|
|
@@ -1012,11 +1044,13 @@ function defaultProvenanceForKind(kind) {
|
|
|
1012
1044
|
case "code_diff":
|
|
1013
1045
|
case "shell_command":
|
|
1014
1046
|
return "observed";
|
|
1015
|
-
case "conversation_turn":
|
|
1016
|
-
case "session_summary":
|
|
1017
1047
|
case "doc_section":
|
|
1018
1048
|
case "note":
|
|
1019
|
-
return "
|
|
1049
|
+
return "authored";
|
|
1050
|
+
case "conversation_turn":
|
|
1051
|
+
return "recorded";
|
|
1052
|
+
case "session_summary":
|
|
1053
|
+
return "derived";
|
|
1020
1054
|
}
|
|
1021
1055
|
}
|
|
1022
1056
|
|
|
@@ -1171,7 +1205,7 @@ function listStaleCandidates(db, projectId, opts = {}) {
|
|
|
1171
1205
|
const rows = db.prepare(
|
|
1172
1206
|
`SELECT id, kind, ts, ts_epoch AS tsEpoch, source, title
|
|
1173
1207
|
FROM nodes
|
|
1174
|
-
WHERE project_id = @projectId AND provenance
|
|
1208
|
+
WHERE project_id = @projectId AND provenance != 'observed' AND ts_epoch < @cutoff
|
|
1175
1209
|
AND id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)
|
|
1176
1210
|
ORDER BY ts_epoch ASC
|
|
1177
1211
|
LIMIT @limit`
|
|
@@ -1191,7 +1225,7 @@ function countStaleCandidates(db, projectId, opts = {}) {
|
|
|
1191
1225
|
const cutoff = now.getTime() - minAgeDays * 864e5;
|
|
1192
1226
|
const row = db.prepare(
|
|
1193
1227
|
`SELECT COUNT(*) AS count FROM nodes
|
|
1194
|
-
WHERE project_id = @projectId AND provenance
|
|
1228
|
+
WHERE project_id = @projectId AND provenance != 'observed' AND ts_epoch < @cutoff
|
|
1195
1229
|
AND id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)`
|
|
1196
1230
|
).get({ projectId, cutoff });
|
|
1197
1231
|
return row.count;
|
|
@@ -1372,6 +1406,11 @@ function countNodesNeedingEmbedding(db, projectId) {
|
|
|
1372
1406
|
).get(projectId);
|
|
1373
1407
|
return row.n;
|
|
1374
1408
|
}
|
|
1409
|
+
function getEmbedding(db, nodeId) {
|
|
1410
|
+
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);
|
|
1411
|
+
if (!row) return null;
|
|
1412
|
+
return new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
|
|
1413
|
+
}
|
|
1375
1414
|
function upsertEmbedding(db, rowid, embedding) {
|
|
1376
1415
|
db.prepare("INSERT OR REPLACE INTO nodes_vec (rowid, embedding) VALUES (?, ?)").run(BigInt(rowid), embedding);
|
|
1377
1416
|
}
|
|
@@ -1449,6 +1488,49 @@ function setMeta(db, key, value) {
|
|
|
1449
1488
|
);
|
|
1450
1489
|
}
|
|
1451
1490
|
|
|
1491
|
+
// src/store/contradictions.ts
|
|
1492
|
+
function recordContradictionCheck(db, input) {
|
|
1493
|
+
db.prepare(
|
|
1494
|
+
`INSERT OR REPLACE INTO contradiction_checks (candidate_id, against_id, contradicts, reason, model, checked_at)
|
|
1495
|
+
VALUES (@candidateId, @againstId, @contradicts, @reason, @model, @now)`
|
|
1496
|
+
).run({
|
|
1497
|
+
candidateId: input.candidateId,
|
|
1498
|
+
againstId: input.againstId,
|
|
1499
|
+
contradicts: input.contradicts ? 1 : 0,
|
|
1500
|
+
reason: input.reason,
|
|
1501
|
+
model: input.model,
|
|
1502
|
+
now: Date.now()
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
function hasContradictionCheck(db, candidateId, againstId) {
|
|
1506
|
+
const row = db.prepare("SELECT 1 FROM contradiction_checks WHERE candidate_id = ? AND against_id = ?").get(candidateId, againstId);
|
|
1507
|
+
return row !== void 0;
|
|
1508
|
+
}
|
|
1509
|
+
function listContradictionSuggestions(db, projectId, opts = {}) {
|
|
1510
|
+
return db.prepare(
|
|
1511
|
+
`SELECT c.candidate_id AS candidateId, n.title AS candidateTitle,
|
|
1512
|
+
c.against_id AS againstId, a.title AS againstTitle,
|
|
1513
|
+
c.reason AS reason, c.checked_at AS checkedAt
|
|
1514
|
+
FROM contradiction_checks c
|
|
1515
|
+
JOIN nodes n ON n.id = c.candidate_id
|
|
1516
|
+
JOIN nodes a ON a.id = c.against_id
|
|
1517
|
+
WHERE n.project_id = @projectId AND c.contradicts = 1
|
|
1518
|
+
AND c.candidate_id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)
|
|
1519
|
+
ORDER BY c.checked_at DESC
|
|
1520
|
+
LIMIT @limit`
|
|
1521
|
+
).all({ projectId, limit: opts.limit ?? 50 });
|
|
1522
|
+
}
|
|
1523
|
+
function countContradictionSuggestions(db, projectId) {
|
|
1524
|
+
const row = db.prepare(
|
|
1525
|
+
`SELECT COUNT(*) AS count
|
|
1526
|
+
FROM contradiction_checks c
|
|
1527
|
+
JOIN nodes n ON n.id = c.candidate_id
|
|
1528
|
+
WHERE n.project_id = @projectId AND c.contradicts = 1
|
|
1529
|
+
AND c.candidate_id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)`
|
|
1530
|
+
).get({ projectId });
|
|
1531
|
+
return row.count;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1452
1534
|
// src/store/store.ts
|
|
1453
1535
|
var MemoryStore = class _MemoryStore {
|
|
1454
1536
|
constructor(db) {
|
|
@@ -1608,6 +1690,10 @@ var MemoryStore = class _MemoryStore {
|
|
|
1608
1690
|
upsertEmbedding(rowid, embedding) {
|
|
1609
1691
|
upsertEmbedding(this.db, rowid, embedding);
|
|
1610
1692
|
}
|
|
1693
|
+
/** The stored vector for one node, or null if it has not been embedded yet. */
|
|
1694
|
+
getEmbedding(nodeId) {
|
|
1695
|
+
return getEmbedding(this.db, nodeId);
|
|
1696
|
+
}
|
|
1611
1697
|
dropAllEmbeddings() {
|
|
1612
1698
|
return dropAllEmbeddings(this.db);
|
|
1613
1699
|
}
|
|
@@ -1638,7 +1724,7 @@ var MemoryStore = class _MemoryStore {
|
|
|
1638
1724
|
setSupersedes(newNodeId, staleNodeId) {
|
|
1639
1725
|
setSupersedes(this.db, newNodeId, staleNodeId);
|
|
1640
1726
|
}
|
|
1641
|
-
/** Aging `
|
|
1727
|
+
/** Aging non-`observed` nodes nothing supersedes yet -- candidates for `nexusmem mark-stale`, not auto-applied. */
|
|
1642
1728
|
listStaleCandidates(projectId, opts = {}) {
|
|
1643
1729
|
return listStaleCandidates(this.db, projectId, opts);
|
|
1644
1730
|
}
|
|
@@ -1646,6 +1732,20 @@ var MemoryStore = class _MemoryStore {
|
|
|
1646
1732
|
countStaleCandidates(projectId, opts = {}) {
|
|
1647
1733
|
return countStaleCandidates(this.db, projectId, opts);
|
|
1648
1734
|
}
|
|
1735
|
+
/** Memoize one SLM contradiction judgment (either verdict). Suggest-only: never writes `supersedes`. */
|
|
1736
|
+
recordContradictionCheck(input) {
|
|
1737
|
+
recordContradictionCheck(this.db, input);
|
|
1738
|
+
}
|
|
1739
|
+
hasContradictionCheck(candidateId, againstId) {
|
|
1740
|
+
return hasContradictionCheck(this.db, candidateId, againstId);
|
|
1741
|
+
}
|
|
1742
|
+
/** Open YES verdicts awaiting a human's `mark-stale`, newest judgment first. */
|
|
1743
|
+
listContradictionSuggestions(projectId, opts = {}) {
|
|
1744
|
+
return listContradictionSuggestions(this.db, projectId, opts);
|
|
1745
|
+
}
|
|
1746
|
+
countContradictionSuggestions(projectId) {
|
|
1747
|
+
return countContradictionSuggestions(this.db, projectId);
|
|
1748
|
+
}
|
|
1649
1749
|
/** Escape hatch for tests and future modules. */
|
|
1650
1750
|
get raw() {
|
|
1651
1751
|
return this.db;
|
|
@@ -2458,7 +2558,12 @@ var SIGNAL_FLOOR = 0.2;
|
|
|
2458
2558
|
var RECENCY_FLOOR = 0.3;
|
|
2459
2559
|
var DEFAULT_HALF_LIFE_DAYS = 30;
|
|
2460
2560
|
var MS_PER_DAY = 864e5;
|
|
2461
|
-
var
|
|
2561
|
+
var HALF_LIFE_RATIO = {
|
|
2562
|
+
observed: 1,
|
|
2563
|
+
authored: 0.75,
|
|
2564
|
+
recorded: 0.5,
|
|
2565
|
+
derived: 0.35
|
|
2566
|
+
};
|
|
2462
2567
|
var SUPERSEDED_PENALTY = 0.5;
|
|
2463
2568
|
var MAX_PRIOR_OVERTURN = 2;
|
|
2464
2569
|
var PRIOR_COUNT = 2;
|
|
@@ -2493,14 +2598,14 @@ function ageDaysOf(ts, now) {
|
|
|
2493
2598
|
function rankHits(hits, opts = {}) {
|
|
2494
2599
|
if (hits.length === 0) return [];
|
|
2495
2600
|
const halfLife = opts.halfLifeDays ?? DEFAULT_HALF_LIFE_DAYS;
|
|
2496
|
-
const
|
|
2601
|
+
const ratios = { ...HALF_LIFE_RATIO, ...opts.halfLifeRatios };
|
|
2497
2602
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
2498
2603
|
const relevances = opts.relevanceScores ? normalizeExternalRelevance(hits, opts.relevanceScores) : normalizeRelevance(hits);
|
|
2499
2604
|
const ranked = hits.map((hit, i) => {
|
|
2500
2605
|
const relevance = relevances[i] ?? RELEVANCE_FLOOR;
|
|
2501
2606
|
const signalWeight = SIGNAL_FLOOR + (1 - SIGNAL_FLOOR) * hit.signal;
|
|
2502
2607
|
const ageDays = ageDaysOf(hit.ts, now);
|
|
2503
|
-
const effectiveHalfLife = hit.provenance
|
|
2608
|
+
const effectiveHalfLife = halfLife * (ratios[hit.provenance] ?? 1);
|
|
2504
2609
|
const recencyFactor = RECENCY_FLOOR + (1 - RECENCY_FLOOR) * 2 ** (-ageDays / effectiveHalfLife);
|
|
2505
2610
|
const rawScore = relevance * signalWeight ** SIGNAL_EXPONENT * recencyFactor ** RECENCY_EXPONENT;
|
|
2506
2611
|
const score = opts.supersededIds?.has(hit.id) ? rawScore * SUPERSEDED_PENALTY : rawScore;
|
|
@@ -2830,8 +2935,8 @@ function toMemoryNodes(turn, projectId, opts = {}) {
|
|
|
2830
2935
|
files: extractMentionedFiles(`${userRedacted.text}
|
|
2831
2936
|
${chunk2.text}`),
|
|
2832
2937
|
signal: scoreConversationTurn(userRedacted.text, chunk2.text),
|
|
2833
|
-
provenance: "
|
|
2834
|
-
// discourse about what happened, not the event itself
|
|
2938
|
+
provenance: "recorded",
|
|
2939
|
+
// verbatim discourse about what happened, not the event itself
|
|
2835
2940
|
meta: {
|
|
2836
2941
|
cwd: turn.cwd,
|
|
2837
2942
|
source: turn.source,
|
|
@@ -3403,8 +3508,8 @@ function toMemoryNodes3(file, projectId, opts = {}) {
|
|
|
3403
3508
|
body: truncate(chunk2.text, maxBody),
|
|
3404
3509
|
files: [{ path: file.path, insertions: null, deletions: null, binary: false }],
|
|
3405
3510
|
signal: scoreDocSection(file.path, chunk2.heading, chunk2.text),
|
|
3406
|
-
provenance: "
|
|
3407
|
-
// a written claim
|
|
3511
|
+
provenance: "authored",
|
|
3512
|
+
// a human's own written claim -- deliberate, but can still go stale
|
|
3408
3513
|
meta: {
|
|
3409
3514
|
path: file.path,
|
|
3410
3515
|
heading: chunk2.heading,
|
|
@@ -3566,7 +3671,7 @@ ${summary.body}`;
|
|
|
3566
3671
|
files: extractMentionedFiles(session.turns.map((t) => `${t.userText}
|
|
3567
3672
|
${t.assistantText}`).join("\n")),
|
|
3568
3673
|
signal: scoreSession(session.turns.length),
|
|
3569
|
-
provenance: "
|
|
3674
|
+
provenance: "derived",
|
|
3570
3675
|
// a model's distillation, not a directly observed event
|
|
3571
3676
|
meta: {
|
|
3572
3677
|
sessionKey: session.sessionKey,
|
|
@@ -3820,6 +3925,97 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
3820
3925
|
return { files, unreadable };
|
|
3821
3926
|
}
|
|
3822
3927
|
|
|
3928
|
+
// src/slm/contradiction.ts
|
|
3929
|
+
var MAX_BODY_CHARS = 1500;
|
|
3930
|
+
var MAX_REASON_CHARS = 200;
|
|
3931
|
+
var CONTRADICTION_INSTRUCTIONS = `You are checking whether a NEWER memory replaces or contradicts an OLDER one, for an AI coding assistant's memory index.
|
|
3932
|
+
|
|
3933
|
+
Answer in exactly this shape:
|
|
3934
|
+
VERDICT: YES or NO
|
|
3935
|
+
REASON: <one line, under 20 words>
|
|
3936
|
+
|
|
3937
|
+
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.`;
|
|
3938
|
+
function buildContradictionPrompt(older, newer) {
|
|
3939
|
+
const body = [
|
|
3940
|
+
`OLDER (${older.title}):`,
|
|
3941
|
+
truncate(older.body, MAX_BODY_CHARS),
|
|
3942
|
+
"",
|
|
3943
|
+
`NEWER (${newer.title}):`,
|
|
3944
|
+
truncate(newer.body, MAX_BODY_CHARS)
|
|
3945
|
+
].join("\n");
|
|
3946
|
+
return `${CONTRADICTION_INSTRUCTIONS}
|
|
3947
|
+
|
|
3948
|
+
---
|
|
3949
|
+
|
|
3950
|
+
${body}
|
|
3951
|
+
|
|
3952
|
+
---
|
|
3953
|
+
|
|
3954
|
+
Answer:`;
|
|
3955
|
+
}
|
|
3956
|
+
function parseContradictionVerdict(raw) {
|
|
3957
|
+
const lines = raw.trim().split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3958
|
+
const verdictLine = lines.find((l) => /^VERDICT:/i.test(l));
|
|
3959
|
+
if (!verdictLine) return null;
|
|
3960
|
+
const verdict = /^VERDICT:\s*(YES|NO)\b/i.exec(verdictLine);
|
|
3961
|
+
if (!verdict) return null;
|
|
3962
|
+
const reasonLine = lines.find((l) => /^REASON:/i.test(l));
|
|
3963
|
+
const reason = reasonLine ? reasonLine.replace(/^REASON:\s*/i, "").trim() : "";
|
|
3964
|
+
return {
|
|
3965
|
+
contradicts: verdict[1].toUpperCase() === "YES",
|
|
3966
|
+
reason: truncate(reason, MAX_REASON_CHARS)
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
// src/retrieval/contradiction.ts
|
|
3971
|
+
var DEFAULT_LIMIT = 10;
|
|
3972
|
+
var DEFAULT_NEIGHBOR_LIMIT = 25;
|
|
3973
|
+
async function checkContradictions(store, embeddingProvider, slmProvider, projectId, candidates, opts = {}) {
|
|
3974
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
3975
|
+
const neighborLimit = opts.neighborLimit ?? DEFAULT_NEIGHBOR_LIMIT;
|
|
3976
|
+
const model = opts.model ?? DEFAULT_SLM_MODEL;
|
|
3977
|
+
const suggestions = [];
|
|
3978
|
+
let judgments = 0;
|
|
3979
|
+
let consecutiveNullReplies = 0;
|
|
3980
|
+
for (const candidate of candidates.slice(0, limit)) {
|
|
3981
|
+
if (opts.maxJudgments !== void 0 && judgments >= opts.maxJudgments) break;
|
|
3982
|
+
const full = store.getNodesByIds([candidate.id])[0];
|
|
3983
|
+
if (!full) continue;
|
|
3984
|
+
const embedding = store.getEmbedding(candidate.id) ?? await embeddingProvider.embed(`${full.title}
|
|
3985
|
+
${full.body}`);
|
|
3986
|
+
if (!embedding) continue;
|
|
3987
|
+
const candidateEpoch = Date.parse(candidate.ts);
|
|
3988
|
+
const nearest = store.vectorSearch(projectId, embedding, neighborLimit + 1).find((hit) => hit.id !== candidate.id && Date.parse(hit.ts) > candidateEpoch);
|
|
3989
|
+
if (!nearest) continue;
|
|
3990
|
+
if (store.hasContradictionCheck(candidate.id, nearest.id)) continue;
|
|
3991
|
+
const reply = await slmProvider.complete(buildContradictionPrompt(full, nearest));
|
|
3992
|
+
if (!reply) {
|
|
3993
|
+
consecutiveNullReplies += 1;
|
|
3994
|
+
if (consecutiveNullReplies >= 2) break;
|
|
3995
|
+
continue;
|
|
3996
|
+
}
|
|
3997
|
+
consecutiveNullReplies = 0;
|
|
3998
|
+
const verdict = parseContradictionVerdict(reply);
|
|
3999
|
+
if (!verdict) continue;
|
|
4000
|
+
judgments += 1;
|
|
4001
|
+
store.recordContradictionCheck({
|
|
4002
|
+
candidateId: candidate.id,
|
|
4003
|
+
againstId: nearest.id,
|
|
4004
|
+
contradicts: verdict.contradicts,
|
|
4005
|
+
reason: verdict.contradicts ? verdict.reason : null,
|
|
4006
|
+
model
|
|
4007
|
+
});
|
|
4008
|
+
if (!verdict.contradicts) continue;
|
|
4009
|
+
suggestions.push({
|
|
4010
|
+
candidateId: candidate.id,
|
|
4011
|
+
againstId: nearest.id,
|
|
4012
|
+
againstTitle: nearest.title,
|
|
4013
|
+
reason: verdict.reason
|
|
4014
|
+
});
|
|
4015
|
+
}
|
|
4016
|
+
return suggestions;
|
|
4017
|
+
}
|
|
4018
|
+
|
|
3823
4019
|
// src/shell/detect.ts
|
|
3824
4020
|
import { existsSync as existsSync4 } from "fs";
|
|
3825
4021
|
import { readFile as readFile9, stat as stat2 } from "fs/promises";
|
|
@@ -4216,6 +4412,8 @@ function extractPhpIncludeSpecifiers(source) {
|
|
|
4216
4412
|
|
|
4217
4413
|
// src/structure/extract-python.ts
|
|
4218
4414
|
var RELATIVE_IMPORT_PATTERN = /\bfrom\s+(\.+)([\w.]*)\s+import\s+([^\n]+)/g;
|
|
4415
|
+
var BARE_FROM_IMPORT_PATTERN = /^[ \t]*from\s+([A-Za-z_]\w*)\s+import\b/gm;
|
|
4416
|
+
var BARE_IMPORT_PATTERN = /^[ \t]*import\s+([^\n]+)/gm;
|
|
4219
4417
|
function cleanNames(raw) {
|
|
4220
4418
|
return raw.split("#")[0].replace(/[()]/g, "").split(",").map((token) => token.trim().split(/\s+as\s+/)[0].trim()).filter((name) => /^[A-Za-z_]\w*$/.test(name));
|
|
4221
4419
|
}
|
|
@@ -4234,6 +4432,16 @@ function extractPythonImportSpecifiers(source) {
|
|
|
4234
4432
|
seen.add(dots + name);
|
|
4235
4433
|
}
|
|
4236
4434
|
}
|
|
4435
|
+
BARE_FROM_IMPORT_PATTERN.lastIndex = 0;
|
|
4436
|
+
while ((match = BARE_FROM_IMPORT_PATTERN.exec(source)) !== null) {
|
|
4437
|
+
seen.add(match[1]);
|
|
4438
|
+
}
|
|
4439
|
+
BARE_IMPORT_PATTERN.lastIndex = 0;
|
|
4440
|
+
while ((match = BARE_IMPORT_PATTERN.exec(source)) !== null) {
|
|
4441
|
+
for (const name of cleanNames(match[1])) {
|
|
4442
|
+
seen.add(name);
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4237
4445
|
return [...seen];
|
|
4238
4446
|
}
|
|
4239
4447
|
|
|
@@ -4314,7 +4522,9 @@ function resolveJavaSpecifier(specifier, trackedPaths) {
|
|
|
4314
4522
|
if (segments.length === 0) return [];
|
|
4315
4523
|
if (isWildcard) {
|
|
4316
4524
|
const dirSuffix = segments.join("/");
|
|
4317
|
-
|
|
4525
|
+
const matches2 = [...trackedPaths].filter((p) => p.endsWith(".java") && endsAtSegmentBoundary(posix3.dirname(p), dirSuffix));
|
|
4526
|
+
const dirs = new Set(matches2.map((p) => posix3.dirname(p)));
|
|
4527
|
+
return dirs.size === 1 ? matches2.sort() : [];
|
|
4318
4528
|
}
|
|
4319
4529
|
const fileSuffix = `${segments.join("/")}.java`;
|
|
4320
4530
|
const matches = [...trackedPaths].filter((p) => endsAtSegmentBoundary(p, fileSuffix));
|
|
@@ -4335,11 +4545,182 @@ function resolvePhpSpecifier(fromPath, specifier, trackedPaths) {
|
|
|
4335
4545
|
|
|
4336
4546
|
// src/structure/resolve-python.ts
|
|
4337
4547
|
import { posix as posix5 } from "path";
|
|
4548
|
+
var STDLIB_MODULES = /* @__PURE__ */ new Set([
|
|
4549
|
+
"__future__",
|
|
4550
|
+
"abc",
|
|
4551
|
+
"argparse",
|
|
4552
|
+
"array",
|
|
4553
|
+
"ast",
|
|
4554
|
+
"asyncio",
|
|
4555
|
+
"base64",
|
|
4556
|
+
"bisect",
|
|
4557
|
+
"builtins",
|
|
4558
|
+
"calendar",
|
|
4559
|
+
"cgi",
|
|
4560
|
+
"cgitb",
|
|
4561
|
+
"cmd",
|
|
4562
|
+
"codecs",
|
|
4563
|
+
"collections",
|
|
4564
|
+
"colorsys",
|
|
4565
|
+
"compileall",
|
|
4566
|
+
"concurrent",
|
|
4567
|
+
"configparser",
|
|
4568
|
+
"contextlib",
|
|
4569
|
+
"contextvars",
|
|
4570
|
+
"copy",
|
|
4571
|
+
"copyreg",
|
|
4572
|
+
"cProfile",
|
|
4573
|
+
"csv",
|
|
4574
|
+
"ctypes",
|
|
4575
|
+
"curses",
|
|
4576
|
+
"dataclasses",
|
|
4577
|
+
"datetime",
|
|
4578
|
+
"dbm",
|
|
4579
|
+
"decimal",
|
|
4580
|
+
"difflib",
|
|
4581
|
+
"dis",
|
|
4582
|
+
"doctest",
|
|
4583
|
+
"email",
|
|
4584
|
+
"encodings",
|
|
4585
|
+
"ensurepip",
|
|
4586
|
+
"enum",
|
|
4587
|
+
"errno",
|
|
4588
|
+
"faulthandler",
|
|
4589
|
+
"fcntl",
|
|
4590
|
+
"filecmp",
|
|
4591
|
+
"fileinput",
|
|
4592
|
+
"fnmatch",
|
|
4593
|
+
"fractions",
|
|
4594
|
+
"ftplib",
|
|
4595
|
+
"functools",
|
|
4596
|
+
"gc",
|
|
4597
|
+
"getopt",
|
|
4598
|
+
"getpass",
|
|
4599
|
+
"gettext",
|
|
4600
|
+
"glob",
|
|
4601
|
+
"graphlib",
|
|
4602
|
+
"grp",
|
|
4603
|
+
"gzip",
|
|
4604
|
+
"hashlib",
|
|
4605
|
+
"heapq",
|
|
4606
|
+
"hmac",
|
|
4607
|
+
"html",
|
|
4608
|
+
"http",
|
|
4609
|
+
"imaplib",
|
|
4610
|
+
"importlib",
|
|
4611
|
+
"inspect",
|
|
4612
|
+
"io",
|
|
4613
|
+
"ipaddress",
|
|
4614
|
+
"itertools",
|
|
4615
|
+
"json",
|
|
4616
|
+
"keyword",
|
|
4617
|
+
"locale",
|
|
4618
|
+
"logging",
|
|
4619
|
+
"lzma",
|
|
4620
|
+
"mailbox",
|
|
4621
|
+
"marshal",
|
|
4622
|
+
"math",
|
|
4623
|
+
"mimetypes",
|
|
4624
|
+
"mmap",
|
|
4625
|
+
"msvcrt",
|
|
4626
|
+
"multiprocessing",
|
|
4627
|
+
"operator",
|
|
4628
|
+
"os",
|
|
4629
|
+
"pathlib",
|
|
4630
|
+
"pdb",
|
|
4631
|
+
"pickle",
|
|
4632
|
+
"pickletools",
|
|
4633
|
+
"pkgutil",
|
|
4634
|
+
"platform",
|
|
4635
|
+
"plistlib",
|
|
4636
|
+
"poplib",
|
|
4637
|
+
"posix",
|
|
4638
|
+
"pprint",
|
|
4639
|
+
"profile",
|
|
4640
|
+
"pstats",
|
|
4641
|
+
"pty",
|
|
4642
|
+
"pwd",
|
|
4643
|
+
"py_compile",
|
|
4644
|
+
"pyclbr",
|
|
4645
|
+
"pydoc",
|
|
4646
|
+
"queue",
|
|
4647
|
+
"quopri",
|
|
4648
|
+
"random",
|
|
4649
|
+
"re",
|
|
4650
|
+
"readline",
|
|
4651
|
+
"reprlib",
|
|
4652
|
+
"resource",
|
|
4653
|
+
"rlcompleter",
|
|
4654
|
+
"runpy",
|
|
4655
|
+
"sched",
|
|
4656
|
+
"secrets",
|
|
4657
|
+
"select",
|
|
4658
|
+
"selectors",
|
|
4659
|
+
"shelve",
|
|
4660
|
+
"shlex",
|
|
4661
|
+
"shutil",
|
|
4662
|
+
"signal",
|
|
4663
|
+
"site",
|
|
4664
|
+
"smtplib",
|
|
4665
|
+
"socket",
|
|
4666
|
+
"socketserver",
|
|
4667
|
+
"sqlite3",
|
|
4668
|
+
"ssl",
|
|
4669
|
+
"stat",
|
|
4670
|
+
"statistics",
|
|
4671
|
+
"string",
|
|
4672
|
+
"stringprep",
|
|
4673
|
+
"struct",
|
|
4674
|
+
"subprocess",
|
|
4675
|
+
"symtable",
|
|
4676
|
+
"sys",
|
|
4677
|
+
"sysconfig",
|
|
4678
|
+
"syslog",
|
|
4679
|
+
"tarfile",
|
|
4680
|
+
"telnetlib",
|
|
4681
|
+
"tempfile",
|
|
4682
|
+
"termios",
|
|
4683
|
+
"test",
|
|
4684
|
+
"textwrap",
|
|
4685
|
+
"threading",
|
|
4686
|
+
"time",
|
|
4687
|
+
"timeit",
|
|
4688
|
+
"tkinter",
|
|
4689
|
+
"token",
|
|
4690
|
+
"tokenize",
|
|
4691
|
+
"tomllib",
|
|
4692
|
+
"trace",
|
|
4693
|
+
"traceback",
|
|
4694
|
+
"tracemalloc",
|
|
4695
|
+
"tty",
|
|
4696
|
+
"turtle",
|
|
4697
|
+
"types",
|
|
4698
|
+
"typing",
|
|
4699
|
+
"unicodedata",
|
|
4700
|
+
"unittest",
|
|
4701
|
+
"urllib",
|
|
4702
|
+
"uuid",
|
|
4703
|
+
"venv",
|
|
4704
|
+
"warnings",
|
|
4705
|
+
"wave",
|
|
4706
|
+
"weakref",
|
|
4707
|
+
"webbrowser",
|
|
4708
|
+
"winreg",
|
|
4709
|
+
"winsound",
|
|
4710
|
+
"wsgiref",
|
|
4711
|
+
"xml",
|
|
4712
|
+
"xmlrpc",
|
|
4713
|
+
"zipapp",
|
|
4714
|
+
"zipfile",
|
|
4715
|
+
"zipimport",
|
|
4716
|
+
"zlib",
|
|
4717
|
+
"zoneinfo"
|
|
4718
|
+
]);
|
|
4338
4719
|
function resolvePythonSpecifier(fromPath, specifier, trackedPaths) {
|
|
4339
4720
|
const dotsMatch = specifier.match(/^\.+/);
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
const segments = specifier.slice(level).split(".").filter(Boolean);
|
|
4721
|
+
const level = dotsMatch ? dotsMatch[0].length : 0;
|
|
4722
|
+
if (level === 0 && STDLIB_MODULES.has(specifier)) return null;
|
|
4723
|
+
const segments = (level === 0 ? specifier : specifier.slice(level)).split(".").filter(Boolean);
|
|
4343
4724
|
if (segments.length === 0) return null;
|
|
4344
4725
|
let dir = posix5.dirname(fromPath);
|
|
4345
4726
|
for (let i = 1; i < level; i++) {
|
|
@@ -4744,6 +5125,21 @@ function runPruneSources(store, projectId, otherProjectIds, sources, yes, out) {
|
|
|
4744
5125
|
`);
|
|
4745
5126
|
return 0;
|
|
4746
5127
|
}
|
|
5128
|
+
async function runAutoContradictionCheck(store, config, projectId, providers) {
|
|
5129
|
+
if (!config.contradictions.autoCheck) return "";
|
|
5130
|
+
const candidates = store.listStaleCandidates(projectId);
|
|
5131
|
+
if (candidates.length === 0) return "";
|
|
5132
|
+
const fresh = await checkContradictions(store, providers.embedder, providers.slm, projectId, candidates, {
|
|
5133
|
+
limit: candidates.length,
|
|
5134
|
+
maxJudgments: config.contradictions.maxPerSync,
|
|
5135
|
+
model: config.contradictions.model
|
|
5136
|
+
});
|
|
5137
|
+
const open = store.countContradictionSuggestions(projectId);
|
|
5138
|
+
if (fresh.length === 0 && open === 0) return "";
|
|
5139
|
+
const freshPart = fresh.length > 0 ? pc7.yellow(`${fresh.length} new`) : `${fresh.length} new`;
|
|
5140
|
+
return ` ${pc7.dim("contradictions:")} ${freshPart}${pc7.dim(`, ${open} open suggestion(s) -- run`)} ${pc7.bold("nexusmem stale")} ${pc7.dim("for detail")}
|
|
5141
|
+
`;
|
|
5142
|
+
}
|
|
4747
5143
|
async function runSync(opts) {
|
|
4748
5144
|
const { repo, ws, projectId, config } = await loadContext(opts.cwd);
|
|
4749
5145
|
const log = (line) => {
|
|
@@ -4797,6 +5193,7 @@ async function runSync(opts) {
|
|
|
4797
5193
|
const docs = await syncDocs(store, projectId, repo.root, config, log);
|
|
4798
5194
|
const structure = await syncStructure(store, projectId, repo.root, config, log);
|
|
4799
5195
|
let embedLine = "";
|
|
5196
|
+
let embeddingAvailable = false;
|
|
4800
5197
|
if (!opts.noEmbed) {
|
|
4801
5198
|
let lastLogged = 0;
|
|
4802
5199
|
const result = await embedPendingNodes(store, new OllamaEmbeddingProvider(), projectId, {
|
|
@@ -4808,6 +5205,7 @@ async function runSync(opts) {
|
|
|
4808
5205
|
log(` ${pc7.dim(`vector: ${attempted}/${total} embedded`)}`);
|
|
4809
5206
|
}
|
|
4810
5207
|
});
|
|
5208
|
+
embeddingAvailable = !result.providerUnavailable;
|
|
4811
5209
|
if (result.embedded > 0) {
|
|
4812
5210
|
const skippedPart = result.skipped > 0 ? pc7.dim(`, ${result.skipped} skipped`) : "";
|
|
4813
5211
|
const remainingPart = result.remaining > 0 ? pc7.yellow(`, ${result.remaining} still pending`) : "";
|
|
@@ -4817,6 +5215,10 @@ async function runSync(opts) {
|
|
|
4817
5215
|
log(`${pc7.dim("vector")} embedding provider unavailable (is Ollama running with nomic-embed-text pulled?) -- BM25-only for now`);
|
|
4818
5216
|
}
|
|
4819
5217
|
}
|
|
5218
|
+
const contradictionLine = !opts.noEmbed && embeddingAvailable ? await runAutoContradictionCheck(store, config, projectId, {
|
|
5219
|
+
embedder: new OllamaEmbeddingProvider(),
|
|
5220
|
+
slm: new OllamaChatProvider({ model: config.contradictions.model })
|
|
5221
|
+
}) : "";
|
|
4820
5222
|
let linkLine = "";
|
|
4821
5223
|
if (opts.linkFailures) {
|
|
4822
5224
|
const linkStats = correlateFailures(store, projectId);
|
|
@@ -4845,7 +5247,7 @@ async function runSync(opts) {
|
|
|
4845
5247
|
` ${pc7.green(`+${totals.inserted} new`)} ${pc7.yellow(`~${totals.updated} updated`)} ${pc7.dim(`=${totals.unchanged} unchanged`)}${deniedPart}`,
|
|
4846
5248
|
` ${pc7.dim(`${stats2.total} node(s) total across ${stats2.distinctFiles} file path(s)`)}`,
|
|
4847
5249
|
""
|
|
4848
|
-
].join("\n") + embedLine + linkLine
|
|
5250
|
+
].join("\n") + embedLine + linkLine + contradictionLine
|
|
4849
5251
|
);
|
|
4850
5252
|
return 0;
|
|
4851
5253
|
} finally {
|
|
@@ -5602,6 +6004,7 @@ ${pc17.bold(String(edges.length))} edge(s) from ${filesScanned} tracked ${TRACKE
|
|
|
5602
6004
|
|
|
5603
6005
|
// src/cli/commands/stale.ts
|
|
5604
6006
|
import pc18 from "picocolors";
|
|
6007
|
+
var STALE_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
5605
6008
|
async function runStale(opts) {
|
|
5606
6009
|
const { projectId, ws } = await loadContext(opts.cwd);
|
|
5607
6010
|
const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
|
|
@@ -5609,16 +6012,35 @@ async function runStale(opts) {
|
|
|
5609
6012
|
try {
|
|
5610
6013
|
const candidates = store.listStaleCandidates(projectId, { minAgeDays: opts.minAgeDays, limit: opts.limit });
|
|
5611
6014
|
if (candidates.length === 0) {
|
|
5612
|
-
out(`${pc18.dim("no stale candidates")} -- no
|
|
6015
|
+
out(`${pc18.dim("no stale candidates")} -- no unconfirmed node older than the threshold lacks a successor
|
|
5613
6016
|
`);
|
|
5614
6017
|
return 0;
|
|
5615
6018
|
}
|
|
6019
|
+
let suggestions = [];
|
|
6020
|
+
if (opts.checkContradictions) {
|
|
6021
|
+
suggestions = await checkContradictions(
|
|
6022
|
+
store,
|
|
6023
|
+
new OllamaEmbeddingProvider(),
|
|
6024
|
+
new OllamaChatProvider({ model: opts.model ?? DEFAULT_SLM_MODEL }),
|
|
6025
|
+
projectId,
|
|
6026
|
+
candidates,
|
|
6027
|
+
{ model: opts.model ?? DEFAULT_SLM_MODEL }
|
|
6028
|
+
);
|
|
6029
|
+
}
|
|
6030
|
+
const byCandidateId = /* @__PURE__ */ new Map();
|
|
6031
|
+
for (const s of store.listContradictionSuggestions(projectId)) {
|
|
6032
|
+
byCandidateId.set(s.candidateId, { againstId: s.againstId, againstTitle: s.againstTitle, reason: s.reason ?? "" });
|
|
6033
|
+
}
|
|
6034
|
+
for (const s of suggestions) byCandidateId.set(s.candidateId, s);
|
|
5616
6035
|
out(
|
|
5617
6036
|
[
|
|
5618
6037
|
`${pc18.bold(String(candidates.length))} stale candidate(s) -- oldest first, none of these were changed:`,
|
|
5619
|
-
...candidates.map(
|
|
5620
|
-
|
|
5621
|
-
|
|
6038
|
+
...candidates.map((c) => {
|
|
6039
|
+
const line = ` ${pc18.dim(c.id)} ${pc18.yellow(`${c.ageDays}d old`)} [${c.kind}] ${c.title}`;
|
|
6040
|
+
const hit = byCandidateId.get(c.id);
|
|
6041
|
+
return hit ? `${line}
|
|
6042
|
+
${pc18.red("likely superseded by")} ${pc18.dim(hit.againstId)} ${hit.againstTitle} -- ${hit.reason}` : line;
|
|
6043
|
+
}),
|
|
5622
6044
|
"",
|
|
5623
6045
|
`run ${pc18.bold("nexusmem mark-stale <id> --supersedes <newId>")} on any that are actually wrong`
|
|
5624
6046
|
].join("\n").concat("\n")
|
|
@@ -5689,6 +6111,7 @@ async function runStatus(opts) {
|
|
|
5689
6111
|
const otherProjectNodes = store.countProjectNodes(otherProjectIds);
|
|
5690
6112
|
const structure = store.fileEdgeStats(projectId);
|
|
5691
6113
|
const staleCount = store.countStaleCandidates(projectId);
|
|
6114
|
+
const flaggedCount = store.countContradictionSuggestions(projectId);
|
|
5692
6115
|
const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
|
|
5693
6116
|
const kinds = Object.entries(stats2.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
|
|
5694
6117
|
const staleProjectWarning = otherProjectIds.length ? `${pc19.yellow("stale ")} ${otherProjectIds.length} prior project ${otherProjectIds.length === 1 ? "identity holds" : "identities hold"} ${otherProjectNodes} node(s) \u2014 run ${pc19.bold(
|
|
@@ -5717,7 +6140,8 @@ async function runStatus(opts) {
|
|
|
5717
6140
|
gitCursor && gitCursor !== repo.head ? `${pc19.yellow("git behind HEAD")} \u2014 run ${pc19.bold("nexusmem sync")}` : "",
|
|
5718
6141
|
chains.failuresTotal ? `${pc19.dim("chains ")} ${pc19.bold(String(chains.resolvedTotal))}/${chains.failuresTotal} failure(s) resolved ${pc19.dim(`(${chains.resolvedByRetry} retry, ${chains.resolvedByDiscussion} discussion)`)}${chains.resolvedTotal < chains.failuresTotal ? ` \u2014 run ${pc19.bold("nexusmem sync --link-failures")} to link more` : ""}` : "",
|
|
5719
6142
|
structure.edges ? `${pc19.dim("structure")} ${pc19.bold(String(structure.edges))} import edge(s) across ${structure.files} file(s)` : "",
|
|
5720
|
-
staleCount ? `${pc19.dim("aging ")} ${pc19.bold(String(staleCount))}
|
|
6143
|
+
staleCount ? `${pc19.dim("aging ")} ${pc19.bold(String(staleCount))} unconfirmed node(s) worth a look \u2014 run ${pc19.bold("nexusmem stale")}` : "",
|
|
6144
|
+
flaggedCount ? `${pc19.dim("flagged ")} ${pc19.bold(String(flaggedCount))} likely-superseded node(s) awaiting review \u2014 run ${pc19.bold("nexusmem stale")} for detail` : ""
|
|
5721
6145
|
].filter((line) => line !== "").join("\n").concat("\n")
|
|
5722
6146
|
);
|
|
5723
6147
|
return 0;
|
|
@@ -5839,8 +6263,19 @@ program.command("mark-stale").description(
|
|
|
5839
6263
|
).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(
|
|
5840
6264
|
(nodeId, options) => guard(() => runMarkStale({ cwd: options.cwd, nodeId, supersedesId: options.supersedes }))()
|
|
5841
6265
|
);
|
|
5842
|
-
program.command("stale").description("List
|
|
5843
|
-
|
|
6266
|
+
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(
|
|
6267
|
+
"--check-contradictions",
|
|
6268
|
+
"ask the local SLM whether a similar newer node actually contradicts each candidate (needs Ollama)"
|
|
6269
|
+
).option("--model <name>", "Ollama chat model for --check-contradictions", STALE_DEFAULT_MODEL).action(
|
|
6270
|
+
(options) => guard(
|
|
6271
|
+
() => runStale({
|
|
6272
|
+
cwd: options.cwd,
|
|
6273
|
+
minAgeDays: options.minAgeDays,
|
|
6274
|
+
limit: options.limit,
|
|
6275
|
+
checkContradictions: options.checkContradictions,
|
|
6276
|
+
model: options.model
|
|
6277
|
+
})
|
|
6278
|
+
)()
|
|
5844
6279
|
);
|
|
5845
6280
|
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 }))());
|
|
5846
6281
|
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(
|
|
@@ -5898,7 +6333,7 @@ program.command("precheck").description("Warn about staged files with unresolved
|
|
|
5898
6333
|
})
|
|
5899
6334
|
)()
|
|
5900
6335
|
);
|
|
5901
|
-
program.command("scan-structure").description("Preview the JS/TS/Python import-graph edges a sync would produce (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--json", "emit edges as JSON on stdout", false).action((options) => guard(() => runScanStructure({ cwd: options.cwd, json: options.json }))());
|
|
6336
|
+
program.command("scan-structure").description("Preview the JS/TS/Python/Go/Rust/Java/PHP import-graph edges a sync would produce (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--json", "emit edges as JSON on stdout", false).action((options) => guard(() => runScanStructure({ cwd: options.cwd, json: options.json }))());
|
|
5902
6337
|
program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
|
|
5903
6338
|
program.parseAsync(process.argv).catch((err) => {
|
|
5904
6339
|
const message = err instanceof Error ? err.message : String(err);
|