@stardustlc/dsh-review 0.3.0 → 0.3.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 +5 -0
- package/docs/VALIDATION.md +4 -0
- package/docs/validation-0.3.1.md +19 -0
- package/lib/gate.d.ts +1 -1
- package/lib/gate.js +20 -6
- package/lib/lenses.js +1 -0
- package/lib/run.d.ts +2 -1
- package/lib/run.js +11 -6
- package/lib/target.js +4 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/docs/VALIDATION.md
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 0.3.1 验证记录
|
|
2
|
+
|
|
3
|
+
日期:2026-10-26。官方 Windows 宿主:DSH 0.2.1-rc.3。
|
|
4
|
+
|
|
5
|
+
## 行为变化
|
|
6
|
+
|
|
7
|
+
复核证据绑定候选位置与非空源码锚点;旧 finding 仍存在或缺少原文件覆盖时不自动标为已修复。仅排除真实账本目录,默认配置不再隐藏仓库同名目录。
|
|
8
|
+
|
|
9
|
+
## 验证与限制
|
|
10
|
+
|
|
11
|
+
- Windows 本地:78 项,78 通过、0 跳过;没有把跳过计为通过。
|
|
12
|
+
- 官方 RC.3 原生 tools.execute 与审阅子代理验收通过:19 次工具调用,覆盖独立复核、旧快照、真实修复、只读权限、取消和不可信手工报告。模型为本地合成适配器,未评测真实模型的审阅质量。
|
|
13
|
+
- GitHub 的跨平台 CI 和 npm 发布完整性以该版本工作流及 registry 为准;本记录不把本地测试等同于已发布。
|
|
14
|
+
|
|
15
|
+
## English
|
|
16
|
+
|
|
17
|
+
Verification requires nonblank snapshot anchors near the candidate. Existing findings remain unresolved when their original anchor still exists or coverage is missing. Only the actual managed ledger directory is excluded.
|
|
18
|
+
|
|
19
|
+
Runtime/account limits are listed above. Synthetic peers and model adapters do not establish live account delivery or model quality.
|
package/lib/gate.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ export interface GateResult {
|
|
|
63
63
|
}
|
|
64
64
|
export declare function candidateId(f: CandidateFinding, target: ReviewTarget): string;
|
|
65
65
|
export declare function parseCandidate(raw: unknown, lens: string, target: ReviewTarget): CandidateFinding | null;
|
|
66
|
-
export declare function parseVote(raw: unknown, taskId: string, expected: string, target: ReviewTarget): Vote;
|
|
66
|
+
export declare function parseVote(raw: unknown, taskId: string, expected: string, target: ReviewTarget, candidate?: CandidateFinding): Vote;
|
|
67
67
|
export declare function voteDecision(votes: Vote[], required: number): Vote['decision'];
|
|
68
68
|
export declare function gateFindings(options: {
|
|
69
69
|
root: string;
|
package/lib/gate.js
CHANGED
|
@@ -39,15 +39,22 @@ export function parseCandidate(raw, lens, target) {
|
|
|
39
39
|
finding.id = candidateId(finding, target);
|
|
40
40
|
return finding;
|
|
41
41
|
}
|
|
42
|
-
export function parseVote(raw, taskId, expected, target) {
|
|
42
|
+
export function parseVote(raw, taskId, expected, target, candidate) {
|
|
43
43
|
const item = raw;
|
|
44
44
|
if (!item || item.candidateId !== expected || !['confirm', 'reject', 'pre-existing', 'unverifiable'].includes(item.decision) || typeof item.reason !== 'string' || item.reason.trim().length < 12 || !Array.isArray(item.witnesses))
|
|
45
45
|
throw new Error('Invalid independent verification');
|
|
46
46
|
for (const witness of item.witnesses)
|
|
47
|
-
if (!witness || (witness.side !== undefined && !['before', 'after'].includes(witness.side)) || !sourceMatches(target, witness.file, witness.line, witness.anchor, witness.side ?? 'after'))
|
|
48
|
-
throw new Error('Verifier
|
|
47
|
+
if (!witness || typeof witness.file !== 'string' || typeof witness.anchor !== 'string' || !witness.anchor.trim() || (witness.side !== undefined && !['before', 'after'].includes(witness.side)) || !sourceMatches(target, witness.file, witness.line, witness.anchor, witness.side ?? 'after'))
|
|
48
|
+
throw new Error('Verifier witness is blank or outside the immutable snapshot');
|
|
49
49
|
if (item.decision !== 'unverifiable' && item.witnesses.length === 0)
|
|
50
50
|
throw new Error('Verifier supplied no checkable source witness');
|
|
51
|
+
if (item.decision !== 'unverifiable') {
|
|
52
|
+
if (!candidate || (candidate.id ?? candidateId(candidate, target)) !== expected)
|
|
53
|
+
throw new Error('Verifier requires the exact candidate being checked');
|
|
54
|
+
const locations = [candidate, ...(candidate.introducedBy ? [candidate.introducedBy] : [])];
|
|
55
|
+
if (!item.witnesses.some(witness => locations.some(location => witness.file === location.file && Math.abs(witness.line - location.line) <= 3)))
|
|
56
|
+
throw new Error('Verifier witness is unrelated to the candidate or introducing change');
|
|
57
|
+
}
|
|
51
58
|
if (item.decision === 'pre-existing' && !item.witnesses.some(witness => witness.side === 'before'))
|
|
52
59
|
throw new Error('Pre-existing verdict requires base evidence');
|
|
53
60
|
return { candidateId: expected, decision: item.decision, reason: item.reason.trim(), witnesses: item.witnesses, taskId };
|
|
@@ -82,14 +89,21 @@ export function gateFindings(options) {
|
|
|
82
89
|
continue;
|
|
83
90
|
}
|
|
84
91
|
if (decision === 'reject') {
|
|
85
|
-
result.rejected.push({ id, reason: votes.map(v => v.reason).join('\n') });
|
|
86
92
|
const prior = record.posted[id];
|
|
87
93
|
if (prior && !prior.disposition) {
|
|
88
|
-
|
|
89
|
-
prior.
|
|
94
|
+
const source = target.files.find(file => file.path === prior.file || file.oldPath === prior.file);
|
|
95
|
+
const removed = source !== undefined && typeof prior.anchor === 'string' && prior.anchor.trim() !== '' && (source.after === null || !source.after.split(/\r?\n/).includes(prior.anchor));
|
|
96
|
+
if (!removed) {
|
|
97
|
+
result.unverified++;
|
|
98
|
+
result.rejected.push({ id, reason: 'The original source anchor remains, or its removal is not covered by this snapshot; human review is required.' });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
prior.disposition = 'fixed';
|
|
102
|
+
prior.note = 'Independent re-verification and snapshot source confirm removal on target ' + target.id;
|
|
90
103
|
prior.verification = votes;
|
|
91
104
|
prior.targetId = target.id;
|
|
92
105
|
}
|
|
106
|
+
result.rejected.push({ id, reason: votes.map(v => v.reason).join('\n') });
|
|
93
107
|
continue;
|
|
94
108
|
}
|
|
95
109
|
if (decision !== 'pre-existing' && severityRank(finding.severity) < severityRank(rules.severityFloor ?? config.severityFloor)) {
|
package/lib/lenses.js
CHANGED
|
@@ -37,6 +37,7 @@ export function verifierTask(input, finding, index) {
|
|
|
37
37
|
'Trace the trigger, consequences, relevant callers and counterexamples. Check whether the defect was already present in the base, rather than merely whether the same line existed.',
|
|
38
38
|
'Return candidateId, decision=confirm|reject|pre-existing|unverifiable, reason (specific causal explanation), witnesses=[{file,line,anchor,side:\"after\"|\"before\"}].',
|
|
39
39
|
'Witnesses must quote exact snapshot source. confirm requires evidence linked to this introducing change; pre-existing requires before-side evidence. If evidence is insufficient use unverifiable. Command receipts are optional; do not run arbitrary code solely to fill a field.',
|
|
40
|
+
'Every witness anchor must be nonblank. At least one witness must be in the candidate file within three lines of its location, or within three lines of its introducing evidence. Include this local witness even when additional evidence is elsewhere.',
|
|
40
41
|
].join('\n'));
|
|
41
42
|
return task;
|
|
42
43
|
}
|
package/lib/run.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { type LensTask } from './lenses.js';
|
|
2
2
|
import { type Mode } from './modes.js';
|
|
3
|
+
import { type CandidateFinding } from './gate.js';
|
|
3
4
|
import { Ledger, type StoredRun } from './ledger.js';
|
|
4
5
|
import { type ReviewTarget } from './target.js';
|
|
5
6
|
import type { ReviewConfig } from './config.js';
|
|
6
7
|
import type { Dispatcher } from './dispatch.js';
|
|
7
8
|
export declare function startRun(target: ReviewTarget, mode: Mode, config: ReviewConfig, ledger: Ledger, independent: boolean): StoredRun;
|
|
8
|
-
export declare function parseTaskResult(task: LensTask, text: string, target: ReviewTarget, maxFindings: number): Record<string, any>;
|
|
9
|
+
export declare function parseTaskResult(task: LensTask, text: string, target: ReviewTarget, maxFindings: number, candidate?: CandidateFinding): Record<string, any>;
|
|
9
10
|
export declare function submitResults(ledger: Ledger, runId: string, targetId: string, values: unknown[], config: ReviewConfig): void;
|
|
10
11
|
export declare function advance(ledger: Ledger, runId: string, config: ReviewConfig): void;
|
|
11
12
|
export declare function assertCurrent(run: StoredRun, ledger: Ledger, config: ReviewConfig, signal?: AbortSignal): Promise<void>;
|
package/lib/run.js
CHANGED
|
@@ -38,7 +38,7 @@ export function startRun(target, mode, config, ledger, independent) {
|
|
|
38
38
|
ledger.event('run-start', { runId: id, targetId: target.id, mode, independent });
|
|
39
39
|
return run;
|
|
40
40
|
}
|
|
41
|
-
export function parseTaskResult(task, text, target, maxFindings) {
|
|
41
|
+
export function parseTaskResult(task, text, target, maxFindings, candidate) {
|
|
42
42
|
if (Buffer.byteLength(text) > 1024 * 1024)
|
|
43
43
|
throw new Error('Worker output exceeds 1 MiB');
|
|
44
44
|
const value = JSON.parse(text.trim().replace(/^\x60{3}(?:json)?\s*\n([\s\S]*?)\n\x60{3}$/, '$1'));
|
|
@@ -51,7 +51,7 @@ export function parseTaskResult(task, text, target, maxFindings) {
|
|
|
51
51
|
return { runId: task.runId, targetId: task.targetId, taskId: task.id, findings: parsed.filter(Boolean), ignoredStyleCount: parsed.filter((finding) => finding === null).length, declined: value.declined };
|
|
52
52
|
}
|
|
53
53
|
if (task.role === 'verify')
|
|
54
|
-
return { runId: task.runId, targetId: task.targetId, ...parseVote(value, task.id, task.candidateId, target) };
|
|
54
|
+
return { runId: task.runId, targetId: task.targetId, ...parseVote(value, task.id, task.candidateId, target, candidate) };
|
|
55
55
|
if (task.role === 'risk') {
|
|
56
56
|
if (!['low', 'medium', 'high'].includes(value.tier) || typeof value.extraHuman !== 'boolean' || !Array.isArray(value.reasons) || !value.reasons.length || value.reasons.some((r) => typeof r !== 'string' || !r.trim()))
|
|
57
57
|
throw new Error('Invalid risk classification');
|
|
@@ -71,7 +71,7 @@ export function submitResults(ledger, runId, targetId, values, config) {
|
|
|
71
71
|
const task = pending.find(task => task.id === value?.taskId);
|
|
72
72
|
if (!task)
|
|
73
73
|
throw new Error('Result duplicated or not a task in this stage');
|
|
74
|
-
return [task.id, parseTaskResult(task, JSON.stringify(value), run.target, config.maxFindings)];
|
|
74
|
+
return [task.id, parseTaskResult(task, JSON.stringify(value), run.target, config.maxFindings, run.candidates.find(candidate => candidate.id === task.candidateId))];
|
|
75
75
|
});
|
|
76
76
|
if (new Set(parsed.map(([id]) => id)).size !== parsed.length)
|
|
77
77
|
throw new Error('Duplicate task result');
|
|
@@ -84,8 +84,13 @@ function votesFor(run) {
|
|
|
84
84
|
const votes = {};
|
|
85
85
|
for (const task of run.tasks.filter(task => task.role === 'verify')) {
|
|
86
86
|
const value = run.results[task.id];
|
|
87
|
-
if (value)
|
|
88
|
-
|
|
87
|
+
if (value) {
|
|
88
|
+
try {
|
|
89
|
+
const checked = parseVote(value, task.id, task.candidateId, run.target, run.candidates.find(candidate => candidate.id === task.candidateId));
|
|
90
|
+
(votes[task.candidateId] ??= []).push(checked);
|
|
91
|
+
}
|
|
92
|
+
catch { /* Old or invalid persisted votes leave the finding unverified. */ }
|
|
93
|
+
}
|
|
89
94
|
}
|
|
90
95
|
return votes;
|
|
91
96
|
}
|
|
@@ -217,7 +222,7 @@ async function executeStages(ledger, runId, config, dispatcher, signal) {
|
|
|
217
222
|
if (!result.ok)
|
|
218
223
|
throw new Error(result.reason ?? 'Worker failed');
|
|
219
224
|
const text = result.output ?? '';
|
|
220
|
-
const parsed = parseTaskResult(task, text, run.target, config.maxFindings);
|
|
225
|
+
const parsed = parseTaskResult(task, text, run.target, config.maxFindings, run.candidates.find(candidate => candidate.id === task.candidateId));
|
|
221
226
|
ledger.transaction(state => { state.runs[runId].results[task.id] = parsed; });
|
|
222
227
|
}
|
|
223
228
|
catch (error) {
|
package/lib/target.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { git } from './exec.js';
|
|
5
5
|
import { parseDiff } from './diff.js';
|
|
6
6
|
import { loadRules, parseRules, RULE_PATHS, isSkipped } from './rules.js';
|
|
7
|
+
import { stateDirectory } from './ledger.js';
|
|
7
8
|
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
8
9
|
export async function captureTarget(root, args, maxBytes, stateDir, signal) {
|
|
9
10
|
root = realpathSync(root);
|
|
@@ -64,8 +65,9 @@ export async function captureTarget(root, args, maxBytes, stateDir, signal) {
|
|
|
64
65
|
else
|
|
65
66
|
rules = loadRules(root);
|
|
66
67
|
const skipped = [], files = [];
|
|
67
|
-
const
|
|
68
|
-
const
|
|
68
|
+
const relativeState = path.relative(root, stateDirectory(root, stateDir)).replaceAll('\\', '/');
|
|
69
|
+
const internal = relativeState && relativeState !== '..' && !relativeState.startsWith('../') && !path.isAbsolute(relativeState) ? relativeState : undefined;
|
|
70
|
+
const inState = (file) => internal !== undefined && (file === internal || file.startsWith(internal + '/'));
|
|
69
71
|
let total = Buffer.byteLength(patch);
|
|
70
72
|
const blob = async (spec) => query(['show', '--no-ext-diff', '--no-textconv', spec]);
|
|
71
73
|
for (const entry of entries) {
|