linksee-memory 0.10.0 → 0.11.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/LICENSE +21 -21
- package/README.md +152 -68
- package/dist/bin/import-sessions.js +6 -6
- package/dist/bin/install-skill.js +14 -14
- package/dist/bin/map-import.d.ts +2 -0
- package/dist/bin/map-import.js +493 -0
- package/dist/bin/setup.js +14 -14
- package/dist/bin/stats.js +20 -20
- package/dist/db/migrate.js +36 -0
- package/dist/db/schema.sql +73 -2
- package/dist/lib/consolidate.js +19 -19
- package/dist/lib/drift-detection.js +38 -2
- package/dist/lib/edge-detection.js +8 -8
- package/dist/lib/guard.js +28 -28
- package/dist/lib/map-import.d.ts +55 -0
- package/dist/lib/map-import.js +150 -0
- package/dist/lib/map-reconcile.d.ts +29 -0
- package/dist/lib/map-reconcile.js +245 -0
- package/dist/lib/map-view.d.ts +103 -0
- package/dist/lib/map-view.js +201 -0
- package/dist/lib/momentum.js +7 -7
- package/dist/lib/truth-engine.js +11 -11
- package/dist/mcp/read-smart.js +8 -8
- package/dist/mcp/server.js +45 -0
- package/dist/skill/SKILL.md +734 -734
- package/package.json +9 -5
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// linksee-memory-map — load the Current Truth Map (map.yaml) into the runtime index
|
|
3
|
+
// and answer topology questions. map.yaml is the desired-state source of truth (anchor
|
|
4
|
+
// #58); this reconciles it into map_nodes/map_edges (full rebuild per project).
|
|
5
|
+
//
|
|
6
|
+
// Usage (CLI-first triage, not a list). The natural working flow:
|
|
7
|
+
// linksee-memory-map where [<file>] # WHERE am I? (no arg = infer from recent edits) → node + AFFECTS
|
|
8
|
+
// linksee-memory-map affects <node> # what to change together if you touch this node
|
|
9
|
+
// linksee-memory-map explain <node> # WHY this status + ✓/✗ evidence + FIX (the diagnosis)
|
|
10
|
+
// linksee-memory-map status # whole-project health % + what needs attention
|
|
11
|
+
// ── also ──
|
|
12
|
+
// linksee-memory-map next # the prioritized next fix candidate(s)
|
|
13
|
+
// linksee-memory-map reconcile # check the hand-written Map against real code/files
|
|
14
|
+
// linksee-memory-map inspect --json # machine-readable dump (CI / tooling)
|
|
15
|
+
// linksee-memory-map blueprint # stage×node board (colors reflect the live verdict)
|
|
16
|
+
// linksee-memory-map [--file map.yaml] [--root <repo>]
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { openDb, runMigrations } from '../db/migrate.js';
|
|
19
|
+
import { parseMapFile, importMap } from '../lib/map-import.js';
|
|
20
|
+
import { blastRadius, getSuspects, getBlueprint, getNode, getProjectMeta, whereAmI } from '../lib/map-view.js';
|
|
21
|
+
import { reconcile } from '../lib/map-reconcile.js';
|
|
22
|
+
function flagValue(argv, name, dflt) {
|
|
23
|
+
const i = argv.indexOf(`--${name}`);
|
|
24
|
+
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
|
|
25
|
+
}
|
|
26
|
+
const argv = process.argv.slice(2);
|
|
27
|
+
// positional args, skipping flags and their values (so `where --file x` doesn't read "--file" as the target)
|
|
28
|
+
const VALUE_FLAGS = new Set(['--file', '--root', '--limit', '--lang']);
|
|
29
|
+
const positionals = [];
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
if (argv[i].startsWith('--')) {
|
|
32
|
+
if (VALUE_FLAGS.has(argv[i]))
|
|
33
|
+
i++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
positionals.push(argv[i]);
|
|
37
|
+
}
|
|
38
|
+
const sub = positionals[0] ?? 'import';
|
|
39
|
+
const arg1 = positionals[1]; // the node-id / file / topic for explain|blast|affects|where
|
|
40
|
+
const mapPath = flagValue(argv, 'file', join(process.cwd(), 'map.yaml'));
|
|
41
|
+
const db = openDb();
|
|
42
|
+
runMigrations(db);
|
|
43
|
+
const map = parseMapFile(mapPath);
|
|
44
|
+
// Always (re)import first so reads reflect the file — map.yaml is authoritative.
|
|
45
|
+
const res = importMap(db, map);
|
|
46
|
+
const COLOR_DOT = { green: '🟢', red: '🔴', gray: '⚪', amber: '🟡', blue: '🔵' };
|
|
47
|
+
const repoRoot = flagValue(argv, 'root', process.cwd());
|
|
48
|
+
// ── i18n: English by default, Japanese with --lang ja (map.yaml content stays the user's language) ──
|
|
49
|
+
const lang = flagValue(argv, 'lang', 'en') === 'ja' ? 'ja' : 'en';
|
|
50
|
+
const EN = {
|
|
51
|
+
status: { active: 'healthy (active)', commitment: 'commitment (has a due date)', suspect: 'needs review (possible drift)', planned: 'planned', paused: 'paused', future_thesis: 'future thesis', experiment: 'experiment' },
|
|
52
|
+
affHard: 'must fix together', affSoft: 'should align', affWatch: 'fyi (may ripple)',
|
|
53
|
+
declared: 'declared:', reality: 'reality: ', verdict: 'verdict: ',
|
|
54
|
+
rConv: 'implemented / matches', rDiv: 'drifted', rAbs: 'not realized',
|
|
55
|
+
cRefuted: 'declared suspect, refuted by reality (→ convergence)', cDrift: 'declared vs reality disagree (drift)', cVerified: 'declared and reality agree (verified)', cAbsence: 'declared but not realized (absence)',
|
|
56
|
+
realityExt: 'awaiting external check (no auto-verify)', realityNone: 'no auto-check configured',
|
|
57
|
+
whyFallback: 'declared color (no auto-check configured)',
|
|
58
|
+
evExt: 'external state — verified by a human, not the scanner', evNone: 'no auto-check configured (declared)',
|
|
59
|
+
affectsHdr: 'AFFECTS (change these together — by strength)', affectsSub: 'changes ripple to:',
|
|
60
|
+
affectsCmd: (id, n) => `change ${id} → fix these together (${n}) — by strength:`,
|
|
61
|
+
whereAutoHdr: 'Inferred "you are here" from your recent edits:', whereAutoNone: 'Could not locate you on the Map from recent edits.',
|
|
62
|
+
whereOwns: (t) => `"${t}" belongs to this Map node:`, whereTopic: 'No file match → closest nodes by topic:',
|
|
63
|
+
whereNone: (t) => `No file or topic on the Map matched "${t}".`,
|
|
64
|
+
needs: 'Needs attention:', actionable: 'Actionable now (local, fixable):', external: 'External checks (verify outside the repo):',
|
|
65
|
+
verified: 'Verified by reality:', noAction: 'No action:', refutedTag: 'refuted suspect → convergence', verifiedTag: 'verified',
|
|
66
|
+
graveyard: 'Deferrals with no expiry/condition (graveyard risk):', graveyardHint: 'add review_by: <date> or revival_condition: <text> in map.yaml',
|
|
67
|
+
overdue: (d) => `Overdue deferrals (promised by a date, now past ${d}):`,
|
|
68
|
+
nLocal: 'Next local fix:', nExternal: 'Next external checks:', nNothing: 'Nothing needs attention — the Map matches reality.',
|
|
69
|
+
evidence: 'evidence:', next: 'next:', empty: '(empty — absence)', impl: 'implementation', notFound: 'node not found',
|
|
70
|
+
};
|
|
71
|
+
const JA = {
|
|
72
|
+
status: { active: '健全(稼働中)', commitment: '約束(締切あり)', suspect: '要確認(ズレの疑い)', planned: '計画', paused: '保留', future_thesis: '将来構想', experiment: '実験中' },
|
|
73
|
+
affHard: '必ず一緒に直す', affSoft: 'できれば揃える', affWatch: '参考(連鎖の可能性)',
|
|
74
|
+
declared: '宣言状態:', reality: '現実判定:', verdict: '結論: ',
|
|
75
|
+
rConv: '実装あり / 一致', rDiv: 'ズレあり', rAbs: '未実現',
|
|
76
|
+
cRefuted: '要確認は現実により反証 (refuted suspect → convergence)', cDrift: '宣言と現実がズレている (drift)', cVerified: '宣言と現実が一致 (verified)', cAbsence: '宣言が現実に未実現 (absence)',
|
|
77
|
+
realityExt: '外部確認待ち(自動確認なし)', realityNone: '自動チェック未設定',
|
|
78
|
+
whyFallback: '宣言ベース(現実の自動チェックは未設定)',
|
|
79
|
+
evExt: '外部状態のため自動確認なし(人が確認)', evNone: '自動チェック未設定(宣言ベース)',
|
|
80
|
+
affectsHdr: 'AFFECTS(変えたら一緒に直す先・強度順)', affectsSub: '変えたら一緒に直す先:',
|
|
81
|
+
affectsCmd: (id, n) => `${id} を変えたら一緒に直す先 (${n}) — 強度順:`,
|
|
82
|
+
whereAutoHdr: '直近の編集から推定した「今いる場所」:', whereAutoNone: '直近の編集からMap上の位置を特定できませんでした。',
|
|
83
|
+
whereOwns: (t) => `"${t}" は Map 上のこのノードに属します:`, whereTopic: 'ファイル一致なし → トピックとして近いノード:',
|
|
84
|
+
whereNone: (t) => `Map上で "${t}" に該当するファイル/トピックは見つかりませんでした。`,
|
|
85
|
+
needs: '要対応:', actionable: '今すぐ直せる(ローカル):', external: '外部チェック(リポジトリ外で確認):',
|
|
86
|
+
verified: '現実で検証済み:', noAction: '対応不要:', refutedTag: '要確認→現実で反証 (convergence)', verifiedTag: '検証済み',
|
|
87
|
+
graveyard: '期限/条件のない保留(墓場リスク):', graveyardHint: 'map.yaml に review_by: <日付> か revival_condition: <条件> を追加',
|
|
88
|
+
overdue: (d) => `期限切れの保留(${d} を過ぎている):`,
|
|
89
|
+
nLocal: '次に直す(ローカル):', nExternal: '次の外部チェック:', nNothing: '対応事項なし — Mapと現実が一致。',
|
|
90
|
+
evidence: 'evidence:', next: 'next:', empty: '(empty — absence)', impl: 'implementation', notFound: 'node not found',
|
|
91
|
+
};
|
|
92
|
+
const T = lang === 'ja' ? JA : EN;
|
|
93
|
+
// Triage/diagnosis commands reflect REALITY, so reconcile first (scans repoRoot).
|
|
94
|
+
const DIAG_SUBS = new Set(['status', 'explain', 'next', 'inspect', 'where', 'blueprint']);
|
|
95
|
+
if (DIAG_SUBS.has(sub))
|
|
96
|
+
reconcile(db, map.project, repoRoot);
|
|
97
|
+
function diagOf(id) {
|
|
98
|
+
const node = getNode(db, map.project, id);
|
|
99
|
+
if (!node)
|
|
100
|
+
return null;
|
|
101
|
+
let ev = {};
|
|
102
|
+
try {
|
|
103
|
+
ev = JSON.parse(node.verdict_evidence || '{}');
|
|
104
|
+
}
|
|
105
|
+
catch { /* none */ }
|
|
106
|
+
return { node, ev };
|
|
107
|
+
}
|
|
108
|
+
// A node "needs attention" if reality flags it (divergence/absence) or it's a hand-declared
|
|
109
|
+
// suspect reality hasn't cleared. A suspect REFUTED by reality (→convergence) does NOT.
|
|
110
|
+
function needsAttention(n) {
|
|
111
|
+
if (n.live_verdict === 'divergence' || n.live_verdict === 'absence')
|
|
112
|
+
return true;
|
|
113
|
+
if (n.status === 'suspect' && n.live_verdict !== 'convergence')
|
|
114
|
+
return true;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const allNodes = () => db.prepare('SELECT * FROM map_nodes WHERE project = ?').all(map.project);
|
|
118
|
+
const parseJson = (s, dflt) => { try {
|
|
119
|
+
return JSON.parse(s || '');
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return dflt;
|
|
123
|
+
} };
|
|
124
|
+
const truncate = (s, n) => (s && s.length > n ? s.slice(0, n) + '…' : s ?? '');
|
|
125
|
+
const shortPath = (p) => (p ? p.split(/[\\/]/).slice(-2).join('/') : '');
|
|
126
|
+
// External = verified outside the repo (network/human); not fixable in code right now.
|
|
127
|
+
const isExternal = (n) => parseJson(n.reality, {}).kind === 'external';
|
|
128
|
+
const TODAY = new Date().toISOString().slice(0, 10);
|
|
129
|
+
// Accounted-for = parked with a reason (paused / external / has a revival plan).
|
|
130
|
+
const isAccountedFor = (n) => n.status === 'paused' || isExternal(n) || !!n.revival_condition || !!n.review_by;
|
|
131
|
+
const isOverdue = (n) => !!n.review_by && n.review_by < TODAY; // promised by a date, now past it
|
|
132
|
+
const noExpiry = (n) => isAccountedFor(n) && !n.review_by && !n.revival_condition; // graveyard risk
|
|
133
|
+
const facetsOf = (n) => parseJson(n.facets, []);
|
|
134
|
+
// A short tag when reality's verdict differs from / confirms the declared status.
|
|
135
|
+
const verdictMark = (n) => {
|
|
136
|
+
if (!n.live_verdict)
|
|
137
|
+
return '';
|
|
138
|
+
if (n.status === 'suspect' && n.live_verdict === 'convergence')
|
|
139
|
+
return ' ✓ declared suspect, reality verified';
|
|
140
|
+
if (n.live_verdict === 'divergence')
|
|
141
|
+
return ' ✗ reality drift';
|
|
142
|
+
if (n.live_verdict === 'absence')
|
|
143
|
+
return ' ⚪ declared but absent';
|
|
144
|
+
if (n.live_verdict === 'convergence')
|
|
145
|
+
return ' ✓ reality verified';
|
|
146
|
+
return '';
|
|
147
|
+
};
|
|
148
|
+
// AFFECTS grouped by edge strength so a big blast radius isn't flat noise.
|
|
149
|
+
function printAffects(id, indent = ' ') {
|
|
150
|
+
const blast = blastRadius(db, map.project, id);
|
|
151
|
+
if (!blast.length) {
|
|
152
|
+
console.log(`${indent}(none)`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const groups = [['hard', T.affHard], ['soft', T.affSoft], ['watch', T.affWatch]];
|
|
156
|
+
for (const [s, label] of groups) {
|
|
157
|
+
const hits = blast.filter((b) => b.strength === s);
|
|
158
|
+
if (!hits.length)
|
|
159
|
+
continue;
|
|
160
|
+
console.log(`${indent}${label} (${s}):`);
|
|
161
|
+
for (const b of hits)
|
|
162
|
+
console.log(`${indent} ${b.id} (${b.relation})`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const declaredJP = (s) => T.status[s] ?? s;
|
|
166
|
+
function printSuspectsWithBlast() {
|
|
167
|
+
const suspects = getSuspects(db, map.project);
|
|
168
|
+
if (suspects.length === 0) {
|
|
169
|
+
console.log('\nNo suspect nodes. (Map declares all surfaces converged.)');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
console.log(`\n🔴 ${suspects.length} suspect node(s) — out-of-band drift candidates:`);
|
|
173
|
+
for (const s of suspects) {
|
|
174
|
+
console.log(`\n ${s.id} — ${s.statement}`);
|
|
175
|
+
if (s.note)
|
|
176
|
+
console.log(` note: ${s.note}`);
|
|
177
|
+
const blast = blastRadius(db, map.project, s.id);
|
|
178
|
+
if (blast.length === 0) {
|
|
179
|
+
console.log(' blast radius: (none — isolated node)');
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
console.log(` blast radius (${blast.length}): touching this implicates —`);
|
|
183
|
+
for (const b of blast)
|
|
184
|
+
console.log(` • ${b.id} [${b.status}] via ${b.relation}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (sub === 'import') {
|
|
188
|
+
console.log(`[map] imported ${map.project} from ${mapPath}`);
|
|
189
|
+
console.log(` ${res.nodes} nodes · ${res.edges} edges · ${res.linked_anchors} anchor link(s)`);
|
|
190
|
+
if (res.warnings.length) {
|
|
191
|
+
console.log(` ⚠ ${res.warnings.length} warning(s):`);
|
|
192
|
+
for (const w of res.warnings)
|
|
193
|
+
console.log(` - ${w}`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.log(' ✓ no topology warnings');
|
|
197
|
+
}
|
|
198
|
+
printSuspectsWithBlast();
|
|
199
|
+
}
|
|
200
|
+
else if (sub === 'suspects') {
|
|
201
|
+
printSuspectsWithBlast();
|
|
202
|
+
}
|
|
203
|
+
else if (sub === 'reconcile') {
|
|
204
|
+
// Check declared intent against actual reality (local code/files). Scans process.cwd().
|
|
205
|
+
const flagRoot = flagValue(argv, 'root', process.cwd());
|
|
206
|
+
const res = reconcile(db, map.project, flagRoot);
|
|
207
|
+
console.log(`[reconcile] ${map.project} — ${res.checked} checked · ${res.external} external (human-confirmed)`);
|
|
208
|
+
if (res.refuted.length) {
|
|
209
|
+
console.log(`\n✓ ${res.refuted.length} suspect(s) REFUTED by reality (declared suspect → reality says convergence):`);
|
|
210
|
+
for (const v of res.refuted)
|
|
211
|
+
console.log(` 🟢 ${v.id} — ${v.reason}\n ${v.evidence.file ?? ''}${v.evidence.line_no ? ':' + v.evidence.line_no : ''}`);
|
|
212
|
+
}
|
|
213
|
+
if (res.confirmed.length) {
|
|
214
|
+
console.log(`\n🔴 ${res.confirmed.length} suspect(s) CONFIRMED by reality:`);
|
|
215
|
+
for (const v of res.confirmed)
|
|
216
|
+
console.log(` 🔴 ${v.id} — ${v.reason}`);
|
|
217
|
+
}
|
|
218
|
+
const newDiv = res.verdicts.filter((v) => v.flipped && v.verdict === 'divergence');
|
|
219
|
+
if (newDiv.length) {
|
|
220
|
+
console.log(`\n⚠ ${newDiv.length} node(s) reality flags as divergence (declared OK, reality disagrees):`);
|
|
221
|
+
for (const v of newDiv)
|
|
222
|
+
console.log(` 🔴 ${v.id} — ${v.reason}`);
|
|
223
|
+
}
|
|
224
|
+
console.log(`\nVerdicts: ${res.verdicts.filter(v => v.verdict === 'convergence').length} convergence · ${res.verdicts.filter(v => v.verdict === 'divergence').length} divergence · ${res.verdicts.filter(v => v.verdict === 'absence').length} absence · ${res.external} external`);
|
|
225
|
+
}
|
|
226
|
+
else if (sub === 'blast') {
|
|
227
|
+
const id = arg1;
|
|
228
|
+
if (!id) {
|
|
229
|
+
console.error('usage: linksee-memory-map blast <node-id>');
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
const blast = blastRadius(db, map.project, id);
|
|
233
|
+
console.log(`blast radius for ${id} (${blast.length}):`);
|
|
234
|
+
for (const b of blast)
|
|
235
|
+
console.log(` • ${b.id} [${b.status}] via ${b.relation}\n ${b.statement}`);
|
|
236
|
+
}
|
|
237
|
+
else if (sub === 'blueprint') {
|
|
238
|
+
// reconcile ran (DIAG_SUBS) → colors reflect the live verdict, not just the declared status.
|
|
239
|
+
const bp = getBlueprint(db, map.project, map.stages);
|
|
240
|
+
const all = allNodes();
|
|
241
|
+
const verified = all.filter((n) => n.live_verdict === 'convergence').length;
|
|
242
|
+
const attention = all.filter(needsAttention).length;
|
|
243
|
+
console.log(`Blueprint — ${bp.project}`);
|
|
244
|
+
console.log(`declared: ${Object.entries(bp.counts).map(([k, v]) => `${k}=${v}`).join(' ')}`);
|
|
245
|
+
console.log(`reality: verified=${verified} needs-attention=${attention}`);
|
|
246
|
+
const line = (n) => ` ${COLOR_DOT[n.color]} ${n.id} — ${n.statement}${verdictMark(n)}`;
|
|
247
|
+
for (const cell of bp.stages) {
|
|
248
|
+
console.log(`\n▌${cell.label} (${cell.stage})`);
|
|
249
|
+
if (cell.nodes.length === 0) {
|
|
250
|
+
console.log(` ${T.empty}`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
for (const n of cell.nodes)
|
|
254
|
+
console.log(line(n));
|
|
255
|
+
}
|
|
256
|
+
console.log(`\n▌${T.impl}`);
|
|
257
|
+
for (const n of bp.implementation)
|
|
258
|
+
console.log(line(n));
|
|
259
|
+
}
|
|
260
|
+
else if (sub === 'status') {
|
|
261
|
+
// Triage, not a list. Local-actionable first (fixable now, has evidence), then external.
|
|
262
|
+
const nodes = allNodes();
|
|
263
|
+
const attention = nodes.filter(needsAttention);
|
|
264
|
+
const local = attention.filter((n) => !isExternal(n));
|
|
265
|
+
const external = attention.filter(isExternal);
|
|
266
|
+
const verified = nodes.filter((n) => n.live_verdict === 'convergence');
|
|
267
|
+
const health = nodes.length ? Math.round(100 * (nodes.length - attention.length) / nodes.length) : 100;
|
|
268
|
+
console.log(`Product: ${map.project}`);
|
|
269
|
+
console.log(`Health: ${health}%`);
|
|
270
|
+
console.log(`\n${T.needs} ${attention.length}`);
|
|
271
|
+
if (local.length) {
|
|
272
|
+
console.log(`\n ${T.actionable}`);
|
|
273
|
+
local.forEach((n, i) => {
|
|
274
|
+
const ev = parseJson(n.verdict_evidence, {});
|
|
275
|
+
const failed = (ev.checks ?? []).find((c) => !c.ok);
|
|
276
|
+
const evidence = failed ? `${shortPath(failed.file)}${failed.line ? ':' + failed.line : ''} (${failed.detail})` : '—';
|
|
277
|
+
console.log(` ${i + 1}. ${n.id} ${n.live_verdict ?? n.status}`);
|
|
278
|
+
console.log(` ${truncate(ev.why || n.note || n.statement, 88)}`);
|
|
279
|
+
console.log(` ${T.evidence} ${evidence}`);
|
|
280
|
+
console.log(` ${T.next} linksee-memory-map explain ${n.id}`);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (external.length) {
|
|
284
|
+
console.log(`\n ${T.external}`);
|
|
285
|
+
external.forEach((n, i) => {
|
|
286
|
+
const ev = parseJson(n.verdict_evidence, {});
|
|
287
|
+
console.log(` ${local.length + i + 1}. ${n.id} ${n.status}`);
|
|
288
|
+
console.log(` ${truncate(ev.why || n.note || n.statement, 88)}`);
|
|
289
|
+
if (ev.expected)
|
|
290
|
+
console.log(` expected: ${ev.expected}`);
|
|
291
|
+
if (ev.check)
|
|
292
|
+
console.log(` check: ${ev.check}`);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// accounted-for can't become a drift graveyard: overdue deferrals re-escalate, missing expiries are flagged.
|
|
296
|
+
const overdue = nodes.filter(isOverdue);
|
|
297
|
+
if (overdue.length) {
|
|
298
|
+
console.log(`\n⏰ ${T.overdue(TODAY)}`);
|
|
299
|
+
for (const n of overdue)
|
|
300
|
+
console.log(` ${n.id} review_by ${n.review_by}${n.revival_condition ? ' — ' + n.revival_condition : ''}`);
|
|
301
|
+
}
|
|
302
|
+
const graveyard = nodes.filter(noExpiry);
|
|
303
|
+
if (graveyard.length) {
|
|
304
|
+
console.log(`\n⚠ ${T.graveyard} ${graveyard.map((n) => n.id).join(', ')}`);
|
|
305
|
+
console.log(` → ${T.graveyardHint}`);
|
|
306
|
+
}
|
|
307
|
+
console.log(`\n${T.verified} ${verified.length}`);
|
|
308
|
+
for (const n of verified)
|
|
309
|
+
console.log(` ${n.id.padEnd(20)} ${n.status === 'suspect' ? T.refutedTag : T.verifiedTag}`);
|
|
310
|
+
console.log(`\n${T.noAction} ${nodes.length - attention.length - verified.length}`);
|
|
311
|
+
}
|
|
312
|
+
else if (sub === 'explain') {
|
|
313
|
+
const id = arg1;
|
|
314
|
+
if (!id) {
|
|
315
|
+
console.error('usage: linksee-memory-map explain <node>');
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
const d = diagOf(id);
|
|
319
|
+
if (!d) {
|
|
320
|
+
console.error(`node not found: ${id}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
const { node, ev } = d;
|
|
324
|
+
const meta = getProjectMeta(db, map.project);
|
|
325
|
+
const stageLabel = node.stage ? (meta?.stages.find((s) => s.id === node.stage)?.label ?? node.stage) : null;
|
|
326
|
+
const v = node.live_verdict;
|
|
327
|
+
const isExt = parseJson(node.reality, {}).kind === 'external';
|
|
328
|
+
const facets = facetsOf(node);
|
|
329
|
+
console.log(`${node.id}${stageLabel ? ` [${stageLabel}]` : ''}${facets.length ? ` {${facets.join(', ')}}` : ''}`);
|
|
330
|
+
console.log(node.statement);
|
|
331
|
+
// declared state and reality verdict are DIFFERENT things — show them separately.
|
|
332
|
+
console.log('\nSTATUS');
|
|
333
|
+
console.log(` ${T.declared} ${declaredJP(node.status)}`);
|
|
334
|
+
if (v) {
|
|
335
|
+
const realityV = v === 'convergence' ? T.rConv : v === 'divergence' ? T.rDiv : T.rAbs;
|
|
336
|
+
const concl = node.status === 'suspect' && v === 'convergence' ? T.cRefuted
|
|
337
|
+
: v === 'divergence' ? T.cDrift
|
|
338
|
+
: v === 'convergence' ? T.cVerified
|
|
339
|
+
: T.cAbsence;
|
|
340
|
+
console.log(` ${T.reality} ${realityV}`);
|
|
341
|
+
console.log(` ${T.verdict} ${concl}`);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
console.log(` ${T.reality} ${isExt ? T.realityExt : T.realityNone}`);
|
|
345
|
+
}
|
|
346
|
+
console.log(`\nWHY\n ${ev.why || node.note || T.whyFallback}`);
|
|
347
|
+
console.log('\nEVIDENCE');
|
|
348
|
+
if (ev.checks && ev.checks.length) {
|
|
349
|
+
for (const c of ev.checks)
|
|
350
|
+
console.log(` ${c.ok ? '✓' : '✗'} ${c.claim}\n ${shortPath(c.file)}${c.line ? ':' + c.line : ''} — ${c.detail}`);
|
|
351
|
+
}
|
|
352
|
+
else if (isExt) {
|
|
353
|
+
console.log(` ${T.evExt}`);
|
|
354
|
+
if (ev.expected)
|
|
355
|
+
console.log(` expected: ${ev.expected}`);
|
|
356
|
+
if (ev.check)
|
|
357
|
+
console.log(` check: ${ev.check}`);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
console.log(` ${T.evNone}`);
|
|
361
|
+
}
|
|
362
|
+
if (ev.fix && ev.fix.length) {
|
|
363
|
+
console.log('\nFIX');
|
|
364
|
+
ev.fix.forEach((f, i) => console.log(` ${i + 1}. ${f}`));
|
|
365
|
+
}
|
|
366
|
+
if (node.review_by || node.revival_condition) {
|
|
367
|
+
console.log('\nDEFERRED UNTIL');
|
|
368
|
+
if (node.review_by)
|
|
369
|
+
console.log(` review_by: ${node.review_by}${isOverdue(node) ? ' ⏰ OVERDUE' : ''}`);
|
|
370
|
+
if (node.revival_condition)
|
|
371
|
+
console.log(` condition: ${node.revival_condition}`);
|
|
372
|
+
}
|
|
373
|
+
console.log(`\n${T.affectsHdr}`);
|
|
374
|
+
printAffects(id);
|
|
375
|
+
console.log(`\nNEXT\n linksee-memory-map reconcile # re-check after a fix\n linksee-memory-map affects ${id}`);
|
|
376
|
+
}
|
|
377
|
+
else if (sub === 'affects') {
|
|
378
|
+
const id = arg1;
|
|
379
|
+
if (!id) {
|
|
380
|
+
console.error('usage: linksee-memory-map affects <node>');
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
const blast = blastRadius(db, map.project, id);
|
|
384
|
+
console.log(T.affectsCmd(id, blast.length));
|
|
385
|
+
printAffects(id);
|
|
386
|
+
}
|
|
387
|
+
else if (sub === 'where') {
|
|
388
|
+
// "I'm about to touch this file — where is it on the Map, and what does it touch?"
|
|
389
|
+
const target = arg1;
|
|
390
|
+
if (!target) {
|
|
391
|
+
// no arg → auto-locate from what you just edited this session
|
|
392
|
+
const res = whereAmI(db, { project: map.project });
|
|
393
|
+
if (!res.matched.length) {
|
|
394
|
+
console.log(T.whereAutoNone);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
console.log(T.whereAutoHdr);
|
|
398
|
+
for (const m of res.matched) {
|
|
399
|
+
console.log(`\n ${m.node.id}${m.stage_label ? ` [${m.stage_label}]` : ''} ${m.node.live_verdict ?? m.node.status} (${m.match_reason})`);
|
|
400
|
+
console.log(` ${T.affectsSub}`);
|
|
401
|
+
printAffects(m.node.id, ' ');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
db.close();
|
|
405
|
+
process.exit(0);
|
|
406
|
+
}
|
|
407
|
+
const norm = (p) => p.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
408
|
+
const t = norm(target);
|
|
409
|
+
const meta = getProjectMeta(db, map.project);
|
|
410
|
+
const stageLabel = (s) => (s ? meta?.stages.find((x) => x.id === s)?.label ?? s : null);
|
|
411
|
+
// Ownership = a node whose reality names this FILE explicitly (check.path / reality.path).
|
|
412
|
+
// `dir:` is a scan SCOPE (e.g. all of src), not ownership — so it does NOT match here.
|
|
413
|
+
const matched = allNodes().filter((n) => {
|
|
414
|
+
const r = parseJson(n.reality, {});
|
|
415
|
+
const paths = [];
|
|
416
|
+
if (r.path)
|
|
417
|
+
paths.push(norm(r.path));
|
|
418
|
+
for (const c of (r.checks ?? []))
|
|
419
|
+
if (c.path)
|
|
420
|
+
paths.push(norm(c.path));
|
|
421
|
+
return paths.some((p) => t === p);
|
|
422
|
+
});
|
|
423
|
+
if (matched.length) {
|
|
424
|
+
console.log(T.whereOwns(target));
|
|
425
|
+
for (const n of matched) {
|
|
426
|
+
console.log(`\n ${n.id}${n.stage ? ` [${stageLabel(n.stage)}]` : ''} ${n.live_verdict ?? n.status}`);
|
|
427
|
+
console.log(` ${n.statement}`);
|
|
428
|
+
console.log(` ${T.affectsSub}`);
|
|
429
|
+
printAffects(n.id, ' ');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
// no file match → treat the arg as a topic (lexical locate, like where_am_i)
|
|
434
|
+
const res = whereAmI(db, { project: map.project, query: target });
|
|
435
|
+
if (!res.matched.length) {
|
|
436
|
+
console.log(T.whereNone(target));
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
console.log(T.whereTopic);
|
|
440
|
+
for (const m of res.matched)
|
|
441
|
+
console.log(` ${m.node.id}${m.stage_label ? ` [${m.stage_label}]` : ''} (${m.match_reason})`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else if (sub === 'next') {
|
|
446
|
+
// local-first: what you can fix in code now, then what to verify externally.
|
|
447
|
+
const attention = allNodes().filter(needsAttention);
|
|
448
|
+
const local = attention.filter((n) => !isExternal(n))
|
|
449
|
+
.sort((a, b) => blastRadius(db, map.project, b.id).length - blastRadius(db, map.project, a.id).length);
|
|
450
|
+
const external = attention.filter(isExternal);
|
|
451
|
+
if (!attention.length) {
|
|
452
|
+
console.log(`✓ ${T.nNothing}`);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
if (local.length) {
|
|
456
|
+
console.log(T.nLocal);
|
|
457
|
+
local.slice(0, 3).forEach((n, i) => {
|
|
458
|
+
const ev = parseJson(n.verdict_evidence, {});
|
|
459
|
+
console.log(` ${i + 1}. ${n.id} — ${truncate(ev.why || n.note || n.statement, 88)}`);
|
|
460
|
+
console.log(` → linksee-memory-map explain ${n.id}`);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
if (external.length) {
|
|
464
|
+
console.log(`${local.length ? '\n' : ''}${T.nExternal}`);
|
|
465
|
+
external.forEach((n) => {
|
|
466
|
+
const ev = parseJson(n.verdict_evidence, {});
|
|
467
|
+
console.log(` • ${n.id}${ev.check ? ` — ${ev.check}` : ''}`);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else if (sub === 'inspect') {
|
|
473
|
+
const nodes = allNodes();
|
|
474
|
+
const out = {
|
|
475
|
+
project: map.project,
|
|
476
|
+
health: nodes.length ? Math.round(100 * (nodes.length - nodes.filter(needsAttention).length) / nodes.length) : 100,
|
|
477
|
+
nodes: nodes.map((n) => {
|
|
478
|
+
const ev = parseJson(n.verdict_evidence, {});
|
|
479
|
+
return {
|
|
480
|
+
id: n.id, stage: n.stage, layer: n.layer, status: n.status, live_verdict: n.live_verdict,
|
|
481
|
+
why: ev.why ?? null, checks: ev.checks ?? [], fix: ev.fix ?? [],
|
|
482
|
+
affects: blastRadius(db, map.project, n.id).map((b) => b.id), needs_attention: needsAttention(n),
|
|
483
|
+
};
|
|
484
|
+
}),
|
|
485
|
+
};
|
|
486
|
+
console.log(JSON.stringify(out, null, 2));
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
console.error(`unknown subcommand: ${sub}`);
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
db.close();
|
|
493
|
+
//# sourceMappingURL=map-import.js.map
|
package/dist/bin/setup.js
CHANGED
|
@@ -32,20 +32,20 @@ const dryRun = args.includes('--dry-run');
|
|
|
32
32
|
const autoYes = args.includes('--yes') || args.includes('-y');
|
|
33
33
|
const showHelp = args.includes('--help') || args.includes('-h');
|
|
34
34
|
if (showHelp) {
|
|
35
|
-
console.log(`linksee-memory-setup — One-command setup for Linksee Memory
|
|
36
|
-
|
|
37
|
-
Usage:
|
|
38
|
-
npx linksee-memory-setup Interactive setup
|
|
39
|
-
npx linksee-memory-setup --yes Accept all defaults, no prompts
|
|
40
|
-
npx linksee-memory-setup --dry-run Show what would happen
|
|
41
|
-
|
|
42
|
-
What it does:
|
|
43
|
-
1. Registers linksee-memory MCP server with Claude Code
|
|
44
|
-
2. Installs SKILL.md (teaches the agent when to recall/remember)
|
|
45
|
-
3. Configures Stop hook (auto-captures every session)
|
|
46
|
-
4. Offers to wire the re-injection guard into THIS project's .claude/settings.json
|
|
47
|
-
|
|
48
|
-
After setup, just chat with Claude Code normally.
|
|
35
|
+
console.log(`linksee-memory-setup — One-command setup for Linksee Memory
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
npx linksee-memory-setup Interactive setup
|
|
39
|
+
npx linksee-memory-setup --yes Accept all defaults, no prompts
|
|
40
|
+
npx linksee-memory-setup --dry-run Show what would happen
|
|
41
|
+
|
|
42
|
+
What it does:
|
|
43
|
+
1. Registers linksee-memory MCP server with Claude Code
|
|
44
|
+
2. Installs SKILL.md (teaches the agent when to recall/remember)
|
|
45
|
+
3. Configures Stop hook (auto-captures every session)
|
|
46
|
+
4. Offers to wire the re-injection guard into THIS project's .claude/settings.json
|
|
47
|
+
|
|
48
|
+
After setup, just chat with Claude Code normally.
|
|
49
49
|
Add "Use Linksee" to any prompt to trigger memory recall.`);
|
|
50
50
|
process.exit(0);
|
|
51
51
|
}
|
package/dist/bin/stats.js
CHANGED
|
@@ -53,11 +53,11 @@ function humanAge(unix) {
|
|
|
53
53
|
function main() {
|
|
54
54
|
const args = parseArgs();
|
|
55
55
|
if (args.help) {
|
|
56
|
-
console.log(`linksee-memory-stats — summary of the local memory DB
|
|
57
|
-
|
|
58
|
-
--json Output machine-readable JSON
|
|
59
|
-
--per-entity N Show top N entities (default 5, 0 to skip)
|
|
60
|
-
-h, --help This message
|
|
56
|
+
console.log(`linksee-memory-stats — summary of the local memory DB
|
|
57
|
+
|
|
58
|
+
--json Output machine-readable JSON
|
|
59
|
+
--per-entity N Show top N entities (default 5, 0 to skip)
|
|
60
|
+
-h, --help This message
|
|
61
61
|
`);
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
@@ -88,23 +88,23 @@ function main() {
|
|
|
88
88
|
const oldest = db.prepare('SELECT MIN(created_at) as t FROM memories').get().t;
|
|
89
89
|
const newest = db.prepare('SELECT MAX(created_at) as t FROM memories').get().t;
|
|
90
90
|
const topEntities = args.perEntity > 0
|
|
91
|
-
? db.prepare(`
|
|
92
|
-
SELECT e.name, e.kind, e.momentum_score, COUNT(m.id) as memory_count,
|
|
93
|
-
MAX(m.last_accessed_at) as last_access
|
|
94
|
-
FROM entities e
|
|
95
|
-
LEFT JOIN memories m ON m.entity_id = e.id
|
|
96
|
-
GROUP BY e.id
|
|
97
|
-
ORDER BY memory_count DESC, e.momentum_score DESC
|
|
98
|
-
LIMIT ?
|
|
91
|
+
? db.prepare(`
|
|
92
|
+
SELECT e.name, e.kind, e.momentum_score, COUNT(m.id) as memory_count,
|
|
93
|
+
MAX(m.last_accessed_at) as last_access
|
|
94
|
+
FROM entities e
|
|
95
|
+
LEFT JOIN memories m ON m.entity_id = e.id
|
|
96
|
+
GROUP BY e.id
|
|
97
|
+
ORDER BY memory_count DESC, e.momentum_score DESC
|
|
98
|
+
LIMIT ?
|
|
99
99
|
`).all(args.perEntity)
|
|
100
100
|
: [];
|
|
101
|
-
const topFiles = db.prepare(`
|
|
102
|
-
SELECT file_path, COUNT(*) as edits, COUNT(DISTINCT session_id) as in_sessions
|
|
103
|
-
FROM session_file_edits
|
|
104
|
-
WHERE operation IN ('edit', 'write')
|
|
105
|
-
GROUP BY file_path
|
|
106
|
-
ORDER BY edits DESC
|
|
107
|
-
LIMIT 5
|
|
101
|
+
const topFiles = db.prepare(`
|
|
102
|
+
SELECT file_path, COUNT(*) as edits, COUNT(DISTINCT session_id) as in_sessions
|
|
103
|
+
FROM session_file_edits
|
|
104
|
+
WHERE operation IN ('edit', 'write')
|
|
105
|
+
GROUP BY file_path
|
|
106
|
+
ORDER BY edits DESC
|
|
107
|
+
LIMIT 5
|
|
108
108
|
`).all();
|
|
109
109
|
const result = {
|
|
110
110
|
db_path: dbPath,
|
package/dist/db/migrate.js
CHANGED
|
@@ -110,6 +110,42 @@ export function runMigrations(db) {
|
|
|
110
110
|
addCol('last_confirmed_at', 'last_confirmed_at INTEGER');
|
|
111
111
|
addCol('owner', 'owner TEXT');
|
|
112
112
|
}
|
|
113
|
+
// v11 → v12: reconciler overlay columns on map_nodes (the Map shipped at v11
|
|
114
|
+
// without them). Only ALTER if map_nodes already exists; a v10→v12 jump creates
|
|
115
|
+
// it fresh (with the columns) via db.exec(sql) below.
|
|
116
|
+
if (currentVersion > 0 && currentVersion < 12) {
|
|
117
|
+
const hasMapNodes = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='map_nodes'").get();
|
|
118
|
+
if (hasMapNodes) {
|
|
119
|
+
const have = new Set(db.prepare('PRAGMA table_info(map_nodes)').all().map((c) => c.name));
|
|
120
|
+
const addCol = (name, ddl) => { if (!have.has(name))
|
|
121
|
+
db.exec(`ALTER TABLE map_nodes ADD COLUMN ${ddl}`); };
|
|
122
|
+
addCol('reality', "reality TEXT NOT NULL DEFAULT '{}'");
|
|
123
|
+
addCol('live_verdict', 'live_verdict TEXT');
|
|
124
|
+
addCol('verdict_evidence', "verdict_evidence TEXT NOT NULL DEFAULT '{}'");
|
|
125
|
+
addCol('reconciled_at', 'reconciled_at INTEGER');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// v12 → v13: edge strength + accounted-for expiry (anti-noise / anti-graveyard).
|
|
129
|
+
if (currentVersion > 0 && currentVersion < 13) {
|
|
130
|
+
const has = (table, col) => db.prepare(`PRAGMA table_info(${table})`).all().some((c) => c.name === col);
|
|
131
|
+
if (db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='map_edges'").get()) {
|
|
132
|
+
if (!has('map_edges', 'strength'))
|
|
133
|
+
db.exec('ALTER TABLE map_edges ADD COLUMN strength TEXT');
|
|
134
|
+
}
|
|
135
|
+
if (db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='map_nodes'").get()) {
|
|
136
|
+
if (!has('map_nodes', 'review_by'))
|
|
137
|
+
db.exec('ALTER TABLE map_nodes ADD COLUMN review_by TEXT');
|
|
138
|
+
if (!has('map_nodes', 'revival_condition'))
|
|
139
|
+
db.exec('ALTER TABLE map_nodes ADD COLUMN revival_condition TEXT');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// v13 → v14: per-project uniqueness. map_nodes PK was the global `id`, and map_edges
|
|
143
|
+
// UNIQUE was global (from_id,to_id,type) — so two projects couldn't both have a `readme`
|
|
144
|
+
// node or a `readme→docs-site` edge. SQLite can't alter a PK/UNIQUE, so drop + recreate;
|
|
145
|
+
// safe because importMap rebuilds both from map.yaml on every run.
|
|
146
|
+
if (currentVersion > 0 && currentVersion < 14) {
|
|
147
|
+
db.exec('DROP TABLE IF EXISTS map_nodes; DROP TABLE IF EXISTS map_edges;');
|
|
148
|
+
}
|
|
113
149
|
db.exec(sql);
|
|
114
150
|
if (currentVersion > 0 && currentVersion < 4) {
|
|
115
151
|
db.exec(`INSERT INTO memories_fts(rowid, content) SELECT id, content FROM memories;`);
|