Files
pure-note/docs/acceptance.md
T
wangairnan 9b0aff6f9f docs: 新增编辑器即时渲染方案、实施与 P3 加固记录
- editor-live-rendering.md(新):Typora 式单编辑面方案——路线选型、装饰层分层设计、
  widget 规格、安全与风险、测试方案、P0–P3 实施拆解;§12 为实施与加固记录:
  两个实测缺陷的根因与修复、3000 行性能实测数据、§12.6 真机输入法人工门槛(阻塞发布)、
  §12.7 编辑区高度
- design.md §8.2:编辑器段落由「分屏实时预览」改为即时渲染表述,链接本方案并标注发布门槛
- decisions.md:D37–D52(装饰器路线、单一 ViewPlugin、行级 reveal、长文档补解析、
  选区端点行采集、reveal 二分、@codemirror/commands 仅为 devDependency、人工门槛、编辑区高度)
- acceptance.md:新增 M5 里程碑与 M5-G 人工门槛行,补 v1.1/v1.2 浏览器复走记录与用例数

文档先行,代码实现见后续提交。
2026-09-11 01:02:36 +08:00

122 lines
11 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.
# 最终验收报告
> 依据:docs/design.md(v1.1);决策记录:docs/decisions.md
> 日期:2026-09-08
## 一、验收结论
**全部通过。** 设计文档定义的功能(§2.1 F1–F8)、安全设计(§9)、测试策略(§13)
与里程碑验收门(§12 M0–M4)均已实现并有自动化测试或实测证据支撑。
`go vet` / `govulncheck` / `npm audit` 干净;全量测试绿;构建冒烟 19 项断言全过;
真实浏览器端到端走查(博客浏览 / 登录 / 编辑器 / 回收站)通过。
## 二、§9.3 上线前安全检查单(逐项)
| 检查项 | 状态 | 证据 |
| --- | --- | --- |
| go vet + govulncheck 干净;npm audit 干净 | ✅ | `go vet ./...` 通过;`govulncheck ./...`:0 个影响本代码的漏洞;`npm audit --omit=dev`:0 vulnerabilities |
| 可见性矩阵自动化用例全绿(列表/详情/标签/RSS/sitemap/图片直链/HTML meta/管理接口) | ✅ | `TestVisibilityMatrix`(表驱动:{匿名,管理员} × {公开,私有,回收站,不存在} × 7 出口 + 图片缓存头分流 + 404 防枚举一致性) |
| 恶意 Markdown 渲染无脚本执行 | ✅ | 前端 vitest 冒烟(script/iframe/事件属性/javascript:/style/SVG 全拦截 + schema 快照);服务端 goldmark 转义 + bluemonday(`TestRenderEscapesDangerousHTML`);CSP `script-src 'self'` 兜底(冒烟断言) |
| 跨站伪造 POST(含 login/logout)被拒;Cookie 属性与 `__Host-` 前缀;刷新后 CSRF 经 /api/me 重取 | ✅ | `TestCSRF`(无 token/错 token/跨源 login/logout/缺 Origin+Referer);`TestLoginFlow`(/api/me 返回 csrf_token);实测 Set-Cookie:`__Host-pn_session=…; Path=/; Max-Age=604800; HttpOnly; Secure; SameSite=Lax` |
| 伪造扩展名/SVG/超限文件被拒 | ✅ | `TestUploadValidation`(魔数不符 415、SVG 拒绝、声明类型白名单、空文件 400、>5MB 413、同字节去重同 id) |
| 登录爆破触发 429;失败日志含 IP | ✅ | `TestLoginRateLimit`(第 6 次失败 429 + Retry-After、正确口令在桶空时同样 429、成功不计费);实测日志 `login_failed ip=127.0.0.1 ip_bucket_left=9 account_bucket_left=4` |
| settings 响应不含口令哈希;PUT 无法写非白名单键;改密通过 | ✅ | `TestSettingsWhitelist`(GET 不含 password/argon2、未知键 400、page_size 生效);`TestPasswordChange`(旧错 401/弱新 400/成功/旧口令失效/当前会话保持) |
| 图片:私有与不存在统一 404;缓存头 immutable vs no-store | ✅ | `TestVisibilityMatrix` 图片出口(公开 immutable / 非公开 no-store / 匿名 404 响应体一致防枚举) |
| 安全头逐项确认(含 HSTS) | ✅ | 冒烟脚本断言 HSTS/X-Frame-Options/nosniff/CSP;全套头由 SecurityHeaders 中间件统一下发(curl 实测) |
| 备份还原演练(停服→删 wal/shm→替换 db→恢复);备份 0600 | ✅ | 演练实测:备份文件 `-rw-------`(0600);删 wal/shm → 换备份 → 起服 → 数据完整可读 |
| 回收站恢复用例;gc dry-run→commit;7 天内孤儿图片不删 | ✅ | `TestTrashLifecycleAndGC` + CLI 实测(dry-run 报告、commit 执行、宽限期保留) |
| user_version 越界启动被拒;--allow-newer 放行 | ✅ | `TestMigrationNewerRejected` + CLI 实测(拒启 exit=1;--allow-newer 后 /api/health 200) |
| 构建冒烟测试通过 | ✅ | `make smoke`(npm ci → vite build → sync-assets → CGO_ENABLED=0 交叉构建 → 起服务 19 项断言:SPA 资源 200+MIME、meta 注入、安全头、私有不可见) |
| Caddy 仅开放 80/443、程序监听 127.0.0.1 | ✅(配置交付) | deploy/Caddyfile + pn.service(`--addr 127.0.0.1:8080 --behind-proxy`)——本机无生产部署环境,按配置文件验收 |
## 三、§13 测试策略落地
| 测试组 | 用例 | 位置 |
| --- | --- | --- |
| 可见性矩阵 | 表驱动全出口(§13 矩阵全覆盖,含图片并集语义与缓存头分流) | internal/httpapi/visibility_test.go |
| 迁移 | 空库→最新幂等;越界拒启;allow-newer 放行;外键级联 | internal/store/store_test.go |
| 认证会话 | 登录成功/失败统一文案、429、改密三态、轮换后 CSRF 不变、/api/me 两态、登出 | internal/httpapi/api_test.go |
| CSRF | 无/错 token 403、跨源(含 login/logout)、缺 Origin+Referer | 同上 |
| 上传 | 魔数/伪造扩展/SVG/超限/空/去重 | 同上 |
| 回收站 | 删→不可见→恢复→可见;图片引用保全;30 天 gc;7 天宽限 | 同上 |
| gc | dry-run 不改数据、过期会话清理 | 同上 |
| slug | 中文 post-YYYYMMDD、同日冲突 -2、回收站占用、手改 409 字段级错误、非法格式 400 | 同上 |
| 元信息注入 | 公开注入笔记 meta;私有/不存在回退站点默认;html/template 转义 | visibility_test.go + webui_test.go |
| 前端 vitest | sanitize schema 快照 + 恶意 Markdown 冒烟 + GFM 回归 + node prop 泄漏检查 | web/tests/markdown.test.tsx(6/6) |
| 即时渲染装饰层 | 语法区间识别 / reveal(含二分等价)/ 安全 / 文首文末边界 / 跨原子区间选区 / 不变量 / 3000 行性能基准 | web/tests/editor-live-preview.test.ts(29)+ editor-boundaries.test.ts(32) |
**回归命令**:`make test`(go vet + go test ./... + vitest)与 `make smoke`。
## 四、里程碑验收门(§12)
| 里程碑 | 验收门 | 状态 |
| --- | --- | --- |
| M0 骨架 | `make build` 产出可运行二进制;start 起服务;/api/health 200 | ✅ |
| M1 公开浏览 | 笔记/标签 API + 博客三页 + 渲染管线 + RSS/sitemap + meta 注入;公开侧可见性用例;构建冒烟 | ✅ |
| M2 管理端 | 登录/会话/CSRF + CRUD + 回收站 + 图片上传 + 编辑器 + 改密;完整矩阵 + 各组用例随功能交付 | ✅ |
| M3 安全加固 | §9.3 全项通过(见上表);CSP nonce 硬化按设计留 M3+ 待办 | ✅ |
| M4 运维 | systemd ×3 + Caddyfile + 备份恢复演练真还原 | ✅(压测 hey 未做:单机个人规模非验收必需,工具未安装;`SetMaxOpenConns(1)` 串行模型按设计实现) |
| M5 编辑器即时渲染(P0–P3) | 编辑态=阅读态排版(`--md-*` 契约)、全语法即时渲染、光标行展开/失焦整篇、任务框回写源文本、源码模式逃生舱;边界与选区回归(`web/tests/editor-boundaries.test.ts` 32 条);3000 行性能基准达成 §7.4 的 <16ms 目标 | ✅ 代码与自动化(实测数据见 [editor-live-rendering.md](editor-live-rendering.md) §12.5) |
| **M5-G 人工门槛:真机输入法** | 系统拼音 / 搜狗拼音按 §12.6 的 I1–I8 用例走查并签字 | ⛔ **未执行 —— 阻塞发布** |
## 五、真实浏览器走查(补充验收)
以内置浏览器对 `make build` 产物做黑盒走查,全部通过:
1. **博客首页**:暗色主题、站点标题、置顶排序、标签云计数、页脚 RSS/Sitemap;
2. **详情页**:面包屑、标题/日期/标签、GFM 表格与任务列表(disabled checkbox)渲染、上一篇/下一篇;
3. **登录**:错误口令提示、成功跳转 /admin;
4. **管理列表**:公/私徽标、slug/时间/标签、状态切换/编辑/删除;
5. **编辑器**:CodeMirror 输入、~~分屏实时预览~~(v1.1 起改为**即时渲染**,方案见 [editor-live-rendering.md](editor-live-rendering.md);源码模式为逃生舱)、保存后自动生成 slug("go")、摘要自动截取、URL 迁移到 /notes/3/edit;
6. **回收站**:删除确认弹窗 → 列表移除 → 回收站出现 → 恢复 → 公开页重新可见(标签页标题即 meta 注入验证)。
### v1.1 编辑器即时渲染复走(2026-09-11)
在 `pn start --dev` + `vite dev` 上对内联渲染逐项复走:标题/粗体/斜体/删除线/行内代码/链接/嵌套引用/
三级无序圆点/有序序号保留/任务框/图片(原尺寸+圆角)/水平线/围栏语言标签与底色/表格等宽淡化均按预期;
光标行展开源码、失焦整篇渲染;点击任务框改写源文本并自动保存;源码模式反复切换无残留。
细节与剩余人工项见 [editor-live-rendering.md](editor-live-rendering.md) §12。
### v1.2 P3 加固复走(2026-09-11)
- **边界**:文档以标题/任务项/围栏/水平线开头、以水平线/图片/围栏/任务结尾均正确渲染;整篇只有一条水平线可用;
文首连续退格不损坏内容;文末追加内容后装饰正确重建。
- **跨原子区间选区**:3000 行文档 `Cmd+A` + 退格清空、`Cmd+Z` 完整恢复;跨隐藏标记的选区替换不残留标记碎片;
只选中可见内容时标记完整保留。
- **性能**:3000 行 / 15.6 万字符文档,视口装饰采集中位数 0.26ms、全篇最坏 5.0ms、最坏选区(Cmd+A)0.98ms、
真实输入同步耗时中位数 0.1ms、冷启动直跳文末最大帧间隔 7.1ms 且零 long task(详见方案 §12.5.2)。
- **修复**:长文档惰性解析导致未解析区间显示裸 Markdown(已修,方案 §12.5.1 P3-1);
`Cmd+A` 全文档装饰构建与线性 reveal 扫描(已修,P3-2)。
- **编辑区高度**(2026-09-11,用户要求):编辑卡默认吃满视口剩余高度、长文改为编辑器内部滚动。
1280×720 下空文档与 3000 行长文的编辑卡均为 610px;长文页面保持一屏(此前 8.4 万像素),
`.cm-scroller` 内部可滚;窄屏 ≤900px 回落内容高度。方案 §12.7。
### ⛔ 未通过的人工门槛(阻塞发布)
真机输入法回归(系统拼音 / 搜狗拼音)**尚未执行**,用例、环境要求与签字要求见
[editor-live-rendering.md](editor-live-rendering.md) §12.6。**在签字通过前不得发布**。
## 六、交付物清单
```
pn(单二进制,CGO_ENABLED=0,go:embed 前端产物)
├── cmd/pn/ start / init / passwd / backup / gc / version
├── internal/{config,store,auth,markdown,middleware,httpapi,webui}
├── web/ React 19 + Vite 8 + Tailwind 4(vitest 71 用例)+ editor/ 即时渲染装饰层
├── deploy/ pn.service、pn-maint.{service,timer}、Caddyfile
├── scripts/smoke.sh 构建冒烟(make smoke)
├── Makefile / README.md / .gitignore
└── docs/{design.md, review-round1.md, decisions.md, acceptance.md}
```
依赖面:Go 直接依赖 6 个(预算 ≤6);npm 生产依赖与设计一致(运行时零新增),0 漏洞;
新增 `@codemirror/commands` 为 devDependency(本就在依赖树中,用于边界回归真实执行编辑命令)。
锁定文件 go.sum / package-lock.json 入库。
## 七、遗留事项(不阻塞验收)
1. CSP `style-src` nonce 硬化 —— 设计明示 M3+ 待办(§9.2/§14),v1 保留 'unsafe-inline';
2. hey 压测 —— M4 可选项,需生产规格环境;
3. `vite dev` 开发态标签页标题显示模板占位符(D23,仅开发态外观);
4. shadcn CLI 未使用,按「复制式组件」理念手写等价最小组件集(D22)。