Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions src/checks/review-gates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,25 +86,45 @@ export function evalSpecReviewsRetention(
}
}

// R<n> 审查文扫描(DEF-003 T4 真值源 · 2.3-W4 升级为最新轮返回):扫描 docs/harness/reviews 与
// reviews/ 双路径,文件名口径 task_<slug>_audit_R<n>_*.md(slug 去 _v<n> 版本后缀);
// 返回最高 R 轮文件(终轮结论为真值 · R2 通过覆盖 R1 退回);无命中 → null。
// R<n> 审查文扫描(DEF-003 T4 真值源 · 2.3-W4 升级为最新轮返回):扫描 docs/harness/reviews、
// reviews/ 与 docs/audits/task-review/ 三址(址B = verify-upstream-dual-address · 下游仓
// CONVENTION_review_gate_dual_address_v1_zh.md 双址规约的上游根治面)。
// 命名口径两种(按址匹配 · 不串址):
// 址A(docs/harness/reviews · reviews/):task_<slug>_audit_R<n>_*.md(slug 含 task_ 前缀 · underscore 形)
// 址B(docs/audits/task-review/):YYYYMMDD_<slug>_r<n>.md(slug 无 task_ 前缀 · dash 形 · 小写 _r<n>)
// slug 去 _v<n> 版本后缀;址B 匹配过 normalizeSlug 归一(dash ↔ underscore)并去 task_ 前缀。
// 返回最高 R 轮文件(终轮结论为真值 · R2 通过覆盖 R1 退回 · 跨址合并同一语义);无命中 → null。
export function findLatestReview(
target: string,
taskFile: string,
): { path: string; name: string; round: number } | null {
const dirs = [path.join(target, 'docs/harness/reviews'), path.join(target, 'reviews')]
// 命名口径按址绑定(不串址):址A 两目录只认 task_*_audit_R<n>_* · 址B 只认 YYYYMMDD_<slug>_r<n>
const dirs: { dir: string; addrB: boolean }[] = [
{ dir: path.join(target, 'docs/harness/reviews'), addrB: false },
{ dir: path.join(target, 'reviews'), addrB: false },
{ dir: path.join(target, 'docs/audits/task-review'), addrB: true },
]
const stripVer = (s: string) => s.replace(/_v\d+$/, '')
const base = stripVer(path.basename(taskFile, '.md'))
const RE = /^(task_.+?)_audit_R(\d+)_.*\.md$/i
const RE_ADDR_B = /^\d{8}_(.+?)_r(\d+)\.md$/i
const slugNormB = normalizeSlug(stripVer(base.replace(/^task_/i, '')))
let best: { path: string; name: string; round: number } | null = null
for (const reviewsDir of dirs) {
for (const { dir: reviewsDir, addrB } of dirs) {
if (!existsSync(reviewsDir)) continue
for (const name of readdirSync(reviewsDir)) {
const m = name.match(RE)
if (!m) continue
if (stripVer(m[1]!) !== base) continue // 捕获组必参与(E5 收窄)
const round = parseInt(m[2]!, 10)
let round: number | null = null
if (!addrB) {
const m = name.match(RE)
if (!m) continue
if (stripVer(m[1]!) !== base) continue // 捕获组必参与(E5 收窄)
round = parseInt(m[2]!, 10)
} else {
const mb = name.match(RE_ADDR_B)
if (!mb) continue
if (normalizeSlug(stripVer(mb[1]!)) !== slugNormB) continue // 捕获组必参与(E5 收窄同口径)
round = parseInt(mb[2]!, 10)
}
if (!best || round > best.round || (round === best.round && name > best.name)) {
best = { path: path.join(reviewsDir, name), name, round }
}
Expand Down
52 changes: 52 additions & 0 deletions test/cli-verify-review.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import os from 'node:os'
import path from 'node:path'
import { describe, it } from 'node:test'
import { fileURLToPath } from 'node:url'
import { findLatestReview } from '../src/checks/review-gates.ts'

const KIT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const CLI_TS = path.join(KIT, 'src', 'cli.ts')
Expand Down Expand Up @@ -175,3 +176,54 @@ describe('DEF-003 T4 · verify 查 R<n> 审查文存在性', { concurrency: 1 },
})
})
})

