@feng0531/dsh-code-review 0.85.13 → 0.86.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/CHANGELOG.md +15 -0
- package/CORE-ALIGNMENT-TASKS.md +8 -2
- package/CORE-SOURCE-INVENTORY.md +4 -2
- package/README.md +17 -7
- package/RELEASE-NOTES.md +12 -17
- package/index.mjs +2 -2
- package/package.json +1 -1
- package/src/background-review.mjs +51 -1
- package/src/coverage-plan.mjs +16 -2
- package/src/dsh-model.mjs +80 -15
- package/src/group-primary.mjs +40 -0
- package/src/input-budget.mjs +2 -0
- package/src/model-contract.mjs +1 -0
- package/src/model-json.mjs +23 -1
- package/src/model-tools.mjs +58 -0
- package/src/model-wire-budget.mjs +32 -0
- package/src/primary-seeds.mjs +7 -2
- package/src/project-execution.mjs +3 -2
- package/src/project-review-service.mjs +47 -10
- package/src/retrieval-loop.mjs +12 -10
- package/src/review-artifacts.mjs +117 -0
- package/src/review-diagnostics.mjs +21 -9
- package/src/review-jobs.mjs +6 -2
- package/src/review-reader-allocation.mjs +75 -0
- package/src/review-reader-policy.mjs +1 -1
- package/src/review-resume.mjs +53 -0
- package/src/review.mjs +185 -54
- package/src/search-seeds.mjs +2 -1
- package/src/service.mjs +25 -8
- package/src/snapshot-reader.mjs +62 -22
- package/src/verification-loop.mjs +2 -1
- package/src/verification-seeds.mjs +18 -1
package/src/snapshot-reader.mjs
CHANGED
|
@@ -21,15 +21,17 @@ function budgetError(kind) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// This capability reads only caller-approved text entries, never the filesystem.
|
|
24
|
-
export function createSnapshotReader(snapshotId, entries, { maxBytes = 4 * 1024 * 1024, maxCalls = 50, maxOutputBytes = 256 * 1024, signal } = {}) {
|
|
24
|
+
export function createSnapshotReader(snapshotId, entries, { maxBytes = 4 * 1024 * 1024, maxCalls = 50, maxOutputBytes = 256 * 1024, signal, reuseResults = false } = {}) {
|
|
25
25
|
if (typeof snapshotId !== 'string' || !snapshotId || snapshotId.length > 200) throw new Error('Invalid snapshot identity');
|
|
26
26
|
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 16 * 1024 * 1024) throw new Error('Invalid reader budget');
|
|
27
27
|
if (!Array.isArray(entries) || entries.length > 500) throw new Error('Invalid reader entries');
|
|
28
28
|
for (const [value, cap] of [[maxCalls, 1000], [maxOutputBytes, 4 * 1024 * 1024]]) {
|
|
29
29
|
if (!Number.isSafeInteger(value) || value < 1 || value > cap) throw new Error('Invalid reader budget');
|
|
30
30
|
}
|
|
31
|
+
if (typeof reuseResults !== 'boolean') throw new Error('Invalid reader reuse option');
|
|
31
32
|
signal?.throwIfAborted();
|
|
32
33
|
|
|
34
|
+
let cache = new Map(), cacheBytes = 0, cacheHits = 0, deliveries = 0;
|
|
33
35
|
let calls = 0, outputBytes = 0, inTransaction = false;
|
|
34
36
|
const begin = () => {
|
|
35
37
|
signal?.throwIfAborted();
|
|
@@ -103,16 +105,66 @@ export function createSnapshotReader(snapshotId, entries, { maxBytes = 4 * 1024
|
|
|
103
105
|
return { snapshotId, sourceId: id, hash: source.hash, start, count: selectedCount, text, endOfSource: endOffset === source.text.length };
|
|
104
106
|
};
|
|
105
107
|
|
|
108
|
+
const validate = (kind, request) => {
|
|
109
|
+
if (!request || typeof request !== 'object') throw new Error('Invalid reader request');
|
|
110
|
+
if (kind === 'read') {
|
|
111
|
+
if (!sources.has(request.id)) throw new Error('Source not authorized');
|
|
112
|
+
if (!Number.isSafeInteger(request.start) || request.start < 1 || !Number.isSafeInteger(request.count) || request.count < 1 || request.count > 200) throw new Error('Invalid line range');
|
|
113
|
+
} else {
|
|
114
|
+
if (!validQuery(request.query, request.limit === undefined ? 20 : request.limit)) throw new Error('Invalid query request');
|
|
115
|
+
if (kind === 'search' && request.sourceIds !== undefined && (!Array.isArray(request.sourceIds) || !request.sourceIds.length || request.sourceIds.length > 20 || new Set(request.sourceIds).size !== request.sourceIds.length || request.sourceIds.some(id => !sources.has(id)))) throw new Error('Invalid search source filter');
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const keyOf = (kind, request) => JSON.stringify(kind === 'read' ? [kind, request.id, request.start, request.count] : kind === 'find' ? [kind, request.query, request.limit ?? 20] : [kind, request.query, request.limit ?? 20, request.sourceIds === undefined ? null : [...request.sourceIds].sort()]);
|
|
119
|
+
const materialize = (kind, request) => kind === 'read' ? read(request) : kind === 'find' ? find(request) : search(request);
|
|
120
|
+
const deliver = (kind, request, decorate = value => value) => {
|
|
121
|
+
signal?.throwIfAborted();
|
|
122
|
+
// Validation is never bypassed by canonicalization (e.g. duplicate filters).
|
|
123
|
+
if (reuseResults) validate(kind, request);
|
|
124
|
+
const key = reuseResults ? keyOf(kind, request) : null;
|
|
125
|
+
const cached = reuseResults && cache.get(key);
|
|
126
|
+
if (!cached) begin();
|
|
127
|
+
const raw = cached ? structuredClone(cached.result) : materialize(kind, request);
|
|
128
|
+
const result = finish(decorate(structuredClone(raw)));
|
|
129
|
+
if (reuseResults) {
|
|
130
|
+
deliveries++;
|
|
131
|
+
if (cached) cacheHits++;
|
|
132
|
+
else {
|
|
133
|
+
const size = resultSize(raw);
|
|
134
|
+
if (cache.size < maxCalls && cacheBytes + size <= maxOutputBytes) { cache.set(key, { result: structuredClone(raw), size }); cacheBytes += size; }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
};
|
|
139
|
+
const preflight = requests => {
|
|
140
|
+
signal?.throwIfAborted();
|
|
141
|
+
if (!Array.isArray(requests) || !requests.length || requests.length > 8) throw new Error('Invalid retrieval batch');
|
|
142
|
+
const staged = new Map(cache); let stagedBytes = cacheBytes, needed = 0, size = 0, hits = 0;
|
|
143
|
+
for (const request of requests) {
|
|
144
|
+
if (!['read', 'find', 'search'].includes(request?.kind)) throw new Error('Unsupported retrieval operation');
|
|
145
|
+
// Dry-run validates against immutable approved sources; it grants no receipts.
|
|
146
|
+
validate(request.kind, request);
|
|
147
|
+
const key = reuseResults ? keyOf(request.kind, request) : null;
|
|
148
|
+
const raw = reuseResults && staged.has(key) ? staged.get(key).result : materialize(request.kind, request), bytes = resultSize(raw);
|
|
149
|
+
if (reuseResults && staged.has(key)) hits++;
|
|
150
|
+
else { needed++; if (reuseResults && staged.size < maxCalls && stagedBytes + bytes <= maxOutputBytes) { staged.set(key, { result: raw, size: bytes }); stagedBytes += bytes; } }
|
|
151
|
+
size += bytes;
|
|
152
|
+
}
|
|
153
|
+
if (calls + needed > maxCalls) throw budgetError('call');
|
|
154
|
+
if (outputBytes + size > maxOutputBytes) throw budgetError('output');
|
|
155
|
+
return { calls: needed, outputBytes: size, cacheHits: hits };
|
|
156
|
+
};
|
|
106
157
|
return Object.freeze({
|
|
107
|
-
usage: () => ({ calls, outputBytes }),
|
|
108
|
-
budget: () => ({ maxCalls, maxOutputBytes, remainingCalls: maxCalls - calls, remainingOutputBytes: maxOutputBytes - outputBytes, maxResultBytes: 64 * 1024 }),
|
|
158
|
+
usage: () => ({ calls, outputBytes, ...(reuseResults ? { cacheHits, deliveries, cacheEntries: cache.size, cacheBytes } : {}) }),
|
|
159
|
+
budget: () => ({ maxCalls, maxOutputBytes, remainingCalls: maxCalls - calls, remainingOutputBytes: maxOutputBytes - outputBytes, maxResultBytes: 64 * 1024, ...(reuseResults ? { reusableResults: cache.size > 0 } : {}) }),
|
|
160
|
+
canServe(requests) { try { preflight(requests); return true; } catch { return false; } },
|
|
109
161
|
// A synchronous host-only batch commits its ledger only as a whole. This
|
|
110
162
|
// also covers decorated response bytes that preflight cannot predict.
|
|
111
163
|
// Callers must not publish results/audit records before this returns.
|
|
112
164
|
transaction(execute) {
|
|
113
165
|
signal?.throwIfAborted();
|
|
114
166
|
if (inTransaction || typeof execute !== 'function') throw new Error('Invalid reader transaction');
|
|
115
|
-
const previousCalls = calls, previousBytes = outputBytes;
|
|
167
|
+
const previousCalls = calls, previousBytes = outputBytes, previousCache = new Map(cache), previousCacheBytes = cacheBytes, previousHits = cacheHits, previousDeliveries = deliveries;
|
|
116
168
|
inTransaction = true;
|
|
117
169
|
try {
|
|
118
170
|
const result = execute();
|
|
@@ -120,29 +172,17 @@ export function createSnapshotReader(snapshotId, entries, { maxBytes = 4 * 1024
|
|
|
120
172
|
signal?.throwIfAborted();
|
|
121
173
|
return result;
|
|
122
174
|
} catch (error) {
|
|
123
|
-
calls = previousCalls; outputBytes = previousBytes;
|
|
175
|
+
calls = previousCalls; outputBytes = previousBytes; cache = previousCache; cacheBytes = previousCacheBytes; cacheHits = previousHits; deliveries = previousDeliveries;
|
|
124
176
|
throw error;
|
|
125
177
|
} finally { inTransaction = false; }
|
|
126
178
|
},
|
|
127
179
|
// Host-only dry run. Validate ALL ranges and sources before charging anything.
|
|
128
180
|
// No evidence or receipts escape this preflight. Decorated reads are still
|
|
129
181
|
// measured by finish; this never reserves or increases the shared budget.
|
|
130
|
-
preflight
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (request?.kind === 'find') return find(request);
|
|
136
|
-
if (request?.kind === 'search') return search(request);
|
|
137
|
-
throw new Error('Unsupported retrieval operation');
|
|
138
|
-
});
|
|
139
|
-
if (calls + requests.length > maxCalls) throw budgetError('call');
|
|
140
|
-
const size = results.reduce((sum, result) => sum + resultSize(result), 0);
|
|
141
|
-
if (outputBytes + size > maxOutputBytes) throw budgetError('output');
|
|
142
|
-
},
|
|
143
|
-
find(request) { begin(); return finish(find(request)); },
|
|
144
|
-
search(request) { begin(); return finish(search(request)); },
|
|
145
|
-
// decorate is a host-only callback; model requests never supply it.
|
|
146
|
-
read(request, decorate = value => value) { begin(); return finish(decorate(read(request))); },
|
|
182
|
+
preflight,
|
|
183
|
+
find(request) { return deliver('find', request); },
|
|
184
|
+
search(request) { return deliver('search', request); },
|
|
185
|
+
// Only immutable raw text is cached; each delivery creates fresh decorations.
|
|
186
|
+
read(request, decorate) { return deliver('read', request, decorate); },
|
|
147
187
|
});
|
|
148
188
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { VERIFICATION_INSTRUCTIONS } from './prompts.mjs';
|
|
2
2
|
import { CONTEXT_SIDE_NOTICE } from './context-identity.mjs';
|
|
3
3
|
import { defectGuidance } from './defect-guidance.mjs';
|
|
4
|
+
import { modelWireBudget } from './model-wire-budget.mjs';
|
|
4
5
|
import { createEvidenceReceipts } from './evidence-receipts.mjs';
|
|
5
6
|
import { retrievalLoop, retrievalRequest } from './retrieval-loop.mjs';
|
|
6
7
|
import { verificationSeeds } from './verification-seeds.mjs';
|
|
@@ -28,7 +29,7 @@ export async function verificationLoop(model, request, candidates, scope, option
|
|
|
28
29
|
onReceipt: (id, resolve) => seedReceipts.set(id, resolve),
|
|
29
30
|
fits: seeds => {
|
|
30
31
|
const next = retrievalRequest(verificationRequest, scope, seeds);
|
|
31
|
-
return Buffer.byteLength(next.input) + Buffer.byteLength(next.instructions) <= (options.maxInputBytes ?? 96 * 1024);
|
|
32
|
+
return request.nativeOutputTools === true ? modelWireBudget(next, options.maxInputBytes ?? 96 * 1024).fits : Buffer.byteLength(next.input) + Buffer.byteLength(next.instructions) <= (options.maxInputBytes ?? 96 * 1024);
|
|
32
33
|
},
|
|
33
34
|
});
|
|
34
35
|
const receipts = createEvidenceReceipts(scope.read, { limit: 50 - seedReceipts.size });
|
|
@@ -35,8 +35,10 @@ export function verificationSeeds(candidates, scope, { signal, fits, onReceipt =
|
|
|
35
35
|
const budget = scope.budget?.();
|
|
36
36
|
// At least one exact final validation read per candidate must remain. This
|
|
37
37
|
// reserves no new authority; additional evidence can still exhaust budgets.
|
|
38
|
-
if (budget &&
|
|
38
|
+
if (budget && budget.remainingOutputBytes <= finalEvidenceBytes) break;
|
|
39
39
|
try {
|
|
40
|
+
const cached = budget?.reusableResults === true && scope.preflight?.([request])?.calls === 0 && scope.canServe?.([request]) === true;
|
|
41
|
+
if (budget && !cached && budget.remainingCalls <= candidates.length) continue;
|
|
40
42
|
// Failed transactions discard their receipt table too, not just the ledger.
|
|
41
43
|
const receipts = createEvidenceReceipts(scope.read, { limit: 1 });
|
|
42
44
|
const item = scope.transaction(() => {
|
|
@@ -48,7 +50,22 @@ export function verificationSeeds(candidates, scope, { signal, fits, onReceipt =
|
|
|
48
50
|
const proposed = [...seeds, { request, result }];
|
|
49
51
|
const nextBudget = scope.budget?.();
|
|
50
52
|
const reserve = finalEvidenceBytes + Buffer.byteLength(JSON.stringify(result)) * references;
|
|
53
|
+
if (cached) {
|
|
54
|
+
// Validation reads the actual returned count, not the seed's requested
|
|
55
|
+
// EOF-clamped count. These are different exact cache keys. Reserve all
|
|
56
|
+
// final reads explicitly before admitting a zero-call cached seed.
|
|
57
|
+
let calls = 0;
|
|
58
|
+
for (const item of proposed) {
|
|
59
|
+
const validation = { kind: 'read', id: item.result.sourceId, start: item.result.start, count: item.result.count };
|
|
60
|
+
const cost = scope.preflight([validation]);
|
|
61
|
+
if (!Number.isSafeInteger(cost?.calls) || cost.calls < 0) throw capacityError();
|
|
62
|
+
const matching = [...ranges.values()].find(range => JSON.stringify(range.request) === JSON.stringify(item.request));
|
|
63
|
+
calls += cost.calls * (matching?.references ?? 1);
|
|
64
|
+
}
|
|
65
|
+
if (nextBudget && calls > nextBudget.remainingCalls) throw capacityError();
|
|
66
|
+
}
|
|
51
67
|
if (Buffer.byteLength(JSON.stringify(proposed)) > MAX_SEED_BYTES || !fits(proposed) || (nextBudget && nextBudget.remainingOutputBytes < reserve)) throw capacityError();
|
|
68
|
+
signal?.throwIfAborted();
|
|
52
69
|
return { request, result };
|
|
53
70
|
});
|
|
54
71
|
// Publish only after the approved reader has committed the transaction.
|