@feng0531/dsh-code-review 0.64.1 → 0.65.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 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.65.0 — SVN 历史文件/目录类型替换
6
+
7
+ 固定历史增删对分别记录 oldKind/newKind,读取各自节点正文和属性,支持已提交文件→目录和目录→文件。预览披露类型与说明,目录一侧后代独立覆盖且由深至浅传播未完成;目录空正文不作为空文件或旧上下文。未提交工作副本替换仍阻断,多记录与无效状态仍拒绝。
8
+
5
9
  ## 0.64.1 — SVN 已提交历史替换双侧采集修复
6
10
 
7
- 固定 revision summary 中同一路径的同类型 deleted/added 两记录合并为 replaced,分别从目标旧/新 revision 读取正文及属性,避免此前将合法历史替换当作重复路径拒绝。只接受一对同类型增删,重复新增/删除、多记录或类型变化继续拒绝;目录替换仍检查子项覆盖。未提交工作副本替换不因此开放。
11
+ 固定 revision summary 中同一路径的同类型 deleted/added 两记录合并为 replaced,分别从目标旧/新 revision 读取正文及属性,避免此前将合法历史替换当作重复路径拒绝。只接受一对同类型增删,重复新增/删除、多记录或类型变化继续拒绝;目录替换仍检查子项覆盖。未提交工作副本替换不因此开放。481 项串行回归、宿主、隔离安装及四矩阵 CI 通过;原产物/npm/Release 128,279 字节逐字节一致,双哈希、清单与冻结发布说明通过。
8
12
 
9
13
  ## 0.64.0 — SVN 历史旧侧独有上下文
10
14
 
@@ -32,7 +32,8 @@
32
32
 
33
33
  ## 当前验证结论
34
34
 
35
- - 最近完整回归:479 项通过。
35
+ - 最近完整回归:481 项通过。
36
+ - v0.64.1 已修复已提交 SVN 历史替换的同路径增删记录,真实目标旧/新正文和属性逐侧读取。481 项串行回归、宿主、隔离安装和四矩阵 CI 通过;原产物/npm/Release 128,279 字节逐字节一致,双哈希、清单与冻结发布说明通过。未提交工作副本替换和类型变化等仍未支持,全面目标继续。
36
37
  - v0.64.0 已交付 SVN 历史 oldContextPaths,仅旧侧正文及来源身份,不合成目标,选中旧变更复用原身份,排除项拒绝回流。479 项串行回归、宿主、隔离安装和四矩阵 CI 通过;原产物/npm/Release 127,879 字节逐字节一致,双哈希、清单与冻结发布说明通过。自动历史链、调用方及全面 SVN 矩阵仍未闭环。
37
38
  - v0.63.0 已交付 SVN 历史显式双侧上下文与目标规则,以及模型 JSON/read 请求的安全诊断。477 项串行回归、宿主、隔离安装和四矩阵 CI 通过;原产物/npm/Release 127,355 字节逐字节一致,双哈希、清单与冻结发布说明通过。用户 loader 的实际协议失败尚未核验,不宣称已修复该样本;历史自动链/调用方、旧侧独有上下文及其他 SVN 缺口继续。
38
39
  - v0.62.0 已交付 SVN 原生固定 revision/区间初始审查:显式远端门禁、固定双侧正文/属性/来源身份、目录后代覆盖和确认执行链。472 项串行回归、宿主、隔离安装与四矩阵 CI 通过;原产物/npm/Release 125,486 字节逐字节一致,双哈希、清单与冻结发布说明通过。历史上下文/规则/调用方、跨 URL 分支和工作副本替换等仍待实现。
package/RELEASE-NOTES.md CHANGED
@@ -1,14 +1,15 @@
1
- # v0.64.1 发布说明
1
+ # v0.65.0 发布说明
2
2
 
3
- ## SVN 已提交历史替换双侧采集修复
3
+ ## SVN 历史文件/目录类型替换
4
4
 
5
- 修复固定 revision 历史审查中,合法替换的同路径删除/新增记录被当作歧义拒绝的问题。
5
+ 支持已提交固定 revision 中文件与目录之间的替换,不使用未提交工作副本的 BASE 或复制来源推断旧侧。
6
6
 
