natureco-cli 5.44.1 → 5.45.1
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 +20 -0
- package/bin/natureco.js +1 -1
- package/package.json +1 -1
- package/src/commands/memory-cmd.js +53 -5
- package/src/tools/memory_tree.js +8 -4
- package/src/utils/memory-lint.js +174 -0
- package/src/utils/tr-text.js +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to NatureCo CLI will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [5.45.1] - 2026-07-08 — "FIX: Türkçe İ/i recall hatası — büyük-harfli her Türkçe kelime sessizce kaçıyordu"
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Ajanın canlı hafıza recall'ı büyük-harfli Türkçe kelimeleri BULAMIYORDU (kritik, doğrulandı).** `memory_tree` search/remove eşleşmeyi `line.toLowerCase().includes(q)` ile yapıyordu; ama JS'in `toLowerCase()`'i locale-duyarsız: `"İstanbul".toLowerCase()` → `"i̇stanbul"` (ASCII i + U+0307 BİRLEŞİK NOKTA) olur ve `"istanbul"` sorgusuyla **EŞLEŞMEZ**. Sonuç: her konuşmada İstanbul, İzmir, İş, İletişim gibi büyük-harfli Türkçe kelimeler recall'da görünmez şekilde kaçıyordu — Türkçe-öncelikli bir üründe milyonlarca kullanıcı için sessiz veri kaybı. Artık ortak `src/utils/tr-text.js` **`foldTr`** helper'ı dört Türkçe i-varyantını (İ/I/ı/i → i) tek forma indiriyor; `{İstanbul, istanbul, ISTANBUL, ıstanbul}` hepsi `"istanbul"` sorgusuyla eşleşiyor, İngilizce bozulmuyor (`FILE`→`file`), anlam taşıyan ş/ç/ğ/ö/ü korunuyor (`şık`≠`sık`). Hem ajan recall'ı (`memory_tree`) hem insan CLI araması (`memory-lint searchTree`) aynı folding'i kullanıyor.
|
|
9
|
+
- **`memory search` regex-özel-karakter tuzağı.** Fallback arama `new RegExp(query, 'i')` kullanıyordu; `"proje kod adı (v2)"` gibi doğal bir sorguda parantezler yakalama-grubu sayılıp literal metinle **eşleşmiyordu**. Artık regex yok: sorgu boşluklardan kelimelere ayrılıp **AND** mantığıyla literal aranıyor — özel karakterler `()[]*?` literal alınır, çok-kelimeli sorgular daha isabetli.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `src/utils/tr-text.js` (`foldTr`, `foldIncludes`) — Türkçe-güvenli case folding, tek kaynak.
|
|
13
|
+
- Lint bulgularında **dal (branch) bağlamı** gösterilir (`(## Projeler)` / `(## Kararlar)`) — kullanıcı çelişen kaydın hangisinin nerede olduğunu görür.
|
|
14
|
+
- 22 yeni regresyon testi (İ/i folding, regex-özel-karakter literal, çok-kelime AND, branch koruma, dosya-yok güvenliği). **596 test yeşil** (593 + 3 skip).
|
|
15
|
+
|
|
16
|
+
## [5.45.0] - 2026-07-08 — "MEMORY: Urðr lint + fallback search entegre edildi"
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **`natureco memory lint`** — Urðr standardından (natureco-official/urdr) türetilen, LLM'siz sağlık denetimi. Düz hafızada (`<user>.json`) ve ağaç hafızada (`tree/<user>/`) **YİNELENEN** (aynı bilgi iki kez) ve **ÇELİŞEN** (aynı konu, farklı değer — ör. iki farklı favori renk ya da iki farklı proje kod adı) kayıtları Jaccard benzerliğiyle yakalar. Bu, recall'ın "yanlış hatırlanan değeri" döndürmesinin kök nedeni. Gerçek kullanıcı hafızasında test edildi (çelişki yakalandı).
|
|
20
|
+
- **`natureco memory search`'e ağaç fallback'i** — düz hafızada bulunamazsa, ağaç hafızada branch-aware tam tarama yapar (Urðr `search.mjs` portu). Yanlış-kök tahmini yüzünden bilgi "erişilemez" kalmaz. LLM'siz, cross-platform. Sonuçlar `dosya › ## dal › yaprak` biçiminde.
|
|
21
|
+
- `src/utils/memory-lint.js` — `lintFacts`/`lintUser`/`searchTree` (5 regresyon testi). 576 test yeşil.
|
|
22
|
+
|
|
23
|
+
Bu, Urðr'nin kanıtlanmış hafıza-güvenilirlik araçlarını NatureCo'nun gerçek memory-tree'sine bağlar; native root isimleri (`0-index.md`, `1-kisisel.md` …) zaten tanınıyor.
|
|
24
|
+
|
|
5
25
|
## [5.44.1] - 2026-07-08 — "MEMORY: açık 'hatırla' komutu HAM kaydedilir (kayıp önlenir)"
|
|
6
26
|
|
|
7
27
|
### Added
|
package/bin/natureco.js
CHANGED
|
@@ -130,7 +130,7 @@ ${chalk.yellow('💻 Geliştirici Araçları')}
|
|
|
130
130
|
|
|
131
131
|
${chalk.yellow('📊 Yönetim & Sistem')}
|
|
132
132
|
${chalk.cyan('dashboard')} Web kontrol paneli (open|status|url)
|
|
133
|
-
${chalk.cyan('memory')} Hafıza yönetimi (status|list|search|show|clear|index|export|import|semantic|wiki)
|
|
133
|
+
${chalk.cyan('memory')} Hafıza yönetimi (status|list|search|lint|show|clear|index|export|import|semantic|wiki)
|
|
134
134
|
${chalk.cyan('logs')} Log yönetimi (tail|show|search|clear|path)
|
|
135
135
|
${chalk.cyan('status')} Sistem durumu (run|simple|usage)
|
|
136
136
|
${chalk.cyan('plugins')} Plugin yönetimi (list|install|uninstall|enable|disable|info|update|search|doctor|registry|marketplace)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "natureco-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.45.1",
|
|
4
4
|
"description": "OpenClaw'dan daha güvenli, daha hızlı, daha ucuz AI agent CLI. Multi-agent, self-evolving skills, audit log, maliyet optimizasyonu ve NatureCo platform-native.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"natureco": "bin/natureco.js"
|
|
@@ -19,6 +19,7 @@ async function memoryCmd(args) {
|
|
|
19
19
|
if (action === 'index') return indexMemory();
|
|
20
20
|
if (action === 'export') return exportMemoryCmd(params[0], params[1]);
|
|
21
21
|
if (action === 'import') return importMemoryCmd(params[0], params[1]);
|
|
22
|
+
if (action === 'lint') return lintMemoryCmd(params[0]);
|
|
22
23
|
if (action === 'semantic') return semanticSearchCmd(params.join(' '));
|
|
23
24
|
// Wiki pages
|
|
24
25
|
if (action === 'wiki') return wikiCmd(params[0], params.slice(1));
|
|
@@ -141,14 +142,24 @@ function searchMemory(query) {
|
|
|
141
142
|
});
|
|
142
143
|
});
|
|
143
144
|
|
|
144
|
-
console.log(chalk.cyan.bold(`\n "${query}" için ${results.length}
|
|
145
|
-
if (results.length === 0) {
|
|
146
|
-
console.log(chalk.gray(' Sonuç bulunamadı.\n'));
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
145
|
+
console.log(chalk.cyan.bold(`\n "${query}" için ${results.length} sonuç (düz hafıza)\n`));
|
|
149
146
|
results.forEach(r => {
|
|
150
147
|
console.log(chalk.white(` [${r.bot}] `) + chalk.gray(`${r.field}: `) + chalk.white(r.value));
|
|
151
148
|
});
|
|
149
|
+
|
|
150
|
+
// v5.45: ağaç hafızada branch-aware FALLBACK (Urðr) — düz hafızada bulunamasa bile
|
|
151
|
+
// bilgi "erişilemez" kalmasın (yanlış-kök tahmini kurtarma ağı). LLM'siz.
|
|
152
|
+
try {
|
|
153
|
+
const { searchTree } = require('../utils/memory-lint');
|
|
154
|
+
let u; try { u = require('../utils/config').getConfig().userName; } catch {}
|
|
155
|
+
const treeHits = searchTree(u || 'default', query);
|
|
156
|
+
if (treeHits.length) {
|
|
157
|
+
console.log(chalk.cyan(`\n Ağaç hafızada ${treeHits.length} sonuç:`));
|
|
158
|
+
treeHits.slice(0, 15).forEach(h => console.log(chalk.gray(` ${h.file} › ## ${h.branch} › `) + chalk.white(h.text)));
|
|
159
|
+
} else if (results.length === 0) {
|
|
160
|
+
console.log(chalk.gray(' Sonuç bulunamadı (düz + ağaç hafıza).'));
|
|
161
|
+
}
|
|
162
|
+
} catch { if (results.length === 0) console.log(chalk.gray(' Sonuç bulunamadı.')); }
|
|
152
163
|
console.log('');
|
|
153
164
|
}
|
|
154
165
|
|
|
@@ -404,4 +415,41 @@ function semanticSearchMemory(query, botId) {
|
|
|
404
415
|
return results;
|
|
405
416
|
}
|
|
406
417
|
|
|
418
|
+
// v5.45: memory lint — Urðr-derived duplicate/conflict audit (flat + tree).
|
|
419
|
+
// Catches "same fact stored twice" and "same subject, different value" (the drift that
|
|
420
|
+
// makes recall return the wrong remembered value, e.g. two different project code names).
|
|
421
|
+
function lintMemoryCmd(user) {
|
|
422
|
+
const { lintUser } = require('../utils/memory-lint');
|
|
423
|
+
let u = user;
|
|
424
|
+
if (!u) { try { u = require('../utils/config').getConfig().userName; } catch {} }
|
|
425
|
+
const { flatFile, flatCount, flatFindings, treeFindings } = lintUser(u || 'default');
|
|
426
|
+
|
|
427
|
+
console.log(chalk.cyan(`\n 🧠 Memory Lint · ${u || 'default'}\n`) + chalk.gray(' ' + '─'.repeat(52)));
|
|
428
|
+
console.log(chalk.gray(` flat: ${flatCount} fact · ${path.basename(flatFile)}`));
|
|
429
|
+
|
|
430
|
+
const all = [...flatFindings, ...treeFindings];
|
|
431
|
+
const dups = all.filter((f) => f.level === 'duplicate');
|
|
432
|
+
const conflicts = all.filter((f) => f.level === 'conflict');
|
|
433
|
+
|
|
434
|
+
if (all.length === 0) {
|
|
435
|
+
console.log(chalk.green('\n ✓ Temiz — yinelenen veya çelişen kayıt yok.\n'));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
// Tree bulgularında dal (branch) bağlamını da göster ki kullanıcı "hangisi nerede" bilsin.
|
|
439
|
+
const br = (b) => (b ? chalk.gray(` (## ${b})`) : '');
|
|
440
|
+
const printFinding = (f, sym) => {
|
|
441
|
+
console.log(` ${chalk.gray(`(%${Math.round(f.sim * 100)})`)} ${f.a}${br(f.aBranch)}`);
|
|
442
|
+
console.log(` ${chalk.gray(' ' + sym)} ${f.b}${br(f.bBranch)}${f.file ? chalk.gray(' [' + f.file + ']') : ''}`);
|
|
443
|
+
};
|
|
444
|
+
if (dups.length) {
|
|
445
|
+
console.log(chalk.yellow(`\n ⚠ ${dups.length} olası YİNELENEN (aynı bilgi iki kez):`));
|
|
446
|
+
for (const f of dups.slice(0, 10)) printFinding(f, '≈');
|
|
447
|
+
}
|
|
448
|
+
if (conflicts.length) {
|
|
449
|
+
console.log(chalk.red(`\n ⚠ ${conflicts.length} olası ÇELİŞKİ (aynı konu, farklı değer):`));
|
|
450
|
+
for (const f of conflicts.slice(0, 10)) printFinding(f, '↔');
|
|
451
|
+
}
|
|
452
|
+
console.log(chalk.gray(`\n Öneri: eskiyen/yanlış kaydı düzeltin — "natureco memory clear" veya elle. Tek doğru kalsın.\n`));
|
|
453
|
+
}
|
|
454
|
+
|
|
407
455
|
module.exports = memoryCmd;
|
package/src/tools/memory_tree.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const os = require('os');
|
|
20
|
+
const { foldTr } = require('../utils/tr-text');
|
|
20
21
|
|
|
21
22
|
const ROOTS = [
|
|
22
23
|
{ id: '1-kisisel', title: 'Kişisel & Tercihler', branches: ['Kimlik', 'Tercihler', 'İletişim Kalıpları'] },
|
|
@@ -91,7 +92,10 @@ function buildDigest(username, maxChars = 2600) {
|
|
|
91
92
|
|
|
92
93
|
function search(username, query) {
|
|
93
94
|
ensureTree(username);
|
|
94
|
-
|
|
95
|
+
// v5.45.1: Türkçe-güvenli eşleşme (foldTr). Eski `line.toLowerCase().includes(q)` locale
|
|
96
|
+
// duyarsızdı → "İstanbul" .toLowerCase() = "i̇stanbul" olur ve "istanbul" sorgusuyla EŞLEŞMEZDİ;
|
|
97
|
+
// her büyük-harfli Türkçe kelime (İzmir, İş, İletişim…) canlı recall'da sessizce kaçıyordu.
|
|
98
|
+
const q = foldTr(query).trim();
|
|
95
99
|
if (!q) return [];
|
|
96
100
|
const hits = [];
|
|
97
101
|
for (const r of ROOTS) {
|
|
@@ -99,7 +103,7 @@ function search(username, query) {
|
|
|
99
103
|
let branch = '';
|
|
100
104
|
for (const line of txt.split('\n')) {
|
|
101
105
|
if (line.startsWith('## ')) branch = line.slice(3).trim();
|
|
102
|
-
else if (line.trim() && !line.startsWith('#') && line
|
|
106
|
+
else if (line.trim() && !line.startsWith('#') && foldTr(line).includes(q)) hits.push(`${r.id}/${branch}: ${line.trim()}`);
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
return hits;
|
|
@@ -121,13 +125,13 @@ function getPending(username) {
|
|
|
121
125
|
function remove(username, root, query) {
|
|
122
126
|
ensureTree(username);
|
|
123
127
|
const r = resolveRoot(root || '3-kararlar');
|
|
124
|
-
const q =
|
|
128
|
+
const q = foldTr(query).trim(); // v5.45.1: Türkçe-güvenli (bkz: search)
|
|
125
129
|
if (!q) return { success: false, error: 'query gerekli' };
|
|
126
130
|
const txt = readSafe(username, r.id);
|
|
127
131
|
const kept = [];
|
|
128
132
|
let removed = 0;
|
|
129
133
|
for (const line of txt.split('\n')) {
|
|
130
|
-
if (/^\s*-\s+/.test(line) && line
|
|
134
|
+
if (/^\s*-\s+/.test(line) && foldTr(line).includes(q)) { removed++; continue; }
|
|
131
135
|
kept.push(line);
|
|
132
136
|
}
|
|
133
137
|
if (removed) fs.writeFileSync(rootPath(username, r.id), kept.join('\n'), 'utf8');
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-lint — flat + tree memory health audit (Urðr-derived, LLM-free)
|
|
3
|
+
*
|
|
4
|
+
* Ported from the Urðr standard (natureco-official/urdr scripts/lint.mjs) into NatureCo's
|
|
5
|
+
* real memory layout so users are protected from the drift that erodes recall:
|
|
6
|
+
* - DUPLICATE facts (Jaccard ≥ 0.85) — the same thing stored twice, slightly reworded.
|
|
7
|
+
* - CONFLICTING facts (Jaccard 0.5–0.85) — same subject, different value, e.g.
|
|
8
|
+
* "favori rengi kırmızı" vs "Favori rengi mavidir", or two different project code names.
|
|
9
|
+
* This is exactly why recall can return the "wrong" remembered value.
|
|
10
|
+
*
|
|
11
|
+
* Flat memory: ~/.natureco/memory/<user>.json (facts: [{value, ...}])
|
|
12
|
+
* Tree memory: ~/.natureco/memory/tree/<user>/*.md (## branch → leaves)
|
|
13
|
+
*
|
|
14
|
+
* v5.45.1: matching is Turkish-aware (tr-text foldTr) and searchTree is regex-free so a
|
|
15
|
+
* natural query like "proje kod adı (v2)" is matched literally, not as a regex.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const { foldTr } = require('./tr-text');
|
|
22
|
+
|
|
23
|
+
const MEMORY_DIR = path.join(os.homedir(), '.natureco', 'memory');
|
|
24
|
+
const DUP = 0.85;
|
|
25
|
+
const CONFLICT = 0.5;
|
|
26
|
+
// Root files: Urðr "root-N-name.md" / "kök-N-…" and NatureCo native "N-name.md".
|
|
27
|
+
const ROOT_RE = /^(?:(?:root|kök|kok)-)?\d[-_].*\.md$/i;
|
|
28
|
+
|
|
29
|
+
const STOP = new Set(['ve', 'ile', 'için', 'icin', 'bir', 'bu', 'da', 'de', 'the', 'and', 'for',
|
|
30
|
+
'kullanici', 'kullanıcı', 'kullanicinin', 'kullanıcının', 'benim', 'onun', 'çok', 'cok']);
|
|
31
|
+
|
|
32
|
+
function tokens(text) {
|
|
33
|
+
// foldTr lowercases and normalizes the four Turkish i-variants so "İstanbul" and
|
|
34
|
+
// "istanbul" tokenize identically (better duplicate/conflict detection for TR facts).
|
|
35
|
+
return new Set(
|
|
36
|
+
foldTr(text)
|
|
37
|
+
.replace(/[*_`|]/g, ' ')
|
|
38
|
+
.replace(/\b\d{2}\.\d{2}\.\d{4}\b/g, ' ')
|
|
39
|
+
.split(/[^a-z0-9çğıöşü-]+/i)
|
|
40
|
+
.filter((w) => w.length > 2 && !STOP.has(w))
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function jaccard(a, b) {
|
|
45
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
46
|
+
let inter = 0;
|
|
47
|
+
for (const x of a) if (b.has(x)) inter++;
|
|
48
|
+
return inter / (a.size + b.size - inter);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Lint an array of fact objects/strings. Returns findings, most-similar first.
|
|
53
|
+
* Each fact may carry an optional `branch` (tree memory) which is propagated into the
|
|
54
|
+
* finding as aBranch/bBranch so the caller can tell the user WHERE each side lives.
|
|
55
|
+
* @returns {Array<{level:'duplicate'|'conflict', sim:number, a:string, b:string, aBranch?:string, bBranch?:string}>}
|
|
56
|
+
*/
|
|
57
|
+
function lintFacts(facts) {
|
|
58
|
+
const items = (facts || [])
|
|
59
|
+
.map((f) => ({ value: f && f.value != null ? f.value : f, branch: f && f.branch }))
|
|
60
|
+
.filter((x) => typeof x.value === 'string' && x.value.trim());
|
|
61
|
+
const toks = items.map((x) => tokens(x.value));
|
|
62
|
+
const findings = [];
|
|
63
|
+
for (let i = 0; i < items.length; i++) {
|
|
64
|
+
for (let j = i + 1; j < items.length; j++) {
|
|
65
|
+
const sim = jaccard(toks[i], toks[j]);
|
|
66
|
+
if (sim < CONFLICT) continue;
|
|
67
|
+
findings.push({
|
|
68
|
+
level: sim >= DUP ? 'duplicate' : 'conflict',
|
|
69
|
+
sim,
|
|
70
|
+
a: items[i].value,
|
|
71
|
+
b: items[j].value,
|
|
72
|
+
aBranch: items[i].branch,
|
|
73
|
+
bBranch: items[j].branch,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return findings.sort((x, y) => y.sim - x.sim);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Is this a real leaf line (not a heading/comment/separator/placeholder/quote)? */
|
|
81
|
+
function isLeaf(trimmed) {
|
|
82
|
+
return !!trimmed
|
|
83
|
+
&& !trimmed.startsWith('<!--')
|
|
84
|
+
&& !trimmed.startsWith('#')
|
|
85
|
+
&& trimmed !== '---'
|
|
86
|
+
&& !/^_no entries yet\._$/i.test(trimmed)
|
|
87
|
+
&& !trimmed.startsWith('>');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Strip a leading list marker ("- ", "* ", "+ ") for clean display/tokenization. */
|
|
91
|
+
function leafText(trimmed) {
|
|
92
|
+
return trimmed.replace(/^[-*+]\s+/, '');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Parse a tree root file into branches → leaves and lint for duplicates/conflicts within it. */
|
|
96
|
+
function lintTreeFile(file) {
|
|
97
|
+
let content;
|
|
98
|
+
try { content = fs.readFileSync(file, 'utf8'); } catch { return []; }
|
|
99
|
+
const leaves = [];
|
|
100
|
+
let branch = '(root)';
|
|
101
|
+
for (const line of content.split(/\r?\n/)) {
|
|
102
|
+
const h = line.match(/^##\s+(.+?)\s*$/);
|
|
103
|
+
if (h) { branch = h[1]; continue; }
|
|
104
|
+
const t = line.trim();
|
|
105
|
+
if (!isLeaf(t)) continue;
|
|
106
|
+
leaves.push({ value: leafText(t), branch });
|
|
107
|
+
}
|
|
108
|
+
return lintFacts(leaves).map((f) => ({ ...f, file: path.basename(file) }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Full audit for a user: flat facts + every tree root file. */
|
|
112
|
+
function lintUser(user) {
|
|
113
|
+
const uname = (user || 'default').toLowerCase();
|
|
114
|
+
const flatFile = path.join(MEMORY_DIR, `${uname}.json`);
|
|
115
|
+
let flatFindings = [], flatCount = 0;
|
|
116
|
+
try {
|
|
117
|
+
const facts = (JSON.parse(fs.readFileSync(flatFile, 'utf8')).facts) || [];
|
|
118
|
+
flatCount = facts.length;
|
|
119
|
+
flatFindings = lintFacts(facts);
|
|
120
|
+
} catch {}
|
|
121
|
+
|
|
122
|
+
const treeDir = path.join(MEMORY_DIR, 'tree', uname);
|
|
123
|
+
const treeFindings = [];
|
|
124
|
+
try {
|
|
125
|
+
for (const f of fs.readdirSync(treeDir)) {
|
|
126
|
+
if (ROOT_RE.test(f)) treeFindings.push(...lintTreeFile(path.join(treeDir, f)));
|
|
127
|
+
}
|
|
128
|
+
} catch {}
|
|
129
|
+
|
|
130
|
+
return { flatFile, flatCount, flatFindings, treeFindings };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* v5.45: Branch-aware fallback search over a user's memory tree (Urðr search.mjs port).
|
|
135
|
+
* The hierarchical 4-step lookup is primary; this is the safety net so a wrong-root guess
|
|
136
|
+
* never makes stored info read as "forgotten". LLM-free, cross-platform.
|
|
137
|
+
*
|
|
138
|
+
* v5.45.1: Turkish-aware (foldTr) and REGEX-FREE. The query is split into whitespace terms
|
|
139
|
+
* and a leaf matches only if it contains ALL terms (AND) — so multi-word queries are useful
|
|
140
|
+
* and special characters like ()[]*? are matched literally instead of breaking the search.
|
|
141
|
+
* @param {string} user
|
|
142
|
+
* @param {string} query
|
|
143
|
+
* @param {{max?:number, dir?:string}} opts dir overrides the tree directory (testing).
|
|
144
|
+
* @returns {Array<{file, branch, text}>}
|
|
145
|
+
*/
|
|
146
|
+
function searchTree(user, query, opts = {}) {
|
|
147
|
+
const max = opts.max || 25;
|
|
148
|
+
const treeDir = opts.dir || path.join(MEMORY_DIR, 'tree', (user || 'default').toLowerCase());
|
|
149
|
+
const terms = String(query == null ? '' : query).split(/\s+/).map(foldTr).filter(Boolean);
|
|
150
|
+
if (!terms.length) return [];
|
|
151
|
+
let files;
|
|
152
|
+
try { files = fs.readdirSync(treeDir).filter((f) => ROOT_RE.test(f)); }
|
|
153
|
+
catch { return []; }
|
|
154
|
+
const out = [];
|
|
155
|
+
for (const file of files.sort()) {
|
|
156
|
+
let content;
|
|
157
|
+
try { content = fs.readFileSync(path.join(treeDir, file), 'utf8'); } catch { continue; }
|
|
158
|
+
let branch = '(root)';
|
|
159
|
+
for (const line of content.split(/\r?\n/)) {
|
|
160
|
+
const h = line.match(/^##\s+(.+?)\s*$/);
|
|
161
|
+
if (h) { branch = h[1]; continue; }
|
|
162
|
+
const t = line.trim();
|
|
163
|
+
if (!isLeaf(t)) continue;
|
|
164
|
+
const folded = foldTr(line);
|
|
165
|
+
if (terms.every((term) => folded.includes(term))) {
|
|
166
|
+
out.push({ file, branch, text: leafText(t).slice(0, 300) });
|
|
167
|
+
if (out.length >= max) return out;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { lintFacts, lintTreeFile, lintUser, searchTree, tokens, jaccard };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tr-text — Turkish-aware text folding for case-insensitive, robust substring matching.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS (real bug, verified):
|
|
5
|
+
* JavaScript's String.prototype.toLowerCase() is locale-INSENSITIVE. On the dotted
|
|
6
|
+
* capital "İ" (U+0130) it produces "i̇" (ASCII i + COMBINING DOT ABOVE U+0307), which
|
|
7
|
+
* does NOT equal a plain "i". So the naive `text.toLowerCase().includes(query)` used by
|
|
8
|
+
* memory recall silently MISSES every capitalized Turkish word:
|
|
9
|
+
* "İstanbul".toLowerCase() -> "i̇stanbul" (query "istanbul" → NO MATCH)
|
|
10
|
+
* "ISPARTA".toLowerCase() -> "isparta" (Turkish query "ısparta" → NO MATCH)
|
|
11
|
+
* For a Turkish-first product this breaks recall for İstanbul, İzmir, İş, İletişim, …
|
|
12
|
+
* on every conversation — invisible data loss for millions of users.
|
|
13
|
+
*
|
|
14
|
+
* THE FIX:
|
|
15
|
+
* Fold all four Turkish "i" letters — İ (dotted upper), I (dotless upper), ı (dotless
|
|
16
|
+
* lower), i (dotted lower) — to a single canonical 'i' BEFORE lowercasing. This makes
|
|
17
|
+
* {İstanbul, istanbul, ISTANBUL, ıstanbul} all match query "istanbul", while English
|
|
18
|
+
* stays intact ("FILE" -> "file", not the Turkish "fıle").
|
|
19
|
+
*
|
|
20
|
+
* We deliberately do NOT fold ş/ç/ğ/ö/ü — those carry meaning ("şık" elegant ≠ "sık"
|
|
21
|
+
* frequent), and collapsing them would create wrong matches. Only the notorious
|
|
22
|
+
* i-dot case-folding trap is normalized.
|
|
23
|
+
*
|
|
24
|
+
* Used by both the agent's live recall (memory_tree search/remove) and the human CLI
|
|
25
|
+
* search (memory-lint searchTree) so matching behaves identically everywhere.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fold text for case-insensitive Turkish-safe comparison. Never throws.
|
|
30
|
+
* @param {*} s
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function foldTr(s) {
|
|
34
|
+
return String(s == null ? '' : s)
|
|
35
|
+
.replace(/İ/g, 'i') // U+0130 dotted capital → i (avoids the U+0307 combining-dot trap)
|
|
36
|
+
.replace(/I/g, 'i') // U+0049 dotless capital → i
|
|
37
|
+
.replace(/ı/g, 'i') // U+0131 dotless lower → i
|
|
38
|
+
.toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Turkish-aware, case-insensitive "does haystack contain needle?". Substring — no regex,
|
|
43
|
+
* so query text with (), [], *, ? etc. is matched literally (never interpreted).
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function foldIncludes(haystack, needle) {
|
|
47
|
+
return foldTr(haystack).includes(foldTr(needle));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { foldTr, foldIncludes };
|