@withtruing/companion 0.13.2 → 0.14.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/README.md +25 -26
- package/dist/anthropic-output-schema.js +41 -0
- package/dist/api-key-caller.js +9 -2
- package/dist/claude-cli-caller.js +107 -8
- package/dist/cli.js +837 -207
- package/dist/codex-binary.js +96 -0
- package/dist/codex-cli-caller.js +90 -16
- package/dist/config.js +177 -2
- package/dist/git-range.js +88 -46
- package/dist/http-client.js +95 -10
- package/dist/investigation-reader.js +12 -6
- package/dist/managed-repo.js +132 -0
- package/dist/observed-sources.js +132 -6
- package/dist/reader-result-contract.js +296 -2
- package/dist/repository-provider.js +608 -0
- package/dist/schema-entities.js +18 -18
- package/dist/service.js +7 -2
- package/dist/surface-mapper.js +53 -11
- package/dist/text-model.js +91 -8
- package/dist/whole-read/archive.js +196 -0
- package/dist/whole-read/checks.js +297 -0
- package/dist/whole-read/claims.js +196 -0
- package/dist/whole-read/coverage-ledger.js +51 -0
- package/dist/whole-read/decode.js +234 -0
- package/dist/whole-read/drafting.js +502 -0
- package/dist/whole-read/generated-rules.js +68 -0
- package/dist/whole-read/go-proof.js +255 -0
- package/dist/whole-read/grouping-prompt.js +19 -0
- package/dist/whole-read/grouping.js +247 -0
- package/dist/whole-read/identity.js +103 -0
- package/dist/whole-read/import-graph.js +293 -0
- package/dist/whole-read/inventory-collector.js +55 -0
- package/dist/whole-read/inventory-rules.js +141 -0
- package/dist/whole-read/lockfiles.js +947 -0
- package/dist/whole-read/notes.js +216 -0
- package/dist/whole-read/npm-proof.js +272 -0
- package/dist/whole-read/owner-decision-detectors.js +270 -0
- package/dist/whole-read/packing.js +612 -0
- package/dist/whole-read/pages.js +429 -0
- package/dist/whole-read/phase-policy.js +54 -0
- package/dist/whole-read/prd-build.js +279 -0
- package/dist/whole-read/prd-fidelity.js +14 -0
- package/dist/whole-read/python-proof.js +489 -0
- package/dist/whole-read/read-prompt.js +67 -0
- package/dist/whole-read/redaction.js +411 -0
- package/dist/whole-read/relationship-mapping.js +98 -0
- package/dist/whole-read/sanitize.js +266 -0
- package/dist/whole-read/secret-rules.js +26 -0
- package/dist/whole-read/structure.js +300 -0
- package/dist/whole-read/types.js +2 -0
- package/dist/whole-read-claims.js +12 -0
- package/dist/whole-read-git.js +340 -0
- package/dist/whole-read-pool.js +80 -0
- package/dist/whole-read-runner.js +945 -0
- package/dist/whole-read-shared-source.js +194 -0
- package/dist/whole-read-temp.js +479 -0
- package/package.json +8 -2
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from './identity.js';
|
|
3
|
+
import { sanitizeBuildMetadata } from './sanitize.js';
|
|
4
|
+
import { WHOLE_READ_PAGE_PROTOCOL, } from './types.js';
|
|
5
|
+
export const MAX_METADATA_PAGE_BYTES = 512 * 1024;
|
|
6
|
+
export const MAX_TRANSPORT_RECORD_BYTES = 16 * 1024;
|
|
7
|
+
const COLLECTION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
8
|
+
const SEMANTIC_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,95}$/;
|
|
9
|
+
const GENERATED_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/;
|
|
10
|
+
const RECORD_KIND = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
11
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
12
|
+
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
13
|
+
const COLLECTION_KINDS = new Set(['inventory', 'notes', 'groups', 'claims', 'progress']);
|
|
14
|
+
const CHILD_FIELDS = {
|
|
15
|
+
group: ['memberships', 'files'],
|
|
16
|
+
note: ['facts', 'exposes', 'uses'],
|
|
17
|
+
claim: ['citations'],
|
|
18
|
+
};
|
|
19
|
+
const CHILD_KINDS = {
|
|
20
|
+
memberships: 'membership', files: 'membership', facts: 'fact',
|
|
21
|
+
exposes: 'expose', uses: 'use', citations: 'citation',
|
|
22
|
+
};
|
|
23
|
+
function fail(code) { throw new Error(code); }
|
|
24
|
+
const byteCount = (value) => Buffer.byteLength(value, 'utf8');
|
|
25
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
26
|
+
const isJsonArray = (value) => Array.isArray(value);
|
|
27
|
+
function validCollectionId(value) {
|
|
28
|
+
return typeof value === 'string' && COLLECTION_ID.test(value);
|
|
29
|
+
}
|
|
30
|
+
function validSemanticId(value, generated = false) {
|
|
31
|
+
return typeof value === 'string' && (generated ? GENERATED_ID : SEMANTIC_ID).test(value);
|
|
32
|
+
}
|
|
33
|
+
function canonical(value) {
|
|
34
|
+
try {
|
|
35
|
+
return canonicalJson(value);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return fail('collection_invalid');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function framedDigest(version, frames) {
|
|
42
|
+
const hash = createHash('sha256');
|
|
43
|
+
for (const frame of [version, ...frames]) {
|
|
44
|
+
const bytes = Buffer.from(canonical(frame), 'utf8');
|
|
45
|
+
hash.update(String(bytes.byteLength));
|
|
46
|
+
hash.update(':');
|
|
47
|
+
hash.update(bytes);
|
|
48
|
+
hash.update(';');
|
|
49
|
+
}
|
|
50
|
+
return `sha256:${hash.digest('hex')}`;
|
|
51
|
+
}
|
|
52
|
+
function assertLogicalRecord(record) {
|
|
53
|
+
if (!validCollectionId(record.collectionId) || !COLLECTION_KINDS.has(record.collectionKind)
|
|
54
|
+
|| !validSemanticId(record.semanticId) || !RECORD_KIND.test(record.recordKind)
|
|
55
|
+
|| !isObject(record.payload))
|
|
56
|
+
fail('collection_invalid');
|
|
57
|
+
if (!sanitizeBuildMetadata(record.payload).ok)
|
|
58
|
+
fail('unsafe_metadata');
|
|
59
|
+
canonical(record.payload);
|
|
60
|
+
}
|
|
61
|
+
function childId(parentId, field, order) {
|
|
62
|
+
const id = `${parentId}:${field}:${order}`;
|
|
63
|
+
if (!validSemanticId(id, true))
|
|
64
|
+
fail('collection_invalid');
|
|
65
|
+
return id;
|
|
66
|
+
}
|
|
67
|
+
/** Normalize unbounded repeated fields before considering fragmentation. */
|
|
68
|
+
export function normalizeLogicalRecord(record) {
|
|
69
|
+
assertLogicalRecord(record);
|
|
70
|
+
const childFields = CHILD_FIELDS[record.recordKind] ?? [];
|
|
71
|
+
const payload = { ...record.payload };
|
|
72
|
+
const childSets = [];
|
|
73
|
+
const children = [];
|
|
74
|
+
for (const field of childFields) {
|
|
75
|
+
if (!(field in payload))
|
|
76
|
+
continue;
|
|
77
|
+
const entries = payload[field];
|
|
78
|
+
if (!isJsonArray(entries))
|
|
79
|
+
fail('collection_invalid');
|
|
80
|
+
delete payload[field];
|
|
81
|
+
childSets.push({ field, count: entries.length });
|
|
82
|
+
for (let order = 0; order < entries.length; order += 1) {
|
|
83
|
+
children.push({
|
|
84
|
+
type: 'child',
|
|
85
|
+
semanticId: childId(record.semanticId, field, order),
|
|
86
|
+
parentId: record.semanticId,
|
|
87
|
+
childKind: CHILD_KINDS[field],
|
|
88
|
+
field,
|
|
89
|
+
order,
|
|
90
|
+
payload: entries[order],
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const parent = {
|
|
95
|
+
type: 'logical', semanticId: record.semanticId, recordKind: record.recordKind,
|
|
96
|
+
payload, childSets,
|
|
97
|
+
};
|
|
98
|
+
return [parent, ...children];
|
|
99
|
+
}
|
|
100
|
+
function directSemanticId(record) {
|
|
101
|
+
return record.type === 'fragment' ? record.parentId : record.semanticId;
|
|
102
|
+
}
|
|
103
|
+
function fragmentsFor(record) {
|
|
104
|
+
const bytes = Buffer.from(canonical(record), 'utf8');
|
|
105
|
+
let chunkBytes = 12 * 1024;
|
|
106
|
+
while (chunkBytes > 0) {
|
|
107
|
+
const chunks = [];
|
|
108
|
+
for (let start = 0; start < bytes.byteLength;) {
|
|
109
|
+
let end = Math.min(bytes.byteLength, start + chunkBytes);
|
|
110
|
+
while (end < bytes.byteLength && end > start && (bytes[end] & 0xc0) === 0x80)
|
|
111
|
+
end -= 1;
|
|
112
|
+
if (end === start)
|
|
113
|
+
fail('record_too_large');
|
|
114
|
+
chunks.push(bytes.subarray(start, end));
|
|
115
|
+
start = end;
|
|
116
|
+
}
|
|
117
|
+
const fragments = chunks.map((chunk, fragmentIndex) => ({
|
|
118
|
+
type: 'fragment',
|
|
119
|
+
parentId: directSemanticId(record),
|
|
120
|
+
fragmentIndex,
|
|
121
|
+
fragmentCount: chunks.length,
|
|
122
|
+
payloadEncoding: 'base64',
|
|
123
|
+
payload: chunk.toString('base64'),
|
|
124
|
+
}));
|
|
125
|
+
const largest = Math.max(...fragments.map(transportRecordByteCount));
|
|
126
|
+
if (largest <= MAX_TRANSPORT_RECORD_BYTES)
|
|
127
|
+
return fragments;
|
|
128
|
+
chunkBytes -= Math.max(1, Math.ceil((largest - MAX_TRANSPORT_RECORD_BYTES) * 3 / 4) + 8);
|
|
129
|
+
}
|
|
130
|
+
return fail('record_too_large');
|
|
131
|
+
}
|
|
132
|
+
function boundedRecords(records) {
|
|
133
|
+
const result = [];
|
|
134
|
+
const semanticIds = new Set();
|
|
135
|
+
for (const record of records) {
|
|
136
|
+
if (record.type === 'fragment')
|
|
137
|
+
fail('collection_invalid');
|
|
138
|
+
const id = directSemanticId(record);
|
|
139
|
+
if (semanticIds.has(id))
|
|
140
|
+
fail('collection_invalid');
|
|
141
|
+
semanticIds.add(id);
|
|
142
|
+
if (transportRecordByteCount(record) <= MAX_TRANSPORT_RECORD_BYTES)
|
|
143
|
+
result.push(record);
|
|
144
|
+
else
|
|
145
|
+
result.push(...fragmentsFor(record));
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
export function transportRecordByteCount(record) {
|
|
150
|
+
return byteCount(canonical(record));
|
|
151
|
+
}
|
|
152
|
+
function pageDigestFields(page) {
|
|
153
|
+
return [{
|
|
154
|
+
collectionId: page.collectionId,
|
|
155
|
+
kind: page.kind,
|
|
156
|
+
pageIndex: page.pageIndex,
|
|
157
|
+
protocolVersion: page.protocolVersion,
|
|
158
|
+
recordCount: page.recordCount,
|
|
159
|
+
}, ...page.records];
|
|
160
|
+
}
|
|
161
|
+
export function metadataPageDigest(page) {
|
|
162
|
+
return framedDigest('whole-read-metadata-page-digest@1', pageDigestFields(page));
|
|
163
|
+
}
|
|
164
|
+
export function metadataPageByteCount(page) {
|
|
165
|
+
return byteCount(canonical(page));
|
|
166
|
+
}
|
|
167
|
+
function pageEnvelopeBytes(identity, pageIndex, recordCount) {
|
|
168
|
+
return metadataPageByteCount({
|
|
169
|
+
protocolVersion: WHOLE_READ_PAGE_PROTOCOL,
|
|
170
|
+
collectionId: identity.collectionId,
|
|
171
|
+
kind: identity.kind,
|
|
172
|
+
pageIndex,
|
|
173
|
+
recordCount,
|
|
174
|
+
records: [],
|
|
175
|
+
digest: `sha256:${'0'.repeat(64)}`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function makePage(identity, pageIndex, records) {
|
|
179
|
+
const fields = {
|
|
180
|
+
protocolVersion: WHOLE_READ_PAGE_PROTOCOL,
|
|
181
|
+
collectionId: identity.collectionId,
|
|
182
|
+
kind: identity.kind,
|
|
183
|
+
pageIndex,
|
|
184
|
+
recordCount: records.length,
|
|
185
|
+
records,
|
|
186
|
+
};
|
|
187
|
+
return { ...fields, digest: metadataPageDigest(fields) };
|
|
188
|
+
}
|
|
189
|
+
/** Encode records in declared order. An empty iterable intentionally produces zero pages. */
|
|
190
|
+
export function* encodeCollection(records) {
|
|
191
|
+
let identity;
|
|
192
|
+
const normalized = [];
|
|
193
|
+
const logicalIds = new Set();
|
|
194
|
+
for (const record of records) {
|
|
195
|
+
assertLogicalRecord(record);
|
|
196
|
+
identity ??= { collectionId: record.collectionId, kind: record.collectionKind };
|
|
197
|
+
if (record.collectionId !== identity.collectionId || record.collectionKind !== identity.kind
|
|
198
|
+
|| logicalIds.has(record.semanticId))
|
|
199
|
+
fail('collection_invalid');
|
|
200
|
+
logicalIds.add(record.semanticId);
|
|
201
|
+
normalized.push(...normalizeLogicalRecord(record));
|
|
202
|
+
}
|
|
203
|
+
if (!identity)
|
|
204
|
+
return;
|
|
205
|
+
const transport = boundedRecords(normalized);
|
|
206
|
+
let pageIndex = 0;
|
|
207
|
+
let current = [];
|
|
208
|
+
let currentRecordBytes = 0;
|
|
209
|
+
for (const record of transport) {
|
|
210
|
+
const recordBytes = transportRecordByteCount(record);
|
|
211
|
+
const nextCount = current.length + 1;
|
|
212
|
+
const nextBytes = pageEnvelopeBytes(identity, pageIndex, nextCount)
|
|
213
|
+
+ currentRecordBytes + recordBytes + (nextCount > 1 ? nextCount - 1 : 0);
|
|
214
|
+
if (nextBytes > MAX_METADATA_PAGE_BYTES && current.length > 0) {
|
|
215
|
+
yield makePage(identity, pageIndex, current);
|
|
216
|
+
pageIndex += 1;
|
|
217
|
+
current = [];
|
|
218
|
+
currentRecordBytes = 0;
|
|
219
|
+
}
|
|
220
|
+
const emptyPageBytes = pageEnvelopeBytes(identity, pageIndex, 1) + recordBytes;
|
|
221
|
+
if (emptyPageBytes > MAX_METADATA_PAGE_BYTES)
|
|
222
|
+
fail('page_too_large');
|
|
223
|
+
current.push(record);
|
|
224
|
+
currentRecordBytes += recordBytes;
|
|
225
|
+
}
|
|
226
|
+
if (current.length > 0)
|
|
227
|
+
yield makePage(identity, pageIndex, current);
|
|
228
|
+
}
|
|
229
|
+
function assertTransportRecord(record) {
|
|
230
|
+
if (!isObject(record) || typeof record.type !== 'string')
|
|
231
|
+
fail('collection_invalid');
|
|
232
|
+
if (record.type === 'logical') {
|
|
233
|
+
if (Object.keys(record).sort().join(',') !== 'childSets,payload,recordKind,semanticId,type'
|
|
234
|
+
|| !validSemanticId(record.semanticId, true) || !RECORD_KIND.test(String(record.recordKind))
|
|
235
|
+
|| !isObject(record.payload) || !Array.isArray(record.childSets))
|
|
236
|
+
fail('collection_invalid');
|
|
237
|
+
const fields = new Set();
|
|
238
|
+
for (const childSet of record.childSets) {
|
|
239
|
+
if (!isObject(childSet) || Object.keys(childSet).sort().join(',') !== 'count,field'
|
|
240
|
+
|| typeof childSet.field !== 'string'
|
|
241
|
+
|| !(childSet.field in CHILD_KINDS) || fields.has(childSet.field)
|
|
242
|
+
|| !Number.isSafeInteger(childSet.count) || Number(childSet.count) < 0)
|
|
243
|
+
fail('collection_invalid');
|
|
244
|
+
fields.add(childSet.field);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if (record.type === 'child') {
|
|
248
|
+
if (Object.keys(record).sort().join(',') !== 'childKind,field,order,parentId,payload,semanticId,type'
|
|
249
|
+
|| !validSemanticId(record.semanticId, true) || !validSemanticId(record.parentId)
|
|
250
|
+
|| typeof record.field !== 'string' || CHILD_KINDS[record.field] !== record.childKind
|
|
251
|
+
|| !Number.isSafeInteger(record.order) || Number(record.order) < 0
|
|
252
|
+
|| !('payload' in record))
|
|
253
|
+
fail('collection_invalid');
|
|
254
|
+
}
|
|
255
|
+
else if (record.type === 'fragment') {
|
|
256
|
+
if (Object.keys(record).sort().join(',') !== 'fragmentCount,fragmentIndex,parentId,payload,payloadEncoding,type'
|
|
257
|
+
|| !validSemanticId(record.parentId, true) || !Number.isSafeInteger(record.fragmentIndex)
|
|
258
|
+
|| Number(record.fragmentIndex) < 0 || !Number.isSafeInteger(record.fragmentCount)
|
|
259
|
+
|| Number(record.fragmentCount) < 1 || Number(record.fragmentIndex) >= Number(record.fragmentCount)
|
|
260
|
+
|| record.payloadEncoding !== 'base64' || typeof record.payload !== 'string'
|
|
261
|
+
|| !BASE64.test(record.payload))
|
|
262
|
+
fail('collection_invalid');
|
|
263
|
+
let decoded;
|
|
264
|
+
try {
|
|
265
|
+
decoded = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.from(record.payload, 'base64'));
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return fail('collection_invalid');
|
|
269
|
+
}
|
|
270
|
+
if (!sanitizeBuildMetadata(decoded).ok)
|
|
271
|
+
fail('unsafe_metadata');
|
|
272
|
+
}
|
|
273
|
+
else
|
|
274
|
+
fail('collection_invalid');
|
|
275
|
+
if (!sanitizeBuildMetadata(record).ok)
|
|
276
|
+
fail('unsafe_metadata');
|
|
277
|
+
canonical(record);
|
|
278
|
+
}
|
|
279
|
+
export function validateMetadataPage(page) {
|
|
280
|
+
if (!isObject(page) || page.protocolVersion !== WHOLE_READ_PAGE_PROTOCOL
|
|
281
|
+
|| !validCollectionId(page.collectionId) || !COLLECTION_KINDS.has(page.kind)
|
|
282
|
+
|| !Number.isSafeInteger(page.pageIndex) || page.pageIndex < 0
|
|
283
|
+
|| !Number.isSafeInteger(page.recordCount) || page.recordCount < 1
|
|
284
|
+
|| !Array.isArray(page.records) || page.recordCount !== page.records.length
|
|
285
|
+
|| !DIGEST.test(page.digest))
|
|
286
|
+
fail('collection_invalid');
|
|
287
|
+
if (Object.keys(page).sort().join(',') !== 'collectionId,digest,kind,pageIndex,protocolVersion,recordCount,records'
|
|
288
|
+
|| !sanitizeBuildMetadata(page).ok)
|
|
289
|
+
fail('unsafe_metadata');
|
|
290
|
+
for (const record of page.records) {
|
|
291
|
+
assertTransportRecord(record);
|
|
292
|
+
if (transportRecordByteCount(record) > MAX_TRANSPORT_RECORD_BYTES)
|
|
293
|
+
fail('record_too_large');
|
|
294
|
+
}
|
|
295
|
+
if (metadataPageByteCount(page) > MAX_METADATA_PAGE_BYTES)
|
|
296
|
+
fail('page_too_large');
|
|
297
|
+
const { digest: _digest, ...fields } = page;
|
|
298
|
+
if (metadataPageDigest(fields) !== page.digest)
|
|
299
|
+
fail('collection_digest_mismatch');
|
|
300
|
+
}
|
|
301
|
+
function orderedPages(pages) {
|
|
302
|
+
const ordered = [...pages].sort((left, right) => left.pageIndex - right.pageIndex);
|
|
303
|
+
if (ordered.length === 0)
|
|
304
|
+
return ordered;
|
|
305
|
+
const identity = { collectionId: ordered[0].collectionId, kind: ordered[0].kind };
|
|
306
|
+
for (let index = 0; index < ordered.length; index += 1) {
|
|
307
|
+
const page = ordered[index];
|
|
308
|
+
if (page.pageIndex !== index)
|
|
309
|
+
fail('collection_incomplete');
|
|
310
|
+
validateMetadataPage(page);
|
|
311
|
+
if (page.collectionId !== identity.collectionId || page.kind !== identity.kind)
|
|
312
|
+
fail('collection_invalid');
|
|
313
|
+
}
|
|
314
|
+
return ordered;
|
|
315
|
+
}
|
|
316
|
+
export function collectionDigest(pages, emptyIdentity) {
|
|
317
|
+
const ordered = orderedPages(pages);
|
|
318
|
+
const identity = ordered.length > 0
|
|
319
|
+
? { collectionId: ordered[0].collectionId, kind: ordered[0].kind }
|
|
320
|
+
: emptyIdentity;
|
|
321
|
+
if (!identity || !validCollectionId(identity.collectionId) || !COLLECTION_KINDS.has(identity.kind)) {
|
|
322
|
+
return fail('collection_invalid');
|
|
323
|
+
}
|
|
324
|
+
return framedDigest('whole-read-collection-digest@1', [
|
|
325
|
+
{ ...identity, pageCount: ordered.length },
|
|
326
|
+
...ordered.map((page) => ({
|
|
327
|
+
digest: page.digest, pageIndex: page.pageIndex, recordCount: page.recordCount,
|
|
328
|
+
})),
|
|
329
|
+
]);
|
|
330
|
+
}
|
|
331
|
+
function decodedFragments(records) {
|
|
332
|
+
const result = [];
|
|
333
|
+
const fragments = new Map();
|
|
334
|
+
const emitted = new Set();
|
|
335
|
+
for (const record of records) {
|
|
336
|
+
if (record.type !== 'fragment') {
|
|
337
|
+
result.push(record);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const group = fragments.get(record.parentId) ?? [];
|
|
341
|
+
group.push(record);
|
|
342
|
+
fragments.set(record.parentId, group);
|
|
343
|
+
if (emitted.has(record.parentId))
|
|
344
|
+
continue;
|
|
345
|
+
emitted.add(record.parentId);
|
|
346
|
+
result.push({ type: 'logical', semanticId: record.parentId, recordKind: 'fragment_placeholder', payload: {}, childSets: [] });
|
|
347
|
+
}
|
|
348
|
+
for (let index = 0; index < result.length; index += 1) {
|
|
349
|
+
const placeholder = result[index];
|
|
350
|
+
if (placeholder.type !== 'logical' || placeholder.recordKind !== 'fragment_placeholder')
|
|
351
|
+
continue;
|
|
352
|
+
const parentId = placeholder.semanticId;
|
|
353
|
+
const group = fragments.get(parentId) ?? [];
|
|
354
|
+
const expected = group[0]?.fragmentCount;
|
|
355
|
+
if (!expected || group.length !== expected)
|
|
356
|
+
fail('collection_incomplete');
|
|
357
|
+
group.sort((left, right) => left.fragmentIndex - right.fragmentIndex);
|
|
358
|
+
if (group.some((fragment, fragmentIndex) => fragment.fragmentIndex !== fragmentIndex
|
|
359
|
+
|| fragment.fragmentCount !== expected))
|
|
360
|
+
fail('collection_incomplete');
|
|
361
|
+
let parsed;
|
|
362
|
+
try {
|
|
363
|
+
parsed = JSON.parse(Buffer.concat(group.map((fragment) => Buffer.from(fragment.payload, 'base64'))).toString('utf8'));
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
fail('collection_invalid');
|
|
367
|
+
}
|
|
368
|
+
assertTransportRecord(parsed);
|
|
369
|
+
if (parsed.type === 'fragment' || directSemanticId(parsed) !== parentId)
|
|
370
|
+
fail('collection_invalid');
|
|
371
|
+
result[index] = parsed;
|
|
372
|
+
}
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
export function decodeCollection(pages, expectedDigest) {
|
|
376
|
+
const ordered = orderedPages(pages);
|
|
377
|
+
if (ordered.length === 0)
|
|
378
|
+
return [];
|
|
379
|
+
if (expectedDigest !== undefined && collectionDigest(ordered) !== expectedDigest) {
|
|
380
|
+
fail('collection_digest_mismatch');
|
|
381
|
+
}
|
|
382
|
+
const records = decodedFragments(ordered.flatMap((page) => page.records));
|
|
383
|
+
const semanticIds = new Set();
|
|
384
|
+
const parents = [];
|
|
385
|
+
const children = new Map();
|
|
386
|
+
for (const record of records) {
|
|
387
|
+
if (semanticIds.has(record.semanticId))
|
|
388
|
+
fail('collection_invalid');
|
|
389
|
+
semanticIds.add(record.semanticId);
|
|
390
|
+
if (record.type === 'logical')
|
|
391
|
+
parents.push(record);
|
|
392
|
+
else {
|
|
393
|
+
const entries = children.get(record.parentId) ?? [];
|
|
394
|
+
entries.push(record);
|
|
395
|
+
children.set(record.parentId, entries);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const consumedChildren = new Set();
|
|
399
|
+
const identity = ordered[0];
|
|
400
|
+
const logical = parents.map((parent) => {
|
|
401
|
+
const payload = { ...parent.payload };
|
|
402
|
+
const parentChildren = children.get(parent.semanticId) ?? [];
|
|
403
|
+
for (const childSet of parent.childSets) {
|
|
404
|
+
if (childSet.field in payload)
|
|
405
|
+
fail('collection_invalid');
|
|
406
|
+
const matching = parentChildren.filter((child) => child.field === childSet.field)
|
|
407
|
+
.sort((left, right) => left.order - right.order);
|
|
408
|
+
if (matching.length !== childSet.count
|
|
409
|
+
|| matching.some((child, order) => child.order !== order
|
|
410
|
+
|| child.semanticId !== childId(parent.semanticId, childSet.field, order))) {
|
|
411
|
+
fail('collection_invalid');
|
|
412
|
+
}
|
|
413
|
+
payload[childSet.field] = matching.map((child) => child.payload);
|
|
414
|
+
for (const child of matching)
|
|
415
|
+
consumedChildren.add(child.semanticId);
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
collectionId: identity.collectionId,
|
|
419
|
+
collectionKind: identity.kind,
|
|
420
|
+
semanticId: parent.semanticId,
|
|
421
|
+
recordKind: parent.recordKind,
|
|
422
|
+
payload,
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
if ([...children.values()].flat().some((child) => !consumedChildren.has(child.semanticId))) {
|
|
426
|
+
fail('collection_invalid');
|
|
427
|
+
}
|
|
428
|
+
return logical;
|
|
429
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Consent-frozen settings and reservation envelopes for every whole-read model phase. */
|
|
2
|
+
export const WHOLE_READ_PHASE_POLICY = {
|
|
3
|
+
read: { model: 'claude-haiku-5-5', fallbackModel: 'claude-sonnet-5-5', effort: 'low',
|
|
4
|
+
thinking: { 'claude-haiku-5-5': 'disabled', 'claude-sonnet-5-5': 'between_tools' },
|
|
5
|
+
maxOutputTokens: 16_000, thinkingAllowanceTokens: 0, perItemOutputTokens: 512 },
|
|
6
|
+
// 16,384 - 2,048 thinking leaves 14,336 answer tokens. At 20 per card (a short number plus
|
|
7
|
+
// punctuation and amortized group-label overhead), one unit can return 716 cards; 1,200 fit in two.
|
|
8
|
+
grouping: { model: 'claude-opus-5-5', fallbackModel: 'claude-sonnet-5-5', effort: 'low',
|
|
9
|
+
thinking: { 'claude-opus-5-5': 'adaptive', 'claude-sonnet-5-5': 'adaptive' },
|
|
10
|
+
maxOutputTokens: 16_384, thinkingAllowanceTokens: 2_048, perItemOutputTokens: 20 },
|
|
11
|
+
// 24,576 leaves medium effort 8,192 thinking tokens once, then fits main's 16 files.
|
|
12
|
+
drafting_leaf: { model: 'claude-sonnet-5-5', fallbackModel: 'claude-opus-5-5', effort: 'medium',
|
|
13
|
+
thinking: { 'claude-sonnet-5-5': 'adaptive', 'claude-opus-5-5': 'adaptive' },
|
|
14
|
+
maxOutputTokens: 24_576, thinkingAllowanceTokens: 8_192, perItemOutputTokens: 1_024 },
|
|
15
|
+
drafting_merge: { model: 'claude-sonnet-5-5', fallbackModel: 'claude-opus-5-5', effort: 'medium',
|
|
16
|
+
thinking: { 'claude-sonnet-5-5': 'adaptive', 'claude-opus-5-5': 'adaptive' },
|
|
17
|
+
maxOutputTokens: 24_576, thinkingAllowanceTokens: 8_192, perItemOutputTokens: 1_024 },
|
|
18
|
+
support_check: { model: 'claude-sonnet-5-5', fallbackModel: 'claude-opus-5-5', effort: 'low',
|
|
19
|
+
thinking: { 'claude-sonnet-5-5': 'between_tools', 'claude-opus-5-5': 'adaptive' },
|
|
20
|
+
maxOutputTokens: 4_096, thinkingAllowanceTokens: 0, perItemOutputTokens: 512 },
|
|
21
|
+
// The 10,240 cap preserves main's 16-candidate view after Opus's one-time allowance.
|
|
22
|
+
relationships: { model: 'claude-opus-5-5', fallbackModel: 'claude-sonnet-5-5', effort: 'low',
|
|
23
|
+
thinking: { 'claude-opus-5-5': 'adaptive', 'claude-sonnet-5-5': 'adaptive' },
|
|
24
|
+
maxOutputTokens: 10_240, thinkingAllowanceTokens: 2_048, perItemOutputTokens: 512 },
|
|
25
|
+
// The 34,048 cap preserves main's 200-item reconciliation units plus one Opus allowance.
|
|
26
|
+
reconciliation: { model: 'claude-opus-5-5', fallbackModel: 'claude-sonnet-5-5', effort: 'low',
|
|
27
|
+
thinking: { 'claude-opus-5-5': 'adaptive', 'claude-sonnet-5-5': 'adaptive' },
|
|
28
|
+
maxOutputTokens: 34_048, thinkingAllowanceTokens: 2_048, perItemOutputTokens: 160 },
|
|
29
|
+
// PRD answer limits stay 4k/16k/8k; each request adds medium adaptive thinking once per unit.
|
|
30
|
+
prd_lanes: { model: 'claude-sonnet-5-5', fallbackModel: 'claude-opus-5-5', effort: 'medium',
|
|
31
|
+
thinking: { 'claude-sonnet-5-5': 'adaptive', 'claude-opus-5-5': 'adaptive' },
|
|
32
|
+
maxOutputTokens: 12_288, thinkingAllowanceTokens: 8_192, perItemOutputTokens: 4_096 },
|
|
33
|
+
prd_draft: { model: 'claude-sonnet-5-5', fallbackModel: 'claude-opus-5-5', effort: 'medium',
|
|
34
|
+
thinking: { 'claude-sonnet-5-5': 'adaptive', 'claude-opus-5-5': 'adaptive' },
|
|
35
|
+
maxOutputTokens: 24_576, thinkingAllowanceTokens: 8_192, perItemOutputTokens: 16_384 },
|
|
36
|
+
prd_check: { model: 'claude-sonnet-5-5', fallbackModel: 'claude-opus-5-5', effort: 'medium',
|
|
37
|
+
thinking: { 'claude-sonnet-5-5': 'adaptive', 'claude-opus-5-5': 'adaptive' },
|
|
38
|
+
maxOutputTokens: 16_384, thinkingAllowanceTokens: 8_192, perItemOutputTokens: 8_192 },
|
|
39
|
+
};
|
|
40
|
+
export function phaseItemsPerUnit(policy) {
|
|
41
|
+
return Math.floor((policy.maxOutputTokens - policy.thinkingAllowanceTokens) / policy.perItemOutputTokens);
|
|
42
|
+
}
|
|
43
|
+
export function phaseUnitOutputTokens(policy, itemCount) {
|
|
44
|
+
return Math.min(policy.maxOutputTokens, policy.thinkingAllowanceTokens + itemCount * policy.perItemOutputTokens);
|
|
45
|
+
}
|
|
46
|
+
export function anthropicThinking(phase, model) {
|
|
47
|
+
const thinking = WHOLE_READ_PHASE_POLICY[phase].thinking[model];
|
|
48
|
+
if (!thinking)
|
|
49
|
+
throw new TypeError('whole_read_model_policy_invalid');
|
|
50
|
+
return { type: thinking };
|
|
51
|
+
}
|
|
52
|
+
export function prdPolicyPhase(phase) {
|
|
53
|
+
return `prd_${phase}`;
|
|
54
|
+
}
|