<!--
  © 2026 IHUI AI (智汇AI) · 版权所有者: 李春川 (Li Chunchuan) · https://aizhs.top
  Provenance-watermarked. 未授权商用可被溯源追责 (Apache-2.0 须保留本声明与 NOTICE)。
  [IHUI-AI-PROVENANCE]:⁠​‌​​‌​​‌‍‍​‌​​‌​​​‍‍​‌​‌​‌​‌‍‍​‌​​‌​​‌‍‍​​‌​‌‌​‌‍‍​‌​​​​​‌‍‍​‌​​‌​​‌‍‍‌​‌‌​‌‌‌‍‍‌‌​​‌‌​​‌‌‌‌​‌​‍‍‌‌​‌‌​​​‌​​​‌‌‌‍‍​‌​​​​​‌‍‍​‌​​‌​​‌‍‍‌​‌‌​‌‌‌‍‍‌‌​​‌‌‌​‌​​‌‌‌​‍‍‌‌​​‌‌​​​‌​​‌​‌‍‍‌​‌‌‌​‌‌‌​‌‌‌​‌‍‍‌​‌‌​‌‌‌‍‍​‌​​‌‌​​‍‍​‌​​​​‌‌‍‍‌​‌‌​‌‌‌‍‍​‌‌​​​​‌‍‍​‌‌​‌​​‌‍‍​‌‌‌‌​‌​‍‍​‌‌​‌​​​‍‍​‌‌‌​​‌‌‍‍​​‌​‌‌‌​‍‍​‌‌‌​‌​​‍‍​‌‌​‌‌‌‌‍‍​‌‌‌​​​​‍‍‌​‌‌​‌‌‌‍‍​‌​‌​​​​‍‍​‌​‌​​‌​‍‍​‌​​‌‌‌‌‍‍​‌​‌​‌‌​‍‍​‌​​​‌​‌‍‍​‌​​‌‌‌​‍‍​‌​​​​​‌‍‍​‌​​‌‌‌​‍‍​‌​​​​‌‌‍‍​‌​​​‌​‌‍‍​​‌​‌‌​‌‍‍​​‌‌​​‌​‍‍​​‌‌​​​​‍‍​​‌‌​​‌​‍‍​​‌‌​‌‌​⁠
-->

# AGENTS.md — IHUI-AI 项目 Agent 指南

> 作用域:`D:/IHUI-AI` 仓库根目录及所有子目录（2026-09-12 修正:仓库早已从 `G:` 迁到 `D:`，旧盘符路径已全部改为从脚本自身位置推导）。
> 历史案例归档见 `.ihui-agent/archive/AGENTS_history.md`。
> 本文件为精简版(2026-07-25 重构,原 783 行 → ≤400 行),保留所有强制规则核心条款。

---

## 1. 任务计划文档规则(强制)

- 项目**唯一**任务计划文档是 `PROJECT_PLAN.md`(根目录),所有任务计划、进度更新、待办清单、状态变更**只写**此文件,**不得**在 `.ihui-agent/`、`docs/`、根目录或其他位置新建计划/TODO/ROADMAP 文件。
- 完成任务后 `[ ]` → `[x] ✅(日期)`;新增任务追加到对应优先级(P0/P1/P2)末尾。commit message:`feat`/`fix`/`docs`/`chore`/`test`/`refactor` 前缀。
- **任务认领(2026-09-23 立)**:agent 开始任务前,在对应 `- [ ]` 后追加 `（进行中）` 标记(全角括号),例 `- [ ]（进行中）O20d 守门...`;完成后按上条改 `[x] ✅(日期)` 并删除 `（进行中）`。扫描 `PROJECT_PLAN.md` 可区分三态:无人认领(`- [ ]` 无标记)/ 进行中(`- [ ]（进行中）`)/ 已完成(`- [x]`)。派单前先扫进行中项,避免重复认领。扫描工具:`node scripts/check-task-claims.mjs`。
- **任务状态一致性(2026-09-26 立,守门 130 配套)**:计划文档被当台账用而**没有主键约束**,这是"完成的为什么还在这里、还有重复"的唯一根因 —— §1 上一条让每个 agent 追加登记,同一任务编号被不同批次各写一份,做完只翻自己那一批那份,其余副本永久烂成"未勾选";而既有防护链(门 71 防丢行 / 84 防写回旧版 / 100 防合并吞文件 / 工作区自愈)**全部单向防丢,没有一道判"状态分叉"**,并集合并又是"每行重数取 max",所以改写必然产出两份。三条规矩:
  1. **派单口径只认一个入口**:`node scripts/plan-tasks.mjs --open`(未勾选 − 同题已完成副本 − 自带作废声明的行 − 同一件事的第二条待办副本;四条判据 F1/F2/F3/F4 都以"编号 + 标题前缀逐字等值"的复合主键为准,不依赖行号顺序也不依赖相似度)。`grep '^- \[ \]'` 不是待办清单(2026-09-26 一次实测的**不重叠**分解:未勾选 205 = 已认领 44 + 其余排除 28 + 真待办 133;同刻 F1/F2/F3/F4 读数均为 0 —— **这些数是当次现读,派单前必须重跑,勿照抄**;报告里其余排除项的明细计数彼此可重叠,只能当诊断看,不能拿来减法核账)。归并出口:`node scripts/plan-tasks-merge.mjs --heal --commit`(F1/F2 翻勾并写明归并到谁;**F4 只加"重复登记副本"指针、绝不动勾选** —— 两条都是没做完的事,把没做的记成做过的比原病更响)。
  2. **一个编号只能有一行"当前状态"**:新登记同一件事时必须把旧行翻勾或就地改写,不得在别处再抄一份。判据是复合主键(编号 + 标题前缀**逐字**等值),不是相似度 —— 相似度只能报数,不配判红。
  3. **证据指针禁止写行号**:行号在任何一次 append 后都会挪位。实测 HEAD 面 27 处 `存活于 L<行号>` 式指针**复核通过率 0/27**(目标行已不是条目行)——上一轮登记"已识别、只标注未翻勾"的那批账,今天已无法按它自己给的证据复核。必须用内容锚点(编号 + 标题原文)。
  - 问责:`pnpm check:plan-task-state`(`--strict`,存量判红);提交链上跑的是**差值棘轮**(只拦"这次提交让分叉变多",存量永不拦 —— 与改动无关的恒红门只会逼人 `--no-verify`,连带废掉全部守门,§12e 同型)。修法只有**归并**(副本翻勾并注记归并到谁),§1 的"禁止无声删除"对已完成行仍然成立。

### 归档机制

- 已完成任务条目(`### XXX(已完成 ✅ ...)` 标题)**禁止直接删除**,必须两步走:① 把完整任务条目(标题 + 正文)移动到 `.ihui-agent/archive/PROJECT_PLAN_YYYY-MM-DD.md`;② 在 `PROJECT_PLAN.md` 原位置留 HTML 注释占位:`<!-- 已归档(YYYY-MM-DD):XXX 任务,完整内容在 .ihui-agent/archive/PROJECT_PLAN_*.md -->`。
- **自动归档**:`scripts/archive-completed-tasks.mjs` 扫描完成 ≥7 天的条目,post-commit 钩子自动 `--auto-commit`。归档 commit 设 `IHUI_ARCHIVE_COMMIT=1` 防递归。
  - **标题形态三处必须同形**(2026-09-25 G-182 实证):本节写的 `### XXX(已完成 ✅ ...)`、守门 13c 的提取式、归档器 `parseCompletedTasks()` 的匹配式,**任一处漂了另外两处不会响**。归档器曾按 `/^### \[x\]/` 找标题,而文件里 `^### [x]` 命中 0 次 ⇒ 连续 11 天每次跑都扫到 0 条、post-commit 静默无操作。现判据:含 `✅` 即算已完成条目,日期取标题里第一个 `YYYY-MM-DD`(真实形态日期常在 ✅ 之前);**无 `✅` 的小节标题(如 `### 已完成清单`)不算条目** ⇒ 归档器的搬运集必须是 13c 保护集的**真子集**,否则一边搬一边护就是互咬。
  - **大批量阀门只挡自动档**:`--auto-commit` 下一次 >25 条或 >256 KB 即拒绝并打印实测数字(退出码 0 —— 自动档少做一件事不是错误,不得把钩子链弄红),需人工 `--allow-mass` 放行。依据实测稳态一次 7 条 / 15,168 B / 全文 0.61%;超量只可能是格式又漂或积压一次放开(参照 §5c 水印门"单次缺口 >200 拒绝自动回写"同一条设计)。
  - **归档锚点必须受版本控制**(2026-09-25 G-183):`.gitignore` 对 `.ihui-agent/` 的处理是"忽略其内容 + `!.ihui-agent/archive/` 例外",**不得改回整目录忽略** —— §1 的"完整内容在 `.ihui-agent/archive/PROJECT_PLAN_*.md`"和守门 71 的"archive 里找得到 ⇒ 不算丢"都以它为事实基础。旧状态(整目录忽略)下:历史那次 `b599edbba` 归档 11 条的 commit **只含 PROJECT_PLAN.md 一个路径**,即副本从未入库,而"A 机归档过"可以授权"B 机删行"。守门 71 曾仍读磁盘副本 ⇒ 尚未入库的本机归档文件可以授权另一次提交删行,**该洞已于 2026-09-25 由 G-184 关闭**:`archivedCopy()` 的归档清单与内容现在**同面**取(`--staged` 走 `git ls-files` 索引、全量走 `git ls-tree HEAD`),面里没有的路径不构成删行凭据;`runCheck` 必须把当次判定面喂给豁免(`archiveExemptFor(isStaged)`)。取证:自测四条组合 + 镜像端到端 **A/B**(同一份归档正文、同样删掉 `## O42`,唯一差别是一次 `git add`:未入库侧必 exit 1、已入库侧必 exit 0),外加一条反向回归锁(函数体内不得再出现 `readdirSync(` / `readFileSync(`,对 HEAD 旧版该锁 5 条断言全红)。**G-183 当时登记的"被门 118 结构性棘轮挡住"这个前提实测已不成立** —— 并行会话已把门 71 的内容面迁到 `scripts/lib/face-reader.mjs`,门 118 见 face-reader 导入即判合规,所以这里不存在"必须先整道迁移"的阻塞;登记这条是为了说明**沿用旧理由会挡住一件已经可以直接做的事**。
  - **归档 commit 必须带 pathspec**(2026-09-25 与上面同批修):旧实现第二步是 `execSync('git commit --no-verify -m "…"')` —— 不带路径 = 把当下共享索引里的东西全提交,而并发会话随时在索引里挂着 staged 内容,于是那枚"归档 commit"会把别人的在途改动打包带走(§12 污染型;它跑在 post-commit 里,没人盯着)。**它一直没炸,只因为匹配式漂了、这段代码从没真跑到过 ⇒ "让工具能用"和"修它的提交面"必须是同一条提交,否则修好匹配式的那一刻就是引爆那一刻。**现形:`execFileSync(GIT_BIN, […, 'commit', '--no-verify', '-m', msg, '--', <计划文档>, <归档文件>])`。A/B 实证:旧版在"计划文档 + 一个被 `git rm --cached` staged 的他人文件"场景下产出的 commit **确实含别人的文件**;新版只含声明的两路径(测试第 20 例钉成回归)。
  - **归档占位点名的文件必须真在审面里(守门 13c 的 A1/A2/A3,2026-09-25 立)**:`<!-- 已归档(…):X,完整内容在 .ihui-agent/archive/<FILE> -->` 是一句**可核验的指针**,不是修辞。A1 = 盘上有 `PROJECT_PLAN_*.md` 而审面(全量 HEAD 树 / `--staged` 索引)没有 ⇒ 红;A2 = 占位点名的具体文件不在审面里 ⇒ 红(写成 `PROJECT_PLAN_*.md` 通配的占位不点名,不参与判定)。**A3(同日 G-192)= 同一把尺子伸进归档件内部** —— 元归档层(归档文件正文里再写"完整内容在 archive/<另一个>")过去完全无人看守,实测 `2026-09-12` 那份里嵌着 **218 条**占位、点名 13 个文件,其中 12 个从未入库。A3 与 A2 共用"缺失⇒红 / 已登记⇒只报数 / 已入库仍挂台账⇒判清单腐烂"三段规矩,标签不串门,同一名字被 A2 与 A3 点到只计一次债;blob 取不到那一层 ⇒ 结论行喊"未判定",不静默通过。立项即抓到两条实测:`PROJECT_PLAN_dedup-2026-09-23.md` 只躺在本机(已随本票入库),以及 **8 个被 52 条占位点名的归档文件在仓库与本机两侧都不存在、其路径也从未出现在历史里** —— 即 §1 那句承诺对这批条目一直是空的。这 8 项进 `LOST_ANCHOR_LEDGER` **只报数**:它们是已发生的债,当场判红就是一台与任何提交都无关的恒红门(§12e 同型);台账是**待偿清单不是豁免清单** —— 修好了仍挂着 ⇒ 判"清单腐烂"红,删了行却没修好 ⇒ 照常红,两个方向各有一条自检钉死。按条目从计划文档历史版本逐字找回另计一票。
  - **台账已第一次真清偿(2026-09-25 G-191,8 项减到 4 项)**:找回的办法是**从 git 历史逐字取回,不是重写历史**。每批归档都由"把正文从计划文档搬走"的那枚提交产生,所以那枚提交**删掉的内容块**就是归档正文;落地判据只有一条 —— 该正文逐字存在于所引提交的父版本 `PROJECT_PLAN.md`,不成立就不落地(实测有 2 条 `2026-07-26` 的占位是在 `3a5b737bf8` 里**凭空新增**的纯 `+` 行,按裸标题 `git log -S` 证明那两个条目从未存在过 ⇒ 从别的提交"补"出来就是编造,已确认为找不回,不当干)。落地 4 份 / **28 块 / 567 行实质正文、0 行无出处**,取证用两把**互相独立**的尺子(逐块对应 23/23 + 格式无关的全量行级比对 567/567 —— 后者专门防"重建脚本自己给自己发合格证")。每份文件里保留逐条 `recovered from <sha>` 注释 + 一句"这是找回件,不是当时原写"的性质说明:**不写进文件,后来人就会把重建文本当一手记录。**
    **取出处时必绕过一个坑**:`b129e09482` 做过一次 `.trae-cn/archive/` → `.ihui-agent/archive/` 的**占位文本搬家**(它不删除任何内容),按"最旧一次 `-S` 命中"取出处会选中搬家那枚而非真正的删除,必须沿搬家链回走。
  - **递归那一格已做完(2026-09-25 G-192),并把看守面扩到归档层自身**:上面"218 条嵌在元归档里的占位"共点名 **13 个**文件,三类处置 —— ① **4 个真存在过**(躺在 `.trae-cn/archive/` 下,搬家时漏带),按 `git cat-file blob <出处>^:.trae-cn/archive/<名>` **原字节**落盘,判据两把:注入水印**之前** `git hash-object` 与出处 blob **同 SHA**,注入**之后**正文仍须逐行全等(合计 2,210 行 / 170,608 B);② **8 个从未作为文件存在过**(`git log --all -- <任意前缀>/<名>` 对 8 个名字全部 0 命中)⇒ 占位被写过、文件没被写过,结构上找不回,进台账只报数;③ 1 个由 G-191 已入库。**为什么必须两把尺子**:找回脚本自己写的是"剥水印→逐行全等",它剥窄或剥宽都会让自己的断言跟着错(§22c"镜像测试只复读实现就是复读机"同型),所以独立复核换成异形判据 —— "blob 每一行必须在落地文件里**同序、连续、逐字**出现",水印层天然落在窗口之外。过程中真抓到一次不等:出处带 **UTF-8 BOM** 而 `watermark.mjs inject` 重写时会去掉 ⇒ 容差只开在 BOM 这一处(字节同值已由注入前的 SHA 判据证明),并写进文件里的出处注释,不让它读成"逐字节永远同值"。**A3 是本格的真正产出**:归档件**内部**的占位同样点名,A1/A2 过去只扫计划文档,这一层零看守。台账 4→12 项(**全部只报数**,所以扩 A3 不产生新恒红面)。
  - **13c 自身的取材口径(同日迁移)**:此前 `ROOT = process.cwd()` + 裸 `git show` 用 `execSync` 拼字符串(无 timeout)+ 占位合规性看 `git diff` 文本,三处都与本仓现行口径相反(工作树滞后即换结论 / 服务账户 PATH 不通 / 别人同时改这文件时合规性跟着盘走)。现全部经 `scripts/lib/face-reader.mjs`,对照面是 `--staged`:HEAD→索引、全量:HEAD^→HEAD、`--worktree` 仅人工;整面取不到 ⇒ **exit 2 无法判定**,不冒红也不记绿。归档件正文也**同面**取(A3 用),单份 blob 取不到 ⇒ 在结论行喊"那一层未判定",不静默通过。另有两条是本门第一次自跑咬出来的:CRLF/LF 的同一条标题不得被读成"删除"(HEAD blob 是 LF、共享工作树常是 CRLF,不剥 `\r` 会把整批已完成条目误报成被删);**点名对象的归属必须拿该目录在审面上的"全集"判**,而不是只看 `PROJECT_PLAN_*` 形状 —— 归档目录里也放非该形状的跟踪件,按形状筛就会把"已入库"读成"落空"。
  - **本门自己修掉的两个"拦了却不说话"型缺陷(2026-09-25 G-192,都值得记住形态)**:① A0 阻塞分支写成 `if (deletedHeadings.length > 0 && !del.addedPlaceholders)`,而**空数组是真值** ⇒ 该分支永不执行,门对"直接删已完成条目"`exit 1` 且**零输出** —— 调用方只看到提交被阻止,看不到原因(现走 `del.compliant`,由 length 决定)。② 门按自身位置推导 `ROOT` 之后,镜像测试靠 `cwd` 定位夹具的那套调用**结构上失效了**:13 例里 11 例其实在审真仓,账面全绿而结论与夹具无关(守门 70 同型)。夹具改经显式 `--root <dir>` 通道进入,落点改回 `scripts/lib/scratch-dir.mjs`(§26,不再往 `os.tmpdir()` 写),并由两条**源码级反向锁**钉住(不得再出现"只靠 cwd"的调用形态、不得回到裸 `mkdtempSync`)—— 这类失效只有源码锁能防,加断言会跟着一起漂绿。取证现为 `--self-test` **17** 条 + 镜像 **20** 例(含 A1/A2/A3 端到端与"点名已入库的对象不该红"的反向对照,后者防的是把整层一律判红那种恒红门)。**本门没有应急跳过通道**(runner 条目无 `skipEnv`、脚本不读 env);登记这一事实,是因为本仓多次出现"文档写了一个跑不通的出路"。
- **手动触发**:`pnpm archive` / `--all`(全部)/ `--days 3`(自定义)/ `--dry-run`(预览);跳过用 `HUSKY_SKIP_ARCHIVE=1 git commit`。
- **守门**:`scripts/check-project-plan-archive.mjs` + pre-commit 第 13c 项。历史案例见 `.ihui-agent/archive/AGENTS_history.md`。

### 唯一例外

- `/goal` 模式:`.ihui-agent/goal-runtime/STATE.md` + `loop-run-log.md`(临时,目标结束后删除);skills:`.ihui-agent/skills/SKILL.md`(AI 工具配置,非计划文档)。

---

## 2. 项目概览

IHUI-AI 是全栈 AI 平台(TS Monorepo + pnpm workspace + Turborepo),8 端清单:

- `apps/api`(Fastify 5 + Drizzle ORM 0.38 + PostgreSQL)
- `apps/web`(Next.js 16.2.12 + React 19 + Tailwind 4 + shadcn/ui)
- `apps/ai-service`(FastAPI + LangGraph + LiteLLM + MCP)
- `apps/miniapp-taro`(Taro 4 + React)
- `apps/desktop` / `apps/extension` / `apps/mobile-rn` / `apps/cli`(各端独立)
- `packages/`(database / auth / types / ui / config / eslint-config / tsconfig)

---

## 3. 代码风格

- 做减法,最小化代码,零冗余。复用现有代码和模式,不创建文档文件(除非明确要求),不加 copyright/license header。

### TypeScript 类型零技术债(强制)

- **尽最大程度禁用 `any`**,优先用 `unknown` + 类型守卫 / `as const` / 泛型 / 条件类型 / 工具类型(`Pick`/`Omit`/`Record`/`Partial`/`Required`/`ReturnType`/`Parameters`)/ 精确接口替代,禁止把 `any` 当"类型兜底"逃避设计。
- **深度分析 TS 用法**:函数签名(入参/出参/泛型约束)、对象字段、API 响应、props、state 必须显式标注精确类型;能由 `tsc` 推断且可读性良好的局部变量可省略,但禁止"省略 = 不写类型"扩散到公共 API。
- **必须用 `any` 的例外(三选一)**:① 第三方库无 `@types` 或类型声明缺失;② 泛型推断失败且无法用 `unknown` + 守卫替代;③ 跨包循环依赖无法用类型导入断言解决。**必须**附行内注释 `// FIXME(any): 原因 + 移除计划 + 截止版本`,后续 PR 必须清理。
- **不留技术债**:新代码 `tsc --noEmit` 必须 0 错误,禁止"先 any 后修"占位;重构遗留 `any` 必须替换为精确类型,**禁止**复制粘贴扩散 `any`;PR 引入新的 `any` 必须在 PR 描述说明例外依据。
- **守门**(分层渐进):
  - **过渡期(当前生效)**:`@typescript-eslint/no-explicit-any: error`(packages/eslint-config/index.js,syntax-level 不需 type info,已生效)。lint-staged 对 staged 源码文件触发阻塞,新代码引入 `any` 会在 `pnpm lint` 报错。
  - **目标态(已评估,推迟启用)**:`@typescript-eslint/no-unsafe-assignment` / `no-unsafe-member-access` / `no-unsafe-call` / `no-unsafe-return` / `no-unsafe-argument` 五条规则设为 `error`。**2026-07-28 评估结论**:实测启用 `recommendedTypeChecked` + `projectService` 后,(a) 性能不可接受 — cli 包(最小)lint 时间 2.7s -> 51.9s(慢 19 倍),web/api 包预计 30s -> 600s+;(b) 历史错误多 — cli 包已报 30+ 处(`require-await` / `no-unsafe-assignment` / `no-base-to-string` / `restrict-template-expressions` / `no-unnecessary-type-assertion`),全量预计 300+ 处;(c) 配置陷阱 — `eslint.config.js` 不被 tsconfig include 需 `allowDefaultProject`,JS 文件需单独豁免 `no-unsafe-*`。**启用前置条件(全部满足才可启用)**:1. lint 性能优化方案落地(eslint cache 持久化 / 仅 staged 文件 typed-lint / CI 才跑全量 typed-lint);2. 历史类型错误清零(`require-await` 等非 unsafe 错误先修);3. `allowDefaultProject` 配置就绪。
  - **测试文件豁免**:`**/*.test.ts` / `**/*.spec.ts` / `**/tests/**` / `**/test/**` / `**/e2e/**` 路径下的 mock/stub 代码允许 `any`(mock 类型断言必需),启用 typed-linting 后通过 `files` overrides 关闭上述规则。
  - CI `pnpm typecheck` 全绿方可合并。

### 共享层优先(强制)

- **写新代码前必须先查共享层**,确认是否已有现成实现。禁止在端内(apps/*)重新实现 `packages/` 已提供的功能。
- **检查清单(按顺序)**:
  1. **hooks**: `packages/shared/src/hooks/` — 基础 hook(clipboard/debounce/countdown/form/mounted/pagination 等)和业务 hook(auth/chat/agents/articles/agent-runtime/confirm-dialog 等)共 16 个。各端 `hooks/` 目录应只做 re-export wrapper + 平台 adapter,不得独立实现。
  2. **utils**: `packages/shared/src/utils/` — 工具函数(date-utils/dangerous-command-detector/format/file-helpers/error-messages/jwt-utils/ai-skill-variables 等)。各端 `lib/` 目录应只做 re-export wrapper。
  3. **types**: `packages/types/src/` — 所有跨端类型(ChatMessage/MessageInputFile/WorkspacePermissionMode/User/ApiRequest 等)。禁止在端内重新声明同名类型。
  4. **api-client**: `packages/api-client/src/` — 所有 API 调用。禁止在端内直接用 `fetch`/`axios`/`Taro.request` 调后端,必须走 `@ihui/api-client`。端内可保留 re-export + 平台 adapter(如 AsyncStorage 持久化)。
  5. **stores**: `packages/shared/src/stores/` — 共享 store 工厂(createAuthStore/createThemeStore)。各端 store 应调工厂 + 注入平台 transport,不得重新定义 state shape。
  6. **constants**: `packages/shared/src/constants/` — 跨端常量(storage key/URL/locale key 等)。禁止在端内硬编码同名常量。
- **如果共享层没有**:
  - 评估是否跨端可用 → 如果是,先提取到 `packages/shared/` 再在端内 import,不得直接在端内写。
  - 如果确认平台特有(依赖 DOM/RN API/Taro API)→ 可在端内实现,但必须在文件头注释说明 `// 平台特有:依赖 [DOM/RN/Taro] API,不适合共享`。
- **工厂模式优先**:跨端 hook/util 用工厂函数 + 依赖注入(参考 `createUseClipboard` / `createAuthStore`),各端传入平台 adapter。不得用 `if (Platform.OS === 'web')` 条件分支在共享层处理平台差异。
- **守门**:PR review 时检查是否有端内文件重新实现了共享层已有功能。发现重复 → 要求改为 import 共享层。

---

## 4. 前端 UI 约束

- compact 紧凑、elegant 优雅。hover 用 subtle 颜色变化,**不要蓝色发光边框**。复用 `packages/ui-react` 的 Card/Button/Input/Dialog。每个页面 < 250 行。时间用 `Intl.DateTimeFormat`,头像用 initials。状态徽章:draft 灰 / published 绿。积分正数绿色,负数红色。

### 品牌 CTA / 主按钮色同源(强制,2026-09-24 收口)

- **唯一写法(2026-09-26 回翻定稿)**:主按钮、选中胶囊、悬浮加号这类"品牌实底 + 其上文字",RN/共享包写 `brand.cta`(底)+ `brand.ctaForeground`(文字)**成对**;CSS/类名侧用 `--color-cta` + `--color-cta-foreground`(小程序 `CategoryBar`/FAB 同值)。**这一档黑/白反转**:亮=纯黑底(`#000000`)/ 纯白字(`#FFFFFF`),暗=纯白底(`#FFFFFF`)/ 纯黑字(`#000000`),与 `--color-primary` 同一明暗行为,`.dark` 显式覆盖。对账由「跨端色值同源对账」守门负责(编号以 `scripts/guardian-runner.mjs` 现值为准,勿照抄文档)。
- **为什么保留独立档名而不并进 `--color-primary`**:primary 在 web 端兼任墨色(`text-primary` 1803 处),改它等于给全站正文染色;CTA 保持独立档名、取值与同主题 primary 全等,语义 = "品牌实底 + 其上文字"。(2026-09-24 曾取 `#4A7A96` 明暗同值以避开"大色块与页面反极",2026-09-26 用户明确要求回翻为黑/白反转,该设计已废。)
- ~~**唯一写法(2026-09-24 已废)**:主按钮/选中胶囊用 `brand.DEFAULT` + `brand.foreground`,CSS 侧 `--color-primary` + `--color-primary-foreground`。~~ 该行描述的正是被本票替换掉的旧档:DEFAULT 亮=纯黑 / 暗=纯白,大色块与页面反极。**现存唯一写法见本节第一条**(`brand.cta` / `--color-cta`)。保留本行仅为让早期条目里的 `brand.DEFAULT` 措辞能被追溯到已废。
- 真要新增品牌档:先在 `tokens.css` 落一个 CSS 变量,再到同源对账门的映射表登记依据;确属 RN 专属(如 `brand.dark` 品牌绿)才能进 `RN_ONLY_BRAND_KEYS` 并写明理由 —— 豁免项若已不存在同样算红(防清单腐烂)。
- 悬空引用由该门的"品牌键"判据直接拦(编译不一定红,运行时是 `undefined` 颜色)。并行会话的暂存区里若还残留 `tokens.brand.ctaFill/ctaText`,改法只有一行:`ctaFill` → `cta`、`ctaText` → `ctaForeground`(2026-09-24 改档;此前该行的指向是 `DEFAULT`/`foreground`,已随 CTA 独立成档而更新)。
- **禁止端内自立 CTA 档**:`brand.ctaFill` / `ctaText` 这类"浅色等于 web、深色另取一档"的**混血**键不得再加回 —— 当年删它不是因为"不许有 CTA 档",而是因为**它在端里自立、且两档取值互不相同**(那才是"手机上改了 web 没改"的成因)。`--color-cta` 走的是相反路径:先落 `tokens.css` 源头(2026-09-26 起 `.dark` 显式覆盖、黑/白反转)、再进同源对账门的映射表,所以它合法而旧键仍属禁止。要调主按钮观感就改 `tokens.css` 的 `--color-cta`(亮档)与 `.dark --color-cta`(暗档)**同一处**,三端一起动;不得在某一端单独覆盖。
- **成对即合规,不得再当债务**:主 CTA 的唯一写法是 `brand.cta` 实底 + `brand.ctaForeground` 文字(旧档 `brand.DEFAULT` + `brand.foreground` 已废,见上),所以这套配对**必须**被所有前景/容器对账判据认作合法 —— 按规矩写就红的门只有一个结局:逼人 `--no-verify`,连带废掉全部守门。拦的是**没有配对前景**的实底卡片与**跨档错配**(底取 `brand.cta`、字却取 `text.primary`/`surface.light` —— 前者黑/白反转下两态与底同色纯隐形,后者按档位法则本就非配对前景)。守门 83 的 R1_BG 已同时认 `brand.DEFAULT|brand.cta`,就是为了"判据必须覆盖门自己产出的形态"。
- **禁止端内自立 CTA 档**:`brand.ctaFill` / `ctaText` 这类"浅色等于 web、深色另取一档"的混血键已全部删除,**不得再加回**。要调暗色主按钮的观感(例如觉得纯白刺眼),改 `tokens.css` 的 `.dark --color-primary` **一处**,web / 小程序 / RN 同时动;不得在某一端单独覆盖 —— 那正是"手机上改了 web 没改"的成因。

> ⚠️ 本行已于 2026-09-24 被上面「禁止端内自立 CTA 档」新条目就地改写取代;保留原文只为不丢行,勿照它执行。

- **成对即合规,不得再当债务**(同日补):上面这条是主 CTA 的唯一写法,所以深色档案下"brand.DEFAULT 实底 + brand.foreground 文字"**必须**被所有前景/容器对账判据认作合法 —— 按规矩写就红的门只有一个结局:逼人 `--no-verify`,连带废掉全部守门。拦的是**没有配对前景**的白卡片与**跨档错配**(底取 brand、字取 `text.primary`/`surface.light`)。

> ⚠️ 本行已于 2026-09-24 被上面「成对即合规」新条目就地改写取代;保留原文只为不丢行,勿照它执行。

- **RN 侧 `dark:` 类必须与 App 主题同源才允许依赖**(强制):共享 preset 是 `darkMode: 'class'`,`dark:*` 工具类读 NativeWind 自己的 colorScheme store。`apps/mobile-rn/src/theme/color-scheme-sync.ts` + `ThemeProvider` 负责把 App 解析出的主题(light/dark/system→系统)落进该 store —— 缺这一步,`dark:` 只跟系统外观,与 App 内主题开关长期分裂(而前景/容器对账门把"同行有 `dark:` 变体"记为已配对,不同步时那些配对只是纸面正确)。**新增深色取用二选一**:走 `tokens.*`(模块级单例,重载 JS 生效,共享包默认),或走 `dark:` 变体(依赖上述同步);不得混用两套、更不得在端内自造第三个色源。回归:`apps/mobile-rn/tests/theme-colorscheme-sync.test.ts`。

- 真要新增品牌档:先在 `tokens.css` 落一个 CSS 变量,再到守门 90 的映射表登记依据;确属 RN 专属(如 `brand.dark` 品牌绿)才能进 `RN_ONLY_BRAND_KEYS` 并写明理由 —— 豁免项若已不存在同样算红(防清单腐烂)。
- 悬空引用由 R3 直接拦(编译不一定红,运行时是 `undefined` 颜色)。并行会话的暂存区里若还残留 `tokens.brand.ctaFill/ctaText`,改法只有一行:`ctaFill` → `DEFAULT`、`ctaText` → `foreground`。

### 描边不得取墨档(强制,2026-09-26 立)

- **规矩**:任何描边 —— RN/共享包的 `borderColor` / `borderBottomColor` / `border: \`1px solid …\``模板串、web 与小程序的`border-primary`/`ring-primary` 类名 —— **一律不得取墨档**。用户原话:「我们项目设计没有这样的纯黑色的描边」。
- **禁的四档**:`brand.DEFAULT`、`brand.foreground`、`brand.ctaForeground`、`text.primary`(亮=近纯黑,暗=近纯白)。它们兼任墨色与文字色,当描边用就是在容器外圈画一道硬黑线。
- **合法出路只有两条,都是项目已有档**:中性描边 → `border.light` / `border.medium`(= web 的 `border-border`);强调描边 → `brandAccent.light` / `brandAccent.deep`(= web 的 `border-brand-accent-deep`,亮 `#4a7a96` / 暗 `#a3c4d6`,端内在 R8 之前已有 7 处这么写)。不得在端内自拼 `#000` / `rgba(0,0,0,…)`,也不得为消红去改判据。
- **`brand.cta` 作描边不由本条判**:它是"主实底"档,与同色填充并用作描边时语义是加厚而非描边,且该形态早已计入守门 83 的 **R3** 计数面(`(backgroundColor|borderColor): *.brand.cta`)。同一笔债两道门各计一次会让两份基线互相顶掉。
- **染色档不算这一型**:`border-primary/20` 是 20% 染色、`border-primary-foreground` 是另一个键名,都不属"纯黑描边"。判据用尾部字符类同时排除两者,别放宽。
- **覆盖面(同日两次补,现行口径以这条为准)**:R8 判**三种书写形态**——RN style 对象(`border\w*:` 取墨档)、Tailwind 类名(`border-primary`/`ring-primary`)、**CSS 声明**(`border*: … var(--color-primary)`,含 TSX 内联字符串形态)。文件面**不再维护端名单**:`R8_DIRS = ['apps','packages']` 整面取,只靠 `e2e/tests/__tests__` 与 `*.test/spec.*` 排除叙述行;扩展名含 `tsx|jsx|ts|js|css|scss`。立项时前两种形态各自漏过一块(extension 不在 R5 端表里 → 类名形态在扩展端失明;.css 不在扩展名里 → **19 处** CSS 声明整面隐身),两处都已随同笔提交清零,基线 `inkBorderCounts` 仍为**空 = 零容忍**。**给守门扩面必须同批改两半**:内容判据的 scope 函数 + 枚举/预筛的目录表与扩展名;本条的第一版就只改了 scope,`git ls-files` 仍用旧表,结果**整块改动静默失效而门照报绿**。防线两条:① 拿真仓 HEAD 喂新判据,它必须点名已知存量(阳性对照);② 镜像测试锁源码——枚举面必须引用新表、旧表不得再出现、预筛常量必须含新形态。
- **历史成因(值得留着)**:本门自己的修复提示曾写着「`brand.DEFAULT` / `--color-primary` 只保留墨色、**描边**、文字色三义」—— 散文把这一档列为描边的合法语义,判据又只管"前景 vs 容器配对"、从不算描边取哪档,**两边一起漏**,所以这一型活了很久还一路报绿。现该措辞已就地改掉。
- **守门**:并入 83(见下方速查),新增判据 **R8**,基线键 `inkBorderCounts` 现为**空 = 零容忍**(立门当日 HEAD 实测 148 处:RN style 面 33 + web/小程序类名面 115,已全部并回上述两档)。行内豁免 `border-ink-exempt: <原因>` 必须带原因且**逐行**生效;上一行算豁免的前提是它是纯注释行。

### 圆角单一源头(强制,2026-09-23 全端收口)

- **唯一真相源**:`packages/design-tokens/src/radius.js` 的 `RADIUS_STEPS` = `xs 2 / sm 4 / md 6 / lg 8 / xl 12 / 2xl 16`(px;`DEFAULT`=8 对齐 web `--radius: 0.5rem`)。改档位只改这一处。`tailwind-preset.js` 必须写 `borderRadius: RADIUS_REM`,`tokens.css` / `app.css` 的 `--radius-*` 必须与之逐档同值。
- **各端取用形态(不得自创写法)**:

  | 场景                                         | 唯一写法                                                                      |
  | -------------------------------------------- | ----------------------------------------------------------------------------- |
  | web / ui-react / extension(Tailwind v4 类名) | `rounded-xs` ~ `rounded-2xl`;CSS 里 `var(--radius-<step>)`                    |
  | miniapp-taro / mobile-rn(NativeWind v3 类名) | 同上,**禁止** `rounded-[24rpx]` 这类任意值                                    |
  | RN StyleSheet / 任意内联 style               | `borderRadius: rnRadius.lg`(`import { rnRadius } from '@ihui/design-tokens'`) |
  | `.css/.scss/.html` 任何端                    | `border-radius: var(--radius-lg)`,禁止 px/rpx 字面量                          |

- **类名语义按 web 对齐(修文档漂移)**:v3 preset 曾把 `rounded-sm` 定成 2px、web v4 是 4px,同名不同值即"手机上圆角和全局不一致"的根因;现 `sm=4px`,2px 由 **`rounded-xs`** 承载。裸 `rounded` 全端统一 8px(web 现实)。旧条目里"`rounded-sm`(2px)"作废。
- **禁止绕档**:每文件自定 `const *_RADIUS = <数字>`(含 `BAR_RX` 这类**名字不带 RADIUS** 的圆角常量)、用 `rpx()` 算圆角、`rounded-[任意值]`、StyleSheet 里写数字字面量、**以及写字符串形态 `borderRadius: '8px'` / `"6px 6px 0 0"`**(字符串曾是守门盲区,2026-09-23 对抗排查补上;多值串须拆成四个角属性引用档位)。
- **SVG 圆角同样受管(B5)**:`rx` / `ry` 会产生圆角,JSX 内联 SVG 必须写 `rx={rnRadius.xs}`;**静态 `.svg` 资产**没有 JS 通道,取值须等于档位值(偏档就近吸附、等距取小),确属形状需要则加 `<!-- radius-exempt: 原因 -->`。
- **生成式 HTML/CSS 字符串**(cli 分享页、`packages/shared/src/design/design-templates.ts`、扩展 content script、api 的 `swagger-theme.ts` 等拼 CSS 文本处):**不得写死数字**,用同表插值 `border-radius: ${RADIUS_CSS_PX.md}`(`import { RADIUS_CSS_PX } from '@ihui/design-tokens'`),不新增第二份真相。
- **真圆/胶囊豁免**:头像 / 装饰点 / 红点 / 进度环 / Switch 拇指 / 半高胶囊输入框**不得方档化把形状改坏**。优先 `size / 2` 表达式;确需保留数值必须在同行或紧邻上行写 `radius-exempt: <一句话原因>`(JS/TSX 用 `//`,CSS 用 `/* */`,静态 .svg 用 `<!-- -->`),不得静默写死。
- **守门**:`scripts/check-radius-single-source.mjs`(guardian 第 **77** 项,blocking)三判据 —— A 档位表四处对账(改一处忘改另一处即红)、B 端内取用必须引用档位(B1 数值与字符串形态 / B2 本地常量 / B3 CSS 字面量 / B4 任意值类 / B5 SVG rx-ry / **B6 引用 `rnRadius`、`RADIUS_CSS_PX` 却没在本文件 import** —— 本门判 HEAD 而 `pnpm typecheck` 只跑 worktree,悬空标识符属于"两边都不红"那一类)、C 覆盖面对账(含圆角的跟踪文件要么落在 SCAN_DIRS,要么在 `OUT_OF_SCOPE` 写明为什么不适用)。**B 的棘轮锚点是「该文件 HEAD 版本自身的违规数」**,不是手工清单 —— 只拦"这次改动把绕档加回来了",不拦仓库既有债。2026-09-24 换锚的起因:并行会话的索引层重建把 309 个路径整文件回写成迁移前的旧基线(**HEAD 积累 1179 处**),静态清单对此完全绿灯;同时全量审计改判 **HEAD blob 而非工作树**,因为滞后的旧草稿会被按磁盘读误记成本仓债务(一道与真实改动无关的红门只会逼人 `--no-verify`,连带废掉全部守门)。`scripts/radius-single-source-baseline.json` 降级为人工兜底,**现须为空**(镜像测试钉死)。判据**不锚定行首**,故 `width:16px; border-radius:50%` 同行多声明、TS 里生成的 CSS 一样可见。按文件自验:`--files <a> <b>`;紧急跳过 `HUSKY_SKIP_RADIUS_GUARD=1`;自检 `--self-test`(49 例,含真实表端到端对账、静态/内联 SVG 两种语义、NaN 防回归、HEAD 锚点四方向正反例、多行 import 不误伤),镜像测试 `node --test scripts/tests/check-radius-single-source.test.mjs`(4 例,含"锚点必须是 HEAD 而非静态清单"的装车证明)。容器纯圆违规仍由 `scripts/check-rounded-full.mjs`(第 11 项)管,两条互补不互替。

### Button 高度档位守门(强制,2026-09-07 立)

- **`@ihui/ui-react` 的 `<Button>` 禁止用 className `h-*` / `w-*` 覆盖高度宽度**,必须用 `size` 档位:`xs`(h-7 px-3 text-xs, 28px)/ `sm`(h-8, 32px)/ `default`(h-9, 36px)/ `lg`(h-10, 40px)/ `icon-2xs`(h-7 w-7, 28px)/ `icon-xs`(h-8 w-8)/ `icon-sm`(h-8 w-8)/ `icon`(h-8 w-8)。需要新高度先在 `packages/ui-react/src/components/button.tsx` size 表立档,禁止逐处打补丁。**2026-09-21 修正文档漂移**:`icon-xs`/`icon-sm`/`icon` 三档当前**同值**(均为 32px,仅保留名称以兼容既有调用),28px 图标请用 `icon-2xs`;此前本文档误写为 h-7/h-8/h-9 三档不同 —— 这正是"按文档用 `icon-xs` 期望 28px → 不达预期 → 再以 `className="h-7 w-7"` 覆盖 → 触发本守门"违规链的成因。守门脚本(`check-button-height.mjs`)的档位清单已改为从 button.tsx **动态解析**,不再手抄。
- **豁免(不属 Button token 体系)**:原生 `<button>` 自绘按钮(IDE 面板 / spec-panel / chat 密集工具条的 24px 紧凑档为有意设计)、`Input` / `SelectTrigger` / `Skeleton` / 图标 svg 等非 Button 元素;Button 上的 `h-5`/`h-6`(24px/20px 紧凑档,存量 45 处表格行操作钮/侧栏密集场景)暂豁免,后续统一时先立对应档位再迁移。
- 守门:`scripts/check-button-height.mjs`(精确 JSX 开标签解析,零误报;拦 h-7 及以上覆盖 + 校验 size 值合法性)+ pre-commit blocking(紧急跳过 `HUSKY_SKIP_BUTTON_HEIGHT_GUARD=1`)。

### 中文字体 + 图标垂直对齐硬约束(强制)

- **根治方案**:`apps/web/app/globals.css` 设 `--text-vcenter-offset: 0.3px` + 全局规则 `:where(button, a, [role='button'], [role='menuitem']):has(>svg):has(>span) > span { transform: translateY(var(--text-vcenter-offset)); }`,button/a 内 "icon + 中文 span" 同行布局自动应用,text-xs (12px) 用专用 0.7px 规则。配套:`apps/web/src/lib/nav-styles.ts` 5 个共享类 + `<CenteredText>` 组件(`apps/web/src/components/common/CenteredText.tsx`)。
- **编码强制 — icon + 文字必须包 `<span>`(2026-09-15 组件层根治)**:根因是补偿规则 `:has(>span) > span` 只能命中 `<span>` 元素,裸文本节点永远无法对齐(复发 3 次以上)。**根治:`@ihui/ui-react` 的 `<Button>` 已内置 `wrapRawTextChildren`,裸文本 children 自动包裹 `<span>`,使用 Button 无需手写。** 手写原生 `<button>`/`<a>`/`[role=button]` 时仍**必须**自行把文字包 `<span>`(自检口诀:**button 里有 svg,则每个直接文字都写 `<span>文字</span>`**)。守门:`apps/web/e2e/icon-text-alignment.spec.ts`(阈值 |delta| ≤ 0.15px)+ code review 必查项。
- **守门**:`apps/web/e2e/icon-text-alignment.spec.ts` 阈值 |delta| ≤ 0.15px,漏改 → CI fail。**严禁** `-mt-px` / `margin-top: -1px` 反向微调 hack。
- **数字计数徽章强制规范(2026-09-15 立,复发根治)**:按钮/标签内嵌数字计数(如待办数、队列数)**禁止**裸 `<span class="px-1">数字</span>` 手搓 — 行高、字形侧空、位数变化都会导致数字在色块内偏移。**必须**使用确定性居中模板:`inline-flex h-4 min-w-4 items-center justify-center rounded bg-*/… px-1 text-[10px] font-semibold leading-none tabular-nums`。要点:`inline-flex + justify-center` 保证任意位数水平居中;`h-4 + leading-none + items-center` 保证垂直居中(不依赖字体行高);`min-w-4` 保证单位数不掉宽;`tabular-nums` 保证多位数字等宽不抖动。
- **flex 自居中 span 豁免 vcenter 补偿(2026-09-15)**:全局补偿规则(`packages/design-tokens/src/styles/tokens.css`)已改为 `> span:not([class~='flex']):not([class*='inline-flex'])` — 徽章等 inline-flex span 由 flexbox 自居中,整盒再被 translate 下推 0.3/0.7px 会"偏下"(用户实测复发)。**手写 flex 徽章 span 天然豁免,无需任何反向 hack;严禁给徽章再加 -mt/translate 反补**。

### 页头返回键单一源头(强制,2026-09-25 立)

- **起因(用户实拍)**:"小程序端所有返回按钮怎么是返回两个字"。web 端 2026-09-08 就把返回键收进顶栏唯一实现(`GlobalTopBar.tsx` 的 `TopBarBackButton`,lucide `<ChevronLeft />`,36×36 方块,守门 46 拦私接),**但小程序端没人跟着收口**:20 处把「返回」两个汉字当箭头渲染(各页自写 `tt('common.back','返回')` + 各自的字号/颜色)、8 处用字符 `‹` 当箭头。守门 102 当时的字符集只有右向 `›»→》` —— **一条门只管自己立项那一型,就是这一型的洞**(判据必须覆盖门自己产出的形态,同 §4 圆角那一条的教训)。
- **唯一写法**:小程序端一律 `<BackChevron onTap={…} />`(`apps/miniapp-taro/src/components/BackChevron.tsx`,内部 `LineIcon name="chevron-left"` 复用 `components/LineIcon/icons.ts` 既有素材 ⇒ 零新素材、零新色值;方块 72rpx=36px 与 web 的 36×36 同档)。返回动作(`navigateBack` / `switchTab` 降级 / 回登录页)语义**留在调用方**,组件只管外观。web/RN 侧对应写法是 `ChevronLeft`(lucide-react / lucide-react-native)。**禁止**在页头返回位写文字、写 `‹`,或端内自造第二种几何(负 margin 凑命中区、每页各自 `w-[80rpx]` 预留宽度 —— 后者会被组件行内 `width` 静默盖掉)。
- **文字标签的合法例外**:错误态卡片按钮、步骤回退链接这类**「返回」是按钮文案而非导航箭头**的位置保留文字,在**可点元素起始行或其紧邻上行**写 `back-label-exempt: <原因>`。它与 `glyph-arrow-exempt` 是两条独立通道,不得混用(给 GA1 开第二道口就等于没有那条锁)。带宾语的标签(`backHome` / `backLogin` / `prevMonth`)按判据结构上不纳,**不得为消红去改 i18n 文案**。
- **守门**:并入 102(见下一节末条),新增判据 **GA4** 与 S0 机制清单第 4 条(`components/BackChevron.tsx` 被摘线或无人 import 即红)。
- **其余三端的同一型(2026-09-25 收口;现值一律按当次实测取,勿照本行数字派单)**:`packages/app/src/features/**` + `apps/mobile-rn/src/screens/**` + `apps/extension` 的页头返回已全部收进 RN 侧唯一实现 **`packages/app/src/components/BackChevron.tsx`**(经 `@ihui/rn-app` 导出;lucide-react-native `ChevronLeft`,命中块 36×36 对齐 web `TopBarBackButton`)。**`colorScheme` 是必填 prop 而非 `= 'light'` 默认值** —— 守门 91 只把"带默认值的形参"认作静默脱主题开关,必填改由 `tsc` 强制,比门更严。HEAD 面实测 `<Text>{t('common.back')}</Text>` 仍有 42 处、`{'‹'}` 0 处:那 42 处是**错误态/空态卡片按钮、翻页、弹窗关闭** —— 那里「返回」是文案不是箭头,换裸箭头反而不表意,逐条带 `back-label-exempt: <原因> until YYYY-MM-DD`(该族已登记进守门 108 的 `FAMILY_LIFETIME_DAYS`,取 365 天:它是结构性定性不是待偿债务,短周期到期只会逼人删标记、删了又被 GA4 判红,两道门互咬)。**这两处"已知残余"已于同日由后续票收掉(2026-09-26 按 HEAD 复验;照旧执行会去找不存在的洞)**:① S0 现登记 5 条,含 RN 共享层那份 `packages/app/src/components/BackChevron.tsx`(该门注释原文即"只登记端内那一份时,共享层被摘线无人喊红");② 混合形态 `← 返回` 由新增判据 **GA5** 纳管,两处真站点的落地形态是 `ModelConfigDialog.tsx` **保留字形 + 带原因 `back-label-exempt`**(实测 `←`=1 / 豁免=1 / `ChevronLeft`=0 —— 正确处置是带理由豁免而非矢量化:该组件作用域内既无 `t` 也无 `tokens`,换矢量要改别人的组件签名)与 `Selecter.tsx` 删字形留文字 + 豁免注明"i18n 与矢量通道双缺"。

<!-- ⚠️ 本行是上面那条的**改写前原文**(2026-09-25 由提交 259fb1262d9 收口后已失效:其中"packages/app 223 处/168 文件""mobile-rn 5 处""共享层两个 NavBar 与 PayResultScreen 用 ‹"四项现值全变),照它派单会把已做完的 180 处重做一遍。保留只为不丢行(§12),勿照它执行。 -->

- **未收口的同一型(如实登记,不是已完成)**:`packages/app/src/features/**` 有 **223 处 / 168 文件**、`apps/mobile-rn/src/screens/**` 有 5 处仍是 `<Text>{t('common.back')}</Text>`;共享层两个 NavBar 与 `PayResultScreen` 用 `‹`。它们由 102 的**按文件 HEAD 棘轮**兜住"不得再加",清理另计一批。

> ⚠️ **上面那格的"223 处未收口 / 清理另计一批"已被 2026-09-26 的实测取代 —— 照它派单会去清一批不存在的东西。**
> 现读(HEAD 面,五个 UI 目录):`common.back` 共 **243 处**,其中 **194 处是 `label=` / `aria` / 配置字段**
> —— 那是喂给 `<BackChevron label={…}/>` 的**无障碍名称**,不是渲染出来的文字(设计要求:可见文案求箭头、
> 无障碍名称须脱离上下文成立)。剩下 **49 处**确实是 `<Text>{t('common.back')}</Text>` 这种"文字摆在按钮里",
> 但**逐条带 `back-label-exempt: <原因>`**(错误态/空态卡片按钮、翻页、弹窗关闭 —— 那里「返回」是按钮文案不是
> 页头箭头,换成裸箭头反而不表意,见本节"文字标签的合法例外"),抽查三处(`AgentDetailScreen.tsx:42`、
> `ActivityDetailScreen.tsx:50`、`miniapp plaza/detail/index.tsx:242`)标记都写在可点元素起始行的紧邻上行、
> 与判据要求的三个落点一致。所以守门 102 的 GA4 报 **0** 不是"判据没看见",而是"该收的收进了唯一实现、
> 该留的按理由留"。**这一批没有剩余清理项**;真要动这 49 处,得先逐条论证它其实是箭头位 —— 那属于改判据,
> 不属于清存量。`‹` 字符在 HEAD 面还剩 10 处,但都不在可证 affordance 语境(GA1 实判 0)。

### 区段头「更多」入口单一源头(强制,2026-09-24 立)

- **三端唯一实现**:RN = `packages/app/src/components/MoreLink.tsx`(经 `@ihui/rn-app` 导出);web = `apps/web/src/components/common/view-more-link.tsx`;小程序 = `LineIcon name="chevron-right"`(`components/LineIcon/icons.ts` 已含该图标,零新素材)。**禁止在任何端再自拼第四份**「标签 + 箭头」。
- **箭头必须是矢量图标,禁止用字符 `›` / `»` / `>` 充当**。字符与文字没有共用度量,`align-items:center` 居中的是各自行盒:实测「更」12px 墨迹偏上 1.0px,而 `›` 在 14/16/18/20px 档偏下 1.0 至 1.5px —— 是**相加**关系,所以"标签小、箭头大"的旧写法错位 2.0 至 2.5px。算式与完整根因见 `docs/UI_GUIDELINES.md` §4.6。
- **标签与箭头必须同一光学尺寸(RN/web 12px、小程序 24rpx)+ 同一缩放倍率**:旧写法箭头带 `allowFontScaling={false}` 而标签允许缩放,用户调大系统字号即分叉。RN 侧把倍率同时喂 `Text.maxFontSizeMultiplier` 与图标 `size`(clamp 1..1.4),并保留 `includeFontPadding: false`(Android 字体留白不对称,端内已有 8 处同写法)。
- **禁止反向微调补丁**:`marginBottom: -2` / `translateY` / 负 margin 凑数(与本节上方"严禁 -mt-px"同一条禁令,换机型即失效)。
- **文案统一 `common.more`**(更多 / More / もっと見る / 더 보기),不得再写"查看更多 / 查看全部 / 完整榜单";但 `accessibilityLabel` **刻意保留长口径**(如"查看更多模型")—— 可见文案求短,无障碍名称须脱离上下文成立。

<!-- ⚠️ 本行是守门 102 的**立项原文**(三类判据),已于 2026-09-25 被下方"四类判据"条就地改写取代;其中"字符集只有 ›»→》""三端唯一实现""--self-test 45 例"三项均已是旧值,照它派单会漏掉左向字形与 GA4。保留本行只为不丢行(§12),勿照它执行。 -->

- **守门**:`scripts/check-glyph-arrow-icon.mjs`(guardian 第 **102** 项,blocking)三类判据:**GA1** 整格字符箭头 `›»→》`(或 `{'>'}`)在可证 affordance 语境(自身或 ≤8 层祖先带 `on*Press/Click/Tap` 或 `role="button"`,或祖先为 `Link/Pressable/Touchable*/Button`)当图标;**GA2** 「更多」类标签与**同词干** `*Arrow*` 样式配对后箭头字号 > 标签字号(单位不同不判红,计入 undetermined);**S0** 三端唯一实现被摘线或无人 import(防"造好没装车")。口径同 77/83:全量判 HEAD blob、`--staged` 判索引、**棘轮锚点 = 该文件 HEAD 自身违规数**(行尾字符箭头存量 70 处只报数不拦,免得恒红逼人绕过钩子连带废掉全部门);行内豁免 `glyph-arrow-exempt: <原因>`(须带原因);取证 `--self-test` 45 例(含阳性对照与棘轮四向)+ 镜像测试 13 例。紧急跳过 `HUSKY_SKIP_GLYPH_ARROW_ICON=1`。

- **守门**:`scripts/check-glyph-arrow-icon.mjs`(guardian 第 **102** 项,blocking)四类判据:**GA1** 整格字符箭头 `›»→》‹←`(或 `{'>'}`)在可证 affordance 语境(自身或 ≤8 层祖先带 `on*Press/Click/Tap` 或 `role="button"`,或祖先为 `Link/Pressable/Touchable*/Button`)当图标 —— **左向 `‹`/`←` 于 2026-09-25 补入**(立项时只有右向四字,于是小程序 NavBar 与 7 个页面头拿 `‹` 当返回图标长期零判据);**GA2** 「更多」类标签与**同词干** `*Arrow*` 样式配对后箭头字号 > 标签字号(单位不同不判红,计入 undetermined);**GA4**(同日新增)整格子内容是「返回」类文案(`t('common.back')` 这类键名末段为 `back`/`back<数字>` 的 i18n 调用,或字面量 `返回`)且处在可证 affordance 语境 ⇒ 即"把返回两个字当返回箭头";**带宾语的标签刻意不纳**(`backHome`/`backLogin`/`prevMonth` —— 那些是按钮文案,换成裸箭头反而不表意);**S0** 共享唯一实现被摘线或无人 import(防"造好没装车"),现登记 4 条:三端「更多」箭头 + `components/BackChevron.tsx`。GA1 与 GA4 **共用同一遍遍历**(`walkAffordanceChildren`),两条对"可证 affordance"的定义必须同形 —— 各写一遍必然在栈深/祖先窗口上漂移。口径同 77/83:全量判 HEAD blob、`--staged` 判索引、**棘轮锚点 = 该文件 HEAD 自身违规数**(行尾字符箭头存量 70 处只报数不拦,免得恒红逼人绕过钩子连带废掉全部门);行内豁免**两条独立通道** `glyph-arrow-exempt: <原因>`(GA1/GA2,只认命中同行)与 `back-label-exempt: <原因>`(GA4,认命中行、可点元素起始行、及其紧邻上行)—— **宽严刻意不对称**且各由自检钉住:GA1 若也按块放行,一个标记就能救整棵子树,"一行救不了别处"那条反向锁就没了;GA4 若只认同行,人按直觉把标记写在可点块上就永远不生效(初版即如此,四处真实站点全部落空 ⇒ 恒红)。**原因不得由注释闭合符冒充**(裸标记 + `*/` 曾被读成"带了原因"⇒ 整行免检;该洞自 §22c 立项起就在 GA1 里,由 GA4 的自检反手抓出,现两条通道同锁,全仓零裸标记存量故改严无债务)。取证 `--self-test` 66 例(含阳性对照、棘轮四向、预筛必须是判据字面量超集的对账)+ 镜像测试 14 例(含"GA1/GA4 豁免宽严不得悄悄对齐"与"预筛漏字形=门对该形态全盲"两条锁)。紧急跳过 `HUSKY_SKIP_GLYPH_ARROW_ICON=1`。

### 禁止分割线(强制)

- 禁止 `<hr>` / `divide-y` / `divide-x` / 单边 `border-t/b/l/r` 当分割线。允许:容器完整描边(`border border-border`)、背景色对比(`bg-card` vs `bg-background`)、间距分隔(`gap-*`)。

#### 单边 border 的明确区分(解决两位开发者的分歧)

历史上出现两种冲突理解:一位开发者认为**所有** `border-t` 一律禁止;另一位认为显式 `border-t border-border/50` 一律允许。现统一为以下规则,**所有** `border-t/b/l/r` 用法必须归入下列两类之一,不得作为纯分割线使用:

- **允许(结构性边框,非分割线)**:
  1. 卡片/面板边缘:作为完整边框系统的一部分,例如带 `border` 的容器其 header 条用 `border-b` 作为容器底边(边框系统的收口,而非在两项之间画线);
  2. 引用块 / 语义强调左边框:`border-l-4` 等作为语义强调(如 blockquote、强调条),而非分隔两项内容的线;
  3. IDE diff 面板等编辑器内 chrome:有意为之的编辑器界面元素。
- **禁止(纯分割线)**:仅在**列表项之间**或**区块之间**用 `border-t/b/l/r` 画一条分隔线,此类场景必须用 `space-y-*` 间距,或 `bg-muted` / `bg-card` 背景对比替代。

> 注:低透明度单边边框(如 `border-t border-border/50`)是典型的"伪分割线",单独守门脚本 `scripts/check-no-divider.mjs` 对其作 **WARN** 告警(非阻塞),提示开发者确认是否为纯分隔用途;纯分隔线务必改用间距或背景对比。此后若确需单边边框,仅限上述"允许"三类情形。

### 禁止渐变遮罩(强制)

- 任何容器禁止 `mask-image` / `-webkit-mask-image` / `linear-gradient` 用作边缘淡出。用显式 UI 元素("查看更多"按钮 / 计数徽章 / 分页)替代。

### 禁用原生提示窗(强制)

- **禁止**使用原生浏览器提示:`title` 属性 / `alert()` / `confirm()` / `prompt()`。必须使用项目自有的 `Tooltip` 组件(`@/components/feedback`)统一提示样式。

### 图标统一用图标库(强制)

- **UI 图标一律用矢量图标库,禁止 emoji 充当图标。** 图标指导航项 / Tab / 按钮 / 状态指示 / 徽章 / 占位默认图标 / 关闭·发送·播放等操作图标 / 章节标题前缀 icon / 等级奖牌等界面元素。
- **web 端**:统一 `lucide-react`(`import { X } from 'lucide-react'`),渲染 `<X className="h-4 w-4" />`;配置数组里的 `icon: 'emoji'` 须改为 `icon: LucideIcon` 组件引用后在渲染处用 `<item.icon className="..." />`。
- **移动端(React Native)**:统一 `lucide-react-native`(`import { X } from 'lucide-react-native'`),渲染 `<X size={16} color={'#6b7280'} />`;颜色按原 `Text` 的 `text-*` 类或 style 颜色意图转十六进制,尺寸按原 fontSize(-xs→12 / -sm→14 / -lg→18 / -2xl→24)。
- **Taro 小程序端(miniapp-taro)**:统一 `apps/miniapp-taro/src/static/images/icons/` 下的 lucide 风格 SVG(24x24,stroke `#6366F1`),渲染 `<Image className="..." src="/static/images/icons/xxx.svg" mode="aspectFit" />`;动态切换用条件 src(如 `{liked ? heart-fill.svg : heart.svg}`);缺失图标用 `node scripts/gen-taro-lucide-icons.mjs <name>` 从 lucide 提取生成(别名如 bar-chart-3 自动解析)。
- **禁止**在 UI 图标位置使用 emoji(如 🏆 🤖 🔥 ✅ ❌ 🎤 🔔 等)。新增图标须先查 `lucide-react` / `lucide-react-native` / Taro `static/images/icons/` 是否有对应名称,无则用近义图标,不得回退 emoji。
- **例外(不强制)**:文档 / 营销页正文里的装饰性 emoji(提示 💡、技术栈列表、章节叙述中的 emoji)属于内容文案,不在此限;但同一页面应风格统一,优先用图标库。
- 守门:`scripts/check-no-emoji-icons.mjs` 扫描 UI 图标位 emoji(icon 字段 / JSX 渲染位 / 三元条件图标),豁免注释、i18n 参数、表情面板、`'★'.repeat` 评分字符串、表格布尔标记、docs/marketing 正文;guardian-runner 第 11h 项(pre-commit staged 模式阻塞 commit)。

### 品牌/平台图标必须是官方真实图标(强制)

- **展示第三方品牌 / 平台身份的图标(模型、推广平台、接入平台、社交渠道等),一律使用该品牌官方发布的真实图标,严禁手绘、自造、近似模仿或用字母占位图代替。** 手绘品牌图标属于品牌失真,被用户发现即为交付事故(2026-09 头条/WordPress/微博图标事故教训)。
- **官方图标获取链路(按优先级)**:
  1. 官网 favicon 直取:`https://<domain>/favicon.ico`;
  2. Google favicon 服务:`https://www.google.com/s2/favicons?domain=<domain>&sz=128`;
  3. 备源:`https://favicon.im/<domain>?larger=true`;
  4. 品牌官方 CDN / 官网 press-kit 资源。
     下载后必须人工目检(Read 工具查看图片)确认是官方真实图形,不是字母占位或第三方水印版。
- **存放与引用分离**:
  - 发布平台官方 favicon:`apps/web/public/publish-icons/`(引用方:`apps/web/src/components/publish/platform-icon.tsx` 的 `PNG_ICONS` 注册表);
  - 页脚/跑马灯 mono 白色剪影素材:`apps/web/public/footer/tuiguangpingtai/`(引用方:`apps/web/src/components/marketing/footer-data.ts`,带 `mono: true` 反色适配)。
    **两套素材禁止混用**:官方彩色 favicon 不得覆盖页脚 mono 剪影同名文件,反之亦然;新增图标先进 `publish-icons/`,不与 footer 素材重名。
- **深色模式可见性**:黑色系官方图标在注册表中标 `invertInDark: true`(深色下反色为白);彩色官方 favicon 无需反色,靠 `bg-primary/10` 容器衬托。
- **素材卫生**:被替代的旧图标文件必须同任务删除,不留孤儿资源;`.ico` 仅在拿不到 `.png/.svg` 时使用。
- 违反本条 = 未完成:用户要求"官方图标"时,交付物必须逐个可溯源到官方域名,可附来源 URL 清单。

### 圆角容器内 absolute 子元素避让

- 父容器 `rounded-xl` + `overflow-hidden` 时,贴边子元素**禁止** `h-full`/`w-full`,用 `top-<radius> bottom-<radius>`(纵向)或 `left-<radius> right-<radius>`(横向)替代。映射:`rounded-lg`→`top-2 bottom-2` / `rounded-xl`→`top-3 bottom-3` / `rounded-2xl`→`top-4 bottom-4`。
- 拖拽手柄用双层 div 结构(外层命中区 + 内层可见细线),**禁止** `before:` 伪元素方案。

### 浮动弹层内边距规范(2026-09-21 立,起因:context-usage-ring 弹层漏 padding 内容贴边)

新建弹层/浮层必须按内容类型对齐以下四档,**禁止自创中间值**:

| 类型          | 判定特征                                                            | 容器内边距                              | 参考实现                                                              |
| ------------- | ------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------- |
| 内容面板      | role=dialog,含标题/表单/信息块(permission、通知、设置卡)            | `p-3`                                   | permission-mode-popover、permission-history-panel、context-usage-ring |
| 菜单/选项列表 | role=menu / select 列表,纯按钮项                                    | `p-1`                                   | Select、TagsView 右键菜单、canvas-overlay                             |
| 轻提示/徽标   | tooltip、角标、状态胶囊                                             | `px-2 py-1`(mini 用 `px-1.5 py-0.5`)    | ByokIncomeChart、activity-bar                                         |
| 复合面板      | 输入行+列表的 overflow-hidden 面板(mention、selector、终端 overlay) | 外层零 padding,**行级自带** `px-3 py-*` | file-mention-popover、context-selector-popover                        |

- 反例存档:自定义 portal 弹层从 Radix 迁出时最易漏搬 `p-3`(PopoverContent 自带),新建 portal 容器必须显式写内边距档位。
- 同族组件必须同档:同一直弹层家族(如 permission 系列)不允许 p-2/p-3 混用。

### 跨端样式同步铁律(强制)

- web 与 miniapp-taro 视觉必须完全一致(除平台独占差异:登录页小程序端无、rem2rpx 自适应缩放、原生导航栏/tabBar 用 `Taro.setNavigationBarColor`/`setTabBarStyle` 而非 CSS var 等)。**任何一端改了样式/组件/主题,必须同步另一端**——这是交付门槛,不是可选项。
- **单一真相源**:design-tokens 在 `packages/design-tokens/src/styles/tokens.css`(`@theme` + `:root` + `.dark`)。miniapp-taro 的 `app.css :root/.dark` 由 `scripts/sync-miniapp-tokens.mjs` 自动同步(**禁止手改 app.css 的 token 块**;改 token 改源头 + 跑同步。2026-09-25 该生成器由端内 `apps/miniapp-taro/scripts/sync-design-tokens.mjs` **搬到根 `scripts/`**(旧路径已删,`pnpm --filter @ihui/miniapp-taro sync-tokens` 入口不变)—— 搬的理由是架构门 103 逼出来的:守门 36 与生成器必须共用**一份**取源实现,而取源住在 `scripts/lib/design-token-blocks.mjs`;端内脚本向上摸工具层是 D2 反向依赖(且 tooling 声明 `exported:false`),把实现下沉进 contract 包又让根工具层按路径穿透包实现细节(D3),而工具层**按包名解析不到 workspace 链接**(实测给根 package.json 加 `@ihui/design-tokens` 后 pnpm 写出的根链接是悬空的,门 78 当场判红,已回退)。摆在工具层 ⇒ 四个端的同名机制全在一处,零穿透零豁免。旧文档那句"根 `scripts/` 下没有同名文件"就此作废,照它跑 `node scripts/sync-design-tokens.mjs` 现在仍会报模块找不到 —— 新名字是 `sync-miniapp-tokens.mjs`)。另有 `scripts/check-miniapp-taro-design-tokens.mjs` 与 `scripts/check-miniapp-tokens-sync.mjs`(后者为 guardian-runner 第 36 项实际调用项)校验同步一致性。
- **主题系统**:miniapp-taro 主题根为 `ThemeRoot`(`@/components/ThemeRoot`,内部调用 `useThemeRoot()`),每个路由页 .tsx 顶层须 `<ThemeRoot>...</ThemeRoot>`。设置页切换主题必须调用 `@/lib/theme` 的 `setThemePreference`(同步原生导航栏/tabBar 配色 + 广播事件),**禁止**只用 `Taro.setStorageSync('theme', ...)` 而不同步原生 chrome(否则导航栏/tabBar 不变色)。
- **禁止深色科技风回潮**:app.css 不得再出现 `page{background:#121217}` / `*{font-family!important}` 等全局深色强制覆盖;页面/组件 CSS 不得硬编码禁用色板(`#00f2ff`/`#121217`/`#1f1f28`/`#1a1a2e` 等),一律改用 `var(--color-*`)。
- **禁止同名工具类冲突**:miniapp-taro 的 `app.css` 不得重定义 `.text-primary`/`.mt-*`/`.flex*`/`.align-*`/`.justify-*` 等与 web 端 Tailwind 同名同义类(语义冲突),局部样式用语义化类名 + `var(--color-*`)。
- **端内 CSS/色值副本一律是派生态,禁止手改、也禁止"只加拦红不写回"**(2026-09-25 立,由实测逼出):`packages/design-tokens/src/styles/tokens.css` 是唯一色值源;所有 v3 端副本(`apps/miniapp-taro/src/app.css` 的受管块、`apps/mobile-rn/global.css`、`packages/design-tokens/src/rn-tokens.ts`、`tailwind-alpha-plugin.js` 的 `ALPHA_USAGE`)必须由 `scripts/lib/design-token-blocks.mjs`(取 tokens.css 的**唯一实现**)+ 各自的 `scripts/sync-*.mjs` 派生,并挂在 `scripts/lib/pre-commit-hook.js` 的 `TOKEN_SYNC_TARGETS` 表上按**各自触发面**自动写回(token 类看 tokens.css,用量类看 v3 三端源码)。两条不可违反的写法:**① 原位写回,禁止整块替换** —— 副本里非受管的端内档(如 `global.css` 的 `.dark` 那批 `--rn-*`)与解释性注释必须逐字留在原位,值已等价时连字节都不改(这才是幂等);实测旧写法整块替换一次就抹掉 13 行在用深色档 + 6 段注释,那正是 RN 侧自动同步长期只拦红不回写的真实原因(接上必炸)。**② 取源只能有一份实现** —— 生成器与守门各写一遍取值逻辑必然不同形(实测:生成器只取 `@theme` 首个非贪婪块,漏掉后续 `:root` 块里的 `--color-*-rgb` 三元组,一次"同步"就删掉守门 93 R6 要求必备的 alpha 通道)。**新增跨端档位的正确顺序**:改 tokens.css 一处 → 跑对应 sync 脚本(或让提交链自动跑)→ 由门复核;不得在任何端手写色值、色名常量或第二张登记表。
- **小程序端有第 6 个受管区 = `app.css` 的 `@theme` 色档注册区**(2026-09-25 立,`f17b5fbdda`):它不是"又一份色值表",而是 Tailwind v4 的**注册动作** —— v4 只把 `@theme` 里的 `--color-*` 认作主题档,色值若只落在普通 `:root/.dark` 上,`bg-muted`/`text-foreground` 这一整族 utility **一条都不产出**(实测真构建 0 落地)。档位仍**唯一派生自** `deriveMiniappManaged(tokens.css, 'light')` 再剔掉 `-rgb` 三元组 —— `-rgb` 进 `@theme` 会长出 `.bg-muted-rgb` 这类脏档(v4 见 `--color-*` 就当颜色档)。三条不得照抄的形态选择:**只用普通 `@theme { }`**;`@theme inline` 会把亮档值烘进每条规则 ⇒ 深色档整族失效;`@theme reference` 会给每条规则附一份亮档 fallback(更贵,且把"没定义"洗成"默默用亮档")。守门 36 现同时核 `:root` / `.dark` / `@theme` 三面(无缺档 + 逐位同值 + 反向拦 `-rgb` 脏档)。**同批判死一条判据缺陷**:`check-miniapp-css-landing` 的 C1 原拿源类名直比产物类名,而 weapp-tailwindcss 会把类名标点转写(`[→_b ]→_B /→_f :→_c (→_p )→_P ,→_m .→_d +→_u`),于是 505 条"缺项"里 474 条是比对噪声;现按同表**正向**转写后再比,且分"原名直中/转写后中"两态计数(合计会把"表漏一条"藏起来)。**判落地的门如果比错形态,症状不是差一点,而是把真信号淹在噪声里** —— 上一轮正是靠"缺项恰好成族"才定位到色档整族没落地。
- **Tailwind v3 消费端的 `/alpha` 修饰符由共享 preset 负责**(2026-09-25 立):v3 的 `bg-<色>/<透明度>` 语法要求把颜色拆成 rgb 分量,而 preset 若只给 `var(--color-x)` 单值,这一整类类名在 miniapp-taro 端**根本不产出 CSS** —— 页面"样式没生效"却零报错,而 web/extension(v4,原生支持)一切正常,即"手机上改了 web 没改"的又一成因。修法唯一:`packages/design-tokens/src/tailwind-alpha-plugin.js` 为每个颜色键补 `<key>-rgb` 三元组并 `addUtilities`,挂在该 preset 的 `plugins` 上,**禁止**在端内自拼 `rgba(...)` 绕过。**新增颜色档必须同时进这个插件**,否则新档又缺 `/alpha`。**这条散文自 2026-09-25 起是机器判据,不再是约定**:守门「跨端色值 token 漂移对账」新增 **R6**(`scripts/check-cross-end-tokens.mjs`,编号以 runner 现值为准)扫三个 v3 消费端源码(`apps/miniapp-taro/src` + `apps/mobile-rn/src` + `packages/app/src`)里真实写过的 `bg|text|border-<档>/<数值>` 类名,逐条要求 ① 该形态在 `ALPHA_USAGE` 登记过、② 插件的 `buildAlphaUtilities` 真产出那个选择器、③ 每档在 `tokens.css` 有 `<key>-rgb` 三元组;反向也判 —— 登了却没人用的行按"清单腐烂"红(每条登记 = 主包里一条死规则)。判不出的形态(动态拼接类名)与默认色板(`bg-white/50`,v3 原生支持)只报数不判红;行内豁免 `alpha-plugin-exempt: <原因>`,须带原因且只救本行/紧邻上一行。口径同 77/83/98:全量判 HEAD blob、`--staged` 判索引⊕HEAD,**登记表与用量必须同面**(表读磁盘 + 用量读 HEAD 会在并行会话刚补行的瞬间产出假红/假绿);腐烂判据在 staged 面走**棘轮**(存量只计数不判红),否则一枚与之无关的提交会被别人欠的债钉红 → 逼人 `--no-verify`。取证:真 miniapp 源码上选择器 73 → 91、REMOVED 0 / ADDED 18(含任意值形态 `bg-muted/[0.12]`);NativeWind 侧装 release 包 v21→v22 逐指标 A/B 六项读数完全相同(零回归)。R6 上线首跑即抓出 `bg-muted/40` 是**从注释文本里 harvest 出来的假用量**(该文件头注的用量统计当时没剥注释),已删该行,现 17 形态。
- ⚠️ **上面那条 `/alpha` 判据目前到不了小程序产物**(2026-09-25 实测,登记于 PROJECT_PLAN O62附②/附③):源码用到的 **687 个 Tailwind utility / 11,105 处用法**,在真实构建(`pnpm --filter @ihui/miniapp-taro build`,exit 0)的 154 个 wxss 里 **0 个产出规则**(`.flex{` `.items-center{` `.rounded-xl{` `.bg-muted{` `.text-sm{` 逐条为 0);而 **base/preflight 层是在的**(`border:0 solid;box-sizing:border-box;margin:0;padding:0`)⇒ 症状精确到"**只有 utilities 层为空**"。阳性对照:同一份 dist 里 `app.css` 手写类名 75/98 可查到规则。**机制不是** `config/index.ts` 那行 `tailwindcss: { enable: true, config: {} }` —— 实测把它改成真实配置路径后 A/B/C 三次构建产物**逐字节相同**(该键不是开关);真因指向 weapp-tailwindcss 5.2.9 **自带一份 vendored tailwind**(`dist/tailwindcss-*.js`)并由它自己的 generator 出 CSS,项目 `tailwind.config.ts` 的 content globs 没有喂进那条链。**后果**:本条 R6 判据守的是"源码用量 ↔ 登记表 ↔ 插件产出"三者一致,这三者都在**项目侧**;小程序实际产物由第三方 generator 决定,R6 的绿灯**不等于**到端生效。改这行配置的爆炸半径见 O62附③(主包余量 42,401 B 而 utilities 约 42,392 B,以及 `text-card` 同名双义会被注入白底白字)。**⚠️ 本条"0 个产出规则"的前提已于同日作废**(2026-09-25 深夜,`edb225c909` + `f17b5fbdda`):真产物落地率现测 **740/771 = 96.0%**,命名档与项目色档都已产出,主包余量 **113,504 B**;当年"整端一起变"的半径已被拆成两步分别验证。现行事实与判据见本节末"v4 色档注册区"条与 PROJECT_PLAN O62 附⑧/附⑨,勿照本行执行。
- ⚠️ **v3 端禁止裸 rpx 长度写进 `text-` / `border-` 任意值**(2026-09-25 立,守门 93 新增 **R7** 是它唯一的判据):`text-[28rpx]` / `border-[2rpx]` 会被 Tailwind v3 解析成**颜色属性**(`color: 28rpx` / `border-color: 2rpx`),整条声明无效且不报错。决定对错的是**单位不是前缀** —— `px`/`rem`/`em` 在 v3 下本就正确落 `font-size`/`border-width`,**不得**给它们加 `length:`(纯噪音);只有 rpx 这一族坏,因为 v3 的类型推断不认 rpx。正确写法:`text-[length:28rpx]`。颜色形态 `text-[var(--color-x)]` / `text-[color:…]` 不受本条约束。存量 1,331 处已清零,故 R7 在 HEAD 上是零容忍。
- ⚠️ **但 `apps/miniapp-taro` 实际跑的是 Tailwind v4.3.3,不是端内 `package.json` 声明的 v3**(2026-09-25 实测,详见 PROJECT_PLAN O62附⑤):`weapp-tailwindcss@5.2.9` 引用的是 **`@tailwindcss/postcss`**(v4 专属入口 —— v4 的 main 导出会**直接报错拒绝**被当 postcss 插件用),产物 base 层也带 v4 指纹(`border:0 solid` 合并写法、`--tw-gradient-position`,且无 v3 独有的 `--tw-bg-opacity`)。**取版本别只读 `package.json`** —— 那样会得到相反结论(本会话就错过一次:用 `resolve('tailwindcss/package.json')` 探到 `MODULE_NOT_FOUND` 就否掉了正确说法,真实依赖名是 `@tailwindcss/postcss`)。两条后果:① 上面 R7 拦的裸 rpx 错属性在 **v3 与 v4 上行为一致**(已用 v4 + 真实源码逐字复测),所以判据与那 1,341 处改写**不因版本换人而作废**;② 但"miniapp 的 `/alpha` 靠 `tailwind-alpha-plugin`"这个前提**尚未证实** —— v4 走 `color-mix` 是另一条路,插件的确定价值在 **mobile-rn(NativeWind v3)**。带真 theme 复测前,**不要把 R6 对 miniapp 那一侧的登记要求当成已证事实扩严**。
- ⚠️ **上面两条 `/alpha` 相关规则的实际适用范围已收窄,读的时候带上这条**(2026-09-25 实测,详见 PROJECT_PLAN O62附⑥):守门 R6 的"必须登记进 `ALPHA_USAGE`"只对**真 v3 消费端**(`apps/mobile-rn/src` + `packages/app/src`)生效 —— `apps/miniapp-taro` 跑的是 v4(见上一条),而 **v4 对 `/alpha` 是原生的**(带真 `@theme` 复测 7/7 命中,含项目自定义色与 `[0.12]` 这类任意值,声明体 `color-mix(in srgb, …)`)。收窄后量到的结论要如实说:**`tailwind-alpha-plugin` 至今零真实使用者**(全仓 64 处 `/alpha` 用量全在 miniapp,登记表已按用量归零为 `{}`),连 `--color-*-rgb` 三元组变量也**只有注释在引用、产物 0 处使用**。**别把它当已交付能力引用** —— 它是等 mobile-rn 真写 `/alpha` 时才生效的机制。要扩面先跑 `node scripts/sync-alpha-usage.mjs`(表由它导出,**不要手改**);R6 报"未登记"时的修复出口也是这条命令,不是"去 ALPHA_USAGE 加一行"。

- **守门**:`scripts/check-miniapp-taro-style-parity.mjs`(RULE-1~5:禁用色板 BLOCK / 其他 hex WARN / app.css 回归 BLOCK / 路由页 ThemeRoot BLOCK / tsx 内联 hex WARN / 已删除装饰类复用 BLOCK),接入 pre-commit(`HUSKY_SKIP_MINIAPP_PARITY=1` 跳过)+ `pnpm check:all`。新增路由页忘挂 `ThemeRoot`、改回深色科技风、复用已删装饰类,均会阻塞提交。

---

## 5. 后端约束

- Drizzle ORM 0.38 + postgres-js。用 Zod 校验请求参数。复用 `packages/auth` 的 authenticate 函数;admin 路由用 preHandler 统一校验(roleId >= 1)。幂等操作用 `onConflictDoNothing`。slug 从 name 自动生成。API 响应统一 `{ code, message, data }` 格式。

### 鉴权面公开化必须显式列举(强制,2026-09-23 立)

- **禁止**用兜底正则把 `/api/<前缀>/[^/]+` 这类"参数路由"形态当作公开面 —— 它会连同**静态子路由**一起放行。实测 `agents.ts` 的 `^/api/agents/[^/]+$` 让 `/agents/health` 游客可访问,而 `/agents/need-tasks` 的 handler 依赖 `request.userId`,游客走到它不是 401 而是 **500**:fail-open 直接崩在鉴权层后面,比 401 更难发现。
- 正确做法:**显式白名单列路径**(如 `/api/agents`、`/api/agents/list`、`/api/categories/list`),详情路由用正则时**必须**配一张静态段排除表(`AGENTS_PROTECTED_STATIC_SEGMENTS`)。**新增 `/agents/<静态段>` 的 GET 路由时必须同步登记该表**,否则会被当成游客详情放行。
- 游客视图的公开数据由 handler 自证:强制 `status=published` + `sanitizePublicAgent` 脱敏,并且测试要断言"未发布读不到 + 脱敏字段不出现",不得只断言 200。
- 改动 router 鉴权面前必须做**影响面核查**:全仓 grep 该路径(含 `packages/` 与各端)确认没有未登录调用方;本仓这两个端点的实际调用方为 0。
- 部署侧 nginx 与蓝绿 nginx 是两份配置:边缘限流(`limit_req_zone` / `limit_req_status 429` / `error_page 429`)改一处必须同步另一处,docker 侧 zone 名须带 `docker_` 前缀以免与 `deploy/nginx/conf.d/*.conf` 重名(Nginx 同 http 上下文重名 zone 会**启动失败**)。静态自检:`apps/api/tests/o5-nginx-edge-ratelimit.test.ts`。

### 测试隔离铁律(强制,2026-09-12 立)

- **测试一律禁止连生产库**。pytest 用例不得对生产 PostgreSQL(`8810`)/ Redis(`8811`)产生任何写入副作用;需要 DB 的路径必须 mock 或注入隔离实例。
- 共享连接池(fastapi 侧 `app.core.db_pool.get_shared_pool`)是唯一生产入口,**所有**用例必须通过 `monkeypatch` 替换为 mock,不得直接 acquire。
- autouse fixture 只兜住"默认安全",用例体内若先于 `monkeypatch` 触发真实写入(如 `TestLoadActiveTests::test_duplicate_skill_skipped` 在 patch 前 `await create_test(...)`),仍会污染库 —— 新增用例必须自查执行顺序。
- 回归事故:`agent_ab_tests` 曾累计 17 行 `skill-a` 垃圾数据,并使 `len(stopped)==2` / `totalTests==3` 断言漂移。修复落点为 `apps/ai-service/tests/conftest.py::_isolate_ab_test_db`。
- 自检:<全量回归跑两次> + <跑前跑后 `agent_ab_tests` 行数不变> 才算通过。

---

## 5b. `.git` 存续治理(强制,2026-09-12 立)

**背景**:本机宿主 safe-delete 层会**整体删除**工作区 `.git`,累计触发 15 次(commit/husky、rebase、filter-repo、`reset --hard`、`git stash create`、本机直推 Gitee/GitCode 后 5s 窗口)。`CODEBUDDY_SAFE_DELETE_ENABLED=0` **已证明防不住**。

**现状结构(不可改回)**:

| 路径                                                                                                                                                                                                                                                                       | 角色                                                        |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `D:/IHUI-AI/.git`                                                                                                                                                                                                                                                          | **28 字节指针文件**(`gitdir: D:/IHUI-AI-git-repo`),不是目录 |
| `D:/IHUI-AI-git-repo`                                                                                                                                                                                                                                                      | 真实 gitdir(544MB),在工作区之外                             |
| `D:/IHUI-AI.git-backup-20260912`                                                                                                                                                                                                                                           | gitdir 完整备份,守护的本地恢复源                            |
| **守护**:计划任务 **`IHUI-AI git-guardian`**(每 2 分钟)→ `"C:/Program Files/nodejs/node.exe" D:/IHUI-AI/scripts/git-guardian.mjs`。本机**未安装 nssm**,故未采用 `--daemon` 常驻服务形态,改用脚本自带的 schtasks 兜底(2026-09-12 15:30 实测启用;16:12 实测**自愈已生效**)。 |
| 分层自愈:`指针 → 环境 → HEAD 语法 → 嵌套 ref → 本地备份 → 远端`;每步破坏性覆盖前先归档现场。实测自愈 **0.9s**(refs 自愈实测 **0.14s**)。                                                                                                                                   |

**守护**:计划任务 **`IHUI-AI git-guardian`**(每 2 分钟)→ `"C:/Program Files/nodejs/node.exe" D:/IHUI-AI/scripts/git-guardian.mjs`。本机**未安装 nssm**,故未采用 `--daemon` 常驻服务形态,改用脚本自带的 schtasks 兜底(2026-09-12 15:30 实测启用;16:12 实测**自愈已生效**)。
分层自愈:`指针 → 环境 → HEAD 语法 → 嵌套 ref → 本地备份 → 远端`;每步破坏性覆盖前先归档现场。实测自愈 **0.9s**(refs 自愈实测 **0.14s**)。
**守护**:计划任务 **`IHUI-AI git-guardian`**(每 2 分钟)→ `"C:/Program Files/nodejs/node.exe" D:/IHUI-AI/scripts/git-guardian.mjs`。改用脚本自带的 schtasks 兜底(2026-09-12 15:30 实测启用;16:12 实测**自愈已生效**)。

> **本句原先还写"本机未安装 nssm,故未采用 `--daemon` 常驻服务形态" —— 该前提已失真并于 2026-09-24 更正**:实测 `C:\Windows\System32\nssm.exe` 在位(331264 字节),且存在 NSSM 服务 **`IHUI-GIT-GUARD`**(`git-guardian.mjs --daemon`,进程自 2026-09-15 08:48:42 常驻),与上面那条每 2 分钟的计划任务**同时运行**。即"守护"当前是**双执行体并存**(常驻 daemon + 2 分钟 tick)。本票只如实登记、不动任何一侧:砍哪一侧属 `.git` 存续决策(§5b 的分层自愈依赖"每 2 分钟必有一次巡检"来兜底,而 daemon 侧是否等价覆盖尚未取证),且它**没有任何到人出口**(实测 `grep -niE "mail|smtp|resend|notify|ftqq|sct|pushplus|alert" scripts/git-guardian.mjs` 零命中),故不影响 §5e 通道口径。
> 分层自愈:`指针 → 环境 → HEAD 语法 → 嵌套 ref → 本地备份 → 远端`;每步破坏性覆盖前先归档现场。实测自愈 **0.9s**(refs 自愈实测 **0.14s**)。
>
> **本节三条"机器事实"于 2026-09-24 在 `G:` 那份 checkout 上被逐条测反,照抄会误诊(与 §15b 的盘符补注同族)**:
> ① **活 gitdir 就是工作区内的 `G:\IHUI-AI\.git`(实体目录 2.4GB,不是 28 字节指针)**,`refs-manifest.json` 在它里面;而 `G:\IHUI-AI-git-repo`(810MB)**没有 manifest**,是无人读取的残壳。守护自评 `node scripts/git-guardian.mjs --status` 对当前形态给 `pointerOk:true / gitdirOk:true`。⇒ §5b"必须外置 + 指针"与 §12d"禁止迁出工作区/改指针(**并行会话的恢复逻辑会把指针文件当损坏清除**,2026-09-10 立)"这两条在本机**互斥**;本票只如实登记、**不动任何一侧**——把 `.git` 迁出去是一次破坏性迁移(会连带移动 2.4GB 对象库并撞上清理层),不是"修复文档与现状不符"。
> ② `git remote get-url origin` 实测 **`ssh://git@ssh.github.com:443/IHUI-INF-AI/IHUI-AI.git`**,且 `ls-remote` / `fetch` / 后台 `git-push-guard` 推送全通;上方"origin 实测为 HTTPS `github.com`,`ssh://git@ssh.github.com:443` 形态在本机从未成立"在这一台机上**不成立**。
> ③ `git config --local http.proxy` 实测 **unset**;上方"本机已另配仓库级持久代理(因为钩子进程不继承 shell env)"在这一台机上**不成立**(ssh-over-443 通道不需要它)。
> **口径(本条的真正约束):凡「盘符 / gitdir 形态 / 远端 URL / 代理」四类,每次使用前按当次实测取值**,不得把另一台机的现状当本机事实写进判据,也不得据此推断"通道坏了"。 —— **本行 2026-09-24 被并发旧基线回写带走过一次,同日原地补注使其重新成为"本侧修改"**

### 嵌套 ref 存续(2026-09-12 立,与 `.git` 同源问题)

**现象**:`refs/remotes/origin/main` 反复变 `[gone]`、`refs/tags/backup/push4-*` 反复"仅远端" → 守门 `30a`(commit 丢失防护)**抖动性阻塞**(刚 fetch 完绿,宿主一清理又红)。曾误判为"真的丢 commit"。

**机理(对照实验,可复现)**:宿主清理层删除 gitdir 下 **depth ≥ 2** 的嵌套命名空间目录:

| ref 路径                                       | 层级 | 结果                   |
| ---------------------------------------------- | ---- | ---------------------- |
| `refs/heads/main`                              | 1    | ✅ 存活                |
| `refs/tags/nightly-*`                          | 1    | ✅ 存活                |
| `refs/remotes/origin/main`                     | 2    | ❌ 目录被删 → `[gone]` |
| `refs/tags/backup/push4-*`、`refs/tags/<ns>/*` | 2    | ❌ 目录被删 → "仅远端" |

且 `git update-ref` 对这类嵌套 ref **返回 0 却不落盘**(静默失败)—— 所以"fetch 成功"不等于"ref 存在"。

**边界精化(2026-09-15 受控实验二次实证)**:

- `git update-ref` 对 **refs/heads 一级松散 ref 可靠**(新建 `refs/heads/<name>` 与覆盖既有 loose 均真实落盘);静默失败**仅限 depth ≥ 2 嵌套命名空间**(`refs/remotes/**`、`refs/tags/<ns>/**`)。
- 假成功链路:update-ref 先成功创建嵌套目录(如 `refs/remotes/origin/`)→ ref 文件被秒清 → 同命令内紧接的 `rev-parse` 读回 `packed-refs` 旧值。**"读回旧值"不代表 packed-refs 被改过**,勿朝 packed 损坏方向排查。
- bash 直写(`printf >` / `cp`)与 bash `mv` 对 gitdir 任意路径均真实落盘——清理层只针对 git ref 命名空间,不是文件系统级拦截;`git pack-refs --all --prune` 重写 `packed-refs` 本身可靠。
- 因此**对齐 `origin/main` 的唯一正解**:`git fetch origin main` → `node scripts/git-refs-heal.mjs`(FETCH_HEAD 权威值 → 固化 packed-refs)。**禁止**手工 `git update-ref` / 松散直写 `origin/main`(写了也会被清,且假成功会误导排查方向)。

**解法(载体替换,非 workaround)**:把嵌套 ref 固化进 `packed-refs`(gitdir **顶层单文件** → 清理不到),期望值另存 `refs-manifest.json`(同为顶层文件)。松散文件被删也照样解析。

- 期望值清单:`<gitdir>/refs-manifest.json`(含 `refs/remotes/origin/main`、`refs/tags/<ns>/*`)
- 固化:`git pack-refs --all --prune`(松散文件被 prune,只剩 packed)
- 手工命令:

  ```bash
  node scripts/git-refs-heal.mjs                   # 离线:按清单重建缺失 ref + 固化(无需网络)
  node scripts/git-refs-heal.mjs --status          # 只看健康
  node scripts/git-refs-heal.mjs --refresh-remote  # 联网:从 origin 校准全量 tag/heads 后固化
  ```

- 守护已在每个 tick 检查 `refsOk`,缺失即离线重建并写审计日志 —— **一般无需人工介入**。
- **`origin/main` 以 `FETCH_HEAD` 为准**:fetch 写入的松散 remote-tracking ref 在 **1 秒内**即被清理,
  若 `packed-refs` 残留旧值,同 sha 也会显示 `## main...origin/main [ahead 1]`。
  修复器已内建该规则(`git-refs-heal.mjs` / 守护 `healRefs()` 均读取 FETCH_HEAD 权威值)。
- **禁止**把嵌套 ref 的存续寄托在松散文件上(必被清理);**禁止**用 `HUSKY_SKIP_COMMIT_LOSS_CHECK=1` 绕过 30a —— 先跑 `git-refs-heal.mjs` 判定是真丢 commit 还是 ref 抖动。

**铁律**:

- **禁止**把 `.git` 改回目录形态:`git init` 必须带 `--separate-git-dir=D:/IHUI-AI-git-repo`;不带参数重建会把 544MB 实体拉回工作区,直接暴露给宿主删除。
- **禁止**删除/清理 `D:/IHUI-AI-git-repo`、`D:/IHUI-AI.git-backup-20260912`,以及两目录的 `*.broken-*` 归档。
- **现场归档一律落在 `gitArchiveDir()`(现 `D:\DevEnv\backups\git\`),手工抢修也不得例外**:把 `.git*` 的归档 `mv` 到**工作区根目录**等于把恢复现场送回宿主清理层的射程内(§5b 背景),而 §15b 已把项目根排除在备份落点之外。2026-09-24 03:13 一次手工 `mv` 就在一级目录留下 7 项 `.git.broken-remote-*`(3.2MB refs 快照 + 4 个 24 字节 `gitdir: G:/...` 旧指针),由守门 44(根目录整洁,blocking)检出后收口 —— 判据本身有效,缺的是"人工路径也必须走 `gitArchiveDir()`"这句。
- **禁止**手工 `git init` 抢修`:git` 消失 —— 先等守护(≤2 分钟;急可 `schtasks /run /tn "IHUI-AI git-guardian"` 立即触发),再查 `.workbuddy/git-guardian.log`(健康时**不写行**,别把「无日志」当没跑)。
- git 调用**不得依赖环境**:脚本一律 `execFileSync(<绝对路径 git>, ['-c','safe.directory=*', ...])`;服务账户(LocalSystem)与交互账户的 `safe.directory` 互不相通。
- **提交/推送**:**本机就是生产机**(`IHUI-API` 的 nssm `AppDirectory=D:\IHUI-AI\apps\api`、`IHUI-DEPLOYLOOP` 的 `AppDirectory=D:\IHUI-AI`,部署日志实测)。**【2026-09-24 深夜就地更正:这句在本侧 checkout 不成立 —— 用户明确指正"本机不是生产机,是开发机",三重实测印证:`Get-Service -Name IHUI*` 计数 0、`Win32_Service` 按 `DisplayName -Like *IHUI*` 零条、`Get-NetTCPConnection -State Listen` 对 8801/8802/8803/8810/8811 全部零命中(web/api/PG/Redis 无一在跑),而 `schtasks /fo CSV` 里仍有 16 条 `IHUI*` 计划任务 —— 形态是"任务在、服务与端口不在"。与 §5b/§15b 同族:上面那串 nssm `AppDirectory` 是 `D:` 那份 checkout 的历史事实,不得当本机现值照抄。后果是实打实的:本会话有 agent 据此把 D20/D29 的建表迁移按住,理由是"部署环会自动应用迁移=生产爆炸半径",该理由在本机不存在。口径:凡"要不要因为怕影响生产而暂缓"的判断,**先实测服务与端口**,查不到=开发机,按常规动作走;但反过来也别把"没有服务"读成"随便动" —— 无 PG 端口意味着迁移在本地连应用与验证都做不了,验收只能走守门 49 的离线判据(B1–B5)并明写"`--db` 模式本机不可用",不得让绿钩子冒充"迁移已验证"。取证陷阱:`powershell.exe -Command "... \$_.Name ..."`(双引号+转义)会**静默返回空**,看似"什么都没找到";判"零命中"必须换单引号包整段命令复测,否则把工具失效当成了事实。】**本行曾被 `978480078d0`(converge 的 worktree-preserving 合并)整文件回写吞掉一次,同日原地重打。`origin` 实测为 **HTTPS** `https://github.com/IHUI-INF-AI/IHUI-AI.git`,且 **HTTPS 直连 `github.com:443` 时通时不通**(同一分钟内 `git fetch` 可成功而 `git push`/`git ls-remote` 连拒 4 次)。唯一稳定通道是**本机代理 `http://127.0.0.1:7897`** —— `deploy/win/ihui-deploy.ps1` 每轮就是用它 fetch(日志行 `git 网络走代理 http://127.0.0.1:7897`),所以"部署环能取到远端而 agent 推不动"不是玄学,是**没走代理**。旧文档写的 `ssh://git@ssh.github.com:443/...` + 仓库级 `core.sshCommand` 形态**在本机从未成立**:`core.sshCommand` 未设置,`ssh.github.com` 握手可通但 `~/.ssh/id_ed25519`、`id_remote_control` 逐把 `git ls-remote` 均 `Permission denied (publickey)`(无任何已登记公钥,登记属账号侧动作)。⚠️ `~/.git-credentials` 在本机**并不存在**,不要按它取 token(HTTPS 走的是凭据管理器)。**agent 需要 git 网络时的两种写法**:临时用 `export http_proxy=http://127.0.0.1:7897 https_proxy=…`(或 `git -c http.proxy=…`);**本机已另配仓库级持久代理** `git config --local http.proxy/https.proxy = http://127.0.0.1:7897`(2026-09-23 立),因为 post-commit 的 `git-push-guard` 与其后台 worker 是钩子派生的 git 进程、**不继承任何 shell 的 env**,没有仓库级配置就仍然直连撞墙(此前"commit 成功、push 反复 failed"的真因即此)。该配置落在指针文件指向的 `D:/IHUI-AI-git-repo/config` 里,**不在工作树内 ⇒ 不受"旧基线整文件回写"影响**,也不会被并发会话抹掉;换网或代理关停时用 `git config --local --unset http.proxy` 撤销即可。新克隆/新机需重跑一次这条配置。本机**不直推 Gitee/GitCode**,交给 `mirror-to-cn.yml` 镜像收敛。自 2026-09-12 15:30 起 gitdir 已移出工作区(仅剩 28 字节指针),本地 git 写操作不再暴露给宿主批量删除层。
  - **🚫 agent 禁止手写 `git push`(2026-09-18 立,"已推完还在等"事故根治)**:①origin 的推送由 post-commit 钩子 `git-push-guard.mjs` 自动完成(内置 ahead 检测+推送+回读验证,幂等)——commit 落地即已推送,**手动盲推必撞 already-pushed 非快进报错**并诱发后台反复干等;②Gitee/GitCode 由 `mirror-to-cn.yml` CI 在 push 后自动镜像(+每日 2 次兜底),**本地手推镜像仓=违反架构**且制造 DIVERGED 竞态。③收尾核验同步状态**只允许** `node scripts/git-push-converge.mjs`(只读判定,六态 ALREADY/PUSHING/PUSHED/SKIP/BEHIND/DIVERGED,不做任何非必要推送);仅在 guard/CI 均失效的应急场景才人工推,且必须先跑该脚本确认状态。多会话并发期:本会话交付已被远端包含(merge-base --is-ancestor 验证)即为完成,本地 HEAD 落后不追、不与并发会话抢 reset/ff。
  - **⚡ 推送异步化(2026-09-18 立,根治第二段)**:guard 检测到 ahead 时默认 spawn 后台 worker 推送(pre-push 质量门不降级,实测单遍 216.8s)并立即返回——**commit 命令秒回,不再被推送拖住**;状态在 `.workbuddy/push-state.json`(running/done/failed,失败由下次 guard 自动重试),converge 读该状态显示 PUSHING;核验若见 PUSHING=正在推,等 1-2 分钟再查即可,勿手动干预。强制同步推送:GUARD_ASYNC=0。(2026-09-18 晚修复:异步 spawn 的 fd 误用 `out.close()` 必抛 TypeError→静默回退同步推送+双重推送竞态,已改 `closeSync`;push-gate 缓存改**内容指纹键控**(HEAD:apps/HEAD:packages 子树+脏文件内容 hash),合并/文档提交不再重跑全量门。)
  - **🔄 主动收敛(2026-09-18 晚立,根治第三段)**:推送遇 non-FF(并发会话推力)时**禁止手工 fetch/merge/push 循环**(实测 3 轮 25 分钟),统一跑 `node scripts/git-sync-converge.mjs`(默认 3 轮:fetch→祖先判定→`merge-tree --write-tree` 索引层合并(**零触碰他人未提交文件**)+commit-tree+update-ref→guard 推送,直至收敛;冲突才需人工)。只读核验仍用 `git-push-converge.mjs`。
  - **🪟 后台进程禁弹窗(2026-09-18 晚立)**:Windows 下 `spawn(detached:true)` + 控制台程序**必弹新 cmd 窗口**——所有 detached spawn 必须带 `windowsHide: true`(guard worker/锁心跳/desktop-dev-saas 已全量修复,仓库既有正例 dev-with-warmup.mjs)。**同理所有 `shell:true` 的 spawnSync/execSync 必须带 `windowsHide: true`**——shell 派生链无 CREATE_NO_WINDOW 时,pnpm -r typecheck 等会为每个子包拉起可见 cmd.exe(2026-09-18 WMI 实锤:一次全量 typecheck ~30 次闪窗,13 处派生点已全量修复)。新增派生点遗漏此参数=用户桌面反复弹窗事故。**⚠️ 默认终端=Windows Terminal 时上述措施全部失效(2026-09-18 深夜 WMI 二次实锤根治)**:WT 宿主忽略 SW_HIDE(windowsHide 无效)且不理 HKCU\Console 离屏档案,同一负载实测 135 个可见窗口样本;已把 `HKCU\Console\%%Startup` 的 DelegationConsole/DelegationTerminal 双双设为 `{B23D10C0-E52E-411E-9D5B-C09FDF709C7D}`(Windows 控制台主机 conhost)——conhost 尊重 SW_HIDE 与离屏档案,复跑同负载可见窗口归零。**禁止改回 WT 委托**;排查弹窗先查此键,取证脚本=.ihui-agent/tmp/console-watch.ps1(WMI 事件+15ms 可见窗口采样,后台 PS 任务约 2 分钟即被杀,足够覆盖一次守门链)。
  - **🧬 机器级根治 windowsHide 默认值(2026-09-20 立,"git 窗口一直弹"第四次复发收口)**:实测 Node v24 `child_process` 的 `windowsHide` **默认 `false`**;当父进程**无控制台**(detached+文件 fd 的 guard worker、计划任务、Qoder/Trae/WorkBuddy 等 GUI 宿主派生)时,派生任何控制台程序(git/cmd/pnpm/node)**必分配一个可见控制台**。逐点补参数不可持续(仅 `scripts/`+`.husky/` 热路径就 74 处漏,全仓 ~600 处,且新写脚本继续产生)。**根治**:`node scripts/install-console-window-hook.mjs --apply` —— 把 `scripts/lib/windows-hide-default.mjs` 复制到**仓库外**稳定目录(默认 `<TEMP 上级>/ihui-node-hooks`,本机 = `D:\caches\ihui-node-hooks`;放仓库外是因为 `NODE_OPTIONS` 指向的文件一旦缺失会让**全机所有 node 进程启动失败**,仓库移动/删除不得波及。钩子源文件**必须是 `.mjs`**:`.gitignore` 第 207 行 `*.cjs` 会把 `.cjs` 静默忽略,§23 同类陷阱,一旦被忽略其他机器就装不上),并以 `--import=file:///...` 写入 HKCU 用户级 `NODE_OPTIONS`。钩子给 7 个派生方法补默认 `windowsHide: true`,**显式传 `false` 一律尊重**;落盘前必先用暂存副本做"node 能否带此 NODE_OPTIONS 启动 + 7 个方法是否真被替换"双试跑,不通过则**拒绝写入**,原值备份于同目录 `node-options-backup.json`。实测**ESM 具名导入 `import { spawnSync }` 同样被拦**(loader 早于 builtin ESM facade 创建)。**换机/重装/新克隆后必须跑一次 `--apply`**;`--verify` 巡检不写盘,`--remove` 卸载。**不得因此省略逐点参数**:干净 checkout、他人机器、CI 没有此 env 时仍靠显式 `windowsHide: true` 生效,新增派生点照旧必须写。
  - **🔒 机制守门(2026-09-20 同日补,防第 5 次复发)**:`scripts/check-no-visible-spawn.mjs`(guardian-runner 第 **52** 项,blocking)——括号配平提取 `spawn/spawnSync/exec/execSync/execFile/execFileSync/fork` 调用,**首参可"肯定"为控制台程序**(白名单含 git/node/pnpm/cmd/pwsh/schtasks/ffmpeg…,并覆盖绝对路径与"路径+参数"两种形态)且 options 缺 `windowsHide` 即拦截。刻意**宁漏不误报**(未知变量名一律放过),避免阻塞他人提交;测试代码路径降为 warn(测试由终端 runner 派生,子进程继承已有控制台不新分配窗口)。`--staged` 供 pre-commit 用(runner 自动下发),`node scripts/check-no-visible-spawn.mjs` 为全量审计,`--self-test` 跑 14 例逻辑自检。§22c 镜像测试:`scripts/tests/check-no-visible-spawn.test.mjs`。
  - **🔗 异步推送配套适配(2026-09-18 晚)**:①check-push-sync(pre-commit #29)遇 push-state running(未过期)放行——否则每次 commit 后 270s 窗口内的下一次 commit 必被 #29 阻塞→--no-verify→80 项守门全跳过;②guard worker 串行化:在途 worker 未落定时新 worker 先等后跑,杜绝多个 270s typecheck 并行打满 CPU。③**推送门中断分类(2026-09-18 深夜)**:typecheck 进程被外部杀死(CTRL_C 注入/宿主清树,exit 3221225786/141/143/130/137,实测日志 39 次)≠ 类型检查结论——check-typecheck 按**临时失败 exit 75** 退出→guardian-runner 原样传播→pre-push 写 `.workbuddy/push-gate-last-result.json` 标记+exit 75→guard worker 见新鲜 75 标记**先带 hook 重试**拿真实结论,仍失败才按用户规则 --no-verify 兜底(此前"被杀"被误判成"他人代码失败"直接绕过真实门禁)。④降级判据第三兜底 `head-diff`:push-scope 与暂存区均为空时用 `git diff --name-only origin/main HEAD` 推断推送范围(实测 4 例"无可判定改动范围,无降级"硬拦);⑤push-state 死 pid 自愈:guard 启动发现 running+死 pid → 顺手改写 failed 终态。⑥**typecheck 再入守卫(2026-09-18 深夜,WMI 实测 24.6 万 cmd/2min fork 风暴根治)**:根包 `pnpm typecheck`=typecheck-full.mjs,pnpm -r 一旦把根包纳入执行即无限递归(每层再起 pnpm -r,指数级进程树,实测 ~2000 cmd/秒持续 26 分钟,即用户"一直闪弹 cmd"的主源);typecheck-full 已加 `IHUI_TYPECHECK_FULL_CHILD` 环境变量再入守卫(子层检测到立即 exit 0),勿删。
- **工作区存续自愈(2026-09-23 立,与 `.git` / 嵌套 ref 同源问题)**:宿主清理层同样会**成批删除工作区里的已跟踪目录**(实测同日三轮 137 → 27 → 1 个,命中 `tests/`、`__tests__/` 整目录与安装器位图资源)。缺失只体现为 `git status` 一片 ` D`,而**下一次提交就会把这些文件从版本树里删掉**(等价一次静默回滚)——`.git` 与 refs 早有分层自愈,工作区存续性此前无人管。自愈脚本 `scripts/heal-worktree-tracked.mjs` 判据三条同时成立才恢复:① 工作区缺该文件;② **索引 blob == HEAD blob**(⇒ 无人对它暂存过任何改动,含 `git rm` 的暂存删除);③ HEAD 中该路径存在。故恢复内容按定义零独有数据,他人已暂存的删除**只报数、不代裁**。触发点挂在 `git-guardian` 巡检的**健康轮次早退之前**(计划任务实跑 `main()` 单轮,`startDaemon` 未启用 —— 挂错位置等于永不执行);`--check` 口径保持零副作用,派生一律带 `windowsHide`(§5b)。手动:`node scripts/heal-worktree-tracked.mjs [--dry-run|--json|--self-test]`;跳过 `IHUI_SKIP_WORKTREE_HEAL=1`。故障演练:删 `scripts/brand-foreground-baseline.json` → 跑一轮守护 → 文件自动找回并写审计行「✅ 工作区存续自愈:恢复 1 个被外部删除的跟踪文件」。
- **同一自愈的第二、三层(2026-09-23 补)**:`alignDrifts()` 对齐**幻影漂移**(索引==HEAD 且 工作区内容==该路径某祖先版本 ⇒ 才动;判据直接复用守门 84(原 76),单一真相源);`refreshStaleIndex()` 刷新**落后索引** —— CAS / converge 用 commit-tree + update-ref 推进 HEAD 却**不动主索引**(`git status` 首列 `M `,实测同日 14 个路径),此时一次不带 pathspec 的普通 commit 就把整批文件写回旧版;三条判据同时成立才刷新(① index≠HEAD ② 索引 blob 确为该路径历史版本 ③ 工作区==索引),且**只逐路径 `update-index`,绝不做全局 `git reset`**(会连带 unstage 他人真正的暂存)。挂点:`git-sync-converge` 的成功出口自动调 `--align-drift`(真仓实测已生效)。`--self-test` 共 12 例,含三条反向对照:真编辑不覆盖 / 暂存后又有改动不刷新 / 他人真暂存不刷新。
- **本地恢复源必须增量刷新(2026-09-24 立,补 §5b 唯一的空白层)**:`git-guardian` 只**读**恢复源(`backupOk` 判存在 + `cpSync(BACKUP → GITDIR)`),此前**没有任何环节更新它**。实测 2026-09-24 04:40:`G:/IHUI-AI.git-backup-20260912` 的 main 停在 `f481c39a0`(09-23 20:13),本机 main 已前进 **97 个提交** ⇒ 宿主再删一次 `.git`,从该源恢复等价于**回滚 97 个提交**;若其中含未推送提交,即 09-23 15:49"15 条未推送 commit 对象永久丢失"的同型事故重演。刷新器 `scripts/git-backup-refresh.mjs` 对备份做**增量 fetch**(`+refs/heads/*` + `+refs/tags/*`、`--update-head-ok`,备份是 `.git` 的非裸 cpSync 副本,默认会被 git 硬拒)、`read-tree --reset HEAD` 重建其索引(不触碰任何工作树)、并复制 `refs-manifest.json`(使离线恢复后立刻具备嵌套 ref 自愈)。取证:`--self-test` 9 例(第 7/8 例专测非裸副本形态 —— 真仓首跑就是被它咬出 `refusing to fetch into branch 'refs/heads/main' checked out`;裸仓测不到)、`--check` 在追平前 exit 1、真刷后 exit 0。触发点:计划任务 **`IHUI Git Backup Refresh`**(每 15 分钟,经**纯 ASCII** `scripts/git-backup-refresh-hidden.vbs` 以 SW_HIDE 派生,注册前已用 `cscript //nologo` 实跑预检)。手动:`node scripts/git-backup-refresh.mjs`;判健康:`node scripts/git-backup-refresh.mjs --check`(零副作用)。
  - **触发点已改挂守护 tick(2026-09-24 同日补)**:实测 `Get-ScheduledTask` 全量列表里**已经没有** `IHUI Git Backup Refresh`(与 §26 记的 `IHUI C-Drive AutoMaintain` 凭空消失同型,而同目录其余 `IHUI*` 任务全在位可列 ⇒ 不是查法失效),恢复源因此又落后 100+ 枚提交。现由 `git-guardian` 每轮 `refreshRecoverySource()`:先 `--check`(零副作用、便宜)早退,判落后才跑增量刷新。**教训:关键自愈层不得挂在一个会自己消失的东西上** —— 计划任务可以当兜底,不能当唯一挂点。
- **禁止 `git pull --rebase`**:2026-09-12 15:2x 一次 rebase 崩溃导致真 gitdir 目录被原生删除。同步一律用 `git fetch <remote> main` + `git merge --ff-only FETCH_HEAD`。

**诊断**:

```bash
node scripts/git-guardian.mjs --status      # pointer/gitdir/git/HEAD/dirty/备份/嵌套ref 全量健康
node scripts/git-guardian.mjs --check       # 只检查,异常 exit 1(CI/巡检用)
node scripts/git-refs-heal.mjs --status     # 嵌套 ref 与清单比对(缺失即 exit 1)
schtasks /query /tn "IHUI-AI git-guardian"  # 守护任务实况(本机无 nssm;急用 schtasks /run 立即触发)
schtasks /query /tn "IHUI-AI git-guardian"  # 守护任务实况(另有 NSSM 服务 IHUI-GIT-GUARD 常驻 --daemon,见 §5b 上方更正;急用 schtasks /run 立即触发)
tail -20 .workbuddy/git-guardian.log        # 自愈审计流水(健康时不写行)
```

---

## 5c. 溯源水印与生成产物(强制,2026-09-12 立)

**三层水印**(`scripts/watermark.mjs`):L1 可见横幅(`// © 2026 IHUI AI …` + `// Provenance-watermarked. …`)、L2 横幅内零宽载荷(`// [IHUI-AI-PROVENANCE]:<zw>`)、L3 文件末尾独立不可见行。
**判据(2026-09-12 加严)**:载荷必须**可解码且等于 `WATERMARK_TEXT`**,仅"存在"不算数。四态:`完好` / `残迹`(有横幅无载荷)/ `载荷损坏`(存在但解码不符)/ `未覆盖`。后三者 `watermark.mjs verify` 与 `check-watermark-coverage` 均 **exit 1**。

- 新建源文件后必跑:`node scripts/watermark.mjs inject <file>`;然后 `node scripts/check-watermark-coverage.mjs`(pre-commit + CI 门禁,只统计 git 跟踪文件)。
- **门禁是自愈式(2026-09-12 立,根治"恒红只能靠跳过"的历史痛点)**:`check-watermark-coverage.mjs` 检出缺口后**自动 `clean+inject` 回写并 `git add` 回暂存区**,修复后 `exit 0`。因此**任何**来源(生成器 / `sed` / 批量脚本 / 手工编辑)产出的无水印或载荷损坏文件,都不可能进入提交 —— 无需再 `HUSKY_SKIP_WATERMARK_GUARD=1`。
  - 纯判定不改文件(CI / 审计):`node scripts/check-watermark-coverage.mjs --no-fix`。
  - 安全闸:单次缺口 > 200 个时**拒绝**自动回写并报错(疑似批量改写事故,须先排查来源再整体 `inject`),避免静默整体重写。
  - CI 侧仍用 `node scripts/watermark.mjs verify` **严格判定**(不自动修复),保证"本地能自愈、远端不掩盖"。
- **生成器仍应自带注入(更早一步,非唯一防线)**:任何 `writeFileSync` 产出 git 跟踪文件后,必须紧随一次水印注入,失败即 `process.exit(1)`:

  ```js
  execFileSync(process.execPath, [resolve(repoRoot, 'scripts/watermark.mjs'), 'inject', outFile], {
    stdio: 'inherit',
  })
  ```

  - 用 `process.execPath` + 绝对路径,**不要**裸 `node` / `spawn('node')`(依赖 PATH,服务/CI 下会失败)。
  - 参考实现:`apps/miniapp-taro/scripts/gen-i18n-compressed.mjs`。
  - 事故:该生成器此前不写横幅 → 产物 `src/i18n/generated/remote-locales.gen.ts` 长期缺载,使 `check-watermark-coverage` 对已跟踪文件恒红,提交只能靠 `HUSKY_SKIP_WATERMARK_GUARD=1` 绕过。

- **禁止对含载荷文件做文本级批量改写**(reflow、空白归一、正则替换、`sed -i`、编码往返、批量重写):`U+200B`/`U+200C`/`U+200D`/`U+2060` 属 Unicode **Cf 类**不可见字符,会被这类操作静默改写。
  - 事故:`apps/ai-service/**` 等 **144 个已跟踪文件、218 处载荷**被破坏,解码成 `PROVENCE-2026` / `IHUHU-AI` / `IIUIUIUI-AI` / 混入控制字符 —— 旧版门禁只验存在性,损坏长期隐形。修复后 `verify` 新增"载荷损坏"计数并阻断。
  - 需批量改名/改动时:先 `clean` → 改 → 再 `inject`;或直接 `inject`(工具已能识别损坏并清洗重注)。
- 注入是**幂等**的(同一载荷 → 同一字节),重复生成不产生 diff;`clean → inject` 往返**零漂移**(已回归验证:可见内容逐字节不变)。
- `clean` 按**行首锚定**(`© YYYY IHUI AI` / `Provenance-watermarked.` / `[IHUI-AI-PROVENANCE]:`)识别横幅,**不会**误伤源码里的 `BANNER_ID` 常量或正则定义。
- **禁止**为加水印而整体重写文件内容(会破坏零宽溯源链),一律用 `watermark.mjs`。

---

## 5d. 模型密钥引导与 `.env` 回填(强制,2026-09-12 立)

**密钥不入仓、不入聊天记录。** 本机模型密钥的唯一权威来源是百度网盘同步目录 `BaiduSyncdisk/密钥/模型/`(每厂商一个 txt,内容为裸 token)。**盘符不得写死**:本机实测真实库在 **`F:/BaiduSyncdisk/密钥/模型/`**,而 `D:/BaiduSyncdisk` 根本不存在 —— 旧版本节与两处脚本都按 `D:` 写,结果是"读不到文件"被下游门禁报成"凭据失效"(2026-09-24 镜像活性假故障的根因)。解析统一走 `scripts/lib/key-dir.mjs`(`resolveKeyDir('模型')` / `resolveKeyDir('git仓库')`,按 F→D→E→G→C 取第一个存在者);目录仍可用环境变量 `IHUI_MODEL_KEY_DIR` / `IHUI_MODEL_KEY_DIR_GIT` / `IHUI_SECRETS_ROOT` 或 `--key-dir` 覆盖。

- 回填流程:先 `node scripts/env-backfill-model-keys.mjs --verify` 巡检(不写盘),确认无误后 `--verify --apply` 写盘。
- 硬性约束:只写 `.env` 中**值为空**的键;已有值一律跳过、**绝不覆盖**;apply 前自动备份到 `.ihui-agent/env-backup/`(已被 `.gitignore` 忽略)。
- 输出恒为脱敏态 `前6位***后2位 (len=N)`,任何情况下不打印完整 key。`.env` 由 `.gitignore` 第 98 行忽略,**永不入库**。
- 多候选消歧:同一源文件出现多把 key 时(如 AGNES 曾有两把),`--verify` 会逐把请求厂商官方 `/models` 端点,取**真能鉴权通过**的那把 —— 不靠猜测、不靠索引。
- 判读规则:`网络不可达` **不等于** key 无效(本机访问不到 Google,`GEMINI_API_KEY` 探测必然超时);只有 `鉴权通过` 才是有效判据。
- 换机器 / 重装后若 ai-service 静默降级,首选动作就是跑一次本脚本巡检。

---

## 5e. 运维到人通知 = 邮件单通道(2026-09-24 收口)

- **告警/运维到人只有邮件一条通道**。此前并行的第三方推送腿(免费额度 5 条/天的推送网关 + 其凭据环境变量/密钥文件/状态文件)已**整体摘除**:代码、环境变量读取、状态文件读写、文档段落均不留残余;摘除原因与演进记录只存在于 PROJECT_PLAN/归档。第三方凭据文件按策略不删(删凭据不是本仓动作),只是不再被任何代码读取。
- **配额模型 = 只按身份去重、无总量封顶**:同一条告警(alertname+instance 指纹)在去重窗口(默认 4h)内只寄一封,不同告警一律照寄。"每日 N 封"计数闸全部禁止 —— 第三方额度是**他人配额**,撞顶即静默丢投递,自保才有意义;SMTP 是我们自己的,自设总量上限等于把"告警静默"再复制一遍。显式开关(`BRIDGE_MAIL_ENABLED` 这类)只允许关"要不要发",不得关"发几封"。
- **失败必须响**:第三方推送时代"邮件只是兜底、失败可以忍"的前提已不存在。邮件寄不出去必须留可诊断痕迹(日志吼 + UNDELIVERED 标记文件,参照 `scripts/check-credential-health.mjs` 的 `UNDELIVERED` 机制与 bridge `alert-bridge-mail-UNDELIVERED.json`),下一次成功投递自动清除;不得静默。
- **发信一律经 `apps/api/scripts/notify-deploy-failure.ts`,不得在任何脚本里自拼 SMTP/Resend**(版式唯一真相源 `apps/api/src/services/email-templates.ts`,由守门 81 `check-brand-email-channel.mjs` 硬拦)。**通道与 From 的硬事实**:① 优先 SMTP,且经 `smtp.qq.com` 中继时 **From 的邮箱段必须等于登录账号**,否则被 550 拒 —— 旧规则"发件人统一 `智汇AI官方 <IHUI-AI@aizhs.top>`"配 QQ 账号正是本条成因(SMTP 恒被拒 → 恒回落纯文本 Resend → 用户收到的邮件永远没样式);② `aizhs.top` 只在 **Resend(已验证域)** 通道作 From,回落链 SMTP → Resend 且 payload 必带 `html`;③ 多行中文正文必须走 `--message-file`(无 BOM UTF-8),命令行参数会过一层控制台代码页(GBK);④ 调用方**一律不得传 `--env-file`**(tsx v4 会劫持它转发给 node,路径不存在时 node 直接 `exit 9`)。运维告警收件人由 `apps/api/.env` 的 `ALERT_EMAIL_TO` 控制(缺该键 ⇒ `alert-notification-service` 的 email 通道被整条排除,含数据库备份缺失 / 24h 错误超阈 / AI 资讯源失败 / 转发配额四类告警,且只在全通道皆空时才 warn)。
- **凭据残留的两类处置边界(2026-09-24 实测划定)**:① **服务环境块**里已无人读取的推送键要**事务式**摘除 —— `nssm set … AppEnvironmentExtra` 是**整块覆盖**语义,同块往往还挂着别的关键值(本机 `IHUI-DEPLOYLOOP` 块内就有 `IHUI_ADMIN_PASSWORD`),写坏等于把部署环打回"凭据过期"那次两天冻结。正确姿势 = 先 `reg` 读整块 → 备份到 `D:\DevEnv\backups\env\` → 只滤掉目标前缀 → **逐条逐字节比对剩余项全等**才 `SetValue` → 写回后**再读再比**,不等即从备份整块还原(本机实测:`IHUI_ADMIN_PASSWORD` 摘前/摘后 SHA 同为 `43CA897ACBFC`,条数 2→1)。② **HKCU 用户级同名键不得摘**:它不是本仓遗留,`~/.workbuddy/skills/serverchan` 与 `~/.agents/skills/serverchan` 的 `SKILL.md` 声明 `env_vars: SERVERCHAN_SENDKEY`、`scripts/send.sh` 直接读它 —— 删了是**删别人工具的凭据**,§5e 说的"本仓代码不再读"从不等于"全机无人读"。同理,孤儿运行态文件(如 `deploy/win/.sct-notify-state.json`,实测 mtime 停在生成方被移除的那一刻、全仓唯一引用是一道**断言它不得存在**的反向测试)可删,而"名字像垃圾"不构成删除依据。
- `apps/api/src/services/alert-notification-service.ts` 的 7 条 IM/pager 腿(钉钉/企微/飞书/Slack/Teams/PagerDuty/generic)**不在本节的摘除范围内**:它是**对外产品能力**(用户给自己的中转站/监控站配告警出口),不是运维到人通道;env 未设 ⇒ 今天不可达,而 email 腿已走 `renderSystemAlertEmail` + `html`(实测 `:29/:448/:471`)。**能力清单的取舍属用户职权,不得按"通道收口"顺手摘**。
- agent 侧长任务完成/阻塞待决策等**交互回复走会话本身**;需要"手机也收到"的事件统一走上述邮件通道。
- **"这条通道是否已摘干净"必须按"服务实际执行的是哪份文件"取径,不得按 `git ls-files`**。判据:`nssm get <svc> AppParameters`(逐个服务),顺着那个路径看内容 —— `deploy/prod-bundle/` 被 `.gitignore:383` 整目录忽略,里面的运行副本**可以完全没有入库源**,对跟踪文件做的 grep 对它零覆盖。实测教训(2026-09-24):两票按跟踪文件做出的"零残留"证明都成立,而 `IHUI-MONITOR` 跑的 `deploy/prod-bundle/monitor.ps1` 仍内联着第三方推送凭据常量与三条纯文本推送通道,其唯一通道当日已被对端配额打死(`monitor-alerts.log` 累计 55338 行推送失败),期间它判出的 `api(8802) 未监听`/公网 500 一封都没到人。现该文件已改为**入库源的转发器**(与 bridge 同一收敛法),真身 `deploy/win/ihui-monitor.ps1`。新写运维脚本若落在 `deploy/prod-bundle/`,必须同时落一份入库源 + 转发壳,否则等于写进盲区。
- 到人链路的**现役生产者清单**(改通道要逐个覆盖,缺一不可):`ihui-alert-bridge`(→`monitoring/alertbridge/alert-webhook-bridge.cjs`)、`IHUI-DEPLOYLOOP`(→`deploy/win/ihui-deploy.ps1`)、`IHUI-MONITOR`(→`deploy/prod-bundle/monitor.ps1` 转发 `deploy/win/ihui-monitor.ps1`)、`scripts/check-credential-health.mjs`、`scripts/git-guardian.mjs`、`.github/workflows/blue-green-deploy.yml`。**改完必须重启对应服务**才生效:PowerShell/Node 都在启动时把脚本读进内存,改文件不动运行中的进程 —— 只 commit 不重启 = 线上仍跑旧逻辑(旧版含每日封顶),而本地一切看上去已修好。
  - **`scripts/git-guardian.mjs` 这一行此前是"设计意图",不是现状**(2026-09-24 补齐):实测该文件里 `mail|smtp|resend|notify|alert` **零命中** —— 它每 2 分钟巡检、判红只落 `.workbuddy/git-guardian.log`,所以"守护发现问题 = 到人"从来不成立。现已接上派发出口(经 `notify-deploy-failure.ts`,不自拼 SMTP,受守门 81 管),按 alert 身份 + 内容指纹 4h 去重、未送达 30min 退避、**无每日总量封顶**,发信失败不改自愈与退出码;核验入口 `--notify-dry-run` / `--notify-test <名>` / `--status` 的 `.notify` 字段。**教训同 §26**:清单里写"某生产者会喊人",必须实测它真有一条到人的出口,否则记账与事实分叉,而分叉形态永远是"安静"。

---

## 6. 验证命令

```bash
pnpm turbo build typecheck lint test          # 全量验证(必须全绿)
pnpm --filter @ihui/api typecheck             # 单独验证后端
pnpm --filter @ihui/web typecheck             # 单独验证前端
cd apps/ai-service && mypy app --ignore-missing-imports --strict   # Python 类型(app 源码路径 apps/ai-service/app,继承 pyproject.toml [tool.mypy])
pnpm dev                                       # 启动所有服务(web + api + ai-service,端口见 docs/port-management.md)
```

---

## 7. 删除/重构安全规则(强制)

删除任何 git 对象(分支/stash/commit/文件)前必须回答:① 该内容承载的**功能**是什么?② 当前 monorepo 中是否有**等价的功能实现**?③ 没有 → **不可以删除**,必须先迁移/开发替代。禁止基于"路径不兼容"或"看起来是垃圾"擅自 drop。stash drop / branch -D 同样适用。

---

## 8. goal 模式工作流(强制)

触发 `/goal <目标条件>` 时按本节流程执行。

### 目标条件硬门槛(单条最大 4000 字符)

必须同时包含:核心任务 + 验证标准(命令退出码/测试输出/文件状态/HTTP 响应)+ 约束边界 + 质量要求 + 异常处理。缺一即拒绝启动。示例见对话上下文。

### 运行时文件(强制)

进入 goal 模式第一轮执行前必须在 `.ihui-agent/goal-runtime/` 创建:

- `STATE.md`:目标条件 + 状态机(`active`/`paused`/`achieved`/`blocked`/`budget_limited`)+ 当前轮次 + Token 累计 + 最近评估结论 + 硬性指标清单。
- `loop-run-log.md`:逐轮追加(轮次号 + 执行摘要 + 工具调用统计 + 评估结论 `yes|no` + 一行理由)。

### 7 步执行循环

1. 目标解析与初始化(拆分硬性/软性指标,初始化 STATE.md)
2. 单轮任务执行(聚焦核心问题,输出执行摘要)
3. 独立评估校验(基于真实结果,禁止模型自评 yes)
4. 循环判定(yes → 第 5 步;no → 续跑;连续 3 轮 no 无进展 → blocked)
5. 最终交付校验(逐条核对硬性指标)
6. 状态清除与交还控制权(输出交付报告)
7. 整合与清理(目标摘要追加到 PROJECT_PLAN.md,删除 STATE.md + loop-run-log.md)

**子命令**:`<目标条件>` 启动第一轮;`(无参数)`/`status` 查询;`pause`/`hold` 暂停;`resume`/`continue` 续跑;`clear`/`stop`/`off`/`reset` 终止清理;`budget <数值>` 设 Token 上限;`log`/`history` 输出日志。

### 红线规则

- 单目标最大自动迭代 **20 轮**,超出 blocked。
- 高危操作(删分支/强推/删库表/影响生产)**必须暂停**请求人工确认。
- 严格围绕目标,禁止扩展需求、做无关重构。
- 每轮**完整承接上下文**(压缩后必须重读 STATE.md)。
- 连续 5 轮工具失败 → blocked。

### 失败回滚

- `blocked` / `budget_limited` 状态下**禁止** agent 自主执行 `git reset --hard` / `git checkout .` / `git clean -f`。
- 必须在 PROJECT_PLAN.md 记录:已修改文件 + 当前分支 + 起始 commit sha + 未完成原因。
- 回滚决策权归属用户。

---

## 9. 多端同步开发强制规则(强制)

- **默认全端连通**:每一个任务默认 8 端(web/api/ai-service/desktop/extension/mobile-rn/miniapp-taro/cli)同步开发,"匹配连通好"= 代码同步(共享 types/UI/schema 跨端一致)+ 链路打通(跨端调用无契约/类型/路由/404 错)+ 验证齐绿(各端 typecheck+build+test 全绿)。禁止只改一端交付、单端验证声明完成、分期交付(平台独占豁免除外)。
- **平台独占豁免需显式标注**:仅天然只属特定端(desktop 系统托盘/extension 上下文菜单/miniapp-taro 微信支付/cli 终端集成/纯文档守门脚本)可豁免,必须在 PROJECT_PLAN.md 标注"平台独占"或"单端文档/脚本",未标注按全端同步执行。
- **多端并行派单**:主 agent 优先用 §11 多 Subagent 并行模式按端拆分,每个 subagent 管自己端的代码+typecheck+build;主 agent 负责跨端契约对齐(共享类型/API 路由/schema)和全链路连通验证,不得下放给单个 subagent。
- **登录/SSO 链路跨端自动同步(强制,2026-09-04 立)**:凡改动登录相关 UI/文案/逻辑,必须**主动**同步所有相关端,不得等用户提醒。关键结构事实:① 共享登录 UI 仅覆盖 mobile-rn(`packages/app/src/features/login/LoginScreen.tsx`),**miniapp-taro 登录页是独立实现**(`apps/miniapp-taro/src/pages/login/login.tsx`),两端都要动;② SSO deep-link scheme:mobile-rn/desktop 用 `ihui://sso/callback`,miniapp-taro 用 `ihui-miniapp://sso/callback`,后端白名单由 `apps/api/.env` 的 `SSO_ALLOWED_DEEP_LINK_SCHEMES` 控制,新增 scheme 必须同步登记;③ web OAuth 中转页 `apps/web/app/(auth)/callback/OAuthCallbackHandler.tsx` 的 `redirect=mobile-rn` 分支负责跳 `ihui://oauth/callback` 回 App(支付宝回调参数为 `auth_code`);④ web 端 SSO 出口页为 `apps/web/app/sso/*`(支持 custom scheme 回跳)。
- **守门**(warn-only):`scripts/check-multi-end-sync.mjs` 检测 staged 跨端分布 4 场景(纯豁免目录→pass;触及 packages/* 未标注→warn;触及≥2 端→pass;触及 1 端未标注→warn),集成于 pre-commit 第 21 项。

---

## 9b. 单分支开发强制规则(强制)

- **禁止创建乱七八糟分支,所有改动统一往 main 合并**。除 main 之外**不允许**新建任何本地/远程分支(`feat/*` / `fix/*` / `hotfix/*` / `add-*` / `rescue/*` / 自定义前缀全部禁止)。
- **唯一例外**:`/goal` 模式目标条件强制要求独立分支时,允许创建 `goal/<目标名>` 临时分支(AGENTS.md §8);`goal/*` 完成后**必须立即删除**,不留历史快照。
- **必要分支判断标准**:**单次任务无法在 main 上原子完成**才允许创建(如 8 端并行多 subagent、紧急 hotfix 需独立回滚通道);**普通功能开发、Bug 修复、refactor、文档/守门脚本改动一律禁止创建分支**,全部在 main 上直接 commit。
- **已合并分支立即删除**:任务合并后**本会话内**完成 `git branch -d <已合并>`(本地) + `git push origin --delete <已合并>`(远程),不留"历史快照"分支污染 main 分支列表。
- **删除未合并分支前必须 tag 备份**(AGENTS.md §22 配套):`git tag backup/cleanup-<date>-<branch> <branch>` → `git push origin --atomic refs/tags/backup/cleanup-*` → 再 `git branch -D`。tag 必须本地+远端双备份,防 git gc 清理。
- **fetch + prune 是日常**:`git fetch origin --prune` 在每个 push 周期跑一次,清理已删远程分支的本地 stale 引用。
- **守门**(2026-07-30 立,2026-08-02 落地):
  - `scripts/check-single-branch.mjs`:检测 `git branch -a` 列表中除 main / upstream 外的分支,发现任意 1 个 → exit 1 阻塞 commit。
  - 集成位置:`scripts/guardian-runner.mjs` id 41(blocking),守门不通过则禁止 commit + push。
  - 豁免:§8 goal 模式临时分支(必须带 `goal/` 前缀,且在 `.ihui-agent/goal-runtime/STATE.md` 标注 `active` 状态才算合法)。
- **历史教训**(2026-07-30 立):仓库曾积累 12 个分支(本地 6 + 远程 7 + 1 upstream),其中 `add-ihui-ai` / `goal/*` / `rescue/*` 等 13 个无价值分支全部已合并或已被 main 覆盖;3 个未合并分支的内容(LLM 三提供商/i18n 五端/console.log→logger/awesome-prs)均已在 main 后续 commit 中包含或演进,merge 会回退 main 功能。教训:**分支不是"工作单元",是"协作单元"**——单 agent 单任务无需分支,直接 main 提交即可。

---

## 10. 交付报告一致性硬约束(强制)

同一份 .md 报告中**不得**同时出现:"无后续建议" / "完整收尾" / "对话可关闭" 与 "P1-P5" / "优化项" / "TODO" / "后续任务"。守门:`scripts/check-delivery-report-consistency.mjs` + pre-commit 第 12 项。

---

## 11. 多 Subagent 并行开发强制规则(强制)

### 任务分配格式(强制)

派发子任务时必须用以下格式,缺一拒绝执行:

```
## 任务目标
<一句话>

## 受影响文件(绝对路径,只允许以下文件)
- <repo-root>\path\to\file1
- <repo-root>\path\to\file2

## 禁止修改
- 任何不在上述清单的文件

## 验证命令(子任务完成后必须自行运行)
- pnpm --filter @ihui/web typecheck
- pnpm --filter @ihui/api test

## 约束边界
- <API 契约/类型/样式/行为约束>

## 交付物
- 完整代码 + 自验通过 + 一句话总结
```

### 联动规则

- 与第 7 节(删除安全)协同:subagent 不得删除非任务清单内文件。
- 与第 16 节(push保护)和 §20 协同:subagent 完成后由主 agent 统一 push。
- **subagent working tree 自检(2026-07-30 立,真实事故)**:subagent 完成任务后**必须**执行 `git status --short` 自检,确认 working tree 状态符合预期——只有任务清单内文件被修改/新增。如果发现意外文件(其他 agent 改动被 `git stash pop` 带回 / lint-staged 副作用 / IDE 自动 stage / 文件被清成 1 行等异常),**立即停止**并报告主 agent,**禁止**继续 commit/push/补救。主 agent 收到异常报告后按 §12 + §22 处理(revert / 隔离 add / `--no-verify` 跳过其他 agent 代码问题)。
- **stash 操作后强制 Read 验证(2026-07-30 立)**:subagent 在执行 `git stash push` / `git stash pop` / `git stash apply` 后,**必须**用 Read 工具验证任务清单内文件内容完整(防止 stash 误操作把文件清成 1 行或吞掉内容)。Read 返回内容与预期不符 → 立即停止,报告主 agent,**禁止**基于未验证的"假设文件完整"继续操作。

---

## 12. 多会话并行操作同一仓库强制规则(强制)

- 多会话/多 agent 在同一仓库并行工作时,**禁止**任何破坏性 git 操作:`git restore` / `git stash push` / `git clean -f` / `git reset --hard` / `Remove-Item` 删除其他 agent 创建的文件(包括"看着像垃圾"的 `commit_msg.txt` / 临时测试文件 / 调试日志)。
- commit 阶段**只 add 本任务相关文件**:`git add <file1> <file2>`,**禁止** `git add .` / `git add -A` / `git add -u`。
- **提交活文档前必须做"工作树 ⊇ HEAD"行级对账(2026-09-24 立,一夜三次自伤换来的)**:`README.md` / `AGENTS.md` / `PROJECT_PLAN.md` 这类多会话共写的文件,工作树副本**常年滞后于 HEAD**(实测分别少 57 / 48 / 54 行),而 `safe-commit` 的 Step ④ 是 `git commit -- <pathspec>` —— 按路径取**工作树**版本,于是"只 add 本任务文件"的规范提交照样把别人已入库的行整批写回旧态,`git status`、diff 行数、typecheck、守门 71 全都看不出来。动作:① 提交前 `node scripts/merge-live-doc.mjs --file <该文档>`(exit 1 = 存在真丢失);② 需归并时 `--apply`(它按锚点把 HEAD 缺失块插回工作树,并区分"被吃掉"与"被就地改写",后者不插回以免新老并存);③ 归并后必须报"仍判 lost = 0 且长行重复新增 = 0"。判据是**字符二元组** Jaccard ≥ 0.6 —— 前缀与按空格切词在中文里都会漏(工具自己栽过一次,README 里一度同时留下 `**第 93 项 X**` 与 `**守门 X**` 两行),已由 `--self-test` 8 例钉死。登记条目见 PLAN 的 O46④/O46⑩。
- **提交活文档前必须做"工作树 ⊇ HEAD"行级对账(2026-09-24 立,一夜三次自伤换来的)**:`README.md` / `AGENTS.md` / `PROJECT_PLAN.md` 这类多会话共写的文件,工作树副本**常年滞后于 HEAD**(实测分别少 57 / 48 / 54 行),而 `safe-commit` 的 Step ④ 是 `git commit -- <pathspec>` —— 按路径取**工作树**版本,于是"只 add 本任务文件"的规范提交照样把别人已入库的行整批写回旧态,`git status`、diff 行数、typecheck、守门 71 全都看不出来。动作:① 提交前 `node scripts/merge-live-doc.mjs --file <该文档>`(exit 1 = 存在真丢失);② 需归并时 `--apply`(它按锚点把 HEAD 缺失块插回工作树,并区分"被吃掉"与"被就地改写",后者不插回以免新老并存);③ 归并后必须报"仍判 lost = 0 且长行重复新增 = 0"。**但提交前的报告有时效边界**(2026-09-24 实测:`merge-live-doc` 报 0 丢失之后、`safe-commit` 落地之前,并发会话又推进了一轮 ⇒ 那次提交仍丢掉 2 条他人登记行)。所以顺序必须是四步:**报告 → 提交 → 立刻 `git show <新提交>^..<新提交>` 做行级对账 → 有丢失就地回补**(发现手段只能是提交后的 diff,`git status` 与提交前那次报告都看不见)。判据是**字符二元组** Jaccard ≥ 0.6 —— 前缀与按空格切词在中文里都会漏(工具自己栽过一次,README 里一度同时留下 `**第 93 项 X**` 与 `**守门 X**` 两行),已由 `--self-test` 8 例钉死。登记条目见 PLAN 的 O46④/O46⑩。
  - **回补过的行还要在"每次收敛/合并之后"复验它仍在 HEAD(2026-09-24 实测该文件第 4 次被顶掉)**:
    并发会话的 union 合并会把别人刚回补的行**再次吃掉** —— 实测
    `packages/shared/src/chat/handoff-package.ts` 的 `i18n-content-exempt-file:` 声明:被 `cfe8f65e4`
    抹掉 → `9e01f6aa9` 原样补回 → 几轮合并后 `git show HEAD:<该文件>` 又是 0 命中。"提交前对账 +
    提交后 diff"两道都保不住它,因为吃掉它的不是回补那次提交。复验是一行的事:
    `git show HEAD:<该文件> | grep -c '<该行稳定前缀>'` 应为非 0,为 0 即重新回补(工作树通常还留着,
    `git diff HEAD -- <该文件>` 立刻可见)。守门 70 的声明式出口是**随行内联**的 —— 行一丢,该文件
    49 处中文立刻判红:红点会等下一个碰它的人来吃,不会静默,但也只有那个人会以为是自己的错。
- 正确流程:预检(`git status --porcelain`)→ 隔离 add 本任务文件 → 验证 staged 仅含本任务文件。
- **任务完成必须自动 commit(2026-09-20 用户指令,强制)**:任务/批次完成且验证全绿后,agent **必须立即自动 commit**——不经询问、不等用户确认、禁止以"不擅自 commit"为由把已验证的工作留在未提交状态。push 仍按 §16/§20 执行(用户未要求时不主动 push)。commit 形态仍受本节约束(多 agent 并行必须 safe-commit.mjs;单 agent 直接 add 声明文件;禁止 `git add .` / `-A` / `-u`)。
- pre-push / pre-commit hook 失败因**其他 agent 引入的代码问题**(schema drift / 其他模块 TS/lint 错误 / 其他 agent 未完成 migration 等,不在本任务范围):**直接用 `--no-verify` 跳过 hook** 完成自己的 commit + push;**禁止**修改其他 agent 代码"帮他们修" / `git reset --hard` / 把"等其他 agent 修复再 push"作为交付结论 / 用 AskUserQuestion 询问用户;自己 commit + push 前只需保证**本任务改动文件** typecheck + lint + build 全绿即可;`--no-verify` 合法场景**仅限**"hook 失败原因是其他 agent 代码",若失败原因是**本任务自己代码**必须修复后正常 commit。

### 强制使用 safe-commit.mjs(2026-08-06 立,真实事故根治)

- **触发背景**:commit `aa15bec23` "fix(web): message-list 消息操作按钮..." 意外包含 `message-input.tsx`(其他 agent 改的 `rounded-t-xl`)。根因:`message-input.tsx` 在 pre-commit hook 执行**前**已被 IDE/其他 agent staged,`takeStagingSnapshot` 把它当成本任务文件,`restoreStaging` 不会 unstage。所有领域级守门(`check-commit-scope` / `check-staged-pollution`)都放过(同目录 `apps/web/src/components/chat/`,scope=web 匹配)。领域级守门**无法防御同目录文件级污染**。
- **强制规则**:多 agent 并行环境(≥2 个 agent 同时工作)下,agent commit **必须**用 `node scripts/safe-commit.mjs -m "<message>" -- <file1> [file2 ...]`,**禁止**直接 `git add <file> && git commit -m "..."`。
- **safe-commit.mjs 5 步法(零信任)**:① `git reset HEAD` 清空整个暂存区(无论谁 staged 的)→ ② `git add -A -- <声明的文件>` 只暂存自己声明的文件 → ③ 校验 `git diff --cached --name-only` === 声明文件(有意外文件立即 exit 1)→ ④ `git commit -- <pathspec>` 原生 pathspec 终极兜底 → ⑤ `git show --name-only HEAD` 验证 commit 内容只包含预期文件。
- **钩子失败的归因是量出来的,不是抄来的(2026-09-25 立,`scripts/lib/commit-gate-attribution.mjs`)**:旧版首次 commit 一失败就打印「按用户规则"hook 失败因其他 agent 代码 → --no-verify 重试"」并整批跳门 —— 它**从未计算过归因**。实测一轮 134 道门 111 通过 / 1 失败,红的是 `check-push-sync`(判远端态、与提交内容无关),措辞仍是"他人代码"。现流程:解析 runner 的失败门清单 → **逐道复跑** → 该门这次的输出**点名本次声明的文件 ⇒ 判"本任务自己的红",拒绝 `--no-verify` 并 exit 1**(必须修);一个都没点名 ⇒ 允许跳,但只说量到的话;批没跑完 / 解析不到 / 复跑全不可用 ⇒ 明写「未归因」,按应急路径落地并留痕于 `.workbuddy/safe-commit-attestation.jsonl`。**"批一道都没跑"从 2026-09-26 起不再是中性事实,而是一个必须先补证据的分支**:`scripts/lib/pre-commit-hook.js` 里 lint-staged 跑在守门批**之前**,于是一次 lint/prettier 失败 = 156 道门全不跑 = 归因永远 `unattributed` = 照样跳门,而账面读起来像"跑过了、只是与本次无关"(实测 `acaf8407cc` 与 `d379215126` 前后各一次;这正是"判据失效的表现永远是安静"那一型,守门 70/76/81 同族)。现由 `safe-commit` 在判到 `unattributed ∨ ranFullBatch===false` 时**自己把 `guardian-runner --staged` 跑一遍**,再把那一轮的输出喂回**同一条铰链**(`classifyHookFailure`)分流:点名本次文件 ⇒ `mine` ⇒ 拒绝跳门;一个都没点名 ⇒ 可跳但措辞必须写明"取证来自 safe-commit 自跑的那一轮,不是钩子内那一轮";自跑也没成功 ⇒ 照旧落地,而 `unattributed` 的措辞必须带上具体原因(runner 不在位 / 派生失败码 / 超时)。**刻意不把 `unattributed` 改成拒绝跳门** —— 那会让"别人把我挡在 lint 外"的应急路径消失,逼人手工 `git commit` 跳得更彻底且不留痕。**一条如实登记的覆盖面上限**:自跑发生在首次 commit 失败之后,而 lint-staged 失败时会回滚它自己动过的暂存区 ⇒ 新增文件此刻已退回未跟踪,自跑那一轮看到的暂存集可能少于声明集(与既有"逐道复跑"同一刻同一索引态,两份证据因此可互相比较)。应急提速:`IHUI_SAFE_COMMIT_BATCH_TIMEOUT_MS`(极小值 ⇒ 自跑判"未成功"并带原因入库,**不是**静默开关)。取证:镜像 13 例(含"汇总块在位时绝不自跑"的反向锁与装车证明)。**红在守门批「之外」也要点名,不得把已量到的批内结论丢掉(2026-09-25 补,G-185)**:钩子在 runner **之后**还跑若干独立 blocking 步(实测 `i18n 死 key 扫描`),它一红就 `exit 1` —— 那一轮 154 道门**全部跑完、blocking 失败 0**,而旧归因把这种组合写成「汇总里没列出失败门 ⇒ 归因未计算」,随后照样 `--no-verify` 落地(提交 `46a4c18ed` 即此型),等于**把已经拿到的守门结论整块丢弃**,账面读起来像"没跑守门"。现规则:解析到批外步骤名(`outsideBatchStep`)后仍按同一条铰链分流 —— 该步结论点名本次声明的文件 ⇒ 判 `mine`(拒绝跳门,必须修);一个都没点名 ⇒ 判 `not-ours` 但措辞必须是「批内 0 失败,红在批外这一步」并点名是哪一步、提醒随后清偿。**失效方向仍是"多要一次定向说明"**,不是"多放一次跳门",也不是"干脆不落地"。取证:C0/C0b/C1/C2/C3 五例(含"批内确有失败门时不得走这一支"的反向对照,否则批外步骤会替批内那道门洗责)。**刻意不建"机器态门 id 清单"** —— 豁免清单必然腐烂(见 §4 对 `RN_ONLY_BRAND_KEYS` 的教训)。 —— 豁免清单必然腐烂(见 §4 对 `RN_ONLY_BRAND_KEYS` 的教训)。取证:镜像测试 5/5(含装车证明 + 真实 `hook-logs/pre-commit.log` 取样校验夹具)+ 三条变异各自变红(ANSI 剥色那条登记为**等价变异**,不造假具硬凑红)。**因此:agent 不得再假设"钩子红了就能跳"** —— 被拒时那句红点名了哪个文件,就去修哪个文件;确属误判才可按 §16 显式说明后手工提交。
- **单 agent 环境豁免**:确认无其他 agent 并行时,可直接 `git add <file> && git commit`,但必须先 `git status --porcelain` 确认 staging area 干净(无其他已 staged 文件)。
- **pre-commit hook 配套提示层**(2026-08-06 立):hook 入口调用 `auditStagingFiles()` 打印 staged 文件清单(按目录分组)+ 同目录多文件警告 + 文件数 > 5 严重警告(warn-only,不阻塞)。提示层无法真正阻止污染,真正阻止污染的是 safe-commit.mjs 的 `git reset HEAD` 清空暂存区。
- **红线**:
  - ❌ 禁止多 agent 并行时直接 `git add <file> && git commit`(staging area 可能有其他 agent 残留)
  - ❌ 禁止用 `git add .` / `git add -A` / `git add -u`(会把其他 agent 改动一起 stage)
  - ❌ 禁止忽略 pre-commit hook 的 `📋 staged 文件清单审计` 警告(同目录多文件时必须核对)
  - ✅ 多 agent 并行时用 `node scripts/safe-commit.mjs -m "..." -- <files>`(自动清空暂存区 + 只 add 声明文件 + 校验)
- **git 写操作全局锁**(2026-08-06 立,`.git` 损坏事故根治):所有 git **写**操作(commit/push/pull/rebase/merge/stash/checkout/gc/repack/fetch)在同一仓库必须**串行化**,同一时刻只允许一个写操作单元执行。
  - **已自动生效**:safe-commit.mjs 整个 commit 流程自带锁(`Step 0/5 获取 git 写锁`);post-commit 钩子自动处理直接 `git commit` 场景。**agent 无需额外操作**。
  - **手动 git 命令**必须遵守:执行 `git pull` / `git rebase` / `git fetch` / `git checkout` / `git stash` 等写操作前,先 `node scripts/git-lock.mjs check`(exit 0 = 无锁可执行;exit 1 = 有其他写操作进行中,等待后重试)。
  - **禁止手动 `git gc` / `git repack` / `git prune`**:需要时用 `node scripts/safe-gc.mjs`(自动检查无锁后执行)。autoGc 已禁用(`gc.auto=0` + `maintenance.auto=false`),无需也不应手动触发 gc。
  - **锁异常处理(2026-09-18 心跳机制升级)**:持锁方 safe-commit/safe-gc 会 spawn 心跳子进程每 5s 续期 `meta.ts`——**活进程的锁绝不会被抢占**;仅当"锁年龄超 300s **且** 持有者 pid 已死"或超 1800s 硬上限(pid 复用兜底)才强制抢占。锁等待超时(safe-commit 15min / post-commit 3min)报错并提示持有者是否存活;紧急可删 `.git/ihui-git-write.lock`(先确认无 git 写进程)。绕过:`IHUI_GIT_NO_LOCK=1`(仅应急,禁用后自行承担并发风险)。
  - **新环境初始化**:重新 clone 后必须执行一次 `node scripts/git-hygiene-init.mjs`(恢复 gc.auto=0 / maintenance.auto=false 防护配置,这些是 local config,clone 不保留)。

### .git 整目录消失事故链根治(2026-09-10 立,当日 7 次事故复盘)

- **根因**:多会话共享同一工作区时,并发 git 手术互踩——会话 A 重建/删除 `.git` 期间,会话 B 的 git 命令撞上 `.git` 缺失,B 误判"仓库损坏"也触发重建/清理,互相把对方的 `.git` 删掉;refs 写入"被吞"多为重建窗口期读写竞态的表象。
- **强制规则**:
  1. `.git` 异常时**先诊断后动手**:`node scripts/git-lock.mjs check` 确认无其他会话持锁 + `ps`/审计日志确认无并行 git 进程,才允许删除/重建。
  2. 重建**只能**走 `node scripts/git-rebuild-local.mjs`(已接入 git-lock,锁被持有时快速失败),**禁止**手工 `rm -rf .git` + `git init` 自由发挥。
  3. 修复 refs 后必须**回读验证**:`git update-ref ... && git rev-parse <ref>` 输出一致才算成功;批量修复后 `git pack-refs --all` 持久化。
  4. **禁止把 `.git` 迁出工作区/改指针文件**(2026-09-10 已验证:并行会话的恢复逻辑会把指针文件当"损坏"清除,反而制造新事故)。
  5. `git ls-remote` 为远端真值唯一来源,本地 `origin/main` 引用异常时用显式 SHA 操作,勿信本地引用。

### 部署/构建全局锁(2026-08-09 立,并发部署事故根治)

- **事故背景**(8-09 实锤):多 Agent/自动化任务并行触发 `build-next-prod.ps1` 时,两个构建同时备份/清理/写入 `apps/web/.next` → 8801 短暂 502 + 监控报警,产物存在损坏风险。原 `apps/web/scripts/check-lock.js` 只有 dev-vs-build 互斥,**没有 build-vs-build 互斥**;且 `build-next-prod.ps1` 曾引用不存在的 `scripts/check-lock.js`,锁从未生效。
- **锁机制**:`node scripts/deploy-lock.mjs acquire|release|check`(锁 = 项目根 `.deploy.lock` 目录,mkdir 原子性)。build 与 build/dev 全部互斥;dev+dev 共存;stale(10min)+ 超时(10min)自动兜底。
- **判活问的是"哪个 pid"——这一格在 2026-09-25 冻结过生产 11h50m**(登记 G-193):`acquire` 是一次性 CLI,打印"锁已获取"就退出,所以 meta 里那个 `pid` **不是持锁者**,拿它判活等于没有判据 —— 要么恒"已退出"(别人构建中途被抢),要么该号被系统复用给别的过程(实测:`meta.ts=11:16:32 pid=888`,而当时占着 888 的是 `C:\Windows\System32\nssm.exe`,**StartTime 11:19:13,比锁晚 160 秒**,一个进程不可能在它存在之前写锁)⇒ `alive=true` 永远成立 ⇒ 每轮白等 600s 后 exit=1,部署环从 11:16 起没产出新构建,而 `git status`、typecheck、154 道门全都看不出来。两条修法同时落地:**① `--owner-pid`**(调用方自己的 `$PID`,这才是这段锁真正的主人;`build-next-prod.ps1` 已传)**② 硬上限 30min**——"名义存活"与"锁龄超上限"同时成立只可能是复用 ⇒ 归档现场后抢占(与 §12 `git-lock` 的 1800s 复用兜底是同一条设计)。**判据矛盾时的失效方向**:宁可抢一把明显超时的锁(现场先归档,可复核),不可无限 wait(那等于把"没人能说清"变成"生产一直不更新")。`release` 在这一格**仍不代删别人的锁**,但必须点名"pid 疑似被复用"并指出出路是下一次 `acquire`。
  - **如实登记未完成的部分**:`apps/web/package.json` 的 `predev` / `dev:clean` / `dev:stable` / `prebuild` 四处**没法声明 owner**(npm 生命周期进程在 dev server 起来之前就退了),它们的互斥仍靠"CLI pid 多半已死 ⇒ 秒抢"这条弱机制;真正的解是心跳续期(照 `git-lock` 每 5s 写 `meta.ts`),而那需要一个能看见 dev server 生死的常驻方 —— 现在没有。所以这一段的事实是:**build 侧已收口,dev 侧靠硬上限兜底(最长 30min 让位),不是已有心跳**。
- **已自动生效**:`build-next-prod.ps1` [0/6] 阶段自动 acquire、[7/6] release;web 包 `prebuild`/`predev` 已接入。**agent 无需额外操作,直接跑构建脚本即可**。
- **手动构建必须遵守**:触发 web 构建前先 `node scripts/deploy-lock.mjs check`(exit 0=可构建;exit 1=有其他构建/部署进行中,等待后重试)。**禁止**绕过锁直接 `next build` 或并发触发 `build-next-prod.ps1`。
- **锁异常处理**:超时自动报错;持有者已退出的悬挂锁**不限锁龄**立即抢占(2026-08-27 判据);"名义存活但锁龄超 30min 硬上限"= pid 复用,同样归档后抢占(见上节 G-193);紧急可删项目根 `.deploy.lock`(先确认无构建进程)。`.deploy.lock/` 已 gitignore。
- **禁止**用 `-CleanCache` 或其它参数绕过锁;多 Agent 协作时若需排队构建,等待而不是强删锁。

### 12b. 协作收尾 SOP (2026-08-18 立, §22c 配套)

当发现其他 agent 正在并行做同一任务时(working tree 与 origin/main 不一致, 或远端新 commit 提到类似功能), 不要立即重写对方主体逻辑, 改走协作收尾路径:

1. **核查现状**: 用 `git diff HEAD --stat` + `git log origin/main..HEAD` 确认
   - 远端是否已 commit (a01fcf4 这种情况)
   - 本地 working tree 是否含对方未提交的改动
   - 对方改动的完整性与正确性
2. **不重写主体**: 仅修复对方代码中的明确缺陷(如测试用例失败、QUIET bug)
3. **最小化改动**: 用 `git add <仅自己改的文件>` 只 stage 自己的改动
4. **--no-verify 跳过**: 因为 hook 会被对方未 commit 的 working tree 改动误判为污染
5. **commit message 标注**: 标题含「(协作收尾)」后缀, 描述中明确
   「不动其他 agent 的主体逻辑」+ 列出具体修复点
6. **push-guard 自动捎带**: push 后 push-guard 会自动捎带其他 agent 的 commit,
   因为 git-pull-rebase --autostash 已同步

### 12c. 并发 commit 防混淆 SOP (2026-08-19 立)

当多个 agent 同时 commit 同一文件时, 可能出现:

- 你的 commit message 描述的是 A, 但 diff 里包含 B 的修改 (race condition)
- 因为另一个 agent 在你 commit 期间 `git add` 了相同/不同文件

防混淆措施:

1. **精确 add**: 用 `git add <具体路径>`, **禁止** `git add .` / `git add -A` / `git add -u`
2. **commit 前验证**: `git diff --cached --stat` 必须与预期文件清单完全一致
3. **commit 后审查**: `git show HEAD --stat` 立即检查 commit 内容, 若含无关文件 → `git reset HEAD~1` 重提
4. **避免同时改同一文件**: 如果发现其他 agent 在改相同文件, 等他完成后再动
5. **接受混合 commit**: 当混合 commit 已 push 到 main 且功能正确, 不要 force-amend, 加一个空 commit 标注"混合 commit 说明"即可

历史案例: d6e8906 + f028e5517b 期间出现"scripts 改动 + api 改动 + mobile-rn 改动"被同一 commit 收录, 后续审计应见此节说明。

### 12d. git worktree 多会话隔离规范(2026-08-31 立,stash 丢失 + 守门误伤事故根治)

- **触发背景**(真实事故链,同日三连):① 多会话共享 working tree,某会话 `git stash push/pop` 冲突导致另一会话已完成的 llm_gateway.py 拦截代码在提交中丢失(提交 diff 只剩 63+/58- 格式化差异);② 守门第 8 项 check-api-routes 全量扫描工作区,104 处"前端调用无后端路由"全部来自其他会话未完成文件,正常提交被阻塞;③ push 门全量 typecheck 报上千 TS 错误全部来自其他会话工作区噪音,推送反复被阻;④ 2026-08-31 补充事故:d22d233091 的 commit message 声称含 §12d 规范但 AGENTS.md 实际 diff 仅 1 行——§12d 本体在提交前被并行会话覆盖工作区而丢失,**commit message 声称的规范条目必须与实际 diff 一致**。根因:**多会话共享同一 working tree + 同一 git index**。
- **第一优先:单写者原则**——同一时刻只允许一个会话写 working tree;并行会话开工前先确认其他会话已收尾(无未提交改动、无进行中 stash)。
- **确需并行写时必须用 worktree 隔离**(与 §9b 单分支规则协同):
  - `git worktree add --detach ../IHUI-AI-wt-<任务名>`(detached HEAD,不占分支名,不违反 §9b)
  - worktree 内正常开发 + commit(本地 sha 可引用;worktree 无 node_modules,hook 必败,可 `--no-verify`)
  - 完成后回主 worktree `git cherry-pick <sha>` 收编,随主 worktree push
  - 收编后立即 `git worktree remove ../IHUI-AI-wt-<任务名>` + `git worktree prune`
- **worktree 内约束**:venv/node_modules 各自安装;端口不得冲突(docs/port-management.md 注册表);共享 DB/Redis 时 schema 迁移互斥。
- **守门兜底(2026-08-31 已落地)**:即使未用 worktree,守门已支持 staged-scope 降级防误伤——① `check-api-routes.mjs`(pre-commit 第 8 项)仅收集暂存区前端文件调用点,暂存区无前端文件→跳过,暂存区为空(手动跑)→保持全量;② 新增 `scripts/check-typecheck.mjs` 包装 push 门全量 typecheck(**判据 = 本次改动范围**:优先 `PUSH_SCOPE_FILES`(pre-push 依 git 传入的 remote_sha..local_sha 计算),暂存区仅兜底;报错文件均不在改动范围内→降级警告放行;解析不到报错文件=tsc 未真正运行→按失败,宁误拦不放过);③ `.husky/pre-push` 第 2 段接入 `node scripts/guardian-runner.mjs --push-gate` 编排。自检:`node scripts/check-typecheck.mjs --self-test`(新增样例 8-12 覆盖 refspec push 与 Next.js 路由组括号路径)。
  - **2026-09-03 push-scope 修复(必读,曾致 push 反复被硬拦)**:原判据只用暂存区,而 `git push <sha>:<ref>` 这类 refspec 推送**不产生暂存区**,若此刻他人也没 staged 文件,降级直接失效 → 他人并行会话的半编辑态报错(实测 miniapp-taro TS1005、web TS2345,单独复验均 0 错误)会硬拦本次 push。故 pre-push 先缓冲 stdin(`PUSH_REFS="$(cat)"`)再回喂 git-lfs,并据 `remote_sha..local_sha` 计算改动文件导出 `PUSH_SCOPE_FILES`;改动文件 >300 时清空该变量退回暂存区兜底(env 有长度上限,截断会漏判→宁可不降级)。同修一处不安全缺陷:tsc 报错正则原排除括号,把 `app/(main)/xxx.tsx` 截断成 `/xxx.tsx`,导致范围内文件匹配不上而**误放行**,现改为「扩展名 + `(\d+,\d+):`」双锚定。
- **全流程已实战演练验证(2026-08-31,主仓零残留)**:worktree add --detach(9785 文件)→worktree 内 commit(--no-verify)→主仓 `cherry-pick --no-commit` 收编验证无冲突→`git restore --staged -- <file>` + 删除文件精准撤销→`git worktree remove` + `git worktree prune`。细则:① cherry-pick --no-commit 验证后**必须立即撤销**,验证/撤销对在同一 git-lock 单元内紧凑完成,防暂存文件被并行会话的 commit 卷入;② 演练/临时文件删除用 `Remove-Item -LiteralPath`(回收站式删除会失败)。
- **应急:主 index 写锁/损坏时用 GIT_INDEX_FILE 旁路提交**(2026-08-31 实战验证,d22d233091 即此法提交):症状为 `fatal: Could not write new index file.`(objects 可写、磁盘充足、无 index.lock)→ 主 index 被外部句柄锁定。手法:`$env:GIT_INDEX_FILE = "$env:TEMP\ihui-index-recover"` 后照常跑 safe-commit 全流程(写入临时 index,主 index 不被触碰);旁路期间 staged-scope 守门读到的暂存区恰为本次声明文件,反而更精准。收尾:`Remove-Item Env:\GIT_INDEX_FILE` 必须清除防污染后续命令;事后 `git reset` 修复主 index(`git write-tree` 应返回非空树)。
- **stash 清理零损失流程(2026-08-31 实例:backup/stash-temp-other-sessions-8e1863c)**:`git tag backup/stash-<名>-<sha7> '<stash-ref>'` → `git rev-parse` 验证 tag 与 stash SHA 一致 → `git stash drop '<stash-ref>'`。tag 指向原 stash commit,内容永不丢失,随时 `git stash apply <tag>` 可恢复。
- **stash 后当日 apply 落地(2026-09-08 立,stash 丢失事故根治)**:任何 `git stash push` 的内容**必须当日 `apply` 落地或 tag 零损失备份**——stash 是黑盒(不提醒/不过期/不可见),滞留 stash 在并行会话合流推进下必然造成"功能被回滚"假象(2026-09-08 实证:压缩入口整合 + IM 聊天室重写 775 行双双滞留 2-3 天)。守门:`scripts/check-stale-stashes.mjs`——guardian-runner 第 30b 项(blocking,≥48h 含源码改动未备份→阻塞 commit)+ dev-web.mjs/apps/web predev 启动 warn 扫描;跳过 `HUSKY_SKIP_STALE_STASH_CHECK=1`。
- **全面禁止 git stash(2026-09-18 立,机制级根治,取代"当日 apply"条——从源头禁止,不再产生新 stash)**:任何情况下禁止 `git stash push` / `git stash`(含路径限定、含 -a/-u),**推了就是要生效**——改动必须直接 commit(走 safe-commit.mjs 多 agent 流程)或用本节 worktree 隔离,绝不允许藏进 stash。已落地 git 层面硬拦截:`.husky/reference-transaction` hook 在 prepared 阶段对 refs/stash 新建事务 exit 1 → `git stash push` 直接 `fatal: reference-transaction hook aborted`(`--no-verify` 也无法绕过;git 无 pre-stash hook,refs/stash 写入走引用事务故可拦)。hook 只拦"新建"不拦"清理"(drop/pop/clear 放行),纯 sh 内建实现不依赖 grep/sed。唯一例外=清理存量 stash:`IHUI_ALLOW_STASH=1` 前缀执行,且必须先按上方零损失流程 tag 备份、在会话报告声明放行原因。
- **红线**:
  - ❌ 禁止一切 `git stash`(2026-09-18 起全面禁止,见上方"全面禁止 git stash"条款,hook 已硬拦截;历史事故:无路径限定全仓快照曾致 stash@{0} 沦为 1781 文件巨型快照)——改动直接 commit 或 worktree 隔离
  - ❌ 禁止在非 worktree 场景用 detached HEAD 承载长期开发(commit 游离无引用)
  - ❌ 禁止 commit message 声称未落盘的规范/代码条目——commit 前 `git show --stat`(或 `git diff HEAD --stat`)核对声称内容与实际 diff 一致(2026-08-31 d22d233091 事故教训)
  - ✅ worktree 收编前 `git log <sha> --stat` 核对内容,收编后 `git worktree list` 确认清理
  - ✅ 怀疑脚本被外部进程(水印/注入)污染时:`node --check` 取证 → `git show HEAD:<path>` 验证基线 → `git restore --source=HEAD --worktree -- <path>` 恢复 → 冒烟(2026-08-31 git-lock.mjs 修复实例:水印进程追加零宽 Unicode 行致 SyntaxError)

---

### 12e. workspace 加依赖禁用 `pnpm install --filter`(2026-09-23 立,自伤实测)

- 实测:为 `apps/extension` 加 `@ihui/design-tokens` workspace 依赖时跑 `pnpm install --filter @ihui/extension`,pnpm 按"只装被选中项目所需"重链接,**顺带剪掉根 `node_modules` 里未被该包引用的链接** —— `lint-staged` 就此消失,`.husky/pre-commit` 第一步即崩,每次 commit 都失败并逼出 `--no-verify`,连带 109 道守门全废(而 `git status` 与 typecheck 都看不出依赖树被削)。
- **规则**:本仓任何"新增/调整 workspace 依赖"一律跑**全量 `pnpm install`**(不带 `--filter`);改完必须验证 `node_modules/lint-staged/bin/lint-staged.js` 与 `node_modules/.bin` 关键入口在位,再提交。
- 排查同类问题的顺序:`grep "Cannot find module" .workbuddy/hook-logs/pre-commit.log`,先怀疑依赖树被动过,再怀疑守门判据。**但"根 `node_modules/.bin` 缺 shim"这一型既不看 package.json 也不报 Cannot find module** —— 它表现为 lint-staged 第一步 `'eslint' 不是内部或外部命令` → 每次 commit 必红 → 各会话合法 `--no-verify` → 约 110 道守门对全队同时失效(2026-09-24 实测连吃三次)。此型 `pnpm install` 与 `pnpm install --force` 都在 1.2 秒内回 "Already up to date"(pnpm 认为树是好的),**上一行那个 remedy 不起作用**;唯一生效的是 `node scripts/repair-node-bin-links.mjs`,且**判据只认实测版本号**:`node_modules/.bin/eslint --version` 与 `tsc --version` 必须都出版本号(或 `pnpm exec eslint --version`)。
- 排查同类问题的顺序:`grep "Cannot find module" .workbuddy/hook-logs/pre-commit.log`,先怀疑依赖树被动过,再怀疑守门判据。

## 13. 文件修改持久化强制规则(强制)

- 任何文件修改后**必须立即用 Read 验证**修改已落地(防止文件系统缓存不一致)。
- 大文件(>500 行)修改后,Read 验证时**必须读取修改区域 ±50 行**,确认上下文完整。
- 若 Read 返回内容与预期不符(陈旧缓存),**必须**重读最多 3 次,仍不符则停止并报告用户。
- **禁止**基于未验证的"假设修改已成功"继续后续操作。

---

## 14. Agent 自主验证强制规则(强制)

- Agent **必须独立完成**它能完成的验证(browser_use / API 测试 / 文件检查 / 命令执行),**禁止**要求用户代为验证。
- **禁止**在交付报告中写"请你刷新浏览器查看效果" / "请你启动 dev server 验证" / "请你手动测试"等甩锅措辞。
- 验证失败时,记录失败原因 + 已尝试方法 + 建议下一步,**不**得假装验证通过。

---

## 15. 工作区卫生强制规则(强制)

**禁止项**:① 在 `G:\` 根目录创建任何文件;② 项目数据(扩展打包/Chrome profile/构建副本/临时 DB/临时配置)写到项目外路径;③ 硬编码 `C:\temp\ihui-*`/`$env:TEMP\ihui-*` 等项目外路径;④ agent 用 RunCommand/PowerShell/Out-File/Set-Content/New-Item 在项目外直接创建文件;⑤ 在 `G:\` 根目录运行 Qt 类外部工具或执行 pnpm 命令(会创建 `.pnpm-store` v11 冲突);⑥ 硬编码中文绝对路径(GBK 乱码)。路径推导用 `$PSScriptRoot`/`__dirname`/`import.meta.url`。唯一例外:纯系统日志(`debug.log`/`next-server.log`)可写 `$env:TEMP`。

**必须用项目内路径**(根 `g:\IHUI-AI`):扩展打包→`apps/extension/.output/chrome-mv3/`;Chrome profile→`.ihui-agent/tmp/chrome-profile/`;临时副本→`.ihui-agent/tmp/<任务名>/`;临时脚本→`.ihui-agent/tmp/<脚本名>.ps1`;临时文件统一放 `.ihui-agent/tmp/`(已 gitignore),任务完成后清理。

**守门脚本**:

- `check-workspace-hygiene.mjs`(第 25 项 BLOCKING:项目外路径写入;WARNING:硬编码中文路径)
- `check-parent-pollution.mjs`(第 26 项 BLOCKING:项目父目录递归 2 层+桌面根级+用户主目录巡查,命中=文件名强信号 `search_*.ps1`/`*_result.txt` 或内容双信号)。**2026-09-23 结构性加固**:新增 `CREDENTIAL_DIR_NAMES` 共 8 项,对 `secrets`/`secret`/`credentials`/`credential`/`密钥`/`certs`/`certificates`/`.pybcrypt` **整目录不扫不删**(目录名按 `toLowerCase()` 比对,大小写不敏感;既往逐 `USER_LEGIT_PATTERNS` 按 filename 豁免已被证明会漏第三把密钥);另新增 `BACKUP_DIR_NAMES` + `BACKUP_DIR_PREFIXES` 整目录豁免,防"卫生守门删掉数据备份"。
- `cleanup-external-junk.ps1`(G:\ 垃圾清理,16 目录+31 文件,`-Force` 跳过确认)
- `g-root-guardian.ps1` v2.0(G:\ 实时守门,FileSystemWatcher+白名单优先 5 层判定,~110-222ms 删除,Windows 计划任务自启)+ 配套 `g-root-blacklist.json`/install/uninstall/status 脚本
- post-commit 自动 `--auto-clean --quiet`(仅清文件名强信号);定时 08:00 巡查;跳过 `HUSKY_SKIP_HYGIENE=1`。**⚠️ `--auto-clean` 对强信号命中直接 `unlinkSync` 实删文件、无任何二次确认** —— 清理类任务的铁律:先跑不带 `--auto-clean` 的 `node scripts/check-parent-pollution.mjs` 看命中清单并逐项验明身份,**命中项落在凭据/密钥目录内或名字含 key/secret/token 的,一律先补豁免再清理,顺序不可颠倒**(2026-09-22 实测用户口令表 `D:/DevEnv/secrets/ihui-app-password.txt` 因 `ihui-` 前缀被判为 agent 垃圾 —— 若不先补目录级豁免而直接跑 `pnpm hygiene:parent:clean`,它**会被无声删除**;该文件现仍完好)。历史案例见 `.ihui-agent/archive/AGENTS_history.md`。

### 15b. 项目外落点唯一制(强制,2026-09-23 立)

用户规定:**任何文件都不得写在项目文件夹之外**,只允许下述四个经批准的落点;不再新增第五个。
禁止在家目录、盘根、`AppData` 下随手建目录 —— 本机曾因此散落 `D:\tmp-*`、`D:\c`、`D:\d`、
`D:\IHUI-AI-backup-*.tar`(8.3GB)等十几个游离项,已收口。

| 落点                                 | 用途                                                                                                                                  | 依据 |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ---- |
| `D:\IHUI-AI\`(含 `.ihui-agent\tmp\`) | 源码、构建产物、一切临时物                                                                                                            | §15  |
| `D:\DevEnv\backups\`                 | **唯一备份目录**:`git\ sql\ deploy\ archives\ desktop\ pg\ env\ releases\`。项目内不得留备份(`backups/` 曾压 64MB DB 快照,已外迁)     | §15b |
| `D:\DevEnv\{cache,tools,runtimes}\`  | 工具链缓存唯一根;家目录里的工具状态一律 `robocopy /MOVE` + **junction** 改道(禁改 `HKCU\Environment\Path`),旧路径经 junction 仍可解析 | §26  |
| `D:\DevEnv\Temp\`                    | `TEMP`/`TMP`/`TMPDIR`(HKCU,需新开终端才继承)                                                                                          | §26  |

> **上表的 `D:` 是"D 盘那份 checkout"的历史值,不得当本机现值照抄**(2026-09-24 实测,登记于 PROJECT_PLAN —— **本行 2026-09-24 被并发旧基线回写带走过一次,同日原地补注使其重新成为"本侧修改"**
> 第三十三批 续五⑦):`gitArchiveDir()`(`scripts/lib/gitdir.mjs`)按**工作树所在盘**推导,工作树在
> `G:\IHUI-AI` 时它返回 **`G:/DevEnv/backups/git`**;而 `D:\DevEnv`(Temp/backups/kc-tools/logs)与
> `G:\DevEnv`(Temp/backups/cache/tools)**同时存在**,各自服务所在盘上的 checkout。
> **规则:凡脚本需要归档/备份/临时落点,一律 `import` 出口函数(`gitArchiveDir()` / `gitdirArchivePath()` /
> `resolveBackupDir()`),禁止再硬编码盘符** —— 硬编码的结果是"备份落点解析到不存在的路径",
> 表现为 `git-guardian --status` 的 `backupOk:false` 这类静默失效(§5b 已记过一次同型)。

**显式例外与已改道项(改道用 junction,故旧路径仍可用)**:

1. `D:\IHUI-AI-git-repo` —— 真 gitdir。§5b 实测宿主层会整体删除工作区内 `.git`,故必须在外;
   `scripts/git-rebuild-local.mjs:169` 硬编码该路径,`git-refs-heal.mjs` / `git-guardian.mjs` 依赖其
   `refs-manifest.json`。同族的 `IHUI-AI.git-backup-20260912` 与 `IHUI-AI-git-repo.broken-*` 由
   `scripts/lib/gitdir.mjs:250` 按路径主动选读,一律禁删。
   **但"必须在盘根"只是历史状态**:2026-09-23 起,gitdir 的**备份与现场归档**统一落
   `D:\DevEnv\backups\git\`(单一真相源 `gitArchiveDir()` / `gitdirArchivePath()`),
   因为旧写法 `${GITDIR}.broken-<ts>` 每次守护/重建归档都必然在盘根长一个新目录(实测累计 3 个 /
   1.94GB)。盘根因此从 6 项收口到 2 项(项目 + 活 gitdir);回归测试
   `scripts/tests/gitdir-archive-paths.test.mjs`(4 例,含"调用点必须真用该出口"的装车断言)。
   **改这类路径必须同批改 `resolveBackupDir`**:它曾只认写死的 `D:/IHUI-AI.git-backup-20260912`,
   目录一迁走就解析到不存在路径,表现为 `git-guardian --status` 的 `backupOk:false` —— 本地恢复源
   静默失效且无告警(与同日生产部署冻结同属"凭据/路径过期只以下游门禁失败形态出现")。
2. `F:\BaiduSyncdisk\密钥\`(盘符按 §5d 由 `scripts/lib/key-dir.mjs` 探测,不得写死)——模型密钥唯一权威源(§5d),不入仓、不入聊天记录。
3. 第三方 IDE/agent 自管家目录的**运行态**(`~/.workbuddy\binaries\PortableGit` 是
   `scripts/lib/gitdir.mjs:36-37` 解析 git 二进制的首选;`.qoder-cn` 承载本项目记忆与工作区状态)。
   这类不属"我们的产物",只登记、不搬动。
4. `~/.ihui`(1.2MB / 693 文件)—— 我们 CLI 的全局状态,**2026-09-23 已按 §26 的 junction 机制改道**
   到 `D:\DevEnv\cache\userhome\.ihui`(复制后逐文件校验 0 差异 → 删源 → `mklink /J`,**未改一行业务代码、未设任何环境变量**)。
   之所以不用"设 `IHUI_HOME`"这条路:`IHUI_HOME` 当前在本仓有**两套互斥语义** ——
   `apps/cli/src/plugins/paths.ts:31` 当它是**家目录**(再拼 `.ihui`),而
   `apps/cli/src/tools/mcp-oauth.ts:43`、`mcp-credentials.ts:26`、
   `apps/ai-service/app/core/message_history.py:33` 当它是**状态目录本身**;另有
   `workspace-ai-service.ts:38/252/2578`、`workspace-ai.ts:714/730/807`、
   `announcements/index.ts:117`、`refresh-cli-token.mjs:34` 与 `apps/cli` 内约 30 处裸 `homedir()`
   完全不认它。**设 env 必造双根分裂**;junction 则对硬编码路径同样生效。
   语义统一属独立技术债(动它需同步改 `plugin-marketplace.test.ts:114-129` 等被钉死的断言),不在本策略范围。

**新写文件前的三问**:① 是源码/产物吗 → 项目内;② 是备份吗 → `D:\DevEnv\backups\<类>\`;
③ 是临时物吗 → `TEMP`。三者都不是 → 停下来问用户,不得自建新目录。

---

## 16. Push 阶段跨 Agent 改动保护规则(强制)

- 本 agent 完成 commit + push 后,不再触碰 working tree,不执行 `git pull` / `git fetch` / `git rebase` / `git push --force`。
- 抹除其他 agent 改动 → **协作事故**;混入其他 agent 改动到自己 commit → **污染事故**;修改其他 agent 文件"帮他们修" → **越权事故**。
- `--no-verify` 跳过 hook 的合法性:见 §12 最后一条(hook 失败因其他 agent 代码 → 合法跳过;本任务代码 → 禁止跳过);`--no-verify` 不是流程事故,前提是本任务改动文件已通过 typecheck + lint + build。

---

## 17. UI 改动验证强制规则(强制)

**触发条件**:UI 样式/布局/交互改动(CSS/className/style/组件结构/Tailwind 类/shadcn props)。

**强制动作**(缺一不可,违反视为交付事故):

1. 改码前 browser ping `http://localhost:8801` 确认服务在线,不通则先启动 web+api+ai-service(端口见 `docs/port-management.md`)。
2. 改码后确认 web+api 服务在跑(browser 实际访问)。
3. 用 browser_use subagent 渲染目标页面,截图自验 4 状态:默认/hover/active/dark mode。
4. 读 DOM 数值验证样式生效(`getAttribute`/`getComputedStyle`,禁止只靠截图)。
5. 交付附 4 状态截图 + "已自验通过"声明;服务起不来禁止交付。

**commit message trailer**:含 `apps/web/**/*.css` 改动必须附 `Verified-DOM: http://localhost:8801/<path> (<DOM 属性=数值> ...)`,commit-msg hook 自动守门。

**Next.js CSS 缓存陷阱**:改 globals.css/styles 后 HMR 不一定重编译 CSS chunk,必须 curl 当前 CSS chunk 验证新值;`grep -c` 返回 0 → kill 旧 next-server 重启 `pnpm --filter @ihui/web dev`,等 15s 重新 curl 确认。

**工具故障应急**:dev server 永远只在集成终端内部运行(`RunCommand long_running_process`+`blocking=false`),禁止 `Start-Process` 派生独立窗口。RunCommand 连续 2 次返回空输出 → 判定失联 → 告知用户在集成终端面板手动执行。工具反复失败时先 Grep project_memory.md 查已知约束。

**豁免**(允许跳过 browser_use):① 纯后端 API(curl 验证);② 纯类型/工具函数(typecheck+test);③ dev server 30 分钟无法修复(降级单元测试);④ CI 环境(e2e)。历史案例见 `.ihui-agent/archive/AGENTS_history.md`。

---

## 18. 启动项目语义(强制)

用户说"启动项目" = 前后端全链路同步启动:web + api + ai-service(端口见 `docs/port-management.md`),必要时检查并启动数据库 / Redis。禁止只启动前端就交付。

---

## 19. i18n 约束规则(强制)

### 翻译文件语言纯度

- **zh-CN.json**:基准语言文件,其他 4 语言必须 parity(key 集合完全一致)。
- **zh-TW.json**:繁体中文,禁止简体字(pre-commit 第 2b 项,opencc 字形转换检测,阻塞)。
- **ko.json**:韩语,禁止中文残留(第 2c 项,字符范围检测,阻塞)。
- **ja.json**:日语,禁止中文残留(第 2d 项,warn-only,日文汉字词易误报)。
- **en.json**:英文,禁止中文残留 + 禁止破碎机翻(第 2e 项,阻塞)。

- **parity 绿不等于内容语种对(2026-09-26 立,由 D20 实测逼出;同日已配尺子 = 守门 133)**:上面五门判的是**键集**与"**有没有**中文残留",所以"某一块整体落成另一语言"这一型结构上看不见。实测抓到三种形态,当时全部门禁都是绿的:① `aiChat.org` 的 **ja 装了繁体、zh-TW 装了谚文**;② `chat.exportMenu.exportPdf` 是 **ja←zh-TW←ko←ja 的三向轮转**(判据必须按**码位**量,按字形肉眼判会把谚文 `을 익스포트` 与假名 `をエクスポート` 看成同一种东西);③ `aiChat.toast.orgSaved` 被机械写成 `orgSaved:{orgSaved:"…"}` **自套一层**,`t('toast.orgSaved')` 取到对象 ⇒ 功能成功但提示永远不响。**批量落 i18n 后必须做两件事**:逐语种量"这块内容是不是这门语言",以及全量扫"同名自套一层"。**尺子**:`scripts/check-i18n-locale-content-language.mjs`(唯一码位族表在 `scripts/lib/i18n-script-families.mjs`,五族区间两两不相交由 `rangesAreDisjoint()` 机器证明)。**三条能力边界要照抄进判断**:汉字族内部**简繁码位判不了**(日本新字体与中国简化字同码位:気/会/図/点),所以只在"族级矛盾"上判红;`en` 整门 L1 刻意跳过(品牌原文必须留汉字);超过 4000 叶的大块计"未判定"而非通过 —— `--strict` 因此**恒 exit 2 直到未判定被清**,那是它拒绝出合格证,不是仓库故障。**定级**:L1 默认档只报数、`--strict` 判红(建门实测候选 16 处,当场 blocking 就是恒红门 ⇒ 逼人跳门连带废掉全部守门,§12e 同型);L2 按每文件 HEAD 自身存量棘轮(建门实测**存量 5 处,不是本节先前写的"现仓 0 处"** —— 该数字已被下面这条实测推翻并就地改写,照旧文派单会去找一个不存在的零存量)。问责入口 `pnpm check:i18n-locale-content-language`(默认档,已串进 `check:all`)/ `pnpm check:i18n-locale-content-language:strict`。**建门当天顺带抓到两件事**:① 门自己第一版把"块级 4000 叶上限"误套在**文件**级 ⇒ `web/*.json` 22,702 叶只扫 4,000,连真事故都看不见;② 上面①②③三处内容**我曾按 `b4d2305c6` 改对过一次,被并发提交 `9ccfd31a8` 拿旧基线整文件写回,五道 i18n 门与部署环一路绿灯**,直到这道新门复验才现形(内容由 `55e772b78` 重新改对)。⇒ **通用教训:语言包这类多会话共写文件,"我改过了"不构成事实,判据必须在被审面上现读。**

### JSON 重复键禁止

- 同一对象内禁止重复 key(`JSON.parse` 时最后一个生效,前面被 shadowed)。
- 添加新键前 Grep 确认同级命名空间无同名块(models/nav/sort/market 等高频命名空间尤需注意)。

### 翻译策略(source of truth:`scripts/brand-glossary.json`)

- 品牌名/公司名/字体名/技术术语:优先 canonical 英文名(智谱清言→Zhipu AI, 宋体→SimSun, 物联网→IoT)。
- 人名:fictional/示例数据用拼音(李思涵→Li Sihan / 리쓰한 / リ・スハン);称呼符合目标语言习惯(李总→이 대표)。
- 占位符 `{var}` / `{{var}}` 必须原样保留。
- zh-TW 用繁体字形(简体→繁体);ko 用 Hangul;ja 汉字词允许(登録/確認/削除)但简体字残留改日文习惯;en 禁破碎机翻(如 AgentDevPlatform)。

**守门工具**:`scan-i18n-zh-residue.mjs <locale>`(zh-TW/ko 阻塞,ja warn)/ `check-i18n-broken-en.mjs`(en 破碎机翻,阻塞)/ `check-i18n-keys.mjs --staged`(key 完整性+parity+白名单+**含点键**+**同层重复 key**,后两项 2026-09-20 起阻塞)/ `brand-glossary.json`(canonical 映射表)+ `apply-brand-glossary.mjs [--dry-run]` / `i18n-diff.mjs`(差异检测,输出 pending.json)/ `i18n-apply.mjs [--check]`(应用器,按 zh-CN 重排 key+parity 校验)/ `check-i18n-namespace-passing.mjs`(检测 useTranslations('xxx') 限定命名空间 + 把 t 传给 @ihui/ui-react 共享登录组件的 bug 模式,warn-only)。

**AI 翻译流水线**(强制,零用户算力):zh-CN.json 新增/修改 key 后必须执行:① `i18n-diff.mjs` 检测差异生成 pending.json;② AI agent 读 pending.json+brand-glossary.json 自行翻译;③ 写 i18n-translations.json(`{ translations: { [lang]: { [key]: value } } }`);④ `i18n-apply.mjs` 应用到 4 语言;⑤ `check-i18n-keys.mjs` 验 parity;⑥ `scan-i18n-zh-residue.mjs ko/zh-TW` 验无残留。

**守门集成**:web(第 2f-web 项 blocking,仅 staged 涉及 zh-CN.json 时);miniapp-taro(第 2f-miniapp-taro 项 blocking,`--target=miniapp-taro`);多 agent 并行时其他 agent 改 target locale 不触发阻塞。

---

## 20. 任务完成硬定义 — 杜绝"commit 后忘记 push"协作事故(强制)

### 任务完成的硬定义(5 条全满足才可声明"完成")

1. ✅ **本地有 commit**:`git log --oneline -1` 显示本次任务的 commit SHA
2. ✅ **工作区干净**:`git status --short` 无本任务残留 untracked / modified
3. ✅ **origin 同步**:`git push origin <branch>` 成功,stdout 含 `X..Y <branch> -> <branch>`
4. ✅ **HEAD 对齐**:`git rev-parse HEAD` === `git rev-parse origin/<branch>`
5. ✅ **守门脚本通过**:`node scripts/git-push-guard.mjs` exit 0

### 4 道自动防线

1. **pre-commit**:`check-push-sync.mjs`(guardian 第 29 项 blocking),commit 前检测本地 ahead(`git rev-list --count origin/<branch>..HEAD`),>0 阻塞;跳过 `HUSKY_SKIP_PUSH_SYNC=1`(不推荐);归档 commit `IHUI_ARCHIVE_COMMIT=1` 豁免。
2. **post-commit(主防线)**:`git-push-guard.mjs` 自动检测 ahead → push + 验证 local == remote,失败阻断提示手动 push;跳过 `HUSKY_SKIP_PUSH=1`(不推荐)。
3. **pre-push**:`.husky/pre-push` 第 2 段跑 `guardian-runner --push-gate`(2026-08-31 改版 / 2026-09-03 push-scope 增强:check-typecheck.mjs 包装,**判据优先 PUSH_SCOPE_FILES 本次推送改动范围,暂存区兜底**——报错文件均不在改动范围内时降级警告放行,防并行会话工作区噪音误伤,见 §12d),失败阻止 push(commit 仍本地保留);跳过 `HUSKY_SKIP_TYPECHECK=1`(不推荐)。
4. **手动兜底**:`node scripts/git-push-guard.mjs` 任何时候可手跑,打印 local vs remote HEAD,完全对齐 exit 0。

### 红线(违反视为协作事故)

- ❌ 禁止 commit 后只输出"已 commit"就声明任务完成,必须 push + 验证
- ❌ 禁止交付报告遗漏 "local HEAD == remote HEAD" commit SHA 对照
- ❌ 禁止用 `--no-verify` 绕过 `git-push-guard`(除非 typecheck 自验通过且显式说明)
- ❌ 禁止把 "git push 失败" 作为交付结论(必须修复后重推或显式说明阻塞原因)

### 交付报告必含证据

```
## Git 同步证据
- 本地 commit: <sha>
- origin commit: <sha>
- 同步状态: local == remote ✅ / 落后 N 个 commit ⚠️
- 守门脚本: node scripts/git-push-guard.mjs exit 0
```

### 工具失联处理流程

- **触发条件**:RunCommand 连续 ≥2 次返回 `{Exited, exit_code 0, 空输出}`(连 `Write-Output "test"` / `git --version` 都无输出),判定平台级故障。
- **红线**:禁止把 git 命令清单甩给用户作为交付物;禁止把"用户手动执行"作为完成结论;禁止用"工具失联"停止 retry;必须自己完成 commit+push+验证;工具失联时报告"blocked"状态不声明完成;工具恢复后立即执行 git 流程;唯一例外是用户主动说"我来手动执行"。
- **retry 策略**:首次失联用 `Write-Output "alive-test"` 探测 → 每隔 1-2 轮 retry RunCommand(可派 subagent 尝试) → 持续 retry 不放弃 → 恢复后立即执行完整 git 流程(add → commit → push → git-push-guard 验证)。
- 历史案例见 `.ihui-agent/archive/AGENTS_history.md`。

---

## 21. 功能开发同步更新 README 规则(强制)

### 触发条件

任务**新增 / 修改 / 删除**以下任一类别能力:

- 新功能模块(新增 P3 深度层 / IM 渠道 / 沙箱后端等)
- 现有功能重大调整(API 路由变更 / 架构重构 / schema 迁移)
- 守门规则 / 工程约束新增(本节本身即触发例)
- 项目对外能力清单变化(支持的平台 / 厂商 / 模型 / 端)

### 强制动作(缺一不可,违反视为交付事故)

1. **同步修改根目录 `README.md`**:在对应章节(功能特性 / 架构 / 平台支持 / 守门规则)更新文字 + 表格。
2. **README 改动必须与本任务代码同 commit 提交**:禁止"代码先 push、README 下一轮补"的分期模式。
3. **README 必须可被 git 远端可见**:commit + push 后 `git rev-parse origin/main` 必须包含 README 改动(由 §20 git-push-guard 自动验证)。
4. **交付报告必须含 "README 更新证据"**:列出修改的章节 + 行数变化。
5. **禁止以"下一步建议"形式把 README 同步留给下一轮**:本任务触发条件则 README 同步属本任务一部分,不得列为 P1/P2 遗留项或"最优下一步建议",违反视为交付事故。

### 豁免场景(允许不更新 README)

- 纯 bug 修复(不改变对外能力)
- 纯重构(不改变功能契约)
- 纯测试 / 文档 / 守门脚本改动(不改变运行时能力)
- 纯配置 / 依赖升级(不改变功能清单)
- 单端内部优化(不改变跨端契约)

### 守门(warn-only)

- `scripts/check-readme-sync.mjs`:staged 中有 `apps/` / `packages/` 下功能代码改动但 `README.md` 不在 staged → warn 提醒。
- 集成位置:`.husky/pre-commit` 第 22 项(warn-only,不阻塞 commit,只提醒)。
- 历史案例见 `.ihui-agent/archive/AGENTS_history.md`。

---

## 22. 防止 commit / push / merge 提交丢失硬性规则(强制)

### 触发背景(2026-07-25 立,真实事故)

reflog 记录 18:12-18:20 期间发生 **6 次 `reset: moving to HEAD~` 操作**,导致 3 个本地 commit 在 main 历史中消失:

- `15b984f90` "fix(api): P0 安全债并行修复"(ws-chat/ws-tasks IDOR + payment-gateway 金额反查)
- `5ef36e59d` "fix(web): sidebar 折叠按钮图标 16→20px" 第一次
- `b120c6e20` "fix(web): sidebar 折叠按钮图标 16→20px" 第二次

幸运的是这 3 个 commit 的工作内容已通过后续 commit(`ce3116ebd` merge + `ff7f744e0` 重做)重新整合到 origin/main,但 commit 本身已不可追溯,git log 不再显示原始 commit hash。

**根因**:多 agent 并行 + 某 agent 自动化流程使用 `git reset` 时,未考虑对其他 agent 本地 commit 的影响,导致 reset 把整个 commit 链(包括其他 agent 的工作)一并丢弃。

### 硬性规则(违反视为协作事故)

1. **禁止**在共享分支(任何已 push 过的分支,包括 main)使用 `git reset --hard`。
2. **禁止**使用 `git reset HEAD~N` / `git reset --soft HEAD~N` 撤销本地 commit(在多 agent 并行环境下,该 commit 可能被其他 agent 依赖)。
3. **禁止**使用 `git push --force` / `git push --force-with-lease`(已 push commit 的"撤销"必须用 `git revert`)。
4. **撤销已 push 提交必须用 `git revert`**:产生新 commit 撤销改动,保留原始 commit hash,所有 agent 都能看到完整历史。
5. **撤销本地未 push commit 推荐 `git revert`**(同样产生新 commit 保留历史);仅在确认 commit 内容无价值、且无其他 agent 引用时,才考虑 `git reset`(不推荐,需记录在 commit message)。
6. **禁止** `git stash drop` / `git stash clear`,除非先 `git stash show <id> --stat` 确认 stash 内容已合并到 working tree 或其他 commit;lint-staged 自动创建的 stash 必须保留至少到 commit 成功后下一次 git gc 周期(默认 14 天)。
7. **多 agent 并行 reset 前必须**:`git log --all --oneline | grep <other-agent-commit-sha>` 确认无其他 agent 引用;并在 `PROJECT_PLAN.md` 记录"reset 影响范围 + 已 tag 备份的 commit hash"。

### 已被 reset 丢失的 commit 永久记录(tag 备份)

3 个被 reset 的 commit 已用以下 tag 永久保留(防止 git gc 清理):

- `lost-commit/P0-security-debt` → `15b984f90`(P0 安全债)
- `lost-commit/sidebar-fold-btn-1` → `5ef36e59d`(sidebar 按钮第一次)
- `lost-commit/sidebar-fold-btn-2` → `b120c6e20`(sidebar 按钮第二次)
- `backup/pre-drop-recovery` → `251956eb6`(恢复前主分支快照)

可通过 `git show <tag-name>` 查看完整 commit 内容;`git tag -l "lost-commit/*"` 列出所有丢失 commit tag。

### 守门(blocking 升级已完成,2026-07-25)

`scripts/check-commit-loss-guard.mjs`(guardian-runner 第 30a 项,2026-07-25 升级为 blocking):

- ✅ **已升级 blocking**:`guardian-runner.mjs` 第 30a 项以 `node scripts/check-commit-loss-guard.mjs --blocking --filter-stash` 调用
- pre-commit 前扫描 `git reflog --all --date=iso` 最近 50 步(2026-07-26 扩),检测是否含 `reset: moving to HEAD~` 模式
- 扫描 `git fsck --unreachable --no-reflogs` 检测是否有悬空 commit
- 5 段检查流程(2026-07-26 强化):reflog reset / fsck 悬空 / lost-commit tag / backup tag / 远程 tag 完整性
- **仅远端 tag 已改为自愈式(2026-09-24)**:另一台机器推来的 `lost-commit/*` / `backup/*` tag 本地缺失时,本门自己 `git fetch --no-tags origin refs/tags/<t>:refs/tags/<t>`(分批 50、超时 60s,`IHUI_TAG_HEAL_MAX` / `IHUI_TAG_HEAL_TIMEOUT_MS` 可调),再 `git pack-refs --all --prune` 固化 —— 松散嵌套 `refs/tags/<ns>/*` 会被宿主清理层删掉,**不 pack 等于下次再红**(§5b)。拉回来即不红;离线 / 无凭据 / 超时只降级为警告并写明原因。理由:东西在**远端**、本地少一个引用根本不是 commit 丢失风险,而恒红的唯一结局是人人 `--no-verify`,把真正防丢的三条一起废掉。`reset` 检测 / 未备份悬空 commit / tag 对象不可达 **仍然 blocking**。
- 镜像测试 `scripts/tests/check-commit-loss-guard.test.mjs`(26 例)含端到端装车证明:本地 bare origin +「另一台机」推 tag → 守门须 exit 0、tag 必须真回到本机(证明不是"干脆不看远端"糊过去)、且落在 packed-refs。
- 发现 reset 操作或未备份悬空 commit → exit 1,阻塞 commit
- **悬空 commit 备份**:被 reset / drop 的 commit 一旦出现在 `git fsck --unreachable` 输出中且无 `lost-commit/*` tag 备份,即为"未备份悬空 commit",会阻塞 commit——需先执行 `git tag lost-commit/<name> <hash> -m "lost via reset"` 永久保留(防止 git gc 清理),再重新 commit
- 紧急跳过:`HUSKY_SKIP_COMMIT_LOSS_CHECK=1 git commit ...`
- 详细档案:见 [docs/lost-commit-archive.md](./docs/lost-commit-archive.md)

### 自动化 tag 同步(2026-07-26 立)

`scripts/sync-lost-commit-tags.mjs`(本任务新增)+ `.husky/post-commit` 第 5 段集成:

- **自动 push**:每次 commit 后自动 `git push origin --atomic refs/tags/lost-commit/* refs/tags/backup/*`,防止本地 git gc 清理 tag 后无远端备份
- **手动 fetch**:`node scripts/sync-lost-commit-tags.mjs --fetch` 一键从 origin 拉回所有 lost-commit/backup tag
- **手动 check**:`node scripts/sync-lost-commit-tags.mjs --check` 校验本地+远端 tag 一致性 + tag 对象可达性
- **package.json scripts**:`tag:sync` / `tag:sync:check` / `tag:sync:fetch` / `tag:sync:push`
- **紧急跳过**:`HUSKY_SKIP_TAG_SYNC=1`
- **触发背景**:2026-07-26 04:23 真实事故 — 本地 tag 被 git gc 清理,远端虽有但 fetch 失败(因为 fetch 默认不包含 tag,需要明确 refspec)
- **详细档案**:见 [docs/lost-commit-archive.md](./docs/lost-commit-archive.md) "🛡️ 防护机制" 段

**改道完整性已由机器看守(2026-09-24 立,守门 `check-home-junctions.mjs`)**:上面那三条人肉命令长期
等于没有校验 —— 实测 `AppData\Roaming\npm` 长成 **2.05GB**、`AppData\Local\pnpm-cache` **758MB** 都是实体目录
(同期 `~\.ihui`、桌面端两处 `com.ihui.desktop` 早已是 junction ⇒ 机制有效,**缺的只是回潮哨兵**)。
该门登记 16 项工具态,判三条:存在却不是指针 = `REAL-DIR` 红(且必须量出体积,否则报告写成"合计约 0 MB"的
假小量级)、指针目标不可达 = `DANGLING` 红、登记表被过滤空 = `EMPTY-REGISTRY` 红;非 Windows 如实报"未判定"
不计通过。**第三方 IDE 自管态(`.workbuddy`、`.qoder-cn`)刻意不进登记表** —— 由镜像测试反向钉死,
防止有人把它们加进来逼后人去挪别人的运行态(挪 `.qoder-cn` 等于丢记忆)。
同日已按 §26 机制把这两处收口(镜像复制→逐文件字节校验→源改名→`mklink /J`→经 junction 回读一致→才删源),
C 盘可用 44G → **45.75G**。

**但"能判"不等于"能修"(2026-09-24 补第二层)**:门 96 上线后当天,`D:\DevEnv\cache\userhome` 整棵被清
(13:15 前后,同分钟 `cache/Temp/tools` 被重建),16 项登记里 **已改道 0 / 违规 9**、4770MB 回到 C 盘。
门判红是对的,可它是 **blocking** 而修复只有人肉五步 —— 结果是**每一次提交都被逼成绕过钩子**,
一次绕过等于约 110 道守门对该提交全部作废(§12e 同型)。恒红门的唯一结局就是没人再守门。
所以补了幂等修复器 `scripts/re-home-junctions.mjs`( `--check` / `--apply` / `--reset-dst` / `--self-test` ):
登记表**复用门 96 的 `registryOf()`**、D 盘根**复用 `seal-c-root-stray` 的 `devEnvRoot()`**,不另立第二份表、
不写死盘符;流程仍是"镜像复制 → 逐文件(相对路径 + 字节)校验 → 源改名 → `mklink /J` → 经 junction
回读一致 → 才删源",任一步不符一律回退(源在校验通过前一字不动);robocopy 返回码 ≥8 视为失败。
**两条实测教训**:① 复制语义必须与校验语义一致 —— 要带 `/XJ /SL`,否则源内部的重解析点被 robocopy
展开成真实文件,而指纹两侧都跳过重解析点,永不收敛(`.cargo` 四轮稳定差 13 个即此;**我第一版把它
误报成"目标正被持续写入",是判据错、不是世界错**);② 被进程占用(EBUSY)的项由修复器自己记
30 分钟冷却,免得守护每 2 分钟重抄一遍 108MB 去撞同一个失败。触发点挂在 `git-guardian` 的
`healHomeJunctions()`(与 `healRootSeal` 同位、**早退之前**;每日 03:00 体检兜不住 23 小时空窗)。
实测:9 项违规 → 8 项已改道并逐字节校验一致(含 `.cargo` 947MB/21760 文件);剩 `~\.codex` 被别的会话
正在跑的 `codex-windows-sandbox-service` 占用 ⇒ 进冷却,该服务退出后 2 分钟内自动补回,**不杀别人的进程**。

**第三层:悬空目标必须能重建,冷却不得拦住人工窗口(2026-09-24 同日补,前向更正上面最后一句)**
上面"该服务退出后自动补回"是**错的**:`CodexSandboxService.OpenAI.Codex` 是 StartMode=Auto 的
LocalSystem 服务(实测 PID 6196,exe 在 `WindowsApps\OpenAI.Codex_*` 里、**不在 `~\.codex` 内**),
它**永不退出**,所以"等它释放"等于无限期恒红。处置是把窗口**造出来**而不是等:停服务 → `--apply` →
`finally` 里无条件启回 → 复核(服务 `Running`、事件日志 15 分钟内无 codex 相关报错、`Get-Item -Force`
的 `Attributes` 含 `ReparsePoint` 且 `Target` 指向 D 盘)。实测 **5732 个文件逐字节校验一致后改道**,
门 96 从"违规 1 / C 盘 108.8MB"到 **违规 0 / exit 0**。动手前必须先确认**只有该服务自身**匹配 codex
(无并发 CLI/IDE 会话),否则不 stop。

补这一层时暴露了两个真缺陷,都由自检抓出(不是演练抓的 —— **演练只能证明"会红",判据才证明"不会修"**):

1. **`repairOne` 的判序**:第一行是 `if (!existsSync(srcPath)) return 'absent'`,而**悬空 junction 的
   `existsSync` 返回 `false`**(它跟随重解析点,目标已被删)。于是"源不存在、无需改道"把这一整型吞掉,
   表现就是上面演练里"守护跑完什么都没补"。门 96 自己判的是 `!existsSync(p) && !isLink(p)`(两半都有),
   修复器只抄了前半 ⇒ **同一个判据在两处必须同形**,否则一边判红、一边判无需修,合起来是恒红且永不自愈。
   现判序改为先 `isLink`(lstat);并把"指针指向与登记表算出的目标不一致"单列 `link-moved` **判红交人工**,
   不擅自改指向 —— 那等于替人决定数据落点。
2. **冷却吞掉人工窗口**:第一次 `--apply --no-cooldown` 之前,上一轮 EBUSY 留下的 30 分钟冷却把服务
   已停好的窗口判成了"跳过"。故加 `--apply --no-cooldown`:**绕过判定但保留既有条目**(守护侧仍需拦住
   "每 2 分钟重抄 108MB 再撞同一个 EBUSY"),且本轮再失败时**不再续冷却**,否则下一个人工窗口照样被拦。
   两处都由镜像测试用正则钉死(`scripts/tests/re-home-junctions.test.mjs`),因为它们是行为分支,不是注释。

### 历史案例

`.ihui-agent/archive/AGENTS_history.md` 记录每次 reset 事故 + 已采取的 tag 备份措施。

---

## 22b. staged-typecheck 闸门:全量 include + 错误过滤(2026-08-18 立,根治改版)

### 触发背景

pre-commit 第 16 项「条件 typecheck 闸门」原策略:临时 tsconfig 只 include staged 文件 → 模块扩展(declare module 'fastify' 等)未被加载 → 报 TS2339 假阳性(如 `pushNotification` / `isMultipart` / `file`)。多 agent 并行时,任何 agent 改一下 ws-notifications.ts 都会让其他 agent 的 web 包 typecheck 失败 100% 误阻塞。

### 核心策略(根治)

`scripts/check-staged-typecheck.mjs`:

- **临时 tsconfig 沿用 package 原始 tsconfig 的【全量 include】**,完整加载所有模块扩展与全局类型,消除 TS2339 假阳性。
- **tsc 输出按行解析**,只把【错误文件属于 staged 文件】的错误视为失败;其他 agent 引入的非 staged 文件错误自动过滤、不阻塞。
- tsc 未能真正运行(如 pnpm/tsc 未找到、进程崩溃、空输出)按失败处理,**禁止静默通过**。
- 临时 tsconfig 清理加重试(Windows transient file lock 兼容);`.gitignore` 已加 `**/tsconfig.staged-typecheck.json` 防残留误入库。

### 守门集成

- `scripts/guardian-runner.mjs` 第 16 项 blocking(已有调用 `node scripts/check-staged-typecheck.mjs --staged`)
- 跳过方法:`HUSKY_SKIP_STAGED_TYPECHECK=1 git commit ...`(应急)
- 适用范围:任意 staged .ts/.tsx 文件(不限 apps/web)
- 自动跳过:apps/ai-service(Python,走 mypy)、apps/desktop(无 typecheck script)、packages/eslint-config / packages/tsconfig(纯配置包)等无 typecheck script / tsconfig 的 package

### 红线规则

- ❌ 禁止把临时 tsconfig 的 include 缩窄到 staged 文件(会回退到旧 partial-include 假阳性 bug)
- ❌ 禁止把 `**/tsconfig.staged-typecheck.json` 从 .gitignore 移除(Windows 偶发 unlink 失败可能残留)
- ❌ 禁止把 `sawAnyTscError` 移除(若 tsc 未能真正运行,必须按失败处理,不能静默通过)
- ✅ 修改 `filterTscOutputForStagedFiles` / `getOriginalInclude` 时必须同步更新 `scripts/tests/check-staged-typecheck.test.mjs`(镜像常量同步锚点:源脚本第 298-327 行 / 244-256 行)
- ✅ 修改源脚本 `check-staged-typecheck.mjs` 的核心函数时,必须同步:
  1. 测试文件 `scripts/tests/check-staged-typecheck.test.mjs` 中的镜像常量
  2. 跑 `scripts/check-staged-typecheck-mirror-sync.mjs` 验证指纹一致

---

## 22c. 镜像常量守门模式规范(2026-08-18 立)

### 背景

部分核心工具脚本(如 `scripts/check-staged-typecheck.mjs`)导出大量内部辅助函数,而测试文件 `scripts/tests/*.test.mjs` 由于路径隔离 / 模块副作用 / 静态导出冲突等原因,**无法直接 `import` 源函数**。沿用"测试文件里复制一份相同实现的镜像常量"做法虽然能跑通断言,但形成两套并行真相:任何对源函数签名的修改(参数顺序、返回值结构、过滤规则常量)必须**手动同步**到测试文件;一旦遗漏,测试将持续"假绿"(通过旧逻辑断言已不存在的字段),守门形同虚设。

### 根治路径(三步)

1. **源函数 export**:把核心函数从源脚本顶部 export 出去,确保测试环境可解析(注意 `.mjs` 必须用 `export` 关键字,且不引入副作用代码)。
2. **测试直接 import**:测试文件 `import { __test__ as <别名> } from '../source.mjs'`,消除"两份真相"。
3. **守门脚本检测 export 锚点**:`scripts/check-staged-typecheck-mirror-sync.mjs` 持续校验:
   - 源文件必须存在并 export `__test__` 对象
   - `__test__` 必须包含指定的函数键(本场景:`getOriginalInclude` / `normalizePath` / `filterTscOutputForStagedFiles`)
   - 测试文件必须存在对应 `import { __test__ as ... } from '../source.mjs'` 语句
   - 任意锚点缺失即 exit 1 报错(提示"镜像常量漂移,需走 §22c 协作收尾")

### 守门脚本工作机制(`check-staged-typecheck-mirror-sync.mjs`)

- **阶段 A · 检测源文件存在**:确认 `scripts/check-staged-typecheck.mjs` 存在于工作区(防止路径漂移)。
- **阶段 B · 检测 export const **test** 锚点**:用正则匹配 `export const __test__ = { ... }`,并校验三个键 (`getOriginalInclude` / `normalizePath` / `filterTscOutputForStagedFiles`) 均出现在对象字面量中(防止 export 但漏字段)。
- **阶段 C · 检测测试文件 import**:确认 `scripts/tests/check-staged-typecheck.test.mjs` 存在 `import { __test__ as ... } from '../check-staged-typecheck.mjs'` 语句(防止 import 路径写错 / 别名错配)。
- **触发场景**:任何对 `check-staged-typecheck.mjs` 的核心函数修改后,pre-commit hook 自动跑该守门脚本;若 export 锚点漂移,立即阻断 commit 并提示同步测试 import。

### 可复用模板

检测模式(正则 + 字符串匹配 + import 锚点)可复用到其他类似场景:

```javascript
// 伪代码:三阶段检测模板
const source = readFileSync('scripts/<source>.mjs', 'utf8')
assert(source.includes('export const __test__ = {'), 'phase B: missing __test__ export')
const requiredKeys = ['funcA', 'funcB', 'funcC']
for (const k of requiredKeys) assert(source.includes(`${k}:`), `phase B: missing key ${k}`)
const test = readFileSync('scripts/tests/<source>.test.mjs', 'utf8')
assert(
  /import\s*\{\s*__test__\s+as\s+\w+\s*\}\s*from\s*['"]\.\.\/<source>\.mjs['"]/.test(test),
  'phase C: missing __test__ import in test file',
)
```

可复用到:任何"测试文件无法直接 import 源函数"的工具脚本场景(如 `scripts/check-commit-scope.mjs` / `scripts/check-staged-pollution.mjs` 等)。

> 📌 配套使用: 本模板的入口守护需搭配 §22d `isDirectRun` 模式使用, 避免 import 时 main() 误触发。

### 红线规则

- ❌ 禁止在测试文件中**复制**源函数实现(产生镜像常量漂移风险)
- ❌ 禁止修改源函数签名后**不更新** `__test__` 对象的导出键(守门立即失败是预期行为,不是 bug)
- ❌ 禁止删除 `check-staged-typecheck-mirror-sync.mjs` 中的任何一项锚点检测(削弱守门强度)
- ✅ 修改源函数后必须 `pnpm test scripts/tests/check-staged-typecheck.test.mjs` + 跑 mirror-sync 守门脚本双验证
- ✅ 新增工具脚本若需要被测试直接 import,必须遵循"源文件 export `__test__` + 测试 import + 守门脚本三阶段检测"模板
- ✅ **判据的对象是某个真实文件的"形态"(标题式 / 路径式 / 字段名 / 分隔符)时,镜像测试至少一条用例的输入必须逐字取自那个真实文件**(或从 HEAD 现读一条),不得全部用自造夹具。实测于 2026-09-25 G-182:`archive-completed-tasks.mjs` 的匹配式与 `PROJECT_PLAN.md` 的真实标题形态漂了 11 天,而它的 15 个镜像测试**全绿** —— 因为夹具复刻的是实现的形状;更糟的是其中一条测试还把「必须以 `### [x]` 开头」当成规格去断言,**测试从防线变成了缺陷的掩体**。⇒ **镜像测试若只复读实现,它就只是复读机。**

---

## 22d. `isDirectRun` 模式规范:ESM 脚本"双形态"入口守护(2026-08-18 立)

### 背景

部分 `.mjs` 工具脚本(如 `scripts/check-staged-typecheck.mjs`)需要同时支持两种使用形态:

1. **CLI 直接执行**:`node scripts/foo.mjs`(命令行手动跑 / pre-commit hook 调用 → 必须自动执行 `main()`)。
2. **模块被 import**:`import { __test__ } from './foo.mjs'`(测试文件复用导出符号 → **绝不**触发 `main()` 副作用)。

ESM(`.mjs`)模块与 CJS(`.cjs` / `.js` + `"type": "module"`)的求值模型:**顶层代码仅在被 import 时执行一次**,但**直接 `node foo.mjs` 时同样会执行顶层代码**。这意味着 — 如果脚本顶层直接写 `main().catch(...)`,测试一旦 `import` 该模块,就会**连带触发 CLI 主流程**(批量跑 typecheck、写临时 tsconfig、调 git diff),把测试环境搞炸,或更糟 — 在测试用例之间留下真实副作用(临时文件未清理、staged 状态污染)。

### 根因:`import.meta.url` vs `process.argv[1]`

- **`import.meta.url`**:ESM 模块加载器注入,恒为**当前模块文件**的 `file://` URL(无论是被 import 还是直接 node 执行,值都相同:指向 `foo.mjs` 自身)。
- **`process.argv[1]`**:Node 启动时传入的第一个脚本参数,只在**直接 node 执行**时等于本模块路径;被 import 时为 undefined 或与本模块无关。

因此判定**"本模块是不是被直接 node 执行"** 可用:`import.meta.url === pathToFileURL(process.argv[1]).href`。两者相等 ⇒ 直接执行,触发 `main()`;否则 ⇒ 被 import,跳过副作用。

> ⚠️ **跨平台陷阱**:Windows 路径是反斜杠 `C:\foo\bar.mjs`,不能直接拼成 `file://` URL,否则 `import.meta.url`(始终是标准 `file:///`)与字符串永远不匹配。**必须**经 `node:url` 的 `pathToFileURL()` 归一化。

### 可复用模板

在 `.mjs` 脚本**底部顶层**(所有 export 之后)粘贴以下 4 行(已适配 Windows / Linux / macOS,推荐用项目现存形式而非手写字符串拼接):

```javascript
import { pathToFileURL } from 'node:url'

const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href

if (isDirectRun) {
  main().catch((e) => {
    console.error(`❌ ${e?.message ?? e}\n${e?.stack ?? ''}`)
    process.exit(2)
  })
}

export const __test__ = {/* 暴露给测试的核心函数 */}
```

位置约束:

- `__test__` 的 `export` 必须放在 **`if (isDirectRun)` 之后**(否则 ESM 提升可能在测试 import 时机前完成,语义仍正确但不直观;且与 §22c "镜像常量守门" 的 `import { __test__ as ... }` 锚点要求一致,便于 grep)。
- `main()` 自身声明应保持文件靠前位置(便于阅读),`if (isDirectRun)` 仅触发调用,**不**延迟或重写 `main`。
- `process.exit(2)` 复用 §22b 中"异常 vs 失败"的退出码约定(2 = 脚本自身异常,1 = 业务失败)。

### 与 §22c 的关系

`isDirectRun` 是 **§22c 镜像常量守门模式的使能条件**。没有它,§22c 的"源文件 export `__test__` + 测试 import"做法会因为 `main()` 顶层自动执行而不可行 — 测试一旦 import 就会触发 CLI 主流程,副作用污染测试环境(写临时文件、调 git 改 staged 状态、写控制台横幅噪音),`vitest` / `node --test` 都会在 setup 阶段立即崩。`isDirectRun` 把"CLI 入口"和"模块导出"两种用法的副作用隔离开,使 §22c 的 export / import 双真相消除得以落地。

两者协作的最小工作流:

1. 源 `.mjs` 脚本加 `isDirectRun` 守护(本节模板)。
2. 源脚本 export `__test__ = { fn1, fn2, ... }`(§22c 第 1 步)。
3. 测试文件 `import { __test__ as X } from '../source.mjs'`(§22c 第 2 步)— 因 `isDirectRun=false`,`main()` 不执行,仅拿到导出对象。
4. 守门脚本 `scripts/check-staged-typecheck-mirror-sync.mjs` 三阶段检测 export / key / import 锚点(§22c 第 3 步)。

### 红线规则

- ❌ 禁止在 **CommonJS / `.cjs`** 脚本中使用 `isDirectRun`(`.cjs` 没有 `import.meta`,需走 `require.main === module` 判定,本规范不适用;混用会导致 `SyntaxError: Cannot use 'import.meta' outside a module`)。
- ❌ 禁止把 `main()` 写成**同步函数**后用 `if (isDirectRun) { main() }`(同步抛错会抛出 toplevel,无法被 `.catch` 包裹,且 ESLint `no-top-level-await` 外的同步 throw 在 Node 18+ 表现不一致;必须保持 `main()` 是 `async` 或返回 `Promise`)。
- ❌ 禁止手写字符串拼接 `import.meta.url === 'file:///' + process.argv[1]` 替代 `pathToFileURL`(Windows 反斜杠永远不匹配,直接判定失败 → CLI 永不触发 main → 静默失控)。
- ❌ 禁止把 `isDirectRun` 守卫放到 `main` 函数**内部**(局部守卫无法阻止其他顶层 import 时副作用已执行的部分,如顶层模块加载就跑了 git diff / 写了临时文件)。
- ❌ 禁止把 `export const __test__` 移到 `if (isDirectRun)` **之前**且不放 `export`(否则测试 import 时拿到 undefined,§22c 守门 phase B 立即失败 — 这是预期行为,不是 bug)。
- ✅ 修改源脚本的 `main()` 调用约定时必须同步 §22c 守门(双验证 `pnpm test` + `node scripts/check-staged-typecheck-mirror-sync.mjs`),否则 `__test__` 导出语义与 import 时机解耦,可能让测试在 CI 通过而在本地 import 时副作用泄漏。
- ✅ 新增任何"需要双形态(CLI + import)"的 `.mjs` 工具脚本,必须沿用本节 4 行模板 + §22c export 规范,**不**为单次用例复制粘贴变体。

---

## 23. `.gitignore __*` 规则静默忽略 `__tests__/` 目录教训(强制)

### 触发背景(2026-07-25 立,真实事故)

`.gitignore` 第 154 行 `__*` 规则(本意是忽略"Agent 临时脚本",如 `__round1.log` / `__audit_dump`)会**静默忽略**所有以 `__` 开头的路径,包括合法的 `__tests__/` 测试目录。**关键陷阱**:`git status` 对 ignore 的文件**完全不显示**(既不 untracked 也不 modified),开发者肉眼完全看不到。

### 真实事故(2026-07-25 阶段 13 集成测试)

集成测试 subagent 在 `apps/web/__tests__/storage-adapter.test.ts` 写完测试文件,本任务合 stage 13 计划清单:

- 创建文件 → `git status` 不显示
- 编辑文件 → `git status` 不显示
- 准备 commit 时,`git add __tests__/` → 无任何文件可加(`__*` 规则吞掉)
- 险些导致整个 stage 13 测试集成**永久丢失**;直到最后 `git check-ignore -v apps/web/__tests__/` 才追查到 `__*` 规则源头

修复方法:把目录重命名为 `tests/`(避开 `__*` 规则)→ 后续 16 个 `__tests__/` 目录已审计,**全部未被 `__*` 规则命中**(未含 `.gitkeep` 且 `git check-ignore` 返回空),本任务仅作为守门+教训记录,不要求批量迁移。

### 硬性规则(违反视为事故)

1. **项目内测试目录必须用 `tests/`**(推荐)或 `spec/` / `__tests__/` + `.gitkeep`(不推荐);**禁止**新建无 `.gitkeep` 的 `__tests__/` 目录。`__tests__/` 命名只在"目录内全部文件被故意 ignore + 留 `.gitkeep` 占位"时使用。
2. **创建测试目录前必须** `git check-ignore -v <path>` 确认不被 ignore(空输出=安全,非空=被命中);`__` 前缀目录需逐个验证。
3. **CI / pre-commit 必跑** `node scripts/check-test-paths.mjs`(本任务新增,§守门脚本速查),命中 `__tests__/` 无 `.gitkeep` 且被 gitignore 吞掉 → 阻塞(退出码 1)。

### 守门脚本:`scripts/check-test-paths.mjs`

- 扫描 `apps/` + `packages/` + `scripts/` 下所有 `__tests__/` 目录
- 每个 `__tests__/` 调 `git check-ignore -v` 复核 + 检查 `.gitkeep`
- 同时检测 `*.tmp` / `*.bak` 目录 + 非白名单隐藏目录
- exit 0 (无阻断) / exit 1 (有 `__tests__/` 被吞掉阻断项)
- 详细用法:脚本头部 docstring

### 关联案例

- 阶段 13 集成测试 subagent:险些丢失 `apps/web/__tests__/storage-adapter.test.ts`
- 历史事故:曾出现"测试写了不显示 → commit 漏文件 → CI 假绿"模式,本节规则根治

---- **2026-07-26**:Commit 丢失防护机制强化。本地 lost-commit/* tag 被 git gc 清理事故暴露后,新增 sync-lost-commit-tags.mjs 自动 push + fetch 机制,创建 docs/lost-commit-archive.md 永久档案,check-commit-loss-guard.mjs 升级为 5 段检查(reflog 50 步 + 远程 tag 校验 + tag 对象可达性)。

---

## 24. 新增功能须用户确认强制规则(强制)

### 触发条件

任务类型 ∈ {新增功能模块 / 新增 API 路由 / 新增页面 / 新增端能力 / 新增对外能力清单 / 在 PROJECT_PLAN.md 中无对应条目的自发性功能开发}。

> 判定"新增功能"而非"修改/修复"的核心标准:**是否引入了 PROJECT_PLAN.md 当前任务清单之外的新能力**。是 → 触发本规则;否(属于现有功能的修复/重构/优化/适配)→ 不触发。

### 强制动作(缺一不可,违反视为越权事故)

1. **暂停开发**:识别到"新增功能"意图后,**禁止**直接动手写代码,必须先用 `AskUserQuestion` 工具向用户确认是否开发该功能。
2. **明确边界**:确认问题必须包含 ① 功能目标一句话描述 ② 受影响范围(端/文件/契约) ③ 是否纳入 PROJECT_PLAN.md(及其优先级 P0/P1/P2)。
3. **等待显式同意**:用户回复"同意/可以/做吧/开发"等肯定语义后才能开始;用户回复"不用/暂缓/先不做"则**禁止开发**;模糊回复需再次确认。
4. **补登记 PROJECT_PLAN.md**:用户同意后,将该功能追加到 PROJECT_PLAN.md 对应优先级末尾(遵循 §1),再开始编码。
5. **commit message 前缀**:新增功能 commit 用 `feat:` 前缀(§1 已规定)。

### 红线(违反视为协作事故)

- ❌ **禁止**未确认直接开发新功能(包括"顺手加一个"、"我看这里缺个"、"我加个小特性")。
- ❌ **禁止**把新功能伪装成"修复/重构"绕过确认(如新增路由写成"重构路由文件")。
- ❌ **禁止**在 `/goal` 模式下借目标执行之机擅自扩展目标范围外的新功能(违反 §8 红线"严格围绕目标,禁止扩展需求")。
- ❌ **禁止**用"已写完了,你看下要不要保留"代替事前确认。

### 豁免场景(允许不确认直接开发)

- **用户在本轮对话已明确要求开发该功能**(如"帮我加个 XXX 功能"、"做个 XXX")——用户意图已显式,无需再次确认。
- **/goal 模式目标条件内包含的功能**(目标已包含 = 已确认)。
- **纯 bug 修复 / 重构 / 类型修复 / 测试 / 文档 / 配置**(不引入新能力)。
- **守门脚本 / 工程约束自身的增量**(本节即触发例,属工程治理而非业务功能)。
- **PROJECT_PLAN.md 已有条目的细化执行**(任务已登记 = 已确认范围)。

### 与其他规则协同

- 与 §8(goal 模式)协同:goal 模式下不得借目标扩需求,新增功能必须先暂停 goal 询问用户。
- 与 §9(多端同步)协同:用户确认新功能后,默认按 8 端同步开发,除非显式标注"平台独占"。
- 与 §21(README 同步)协同:新功能开发必须同 commit 更新 README(§21 触发条件命中)。
- 与 §7(删除安全)协同:新功能开发中如需删除/重构现有代码,仍遵循 §7 三问。

---

## 25. `verify-*.mjs` / `verify-*.ts` 临时验证文件归档规则(强制)

### 触发条件

Agent 在调试 / 验证 / 探查某项功能时,常在 `apps/web/` / `apps/api/` 等源码根目录随手写一个 `verify-xxx.mjs` 脚本(如 `verify-permission-popover-v2.mjs` / `verify-permission-popover-v3.mjs` / `verify-login-tabs.mjs`)快速跑一次。这类临时文件**禁止**提交到 git,必须归档到 `.ihui-agent/tmp/<任务名>/`。

### 强制动作(缺一不可,违反视为协作事故)

1. **临时文件必须放 `.ihui-agent/tmp/<任务名>/`**:例如 `.ihui-agent/tmp/perm-popover-debug/verify-v2.mjs`。
2. _*禁止放 apps/* 根目录_*:`apps/web/verify-*.mjs` / `apps/api/verify-*.ts` 等位置**严禁** commit。
3. **禁止放 .ihui-agent/ 根目录**:`.ihui-agent/verify-*.mjs` 与守门脚本混在一起,难追溯。
4. **路径推导用项目内路径**:`$PSScriptRoot` / `__dirname` / `import.meta.url`,不写硬编码绝对路径(§15 卫生规则)。
5. **任务完成后清理**:`rm -rf .ihui-agent/tmp/<任务名>/`(已 gitignore,自动忽略)。
6. **commit 阶段禁 add**:`git add <本任务文件>`(§12 多会话保护),**禁止** `git add .` / `git add -A` 一次性把所有 verify-*.mjs 加进去。

### 红线(违反视为协作事故)

- ❌ **禁止** `apps/web/verify-*.mjs` 等源码根目录临时文件 commit(会被守门脚本警告 + 污染 main 分支)。
- ❌ **禁止** 用 "这是为了验证 XXX 功能" 借口把临时文件 commit 进来。
- ❌ **禁止** 留 `verify-v1.mjs` / `verify-v2.mjs` / `verify-v3.mjs` 等版本号后缀文件(版本号在 git history 里有)。

### 豁免场景(允许放源码目录)

- 守门脚本:`scripts/check-*.mjs` / `scripts/verify-*.mjs`(正式工具,有 README/CLI/help)。
- 测试文件:`*.test.ts` / `*.spec.ts` / `tests/` / `__tests__/`(符合 §23 测试目录规则)。
- 长期保留的 E2E 脚本:`scripts/e2e-*.mjs`(已纳入 CI,有意保留)。

### 守门脚本:`scripts/check-verify-tmp-files.mjs`

- 扫描 `apps/*/verify-*.mjs` / `apps/*/verify-*.ts` / `scripts/verify-*.mjs`(白名单外)
- 发现任意 1 个 → 警告(不阻断,只提示)
- 集成位置:CI / guardian-runner 后续项(暂 warn-only,后续按需升级 blocking)
- 退出码:0(有警告但通过)+ 输出警告列表

### 与其他规则协同

- 与 §12(多会话并行)协同:`git add` 阶段只加本任务文件,不批量加 verify-*.mjs。
- 与 §13(文件修改持久化)协同:Read 验证 verify-*.mjs 的修改生效。
- 与 §15(工作区卫生)协同:临时文件必须项目内路径(`.ihui-agent/tmp/`),不写 `G:\` 根目录或 `C:\temp\`。
- 与 §23(测试目录)协同:`verify-*.mjs` 命名 ≠ 测试文件,不能伪装成 `*.test.mjs` 绕过守门。

---

## 守门脚本速查(pre-commit 项,按类别)
- **对象空间落地与活文档编辑的常驻出口 = `scripts/object-space-land.mjs` / `scripts/live-doc-edit.mjs` / `scripts/gate-registry-insert.mjs`(+ 共用 plumbing `scripts/lib/bypass-git.mjs`),不是守门**(2026-09-26 立;镜像测试 `node --test scripts/tests/{object-space-land,live-doc-edit,gate-registry-insert,bypass-git}.test.mjs` = 36 例)。为什么这条要进 AGENTS:本仓在共享工作区里做旁路提交是**常态**(活文档与注册表的工作树副本常年滞后 HEAD,按 pathspec 交工作树就是把别人已入库的行整批写回旧态),而 2026-09-26 那一轮同一套 plumbing(临时 `GIT_INDEX_FILE` + `hash-object` + `update-index` + `write-tree` + `commit-tree` + CAS `update-ref`)在同一会话里被手写了 **6 份**、全躺在 `.ihui-agent/tmp/` 不受版本控制,并且已经互相漂开:索引可动性判据两种写法、零损失判据三种写法 —— 其中一份拿"重复行计数"当零损失判据,被 `guardian-runner.mjs` 里天然存在的 `  },` 与空串行顶出"凭空多出 1322 条"而**一次也没落地成功**,换成结构等值判据才通过。不入库的代价是确定的:下次会话要么再手写一遍(带着同一个 bug 的新版本),要么图省事走 pathspec。**三条判据照抄别重新发明**:① CAS 循环内**每次重读 HEAD** 且"任一目标路径被别人改过即停"(不是抢锁,是拒绝覆盖);② 零损失判据 = **结构等值**(`new == 前缀 ⊕ 本块 ⊕ 后缀`),不是重复行计数;③ 落地后 `git show --name-only` 回读声明路径,并把**共享主索引**逐路径对齐到新 blob —— **只在** 索引 blob 等于父提交 blob 或索引里没有这条时动,否则点名"归属他人"不动(§12d 第三层那型:一次不带 pathspec 的普通提交就会把旁路交付写回旧版)。取号类判据(`gate-registry-insert`)必须同认 `id: '134'` 与 `id: "134"` 两种形态 —— 双引号 id 对一切按单引号解析注册表的判据(含守门 89 的撞号 R5)隐身,这是本轮真实踩过并用夹具钉死的坑。

- **批量写计数诚实性对账**(guardian id 以 `scripts/guardian-runner.mjs` 现值为准 —— 本枚落地时取到 134,并发会话可能挪号;`blocking`,`stagedTriggers=apps/api/src/routes/` + `apps/api/src/db/`,紧急跳过 `HUSKY_SKIP_BATCH_WRITE_COUNT_HONESTY=1`):`scripts/check-batch-write-count-honesty.mjs` —— 拦的是本仓 2026-09-26 一整批同型静默失真的**形状**:`db.delete/update ... where inArray(ids)` 之后把**请求侧数组的 `.length`** 当 `affected` / `deleted` / `restored` 回给前端。那一型的问题是"改了 0 行"与"改成功"在响应上完全同形 —— `where` 带 `eq(roleId)` / `eq(memberId)` 过滤时,传进来的 id 一个都不命中也照样回 `deleted: 3`,后台显示"已删除 3 项"而库里一行没动。当轮人肉清出 8 处(`chat.ts` batch、`admin-sys/role-routes.ts` cancelAll+selectAll、`admin-demand-square.ts`、`admin/_shared.ts` 的 `registerCrud` 批量删〔**19 处 admin 路由复用同一个工厂**〕、`message.ts` 批量删、`workspace.ts` batch-delete/batch-restore),统一出口是 `apps/api/src/utils/batch-outcome.ts`(`dedupeIds` + `batchWriteOutcome(requested, confirmed)` ⇒ `{requestedIds, affected, missedIds}`),db 层一律 `.returning({id})` 回报**库确认集合**,响应新增 `missedIds` 逐条点名(既有键名与 status code 逐字不变)。**四条放过通道,每条都有正反用例**:① 同一写链带 `.returning(`;② 计数根标识符 ≤2 跳可追到归属预查询(`business-card-routes.ts` 那种"先查 owned 再删"的**正确写法**不得判红);③ 该文件 import 了唯一出口;④ 行内 `batch-count-exempt: <原因>`(须带原因,裸标记不计)。**刻意不判的两型**:布尔 `deleted: true`(全仓惯例,HEAD 面实测 257 处、10+ 条用例当契约钉,要改属全 API 语义决策、交人定,不是这道门能顺手做的)与读查询 `count: rows.length`(不是写)。**判不了的形状如实登记**:写链在 db 层而计数在 handler(`workspace.ts` 那一型收口前看不见)、`send` 在嵌套箭头里、`deleted: (await …).length` 非点号链形态 —— 这些落 `未判定` 并报数,**`--strict` 下有未判定即 exit 2**(拒绝出合格证)。**口径同 70/77/83/98/103/118**:全量判 HEAD blob、`--staged` 判索引 blob、`--worktree` 仅人工、两面旗同给 exit 2、取不到判"无法判定"、枚举到 0 个候选判死;棘轮锚点 = 该文件 HEAD 自身违规数,所以存量未清时判红不会变成人人跳门。**两处遮噪方向不同**(本门自己踩过):判写链要"剥注释 + 保留字符串"(标识符落在字符串里的形态要看得见),判 `import` 说明符则连字符串一起抹会直接失明。取证 `--self-test`(35 条 = 构造面 27 + 临时 git 仓端到面 8)+ 镜像 10 例(含"禁 `process.cwd()` 定根 / 禁自派生 git 读内容 / 默认档必须判 HEAD"三条反向锁)。立门前阳性对照用**修复前的 HEAD blob** 喂同一判据:`_shared.ts ⇒ 342:deleted:idList.length`、`message.ts ⇒ 348:deleted:body.ids.length`,证明判据真能命中本仓实际产出的形态。

- **i18n**(2/2b/2c/2d/2e/2f/2g-web/2f-mobile-rn/2f-cli):check-i18n-keys(parity+白名单)/ scan-i18n-zh-residue(zh-TW/ko 阻塞,ja warn)/ check-i18n-broken-en(阻塞)/ i18n-diff(翻译流水线,2f-web + 2f-miniapp-taro 阻塞)/ check-i18n-namespace-passing(命名空间传递,warn)/ check-cli-i18n-parity(cli 端 parity,warn,2f-cli)
- **代码质量**(1/3/4/4b/4c/5/6/7/8/9/10):API key 泄露 / schema drift / 陈旧 dist / UTF-8 完整性 / lint-staged / sanitizer / dedupe / 路由一致性 / safeParse(warn)/ OpenAPI(info)
- **UI/样式**(11/11b/17/18/20/24a/24b/27/28/36/102):圆角 / 圆角溢出(父 rounded + 子 bg 贴边,warn,2026-08-06 立)/ CSS token / title tooltip / Tailwind 冲突 / 侧边栏宽度+端口注册表(warn)/ z-index 层叠+遮罩 z-index(阻塞,id 27/28,2026-09-22 加固:补 skipEnv + onFailHint,并在 id 27 新增第 5 项两组契约——桌面端自绘窗口控制三按钮挂 z-max(10003) 不能降(须高于 resize 抓手 z-loading=10000),`z-modal` 遮罩永远盖不到它,故等效压暗层 `data-window-controls`+`data-window-controls-dim` 与失焦非活动态 `data-window-inactive` 两组标记缺一不可,**两态弱化/瞬时规则必须写在 `apps/web/app/globals.css`**(入口 CSS 变更必然重编译;实测生产构建里组件内 Tailwind 任意变体未进 CSS 产物);判据有效性自查 `--self-test`;紧急跳过 HUSKY_SKIP_Z_INDEX_GUARD / HUSKY_SKIP_OVERLAY_ZINDEX)/ miniapp-taro design-tokens 同步(阻塞,防 app.css 漂移)
- **工程约束**(12/13b/13c/15/19/21/22/23):交付报告 / PLAN 体积(warn)+防误删 / 迁移完整性 / staged 污染(warn)/ 多端同步(warn)/ README 同步(warn)/ staged 清单(info)
- **Push/工作区**(25/26/29):项目外路径(阻塞)/ 父目录污染(阻塞)/ Push 同步(阻塞)
- **防提交丢失**(30a):reflog reset 检测 + fsck 悬空 commit 检测 + lost-commit/* tag 备份清单(AGENTS.md §22 配套,blocking);**仅远端 tag 缺失走自愈 fetch + packed-refs 固化,拉不动才降为警告**(2026-09-24)
- **对话流元素覆盖**(57):锚点 / SSE 契约双端 / 清单条目三条判据(blocking);**判仓库内容而非共享工作区快照** —— 已暂存取索引 blob、仅工作树脏取 HEAD blob,别人未提交的重写不得钉红无关提交(2026-09-24,与守门 77 同取向)
- **凭据外泄对账**(67):非 2xx 响应**不经** `response-sanitizer`(其 496 行 `statusCode<200||>=300` 直接 return payload),故把上游响应体拼进 error message 是脱敏旁路。判据 = **A∧B ＋ C/D/E/F 至少一条**:**A** 错误构造上下文含 4xx/5xx 响应 **与 `throw new Error(...)`/`XxxError(...)`** 两类(service 层走后者);**B** 外泄形态 = `JSON.stringify(X)` **或整对象插值** `${x}` / `${x.slice(…)}`(字段投影 `${json.error}` **不算**,那是推荐写法);**C** 凭据语义(变量名/声明右侧/对象 key/message 字面量命中 `token|secret|credential|password|api_?key|bearer|access_token|refresh_token|id_token`);**D/E 来源证据**:不认变量名,沿 `X=(await R.json()|R.text())` → `R=await fetch('<令牌端点>')` 回溯,以路由注册行为处理器边界、窗口 ≤40 行,越界放弃(宁漏不误报);**F 关键词兜底**:被调方是 **SDK 而非 fetch** 时 URL 不在文件里(D/E 结构上够不到),改以「消息自带凭据端点关键词(`AssumeRole`/OAuth/`device/code`/`gettoken`/`tenant_access_token` …)∧ 整对象被倒进 message(目标路径末段为 `body`/`data`/`payload` …)」双条件命中,单条件一律不判。其余非令牌端点的错误体透传只进低置信候选不计失败(现 30 处,含经 `callVendor`/`cozeRequest`/`callLuyala` 转发的动态 URL,调用点 path 均为推理接口;`throw` 形态新增的 4 处已逐个读明:1 处即下述 STS 真缺陷,3 处不含凭据——明细见 README 守门 67 节)。成因:同日实测出**五处**同族真缺陷——IMS 令牌体进 502(`tokenData`,C 抓)、GitHub `/login/device/code` 体进 400(变量名 `json`,`device_code` 按 RFC 8628 §1.5 是 bearer 凭据,C 全盲靠 D)、PayPal `/v1/oauth2/token` 原始文本进 `throw`(**既非 stringify 也非 status(),靠 A 扩展 + E**)、阿里云 STS `AssumeRole` 的 `response.body` 进 `throw`(走 SDK 无 URL 字面量 → 靠 F)、腾讯云 `oss-sts-service.ts` 的兜底 `errMsg = … ?? JSON.stringify(result)`(成功体含 TmpSecretKey / Token,且是"上行取体下行拼"的两行式 → 靠 F 的变量链配对)。**教训:每一代判据只会漏掉自己形态之外的那一种**,四通道是叠加不是替换;名字类判据必然漏掉名字最无辜的那一处。修这类缺陷**严禁**用加 `skipResponseSanitization` 消红——那是关掉脱敏保护。覆盖 **JS/TS 与 Python 两套语法**(`.py` 亦在 `apps/` 扫描内):`json.dumps`、f-string `{x}` / `{x[:200]}`、`raise XError(...)`、`status_code=4xx`、`#` 注释豁免都是独立的语法锚点,缺一条该语言整条空转;F 通道的关键词与 dump 只认两种配对:**同行**,或**关键词行插值的变量其声明右侧就是那记 dump(两行式)**;整窗任意配对会产出假阳性(实测 cnblogs/oschina/segmentfault 三处),已各留正反用例钉住。`--staged` 只收窄文件清单、内容一律读工作树(与 lint-staged 同形态),index 与工作树错配由 30c/65 负责。紧急跳过 `HUSKY_SKIP_CREDENTIAL_LEAK_IN_MESSAGE=1`,自检 `node scripts/check-credential-leak-in-message.mjs --self-test` + `node --test scripts/tests/check-credential-leak-in-message.test.mjs`
- **适配层接线**(64):miniapp-taro `adapters/*.taro.tsx` 未被 adapters **目录外**源文件从 adapters 路径 import 即拦截(blocking,2026-09-22 立)。补 `check-adapter-style-parity.mjs` 只守硬编码颜色、不守"是否被 import"的缺口——9 个屏级适配器 3078 行"造好没装车"直到删除始终无闸可挡,即本条成因。存量基线已清零(`scripts/adapter-wiring-baseline.json` = `unwiredAdapters: []`),任何新增未接线适配器一律直接拦截;判据必须限定 specifier,否则端内同名自有组件(`components/NavBar.tsx` 等)会造成假阳性放过死代码。紧急跳过 `HUSKY_SKIP_ADAPTER_WIRING=1`,自检 `node --test scripts/tests/check-adapter-wiring.test.mjs`
- **Python 类型**(35):mypy 检查(阻塞,防 ai-service Python 类型回退)
- **依赖治理**(38):solito 幽灵依赖回归守门(阻塞,防 P0 优化被回退)
- **迁移完整性**(39):mobile-rn screen 迁移守门(阻塞,防独立实现回升,白名单:Debug/DevEnter/SharedDemo/profileMenuData)
- **共享层重复**(40):端内重新实现 shared hook/util 检测(阻塞,防端内独立实现回升,白名单:web/useChat + web/useAuth + web/useAgentRuntime + web/useClipboard + web/useNotificationStore + mobile-rn/useAuth + mobile-rn/useNotificationStore(后二者均为 re-export wrapper + 平台 adapter,属 §3 允许形态;守门的 re-export 判定只看 export 那一行有无 `from`,函数型 wrapper 必须显式登记))
- **条件**(16/16b):staged-typecheck(任意 staged .ts/.tsx → 全量 include + 错误过滤,2026-08-18 根治);packages/database/src staged → build(脚本:check-staged-typecheck.mjs,详见 §22b)
- (16c):check-staged-typecheck-mirror-sync(源/测镜像漂移防御,blocking,AGENTS.md §22b 配套,2026-08-18 立)
- **React 事件闭包**(42):check-event-closure-leak(异步回调闭包访问 SyntheticEvent 属性检测,blocking,AGENTS.md §42 配套,2026-08-12 立)
- **桌面弹窗防护**(52):check-no-visible-spawn(派生控制台程序漏 `windowsHide` 检测,blocking,AGENTS.md §5b 机器级根治配套,2026-09-20 立;`--self-test` 14 例 + §22c 镜像测试)
- **工具名本地化**(55/56):check-tool-name-display-coverage(注册表 `_TOOLS` × 词表 × 五语言 taskStatus 三方比对,blocking,拦"新增工具不补功能名");check-tool-display-resolvable(91 个功能名在 shared + 5 端合并视图 + 小程序离线包逐语言解析,拦两类静默失败:端内取词缺键回显 `toolReadFile`、忘跑 `pnpm gen:i18n` 致离线包过期;`node --test scripts/tests/check-tool-display-resolvable.test.mjs` 自检)。紧急跳过 `HUSKY_SKIP_TOOL_NAME_COVERAGE=1` / `HUSKY_SKIP_TOOL_DISPLAY_RESOLVABLE=1`
- **快捷键声明与归属对账**(69):check-declared-shortcuts.mjs(blocking,2026-09-22 立并注册)——两类红点:① "声明未绑"(UI 标了 `Ctrl+X` 而全仓无处理器,含注册表"有键无消费者");② "同键被他功能接走"(`field` 类声明与全局注册表同键,但声明方拿不出持有证据;成因是 `view-switcher` 曾把 document/browser/figma/code-changes/agent 五项标成 `Ctrl+1-5`,而该族键位实际切 AI 对话模式)。合法证据三选一:本文件出现该条目 `event` 引号字面量 / 本文件自有同键 handler + `stopPropagation`(独占截断,`RichTextEditor` 的修法)/ 条目自身与注册表同义镜像(命令面板原样列出全局键位)。**刻意只认 `field`**:`<kbd>`/正文常合法描述**别的表面**的键位,纳进必假红。全量审计 `node scripts/check-declared-shortcuts.mjs`,自检 `node --test scripts/tests/check-declared-shortcuts.test.mjs`,紧急跳过 `HUSKY_SKIP_DECLARED_SHORTCUTS=1`
- **硬编码中文基线棘轮**(70):scan-hardcoded-zh.mjs(blocking,2026-09-22 接线)—— 扫 `apps/web/{app,src/components,src/hooks}` + `packages/{ui-react,shared}/src` + 2026-09-24 补的三端(`apps/mobile-rn/src`、`apps/extension/{entrypoints,src,lib}`、`apps/cli/src`),排除注释/metadata/useTranslations 行/admin/测试。**每文件命中数与基线额度比较**(`scripts/hardcoded-zh-baseline.json`):只拦"比基线更多",存量债(886 文件 / 13090 行)冻结、新增即拦;清理后 `--update-baseline` 下调额度(必须人工确认后再跑,禁止为过门而调高)。该脚本 2026-07-20 即存在但从未接入守门链,是"造好没装车"的第二个实例(第一个是同一批的 69)。`--staged` 由 runner 自动下发(runner 传 `--exit 1`,不带该参数时**永远 exit 0** —— 集成测试踩过),暂存集为空时回退全量防"空暂存恒绿"。**两处同日修**:① 14 例镜像测试自 §15「ROOT 由脚本自身位置推导」起 **13/14 恒红** —— 测试靠 `cwd` 定位夹具而脚本按定义忽略 cwd,14 例全在扫真仓(不是判据错,是调用方式失效);加 `--root <dir>` 显式测试通道(生产不带,语义不变)后 15/15 绿。② 命中判定漏剥**行尾 `//`** ⇒ PriceChart / TerminalTab / TerminalStatusIndicators 各多出 2/1/1 处假阳,把它们顶过基线额度、把本门钉成"谁碰这三个文件谁被拦";同时修 `'https://x/*'` 这类串内 `/*` 骗进块注释状态机的假绿。**遗留(已量化,未修)**:全量模式按共享工作树判,而共享工作树对约 2064 个路径落后 HEAD —— 同一份 HEAD 代码,临时 worktree 干净检出报 **1** 个文件越线(`packages/shared/src/chat/handoff-package.ts` 49 处 / 基线 0),本机工作树报 **281** 个。要么让全量模式改判 HEAD blob(守门 77 已验证的口径),要么把 `heal-worktree-tracked` 的刷新面扩大;前者是判据口径、后者是环境自愈,都不该靠"反正本地红着习惯就好"糊过去。紧急跳过 `HUSKY_SKIP_HARDCODED_ZH_GUARD=1`
- **计划登记行防丢**(71):check-plan-line-loss.mjs(blocking,2026-09-22 立,`stagedTriggers=PROJECT_PLAN.md`)—— 共享工作区里并发会话按"内存中那份旧计划文档"整文件提交,会把别人**已入库**的登记行按旧基线回写掉(2026-09-22 一小时内两次发生,本会话 10 条 G-152/G-166/D107b 进度行被抹)。既有 13c `check-project-plan-archive.mjs` 只认 `### XXX(已完成 ✅)` 任务标题行,条目内 bullet 登记行不在其视野,故补此闸。**登记编号族含 `G-x`/`Dx`/`Px`/`Wx` 与 `守门 NN`**(2026-09-23 补最后一族:各道闸门在计划里的登记行用的正是 `**守门 NN …**` 前缀 —— 实测一枚并发暂存版本整块删掉别人的守门 72 登记(40 行),只认 G/D/P/W 完全看不见);自愈结论一律写 `.workbuddy/plan-heal.log`(旧版丢 `/dev/null` 配 `|| true`,再叠加未 `.trim()` 的 sha,自愈静默失效一整天)。判据按**编号标记的原文前缀**在待提交内容里全文搜:整行消失 → 拦;只改写文案、保留编号 → 不报(不误伤正常编辑);原文能在 `.ihui-agent/archive/PROJECT_PLAN_*.md` 找到(§1 归档)→ 放行。写闸过程中真修掉一个自造假阳:标记若按"编号 + 后续文本"重拼,`D107b` 会被拆成源文本里不存在的 `D107 b`,使正常提交被误判丢失。自检 `node scripts/check-plan-line-loss.mjs --self-test`(9 例正反成对,含 missingFrom 双目标比对);**自愈面**:`.husky/post-commit` 第 6 段每次提交后扫最近历史自动回捞(并发会话 routinely 用 `--no-verify` 绕过 pre-commit,故这一层必须有),手动 `--heal` 只写工作区、`--heal --commit` 顺带前向提交(基线一律取 HEAD,不代收别人未提交的内容)。**2026-09-23 两处加固(都是"本地全绿也发现不了"那一类)**:① 自愈比对改成**工作区与 HEAD 分别判缺失** —— commit-tree 旁路(`git-sync-converge` 索引层合并、临时索引提交)**不跑钩子**,它把已入库行从 HEAD 合掉时共享工作区常还留着那行,单目标会"无缺失"提前返回 → HEAD 永久缺行(本次 G-154 登记行即此因),现由 converge 落合并提交后就地补跑一次自愈;② `rev-parse`/`hash-object` 未 `.trim()`,尾部换行使 `read-tree` 报 `Not a valid object name`,**自愈提交自上线起从未成功过**,而 post-commit 写作 `|| true` 所以毫无声响 —— 判据只能在**独立仓库**做端到端取证才暴露(临时 repo 造一次真旁路合行 → A/B:旧版判"无缺失",新版识别并建前向恢复提交)。跳过 `HUSKY_SKIP_PLAN_HEAL=1`;紧急跳过本闸 `HUSKY_SKIP_PLAN_LINE_LOSS=1`(会把别人的登记行写没,慎用)
- **Dockerfile 构建上下文对账**(72):check-dockerfile-copy-paths.mjs(blocking,2026-09-22 立)—— 堵的是"本地全绿也发现不了"的一类:提交 `79b906463f` 给根 package.json 加 `postinstall: node scripts/fix-expo-metro-junction.mjs`,而 `deploy/docker/Dockerfile.{api,web,cli,migrate}` 只 COPY 清单就跑 `pnpm install` ⇒ 镜像缺该脚本,CI 上 build-api/build-web 同时 `MODULE_NOT_FOUND`(本机无 docker,typecheck/lint/单测全都不会知道)。两条判据只用仓库内信息:**A** 凡 COPY `pnpm-workspace.yaml`(根上下文标记)的 Dockerfile,必须 COPY 钩子里 `node <file>` 引用的每个脚本(只 COPY 单文件,勿 COPY 整个 `scripts/` 以保层缓存);**B** 每条非通配、非 `--from=` 的 COPY 源必须存在于**构建上下文**,上下文由 `.github/workflows` 的 `context:`/`file:` 成对解析,解析不到即跳过并在结论行如实报 `B 核了 N/M`;**C** `pnpm --filter <spec> run <script>` 对闭包里**没有该脚本**的包是**静默跳过而非报错**,若被跳过的包自带 `build`(产出 dist)则下游按 `main`/`exports` 读 dist 必 `Module not found` —— 判据 = 从 workspace 包图展开闭包(`pkg`=自身、`pkg^...`=仅依赖、`pkg...`=自身+依赖,取反与上游方向放过),点名"有 build 却缺被调用脚本"的包。**A/B 修完 build-api 绿了,build-web 仍恒红就是 C 类**(Dockerfile.web 跑 `--filter @ihui/web... run build:static`,而 web 的 7 个可构建依赖全都没有 `build:static` ⇒ 一个都没构建 ⇒ `@ihui/api-client` 解析失败;同仓 Dockerfile.api 用 `run build` 人人都有故一直绿,两条只差脚本名)。**存在性一律按提交内容(`git ls-tree -r HEAD`)判,不按工作树**——并行会话可能删了文件但未暂存,按 `existsSync` 会产出与真实构建相反的假阳性(实测踩过)。`COPY . .` 与 `pnpm-lock.yaml*` 不参与判定。自检 `--self-test`(10 例,含 C 的 5 例正反对照)+ `node --test scripts/tests/check-dockerfile-copy-paths.test.mjs`(10 例,含"真仓包图上旧行必红且点名 api-client、修后必绿、api 作同仓对照"与 `expandFilterSpec` 三形态);紧急跳过 `HUSKY_SKIP_DOCKERFILE_COPY_GUARD=1`
- **mobile-rn 深色前景/容器对账**(83,原 75):check-brand-foreground.mjs(blocking,2026-09-23 立)—— 四条判据:**R1** 零豁免,同一 style 块内 `tokens.brand.DEFAULT` 作背景 × `surface.light`/`text.primary` 作前景(深色档案下 brand.DEFAULT=纯白 ⇒ 白底白字);**R4** 同一关系被拆到**兄弟 key**(`retryBtn` × `retryText`)时按命名配对判,走基线棘轮;**R2** `surface.light` 背景 / α≥0.5 白 rgba / 无 `dark:` 变体的 `bg-white` 作容器底,每文件计数对 `scripts/brand-foreground-baseline.json` 只减不增;**R3** 数 brand.DEFAULT 填充/描边但**不计 §4 成对 CTA**(同块或兄弟键用 `brand.foreground` 作前景)—— 见本节上方「品牌 CTA / 主按钮色同源」,按规矩写就红的门只会逼人 `--no-verify`;**无配对的白卡片照旧计**(自检里两条阳性对照钉住)。**内容口径**:全量与 `--update-baseline` 判 **HEAD blob**,`--staged` 判暂存 —— 共享工作树滞后 HEAD,按磁盘算会在恒红/假绿间来回跳并把错数写回基线(2026-09-24 一天内 R3 登记被整文件回退三次的直接成因)。`--staged` / `--update-baseline` / `--self-test`;紧急跳过 `HUSKY_SKIP_BRAND_FOREGROUND=1`。

- **mobile-rn 深色前景/容器对账**(83,原 75):check-brand-foreground.mjs(blocking,2026-09-23 立)—— 四条判据:**R1** 零豁免,同一 style 块内 `tokens.brand.DEFAULT` 作背景 × `surface.light`/`text.primary` 作前景(深色档案下 brand.DEFAULT=纯白 ⇒ 白底白字);**R4** 同一关系被拆到**兄弟 key**(`retryBtn` × `retryText`)时按命名配对判,走基线棘轮;**R2** `surface.light` 背景 / α≥0.5 白 rgba / 无 `dark:` 变体的 `bg-white` 作容器底,每文件计数对 `scripts/brand-foreground-baseline.json` 只减不增;**R3** 数 brand.DEFAULT 填充/描边但**不计 §4 成对 CTA**(同块或兄弟键用 `brand.foreground` 作前景)—— 见本节上方「品牌 CTA / 主按钮色同源」,按规矩写就红的门只会逼人 `--no-verify`;**无配对的白卡片照旧计**(自检里两条阳性对照钉住)。**内容口径**:全量与 `--update-baseline` 判 **HEAD blob**,`--staged` 判暂存 —— 共享工作树滞后 HEAD,按磁盘算会在恒红/假绿间来回跳并把错数写回基线(2026-09-24 一天内 R3 登记被整文件回退三次的直接成因)。`--staged` / `--update-baseline` / `--self-test`;紧急跳过 `HUSKY_SKIP_BRAND_FOREGROUND=1`。
- **mobile-rn 深色前景/容器对账**(83,原 75):check-brand-foreground.mjs(blocking,2026-09-23 立)—— R1 零豁免:同一 style 块内 `tokens.brand.DEFAULT` 作背景 × `surface.light`/`text.primary` 作前景(深色档案下 brand.DEFAULT=纯白 ⇒ 白底白字);R2 基线棘轮:`surface.light` 背景 / α≥0.5 白 rgba / 无 `dark:` 变体的 `bg-white` 作容器底,每文件计数对 `scripts/brand-foreground-baseline.json`(现 13 文件 24 处,均为媒体上的合法浮层或已带 `dark:` 变体)只减不增。`--staged` / `--update-baseline` / `--self-test`(11 例);紧急跳过 `HUSKY_SKIP_BRAND_FOREGROUND=1`。
- **反回退对账**(84,原 76):check-stale-revert.mjs(blocking,2026-09-23 立)—— 堵共享工作区**静默回滚**:§12d converge 用 merge-tree/commit-tree 只推进 HEAD+index、**不 checkout**,工作区落后 HEAD 时 `git add <file>` 交的是旧基线(实测 503 文件落后 486 提交),等于把别人该路径的后续改动静默回滚,而 diff 只显"改了几行"。判据 R1 = 暂存 blob != HEAD blob **且字节级等于该路径某祖先提交版本** → 拦并点名回到的 commit;真新编辑不可能恰好等于历史 blob,故误报极低。三条护栏:merge/cherry-pick/revert 上下文整轮豁免、暂存删除只 warn(`git rm` 合法,机器分不清就被删)、判定文件 >300 跳过(性能护栏,防逼人 --no-verify 连带关掉全部守门)。取证 `--self-test` 8 例(含"写回 v1 必判红"阳性对照)+ 临时 index 端到端演练 3/3;**有意回退一律改用 `git revert` 生成前向提交**。紧急跳过 `HUSKY_SKIP_STALE_REVERT_GUARD=1`。

<!-- 合并归并说明:本行下方两条登记分属两个会话同日新增的闸门(78 workspace 依赖链接对账 / 79 提交内容含冲突标记),两侧均保留,不构成互斥。
     (2026-9-24 校正:原写法把冲突标记门写成 77,而 77 是「全 8 端圆角单一源头对账」——同日撞号正是本节要防的事,登记新门前请按守门速查逐条核对 id。) -->

- **workspace 依赖链接对账**(78):check-workspace-dep-links.mjs(blocking,2026-09-23 立)—— 堵的是同一族"**本地全绿、出事的是别人**"的故障,现共**五维**,每维的修复动作不同,红点会点名是哪一维:**① 声明↔链接**:`package.json` 声明 `workspace:*` 而 `node_modules` 里没有那条链接(§12e 的 `pnpm install --filter` 后遗症)。2026-09-23 实例:`@ihui/extension` 缺 `@ihui/design-tokens` → rollup `failed to resolve import` → `pnpm -r build` 连 4 次全红 → 部署进入 30 分钟冷却循环、线上停在旧提交,而 **typecheck/lint/单测全都不会红**(TS 走 tsconfig paths,不看 node_modules)。判据 = 每个包 dependencies/devDependencies/peerDependencies 里所有 `workspace:` 声明,必须在 `<pkg>/node_modules/<dep>` 或根 `node_modules/<dep>` 可解析(`existsSync` 跟随符号链接 ⇒ **悬空链接同样判红**)。**② 链接↔内容**(2026-09-24 补,`findGuttedLinks`):每条**符号链接**的目标必须是真包(有可 parse 且带 `name` 的 `package.json`)——`existsSync` 对"指向**空目录**的链接"仍返回 true,而 09-24 本机全机门禁停摆正是这一型:`node_modules/typescript`、`node_modules/eslint` 指向 `.pnpm` 里的空目录,`.bin` 只剩 16 项 ⇒ lint-staged 第一步 `✖ eslint --fix` 阻止提交 ⇒ **每一次提交都被迫 `--no-verify`,约 110 道守门对全队同时失效**,而 `git status`/typecheck/其余报告全都看不出来。刻意**不**比 name(pnpm 别名 `foo@npm:bar` 必产假阳);真目录(file:/workspace: 直连)不参与;`--strict` 档另把本维延伸到 `node_modules/.pnpm/*/node_modules/*` **传递闭包**(实测真仓浅扫 717 条 / 深扫 10,015 条,均 0 红,深扫 +1.3s);**深扫刻意不进提交链** —— 并发 `pnpm install` 半复制态会一次闪出上百条,而本门 blocking,恒红门的唯一结局就是各会话跳门、连带全部守门作废(本仓最高反面教训);`.pnpm` 里含 `_tmp_` 的安装中临时键**跳过并如实计数**,不判红。**③ shim 完整性(仅报数)**:直接声明的依赖有 `bin` 而该处 `.bin` 无同名 shim;不判红是因为它**在并发 install 窗口内会闪**(实测 0↔113 跳),判红落在 `pnpm check:dep-links:strict`(已串进 `check:all`)。稳态真仓实测 **0 条**缺失、25 个包各有 `.bin`(apps/api 45 项 / web 33 / extension 24 / shared 15 …)⇒ 旧条目写的"真仓 102 条含 apps/api 15 条""apps/api 根本没有自身 `.bin`"**是那次削损的现场读数,不是基线,已于 2026-09-24 全量安装后复测证伪并就地更正**;同时 pnpm 只为**直接声明的依赖**建 shim(根里 hoist 上来的 `expo`/`react-native` 不在根 `package.json` 内,按定义没有),`declaredFor(owner)` 就是为剔除这类假阳而存在。**④ 钩子命令可解析性(判红,不需基线假设)**:`lint-staged` 配置里必然被 spawn 的命令(现 `eslint`/`prettier`)必须在**根** `node_modules/.bin` 解析得到。两处刻意的窄口径:**不**接受"`node_modules/<cmd>` 包目录存在"作为通过(故障当天恰是"包体完好、shim 没了":`node node_modules/eslint/bin/eslint.js --version` 出 v10.8.1,而按 PATH 找 `eslint` 报「不是内部或外部命令」);**不**查全局 PATH(本会话 `where eslint` 能命中全局版,而钩子进程的 PATH 里没有 ⇒ 拿全局命中当"能解析"就是造一台**在故障现场报绿的尺子**)。**⑤ shim 指向的入口文件(判红)**:shim 在位而它 exec 的那个 `bin/*.js` 被删 ⇒ ④ 全绿、lint-staged 一 spawn 就 `Cannot find module`。`resolveShimEntry` 双路兜底(先剥 shim 文本的 `%~dp0`/`$basedir` 模板,再退回包清单的 `bin` 字段),两路都探不到一律记 `unresolved` **不判红**;两个写门时自己踩到的坑已由反向对照钉死 —— pnpm 的 `.CMD` **第一个候选是 `"%~dp0\node.exe"`**(存在性分支头),按"第一个命中"取值会让本门在**完好仓库**上恒红;`%~dp0` 本身就是 `.bin`,再拼一层 `..` 会多跳一级把目标算到 `node_modules` 外面。两条反假绿护栏:扫不到任何 workspace 包 → `exit 1`(不报绿);根 `node_modules` 不存在 → 显式提示"先 pnpm install"并**无法判定**(不记为通过);**扫到 0 条链接一律判红**(空扫不报绿,浅/深合计口径,默认档与改前逐字等值)。`--staged` **不**随暂存收窄范围,恒全量判定(全量 25 包约 0.2s)—— 这类破损与"本次改了什么"无关:手动删链接、他机跑过 `--filter`、清理工具动过依赖树,按 staged 收范围恰好放过整类。取证:`--self-test` + `node --test scripts/tests/check-workspace-dep-links.test.mjs`(例数以各自末行现测为准,不写死防漂移;含**装车证明** runner 里确有 id 78 + blocking + skipEnv、"真仓 eslint/prettier 第五维必须判绿"的**反恒红**对照、以及"深扫只在 strict 生效"的接线方向对照)。**修复动作按维度分**:① ② 靠全量 `pnpm install`(不带 `--filter`);③ ④ ⑤ 靠 `node scripts/repair-node-bin-links.mjs` —— 因为 **shim 不会被 `pnpm install` 或 `pnpm install --force` 重建**(两条都只回 `Already up to date`,1.2s 收工)。验收一律实测命令本身(`.bin/eslint --version` + `.bin/tsc --version` 都出版本号、`pnpm --filter @ihui/api typecheck` 真跑、门 78 全量 exit 0),只看 `node_modules/eslint` 在不在等于没测;`repair-node-bin-links.mjs --check` 在修复后的现读是「根依赖 16 个包 / bin 声明 9 个:`.bin` 完整,无缺失」。另留一条**归属纪律**:本机第一次补回 shim 的 mtime(11:22:56)与另一会话 `repair-node-bin-links.mjs` 入库(11:35)、与我自跑 `pnpm install --force` 都在同窗口,**归属未定 —— 只记事实,不记成果**。紧急跳过 `HUSKY_SKIP_WORKSPACE_DEP_LINKS=1`。

- **提交内容含冲突标记**(79):check-no-conflict-markers.mjs(blocking,2026-09-23 立)—— 2026-09-23 15:49 `.git` 被宿主清除,恢复期某会话在共享工作区跑真实 `git merge`,留下 103 个未合并路径与 94 个带字面标记的工作区文件;而全链守门**没有任何一道**看"被提交的内容含冲突标记",于是 `apps/cli/tests/file-edit.test.ts` 带着 `<<<<<<<` 一路进 HEAD 树,整轮 merge 结束都没被发现。判据 = 同文件内**成对**出现的行首 `<<<<<<< ` 与 `>>>>>>> `(中间允许夹整行 `=======`);**强制成对**是硬要求 —— 单行 `=======` 在 Markdown setext 标题下划线、表格分隔、ASCII 示意图里都合法,只判单行会满天假红(实测全仓 `^=======$` 命中远多于成对命中);未配对的孤立标记不计红但如实报数("只剩一半"= 标记被手删的强信号)。三模式:`--staged`(只判索引内容,`git show :<path>`,取不到退回工作区;路径清单含 `U` 未合并态,因 merge 冲突时 git 把该文件标为 unmerged,只按 ACMR 过滤会恰好漏掉本门要拦的那一类)/ 缺省(判所有跟踪文件工作区内容,`git ls-files`,实测 16561 候选 1.5s)/ `--rev <sha|HEAD>`(判提交树,`git grep -Ilz` 定位候选 + blob 复核,供事后核验)。护栏:自豁免(本门脚本与测试必含字面量,按文件名前缀跳过)、>2MB 大文件、二进制(前 8KB 含 NUL),三类**均在输出里如实计数**;退出码 0/1/2(2=git 候选解析失败等脚本自身异常,绝不静默放行)。取证 `--self-test` 23 例(含正反成对对照 + 真实 merge 未合并路径现场:成对判红 / 单行 setext 判绿 / 索引脏即红而仅工作区脏判绿 / rev 隔离 / 自豁免与大文件二进制计数)+ §22c 镜像测试 11 例。修复口径:**用 `git checkout --ours/--theirs` 或按 §12b 协作收尾重新归并,禁止手删三行标记当作已解决**;紧急跳过 `HUSKY_SKIP_CONFLICT_MARKERS=1`。

- **热路径 git 只读调用超时**(80):check-git-read-timeout.mjs(blocking,2026-09-23 立)—— 堵"提交像死掉了"这类无界挂起:`scripts/check-port-registry.mjs` 一处 `execSync('git ls-files')` 无 `timeout`,在共享工作区挂住 **80 分钟而 CPU 只用 2.84s**(等锁/等 IO 型),而 `git status`、typecheck 全看不出异常。全仓首参锚定实测 **159 处** git 派生调用**无一带 timeout** ⇒ 不是个别疏忽,是没有约束。口径刻意收窄三条(与门 52 同取向:宁漏不误报):① 只判钩子/守护链可达的 `HOT` 文件清单,不判测试夹具与端内代码;② 只判**动词为字面量**的调用 —— `git(args)` / `runGit(args)` 这类包装器动词未知,整体加超时会连带 bound 写操作,而 **`commit`/`add`/`reset`/`mktree` 中途被 SIGTERM 可能留下 `.git/index.lock`**,把一次挂起换成全局阻塞;③ 只判只读动词表内的调用。写动词与包装器**不判但如实计数**(输出行含 `写动词不判 N 处 / 包装器不判 M 处`),绝不静默成"看起来全绿"。字符串与注释内的命中一律丢弃(`markHidden`)——该缺陷是自检抓出来的:判据会把 `const fixture = "execFileSync('git', …)"` 这种测试夹具源码当真调用判红。`timeout` 两种写法都认(`timeout: <ms>` 与简写 `{ timeout }`)。**动词提取按 token 位置逐个走,并对 `-C <dir>` / `-c k=v` 这类带值选项连值一起跳**(`pickVerb`)——旧正则要求数组首元素即动词,于是 `execFileSync(GIT_BIN, ['-C', repoDir, 'status'])` 与 `['-c','safe.directory=*','for-each-ref']` 这两种最常见形态**根本匹配不到**:门恒报"0 处"而实际有 2 处无界只读调用(`git-push-guard.mjs`,已随本次清零)。判据盲区由并行会话的交付报告指出、以自检用例钉住;教训是**"扫到 0"必须先怀疑判据,再相信世界**。`HOT` 现 18 项(新增 `apps/cli/src/worktree.ts`)。**存量已随本门一并清零**(runner 3 处 + converge 4 处 + commit-loss-guard 默认值 1 处),不留基线债。取证 `--self-test` 12 例(含夹具/注释不判、简写属性不假红、写动词计数、-C 与 -c 带值选项取动词)+ `node --test scripts/tests/check-git-read-timeout.test.mjs` 7 例(含**装车证明**)。紧急跳过 `HUSKY_SKIP_GIT_READ_TIMEOUT=1`。 **登记新门前先查编号占用**(`grep -oE "^    id: .[0-9]+.," scripts/guardian-runner.mjs | sort -V | uniq -d` 应只出历史遗留项):同日多会话在数组同一位置各加一道门必然撞号 —— 实测 75/76 已各重复一次,本门最初登记的 79 也与并线进来的 check-no-conflict-markers 同号,故改号为 80 并在镜像测试里钉死"本门编号在 runner 中必须出现恰好一次"。
- **品牌邮件通道对账**(81):check-brand-email-channel.mjs(blocking,2026-09-23 立)—— 拦绕过品牌模板层的 ops 自发纯文本邮件("能发出去、typecheck/lint 全绿,用户收到的邮件没样式",与守门 72/78 同族):R1 `Send-MailMessage` 调用语句缺 `-BodyAsHtml` / R2 `api.resend.com/emails`(含 host+path 分行形态)同一发送上下文无 `html` 字段 / R3 有发信动作(`createTransport`/`sendMail`/`api.resend.com`)却不引用 `email-templates` 也不调用 `notify-deploy-failure`;范围 `deploy/**`+`scripts/**`(不含 tests)+workflows `*.yml`,注释行/裸域名不判(宁漏不误报),行内豁免 `brand-mail-exempt: <原因>`,存量走 `scripts/brand-email-channel-baseline.json` 只减不增(建门实测:ps1 自拼通道已被并行会话清除,副产品揪出第三条通道 `scripts/check-credential-health.mjs` 已入基线待迁移)。**修法的唯一姿势:ops 邮件一律经 `apps/api/scripts/notify-deploy-failure.ts`(版式由 `email-templates.ts` 单点决定),不得端内自拼传输层**。取证 `--self-test` 30 例(正反成对+临时仓双取材面)+ 镜像测试 8 例(含装车证明);`--staged` 暂存集为空/取不到 → 退化全量(守门 70 教训);紧急跳过 `HUSKY_SKIP_BRAND_MAIL_GUARD=1`。

- **品牌邮件通道对账**(81):check-brand-email-channel.mjs(blocking,2026-09-23 立)—— 拦绕过品牌模板层的 ops 自发纯文本邮件("能发出去、typecheck/lint 全绿,用户收到的邮件没样式",与守门 72/78 同族):R1 `Send-MailMessage` 调用语句缺 `-BodyAsHtml` / R2 `api.resend.com/emails`(含 host+path 分行形态)同一发送上下文无 `html` 字段 / R3 有发信动作(`createTransport`/`sendMail`/`api.resend.com`)却不引用 `email-templates` 也不调用 `notify-deploy-failure`;范围 `deploy/**`+`scripts/**`(不含 tests)+workflows `*.yml`,注释行/裸域名不判(宁漏不误报),行内豁免 `brand-mail-exempt: <原因>`,存量走 `scripts/brand-email-channel-baseline.json` 只减不增(建门实测:ps1 自拼通道已被并行会话清除,副产品揪出第三条通道 `scripts/check-credential-health.mjs` 已入基线待迁移)。**修法的唯一姿势:ops 邮件一律经 `apps/api/scripts/notify-deploy-failure.ts`(版式由 `email-templates.ts` 单点决定),不得端内自拼传输层**。取证 `--self-test` 30 例(正反成对+临时仓双取材面)+ 镜像测试 8 例(含装车证明);`--staged` 暂存集为空/取不到 → 退化全量(守门 70 教训);紧急跳过 `HUSKY_SKIP_BRAND_MAIL_GUARD=1`。
- **品牌邮件通道对账**(81):check-brand-email-channel.mjs(blocking,2026-09-23 立)—— 拦绕过品牌模板层的 ops 自发纯文本邮件("能发出去、typecheck/lint 全绿,用户收到的邮件没样式",与守门 72/78 同族):R1 `Send-MailMessage` 调用语句缺 `-BodyAsHtml` / R2 `api.resend.com/emails`(含 host+path 分行形态)同一发送上下文无 `html` 字段 / R3 有发信动作(`createTransport`/`sendMail`/`api.resend.com`)却不引用 `email-templates` 也不调用 `notify-deploy-failure`;范围 `deploy/**`+`scripts/**`(不含 tests)+workflows `*.yml`,注释行/裸域名不判(宁漏不误报),行内豁免 `brand-mail-exempt: <原因>`,存量走 `scripts/brand-email-channel-baseline.json` 只减不增(**2026-09-24 实测基线 `counts` 已为 `{}`** —— 建门时揪出的第三条通道 `scripts/check-credential-health.mjs` 已迁到同一派发器,未用 `brand-mail-exempt:` 糊过去)。**范围已两度扩面**(2026-09-24):原只扫 `deploy/**`+`scripts/**`,而告警邮件正文的实际出口在 `monitoring/alertbridge/*.cjs` —— 目录与扩展名**双重不可见**;现含 `monitoring/**` + `.cjs/.mts` + `apps/api/scripts/**`,并新增两条判据:**R3b** 接了唯一出口却自带一份 HTML 版式(三段与门:邮件版式标记 ∧ ±10 行样式指纹 ∧ 本文件确在邮件语境,注释行不计)—— R1/R2/R3a 只看"有没有走模板",对这一型全盲;**R4 告警接收面** `monitoring/alertmanager/**` 的 yml/yaml/tmpl 出现 `email_configs`/`smtp_*`(AM 原生邮件 = 无版式纯文本)、IM 中转 receiver(名字与 host/路径**都**认,`feishu-copy` 改名躲不过)、或 `webhook_configs` 里 url ≠ `BRIDGE_URL` ⇒ 红;注释行不判红但 `amCommentLiterals` 如实计数。**全仓不扫 yml**(根 compose / prometheus / loki 的历史注释里就写着 `dingtalk-webhook`,全仓扫必假红 —— 面外写进断言)。`stagedTriggers` 与扫描面**必须同步扩**,否则"只改 bridge/模板的提交"根本不唤起本门(判据存在而永不调用 = 没有,守门 70/76 同型;该字段今晚被并发整文件回写抹掉**三次**,由镜像测试"装车证明"抓回)。**修法的唯一姿势:ops 邮件一律经 `apps/api/scripts/notify-deploy-failure.ts`(版式由 `email-templates.ts` 单点决定),不得端内自拼传输层;Alertmanager 出口只有 bridge 一条**。取证 `--self-test` 61 例(正反成对+临时仓双取材面)+ 镜像测试 20 例(含装车证明与 `BRIDGE_URL` 与渲染器逐字等值的漂移即红断言);`--staged` 暂存集为空/取不到 → 退化全量(守门 70 教训);紧急跳过 `HUSKY_SKIP_BRAND_MAIL_GUARD=1`。
- **主题接线对账**(91):check-theme-prop-wiring.mjs(blocking,2026-09-24 立)—— 堵"顶栏深色 + 正文浅色"这类**同屏双色档案**:`packages/app` 213 个 theme-driven 组件的形参默认值是 `colorScheme = 'light'`,调用方漏传就静默脱主题,类型系统完全不红(`colorScheme?:` 可选)。三判据 —— **missing**(渲染点未传该属性)/ **literal**(写死 `'light'` 或 `'dark'`)/ **spread-unknown**(仅 `{...props}` 展开且回溯不出来源,判不出即如实报数,不静默放行)。建门实测:此前一轮用"JSX 元素文本里有没有 `colorScheme` 字样"统计出"漏传 0 处",**该结论是错的** —— 换成花括号深度扫描 + 组件清单自动推导后真实命中 **118 处 / 117 文件**,即 115 个屏一直在静默锁死浅色档案。**盲区已补(比漏修更值得记)**:写死的 `'light'` 有 5 处藏在**对象构造里**而非 JSX 属性上(`const props = { colorScheme: 'light' }; return <SharedX {...props} />`),"字面量"判据只解析 attrs 故从不判红,"未接线"判据见到 spread 就笼统归"判不出" —— 这 5 屏以"待人工核"的名义静默锁死浅色,**门一直是瞎的**。现由 `resolveSpreadThemeValue()` 回溯对象构造再判:对象里有字面量 → 判红;对象里确实没这个键 → 判 missing;props 来自函数形参且本文件无对象字面量 → 仍承认判不出,不猜。**基线 `scripts/theme-prop-wiring-baseline.json` 现须为空 `{counts:{}}`(零容忍)**,`--strict` 口径为"0 未接线 / 0 字面量 / 0 判不出"。行内豁免 `theme-wiring-exempt: <原因>`(须带原因)。取证 `--self-test`(**含阳性对照**:注释里写 `colorScheme: 'light'` 不得误判、不传 `src` 时行为与旧版一致)+ §22c 镜像测试 `scripts/tests/check-theme-prop-wiring.test.mjs`(含**装车证明**:runner 里必须真有 id 91 + blocking + skipEnv,且本门编号在 runner 中恰好出现一次)。**内容口径(2026-09-25 补齐,此前它是全链最后一道按磁盘判的主题门)**:全量判 **HEAD blob**、`--staged` 判**索引 blob**、`--worktree` 仅人工逃生舱;枚举与内容同面同轮;取不到 ⇒ **exit 2「无法判定」**(不冒红也不记绿),`--staged --worktree` 同给判死,全量枚举到 0 个 .tsx 判死;git 面必须先 `prefetch`(一次 `cat-file --batch`)再 `read`,未预取即 read 抛错而非静默跳过。立项当场即由此把他人**未提交**的 `ChatScreen.tsx` 摘 prop 判成本仓违规(HEAD 其实合规)。取证 `--self-test` 的 F1–F4 在 `mkScratch` 临时仓里造"索引≠磁盘"现场,并由**三条变异**分别证明 F1/F4 不是恒真;紧急跳过 `HUSKY_SKIP_THEME_PROP_WIRING=1`。
- **HEAD 悬空具名导入对账**(98):`scripts/check-dangling-local-imports.mjs`(blocking,2026-09-24 立)—— 与守门 77 B6 同族的**另一半**:77 管"用了标识符却没 import",本门管"`import { X } from './y'` 而 y 根本不导出 X"。两者都只在编译期可见,而 `pnpm typecheck` 只跑共享工作区 —— 工作区恰好是旧基线时**两边都不红**。2026-09-24 一天内各中一次:`rnRadius['2xl']` 两处未 import 让真机 release 包启动即 SIGABRT(Metro 不查类型,打包成功≠能跑);`PermissionTierRow` 经 `git log --all -S` 证明**从未在任何提交里存在过**,而引用它的注释还写着"已抽到 ChatDisclosure"。判据 D1(具名导入无对应导出)/ D2(相对路径解析不到);口径与 77/83/90 一致:全量判 **HEAD blob**、`--staged` 判索引 blob、**棘轮锚点恒为该文件 HEAD 自身违规数**(把锚点写成 0 会让存量 300+ 文件整片报红 ⇒ 逼人 `--no-verify` ⇒ 全部守门作废);宁漏不误报 —— `export *` 目标不可枚举即放过,缩进/注释/模板字符串里的 import 形态一律不判。真仓 HEAD 实测 8042 源文件、悬空 6 处且**全在测试面**,生产代码 0 处。取证 `--self-test` 19 例 + §22c 镜像测试 5 例(含"存量只允许落在测试文件"与 runner 装车证明)。紧急跳过 `HUSKY_SKIP_DANGLING_IMPORTS=1`。
- **架构契约对账**(103):`scripts/check-architecture-policy.mjs`(blocking,2026-09-25 立)—— 与其余全部守门**方向相反**的一道:其余是"发现一类违规 → 写一条判据"(打地鼠),本门读 `config/architecture-policy.yaml` 这张**声明表**,从声明反查违规。四类红:**T1** 表与现实脱节(模块改名/目录搬走而表没跟上 —— 表一过期,依赖它的所有判断都在对着空气打分)/ **C1** 单文件行上限(阈值 6000,**取自 HEAD 实测最大文件 5258 行之上**,依据写在 yaml 注释里)/ **D1 D2** 依赖方向(包 import 端、或 `requires` 未声明就 import;以及反向按 rank 更深一层)/ **D3** 深导入(绕过 `public_entrypoints` 直接摸别的包内部 —— 它是"端内重复实现"的入口)。**渐进收口是设计前提**:存量模块一律 `managed: false` ⇒ 只报数不判红(立项实测全量 exit 0、报数 3 处、`managed:true` 0 个),翻 true 前先用 `--managed-trial <id>` 看条数,**不得为消红去改阈值或把不该对外的模块标成 exported**。行内豁免 `arch-exempt: <原因>`(须带原因)。口径同 70/77/83/98/101:全量判 **HEAD blob**、`--staged` 判索引 blob,策略表自身按 HEAD→索引→工作树降级取且**退到工作树会大声提示**;取不到判"无法判定",不冒红也不记绿。取证 `--self-test` 51 例(成对正反 + 两面口径差异 + 解析器坏了必须大声失败)+ 镜像测试 10 例(含 runner 装车证明与"同一代码 managed 两侧结论不同"的判据有牙证明)。编号说明:任务书原定 102,落地前已被 `check-glyph-arrow-icon` 占用,按"后来者改号"顺延 103。紧急跳过 `HUSKY_SKIP_ARCH_POLICY=1`。
- **守门 77 B6 的括号形态盲区(2026-09-24 补)**:B6 首版的使用形态正则是 `rnRadius\s*\.`,而本门 `targetOf()` 对 2xl 档**规定的写法恰是 `rnRadius['2xl']`**(`rnRadius.2xl` 不是合法 JS)—— 门让你怎么写,门就看不见怎么写。现判据改为 `\s*(?:\.|\[)` 两种形态同视,`--self-test` 补 3 例成对正反对照,镜像测试补"括号形态必须被看见"的装车证明。教训:**判据必须覆盖自己产出的那一种形态**,否则它只拦得住别人、拦不住自己。

### 计划任务与 .vbs 的硬约束(2026-09-20 立,由本人引入的弹窗回归收口)

- **🚫 计划任务禁止直接执行控制台程序**:InteractiveToken 下 `/tr "node.exe xxx"` 会让 Windows **显示控制台窗口**(每 2 分钟闪一扇黑窗)。一律经 `wscript.exe "<name>-hidden.vbs"` 包装(`objShell.Run(cmd, 0, False)` = SW_HIDE);`-WindowStyle Hidden` 与 `powershell -WindowStyle Hidden` **仍会闪**,不作为豁免手段。现存正例:`git-guardian-hidden.vbs` / `cleanup-zombie-processes-hidden.vbs` / `kill-git-selector-hidden.vbs`。
- **🅰 `.vbs` 文件必须纯 ASCII**:`cscript`/`wscript` 按 **ANSI(本机 GBK)代码页**解码 `.vbs`,UTF-8 中文注释会被错切成伪引号 → **编译期**语法错 → wscript 弹 "Windows Script Host" 对话框,而 `schtasks` 仍报成功(与 §27 的 PowerShell/ANSI 同类陷阱)。`.vbs` 里**不得写中文注释**,说明一律英文;`git-guardian.mjs --install` 已内置"注册前用 `cscript //nologo` 实跑一次、非零退出即拒绝注册"的预检。
- **`WshShell.Run` 命令串引号规则**:裸程序名**不能**加引号(`"node.exe" arg` 会按字面文件名解析而失败),仅路径真含空格时才加引号;参数照常加引号。
- **`WScript.Shell.Environment()("不存在的键")` 返回 Null**,`Len(Null)` 亦为 Null,后续比较抛 "Type mismatch" 并静默中止。读环境变量必须 `& ""` 归一:`raw = shell.Environment("Process").Item("X") & ""`。
- **Electron 宿主无视 `NODE_OPTIONS`**:实测 Qoder/Trae 自带运行时内 `process.env.NODE_OPTIONS === undefined`(Electron 主动剥离),故机器级 windowsHide 钩子**只覆盖独立 `node.exe`**,覆盖不到 IDE 内部的 git 调用。后者若弹窗,只能关 IDE 自带 git 集成或改其调用方,不要误以为环境变量层能解决。

- **圆角容器背景溢出守门**(11b): `scripts/check-rounded-overflow.mjs`(warn) —— 拦"父 rounded-lg/xl/2xl/md 无 overflow-hidden × 子 bg-* 贴边无 rounded-* 无 margin 且非 absolute/fixed"六条件齐备的圆角冒角,--staged 只咬新增行,违规 exit 1(`scripts/check-rounded-overflow.mjs:457`,判定 `isViolation:229`);真跑全量 rc=0(3s);无 --self-test;紧急跳过 `HUSKY_SKIP_ROUNDED_OVERFLOW=1`(脚本自读 :56)。
- **TagsView 选中态描边定稿守门**(11c): `scripts/check-tagsview-visual.mjs`(blocking) —— 拦 TagsView active 态丢 `outline-2+outline-border` 定稿、全站出现 `outline-black`/`dark:outline-white` 回潮、pinned/inactive 背景 token 被换、测试反向断言被删,违规 exit 1(`scripts/check-tagsview-visual.mjs:188`,全站扫描段 `extractQuotedStrings:64`);真跑全量 rc=0(1s);无 --self-test;⚠️失败提示与头注称 `HUSKY_SKIP_TAGSVIEW_GUARD=1` 可跳,实际脚本与 runner 条目均不读该 env——无真实应急通道。
- **单文件行数守门**(11e): `scripts/check-file-size.mjs`(blocking) —— 拦 --staged 新增文件超 800 行(`FILE_SIZE_LIMIT`/`--limit` 可改阈值),exit 1(`scripts/check-file-size.mjs:216`),全量模式仅报告 exit 0;真跑全量 rc=0(1s);无 --self-test;紧急跳过 `HUSKY_SKIP_FILE_SIZE=1`(脚本自读 :51)。
- **原生弹窗禁用守门**(11f): `scripts/check-no-native-dialog.mjs`(blocking) —— 拦 staged 新增行里调用形态 `alert(`/`confirm(`/`prompt(`(去注释+去字符串后判定,`stripStringsAndComments:79`),exit 1(`:298`),全量仅报告;真跑全量 rc=0(7s);无 --self-test;紧急跳过 `HUSKY_SKIP_NATIVE_DIALOG=1`(脚本自读 :40)。
- **渐变遮罩禁用守门**(11g): `scripts/check-no-mask-image.mjs`(blocking) —— 拦 staged 新增行出现 `mask-image`/`-webkit-mask-image` 字面量(注释行豁免 `isExempt:63`),exit 1(`:244`),全量仅报告;真跑全量 rc=0(3s);无 --self-test;紧急跳过 `HUSKY_SKIP_MASK_IMAGE=1`(脚本自读 :39)。
- **i18n JSON 同层重复键守门**(2e-dupns): `scripts/check-i18n-duplicate-namespaces.mjs`(blocking) —— 字符级深度栈扫 `packages/i18n/messages/**/*.json` 找同一对象层重复键(JSON.parse last-wins 静默遮蔽),发现即 exit 1(`:163`,核心 `findDuplicateKeys:64`),另接 cert `i18n-duplicate-ns`(warnOnly:false)+ CI path 触发;真跑全量 rc=0(0s);无 --self-test;紧急跳过 `HUSKY_SKIP_I18N_DUP_NS=1`(脚本自读 :115);⚠️头注称"调用方 guardian-runner 第 2f 项",实际条目 id 为 `2e-dupns`。
- **陈旧副本/保护区删除守门**(30c): `scripts/check-stale-copy.mjs`(blocking) —— 拦两态:staged D 命中保护区(drizzle .sql/各端 tests/scripts/check-*/scripts/lib)或 staged M 内容 = 该路径基线祖先的历史 blob(`git log --find-object` 判据),exit 1(`:116`);真跑 rc=1(13s)红 3 处(`PROJECT_PLAN.md`、`scripts/check-c-drive-pollution.mjs` 及其测试被判陈旧副本——系本机共享工作树滞后/并行会话现场,存量红待清,非 HEAD 缺陷);无 --self-test;紧急跳过 `HUSKY_SKIP_STALE_COPY=1`(脚本自读 :55)。
- **LLM provider 字典 schema 守门**(33): `scripts/check-llm-provider-schema.mjs`(blocking) —— 对 `apps/ai-service/.env` 的 `LLM_PROVIDERS(_JSON)` 跑 7 条规则(32 provider 白名单/字段类型/未知 provider/空值/重复等),含 error 即 exit 1(`:339`),`.env` 缺失或参数错 exit 2(`:306/:316/:324`);真跑 rc=0(0s);无 --self-test,但镜像测试 node --test 真跑 rc=0(45 例);无应急通道(runner 条目无 skipEnv、脚本不读 env);⚠️头注"集成位置:.husky/pre-commit 第 N+1 项"与实际不符(实际挂 runner id 33 + 专属 CI `llm-provider-schema-test.yml`)。
- **新增 @ts-ignore/@ts-nocheck 守门**(34): `scripts/check-ts-ignore.mjs`(warn) —— 扫 staged .ts/.tsx/.mjs/.js/.cjs 注释形态新增 @ts-ignore/@ts-nocheck(e2e 等白名单 `SKIP_PATTERNS:55`),exit 1(`:160`)但 runner 以 warn 计不阻塞;真跑 rc=0(0s);无 --self-test,但镜像测试 node --test 真跑 rc=0(15 例);无应急通道(warn 级)。
- **web globals.css token 单源守门**(37): `scripts/check-web-tokens-sync.mjs`(blocking) —— 拦两态:globals.css 丢失 `@import tokens.css`(:136-141 REGRESSION)或顶层 `:root/.dark` 重宣 tokens.css @theme 变量,exit 1(`scripts/check-web-tokens-sync.mjs:132` 读失败 / 末尾 exitCode 汇总);真跑全量 rc=0(0s);无 --self-test;无应急通道(runner 无 skipEnv、脚本不读 env);⚠️头注只述 :root/.dark 回归,实际还承载 @import 存在性判据(门 89 台账引用的是后者)。
- **solito 幽灵依赖回潮守门**(38): `scripts/check-solito-residue.mjs`(blocking) —— 五查(package.json 三类依赖/pnpm.patchedDependencies/pnpm-workspace publicHoist/`patches/solito*.patch`/packages/app tsx `from 'solito/...'`)命中即 exit 1(`:353`,检测函数 `:110/:154/:189/:234`);真跑 rc=0(0s);无 --self-test,但镜像测试 node --test 真跑 rc=0(15 例);无应急通道。
- **共享层重复实现守门**(40): `scripts/check-shared-layer-duplication.mjs`(blocking) —— apps/*/src 的 export 名与 packages/shared/src export 名求交集(`collectExports:21`,`scanAppDir:85`),端内重新实现即 exit 1(`:152`);真跑 rc=0(0s);无 --self-test;无应急通道(runner 无 skipEnv;re-export wrapper 豁免逻辑在白名单)。
- **统一返回键私接守门**(46): `scripts/check-inline-back-button.mjs`(blocking) —— 拦 apps/web/src 内 `router.back()`/`history.back()` 出现在 GlobalTopBar.tsx 之外(注释行自动跳过 `isCommentLine:51`,ALLOWLIST/BANNED :317-318),exit 1(`:90`);真跑全量 rc=0(15s);无 --self-test;⚠️头注称 `HUSKY_SKIP_INLINE_BACK_GUARD=1` 可跳,实际脚本与 runner 条目均不读该 env——无真实应急通道。
- **GitHub Actions 步骤顺序守门**(48): `scripts/check-workflow-step-order.mjs`(blocking) —— 同 job 内 `actions/setup-node` 带 `cache: pnpm` 而 `pnpm/action-setup` 未排其前 → exit 1(`scripts/workflow 扫描主体 :79`,立因 2026-09-10 两 workflow 恒败);真跑 rc=0(1s);无 --self-test;紧急跳过 `HUSKY_SKIP_WORKFLOW_ORDER=1`(脚本自读 :30)。
- **迁移记账结构守门**(49): `scripts/check-migration-bookkeeping.mjs`(blocking) —— 离线 B1-B5:journal tag↔drizzle/*.sql 双向双射、tag 唯一、when 严格递增唯一、idx 唯一、journal 结构完整,违反 exit 1(`:258`);`--db` 追加 B6-B9 库内双射(行数/created_at 集合/max 相等/hash 合法唯一);真跑(离线)rc=0(0s),`--db` 模式未跑(连库);无 --self-test;紧急跳过 `HUSKY_SKIP_MIGRATION_BOOKKEEPING=1`(脚本自读 :56);另被 ci.yml run 步引用。
- **next-env.d.ts 构建污染守门**(50): `scripts/check-next-env-dist.mjs`(blocking) —— --staged:暂存的 `apps/web/next-env.d.ts` 引用 `.next-<suffix>` 变体即无条件 exit 1(`:112`);全量:变体目录仍存活(并发会话在用)只提示、目录已消失才判红;真跑全量 rc=0(0s);无 --self-test;⚠️头注称 `HUSKY_SKIP_NEXT_ENV_DIST=1` 可跳,脚本与 runner 条目实际都不读该 env——无真实应急通道。
- **能力目录三方一致守门**(51): `scripts/check-capability-catalog.mjs`(blocking) —— A `capability-catalog.ts`↔`generated/capabilities.json` 产物一致 / B v1 路由逐 handler 被能力闸覆盖 / C scope 语义(platform/thirdPartyEligible=false 不得进 /v1 rules)/ D 反向核对 warn,违规 exit 1(`:1097` 附近聚合,解析核心 `parseCatalogEntries:285`),--staged 把 B/C 收窄到暂存 v1 文件;真跑全量 rc=0(2s);--self-test rc=0 ✅ 18 条断言全绿 + 镜像测试 `scripts/tests/check-capability-catalog.test.mjs` node --test 真跑 rc=0(20 例);紧急跳过 `HUSKY_SKIP_CAPABILITY_CATALOG_GUARD=1`(脚本自读 :893;id 51 注册块 HEAD:1131/1133 已 grep 直验在场)。
- **未提交改动年龄守门**(54): `scripts/check-uncommitted-age.mjs`(warn) —— `git status --porcelain -z` 取已改+未跟踪源码文件(.ts/.tsx/.js/.jsx/.mjs/.cjs/.py/.css/.sql),mtime 年龄>45 分钟(阈值可 `resolveThresholdMin:200` 覆盖)即判风险 exit 1(runner warn 计不阻塞;绝不输出文件内容,.env*/generated/.ihui-agent 豁免);真跑 rc=0(0s);--self-test rc=0 ✅ 28 例全绿 + 镜像测试 node --test 真跑 rc=0(21 例);无应急通道(warn 级)。
- **权限模式词汇对账守门**(68): `scripts/check-permission-mode-vocabulary.mjs`(blocking) —— R1 TS/Py 双注册表成员+别名逐字同(`checkMirror:163`)/R2 别名值域闭合(`checkAliasClosure:195`)/R3 已知消费点字面量落在成员∪别名键、AgentLoopV2 禁 `auto`/自造白名单(`checkConsumers:214`/`checkNoSecondList:258`)/wire 镜像(`checkWireMirrors:312`),违规 exit 1(`:586`);真跑全量 rc=0(0s);--self-test rc=0 ✅ 21 条输出(含"两侧一致放过"反例)+ 镜像测试 node --test 真跑 rc=0(11 例);紧急跳过 `HUSKY_SKIP_PERMISSION_VOCAB=1`(runner skipEnv)。
- **端内绕过 api-client 直连后端守门**(73): `scripts/check-direct-backend-calls.mjs`(blocking) —— 拦端内裸 `fetch(`/`axios.*(`/`{Taro,wx,my,tt,…}.request(`/`new XMLHttpRequest(`+`.open(`/`navigator.sendBeacon(`/`http(s).request(` 且 URL 经污点传播锚定本仓后端(BASE_URL 符号族/自家 host/锚定 `/api/` 段,跨文件回溯深度 5,`findCallSites:410`),违规 exit 1,存量走 `scripts/direct-backend-calls-baseline.json` 棘轮只减不增;未真跑(副作用 grep 命中 :76/:1093 基线回写形态,按规则整门保守不跑);--self-test 存在(3 处)未跑(其镜像测试 node --test 真跑 rc=0,15 例);紧急跳过 `HUSKY_SKIP_DIRECT_BACKEND_CALLS=1`(runner skipEnv,留痕)。
- **词表键五语言可解析守门**(74): `scripts/check-word-table-resolvable.mjs`(blocking) —— W1 对象字面量词表发现(`scanObjectLiterals:133`/`discoverWordTables:171`)/W2 权威语料 ≥2 键且 ≥50% 锚定为 i18n 词表/W3 逐键×逐语言在消费端 shared+端合并视图必须解析出非空且≠键名/W4 miniapp 离线包载荷/W5 未接入端只记 notice 不计退出码,违反 exit 1(`:885` 提示行所在判红聚合)。**W3 的"消费端"按符号粒度认**(2026-09-24 修盲区):同一模块多导出时,只有经模块内引用链**真正读到这张表**的导出符号才算消费端。旧判据是"提到任一导出符号即算",于是 `packages/shared/src/utils/error-messages.ts` 里返回固定中文、根本不查词表的 `toUserFriendlyMessage` 把 mobile-rn 40 个屏算成了 `errors.*` 的消费端,产出 70 枚 blocking 恒红 —— 而那 14 枚键的出口 `getErrorI18nKey` 全仓零调用方。顶层切分失效与 re-export 两种情形一律**退回全量符号面**(宁可多报,绝不让判据失效表现为"没有消费端"而把红洗成绿),`--self-test` 与镜像测试各含变异对照。取证口径:干净 HEAD(`git archive` 隔离检出)真跑 exit 0,55 张表 54 张结论逐字不变、唯一变化的正是上述那一张;--self-test rc=0 ✅ 33 条(含真语料注入验证)+ 镜像测试 node --test 真跑 rc=0(26 例);紧急跳过 `HUSKY_SKIP_WORD_TABLE_RESOLVABLE=1`(runner skipEnv + 脚本自读 :57)。
- **Agent Engine 协议三方 parity 守门**(无 id,hook :582): `scripts/check-agent-engine-parity.mjs`(blocking) —— JSON-RPC 引擎 handler 表(agent_engine.py 事实源)↔ TS `packages/sdk/src/agent-engine.ts` ↔ Python SDK 的方法/notifications/streaming/error code 常量集合静态对齐,漂移 exit 1(`:216`),hook 以 `--quiet` 跑;真跑全量 rc=0(0s);无 --self-test;紧急跳过 `HUSKY_SKIP_AGENT_ENGINE_PARITY=1`(hook :578)。
- **凭据前缀一致性守门**(无 id,hook :538): `scripts/check-api-credential-prefix.mjs`(blocking,--staged) —— 面向用户文案/示例里 `Authorization: Bearer`/`api_key` 必须 `ihui_` 前缀,`sk_` 仅允许在描述 `X-Api-Secret` 语境,暂存违规 exit 1(`scanFile:160`,作用域 `inScope:145`),全量模式仅报告 exit 0;真跑全量 rc=0(2s);--self-test rc=0 ✅ 20 条规则;无镜像测试;紧急跳过 `HUSKY_SKIP_CREDENTIAL_PREFIX_GUARD=1`(hook :534)。
- **auth refresh 单例守门**(无 id,hook :273): `scripts/check-auth-refresh-singleton.mjs`(blocking) —— 客户端业务代码禁绕过 `refreshAccessTokenOnce` 单例裸发 `/auth/refresh`(fetchApi/fetch/裸 `refreshAccessToken(` 三形态,`scanFile:131`;定义处/后端/测试/token-provider 注入 :98/extension 合法刷新域 :115 白名单),违规 exit 1(`:211`);未真跑(网络 grep 命中 `fetch(` —— 命中行是门自身判据模式串,脚本实为只读,仍按规则保守不跑);无 --self-test;紧急跳过 `HUSKY_SKIP_AUTH_REFRESH=1`(hook :269)。
- **桌面端事件链路接线守门**(无 id,hook :606): `scripts/check-desktop-event-wiring.mjs`(blocking) —— Tauri `.emit("事件名")` → `use-desktop.ts` listen+case+`new CustomEvent` → 前端 `addEventListener`/事件注册表三层对账,任一层断链 exit 1(立因托盘主题/设置点击无反应);**它在 `guardian-runner` 与 `check:all` 的清单之外**(钩子直调、无 id)—— 所以"全量审计 0 红"不覆盖它,反过来它红了也只会表现为"每次提交都要跳门";紧急跳过 `HUSKY_SKIP_DESKTOP_EVENT_WIRING=1`(hook :602);另在 `pnpm check:all` 链内。
  - **G4 的"就绪点亮"按可达性判,不按字面量所在函数体判(2026-09-26 修,登记 G-205)**:该判据原先只取 `take_pending_deep_links` 的函数体找 `target_ready = true`,而 `15573c449` 把状态迁移抽成了纯函数 `apply_take_ready`(为了 Rust 侧单测能直接断言)—— 赋值换了地方,门就红了,于是**全队每次提交被逼 `--no-verify`、约 158 道门对每次提交作废**。现判据 = 命令体内没有时,**沿命令体真的调用到了的函数再找一跳**(不追传递闭包、不做全文件搜索)。正反自检两条:赋值提交给同文件 helper ⇒ 不得判红(门必须认自己产出的形态)/ 命令体与 helper 都不置就绪 ⇒ **必红**(允许一跳不等于放过)。**同格判据由两条会话同日独立改到,落地的是走得更远的那一版**(它同时新增 **G8** 整页导航复位对账,并含"导航面找不到任何发起点 ⇒ 判失明、不记通过");本会话那版作废 —— 记这一句是为了让后来人知道**最终生效的是哪一份实现**,两条登记并存就会有人按没落地的改法去推下一步。**通用规矩:凡"某处必须出现 X"的判断,先问"X 被抽进被调函数还算不算";按文本位置判 red 的代价从来不是误报一次,而是大家学会跳过钩子。**
- **miniapp ICU .replace 反模式守门**(无 id,hook :403 条件 blocking): `scripts/check-miniapp-replace-antipattern.mjs`(blocking,--staged) —— 拦 `t/tt('key', '...{{n}}...').replace('{{n}}',v)` 三命中规则(5 白名单:`t(key,{vars})` 等),staged 新增行命中 exit 1(`main:302`,异常 :305/:384 exit 2),仅 staged 触及 apps/miniapp-taro/src/ 时执行;真跑全量 rc=0(1s);无 --self-test;紧急跳过 `HUSKY_SKIP_MINIAPP_ICU_CHECK=1`(hook :387)。
- **createPortal 定位守门**(无 id,hook :495): `scripts/check-portal-fixed.mjs`(blocking,--staged) —— `createPortal(<div …>)` 定位容器必须显式 `position:'fixed'/'absolute'` style 或 `fixed/` 类(`checkPortalElement:65`;style 引外部变量降级 WARN 留人工 review),违规 exit 1(`:134`);真跑全量 rc=1(0s)红 1 处(`apps/web/src/components/feedback/portal-panel.tsx:147` 缺显式 fixed——全量口径存量待清;hook 实际以 --staged 只咬新增);无 --self-test;紧急跳过 `HUSKY_SKIP_PORTAL_GUARD=1`(hook :491)。
- **mobile-rn global.css token 同步守门**(无 id,由 `scripts/lib/pre-commit-hook.js` 的 16c 步条件 blocking 调用): `scripts/check-rn-global-css-sync.mjs`(blocking) —— `apps/mobile-rn/global.css` 的 `--color-*` 值与 `packages/design-tokens/src/styles/tokens.css` 逐变量对账(NativeWind 手抄副本,漂移即视觉 bug),不一致 exit 1,staged 触及两文件之一才触发。**取材面 2026-09-26 收口**(与 36/124/93 同口径):默认判 **HEAD blob**、`--staged` 判**索引 blob**、`--worktree` 只作人工逃生舱、两面旗同给 exit 2、任一面取不到**不回落**另一个面 ⇒ exit 2。旧形态两处缺陷叠在一起:① 默认判磁盘;② `--staged` 索引取不到时悄悄回落 HEAD —— **回落就是把"没判"写成"判过了"**,而自洽的假绿比可见的红难查。**同批给钩子调用点补上 `--staged`**(这条不是顺手改:本门收口后默认档判 HEAD,钩子若不传面旗就等于在审上一提交态,生成器刚写回、已 `git add` 的新值它结构上看不见 —— 而它是 `TOKEN_SYNC_TARGETS` mobile-rn 那一行写回之后的**唯一**复核闸)。取证:镜像测试 22 例(含 F1–F5:纯函数四态锁、临时仓三面互异、CLI 三面成对、无提交判死、F5 形状锁禁止 `gitShow` / `readFileSync(join(root` / `node:fs`),变异自证 `def:'worktree'` 后 4 例红 ⇒ "默认判 HEAD"不是恒真);真仓 HEAD 与索引两面 exit 0(187 受管档逐位同值、26 个端内自立 `--color-*` 档只报数)。仍无应急通道(hook 未设 skip env)。
- **SiteFooter 防回退守门**(无 id,hook :471): `scripts/check-site-footer.mjs`(blocking) —— 6 项定稿断言(关键 className 集合/ECOSYSTEM_GROUPS=5 分组/INTERNATIONAL+CHINESE_MODELS import+export/单一 `useTranslations('footer')`/5 语言 footer 键齐),失败 exit 1(`:174`,判定 `check:39`);真跑 rc=0(0s);无 --self-test;紧急跳过 `HUSKY_SKIP_FOOTER_GUARD=1`(hook :467);另有 `pnpm footer:guard`。
- **staged 文件数预检守门**(无 id,hook :44): `scripts/check-staged-files-count.mjs`(warn) —— staged 文件数 > 阈值(默认 10,`--max` 可调)打警告,仅 `--strict` 才 exit 1(`:66`);hook 用 execSync+catch 吞失败,恒不阻塞;真跑 rc=0(1s);无 --self-test;紧急跳过 `HUSKY_SKIP_STAGED_COUNT=1`(脚本自读 :36);⚠️头注"集成位置:.husky/pre-commit 第 0 项"与实际不符(实挂 `scripts/lib/pre-commit-hook.js:44`,且为 warn)。
- **样式改动 Verified-DOM trailer 守门**(无 id,.husky/commit-msg:10): `scripts/check-style-verification.mjs`(warn——wiring `|| true` 吞退出码) —— staged 含 apps/web `.css` 时 commit message 必须带 `Verified-DOM:` trailer,缺失 exit 1(`:79`),缺参数亦 exit 1(`:33`);未真跑(需 commit-msg 文件参数);无 --self-test,但镜像测试 node --test 真跑 rc=0(15 例);紧急跳过 `HUSKY_SKIP_STYLE_VERIFY=1`(脚本自读 :25);⚠️AGENTS.md §17 称"commit-msg hook 自动守门"暗示阻塞,实际接线 `|| true`(hook 注释自述"失败不阻断 commit,仅警告")。
- **Agent SSE 事件契约 parity 守门**(无 id,cert `sse-event-parity` warnOnly:false + `8end-consistency-cert.yml` path 触发 + check:all): `scripts/check-agent-event-parity.mjs`(仅CI) —— 全仓 SSE 生产面(api 路由 `event:` 模板/broadcastSSEEvent/@ihui/types 契约声明/ai-service agents·agent_runtime·langgraph·llm 生产者)提取事件名与消费面监听集合双向对账,漂移 exit 1(`:716`);未真跑(副作用 grep 命中 :77-79 —— 命中为 tee 结果文件回写);无 --self-test;无应急通道(CI 门)。
- **mobile-rn 语言包 key parity 守门**(无 id,cert `i18n-parity-mobile-rn` warnOnly:false + CI path 触发): `scripts/check-i18n-parity.mjs`(仅CI) —— `packages/i18n/messages/mobile-rn` 各语言键集以 `en.json` 为基准做缺失/多余 diff(`getAllKeys:11`),报告写 `tmp/i18n-parity-report.txt`(:62-63);未真跑(写 `tmp/` 报告命中副作用 grep);无 --self-test;⚠️脚本源码无任何 process.exit——退出码恒 0,cert 虽标 warnOnly:false 该判据是否真生效待持有人核;无应急通道。
- **死代码棘轮守门**(无 id,`knip.yml:74` run 步): `scripts/check-knip-ratchet.mjs`(仅CI) —— `knip --reporter json` 分类计数 vs `scripts/knip-baseline.json`(可 `KNIP_RATCHET_TOLERANCE` 加容差),任一项超基线 exit 1(`:182`),knip 不可用/解析失败 exit 2(`:74-99`),`--update` 收紧基线(`:140`);未真跑(基线回写 + 全量 knip 重);无 --self-test;无应急通道(CI 门)。
- **空库全链迁移重放守门**(无 id,`db-from-zero-migrate.yml:93` run 步 + path 触发): `scripts/check-migration-from-zero.mjs`(仅CI) —— 一次性空库按 journal 逐迁移 psql 重放 + `drizzle-kit migrate` 全链 + 迁移后库↔drizzle 元数据表/列双向 diff(`KNOWN_SCHEMA_HOLES` 基线扣减),不 100% 应用或 diff 非空即非零退出(`:719` 为脚本异常态),临时库必定 DROP;未真跑(在生产 PG :8810 建删临时库,§5 测试隔离铁律);无 --self-test,但镜像测试 node --test 真跑 rc=0(16 例);无应急通道(CI 门)。
- **NativeWind 升级就绪监控(反向判据)**(无 id,`nativewind-monitor.yml:45` 定时 + `pnpm nativewind:status`): `scripts/check-nativewind-status.mjs`(仅CI) —— `npm view nativewind dist-tags`,latest 为 5.x stable → exit 1(=升级窗口已到、提醒移除 metro monkey-patch,红≠违规,`:73`);registry 不可达 exit 2 不误报(`:48/:56`);真跑 rc=0(2s,latest 仍 4.x/preview);无 --self-test,但镜像测试 node --test 真跑 rc=0(15 例);无应急通道(监控语义)。
- **!important 禁用守门(CSS 感知版)**(无 id,`style-spec.yml:57` run 步): `scripts/check-no-important.mjs`(仅CI) —— 解析式扫 apps/web 样式(剥注释后),非豁免 `!important` 判违规 exit 1(`:141`,分析 `analyze:62`),豁免仅两类:`prefers-reduced-motion` 块 + 行内 `-- ihui-allow-important: 理由`;真跑 rc=0(1s);无 --self-test;无应急通道(CI 门)。
- **跨端色值 token 漂移对账**(93): `scripts/check-cross-end-tokens.mjs`(blocking,2026-09-24 由 id 90 改号而来 —— 与 SSE 门撞号，守门 89 的 R5 实测抓到) —— RN `rn-tokens.ts` hex ↔ miniapp/web `tokens.css` 按“确定语义相同”映射表逐位比对(hsl→hex 归一、rgba 空格归一、.dark 未覆盖按 cascade 回退),不一致 exit 1(:226/:263);另由 cert `cross-end-tokens`(warnOnly:false)+ CI path 触发 + check:all 承载;真跑 rc=0(0s);无 --self-test;无应急通道(runner 未声明 skipEnv)。
- **button 文本换行浏览器实测守门**(无 id,仅 `pnpm check:button-wrap:browser`): `scripts/check-button-text-wrap.mjs`(手动) —— Playwright 实测各 button 内最深 span 高/单行基线 ratio>1.4 判换行成 2 行,`--strict` 下命中或 server 不可达场景 exit 1(`:318/:393/:461/:468`,算法 `buildDetectFn:188`,探活 `probeServer:255`);未真跑(需 Playwright+dev server,且写临时目录 :351 命中副作用 grep);无 --self-test;无应急通道(不在提交链);⚠️头注称可用于 pre-commit/nightly,当前未接任何提交链。
- **全端杀手锏常量二次写死守门**(无 id,check:all): `scripts/check-killer-parity-ends.mjs`(blocking 语义,仅 check:all 内) —— 扫全部 TS 端源码中绕过 `tunables.py`→`constants.ts` 单源直接硬编码同值的"二次写死"(白名单显式豁免),违规 exit 1(`:156`,`--warn-only` 降级);真跑 rc=0(0s);无 --self-test;无应急通道。
- **mobile-rn 硬编码 hex 基线棘轮守门**(无 id,check:all + `pnpm check:rn-parity`): `scripts/check-mobile-rn-style-parity.mjs`(blocking 语义,仅 check:all 内) —— RULE-1 基线外新增 `#hex` 字面量即 BLOCK(exit 1,`scanFile:98`,基线 `scripts/rn-style-parity-baseline.json` 只减不增,`--update-baseline` 收紧 :130),RULE-2 基线失条目 WARN;未真跑(--update-baseline 回写命中副作用 grep);无 --self-test;无应急通道。
- **侧边栏导航死链守门**(无 id,check:all + `pnpm check:nav-dead-links`): `scripts/check-nav-dead-links.mjs`(blocking 语义,仅 check:all 内) —— `nav-data.ts` 全部 href 必须能在 apps/web/app 推出 page 路由((group)/_private 不占段、动态段通配 `toRoutePattern:59`),死链 exit 1(`:103`);真跑 rc=0(1s);无 --self-test;无应急通道。
- **workspace 包可安装性自检**(无 id,仅 `pnpm check:pkg-installable <pkg>`): `scripts/check-pkg-installable.mjs`(手动,发布前硬闸) —— 真 `npm pack` + 解包 8 项判据(dist 双产物在包内/不含 src/无凭据噪音/无 `workspace:` 残留/LICENSE+NOTICE/ESM 相对 import 带扩展名/manifest 名版一致),blocker exit 1(`:190/:201`),脚本异常 exit 2(`:167`),private 只提示不计红;未真跑(npm pack+mkdtemp 解包 :174 命中副作用 grep);无 --self-test,但镜像测试 node --test 真跑 rc=0(14 例);无应急通道。
- **自写 popover data-state 守门**(无 id,check:all + `pnpm check:popover-trigger-data-state`): `scripts/check-popover-trigger-data-state.mjs`(warn/手动,--staged 才 blocking) —— 含 `createPortal(` 且不 import Radix 的文件必须手挂 `data-state` 属性(`classifyFile:111`),全量模式 WARN exit 0,`--staged` 新增违规 exit 1(`:209`);真跑(全量)rc=0(0s);无 --self-test;紧急跳过 `HUSKY_SKIP_POPOVER_DATA_STATE=1`(脚本自读 :47)。
- **web 死 i18n key 审计别名 wrapper**(无 id,wiring 仅 template 层引用): `scripts/scan-web-dead-i18n-keys.mjs`(手动别名) —— 自身零判据:转发全部参数到 `scan-dead-i18n-keys.mjs --target=web` 并透传退出码(`:11-15` spawnSync,status 透传),实际判据/报告在统一入口(check:all 走 `--target all --exit 1`,CI 审计 workflow 跑其镜像测试);真跑 rc=0(3s,经委托链);无 --self-test,但镜像测试 node --test 真跑 rc=0(7 例)(其本体);无应急通道;⚠️门 89 判其接线为 template 弱接线,登记时勿当作独立闸。
- **守门接线层对账**(89): `scripts/check-gate-wiring.mjs`(blocking) —— 本门是其余全部守门的"装车检",查接线层四类结构缺陷:**R1/R2** 脚本头部或 AGENTS.md 声称"已接 pre-commit/pre-push/CI 必跑/第 N 项"而五处权威点(`guardian-runner` 的 `script:` ∪ `scripts/lib/pre-commit-hook.js` ∪ `.husky/*` ∪ 根 `package.json` ∪ `.github/workflows`+cert)全部零命中 → 拦;**R4**(2026-09-24 由"仅报数"升档,前置=真仓缺口 48→0 已清零)门已接线但 AGENTS.md/README.md 通篇未点名 → 拦(判据刻意宽松:出现去后缀同名即算,故只会漏报不会误拦);**R5** 同一 `id` 登记两道门 → 拦(串 skipEnv 与失败归属);**R7** 台账 `type=dispatcher` 的依据文件不存在或文件里没提该脚本 → 拦。R3(五处零命中且无肯定式声称)/R6(共用 skipEnv)/弱接线(仅 CI)只如实报数不判红。**取材口径分两面**:接线面(哪些脚本被登记为已接)一律按 **HEAD** 判 —— 共享工作区的工作区文件会滞后,产出相反结论;而**文档面**(AGENTS.md / README.md 有没有点名)按 **HEAD ∪ 索引**判(2026-09-24 改),因为"同一枚提交里既注册新门又补点名行"是正确姿势,只读 HEAD 会让这枚提交被自己判红 ⇒ 逼人 `--no-verify` ⇒ 全部守门作废;索引取不到退回 HEAD,口径在结论行如实报出,且**绝不读工作区**(只躺在工作区没 `git add` 的文档行不算点名,已由自检 M9d/M9e 一红一绿的可逆对照钉死)。;台账 `scripts/gate-wiring-allowlist.json` 只能救 R3,凡声称已接线者台账不救。⚠️ 本门对自身 `SELF_EXEMPT`(创建当期 HEAD 里还没有它),所以"它自己被摘线"由镜像测试 T13 钉死。真跑 `node scripts/check-gate-wiring.mjs` exit 0(已接线 143 / 台账豁免 13 / R4 0 枚,数字以命令末行现测为准);自检 `--self-test`(例数以末行为准,含 M8a/M8b 的 R4 双向端到端证明 + M9a-M9e 的文档取材口径五形态对照)+ `node --test scripts/tests/check-gate-wiring.test.mjs`(例数以末行为准,含 T13 装车证明与 T14/T15 取材对照);紧急跳过 `HUSKY_SKIP_GATE_WIRING=1`。
- **跨端主题透线对账**(91): `scripts/check-theme-prop-wiring.mjs`(blocking) —— packages/app 的主题驱动组件(形参解构含 `colorScheme` 且一律带 `= 'light'` 默认值)必须在每个 JSX 渲染点被**真的传参**:默认值就是"静默失效开关"，写死或漏传都不报错、typecheck 也不红。拦两类真事故形态 —— L1 调用方写死 `colorScheme="light"`(实测导航栏深色而页面体浅色，同一屏两套档案)、L2 漏传(当前 0 处，本门把它钉死在 0；刻意不把 213 处形参改成必填:无收益且会与并行会话在 213 个文件上对撞)。组件集**不靠手工清单**(清单过期正是上一道门漏判的原因)，改为扫 `packages/app/src` 导出组件名 + **import 解析**(含 alias / 默认导入 / 命名空间成员)，只有解析到 `@ihui/rn-app` 或包内相对路径才算共享主题组件 —— 端内同名自绘版(NavBar/TabBar/Carousel/UserInfoCard)因此不会误判；`{...spread}` 转发的渲染点单列「不确定」如实报数(不静默放过、也不误判为红)。实测真仓 `--staged` exit 0；应急跳过 `HUSKY_SKIP_THEME_PROP_WIRING=1`。

- **顶部状态栏避让单一源头**(97):`scripts/check-statusbar-single-source.mjs`(blocking,2026-09-24 立)—— 真机实测(Redmi 720x1640 / density 320)状态栏 inset = 68px = **34dp**;而 399 个 `*Screen.tsx` 里真读 inset 的只有 3 个:83 处在页头/根容器写死 `paddingTop: 48` 硬蒙量级(换机型即失配)、113 处完全没有顶距 ⇒ 页头与系统时钟/电量**叠字**。**唯一注入点 = `apps/mobile-rn/App.tsx` 的 `<SafeAreaView edges={['top']}>`**;状态栏是平台概念,所以 `packages/app`(跨端共享屏)一律不碰顶距,共享层 NavBar 那个默认 0、零调用方的 `statusBarHeight` prop 属第二个真相源,**已删**,不得再加回。三判据:**S1** 单点在位(该文件必须真有 `SafeAreaView` 且 `edges` 含 `'top'` —— 防"装好被摘线",机制不在而门仍报绿等于没有)/ **S2** 第二取值口(代码里出现 `StatusBar.currentHeight` 或 `statusBarHeight` 即红;前者 iOS 恒 `undefined` ⇒ 0)/ **S3** 魔法顶距(`container|page|root|wrapper|header|headerRow|screen|body` 的对象内 `paddingTop: 24..60` 字面量即红,**零容忍** —— 83 处已随本票清零,所以不需要基线)。口径同守门 77/83:全量判 HEAD blob、`--staged` 判索引 blob,不判滞后的共享工作树。行内豁免 `statusbar-exempt: <一句话原因>`(**M2:逐行生效且必须带原因**,一行标记救不了同文件另一处)。**M1 泄压阀**:含 JSX `<Modal` 的文件(Drawer/SideMenu/BottomPops/HandPlatePops 等)渲染在导航树外、不继承单点,其顶距命中只报数不判红(`--all` 逐条列出);注释里提 `<Modal` **不配豁免**。S2 的 `statusBarHeight` 只拦"布局取值"(`paddingTop/top/marginTop/= …`),共享层"调用方注入、默认 0"的 prop 声明不判红。扫描面为整包 `packages/app` + `apps/mobile-rn`(App.tsx 自身也在 S2/S3 射程内);全量经一次 `git grep` 预筛(627→87,38s→5s,模式串为判据字面量严格超集,筛不动退回全量)。取证 `--self-test` **40 例**(含 Modal 豁免、逐行豁免、单点被摘等 E2E 阳性对照)+ §22c 镜像测试 12 例(装车证明 + `--json` 可 parse + AGENTS/README 点名)。**两条写门过程中的教训值得留**:① 第一版"沉浸式豁免名单"是拿"出现 `position:'absolute'` + `top:0`"探的,8 个候选全是假阳(命中的是眼睛按钮和下拉框)—— 判据探不到就别硬编,默认值本身就是正确答案;② `readFileSync` 漏 import 被外层 `catch` 吞成"取不到内容",一个编码错误伪装成业务结论,故**工作树面不得套 try**。紧急跳过 `HUSKY_SKIP_STATUSBAR_SINGLE_SOURCE=1`。
- **暂存删除存续性对账**(99):`scripts/check-staged-deletions.mjs`(blocking,2026-09-24 立,Agent A 交付)—— 堵守门 65(整树删除拦截,只看规模 ≥1000/≥20%)与 `heal-worktree-tracked`(对暂存删除按设计"只报数、不代裁")之间的**空档**:删除规模不大、但仓库仍在引用它的暂存删除(`D `),任何人跑一次不带 pathspec 的普通 commit 就把已入库的功能与测试从版本树里删掉 —— 一次静默回滚,而 `git status`/diff/守门 65 全都看不见。判"误删"依据两条同时成立才判红:**E1 引用仍在**(索引 blob 里的相对 import / require / 动态 import / barrel `export *` / 别名路径含父目录名 / 整仓库路径字面量,覆盖代码+配置,`.md` 叙述与夹具/产物/记账 JSON/生成物不算)∧ **E2 无替代路径**(同名同后缀文件不在索引其他目录,有 = 已迁移放行)。只成立一条 → 不计红但**如实报数**(绝不静默成"看起来全绿");引用方(barrel)自己也一起删 = 正当删除形态,放行。护栏:名字 <4 字符 / 候选引用方 >2000 / 删除面 >400(那是 65 的地盘)一律 `undetermined` 只报数;豁免清单 `scripts/staged-deletions-allowlist.json`(坏 JSON 显式报错,不静默当空清单)。**不加 stagedTriggers —— 暂存删除可触及任意路径,任何提交都不得跳过**。口径:判索引 blob(与 70/77/83 同取向),全程只读。取证 `--self-test` 33 例(正反成对:barrel 仍 export 必红 / 引用方一起删必绿 / 歧义、注释、夹具、生成物各一反例)+ §22c 镜像测试 `scripts/tests/check-staged-deletions.test.mjs` 12 例(含"未注册时绿、注册后必须 blocking + skipEnv"的装车前置断言)。紧急跳过 `HUSKY_SKIP_STAGED_DELETIONS=1`。
- **合并新增文件存续性对账**(100):`scripts/check-merge-addition-loss.mjs`(blocking,2026-09-24 立)—— 堵的是**合并提交在文件面上的静默吞并**,与守门 71(只管 PROJECT_PLAN 登记行)、84(只管「暂存内容 == 祖先 blob」)都不重叠:2026-09-24 实测一枚写着「台账按 union 归并,双方每一行均存活」的合并 `9a0f7610e9`,把对侧**独有的 35 个新增路径整批抹掉**、连带 72 个文件回退成旧基线(相对共同祖先净 **−12014 行**),而它**不产生冲突、也不进 diff 报告** —— 「一侧新增、另一侧从未有过」的路径在「取某一侧整棵树」的合并里会直接消失。判据一条,且结构上排除了误伤正常合并:**A1** 路径 P ∈ 某父提交树 ∧ P ∉ 本次合并的共同基底(`git merge-base --all <parents>`)⇒ P 必须 ∈ 合并结果;「∉ 基底」正是把「对侧曾删除它」这一唯一正当解释排除掉。真要在合并里删除,必须**合并之后**单独 `git rm`(那时所有父都不含它,自动放过)。**口径是本门的生命线**:默认只判 `origin/main..HEAD` 里的合并 —— 已入库的历史事故若每轮重判,会让之后每一次提交恒红,唯一结局是人人绕过钩子、连带废掉全部守门;别人推来的合并由 `--all-new` 增量台账(`.workbuddy/merge-addition-loss-audited.json`)判到**一次**,`--limit N` 留给人工回看。取证 `--self-test` 9 例(真临时仓:正常合并必绿 / 整树回写必红并点名 / 未推必拦与已推必放用同一条判据 / 台账不重复判红)+ §22c 镜像测试 5 例。含两条**写门时自己踩到**的缺陷:① `rev-list --parents` 的第一个 token 是提交**自己**,`slice(2)` 会把合并误判成单父 ⇒ 整门恒绿;② 树缓存键必须先剥到 tree oid,按 `'HEAD'` 这种符号名缓存会在 ref 移动后读到旧树(自检第一轮就是把真事故判成了绿)。紧急跳过 `HUSKY_SKIP_MERGE_ADDITION_LOSS=1`。
- **本轮四道门的口径/落点收口(2026-09-24 立,一批内完成)**:① **守门 94 `check-error-code-coverage.mjs`** 此前**以 uncaught 异常 exit 1 冒充判据失败**——成因不在判据,而在它按磁盘读输入,而共享工作树的 `packages/i18n/messages/web/zh-CN.json` 副本滞后 HEAD(HEAD 有 `ai.pane.errorCatalog`)。现统一为 `makeFaceReader` 单一取材层(默认 **HEAD blob** / `--staged` 判索引 / `--worktree` 仅逃生舱,两个面旗同给判死),清单与内容同面同轮;取不到输入 ⇒ `UndeterminedError` ⇒ **exit 2 显式"无法判定"**,绝不冒烟成判据红也绝不记绿。② **守门 84 `check-stale-revert.mjs` 的 `MAX_FILES=300` 护栏原先一超限就整门跳过**,而共享工作区常年滞后数百文件 ⇒ 恰在最需要它的时候失明;现把**乘数级路径**(`guardian-runner.mjs` / 各道 `check-*.mjs` / `scripts/lib/` / `.husky/` 钩子 / 根 `package.json` / `pnpm-*.yaml` / `.github/workflows/` / 门的测试)从护栏里摘出来恒照判——它们被写回旧版不表现为"少一个功能",而是**一批守门静默失效**(当天 `guardian-runner.mjs` 落后 61 行、离一次 `git add` 只差一步)。③ **`union-converge` 的"对侧自己动过"原以文件为粒度**,两侧同改同一文件会整文件取对侧而丢本侧改动;现走 `git merge-file` 真三方,**冲突/二进制/非普通 mode 一律折进落地闸并点名文件**(不猜、不选边),另加第四条断言 `lostAddedLines` 保住两侧相对基底的新增行。④ **`git-guardian` 有了真实的到人出口**(见 §5e:它此前在"现役生产者清单"里挂着名字却零命中):判红经 `notify-deploy-failure.ts` 派发,按 alert 身份 + 内容指纹 4h 去重、未送达 30min 退避、**无每日总量封顶**,发信失败不改自愈与退出码。取证:门 94 镜像 6 例 / 门 84 镜像 4 例(含"顶过上限仍判红"与"真新编辑不判红"成对)/ union 自检 20 例 + 镜像 9 例 / 守护通知镜像 19 例;两门新入守门 80 的 HOT 清单(它们现在派生 git)。
- **合并吞并的修复出口 `scripts/union-converge.mjs`**(2026-09-24 立,守门 100 的另一半)—— 判据只说"红了",不修就等于把红留给下一个人(§12e 同型)。构造是**集合运算不是启发式**:合并树 = 本侧整棵树 ∪ 对侧「相对共同基底自己动过的路径」逐个取对侧版本 ∪ 活文档(PROJECT_PLAN / AGENTS / README)按「每行重数 = max(ours, theirs)」union;**对侧的删除不随合并传播**(确要删必须在合并之后显式 `git rm`,那才是 A1 认得的合法形态)。落地前自证 `丢本侧路径 = 0 ∧ 丢对侧路径 = 0 ∧ 三份文档未存活行 = 0`,落地后**用守门 100 本人的 A1 复核这枚合并**(用它的判据验它的产物);写盘一律临时索引 + commit-tree + CAS update-ref,从不 checkout、不碰共享工作区(§12d)。触发点两处:① `git-sync-converge` 的 merge-tree **冲突分支**先交它归并,收不住才退回"需人工"(人工最容易犯的错就是选边,而本次事故正是这么来的);② `git-guardian` 每轮的 `auditMergeAdditionLoss()` 调 `check-merge-addition-loss.mjs --all-new` 增量台账 —— 提交链上那道门按设计只判未推的合并,而 converge / 手工 commit-tree **不跑钩子**,别人造好推来的合并只能靠这一层看到一次(**只判不修**:自动重做合并风险远大于收益)。取证 `--self-test` 8 例 + §22c 镜像测试 4 例(含"选边式合并必须判失败"反向对照与两条挂点装车证明);手动:`node scripts/union-converge.mjs [--apply|--theirs <sha>|--self-test]`。

- **prod-bundle 影子副本对账**:`scripts/check-prod-bundle-shadow.mjs`(2026-09-24 立,挂在 `pnpm check:all` 与 `node scripts/check-prod-bundle-shadow.mjs`)—— §5e 说"落进 `deploy/prod-bundle/` 的运维脚本必须同时落一份入库源,否则等于写进盲区",但这句话过去**只有散文没有尺子**。本门登记「入库源 ↔ 生产实际执行的那份」逐字节必须等值,三条判据:S1 入库源必须真被 git 跟踪(不被跟踪就没有对账对象)、S2 运行副本必须真被忽略(否则登记表过期,同样判红)、S3 两侧 sha1 全等,不等即红并给出差异行数。任一侧取不到 ⇒ **exit 2 显式"无法判定"**,绝不记绿。首跑抓到并入库的是数据库备份那一对:`deploy/prod-bundle/pg-backup{,-scheduler}.ps1`(整目录被 `.gitignore` 忽略、全仓零入库源,而它每天 03:00 产出 94MB 生产库 dump)。本门**刻意没有改成转发壳**——把 runner 换成转发壳后重启服务,`pg_dump` 出现 3.5 分钟零输出挂死(旧 runner 同库同日 34 秒跑完),在解释清之前不动生产备份;因此两侧都是全文,靠本门钉住等值。自检 `--self-test`(5 例,含"入库源被摘线必红"与"缺一侧必计无法判定")。紧急跳过 `HUSKY_SKIP_PROD_BUNDLE_SHADOW=1`。
  - 同批登记两条实测事实,免得下一个人重新猜:(a) 备份脚本靠什么凭据认证**至今没答上** —— 脚本显式 `$dbPw = ""`、注释写"pg_hba 本地 trust 免密",而实测机器级与用户级都没有 `PGPASSWORD`、`systemprofile` 与 Administrator 下都没有 `pgpass.conf`、`postgres` 对错误口令回 `password authentication failed`、空口令被拒;任何**带控制台上下文**的 `pg_dump`/`psql` 会阻塞在口令提示(`stdin` 重定向也拦不住,libpq 走 ReadConsoleW)。兜底不是没有:`apps/api/src/services/alert-check-service.ts` 巡检两条备份链目录,判"备份缺失 / 0 字节"并走邮件。(b) 调度器 `Run-Backup` 用 `& script.ps1` 调子脚本,而 PowerShell 的 `&` **不因被调脚本 `exit 1` 抛异常** ⇒ `catch` 永不触发;实测一次真实失败(日志先打 `[ERROR] 备份失败`)之后仍打"备份完成"。修它要改生产 runner 并重启服务(该服务"启动即备份"),与凭据问题一并处理。

### 守门手动触发 / 紧急跳过抽查

- **手动触发全量守门**:`node scripts/guardian-runner.mjs --staged`(pre-commit 模式,传给所有脚本);不带 `--staged` 为全量扫描。
- **执行语义(2026-09-22 由 fail-fast 改为跑完再汇总)**:任一 blocking 门失败不再中断本轮 —— 全部 96 项跑完后末尾列「失败门清单 + 单独复现命令」再 exit(1)。原 fail-fast 会遮蔽其后所有门(实测同一工作区真实存在 5 道红,旧行为只显示 1 道,且让人误判"新注册的门没生效")。**两条不变量**:① 子门 `exit 75`(中断)仍**立即**以 75 向上传播,不收敛成 1(§5b ⑦ 的 push guard 重试链依赖它);② 逃生舱 `GUARDIAN_STOP_ON_FIRST=1` 完整恢复旧的"首个失败立即 exit(1)"。绿的路径耗时不变(原本就要跑完),仅失败轮次变长。
- **commit 污染防护(scope 一致性)**:`.husky/commit-msg` 的 `check-commit-scope-consistency.mjs`(AGENTS.md §16 配套);紧急跳过 `HUSKY_SKIP_SCOPE_CHECK=1 git commit ...`。
- **commit 丢失防护**:guardian 第 30a 项 `check-commit-loss-guard.mjs`(AGENTS.md §22 配套);紧急跳过 `HUSKY_SKIP_COMMIT_LOSS_CHECK=1 git commit ...`。

> post-commit 钩子:`git-push-guard.mjs` 自动 push + 验证 local == remote(见 §20)。

---

## 26. C 盘防护强制规则(强制)

### 触发背景(2026-07-27 立,真实事故)

C 盘 120 GB 频繁告急,根因排查发现:

- **第三方 AI IDE 自身缓存 12.88 GB**(国内版 CN 7.68 + 旧版 3.46 + 国内旧版 1.74)
- **Chrome OptGuideOnDeviceModel 4 GB**(Chrome 内置 AI 模型,用户不用)
- **Local\Temp 累积 1.6 GB**(第三方 AI IDE 旧版安装包 + pip 安装临时)
- **项目历史违规写入 `C:\temp\ihui-*` 0.33 GB**

已通过环境变量迁移 + 符号链接 + 自动维护计划任务根治。

### 开发工具缓存路径强制规则(强制)

**所有开发工具的全局缓存/存储/临时目录必须指向 D 盘**(2026-09-23 全量实测校正 —— 下表旧版写的
`D:\caches\*` 是**死路径**,本机不存在 `D:\caches`,且 `CARGO_HOME`/`RUSTUP_HOME`/`OLLAMA_MODELS`
当时**根本没设**,即"文档说已迁、实际还在 C 盘";真实外置根是 `D:\DevEnv\`)。

| 工具                | 环境变量 / 配置                             | 实测路径                                                                            |
| ------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------- |
| Temp/TMP            | `TEMP` / `TMP` / `TMPDIR`                   | `D:\DevEnv\Temp`(2026-09-23 才真正写入 HKCU)                                        |
| pnpm                | `PNPM_HOME` + `pnpm store path`             | `D:\DevEnv\tools\pnpm`,store=`...\pnpm\store\v11`                                   |
| npm                 | `npm config`                                | `D:\DevEnv\cache\npm`                                                               |
| pip                 | `PIP_CACHE_DIR`                             | `D:\DevEnv\cache\pip`                                                               |
| uv                  | `UV_CACHE_DIR`                              | `D:\DevEnv\cache\uv`                                                                |
| Cargo               | junction(`%USERPROFILE%\.cargo`)            | `D:\DevEnv\cache\userhome\.cargo`                                                   |
| Rustup              | junction(`%USERPROFILE%\.rustup`)           | `D:\DevEnv\cache\userhome\.rustup`                                                  |
| Maven/.m2           | junction(`%USERPROFILE%\.m2`)               | `D:\DevEnv\cache\userhome\.m2`                                                      |
| Codex/.cache/.codex | junction                                    | `D:\DevEnv\cache\userhome\{.cache,.codex,.codex-session-delete}`                    |
| Trae 全家           | junction(`.trae`/`.trae-cn`/`.trae-aicc`)   | `D:\DevEnv\cache\userhome\`                                                         |
| DeepSeek CLI        | junction(`%USERPROFILE%\.deepseek`)         | `D:\DevEnv\cache\userhome\.deepseek`                                                |
| Ollama(含服务)      | junction(`%USERPROFILE%\.ollama`)           | `D:\DevEnv\cache\ollama`(服务 `OLLAMA_MODELS` 路径经 junction 解析)                 |
| IHUI CLI 状态       | junction(`%USERPROFILE%\.ihui`)             | `D:\DevEnv\cache\userhome\.ihui`                                                    |
| 桌面端 Local 态     | junction(`%LOCALAPPDATA%\com.ihui.desktop`) | `D:\DevEnv\cache\userhome\appdata-local-com.ihui.desktop`(515 文件/39.25MB)         |
| 桌面端 Roaming 态   | junction(`%APPDATA%\com.ihui.desktop`)      | `D:\DevEnv\cache\userhome\appdata-roaming-com.ihui.desktop`(auth/tray/window-state) |
| **服务身份 TEMP**   | **没有 env 可改**(HKCU 迁移对它无效)        | 必须显式选目录 + 用完删除,详见下方"junction 管不了身份"                             |
| Go                  | `GOPATH` / `GOMODCACHE` / `GOCACHE`         | `D:\DevEnv\cache\go{,\pkg\mod}` / `...\go-build`                                    |
| Playwright          | `PLAYWRIGHT_BROWSERS_PATH`                  | `D:\DevEnv\cache\playwright`                                                        |

| 工具                | 环境变量 / 配置                           | 实测路径                                                            |
| ------------------- | ----------------------------------------- | ------------------------------------------------------------------- |
| Temp/TMP            | `TEMP` / `TMP` / `TMPDIR`                 | `D:\DevEnv\Temp`(2026-09-23 才真正写入 HKCU)                        |
| pnpm                | `PNPM_HOME` + `pnpm store path`           | `D:\DevEnv\tools\pnpm`,store=`...\pnpm\store\v11`                   |
| npm                 | `npm config`                              | `D:\DevEnv\cache\npm`                                               |
| pip                 | `PIP_CACHE_DIR`                           | `D:\DevEnv\cache\pip`                                               |
| uv                  | `UV_CACHE_DIR`                            | `D:\DevEnv\cache\uv`                                                |
| Cargo               | junction(`%USERPROFILE%\.cargo`)          | `D:\DevEnv\cache\userhome\.cargo`                                   |
| Rustup              | junction(`%USERPROFILE%\.rustup`)         | `D:\DevEnv\cache\userhome\.rustup`                                  |
| Maven/.m2           | junction(`%USERPROFILE%\.m2`)             | `D:\DevEnv\cache\userhome\.m2`                                      |
| Codex/.cache/.codex | junction                                  | `D:\DevEnv\cache\userhome\{.cache,.codex,.codex-session-delete}`    |
| Trae 全家           | junction(`.trae`/`.trae-cn`/`.trae-aicc`) | `D:\DevEnv\cache\userhome\`                                         |
| DeepSeek CLI        | junction(`%USERPROFILE%\.deepseek`)       | `D:\DevEnv\cache\userhome\.deepseek`                                |
| Ollama(含服务)      | junction(`%USERPROFILE%\.ollama`)         | `D:\DevEnv\cache\ollama`(服务 `OLLAMA_MODELS` 路径经 junction 解析) |
| IHUI CLI 状态       | junction(`%USERPROFILE%\.ihui`)           | `D:\DevEnv\cache\userhome\.ihui`                                    |
| Go                  | `GOPATH` / `GOMODCACHE` / `GOCACHE`       | `D:\DevEnv\cache\go{,\pkg\mod}` / `...\go-build`                    |
| Playwright          | `PLAYWRIGHT_BROWSERS_PATH`                | `D:\DevEnv\cache\playwright`                                        |

**改道机制定为 junction,不 env 优先**:家目录工具态一律 `robocopy <src> <dst> /E /MOVE` →
`mklink /J <旧路径> <新路径>`。理由:工具态常有**硬编码**读取方(如 `~/.cargo\bin` 在
`HKCU\Environment\Path` 首位、守门脚本按 `%USERPROFILE%` 拼路径),junction 让旧路径继续可解析,
因此**不需要也不允许**去改 `Path` 或逐个改码;改环境变量反而会造出"双根分裂"。
校验方法:`rustup show home`、`reg query HKCU\Environment`、`(Get-Item ~\.cargo).Attributes -match ReparsePoint`。
新增工具的缓存落点一律走 `D:\DevEnv\cache\userhome\<名称>` + junction,不得再在家目录留实体目录。
**盘根"写歪项"一律封口改道,不许只删(2026-09-24 立)**:第三方程序(剪映、微信输入法、MSYS 侧工具、
各类安装器)用**相对路径**写自己的状态,而进程工作目录恰好是 `C:\` ⇒ `common_attachment`、
`persistent_data`、`tmp`、`tools` 直接长在盘根。**这类残骸删掉必然复发,因为成因改不了(闭源)**。
唯一正解是把那个名字换成 junction 指向 §15b 落点 —— 程序按原路径读写不变,内容落到 D 盘。
清单与执行的唯一入口是 `scripts/seal-c-root-stray.mjs`(`--check` 零副作用 / `--apply` 幂等,
**换机或重装后需重跑一次**),守门与 `c-drive-auto-maintain.ps1` 第 4 段都 import 同一份清单,
**禁止在别处再抄一份名字**。发现某新残骸也属这一型:加进 `SEALED_DIRS`(必须带 `owner` + `evidence`
两项取证)而不是写进删除名单。
**封口必须同时挂进 `git-guardian` 的自愈轮(2026-09-24 实测)**:`--apply` 做的 junction **会被重启清掉**
(当天 4 个里死 3 个,D 侧内容完好)。只靠每日 03:00 体检 = 最长 23 小时空窗,够第三方自建真目录回到
复发原点。正解是第三层 `healRootSeal()` 挂在 `git-guardian` 的 `!CHECK_ONLY` 分支(与
`healWorktreeTracked` 同位,2 分钟一轮),`seal --check` 判红才 `--apply`;挂点写成 CHECK_ONLY
路径等于永不执行(工作区自愈层踩过同一坑),三条装车证明由 `scripts/tests/seal-c-root-stray.test.mjs` 钉死。
**⚠️ junction 的头号危险是"被递归穿透"(同日实测)**:PowerShell 7 的 `Get-ChildItem -Recurse`
**会穿过 junction** 枚举到目标里的文件,于是"按名字删 `C:\tmp\ihui-*`"会顺着链接清空 D 盘真实目标,
把改道机制变成自毁机制。三条要求:① 任何递归删除/枚举前必须判 `ReparsePoint`,重解析点只能
`[System.IO.Directory]::Delete($path, $false)` 断链;② 量体积的工具遇 junction **不得跟随**
(否则把 D 盘的量报成 C 盘的债);③ Node 侧 `lstatSync(p).isSymbolicLink()` 对 junction 报 `true`,
`rmSync(link)` 只断链不穿透(均已实测)。判据由 `seal-c-root-stray` 与 C 盘污染守门的镜像测试钉死。
**要藏住 junction 的名字,只能用 PowerShell 提供器 + 父目录枚举复核(2026-09-24 实测)**:用户选择
"设隐藏,保留改道"后,`attrib +h <junction>` 是**陷阱** —— 它把 Hidden 设到**目标**那一侧,链接本体
纹丝不动,而 `attrib` 回显时又顺着链接读目标,于是打印出 `H` 让调用者以为成功了(本仓第一版就这样
"隐藏了 4 次",C 盘那 4 个名字照旧可见,反倒把 D 盘的 4 个数据目录藏掉了)。正确做法:
`(Get-Item -LiteralPath <链接> -Force).Attributes = $i.Attributes -bor [System.IO.FileAttributes]::Hidden`,
并且**唯一可信的 oracle 是父目录枚举** `Get-ChildItem <父目录> -Force`(那正是 Explorer 读的那份目录项
属性)—— 设完必须自己回读,不许把"没抛错"当成成功。隐藏只影响浏览,穿透读写与
`isSymbolicLink()` 判定均不受影响(已实测)。策略固化在 `seal-c-root-stray.mjs` 的 `setLinkHidden`,
每次 `--apply` 都确保在位(封口被重建也不会露回来),镜像测试断言源码里**不得再出现 `attrib`**。
**改页面文件必须留"待重启生效"哨兵(2026-09-24 立)**:本机 C/D 两个 pagefile 都是**手设固定值**
(C 32768MB / D 98304MB)而非系统管理。要缩 C 的占用,改的是
`HKLM\...\Session Manager\Memory Management\PagingFiles`(用 `Set-CimInstance Win32_PageFileSetting`
写入,回读该注册表值才算落盘)—— 但**内存管理器运行期锁住 pagefile.sys,磁盘上的旧大小只有重启才收缩**,
而本机是生产机(20+ 个 IHUI-* 服务在跑),重启时机归用户。所以任何这类改动都必须同时留一条
会自我清空的哨兵:比对「配置上限 vs WMI `Win32_PageFileUsage.AllocatedBaseSize`」,落差 >512MB 且 >25%
就报「待重启生效」,缩到位后不再报。**量这个大小有三连坑,都不报错、只给假绿**:`fs.statSync` 对
`pagefile.sys` 必报 `EINVAL`(打不开句柄);`cmd /c for %A in (...) do %~zA` 会被 Node 的加引号 +
cmd 剥首尾引号的双层规则打掉;属性名写成 MSDN 文档的 `AllocBaseSize`(本机真名是
**`AllocatedBaseSize`**)会被 PowerShell 静默渲染成空串。三条已由守门 `--self-test` 与镜像测试钉死,
且"一条都没量到"必须打印**未判定**、绝不记为通过。
**⚠️ junction 只管"路径",管不了"身份"(2026-09-24 实测的第四类真因)**:同一个 `$env:TEMP` /
`os.tmpdir()` 在**不同身份下指向不同目录** —— HKCU 把交互账户 TEMP 迁到 `D:\DevEnv\Temp` 之后,
nssm 服务(IHUI-API / IHUI-DEPLOYLOOP 以 LocalSystem 运行)拿到的仍是 `C:\Windows\Temp`。
**任何"改成 `$env:TEMP`"式修复对它只是往 C 盘内部挪坑。**所以三条硬要求:
① 服务/守护类脚本若要落临时物,必须**显式指定目录**,不能信任 `$env:TEMP`;
② **用完必须删** —— 实测反例:`deploy/win/ihui-deploy.ps1` 把构建 stdout/stderr 重定向到
`$env:TEMP` 的 `ihui-next-build-<PID>-try<N>-{out,err}.log`,Tail 进 deploy-loop.log 之后**从不删除**
⇒ 部署环每 30 分钟漏 2 个,`C:\Windows\Temp` 攒到 **526 项 / 6.86MB**(同文件里的
`ihui-align-$PID.log` 反而有删,所以泄漏面精确到构建这一处;2026-09-24 已改为用完即删,
并现场观察到一轮构建结束后该 2 个文件自动消失);
③ **守门必须扫"服务身份的 TEMP"** —— 守门 `check-c-drive-pollution.mjs`(编号同日漂移过
同日三次撞号)此前只扫 `tmpdir()`,即"看门人自己的 TEMP",于是它一路报"本项目产物 0 项"
而真凶全在 `C:\Windows\Temp`。现 `tempScanDirs()` 显式并入 `SystemRoot\Temp` 并由 `--self-test`
钉死(扫描面缩回去即红)。**同批修掉这条门自身的两处假绿灯**:同一目录因大小写被计两次
(`C:\windows\Temp` 与 `C:\Windows\Temp` ⇒ 实测 1056 项 vs 真实 526,按小写键去重)、文件一律
记 `sizeMB: 0`(只有目录量体积 ⇒ 526 个小文件把 6.86MB 报成"合计约 0 MB")。
它的修复提示原写 `pnpm c-drive:clean-ours`,而根 package.json **从来没有这个脚本**
(实测取该键得 undefined)⇒ 门给出的是跑不通的出路,已改为真实入口并要求先 `-DryRun`。
**该方法唯一的真实危险**:`robocopy` 非零返回码(如 `.codex` 持锁 rc=9)会让内容已搬走但**不建 junction**,
路径直接消失 —— 必须回读"旧路径是否存在 + 新路径文件数/字节"再补 junction(本次即手工补建后凭据零丢失)。
**校验必须逐文件比对相对路径 + 字节数**(`robocopy /E` 不带 `/MOVE` 先做镜像副本),通过后才删源。

**禁止 agent 在代码或脚本中硬编码 C 盘路径**作为写入目标:

- ❌ `C:\temp\*` / `C:\Users\荣耀\AppData\Local\Temp\*`(用 `os.tmpdir()` / `$env:TEMP` 替代,会自动走 D 盘)
- ❌ `C:\Users\荣耀\AppData\Local\*\cache`(用工具自带配置或环境变量)
- ❌ `C:\Users\荣耀\AppData\Roaming\AICodingIDE*\*`(第三方 AI IDE 自身管理,agent 不触碰)

**唯一例外**:系统日志(`debug.log` / `next-server.log`)可走 `$env:TEMP`(已指向 D 盘)。

### 自动维护计划任务

| `IHUI C-Drive AutoMaintain` | 每天 3am | `wscript.exe scripts/c-drive-maintain-hidden.vbs` → `pwsh -File c-drive-auto-maintain.ps1` | **六段**:①Chrome 缓存 ②Temp 旧目录 ③本项目 C 盘产物 ④盘根封口体检 ⑤系统自产残骸(卡死打印队列/内核转储/孤儿浏览器构建) ⑥回潮源封禁(Chrome 政策,写前先验政策名真在 chrome.dll 里) |

| 任务名                      | 触发     | 实际执行体                                                                                 | 功能                                  |
| --------------------------- | -------- | ------------------------------------------------------------------------------------------ | ------------------------------------- |
| `IHUI C-Drive AutoMaintain` | 每天 3am | `wscript.exe scripts/c-drive-maintain-hidden.vbs` → `pwsh -File c-drive-auto-maintain.ps1` | 清理 Chrome/Temp 缓存 + 报告 C 盘状态 |

**⚠️ 注册名含空格,不是连字符**(`schtasks /query /fo CSV` 实读回 = `\IHUI C-Drive AutoMaintain`)。按文档里旧写的 `IHUI-C-Drive-AutoMaintain` 去查,`schtasks` 回「系统找不到指定的文件」—— 2026-09-23 我就是因此**误判成"任务从未注册"**,而它当时确实还没注册(23:55 才注册),但**名字陷阱会让人在任何一天都得出同样的错结论**。Git Bash 下查还要加 `MSYS_NO_PATHCONV=1`,否则 `/query` 被改写成路径。判存在与否只认 `schtasks /query /fo CSV | grep -i c-drive` 这种全量列名法,不要拿文档里的名字去点名查。

**2026-09-23 曾实测:该任务在本机并不存在**(当时代号未注册,`D:\DevEnv\logs\c-drive-maintain.log` 也从未生成)。当时的表是**设计意图**而非现状,曾据此以为"每天在清"⇒ 实际零执行,这是 C 盘能攒下 13.2GB `.next` 构建备份的直接原因之一。

**同日 23:59 经用户授权后已真正注册**(状态=现状,不再是设计意图):

> **该"现状"于 2026-09-24 再次失真,已三路取证终判为「当前不存在」**(本节下方表格里的"每天 3am"因此也是设计意图而非实况):
> ① `schtasks /query /fo CSV | grep -i c-drive` **零命中**(这正是本节规定的权威查法);
> ② `Get-ScheduledTask | Where TaskName -match 'C-Drive|Maintain'` 返回**空**;
> ③ 递归枚举 `C:\Windows\System32\Tasks\*.XML`,**没有**任何 C-Drive/Maintain 定义文件 —— 而同目录其余 **14 个 `IHUI*` 任务全部在位可列**
> ⇒ 排除"查法失效"这一假阴性解释,任务确实不在了。
> `D:\DevEnv\logs\c-drive-maintain.log` 今天(09-24 10:59)那条记录是**人工 `-DryRun` 预演**,不是 03:00 自动执行(全文 `[DRY]` 无 `[DEL]`,合计释放 0 MB),
> 所以"每天在清"在今天并没有发生。**注册动作 = 影响全机的每日自动删除,仍须用户授权,agent 不得自行 `schtasks /create` 恢复**;
> 上一条"回读 `schtasks /Query /XML` 实证 `LogonType=S4U`"当时为真,但那份定义现已不在 —— 名字陷阱的解释**不成立**(权威全量列表法连空格名一起扫,零命中)。
>
> **同日 17:1x 已按用户授权重新注册,并留配对证明(本条从"设计意图"重新变回现状)**:
> ① **注册前实跑预检**(§26 硬约束):在 `scripts/` 放一份与 `c-drive-maintain-hidden.vbs` 同体、仅把参数换成
> `-DryRun` 的副本,用 `cscript //nologo` 真跑一遍 —— `cscript` 退出 0、日志新增 76 行、
> 首行 `DRY RUN(全脚本不删任何东西)`、**`[DEL]` 计数 = 0**(证明拉起链通且零删除),预检副本当场删除;
> ② 注册命令 `schtasks /create /tn "IHUI C-Drive AutoMaintain" /tr "wscript.exe \"<工作树实测绝对路径>\c-drive-maintain-hidden.vbs\"" /sc daily /st 03:00 /f`,
> 再 `cscript //nologo scripts/task-set-s4u.vbs "IHUI C-Drive AutoMaintain"` 升 S4U(`/RU <user> /NP` 会**弹密码提示**、非交互跑不通,
> 而本机 pwsh 无 ScheduledTasks 模块 ⇒ 只有 Schedule.Service COM 这条活路,该工具头注已记);
> ③ **XML 回读独立取证**:`<LogonType>S4U</LogonType>`、`<StartBoundary>…T03:00:00</StartBoundary>`、
> `<Command>wscript.exe</Command>` ⇒ 三项齐备才算注册落地(不得把 `schtasks` 打印的"成功"当成功);
> ④ 权威存在法同一时刻由 0 命中翻成 **1 命中**(`\IHUI C-Drive AutoMaintain`)。
> **盘符按当次实测取**:本机工作树是 `G:\IHUI-AI`,注册的 `/tr` 就指 `G:\…`,不得照抄本节正文里的 `D:/…`(§5b/§15b 已各记过一次盘符失真)。
>
> **同批根掉了一句会再骗一次的东西**:守门 92 `check-c-drive-pollution.mjs` 原先在结论后**无条件打印**
> "计划任务 IHUI C-Drive AutoMaintain 每天 03:00 已注册(S4U,wscript 包装)",而脚本内**一次 `schtasks` 都没调过**
> —— 一道只读污染门正在替一个当时并不存在的防护背书。现改为**实测三态判据** `registered / unregistered / undetermined`
> (查法仍是全量 `/FO CSV /NH` + 名字不区分大小写匹配,**禁止**点名查),非 win32 / ENOENT / 超时 / 空输出 /
> 输出形态解不出任务路径一律 `未判定` 并写原因,**绝不计为通过**;本门仍只读、未注册不改退出码(warn-only 语义由断言钉死)。
> 配对证明:注册前该判据实测 `unregistered`(346 项零命中),注册后 `registered`(347 项命中 1 条),两次退出码均 0。

- **动作链按本节下方「计划任务禁止直接执行控制台程序」硬约束走**:`wscript.exe` → 纯 ASCII 的 `scripts/c-drive-maintain-hidden.vbs` → `pwsh -NoProfile -ExecutionPolicy Bypass -File …ps1`。注册前用 `cscript //nologo` 实跑过一份**只带 `-DryRun` 的同体副本**做语法+拉起链证明(实测写出 `[WARN] … DRY RUN(全脚本不删任何东西)`),因此注册过程零删除。
- **登录类型已升 S4U**(与凭据巡检同一套 `scripts/task-set-s4u.vbs`),否则 3am 无人登录时不会跑。回读 `schtasks /Query /XML` 实证:`LogonType=S4U`、`Command=wscript.exe`、`StartBoundary=03:00`、下次运行 `2026-09-24 03:00`。
- **它的删除面是"按名字"的,不是"按目录整片"**(2026-09-24 把行为收拢到与这句话一致,之前并非如此):盘根只认 `IHUI-*` / `.empty-tmp*` / `.pnpm-store`;`C:\tmp` 与 `C:\temp` 内只删 `ihui-*` / `IHUI-*` / `next-backup-*` / `probe-*` / `wb-ext-debug.log`;`%LOCALAPPDATA%\Temp` **默认也只删这四类名字**,原来是对"所有 mtime>3 天的目录"整片删(09-23 一次误跑就是因此清掉约 29.8MB 无主旧目录)—— 宽口径整片清扫现在必须显式设 `IHUI_TEMP_WIDE_SWEEP=1` 才恢复,未开启时把"被跳过多少个、合计多少 MB"如实打印,不静默。唯一剩下的非按名字删除面是 `C:\Windows\Temp` 里 mtime>3 天的**文件**(系统临时文件,保留原语义),日志里以 `[NOTE]` + 逐条留痕明示。
- **Chrome 缓存那 6 条路径是死路径,但故意不"修好"**:它们硬编码 `C:\Users\荣耀\AppData\Local\...`,本机用户是 `Administrator` ⇒ 天天空转。改成指向真实用户配置等于**突然开始删一个在用浏览器的缓存**,风险远大于收益;现在改为检测到路径不存在就显式 `[WARN]`(6/6 不存在 + 当前用户名),不再静默。要清这台机器的 Chrome 缓存,请人工确认后再动。
- **全脚本的删除出口只有 `ForceDelete` 一个**,且每次过 `Test-Protected`(`secrets` / `密钥` / `credentials` / `backups` / `BaiduSyncdisk` 等整目录不碰);`-DryRun` 拦在该出口上,预演模式**一条 `[DEL]` 都不会出现**(实测 179 行日志 `[DEL]` 计数 = 0)。
- **TEMP 漂移(2026-09-24 已根治写入侧,残余只是"活进程不刷新")**:HKCU `TEMP` 已指 `D:\DevEnv\Temp`,但活着的宿主/终端进程仍持 `C:\Users\Administrator\AppData\Local\Temp`(实测 `node -p os.tmpdir()` 即旧值),新开终端/重启宿主后自愈。**更严重的那一半是"服务身份根本不读 HKCU"** —— LocalSystem 的 TEMP 是 `C:\Windows\Temp`,已按上文③把守门扫描面补齐,并把 `deploy/win/ihui-deploy.ps1` 的构建重定向日志改为**用完即删**(实测存量 526 项 / 6.86MB 清完、下一轮构建结束后 2 个文件自行消失)。判据与污染可见性由守门 `check-c-drive-pollution.mjs`(只读、永不删;编号见 runner)承担。
- **家目录实体目录再少两处**(2026-09-24):桌面端 `%LOCALAPPDATA%\com.ihui.desktop`(515 文件/39.25MB)与 `%APPDATA%\com.ihui.desktop`(auth/tray/window-state)已按本节 junction 机制改道到 `D:\DevEnv\cache\userhome\appdata-{local,roaming}-com.ihui.desktop`;流程是"镜像复制 ⇒ 逐文件(相对路径+字节)校验 ⇒ 源改名为 `.pre-junction-<ts>` ⇒ `mklink /J` ⇒ 经 junction 回读数量一致 ⇒ 才删源",任一步不符即改名回退。改道前已确认桌面端不在运行(该目录最后写入 09-06,机上 13 个 `msedgewebview2.exe` 全属他应用)。同批删除 `AppData\Local\智汇AI` 与 `AppData\Local\ihui-node-hooks` 两个**空**孤儿目录(后者说明机器级 windowsHide 钩子当前未装:`HKCU\Environment\NODE_OPTIONS` 实测未设)。
- **仍未闭环的一条**:TEMP 漂移 —— HKCU `TEMP` 已指 `D:\DevEnv\Temp`,但活着的宿主/终端进程仍持 `C:\Users\Administrator\AppData\Local\Temp`(实测 `node -p os.tmpdir()` 即旧值),所以走 `os.tmpdir()` 的脚本会继续落 C 盘;新开终端/重启宿主后自愈。判据与污染可见性由守门 **93** `check-c-drive-pollution.mjs`(只读、永不删)承担。

**同日修 `c-drive-auto-maintain.ps1` 的三处失效**(全部实测取证):

1. 原"清理本项目产物"段扫的是 `C:\temp\ihui-*`,而我们实际写到 `C:\tmp\*`(构建备份)、`%LOCALAPPDATA%\Temp\ihui-*`(测试夹具)、以及盘根本身 —— **扫错目录 = 每天跑也零效果**。现覆盖这三处 + 盘根 `IHUI-*`/`.empty-tmp*`/`.pnpm-store`。
2. `ForceDelete` 只有 `DeleteDirectory` 分支,对**文件**路径必然抛后被 catch 吞掉 ⇒ 静默"清理成功但什么都没删"(盘根 `IHUI-probe-*.ps1` 就属于这一类)。现按 Leaf/Directory 分流。
3. 新增 `-DryRun`,**拦在 `ForceDelete` 这个唯一删除出口上**而非某一段里。教训实测:最初只把 DryRun 写在第三段,预演模式下第一段(Chrome 缓存)和第二段(Temp >3 天目录)**照样被真删了**(DeletePermanently 不进回收站);当时释放约 29.8MB,均为 `Temp` 下 mtime>3 天的目录,项目文件/备份/凭据全在 D 盘未受影响。现每段逐条留痕(`[DEL]`/`[DRY]`),不再静默删除。

**手动触发**:`pwsh -File scripts/c-drive-auto-maintain.ps1`
**查看日志**:`D:\DevEnv\logs\c-drive-maintain.log`(旧文档写 `D:\caches\...`,该目录本机不存在,日志从未写出)
**查看任务状态**:`Get-ScheduledTask -TaskName "IHUI-*"` / `schtasks /Query /TN "IHUI-C-Drive-AutoMaintain"`

### 2026-09-23 已办与剩余阻碍

**已办**:① **10.3GB 游离备份收口进 `D:\DevEnv\backups\`**(根目录散落的 `IHUI-AI-backup-*.tar` 8.3G、
`IHUI-AI-backup-20260911.git` 727M、`IHUI-AI_workbak_20260910` 537M、两个根 `.sql`、`IHUI AI Desktop` 94M
等,同卷 `mv` + 每步回读"源已无 + 目标体积一致");② **C 盘 20G → 31G 可用**;③ **12 项家目录改道**
(`.rustup .cargo .m2 .cache .codex .trae .trae-cn .trae-aicc .deepseek .codex-session-delete .ihui` →
`D:\DevEnv\cache\userhome\`,`.ollama` → `D:\DevEnv\cache\ollama`);④ **Ollama 2.9GB** 先镜像复制 +
逐文件(路径+字节)校验 + sha256 寻址核对,13/13 一致后才 `nssm stop` → 删 C 原件 → `mklink /J` →
`nssm start`;验收=服务 RUNNING **且真跑一次 `qwen2.5-coder:1.5b` 生成成功**(1.53s,日志无 error);
顺带修掉该服务把 `TEMP`/`TMP` 钉死在 `C:\Users\...\AppData\Local\Temp` 的配置(nssm LocalSystem
自带环境块,HKCU 的 TEMP 迁移对它无效,它一直在往 C 写)。原环境块备份在
`D:\DevEnv\backups\env\ollama-service-env-original.txt`。

**剩余客观阻碍(不是遗漏)**:`~/.workbuddy` 3.1GB 内含 `binaries\PortableGit`
(被 `scripts/lib/gitdir.mjs:36-37` 当 git 二进制首选解析)不得搬,其 `logs`/`traces` 约 2GB 属该 IDE
自管;`.qoder-cn`/`.qoder` 是本会话宿主状态,改道即丢记忆。`scripts/kill-git-selector-hidden.vbs`
已改为随自身目录定位目标脚本,但其包装的 `kill-git-selector.ps1` **在仓库里并不存在**(死代码);
`scripts/release-desktop-local.mjs:67,74` 需要 `%USERPROFILE%\.tauri\ihui-updater.key`,本机无 `.tauri`
→ 桌面端发布在此机必 `exit 1`(需发布机或补生成密钥)。

### 守门(已实现,guardian-runner 第 45 项)

- `scripts/check-c-drive-paths.mjs`(guardian-runner 第 45 项,warn-only,2026-08-13 立):扫描 staged 文件中硬编码的 C 盘写入路径(`C:\temp\` / `C:\Users\*\AppData\Local\Temp\` 等,排除 `os.tmpdir()` / `$env:TEMP` / 注释 / 文档)。
- **`scripts/check-c-drive-pollution.mjs`(guardian-runner 已注册项,warn-only,2026-09-23 立。**编号同日重排 5 次(85→90→91→92→93→96),文档一律不写死,以 runner 里该 `script:` 所在条目的 `id` 为准**)—— 补上第 45 项看不见的那一半**。成因:第 45 项只扫**源码字面量**,而 C 盘残骸恰恰是从 `os.tmpdir()` / `$env:TEMP` 这类"源码里没写 C"的路径流出去的;`check-parent-pollution`(只扫项目父目录 `D:\`)和 `check-root-dir-clean`(只扫项目根)同样不看 C 盘文件系统 —— 全链 90+ 道门没有一道实地扫过 C,于是 13.2GB `.next` 备份和单日 45 个 git 夹具可以在全量审计恒绿的情况下一直堆在 C 盘。本门实地扫 `C:\` 根 + `C:\tmp` + `C:\temp` + 活 TEMP,按名字白名单只认**本项目产物**(他人条目进"未识别清单",只登记不定性、不清理);并单独判 **TEMP 漂移**(注册表 `HKCU\Environment\TEMP` 已指 `D:\DevEnv\Temp` 而活进程仍持 `C:\Users\...\AppData\Local\Temp`)—— 这就是"改了指针但残骸天天还在长"的机制,新建终端/重启宿主后自愈。
- **`scripts/check-c-drive-pollution.mjs`(guardian-runner 第 93 项,warn-only,2026-09-23 立)—— 补上第 45 项看不见的那一半**。成因:第 45 项只扫**源码字面量**,而 C 盘残骸恰恰是从 `os.tmpdir()` / `$env:TEMP` 这类"源码里没写 C"的路径流出去的;`check-parent-pollution`(只扫项目父目录 `D:\`)和 `check-root-dir-clean`(只扫项目根)同样不看 C 盘文件系统 —— 全链 90+ 道门没有一道实地扫过 C,于是 13.2GB `.next` 备份和单日 45 个 git 夹具可以在全量审计恒绿的情况下一直堆在 C 盘。本门实地扫 `C:\` 根 + `C:\tmp` + `C:\temp` + 活 TEMP,按名字白名单只认**本项目产物**(他人条目进"未识别清单",只登记不定性、不清理);并单独判 **TEMP 漂移**(注册表 `HKCU\Environment\TEMP` 已指 `D:\DevEnv\Temp` 而活进程仍持 `C:\Users\...\AppData\Local\Temp`)—— 这就是"改了指针但残骸天天还在长"的机制,新建终端/重启宿主后自愈。
  - **定级 warn 而非 blocking**:盘根多数条目不属本仓,拦提交只会逼人 `--no-verify`,连带废掉其余守门(与守门 77/52 同取向)。`--strict` 供 CI/巡检改判红。
  - 本门**只读,永不删文件**;清理动作一律走 `c-drive-auto-maintain.ps1`(带 `-DryRun` 与逐条留痕)。
  - 取证:`--self-test` 12 例 + §22c 镜像测试 `node --test scripts/tests/check-c-drive-pollution.test.mjs`(7 例,含"他人工具态不得被判为我们的"、"TEMP 漂移判据"与**"本门编号在 runner 中必须唯一(反查 id,不硬写编号)+ 邻门注册块不得缺失)"**)。
  - 取证:`--self-test` 11 例 + §22c 镜像测试 `node --test scripts/tests/check-c-drive-pollution.test.mjs`(7 例,含"他人工具态不得被判为我们的"、"TEMP 漂移判据"与**"本门编号在 runner 中必须唯一(反查 id,不硬写编号)+ 邻门注册块不得缺失)"**)。
  - **自有产物特征含一条"盘根单字母目录"**:`C:\c` 这类是 MSYS/Git-Bash 把 `/c/...` 当**相对路径**用的错位指纹。实测 2026-08-06 一次就这样在 C 盘里套出 515MB(4 份 origin 浅克隆 + 一份错位的 npm 全局前缀),`git status` 与其余守门全都不知道。只认目录、同名文件不判(宁漏不误报);本门仍**只报不删**,该形态是否清理由人定。
  - **编号事故实录(一天撞四次,且"先查占用"被证明不够)**:85(与 `check-test-paths` 撞)→ 90(与 `ce261e1a8` 的 `check-sse-dispatch-parity` 撞)→ 91(与 `check-error-code-coverage` 撞)→ 92 又撞一次 —— **这次不是我没查**:我取 92 时它确实在 91,是别的会话随后把 `errorCode` 从 91 重排到 92,把重复号**带进了 origin/main**。⇒ 在高并发同日仓里,"提交前查一次占用"挡不住别人事后挪号,**唯一可靠的是让 runner 自己说话**:本门镜像测试现在断言"全 runner 任何 id 不得出现两次"(反查法,不硬写编号),它正是这次红掉的成因;本门编号此后仍在漂移,现值一律以 runner 为准。其中改 90 那次最严重:按整文件提交 `guardian-runner.mjs`(提交 `5db08f26e`)曾把别人刚装上的门**注册块直接覆盖掉**(diff 里就是一行 `script:` 被替换)—— 撞号只是重名,覆盖却是替别人卸闸;已按原文回插,并把断言写成"邻门注册块必须存在"。⇒ 两条硬规矩:① 改共享注册类文件(runner / package.json / CI)必须 `git show <commit> -- <f> | grep '^[-+].*(id:|script:|label:)'` 逐块核对;② **判据要能让机器自己发现撞号**,不要依赖人记得去查。紧急跳过 `HUSKY_SKIP_C_DRIVE_POLLUTION=1`。
  - **编号事故实录(一天撞四次,且"先查占用"被证明不够)**:85(与 `check-test-paths` 撞)→ 90(与 `ce261e1a8` 的 `check-sse-dispatch-parity` 撞)→ 91(与 `check-error-code-coverage` 撞)→ 92 又撞一次 —— **这次不是我没查**:我取 92 时它确实在 91,是别的会话随后把 `errorCode` 从 91 重排到 92,把重复号**带进了 origin/main**。⇒ 在高并发同日仓里,"提交前查一次占用"挡不住别人事后挪号,**唯一可靠的是让 runner 自己说话**:本门镜像测试现在断言"全 runner 任何 id 不得出现两次"(反查法,不硬写编号),它正是这次红掉的成因;终落 **93**。其中改 90 那次最严重:按整文件提交 `guardian-runner.mjs`(提交 `5db08f26e`)曾把别人刚装上的门**注册块直接覆盖掉**(diff 里就是一行 `script:` 被替换)—— 撞号只是重名,覆盖却是替别人卸闸;已按原文回插,并把断言写成"邻门注册块必须存在"。⇒ 两条硬规矩:① 改共享注册类文件(runner / package.json / CI)必须 `git show <commit> -- <f> | grep '^[-+].*(id:|script:|label:)'` 逐块核对;② **判据要能让机器自己发现撞号**,不要依赖人记得去查。紧急跳过 `HUSKY_SKIP_C_DRIVE_POLLUTION=1`。
- **临时夹具唯一落点:`scripts/lib/scratch-dir.mjs`(`mkScratch` / `rmScratch`,2026-09-23 立)**。两条选址硬约束都由实测踩坑固化:① 不得用 `os.tmpdir()`(活进程 TEMP 可能仍钉在 C 盘);② 不得落在仓库树内 —— git 夹具要模拟"非 git 目录",放在 `.ihui-agent/tmp/` 里时 `git rev-parse --show-toplevel` 会向上逃逸到真仓库,使该用例恒红(已用 HEAD 副本 A/B 实证,是当初先试后撤的方案)。现锚定**工作树同盘的 `DevEnv/Temp/ihui-scratch`**(§15b 批准的临时物落点),与 `gitArchiveDir()` 同一套盘符推导;`IHUI_SCRATCH_DIR` 为换机/CI 逃生舱,指向仓库内时按硬约束直接拒建而非静默产出会逃逸的夹具。回归:`node --test scripts/tests/scratch-dir.test.mjs`(4 例)。
- **行为(与 guardian-runner.mjs 实际一致)**:本脚本违规时 exit 1(供统计),guardian-runner 以 warn 模式捕获后计为"警告"、**不阻塞 commit**;仅 blocking 项失败才 exit 1 阻塞 commit。
- 紧急跳过(应急,默认不推荐):`HUSKY_SKIP_C_DRIVE_PATHS=1 git commit ...`

### 历史案例

- 2026-07-27:C 盘 28 GB → 42 GB,释放 13.85 GB(第三方 AI IDE 旧版残留 + Chrome OptGuideOnDeviceModel + Temp 旧文件)
- 后续配置 11 个环境变量永久指向 D 盘,杜绝开发工具缓存再写 C 盘
- 第三方 AI IDE 自身缓存由其自管理,项目维护脚本不再代清理

---

## 27. PowerShell 7 强制规则(强制,2026-08-13 立)

### 触发背景

Windows PowerShell 5.1(`powershell.exe`)已 EOL(微软停止维护),且存在已知 bug:

- `Out-File -Encoding UTF8` 实际写 UTF-16 LE(BOM 处理 bug)
- ANSI 代码页读 UTF-8 写入的 `.ps1` 文件时,`if/else` 块解析截断(`Missing closing '}'` 误报)
- 中文路径/文件名处理在 5.1 上不稳定
- 跨平台兼容性差(Linux/macOS 跑不通)
- 真实事故:2026-08-13 C 盘 130MB 修复任务,D 盘诊断脚本二次报 `Missing closing '}'`,根因是 5.1 读 pwsh 7 写入的 UTF-8 文件的 BOM bug

### 强制规则(违反视为交付事故)

1. **agent 跑 PowerShell 必须用 `pwsh.exe`**(PowerShell 7+):
   - ✅ `pwsh -File scripts/foo.ps1`
   - ✅ `& "C:/Program Files/PowerShell/7/pwsh.exe" -File ...`
   - ❌ `powershell -File scripts/foo.ps1`
   - ❌ `powershell.exe -Command ...`
   - 唯一例外:Windows 系统 5.1 专属 cmdlet 需在脚本里 `Set-Alias` 显式标注
2. **所有项目内 `.ps1` 文件第一行必须 `#requires -Version 7`**:在 5.1 上跑会**直接报错退出**,这正是强制效果
3. **CI / 守门脚本必须用 `pwsh`** 跑
4. **路径统一用正斜杠 `/`**:`C:/Program Files/PowerShell/7/`,避免 5.1 反斜杠转义 bug
5. **`.ps1` 文件用 UTF-8 with BOM 写**:PowerShell 5.1 默认以 ANSI 代码页读 `.ps1`,`Out-File -Encoding UTF8` 在 5.1 上写的是 UTF-16 LE,必须用 `[System.IO.File]::WriteAllText($path, $content, [System.Text.UTF8Encoding]::new($true))`

### 守门(blocking)

- `scripts/check-pwsh-version.mjs`:检查 `.ps1` 前 5 行是否含 `#requires -Version 7`,缺失则 exit 1。ROOT 由脚本自身位置推导(不写死盘符)
  - `--staged`:仅检查 **git index 中已暂存的** `.ps1`(`.husky/pre-commit` 用此模式)
  - 缺省:全树扫描(人工 / CI 全量审计用)
- 集成位置:`.husky/pre-commit` 直接调用 `node scripts/check-pwsh-version.mjs --staged`(blocking);跳过 `HUSKY_SKIP_PWSH_VERSION_GUARD=1`(应急,默认不推荐)
- 检查范围:项目内 `.ps1`(`scripts/`、`apps/*/scripts/`、`.ihui-agent/scripts/` 等)
- 白名单:`*.venv/*`、`venv/*`、`node_modules/*`、`.git/*`、`tmp/*`、`deploy/*`、`.ihui-agent/*`、`site-packages/*`(playwright 驱动)、`.workbuddy/quarantine/*`(污染治理隔离归档)
- **2026-09-15 修复(本守门自身 P0 回归)**:`--staged` 自挂载起只在用法注释里声明、**从未实现**,实现中只有无条件的全树 `scan(ROOT)`,导致工作区里未跟踪且被 gitignore 的遗留 `.ps1`(实测 `.android-toolchain/*.ps1`、`.tmp-wechat-test/watch.ps1`)**阻断每一次提交**——而它们在干净 checkout / CI 里根本不存在。已真正实现 `--staged`,并补齐 `deploy/*` 等白名单口径。

### 安装指引(机器上没装 PowerShell 7)

```powershell
winget install --id Microsoft.PowerShell -e --source winget
# 或一键脚本
iex "& { $(irm https://aka.ms/install-powershell.ps1) } -UseMSI"
```

### 历史教训

- 2026-08-13:CredentialHelperSelector 弹窗修复任务全程用 `powershell`(5.1),导致 D 盘诊断脚本二次报 `Missing closing '}'`(5.1 对 BOM 处理 bug)。改用 `pwsh` + `#requires -Version 7` 后立即通过。教训:RunCommand 默认 PowerShell 解释器**不是 7 也不是 5 的中性选择**——必须显式指定 `pwsh`。

---

## 28. 根目录整洁铁律(强制,2026-08-15 立)

### 触发背景

根目录曾散落 10+ 临时产物:`debug.log` / `aha_electron_2026.0810.log` / `cookies.txt` /
`page_eval/prompts/response/usage.html` / 过时 `start-dev.ps1`(D 盘旧路径 + 8810 旧端口) /
`browser_test_output/` 等。2026-08-15 一次性清理后,立此铁律防回潮。

### 强制规则(违反视为交付事故)

1. **一级目录只允许白名单内条目**,白名单封闭维护于 `scripts/check-root-dir-clean.mjs`(4 组 Set):
   - `ALLOWED_FILES`:配置(`package.json`/`pnpm-*.yaml`/`turbo.json`/`eslint.config.mjs`/`tsconfig.base.json`/`knip.jsonc`/`docker-compose.yml`/`railway.json`/`render.yaml`/`app.json`)+ 标准文档(`README*.md`/`LICENSE`/`SECURITY.md`/`CONTRIBUTING.md`/`CODE_OF_CONDUCT.md`)+ 项目强制(`AGENTS.md`/`PROJECT_PLAN.md`)
   - `ALLOWED_DIRS`:`apps`/`packages`/`scripts`/`docs`/`deploy`/`monitoring`/`reports`/`sdks`/`cert`/`products`/`logs`/`node_modules`/`tmp`/`test-results`
   - `ALLOWED_HIDDEN_FILES` / `ALLOWED_HIDDEN_DIRS`:`.env*`/`.gitignore`/`.git`/`.github`/`.husky`/IDE/工具目录等
2. **禁止在一级目录生成 `.log` / `.html` / `cookies*` / 截图 / ad-hoc 脚本**:临时产物一律进 `tmp/` 或 `logs/`,测试产物进 `test-results/` 或对应子目录。
3. **新增合法根目录文件/目录必须显式审批**:把条目加进 `scripts/check-root-dir-clean.mjs` 白名单并随 commit 提交,禁止绕过白名单。
4. **任务收尾必查**:交付前跑 `node scripts/check-root-dir-clean.mjs --staged`,0 违规才算完成。
5. **忽略产物也纳入视野(2026-09-23 补)**:守门原对"被 git 忽略的一级目录条目"整体跳过(该豁免必要 —— 忽略条目本由 `.gitignore` 承接,若一并拦截则各类构建产物与本地目录会让每次提交皆红;注意 `tmp/`、`logs/` 等已在 `ALLOWED_DIRS`,驱动这条豁免的是"未进白名单却被忽略"的产物),致本规则第 2 条的禁令对被忽略文件零覆盖 —— 实测一次性翻出 14 个静默残留(`_knip.log`/`.tmp-tsc.log`/`build-oidc.log` 等),`git status` 与守门都看不见。现新增**只告警层**(不计失败、不改退出语义:全量恒 0,`--staged` 仍只对非忽略违规 exit 1)。看到该告警即删除或移入 `tmp/`、`logs/`。

### 守门(blocking)

- `scripts/check-root-dir-clean.mjs`:扫描一级目录,白名单外条目 `--staged` 模式 exit 1
- 集成位置:`scripts/guardian-runner.mjs` pre-commit 第 44 项(2026-08-15 立)
- 逃生舱(应急,默认不推荐):`HUSKY_SKIP_ROOT_DIR_GUARD=1 git commit ...`

### 配套

- `.gitignore` 已补 `browser_test_output/`、`cookies.txt` 防回潮(未跟踪垃圾不污染 `git status`)

---

## 29. dangling commit 备份策略(2026-08-19 立)

### 由来

`commit-loss-check` 守门会拦截 dangling commits,要求先备份为 tag 再继续操作。
本仓库实践:所有 dangling commits 备份为 `lost-commit/wip-*` tag(以 commit 短 hash 命名,
例如 `lost-commit/0141d221`、`lost-commit/wip-batch-00f2429`)。

截至 2026-08-19,本地 `lost-commit/*` tag 数量 = **4188** 个,远超 §22「已被 reset 丢失的 commit
永久记录」清单的 3 个手工备份 tag — 绝大多数来自 `check-commit-loss-guard.mjs` 自动 fsck
悬空检测后批量打 tag 的产物(典型为 merge commit / stash pop 失败的中间 commit)。

抽样 `git show --stat lost-commit/{0141d221,02000bfd,043af88d}` 均显示为 Merge commit,
作者 `AI智汇社 <lizong@aizhs.top>`,日期集中在 2026-08-17 — 与 §22 守门日志一致。

### 保留周期

建议 **30 天**。超出后一次性 GC(由仓库维护者人工触发,不在守门脚本里自动跑):

```bash
# 1. 先确认无重要未提交工作(红线!)
git status               # 应 clean
git stash list           # 应为空
git diff --stat          # 应无改动

# 2. 列出所有 lost-commit tag(预演,确认数量级合理)
git tag -l 'lost-commit/*' | wc -l   # 当前 4188

# 3. 本地一次性删除
git tag -l 'lost-commit/*' | xargs git tag -d

# 4. 同步删除远程 tag(仅当你确认远端也允许清理时;默认保留远端)
#    警告:此操作会真正影响 origin ref 列表,需在 PR 中明确说明
git tag -l 'lost-commit/*' | xargs -I{} git push origin :refs/tags/{}

# 5. 验证 (强校验,任何 missing/unreachable 都应人工复核)
git fsck --unreachable --no-reflogs
git tag -l 'lost-commit/*' | wc -l   # 应为 0
```

### 为什么必须人工触发而不是脚本自动化

- **守门不擅自删 tag**: `check-commit-loss-guard.mjs` 的职责是"检出 + 备份",
  不应承担"GC 清理"职责 — 后者一旦误删,会真正丢失 dangling commit 的可达路径,
  即使远端有备份也增加了恢复成本。
- **数量级跃升需要人审**: 从 0 → 4188 是日积月累的结果,任何"一键 GC"脚本都应要求
  人类在 PR 里显式 ack,而不是 nightly cron 自动跑。
- **§22 守门只挡新增**: pre-commit 第 30a 项(`--blocking --filter-stash`)只关心
  "这一次操作是否会产生 dangling",不关心历史积累。

### 红线

- ❌ **不要把 lost-commit tag push 到 origin 之外的 fork / mirror**(会污染外部 ref 列表)。
  §22「自动化 tag 同步」明确禁止了向非 origin 的推送。
- ❌ **GC 前未确认 `git status clean` + `git stash list` 为空**(会丢失未提交改动)。
- ❌ **一次性删除超过 1000 个 tag 后不验证 `git fsck` 就 push**(可能误删有意义的 commit,
  因为 fsck 检出的是"创建时刻"的悬挂,不代表当下已被 merge 进 main)。
- ❌ **amend 含 dangling 备份说明的 commit**(本节一旦 commit,内容视为定稿;
  后续 GC 时间窗的调整应新加 §29a / §29b,不要回头改这一节)。

### 配套

- §22「防止 commit / push / merge 提交丢失硬性规则」— 提供守门 + 备份机制
- §22「自动化 tag 同步」(`scripts/sync-lost-commit-tags.mjs`)— 保证远端有副本
- `docs/lost-commit-archive.md` — 丢失 commit 的永久档案(人工可读清单)
- `.ihui-agent/archive/AGENTS_history.md` — 历史 GC 案例(本节首次落地后应补一条案例)

---

## 42. React SyntheticEvent 闭包陷阱(强制)

### 触发背景(2026-08-12 立,真实 bug)

`apps/web/src/components/chat/model-selector.tsx` 原 `onMouseLeave` 在 `setTimeout` 回调闭包内访问 `e.currentTarget`:

```tsx
setTimeout(() => {
  setPopoverAnchor((prev) => (prev?.el === e.currentTarget ? null : prev))
}, 100)
```

React 17+ 的 SyntheticEvent 在事件处理函数返回后 `currentTarget` 会被置 `null`,异步回调触发时 `prev?.el === null` 永远为 `false`,关闭分支永远不进 → model picker 常驻显示。

### 陷阱机制

- React 事件处理函数(`onClick={e => ...}` 等)**同步作用域内** `e.target` / `e.currentTarget` 有效。
- 一旦进入异步回调(`setTimeout` / `setInterval` / `requestAnimationFrame` / `requestIdleCallback` / `queueMicrotask` / 未捕获 `e` 的 promise)或 memoized / 延迟执行的闭包场景,SyntheticEvent 会被事件池复用/清空,`e.target` / `e.currentTarget` 失效或为 `null`(React 17+ 不再 pooled,但 `currentTarget` 一定在 handler 返回后置 null)。

### 强制做法(违反视为 defect 事故,强制)

1. **同步缓存**:在事件处理函数**同步作用域**先缓存所需字段,再在异步中使用。例:`const id = e.currentTarget.dataset.id` → 异步闭包内读 `id`,严禁直接读 `e.currentTarget.dataset.id`。
2. **用 ref 管理 DOM**:`anchorRef.current` 替代 `e.currentTarget`,适合需在多个回调 / 清理函数中引用同一 DOM。
3. **异步闭包内禁止访问 `e.` 任何属性/方法**(`currentTarget` / `target` / `preventDefault` / `stopPropagation`)。

### 守门

`scripts/check-event-closure-leak.mjs`(guardian-runner 第 42 项,blocking):用括号配对算法提取异步回调第一个参数(箭头函数体),在其内搜 `e.X` 模式,命中 → exit 1 阻塞 commit。
参考修复:`apps/web/src/components/chat/model-selector.tsx` MemberDiscountSection。

---

## 关键参考文档

| 文档                      | 说明                                                          |
| ------------------------- | ------------------------------------------------------------- |
| `PROJECT_PLAN.md`         | 唯一任务计划文档(必读)                                        |
| `.ihui-agent/archive/`    | 历史归档(audit/交接/迁移报告,只读)                            |
| `docs/architecture.md`    | 系统架构文档                                                  |
| `docs/port-management.md` | 端口注册表(88xx 段)                                           |
| `docs/learning-assets.md` | 学习资产登记(34 个工作流反馈来源,新增/删除工作流必须同步更新) |

<!-- ⁠​‌​​‌​​‌‍‍​‌​​‌​​​‍‍​‌​‌​‌​‌‍‍​‌​​‌​​‌‍‍​​‌​‌‌​‌‍‍​‌​​​​​‌‍‍​‌​​‌​​‌‍‍‌​‌‌​‌‌‌‍‍‌‌​​‌‌​​‌‌‌‌​‌​‍‍‌‌​‌‌​​​‌​​​‌‌‌‍‍​‌​​​​​‌‍‍​‌​​‌​​‌‍‍‌​‌‌​‌‌‌‍‍‌‌​​‌‌‌​‌​​‌‌‌​‍‍‌‌​​‌‌​​​‌​​‌​‌‍‍‌​‌‌‌​‌‌‌​‌‌‌​‌‍‍‌​‌‌​‌‌‌‍‍​‌​​‌‌​​‍‍​‌​​​​‌‌‍‍‌​‌‌​‌‌‌‍‍​‌‌​​​​‌‍‍​‌‌​‌​​‌‍‍​‌‌‌‌​‌​‍‍​‌‌​‌​​​‍‍​‌‌‌​​‌‌‍‍​​‌​‌‌‌​‍‍​‌‌‌​‌​​‍‍​‌‌​‌‌‌‌‍‍​‌‌‌​​​​‍‍‌​‌‌​‌‌‌‍‍​‌​‌​​​​‍‍​‌​‌​​‌​‍‍​‌​​‌‌‌‌‍‍​‌​‌​‌‌​‍‍​‌​​​‌​‌‍‍​‌​​‌‌‌​‍‍​‌​​​​​‌‍‍​‌​​‌‌‌​‍‍​‌​​​​‌‌‍‍​‌​​​‌​‌‍‍​​‌​‌‌​‌‍‍​​‌‌​​‌​‍‍​​‌‌​​​​‍‍​​‌‌​​‌​‍‍​​‌‌​‌‌​⁠ -->

| 路径                             | 角色                                                        |
| -------------------------------- | ----------------------------------------------------------- |
| `D:/IHUI-AI/.git`                | **28 字节指针文件**(`gitdir: D:/IHUI-AI-git-repo`),不是目录 |
| `D:/IHUI-AI-git-repo`            | 真实 gitdir(544MB),在工作区之外                             |
| `D:/IHUI-AI.git-backup-20260912` | gitdir 完整备份,守护的本地恢复源                            |

- **唯一写法**:主按钮、选中胶囊、悬浮加号这类"品牌实底 + 其上文字",RN/共享包写 `brand.DEFAULT`(底)+ `brand.foreground`(文字)**成对**;CSS/类名侧用 `--color-primary` + `--color-primary-foreground`(小程序 `CategoryBar`/FAB 同值)。这两档在两主题下逐位同值(亮 `#000/#fff`、暗 `#fff/#000`),对账由「跨端色值同源对账」守门负责(编号以 `scripts/guardian-runner.mjs` 现值为准,勿照抄文档)。
- **唯一写法**:主按钮、选中胶囊、悬浮加号这类"品牌实底 + 其上文字",RN/共享包写 `brand.DEFAULT`(底)+ `brand.foreground`(文字)成对;CSS/类名侧用 `--color-primary` + `--color-primary-foreground`(小程序 `CategoryBar`/FAB 同值)。这两档在两主题下逐位同值(亮 `#000/#fff`、暗 `#fff/#000`),对账由守门 90 负责。

> ⚠️ 本行已于 2026-09-24 被上面「唯一写法(2026-09-24 改档)」就地改写取代(旧档 brand.DEFAULT/foreground 已废);保留原文只为不丢行,勿照它执行。

- 悬空引用由该门的"品牌键"判据直接拦(编译不一定红,运行时是 `undefined` 颜色)。并行会话的暂存区里若还残留 `tokens.brand.ctaFill/ctaText`,改法只有一行:`ctaFill` → `DEFAULT`、`ctaText` → `foreground`。
- **清单↔锁 specifier 对账**(101):`scripts/check-lock-manifest-consistency.mjs`(blocking,2026-09-24 立)—— 堵的是**依赖真值**这一整类"本地全绿、生产构建必炸":`package.json` 声明与 `pnpm-lock.yaml` 的 `importers.<pkg>` specifier 不一致时,pnpm **整段跳过该包的链接步骤**(不报错、不改锁、`pnpm install` 与 `pnpm install --force` 都只回 "Already up to date"),本地 node_modules 早就装好 ⇒ typecheck/lint/单测/其余守门全绿,直到 bundler 报 Module not found 打死生产构建。立因实例:当天 `apps/web` 写 `"xlsx": "^0.18.5"` 而锁记 `npm:@e965/xlsx@^0.20.3`,症状是"97 个声明依赖精确缺 3 条"——**只缺某一个 importer 的全部新增项**这个形状本身就是指纹(第一反应容易误判成"stale dist / 服务进程锁住文件",两者当日都被实测证伪:短停 IHUI-WEB 后 `--force` 仍 285ms 回绿)。三条判据 R1 缺记账 / R2 specifier 不等 / R3 反向孤儿**只报数不判红**;两个必须建模的 pnpm 合法行为,否则 22 枚存量误红逼人绕过钩子:**维度 A** 读 `pnpm-workspace.yaml` 的 `overrides`(真仓 50 条,裸名与 `pkg@<=版本` 两形态),lock 等于 override 目标即放过(19 条),但**严格侧保留**——某依赖只有唯一一条裸名 override 而 lock 仍停在 manifest 原值 ⇒ 判 `override-not-applied` 红(否则维度 A 就成了无条件放过);**维度 B** peerDependencies 不比 specifier(pnpm 记进 devDeps 段且写解析后的范围,3 条),但仍要求"任一段有条目",缺条目照红。**口径是本门生命线**:`--staged` 判**索引 blob**、全量判 **HEAD blob**(同 70/77/83/98/99),只允许同一轮读同一个面(含包清单枚举也走同一面,否则"glob 读盘 + 内容读 git"会造出自洽但基准错位的假绿尺子),`--worktree` 仅作人工排查逃生舱、两个面旗标同时给 ⇒ exit 2;取不到(缺路径/unmerged/非 blob/二进制/git 失败/列出 0 路径/toplevel 不匹配)一律 **exit 2 显式"无法判定"并点名路径**,绝不记绿。不挂 `stagedTriggers` —— 前缀语义表达不出"任意 `*/package.json`",而漏挂等于没有这道门(守门 81 教训),实测 282ms/342ms,每轮跑得起。取证 `--self-test` 34 例 + §22c 镜像测试 24 例,含三条端到端**假绿/假红钉死**对照(临时 git 仓:索引坏而盘上好 ⇒ staged 必红;索引好而盘上别人半编辑 ⇒ staged 必绿;该面取不到 ⇒ exit 2)与"改写真 decision/真事故值必红"的阳性对照;写门过程中自检另抓出 `splitOverrideKey` 把 13 条 `>=` 版本选择器误判成父作用域(整类隐身)这一自造缺陷。紧急跳过 `HUSKY_SKIP_LOCK_MANIFEST_GUARD=1`。

  - **`--take-ours <path>` 是"声明式例外",不是选边后门**(2026-09-24 加):两侧同改且真三方报冲突时,默认一律 `needHuman` 交人工 —— 但**报冲突不等于必须交人工**:若合并树的内容已让对侧那一版判据**必红**,取本侧就是被内容强制的唯一解。当天实例:`scripts/tests/check-cross-end-tokens.test.mjs` 两侧在同一段各写各的,对侧留着回归锁"brand 里不得再有 CTA 档",而 `packages/design-tokens` **只有本侧改过**(对侧动过数实测 0)⇒ 合并树必含 `brand.cta` ⇒ 那把锁 100% 失败;取证是**跑两版**:本侧版 `pass 11 / fail 0`,对侧版 `pass 7 / fail 1`(红的正是那把锁)。使用该例外必须**逐条附这样的取证**,且工具会把声明过的项目打进输出与合并提交信息(`;取本侧(已声明+可复核): <path>`)⇒ 无从静默。判据底线不变:落地前 `丢本侧路径 = 0 ∧ 丢对侧路径 = 0 ∧ 活文档未存活行 = 0`,落地后由守门 100 的 A1 复核。镜像测试钉住三件事:默认必须 `needHuman`、声明后 `keptOurs` 必须点名、且**一条例外不得连带丢掉对侧其它独有新增**。

- **mobile-rn 深色前景/容器对账**(83,原 75):check-brand-foreground.mjs(blocking,2026-09-23 立)—— 四条判据:**R1** 零豁免,同一 style 块内 `tokens.brand.DEFAULT` 作背景 × `surface.light`/`text.primary` 作前景(深色档案下 brand.DEFAULT=纯白 ⇒ 白底白字);**R4** 同一关系被拆到**兄弟 key**(`retryBtn` × `retryText`)时按命名配对判,走基线棘轮;**R2** `surface.light` 背景 / α≥0.5 白 rgba / 无 `dark:` 变体的 `bg-white` 作容器底,每文件计数对 `scripts/brand-foreground-baseline.json` 只减不增;**R3** 数 brand.DEFAULT 填充/描边但**不计 §4 成对 CTA**(同块或兄弟键用 `brand.foreground` 作前景)—— 见本节上方「品牌 CTA / 主按钮色同源」,按规矩写就红的门只会逼人 `--no-verify`;**无配对的白卡片照旧计**(自检里两条阳性对照钉住)。**内容口径**:全量与 `--update-baseline` 判 **HEAD blob**,`--staged` 判暂存 —— 共享工作树滞后 HEAD,按磁盘算会在恒红/假绿间来回跳并把错数写回基线(2026-09-24 一天内 R3 登记被整文件回退三次的直接成因)。`--staged` / `--update-baseline` / `--self-test`;紧急跳过 `HUSKY_SKIP_BRAND_FOREGROUND=1`。 ⚠️ 本行为 2026-09-24 就地改写**前**的原文,保留以不丢行(§12);现行文本见下方「品牌实底前景/容器对账(RN + 共享包 + web 类名面)」条,勿照本行执行。
- **mobile-rn 深色前景/容器对账**(83,原 75):check-brand-foreground.mjs(blocking,2026-09-23 立)—— R1 零豁免:同一 style 块内 `tokens.brand.DEFAULT` 作背景 × `surface.light`/`text.primary` 作前景(深色档案下 brand.DEFAULT=纯白 ⇒ 白底白字);R2 基线棘轮:`surface.light` 背景 / α≥0.5 白 rgba / 无 `dark:` 变体的 `bg-white` 作容器底,每文件计数对 `scripts/brand-foreground-baseline.json`(现 13 文件 24 处,均为媒体上的合法浮层或已带 `dark:` 变体)只减不增。`--staged` / `--update-baseline` / `--self-test`(11 例);紧急跳过 `HUSKY_SKIP_BRAND_FOREGROUND=1`。 ⚠️ 本行为 2026-09-24 就地改写**前**的原文,保留以不丢行(§12);现行文本见下方「品牌实底前景/容器对账(RN + 共享包 + web 类名面)」条,勿照本行执行。
- **mobile-rn 深色前景/容器对账**(83,原 75):check-brand-foreground.mjs(blocking,2026-09-23 立)—— 四条判据:**R1** 零豁免,同一 style 块内 `tokens.brand.DEFAULT` 作背景 × `surface.light`/`text.primary` 作前景(深色档案下 brand.DEFAULT=纯白 ⇒ 白底白字);**R4** 同一关系被拆到**兄弟 key**(`retryBtn` × `retryText`)时按命名配对判,走基线棘轮;**R2** `surface.light` 背景 / α≥0.5 白 rgba / 无 `dark:` 变体的 `bg-white` 作容器底,每文件计数对 `scripts/brand-foreground-baseline.json` 只减不增;**R3** 数 brand.DEFAULT 填充/描边但**不计 §4 成对 CTA**(同块或兄弟键用 `brand.foreground` 作前景)—— 见本节上方「品牌 CTA / 主按钮色同源」,按规矩写就红的门只会逼人 `--no-verify`;**无配对的白卡片照旧计**(自检里两条阳性对照钉住)。**内容口径**:全量与 `--update-baseline` 判 **HEAD blob**,`--staged` 判暂存 —— 共享工作树滞后 HEAD,按磁盘算会在恒红/假绿间来回跳并把错数写回基线(2026-09-24 一天内 R3 登记被整文件回退三次的直接成因)。`--staged` / `--update-baseline` / `--self-test`;紧急跳过 `HUSKY_SKIP_BRAND_FOREGROUND=1`。 ⚠️ 本行为 2026-09-24 就地改写**前**的原文,保留以不丢行(§12);现行文本见下方「品牌实底前景/容器对账(RN + 共享包 + web 类名面)」条,勿照本行执行。
- **品牌实底前景/容器对账(RN + 共享包 + web 类名面)**(83,原 75):check-brand-foreground.mjs(blocking,2026-09-23 立)—— 五条判据,`brand.DEFAULT` 与 `brand.cta` **同形认**(只认 DEFAULT 会让迁移后的主实底整片隐身 —— 判据必须覆盖门自己产出的形态):**R1** 零豁免,同块内 `backgroundColor: (tokens|tk).brand.(DEFAULT|cta)` × `surface.light`/`text.primary` 作前景;**R4** 同一关系拆到**兄弟 key**(`retryBtn` × `retryText`)按命名配对判,走棘轮(现 127 处);**R2** 拦"浅色当容器底"(`surface.light` / α≥0.5 白 rgba / 无 `dark:` 变体的 `bg-white`),棘轮现 24 处;**R3** 数品牌实底填充/描边,**按档配对**免债:DEFAULT 底 ↔ `brand.foreground`、cta 底 ↔ `brand.ctaForeground`,**跨档配对仍计**(DEFAULT 底 × ctaForeground 深色档案下=白底白字),棘轮现 235 处;**R5** web/ui-react 的 **Tailwind 类名形态**(R1–R4 全解析 RN style 对象,对 `bg-primary`+`text-primary-foreground` 这一 §4 已废配对原本整侧盲视),同行成对才算,变体前缀(`data-[state=checked]:bg-primary`)与染色底(`bg-primary/10`)不算,`r5-cta-exempt: 原因` 豁免,基线 `webClassPairCounts` **现为空 = 零容忍**(立项实测 22 处/14 文件,含 `@ihui/ui-react` Button 的 6 个 variant,当日由并行提交 `4e0b24689a` 迁完)。**R5 两条已知限制如实登记**:跨行配对不计(宁窄不误)、变体前缀不算实底(排除前缀的字符类与排除 `/90` 的是同一个,放宽任一即破坏口径)。**内容口径**:全量与 `--update-baseline` 判 **HEAD blob**,`--staged` 判暂存 —— 共享工作树滞后 HEAD,按磁盘算会恒红/假绿来回跳并把错数写回基线(2026-09-24 一天内 R3 登记被整文件回退三次的直接成因)。基线 6 键:`counts`/`ctaCounts`/`r4Counts`/`webClassPairCounts` + 两个文档性注记键(`pairedCtaNotVisibleToRule`/`ctaFillRenameLedger`)—— `--update-baseline` **必须保留注记键**,旧写法整文件重写会把他人审计台账冲掉(本轮并入远端时实测踩过)。取证:`--self-test` **94 条断言**(含 3 条阳性对照 + 变异对照)+ `node --test scripts/tests/check-brand-foreground.test.mjs` **13 例**;`--staged` / `--update-baseline` / `--self-test`;紧急跳过 `HUSKY_SKIP_BRAND_FOREGROUND=1`。
- **品牌实底前景/容器对账(RN + 共享包 + web 类名面)**(83,原 75):check-brand-foreground.mjs(blocking,2026-09-23 立)—— 五条判据,`brand.DEFAULT` 与 `brand.cta` **同形认**(只认 DEFAULT 会让迁移后的主实底整片隐身 —— 判据必须覆盖门自己产出的形态):**R1** 零豁免,同块内 `backgroundColor: (tokens|tk).brand.(DEFAULT|cta)` × `surface.light`/`text.primary` 作前景;**R4** 同一关系拆到**兄弟 key**(`retryBtn` × `retryText`)按命名配对判,走棘轮(现 127 处);**R2** 拦"浅色当容器底"(`surface.light` / α≥0.5 白 rgba / 无 `dark:` 变体的 `bg-white`),棘轮现 24 处;**R3** 数品牌实底填充/描边,**按档配对**免债:DEFAULT 底 ↔ `brand.foreground`、cta 底 ↔ `brand.ctaForeground`,**跨档配对仍计**(DEFAULT 底 × ctaForeground 深色档案下=白底白字),棘轮现 235 处;**R5** web/ui-react 的 **Tailwind 类名形态**(R1–R4 全解析 RN style 对象,对 `bg-primary`+`text-primary-foreground` 这一 §4 已废配对原本整侧盲视),同行成对才算,变体前缀(`data-[state=checked]:bg-primary`)与染色底(`bg-primary/10`)不算,`r5-cta-exempt: 原因` 豁免,基线 `webClassPairCounts` **现为空 = 零容忍**(立项实测 22 处/14 文件,含 `@ihui/ui-react` Button 的 6 个 variant,当日由并行提交 `4e0b24689a` 迁完)。**R5 两条已知限制如实登记**:跨行配对不计(宁窄不误)、变体前缀不算实底(排除前缀的字符类与排除 `/90` 的是同一个,放宽任一即破坏口径)。**内容口径**:全量与 `--update-baseline` 判 **HEAD blob**,`--staged` 判暂存 —— 共享工作树滞后 HEAD,按磁盘算会恒红/假绿来回跳并把错数写回基线(2026-09-24 一天内 R3 登记被整文件回退三次的直接成因)。基线 6 键:`counts`/`ctaCounts`/`r4Counts`/`webClassPairCounts` + 两个文档性注记键(`pairedCtaNotVisibleToRule`/`ctaFillRenameLedger`)—— `--update-baseline` **必须保留注记键**,旧写法整文件重写会把他人审计台账冲掉(本轮并入远端时实测踩过)。取证:`--self-test` **94 条断言**(含 3 条阳性对照 + 变异对照)+ `node --test scripts/tests/check-brand-foreground.test.mjs` **13 例**;`--staged` / `--update-baseline` / `--self-test`;紧急跳过 `HUSKY_SKIP_BRAND_FOREGROUND=1`。
- ⚠️ 本行原为守门 83 的**第三条重复登记**,且把取证量写死成"`--self-test` 11 例"(实为 94 条断言)。2026-09-24 就地改写为指向条:该门的现行条目见**上方两条**(编号 83,判据 R1–R5);保留本行只为不丢行,勿照它执行。
- **守门 83 追加 R7(2026-09-25,JSX 渲染嵌套级跨档配对)**:R1 只看同一 style 块、R4 只认命名兄弟(`X`/`XText`),但真实代码里"底"与"它的文字"常毫无命名关系 —— `balanceCard: {backgroundColor: tk.brand.cta}` 配 `balanceLabel: {color: tk.surface.light}`,深色档 `surface.light=#262626` 压 `#4A7A96` = **3.25:1,低于 AA 4.5:1**。R7 用递归下降扫 JSX(深度感知、跳字符串/注释,吃三元、`.map()` 体、泛型 `<FlatList<T>>`、多行与数组 `style={[a, b && c]}`),把前景归到**最近一个自带背景的祖先**;该祖先须整族为品牌档且所有候选前景全不匹配才判,命名兄弟让给 R4 不重复计;并覆盖内联 `color=` 的 icon/spinner(如 `<Plus color={tk.surface.card}>` 落在 cta 悬浮钮上 —— R1/R4 一条都看不见)。**HEAD 实测 16 处 / 9 文件**冻进基线键 `nestMismatchCounts`(只减不增),人工出口 `r7-nest-exempt: <原因>`(**须带原因,不得用它清账**)。构建期自查出两类假阳性并已钉成回归:① 嵌套元素 `style=` 并入宿主 key 集(致 `commentEmptyText` 冒充 `commentNickname` 的底)② 与 R4 重叠的命名兄弟重复计债。取证:`--self-test` 133 条断言(含 34 条 R7,含"把真文件 ctaForeground 改回 surface.light 必命中、换回必 0"的判别力证明)+ 镜像测试 17 例。**性能**:R7 在 557 文件上 137ms(R1+R3+R4 合计 174ms);本门 27–45s 的大头是既有的逐文件 `git show` 取材成本,不是新判据。
- ⚠️ 上面那条的"判定文件 >300 跳过"护栏已被 **2026-09-24 的乘数级例外**收窄(普通文件仍受护栏约束,但门的注册表/门自身/`scripts/lib/`/钩子/根清单/工作流恒照判)——现行口径见本节「本轮四道门的口径/落点收口」第 ② 条与 `scripts/tests/check-stale-revert.test.mjs`;保留原文只为不丢行,勿照它执行。
- **UI/样式**(11/11b/17/18/20/24a/24b/27/28/36):圆角 / 圆角溢出(父 rounded + 子 bg 贴边,warn,2026-08-06 立)/ CSS token / title tooltip / Tailwind 冲突 / 侧边栏宽度+端口注册表(warn)/ z-index 层叠+遮罩 z-index(阻塞,id 27/28,2026-09-22 加固:补 skipEnv + onFailHint,并在 id 27 新增第 5 项两组契约——桌面端自绘窗口控制三按钮挂 z-max(10003) 不能降(须高于 resize 抓手 z-loading=10000),`z-modal` 遮罩永远盖不到它,故等效压暗层 `data-window-controls`+`data-window-controls-dim` 与失焦非活动态 `data-window-inactive` 两组标记缺一不可,**两态弱化/瞬时规则必须写在 `apps/web/app/globals.css`**(入口 CSS 变更必然重编译;实测生产构建里组件内 Tailwind 任意变体未进 CSS 产物);判据有效性自查 `--self-test`;紧急跳过 HUSKY_SKIP_Z_INDEX_GUARD / HUSKY_SKIP_OVERLAY_ZINDEX)/ miniapp-taro design-tokens 同步(阻塞,防 app.css 漂移)
- **桌面端本地缓存明文巡检**(warn-only,2026-09-24 立,**不进提交链**):`scripts/check-desktop-cache-plaintext.mjs` —— D48 的加密代码早在 HEAD(`apps/web/src/lib/chat-persist-crypto.ts` / `local-vault.ts` / `desktop-token-vault.ts`),而桌面端 WebView 数据目录整棵 `ihuiVaultV1` **零命中** ⇒ 票面"静态盘 grep 明文会话为 0"这条验收**是虚假通过**:它绿是因为这台机当前没有登录数据,不是因为数据被加密。四判据必须同时成立才算数 —— **判据 0** 含唯一 nonce 中文串的**阳性对照**(先证明"扫描动作本身看得见明文",缺了它的门等于没有);**判据 1** 断言 `ihui-chat` / goal 键的**结构位**恰为 `{ihuiVaultV1}` → `{alg,kid,iv,ct}`,常量自 `local-vault.ts` 源码解析(**禁止抄第二份字面量**,§"两处算同一 key 必须共用一份实现");**判据 2** 目录级 CJK 命中为 0 **且必须打印实扫清单**,扫描面刻意排除 `Cache_Data`/`Code Cache`/`GPUCache`(实测无正文,35MB+ 只会拖慢并引入二进制误报);**判据 3** 密钥不入仓(`git ls-files` 双向断言)+ `ihui-vault.json` 落点字符串单源。路径一律 `realpathSync` 解析 junction 真身,**禁写死盘符**(§26 台账里的 `D:` 在本机已漂到 `G:`);非 win32 / 目录不存在 → 显式判"未判定"并给原因,**绝不记为通过**。全程只读,任何递归枚举前先判重解析点(§26 的 junction 穿透清空事故)。定级 warn 且刻意不挂 guardian-runner:它判的是**机器状态**,提交者结构上无法满足,挂 blocking 只会逼全队 `--no-verify` 连带废掉全部守门(§12e 同型)。入口 `pnpm check:desktop-cache-plaintext`;取证 `--self-test` 10/10 + `node --test scripts/tests/check-desktop-cache-plaintext.test.mjs` 12/12。
- **上面"真实库在 `F:`、`D:/BaiduSyncdisk` 根本不存在"是另一台机(G: 那份 checkout)的实测,在这一台机上正好相反**(2026-09-25 实测:`F:`/`E:` 都 absent,`D:/BaiduSyncdisk/密钥/` 才是真库 —— 里面有 `模型/`、`git仓库/`、`IHUI生产账号/` 等)。与 §5b 四条"机器事实"同族:**盘符每次使用前按当次实测取值**,不得把另一台机的现状当本机事实,更不得据此推断"凭据失效"。
- **非 node 调用方(PowerShell / 批处理 / 计划任务)取凭据路径的唯一出口 = `node scripts/secret-path.mjs <子目录> <文件名>`**(2026-09-25 立)。它只做 `key-dir.mjs` 的出口包装,**stdout 恒为路径、绝不含任何凭据内容**(NSSM 会把服务 stdout 落进日志,打内容等于把口令写进日志),且**区分"凭据目录不存在"(要先 mkdir)与"文件不存在"**(两者处置动作不同,混成一句会让人写好文件仍读不到)。退出码:`0` 解析到 / `1` 目录或文件缺失 / `2` 连根都不可达(`2` 是"无法判定",不得报成"口令错误")。调用方**禁止再自己抄一份 F→D→E→G→C 候选** —— 那正是本节记过的"读不到被下游说成失效"的成因。
- **口令落点按用途分子目录,规矩同一套**:现有 `模型/`(厂商 key)、`git仓库/`(远端凭据),2026-09-25 起新增 **`db-backup/ihui-backup.txt`**(PG 备份专用角色口令,见 `deploy/win/ihui-pg-backup-role.sql` 与 `deploy/win/ihui-pg-backup.ps1` 的凭据段)。新增用途就再加一个子目录,**不要**把口令挪进仓库、`.env`、日志或聊天记录。
  - **跨进程参数选 ASCII**(本例 `db-backup` 而非 `数据库备份`)——但先看清**本机实测中文是无损的**:`.ps1` 保持无 BOM UTF-8,经服务实际使用的 `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`(**在这个 Windows 11 26200 上产品版本已是 7.6.2**,`Get-Item .VersionInfo.ProductVersion` 实读)传给 node 后 hex 逐字节一致。选 ASCII 只是让"解释器版本 / 控制台代码页"这类未证差异**不可能**影响一条功能参数;真正会发生的现象是**输出面**因 GBK 控制台代码页花屏(实测),那不影响判据,所以中文留在注释与 `Write-Host` 里无妨。
  - ⚠️ **由此推翻 §27 的一个前提**:在这一台机上 `powershell.exe` 与 `pwsh.exe` **是同一个引擎(7.6.2)**,所以"必须用 pwsh 才安全"在这里不是引擎差异而是路径习惯。**别再按 5.1 的假设写防御代码** —— 本票就先写了一段 `ErrorActionPreference='Continue'` 包裹(防"Stop + 原生 `2>&1` 把 stderr 升成终止错误"),实测两引擎都不抛、`$LASTEXITCODE` 完好,遂删。凡"某个 PowerShell 版本会怎样"的断言,一律当场跑一次再说。
  - **`merge-live-doc` 的第三种定性:同锚点 ⇒ 就地改写(2026-09-25 补,`--self-test` 13→16 例)**。上面两句判据只分"吃掉 vs 存活":容器短路管"旧行逐字活在新行里",Jaccard 管"两行很像"。**改行中段**时两者同时失效(旧行不逐字存活、长行大改后相似度掉到阈值下),于是正当的就地改写被判成"真丢失"并要人跑 `--apply` —— 而 `--apply` 会把旧行原样插回 ⇒ **同一件事新旧两行并存**,本节上方那对重复的"提交活文档前必须做…"登记行、以及守门 83 的三条同体行,制造路径之一就是这个。现规则:三份活文档都是「一件事一行」且行首有稳定锚点(`- **名称**` / `### 标题`),**同一锚点在"HEAD 缺失集"与"工作树独有集"里各只出现一次** ⇒ 判 `superseded`(不插回)。唯一性是生命线:锚点在任一侧数量 ≠ 1 就不猜,退回原判据(宁可多报交人工,也不能把"整段登记被删"洗成"他改写了")。取证两层:①`node scripts/merge-live-doc.mjs --self-test` 16 例(⑭改写⇒0 / ⑮删除⇒仍 lost / ⑯同锚点两条⇒不猜);②CLI 级装车证明 `scripts/tests/merge-live-doc-anchor.test.mjs`(3 例 A/B/C)—— 本文件顶层就是 CLI 且**没有 §22d `isDirectRun` 守卫**(`lib/live-doc-similarity.mjs` 就是为此被抽出来的),所以测试只能把脚本 spawn 起来验,不能 import 判据函数;C 例自证夹具确实"不像",防止 A 臂退化成容器短路的测试。**误判的代价不是"多一行报告",是逼人跑 `--apply`,而 `--apply` 造出的重复行下一次又被判成需要归并 —— 这是它自己的正反馈环。**
- **反回退对账**(84,原 76):check-stale-revert.mjs(blocking,2026-09-23 立)—— 堵共享工作区**静默回滚**:§12d converge 用 merge-tree/commit-tree 只推进 HEAD+index、**不 checkout**,工作区落后 HEAD 时 `git add <file>` 交的是旧基线(实测 503 文件落后 486 提交),等于把别人该路径的后续改动静默回滚,而 diff 只显"改了几行"。判据 R1 = 暂存 blob != HEAD blob **且字节级等于该路径某祖先提交版本** → 拦并点名回到的 commit;真新编辑不可能恰好等于历史 blob,故误报极低。三条护栏:merge 上下文**不再整轮豁免**(2026-09-25 收窄,见下)、暂存删除只 warn(`git rm` 合法,机器分不清就被删)、判定文件 >300 跳过(性能护栏,防逼人 --no-verify 连带关掉全部守门;**乘数级路径不吃这条护栏**,2026-09-24 立)。**合并期窄判据 R1m**:某路径上 `ours == theirs`(两父逐字节一致)⇒ 合并对它无事可做 ⇒ 索引里与两父都不同的内容**不可能来自本次合并**,再要求它恰等于该路径某历史版本才判红 —— 第三个条件是必须的,因为"人工解冲突写进新内容"同样满足前两条,而那是正当形态。`cherry-pick / revert / rebase` 仍整轮豁免(取历史内容本就是其语义)。立因(第四十二批未闭环④):旧口径那句"merge 上下文整轮豁免"是**本门自己的盲区**,当夜 30c 判红、84 沉默,合并期把旧基线写进索引完全无人看守。取证 `--self-test` **12 例**(新增 5a/5b/5c/5d/6b 五例合并对照:两父不同⇒绿、两父一致而等于历史版本⇒红、等于新写内容⇒绿、cherry-pick⇒仍绿)+ §22c 镜像测试 7 例(含"整轮豁免那句写法不得回来"的**反向回归锁** —— 收窄是判据不是临时关闭,必须能被机器发现)+ 临时 index 端到端演练 3/3(2026-09-23 原始取证,保留);**有意回退一律改用 `git revert` 生成前向提交**。**一条本门看不见的时间窗(2026-09-26 实测)**:84 判的是**索引**里的 blob,而"工作树副本 == 祖先版本、但尚未 `git add`"这一格它结构上碰不到 —— 偏偏危险就在这格里:任何人一次 `git add <该文件>` 或一条不带 pathspec 的 `git commit -a` 就会把它交上去。当天实测 `scripts/guardian-runner.mjs` 的工作树副本等于祖先 `4c1610662`(09-25 19:34),**比 HEAD 少 3 道门(124/125/126)**,而 `git status` 只显示一个 ` M`、13c/71/84 全都不响。量这一型的尺子是 `node scripts/heal-worktree-tracked.mjs --align-drift`(判据:索引==HEAD 且工作树==该路径某祖先 ⇒ 才动,可证零独有数据),手工等价动作是 `git checkout-index -f -- <path>`;**先看判据再动手,不满足就不许对齐** —— 它也可能是别人真在写的现场。紧急跳过 `HUSKY_SKIP_STALE_REVERT_GUARD=1`。
- **架构契约对账**(103):`scripts/check-architecture-policy.mjs`(blocking,2026-09-25 立)—— 与其余全部守门**方向相反**的一道:其余是"发现一类违规 → 写一条判据"(打地鼠),本门读 `config/architecture-policy.yaml` 这张**声明表**,从声明反查违规。四类红:**T1** 表与现实脱节(模块改名/目录搬走而表没跟上 —— 表一过期,依赖它的所有判断都在对着空气打分)/ **C1** 单文件行上限(阈值 6000,**取自 HEAD 实测最大文件 5258 行之上**,依据写在 yaml 注释里)/ **D1 D2** 依赖方向(包 import 端、或 `requires` 未声明就 import;以及反向按 rank 更深一层)/ **D3** 深导入(绕过 `public_entrypoints` 直接摸别的包内部 —— 它是"端内重复实现"的入口)。**渐进收口是设计前提**:存量模块一律 `managed: false` ⇒ 只报数不判红(立项实测全量 exit 0、报数 3 处、`managed:true` 0 个),翻 true 前先用 `--managed-trial <id>` 看条数,**不得为消红去改阈值或把不该对外的模块标成 exported**。行内豁免 `arch-exempt: <原因>`(须带原因)。口径同 70/77/83/98/101:全量判 **HEAD blob**、`--staged` 判索引 blob。**策略表自身的取材按档定向**(`policyFaceOrder`):全量档 HEAD 优先、**`--staged` 档索引优先**,两者都再降级到工作树;取不到判"无法判定",不冒红也不记绿。⚠️ 这里 2026-09-25 修掉一处**本门对自身改动的盲视**:原实现两个面都 HEAD 优先,于是"修改策略表自身"的那枚提交完全不进本门审查 —— 实测往索引版 `apps/cli.requires` 注入一条 `apps/api`(端应用 `exported:false`,T1 必判红)后全量与 `--staged` 双双 exit 0,而 `--staged` 输出照旧打印旧表的 `managed:true` 清单。这个洞偏偏落在"规矩 2:翻 `managed:true` 前先试跑"所要求的动作上(提交链是唯一无验的一环),且源码旧注释"表是输入不是被审对象"正是它的设计理由,已就地推翻。取证 `--self-test` 51 例(成对正反 + 两面口径差异 + 解析器坏了必须大声失败)+ 镜像测试 12 例(含 runner 装车证明、"同一代码 managed 两侧结论不同"、以及 T12 的**取材面双向条件不变量** —— 索引表≠HEAD 表则两档结论必不同形、相同则必同形;变异回旧顺序只有 T12 红)。**收口进度不写进文档**:某模块是否 `managed:true` 一律按当次实测取(`--staged` 输出行即现值),理由与本门刚修的那类缺陷同源 —— 登记过期数字会替人做出"已经收口"的判断。编号说明:任务书原定 102,落地前已被 `check-glyph-arrow-icon` 占用,按"后来者改号"顺延 103。紧急跳过 `HUSKY_SKIP_ARCH_POLICY=1`。
- **Tailwind v3 消费端的 `/alpha` 修饰符由共享 preset 负责**(2026-09-25 立):v3 的 `bg-<色>/<透明度>` 语法要求把颜色拆成 rgb 分量,而 preset 若只给 `var(--color-x)` 单值,这一整类类名在 miniapp-taro 端**根本不产出 CSS** —— 页面"样式没生效"却零报错,而 web/extension(v4,原生支持)一切正常,即"手机上改了 web 没改"的又一成因。修法唯一:`packages/design-tokens/src/tailwind-alpha-plugin.js` 为每个颜色键补 `<key>-rgb` 三元组并 `addUtilities`,挂在该 preset 的 `plugins` 上,**禁止**在端内自拼 `rgba(...)` 绕过。**新增颜色档必须同时进这个插件**,否则新档又缺 `/alpha`。取证:真 miniapp 源码上选择器 73 → 91、REMOVED 0 / ADDED 18(含任意值形态 `bg-muted/[0.12]`);NativeWind 侧装 release 包 v21→v22 逐指标 A/B 六项读数完全相同(零回归)。
- **硬编码中文基线棘轮**(70):scan-hardcoded-zh.mjs(blocking,2026-09-22 接线)—— 扫 `apps/web/{app,src/components,src/hooks}` + `packages/{ui-react,shared}/src` + 2026-09-24 补的三端(`apps/mobile-rn/src`、`apps/extension/{entrypoints,src,lib}`、`apps/cli/src`),排除注释/metadata/useTranslations 行/admin/测试。**每文件命中数与基线额度比较**(`scripts/hardcoded-zh-baseline.json`):只拦"比基线更多",存量债(886 文件 / 13090 行)冻结、新增即拦;清理后 `--update-baseline` 下调额度(必须人工确认后再跑,禁止为过门而调高)。该脚本 2026-07-20 即存在但从未接入守门链,是"造好没装车"的第二个实例(第一个是同一批的 69)。`--staged` 由 runner 自动下发(runner 传 `--exit 1`,不带该参数时**永远 exit 0** —— 集成测试踩过),暂存集为空时回退全量防"空暂存恒绿"。**两处同日修**:① 14 例镜像测试自 §15「ROOT 由脚本自身位置推导」起 **13/14 恒红** —— 测试靠 `cwd` 定位夹具而脚本按定义忽略 cwd,14 例全在扫真仓(不是判据错,是调用方式失效);加 `--root <dir>` 显式测试通道(生产不带,语义不变)后 15/15 绿。② 命中判定漏剥**行尾 `//`** ⇒ PriceChart / TerminalTab / TerminalStatusIndicators 各多出 2/1/1 处假阳,把它们顶过基线额度、把本门钉成"谁碰这三个文件谁被拦";同时修 `'https://x/*'` 这类串内 `/*` 骗进块注释状态机的假绿。**遗留已消除(2026-09-25 实测复核)**：全量模式的取材口径**已改为判 HEAD blob** —— `scan-hardcoded-zh.mjs` 现有"磁盘/索引/HEAD 三种取材共用一份判据"(约 :204 行注释与实现)，实测 `node scripts/scan-hardcoded-zh.mjs` 全量**越线文件数 = 0**。本段此前写的"全量模式按共享工作树判 ⇒ 同一份 HEAD 代码干净检出报 1 个、本机工作树报 281 个"是**已修状态**，保留原话只为说明那一型为什么会发生；**下一个接手者不必再做这件事**，若要复核判据请跑上面那条命令看末行现值。（另记 `heal-worktree-tracked` 的刷新面仍在扩大，两件事互补不互替。）紧急跳过 `HUSKY_SKIP_HARDCODED_ZH_GUARD=1`
- **架构契约对账**(103):`scripts/check-architecture-policy.mjs`(blocking,2026-09-25 立)—— 与其余全部守门**方向相反**的一道:其余是"发现一类违规 → 写一条判据"(打地鼠),本门读 `config/architecture-policy.yaml` 这张**声明表**,从声明反查违规。四类红:**T1** 表与现实脱节(模块改名/目录搬走而表没跟上 —— 表一过期,依赖它的所有判断都在对着空气打分)/ **C1** 单文件行上限(阈值 6000,**取自 HEAD 实测最大文件 5258 行之上**,依据写在 yaml 注释里)/ **D1 D2** 依赖方向(包 import 端、或 `requires` 未声明就 import;以及反向按 rank 更深一层)/ **D3** 深导入(绕过 `public_entrypoints` 直接摸别的包内部 —— 它是"端内重复实现"的入口)。**渐进收口是设计前提**:存量模块一律 `managed: false` ⇒ 只报数不判红(立项实测全量 exit 0、报数 3 处、`managed:true` 0 个),翻 true 前先用 `--managed-trial <id>` 看条数,**不得为消红去改阈值或把不该对外的模块标成 exported**。行内豁免 `arch-exempt: <原因>`(须带原因)。口径同 70/77/83/98/101:全量判 **HEAD blob**、`--staged` 判索引 blob。**策略表自身的取材按档定向**(`policyFaceOrder`):全量档 HEAD 优先、**`--staged` 档索引优先**,两者都再降级到工作树;取不到判"无法判定",不冒红也不记绿。⚠️ 这里 2026-09-25 修掉一处**本门对自身改动的盲视**:原实现两个面都 HEAD 优先,于是"修改策略表自身"的那枚提交完全不进本门审查 —— 实测往索引版 `apps/cli.requires` 注入一条 `apps/api`(端应用 `exported:false`,T1 必判红)后全量与 `--staged` 双双 exit 0,而 `--staged` 输出照旧打印旧表的 `managed:true` 清单。这个洞偏偏落在"规矩 2:翻 `managed:true` 前先试跑"所要求的动作上(提交链是唯一无验的一环),且源码旧注释"表是输入不是被审对象"正是它的设计理由,已就地推翻。取证 `--self-test` 51 例(成对正反 + 两面口径差异 + 解析器坏了必须大声失败)+ 镜像测试 13 例(含 runner 装车证明、"同一代码 managed 两侧结论不同"、T12 的**纯函数+构造面**取材面证明、以及 T12b 的尺子反例;变异回旧顺序只有 T12 红)。**本门自身踩过的判据教训(勿再犯)**:这里曾写过一条"索引表≠HEAD 表 ⇒ 两档收口集合必异形"的条件断言,两条前提都不成立(改注释/requires 都会让表不同而集合不变;尺子若吞到行尾则两档因"扫描 N 文件"天然不等)⇒ 一支误红、一支无牙,已删。**证明取材面这类行为只能用纯函数+构造面**,不得依赖仓库瞬时状态。**收口进度不写进文档**:某模块是否 `managed:true` 一律按当次实测取(`--staged` 输出行即现值),理由与本门刚修的那类缺陷同源 —— 登记过期数字会替人做出"已经收口"的判断。编号说明:任务书原定 102,落地前已被 `check-glyph-arrow-icon` 占用,按"后来者改号"顺延 103。紧急跳过 `HUSKY_SKIP_ARCH_POLICY=1`。
- **架构契约对账**(103):`scripts/check-architecture-policy.mjs`(blocking,2026-09-25 立)—— 与其余全部守门**方向相反**的一道:其余是"发现一类违规 → 写一条判据"(打地鼠),本门读 `config/architecture-policy.yaml` 这张**声明表**,从声明反查违规。四类红:**T1** 表与现实脱节(模块改名/目录搬走而表没跟上 —— 表一过期,依赖它的所有判断都在对着空气打分)/ **C1** 单文件行上限(阈值 6000,**取自 HEAD 实测最大文件 5258 行之上**,依据写在 yaml 注释里)/ **D1 D2** 依赖方向(包 import 端、或 `requires` 未声明就 import;以及反向按 rank 更深一层)/ **D3** 深导入(绕过 `public_entrypoints` 直接摸别的包内部 —— 它是"端内重复实现"的入口)。**渐进收口是设计前提**:立项时 24 块一律 `managed: false`(只报数不判红),逐块翻正时才按判红口径问责。⚠️ "**存量一律 false**"描述的是**立项状态,不是现值** —— 现值一律按当次实测取,见下。翻 true 前先用 `--managed-trial <id>` 看条数,**不得为消红去改阈值或把不该对外的模块标成 exported**。行内豁免 `arch-exempt: <原因>`(须带原因)。口径同 70/77/83/98/101:全量判 **HEAD blob**、`--staged` 判索引 blob。**策略表自身的取材按档定向**(`policyFaceOrder`):全量档 HEAD 优先、**`--staged` 档索引优先**,两者都再降级到工作树;取不到判"无法判定",不冒红也不记绿。⚠️ 这里 2026-09-25 修掉一处**本门对自身改动的盲视**:原实现两个面都 HEAD 优先,于是"修改策略表自身"的那枚提交完全不进本门审查 —— 实测往索引版 `apps/cli.requires` 注入一条 `apps/api`(端应用 `exported:false`,T1 必判红)后全量与 `--staged` 双双 exit 0,而 `--staged` 输出照旧打印旧表的 `managed:true` 清单。这个洞偏偏落在"规矩 2:翻 `managed:true` 前先试跑"所要求的动作上(提交链是唯一无验的一环),且源码旧注释"表是输入不是被审对象"正是它的设计理由,已就地推翻。取证 `--self-test` 51 例(成对正反 + 两面口径差异 + 解析器坏了必须大声失败)+ 镜像测试 13 例(含 runner 装车证明、"同一代码 managed 两侧结论不同"、T12 的**纯函数+构造面**取材面证明、以及 T12b 的尺子反例;变异回旧顺序只有 T12 红)。**本门自身踩过的判据教训(勿再犯)**:这里曾写过一条"索引表≠HEAD 表 ⇒ 两档收口集合必异形"的条件断言,两条前提都不成立(改注释/requires 都会让表不同而集合不变;尺子若吞到行尾则两档因"扫描 N 文件"天然不等)⇒ 一支误红、一支无牙,已删。**证明取材面这类行为只能用纯函数+构造面**,不得依赖仓库瞬时状态。**收口进度不写进文档**:某模块是否 `managed:true` 一律按当次实测取(`--staged` 输出行即现值),理由与本门刚修的那类缺陷同源 —— 登记过期数字会替人做出"已经收口"的判断。编号说明:任务书原定 102,落地前已被 `check-glyph-arrow-icon` 占用,按"后来者改号"顺延 103。紧急跳过 `HUSKY_SKIP_ARCH_POLICY=1`。

> ⚠️ 本行为守门 103 的**立项原文**,其中"策略表自身按 HEAD→索引→工作树降级"已被 2026-09-25 同日的批量归并留成旧副本 —— 现行判据是**按档定向取材**(`policyFaceOrder`:全量 HEAD 优先 / `--staged` 索引优先),原文照抄会重新引入"改表那枚提交不被审"的盲视;现行条目见本节内"镜像测试 13 例"那一条,勿照本行执行。保留本行只为不丢行(§12)。

- **`merge-live-doc` 的第三种定性:同锚点 ⇒ 就地改写(2026-09-25 补,`--self-test` 13→16 例)**。上面两句判据只分"吃掉 vs 存活":容器短路管"旧行逐字活在新行里",Jaccard 管"两行很像"。**改行中段**时两者同时失效(旧行不逐字存活、长行大改后相似度掉到阈值下),于是正当的就地改写被判成"真丢失"并要人跑 `--apply` —— 而 `--apply` 会把旧行原样插回 ⇒ **同一件事新旧两行并存**,本节上方那对重复的"提交活文档前必须做…"登记行、以及守门 83 的三条同体行,制造路径之一就是这个。现规则:三份活文档都是「一件事一行」且行首有稳定锚点(`- **名称**` / `### 标题`),**同一锚点在"HEAD 缺失集"与"工作树独有集"里各只出现一次** ⇒ 判 `superseded`(不插回)。唯一性是生命线:锚点在任一侧数量 ≠ 1 就不猜,退回原判据(宁可多报交人工,也不能把"整段登记被删"洗成"他改写了")。取证两层:①`node scripts/merge-live-doc.mjs --self-test` 16 例(⑭改写⇒0 / ⑮删除⇒仍 lost / ⑯同锚点两条⇒不猜);②CLI 级装车证明 `scripts/tests/merge-live-doc-anchor.test.mjs`(3 例 A/B/C)—— 本文件顶层就是 CLI 且**没有 §22d `isDirectRun` 守卫**(`lib/live-doc-similarity.mjs` 就是为此被抽出来的),所以测试只能把脚本 spawn 起来验,不能 import 判据函数;C 例自证夹具确实"不像",防止 A 臂退化成容器短路的测试。**误判的代价不是"多一行报告",是逼人跑 `--apply`,而 `--apply` 造出的重复行下一次又被判成需要归并 —— 这是它自己的正反馈环。**

> ⚠️ 本行是守门 103 的**中间态副本**(2026-09-25 同日两次改写之间的一次),其中"取材面双向条件不变量"这条断言**已被删除** —— 它两条前提都不成立(改注释/requires 会让表不同而 managed 集合不变;尺子若吞到行尾则两档因"扫描 N 文件"天然不等),留着会教人把它当判据复原。现行是"镜像测试 13 例"那一条(T12 改用纯函数+构造面、新增 T12b 尺子反例、T8 改为自带构造的 false 侧)。勿照本行执行;保留只为不丢行(§12)。

- ⚠️ **上面那条 `/alpha` 判据目前到不了小程序产物**(2026-09-25 实测,登记于 PROJECT_PLAN O62附②):`apps/miniapp-taro/config/index.ts` 的 `tailwindcss: { enable: true, config: {} }` 让端内 `tailwind.config.ts`(content globs + `presets:[@ihui/design-tokens/tailwind-preset]` + 本插件)在真实构建中**从未被加载** —— 实测真实 build 后 dist 的 154 个 wxss 里,源码用到的 **687 个 Tailwind utility / 11,105 处用法有 0 个产出规则**,`.flex{` 与 preflight 指纹双双缺失;阳性对照是同一份 dist 里 `app.css` 手写类名 75/98 可查到规则,故不是检索姿势问题。**后果**:改这条配置等于让整端 11,105 处用法同时开始产出 CSS,而现状很可能是手写 CSS 已补偿过(自有类名 2,228 个),开启后好坏只能由微信开发者工具真机渲染判定 ⇒ 属未决决策项,不是已收口项。**同时提醒同源对账门的边界**:门 36/37/93/`check-miniapp-taro-style-parity` 全部只核源码与 token 源头,**没有一道看产物**;它们五道全绿而到端 CSS 为零,是可能且已发生的。新增跨端同源门时,请先问自己判的是"写的同源"还是"生效的同源"。**⚠️ 本条的"0 产出"现值已作废**(同上,`edb225c909` + `f17b5fbdda`):真产物落地率 740/771 = 96.0%,`app.css` 现有一个由 `sync-miniapp-tokens.mjs` 派生的 `@theme` 注册区;但"没有一道同源门看产物"这半句仍然成立 —— 看产物的是 `check-miniapp-css-landing.mjs`(手动/CI,未接提交链)。

- **prod-bundle 影子副本对账**:`scripts/check-prod-bundle-shadow.mjs`(2026-09-24 立;**2026-09-25 接进提交链 = guardian id `104`,blocking,紧急跳过 `HUSKY_SKIP_PROD_BUNDLE_SHADOW=1`**)—— 此前它只挂在 `pnpm check:all`,`grep -c check-prod-bundle-shadow scripts/guardian-runner.mjs` = 0,即"门存在、判据正确、没有任何调度器跑它",正是本仓最高频的**造好没装车**。§5e 那句"落进 `deploy/prod-bundle/` 的运维脚本必须同时落一份入库源,否则等于写进盲区"从此才有尺子。本门登记「入库源 ↔ 生产实际执行的那份」逐字节必须等值,判据:S0 入库源缺失(跟踪文件,任何检出都该在 ⇒ 判红,不是机器态)、S1 入库源必须真被 git 跟踪(不被跟踪就没有对账对象)、S2 运行副本必须真被忽略(否则登记表过期)、S3 两侧 sha1 全等,不等即红并点名两侧路径。**退出码按"机器态 vs 内容态"三态分流**:整目录 `deploy/prod-bundle/` 被 `.gitignore` 忽略 ⇒ 运行副本**只存在于部署机**,若在非部署机/CI/干净检出上判红,结局是逼人 `--no-verify` 连带 130+ 道门全废(§4/§12e 同型),故"缺运行副本"= **未判定,exit 0 但逐条喊出原因**(禁静默绿,也禁静默红);git 问不到 = exit 2 优先于 1。聚合抽成导出纯函数 `decide()`,机器态分流可用构造输入证明、不必真机改名。刻意**不挂 `stagedTriggers`**:会漂移那侧是被忽略的运行副本,结构上永不出现在暂存区,按 staged 收窄等于放过本门立门那一型。首跑抓到并入库的是数据库备份那一对:`deploy/prod-bundle/pg-backup{,-scheduler}.ps1`(整目录被忽略、全仓零入库源,而它每天 03:00 产出 94MB 生产库 dump)。本门**刻意没有改成转发壳**——把 runner 换成转发壳后重启服务,`pg_dump` 出现 3.5 分钟零输出挂死(旧 runner 同库同日 34 秒跑完),在解释清之前不动生产备份;因此两侧都是全文,靠本门钉住等值。自检 `--self-test`(例数以命令末行现测为准,含"入库源被摘线必红""缺运行副本计未判定不判红"成对正反例;**必须连跑两次**——该门曾 `--self-test` 第二次起恒 exit 2,只能跑一次的取证等于没取证)。
- **架构契约对账**(103):`scripts/check-architecture-policy.mjs`(blocking,2026-09-25 立)—— 与其余全部守门**方向相反**的一道:其余是"发现一类违规 → 写一条判据"(打地鼠),本门读 `config/architecture-policy.yaml` 这张**声明表**,从声明反查违规。四类红:**T1** 表与现实脱节(模块改名/目录搬走而表没跟上 —— 表一过期,依赖它的所有判断都在对着空气打分)/ **C1** 单文件行上限(阈值 6000,**取自 HEAD 实测最大文件 5258 行之上**,依据写在 yaml 注释里)/ **D1 D2** 依赖方向(包 import 端、或 `requires` 未声明就 import;以及反向按 rank 更深一层)/ **D3** 深导入(绕过 `public_entrypoints` 直接摸别的包内部 —— 它是"端内重复实现"的入口)。**渐进收口是设计前提**:存量模块一律 `managed: false` ⇒ 只报数不判红(立项实测全量 exit 0、报数 3 处、`managed:true` 0 个)(⚠️ 这几个数字是**立项状态、不是现值**;现值一律跑 `node scripts/check-architecture-policy.mjs` 看 `managed:true` 行),翻 true 前先用 `--managed-trial <id>` 看条数,**不得为消红去改阈值或把不该对外的模块标成 exported**。行内豁免 `arch-exempt: <原因>`(须带原因)。口径同 70/77/83/98/101:全量判 **HEAD blob**、`--staged` 判索引 blob,策略表自身按 HEAD→索引→工作树降级取且**退到工作树会大声提示**;取不到判"无法判定",不冒红也不记绿。取证 `--self-test` 51 例(成对正反 + 两面口径差异 + 解析器坏了必须大声失败)+ 镜像测试 10 例(含 runner 装车证明与"同一代码 managed 两侧结论不同"的判据有牙证明)。编号说明:任务书原定 102,落地前已被 `check-glyph-arrow-icon` 占用,按"后来者改号"顺延 103。紧急跳过 `HUSKY_SKIP_ARCH_POLICY=1`。
- **`merge-live-doc` 的第三种定性:同锚点 ⇒ 就地改写(2026-09-25 补,`--self-test` 13→16 例)**。上面两句判据只分"吃掉 vs 存活":容器短路管"旧行逐字活在新行里",Jaccard 管"两行很像"。**改行中段**时两者同时失效(旧行不逐字存活、长行大改后相似度掉到阈值下),于是正当的就地改写被判成"真丢失"并要人跑 `--apply` —— 而 `--apply` 会把旧行原样插回 ⇒ **同一件事新旧两行并存**,本节上方那对重复的"提交活文档前必须做…"登记行、以及守门 83 的三条同体行,制造路径之一就是这个。现规则:三份活文档都是「一件事一行」且行首有稳定锚点(`- **名称**` / `### 标题`),**同一锚点在"HEAD 缺失集"与"工作树独有集"里各只出现一次** ⇒ 判 `superseded`(不插回)。唯一性是生命线:锚点在任一侧数量 ≠ 1 就不猜,退回原判据(宁可多报交人工,也不能把"整段登记被删"洗成"他改写了")。取证两层:①`node scripts/merge-live-doc.mjs --self-test` 16 例(⑭改写⇒0 / ⑮删除⇒仍 lost / ⑯同锚点两条⇒不猜);②CLI 级装车证明 `scripts/tests/merge-live-doc-anchor.test.mjs`(3 例 A/B/C)—— 本文件顶层就是 CLI 且**没有 §22d `isDirectRun` 守卫**(`lib/live-doc-similarity.mjs` 就是为此被抽出来的),所以测试只能把脚本 spawn 起来验,不能 import 判据函数;C 例自证夹具确实"不像",防止 A 臂退化成容器短路的测试。**误判的代价不是"多一行报告",是逼人跑 `--apply`,而 `--apply` 造出的重复行下一次又被判成需要归并 —— 这是它自己的正反馈环。**
- **架构契约对账**(103):`scripts/check-architecture-policy.mjs`(blocking,2026-09-25 立)—— 与其余全部守门**方向相反**的一道:其余是"发现一类违规 → 写一条判据"(打地鼠),本门读 `config/architecture-policy.yaml` 这张**声明表**,从声明反查违规。四类红:**T1** 表与现实脱节(模块改名/目录搬走而表没跟上 —— 表一过期,依赖它的所有判断都在对着空气打分)/ **C1** 单文件行上限(阈值 6000,**取自 HEAD 实测最大文件 5258 行之上**,依据写在 yaml 注释里)/ **D1 D2** 依赖方向(包 import 端、或 `requires` 未声明就 import;以及反向按 rank 更深一层)/ **D3** 深导入(绕过 `public_entrypoints` 直接摸别的包内部 —— 它是"端内重复实现"的入口)。**渐进收口是设计前提**:存量模块一律 `managed: false` ⇒ 只报数不判红(立项实测全量 exit 0、报数 3 处、`managed:true` 0 个)(⚠️ 这几个数字是**立项状态、不是现值**;现值一律跑 `node scripts/check-architecture-policy.mjs` 看 `managed:true` 行),翻 true 前先用 `--managed-trial <id>` 看条数,**不得为消红去改阈值或把不该对外的模块标成 exported**。行内豁免 `arch-exempt: <原因>`(须带原因)。口径同 70/77/83/98/101:全量判 **HEAD blob**、`--staged` 判索引 blob。**策略表自身的取材按档定向**(`policyFaceOrder`):全量档 HEAD 优先、**`--staged` 档索引优先**,两者都再降级到工作树;取不到判"无法判定",不冒红也不记绿。⚠️ 这里 2026-09-25 修掉一处**本门对自身改动的盲视**:原实现两个面都 HEAD 优先,于是"修改策略表自身"的那枚提交完全不进本门审查 —— 实测往索引版 `apps/cli.requires` 注入一条 `apps/api`(端应用 `exported:false`,T1 必判红)后全量与 `--staged` 双双 exit 0,而 `--staged` 输出照旧打印旧表的 `managed:true` 清单。这个洞偏偏落在"规矩 2:翻 `managed:true` 前先试跑"所要求的动作上(提交链是唯一无验的一环),且源码旧注释"表是输入不是被审对象"正是它的设计理由,已就地推翻。取证 `--self-test` 51 例(成对正反 + 两面口径差异 + 解析器坏了必须大声失败)+ 镜像测试 12 例(含 runner 装车证明、"同一代码 managed 两侧结论不同"、以及 T12 的**取材面双向条件不变量** —— 索引表≠HEAD 表则两档结论必不同形、相同则必同形;变异回旧顺序只有 T12 红)。**收口进度不写进文档**:某模块是否 `managed:true` 一律按当次实测取(`--staged` 输出行即现值),理由与本门刚修的那类缺陷同源 —— 登记过期数字会替人做出"已经收口"的判断。编号说明:任务书原定 102,落地前已被 `check-glyph-arrow-icon` 占用,按"后来者改号"顺延 103。紧急跳过 `HUSKY_SKIP_ARCH_POLICY=1`。

<!-- ⚠️ 本行与上方同名条目(第 1339 行)同体,是同日两次改写被并集留下的旧副本;现行以第 1339 行为准。其中"10 例/12 例"之类的镜像测试例数、以及任何具体收口块数,都按当次实测取,勿照本行派单。保留本行只为不丢行(AGENTS §12)。 -->- **架构契约对账**(103):`scripts/check-architecture-policy.mjs`(blocking,2026-09-25 立)—— 与其余全部守门**方向相反**的一道:其余是"发现一类违规 → 写一条判据"(打地鼠),本门读 `config/architecture-policy.yaml` 这张**声明表**,从声明反查违规。四类红:**T1** 表与现实脱节(模块改名/目录搬走而表没跟上 —— 表一过期,依赖它的所有判断都在对着空气打分)/ **C1** 单文件行上限(阈值 6000,**取自 HEAD 实测最大文件 5258 行之上**,依据写在 yaml 注释里)/ **D1 D2** 依赖方向(包 import 端、或 `requires` 未声明就 import;以及反向按 rank 更深一层)/ **D3** 深导入(绕过 `public_entrypoints` 直接摸别的包内部 —— 它是"端内重复实现"的入口)。**渐进收口是设计前提**:存量模块一律 `managed: false` ⇒ 只报数不判红(立项实测全量 exit 0、报数 3 处、`managed:true` 0 个)(⚠️ 这几个数字是**立项状态、不是现值**;现值一律跑 `node scripts/check-architecture-policy.mjs` 看 `managed:true` 行),翻 true 前先用 `--managed-trial <id>` 看条数,**不得为消红去改阈值或把不该对外的模块标成 exported**。行内豁免 `arch-exempt: <原因>`(须带原因)。口径同 70/77/83/98/101:全量判 **HEAD blob**、`--staged` 判索引 blob。**策略表自身的取材按档定向**(`policyFaceOrder`):全量档 HEAD 优先、**`--staged` 档索引优先**,两者都再降级到工作树;取不到判"无法判定",不冒红也不记绿。⚠️ 这里 2026-09-25 修掉一处**本门对自身改动的盲视**:原实现两个面都 HEAD 优先,于是"修改策略表自身"的那枚提交完全不进本门审查 —— 实测往索引版 `apps/cli.requires` 注入一条 `apps/api`(端应用 `exported:false`,T1 必判红)后全量与 `--staged` 双双 exit 0,而 `--staged` 输出照旧打印旧表的 `managed:true` 清单。这个洞偏偏落在"规矩 2:翻 `managed:true` 前先试跑"所要求的动作上(提交链是唯一无验的一环),且源码旧注释"表是输入不是被审对象"正是它的设计理由,已就地推翻。取证 `--self-test` 51 例(成对正反 + 两面口径差异 + 解析器坏了必须大声失败)+ 镜像测试 13 例(含 runner 装车证明、"同一代码 managed 两侧结论不同"、T12 的**纯函数+构造面**取材面证明、以及 T12b 的尺子反例;变异回旧顺序只有 T12 红)。**本门自身踩过的判据教训(勿再犯)**:这里曾写过一条"索引表≠HEAD 表 ⇒ 两档收口集合必异形"的条件断言,两条前提都不成立(改注释/requires 都会让表不同而集合不变;尺子若吞到行尾则两档因"扫描 N 文件"天然不等)⇒ 一支误红、一支无牙,已删。**证明取材面这类行为只能用纯函数+构造面**,不得依赖仓库瞬时状态。**收口进度不写进文档**:某模块是否 `managed:true` 一律按当次实测取(`--staged` 输出行即现值),理由与本门刚修的那类缺陷同源 —— 登记过期数字会替人做出"已经收口"的判断。编号说明:任务书原定 102,落地前已被 `check-glyph-arrow-icon` 占用,按"后来者改号"顺延 103。紧急跳过 `HUSKY_SKIP_ARCH_POLICY=1`。

- ⚠️ **中文出参会被控制台码页吃掉(2026-09-25 实测,和上一条同为"码页"但方向相反)**:上面说中文当 **argv** 传出去无损;但 node 往 stdout 打的**路径**含中文(`密钥/...`)时,PowerShell 按 **GBK 码页**解码 ⇒ 拿回的是乱码字符串 ⇒ `Test-Path` 判"不存在" ⇒ 脚本静默退回兜底凭据,表现得像"口令文件没建"。**凡"把一个值读回来继续用"的场合(路径/文件名/URL),调 node 前必须显式 `[Console]::OutputEncoding = UTF8` 并在 finally 还原**;只在控制台显示乱码的是"输出面",这条不成立 —— 值一旦参与判据就不是显示问题。同类修法也适用于 §5e 说的"多行中文正文走 --message-file"。
- **凭据目录现状**(2026-09-25 起):`db-backup/ihui-backup.txt` = 只读备份账号 `beifen` 的口令;`IHUI生产账号/pgsql管理员-postgres-*.txt` = 该库 `postgres` 的口令(此前**从未设过**,详见 `deploy/win/ihui-pg-backup-role.sql` 顶部的执行记录)。两份都在仓库外、都在 §5d 权威目录里,都不进日志/聊天/提交。
- **"服务身份能不能跑到这一步"要用它那份环境验,不得用我的终端验(2026-09-25 实测)**:上面那条讲"哪份文件",这条讲**那份文件跑在谁的身份下**。nssm 服务全是 `LocalSystem`,它读的是**机器级 PATH**,而本机机器级 PATH 里那串 `D:\nodejs\` 是**死路径**(目录根本不存在),node 真身 `D:\DevEnv\runtimes\node\node.exe` 只挂在**用户级 PATH** —— 于是 `Get-Command node.exe` 在人手里命中、在服务手里落空,凡是"取凭据/发通知要先找 node"的分支会**静默跳过并回落兜底档**,现象是"手动跑是对的、自动跑用的是旧账号",而日志只有一行 WARN。处置口径:① 取可执行程序一律走 `Resolve-NodeExe`(`deploy/win/ihui-deploy.ps1:120-131`,绝对路径候选已内建),不得只写 `Get-Command`;② 兜底全落空必须**喊出来**(Write-Host WARN/ERROR + 非零),严禁 `if (Get-Command …) { … }` 无 else 静默降级 —— 那与 §5e"失败必须响"是同一条禁令;③ 验证要剥掉环境再跑**服务真正执行的那份副本**:`$env:Path` 去掉含 `node`/`DevEnv` 的条目后跑 `deploy/prod-bundle/*.ps1`,看输出行报的是哪个角色,而不是在我终端里跑 `deploy/win/*`。实例与取证见 `deploy/win/ihui-pg-backup.ps1` 的凭据段注释与 PLAN 第四十九批·续十。
  - **"点名"必须连结论行的缩进续行一起看(2026-09-25 实战补)**:守门 84/30c 这类门把**结论**与**定位**拆成两行
    (`❌ 检出 1 个文件的暂存内容等于其历史提交版本:` 一行不带路径,下一缩进行才给路径)。`findingLines()` 原为
    逐行匹配 `FINDING_LINE_RE`,于是第二行不算结论行 ⇒ 看不见被点名的文件 ⇒ 判 `not-ours` ⇒ **放行跳门** ——
    方向正好反了:**门说得越具体,铰链越松**,而这条铰链决定的是"能不能绕过 130+ 道门"。现并入结论行下方
    ≤3 行的缩进续行;取证是自检 A2b(两行形态必判 mine)/ A3b(续行点的是别人的文件仍判 not-ours),
    变异证明 = 把续行上限改回 0 即退回旧误判。**判据失效的方向必须是"多要一次定向说明",绝不能是"多放一次跳门"。**

  - **认领升级为租约(2026-09-25 立,守门 109 `check-task-claims.mjs --check-gate`)**:裸 `（进行中）` 只是文本 —— 没有持有者、没有时间戳、不判年龄,于是 agent 一死标记就永久留在 HEAD,下一个派单会话按上一条扫描**要么看不见那件活、要么把它当"别人正在做"而按住不做**(本仓记录里已反复出现,不需要构造)。**新格式**:`- [ ]（进行中@YYYY-MM-DD/持有者短名）`,三要素齐备才算一次认领;裸 `（进行中）` 仍被解析但**只计数不判红**(实测存量 36 条全是裸标记,当场判红就是一台恒红门)。年龄 > 72h(`IHUI_CLAIM_LEASE_TTL_HOURS` / `--ttl-hours` 可调)判红并点名行号与持有者;写了 `@日期` 却缺 `/持有者` 同样判红 —— 半个租约比没有租约更危险,它看起来像被管过。同一行既"进行中"又 `[x]` 判红(协议自相矛盾 = 释放失败的现场)。**门绝不自动摘除任何人的认领**(那属 §16 越权),出路只有三条:续租(改日期)/ 完成翻勾并摘牌 / 显式让渡给某持有者。派单前除扫进行中项,还要跑一次三轴基线自检 `node scripts/check-baseline-freshness.mjs --preflight`(见 §守门脚本速查)。
- **单一真相源**:design-tokens 在 `packages/design-tokens/src/styles/tokens.css`(`@theme` + `:root` + `.dark`)。miniapp-taro 的 `app.css :root/.dark` 由 `apps/miniapp-taro/scripts/sync-design-tokens.mjs` 自动同步(**禁止手改 app.css 的 token 块**;改 token 改源头 + 跑同步,该脚本是端内脚本,根 `scripts/` 下没有同名文件,`node scripts/sync-design-tokens.mjs` 会报模块找不到)。另有 `scripts/check-miniapp-taro-design-tokens.mjs` 与 `scripts/check-miniapp-tokens-sync.mjs`(后者为 guardian-runner 第 36 项实际调用项)校验同步一致性。
- **发给 provider 的 function parameters 必须由校验面单向投影**(2026-09-25 立,A13):统一出口是 `@ihui/types` 的 `projectToolInputSchema` / `toolsToProviderSchema` (`packages/types/src/schema-projection.ts`),**禁止端内手搓 `parameters` 对象或自拼第二份 JSON Schema** —— "运行时怎么校验"与"模型被告知怎么填"必须是同一份描述的两个投影,否则两边各自漂移而 typecheck 全绿。等价性由 158/158 个真工具对象的同瞬间 A/B 证明(线字节 + 属性名集合 + required 集合三项全等)。
- **候选序解析器必须同时提供「出处」出口**(2026-09-25 立,A19):`scripts/lib/key-dir.mjs` 的 `resolveKeyDirDetailed()` 返回 `{path, triedCandidates[], winnerIndex}` 三态 —— 一个根都没命中(盘没挂上 / 根缺失 = **无法判定**,不得报成「凭据失效」)/ 根在而子目录不在(先建目录再放口令)/ 命中。`resolveKeyDir()` 的签名与行为**一字未动**(它是 12+ 个脚本的依赖面,detailed 版是它的超集,旧版由新版降格投影而来,等价性由单测钉住)。新增任何候选序解析器都要照此配对,调用方**禁止再自己抄一份 F→D→E→G→C 候选表** —— 那正是本节记过的「读不到被下游报成失效」的成因。**但这条是散文约束、没有门**:判「有没有真的回答出处」结构上判不了,为它立门只会逼人写空调用去应付(如实登记,别误以为这里有尺子)。
- **架构契约对账**(103):`scripts/check-architecture-policy.mjs`(blocking,2026-09-25 立)—— 与其余全部守门**方向相反**的一道:其余是"发现一类违规 → 写一条判据"(打地鼠),本门读 `config/architecture-policy.yaml` 这张**声明表**,从声明反查违规。四类红:**T1** 表与现实脱节(模块改名/目录搬走而表没跟上 —— 表一过期,依赖它的所有判断都在对着空气打分)/ **C1** 单文件行上限(阈值 6000,**取自 HEAD 实测最大文件 5258 行之上**,依据写在 yaml 注释里)/ **D1 D2** 依赖方向(包 import 端、或 `requires` 未声明就 import;以及反向按 rank 更深一层)/ **D3** 深导入(绕过 `public_entrypoints` 直接摸别的包内部 —— 它是"端内重复实现"的入口)。**渐进收口是设计前提**:立项时 24 块一律 `managed: false`(只报数不判红),逐块翻正时才按判红口径问责。⚠️ "**存量一律 false**"描述的是**立项状态,不是现值** —— 现值一律按当次实测取,见下。翻 true 前先用 `--managed-trial <id>` 看条数,**不得为消红去改阈值或把不该对外的模块标成 exported**。行内豁免 `arch-exempt: <原因>`(须带原因)。口径同 70/77/83/98/101:全量判 **HEAD blob**、`--staged` 判索引 blob。**策略表自身的取材按档定向**(`policyFaceOrder`):全量档 HEAD 优先、**`--staged` 档索引优先**,两者都再降级到工作树;取不到判"无法判定",不冒红也不记绿。⚠️ 这里 2026-09-25 修掉一处**本门对自身改动的盲视**:原实现两个面都 HEAD 优先,于是"修改策略表自身"的那枚提交完全不进本门审查 —— 实测往索引版 `apps/cli.requires` 注入一条 `apps/api`(端应用 `exported:false`,T1 必判红)后全量与 `--staged` 双双 exit 0,而 `--staged` 输出照旧打印旧表的 `managed:true` 清单。这个洞偏偏落在"规矩 2:翻 `managed:true` 前先试跑"所要求的动作上(提交链是唯一无验的一环),且源码旧注释"表是输入不是被审对象"正是它的设计理由,已就地推翻。取证 `--self-test` 51 例(成对正反 + 两面口径差异 + 解析器坏了必须大声失败)+ 镜像测试 13 例(含 runner 装车证明、"同一代码 managed 两侧结论不同"、T12 的**纯函数+构造面**取材面证明、以及 T12b 的尺子反例;变异回旧顺序只有 T12 红)。**本门自身踩过的判据教训(勿再犯)**:这里曾写过一条"索引表≠HEAD 表 ⇒ 两档收口集合必异形"的条件断言,两条前提都不成立(改注释/requires 都会让表不同而集合不变;尺子若吞到行尾则两档因"扫描 N 文件"天然不等)⇒ 一支误红、一支无牙,已删。**证明取材面这类行为只能用纯函数+构造面**,不得依赖仓库瞬时状态。**收口进度不写进文档**:某模块是否 `managed:true` 一律按当次实测取(`--staged` 输出行即现值),理由与本门刚修的那类缺陷同源 —— 登记过期数字会替人做出"已经收口"的判断。编号说明:任务书原定 102,落地前已被 `check-glyph-arrow-icon` 占用,按"后来者改号"顺延 103。紧急跳过 `HUSKY_SKIP_ARCH_POLICY=1`。 **E1/E2 两条同日补**:E1 公开入口存在性对账 —— `public_entrypoints` 声明的入口必须在**被审判的同一取材面**里解析得到(HEAD 面实测 75 条声明入口全部在位 ⇒ 本条可即时问责,不像 E2 那样带存量)/ E2 契约工件齐备性 —— 纳管模块必须有策略表字段点名的自述与契约文件(**清单取表里的字段,不在代码里硬编码第二份名字**),默认档**只报数**:HEAD 实测 16 块未齐备,当场判红就是一台恒红门(§12e 同型),要问责跑 `--strict`。退出码口径:判红只算「C1/T1 全仓即时 + 已收口模块的契约违规」,E1/E2 的红仅按 `managed:true` 问责。**同批给策略表配了只读出口** `scripts/module-context.mjs`(pnpm `module:context`):按模块 id 打印「是否纳管 / 声明依赖 / 本模块契约文件 / 依赖模块契约文件 / 公开入口 / 边界条款」,`--files` 直接吐派单用的文件清单 —— §11 那张「受影响文件」格过去全靠派单人手写,而 §11 自己就记着这张清单写错会造成越权/污染。未知模块 id **exit 2 并点名**(实测 `node scripts/module-context.mjs nosuch-module-xyz` → rc 2);`--json` 必须可 `JSON.parse`;E2 未齐备既打印 missing **也进退出码** —— 上游那一版只打印不判,这一格我们补上了。
- **第三方来源台账对账**(guardian id **107**,blocking,紧急跳过 `HUSKY_SKIP_PROVENANCE_LEDGER=1`):`scripts/provenance-ledger.mjs` 把"哪些内容不是我们写的"变成机器可查事实 —— 三本账(嵌入 / 复制 / 覆盖)+ 一本机制来源账,数据在 `config/third-party-provenance/*.json`,许可原文按内容哈希自证(读盘不联网)。四类红:P1 `roots` 指向不存在的路径 / P2 盘上有 vendored 目录或第三方归属头而台账无条目 / P3 已登记条目 hash 漂移 / P4 revision 不可考却没显式 `null` + 说明 + P8 **归属反噬**(2026-09-25 同日补):已登记的第三方内容上不得出现我方水印横幅/版权主张,出现即红 —— 起因是实测到 `apps/web/public/pdfjs/pdf.worker.min.mjs` 第 1 行是我们的横幅(等于把 Mozilla 的作品声明成我们的)。水印层的排除源唯一 = 台账 roots(经 `scripts/lib/third-party-roots.mjs`,`watermark.mjs` 与 `check-watermark-coverage.mjs` 两个消费者共用),**这些文件不是改成看不见,而是改由 P8 审计**。口径同 70/77/83/98/101/103(全量判 HEAD blob、`--staged` 判索引、取不到判"无法判定")。**刻意不挂 `stagedTriggers`**:新登记一条 vendored 内容可以发生在任何路径。立项两记否证值得记:(a) 规格称 `vendor/` 是唯一 vendored 件 —— 实测另有 PDF.js worker、`gradlew{,.bat}`、lucide svg 三处未登记;(b) §5c 的零宽水印门禁与"许可原文不得改动格式"**结构冲突**,那些文件至今幸免只因 `SKIP_DIRS` 里恰有一条 `'vendor'`,是巧合不是设计 —— 处置写进台账注释,**不得用豁免遮**。
  - **可审计锚点必须受版本控制(P5 的 specFile 由"工作树存在"改成"按被审面存在",2026-09-25 立,G-173 换来的)**:旧判据是裸 `existsSync(join(root, e.specFile))`,而台账 `$schemaNote` 当时明写"specFile 允许是 gitignore 的临时规格"——**"允许临时"与"blocking 判存在"结构互斥**:gitignored 路径不进任何检出,而本仓 post-commit 的 `--auto-clean` 自己会清掉它,结果就是作者机常绿、别的检出上**每次提交都被逼 `--no-verify`**(实测 145 项里它一项红)。**通用规矩:凡被 blocking 门要求"必须存在的文件",其路径要先过面判据(`reader.has`)或至少过 `git check-ignore`;写进 `.ihui-agent/` 的引用不叫证据,叫机器-local 巧合。** 取证是自检 `8e` 两条成对用例 —— 夹具故意让锚点**在工作树上存在但未提交**,新判据必须红;旧写法在这里报绿,所以这条同时是"新判据有牙"的证明,不许用"改文案"把它退化成恒红。数据侧 `specFile` 已重指到受版本控制的 `AGENTS.md` 本条,并用 `specNote` 如实声明"上游研究笔记未幸存、内容无从找回,**不得把该字段读成原文仍在**"——恢复不了的东西不要伪造。

- **豁免到期账**(guardian id **108**,blocking,紧急跳过 `HUSKY_SKIP_EXEMPTION_EXPIRY=1`):`scripts/check-exemption-expiry.mjs` 钉"豁免只有出生、没有死亡"。实测全仓 **12 类**行内豁免 / 252 处 / 107 文件,**带真到期日 0 处**(这三个数是**立项读数、不是现值** —— 现值一律跑 `node scripts/check-exemption-expiry.mjs --json` 看 `totals`)。2026-09-25 补第 13 族 **`back-label-exempt`**(守门 102 GA4 的合法例外通道),在 `FAMILY_LIFETIME_DAYS` 给它 **365 天**而非同门 `glyph-arrow-exempt` 的 30:它是**结构性定性**(该位置的「返回」是按钮文案而非导航箭头),位置性质不随时间改变;短周期到期只会逼人删标记,删了又被 GA4 判红,**两道门互咬**。E1 新增不带到期日 ⇒ 红;E2 已过期仍生效 ⇒ 红;E3 lint 抑制面(`eslint-disable` / `@ts-ignore`)只报数。**三重防恒红**:存量进 `scripts/exemption-expiry-baseline.json` 只报数、E1 锚点 = 该文件该族在 **HEAD 自身的存量数**(棘轮)、`grandfatherUntil` 前不追存量 —— 与改动无关的 blocking 红只会逼人 `--no-verify` 连带废掉全部守门。到期**只判红只点名,绝不自动摘除任何人的豁免**。副产品:第 12 族 `r3-cta-exempt` 3 处是从来没生效过的悬空豁免(守门 83 只认 `r5-cta-exempt`),归属该门持有者。
- **任务认领租约对账**(guardian id **109**,blocking,`stagedTriggers=PROJECT_PLAN.md`,紧急跳过 `HUSKY_SKIP_TASK_CLAIM_LEASE=1`):`scripts/check-task-claims.mjs --check-gate` 把 §1 的认领标记升级成带归属与寿命的锁(见 §1 第二条)。CL1 年龄超阈值(默认 72h)/ CL2 有 `@日期` 无 `/持有者` / CL3 同一行**在勾选框位置**既已 `[x]` 又挂着租约形态 `（进行中@…）`(正文叙述里的 `[x]` 不算 —— 判据把叙述当状态,就会替另一会话持有的行造出恒红)。**CL3 自 2026-09-26 起套「HEAD 自身存量」棘轮**:锚点是矛盾行的**行文本多重集**,不是"前 N 条"计数 —— 计数式切法会让新塞进文档前面的一颗顶掉一颗旧残留的名额,两者都得绿(镜像测试 T17 当场抓到第一版实现就是这样)。归并器"翻勾不摘牌"已在**生产者侧**收口(`scripts/plan-tasks-merge.mjs` 的 `rewriteFork()` 翻勾前先摘牌,配 T6 与变异对照),HEAD 里那批残留(逐条带 `**[归并]**` 出处标记)按存量只报数,**换个文案的新增一律判红**;`--worktree` / `--plan` 两条人工与取证面**不套棘轮**,仍全量判。锚点取不到 ⇒ 显式喊"未判定"并列出条数,**不冒绿**。棘轮锚点取 HEAD 而不是手工清单,是为了让它只会自己收紧:谁把残留清进 HEAD,红线就跟着降。**向后兼容第一**:存量裸 `（进行中）` 一律只计数不判红,否则上线当天即恒红门。门**绝不自动摘除任何人的认领**(§16 越权)。
- **产物预算对账**(guardian id **110**,**warn**,紧急跳过 `HUSKY_SKIP_ARTIFACT_BUDGET=1`):`scripts/check-artifact-budget.mjs` 是全仓第一次去量**交付出去的产物**而非源码(AGENTS 末段自己写过"同源门全部只核源码,没有一道看产物")。为什么 warn:产物在不在本机是**机器状态**,提交者结构上满足不了 ⇒ blocking 就是恒红门;问责放 CI(build 之后必有产物),镜像测试钉"一旦接提交链就必须同时有 CI 调用点"。缺产物不判红也不静默绿:exit 0 + 逐条"未判定:<原因>"。主包口径由构造面正反例钉死(同样字节放分包目录 ⇒ 绿,放主包 ⇒ 红)。
- **工具契约声明对账**(guardian id **111**,blocking,`stagedTriggers=apps/cli/src/tools/`,紧急跳过 `HUSKY_SKIP_TOOL_CONTRACT_DECLARED=1`):`scripts/check-tool-contract-declared.mjs` 守 A13 第一阶段立下的规矩 —— 一次可被模型调用的能力必须在实现旁边有**契约声明**(副作用范围 / 权限 / 结果预算),因为下游三个面(发给 provider 的 function parameters、运行时入参校验、批准与预算判定)现在都从**同一份描述**投影 (`packages/types/src/{tool-contract,schema-projection}.ts`)。没有声明不是"少一条元数据",而是那一格退回"三个面各写各的" —— 本仓 8 道"声明↔实现对账"同族门存在的理由就是它。**存量 104 个走棘轮**(锚点 = 该文件 HEAD 自身违规数,老文件不会被别人欠的债钉红)。**缺省语义本门故意不管**:翻成"缺省即不可信"是第二阶段,前置是先把 `--flip-audit` 点名的 16 个"连 dangerLevel 都没写"的工具逐个补档(`list_dir`/`grep`/`glob`/`run_command` 等 14 个 builtins/terminal/memory 族 + `mcp-runtime.ts` 两个),否则用户侧表现是"昨天能跑今天全要批准" —— 那不是收紧安全,是制造事故。
- **测试收集存续性对账**(guardian id **114**,**warn**,紧急跳过 `HUSKY_SKIP_TEST_COLLECTION=1`):`scripts/check-test-collection-runs.mjs` 判的是"**收集阶段**就失败的套件数必须为 0"。立因是实测到的结构性失明:`apps/mobile-rn` 一度 `Test Files 15 failed | 37 passed`,其中 14 个是 transform / import 解析期失败,内含 131 条 it 声明(展开 137 枚用例)**一条都没跑** —— 端内约三分之一覆盖被静默削掉,而提交链里 130+ 道门没有一道跑 vitest(`check-staged-typecheck` 走 tsc,结构上看不见解析期失败),CI 会红但本机可以永远看着绿。**定级 warn 而非 blocking 是设计前提**:本门判的是端内测试能否被收集,一次提交完全可能与它无关,与改动无关的恒红门只会逼人 `--no-verify` 并连带废掉全部门(§12e 同型);要问责跑 `--strict`(CI / 巡检)。**三条不可动摇的口径**:① 零测试文件不判绿(空扫就是它要防的同一型故障);② 断言失败与收集失败绝不混计 —— `Test Files 1 failed` 两种故障都会出现,故文件级计数不能作判据,只认 `Failed Suites` 区块与带方括号的套件级形态;③ 只接 vitest,pytest 的收集期故障是另一套语法(`ERROR … - ModuleNotFoundError` + `no tests ran`),照抄判据必然空转,非 vitest 入口一律判"未判定"并写明原因,不冒绿。`stagedTriggers` 取 `apps/mobile-rn/` + `packages/` —— 能打断这份收集的只有 RN 端自身与它吃的共享包。
- 三件同批事实:以上五道门都按 §12 的"活文档与注册表单写者"由主会话统一接线(`guardian-runner.mjs` / `package.json` / 本节 / README 守门章节同枚提交),各实现票按任务书**没有**碰过这些共享注册文件 —— 并行改注册表必然互相覆盖注册块(门 93 记过同型事故)。

- **小程序端「源 ↔ 派生产物」存续性对账**(105):`scripts/check-miniapp-generated.mjs`(**blocking;2026-09-25 由 warn 升档,前置条件实测达成**)—— 管四件"生成器造好了但没人跑"的派生产物:i18n 离线包 `remote-locales.gen.ts`、`static/images/icons/*.svg`、`LineIcon/icons.ts`、`assets/tabbar/*.png`。四条判据:**G1** 源里有而产物没有 ⇒ 判红(端上会缺词、图片 404 且**不报错**);**G2** 产物里有而源已无 ⇒ 默认只报数,`--strict` 才判红(删产物属 §7 删除安全);**G3** `icons.ts` 有键而 `icons/` 无同名 svg ⇒ **永不判红**(图标源在"图片全量外置 CDN"那轮被清空,判红等于造一道恒红门);**G4** 模板字符串拼出的动态路径结构上判不了 ⇒ 如实计数,绝不静默成"看起来全绿"。取材面同 70/77/83/98/101:全量判 HEAD blob、`--staged` 判索引 blob、`--worktree` 只作人工与 dev 链逃生舱;取不到 ⇒ **exit 2「无法判定」**。定级 warn 的实证理由:立项当天全量面就有 8 处 G1 + 9 处孤儿在红,挂 blocking 会让每次提交都被逼 `--no-verify`(§12e 那型);**升 blocking 的前置条件是 G1 存量清零 —— 该前置已于 2026-09-25 实测达成并升档**(全量面 `未发现漏生成`、exit 0;R2 孤儿与 G3 图标按设计**不是** blocking 判据,默认只报数,所以升档不新增任何恒红面)。升档的直接起因是本会话自己改了 i18n 键名(`policyForbidsExecution` → `policySkipsApproval`)却没重生成离线包,门判出 4 处 B2 却因为是 warn 而**没拦住任何提交**,于是静默上线一版"小程序取不到该词"的产物 —— **warn 的代价不是"少一道闸",是"门判对了也没人被打断"**。修法单命令、不需要人工裁:`pnpm --filter @ihui/miniapp-taro gen:i18n`。取证 `--self-test`(15 例,成对正反例、零副作用);紧急跳过 `HUSKY_SKIP_MINIAPP_GENERATED=1`;它同时被 `dev-weapp.mjs` 消费 —— 判据要在有人跑它的那一刻才成立。
  - **为什么"产物存在"不等于"能力在线"(本门立论,值得推广到别的生成器)**:这四个生成器里此前**只有 i18n 那个进了构建链**,其余三个既无构建入口也无门禁 ⇒ "造好没装车"只能靠人记得手动跑;而 dev 链不跑 i18n 生成器,意味着**开发时离线语言包常年是旧的**,只有 release 才被发现。同类盲区见守门 64(适配器接线)/ 70(硬编码中文)/ 81(邮件通道):**判据必须在真跑它的那一刻才成立,否则等于没有。**
- ⚠️ **中文出参会被控制台码页吃掉(2026-09-25 实测,和上一条同为"码页"但方向相反)**:上面说中文当 **argv** 传出去无损;但 node 往 stdout 打的**路径**含中文(`密钥/...`)时,PowerShell 按 **GBK 码页**解码 ⇒ 拿回的是乱码字符串 ⇒ `Test-Path` 判"不存在" ⇒ 脚本静默退回兜底凭据,表现得像"口令文件没建"。**凡"把一个值读回来继续用"的场合(路径/文件名/URL),调 node 前必须显式 `[Console]::OutputEncoding = UTF8` 并在 finally 还原**;只在控制台显示乱码的是"输出面",这条不成立 —— 值一旦参与判据就不是显示问题。同类修法也适用于 §5e 说的"多行中文正文走 --message-file"。
- **凭据目录现状**(2026-09-25 起):`db-backup/ihui-backup.txt` = 只读备份账号 `beifen` 的口令;`IHUI生产账号/pgsql管理员-postgres-*.txt` = 该库 `postgres` 的口令(此前**从未设过**,详见 `deploy/win/ihui-pg-backup-role.sql` 顶部的执行记录)。两份都在仓库外、都在 §5d 权威目录里,都不进日志/聊天/提交。
- **发给 provider 的 function parameters 必须由校验面单向投影**(2026-09-25 立,A13):统一出口是 `@ihui/types` 的 `projectToolInputSchema` / `toolsToProviderSchema` (`packages/types/src/schema-projection.ts`),**禁止端内手搓 `parameters` 对象或自拼第二份 JSON Schema** —— "运行时怎么校验"与"模型被告知怎么填"必须是同一份描述的两个投影,否则两边各自漂移而 typecheck 全绿。等价性由 158/158 个真工具对象的同瞬间 A/B 证明(线字节 + 属性名集合 + required 集合三项全等)。 **⚠️ 那句"158/158"当场不可复核,现已换成可重跑判据(2026-09-25)**:158 出自提交 `dd5142f2255` 的一次性 A/B 脚本,而那个脚本**没入库**、**口径也不同** —— 它按 `文件#name` 计数,把 re-export 的同一工具重复计了 56 次,所以 158 ≠ 门 111 的 104。更糟的是那条 A/B **今天再跑仍报 158/158,而它是恒真式**:它比的"旧侧" `toolsToProviderSchema` 在 `dd5142f2255` 之后自己就改成调投影器了,等于拿新实现和它自己比 ⇒ "已证明"三个字当时成立、事后失效,而账面什么都看不出来。现在这句话的唯一入库载体是 `apps/cli/tests/a13-projection-equivalence.test.ts`:旧侧取 `git show dd5142f2255^` 的**历史源码逐字快照**(`tests/fixtures/toJsonProperty.legacy.ts`,头注禁止演进),断言①门数到的工具集合与运行时枚举到的**逐名相等**、②逐工具三项全等(线字节 / properties 键集 / required 集)、③两条变异对照各红在对应判据、④枚举到 0 个工具判死不判绿。**登记一条已知局限**:运行时侧读磁盘而门侧判 HEAD ⇒ 他人在飞新增的真工具会让"逐名相等"点名它,红可能来自别人未提交的文件(§77/§83 同型)。
- **压缩分母对账**(112):`scripts/check-compaction-denominator.mjs`(blocking,2026-09-25 立)—— 钉的是 A30 那条实测缺口:provider 的 context window 是 **input 与 output 共享的同一个窗口**,所以"什么时候该压缩"的分母必须**先扣掉本轮留给输出的预留**;不扣就是拿"占满整个共享窗口的 88%"当阈值,而同一窗口里模型还要生成回复 —— 故障形态是**越接近上限越容易炸,现象却写成"压缩判过了还是 400/overflow",极难归因到分母**。立门前先确认"没有等价实现"再确认"没有这个名字"(两步都跑:本仓实测 `grep -rniE "outputReserve|effectiveContextWindow" apps/cli/src packages/context-compaction/src` = 0)。唯一出口 `effectiveContextWindow({contextWindow, maxOutputTokens, buffer, enabled})`,预留封顶 `MAX_OUTPUT_RESERVE_TOKENS`;凡「token 数 ÷ `contextLimit`/`contextWindow`」而分母未经该出口 ⇒ 红。**只从包主入口导出,不得新建子路径** —— 架构契约表没登记 `packages/context-compaction` 的子入口,深导入会被守门 103 判 D3 红(本仓踩过)。**行为后果如实登记**:分母只会变小 ⇒ **压缩更早触发**,这是用户可感知的变化,回退有两条路(构造参数 `outputReserveEnabled:false` / 环境变量 `IHUI_COMPACTION_OUTPUT_RESERVE=0`);存量 8 文件/12 处进 `scripts/compaction-denominator-baseline.json` 只减不增,其中 **api 端 `/chat/stream` 的溢出面尚未收口** —— 那是留下的账,不得读成"已修完"。口径同 77/83/98/103:全量判 HEAD blob、`--staged` 判索引 blob、取不到 ⇒ **exit 2 无法判定**、枚举到 0 个文件判死不记绿。取证 `--self-test` 15 条(成对正反例 + 空枚举反向对照)+ §22c 镜像测试;紧急跳过 `HUSKY_SKIP_COMPACTION_DENOMINATOR=1`。
- **路由身份键对账**(113):`scripts/check-tool-arg-routing-identity.mjs`(blocking,`stagedTriggers=apps/cli/src/tools/`,2026-09-25 立) —— 钉的是**"谁的数据"不得由模型决定**:路由身份一律由宿主在 closure / ctx 绑定,绝不得出现在模型可见参数集合里。键清单的**唯一源**是 `packages/types/src/tool-contract.ts` 的 `ROUTING_IDENTITY_KEYS`(门与类型层共用一份实现,禁止第二份;该常量被摘线本门即 exit 2 并点名,而不是"清单空 ⇒ 全绿")。三类落点都判:直接 `parameters` 字面量、具名 helper 单跳回溯、契约 `shape.input.properties` 跨文件解析;键名归一化(小写去 `_`/`-`)故 `sessionId`/`session_id`/`SESSION_ID` 同视。**刻意只收 16 个"填错会把结果送到别人那里"的键** —— `messageId`/`toolCallId`/`taskId`/`fileId`/`path` 属内容引用或业务句柄,纳进来会误拦正当用法(加键先问那一句,镜像测试反向钉死)。投影出口侧不是"未判到":静态验 `schema-projection.ts` 里 `properties[name]` 的键名逐字取自 `Object.entries(parameters)`、无字面量下标/改名 ⇒ 判 `name-preserving`;解析不出即计未判定并 exit 2。口径同 77/83/98/103/112(全量判 HEAD blob、`--staged` 判索引、暂存集空退全量并如实点名、枚举 0 个判死)。**首跑 16 处已冻棘轮基线,但那不是清偿**:`memory.ts` 把 `user_id` 标成 `required`,而服务端 `routes/memory.ts:118` 只认 `request.userId!` ⇒ 真是"模型不可能满足的必填参数"(功能面 bug);`debug.ts`/`terminal.ts` 的 11 处是工具自句柄,须把排除理由正式登记进键清单注释,**不得用行内豁免遮掉**。取证 `--self-test` 23 例 + §22c 镜像 7 例;紧急跳过 `HUSKY_SKIP_TOOL_ARG_ROUTING_IDENTITY=1`。
  - ⚠️ **中文出参会被控制台码页吃掉(2026-09-25 实测,和上一条同为"码页"但方向相反)**:上面说中文当 **argv** 传出去无损;但 node 往 stdout 打的**路径**含中文(`密钥/...`)时,PowerShell 按 **GBK 码页**解码 ⇒ 拿回的是乱码字符串 ⇒ `Test-Path` 判"不存在" ⇒ 脚本静默退回兜底凭据,表现得像"口令文件没建"。**凡"把一个值读回来继续用"的场合(路径/文件名/URL),调 node 前必须显式 `[Console]::OutputEncoding = UTF8` 并在 finally 还原**;只在控制台显示乱码的是"输出面",这条不成立 —— 值一旦参与判据就不是显示问题。同类修法也适用于 §5e 说的"多行中文正文走 --message-file"。
  - **凭据目录现状**(2026-09-25 起):`db-backup/ihui-backup.txt` = 只读备份账号 `beifen` 的口令;`IHUI生产账号/pgsql管理员-postgres-*.txt` = 该库 `postgres` 的口令(此前**从未设过**,详见 `deploy/win/ihui-pg-backup-role.sql` 顶部的执行记录)。两份都在仓库外、都在 §5d 权威目录里,都不进日志/聊天/提交。
    **本票新建的两把常驻工具(量算/归并出口,不是守门 —— 勿因"没接 guardian-runner"就当作没装车)**:
    `scripts/c-disk-breakdown.mjs` —— 全盘**只读**量算仪,junction 安全(刻意不跟随重解析点,否则把 D 盘的量算成
    C 盘的债),强制对账「遍历 + 特殊文件 + 未解释 = 已用」,未解释 >2GB 或为负都当场喊话。报磁盘状态先跑它,
    不要跑 `df`(§26 已记过一次它给出相反百分比)。用法 `node scripts/c-disk-breakdown.mjs [--root C:/] [--depth N] [--json]`。
    它的文件名不以 `check|scan|guard` 开头 ⇒ 守门 89 结构上看不见它,所以它的不变量由自己那把尺子钉:
    `scripts/tests/c-disk-breakdown.test.mjs`(7 例:滚到全部祖先 / junction 不跟随 / ROOTKEY 尾斜杠 / 量不到不得伪装成结论)。
    `scripts/plan-union-merge.mjs` —— 活文档台账被撞号或整段回写时的人工归并出口(临时索引 + commit-tree + CAS,
    落地前自证"本侧/对侧路径零丢失"),镜像测试 4 例在位。两者**都不做任何删除**。
- **入参校验装车对账**(115):`scripts/check-tool-arg-validation-wired.mjs`(blocking,`stagedTriggers=apps/cli/src/tools/` + `packages/types/src/`,2026-09-25 立) —— 堵的是 A36 查出来的那一格:`apps/cli/src/tools/argument-validator.ts` 校验器写得挺全(field/expected/actual、嵌套 `items[0]` 路径都有),**但生产面零调用方** ⇒ 工具入参从来没被校验过,`required` 只拿去生成提示文案和 provider schema,模型少传/传错类型时由 104 枚 handler 各自兜 ⇒ 同一类错误 104 种表现。**只判两条**:① `validateToolArguments(` 必须有非测试调用方(**注释里的提及不算** —— 本仓最高频失效型就是"看起来有、其实没装车",全仓三处 `argument-validator` 的出现位置全是注释);② 模式开关必须在位、**默认 off**、`shadow` 档存在 —— `enforce` 未实现前默认绝不能是 enforce,因为那些 `parameters` 描述**从未被执行过、准确度未知**,直接打开拒绝就是运行时版恒红事故(与 §12e/守门 77/83 同一条理由)。影子档只记账:不改 `call.arguments`、不改返回值、不拦调用,遥测**不落任何入参值**(`ValidationError.actual` 在 `enum_mismatch` 分支装的就是用户原值,`expected` 装枚举表 ⇒ 双双排除)。已知覆盖面缺口如实登记:`hubEnabled && hubResolver` 分支在拿到 Tool 对象之前就 return,那条路径影子不生效。三步顺序(①影子 → ②用影子数据修描述 → ③才谈默认 enforce + 逐条回灌 `{字段路径,期望,实得}`)登记在 PROJECT_PLAN 第八波。口径同 77/83/98/103/112/113(全量判 HEAD blob、`--staged` 判索引、取不到 exit 2、枚举 0 判死);取证 `--self-test` 14 条 + §22c 镜像 5 例;紧急跳过 `HUSKY_SKIP_TOOL_ARG_VALIDATION_WIRED=1`。
  **本票新建的两把常驻工具(量算/归并出口,不是守门 —— 勿因"没接 guardian-runner"就当作没装车)**:
  `scripts/c-disk-breakdown.mjs` —— 全盘**只读**量算仪,junction 安全(刻意不跟随重解析点,否则把 D 盘的量算成
  C 盘的债),强制对账「遍历 + 特殊文件 + 未解释 = 已用」,未解释 >2GB 或为负都当场喊话。报磁盘状态先跑它,
  不要跑 `df`(§26 已记过一次它给出相反百分比)。用法 `node scripts/c-disk-breakdown.mjs [--root C:/] [--depth N] [--json]`。
  它的文件名不以 `check|scan|guard` 开头 ⇒ 守门 89 结构上看不见它,所以它的不变量由自己那把尺子钉:
  `scripts/tests/c-disk-breakdown.test.mjs`(7 例:滚到全部祖先 / junction 不跟随 / ROOTKEY 尾斜杠 / 量不到不得伪装成结论)。
  `scripts/plan-union-merge.mjs` —— 活文档台账被撞号或整段回写时的人工归并出口(临时索引 + commit-tree + CAS,
  落地前自证"本侧/对侧路径零丢失"),镜像测试 4 例在位。两者**都不做任何删除**。
- **出站事实对账**(116):`scripts/check-egress-facts.mjs`(blocking,2026-09-25 立) —— 钉 A34:一趟请求**实际**用的代理/CA/NO_PROXY 配置必须是**响应上的返回值**,不是日志行。成因不是假想:AGENTS §5b 连着记过"git 网络时通时不通""钩子进程不继承 shell env""服务身份与交互账户的 `safe.directory` 互不相通",每次都是人肉现读 `git config --local`,因为没有任何一次调用把"我这趟用了哪份配置"带回来。唯一出口 `apps/api/src/utils/proxy-dispatcher.ts` 的 `proxiedFetch`,响应挂**不可枚举** `egress` 字段(不可枚举是为防它被 `JSON.stringify(res)` 意外带进日志/响应体 —— 凭据面同族),用 `readEgressFacts(res)` 取;`isProxiedUrl` 已改成该事实的投影 ⇒ **决策与事实共用一份判据**(两处算同一件事必漂移,本仓记过多次)。厂商域名**不硬编码**,由 `VENDORS.baseUrl` + `DEFAULT_PROXY_DOMAINS` 两张真表推导(实测 100 个;推不出 ⇒ exit 2 不记绿)。覆盖面如实登记 = `apps/api/src` + `apps/cli/src/provider`;`packages/api-client` 打自家后端不入面,Python 侧只登记。**两条"没做"同样在案**:① 错误分流(被策略拦 vs 网络错两个码)—— 本仓 TS 侧没有任何出口会在传输前拒发,造两个码就是永远不响的门;② `NO_PROXY` 只读回成事实、未让它生效(改路由波及全部厂商调用,另票)。存量 6 处/5 文件进 `scripts/egress-facts-baseline.json` 每文件棘轮,只减不增;口径同 77/83/98/103/112/113/115。取证 `--self-test` 28 条 + §22c 镜像 5 例(含真表真挂装车证明);紧急跳过 `HUSKY_SKIP_EGRESS_FACTS=1`。
- **对话流元素覆盖**(57):**五类**判据(blocking)—— ① 锚点漂移(`anchor-missing-file`/`anchor-missing-marker`)② SSE 契约双端不齐 ③ 清单条目倒退(`entryCountBaseline`)④ **锚点存续性倒退**(`anchorCountBaseline` 的 `perElement` 逐条目 + `total`,含"该字段整块不见 = 台账被旧基线回写"的指纹;搬到同条目另一端是合法重构、总数不变即绿)⑤ **注释式摘线**(匹配面是**剥注释后的代码面**,字符串感知状态机;存量出口 `commentOnlyAnchorBaseline` 只报数不判红、只减不增,首轮 3 处存量已于 2026-09-25 按「mustMatch 重指向宿主代码面真实标识」清偿归零 ⇒ 新增该型直接判红,且**禁止**反向删宿主注释或补登记来消账);**判仓库内容而非共享工作区快照** —— 已暂存取索引 blob、仅工作树脏取 HEAD blob,别人未提交的重写不得钉红无关提交(2026-09-24,与守门 77 同取向)
- **测试收集存续性对账**(114,`scripts/check-test-collection-runs.mjs`,**warn 级**,紧急跳过 `HUSKY_SKIP_TEST_COLLECTION=1`)—— 堵"测试到底有没有在跑"这一维:mobile-rn 实测曾有 14 个套件在**收集阶段**整体失败、内含 131 条 it 声明(展开 137 枚用例)一条都没跑,而头条写成"2 failed"看着近乎全绿;更根本的是提交链百余道门**无一跑测试**(`check-staged-typecheck` 走 tsc,结构上看不见 transform / 解析期失败),CI 会红而提交链不拦。判据只认 `Failed Suites` 区块头与 `FAIL <file> [ <file> ]` 方括号形态两信号取大值(文件级计数在两种故障下长得一样,不作判据),断言失败另计并明写不计入本门;`No test files found` 或 `Test Files …(0)` 判红"空扫 ⇒ 覆盖归零";取不到结论(无 test script / 入口非 vitest / 入口不可解析 / 超时 / 无汇总行 / 空输出)一律**未判定**并点名原因,绝不记为通过。**刻意 warn 级**:该数字随他人工作树在 2→0→43 间跳,挂 blocking 等于每台每次逼人 `--no-verify`,一次绕过约 140 道门全废(§12e 同型);问责出口是 `--strict` / `pnpm check:test-collection`(CI 侧可换 `--require-idle` 型判红)。取证 `--self-test` 18 例(mkScratch 真造临时端、真派生)+ 镜像 17 例(含 T1 装车 / T2 摘线必判未接线 / T3 定级被改成 blocking 必读得出 / T4 全 runner id 不得重复 / T12 反向钉死不得 shell:true)。**已知边界**:只接 JS 系(vitest);pytest 侧是另一套版式、单列一票;`Failed Suites` 版式在 vitest 3.x(miniapp)是否逐字相同**未取证**,故扩端顺序是先 `--end apps/web` 连测零红再接提交链;被吞 it 的精确条数只能估算。
- **门脚本取材面纪律对账**(118):`scripts/check-gate-face-discipline.mjs`(blocking,`stagedTriggers=scripts/`,2026-09-25 立) —— **门数与分档读数一律现读,本文不钉数字**(跑 `node scripts/check-gate-face-discipline.mjs --json` 看 `counts`;2026-09-26 那次实测是 face 28 / loose 71(其中 half-wired 1)/ unknown 38 / no-content 49 —— 写死它下次收紧就变成假账。**那 1 道半接线同日收口了**:`check-rn-global-css-sync` 换成走层的 `catBatch` 三面取材,收口后现读 `half-wired` 应为 0 —— 这条判据落地当天就产出了第一次真实修复,不是只多了一批红)。按磁盘判的门的结构性缺陷:共享工作树常年滞后 HEAD ⇒ 同一份 HEAD 代码在"恒红"与"假绿"之间来回跳,并把错数写回棘轮基线(守门 83 的 R3 登记一天内被整文件回退三次即此型)。**判据锚在"本次改动动过的门"上**(结构棘轮,不留会腐烂的白名单),全量档只报数 —— 一次性把 74 道判红就是逼各会话合法 `--no-verify`、连带废掉全部守门。**2026-09-26 收紧:"引了 `face-reader`" 不再构成合规** —— 旧判序第一刀是 `FACE_IMPORT_RE` 命中即判 `face`,于是"import 了层、内容却仍由自己 `git show` / 磁盘 `readFileSync` 取"这种**半接线**被系统性放过。实证不是假想:门 36 与门 124 都 import 了 `face-reader`(只用 `gitRaw`),却都**默认按磁盘判**;把两者改成 `catBatch` 判 HEAD 之后,本门分类读数一字未变 —— 一道自称守取材面纪律的门,对它立项要防的那一型完全失明。现判序:① 真调用层的**读取入口**(`catBatch` / `readWorktreeFile`,含 `as` 别名与 `import * as ns` 命名空间形态)⇒ `face`;② 引了层却没用它读内容、而内容来自自派生 git 或磁盘 ⇒ **`half-wired`(提交档判红)**;③ 其余照旧 `loose-git` / `loose-fs`。**只有非读取导出不算**:`selectFace` / `gitBinary` / `gitErrText` / `catBatchOids` / `catBatchSizes` 都拿不到正文,把它们当读取凭证就等于没收紧。反向护栏同等重要:别名与多行导入必须认(否则新判据对合法写法产假阳,`T10` 钉死);`usesLayerRead` 导出给镜像用,**禁止**在测试里再抄一份判据(§22c)。**同日两处精度修正(都是量出来的假阳,不是审美)**:① "读内容"与"枚举"必须分开 —— `cat-file -e`(存在性)、`ls-tree --name-only`(路径清单)、`rev-list`/`merge-base` 不产生 blob 正文,把它们算成"散写读内容"会把只做枚举的门钉成半接线(它一处 blob 都没读);**假阳比漏报更贵** —— 它指使人去"修"没坏的东西,还把判据口径说歪成"数字很多"。② 仓库锚点必须落在**路径函数实参**里:旧写法的 `|\bROOT\s*,` 一支会把 `function audit(limit = 400, cwd = ROOT)` 读成"以仓库根拼路径"。③ 借层的 transport 自己读内容(`gitRaw(['show', spec])`)同样算半接线 —— 管子共用不等于面共用。**已知空档如实登记**:锚点只认大写 `ROOT`,故 `join(root, …)` 这类小写模块根写法落进 `unknown`(不判红)。试过放宽(74→93),其中两格是假阳(一格读 `.workbuddy` 运行台账、一格读按设计只存在于部署机的 gitignore 副本),**已收回**:判据分不清"读被审内容"与"读运行态",就不该假装分得清并据此判红;补它要认识 gitignore 语义(参照 `check-prod-bundle-shadow` 的 S2),不是一行正则的事。两层遮噪方向不同,别混用:认 face-reader 导入与 git 动词必须"只遮注释、保留字符串"(模块说明符和 `cat-file` 本身就是字符串),认 `readFileSync` 调用必须"注释与字符串都遮"(`markHidden` 那一档连字符串一起抹,直接套它会让 F1/F2 双双假绿)。取不到 ⇒ exit 2,全量面枚举到 0 道门判死。自检与镜像例数以命令末行为准(本枚落地时 24 例 / 镜像 11 例,含 F16b 与 T9–T11 的收紧锁);紧急跳过 `HUSKY_SKIP_GATE_FACE_DISCIPLINE=1`。
- **子代理权限继承对账**(119):`scripts/check-subagent-permission-inherited.mjs`(blocking,`stagedTriggers=apps/cli/src/`,2026-09-25 立) —— 立门前先否证了票面前提:`spawn-subagent.ts` / `subagent-registry.ts` / `permission-engine.ts` 三路径在工作树+索引+HEAD **三件套均不存在**,全仓把 bypassPermissions 写成默认档 **0 处** ⇒ "派生默认绕过权限闸"不成立。真缺陷是**反方向那一格**:HEAD 面实测 **7 处 / 4 文件**的派生点**整个不下传** permissionMode 与 permissions,子代理落兜底档后父会话 `--disallowed-tools` 的显式 deny 在子代理里查不到(实测不传规则返回 ask、传规则返回 deny,已由 vitest 钉住)。三判据:**P1** 字面量 bypassPermissions 作默认档(零容忍、不吃豁免)/ **P2** 构造调用必须下传两键(**棘轮锚点 = 该文件 HEAD 自身计数**,存量 7 处只报数,绝不造恒红门)/ **P3** 调用之外读到权限键却没喂进去(半个继承)。**继承落点的代码改动因全在实现票清单外而未做**,补丁方案与越权点写在交付报告里 —— 登记事实,不代裁。自检 39 例(含"把 import 语句里的子串误判成读到键"的反向锁)+ 镜像 12 例;紧急跳过 `HUSKY_SKIP_SUBAGENT_PERMISSION_INHERITED=1`。
- **名单类判据正向证明对账**(120):`scripts/check-list-predicate-has-positive-proof.mjs`(blocking,**不挂** `stagedTriggers` —— 名单漂移可发生在任意路径,2026-09-25 立) —— 第八批三份独立取证收敛到同一个形态缺陷:本仓大量门是"名单驱动"(拦名单里的标识符、或放过白名单成员),而它们的自测只有**反向证明**(拦到坏值才红),从没证明**名单里某一条真能命中**;名单于是可以是张死表而门一路报绿。本门要求每条登记名单在其自测/镜像测试里至少有一条断言的输入**取自名单本身**;判不出 ⇒ 记「未判定」并点名,**不记为通过**。真仓 HEAD 面实测 3 条有真证(门 52 的 43 成员 / 门 80 的 22 / 门 107 的 6),尚未入库的名单判"未判定"而非红 ⇒ 不出生即红。自检 15 条 + 镜像 6 例;紧急跳过 `HUSKY_SKIP_LIST_POSITIVE_PROOF=1`。
- **声明策略消费者对账**(121):`scripts/check-declared-policy-has-consumer.mjs`(blocking,`stagedTriggers=apps/,packages/`,2026-09-25 立) —— 判五类"声明"必须有生产面消费者(带链 import 且取用):MAX_AGE/TTL/RETENTION 策略常量、`*BudgetContract*`、packages/types 的 `*Contract` 导出类型与情态谓词、`(prune|cleanup|purge|expire|sweep)` 导出清理函数。注释/字符串/纯 re-export/测试面/scripts 层都不算消费者。立票实测:`apps/cli/src/sessions/state-store.ts` 的 `pruneOldSessions`(7 天保留)全仓零调用方 ⇒ 已挂到 `saveSession` 写前(60s 节流 + `IHUI_SESSION_PRUNE_DISABLED` 出口 + 裁剪异常不冒泡,"宁可不删");`ToolResultBudgetContract` 的唯一合理消费点在 `tools/index.ts` 的 executor 边界,该文件在飞 ⇒ 未接线登记,摘除会砸坏守门 111 的 CONTRACT_GROUPS(§7 有承接功能不得删)。全量现测:候选 131 / 未接线 32 处 29 文件只报数(subagents/state-store.ts 与 apps/api cleanup 族 9 处同型,清偿另计批)。自检 22 例 + 镜像 8 例;紧急跳过 `HUSKY_SKIP_DECLARED_POLICY_CONSUMER=1`。
- **文件写盘安全对账**(122):`scripts/check-file-write-safety.mjs`(blocking,`stagedTriggers=apps/cli/src/tools/`,2026-09-25 立) —— 工具层写文件必须走原子写出口(同目录临时文件 + `rename` 替换 + Windows rename 目标已存在会 EPERM/ENOENT 带重试 + **不跟随符号链接/junction**(§26 递归穿透事故同族)+ **读后写 stale 校验**:编辑前读到的内容与落盘前磁盘不一致时拒绝而非覆盖 —— 多会话共享工作区的真实风险)。裸 `writeFileSync` 存量走棘轮(实测 `file-edit.ts` 2 处只报数),新增即红。自检 24 例 + 镜像 13 例;紧急跳过 `HUSKY_SKIP_FILE_WRITE_SAFETY=1`。
- **工具族注册对账**(guardian id **131**,**warn**,紧急跳过 `HUSKY_SKIP_TOOL_FAMILY_REGISTERED=1`):`scripts/check-tool-family-registered.mjs` 问的是另一句话 —— tools/** 里 export 的每一族在生产面有没有把它挂进注册表的调用点。它补的是 `check-tool-registry-projection`(131 号那条近邻门)结构性看不见的那一格:**没注册 ⇒ 两侧都没有 ⇒ 那道门一致而绿**。三条到达路径(跨文件具名导入且落注册参数位 / 同文件一跳委托但须证明 owner 在生产面被调 / 随父族组合传播);注释与字符串先抹掉再判(否则门给自己发合格证);动态拼接、`export *`、命名空间转发、同名本地符号一律「未判定」—— 不记绿也绝不判红。定级 warn + **HEAD 存量棘轮**(只拦新出现的零注册族与"本次把注册点摘掉"),问责 `pnpm check:tool-family`(`--strict`),**升 blocking 前置 = 未注册存量归零**;现值一律跑该命令看末行(立项读数:族 18 / 已注册 17 / 未注册 1 = `DEBUG_TOOLS`,即 10 枚 DAP 工具与那句"30 分钟自动清理"对模型不可达 —— 是否注册它属 §24 产品决策,不是清账动作)。

- **工具执行预算对账**(123):`scripts/check-tool-exec-budget.mjs`(blocking,`stagedTriggers=apps/cli/src/tools/`,2026-09-25 立) —— 钉"工具执行无界"三源:`ToolContext` 无 `signal` 字段(取消无通道)、执行点无超时、`util/exec.ts` 无 `maxBuffer`(node 默认 1MB,超限抛错并丢已缓冲输出)。S1 系判**机制在位**(常量三件套 `TOOL_EXEC_BUDGET_{DEFAULT,MAX,SETTLE_GRACE}_MS` / 唯一解析出口 `resolveToolExecBudgetMs` 且内含 `Math.min` 封顶 / `linkAbortSignal` / `executeWithinExecBudget` 在代码位出现"自身定义+至少一次调用");S2 判**注入点**(未传 signal 的 ctx 构造逐处点名)。取材面=HEAD/索引,取不到 exit 2。自检 26 例 + 镜像 9 例;紧急跳过 `HUSKY_SKIP_TOOL_EXEC_BUDGET=1`。
- **记忆端点属主绑定对账**(117,**blocking**,2026-09-25 升档;紧急跳过 `HUSKY_SKIP_MEMORY_OWNER_BINDING=1`):`scripts/check-memory-owner-binding.mjs` 钉的是**"认证不等于授权"**那一层 —— `apps/ai-service/app/api/memory.py` 把 `user_id` 当请求体/Query 参数收,全文件**没有一处**与令牌主体对齐;`/api/memory` 不在 `PUBLIC_PATHS` ⇒ 匿名进不来,但**任何已登录用户填别人的 UUID 就能读/改/删别人的记忆**。判洞要问"有没有把请求里的 id 与令牌里的 id 比对",不是"有没有全局中间件"。两条 Python 语法锚点必须同时认:`user_id: str = Query(...)` 与 Pydantic `Field(...)` —— **只认 Query 会让整类 POST 端点隐身**;对齐出口只写在注释里不算(自称已拆、实际没拆那一型)。**定级史(照抄旧档会判错)**:立项时 warn —— 当时未对齐存量 2 个文件;现值一律 `node scripts/check-memory-owner-binding.mjs` 自取 —— `app/api/memory.py` 已于同日 `3b0170fa176` 九端点统一 `require_request_user_id` 归零,剩下的是**此前无人枚举过的** `app/routers/rules.py`,而"怎么对齐"取决于 **v1 对外 API 能不能代表他人访问记忆** —— 实测 `apps/api/src/routes/v1-knowledge-tools.ts` 有 2 条转发是 `jsonInit(parsed.data)` **原样透传客户端自报身份**(本门把这类列成"上游透传清单"只报数,就是给选型看爆炸半径用的)。收紧成 403 与"忽略并覆盖"两种改法**都会让本门归零**,故存量未清时判红就是恒红门,唯一结局是逼人 `--no-verify` 连带废掉全部守门(§12e 同型)。**2026-09-25 前置已满足并升 blocking**:`752c6eb110b` 收掉最后一个未对齐面(`app/routers/rules.py`)后 HEAD 面实测"未对齐 0 个",runner 侧同时带 `args:['--strict']`(本门默认档只报数,不带它升档是假的)。**降回 warn 等于重新放开新增敞口**;若 HEAD 面再报未对齐(例:有人跳过钩子塞进去),先清偿再提别的票,不得削判据。口径同 70/77/83/98/101/103(全量判 HEAD blob、`--staged` 判索引、枚举到 0 个文件判"无法判定"而非记绿 —— 判据失明不是通过)。仓里现成出口 = `app/core/jwt_auth.py` 的 `require_request_user_id`(带 `DEV_ANONYMOUS_PRINCIPAL` 开发降级,以免 ASGI in-process 的既有测试整片 401 —— 那不构成安全结论)。取证 `--self-test` 17 例(正反成对) + §22c 镜像 14 例(含"未注册时不得被判定为已装车"的方向性对照);手动问责档 `pnpm check:memory-owner`(走 `--strict`)。
- **记忆端点属主绑定对账**(117,**warn**,紧急跳过 `HUSKY_SKIP_MEMORY_OWNER_BINDING=1`):`scripts/check-memory-owner-binding.mjs` 钉的是**"认证不等于授权"**那一层 —— `apps/ai-service/app/api/memory.py` 把 `user_id` 当请求体/Query 参数收,全文件**没有一处**与令牌主体对齐;`/api/memory` 不在 `PUBLIC_PATHS` ⇒ 匿名进不来,但**任何已登录用户填别人的 UUID 就能读/改/删别人的记忆**。判洞要问"有没有把请求里的 id 与令牌里的 id 比对",不是"有没有全局中间件"。两条 Python 语法锚点必须同时认:`user_id: str = Query(...)` 与 Pydantic `Field(...)` —— **只认 Query 会让整类 POST 端点隐身**;对齐出口只写在注释里不算(自称已拆、实际没拆那一型)。**定级 warn 是设计前提而非偷懒**:HEAD 实测未对齐存量 **2 个文件**(`app/api/memory.py` 与**此前无人枚举过的** `app/routers/rules.py`),而"怎么对齐"取决于 **v1 对外 API 能不能代表他人访问记忆** —— 实测 `apps/api/src/routes/v1-knowledge-tools.ts` 有 2 条转发是 `jsonInit(parsed.data)` **原样透传客户端自报身份**(本门把这类列成"上游透传清单"只报数,就是给选型看爆炸半径用的)。收紧成 403 与"忽略并覆盖"两种改法**都会让本门归零**,故在选型落地前它是"存量报数、新增判红"的棘轮;当场 blocking = 与任何提交都无关的恒红门,唯一结局是逼人 `--no-verify` 连带废掉全部守门(§12e 同型)。**升 blocking 的前置条件 = 未对齐存量归零**。口径同 70/77/83/98/101/103(全量判 HEAD blob、`--staged` 判索引、枚举到 0 个文件判"无法判定"而非记绿 —— 判据失明不是通过)。仓里现成出口 = `app/core/jwt_auth.py` 的 `require_request_user_id`(带 `DEV_ANONYMOUS_PRINCIPAL` 开发降级,以免 ASGI in-process 的既有测试整片 401 —— 那不构成安全结论)。取证 `--self-test` 17 例(正反成对) + §22c 镜像 14 例(含"未注册时不得被判定为已装车"的方向性对照);手动问责档 `pnpm check:memory-owner`(走 `--strict`)。
- **钩子失败的归因是量出来的,不是抄来的(2026-09-25 立,`scripts/lib/commit-gate-attribution.mjs`)**:旧版首次 commit 一失败就打印「按用户规则"hook 失败因其他 agent 代码 → --no-verify 重试"」并整批跳门 —— 它**从未计算过归因**。实测一轮 134 道门 111 通过 / 1 失败,红的是 `check-push-sync`(判远端态、与提交内容无关),措辞仍是"他人代码"。现流程:解析 runner 的失败门清单 → **逐道复跑** → 该门这次的输出**点名本次声明的文件 ⇒ 判"本任务自己的红",拒绝 `--no-verify` 并 exit 1**(必须修);一个都没点名 ⇒ 允许跳,但只说量到的话;批没跑完 / 解析不到 / 复跑全不可用 ⇒ 明写「未归因」,按应急路径落地并留痕于 `.workbuddy/safe-commit-attestation.jsonl`。**"批一道都没跑"从 2026-09-26 起不再是中性事实,而是一个必须先补证据的分支**:`scripts/lib/pre-commit-hook.js` 里 lint-staged 跑在守门批**之前**,于是一次 lint/prettier 失败 = 156 道门全不跑 = 归因永远 `unattributed` = 照样跳门,而账面读起来像"跑过了、只是与本次无关"(实测 `acaf8407cc` 与 `d379215126` 前后各一次;这正是"判据失效的表现永远是安静"那一型,守门 70/76/81 同族)。现由 `safe-commit` 在判到 `unattributed ∨ ranFullBatch===false` 时**自己把 `guardian-runner --staged` 跑一遍**,再把那一轮的输出喂回**同一条铰链**(`classifyHookFailure`)分流:点名本次文件 ⇒ `mine` ⇒ 拒绝跳门;一个都没点名 ⇒ 可跳但措辞必须写明"取证来自 safe-commit 自跑的那一轮,不是钩子内那一轮";自跑也没成功 ⇒ 照旧落地,而 `unattributed` 的措辞必须带上具体原因(runner 不在位 / 派生失败码 / 超时)。**刻意不把 `unattributed` 改成拒绝跳门** —— 那会让"别人把我挡在 lint 外"的应急路径消失,逼人手工 `git commit` 跳得更彻底且不留痕。**一条如实登记的覆盖面上限**:自跑发生在首次 commit 失败之后,而 lint-staged 失败时会回滚它自己动过的暂存区 ⇒ 新增文件此刻已退回未跟踪,自跑那一轮看到的暂存集可能少于声明集(与既有"逐道复跑"同一刻同一索引态,两份证据因此可互相比较)。应急提速:`IHUI_SAFE_COMMIT_BATCH_TIMEOUT_MS`(极小值 ⇒ 自跑判"未成功"并带原因入库,**不是**静默开关)。取证:镜像 13 例(含"汇总块在位时绝不自跑"的反向锁与装车证明)。**stdout 没汇总而要去钩子日志取第二输入源时,该汇总块必须先过"轮次绑定"这道锁(2026-09-25 补,G-179)**:钩子日志是多轮追加的,而旧锁只要求"该汇总之前的窗口里出现过本次声明的**任一**文件名"—— 那段窗口实测 48KB–202KB,里头**必然**提到 `PROJECT_PLAN.md` / `AGENTS.md`(守门 71/13c 的结论行就点名它们),于是"我自己的红"可以借别人那一轮的汇总被洗成 `not-ours`(实测提交 `48ac2c03e`:红在 eslint `no-unused-vars`,lint-staged 失败使守门批根本没跑 ⇒ 152 道门被一次误归因跳掉,HEAD 里真留下那枚 lint 错误)。现规则:**该汇总之前那次 `staged 文件清单` 回显必须完整包含本次声明集**才允许用于归因 —— 回显由钩子入口 `auditStagingFiles()` 全量打印,是把一段日志绑到具体某枚提交的唯一内容证据(日志没有时间戳、也没有轮次号,实测按 `^YYYY-MM-DD` 抓零命中)。取"包含"不取"等值":回显反映磁盘实际暂存,并发会话同窗口 staged 的东西会造成超集,而它们结构上进不了本枚提交(commit 带 pathspec)。**绑定不成 ⇒ 落 `unattributed`,照旧能提交,只是措辞不得声称"因他人代码"—— 失效方向必须是"多要一次定向说明",绝不能是"多放一次跳门"**(与本节末那条 `findingLines` 教训同一条禁令)。真仓双向证明(不是夹具):合法那一轮绑定成功并取到 `总检查数: 152(已执行 152)` 的汇总,事故那一轮被拒且点名缺的 2 个文件。**stdout 没汇总时,第二输入源必须先过"轮次绑定"这道锁**(2026-09-25 补,G-179):钩子日志是多轮追加的,而旧锁只要求"该汇总块之前的窗口里出现过本次声明的**任一**文件名"—— 那段窗口实测 48KB–202KB,里头**必然**提到 `PROJECT_PLAN.md` / `AGENTS.md`(守门 71/13c 的结论行就点名它们),于是"我的红"可以借别人那一轮的汇总被洗成 `not-ours`(实测提交 `48ac2c03e`:红在 eslint `no-unused-vars`,而 lint-staged 失败使守门批根本没跑 ⇒ 152 道门被一次误归因跳掉)。现规则:该汇总块之前那次 `staged 文件清单` 回显必须**完整包含**本次声明集(回显由钩子入口 `auditStagingFiles()` 全量打印,是唯一能把一段日志绑定到具体某枚提交的信号 —— 日志没有时间戳、也没有轮次号);缺任何一项 ⇒ 判"不是本轮" ⇒ 落到 `unattributed`。**失效方向刻意是"多要一次定向说明",绝不是"多放一次跳门"**。真仓双向证明:合法那一轮 `绑定成功` 并取到 `总检查数: 152(已执行 152)` 的汇总块,事故那一轮 `拒绝` 且点名缺的 2 个文件(4 项声明 vs 4 项回显,重名 2 项 —— 正是旧锁放过的形态)。**刻意不建"机器态门 id 清单"** —— 豁免清单必然腐烂(见 §4 对 `RN_ONLY_BRAND_KEYS` 的教训)。取证:镜像测试 5/5(含装车证明 + 真实 `hook-logs/pre-commit.log` 取样校验夹具)+ 三条变异各自变红(ANSI 剥色那条登记为**等价变异**,不造假具硬凑红)。**因此:agent 不得再假设"钩子红了就能跳"** —— 被拒时那句红点名了哪个文件,就去修哪个文件;确属误判才可按 §16 显式说明后手工提交。
- **守门接线层对账**(89): `scripts/check-gate-wiring.mjs`(blocking) —— 本门是其余全部守门的"装车检",查接线层四类结构缺陷:**R1/R2** 脚本头部或 AGENTS.md 声称"已接 pre-commit/pre-push/CI 必跑/第 N 项"而五处权威点(`guardian-runner` 的 `script:` ∪ `scripts/lib/pre-commit-hook.js` ∪ `.husky/*` ∪ 根 `package.json` ∪ `.github/workflows`+cert)全部零命中 → 拦;**R4**(2026-09-24 由"仅报数"升档,前置=真仓缺口 48→0 已清零)门已接线但 AGENTS.md/README.md 通篇未点名 → 拦(判据刻意宽松:出现去后缀同名即算,故只会漏报不会误拦);**R5** 同一 `id` 登记两道门 → 拦(串 skipEnv 与失败归属);**R7** 台账 `type=dispatcher` 的依据文件不存在或文件里没提该脚本 → 拦。**R8**(2026-09-25 补)某道门的 `stagedTriggers` 只要会让归一层 `scripts/lib/guardian-triggers.mjs` **当场抛错**即拦 —— runner 的 `for (const check of effectiveChecks)` 没有 catch,一次抛错=整条 pre-commit 中止(同型崩点实测让 3 枚提交被迫 `--no-verify`,152 道门对那 3 枚全废)。**红条件委托那道 lib 的契约而不是本门自写形状规则**:自己写必然与 lib 漂移,而漂移的代价固定是"一道对合法注册恒红的 blocking 门 ⇒ 逼人跳门 ⇒ 全部守门作废"。据此三类**只分类报数不判红**:裸字符串=「已救回」(lib 有意归一,再判红就是两道门互相矛盾)、falsy 字面量=「死注册」(runner 的守卫走不到归一层,它结构上崩不了)、值不是字面量(变量/模板串)=「判不出」(不猜,也绝不静默算通过)。取材面**与 R1/R2 刻意不同**:全量档 HEAD、`--staged` 档优先索引 —— 注册表自身就是被审对象,"同一枚提交里既修注册形态又新立判据"若被自己的新判据判红,就等于本门亲手制造跳门(与 R2/R4 的 HEAD∪索引例外同一理由,两面一绿一红时才算证明过判据有牙)。R3(五处零命中且无肯定式声称)/R6(共用 skipEnv)/弱接线(仅 CI)只如实报数不判红。**取材口径分两面**:接线面(哪些脚本被登记为已接)一律按 **HEAD** 判 —— 共享工作区的工作区文件会滞后,产出相反结论;而**文档面**(AGENTS.md / README.md 有没有点名)按 **HEAD ∪ 索引**判(2026-09-24 改),因为"同一枚提交里既注册新门又补点名行"是正确姿势,只读 HEAD 会让这枚提交被自己判红 ⇒ 逼人 `--no-verify` ⇒ 全部守门作废;索引取不到退回 HEAD,口径在结论行如实报出,且**绝不读工作区**(只躺在工作区没 `git add` 的文档行不算点名,已由自检 M9d/M9e 一红一绿的可逆对照钉死)。;台账 `scripts/gate-wiring-allowlist.json` 只能救 R3,凡声称已接线者台账不救。⚠️ 本门对自身 `SELF_EXEMPT`(创建当期 HEAD 里还没有它),所以"它自己被摘线"由镜像测试 T13 钉死。真跑 `node scripts/check-gate-wiring.mjs` exit 0(已接线 143 / 台账豁免 13 / R4 0 枚,数字以命令末行现测为准);自检 `--self-test`(例数以末行为准,含 M8a/M8b 的 R4 双向端到端证明 + M9a-M9e 的文档取材口径五形态对照)+ `node --test scripts/tests/check-gate-wiring.test.mjs`(例数以末行为准,含 T13 装车证明与 T14/T15 取材对照);紧急跳过 `HUSKY_SKIP_GATE_WIRING=1`。
- **记忆端点属主绑定对账**(117,**warn**,紧急跳过 `HUSKY_SKIP_MEMORY_OWNER_BINDING=1`):`scripts/check-memory-owner-binding.mjs` 钉的是**"认证不等于授权"**那一层 —— `apps/ai-service/app/api/memory.py` 把 `user_id` 当请求体/Query 参数收,全文件**没有一处**与令牌主体对齐;`/api/memory` 不在 `PUBLIC_PATHS` ⇒ 匿名进不来,但**任何已登录用户填别人的 UUID 就能读/改/删别人的记忆**。判洞要问"有没有把请求里的 id 与令牌里的 id 比对",不是"有没有全局中间件"。两条 Python 语法锚点必须同时认:`user_id: str = Query(...)` 与 Pydantic `Field(...)` —— **只认 Query 会让整类 POST 端点隐身**;对齐出口只写在注释里不算(自称已拆、实际没拆那一型)。**定级 warn 是设计前提而非偷懒**:未对齐存量:立项读数 2 个文件;现值一律 `node scripts/check-memory-owner-binding.mjs` 自取 —— `app/api/memory.py` 已于同日 `3b0170fa176` 九端点统一 `require_request_user_id` 归零,剩下的是**此前无人枚举过的** `app/routers/rules.py`,而"怎么对齐"取决于 **v1 对外 API 能不能代表他人访问记忆** —— 实测 `apps/api/src/routes/v1-knowledge-tools.ts` 有 2 条转发是 `jsonInit(parsed.data)` **原样透传客户端自报身份**(本门把这类列成"上游透传清单"只报数,就是给选型看爆炸半径用的)。收紧成 403 与"忽略并覆盖"两种改法**都会让本门归零**,故在选型落地前它是"存量报数、新增判红"的棘轮;当场 blocking = 与任何提交都无关的恒红门,唯一结局是逼人 `--no-verify` 连带废掉全部守门(§12e 同型)。**升 blocking 的前置条件 = 未对齐存量归零**。口径同 70/77/83/98/101/103(全量判 HEAD blob、`--staged` 判索引、枚举到 0 个文件判"无法判定"而非记绿 —— 判据失明不是通过)。仓里现成出口 = `app/core/jwt_auth.py` 的 `require_request_user_id`(带 `DEV_ANONYMOUS_PRINCIPAL` 开发降级,以免 ASGI in-process 的既有测试整片 401 —— 那不构成安全结论)。取证 `--self-test` 17 例(正反成对) + §22c 镜像 14 例(含"未注册时不得被判定为已装车"的方向性对照);手动问责档 `pnpm check:memory-owner`(走 `--strict`)。

<!-- ⚠️ 本行是守门 102 的**四类判据原文**(2026-09-25 第二轮收口前),已被下方"六类判据"条就地取代:缺 GA5 混写与 GA6 双箭头两条,S0 也只登记 4 个机制。照它派单会以为 RN 共享层那份 BackChevron 有人看守。保留原文只为不丢行(AGENTS §12),勿照它执行。 -->

- **守门**:`scripts/check-glyph-arrow-icon.mjs`(guardian 第 **102** 项,blocking)六类判据:**GA1** 整格字符箭头 `›»→》‹←`(或 `{'>'}`)在可证 affordance 语境(自身或 ≤8 层祖先带 `on*Press/Click/Tap` 或 `role="button"`,或祖先为 `Link/Pressable/Touchable*/Button`)当图标 —— **左向 `‹`/`←` 于 2026-09-25 补入**(立项时只有右向四字,于是小程序 NavBar 与 7 个页面头拿 `‹` 当返回图标长期零判据);**GA2** 「更多」类标签与**同词干** `*Arrow*` 样式配对后箭头字号 > 标签字号(单位不同不判红,计入 undetermined);**GA4**(同日新增)整格子内容是「返回」类文案(`t('common.back')` 这类键名末段为 `back`/`back<数字>` 的 i18n 调用,或字面量 `返回`)且处在可证 affordance 语境 ⇒ 即"把返回两个字当返回箭头";**带宾语的标签刻意不纳**(`backHome`/`backLogin`/`prevMonth` —— 那些是按钮文案,换成裸箭头反而不表意);**GA5**(同日新增)「返回」与字形的**混写**两型:`← 返回` 整格(GA1 要整格只有字形、GA4 要整格只有文案,混排两条都不纳),以及字形与返回调用**分居同一元素的两个子节点**(`← {t('common.back')}` —— `walkAffordanceChildren` 走 `loneChildText`,多子元素的元素根本不入选 ⇒ 前三条结构上全盲);**GA6**(同日新增,跨文件判据)小程序页面渲染页内返回键(`BackChevron` 或 `<NavBar/>`)却是**原生导航栏** ⇒ 同屏两个箭头 + 双层 chrome,读 Taro 页面 config、页内未写则继承 `app.config.ts` 的全局 window,两处都取不到 ⇒ 计入「未判定」点名(不记绿也不冒红);**S0** 共享唯一实现被摘线或无人 import(防"造好没装车"),现登记 5 条:三端「更多」箭头 + 页头返回键的**两份同名实现**(小程序端内与 RN 共享层 `packages/app` —— 只登记一份时另一份被摘线无人喊红)。GA1 与 GA4 **共用同一遍遍历**(`walkAffordanceChildren`),两条对"可证 affordance"的定义必须同形 —— 各写一遍必然在栈深/祖先窗口上漂移。口径同 77/83:全量判 HEAD blob、`--staged` 判索引、**棘轮锚点 = 该文件 HEAD 自身违规数**(行尾字符箭头存量 70 处只报数不拦,免得恒红逼人绕过钩子连带废掉全部门);豁免**三条互不串门的通道**:`glyph-arrow-exempt: <原因>`(GA1/GA2,只认命中同行)、 `back-label-exempt: <原因>`(GA4/GA5,认命中行、可点元素起始行、及其紧邻上行)、**文件级** `nav-chrome-exempt: <原因>`(GA6 —— 它的"错"是页面配置 × 页面渲染的组合,不落在某一行上,逐行豁免无意义)。三个族都已登记进守门 108 的存活期表(`back-label`/`nav-chrome` 取 365 天:结构性定性而非待偿债务,短周期到期逼人删标记、删了又被判红,两道门互咬)。前两条**宽严刻意不对称**且各由自检钉住:GA1 若也按块放行,一个标记就能救整棵子树,"一行救不了别处"那条反向锁就没了;GA4 若只认同行,人按直觉把标记写在可点块上就永远不生效(初版即如此,四处真实站点全部落空 ⇒ 恒红)。**原因不得由注释闭合符冒充**(裸标记 + `*/` 曾被读成"带了原因"⇒ 整行免检;该洞自 §22c 立项起就在 GA1 里,由 GA4 的自检反手抓出,现两条通道同锁,全仓零裸标记存量故改严无债务)。取证 `--self-test` 81 例(含阳性对照、棘轮四向、预筛必须是判据字面量**超集**的对账 —— GA5 的 `«` 与 GA6 的 `BackChevron`/`NavBar`/`navigationStyle` 三个标识符少一个,门就在自己立项那一型上失明;GA6 的继承链三态、GA5-B 的"去掉判据即翻绿"存在性证明)+ 镜像测试 16 例(含"GA1/GA4 豁免宽严不得悄悄对齐"、"预筛漏字形=门对该形态全盲"、**"GA5/GA6 必须真挂在 `scan()` 上"**(守门 70/76/81 同型:函数在、自检过,但 `scan()` 没调它 = 提交链上一路绿灯)、"GA6 不得按磁盘读 config"、"新增豁免族必须进 108 存活期表")。紧急跳过 `HUSKY_SKIP_GLYPH_ARROW_ICON=1`。
- **GA1 存量已清零(2026-09-25 第三轮;取代上一行末「行尾字符箭头存量 70 处」那个读数)**:
  70 处 / 31 文件的行尾 `› » → 》` 字符箭头全部换成各端矢量 —— 小程序 53 处 → `LineIcon chevron-right`
  (尺寸统一 24rpx = §4 的小程序光学档,色一律沿用该元素原 token),web 8 处 → lucide-react 的
  `ChevronLeft`/`ChevronRight`/`ArrowRight`(跟随原字号),共享屏层 9 处 → lucide-react-native 的
  `ChevronRight`(**沿用原 fontSize,只换载体,不顺手改观感**)。随之失去使用者的样式规则一并删
  (`member-arrow`/`pf-arrow`/`vs-share-arrow`/`cd-aigc-arrow`/`card-entry-arrow`/`ask-create-picker-arrow`
  与 `itemArrow`、`modelBarArrow` 的 `fontSize`),仍被别处引用的(`py-coupon-arrow`/`income-stat-value`)保留。
  **棘轮锚点仍是「该文件 HEAD 自身违规数」** —— 存量清零不等于判据变严,判据一个字没改。
  **路上踩到两条,记为规矩:**① 补 import 不得用 `/^import /` 找「最后一行」——多行 import 的首行也匹配,
  于是新 import 被插进别人 import 的中间,当场写坏文件(eslint 报 `Parsing error: Identifier expected`);
  要按「以 `from …` 收尾的完整 import 行」定位。② 样式工厂里的局部色变量不得直接搬到组件 JSX 作用域
  (`arrowColor` 定义在 makeStyles 内、JSX 在组件内 ⇒ 改用组件作用域的 `tk.text.tertiary`),
  否则 lint 判 unused、运行时取到 undefined。
- **守门 102 的 HEAD 存量已全清 + 覆盖面换成实测(2026-09-26,终读:S0/GA1/GA2/GA4/GA5/GA6 全部 0)**。三件事值得单独留成规矩:
  ① **"全端已覆盖"不能建立在"恰好没有这种文件"上**:原 `TSX_RE = /\.tsx$/` 让 GA1/GA2/GA4/GA5 只在 `.tsx` 上判。
  实测受管 12 目录里 `.js/.html` 共 13 个、喂门自己的 `auditFile` 违规 0(命中全是注释与 config),全仓 `.jsx` 存量 0,
  CSS `content:` 用字符箭头 0 处 —— 数字上"不扩今天不漏",但仍把 `TSX_RE` 扩到 `\.(tsx|jsx)$`(`SRC_RE` 同步),
  并加自检锁"同一条违规在 `.jsx` 与 `.tsx` 上必须同判"。**为什么**:本门立项的理由就是"一条门只管自己立项那一型"
  (左向 `‹` 不在字符集里 ⇒ 9 个真站点长期零判据);一枚新 `.jsx` 就能让这句话静默作废而账面仍全绿。
  锁的有牙证明靠变异:`TSX_RE` 改回 `/\.tsx$/` ⇒ 自检 82/83 且红的正是这一条,还原后 83/83 并与备份 `diff` 逐字一致。
  ② **留在工作树里等 owner 提交的回退,必须当场证明"提交它会被拦"**:最后 8 处所在四文件被并发会话持有
  (工作树各有 19–613 行在飞改动),本票按「`git show HEAD:<path>` ⊕ 锚点精确替换(计数断言)→ 私有索引 →
  commit-tree + CAS」落地,不交工作树版本。其中 `SettingsScreen.tsx` 的工作树副本把 HEAD 的 `<BackChevron/>`
  换回「返回」两个字 —— 把那份 blob 只放进 `GIT_INDEX_FILE` 指的探针索引、按 `--staged` 喂本门 ⇒
  **exit 1 并点名 GA4**。"延后"与"放开一条回退通道"的差别,就是这条证明。
  **另:commit-tree 之后必须逐路径把主索引对齐到新 blob** —— 否则那四个路径留在"索引 == 旧 HEAD"的 `M ` 形态
  (§12d 第三层描述的形态),此后一次不带 pathspec 的普通提交就把修法写回旧版。
  ③ **GA5 那 1 处的正确处置是带理由豁免,不是矢量化**:它在 `ModelConfigDialog.tsx` 的 `RatioSelector` 内,
  实测该组件作用域里没有 `t`、也没有 `tokens`(换矢量要动组件签名)—— **不得为消红去改别人组件的 API**;
  与 `packages/app/src/components/Selecter.tsx:403` 同一处置(同为步骤回退标签,`back-label-exempt` 带原因与到期日)。
- **miniapp 原生 chrome 两份副本已收口为派生态 + 专属门**(2026-09-25 立):`apps/miniapp-taro/src/theme.json`(微信 darkmode 配置,`app.config.ts` 以 `@变量` 读取的编译期源)与 `apps/miniapp-taro/src/lib/theme.ts` 的 `THEME_CHROME`(运行期 `setNavigationBarColor`/`setTabBarStyle`)此前是手抄且**零生成器、零守门**(`grep -rl theme.json scripts/*.mjs` 立项时零命中)。现由 `scripts/sync-miniapp-chrome.mjs` 派生(原位写回、幂等;同色不同字节如 `#A3A3A3` 保留文件原字节,首跑不产伪 diff),挂进 `TOKEN_SYNC_TARGETS`(trigger `tokens`,failMode `block`),由守门 `scripts/check-miniapp-chrome.mjs`(guardian-runner id **122**,blocking,紧急跳过 `HUSKY_SKIP_MINIAPP_CHROME=1`)复核。**登记分流是记录在案的决策,不是漂移**:dark `navBg`/`windowBg` = `#262626` 出自 commit `d2d80c1b23`(2026-09-06)明文「对齐 rn gray.800」,而源头 `.dark --color-background` = `hsl(0 0% 14%)` = `#242424`;该分歧登记在生成器的 `CHROME_DECLARED_DIVERGENCE` 且带 `nearToken` 机判 —— 源头一旦重新同值即判"登记表腐烂"红。**不得**把 #262626「同步」回 #242424,那是回滚那枚决策;`tabSelected` 的 #8fb8cc/#a3c4d6 即 `--color-brand-accent` 两档(2026-09-14 定稿的跨端强调色)。
- **小程序端「源 ↔ 派生产物」存续性对账**(105):`scripts/check-miniapp-generated.mjs`(**warn,刻意不 blocking**)—— 管四件"生成器造好了但没人跑"的派生产物:i18n 离线包 `remote-locales.gen.ts`、`static/images/icons/*.svg`、`LineIcon/icons.ts`、`assets/tabbar/*.png`。四条判据:**G1** 源里有而产物没有 ⇒ 判红(端上会缺词、图片 404 且**不报错**);**G2** 产物里有而源已无 ⇒ 默认只报数,`--strict` 才判红(删产物属 §7 删除安全);**G3** `icons.ts` 有键而 `icons/` 无同名 svg ⇒ **永不判红**(图标源在"图片全量外置 CDN"那轮被清空,判红等于造一道恒红门);**G4** 模板字符串拼出的动态路径结构上判不了 ⇒ 如实计数,绝不静默成"看起来全绿"。取材面同 70/77/83/98/101:全量判 HEAD blob、`--staged` 判索引 blob、`--worktree` 只作人工与 dev 链逃生舱;取不到 ⇒ **exit 2「无法判定」**。定级 warn 的实证理由:立项当天全量面就有 8 处 G1 + 9 处孤儿在红,挂 blocking 会让每次提交都被逼 `--no-verify`(§12e 那型);**升 blocking 的前置条件是 G1 存量清零**。取证 `--self-test`(15 例,成对正反例、零副作用);紧急跳过 `HUSKY_SKIP_MINIAPP_GENERATED=1`;它同时被 `dev-weapp.mjs` 消费 —— 判据要在有人跑它的那一刻才成立。
- **自动归档**:`scripts/archive-completed-tasks.mjs` 扫描完成 ≥7 天的条目,post-commit 钩子自动 `--auto-commit`,归档 commit 设 `IHUI_ARCHIVE_COMMIT=1` 防递归。
  - ⚠️ **本条的前半天读数已被同日晚间的实测成反向,现值以这一格为准(2026-09-25 深夜)**:链已由 `edb225c909` 打开,而**起效的载体是 `apps/miniapp-taro/src/app.css` 里的 `@source "./**/*.{ts,tsx}"`**,不是 `config/index.ts` 那行 `tailwindcss: {…}`(附⑧的归因更正)。落地后果实测:命名档 utility 已进产物、C1 类名覆盖 0.79% → **32.15%**、主包**净省 61,654 B**(旧"余量 42,401 vs utilities 42,392,开链会撑爆"这组数**方向是反的**,作废)、余量 114,379 B;`text-card` 同名双义已因改名 `.aigc-text-card` 消除。**仍然成立只剩一点**:项目**色**档(`bg-*`/`text-*`/`border-*` 含整个 `/alpha` 族)依旧 0 落地 —— v4 只认 `@theme`,而副本色值写在普通 `(:root/.dark)`,所以"R6 绿灯 ≠ 到端生效"这句对本条**不变**,色档接线是 O62 **附⑧补** 的在账项,不得读成已收口。
- ⚠️ **但 `apps/miniapp-taro` 实际跑的是 Tailwind v4.3.3,不是端内 `package.json` 声明的 v3**(2026-09-25 实测,详见 PROJECT_PLAN O62附⑤):`weapp-tailwindcss@5.2.9` 引用的是 **`@tailwindcss/postcss`**(v4 专属入口 —— v4 的 main 导出会**直接报错拒绝**被当 postcss 插件用),产物 base 层也带 v4 指纹(`border:0 solid` 合并写法、`--tw-gradient-position`,且无 v3 独有的 `--tw-bg-opacity`)。**取版本别只读 `package.json`** —— 那样会得到相反结论(本会话就错过一次:用 `resolve('tailwindcss/package.json')` 探到 `MODULE_NOT_FOUND` 就否掉了正确说法,真实依赖名是 `@tailwindcss/postcss`)。
  - ⚠️ **同日晚间顺着解析图复核,这条结论仍成立、但当时用的依据是错的,不得照它推论**:"weapp-tailwindcss 引用 `@tailwindcss/postcss`"在依赖图里**不成立** —— miniapp importer 的 weapp-tw peer 链的是 `tailwindcss@3.4.19`,而 `@tailwindcss/postcss@4.3.3` 是 **apps/web importer** 的依赖(从 miniapp 锚 resolve 得 MODULE_NOT_FOUND)。v4 的真身是 weapp-tailwindcss **自带 vendored 引擎**(其 dist 导出 `loadTailwindV4DesignSystem`)。判"这端到底跑哪个引擎"的两条可靠证据是:**产物指纹**(`--tw-leading`/`--tw-gradient-position` 在、`--tw-bg-opacity`/`--tw-text-opacity` 0)与**包内 vendored 路径**,不是"谁的 package.json 里出现过后者" —— 后者会把别的 importer 的依赖读成本端的(本仓 §5b/§15b 记过同型:"凡四类机器事实,每次按当次实测取值")。两条后果:① 上面 R7 拦的裸 rpx 错属性在 **v3 与 v4 上行为一致**(已用 v4 + 真实源码逐字复测),所以判据与那 1,341 处改写**不因版本换人而作废**;② 但"miniapp 的 `/alpha` 靠 `tailwind-alpha-plugin`"这个前提**尚未证实** —— v4 走 `color-mix` 是另一条路,插件的确定价值在 **mobile-rn(NativeWind v3)**。带真 theme 复测前,**不要把 R6 对 miniapp 那一侧的登记要求当成已证事实扩严**。
- **豁免到期账**(guardian id **108**,blocking,紧急跳过 `HUSKY_SKIP_EXEMPTION_EXPIRY=1`):`scripts/check-exemption-expiry.mjs` 钉"豁免只有出生、没有死亡"。实测全仓 **12 类**行内豁免 / 252 处 / 107 文件,**带真到期日 0 处**(这三个数是**立项读数、不是现值** —— 现值一律跑 `node scripts/check-exemption-expiry.mjs --json` 看 `totals`)。2026-09-25 补第 13 族 **`back-label-exempt`**(守门 102 GA4 的合法例外通道),在 `FAMILY_LIFETIME_DAYS` 给它 **365 天**而非同门 `glyph-arrow-exempt` 的 30:它是**结构性定性**(该位置的「返回」是按钮文案而非导航箭头),位置性质不随时间改变;短周期到期只会逼人删标记,删了又被 GA4 判红,**两道门互咬**。E1 新增不带到期日 ⇒ 红;E2 已过期仍生效 ⇒ 红;E3 lint 抑制面(`eslint-disable` / `@ts-ignore`)只报数。**三重防恒红**:存量进 `scripts/exemption-expiry-baseline.json` 只报数、E1 锚点 = 该文件该族在 **HEAD 自身的存量数**(棘轮)、`grandfatherUntil` 前不追存量 —— 与改动无关的 blocking 红只会逼人 `--no-verify` 连带废掉全部守门。到期**只判红只点名,绝不自动摘除任何人的豁免**。**记账面 = 被豁免的那一侧**:`scripts/**` 里的标记是"门在描述自己的出口"(判据正则 / 头注 / 镜像测试夹具),只报数不入账(实测 HEAD 面 372 条里 125 条在那一面);不收窄的话,**任何新增带豁免出口的门都会在自己立项那枚提交上被 E1 判红**——锚点是该文件 HEAD 自身的 0,而标记不写出来门就不工作,等于没有合法出口。这条规矩写在 `mergeBaseline` 而不是手工清一次:旧基线被一次回退带回来时,下一次 `--update-baseline` 自行收干净。副产品:第 12 族 `r3-cta-exempt` 3 处是从来没生效过的悬空豁免(守门 83 只认 `r5-cta-exempt`),归属该门持有者。
- **miniapp 原生 chrome 两份副本已收口为派生态 + 专属门**(2026-09-25 立):`apps/miniapp-taro/src/theme.json`(微信 darkmode 配置,`app.config.ts` 以 `@变量` 读取的编译期源)与 `apps/miniapp-taro/src/lib/theme.ts` 的 `THEME_CHROME`(运行期 `setNavigationBarColor`/`setTabBarStyle`)此前是手抄且**零生成器、零守门**(`grep -rl theme.json scripts/*.mjs` 立项时零命中)。现由 `scripts/sync-miniapp-chrome.mjs` 派生(原位写回、幂等;同色不同字节如 `#A3A3A3` 保留文件原字节,首跑不产伪 diff),挂进 `TOKEN_SYNC_TARGETS`(trigger `tokens`,failMode `block`),由守门 `scripts/check-miniapp-chrome.mjs`(guardian-runner **id 以 runner 现值为准** —— 本条初登 122,落地前该号被另一路的 `check-file-write-safety` 占用,按"后来者改号"顺延为 **124**;blocking,紧急跳过 `HUSKY_SKIP_MINIAPP_CHROME=1`)复核。取材面与 36/77/83/93/98/103 同:全量判 **HEAD blob**、`--staged` 判索引 blob、`--worktree` 只是人工/验生成器写回的逃生舱,两面旗同给 ⇒ exit 2;取不到判"无法判定",**不回落**另一个面。**登记分流是记录在案的决策,不是漂移**:dark `navBg`/`windowBg` = `#262626` 出自 commit `d2d80c1b23`(2026-09-06)明文「对齐 rn gray.800」,而源头 `.dark --color-background` = `hsl(0 0% 14%)` = `#242424`;该分歧登记在生成器的 `CHROME_DECLARED_DIVERGENCE` 且带 `nearToken` 机判 —— 源头一旦重新同值即判"登记表腐烂"红。**不得**把 #262626「同步」回 #242424,那是回滚那枚决策;`tabSelected` 的 #8fb8cc/#a3c4d6 即 `--color-brand-accent` 两档(2026-09-14 定稿的跨端强调色)。
- **派生面登记表必须自证自洽(守门 `scripts/check-token-sync-registry.mjs`,编号以 runner 现值为准,紧急跳过 `HUSKY_SKIP_TOKEN_SYNC_REGISTRY=1`)**:`scripts/lib/pre-commit-hook.js` 的 `TOKEN_SYNC_TARGETS` 是"改 `tokens.css` ⇒ 各端副本自动写回"的唯一登记表,每行三段 **触发文件 / 写回命令 / 复核门(`check:` 字段)**。四种腐烂过去无人看守:触发文件改名 ⇒ 该行**永不触发**(副本从此不跟随);写回脚本被删或改名 ⇒ 提交链跑到那步才崩;复核门被摘线或降级成 warn ⇒ 照样写回却没人判对错;**新加一行忘了配门 ⇒ 新派生面零覆盖** —— 最后一种最贵,表里挂着一行没人配门的行,比根本没有这行更糟,它会替人做出"已经收口了"的判断。本门按被审判的面逐行核这四条,取不到判"无法判定"。**新增一端派生副本的正确顺序**:先在 `tokens.css` 落源头 → 加生成器 → 在表里加一行(含 `check:`)→ 配那道复核门并注册进 runner ⇒ 四段同笔提交,门当场就能问出缺哪一段。修复口径是**改表与配门**,不得为消红放宽判据、更不得删表行(删行等于关掉那一端的自动同步)。
- **小程序端有第 6 个受管区 = `app.css` 的 `@theme` 色档注册区**(2026-09-25 立,`f17b5fbdda`):它不是"又一份色值表",而是 Tailwind v4 的**注册动作** —— v4 只把 `@theme` 里的 `--color-*` 认作主题档,色值若只落在普通 `:root/.dark` 上,`bg-muted`/`text-foreground` 这一整族 utility **一条都不产出**(实测真构建 0 落地)。档位仍**唯一派生自** `deriveMiniappManaged(tokens.css, 'light')` 再剔掉 `-rgb` 三元组 —— `-rgb` 进 `@theme` 会长出 `.bg-muted-rgb` 这类脏档(v4 见 `--color-*` 就当颜色档)。三条不得照抄的形态选择:**只用普通 `@theme { }`**;`@theme inline` 会把亮档值烘进每条规则 ⇒ 深色档整族失效;`@theme reference` 会给每条规则附一份亮档 fallback(更贵,且把"没定义"洗成"默默用亮档")。守门 36 现同时核 `:root` / `.dark` / `@theme` 三面(无缺档 + 逐位同值 + 反向拦 `-rgb` 脏档)。**同批判死一条判据缺陷**:`check-miniapp-css-landing` 的 C1 原拿源类名直比产物类名,而 weapp-tailwindcss 会把 **wxss 选择器**里的标点转写(`[→_b ]→_B /→_f :→_c (→_p )→_P ,→_m .→_d +→_u !→_e`,10 条全部逐字取自同一次真构建),于是 505 条"缺项"里 474 条是比对噪声。⚠️ **但"按转写表把名字认回来"这一步本身同日被我改错、已推翻(附⑩)**:转写只发生在 CSS 侧,产物 JS/WXML 侧仍是**源名**,DOM 上实测连方括号都被当空白吃掉 ⇒ 那些转写规则没有任何元素挂得上,是**死规则**不是命中。现判据是 **C4 可达性**:一个档要算"到端可用",必须**同时在 wxss 里有对应选择器、且在产物运行时 class 语料(js/wxml/wxs)里挂得上**;只在 CSS 出现的进 `deadRuleKinds` 且**不进 pct**。**但"运行时挂不上"也不是一句端常态** —— 2026-09-26 同一份 `config/index.ts` 连测三档构建,得到三种完全不同的产物形态:
  ① CSS 腿在、JS 腿整轮空转 ⇒ `hit 269/771 = 34.89% / 死规则 485 / C5=idle`;
  ② 两腿都在(补 `cssEntries` 显式入口后)⇒ `93.77% / 死规则 31 / C5=in`;
  ③ **CSS 腿整个没跑** ⇒ 产物用 Tailwind 自己的转义选择器 `.z-\[1001\]`,与运行时源名**字面相同**,
  于是 `99.35% / 死规则 0 / C5=in` —— **一门看产物的门把最坏的一档读成了最好的一档**。
  ⇒ 现判据补齐 C5(JS 腿活性)与 **C6(CSS 腿整条未跑:源码有含标点档而产物 0 个转写形态类名)**;
  任何"可达率"读数都必须**先认档再说数**,脱离 C5/C6 的单个百分比没有意义。**三条教训叠在一起**:① 判落地的门比错形态,症状不是差一点,而是把真信号淹在噪声里;② 但在把两侧名字"对上"之前,必须先问**挂载侧到底写了什么** —— "CSS 里有这条规则"与"这个样式生效了"之间隔着一维,只读声明侧的绿灯结构上排除不了"规则存在、没人挂得上"。
- ⚠️ **上面那条 `/alpha` 判据目前到不了小程序产物**(2026-09-25 实测,登记于 PROJECT_PLAN O62附②/附③):源码用到的 **687 个 Tailwind utility / 11,105 处用法**,在真实构建(`pnpm --filter @ihui/miniapp-taro build`,exit 0)的 154 个 wxss 里 **0 个产出规则**(`.flex{` `.items-center{` `.rounded-xl{` `.bg-muted{` `.text-sm{` 逐条为 0);而 **base/preflight 层是在的**(`border:0 solid;box-sizing:border-box;margin:0;padding:0`)⇒ 症状精确到"**只有 utilities 层为空**"。阳性对照:同一份 dist 里 `app.css` 手写类名 75/98 可查到规则。**机制不是** `config/index.ts` 那行 `tailwindcss: { enable: true, config: {} }` —— 实测把它改成真实配置路径后 A/B/C 三次构建产物**逐字节相同**(该键不是开关);真因指向 weapp-tailwindcss 5.2.9 **自带一份 vendored tailwind**(`dist/tailwindcss-*.js`)并由它自己的 generator 出 CSS,项目 `tailwind.config.ts` 的 content globs 没有喂进那条链。**后果**:本条 R6 判据守的是"源码用量 ↔ 登记表 ↔ 插件产出"三者一致,这三者都在**项目侧**;小程序实际产物由第三方 generator 决定,R6 的绿灯**不等于**到端生效。改这行配置的爆炸半径见 O62附③(主包余量 42,401 B 而 utilities 约 42,392 B,以及 `text-card` 同名双义会被注入白底白字)。**⚠️ 本条"0 个产出规则"的前提已于同日作废**(2026-09-25 深夜,`edb225c909` + `f17b5fbdda`):命名档与项目色档**都已产出且运行时可达**(`.bg-muted{background-color:var(--color-muted)}` 这类),主包余量 **113,504 B**。但可达性只到命名档为止 —— **含标点的那一整族(arbitrary value / `!` important / 分数)是死规则**:weapp 只转写了 wxss 选择器(`z-[1001]` → `.z-_b1001_B`),运行时 className 仍是源名、DOM 上实测连方括号都被当成空白吃掉 ⇒ 那是**三档构建模式中的第 ① 档**(JS 转写腿整轮空转),不是端常态:同一配置另有两档分别读到 93.77% 与 99.35%。旧稿此处还误写过 96.0%(把死规则计成了命中)、后又误把 34.89% 当端常态 —— 两个方向都错过,现值一律"先认档(C5/C6)再说数"(`附⑩`/`附⑪`)。现行事实见本节末"v4 色档注册区"条与 PROJECT_PLAN O62 附⑧/附⑨/**附⑩**,勿照本行执行。
- ⚠️ **上面那条 `/alpha` 判据目前到不了小程序产物**(2026-09-25 实测,登记于 PROJECT_PLAN O62附②):`apps/miniapp-taro/config/index.ts` 的 `tailwindcss: { enable: true, config: {} }` 让端内 `tailwind.config.ts`(content globs + `presets:[@ihui/design-tokens/tailwind-preset]` + 本插件)在真实构建中**从未被加载** —— 实测真实 build 后 dist 的 154 个 wxss 里,源码用到的 **687 个 Tailwind utility / 11,105 处用法有 0 个产出规则**,`.flex{` 与 preflight 指纹双双缺失;阳性对照是同一份 dist 里 `app.css` 手写类名 75/98 可查到规则,故不是检索姿势问题。**后果**:改这条配置等于让整端 11,105 处用法同时开始产出 CSS,而现状很可能是手写 CSS 已补偿过(自有类名 2,228 个),开启后好坏只能由微信开发者工具真机渲染判定 ⇒ 属未决决策项,不是已收口项。**同时提醒同源对账门的边界**:门 36/37/93/`check-miniapp-taro-style-parity` 全部只核源码与 token 源头,**没有一道看产物**;它们五道全绿而到端 CSS 为零,是可能且已发生的。新增跨端同源门时,请先问自己判的是"写的同源"还是"生效的同源"。**⚠️ 本条的"0 产出"现值已作废**(同上,`edb225c909` + `f17b5fbdda`):`app.css` 现有一个由 `sync-miniapp-tokens.mjs` 派生的 `@theme` 注册区,命名档与项目色档已产出**且运行时可达**;但含标点那一族(arbitrary value 等)是"CSS 有规则、运行时挂不上"的死规则,真实可达率**必须先认构建档位**再读(同一配置实测三档:34.89% / 93.77% / 99.35%,见下下条与 PROJECT_PLAN 附⑩/附⑪)。"没有一道同源门看产物"这半句仍然成立 —— 看产物的是 `check-miniapp-css-landing.mjs`(手动/CI,未接提交链),它现在连**运行时挂不挂得上**也一起看。
- ⚠️ **上面那条 `/alpha` 判据目前到不了小程序产物**(2026-09-25 实测,登记于 PROJECT_PLAN O62附②/附③):源码用到的 **687 个 Tailwind utility / 11,105 处用法**,在真实构建(`pnpm --filter @ihui/miniapp-taro build`,exit 0)的 154 个 wxss 里 **0 个产出规则**(`.flex{` `.items-center{` `.rounded-xl{` `.bg-muted{` `.text-sm{` 逐条为 0);而 **base/preflight 层是在的**(`border:0 solid;box-sizing:border-box;margin:0;padding:0`)⇒ 症状精确到"**只有 utilities 层为空**"。阳性对照:同一份 dist 里 `app.css` 手写类名 75/98 可查到规则。**机制不是** `config/index.ts` 那行 `tailwindcss: { enable: true, config: {} }` —— 实测把它改成真实配置路径后 A/B/C 三次构建产物**逐字节相同**(该键不是开关);真因指向 weapp-tailwindcss 5.2.9 **自带一份 vendored tailwind**(`dist/tailwindcss-*.js`)并由它自己的 generator 出 CSS,项目 `tailwind.config.ts` 的 content globs 没有喂进那条链。**后果**:本条 R6 判据守的是"源码用量 ↔ 登记表 ↔ 插件产出"三者一致,这三者都在**项目侧**;小程序实际产物由第三方 generator 决定,R6 的绿灯**不等于**到端生效。改这行配置的爆炸半径见 O62附③(主包余量 42,401 B 而 utilities 约 42,392 B,以及 `text-card` 同名双义会被注入白底白字)。
- ⚠️ **上面那条 `/alpha` 判据目前到不了小程序产物**(2026-09-25 实测,登记于 PROJECT_PLAN O62附②):`apps/miniapp-taro/config/index.ts` 的 `tailwindcss: { enable: true, config: {} }` 让端内 `tailwind.config.ts`(content globs + `presets:[@ihui/design-tokens/tailwind-preset]` + 本插件)在真实构建中**从未被加载** —— 实测真实 build 后 dist 的 154 个 wxss 里,源码用到的 **687 个 Tailwind utility / 11,105 处用法有 0 个产出规则**,`.flex{` 与 preflight 指纹双双缺失;阳性对照是同一份 dist 里 `app.css` 手写类名 75/98 可查到规则,故不是检索姿势问题。**后果**:改这条配置等于让整端 11,105 处用法同时开始产出 CSS,而现状很可能是手写 CSS 已补偿过(自有类名 2,228 个),开启后好坏只能由微信开发者工具真机渲染判定 ⇒ 属未决决策项,不是已收口项。**同时提醒同源对账门的边界**:门 36/37/93/`check-miniapp-taro-style-parity` 全部只核源码与 token 源头,**没有一道看产物**;它们五道全绿而到端 CSS 为零,是可能且已发生的。新增跨端同源门时,请先问自己判的是"写的同源"还是"生效的同源"。
- **门脚本取材面纪律对账**(118):`scripts/check-gate-face-discipline.mjs`(blocking,`stagedTriggers=scripts/`,2026-09-25 立) —— 本仓 159 道门里只有 **24** 道走 `scripts/lib/face-reader.mjs`,散写 git 读内容或按磁盘 `readFileSync(join(ROOT,…))` 判的有 **74** 道,另 32 道"读文件但找不到仓库锚点"(疑临时夹具,结构上判不了,只报数)。按磁盘判的门的结构性缺陷:共享工作树常年滞后 HEAD ⇒ 同一份 HEAD 代码在"恒红"与"假绿"之间来回跳,并把错数写回棘轮基线(守门 83 的 R3 登记一天内被整文件回退三次即此型)。**判据锚在"本次改动动过的门"上**(结构棘轮,不留会腐烂的白名单),全量档只报数 —— 一次性把 74 道判红就是逼各会话合法 `--no-verify`、连带废掉全部守门。**两层遮噪方向不同,别混用**:认 face-reader 导入与 git 动词必须"只遮注释、保留字符串"(模块说明符和 `cat-file` 本身就是字符串),认 `readFileSync` 调用必须"注释与字符串都遮"(`markHidden` 那一档连字符串一起抹,直接套它会让 F1/F2 双双假绿)。取不到 ⇒ exit 2,全量面枚举到 0 道门判死。自检 16 例 + 镜像测试;紧急跳过 `HUSKY_SKIP_GATE_FACE_DISCIPLINE=1`。
- **记忆端点属主绑定对账**(117,**blocking**,2026-09-25 升档;紧急跳过 `HUSKY_SKIP_MEMORY_OWNER_BINDING=1`):`scripts/check-memory-owner-binding.mjs` 钉的是**"认证不等于授权"**那一层 —— `apps/ai-service/app/api/memory.py` 把 `user_id` 当请求体/Query 参数收,全文件**没有一处**与令牌主体对齐;`/api/memory` 不在 `PUBLIC_PATHS` ⇒ 匿名进不来,但**任何已登录用户填别人的 UUID 就能读/改/删别人的记忆**。判洞要问"有没有把请求里的 id 与令牌里的 id 比对",不是"有没有全局中间件"。两条 Python 语法锚点必须同时认:`user_id: str = Query(...)` 与 Pydantic `Field(...)` —— **只认 Query 会让整类 POST 端点隐身**;对齐出口只写在注释里不算(自称已拆、实际没拆那一型)。**定级史(照抄旧档会判错)**:立项时 warn —— 当时未对齐存量 2 个文件;现值一律 `node scripts/check-memory-owner-binding.mjs` 自取 —— `app/api/memory.py` 已于同日 `3b0170fa176` 九端点统一 `require_request_user_id` 归零,剩下的是**此前无人枚举过的** `app/routers/rules.py`,而"怎么对齐"取决于 **v1 对外 API 能不能代表他人访问记忆** —— 实测 `apps/api/src/routes/v1-knowledge-tools.ts` 有 2 条转发是 `jsonInit(parsed.data)` **原样透传客户端自报身份**(本门把这类列成"上游透传清单"只报数,就是给选型看爆炸半径用的)。收紧成 403 与"忽略并覆盖"两种改法**都会让本门归零**,故存量未清时判红就是恒红门,唯一结局是逼人 `--no-verify` 连带废掉全部守门(§12e 同型)。**2026-09-25 前置已满足并升 blocking**:`752c6eb110b` 收掉最后一个未对齐面(`app/routers/rules.py`)后 HEAD 面实测"未对齐 0 个",runner 侧同时带 `args:['--strict']`(本门默认档只报数,不带它升档是假的)。**⚠️ 上面那句"HEAD 面实测未对齐 0 个"当次即作废,是本节里一个真实的假绿灯(2026-09-25 同日更正)**:`3b0170fa176` 把参数一律改写成 `user_id: str | None = Query(...)`,而本门两条正则只认 `: str =` ⇒ **一个端点都扫不到** ⇒ 输出"收 user_id 的端点文件 0 个 / 未对齐 0 个"并 exit 0 —— 一道刚被升到 blocking 的安全门变成永远绿灯的尺子,它既看不见当时的敞口,也看不见"明天有人把 `require_request_user_id` 删掉"。补宽正则(可选标注两档都认)后 HEAD 面实测:**收 user_id 的端点文件 2 个 / 已对齐 1 / 未对齐 1**,那一个就是 `app/routers/usage.py` 的 `/stats`+`/quota`+`/record` —— 写法是 `uid = user_id or request.state.user_id`(**自报身份优先于令牌主体**),而参数说明里的"(管理员用)"在该文件内没有任何对应校验 ⇒ 任何已登录用户带 `?user_id=<别人>` 即可读他人用量/配额。已按 `require_request_user_id` + `roleId ≥ 1` 才可代表他人收口(实测 `apps/web` 两处调用方都只传 `days`、`usage/record` 全仓零调用方 ⇒ 不影响在跑链路),回归 `apps/ai-service/tests/test_usage_authz.py` 10 例。**本条的通用教训:改被审代码的写法,必须同时改审它的正则** —— 否则"升级成 blocking"这个动作本身就是在给一台瞎掉的尺子背书。取证 `--self-test` 17→**21 例**(新增 S17–S20:可选标注的 Query/Field 各一对正反例,夹具文本逐字取自真实文件)。**降回 warn 等于重新放开新增敞口**;若 HEAD 面再报未对齐(例:有人跳过钩子塞进去),先清偿再提别的票,不得削判据。口径同 70/77/83/98/101/103(全量判 HEAD blob、`--staged` 判索引、枚举到 0 个文件判"无法判定"而非记绿 —— 判据失明不是通过)。仓里现成出口 = `app/core/jwt_auth.py` 的 `require_request_user_id`(带 `DEV_ANONYMOUS_PRINCIPAL` 开发降级,以免 ASGI in-process 的既有测试整片 401 —— 那不构成安全结论)。取证 `--self-test` 17 例(正反成对) + §22c 镜像 14 例(含"未注册时不得被判定为已装车"的方向性对照);手动问责档 `pnpm check:memory-owner`(走 `--strict`)。
- **提交活文档前必须做"工作树 ⊇ HEAD"行级对账(2026-09-24 立,一夜三次自伤换来的)**:`README.md` / `AGENTS.md` / `PROJECT_PLAN.md` 这类多会话共写的文件,工作树副本**常年滞后于 HEAD**(实测分别少 57 / 48 / 54 行),而 `safe-commit` 的 Step ④ 是 `git commit -- <pathspec>` —— 按路径取**工作树**版本,于是"只 add 本任务文件"的规范提交照样把别人已入库的行整批写回旧态,`git status`、diff 行数、typecheck、守门 71 全都看不出来。动作:① 提交前 `node scripts/merge-live-doc.mjs --file <该文档>`(exit 1 = 存在真丢失);② 需归并时 `--apply`(它按锚点把 HEAD 缺失块插回工作树,并区分"被吃掉"与"被就地改写",后者不插回以免新老并存);③ 归并后必须报"仍判 lost = 0 且长行重复新增 = 0"。**但提交前的报告有时效边界**(2026-09-24 实测:`merge-live-doc` 报 0 丢失之后、`safe-commit` 落地之前,并发会话又推进了一轮 ⇒ 那次提交仍丢掉 2 条他人登记行)。所以顺序必须是四步:**报告 → 提交 → 立刻 `git show <新提交>^..<新提交>` 做行级对账 → 有丢失就地回补**(发现手段只能是提交后的 diff,`git status` 与提交前那次报告都看不见)。判据是**字符二元组** Jaccard ≥ 0.6 —— 前缀与按空格切词在中文里都会漏(工具自己栽过一次,README 里一度同时留下 `**第 93 项 X**` 与 `**守门 X**` 两行),已由 `--self-test` 8 例钉死。登记条目见 PLAN 的 O46④/O46⑩。**⚠️ 但"`lost=0`"从来不等于"工作树 ⊇ HEAD"(2026-09-25 实测并修工具)**:superseded 这一类的定义恰恰是"HEAD 的那一行文字不在工作树里",所以工具在 `lost=0` 时印的 `✅ 工作树 ⊇ HEAD,可安全提交` 是一句**做不到的承诺**,而本会话就在那句绿灯下把 20 条别人已翻勾/已追加注记的登记行提交成了旧形态。现分三态:`lost`(插回)/ `superseded`(有人改写,不插回)/ **`stale`(工作树停在旧形态:剥掉勾选状态后 WT 是 HEAD 的严格前缀 + 唯一候选 + HEAD 已勾 ⇒ 也不插回,但报告必须喊"这几行会被退回旧态,要人工取 HEAD 形态")**。为什么 `stale` 不自动回退:实测近 60 枚提交里有 **241** 次"合法缩短"(其中 **73** 次根本不带勾选状态变化),它们与"副本滞后"在内容上同形 —— 自动回退等于连人真删的字一起复原,那比原病更响。唯一性是生命线(两条同前缀候选 ⇒ 不猜)。工具报告现在只在**逐字包含全部 HEAD 行**时才声称 ⊇,否则按三态报数并给出出口;取证 `--self-test` 16→20 例 + 镜像 3→4 例(镜像 A 例的断言方向已反过来:有非逐字存活行时**不得**出现"⊇ HEAD"字样)。
- **钩子失败的归因是量出来的,不是抄来的(2026-09-25 立,`scripts/lib/commit-gate-attribution.mjs`)**:旧版首次 commit 一失败就打印「按用户规则"hook 失败因其他 agent 代码 → --no-verify 重试"」并整批跳门 —— 它**从未计算过归因**。实测一轮 134 道门 111 通过 / 1 失败,红的是 `check-push-sync`(判远端态、与提交内容无关),措辞仍是"他人代码"。现流程:解析 runner 的失败门清单 → **逐道复跑** → 该门这次的输出**点名本次声明的文件 ⇒ 判"本任务自己的红",拒绝 `--no-verify` 并 exit 1**(必须修);一个都没点名 ⇒ 允许跳,但只说量到的话;批没跑完 / 解析不到 / 复跑全不可用 ⇒ 明写「未归因」,按应急路径落地并留痕于 `.workbuddy/safe-commit-attestation.jsonl`。**红在守门批「之外」也要点名,不得把已量到的批内结论丢掉(2026-09-25 补,G-185)**:钩子在 runner **之后**还跑若干独立 blocking 步(实测 `i18n 死 key 扫描`),它一红就 `exit 1` —— 那一轮 154 道门**全部跑完、blocking 失败 0**,而旧归因把这种组合写成「汇总里没列出失败门 ⇒ 归因未计算」,随后照样 `--no-verify` 落地(提交 `46a4c18ed` 即此型),等于**把已经拿到的守门结论整块丢弃**,账面读起来像"没跑守门"。现规则:解析到批外步骤名(`outsideBatchStep`)后仍按同一条铰链分流 —— 该步结论点名本次声明的文件 ⇒ 判 `mine`(拒绝跳门,必须修);一个都没点名 ⇒ 判 `not-ours` 但措辞必须是「批内 0 失败,红在批外这一步」并点名是哪一步、提醒随后清偿。**失效方向仍是"多要一次定向说明"**,不是"多放一次跳门",也不是"干脆不落地"。取证:C0/C0b/C1/C2/C3 五例(含"批内确有失败门时不得走这一支"的反向对照,否则批外步骤会替批内那道门洗责)。**刻意不建"机器态门 id 清单"** —— 豁免清单必然腐烂(见 §4 对 `RN_ONLY_BRAND_KEYS` 的教训)。 —— 豁免清单必然腐烂(见 §4 对 `RN_ONLY_BRAND_KEYS` 的教训)。取证:镜像测试 5/5(含装车证明 + 真实 `hook-logs/pre-commit.log` 取样校验夹具)+ 三条变异各自变红(ANSI 剥色那条登记为**等价变异**,不造假具硬凑红)。**因此:agent 不得再假设"钩子红了就能跳"** —— 被拒时那句红点名了哪个文件,就去修哪个文件;确属误判才可按 §16 显式说明后手工提交。
- **mobile-rn global.css token 同步守门**(无 id,hook :348 条件 blocking): `scripts/check-rn-global-css-sync.mjs`(blocking) —— `apps/mobile-rn/global.css` 的 `--color-*` 值与 `packages/design-tokens/src/styles/tokens.css` 逐变量对账(NativeWind 4.x 手抄副本,漂移即视觉 bug),不一致 exit 1(`:117`),staged 触及两文件之一才触发;真跑全量 rc=0(1s);无 --self-test,但镜像测试 node --test 真跑 rc=0(15 例);无应急通道(hook 未设 skip env;头注自述 `--staged` 被接受但语义仍是全量扫描)。
- **钩子失败的归因是量出来的,不是抄来的(2026-09-25 立,`scripts/lib/commit-gate-attribution.mjs`)**:旧版首次 commit 一失败就打印「按用户规则"hook 失败因其他 agent 代码 → --no-verify 重试"」并整批跳门 —— 它**从未计算过归因**。实测一轮 134 道门 111 通过 / 1 失败,红的是 `check-push-sync`(判远端态、与提交内容无关),措辞仍是"他人代码"。现流程:解析 runner 的失败门清单 → **逐道复跑** → 该门这次的输出**点名本次声明的文件 ⇒ 判"本任务自己的红",拒绝 `--no-verify` 并 exit 1**(必须修);一个都没点名 ⇒ 允许跳,但只说量到的话;批没跑完 / 解析不到 / 复跑全不可用 ⇒ 明写「未归因」,按应急路径落地并留痕于 `.workbuddy/safe-commit-attestation.jsonl`。**stdout 没汇总而要去钩子日志取第二输入源时,该汇总块必须先过"轮次绑定"这道锁(2026-09-25 补,G-179)**:钩子日志是多轮追加的,而旧锁只要求"该汇总之前的窗口里出现过本次声明的**任一**文件名"—— 那段窗口实测 48KB–202KB,里头**必然**提到 `PROJECT_PLAN.md` / `AGENTS.md`(守门 71/13c 的结论行就点名它们),于是"我自己的红"可以借别人那一轮的汇总被洗成 `not-ours`(实测提交 `48ac2c03e`:红在 eslint `no-unused-vars`,lint-staged 失败使守门批根本没跑 ⇒ 152 道门被一次误归因跳掉,HEAD 里真留下那枚 lint 错误)。现规则:**该汇总之前那次 `staged 文件清单` 回显必须完整包含本次声明集**才允许用于归因 —— 回显由钩子入口 `auditStagingFiles()` 全量打印,是把一段日志绑到具体某枚提交的唯一内容证据(日志没有时间戳、也没有轮次号,实测按 `^YYYY-MM-DD` 抓零命中)。取"包含"不取"等值":回显反映磁盘实际暂存,并发会话同窗口 staged 的东西会造成超集,而它们结构上进不了本枚提交(commit 带 pathspec)。**绑定不成 ⇒ 落 `unattributed`,照旧能提交,只是措辞不得声称"因他人代码"—— 失效方向必须是"多要一次定向说明",绝不能是"多放一次跳门"**(与本节末那条 `findingLines` 教训同一条禁令)。真仓双向证明(不是夹具):合法那一轮绑定成功并取到 `总检查数: 152(已执行 152)` 的汇总,事故那一轮被拒且点名缺的 2 个文件。**stdout 没汇总时,第二输入源必须先过"轮次绑定"这道锁**(2026-09-25 补,G-179):钩子日志是多轮追加的,而旧锁只要求"该汇总块之前的窗口里出现过本次声明的**任一**文件名"—— 那段窗口实测 48KB–202KB,里头**必然**提到 `PROJECT_PLAN.md` / `AGENTS.md`(守门 71/13c 的结论行就点名它们),于是"我的红"可以借别人那一轮的汇总被洗成 `not-ours`(实测提交 `48ac2c03e`:红在 eslint `no-unused-vars`,而 lint-staged 失败使守门批根本没跑 ⇒ 152 道门被一次误归因跳掉)。现规则:该汇总块之前那次 `staged 文件清单` 回显必须**完整包含**本次声明集(回显由钩子入口 `auditStagingFiles()` 全量打印,是唯一能把一段日志绑定到具体某枚提交的信号 —— 日志没有时间戳、也没有轮次号);缺任何一项 ⇒ 判"不是本轮" ⇒ 落到 `unattributed`。**失效方向刻意是"多要一次定向说明",绝不是"多放一次跳门"**。真仓双向证明:合法那一轮 `绑定成功` 并取到 `总检查数: 152(已执行 152)` 的汇总块,事故那一轮 `拒绝` 且点名缺的 2 个文件(4 项声明 vs 4 项回显,重名 2 项 —— 正是旧锁放过的形态)。**刻意不建"机器态门 id 清单"** —— 豁免清单必然腐烂(见 §4 对 `RN_ONLY_BRAND_KEYS` 的教训)。取证:镜像测试 5/5(含装车证明 + 真实 `hook-logs/pre-commit.log` 取样校验夹具)+ 三条变异各自变红(ANSI 剥色那条登记为**等价变异**,不造假具硬凑红)。**因此:agent 不得再假设"钩子红了就能跳"** —— 被拒时那句红点名了哪个文件,就去修哪个文件;确属误判才可按 §16 显式说明后手工提交。
- **小程序端有第 6 个受管区 = `app.css` 的 `@theme` 色档注册区**(2026-09-25 立,`f17b5fbdda`):它不是"又一份色值表",而是 Tailwind v4 的**注册动作** —— v4 只把 `@theme` 里的 `--color-*` 认作主题档,色值若只落在普通 `:root/.dark` 上,`bg-muted`/`text-foreground` 这一整族 utility **一条都不产出**(实测真构建 0 落地)。档位仍**唯一派生自** `deriveMiniappManaged(tokens.css, 'light')` 再剔掉 `-rgb` 三元组 —— `-rgb` 进 `@theme` 会长出 `.bg-muted-rgb` 这类脏档(v4 见 `--color-*` 就当颜色档)。三条不得照抄的形态选择:**只用普通 `@theme { }`**;`@theme inline` 会把亮档值烘进每条规则 ⇒ 深色档整族失效;`@theme reference` 会给每条规则附一份亮档 fallback(更贵,且把"没定义"洗成"默默用亮档")。守门 36 现同时核 `:root` / `.dark` / `@theme` 三面(无缺档 + 逐位同值 + 反向拦 `-rgb` 脏档)。**同批判死一条判据缺陷**:`check-miniapp-css-landing` 的 C1 原拿源类名直比产物类名,而 weapp-tailwindcss 会把 **wxss 选择器**里的标点转写(`[→_b ]→_B /→_f :→_c (→_p )→_P ,→_m .→_d +→_u !→_e`,10 条全部逐字取自同一次真构建),于是 505 条"缺项"里 474 条是比对噪声。⚠️ **但"按转写表把名字认回来"这一步本身同日被我改错、已推翻(附⑩)**:转写只发生在 CSS 侧,产物 JS/WXML 侧仍是**源名**,DOM 上实测连方括号都被当空白吃掉 ⇒ 那些转写规则没有任何元素挂得上,是**死规则**不是命中。现判据是 **C4 可达性**:一个档要算"到端可用",必须**同时在 wxss 里有对应选择器、且在产物运行时 class 语料(js/wxml/wxs)里挂得上**;只在 CSS 出现的进 `deadRuleKinds` 且**不进 pct**。现测 **hit 269/771 = 34.89%(原名直中 266 + 转写可达 0 + 复合首族 3)/ 死规则 485 / 确定缺失 6**。**两条教训叠在一起**:① 判落地的门比错形态,症状不是差一点,而是把真信号淹在噪声里;② 但在把两侧名字"对上"之前,必须先问**挂载侧到底写了什么** —— "CSS 里有这条规则"与"这个样式生效了"之间隔着一维,只读声明侧的绿灯结构上排除不了"规则存在、没人挂得上"。
- ⚠️ **上面那条 `/alpha` 判据目前到不了小程序产物**(2026-09-25 实测,登记于 PROJECT_PLAN O62附②/附③):源码用到的 **687 个 Tailwind utility / 11,105 处用法**,在真实构建(`pnpm --filter @ihui/miniapp-taro build`,exit 0)的 154 个 wxss 里 **0 个产出规则**(`.flex{` `.items-center{` `.rounded-xl{` `.bg-muted{` `.text-sm{` 逐条为 0);而 **base/preflight 层是在的**(`border:0 solid;box-sizing:border-box;margin:0;padding:0`)⇒ 症状精确到"**只有 utilities 层为空**"。阳性对照:同一份 dist 里 `app.css` 手写类名 75/98 可查到规则。**机制不是** `config/index.ts` 那行 `tailwindcss: { enable: true, config: {} }` —— 实测把它改成真实配置路径后 A/B/C 三次构建产物**逐字节相同**(该键不是开关);真因指向 weapp-tailwindcss 5.2.9 **自带一份 vendored tailwind**(`dist/tailwindcss-*.js`)并由它自己的 generator 出 CSS,项目 `tailwind.config.ts` 的 content globs 没有喂进那条链。**后果**:本条 R6 判据守的是"源码用量 ↔ 登记表 ↔ 插件产出"三者一致,这三者都在**项目侧**;小程序实际产物由第三方 generator 决定,R6 的绿灯**不等于**到端生效。改这行配置的爆炸半径见 O62附③(主包余量 42,401 B 而 utilities 约 42,392 B,以及 `text-card` 同名双义会被注入白底白字)。**⚠️ 本条"0 个产出规则"的前提已于同日作废**(2026-09-25 深夜,`edb225c909` + `f17b5fbdda`):命名档与项目色档**都已产出且运行时可达**(`.bg-muted{background-color:var(--color-muted)}` 这类),主包余量 **113,504 B**。但可达性只到命名档为止 —— **含标点的那一整族(arbitrary value / `!` important / 分数)是死规则**:weapp 只转写了 wxss 选择器(`z-[1001]` → `.z-_b1001_B`),运行时 className 仍是源名、DOM 上实测连方括号都被当成空白吃掉 ⇒ 全端真实可达率现测 **269/771 = 34.89%,死规则 485 类**(旧稿此处曾误写 96.0%,那是把死规则计了命中,`附⑩` 推翻)。现行事实见本节末"v4 色档注册区"条与 PROJECT_PLAN O62 附⑧/附⑨/**附⑩**,勿照本行执行。
- ⚠️ **上面那条 `/alpha` 判据目前到不了小程序产物**(2026-09-25 实测,登记于 PROJECT_PLAN O62附②):`apps/miniapp-taro/config/index.ts` 的 `tailwindcss: { enable: true, config: {} }` 让端内 `tailwind.config.ts`(content globs + `presets:[@ihui/design-tokens/tailwind-preset]` + 本插件)在真实构建中**从未被加载** —— 实测真实 build 后 dist 的 154 个 wxss 里,源码用到的 **687 个 Tailwind utility / 11,105 处用法有 0 个产出规则**,`.flex{` 与 preflight 指纹双双缺失;阳性对照是同一份 dist 里 `app.css` 手写类名 75/98 可查到规则,故不是检索姿势问题。**后果**:改这条配置等于让整端 11,105 处用法同时开始产出 CSS,而现状很可能是手写 CSS 已补偿过(自有类名 2,228 个),开启后好坏只能由微信开发者工具真机渲染判定 ⇒ 属未决决策项,不是已收口项。**同时提醒同源对账门的边界**:门 36/37/93/`check-miniapp-taro-style-parity` 全部只核源码与 token 源头,**没有一道看产物**;它们五道全绿而到端 CSS 为零,是可能且已发生的。新增跨端同源门时,请先问自己判的是"写的同源"还是"生效的同源"。**⚠️ 本条的"0 产出"现值已作废**(同上,`edb225c909` + `f17b5fbdda`):`app.css` 现有一个由 `sync-miniapp-tokens.mjs` 派生的 `@theme` 注册区,命名档与项目色档已产出**且运行时可达**;但含标点那一族(arbitrary value 等)是"CSS 有规则、运行时挂不上"的死规则,全端真实可达率现测 **269/771 = 34.89%**(见下下条与 PROJECT_PLAN 附⑩)。"没有一道同源门看产物"这半句仍然成立 —— 看产物的是 `check-miniapp-css-landing.mjs`(手动/CI,未接提交链),它现在连**运行时挂不挂得上**也一起看。
- **门脚本取材面纪律对账**(118):`scripts/check-gate-face-discipline.mjs`(blocking,`stagedTriggers=scripts/`,2026-09-25 立) —— **门数与分档读数一律现读,本文不钉数字**(跑 `node scripts/check-gate-face-discipline.mjs --json` 看 `counts`;2026-09-26 那次实测是 face 28 / loose 74(含半接线 1)/ unknown 35 / no-content 49 —— 写死它下次收紧就变成假账)。按磁盘判的门的结构性缺陷:共享工作树常年滞后 HEAD ⇒ 同一份 HEAD 代码在"恒红"与"假绿"之间来回跳,并把错数写回棘轮基线(守门 83 的 R3 登记一天内被整文件回退三次即此型)。**判据锚在"本次改动动过的门"上**(结构棘轮,不留会腐烂的白名单),全量档只报数 —— 一次性把 74 道判红就是逼各会话合法 `--no-verify`、连带废掉全部守门。**2026-09-26 收紧:"引了 `face-reader`" 不再构成合规** —— 旧判序第一刀是 `FACE_IMPORT_RE` 命中即判 `face`,于是"import 了层、内容却仍由自己 `git show` / 磁盘 `readFileSync` 取"这种**半接线**被系统性放过。实证不是假想:门 36 与门 124 都 import 了 `face-reader`(只用 `gitRaw`),却都**默认按磁盘判**;把两者改成 `catBatch` 判 HEAD 之后,本门分类读数一字未变 —— 一道自称守取材面纪律的门,对它立项要防的那一型完全失明。现判序:① 真调用层的**读取入口**(`catBatch` / `readWorktreeFile`,含 `as` 别名与 `import * as ns` 命名空间形态)⇒ `face`;② 引了层却没用它读内容、而内容来自自派生 git 或磁盘 ⇒ **`half-wired`(提交档判红)**;③ 其余照旧 `loose-git` / `loose-fs`。**只有非读取导出不算**:`selectFace` / `gitBinary` / `gitErrText` / `catBatchOids` / `catBatchSizes` 都拿不到正文,把它们当读取凭证就等于没收紧。反向护栏同等重要:别名与多行导入必须认(否则新判据对合法写法产假阳,`T10` 钉死);`usesLayerRead` 导出给镜像用,**禁止**在测试里再抄一份判据(§22c)。两层遮噪方向不同,别混用:认 face-reader 导入与 git 动词必须"只遮注释、保留字符串"(模块说明符和 `cat-file` 本身就是字符串),认 `readFileSync` 调用必须"注释与字符串都遮"(`markHidden` 那一档连字符串一起抹,直接套它会让 F1/F2 双双假绿)。取不到 ⇒ exit 2,全量面枚举到 0 道门判死。自检与镜像例数以命令末行为准(本枚落地时 24 例 / 镜像 11 例,含 F16b 与 T9–T11 的收紧锁);紧急跳过 `HUSKY_SKIP_GATE_FACE_DISCIPLINE=1`。
  - **归档占位点名的文件必须真在审面里(守门 13c 的 A1/A2,2026-09-25 立)**:`<!-- 已归档(…):X,完整内容在 .ihui-agent/archive/<FILE> -->` 是一句**可核验的指针**,不是修辞。A1 = 盘上有 `PROJECT_PLAN_*.md` 而审面(全量 HEAD 树 / `--staged` 索引)没有 ⇒ 红;A2 = 占位点名的具体文件不在审面里 ⇒ 红(写成 `PROJECT_PLAN_*.md` 通配的占位不点名,不参与判定)。立项即抓到两条实测:`PROJECT_PLAN_dedup-2026-09-23.md` 只躺在本机(已随本票入库),以及 **8 个被 52 条占位点名的归档文件在仓库与本机两侧都不存在、其路径也从未出现在历史里** —— 即 §1 那句承诺对这批条目一直是空的。这 8 项进 `LOST_ANCHOR_LEDGER` **只报数**:它们是已发生的债,当场判红就是一台与任何提交都无关的恒红门(§12e 同型);台账是**待偿清单不是豁免清单** —— 修好了仍挂着 ⇒ 判"清单腐烂"红,删了行却没修好 ⇒ 照常红,两个方向各有一条自检钉死。按条目从计划文档历史版本逐字找回另计一票。
  - **13c 自身的取材口径(同日迁移)**:此前 `ROOT = process.cwd()` + 裸 `git show` 用 `execSync` 拼字符串(无 timeout)+ 占位合规性看 `git diff` 文本,三处都与本仓现行口径相反(工作树滞后即换结论 / 服务账户 PATH 不通 / 别人同时改这文件时合规性跟着盘走)。现全部经 `scripts/lib/face-reader.mjs`,对照面是 `--staged`:HEAD→索引、全量:HEAD^→HEAD、`--worktree` 仅人工;整面取不到 ⇒ **exit 2 无法判定**,不冒红也不记绿。取证 `--self-test` 10 条,含两条本门自己踩出来的:CRLF/LF 同一条标题不得读成"删除";**A2 的归属必须看该目录在审面上的全集**而不是只看 `PROJECT_PLAN_*` 形状 —— 本门第一次自跑就是被这条假红咬到的(已跟踪的 `orphan-capabilities-equivalence-2026-09-24.md` 被点名"落空")。
- **宿主级提示块与第三方内容的边界唯一出口**(强制,2026-09-26 立,第九轮 ZCode 吸收 A9D-1/A9D-4):CLI 侧一切"以宿主名义进提示"的块必须经 `apps/cli/src/utils/prompt-boundary.ts` —— `frameSystemReminder`(带 `kind` 的结构块,kind 是封闭集、空正文拒发)/ `neutralizeBoundaries`(非宿主内容进提示前必须过)/ `buildSkillPromptSection`(技能名单条与总量两道字节预算)。**`[系统提醒]` 这类裸文本前缀不得再被任何产出点复用**:前缀是纯文本,工具输出里出现同样字样时模型无从分辨"宿主在说话"与"第三方内容在冒充",这是结构缺陷不是措辞偏好(反第二真相由 `apps/cli/tests/prompt-boundary.test.ts` 末组钉死:源码面除唯一出口外不得再出现该字样)。第三方内容(技能正文/工具输出/fs 事件)**被省略必须在输出里留计数行** —— 静默变短等于伪造完整性,与 §5e"失败必须响"、守门 77"绝不静默成看起来全绿"是同一条禁令。不得为过测试去放宽中和判据或把预算调到 0。
- **shared 包非 Node 宿主纯度对账**(guardian id **126**,blocking,紧急跳过 `HUSKY_SKIP_SHARED_NONDE_PURITY=1`):`scripts/check-shared-nonde-node-purity.mjs` 判的是"**本地 typecheck 全绿、端上构建才炸**"那一族(与守门 72/78 同型):`packages/shared` 被 web / 小程序 / RN / `packages/app` 这些**非 Node 宿主**共同消费,而入口闭包可达面内一旦出现 `node:fs`/`node:dns` 等内建导入,`tsc` 结构上看不见(它走 tsconfig paths,不看打包器)。判据按**可达性**而非"文件里有没有 `node:`":从 `src/index.ts` + `package.json` 的**非通配** exports 做值边传递闭包(`import type` 不计,内联 `export { type X }` 保守算值边),闭包内出现内建导入即红并打印链路;**宿主直接 import 那条子路径**(绕过 barrel 的第二个入口)同样判红 —— 那正是"barrel 没导出所以巧合安全"会被一次顺手改动打破的路。三态如实:「含内建但闭包不可达」与「动态拼接/解析不到」**只报数且必须打印**(否则读报告的人会以为 shared 是纯的),取不到判"无法判定" exit 2,枚举到 0 个 shared 候选或 0 个宿主候选判死不记绿。修法唯一:把平台特有依赖改成 adapter 注入或移出共享面(§3 工厂模式),**不得**为消红放宽判据、也不得把可达文件改判成不可达;现状实测 HEAD 面不可达 1 处(`packages/shared/src/utils/ssrf-guard.ts` 的 `node:dns`,属待处置源码面)。
- **跨语言出站路由声明对账**(guardian id **127**,blocking,紧急跳过 `HUSKY_SKIP_DECLARED_OUTBOUND_ROUTES=1`):`scripts/check-declared-outbound-routes.mjs` 钉"代码里写死了要打的自家路由,而那条路由两侧都不存在" —— 声明面(Python hub 路径表 + TS `egress-call` 字面量)对注册面(Fastify 沿 `register(prefix)` 图走通 + FastAPI `include_router`/`APIRouter(prefix=)` 拼接)按归一路径段集合比对,参数段 `{x}`/`:x`/模板插值同视。**三态是这门的全部价值**:命中 / 未匹配 / **不透明前缀下的未判定**(子树挂载、同文件 const 插件、`app.mount` 一律承认看不见,不得把"看不见"洗成"确信没有")。默认档只报数、`--strict` 才判红 —— 实测 12 条存量(6 条 hub 死支柱 + 6 条 api 自转发)若当场 blocking 就是一台与任何提交都无关的恒红门,唯一结局是逼人 `--no-verify` 连带废掉全部守门(§12e 同型);问责入口 `pnpm check:outbound-routes`。豁免 `route-declare-exempt: <原因>` 只救本行且须带原因,**禁止**用基线文件消红。口径同 70/77/83/98/101/103/126(全量判 HEAD blob、`--staged` 判索引、两面旗同给 exit 2、注册集为 0 判死不记绿)。取证 `--self-test` 51 例 + §22c 镜像 6 例(含 T1"接线成套性:已接则必须 blocking + skipEnv 齐备"、T4"无提交判死"、T6 取材面形状锁)。
- **工具名本地化**(55/56):check-tool-name-display-coverage(注册表 `_TOOLS` × 词表 × 五语言 taskStatus 三方比对,blocking,拦"新增工具不补功能名");check-tool-display-resolvable(91 个功能名在 shared + 5 端合并视图 + 小程序离线包逐语言解析,拦两类静默失败:端内取词缺键回显 `toolReadFile`、忘跑 `pnpm gen:i18n` 致离线包过期;`node --test scripts/tests/check-tool-display-resolvable.test.mjs` 自检)。紧急跳过 `HUSKY_SKIP_TOOL_NAME_COVERAGE=1` / `HUSKY_SKIP_TOOL_DISPLAY_RESOLVABLE=1` **判定面 2026-09-26 收口(同 36/124/93 口径)**:默认 **HEAD blob** / `--staged` **索引 blob** / `--worktree` 逃生舱 / 两面旗同给 exit 2 / 取不到 ⇒ exit 2 **不回落**;34 个输入在同一次 `cat-file --batch` 里**同面同轮**读满(旧写法把清单来自磁盘、内容来自 `git show` 混着取,并发会话推进的瞬间会产出自洽却错位的尺子)。`--self-test` 也改跑在被审面上。**反向锁的去处**:本门此前用"只报数"冒充"扫过了",现在未判定与通过是两件事 —— 缺语言包等 4 个必需输入取不到即判死。镜像测试新增 `scripts/tests/check-tool-display-resolvable.test.mjs` 的 F1–F4(四态 / 临时仓索引≠磁盘 / 形状锁),并把原"装车证明"从磁盘面重指到 HEAD 面(断言本体一字未改)。
- **i18n JSON 同层重复键守门**(guardian 条目 `2e-dupns`,blocking,紧急跳过 `HUSKY_SKIP_I18N_DUP_NS=1`): `scripts/check-i18n-duplicate-namespaces.mjs` —— 字符级深度栈扫 `packages/i18n/messages/**/*.json` 找同一对象层重复键(JSON.parse last-wins 静默遮蔽),发现即 exit 1。**判定面 2026-09-26 收口**:默认判 **HEAD blob**、`--staged` 判**索引 blob**、`--worktree` 只作人工/夹具逃生舱、两面旗同给 exit 2、任一面取不到 ⇒ **exit 2 无法判定且不回落**;清单(`ls-tree` / `ls-files`)与正文(**一次** `cat-file --batch`)**同面同轮**;枚举到 0 个 `.json` **判死而不记绿**。收口前的真实代价已量化:它按磁盘判而 runner 一律追加 `--staged`(它不认识),于是并行会话在 `packages/i18n/messages/**` 里的半编辑语料**把当天每一次提交都钉红**(实测一轮 4 次 `--no-verify` 归因里都有它)—— 一次绕过约等于全部 156 道门对该提交作废(§12e 同型)。同批修掉 `const ROOT = process.cwd()`(扫哪棵树由调用者站哪决定 ⇒ 守门 70 的镜像测试 13/14 恒红那一型),ROOT 改由脚本自身位置推导;`--root <dir>` 保留为**测试通道**且**只在 `--worktree` 档有效**(换根却仍按 HEAD/索引读 = 双根分裂,直接判死)。镜像 7 例:R1/R2 定根、R3 通道三态、R4 形状锁(收口后方向反转:必须引 `face-reader` 且必须出现 `catBatch(`,否则视为退化)、R5 面四态纯函数、**R6 同一棵临时仓三面三答**(HEAD 干净 / 索引脏 / 磁盘脏,且索引摘除后不得借 HEAD 内容凑数)、R6b 空目录必须给可诊断结论、禁打"✅ 无重复"。

>

- **D3(2026-09-26 G-196 新增):别名导入解析不到文件必拦**。旧射程只判仓内相对路径,于是 `@/stores/x` 指向一个**从未写下**的模块时本门永远看不见 —— G-195 那处就是这样进了 HEAD、生产构建死、154 道门全绿。判据取 `tsconfig` 的 `compilerOptions.paths`(只认尾随 `*` 的前缀映射;`extends` 与精确键刻意不碰,认它们就要完整 resolver),口径与 D1/D2 同形(全量 HEAD blob / `--staged` 索引 blob / 棘轮锚点=该文件 HEAD 自身)。HEAD 现测那一处进 **`KNOWN_ALIAS_LEDGER` 只报数不判红** —— 存量未清就上 blocking 就是恒红门(§12e),**修好 G-195 必须删台账行**。两条反"假绿"锁由镜像钉住:别名表为空时不得把"0 处"当通过;台账外的任何 D3 一律红。**踩过的两个坑值得抄下**:`stripJsonc` 第一版是**正则**剥块注释,而每个 tsconfig 的 include 里都有 `"**/*.ts"` 这种 glob —— 串里同时含着块注释的开与闭序列,整段 JSON 被吞 ⇒ 别名表静默为空 ⇒ 判据失明而报告一切正常(`node --check` 随后炸在我描述该 bug 的注释里,因为注释把那两个序列原样写了出来 —— **说明性文字也会带执行性字符**);以及 `readHead` 只按"源文件清单"建批量读,拿它读 `tsconfig.json` 恒 null,必须**另开一次只喂 tsconfig 的批量读**。

> ⚠️ **上面这段里两个数在 2026-09-26 的三路并行审计中被测错,现值以这一格为准**:渲染位「返回」是 **47 处**
> (不是 49 —— 多出的 2 条,一条是 JSDoc 里逐字引用了 JSX,一条是 `ariaLabel=` 驼峰属性不被 `aria-`/`label=` 滤网命中);
> `‹` 字符是 **2 行**(不是 10;`{'‹'}` 在 HEAD 面 0 处,第 3 处只存在于被并发会话污染的工作树里)。
> 并且"GA4 = 0"要带一条限定:门 102 现有 **1 处自报盲区**(`--all` 点名
> `packages/app/src/features/course-screen/CourseScreen.tsx:113`)—— 跨行自闭合标签会让该处之后的遍历栈失配。
> **所以那个 0 读作"没有已判定的违规",不读作"这一族文件都被看过"**;盲区现在由 `backBlind` 探针点名,不再静默。
> 同一轮还量到"第二/三份自绘返回键"6+3 处、以及 web 端 16 处 `router.back()` 落在守门 46 扫描面之外 ——
> 清单、file:line 与处置顺序在 PROJECT_PLAN 第五十波·续末③ 的 A/B/C/D 四格,**派单前先读它**。

- **锁异常处理**:超时自动报错;超过 10min 的悬挂锁(持锁进程已死)自动抢占;紧急可删项目根 `.deploy.lock`(先确认无构建进程)。`.deploy.lock/` 已 gitignore。
  ② 两腿都在(补 `cssEntries` 显式入口后)⇒ `93.77% / 死规则 31 / C5=in`;**而热缓存仍会把整包复制进 42 个页面 wxss(主包 +1,033,700 B ⇒ 越过 2MB 硬上限),该构型由关闭 `cache.enable` 收口,见附⑫**;
- **跨语言出站路由声明对账**(guardian id **127**,blocking,紧急跳过 `HUSKY_SKIP_DECLARED_OUTBOUND_ROUTES=1`):`scripts/check-declared-outbound-routes.mjs` 钉"代码里写死了要打的自家路由,而那条路由两侧都不存在" —— 声明面(Python hub 路径表 + TS `egress-call` 字面量)对注册面(Fastify 沿 `register(prefix)` 图走通 + FastAPI `include_router`/`APIRouter(prefix=)` 拼接)按归一路径段集合比对,参数段 `{x}`/`:x`/模板插值同视。**三态是这门的全部价值**:命中 / 未匹配 / **不透明前缀下的未判定**(子树挂载、同文件 const 插件、`app.mount` 一律承认看不见,不得把"看不见"洗成"确信没有")。默认档只报数、`--strict` 才判红 —— 实测 12 条存量(6 条 hub 死支柱 + 6 条 api 自转发)若当场 blocking 就是一台与任何提交都无关的恒红门,唯一结局是逼人 `--no-verify` 连带废掉全部守门(§12e 同型);问责入口 `pnpm check:outbound-routes`。豁免 `route-declare-exempt: <原因>` 只救本行且须带原因,**禁止**用基线文件消红。口径同 70/77/83/98/101/103/126(全量判 HEAD blob、`--staged` 判索引、两面旗同给 exit 2、注册集为 0 判死不记绿)。取证 `--self-test` 51 例 + §22c 镜像 6 例(含 T1"未接线时不得被判定为已装车"的方向性对照、T4"无提交判死"、T6 取材面形状锁)。
  - **D3(2026-09-26 G-196 新增):别名导入解析不到文件必拦**。旧射程只判仓内相对路径,于是 `@/stores/x` 指向一个**从未写下**的模块时本门永远看不见 —— G-195 那处就是这样进了 HEAD、生产构建死、154 道门全绿。判据取 `tsconfig` 的 `compilerOptions.paths`(只认尾随 `*` 的前缀映射;`extends` 与精确键刻意不碰,认它们就要完整 resolver),口径与 D1/D2 同形(全量 HEAD blob / `--staged` 索引 blob / 棘轮锚点=该文件 HEAD 自身)。HEAD 现测那一处进 **`KNOWN_ALIAS_LEDGER` 只报数不判红** —— 存量未清就上 blocking 就是恒红门(§12e),**修好 G-195 必须删台账行**。两条反"假绿"锁由镜像钉住:别名表为空时不得把"0 处"当通过;台账外的任何 D3 一律红。**踩过的两个坑值得抄下**:`stripJsonc` 第一版是**正则**剥块注释,而每个 tsconfig 的 include 里都有 `"**/*.ts"` 这种 glob —— 串里同时含着块注释的开与闭序列,整段 JSON 被吞 ⇒ 别名表静默为空 ⇒ 判据失明而报告一切正常(`node --check` 随后炸在我描述该 bug 的注释里,因为注释把那两个序列原样写了出来 —— **说明性文字也会带执行性字符**);以及 `readHead` 只按"源文件清单"建批量读,拿它读 `tsconfig.json` 恒 null,必须**另开一次只喂 tsconfig 的批量读**。

### 跨端 UI 单一源:小程序端与 App(RN)端必须长成同一张脸(强制,2026-09-26 立,O81)

- **交付标准**:除登录方式与平台机制(原生导航栏/状态栏/输入法)外,`apps/miniapp-taro` 与 `packages/app`+`apps/mobile-rn` 的同一界面元素**必须逐档同值**,且**改一端另一端自动生效**。
- **自动生效只有一种实现方式**:一份与平台无关的组件源(`packages/shared`)+ 每端注入平台 adapter(§3 工厂/DI 同一条规矩)。**禁止**"两端各写一份再靠对账追平"——本仓 2026-09-22 已试过单向包装并因"同名 ≠ 同契约"删掉 4,662 行适配器。
- **可见真值仍只有一处**:色档 `packages/design-tokens/src/styles/tokens.css`、圆角 `packages/design-tokens/src/radius.js`。两端的 `global.css` / `app.css` / `rn-tokens.ts` 一律是**派生态**(见本节"端内 CSS/色值副本一律是派生态"条),**禁止手改、禁止在端内抄第二个数字**。几何档(尺寸/字号/间距)尚无单一源文件 —— 那是 O81 第二票要建的 `packages/design-tokens/src/geometry.js`,**建它必须同一枚提交就有消费方**,不得先建表再等人用。
- **对账**:`scripts/check-cross-end-ui-parity.mjs`(guardian 第 **128** 项,blocking,紧急跳过 `HUSKY_SKIP_CROSS_END_UI_PARITY=1`)。同名配对组件的单位归一后比几何档集合(`72rpx` ≡ `36px` ≡ `36dp`;Tailwind `h-9` = 36、`text-sm` = 14),差档即点名;**棘轮锚点 = `scripts/cross-end-ui-parity-baseline.json`**(立项读数:19 对同名组件 / 18 对有差异 / 174 处差异档),所以它只拦"把两端差异加大",不会把存量债变成人人跳门(§12e)。圆角与色档**不在本门射程**(守门 77 / 93 各管一段,一台尺子只量一段)。
- **差异必须显式登记**:确属平台导致的(原生 chrome、键盘避让、系统字号缩放路径不同)写进台账 `waivers.<组件>.reason` 并由守门 108 管到期;**静默不同形**与**为消红给单端补数字**都算违规。
- **不得做的事**:不得为让本门变绿去改判据或调台账数字;不得把"两份实现"读成"两份真相"。

- **可见真值仍只有一处**:色档 `packages/design-tokens/src/styles/tokens.css`、圆角 `packages/design-tokens/src/radius.js`、**几何档 `packages/design-tokens/src/geometry.js`**(2026-09-26 O81 票② 立:`GEOMETRY_PX` 一张表 → `rnGeometry`(dp) / `taroGeometry`(已折 2 倍 rpx) 两个投影,与 `radius.js` 同形)。两端的 `global.css` / `app.css` / `rn-tokens.ts` 一律是**派生态**(见本节"端内 CSS/色值副本一律是派生态"条),**禁止手改、禁止在端内抄第二个数字**。命中区/图标墨迹这类尺寸**不得**再写 `const *_SIZE = <数字>` 当端内既定档 —— 两份"唯一实现"的返回键就是那样各差 2px 的。
- **守门**:`scripts/check-prompt-injection-registry.mjs`(guardian 第 **129** 项,blocking,紧急跳过 `HUSKY_SKIP_PROMPT_INJECTION_REGISTRY=1`;手动入口 `pnpm check:prompt-injection`)。三条判据,零豁免清单(判据输入全部由被审面自身推导):**R1** 每条登记的 `producer` 文件必须在**被审面**上存在、其代码面出现该 id、且有一处记账出口调用(注释里的提及不算装车);**R2** `renderInjectionNotice(` 在生产面(非测试)至少一个调用点,且登记表与唯一出口在面上解析得到、导出仍在位;**R3** 出现 `[系统提醒]` / `[系统提示]` 这类裸宿主前缀而该文件既不调用也不定义唯一出口 ⇒ 违规,**锚点 = 该文件 HEAD 自身的违规数**(棘轮,只拦新增;把存量当场判红就是一台与任何提交无关的恒红门,唯一结局是逼人 `--no-verify` 连带废掉全部守门,§12e 同型)。口径同 70/77/83/98/103/118:全量判 **HEAD blob**、`--staged` 判**索引 blob**、`--worktree` 仅人工逃生舱、两面旗同给 exit 2、取不到判"无法判定"、枚举到 0 个候选判死。**接线史(两型同体,值得记住)**:该门立项时只有 `package.json` 一个脚本、**不在提交链**,而头注自称"guardian 第 128 项"与 runner 现值不符(128 归 `check-cross-end-ui-parity`)—— "造好没装车"与"编号谎报"同时存在,由守门 89 的 R4(已接线而文档通篇不点名)暴露;2026-09-26 按空闲号 129 补装进 `scripts/guardian-runner.mjs`,并在本节点名。**编号一律以 runner 现值为准,不得照文档派单;头注与 runner 不一致时,以 runner 为真值去改头注,而不是让后人按谎报的编号找门。**

- **parity 绿不等于内容语种对(2026-09-26 立,由 D20 实测逼出)**:上面五门判的是**键集**与"**有没有**中文残留",所以"某一块整体落成另一门语言"这一型结构上看不见。实测抓到三种形态,全部门禁当时都是绿的:① `aiChat.org` 的 **ja 装了繁体、zh-TW 装了谚文**;② `chat.exportMenu.exportPdf` 是 **ja←zh-TW←ko←ja 的三向轮转**(判据必须按**码位**量,按字形肉眼判会把谚文 `을 익스포트` 与假名 `をエクスポート` 看成同一种东西);③ `aiChat.toast.orgSaved` 被机械写成 `orgSaved:{orgSaved:"…"}` **自套一层**,`t('toast.orgSaved')` 取到对象 ⇒ 功能成功但提示永远不响。**批量落 i18n 后必须做两件事**:逐语种量"这块内容是不是这门语言"(CJK 三族互判),以及全量扫"同名自套一层"(`{"x":{"x":…}}`)—— 后一型现仓 0 处,新增即应判红。补正式门禁另计一票(现无门判这两型),在此之前不得把 parity 通过当成语种通过。
- **RN 函数形态 style 被 cssInterop 吃掉**(guardian id **131**,blocking,`stagedTriggers=apps/mobile-rn/ + packages/app/`,紧急跳过 `HUSKY_SKIP_RN_INTEROP_FN_STYLE=1`):`scripts/check-rn-interop-fn-style.mjs` 拦"函数形态 `style={({pressed}) => …}` 落在被 `cssInterop` 注册过的 RN 组件上"。真机 A/B 定案(PROJECT_PLAN「O83 登记④」):interop 对非数组声明执行 `{ ...declaration }`,而 `{ ...函数 }` === `{}`,且 `applyStyles` 先把 `state.props` 清成 `{}` 再合并 ⇒ **该元素整份内联 style 静默消失**,退回默认 column + stretch(实测把「更多」折成两行)。两个设计点:① **注册表现读**,不维护手工清单 —— 每次从 `node_modules/react-native-css-interop/…/components.js` 抽 `cssInterop(...)` 组件名,读不到判"未判定"(内置兜底表只用于"包没装时仍能报数",且表里有现读没有的名字 ⇒ 判"清单腐烂"红);反直觉事实:**`Modal` 不在注册表里**,`StatusBar` 在。正则必须认 CJS 转译形态 `(0, api_1.cssInterop)(react_native_1.Pressable, …)` —— 按 ESM 直写形态写的正则在真包上恒空,而恒空表现为"什么都没注册",门对整型缺陷全盲却一路报绿(§22c"判据失效的表现永远是安静"同型)。② 棘轮锚点 = **该文件 HEAD 自身存量数**(与 77/83/98/102/113 同族):立项实测存量 118 处 / 58 文件,当场判红就是恒红门(§12e)。行内出口 `interop-style-exempt: <原因>`(**必须带原因**,裸标记不放行;已进守门 108 存活期表 30 天)。回溯不到 JSX 开标签的命中计"未判定"并报告,**不静默丢弃**。口径同 70/77/83/98/101/103/118(全量判 HEAD blob、`--staged` 判索引 blob、两面旗同给 exit 2、取不到 exit 2)。取证 `--self-test` 11 条 + §22c 镜像 9 例(T1 装车 / T2 摘线不得被读成已装车 / T4 取材面形状锁 / T5 真仓阳性对照"看不见存量不算通过" / T6 注册表现读且兜底表是其子集)。
- **RN 函数形态 style 被 cssInterop 吃掉**(guardian id **131**,blocking,`stagedTriggers=apps/mobile-rn/ + packages/app/`,紧急跳过 `HUSKY_SKIP_RN_INTEROP_FN_STYLE=1`):`scripts/check-rn-interop-fn-style.mjs` 拦"函数形态 `style={({pressed}) => …}` 落在被 `cssInterop` 注册过的 RN 组件上"。真机 A/B 定案(PROJECT_PLAN「O83 登记④」):interop 对非数组声明执行 `{ ...declaration }`,而 `{ ...函数 }` === `{}`,且 `applyStyles` 先把 `state.props` 清成 `{}` 再合并 ⇒ **该元素整份内联 style 静默消失**,退回默认 column + stretch(实测把「更多」折成两行)。两个设计点:① **注册表现读**,不维护手工清单 —— 每次从 `node_modules/react-native-css-interop/…/components.js` 抽 `cssInterop(...)` 组件名,读不到判"未判定"(内置兜底表只用于"包没装时仍能报数",且表里有现读没有的名字 ⇒ 判"清单腐烂"红);反直觉事实:**`Modal` 不在注册表里**,`StatusBar` 在。正则必须认 CJS 转译形态 `(0, api_1.cssInterop)(react_native_1.Pressable, …)` —— 按 ESM 直写形态写的正则在真包上恒空,而恒空表现为"什么都没注册",门对整型缺陷全盲却一路报绿(§22c"判据失效的表现永远是安静"同型)。② 棘轮锚点 = **该文件 HEAD 自身存量数**(与 77/83/98/102/113 同族):立项实测存量 118 处 / 58 文件,当场判红就是恒红门(§12e)。三站点已清(见 PROJECT_PLAN「G-211 更正」),现值一律跑本门读末行,不得引用文档数字。行内出口 `interop-style-exempt: <原因>`(**必须带原因**,裸标记不放行;已进守门 108 存活期表 30 天)。回溯不到 JSX 开标签的命中计"未判定"并报告,**不静默丢弃**。口径同 70/77/83/98/101/103/118(全量判 HEAD blob、`--staged` 判索引 blob、两面旗同给 exit 2、取不到 exit 2)。**判据面先剥注释与字符串**(等长遮罩,行号不变 —— 本门按行回溯 JSX 开标签,删字符会错位;豁免标记写在注释里,所以判据看遮罩面而豁免看原文):本门第一次自跑就是被自己写的说明咬到的 —— 给 CategoryInlineBar 补一句"早先这里写 style=函数形态"之后,`--worktree` 读数从预期的 115 变成 116,**门把解释自己的散文判成了违规**(守门 70 的 URL 假注释态同型)。取证 `--self-test` 14 条(新增"注释里的该形态不得计入"+"同一形态换成真代码必须命中"成对) + §22c 镜像 10 例 —— 其中 **T10 是端到端双向锁**:往临时索引里注入一处真代码函数形态 ⇒ 必红,把同一形态只写进注释 ⇒ 必绿,两条同时成立才算"遮罩关掉的是误报、不是判据"(只留后者就是替"门瞎了"背书)。它的牙是量出来的:把判据面从遮罩面改回原文面(`RE.test(line)`),T10 的 B 臂当场翻红,红语即"注释形态不得判红"(T1 装车 / T2 摘线不得被读成已装车 / T4 取材面形状锁 / T5 真仓阳性对照"看不见存量不算通过" / T6 注册表现读且兜底表是其子集)。
  1. **派单口径只认一个入口**:`node scripts/plan-tasks.mjs --open`(未勾选 − 同题已完成副本 − 自带作废声明的行 − 同一件事的第二条待办副本;五条判据 F1/F2/F3/F4/F6 都以"编号 + 标题前缀逐字等值"的复合主键为准,不依赖行号顺序也不依赖相似度)。`grep '^- \[ \]'` 不是待办清单(2026-09-26 一次实测的**不重叠**分解:未勾选 205 = 已认领 44 + 其余排除 28 + 真待办 133;同刻 F1/F2/F3/F4 读数均为 0 —— **这些数是当次现读,派单前必须重跑,勿照抄**;报告里其余排除项的明细计数彼此可重叠,只能当诊断看,不能拿来减法核账)。归并出口:`node scripts/plan-tasks-merge.mjs --heal --commit`(F1/F2 翻勾并写明归并到谁;**F4 只加"重复登记副本"指针、绝不动勾选** —— 两条都是没做完的事,把没做的记成做过的比原病更响)。
     - **F6 是"块"级量纲,行级四条判不到它**:一整块多行登记被并发 union 追加两遍时,每一行都"只是又一个孪生行",F1–F4 与门 71 同时全绿(本会话 2026-09-26 真实自伤:探针登记块从 1 份涨到 3 份而账面什么都没喊)。判据 `findDupBlocks` 只认 **≥3 行且每行 ≥40 字符的连续 bullet 块逐字相同**(低于阈值会把台账里天然成对的短行全数成百地报出来,噪声淹掉信号);修复出口 `node scripts/plan-tasks-merge.mjs --dedupe-blocks`(先不带 `--commit` 出报告,确认四条零损失断言后再 `--commit`),它**只删第 2..N 份逐字相同的副本**,漂移副本(首行同而正文不同)一份都不动 —— 机器折半即有损,那型交人工。与 `--heal` 的关键区别是**不进 post-commit 自动档**:删行是活文档上最危险的动作。
     - **F6 是"块"级量纲,行级四条判不到它**:一整块多行登记被并发 union 追加两遍时,每一行都"只是又一个孪生行",F1–F4 与门 71 同时全绿(本会话 2026-09-26 真实自伤:探针登记块从 1 份涨到 3 份而账面什么都没喊)。判据 `findDupBlocks` 只认 **≥3 行且每行 ≥40 字符的连续 bullet 块逐字相同**(低于阈值会把台账里天然成对的短行全数成百地报出来,噪声淹掉信号);修复出口 `node scripts/plan-tasks-merge.mjs --dedupe-blocks`(先不带 `--commit` 出报告,确认四条零损失断言后再 `--commit`),它**只删第 2..N 份逐字相同的副本**,漂移副本(首行同而正文不同)一份都不动 —— 机器折半即有损,那型交人工。与 `--heal` 的关键区别是**不进 post-commit 自动档**:删行是活文档上最危险的动作。**合并落地闸要跑两把尺子**(同日由一次真实的"已付的账原地复活"逼出):`git-sync-converge` 的**无冲突** merge-tree 分支原先零看守 —— 本地把 F6 从 8 清偿到 0、远端一份陈旧计划文档带着那 8 块进来,merge-tree 判"无冲突 ✅ 推进",账就复活了;而 `union-converge` 的判据是"相对 max(两侧)",这一型里 max(两侧) 恰好等于合并结果 ⇒ **第一把尺子结构上看不见它**。现落地闸同时跑 ① 相对两侧上限 ② 相对入库基线 `scripts/plan-task-state-baseline.json`(维度清单仍只有一份 = `plan-tasks.mjs` 导出的 `probe()`,别处不得再抄),两把各有正反对照(PS-1 同形而基线收紧必点名 / PS-2 基线与实态一致不得判红 —— 第二把若恒红就等于把所有合并堵死,与恒红门同罪)。
- **对象空间落地与活文档编辑的常驻出口 = `scripts/object-space-land.mjs` / `scripts/live-doc-edit.mjs` / `scripts/gate-registry-insert.mjs`(+ 共用 plumbing `scripts/lib/bypass-git.mjs`),不是守门**(2026-09-26 立;镜像测试 `node --test scripts/tests/{object-space-land,live-doc-edit,gate-registry-insert,bypass-git}.test.mjs` = 36 例)。为什么这条要进 AGENTS:本仓在共享工作区里做旁路提交是**常态**(活文档与注册表的工作树副本常年滞后 HEAD,按 pathspec 交工作树就是把别人已入库的行整批写回旧态),而 2026-09-26 那一轮同一套 plumbing(临时 `GIT_INDEX_FILE` + `hash-object` + `update-index` + `write-tree` + `commit-tree` + CAS `update-ref`)在同一会话里被手写了 **6 份**、全躺在 `.ihui-agent/tmp/` 不受版本控制,并且已经互相漂开:索引可动性判据两种写法、零损失判据三种写法 —— 其中一份拿"重复行计数"当零损失判据,被 `guardian-runner.mjs` 里天然存在的 `  },` 与空串行顶出"凭空多出 1322 条"而**一次也没落地成功**,换成结构等值判据才通过。不入库的代价是确定的:下次会话要么再手写一遍(带着同一个 bug 的新版本),要么图省事走 pathspec。**三条判据照抄别重新发明**:① CAS 循环内**每次重读 HEAD** 且"任一目标路径被别人改过即停"(不是抢锁,是拒绝覆盖);② 零损失判据 = **结构等值**(`new == 前缀 ⊕ 本块 ⊕ 后缀`),不是重复行计数;③ 落地后 `git show --name-only` 回读声明路径,并把**共享主索引**逐路径对齐到新 blob —— **只在** 索引 blob 等于父提交 blob 或索引里没有这条时动,否则点名"归属他人"不动(§12d 第三层那型:一次不带 pathspec 的普通提交就会把旁路交付写回旧版)。取号类判据(`gate-registry-insert`)必须同认 `id: '134'` 与 `id: "134"` 两种形态 —— 双引号 id 对一切按单引号解析注册表的判据(含守门 89 的撞号 R5)隐身,这是本轮真实踩过并用夹具钉死的坑。
- **共享层重复**(40):端内重新实现 shared hook/util 检测(阻塞,防端内独立实现回升,白名单:web/useChat + web/useAuth + web/useAgentRuntime + web/useClipboard + web/useNotificationStore + mobile-rn/useAuth + mobile-rn/useNotificationStore(后二者均为 re-export wrapper + 平台 adapter,属 §3 允许形态;守门的 re-export 判定只看 export 那一行有无 `from`,函数型 wrapper 必须显式登记))。〔归并补(副本独有说法,现行判据以上文为准):-**共享层重复实现守门**(40):`scripts/check-shared-layer-duplication.mjs`(blocking)——apps/*/src的export名与packages/shared/srcexport名求交集(`collectExports:21`,`scanAppDir:85`),端内重新实现即exit1(`:152`);(原 L1422) / 无应急通道(runner无skipEnv;(原 L1422) / re-exportwrapper豁免逻辑在白名单)。(原 L1422)〕
- **品牌邮件通道对账**(81):check-brand-email-channel.mjs(blocking,2026-09-23 立)—— 拦绕过品牌模板层的 ops 自发纯文本邮件("能发出去、typecheck/lint 全绿,用户收到的邮件没样式",与守门 72/78 同族):R1 `Send-MailMessage` 调用语句缺 `-BodyAsHtml` / R2 `api.resend.com/emails`(含 host+path 分行形态)同一发送上下文无 `html` 字段 / R3 有发信动作(`createTransport`/`sendMail`/`api.resend.com`)却不引用 `email-templates` 也不调用 `notify-deploy-failure`;范围 `deploy/**`+`scripts/**`(不含 tests)+workflows `*.yml`,注释行/裸域名不判(宁漏不误报),行内豁免 `brand-mail-exempt: <原因>`,存量走 `scripts/brand-email-channel-baseline.json` 只减不增(**2026-09-24 实测基线 `counts` 已为 `{}`** —— 建门时揪出的第三条通道 `scripts/check-credential-health.mjs` 已迁到同一派发器,未用 `brand-mail-exempt:` 糊过去)。**范围已两度扩面**(2026-09-24):原只扫 `deploy/**`+`scripts/**`,而告警邮件正文的实际出口在 `monitoring/alertbridge/*.cjs` —— 目录与扩展名**双重不可见**;现含 `monitoring/**` + `.cjs/.mts` + `apps/api/scripts/**`,并新增两条判据:**R3b** 接了唯一出口却自带一份 HTML 版式(三段与门:邮件版式标记 ∧ ±10 行样式指纹 ∧ 本文件确在邮件语境,注释行不计)—— R1/R2/R3a 只看"有没有走模板",对这一型全盲;**R4 告警接收面** `monitoring/alertmanager/**` 的 yml/yaml/tmpl 出现 `email_configs`/`smtp_*`(AM 原生邮件 = 无版式纯文本)、IM 中转 receiver(名字与 host/路径**都**认,`feishu-copy` 改名躲不过)、或 `webhook_configs` 里 url ≠ `BRIDGE_URL` ⇒ 红;注释行不判红但 `amCommentLiterals` 如实计数。**全仓不扫 yml**(根 compose / prometheus / loki 的历史注释里就写着 `dingtalk-webhook`,全仓扫必假红 —— 面外写进断言)。`stagedTriggers` 与扫描面**必须同步扩**,否则"只改 bridge/模板的提交"根本不唤起本门(判据存在而永不调用 = 没有,守门 70/76 同型;该字段今晚被并发整文件回写抹掉**三次**,由镜像测试"装车证明"抓回)。**修法的唯一姿势:ops 邮件一律经 `apps/api/scripts/notify-deploy-failure.ts`(版式由 `email-templates.ts` 单点决定),不得端内自拼传输层;Alertmanager 出口只有 bridge 一条**。取证 `--self-test` 61 例(正反成对+临时仓双取材面)+ 镜像测试 20 例(含装车证明与 `BRIDGE_URL` 与渲染器逐字等值的漂移即红断言);`--staged` 暂存集为空/取不到 → 退化全量(守门 70 教训);紧急跳过 `HUSKY_SKIP_BRAND_MAIL_GUARD=1`。〔归并补(副本独有说法,现行判据以上文为准):范围`deploy/**`+`scripts/**`(不含tests)+workflows`*.yml`,注释行/裸域名不判(宁漏不误报),行内豁免`brand-mail-exempt:<原因>`,存量走`scripts/brand-email-channel-baseline.json`只减不增(建门实测:ps1自拼通道已被并行会话清除,副产品揪出第三条通道`scripts/check-credential-health.mjs`已入基线待迁移)。(原 L1395) / **修法的唯一姿势:ops邮件一律经`apps/api/scripts/notify-deploy-failure.ts`(版式由`email-templates.ts`单点决定),不得端内自拼传输层**。(原 L1395) / 取证`--self-test`30例(正反成对+临时仓双取材面)+镜像测试8例(含装车证明);(原 L1395)〕
- **主题接线对账**(91):check-theme-prop-wiring.mjs(blocking,2026-09-24 立)—— 堵"顶栏深色 + 正文浅色"这类**同屏双色档案**:`packages/app` 213 个 theme-driven 组件的形参默认值是 `colorScheme = 'light'`,调用方漏传就静默脱主题,类型系统完全不红(`colorScheme?:` 可选)。三判据 —— **missing**(渲染点未传该属性)/ **literal**(写死 `'light'` 或 `'dark'`)/ **spread-unknown**(仅 `{...props}` 展开且回溯不出来源,判不出即如实报数,不静默放行)。建门实测:此前一轮用"JSX 元素文本里有没有 `colorScheme` 字样"统计出"漏传 0 处",**该结论是错的** —— 换成花括号深度扫描 + 组件清单自动推导后真实命中 **118 处 / 117 文件**,即 115 个屏一直在静默锁死浅色档案。**盲区已补(比漏修更值得记)**:写死的 `'light'` 有 5 处藏在**对象构造里**而非 JSX 属性上(`const props = { colorScheme: 'light' }; return <SharedX {...props} />`),"字面量"判据只解析 attrs 故从不判红,"未接线"判据见到 spread 就笼统归"判不出" —— 这 5 屏以"待人工核"的名义静默锁死浅色,**门一直是瞎的**。现由 `resolveSpreadThemeValue()` 回溯对象构造再判:对象里有字面量 → 判红;对象里确实没这个键 → 判 missing;props 来自函数形参且本文件无对象字面量 → 仍承认判不出,不猜。**基线 `scripts/theme-prop-wiring-baseline.json` 现须为空 `{counts:{}}`(零容忍)**,`--strict` 口径为"0 未接线 / 0 字面量 / 0 判不出"。行内豁免 `theme-wiring-exempt: <原因>`(须带原因)。取证 `--self-test`(**含阳性对照**:注释里写 `colorScheme: 'light'` 不得误判、不传 `src` 时行为与旧版一致)+ §22c 镜像测试 `scripts/tests/check-theme-prop-wiring.test.mjs`(含**装车证明**:runner 里必须真有 id 91 + blocking + skipEnv,且本门编号在 runner 中恰好出现一次)。**内容口径(2026-09-25 补齐,此前它是全链最后一道按磁盘判的主题门)**:全量判 **HEAD blob**、`--staged` 判**索引 blob**、`--worktree` 仅人工逃生舱;枚举与内容同面同轮;取不到 ⇒ **exit 2「无法判定」**(不冒红也不记绿),`--staged --worktree` 同给判死,全量枚举到 0 个 .tsx 判死;git 面必须先 `prefetch`(一次 `cat-file --batch`)再 `read`,未预取即 read 抛错而非静默跳过。立项当场即由此把他人**未提交**的 `ChatScreen.tsx` 摘 prop 判成本仓违规(HEAD 其实合规)。取证 `--self-test` 的 F1–F4 在 `mkScratch` 临时仓里造"索引≠磁盘"现场,并由**三条变异**分别证明 F1/F4 不是恒真;紧急跳过 `HUSKY_SKIP_THEME_PROP_WIRING=1`。〔归并补(副本独有说法,现行判据以上文为准):-**跨端主题透线对账**(91):`scripts/check-theme-prop-wiring.mjs`(blocking)——packages/app的主题驱动组件(形参解构含`colorScheme`且一律带`='light'`默认值)必须在每个JSX渲染点被**真的传参**:默认值就是"静默失效开关"，写死或漏传都不报错、typecheck也不红。(原 L1458) / 拦两类真事故形态——L1调用方写死`colorScheme="light"`(实测导航栏深色而页面体浅色，同一屏两套档案)、L2漏传(当前0处，本门把它钉死在0；(原 L1458) / 刻意不把213处形参改成必填:无收益且会与并行会话在213个文件上对撞)。(原 L1458) / 组件集**不靠手工清单**(清单过期正是上一道门漏判的原因)，改为扫`packages/app/src`导出组件名+**import解析**(含alias/默认导入/命名空间成员)，只有解析到`@ihui/rn-app`或包内相对路径才算共享主题组件——端内同名自绘版(NavBar/TabBar/Carousel/UserInfoCard)因此不会误判；(原 L1458) / `{...spread}`转发的渲染点单列「不确定」如实报数(不静默放过、也不误判为红)。(原 L1458) / 实测真仓`--staged`exit0；(原 L1458) / 应急跳过`HUSKY_SKIP_THEME_PROP_WIRING=1`。(原 L1458)〕
- **品牌实底前景/容器对账(RN + 共享包 + web 类名面)**(83,原 75):check-brand-foreground.mjs(blocking,2026-09-23 立)—— 五条判据,`brand.DEFAULT` 与 `brand.cta` **同形认**(只认 DEFAULT 会让迁移后的主实底整片隐身 —— 判据必须覆盖门自己产出的形态):**R1** 零豁免,同块内 `backgroundColor: (tokens|tk).brand.(DEFAULT|cta)` × `surface.light`/`text.primary` 作前景;**R4** 同一关系拆到**兄弟 key**(`retryBtn` × `retryText`)按命名配对判,走棘轮(现 127 处);**R2** 拦"浅色当容器底"(`surface.light` / α≥0.5 白 rgba / 无 `dark:` 变体的 `bg-white`),棘轮现 24 处;**R3** 数品牌实底填充/描边,**按档配对**免债:DEFAULT 底 ↔ `brand.foreground`、cta 底 ↔ `brand.ctaForeground`,**跨档配对仍计**(DEFAULT 底 × ctaForeground 深色档案下=白底白字),棘轮现 235 处;**R5** web/ui-react 的 **Tailwind 类名形态**(R1–R4 全解析 RN style 对象,对 `bg-primary`+`text-primary-foreground` 这一 §4 已废配对原本整侧盲视),同行成对才算,变体前缀(`data-[state=checked]:bg-primary`)与染色底(`bg-primary/10`)不算,`r5-cta-exempt: 原因` 豁免,基线 `webClassPairCounts` **现为空 = 零容忍**(立项实测 22 处/14 文件,含 `@ihui/ui-react` Button 的 6 个 variant,当日由并行提交 `4e0b24689a` 迁完)。**R5 两条已知限制如实登记**:跨行配对不计(宁窄不误)、变体前缀不算实底(排除前缀的字符类与排除 `/90` 的是同一个,放宽任一即破坏口径)。**内容口径**:全量与 `--update-baseline` 判 **HEAD blob**,`--staged` 判暂存 —— 共享工作树滞后 HEAD,按磁盘算会恒红/假绿来回跳并把错数写回基线(2026-09-24 一天内 R3 登记被整文件回退三次的直接成因)。基线 6 键:`counts`/`ctaCounts`/`r4Counts`/`webClassPairCounts` + 两个文档性注记键(`pairedCtaNotVisibleToRule`/`ctaFillRenameLedger`)—— `--update-baseline` **必须保留注记键**,旧写法整文件重写会把他人审计台账冲掉(本轮并入远端时实测踩过)。取证:`--self-test` **94 条断言**(含 3 条阳性对照 + 变异对照)+ `node --test scripts/tests/check-brand-foreground.test.mjs` **13 例**;`--staged` / `--update-baseline` / `--self-test`;紧急跳过 `HUSKY_SKIP_BRAND_FOREGROUND=1`。〔归并补(副本独有说法,现行判据以上文为准):-**mobile-rn深色前景/容器对账**(83,原75):check-brand-foreground.mjs(blocking,2026-09-23立)——四条判据:**R1**零豁免,同一style块内`tokens.brand.DEFAULT`作背景×`surface.light`/`text.primary`作前景(深色档案下brand.DEFAULT=纯白⇒白底白字);(原 L1382) / **R4**同一关系被拆到**兄弟key**(`retryBtn`×`retryText`)时按命名配对判,走基线棘轮;(原 L1382) / **R2**`surface.light`背景/α≥0.5白rgba/无`dark:`变体的`bg-white`作容器底,每文件计数对`scripts/brand-foreground-baseline.json`只减不增;(原 L1382) / **R3**数brand.DEFAULT填充/描边但**不计§4成对CTA**(同块或兄弟键用`brand.foreground`作前景)——见本节上方「品牌CTA/主按钮色同源」,按规矩写就红的门只会逼人`--no-verify`;(原 L1382) / **无配对的白卡片照旧计**(自检里两条阳性对照钉住)。(原 L1382) / **内容口径**:全量与`--update-baseline`判**HEADblob**,`--staged`判暂存——共享工作树滞后HEAD,按磁盘算会在恒红/假绿间来回跳并把错数写回基线(2026-09-24一天内R3登记被整文件回退三次的直接成因)。(原 L1382) / -**mobile-rn深色前景/容器对账**(83,原75):check-brand-foreground.mjs(blocking,2026-09-23立)——R1零豁免:同一style块内`tokens.brand.DEFAULT`作背景×`surface.light`/`text.primary`作前景(深色档案下brand.DEFAULT=纯白⇒白底白字);(原 L1384) / R2基线棘轮:`surface.light`背景/α≥0.5白rgba/无`dark:`变体的`bg-white`作容器底,每文件计数对`scripts/brand-foreground-baseline.json`(现13文件24处,均为媒体上的合法浮层或已带`dark:`变体)只减不增。(原 L1384) / `--staged`/`--update-baseline`/`--self-test`(11例);(原 L1384)〕
- **反回退对账**(84,原 76):check-stale-revert.mjs(blocking,2026-09-23 立)—— 堵共享工作区**静默回滚**:§12d converge 用 merge-tree/commit-tree 只推进 HEAD+index、**不 checkout**,工作区落后 HEAD 时 `git add <file>` 交的是旧基线(实测 503 文件落后 486 提交),等于把别人该路径的后续改动静默回滚,而 diff 只显"改了几行"。判据 R1 = 暂存 blob != HEAD blob **且字节级等于该路径某祖先提交版本** → 拦并点名回到的 commit;真新编辑不可能恰好等于历史 blob,故误报极低。三条护栏:merge 上下文**不再整轮豁免**(2026-09-25 收窄,见下)、暂存删除只 warn(`git rm` 合法,机器分不清就被删)、判定文件 >300 跳过(性能护栏,防逼人 --no-verify 连带关掉全部守门;**乘数级路径不吃这条护栏**,2026-09-24 立)。**合并期窄判据 R1m**:某路径上 `ours == theirs`(两父逐字节一致)⇒ 合并对它无事可做 ⇒ 索引里与两父都不同的内容**不可能来自本次合并**,再要求它恰等于该路径某历史版本才判红 —— 第三个条件是必须的,因为"人工解冲突写进新内容"同样满足前两条,而那是正当形态。`cherry-pick / revert / rebase` 仍整轮豁免(取历史内容本就是其语义)。立因(第四十二批未闭环④):旧口径那句"merge 上下文整轮豁免"是**本门自己的盲区**,当夜 30c 判红、84 沉默,合并期把旧基线写进索引完全无人看守。取证 `--self-test` **12 例**(新增 5a/5b/5c/5d/6b 五例合并对照:两父不同⇒绿、两父一致而等于历史版本⇒红、等于新写内容⇒绿、cherry-pick⇒仍绿)+ §22c 镜像测试 7 例(含"整轮豁免那句写法不得回来"的**反向回归锁** —— 收窄是判据不是临时关闭,必须能被机器发现)+ 临时 index 端到端演练 3/3(2026-09-23 原始取证,保留);**有意回退一律改用 `git revert` 生成前向提交**。**一条本门看不见的时间窗(2026-09-26 实测)**:84 判的是**索引**里的 blob,而"工作树副本 == 祖先版本、但尚未 `git add`"这一格它结构上碰不到 —— 偏偏危险就在这格里:任何人一次 `git add <该文件>` 或一条不带 pathspec 的 `git commit -a` 就会把它交上去。当天实测 `scripts/guardian-runner.mjs` 的工作树副本等于祖先 `4c1610662`(09-25 19:34),**比 HEAD 少 3 道门(124/125/126)**,而 `git status` 只显示一个 ` M`、13c/71/84 全都不响。量这一型的尺子是 `node scripts/heal-worktree-tracked.mjs --align-drift`(判据:索引==HEAD 且工作树==该路径某祖先 ⇒ 才动,可证零独有数据),手工等价动作是 `git checkout-index -f -- <path>`;**先看判据再动手,不满足就不许对齐** —— 它也可能是别人真在写的现场。紧急跳过 `HUSKY_SKIP_STALE_REVERT_GUARD=1`。〔归并补(副本独有说法,现行判据以上文为准):三条护栏:merge/cherry-pick/revert上下文整轮豁免、暂存删除只warn(`gitrm`合法,机器分不清就被删)、判定文件>300跳过(性能护栏,防逼人--no-verify连带关掉全部守门)。(原 L1385) / 取证`--self-test`8例(含"写回v1必判红"阳性对照)+临时index端到端演练3/3;(原 L1385)〕
- **硬编码中文基线棘轮**(70):scan-hardcoded-zh.mjs(blocking,2026-09-22 接线)—— 扫 `apps/web/{app,src/components,src/hooks}` + `packages/{ui-react,shared}/src` + 2026-09-24 补的三端(`apps/mobile-rn/src`、`apps/extension/{entrypoints,src,lib}`、`apps/cli/src`),排除注释/metadata/useTranslations 行/admin/测试。**每文件命中数与基线额度比较**(`scripts/hardcoded-zh-baseline.json`):只拦"比基线更多",存量债(886 文件 / 13090 行)冻结、新增即拦;清理后 `--update-baseline` 下调额度(必须人工确认后再跑,禁止为过门而调高)。该脚本 2026-07-20 即存在但从未接入守门链,是"造好没装车"的第二个实例(第一个是同一批的 69)。`--staged` 由 runner 自动下发(runner 传 `--exit 1`,不带该参数时**永远 exit 0** —— 集成测试踩过),暂存集为空时回退全量防"空暂存恒绿"。**两处同日修**:① 14 例镜像测试自 §15「ROOT 由脚本自身位置推导」起 **13/14 恒红** —— 测试靠 `cwd` 定位夹具而脚本按定义忽略 cwd,14 例全在扫真仓(不是判据错,是调用方式失效);加 `--root <dir>` 显式测试通道(生产不带,语义不变)后 15/15 绿。② 命中判定漏剥**行尾 `//`** ⇒ PriceChart / TerminalTab / TerminalStatusIndicators 各多出 2/1/1 处假阳,把它们顶过基线额度、把本门钉成"谁碰这三个文件谁被拦";同时修 `'https://x/*'` 这类串内 `/*` 骗进块注释状态机的假绿。**遗留已消除(2026-09-25 实测复核)**：全量模式的取材口径**已改为判 HEAD blob** —— `scan-hardcoded-zh.mjs` 现有"磁盘/索引/HEAD 三种取材共用一份判据"(约 :204 行注释与实现)，实测 `node scripts/scan-hardcoded-zh.mjs` 全量**越线文件数 = 0**。本段此前写的"全量模式按共享工作树判 ⇒ 同一份 HEAD 代码干净检出报 1 个、本机工作树报 281 个"是**已修状态**，保留原话只为说明那一型为什么会发生；**下一个接手者不必再做这件事**，若要复核判据请跑上面那条命令看末行现值。（另记 `heal-worktree-tracked` 的刷新面仍在扩大，两件事互补不互替。）紧急跳过 `HUSKY_SKIP_HARDCODED_ZH_GUARD=1`。〔归并补(副本独有说法,现行判据以上文为准):**遗留(已量化,未修)**:全量模式按共享工作树判,而共享工作树对约2064个路径落后HEAD——同一份HEAD代码,临时worktree干净检出报**1**个文件越线(`packages/shared/src/chat/handoff-package.ts`49处/基线0),本机工作树报**281**个。(原 L1379) / 要么让全量模式改判HEADblob(守门77已验证的口径),要么把`heal-worktree-tracked`的刷新面扩大;(原 L1379) / 前者是判据口径、后者是环境自愈,都不该靠"反正本地红着习惯就好"糊过去。(原 L1379)〕
- **架构契约对账**(103):`scripts/check-architecture-policy.mjs`(blocking,2026-09-25 立)—— 与其余全部守门**方向相反**的一道:其余是"发现一类违规 → 写一条判据"(打地鼠),本门读 `config/architecture-policy.yaml` 这张**声明表**,从声明反查违规。四类红:**T1** 表与现实脱节(模块改名/目录搬走而表没跟上 —— 表一过期,依赖它的所有判断都在对着空气打分)/ **C1** 单文件行上限(阈值 6000,**取自 HEAD 实测最大文件 5258 行之上**,依据写在 yaml 注释里)/ **D1 D2** 依赖方向(包 import 端、或 `requires` 未声明就 import;以及反向按 rank 更深一层)/ **D3** 深导入(绕过 `public_entrypoints` 直接摸别的包内部 —— 它是"端内重复实现"的入口)。**渐进收口是设计前提**:立项时 24 块一律 `managed: false`(只报数不判红),逐块翻正时才按判红口径问责。⚠️ "**存量一律 false**"描述的是**立项状态,不是现值** —— 现值一律按当次实测取,见下。翻 true 前先用 `--managed-trial <id>` 看条数,**不得为消红去改阈值或把不该对外的模块标成 exported**。行内豁免 `arch-exempt: <原因>`(须带原因)。口径同 70/77/83/98/101:全量判 **HEAD blob**、`--staged` 判索引 blob。**策略表自身的取材按档定向**(`policyFaceOrder`):全量档 HEAD 优先、**`--staged` 档索引优先**,两者都再降级到工作树;取不到判"无法判定",不冒红也不记绿。⚠️ 这里 2026-09-25 修掉一处**本门对自身改动的盲视**:原实现两个面都 HEAD 优先,于是"修改策略表自身"的那枚提交完全不进本门审查 —— 实测往索引版 `apps/cli.requires` 注入一条 `apps/api`(端应用 `exported:false`,T1 必判红)后全量与 `--staged` 双双 exit 0,而 `--staged` 输出照旧打印旧表的 `managed:true` 清单。这个洞偏偏落在"规矩 2:翻 `managed:true` 前先试跑"所要求的动作上(提交链是唯一无验的一环),且源码旧注释"表是输入不是被审对象"正是它的设计理由,已就地推翻。取证 `--self-test` 51 例(成对正反 + 两面口径差异 + 解析器坏了必须大声失败)+ 镜像测试 13 例(含 runner 装车证明、"同一代码 managed 两侧结论不同"、T12 的**纯函数+构造面**取材面证明、以及 T12b 的尺子反例;变异回旧顺序只有 T12 红)。**本门自身踩过的判据教训(勿再犯)**:这里曾写过一条"索引表≠HEAD 表 ⇒ 两档收口集合必异形"的条件断言,两条前提都不成立(改注释/requires 都会让表不同而集合不变;尺子若吞到行尾则两档因"扫描 N 文件"天然不等)⇒ 一支误红、一支无牙,已删。**证明取材面这类行为只能用纯函数+构造面**,不得依赖仓库瞬时状态。**收口进度不写进文档**:某模块是否 `managed:true` 一律按当次实测取(`--staged` 输出行即现值),理由与本门刚修的那类缺陷同源 —— 登记过期数字会替人做出"已经收口"的判断。编号说明:任务书原定 102,落地前已被 `check-glyph-arrow-icon` 占用,按"后来者改号"顺延 103。紧急跳过 `HUSKY_SKIP_ARCH_POLICY=1`。 **E1/E2 两条同日补**:E1 公开入口存在性对账 —— `public_entrypoints` 声明的入口必须在**被审判的同一取材面**里解析得到(HEAD 面实测 75 条声明入口全部在位 ⇒ 本条可即时问责,不像 E2 那样带存量)/ E2 契约工件齐备性 —— 纳管模块必须有策略表字段点名的自述与契约文件(**清单取表里的字段,不在代码里硬编码第二份名字**),默认档**只报数**:HEAD 实测 16 块未齐备,当场判红就是一台恒红门(§12e 同型),要问责跑 `--strict`。退出码口径:判红只算「C1/T1 全仓即时 + 已收口模块的契约违规」,E1/E2 的红仅按 `managed:true` 问责。**同批给策略表配了只读出口** `scripts/module-context.mjs`(pnpm `module:context`):按模块 id 打印「是否纳管 / 声明依赖 / 本模块契约文件 / 依赖模块契约文件 / 公开入口 / 边界条款」,`--files` 直接吐派单用的文件清单 —— §11 那张「受影响文件」格过去全靠派单人手写,而 §11 自己就记着这张清单写错会造成越权/污染。未知模块 id **exit 2 并点名**(实测 `node scripts/module-context.mjs nosuch-module-xyz` → rc 2);`--json` 必须可 `JSON.parse`;E2 未齐备既打印 missing **也进退出码** —— 上游那一版只打印不判,这一格我们补上了。〔归并补(副本独有说法,现行判据以上文为准):口径同70/77/83/98/101:全量判**HEADblob**、`--staged`判索引blob,策略表自身按HEAD→索引→工作树降级取且**退到工作树会大声提示**;(原 L1400) / 取证`--self-test`51例(成对正反+两面口径差异+解析器坏了必须大声失败)+镜像测试10例(含runner装车证明与"同一代码managed两侧结论不同"的判据有牙证明)。(原 L1400) / 〔归并补:取证`--self-test`51例(成对正反+两面口径差异+解析器坏了必须大声失败)+镜像测试12例(含runner装车证明、"同一代码managed两侧结论不同"、以及T12的**取材面双向条件不变量**——索引表≠HEAD表则两档结论必不同形、相同则必同形;(原 L1944) / (原L1946)/**渐进收口是设计前提**:存量模块一律`managed:false`⇒只报数不判红(立项实测全量exit0、报数3处、`managed:true`0个)(⚠️这几个数字是**立项状态、不是现值**;(原 L1944) / **渐进收口是设计前提**:存量模块一律`managed:false`⇒只报数不判红(立项实测全量exit0、报数3处、`managed:true`0个)(⚠️这几个数字是**立项状态、不是现值**;(原 L1954)〕。〔归并补(副本独有说法,现行判据以上文为准):-**为什么"产物存在"不等于"能力在线"(本门立论,值得推广到别的生成器)**:这四个生成器里此前**只有i18n那个进了构建链**,其余三个既无构建入口也无门禁⇒"造好没装车"只能靠人记得手动跑;(原 L2137) / 而dev链不跑i18n生成器,意味着**开发时离线语言包常年是旧的**,只有release才被发现。(原 L2137) / 同类盲区见守门64(适配器接线)/70(硬编码中文)/81(邮件通道):**判据必须在真跑它的那一刻才成立,否则等于没有。(原 L2137)〕
  - **为什么"产物存在"不等于"能力在线"(本门立论,值得推广到别的生成器)**:这四个生成器里此前**只有 i18n 那个进了构建链**,其余三个既无构建入口也无门禁 ⇒ "造好没装车"只能靠人记得手动跑;而 dev 链不跑 i18n 生成器,意味着**开发时离线语言包常年是旧的**,只有 release 才被发现。同类盲区见守门 64(适配器接线)/ 70(硬编码中文)/ 81(邮件通道):**判据必须在真跑它的那一刻才成立,否则等于没有。**。〔归并补(副本独有说法,现行判据以上文为准):-**小程序端「源↔派生产物」存续性对账**(105):`scripts/check-miniapp-generated.mjs`(**warn,刻意不blocking**)——管四件"生成器造好了但没人跑"的派生产物:i18n离线包`remote-locales.gen.ts`、`static/images/icons/*.svg`、`LineIcon/icons.ts`、`assets/tabbar/*.png`。(原 L2057) / **升blocking的前置条件是G1存量清零**。(原 L2057)〕
  任何"可达率"读数都必须**先认档再说数**,脱离 C5/C6 的单个百分比没有意义。**三条教训叠在一起**:① 判落地的门比错形态,症状不是差一点,而是把真信号淹在噪声里;② 但在把两侧名字"对上"之前,必须先问**挂载侧到底写了什么** —— "CSS 里有这条规则"与"这个样式生效了"之间隔着一维,只读声明侧的绿灯结构上排除不了"规则存在、没人挂得上"。〔归并补:现测**hit269/771=34.89%(原名直中266+转写可达0+复合首族3)/死规则485/确定缺失6**。(原 L2094) / **两条教训叠在一起**:①判落地的门比错形态,症状不是差一点,而是把真信号淹在噪声里;(原 L2094)〕
- ⚠️ **上面那条 `/alpha` 判据目前到不了小程序产物**(2026-09-25 实测,登记于 PROJECT_PLAN O62附②):`apps/miniapp-taro/config/index.ts` 的 `tailwindcss: { enable: true, config: {} }` 让端内 `tailwind.config.ts`(content globs + `presets:[@ihui/design-tokens/tailwind-preset]` + 本插件)在真实构建中**从未被加载** —— 实测真实 build 后 dist 的 154 个 wxss 里,源码用到的 **687 个 Tailwind utility / 11,105 处用法有 0 个产出规则**,`.flex{` 与 preflight 指纹双双缺失;阳性对照是同一份 dist 里 `app.css` 手写类名 75/98 可查到规则,故不是检索姿势问题。**后果**:改这条配置等于让整端 11,105 处用法同时开始产出 CSS,而现状很可能是手写 CSS 已补偿过(自有类名 2,228 个),开启后好坏只能由微信开发者工具真机渲染判定 ⇒ 属未决决策项,不是已收口项。**同时提醒同源对账门的边界**:门 36/37/93/`check-miniapp-taro-style-parity` 全部只核源码与 token 源头,**没有一道看产物**;它们五道全绿而到端 CSS 为零,是可能且已发生的。新增跨端同源门时,请先问自己判的是"写的同源"还是"生效的同源"。**⚠️ 本条的"0 产出"现值已作废**(同上,`edb225c909` + `f17b5fbdda`):`app.css` 现有一个由 `sync-miniapp-tokens.mjs` 派生的 `@theme` 注册区,命名档与项目色档已产出**且运行时可达**;但含标点那一族(arbitrary value 等)是"CSS 有规则、运行时挂不上"的死规则,真实可达率**必须先认构建档位**再读(同一配置实测三档:34.89% / 93.77% / 99.35%,见下下条与 PROJECT_PLAN 附⑩/附⑪)。"没有一道同源门看产物"这半句仍然成立 —— 看产物的是 `check-miniapp-css-landing.mjs`(手动/CI,未接提交链),它现在连**运行时挂不挂得上**也一起看。〔归并补:**⚠️本条的"0产出"现值已作废**(同上,`edb225c909`+`f17b5fbdda`):真产物落地率740/771=96.0%,`app.css`现有一个由`sync-miniapp-tokens.mjs`派生的`@theme`注册区;(原 L1958) / 但"没有一道同源门看产物"这半句仍然成立——看产物的是`check-miniapp-css-landing.mjs`(手动/CI,未接提交链)。(原 L1958) / 但含标点那一族(arbitraryvalue等)是"CSS有规则、运行时挂不上"的死规则,全端真实可达率现测**269/771=34.89%**(见下下条与PROJECT_PLAN附⑩)。(原 L2096)〕
- **记忆端点属主绑定对账**(117,**blocking**,2026-09-25 升档;紧急跳过 `HUSKY_SKIP_MEMORY_OWNER_BINDING=1`):`scripts/check-memory-owner-binding.mjs` 钉的是**"认证不等于授权"**那一层 —— `apps/ai-service/app/api/memory.py` 把 `user_id` 当请求体/Query 参数收,全文件**没有一处**与令牌主体对齐;`/api/memory` 不在 `PUBLIC_PATHS` ⇒ 匿名进不来,但**任何已登录用户填别人的 UUID 就能读/改/删别人的记忆**。判洞要问"有没有把请求里的 id 与令牌里的 id 比对",不是"有没有全局中间件"。两条 Python 语法锚点必须同时认:`user_id: str = Query(...)` 与 Pydantic `Field(...)` —— **只认 Query 会让整类 POST 端点隐身**;对齐出口只写在注释里不算(自称已拆、实际没拆那一型)。**定级史(照抄旧档会判错)**:立项时 warn —— 当时未对齐存量 2 个文件;现值一律 `node scripts/check-memory-owner-binding.mjs` 自取 —— `app/api/memory.py` 已于同日 `3b0170fa176` 九端点统一 `require_request_user_id` 归零,剩下的是**此前无人枚举过的** `app/routers/rules.py`,而"怎么对齐"取决于 **v1 对外 API 能不能代表他人访问记忆** —— 实测 `apps/api/src/routes/v1-knowledge-tools.ts` 有 2 条转发是 `jsonInit(parsed.data)` **原样透传客户端自报身份**(本门把这类列成"上游透传清单"只报数,就是给选型看爆炸半径用的)。收紧成 403 与"忽略并覆盖"两种改法**都会让本门归零**,故存量未清时判红就是恒红门,唯一结局是逼人 `--no-verify` 连带废掉全部守门(§12e 同型)。**2026-09-25 前置已满足并升 blocking**:`752c6eb110b` 收掉最后一个未对齐面(`app/routers/rules.py`)后 HEAD 面实测"未对齐 0 个",runner 侧同时带 `args:['--strict']`(本门默认档只报数,不带它升档是假的)。**⚠️ 上面那句"HEAD 面实测未对齐 0 个"当次即作废,是本节里一个真实的假绿灯(2026-09-25 同日更正)**:`3b0170fa176` 把参数一律改写成 `user_id: str | None = Query(...)`,而本门两条正则只认 `: str =` ⇒ **一个端点都扫不到** ⇒ 输出"收 user_id 的端点文件 0 个 / 未对齐 0 个"并 exit 0 —— 一道刚被升到 blocking 的安全门变成永远绿灯的尺子,它既看不见当时的敞口,也看不见"明天有人把 `require_request_user_id` 删掉"。补宽正则(可选标注两档都认)后 HEAD 面实测:**收 user_id 的端点文件 2 个 / 已对齐 1 / 未对齐 1**,那一个就是 `app/routers/usage.py` 的 `/stats`+`/quota`+`/record` —— 写法是 `uid = user_id or request.state.user_id`(**自报身份优先于令牌主体**),而参数说明里的"(管理员用)"在该文件内没有任何对应校验 ⇒ 任何已登录用户带 `?user_id=<别人>` 即可读他人用量/配额。已按 `require_request_user_id` + `roleId ≥ 1` 才可代表他人收口(实测 `apps/web` 两处调用方都只传 `days`、`usage/record` 全仓零调用方 ⇒ 不影响在跑链路),回归 `apps/ai-service/tests/test_usage_authz.py` 10 例。**本条的通用教训:改被审代码的写法,必须同时改审它的正则** —— 否则"升级成 blocking"这个动作本身就是在给一台瞎掉的尺子背书。取证 `--self-test` 17→**21 例**(新增 S17–S20:可选标注的 Query/Field 各一对正反例,夹具文本逐字取自真实文件)。**降回 warn 等于重新放开新增敞口**;若 HEAD 面再报未对齐(例:有人跳过钩子塞进去),先清偿再提别的票,不得削判据。口径同 70/77/83/98/101/103(全量判 HEAD blob、`--staged` 判索引、枚举到 0 个文件判"无法判定"而非记绿 —— 判据失明不是通过)。仓里现成出口 = `app/core/jwt_auth.py` 的 `require_request_user_id`(带 `DEV_ANONYMOUS_PRINCIPAL` 开发降级,以免 ASGI in-process 的既有测试整片 401 —— 那不构成安全结论)。取证 `--self-test` 17 例(正反成对) + §22c 镜像 14 例(含"未注册时不得被判定为已装车"的方向性对照);手动问责档 `pnpm check:memory-owner`(走 `--strict`)。〔归并补(副本独有说法,现行判据以上文为准):**定级warn是设计前提而非偷懒**:HEAD实测未对齐存量**2个文件**(`app/api/memory.py`与**此前无人枚举过的**`app/routers/rules.py`),而"怎么对齐"取决于**v1对外API能不能代表他人访问记忆**——实测`apps/api/src/routes/v1-knowledge-tools.ts`有2条转发是`jsonInit(parsed.data)`**原样透传客户端自报身份**(本门把这类列成"上游透传清单"只报数,就是给选型看爆炸半径用的)。(原 L2019) / 收紧成403与"忽略并覆盖"两种改法**都会让本门归零**,故在选型落地前它是"存量报数、新增判红"的棘轮;(原 L2019) / 当场blocking=与任何提交都无关的恒红门,唯一结局是逼人`--no-verify`连带废掉全部守门(§12e同型)。(原 L2019) / **升blocking的前置条件=未对齐存量归零**。(原 L2019) / **定级warn是设计前提而非偷懒**:未对齐存量:立项读数2个文件;(原 L2022)〕
  - **13c 自身的取材口径(同日迁移)**:此前 `ROOT = process.cwd()` + 裸 `git show` 用 `execSync` 拼字符串(无 timeout)+ 占位合规性看 `git diff` 文本,三处都与本仓现行口径相反(工作树滞后即换结论 / 服务账户 PATH 不通 / 别人同时改这文件时合规性跟着盘走)。现全部经 `scripts/lib/face-reader.mjs`,对照面是 `--staged`:HEAD→索引、全量:HEAD^→HEAD、`--worktree` 仅人工;整面取不到 ⇒ **exit 2 无法判定**,不冒红也不记绿。取证 `--self-test` 10 条,含两条本门自己踩出来的:CRLF/LF 同一条标题不得读成"删除";**A2 的归属必须看该目录在审面上的全集**而不是只看 `PROJECT_PLAN_*` 形状 —— 本门第一次自跑就是被这条假红咬到的(已跟踪的 `orphan-capabilities-equivalence-2026-09-24.md` 被点名"落空")。〔归并补(副本独有说法,现行判据以上文为准):2026-09-26那次实测是face28/loose71(其中half-wired1)/unknown38/no-content49——写死它下次收紧就变成假账。(原 L2010) / **那1道半接线同日收口了**:`check-rn-global-css-sync`换成走层的`catBatch`三面取材,收口后现读`half-wired`应为0——这条判据落地当天就产出了第一次真实修复,不是只多了一批红)。(原 L2010) / **同日两处精度修正(都是量出来的假阳,不是审美)**:①"读内容"与"枚举"必须分开——`cat-file-e`(存在性)、`ls-tree--name-only`(路径清单)、`rev-list`/`merge-base`不产生blob正文,把它们算成"散写读内容"会把只做枚举的门钉成半接线(它一处blob都没读);(原 L2010) / **假阳比漏报更贵**——它指使人去"修"没坏的东西,还把判据口径说歪成"数字很多"。(原 L2010) / ②仓库锚点必须落在**路径函数实参**里:旧写法的`|\bROOT\s*,`一支会把`functionaudit(limit=400,cwd=ROOT)`读成"以仓库根拼路径"。(原 L2010) / ③借层的transport自己读内容(`gitRaw(['show',spec])`)同样算半接线——管子共用不等于面共用。(原 L2010) / **已知空档如实登记**:锚点只认大写`ROOT`,故`join(root,…)`这类小写模块根写法落进`unknown`(不判红)。(原 L2010) / 试过放宽(74→93),其中两格是假阳(一格读`.workbuddy`运行台账、一格读按设计只存在于部署机的gitignore副本),**已收回**:判据分不清"读被审内容"与"读运行态",就不该假装分得清并据此判红;(原 L2010) / 补它要认识gitignore语义(参照`check-prod-bundle-shadow`的S2),不是一行正则的事。(原 L2010) / -**门脚本取材面纪律对账**(118):`scripts/check-gate-face-discipline.mjs`(blocking,`stagedTriggers=scripts/`,2026-09-25立)——本仓159道门里只有**24**道走`scripts/lib/face-reader.mjs`,散写git读内容或按磁盘`readFileSync(join(ROOT,…))`判的有**74**道,另32道"读文件但找不到仓库锚点"(疑临时夹具,结构上判不了,只报数)。(原 L2073) / **两层遮噪方向不同,别混用**:认face-reader导入与git动词必须"只遮注释、保留字符串"(模块说明符和`cat-file`本身就是字符串),认`readFileSync`调用必须"注释与字符串都遮"(`markHidden`那一档连字符串一起抹,直接套它会让F1/F2双双假绿)。(原 L2073)〕
