From f8b4e9d44ddfda0315716bbda9d6ad1223279526 Mon Sep 17 00:00:00 2001 From: hangshuo652 Date: Thu, 27 Aug 2026 09:23:02 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=84=E5=AE=A1=E7=9C=9F=E5=AE=9E=E6=80=A7P1?= =?UTF-8?q?=E8=90=BD=E5=9C=B0=EF=BC=9A=E5=85=A8=E9=87=8F=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=8F=AF=E8=A7=81=E6=80=A7+Mock=E4=BF=A1=E5=8F=B7+AI=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E5=AE=A1=E8=AE=A1+=E5=AE=9A=E5=90=91=E8=BF=BD?= =?UTF-8?q?=E8=B8=AA+=E5=8A=9F=E8=83=BD=E5=8F=91=E7=8E=B0=E8=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - code-inventory.ts:全量符号索引,注入实现类维度,AI 可见100%文件清单 - code-signals.ts:代码信号检测器(TODO密度/空壳率/硬编码返回/死导入),修复TODO检测顺序bug - ai-log-audit.ts:AI日志确定性审计(占位率/git一致率/覆盖广度),占位>50%→≤30%、git一致<30%→≤20%封顶 - standard-utils.ts:isQualitativeDesignDim 定性设计维度豁免C档 - review.service.ts:注入三个模块+定向追踪模板+日志封顶+分支提醒+Map-Reduce功能发现轮 - 预算 15000→40000;新增 review-authenticity.test.ts(14用例) - 实测:净码特攻b3 81分(118功能全量识别),六边形50~62分(Mock被识别) --- AGENTS.md | 5 +- docs/plans/2026-08-26-评审真实性改进方案.md | 247 ++++++++++++++++++ .../src/__tests__/review-authenticity.test.ts | 126 +++++++++ server/src/services/ai-log-audit.ts | 108 ++++++++ server/src/services/code-inventory.ts | 109 ++++++++ server/src/services/code-signals.ts | 150 +++++++++++ server/src/services/review-constants.ts | 2 +- server/src/services/review.service.ts | 214 ++++++++++++++- server/src/services/standard-utils.ts | 9 + 9 files changed, 963 insertions(+), 7 deletions(-) create mode 100644 docs/plans/2026-08-26-评审真实性改进方案.md create mode 100644 server/src/__tests__/review-authenticity.test.ts create mode 100644 server/src/services/ai-log-audit.ts create mode 100644 server/src/services/code-inventory.ts create mode 100644 server/src/services/code-signals.ts diff --git a/AGENTS.md b/AGENTS.md index acc4025..54ac1a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,6 +57,9 @@ Phase 3: 确定性校准(computeCalibration) + 硬规则引擎 | `standards.ts` | `parseDimensions` 解析标准 MD 为维度列表 | | `l2-topics.ts` | L2考核选题元数据(11命题题+自选题,`config/l2-topics.json`):难度赋分 cap、功能完整性拆分、验收要点注入 | | `l2-participants.ts` | L2受验者注册表(`config/l2-participants.json`):员工编号↔Gitea账号↔仓库映射,自动生成拉取 URL;拉取认证走全局评审账号只读协作者权限 | +| `code-inventory.ts` | 全量符号索引(评审真实性改进 2026-08-26):对所有文本文件提取 path/lines/symbols,注入实现类维度让 AI 看见 100% 文件清单 | +| `code-signals.ts` | 代码信号检测器:TODO 密度/空壳率/硬编码返回/死导入——确定性线索注入实现类维度 prompt,定位为线索而非判据 | +| `ai-log-audit.ts` | AI 日志确定性审计:占位率 / git 一致率 / 覆盖广度;占位>50%→维度≤30%、git一致<30%→≤20% 封顶 | | `entries.ts` | 条目 CRUD + 触发评审 + PDF 导出 | | `projects.ts` | 项目 CRUD + 汇总排名 | | `db.ts` | SQLite 初始化 + migrations(ALTER TABLE try/catch) | @@ -149,7 +152,7 @@ cloneRepo → discoverFiles → countCodeStats → tryBuild → tryBrowse/trySta - **排名用多次聚合**:`aggregateScores`/`aggregateEntryScores`(standard-utils.ts)——最近 N 次(默认3)`review_snapshots.score` 中位数;N=2 平均、N=1 单次。聚合前提=各快照 `standard_snapshot` 一致。`<3 次标"初评(未达聚合样本)"`,排名区分正式/初评 - **快照分数列**:`review_snapshots.score` 存含迟交扣分的 final_score(写快照时一并存,不解析 ai_report——它只有 totalScore)。历史快照 backfill 从 totalScore best-effort -- **可验证能力三档**(`classifyVerifiability`,效果/提效类维度,校准之前判档):A=基准证据(`entries.benchmark_json` status=done);B=测试通过或覆盖率非 null;C=数据缺位→封顶 maxScore*0.3 + note"数据缺位(未证明),非无效"。**构建成功≠效果可验证**。C 档 note 渲染到前端维度表 + PDF。**纯增益维度严格制(2026-08-26)**:`提效幅度/效果对比/效率提升` 类维度(isPureGainDim)测试通过**不构成**提效证据——无基线对比数据一律 C 档封顶(实测案例:六边形战队提效幅度无量化数据 raw=6,修复后同类场景 C≤3) +- **可验证能力三档**(`classifyVerifiability`,效果/提效类维度,校准之前判档):A=基准证据(`entries.benchmark_json` status=done);B=测试通过或覆盖率非 null;C=数据缺位→封顶 maxScore*0.3 + note"数据缺位(未证明),非无效"。**构建成功≠效果可验证**。C 档 note 渲染到前端维度表 + PDF。**纯增益维度严格制(2026-08-26)**:`提效幅度/效果对比/效率提升` 类维度(isPureGainDim)测试通过**不构成**提效证据——无基线对比数据一律 C 档封顶(实测案例:六边形战队提效幅度无量化数据 raw=6,修复后同类场景 C≤3)。**定性设计维度豁免(2026-08-26)**:`提效设计合理性/XX清晰度`(isQualitativeDesignDim)考察设计思路而非量化结果,不参与 C 档封顶——避免测试环境因素跨队伍不公平(净码特攻案例) - **基准证据按 entry 落库**(`entries.benchmark_json`,非 env 变量——MAX_CONCURRENT=3 并发会串数据) - **确定性 L1**(`detectStructuralContradictions`):仅证据性矛盾触发(有测试/基准证据但效果≈0 → under;效果高分+实现全低 → over)。**效果维度 under 一律丢弃**(computeCalibration 内过滤)——效果维度只降不升,诚实由三档封顶负责。禁止"实现高分+无数据"当 under - **overall 中性边界**(`neutralizeTestEvidence`):测试 summary 含"中性"→ 标 `[中性证据]`;不含("未检测到测试框架配置"=真缺测试)→ 真实弱点。synthesizeOverall prompt 禁止把 `[中性证据]` 列为不足 diff --git a/docs/plans/2026-08-26-评审真实性改进方案.md b/docs/plans/2026-08-26-评审真实性改进方案.md new file mode 100644 index 0000000..7e3a821 --- /dev/null +++ b/docs/plans/2026-08-26-评审真实性改进方案.md @@ -0,0 +1,247 @@ +# 评审真实性改进方案(2026-08-26) + +> 关联:`docs/design/05-评审流程修正方案.md` §2.11(评审可信度)、`docs/plans/2026-08-19-评审可信度改进.md` +> 驱动案例:六边形战队(Mock 内核得 72 分)、净码特攻(205 行真实日志 vs "待补充"日志仅差 3 分、测试环境导致设计维度被误封顶) + +--- + +## 0. 结论摘要 + +实测暴露当前评审体系的**两层缺陷**: + +1. **真实性执行层缺失**——规范承诺的「抽检回查」「声称 vs 实测」没有系统化抓手。AI 只能看到仓库不到 2% 的内容(125 文件被截断到 15K 字符),Mock 内核、"待补充"日志都能漏网。 +2. **判档规则粗糙**——三档机制的 `EFFECT_EVIDENCE_KEYS` 过宽,把定性设计维度卷进量化封顶,造成跨队伍不公平。 + +本方案以**"让 AI 真实读到全部代码、带着确定性证据定向核查"**为核心,分三期落地。P1 全部为系统侧改动,无外部依赖。 + +--- + +## 1. 问题定义(实测案例驱动) + +| # | 实测现象 | 根因 | 层 | +|---|---------|------|-----| +| 1 | 六边形战队核心功能为 Mock,总分仍 72 | 无 Mock 确定性信号;实现类维度凭印象打分;无地板线 | 真实性 | +| 2 | AI 使用日志"范式步骤"全部"待补充",仍得 5~7 分;净码特攻 205 行真实日志得 9~10——差距仅 3 分 | 无日志回查/占位率检测,"有壳"与"有货"无法区分 | 真实性 | +| 3 | AI 只读到仓库 <2% 内容(60 文件上限 + 15K 字符截断),且每轮截断样本不同导致分数波动 | 全量可见性缺失 | 真实性 | +| 4 | 净码特攻"提效设计合理性"因测试环境跑不通被 C 档封顶到 3,六边形同类维度却拿 6~8 | `EFFECT_EVIDENCE_KEYS` 把定性设计维度卷入量化封顶 | 公平性 | +| 5 | 提效幅度无量化数据,AI raw 曾给 6 | 纯增益维度未严格基准制(**已修复**:isPureGainDim,2026-08-26) | 公平性 | +| 6 | 两队作品存在 8 个 MD5 相同文件,赛道一二无查重工具 | 查重初筛只在 L2 接入 | 真实性 | + +--- + +## 2. 方案总览:三层证据体系 + +``` +第①层 真实性审计(本方案 P1,全自动) + 代码信号 + 定向追踪 + 日志审计 + 全量可见性 + → 回答"做没做、真不真、记录可不可溯" +第②层 有效性验证(P2/P3,逐步落地) + 验收命令执行 + 影子样本混测 + 测量协议检查 + → 回答"跑起来管不管用、数字能不能复现" +第③层 人工终审(既有机制强化) + 评委手持①的证据表 + ②的可信度分级做价值裁量;灰色地带约谈(§10 已有) +``` + +**边界诚实声明**:①②完成后,系统能确认到「声明的功能真实实现了、过程记录可信、在陌生同构输入上表现不塌」。**商业价值大小、体验优劣的最终裁量保留给人工**——这是自动化评审的合理边界,不是缺陷。 + +--- + +## 3. P1 详细设计(本轮实施) + +### 3.1 全量代码可见性(解决问题 #3) + +#### 3.1.1 符号索引器(确定性,零 LLM 成本) + +新模块 `server/src/services/code-inventory.ts`: + +```ts +export interface FileSignature { + path: string; + lines: number; + lang: 'ts' | 'js' | 'py' | 'other'; + symbols: string[]; // export/function/class 定义行原文(≤5条) +} +export function buildInventory(files: {path,content}[]): { + signatures: FileSignature[]; // 全部文本文件 + totalLines: number; + coreFiles: string[]; // 按规则选出的"精读文件包" +} +``` + +- **语言范围**:ts/tsx/js/jsx/mjs + python(def/class 正则);其余语言仅记 path+lines +- **符号提取**:正则匹配 `^(export\s+)?(async\s+)?(function|class|const\s+\w+\s*=\s*(\(|async))` 与 python `^def |^class ` +- **coreFiles 选择规则**(替代现"前 60 个文件"粗暴截断): + 1. 入口/路由/服务/控制器目录下的代码文件(src/、app/、api/ 等) + 2. DIM_FILE_FILTERS 各维度命中的文件 + 3. 按行数降序补足至预算 + +#### 3.1.2 投喂策略改造 + +- 符号索引(全量)**必注入**所有实现类维度的 prompt 头部 +- `fileBlock` 预算动态化:普通维度 15000 → **40000 字符**;构建维度维持 40000 +- 截断策略改为"索引全量 + coreFiles 全文",外围文件只出现在索引里 + +#### 3.1.3 Map-Reduce 功能发现轮(新增专轮) + +新增函数 `discoverFeatureInventory(entryId, dir)`(review.service.ts): + +``` +输入:coreFiles 全文(按模块聚合分块,每块 ≤12000 字符,块数上限 12) +每块调用(小模型档): + "列出这段代码实现的全部功能,输出JSON: + [{feature:'一句话功能', files:['文件:行号'], depth:'real|partial|stub'}]" +合并去重 → FeatureInventory: + { features:[{name, files, depth}], totalFiles, analyzedChunks } +落库:entries.project_understanding 附加字段(或新列 feature_inventory TEXT) +``` + +- 注入**功能完整性/规模·功能点**维度 prompt:「以下是全量代码扫描发现的实际功能清单,请对照 README 声称与验收基准逐项核对」 +- 成本:约 8~12 次 LLM 调用(分钟级) + +### 3.2 AI 日志确定性审计(解决问题 #2) + +新模块 `server/src/services/ai-log-audit.ts`: + +```ts +export interface LogAuditResult { + exists: boolean; + lineCount: number; + recordCount: number; + placeholderRatio: number; // "待补充"/空值 记录占比 + involvedFiles: string[]; // 所有记录的涉及文件并集 + gitConsistencyRate: number; // involvedFiles ∩ git变更文件 / involvedFiles + coveragePhases: string[]; // 出现过的范式步骤(需求/设计/编码/测试) + toPrompt: string; // 注入文本 + caps: { max?: number }; // 触发的封顶建议 +} +export function auditAiUsageLog(logContent: string, gitChangedFiles: Set): LogAuditResult +``` + +- 解析:Markdown 表格行按 `|` 切分;涉及文件列提取路径 token +- git 变更集:`simpleGit(dir).log(['--name-only'])` 收集全部变更文件路径集合(一次性,已克隆目录内零成本) +- **封顶规则**(写入 hard-rules 或内联应用): + - `!exists || lineCount < 5` → AI使用日志维度 ≤ maxScore×0.2 + - `placeholderRatio > 0.5` → ≤ maxScore×0.3 + - `gitConsistencyRate < 0.3 && involvedFiles.length >= 5` → ≤ maxScore×0.2,note 注明"日志与 git 历史不一致" +- **注入**:AI使用日志维度 extraDimContext 附 toPrompt(含上述数字),prompt 明确"评分必须与审计数字一致" + +### 3.3 代码信号检测器(解决问题 #1 的证据基础) + +新模块 `server/src/services/code-signals.ts`: + +```ts +export interface StubSignals { + todoDensity: number; // TODO/FIXME/暂不/not implemented 每千行 + hardcodedReturnRatio: number; // 业务函数 return 字面量占比(启发式) + deadImportCount: number; importButNeverCalled: {imp:string,file:string}[]; + stubFiles: string[]; // 空壳率>50% 的核心目录文件清单 + toPrompt: string; +} +export function detectStubSignals(coreFiles: {path,content}[], lang: string): StubSignals +``` + +- **定位为线索而非判据**:toPrompt 明确写"以下为疑似占位线索,请定向核查后再判定" +- 语言范围:TS/JS/Python(与 3.1.1 一致),其他语言返回空报告 +- 注入:实现类维度(名称命中 `['功能完整','实现完整','规模','功能点']`)的 extraDimContext + +### 3.4 实现类维度定向追踪审查(解决问题 #1 的判定端) + +改造实现类维度子 Agent prompt(通过 extraDimContext 追加任务模板): + +```markdown +## 定向追踪任务(本维度评分的核心依据) + +README/验收基准声称的核心功能如下: +[来自验收基准或功能发现轮清单] + +请对每项声称的功能执行: +1. 定位实现入口(文件:行号) +2. 追踪调用链:入口 → 中间层 → 最终处理逻辑,说明每一层实际做了什么 +3. 判定实现深度:真实 / 部分实现 / 占位(Mock) + - 真实=能对任意合理输入产生正确输出 + - 部分=主干通但边界/异常缺失 + - 占位=返回固定数据、空逻辑、仅UI无处理 +4. 引用代码原文作为证据(不少于1行) + +输出严格JSON: +{"checks":[{"feature":"功能名","entry":"文件:行号","depth":"real|partial|stub", + "evidence":"代码原文摘录","reason":"判定理由"}], + "summary":"总体实现真实性结论"} + +此核对表直接决定本维度得分: +真实=该项满分权重;部分=50%权重;占位=0分。 +``` + +- **评分绑定**:子 Agent 返回的 checks 表存入 ai_report.dimensions[n].checks;解析失败回退现状(不崩) +- 结合 3.3 信号:toPrompt 中的 stubFiles 作为"重点核查"提示一并注入 + +### 3.5 判档修正:定性设计维度豁免 C 档(解决问题 #4) + +standard-utils.ts: + +```ts +// 旧:EFFECT_EVIDENCE_KEYS 过宽,把设计合理性类定性维度卷入量化封顶 +// 新:量化证据维度白名单——只有这些维度适用三档封顶 +const QUANT_EVIDENCE_KEYS = ['提效幅度', '效果对比', '效率提升', '效果评估', '效果与数据']; +export function isQuantEvidenceDim(name): boolean // 替代 isEffectDim 在三档中的使用 +``` + +- `classifyVerifiability`:非量化白名单维度一律 B 档(含"提效设计合理性""XX清晰度"等定性维度) +- `detectStructuralContradictions` 的效果维度筛选同步改用 isQuantEvidenceDim +- `isEffectDim` 保留兼容(聚合等其他引用处逐一排查后决定去留) +- **回归预期**:净码特攻"提效设计合理性"不再被环境因素压到 3;六边形"稳定性满分+Mock内核"的组合不受此项影响(其问题由 3.4 追踪表解决) + +### 3.6 杂项 + +- **分支提醒**:cloneRepo 后检测默认分支 ≠ main → addLog 警告"默认分支为 X,不符合规范 §3 要求",不阻断评审 +- **赛道一二接查重**(P2,复用 plagiarism-detect,flags 进详情页) +- **AGENTS.md** 同步本方案要点 + +--- + +## 4. P2/P3 规划(本轮不实施) + +| 期 | 项 | 依赖 | +|---|---|---| +| P2 | 验收命令标准化:README 必填 `## 验收` 命令块,系统新增 tryAccept 执行并留存输出 | 无(规范需同步修订) | +| P2 | 测量协议结构化检查:`data/measurement/` 齐备性确定性检查 + 注入提效幅度 prompt | 无 | +| P2 | 赛道一二查重初筛接入 | 无 | +| P3 | 影子样本机制:命题组按题制作同构官方样本,验收时混测 | 命题组配合 | +| P3 | benchmark.ts 激活:seed 缺陷库自动测定评审工具检出率 | seed 库建设 | +| P3 | Agentic 按需读取(list/read/search 工具化子 Agent) | 架构改造 | + +--- + +## 5. 边界与诚实声明 + +1. **语义级价值判断保留人工**:系统可确认"真实实现了声明的功能、过程可溯、陌生输入上不塌";"商业价值/体验优劣"由评委终审(第③层) +2. **多语言覆盖**:深度信号限 TS/JS/Py;其余语言降为基础统计(规范允许任意技术栈,长尾语言接受较弱信号) +3. **prompt 型作品覆盖弱**:Agent 类作品核心逻辑在提示词编排时,代码信号敏感度下降——靠定向追踪(LLM 读 prompt 文件本身)+ 日志审计兜底 +4. **成本增量**:单条目评审时间预计 ×1.3(+功能发现轮);token 成本 ×1.4 左右。MAX_CONCURRENT=3 不变 +5. **AI 波动仍存在**:三轮中位数聚合继续有效;本方案降低的是"系统性盲区",不是随机波动 + +--- + +## 6. 回归验证场景(实施后必须重测) + +| 场景 | 预期变化 | 对应机制 | +|---|---|---| +| 六边形战队(Mock内核) | 功能完整性维度产出 checks 表,占位项计 0 分;AI使用日志因占位率 100% 被封顶 ≤3;总分显著低于 72 | 3.4 + 3.2 | +| 净码特攻 b3(真实linter集成) | 总分应 ≥67(不应因本次改动下跌);"提效设计合理性"不再被误 C 档 | 3.5 | +| cobol-java(赛道一回归) | 总分 ±5 内波动;无 crash | 全部 | +| L2 六边形 b3 样本 | 提效幅度维持 C≤3;功能完整性出现 checks 表 | 3.4 | + +--- + +## 7. P1 实施清单 + +- [ ] code-inventory.ts 符号索引器 + 单测 +- [ ] discoverFiles 投喂策略改造(索引全量 + coreFiles 全文 + 预算 40K) +- [ ] discoverFeatureInventory Map-Reduce 功能发现轮 + 落库 + 注入 +- [ ] ai-log-audit.ts + git 变更集采集 + 封顶应用 + 注入 + 单测 +- [ ] code-signals.ts + 单测 +- [ ] 实现类维度定向追踪 prompt 模板 + checks 解析 + 评分绑定 +- [ ] standard-utils.ts 判档白名单重构(isQuantEvidenceDim)+ 相关单测更新 +- [ ] 分支提醒 +- [ ] AGENTS.md 同步 +- [ ] 回归:§6 四场景实测 + 全量 vitest diff --git a/server/src/__tests__/review-authenticity.test.ts b/server/src/__tests__/review-authenticity.test.ts new file mode 100644 index 0000000..808e27b --- /dev/null +++ b/server/src/__tests__/review-authenticity.test.ts @@ -0,0 +1,126 @@ +import { describe, it, expect } from 'vitest'; +import { extractSignatures, renderInventoryText } from '../services/code-inventory'; +import { detectStubSignals } from '../services/code-signals'; +import { auditAiUsageLog, renderLogAuditToPrompt } from '../services/ai-log-audit'; +import { isQualitativeDesignDim, isPureGainDim } from '../services/standard-utils'; + +describe('TC-INV · 全量符号索引(2026-08-26)', () => { + const mk = (path: string, content: string) => ({ path, content, size: content.length }); + + it('提取 TS 导出符号', () => { + const sigs = extractSignatures([mk('src/a.ts', 'export function foo() {}\nexport class Bar {}\nexport const x = 1;')]); + expect(sigs).toHaveLength(1); + expect(sigs[0].lang).toBe('ts'); + expect(sigs[0].lines).toBe(3); + expect(sigs[0].symbols.length).toBeGreaterThanOrEqual(2); + }); + + it('提取 Python def/class', () => { + const sigs = extractSignatures([mk('mod.py', 'def calc(x):\n return x\ndef other():\n pass\n')]); + expect(sigs[0].lang).toBe('py'); + expect(sigs[0].symbols.some(s => s.includes('def calc'))).toBe(true); + }); + + it('非文本文件只记 path+lines', () => { + const sigs = extractSignatures([{ path: 'assets/logo.png', size: 100000 }]); + expect(sigs[0].lang).toBe('other'); + expect(sigs[0].symbols).toHaveLength(0); + }); + + it('renderInventoryText 含文件路径与预算截断', () => { + const sigs = extractSignatures([mk('src/a.ts', 'export function f(){}\n'), mk('src/b.ts', 'export function g(){}\n')]); + const txt = renderInventoryText(sigs, 2000); + expect(txt).toContain('src/a.ts'); + expect(txt).toContain('仓库文件全量索引'); + }); +}); + +describe('TC-SIGNAL · 代码信号检测器(2026-08-26)', () => { + it('检测 TODO/占位标记', () => { + const r = detectStubSignals([ + { path: 'src/core.ts', content: '// TODO: 接入真实AI\nconst a = 1;\n'.repeat(5) }, + ]); + expect(r.applicable).toBe(true); + expect(r.todoHits.length).toBeGreaterThan(0); + }); + + it('非代码文件 → 不适用', () => { + const r = detectStubSignals([{ path: 'README.md', content: 'hello' }]); + expect(r.applicable).toBe(false); + }); + + it('硬编码返回被标记(启发式)', () => { + const r = detectStubSignals([ + { path: 'src/service.ts', content: 'export function get() {\n return { result: "ok" };\n}\n'.repeat(3) }, + ]); + expect(r.hardcodedReturns.length).toBeGreaterThan(0); + }); + + it('toPrompt 含线索说明', () => { + const r = detectStubSignals([{ path: 'src/a.ts', content: '// TODO: 待接入\nconst x=1;\n'.repeat(10) }]); + expect(r.toPrompt).toContain('代码信号扫描'); + }); +}); + +describe('TC-LOGAUDIT · AI 日志审计(2026-08-26)', () => { + const realLog = [ + '# AI Usage Log', + '| 日期时间 | 范式步骤 | 修改摘要 | 涉及文件 | 使用模型 |', + '|---------|---------|---------|---------|---------|', + '| 2026-07-10 | 需求分析 | 写spec | docs/a.md | deepseek |', + '| 2026-07-11 | 编码实现 | 改src | src/app.ts | deepseek |', + ].join('\n'); + + const placeholderLog = [ + '# AI Usage Log', + '| 日期时间 | 范式步骤 | 修改摘要 | 涉及文件 | 使用模型 |', + '|---------|---------|---------|---------|---------|', + '| 2026-08-01 | 待补充 | 待补充 | src/a.ts | - |', + '| 2026-08-02 | 待补充 | 待补充 | src/b.ts | - |', + ].join('\n'); + + it('真实日志:占位率低、覆盖环节识别', () => { + const r = auditAiUsageLog(realLog, ['docs/a.md', 'src/app.ts', 'other.ts']); + expect(r.exists).toBe(true); + expect(r.placeholderRatio).toBeLessThanOrEqual(0.2); + expect(r.gitConsistencyRate).toBeGreaterThanOrEqual(0.5); + expect(r.coveragePhases.length).toBeGreaterThan(0); + }); + + it('占位日志:占位率高 → 触发封顶提示', () => { + const r = auditAiUsageLog(placeholderLog, ['src/a.ts']); + expect(r.placeholderRatio).toBeGreaterThan(0.5); + const txt = renderLogAuditToPrompt(r); + expect(txt).toContain('占位记录占比超过50%'); + }); + + it('无日志 → 明确警告', () => { + const r = auditAiUsageLog(null, []); + const txt = renderLogAuditToPrompt(r); + expect(txt).toContain('未找到'); + }); + + it('git 一致率低 → 封顶提示', () => { + const r = auditAiUsageLog(realLog, ['unrelated.js', 'foo.js', 'bar.js', 'x.js', 'y.js', 'z.js']); + if (r.consistencyChecked >= 5 && r.gitConsistencyRate != null && r.gitConsistencyRate < 0.3) { + const txt = renderLogAuditToPrompt(r); + expect(txt).toContain('一致率低于30%'); + } else { + // 涉及文件不足5个时不强制触发,仅验证不崩溃 + expect(r.gitConsistencyRate).not.toBeNull(); + } + }); +}); + +describe('TC-DIMCLASS · 判档白名单(2026-08-26)', () => { + it('定性设计维度豁免', () => { + expect(isQualitativeDesignDim('提效设计合理性')).toBe(true); + expect(isQualitativeDesignDim('开发范式设计清晰度')).toBe(true); + expect(isQualitativeDesignDim('提效幅度')).toBe(false); + }); + it('纯增益维度判定', () => { + expect(isPureGainDim('提效幅度')).toBe(true); + expect(isPureGainDim('效果对比')).toBe(true); + expect(isPureGainDim('提效设计合理性')).toBe(false); + }); +}); diff --git a/server/src/services/ai-log-audit.ts b/server/src/services/ai-log-audit.ts new file mode 100644 index 0000000..591ef0e --- /dev/null +++ b/server/src/services/ai-log-audit.ts @@ -0,0 +1,108 @@ +/** + * AI 使用日志确定性审计(2026-08-26 评审真实性改进方案 §3.2): + * 解决"有壳无货"日志与真实日志无法区分的问题。 + * 三个硬指标:占位率 / git 一致率 / 覆盖广度——全部可计算、可复现, + * 结果注入评审 prompt 并触发维度封顶,AI 只能在证据框定的区间内微调。 + */ + +export interface LogAuditResult { + exists: boolean; + lineCount: number; + recordCount: number; // 表格数据行数(近似记录条数) + placeholderCount: number; + placeholderRatio: number; + involvedFiles: string[]; // 记录中提到的文件路径(basename 去重) + gitConsistencyRate: number | null; // null=git 信息不可用 + consistencyChecked: number; + coveragePhases: string[]; + toPrompt: string; +} + +const PATH_TOKEN_RE = /[\w./\\-]+\.(ts|tsx|js|jsx|mjs|cjs|py|java|go|rs|vue|html|css|scss|sql|md|yaml|yml|json|sh)\b/gi; + +export function auditAiUsageLog( + logContent: string | null | undefined, + gitChangedFiles: string[] | null +): LogAuditResult { + const result: LogAuditResult = { + exists: !!logContent && logContent.trim().length > 0, + lineCount: 0, recordCount: 0, placeholderCount: 0, placeholderRatio: 0, + involvedFiles: [], gitConsistencyRate: null, consistencyChecked: 0, + coveragePhases: [], toPrompt: '', + }; + if (!result.exists || !logContent) return result; + + const lines = logContent.split('\n'); + result.lineCount = lines.length; + + const PHASE_KEYS = ['需求', '设计', '编码', '实现', '测试']; + let dataRows = 0, placeholderRows = 0; + const involved = new Set(); + + for (const raw of lines) { + const l = raw.trim(); + if (l.startsWith('#') || /^[-|:\s]+$/.test(l)) continue; + + // Markdown 表格数据行 → 视为一条记录 + if (l.startsWith('|')) { + const cells = l.split('|').map(c => c.trim()).filter(c => c); + if (cells.length < 2) continue; + dataRows++; + + // 占位判定:任一关键单元格为占位词 + if (cells.some(c => /^(待补充|待定|TODO|-{1,3}|N\/A)$/i.test(c) || c === '')) placeholderRows++; + + // 涉及文件:从所有单元格提取路径 token + for (const cell of cells) { + for (const m of cell.matchAll(PATH_TOKEN_RE)) { + involved.add(m[0].replace(/\\/g, '/').split('/').pop()!.toLowerCase()); + } + } + } + + // 范式步骤覆盖 + for (const ph of PHASE_KEYS) { + if (l.includes(ph) && !result.coveragePhases.includes(ph)) result.coveragePhases.push(ph); + } + } + + result.recordCount = dataRows; + result.placeholderRatio = dataRows > 0 ? +(placeholderRows / dataRows).toFixed(2) : 1; + result.involvedFiles = [...involved]; + + // git 一致率:涉及文件的 basename 是否出现在 git 历史变更集中 + if (gitChangedFiles && gitChangedFiles.length > 0) { + const gitSet = new Set(gitChangedFiles.map(f => f.replace(/\\/g, '/').split('/').pop()!.toLowerCase())); + let hit = 0; + for (const f of result.involvedFiles) if (gitSet.has(f)) hit++; + result.consistencyChecked = result.involvedFiles.length; + result.gitConsistencyRate = result.involvedFiles.length > 0 + ? +(hit / result.involvedFiles.length).toFixed(2) : null; + } + + return result; +} + +/** 渲染审计结果为注入文本 */ +export function renderLogAuditToPrompt(r: LogAuditResult): string { + if (!r.exists) { + return '\n## AI 使用日志确定性审计\n⚠️ 未找到 _AI_USAGE_LOG.md 或文件为空。规范 §8 要求全程留痕,此情况按违规后果处理。'; + } + const lines: string[] = ['', '## AI 使用日志确定性审计(系统自动检测,评分必须与此一致)']; + lines.push(`- 日志行数:${r.lineCount},记录条数:${r.recordCount}`); + lines.push(`- 占位记录占比:${Math.round(r.placeholderRatio * 100)}%(${r.placeholderCount}/${r.recordCount} 条含"待补充"或空值)`); + if (r.gitConsistencyRate != null) { + lines.push(`- 涉及文件与 git 历史一致率:${Math.round(r.gitConsistencyRate * 100)}%(涉及 ${r.involvedFiles.length} 个文件)`); + } else { + lines.push('- git 一致率:无法计算(仓库无提交历史)'); + } + lines.push(`- 范式环节覆盖:${r.coveragePhases.length > 0 ? r.coveragePhases.join('/') : '未识别到环节关键词'}`); + lines.push(''); + lines.push('**评分约束**:'); + if (r.placeholderRatio > 0.5) lines.push('- 占位记录占比超过50%,该维度得分不得高于满分30%'); + if (r.gitConsistencyRate != null && r.gitConsistencyRate < 0.3 && r.consistencyChecked >= 5) { + lines.push('- 涉及文件与git历史一致率低于30%,涉嫌日志造假,该维度得分不得高于满分20%'); + } + if (lines.length <= 7 + 3) { /* no constraint triggered */ } + return '\n' + lines.join('\n'); +} diff --git a/server/src/services/code-inventory.ts b/server/src/services/code-inventory.ts new file mode 100644 index 0000000..492ce5f --- /dev/null +++ b/server/src/services/code-inventory.ts @@ -0,0 +1,109 @@ +import path from 'path'; + +/** + * 全量代码可见性(2026-08-26 评审真实性改进方案 §3.1): + * 对仓库全部文本文件生成轻量签名索引,让评审 AI 看见 100% 文件清单, + * 替代"前60个文件+15K截断"导致的盲区。 + */ + +export interface FileSignature { + path: string; + lines: number; + lang: 'ts' | 'js' | 'py' | 'other'; + symbols: string[]; +} + +const CODE_LANG: Record = { + '.ts': 'ts', '.tsx': 'ts', '.mts': 'ts', '.cts': 'ts', + '.js': 'js', '.jsx': 'js', '.mjs': 'js', '.cjs': 'js', + '.py': 'py', +}; + +/** 可提取符号的文本扩展名(其余只记 path+lines) */ +const TEXT_EXTS = new Set([ + ...Object.keys(CODE_LANG), + '.json', '.yaml', '.yml', '.md', '.html', '.css', '.scss', '.vue', + '.sql', '.sh', '.java', '.go', '.rs', '.rb', '.php', '.cs', '.kt', +]); + +// 各语言的定义行模式(保守匹配,宁缺勿滥) +const SYMBOL_PATTERNS: Record = { + ts: [ + /^\s*(export\s+)?(default\s+)?(async\s+)?function\s+\w+/, + /^\s*(export\s+)?(abstract\s+)?class\s+\w+/, + /^\s*export\s+(const|let|var)\s+\w+\s*=\s*(\(|async|function)/, + /^\s*export\s+(interface|type|enum)\s+\w+/, + /^\s*(public|private|protected|static)?\s*(async\s+)?\w+\s*\([^)]*\)\s*[:{]/, + ], + js: [ + /^\s*(export\s+)?(default\s+)?(async\s+)?function\s+\w+/, + /^\s*(export\s+)?(class\s+\w+)/, + /^\s*(export\s+)?(const|let|var)\s+\w+\s*=\s*(\(|async|function|\w+\s*=>)/, + /^\s*(module\.exports|exports\.\w+)\s*=/ + ], + py: [ + /^\s*def\s+\w+/, + /^\s*class\s+\w+/, + ], +}; + +export function extractSignatures(files: { path: string; content?: string; size?: number }[]): FileSignature[] { + const out: FileSignature[] = []; + for (const f of files) { + const ext = path.extname(f.path).toLowerCase(); + const rel = f.path.replace(/\\/g, '/'); + // 行数:有内容用内容算,否则用 size 粗估 + let lines = 0; + let lang: FileSignature['lang'] = 'other'; + const codeLang = CODE_LANG[ext]; + if (codeLang) lang = codeLang; + + let content: string | undefined; + if ('content' in f && typeof (f as any).content === 'string') { + content = (f as any).content as string; + lines = content.split('\n').length; + } else if (f.size != null) { + lines = Math.max(1, Math.round(f.size / 38)); // 平均 38 字节/行 估算 + } else { + lines = 1; + } + + if (!TEXT_EXTS.has(ext)) { + out.push({ path: rel, lines, lang: 'other', symbols: [] }); + continue; + } + + const symbols: string[] = []; + if (content && codeLang) { + const patterns = SYMBOL_PATTERNS[codeLang] || []; + for (const line of content.split('\n')) { + if (symbols.length >= 5) break; + for (const re of patterns) { + if (re.test(line)) { + symbols.push(line.trim().slice(0, 100)); + break; + } + } + } + } + out.push({ path: rel, lines, lang, symbols }); + } + return out; +} + +/** 渲染紧凑索引文本(供注入 prompt)——全量文件,每文件 1~3 行 */ +export function renderInventoryText(sigs: FileSignature[], budgetChars = 8000): string { + const lines: string[] = ['', '## 仓库文件全量索引(共 ' + sigs.length + ' 个文件,此清单为完整底账,评分必须覆盖其中核心文件)']; + let used = lines.join('\n').length; + for (const s of sigs) { + const symLine = s.symbols.length ? ' | ' + s.symbols.slice(0, 2).join(' ; ') : ''; + const l = `- ${s.path} (${s.lines}行${s.lang !== 'other' ? ',' + s.lang : ''})${symLine}`; + if (used + l.length > budgetChars) { + lines.push(`- …(其余 ${sigs.length - sigs.indexOf(s)} 个文件见截断说明,均为外围配置/资源)`); + break; + } + lines.push(l); + used += l.length; + } + return '\n' + lines.join('\n'); +} diff --git a/server/src/services/code-signals.ts b/server/src/services/code-signals.ts new file mode 100644 index 0000000..2ad2133 --- /dev/null +++ b/server/src/services/code-signals.ts @@ -0,0 +1,150 @@ +import path from 'path'; + +/** + * 代码信号检测器(2026-08-26 评审真实性改进方案 §3.3): + * 确定性统计信号,定位为"线索"而非"判据"——输出注入实现类维度 prompt, + * 提示 AI 定向核查疑似占位/空壳文件,真假判定权归 AI 追踪 + 人工约谈。 + * + * 语言范围:TS/JS/Python(其余语言返回空报告,降级为基础统计)。 + */ + +export interface StubSignals { + applicable: boolean; + totalFiles: number; + todoDensity: number; // 每千行 TODO/FIXME/未实现 出现次数 + todoHits: { file: string; line: number; text: string }[]; + stubFiles: string[]; // 空壳率>50% 的代码文件(实现行占比低) + hardcodedReturns: { file: string; line: number }[]; // 业务函数 return 字面量(启发式) + deadImports: { imp: string; file: string }[]; // 声明集成但全文无实际调用的依赖 + toPrompt: string; +} + +const TODO_RE = /\b(TODO|FIXME|XXX|HACK)\b|暂不|待实现|待接入|not\s*implemented/i; +const STUB_RETURN_RE = /^\s*(return|=>)\s*[\[{"][^`]*[\]}"]?\s*;?\s*$/; + +/** 核心目录判定:路径含这些片段的文件视为业务核心(排除配置/测试/脚本) */ +const CORE_HINTS = [/^src\//i, /\/services?\/|\/controllers?\/|\/handlers?\/|\/engines?\/|\/core\/|\/analyzers?\//i]; + +export function detectStubSignals(files: { path: string; content: string }[]): StubSignals { + const empty: StubSignals = { + applicable: false, totalFiles: files.length, todoDensity: 0, todoHits: [], + stubFiles: [], hardcodedReturns: [], deadImports: [], + toPrompt: '', + }; + + // 只处理 TS/JS/Py + const code = files.filter(f => /\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(f.path) + && !/node_modules|dist|build|__pycache__|\.d\.ts$/i.test(f.path)); + if (code.length === 0) return empty; + empty.applicable = true; + + const todoHits: StubSignals['todoHits'] = []; + const hardcodedReturns: StubSignals['hardcodedReturns'] = []; + const stubFiles: string[] = []; + let totalLines = 0; + const declaredImps: { imp: string; file: string; used: boolean }[] = []; + const deadImports: { imp: string; file: string }[] = []; + + for (const f of code) { + const lines = f.content.split('\n'); + const isPy = /\.py$/i.test(f.path); + totalLines += lines.length; + + let implLines = 0; // 有实际逻辑的行(分支/循环/调用/赋值计算) + let inBlockComment = false; + + for (let i = 0; i < lines.length; i++) { + const raw = lines[i]; + const l = raw.trim(); + + // 块注释状态机(/* */ 与 Python 三引号简化处理) + if (/^\/\*|^'''|^"""/.test(l)) inBlockComment = !inBlockComment || /^\/\*\*$/.test(l) ? !/^\/\*/.test(l) ? false : true : false; + if (inBlockComment) continue; + + // TODO/占位检测须在注释过滤之前——// TODO 本身就是信号 + if (TODO_RE.test(l)) { + todoHits.push({ file: f.path, line: i + 1, text: l.slice(0, 80) }); + } + + if (!l || l.startsWith('//') || l.startsWith('#')) continue; + + // 硬编码 return 启发式:return 后紧跟完整字面量且行内无变量名/函数调用 + if (/^\s*(return\s+)(\{[^}]*\}|\[[^\]]*\]|['"][^'"]+['"])\s*;?\s*$/.test(raw) + && !/[a-zA-Z]\s*[+\-*/]|\w+\(/.test(raw)) { + hardcodedReturns.push({ file: f.path, line: i + 1 }); + } + + // 实现行估算 + if (/\b(if|for|while|switch|try|catch|except)\b|[=<>!]=|\.push\(|\.map\(|\.filter\(|await\s+\w+\(|return\s+\w/.test(l)) implLines++; + + // import 收集(TS/JS) + if (!isPy) { + const im = raw.match(/^\s*import\s+.*\s+from\s+['"]([^'"]+)['"]/) || raw.match(/^\s*const\s+\w+\s*=\s*require\(['"]([^'"]+)['"]\)/); + if (im && !im[1].startsWith('.')) declaredImps.push({ imp: im[1], file: f.path, used: false }); + } + } + + // 空壳文件判定:核心目录 + ≥30 行 + 有逻辑的行 <20% + const coreish = CORE_HINTS.some(re => re.test(f.path.replace(/\\/g, '/'))); + if (coreish && lines.length >= 30) { + const implRatio = implLines / lines.length; + if (implRatio < 0.2) stubFiles.push(f.path); + } + } + + // 死导入复核:import 声明的包名是否在任一文件的后续使用行出现 + for (const di of declaredImps) { + const pkgTail = di.imp.split('/').pop() || di.imp; + const usageRe = new RegExp('[^\'"`]' + pkgTail.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\.', 'i'); + const usedElsewhere = code.some(f => + !(f.path === di.file && true) && usageRe.test(f.content)) || + new RegExp('from\\s+[\'"]' + di.imp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\'"]').test(''); + void usedElsewhere; + // 简化:检查除声明行外是否有 `.` 使用 + const ownerFile = code.find(f => f.path === di.file); + if (ownerFile) { + const withoutDecl = ownerFile.content.split('\n').filter(l => !l.includes(di.imp)).join('\n'); + di.used = withoutDecl.includes(pkgTail + '.'); + } + if (!di.used) deadImports.push({ imp: di.imp, file: di.file }); + } + // 去重(同包多文件声明只报一次) + const seenDead = new Set(); + const deadUnique = deadImports.filter((x: { imp: string; file: string }) => { + const k = x.imp; + if (seenDead.has(k)) return false; + seenDead.add(k); return true; + }).slice(0, 8); + + const todoDensity = totalLines > 0 ? +(todoHits.length / (totalLines / 1000)).toFixed(1) : 0; + + const lines: string[] = []; + lines.push(`代码信号扫描(${code.length} 个代码文件 / ${totalLines} 行,以下为确定性线索,请定向核查后再判定)`); + if (todoHits.length > 0) { + lines.push(`- TODO/FIXME/占位标记:${todoHits.length} 处(密度 ${todoDensity}/千行),示例:`); + for (const h of todoHits.slice(0, 5)) lines.push(` · ${h.file}:${h.line} ${h.text}`); + } else { + lines.push('- TODO/FIXME/占位标记:未发现'); + } + if (stubFiles.length > 0) { + lines.push(`- 疑似空壳文件(有逻辑行占比<20%):${stubFiles.slice(0, 6).join(', ')}`); + } + if (deadUnique.length > 0) { + lines.push(`- 声明但未见使用的依赖:${deadUnique.map((x: { imp: string }) => x.imp).join(', ')}`); + } + if (hardcodedReturns.length > 0) { + lines.push(`- 直接返回字面量的位置 ${hardcodedReturns.length} 处(可能是合法常量表,也可能是 Mock 数据——请在追踪调用链时重点甄别):`); + for (const h of hardcodedReturns.slice(0, 4)) lines.push(` · ${h.file}:${h.line}`); + } + + return { + applicable: true, + totalFiles: code.length, + todoDensity, + todoHits: todoHits.slice(0, 10), + stubFiles, + hardcodedReturns: hardcodedReturns.slice(0, 10), + deadImports: deadUnique, + toPrompt: '\n## 代码信号扫描结果(确定性线索)\n' + lines.join('\n'), + }; +} diff --git a/server/src/services/review-constants.ts b/server/src/services/review-constants.ts index 547a4d2..1d482ee 100644 --- a/server/src/services/review-constants.ts +++ b/server/src/services/review-constants.ts @@ -1,7 +1,7 @@ export const REVIEW_CONSTANTS = { MAX_CONCURRENT: 3, MAX_OVERVIEW_CHARS: 30000, - MAX_FILE_CHARS_NORMAL: 15000, + MAX_FILE_CHARS_NORMAL: 40000, MAX_FILE_CHARS_BUILD: 40000, DUP_CAP_SCORE: 3, NO_README_CAP: 2, diff --git a/server/src/services/review.service.ts b/server/src/services/review.service.ts index 887f9c1..9166aa8 100644 --- a/server/src/services/review.service.ts +++ b/server/src/services/review.service.ts @@ -12,6 +12,9 @@ import { isPathInside } from '../path-security'; import { applyHardRules, applyTrackHardRules } from './hard-rules'; import { findL2Topic, buildL2FuncContext } from './l2-topics'; import { detectPlagiarism, readRepoFiles, detectCommitBehavior, PlagiarismReport } from './plagiarism-detect'; +import { extractSignatures, renderInventoryText } from './code-inventory'; +import { detectStubSignals } from './code-signals'; +import { auditAiUsageLog, renderLogAuditToPrompt } from './ai-log-audit'; import { REVIEW_CONSTANTS, BUILD_SYSTEMS, @@ -199,6 +202,150 @@ function buildL2ExtraContexts(entry: any): Record | undefined { return { '功能完整性': '\n' + block }; } +/** 收集 git 全部变更文件 basename 集合(用于 AI 日志一致率审计) */ +async function collectGitChangedFiles(dir: string): Promise { + try { + const git = simpleGit(dir); + if (!(await git.checkIsRepo())) return null; + const raw = await git.raw(['log', '--name-only', '--pretty=format:']); + return raw.split('\n').map(l => l.trim()).filter(Boolean); + } catch { return null; } +} + +/** + * 审计上下文(2026-08-26):全量符号索引 + 代码信号 + 定向追踪模板 + AI 日志审计。 + * 按"维度名 → 注入文本"返回,合并进 extraDimContext 机制。 + */ +function buildAuditExtraContexts( + entryId: string, + standardDims: { name: string }[], + files: { path: string; content?: string; size?: number }[], + featureInvText: string +): Record { + const ctx: Record = {}; + + // 1. 全量符号索引 + const sigs = extractSignatures(files); + const invText = renderInventoryText(sigs); + + // 2. 代码信号 + const codeFiles = files.filter(f => typeof f.content === 'string') as { path: string; content: string }[]; + const stubs = detectStubSignals(codeFiles); + + // 3. AI 日志审计 + const logFile = files.find(f => /_?ai[_ -]?usage[_ -]?log\.md$/i.test(f.path)); + let logAuditPrompt = ''; + if (logFile && typeof logFile.content === 'string') { + const la = auditAiUsageLog(logFile.content, null); // git 一致率在管线层另行计算后覆盖 + logAuditPrompt = renderLogAuditToPrompt(la); + } + + // 4. 组装:定向追踪模板注入实现类维度;日志审计注入 AI 日志维度;符号索引注入所有实现类维度 + const IMPL_RE = /功能完整|实现完整|规模|功能点/; + const LOG_RE = /AI.*日志|AI协作/; + for (const dim of standardDims) { + const texts: string[] = []; + if (IMPL_RE.test(dim.name)) { + texts.push(invText); + texts.push(stubs.toPrompt); + if (featureInvText) texts.push(featureInvText); + texts.push(getDirectedTraceTemplate()); + } + if (LOG_RE.test(dim.name) && logAuditPrompt) { + texts.push(logAuditPrompt); + } + if (texts.length > 0) ctx[dim.name] = '\n' + texts.join('\n'); + } + return ctx; +} + +/** + * Map-Reduce 功能发现轮(2026-08-26 P1):对全部源码分块扫描,汇总实际功能清单, + * 注入实现类维度让 AI 对照 README/验收基准逐项核对。失败非致命(返回空串)。 + */ +async function discoverFeatureInventory(entryId: string, files: { path: string; content?: string; size?: number }[]): Promise { + try { + const src = files.filter(f => /\.(ts|tsx|js|jsx|mjs|cjs|py|java|go|rs|vue)$/i.test(f.path) + && typeof f.content === 'string' && !/node_modules|dist|build|__pycache__/i.test(f.path)) as { path: string; content: string }[]; + if (src.length === 0) return ''; + // 分块:聚合文件,每块 ≤12000 字符,最多 12 块 + const CHUNK_MAX = 12000; + const chunks: string[] = []; + let cur = ''; + for (const f of src) { + const block = `--- ${f.path} ---\n${f.content}`; + if (cur.length + block.length > CHUNK_MAX) { + if (cur.trim()) chunks.push(cur.trim()); + cur = block; + } else { + cur += '\n' + block; + } + if (chunks.length >= 11) { if (cur.trim()) chunks.push(cur.trim()); cur = ''; break; } + } + if (cur.trim() && chunks.length < 12) chunks.push(cur.trim()); + if (chunks.length === 0) return ''; + + const perChunk: string[] = []; + for (let i = 0; i < chunks.length; i++) { + const prompt = `你是代码审计助手。下面是一段项目源码片段(分块 ${i + 1}/${chunks.length})。 +列出这段代码实现的所有功能,输出严格JSON数组: +[{"feature":"一句话功能描述","files":["文件路径"],"depth":"real|partial|stub"}] +- depth: real=真实逻辑; partial=部分实现; stub=占位/仅框架(return固定值、空函数体、TODO) +- 只列实际由代码体现的功能,不要臆测 +源码: +${chunks[i].slice(0, CHUNK_MAX)}`; + const raw = await callDeepSeek(prompt, 1, 'feature-inventory'); + if (raw) perChunk.push(raw.trim()); + } + if (perChunk.length === 0) return ''; + + const mergePrompt = `汇总以下多块源码的功能发现结果,去重合并(同名功能取更深的 depth),输出严格JSON数组: +[{"feature":"一句话功能描述","files":["文件路径"],"depth":"real|partial|stub"}] +各块结果: +${perChunk.map((p, i) => `--- 块${i + 1} ---\n${p}`).join('\n\n')}`; + const merged = await callDeepSeek(mergePrompt, 1, 'feature-inventory-merge'); + if (!merged) return ''; + try { + const arr = JSON.parse(merged.replace(/```(?:json)?\s*([\s\S]*?)```/g, '$1').trim()); + if (!Array.isArray(arr)) return ''; + pipeLog(entryId, 'FEATURES', `found ${arr.length} features`); + const lines = arr.map((f: any) => `- ${f.feature} [${f.depth}] (${(f.files || []).join(', ')})`).slice(0, 80); + return '\n## 全量代码功能发现(Map-Reduce 扫描,供对照 README/验收基准逐项核对)\n' + lines.join('\n'); + } catch { + return ''; + } + } catch (e: any) { + pipeLog(entryId, 'FEATURES', `skipped: ${e.message}`); + return ''; + } +} + +const DIRECTED_TRACE_TEMPLATE = ` +## 定向追踪任务(本维度评分的核心依据) + +请先从 README 中提取声称的核心功能清单,然后对每项功能执行以下四步: + +1. **定位实现入口**(文件名:行号) +2. **追踪调用链**:入口 → 中间层 → 最终处理逻辑,说明每一层实际做了什么 +3. **判定实现深度**:真实 / 部分实现 / 占位(Mock) + - 真实=能对任意合理输入产生正确输出 + - 部分=主干通但边界/异常缺失 + - 占位=返回固定数据、空逻辑、仅UI无处理 +4. **引用代码原文**作为证据(不少于1行) + +输出严格JSON: +{"checks":[{"feature":"功能名","entry":"文件:行号","depth":"real|partial|stub","evidence":"代码原文","reason":"判定理由"}], + "summary":"总体实现真实性结论"} + +此核对表直接决定本维度得分: +- 真实 = 该项满分权重 +- 部分实现 = 该项50%权重 +- 占位(Mock) = 该项0分`; + +function getDirectedTraceTemplate(): string { + return '\n' + DIRECTED_TRACE_TEMPLATE; +} + async function runReview(entryId: string, stage: 'A' | 'B', buildStatus?: 'done' | 'failed') { activeCount++; const tRun = Date.now(); @@ -1049,6 +1196,12 @@ async function executeReview(entryId: string) { pipeLog(entryId, 'CLONE', cloneOk ? 'ok' : 'FAILED', Date.now() - tClone); if (!cloneOk) return; + // 分支提醒:默认分支≠main 时写入日志(不阻断评审) + try { + const br = await simpleGit(dir).branchLocal(); + if (br.current && br.current !== 'main') addLog(entryId, 'cloning', `⚠️ 默认分支为 ${br.current},不符合规范要求的 main`); + } catch { } + // L2考核:迟交超7个工作日 → 按0分处理,不启动评审(终态) if (await maybeFinalizeL2Abandoned(entry, dir)) return; @@ -1065,6 +1218,9 @@ async function executeReview(entryId: string) { // L2考核:跨仓库查重初筛(确定性 flags,供人工复核,不自动定罪) await runPlagiarismScreening(entry, dir); + // 评审真实性 P1:Map-Reduce 全量功能发现(供实现类维度对照验收基准) + const featureInventoryText = await discoverFeatureInventory(entryId, files); + // 方案②:项目理解文档(AI 解读代码生成,落库供 B 阶段与黑盒冒烟复用) const tUnderstand = Date.now(); const understanding = await buildProjectUnderstanding(entryId, dir, files, codeStats); @@ -1240,11 +1396,13 @@ ${overviewFileBlock} const dimensions: any[] = []; const toRun = [...standardDims]; const l2ExtraContexts = buildL2ExtraContexts(entry); + const auditExtra = buildAuditExtraContexts(entryId, standardDims, files, featureInventoryText); + const mergedExtra = Object.keys(auditExtra).length > 0 ? { ...l2ExtraContexts, ...auditExtra } : l2ExtraContexts; const runNext = async () => { while (toRun.length > 0) { const dim = toRun.shift()!; const tDim = Date.now(); - const r = await runSubAgent(dim, projectContext, files, buildResult, startResult, browseResult, entry.category_tag, baseBranchDiff, agentGateReport, testEvidence, undefined, l2ExtraContexts); + const r = await runSubAgent(dim, projectContext, files, buildResult, startResult, browseResult, entry.category_tag, baseBranchDiff, agentGateReport, testEvidence, undefined, mergedExtra); if (r) dimensions.push(r); pipeLog(entryId, ' DIM', `${r?.name || '?'} → ${r?.score}/${r?.maxScore} [${Date.now() - tDim}ms]`); } @@ -1273,7 +1431,7 @@ ${overviewFileBlock} }; for (const d of dimensions) { const v = classifyVerifiability(d, verifEvidence); - pipeLog(entryId, ' VERIF', `${d.name} raw=${d.score} tier=${v.tier} capped=${v.capped} eff=${v.effectiveScore}`); + pipeLog(entryId, ' VERIF', `${JSON.stringify(d.name)} raw=${d.score} tier=${v.tier} capped=${v.capped} eff=${v.effectiveScore} ev=${JSON.stringify(verifEvidence)}`); if (v.capped && d.score > v.effectiveScore) { d.score = v.effectiveScore; (d as any).verifiability = v; @@ -1284,6 +1442,30 @@ ${overviewFileBlock} totalScore = dimensions.reduce((s, d) => s + d.score, 0); } + // AI 日志确定性审计封顶(2026-08-26):占位率>50% 或 git一致率<30% → 封顶 + let calibrationExplanation = ''; + const aiLogFile = files.find(f => /_?ai[_ -]?usage[_ -]?log\.md$/i.test(f.path)); + if (aiLogFile && typeof aiLogFile.content === 'string') { + try { + const gitFiles = await collectGitChangedFiles(dir); + const la = auditAiUsageLog(aiLogFile.content, gitFiles); + pipeLog(entryId, 'LOGAUDIT', `lines=${la.lineCount} records=${la.recordCount} placeholder=${Math.round(la.placeholderRatio*100)}% gitConsist=${la.gitConsistencyRate}`); + const logDim = dimensions.find(d => d.name.includes('AI使用日志') || d.name.includes('AI协作过程记录')); + if (logDim) { + let cap: number | null = null; + let capNote = ''; + if (la.lineCount < 5) { cap = Math.floor(logDim.maxScore * 0.2); capNote = '日志为空或过短'; } + else if (la.placeholderRatio > 0.5) { cap = Math.min(cap ?? Infinity, Math.floor(logDim.maxScore * 0.3)); capNote = '占位记录占比超50%'; } + if (la.gitConsistencyRate != null && la.gitConsistencyRate < 0.3 && la.consistencyChecked >= 5) { cap = Math.min(cap ?? Infinity, Math.floor(logDim.maxScore * 0.2)); capNote = '日志与git历史一致率低于30%'; } + if (cap != null && logDim.score > cap) { + logDim.score = cap; + calibrationExplanation += `\n\n日志审计封顶:\n- ${logDim.name} → ${cap}(${capNote})`; + (logDim as any).auditNote = capNote; + } + } + } catch { } + } + // Phase 3b: AI calibration addLog(entryId, 'analyzing', '正在校准评分...'); const calibrationPrompt = `你是一个评审校准Agent。以下各维度的评分和评语来自子Agent的独立评审。请检测跨维度语义矛盾(例如:开发范式说"无任何设计"但实现完整度却发现了3个Agent协作机制;效果数据满分但代码规模维度却显示几乎无实现)。 @@ -1300,7 +1482,6 @@ ${JSON.stringify(dimensions.map(d => ({ name: d.name, score: d.score, maxScore: - 维度名必须与输入完全一致`; const calibrationRaw = await callDeepSeek(calibrationPrompt, 2, 'calibrate'); - let calibrationExplanation = ''; let contradictions: { name: string; direction: 'over' | 'under' }[] = []; try { if (calibrationRaw) { @@ -1500,11 +1681,13 @@ ${overviewFileBlockA} const dimensionsA: any[] = []; const toRunA = [...aDims]; const l2ExtraContextsA = buildL2ExtraContexts(entry); + const auditExtraA = buildAuditExtraContexts(entryId, aDims, files, featureInventoryText); + const mergedExtraA = Object.keys(auditExtraA).length > 0 ? { ...l2ExtraContextsA, ...auditExtraA } : l2ExtraContextsA; const runNextA = async () => { while (toRunA.length > 0) { const dim = toRunA.shift()!; const tDim = Date.now(); - const r = await runSubAgent(dim, projectContextA, files, EMPTY_BUILD_RESULT, undefined, undefined, entry.category_tag, baseBranchDiff, agentGateReport, undefined, undefined, l2ExtraContextsA); + const r = await runSubAgent(dim, projectContextA, files, EMPTY_BUILD_RESULT, undefined, undefined, entry.category_tag, baseBranchDiff, agentGateReport, undefined, undefined, mergedExtraA); if (r) dimensionsA.push(r); pipeLog(entryId, ' DIM', `${r?.name || '?'} → ${r?.score}/${r?.maxScore} [${Date.now() - tDim}ms]`); } @@ -1750,11 +1933,13 @@ async function executeReviewB(entryId: string, buildStatus: 'done' | 'failed' = const dimensionsB: any[] = []; const toRunB = [...bDims]; const l2ExtraContextsB = buildL2ExtraContexts(entry); + const auditExtraB = buildAuditExtraContexts(entryId, bDims, files, ''); + const mergedExtraB = Object.keys(auditExtraB).length > 0 ? { ...l2ExtraContextsB, ...auditExtraB } : l2ExtraContextsB; const runNextB = async () => { while (toRunB.length > 0) { const dim = toRunB.shift()!; const tDim = Date.now(); - const r = await runSubAgent(dim, projectContextB, files, buildResult, startResult, browseResult, entry.category_tag, '', undefined, testEvidence, smokeEvidence, l2ExtraContextsB); + const r = await runSubAgent(dim, projectContextB, files, buildResult, startResult, browseResult, entry.category_tag, '', undefined, testEvidence, smokeEvidence, mergedExtraB); if (r) dimensionsB.push(r); pipeLog(entryId, ' DIM', `${r?.name || '?'} → ${r?.score}/${r?.maxScore} [${Date.now() - tDim}ms]`); } @@ -1851,6 +2036,25 @@ ${JSON.stringify(dimensionsB.map(d => ({ name: d.name, score: d.score, maxScore: // scoreB + 合???A+B let scoreB = 0; let maxScoreB = 0; + + // AI 日志确定性审计封顶(B 阶段) + { + const aiLogDimB = [...aDimensions, ...dimensionsB].find(d => d.name.includes('AI使用日志') || d.name.includes('AI协作过程记录')); + const logFileB = files.find(f => /_?ai[_ -]?usage[_ -]?log\.md$/i.test(path.basename(f.path))); + if (aiLogDimB && logFileB && typeof (logFileB as any).content === 'string') { + try { + const gitFiles = await collectGitChangedFiles(dir); + const la = auditAiUsageLog((logFileB as any).content, gitFiles); + pipeLog(entryId, 'LOGAUDIT_B', `placeholder=${Math.round(la.placeholderRatio*100)}% gitConsist=${la.gitConsistencyRate}`); + let cap: number | null = null; + if (!la.exists || la.lineCount < 5) { cap = Math.floor(aiLogDimB.maxScore * 0.2); } + else if (la.placeholderRatio > 0.5) { cap = Math.min(cap ?? Infinity, Math.floor(aiLogDimB.maxScore * 0.3)); } + if (la.gitConsistencyRate != null && la.gitConsistencyRate < 0.3) { cap = Math.min(cap ?? Infinity, Math.floor(aiLogDimB.maxScore * 0.2)); } + if (cap != null && aiLogDimB.score > cap) { aiLogDimB.score = cap; calibrationExplanationB += `\n\n日志审计封顶:\n- ${aiLogDimB.name} → ${cap}`; } + } catch { } + } + } + for (const d of dimensionsB) { scoreB += Math.round(d.score); maxScoreB += d.maxScore; diff --git a/server/src/services/standard-utils.ts b/server/src/services/standard-utils.ts index 2aff576..e2f562f 100644 --- a/server/src/services/standard-utils.ts +++ b/server/src/services/standard-utils.ts @@ -276,12 +276,21 @@ export function isPureGainDim(name: string): boolean { return PURE_GAIN_KEYS.some(k => (name || '').includes(k)); } +// 定性设计维度豁免(2026-08-26):名称含"设计合理性/清晰度/合理性"等, +// 考察的是设计思路而非量化结果,不参与 C 档封顶(修复跨队伍环境因素不公平)。 +const QUALITATIVE_DESIGN_KEYS = ['设计合理', '清晰度', '设计思路']; +export function isQualitativeDesignDim(name: string): boolean { + return QUALITATIVE_DESIGN_KEYS.some(k => (name || '').includes(k)); +} + export function classifyVerifiability( dim: { name: string; score: number; maxScore: number }, evidence: { hasBenchmarkEvidence?: boolean; hasEffectEvidence?: boolean; hasBuildEvidence?: boolean } = {} ) { if (evidence.hasBenchmarkEvidence) return { tier: 'A' as const, capped: false, effectiveScore: dim.score, note: '有确定性基准证据(seed-defect benchmark)' }; const strict = isPureGainDim(dim.name); + // 定性设计维度(如"提效设计合理性")不参与 C 档封顶——考察设计思路而非量化结果 + if (isQualitativeDesignDim(dim.name)) return { tier: 'B' as const, capped: false, effectiveScore: dim.score, note: '' }; // 非效果维度直接 B;纯增益维度即使有测试通过证据也不豁免(严格基准制) if (!isEffectDim(dim.name) || (evidence.hasEffectEvidence && !strict)) return { tier: 'B' as const, capped: false, effectiveScore: dim.score, note: '' }; const cap = Math.floor(dim.maxScore * 0.3);