planP0E1approved2026-09-25

Perfowl 首页改版恢复与生产部署隔离方案

Lead AI 决策,2026-09-25。 本文是 Implementation AI 的唯一实施依据;先修部署边界,再恢复首页视觉,不以一次 HTTP 200 或一次 Cloudflare 上传代替页面内容验收。本批不改 MacApp 产品、指标公式、原始 Session、历史归档及其他 AI 未提交文件。Owner 已规定 Lead 完成方案后直接交 Implementation 执行,实施完成再由 Lead 独立验收。

0 · 已核实的单因链与证据等级

  1. 改版快照 18d818263f5a837a9b855d6341792bf2c903b98b 只在 wip/site-home-20260924,其父提交为 Build74 基线 1ec9462;两远程该 WIP ref 均保存同一 SHA。18d8182 不是当前 main 的祖先。其首页入口是 <ImmersiveHome />,含 653 行沉浸组件、Three.js、品牌 SVG 与五章页面;当前 main 无该组件,首页仍为 VitePress layout: home。因此素材没有丢失,代码没有从 main 被 git revert;改版从未合入 main。
  2. 本机 .git/hooks/pre-push 只检测推送范围含 site/ 或 docs/,不区分分支,并在实际 Git 推送完成之前从当前工作树运行 npm run deploy。受版本控制的 site/package.json 将 deploy 固定为 wrangler pages deploy ... --branch main --commit-dirty=true。因此 WIP 分支也能发布生产,后续 main 文档推送又能用旧首页覆盖生产;失败的 Git push 甚至可能留下已发布但远程 ref 未落地的站点。该机制由本地源码直接证明(E1)。
  3. 本地远程 reflog:WIP ref 于 2026-09-24 00:59 更新;main 于 07:04 推送 4c1e424,该提交触及 site/devlog/index.md、其 main 首页无 ImmersiveHome,满足上述 hook 的再次部署条件。07:04 是可定位的最早覆盖触发点,不把缺失的云端旧部署日志包装成精确首次回滚时刻。 当前 Cloudflare 生产列表来源为后续 main 提交,线上首页为旧布局。
  4. 共享 main 当前有四项他方 WIP,含一个已修改 devlog、两个未跟踪 devlog 与 MacApp/Tests/1.trace/;全部受保护。仓库外或独立干净 worktree 才能作为可复现的生产构建输入。

1 · Lead 决策与实施顺序

分为 G0 部署门禁 与 G1 视觉恢复 两个独立批次,每批各一个窄范围提交;Implementation 先只交付 G0,Lead 独立验收 G0 PASS 后才发 G1 执行指令。G0 未通过,不允许 G1 生产发布。禁止直接 git merge/cherry-pick 整个 WIP 分支、直接从 WIP 运行现有 npm run deploy、将其代码未经核验地覆盖当前 site/,也禁止 reset/clean/git add -A 清理共享工作区。

G0 · 将生产发布改为“推送后、双远程一致、干净提交、一次部署”

  1. 修改受版本控制的 site/package.json deploy 入口,接入仓库内的生产发布守卫(建议 site/scripts/deploy-production.*)。任何 build、archive 同步或 Cloudflare 调用之前,守卫必须 fail-closed 检查:目标提交 SHA 是当前 HEAD;git status --porcelain 为空(忽略受 Git 忽略的依赖与构建物);实时 git ls-remote origin refs/heads/main 和 git ls-remote github refs/heads/main 均精确等于该 SHA;本次目标明确为 production/main。缺值、超时、网络失败、ref 不一致、工作树不干净一律不部署。不得仅信任本地 remote-tracking ref、commit message、Webhook 或 --commit-dirty=true。
  2. 守卫通过后只构建一次、发布一次:wrangler pages deploy 显式带 --branch main --commit-hash <完整 SHA> --commit-dirty=false。构建前后的 SHA 与干净状态再次核对;产物来自该提交的隔离干净 worktree,不是含四项 WIP 的共享目录。发布返回的 deployment id/URL、Cloudflare Source SHA、构建摘要和首页内容校验写入证据。
  3. 现有本机 pre-push hook 是未入库的本机状态,不能作为跨环境唯一守卫;受控 npm run deploy 必须自身安全。旧 hook 在推送 G0/G1 新提交时即使自动调用 deploy,也应因远端 ref 尚未前进而在构建前拒绝生产发布;hook 对 Git 推送可继续非阻断。正式发布只能发生于双远程成功之后,由明确的交付步骤从隔离 worktree 触发一次。后续可另行替换本机 hook 为纯提示,但不得让其继续绕过受控入口。
  4. 为 WIP 预览设置与生产分离的命令/流程:非 main 来源只能部署 Cloudflare Preview,--branch 传明确的非 main 预览分支,且必须显示 Preview 环境;不能在预览路径上复用固定 --branch main。不要把 WIP 预览成功记为生产验收。
  5. 守卫测试至少覆盖:非 main/共享脏树、仅一个远程前进、两个远程均未前进、ls-remote 错误/超时、两个远程同 SHA 且干净、预览分支误设 main。用桩验证“拒绝时构建/上传调用次数均为 0;放行时生产上传恰 1 次、SHA 参数精确”。G0 推送后核实 Cloudflare 生产 Source 未因 pre-push 再次变化。

