lynkr 9.7.3 → 9.9.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/README.md +63 -25
- package/bin/cli.js +16 -1
- package/bin/lynkr-init.js +44 -1
- package/bin/lynkr-usage.js +78 -0
- package/bin/wrap.js +60 -35
- package/config/difficulty-anchors.json +22 -0
- package/package.json +23 -2
- package/scripts/audit-log-reader.js +399 -0
- package/scripts/build-eval-set.js +256 -0
- package/scripts/calibrate-thresholds.js +38 -157
- package/scripts/compact-dictionary.js +204 -0
- package/scripts/mine-difficulty-anchors.js +288 -0
- package/scripts/test-deduplication.js +448 -0
- package/scripts/validate-difficulty-classifier.js +123 -0
- package/scripts/validate-intent-anchors.js +186 -0
- package/scripts/ws7-anchor-replay.js +108 -0
- package/skills/lynkr/SKILL.md +195 -0
- package/src/api/middleware/loop-guard.js +87 -0
- package/src/api/middleware/request-logging.js +5 -64
- package/src/api/middleware/session.js +0 -0
- package/src/api/openai-router.js +120 -101
- package/src/api/providers-handler.js +27 -2
- package/src/api/router.js +467 -125
- package/src/budget/index.js +2 -19
- package/src/cache/semantic.js +9 -0
- package/src/clients/databricks.js +455 -142
- package/src/clients/gpt-utils.js +11 -105
- package/src/clients/openai-format.js +10 -3
- package/src/clients/openrouter-utils.js +49 -24
- package/src/clients/prompt-cache-injection.js +1 -0
- package/src/clients/provider-capabilities.js +1 -1
- package/src/clients/responses-format.js +34 -3
- package/src/clients/routing.js +15 -0
- package/src/config/index.js +36 -2
- package/src/context/gcf.js +275 -0
- package/src/context/tool-result-compressor.js +932 -47
- package/src/dashboard/api.js +1 -0
- package/src/logger/index.js +14 -1
- package/src/memory/search.js +12 -40
- package/src/memory/tools.js +3 -24
- package/src/orchestrator/bypass.js +4 -2
- package/src/orchestrator/index.js +120 -85
- package/src/routing/affinity-store.js +194 -0
- package/src/routing/agentic-detector.js +36 -6
- package/src/routing/bandit.js +25 -6
- package/src/routing/calibration.js +212 -0
- package/src/routing/classifier-setup.js +207 -0
- package/src/routing/client-profiles.js +292 -0
- package/src/routing/complexity-analyzer.js +88 -15
- package/src/routing/deescalator.js +148 -0
- package/src/routing/degradation.js +109 -0
- package/src/routing/difficulty-classifier.js +219 -0
- package/src/routing/feedback.js +157 -0
- package/src/routing/index.js +931 -90
- package/src/routing/intent-score.js +441 -0
- package/src/routing/interaction.js +3 -0
- package/src/routing/knn-router.js +70 -21
- package/src/routing/model-registry.js +28 -7
- package/src/routing/model-tiers.js +25 -2
- package/src/routing/reward-pipeline.js +68 -2
- package/src/routing/risk-analyzer.js +30 -1
- package/src/routing/risk-classifier.js +6 -2
- package/src/routing/session-affinity.js +162 -34
- package/src/routing/telemetry.js +286 -13
- package/src/routing/verifier.js +267 -0
- package/src/server.js +86 -21
- package/src/sessions/cleanup.js +17 -0
- package/src/tools/index.js +1 -15
- package/src/tools/smart-selection.js +10 -0
- package/src/tools/web-client.js +3 -3
- package/.eslintrc.cjs +0 -12
- package/benchmark-configs/litellm_config.yaml +0 -86
- package/benchmark-configs/lynkr.env +0 -48
- package/benchmark-configs/portkey-config.json +0 -60
- package/benchmark-configs/portkey-docker.sh +0 -23
- package/benchmark-tier-routing.js +0 -449
- package/funding.json +0 -110
- package/src/api/middleware/validation.js +0 -261
- package/src/routing/drift-monitor.js +0 -113
- package/src/workers/helpers.js +0 -185
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Calibrate tier thresholds from telemetry.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* data/calibrated-thresholds.json. ModelTierSelector picks the file up on
|
|
9
|
-
* next start.
|
|
5
|
+
* CLI wrapper around `src/routing/calibration.js`. WS5.6 moved the core
|
|
6
|
+
* logic into a module so the same code path drives both this manual
|
|
7
|
+
* script and the in-process auto-calibration scheduler.
|
|
10
8
|
*
|
|
11
9
|
* Usage: node scripts/calibrate-thresholds.js [--days N] [--dry-run]
|
|
12
10
|
* npx lynkr calibrate
|
|
@@ -16,31 +14,9 @@
|
|
|
16
14
|
* - Exits 0 with a "skipped" message.
|
|
17
15
|
*/
|
|
18
16
|
|
|
19
|
-
const
|
|
20
|
-
const path = require('path');
|
|
17
|
+
const { runCalibration } = require('../src/routing/calibration');
|
|
21
18
|
|
|
22
19
|
const DEFAULT_DAYS = 7;
|
|
23
|
-
const MIN_SAMPLES = 100;
|
|
24
|
-
/** Quality score below which a complexity bucket is "underperforming" for its tier. */
|
|
25
|
-
const QUALITY_FLOOR = {
|
|
26
|
-
SIMPLE: 55,
|
|
27
|
-
MEDIUM: 60,
|
|
28
|
-
COMPLEX: 65,
|
|
29
|
-
REASONING: 70,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const OUTPUT_PATH = path.join(__dirname, '../data/calibrated-thresholds.json');
|
|
33
|
-
const TELEMETRY_DB_CANDIDATES = [
|
|
34
|
-
path.join(__dirname, '../.lynkr/telemetry.db'),
|
|
35
|
-
path.join(__dirname, '../data/lynkr.db'),
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
function _findDb() {
|
|
39
|
-
for (const p of TELEMETRY_DB_CANDIDATES) {
|
|
40
|
-
if (fs.existsSync(p)) return p;
|
|
41
|
-
}
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
20
|
|
|
45
21
|
function _parseArgs(argv) {
|
|
46
22
|
const out = { days: DEFAULT_DAYS, dryRun: false };
|
|
@@ -52,141 +28,46 @@ function _parseArgs(argv) {
|
|
|
52
28
|
return out;
|
|
53
29
|
}
|
|
54
30
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
31
|
+
function _reportSkipped(result) {
|
|
32
|
+
switch (result.reason) {
|
|
33
|
+
case 'no_db':
|
|
34
|
+
console.log('No telemetry DB found — skipping calibration.');
|
|
35
|
+
break;
|
|
36
|
+
case 'db_open_failed':
|
|
37
|
+
console.error(`Failed to open telemetry DB: ${result.error}`);
|
|
38
|
+
process.exit(2);
|
|
39
|
+
break;
|
|
40
|
+
case 'query_failed':
|
|
41
|
+
console.error(`Telemetry query failed (DB may be corrupt or schema missing): ${result.error}`);
|
|
42
|
+
break;
|
|
43
|
+
case 'insufficient_samples':
|
|
44
|
+
console.log(
|
|
45
|
+
`Only ${result.count} rows with quality_score (need ≥${result.minSamples}). Skipping.`
|
|
46
|
+
);
|
|
47
|
+
break;
|
|
48
|
+
case 'write_failed':
|
|
49
|
+
console.error(`Failed to write calibrated thresholds: ${result.error}`);
|
|
50
|
+
process.exit(2);
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
console.log(`Skipped: ${result.reason}`);
|
|
69
54
|
}
|
|
70
|
-
return new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
71
55
|
}
|
|
72
56
|
|
|
73
|
-
function calibrate(
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
let db;
|
|
81
|
-
try {
|
|
82
|
-
db = _openDb(dbPath);
|
|
83
|
-
} catch (err) {
|
|
84
|
-
console.error(`Failed to open telemetry DB: ${err.message}`);
|
|
85
|
-
return { skipped: true, reason: 'db_open_failed', error: err.message };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const since = Date.now() - days * 24 * 3600 * 1000;
|
|
89
|
-
let rows;
|
|
90
|
-
try {
|
|
91
|
-
rows = db
|
|
92
|
-
.prepare(
|
|
93
|
-
`SELECT tier, complexity_score AS score, quality_score AS q
|
|
94
|
-
FROM routing_telemetry
|
|
95
|
-
WHERE timestamp >= ?
|
|
96
|
-
AND quality_score IS NOT NULL
|
|
97
|
-
AND complexity_score IS NOT NULL
|
|
98
|
-
AND tier IS NOT NULL`
|
|
99
|
-
)
|
|
100
|
-
.all(since);
|
|
101
|
-
} catch (err) {
|
|
102
|
-
console.error(`Telemetry query failed (DB may be corrupt or schema missing): ${err.message}`);
|
|
103
|
-
return { skipped: true, reason: 'query_failed', error: err.message };
|
|
104
|
-
} finally {
|
|
105
|
-
try { db.close(); } catch {}
|
|
57
|
+
function calibrate(opts) {
|
|
58
|
+
const result = runCalibration(opts);
|
|
59
|
+
if (result.skipped) {
|
|
60
|
+
_reportSkipped(result);
|
|
61
|
+
return result;
|
|
106
62
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return { skipped: true, reason: 'insufficient_samples', count: rows ? rows.length : 0 };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Bucket by score (0-100 in width-5 buckets) per tier, compute median quality.
|
|
114
|
-
const buckets = new Map(); // tier -> Map<bucketLowerBound, q-values[]>
|
|
115
|
-
for (const row of rows) {
|
|
116
|
-
const s = Math.max(0, Math.min(100, Math.floor(row.score)));
|
|
117
|
-
const bucket = Math.floor(s / 5) * 5;
|
|
118
|
-
if (!buckets.has(row.tier)) buckets.set(row.tier, new Map());
|
|
119
|
-
const b = buckets.get(row.tier);
|
|
120
|
-
if (!b.has(bucket)) b.set(bucket, []);
|
|
121
|
-
b.get(bucket).push(row.q);
|
|
63
|
+
if (result.dryRun) {
|
|
64
|
+
console.log(JSON.stringify(result, null, 2));
|
|
65
|
+
return result;
|
|
122
66
|
}
|
|
123
|
-
|
|
124
|
-
const _median = (arr) => {
|
|
125
|
-
const s = arr.slice().sort((a, b) => a - b);
|
|
126
|
-
const m = Math.floor(s.length / 2);
|
|
127
|
-
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
// Default ranges; will adjust per-tier upper bound if late buckets show poor quality.
|
|
131
|
-
const ranges = { ...DEFAULT_RANGES };
|
|
132
67
|
const tierOrder = ['SIMPLE', 'MEDIUM', 'COMPLEX', 'REASONING'];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const floor = QUALITY_FLOOR[tier];
|
|
137
|
-
const tierBuckets = buckets.get(tier);
|
|
138
|
-
if (!tierBuckets) {
|
|
139
|
-
stats[tier] = { samples: 0, adjusted: false };
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
const ordered = Array.from(tierBuckets.entries()).sort((a, b) => a[0] - b[0]);
|
|
143
|
-
let suggestedUpper = DEFAULT_RANGES[tier][1];
|
|
144
|
-
const buckets_summary = [];
|
|
145
|
-
for (const [lo, vals] of ordered) {
|
|
146
|
-
if (vals.length < 5) {
|
|
147
|
-
buckets_summary.push({ bucket: lo, samples: vals.length, median: null });
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
const med = _median(vals);
|
|
151
|
-
buckets_summary.push({ bucket: lo, samples: vals.length, median: med });
|
|
152
|
-
if (med < floor && lo + 4 < suggestedUpper) {
|
|
153
|
-
suggestedUpper = lo + 4; // shrink tier upper bound just below the failing bucket
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (suggestedUpper !== DEFAULT_RANGES[tier][1]) {
|
|
157
|
-
ranges[tier] = [DEFAULT_RANGES[tier][0], suggestedUpper];
|
|
158
|
-
stats[tier] = { samples: ordered.reduce((s, [, v]) => s + v.length, 0), adjusted: true, buckets: buckets_summary };
|
|
159
|
-
} else {
|
|
160
|
-
stats[tier] = { samples: ordered.reduce((s, [, v]) => s + v.length, 0), adjusted: false, buckets: buckets_summary };
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Re-stitch ranges so they don't overlap or leave gaps.
|
|
165
|
-
for (let i = 1; i < tierOrder.length; i++) {
|
|
166
|
-
const prev = ranges[tierOrder[i - 1]];
|
|
167
|
-
const cur = ranges[tierOrder[i]];
|
|
168
|
-
if (cur[0] !== prev[1] + 1) cur[0] = prev[1] + 1;
|
|
169
|
-
if (cur[0] > cur[1]) cur[1] = cur[0]; // collapsed; tier disabled in practice
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const out = {
|
|
173
|
-
calibratedAt: new Date().toISOString(),
|
|
174
|
-
days,
|
|
175
|
-
sampleCount: rows.length,
|
|
176
|
-
ranges,
|
|
177
|
-
stats,
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
if (dryRun) {
|
|
181
|
-
console.log(JSON.stringify(out, null, 2));
|
|
182
|
-
return { ...out, dryRun: true };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true });
|
|
186
|
-
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(out, null, 2));
|
|
187
|
-
console.log(`Wrote ${OUTPUT_PATH}`);
|
|
188
|
-
console.log(`Ranges: ${tierOrder.map((t) => `${t}=${ranges[t].join('-')}`).join(', ')}`);
|
|
189
|
-
return out;
|
|
68
|
+
console.log(`Wrote ${result.writtenTo}`);
|
|
69
|
+
console.log(`Ranges: ${tierOrder.map((t) => `${t}=${result.ranges[t].join('-')}`).join(', ')}`);
|
|
70
|
+
return result;
|
|
190
71
|
}
|
|
191
72
|
|
|
192
73
|
if (require.main === module) {
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compact LLM Audit Dictionary
|
|
5
|
+
*
|
|
6
|
+
* Removes redundant UPDATE entries from the dictionary file, keeping only:
|
|
7
|
+
* - One entry per hash with full content
|
|
8
|
+
* - Latest metadata (useCount, lastSeen)
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node scripts/compact-dictionary.js [options]
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* --dict-path <path> Path to dictionary file (default: logs/llm-audit-dictionary.jsonl)
|
|
15
|
+
* --backup Create backup before compacting (default: true)
|
|
16
|
+
* --dry-run Show what would be done without making changes
|
|
17
|
+
* --help Show this help message
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const readline = require('readline');
|
|
23
|
+
|
|
24
|
+
// Parse command line arguments
|
|
25
|
+
function parseArgs() {
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const options = {
|
|
28
|
+
dictPath: 'logs/llm-audit-dictionary.jsonl',
|
|
29
|
+
backup: true,
|
|
30
|
+
dryRun: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
const arg = args[i];
|
|
35
|
+
switch (arg) {
|
|
36
|
+
case '--dict-path':
|
|
37
|
+
options.dictPath = args[++i];
|
|
38
|
+
break;
|
|
39
|
+
case '--backup':
|
|
40
|
+
options.backup = true;
|
|
41
|
+
break;
|
|
42
|
+
case '--no-backup':
|
|
43
|
+
options.backup = false;
|
|
44
|
+
break;
|
|
45
|
+
case '--dry-run':
|
|
46
|
+
options.dryRun = true;
|
|
47
|
+
break;
|
|
48
|
+
case '--help':
|
|
49
|
+
console.log(`
|
|
50
|
+
Compact LLM Audit Dictionary
|
|
51
|
+
|
|
52
|
+
Removes redundant UPDATE entries from the dictionary file.
|
|
53
|
+
|
|
54
|
+
Usage:
|
|
55
|
+
node scripts/compact-dictionary.js [options]
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
--dict-path <path> Path to dictionary file (default: logs/llm-audit-dictionary.jsonl)
|
|
59
|
+
--backup Create backup before compacting (default: true)
|
|
60
|
+
--no-backup Skip creating backup
|
|
61
|
+
--dry-run Show what would be done without making changes
|
|
62
|
+
--help Show this help message
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
node scripts/compact-dictionary.js --dict-path logs/llm-audit-dictionary.jsonl --dry-run
|
|
66
|
+
`);
|
|
67
|
+
process.exit(0);
|
|
68
|
+
default:
|
|
69
|
+
if (arg.startsWith('--')) {
|
|
70
|
+
console.error(`Unknown option: ${arg}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return options;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Read and compact dictionary
|
|
80
|
+
async function compactDictionary(dictPath) {
|
|
81
|
+
if (!fs.existsSync(dictPath)) {
|
|
82
|
+
throw new Error(`Dictionary file not found: ${dictPath}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`Reading dictionary: ${dictPath}`);
|
|
86
|
+
|
|
87
|
+
// Map: hash -> entry object
|
|
88
|
+
// For each hash, we'll keep the latest metadata merged with content
|
|
89
|
+
const entries = new Map();
|
|
90
|
+
let totalLines = 0;
|
|
91
|
+
let malformedLines = 0;
|
|
92
|
+
|
|
93
|
+
// Read all entries
|
|
94
|
+
const fileStream = fs.createReadStream(dictPath);
|
|
95
|
+
const rl = readline.createInterface({
|
|
96
|
+
input: fileStream,
|
|
97
|
+
crlfDelay: Infinity,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
for await (const line of rl) {
|
|
101
|
+
totalLines++;
|
|
102
|
+
if (!line.trim()) continue;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const entry = JSON.parse(line);
|
|
106
|
+
if (!entry.hash) {
|
|
107
|
+
malformedLines++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const hash = entry.hash;
|
|
112
|
+
|
|
113
|
+
// Check if we already have an entry for this hash
|
|
114
|
+
if (entries.has(hash)) {
|
|
115
|
+
const existing = entries.get(hash);
|
|
116
|
+
|
|
117
|
+
// Merge: keep content from entry that has it, use latest metadata
|
|
118
|
+
const merged = {
|
|
119
|
+
hash,
|
|
120
|
+
firstSeen: existing.firstSeen || entry.firstSeen,
|
|
121
|
+
useCount: entry.useCount || existing.useCount,
|
|
122
|
+
lastSeen: entry.lastSeen || existing.lastSeen,
|
|
123
|
+
content: existing.content || entry.content,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
entries.set(hash, merged);
|
|
127
|
+
} else {
|
|
128
|
+
// First time seeing this hash
|
|
129
|
+
entries.set(hash, entry);
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
malformedLines++;
|
|
133
|
+
console.warn(`Skipping malformed line ${totalLines}: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const compactedCount = entries.size;
|
|
138
|
+
const removedCount = totalLines - malformedLines - compactedCount;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
entries,
|
|
142
|
+
stats: {
|
|
143
|
+
totalLines,
|
|
144
|
+
malformedLines,
|
|
145
|
+
uniqueHashes: compactedCount,
|
|
146
|
+
removedEntries: removedCount,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Write compacted dictionary
|
|
152
|
+
async function writeCompactedDictionary(dictPath, entries, backup = true) {
|
|
153
|
+
// Create backup if requested
|
|
154
|
+
if (backup) {
|
|
155
|
+
const backupPath = `${dictPath}.backup.${Date.now()}`;
|
|
156
|
+
console.log(`Creating backup: ${backupPath}`);
|
|
157
|
+
fs.copyFileSync(dictPath, backupPath);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Write compacted entries
|
|
161
|
+
console.log(`Writing compacted dictionary: ${dictPath}`);
|
|
162
|
+
const lines = Array.from(entries.values()).map((entry) => JSON.stringify(entry));
|
|
163
|
+
fs.writeFileSync(dictPath, lines.join('\n') + '\n');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Main
|
|
167
|
+
async function main() {
|
|
168
|
+
try {
|
|
169
|
+
const options = parseArgs();
|
|
170
|
+
const dictPath = path.resolve(options.dictPath);
|
|
171
|
+
|
|
172
|
+
console.log('=== LLM Audit Dictionary Compaction ===\n');
|
|
173
|
+
|
|
174
|
+
// Read and compact
|
|
175
|
+
const { entries, stats } = await compactDictionary(dictPath);
|
|
176
|
+
|
|
177
|
+
// Report statistics
|
|
178
|
+
console.log('\nCompaction Statistics:');
|
|
179
|
+
console.log(` Total lines in dictionary: ${stats.totalLines}`);
|
|
180
|
+
console.log(` Malformed lines skipped: ${stats.malformedLines}`);
|
|
181
|
+
console.log(` Unique content hashes: ${stats.uniqueHashes}`);
|
|
182
|
+
console.log(` Redundant entries removed: ${stats.removedEntries}`);
|
|
183
|
+
|
|
184
|
+
const reductionPercent =
|
|
185
|
+
stats.totalLines > 0
|
|
186
|
+
? ((stats.removedEntries / stats.totalLines) * 100).toFixed(1)
|
|
187
|
+
: 0;
|
|
188
|
+
console.log(` Size reduction: ${reductionPercent}%\n`);
|
|
189
|
+
|
|
190
|
+
if (options.dryRun) {
|
|
191
|
+
console.log('DRY RUN: No changes made to dictionary file.');
|
|
192
|
+
console.log(`Would have written ${stats.uniqueHashes} entries.\n`);
|
|
193
|
+
} else {
|
|
194
|
+
// Write compacted dictionary
|
|
195
|
+
await writeCompactedDictionary(dictPath, entries, options.backup);
|
|
196
|
+
console.log('✓ Dictionary compaction complete!\n');
|
|
197
|
+
}
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error('Error:', err.message);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
main();
|