fix(build): validation_issues 分批插入;import/build 报告写序对齐 HTML 先;canonical mismatch evidence 修正

- validation_issues 落库改分批(VALIDATION_ISSUES_CHUNK=1000 行/批循环):
  单 INSERT 占位符上限(pg ~65535 ÷ 6 列)会被超大 issue 集撞穿;
  run 行先落、批次间无序依赖。新增测试:1101 坏链注入 → 全量 1101 行
  落库、收口 VALIDATION_FAILED(非 GATE_INTERNAL)
- import 域报告写序换 HTML 先(与 build 域对齐——json 在则 html 必在)
- gates canonical 指错分支 evidence 改 kind:'canonical-mismatch'
  (mismatch 语义不再误用 missing 键;缺席分支保持 missing:'canonical')
- build.html 尾换行对齐 import 域
- 计划文档 Task 9 补审查遗留清单(helper 迁移/Plan 5 前置门禁面)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-22 13:07:01 +08:00
parent 8f2b9825a6
commit fa27a3a308
5 changed files with 69 additions and 16 deletions
+17 -11
View File
@@ -222,6 +222,9 @@ async function writeAtomically(path: string, content: string): Promise<void> {
}
}
/** validation_issues 分批插入的批大小(单 INSERT 占位符上限防线——1000 行 × 6 列远低于上限) */
const VALIDATION_ISSUES_CHUNK = 1000
/** 既有 job 的复用查询(不动行;三哈希取 build_input_snapshots 行值——不可变权威);无则 null */
async function reuseExisting(
db: BuildDb,
@@ -499,17 +502,20 @@ export async function createBuildJob(db: BuildDb, input: CreateBuildJobInput): P
})
.returning({ id: validationRuns.id })
validationRunId = runRows[0]!.id
// 分批插入(审查 fast-follow):单条 INSERT 参数上限(pg ~65535 占位符 × 列数)
// 会先被超大 issue 集撞穿——每 1000 行一批循环落库;run 行已先落,批次间无序依赖
if (gates.issues.length > 0) {
await db.insert(validationIssues).values(
gates.issues.map((i) => ({
validationRunId,
severity: i.severity,
rule: i.rule,
url: i.url,
evidence: { ...i.evidence, message: i.message },
resolutionStatus: 'open',
})),
)
const rows = gates.issues.map((i) => ({
validationRunId,
severity: i.severity,
rule: i.rule,
url: i.url,
evidence: { ...i.evidence, message: i.message },
resolutionStatus: 'open',
}))
for (let offset = 0; offset < rows.length; offset += VALIDATION_ISSUES_CHUNK) {
await db.insert(validationIssues).values(rows.slice(offset, offset + VALIDATION_ISSUES_CHUNK))
}
}
} catch (err) {
throw new BuildError(
@@ -586,7 +592,7 @@ export async function createBuildJob(db: BuildDb, input: CreateBuildJobInput): P
phase = 'reporting'
try {
await mkdir(reportDir, { recursive: true })
await writeAtomically(reportHtmlPath, renderBuildReportHtml(report))
await writeAtomically(reportHtmlPath, `${renderBuildReportHtml(report)}\n`)
await writeAtomically(reportPath, reportText)
} catch (err) {
throw new BuildError(
+3 -3
View File
@@ -223,7 +223,7 @@ export async function runGates(input: RunGatesInput): Promise<RunGatesResult> {
const urlOf = (rel: string): string => routeByTargetFile.get(rel) ?? `/${rel}`
// ① internal-links:根相对目标全命中(html/asset 文件在场,或**非文件类路由**——
// redirect/gone 落点无产物文件由服务端响应,不算死链;html/asset 行不进兜底:其
// 非文件类路由(redirect/gone/dynamic/unresolved)落点无产物文件由服务端响应,不算死链;html/asset 行不进兜底:其
// 产物缺席时死链与完整性双报告——链接级真相不因路由行在场而掩盖)
const nonFileRoutePaths = new Set(
manifest.routes
@@ -326,7 +326,7 @@ export async function runGates(input: RunGatesInput): Promise<RunGatesResult> {
rule: 'seo-baseline',
url,
message: `SEO 基线:页面 ${url} canonical 指向 ${canonical}(期望 ${expected})`,
evidence: { page: page.rel, missing: 'canonical', expected, actual: canonical },
evidence: { page: page.rel, kind: 'canonical-mismatch', expected, actual: canonical },
})
}
} else if (!canonical.startsWith(origin)) {
@@ -336,7 +336,7 @@ export async function runGates(input: RunGatesInput): Promise<RunGatesResult> {
rule: 'seo-baseline',
url,
message: `SEO 基线:页面 ${url} canonical 非 origin 起头(${canonical})`,
evidence: { page: page.rel, missing: 'canonical', expected: null, actual: canonical },
evidence: { page: page.rel, kind: 'canonical-mismatch', expected: null, actual: canonical },
})
}
if (page.h1Count !== 1) {
@@ -567,8 +567,9 @@ export async function createImportJob(
let reportWritten = true
try {
await mkdir(reportDir, { recursive: true })
await writeAtomically(reportPath, `${JSON.stringify(report, null, 2)}\n`)
// HTML 先写(与 build 域对齐——json 在则 html 必在:JSON 落盘即"双件齐备"信号)
await writeAtomically(htmlPath, `${renderImportReportHtml(report)}\n`)
await writeAtomically(reportPath, `${JSON.stringify(report, null, 2)}\n`)
} catch (err) {
reportWritten = false
failed = {