G1 · 有选择地恢复首页,不回退当前知识库与文档

  1. 以当前 main 为起点,只从 18d8182 选择首页必需的视觉资产:ImmersiveHome.vue、相关首页 CSS、site/index.md 的组件入口、品牌 SVG、Three.js 锁定依赖,以及 theme/index.ts 中最小注册。保留当前 main 的导航、知识库、术语提示、归档路由及 devlog;config.ts 只作确有必要的逐项合并。根目录 .miora 与生成图不是站点必需发布物,不整批复制。
  2. WIP 是 in-progress 视觉快照,不是获验收的能力文案。它只有五章,不能宣称已实现策略草案中的 S6/S7;是否补证据分级区应以本批可验证的静态内容为准,不扩成内容营销重构。逐条审查「精准定位」「直达根因」「干净、可对比」「一键导出结论」及能耗/温度等陈述,按当前知识库和 E2/E4 门禁改成有来源、有限定的表述。WIP 的 FPS/CPU/Network 动态数字与曲线必须明确标“示意数据”,不能让访客误认真机实测。当前 /download/ 明记公开下载 blocked,首页 CTA 应如实写“查看发布状态/了解产品”,不得写成可立即下载。
  3. 保留 WIP 的沉浸视觉意图,同时提供无 WebGL、WebGL context loss、禁用 JS/加载失败、移动窄屏和 prefers-reduced-motion 下可读可导航的静态回退;页面标题、章节、CTA 必须可被 SSR/键盘/读屏获得。进入/离开首页的深色主题、动画循环、监听器与 GPU 资源需成对回收;后台标签不持续渲染。不得引入外部 CDN、竞品资产或隐私标识。
  4. 效率预算:守卫本身除两次远程 ref 查询外为 O(1),每次发布只做一次 VitePress build/一次上传,不因双远程推送重复构建;远程查询分别设有界超时。首页与当前生产页、原 WIP 隔离预览按同环境交错至少 3 次测首屏文本可见、LCP、JS 转移体积、主线程长任务、动画帧耗时、CPU/内存及移动端最差场景;目标为首屏文本在 WebGL 前可见、桌面 LCP ≤2.5 s、模拟移动 Fast 4G LCP ≤4 s、可见动画 p95 帧间隔 ≤33 ms。若预算失败,优先关闭重型效果/降级而非放宽门槛;报告真实差值与设备/节流条件,不用构建通过冒充运行时性能。

2 · 验收矩阵与发布/回滚

门必须交付的独立可核证证据
G0 隔离上述 fail-closed 测试、双远程实时 SHA、脏树拒绝、旧 hook 前推送调用不能部署、Preview 永不占 Production、Cloudflare Source 与提交一致
G1 内容当前 main 的 knowledge/devlog/archive 路由与旧归档保留;首页独有 SSR 文本/视觉标识、示意数据标签、真实 CTA、能力陈述逐项来源;0 死链与 66 项知识门禁
G1 交互桌面/移动截图及短录屏;滚动五章、导航点、键盘、禁动效、WebGL 不可用、页面离开后无动画/监听残留;真实浏览器检查,不仅比 HTML 字符串
G1 性能相同环境三轮交错前后表;LCP/资源字节/CPU/RSS/动画 p95 与最差档;预算内才允许生产
发布G0 先完成;G1 代码窄范围提交并双远程同 SHA;从该 SHA 的干净隔离 worktree 一次生产部署;线上 / 的新组件/文案/示意标签和非首页关键路由逐内容核验;记录 deployment URL、Source SHA 与可复用的旧生产快照

G0 与 G1 都要把实际修改、方案偏差、命令、证据等级、遗留项追加到本 devlog,并按 AGENTS.md C2/C3 新增完成批次归档 HTML/时间线及门禁联动;不得回改旧归档。站点构建与浏览器验收是 E2/E3,生产真实访问与交互另列 E4-Web,不与 iOS 真机 E4 混称。Lead 在收到交付后独立判定 PASS/FAIL;Implementation 不自行宣布网站恢复完成。

