cachelint 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.
@@ -0,0 +1,1200 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Versioned constants pinned in `cachelint.lock`.
5
+ *
6
+ * Bumping any of these means a previously-committed lock will be reported
7
+ * by `cachelint check` as "rules-moved" (exit code 1) with a specific
8
+ * upgrade message — never a generic diff. See docs/determinism-contract.md.
9
+ */
10
+ /** The published npm version of cachelint. */
11
+ declare const CACHELINT_VERSION: string;
12
+ /**
13
+ * Version of the lockfile's normalization rules (BOM strip, CRLF→LF,
14
+ * trailing-newline policy, sorted-by-codepoint JSON keys, ASCII-only escape).
15
+ * Bump only when a deliberate, documented behavior change ships.
16
+ */
17
+ declare const NORMALIZATION_POLICY_VERSION: 1;
18
+ /**
19
+ * Version of the rule table (which rules ship, which severities they default
20
+ * to). Adding a new rule WITHOUT changing existing rule outputs does not
21
+ * require a bump; changing what an existing rule flags does.
22
+ */
23
+ declare const RULE_TABLE_VERSION: 1;
24
+ /**
25
+ * Version of the cost model (volatility classes, from-here/chars-per-token
26
+ * formula, earliest-divergence aggregation, cache-engagement semantics).
27
+ * Bump when the cost *formula* or engagement semantics change — NOT for
28
+ * price-data updates, which bump the pricing table's own
29
+ * `PRICING_TABLE_VERSION` / `PRICING_AS_OF` in `src/cost/pricing.ts` instead.
30
+ */
31
+ declare const COST_MODEL_VERSION: 1;
32
+
33
+ /**
34
+ * The canonical MVP rule-id list and severity vocabulary.
35
+ *
36
+ * Kept in its own tiny module so `src/config/schema.ts` (Unit 2B) can validate
37
+ * config keys against it without importing the rule implementations (Unit 2C),
38
+ * which pull in `acorn` and other heavier deps. Unit 2C's `src/rules/index.ts`
39
+ * imports this list and asserts each rule registers under one of these ids.
40
+ *
41
+ * R006 / R007 are deferred to v0.2 (see the plan's Scope Boundaries) and are
42
+ * intentionally NOT here yet: a config that names `R006` should fail with the
43
+ * "unknown rule id" message until the rule actually ships.
44
+ */
45
+ /** Every rule id the MVP knows about. Order is the documented display order. */
46
+ declare const RULE_IDS: readonly ["R001", "R002", "R003", "R004", "R005"];
47
+ /** A known rule id. */
48
+ type RuleId = (typeof RULE_IDS)[number];
49
+ /** Default severity for each MVP rule (the "rule table", versioned via RULE_TABLE_VERSION). */
50
+ declare const DEFAULT_RULE_SEVERITY: Readonly<Record<RuleId, "error" | "warn">>;
51
+ /**
52
+ * How a finding's bytes move over time — the cost model's classification.
53
+ *
54
+ * - `"per-call"`: the offending value differs on every API call (interpolated
55
+ * timestamp, fresh UUID) → every call from the finding onward misses the
56
+ * cache; priced in dollars per 1,000 calls.
57
+ * - `"regeneration"`: the bytes are identical call-to-call and only change
58
+ * when the file is regenerated/edited (hardcoded timestamp, BOM, line
59
+ * endings) → reported as "tokens at risk", never given a dollar figure
60
+ * (conservative — see docs/cost-model.md, Unit 6).
61
+ */
62
+ type Volatility = "per-call" | "regeneration";
63
+ /**
64
+ * Default volatility class for each MVP rule (the rule-level default; R001
65
+ * overrides per pattern via `RawDiagnostic.volatility`). Changing a class is a
66
+ * cost-model semantics change → bump COST_MODEL_VERSION in `src/version.ts`.
67
+ *
68
+ * R003 is deliberately "regeneration": unsorted `JSON.stringify` is a
69
+ * fragility/risk — its output is often byte-stable run-to-run, so pricing it
70
+ * as per-call waste would overstate.
71
+ */
72
+ declare const DEFAULT_RULE_VOLATILITY: Readonly<Record<RuleId, Volatility>>;
73
+
74
+ /**
75
+ * One warning emitted during resolution but not severe enough to abort.
76
+ * (Hard problems throw `SourceError` instead.)
77
+ */
78
+ interface ResolutionWarning {
79
+ /** Machine-readable code: `"SKIPPED_SECRET"`, `"SKIPPED_BINARY"`, `"GLOB_EMPTY"`. */
80
+ readonly code: string;
81
+ /** Human one-liner. */
82
+ readonly message: string;
83
+ /** Optional path associated with the warning. */
84
+ readonly path?: string;
85
+ }
86
+
87
+ /**
88
+ * cachelint typed-error taxonomy.
89
+ *
90
+ * Every error thrown out of a command, the library API, or a rule MUST be one
91
+ * of these subclasses — so callers can `instanceof`-check, and the CLI's
92
+ * top-level catch can map each to its documented exit code.
93
+ *
94
+ * Exit-code contract (v1.0):
95
+ * 0 ok
96
+ * 1 hash moved / error-severity rule fired
97
+ * 2 Pro feature invoked without a license
98
+ * 3 cachelint.lock missing or corrupt
99
+ * 4 config / source error (incl. missing ANTHROPIC_API_KEY when --exact is licensed)
100
+ */
101
+
102
+ /** Base class for every error cachelint deliberately throws. */
103
+ declare class CachelintError extends Error {
104
+ /** Stable string code (e.g. `"CFG_INVALID"`) — safe to log and to grep. */
105
+ readonly code: string;
106
+ /** Suggested exit code if this error propagates to the CLI's top-level. */
107
+ readonly exitCode: number;
108
+ /** Optional one-line hint to print after the message. */
109
+ readonly hint?: string;
110
+ constructor(args: {
111
+ code: string;
112
+ message: string;
113
+ exitCode: number;
114
+ hint?: string;
115
+ cause?: unknown;
116
+ });
117
+ }
118
+ /** Configuration file / schema problems. Maps to exit 4. */
119
+ declare class ConfigError extends CachelintError {
120
+ /** Optional file path the error was found in. */
121
+ readonly file?: string;
122
+ /** Optional "key.path" inside the config that failed. */
123
+ readonly keyPath?: string;
124
+ constructor(args: {
125
+ code?: string;
126
+ message: string;
127
+ file?: string;
128
+ keyPath?: string;
129
+ hint?: string;
130
+ cause?: unknown;
131
+ });
132
+ }
133
+ /** Source resolution problems (missing files, oversize, binary, traversal). Maps to exit 4. */
134
+ declare class SourceError extends CachelintError {
135
+ readonly path?: string;
136
+ constructor(args: {
137
+ code?: string;
138
+ message: string;
139
+ path?: string;
140
+ hint?: string;
141
+ cause?: unknown;
142
+ });
143
+ }
144
+ /** Rule-engine problems (bad rule definition, sandbox violation). Maps to exit 4. */
145
+ declare class RuleError extends CachelintError {
146
+ readonly ruleId?: string;
147
+ constructor(args: {
148
+ code?: string;
149
+ message: string;
150
+ ruleId?: string;
151
+ hint?: string;
152
+ cause?: unknown;
153
+ });
154
+ }
155
+ /** `cachelint.lock` problems. Maps to exit 3. */
156
+ declare class LockError extends CachelintError {
157
+ constructor(args: {
158
+ code?: string;
159
+ message: string;
160
+ hint?: string;
161
+ cause?: unknown;
162
+ });
163
+ }
164
+ /** Specialisation: the lock file isn't there yet. */
165
+ declare class LockMissingError extends LockError {
166
+ constructor(path: string);
167
+ }
168
+ /** Specialisation: the lock file is unparseable / a different major / corrupt. */
169
+ declare class LockCorruptError extends LockError {
170
+ constructor(message: string, hint?: string);
171
+ }
172
+ /** Entitlement: Pro feature was invoked without a valid license. Maps to exit 2. */
173
+ declare class LicenseError extends CachelintError {
174
+ constructor(args: {
175
+ code?: string;
176
+ message: string;
177
+ hint?: string;
178
+ });
179
+ }
180
+ /** `--exact` token counting is requested but the API key is missing. Maps to exit 4 (config, not entitlement). */
181
+ declare class ExactCountUnavailableError extends CachelintError {
182
+ constructor(message: string, hint?: string);
183
+ }
184
+ /** Output adapter problems (unknown adapter, malformed adapter output). Maps to exit 4. */
185
+ declare class AdapterError extends CachelintError {
186
+ constructor(args: {
187
+ code?: string;
188
+ message: string;
189
+ hint?: string;
190
+ cause?: unknown;
191
+ });
192
+ }
193
+ /**
194
+ * Warning code signalling that cost estimation degraded (R7): the run
195
+ * completed without a `cost` block, and this warning is the only trace.
196
+ * Agents/embedders match on this sentinel — shared by `lint` and `check`.
197
+ */
198
+ declare const COST_UNAVAILABLE_CODE: "COST_UNAVAILABLE";
199
+ /** A rule violation surfaced to the user. */
200
+ type DiagnosticSeverity = "error" | "warn";
201
+ interface Diagnostic {
202
+ /** Rule id (e.g. "R001"). */
203
+ rule: string;
204
+ /** Severity at this site (after pragmas, config overrides, --strict). */
205
+ severity: DiagnosticSeverity;
206
+ /** Repo-relative file path with `/` separators. */
207
+ file: string;
208
+ /** 1-based line. */
209
+ line: number;
210
+ /** 1-based column. */
211
+ column: number;
212
+ /** One-line human message. */
213
+ message: string;
214
+ /** Short hint for the fix (one line). */
215
+ fixHint?: string;
216
+ /** URL of the rule's docs page (docs/rules/<id>.md). */
217
+ whyUrl?: string;
218
+ /** Snippet of the offending line (trimmed, no secrets). */
219
+ snippet?: string;
220
+ /**
221
+ * Volatility class stamped by the rules engine (per-pattern override or
222
+ * rule-level default). Classification metadata, not cost output — it stays
223
+ * in `--json` even when cost output is suppressed.
224
+ */
225
+ volatility?: Volatility;
226
+ }
227
+
228
+ /**
229
+ * The `cachelint.config.{json,yaml,yml}` schema.
230
+ *
231
+ * Config is **data only** — JSON or YAML, never `.ts`/`.js` (no code-execution
232
+ * attack surface at config-load time; see the plan's Key Technical Decisions).
233
+ * This module owns:
234
+ * - `configSchema` — the zod schema (strict: unknown keys rejected)
235
+ * - `CachelintConfig` — the inferred TS type
236
+ * - `defineConfig` — a typed identity helper for programmatic use
237
+ * - `configJsonSchema()` — the JSON Schema (draft 2020-12) for editors
238
+ *
239
+ * Loading + format resolution + turning zod errors into `ConfigError` lives in
240
+ * `src/config/load.ts`. The committed `schema/cachelint.config.schema.json` is
241
+ * generated from `configJsonSchema()`; `test/config/schema.test.ts` asserts the
242
+ * committed file is in sync so it can never silently drift.
243
+ */
244
+
245
+ /**
246
+ * The full config schema. `strictObject` ⇒ an unknown top-level key is a hard
247
+ * error (catches typos like `prompt_glob` early instead of silently ignoring).
248
+ */
249
+ declare const configSchema: z.ZodObject<{
250
+ $schema: z.ZodOptional<z.ZodString>;
251
+ prompt_globs: z.ZodOptional<z.ZodArray<z.ZodString>>;
252
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
253
+ disable: z.ZodOptional<z.ZodArray<z.ZodEnum<{
254
+ R001: "R001";
255
+ R002: "R002";
256
+ R003: "R003";
257
+ R004: "R004";
258
+ R005: "R005";
259
+ }>>>;
260
+ rules: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
261
+ R001: "R001";
262
+ R002: "R002";
263
+ R003: "R003";
264
+ R004: "R004";
265
+ R005: "R005";
266
+ }> & z.core.$partial, z.ZodEnum<{
267
+ error: "error";
268
+ off: "off";
269
+ warn: "warn";
270
+ }>>>;
271
+ bundles: z.ZodOptional<z.ZodArray<z.ZodObject<{
272
+ name: z.ZodString;
273
+ files: z.ZodArray<z.ZodString>;
274
+ }, z.core.$strict>>>;
275
+ max_file_bytes: z.ZodOptional<z.ZodNumber>;
276
+ max_total_bytes: z.ZodOptional<z.ZodNumber>;
277
+ pro: z.ZodOptional<z.ZodObject<{
278
+ exact_limit: z.ZodOptional<z.ZodNumber>;
279
+ packs: z.ZodOptional<z.ZodArray<z.ZodString>>;
280
+ }, z.core.$strict>>;
281
+ }, z.core.$strict>;
282
+ /** The validated, parsed config object. */
283
+ type CachelintConfig = z.infer<typeof configSchema>;
284
+ /** The shape a user writes (identical here — kept distinct for future divergence). */
285
+ type CachelintConfigInput = z.input<typeof configSchema>;
286
+ /**
287
+ * Typed identity helper for constructing a config in code (e.g. when calling
288
+ * the library API: `lint({ config: defineConfig({ prompt_globs: [...] }) })`).
289
+ * Does **no** runtime validation — that's `loadConfig` / `parseConfig`'s job —
290
+ * so it stays zero-cost and usable in any context. Mirrors the Vite/Vitest
291
+ * `defineConfig` convention.
292
+ */
293
+ declare function defineConfig(config: CachelintConfigInput): CachelintConfigInput;
294
+ /** The JSON Schema (draft 2020-12) for `cachelint.config.*` — for editor autocomplete. */
295
+ declare function configJsonSchema(): Record<string, unknown>;
296
+
297
+ /**
298
+ * Config discovery + loading for `cachelint.config.{json,yaml,yml}`.
299
+ *
300
+ * Data only — JSON or YAML, never executable. Everything that can go wrong
301
+ * surfaces as a `ConfigError` (exit code 4) with a precise message:
302
+ * - file named explicitly but missing → CFG_NOT_FOUND
303
+ * - a `.ts` / `.js` / `.cjs` / `.mjs` config path → CFG_CODE_NOT_ALLOWED
304
+ * - unparseable JSON / YAML → CFG_PARSE
305
+ * - schema violation (unknown key, bad type, …) → CFG_INVALID (lists the offending paths)
306
+ * - more than one auto-discovered config file → CFG_AMBIGUOUS
307
+ *
308
+ * `loadConfig` returns `{ config, path }` (or `{ config: {}, path: null }` when
309
+ * nothing is found and none was requested). `parseConfig` validates an already
310
+ * in-memory object (used by the library API and by tests).
311
+ */
312
+
313
+ /** Filenames auto-discovered in the project root, in precedence order. */
314
+ declare const CONFIG_FILENAMES: readonly ["cachelint.config.json", "cachelint.config.yaml", "cachelint.config.yml"];
315
+ interface LoadedConfig {
316
+ /** The validated config (an empty object if no file was found and none was requested). */
317
+ readonly config: CachelintConfig;
318
+ /** POSIX-relative path of the file that was loaded, or `null` if none. */
319
+ readonly path: string | null;
320
+ }
321
+ interface LoadConfigOptions {
322
+ /** Project root (absolute or resolved against cwd). Defaults to `process.cwd()`. */
323
+ readonly projectRoot?: string;
324
+ /**
325
+ * Explicit config path (from `--config`). If given and missing → CFG_NOT_FOUND.
326
+ * If omitted, we auto-discover among `CONFIG_FILENAMES` in the project root.
327
+ */
328
+ readonly explicitPath?: string;
329
+ }
330
+ /**
331
+ * Resolve, read, parse, and validate the project config.
332
+ * @throws {ConfigError} on any discovery/parse/validation problem.
333
+ */
334
+ declare function loadConfig(opts?: LoadConfigOptions): LoadedConfig;
335
+ /**
336
+ * Validate an already-in-memory config object (no disk access). Used by the
337
+ * library API (`lint({ config })`) and by tests.
338
+ * @throws {ConfigError} on schema violation.
339
+ */
340
+ declare function parseConfig(data: unknown, sourceLabel?: string): CachelintConfig;
341
+ /** What every command accepts for its `config` argument: a file path, an in-memory object, or nothing (auto-discover). */
342
+ type ConfigArg = string | CachelintConfigInput | undefined;
343
+ /** Resolve a command's `config` argument to a validated config + the loaded path (null for in-memory / not found). */
344
+ declare function resolveConfigArg(arg: ConfigArg, projectRoot: string): {
345
+ config: CachelintConfig;
346
+ configPath: string | null;
347
+ };
348
+
349
+ /**
350
+ * The cachelint rules engine.
351
+ *
352
+ * A *rule* is a pure function: `match(raw, ctx) => RawDiagnostic[]`. No I/O, no
353
+ * network, no filesystem writes, no dynamic `import()`. `test/rules/purity.sandbox.test.ts`
354
+ * enforces this by running each rule with `fs`/`net`/`child_process` poisoned.
355
+ *
356
+ * The engine:
357
+ * - owns the registry (id → Rule), keyed by the `src/rules/ids.ts` allow-list;
358
+ * - resolves effective severity per rule from config (`disable` + `rules`);
359
+ * - runs enabled rules over each `ResolvedSource` (operating on the RAW text,
360
+ * so reported line/column match the file as the author sees it);
361
+ * - applies `cachelint-disable-next-line` pragmas;
362
+ * - fills in `rule` / `severity` / `file` / `whyUrl` so rules only emit the
363
+ * location + message.
364
+ *
365
+ * `--strict` is NOT applied here — it only changes the *exit code*
366
+ * (`diagnosticsExitCode(..., { strict })` in `errors.ts`), not the displayed
367
+ * severity, mirroring ESLint's `--max-warnings 0`.
368
+ */
369
+
370
+ /** What a rule emits: a location + a message. The engine adds the rest. */
371
+ interface RawDiagnostic {
372
+ /** 1-based line in the RAW file. */
373
+ readonly line: number;
374
+ /** 1-based column in the RAW file. */
375
+ readonly column: number;
376
+ /** One-line human message. */
377
+ readonly message: string;
378
+ /** Short fix hint (one line). */
379
+ readonly fixHint?: string;
380
+ /** Trimmed snippet of the offending line (rule must ensure no secrets). */
381
+ readonly snippet?: string;
382
+ /** Per-pattern volatility override; when absent the rule's `volatility` applies. */
383
+ readonly volatility?: Volatility;
384
+ }
385
+ /** Per-file context handed to a rule's `match`. Read-only; no I/O capability. */
386
+ interface RuleContext {
387
+ /** Repo-relative POSIX path. */
388
+ readonly file: string;
389
+ /** Lowercased file extension including the dot ("" if none). */
390
+ readonly ext: string;
391
+ /** Whether the file had a leading UTF-8 BOM (precomputed in resolution). */
392
+ readonly bomByte: boolean;
393
+ /** Whether the file mixed CRLF and bare-LF/CR endings (precomputed). */
394
+ readonly mixedLineEndings: boolean;
395
+ /**
396
+ * Whether this file is in scope for "prompt-only" rules (currently just
397
+ * R003). True when the path matched `config.prompt_globs`, was named as a
398
+ * literal CLI argument, or (Unit 3) is already recorded in `cachelint.lock`.
399
+ * R003 never runs against arbitrary source — this gate is why.
400
+ */
401
+ readonly promptScoped: boolean;
402
+ }
403
+ /** A rule definition. `match` MUST be pure. */
404
+ interface Rule {
405
+ readonly id: RuleId;
406
+ /** Default severity (overridable per project via `config.rules`). */
407
+ readonly defaultSeverity: "error" | "warn";
408
+ /** Rule-level volatility class (overridable per finding via `RawDiagnostic.volatility`). */
409
+ readonly volatility: Volatility;
410
+ /** Short human title (also the SARIF rule shortDescription). */
411
+ readonly title: string;
412
+ /** Pure: no I/O, no network, no fs writes, no dynamic import. */
413
+ match(raw: string, ctx: RuleContext): RawDiagnostic[];
414
+ }
415
+ /** All registered rules, in display order. */
416
+ declare function allRules(): ReadonlyArray<Rule>;
417
+ /** Look up a rule by id (or `undefined`). */
418
+ declare function getRule(id: string): Rule | undefined;
419
+ /** The docs URL for a rule. */
420
+ declare function ruleDocsUrl(id: RuleId): string;
421
+
422
+ /**
423
+ * Static, versioned, dated Anthropic pricing table — the single source of
424
+ * price data for cost estimation. Embedded in the binary: no network, no
425
+ * environment reads, no I/O. Pure module.
426
+ *
427
+ * Versioning split (see docs/determinism-contract.md once Unit 6 documents it):
428
+ * - Price-data updates bump `PRICING_TABLE_VERSION` and `PRICING_AS_OF`.
429
+ * - Formula / engagement-semantics changes bump `COST_MODEL_VERSION` in
430
+ * `src/version.ts` instead.
431
+ *
432
+ * Prices verified against platform.claude.com/docs/en/pricing as of 2026-06.
433
+ */
434
+ /** Pricing facts for one model, in USD per million tokens. */
435
+ interface ModelPricing {
436
+ /** Model id as it appears in API requests (e.g. "claude-sonnet-4-6"). */
437
+ readonly model: string;
438
+ /** Base input price, $/MTok. */
439
+ readonly baseInputPerMTok: number;
440
+ /** Cache-read (cache-hit) input price, $/MTok. */
441
+ readonly cacheReadPerMTok: number;
442
+ /** Minimum prompt-prefix length (tokens) below which caching never engages. */
443
+ readonly minCacheablePrefixTokens: number;
444
+ }
445
+ /** Version of the price *data* below. Bump on any price/minimum change. */
446
+ declare const PRICING_TABLE_VERSION: 1;
447
+ /** The month the prices below were verified against the published price list. */
448
+ declare const PRICING_AS_OF = "2026-06";
449
+ /**
450
+ * The Sonnet-class reference model every dollar figure is computed against.
451
+ * Deliberately decoupled from `--exact`'s tokenizer default — see the plan's
452
+ * Key Technical Decisions (no reference-model override surface in v1).
453
+ */
454
+ declare const REFERENCE_MODEL = "claude-sonnet-4-6";
455
+ /**
456
+ * Look up a model's pricing. Returns `null` for an unknown model id — callers
457
+ * must handle it explicitly and never produce NaN dollars from a miss.
458
+ */
459
+ declare function getModelPricing(model: string): ModelPricing | null;
460
+ /** Pricing for the Sonnet-class reference model (always present in the table). */
461
+ declare function referencePricing(): ModelPricing;
462
+ /**
463
+ * Dollars wasted per invalidated token: the cache-read discount delta,
464
+ * (base input − cache read) / 1e6. The cache-write premium (1.25× on the
465
+ * re-written prefix) is deliberately ignored — every modeling choice errs
466
+ * toward understating, so a skeptical bill-payer recomputing the figure can
467
+ * only find it conservative.
468
+ */
469
+ declare function wastePerTokenUsd(p: ModelPricing): number;
470
+
471
+ /**
472
+ * The cost-estimation engine: from-here token counts, volatility-classed
473
+ * earliest-divergence aggregation, dollars per 1,000 calls.
474
+ *
475
+ * Pure: no I/O, no clock, no environment — mirroring the rules engine's
476
+ * purity discipline (`test/cost/purity.test.ts` enforces it statically).
477
+ *
478
+ * ALL cost math runs on NORMALIZED text — the same basis hashing and
479
+ * `--exact` use (`src/sources/normalize.ts`) — while diagnostics carry RAW
480
+ * line/column. The two coordinate systems differ (BOM strip, CRLF→LF,
481
+ * per-line right-trim), but normalization preserves line boundaries, so the
482
+ * mapping is line-wise: normalized offset = Σ(normalized line length + 1 LF)
483
+ * for the lines before the finding + min(column − 1, the finding's normalized
484
+ * line length). Everything is clamped so from-here counts are never negative
485
+ * — even when the column points into right-trimmed whitespace or the line
486
+ * sits in a collapsed trailing-blank-line run.
487
+ *
488
+ * Modeling choices (all conservative — err toward understating):
489
+ * - "per-call" findings get dollars; "regeneration" findings only count as
490
+ * "tokens at risk" and never get a dollar figure.
491
+ * - Per file *per class*, only the earliest finding counts toward totals
492
+ * (`countsTowardTotal`): later same-class findings sit inside the
493
+ * already-invalidated suffix. Classes never suppress each other — a BOM
494
+ * at 1:1 does not swallow a later interpolated UUID's dollar line.
495
+ * - Files are disjoint, so summing per-file earliest findings never double
496
+ * counts. At-risk tokens are totaled separately and NEVER added to waste.
497
+ * - chars/4 token heuristic: `fromHereTokens = ceil(remainingChars / 4)`.
498
+ */
499
+
500
+ /** Cost data for one diagnostic (parallel to the input diagnostics array). */
501
+ interface DiagnosticCost {
502
+ /** ceil(normalized chars from the finding to EOF / 4). */
503
+ readonly fromHereTokens: number;
504
+ /** Resolved volatility class (per-pattern override or rule default). */
505
+ readonly volatility: Volatility;
506
+ /**
507
+ * True only for the earliest finding in its file *and* class — the one
508
+ * whose `fromHereTokens` feeds the per-file and overall totals. Only totals
509
+ * are summable; summing per-diagnostic figures double-counts.
510
+ */
511
+ readonly countsTowardTotal: boolean;
512
+ /**
513
+ * Dollars per 1,000 calls — present only for the "per-call" class.
514
+ *
515
+ * Do NOT sum across diagnostics — use `CostTotals.wastedUsdPer1kCalls`
516
+ * instead; per-diagnostic figures overlap within a file, and entries with
517
+ * `countsTowardTotal: false` are informational only.
518
+ */
519
+ readonly wastedUsdPer1kCalls?: number;
520
+ }
521
+ /** Aggregated totals (per file, and overall across files). */
522
+ interface CostTotals {
523
+ /** From-here tokens of the earliest per-call finding (summed across files). */
524
+ readonly wastedTokens: number;
525
+ /** `wastedTokens` priced at the waste-per-token basis, per 1,000 calls. */
526
+ readonly wastedUsdPer1kCalls: number;
527
+ /** From-here tokens of the earliest regeneration finding — tracked separately, never added to waste. */
528
+ readonly atRiskTokens: number;
529
+ }
530
+ /** The full estimate for one run. */
531
+ interface CostEstimate {
532
+ /**
533
+ * One entry per input diagnostic, same order. `null` when no normalized
534
+ * text was supplied for the diagnostic's file (no cost can be computed).
535
+ */
536
+ readonly perDiagnostic: ReadonlyArray<DiagnosticCost | null>;
537
+ /** Totals per file (sorted by file path), only for files with cost data. */
538
+ readonly perFile: ReadonlyMap<string, CostTotals>;
539
+ /** Overall totals across all files. */
540
+ readonly totals: CostTotals;
541
+ }
542
+ /** Dollars per 1,000 calls for `tokens` re-billed at the full base-input rate. */
543
+ declare function usdPer1kCalls(tokens: number, pricing: ModelPricing): number;
544
+ /**
545
+ * The chars/4 heuristic: estimated tokens for `chars` normalized characters.
546
+ * Exported so `check`'s receipt (Unit 4) prices bundles on exactly the same
547
+ * basis as the per-diagnostic estimates.
548
+ */
549
+ declare function heuristicTokens(chars: number): number;
550
+ /**
551
+ * Estimate costs for a set of diagnostics against per-file NORMALIZED text
552
+ * (the `ResolvedSource.normalized` field — never raw text).
553
+ *
554
+ * `pricing` defaults to the Sonnet-class reference model; pass another table
555
+ * entry to re-price (the public output keeps the reference basis in v1).
556
+ */
557
+ declare function estimateCosts(diagnostics: ReadonlyArray<Diagnostic>, normalizedTextByFile: ReadonlyMap<string, string>, pricing?: ModelPricing): CostEstimate;
558
+
559
+ /**
560
+ * Shared dollar/label formatting for every cost-rendering surface (lint
561
+ * summary, check receipt, step summary). Pure functions only — renderers
562
+ * format precomputed numbers, nothing else.
563
+ */
564
+
565
+ /**
566
+ * Format a dollars-per-1,000-calls figure with 2 significant digits, e.g.
567
+ * `2.134 → "$2.1"`, `0.0123 → "$0.012"`, `126 → "$130"` (never exponent
568
+ * notation). Returns `null` below $0.01 per 1k calls — the caller then
569
+ * renders the tokens-only form.
570
+ *
571
+ * The 2-significant-digit rule is `Number(usd.toPrecision(2))` re-stringified:
572
+ * deterministic, trailing zeros dropped (`0.010 → "$0.01"`).
573
+ */
574
+ declare function formatUsdPer1k(usd: number): string | null;
575
+ /**
576
+ * The assumption-stating label printed next to every dollar figure, e.g.
577
+ * `est. if caching is enabled, claude-sonnet-4-6 pricing, 2026-06`.
578
+ *
579
+ * Accepts anything carrying a `model` name (a full `ModelPricing` entry, or
580
+ * the pricing metadata echoed in a result's cost block) so renderers can
581
+ * format from precomputed data without re-querying the table.
582
+ */
583
+ declare function costLabel(pricing: Pick<ModelPricing, "model">, asOf: string): string;
584
+ /**
585
+ * Deterministic thousands grouping (`1234567 → "1,234,567"`) — never
586
+ * `toLocaleString`, whose output depends on the ICU build.
587
+ */
588
+ declare function formatTokens(n: number): string;
589
+ /** Token line for the earliest per-call-volatile finding in a file. */
590
+ declare function costLineInvalidates(fromHereTokens: number): string;
591
+ /** Dollar line under the invalidates line. `usd` is a `formatUsdPer1k` result. */
592
+ declare function costLineWasted(usd: string, label: string): string;
593
+ /** Printed for later same-class findings in the same file (no double-claiming). */
594
+ declare const COST_LINE_OVERLAPPED = "(overlapped by the finding above \u2014 no additional waste)";
595
+ /** Token line for the earliest regeneration-class finding in a file (never dollars). */
596
+ declare function costLineAtRisk(fromHereTokens: number): string;
597
+ /**
598
+ * End-of-output cost summary phrase (without any `cachelint: ` prefix — the
599
+ * renderer adds its own). Returns `null` when no per-call-volatile waste was
600
+ * found (R2: the summary exists only when per-call findings exist); at-risk
601
+ * tokens are appended as a separate segment, never added to the waste figure.
602
+ * Below $0.01 per 1,000 calls the phrase falls back to tokens-only.
603
+ */
604
+ declare function costSummary(totals: {
605
+ readonly wastedTokens: number;
606
+ readonly wastedUsdPer1kCalls: number;
607
+ readonly atRiskTokens: number;
608
+ }, label: string): string | null;
609
+ /**
610
+ * The receipt headline: protected tokens + dollar value over the ENGAGED
611
+ * bundles only (bundles under the cacheable minimum are excluded — they are
612
+ * called out separately via `receiptWontEngageLine`). `usd` is a
613
+ * `formatUsdPer1k` result; `null` renders the under-a-cent form.
614
+ */
615
+ declare function receiptProtectedLine(tokens: number, engagedBundles: number, usd: string | null, label: string): string;
616
+ /** Per-bundle minimum-cacheable qualifier: this bundle is too short to cache at all. */
617
+ declare function receiptWontEngageLine(name: string, tokens: number, minimumTokens: number): string;
618
+ /** Printed instead of the headline when NO bundle reaches the cacheable minimum. */
619
+ declare function receiptAllBelowMinimumLine(minimumTokens: number, minimumSource: string): string;
620
+ /**
621
+ * Warn-findings qualifier printed next to the protected total on a green
622
+ * check (plan gap 1): the receipt must not claim the full value when warn
623
+ * findings already erode it. Returns `null` when there is neither waste nor
624
+ * at-risk data (clean run — no qualifier).
625
+ */
626
+ declare function receiptWarnQualifier(totals: {
627
+ readonly wastedTokens: number;
628
+ readonly wastedUsdPer1kCalls: number;
629
+ readonly atRiskTokens: number;
630
+ }): string | null;
631
+ /**
632
+ * The structural slice of a check cost report the receipt builder needs —
633
+ * kept structural (not `CheckCostReport`) so this module never imports from
634
+ * `src/commands/`.
635
+ */
636
+ interface ReceiptLinesInput {
637
+ readonly receipt: {
638
+ readonly bundles: ReadonlyArray<{
639
+ readonly name: string;
640
+ readonly protectedTokens: number;
641
+ readonly engaged: boolean;
642
+ }>;
643
+ readonly protectedTokens: number;
644
+ readonly protectedUsdPer1kCalls: number;
645
+ readonly engagementMinimumTokens: number;
646
+ readonly engagementMinimumSource: string;
647
+ readonly exactFallbackReason?: string;
648
+ };
649
+ readonly totals: {
650
+ readonly wastedTokens: number;
651
+ readonly wastedUsdPer1kCalls: number;
652
+ readonly atRiskTokens: number;
653
+ };
654
+ }
655
+ /**
656
+ * The green-check value receipt as UNPREFIXED phrase lines — the single
657
+ * builder behind every receipt surface. `renderCheckHuman` prefixes each line
658
+ * with `cachelint: `; the CI step summary turns each into a `- ` markdown
659
+ * bullet. Line order: protected headline (+ warn qualifier), per-bundle
660
+ * won't-engage callouts, the all-below-minimum callout (+ warn qualifier),
661
+ * and the `--exact` wholesale-fallback note.
662
+ */
663
+ declare function buildReceiptLines(cost: ReceiptLinesInput, label: string): string[];
664
+ /**
665
+ * One-line honesty note when `--exact` ran but could not cover every file of
666
+ * at least one bundle (call cap exhausted, or content changed mid-run): the
667
+ * affected bundles fall back WHOLESALE to the chars/4 heuristic — exact and
668
+ * heuristic counts are never blended within a bundle (R6).
669
+ */
670
+ declare const RECEIPT_EXACT_FALLBACK_LINE = "--exact did not cover every bundle file (call cap reached, or content changed during the run) \u2014 affected bundles use the chars/4 estimate";
671
+
672
+ /**
673
+ * `cachelint lint` — scan prompt content files for silent cache invalidators.
674
+ *
675
+ * Pipeline: load config → resolve sources (shared substrate) → run the rules
676
+ * engine → apply `disable` / `rules` severity overrides + `cachelint-disable`
677
+ * pragmas (inside the engine) → render. The library entry `lint()` returns the
678
+ * structured result and never writes to disk; the CLI wrapper adds `--json` /
679
+ * `--sarif` rendering and the process exit code.
680
+ *
681
+ * Exit-code contribution: `1` if any `error`-severity diagnostic fired (or any
682
+ * `warn` when `--strict`), else `0`. Pro-gated `--sarif` without a license is
683
+ * `2` (thrown as `LicenseError` before any output).
684
+ *
685
+ * R003 scoping: a file is "prompt-scoped" (eligible for R003) when it's matched
686
+ * by `config.prompt_globs` or named as a *literal* path on the command line.
687
+ * Glob-expanded CLI paths are NOT auto-scoped — that's what keeps a first run
688
+ * from lighting up with R003 findings across a whole repo.
689
+ */
690
+
691
+ interface LintArgs {
692
+ /** Paths/globs to lint. If empty, falls back to `config.prompt_globs`. */
693
+ readonly paths?: ReadonlyArray<string>;
694
+ /** Config: a file path, an in-memory config object, or omitted (auto-discover). */
695
+ readonly config?: ConfigArg;
696
+ /** Project root. Defaults to `process.cwd()`. */
697
+ readonly projectRoot?: string;
698
+ /** Treat `warn`-severity diagnostics as build failures for the exit code. */
699
+ readonly strict?: boolean;
700
+ /** Pro: load a provider bug-pattern pack by id (e.g. `"anthropic-2026-q1"`). */
701
+ readonly pack?: string;
702
+ /**
703
+ * Compute cost estimates and attach them as `LintResult.cost` (default
704
+ * `true`; the CLI's global `--no-cost` passes `false`). Cost output is
705
+ * cosmetic only — it never affects diagnostics, exit codes, or hashing (R7).
706
+ * Public API addition (additive, optional).
707
+ */
708
+ readonly cost?: boolean;
709
+ }
710
+ /**
711
+ * The structured cost block attached to a `LintResult` (and echoed verbatim
712
+ * into `--json` output). Absent entirely when cost output is disabled
713
+ * (`cost: false` / `--no-cost`) or when cost computation degraded — consumers
714
+ * detect cost availability by the block's presence (R4).
715
+ *
716
+ * Summability contract (documented as part of `costModelVersion: 1`): only
717
+ * `totals` is summable; per-diagnostic figures overlap within a file, and only
718
+ * entries with `countsTowardTotal: true` feed the totals.
719
+ */
720
+ interface LintCostReport {
721
+ /** Cost-*formula* version (see `COST_MODEL_VERSION` in src/version.ts). */
722
+ readonly costModelVersion: typeof COST_MODEL_VERSION;
723
+ /** Price-*data* provenance for every dollar figure in this block. */
724
+ readonly pricing: {
725
+ /** Version of the embedded pricing table (`PRICING_TABLE_VERSION`). */
726
+ readonly tableVersion: typeof PRICING_TABLE_VERSION;
727
+ /** Month the prices were verified, e.g. `"2026-06"`. */
728
+ readonly asOf: string;
729
+ /** The Sonnet-class reference model all dollar figures use. */
730
+ readonly referenceModel: string;
731
+ };
732
+ /**
733
+ * One entry per `LintResult.diagnostics` element, same order. `null` when
734
+ * no cost could be computed for that diagnostic's file.
735
+ */
736
+ readonly perDiagnostic: ReadonlyArray<DiagnosticCost | null>;
737
+ /** Earliest-divergence totals across all files (waste and at-risk are disjoint). */
738
+ readonly totals: CostTotals;
739
+ /**
740
+ * NORMALIZED character count per resolved file (repo-relative POSIX path →
741
+ * chars), keys sorted by path. Additive in cost-model v1 (Unit 4): the
742
+ * auditable input of the chars/4 heuristic — `check` derives its per-bundle
743
+ * receipt token counts from these without a second resolution pass.
744
+ *
745
+ * Part of the `costModelVersion: 1` stability contract: its presence,
746
+ * keying, and normalized-chars semantics only change with a
747
+ * `COST_MODEL_VERSION` bump.
748
+ */
749
+ readonly normalizedCharsByFile: Readonly<Record<string, number>>;
750
+ }
751
+ interface LintResult {
752
+ /** cachelint version that produced this result. */
753
+ readonly version: string;
754
+ /** POSIX-relative path of the loaded config file, or `null`. */
755
+ readonly configPath: string | null;
756
+ /** The Pro pack that was loaded for this run, or `null`. */
757
+ readonly packLoaded: string | null;
758
+ /** All diagnostics, sorted by (file, line, column, rule). */
759
+ readonly diagnostics: Diagnostic[];
760
+ /** Non-fatal resolution notes (skipped secrets/binaries, empty globs). */
761
+ readonly warnings: ResolutionWarning[];
762
+ /** `1` if the run should fail CI, else `0` (honoring `strict`). */
763
+ readonly exitCode: 0 | 1;
764
+ /** Cost estimates (R1/R2/R4). Absent under `cost: false` or on degradation. */
765
+ readonly cost?: LintCostReport;
766
+ }
767
+ /** Run the lint pipeline. Reads the filesystem; never writes. */
768
+ declare function lint(args?: LintArgs): LintResult;
769
+ interface RenderOptions {
770
+ /** Emit ANSI colors. */
771
+ readonly color?: boolean;
772
+ }
773
+ /** Human-readable report (ripgrep-ish: `file:line:col level RID message`). */
774
+ declare function renderLintHuman(result: LintResult, opts?: RenderOptions): string;
775
+ /** Stable JSON report. */
776
+ declare function renderLintJson(result: LintResult): string;
777
+
778
+ /**
779
+ * `cachelint.lock` — format, canonical serialization, read, atomic write.
780
+ *
781
+ * The lock is the single committed artifact and it stores **hashes only —
782
+ * never content**. It is byte-identical across Windows / macOS / Linux and
783
+ * Node 22/24 for the same inputs, because it is written through the
784
+ * canonical encoding below:
785
+ *
786
+ * - object keys sorted by Unicode codepoint (UTF-16 code-unit order — equal
787
+ * to codepoint order for the BMP, which is all we ever emit)
788
+ * - ASCII-only output: every char >= U+0080 is escaped as `\uXXXX` (surrogate
789
+ * pairs as two `\uXXXX`), so a non-ASCII path/message can't introduce
790
+ * OS-encoding drift
791
+ * - numbers serialized via `String(n)` (we only ever emit non-negative
792
+ * integers, where that equals JSON's own rendering)
793
+ * - 2-space indentation, LF newlines, and **no trailing newline** after the
794
+ * closing `}`
795
+ *
796
+ * The policy versions (`normalizationPolicyVersion`, `ruleTableVersion`) are
797
+ * stored so `cachelint check` can report a deliberate bump as "policy moved"
798
+ * rather than a confusing per-file diff. `cachelintVersion` is informational
799
+ * only — `check` ignores it when deciding pass/fail, so a no-op upgrade does
800
+ * not churn the file.
801
+ */
802
+ /** Bump when the *structure* of `cachelint.lock` changes (not its contents). */
803
+ declare const LOCKFILE_FORMAT_VERSION: 1;
804
+ /** Default lockfile path, relative to the project root. */
805
+ declare const DEFAULT_LOCKFILE_NAME = "cachelint.lock";
806
+ /** One file inside a bundle: its repo-relative POSIX path + the sha256 of its normalized content. */
807
+ interface LockFileEntry {
808
+ readonly path: string;
809
+ readonly sha256: string;
810
+ }
811
+ /** One bundle: a named, ordered group of files and the hash over their per-file hashes. */
812
+ interface LockBundle {
813
+ readonly name: string;
814
+ readonly files: ReadonlyArray<LockFileEntry>;
815
+ readonly stablePrefixHash: string;
816
+ }
817
+ /** The full lockfile object. */
818
+ interface Lockfile {
819
+ readonly version: number;
820
+ readonly cachelintVersion: string;
821
+ readonly normalizationPolicyVersion: number;
822
+ readonly ruleTableVersion: number;
823
+ /** Bundles, sorted by `name` (codepoint order). */
824
+ readonly bundles: ReadonlyArray<LockBundle>;
825
+ }
826
+ /** Serialize any JSON value with sorted keys, ASCII-only escaping, 2-space indent, no trailing newline. */
827
+ declare function canonicalJsonStringify(value: unknown): string;
828
+ /** The exact bytes that should be on disk for this lockfile (LF, no trailing newline). */
829
+ declare function serializeLockfile(lock: Lockfile): string;
830
+ /** Parse + validate a lockfile's text. Throws `LockCorruptError` on anything off. */
831
+ declare function parseLockfile(text: string, label?: string): Lockfile;
832
+
833
+ /**
834
+ * `cachelint hash [paths…]` — compute every bundle's `stablePrefixHash` and
835
+ * write / update `cachelint.lock`.
836
+ *
837
+ * The lock is the analogue of `package-lock.json`: `hash` is the *writer* (a
838
+ * developer runs it locally and commits the result); `check` is the *verifier*
839
+ * (CI). One direction. The file stores hashes only — never content. The write is
840
+ * atomic (temp file → rename).
841
+ *
842
+ * The library entry `hash()` returns the lockfile object + the text written + the
843
+ * path; it always writes (that's the command's whole job), so unlike `lint()` it
844
+ * is not side-effect-free.
845
+ */
846
+
847
+ interface HashArgs {
848
+ /** Paths/globs to hash as one-file bundles. If empty, falls back to `config.prompt_globs` (+ always `config.bundles`). */
849
+ readonly paths?: ReadonlyArray<string>;
850
+ readonly config?: ConfigArg;
851
+ /** Project root. Defaults to `process.cwd()`. */
852
+ readonly projectRoot?: string;
853
+ /** Lockfile path relative to the project root (default `cachelint.lock`). */
854
+ readonly out?: string;
855
+ }
856
+ interface HashResult {
857
+ /** The lockfile object that was written. */
858
+ readonly lockfile: Lockfile;
859
+ /** The exact text written to disk. */
860
+ readonly text: string;
861
+ /** Absolute path of the lockfile. */
862
+ readonly path: string;
863
+ /** POSIX-relative path of the loaded config, or `null`. */
864
+ readonly configPath: string | null;
865
+ /** Non-fatal resolution notes (skipped secrets/binaries, empty globs). */
866
+ readonly warnings: ResolutionWarning[];
867
+ /** Number of bundles recorded. */
868
+ readonly bundleCount: number;
869
+ }
870
+ /** Compute bundles and write `cachelint.lock`. */
871
+ declare function hash(args?: HashArgs): HashResult;
872
+ /** Human-readable summary for the CLI. */
873
+ declare function renderHashHuman(r: HashResult, projectRoot?: string): string;
874
+ /** Stable JSON report — lets an agent read back the written bundle hashes without parsing prose. */
875
+ declare function renderHashJson(r: HashResult, projectRoot?: string): string;
876
+
877
+ /**
878
+ * Categorized diff between a committed `cachelint.lock` and a freshly-recomputed
879
+ * one. This is what `cachelint check` reports.
880
+ *
881
+ * What counts as a *regression* (exit code 1):
882
+ * - a bundle's `stablePrefixHash` moved
883
+ * - a bundle was added or removed
884
+ * - a bundle's file list / per-file hashes changed
885
+ * - the normalization policy or rule-table version changed (a deliberate
886
+ * `cachelint` upgrade — reported with its own message, not as a file diff)
887
+ *
888
+ * `cachelintVersion` is intentionally NOT compared — a no-op upgrade must not
889
+ * make `check` fail.
890
+ */
891
+
892
+ interface PolicyChange {
893
+ readonly field: "normalizationPolicyVersion" | "ruleTableVersion";
894
+ readonly committed: number;
895
+ readonly current: number;
896
+ }
897
+ interface BundleHashChange {
898
+ readonly name: string;
899
+ readonly committedHash: string;
900
+ readonly currentHash: string;
901
+ /** Files whose path set or per-file hash changed (for the human report). */
902
+ readonly fileChanges: ReadonlyArray<{
903
+ path: string;
904
+ committed: string | null;
905
+ current: string | null;
906
+ }>;
907
+ }
908
+ interface LockDiff {
909
+ readonly policyChanges: ReadonlyArray<PolicyChange>;
910
+ readonly addedBundles: ReadonlyArray<string>;
911
+ readonly removedBundles: ReadonlyArray<string>;
912
+ readonly changedBundles: ReadonlyArray<BundleHashChange>;
913
+ }
914
+ /** Whether this diff represents a regression (i.e. `check` should fail). */
915
+ declare function isRegression(d: LockDiff): boolean;
916
+ /** Compute the diff of `current` against the `committed` lockfile. */
917
+ declare function diffLockfiles(committed: Lockfile, current: Lockfile): LockDiff;
918
+
919
+ interface ExactCountResult {
920
+ readonly sha256: string;
921
+ readonly model: string;
922
+ readonly tokens: number;
923
+ }
924
+
925
+ /**
926
+ * `cachelint check [--config]` — the headline CI gate.
927
+ *
928
+ * It recomputes every bundle's `stablePrefixHash`, reads the committed
929
+ * `cachelint.lock`, runs the lint rules over the bundle's files, and exits with
930
+ * the pinned table:
931
+ * 0 ok (lock matches; lint clean, or only `warn`s without `--strict`)
932
+ * 1 a `stablePrefixHash` moved / a bundle was added or removed / the
933
+ * normalization-policy or rule-table version changed, OR an `error`-severity
934
+ * rule fired (or a `warn` when `--strict`)
935
+ * 2 a Pro feature (`--exact`, `--sarif`, `--pack`) was invoked without a license
936
+ * 3 `cachelint.lock` missing or corrupt
937
+ * 4 config or source error (incl. missing `ANTHROPIC_API_KEY` when `--exact` IS licensed)
938
+ *
939
+ * Exit-code mapping is enforced by the typed errors (`LockMissingError`/3,
940
+ * `LockCorruptError`/3, `LicenseError`/2, `ExactCountUnavailableError`/4,
941
+ * `ConfigError`/`SourceError`/4); a regression diff or failing lint produces 1
942
+ * directly via the returned `exitCode`.
943
+ */
944
+
945
+ interface CheckArgs {
946
+ /** Paths/globs to treat as one-file bundles. If empty, falls back to `config.prompt_globs` (+ always `config.bundles`). */
947
+ readonly paths?: ReadonlyArray<string>;
948
+ readonly config?: ConfigArg;
949
+ readonly projectRoot?: string;
950
+ /** Lockfile path relative to the project root (default `cachelint.lock`). */
951
+ readonly lockfile?: string;
952
+ /** Treat `warn`-severity rule findings as failures for the exit code. */
953
+ readonly strict?: boolean;
954
+ /** Pro: also count tokens via the Anthropic count-tokens API. */
955
+ readonly exact?: boolean;
956
+ /** Pro: model whose tokenizer to use for `--exact` (default `DEFAULT_EXACT_MODEL`). */
957
+ readonly exactModel?: string;
958
+ /** Pro: cap on `--exact` API calls per run (default 100). */
959
+ readonly exactLimit?: number;
960
+ /** Pro: load a provider bug-pattern pack by id. */
961
+ readonly pack?: string;
962
+ /**
963
+ * Compute cost estimates + the value receipt and attach them as
964
+ * `CheckResult.cost` (default `true`; the CLI's global `--no-cost` passes
965
+ * `false` — forwarded to the embedded `lint()` call). Cost output is
966
+ * cosmetic only — it never affects diagnostics, exit codes, lockfile bytes,
967
+ * or hashing (R7). Public API addition (additive, optional).
968
+ */
969
+ readonly cost?: boolean;
970
+ }
971
+ /** One bundle's entry in the green-check value receipt (R3). */
972
+ interface ReceiptBundle {
973
+ readonly name: string;
974
+ /**
975
+ * Estimated cacheable-prefix tokens this bundle protects: `ceil(normalized
976
+ * chars / 4)` summed over the bundle's files — or the summed `--exact`
977
+ * counts when `tokenSource` is `"exact"`.
978
+ */
979
+ readonly protectedTokens: number;
980
+ /**
981
+ * `"exact"` only when EVERY file of this bundle got an `--exact` count this
982
+ * run; any skipped file (call cap, sha mismatch) falls the whole bundle
983
+ * back to `"heuristic"` — exact and heuristic counts are never blended (R6).
984
+ */
985
+ readonly tokenSource: "exact" | "heuristic";
986
+ /** Whether `protectedTokens` reaches the engagement minimum (caching can engage). */
987
+ readonly engaged: boolean;
988
+ /** Reference-model dollar value per 1,000 calls — present only when engaged. */
989
+ readonly protectedUsdPer1kCalls?: number;
990
+ }
991
+ /**
992
+ * The green-check value receipt (R3): what a stable prefix is worth. All
993
+ * dollar figures use the Sonnet-class reference model regardless of
994
+ * `--exact-model` (R5); the engagement minimum may be raised by an
995
+ * `--exact-model` present in the pricing table (the user handed us evidence
996
+ * of their real model — ignoring its higher minimum would overstate).
997
+ */
998
+ interface CheckReceipt {
999
+ /** One entry per bundle, in lockfile (name-sorted) order. */
1000
+ readonly bundles: ReadonlyArray<ReceiptBundle>;
1001
+ /** Protected tokens summed over ENGAGED bundles only (below-minimum bundles protect nothing yet). */
1002
+ readonly protectedTokens: number;
1003
+ /** `protectedTokens` valued at the reference model, per 1,000 calls. */
1004
+ readonly protectedUsdPer1kCalls: number;
1005
+ /** The minimum cacheable prefix length (tokens) used for the engagement check. */
1006
+ readonly engagementMinimumTokens: number;
1007
+ /** Model id whose minimum governs: the reference model, or a higher-minimum `--exact-model`. */
1008
+ readonly engagementMinimumSource: string;
1009
+ /** True when at least one bundle's token count came from `--exact` results. */
1010
+ readonly exactRefined: boolean;
1011
+ /** Present when `--exact` ran but at least one bundle fell back to the heuristic. */
1012
+ readonly exactFallbackReason?: string;
1013
+ }
1014
+ /**
1015
+ * The structured cost block attached to a `CheckResult` (and echoed verbatim
1016
+ * into `--json` output). The shared shape is exactly the embedded `lint()`
1017
+ * call's `LintCostReport` (no second resolution or estimation pass); `check`
1018
+ * adds only the `receipt`. Absent entirely under `cost: false` / `--no-cost`
1019
+ * or on degradation — presence signals availability (R4).
1020
+ */
1021
+ interface CheckCostReport extends LintCostReport {
1022
+ readonly receipt: CheckReceipt;
1023
+ }
1024
+ interface CheckResult {
1025
+ readonly version: string;
1026
+ readonly configPath: string | null;
1027
+ /** The Pro pack loaded for this run, or `null`. */
1028
+ readonly packLoaded: string | null;
1029
+ readonly lockfilePath: string;
1030
+ /** Diff of the recomputed bundles against the committed lock. */
1031
+ readonly diff: LockDiff;
1032
+ /** Whether the diff is a regression (hash moved / bundle added-removed / policy bumped). */
1033
+ readonly regression: boolean;
1034
+ /** Lint diagnostics over the bundle files. */
1035
+ readonly diagnostics: Diagnostic[];
1036
+ /** `--exact` token counts, when requested + licensed (else `null`). */
1037
+ readonly exactCounts: ExactCountResult[] | null;
1038
+ readonly warnings: ResolutionWarning[];
1039
+ /** `0` if everything checks out, `1` on a regression or a failing rule. */
1040
+ readonly exitCode: 0 | 1;
1041
+ /** Cost estimates + value receipt (R1/R3/R4). Absent under `cost: false` or on degradation. */
1042
+ readonly cost?: CheckCostReport;
1043
+ }
1044
+ /** Run the check pipeline. Reads the filesystem; never writes. Throws typed errors for exit 2/3/4. */
1045
+ declare function check(args?: CheckArgs): Promise<CheckResult>;
1046
+ declare function renderCheckHuman(r: CheckResult): string;
1047
+ declare function renderCheckJson(r: CheckResult, projectRoot?: string): string;
1048
+
1049
+ /**
1050
+ * Offline verification of a cachelint Pro license token. **No network, ever.**
1051
+ *
1052
+ * Token: `base64url(payloadJson) "." base64url(ed25519Signature)`. The signature
1053
+ * covers the *exact bytes* of `base64url(payloadJson)` (the part before the dot)
1054
+ * — so re-serializing the payload can't change what was signed. Verification:
1055
+ *
1056
+ * 1. split on the single `.`; base64url-decode both halves
1057
+ * 2. parse the payload JSON; require `v === 1`
1058
+ * 3. look up `kid` in the trusted-key set; reject an unknown `kid`
1059
+ * 4. `crypto.verify(null, signedBytes, key, sig)` (Ed25519 ⇒ algorithm `null`)
1060
+ * 5. reject if the `nonce` is on the revoked list
1061
+ * 6. reject if `plan !== "pro"`
1062
+ * 7. `exp`: accepted up to {@link GRACE_PERIOD_DAYS} days past expiry (with a
1063
+ * warning); beyond that, rejected
1064
+ *
1065
+ * On any failure this throws `LicenseError` (exit code 2) — and never echoes the
1066
+ * token bytes.
1067
+ */
1068
+ /** Days after `exp` that a token is still honored (with a warning) before hard rejection. */
1069
+ declare const GRACE_PERIOD_DAYS = 14;
1070
+ /** The Pro capabilities a license may grant. */
1071
+ type LicenseFeature = "sarif" | "exact" | "pack";
1072
+ interface LicensePayload {
1073
+ /** Payload schema version. Must be `1`. */
1074
+ readonly v: 1;
1075
+ /** Subject — the buyer's email (printed by `cachelint --version` as the only anti-share signal). */
1076
+ readonly sub: string;
1077
+ /** Plan — currently always `"pro"`. */
1078
+ readonly plan: "pro";
1079
+ /** Granted features. */
1080
+ readonly features: ReadonlyArray<LicenseFeature>;
1081
+ /** Issued-at (unix seconds). */
1082
+ readonly iat: number;
1083
+ /** Expiry (unix seconds). */
1084
+ readonly exp: number;
1085
+ /** Per-token nonce (revocation handle). */
1086
+ readonly nonce: string;
1087
+ /** Signing-key id (matched against the trusted-key set). */
1088
+ readonly kid: number;
1089
+ }
1090
+ interface VerifiedLicense {
1091
+ readonly payload: LicensePayload;
1092
+ /** True if the token is past `exp` but still inside the grace window. */
1093
+ readonly inGracePeriod: boolean;
1094
+ /** A human warning to surface (e.g. grace-period notice), or `null`. */
1095
+ readonly warning: string | null;
1096
+ }
1097
+ /** Verify `token`. Throws `LicenseError` on anything wrong. `now` is injectable for tests (unix ms). */
1098
+ declare function verifyLicenseToken(token: string, now?: number): VerifiedLicense;
1099
+
1100
+ /**
1101
+ * Pro-feature gate — the single place core code asks "is this licensed?".
1102
+ *
1103
+ * It reads the stored token (env override, then the per-user file), verifies it
1104
+ * offline (Ed25519, no network), and caches the result for the process. The
1105
+ * gate is *never* consulted on a core path — `lint`/`hash`/`check` without a Pro
1106
+ * flag don't touch it. On any verification failure the gate simply behaves as
1107
+ * "no license" (the failure is surfaced by `cachelint activate` / `license status`,
1108
+ * not by a core command).
1109
+ *
1110
+ * `requirePro(feature, …)` throws `LicenseError` (exit code 2) with the upgrade
1111
+ * hint when the feature isn't licensed.
1112
+ */
1113
+
1114
+ /** The Pro-gated capabilities. */
1115
+ type ProFeature = LicenseFeature;
1116
+ /** Public, token-free view of the active license. */
1117
+ interface ActiveLicenseInfo {
1118
+ readonly plan: "pro";
1119
+ /** Buyer email. */
1120
+ readonly sub: string;
1121
+ /** Expiry (unix seconds). */
1122
+ readonly exp: number;
1123
+ /** Signing-key id. */
1124
+ readonly kid: number;
1125
+ /** Granted features. */
1126
+ readonly features: ReadonlyArray<ProFeature>;
1127
+ /** True if past `exp` but inside the grace window. */
1128
+ readonly inGracePeriod: boolean;
1129
+ /** Where the token came from (`env …` or a file path) — never the token itself. */
1130
+ readonly source: string;
1131
+ }
1132
+ /** The active license info, or `null` if none / invalid. Never returns the raw token. */
1133
+ declare function activeLicenseInfo(): ActiveLicenseInfo | null;
1134
+ /** Whether a Pro feature is licensed. With no argument: whether *any* valid Pro license is active. */
1135
+ declare function isPro(feature?: ProFeature): boolean;
1136
+ /**
1137
+ * Throw {@link LicenseError} (exit code 2) unless `feature` is licensed.
1138
+ * @param feature the capability being used
1139
+ * @param whatHuman a short noun phrase for the message, e.g. "`--sarif` output"
1140
+ */
1141
+ declare function requirePro(feature: ProFeature, whatHuman: string): void;
1142
+
1143
+ /**
1144
+ * `cachelint activate <key>` / `cachelint license [status]` / `cachelint deactivate`.
1145
+ *
1146
+ * All offline. `activate` verifies the token (Ed25519, no network) and, on
1147
+ * success, writes it to the per-user license file; it never echoes the token
1148
+ * back. `license status` prints the plan, buyer email, expiry, and granted
1149
+ * features — never the raw token. `deactivate` removes the file (and warns if a
1150
+ * `CACHELINT_LICENSE_KEY` env override is still in effect).
1151
+ */
1152
+
1153
+ interface ActivateResult {
1154
+ readonly info: ActiveLicenseInfo;
1155
+ /** Path the token was written to. */
1156
+ readonly storedAt: string;
1157
+ /** Grace-period / other notice, or `null`. */
1158
+ readonly warning: string | null;
1159
+ }
1160
+ /** Verify and store a license token. Throws `LicenseError` (exit 2) on a bad/expired token. */
1161
+ declare function activate(token: string): ActivateResult;
1162
+ interface LicenseStatus {
1163
+ readonly active: boolean;
1164
+ readonly info: ActiveLicenseInfo | null;
1165
+ /** Path where a stored license would live (whether or not one is there). */
1166
+ readonly filePath: string;
1167
+ }
1168
+ /** Inspect the active license. Never throws for "no license" — that's just `active: false`. */
1169
+ declare function licenseStatus(): LicenseStatus;
1170
+ interface DeactivateResult {
1171
+ /** Whether a stored file was actually removed. */
1172
+ readonly removed: boolean;
1173
+ /** True if a CACHELINT_LICENSE_KEY env override is still active (so Pro stays on). */
1174
+ readonly envStillActive: boolean;
1175
+ readonly filePath: string;
1176
+ }
1177
+ /** Remove the stored license file. */
1178
+ declare function deactivate(): DeactivateResult;
1179
+
1180
+ /**
1181
+ * Provider-specific bug-pattern packs (Pro, `--pack <id>`).
1182
+ *
1183
+ * The pack *surface* ships in the MVP — the flag is recognized, Pro-gated, and
1184
+ * the id is validated — but no pack rules are wired in yet: the rules need an
1185
+ * FP audit against real provider-using repos before they earn a place, and
1186
+ * shipping speculative ones would undermine the linter's signal. `packRules(id)`
1187
+ * therefore returns `[]` for now. When the `anthropic-2026-q1` rules are ready,
1188
+ * wiring them in is two steps: (1) add an `extraRules?: ReadonlyArray<Rule>`
1189
+ * option to `RunRulesOptions` in `src/rules/index.ts` and run them alongside the
1190
+ * static `REGISTRY`; (2) have `lint()`/`check()` pass `packRules(packLoaded)`.
1191
+ *
1192
+ * `anthropic-2026-q1` is reserved for rules derived from the early-2026 Claude
1193
+ * Code cache regressions (`anthropics/claude-code#34629` / `#46829` / `#41930`
1194
+ * and the April-23 postmortem).
1195
+ */
1196
+
1197
+ /** Pack ids this build recognizes. */
1198
+ declare const KNOWN_PACK_IDS: ReadonlySet<string>;
1199
+
1200
+ export { type ActivateResult, type ActiveLicenseInfo, AdapterError, CACHELINT_VERSION, CONFIG_FILENAMES, COST_LINE_OVERLAPPED, COST_MODEL_VERSION, COST_UNAVAILABLE_CODE, type CachelintConfig, type CachelintConfigInput, CachelintError, type CheckArgs, type CheckCostReport, type CheckReceipt, type CheckResult, type ConfigArg, ConfigError, type CostEstimate, type CostTotals, DEFAULT_LOCKFILE_NAME, DEFAULT_RULE_SEVERITY, DEFAULT_RULE_VOLATILITY, type DeactivateResult, type Diagnostic, type DiagnosticCost, type DiagnosticSeverity, ExactCountUnavailableError, GRACE_PERIOD_DAYS, type HashArgs, type HashResult, KNOWN_PACK_IDS, LOCKFILE_FORMAT_VERSION, LicenseError, type LicenseFeature, type LicensePayload, type LicenseStatus, type LintArgs, type LintCostReport, type LintResult, type LoadConfigOptions, type LoadedConfig, type LockBundle, LockCorruptError, type LockDiff, LockError, type LockFileEntry, LockMissingError, type Lockfile, type ModelPricing, NORMALIZATION_POLICY_VERSION, PRICING_AS_OF, PRICING_TABLE_VERSION, type ProFeature, RECEIPT_EXACT_FALLBACK_LINE, REFERENCE_MODEL, RULE_IDS, RULE_TABLE_VERSION, type RawDiagnostic, type ReceiptBundle, type ReceiptLinesInput, type Rule, type RuleContext, RuleError, type RuleId, SourceError, type VerifiedLicense, type Volatility, activate, activeLicenseInfo, allRules, buildReceiptLines, canonicalJsonStringify, check, configJsonSchema, configSchema, costLabel, costLineAtRisk, costLineInvalidates, costLineWasted, costSummary, deactivate, defineConfig, diffLockfiles, estimateCosts, formatTokens, formatUsdPer1k, getModelPricing, getRule, hash, heuristicTokens, isPro, isRegression, licenseStatus, lint, loadConfig, parseConfig, parseLockfile, receiptAllBelowMinimumLine, receiptProtectedLine, receiptWarnQualifier, receiptWontEngageLine, referencePricing, renderCheckHuman, renderCheckJson, renderHashHuman, renderHashJson, renderLintHuman, renderLintJson, requirePro, resolveConfigArg, ruleDocsUrl, serializeLockfile, usdPer1kCalls, verifyLicenseToken, wastePerTokenUsd };