@feng0531/dsh-code-review 0.86.1 → 0.87.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.
@@ -0,0 +1,74 @@
1
+ import { createHash } from 'node:crypto';
2
+ const KINDS = new Set(['verification', 'synthesis', 'interaction']);
3
+ const REASONS = new Set(['READER_CALL_BUDGET_EXCEEDED', 'READER_OUTPUT_BUDGET_EXCEEDED', 'STAGE_INTERRUPTED', 'STAGE_CANCELLED', 'STAGE_TIMEOUT', 'STAGE_NO_PROGRESS', 'STAGE_ATTEMPTS_EXHAUSTED', 'STAGE_FAILED']);
4
+ const fail = code => Object.assign(new Error(code), { code });
5
+ const canonical = value => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().filter(key => value[key] !== undefined).map(key => [key, canonical(value[key])])) : value;
6
+ const digest = value => createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex');
7
+ const same = (a,b) => JSON.stringify(a) === JSON.stringify(b);
8
+
9
+ // Host metadata only: accepted generation belongs in the separate owner-bound
10
+ // checkpoint. No read results/receipts are persisted or minted by this machine.
11
+ export function createStageContinuation(snapshot, options, { completedPrimary = [], units = [], checkpoint, maxAttempts = 2 } = {}) {
12
+ if (!snapshot || typeof snapshot.id !== 'string' || !Array.isArray(snapshot.files) || !Number.isSafeInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 4 || !Array.isArray(units) || units.length > 2000) throw fail('INVALID_STAGE_CONTINUATION');
13
+ const fileIds = new Set(snapshot.files.filter(f => f.eligibility === 'reviewable').map(f => f.id));
14
+ if (!Array.isArray(completedPrimary) || new Set(completedPrimary).size !== completedPrimary.length || completedPrimary.some(id => !fileIds.has(id))) throw fail('INVALID_STAGE_PRIMARY');
15
+ const primary = [...completedPrimary].sort(), states = new Map();
16
+ for (const unit of units) {
17
+ if (!unit || typeof unit.id !== 'string' || !unit.id || unit.id.length > 500 || states.has(unit.id) || unit.id.startsWith('primary:') || !KINDS.has(unit.kind) || !Array.isArray(unit.fileIds) || !unit.fileIds.length || new Set(unit.fileIds).size !== unit.fileIds.length || unit.fileIds.some(id => !fileIds.has(id)) || !Array.isArray(unit.dependencies ?? []) || new Set(unit.dependencies ?? []).size !== (unit.dependencies ?? []).length) throw fail('INVALID_STAGE_UNIT');
18
+ states.set(unit.id, { id: unit.id, kind: unit.kind, fileIds: [...unit.fileIds], dependencies: [...(unit.dependencies ?? [])], status: 'pending', attempts: 0, pendingReason: null, stopped: false });
19
+ }
20
+ const visit = (id, visiting = new Set(), visited = new Set()) => {
21
+ if (visited.has(id)) return;
22
+ if (visiting.has(id)) throw fail('INVALID_STAGE_DEPENDENCIES');
23
+ visiting.add(id);
24
+ for (const dep of states.get(id).dependencies) {
25
+ if (typeof dep !== 'string' || dep === id) throw fail('INVALID_STAGE_DEPENDENCIES');
26
+ if (dep.startsWith('primary:')) { if (!fileIds.has(dep.slice(8))) throw fail('INVALID_STAGE_DEPENDENCIES'); }
27
+ else { if (!states.has(dep)) throw fail('INVALID_STAGE_DEPENDENCIES'); visit(dep, visiting, visited); }
28
+ }
29
+ visiting.delete(id); visited.add(id);
30
+ };
31
+ const visited = new Set(); for (const id of states.keys()) visit(id, new Set(), visited);
32
+ const identityHash = digest({ snapshot, options, primary, units: [...states.values()].map(({id,kind,fileIds,dependencies})=>({id,kind,fileIds,dependencies})), maxAttempts });
33
+ if (checkpoint !== undefined) {
34
+ if (!checkpoint || checkpoint.schemaVersion !== 1 || checkpoint.snapshotId !== snapshot.id || checkpoint.identityHash !== identityHash || checkpoint.maxAttempts !== maxAttempts || !same(checkpoint.completedPrimary, primary) || !Array.isArray(checkpoint.units) || checkpoint.units.length !== states.size || Object.keys(checkpoint).sort().join(',') !== 'completedPrimary,identityHash,maxAttempts,schemaVersion,snapshotId,units') throw fail('STAGE_CONTINUATION_IDENTITY_MISMATCH');
35
+ const seen = new Set();
36
+ for (const saved of checkpoint.units) {
37
+ const state = states.get(saved?.id);
38
+ if (!state || seen.has(saved.id) || !same(saved.fileIds,state.fileIds) || !same(saved.dependencies,state.dependencies) || saved.kind !== state.kind || !['pending','running','completed'].includes(saved.status) || !Number.isSafeInteger(saved.attempts) || saved.attempts < 0 || saved.attempts > maxAttempts || typeof saved.stopped !== 'boolean' || saved.pendingReason !== null && !REASONS.has(saved.pendingReason) || Object.keys(saved).sort().join(',') !== 'attempts,dependencies,fileIds,id,kind,pendingReason,status,stopped' || (saved.status !== 'pending' && saved.attempts === 0)) throw fail('INVALID_STAGE_CHECKPOINT');
39
+ seen.add(saved.id); Object.assign(state, { status: saved.status === 'running' ? 'pending' : saved.status, attempts: saved.attempts, pendingReason: saved.status === 'running' ? 'STAGE_INTERRUPTED' : saved.pendingReason, stopped: saved.stopped });
40
+ }
41
+ }
42
+ const get = id => { const state=states.get(id); if (!state) throw fail('UNKNOWN_STAGE_UNIT'); return state; };
43
+ const dependenciesReady = state => state.fileIds.every(id => primary.includes(id)) && state.dependencies.every(dep => dep.startsWith('primary:') ? primary.includes(dep.slice(8)) : get(dep).status === 'completed');
44
+ for (const state of states.values()) {
45
+ if (state.status === 'completed' && (!dependenciesReady(state) || state.stopped || state.pendingReason !== null)) throw fail('INVALID_STAGE_CHECKPOINT');
46
+ }
47
+ const copy = state => structuredClone(state);
48
+ const api = {
49
+ ready() { return [...states.values()].filter(s => s.status === 'pending' && !s.stopped && s.attempts < maxAttempts && dependenciesReady(s)).map(copy); },
50
+ start(id, { signal } = {}) {
51
+ signal?.throwIfAborted(); const state=get(id);
52
+ if (state.status !== 'pending' || state.stopped || state.attempts >= maxAttempts || !dependenciesReady(state)) throw fail('STAGE_NOT_READY');
53
+ state.status='running'; state.attempts++; state.pendingReason=null;
54
+ return { id, attempt: state.attempts, kind: state.kind, fileIds: [...state.fileIds], boundary: 'stage-start', requiresFreshEvidence: true };
55
+ },
56
+ complete(id, attempt, { signal } = {}) {
57
+ const state=get(id);
58
+ if (state.status !== 'running' || state.attempts !== attempt) throw fail('STALE_STAGE_ATTEMPT');
59
+ if (signal?.aborted) { state.status='pending'; state.pendingReason='STAGE_CANCELLED'; state.stopped=true; signal.throwIfAborted(); }
60
+ state.status='completed'; state.pendingReason=null;
61
+ },
62
+ yield(id, reason = 'STAGE_NO_PROGRESS') {
63
+ const state=get(id); if (state.status !== 'running' || !REASONS.has(reason)) throw fail('INVALID_STAGE_TRANSITION');
64
+ state.status='pending'; state.pendingReason=state.attempts >= maxAttempts ? 'STAGE_ATTEMPTS_EXHAUSTED' : reason; state.stopped=state.attempts >= maxAttempts;
65
+ },
66
+ stop(id, reason = 'STAGE_NO_PROGRESS') {
67
+ const state=get(id); if (state.status === 'completed' || !REASONS.has(reason)) throw fail('INVALID_STAGE_TRANSITION');
68
+ state.status='pending'; state.pendingReason=reason; state.stopped=true;
69
+ },
70
+ checkpoint() { return { schemaVersion: 1, snapshotId: snapshot.id, identityHash, maxAttempts, completedPrimary: [...primary], units: [...states.values()].map(copy) }; },
71
+ summary() { const values=[...states.values()]; return { units: values.length, completed: values.filter(s=>s.status==='completed').length, pending: values.filter(s=>s.status==='pending').length, running: values.filter(s=>s.status==='running').length, stopped: values.filter(s=>s.stopped).length, attempts: values.reduce((n,s)=>n+s.attempts,0) }; },
72
+ };
73
+ return Object.freeze(api);
74
+ }
@@ -0,0 +1,22 @@
1
+ import { prepareStageGenerations } from './review-stage-state.mjs';
2
+
3
+ // Only host-validated generations enter this cache. Failed admission leaves the
4
+ // last good set unchanged; imported checkpoints still fail closed in the validator.
5
+ export function createStageGenerationCache(snapshot, records = [], { maxBytes = 8 * 1024 * 1024 } = {}) {
6
+ let saved = prepareStageGenerations(snapshot, records, { maxBytes });
7
+ return {
8
+ records() { return structuredClone(saved); },
9
+ admit(record) {
10
+ let normalized;
11
+ try { [normalized] = prepareStageGenerations(snapshot, [record]); }
12
+ catch { return { accepted: false, code: 'STAGE_CACHE_RECORD_REJECTED' }; }
13
+ const next = saved.filter(row => row.key !== normalized.key);
14
+ next.push(normalized);
15
+ let checked;
16
+ try { checked = prepareStageGenerations(snapshot, next, { maxBytes }); }
17
+ catch { return { accepted: false, code: 'STAGE_CACHE_CAPACITY_EXCEEDED' }; }
18
+ saved = checked;
19
+ return { accepted: true, record: structuredClone(normalized) };
20
+ },
21
+ };
22
+ }
@@ -1,4 +1,5 @@
1
- import { VERIFICATION_INSTRUCTIONS } from './prompts.mjs';
1
+ import { prepareMigrationRegression, migrationRegressionMetadata } from './migration-regression.mjs';
2
+ import { VERIFICATION_INSTRUCTIONS, MIGRATION_VERIFICATION_INSTRUCTIONS } from './prompts.mjs';
2
3
  import { CONTEXT_SIDE_NOTICE } from './context-identity.mjs';
