@kage-core/kage-graph-mcp 2.2.6 → 2.3.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/dist/cli.js +79 -1
- package/dist/index.js +42 -0
- package/dist/kernel.js +517 -12
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -71,12 +71,14 @@ Usage:
|
|
|
71
71
|
kage slots set --project <dir> --label <label> --content <text> [--description <text>] [--paths a,b] [--tags a,b] [--size-limit <n>] [--unpinned] [--json]
|
|
72
72
|
kage slots delete --project <dir> --label <label> [--json]
|
|
73
73
|
kage handoff --project <dir> [--json]
|
|
74
|
+
kage layers --project <dir> [--json]
|
|
74
75
|
kage lifecycle --project <dir> [--json]
|
|
75
76
|
kage reverify --project <dir> --packet <id> [--json]
|
|
76
77
|
kage reconcile --project <dir> [--session <id>] [--json]
|
|
77
78
|
kage timeline --project <dir> [--days <n>] [--json]
|
|
78
79
|
kage lineage --project <dir> [--json]
|
|
79
80
|
kage supersede --project <dir> --packet <old-id> --replacement <new-id> [--reason <text>] [--json]
|
|
81
|
+
kage conflicts --project <dir> [--json]
|
|
80
82
|
kage contributors --project <dir> [--json]
|
|
81
83
|
kage profile --project <dir> [--json]
|
|
82
84
|
kage xray --project <dir> [--json]
|
|
@@ -108,7 +110,8 @@ Usage:
|
|
|
108
110
|
kage graph "<query>" --project <dir> [--json]
|
|
109
111
|
kage graph-registry --project <dir> [--json]
|
|
110
112
|
kage embeddings build --project <dir> [--model Xenova/all-MiniLM-L6-v2] [--json]
|
|
111
|
-
kage recall "<query>" --project <dir> [--json] [--explain] [--embeddings] [--max-context-tokens <n>] [--structural-hops <n>]
|
|
113
|
+
kage recall "<query>" --project <dir> [--json] [--explain] [--embeddings] [--docs] [--max-context-tokens <n>] [--structural-hops <n>]
|
|
114
|
+
kage docs-search "<query>" --project <dir> [--limit <n>] [--json] search this repo's own committed docs (README, docs/**, *.md)
|
|
112
115
|
kage file-context --project <dir> --path <file> [--json]
|
|
113
116
|
kage observe --project <dir> --event <json>
|
|
114
117
|
kage sessions --project <dir> [--json]
|
|
@@ -166,6 +169,15 @@ function numberArg(args, name, fallback) {
|
|
|
166
169
|
const value = takeArg(args, name);
|
|
167
170
|
return value ? Number(value) : fallback;
|
|
168
171
|
}
|
|
172
|
+
function printContradictionWarning(contradictions) {
|
|
173
|
+
if (!contradictions?.length)
|
|
174
|
+
return;
|
|
175
|
+
console.log(`\n⚠ This contradicts ${contradictions.length} existing memor${contradictions.length === 1 ? "y" : "ies"}:`);
|
|
176
|
+
for (const item of contradictions) {
|
|
177
|
+
console.log(` - ${item.packet_id} (${item.title}): ${item.reason}`);
|
|
178
|
+
}
|
|
179
|
+
console.log(" Resolve with kage supersede --packet <old> --replacement <new>, or keep both intentionally.");
|
|
180
|
+
}
|
|
169
181
|
function firstPositional(args) {
|
|
170
182
|
return args.find((arg, index) => index > 0 && !arg.startsWith("--") && !args[index - 1]?.startsWith("--"));
|
|
171
183
|
}
|
|
@@ -1363,6 +1375,21 @@ async function main() {
|
|
|
1363
1375
|
}
|
|
1364
1376
|
return;
|
|
1365
1377
|
}
|
|
1378
|
+
if (command === "layers" || command === "memory-layers") {
|
|
1379
|
+
const result = (0, kernel_js_1.kageLayers)(projectArg(args));
|
|
1380
|
+
if (args.includes("--json")) {
|
|
1381
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
console.log("Kage memory layers");
|
|
1385
|
+
for (const layer of result.layers) {
|
|
1386
|
+
console.log(` ${layer.layer} ${layer.label} (${layer.count})`);
|
|
1387
|
+
console.log(` ${layer.description}`);
|
|
1388
|
+
for (const ex of layer.examples)
|
|
1389
|
+
console.log(` · ${ex}`);
|
|
1390
|
+
}
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1366
1393
|
if (command === "lifecycle" || command === "memory-lifecycle") {
|
|
1367
1394
|
const result = (0, kernel_js_1.kageMemoryLifecycle)(projectArg(args));
|
|
1368
1395
|
if (args.includes("--json")) {
|
|
@@ -1604,6 +1631,27 @@ async function main() {
|
|
|
1604
1631
|
console.log(`Warnings:\n${result.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
|
|
1605
1632
|
return;
|
|
1606
1633
|
}
|
|
1634
|
+
if (command === "conflicts") {
|
|
1635
|
+
const result = (0, kernel_js_1.kageConflicts)(projectArg(args));
|
|
1636
|
+
if (args.includes("--json")) {
|
|
1637
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
if (!result.count) {
|
|
1641
|
+
console.log("Memory conflicts: none — no contradicting packet pairs found.");
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
console.log(`Memory conflicts: ${result.count} contradicting packet pair${result.count === 1 ? "" : "s"}`);
|
|
1645
|
+
for (const pair of result.pairs) {
|
|
1646
|
+
console.log(`\n ⚠ ${pair.a.title} <--> ${pair.b.title}`);
|
|
1647
|
+
console.log(` ${pair.a.id}`);
|
|
1648
|
+
console.log(` ${pair.b.id}`);
|
|
1649
|
+
console.log(` shared paths: ${pair.shared_paths.join(", ")}`);
|
|
1650
|
+
console.log(` ${pair.reason}`);
|
|
1651
|
+
}
|
|
1652
|
+
console.log("\nResolve each with: kage supersede --packet <old> --replacement <new>");
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1607
1655
|
if (command === "module-health") {
|
|
1608
1656
|
const result = (0, kernel_js_1.kageModuleHealth)(projectArg(args));
|
|
1609
1657
|
if (args.includes("--json")) {
|
|
@@ -1918,6 +1966,7 @@ async function main() {
|
|
|
1918
1966
|
graphNodes: listArg(takeArg(args, "--graph-nodes")),
|
|
1919
1967
|
allowMissingPaths: args.includes("--allow-missing-paths"),
|
|
1920
1968
|
strictCitations: true,
|
|
1969
|
+
strictContradictions: args.includes("--strict-contradictions"),
|
|
1921
1970
|
discoveryTokens: args.includes("--discovery-tokens") ? numberArg(args, "--discovery-tokens", 0) : undefined,
|
|
1922
1971
|
});
|
|
1923
1972
|
if (!result.ok) {
|
|
@@ -1925,6 +1974,7 @@ async function main() {
|
|
|
1925
1974
|
process.exit(2);
|
|
1926
1975
|
}
|
|
1927
1976
|
console.log(`Captured ${personal ? "personal" : "session"} learning: ${result.path}`);
|
|
1977
|
+
printContradictionWarning(result.contradictions);
|
|
1928
1978
|
if (result.warnings?.length)
|
|
1929
1979
|
console.log(`Warnings:\n${result.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
|
|
1930
1980
|
console.log(personal
|
|
@@ -2003,6 +2053,11 @@ async function main() {
|
|
|
2003
2053
|
const result = args.includes("--embeddings")
|
|
2004
2054
|
? await (0, kernel_js_1.recallWithEmbeddings)(projectArg(args), query, 5, args.includes("--explain"))
|
|
2005
2055
|
: (0, kernel_js_1.recall)(projectArg(args), query, 5, args.includes("--explain"), { maxContextTokens, structuralHops });
|
|
2056
|
+
if (args.includes("--docs")) {
|
|
2057
|
+
const docsSection = (0, kernel_js_1.docsRecallSection)(projectArg(args), query, 3);
|
|
2058
|
+
if (docsSection)
|
|
2059
|
+
result.context_block = `${result.context_block}\n\n${docsSection}`;
|
|
2060
|
+
}
|
|
2006
2061
|
if (args.includes("--json"))
|
|
2007
2062
|
console.log(JSON.stringify(result, null, 2));
|
|
2008
2063
|
else {
|
|
@@ -2034,6 +2089,27 @@ async function main() {
|
|
|
2034
2089
|
console.log(`Packets: ${result.packet_count}`);
|
|
2035
2090
|
return;
|
|
2036
2091
|
}
|
|
2092
|
+
if (command === "docs-search") {
|
|
2093
|
+
const query = firstPositional(args);
|
|
2094
|
+
if (!query)
|
|
2095
|
+
usage();
|
|
2096
|
+
const limit = numberArg(args, "--limit", 5);
|
|
2097
|
+
const hits = (0, kernel_js_1.searchDocs)(projectArg(args), query, limit);
|
|
2098
|
+
if (args.includes("--json")) {
|
|
2099
|
+
console.log(JSON.stringify({ query, source: "repo-docs", hits }, null, 2));
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
if (!hits.length) {
|
|
2103
|
+
console.log("No matching docs found in this repo's own committed documentation.");
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
console.log(`Docs search (this repo's own committed documentation) — "${query}":`);
|
|
2107
|
+
for (const hit of hits) {
|
|
2108
|
+
console.log(`\n${hit.doc_path}:${hit.line} [${hit.score}] ${hit.heading}`);
|
|
2109
|
+
console.log(` ${hit.snippet}`);
|
|
2110
|
+
}
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2037
2113
|
if (command === "observe") {
|
|
2038
2114
|
const event = takeArg(args, "--event");
|
|
2039
2115
|
if (!event)
|
|
@@ -2173,6 +2249,7 @@ async function main() {
|
|
|
2173
2249
|
graphNodes: listArg(takeArg(args, "--graph-nodes")),
|
|
2174
2250
|
allowMissingPaths: args.includes("--allow-missing-paths"),
|
|
2175
2251
|
strictCitations: true,
|
|
2252
|
+
strictContradictions: args.includes("--strict-contradictions"),
|
|
2176
2253
|
};
|
|
2177
2254
|
const result = (0, kernel_js_1.capture)(input);
|
|
2178
2255
|
if (!result.ok) {
|
|
@@ -2180,6 +2257,7 @@ async function main() {
|
|
|
2180
2257
|
process.exit(2);
|
|
2181
2258
|
}
|
|
2182
2259
|
console.log(`Captured repo-local packet: ${result.path}`);
|
|
2260
|
+
printContradictionWarning(result.contradictions);
|
|
2183
2261
|
if (result.warnings?.length)
|
|
2184
2262
|
console.log(`Warnings:\n${result.warnings.map((warning) => ` - ${warning}`).join("\n")}`);
|
|
2185
2263
|
console.log("Repo-local memory is written immediately. Promotion to org/global still requires explicit review.");
|
package/dist/index.js
CHANGED
|
@@ -160,6 +160,7 @@ function listTools() {
|
|
|
160
160
|
limit: { type: "number" },
|
|
161
161
|
explain: { type: "boolean" },
|
|
162
162
|
embeddings: { type: "boolean" },
|
|
163
|
+
docs: { type: "boolean", description: "If true, append a Docs section (<=3 hits) drawn from this repo's own committed documentation." },
|
|
163
164
|
json: { type: "boolean" },
|
|
164
165
|
max_context_tokens: { type: "number" },
|
|
165
166
|
structural_hops: { type: "number", description: "If >0, append a bounded N-hop code-graph blast radius seeded from the recalled memory's files." },
|
|
@@ -374,6 +375,19 @@ function listTools() {
|
|
|
374
375
|
required: ["project_dir"],
|
|
375
376
|
},
|
|
376
377
|
},
|
|
378
|
+
{
|
|
379
|
+
name: "kage_docs_search",
|
|
380
|
+
description: "Search this repo's OWN committed documentation (README, docs/**, *.md, common doc dirs — including any framework/API docs checked into the repo). BM25 over heading-anchored chunks from .agent_memory/indexes/docs-index.json. Returns ranked doc hits with doc_path, heading, line, and snippet. This indexes only files on disk in the project, never the internet.",
|
|
381
|
+
inputSchema: {
|
|
382
|
+
type: "object",
|
|
383
|
+
properties: {
|
|
384
|
+
query: { type: "string" },
|
|
385
|
+
project_dir: { type: "string" },
|
|
386
|
+
limit: { type: "number" },
|
|
387
|
+
},
|
|
388
|
+
required: ["query", "project_dir"],
|
|
389
|
+
},
|
|
390
|
+
},
|
|
377
391
|
{
|
|
378
392
|
name: "kage_metrics",
|
|
379
393
|
description: "Return concise Kage adoption and quality metrics: code graph counts, language/parser coverage, memory graph evidence coverage, pending/approved packets, validation state, and readiness score.",
|
|
@@ -602,6 +616,17 @@ function listTools() {
|
|
|
602
616
|
required: ["project_dir", "packet_id", "replacement_packet_id"],
|
|
603
617
|
},
|
|
604
618
|
},
|
|
619
|
+
{
|
|
620
|
+
name: "kage_conflicts",
|
|
621
|
+
description: "List repo-local memory packet pairs that contradict each other (same cited path, same subject, opposing claim). Resolve each with kage_supersede, or keep both intentionally.",
|
|
622
|
+
inputSchema: {
|
|
623
|
+
type: "object",
|
|
624
|
+
properties: {
|
|
625
|
+
project_dir: { type: "string" },
|
|
626
|
+
},
|
|
627
|
+
required: ["project_dir"],
|
|
628
|
+
},
|
|
629
|
+
},
|
|
605
630
|
{
|
|
606
631
|
name: "kage_benchmark",
|
|
607
632
|
description: "Return Kage proof metrics, or set mode=memory_quality / memory_scale for synthetic memory retrieval benchmarks.",
|
|
@@ -1052,10 +1077,21 @@ async function callTool(name, args) {
|
|
|
1052
1077
|
const result = args?.embeddings
|
|
1053
1078
|
? await (0, kernel_js_1.recallWithEmbeddings)(String(args?.project_dir ?? ""), String(args?.query ?? ""), Number(args?.limit ?? 5), Boolean(args?.explain))
|
|
1054
1079
|
: (0, kernel_js_1.recall)(String(args?.project_dir ?? ""), String(args?.query ?? ""), Number(args?.limit ?? 5), Boolean(args?.explain), { maxContextTokens, structuralHops });
|
|
1080
|
+
if (args?.docs) {
|
|
1081
|
+
const docsSection = (0, kernel_js_1.docsRecallSection)(String(args?.project_dir ?? ""), String(args?.query ?? ""), 3);
|
|
1082
|
+
if (docsSection)
|
|
1083
|
+
result.context_block = `${result.context_block}\n\n${docsSection}`;
|
|
1084
|
+
}
|
|
1055
1085
|
return {
|
|
1056
1086
|
content: [{ type: "text", text: args?.json || args?.explain ? JSON.stringify(result, null, 2) : result.context_block }],
|
|
1057
1087
|
};
|
|
1058
1088
|
}
|
|
1089
|
+
if (name === "kage_docs_search") {
|
|
1090
|
+
const hits = (0, kernel_js_1.searchDocs)(String(args?.project_dir ?? ""), String(args?.query ?? ""), Number(args?.limit ?? 5));
|
|
1091
|
+
return {
|
|
1092
|
+
content: [{ type: "text", text: JSON.stringify({ query: String(args?.query ?? ""), source: "repo-docs", hits }, null, 2) }],
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1059
1095
|
if (name === "kage_graph") {
|
|
1060
1096
|
const result = (0, kernel_js_1.queryGraph)(String(args?.project_dir ?? ""), String(args?.query ?? ""), Number(args?.limit ?? 10));
|
|
1061
1097
|
return {
|
|
@@ -1224,6 +1260,12 @@ async function callTool(name, args) {
|
|
|
1224
1260
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1225
1261
|
};
|
|
1226
1262
|
}
|
|
1263
|
+
if (name === "kage_conflicts") {
|
|
1264
|
+
const result = (0, kernel_js_1.kageConflicts)(String(args?.project_dir ?? ""));
|
|
1265
|
+
return {
|
|
1266
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1227
1269
|
if (name === "kage_module_health") {
|
|
1228
1270
|
const result = (0, kernel_js_1.kageModuleHealth)(String(args?.project_dir ?? ""));
|
|
1229
1271
|
return {
|
package/dist/kernel.js
CHANGED
|
@@ -67,6 +67,8 @@ exports.valueSummary = valueSummary;
|
|
|
67
67
|
exports.formatTokenCount = formatTokenCount;
|
|
68
68
|
exports.kageFileContext = kageFileContext;
|
|
69
69
|
exports.kageActivity = kageActivity;
|
|
70
|
+
exports.detectContradictions = detectContradictions;
|
|
71
|
+
exports.kageConflicts = kageConflicts;
|
|
70
72
|
exports.kageMemoryReconciliation = kageMemoryReconciliation;
|
|
71
73
|
exports.evaluateMemoryAdmission = evaluateMemoryAdmission;
|
|
72
74
|
exports.validatePacket = validatePacket;
|
|
@@ -87,6 +89,9 @@ exports.writeLspSymbolIndex = writeLspSymbolIndex;
|
|
|
87
89
|
exports.writeCodeIndex = writeCodeIndex;
|
|
88
90
|
exports.buildCodeGraph = buildCodeGraph;
|
|
89
91
|
exports.buildKnowledgeGraph = buildKnowledgeGraph;
|
|
92
|
+
exports.buildDocsIndex = buildDocsIndex;
|
|
93
|
+
exports.searchDocs = searchDocs;
|
|
94
|
+
exports.docsRecallSection = docsRecallSection;
|
|
90
95
|
exports.buildIndexes = buildIndexes;
|
|
91
96
|
exports.indexProject = indexProject;
|
|
92
97
|
exports.refreshProject = refreshProject;
|
|
@@ -189,6 +194,7 @@ exports.capturePersonal = capturePersonal;
|
|
|
189
194
|
exports.syncSetup = syncSetup;
|
|
190
195
|
exports.syncStatus = syncStatus;
|
|
191
196
|
exports.syncPersonal = syncPersonal;
|
|
197
|
+
exports.kageLayers = kageLayers;
|
|
192
198
|
const node_crypto_1 = require("node:crypto");
|
|
193
199
|
const node_child_process_1 = require("node:child_process");
|
|
194
200
|
const node_fs_1 = require("node:fs");
|
|
@@ -249,6 +255,9 @@ exports.SETUP_AGENTS = [
|
|
|
249
255
|
"kilo-code",
|
|
250
256
|
"claude-desktop",
|
|
251
257
|
"aider",
|
|
258
|
+
"openclaw",
|
|
259
|
+
"copilot",
|
|
260
|
+
"hermes",
|
|
252
261
|
"generic-mcp",
|
|
253
262
|
];
|
|
254
263
|
const DEFAULT_CONFIDENCE = 0.7;
|
|
@@ -1345,6 +1354,184 @@ function duplicateCandidatesWithContext(packet, context, threshold = 0.58) {
|
|
|
1345
1354
|
status: entry.packet.status,
|
|
1346
1355
|
}));
|
|
1347
1356
|
}
|
|
1357
|
+
// Negation cues: a body that flips a claim. "do not use", "is not", "never",
|
|
1358
|
+
// "should not", "no longer", "stop using", "avoid", "instead of".
|
|
1359
|
+
const CONTRADICTION_NEGATION_CUES = [
|
|
1360
|
+
"do not",
|
|
1361
|
+
"don't",
|
|
1362
|
+
"does not",
|
|
1363
|
+
"doesn't",
|
|
1364
|
+
"is not",
|
|
1365
|
+
"isn't",
|
|
1366
|
+
"are not",
|
|
1367
|
+
"aren't",
|
|
1368
|
+
"was not",
|
|
1369
|
+
"should not",
|
|
1370
|
+
"shouldn't",
|
|
1371
|
+
"must not",
|
|
1372
|
+
"cannot",
|
|
1373
|
+
"can't",
|
|
1374
|
+
"never",
|
|
1375
|
+
"no longer",
|
|
1376
|
+
"not idempotent",
|
|
1377
|
+
"not safe",
|
|
1378
|
+
"not thread-safe",
|
|
1379
|
+
"not supported",
|
|
1380
|
+
"not required",
|
|
1381
|
+
"not needed",
|
|
1382
|
+
"stop using",
|
|
1383
|
+
"avoid",
|
|
1384
|
+
"deprecated",
|
|
1385
|
+
"removed",
|
|
1386
|
+
"disable",
|
|
1387
|
+
"disabled",
|
|
1388
|
+
];
|
|
1389
|
+
// Replacement cues: a body that points the claim at a different answer.
|
|
1390
|
+
// "use Y instead", "replaced by", "switched to", "now use", "migrate to".
|
|
1391
|
+
const CONTRADICTION_REPLACEMENT_CUES = [
|
|
1392
|
+
"instead",
|
|
1393
|
+
"instead of",
|
|
1394
|
+
"replaced by",
|
|
1395
|
+
"replaced with",
|
|
1396
|
+
"replace with",
|
|
1397
|
+
"switched to",
|
|
1398
|
+
"switch to",
|
|
1399
|
+
"migrated to",
|
|
1400
|
+
"migrate to",
|
|
1401
|
+
"now use",
|
|
1402
|
+
"now uses",
|
|
1403
|
+
"moved to",
|
|
1404
|
+
"superseded by",
|
|
1405
|
+
"in favor of",
|
|
1406
|
+
"rather than",
|
|
1407
|
+
];
|
|
1408
|
+
function normalizedClaimText(packet) {
|
|
1409
|
+
return `${packet.title}\n${packet.summary}\n${packet.body}`.toLowerCase();
|
|
1410
|
+
}
|
|
1411
|
+
function negationCueCount(text) {
|
|
1412
|
+
let count = 0;
|
|
1413
|
+
for (const cue of CONTRADICTION_NEGATION_CUES)
|
|
1414
|
+
if (text.includes(cue))
|
|
1415
|
+
count += 1;
|
|
1416
|
+
return count;
|
|
1417
|
+
}
|
|
1418
|
+
function replacementCueCount(text) {
|
|
1419
|
+
let count = 0;
|
|
1420
|
+
for (const cue of CONTRADICTION_REPLACEMENT_CUES)
|
|
1421
|
+
if (text.includes(cue))
|
|
1422
|
+
count += 1;
|
|
1423
|
+
return count;
|
|
1424
|
+
}
|
|
1425
|
+
// The subject is the shared vocabulary that BOTH packets are talking about. We
|
|
1426
|
+
// require the two packets to agree on a subject (token overlap in title+summary)
|
|
1427
|
+
// but DISAGREE on polarity around it.
|
|
1428
|
+
function sharedSubjectTokens(a, b) {
|
|
1429
|
+
const aSubject = tokenSet(`${a.title}\n${a.summary}`);
|
|
1430
|
+
const bSubject = tokenSet(`${b.title}\n${b.summary}`);
|
|
1431
|
+
const shared = new Set();
|
|
1432
|
+
for (const token of aSubject)
|
|
1433
|
+
if (bSubject.has(token))
|
|
1434
|
+
shared.add(token);
|
|
1435
|
+
return shared;
|
|
1436
|
+
}
|
|
1437
|
+
// Opposing-polarity signal between two same-subject bodies. Conservative: a
|
|
1438
|
+
// contradiction needs one side to assert a claim and the other to negate or
|
|
1439
|
+
// replace it — NOT both sides carrying identical framing (that's a duplicate).
|
|
1440
|
+
function opposingPolarity(aText, bText) {
|
|
1441
|
+
const aNeg = negationCueCount(aText);
|
|
1442
|
+
const bNeg = negationCueCount(bText);
|
|
1443
|
+
const aRepl = replacementCueCount(aText);
|
|
1444
|
+
const bRepl = replacementCueCount(bText);
|
|
1445
|
+
// (1) Negation asymmetry: one side negates the claim, the other does not.
|
|
1446
|
+
// Two equally-hedged notes (both negate, or neither negates) are not flagged.
|
|
1447
|
+
if (aNeg === 0 && bNeg >= 1)
|
|
1448
|
+
return "one memory negates a claim the other asserts";
|
|
1449
|
+
if (bNeg === 0 && aNeg >= 1)
|
|
1450
|
+
return "one memory negates a claim the other asserts";
|
|
1451
|
+
// (2) Replacement asymmetry: one side says "use Y instead", the other still
|
|
1452
|
+
// asserts the original (no replacement framing). Same-subject + redirect.
|
|
1453
|
+
if (aRepl === 0 && bRepl >= 1)
|
|
1454
|
+
return "one memory replaces the approach the other still recommends";
|
|
1455
|
+
if (bRepl === 0 && aRepl >= 1)
|
|
1456
|
+
return "one memory replaces the approach the other still recommends";
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
// Detect existing approved packets that contradict `candidate`. Pure: takes the
|
|
1460
|
+
// candidate and (optionally) the existing pool, performs no writes. Returns the
|
|
1461
|
+
// conflicting approved packets with the shared paths and a human reason.
|
|
1462
|
+
function detectContradictions(projectDir, candidate, opts = {}) {
|
|
1463
|
+
const subjectThreshold = opts.subjectThreshold ?? 0.34;
|
|
1464
|
+
const candidatePaths = new Set(candidate.paths.filter((path) => meaningfulMemoryPath(path) && !shouldSkipRepoMemoryPath(path)));
|
|
1465
|
+
if (!candidatePaths.size)
|
|
1466
|
+
return [];
|
|
1467
|
+
const existing = (opts.existing ?? loadApprovedPackets(projectDir)).filter((packet) => packet.id !== candidate.id && packet.status === "approved");
|
|
1468
|
+
const candidateText = normalizedClaimText(candidate);
|
|
1469
|
+
const candidateSubject = tokenSet(`${candidate.title}\n${candidate.summary}`);
|
|
1470
|
+
const findings = [];
|
|
1471
|
+
for (const packet of existing) {
|
|
1472
|
+
// (a) shared cited path — the structural anchor that makes a contradiction
|
|
1473
|
+
// about the SAME thing rather than two unrelated facts.
|
|
1474
|
+
const sharedPaths = packet.paths.filter((path) => candidatePaths.has(path));
|
|
1475
|
+
if (!sharedPaths.length)
|
|
1476
|
+
continue;
|
|
1477
|
+
// (b) same subject — high title/summary similarity. Reuses the duplicate
|
|
1478
|
+
// similarity machinery (jaccard over token sets) so detection rides on
|
|
1479
|
+
// the same proven scorer rather than a new bespoke metric.
|
|
1480
|
+
const subjectSimilarity = jaccard(candidateSubject, tokenSet(`${packet.title}\n${packet.summary}`));
|
|
1481
|
+
const subjectOverlap = sharedSubjectTokens(candidate, packet).size;
|
|
1482
|
+
if (subjectSimilarity < subjectThreshold && subjectOverlap < 2)
|
|
1483
|
+
continue;
|
|
1484
|
+
// (c) opposing polarity — the claim itself is flipped/redirected.
|
|
1485
|
+
const reason = opposingPolarity(candidateText, normalizedClaimText(packet));
|
|
1486
|
+
if (!reason)
|
|
1487
|
+
continue;
|
|
1488
|
+
findings.push({
|
|
1489
|
+
packet_id: packet.id,
|
|
1490
|
+
title: packet.title,
|
|
1491
|
+
shared_paths: sharedPaths,
|
|
1492
|
+
reason: `${reason} (shared path: ${sharedPaths.join(", ")})`,
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
return findings
|
|
1496
|
+
.sort((a, b) => a.title.localeCompare(b.title))
|
|
1497
|
+
.slice(0, 5);
|
|
1498
|
+
}
|
|
1499
|
+
// Repo-wide pairwise contradiction scan across approved packets. Reuses
|
|
1500
|
+
// detectContradictions per packet against the already-loaded pool so the same
|
|
1501
|
+
// conservative heuristic governs both write-time surfacing and the audit view.
|
|
1502
|
+
function kageConflicts(projectDir) {
|
|
1503
|
+
ensureMemoryDirs(projectDir);
|
|
1504
|
+
const approved = loadApprovedPackets(projectDir);
|
|
1505
|
+
const byId = new Map(approved.map((packet) => [packet.id, packet]));
|
|
1506
|
+
const pairs = [];
|
|
1507
|
+
const seen = new Set();
|
|
1508
|
+
for (const packet of approved) {
|
|
1509
|
+
const findings = detectContradictions(projectDir, packet, { existing: approved });
|
|
1510
|
+
for (const finding of findings) {
|
|
1511
|
+
const ordered = [packet.id, finding.packet_id].sort();
|
|
1512
|
+
const key = ordered.join("\0");
|
|
1513
|
+
if (seen.has(key))
|
|
1514
|
+
continue;
|
|
1515
|
+
seen.add(key);
|
|
1516
|
+
const firstPacket = byId.get(ordered[0]);
|
|
1517
|
+
const secondPacket = byId.get(ordered[1]);
|
|
1518
|
+
pairs.push({
|
|
1519
|
+
a: { id: firstPacket.id, title: firstPacket.title },
|
|
1520
|
+
b: { id: secondPacket.id, title: secondPacket.title },
|
|
1521
|
+
shared_paths: finding.shared_paths,
|
|
1522
|
+
reason: finding.reason,
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
pairs.sort((x, y) => x.a.title.localeCompare(y.a.title) || x.b.title.localeCompare(y.b.title));
|
|
1527
|
+
return {
|
|
1528
|
+
ok: true,
|
|
1529
|
+
project_dir: projectDir,
|
|
1530
|
+
generated_at: nowIso(),
|
|
1531
|
+
count: pairs.length,
|
|
1532
|
+
pairs,
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1348
1535
|
function packetFeedbackScore(packet) {
|
|
1349
1536
|
const quality = (packet.quality ?? {});
|
|
1350
1537
|
return Number(quality.votes_up ?? 0) * 2 - Number(quality.votes_down ?? 0) * 3 - Number(quality.reports_stale ?? 0) * 4;
|
|
@@ -5984,6 +6171,213 @@ function hydrateKnowledgeGraphArtifact(projectDir, artifact) {
|
|
|
5984
6171
|
edges: readJson(edgesPath),
|
|
5985
6172
|
};
|
|
5986
6173
|
}
|
|
6174
|
+
// --- Docs search index ---------------------------------------------------
|
|
6175
|
+
// Searchable index over the REPO's OWN committed documentation (project README,
|
|
6176
|
+
// docs/**, *.md, and common doc dirs — plus any framework/API docs that are
|
|
6177
|
+
// checked into this repo). It indexes nothing from the internet; only files
|
|
6178
|
+
// that exist on disk in this project. Recall can therefore answer from docs,
|
|
6179
|
+
// not just learned memory packets and code.
|
|
6180
|
+
const DOCS_INDEX_SCHEMA_VERSION = 1;
|
|
6181
|
+
// Heading-anchored chunks are capped so a single long section can't dominate the
|
|
6182
|
+
// BM25 length normalization or bloat the artifact.
|
|
6183
|
+
const DOCS_CHUNK_MAX_CHARS = 1600;
|
|
6184
|
+
// Directory names that commonly hold prose documentation, beyond the root README
|
|
6185
|
+
// and a top-level docs/ dir. These are matched as path segments.
|
|
6186
|
+
const DOCS_DIR_NAMES = new Set(["docs", "doc", "documentation", "guides", "guide", "wiki", "manual", "handbook"]);
|
|
6187
|
+
const DOCS_EXTENSIONS = new Set([".md", ".mdx", ".markdown", ".rst", ".txt"]);
|
|
6188
|
+
function isDocFile(relativePath) {
|
|
6189
|
+
if (shouldSkipCodePath(relativePath))
|
|
6190
|
+
return false;
|
|
6191
|
+
if (isNoisePath(relativePath))
|
|
6192
|
+
return false;
|
|
6193
|
+
const segments = relativePath.split("/");
|
|
6194
|
+
const name = segments[segments.length - 1];
|
|
6195
|
+
if (!DOCS_EXTENSIONS.has(extensionOf(name)))
|
|
6196
|
+
return false;
|
|
6197
|
+
// A markdown/rst/txt file qualifies if it is the root README, sits under a
|
|
6198
|
+
// recognised doc directory, or is any committed *.md anywhere in the tree.
|
|
6199
|
+
if (/^readme\b/i.test(name))
|
|
6200
|
+
return true;
|
|
6201
|
+
if (segments.slice(0, -1).some((part) => DOCS_DIR_NAMES.has(part.toLowerCase())))
|
|
6202
|
+
return true;
|
|
6203
|
+
const extension = extensionOf(name);
|
|
6204
|
+
return extension === ".md" || extension === ".mdx" || extension === ".markdown";
|
|
6205
|
+
}
|
|
6206
|
+
function discoverDocFiles(projectDir) {
|
|
6207
|
+
const all = walkFiles(projectDir, (absolute) => {
|
|
6208
|
+
const relativePath = (0, node_path_1.relative)(projectDir, absolute).replace(/\\/g, "/");
|
|
6209
|
+
return isDocFile(relativePath);
|
|
6210
|
+
});
|
|
6211
|
+
return all.map((absolute) => (0, node_path_1.relative)(projectDir, absolute).replace(/\\/g, "/")).sort();
|
|
6212
|
+
}
|
|
6213
|
+
function headingAnchor(heading) {
|
|
6214
|
+
return heading
|
|
6215
|
+
.toLowerCase()
|
|
6216
|
+
.replace(/[^\w\s-]/g, "")
|
|
6217
|
+
.trim()
|
|
6218
|
+
.replace(/\s+/g, "-");
|
|
6219
|
+
}
|
|
6220
|
+
// Split a doc into heading-anchored chunks. Each chunk carries the heading path
|
|
6221
|
+
// (e.g. "Setup > Install") plus the body beneath it, capped at DOCS_CHUNK_MAX_CHARS.
|
|
6222
|
+
function chunkDoc(docPath, text) {
|
|
6223
|
+
const lines = text.split(/\r?\n/);
|
|
6224
|
+
const chunks = [];
|
|
6225
|
+
const headingStack = [];
|
|
6226
|
+
let bodyLines = [];
|
|
6227
|
+
let chunkStartLine = 1;
|
|
6228
|
+
let inFence = false;
|
|
6229
|
+
const headingPath = () => headingStack.map((entry) => entry.title).join(" > ");
|
|
6230
|
+
const flush = (startLine) => {
|
|
6231
|
+
const body = bodyLines.join("\n").trim();
|
|
6232
|
+
bodyLines = [];
|
|
6233
|
+
if (!body)
|
|
6234
|
+
return;
|
|
6235
|
+
const heading = headingPath() || (0, node_path_1.basename)(docPath);
|
|
6236
|
+
for (let offset = 0; offset < body.length; offset += DOCS_CHUNK_MAX_CHARS) {
|
|
6237
|
+
const slice = body.slice(offset, offset + DOCS_CHUNK_MAX_CHARS);
|
|
6238
|
+
chunks.push({
|
|
6239
|
+
doc_path: docPath,
|
|
6240
|
+
heading,
|
|
6241
|
+
anchor: headingAnchor(headingStack[headingStack.length - 1]?.title ?? heading),
|
|
6242
|
+
text: slice,
|
|
6243
|
+
line: startLine,
|
|
6244
|
+
});
|
|
6245
|
+
}
|
|
6246
|
+
};
|
|
6247
|
+
lines.forEach((line, index) => {
|
|
6248
|
+
const fenceMatch = line.match(/^\s*(?:```|~~~)/);
|
|
6249
|
+
if (fenceMatch)
|
|
6250
|
+
inFence = !inFence;
|
|
6251
|
+
const headingMatch = inFence ? null : line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
|
|
6252
|
+
if (headingMatch) {
|
|
6253
|
+
flush(chunkStartLine);
|
|
6254
|
+
const level = headingMatch[1].length;
|
|
6255
|
+
const title = headingMatch[2].trim();
|
|
6256
|
+
while (headingStack.length && headingStack[headingStack.length - 1].level >= level)
|
|
6257
|
+
headingStack.pop();
|
|
6258
|
+
headingStack.push({ level, title });
|
|
6259
|
+
chunkStartLine = index + 1;
|
|
6260
|
+
return;
|
|
6261
|
+
}
|
|
6262
|
+
bodyLines.push(line);
|
|
6263
|
+
});
|
|
6264
|
+
flush(chunkStartLine);
|
|
6265
|
+
return chunks;
|
|
6266
|
+
}
|
|
6267
|
+
function buildDocsIndex(projectDir) {
|
|
6268
|
+
const docFiles = discoverDocFiles(projectDir);
|
|
6269
|
+
const chunks = [];
|
|
6270
|
+
for (const docPath of docFiles) {
|
|
6271
|
+
const text = safeReadText((0, node_path_1.join)(projectDir, docPath));
|
|
6272
|
+
if (!text)
|
|
6273
|
+
continue;
|
|
6274
|
+
chunks.push(...chunkDoc(docPath, text));
|
|
6275
|
+
}
|
|
6276
|
+
const artifact = {
|
|
6277
|
+
schema_version: DOCS_INDEX_SCHEMA_VERSION,
|
|
6278
|
+
generated_at: nowIso(),
|
|
6279
|
+
source: "repo-docs",
|
|
6280
|
+
doc_count: docFiles.length,
|
|
6281
|
+
chunk_count: chunks.length,
|
|
6282
|
+
chunks,
|
|
6283
|
+
};
|
|
6284
|
+
writeJson((0, node_path_1.join)(indexesDir(projectDir), "docs-index.json"), artifact);
|
|
6285
|
+
return artifact;
|
|
6286
|
+
}
|
|
6287
|
+
function readDocsIndex(projectDir) {
|
|
6288
|
+
const path = (0, node_path_1.join)(indexesDir(projectDir), "docs-index.json");
|
|
6289
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
6290
|
+
return null;
|
|
6291
|
+
try {
|
|
6292
|
+
const artifact = readJson(path);
|
|
6293
|
+
if (!Array.isArray(artifact?.chunks))
|
|
6294
|
+
return null;
|
|
6295
|
+
return artifact;
|
|
6296
|
+
}
|
|
6297
|
+
catch {
|
|
6298
|
+
return null;
|
|
6299
|
+
}
|
|
6300
|
+
}
|
|
6301
|
+
// BM25 over doc chunks, reusing the same lexical scorer (tokenize + stemming +
|
|
6302
|
+
// IDF) as packet recall. Heading text is weighted above body, mirroring how the
|
|
6303
|
+
// packet scorer weights titles.
|
|
6304
|
+
function scoreDocsBm25(queryTerms, chunks) {
|
|
6305
|
+
const terms = expandQueryTerms(queryTerms);
|
|
6306
|
+
const result = new Map();
|
|
6307
|
+
if (!terms.length || !chunks.length)
|
|
6308
|
+
return result;
|
|
6309
|
+
const HEADING_WEIGHT = 3;
|
|
6310
|
+
const documents = chunks.map((chunk) => {
|
|
6311
|
+
const termFrequency = new Map();
|
|
6312
|
+
let length = 0;
|
|
6313
|
+
const add = (textValue, weight) => {
|
|
6314
|
+
for (const token of tokenize(textValue)) {
|
|
6315
|
+
termFrequency.set(token, (termFrequency.get(token) ?? 0) + weight);
|
|
6316
|
+
length += weight;
|
|
6317
|
+
}
|
|
6318
|
+
};
|
|
6319
|
+
add(chunk.heading, HEADING_WEIGHT);
|
|
6320
|
+
add(chunk.text, 1);
|
|
6321
|
+
return { termFrequency, length: Math.max(1, length) };
|
|
6322
|
+
});
|
|
6323
|
+
const averageLength = documents.reduce((sum, document) => sum + document.length, 0) / documents.length || 1;
|
|
6324
|
+
const documentFrequency = new Map();
|
|
6325
|
+
for (const term of terms) {
|
|
6326
|
+
documentFrequency.set(term, documents.filter((document) => document.termFrequency.has(term)).length);
|
|
6327
|
+
}
|
|
6328
|
+
documents.forEach((document, index) => {
|
|
6329
|
+
let score = 0;
|
|
6330
|
+
for (const term of terms) {
|
|
6331
|
+
const termFrequency = document.termFrequency.get(term) ?? 0;
|
|
6332
|
+
if (termFrequency <= 0)
|
|
6333
|
+
continue;
|
|
6334
|
+
const df = documentFrequency.get(term) ?? 0;
|
|
6335
|
+
const idf = Math.log(1 + (documents.length - df + 0.5) / (df + 0.5));
|
|
6336
|
+
const denominator = termFrequency + BM25_K1 * (1 - BM25_B + BM25_B * (document.length / averageLength));
|
|
6337
|
+
score += idf * ((termFrequency * (BM25_K1 + 1)) / denominator);
|
|
6338
|
+
}
|
|
6339
|
+
if (score > 0)
|
|
6340
|
+
result.set(index, Number(score.toFixed(2)));
|
|
6341
|
+
});
|
|
6342
|
+
return result;
|
|
6343
|
+
}
|
|
6344
|
+
function docsSnippet(text) {
|
|
6345
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
6346
|
+
return collapsed.length > 200 ? `${collapsed.slice(0, 197)}...` : collapsed;
|
|
6347
|
+
}
|
|
6348
|
+
function searchDocs(projectDir, query, limit = 5) {
|
|
6349
|
+
// Use the persisted index when present; rebuild on the fly otherwise so the
|
|
6350
|
+
// search works even before the first refresh.
|
|
6351
|
+
const artifact = readDocsIndex(projectDir) ?? buildDocsIndex(projectDir);
|
|
6352
|
+
const chunks = artifact.chunks;
|
|
6353
|
+
const scores = scoreDocsBm25(tokenize(query), chunks);
|
|
6354
|
+
return [...scores.entries()]
|
|
6355
|
+
.sort((a, b) => b[1] - a[1] || chunks[a[0]].doc_path.localeCompare(chunks[b[0]].doc_path) || chunks[a[0]].line - chunks[b[0]].line)
|
|
6356
|
+
.slice(0, Math.max(0, limit))
|
|
6357
|
+
.map(([index, score]) => {
|
|
6358
|
+
const chunk = chunks[index];
|
|
6359
|
+
return {
|
|
6360
|
+
doc_path: chunk.doc_path,
|
|
6361
|
+
heading: chunk.heading,
|
|
6362
|
+
line: chunk.line,
|
|
6363
|
+
snippet: docsSnippet(chunk.text),
|
|
6364
|
+
score,
|
|
6365
|
+
};
|
|
6366
|
+
});
|
|
6367
|
+
}
|
|
6368
|
+
// Renders a "Docs" section appended to recall output. Honest framing: these are
|
|
6369
|
+
// the repo's own committed docs, not the internet.
|
|
6370
|
+
function docsRecallSection(projectDir, query, limit = 3) {
|
|
6371
|
+
const hits = searchDocs(projectDir, query, limit);
|
|
6372
|
+
if (!hits.length)
|
|
6373
|
+
return null;
|
|
6374
|
+
const lines = ["Docs (from this repo's own committed documentation):"];
|
|
6375
|
+
for (const hit of hits) {
|
|
6376
|
+
lines.push(`- ${hit.doc_path}:${hit.line} — ${hit.heading}`);
|
|
6377
|
+
lines.push(` ${hit.snippet}`);
|
|
6378
|
+
}
|
|
6379
|
+
return lines.join("\n");
|
|
6380
|
+
}
|
|
5987
6381
|
function buildPacketIndexes(projectDir) {
|
|
5988
6382
|
ensureMemoryDirs(projectDir);
|
|
5989
6383
|
const packets = loadPacketsFromDir(packetsDir(projectDir)).sort((a, b) => a.id.localeCompare(b.id));
|
|
@@ -6029,6 +6423,10 @@ function buildPacketIndexes(projectDir) {
|
|
|
6029
6423
|
writeJson(written[2], byTag);
|
|
6030
6424
|
writeJson(written[3], byType);
|
|
6031
6425
|
written.push(writeSparseVectorIndex(projectDir, packets));
|
|
6426
|
+
// Docs search index over the repo's own committed documentation. Built here so
|
|
6427
|
+
// it stays current through both indexProject and refreshProject.
|
|
6428
|
+
buildDocsIndex(projectDir);
|
|
6429
|
+
written.push((0, node_path_1.join)(indexesDir(projectDir), "docs-index.json"));
|
|
6032
6430
|
return written;
|
|
6033
6431
|
}
|
|
6034
6432
|
function readCurrentCodeGraph(projectDir, expectedInputHash) {
|
|
@@ -6130,6 +6528,7 @@ function currentOrBuildGraphs(projectDir) {
|
|
|
6130
6528
|
(0, node_path_1.join)(indexesDir(projectDir), "by-tag.json"),
|
|
6131
6529
|
(0, node_path_1.join)(indexesDir(projectDir), "by-type.json"),
|
|
6132
6530
|
(0, node_path_1.join)(indexesDir(projectDir), "vector-local.json"),
|
|
6531
|
+
(0, node_path_1.join)(indexesDir(projectDir), "docs-index.json"),
|
|
6133
6532
|
(0, node_path_1.join)(indexesDir(projectDir), "structural.json"),
|
|
6134
6533
|
(0, node_path_1.join)(indexesDir(projectDir), "graph.json"),
|
|
6135
6534
|
(0, node_path_1.join)(indexesDir(projectDir), "code-graph.json"),
|
|
@@ -7404,13 +7803,20 @@ function recallWithVectorScores(projectDir, query, limit = 5, explain = false, i
|
|
|
7404
7803
|
? [`## Structural Blast Radius (${structuralHops}-hop)`, ...blastRadius.map((path, index) => `${index + 1}. ${path}`), ""]
|
|
7405
7804
|
: []),
|
|
7406
7805
|
scored.length ? "## Relevant Memory" : "No relevant repo memory found.",
|
|
7407
|
-
...scored.flatMap((entry, index) =>
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7806
|
+
...scored.flatMap((entry, index) => {
|
|
7807
|
+
const contradicts = (entry.packet.quality ?? {}).contradicts;
|
|
7808
|
+
const contested = Array.isArray(contradicts) && contradicts.length > 0;
|
|
7809
|
+
return [
|
|
7810
|
+
"",
|
|
7811
|
+
`${index + 1}. [${entry.packet.type} | ${entry.packet.scope} | confidence ${entry.packet.confidence.toFixed(2)}] ${entry.packet.title}`,
|
|
7812
|
+
` Summary: ${entry.packet.summary}`,
|
|
7813
|
+
` Why matched: ${entry.why_matched.join(", ") || "text relevance"}`,
|
|
7814
|
+
` Source: ${sourceLabel(entry.packet)}`,
|
|
7815
|
+
...(contested
|
|
7816
|
+
? [` ⚠ Contested: this memory contradicts ${contradicts.length} other packet(s) (${contradicts.join(", ")}). Resolve with kage conflicts / kage supersede before relying on it.`]
|
|
7817
|
+
: []),
|
|
7818
|
+
];
|
|
7819
|
+
}),
|
|
7414
7820
|
"",
|
|
7415
7821
|
pendingScored.length ? "## Working Memory (Pending Review)" : "",
|
|
7416
7822
|
...pendingScored.flatMap((entry, index) => [
|
|
@@ -8089,9 +8495,15 @@ function kageRisk(projectDir, targets = [], changedFiles = []) {
|
|
|
8089
8495
|
const graph = readCurrentCodeGraph(projectDir) ?? buildCodeGraph(projectDir);
|
|
8090
8496
|
const graphPaths = new Set(graph.files.map((file) => file.path));
|
|
8091
8497
|
const dependents = codeDependents(graph);
|
|
8092
|
-
const
|
|
8498
|
+
const explicitTargets = targets.length > 0;
|
|
8499
|
+
const resolvedTargets = unique((explicitTargets ? targets : changedFiles.length ? changedFiles : gitChangedFiles(projectDir))
|
|
8093
8500
|
.map((path) => gitPathToProjectRelative(projectDir, path) ?? path)
|
|
8094
|
-
.filter((path) => path && !isNoisePath(path))
|
|
8501
|
+
.filter((path) => path && !isNoisePath(path))
|
|
8502
|
+
// Risk is a CODE assessment. When inferring targets from the working tree,
|
|
8503
|
+
// keep only files the code graph actually knows about — memory packets,
|
|
8504
|
+
// dotfiles, and docs have no dependents/hotspot signal and are pure noise
|
|
8505
|
+
// here. Explicitly-named targets are always honored.
|
|
8506
|
+
.filter((path) => explicitTargets || graphPaths.has(path)));
|
|
8095
8507
|
const warnings = [];
|
|
8096
8508
|
if (!gitHead(projectDir))
|
|
8097
8509
|
warnings.push("Git history is unavailable, so churn, ownership, and co-change signals may be empty.");
|
|
@@ -9204,9 +9616,15 @@ function renderClaudeMemAuditReceipt(report) {
|
|
|
9204
9616
|
function kageReviewerSuggestions(projectDir, targets = [], changedFiles = []) {
|
|
9205
9617
|
const graph = readCurrentCodeGraph(projectDir) ?? buildCodeGraph(projectDir);
|
|
9206
9618
|
const graphPaths = new Set(graph.files.map((file) => file.path));
|
|
9207
|
-
const
|
|
9619
|
+
const explicitTargets = targets.length > 0;
|
|
9620
|
+
const resolvedTargets = unique((explicitTargets ? targets : changedFiles.length ? changedFiles : gitChangedFiles(projectDir))
|
|
9208
9621
|
.map((path) => gitPathToProjectRelative(projectDir, path) ?? path)
|
|
9209
|
-
.filter((path) => path && !isNoisePath(path))
|
|
9622
|
+
.filter((path) => path && !isNoisePath(path))
|
|
9623
|
+
// Risk is a CODE assessment. When inferring targets from the working tree,
|
|
9624
|
+
// keep only files the code graph actually knows about — memory packets,
|
|
9625
|
+
// dotfiles, and docs have no dependents/hotspot signal and are pure noise
|
|
9626
|
+
// here. Explicitly-named targets are always honored.
|
|
9627
|
+
.filter((path) => explicitTargets || graphPaths.has(path)));
|
|
9210
9628
|
const warnings = [];
|
|
9211
9629
|
if (!gitHead(projectDir))
|
|
9212
9630
|
warnings.push("Git history is unavailable, so reviewer suggestions cannot be computed.");
|
|
@@ -12509,6 +12927,7 @@ function learn(input) {
|
|
|
12509
12927
|
strictCitations: input.strictCitations,
|
|
12510
12928
|
graphNodes: input.graphNodes,
|
|
12511
12929
|
pendingReview: input.pendingReview,
|
|
12930
|
+
strictContradictions: input.strictContradictions,
|
|
12512
12931
|
discoveryTokens: input.discoveryTokens,
|
|
12513
12932
|
});
|
|
12514
12933
|
}
|
|
@@ -12618,9 +13037,26 @@ function capture(input) {
|
|
|
12618
13037
|
const validation = validatePacket(packet);
|
|
12619
13038
|
if (!validation.ok)
|
|
12620
13039
|
return { ok: false, errors: validation.errors, warnings };
|
|
13040
|
+
// Memory-vs-memory contradiction detection: surface (and optionally refuse)
|
|
13041
|
+
// a capture that directly opposes an existing approved packet on a shared
|
|
13042
|
+
// path, instead of silently storing two conflicting facts.
|
|
13043
|
+
const contradictions = detectContradictions(input.projectDir, packet);
|
|
13044
|
+
if (contradictions.length && input.strictContradictions) {
|
|
13045
|
+
return {
|
|
13046
|
+
ok: false,
|
|
13047
|
+
errors: [
|
|
13048
|
+
`Contradiction blocked: this memory contradicts ${contradictions.length} existing approved ` +
|
|
13049
|
+
`packet(s): ${contradictions.map((c) => `${c.packet_id} (${c.title})`).join("; ")}. ` +
|
|
13050
|
+
`Resolve with kage supersede --packet <old> --replacement <new>, or drop --strict-contradictions to keep both.`,
|
|
13051
|
+
],
|
|
13052
|
+
warnings,
|
|
13053
|
+
contradictions,
|
|
13054
|
+
};
|
|
13055
|
+
}
|
|
12621
13056
|
packet.quality = {
|
|
12622
13057
|
...packet.quality,
|
|
12623
13058
|
...evaluateMemoryQuality(input.projectDir, packet),
|
|
13059
|
+
...(contradictions.length ? { contradicts: contradictions.map((c) => c.packet_id) } : {}),
|
|
12624
13060
|
};
|
|
12625
13061
|
const path = writePacket(input.projectDir, packet, input.pendingReview ? "pending" : "packets");
|
|
12626
13062
|
recordMemoryAudit(input.projectDir, "capture", [packet], {
|
|
@@ -12629,7 +13065,7 @@ function capture(input) {
|
|
|
12629
13065
|
path: (0, node_path_1.relative)(input.projectDir, path),
|
|
12630
13066
|
source_kind: packet.source_refs[0]?.kind ?? "explicit_capture",
|
|
12631
13067
|
});
|
|
12632
|
-
return { ok: true, packet, path, errors: [], warnings };
|
|
13068
|
+
return { ok: true, packet, path, errors: [], warnings, ...(contradictions.length ? { contradictions } : {}) };
|
|
12633
13069
|
}
|
|
12634
13070
|
function createPublicCandidate(projectDir, id) {
|
|
12635
13071
|
ensureMemoryDirs(projectDir);
|
|
@@ -13163,6 +13599,9 @@ exit 0
|
|
|
13163
13599
|
"roo-code": (0, node_path_1.join)(home, ".roo", "mcp_settings.json"),
|
|
13164
13600
|
"kilo-code": (0, node_path_1.join)(home, ".kilo", "mcp_settings.json"),
|
|
13165
13601
|
"claude-desktop": (0, node_path_1.join)(home, ".config", "claude", "claude_desktop_config.json"),
|
|
13602
|
+
openclaw: (0, node_path_1.join)(home, ".openclaw", "mcp.json"),
|
|
13603
|
+
copilot: (0, node_path_1.join)(home, ".config", "github-copilot", "mcp.json"),
|
|
13604
|
+
hermes: (0, node_path_1.join)(home, ".hermes", "mcp.json"),
|
|
13166
13605
|
"generic-mcp": "",
|
|
13167
13606
|
};
|
|
13168
13607
|
setSnippet(paths[agent] || null, universal, [`Merge this MCP stdio config into ${agent}'s MCP settings.`, "Restart the agent after updating config."]);
|
|
@@ -14693,6 +15132,12 @@ function prCheck(projectDir) {
|
|
|
14693
15132
|
if (reconciliation.unresolved_count > 0) {
|
|
14694
15133
|
warnings.push(`${reconciliation.unresolved_count} memory reconciliation item(s) may need update after recent code changes (review on handoff; not blocking).`);
|
|
14695
15134
|
}
|
|
15135
|
+
// Surface unresolved memory-vs-memory contradictions (not a hard fail —
|
|
15136
|
+
// resolving them is an agent/human decision via supersede or keep-both).
|
|
15137
|
+
const conflicts = kageConflicts(projectDir);
|
|
15138
|
+
if (conflicts.count > 0) {
|
|
15139
|
+
warnings.push(`${conflicts.count} memory contradiction pair(s) unresolved — run kage conflicts, then kage supersede the wrong one (not blocking).`);
|
|
15140
|
+
}
|
|
14696
15141
|
if (!codeGraphCurrent || !memoryGraphCurrent) {
|
|
14697
15142
|
errors.push("Generated graph artifacts are missing or not current for this working tree content.");
|
|
14698
15143
|
requiredActions.push("Run kage refresh --project <dir> before merge.");
|
|
@@ -15828,8 +16273,35 @@ function supersedeMemory(projectDir, oldPacketId, replacementPacketId, reason =
|
|
|
15828
16273
|
upsertPacketEdge(oldPacket, "superseded_by", replacementPacket.id, trimmedReason, at);
|
|
15829
16274
|
replacementPacket.updated_at = at;
|
|
15830
16275
|
upsertPacketEdge(replacementPacket, "supersedes", oldPacket.id, trimmedReason, at);
|
|
16276
|
+
// Superseding resolves any recorded contradiction involving the retired
|
|
16277
|
+
// packet: drop the old id from every other packet's quality.contradicts, and
|
|
16278
|
+
// clear the old packet's own contradicts list (it is no longer live memory).
|
|
16279
|
+
const clearContradiction = (packet) => {
|
|
16280
|
+
const quality = (packet.quality ?? {});
|
|
16281
|
+
const existing = Array.isArray(quality.contradicts) ? quality.contradicts : [];
|
|
16282
|
+
if (!existing.length)
|
|
16283
|
+
return false;
|
|
16284
|
+
const next = existing.filter((id) => id !== oldPacket.id && id !== replacementPacket.id);
|
|
16285
|
+
if (next.length === existing.length)
|
|
16286
|
+
return false;
|
|
16287
|
+
if (next.length)
|
|
16288
|
+
quality.contradicts = next;
|
|
16289
|
+
else
|
|
16290
|
+
delete quality.contradicts;
|
|
16291
|
+
packet.quality = quality;
|
|
16292
|
+
packet.updated_at = at;
|
|
16293
|
+
return true;
|
|
16294
|
+
};
|
|
16295
|
+
clearContradiction(oldPacket);
|
|
16296
|
+
clearContradiction(replacementPacket);
|
|
15831
16297
|
writeJson(oldEntry.path, oldPacket);
|
|
15832
16298
|
writeJson(replacementEntry.path, replacementPacket);
|
|
16299
|
+
for (const entry of entries) {
|
|
16300
|
+
if (entry.packet.id === oldPacket.id || entry.packet.id === replacementPacket.id)
|
|
16301
|
+
continue;
|
|
16302
|
+
if (clearContradiction(entry.packet))
|
|
16303
|
+
writeJson(entry.path, entry.packet);
|
|
16304
|
+
}
|
|
15833
16305
|
recordMemoryAudit(projectDir, "supersede", [oldPacket, replacementPacket], {
|
|
15834
16306
|
old_packet_id: oldPacket.id,
|
|
15835
16307
|
replacement_packet_id: replacementPacket.id,
|
|
@@ -16495,3 +16967,36 @@ function syncPersonal() {
|
|
|
16495
16967
|
result.ok = true;
|
|
16496
16968
|
return result;
|
|
16497
16969
|
}
|
|
16970
|
+
// Kage's memory is hierarchical by construction:
|
|
16971
|
+
// L0 raw — session observations captured by hooks (unreviewed signal)
|
|
16972
|
+
// L1 reviewed — verified memory packets (the trusted store)
|
|
16973
|
+
// L2 synthesis — generated overviews: repo maps, project profile, branch/change summaries
|
|
16974
|
+
// This surfaces that existing structure rather than inventing a field.
|
|
16975
|
+
function kageLayers(projectDir) {
|
|
16976
|
+
const L2_TYPES = new Set(["repo_map"]);
|
|
16977
|
+
const isSynthesis = (p) => {
|
|
16978
|
+
const q = (p.quality ?? {});
|
|
16979
|
+
return L2_TYPES.has(p.type) || q.candidate_kind === "change_memory";
|
|
16980
|
+
};
|
|
16981
|
+
const entries = (0, node_fs_1.existsSync)(packetsDir(projectDir)) ? loadPacketEntriesFromDir(packetsDir(projectDir)) : [];
|
|
16982
|
+
const l2 = entries.filter((e) => isSynthesis(e.packet));
|
|
16983
|
+
const l1 = entries.filter((e) => !isSynthesis(e.packet));
|
|
16984
|
+
let observationCount = 0;
|
|
16985
|
+
const obsDir = observationsDir(projectDir);
|
|
16986
|
+
if ((0, node_fs_1.existsSync)(obsDir)) {
|
|
16987
|
+
try {
|
|
16988
|
+
observationCount = (0, node_fs_1.readdirSync)(obsDir).filter((f) => f.endsWith(".jsonl") || f.endsWith(".json")).length;
|
|
16989
|
+
}
|
|
16990
|
+
catch {
|
|
16991
|
+
observationCount = 0;
|
|
16992
|
+
}
|
|
16993
|
+
}
|
|
16994
|
+
return {
|
|
16995
|
+
project_dir: projectDir,
|
|
16996
|
+
layers: [
|
|
16997
|
+
{ layer: "L0", label: "Raw observations", description: "Session signal captured by hooks; unreviewed, feeds auto-distill.", count: observationCount, examples: [] },
|
|
16998
|
+
{ layer: "L1", label: "Reviewed memory", description: "Verified packets — cited, fingerprinted, the trusted recall store.", count: l1.length, examples: l1.slice(0, 3).map((e) => e.packet.title) },
|
|
16999
|
+
{ layer: "L2", label: "Synthesis", description: "Generated overviews: repo maps and branch/change summaries.", count: l2.length, examples: l2.slice(0, 3).map((e) => e.packet.title) },
|
|
17000
|
+
],
|
|
17001
|
+
};
|
|
17002
|
+
}
|