patchling 0.2.0 → 0.2.1
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/README.md +53 -8
- package/package.json +6 -4
- package/src/applydiff.d.ts +69 -0
- package/src/applydiff.js +782 -0
- package/src/filter.d.ts +366 -0
- package/src/filter.js +1513 -0
- package/src/gptdiff.d.ts +28 -1
- package/src/gptdiff.js +78 -10
- package/src/index.d.ts +3 -0
- package/src/index.js +25 -0
- package/src/llm.d.ts +4 -1
- package/src/llm.js +4 -0
package/src/filter.d.ts
ADDED
|
@@ -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
|
+
};
|