Files
cms-website/docs/evidence/phase4-preview.md
T
浅念 236f3dc4d3 fix(preview+pipeline): 资产键编码归一——7 张全角名图片预览 404 修复;证据文档叙述修正
spec 复审发现(浏览器可见缺陷):manifest 资产行 exact_key 有 7 例为裸形
(文件名含全角括号/顿号/间隔号)——html/redirect 键源数据侧已编码、asset
键来自文件名本体是裸形;浏览器对裸非 ASCII 一律百分号编码后发请求,预览
按请求字节逐字匹配 → 裸键永不命中 → 两页 3 张图破图(dist 文件在盘;
生产面 nginx 先解码不受影响)。修补 pipeline.toManifestRoute:exact_key
归一为 URL 线形(可打印 ASCII 原样、其余 UTF-8 百分号编码大写 hex),
target_file 仍盘上裸名、DB/渲染快照域不变。

回归三层:build-pipeline 注入裸全角资产行(红验证);preview-server 2c
非 ASCII redirect → Location → 资产终端 200;e2e 断言 ④c(7 条全查
跟随到终端 200)+ ⑦c(manifest 2772 键全 ASCII 不变量)。spec 复审复跑
(新库全链 job e059bf41)23/23 PASS,浏览器实测 3 张原破图全部加载。

证据文档:§5.2 失败计数修正(5×500+2×mojibake=7 例)、§2 ①c 措辞对齐
断言实义(抽前 3 标题)、§5.5 修补记录、计时/产物/job 面更新至复跑值。
验证:vitest 382 + 主题 26 全绿(408);typecheck 过;lint 裸跑零告警。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 15:31:14 +08:00