本方案提交时旧 pre-push 仍不安全:为避免只发布方案却再次从共享脏树覆盖生产,Lead 对本方案的双远程 Git 推送明确跳过该本机 hook,已在本地运行构建;方案的公开站点页待 G0 安全发布入口落地后由 Implementation 一并核验。该一次性 containment 不是今后可绕过 G0 的通用发布方式。

若新生产页面有严重错误,保留 Git 历史,从发布前记录的已验证旧生产 SHA 创建干净隔离构建并经同一受控入口重新发布旧页面;不得 reset --hard、强推或删除 WIP 分支。回滚后仍记录旧/新 deployment URL、触发条件和复核结果。

Implementation Update · 2026-09-25 · G0(生产发布受控入口)

关联方案:本文 §1 G0(Lead 决策提交 4ff2d2a,status: approved)。本批只实现 G0,不含 G1 视觉恢复,未部署生产、未改首页、未合并/移植 wip/site-home-20260924、未触碰共享区其他 AI 的 4 项未提交文件。

实际修改范围与关键文件(4 个入库路径,1 个新增归档):

文件变更
site/scripts/deploy-production.py新增。fail-closed 生产发布守卫 + 唯一一次构建/上传;CLI:--sha --remotes --timeout --dry-run --preview [BRANCH] --project --json
site/scripts/tests/test_deploy_production.py新增。26 项拒绝/放行测试(决策表 10 + 端到端 16),脚本化 runner 断言调用次数与参数
site/package.jsondeploy 由 npm run build && npx wrangler pages deploy … --branch main --commit-dirty=true 改为 python3 scripts/deploy-production.py;新增 deploy:dry-run、deploy:preview
site/devlog/index.mdC2 时间线顶部节点 + C3「八阶段总计划」「未关门门禁」两表联动
docs/architecture/perfowl-homepage-restoration-and-production-deploy-gate-2026-09-25.html本 devlog 的完成批次归档快照(C2)

最终实现方式:

  1. fail-closed 前置校验(在任何 build / 归档同步 / Cloudflare 调用之前完成,纯函数 evaluate_gate() 决策、零副作用):

① 目标 SHA 为完整 40-hex 且等于当前 HEAD; ② git status --porcelain 为空(Git 自身排除被忽略的依赖与构建产物,故含其他 AI 未提交文件的共享工作树必然拒绝); ③ 实时 git ls-remote origin refs/heads/main 与 git ls-remote github refs/heads/main 各自精确等于该 SHA(只解析恰好一行 <sha>\trefs/heads/main;多行、非 40-hex、空输出一律计入不可解析); ④ 目标明确为 production/main。 缺值、超时(默认每远程 20 s 有界)、网络失败、ref 不一致、工作树不干净 一律拒绝,且拒绝路径不执行任何构建或上传。不使用本地 remote-tracking ref、commit message、Webhook 或 --commit-dirty 作为判据。

  1. 放行后只构建一次、只上传一次:npm run build 一次 → 复核 HEAD 未变且跟踪文件未被改动 → npx wrangler pages deploy .vitepress/dist --project-name perfowl --branch main --commit-hash <完整 SHA> --commit-dirty=false 一次。产物来自隔离干净 worktree(由守卫的干净判据强制)。
  2. Preview 与生产分离:--preview(可带分支名,缺省取当前分支)必须解析出非 main 分支,否则拒绝;Preview 路径不做双远程 SHA 等值检查(允许预览未合入的 WIP),但保留「目标 SHA == HEAD」与「干净工作树」。输出显式标注 environment=Preview,永不占 Production。
  3. 效率(AGENTS 效率门禁):守卫除两次远程 ref 查询外为 O(1)(1 次 rev-parse + 1 次 status + 2 次 ls-remote + 1 次 exists),无仓库规模相关的遍历;远程查询各有界超时;每次发布只 1 次 build、1 次 upload,不因双远程推送重复构建。守卫自报分阶段耗时(preflight / remote_refs / build / upload)。

相对原方案的偏差及原因:

验证命令与证据等级(E2 构建/测试 + E3 真实 Git 与网络环境;无 E4,也不与 iOS 真机 E4 混称):

python3 site/scripts/tests/test_deploy_production.py          # 26 tests → OK
cd site && python3 scripts/deploy-production.py --dry-run     # 共享脏树 → REJECT(2),build=0 upload=0
cd <该提交的隔离干净 worktree>/site && python3 scripts/deploy-production.py --dry-run
                                                              # 干净 + 双远程同 SHA → GATE PASS(0),build=0 upload=0
