@feng0531/dsh-code-review 0.33.3 → 0.33.5

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,14 +1,24 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.33.5
4
+
5
+ - 少于4个可审查文件免模型分组:精确增删行数<200本地合组,否则逐文件;未知精度不合组。仅调度导航,不证明语义依赖,不免主审或风险/证据校验。
6
+ - 预览同算法计入本地综合输入及调用预算,综合超限仍明确blocked;331离线测试通过,候选待发布门禁。
7
+
8
+ ## 0.33.4
9
+
10
+ - 文件失败保留明确MODEL_INVALID_JSON/MODEL_TIMEOUT错误码;取消为CANCELLED,其余未知错误为REVIEW_EXECUTION_FAILED,不猜测根因、不重试。
11
+ - 报告selection保留total/reviewable/excluded/blocked分母;零选择、全排除或全阻塞返回partial及未执行限制,不冒充完整通过。327冻结离线测试及标签四矩阵通过,唯一发布37910021333原工件89493字节与npm/Release逐字节及双hash一致。
12
+
3
13
  ## 0.33.3
4
14
 
5
15
  - 超预算主审切为精确窗口时,上下文正文留在原批准快照按需read;初始输入仅带版本侧catalog,避免上下文重复导致所有窗口持续超限。不扩大读取范围,不裁剪证据;报告明确未声明全部上下文核查。
6
- - 当前/历史旧侧隔离、无scope仍阻断、预览执行预算一致、超时等适配器清理且窗口pending验收通过;320离线及真实宿主离线smoke通过。候选待远端门禁,不宣称用户碰撞审查已恢复或超时根因已定位。
16
+ - 当前/历史旧侧隔离、无scope仍阻断、预览执行预算一致、超时等适配器清理且窗口pending验收通过;320离线及真实宿主离线smoke、固定标签四矩阵及唯一发布通过;npm/Release原工件88999字节双hash一致。不宣称用户碰撞审查已恢复或超时根因已定位。
7
17
 
8
18
  ## 0.33.2
9
19
 
10
20
  - 接受唯一完整JSON的单一json/无语言Markdown围栏,不提取任意说明中的JSON、不修正文法/字段、不重试;后续原schema及证据校验不变。
11
- - 不完整JSON返回MODEL_INVALID_JSON及字节/封装类型诊断,不泄露响应正文。317离线测试通过。实际碰撞审查原始响应未知,不能宣称复现其三处根因;超时与大文件范围缺口仍待定位。
21
+ - 不完整JSON返回MODEL_INVALID_JSON及字节/封装类型诊断,不泄露响应正文。317离线测试及固定四矩阵/唯一发布通过,npm/Release原工件88609字节双hash匹配。实际碰撞审查原始响应未知,不能宣称复现其三处根因;超时与大文件范围缺口仍待定位。
12
22
 
13
23
  ## 0.33.1
14
24
 
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 当前状态
6
6
 
7
- 当前正式版本为 0.33.1(固定标签四矩阵及npm/Release原工件字节双hash门禁通过);0.33.2为JSON封装兼容候选,尚未正式验收。
7
+ 当前正式版本为 0.33.4(固定标签四矩阵及npm/Release原工件字节双hash门禁通过)。JSON围栏兼容与有界窗口上下文已交付;实际项目审查效果仍需原报告和新预览执行验证。
8
8
 
9
9
  Git历史预览新增oldContextPaths:最多20个显式仓库相对文件,与contextPaths/自动上下文共享20槽、256KiB每blob及4MiB快照。仅从baseline普通Git blob捕获,不checkout/fetch/读取工作树;根提交无baseline拒绝,working/SVN明确拒绝。旧侧条目携带oldRevision/oldBlobOid/oldHash,仅进入context-old;oldOnly表示仅批准旧侧,不证明target路径不存在。所选变更复用原changed-old,不重复上下文;excluded/blocked变更、链接/submodule、凭据路径不能重新进入。预览显示身份元数据、不显示正文,确认执行才按宿主正常权限发送。主审/综合/复核按侧提示,计入原输入预算,旧证据不能冒充新侧回归证明。
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.33.3",
3
+ "version": "0.33.5",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
@@ -1,3 +1,14 @@
1
+ import {changeMap} from './change-map.mjs';
2
+ // Local partitions carry no semantic dependency claim. Unknown precision never bundles.
3
+ export function localBusinessGroups(files) {
4
+ const selected=files.filter(f=>f.eligibility==='reviewable');
5
+ if(selected.length<2||selected.length>=4||selected.some(f=>typeof f.left?.text!=='string'||typeof f.right?.text!=='string'))return null;
6
+ const changes=selected.map(f=>changeMap(f.left.text,f.right.text));
7
+ const exact=changes.every(c=>c.status==='unchanged'||c.precision==='exact');
8
+ const churn=exact?changes.reduce((sum,c)=>sum+c.edits.reduce((n,e)=>n+e.old.count+e.new.count,0),0):null;
9
+ const bundle=churn!==null&&churn<200;
10
+ return {status:'skipped',strategy:bundle?'local-bundle':'local-per-file',churn,groups:(bundle?[selected]:selected.map(f=>[f])).map((members,i)=>({id:'local'+(i+1),fileIds:members.map(f=>f.id),reason:'small change set; no semantic partition',confidence:'local-navigation'})),links:[]};
11
+ }
1
12
  export const BUSINESS_GROUP_INSTRUCTIONS = '按共同业务契约对给定变更文件元数据分组,仅为审查调度假设,路径不证明依赖。返回{groups:[{fileIds:[已有id],reason:非空说明}]}。每组1至4文件,每个文件恰好一次,无关联可单文件,不新增路径或读文件。';
