patchling 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Fresh in-memory cache. Pass one instance as `cache` to share it across calls.
3
+ * @returns {RelevanceCache}
4
+ */
5
+ export function createMemoryCache(): RelevanceCache;
6
+ /**
7
+ * Parse a yes/no relevance answer.
8
+ *
9
+ * Think tags, one fenced block, and chain-of-thought reasoning are removed
10
+ * first. Whitespace, quotes, markdown bold or backticks, and one `Answer:`
11
+ * or `Decision:` prefix are wrappers. Wrappers may sit on the decision word
12
+ * itself when a reason follows (`**no** unrelated`, `"no" unrelated`). The
13
+ * first token must then be exactly `yes` or `no`, and the next character
14
+ * must be the end of the string, whitespace, or one of `. , ! : ; )`.
15
+ * Anything else is malformed (`null`); callers include the file and do not
16
+ * cache it.
17
+ *
18
+ * @param {unknown} raw
19
+ * @returns {{ decision: 'yes' | 'no' | null, cleaned: string }}
20
+ */
21
+ export function parseRelevanceAnswer(raw: unknown): {
22
+ decision: "yes" | "no" | null;
23
+ cleaned: string;
24
+ };
25
+ /**
26
+ * Code-point stand-in for a tokenizer. Empty text is 0. Otherwise
27
+ * `ceil(codePoints / 4)`. A surrogate pair counts as one code point.
28
+ * @param {string} text
29
+ * @returns {number}
30
+ */
31
+ export function countTokens(text: string): number;
32
+ /**
33
+ * True when `content` looks like a binary file (NUL, invalid UTF-8, or a
34
+ * dense run of non-text controls). The whole payload is validated. An
35
+ * incomplete trailing sequence is tolerated only when it ends on the
36
+ * 8192-byte probe boundary. Non-text values other than byte arrays are not
37
+ * binary; {@link filterRelevant} rejects them when it coerces the file.
38
+ * @param {unknown} content
39
+ * @returns {boolean}
40
+ */
41
+ export function isBinaryContent(content: unknown): boolean;
42
+ /**
43
+ * @param {string} filePath
44
+ * @param {string[]} patterns
45
+ * @returns {boolean}
46
+ */
47
+ export function isIgnoredPath(filePath: string, patterns?: string[]): boolean;
48
+ /**
49
+ * Ask a cheap model which files are relevant to `task`.
50
+ *
51
+ * `files` is a `{ path: content }` map (string or byte array). The returned
52
+ * `files` map contains only the `yes` files, in input order, with their
53
+ * original contents — a truncated preview is what the model saw, not what is
54
+ * forwarded. `decisions` has one entry per input file.
55
+ *
56
+ * `cache` is a {@link RelevanceCache}, a filesystem path (memory plus that
57
+ * JSON file), `true` or omitted (a fresh in-memory cache for this call), or
58
+ * `null` / `false` (disabled). The cache key hashes the prompt version, model,
59
+ * output-token limit, the reasoning effort actually sent, task, path, mode,
60
+ * preview note, and the body that was sent. A cache read failure is a miss.
61
+ * A cache write failure is logged and the run continues. Each path is
62
+ * classified on its own, including paths whose contents match.
63
+ *
64
+ * Binary files and files over `maxBytes` / `maxTokens` are not sent in full.
65
+ * `onBinary` and `onOversize` are `include` (keep, no model call), `exclude`
66
+ * (drop, no model call), or `truncate` (ask about a truncated or redacted
67
+ * preview). Pass `0` or `null` for `maxBytes` or `maxTokens` to disable that
68
+ * limit. Credentials resolve the same way as the rest of the library.
69
+ *
70
+ * @param {Record<string, string | Uint8Array>} files
71
+ * @param {string} task
72
+ * @param {object} [opts]
73
+ * @param {string} [opts.model] `GPTDIFF_FILTER_MODEL`, else {@link DEFAULT_FILTER_MODEL}
74
+ * @param {number} [opts.concurrency] max in-flight calls (default {@link DEFAULT_FILTER_CONCURRENCY})
75
+ * @param {RelevanceCache | string | boolean | null | { get: Function, put?: Function, set?: Function }} [opts.cache]
76
+ * @param {number | null} [opts.maxBytes] UTF-8 cap; `0` or `null` disables; default {@link DEFAULT_FILTER_MAX_BYTES}
77
+ * @param {number | null} [opts.maxTokens] input-token cap; `0` or `null` disables; default {@link DEFAULT_FILTER_MAX_TOKENS}
78
+ * @param {'include' | 'exclude' | 'truncate'} [opts.onOversize] default `truncate`
79
+ * @param {'include' | 'exclude' | 'truncate'} [opts.onBinary] default `exclude`
80
+ * @param {number} [opts.maxOutputTokens] completion cap (default {@link DEFAULT_FILTER_MAX_OUTPUT_TOKENS})
81
+ * @param {string | null} [opts.reasoningEffort] default {@link DEFAULT_FILTER_REASONING_EFFORT}; `null` omits it
82
+ * @param {number} [opts.retries] extra attempts after a timeout, HTTP 5xx, or a billing-reservation error (default 2). A 4xx that names `reasoning_effort` is retried once without that parameter and does not consume one of these attempts.
83
+ * @param {number} [opts.retryBaseMs] backoff base in ms (default 200, doubles, capped at 30s)
84
+ * @param {boolean | string[] | null} [opts.ignore] lockfile and generated-file patterns.
85
+ * `true` or omitted uses {@link DEFAULT_IGNORE_PATTERNS}. `false` or `null` classifies every path.
86
+ * An array replaces the default list.
87
+ * @param {(decision: RelevanceDecision) => void} [opts.onDecision] called as each file finishes (completion order)
88
+ * @param {string} [opts.apiKey] `GPTDIFF_LLM_API_KEY` when omitted
89
+ * @param {string} [opts.baseUrl] `GPTDIFF_LLM_BASE_URL` when omitted
90
+ * @param {Function} [opts.callLlm] injectable client (tests)
91
+ * @returns {Promise<FilterRelevantResult>}
92
+ */
93
+ export function filterRelevant(files: Record<string, string | Uint8Array>, task: string, opts?: {
94
+ model?: string;
95
+ concurrency?: number;
96
+ cache?: RelevanceCache | string | boolean | null | {
97
+ get: Function;
98
+ put?: Function;
99
+ set?: Function;
100
+ };
101
+ maxBytes?: number | null;
102
+ maxTokens?: number | null;
103
+ onOversize?: "include" | "exclude" | "truncate";
104
+ onBinary?: "include" | "exclude" | "truncate";
105
+ maxOutputTokens?: number;
106
+ reasoningEffort?: string | null;
107
+ retries?: number;
108
+ retryBaseMs?: number;
109
+ ignore?: boolean | string[] | null;
110
+ onDecision?: (decision: RelevanceDecision) => void;
111
+ apiKey?: string;
112
+ baseUrl?: string;
113
+ callLlm?: Function;
114
+ }): Promise<FilterRelevantResult>;
115
+ /**
116
+ * One-screen summary of a filter pass. Per-file lines when `verbose`.
117
+ * @param {FilterRelevantResult} result
118
+ * @param {boolean} [verbose]
119
+ * @returns {string}
120
+ */
121
+ export function formatFilterSummary(result: FilterRelevantResult, verbose?: boolean): string;
122
+ /**
123
+ * Write the filter result as JSON so callers can inspect every decision.
124
+ * Decision fields stay camelCase. `files` is the list of kept paths.
125
+ * @param {FilterRelevantResult} result
126
+ * @param {string} path
127
+ * @returns {Promise<void>}
128
+ */
129
+ export function dumpDecisions(result: FilterRelevantResult, path: string): Promise<void>;
130
+ /** Cheap default for yes/no classification. Override with GPTDIFF_FILTER_MODEL. */
131
+ export const DEFAULT_FILTER_MODEL: "xiaomi/mimo-v2.6-flash";
132
+ /** In-flight classifications per call when `concurrency` is omitted. */
133
+ export const DEFAULT_FILTER_CONCURRENCY: 8;
134
+ /** UTF-8 size cap. `0` or `null` disables it. */
135
+ export const DEFAULT_FILTER_MAX_BYTES: 200000;
136
+ /** Input-token cap (`ceil(code points / 4)`). `0` or `null` disables it. */
137
+ export const DEFAULT_FILTER_MAX_TOKENS: 12000;
138
+ /**
139
+ * Completion cap for the yes/no call. High enough that a reasoning model's
140
+ * scratchpad does not consume the whole answer. `reasoning_effort` is also
141
+ * sent as `low`.
142
+ */
143
+ export const DEFAULT_FILTER_MAX_OUTPUT_TOKENS: 1024;
144
+ /** Sent on the filter call so reasoning models keep the yes/no inside the budget. */
145
+ export const DEFAULT_FILTER_REASONING_EFFORT: "low";
146
+ /**
147
+ * Bumped when the prompt or the yes/no contract changes so an old judgment is
148
+ * not reused. The cache key stringifies this value. `PROMPT_VERSION` is the
149
+ * shared name; `FILTER_PROMPT_VERSION` is the same number.
150
+ */
151
+ export const PROMPT_VERSION: 3;
152
+ export const FILTER_PROMPT_VERSION: 3;
153
+ /**
154
+ * Lockfiles and generated paths skipped unless `ignore` is `false` or a custom list.
155
+ * Patterns are case-insensitive globs (`**` crosses directories, `*` does not).
156
+ */
157
+ export const DEFAULT_IGNORE_PATTERNS: string[];
158
+ /**
159
+ * @typedef {object} RelevanceDecision
160
+ * @property {string} path
161
+ * @property {'yes' | 'no'} decision `yes` files go on to generateDiff.
162
+ * Ambiguous answers and errors are `yes`.
163
+ * @property {string} reason short explanation; the cleaned model text when a call was made
164
+ * @property {string | null} raw model text before parsing, or null when no call was made
165
+ * @property {number} tokens total tokens for the call; 0 when no call was made
166
+ * @property {number} promptTokens
167
+ * @property {number} completionTokens
168
+ * @property {boolean} cached true when this judgment was reused (a cache hit, or a later path in a shared request)
169
+ * @property {'binary' | 'oversize' | 'truncated' | 'ignored' | null} skippedReason
170
+ * set when the file was not sent in full. `ignored` is a lockfile or generated path.
171
+ * @property {string | null} error set when the call threw or the answer was empty;
172
+ * the file is still included
173
+ * @property {boolean} failedOpen true when the file was included without a
174
+ * definitive yes/no (empty, malformed, thrown, or a cache failure that
175
+ * blocked a decision)
176
+ */
177
+ /**
178
+ * @typedef {object} FilterRelevantResult
179
+ * @property {Record<string, string | Uint8Array>} files `yes` files in input order.
180
+ * Valid UTF-8 bytes are decoded to text, keeping a leading BOM. A binary
181
+ * file kept by policy is the original value, bytes untouched.
182
+ * @property {RelevanceDecision[]} decisions one per input file, in input order
183
+ * @property {string} model
184
+ * @property {RelevanceDecision[]} errors decisions whose call threw
185
+ * @property {number} billedTokens sum of `tokens` on decisions that were not cache hits
186
+ * @property {number} billedPromptTokens
187
+ * @property {number} billedCompletionTokens
188
+ * @property {number} failedOpen files included because the call failed, the answer
189
+ * was empty, or the answer was ambiguous
190
+ */
191
+ /**
192
+ * Record stored in a cache. Snake_case so a JSON file matches the Python cache.
193
+ * @typedef {object} RelevanceCacheRecord
194
+ * @property {'yes' | 'no'} decision
195
+ * @property {string} [reason]
196
+ * @property {string | null} [raw]
197
+ * @property {number} [tokens]
198
+ * @property {number} [prompt_tokens]
199
+ * @property {number} [completion_tokens]
200
+ * @property {'binary' | 'oversize' | 'truncated' | null} [skipped_reason]
201
+ * @property {string | null} [error]
202
+ * @property {boolean} [failed_open]
203
+ * @property {string} [cache_key]
204
+ */
205
+ /**
206
+ * In-memory relevance cache with an optional on-disk JSON file.
207
+ *
208
+ * The key is the SHA-256 of the exact classification inputs, length-prefixed
209
+ * in this order: prompt version, model, output-token limit, reasoning effort
210
+ * actually sent, task, path, mode (`full`, `truncated`, or `binary`), the
211
+ * preview note (or `""`), and the content actually sent. Two paths never
212
+ * share a key, even when their contents match. A full file and a preview
213
+ * truncated to the same bytes do not share a key either.
214
+ */
215
+ export class RelevanceCache {
216
+ /**
217
+ * SHA-256 hex of the length-prefixed classification inputs.
218
+ * Each field is its UTF-8 bytes, written as `byteLength`, a colon, the
219
+ * bytes, and a newline. Field order: prompt version, model, output-token
220
+ * limit, reasoning effort actually sent (`""` when omitted or rejected),
221
+ * task, path, mode (`full`, `truncated`, or `binary`), note (or `""`),
222
+ * content.
223
+ * @param {object} fields
224
+ * @param {string} fields.model
225
+ * @param {string} [fields.intent] task text
226
+ * @param {string} [fields.task] task text when `intent` is omitted
227
+ * @param {string} fields.path
228
+ * @param {string} fields.content body sent to the model, after truncation
229
+ * @param {'full' | 'truncated' | 'binary'} [fields.mode] default `full`
230
+ * @param {string} [fields.note] exact preview note, or empty
231
+ * @param {number} [fields.maxOutputTokens]
232
+ * @param {string | null} [fields.reasoningEffort] effort actually sent
233
+ * @param {number | string} [fields.promptVersion]
234
+ * @returns {Promise<string>}
235
+ */
236
+ static makeKey(fields: {
237
+ model: string;
238
+ intent?: string;
239
+ task?: string;
240
+ path: string;
241
+ content: string;
242
+ mode?: "full" | "truncated" | "binary";
243
+ note?: string;
244
+ maxOutputTokens?: number;
245
+ reasoningEffort?: string | null;
246
+ promptVersion?: number | string;
247
+ }): Promise<string>;
248
+ /**
249
+ * @param {string} [path] JSON file. Omit for memory only.
250
+ * Loaded on the first read. Corrupt files are ignored.
251
+ */
252
+ constructor(path?: string);
253
+ path: string;
254
+ /** @type {Record<string, RelevanceCacheRecord>} */
255
+ _entries: Record<string, RelevanceCacheRecord>;
256
+ /** @type {Promise<void> | null} */
257
+ _loadPromise: Promise<void> | null;
258
+ /**
259
+ * @param {string} key
260
+ * @returns {Promise<RelevanceCacheRecord | undefined>}
261
+ */
262
+ get(key: string): Promise<RelevanceCacheRecord | undefined>;
263
+ /**
264
+ * @param {string} key
265
+ * @param {RelevanceCacheRecord} record
266
+ * @returns {Promise<void>}
267
+ */
268
+ put(key: string, record: RelevanceCacheRecord): Promise<void>;
269
+ /**
270
+ * Alias of {@link put} so a `{ get, set }` cache and a `RelevanceCache` share a shape.
271
+ * @param {string} key
272
+ * @param {RelevanceCacheRecord} record
273
+ * @returns {Promise<void>}
274
+ */
275
+ set(key: string, record: RelevanceCacheRecord): Promise<void>;
276
+ /** Write `{ version: 1, entries }` atomically. No-op without a path. */
277
+ flush(): Promise<void>;
278
+ /** @returns {Promise<void>} */
279
+ load(): Promise<void>;
280
+ /** @returns {Promise<void>} */
281
+ _load(): Promise<void>;
282
+ }
283
+ export type RelevanceDecision = {
284
+ path: string;
285
+ /**
286
+ * `yes` files go on to generateDiff.
287
+ * Ambiguous answers and errors are `yes`.
288
+ */
289
+ decision: "yes" | "no";
290
+ /**
291
+ * short explanation; the cleaned model text when a call was made
292
+ */
293
+ reason: string;
294
+ /**
295
+ * model text before parsing, or null when no call was made
296
+ */
297
+ raw: string | null;
298
+ /**
299
+ * total tokens for the call; 0 when no call was made
300
+ */
301
+ tokens: number;
302
+ promptTokens: number;
303
+ completionTokens: number;
304
+ /**
305
+ * true when this judgment was reused (a cache hit, or a later path in a shared request)
306
+ */
307
+ cached: boolean;
308
+ /**
309
+ * set when the file was not sent in full. `ignored` is a lockfile or generated path.
310
+ */
311
+ skippedReason: "binary" | "oversize" | "truncated" | "ignored" | null;
312
+ /**
313
+ * set when the call threw or the answer was empty;
314
+ * the file is still included
315
+ */
316
+ error: string | null;
317
+ /**
318
+ * true when the file was included without a
319
+ * definitive yes/no (empty, malformed, thrown, or a cache failure that
320
+ * blocked a decision)
321
+ */
322
+ failedOpen: boolean;
323
+ };
324
+ export type FilterRelevantResult = {
325
+ /**
326
+ * `yes` files in input order.
327
+ * Valid UTF-8 bytes are decoded to text, keeping a leading BOM. A binary
328
+ * file kept by policy is the original value, bytes untouched.
329
+ */
330
+ files: Record<string, string | Uint8Array>;
331
+ /**
332
+ * one per input file, in input order
333
+ */
334
+ decisions: RelevanceDecision[];
335
+ model: string;
336
+ /**
337
+ * decisions whose call threw
338
+ */
339
+ errors: RelevanceDecision[];
340
+ /**
341
+ * sum of `tokens` on decisions that were not cache hits
342
+ */
343
+ billedTokens: number;
344
+ billedPromptTokens: number;
345
+ billedCompletionTokens: number;
346
+ /**
347
+ * files included because the call failed, the answer
348
+ * was empty, or the answer was ambiguous
349
+ */
350
+ failedOpen: number;
351
+ };
352
+ /**
353
+ * Record stored in a cache. Snake_case so a JSON file matches the Python cache.
354
+ */
355
+ export type RelevanceCacheRecord = {
356
+ decision: "yes" | "no";
357
+ reason?: string;
358
+ raw?: string | null;
359
+ tokens?: number;
360
+ prompt_tokens?: number;
361
+ completion_tokens?: number;
362
+ skipped_reason?: "binary" | "oversize" | "truncated" | null;
363
+ error?: string | null;
364
+ failed_open?: boolean;
365
+ cache_key?: string;
366
+ };