SPEC-010 — 当前缺陷止损清单¶
0. 本文与 SPEC-009 的关系(先读这段)¶
🔴 本文不是方案设计,是缺陷清单。
| SPEC-009(见下方位置说明) | 本文 SPEC-010 | |
|---|---|---|
| 建于 | 2026-08-25(worktree codex/phone-country-cloud-spec) |
2026-08-30 |
| 体量 | 1543 行 / 19 章 | 本文 |
| 回答 | 要建成什么样 — 数据模型、同步协议、验收 Fixtures、迁移、回滚 | 现在坏在哪 — 当前代码正在生效的破坏 |
| 状态 | G1 BLOCK(2026-08-30 terra+dshf 双路终审) | draft |
两份工作互补,不是重复。 SPEC-009 的 G1 终审报出的 2 个 P0 全部出自本文的缺陷发现, 其正文对两者零提及:
- 旧 V1 云通道盲区 — 见 0091-sync-flag-not-reset-on-fast-paths
- 点分隔电话 cleaner 违反 SPEC-009 §3 第 2 条不变量「原文不可丢」 — 见 0089-dot-separated-phone-destroyed
🔴 SPEC-009 当前不在本仓库:它是 worktree
codex/phone-country-cloud-spec(路径../02_laifaxin-chajian-ditu-g1-spec)里的未提交文件docs/specs/active/SPEC-009-contact-normalization-cloud-ai.md。 因其尚未进入 git,本文不使用 wikilink 指向它,避免「声称即实存」。 它提交后应把本节的文本引用改回 wikilink。
⇒ SPEC-009 解除 G1 BLOCK 依赖本文的止损动作先落地。 本文的 §4「14 项裁决」与 SPEC-009 的 §4/§19 存在部分重叠,以 SPEC-009 为准; 本文独有的价值在 §5 缺陷清单与 §6 Phase 0 止损。
1. 结论¶
14 项拟议规则里:ACCEPT 4 项(#2/#5/#8/#12)、MODIFY 6 项(#1/#4/#6/#7/#11/#14)、REJECT 3 项(#3 规则层否决 + #9/#10 本轮不做)、改为复用评估 1 项(#13) = 14。 另有 4 个决策点必须 Tony 拍板(见 §8),未拍板前对应 Phase 不启动。
先修两个正在生效的数据销毁路径(0088-delete-no-email-filter-drops-all-rows / 0089-dot-separated-phone-destroyed),再谈规则升级。
2. 审查方法与证据等级¶
| 轮 | 执行方 | 产出 |
|---|---|---|
| 1 | Claude 主控 + 16 子 agent(7 sonnet 侦察 / 7 opus 证伪 / 2 opus 裁决) | 初版 findings |
| 2 | 主控逐条复核 file:line | 推翻 1 条假阳性(MapTaskData.sync 因果链) |
| 3 | terra(openai/gpt-5.6-terra) + dshf(deepseek-v4-flash) 跨厂商双路盲化 | 推翻 1 条存在性结论、修正 4 处行号/表述、新增 1 条 P1、改 1 项裁决 |
| 4 | 同双路做落档前复审 | 两路均 NO-GO(terra 3.8 / dshf 7.5),提 4 条 MUST_FIX |
| 5 | 同双路做落档后复审 | 两路均 NO-GO(terra 4.8 / dshf 5.5),提 5 条 MUST_FIX;X_UNFIXED 与 X_SELFREF_OK=NO 两家字符级一致 |
| 6 | sonnet5(与主控同厂商)10 agent 五 lens 专攻三方共识 | 推翻共识 1 条(C3)、削弱 1 条(C1)、新增 2 个 issue(0090-manifest-host-permission-domain-typo / 0091-sync-flag-not-reset-on-fast-paths)、指出 Phase 0 门槛不可满足 |
双路读同一固定快照、独立 session、互不共享结论。轮 3 评分 terra 7.1 / dshf 6.8;轮 4 terra 3.8 / dshf 7.5。
3. 当前实现真实状态¶
🔴 as-of 声明:本节所有结论基于 commit
f25b565(v0.10.124,2026-05-31)+ 工作区 SPEC-008 在制品, 核验日期 2026-08-30。Phase 0 起任何一次实施都会使本表部分失真 —— 每个 Phase 结束时必须重新核验本表, 不得把本表当作后续 Phase 的现状依据。
存在性结论按 capability-existence-proof 的三态标注:
| 能力 | 态 | 证据 |
|---|---|---|
| DNS / NS / MX / A 查询 | ABSENT | 已查 5 类:库名(dns/doh)、API(chrome.dns/browser.dns/resolveMx/nameserver)、自研动词(resolveXxx/lookupXxx)、中文(解析/域名查询)、文件名(*dns*) — 三方各自复验零命中 |
| libphonenumber / E.164 实现 | ABSENT | 5 类全零:库名(libphonenumber)、平台 API(无)、自研动词(toE164/formatE164/parsePhoneNumber/normalizePhoneNumber)、中文(号码格式化/国际格式/区号解析)、文件名(*e164*/*phone-format*) — 仅 scraper.ts:200 注释提及 E.164 15 位上限 |
| Markdown 导出 | PRESENT | 入口 fetch-snapshot-list.tsx:103(按钮「导出 Markdown」)→ 副作用 :62 → 实现 fetch-snapshot.ts:231 |
| HTML→Markdown 转换库 | ABSENT | 5 类全零:库名(turndown/remark/html-to-md/html2md/showdown/marked)、平台 API(无)、自研动词(toMarkdown/htmlToMarkdown/convertToMarkdown/renderMarkdown/parseMarkdown)、中文(转.*Markdown/转换.*格式)、文件名(*markdown*/*turndown*) — 自研仅覆盖快照导出,不含 DOM 转换 |
| 截图 / OCR | ABSENT | 5 类全零:库名(html2canvas/dom-to-image/puppeteer)、平台 API(captureVisibleTab/tabCapture/toDataURL)、自研动词(takeScreenshot/captureScreen/grabImage)、中文(截图/识别)、文件名(*screenshot*/*capture*/*ocr*) |
| AI 调用 | ABSENT | 5 类全零:库名(openai/anthropic/openrouter)、平台 API(chat/completions)、自研动词(callLLM/askAI/promptModel/generateCompletion)、中文(智能分析)、文件名(*ai-*/*llm*/*prompt*) |
| 业务单元测试 | ABSENT | 5 类全零:①库名(vitest/jest/@testing-library/mocha/chai — 裸 grep 的 jest/chai 经核实为子串误匹配 OranJESTad/writeCHAIn)、②平台 API(describe(/expect(/beforeEach 零命中;it( 裸 grep 76 次经逐条核实全为子串误匹配 —— clearTranExit(34 / split(32 / handleSubmit(10 等,加词边界后为零)、③自研动词(runTests/assertEqual/mockChrome 零命中)、④中文(测试 命中 5 处,全为业务文案:regex-tester.tsx:7,97 正则测试器 UI、settings-view.tsx:1605 与 scraper.ts:111 的「测试号黑名单」,无测试设施)、⑤文件名(*.test.ts(x)/*.spec.ts(x)/vitest.config.*/jest.config.* 全零);package.json 的 test:* 五项全为治理脚本自测 |
| 邮箱提取 | PRESENT | 三路:mailto scraper.ts:141-163、cf 反混淆 :226(定义)/:376-384(调用)、自由文本 :134+:350-374 |
| 电话提取 | PRESENT | 三层 Tier scraper.ts:202-208;塌缩成单值 scraper-executor.ts:30 |
| 国家识别 | PRESENT(单一来源) | 仅地图 addressInfo:parse-data.ts:40 / :121。无 JSON-LD / ccTLD / 语言推断 / 冲突检测 |
| 云端同步(新 SPEC-004 ContactPool 管线) | PRESENT(客户端就绪,服务端未上线) | enableCloudSync: false 默认(storage-data.ts:253);cloud-sync-panel.tsx:4 自陈服务端未上线;目标域名 cloud.laifaxin.com |
| 云端同步(旧 MapTaskData 通道) | 🔴 PRESENT 且当前活着 | cloud-data-sync.tsx 由 main-layout.tsx:429 无条件挂载,pollingInterval: 10*1000 每 10 秒轮询;启动条件仅 uid && mapDataSync(服务端下发 + VIP),与 enableCloudSync 无关;打生产域名 https://ggdt.laifaxin.com(api/request.tsx:24-26)。详见 0091-sync-flag-not-reset-on-fast-paths |
pnpm verify |
17 段 | 按 && 切分实测;对纯函数正确性零证明力(无业务单测) |
国家字段的可见性(Phase 3 依据,勿简化):
countryCode 不在主表默认列 DEFAULT_FIELDS(fields.ts:36-137),
但在 CSV 导出列 exportFields(fields.ts:181,title「国家」)。
⇒ 国家已有出口,缺的是主表可见性与筛选能力,不是从零建。
4. 14 项裁决¶
| # | 拟议规则 | 裁决 | Phase | 依据 |
|---|---|---|---|---|
| 1 | NS 作为邮箱域名存在性主判断 | MODIFY | 待拍板(§8-2)后单独立项,不在 Phase 0-6 | DNS 为 ABSENT ⇒ 纯新建;DoH 隐私未决。Phase 1 只建状态字段与 rawValue 保留,不查 DNS |
| 2 | MX/A 只记录,不做 SMTP 探测 | ACCEPT | 同 1,随其一起 | 方向对,无独立价值 |
| 3 | 点号分隔电话一律判无效 | REJECT | — | 双路一致:点号是法/比合法格式。详见 §7 三层说明 |
| 4 | 电话字符白名单 | MODIFY | Phase 1 | 白名单可做,但必须放行点号;tel: 来源(Tier 1)不过噪音过滤 |
| 5 | 一值一行 | ACCEPT | Phase 2 | terra 论证:保留单 string 只改渲染层无法修复 P1-09 / P1-18 / P1-19。须建立「独立值 / 来源 / 规范化键 / 展示聚合」四层边界 |
| 6 | 国家未知进 PENDING_COUNTRY | MODIFY | Phase 3 | 国家已在导出列可见(fields.ts:181)但不在主表默认列 ⇒ 加状态字段须同时加主表可选列 + 筛选器,否则状态无出口 |
| 7 | 无加号号码国家推断 | MODIFY | Phase 1(修键)+ 待拍板(推断) | 先修「取后 10 位」归一(P1-06,当前真 bug),推断逻辑依赖 §8-1 |
| 8 | 导入域名不强制选国家 | ACCEPT | Phase 3 | 双路一致支持。附加固条件:可选 hint 标 user-declared 且不计入证据链、导入后显示「N 行国家未知」、支持 CSV 带国家列 |
| 9 | AI 只处理 PENDING/CONFLICT | REJECT(本轮) | — | ABSENT + 无后端 + key 归属未定 + 商店审查口径未评估 |
| 10 | 截图辅助 | REJECT(本轮) | — | ABSENT;无证据表明文本不足是瓶颈 |
| 11 | 持续上传云端 | MODIFY(阻断方案本身) | Phase 4 | 七个客户端缺陷是硬前置(P1-02 / 11 / 12 / 14 / 15 / 16 / 20),与 §6 Phase 4 一致。不阻断当前版本(默认关闭) |
| 12 | 本地容量上限 | ACCEPT | Phase 4 | 三张表确无上限(P1-15),manifest 未声明 unlimitedStorage |
| 13 | Markdown 不整页存本地 | 改为复用评估 | Phase 5 | Markdown 导出为 PRESENT,先评估现有实现够不够,不新建 |
| 14 | 文档同步机器闸 | MODIFY | Phase 6 | Docs-Impact 只验「路径存在 + staged」,不验内容真改 |
5. 阻断项¶
P0(当前生效,两路各自独立复现)¶
P1 完整清单(22 条)¶
| 编号 | 问题 | 位置 |
|---|---|---|
| P1-01 | 「只保留企业邮箱」是死代码,条件在唯一调用点恒 false | jsstore/search-data.ts:293 |
| P1-02 | writeCloudRecords 绕过全部邮箱质量防线(无 dedup / lowercase / broken 过滤) |
contact-pool.ts:209-233 |
| P1-03 | EMAIL_REGEX 无左右边界锚定,{1,64} 只截断不拒绝 ⇒ 产出外观合法的假邮箱 |
scraper.ts:134 |
| P1-04 | 基线假绿:零 src 运行时单测,pnpm verify 17 段对邮箱/电话链路正确性零证明力 |
package.json verify |
| P1-05 | PHONE_TEXT_REGEX 分隔符类不含括号,裸括号区号电话 Tier3 抓不到 |
scraper.ts:205-206 |
| P1-06 | 电话归一取后 10 位 ⇒ 跨国误合并(+44 20 7123 4567 与美国 (207) 123-4567 同键,后到者被丢弃) |
scraper-executor.ts:36-39 |
| P1-07 | 网址批量导入的商户行没有 countryCode 键,与 Maps 路径的空字符串是两种不同的「空」 | task-manager.ts:201-205 |
| P1-08 | handleError 不区分永久性与瞬时错误,全部走同一条重试路径 |
cloud-sync-orchestrator.ts:349-366 |
| P1-09 | 来源标签 (地图) 拼进持久化 phone 值,且 .join(', ') 多值拼串 |
scraper-executor.ts:61 |
| P1-10 | CSV「创建时间」导出 13 位毫秒戳;exportFields 缺 valueType 使格式化分支成死代码 |
fields.ts:139-248 + export.ts:15-16 |
| P1-11 | 整批无条件 markUploaded,忽略服务端逐条 accepted/rejected |
cloud-sync-orchestrator.ts:106-113 |
| P1-12 | 匿名 clientId 与产品账号割裂,登出/切号不清空不轮换 | auth-provider.tsx:80-84 vs cloud-sync-client.ts:18-19 |
| P1-13 | 三张云端表无容量上限或过期清理,manifest 未声明 unlimitedStorage |
contact-pool.ts / contribution-ledger.ts / domain-state |
| P1-14 | 首次同步三路并发匿名注册竞态,clientId/token/userId 可落成互不匹配 | cloud-sync-orchestrator.ts:292-296 |
| P1-15 | 云端拉取覆盖本地待上传记录,被覆盖行永失上传资格 | contact-pool.ts:209-235 vs :178 |
| P1-16 | ContributionLedger 被拒条目形成永久重投循环,长期占据 FIFO 队首 | cloud-sync-orchestrator.ts:207-224 |
| P1-17 | 脱敏正则漏过 tel: 链接的连续数字,sanitized:true 与实际内容不符 |
fetch-snapshot.ts sanitize |
| P1-18 | 云端表格 WhatsApp 深链用未拆分/未编码的合并多值 phone 拼 URL | cloud-table.tsx |
| P1-19 | 详情抽屉 tel: 对合并多值串做纯字符过滤,多号码拼成一个乱码号 |
merchant-detail-drawer.tsx:396 |
| P1-20 | writeContactRecord 不与 existing 取并集,本地少量结果覆盖云端共识并重置为 pending |
contact-pool.ts:75-100 |
已升级移出(不占 P1 编号):坐标过滤误杀点分隔电话并删存量 → 已升 P0,见 0089-dot-separated-phone-destroyed。
| P1-21 | 两条快路径漏重置 sync:0,补写的社媒/电话永不上传旧云通道 | scraper-executor.ts:148 / :163,见 0091-sync-flag-not-reset-on-fast-paths |
| P1-22 | manifest host_permissions 域名拼写错误(潜伏,被 <all_urls> 掩盖) | wxt.config.ts:28,见 0090-manifest-host-permission-domain-typo |
(清单实为 22 条,编号连续。)
已降级(不计入):rows as any 双层 any(→P2,无已触发的错误形态)。
🔴 一条降级已被推翻:「忘重置 sync:0」曾被主控降为 P2,理由是「MapTaskData.sync 无云端消费方」——
该理由错误:主控只查了新 SPEC-004 管线(cloud-sync-orchestrator.ts),漏了旧通道
cloud-data-sync.tsx:39,71。现已恢复为 P1-21。
P1-02 / 11 / 12 / 14 / 15 / 16 / 20 的定级口径:默认关闭(enableCloudSync: false)故当前 P1;
一旦服务端上线或用户填入自建地址,立即成为上线阻断。
6. 实施计划¶
| Phase | 修改范围 | 进入下一阶段的门槛 |
|---|---|---|
| 0 | ISSUE-0088;ISSUE-0089 的止损部分(停用存量销毁,见 §7);引入 vitest | 见下方门槛说明 |
| 1 | 电话清洗:白名单放行点号(判据待 §8-1)+ 去重键改「完整 digits + countryCode」+ 保留 rawValue / rejectionReason + P1-05 括号 | 攻击样本全绿;P1-05 括号号码可抓 |
| 2 | 一值一行数据契约(四层边界),含 P1-09 / P1-18 / P1-19 | tel: 与 WhatsApp 深链对多号码正确;无标签进数据值 |
| 3 | 国家:状态字段 + 主表可选列 + 筛选器 + 导入可选 hint + P1-07 键缺失 | 国家未知行可在主表筛出 |
| 4 | 云端七前置:P1-02 / 11 / 12 / 14 / 15 / 16 / 20 + 容量上限 P1-13 | 服务端上线前不合并;P1-12 身份绑定方案须先经 §8-4 拍板并有验收用例 |
| 5 | AI / 截图不做;Markdown 走复用评估(现有 exportSnapshotsAsMarkdown 是否够用) |
评估结论落档,不写代码 |
| 6 | 产品文档补邮箱/电话规则;Docs-Impact 增加机械判据校验(声明的文档路径在本次提交中 diff 非空),🔴 不做业务语义判断 — SPEC-008-doc-function-sync-governance §3.2 已明确否决「业务语义自动判断」与 diff-glob 推断,本 Phase 不得越过该边界 | 负向用例:声明了路径但该文档 diff 为空 ⇒ 拒绝 |
🔴 Phase 0 退出门槛(修正版) —— 原写「两个 P0 各有失败→通过的回归测试」对 ISSUE-0088 不可满足:
| P0 | 缺陷所在文件 | 可测性 | 门槛 |
|---|---|---|---|
| 0089 | scraper.ts(grep -c '^import' = 0,纯函数模块) |
vitest 直接可测 | 必须有失败→通过的单测 |
| 0088 | jsstore/search-data.ts(import wxt/storage + jsstore/base,后者用 workerInjector 真实 Web Worker + IndexedDB,jsdom 下无官方 mock) |
单测不可行 | 改为:①对被抽出的纯判定函数做单测,或 ②扩展浏览器运行时手工验证并留存证据(截图 + 日志),二选一 |
⇒ 不得以「给 scraper.ts 写了测试 + pnpm verify 绿」宣告 Phase 0 完成 —— 那样 0088 会零测试覆盖通过。
Phase 0 不可修改:任何 schema / jsstore/base.ts / cloud-sync-* / 9 个 scanner baseline / verify 的任何一段。
每个 Phase 结束时必须重新核验 §3 的 as-of 表,并更新本文 updated 字段。
7. 点分隔电话的三层区分(避免误读)¶
这三件事不矛盾,是三个层次:
| 层 | 内容 | 状态 |
|---|---|---|
| 规则层 | 拟议规则第 3 项「点号一律判无效」 | 已裁决 REJECT — 采纳它等于把 ISSUE-0089 固化成规范 |
| 止损层 | 停止 cleanPollutedContactPool 继续销毁存量 |
Phase 0 立即做,不需拍板 — 停止破坏不等于选定新判据 |
| 判据层 | 新的坐标 vs 电话判别器往哪边偏 | 待 §8-1 拍板 — 这是产品市场取舍 |
⇒ Phase 0 只做止损层;判据层落地在 Phase 1,前置是 §8-1。
8. 待 Tony 拍板(未解除的 BLOCK)¶
- 点分隔电话收不收 — 收 = 放宽坐标判据、承担 GPS 噪音回流;不收 = 放弃法/比/瑞电话采集。双路审查均确认这是产品市场取舍。阻塞 Phase 1 的判据层
- 能否把客户的潜客域名送第三方 DoH — 属对客户的数据承诺范畴。阻塞裁决 1/2 的立项
- 一值一行迁移的不可恢复损失 —
slice(0,5)已截断的号码、(地图)有损编码导致来源不可反推。阻塞 Phase 2 的迁移动作(非渲染改造) - cloud-sync 匿名身份与产品账号的绑定方案 — 阻塞 Phase 4
9. 非目标¶
- 不建复杂邮箱评分系统
- 不重写插件
- 不建密码学审查系统
- 不为「完整」而建无维护者的超大数据模型 — 每个新字段必须有明确读者