@clear-capabilities/agentic-security-scanner 0.120.0 → 0.121.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 +30 -0
- package/bin/agentic-security.js +11 -0
- package/dist/752.index.js +281 -0
- package/dist/985.index.js +306 -2
- package/dist/agentic-security.mjs +1 -1
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +6 -3
- package/src/mcp/tools.js +27 -1
- package/src/posture/cache-economics.js +261 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.121.0 — prompt-cache economics (measured, cache-aware cost optimization)
|
|
4
|
+
|
|
5
|
+
Cache-economics core (PRD `docs/CACHE_ECONOMICS_PRD.md`, features F1–F3). Turns
|
|
6
|
+
Claude Code's own transcript usage into a dollarized, cache-aware view of token
|
|
7
|
+
cost. No network, advisory-only.
|
|
8
|
+
|
|
9
|
+
- **F1 — cache telemetry + report.** New `scanner/src/posture/cache-economics.js`
|
|
10
|
+
parses per-turn `usage` (cache read/write at 0.1× / 1.25×–2× input) and reports
|
|
11
|
+
cache-hit %, $ saved by caching, $ wasted on avoidable misses, $/turn, and a
|
|
12
|
+
per-model breakdown. Surfaced as the `cache-report` CLI subcommand (documented as
|
|
13
|
+
`/posture --cache`) and the read-only `query_cache_telemetry` MCP tool.
|
|
14
|
+
- **F2 — silent-invalidator detector ("cache bodyguard").** Retrospectively
|
|
15
|
+
attributes cache drops to model-switch / TTL-gap / prefix-change (shown in the
|
|
16
|
+
report), plus a live PreToolUse hook (`hooks/cache-invalidator-guard.js`) that warns
|
|
17
|
+
before an edit to a cache anchor (`CLAUDE.md`, `.claude/settings*.json`) with the
|
|
18
|
+
estimated re-warm cost. Throttled; `AGENTIC_SECURITY_QUIET` / `…_CACHE_GUARD=off`.
|
|
19
|
+
- **F3 — break-even + TTL-aware switching.** `hooks/model-cost-advisor.js` now reads
|
|
20
|
+
the *real* cached size from the transcript (`hooks/lib/transcript.js`), shows a
|
|
21
|
+
model switch's break-even ("worth it past ~N more turns"), suppresses switches that
|
|
22
|
+
won't pay off (preferring a cache-safe effort drop), and treats a cache gone cold
|
|
23
|
+
past the TTL as free to switch. New config: `ttlSeconds`, `breakEvenMaxTurns`.
|
|
24
|
+
|
|
25
|
+
Also lands the OpenRouter-derived advisor controls that F3 builds on: a
|
|
26
|
+
**`costQualityTradeoff` 0–10 dial** (replaces the one-sided `minSavingsUsd` gate;
|
|
27
|
+
0 = never downgrade, 10 = cheapest) and the initial per-prompt **cache-rewarm
|
|
28
|
+
penalty** that prefers a cache-preserving effort drop over a model switch.
|
|
29
|
+
|
|
30
|
+
F4–F6 (depth-first formalization, subagent-offload advice, cost HUD/statusline) are
|
|
31
|
+
specced in the PRD and ship next.
|
|
32
|
+
|
|
3
33
|
## 0.120.0 — model-cost optimizer (per-prompt model + depth advisor)
|
|
4
34
|
|
|
5
35
|
New opt-in Claude Code plugin feature; no functional change to the scanner.
|
package/bin/agentic-security.js
CHANGED
|
@@ -1631,6 +1631,17 @@ async function main() {
|
|
|
1631
1631
|
case 'rule-synth': process.exit(await cmdRuleSynth(args));
|
|
1632
1632
|
case 'digest': process.exit(await cmdDigest(args));
|
|
1633
1633
|
case 'setup': process.exit(await cmdSetup(args));
|
|
1634
|
+
case 'cache-report': {
|
|
1635
|
+
// Prompt-cache economics for the current session: parse the Claude Code
|
|
1636
|
+
// transcript usage and report cache-hit %, $ saved, and avoidable leaks.
|
|
1637
|
+
// Advisory/read-only — always exits 0.
|
|
1638
|
+
const { analyzeTranscript, formatCacheReport } = await import('../src/posture/cache-economics.js');
|
|
1639
|
+
const projectDir = path.resolve(args.flags.root || process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
1640
|
+
const result = analyzeTranscript({ transcriptPath: args.flags.transcript, projectDir });
|
|
1641
|
+
if (args.flags.json) console.log(JSON.stringify(result, null, 2));
|
|
1642
|
+
else console.log(formatCacheReport(result));
|
|
1643
|
+
process.exit(0);
|
|
1644
|
+
}
|
|
1634
1645
|
case 'mcp': {
|
|
1635
1646
|
const { runStdio } = await import('../src/mcp/stdio.js');
|
|
1636
1647
|
const root = args.flags.root || process.env.AGENTIC_SECURITY_MCP_ROOT || process.cwd();
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
export const id = 752;
|
|
2
|
+
export const ids = [752];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 8752:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ analyzeTranscript: () => (/* binding */ analyzeTranscript),
|
|
10
|
+
/* harmony export */ formatCacheReport: () => (/* binding */ formatCacheReport)
|
|
11
|
+
/* harmony export */ });
|
|
12
|
+
/* unused harmony export _internal */
|
|
13
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
|
|
14
|
+
/* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8161);
|
|
15
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
|
|
16
|
+
// Prompt-cache economics — turn Claude Code's own transcript usage into a
|
|
17
|
+
// dollarized report: how much prompt caching saved, how much was wasted on
|
|
18
|
+
// avoidable cache misses, and what invalidated the cache.
|
|
19
|
+
//
|
|
20
|
+
// Source of truth: the Claude Code transcript at
|
|
21
|
+
// ~/.claude/projects/<enc>/<session>.jsonl
|
|
22
|
+
// where <enc> is CLAUDE_PROJECT_DIR with `/` and `.` replaced by `-`. Each
|
|
23
|
+
// assistant turn carries `message.usage` with input/output/cache_read/
|
|
24
|
+
// cache_creation token counts (and a 5m/1h write split). We price those against
|
|
25
|
+
// per-model rates to compute real economics — no estimates, no network.
|
|
26
|
+
//
|
|
27
|
+
// Pure compute on parsed records; only `locateTranscript`/`parseTranscriptUsage`
|
|
28
|
+
// touch the filesystem. ESM (scanner tree). A trimmed CJS twin lives at
|
|
29
|
+
// hooks/lib/transcript.js for the CJS hooks; test/cache-economics.test.js asserts
|
|
30
|
+
// the two agree.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// Cents-scale money formatter (fmtUsd in risk-dollars.js targets five-figure
|
|
36
|
+
// breach costs and won't round sub-dollar values).
|
|
37
|
+
function money(n) {
|
|
38
|
+
const v = Number(n) || 0;
|
|
39
|
+
return Math.abs(v) >= 1 ? `$${v.toFixed(2)}` : `$${v.toFixed(4)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Per-1M-token rates (input / output). Mirror hooks/model-cost-advisor.js MODELS.
|
|
43
|
+
const MODEL_RATES = {
|
|
44
|
+
opus: { label: 'Opus 4.8', in: 5, out: 25 },
|
|
45
|
+
sonnet: { label: 'Sonnet 4.6', in: 3, out: 15 },
|
|
46
|
+
haiku: { label: 'Haiku 4.5', in: 1, out: 5 },
|
|
47
|
+
};
|
|
48
|
+
const CACHE_READ_MULT = 0.1; // cache read ≈ 0.1× input
|
|
49
|
+
const CACHE_WRITE_MULT = 1.25; // 5-minute cache write ≈ 1.25× input
|
|
50
|
+
const CACHE_WRITE_1H_MULT = 2.0; // 1-hour cache write ≈ 2× input
|
|
51
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
52
|
+
|
|
53
|
+
// Map any model string to a rate family. Returns null for unpriceable models
|
|
54
|
+
// (e.g. "<synthetic>" sidechain/compaction turns) so they're skipped.
|
|
55
|
+
function rateFor(model) {
|
|
56
|
+
if (typeof model !== 'string') return null;
|
|
57
|
+
const s = model.toLowerCase();
|
|
58
|
+
if (s.includes('haiku')) return MODEL_RATES.haiku;
|
|
59
|
+
if (s.includes('sonnet')) return MODEL_RATES.sonnet;
|
|
60
|
+
if (s.includes('opus')) return MODEL_RATES.opus;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Transcript discovery + parse ─────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function encodeProjectDir(dir) {
|
|
67
|
+
return String(dir).replace(/[/.]/g, '-');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Locate the session transcript. Prefer an explicit (hook-provided) path; else
|
|
71
|
+
// derive the project's transcript dir and take the most-recently-modified jsonl.
|
|
72
|
+
function locateTranscript({ transcriptPath, projectDir } = {}) {
|
|
73
|
+
try {
|
|
74
|
+
if (transcriptPath && node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(transcriptPath)) return transcriptPath;
|
|
75
|
+
} catch { /* fall through */ }
|
|
76
|
+
try {
|
|
77
|
+
const dir = node_path__WEBPACK_IMPORTED_MODULE_2__.join(node_os__WEBPACK_IMPORTED_MODULE_1__.homedir(), '.claude', 'projects', encodeProjectDir(projectDir || process.cwd()));
|
|
78
|
+
if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(dir)) return null;
|
|
79
|
+
const files = node_fs__WEBPACK_IMPORTED_MODULE_0__.readdirSync(dir)
|
|
80
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
81
|
+
.map(f => ({ f: node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, f), m: node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, f)).mtimeMs }))
|
|
82
|
+
.sort((a, b) => b.m - a.m);
|
|
83
|
+
return files.length ? files[0].f : null;
|
|
84
|
+
} catch { return null; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Parse a transcript jsonl into per-assistant-turn usage records. Skips lines
|
|
88
|
+
// that aren't priceable assistant turns.
|
|
89
|
+
function parseTranscriptUsage(jsonlPath) {
|
|
90
|
+
let raw;
|
|
91
|
+
try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(jsonlPath, 'utf8'); } catch { return []; }
|
|
92
|
+
const records = [];
|
|
93
|
+
for (const line of raw.split('\n')) {
|
|
94
|
+
const t = line.trim();
|
|
95
|
+
if (!t) continue;
|
|
96
|
+
let o;
|
|
97
|
+
try { o = JSON.parse(t); } catch { continue; }
|
|
98
|
+
if (o.type !== 'assistant') continue;
|
|
99
|
+
const msg = o.message;
|
|
100
|
+
const u = msg && msg.usage;
|
|
101
|
+
if (!u || !msg.model || !rateFor(msg.model)) continue;
|
|
102
|
+
const cc = u.cache_creation || {};
|
|
103
|
+
records.push({
|
|
104
|
+
model: msg.model,
|
|
105
|
+
input: u.input_tokens || 0,
|
|
106
|
+
output: u.output_tokens || 0,
|
|
107
|
+
cacheRead: u.cache_read_input_tokens || 0,
|
|
108
|
+
cacheCreate: u.cache_creation_input_tokens || 0,
|
|
109
|
+
cacheCreate5m: cc.ephemeral_5m_input_tokens || 0,
|
|
110
|
+
cacheCreate1h: cc.ephemeral_1h_input_tokens || 0,
|
|
111
|
+
ts: o.timestamp ? Date.parse(o.timestamp) : null,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return records;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Pure economics ───────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
function writeCostUsd(r, inRate) {
|
|
120
|
+
const m5 = r.cacheCreate5m || 0, m1 = r.cacheCreate1h || 0;
|
|
121
|
+
if (m5 + m1 > 0) return (m5 * CACHE_WRITE_MULT + m1 * CACHE_WRITE_1H_MULT) * inRate;
|
|
122
|
+
return (r.cacheCreate || 0) * CACHE_WRITE_MULT * inRate; // breakdown absent
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Aggregate economics over parsed records.
|
|
126
|
+
function computeCacheEconomics(records) {
|
|
127
|
+
let turns = 0, inTok = 0, outTok = 0, cacheRead = 0, cacheCreate = 0;
|
|
128
|
+
let actualUsd = 0, uncachedUsd = 0, writePremiumUsd = 0;
|
|
129
|
+
const perModel = {};
|
|
130
|
+
|
|
131
|
+
for (const r of records) {
|
|
132
|
+
const rate = rateFor(r.model);
|
|
133
|
+
if (!rate) continue;
|
|
134
|
+
turns++;
|
|
135
|
+
const inRate = rate.in / 1e6, outRate = rate.out / 1e6;
|
|
136
|
+
|
|
137
|
+
const readCost = r.cacheRead * inRate * CACHE_READ_MULT;
|
|
138
|
+
const writeCost = writeCostUsd(r, inRate);
|
|
139
|
+
const inCost = r.input * inRate;
|
|
140
|
+
const outCost = r.output * outRate;
|
|
141
|
+
const turnActual = readCost + writeCost + inCost + outCost;
|
|
142
|
+
// What this turn would have cost with NO caching: every input-side token full price.
|
|
143
|
+
const turnUncached = (r.cacheRead + r.cacheCreate + r.input) * inRate + outCost;
|
|
144
|
+
|
|
145
|
+
actualUsd += turnActual;
|
|
146
|
+
uncachedUsd += turnUncached;
|
|
147
|
+
writePremiumUsd += writeCost - (r.cacheCreate * inRate); // the >1× premium paid to cache
|
|
148
|
+
|
|
149
|
+
inTok += r.input; outTok += r.output; cacheRead += r.cacheRead; cacheCreate += r.cacheCreate;
|
|
150
|
+
|
|
151
|
+
const key = rate.label;
|
|
152
|
+
const pm = perModel[key] || (perModel[key] = { turns: 0, actualUsd: 0, cacheRead: 0, inputSide: 0 });
|
|
153
|
+
pm.turns++; pm.actualUsd += turnActual; pm.cacheRead += r.cacheRead;
|
|
154
|
+
pm.inputSide += r.cacheRead + r.cacheCreate + r.input;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const inputSide = cacheRead + cacheCreate + inTok;
|
|
158
|
+
return {
|
|
159
|
+
turns,
|
|
160
|
+
tokens: { input: inTok, output: outTok, cacheRead, cacheCreate },
|
|
161
|
+
actualUsd,
|
|
162
|
+
uncachedUsd,
|
|
163
|
+
savedUsd: uncachedUsd - actualUsd, // net $ caching saved (can dip negative early)
|
|
164
|
+
writePremiumUsd, // $ invested establishing caches
|
|
165
|
+
cacheHitRatio: inputSide ? cacheRead / inputSide : 0,
|
|
166
|
+
costPerTurnUsd: turns ? actualUsd / turns : 0,
|
|
167
|
+
perModel,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Attribute cache drops: a turn that re-ingests a large prefix cold after a warm
|
|
172
|
+
// prior turn. Cause = model-switch | cache-expired | prefix-change.
|
|
173
|
+
function detectInvalidators(records) {
|
|
174
|
+
const leaks = [];
|
|
175
|
+
const MIN_WARM = 2000;
|
|
176
|
+
for (let i = 1; i < records.length; i++) {
|
|
177
|
+
const prev = records[i - 1], cur = records[i];
|
|
178
|
+
const prevWarm = prev.cacheRead + prev.input + prev.cacheCreate;
|
|
179
|
+
if (prevWarm < MIN_WARM) continue;
|
|
180
|
+
const curFresh = cur.input + cur.cacheCreate;
|
|
181
|
+
const coldish = cur.cacheRead < prevWarm * 0.25 && curFresh > prevWarm * 0.5;
|
|
182
|
+
if (!coldish) continue;
|
|
183
|
+
|
|
184
|
+
let cause;
|
|
185
|
+
if (cur.model !== prev.model) cause = 'model-switch';
|
|
186
|
+
else if (cur.ts && prev.ts && (cur.ts - prev.ts) > TTL_MS) cause = 'cache-expired';
|
|
187
|
+
else cause = 'prefix-change';
|
|
188
|
+
|
|
189
|
+
const rate = rateFor(cur.model);
|
|
190
|
+
const inRate = rate ? rate.in / 1e6 : 0;
|
|
191
|
+
// Extra paid vs. having kept the prefix as a cheap cache read.
|
|
192
|
+
const wastedUsd = prevWarm * inRate * (1 - CACHE_READ_MULT);
|
|
193
|
+
leaks.push({ turn: i, cause, wastedUsd, model: cur.model });
|
|
194
|
+
}
|
|
195
|
+
return leaks;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Convenience: locate → parse → compute → detect. Returns { ok:false } when no
|
|
199
|
+
// transcript is available.
|
|
200
|
+
function analyzeTranscript(opts = {}) {
|
|
201
|
+
const transcript = locateTranscript(opts);
|
|
202
|
+
if (!transcript) return { ok: false, reason: 'no-transcript' };
|
|
203
|
+
const records = parseTranscriptUsage(transcript);
|
|
204
|
+
if (!records.length) return { ok: false, reason: 'no-priceable-turns', transcript };
|
|
205
|
+
return {
|
|
206
|
+
ok: true,
|
|
207
|
+
transcript,
|
|
208
|
+
metrics: computeCacheEconomics(records),
|
|
209
|
+
leaks: detectInvalidators(records),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── Report formatting ────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
const CAUSE_LABEL = {
|
|
216
|
+
'model-switch': 'model switch (cache is model-scoped)',
|
|
217
|
+
'cache-expired': 'cache expired (gap > 5-min TTL)',
|
|
218
|
+
'prefix-change': 'prefix changed (system prompt / tools / context edit)',
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
function formatCacheReport(result) {
|
|
222
|
+
if (!result.ok) {
|
|
223
|
+
return result.reason === 'no-transcript'
|
|
224
|
+
? 'agentic-security: no Claude Code transcript found for this project yet.'
|
|
225
|
+
: 'agentic-security: transcript has no priceable model turns yet.';
|
|
226
|
+
}
|
|
227
|
+
const m = result.metrics;
|
|
228
|
+
const lines = [];
|
|
229
|
+
lines.push('');
|
|
230
|
+
lines.push(' Prompt-cache economics — this session');
|
|
231
|
+
lines.push(` ${result.turns ?? m.turns} model turns\n`);
|
|
232
|
+
lines.push(` cache hit ratio ${(m.cacheHitRatio * 100).toFixed(1)}% (input-side tokens served from cache)`);
|
|
233
|
+
lines.push(` spent ${money(m.actualUsd)} (~${money(m.costPerTurnUsd)}/turn)`);
|
|
234
|
+
lines.push(` ▶ saved by caching ${money(m.savedUsd)} vs. ${money(m.uncachedUsd)} with no cache`);
|
|
235
|
+
lines.push(` invested in caches ${money(m.writePremiumUsd)} (write premium over base input)`);
|
|
236
|
+
lines.push('');
|
|
237
|
+
lines.push(' tokens: '
|
|
238
|
+
+ `${m.tokens.cacheRead.toLocaleString()} cached-read · `
|
|
239
|
+
+ `${m.tokens.cacheCreate.toLocaleString()} cache-write · `
|
|
240
|
+
+ `${m.tokens.input.toLocaleString()} fresh-in · `
|
|
241
|
+
+ `${m.tokens.output.toLocaleString()} out`);
|
|
242
|
+
|
|
243
|
+
const models = Object.keys(m.perModel);
|
|
244
|
+
if (models.length > 1) {
|
|
245
|
+
lines.push('\n by model:');
|
|
246
|
+
for (const k of models.sort()) {
|
|
247
|
+
const pm = m.perModel[k];
|
|
248
|
+
const hr = pm.inputSide ? (pm.cacheRead / pm.inputSide * 100).toFixed(0) : '0';
|
|
249
|
+
lines.push(` ${k.padEnd(12)} ${pm.turns} turns · ${money(pm.actualUsd)} · ${hr}% cached`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (result.leaks && result.leaks.length) {
|
|
254
|
+
const wasted = result.leaks.reduce((s, l) => s + l.wastedUsd, 0);
|
|
255
|
+
lines.push(`\n ⚠ cache leaks (${result.leaks.length}, ~${money(wasted)} wasted re-ingesting context):`);
|
|
256
|
+
const byCause = {};
|
|
257
|
+
for (const l of result.leaks) {
|
|
258
|
+
(byCause[l.cause] || (byCause[l.cause] = { n: 0, usd: 0 })).n++;
|
|
259
|
+
byCause[l.cause].usd += l.wastedUsd;
|
|
260
|
+
}
|
|
261
|
+
for (const c of Object.keys(byCause).sort()) {
|
|
262
|
+
lines.push(` · ${byCause[c].n}× ${CAUSE_LABEL[c] || c} — ~${money(byCause[c].usd)}`);
|
|
263
|
+
}
|
|
264
|
+
lines.push(' Keep one model + a stable system prompt within a working window to avoid these.');
|
|
265
|
+
} else {
|
|
266
|
+
lines.push('\n ✓ no cache leaks detected — your context stayed warm.');
|
|
267
|
+
}
|
|
268
|
+
lines.push('');
|
|
269
|
+
return lines.join('\n');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Test surface (underscore export is exempt from the dead-module gate).
|
|
273
|
+
const _internal = {
|
|
274
|
+
MODEL_RATES, CACHE_READ_MULT, CACHE_WRITE_MULT, CACHE_WRITE_1H_MULT,
|
|
275
|
+
rateFor, locateTranscript, parseTranscriptUsage, computeCacheEconomics, detectInvalidators,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
/***/ })
|
|
280
|
+
|
|
281
|
+
};
|
package/dist/985.index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const id = 985;
|
|
2
|
-
export const ids = [985];
|
|
2
|
+
export const ids = [985,752];
|
|
3
3
|
export const modules = {
|
|
4
4
|
|
|
5
5
|
/***/ 3985:
|
|
@@ -25,6 +25,8 @@ var promises_ = __webpack_require__(1455);
|
|
|
25
25
|
var fix_history = __webpack_require__(4407);
|
|
26
26
|
// EXTERNAL MODULE: ./src/posture/integrity.js
|
|
27
27
|
var integrity = __webpack_require__(1130);
|
|
28
|
+
// EXTERNAL MODULE: ./src/posture/cache-economics.js
|
|
29
|
+
var cache_economics = __webpack_require__(8752);
|
|
28
30
|
;// CONCATENATED MODULE: ./src/mcp/redact.js
|
|
29
31
|
// Secret redactor for MCP tool outputs and audit log argument summaries.
|
|
30
32
|
//
|
|
@@ -395,6 +397,7 @@ const cve_lookup_internals = { CACHE_DIR, CVE_RE, _stalenessTier };
|
|
|
395
397
|
|
|
396
398
|
|
|
397
399
|
|
|
400
|
+
|
|
398
401
|
// Lazy-loaded: these transitively pull in npm packages (fast-glob,
|
|
399
402
|
// @babel/core) that aren't available in the plugin-cache install path
|
|
400
403
|
// (no node_modules). Deferring keeps the MCP server bootable everywhere;
|
|
@@ -1396,6 +1399,31 @@ const lookup_cve = {
|
|
|
1396
1399
|
},
|
|
1397
1400
|
};
|
|
1398
1401
|
|
|
1402
|
+
const query_cache_telemetry = {
|
|
1403
|
+
name: 'query_cache_telemetry',
|
|
1404
|
+
description: 'Read prompt-cache economics for the current session from the Claude Code transcript: cache-hit %, $ saved by caching, $ wasted on avoidable cache misses (model switches / TTL gaps / prefix changes), and a per-model breakdown. Read-only, no network. Use to reason about token-cost efficiency and whether a model switch is worth the cache rewarm.',
|
|
1405
|
+
inputSchema: {
|
|
1406
|
+
type: 'object',
|
|
1407
|
+
additionalProperties: false,
|
|
1408
|
+
properties: {
|
|
1409
|
+
// Optional explicit transcript path; otherwise derived from the session root.
|
|
1410
|
+
transcript_path: { type: 'string', minLength: 1, maxLength: 4096 },
|
|
1411
|
+
},
|
|
1412
|
+
required: [],
|
|
1413
|
+
},
|
|
1414
|
+
async handler({ transcript_path } = {}, ctx) {
|
|
1415
|
+
const result = (0,cache_economics.analyzeTranscript)({ transcriptPath: transcript_path, projectDir: ctx?.sessionRoot || process.cwd() });
|
|
1416
|
+
if (!result.ok) return { _meta: META, ok: false, reason: result.reason };
|
|
1417
|
+
return {
|
|
1418
|
+
_meta: META,
|
|
1419
|
+
ok: true,
|
|
1420
|
+
metrics: result.metrics,
|
|
1421
|
+
leaks: result.leaks,
|
|
1422
|
+
report: (0,cache_economics.formatCacheReport)(result),
|
|
1423
|
+
};
|
|
1424
|
+
},
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1399
1427
|
// ─── synthesize_sca_upgrade ───────────────────────────────────────────────
|
|
1400
1428
|
// Phase 3 / Item 5 of the SCA improvement plan. Read-only counterpart to
|
|
1401
1429
|
// apply_sca_upgrade — produces a structured upgrade plan via the
|
|
@@ -1470,7 +1498,7 @@ const apply_sca_upgrade = {
|
|
|
1470
1498
|
},
|
|
1471
1499
|
};
|
|
1472
1500
|
|
|
1473
|
-
const ALL_TOOLS = [scan_diff, query_taint, explain_finding, apply_fix, verify_fix, synthesize_fix, find_rule_module, append_scratchpad, read_scratchpad, append_agents_memory, read_agents_memory, lookup_cve, synthesize_sca_upgrade, apply_sca_upgrade, query_triage_memory, query_findings_memory];
|
|
1501
|
+
const ALL_TOOLS = [scan_diff, query_taint, explain_finding, apply_fix, verify_fix, synthesize_fix, find_rule_module, append_scratchpad, read_scratchpad, append_agents_memory, read_agents_memory, lookup_cve, synthesize_sca_upgrade, apply_sca_upgrade, query_triage_memory, query_findings_memory, query_cache_telemetry];
|
|
1474
1502
|
|
|
1475
1503
|
;// CONCATENATED MODULE: ./src/mcp/validate.js
|
|
1476
1504
|
// Minimal JSON Schema validator — just the subset our tool schemas use.
|
|
@@ -1912,6 +1940,282 @@ function runStdio({
|
|
|
1912
1940
|
}
|
|
1913
1941
|
|
|
1914
1942
|
|
|
1943
|
+
/***/ }),
|
|
1944
|
+
|
|
1945
|
+
/***/ 8752:
|
|
1946
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
1947
|
+
|
|
1948
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
1949
|
+
/* harmony export */ analyzeTranscript: () => (/* binding */ analyzeTranscript),
|
|
1950
|
+
/* harmony export */ formatCacheReport: () => (/* binding */ formatCacheReport)
|
|
1951
|
+
/* harmony export */ });
|
|
1952
|
+
/* unused harmony export _internal */
|
|
1953
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
|
|
1954
|
+
/* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8161);
|
|
1955
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
|
|
1956
|
+
// Prompt-cache economics — turn Claude Code's own transcript usage into a
|
|
1957
|
+
// dollarized report: how much prompt caching saved, how much was wasted on
|
|
1958
|
+
// avoidable cache misses, and what invalidated the cache.
|
|
1959
|
+
//
|
|
1960
|
+
// Source of truth: the Claude Code transcript at
|
|
1961
|
+
// ~/.claude/projects/<enc>/<session>.jsonl
|
|
1962
|
+
// where <enc> is CLAUDE_PROJECT_DIR with `/` and `.` replaced by `-`. Each
|
|
1963
|
+
// assistant turn carries `message.usage` with input/output/cache_read/
|
|
1964
|
+
// cache_creation token counts (and a 5m/1h write split). We price those against
|
|
1965
|
+
// per-model rates to compute real economics — no estimates, no network.
|
|
1966
|
+
//
|
|
1967
|
+
// Pure compute on parsed records; only `locateTranscript`/`parseTranscriptUsage`
|
|
1968
|
+
// touch the filesystem. ESM (scanner tree). A trimmed CJS twin lives at
|
|
1969
|
+
// hooks/lib/transcript.js for the CJS hooks; test/cache-economics.test.js asserts
|
|
1970
|
+
// the two agree.
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
// Cents-scale money formatter (fmtUsd in risk-dollars.js targets five-figure
|
|
1976
|
+
// breach costs and won't round sub-dollar values).
|
|
1977
|
+
function money(n) {
|
|
1978
|
+
const v = Number(n) || 0;
|
|
1979
|
+
return Math.abs(v) >= 1 ? `$${v.toFixed(2)}` : `$${v.toFixed(4)}`;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// Per-1M-token rates (input / output). Mirror hooks/model-cost-advisor.js MODELS.
|
|
1983
|
+
const MODEL_RATES = {
|
|
1984
|
+
opus: { label: 'Opus 4.8', in: 5, out: 25 },
|
|
1985
|
+
sonnet: { label: 'Sonnet 4.6', in: 3, out: 15 },
|
|
1986
|
+
haiku: { label: 'Haiku 4.5', in: 1, out: 5 },
|
|
1987
|
+
};
|
|
1988
|
+
const CACHE_READ_MULT = 0.1; // cache read ≈ 0.1× input
|
|
1989
|
+
const CACHE_WRITE_MULT = 1.25; // 5-minute cache write ≈ 1.25× input
|
|
1990
|
+
const CACHE_WRITE_1H_MULT = 2.0; // 1-hour cache write ≈ 2× input
|
|
1991
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
1992
|
+
|
|
1993
|
+
// Map any model string to a rate family. Returns null for unpriceable models
|
|
1994
|
+
// (e.g. "<synthetic>" sidechain/compaction turns) so they're skipped.
|
|
1995
|
+
function rateFor(model) {
|
|
1996
|
+
if (typeof model !== 'string') return null;
|
|
1997
|
+
const s = model.toLowerCase();
|
|
1998
|
+
if (s.includes('haiku')) return MODEL_RATES.haiku;
|
|
1999
|
+
if (s.includes('sonnet')) return MODEL_RATES.sonnet;
|
|
2000
|
+
if (s.includes('opus')) return MODEL_RATES.opus;
|
|
2001
|
+
return null;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// ── Transcript discovery + parse ─────────────────────────────────────────────
|
|
2005
|
+
|
|
2006
|
+
function encodeProjectDir(dir) {
|
|
2007
|
+
return String(dir).replace(/[/.]/g, '-');
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// Locate the session transcript. Prefer an explicit (hook-provided) path; else
|
|
2011
|
+
// derive the project's transcript dir and take the most-recently-modified jsonl.
|
|
2012
|
+
function locateTranscript({ transcriptPath, projectDir } = {}) {
|
|
2013
|
+
try {
|
|
2014
|
+
if (transcriptPath && node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(transcriptPath)) return transcriptPath;
|
|
2015
|
+
} catch { /* fall through */ }
|
|
2016
|
+
try {
|
|
2017
|
+
const dir = node_path__WEBPACK_IMPORTED_MODULE_2__.join(node_os__WEBPACK_IMPORTED_MODULE_1__.homedir(), '.claude', 'projects', encodeProjectDir(projectDir || process.cwd()));
|
|
2018
|
+
if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(dir)) return null;
|
|
2019
|
+
const files = node_fs__WEBPACK_IMPORTED_MODULE_0__.readdirSync(dir)
|
|
2020
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
2021
|
+
.map(f => ({ f: node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, f), m: node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, f)).mtimeMs }))
|
|
2022
|
+
.sort((a, b) => b.m - a.m);
|
|
2023
|
+
return files.length ? files[0].f : null;
|
|
2024
|
+
} catch { return null; }
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// Parse a transcript jsonl into per-assistant-turn usage records. Skips lines
|
|
2028
|
+
// that aren't priceable assistant turns.
|
|
2029
|
+
function parseTranscriptUsage(jsonlPath) {
|
|
2030
|
+
let raw;
|
|
2031
|
+
try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(jsonlPath, 'utf8'); } catch { return []; }
|
|
2032
|
+
const records = [];
|
|
2033
|
+
for (const line of raw.split('\n')) {
|
|
2034
|
+
const t = line.trim();
|
|
2035
|
+
if (!t) continue;
|
|
2036
|
+
let o;
|
|
2037
|
+
try { o = JSON.parse(t); } catch { continue; }
|
|
2038
|
+
if (o.type !== 'assistant') continue;
|
|
2039
|
+
const msg = o.message;
|
|
2040
|
+
const u = msg && msg.usage;
|
|
2041
|
+
if (!u || !msg.model || !rateFor(msg.model)) continue;
|
|
2042
|
+
const cc = u.cache_creation || {};
|
|
2043
|
+
records.push({
|
|
2044
|
+
model: msg.model,
|
|
2045
|
+
input: u.input_tokens || 0,
|
|
2046
|
+
output: u.output_tokens || 0,
|
|
2047
|
+
cacheRead: u.cache_read_input_tokens || 0,
|
|
2048
|
+
cacheCreate: u.cache_creation_input_tokens || 0,
|
|
2049
|
+
cacheCreate5m: cc.ephemeral_5m_input_tokens || 0,
|
|
2050
|
+
cacheCreate1h: cc.ephemeral_1h_input_tokens || 0,
|
|
2051
|
+
ts: o.timestamp ? Date.parse(o.timestamp) : null,
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
return records;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// ── Pure economics ───────────────────────────────────────────────────────────
|
|
2058
|
+
|
|
2059
|
+
function writeCostUsd(r, inRate) {
|
|
2060
|
+
const m5 = r.cacheCreate5m || 0, m1 = r.cacheCreate1h || 0;
|
|
2061
|
+
if (m5 + m1 > 0) return (m5 * CACHE_WRITE_MULT + m1 * CACHE_WRITE_1H_MULT) * inRate;
|
|
2062
|
+
return (r.cacheCreate || 0) * CACHE_WRITE_MULT * inRate; // breakdown absent
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// Aggregate economics over parsed records.
|
|
2066
|
+
function computeCacheEconomics(records) {
|
|
2067
|
+
let turns = 0, inTok = 0, outTok = 0, cacheRead = 0, cacheCreate = 0;
|
|
2068
|
+
let actualUsd = 0, uncachedUsd = 0, writePremiumUsd = 0;
|
|
2069
|
+
const perModel = {};
|
|
2070
|
+
|
|
2071
|
+
for (const r of records) {
|
|
2072
|
+
const rate = rateFor(r.model);
|
|
2073
|
+
if (!rate) continue;
|
|
2074
|
+
turns++;
|
|
2075
|
+
const inRate = rate.in / 1e6, outRate = rate.out / 1e6;
|
|
2076
|
+
|
|
2077
|
+
const readCost = r.cacheRead * inRate * CACHE_READ_MULT;
|
|
2078
|
+
const writeCost = writeCostUsd(r, inRate);
|
|
2079
|
+
const inCost = r.input * inRate;
|
|
2080
|
+
const outCost = r.output * outRate;
|
|
2081
|
+
const turnActual = readCost + writeCost + inCost + outCost;
|
|
2082
|
+
// What this turn would have cost with NO caching: every input-side token full price.
|
|
2083
|
+
const turnUncached = (r.cacheRead + r.cacheCreate + r.input) * inRate + outCost;
|
|
2084
|
+
|
|
2085
|
+
actualUsd += turnActual;
|
|
2086
|
+
uncachedUsd += turnUncached;
|
|
2087
|
+
writePremiumUsd += writeCost - (r.cacheCreate * inRate); // the >1× premium paid to cache
|
|
2088
|
+
|
|
2089
|
+
inTok += r.input; outTok += r.output; cacheRead += r.cacheRead; cacheCreate += r.cacheCreate;
|
|
2090
|
+
|
|
2091
|
+
const key = rate.label;
|
|
2092
|
+
const pm = perModel[key] || (perModel[key] = { turns: 0, actualUsd: 0, cacheRead: 0, inputSide: 0 });
|
|
2093
|
+
pm.turns++; pm.actualUsd += turnActual; pm.cacheRead += r.cacheRead;
|
|
2094
|
+
pm.inputSide += r.cacheRead + r.cacheCreate + r.input;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
const inputSide = cacheRead + cacheCreate + inTok;
|
|
2098
|
+
return {
|
|
2099
|
+
turns,
|
|
2100
|
+
tokens: { input: inTok, output: outTok, cacheRead, cacheCreate },
|
|
2101
|
+
actualUsd,
|
|
2102
|
+
uncachedUsd,
|
|
2103
|
+
savedUsd: uncachedUsd - actualUsd, // net $ caching saved (can dip negative early)
|
|
2104
|
+
writePremiumUsd, // $ invested establishing caches
|
|
2105
|
+
cacheHitRatio: inputSide ? cacheRead / inputSide : 0,
|
|
2106
|
+
costPerTurnUsd: turns ? actualUsd / turns : 0,
|
|
2107
|
+
perModel,
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// Attribute cache drops: a turn that re-ingests a large prefix cold after a warm
|
|
2112
|
+
// prior turn. Cause = model-switch | cache-expired | prefix-change.
|
|
2113
|
+
function detectInvalidators(records) {
|
|
2114
|
+
const leaks = [];
|
|
2115
|
+
const MIN_WARM = 2000;
|
|
2116
|
+
for (let i = 1; i < records.length; i++) {
|
|
2117
|
+
const prev = records[i - 1], cur = records[i];
|
|
2118
|
+
const prevWarm = prev.cacheRead + prev.input + prev.cacheCreate;
|
|
2119
|
+
if (prevWarm < MIN_WARM) continue;
|
|
2120
|
+
const curFresh = cur.input + cur.cacheCreate;
|
|
2121
|
+
const coldish = cur.cacheRead < prevWarm * 0.25 && curFresh > prevWarm * 0.5;
|
|
2122
|
+
if (!coldish) continue;
|
|
2123
|
+
|
|
2124
|
+
let cause;
|
|
2125
|
+
if (cur.model !== prev.model) cause = 'model-switch';
|
|
2126
|
+
else if (cur.ts && prev.ts && (cur.ts - prev.ts) > TTL_MS) cause = 'cache-expired';
|
|
2127
|
+
else cause = 'prefix-change';
|
|
2128
|
+
|
|
2129
|
+
const rate = rateFor(cur.model);
|
|
2130
|
+
const inRate = rate ? rate.in / 1e6 : 0;
|
|
2131
|
+
// Extra paid vs. having kept the prefix as a cheap cache read.
|
|
2132
|
+
const wastedUsd = prevWarm * inRate * (1 - CACHE_READ_MULT);
|
|
2133
|
+
leaks.push({ turn: i, cause, wastedUsd, model: cur.model });
|
|
2134
|
+
}
|
|
2135
|
+
return leaks;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// Convenience: locate → parse → compute → detect. Returns { ok:false } when no
|
|
2139
|
+
// transcript is available.
|
|
2140
|
+
function analyzeTranscript(opts = {}) {
|
|
2141
|
+
const transcript = locateTranscript(opts);
|
|
2142
|
+
if (!transcript) return { ok: false, reason: 'no-transcript' };
|
|
2143
|
+
const records = parseTranscriptUsage(transcript);
|
|
2144
|
+
if (!records.length) return { ok: false, reason: 'no-priceable-turns', transcript };
|
|
2145
|
+
return {
|
|
2146
|
+
ok: true,
|
|
2147
|
+
transcript,
|
|
2148
|
+
metrics: computeCacheEconomics(records),
|
|
2149
|
+
leaks: detectInvalidators(records),
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// ── Report formatting ────────────────────────────────────────────────────────
|
|
2154
|
+
|
|
2155
|
+
const CAUSE_LABEL = {
|
|
2156
|
+
'model-switch': 'model switch (cache is model-scoped)',
|
|
2157
|
+
'cache-expired': 'cache expired (gap > 5-min TTL)',
|
|
2158
|
+
'prefix-change': 'prefix changed (system prompt / tools / context edit)',
|
|
2159
|
+
};
|
|
2160
|
+
|
|
2161
|
+
function formatCacheReport(result) {
|
|
2162
|
+
if (!result.ok) {
|
|
2163
|
+
return result.reason === 'no-transcript'
|
|
2164
|
+
? 'agentic-security: no Claude Code transcript found for this project yet.'
|
|
2165
|
+
: 'agentic-security: transcript has no priceable model turns yet.';
|
|
2166
|
+
}
|
|
2167
|
+
const m = result.metrics;
|
|
2168
|
+
const lines = [];
|
|
2169
|
+
lines.push('');
|
|
2170
|
+
lines.push(' Prompt-cache economics — this session');
|
|
2171
|
+
lines.push(` ${result.turns ?? m.turns} model turns\n`);
|
|
2172
|
+
lines.push(` cache hit ratio ${(m.cacheHitRatio * 100).toFixed(1)}% (input-side tokens served from cache)`);
|
|
2173
|
+
lines.push(` spent ${money(m.actualUsd)} (~${money(m.costPerTurnUsd)}/turn)`);
|
|
2174
|
+
lines.push(` ▶ saved by caching ${money(m.savedUsd)} vs. ${money(m.uncachedUsd)} with no cache`);
|
|
2175
|
+
lines.push(` invested in caches ${money(m.writePremiumUsd)} (write premium over base input)`);
|
|
2176
|
+
lines.push('');
|
|
2177
|
+
lines.push(' tokens: '
|
|
2178
|
+
+ `${m.tokens.cacheRead.toLocaleString()} cached-read · `
|
|
2179
|
+
+ `${m.tokens.cacheCreate.toLocaleString()} cache-write · `
|
|
2180
|
+
+ `${m.tokens.input.toLocaleString()} fresh-in · `
|
|
2181
|
+
+ `${m.tokens.output.toLocaleString()} out`);
|
|
2182
|
+
|
|
2183
|
+
const models = Object.keys(m.perModel);
|
|
2184
|
+
if (models.length > 1) {
|
|
2185
|
+
lines.push('\n by model:');
|
|
2186
|
+
for (const k of models.sort()) {
|
|
2187
|
+
const pm = m.perModel[k];
|
|
2188
|
+
const hr = pm.inputSide ? (pm.cacheRead / pm.inputSide * 100).toFixed(0) : '0';
|
|
2189
|
+
lines.push(` ${k.padEnd(12)} ${pm.turns} turns · ${money(pm.actualUsd)} · ${hr}% cached`);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
if (result.leaks && result.leaks.length) {
|
|
2194
|
+
const wasted = result.leaks.reduce((s, l) => s + l.wastedUsd, 0);
|
|
2195
|
+
lines.push(`\n ⚠ cache leaks (${result.leaks.length}, ~${money(wasted)} wasted re-ingesting context):`);
|
|
2196
|
+
const byCause = {};
|
|
2197
|
+
for (const l of result.leaks) {
|
|
2198
|
+
(byCause[l.cause] || (byCause[l.cause] = { n: 0, usd: 0 })).n++;
|
|
2199
|
+
byCause[l.cause].usd += l.wastedUsd;
|
|
2200
|
+
}
|
|
2201
|
+
for (const c of Object.keys(byCause).sort()) {
|
|
2202
|
+
lines.push(` · ${byCause[c].n}× ${CAUSE_LABEL[c] || c} — ~${money(byCause[c].usd)}`);
|
|
2203
|
+
}
|
|
2204
|
+
lines.push(' Keep one model + a stable system prompt within a working window to avoid these.');
|
|
2205
|
+
} else {
|
|
2206
|
+
lines.push('\n ✓ no cache leaks detected — your context stayed warm.');
|
|
2207
|
+
}
|
|
2208
|
+
lines.push('');
|
|
2209
|
+
return lines.join('\n');
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// Test surface (underscore export is exempt from the dead-module gate).
|
|
2213
|
+
const _internal = {
|
|
2214
|
+
MODEL_RATES, CACHE_READ_MULT, CACHE_WRITE_MULT, CACHE_WRITE_1H_MULT,
|
|
2215
|
+
rateFor, locateTranscript, parseTranscriptUsage, computeCacheEconomics, detectInvalidators,
|
|
2216
|
+
};
|
|
2217
|
+
|
|
2218
|
+
|
|
1915
2219
|
/***/ })
|
|
1916
2220
|
|
|
1917
2221
|
};
|