python3 site/scripts/metric-knowledge-gate.py                 # 66 contracts PASS
env -u CODEBUDDY_SAFE_DELETE_BULK_STATE_DIR -u CODEBUDDY_TOOL_CALL_ID npm run build   # exit 0

旧 hook 的 containment 证明(E3):本机 pre-push 在推送完成之前执行 cd $REPO_ROOT/site && npm run deploy,此刻远端 refs/heads/main 尚未前进 ⇒ 守卫第③项必然不满足;在共享脏树上第②项也必然不满足。两者都使守卫在构建之前返回 2,因此旧 hook 即使触发也不产生构建与上传。本批双远程推送另行采取明确防误部署措施(推送时不运行本机 hook),并在推送后对照基线复校 Cloudflare 生产 Source 未变化。

当前状态:implemented · gate-open。G0 是否 PASS、是否进入 G1,由 Lead 独立验收后决定;Implementation 不自行宣布 G0 PASS。

遗留项 / 后续门禁:

Lead Independent G0 Audit · 2026-09-25 · REPAIR REQUIRED

Lead 对 c7c54fa5c6bbe4d42b8eb41d3b2eb6579b6e77f4 只读复核:提交仅六个本批文件、双远程 main 同 SHA,26 项原测试全部通过,归档 HTML 严格标签嵌套 0/0;本机 Cloudflare Production 最新 Source 仍是 ea2a351,没有本批部署。这些交付与防误推送事实成立,但 G0 的“非 main 不占生产 / 两个远程不可绕过 / 构建后仍来源一致”门未通过,因此 G1 不放行。

  1. P0:双远程硬门可由公开 CLI 选项关闭。 deploy-production.py:397,413 的 --remotes 被解析为任意长度元组;生产 evaluate_gate() 仅遍历该元组。Lead 直接执行纯函数对抗探针:remotes=() 与仅 ('origin',) 均返回空拒绝原因,即0 个或 1 个真实远程也能放行生产。26 项测试没有空/单远程反例。生产检查必须固定且精确为 origin 与 github,不得由生产调用者缩减;测试注入只能在不可作为正式 CLI 配置的内部桩中进行。
  2. P0:旧 WIP 分支的真实推送入口仍会绕过新脚本。 两远程 wip/site-home-20260924 仍指向 18d8182,该提交的 site/package.json 保留旧命令 npm run build && wrangler ... --branch main --commit-dirty=true。本机 .git/hooks/pre-push 仍不区分分支,遇该分支的 site/ 变更会从WIP 工作树调用该旧命令;新守卫只存在于当前 main,不能覆盖旧 Git 快照。故「旧 hook 现在只调用受控入口」「Preview 永不占 Production」只对当前 main 工作树成立,不能推广到 WIP。必须先使本机旧 hook 不再部署任意分支(备份后改为纯提示/无发布副作用),并提供 WIP 分支推送的无生产调用反例;Preview 必须由受控流程显式选择非 main。其他主机旧 hook 的状态若未核实,保留为环境边界,不宣称全域保证。
  3. P0:构建完成到上传间存在可复现的过期来源竞态。 代码只在 build 之前调用两次 ls-remote;build 后仅重查 HEAD 与跟踪文件,未重查两个远程。Lead 用原测试 FakeRunner 的对抗变体在 build 返回时将两远程从目标 SHA 切到另一 SHA,实际结果仍是 exit=0 / upload=1,可把旧页面重新发为 Production。上传前须再次实时核对两远程与目标 SHA;不一致时 upload=0。上传后的最新 Production Source 与双远程 SHA 仍须核查,不能把一次上传成功当作最终上线正确。
  4. P1:构建后脏树检查缩到 tracked-only,且构建/上传进程无超时。 --untracked-files=no 会忽略构建期间新出现的非生成型 site/ 文件;报告中「未放宽发布前置」因此不够准确。应只豁免经证实的生成目录,保留其他未跟踪路径的拒绝,并补“build 时生成新 site/rogue.md ⇒ upload=0”fixture。npm run build/npx wrangler 当前 timeout=None,需按实测 95 秒构建与网络上传设置明确且有余量的有界预算,超限不得上传/不得自称成功。

唯一 G0-R1 最小返工合同

Implementation Update · 2026-09-25 · G0-R1(双远程硬门、上传前重查、有界预算、旧 hook 处置)

关联合同:上文「Lead Independent G0 Audit · 2026-09-25 · REPAIR REQUIRED」→「唯一 G0-R1 最小返工合同」(Lead 基线 6ebcd30)。本批只修 G0:未恢复首页、未 merge/cherry-pick WIP、未执行任何真实 Cloudflare 上传、未触碰他方 4 项未提交文件、未进入 G1。G0 PASS 由 Lead 独立裁定。

