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
+46 -1
View File
@@ -288,7 +288,9 @@ describe('最小门禁(纯产物夹具直调)', () => {
await writeFile(join(fx.distDir, '404.html'), pageHtml({ canonical: `${ORIGIN}/404`, links: [] }), 'utf8')
await writeFile(join(fx.distDir, 'index.html'), pageHtml({ canonical: `${ORIGIN}/`, links: BASE_LINKS }), 'utf8')
const r2 = await runFixture(fx)
const canonicalIssue = r2.issues.find((i) => i.rule === 'seo-baseline' && i.evidence.missing === 'canonical')
const canonicalIssue = r2.issues.find(
(i) => i.rule === 'seo-baseline' && i.evidence.kind === 'canonical-mismatch',
)
expect(canonicalIssue).toBeDefined()
expect(canonicalIssue!.severity).toBe('P0')
expect(canonicalIssue!.url).toBe('/about/')
@@ -487,6 +489,49 @@ describe('P0 阻断语义(mini 全链:门禁接入 build 状态机)', () =
})
})
it('issues 分批落库:>1100 条 P0(1101 坏链注入)→ 全量落库不 GATE_INTERNAL、job failed(VALIDATION_FAILED)', { timeout: 300_000 }, async () => {
await withTestDb(async (db) => {
const w = await makeWork('sf-gate-batch')
try {
const { key, siteId } = await seedPublishedSite(db, w)
try {
const artifactsRoot = join(w.work, 'artifacts')
let batchJobId = ''
// 1101 条互异坏链(超 1000 分批界——两批:1000+101)
const badLinks = Array.from({ length: 1101 }, (_, n) => `<a href="/no-such-${n}/">bad-${n}</a>`).join('')
const err: unknown = await createBuildJob(db, {
siteKey: key,
artifactsRoot,
idempotencyKey: `build:test:gate-batch:${rand()}`,
onStatus: (s, jobId) => {
if (s === 'validating') {
batchJobId = jobId
const index = join(artifactsRoot, 'jobs', jobId, 'dist', 'index.html')
writeFileSync(index, `${readFileSync(index, 'utf8')}${badLinks}`, 'utf8')
}
},
}).then(
() => {
throw new Error('不应成功')
},
(e: unknown) => e,
)
// 门禁正常判定收口(分批落库破裂会变 GATE_INTERNAL——此断言即防回归)
expect(err).toBeInstanceOf(BuildError)
expect((err as BuildError).code).toBe('VALIDATION_FAILED')
const rows = (await db.execute(`select count(*)::int as n from validation_issues vi
join validation_runs vr on vi.validation_run_id = vr.id
where vr.build_job_id = '${batchJobId}'`)) as any
expect(Number(rows[0].n)).toBe(1101)
} finally {
await cleanupSiteRows(db, siteId)
}
} finally {
await rm(w.work, { recursive: true, force: true })
}
})
})
it('全过 → validation_runs PASS + 零 issue 行 + job succeeded + 报告 json/html 双落盘(HTML 先写)', { timeout: 300_000 }, async () => {
await withTestDb(async (db) => {
const w = await makeWork('sf-gate-pass')