sigmap 7.0.1 → 7.1.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,21 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [7.1.0] — 2026-06-16
14
+
15
+ Minor release — a token-savings dashboard in the terminal, plus domain and sponsorship docs.
16
+
17
+ ### Added
18
+ - **Token-savings dashboard — `sigmap gain` (#260):** surfaces cumulative savings right in the terminal — total tokens saved, % efficiency, estimated $ saved, latency, and a by-operation breakdown, plus `gain --all` for daily / weekly / monthly trends. Savings are captured per operation (`ask`, `generate`) into a dedicated local log `.context/gain.ndjson` (counts only — no paths, source, or query text). Capture is **default-on** and privacy-safe; opt out via `--no-track`, `SIGMAP_NO_TRACK=1`, or `config.gainTracking:false`. The legacy `usage.ndjson` / `--track` health log is unchanged. New `src/tracking/{aggregate,pricing}.js` (zero-dep aggregation + model→$/Mtok pricing) and `src/format/gain-terminal.js` (ANSI renderer, `NO_COLOR`/non-TTY safe). Flags: `gain --all | --json | --since <7d|ISO> | --top <n> | --model <name> | --reset`. "Saved" is labeled everywhere as an estimate vs the whole-file baseline.
19
+
20
+ ### Fixed
21
+ - **Docs served at the sigmap.io root (#258):** the docs site now builds with base `/` (dropped the `/sigmap/` path prefix) and the project domain points to sigmap.io.
22
+
23
+ ### Changed
24
+ - **Sponsorship transparency (#257):** README gained a Sponsor section with a tier ladder and funding goal; detail moved into `SPONSOR.md`, with an "About the maintainer" note and a low-barrier welcome.
25
+
26
+ ---
27
+
13
28
  ## [7.0.1] — 2026-06-14
14
29
 
15
30
  Patch release — supply-chain hardening and package hygiene, plus a wider star nudge.
package/README.md CHANGED
@@ -100,7 +100,7 @@ Repos tested : 21 (JavaScript, Python, Go, Rust, Java, R, C++, C#, Dart, Swift
100
100
  Measured on 90 coding tasks across 18 real public repos. No LLM API — fully reproducible.
101
101
 
102
102
  **Resources:**
103
- - [Full methodology →](https://manojmallick.github.io/sigmap/guide/benchmark.html)
103
+ - [Full methodology →](https://sigmap.io/guide/benchmark.html)
104
104
  - [Benchmark suite (GitHub)](https://github.com/manojmallick/sigmap-benchmark-suite) — scripts, tasks, and raw data
105
105
  - [Benchmark data (Zenodo)](https://zenodo.org/records/19898842) — archived results for reproducibility
106
106
 
@@ -172,8 +172,8 @@ sigmap --adapter claude # works with Claude Code
172
172
  **Open-source agents & local LLMs:**
173
173
 
174
174
  Use SigMap with open-source tools and fully self-hosted setups:
175
- - **[Open-source agents guide →](https://manojmallick.github.io/sigmap/guide/agents)** — OpenCode, Aider, OpenHands, Cline
176
- - **[Local LLMs guide →](https://manojmallick.github.io/sigmap/guide/local-llms)** — Ollama, llama.cpp, vLLM (no API keys, full privacy)
175
+ - **[Open-source agents guide →](https://sigmap.io/guide/agents)** — OpenCode, Aider, OpenHands, Cline
176
+ - **[Local LLMs guide →](https://sigmap.io/guide/local-llms)** — Ollama, llama.cpp, vLLM (no API keys, full privacy)
177
177
 
178
178
  **IDE extensions:**
179
179
 
@@ -219,21 +219,21 @@ sigmap --health
219
219
  | 👶 **New** | [Quick start guide](docs/readmes/GETTING_STARTED.md) — setup in 60 seconds |
220
220
  | ⚡ **Daily** | `sigmap ask` / `sigmap validate` / `sigmap judge` |
221
221
  | 🧠 **Advanced** | [Context strategies](docs/readmes/CONTEXT_STRATEGIES.md) · [MCP setup](docs/readmes/MCP_SETUP.md) |
222
- | 🏢 **Teams** | [Config reference](https://manojmallick.github.io/sigmap/guide/config.html) · [CI setup](docs/readmes/ENTERPRISE_SETUP.md) |
222
+ | 🏢 **Teams** | [Config reference](https://sigmap.io/guide/config.html) · [CI setup](docs/readmes/ENTERPRISE_SETUP.md) |
223
223
 
224
224
  ---
225
225
 
226
226
  ## Docs
227
227
 
228
- **[manojmallick.github.io/sigmap](https://manojmallick.github.io/sigmap)**
228
+ **[sigmap.io](https://sigmap.io)**
229
229
 
230
230
  | Section | Link |
231
231
  |---|---|
232
- | CLI reference (32 commands) | [cli.html](https://manojmallick.github.io/sigmap/guide/cli.html) |
233
- | Benchmark methodology | [benchmark.html](https://manojmallick.github.io/sigmap/guide/benchmark.html) |
234
- | Config reference | [config.html](https://manojmallick.github.io/sigmap/guide/config.html) |
235
- | Roadmap | [roadmap.html](https://manojmallick.github.io/sigmap/guide/roadmap.html) |
236
- | 31 languages | [generalization.html](https://manojmallick.github.io/sigmap/guide/generalization.html) |
232
+ | CLI reference (32 commands) | [cli.html](https://sigmap.io/guide/cli.html) |
233
+ | Benchmark methodology | [benchmark.html](https://sigmap.io/guide/benchmark.html) |
234
+ | Config reference | [config.html](https://sigmap.io/guide/config.html) |
235
+ | Roadmap | [roadmap.html](https://sigmap.io/guide/roadmap.html) |
236
+ | 31 languages | [generalization.html](https://sigmap.io/guide/generalization.html) |
237
237
 
238
238
  ---
239
239
 
@@ -245,6 +245,14 @@ If SigMap saves you context or API spend, a ⭐ on [GitHub](https://github.com/m
245
245
 
246
246
  ---
247
247
 
248
+ ## Sponsor
249
+
250
+ SigMap is built and maintained by one developer, kept **zero-dependency**, offline, and free. If it saves your team context or API spend, sponsoring keeps it that way — and funds the benchmark CI, the `sigmap.io` domain, and ongoing supply-chain hardening.
251
+
252
+ 💜 **[Become a sponsor →](https://github.com/sponsors/manojmallick)** · see **[SPONSOR.md](SPONSOR.md)** for tiers and exactly where your support goes. Any amount helps — even $1/mo — and a ⭐ or a share counts too.
253
+
254
+ ---
255
+
248
256
  ## Contributing
249
257
 
250
258
  SigMap welcomes contributions!
@@ -284,7 +292,7 @@ TypeScript · JavaScript · Python · Java · Kotlin · Go · Rust · C# · C/C+
284
292
 
285
293
  All implemented with zero external dependencies.
286
294
 
287
- [Full language table →](https://manojmallick.github.io/sigmap/guide/generalization.html)
295
+ [Full language table →](https://sigmap.io/guide/generalization.html)
288
296
 
289
297
  ---
290
298
 
@@ -296,7 +304,7 @@ MIT © 2026 [Manoj Mallick](https://github.com/manojmallick) · Made in Amsterda
296
304
 
297
305
  <div align="center">
298
306
 
299
- **[Docs](https://manojmallick.github.io/sigmap) · [Changelog](CHANGELOG.md) · [Roadmap](https://manojmallick.github.io/sigmap/roadmap.html) · [npm](https://www.npmjs.com/package/sigmap)**
307
+ **[Docs](https://sigmap.io) · [Changelog](CHANGELOG.md) · [Roadmap](https://sigmap.io/roadmap.html) · [npm](https://www.npmjs.com/package/sigmap)**
300
308
 
301
309
  ⭐ [Star on GitHub](https://github.com/manojmallick/sigmap) if SigMap saves you tokens.
302
310
 
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.0.1',
6257
+ version: '7.1.0',
6258
6258
  description: 'SigMap MCP server — code signatures on demand',
6259
6259
  };
6260
6260
 
@@ -8928,6 +8928,20 @@ __factories["./src/discovery/source-root-registry"] = function(module, exports)
8928
8928
  module.exports = { REGISTRY };
8929
8929
  };
8930
8930
 
8931
+ // ── ./src/util/git ──
8932
+ __factories["./src/util/git"] = function(module, exports) {
8933
+ 'use strict';
8934
+ const { execFileSync } = require('child_process');
8935
+ function git(args, opts = {}) {
8936
+ return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], ...opts });
8937
+ }
8938
+ function tryGit(args, opts = {}) {
8939
+ try { return git(args, opts).toString().trim(); }
8940
+ catch (_) { return ''; }
8941
+ }
8942
+ module.exports = { git, tryGit };
8943
+ };
8944
+
8931
8945
  // ── ./src/discovery/sigmapignore ──
8932
8946
  __factories["./src/discovery/sigmapignore"] = function(module, exports) {
8933
8947
  'use strict';
@@ -10947,7 +10961,7 @@ function __tryGit(args, opts = {}) {
10947
10961
  catch (_) { return ''; }
10948
10962
  }
10949
10963
 
10950
- const VERSION = '7.0.1';
10964
+ const VERSION = '7.1.0';
10951
10965
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
10952
10966
 
10953
10967
  function requireSourceOrBundled(key) {
@@ -12180,6 +12194,7 @@ function runDiff(cwd, config, stagedOnly, baseRef) {
12180
12194
  // Core generate pipeline
12181
12195
  // ---------------------------------------------------------------------------
12182
12196
  function runGenerate(cwd, config, reportMode, reportJson = false) {
12197
+ const __genT0 = Date.now();
12183
12198
  const ignorePatterns = loadIgnorePatterns(cwd);
12184
12199
  let allFiles = buildFileList(cwd, config);
12185
12200
 
@@ -12379,7 +12394,7 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
12379
12394
  printReport(result.inputTokenTotal, result.finalTokens, result.fileCount, result.droppedCount, reportJson, effectiveMaxTokens, result.coverageResult, config.autoMaxTokens !== false && effectiveMaxTokens !== config.maxTokens, config.maxTokens);
12380
12395
  }
12381
12396
 
12382
- // Usage tracking (v0.9) — optional append-only NDJSON log
12397
+ // Usage tracking (v0.9) — legacy health log, opt-in via config.tracking / --track.
12383
12398
  const trackingEnabled = !!(config.tracking || process.argv.includes('--track'));
12384
12399
  if (trackingEnabled && !reportMode) {
12385
12400
  try {
@@ -12399,6 +12414,22 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
12399
12414
  }
12400
12415
  }
12401
12416
 
12417
+ // gain: capture generate savings (separate gain.ndjson; default ON, opt-out).
12418
+ if (!reportMode) {
12419
+ try {
12420
+ const { recordUsage, isTrackingEnabled } = requireSourceOrBundled('./src/tracking/logger');
12421
+ if (isTrackingEnabled(config, process.argv)) {
12422
+ recordUsage({
12423
+ version: VERSION,
12424
+ op: 'generate',
12425
+ baselineTokens: result.inputTokenTotal,
12426
+ actualTokens: result.finalTokens,
12427
+ durationMs: Date.now() - __genT0,
12428
+ }, cwd);
12429
+ }
12430
+ } catch (_) { /* gain capture is best-effort */ }
12431
+ }
12432
+
12402
12433
  // Feature 8: post-run summary — 6-line stdout after every normal run
12403
12434
  if (!reportMode
12404
12435
  && !process.argv.includes('--json')
@@ -12695,6 +12726,13 @@ Usage:
12695
12726
  ${cmd} --suggest-tool "<task>" --json Machine-readable tier recommendation
12696
12727
  ${cmd} --health Print composite health score
12697
12728
  ${cmd} --health --json Machine-readable health score
12729
+ ${cmd} gain Token-savings dashboard (totals + by-operation)
12730
+ ${cmd} gain --all Add daily / weekly / monthly trend tables
12731
+ ${cmd} gain --json Aggregate savings as JSON
12732
+ ${cmd} gain --since 7d Window filter (7d, 30d, 12h, or ISO date)
12733
+ ${cmd} gain --top <n> | --model <name> Limit rows / set $ pricing model
12734
+ ${cmd} gain --reset Clear the local savings log (.context/gain.ndjson)
12735
+ ${cmd} ... --no-track Disable gain savings capture for this run
12698
12736
  ${cmd} --diff Generate context for git-changed files only
12699
12737
  ${cmd} --diff <base-ref> Generate context + structural diff vs base ref (e.g. main)
12700
12738
  ${cmd} --diff --staged Generate context for staged files only
@@ -13094,6 +13132,7 @@ function main() {
13094
13132
 
13095
13133
  // v4.2: `sigmap ask "<query>"` — unified pipeline
13096
13134
  if (args[0] === 'ask') {
13135
+ const __askT0 = Date.now();
13097
13136
  // v6.8: Handle --followup flag which may appear before or after query
13098
13137
  let query = args[1];
13099
13138
  if (query === '--followup' && args[2]) {
@@ -13261,6 +13300,21 @@ function main() {
13261
13300
  bar,
13262
13301
  ].join('\n'));
13263
13302
  }
13303
+ // gain: capture this query's savings for the `sigmap gain` dashboard.
13304
+ try {
13305
+ const { recordUsage, isTrackingEnabled } = requireSourceOrBundled('./src/tracking/logger');
13306
+ if (isTrackingEnabled(config, args)) {
13307
+ recordUsage({
13308
+ version: VERSION,
13309
+ op: 'ask',
13310
+ baselineTokens: rawTok,
13311
+ actualTokens: ctxTok,
13312
+ durationMs: Date.now() - __askT0,
13313
+ model,
13314
+ }, cwd);
13315
+ }
13316
+ } catch (_) { /* gain capture is best-effort */ }
13317
+
13264
13318
  // v7.0.0: record the run and show the one-time star nudge (interactive only).
13265
13319
  try {
13266
13320
  const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
@@ -13273,6 +13327,44 @@ function main() {
13273
13327
  process.exit(0);
13274
13328
  }
13275
13329
 
13330
+ // `sigmap gain` — token-savings dashboard (totals, by-operation, trends).
13331
+ if (args[0] === 'gain') {
13332
+ const valOf = (f, d) => { const i = args.indexOf(f); return i >= 0 && args[i + 1] ? args[i + 1] : d; };
13333
+ if (args.includes('--reset')) {
13334
+ try {
13335
+ const { GAIN_FILE } = requireSourceOrBundled('./src/tracking/logger');
13336
+ const p = path.join(cwd, GAIN_FILE);
13337
+ if (fs.existsSync(p)) fs.unlinkSync(p);
13338
+ console.log('[sigmap] gain: usage log cleared.');
13339
+ } catch (e) { console.error(`[sigmap] gain: ${e.message}`); }
13340
+ process.exit(0);
13341
+ }
13342
+ try {
13343
+ const { readGainLog } = requireSourceOrBundled('./src/tracking/logger');
13344
+ const { aggregate } = requireSourceOrBundled('./src/tracking/aggregate');
13345
+ const { renderSummary, renderBreakdown } = requireSourceOrBundled('./src/format/gain-terminal');
13346
+ const records = readGainLog(cwd);
13347
+ const agg = aggregate(records, {
13348
+ model: valOf('--model', 'claude-sonnet'),
13349
+ since: valOf('--since', null),
13350
+ top: parseInt(valOf('--top', args.includes('--all') ? '0' : '10'), 10),
13351
+ });
13352
+ if (args.includes('--json')) {
13353
+ process.stdout.write(JSON.stringify(agg, null, 2) + '\n');
13354
+ } else {
13355
+ const out = args.includes('--all')
13356
+ ? renderSummary(agg, { version: VERSION }) + '\n' + renderBreakdown(agg)
13357
+ : renderSummary(agg, { version: VERSION });
13358
+ process.stdout.write(out + '\n');
13359
+ }
13360
+ } catch (e) {
13361
+ console.error(`[sigmap] gain: ${e.message}`);
13362
+ console.error(' (gain modules require src/ — for single-file installs, regenerate the bundle)');
13363
+ process.exit(1);
13364
+ }
13365
+ process.exit(0);
13366
+ }
13367
+
13276
13368
  // v4.2: `sigmap suggest-profile` — auto-detect task type from git state
13277
13369
  if (args[0] === 'suggest-profile') {
13278
13370
  const short = args.includes('--short');
@@ -13359,7 +13451,7 @@ function main() {
13359
13451
  const shareText = [
13360
13452
  'Generated with SigMap — zero-dependency AI context engine',
13361
13453
  `${reduction}% fewer tokens · ${hitAt5}% retrieval accuracy · 6× better results`,
13362
- 'https://sigmap.dev',
13454
+ 'https://sigmap.io',
13363
13455
  ].join('\n');
13364
13456
 
13365
13457
  console.log(shareText);
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.0.1 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.1.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
 
@@ -67,6 +67,13 @@ sigmap --suggest-tool "<task>" Recommend model tier for a task descrip
67
67
  sigmap --suggest-tool "<task>" --json Machine-readable tier recommendation
68
68
  sigmap --health Print composite health score
69
69
  sigmap --health --json Machine-readable health score
70
+ sigmap gain Token-savings dashboard (totals + by-operation)
71
+ sigmap gain --all Add daily / weekly / monthly trend tables
72
+ sigmap gain --json Aggregate savings as JSON
73
+ sigmap gain --since 7d Window filter (7d, 30d, 12h, or ISO date)
74
+ sigmap gain --top <n> | --model <name> Limit rows / set $ pricing model
75
+ sigmap gain --reset Clear the local savings log (.context/gain.ndjson)
76
+ sigmap ... --no-track Disable gain savings capture for this run
70
77
  sigmap --diff Generate context for git-changed files only
71
78
  sigmap --diff <base-ref> Generate context + structural diff vs base ref (e.g. main)
72
79
  sigmap --diff --staged Generate context for staged files only
@@ -273,7 +280,7 @@ the relevant regulation before publication; SigMap makes no legal claims.
273
280
  - Author: Manoj Mallick
274
281
  - License: MIT
275
282
  - Repository: https://github.com/manojmallick/sigmap
276
- - Documentation: https://manojmallick.github.io/sigmap/
283
+ - Documentation: https://sigmap.io/
277
284
  - npm: https://www.npmjs.com/package/sigmap
278
285
  - Benchmark dataset: https://doi.org/10.5281/zenodo.19898842
279
286
  - Issues: https://github.com/manojmallick/sigmap/issues
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.0.1 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.1.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
 
@@ -39,18 +39,18 @@ npx sigmap --mcp # start the MCP server over stdio
39
39
  ```
40
40
 
41
41
  ## Docs
42
- - [Full documentation](https://manojmallick.github.io/sigmap/)
43
- - [CLI reference](https://manojmallick.github.io/sigmap/guide/cli)
44
- - [MCP tools](https://manojmallick.github.io/sigmap/guide/mcp)
45
- - [Benchmark methodology](https://manojmallick.github.io/sigmap/guide/benchmark)
46
- - [Configuration guide](https://manojmallick.github.io/sigmap/guide/config)
42
+ - [Full documentation](https://sigmap.io/)
43
+ - [CLI reference](https://sigmap.io/guide/cli)
44
+ - [MCP tools](https://sigmap.io/guide/mcp)
45
+ - [Benchmark methodology](https://sigmap.io/guide/benchmark)
46
+ - [Configuration guide](https://sigmap.io/guide/config)
47
47
  - [Changelog](https://github.com/manojmallick/sigmap/blob/main/CHANGELOG.md)
48
48
 
49
49
  ## Optional
50
50
  - [GitHub repository](https://github.com/manojmallick/sigmap)
51
51
  - [npm package](https://www.npmjs.com/package/sigmap)
52
52
  - [Benchmark dataset (Zenodo)](https://doi.org/10.5281/zenodo.19898842)
53
- - [Full LLM reference](https://manojmallick.github.io/sigmap/llms-full.txt)
53
+ - [Full LLM reference](https://sigmap.io/llms-full.txt)
54
54
 
55
55
  SigMap — grounded AI coding context. The lightweight, deterministic alternative
56
56
  to embeddings for feeding the right code to AI assistants.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "7.0.1",
3
+ "version": "7.1.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": {
@@ -82,7 +82,7 @@
82
82
  "type": "git",
83
83
  "url": "https://github.com/manojmallick/sigmap.git"
84
84
  },
85
- "homepage": "https://manojmallick.github.io/sigmap/",
85
+ "homepage": "https://sigmap.io/",
86
86
  "bugs": {
87
87
  "url": "https://github.com/manojmallick/sigmap/issues"
88
88
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "7.0.1",
3
+ "version": "7.1.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -18,7 +18,7 @@
18
18
  "url": "https://github.com/manojmallick/sigmap.git",
19
19
  "directory": "packages/cli"
20
20
  },
21
- "homepage": "https://manojmallick.github.io/sigmap/",
21
+ "homepage": "https://sigmap.io/",
22
22
  "license": "MIT",
23
23
  "engines": {
24
24
  "node": ">=18.0.0"
@@ -1,6 +1,6 @@
1
1
  # sigmap-core
2
2
 
3
- Programmatic API for [SigMap](https://manojmallick.github.io/sigmap/) — zero-dependency code signature extraction, ranked retrieval, secret scanning, project health scoring, and multi-adapter output formatting (v3.0+).
3
+ Programmatic API for [SigMap](https://sigmap.io/) — zero-dependency code signature extraction, ranked retrieval, secret scanning, project health scoring, and multi-adapter output formatting (v3.0+).
4
4
 
5
5
  ## Installation
6
6
 
@@ -145,7 +145,7 @@ const geminiInstruction = adapt(context, 'gemini');
145
145
  // All 6 adapters: copilot | claude | cursor | windsurf | openai | gemini
146
146
  ```
147
147
 
148
- See the full [roadmap](https://manojmallick.github.io/sigmap/roadmap.html).
148
+ See the full [roadmap](https://sigmap.io/roadmap.html).
149
149
 
150
150
  ## Zero dependencies
151
151
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "7.0.1",
3
+ "version": "7.1.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -20,7 +20,7 @@
20
20
  "url": "https://github.com/manojmallick/sigmap.git",
21
21
  "directory": "packages/core"
22
22
  },
23
- "homepage": "https://manojmallick.github.io/sigmap/",
23
+ "homepage": "https://sigmap.io/",
24
24
  "license": "MIT",
25
25
  "engines": {
26
26
  "node": ">=18.0.0"
@@ -0,0 +1,206 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * SigMap `gain` — terminal renderer.
5
+ *
6
+ * Zero-dependency ANSI dashboard for token savings. Honors NO_COLOR and
7
+ * non-TTY output (plain text for pipes / --json consumers elsewhere).
8
+ *
9
+ * "saved" is a counterfactual estimate (whole-file baseline − actual context),
10
+ * never a measured delta — the footer says so on every view.
11
+ */
12
+
13
+ const USE_COLOR = !process.env.NO_COLOR && process.stdout.isTTY;
14
+
15
+ const C = {
16
+ reset: USE_COLOR ? '\x1b[0m' : '',
17
+ dim: USE_COLOR ? '\x1b[2m' : '',
18
+ bold: USE_COLOR ? '\x1b[1m' : '',
19
+ green: USE_COLOR ? '\x1b[32m' : '',
20
+ yellow: USE_COLOR ? '\x1b[33m' : '',
21
+ red: USE_COLOR ? '\x1b[31m' : '',
22
+ cyan: USE_COLOR ? '\x1b[36m' : '',
23
+ gray: USE_COLOR ? '\x1b[90m' : '',
24
+ };
25
+
26
+ // ── formatters ───────────────────────────────────────────────────────────
27
+ function humanTokens(n) {
28
+ n = Number(n) || 0;
29
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
30
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
31
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
32
+ return String(Math.round(n));
33
+ }
34
+
35
+ function fmtInt(n) {
36
+ return (Number(n) || 0).toLocaleString('en-US');
37
+ }
38
+
39
+ function fmtUSD(n) {
40
+ n = Number(n) || 0;
41
+ if (n >= 1000) return '$' + n.toLocaleString('en-US', { maximumFractionDigits: 0 });
42
+ return '$' + n.toFixed(2);
43
+ }
44
+
45
+ function fmtDuration(ms) {
46
+ ms = Number(ms) || 0;
47
+ if (ms >= 3.6e6) return (ms / 3.6e6).toFixed(1) + 'h';
48
+ if (ms >= 60000) return (ms / 60000).toFixed(1) + 'm';
49
+ if (ms >= 1000) return (ms / 1000).toFixed(1) + 's';
50
+ return Math.round(ms) + 'ms';
51
+ }
52
+
53
+ function fmtPct(p) {
54
+ return (Number(p) || 0).toFixed(1) + '%';
55
+ }
56
+
57
+ function colorPct(p, text) {
58
+ if (!USE_COLOR) return text;
59
+ const c = p >= 85 ? C.green : p >= 60 ? C.yellow : C.red;
60
+ return c + text + C.reset;
61
+ }
62
+
63
+ function pad(s, w, align) {
64
+ s = String(s);
65
+ const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
66
+ const gap = Math.max(0, w - visible.length);
67
+ return align === 'right' ? ' '.repeat(gap) + s : s + ' '.repeat(gap);
68
+ }
69
+
70
+ /** Solid horizontal efficiency bar with a dotted remainder. */
71
+ function bar(pct, width) {
72
+ const filled = Math.round((clamp(pct, 0, 100) / 100) * width);
73
+ const solid = '█'.repeat(filled);
74
+ const rest = '░'.repeat(Math.max(0, width - filled));
75
+ return (USE_COLOR ? C.green : '') + solid + (USE_COLOR ? C.gray : '') + rest + C.reset;
76
+ }
77
+
78
+ /** Proportional impact bar (share of total saved). */
79
+ function impactBar(sharePct, width) {
80
+ const n = Math.max(1, Math.round((clamp(sharePct, 0, 100) / 100) * width));
81
+ return (USE_COLOR ? C.cyan : '') + '█'.repeat(n) + C.reset;
82
+ }
83
+
84
+ const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
85
+ const rule = (w) => C.gray + '─'.repeat(w) + C.reset;
86
+
87
+ // ── views ──────────────────────────────────────────────────────────────
88
+ /**
89
+ * Render the global summary + by-operation table.
90
+ * @param {object} agg output of aggregate()
91
+ * @param {object} [opts] { version, scope }
92
+ * @returns {string}
93
+ */
94
+ function renderSummary(agg, opts = {}) {
95
+ const W = 74;
96
+ const t = agg.totals;
97
+ const L = [];
98
+ const scope = opts.scope || 'this repo';
99
+ const ver = opts.version ? `v${opts.version}` : '';
100
+
101
+ L.push('');
102
+ const heading = `⚡ SigMap — Token Savings (${scope})`;
103
+ L.push(' ' + C.bold + C.cyan + heading + C.reset +
104
+ pad(`${C.green}✓${C.reset} ${C.dim}${ver}${C.reset}`, W - heading.length, 'right'));
105
+ L.push(' ' + rule(W));
106
+ L.push('');
107
+
108
+ if (t.count === 0) {
109
+ L.push(` ${C.yellow}No usage recorded yet.${C.reset}`);
110
+ L.push(` ${C.dim}Run a few queries (sigmap ask "...") or seed a demo:${C.reset}`);
111
+ L.push(` ${C.dim} node scripts/gain.mjs --demo${C.reset}`);
112
+ L.push('');
113
+ return L.join('\n');
114
+ }
115
+
116
+ const label = (k) => C.dim + pad(k, 21) + C.reset;
117
+ L.push(' ' + label('Total operations') + ': ' + C.bold + fmtInt(t.count) + C.reset);
118
+ L.push(' ' + label('Whole-file baseline') + ': ' + humanTokens(t.baseline) + ' tok' +
119
+ ` ${C.dim}← est. cost of feeding full files${C.reset}`);
120
+ L.push(' ' + label('SigMap context') + ': ' + humanTokens(t.actual) + ' tok');
121
+ L.push(' ' + label('Tokens saved') + ': ' + C.bold + humanTokens(t.saved) + C.reset +
122
+ ' (' + colorPct(t.savedPct, fmtPct(t.savedPct)) + ')');
123
+ L.push(' ' + label('Est. money saved') + ': ' + C.bold + C.green + fmtUSD(t.usdSaved) + C.reset +
124
+ ` ${C.dim}(${agg.price.model} input @ $${agg.price.perMtok}/M · --model to change)${C.reset}`);
125
+ L.push(' ' + label('Avg latency') + ': ' + fmtDuration(t.avgMs) + ' / op' +
126
+ ` ${C.dim}(local, no API round-trip)${C.reset}`);
127
+ L.push('');
128
+ L.push(' ' + label('Efficiency') + ': ▕' + bar(t.savedPct, 30) + '▏ ' +
129
+ colorPct(t.savedPct, C.bold + fmtPct(t.savedPct) + C.reset));
130
+ L.push('');
131
+
132
+ // By operation table
133
+ L.push(` ${C.bold}By operation${C.reset}`);
134
+ L.push(' ' + rule(W));
135
+ L.push(' ' + C.dim +
136
+ pad('#', 3) + pad('Operation', 24) + pad('Count', 8, 'right') +
137
+ pad('Saved', 9, 'right') + pad('Avg%', 8, 'right') + pad('Time', 8, 'right') +
138
+ ' Impact' + C.reset);
139
+ agg.byOp.forEach((o, i) => {
140
+ L.push(' ' +
141
+ pad(`${i + 1}.`, 3) +
142
+ pad(o.op, 24) +
143
+ pad(fmtInt(o.count), 8, 'right') +
144
+ pad(humanTokens(o.saved), 9, 'right') +
145
+ pad(colorPct(o.avgPct, fmtPct(o.avgPct)), 8, 'right') +
146
+ pad(fmtDuration(o.avgMs), 8, 'right') +
147
+ ' ' + impactBar(o.sharePct, 12));
148
+ });
149
+ L.push(' ' + rule(W));
150
+ L.push(` ${C.dim}saved = baseline − actual · estimated vs whole-file reads · local-only${C.reset}`);
151
+ L.push('');
152
+ return L.join('\n');
153
+ }
154
+
155
+ /**
156
+ * Render daily / weekly / monthly trend tables.
157
+ * @param {object} agg
158
+ * @returns {string}
159
+ */
160
+ function renderBreakdown(agg) {
161
+ const L = [];
162
+ if (agg.totals.count === 0) return renderSummary(agg);
163
+
164
+ const section = (icon, title, rows, keyHeader) => {
165
+ L.push('');
166
+ L.push(` ${C.bold}${icon} ${title}${C.reset}`);
167
+ L.push(' ' + C.dim +
168
+ pad(keyHeader, 14) + pad('Ops', 7, 'right') + pad('Baseline', 11, 'right') +
169
+ pad('Actual', 10, 'right') + pad('Saved', 10, 'right') + pad('Save%', 8, 'right') +
170
+ pad('Time', 8, 'right') + C.reset);
171
+ let TB = 0, TA = 0, TS = 0, TC = 0, TM = 0;
172
+ for (const r of rows) {
173
+ TB += r.baseline; TA += r.actual; TS += r.saved; TC += r.count; TM += r.ms;
174
+ L.push(' ' +
175
+ pad(r.key, 14) + pad(fmtInt(r.count), 7, 'right') +
176
+ pad(humanTokens(r.baseline), 11, 'right') + pad(humanTokens(r.actual), 10, 'right') +
177
+ pad(humanTokens(r.saved), 10, 'right') +
178
+ pad(colorPct(r.savedPct, fmtPct(r.savedPct)), 8, 'right') +
179
+ pad(fmtDuration(r.ms), 8, 'right'));
180
+ }
181
+ const tp = TB > 0 ? (TS / TB) * 100 : 0;
182
+ L.push(' ' + C.bold +
183
+ pad('TOTAL', 14) + pad(fmtInt(TC), 7, 'right') +
184
+ pad(humanTokens(TB), 11, 'right') + pad(humanTokens(TA), 10, 'right') +
185
+ pad(humanTokens(TS), 10, 'right') + pad(fmtPct(tp), 8, 'right') +
186
+ pad(fmtDuration(TM), 8, 'right') + C.reset);
187
+ };
188
+
189
+ const daily = agg.buckets.daily.slice(-30);
190
+ section('📅', `Daily (last ${daily.length})`, daily, 'Date');
191
+ const weekly = agg.buckets.weekly.slice(-6);
192
+ section('📆', `Weekly (last ${weekly.length})`, weekly, 'Week of');
193
+ const monthly = agg.buckets.monthly.slice(-3);
194
+ section('🗓️', `Monthly (last ${monthly.length})`, monthly, 'Month');
195
+ L.push('');
196
+ L.push(` ${C.dim}saved = baseline − actual · estimated vs whole-file reads · local-only${C.reset}`);
197
+ L.push('');
198
+ return L.join('\n');
199
+ }
200
+
201
+ module.exports = {
202
+ renderSummary,
203
+ renderBreakdown,
204
+ // exported for tests
205
+ humanTokens, fmtUSD, fmtDuration, fmtPct,
206
+ };
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.0.1',
21
+ version: '7.1.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * SigMap usage aggregation for the `gain` dashboard.
5
+ *
6
+ * Pure, zero-dependency functions that turn raw NDJSON usage records into the
7
+ * totals / by-operation / time-bucket shapes the terminal renderer consumes.
8
+ *
9
+ * Tolerant of BOTH schemas:
10
+ * - new: { op, baselineTokens, actualTokens, savedTokens, savedPct, durationMs, model }
11
+ * - legacy: { rawTokens, finalTokens, reductionPct } (from logger.js v0.9)
12
+ *
13
+ * "saved" is a counterfactual estimate (baseline − actual), never a measured
14
+ * delta. Callers are responsible for labeling it as such in the UI.
15
+ */
16
+
17
+ const { resolvePrice } = require('./pricing');
18
+
19
+ const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
20
+
21
+ /**
22
+ * Normalize one raw record into a canonical shape.
23
+ * @param {object} rec
24
+ */
25
+ function normalize(rec) {
26
+ const baseline = num(rec.baselineTokens != null ? rec.baselineTokens : rec.rawTokens);
27
+ const actual = num(rec.actualTokens != null ? rec.actualTokens : rec.finalTokens);
28
+ const saved = rec.savedTokens != null ? num(rec.savedTokens) : Math.max(0, baseline - actual);
29
+ const savedPct = rec.savedPct != null
30
+ ? num(rec.savedPct)
31
+ : rec.reductionPct != null
32
+ ? num(rec.reductionPct)
33
+ : baseline > 0 ? (saved / baseline) * 100 : 0;
34
+ return {
35
+ ts: rec.ts || null,
36
+ op: normalizeOp(rec.op),
37
+ baseline,
38
+ actual,
39
+ saved,
40
+ savedPct: clamp(savedPct, 0, 100),
41
+ durationMs: num(rec.durationMs),
42
+ model: rec.model || null,
43
+ };
44
+ }
45
+
46
+ function normalizeOp(op) {
47
+ if (!op) return 'generate';
48
+ return String(op);
49
+ }
50
+
51
+ function num(v) {
52
+ const n = Number(v);
53
+ return Number.isFinite(n) ? n : 0;
54
+ }
55
+
56
+ /**
57
+ * Parse a --since value into a cutoff Date (or null for "all time").
58
+ * Accepts: "7d", "30d", "12h", or an ISO date "2026-06-01".
59
+ * @param {string} since
60
+ * @param {number} [nowMs] - injectable clock for tests
61
+ * @returns {Date|null}
62
+ */
63
+ function parseSince(since, nowMs) {
64
+ if (!since) return null;
65
+ const now = nowMs != null ? nowMs : Date.now();
66
+ const rel = /^(\d+)([dhw])$/.exec(String(since).trim());
67
+ if (rel) {
68
+ const n = parseInt(rel[1], 10);
69
+ const unit = rel[2];
70
+ const ms = unit === 'h' ? 3.6e6 : unit === 'w' ? 6.048e8 : 8.64e7;
71
+ return new Date(now - n * ms);
72
+ }
73
+ const d = new Date(since);
74
+ return Number.isNaN(d.getTime()) ? null : d;
75
+ }
76
+
77
+ /**
78
+ * Bucket records by calendar granularity.
79
+ * @param {object[]} records - normalized records
80
+ * @param {'day'|'week'|'month'} granularity
81
+ * @returns {Array<{key,count,baseline,actual,saved,savedPct,ms}>} ascending by key
82
+ */
83
+ function bucketBy(records, granularity) {
84
+ const map = new Map();
85
+ for (const r of records) {
86
+ if (!r.ts) continue;
87
+ const key = bucketKey(r.ts, granularity);
88
+ if (!key) continue;
89
+ let b = map.get(key);
90
+ if (!b) { b = { key, count: 0, baseline: 0, actual: 0, saved: 0, ms: 0 }; map.set(key, b); }
91
+ b.count += 1;
92
+ b.baseline += r.baseline;
93
+ b.actual += r.actual;
94
+ b.saved += r.saved;
95
+ b.ms += r.durationMs;
96
+ }
97
+ return [...map.values()]
98
+ .map((b) => ({ ...b, savedPct: b.baseline > 0 ? clamp((b.saved / b.baseline) * 100, 0, 100) : 0 }))
99
+ .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
100
+ }
101
+
102
+ function bucketKey(ts, granularity) {
103
+ const d = new Date(ts);
104
+ if (Number.isNaN(d.getTime())) return null;
105
+ const y = d.getUTCFullYear();
106
+ const m = String(d.getUTCMonth() + 1).padStart(2, '0');
107
+ const day = String(d.getUTCDate()).padStart(2, '0');
108
+ if (granularity === 'month') return `${y}-${m}`;
109
+ if (granularity === 'week') {
110
+ // ISO-ish: key by the Monday (UTC) of that week.
111
+ const tmp = new Date(Date.UTC(y, d.getUTCMonth(), d.getUTCDate()));
112
+ const dow = (tmp.getUTCDay() + 6) % 7; // 0 = Monday
113
+ tmp.setUTCDate(tmp.getUTCDate() - dow);
114
+ return `${tmp.getUTCFullYear()}-${String(tmp.getUTCMonth() + 1).padStart(2, '0')}-${String(tmp.getUTCDate()).padStart(2, '0')}`;
115
+ }
116
+ return `${y}-${m}-${day}`;
117
+ }
118
+
119
+ /**
120
+ * Full aggregation for the `gain` dashboard.
121
+ * @param {object[]} rawRecords
122
+ * @param {object} [opts]
123
+ * @param {string} [opts.model] pricing model
124
+ * @param {string} [opts.since] window filter
125
+ * @param {number} [opts.top] limit byOp rows (0 = all)
126
+ * @param {number} [opts.nowMs] injectable clock
127
+ * @returns {object}
128
+ */
129
+ function aggregate(rawRecords, opts = {}) {
130
+ const price = resolvePrice(opts.model);
131
+ const cutoff = parseSince(opts.since, opts.nowMs);
132
+
133
+ let records = (rawRecords || []).map(normalize);
134
+ if (cutoff) records = records.filter((r) => r.ts && new Date(r.ts) >= cutoff);
135
+
136
+ const totals = {
137
+ count: records.length,
138
+ baseline: 0,
139
+ actual: 0,
140
+ saved: 0,
141
+ totalMs: 0,
142
+ savedPct: 0,
143
+ avgMs: 0,
144
+ usdSaved: 0,
145
+ firstTs: null,
146
+ lastTs: null,
147
+ };
148
+
149
+ const opMap = new Map();
150
+ for (const r of records) {
151
+ totals.baseline += r.baseline;
152
+ totals.actual += r.actual;
153
+ totals.saved += r.saved;
154
+ totals.totalMs += r.durationMs;
155
+ if (r.ts) {
156
+ if (!totals.firstTs || r.ts < totals.firstTs) totals.firstTs = r.ts;
157
+ if (!totals.lastTs || r.ts > totals.lastTs) totals.lastTs = r.ts;
158
+ }
159
+ let o = opMap.get(r.op);
160
+ if (!o) { o = { op: r.op, count: 0, baseline: 0, saved: 0, ms: 0 }; opMap.set(r.op, o); }
161
+ o.count += 1;
162
+ o.baseline += r.baseline;
163
+ o.saved += r.saved;
164
+ o.ms += r.durationMs;
165
+ }
166
+
167
+ totals.savedPct = totals.baseline > 0 ? clamp((totals.saved / totals.baseline) * 100, 0, 100) : 0;
168
+ totals.avgMs = totals.count > 0 ? Math.round(totals.totalMs / totals.count) : 0;
169
+ totals.usdSaved = totals.saved * price.perToken;
170
+
171
+ let byOp = [...opMap.values()].map((o) => ({
172
+ op: o.op,
173
+ count: o.count,
174
+ saved: o.saved,
175
+ avgPct: o.baseline > 0 ? clamp((o.saved / o.baseline) * 100, 0, 100) : 0,
176
+ avgMs: o.count > 0 ? Math.round(o.ms / o.count) : 0,
177
+ usdSaved: o.saved * price.perToken,
178
+ sharePct: totals.saved > 0 ? (o.saved / totals.saved) * 100 : 0,
179
+ })).sort((a, b) => b.saved - a.saved);
180
+
181
+ if (opts.top && opts.top > 0) byOp = byOp.slice(0, opts.top);
182
+
183
+ return {
184
+ price,
185
+ totals,
186
+ byOp,
187
+ buckets: {
188
+ daily: bucketBy(records, 'day'),
189
+ weekly: bucketBy(records, 'week'),
190
+ monthly: bucketBy(records, 'month'),
191
+ },
192
+ };
193
+ }
194
+
195
+ module.exports = { aggregate, bucketBy, parseSince, normalize };
@@ -18,6 +18,9 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
 
20
20
  const LOG_FILE = path.join('.context', 'usage.ndjson');
21
+ // Dedicated log for the `gain` dashboard (extended schema). Kept separate from
22
+ // usage.ndjson so the legacy health/nudge history never collides with it.
23
+ const GAIN_FILE = path.join('.context', 'gain.ndjson');
21
24
 
22
25
  /**
23
26
  * Append one run entry to the usage log.
@@ -73,6 +76,25 @@ function readLog(cwd) {
73
76
  }
74
77
  }
75
78
 
79
+ /**
80
+ * Read and parse all `gain` dashboard records (oldest first).
81
+ * @param {string} cwd
82
+ * @returns {object[]}
83
+ */
84
+ function readGainLog(cwd) {
85
+ try {
86
+ const logPath = path.join(cwd, GAIN_FILE);
87
+ if (!fs.existsSync(logPath)) return [];
88
+ return fs.readFileSync(logPath, 'utf8')
89
+ .split('\n')
90
+ .filter(Boolean)
91
+ .map((line) => { try { return JSON.parse(line); } catch (_) { return null; } })
92
+ .filter(Boolean);
93
+ } catch (_) {
94
+ return [];
95
+ }
96
+ }
97
+
76
98
  /**
77
99
  * Compute summary statistics from an array of log records.
78
100
  * @param {object[]} entries
@@ -112,4 +134,64 @@ function summarize(entries) {
112
134
  };
113
135
  }
114
136
 
115
- module.exports = { logRun, readLog, summarize };
137
+ /**
138
+ * Whether `gain` savings capture is enabled. Default: ON (privacy-safe,
139
+ * local-only, counts only — no paths, source, or query text). This is
140
+ * intentionally decoupled from the legacy `config.tracking` flag (which gates
141
+ * the usage.ndjson health log and defaults OFF). Opt out of gain capture via:
142
+ * config.gainTracking === false · --no-track · SIGMAP_NO_TRACK=1
143
+ * @param {object} [config]
144
+ * @param {string[]} [argv]
145
+ * @returns {boolean}
146
+ */
147
+ function isTrackingEnabled(config, argv) {
148
+ const a = argv || (typeof process !== 'undefined' ? process.argv : []);
149
+ if (process.env && process.env.SIGMAP_NO_TRACK) return false;
150
+ if (a && a.includes('--no-track')) return false;
151
+ if (config && config.gainTracking === false) return false;
152
+ return true;
153
+ }
154
+
155
+ /**
156
+ * Append one operation to the usage log using the extended `gain` schema.
157
+ * Reuses the same NDJSON file as logRun and is tolerant of partial input.
158
+ * Never throws — tracking must never break the main process.
159
+ *
160
+ * @param {object} entry
161
+ * @param {string} entry.op e.g. 'ask' | 'generate' | 'query' | 'mcp:get_map'
162
+ * @param {number} entry.baselineTokens whole-file / candidate baseline (counterfactual)
163
+ * @param {number} entry.actualTokens tokens SigMap actually emitted
164
+ * @param {number} [entry.durationMs]
165
+ * @param {string} [entry.model]
166
+ * @param {string} [entry.version]
167
+ * @param {string} cwd
168
+ */
169
+ function recordUsage(entry, cwd) {
170
+ try {
171
+ const logPath = path.join(cwd, GAIN_FILE);
172
+ const dir = path.dirname(logPath);
173
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
174
+
175
+ const baseline = Math.max(0, Number(entry.baselineTokens) || 0);
176
+ const actual = Math.max(0, Number(entry.actualTokens) || 0);
177
+ const saved = Math.max(0, baseline - actual);
178
+ const record = {
179
+ ts: new Date().toISOString(),
180
+ v: entry.version || '0.9.0',
181
+ op: entry.op || 'generate',
182
+ baselineTokens: baseline,
183
+ actualTokens: actual,
184
+ savedTokens: saved,
185
+ savedPct: baseline > 0 ? parseFloat(((saved / baseline) * 100).toFixed(1)) : 0,
186
+ durationMs: Math.max(0, Math.round(Number(entry.durationMs) || 0)),
187
+ model: entry.model || null,
188
+ ok: entry.ok !== false,
189
+ };
190
+ fs.appendFileSync(logPath, JSON.stringify(record) + '\n', 'utf8');
191
+ } catch (err) {
192
+ // Never crash the main process — tracking is optional.
193
+ if (process.stderr) process.stderr.write(`[sigmap] tracking: could not write log: ${err.message}\n`);
194
+ }
195
+ }
196
+
197
+ module.exports = { logRun, recordUsage, readLog, readGainLog, summarize, isTrackingEnabled, GAIN_FILE };
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * SigMap pricing table — input-token $/Mtok assumptions for the `gain` dashboard.
5
+ *
6
+ * These are ASSUMPTIONS used only to translate "tokens saved" into an estimated
7
+ * dollar figure. They are deliberately conservative and configurable via
8
+ * --model <name> or config.pricingModel
9
+ * The `gain` views always print the model + rate inline so the $ is never
10
+ * presented as exact. Zero npm dependencies.
11
+ */
12
+
13
+ // USD per 1,000,000 input tokens.
14
+ const PRICES = {
15
+ 'claude-sonnet': 3.0,
16
+ 'claude-opus': 15.0,
17
+ 'claude-haiku': 0.8,
18
+ 'gpt-4o': 2.5,
19
+ 'gpt-4o-mini': 0.15,
20
+ 'gemini-1.5-pro': 1.25,
21
+ 'gemini-1.5-flash': 0.075,
22
+ };
23
+
24
+ const DEFAULT_MODEL = 'claude-sonnet';
25
+
26
+ /**
27
+ * Resolve a price (USD per token) for a model name.
28
+ * @param {string} [model]
29
+ * @returns {{ model: string, perMtok: number, perToken: number }}
30
+ */
31
+ function resolvePrice(model) {
32
+ const key = (model || DEFAULT_MODEL).toLowerCase();
33
+ const perMtok = PRICES[key] != null ? PRICES[key] : PRICES[DEFAULT_MODEL];
34
+ const resolved = PRICES[key] != null ? key : DEFAULT_MODEL;
35
+ return { model: resolved, perMtok, perToken: perMtok / 1_000_000 };
36
+ }
37
+
38
+ /** @returns {string[]} known model keys */
39
+ function listModels() {
40
+ return Object.keys(PRICES);
41
+ }
42
+
43
+ module.exports = { PRICES, DEFAULT_MODEL, resolvePrice, listModels };