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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// src/exit-codes.ts
|
|
2
|
+
var EXIT_OK = 0;
|
|
3
|
+
var EXIT_CHECK_FAILED = 1;
|
|
4
|
+
var EXIT_PRO_REQUIRED = 2;
|
|
5
|
+
var EXIT_LOCK_PROBLEM = 3;
|
|
6
|
+
var EXIT_CONFIG_OR_SOURCE_ERROR = 4;
|
|
7
|
+
|
|
8
|
+
// src/errors.ts
|
|
9
|
+
var CachelintError = class extends Error {
|
|
10
|
+
/** Stable string code (e.g. `"CFG_INVALID"`) — safe to log and to grep. */
|
|
11
|
+
code;
|
|
12
|
+
/** Suggested exit code if this error propagates to the CLI's top-level. */
|
|
13
|
+
exitCode;
|
|
14
|
+
/** Optional one-line hint to print after the message. */
|
|
15
|
+
hint;
|
|
16
|
+
constructor(args) {
|
|
17
|
+
super(args.message);
|
|
18
|
+
this.name = this.constructor.name;
|
|
19
|
+
this.code = args.code;
|
|
20
|
+
this.exitCode = args.exitCode;
|
|
21
|
+
if (args.hint !== void 0) this.hint = args.hint;
|
|
22
|
+
if (args.cause !== void 0) this.cause = args.cause;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var ConfigError = class extends CachelintError {
|
|
26
|
+
/** Optional file path the error was found in. */
|
|
27
|
+
file;
|
|
28
|
+
/** Optional "key.path" inside the config that failed. */
|
|
29
|
+
keyPath;
|
|
30
|
+
constructor(args) {
|
|
31
|
+
super({
|
|
32
|
+
code: args.code ?? "CFG_INVALID",
|
|
33
|
+
message: args.message,
|
|
34
|
+
exitCode: EXIT_CONFIG_OR_SOURCE_ERROR,
|
|
35
|
+
...args.hint !== void 0 ? { hint: args.hint } : {},
|
|
36
|
+
...args.cause !== void 0 ? { cause: args.cause } : {}
|
|
37
|
+
});
|
|
38
|
+
if (args.file !== void 0) this.file = args.file;
|
|
39
|
+
if (args.keyPath !== void 0) this.keyPath = args.keyPath;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var SourceError = class extends CachelintError {
|
|
43
|
+
path;
|
|
44
|
+
constructor(args) {
|
|
45
|
+
super({
|
|
46
|
+
code: args.code ?? "SRC_INVALID",
|
|
47
|
+
message: args.message,
|
|
48
|
+
exitCode: EXIT_CONFIG_OR_SOURCE_ERROR,
|
|
49
|
+
...args.hint !== void 0 ? { hint: args.hint } : {},
|
|
50
|
+
...args.cause !== void 0 ? { cause: args.cause } : {}
|
|
51
|
+
});
|
|
52
|
+
if (args.path !== void 0) this.path = args.path;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var RuleError = class extends CachelintError {
|
|
56
|
+
ruleId;
|
|
57
|
+
constructor(args) {
|
|
58
|
+
super({
|
|
59
|
+
code: args.code ?? "RULE_INVALID",
|
|
60
|
+
message: args.message,
|
|
61
|
+
exitCode: EXIT_CONFIG_OR_SOURCE_ERROR,
|
|
62
|
+
...args.hint !== void 0 ? { hint: args.hint } : {},
|
|
63
|
+
...args.cause !== void 0 ? { cause: args.cause } : {}
|
|
64
|
+
});
|
|
65
|
+
if (args.ruleId !== void 0) this.ruleId = args.ruleId;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var LockError = class extends CachelintError {
|
|
69
|
+
constructor(args) {
|
|
70
|
+
super({
|
|
71
|
+
code: args.code ?? "LOCK_INVALID",
|
|
72
|
+
message: args.message,
|
|
73
|
+
exitCode: EXIT_LOCK_PROBLEM,
|
|
74
|
+
...args.hint !== void 0 ? { hint: args.hint } : {},
|
|
75
|
+
...args.cause !== void 0 ? { cause: args.cause } : {}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var LockMissingError = class extends LockError {
|
|
80
|
+
constructor(path) {
|
|
81
|
+
super({
|
|
82
|
+
code: "LOCK_MISSING",
|
|
83
|
+
message: `cachelint.lock not found at ${path}`,
|
|
84
|
+
hint: "Run `cachelint hash <paths>` once and commit the resulting cachelint.lock."
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var LockCorruptError = class extends LockError {
|
|
89
|
+
constructor(message, hint) {
|
|
90
|
+
super({ code: "LOCK_CORRUPT", message, ...hint !== void 0 ? { hint } : {} });
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var LicenseError = class extends CachelintError {
|
|
94
|
+
constructor(args) {
|
|
95
|
+
super({
|
|
96
|
+
code: args.code ?? "PRO_REQUIRED",
|
|
97
|
+
message: args.message,
|
|
98
|
+
exitCode: EXIT_PRO_REQUIRED,
|
|
99
|
+
...args.hint !== void 0 ? { hint: args.hint } : {}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var ExactCountUnavailableError = class extends CachelintError {
|
|
104
|
+
constructor(message, hint) {
|
|
105
|
+
super({
|
|
106
|
+
code: "EXACT_UNAVAILABLE",
|
|
107
|
+
message,
|
|
108
|
+
exitCode: EXIT_CONFIG_OR_SOURCE_ERROR,
|
|
109
|
+
...hint !== void 0 ? { hint } : {}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var AdapterError = class extends CachelintError {
|
|
114
|
+
constructor(args) {
|
|
115
|
+
super({
|
|
116
|
+
code: args.code ?? "ADAPTER_INVALID",
|
|
117
|
+
message: args.message,
|
|
118
|
+
exitCode: EXIT_CONFIG_OR_SOURCE_ERROR,
|
|
119
|
+
...args.hint !== void 0 ? { hint: args.hint } : {},
|
|
120
|
+
...args.cause !== void 0 ? { cause: args.cause } : {}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
var COST_UNAVAILABLE_CODE = "COST_UNAVAILABLE";
|
|
125
|
+
function costUnavailableWarning(err) {
|
|
126
|
+
return {
|
|
127
|
+
code: COST_UNAVAILABLE_CODE,
|
|
128
|
+
message: `cost estimation failed (${err instanceof Error ? err.message : String(err)}) \u2014 continuing without cost data`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function diagnosticsExitCode(diagnostics, opts = {}) {
|
|
132
|
+
const strict = opts.strict === true;
|
|
133
|
+
for (const d of diagnostics) {
|
|
134
|
+
if (d.severity === "error") return EXIT_CHECK_FAILED;
|
|
135
|
+
if (strict && d.severity === "warn") return EXIT_CHECK_FAILED;
|
|
136
|
+
}
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
function redactSecrets(text) {
|
|
140
|
+
if (typeof text !== "string") return text;
|
|
141
|
+
let out = text;
|
|
142
|
+
out = out.replace(/sk-ant-[A-Za-z0-9_-]{10,}/g, "<redacted>");
|
|
143
|
+
out = out.replace(/sk-(?:proj-)?[A-Za-z0-9_-]{10,}/g, "<redacted>");
|
|
144
|
+
out = out.replace(/Bearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer <redacted>");
|
|
145
|
+
out = out.replace(/(x-api-key[\s:=]+)["']?[A-Za-z0-9._~+/=-]{8,}["']?/gi, "$1<redacted>");
|
|
146
|
+
out = out.replace(/(Authorization[\s:=]+)["']?[^"'\s,;]{8,}["']?/gi, "$1<redacted>");
|
|
147
|
+
out = out.replace(/\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, "<redacted-token>");
|
|
148
|
+
out = out.replace(
|
|
149
|
+
/\b(ANTHROPIC_API_KEY|OPENAI_API_KEY|CACHELINT_LICENSE_KEY)\b\s*[=:]\s*["']?[^"'\s,;]+["']?/g,
|
|
150
|
+
"$1=<redacted>"
|
|
151
|
+
);
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function renderError(err, opts = {}) {
|
|
155
|
+
const verbose = opts.verbose === true;
|
|
156
|
+
if (err instanceof CachelintError) {
|
|
157
|
+
const lines = [];
|
|
158
|
+
lines.push(`cachelint: ${err.message} [${err.code}]`);
|
|
159
|
+
if (err.hint) lines.push(` hint: ${err.hint}`);
|
|
160
|
+
if (verbose && err.stack) lines.push(err.stack);
|
|
161
|
+
return redactSecrets(lines.join("\n"));
|
|
162
|
+
}
|
|
163
|
+
if (err instanceof Error) {
|
|
164
|
+
if (verbose && err.stack) return redactSecrets(`cachelint: unexpected error
|
|
165
|
+
${err.stack}`);
|
|
166
|
+
return redactSecrets(`cachelint: unexpected error: ${err.message}`);
|
|
167
|
+
}
|
|
168
|
+
return redactSecrets(`cachelint: unexpected error: ${String(err)}`);
|
|
169
|
+
}
|
|
170
|
+
function exitCodeFor(err) {
|
|
171
|
+
if (err instanceof CachelintError) return err.exitCode;
|
|
172
|
+
return EXIT_CONFIG_OR_SOURCE_ERROR;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export {
|
|
176
|
+
EXIT_OK,
|
|
177
|
+
EXIT_CHECK_FAILED,
|
|
178
|
+
EXIT_PRO_REQUIRED,
|
|
179
|
+
EXIT_LOCK_PROBLEM,
|
|
180
|
+
EXIT_CONFIG_OR_SOURCE_ERROR,
|
|
181
|
+
CachelintError,
|
|
182
|
+
ConfigError,
|
|
183
|
+
SourceError,
|
|
184
|
+
RuleError,
|
|
185
|
+
LockError,
|
|
186
|
+
LockMissingError,
|
|
187
|
+
LockCorruptError,
|
|
188
|
+
LicenseError,
|
|
189
|
+
ExactCountUnavailableError,
|
|
190
|
+
AdapterError,
|
|
191
|
+
COST_UNAVAILABLE_CODE,
|
|
192
|
+
costUnavailableWarning,
|
|
193
|
+
diagnosticsExitCode,
|
|
194
|
+
redactSecrets,
|
|
195
|
+
renderError,
|
|
196
|
+
exitCodeFor
|
|
197
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_EXACT_LIMIT,
|
|
3
|
+
DEFAULT_EXACT_MODEL,
|
|
4
|
+
countTokensExact
|
|
5
|
+
} from "./chunk-UUVSBJ62.js";
|
|
6
|
+
import {
|
|
7
|
+
heuristicTokens,
|
|
8
|
+
lint,
|
|
9
|
+
usdPer1kCalls
|
|
10
|
+
} from "./chunk-2JSY5BQA.js";
|
|
11
|
+
import {
|
|
12
|
+
getModelPricing,
|
|
13
|
+
referencePricing
|
|
14
|
+
} from "./chunk-O7ZE6CFI.js";
|
|
15
|
+
import {
|
|
16
|
+
requirePro
|
|
17
|
+
} from "./chunk-GXASKZ4Z.js";
|
|
18
|
+
import {
|
|
19
|
+
diffLockfiles,
|
|
20
|
+
isRegression,
|
|
21
|
+
renderDiffHuman
|
|
22
|
+
} from "./chunk-DYQPXD3G.js";
|
|
23
|
+
import {
|
|
24
|
+
COST_LINE_OVERLAPPED,
|
|
25
|
+
RECEIPT_EXACT_FALLBACK_LINE,
|
|
26
|
+
buildReceiptLines,
|
|
27
|
+
costLabel,
|
|
28
|
+
costLineAtRisk,
|
|
29
|
+
costLineInvalidates,
|
|
30
|
+
costLineWasted,
|
|
31
|
+
costSummary,
|
|
32
|
+
formatUsdPer1k
|
|
33
|
+
} from "./chunk-VWE5TDL5.js";
|
|
34
|
+
import {
|
|
35
|
+
DEFAULT_LOCKFILE_NAME,
|
|
36
|
+
computeBundles,
|
|
37
|
+
makeLockfile,
|
|
38
|
+
readLockfileIfExists
|
|
39
|
+
} from "./chunk-BUZCVKMZ.js";
|
|
40
|
+
import {
|
|
41
|
+
resolveConfigArg,
|
|
42
|
+
resolveSources,
|
|
43
|
+
toPosixRelative
|
|
44
|
+
} from "./chunk-MW7BXKPR.js";
|
|
45
|
+
import {
|
|
46
|
+
CACHELINT_VERSION
|
|
47
|
+
} from "./chunk-WPJYUQMU.js";
|
|
48
|
+
import {
|
|
49
|
+
COST_UNAVAILABLE_CODE,
|
|
50
|
+
LockMissingError,
|
|
51
|
+
costUnavailableWarning,
|
|
52
|
+
redactSecrets
|
|
53
|
+
} from "./chunk-XKRPGMZT.js";
|
|
54
|
+
|
|
55
|
+
// src/commands/check.ts
|
|
56
|
+
import { resolve as pathResolve } from "path";
|
|
57
|
+
async function check(args = {}) {
|
|
58
|
+
const root = pathResolve(args.projectRoot ?? process.cwd());
|
|
59
|
+
const { config, configPath } = resolveConfigArg(args.config, root);
|
|
60
|
+
const { bundles, warnings } = computeBundles({
|
|
61
|
+
...args.paths !== void 0 ? { paths: args.paths } : {},
|
|
62
|
+
config,
|
|
63
|
+
projectRoot: root
|
|
64
|
+
});
|
|
65
|
+
const lockPath = pathResolve(root, args.lockfile ?? DEFAULT_LOCKFILE_NAME);
|
|
66
|
+
const committed = readLockfileIfExists(lockPath);
|
|
67
|
+
if (committed === null) throw new LockMissingError(args.lockfile ?? DEFAULT_LOCKFILE_NAME);
|
|
68
|
+
const current = makeLockfile(bundles);
|
|
69
|
+
const diff = diffLockfiles(committed, current);
|
|
70
|
+
const regression = isRegression(diff);
|
|
71
|
+
const allFiles = [...new Set(bundles.flatMap((b) => b.files.map((f) => f.path)))].sort();
|
|
72
|
+
const lintResult = lint({
|
|
73
|
+
paths: allFiles,
|
|
74
|
+
config,
|
|
75
|
+
projectRoot: root,
|
|
76
|
+
...args.strict !== void 0 ? { strict: args.strict } : {},
|
|
77
|
+
...args.pack !== void 0 ? { pack: args.pack } : {},
|
|
78
|
+
// R7: cost is forwarded but never recomputed — check reuses the embedded
|
|
79
|
+
// lint call's resolution/cost data (no second resolution pass).
|
|
80
|
+
...args.cost !== void 0 ? { cost: args.cost } : {}
|
|
81
|
+
});
|
|
82
|
+
const diagnostics = lintResult.diagnostics;
|
|
83
|
+
let exactCounts = null;
|
|
84
|
+
if (args.exact === true) {
|
|
85
|
+
requirePro("exact", "`--exact` token counting");
|
|
86
|
+
const model = args.exactModel ?? DEFAULT_EXACT_MODEL;
|
|
87
|
+
const resolved = resolveSources({
|
|
88
|
+
paths: allFiles,
|
|
89
|
+
projectRoot: root,
|
|
90
|
+
required: false,
|
|
91
|
+
...config.exclude !== void 0 ? { exclude: config.exclude } : {},
|
|
92
|
+
...config.max_file_bytes !== void 0 ? { maxFileBytes: config.max_file_bytes } : {},
|
|
93
|
+
...config.max_total_bytes !== void 0 ? { maxTotalBytes: config.max_total_bytes } : {}
|
|
94
|
+
});
|
|
95
|
+
const textBySha = new Map(resolved.sources.map((s) => [s.sha256, s.normalized]));
|
|
96
|
+
const seen = /* @__PURE__ */ new Set();
|
|
97
|
+
const requests = [];
|
|
98
|
+
for (const b of current.bundles) {
|
|
99
|
+
for (const f of b.files) {
|
|
100
|
+
if (seen.has(f.sha256)) continue;
|
|
101
|
+
seen.add(f.sha256);
|
|
102
|
+
const text = textBySha.get(f.sha256);
|
|
103
|
+
if (text === void 0) {
|
|
104
|
+
warnings.push({ code: "EXACT_SHA_MISMATCH", message: `--exact: skipping (content changed during the run)`, path: f.path });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
requests.push({ sha256: f.sha256, model, text });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exactCounts = await countTokensExact(requests, args.exactLimit ?? DEFAULT_EXACT_LIMIT);
|
|
111
|
+
}
|
|
112
|
+
const exitCode = regression || lintResult.exitCode === 1 ? 1 : 0;
|
|
113
|
+
let cost;
|
|
114
|
+
if (args.cost !== false) {
|
|
115
|
+
if (lintResult.cost === void 0) {
|
|
116
|
+
for (const w of lintResult.warnings) {
|
|
117
|
+
if (w.code === COST_UNAVAILABLE_CODE) warnings.push(w);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
try {
|
|
121
|
+
cost = {
|
|
122
|
+
...lintResult.cost,
|
|
123
|
+
receipt: computeReceipt(bundles, lintResult.cost.normalizedCharsByFile, exactCounts, args.exactModel)
|
|
124
|
+
};
|
|
125
|
+
} catch (err) {
|
|
126
|
+
warnings.push(costUnavailableWarning(err));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
version: CACHELINT_VERSION,
|
|
132
|
+
configPath,
|
|
133
|
+
packLoaded: lintResult.packLoaded,
|
|
134
|
+
lockfilePath: lockPath,
|
|
135
|
+
diff,
|
|
136
|
+
regression,
|
|
137
|
+
diagnostics,
|
|
138
|
+
exactCounts,
|
|
139
|
+
warnings,
|
|
140
|
+
exitCode,
|
|
141
|
+
...cost !== void 0 ? { cost } : {}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function computeReceipt(bundles, normalizedCharsByFile, exactCounts, exactModelArg) {
|
|
145
|
+
const ref = referencePricing();
|
|
146
|
+
let engagementMinimumTokens = ref.minCacheablePrefixTokens;
|
|
147
|
+
let engagementMinimumSource = ref.model;
|
|
148
|
+
if (exactModelArg !== void 0) {
|
|
149
|
+
const named = getModelPricing(exactModelArg);
|
|
150
|
+
if (named !== null && named.minCacheablePrefixTokens > engagementMinimumTokens) {
|
|
151
|
+
engagementMinimumTokens = named.minCacheablePrefixTokens;
|
|
152
|
+
engagementMinimumSource = named.model;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const exactBySha = exactCounts === null ? null : new Map(exactCounts.map((e) => [e.sha256, e.tokens]));
|
|
156
|
+
const entries = [];
|
|
157
|
+
let exactRefined = false;
|
|
158
|
+
let anyFallback = false;
|
|
159
|
+
for (const b of bundles) {
|
|
160
|
+
let heuristicSum = 0;
|
|
161
|
+
let exactSum = 0;
|
|
162
|
+
let fullyExact = exactBySha !== null;
|
|
163
|
+
for (const f of b.files) {
|
|
164
|
+
const chars = normalizedCharsByFile[f.path];
|
|
165
|
+
if (chars === void 0) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`cost estimate unavailable: ${f.path} is in the bundle but was not part of source resolution (binary, oversize, or excluded)`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
heuristicSum += heuristicTokens(chars);
|
|
171
|
+
const exact = exactBySha?.get(f.sha256);
|
|
172
|
+
if (exact === void 0) fullyExact = false;
|
|
173
|
+
else exactSum += exact;
|
|
174
|
+
}
|
|
175
|
+
const useExact = exactBySha !== null && fullyExact;
|
|
176
|
+
if (exactBySha !== null) {
|
|
177
|
+
if (useExact) exactRefined = true;
|
|
178
|
+
else anyFallback = true;
|
|
179
|
+
}
|
|
180
|
+
const protectedTokens = useExact ? exactSum : heuristicSum;
|
|
181
|
+
const engaged = protectedTokens >= engagementMinimumTokens;
|
|
182
|
+
entries.push({
|
|
183
|
+
name: b.name,
|
|
184
|
+
protectedTokens,
|
|
185
|
+
tokenSource: useExact ? "exact" : "heuristic",
|
|
186
|
+
engaged,
|
|
187
|
+
...engaged ? { protectedUsdPer1kCalls: usdPer1kCalls(protectedTokens, ref) } : {}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
let engagedTokens = 0;
|
|
191
|
+
for (const e of entries) {
|
|
192
|
+
if (e.engaged) engagedTokens += e.protectedTokens;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
bundles: entries,
|
|
196
|
+
protectedTokens: engagedTokens,
|
|
197
|
+
protectedUsdPer1kCalls: usdPer1kCalls(engagedTokens, ref),
|
|
198
|
+
engagementMinimumTokens,
|
|
199
|
+
engagementMinimumSource,
|
|
200
|
+
exactRefined,
|
|
201
|
+
...anyFallback ? { exactFallbackReason: RECEIPT_EXACT_FALLBACK_LINE } : {}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function renderCheckHuman(r) {
|
|
205
|
+
const lines = [];
|
|
206
|
+
if (r.packLoaded !== null) lines.push(`note pack "${r.packLoaded}" loaded (no additional rules in this build yet)`);
|
|
207
|
+
for (const w of r.warnings) lines.push(`note ${w.path ? w.path + ": " : ""}${w.message}`);
|
|
208
|
+
if (r.regression) {
|
|
209
|
+
lines.push("cachelint: cachelint.lock is out of date \u2014");
|
|
210
|
+
lines.push(renderDiffHuman(r.diff));
|
|
211
|
+
}
|
|
212
|
+
const label = r.cost !== void 0 ? costLabel({ model: r.cost.pricing.referenceModel }, r.cost.pricing.asOf) : "";
|
|
213
|
+
let errors = 0;
|
|
214
|
+
let warns = 0;
|
|
215
|
+
for (let i = 0; i < r.diagnostics.length; i++) {
|
|
216
|
+
const d = r.diagnostics[i];
|
|
217
|
+
if (d.severity === "error") errors++;
|
|
218
|
+
else warns++;
|
|
219
|
+
const level = d.severity === "error" ? "error " : "warning";
|
|
220
|
+
lines.push(`${d.file}:${d.line}:${d.column} ${level} ${d.rule} ${d.message}`);
|
|
221
|
+
if (d.whyUrl) lines.push(` why: ${d.whyUrl}`);
|
|
222
|
+
const dc = r.cost?.perDiagnostic[i] ?? null;
|
|
223
|
+
if (dc !== null) {
|
|
224
|
+
if (!dc.countsTowardTotal) {
|
|
225
|
+
lines.push(` ${COST_LINE_OVERLAPPED}`);
|
|
226
|
+
} else if (dc.volatility === "per-call") {
|
|
227
|
+
lines.push(` ${costLineInvalidates(dc.fromHereTokens)}`);
|
|
228
|
+
const usd = dc.wastedUsdPer1kCalls !== void 0 ? formatUsdPer1k(dc.wastedUsdPer1kCalls) : null;
|
|
229
|
+
if (usd !== null) lines.push(` ${costLineWasted(usd, label)}`);
|
|
230
|
+
} else {
|
|
231
|
+
lines.push(` ${costLineAtRisk(dc.fromHereTokens)}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (r.exactCounts) {
|
|
236
|
+
for (const e of r.exactCounts) lines.push(`tokens ${e.sha256.slice(0, 12)} ${e.tokens} (${e.model})`);
|
|
237
|
+
}
|
|
238
|
+
if (r.exitCode === 0) {
|
|
239
|
+
lines.push(errors + warns > 0 ? `cachelint: lock OK; ${warns} warning${warns === 1 ? "" : "s"} (non-blocking)` : "cachelint: lock OK, no problems found.");
|
|
240
|
+
if (r.cost !== void 0) pushReceiptLines(lines, r.cost, label);
|
|
241
|
+
} else {
|
|
242
|
+
if (r.cost !== void 0) {
|
|
243
|
+
const summary = costSummary(r.cost.totals, label);
|
|
244
|
+
if (summary !== null) lines.push(`cachelint: ${summary}`);
|
|
245
|
+
}
|
|
246
|
+
const bits = [];
|
|
247
|
+
if (r.regression) bits.push("stable-prefix hash moved");
|
|
248
|
+
if (errors > 0) bits.push(`${errors} error-severity finding${errors === 1 ? "" : "s"}`);
|
|
249
|
+
if (!r.regression && errors === 0 && warns > 0) bits.push(`${warns} warning${warns === 1 ? "" : "s"} (elevated by --strict)`);
|
|
250
|
+
lines.push(`cachelint: FAILED \u2014 ${bits.length > 0 ? bits.join("; ") : "see above"}`);
|
|
251
|
+
}
|
|
252
|
+
return redactSecrets(lines.join("\n"));
|
|
253
|
+
}
|
|
254
|
+
function pushReceiptLines(lines, cost, label) {
|
|
255
|
+
for (const l of buildReceiptLines(cost, label)) lines.push(`cachelint: ${l}`);
|
|
256
|
+
}
|
|
257
|
+
function renderCheckJson(r, projectRoot = process.cwd()) {
|
|
258
|
+
return redactSecrets(
|
|
259
|
+
JSON.stringify(
|
|
260
|
+
{
|
|
261
|
+
version: r.version,
|
|
262
|
+
configPath: r.configPath,
|
|
263
|
+
packLoaded: r.packLoaded,
|
|
264
|
+
lockfilePath: toPosixRelative(r.lockfilePath, pathResolve(projectRoot)),
|
|
265
|
+
exitCode: r.exitCode,
|
|
266
|
+
regression: r.regression,
|
|
267
|
+
diff: r.diff,
|
|
268
|
+
diagnostics: r.diagnostics,
|
|
269
|
+
...r.exactCounts ? { exactCounts: r.exactCounts } : {},
|
|
270
|
+
warnings: r.warnings,
|
|
271
|
+
// R4: present only when cost was computed — `--no-cost` (and
|
|
272
|
+
// degradation) omit the block by construction. The shared shape
|
|
273
|
+
// mirrors lint's cost block exactly; `receipt` is check-specific.
|
|
274
|
+
...r.cost !== void 0 ? { cost: r.cost } : {}
|
|
275
|
+
},
|
|
276
|
+
null,
|
|
277
|
+
2
|
|
278
|
+
)
|
|
279
|
+
) + "\n";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export {
|
|
283
|
+
check,
|
|
284
|
+
renderCheckHuman,
|
|
285
|
+
renderCheckJson
|
|
286
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* v1.0 stability contract for cachelint's process exit codes.
|
|
4
|
+
*
|
|
5
|
+
* Imported by both the CLI (src/cli.ts) and the error taxonomy (src/errors.ts),
|
|
6
|
+
* which is why it lives in its own module — importing cli.ts has side effects
|
|
7
|
+
* (it calls `program.parseAsync`), and we want errors.ts to be safe to import
|
|
8
|
+
* anywhere.
|
|
9
|
+
*
|
|
10
|
+
* The table:
|
|
11
|
+
* 0 ok
|
|
12
|
+
* 1 hash moved / error-severity rule fired (real regression)
|
|
13
|
+
* 2 Pro feature invoked without a license (entitlement)
|
|
14
|
+
* 3 cachelint.lock missing or corrupt
|
|
15
|
+
* 4 config / source error (incl. missing ANTHROPIC_API_KEY when --exact IS licensed)
|
|
16
|
+
*/
|
|
17
|
+
declare const EXIT_OK = 0;
|
|
18
|
+
declare const EXIT_CHECK_FAILED = 1;
|
|
19
|
+
declare const EXIT_PRO_REQUIRED = 2;
|
|
20
|
+
declare const EXIT_LOCK_PROBLEM = 3;
|
|
21
|
+
declare const EXIT_CONFIG_OR_SOURCE_ERROR = 4;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* cachelint — CLI entry point.
|
|
25
|
+
*
|
|
26
|
+
* Unit 1 ships stub subcommands so the bin is real and `--help` / `--version`
|
|
27
|
+
* are instant; Units 2–4 fill in the actual behavior.
|
|
28
|
+
*
|
|
29
|
+
* Design notes:
|
|
30
|
+
* - Heavy modules (rules engine, lockfile, license verify) are lazy-imported
|
|
31
|
+
* INSIDE each action so cold-start for `--help` / `--version` stays fast.
|
|
32
|
+
* - Exit codes are pinned (see Requirements R4 in the plan):
|
|
33
|
+
* 0 ok or only warns without --strict
|
|
34
|
+
* 1 hash moved / error-severity rule fired (real regression)
|
|
35
|
+
* 2 Pro feature invoked without a license (entitlement)
|
|
36
|
+
* 3 cachelint.lock missing or corrupt
|
|
37
|
+
* 4 config or source error (incl. missing ANTHROPIC_API_KEY when --exact IS licensed)
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run the CLI. Extracted so the module can be imported in tests without
|
|
42
|
+
* arg parsing as a side effect.
|
|
43
|
+
*/
|
|
44
|
+
declare function runCli(argv?: readonly string[]): Promise<void>;
|
|
45
|
+
|
|
46
|
+
export { EXIT_CHECK_FAILED, EXIT_CONFIG_OR_SOURCE_ERROR, EXIT_LOCK_PROBLEM, EXIT_OK, EXIT_PRO_REQUIRED, runCli };
|