实际修改范围与关键文件

路径变更
site/scripts/deploy-production.py关键路径重写:删除 --remotes;生产远程集合固定为模块常量;checkout 状态派生生产分支;构建后(上传前)二次实时双远程核查;生成物白名单取代 tracked-only;build/upload/verify 三类有界超时;上传后 Production Source 核查
site/scripts/tests/test_deploy_production.py26 → 58 项(原 26 项语义全部保留,新增 32 项对抗 fixture)
.gitignore新增 site/.vitepress/.temp/、site/.wrangler/
site/devlog/index.mdC2/C3 联动:时间线节点链接指向本归档快照 + 「八阶段总计划」P7 行 + 「未关门门禁」G0 行补 G0-R1 事实
.git/hooks/pre-push本机未入库状态:原始 1683 B / sha256 d5fdf780…774d 备份为 pre-push.legacy-autodeploy-2026-09-25.bak,替换为无发布副作用的纯提示 hook(971 B)
docs/architecture/…-deploy-gate-2026-09-25-g0r1.html完成批次归档快照(C2,新文件;旧快照不回改)

逐条回应 Lead 阻断项

  1. P0-1 双远程硬门不可由 CLI 缩减。 --remotes 已从 CLI 删除。生产远程集合为模块常量 PRODUCTION_REMOTES = ("origin", "github");evaluate_gate(gate) 不再接受远程参数,并额外拒绝任何 remote_heads 键集不等于该常量的 gate(生产必查的集合是代码事实,不是调用者输入)。对抗探针(对真实模块):旧调用形状 evaluate_gate(x, ()) → TypeError;remote_heads={} / 仅 origin / 仅 github / 多一个远程 → 全部 allowed=False 且理由含 "cannot be reduced by a caller";--remotes origin → argparse SystemExit(2) unrecognized arguments。测试注入只经由内部 Gate 构造与脚本化 runner,不能作为正式 CLI 配置。
  2. P0-2 WIP 旧入口的自动触发点已消除,并给出无生产调用反例。 wip/site-home-20260924 的 18d8182 仍带旧 --branch main --commit-dirty=true(不回改他人分支/快照);其唯一自动触发点(本机 pre-push)已去副作用。记录型 PATH shim 对照反例:对同一 WIP ref stdin,旧 hook(备份件)调用 npm run deploy(即真实生产发布),新 hook 0 次调用且 exit 0。守卫层独立反例:干净 detached worktree @ WIP 提交 18d8182 → REJECT(2),理由为两个远程 refs/heads/main 均不等于该提交(WIP 提交不是 main,永不能作为生产来源);test_wip_branch_checkout_never_uploads 另证「非 main 且非 detached 的 checkout,即使干净、双远程同 SHA,也拒绝且 upload=0」。Preview 路径必须解析出非 main 分支(空或等于 main 一律拒绝),确保 WIP 旧脚本/旧 hook 无法把 Preview 标成 main。
  3. P0-3 构建→上传的过期来源竞态已封闭。 上传前新增第二轮实时双远程查询(_query_remotes()),并以 evaluate_pre_upload() 判定:HEAD 变化、任一远程前进或不可解析、非生成型路径漂移 → upload=0。对抗探针复现 Lead 同款攻击(build 返回时把两远程切到另一 SHA):exit 2 / build=1 / upload=0,理由为 "a remote advanced while the site was building; refusing to publish a stale commit"。上传后新增 wrangler pages deployment list 核对最新 Production 行的 Source;不一致、不可解析或超时 → exit 4 且 productionSourceVerified=false,显式打印 "reporting partial, not success"——不把一次上传成功当作上线正确。
  4. P1-4a 构建后脏树检查不再放宽。 --untracked-files=no 已废弃,改回完整 git status --porcelain,仅豁免经证实的生成目录 GENERATED_SITE_PATHS(site/.vitepress/{.temp,cache,dist}、site/node_modules、site/public/archive、site/.wrangler);.gitignore 同步补两条作为第二层。fixture:build 期间生成 site/rogue.md → upload=0 且理由点名该文件;只生成上述目录 → 仍正常上传。
  5. P1-4b 构建/上传改为有界预算。 --build-timeout 默认 600 s(本机实测构建 95.06 s ⇒ ≈6.3× 余量)、--upload-timeout 默认 900 s、--verify-timeout 默认 90 s、--ls-remote-timeout 默认 20 s(每远程)。超时一律按失败上报:build 超时 → upload=0;upload 超时 → 打印 "UPLOAD TIMED OUT … NOT reporting success" 且 productionSourceVerified=false;verify 超时 → exit 4。
  6. 生产分支由实际 checkout 状态派生。 不再把 main 当作已验证事实:读取 git rev-parse --abbrev-ref HEAD,判 main / detached / 其他,并将 checkoutKind、branchRef、deployBranch 写入证据;只有 main 或 detached 干净隔离工作树可生产。