// verify-upstream-dual-address(ops-desk-api 仓 CONVENTION_review_gate_dual_address_v1_zh.md 上游根治):
// findLatestReview 扩第三扫描址 docs/audits/task-review/(址B)+ 第二命名口径 YYYYMMDD_<slug>_r<n>.md。
// 址B slug 为 dash 形(hitl-auto-resume-claim-race)· task slug 为 underscore 形(task_hitl_auto_resume_claim_race)
// → 匹配须过 normalizeSlug 归一且去 task_ 前缀;跨址合并沿用「最高 R 轮取最新」语义。
// 红→绿钉死:修复前址B 完全不入扫描面(址B 单落 verify 误报 missing R<n> review BLOCKED · 误报两起在案)。
describe('verify-upstream-dual-address · 址B docs/audits/task-review/ 扫描与命名口径', { concurrency: 1 }, () => {
const REVIEW_BODY = '# R1 fixture\n\n## 结论\n\nPASS · 零内容阻塞(fixture)\n\n审查结论:fixture 范围与验收全项合规,无阻塞遗留,准予关账。\n'

it('址B 单落(不落址A)→ VERIFY: PASS · exit 0(命名 YYYYMMDD_<slug>_r<n>.md · dash slug)', async () => {
await withTemp(async (dir) => {
await seedTask(dir)
await writeRel(dir, 'docs/audits/task-review/20260820_vr-ok_r1.md', REVIEW_BODY)
const r = runCli(['verify', '--task', TASK_REL, '--target', dir])
assert.equal(r.status, 0, r.combined)
assert.match(r.combined, /VERIFY: PASS/)
})
})

it('址B 落址A 式命名(task_*_audit_R1_*)不认 → 仍 BLOCKED(址B 命名口径不串址)', async () => {
await withTemp(async (dir) => {
await seedTask(dir)
await writeRel(dir, 'docs/audits/task-review/task_vr_ok_audit_R1_2026-08-20.md', REVIEW_BODY)
const r = runCli(['verify', '--task', TASK_REL, '--target', dir])
assert.equal(r.status, 2, r.combined)
assert.match(r.combined, /VERIFY: BLOCKED · missing R<n> review/)
})
})

it('findLatestReview 跨址合并取最高 R 轮:址A R1 + 址B r2 → round=2 命中址B 档', async () => {
await withTemp(async (dir) => {
await seedTask(dir)
await writeRel(dir, REVIEW_REL, REVIEW_BODY)
await writeRel(dir, 'docs/audits/task-review/20260821_vr-ok_r2.md', REVIEW_BODY)
const latest = findLatestReview(dir, path.join(dir, TASK_REL))
assert.ok(latest, '址A+址B 双落须有命中')
assert.equal(latest.round, 2)
assert.match(latest.name, /^20260821_vr-ok_r2\.md$/)
})
})

it('findLatestReview 址B slug 去 _v<n> 版本后缀生效(task 文件 task_vr_suff_v3.md ↔ 址B vr-suff_r1)', async () => {
await withTemp(async (dir) => {
await seedTask(dir, 'docs/tasks/active/task_vr_suff_v3.md', 'vr_suff')
await writeRel(dir, 'docs/audits/task-review/20260820_vr-suff_r1.md', REVIEW_BODY)
const latest = findLatestReview(dir, path.join(dir, 'docs/tasks/active/task_vr_suff_v3.md'))
assert.ok(latest, '址B 档须命中 _v<n> 后缀 task')
assert.equal(latest.round, 1)
})
})
})
Loading