@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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.3.1(2026-10-26)
4
+
5
+ - 复核证据绑定候选位置与非空源码锚点;旧 finding 仍存在或缺少原文件覆盖时不自动标为已修复。仅排除真实账本目录,默认配置不再隐藏仓库同名目录。
6
+ - Windows 回归 78 项通过、0 项跳过;宿主与未验收范围见[本次记录](docs/validation-0.3.1.md)。
7
+
3
8
  ## 0.3.0(2026-10-17)
4
9
 
5
10
  重做 v0.2 中尚未可靠接通的审阅闭环,按实际执行和证据决定是否完成。
@@ -1,5 +1,9 @@
1
1
  # 验证记录
2
2
 
3
+ ## 2026-10-26:0.3.1
4
+
5
+ [本次修复、回归与未验收范围](validation-0.3.1.md)。
6
+
3
7
  日期:2026-10-17。版本:@stardustlc/dsh-review 0.3.0。
4
8
 
5
9
  ## 本地回归
@@ -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 cited source outside the immutable snapshot');
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
- prior.disposition = prior.targetId === target.id ? 'wrong' : 'fixed';
89
- prior.note = 'Independent re-verification rejects the former finding on target ' + target.id;
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
- (votes[task.candidateId] ??= []).push(value);
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 internal = path.isAbsolute(stateDir) ? path.relative(root, stateDir).replaceAll('\\', '/') : stateDir;
68
- const inState = (file) => file === internal || file.startsWith(internal + '/') || file === '.dsh-review' || file.startsWith('.dsh-review/');
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardustlc/dsh-review",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "以证据为准的代码审阅插件:审 diff、验证结论、记录噪音,让 agent 先读、人只读结论。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",