被取代的过度结论(明确撤回)

  1. ~~「旧 hook 现在只调用受控入口,因此安全」~~ —— 只对当前 main 工作树成立。WIP 快照内仍是旧命令,因此真正的修复是「本机 hook 去副作用 + 守卫拒绝非 main checkout」,不是把守卫加进当前 main 就完事。
  2. ~~「守卫除两次远程 ref 查询外为 O(1)」~~ —— 生产路径现为构建前 2 次 + 上传前 2 次 = 4 次实时远程查询,另有可选 1 次 Source 核查。复杂度仍是 O(1)(不随仓库规模增长),但网络往返翻倍。
  3. ~~「构建后只查跟踪文件,未放宽发布前置」~~ —— 该表述不准确:--untracked-files=no 确实放过了构建期间新出现的非生成型未跟踪文件。已改回完整状态检查 + 生成物白名单,承认前一版是放宽。
  4. ~~「Preview 永不占 Production」作为全域结论~~ —— 仅对本仓库的受控入口成立;已发布 Git 快照中的旧命令不受本批约束。

效率与资源对照(E2/E3,本机 macOS,time / /usr/bin/time -l;守卫 dry-run,真实 Git 与网络)

路径墙钟user CPUsys CPU最大 RSS块 I/O
拒绝:共享脏树(2 次远程查询,构建前即拒)15.88 s1.00 s0.26 s33.5 MB0 / 0
放行:干净 detached worktree @ 远程 SHA(dry-run)15.61 s0.79 s0.25 s34.0 MB0 / 0
完整放行(本批未执行)上界≥ build 95.06 s + 4 次远程查询 + 1 次上传————

墙钟几乎全部是 git ls-remote 的 SSH 往返(GitLab 局域网 + GitHub 各约 7 s);守卫自身逻辑(rev-parse×2 + status×2 + exists)为常数级。每次发布仍严格 1 次 build + 1 次 upload。--help 级别的 CLI 解析、evaluate_* 纯函数调用均 <1 ms(58 项测试 0.015 s)。

跨主机 hook 边界(不能证明的部分):本批只处置了本机 .git/hooks/pre-push(未入库状态)。其他主机、其他克隆、CI runner 或任何本仓库管辖之外的机器上的同类 hook 未经核实,仍可能保留旧行为;已发布 Git 快照中的旧 deploy 命令也不会因本批而改变。本批不宣称全域保证,该边界保留为环境已知风险。

验证命令与证据等级(E2 测试/构建 + E3 真实 Git 与网络;无 E4,不与 iOS 真机 E4 混称)

python3 site/scripts/tests/test_deploy_production.py            # 58 tests → OK
python3 <对抗探针>                                               # 空/单远程、CLI、构建中远程前进、非 main checkout
bash .git/hooks/pre-push  <WIP refs>  (记录型 PATH shim)         # 新 hook 0 次发布调用;旧 hook 调用 npm run deploy
python3 scripts/deploy-production.py --repo-root <WIP worktree> --dry-run   # REJECT(2)
python3 scripts/metric-knowledge-gate.py                        # 66 contracts PASS
env -u CODEBUDDY_SAFE_DELETE_BULK_STATE_DIR -u CODEBUDDY_TOOL_CALL_ID npm run build

当前状态:implemented · gate-open。G0 PASS / 是否进入 G1 由 Lead 独立复核后决定;Implementation 不自行宣布 PASS。

遗留项

Lead Independent G0-R1 Audit · 2026-09-25 · REPAIR REQUIRED

