sigrank 1.0.2 → 1.0.4
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/manifest.json +2 -2
- package/omp-cache.mjs +477 -0
- package/package.json +3 -1
package/manifest.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/anthropics/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json",
|
|
3
3
|
"manifest_version": "0.4",
|
|
4
4
|
"name": "sigrank",
|
|
5
|
-
"version": "1.0.
|
|
6
|
-
"description": "Ranks AI coding operators by token cascade efficiency (Yield = cache_read \u00d7 output / input\u00b2).
|
|
5
|
+
"version": "1.0.3",
|
|
6
|
+
"description": "Ranks AI coding operators by token cascade efficiency (Yield = cache_read \u00d7 output / input\u00b2). 25 tools including SigRank Standard record, yield calculation, local token usage pulls, signed submission, and tokscale-powered analytics (market share, developer profiles, model trends, cost analysis, device profiles, MCP usage, competitive intelligence). Privacy-first: runs locally, only four token counts leave the machine.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "SunrisesIllNeverSee",
|
|
9
9
|
"url": "https://github.com/SunrisesIllNeverSee"
|
package/omp-cache.mjs
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* omp-cache.mjs — Privacy-safe incremental SQLite cache for omp session scans.
|
|
3
|
+
*
|
|
4
|
+
* Stage 2 of the OMP Loading Optimization Plan. Wraps the Stage 1 bounded-concurrency
|
|
5
|
+
* scanner with a persistent index at ~/.sigrank-mcp/omp-index.sqlite that stores:
|
|
6
|
+
* - File fingerprints (path, size, mtime_ms)
|
|
7
|
+
* - Usage events (session_id, message_id, timestamp, 4 token pillars)
|
|
8
|
+
* - No transcript text, ever.
|
|
9
|
+
*
|
|
10
|
+
* On a warm run with no changed files, the cache avoids all transcript reads and
|
|
11
|
+
* serves usage records directly from SQLite — turning a ~35s scan into a metadata
|
|
12
|
+
* walk + indexed aggregation.
|
|
13
|
+
*
|
|
14
|
+
* Activation:
|
|
15
|
+
* SIGRANK_OMP_CACHE=1 — use cache, fall back to uncached on error
|
|
16
|
+
* SIGRANK_OMP_CACHE=parity — use cache AND run uncached scan, fail on drift
|
|
17
|
+
* (unset) — uncached (Stage 1 behavior, the default)
|
|
18
|
+
*
|
|
19
|
+
* SQLite access: shells out to the `sqlite3` CLI (same as the devin adapter).
|
|
20
|
+
* No native npm dependency. Works on Node 18+ (the project's minimum).
|
|
21
|
+
*
|
|
22
|
+
* Concurrency: SQLite WAL mode + busy timeout. If the write lock can't be
|
|
23
|
+
* obtained, falls back to uncached scan without damaging the cache.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
27
|
+
import { promisify } from "node:util";
|
|
28
|
+
import { mkdir, stat, unlink } from "node:fs/promises";
|
|
29
|
+
import { join, dirname } from "node:path";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
|
|
32
|
+
const execFileP = promisify(execFileCb);
|
|
33
|
+
|
|
34
|
+
const CACHE_SCHEMA_VERSION = "1";
|
|
35
|
+
const CACHE_PARSER_VERSION = "omp-stage1-v1";
|
|
36
|
+
const BUSY_TIMEOUT_MS = 5_000;
|
|
37
|
+
const SQLITE_MAX_BUFFER = 256 * 1024 * 1024; // 256MB
|
|
38
|
+
|
|
39
|
+
/** Default cache location. */
|
|
40
|
+
export function defaultCachePath() {
|
|
41
|
+
return join(homedir(), ".sigrank-mcp", "omp-index.sqlite");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Run a SQLite command, return parsed JSON rows or [] on error. */
|
|
45
|
+
async function sqliteJson(dbPath, sql, timeoutMs = 10_000) {
|
|
46
|
+
try {
|
|
47
|
+
const { stdout } = await execFileP("sqlite3", ["-json", dbPath, sql], {
|
|
48
|
+
timeout: timeoutMs,
|
|
49
|
+
maxBuffer: SQLITE_MAX_BUFFER,
|
|
50
|
+
});
|
|
51
|
+
return JSON.parse(stdout || "[]");
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Run a SQLite command that doesn't return rows (DDL/DML). Returns success boolean. */
|
|
58
|
+
async function sqliteExec(dbPath, sql, timeoutMs = 30_000) {
|
|
59
|
+
try {
|
|
60
|
+
await execFileP("sqlite3", [dbPath, sql], {
|
|
61
|
+
timeout: timeoutMs,
|
|
62
|
+
maxBuffer: SQLITE_MAX_BUFFER,
|
|
63
|
+
});
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Run multiple SQL statements in a single sqlite3 call (semicolon-separated). */
|
|
71
|
+
async function sqliteExecBatch(dbPath, sql, timeoutMs = 60_000) {
|
|
72
|
+
return sqliteExec(dbPath, sql, timeoutMs);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Check if the cache database exists and has a compatible schema. */
|
|
76
|
+
async function cacheIsHealthy(dbPath) {
|
|
77
|
+
const rows = await sqliteJson(
|
|
78
|
+
dbPath,
|
|
79
|
+
`SELECT key, value FROM metadata WHERE key IN ('schema_version','parser_version');`,
|
|
80
|
+
2_000,
|
|
81
|
+
);
|
|
82
|
+
if (!rows || rows.length < 2) return false;
|
|
83
|
+
const meta = Object.fromEntries(rows.map((r) => [r.key, r.value]));
|
|
84
|
+
return (
|
|
85
|
+
meta.schema_version === CACHE_SCHEMA_VERSION &&
|
|
86
|
+
meta.parser_version === CACHE_PARSER_VERSION
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Initialize the cache database with the schema. */
|
|
91
|
+
async function initCacheDb(dbPath) {
|
|
92
|
+
// Ensure the directory exists with 0700 permissions
|
|
93
|
+
await mkdir(dirname(dbPath), { recursive: true, mode: 0o700 });
|
|
94
|
+
|
|
95
|
+
const sql = `
|
|
96
|
+
PRAGMA journal_mode=WAL;
|
|
97
|
+
PRAGMA busy_timeout=${BUSY_TIMEOUT_MS};
|
|
98
|
+
PRAGMA synchronous=NORMAL;
|
|
99
|
+
|
|
100
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
101
|
+
key TEXT PRIMARY KEY,
|
|
102
|
+
value TEXT NOT NULL
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
CREATE TABLE IF NOT EXISTS roots (
|
|
106
|
+
root TEXT PRIMARY KEY,
|
|
107
|
+
last_seen_at INTEGER NOT NULL
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
111
|
+
path TEXT PRIMARY KEY,
|
|
112
|
+
root TEXT NOT NULL,
|
|
113
|
+
size INTEGER NOT NULL,
|
|
114
|
+
mtime_ms INTEGER NOT NULL,
|
|
115
|
+
scanned_at INTEGER NOT NULL
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
CREATE TABLE IF NOT EXISTS usage_events (
|
|
119
|
+
file_path TEXT NOT NULL,
|
|
120
|
+
session_id TEXT,
|
|
121
|
+
message_id TEXT,
|
|
122
|
+
event_ts TEXT,
|
|
123
|
+
input INTEGER NOT NULL,
|
|
124
|
+
output INTEGER NOT NULL,
|
|
125
|
+
cache_create INTEGER NOT NULL,
|
|
126
|
+
cache_read INTEGER NOT NULL,
|
|
127
|
+
ordinal INTEGER NOT NULL,
|
|
128
|
+
PRIMARY KEY (file_path, ordinal)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
CREATE INDEX IF NOT EXISTS usage_events_dedup
|
|
132
|
+
ON usage_events(session_id, message_id);
|
|
133
|
+
|
|
134
|
+
CREATE INDEX IF NOT EXISTS usage_events_timestamp
|
|
135
|
+
ON usage_events(event_ts);
|
|
136
|
+
|
|
137
|
+
DELETE FROM metadata WHERE key IN ('schema_version', 'parser_version');
|
|
138
|
+
INSERT INTO metadata VALUES ('schema_version', '${CACHE_SCHEMA_VERSION}');
|
|
139
|
+
INSERT INTO metadata VALUES ('parser_version', '${CACHE_PARSER_VERSION}');
|
|
140
|
+
`;
|
|
141
|
+
return sqliteExecBatch(dbPath, sql);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Delete the cache file (corruption recovery). */
|
|
145
|
+
async function deleteCache(dbPath) {
|
|
146
|
+
try {
|
|
147
|
+
await unlink(dbPath);
|
|
148
|
+
} catch {
|
|
149
|
+
// already gone
|
|
150
|
+
}
|
|
151
|
+
// Also clean up WAL and SHM files
|
|
152
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
153
|
+
try {
|
|
154
|
+
await unlink(dbPath + suffix);
|
|
155
|
+
} catch {
|
|
156
|
+
// ignore
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Collect file metadata (path, size, mtime_ms) for all jsonl files under a root. */
|
|
162
|
+
async function collectFileMeta(rootDir, walkFiles, isJsonl, maxFiles) {
|
|
163
|
+
const metas = [];
|
|
164
|
+
for await (const path of walkFiles(rootDir, isJsonl, { n: 0 }, maxFiles)) {
|
|
165
|
+
try {
|
|
166
|
+
const st = await stat(path);
|
|
167
|
+
metas.push({
|
|
168
|
+
path,
|
|
169
|
+
root: rootDir,
|
|
170
|
+
size: st.size,
|
|
171
|
+
mtime_ms: Math.floor(st.mtimeMs),
|
|
172
|
+
});
|
|
173
|
+
} catch {
|
|
174
|
+
// file vanished between walk and stat — skip
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return metas;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Get cached file fingerprints from the database. */
|
|
181
|
+
async function getCachedFiles(dbPath, rootDir) {
|
|
182
|
+
return sqliteJson(
|
|
183
|
+
dbPath,
|
|
184
|
+
`SELECT path, root, size, mtime_ms FROM files WHERE root = '${rootDir.replace(/'/g, "''")}';`,
|
|
185
|
+
5_000,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Compare current file metadata against cached fingerprints.
|
|
190
|
+
* Returns { newFiles, changedFiles, deletedFiles, unchangedFiles }. */
|
|
191
|
+
function diffFiles(currentMetas, cachedRows) {
|
|
192
|
+
const cachedMap = new Map(cachedRows.map((r) => [r.path, r]));
|
|
193
|
+
const currentPaths = new Set(currentMetas.map((m) => m.path));
|
|
194
|
+
|
|
195
|
+
const newFiles = [];
|
|
196
|
+
const changedFiles = [];
|
|
197
|
+
const unchangedFiles = [];
|
|
198
|
+
|
|
199
|
+
for (const m of currentMetas) {
|
|
200
|
+
const cached = cachedMap.get(m.path);
|
|
201
|
+
if (!cached) {
|
|
202
|
+
newFiles.push(m);
|
|
203
|
+
} else if (cached.size !== m.size || cached.mtime_ms !== m.mtime_ms) {
|
|
204
|
+
changedFiles.push(m);
|
|
205
|
+
} else {
|
|
206
|
+
unchangedFiles.push(m);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const deletedFiles = cachedRows
|
|
211
|
+
.filter((r) => !currentPaths.has(r.path))
|
|
212
|
+
.map((r) => r.path);
|
|
213
|
+
|
|
214
|
+
return { newFiles, changedFiles, unchangedFiles, deletedFiles };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Escape a string for SQL single-quoted context. */
|
|
218
|
+
function sqlEscape(s) {
|
|
219
|
+
return String(s ?? "").replace(/'/g, "''");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Format a value for SQL (null for null/undefined, quoted string otherwise). */
|
|
223
|
+
function sqlVal(v) {
|
|
224
|
+
if (v == null) return "NULL";
|
|
225
|
+
return `'${sqlEscape(v)}'`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Update the cache for a set of files: parse them, insert usage events.
|
|
229
|
+
* Uses the Stage 1 parseOmpFile function passed in. */
|
|
230
|
+
async function updateFilesInCache(dbPath, fileMetas, parseOmpFile, readUtf8) {
|
|
231
|
+
for (const meta of fileMetas) {
|
|
232
|
+
const text = await readUtf8(meta.path);
|
|
233
|
+
const records = [...parseOmpFile(text, meta.path)];
|
|
234
|
+
|
|
235
|
+
if (process.env.SIGRANK_OMP_DEBUG) {
|
|
236
|
+
console.error(`[omp-cache] updating ${meta.path}: ${records.length} records, size=${meta.size}, mtime=${meta.mtime_ms}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Build SQL: delete old rows for this file, insert new ones, update fingerprint
|
|
240
|
+
let sql = `
|
|
241
|
+
BEGIN TRANSACTION;
|
|
242
|
+
DELETE FROM usage_events WHERE file_path = '${sqlEscape(meta.path)}';
|
|
243
|
+
DELETE FROM files WHERE path = '${sqlEscape(meta.path)}';
|
|
244
|
+
`;
|
|
245
|
+
|
|
246
|
+
if (records.length > 0) {
|
|
247
|
+
const values = records
|
|
248
|
+
.map((r, i) => {
|
|
249
|
+
return `('${sqlEscape(meta.path)}', ${sqlVal(r.sid)}, ${sqlVal(r.id)}, ${sqlVal(r.ts)}, ${r.input}, ${r.output}, ${r.cacheCreate}, ${r.cacheRead}, ${i})`;
|
|
250
|
+
})
|
|
251
|
+
.join(",\n");
|
|
252
|
+
sql += `INSERT INTO usage_events (file_path, session_id, message_id, event_ts, input, output, cache_create, cache_read, ordinal) VALUES ${values};\n`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
sql += `INSERT INTO files (path, root, size, mtime_ms, scanned_at) VALUES ('${sqlEscape(meta.path)}', '${sqlEscape(meta.root)}', ${meta.size}, ${meta.mtime_ms}, ${Date.now()});\n`;
|
|
256
|
+
sql += `COMMIT;`;
|
|
257
|
+
|
|
258
|
+
const ok = await sqliteExecBatch(dbPath, sql, 10_000);
|
|
259
|
+
if (!ok && process.env.SIGRANK_OMP_DEBUG) {
|
|
260
|
+
console.error(`[omp-cache] SQL FAILED for ${meta.path}`);
|
|
261
|
+
console.error(`[omp-cache] SQL: ${sql.substring(0, 300)}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Remove cached data for deleted files. */
|
|
267
|
+
async function removeDeletedFiles(dbPath, deletedPaths) {
|
|
268
|
+
if (deletedPaths.length === 0) return;
|
|
269
|
+
const pathList = deletedPaths.map((p) => `'${sqlEscape(p)}'`).join(",");
|
|
270
|
+
await sqliteExecBatch(
|
|
271
|
+
dbPath,
|
|
272
|
+
`
|
|
273
|
+
BEGIN TRANSACTION;
|
|
274
|
+
DELETE FROM usage_events WHERE file_path IN (${pathList});
|
|
275
|
+
DELETE FROM files WHERE path IN (${pathList});
|
|
276
|
+
COMMIT;
|
|
277
|
+
`,
|
|
278
|
+
10_000,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Update the roots table to mark this root as seen. */
|
|
283
|
+
async function touchRoot(dbPath, rootDir) {
|
|
284
|
+
await sqliteExecBatch(
|
|
285
|
+
dbPath,
|
|
286
|
+
`INSERT OR REPLACE INTO roots (root, last_seen_at) VALUES ('${sqlEscape(rootDir)}', ${Date.now()});`,
|
|
287
|
+
2_000,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Read all usage events from the cache as records (same shape as ompAdapter.messages). */
|
|
292
|
+
async function* readCachedRecords(dbPath) {
|
|
293
|
+
const rows = await sqliteJson(
|
|
294
|
+
dbPath,
|
|
295
|
+
`SELECT file_path, session_id, message_id, event_ts, input, output, cache_create, cache_read
|
|
296
|
+
FROM usage_events ORDER BY file_path, ordinal;`,
|
|
297
|
+
30_000,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
for (const row of rows) {
|
|
301
|
+
yield {
|
|
302
|
+
id: row.message_id || null,
|
|
303
|
+
sid: row.session_id || null,
|
|
304
|
+
ts: row.event_ts || null,
|
|
305
|
+
input: row.input,
|
|
306
|
+
output: row.output,
|
|
307
|
+
cacheCreate: row.cache_create,
|
|
308
|
+
cacheRead: row.cache_read,
|
|
309
|
+
file: row.file_path,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Run a cached omp scan. If the cache is healthy, does an incremental update
|
|
316
|
+
* (only reparse new/changed files) and yields records from the cache.
|
|
317
|
+
* Falls back to uncached scan on any error.
|
|
318
|
+
*
|
|
319
|
+
* @param {string} rootDir - the omp sessions directory
|
|
320
|
+
* @param {Function} uncachedScan - async generator that yields records (Stage 1)
|
|
321
|
+
* @param {Function} parseOmpFile - Stage 1 parser function
|
|
322
|
+
* @param {Function} readUtf8 - file reader
|
|
323
|
+
* @param {Function} walkFiles - file walker
|
|
324
|
+
* @param {Function} isJsonl - file predicate
|
|
325
|
+
* @param {number} maxFiles - file cap
|
|
326
|
+
* @param {string} cachePath - override cache db path (for tests)
|
|
327
|
+
* @returns {AsyncGenerator} - yields usage records
|
|
328
|
+
*/
|
|
329
|
+
export async function* cachedOmpScan({
|
|
330
|
+
rootDir,
|
|
331
|
+
uncachedScan,
|
|
332
|
+
parseOmpFile,
|
|
333
|
+
readUtf8,
|
|
334
|
+
walkFiles,
|
|
335
|
+
isJsonl,
|
|
336
|
+
maxFiles,
|
|
337
|
+
cachePath,
|
|
338
|
+
}) {
|
|
339
|
+
const dbPath = cachePath || defaultCachePath();
|
|
340
|
+
const parity = process.env.SIGRANK_OMP_CACHE === "parity";
|
|
341
|
+
|
|
342
|
+
// Try to use the cache
|
|
343
|
+
let healthy = await cacheIsHealthy(dbPath);
|
|
344
|
+
if (process.env.SIGRANK_OMP_DEBUG) {
|
|
345
|
+
console.error(`[omp-cache] initial healthy=${healthy} db=${dbPath}`);
|
|
346
|
+
}
|
|
347
|
+
if (!healthy) {
|
|
348
|
+
// If the db file exists but is corrupt/incompatible, delete it first
|
|
349
|
+
// (initCacheDb can't write to a non-SQLite file)
|
|
350
|
+
try {
|
|
351
|
+
const st = await stat(dbPath);
|
|
352
|
+
if (st.isFile() && st.size > 0) {
|
|
353
|
+
if (process.env.SIGRANK_OMP_DEBUG) {
|
|
354
|
+
console.error(`[omp-cache] deleting existing file (size=${st.size})`);
|
|
355
|
+
}
|
|
356
|
+
await deleteCache(dbPath);
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
// file doesn't exist — fine
|
|
360
|
+
}
|
|
361
|
+
// Try to initialize
|
|
362
|
+
const ok = await initCacheDb(dbPath);
|
|
363
|
+
if (process.env.SIGRANK_OMP_DEBUG) {
|
|
364
|
+
console.error(`[omp-cache] initCacheDb ok=${ok}`);
|
|
365
|
+
}
|
|
366
|
+
if (!ok) {
|
|
367
|
+
// Can't create cache — fall back to uncached
|
|
368
|
+
yield* uncachedScan();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
healthy = await cacheIsHealthy(dbPath);
|
|
372
|
+
if (process.env.SIGRANK_OMP_DEBUG) {
|
|
373
|
+
console.error(`[omp-cache] post-init healthy=${healthy}`);
|
|
374
|
+
}
|
|
375
|
+
if (!healthy) {
|
|
376
|
+
yield* uncachedScan();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
// Collect current file metadata
|
|
383
|
+
const currentMetas = await collectFileMeta(rootDir, walkFiles, isJsonl, maxFiles);
|
|
384
|
+
|
|
385
|
+
// Get cached fingerprints
|
|
386
|
+
const cachedRows = await getCachedFiles(dbPath, rootDir);
|
|
387
|
+
|
|
388
|
+
// Diff
|
|
389
|
+
const { newFiles, changedFiles, unchangedFiles, deletedFiles } = diffFiles(
|
|
390
|
+
currentMetas,
|
|
391
|
+
cachedRows,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
// Update cache for new and changed files
|
|
395
|
+
const toUpdate = [...newFiles, ...changedFiles];
|
|
396
|
+
if (toUpdate.length > 0) {
|
|
397
|
+
await updateFilesInCache(dbPath, toUpdate, parseOmpFile, readUtf8);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Remove deleted files
|
|
401
|
+
if (deletedFiles.length > 0) {
|
|
402
|
+
await removeDeletedFiles(dbPath, deletedFiles);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Mark root as seen
|
|
406
|
+
await touchRoot(dbPath, rootDir);
|
|
407
|
+
|
|
408
|
+
// Yield records from cache
|
|
409
|
+
yield* readCachedRecords(dbPath);
|
|
410
|
+
|
|
411
|
+
// Parity mode: also run uncached and compare
|
|
412
|
+
if (parity) {
|
|
413
|
+
const cachedRecords = [...readCachedRecords(dbPath)];
|
|
414
|
+
const uncachedRecords = [];
|
|
415
|
+
for await (const r of uncachedScan()) {
|
|
416
|
+
uncachedRecords.push(r);
|
|
417
|
+
}
|
|
418
|
+
// Compare totals (dedup happens downstream in tokenpull, so we compare
|
|
419
|
+
// the raw record sums — they should match since the cache stores all
|
|
420
|
+
// records from all files)
|
|
421
|
+
const sumFields = (records, field) =>
|
|
422
|
+
records.reduce((s, r) => s + (r[field] || 0), 0);
|
|
423
|
+
|
|
424
|
+
for (const field of ["input", "output", "cacheCreate", "cacheRead"]) {
|
|
425
|
+
const cachedSum = sumFields(cachedRecords, field);
|
|
426
|
+
const uncachedSum = sumFields(uncachedRecords, field);
|
|
427
|
+
if (cachedSum !== uncachedSum) {
|
|
428
|
+
console.error(
|
|
429
|
+
`[omp-cache] PARITY DRIFT: ${field} cached=${cachedSum} uncached=${uncachedSum} (diff=${cachedSum - uncachedSum})`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
} catch (err) {
|
|
435
|
+
// Any error during cache operations — fall back to uncached
|
|
436
|
+
// Don't delete the cache; it may be partially valid and recoverable
|
|
437
|
+
if (process.env.SIGRANK_OMP_DEBUG) {
|
|
438
|
+
console.error(`[omp-cache] fallback to uncached: ${err.message}`);
|
|
439
|
+
}
|
|
440
|
+
yield* uncachedScan();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Check if the omp cache is enabled via environment variable. */
|
|
446
|
+
export function ompCacheEnabled() {
|
|
447
|
+
const v = process.env.SIGRANK_OMP_CACHE;
|
|
448
|
+
return v === "1" || v === "parity";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Rebuild the omp cache from scratch (deletes existing cache, forces full scan). */
|
|
452
|
+
export async function rebuildOmpCache(cachePath) {
|
|
453
|
+
const dbPath = cachePath || defaultCachePath();
|
|
454
|
+
await deleteCache(dbPath);
|
|
455
|
+
// The next scan will rebuild it
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Get cache statistics for diagnostics. */
|
|
459
|
+
export async function getCacheStats(cachePath) {
|
|
460
|
+
const dbPath = cachePath || defaultCachePath();
|
|
461
|
+
const healthy = await cacheIsHealthy(dbPath);
|
|
462
|
+
if (!healthy) return { healthy: false, files: 0, events: 0 };
|
|
463
|
+
|
|
464
|
+
const fileRows = await sqliteJson(dbPath, `SELECT COUNT(*) as n FROM files;`, 2_000);
|
|
465
|
+
const eventRows = await sqliteJson(
|
|
466
|
+
dbPath,
|
|
467
|
+
`SELECT COUNT(*) as n FROM usage_events;`,
|
|
468
|
+
5_000,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
healthy: true,
|
|
473
|
+
files: fileRows[0]?.n || 0,
|
|
474
|
+
events: eventRows[0]?.n || 0,
|
|
475
|
+
path: dbPath,
|
|
476
|
+
};
|
|
477
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigrank",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",
|
|
5
5
|
"description": "SigRank MCP server — AI operator evaluation and token cascade efficiency tools. The yield cascade + live leaderboard as MCP tools any agent can call.",
|
|
6
6
|
"mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"start": "node index.mjs",
|
|
27
27
|
"test": "node test.mjs && node __tests__/sign.test.mjs && node --test __tests__/cascade.test.mjs __tests__/badges.test.mjs __tests__/tui-input.test.mjs __tests__/standard-record.test.mjs __tests__/standard-cli.test.mjs",
|
|
28
|
+
"test:packaged": "node scripts/packed-artifact-smoke.mjs",
|
|
28
29
|
"sync:platforms": "node scripts/sync-platforms.mjs",
|
|
29
30
|
"sync:spine": "node scripts/sync-spine.mjs",
|
|
30
31
|
"spine:check": "node scripts/sync-spine.mjs --check",
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"connect.mjs",
|
|
52
53
|
"submit.mjs",
|
|
53
54
|
"preflight.mjs",
|
|
55
|
+
"omp-cache.mjs",
|
|
54
56
|
"adapters.mjs",
|
|
55
57
|
"tokenpull.mjs",
|
|
56
58
|
"tui.mjs",
|