depgraph-core 1.8.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/depgraph.js CHANGED
@@ -65,7 +65,8 @@ var require_constants = __commonJS({
65
65
  ".vue",
66
66
  ".svelte",
67
67
  ".dart",
68
- ".rs"
68
+ ".rs",
69
+ ".sql"
69
70
  ]);
70
71
  exports2.MAX_FILE_SIZE = 3e5;
71
72
  exports2.MAX_BFS_DEPTH = 10;
@@ -3466,6 +3467,322 @@ var require_rust = __commonJS({
3466
3467
  }
3467
3468
  });
3468
3469
 
3470
+ // dist/languages/sql/helpers.js
3471
+ var require_helpers = __commonJS({
3472
+ "dist/languages/sql/helpers.js"(exports2) {
3473
+ "use strict";
3474
+ Object.defineProperty(exports2, "__esModule", { value: true });
3475
+ exports2.NON_TABLES = exports2.ROUTINE_RECOVERY_RX = exports2.QUAL_NAME = exports2.NAME_PART = void 0;
3476
+ exports2.lineOf = lineOf;
3477
+ exports2.normIdent = normIdent;
3478
+ exports2.maskSqlComments = maskSqlComments;
3479
+ exports2.collectCteNames = collectCteNames;
3480
+ exports2.collectTableRefs = collectTableRefs;
3481
+ exports2.collectFkRefs = collectFkRefs;
3482
+ exports2.NAME_PART = '(?:"(?:[^"\\n]|"")*"|`(?:[^`\\n]|``)*`|\\[(?:[^\\]\\n]|\\]\\])*\\]|[\\w$]+)';
3483
+ exports2.QUAL_NAME = `${exports2.NAME_PART}(?:\\s*\\.\\s*${exports2.NAME_PART})*`;
3484
+ exports2.ROUTINE_RECOVERY_RX = new RegExp(`\\bCREATE\\s+(?:OR\\s+(?:REPLACE|ALTER)\\s+)?(?:FUNCTION|PROC(?:EDURE)?)\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${exports2.QUAL_NAME})`, "gi");
3485
+ function lineOf(text, offset) {
3486
+ return text.slice(0, offset).split("\n").length;
3487
+ }
3488
+ function normIdent(name) {
3489
+ return name.split(".").map((p) => {
3490
+ const s = p.trim();
3491
+ if (s.length >= 2 && (s[0] === s[s.length - 1] && (s[0] === '"' || s[0] === "`") || s[0] === "[" && s[s.length - 1] === "]")) {
3492
+ return s.slice(1, -1).toLowerCase();
3493
+ }
3494
+ return s.toLowerCase();
3495
+ }).join(".");
3496
+ }
3497
+ function maskSqlComments(text) {
3498
+ const out = [];
3499
+ let i = 0;
3500
+ const n = text.length;
3501
+ function blank(upto) {
3502
+ for (const ch of text.slice(i, upto))
3503
+ out.push(ch === "\n" ? "\n" : " ");
3504
+ return upto;
3505
+ }
3506
+ while (i < n) {
3507
+ const c = text[i];
3508
+ if (c === "'") {
3509
+ let j = i + 1;
3510
+ while (j < n && text[j] !== "\n") {
3511
+ if (text[j] === "'") {
3512
+ if (j + 1 < n && text[j + 1] === "'") {
3513
+ j += 2;
3514
+ continue;
3515
+ }
3516
+ j++;
3517
+ break;
3518
+ }
3519
+ j++;
3520
+ }
3521
+ i = blank(j);
3522
+ } else if (c === '"' || c === "`" || c === "[") {
3523
+ const closer = c === "[" ? "]" : c;
3524
+ let j = i + 1;
3525
+ let closed = false;
3526
+ while (j < n && text[j] !== "\n") {
3527
+ if (text[j] === closer) {
3528
+ if (j + 1 < n && text[j + 1] === closer) {
3529
+ j += 2;
3530
+ continue;
3531
+ }
3532
+ j++;
3533
+ closed = true;
3534
+ break;
3535
+ }
3536
+ j++;
3537
+ }
3538
+ const span = text.slice(i, j);
3539
+ if (closed && !span.includes("--") && !span.includes("/*")) {
3540
+ out.push(span);
3541
+ i = j;
3542
+ } else {
3543
+ let eol = text.indexOf("\n", i);
3544
+ if (eol === -1)
3545
+ eol = n;
3546
+ i = blank(eol);
3547
+ }
3548
+ } else if (c === "-" && i + 1 < n && text[i + 1] === "-") {
3549
+ let j = i;
3550
+ while (j < n && text[j] !== "\n")
3551
+ j++;
3552
+ i = blank(j);
3553
+ } else if (c === "/" && i + 1 < n && text[i + 1] === "*") {
3554
+ let depth = 1;
3555
+ let j = i + 2;
3556
+ while (j < n && depth > 0) {
3557
+ if (text[j] === "/" && j + 1 < n && text[j + 1] === "*") {
3558
+ depth++;
3559
+ j += 2;
3560
+ } else if (text[j] === "*" && j + 1 < n && text[j + 1] === "/") {
3561
+ depth--;
3562
+ j += 2;
3563
+ } else
3564
+ j++;
3565
+ }
3566
+ i = blank(j);
3567
+ } else {
3568
+ out.push(c);
3569
+ i++;
3570
+ }
3571
+ }
3572
+ return out.join("");
3573
+ }
3574
+ var NON_TABLES = /* @__PURE__ */ new Set([
3575
+ "select",
3576
+ "where",
3577
+ "set",
3578
+ "dual",
3579
+ "null",
3580
+ "true",
3581
+ "false",
3582
+ "first",
3583
+ "skip",
3584
+ "rows",
3585
+ "next",
3586
+ "only",
3587
+ "lateral",
3588
+ "values",
3589
+ "inserted",
3590
+ "deleted",
3591
+ "new",
3592
+ "old"
3593
+ ]);
3594
+ exports2.NON_TABLES = NON_TABLES;
3595
+ function collectCteNames(text) {
3596
+ const ctes = /* @__PURE__ */ new Set();
3597
+ const rx = /\bWITH\s+(?:RECURSIVE\s+)?([\w$]+)\s*(?:\([^()]*\))?\s+AS\s*\(/gi;
3598
+ for (const m of text.matchAll(rx))
3599
+ ctes.add(normIdent(m[1]));
3600
+ return ctes;
3601
+ }
3602
+ function collectTableRefs(masked, extraNonTables = /* @__PURE__ */ new Set()) {
3603
+ const skip = /* @__PURE__ */ new Set([...NON_TABLES, ...extraNonTables]);
3604
+ const refs = [];
3605
+ const seen = /* @__PURE__ */ new Set();
3606
+ const rx = new RegExp(`\\b(?:FROM|JOIN|INTO|UPDATE)\\s+(${exports2.QUAL_NAME})`, "gi");
3607
+ for (const m of masked.matchAll(rx)) {
3608
+ const raw = m[1];
3609
+ const key = normIdent(raw);
3610
+ if (skip.has(key) || seen.has(key))
3611
+ continue;
3612
+ seen.add(key);
3613
+ refs.push({ name: raw, line: lineOf(masked, m.index ?? 0) });
3614
+ }
3615
+ return refs;
3616
+ }
3617
+ function collectFkRefs(masked) {
3618
+ const refs = [];
3619
+ const seen = /* @__PURE__ */ new Set();
3620
+ const rx = new RegExp(`\\bREFERENCES\\s+(${exports2.QUAL_NAME})`, "gi");
3621
+ for (const m of masked.matchAll(rx)) {
3622
+ const raw = m[1];
3623
+ const key = normIdent(raw);
3624
+ if (seen.has(key))
3625
+ continue;
3626
+ seen.add(key);
3627
+ refs.push({ name: raw, line: lineOf(masked, m.index ?? 0) });
3628
+ }
3629
+ return refs;
3630
+ }
3631
+ }
3632
+ });
3633
+
3634
+ // dist/languages/sql/patterns.js
3635
+ var require_patterns = __commonJS({
3636
+ "dist/languages/sql/patterns.js"(exports2) {
3637
+ "use strict";
3638
+ Object.defineProperty(exports2, "__esModule", { value: true });
3639
+ exports2.sqlEntityPatterns = void 0;
3640
+ var helpers_1 = require_helpers();
3641
+ exports2.sqlEntityPatterns = [
3642
+ {
3643
+ regex: new RegExp(`\\bCREATE\\s+(?:TEMP(?:ORARY)?\\s+)?TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${helpers_1.QUAL_NAME})`, "gi"),
3644
+ type: "table"
3645
+ },
3646
+ {
3647
+ regex: new RegExp(`\\bCREATE\\s+(?:OR\\s+(?:REPLACE|ALTER)\\s+)?(?:MATERIALIZED\\s+)?VIEW\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${helpers_1.QUAL_NAME})`, "gi"),
3648
+ type: "view"
3649
+ },
3650
+ {
3651
+ regex: new RegExp(`\\bCREATE\\s+(?:OR\\s+(?:REPLACE|ALTER)\\s+)?FUNCTION\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${helpers_1.QUAL_NAME})`, "gi"),
3652
+ type: "function"
3653
+ },
3654
+ {
3655
+ regex: new RegExp(`\\bCREATE\\s+(?:OR\\s+(?:REPLACE|ALTER)\\s+)?PROC(?:EDURE)?\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${helpers_1.QUAL_NAME})`, "gi"),
3656
+ type: "procedure"
3657
+ },
3658
+ {
3659
+ regex: new RegExp(`\\bCREATE\\s+(?:OR\\s+(?:REPLACE|ALTER)\\s+)?TRIGGER\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${helpers_1.QUAL_NAME})`, "gi"),
3660
+ type: "trigger"
3661
+ },
3662
+ {
3663
+ regex: new RegExp(`\\bCREATE\\s+(?:UNIQUE\\s+)?INDEX\\s+(?:CONCURRENTLY\\s+)?(?:IF\\s+NOT\\s+EXISTS\\s+)?(${helpers_1.QUAL_NAME})\\s+ON\\b`, "gi"),
3664
+ type: "index"
3665
+ }
3666
+ ];
3667
+ }
3668
+ });
3669
+
3670
+ // dist/languages/sql/extractor.js
3671
+ var require_extractor = __commonJS({
3672
+ "dist/languages/sql/extractor.js"(exports2) {
3673
+ "use strict";
3674
+ var __importDefault2 = exports2 && exports2.__importDefault || function(mod) {
3675
+ return mod && mod.__esModule ? mod : { "default": mod };
3676
+ };
3677
+ Object.defineProperty(exports2, "__esModule", { value: true });
3678
+ exports2.extractEntities = extractEntities;
3679
+ exports2.extractImports = extractImports;
3680
+ exports2.extractExports = extractExports;
3681
+ var path_1 = __importDefault2(require("path"));
3682
+ var patterns_1 = require_patterns();
3683
+ var helpers_1 = require_helpers();
3684
+ var _currentFile = "";
3685
+ function extractEntities(code, filePath) {
3686
+ _currentFile = filePath;
3687
+ const masked = (0, helpers_1.maskSqlComments)(code);
3688
+ const entities = [];
3689
+ const seenNames = /* @__PURE__ */ new Set();
3690
+ for (const { regex, type } of patterns_1.sqlEntityPatterns) {
3691
+ regex.lastIndex = 0;
3692
+ for (const m of masked.matchAll(regex)) {
3693
+ const raw = m[1].trim();
3694
+ const key = (0, helpers_1.normIdent)(raw);
3695
+ if (seenNames.has(key))
3696
+ continue;
3697
+ seenNames.add(key);
3698
+ entities.push({ name: raw, type, line: (0, helpers_1.lineOf)(code, m.index ?? 0), complexity: "low" });
3699
+ }
3700
+ }
3701
+ helpers_1.ROUTINE_RECOVERY_RX.lastIndex = 0;
3702
+ for (const m of masked.matchAll(helpers_1.ROUTINE_RECOVERY_RX)) {
3703
+ const raw = m[1].trim();
3704
+ const key = (0, helpers_1.normIdent)(raw);
3705
+ if (seenNames.has(key))
3706
+ continue;
3707
+ seenNames.add(key);
3708
+ entities.push({ name: `${raw}()`, type: "procedure", line: (0, helpers_1.lineOf)(code, m.index ?? 0), complexity: "low" });
3709
+ }
3710
+ return entities;
3711
+ }
3712
+ function extractImports(code) {
3713
+ const masked = (0, helpers_1.maskSqlComments)(code);
3714
+ const imports = [];
3715
+ const fileBase = path_1.default.basename(_currentFile, path_1.default.extname(_currentFile));
3716
+ if (!fileBase)
3717
+ return imports;
3718
+ const ctes = (0, helpers_1.collectCteNames)(masked);
3719
+ for (const ref of (0, helpers_1.collectFkRefs)(masked)) {
3720
+ imports.push({ source: fileBase, names: [ref.name], isLocal: true });
3721
+ }
3722
+ for (const ref of (0, helpers_1.collectTableRefs)(masked, ctes)) {
3723
+ imports.push({ source: fileBase, names: [ref.name], isLocal: true });
3724
+ }
3725
+ return imports;
3726
+ }
3727
+ function extractExports(code) {
3728
+ const masked = (0, helpers_1.maskSqlComments)(code);
3729
+ const names = [];
3730
+ const seen = /* @__PURE__ */ new Set();
3731
+ for (const { regex } of patterns_1.sqlEntityPatterns) {
3732
+ regex.lastIndex = 0;
3733
+ for (const m of masked.matchAll(regex)) {
3734
+ const key = (0, helpers_1.normIdent)(m[1].trim());
3735
+ if (!seen.has(key)) {
3736
+ seen.add(key);
3737
+ names.push(m[1].trim());
3738
+ }
3739
+ }
3740
+ }
3741
+ return names;
3742
+ }
3743
+ }
3744
+ });
3745
+
3746
+ // dist/languages/sql/index.js
3747
+ var require_sql = __commonJS({
3748
+ "dist/languages/sql/index.js"(exports2) {
3749
+ "use strict";
3750
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
3751
+ if (k2 === void 0) k2 = k;
3752
+ var desc = Object.getOwnPropertyDescriptor(m, k);
3753
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
3754
+ desc = { enumerable: true, get: function() {
3755
+ return m[k];
3756
+ } };
3757
+ }
3758
+ Object.defineProperty(o, k2, desc);
3759
+ }) : (function(o, m, k, k2) {
3760
+ if (k2 === void 0) k2 = k;
3761
+ o[k2] = m[k];
3762
+ }));
3763
+ var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
3764
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
3765
+ };
3766
+ Object.defineProperty(exports2, "__esModule", { value: true });
3767
+ exports2.SqlParser = void 0;
3768
+ var registry_1 = require_registry();
3769
+ var patterns_1 = require_patterns();
3770
+ var extractor_1 = require_extractor();
3771
+ __exportStar(require_helpers(), exports2);
3772
+ __exportStar(require_patterns(), exports2);
3773
+ __exportStar(require_extractor(), exports2);
3774
+ exports2.SqlParser = {
3775
+ lang: "sql",
3776
+ extensions: [".sql"],
3777
+ extractEntities: extractor_1.extractEntities,
3778
+ extractImports: extractor_1.extractImports,
3779
+ extractExports: extractor_1.extractExports,
3780
+ entityPatterns: patterns_1.sqlEntityPatterns
3781
+ };
3782
+ (0, registry_1.registerParser)(exports2.SqlParser);
3783
+ }
3784
+ });
3785
+
3469
3786
  // dist/stages/collector.js
