@feng0531/dsh-code-review 0.88.0 → 0.90.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/CHANGELOG.md +14 -0
- package/README.md +15 -1
- package/RELEASE-NOTES.md +10 -12
- package/evaluation/README.md +6 -0
- package/index.mjs +9 -2
- package/package.json +2 -2
- package/src/approved-retrieval.mjs +13 -0
- package/src/background-review.mjs +1 -0
- package/src/child-output-schema.mjs +22 -0
- package/src/counter-verification.mjs +48 -0
- package/src/finding-ledger.mjs +169 -0
- package/src/host-review-children.mjs +100 -0
- package/src/incremental-review.mjs +95 -0
- package/src/project-execution.mjs +6 -1
- package/src/project-review-service.mjs +12 -6
- package/src/report-findings.mjs +5 -0
- package/src/report-summary.mjs +43 -4
- package/src/review-child-broker.mjs +65 -0
- package/src/review-child-plugin.mjs +32 -0
- package/src/review-effort.mjs +104 -0
- package/src/review-history.mjs +29 -0
- package/src/review-jobs.mjs +1 -0
- package/src/review.mjs +10 -6
- package/src/service.mjs +37 -10
- package/src/verification-loop.mjs +3 -0
- package/src/whole-scope-review.mjs +51 -18
- package/src/whole-scope-workers.mjs +88 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { planIncrementalReview } from './finding-ledger.mjs';
|
|
3
|
+
|
|
4
|
+
const fail = message => { throw new Error('Incremental review: ' + message); };
|
|
5
|
+
const object = value => value && typeof value === 'object' && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
6
|
+
const hash = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
7
|
+
function exact(value, keys) {
|
|
8
|
+
if (!object(value) || Reflect.ownKeys(value).some(key => typeof key !== 'string' || !keys.includes(key) || !Object.hasOwn(Object.getOwnPropertyDescriptor(value, key), 'value'))) fail('unknown metadata field');
|
|
9
|
+
}
|
|
10
|
+
function canonical(value) {
|
|
11
|
+
if (value === undefined) return null;
|
|
12
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value)) return value;
|
|
13
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
14
|
+
if (!object(value)) fail('non-JSON snapshot metadata');
|
|
15
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]));
|
|
16
|
+
}
|
|
17
|
+
const digest = value => createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex');
|
|
18
|
+
|
|
19
|
+
// Includes BOTH approved source sides, existence, properties, mappings and source
|
|
20
|
+
// provenance. A right-text hash alone would silently miss changed old-side diffs.
|
|
21
|
+
export function incrementalFileHash(file) {
|
|
22
|
+
if (!object(file)) fail('invalid snapshot file');
|
|
23
|
+
return digest(Object.fromEntries(Object.entries(file).filter(([key]) => !['id', 'eligibility', 'reason'].includes(key))));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A changed approved context/rule/requirement invalidates automatic safe narrowing.
|
|
27
|
+
// Explicit user-selected partial re-review is not a complete dependency check. This is a local policy binding, never defect evidence.
|
|
28
|
+
export function incrementalContextHash(snapshot) {
|
|
29
|
+
return digest([snapshot.context ?? [], snapshot.rules ?? [], snapshot.businessRequirement ?? null]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Pure primary selection; never creates last-reviewed-right -> current-right diff.
|
|
33
|
+
* previousManifest must be owned host metadata, not model/public request data.
|
|
34
|
+
* Automatic narrowing requires repository-wide complete graph attestation, never inferred
|
|
35
|
+
* from empty dependency lists or partial import/caller navigation. No attestation
|
|
36
|
+
* is available in today's capture path, so automatic production use stays full.
|
|
37
|
+
* An explicit diff-only user scope may omit unchanged primaries, always partial.
|
|
38
|
+
* The caller MUST construct retrieval from retrievalSnapshot (original approved
|
|
39
|
+
* source catalog) and use reviewSnapshot for primary scheduling. Skipped files
|
|
40
|
+
* are unreviewed, not completed; caller MUST enforce plan.requiredReportStatus.
|
|
41
|
+
*/
|
|
42
|
+
export function buildIncrementalSelection(approvedFreshSnapshot, options) {
|
|
43
|
+
exact(options, ['previousManifest', 'currentManifest', 'dependencyGraph', 'capturePolicyUnchanged', 'explicitDiffOnly']);
|
|
44
|
+
if (options.capturePolicyUnchanged !== undefined && typeof options.capturePolicyUnchanged !== 'boolean') fail('invalid policy binding');
|
|
45
|
+
const snapshot = approvedFreshSnapshot;
|
|
46
|
+
if (!object(snapshot) || typeof snapshot.id !== 'string' || !snapshot.id || !Array.isArray(snapshot.files) || snapshot.files.length > 200 || !Array.isArray(snapshot.context) || snapshot.context.length > 100) fail('invalid approved snapshot');
|
|
47
|
+
const { previousManifest, currentManifest, dependencyGraph } = options;
|
|
48
|
+
// Also validates exact manifest schemas/identities/paths/hash shapes. A graph
|
|
49
|
+
// assertion cannot bypass repository, route, rule or requirement-policy binding.
|
|
50
|
+
const incremental = planIncrementalReview(previousManifest, currentManifest);
|
|
51
|
+
if (currentManifest.snapshotId !== snapshot.id) fail('fresh snapshot identity mismatch');
|
|
52
|
+
const current = new Map(currentManifest.files.map(file => [file.path, file]));
|
|
53
|
+
if (current.size !== snapshot.files.length) fail('manifest inventory mismatch');
|
|
54
|
+
const ids = new Set(), paths = new Set();
|
|
55
|
+
for (const file of snapshot.files) {
|
|
56
|
+
if (!object(file) || typeof file.id !== 'string' || !file.id || ids.has(file.id) || paths.has(file.path) || !['reviewable', 'excluded', 'blocked'].includes(file.eligibility)) fail('invalid snapshot inventory');
|
|
57
|
+
ids.add(file.id); paths.add(file.path);
|
|
58
|
+
if (!current.has(file.path) || current.get(file.path).hash !== incrementalFileHash(file)) fail('manifest source mismatch');
|
|
59
|
+
}
|
|
60
|
+
let graphKnown = false;
|
|
61
|
+
if (dependencyGraph !== undefined) {
|
|
62
|
+
exact(dependencyGraph, ['status', 'repositoryWide', 'previousSnapshotId', 'currentSnapshotId', 'previousContextHash', 'currentContextHash']);
|
|
63
|
+
if (!['complete', 'unknown'].includes(dependencyGraph.status) || typeof dependencyGraph.repositoryWide !== 'boolean') fail('invalid dependency graph metadata');
|
|
64
|
+
if (dependencyGraph.status === 'complete') {
|
|
65
|
+
if (dependencyGraph.previousSnapshotId !== previousManifest.snapshotId || dependencyGraph.currentSnapshotId !== snapshot.id || !hash(dependencyGraph.previousContextHash) || dependencyGraph.currentContextHash !== incrementalContextHash(snapshot)) fail('dependency graph binding mismatch');
|
|
66
|
+
graphKnown = dependencyGraph.repositoryWide === true && dependencyGraph.previousContextHash === dependencyGraph.currentContextHash;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Any unknown dependency inventory makes the WHOLE repository graph unknown.
|
|
70
|
+
// A seemingly isolated unchanged file cannot rule out uncaptured reverse users.
|
|
71
|
+
const inventoriesKnown = [previousManifest, currentManifest].every(manifest => manifest.files.every(file => hash(file.hash) && Array.isArray(file.dependencies) && file.dependencies.every(dep => hash(dep.hash) && manifest.files.some(target => target.path === dep.path && target.hash === dep.hash))));
|
|
72
|
+
const safe = options.capturePolicyUnchanged === true && graphKnown && inventoriesKnown ? new Set(incremental.unchangedNavigationPaths) : new Set();
|
|
73
|
+
if (options.explicitDiffOnly !== undefined && typeof options.explicitDiffOnly !== 'boolean') fail('invalid explicit diff-only selection');
|
|
74
|
+
if (options.explicitDiffOnly === true) {
|
|
75
|
+
const old=new Map(previousManifest.files.map(file=>[file.path,file.hash]));
|
|
76
|
+
for(const file of snapshot.files) if(old.get(file.path)===incrementalFileHash(file)) safe.add(file.path);
|
|
77
|
+
}
|
|
78
|
+
const unreviewed = snapshot.files.filter(file => file.eligibility === 'reviewable' && safe.has(file.path));
|
|
79
|
+
const unreviewedPaths = unreviewed.map(file => file.path);
|
|
80
|
+
const skipped = new Set(unreviewedPaths);
|
|
81
|
+
const excludedRecords = unreviewed.map(file => ({ fileId: file.id, path: file.path, status: 'excluded', reason: 'incremental-unchanged-not-reviewed' }));
|
|
82
|
+
const reviewSnapshot = { ...snapshot, files: snapshot.files.map(file => skipped.has(file.path) ? { ...file, eligibility: 'excluded', reason: 'incremental-unchanged-not-reviewed' } : file) };
|
|
83
|
+
const plan = {
|
|
84
|
+
mode: skipped.size ? (options.explicitDiffOnly ? 'explicit-diff-only-subset' : 'incremental-primary-subset') : 'full-fresh-primary',
|
|
85
|
+
primaryPaths: reviewSnapshot.files.filter(file => file.eligibility === 'reviewable').map(file => file.path),
|
|
86
|
+
unreviewedPaths, removedPaths: incremental.removedPaths, excludedRecords,
|
|
87
|
+
requiredReportStatus: skipped.size ? 'partial' : null,
|
|
88
|
+
coverage: false, requiresFreshEvidence: true,
|
|
89
|
+
notice: skipped.size ? 'Unchanged primaries were not reviewed; original approved sources remain retrieval context only. No full coverage, fix verification, new incremental baseline, or prior receipt reuse.' : 'No safe narrowing; fresh primary scope retained. Unknown repository dependencies require full review, not assumed isolation.',
|
|
90
|
+
};
|
|
91
|
+
// Freeze only newly authored metadata. Never freeze caller-owned snapshot/body.
|
|
92
|
+
for (const key of ['primaryPaths', 'unreviewedPaths', 'removedPaths', 'excludedRecords']) Object.freeze(plan[key]);
|
|
93
|
+
for (const row of excludedRecords) Object.freeze(row);
|
|
94
|
+
return Object.freeze({ reviewSnapshot, retrievalSnapshot: snapshot, plan: Object.freeze(plan) });
|
|
95
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { reviewEffortPolicy } from './review-effort.mjs';
|
|
2
|
+
import { PROJECT_REVIEW_INSTRUCTIONS } from './prompts.mjs';
|
|
1
3
|
import { reviewSnapshot } from './review.mjs';
|
|
2
4
|
import { retrievalRounds } from './review-execution-policy.mjs';
|
|
3
5
|
|
|
@@ -9,7 +11,7 @@ export function projectConcurrency(value = DEFAULT_PROJECT_CONCURRENCY) {
|
|
|
9
11
|
|
|
10
12
|
// Parallelize independent approved snapshots, never dependent stages within a batch.
|
|
11
13
|
// Admission and the shared call reservation are synchronous; completion order is not report order.
|
|
12
|
-
export async function reviewProjectBatches(batches, model, { concurrency = DEFAULT_PROJECT_CONCURRENCY, remainingCalls, signal, onProgress, maxRetrievalRounds = 3, resumeStates, onResumeCheckpoint, onModelArtifact, enableGroupPrimary = false, nativeOutputTools = false } = {}) {
|
|
14
|
+
export async function reviewProjectBatches(batches, model, { concurrency = DEFAULT_PROJECT_CONCURRENCY, reviewEffortMode = 'auto', enableCounterVerification = false, remainingCalls, signal, onProgress, maxRetrievalRounds = 3, resumeStates, onResumeCheckpoint, onModelArtifact, enableGroupPrimary = false, nativeOutputTools = false } = {}) {
|
|
13
15
|
projectConcurrency(concurrency);
|
|
14
16
|
retrievalRounds(maxRetrievalRounds);
|
|
15
17
|
if (!Array.isArray(batches) || typeof remainingCalls !== 'function') throw new Error('Invalid project execution');
|
|
@@ -25,7 +27,10 @@ export async function reviewProjectBatches(batches, model, { concurrency = DEFAU
|
|
|
25
27
|
const batch = batches[index];
|
|
26
28
|
try {
|
|
27
29
|
if (batchSignal.aborted || remainingCalls() <= 0) { results[index] = unstarted(batch); continue; }
|
|
30
|
+
const effort=reviewEffortPolicy(batch,{mode:reviewEffortMode});
|
|
28
31
|
results[index] = await reviewSnapshot(batch, model, {
|
|
32
|
+
instructions: PROJECT_REVIEW_INSTRUCTIONS + '\nReview focus (not proof): ' + JSON.stringify(effort.checks),
|
|
33
|
+
enableCounterVerification:enableCounterVerification && effort.counterVerification.eligible,
|
|
29
34
|
signal: batchSignal, maxCalls: remainingCalls(), remainingCalls, enableRetrieval: true, enableVerification: true,
|
|
30
35
|
enableGroupPrimary, nativeOutputTools, enableAnchorCorrection: true, correctProtocol: true, maxRetrievalRounds,
|
|
31
36
|
resumeState: resumeStates?.get(batch.id), onResumeCheckpoint: onResumeCheckpoint ? report => onResumeCheckpoint(batch, report) : undefined, onModelArtifact,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reviewEffortPolicy, reviewEffortMode } from './review-effort.mjs';
|
|
1
2
|
import { previewValidity } from './preview-validity.mjs';
|
|
2
3
|
import { REVIEW_RUNTIME } from './runtime-identity.mjs';
|
|
3
4
|
import { buildResumeState, validateResumeState } from './review-resume.mjs';
|
|
@@ -72,10 +73,12 @@ async function captureProject(cwd, args, signal) {
|
|
|
72
73
|
return { ...result, id: hash(JSON.stringify(result)) };
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
export function createProjectReviewService(llm, { executionStrategy = 'staged', allowModelSending = false, authorize = async () => false, now = Date.now, ttlMs = 300000, maxPreviews = 8, projectMaxCalls = Number.MAX_SAFE_INTEGER, projectConcurrency = DEFAULT_PROJECT_CONCURRENCY, maxRetrievalRounds = 3, nativeOutputTools = false, enableGroupPrimary = false } = {}) {
|
|
76
|
+
export function createProjectReviewService(llm, { reviewEffort = 'auto', enableCounterVerification = false, executionStrategy = 'staged', allowModelSending = false, authorize = async () => false, now = Date.now, ttlMs = 300000, maxPreviews = 8, projectMaxCalls = Number.MAX_SAFE_INTEGER, projectConcurrency = DEFAULT_PROJECT_CONCURRENCY, maxRetrievalRounds = 3, nativeOutputTools = false, enableGroupPrimary = false } = {}) {
|
|
76
77
|
for (const n of [ttlMs, maxPreviews]) if (!Number.isSafeInteger(n) || n < 1) throw new Error('Invalid project preview limit');
|
|
77
78
|
if (!Number.isSafeInteger(projectMaxCalls) || projectMaxCalls < 1) throw new Error('Invalid project model call limit');
|
|
78
79
|
|
|
80
|
+
reviewEffortMode(reviewEffort);
|
|
81
|
+
if(typeof enableCounterVerification!=='boolean')throw new Error('Invalid counterverification policy');
|
|
79
82
|
validateProjectConcurrency(projectConcurrency);
|
|
80
83
|
retrievalRounds(maxRetrievalRounds);
|
|
81
84
|
if (typeof nativeOutputTools !== 'boolean' || typeof enableGroupPrimary !== 'boolean') throw new Error('Invalid project execution options');
|
|
@@ -94,16 +97,17 @@ export function createProjectReviewService(llm, { executionStrategy = 'staged',
|
|
|
94
97
|
if (executionStrategy === 'whole-scope') throw new Error('Whole-scope currently supports fresh single-round changes only; project requires explicit staged configuration');
|
|
95
98
|
if (busy) throw new Error('Review service busy');
|
|
96
99
|
if (!args || typeof args !== 'object') throw new Error('Project preview parameters required');
|
|
97
|
-
if (Object.keys(args).some(k => !['reviewMode', 'repositoryPath', 'selectedPaths', 'resumeFromJobId'].includes(k)) || args.reviewMode !== 'project') throw new Error('Project mode supports only reviewMode/repositoryPath/selectedPaths; change/history context parameters are not project parameters');
|
|
100
|
+
if (Object.keys(args).some(k => !['reviewMode', 'repositoryPath', 'selectedPaths', 'resumeFromJobId', 'reviewEffort'].includes(k)) || args.reviewMode !== 'project') throw new Error('Project mode supports only reviewMode/repositoryPath/selectedPaths; change/history context parameters are not project parameters');
|
|
98
101
|
const current = owner(exec);
|
|
99
102
|
busy = true;
|
|
100
103
|
try {
|
|
101
104
|
const captureArgs = structuredClone(args);
|
|
105
|
+
const effortMode=reviewEffortMode(captureArgs.reviewEffort ?? reviewEffort); delete captureArgs.reviewEffort;
|
|
102
106
|
const sourceJobId = captureArgs.resumeFromJobId; delete captureArgs.resumeFromJobId;
|
|
103
107
|
if (sourceJobId !== undefined && (typeof sourceJobId !== 'string' || !/^[a-f0-9-]{36}$/.test(sourceJobId) || typeof exec.resolveReviewResume !== 'function')) throw new Error('Invalid or unavailable resume job');
|
|
104
108
|
const saved = sourceJobId === undefined ? undefined : await exec.resolveReviewResume(sourceJobId);
|
|
105
109
|
const s = await captureProject(current.cwd, captureArgs, signalFor(exec));
|
|
106
|
-
const identity = resumeIdentity(captureArgs, current.route);
|
|
110
|
+
const identity = resumeIdentity({...captureArgs,reviewEffort:effortMode,enableCounterVerification}, current.route);
|
|
107
111
|
let resumeStates;
|
|
108
112
|
if (saved !== undefined) {
|
|
109
113
|
if (saved?.schemaVersion !== 1 || saved.mode !== 'project' || !Array.isArray(saved.batches)) throw new Error('Invalid project resume checkpoint');
|
|
@@ -119,7 +123,7 @@ export function createProjectReviewService(llm, { executionStrategy = 'staged',
|
|
|
119
123
|
while (previews.size >= maxPreviews) previews.delete(previews.keys().next().value);
|
|
120
124
|
const previewId = randomUUID();
|
|
121
125
|
const expires = now() + ttlMs;
|
|
122
|
-
previews.set(previewId, { ...current, args: captureArgs, snapshotId: s.id, resumeStates, sourceJobId, expires });
|
|
126
|
+
previews.set(previewId, { ...current, args: captureArgs, snapshotId: s.id, effortMode, resumeStates, sourceJobId, expires });
|
|
123
127
|
return {
|
|
124
128
|
...(resumeStates ? { resume: { sourceJobId, reusableFiles: [...resumeStates.values()].reduce((sum, state) => sum + state.completed.length, 0) } } : {}),
|
|
125
129
|
previewId, expiresAt: new Date(expires).toISOString(), validForMs: ttlMs, snapshotId: s.id, reviewMode: 'project', repositoryRoot: s.root, vcs: s.vcs,
|
|
@@ -128,6 +132,7 @@ export function createProjectReviewService(llm, { executionStrategy = 'staged',
|
|
|
128
132
|
inventoryComplete: s.inventoryComplete, inventoryScope: s.inventoryScope, inventoryNodes: s.inventoryNodes, batches: s.batches.length,
|
|
129
133
|
privateArtifacts: { enabled: typeof exec.onModelArtifact === 'function', sourceBearing: true, maxArtifactBytes: 1048576, maxJobBytes: 8388608, maxArtifacts: 4096, notice: 'Bounded private untrusted model outputs; raw access requires explicit consent and is not review evidence.' },
|
|
130
134
|
execution: { nativeOutputTools, enableGroupPrimary, batchConcurrency: projectConcurrency, maxRetrievalRounds },
|
|
135
|
+
reviewEffort: {mode:effortMode,status:"planned-per-batch",counterVerification:enableCounterVerification},
|
|
131
136
|
files: s.batches.flatMap(b => b.files.map(f => ({ path: f.path, eligibility: f.eligibility, reason: f.reason }))),
|
|
132
137
|
notice: s.notice + PREVIEW_NOTICE_SUFFIX + ' Each stage allows at most ' + maxRetrievalRounds + ' retrieval rounds; exhaustion requires a final-only response and explicit unfinished coverage.',
|
|
133
138
|
};
|
|
@@ -157,7 +162,7 @@ export function createProjectReviewService(llm, { executionStrategy = 'staged',
|
|
|
157
162
|
files: s.batches.flatMap(b => b.files.map(f => ({ fileId: f.id, path: f.path, status: f.eligibility === 'reviewable' ? 'pending' : f.eligibility }))),
|
|
158
163
|
});
|
|
159
164
|
|
|
160
|
-
const identity = resumeIdentity(p.args, p.route);
|
|
165
|
+
const identity = resumeIdentity({...p.args,reviewEffort:p.effortMode,enableCounterVerification}, p.route);
|
|
161
166
|
const states = new Map();
|
|
162
167
|
for (const [id, saved] of p.resumeStates ?? []) {
|
|
163
168
|
const batch = s.batches.find(batch => batch.id === id);
|
|
@@ -182,7 +187,7 @@ export function createProjectReviewService(llm, { executionStrategy = 'staged',
|
|
|
182
187
|
};
|
|
183
188
|
|
|
184
189
|
const results = await reviewProjectBatches(s.batches, model, {
|
|
185
|
-
concurrency: projectConcurrency, maxRetrievalRounds, enableGroupPrimary, nativeOutputTools, signal, remainingCalls: () => projectMaxCalls - calls,
|
|
190
|
+
concurrency: projectConcurrency, reviewEffortMode:p.effortMode, enableCounterVerification, maxRetrievalRounds, enableGroupPrimary, nativeOutputTools, signal, remainingCalls: () => projectMaxCalls - calls,
|
|
186
191
|
onProgress: exec.reviewCheckpoint, resumeStates: states, onResumeCheckpoint: persist, onModelArtifact: exec.onModelArtifact,
|
|
187
192
|
});
|
|
188
193
|
|
|
@@ -208,6 +213,7 @@ export function createProjectReviewService(llm, { executionStrategy = 'staged',
|
|
|
208
213
|
coverage, files, findings, scopeNotices: results.flatMap(result => result.scopeNotices ?? []), artifacts: results.flatMap(result => result.artifacts ?? []), batches: results, resume: { reusedFiles: files.filter(file => file.reused).length },
|
|
209
214
|
privateArtifacts: { enabled: typeof exec.onModelArtifact === 'function', sourceBearing: true, maxArtifactBytes: 1048576, maxJobBytes: 8388608, maxArtifacts: 4096, notice: 'Bounded private untrusted model outputs; raw access requires explicit consent and is not review evidence.' },
|
|
210
215
|
execution: { nativeOutputTools, enableGroupPrimary, batchConcurrency: projectConcurrency, maxRetrievalRounds },
|
|
216
|
+
reviewEffort: {mode:p.effortMode,status:"planned-per-batch",counterVerification:enableCounterVerification},
|
|
211
217
|
notice: s.notice + REPORT_NOTICE_SUFFIX,
|
|
212
218
|
};
|
|
213
219
|
|
package/src/report-findings.mjs
CHANGED
|
@@ -42,6 +42,8 @@ export function findingsMarkdown(report) {
|
|
|
42
42
|
for (const [label, items] of sections) {
|
|
43
43
|
if (!items.length) continue;
|
|
44
44
|
output.push('', '### ' + label);
|
|
45
|
+
const levels = {critical:0,high:1,medium:2,low:3};
|
|
46
|
+
items.sort((a,b)=>(levels[a.f.severity]??4)-(levels[b.f.severity]??4)||a.i-b.i);
|
|
45
47
|
for (const {f,i} of items) {
|
|
46
48
|
const severity = ({critical:'严重',high:'高',medium:'中',low:'低'})[f.severity] ?? safe(f.severity ?? '未分级');
|
|
47
49
|
const hypothesis = label === '复核后不确定' || label === '复核未完成' || label === '已反驳';
|
|
@@ -58,6 +60,9 @@ export function findingsMarkdown(report) {
|
|
|
58
60
|
else if (f.suggestion) output.push('- 建议:' + safe(f.suggestion));
|
|
59
61
|
const related = [...duplicate].filter(([,first])=>first===i).map(([index])=>location(findings[index]));
|
|
60
62
|
if (related.length) output.push('- 相同证据与解释的其他位置:' + related.join(';') + '。正文合并展示,原始候选仍保留。');
|
|
63
|
+
const ledgerItem = report.reviewHistory?.ledger?.findings?.find(row => row.findingIndices?.includes(i));
|
|
64
|
+
if (ledgerItem?.observation === 'repeated') output.push('- 历史台账:本轮再次观察到相同定位与缺陷主张;不重复创建新问题,当前证据仍须独立核对。');
|
|
65
|
+
if (ledgerItem?.humanDecisions?.length) output.push('- 人工处置记录:' + ledgerItem.humanDecisions.map(row => safe(row.decision) + ':' + safe(row.reason)).join(';') + ';这是历史决定,不是修复或缺陷证明。');
|
|
61
66
|
if (f.reused || f.priorRun) output.push('- 来源:包含既往审核结果,不表示本轮重新取证。');
|
|
62
67
|
output.push('- 完整证据与判定过程:见技术附录中的候选 ' + (i+1) + '。');
|
|
63
68
|
}
|
package/src/report-summary.mjs
CHANGED
|
@@ -30,6 +30,20 @@ export function verificationPresentation(finding, report = {}) {
|
|
|
30
30
|
return 'uncertain';
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Candidate numbers refer to the unchanged original finding/appendix order.
|
|
34
|
+
// Only evidence-qualified support is prioritized; severity never promotes a hypothesis.
|
|
35
|
+
export function supportedFindingPriorities(report = {}) {
|
|
36
|
+
const severity = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
37
|
+
const category = { introduced: 0, current: 1, preexisting: 2 };
|
|
38
|
+
return rows(report.findings).flatMap((finding, index) => {
|
|
39
|
+
const state = verificationPresentation(finding, report);
|
|
40
|
+
if (!Object.hasOwn(category, state)) return [];
|
|
41
|
+
const level = Object.hasOwn(severity, finding.severity) ? finding.severity : 'unknown';
|
|
42
|
+
return [{ candidate: index + 1, state, severity: level }];
|
|
43
|
+
}).sort((a, b) => category[a.state] - category[b.state] ||
|
|
44
|
+
(severity[a.severity] ?? 4) - (severity[b.severity] ?? 4) || a.candidate - b.candidate);
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
/**
|
|
34
48
|
* Pure presentation data for Markdown/Lead renderers. Counts are candidate records,
|
|
35
49
|
* not proven defects. coverage counts main-review entries (directories included in
|
|
@@ -82,6 +96,12 @@ export function reportSummaryData(report = {}) {
|
|
|
82
96
|
counts: { total: findings.length, ...counts },
|
|
83
97
|
coverage: { total, loaded: files.length, detailsComplete, unit: project ? 'files' : 'change-entries', ...coverage },
|
|
84
98
|
unfinished, crossFile, stages, unverifiedCandidates,
|
|
99
|
+
verification: {
|
|
100
|
+
supported: counts.introduced + counts.preexisting + counts.current,
|
|
101
|
+
uncertain: findings.filter(finding => verificationPresentation(finding, report) === 'uncertain').length,
|
|
102
|
+
incomplete: findings.filter(finding => verificationPresentation(finding, report) === 'incomplete').length,
|
|
103
|
+
},
|
|
104
|
+
priorities: supportedFindingPriorities(report),
|
|
85
105
|
limitations: rows(report.limitations).length,
|
|
86
106
|
scopeNotices: rows(report.scopeNotices).length,
|
|
87
107
|
reused: findings.some(finding => finding?.reused || finding?.priorRun),
|
|
@@ -93,16 +113,35 @@ export function reportSummaryMarkdown(report = {}) {
|
|
|
93
113
|
const data = reportSummaryData(report), { counts, coverage } = data;
|
|
94
114
|
const state = data.emptySelection ? '未选择条目,没有执行代码审核(' + (STATUS_LABELS[data.status] ?? '状态未知') + ')' :
|
|
95
115
|
data.complete ? '所选范围已查完' : '尚未查完(' + (STATUS_LABELS[data.status] ?? '状态未知') + ')';
|
|
96
|
-
const
|
|
116
|
+
const firstCounts = counts.total ? [
|
|
117
|
+
counts.introduced ? '复核支持本次新增 ' + counts.introduced : '',
|
|
118
|
+
counts.current ? '当前源码复核支持 ' + counts.current : '',
|
|
119
|
+
counts.preexisting ? '复核支持原有 ' + counts.preexisting : '',
|
|
120
|
+
!data.verification.supported ? '尚无复核支持的问题' : '',
|
|
121
|
+
].filter(Boolean).join(';') + ';' : '';
|
|
122
|
+
const headline = firstCounts + state + (data.outdated ? ';报告已过期' : '') + ';' +
|
|
97
123
|
(counts.total ? '记录 ' + counts.total + ' 个候选问题' : '未记录候选问题') + ';不得据此放行。';
|
|
98
124
|
const output = ['', '## 结论摘要', '- ' + headline];
|
|
125
|
+
if (counts.total || !data.complete || data.outdated) {
|
|
126
|
+
output.push('- 下一步:' + (data.outdated ? '先基于当前批准快照重新审核;' : '') +
|
|
127
|
+
(data.verification.supported ? '按下列候选编号核对触发条件与修复必要性;' : '无复核支持的问题不等于审核通过;') +
|
|
128
|
+
'已复核不确定 ' + data.verification.uncertain + '、复核未完成 ' + data.verification.incomplete +
|
|
129
|
+
',先核实而非直接修复;已加载未完成条目 ' + data.unfinished.length +
|
|
130
|
+
(coverage.detailsComplete ? '' : ',明细尚未加载齐') +
|
|
131
|
+
(data.complete ? ';处理已结束,不代表所有疑点已排除。' : ';须补齐未完成范围和阶段,不能视为无问题。'));
|
|
132
|
+
}
|
|
133
|
+
if (data.priorities.length) {
|
|
134
|
+
const labels = { introduced: '本次新增', current: '当前源码', preexisting: '原有' };
|
|
135
|
+
const levels = { critical: '严重', high: '高', medium: '中', low: '低', unknown: '未分级' };
|
|
136
|
+
output.push('- 优先核对(仅复核支持,类别内按严重程度):' + data.priorities.slice(0, 5)
|
|
137
|
+
.map(item => '候选 ' + item.candidate + '(' + labels[item.state] + '/' + levels[item.severity] + ')').join(';') +
|
|
138
|
+
(data.priorities.length > 5 ? ';其余 ' + (data.priorities.length - 5) + ' 项见审查发现与技术附录' : '') + '。');
|
|
139
|
+
}
|
|
99
140
|
if (counts.total) {
|
|
100
141
|
const labels = { introduced: '复核支持本次新增', unresolved: '待核实', preexisting: '复核支持原有', refuted: '复核反驳', current: '当前源码复核支持' };
|
|
101
142
|
output.push('- 候选复核:' + Object.entries(labels).filter(([key]) => counts[key] > 0).map(([key, label]) => label + ' ' + counts[key]).join(';') + '。');
|
|
102
143
|
}
|
|
103
|
-
const
|
|
104
|
-
const uncertain = states.filter(state => state === 'uncertain').length;
|
|
105
|
-
const incomplete = states.filter(state => state === 'incomplete').length;
|
|
144
|
+
const { uncertain, incomplete } = data.verification;
|
|
106
145
|
output.push('- 复核状态分列:复核支持 ' + (counts.introduced + counts.preexisting + counts.current) + ';已复核不确定 ' + uncertain + ';复核未完成(未复核或未完成有效复核) ' + incomplete + (counts.refuted ? ';已反驳 ' + counts.refuted : '') + '。');
|
|
107
146
|
if (counts.unresolved) output.push('- “待核实”为合计:已复核不确定与复核未完成分开列示,前者不表示复核未执行;均不能直接视为缺陷或必改项。');
|
|
108
147
|
const unit = coverage.unit === 'files' ? '个文件' : '个变更条目(含目录)';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Process-local ownership broker. Fixtures exercise this contract, not host integration.
|
|
2
|
+
const bindings = new Map();
|
|
3
|
+
const revoked = new Set();
|
|
4
|
+
const attachments = new WeakMap();
|
|
5
|
+
const allowedTools = new Set(['review_snapshot', 'structured_output']);
|
|
6
|
+
const failure = (message) => Object.assign(new Error(message), { code: 'REVIEW_CHILD_IDENTITY' });
|
|
7
|
+
|
|
8
|
+
export function registerReviewChild(childId, binding) {
|
|
9
|
+
if (typeof childId !== 'string' || !childId || bindings.has(childId) || revoked.has(childId)) throw failure('Child id is invalid, already reserved, or revoked');
|
|
10
|
+
if (!binding || typeof binding.parentId !== 'string' || !binding.parentId || typeof binding.presetId !== 'string' || !binding.presetId || typeof binding.assertAgent !== 'function' || typeof binding.executeRequests !== 'function' || typeof binding.beforeModelCall !== 'function') throw failure('Incomplete restricted child binding');
|
|
11
|
+
const entry = { binding };
|
|
12
|
+
bindings.set(childId, entry);
|
|
13
|
+
let active = true;
|
|
14
|
+
return () => {
|
|
15
|
+
if (!active) return;
|
|
16
|
+
active = false;
|
|
17
|
+
bindings.delete(childId);
|
|
18
|
+
// Do not allow a late stream event to become an unrelated, unguarded call.
|
|
19
|
+
revoked.add(childId);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getReviewChildBinding(childId) {
|
|
24
|
+
if (revoked.has(childId)) throw failure('Review child binding was revoked');
|
|
25
|
+
return bindings.get(childId)?.binding;
|
|
26
|
+
}
|
|
27
|
+
export const getBinding = getReviewChildBinding;
|
|
28
|
+
|
|
29
|
+
export function assertReviewChildAgent(childId, agent) {
|
|
30
|
+
const binding = getReviewChildBinding(childId);
|
|
31
|
+
const header = agent?.session?.header;
|
|
32
|
+
if (!binding || agent?.id !== childId || header?.id !== childId || header.parentSession !== binding.parentId || header.agentPreset !== binding.presetId) throw failure('Review child session, parent, or preset does not match its reservation');
|
|
33
|
+
binding.assertAgent(agent);
|
|
34
|
+
return binding;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function executeReviewChildRequests(args, exec) {
|
|
38
|
+
const childId = exec?.agent?.id;
|
|
39
|
+
const binding = assertReviewChildAgent(childId, exec?.agent);
|
|
40
|
+
return binding.executeRequests(args, exec);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function attachReviewChildMiddleware(ctx) {
|
|
44
|
+
if (attachments.has(ctx)) return attachments.get(ctx);
|
|
45
|
+
const dispose = ctx.on('llm/stream', function (options, next) {
|
|
46
|
+
const childId = options?.sessionId;
|
|
47
|
+
const binding = getReviewChildBinding(childId);
|
|
48
|
+
if (!binding) return next();
|
|
49
|
+
try {
|
|
50
|
+
const agent = ctx.get?.('agents')?.get?.(childId);
|
|
51
|
+
assertReviewChildAgent(childId, agent);
|
|
52
|
+
if (!Array.isArray(options.tools) || !options.tools.some(tool => tool?.name === 'structured_output') || options.tools.some(tool => !allowedTools.has(tool?.name)) || new Set(options.tools.map(tool => tool.name)).size !== options.tools.length) throw failure('Restricted child model tools changed');
|
|
53
|
+
// Reservation/route/budget guard executes synchronously, before next dispatch.
|
|
54
|
+
const result = binding.beforeModelCall(options);
|
|
55
|
+
if (result && typeof result.then === 'function') throw failure('Child model-call guard must be synchronous');
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// Poison the owner's activation even when failure precedes its budget guard.
|
|
58
|
+
binding.onFatal?.(error);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
return next();
|
|
62
|
+
});
|
|
63
|
+
attachments.set(ctx, dispose);
|
|
64
|
+
return dispose;
|
|
65
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { assertReviewChildAgent, executeReviewChildRequests } from './review-child-broker.mjs';
|
|
2
|
+
|
|
3
|
+
export const name = 'dsh-code-review-restricted-child';
|
|
4
|
+
export const inject = ['tools'];
|
|
5
|
+
|
|
6
|
+
export function registerSnapshotTool(ctx) {
|
|
7
|
+
// Presets are shared generations: never capture a particular child here.
|
|
8
|
+
// Restrict inherited tools before resolution; host-owned structured_output remains.
|
|
9
|
+
ctx.tools.register({
|
|
10
|
+
name: 'review_snapshot',
|
|
11
|
+
description: 'Internal tool for registered review children only; ordinary parent/other agents cannot call it. Read only the approved review snapshot using catalog retrieval requests. No filesystem, shell, network, or additional agents.',
|
|
12
|
+
parameters: { type: 'object', additionalProperties: false, properties: { requests: { type: 'array', items: { type: 'object' } } }, required: ['requests'] },
|
|
13
|
+
output: {
|
|
14
|
+
schema: { type: 'object', additionalProperties: false, properties: { json: { type: 'string' } }, required: ['json'] },
|
|
15
|
+
render: (_args, value) => [{ type: 'text', text: value.json }],
|
|
16
|
+
},
|
|
17
|
+
async execute(args, exec) {
|
|
18
|
+
const result = await executeReviewChildRequests(args, exec);
|
|
19
|
+
// Recheck after asynchronous business logic: revoked work cannot leak a result.
|
|
20
|
+
assertReviewChildAgent(exec?.agent?.id, exec?.agent);
|
|
21
|
+
return { json: JSON.stringify(result) };
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function apply(ctx) {
|
|
26
|
+
ctx.tools.restrict({ allow: ['review_snapshot'] });
|
|
27
|
+
// Identity only: installation above must precede the host's tool assembly.
|
|
28
|
+
ctx.on('agent/pre-step', ({ agent }, next) => {
|
|
29
|
+
assertReviewChildAgent(agent?.id, agent);
|
|
30
|
+
return next();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lineDiff } from './line-diff.mjs';
|
|
3
|
+
|
|
4
|
+
// Planning only: these lexical signals are neither findings nor approval evidence.
|
|
5
|
+
const LIMITS = Object.freeze({ files: 200, sourceChars: 262144, totalSourceChars: 2097152, metadataChars: 4096 });
|
|
6
|
+
const BASELINE = Object.freeze([
|
|
7
|
+
'Require exact changed-code evidence, a concrete trigger and impact before reporting a defect.',
|
|
8
|
+
'Compare before/after behavior and captured callers; missing context remains uncertainty.',
|
|
9
|
+
'Retain strict evidence verification in every effort tier; no policy result proves completion or approval.',
|
|
10
|
+
]);
|
|
11
|
+
const CATEGORIES = Object.freeze({
|
|
12
|
+
lifecycle: { pattern: /\b(?:dispose|destroy|shutdown|close|release|free|cancel|abort|unsubscribe|removeEventListener)\b/i,
|
|
13
|
+
guidance: 'Seek cleanup guards, ownership transfers and completion before teardown on success, failure and cancellation paths.' },
|
|
14
|
+
concurrency: { pattern: /\b(?:await|async|mutex|lock|atomic|thread|worker|synchronized|volatile|Promise|goroutine)\b/i,
|
|
15
|
+
guidance: 'Seek actual synchronization, queue/actor isolation and cancellation ordering; source order alone is not a happens-before proof.' },
|
|
16
|
+
persistence: { pattern: /\b(?:transaction|commit|rollback|writeFile|rename|fsync|INSERT|UPDATE|DELETE|migration|serialize|deserialize)\b/i,
|
|
17
|
+
guidance: 'Seek rollback, atomic publication, schema/version guards and recovery behavior against captured storage consumers.' },
|
|
18
|
+
compatibility: { pattern: /\b(?:export|public|interface|ABI|API|schema|protocol|deprecated|version|override)\b/i,
|
|
19
|
+
guidance: 'Seek caller contracts, compatibility adapters and supported runtime/version evidence; do not invent a public contract.' },
|
|
20
|
+
sensitive: { pattern: /\b(?:auth|authorize|authorization|authenticate|authentication|permissions?|privilege|chmod|sudo|install|update|release|CI|postinstall|preinstall|pull_request_target|workflow_dispatch|publish|deploy|installer|autoUpdate)\b/i,
|
|
21
|
+
guidance: 'Seek trust-boundary validation and explicit authorization in captured code; installation, update, CI and release actions require separate human handling, never automatic approval.' },
|
|
22
|
+
});
|
|
23
|
+
const PATH_HINT = /(?:^|[\\/_.-])(?:auth|permissions?|install|update|release|deploy)(?:[\\/_.-]|$)|(?:^|[\\/])\.github[\\/]workflows[\\/]|(?:^|[\\/])\.gitlab-ci\.yml$/i;
|
|
24
|
+
const digest = value => createHash('sha256').update(value).digest('hex');
|
|
25
|
+
|
|
26
|
+
export function reviewEffortMode(value = 'auto') {
|
|
27
|
+
if (!['auto', 'quick', 'standard', 'deep'].includes(value)) throw new TypeError('Invalid review effort mode');
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Unknown severities are never silently promoted or treated as verified severe findings.
|
|
32
|
+
export function counterVerificationEligible(policy, candidate) {
|
|
33
|
+
return policy?.version === 1 && policy.tier === 'deep' && policy.counterVerification?.eligible === true &&
|
|
34
|
+
['high', 'critical'].includes(candidate?.severity);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Pure bounded planning over captured snapshot.files[].left/right.text.
|
|
38
|
+
* Identity is a source/selected-metadata binding, not a receipt or reusable coverage.
|
|
39
|
+
* A null sourceFingerprint explicitly means the complete binding is unavailable.
|
|
40
|
+
* Requirements and other free-text instructions are not interpreted or returned.
|
|
41
|
+
*/
|
|
42
|
+
export function reviewEffortPolicy(snapshot, { mode = 'auto' } = {}) {
|
|
43
|
+
mode = reviewEffortMode(mode);
|
|
44
|
+
if (!snapshot || !Array.isArray(snapshot.files)) throw new TypeError('Invalid effort snapshot');
|
|
45
|
+
const reasons = new Set(), categories = new Set();
|
|
46
|
+
let complete = true, used = 0, filesScanned = 0, exactChanges = true;
|
|
47
|
+
const hash = createHash('sha256');
|
|
48
|
+
const field = value => {
|
|
49
|
+
if (value === undefined) return null;
|
|
50
|
+
if (typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))) return value;
|
|
51
|
+
if (typeof value === 'string' && value.length <= LIMITS.metadataChars) return value;
|
|
52
|
+
complete = false; reasons.add('metadata-unavailable'); return null;
|
|
53
|
+
};
|
|
54
|
+
const bind = values => hash.update(JSON.stringify(values) + '\n');
|
|
55
|
+
bind(['review-effort-v1', ...['id', 'root', 'vcs', 'reviewMode', 'baseRevision', 'targetRevision', 'changeScope'].map(k => field(snapshot[k])), snapshot.files.length]);
|
|
56
|
+
if (typeof snapshot.id !== 'string' || !snapshot.id || typeof snapshot.root !== 'string' || !snapshot.root) {
|
|
57
|
+
complete = false; reasons.add('identity-unavailable');
|
|
58
|
+
}
|
|
59
|
+
if (snapshot.files.length > LIMITS.files) { complete = false; reasons.add('file-limit'); }
|
|
60
|
+
for (let i = 0; i < Math.min(snapshot.files.length, LIMITS.files); i++) {
|
|
61
|
+
const file = snapshot.files[i]; filesScanned++;
|
|
62
|
+
if (!file || typeof file !== 'object') { complete = false; exactChanges = false; reasons.add('source-unavailable'); continue; }
|
|
63
|
+
const path = field(file.path);
|
|
64
|
+
bind([field(file.id), path, field(file.oldPath), field(file.status), field(file.eligibility), field(file.reviewMode)]);
|
|
65
|
+
if (typeof path !== 'string' || !path) { complete = false; reasons.add('identity-unavailable'); }
|
|
66
|
+
if (typeof path === 'string' && PATH_HINT.test(path)) reasons.add('sensitive-path-hint');
|
|
67
|
+
const texts = [];
|
|
68
|
+
for (const side of ['left', 'right']) {
|
|
69
|
+
const source = file[side];
|
|
70
|
+
bind([side, field(source?.exists), field(source?.hash), field(source?.encoding)]);
|
|
71
|
+
const text = source?.text;
|
|
72
|
+
if (typeof text !== 'string' || text.length > LIMITS.sourceChars || used + text.length > LIMITS.totalSourceChars) {
|
|
73
|
+
complete = false; exactChanges = false; reasons.add(typeof text === 'string' ? 'source-limit' : 'source-unavailable');
|
|
74
|
+
texts.push(null); continue;
|
|
75
|
+
}
|
|
76
|
+
used += text.length; bind([text]); texts.push(text);
|
|
77
|
+
}
|
|
78
|
+
if (texts.includes(null)) continue;
|
|
79
|
+
const diff = lineDiff(texts[0], texts[1], { maxCells: 10000 });
|
|
80
|
+
if (diff.status === 'unchanged') continue;
|
|
81
|
+
if (diff.status !== 'changed' || diff.precision !== 'exact') { exactChanges = false; reasons.add('change-scope-unavailable'); continue; }
|
|
82
|
+
// Only exact added/removed lines drive escalation, never an unchanged keyword elsewhere.
|
|
83
|
+
for (const edit of diff.edits) for (const text of [edit.removed, edit.added]) {
|
|
84
|
+
for (const [category, rule] of Object.entries(CATEGORIES)) if (rule.pattern.test(text)) categories.add(category);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const category of categories) reasons.add('changed-' + category);
|
|
88
|
+
if (!reasons.size) reasons.add('standard-baseline');
|
|
89
|
+
const tier = mode === 'auto' ? (categories.size || !complete || !exactChanges ? 'deep' : 'standard') : mode;
|
|
90
|
+
const sensitive = categories.has('sensitive') || reasons.has('sensitive-path-hint');
|
|
91
|
+
return {
|
|
92
|
+
version: 1, status: 'planned', mode, tier, reasons: [...reasons].sort(),
|
|
93
|
+
checks: [...BASELINE, ...[...categories].sort().map(category => CATEGORIES[category].guidance)],
|
|
94
|
+
counterVerification: {
|
|
95
|
+
eligible: tier === 'deep', severities: ['high', 'critical'], requiresFreshEvidence: true,
|
|
96
|
+
categories: [...categories].sort(), guidance: [...categories].sort().map(category => CATEGORIES[category].guidance),
|
|
97
|
+
instructions: 'Attach these counterevidence questions to the primary review without another planning call. For high/critical candidates only, use a separate fresh verifier against exact captured evidence; do not downgrade or clear evidence merely because a counterexample was suggested.',
|
|
98
|
+
},
|
|
99
|
+
humanReview: { required: sensitive, advisoryOnly: true, approvalGranted: false },
|
|
100
|
+
identity: { snapshotIdHash: typeof snapshot.id === 'string' && snapshot.id.length <= LIMITS.metadataChars ? digest(snapshot.id) : null,
|
|
101
|
+
sourceFingerprint: complete ? hash.digest('hex') : null, complete, scope: 'captured-file-sources-and-selected-metadata', reusableCoverage: false },
|
|
102
|
+
scan: { filesScanned, totalFiles: snapshot.files.length, complete: complete && exactChanges, limits: { ...LIMITS } },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { hash } from './content.mjs';
|
|
2
|
+
import { buildFindingLedger } from './finding-ledger.mjs';
|
|
3
|
+
import { buildIncrementalSelection, incrementalFileHash } from './incremental-review.mjs';
|
|
4
|
+
const canonical = value => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().filter(k=>value[k]!==undefined).map(k=>[k,canonical(value[k])])) : value;
|
|
5
|
+
const digest = value => hash(JSON.stringify(canonical(value)));
|
|
6
|
+
export function reviewHistoryIdentity(snapshot,route,capturePolicy={}) {
|
|
7
|
+
return {repository:digest({root:snapshot.root,vcs:snapshot.vcs,repositoryUuid:snapshot.repositoryUuid ?? snapshot.history?.repositoryUuid,url:snapshot.repositoryUrl ?? snapshot.history?.url}),route:digest(route),rulesHash:digest(snapshot.rules??[]),requirementPolicyHash:digest({requirement:snapshot.businessRequirement,capturePolicy})};
|
|
8
|
+
}
|
|
9
|
+
export function reviewSourceManifest(snapshot,identity) {
|
|
10
|
+
// No inferred complete dependency graph: conservatively bind every captured source.
|
|
11
|
+
const files=snapshot.files.map(file=>({path:file.path,hash:incrementalFileHash(file)}));
|
|
12
|
+
// Dependency completeness is unknown. Do not invent an all-files graph or skip
|
|
13
|
+
// unchanged files merely because this snapshot happened to capture no callers.
|
|
14
|
+
return {identity,snapshotId:snapshot.id,files};
|
|
15
|
+
}
|
|
16
|
+
export function prepareReviewHistory(snapshot,route,capturePolicy,{previous,outcomes,explicitDiffOnly=false}={}) {
|
|
17
|
+
const identity=reviewHistoryIdentity(snapshot,route,capturePolicy);
|
|
18
|
+
const manifest=reviewSourceManifest(snapshot,identity);
|
|
19
|
+
if (!previous) {if(outcomes?.length)throw new Error('Finding outcomes require an owned prior review');return {identity,manifest};}
|
|
20
|
+
if(!previous.ledger||!previous.manifest||previous.ledger.cycle>=3)throw new Error('Review-fix cycle cap reached or prior history unavailable; step back and reconsider');
|
|
21
|
+
// Validate exact ledger identity and decisions without borrowing any conclusions.
|
|
22
|
+
buildFindingLedger({snapshotId:snapshot.id,findings:[],files:[]},identity,{previous:previous.ledger,outcomes});
|
|
23
|
+
const selection=buildIncrementalSelection(snapshot,{previousManifest:previous.manifest,currentManifest:manifest,explicitDiffOnly});
|
|
24
|
+
return {identity,manifest,previous:previous.ledger,outcomes,incremental:selection.plan,selection};
|
|
25
|
+
}
|
|
26
|
+
export function finishReviewHistory(report,prepared) {
|
|
27
|
+
const ledger=buildFindingLedger(report,prepared.identity,{...(prepared.previous?{previous:prepared.previous}:{}),...(prepared.outcomes?{outcomes:prepared.outcomes}:{})});
|
|
28
|
+
return {ledger,manifest:prepared.manifest,...(prepared.incremental?{incremental:prepared.incremental}:{})};
|
|
29
|
+
}
|
package/src/review-jobs.mjs
CHANGED
|
@@ -77,6 +77,7 @@ export function createReviewJobs({directory}={}){
|
|
|
77
77
|
return {jobId:id,status:job.record.status,waitRevision:job.savedRevision,progress:jobProgress(job.record),nextAction:'Call code_review_wait with waitFor=terminal until terminal status; reuse the returned waitRevision as after without guessing increments. No code_review_status call is needed after each event. Do not finish the user request at start.',notice:'Background job accepted; start is not a completed review. Explicit cancel or plugin disposal stops it.'};
|
|
78
78
|
},
|
|
79
79
|
async resume(id,exec){const r=await owned(id,exec);if(jobs.get(id)&&!jobs.get(id).settled||['queued','running'].includes(r.status)||!r.resumeState)throw new Error('No terminal completed-chain resume state available');return structuredClone(r.resumeState);},
|
|
80
|
+
async priorReview(id,exec){const r=await owned(id,exec);if(jobs.get(id)&&!jobs.get(id).settled||r.status!=='completed'||!r.result?.report?.reviewHistory||r.result.report.outdated)throw new Error('Owned terminal current review history unavailable');return structuredClone(r.result.report.reviewHistory);},
|
|
80
81
|
async artifacts(id,exec,page){const r=await owned(id,exec);return artifacts.read(id,r.owner,page);},
|
|
81
82
|
async status(id,exec,page){const r=structuredClone(await owned(id,exec));delete r.resumeState;const active=jobs.get(id);if(active&&!active.settled&&!['queued','running'].includes(r.status)){r.status='running';r.stage='finalizing';delete r.result;delete r.finishedAt;}r.recovery=recoveryCoverage(r);r.progress=jobProgress(r);r.waitRevision=jobs.get(id)?.savedRevision??r.checkpoints.length;r.nextAction=['queued','running'].includes(r.status)?'Call code_review_wait with waitFor=terminal; reuse the exact returned waitRevision as after, never guess increments. No code_review_status call is needed after each event. Do not end the user request while task runs.':r.result?.previewId?'Inspect preview against current explicit user authorization. For a matching explicit review request, execute with confirmed=true without asking again. Do not execute preview-only or explicit wait-for-confirmation requests; ask for authorization if scope/model/destination exceeds authorization or is unclear. Preserve host permissions and snapshot/identity checks.':'Collect final report and unfinished scope, then provide one final answer.';if(!page)return r;const {offset=0,limit=20,checkpointIndex}=page;if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(limit)||limit<1||limit>100)throw new Error('Invalid result page');if(checkpointIndex!==undefined){if(!Number.isSafeInteger(checkpointIndex)||checkpointIndex<0||checkpointIndex>=r.checkpoints.length)throw new Error('Invalid checkpoint index');return {jobId:r.jobId,status:r.status,waitRevision:r.waitRevision,progress:r.progress,nextAction:r.nextAction,checkpointIndex,checkpoint:pageCheckpoint(r.checkpoints[checkpointIndex],offset,limit),page:{offset,limit}};}const result=r.result;const report=result?.report;return {...r,result:undefined,recovery:{...r.recovery,unfinishedStages:r.recovery.unfinishedStages.slice(offset,offset+limit),files:r.recovery.files.slice(offset,offset+limit),windows:r.recovery.windows.slice(offset,offset+limit),suggestedPaths:r.recovery.suggestedPaths.slice(offset,offset+limit),totalObservedFiles:r.recovery.files.length},checkpoints:r.checkpoints.slice(offset,offset+limit).map(c=>pageCheckpoint(c,0,limit)),checkpointPage:{offset,limit,total:r.checkpoints.length},...(result?{resultSummary:report?{status:report.status,coverage:report.coverage,model:report.model,snapshotId:report.snapshotId,diagnostics:report.diagnostics,execution:report.execution,modelCalls:report.modelCalls,totalFiles:report.files?.length,totalFindings:report.findings?.length,totalLimitations:report.limitations?.length,totalScopeNotices:report.scopeNotices?.length??0}:({...result,files:result.files?.slice(offset,offset+limit),filePage:result.files?{offset,limit,total:result.files.length}:undefined}),resultPage:report?{files:report.files?.slice(offset,offset+limit),findings:report.findings?.slice(offset,offset+limit),totalFiles:report.files?.length,totalFindings:report.findings?.length}:undefined}: {})};},
|
|
82
83
|
async wait(id,exec,{after,waitFor='progress',timeoutMs=60000,signal}={}){
|