7
- - 仅合并同类型节点的一对 deleted/added,为 replaced;其他重复和类型变化拒绝,不任意选择。
8
- - 从被替换目标的旧 revision 和新 revision 分别读取真实正文与属性,复制替换不使用复制来源 BASE 冒充旧侧。
9
- - 来源逐侧固定 URL/UUID/peg/operative revision,仍保持选择、预算与目录后代覆盖保护。
10
- - 独立 SVN 测试覆盖已提交普通与复制替换以及旧属性删除、新属性添加。
7
+ - 增删对分别绑定 oldKind/newKind,各侧读取对应固定节点正文与属性。
8
+ - 预览明确披露旧/新节点类型,目录空正文不冒充空文件。
9
+ - 目录一侧的后代仍逐项覆盖,未全选或子项阻断由深至浅传播至父节点。
10
+ - 旧侧目录不能作为旧文件上下文,避免给模型伪造定义正文。
11
+ - 保留对重复新增/删除、多条记录、不安全来源及超限的拒绝。
11
12
 
12
- 本版仅针对已提交的固定历史版本;未提交工作副本替换、类型变化及其他全面 SVN 缺口仍按 [支持矩阵](SVN-SUPPORT-MATRIX.md) 推进,不以候选 diff 重建冒充支持。
13
+ 本版只针对已提交的历史类型替换;未提交工作副本替换、自动历史链、调用方和其他全面 SVN 缺口仍在 [支持矩阵](SVN-SUPPORT-MATRIX.md) 中,目标尚未完成。
13
14
 
14
15
  用法见 [README](README.md)。
@@ -19,7 +19,7 @@
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 字节逐字节一致,双哈希/清单/冻结发布说明通过。v0.61.0 已发布同仓库目录复制后代展开与继承来源身份,目标旧侧为空且预览披露。465 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 120,159 字节、双哈希/清单/冻结发布说明通过。复制替换仍未支持 | 第二批:明确旧侧来源和后代覆盖 |
21
21
  | 普通新增/删除目录及后代 | v0.60.0 已发布:补齐默认 status 隐藏的删除后代,离线递归枚举与末尾清单复核;普通结构目录只在后代全选且可审查时开放,凭据/阻断子项导致目录覆盖不足。461 项串行回归、宿主、隔离安装与四矩阵 CI 通过;原产物/npm/Release 118,284 字节逐字节一致,双哈希/清单/冻结发布说明通过 | 第二批普通目录子批 |
22
- | 替换、删除再新增、文件/目录类型变化 | 普通与复制替换分别用 SVN_REPLACEMENT/SVN_COPIED_REPLACEMENT 诊断;离线探测确认前者 BASE 无 pristine,后者可能返回复制来源,禁止伪造旧侧。已新增未接入采集的 candidate-only 完整 diff 重建组件及真实离线测试:匹配完整工作侧,拒绝片段、错侧、截断与超限。diff 仍不能独立证明旧节点/编码/旧属性,因此未提交工作副本真实双侧替换仍未支持,不能把辅助组件算作支持完成。本地修复已提交固定历史版本的同路径 deleted/added 合并,真实目标旧/新正文与属性已测试;待 v0.64.1 发布核验,类型变化仍拒绝 | 第二批:区分 BASE 旧节点与目标节点 |
22
+ | 替换、删除再新增、文件/目录类型变化 | 普通与复制替换分别用 SVN_REPLACEMENT/SVN_COPIED_REPLACEMENT 诊断;离线探测确认前者 BASE 无 pristine,后者可能返回复制来源,禁止伪造旧侧。已新增未接入采集的 candidate-only 完整 diff 重建组件及真实离线测试:匹配完整工作侧,拒绝片段、错侧、截断与超限。diff 仍不能独立证明旧节点/编码/旧属性,因此未提交工作副本真实双侧替换仍未支持,不能把辅助组件算作支持完成。v0.64.1 已发布已提交固定历史版本的同路径 deleted/added 合并,真实目标旧/新正文与属性通过测试;481 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 128,279 字节、双哈希/清单/发布说明通过。本地新增已提交固定历史文件/目录类型替换:分侧 kind/正文/属性、预览披露和目录后代覆盖,待 v0.65.0 发布。未提交替换仍拒绝 | 第二批:区分 BASE 旧节点与目标节点 |
23
23
  | 缺失、文本/属性/树冲突、阻塞 | v0.60.1 已发布稳定状态诊断:SVN_MISSING_NODE/OBSTRUCTED_NODE/INCOMPLETE_NODE/TEXT_CONFLICT/PROPERTY_CONFLICT/TREE_CONFLICT 等,预览与报告透传 blockCode。463 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 119,462 字节、双哈希/清单/发布说明通过。诊断不算真实双侧支持,仍需后续实现 | 第三批:明确状态诊断;不能合成不存在的工作侧或声称审核通过 |
