@withtruing/companion 0.13.3 → 0.14.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.
Files changed (56) hide show
  1. package/README.md +24 -25
  2. package/dist/anthropic-output-schema.js +41 -0
  3. package/dist/api-key-caller.js +9 -2
  4. package/dist/claude-cli-caller.js +107 -8
  5. package/dist/cli.js +833 -206
  6. package/dist/codex-cli-caller.js +85 -14
  7. package/dist/config.js +177 -2
  8. package/dist/git-range.js +88 -46
  9. package/dist/http-client.js +107 -10
  10. package/dist/investigation-reader.js +12 -6
  11. package/dist/managed-repo.js +132 -0
  12. package/dist/observed-sources.js +132 -6
  13. package/dist/reader-result-contract.js +296 -2
  14. package/dist/repository-provider.js +608 -0
  15. package/dist/schema-entities.js +18 -18
  16. package/dist/service.js +7 -2
  17. package/dist/surface-mapper.js +53 -11
  18. package/dist/text-model.js +91 -8
  19. package/dist/whole-read/archive.js +196 -0
  20. package/dist/whole-read/checks.js +297 -0
  21. package/dist/whole-read/claims.js +196 -0
  22. package/dist/whole-read/coverage-ledger.js +51 -0
  23. package/dist/whole-read/decode.js +234 -0
  24. package/dist/whole-read/drafting.js +502 -0
  25. package/dist/whole-read/generated-rules.js +68 -0
  26. package/dist/whole-read/go-proof.js +255 -0
  27. package/dist/whole-read/grouping-prompt.js +19 -0
  28. package/dist/whole-read/grouping.js +247 -0
  29. package/dist/whole-read/identity.js +103 -0
  30. package/dist/whole-read/import-graph.js +293 -0
  31. package/dist/whole-read/inventory-collector.js +55 -0
  32. package/dist/whole-read/inventory-rules.js +141 -0
  33. package/dist/whole-read/lockfiles.js +947 -0
  34. package/dist/whole-read/notes.js +216 -0
  35. package/dist/whole-read/npm-proof.js +272 -0
  36. package/dist/whole-read/owner-decision-detectors.js +270 -0
  37. package/dist/whole-read/packing.js +612 -0
  38. package/dist/whole-read/pages.js +429 -0
  39. package/dist/whole-read/phase-policy.js +54 -0
  40. package/dist/whole-read/prd-build.js +279 -0
  41. package/dist/whole-read/prd-fidelity.js +14 -0
  42. package/dist/whole-read/python-proof.js +489 -0
  43. package/dist/whole-read/read-prompt.js +67 -0
  44. package/dist/whole-read/redaction.js +411 -0
  45. package/dist/whole-read/relationship-mapping.js +98 -0
  46. package/dist/whole-read/sanitize.js +266 -0
  47. package/dist/whole-read/secret-rules.js +26 -0
  48. package/dist/whole-read/structure.js +300 -0
  49. package/dist/whole-read/types.js +2 -0
  50. package/dist/whole-read-claims.js +12 -0
  51. package/dist/whole-read-git.js +340 -0
  52. package/dist/whole-read-pool.js +80 -0
  53. package/dist/whole-read-runner.js +945 -0
  54. package/dist/whole-read-shared-source.js +194 -0
  55. package/dist/whole-read-temp.js +479 -0
  56. package/package.json +8 -2