Lead 对 c93dea617c2a4abaac788c3c79ecdbed9f6af9ec 作只读复核:提交仅六个约定路径,双远程跟踪 ref 同 SHA;独立复跑部署守卫测试 58/58 OK;本机 pre-push 已变为无发布调用的提示 hook,原件有备份;执行归档中的 Cloudflare 推送前后列表逐字节一致,最新 Production Source 仍为 ea2a351。本批没有真实上传,G1 没有开始。这些子门成立;G0 总门仍为 REPAIR REQUIRED,G1 不放行。

  1. P0 · 构建后 rename 可绕过源码漂移门。 is_generated() 对 porcelain rename 只取 -> 右侧目的路径;R site/index.md -> site/.vitepress/dist/evil.md 因目的路径在生成白名单内被当作纯生成物。Lead 在真实模块和既有 FakeRunner 上复算:is_generated=True、execute exit=0 / build=1 / upload=1 / DEPLOY OK,尽管受版本控制的站点源文件已被移走。判定必须同时检查 rename/copy 的源与目的;任何非生成型路径参与变更均拒绝上传。不能仅检查目的路径。
  2. P0 · 上传期间远程前进仍可误报生产成功。 当前第二次 ls-remote 在上传前,上传返回后只读 Cloudflare Production Source,不再读双远程。Lead 在上传动作后将两个远程从目标 A 切到 B,同时令 Production Source 为 A:实际 exit=0 / build=1 / upload=1 / DEPLOY OK,上传后 ls-remote 次数为 0。这正违反上文 G0-R1 合同的「上传后的最新 Production Source 与双远程 SHA 仍须核查」。已上传时若任一远程前进/不可解析,必须报告 partial、不得报告生产验收成功;不得试图用脚本回滚云端。
  3. P1 · Preview 误用 Production 行验收。 execute() 无论 mode 均调用 evaluate_post_upload() 查最新 Production 行。Lead 的 preview 反例已上传一次 Preview,但 Production Source 为另一 SHA 时返回 exit=4;若 Production 恰为相同前缀又会把 Preview 误称为 source-verified。Preview 应只用同一受控入口验证对应 Preview 分支/部署,不借 Production 行证明 Preview 成功;拒绝 main Preview 的现有门保持。

唯一 G0-R2 最小返工合同

Implementation Update · 2026-09-25 · G0-R2(rename/copy 双向门、上传后远程复检、环境感知验收)

关联合同:上文「Lead Independent G0-R1 Audit · 2026-09-25 · REPAIR REQUIRED」→「唯一 G0-R2 最小返工合同」(Lead 基线 afb00f2)。本批只改 site/scripts/deploy-production.py、其对应测试、本 devlog 与 C2/C3 联动文件;未改 MacApp、首页视觉、旧归档、WIP 分支或他方四项未提交路径;未创建常驻 worktree(仅在测量时临时 git worktree add --detach 后于同一条命令内 remove --force);未真实上传 Cloudflare;未进入 G1。G0 PASS 由 Lead 独立裁定。

精确改动范围

路径变更
site/scripts/deploy-production.py+315 / −25:is_generated() 改为逐路径判定(新增 status_paths() / _unquote(),rename/copy 同时检查源与目的);新增 parse_deployment_rows() / select_verification_row();evaluate_post_upload() 增加 mode / deploy_branch;新增 evaluate_post_upload_remotes();execute() 增加上传后远程复检与环境感知验收 + 7 个新证据字段
site/scripts/tests/test_deploy_production.py+376 / −7:58 → 88 项(新增 30 项,保留原 58 项,其中 3 项的断言/fixture 被合同强制取代,见下)
site/devlog/index.mdC2/C3 联动(时间线节点 + 「八阶段总计划」P7 行 + 「未关门门禁」G0 行)
docs/architecture/…deploy-gate-2026-09-25-g0r2.html新归档快照(C2;旧快照 …-g0r1.html 与 …-2026-09-25.html 均不回改)

逐条回应 Lead 阻断项

  1. P0-1 rename/copy 不能再把站点源码「洗成」生成物。 新增 status_paths():按 porcelain v1 解析,R/C 行产出源与目的两个路径,切分 " -> " 时遵守引号(引号内的箭头不切分),并解开 git 的 C 风格引用(含八进制 UTF-8 字节)。is_generated() 现在要求该行触及的每一个路径都在 GENERATED_SITE_PATHS 内,否则整行计入漂移,且拒绝理由显式写明「a rename/copy is judged on BOTH its source and its destination」并原样列出含源路径的 status 行。Lead 的原反例 R site/index.md -> site/.vitepress/dist/evil.md:修复前 is_generated=True、纯函数零拒绝理由、真实 execute() 走到 exit=0 / build=1 / upload=1 / DEPLOY OK;修复后 is_generated=False、理由点名 site/index.md、exit=2 / build=1 / upload=0 / verify=0(探针 A1–A4,见证据包)。反向保护同样保留:R site/.vitepress/cache/a.json -> site/.vitepress/dist/a.json(两侧都在生成白名单内)仍可上传,避免把正常构建输出误判为漂移。
  2. P0-2 上传期间的远程前进不再被报成生产成功。 上传返回后新增第三次实时双远程查询(_query_remotes(),每个远程沿用 20 s 上界,无轮询、无无限等待),由 evaluate_post_upload_remotes() 判定;任一远程前进、超时或不可解析 ⇒ exit=4、verdict="partial"、productionSourceVerified=false,并把云端 Source 与远程 live head 并列打印(evidence <remote>: post-upload live head = … | cloudflare <env> Source short prefix = …)以及写入 postUploadRemoteHeads / postUploadRemoteNotes / postUploadRemoteReasons。不做任何云端回滚,只如实报 partial。Lead 的反例(上传后两远程 A→B、同时 Production Source 仍为 A):修复前 exit=0 / upload=1 / ls-remote 次数 4 / productionSourceVerified=true;修复后 exit=4 / upload=1 / ls-remote 次数 6 / productionSourceVerified=false,且 B1 双前进、B2 单前进、B3 超时、B4 解析失败四条路径分别留证(探针 B1–B4)。
  3. P1-3 Preview 不再借 Production 行验收。 evaluate_post_upload() 增加 mode / deploy_branch:production 只认最新 Production 行(且该行必须在 main,否则拒绝);preview 只认 Environment=Preview 且 Branch 等于本次发布分支的行,绝不使用 Production 行。Lead 的反例(Preview 已发布,但列表只有同短前缀的 Production 行):修复前 exit=0 且 productionSourceVerified=true;修复后 exit=4,理由含 "a preview publish is never proven by a Production row"。对照正向用例(列表含匹配的 Preview 行)仍为 exit=0;--preview main 仍 exit=2 且 build=0 / upload=0 / verify=0(探针 C1–C3)。Preview 路径按设计不查询生产远程(ls-remote 次数 0,postUploadRemoteHeads={})。