148 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 阶段 4 真实 hesupipe 端到端验收证据(Plan 4 · Task 8 —— 浏览器时刻)
**verdict:PASS**(断言总表 23 项脚本断言全过 + 浏览器时刻 6 截图无破版无横向溢出;三处验收内修补——预览服务器 Location 头 ASCII 化(§5.2)、CSP 放行内联样式(§5.3)、管线资产键编码归一(§5.5,spec 复审)——均有回归测试锁定;观察项未触发主题补规则,见 §5.1)
| 项 | 值 |
|---|---|
| 日期 | 2026-10-22 |
| 环境 | Windows 11 · Node v24.13.0 · pnpm 10.7.1 · tsx 直吃 `.mjs` 编排脚本(`@sf/*` 经根 tsconfig paths 解析,与 sf CLI 同机制) |
| 数据库 | 本项目 PG(docker `sf-postgres`,postgres:16,localhost:5434)临时库 `sf_phase4_hesupipe`(验后已 DROP,见 §7) |
| 站点/输入 | 复用阶段 3 同源副本(`storage/incoming/` 的 WXR + 媒体 + 侧车;storage/ 不入库)——site `hesupipe-real`、origin=https://hesupipe.com、locale=en |
| 全链 job(spec 复审复跑,权威) | import `c7b4fd9d-61d6-46d5-8022-86259a1675ac`(succeeded_with_issues,57,198.6ms)· publish 192 → upToDate · build `e059bf41-0ba6-4539-a9b4-6eb8a75c1b6e`(succeeded,9,497.9ms)· validation run `6459e0df-dd53-4158-b300-7c610100eeef` PASS(min-gates-v1,p0=0 p2=0)。首轮 job:import `9d82df35…`(51,123.8ms)· build `619c440c…`(11,885.4ms,artifacts/ 本地留档) |
| 产物面 | dist 1029 文件(190 html + 836 媒体 + sitemap.xml + robots.txt + 404.html)· route-manifest 2772 行(html 190 / redirect 1746 / asset 836)· dist SHA-256 `b97ecbda…aa6276` |
| 断言明细 | `docs/evidence/phase4-assertions.json`(脚本逐断言落盘,verdict=PASS 23/23) |
| 截图 | `docs/evidence/screenshots/phase4-{mobile-375,desktop-1440}-{home,product-detail,news-detail}.png` ×6(§3) |
| 编排脚本 | `scripts/e2e-real.mjs`(`chain` 全链+断言 / `serve` 截图会话常驻预览,两个子命令) |
## 1. 全链命令原文(仓库根 `D:\project\cms-one`,Git Bash)
```bash
# 临时库(阶段 3 的 sf_phase3_hesupipe 已在其任务末 DROP;本任务新建同构库)
docker exec sf-postgres psql -U siteforge -d postgres -c "CREATE DATABASE sf_phase4_hesupipe;"
DATABASE_URL="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_hesupipe" pnpm --filter @sf/api db:migrate
# 全链 + 断言(register 副本复用 → import → publish dry-run/confirm → build → 进程内预览
# 断言 ①-⑤⑦⑧⑨ + SQL/产物计数对拍 → docs/evidence/phase4-assertions.json)
DATABASE_URL_TEST="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_hesupipe" \
pnpm exec tsx scripts/e2e-real.mjs chain --site hesupipe-real
# 截图会话(serve:对既有 succeeded build 起常驻预览 127.0.0.1:4571,Playwright MCP 对其取证)
DATABASE_URL_TEST="postgres://siteforge:siteforge_dev@localhost:5434/sf_phase4_hesupipe" \
pnpm exec tsx scripts/e2e-real.mjs serve --build e059bf41-0ba6-4539-a9b4-6eb8a75c1b6e --port 4571
```
互斥纪律(Task 5 备忘):链路执行期间零并发测试/零并发构建(vitest 全套在链路完成后单独跑,见 §5.3);astro 计时探针冻结输入重跑(`SF_BUILD_INPUT` 指向 job input 目录,前后清主题暂存 dist)。
## 2. 断言总表(预期 vs 实测 vs 判定)
预期来源:Plan 4 Task 8 节 + 快线 §8.7。①-⑤⑦⑨ 与计数对拍由 `chain` 子命令逐项断言(明细 JSON 同步落盘);⑥⑧ 在浏览器时刻/计时节单独取证(§3/§4)。
| # | 断言项 | 预期 | 实测 | 判定 |
|---|---|---|---|---|
| ①a | 首页 `/` | HTTP 200 + 真实内容 | 200;站点名在体 + 最新 3 条新闻标题在体(Chile/Tanzania/Burkina Faso 三篇实题) | PASS |
| ①b | 编码内容页 `/how-to-choose-pe-pipe%ef%bc%9f/` | 同上 | 200;标题"Before choosing a PE pipe, be sure to confirm the purpose"在体 | PASS |
| ①c | 产品分类列表 `/product-category/hdpe-pipe-fittings/` | 同上 | 200;term 名在体;term 含 14 产品,断言抽前 3 标题在体 | PASS |
| ①d | 产品详情 `/product/ecological-wooden-door/` | 同上 | 200;标题"ECOLOGICAL WOOD DOORS"在体 + 12 个本地化媒体引用 | PASS |
| ①e | 新闻列表 `/news/` | 同上 | 200;最新 5 条标题在体 | PASS |
| ①f | 新闻详情 `/2023-pipe-market-analysis/` | 同上 | 200;标题 + 日期"2023-11-19 13:32:01"在体 | PASS |
| ② | 图片本地化抽检 10 张 | 根相对引用 + dist 文件存在 + preview 200 + assets.json 在册 | 10/10:全部 `/wp-content/uploads/…` 根相对、dist 文件在、HTTP 200(11KB-1MB 实尺寸)、assets.json 全在册 | PASS |
| ③ | SEO 三件套 ×5 页 | title/description 非空 + canonical === origin+路径 | 5/5 逐页精确(含编码路径 canonical `…/how-to-choose-pe-pipe%ef%bc%9f/` 原样保持) | PASS |
| ④a | 未命中 404 | 404 + noindex | status=404 + `X-Robots-Tag: noindex, nofollow`(自定义 404.html) | PASS |
| ④b | 301 抽检 | `?attachment_id=` 原串 3 + 路径形态 2,逐条 301 + Location 与 manifest 原串一致 | 5/5:全 301、locationMatch 全真(ASCII 位置串逐字节等;非 ASCII 原串的头域派生形见 §5.2) | PASS |
| ④c | 非 ASCII redirect → 资产终端(7 条全查) | 301 → 跟随 Location 编码形 → 终端 200 + 图片 MIME | 7/7(如 `/?attachment_id=2835` → `…School-%EF%BC%881%EF%BC%89.jpg` → 200 image/jpeg);§5.5 修补回归 | PASS |
| ⑤a | sitemap.xml(盘读) | 条目 = manifest indexable html 数 | loc=190 = indexable=190 | PASS |
| ⑤b | robots.txt(盘读) | 存在 + Sitemap 行指向 origin | 存在,`Sitemap: https://hesupipe.com/sitemap.xml` | PASS |
| ⑥ | 浏览器时刻(375/1440 ×3 页) | 无横向溢出 + 视觉无破版 | scrollWidth 360≤375 / 1425≤1440 ×6 组合全过;6 截图样式生效无破版(§3) | PASS |
| ⑦a | html 全量 | 190 行全 200 | ok=190/190(keep-alive 并发 8;content-type=text/html; charset=utf-8) | PASS |
| ⑦b | redirect 全量 | 1746 行逐条 301 + Location 一致 | ok=1746/1746(含 24 条 `?attachment_id=` exact 行按原串发请求;非 ASCII Location 修复后全过,§5.2) | PASS |
| ⑦c | manifest 键线形 | 全 2772 键纯 ASCII(编码线形) | nonAsciiKeys=0(§5.5 修补不变量——裸非 ASCII 键永不匹配浏览器请求形) | PASS |
| ⑧ | 计时对账 | 分钟级预算内 + 与阶段 3 基线同量级 | 首轮 ~64s(import 51.1s + build 11.9s);spec 复审复跑 ~70s(import 57.2s + build 9.5s);幂等复跑 ~15s(§4) | PASS |
| ⑨ | 门禁 | validation_runs PASS | job=succeeded · run PASS · min-gates-v1 · p0=0 p2=0 · issues=[] | PASS |
| 计数 | content_entries | 192(129+48+15) | SQL=192 {news_article:129, product:48, basic_page:15} | PASS |
| 计数 | published 指针 | 192 | SQL=192 | PASS |
| 计数 | routes | 2772(190/1746/836) | SQL 分组逐桶吻合 | PASS |
| 计数 | media ready | 836 | SQL=836 | PASS |
| 计数 | audit content.publish | ≥1 | =1(confirm 写入) | PASS |
| 计数 | 构建产物面 | dist≈1029 | distFileCount=1029 · mediaCopied=836 · sitemapEntries=190 | PASS |
## 3. 浏览器时刻(Playwright MCP × 常驻预览 127.0.0.1:4571)
三页取样(与 ① 断言页一致):首页 `/`、产品详情 `/product/ecological-wooden-door/`(12 媒体引用最多)、新闻详情 `/2023-pipe-market-analysis/`。每页双视口截图(整页)。
| 截图 | 视口 | scrollWidth(≤视口) | 视觉判定 |
|---|---|---|---|
| `screenshots/phase4-mobile-375-home.png` | 375×812 | 360 | 样式生效(标题层级/列表/图片自适应),无破版 |
| `screenshots/phase4-mobile-375-product-detail.png` | 375×812 | 360 | 产品标题 + 木门样图横排自适应缩放,无溢出无破图 |
| `screenshots/phase4-mobile-375-news-detail.png` | 375×812 | 360 | 文章标题/日期/正文排版正常,头图自适应 |
| `screenshots/phase4-desktop-1440-home.png` | 1440×900 | 1425 | 桌面版式正常,新闻列表/产品分节无错位 |
| `screenshots/phase4-desktop-1440-product-detail.png` | 1440×900 | 1425 | 图廊 + 描述正文排版正常 |
| `screenshots/phase4-desktop-1440-news-detail.png` | 1440×900 | 1425 | 标题/日期/正文/头图排版正常 |
- 溢出判据:`document.body.scrollWidth <= window.innerWidth`(六组合全 false 无横向溢出;360/1425 与视口的 15px 差是滚动条槽位)。
- 截图取自首轮 build(`619c440c`)产物;spec 复审复跑(`e059bf41`,§5.5)后浏览器复验过同一会话流程(两页 3 张原破图加载成功,见 §5.5)。
- 控制台仅 1 条噪声:`/favicon.ico` 404(源数据无 favicon,路由表无此行——如期 404,装饰性,不修,§5.4)。
## 4. 耗时分解(vs 基线)
| 阶段 | spec 复审复跑(权威) | 首轮 | 对账 |
|---|---|---|---|
| register(快照副本复用) | 服务内 2,874ms | 1,750ms | 首登 19.3s(阶段 3)的重哈希路径,符合内容寻址预期 |
| import | **57,198.6ms** | **51,123.8ms** | 阶段 3 基线 51,768.0ms——同量级(-1.2% / +10.5%,机噪区间) |
| publish(dry-run + confirm) | 6ms + 17ms(upToDate=192 幂等) | 首跑 192 发布;复跑 87ms + 77ms | — |
| build 总 | **9,497.9ms** | **11,885.4ms** | 分相 marks:snapshot=431 / building=8,880 / validating+report=174(首轮 450 / 11,240 / 183) |
| └ astro 纯构建探针 | 4,865ms | 4,567 / 5,650 / 7,486ms(冻结输入三跑) | building 相与探针差 ≈ 媒体复制 836 文件(~164.5MiB)+ 主题运行器编排,量级自洽 |
| 全量冒烟(190 html + 1746 redirect) | **285ms** | 1,225ms | keep-alive 并发 8,quiet 服务器 |
| 幂等复跑链(同库重提) | — | register 1.8s + import 复用 94ms + publish 164ms + build 复用(不重跑,读原报告) | 断言二次全过(首轮 21/21) |
分钟级预算内:首轮全链 ~64s、spec 复审复跑(新库全量,含 §5.5 修补验证)~70s、幂等复跑 ~15s。
## 5. 观察项兑现(验收内修补与判定)
### 5.1 主题分发观察项:**未触发**
计划预告的三类分发观察项在真实 190 路由集上均未出现,无需补主题规则:
- `/news/` 兄弟特例:无冲突(`/news/` 列表行唯一);
- 嵌套分类前缀:数据集 product_cat 无嵌套(10 个分类全一级);
- 裸 `/product-category/` 前缀:10 个归档页均有 term 且有产物(`hdpe-pipe-fittings` 14 产品为最大者);
- `/products/` 走 basic_page 内容页(阶段 3 既有 exact 冲突事实的延续,generic 模板正常渲染)。
### 5.2 预览服务器修补一:Location 头 ASCII 化(真实缺陷,HTTP 级全量校验显影)
⑦b 首轮 1739/1746(7 例失败):manifest location 含非 ASCII 码位(全角括号`(1)`、间隔号`·`)时,`res.setHeader('Location', 原串)` 触发 Node `ERR_INVALID_CHAR` 崩 500(5×500)或被按 latin1 写出 mojibake(2×301,`·` → `·`)。修补:`apps/preview-server/server.mjs` 新增 `asciiLocation()`——非 ASCII 码位按 UTF-8 百分号编码(与 WP/nginx 形态一致,大写 hex),控制字符同路编码(头注入面封死);manifest 原串保持权威,HTML 迁移页内嵌转义原文。断言侧比较改用同规则镜像(`asciiLocationMirror`),预期口径更新为"manifest 原串的 RFC 9110 头域 ASCII 派生形"。回归测试:`tests/integration/preview-server.test.ts` 测试 2b(非 ASCII redirect 行 → 301 + 编码 Location + 迁移页原文)。修后 1746/1746。
### 5.3 预览服务器修补二:CSP 放行内联样式(浏览器时刻显影,HTTP 级不可见)
截图会话首开时页面在浏览器里裸奔(无样式)——Astro 5 把 scoped 样式内联为 `<style>` 元素,`default-src 'self'` 拦掉全部内联样式(控制台 CSP violation)。HTTP 级断言看不到此面(样式不在头域语义内)。修补:CSP 常量改为 `default-src 'self'; style-src 'self' 'unsafe-inline'`(仅样式面放开;脚本/连接/图片仍走 default-src)。回归测试:测试 6 断言更新(/ 与 404 双路径)。修后 6 截图全部样式生效。
### 5.4 不修项(如实记录)
- `/favicon.ico` 404:源数据无 favicon、路由表无行,预览 404 是正确行为;控制台一条装饰性噪声。
- 集成测试复跑:`pnpm vitest run tests/integration/preview-server.test.ts` 15/15 过(含 2b/2c/6),与链路互斥执行。
### 5.5 管线修补三:资产 exact_key 编码归一(spec 复审发现,浏览器可见缺陷)
**缺陷**:manifest 资产行的 exact_key 有 7 例为裸形(文件名含全角括号/顿号/间隔号,如 `/wp-content/uploads/2024/10/HDPE-carat-pipe-(1).jpg`)——html/redirect 键在源数据侧已是百分号编码形,asset 键来自文件名本体是裸形。浏览器对裸非 ASCII 一律按 WHATWG URL 规则百分号编码(大写 hex)后发请求,预览按请求字节逐字匹配 → 裸键永不命中 → `/hdpe-carat-pipe/` 与 `/product/steel-belt-reinforced-pe-corrugated-pipe/` 共 3 张图破图(dist 文件实际在盘;生产面 nginx 先解码再查文件,不受影响——预览面缺陷)。
**修补**(`apps/api/src/modules/build/pipeline.ts` `wireKeyOf()`):manifest 生成(`toManifestRoute`)对 exact_key 归一为 URL 线形——可打印 ASCII(含既有 %xx 序列的字面 `%`)原样,其余码位按 UTF-8 百分号编码(大写 hex,空格同路编码)——与 §5.2 `asciiLocation` 同族规则、与 URL 解析器/浏览器编码形态逐字节一致(首轮 manifest 2772 键实测仅这 7 键异形)。**归一只动 manifest 键**(服务权威面):`target_file` 仍是盘上裸文件名(文件系统域不解码),route_registry/渲染快照/DB 键保持导入原串。
**回归锁定**(三层):
1. `tests/integration/build-pipeline.test.ts`(主题夹具直跑):注入裸全角名资产行 → manifest exact_key = `%EF%BC%881%EF%BC%89` 编码形 + target_file 裸形 + 全键纯 ASCII 不变量(红验证:修补前该用例失败);
2. `tests/integration/preview-server.test.ts` 测试 2c:非 ASCII redirect → Location 编码形 → 资产行命中 → 终端 200(+裸形直接请求亦命中);
3. `scripts/e2e-real.mjs` 验收断言 ④c(7 条非 ASCII redirect 全查:301 → 跟随 → 终端 200 image/*)+ ⑦c(manifest 2772 键全 ASCII 不变量)。
**复验**(spec 复审复跑,新库全链 job `e059bf41`):断言 23/23 PASS;manifest 7 资产键全部编码形(`nonAsciiKeys=0`);Playwright 浏览器实测两页 3 张原破图全部加载成功(img.complete=true,naturalWidth 750/800/800),控制台仅余 favicon 404 装饰性噪声。
## 6. 差异分析(如实记录,无硬凑)
1. **编码 html 路由仅 1 条(vs 计划假设的编码内容页更多)**:数据集 ~54 条含中文/全角的 permalink 全部是 attachment 降级 redirect 行(媒体文件目标),有内容的编码路由只有 `/how-to-choose-pe-pipe%ef%bc%9f/` 一条——①b 取样即为该条,编码链路(routes.json → dist 文件名 → manifest → 预览匹配逐字节对齐)在其身上完整验证;⑦a 190 行全 200 亦覆盖全部编码形态。
2. **urlRewrite unresolved=317 非死链**:该计数是"body 内 src/href 有可比对路径但未命中 ready 媒体索引"的引用数——主体是**站内页面链接**(`<a href="https://hesupipe.com/…">`,非媒体、本就不在改写索引内,保持原样正确)+ 外站热链(cn-hdpe.com 等 19+)+ 已知缺失媒体 19 与 WP 尺寸变体 2(阶段 3 §5.2/5.3 同源)。门禁 ①internal-links 对根相对链接全量校验 0 死链(p0=0);dist 实测 460 个根相对图片引用全部命中产物文件。
3. **build 复用路径的 durationMs**:复用命中返回的是原 job 行的 durationMs(11,875.1ms,非本次调用墙钟)——服务口径"未重跑、未改行",产物/计数读原 job 报告,断言不受影响。
4. ** astro 探针三次波动 4.6-7.5s**:同冻结输入,机噪区间;取区间而非单点呈报。
## 7. 清理与保留声明
- **serve 会话与临时库**:截图/复验会话(端口 4571)已停;`docker exec sf-postgres psql -U siteforge -d postgres -c "DROP DATABASE sf_phase4_hesupipe WITH (FORCE);"` 验后执行(spec 复审复跑后二次 DROP)。本项目开发库 `siteforge` 全程零写入(所有命令经 `DATABASE_URL_TEST` 指临时库)。
- **保留**:`storage/incoming/` 副本(阶段 3 已声明,约 345MB,gitignore);`artifacts/jobs/619c440c…/` 与 `artifacts/jobs/e059bf41…/`(dist + route-manifest + 报告,artifacts/ 不入库,本地留档);`docs/evidence/screenshots/*.png` ×6 入库。
- cms 源侧(`D:\project\cms`)本任务零访问(输入全用阶段 3 副本)。