评审真实性P1落地:全量代码可见性+Mock信号+AI日志审计+定向追踪+功能发现轮
- 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被识别)
This commit is contained in:
@@ -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 禁止把 `[中性证据]` 列为不足
|
||||
|
||||
@@ -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<string>): 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
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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<string>();
|
||||
|
||||
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');
|
||||
}
|
||||
@@ -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<string, 'ts' | 'js' | 'py'> = {
|
||||
'.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<string, RegExp[]> = {
|
||||
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');
|
||||
}
|
||||
@@ -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;
|
||||
// 简化:检查除声明行外是否有 `<pkg>.` 使用
|
||||
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<string>();
|
||||
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'),
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, string> | undefined {
|
||||
return { '功能完整性': '\n' + block };
|
||||
}
|
||||
|
||||
/** 收集 git 全部变更文件 basename 集合(用于 AI 日志一致率审计) */
|
||||
async function collectGitChangedFiles(dir: string): Promise<string[] | null> {
|
||||
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<string, string> {
|
||||
const ctx: Record<string, string> = {};
|
||||
|
||||
// 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<string> {
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user