@feng0531/dsh-code-review 0.21.0 → 0.21.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.21.1
4
+
5
+ - 新增低层不可变Git commit/range对象快照,不读工作树;根提交空侧,merge要求明确区间,端点树差异、重命名按增删表达。
6
+ - 严格revision参数、安全blob模式、文件/树/输出/总大小界限及取消。全量251通过;注册/上下文历史接入待完成。
7
+
3
8
  ## 0.21.0
4
9
 
5
10
  - 注册入口开启唯一精确锚纠正,跨文件限定原组候选主ID,原候选/状态JSON保留及Markdown提示。
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 当前状态
6
6
 
7
- 当前代码版本为 0.21.0
7
+ 当前代码版本为 0.21.1
8
8
 
9
9
  工具对错误行锚可进行一次同文件同侧唯一完整原文纠正,须重叠原引用精确编辑;原候选/状态保留JSON,Markdown提示。纠正后仍校验窗口、跨文件编辑及生成读取证据,不增调用、不猜歧义或跨文件迁移,不证明缺陷或因果。
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feng0531/dsh-code-review",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "private": false,
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
@@ -0,0 +1,21 @@
1
+ import {realpath} from 'node:fs/promises';
2
+ import {checked} from './process.mjs';
3
+ import {hash,decode,relativePath} from './content.mjs';
4
+ const utf8=b=>new TextDecoder('utf-8',{fatal:true}).decode(b);
5
+ // Object-only capture: no checkout, worktree reads, external diff or fetch.
6
+ export async function captureGitHistory(cwd, options={}) {
7
+ const {commit,baseRevision,targetRevision,selectedPaths,signal,maxFiles=200,maxFileBytes=256*1024,maxSnapshotBytes=4*1024*1024}=options;
8
+ if(!Number.isSafeInteger(maxFiles)||maxFiles<1||maxFiles>200||!Number.isSafeInteger(maxFileBytes)||maxFileBytes<1||maxFileBytes>256*1024||!Number.isSafeInteger(maxSnapshotBytes)||maxSnapshotBytes<1||maxSnapshotBytes>4*1024*1024)throw new Error('Invalid historical capture limits');
9
+ if(selectedPaths!==undefined&&(!Array.isArray(selectedPaths)||selectedPaths.some(p=>relativePath(p)!==p)))throw new Error('Invalid selected paths');
10
+ const git=(root,args,limit=1024*1024)=>checked('git',['--no-optional-locks','-c','core.fsmonitor=false','-c','core.untrackedCache=false',...args],{...options,cwd:root,maxBytes:limit});
11
+ const root=await realpath(utf8(await git(cwd,['rev-parse','--show-toplevel'])).trim());
12
+ const resolve=async ref=>{if(typeof ref!=='string'||!ref||ref.length>256||ref.startsWith('-')||/[\s\0]/.test(ref))throw new Error('Invalid revision');const oid=utf8(await git(root,['rev-parse','--verify','--end-of-options',ref+'^{commit}'])).trim();if(!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(oid))throw new Error('Invalid commit object');return oid;};
13
+ let base,target;
14
+ if(commit!==undefined){if(baseRevision!==undefined||targetRevision!==undefined)throw new Error('Commit and range are mutually exclusive');target=await resolve(commit);const ancestry=utf8(await git(root,['rev-list','--parents','-n','1',target])).trim().split(' ');if(ancestry.length>2)throw new Error('Merge commit requires explicit revision range');base=ancestry[1]??null;}
15
+ else {base=await resolve(baseRevision);target=await resolve(targetRevision);}
16
+ const tree=async oid=>{const map=new Map();if(!oid)return map;const records=utf8(await git(root,['ls-tree','-rz','--full-tree',oid])).split('\0').filter(Boolean);if(records.length>10000)throw new Error('Historical tree entry limit exceeded');for(const r of records){const m=/^(\d{6}) (blob|commit) ([a-f0-9]+)\t([\s\S]+)$/.exec(r);if(!m)throw new Error('Invalid historical tree');relativePath(m[4]);map.set(m[4],{mode:m[1],type:m[2],oid:m[3]});}return map;};
17
+ const left=await tree(base),right=await tree(target),names=[...new Set([...left.keys(),...right.keys()])].filter(p=>JSON.stringify(left.get(p))!==JSON.stringify(right.get(p))).sort();
18
+ if(names.length>maxFiles)throw new Error('Historical change count limit exceeded');if(selectedPaths?.some(p=>!names.includes(p)))throw new Error('Selected path is not a historical change');
19
+ const files=[];for(const name of names){signal?.throwIfAborted();const l=left.get(name),r=right.get(name),item={id:hash(name),path:name,rawStatus:!l?'A':!r?'D':'M',eligibility:'reviewable',properties:[]};files.push(item);if(selectedPaths&&!selectedPaths.includes(name)){item.eligibility='excluded';item.reason='Not explicitly selected';continue;}try{for(const e of [l,r].filter(Boolean))if(e.type!=='blob'||!['100644','100755'].includes(e.mode))throw new Error('Historical link or submodule excluded');const read=async e=>decode(e?await git(root,['cat-file','blob',e.oid],maxFileBytes):Buffer.alloc(0));item.left=await read(l);item.right=await read(r);item.rightExists=!!r;if(l&&r&&item.left.hash===item.right.hash){item.eligibility='excluded';item.reason='Mode-only historical change not analyzed';}}catch(error){signal?.throwIfAborted();item.eligibility='blocked';item.reason=error.message;delete item.left;delete item.right;}if(Buffer.byteLength(JSON.stringify(files))>maxSnapshotBytes)throw new Error('Historical snapshot limit exceeded');}
20
+ const snapshot={schemaVersion:1,vcs:'git',root,baseline:base,history:{mode:commit!==undefined?'commit':'range',base,target,semantics:'exact-tree-endpoints; renames represented as add/delete'},files};const serialized=JSON.stringify(snapshot);if(Buffer.byteLength(serialized)>maxSnapshotBytes)throw new Error('Historical snapshot limit exceeded');return {...snapshot,id:hash(serialized)};
21
+ }