sigmap 7.1.0 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,24 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [7.2.0] — 2026-06-17
14
+
15
+ Minor release — release-pipeline robustness. Hardens the bundle/release machinery that produced the v7.1.0 binary failure, with no user-facing CLI changes.
16
+
17
+ ### Added
18
+ - **Bundle integrity check (#266):** `scripts/check-bundle.mjs` verifies every `src/` module is registered in `gen-context.js` `__factories` (the standalone/SEA-binary code path). Runs in CI on every PR (Node 18/20/22) and in `prepublishOnly`; `--fix` inserts missing factories from source. `build-binary.mjs` reuses the same check. Catches — before merge — the gap that broke the v7.1.0 binaries.
19
+ - **version.json metadata gate (#268):** `scripts/check-version-meta.mjs` derives `mcp_tools` (from `src/mcp/tools.js`) and `tests` (test-file count) and fails on drift; wired into `prepublishOnly`. `languages` stays editorial. Corrected stale `tests` count.
20
+ - **Standalone-bundle smoke test (#274):** runs `gen-context.js` from a temp dir with no `src/` present (the binary path) — `generate` + `--health` + `gain` — in the Node 18/20/22 matrix. Functional complement to the presence check.
21
+ - **`docs/RELEASING.md` (#274):** documents the release flow, branch model, tag triggers, and the prepublish/CI gates.
22
+
23
+ ### Fixed
24
+ - **`--health` clarity (#270):** the per-repo "extractor coverage" line (languages present in this repo ÷ supported) read as contradictory beside a 100/100 score. Relabeled as informational ("repo languages … not scored") with a "score basis" line. The `--health --json` `extractorCoverage` field is unchanged.
25
+
26
+ ### Changed
27
+ - **Repo declutter (#272):** removed the committed, unreferenced `TESTING_IMPORT_GRAPH.md` (gitignored); stale planning docs archived out of the repo. `PROJECT_MAP.md` kept (live generated artifact).
28
+
29
+ ---
30
+
13
31
  ## [7.1.0] — 2026-06-16
14
32
 
15
33
  Minor release — a token-savings dashboard in the terminal, plus domain and sponsorship docs.
package/gen-context.js CHANGED
@@ -6254,7 +6254,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
6254
6254
 
6255
6255
  const SERVER_INFO = {
6256
6256
  name: 'sigmap',
6257
- version: '7.1.0',
6257
+ version: '7.2.0',
6258
6258
  description: 'SigMap MCP server — code signatures on demand',
6259
6259
  };
6260
6260
 
@@ -7023,12 +7023,15 @@ __factories["./src/tracking/logger"] = function(module, exports) {
7023
7023
  * config.tracking: true (gen-context.config.json)
7024
7024
  * --track CLI flag
7025
7025
  */
7026
-
7026
+
7027
7027
  const fs = require('fs');
7028
7028
  const path = require('path');
7029
-
7029
+
7030
7030
  const LOG_FILE = path.join('.context', 'usage.ndjson');
7031
-
7031
+ // Dedicated log for the `gain` dashboard (extended schema). Kept separate from
7032
+ // usage.ndjson so the legacy health/nudge history never collides with it.
7033
+ const GAIN_FILE = path.join('.context', 'gain.ndjson');
7034
+
7032
7035
  /**
7033
7036
  * Append one run entry to the usage log.
7034
7037
  * @param {object} entry - Run metrics from runGenerate()
@@ -7039,7 +7042,7 @@ __factories["./src/tracking/logger"] = function(module, exports) {
7039
7042
  const logPath = path.join(cwd, LOG_FILE);
7040
7043
  const dir = path.dirname(logPath);
7041
7044
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
7042
-
7045
+
7043
7046
  const record = {
7044
7047
  ts: new Date().toISOString(),
7045
7048
  version: entry.version || '0.9.0',
@@ -7053,14 +7056,14 @@ __factories["./src/tracking/logger"] = function(module, exports) {
7053
7056
  overBudget: entry.overBudget || false,
7054
7057
  budgetLimit: entry.budgetLimit || 6000,
7055
7058
  };
7056
-
7059
+
7057
7060
  fs.appendFileSync(logPath, JSON.stringify(record) + '\n', 'utf8');
7058
7061
  } catch (err) {
7059
7062
  // Never crash the main process — tracking is optional
7060
7063
  process.stderr.write(`[sigmap] tracking: could not write log: ${err.message}\n`);
7061
7064
  }
7062
7065
  }
7063
-
7066
+
7064
7067
  /**
7065
7068
  * Read and parse all usage log entries.
7066
7069
  * @param {string} cwd - Project root (absolute path)
@@ -7082,7 +7085,26 @@ __factories["./src/tracking/logger"] = function(module, exports) {
7082
7085
  return [];
7083
7086
  }
7084
7087
  }
7085
-
7088
+
7089
+ /**
7090
+ * Read and parse all `gain` dashboard records (oldest first).
7091
+ * @param {string} cwd
7092
+ * @returns {object[]}
7093
+ */
7094
+ function readGainLog(cwd) {
7095
+ try {
7096
+ const logPath = path.join(cwd, GAIN_FILE);
7097
+ if (!fs.existsSync(logPath)) return [];
7098
+ return fs.readFileSync(logPath, 'utf8')
7099
+ .split('\n')
7100
+ .filter(Boolean)
7101
+ .map((line) => { try { return JSON.parse(line); } catch (_) { return null; } })
7102
+ .filter(Boolean);
7103
+ } catch (_) {
7104
+ return [];
7105
+ }
7106
+ }
7107
+
7086
7108
  /**
7087
7109
  * Compute summary statistics from an array of log records.
7088
7110
  * @param {object[]} entries
@@ -7102,13 +7124,13 @@ __factories["./src/tracking/logger"] = function(module, exports) {
7102
7124
  overBudgetRuns: 0,
7103
7125
  };
7104
7126
  }
7105
-
7127
+
7106
7128
  const reductions = entries.map((e) => e.reductionPct || 0);
7107
7129
  const finals = entries.map((e) => e.finalTokens || 0);
7108
7130
  const raws = entries.map((e) => e.rawTokens || 0);
7109
-
7131
+
7110
7132
  const avg = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
7111
-
7133
+
7112
7134
  return {
7113
7135
  totalRuns: entries.length,
7114
7136
  avgReductionPct: parseFloat(avg(reductions).toFixed(1)),
@@ -7121,8 +7143,524 @@ __factories["./src/tracking/logger"] = function(module, exports) {
7121
7143
  overBudgetRuns: entries.filter((e) => e.overBudget).length,
7122
7144
  };
7123
7145
  }
7146
+
7147
+ /**
7148
+ * Whether `gain` savings capture is enabled. Default: ON (privacy-safe,
7149
+ * local-only, counts only — no paths, source, or query text). This is
7150
+ * intentionally decoupled from the legacy `config.tracking` flag (which gates
7151
+ * the usage.ndjson health log and defaults OFF). Opt out of gain capture via:
7152
+ * config.gainTracking === false · --no-track · SIGMAP_NO_TRACK=1
7153
+ * @param {object} [config]
7154
+ * @param {string[]} [argv]
7155
+ * @returns {boolean}
7156
+ */
7157
+ function isTrackingEnabled(config, argv) {
7158
+ const a = argv || (typeof process !== 'undefined' ? process.argv : []);
7159
+ if (process.env && process.env.SIGMAP_NO_TRACK) return false;
7160
+ if (a && a.includes('--no-track')) return false;
7161
+ if (config && config.gainTracking === false) return false;
7162
+ return true;
7163
+ }
7164
+
7165
+ /**
7166
+ * Append one operation to the usage log using the extended `gain` schema.
7167
+ * Reuses the same NDJSON file as logRun and is tolerant of partial input.
7168
+ * Never throws — tracking must never break the main process.
7169
+ *
7170
+ * @param {object} entry
7171
+ * @param {string} entry.op e.g. 'ask' | 'generate' | 'query' | 'mcp:get_map'
7172
+ * @param {number} entry.baselineTokens whole-file / candidate baseline (counterfactual)
7173
+ * @param {number} entry.actualTokens tokens SigMap actually emitted
7174
+ * @param {number} [entry.durationMs]
7175
+ * @param {string} [entry.model]
7176
+ * @param {string} [entry.version]
7177
+ * @param {string} cwd
7178
+ */
7179
+ function recordUsage(entry, cwd) {
7180
+ try {
7181
+ const logPath = path.join(cwd, GAIN_FILE);
7182
+ const dir = path.dirname(logPath);
7183
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
7184
+
7185
+ const baseline = Math.max(0, Number(entry.baselineTokens) || 0);
7186
+ const actual = Math.max(0, Number(entry.actualTokens) || 0);
7187
+ const saved = Math.max(0, baseline - actual);
7188
+ const record = {
7189
+ ts: new Date().toISOString(),
7190
+ v: entry.version || '0.9.0',
7191
+ op: entry.op || 'generate',
7192
+ baselineTokens: baseline,
7193
+ actualTokens: actual,
7194
+ savedTokens: saved,
7195
+ savedPct: baseline > 0 ? parseFloat(((saved / baseline) * 100).toFixed(1)) : 0,
7196
+ durationMs: Math.max(0, Math.round(Number(entry.durationMs) || 0)),
7197
+ model: entry.model || null,
7198
+ ok: entry.ok !== false,
7199
+ };
7200
+ fs.appendFileSync(logPath, JSON.stringify(record) + '\n', 'utf8');
7201
+ } catch (err) {
7202
+ // Never crash the main process — tracking is optional.
7203
+ if (process.stderr) process.stderr.write(`[sigmap] tracking: could not write log: ${err.message}\n`);
7204
+ }
7205
+ }
7206
+
7207
+ module.exports = { logRun, recordUsage, readLog, readGainLog, summarize, isTrackingEnabled, GAIN_FILE };
7208
+
7209
+ };
7210
+
7211
+ // ── ./src/tracking/pricing ──
7212
+ __factories["./src/tracking/pricing"] = function(module, exports) {
7213
+
7214
+ /**
7215
+ * SigMap pricing table — input-token $/Mtok assumptions for the `gain` dashboard.
7216
+ *
7217
+ * These are ASSUMPTIONS used only to translate "tokens saved" into an estimated
7218
+ * dollar figure. They are deliberately conservative and configurable via
7219
+ * --model <name> or config.pricingModel
7220
+ * The `gain` views always print the model + rate inline so the $ is never
7221
+ * presented as exact. Zero npm dependencies.
7222
+ */
7223
+
7224
+ // USD per 1,000,000 input tokens.
7225
+ const PRICES = {
7226
+ 'claude-sonnet': 3.0,
7227
+ 'claude-opus': 15.0,
7228
+ 'claude-haiku': 0.8,
7229
+ 'gpt-4o': 2.5,
7230
+ 'gpt-4o-mini': 0.15,
7231
+ 'gemini-1.5-pro': 1.25,
7232
+ 'gemini-1.5-flash': 0.075,
7233
+ };
7234
+
7235
+ const DEFAULT_MODEL = 'claude-sonnet';
7236
+
7237
+ /**
7238
+ * Resolve a price (USD per token) for a model name.
7239
+ * @param {string} [model]
7240
+ * @returns {{ model: string, perMtok: number, perToken: number }}
7241
+ */
7242
+ function resolvePrice(model) {
7243
+ const key = (model || DEFAULT_MODEL).toLowerCase();
7244
+ const perMtok = PRICES[key] != null ? PRICES[key] : PRICES[DEFAULT_MODEL];
7245
+ const resolved = PRICES[key] != null ? key : DEFAULT_MODEL;
7246
+ return { model: resolved, perMtok, perToken: perMtok / 1_000_000 };
7247
+ }
7248
+
7249
+ /** @returns {string[]} known model keys */
7250
+ function listModels() {
7251
+ return Object.keys(PRICES);
7252
+ }
7253
+
7254
+ module.exports = { PRICES, DEFAULT_MODEL, resolvePrice, listModels };
7255
+
7256
+ };
7257
+
7258
+ // ── ./src/tracking/aggregate ──
7259
+ __factories["./src/tracking/aggregate"] = function(module, exports) {
7260
+
7261
+ /**
7262
+ * SigMap usage aggregation for the `gain` dashboard.
7263
+ *
7264
+ * Pure, zero-dependency functions that turn raw NDJSON usage records into the
7265
+ * totals / by-operation / time-bucket shapes the terminal renderer consumes.
7266
+ *
7267
+ * Tolerant of BOTH schemas:
7268
+ * - new: { op, baselineTokens, actualTokens, savedTokens, savedPct, durationMs, model }
7269
+ * - legacy: { rawTokens, finalTokens, reductionPct } (from logger.js v0.9)
7270
+ *
7271
+ * "saved" is a counterfactual estimate (baseline − actual), never a measured
7272
+ * delta. Callers are responsible for labeling it as such in the UI.
7273
+ */
7274
+
7275
+ const { resolvePrice } = __require('./src/tracking/pricing');
7276
+
7277
+ const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
7278
+
7279
+ /**
7280
+ * Normalize one raw record into a canonical shape.
7281
+ * @param {object} rec
7282
+ */
7283
+ function normalize(rec) {
7284
+ const baseline = num(rec.baselineTokens != null ? rec.baselineTokens : rec.rawTokens);
7285
+ const actual = num(rec.actualTokens != null ? rec.actualTokens : rec.finalTokens);
7286
+ const saved = rec.savedTokens != null ? num(rec.savedTokens) : Math.max(0, baseline - actual);
7287
+ const savedPct = rec.savedPct != null
7288
+ ? num(rec.savedPct)
7289
+ : rec.reductionPct != null
7290
+ ? num(rec.reductionPct)
7291
+ : baseline > 0 ? (saved / baseline) * 100 : 0;
7292
+ return {
7293
+ ts: rec.ts || null,
7294
+ op: normalizeOp(rec.op),
7295
+ baseline,
7296
+ actual,
7297
+ saved,
7298
+ savedPct: clamp(savedPct, 0, 100),
7299
+ durationMs: num(rec.durationMs),
7300
+ model: rec.model || null,
7301
+ };
7302
+ }
7303
+
7304
+ function normalizeOp(op) {
7305
+ if (!op) return 'generate';
7306
+ return String(op);
7307
+ }
7308
+
7309
+ function num(v) {
7310
+ const n = Number(v);
7311
+ return Number.isFinite(n) ? n : 0;
7312
+ }
7313
+
7314
+ /**
7315
+ * Parse a --since value into a cutoff Date (or null for "all time").
7316
+ * Accepts: "7d", "30d", "12h", or an ISO date "2026-06-01".
7317
+ * @param {string} since
7318
+ * @param {number} [nowMs] - injectable clock for tests
7319
+ * @returns {Date|null}
7320
+ */
7321
+ function parseSince(since, nowMs) {
7322
+ if (!since) return null;
7323
+ const now = nowMs != null ? nowMs : Date.now();
7324
+ const rel = /^(\d+)([dhw])$/.exec(String(since).trim());
7325
+ if (rel) {
7326
+ const n = parseInt(rel[1], 10);
7327
+ const unit = rel[2];
7328
+ const ms = unit === 'h' ? 3.6e6 : unit === 'w' ? 6.048e8 : 8.64e7;
7329
+ return new Date(now - n * ms);
7330
+ }
7331
+ const d = new Date(since);
7332
+ return Number.isNaN(d.getTime()) ? null : d;
7333
+ }
7334
+
7335
+ /**
7336
+ * Bucket records by calendar granularity.
7337
+ * @param {object[]} records - normalized records
7338
+ * @param {'day'|'week'|'month'} granularity
7339
+ * @returns {Array<{key,count,baseline,actual,saved,savedPct,ms}>} ascending by key
7340
+ */
7341
+ function bucketBy(records, granularity) {
7342
+ const map = new Map();
7343
+ for (const r of records) {
7344
+ if (!r.ts) continue;
7345
+ const key = bucketKey(r.ts, granularity);
7346
+ if (!key) continue;
7347
+ let b = map.get(key);
7348
+ if (!b) { b = { key, count: 0, baseline: 0, actual: 0, saved: 0, ms: 0 }; map.set(key, b); }
7349
+ b.count += 1;
7350
+ b.baseline += r.baseline;
7351
+ b.actual += r.actual;
7352
+ b.saved += r.saved;
7353
+ b.ms += r.durationMs;
7354
+ }
7355
+ return [...map.values()]
7356
+ .map((b) => ({ ...b, savedPct: b.baseline > 0 ? clamp((b.saved / b.baseline) * 100, 0, 100) : 0 }))
7357
+ .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
7358
+ }
7359
+
7360
+ function bucketKey(ts, granularity) {
7361
+ const d = new Date(ts);
7362
+ if (Number.isNaN(d.getTime())) return null;
7363
+ const y = d.getUTCFullYear();
7364
+ const m = String(d.getUTCMonth() + 1).padStart(2, '0');
7365
+ const day = String(d.getUTCDate()).padStart(2, '0');
7366
+ if (granularity === 'month') return `${y}-${m}`;
7367
+ if (granularity === 'week') {
7368
+ // ISO-ish: key by the Monday (UTC) of that week.
7369
+ const tmp = new Date(Date.UTC(y, d.getUTCMonth(), d.getUTCDate()));
7370
+ const dow = (tmp.getUTCDay() + 6) % 7; // 0 = Monday
7371
+ tmp.setUTCDate(tmp.getUTCDate() - dow);
7372
+ return `${tmp.getUTCFullYear()}-${String(tmp.getUTCMonth() + 1).padStart(2, '0')}-${String(tmp.getUTCDate()).padStart(2, '0')}`;
7373
+ }
7374
+ return `${y}-${m}-${day}`;
7375
+ }
7376
+
7377
+ /**
7378
+ * Full aggregation for the `gain` dashboard.
7379
+ * @param {object[]} rawRecords
7380
+ * @param {object} [opts]
7381
+ * @param {string} [opts.model] pricing model
7382
+ * @param {string} [opts.since] window filter
7383
+ * @param {number} [opts.top] limit byOp rows (0 = all)
7384
+ * @param {number} [opts.nowMs] injectable clock
7385
+ * @returns {object}
7386
+ */
7387
+ function aggregate(rawRecords, opts = {}) {
7388
+ const price = resolvePrice(opts.model);
7389
+ const cutoff = parseSince(opts.since, opts.nowMs);
7390
+
7391
+ let records = (rawRecords || []).map(normalize);
7392
+ if (cutoff) records = records.filter((r) => r.ts && new Date(r.ts) >= cutoff);
7393
+
7394
+ const totals = {
7395
+ count: records.length,
7396
+ baseline: 0,
7397
+ actual: 0,
7398
+ saved: 0,
7399
+ totalMs: 0,
7400
+ savedPct: 0,
7401
+ avgMs: 0,
7402
+ usdSaved: 0,
7403
+ firstTs: null,
7404
+ lastTs: null,
7405
+ };
7406
+
7407
+ const opMap = new Map();
7408
+ for (const r of records) {
7409
+ totals.baseline += r.baseline;
7410
+ totals.actual += r.actual;
7411
+ totals.saved += r.saved;
7412
+ totals.totalMs += r.durationMs;
7413
+ if (r.ts) {
7414
+ if (!totals.firstTs || r.ts < totals.firstTs) totals.firstTs = r.ts;
7415
+ if (!totals.lastTs || r.ts > totals.lastTs) totals.lastTs = r.ts;
7416
+ }
7417
+ let o = opMap.get(r.op);
7418
+ if (!o) { o = { op: r.op, count: 0, baseline: 0, saved: 0, ms: 0 }; opMap.set(r.op, o); }
7419
+ o.count += 1;
7420
+ o.baseline += r.baseline;
7421
+ o.saved += r.saved;
7422
+ o.ms += r.durationMs;
7423
+ }
7424
+
7425
+ totals.savedPct = totals.baseline > 0 ? clamp((totals.saved / totals.baseline) * 100, 0, 100) : 0;
7426
+ totals.avgMs = totals.count > 0 ? Math.round(totals.totalMs / totals.count) : 0;
7427
+ totals.usdSaved = totals.saved * price.perToken;
7428
+
7429
+ let byOp = [...opMap.values()].map((o) => ({
7430
+ op: o.op,
7431
+ count: o.count,
7432
+ saved: o.saved,
7433
+ avgPct: o.baseline > 0 ? clamp((o.saved / o.baseline) * 100, 0, 100) : 0,
7434
+ avgMs: o.count > 0 ? Math.round(o.ms / o.count) : 0,
7435
+ usdSaved: o.saved * price.perToken,
7436
+ sharePct: totals.saved > 0 ? (o.saved / totals.saved) * 100 : 0,
7437
+ })).sort((a, b) => b.saved - a.saved);
7438
+
7439
+ if (opts.top && opts.top > 0) byOp = byOp.slice(0, opts.top);
7440
+
7441
+ return {
7442
+ price,
7443
+ totals,
7444
+ byOp,
7445
+ buckets: {
7446
+ daily: bucketBy(records, 'day'),
7447
+ weekly: bucketBy(records, 'week'),
7448
+ monthly: bucketBy(records, 'month'),
7449
+ },
7450
+ };
7451
+ }
7452
+
7453
+ module.exports = { aggregate, bucketBy, parseSince, normalize };
7124
7454
 
7125
- module.exports = { logRun, readLog, summarize };
7455
+ };
7456
+
7457
+ // ── ./src/format/gain-terminal ──
7458
+ __factories["./src/format/gain-terminal"] = function(module, exports) {
7459
+
7460
+ /**
7461
+ * SigMap `gain` — terminal renderer.
7462
+ *
7463
+ * Zero-dependency ANSI dashboard for token savings. Honors NO_COLOR and
7464
+ * non-TTY output (plain text for pipes / --json consumers elsewhere).
7465
+ *
7466
+ * "saved" is a counterfactual estimate (whole-file baseline − actual context),
7467
+ * never a measured delta — the footer says so on every view.
7468
+ */
7469
+
7470
+ const USE_COLOR = !process.env.NO_COLOR && process.stdout.isTTY;
7471
+
7472
+ const C = {
7473
+ reset: USE_COLOR ? '\x1b[0m' : '',
7474
+ dim: USE_COLOR ? '\x1b[2m' : '',
7475
+ bold: USE_COLOR ? '\x1b[1m' : '',
7476
+ green: USE_COLOR ? '\x1b[32m' : '',
7477
+ yellow: USE_COLOR ? '\x1b[33m' : '',
7478
+ red: USE_COLOR ? '\x1b[31m' : '',
7479
+ cyan: USE_COLOR ? '\x1b[36m' : '',
7480
+ gray: USE_COLOR ? '\x1b[90m' : '',
7481
+ };
7482
+
7483
+ // ── formatters ───────────────────────────────────────────────────────────
7484
+ function humanTokens(n) {
7485
+ n = Number(n) || 0;
7486
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
7487
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
7488
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
7489
+ return String(Math.round(n));
7490
+ }
7491
+
7492
+ function fmtInt(n) {
7493
+ return (Number(n) || 0).toLocaleString('en-US');
7494
+ }
7495
+
7496
+ function fmtUSD(n) {
7497
+ n = Number(n) || 0;
7498
+ if (n >= 1000) return '$' + n.toLocaleString('en-US', { maximumFractionDigits: 0 });
7499
+ return '$' + n.toFixed(2);
7500
+ }
7501
+
7502
+ function fmtDuration(ms) {
7503
+ ms = Number(ms) || 0;
7504
+ if (ms >= 3.6e6) return (ms / 3.6e6).toFixed(1) + 'h';
7505
+ if (ms >= 60000) return (ms / 60000).toFixed(1) + 'm';
7506
+ if (ms >= 1000) return (ms / 1000).toFixed(1) + 's';
7507
+ return Math.round(ms) + 'ms';
7508
+ }
7509
+
7510
+ function fmtPct(p) {
7511
+ return (Number(p) || 0).toFixed(1) + '%';
7512
+ }
7513
+
7514
+ function colorPct(p, text) {
7515
+ if (!USE_COLOR) return text;
7516
+ const c = p >= 85 ? C.green : p >= 60 ? C.yellow : C.red;
7517
+ return c + text + C.reset;
7518
+ }
7519
+
7520
+ function pad(s, w, align) {
7521
+ s = String(s);
7522
+ const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
7523
+ const gap = Math.max(0, w - visible.length);
7524
+ return align === 'right' ? ' '.repeat(gap) + s : s + ' '.repeat(gap);
7525
+ }
7526
+
7527
+ /** Solid horizontal efficiency bar with a dotted remainder. */
7528
+ function bar(pct, width) {
7529
+ const filled = Math.round((clamp(pct, 0, 100) / 100) * width);
7530
+ const solid = '█'.repeat(filled);
7531
+ const rest = '░'.repeat(Math.max(0, width - filled));
7532
+ return (USE_COLOR ? C.green : '') + solid + (USE_COLOR ? C.gray : '') + rest + C.reset;
7533
+ }
7534
+
7535
+ /** Proportional impact bar (share of total saved). */
7536
+ function impactBar(sharePct, width) {
7537
+ const n = Math.max(1, Math.round((clamp(sharePct, 0, 100) / 100) * width));
7538
+ return (USE_COLOR ? C.cyan : '') + '█'.repeat(n) + C.reset;
7539
+ }
7540
+
7541
+ const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
7542
+ const rule = (w) => C.gray + '─'.repeat(w) + C.reset;
7543
+
7544
+ // ── views ──────────────────────────────────────────────────────────────
7545
+ /**
7546
+ * Render the global summary + by-operation table.
7547
+ * @param {object} agg output of aggregate()
7548
+ * @param {object} [opts] { version, scope }
7549
+ * @returns {string}
7550
+ */
7551
+ function renderSummary(agg, opts = {}) {
7552
+ const W = 74;
7553
+ const t = agg.totals;
7554
+ const L = [];
7555
+ const scope = opts.scope || 'this repo';
7556
+ const ver = opts.version ? `v${opts.version}` : '';
7557
+
7558
+ L.push('');
7559
+ const heading = `⚡ SigMap — Token Savings (${scope})`;
7560
+ L.push(' ' + C.bold + C.cyan + heading + C.reset +
7561
+ pad(`${C.green}✓${C.reset} ${C.dim}${ver}${C.reset}`, W - heading.length, 'right'));
7562
+ L.push(' ' + rule(W));
7563
+ L.push('');
7564
+
7565
+ if (t.count === 0) {
7566
+ L.push(` ${C.yellow}No usage recorded yet.${C.reset}`);
7567
+ L.push(` ${C.dim}Run a few queries (sigmap ask "...") or seed a demo:${C.reset}`);
7568
+ L.push(` ${C.dim} node scripts/gain.mjs --demo${C.reset}`);
7569
+ L.push('');
7570
+ return L.join('\n');
7571
+ }
7572
+
7573
+ const label = (k) => C.dim + pad(k, 21) + C.reset;
7574
+ L.push(' ' + label('Total operations') + ': ' + C.bold + fmtInt(t.count) + C.reset);
7575
+ L.push(' ' + label('Whole-file baseline') + ': ' + humanTokens(t.baseline) + ' tok' +
7576
+ ` ${C.dim}← est. cost of feeding full files${C.reset}`);
7577
+ L.push(' ' + label('SigMap context') + ': ' + humanTokens(t.actual) + ' tok');
7578
+ L.push(' ' + label('Tokens saved') + ': ' + C.bold + humanTokens(t.saved) + C.reset +
7579
+ ' (' + colorPct(t.savedPct, fmtPct(t.savedPct)) + ')');
7580
+ L.push(' ' + label('Est. money saved') + ': ' + C.bold + C.green + fmtUSD(t.usdSaved) + C.reset +
7581
+ ` ${C.dim}(${agg.price.model} input @ $${agg.price.perMtok}/M · --model to change)${C.reset}`);
7582
+ L.push(' ' + label('Avg latency') + ': ' + fmtDuration(t.avgMs) + ' / op' +
7583
+ ` ${C.dim}(local, no API round-trip)${C.reset}`);
7584
+ L.push('');
7585
+ L.push(' ' + label('Efficiency') + ': ▕' + bar(t.savedPct, 30) + '▏ ' +
7586
+ colorPct(t.savedPct, C.bold + fmtPct(t.savedPct) + C.reset));
7587
+ L.push('');
7588
+
7589
+ // By operation table
7590
+ L.push(` ${C.bold}By operation${C.reset}`);
7591
+ L.push(' ' + rule(W));
7592
+ L.push(' ' + C.dim +
7593
+ pad('#', 3) + pad('Operation', 24) + pad('Count', 8, 'right') +
7594
+ pad('Saved', 9, 'right') + pad('Avg%', 8, 'right') + pad('Time', 8, 'right') +
7595
+ ' Impact' + C.reset);
7596
+ agg.byOp.forEach((o, i) => {
7597
+ L.push(' ' +
7598
+ pad(`${i + 1}.`, 3) +
7599
+ pad(o.op, 24) +
7600
+ pad(fmtInt(o.count), 8, 'right') +
7601
+ pad(humanTokens(o.saved), 9, 'right') +
7602
+ pad(colorPct(o.avgPct, fmtPct(o.avgPct)), 8, 'right') +
7603
+ pad(fmtDuration(o.avgMs), 8, 'right') +
7604
+ ' ' + impactBar(o.sharePct, 12));
7605
+ });
7606
+ L.push(' ' + rule(W));
7607
+ L.push(` ${C.dim}saved = baseline − actual · estimated vs whole-file reads · local-only${C.reset}`);
7608
+ L.push('');
7609
+ return L.join('\n');
7610
+ }
7611
+
7612
+ /**
7613
+ * Render daily / weekly / monthly trend tables.
7614
+ * @param {object} agg
7615
+ * @returns {string}
7616
+ */
7617
+ function renderBreakdown(agg) {
7618
+ const L = [];
7619
+ if (agg.totals.count === 0) return renderSummary(agg);
7620
+
7621
+ const section = (icon, title, rows, keyHeader) => {
7622
+ L.push('');
7623
+ L.push(` ${C.bold}${icon} ${title}${C.reset}`);
7624
+ L.push(' ' + C.dim +
7625
+ pad(keyHeader, 14) + pad('Ops', 7, 'right') + pad('Baseline', 11, 'right') +
7626
+ pad('Actual', 10, 'right') + pad('Saved', 10, 'right') + pad('Save%', 8, 'right') +
7627
+ pad('Time', 8, 'right') + C.reset);
7628
+ let TB = 0, TA = 0, TS = 0, TC = 0, TM = 0;
7629
+ for (const r of rows) {
7630
+ TB += r.baseline; TA += r.actual; TS += r.saved; TC += r.count; TM += r.ms;
7631
+ L.push(' ' +
7632
+ pad(r.key, 14) + pad(fmtInt(r.count), 7, 'right') +
7633
+ pad(humanTokens(r.baseline), 11, 'right') + pad(humanTokens(r.actual), 10, 'right') +
7634
+ pad(humanTokens(r.saved), 10, 'right') +
7635
+ pad(colorPct(r.savedPct, fmtPct(r.savedPct)), 8, 'right') +
7636
+ pad(fmtDuration(r.ms), 8, 'right'));
7637
+ }
7638
+ const tp = TB > 0 ? (TS / TB) * 100 : 0;
7639
+ L.push(' ' + C.bold +
7640
+ pad('TOTAL', 14) + pad(fmtInt(TC), 7, 'right') +
7641
+ pad(humanTokens(TB), 11, 'right') + pad(humanTokens(TA), 10, 'right') +
7642
+ pad(humanTokens(TS), 10, 'right') + pad(fmtPct(tp), 8, 'right') +
7643
+ pad(fmtDuration(TM), 8, 'right') + C.reset);
7644
+ };
7645
+
7646
+ const daily = agg.buckets.daily.slice(-30);
7647
+ section('📅', `Daily (last ${daily.length})`, daily, 'Date');
7648
+ const weekly = agg.buckets.weekly.slice(-6);
7649
+ section('📆', `Weekly (last ${weekly.length})`, weekly, 'Week of');
7650
+ const monthly = agg.buckets.monthly.slice(-3);
7651
+ section('🗓️', `Monthly (last ${monthly.length})`, monthly, 'Month');
7652
+ L.push('');
7653
+ L.push(` ${C.dim}saved = baseline − actual · estimated vs whole-file reads · local-only${C.reset}`);
7654
+ L.push('');
7655
+ return L.join('\n');
7656
+ }
7657
+
7658
+ module.exports = {
7659
+ renderSummary,
7660
+ renderBreakdown,
7661
+ // exported for tests
7662
+ humanTokens, fmtUSD, fmtDuration, fmtPct,
7663
+ };
7126
7664
 
7127
7665
  };
7128
7666
 
@@ -10961,7 +11499,7 @@ function __tryGit(args, opts = {}) {
10961
11499
  catch (_) { return ''; }
10962
11500
  }
10963
11501
 
10964
- const VERSION = '7.1.0';
11502
+ const VERSION = '7.2.0';
10965
11503
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
10966
11504
 
10967
11505
  function requireSourceOrBundled(key) {
@@ -14376,7 +14914,8 @@ function main() {
14376
14914
  console.log(` p50 token count : ${result.p50TokenCount}`);
14377
14915
  console.log(` p95 token count : ${result.p95TokenCount}`);
14378
14916
  console.log(` overbudget streak: ${result.overBudgetStreak}`);
14379
- console.log(` extractor cover.: ${result.extractorCoverage}%`);
14917
+ console.log(` repo languages : ${result.extractorCoverage}% of supported langs used here (informational — not scored)`);
14918
+ console.log(` ${'score basis'.padEnd(16)}: staleness · token reduction · over-budget rate`);
14380
14919
  }
14381
14920
  process.exit(0);
14382
14921
  }
package/llms-full.txt CHANGED
@@ -9,7 +9,7 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
9
9
  grounded. Deterministic, offline, no embeddings or vector database. Works with
10
10
  Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
11
11
 
12
- # Version: 7.1.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.2.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
13
13
  # Source: auto-generated from package.json, version.json, src/mcp/tools.js, src/config/defaults.js
14
14
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
15
15
 
package/llms.txt CHANGED
@@ -9,7 +9,7 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
9
9
  grounded. Deterministic, offline, no embeddings or vector database. Works with
10
10
  Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
11
11
 
12
- # Version: 7.1.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.2.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
13
13
  # Source: auto-generated from package.json, version.json, src/mcp/tools.js, src/config/defaults.js
14
14
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
15
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "7.1.0",
3
+ "version": "7.2.0",
4
4
  "description": "97% token reduction for AI coding. Extracts function & class signatures with TF-IDF ranking to feed only the right files to Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP. Zero dependencies, runs offline via npx.",
5
5
  "main": "packages/core/index.js",
6
6
  "exports": {
@@ -31,12 +31,14 @@
31
31
  "health": "node gen-context.js --health",
32
32
  "map": "node gen-project-map.js",
33
33
  "mcp": "node gen-context.js --mcp",
34
+ "check:bundle": "node scripts/check-bundle.mjs",
35
+ "check:version-meta": "node scripts/check-version-meta.mjs",
34
36
  "build:binary": "node scripts/build-binary.mjs",
35
37
  "verify:binary": "node scripts/verify-binary.mjs",
36
38
  "version:sync": "node scripts/sync-versions.mjs",
37
39
  "generate:llms": "node scripts/generate-llms.mjs",
38
40
  "validate:llms": "node scripts/validate-llms.mjs",
39
- "prepublishOnly": "node scripts/generate-llms.mjs"
41
+ "prepublishOnly": "node scripts/check-bundle.mjs && node scripts/check-version-meta.mjs && node scripts/generate-llms.mjs"
40
42
  },
41
43
  "files": [
42
44
  "gen-context.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "7.1.0",
3
+ "version": "7.2.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "7.1.0",
3
+ "version": "7.2.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
package/src/mcp/server.js CHANGED
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '7.1.0',
21
+ version: '7.2.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24