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.
- package/LICENSE +27 -0
- package/LICENSE-PRO.md +67 -0
- package/README.md +298 -0
- package/dist/check-2ONJCYKP.js +23 -0
- package/dist/chunk-2JSY5BQA.js +340 -0
- package/dist/chunk-4GHSUBAY.js +28 -0
- package/dist/chunk-4LB5LF2P.js +51 -0
- package/dist/chunk-BUZCVKMZ.js +246 -0
- package/dist/chunk-DYQPXD3G.js +92 -0
- package/dist/chunk-GXASKZ4Z.js +182 -0
- package/dist/chunk-JZU4PCTS.js +80 -0
- package/dist/chunk-MW7BXKPR.js +593 -0
- package/dist/chunk-O7ZE6CFI.js +32 -0
- package/dist/chunk-SQ4IAMTX.js +65 -0
- package/dist/chunk-UUVSBJ62.js +77 -0
- package/dist/chunk-VWE5TDL5.js +91 -0
- package/dist/chunk-WKFGRNYK.js +494 -0
- package/dist/chunk-WPJYUQMU.js +26 -0
- package/dist/chunk-XKRPGMZT.js +197 -0
- package/dist/chunk-ZSEZQ422.js +286 -0
- package/dist/cli.d.ts +46 -0
- package/dist/cli.js +199 -0
- package/dist/gate-63NKQRXL.js +16 -0
- package/dist/hash-XUTGK6SB.js +15 -0
- package/dist/index.d.ts +1200 -0
- package/dist/index.js +184 -0
- package/dist/license-GFDBTQHG.js +19 -0
- package/dist/lint-PQHIDWOY.js +19 -0
- package/dist/sarif-EQ6VJNGN.js +63 -0
- package/dist/step-summary-NKD4N7AY.js +160 -0
- package/dist/store-KZIQANIT.js +14 -0
- package/package.json +85 -0
- package/schema/cachelint.config.schema.json +114 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|