2
13
  export function prepareBusinessGroups(files) {
3
14
  const selected=files.filter(f=>f.eligibility==='reviewable');
@@ -1,3 +1,4 @@
1
+ import {localBusinessGroups} from './business-groups.mjs';
1
2
  import { prepareInteractionInput } from './interaction-input.mjs';
2
3
  import { initialInputBudget } from './input-budget.mjs';
3
4
  import { preparePrimaryInput } from './primary-input.mjs';
@@ -18,7 +19,10 @@ export function buildCoveragePlan(input, options) {
18
19
  const prepared = preparePrimaryInput(input, file, options);
19
20
  return { ...item, ...prepared.metadata, status: prepared.budget.fits ? 'ready' : 'input-blocked', minimumCalls: prepared.budget.fits ? ((prepared.batches?.length ?? 1) * (options.enableRiskPlanning === true ? 2 : 1) + (prepared.synthesis?.budget.fits ? 1 : 0)) : 0 };
20
21
  });
21
- const interactions = options.scope && options.grouping ? options.grouping.groups.filter(g => g.fileIds.length > 1).map(group => { const prepared = prepareInteractionInput(input, group, payload => initialInputBudget({ instructions: options.instructions, input: payload }, options.scope, options.maxInputBytes)); return { groupId: group.id, fileIds: prepared.fileIds, status: prepared.status, reason: prepared.reason, minimumCalls: prepared.status === 'ready' ? (options.enableRiskPlanning === true ? 2 : 1) : 0 }; }) : [];
22
- const businessGrouping = options.enableBusinessGrouping === true && input.files.filter(f => f.eligibility === 'reviewable').length > 1 ? { minimumCalls: 1, dynamicInteractionCalls: 'unknown-until-execution', maximumAdditionalInitialCalls: Math.floor(input.files.filter(f => f.eligibility === 'reviewable').length / 2) * (options.enableRiskPlanning === true ? 2 : 1), notice: '模型分组假设仅调度,额外综合动态且不保证预算或覆盖。' } : null;
22
+ const local = options.enableBusinessGrouping === true ? localBusinessGroups(input.files) : null;
23
+ const groups = [...(options.grouping?.groups ?? [])];
24
+ for(const group of local?.groups ?? [])if(!groups.some(g=>[...g.fileIds].sort().join(',')===[...group.fileIds].sort().join(',')))groups.push(group);
25
+ const interactions = options.scope && groups.length ? groups.filter(g => g.fileIds.length > 1).map(group => { const prepared = prepareInteractionInput(input, group, payload => initialInputBudget({ instructions: options.instructions, input: payload }, options.scope, options.maxInputBytes)); return { groupId: group.id, fileIds: prepared.fileIds, status: prepared.status, reason: prepared.reason, minimumCalls: prepared.status === 'ready' ? (options.enableRiskPlanning === true ? 2 : 1) : 0 }; }) : [];
26
+ const businessGrouping = options.enableBusinessGrouping === true && input.files.filter(f => f.eligibility === 'reviewable').length > 1 ? local ? {minimumCalls:0,strategy:local.strategy,churn:local.churn,notice:'本地调度无语义依赖证明。'} : { minimumCalls: 1, dynamicInteractionCalls: 'unknown-until-execution', maximumAdditionalInitialCalls: Math.floor(input.files.filter(f => f.eligibility === 'reviewable').length / 2) * (options.enableRiskPlanning === true ? 2 : 1), notice: '模型分组假设仅调度,额外综合动态且不保证预算或覆盖。' } : null;
23
27
  return { ...(businessGrouping ? { businessGrouping } : {}), interactions, snapshotId: input.id, maxInputBytes: options.maxInputBytes, items, minimumCalls: (businessGrouping?.minimumCalls ?? 0) + items.reduce((sum,item) => sum + item.minimumCalls, 0) + interactions.reduce((sum,item) => sum + item.minimumCalls, 0), initialRequestsOnly: true };
