ctxdiet 0.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/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/src/agents.js +81 -0
- package/dist/src/constants.js +31 -0
- package/dist/src/fix.js +270 -0
- package/dist/src/index.js +76 -0
- package/dist/src/pricing.js +13 -0
- package/dist/src/report.js +181 -0
- package/dist/src/scan.js +211 -0
- package/dist/src/sources.js +347 -0
- package/dist/src/tokens.js +16 -0
- package/dist/src/trim.js +69 -0
- package/dist/src/types.js +2 -0
- package/package.json +63 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.printScanResult = printScanResult;
|
|
7
|
+
exports.printBeforeAfter = printBeforeAfter;
|
|
8
|
+
exports.toJson = toJson;
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
11
|
+
const pricing_1 = require("./pricing");
|
|
12
|
+
const sources_1 = require("./sources");
|
|
13
|
+
const fmt = (n) => Math.round(n).toLocaleString("en-US");
|
|
14
|
+
const usd = (n) => `$${n.toFixed(2)}`;
|
|
15
|
+
function dollars(tokens, o) {
|
|
16
|
+
return usd((0, pricing_1.monthlyCost)(tokens, o.sessionsPerMonth, o.model));
|
|
17
|
+
}
|
|
18
|
+
function gradeBadge(g) {
|
|
19
|
+
const paint = g === "A" || g === "B"
|
|
20
|
+
? chalk_1.default.bgGreen.black
|
|
21
|
+
: g === "C"
|
|
22
|
+
? chalk_1.default.bgYellow.black
|
|
23
|
+
: chalk_1.default.bgRed.white;
|
|
24
|
+
return paint.bold(` ${g} `);
|
|
25
|
+
}
|
|
26
|
+
function printScanResult(r, o) {
|
|
27
|
+
if (o.json) {
|
|
28
|
+
console.log(JSON.stringify(toJson(r), null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log();
|
|
32
|
+
console.log(chalk_1.default.bold("ctxdiet") +
|
|
33
|
+
chalk_1.default.dim(` · ${(0, sources_1.shortenPath)(o.path, o.home)} · grade `) +
|
|
34
|
+
gradeBadge(r.grade));
|
|
35
|
+
if (r.detectedAgents.length === 0) {
|
|
36
|
+
console.log();
|
|
37
|
+
console.log(chalk_1.default.dim("No agent setup detected here. Supported: Claude Code, Codex/AGENTS.md, " +
|
|
38
|
+
"Cursor, Gemini CLI, Windsurf, GitHub Copilot."));
|
|
39
|
+
console.log();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log(chalk_1.default.dim("Detected agents: ") +
|
|
43
|
+
r.detectedAgents.map((a) => chalk_1.default.cyan(a.label)).join(chalk_1.default.dim(", ")));
|
|
44
|
+
console.log();
|
|
45
|
+
const high = r.findings.filter((f) => f.confidence === "high");
|
|
46
|
+
const low = r.findings.filter((f) => f.confidence === "low");
|
|
47
|
+
const table = new cli_table3_1.default({
|
|
48
|
+
head: ["Agent", "Type", "Finding", "Tokens/session", "$/month"].map((h) => chalk_1.default.bold(h)),
|
|
49
|
+
colAligns: ["left", "left", "left", "right", "right"],
|
|
50
|
+
style: { head: [], border: [] },
|
|
51
|
+
});
|
|
52
|
+
for (const agent of r.detectedAgents) {
|
|
53
|
+
const items = high.filter((f) => f.agent === agent.label);
|
|
54
|
+
if (items.length === 0) {
|
|
55
|
+
const hasReview = low.some((f) => f.agent === agent.label);
|
|
56
|
+
table.push([
|
|
57
|
+
agent.label,
|
|
58
|
+
"—",
|
|
59
|
+
hasReview
|
|
60
|
+
? chalk_1.default.dim("review items below")
|
|
61
|
+
: chalk_1.default.green("clean"),
|
|
62
|
+
chalk_1.default.dim("0"),
|
|
63
|
+
chalk_1.default.dim("$0.00"),
|
|
64
|
+
]);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
for (const f of items) {
|
|
68
|
+
table.push([
|
|
69
|
+
agent.label,
|
|
70
|
+
f.category,
|
|
71
|
+
f.title,
|
|
72
|
+
f.tokensPerSession > 0
|
|
73
|
+
? chalk_1.default.green(fmt(f.tokensPerSession))
|
|
74
|
+
: chalk_1.default.dim("0"),
|
|
75
|
+
f.tokensPerSession > 0
|
|
76
|
+
? chalk_1.default.green(dollars(f.tokensPerSession, o))
|
|
77
|
+
: chalk_1.default.dim("$0.00"),
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
console.log(table.toString());
|
|
82
|
+
console.log();
|
|
83
|
+
// Headline — the whole pitch.
|
|
84
|
+
const acrossNote = r.detectedAgents.length > 1
|
|
85
|
+
? chalk_1.default.dim(` (summed across ${r.detectedAgents.length} agents)`)
|
|
86
|
+
: "";
|
|
87
|
+
console.log(chalk_1.default.bold("Estimated savings if fixed: ") +
|
|
88
|
+
chalk_1.default.bold.green(`~${fmt(r.headlineSavings)} tokens/session (~${dollars(r.headlineSavings, o)}/month)`) +
|
|
89
|
+
acrossNote);
|
|
90
|
+
// LOW-confidence review section — explicitly not counted above.
|
|
91
|
+
if (low.length > 0) {
|
|
92
|
+
console.log();
|
|
93
|
+
console.log(chalk_1.default.yellow.bold("Review — ctxdiet won't change these on its own"));
|
|
94
|
+
console.log(chalk_1.default.dim("They cost tokens every session; only you know if they're still in use."));
|
|
95
|
+
const reviewTable = new cli_table3_1.default({
|
|
96
|
+
head: ["Agent", "Type", "Item", "Est. tokens/session"].map((h) => chalk_1.default.dim(h)),
|
|
97
|
+
colAligns: ["left", "left", "left", "right"],
|
|
98
|
+
style: { head: [], border: [] },
|
|
99
|
+
});
|
|
100
|
+
for (const f of low) {
|
|
101
|
+
reviewTable.push([
|
|
102
|
+
f.agent,
|
|
103
|
+
f.category,
|
|
104
|
+
f.title,
|
|
105
|
+
chalk_1.default.yellow(fmt(f.tokensPerSession)),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
console.log(reviewTable.toString());
|
|
109
|
+
console.log(chalk_1.default.yellow(`Optional: ~${fmt(r.lowConfidencePotential)} tokens/session ` +
|
|
110
|
+
`(~${dollars(r.lowConfidencePotential, o)}/month) if you disable what you don't need.`));
|
|
111
|
+
}
|
|
112
|
+
const modelNote = o.modelDetected ? " (from your Claude config)" : "";
|
|
113
|
+
console.log();
|
|
114
|
+
console.log(chalk_1.default.dim(`Estimate: chars/4 at ${o.model} pricing${modelNote}, ${o.sessionsPerMonth} sessions/month. ` +
|
|
115
|
+
`Run \`npx ctxdiet fix\` to apply.`));
|
|
116
|
+
console.log();
|
|
117
|
+
}
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// before/after money shot
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
function printBeforeAfter(before, after, o, lowApplied) {
|
|
122
|
+
const savedTokens = before.baselineTokens - after.baselineTokens;
|
|
123
|
+
const beforeCost = (0, pricing_1.monthlyCost)(before.baselineTokens, o.sessionsPerMonth, o.model);
|
|
124
|
+
const afterCost = (0, pricing_1.monthlyCost)(after.baselineTokens, o.sessionsPerMonth, o.model);
|
|
125
|
+
const arrow = chalk_1.default.dim("→");
|
|
126
|
+
const table = new cli_table3_1.default({
|
|
127
|
+
head: ["", "Before", "", "After", "Saved"].map((h) => chalk_1.default.bold(h)),
|
|
128
|
+
colAligns: ["left", "right", "left", "right", "right"],
|
|
129
|
+
style: { head: [], border: [] },
|
|
130
|
+
});
|
|
131
|
+
table.push([
|
|
132
|
+
"Context tokens/session",
|
|
133
|
+
fmt(before.baselineTokens),
|
|
134
|
+
arrow,
|
|
135
|
+
fmt(after.baselineTokens),
|
|
136
|
+
chalk_1.default.green(fmt(savedTokens)),
|
|
137
|
+
]);
|
|
138
|
+
table.push([
|
|
139
|
+
"$/month",
|
|
140
|
+
usd(beforeCost),
|
|
141
|
+
arrow,
|
|
142
|
+
usd(afterCost),
|
|
143
|
+
chalk_1.default.green(usd(beforeCost - afterCost)),
|
|
144
|
+
]);
|
|
145
|
+
table.push(["Grade", gradeBadge(before.grade), arrow, gradeBadge(after.grade), ""]);
|
|
146
|
+
console.log();
|
|
147
|
+
console.log(chalk_1.default.bold("Before vs after"));
|
|
148
|
+
console.log(table.toString());
|
|
149
|
+
if (lowApplied > 0) {
|
|
150
|
+
console.log(chalk_1.default.dim(`Includes ~${fmt(lowApplied)} tokens/session from review items you chose to disable.`));
|
|
151
|
+
}
|
|
152
|
+
console.log();
|
|
153
|
+
}
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// JSON serialization (no large blobs)
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
function toJson(r) {
|
|
158
|
+
const { options } = r;
|
|
159
|
+
return {
|
|
160
|
+
path: options.path,
|
|
161
|
+
model: options.model,
|
|
162
|
+
sessionsPerMonth: options.sessionsPerMonth,
|
|
163
|
+
method: "chars/4 heuristic — estimate only; usage history not analyzed",
|
|
164
|
+
detectedAgents: r.detectedAgents,
|
|
165
|
+
grade: r.grade,
|
|
166
|
+
baselineTokens: r.baselineTokens,
|
|
167
|
+
headlineSavingsTokens: r.headlineSavings,
|
|
168
|
+
headlineSavingsUsdPerMonth: Number((0, pricing_1.monthlyCost)(r.headlineSavings, options.sessionsPerMonth, options.model).toFixed(2)),
|
|
169
|
+
lowConfidencePotentialTokens: r.lowConfidencePotential,
|
|
170
|
+
findings: r.findings.map((f) => ({
|
|
171
|
+
agent: f.agent,
|
|
172
|
+
category: f.category,
|
|
173
|
+
title: f.title,
|
|
174
|
+
detail: f.detail,
|
|
175
|
+
tokensPerSession: f.tokensPerSession,
|
|
176
|
+
confidence: f.confidence,
|
|
177
|
+
fixable: f.fixable,
|
|
178
|
+
manualReview: f.manualReview ?? false,
|
|
179
|
+
})),
|
|
180
|
+
};
|
|
181
|
+
}
|
package/dist/src/scan.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.scan = scan;
|
|
40
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
41
|
+
const agents_1 = require("./agents");
|
|
42
|
+
const constants_1 = require("./constants");
|
|
43
|
+
const src = __importStar(require("./sources"));
|
|
44
|
+
const tokens_1 = require("./tokens");
|
|
45
|
+
const trim_1 = require("./trim");
|
|
46
|
+
/** Heavy dirs/files present in the project, sized once and reused per agent. */
|
|
47
|
+
function presentHeavyPaths(o) {
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const name of [...src.HEAVY_DIRS, ...src.HEAVY_FILES]) {
|
|
50
|
+
const full = node_path_1.default.join(o.path, name);
|
|
51
|
+
if (!src.isFile(full) && !src.isDir(full))
|
|
52
|
+
continue;
|
|
53
|
+
out.push({ name, tokens: src.estimatePathTokens(full) });
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Pure scan: detects active agents, reads their persistent context, returns
|
|
59
|
+
* findings + metrics. No printing, no writes — safe to call before/after a fix.
|
|
60
|
+
*/
|
|
61
|
+
function scan(o) {
|
|
62
|
+
const findings = [];
|
|
63
|
+
let baselineTokens = 0;
|
|
64
|
+
const agents = (0, agents_1.detectAgents)(o);
|
|
65
|
+
const heavy = presentHeavyPaths(o);
|
|
66
|
+
for (const agent of agents) {
|
|
67
|
+
baselineTokens += scanAgent(agent, o, heavy, findings);
|
|
68
|
+
}
|
|
69
|
+
const headlineSavings = findings
|
|
70
|
+
.filter((f) => f.confidence === "high")
|
|
71
|
+
.reduce((s, f) => s + f.tokensPerSession, 0);
|
|
72
|
+
const lowConfidencePotential = findings
|
|
73
|
+
.filter((f) => f.confidence === "low")
|
|
74
|
+
.reduce((s, f) => s + f.tokensPerSession, 0);
|
|
75
|
+
return {
|
|
76
|
+
options: o,
|
|
77
|
+
detectedAgents: agents.map((a) => ({ id: a.id, label: a.label })),
|
|
78
|
+
findings,
|
|
79
|
+
baselineTokens,
|
|
80
|
+
headlineSavings,
|
|
81
|
+
lowConfidencePotential,
|
|
82
|
+
grade: (0, constants_1.grade)(headlineSavings),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Scan a single agent; returns its baseline-token contribution. */
|
|
86
|
+
function scanAgent(agent, o, heavy, findings) {
|
|
87
|
+
let baseline = 0;
|
|
88
|
+
// --- Memory / instruction files (HIGH-confidence, auto-trimmable) ---
|
|
89
|
+
for (const file of agent.memoryFiles(o)) {
|
|
90
|
+
if (!src.isFile(file))
|
|
91
|
+
continue;
|
|
92
|
+
const original = src.readFileSafe(file);
|
|
93
|
+
const origTokens = (0, tokens_1.estimateTokens)(original);
|
|
94
|
+
baseline += origTokens;
|
|
95
|
+
const trimmed = (0, trim_1.trimMarkdown)(original);
|
|
96
|
+
const saved = origTokens - (0, tokens_1.estimateTokens)(trimmed);
|
|
97
|
+
const label = src.displayPath(file, o.path, o.home);
|
|
98
|
+
if (saved > 0) {
|
|
99
|
+
findings.push({
|
|
100
|
+
agent: agent.label,
|
|
101
|
+
category: "Memory",
|
|
102
|
+
title: `${label}: ${saved.toLocaleString()} redundant tokens`,
|
|
103
|
+
detail: "duplicate lines, blank runs, trailing whitespace",
|
|
104
|
+
tokensPerSession: saved,
|
|
105
|
+
confidence: "high",
|
|
106
|
+
fixable: true,
|
|
107
|
+
action: { type: "trim", path: file },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else if (origTokens > constants_1.LARGE_CLAUDEMD_TOKENS) {
|
|
111
|
+
findings.push({
|
|
112
|
+
agent: agent.label,
|
|
113
|
+
category: "Memory",
|
|
114
|
+
title: `${label}: large (${origTokens.toLocaleString()} tokens)`,
|
|
115
|
+
detail: "no auto-trimmable redundancy — shorten manually",
|
|
116
|
+
tokensPerSession: 0,
|
|
117
|
+
confidence: "high",
|
|
118
|
+
fixable: false,
|
|
119
|
+
manualReview: true,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// --- Ignore file (HIGH-confidence, auto-fixable) ---
|
|
124
|
+
if (agent.ignoreFile && heavy.length > 0) {
|
|
125
|
+
const ignorePath = node_path_1.default.join(o.path, agent.ignoreFile);
|
|
126
|
+
const ignoreExists = src.isFile(ignorePath);
|
|
127
|
+
const content = ignoreExists ? src.readFileSafe(ignorePath) : null;
|
|
128
|
+
const patterns = content ? src.parseIgnore(content) : [];
|
|
129
|
+
const uncovered = heavy.filter((h) => !src.ignoreCovers(patterns, h.name));
|
|
130
|
+
const heavyTokens = uncovered.reduce((s, h) => s + h.tokens, 0);
|
|
131
|
+
if (uncovered.length > 0) {
|
|
132
|
+
baseline += heavyTokens;
|
|
133
|
+
const names = uncovered.map((h) => h.name);
|
|
134
|
+
const missingDefaults = src.DEFAULT_IGNORE_PATTERNS.filter((p) => !patterns.includes(p));
|
|
135
|
+
findings.push({
|
|
136
|
+
agent: agent.label,
|
|
137
|
+
category: "Ignore",
|
|
138
|
+
title: content === null
|
|
139
|
+
? `${agent.ignoreFile} missing — ${uncovered.length} heavy path(s) unignored`
|
|
140
|
+
: `${agent.ignoreFile} weak — ${uncovered.length} heavy path(s) unignored`,
|
|
141
|
+
detail: names.join(", "),
|
|
142
|
+
tokensPerSession: heavyTokens,
|
|
143
|
+
confidence: "high",
|
|
144
|
+
fixable: true,
|
|
145
|
+
action: content === null
|
|
146
|
+
? {
|
|
147
|
+
type: "ignore-create",
|
|
148
|
+
path: ignorePath,
|
|
149
|
+
content: src.DEFAULT_IGNORE_PATTERNS.join("\n") + "\n",
|
|
150
|
+
}
|
|
151
|
+
: { type: "ignore-augment", path: ignorePath, added: missingDefaults },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// --- MCP servers (LOW-confidence, review only) ---
|
|
156
|
+
for (const file of agent.mcpFiles(o)) {
|
|
157
|
+
for (const server of src.readMcpServers(file)) {
|
|
158
|
+
baseline += constants_1.MCP_SERVER_TOKEN_EST;
|
|
159
|
+
findings.push({
|
|
160
|
+
agent: agent.label,
|
|
161
|
+
category: "MCP",
|
|
162
|
+
title: `${server} (${src.displayPath(file, o.path, o.home)})`,
|
|
163
|
+
detail: "usage not confirmed — disable only if you know you don't use it",
|
|
164
|
+
tokensPerSession: constants_1.MCP_SERVER_TOKEN_EST,
|
|
165
|
+
confidence: "low",
|
|
166
|
+
fixable: true,
|
|
167
|
+
manualReview: true,
|
|
168
|
+
action: { type: "mcp-disable", path: file, server },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// --- Definition inventory (Claude-style ~/.claude only) ---
|
|
173
|
+
if (agent.ownsDefinitions) {
|
|
174
|
+
const inv = src.scanDefinitions(o);
|
|
175
|
+
for (const dead of inv.dead) {
|
|
176
|
+
baseline += dead.tokens;
|
|
177
|
+
findings.push({
|
|
178
|
+
agent: agent.label,
|
|
179
|
+
category: "Definitions",
|
|
180
|
+
title: `${src.displayPath(dead.path, o.path, o.home)} — ${dead.reason}`,
|
|
181
|
+
tokensPerSession: dead.tokens,
|
|
182
|
+
confidence: "high",
|
|
183
|
+
fixable: true,
|
|
184
|
+
action: {
|
|
185
|
+
type: "archive",
|
|
186
|
+
path: dead.path,
|
|
187
|
+
archiveTo: src.archivePathFor(dead.path, o.home),
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
for (const real of inv.real) {
|
|
192
|
+
baseline += real.tokens;
|
|
193
|
+
findings.push({
|
|
194
|
+
agent: agent.label,
|
|
195
|
+
category: "Definitions",
|
|
196
|
+
title: src.displayPath(real.path, o.path, o.home),
|
|
197
|
+
detail: "usage not confirmed — remove only if you recognize it as unused",
|
|
198
|
+
tokensPerSession: real.tokens,
|
|
199
|
+
confidence: "low",
|
|
200
|
+
fixable: true,
|
|
201
|
+
manualReview: true,
|
|
202
|
+
action: {
|
|
203
|
+
type: "archive",
|
|
204
|
+
path: real.path,
|
|
205
|
+
archiveTo: src.archivePathFor(real.path, o.home),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return baseline;
|
|
211
|
+
}
|