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 ADDED
@@ -0,0 +1,27 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cachelint contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ Note: the **Pro** features of cachelint (SARIF output, `--exact` token counting,
26
+ provider-specific bug-pattern packs) are governed by the terms in LICENSE-PRO.md
27
+ and are unlocked by a license key. The MIT terms above apply to the free core.
package/LICENSE-PRO.md ADDED
@@ -0,0 +1,67 @@
1
+ # cachelint Pro — Commercial License
2
+
3
+ The cachelint **core** — everything reachable without a Pro license key — is MIT
4
+ licensed; see [`LICENSE`](./LICENSE). This document governs the **Pro features**:
5
+ SARIF output (`--sarif`), exact token counting (`--exact`), and provider
6
+ bug-pattern packs (`--pack …`).
7
+
8
+ ## Grant
9
+
10
+ Subject to your compliance with these terms, you (the named licensee) are granted
11
+ a non-exclusive, non-transferable, worldwide license to use the cachelint Pro
12
+ features for the duration of your license term:
13
+
14
+ - **Individual license** — one named person; perpetual; covers the version
15
+ released during the purchase term and any patch releases of that line.
16
+ - **Team license** — your organization; renews **annually**; unlimited seats
17
+ within the organization.
18
+
19
+ The buyer's email address is embedded in the license key payload and is printed
20
+ by `cachelint --version` when a license is active. This is the only anti-sharing
21
+ signal in the product; you consent to it at checkout. There is **no telemetry**,
22
+ **no phone-home**, and **no network call** anywhere in license verification — keys
23
+ are verified offline with a bundled Ed25519 public key.
24
+
25
+ ## Restrictions
26
+
27
+ You may not: redistribute or resell the license key; remove or obscure the
28
+ buyer-email signal; use the key on behalf of a different person/organization than
29
+ the named licensee; or circumvent the license gate.
30
+
31
+ A leaked or shared key may be added to the revocation denylist that ships in
32
+ subsequent releases; team keys also expire annually, bounding the window. A key
33
+ within 14 days past its expiry still works (with a warning) so a
34
+ renewal lapse never breaks your CI mid-sprint.
35
+
36
+ ## Important: SARIF on private repositories
37
+
38
+ `--sarif` produces a SARIF file for GitHub Code Scanning. **Uploading SARIF to a
39
+ private repository requires GitHub Advanced Security.** On public repositories it
40
+ works on the free GitHub plan. cachelint cannot change this — it is GitHub's
41
+ limitation — so if your repo is private and you don't have Advanced Security, the
42
+ free `cachelint lint` (plain stderr + exit codes) and `cachelint check` (the hash
43
+ gate) are the parts of the gate you'll actually use; `--sarif` adds annotations on
44
+ top, not core functionality.
45
+
46
+ ## Refunds
47
+
48
+ If cachelint Pro doesn't do what the README says it does, ask for a refund within
49
+ 30 days of purchase. (We'd rather fix it — open an issue.)
50
+
51
+ ## Warranty / liability
52
+
53
+ The Pro features are provided "as is", without warranty of any kind. To the
54
+ maximum extent permitted by law, the maintainers are not liable for any damages
55
+ arising from their use. Nothing here limits liability that cannot be limited by
56
+ law.
57
+
58
+ ## Contact
59
+
60
+ Purchases, license questions, key reissues: see <https://github.com/davioe/cachelint#pro>.
61
+
62
+ ---
63
+
64
+ *This file ships in the npm package and the repo so the terms travel with the
65
+ tool. See [`docs/licensing.md`](./docs/licensing.md) for the authoritative
66
+ description of the token format, offline verification, the 14-day grace period,
67
+ and key-rotation mechanics.*
package/README.md ADDED
@@ -0,0 +1,298 @@
1
+ # cachelint
2
+
3
+ > A linter + CI gate for LLM prompt-cache regressions.
4
+
5
+ Anthropic and OpenAI prompt caching are **prefix matches**: a single byte change
6
+ anywhere in the cacheable prefix invalidates the cache. Common causes — an
7
+ interpolated `{{ now }}` in a system prompt, an unsorted `JSON.stringify`, a
8
+ leading BOM, mixed line endings — sail through code review and quietly **10–20×
9
+ your token cost** in production. It's documented in Anthropic's own bug tracker:
10
+
11
+ - [`anthropics/claude-code#34629`](https://github.com/anthropics/claude-code/issues/34629) — `cache_read_input_tokens` never grows, ~20× cost increase
12
+ - [`anthropics/claude-code#46829`](https://github.com/anthropics/claude-code/issues/46829) — TTL silently regressed 1h → 5m, 15–53% overpayment
13
+ - [`anthropics/claude-code#41930`](https://github.com/anthropics/claude-code/issues/41930) — widespread quota drain, March 23 2026
14
+ - Anthropic's [April 23 2026 engineering postmortem](https://www.anthropic.com/engineering/april-23-postmortem) — a caching-logic bug was one of the three root causes
15
+ - …and cross-ecosystem reports (`openclaw/openclaw#19534`, `pydantic-ai#3453`, `BerriAI/litellm#5285`, …) all tracing back to dynamic content in a "stable" prompt.
16
+
17
+ `cachelint` does **one thing**: it catches your own committed prompt content
18
+ silently breaking the cacheable prefix — at edit time and in CI — before it
19
+ ships. It is **zero-cost to run** (no servers, no paid APIs, no telemetry).
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm i -g cachelint # or: npx cachelint …
25
+ ```
26
+
27
+ ESM-only, Node ≥ 22. Ships a `cachelint` binary and a library
28
+ (`import { lint, hash, check } from "cachelint"`).
29
+
30
+ ## Quickstart
31
+
32
+ 1. **Lint** your prompt files for known silent-invalidator patterns:
33
+
34
+ ```bash
35
+ cachelint lint prompts/system.md prompts/tools.json
36
+ ```
37
+
38
+ 2. **Hash** them and commit the lockfile (like `package-lock.json` — `hash`
39
+ writes, `check` verifies):
40
+
41
+ ```bash
42
+ cachelint hash prompts/system.md prompts/tools.json # writes cachelint.lock
43
+ git add cachelint.lock && git commit -m "lock prompt-cache prefixes"
44
+ ```
45
+
46
+ 3. **Check** in CI — fails the build if a committed prompt's stable-prefix hash
47
+ moved without a fresh `cachelint hash`:
48
+
49
+ ```bash
50
+ cachelint check
51
+ ```
52
+
53
+ That's it. If `check` fails and the change was intentional, re-run
54
+ `cachelint hash …` and commit the updated `cachelint.lock`.
55
+
56
+ ### Optional config
57
+
58
+ `cachelint.config.json` (or `.yaml` / `.yml` — data only, never executable):
59
+
60
+ ```jsonc
61
+ {
62
+ "$schema": "https://raw.githubusercontent.com/davioe/cachelint/main/schema/cachelint.config.schema.json",
63
+ "prompt_globs": ["prompts/**/*.md", "prompts/**/*.json"],
64
+ "bundles": [
65
+ { "name": "system", "files": ["prompts/system.md", "prompts/tools.json"] }
66
+ ],
67
+ "disable": [], // e.g. ["R003"]
68
+ "rules": { "R001": "error" } // override default severities; "off" disables
69
+ }
70
+ ```
71
+
72
+ With `prompt_globs` set you can just run `cachelint lint` / `cachelint hash` /
73
+ `cachelint check` with no arguments.
74
+
75
+ ### Suppressing a finding
76
+
77
+ Inline, ESLint-style — `cachelint-disable-next-line` works in any comment style:
78
+
79
+ ```md
80
+ <!-- cachelint-disable-next-line R001 -->
81
+ The treaty was signed on 1989-11-09T18:53:00Z.
82
+ ```
83
+
84
+ ## GitHub Action
85
+
86
+ ```yaml
87
+ # .github/workflows/cachelint.yml
88
+ permissions:
89
+ contents: read
90
+ # security-events: write # only if you set `sarif:` below (Pro)
91
+ jobs:
92
+ cachelint:
93
+ runs-on: ubuntu-latest
94
+ steps:
95
+ - uses: actions/checkout@v4
96
+ - uses: davioe/cachelint@<commit-sha> # pin to a SHA, not a tag
97
+ with:
98
+ paths: 'prompts/**/*.md prompts/**/*.json'
99
+ # license: ${{ secrets.CACHELINT_LICENSE_KEY }} # optional, for Pro
100
+ ```
101
+
102
+ The action masks the license input, installs an **exact** `cachelint` version
103
+ (never a floating tag), runs `lint` then `check`, and (Pro) uploads SARIF for PR
104
+ annotations. Pin the action itself to a **commit SHA**, not a moving tag.
105
+
106
+ ### CI job summary
107
+
108
+ On GitHub Actions — and on any CI that exposes a `GITHUB_STEP_SUMMARY` file
109
+ path — `cachelint lint` and `cachelint check` each append a titled
110
+ cache-health section (findings table, bundle stability, cost estimates) to the
111
+ job summary, even on thrown errors. It is best-effort and purely cosmetic:
112
+ it never affects exit codes, respects `--no-cost`, redacts secrets, and is a
113
+ **silent no-op** where the env var is absent. Opt out by setting
114
+ `CACHELINT_NO_STEP_SUMMARY` to **any non-empty value** — e.g.
115
+ `CACHELINT_NO_STEP_SUMMARY=1` (or the action's `no-step-summary: "true"`
116
+ input).
117
+
118
+ ### Fork PRs on public repos
119
+
120
+ Fork PRs don't get repo secrets, so a license-gated step would fail on them. If
121
+ you use a Pro feature, gate just that and let the free `lint` + `check` (which
122
+ need no secret) run everywhere:
123
+
124
+ ```yaml
125
+ - uses: davioe/cachelint@<sha>
126
+ continue-on-error: ${{ github.event.pull_request.head.repo.fork }}
127
+ with:
128
+ paths: 'prompts/**/*'
129
+ license: ${{ secrets.CACHELINT_LICENSE_KEY }}
130
+ sarif: cachelint.sarif
131
+ ```
132
+
133
+ (The free tier alone catches the whole silent-invalidator class; Pro adds the
134
+ SARIF annotations on top.)
135
+
136
+ ### README badge *(once published)*
137
+
138
+ Once the repo is public, advertise the gate with a static shields.io badge —
139
+ swap in your `OWNER/REPO` and workflow file name:
140
+
141
+ ```md
142
+ [![cachelint](https://img.shields.io/github/actions/workflow/status/OWNER/REPO/cachelint.yml?label=cachelint)](https://github.com/OWNER/REPO/actions/workflows/cachelint.yml)
143
+ ```
144
+
145
+ Or the label-only variant if you don't run the workflow on every push:
146
+
147
+ ```md
148
+ [![prompt cache: protected](https://img.shields.io/badge/prompt_cache-protected-brightgreen)](https://github.com/davioe/cachelint)
149
+ ```
150
+
151
+ ## Pre-commit hooks
152
+
153
+ The repo ships [`.pre-commit-hooks.yaml`](.pre-commit-hooks.yaml) with two
154
+ hooks for the [pre-commit](https://pre-commit.com) framework — `cachelint`
155
+ (lint) and `cachelint-check` (the lockfile gate). *(Once published — the
156
+ `repo:` URL goes live when the repo does.)*
157
+
158
+ ```yaml
159
+ # .pre-commit-config.yaml
160
+ repos:
161
+ - repo: https://github.com/davioe/cachelint
162
+ rev: v0.1.0 # pin to a release tag
163
+ hooks:
164
+ - id: cachelint
165
+ - id: cachelint-check
166
+ ```
167
+
168
+ Both hooks run config-scoped on the whole project (`pass_filenames: false`,
169
+ `always_run: true`) — deliberate: `check` must see the full bundle set
170
+ (passed filenames would redefine the bundles), and `lint` stays scoped to
171
+ `prompt_globs` so R003 behaves exactly like CI.
172
+
173
+ ## What gets caught
174
+
175
+ | Rule | Pattern | Severity | Why it matters | Docs |
176
+ | ----- | ---------------------------------------------------------------------- | -------- | --------------------------------------------------- | ---- |
177
+ | R001 | Interpolated timestamp (`{{ now }}`, `Date.now()`, `datetime.now()`, "Current Date & Time:", ISO-8601 datetimes) | warn | Prefix bytes change every call → 100% cache miss after that point | [R001](docs/rules/R001.md) |
178
+ | R002 | Interpolated UUID / random (`crypto.randomUUID()`, `Math.random()`, `uuidv4()`, `nanoid()`, `{{ requestId }}`, …) | warn | Same — non-determinism in the prefix | [R002](docs/rules/R002.md) |
179
+ | R003 | Unsorted `JSON.stringify` (no key-sort replacer) — only on files in `prompt_globs` / `cachelint.lock` / named literally | warn | Object key order isn't guaranteed stable → cache thrash | [R003](docs/rules/R003.md) |
180
+ | R004 | Leading UTF-8 BOM | error | An invisible char at the head of the prompt; cross-OS checkouts introduce it | [R004](docs/rules/R004.md) |
181
+ | R005 | Mixed CRLF / LF / CR line endings in one file | error | Hashes differently on Windows vs Linux even though the text "looks" identical | [R005](docs/rules/R005.md) |
182
+
183
+ `cachelint hash` / `check` normalize line endings + strip the BOM before
184
+ hashing, so the *gate* isn't fooled — but the lint rules still flag the source
185
+ so the inconsistency gets fixed. Pre-launch the false-positive rate of
186
+ R001–R003 is audited against real OSS repos and published in each rule's page.
187
+
188
+ ## Output
189
+
190
+ - Human (default, on stderr): `file:line:col level RID message`, plus a fix
191
+ hint and a docs link per finding.
192
+ - `--json` (on stdout): a stable schema — `{ version, configPath, diagnostics, warnings, exitCode }`,
193
+ plus an optional `cost` block (absent under `--no-cost`).
194
+ - `--sarif <path>` (Pro): SARIF v2.1 for GitHub Code Scanning.
195
+
196
+ ### Cost estimates
197
+
198
+ Findings carry a deliberately conservative dollar estimate of what the broken
199
+ prefix costs you (full recipe, assumptions, and error bounds:
200
+ [`docs/cost-model.md`](docs/cost-model.md)). Per finding, `cachelint lint`
201
+ prints:
202
+
203
+ ```
204
+ prompts/random.md:1:13 warning R002 `crypto.randomUUID()` in prompt content — a fresh UUID every call
205
+ › Session id: crypto.randomUUID()
206
+ fix: Generate the id outside the cached prefix (or after the last breakpoint).
207
+ why: https://github.com/davioe/cachelint/blob/main/docs/rules/R002.md
208
+ → invalidates ~8 stable prefix tokens from here
209
+ ≈ $0.022 wasted per 1,000 calls (est. if caching is enabled, claude-sonnet-4-6 pricing, 2026-06)
210
+ ```
211
+
212
+ …and a run summary:
213
+
214
+ ```
215
+ cachelint: 5 problems (2 errors, 3 warnings)
216
+ cachelint: est. waste ≈ $0.068 per 1,000 calls · ~19 prefix tokens at risk (est. if caching is enabled, claude-sonnet-4-6 pricing, 2026-06)
217
+ ```
218
+
219
+ A failing `cachelint check` prints the waste/at-risk totals before its
220
+ verdict (and a *green* check prints a value receipt of what the stable
221
+ prefixes protect):
222
+
223
+ ```
224
+ cachelint: cachelint.lock is out of date —
225
+ ~ bundle "prompts/a.md" stablePrefixHash moved:
226
+ was: 70a6822d8636ba2c31494cda7700b158cf426837df5fe6515ba228d817615f15
227
+ now: ccbaa0ede490b50c246fb4f98e227968653eff0f5a8f666d1fba78ae6cb33d87
228
+ prompts/a.md: 6cdf29393117 → 6a02769c47b4
229
+
230
+ If this change is intentional: run `cachelint hash …` and commit the updated cachelint.lock.
231
+ prompts/a.md:2:13 warning R001 template helper injects the current date/time into the prompt
232
+ why: https://github.com/davioe/cachelint/blob/main/docs/rules/R001.md
233
+ → invalidates ~3 stable prefix tokens from here
234
+ prompts/b.md:1:1 error R004 file begins with a UTF-8 BOM (U+FEFF) — an invisible character at the head of the prompt
235
+ why: https://github.com/davioe/cachelint/blob/main/docs/rules/R004.md
236
+ ~4 prefix tokens at risk (byte-stable — changes on edit/regeneration, not per call)
237
+ cachelint: est. waste ~3 stable prefix tokens per call (under $0.01 per 1,000 calls) · ~4 prefix tokens at risk
238
+ cachelint: FAILED — stable-prefix hash moved; 1 error-severity finding
239
+ ```
240
+
241
+ Cost output is cosmetic — it never affects exit codes, hashing, or the
242
+ lockfile — and `--no-cost` removes it from every surface (human, `--json`,
243
+ CI summary). See [`docs/cost-model.md`](docs/cost-model.md) for why the
244
+ numbers are defensible: volatility classes, the earliest-divergence model,
245
+ and every conservatism guarantee.
246
+
247
+ ### Exit codes (v1.0 stability contract)
248
+
249
+ | code | meaning |
250
+ | ---- | ------- |
251
+ | `0` | ok (or only `warn`-severity findings without `--strict`) |
252
+ | `1` | a `stablePrefixHash` moved (regression), or an `error`-severity rule fired (or a `warn` under `--strict`) |
253
+ | `2` | a Pro feature (`--sarif`, `--exact`, `--pack`) was invoked without a license |
254
+ | `3` | `cachelint.lock` is missing or corrupt |
255
+ | `4` | config or source error |
256
+
257
+ ## OSS vs Pro
258
+
259
+ | | Free (MIT) | Pro |
260
+ | --- | --- | --- |
261
+ | `cachelint lint` (all rules, pragmas, `--json`) | ✅ | ✅ |
262
+ | `cachelint hash` / `check` — single **and** multi-file bundles | ✅ | ✅ |
263
+ | The GitHub Action | ✅ | ✅ |
264
+ | `--sarif` PR annotations (GitHub Code Scanning¹) | — | ✅ |
265
+ | `--exact` token counting (Anthropic count-tokens API, your key) | — | ✅ |
266
+ | Provider bug-pattern packs (`--pack anthropic-2026-q1`) | — | ✅ |
267
+
268
+ ¹ Uploading SARIF to a **private** repo requires GitHub Advanced Security; on
269
+ public repos it works on the free plan. Disclosed in [`LICENSE-PRO.md`](LICENSE-PRO.md).
270
+
271
+ Pro is a one-time individual / annual team license. Keys are **offline-verifiable**
272
+ (Ed25519 via `node:crypto`) — no servers, no telemetry, works in air-gapped CI.
273
+ `cachelint activate <key>` to enable, `cachelint license status` to inspect,
274
+ `cachelint deactivate` to remove; `CACHELINT_LICENSE_KEY` env for CI. The buyer's
275
+ email is embedded in the key and printed by `cachelint --version` — the only
276
+ anti-share signal, disclosed at checkout. See [`docs/licensing.md`](docs/licensing.md).
277
+
278
+ ## Design contract
279
+
280
+ - **Zero operating cost.** No servers. No paid APIs (`--exact` uses *your*
281
+ `ANTHROPIC_API_KEY`). No telemetry, ever.
282
+ - **Determinism.** `cachelint.lock` is byte-identical across Windows / macOS /
283
+ Linux and Node 22/24. Full spec: [`docs/determinism-contract.md`](docs/determinism-contract.md).
284
+ - **Config is data, not code.** `.json` / `.yaml` only — no `.ts`/`.js` config,
285
+ so a shared config can't execute anything.
286
+ - **Rules are pure functions** (no I/O, no network) — enforced by a sandbox
287
+ harness, so future community rule packs are safe by construction.
288
+ - **No secret ever leaks.** API keys and license tokens are redacted in every
289
+ output path, including `--verbose`.
290
+
291
+ ## License
292
+
293
+ Free core: **MIT** ([LICENSE](LICENSE)). Pro features: [LICENSE-PRO.md](LICENSE-PRO.md).
294
+
295
+ ## Contributing
296
+
297
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Implementation plan:
298
+ [`docs/plans/2026-05-11-002-feat-cachelint-mvp-plan.md`](docs/plans/2026-05-11-002-feat-cachelint-mvp-plan.md).
@@ -0,0 +1,23 @@
1
+ import {
2
+ check,
3
+ renderCheckHuman,
4
+ renderCheckJson
5
+ } from "./chunk-ZSEZQ422.js";
6
+ import "./chunk-UUVSBJ62.js";
7
+ import "./chunk-2JSY5BQA.js";
8
+ import "./chunk-O7ZE6CFI.js";
9
+ import "./chunk-GXASKZ4Z.js";
10
+ import "./chunk-4LB5LF2P.js";
11
+ import "./chunk-WKFGRNYK.js";
12
+ import "./chunk-DYQPXD3G.js";
13
+ import "./chunk-VWE5TDL5.js";
14
+ import "./chunk-BUZCVKMZ.js";
15
+ import "./chunk-MW7BXKPR.js";
16
+ import "./chunk-4GHSUBAY.js";
17
+ import "./chunk-WPJYUQMU.js";
18
+ import "./chunk-XKRPGMZT.js";
19
+ export {
20
+ check,
21
+ renderCheckHuman,
22
+ renderCheckJson
23
+ };