@feng0531/dsh-code-review 0.60.0 → 0.60.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
@@ -2,9 +2,13 @@
2
2
 
3
3
  本文件介绍对使用者有意义的变化。安装与参数见 [README](README.md),本次发布正文见 [发布说明](RELEASE-NOTES.md)。历史提交和安装包保留在 [GitHub Releases](https://github.com/f-e-n-g-0531/dsh-code-review/releases);本文不把开发日志当作功能介绍。
4
4
 
5
+ ## 0.60.1 — SVN 阻断状态的准确诊断
6
+
7
+ 对替换/复制替换、缺失、阻塞、不完整、文本/属性/树冲突、switched 与 external 分别给出稳定 blockCode 和解释,预览及结构化报告保留原因码。普通替换实测 local BASE 无 pristine,复制替换可能返回来源 BASE,因此继续明确阻断,不合成旧侧、不冒充新增。诊断改进不表示真实双侧替换审查已支持。
8
+
5
9
  ## 0.60.0 — SVN 普通目录新增与删除后代覆盖
6
10
 
7
- 离线展开默认 status 隐藏的删除目录后代,纳入原文件数与共享元数据预算并在采集末尾复核。普通新增和删除目录只在后代显式选择且可审查时开放目录节点,后代仍逐项覆盖;凭据或阻断子项不会被目录成功掩盖。不扩展到目录复制、替换或历史模式。
11
+ 离线展开默认 status 隐藏的删除目录后代,纳入原文件数与共享元数据预算并在采集末尾复核。普通新增和删除目录只在后代显式选择且可审查时开放目录节点,后代仍逐项覆盖;凭据或阻断子项不会被目录成功掩盖。不扩展到目录复制、替换或历史模式。461 项串行回归、宿主、隔离安装与四矩阵 CI 通过;原产物/npm/Release 118,284 字节逐字节一致,双哈希、清单与冻结发布说明通过。
8
12
 
9
13
  ## 0.59.0 — SVN 复制新增文件与移动两侧覆盖
10
14
 
@@ -32,7 +32,8 @@
32
32
 
33
33
  ## 当前验证结论
34
34
 
35
- - 最近完整回归:459 项通过。
35
+ - 最近完整回归:461 项通过。
36
+ - v0.60.0 已交付 SVN 普通目录新增/删除的后代逐项覆盖,离线补齐默认 status 隐藏的删除后代;显式选择、凭据排除与原预算仍生效。461 项串行回归、宿主、隔离安装和四矩阵 CI 通过;原产物/npm/Release 118,284 字节逐字节一致,双哈希、清单与冻结发布说明通过。目录复制、替换及历史等仍在后续矩阵中。
36
37
  - v0.59.0 已交付 SVN 同仓库复制新增文件和文件移动的新增/删除独立覆盖。459 项串行回归、实际宿主、隔离安装和四组 CI 通过;原产物/npm/Release 117,098 字节逐字节一致,SHA-256、SHA-512、清单与冻结发布说明通过。复制替换、目录后代和历史审查等仍未闭环。
37
38
  - v0.58.1 已交付 SVN 根目录 mergeinfo 属性预览修复及全面支持矩阵。456 项串行回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 116,254 字节逐字节一致,SHA-256、SHA-512、清单与冻结发布说明核验通过。SVN 全面扩展目标仍在进行,历史与特殊状态尚未全部支持。
38
39
  - v0.58.0 已正式交付实验性本地评测入口及可靠性整理。454 项串行回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 113,697 字节逐字节一致,SHA-256、SHA-512、清单与冻结发布说明核验通过。正常 DSH 审查行为与预算不变;尚无真实模型基准评分。
package/RELEASE-NOTES.md CHANGED
@@ -1,16 +1,13 @@
1
- # v0.60.0 发布说明
1
+ # v0.60.1 发布说明
2
2
 
3
- ## SVN 普通目录新增与删除后代覆盖
3
+ ## SVN 阻断状态的准确诊断
4
4
 
5
- SVN 默认 status 可能只显示删除目录父节点,隐藏其后代。本版本在离线、原文件数上限和共享元数据预算内展开这些后代,并在采集末尾复核清单。
5
+ 区分普通替换、复制替换、缺失、阻塞、不完整、文本冲突、属性冲突、树冲突、switched 与 external,提供稳定 blockCode 与解释;预览和结构化审查报告保留原因码。
6
6
 
7
- - 普通新增和删除目录的属性、子目录、子文件分别列入快照覆盖。
8
- - 指定父目录不会自动批准后代;未全选后代时明确覆盖不足。
9
- - 凭据子文件继续排除,阻断或排除后代不会被父目录的成功状态掩盖。
10
- - 独立测试验证仓库离线、删除子文件 BASE 正文、稳定快照、预览零发送和文件数超限。
7
+ - 缺失不冒充计划删除;冲突标记不冒充已解决目标。
8
+ - 普通替换的 local BASE 可能无 pristine,复制替换的 BASE 可能属于来源,均不能当作目标旧侧。
9
+ - 独立 SVN 替换测试验证不读取伪造双侧、预览零发送、覆盖非完成及工作副本状态不变。
11
10
 
12
- ## 当前限制
13
-
14
- 本版不支持目录复制或替换,未跟踪目录也不能自动读取其子文件。关键字、固定 revision 历史审查等继续按 [SVN 支持矩阵](SVN-SUPPORT-MATRIX.md) 推进;全面 SVN 目标尚未完成。
11
+ 本版本提升诊断,不宣称替换或冲突场景已经可进行完整代码审查;这些项目仍在 [SVN 支持矩阵](SVN-SUPPORT-MATRIX.md) 中保留待实现。正常节点的读取与预算不变。
15
12
 
16
13
  日常使用见 [README](README.md)。
@@ -18,9 +18,9 @@
18
18
  | 子目录属性变更 | 已有独立属性审查测试 | 第一批 |
19
19
  | 根目录属性/mergeinfo | v0.58.1 已发布;独立复现后修复,指定三文件和默认预览均通过、0 模型发送、状态不变;456 项串行回归、宿主、隔离安装和四矩阵 CI 通过,原产物/npm/Release 116,254 字节逐字节一致及双哈希/清单/发布说明通过 | 第一批先发布此修复 |
20
20
  | 复制/移动、copyfrom 及目录结构变更 | v0.59.0 已发布同仓库复制新增文件及文件移动的新增/删除独立覆盖:新增目标旧侧为空,copyfrom 只作来源信息,预览披露并冻结身份;离线与凭据来源拒绝测试通过。459 项串行回归、宿主、隔离安装、四矩阵 CI 通过;原产物/npm/Release 117,098 字节逐字节一致,双哈希/清单/冻结发布说明通过。复制目录及后代覆盖仍未实现 | 第二批:明确旧侧来源和后代覆盖 |
21
- | 普通新增/删除目录及后代 | 本地开发补齐默认 status 隐藏的删除后代,离线递归枚举与末尾清单复核;普通结构目录只在后代全选且可审查时开放,凭据/阻断子项导致目录覆盖不足。尚待本批发布核验 | 第二批普通目录子批 |
22
- | 替换、删除再新增、文件/目录类型变化 | 当前 replaced 被拒绝 | 第二批:区分 BASE 旧节点与目标节点 |
23
- | 缺失、文本/属性/树冲突、阻塞 | 当前多数仅统一 blocked | 第三批:明确状态诊断;不能合成不存在的工作侧或声称审核通过 |
21
+ | 普通新增/删除目录及后代 | v0.60.0 已发布:补齐默认 status 隐藏的删除后代,离线递归枚举与末尾清单复核;普通结构目录只在后代全选且可审查时开放,凭据/阻断子项导致目录覆盖不足。461 项串行回归、宿主、隔离安装与四矩阵 CI 通过;原产物/npm/Release 118,284 字节逐字节一致,双哈希/清单/冻结发布说明通过 | 第二批普通目录子批 |
22
+ | 替换、删除再新增、文件/目录类型变化 | 普通与复制替换分别用 SVN_REPLACEMENT/SVN_COPIED_REPLACEMENT 诊断;离线探测确认前者 BASE 无 pristine,后者可能返回复制来源,禁止伪造旧侧。真实双侧替换仍未支持 | 第二批:区分 BASE 旧节点与目标节点 |
23
+ | 缺失、文本/属性/树冲突、阻塞 | 本地新增稳定状态诊断:SVN_MISSING_NODE/OBSTRUCTED_NODE/INCOMPLETE_NODE/TEXT_CONFLICT/PROPERTY_CONFLICT/TREE_CONFLICT 等;预览与报告透传 blockCode。诊断不算真实双侧审查支持,仍需后续场景实现,待本批发布核验 | 第三批:明确状态诊断;不能合成不存在的工作侧或声称审核通过 |
24
24
  | svn:keywords、svn:eol-style | keywords 当前阻断;换行保留原文但无规范化映射 | 第三批:双侧可验证映射,保留原字节和定位 |
25
25
  | svn:special/链接 | 当前阻断,不跟随链接取正文 | 第三批:如支持只审链接目标数据;不沿链接读取文件 |
26
26
  | switched、externals、稀疏/混合 revision | 多数拒绝;调用方/依赖链要求祖先 normal | 第四批:明确仓库边界与独立来源,禁止隐式跨仓读取 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.60.0",
3
+ "version": "0.60.1",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
package/src/review.mjs CHANGED
@@ -111,7 +111,7 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
111
111
  const interactionPlans = scope && report.grouping ? report.grouping.groups.filter(g => g.fileIds.length > 1).map(group => ({ group, prepared: prepareInteractionInput(input, group, payload => initialInputBudget({ instructions: REVIEW_INSTRUCTIONS, input: payload }, scope, maxInputBytes)) })) : [];
112
112
  for (const plan of interactionPlans.filter(p => p.prepared.status !== 'not-applicable')) report.interactions.push({ groupId: plan.group.id, fileIds: plan.prepared.fileIds, status: plan.prepared.status === 'ready' ? 'pending' : 'blocked', reason: plan.prepared.reason, lifecycle: 'shares-last-primary-timeout' });
113
113
  for (const file of input.files) {
114
- const state = { fileId: file.id, path: file.path, status: file.eligibility === 'reviewable' ? 'pending' : file.eligibility, reason: file.reason };
114
+ const state = { fileId: file.id, path: file.path, status: file.eligibility === 'reviewable' ? 'pending' : file.eligibility, reason: file.reason, ...(file.blockCode ? {blockCode:file.blockCode} : {}) };
115
115
  report.files.push(state);
116
116
  if (state.status !== 'pending') continue;
117
117
  if (signal?.aborted) { state.status = 'cancelled'; state.reason = '用户取消'; continue; }
package/src/service.mjs CHANGED
@@ -49,7 +49,7 @@ export function createReviewService(llm, { capture = captureSnapshot, now = Date
49
49
  while (previews.size >= maxPreviews) previews.delete(previews.keys().next().value);
50
50
  const previewId = randomUUID();
51
51
  previews.set(previewId, { ...current, target, options, rounds, snapshotId: snapshot.id, expires: now() + ttlMs });
52
- return { ...(options.businessRequirement!==undefined?{businessRequirement:{text:options.businessRequirement,hash:hash(options.businessRequirement),bytes:Buffer.byteLength(options.businessRequirement)}}:{}), previewId, plan, snapshotId: snapshot.id, repositoryRoot: snapshot.root, vcs: snapshot.vcs, ...(snapshot.history ? { history: snapshot.history } : {}), model: current.route, modelSendingEnabled: allowModelSending, files: snapshot.files.map(f => ({ path: f.path, eligibility: f.eligibility, reason: f.reason, ...(f.copySource ? {copySource:f.copySource,copyNotice:f.copyNotice} : {}) })), contextPaths: snapshot.context.map(c => c.path), ...(options.oldContextPaths !== undefined ? {oldContextPaths:options.oldContextPaths,oldContextSources:snapshot.context.filter(c=>c.oldOnly).map(c=>({path:c.path,side:'context-old',revision:c.oldRevision,blobOid:c.oldBlobOid,hash:c.oldHash}))} : {}), ...(snapshot.callerDiscovery ? {callerDiscovery:snapshot.callerDiscovery} : {}), ...(snapshot.autoContext ? { autoContext: snapshot.autoContext } : {}), rules: (snapshot.rules ?? []).map(({ path, hash }) => ({ path, hash })), notice: (options.oldContextPaths !== undefined ? 'oldContextPaths仅为baseline旧侧正文,无目标侧替代;选中变更复用原old身份,旧侧不能证明新侧行为。' : '') + (snapshot.callerDiscovery ? 'callerScopePaths已授权预览读取限定目录内tracked候选正文,包括不匹配文件;非匹配正文不发送模型,匹配捕获上下文会发送。仅字面引用导航,不是函数调用或完整调用覆盖。' : '') + (enableRiskPlanning ? '执行按plan阈值决定风险假设规划,小变更可跳过独立规划但仍主审并复核;计划不是证据,所有阶段共享原预算。' : '') + '执行会向上述模型提供方发送可审查文件两侧内容、预览列出的显式/自动上下文、所选项目规则全文及预览业务需求文本。模型可在该快照范围内多轮只读检索,不读取范围外文件;候选生成后会进行证据与反证复核,两阶段各最多3轮检索,共享每文件120秒和总模型调用100次预算;复核不能证明缺陷成立。plan.reviewRounds为独立审查工作量,非模型reasoning effort;后续轮不带首轮风险计划,所有轮共享原超时与预算。plan仅预检初始输入,ready不代表已审查;minimumCalls不含后续检索/复核/动态业务综合;多文件可先进行业务分组,分组失败保留原组并记录限制,动态综合数执行前未知,实际预算可能不足。请先向用户展示范围,获得确认后执行。' };
52
+ return { ...(options.businessRequirement!==undefined?{businessRequirement:{text:options.businessRequirement,hash:hash(options.businessRequirement),bytes:Buffer.byteLength(options.businessRequirement)}}:{}), previewId, plan, snapshotId: snapshot.id, repositoryRoot: snapshot.root, vcs: snapshot.vcs, ...(snapshot.history ? { history: snapshot.history } : {}), model: current.route, modelSendingEnabled: allowModelSending, files: snapshot.files.map(f => ({ path: f.path, eligibility: f.eligibility, reason: f.reason, ...(f.blockCode ? {blockCode:f.blockCode} : {}), ...(f.copySource ? {copySource:f.copySource,copyNotice:f.copyNotice} : {}) })), contextPaths: snapshot.context.map(c => c.path), ...(options.oldContextPaths !== undefined ? {oldContextPaths:options.oldContextPaths,oldContextSources:snapshot.context.filter(c=>c.oldOnly).map(c=>({path:c.path,side:'context-old',revision:c.oldRevision,blobOid:c.oldBlobOid,hash:c.oldHash}))} : {}), ...(snapshot.callerDiscovery ? {callerDiscovery:snapshot.callerDiscovery} : {}), ...(snapshot.autoContext ? { autoContext: snapshot.autoContext } : {}), rules: (snapshot.rules ?? []).map(({ path, hash }) => ({ path, hash })), notice: (options.oldContextPaths !== undefined ? 'oldContextPaths仅为baseline旧侧正文,无目标侧替代;选中变更复用原old身份,旧侧不能证明新侧行为。' : '') + (snapshot.callerDiscovery ? 'callerScopePaths已授权预览读取限定目录内tracked候选正文,包括不匹配文件;非匹配正文不发送模型,匹配捕获上下文会发送。仅字面引用导航,不是函数调用或完整调用覆盖。' : '') + (enableRiskPlanning ? '执行按plan阈值决定风险假设规划,小变更可跳过独立规划但仍主审并复核;计划不是证据,所有阶段共享原预算。' : '') + '执行会向上述模型提供方发送可审查文件两侧内容、预览列出的显式/自动上下文、所选项目规则全文及预览业务需求文本。模型可在该快照范围内多轮只读检索,不读取范围外文件;候选生成后会进行证据与反证复核,两阶段各最多3轮检索,共享每文件120秒和总模型调用100次预算;复核不能证明缺陷成立。plan.reviewRounds为独立审查工作量,非模型reasoning effort;后续轮不带首轮风险计划,所有轮共享原超时与预算。plan仅预检初始输入,ready不代表已审查;minimumCalls不含后续检索/复核/动态业务综合;多文件可先进行业务分组,分组失败保留原组并记录限制,动态综合数执行前未知,实际预算可能不足。请先向用户展示范围,获得确认后执行。' };
53
53
  } finally { busy = false; }
54
54
  },
55
55
  async execute(args, exec) {
@@ -0,0 +1,17 @@
1
+ // Stable offline diagnoses, not permission to resolve or mutate working nodes.
2
+ export function svnStateIssue(state) {
3
+ if (!state || typeof state['@_item'] !== 'string') return {code:'SVN_INVALID_STATE',reason:'SVN status entry is incomplete'};
4
+ const item=state['@_item'];
5
+ if (state['@_tree-conflicted']==='true') return {code:'SVN_TREE_CONFLICT',reason:'Tree conflict requires explicit user resolution; no coherent target inferred'};
6
+ if (item==='conflicted') return {code:'SVN_TEXT_CONFLICT',reason:'Text conflict is unresolved; conflict markers are not a resolved target'};
7
+ if (state['@_props']==='conflicted') return {code:'SVN_PROPERTY_CONFLICT',reason:'Property conflict is unresolved; property values are not a resolved target'};
8
+ if (item==='missing') return {code:'SVN_MISSING_NODE',reason:'Tracked node is missing without scheduled deletion; no deletion inferred'};
9
+ if (item==='obstructed') return {code:'SVN_OBSTRUCTED_NODE',reason:'Working node obstructs the tracked node; no replacement inferred'};
10
+ if (item==='incomplete') return {code:'SVN_INCOMPLETE_NODE',reason:'Working-copy node is incomplete; source coverage is unavailable'};
11
+ if (item==='replaced') return {code:state['@_copied']==='true'?'SVN_COPIED_REPLACEMENT':'SVN_REPLACEMENT',reason:'Replacement requires verified target old node; local BASE may be unavailable or describe the copy source'};
12
+ if (state['@_switched']==='true') return {code:'SVN_SWITCHED_NODE',reason:'Switched node requires separately frozen repository URL identity'};
13
+ if (state['@_file-external']==='true' || item==='external') return {code:'SVN_EXTERNAL_NODE',reason:'External node requires a separate explicitly selected repository scope'};
14
+ if (state['@_copied']==='true' && item!=='added') return {code:'SVN_COPIED_NODE',reason:'Copied non-addition requires verified target baseline'};
15
+ if (!['modified','added','deleted','normal','unversioned'].includes(item)) return {code:'SVN_UNSUPPORTED_STATE',reason:'Unsupported SVN node state: '+item};
16
+ return null;
17
+ }
package/src/svn.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isSecretPath } from './secret-path.mjs';
2
+ import { svnStateIssue } from './svn-state.mjs';
2
3
  import { realpath, lstat } from 'node:fs/promises';
3
4
  import path from 'node:path';
4
5
  import { isDeepStrictEqual } from 'node:util';
@@ -72,9 +73,8 @@ export async function captureSvn(cwd, options = {}) {
72
73
  }
73
74
  try {
74
75
  if (name !== '.') relativePath(name);
75
- if (!['modified', 'added', 'deleted', 'normal', 'unversioned'].includes(status) || state['@_props'] === 'conflicted' || state['@_tree-conflicted'] === 'true') throw new Error('Unsupported or conflicted SVN state: ' + status);
76
- if (state['@_switched'] === 'true' || state['@_file-external'] === 'true') throw new Error('Switched or external node requires explicit historical support');
77
- if (state['@_copied'] === 'true' && status !== 'added') throw new Error('Copied replacement requires verified target baseline');
76
+ const issue = svnStateIssue(state);
77
+ if (issue) { item.blockCode = issue.code; throw new Error(issue.reason); }
78
78
  const hasBase = !['added', 'unversioned'].includes(status);
79
79
  let kind;
80
80
  if (status === 'unversioned') kind = (await lstat(path.join(root, name))).isDirectory() ? 'dir' : 'file';