24
28
  }
package/src/review.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { correctAnchor } from './anchor-correction.mjs';
2
2
  import { validateBusinessRequirement } from './business-requirement.mjs';
3
3
  import { riskReadCoverage } from './risk-read-coverage.mjs';
4
- import { BUSINESS_GROUP_INSTRUCTIONS, prepareBusinessGroups, validateBusinessGroups } from './business-groups.mjs';
4
+ import { BUSINESS_GROUP_INSTRUCTIONS, localBusinessGroups, prepareBusinessGroups, validateBusinessGroups } from './business-groups.mjs';
5
5
  import { RISK_PLAN_INSTRUCTIONS, validateRiskPlan } from './risk-plan.mjs';
6
6
  import { prepareInteractionInput } from './interaction-input.mjs';
7
7
  import { validateInteractionFindings } from './interaction-findings.mjs';
@@ -61,7 +61,7 @@ async function invoke(model, request, timeoutMs, parentSignal) {
61
61
  const abort = () => controller.abort(parentSignal.reason);
62
62
  parentSignal?.addEventListener('abort', abort, { once: true });
63
63
  if (parentSignal?.aborted) abort();
64
- const timer = setTimeout(() => controller.abort(new Error('Model timeout')), timeoutMs);
64
+ const timer = setTimeout(() => controller.abort(Object.assign(new Error('Model timeout'), { code: 'MODEL_TIMEOUT' })), timeoutMs);
65
65
  try {
66
66
  controller.signal.throwIfAborted();
67
67
  // Executors must honor cancellation; never release ownership before cleanup settles.
@@ -141,12 +141,16 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
141
141
  if (options.enableBusinessGrouping === true && !report.businessGrouping && prepareBusinessGroups(input.files)) {
142
142
  report.businessGrouping = { status: 'pending' };
143
143
  try {
144
+ const local = localBusinessGroups(input.files);
145
+ let grouped = local;
146
+ if (!local) {
144
147
  const groupInput = prepareBusinessGroups(input.files);
145
148
  if (Buffer.byteLength(groupInput) + Buffer.byteLength(BUSINESS_GROUP_INSTRUCTIONS) > maxInputBytes) throw new Error('Business grouping input budget exceeded');
146
149
  beforeCall();
147
150
  const response = await model({ ...enclosingRequest, instructions: BUSINESS_GROUP_INSTRUCTIONS, input: groupInput });
148
151
  enclosingRequest.signal.throwIfAborted();
149
- const grouped = validateBusinessGroups(response, input.files);
152
+ grouped = validateBusinessGroups(response, input.files);
153
+ }
150
154
  report.businessGrouping = { status: 'completed', ...grouped };
151
155
  for (const group of grouped.groups.filter(g => g.fileIds.length > 1)) {
152
156
  if (interactionPlans.some(p => [...p.group.fileIds].sort().join(',') === [...group.fileIds].sort().join(','))) continue;
@@ -247,10 +251,14 @@ export async function reviewSnapshot(snapshot, model, options = {}) {
247
251
  } catch (error) {
248
252
  state.status = signal?.aborted ? 'cancelled' : 'failed';
249
253
  state.reason = error?.message ?? String(error);
254
+ state.failureCode = signal?.aborted ? 'CANCELLED' : ['MODEL_INVALID_JSON','MODEL_TIMEOUT'].includes(error?.code) ? error.code : 'REVIEW_EXECUTION_FAILED';
250
255
  }
251
256
  }
252
257
  for (const interaction of report.interactions) if (interaction.status !== 'completed') report.limitations.push({ text: '跨文件综合未完成:' + interaction.groupId + ' (' + interaction.status + ')' });
253
258
  if (report.grouping?.links.some(l => l.split)) report.limitations.push({ text: '跨组关系未综合覆盖;关联分组不代表全部跨文件交互覆盖' });
259
+ const reviewableCount = input.files.filter(f => f.eligibility === 'reviewable').length;
260
+ report.selection = { total: input.files.length, reviewable: reviewableCount, excluded: input.files.filter(f => f.eligibility === 'excluded').length, blocked: input.files.filter(f => f.eligibility === 'blocked').length };
261
+ if (!reviewableCount) report.limitations.push({ text: input.files.length ? '没有可审查文件;排除或阻塞不表示代码审核通过。' : '未选择变更文件,没有执行代码审核。' });
254
262
  const unfinished = report.files.some(f => !['completed', 'excluded'].includes(f.status));
255
263
  report.status = signal?.aborted ? 'cancelled' : unfinished || report.limitations.length ? 'partial' : 'completed';
256
264
  if (report.files.some(f => f.status === 'failed') && !report.files.some(f => f.status === 'completed')) report.status = signal?.aborted ? 'cancelled' : 'failed';