3
4
  import { defectGuidance } from './defect-guidance.mjs';
4
5
  import { modelWireBudget } from './model-wire-budget.mjs';
@@ -15,13 +16,17 @@ export { VERIFICATION_INSTRUCTIONS };
15
16
  export async function verificationLoop(model, request, candidates, scope, options = {}) {
16
17
  request.signal?.throwIfAborted();
17
18
  if (!Array.isArray(candidates) || candidates.length > 50) throw new Error('Invalid candidates');
19
+ if (options.enableMigrationRegression !== undefined && typeof options.enableMigrationRegression !== 'boolean') throw new Error('Invalid migration regression policy');
18
20
  if (!candidates.length) return [];
19
21
  const ids = candidates.map((_, i) => 'c' + (i + 1));
20
22
  const envelope = request.input ? JSON.parse(request.input) : {};
21
23
  const rules = envelope.rules ?? [];
22
- const input = JSON.stringify({ contextSideNotice:CONTEXT_SIDE_NOTICE, ...(envelope.businessRequirement!==undefined?{businessRequirement:envelope.businessRequirement,requirementNotice:envelope.requirementNotice}:{}), defectGuidance: [...new Set(candidates.map(f => scope.catalog().find(s => s.fileId === f.fileId)?.path).filter(Boolean))].map(path => envelope.file?.path === path && envelope.defectGuidance ? envelope.defectGuidance : envelope.files?.find(f=>f.path===path)?.defectGuidance ?? defectGuidance(path)), rules, candidates: candidates.map((finding, i) => ({ candidateId: ids[i], finding })) });
24
+ const project = options.migrationSnapshot?.projectScope || options.migrationSnapshot?.files?.some(file => file.reviewMode === 'project') || envelope.file?.reviewMode === 'project';
25
+ if (options.enableMigrationRegression === true && !project && options.migrationContracts?.length && options.migrationSnapshot?.id !== envelope.snapshotId) throw new Error('Migration verification snapshot mismatch');
26
+ const migrationContext = options.enableMigrationRegression === true && !project && options.migrationContracts?.length ? prepareMigrationRegression(options.migrationSnapshot, scope, options.migrationContracts, candidates) : undefined;
27
+ const input = JSON.stringify({ ...(envelope.snapshotId ? { snapshotId: envelope.snapshotId } : {}), ...(migrationContext ? { migrationContracts: migrationRegressionMetadata(migrationContext) } : {}), contextSideNotice:CONTEXT_SIDE_NOTICE, ...(envelope.businessRequirement!==undefined?{businessRequirement:envelope.businessRequirement,requirementNotice:envelope.requirementNotice}:{}), defectGuidance: [...new Set(candidates.map(f => scope.catalog().find(s => s.fileId === f.fileId)?.path).filter(Boolean))].map(path => envelope.file?.path === path && envelope.defectGuidance ? envelope.defectGuidance : envelope.files?.find(f=>f.path===path)?.defectGuidance ?? defectGuidance(path)), rules, candidates: candidates.map((finding, i) => ({ candidateId: ids[i], finding })) });
23
28
  const seedReceipts = new Map();
24
- const verificationRequest = { ...request, input, instructions:envelope.file?.reviewMode==='project'?VERIFICATION_INSTRUCTIONS+' 本次为当前项目源码审查,空old只是传输占位,不是历史版本。不要要求回归因果;regression.classification用uncertain并说明不适用,仅凭new侧原文核实当前缺陷和反证。':VERIFICATION_INSTRUCTIONS };
29
+ const verificationRequest = { ...request, input, instructions:envelope.file?.reviewMode==='project'?VERIFICATION_INSTRUCTIONS+' 本次为当前项目源码审查,空old只是传输占位,不是历史版本。不要要求回归因果;regression.classification用uncertain并说明不适用,仅凭new侧原文核实当前缺陷和反证。':VERIFICATION_INSTRUCTIONS + (migrationContext ? ' ' + MIGRATION_VERIFICATION_INSTRUCTIONS : '') };
25
30
  // Explicit final-only mode has extra finalization metadata/instructions and
26
31
  // permits no retrieval rounds. Do not spend its budget on optional prefetch.
27
32
  const initialRetrieved = options.maxRounds === 0 ? [] : verificationSeeds(candidates, scope, {
@@ -39,5 +44,5 @@ export async function verificationLoop(model, request, candidates, scope, option
39
44
  // read under this invocation's receipt table and the shared approved budget.
40
45
  const response = await retrievalLoop(model, verificationRequest, retrievalScope, { ...options, initialRetrieved });
41
46
  request.signal?.throwIfAborted();
42
- return validateCandidateVerification(response, ids, scope.read, resolveEvidence, { catalog: scope.catalog(), fileIds: Object.fromEntries(ids.map((id, i) => [id, candidates[i].fileId])) });
47
+ return validateCandidateVerification(response, ids, scope.read, resolveEvidence, { enableMigrationRegression: options.enableMigrationRegression === true && !project, migrationContext, catalog: scope.catalog(), fileIds: Object.fromEntries(ids.map((id, i) => [id, candidates[i].fileId])) });
43
48
  }