@@ -0,0 +1,612 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { decodeCanonicalBlob } from './decode.js';
3
+ import { redactCanonicalText } from './redaction.js';
4
+ import { sanitizeBuildMetadata } from './sanitize.js';
5
+ import { WHOLE_READ_PHASE_POLICY } from './phase-policy.js';
6
+ export const READ_PLANNED_INPUT_TOKENS_PER_CHARACTER = 0.5;
7
+ export const READ_OUTPUT_TOKENS_PER_INPUT_TOKEN = 0.6;
8
+ export const MIN_WHOLE_READ_OUTPUT_TOKENS = 2_048;
9
+ export const MAX_WHOLE_READ_OUTPUT_TOKENS = WHOLE_READ_PHASE_POLICY.read.maxOutputTokens;
10
+ /** At 0.5 planned tokens per character this stays below Haiku's 100k lower-price prompt tier. */
11
+ export const MAX_WHOLE_READ_PROMPT_CHARACTERS = 106_666;
12
+ export const WHOLE_READ_PACKING_VERSION = 'whole-read-packing@4';
13
+ export function plannedReadInputTokens(promptCharacters) {
14
+ if (!Number.isSafeInteger(promptCharacters) || promptCharacters <= 0)
15
+ return invalid();
16
+ return Math.ceil(promptCharacters * READ_PLANNED_INPUT_TOKENS_PER_CHARACTER);
17
+ }
18
+ export function readUnitOutputTokens(promptCharacters) {
19
+ return Math.min(MAX_WHOLE_READ_OUTPUT_TOKENS, Math.max(MIN_WHOLE_READ_OUTPUT_TOKENS, Math.ceil(plannedReadInputTokens(promptCharacters) * READ_OUTPUT_TOKENS_PER_INPUT_TOKEN)));
20
+ }
21
+ const invalid = () => { throw new TypeError('whole_read_plan_invalid'); };
22
+ const hash = (value) => `sha256:${createHash('sha256').update(value, 'utf8').digest('hex')}`;
23
+ function crlfBoundaries(source) {
24
+ const result = new Set();
25
+ for (const line of source.decoded.mapping.lines)
26
+ if (line.endCharacter - line.contentEndCharacter === 2) {
27
+ result.add(source.decoded.mapping.byteOffsetAtCharacter(line.contentEndCharacter + 1));
28
+ }
29
+ return result;
30
+ }
31
+ function prepare(source) {
32
+ if (!source.path || !sanitizeBuildMetadata({ path: source.path }).ok)
33
+ return invalid();
34
+ const redacted = redactCanonicalText({ text: source.decoded.text });
35
+ if (redacted.mapping.segments.every((segment) => segment.kind === 'source')
36
+ && redacted.text === source.decoded.text)
37
+ return {
38
+ source, text: source.decoded.text, codeUnits: source.decoded.mapping.characterCodeUnitOffsets,
39
+ boundaries: source.decoded.mapping.canonicalByteOffsets.map((byte, delivered) => ({ byte, delivered })),
40
+ lines: source.decoded.mapping.lines, forbiddenBytes: crlfBoundaries(source), replacementBreaks: [],
41
+ hasDeliveredBoundary: () => true,
42
+ deliveredSpan: (startByte, endByte) => [source.decoded.mapping.characterOffsetAtByte(startByte),
43
+ source.decoded.mapping.characterOffsetAtByte(endByte)],
44
+ };
45
+ const bomLength = source.decoded.bom?.endByte ?? 0;
46
+ const utf8 = source.decoded.encoding === 'utf8' ? undefined
47
+ : decodeCanonicalBlob(new TextEncoder().encode(source.decoded.text));
48
+ if (utf8 && utf8.status !== 'decoded')
49
+ return invalid();
50
+ const toActual = (utf8Byte) => source.decoded.encoding === 'utf8'
51
+ ? utf8Byte + bomLength
52
+ : source.decoded.mapping.byteOffsetAtCharacter(utf8.mapping.characterOffsetAtByte(utf8Byte));
53
+ const lineBreakCounts = [0];
54
+ for (const character of source.decoded.text) {
55
+ lineBreakCounts.push(lineBreakCounts.at(-1) + (character === '\n' ? 1 : 0));
56
+ }
57
+ const replacementBreaks = redacted.mapping.segments.flatMap((segment) => {
58
+ if (segment.kind !== 'replacement')
59
+ return [];
60
+ const start = source.decoded.mapping.characterOffsetAtByte(toActual(segment.canonicalStartByte));
61
+ const end = source.decoded.mapping.characterOffsetAtByte(toActual(segment.canonicalEndByte));
62
+ const count = lineBreakCounts[end] - lineBreakCounts[start];
63
+ return count ? [{ deliveredAfter: segment.deliveredEnd, count }] : [];
64
+ });
65
+ const boundaries = [];
66
+ const add = (point) => {
67
+ const previous = boundaries.at(-1);
68
+ if (previous?.delivered === point.delivered)
69
+ boundaries[boundaries.length - 1] = point;
70
+ else
71
+ boundaries.push(point);
72
+ };
73
+ for (const segment of redacted.mapping.segments) {
74
+ if (segment.kind === 'source')
75
+ segment.canonicalByteOffsets.forEach((byte, index) => add({
76
+ delivered: segment.deliveredStart + index, byte: toActual(byte),
77
+ }));
78
+ else {
79
+ add({ delivered: segment.deliveredStart, byte: toActual(segment.canonicalStartByte) });
80
+ add({ delivered: segment.deliveredEnd, byte: toActual(segment.canonicalEndByte) });
81
+ }
82
+ }
83
+ if (!boundaries.length) {
84
+ const start = source.decoded.bom?.endByte ?? 0;
85
+ boundaries.push({ delivered: 0, byte: start });
86
+ }
87
+ const deliveredSpan = (startByte, endByte) => {
88
+ const start = boundaries.find((point) => point.byte === startByte);
89
+ const end = boundaries.findLast((point) => point.byte === endByte);
90
+ if (!start || !end || start.delivered > end.delivered)
91
+ return invalid();
92
+ return [start.delivered, end.delivered];
93
+ };
94
+ const hasDeliveredBoundary = (byte) => {
95
+ let low = 0;
96
+ let high = boundaries.length - 1;
97
+ while (low <= high) {
98
+ const middle = (low + high) >>> 1;
99
+ const point = boundaries[middle].byte;
100
+ if (point === byte)
101
+ return true;
102
+ if (point < byte)
103
+ low = middle + 1;
104
+ else
105
+ high = middle - 1;
106
+ }
107
+ return false;
108
+ };
109
+ const forbiddenBytes = crlfBoundaries(source);
110
+ const codeUnits = [0];
111
+ let codeUnit = 0;
112
+ for (const character of redacted.text) {
113
+ codeUnit += character.length;
114
+ codeUnits.push(codeUnit);
115
+ }
116
+ return { source, text: redacted.text, codeUnits, boundaries, lines: source.decoded.mapping.lines,
117
+ forbiddenBytes, replacementBreaks, deliveredSpan, hasDeliveredBoundary };
118
+ }
119
+ /** Recreates the exact redacted text slice used while packing a canonical byte span. */
120
+ export function deliveredCanonicalSlice(decoded, startByte, endByte) {
121
+ const prepared = prepare({ entryId: 'delivery', path: 'delivery', blobId: 'delivery',
122
+ inventoryRevision: 1, decoded });
123
+ const [start, end] = prepared.deliveredSpan(startByte, endByte);
124
+ return prepared.text.slice(prepared.codeUnits[start], prepared.codeUnits[end]);
125
+ }
126
+ function preparedOrUndefined(source) {
127
+ try {
128
+ return prepare(source);
129
+ }
130
+ catch {
131
+ return undefined;
132
+ }
133
+ }
134
+ const sourceText = (item, start, end) => item.prepared.text.slice(item.prepared.codeUnits[start], item.prepared.codeUnits[end]);
135
+ const sourceCharacters = (item, start, end) => item.prepared.codeUnits[end] - item.prepared.codeUnits[start];
136
+ function marker(item, number) {
137
+ const fragment = item.fragmentIndex === undefined ? '' : ` fragment=${item.fragmentIndex}`;
138
+ return `\n\n[${number}] path=${JSON.stringify(item.path)} blob=${item.blobId} kind=${item.kind} lines=${item.startLine}-${item.endLine} range=[${item.primarySpan.startByte},${item.primarySpan.endByte})${fragment}\n`;
139
+ }
140
+ const linePrefix = (line, fragmentIndex) => fragmentIndex === undefined
141
+ ? `${String(line).padStart(6)}| `
142
+ : `${String(line).padStart(6)}[fragment ${fragmentIndex}]| `;
143
+ function lineCompleteText(text, breaks, startLine, endLine, includeTrailingEmpty) {
144
+ let complete = '';
145
+ let cursor = 0;
146
+ for (const point of breaks) {
147
+ if (!Number.isSafeInteger(point.afterCharacter) || point.afterCharacter < cursor
148
+ || point.afterCharacter > text.length || !Number.isSafeInteger(point.count) || point.count <= 0)
149
+ return undefined;
150
+ complete += text.slice(cursor, point.afterCharacter) + '\n'.repeat(point.count);
151
+ cursor = point.afterCharacter;
152
+ }
153
+ complete += text.slice(cursor);
154
+ const expectedBreaks = endLine - startLine;
155
+ const rawBreaks = [...complete.matchAll(/\n/g)].length;
156
+ const actualBreaks = !includeTrailingEmpty && complete.endsWith('\n') ? rawBreaks - 1 : rawBreaks;
157
+ return actualBreaks === expectedBreaks ? complete : undefined;
158
+ }
159
+ function numberedText(text, startLine, endLine, includeTrailingEmpty, breaks, fragmentIndex) {
160
+ if (fragmentIndex !== undefined)
161
+ return linePrefix(startLine, fragmentIndex) + text;
162
+ const complete = lineCompleteText(text, breaks, startLine, endLine, includeTrailingEmpty);
163
+ if (complete === undefined)
164
+ return undefined;
165
+ let value = linePrefix(startLine);
166
+ let line = startLine;
167
+ let cursor = 0;
168
+ while (line < endLine) {
169
+ const newline = complete.indexOf('\n', cursor);
170
+ if (newline < 0)
171
+ break;
172
+ value += complete.slice(cursor, newline + 1);
173
+ cursor = newline + 1;
174
+ line += 1;
175
+ value += linePrefix(line);
176
+ }
177
+ return value + complete.slice(cursor);
178
+ }
179
+ function deliveredText(text, item, startLine, options, breaks, fragmentIndex) {
180
+ if (!options.numberLines)
181
+ return text;
182
+ return numberedText(text, startLine, fragmentIndex === undefined ? item.endLine : startLine, item.kind === 'whole', breaks, fragmentIndex) ?? invalid();
183
+ }
184
+ /** Complete prompts are counted with JavaScript String.length (UTF-16 code units), exactly as sent. */
185
+ export function assembleWholeReadPrompt(prompt, contexts, items, options) {
186
+ let value = `${prompt.instructions}\n${prompt.schema}`;
187
+ for (const context of contexts)
188
+ value += `\n<context ref="${context.ref}">\n${context.text}\n</context>`;
189
+ items.forEach(({ item, primaryText, overlapText }, index) => {
190
+ value += marker(item, index + 1);
191
+ if (overlapText !== undefined) {
192
+ if (!item.overlapSpans[0] || item.overlapStartLine === undefined)
193
+ return invalid();
194
+ value += `<overlap context-only lines=${item.overlapStartLine}-${item.startLine - 1}>\n`
195
+ + deliveredText(overlapText, { ...item, endLine: item.startLine - 1 }, item.overlapStartLine, options, item.overlapReplacementBreaks ?? [])
196
+ + '\n</overlap>\n';
197
+ }
198
+ value += deliveredText(primaryText, item, item.startLine, options, item.primaryReplacementBreaks ?? [], item.fragmentIndex);
199
+ });
200
+ return value;
201
+ }
202
+ function completePrompt(prompt, contexts, items, options) {
203
+ return assembleWholeReadPrompt(prompt, contexts, items.map((item) => ({
204
+ item,
205
+ primaryText: sourceText(item, item.deliveredStart, item.deliveredEnd),
206
+ ...(item.overlapDelivered ? { overlapText: sourceText(item, ...item.overlapDelivered) } : {}),
207
+ })), options);
208
+ }
209
+ function numberedCharacterCount(text, startLine, endLine, includeTrailingEmpty, breaks, fragmentIndex) {
210
+ if (fragmentIndex !== undefined)
211
+ return text.length + linePrefix(startLine, fragmentIndex).length;
212
+ let previous = 0;
213
+ let inserted = 0;
214
+ for (const point of breaks) {
215
+ if (!Number.isSafeInteger(point.afterCharacter) || point.afterCharacter < previous
216
+ || point.afterCharacter > text.length || !Number.isSafeInteger(point.count) || point.count <= 0)
217
+ return undefined;
218
+ previous = point.afterCharacter;
219
+ inserted += point.count;
220
+ }
221
+ const rawBreaks = [...text.matchAll(/\n/g)].length + inserted;
222
+ const endsWithBreak = breaks.at(-1)?.afterCharacter === text.length || text.endsWith('\n');
223
+ const actualBreaks = !includeTrailingEmpty && endsWithBreak ? rawBreaks - 1 : rawBreaks;
224
+ if (actualBreaks !== endLine - startLine)
225
+ return undefined;
226
+ let count = text.length + inserted;
227
+ for (let line = startLine; line <= endLine; line += 1)
228
+ count += linePrefix(line).length;
229
+ return count;
230
+ }
231
+ function textCharacterCount(item, start, end, startLine, endLine, numberLines, breaks, fragmentIndex) {
232
+ if (!numberLines)
233
+ return sourceCharacters(item, start, end);
234
+ return numberedCharacterCount(sourceText(item, start, end), startLine, endLine, item.kind === 'whole', breaks, fragmentIndex);
235
+ }
236
+ function itemCharacterCount(item, number, numberLines) {
237
+ const primary = textCharacterCount(item, item.deliveredStart, item.deliveredEnd, item.startLine, item.endLine, numberLines, item.primaryReplacementBreaks ?? [], item.fragmentIndex);
238
+ if (primary === undefined)
239
+ return undefined;
240
+ let count = marker(item, number).length + primary;
241
+ if (item.overlapDelivered) {
242
+ if (item.overlapStartLine === undefined)
243
+ return invalid();
244
+ const overlap = textCharacterCount(item, ...item.overlapDelivered, item.overlapStartLine, item.startLine - 1, numberLines, item.overlapReplacementBreaks ?? []);
245
+ if (overlap === undefined)
246
+ return undefined;
247
+ count += `<overlap context-only lines=${item.overlapStartLine}-${item.startLine - 1}>\n`.length
248
+ + overlap + '\n</overlap>\n'.length;
249
+ }
250
+ return count;
251
+ }
252
+ function requiredRefs(items, limits) {
253
+ return new Set(items.flatMap((item) => limits.requiredContextRefs?.(publicItem(item)) ?? []));
254
+ }
255
+ function allowedRefs(items, limits) {
256
+ const refs = limits.contextRefsForItems?.(items.map(publicItem));
257
+ return refs ? new Set(refs) : undefined;
258
+ }
259
+ function fitCharacters(itemCharacters, prompt, limits, required = new Set(), allowed) {
260
+ const available = new Set(prompt.context.map((item) => item.ref));
261
+ if ([...required].some((ref) => !available.has(ref) || allowed && !allowed.has(ref)))
262
+ return undefined;
263
+ const selected = prompt.context.filter((item) => !allowed || allowed.has(item.ref));
264
+ let contextCharacters = selected.reduce((sum, item) => sum + `\n<context ref="${item.ref}">\n${item.text}\n</context>`.length, 0);
265
+ for (;;) {
266
+ const characters = prompt.instructions.length + 1 + prompt.schema.length + contextCharacters + itemCharacters;
267
+ if (characters <= limits.prompt)
268
+ return { characters, contextRefs: selected.map((item) => item.ref) };
269
+ const index = selected.findLastIndex((item) => !required.has(item.ref));
270
+ if (index < 0)
271
+ return undefined;
272
+ const [removed] = selected.splice(index, 1);
273
+ contextCharacters -= `\n<context ref="${removed.ref}">\n${removed.text}\n</context>`.length;
274
+ }
275
+ }
276
+ function fit(items, prompt, limits) {
277
+ let characters = 0;
278
+ for (const [index, item] of items.entries()) {
279
+ const itemCharacters = itemCharacterCount(item, index + 1, limits.numberLines);
280
+ if (itemCharacters === undefined)
281
+ return undefined;
282
+ characters += itemCharacters;
283
+ }
284
+ return fitCharacters(characters, prompt, limits, requiredRefs(items, limits), allowedRefs(items, limits));
285
+ }
286
+ function replacementBreaks(prepared, start, end) {
287
+ return prepared.replacementBreaks.filter((point) => point.deliveredAfter > start && point.deliveredAfter <= end)
288
+ .map((point) => ({ afterCharacter: prepared.codeUnits[point.deliveredAfter] - prepared.codeUnits[start],
289
+ count: point.count }));
290
+ }
291
+ function whole(prepared) {
292
+ const startByte = prepared.source.decoded.bom?.endByte ?? 0;
293
+ const endByte = prepared.source.decoded.canonicalByteLength;
294
+ const [deliveredStart, deliveredEnd] = prepared.deliveredSpan(startByte, endByte);
295
+ const primaryReplacementBreaks = replacementBreaks(prepared, deliveredStart, deliveredEnd);
296
+ return { prepared, entryId: prepared.source.entryId, path: prepared.source.path, blobId: prepared.source.blobId,
297
+ inventoryRevision: prepared.source.inventoryRevision, kind: 'whole', primarySpan: { startByte, endByte },
298
+ overlapSpans: [], startLine: 1, endLine: Math.max(1, prepared.source.decoded.lineCount),
299
+ ...(primaryReplacementBreaks.length ? { primaryReplacementBreaks } : {}),
300
+ deliveredStart, deliveredEnd };
301
+ }
302
+ function chunk(prepared, start, end) {
303
+ const first = prepared.lines[start];
304
+ const last = prepared.lines[end - 1];
305
+ const primarySpan = { startByte: first.startByte, endByte: last.endByte };
306
+ const [deliveredStart, deliveredEnd] = prepared.deliveredSpan(primarySpan.startByte, primarySpan.endByte);
307
+ const overlapCount = Math.min(start, 40, Math.floor((end - start) * 0.1));
308
+ let overlapIndex = start - overlapCount;
309
+ while (overlapIndex < start && !prepared.hasDeliveredBoundary(prepared.lines[overlapIndex].startByte)) {
310
+ overlapIndex += 1;
311
+ }
312
+ const overlapLine = overlapIndex < start ? prepared.lines[overlapIndex] : undefined;
313
+ const overlapSpan = overlapLine ? { startByte: overlapLine.startByte, endByte: first.startByte } : undefined;
314
+ const primaryReplacementBreaks = replacementBreaks(prepared, deliveredStart, deliveredEnd);
315
+ const overlapDelivered = overlapLine ? prepared.deliveredSpan(overlapLine.startByte, first.startByte) : undefined;
316
+ const overlapReplacementBreaks = overlapDelivered ? replacementBreaks(prepared, ...overlapDelivered) : [];
317
+ return { prepared, entryId: prepared.source.entryId, path: prepared.source.path, blobId: prepared.source.blobId,
318
+ inventoryRevision: prepared.source.inventoryRevision, kind: 'line_chunk', primarySpan,
319
+ overlapSpans: overlapSpan ? [overlapSpan] : [], startLine: first.lineNumber, endLine: last.lineNumber,
320
+ ...(primaryReplacementBreaks.length ? { primaryReplacementBreaks } : {}),
321
+ ...(overlapLine ? { overlapStartLine: overlapLine.lineNumber,
322
+ overlapDelivered, ...(overlapReplacementBreaks.length ? { overlapReplacementBreaks } : {}) } : {}),
323
+ deliveredStart, deliveredEnd };
324
+ }
325
+ function fragment(prepared, line, start, end, index) {
326
+ const primaryReplacementBreaks = replacementBreaks(prepared, start.delivered, end.delivered);
327
+ return { prepared, entryId: prepared.source.entryId, path: prepared.source.path, blobId: prepared.source.blobId,
328
+ inventoryRevision: prepared.source.inventoryRevision, kind: 'fragment',
329
+ primarySpan: { startByte: start.byte, endByte: end.byte }, overlapSpans: [],
330
+ startLine: line.lineNumber, endLine: line.lineNumber, fragmentIndex: index,
331
+ ...(primaryReplacementBreaks.length ? { primaryReplacementBreaks } : {}),
332
+ deliveredStart: start.delivered, deliveredEnd: end.delivered };
333
+ }
334
+ function fragmentsFor(prepared, line, prompt, limits) {
335
+ const points = prepared.boundaries.filter((point) => point.byte >= line.startByte && point.byte <= line.endByte
336
+ && !prepared.forbiddenBytes.has(point.byte));
337
+ if (points[0]?.byte !== line.startByte || points.at(-1)?.byte !== line.endByte)
338
+ return undefined;
339
+ const result = [];
340
+ let cursor = 0;
341
+ while (cursor < points.length - 1) {
342
+ let low = cursor + 1;
343
+ let high = points.length - 1;
344
+ let best = -1;
345
+ while (low <= high) {
346
+ const middle = (low + high) >>> 1;
347
+ if (fit([fragment(prepared, line, points[cursor], points[middle], result.length)], prompt, limits)) {
348
+ best = middle;
349
+ low = middle + 1;
350
+ }
351
+ else
352
+ high = middle - 1;
353
+ }
354
+ if (best < 0)
355
+ return undefined;
356
+ result.push(fragment(prepared, line, points[cursor], points[best], result.length));
357
+ cursor = best;
358
+ }
359
+ return result;
360
+ }
361
+ function itemsFor(prepared, prompt, limits) {
362
+ const asWhole = whole(prepared);
363
+ if (fit([asWhole], prompt, limits))
364
+ return [asWhole];
365
+ const lines = prepared.lines.filter((line) => line.endByte > line.startByte);
366
+ const validEnds = lines.map((line, index) => prepared.hasDeliveredBoundary(line.endByte) ? index + 1 : 0)
367
+ .filter((end) => end > 0);
368
+ const result = [];
369
+ let cursor = 0;
370
+ let validStart = 0;
371
+ while (cursor < lines.length) {
372
+ while (validEnds[validStart] !== undefined && validEnds[validStart] <= cursor)
373
+ validStart += 1;
374
+ let low = validStart;
375
+ let high = validEnds.length - 1;
376
+ let best = cursor;
377
+ while (low <= high) {
378
+ const middle = (low + high) >>> 1;
379
+ const end = validEnds[middle];
380
+ if (fit([chunk({ ...prepared, lines }, cursor, end)], prompt, limits)) {
381
+ best = end;
382
+ low = middle + 1;
383
+ }
384
+ else
385
+ high = middle - 1;
386
+ }
387
+ if (best > cursor) {
388
+ result.push(chunk({ ...prepared, lines }, cursor, best));
389
+ cursor = best;
390
+ continue;
391
+ }
392
+ const fragments = fragmentsFor(prepared, lines[cursor], prompt, limits);
393
+ if (!fragments)
394
+ return undefined;
395
+ result.push(...fragments);
396
+ cursor += 1;
397
+ }
398
+ return result;
399
+ }
400
+ const publicItem = ({ prepared: _prepared, deliveredStart: _start, deliveredEnd: _end, overlapDelivered: _overlap, ...item }) => item;
401
+ function unit(passId, index, items, prompt, limits, parent) {
402
+ const fitted = fit(items, prompt, limits) ?? invalid();
403
+ const included = new Set(fitted.contextRefs);
404
+ const promptText = completePrompt(prompt, prompt.context.filter((item) => included.has(item.ref)), items, { numberLines: limits.numberLines });
405
+ if (promptText.length !== fitted.characters)
406
+ return invalid();
407
+ const promptHash = hash(promptText);
408
+ const identity = hash(JSON.stringify({ passId, parent, index, promptHash,
409
+ packingVersion: WHOLE_READ_PACKING_VERSION, items: items.map((item) => publicItem(item)) }));
410
+ return { unitId: `unit_${identity.slice(7, 31)}`, items: items.map(publicItem), contextRefs: fitted.contextRefs,
411
+ promptVersion: prompt.version, promptHash, promptCharacters: fitted.characters,
412
+ outputTokens: limits.outputEnvelope === 'per_item'
413
+ ? Math.min(limits.maxOutputTokens, limits.thinkingAllowanceTokens
414
+ + items.length * limits.outputTokensPerItem) : readUnitOutputTokens(fitted.characters) };
415
+ }
416
+ function result(units, setAside, failures, replacesUnitId) {
417
+ return { packingVersion: WHOLE_READ_PACKING_VERSION, units, setAside, failures,
418
+ remainingEnvelope: { unitCount: units.length,
419
+ promptCharacters: units.reduce((sum, item) => sum + item.promptCharacters, 0),
420
+ outputTokens: units.reduce((sum, item) => sum + item.outputTokens, 0) },
421
+ ...(replacesUnitId ? { replacesUnitId } : {}) };
422
+ }
423
+ function limitsFor(input) {
424
+ const prompt = Math.min(input.maxPromptCharacters ?? MAX_WHOLE_READ_PROMPT_CHARACTERS, MAX_WHOLE_READ_PROMPT_CHARACTERS);
425
+ const thinkingAllowanceTokens = input.thinkingAllowanceTokens ?? 0;
426
+ if (!input.passId || !input.prompt.version || !Number.isSafeInteger(prompt) || prompt <= 0
427
+ || typeof input.numberLines !== 'boolean'
428
+ || !Number.isSafeInteger(input.maxOutputTokens) || input.maxOutputTokens <= 0
429
+ || !Number.isSafeInteger(thinkingAllowanceTokens) || thinkingAllowanceTokens < 0
430
+ || !Number.isSafeInteger(input.outputTokensPerItem) || input.outputTokensPerItem <= 0
431
+ || (input.outputEnvelope === 'per_item'
432
+ && thinkingAllowanceTokens + input.outputTokensPerItem > input.maxOutputTokens))
433
+ return invalid();
434
+ return { prompt, numberLines: input.numberLines, outputEnvelope: input.outputEnvelope ?? 'read',
435
+ maxOutputTokens: input.maxOutputTokens, thinkingAllowanceTokens,
436
+ outputTokensPerItem: input.outputTokensPerItem, requiredContextRefs: input.requiredContextRefs,
437
+ contextRefsForItems: input.contextRefsForItems };
438
+ }
439
+ const outputFits = (itemCount, limits) => limits.outputEnvelope === 'read'
440
+ || limits.thinkingAllowanceTokens + itemCount * limits.outputTokensPerItem <= limits.maxOutputTokens;
441
+ export function planUnits(input) {
442
+ const limits = limitsFor(input);
443
+ const setAside = [];
444
+ const failures = [];
445
+ const items = [];
446
+ const ids = new Set();
447
+ for (const source of input.entries) {
448
+ if (!source.entryId || ids.has(source.entryId) || !source.path
449
+ || !sanitizeBuildMetadata({ path: source.path }).ok || !source.blobId
450
+ || !Number.isSafeInteger(source.inventoryRevision))
451
+ invalid();
452
+ ids.add(source.entryId);
453
+ if (source.decoded.canonicalByteLength === 0) {
454
+ setAside.push({ entryId: source.entryId, reason: 'empty' });
455
+ continue;
456
+ }
457
+ const prepared = preparedOrUndefined(source);
458
+ let planned;
459
+ try {
460
+ planned = prepared && source.decoded.characterCount ? itemsFor(prepared, input.prompt, limits) : undefined;
461
+ }
462
+ catch {
463
+ planned = undefined;
464
+ }
465
+ if (planned)
466
+ items.push(...planned);
467
+ else
468
+ failures.push({ entryId: source.entryId, state: 'unresolved', subState: 'failed', code: 'unit_unsplittable',
469
+ span: { startByte: source.decoded.bom?.endByte ?? 0, endByte: source.decoded.canonicalByteLength } });
470
+ }
471
+ const groups = [];
472
+ let currentCharacters = 0;
473
+ for (const item of items) {
474
+ const current = groups.at(-1);
475
+ const itemCharacters = itemCharacterCount(item, (current?.length ?? 0) + 1, limits.numberLines) ?? invalid();
476
+ const appendedCharacters = currentCharacters + itemCharacters;
477
+ if (current && outputFits(current.length + 1, limits)
478
+ && fitCharacters(appendedCharacters, input.prompt, limits, requiredRefs([...current, item], limits), allowedRefs([...current, item], limits))) {
479
+ current.push(item);
480
+ currentCharacters = appendedCharacters;
481
+ }
482
+ else {
483
+ groups.push([item]);
484
+ currentCharacters = itemCharacterCount(item, 1, limits.numberLines) ?? invalid();
485
+ }
486
+ }
487
+ return result(groups.map((group, index) => unit(input.passId, index, group, input.prompt, limits)), setAside, failures);
488
+ }
489
+ function restore(item, prepared) {
490
+ const [deliveredStart, deliveredEnd] = prepared.deliveredSpan(item.primarySpan.startByte, item.primarySpan.endByte);
491
+ const overlap = item.overlapSpans[0];
492
+ return { ...item, path: prepared.source.path, prepared, deliveredStart, deliveredEnd,
493
+ ...(overlap ? { overlapDelivered: prepared.deliveredSpan(overlap.startByte, overlap.endByte) } : {}) };
494
+ }
495
+ function splitItem(item) {
496
+ const lines = item.prepared.lines.filter((line) => line.startByte >= item.primarySpan.startByte
497
+ && line.endByte <= item.primarySpan.endByte && line.endByte > line.startByte);
498
+ if (lines.length > 1) {
499
+ const all = item.prepared.lines.filter((line) => line.endByte > line.startByte);
500
+ const start = all.findIndex((line) => line.startByte === lines[0].startByte);
501
+ const splits = Array.from({ length: lines.length - 1 }, (_, index) => index + 1)
502
+ .filter((offset) => item.prepared.hasDeliveredBoundary(lines[offset].startByte));
503
+ const middle = splits.reduce((best, offset) => best === undefined
504
+ || Math.abs(offset - lines.length / 2) < Math.abs(best - lines.length / 2) ? offset : best, undefined);
505
+ if (middle === undefined)
506
+ return undefined;
507
+ return [chunk({ ...item.prepared, lines: all }, start, start + middle),
508
+ chunk({ ...item.prepared, lines: all }, start + middle, start + lines.length)];
509
+ }
510
+ const points = item.prepared.boundaries.filter((point) => point.byte >= item.primarySpan.startByte
511
+ && point.byte <= item.primarySpan.endByte && !item.prepared.forbiddenBytes.has(point.byte));
512
+ if (points.length <= 2)
513
+ return undefined;
514
+ const middle = Math.floor((points.length - 1) / 2);
515
+ const line = item.prepared.lines.find((candidate) => candidate.lineNumber === item.startLine);
516
+ if (!line)
517
+ return undefined;
518
+ const base = item.fragmentIndex ?? 0;
519
+ return [fragment(item.prepared, line, points[0], points[middle], base * 2),
520
+ fragment(item.prepared, line, points[middle], points.at(-1), base * 2 + 1)];
521
+ }
522
+ function fitExistingItem(item, prompt, limits) {
523
+ const pending = [item];
524
+ const fitted = [];
525
+ while (pending.length) {
526
+ const candidate = pending.shift();
527
+ if (fit([candidate], prompt, limits)) {
528
+ fitted.push(candidate);
529
+ continue;
530
+ }
531
+ const split = splitItem(candidate);
532
+ if (!split)
533
+ return undefined;
534
+ pending.unshift(...split);
535
+ }
536
+ return fitted;
537
+ }
538
+ /** Re-packs existing unread coverage without re-listing or re-reading inventory. */
539
+ export function repackUnits(input) {
540
+ const limits = limitsFor(input);
541
+ if (!input.units.length)
542
+ return invalid();
543
+ const sources = new Map(input.entries.map((entry) => [entry.entryId, preparedOrUndefined(entry)]));
544
+ const failures = [];
545
+ const items = [];
546
+ for (const planned of input.units.flatMap((candidate) => candidate.items)) {
547
+ const prepared = sources.get(planned.entryId);
548
+ if (!prepared) {
549
+ failures.push({ entryId: planned.entryId, state: 'unresolved', subState: 'failed',
550
+ code: 'unit_unsplittable', span: planned.primarySpan });
551
+ continue;
552
+ }
553
+ let restored;
554
+ try {
555
+ restored = restore(planned, prepared);
556
+ }
557
+ catch {
558
+ failures.push({ entryId: planned.entryId, state: 'unresolved', subState: 'failed',
559
+ code: 'unit_unsplittable', span: planned.primarySpan });
560
+ continue;
561
+ }
562
+ const fitted = fitExistingItem(restored, input.prompt, limits);
563
+ if (fitted)
564
+ items.push(...fitted);
565
+ else
566
+ failures.push({ entryId: planned.entryId, state: 'unresolved', subState: 'failed',
567
+ code: 'unit_unsplittable', span: planned.primarySpan });
568
+ }
569
+ if (failures.length)
570
+ return result([], [], failures);
571
+ const groups = [];
572
+ let currentCharacters = 0;
573
+ for (const item of items) {
574
+ const current = groups.at(-1);
575
+ const itemCharacters = itemCharacterCount(item, (current?.length ?? 0) + 1, limits.numberLines) ?? invalid();
576
+ const appendedCharacters = currentCharacters + itemCharacters;
577
+ if (current && outputFits(current.length + 1, limits)
578
+ && fitCharacters(appendedCharacters, input.prompt, limits, requiredRefs([...current, item], limits), allowedRefs([...current, item], limits))) {
579
+ current.push(item);
580
+ currentCharacters = appendedCharacters;
581
+ }
582
+ else {
583
+ groups.push([item]);
584
+ currentCharacters = itemCharacterCount(item, 1, limits.numberLines) ?? invalid();
585
+ }
586
+ }
587
+ return result(groups.map((group, index) => unit(input.passId, index, group, input.prompt, limits, `replan:${WHOLE_READ_PACKING_VERSION}`)), [], []);
588
+ }
589
+ export function subdivideUnit(input) {
590
+ const limits = limitsFor(input);
591
+ const sources = new Map(input.entries.map((entry) => [entry.entryId, preparedOrUndefined(entry)]));
592
+ const parent = [];
593
+ for (const item of input.parent.items) {
594
+ const prepared = sources.get(item.entryId);
595
+ try {
596
+ if (!prepared)
597
+ throw new TypeError('whole_read_plan_invalid');
598
+ parent.push(restore(item, prepared));
599
+ }
600
+ catch {
601
+ return result([], [], [{ entryId: item.entryId, state: 'unresolved', subState: 'failed',
602
+ code: 'unit_unsplittable', span: item.primarySpan }], input.parent.unitId);
603
+ }
604
+ }
605
+ const children = parent.length > 1 ? parent : [...(splitItem(parent[0] ?? invalid()) ?? [])];
606
+ if (!children.length || children.some((child) => !fit([child], input.prompt, limits))) {
607
+ const item = parent[0];
608
+ return result([], [], [{ entryId: item.entryId, state: 'unresolved', subState: 'failed',
609
+ code: 'unit_unsplittable', span: item.primarySpan }], input.parent.unitId);
610
+ }
611
+ return result(children.map((child, index) => unit(input.passId, index, [child], input.prompt, limits, input.parent.unitId)), [], [], input.parent.unitId);
612
+ }