24
24
  | svn:keywords、svn:eol-style | keywords 当前阻断;换行保留原文但无规范化映射 | 第三批:双侧可验证映射,保留原字节和定位 |
25
25
  | svn:special/链接 | 当前阻断,不跟随链接取正文 | 第三批:如支持只审链接目标数据;不沿链接读取文件 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.64.1",
3
+ "version": "0.65.0",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
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.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,...(c.oldSvnSource?{svnSource:c.oldSvnSource}:{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.rawStatus==='replaced'&&snapshot.history?.vcs==='svn'?{oldKind:f.oldKind,newKind:f.newKind,nodeNotice:f.nodeNotice}:{}), ...(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,...(c.oldSvnSource?{svnSource:c.oldSvnSource}:{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) {
@@ -33,8 +33,9 @@ export function svnHistoryDestination(value, allowNetwork) {
33
33
  export function mergeSvnHistoryRecord(previous, item) {
34
34
  if(!['added','deleted','modified','none'].includes(item?.['@_item'])||!['file','dir'].includes(item?.['@_kind']))throw new Error('Ambiguous or unsupported SVN history summary');
35
35
  if(!previous)return item;
36
- if(previous['@_kind']!==item['@_kind']||!['added','deleted'].includes(previous['@_item'])||!['added','deleted'].includes(item['@_item'])||previous['@_item']===item['@_item'])throw new Error('Ambiguous or unsupported SVN history summary');
37
- return {...item,'@_item':'replaced'};
36
+ if(!['added','deleted'].includes(previous['@_item'])||!['added','deleted'].includes(item['@_item'])||previous['@_item']===item['@_item'])throw new Error('Ambiguous or unsupported SVN history summary');
37
+ const old=previous['@_item']==='deleted'?previous:item,target=previous['@_item']==='added'?previous:item;
38
+ return {...target,'@_item':'replaced',oldKind:old['@_kind'],newKind:target['@_kind']};
38
39
  }
39
40
 
40
41
  // Fixed URL objects only. No checkout, update, merge, working body or lazy HEAD read.
@@ -101,23 +102,24 @@ export async function captureSvnHistory(cwd,options={}) {
101
102
  const node=await identity(name,rev),props=await properties(name,rev);
102
103
  if(Object.hasOwn(props,'svn:special'))throw new Error('Historical SVN special node excluded');
103
104
  const content=node['@_kind']==='dir'?decode(Buffer.alloc(0)):decode(await run(['cat','--ignore-keywords','-r',rev,'--',at(name,rev)],true));
104
- return {content,props,kind:node['@_kind'],source:{repositoryUuid:local.repository.uuid,url:at(name,rev).slice(0,-rev.length-1),pegRevision:rev,operativeRevision:rev,hash:content.hash,textHash:hash(content.text)}};
105
+ return {content,props,kind:node['@_kind'],source:{repositoryUuid:local.repository.uuid,url:at(name,rev).slice(0,-rev.length-1),pegRevision:rev,operativeRevision:rev,kind:node['@_kind'],hash:content.hash,textHash:hash(content.text)}};
105
106
  };
106
107
  const files=[];
107
108
  for(const [name,record]of [...records].sort(([a],[b])=>a<b?-1:a>b?1:0)){
108
109
  signal.throwIfAborted();
109
- const status=record['@_item'],item={id:hash(name),path:name,kind:record['@_kind'],rawStatus:status,eligibility:'reviewable',properties:[],rightExists:status!=='deleted'};
110
+ const status=record['@_item'],item={id:hash(name),path:name,kind:record['@_kind'],oldKind:status==='added'?null:record.oldKind??record['@_kind'],newKind:status==='deleted'?null:record.newKind??record['@_kind'],rawStatus:status,eligibility:'reviewable',properties:[],rightExists:status!=='deleted'};
110
111
  files.push(item);
111
112
  if(isSecretPath(name)){item.eligibility='excluded';item.reason='Credential path excluded';continue;}
112
113
  if(selectedPaths&&!selectedPaths.includes(name)){item.eligibility='excluded';item.reason='Not explicitly selected';continue;}
113
114
  try{
114
- const readSide=async(rev,exists)=>{
115
+ const readSide=async(rev,exists,kind)=>{
115
116
  if(!exists)return {content:decode(Buffer.alloc(0)),props:{}};
116
117
  const result=await readSource(name,rev);
117
- if(result.kind!==item.kind)throw new Error('Historical node kind differs');
118
+ if(result.kind!==kind)throw new Error('Historical node kind differs');
118
119
  return result;
119
120
  };
120
- const left=await readSide(range.base,status!=='added'),right=await readSide(range.target,status!=='deleted');
121
+ const left=await readSide(range.base,status!=='added',item.oldKind),right=await readSide(range.target,status!=='deleted',item.newKind);
122
+ if(status==='replaced')item.nodeNotice='Fixed historical replacement; each side has its own node kind. Directory content is not file text; descendant changes require separate coverage.';
121
123
  item.left=left.content;item.right=right.content;
122
124
  item.svnSources={...(left.source?{old:left.source}:{}),...(right.source?{new:right.source}:{})};
123
125
  for(const key of [...new Set([...Object.keys(left.props),...Object.keys(right.props)])].sort())if(left.props[key]!==right.props[key])item.properties.push({name:key,old:left.props[key]??null,new:right.props[key]??null});
@@ -126,7 +128,7 @@ export async function captureSvnHistory(cwd,options={}) {
126
128
  }catch(error){signal.throwIfAborted();if(error.code==='SVN_HISTORY_BUDGET')throw error;item.eligibility='blocked';item.reason=error.message;delete item.left;delete item.right;}
127
129
  if(Buffer.byteLength(JSON.stringify(files))>maxSnapshotBytes)throw new Error('SVN historical snapshot limit exceeded');
128
130
  }
129
- for(const item of files.filter(f=>f.kind==='dir'&&['added','deleted','replaced'].includes(f.rawStatus)&&f.eligibility==='reviewable')){
131
+ for(const item of files.filter(f=>(f.oldKind==='dir'||f.newKind==='dir')&&['added','deleted','replaced'].includes(f.rawStatus)&&f.eligibility==='reviewable').sort((a,b)=>b.path.length-a.path.length)){
130
132
  if(files.some(child=>child.path.startsWith(item.path==='.'?'':item.path+'/')&&child!==item&&child.eligibility!=='reviewable')){item.eligibility='blocked';item.reason='Historical directory descendant coverage incomplete';delete item.left;delete item.right;}
131
133
  }
132
134
  const context=[];
@@ -141,7 +143,7 @@ export async function captureSvnHistory(cwd,options={}) {
141
143
  for(const name of [...contextPaths].sort()){
142
144
  const target=await targetContent(name),record=records.get(name),changed=files.find(file=>file.path===name);
143
145
  const item={path:name,...target.content,revision:range.target,svnSource:target.source};
144
- if(changed){if(record['@_item']!=='added'){item.oldText=changed.left.text;item.oldRevision=range.base;item.oldSvnSource=changed.svnSources.old;}}
146
+ if(changed){if(changed.oldKind==='file'){item.oldText=changed.left.text;item.oldRevision=range.base;item.oldSvnSource=changed.svnSources.old;}}
145
147
  else{
146
148
  // No change record means both fixed sides are present and equal. Read and verify
147
149
  // rather than inferring baseline content from target or from the working tree.
@@ -153,7 +155,7 @@ export async function captureSvnHistory(cwd,options={}) {
153
155
  for(const name of [...oldContextPaths].sort()){
154
156
  const changed=files.find(file=>file.path===name);
155
157
  if(changed){
156
- if(changed.eligibility!=='reviewable'||changed.kind!=='file'||!changed.svnSources?.old)throw new Error('Old SVN context conflicts with excluded blocked or absent baseline change');
158
+ if(changed.eligibility!=='reviewable'||changed.oldKind!=='file'||!changed.svnSources?.old)throw new Error('Old SVN context conflicts with excluded blocked or absent baseline change');
157
159
  // The original changed-file old identity already authorizes this side.
158
160
  continue;
159
161
  }