sigrank 1.0.3 → 1.0.5

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/bin/sigrank.mjs CHANGED
@@ -69,9 +69,13 @@ async function exportStandardRecord() {
69
69
  const platform = flag("platform", "claude");
70
70
  const window = flag("window", "30d");
71
71
 
72
- const explicit = ["input", "output", "cache-write", "cache-read"].every(
73
- (name) => flag(name) !== undefined,
74
- );
72
+ const explicitFlags = ["input", "output", "cache-write", "cache-read"];
73
+ const hasExplicit = explicitFlags.some((name) => flag(name) !== undefined);
74
+ const explicit = ["input", "output"].every((name) => flag(name) !== undefined);
75
+
76
+ if (hasExplicit && !explicit) {
77
+ throw new Error("Explicit Standard export requires both --input and --output.");
78
+ }
75
79
 
76
80
  let pillars;
77
81
  let sourcePlatform = platform;
@@ -80,8 +84,8 @@ async function exportStandardRecord() {
80
84
  pillars = {
81
85
  input: Number(flag("input")),
82
86
  output: Number(flag("output")),
83
- cache_write: Number(flag("cache-write")),
84
- cache_read: Number(flag("cache-read")),
87
+ cache_write: flag("cache-write") === undefined ? null : Number(flag("cache-write")),
88
+ cache_read: flag("cache-read") === undefined ? null : Number(flag("cache-read")),
85
89
  };
86
90
  } else {
87
91
  const pulled = await callTool("tokenpull", { platform });
@@ -97,14 +101,14 @@ async function exportStandardRecord() {
97
101
  pillars = {
98
102
  input: selected.pillars.input,
99
103
  output: selected.pillars.output,
100
- cache_write: selected.pillars.cacheCreate,
101
- cache_read: selected.pillars.cacheRead,
104
+ cache_write: selected.pillars.cacheCreate ?? null,
105
+ cache_read: selected.pillars.cacheRead ?? null,
102
106
  };
103
107
  }
104
108
 
105
109
  for (const [key, value] of Object.entries(pillars)) {
106
- if (!Number.isFinite(value) || value < 0) {
107
- throw new Error(`${key} must be a non-negative finite number.`);
110
+ if (value !== null && (!Number.isFinite(value) || !Number.isInteger(value) || value < 0)) {
111
+ throw new Error(`${key} must be a non-negative integer token count or null.`);
108
112
  }
109
113
  }
110
114
 
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
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.2",
5
+ "version": "1.0.4",
6
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",
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",
3
+ "version": "1.0.5",
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",
@@ -6,7 +6,7 @@ export const SIGRANK_STANDARD_VERSION = "sigrank/0.1-draft";
6
6
  export const TOOL_DEF = {
7
7
  name: "get_sigrank_standard_record",
8
8
  description:
9
- "Build a SigRank Standard v0.1-draft portable operator record from four token pillars. Computes the canonical cascade locally through @sigrank/cascade and returns versioned telemetry plus the normative core metrics: Yield, Leverage, Velocity, SNR, and 10xDEV. No data is submitted or persisted. The core record contains token telemetry only and does not require prompt text, code, or response content.",
9
+ "Build a SigRank Standard v0.1-draft portable operator record from available token telemetry. Input and output are required; unavailable cache telemetry remains null. Computes the canonical cascade locally through @sigrank/cascade and returns the normative core metrics: Yield, Leverage, Velocity, SNR, and 10xDEV. No data is submitted or persisted.",
10
10
  annotations: {
11
11
  title: "Export SigRank standard record",
12
12
  ...ANNOTATIONS.readOnlyHint,
@@ -17,26 +17,29 @@ export const TOOL_DEF = {
17
17
  inputSchema: {
18
18
  type: "object",
19
19
  additionalProperties: false,
20
- required: ["input", "output", "cache_write", "cache_read"],
20
+ required: ["input", "output"],
21
21
  properties: {
22
- input: { type: "number", minimum: 0, description: "Fresh input tokens." },
23
- output: { type: "number", minimum: 0, description: "Output tokens." },
24
- cache_write: { type: "number", minimum: 0, description: "Cache-write / cache-creation tokens." },
25
- cache_read: { type: "number", minimum: 0, description: "Cache-read tokens." },
22
+ input: { type: "integer", minimum: 0, description: "Fresh input tokens." },
23
+ output: { type: "integer", minimum: 0, description: "Output tokens." },
24
+ cache_write: { type: ["integer", "null"], minimum: 0, description: "Cache-write / cache-creation tokens, or null when unavailable." },
25
+ cache_read: { type: ["integer", "null"], minimum: 0, description: "Cache-read tokens, or null when unavailable." },
26
26
  provider: { type: "string", description: "Optional provider identifier." },
27
27
  model: { type: "string", description: "Optional model identifier." },
28
28
  tool: { type: "string", description: "Optional tool/client identifier." },
29
- timestamp: { type: "string", description: "Optional ISO-8601 timestamp. Defaults to the current time." }
29
+ timestamp: { type: "string", format: "date-time", description: "Optional ISO-8601 timestamp. Defaults to the current time." }
30
30
  }
31
31
  },
32
32
  outputSchema: {
33
33
  type: "object",
34
- required: ["spec", "timestamp", "source", "telemetry", "metrics"],
34
+ additionalProperties: false,
35
+ required: ["spec", "timestamp", "source", "telemetry", "metrics", "warnings"],
35
36
  properties: {
36
- spec: { type: "string", description: "SigRank specification version." },
37
- timestamp: { type: "string", description: "ISO-8601 record timestamp." },
37
+ spec: { const: SIGRANK_STANDARD_VERSION, description: "SigRank specification version." },
38
+ timestamp: { type: "string", format: "date-time", description: "ISO-8601 record timestamp." },
38
39
  source: {
39
40
  type: "object",
41
+ additionalProperties: false,
42
+ required: ["provider", "model", "tool"],
40
43
  properties: {
41
44
  provider: { type: "string" },
42
45
  model: { type: "string" },
@@ -45,15 +48,19 @@ export const TOOL_DEF = {
45
48
  },
46
49
  telemetry: {
47
50
  type: "object",
51
+ additionalProperties: false,
52
+ required: ["input", "output", "cache_write", "cache_read"],
48
53
  properties: {
49
- input: { type: "number" },
50
- output: { type: "number" },
51
- cache_write: { type: "number" },
52
- cache_read: { type: "number" }
54
+ input: { type: "integer", minimum: 0 },
55
+ output: { type: "integer", minimum: 0 },
56
+ cache_write: { type: ["integer", "null"], minimum: 0 },
57
+ cache_read: { type: ["integer", "null"], minimum: 0 }
53
58
  }
54
59
  },
55
60
  metrics: {
56
61
  type: "object",
62
+ additionalProperties: false,
63
+ required: ["yield", "leverage", "velocity", "snr", "dev10x"],
57
64
  properties: {
58
65
  yield: { type: ["number", "null"] },
59
66
  leverage: { type: ["number", "null"] },
@@ -67,24 +74,48 @@ export const TOOL_DEF = {
67
74
  }
68
75
  };
69
76
 
70
- export async function handleGetSigRankStandardRecord(args) {
71
- const input = Number(args?.input);
72
- const output = Number(args?.output);
73
- const cacheWrite = Number(args?.cache_write);
74
- const cacheRead = Number(args?.cache_read);
77
+ const ISO_DATE_TIME =
78
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
75
79
 
76
- if (![input, output, cacheWrite, cacheRead].every(Number.isFinite)) {
77
- throw new Error("get_sigrank_standard_record requires four finite numeric token pillars.");
80
+ function tokenCount(value, name, required) {
81
+ if (value === null || value === undefined) {
82
+ if (required) throw new Error(`${name} is required.`);
83
+ return null;
78
84
  }
79
- if ([input, output, cacheWrite, cacheRead].some((v) => v < 0)) {
80
- throw new Error("get_sigrank_standard_record requires non-negative token pillars.");
85
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
86
+ throw new Error(`${name} must be a non-negative integer token count.`);
87
+ }
88
+ if (value < 0) throw new Error(`${name} must be a non-negative integer token count.`);
89
+ return value;
90
+ }
91
+
92
+ function recordTimestamp(value) {
93
+ if (value === undefined) return new Date().toISOString();
94
+ if (typeof value !== "string" || !ISO_DATE_TIME.test(value) || !Number.isFinite(Date.parse(value))) {
95
+ throw new Error("timestamp must be a valid ISO-8601 date-time.");
81
96
  }
97
+ return value;
98
+ }
99
+
100
+ export async function handleGetSigRankStandardRecord(args) {
101
+ const input = tokenCount(args?.input, "input", true);
102
+ const output = tokenCount(args?.output, "output", true);
103
+ const cacheWrite = tokenCount(args?.cache_write, "cache_write", false);
104
+ const cacheRead = tokenCount(args?.cache_read, "cache_read", false);
82
105
 
83
- const c = cascade({ input, output, cacheCreate: cacheWrite, cacheRead });
106
+ const c = cascade({
107
+ input,
108
+ output,
109
+ cacheCreate: cacheWrite ?? 0,
110
+ cacheRead: cacheRead ?? 0,
111
+ });
112
+ const warnings = [...(c.warnings || [])];
113
+ if (cacheWrite === null) warnings.push("cache_write is unavailable; 10xDEV is undefined.");
114
+ if (cacheRead === null) warnings.push("cache_read is unavailable; Yield, Leverage, and 10xDEV are undefined.");
84
115
 
85
116
  return {
86
117
  spec: SIGRANK_STANDARD_VERSION,
87
- timestamp: args?.timestamp || new Date().toISOString(),
118
+ timestamp: recordTimestamp(args?.timestamp),
88
119
  source: {
89
120
  provider: args?.provider || "unknown",
90
121
  model: args?.model || "unknown",
@@ -97,12 +128,12 @@ export async function handleGetSigRankStandardRecord(args) {
97
128
  cache_read: cacheRead,
98
129
  },
99
130
  metrics: {
100
- yield: c.yield,
101
- leverage: c.leverage,
131
+ yield: cacheRead === null ? null : c.yield,
132
+ leverage: cacheRead === null ? null : c.leverage,
102
133
  velocity: c.velocity,
103
134
  snr: c.snr,
104
- dev10x: c.dev10x,
135
+ dev10x: cacheWrite === null || cacheRead === null ? null : c.dev10x,
105
136
  },
106
- warnings: c.warnings || [],
137
+ warnings,
107
138
  };
108
139
  }