3470
3787
  var require_collector = __commonJS({
3471
3788
  "dist/stages/collector.js"(exports2) {
@@ -3683,7 +4000,8 @@ var require_graph = __commonJS({
3683
4000
  `${base}/index.ts`,
3684
4001
  `${base}/index.js`,
3685
4002
  `${base}.dart`,
3686
- `${base}.rs`
4003
+ `${base}.rs`,
4004
+ `${base}.sql`
3687
4005
  ];
3688
4006
  for (const candidate of candidates) {
3689
4007
  const normalized = candidate.replace(/\\/g, "/");
@@ -4145,6 +4463,7 @@ require_ruby();
4145
4463
  require_swift();
4146
4464
  require_dart();
4147
4465
  require_rust();
4466
+ require_sql();
4148
4467
  var fs_1 = __importDefault(require("fs"));
4149
4468
  var collector_1 = require_collector();
4150
4469
  var parser_1 = require_parser();
@@ -4173,7 +4492,7 @@ function getFlag(flag) {
4173
4492
  }
4174
4493
  function printHelp() {
4175
4494
  console.log(`
4176
- ${bold("DepGraph")} ${dim("v1.8.0")}
4495
+ ${bold("DepGraph")} ${dim("v1.9.1")}
4177
4496
  ${dim("Dependency mapping \xB7 Impact simulation \xB7 Developer intelligence")}
4178
4497
 
4179
4498
  ${bold("USAGE")}
@@ -4222,7 +4541,7 @@ ${bold("GIT EXAMPLES")}
4222
4541
  function printBanner() {
4223
4542
  console.log(`
4224
4543
  ${bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501")}
4225
- ${bold(" DepGraph")} ${dim("v1.8.0")}
4544
+ ${bold(" DepGraph")} ${dim("v1.9.1")}
4226
4545
  ${bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501")}
4227
4546
  `);
4228
4547
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depgraph-core",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
4
4
  "description": "Dependency mapping and impact simulation for JS/TS projects",
5
5
  "main": "depgraph.js",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "release:mcp": "npm run build && npm run bundle:mcp",
16
16
  "release:all": "npm run build && npm run bundle && npm run bundle:mcp",
17
17
  "mcp:register": "claude mcp add depgraph -- node $(pwd)/depgraph-mcp.js",
18
+ "benchmark": "node scripts/benchmark.js",
18
19
  "test": "vitest",
19
20
  "test:run": "vitest run"
20
21
  },
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ // Measures real response sizes from depgraph-mcp.js for each tool variant.
3
+ // Usage:
4
+ // node scripts/benchmark.js
5
+ // BENCH_DIR=/other/project node scripts/benchmark.js
6
+ 'use strict';
7
+ const { execSync } = require('child_process');
8
+ const path = require('path');
9
+
10
+ const SERVER = path.resolve(__dirname, '..', 'depgraph-mcp.js');
11
+ const DIR = process.env.BENCH_DIR ?? path.resolve(__dirname, '..');
12
+
13
+ const os = require('os');
14
+ const fs = require('fs');
15
+ const TMPF = path.join(os.tmpdir(), `depgraph-bench-${process.pid}.txt`);
16
+
17
+ function call(toolName, args) {
18
+ const messages = [
19
+ {
20
+ jsonrpc: '2.0', id: 1, method: 'initialize',
21
+ params: { protocolVersion: '2024-11-05', capabilities: {},
22
+ clientInfo: { name: 'bench', version: '1.0' } },
23
+ },
24
+ {
25
+ jsonrpc: '2.0', id: 2, method: 'tools/call',
26
+ params: { name: toolName, arguments: args },
27
+ },
28
+ ].map(m => JSON.stringify(m)).join('\n') + '\n';
29
+
30
+ fs.writeFileSync(TMPF, messages, 'utf-8');
31
+
32
+ const out = execSync(
33
+ `cat ${TMPF} | node ${SERVER}`,
34
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
35
+ );
36
+
37
+ const lines = out.trim().split('\n').filter(Boolean);
38
+ const result = JSON.parse(lines[lines.length - 1]);
39
+ return result?.result?.content?.[0]?.text ?? '';
40
+ }
41
+
42
+ // ~3.5 chars/token for dense JSON; ~4.0 for prose (matches Claude/GPT-4 closely enough)
43
+ const tokEst = (s, prose = false) => Math.round(s.length / (prose ? 4.0 : 3.5));
44
+
45
+ const CASES = [
46
+ {
47
+ label: 'analyze_project — full (BASELINE)',
48
+ fn: () => call('analyze_project', { projectDir: DIR }),
49
+ },
50
+ {
51
+ label: 'analyze_project — verbosity:"overview"',
52
+ fn: () => call('analyze_project', { projectDir: DIR, verbosity: 'overview' }),
53
+ },
54
+ {
55
+ label: 'analyze_project — verbosity:"sketch"',
56
+ fn: () => call('analyze_project', { projectDir: DIR, verbosity: 'sketch' }),
57
+ },
58
+ {
59
+ label: 'analyze_project — focus + depth:2 (overview)',
60
+ fn: () => call('analyze_project',
61
+ { projectDir: DIR, focus: 'buildGraph', depth: 2, verbosity: 'overview' }),
62
+ },
63
+ {
64
+ label: 'analyze_project — focus + depth:1 (overview)',
65
+ fn: () => call('analyze_project',
66
+ { projectDir: DIR, focus: 'buildGraph', depth: 1, verbosity: 'overview' }),
67
+ },
68
+ {
69
+ label: 'get_graph_summary — overview',
70
+ fn: () => call('get_graph_summary', { projectDir: DIR }),
71
+ },
72
+ {
73
+ label: 'get_graph_summary — verbosity:"sketch"',
74
+ fn: () => call('get_graph_summary', { projectDir: DIR, verbosity: 'sketch' }),
75
+ },
76
+ {
77
+ label: 'get_graph_summary — format:"prose"',
78
+ fn: () => call('get_graph_summary', { projectDir: DIR, format: 'prose' }),
79
+ prose: true,
80
+ },
81
+ {
82
+ label: 'describe_node("buildGraph")',
83
+ fn: () => call('describe_node', { projectDir: DIR, nodeName: 'buildGraph' }),
84
+ },
85
+ {
86
+ label: 'describe_node("simulateImpact")',
87
+ fn: () => call('describe_node', { projectDir: DIR, nodeName: 'simulateImpact' }),
88
+ },
89
+ {
90
+ label: 'simulate_impact("buildGraph", ...)',
91
+ fn: () => call('simulate_impact', {
92
+ projectDir: DIR,
93
+ targetNode: 'buildGraph',
94
+ changeDescription: 'removing parsed arg',
95
+ }),
96
+ },
97
+ ];
98
+
99
+ console.log(`\nBenchmarking against: ${DIR}`);
100
+ console.log(`MCP server: ${SERVER}\n`);
101
+
102
+ const rows = CASES.map(c => {
103
+ process.stdout.write(` ${c.label} ...`);
104
+ const text = c.fn();
105
+ const chars = text.length;
106
+ const tokens = tokEst(text, c.prose ?? false);
107
+ process.stdout.write(` ${tokens.toLocaleString()} tokens\n`);
108
+ return { label: c.label, chars, tokens };
109
+ });
110
+
111
+ const baseline = rows[0].tokens;
112
+
113
+ const W = { label: 50, chars: 10, tokens: 9, pct: 14 };
114
+ const rule = '─'.repeat(W.label + W.chars + W.tokens + W.pct + 4);
115
+
116
+ console.log('\n' + rule);
117
+ console.log(
118
+ 'Tool call'.padEnd(W.label) +
119
+ 'Chars'.padStart(W.chars) +
120
+ 'Tokens'.padStart(W.tokens) +
121
+ 'vs baseline'.padStart(W.pct)
122
+ );
123
+ console.log(rule);
124
+
125
+ rows.forEach((r, i) => {
126
+ const pct = i === 0
127
+ ? '(baseline)'
128
+ : `−${Math.round((1 - r.tokens / baseline) * 100)} %`;
129
+ console.log(
130
+ r.label.padEnd(W.label) +
131
+ r.chars.toLocaleString().padStart(W.chars) +
132
+ r.tokens.toLocaleString().padStart(W.tokens) +
133
+ pct.padStart(W.pct)
134
+ );
135
+ });
136
+
137
+ console.log(rule);
138
+ console.log(`\nBaseline: ${baseline.toLocaleString()} tokens (${rows[0].chars.toLocaleString()} chars)\n`);