被取代的既有断言(点名、旧值、接续覆盖)

  1. ExecuteTests.test_each_remote_is_queried_before_and_after_the_build:真实放行路径的 ls-remote 期望值 4 → 6(构建前 2 + 上传前 2 + 上传后 2,三段各自覆盖固定的 origin+github)。原因:合同第 2 条要求上传后复检,该断言不可能同时保持 4;旧值已不再成立。接续覆盖:同一测试扩展到三段逐一断言,另加 test_post_upload_remote_recheck_uses_the_ls_remote_budget 断言新增两次查询沿用 ls_remote_timeout。
  2. ExecuteTests.test_preview_never_uses_main_branch 与 test_preview_branch_resolved_from_current_branch:两者的 fixture 原先只给默认的 Production 行,合同第 3 条生效后 preview 会正确判为未验证(exit=4)。断言目标不变(Preview 必须显式非 main、--branch 逐字一致),仅把 verify_listing 换成含匹配 Preview 行的列表。换言之:这两项从「借 Production 行就算成功」变为「必须有自己的 Preview 行」,正是 Lead 要求的行为。
  3. 无其他既有测试被改动;原 58 项的名称、意图与覆盖全部保留,仅上述 3 项的期望值/fixture 因合同强制而更新。

证据等级:Cloudflare Source 是短前缀,不是完整 40 位证明

效率与资源对照(E3,本机 macOS,/usr/bin/time -l,真实 Git 与网络;单次远程查询实测 origin ≈ 10.6–11.2 s、github ≈ 3.83 s)

路径墙钟user CPUsys CPU最大 RSS块 I/O远程查询次数
拒绝:共享脏树 dry-run(构建前即拒)14.43 s0.28 s0.13 s18.9 MiB0 / 02
放行:干净 detached worktree dry-run14.42 s0.26 s0.13 s17.5 MiB0 / 02
放行:完整路径(本批未执行)≥ build 95.58 s + 3 轮远程查询 ≈ 43 s + 1 次上传 + 1 次验收————6

本批新增成本:仅在放行路径增加一轮双远程查询(≈ 14.4 s,两次 SSH 往返之和),拒绝路径一分增加;仍严格 1 次 build + 1 次 upload,新增查询沿用每次 20 s 上界、无轮询、无无限等待。守卫自身逻辑(rev-parse×2 + status×2 + exists + 纯函数判定)为常数级:88 项测试合计 0.015 s。

验证命令与证据等级(E2 测试 + E3 真实 Git 与网络;无 E4,不与 iOS 真机 E4 混称)

python3 site/scripts/tests/test_deploy_production.py            # 88 tests → OK
python3 <A/B 对抗探针 against c93dea6 and the working tree>     # 三族反例:修复前全部放行,修复后全部阻断
python3 site/scripts/metric-knowledge-gate.py                    # 66 contracts PASS
env -u CODEBUDDY_SAFE_DELETE_BULK_STATE_DIR -u CODEBUDDY_TOOL_CALL_ID npm run build

当前状态:implemented · gate-open。G0 PASS / 是否进入 G1 由 Lead 独立复核后决定;Implementation 不自行宣布 PASS。

遗留项