# realtycheck（propertylab_*）→ master catalogue：讨论与决定记录

> **先读这份。** 这份文件记录 2026-09-23 至 2026-09-24 和 owner 的整段讨论：问了什么、查到什么、
> 定了什么、做了什么、还剩什么。目的是在对话 compact 或换 session 之后，不需要重新问一遍。
>
> - 位置：`storage/app/propertylab-catalogue-match/`（gitignored，只在 wk 这台机器上）
> - 代码层面的正式文档（在 repo 里）：`docs/modules_handbook/shared/project-catalogue/realtycheck-transactions.md`
> - Claude memory：`realtycheck-catalogue-crosswalk.md`
> - **数字以本文件为准时，写明了日期。代码行为以代码为准**，文档可能过时。
> - 更新规则：每次有新决定或新进展，在 §4「决定记录」和 §9「进度」追加一行，不要改历史。

---

## 1. 一句话背景

`/property/research`（Property Explorer）的图表数据，不是 catalogue 的，是 **realtycheck.my** 的第三方数据，
存在 site DB `petav3wk` 的 `propertylab_*` 表里。老板要求：以后这些数据要以 **master catalogue**
（`master_projects.catalog_projects`）为准。所以要先把 realtycheck 的 scheme 和 catalogue project **一一对上**，
再把成交数据导进 master。

目标文档（owner 2026-09-23 给的截图）：
- **1.1 Subsale Highrise**：把 IQI / Transactable Data 整合进 Master Database (Project)；用 AI LLM 做 matching；
  **confidence > 90% 才自动对上**；目的：在 Property Explorer 显示成交数据 + EdgeProp 的 asking / rental price。
- **1.2 Landed & Commercial**：先做 backend，frontend 放 phase 2。

---

## 2. 时间线：问了什么、答了什么

| # | 时间 (UTC) | Owner 问 | 结论 |
|---|---|---|---|
| 1 | 09-23 14:15 | `/property/research` 那张图的数据在哪？是不是 catalogue 的？ | 不是。来源 realtycheck.my → SQLite archive → `scripts/propertylab/prepare_research_import.py` → `php artisan propertylab:import-research --activate` → `petav3wk.propertylab_*`。读取逻辑在 `src/PropertyLab/Research/ResearchRepository.php`（`getSchemeEvidence`）。research 模块完全没有引用 `catalog_*` |
| 2 | 14:24 | `propertylab_*` 在 master 上吗？和 catalogue 完全不同？老板要把这些数据放进 catalogue，复杂吗？catalogue 已经有 subsale data 了吧？ | 代码在 master（commit `ade4dfa03`，2026-09-18），但 git 不带数据，数据只在 wk 跑过 import。两边完全不同、没有任何 ID 关联。catalogue 的马来西亚 subsale 只有 **项目级汇总 + 每季度 PSF**（EdgeProp），没有逐月、没有逐笔。`market_transactions` 234 万行 **100% 是香港**。难点不在建表，在 matching |
| 3 | 14:32 | realtycheck 有多少项目？都是 completed/subsale 吗？catalogue 有多少？ | 见 §3 数据事实 |
| 4 | 14:35 | 图上的数据是 database 的哪个 column？ | 见 §3.3（以 SUNWAY VELOCITY TWO 为例） |
| 5 | 14:40 | 步骤是不是：① match ② 把数据 import 进 master？ | 对，但要加第 0 步（授权、范围、对不上的怎么办）和第 3 步（前端显示）。而且是**复制 + 登记为新的 data provider**，不是搬走；不能写进 `catalog_projects` 字段；ingestion 没有 `catalogProjectId` 会**新建项目**，adapter 必须跳过对不上的 |
| 6 | 14:45 | 先把 `/property/research` 改成只有 admin 能进、只有 admin 看到 sidebar 菜单（临时） | ✅ 已做。见 §4 D1 |
| 7 | 14:53 | 用 AI + web search 把 scheme 和 catalogue project 对上，报告哪些是同一个 | ✅ 做了第一版（v1），见 §5.1 |
| 8 | 16:49 | 同一个项目在 catalogue 里可以有多个来源（EdgeProp、PropertyGuru）对吗？ | 对，一行 `catalog_project_sources` 一个来源；没有 PropertyGuru（可能指 iProperty，属于 PropertyGuru 集团）；**不同来源不会自动合并**（"no fuzzy merging"） |
| 9 | 16:58 | 同项目不同来源，已经合并成一个了吗？ | 只合并了一部分，见 §3.4 |
| 10 | 17:07 | 只专注 subsale/completed，下一步怎么做？ | 提出方案：新 provider `realtycheck` + 新表 `catalog_project_price_months`，uuid 当身份，Hub 上导入 |
| 11 | 17:24 | 4,370 和 656 是什么？production 要有这些图、`/property/research` 要改成以 catalogue 为准，怎么做？ | 4,370 = v1 confident 里「住宅 + 对到 EdgeProp subsale 行」；656 = probable 里的同类。A 部分（数据进 master）+ B 部分（Explorer 改读 catalogue），见 §8 |
| 12 | 17:30 | 在哪里可以看这些，我帮你判断 probable / part of / need human check？ | 给了 xlsx 位置 |
| 13 | 17:44 | **「I 100% trust you」**，你帮我验证并定案；**重点是 high-rise**；taman/landed、commercial shoplot 你自己定。目标文档是图 2 | ✅ 做了 FINAL 版，见 §5.2 |
| 14 | 18:38 | 怎么把数据 deploy 进 master？✅🟡🔗❌ 四种状态是什么意思，在数据库里怎么存？ | 只有 ✅ LINK 写进 master；其余三种只留在 crosswalk。见 §5.3 |
| 15 | 18:45 | 我有 Hub 权限；单笔成交记录是什么？老板已批准数据授权 | 单笔 = realtycheck 页面列出的最近几笔成交（每个 scheme 最多 8 笔）。建议一起放进 master |
| 16 | 18:47 | 选择「一起放 (Recommended)」 | ✅ 开始写代码，见 §6 |
| 17 | 09-24 04:0x | 做了什么、下一步是什么？（要求中文回复，keyword 用英文） | 见 §9 |
| 18 | 09-24 | 要一份 md 记录所有讨论；HOLD / RELATED / NONE 打算怎么处理？先忽略吗？ | 本文件；计划见 §7 |
| 19 | 09-24 | 同意：小镇 taman、kampung、店屋、工业**先不放进 catalogue**。HOLD 和 RELATED 为什么不能直接定「导」或「不导」？ | 两者**现在都是「不导」**。HOLD = 很可能同一个，但缺证据（多数是 catalogue 那行没价格可对）→ 再查一轮就能变成 LINK 或 NONE。RELATED = 确定有关系、但不是同一个东西，硬挂会把错的价格放到项目页（例：TAMAN SEGAMAT JAYA 的 flat 中位价 RM 41,500，catalogue 那行是 terrace RM 250,000）→ 结论就是不导；要用这些数据只能先在 catalogue 补建那栋楼 |
| 20 | 09-24 | **Yes**，HOLD 再查一轮 → 每个变成导或不导。RELATED 在 Kuala Lumpur、Selangor、Klang Valley、Seremban、Kajang、Semenyih 各有多少？ | HOLD 全部 277 个开始 web 复核（§7.2）。RELATED 分区数字见 §7.1，名单 `RELATED-by-region.csv` |
| 21 | 09-25 | 以后把 production 数据库迁移到 wk，`propertylab_*` 要保留吗？ | 要，见 D16 |
| 22 | 09-25 | 为什么不会新建项目？NONE 都不是 high-rise 吗？ | 不是；high-rise NONE 911 个，里面有漏配，见 §9 09-25 |
| 23 | 09-25 | **Yes**，911 个 high-rise NONE 做第二轮复核，先 Klang Valley 和成交多的 | D17 → D18 |
| 24 | 09-27 | 完整的 deploy 步骤？跑了会触发什么？ | Hub：deploy → 两条 catalogue migrate → `catalogue:sync realtycheck --file=…`；只新增来源行 + 两张表的行，不改项目字段、不新建项目 |
| 25 | 09-27 | Hub 跑完：`Run #12 [success] realtycheck/MY: 5657 created, 0 updated, 0 failed`；production 跑 `catalogue:mirror-from-master` 显示读 master **LIVE**。下一步？ | 导入完成；production 直接读 master，不需要 mirror。下一步 = 前端（§9 下一步 6、7） |
| 26 | 09-27 | Explorer 改读 catalogue；**先别让会员看到**，用 env 控制（像 `AREA_GUIDE_LOCKED=true`）。项目页那个 component（`/my/projects/binastra-cochrane`，admin 编辑页的 preview tab 也是它）：有 subsale 成交的项目加上成交走势图和「最近成交」列表；Explorer 也显示这个 component | ✅ 做了，见 D19–D22、§12 |
| 27 | 09-27 | 会员只看已发布 —— admin 能在 Explorer 看未发布的 subsale 项目吗？ | 能：有 `view-projects` 权限的人（12 个 admin 全有）看全部 5,247 个，未发布的标「Unpublished」，抽屉也能打开 |
| 28 | 09-28 | 改成 `true` 以后 admin 还看得到吗？Heat Map、AI Advisor 也一样吗？ | admin 永远看得到；锁住 = 会员看不到 tab **而且**网址 404。当时 Explorer + Heat Map 共用一个开关，Advisor 没开关（永远 admin-only） |
| 29 | 09-28 | Property Explorer、Heat Map、AI Advisor **各自一个开关**；`SALE_HISTORY_LOCKED` 不要了，「成交记录」tab 跟着 Property Explorer | ✅ 做了，见 D23 |

---

## 3. 数据事实（查询日期 2026-09-23，除非另注）

### 3.1 realtycheck（`petav3wk.propertylab_*`）

- **18,917 个 scheme**，全部有 monthly median（2021-01 → 2026-03），17,668 个有单笔成交。
- Category：Landed 12,461 · Shop 2,407 · Condo/Apartment 1,682 · Flat 843 · Industrial 713 · Serviced Apartment 536 · Office/SOHO 275。
- State 最多：Selangor 3,454 · Johor 1,794 · Perak 1,772 · Pulau Pinang 1,687 · KL 1,326；1,249 个没有 state。
- 当前 snapshot：`15799fa9…`，**2026-09-07 收集**，2026-09-18 导入。静态 snapshot，**不会自动更新**。
- 表：`propertylab_schemes`（项目）、`propertylab_scheme_snapshot_values`（median_rm / reported_psf / source_n）、
  `propertylab_monthly_observations`（每月）、`propertylab_sale_observations`（单笔，109,648 行，其中 78,606 行对得上 scheme）、
  `propertylab_places`（设施地点 52,942 个，不是项目数据）。
- 基本是已建成物业的 subsale 成交，但没有 primary/secondary 字段，分不出 developer 一手转让。
- 原始 archive 还有 **没导入** 的：`new_launch_pins` 4,965、`launch_details` 2,909、`known_schemes` 26,110。
- ⚠️ 原始 SQLite archive / NDJSON 在这台机器 `storage/app/` 下**找不到**。要重新 import 得先找到 archive。
- ⚠️ 没有 district 的行，坐标是按名字 geocode 的，可能落在别处同名地点旁边。
- ⚠️ **Landed PSF 和 catalogue（EdgeProp）面积基准不同**（realtycheck ≈ 1.34 倍），**不能混用**；high-rise PSF 两边几乎一致（≈ 1.00）；median price 各类型都一致。

### 3.2 Catalogue（`master_projects.catalog_projects`）

- 总数 **38,632**：Malaysia 19,507 · Hong Kong 18,174 · UAE 951。
- 马来西亚 active source：`edgeprop`（subsale file）15,520 · `edgeprop-nl`（新盘）2,731 · `propertysifu` 299 · `iproperty` 150。
- 约 15,500 个是 subsale，约 3,000 个是新盘（`market_segments` 有 `new_project` 的 3,175）。有 `price_median` + `psf_median` 的只有 9,343 个。
- `sale_status`：空白 11,536 · Completed 5,515 · New Launch 2,456 —— 大部分空白，不能用来分类。
- `SEGMENT_SUBSALE` 在马来西亚**没有任何一行**用，`hasSubsale()` 对 MY 永远 false。判断 subsale 要看 source 是不是 `edgeprop`。
- `property_type` 脏数据：约 1,700–1,800 行是 "0"/"1"/"2"/"3"。
- Bukit Sentosa 对比：realtycheck RM 200k / PSF 206（每月）vs catalogue id 4764（EdgeProp）RM 300k / PSF 232（每季度）。两边数字会打架 → realtycheck 不能覆盖 EdgeProp 的 `price_median`。

### 3.3 图表数据的 column（例：SUNWAY VELOCITY TWO，`scheme_id = 200601bacd03bfba`）

- 图上每个点 = `propertylab_monthly_observations` 一行：`month`、`price_rm`（纵轴）、`reported_psf`（切到 PSF 模式时）、`source_n`（当月笔数，统计窗口未确认 → flag `aggregate_count_window_unconfirmed`）。缺的月份 = 数据库没有那一行，不是 0 笔。
- 「Source median」卡片 = `propertylab_scheme_snapshot_values`（median_rm 1,000,000 / psf 1,056 / source_n 42）。
- 「前 3 个月 vs 最近 3 个月」「+3.7%」、趋势线（最小二乘）、outlier（Theil-Sen + MAD modified z-score > 3.5，≥7 个月才筛，超过 25% 就放弃）都是**前端现算**：`resources/js/utils/propertylab/chartModel.js`、`historyAnalysis.js`，图表组件 `resources/js/Components/PropertyLab/Research/EvidenceChart.vue`。
- 注意：很多月份 `source_n = 1`，所以价格曲线跳动大多是户型大小不同；**PSF 曲线比价格曲线更可靠**。

### 3.4 Catalogue 多来源 / 重复项

- 来源数：1 个 16,354 · 2 个 1,164（最常见 EdgeProp + EdgeProp 新盘 1,069）· 3 个 6 · 0 个 1,983。
- 合并机制：① 新盘 adapter 导入时自动（名字一字不差 + 坐标约 1 m 内）；② `catalogue:my-map-sources` 提议 + admin 在 Catalogue → New Project Database → **Action to Combine** 确认（**337 个在排队**，`my_auto_attach_enabled = false`）；③ admin 在 project 页 Review & combine（`attachSource`）。
- 找到 **1,144 组重复**：548 组是 EdgeProp subsale 自己列了两次（现有工具看不到）、502 组是「有来源行 + 无来源旧行」（没有工具处理）、86 组涉及新盘爬虫行（Combine 看得到）、8 其他。清单在 xlsx 的 **Catalogue duplicates** tab。
- 其他问题：部分坐标偏几十到几百 km；area 标错（Tangkak 的 estate 标成 Muar）；部分 landed estate 标成 condo。
- crosswalk 遇到重复项时，挂在**有来源的主行**上，不挂没来源的旧行。

---

## 4. 决定记录（只追加，不改）

| ID | 日期 | 决定 | 谁定 |
|---|---|---|---|
| D1 | 09-23 | `/property/research` 页面 + 所有 JSON endpoint + sidebar 菜单**只给 admin**（`User::isAdmin()` = super-admin/admin；不用 `admin` middleware，因为它放行 sales agent）。非 admin 得 404。**临时**：Explorer 改读 catalogue 后，路由和 sidebar 两处要一起放开 | Owner |
| D2 | 09-23 | 用 AI + web search 做 matching | Owner |
| D3 | 09-23 | **只有 confidence ≥ 90% 才自动链接**（目标文档） | Owner / 目标文档 |
| D4 | 09-23 | 「I 100% trust you」— 人工复核交给 Claude；**重点 high-rise**；landed / commercial 由 Claude 定案 | Owner |
| D5 | 09-23 | Phase 1 = high-rise（1.1）；Phase 2 = landed + commercial，**先做 backend**（1.2） | 目标文档 |
| D6 | 09-23 | realtycheck 数据进 master、分发到所有站点：**老板已批准授权** | Owner（老板） |
| D7 | 09-23 | Owner 有 **Hub 权限**，Hub 上的 migration / import 由 owner 执行 | Owner |
| D8 | 09-23 | **单笔成交记录也一起放进 master**（「一起放 (Recommended)」） | Owner |
| D9 | 09-23 | 设计：新 provider `realtycheck`；只挂到**已存在**的 project（attach-only）；**不改任何 canonical 字段**；两张新 master 表；用 **uuid** 当身份；reruns upsert、never delete | Claude，owner 同意 |
| D10 | 09-23 | 建议 Hub 一次导入 **all** 数据包（5,238），而不是只导 high-rise | Claude 建议，**待 owner 确认** |
| D11 | 09-23 | 建议 landed 只显示价格（RM），不显示 PSF；high-rise 两个都显示 | Claude 建议，前端阶段再确认 |
| D12 | 09-24 | NONE 里的小镇 taman、kampung、店屋、工业（landed + commercial）**先不放进 catalogue project**。Explorer 改读 catalogue 时怎么继续显示它们（建议 hybrid），到那时再定。high-rise NONE 909 个未定 | Owner |
| D13 | 09-24 | HOLD 和 RELATED 目前都 = **不导入**。建议规则：HOLD 再验证一轮，每个变成 LINK（导）或 NONE（不导），不留中间状态；RELATED 按现状永久不导，除非以后补建缺的那栋楼 | Claude 建议，**待 owner 确认 + 是否开始 HOLD 验证** |
| D14 | 09-24 | Owner 确认 D13 的 HOLD 部分：**277 个 HOLD 全部再查一轮**，每个只能变成 LINK（导）或不导（NONE / RELATED），不留「暂缓」。LINK 仍须 ≥ 90% | Owner |
| D15 | 09-24 | HOLD 复核完成：277 个 → **126 LINK / 57 RELATED / 94 NONE**，HOLD 清零。LINK 仍要求 ≥ 90%；high-rise 42 个全部由 Claude 裁决（其中 5 个 Claude 亲自查）。另修正旧链接 MENARA NORTHAM → EdgeProp 行 "Mansion One"（和 NORTHAM TOWER 同一栋） | Claude（按 D4 / D14 授权） |
| D16 | 09-25 | 以后把 production 数据库迁移 / 刷新到 wk 时，**所有 `propertylab_*` 表都要保留 wk 的版本**（mysqldump 用 `--ignore-table` 排除）。原因：production 这 12 张表全是空的（0 行，2026-09-25 查），原始 realtycheck archive 也已不在服务器上，覆盖了就无法重新导入。已备份到 `backup/propertylab_tables_20260925.sql.gz`（38.7 MB，12 张表），并写进 Claude memory（prod-to-dev-db-refresh） | Owner |
| D17 | 09-25 | Owner：911 个 high-rise NONE 做**第二轮复核**，先 Klang Valley、成交多的优先。做法：`scripts/pipeline/none_hr_candidates.py` 重新找候选（同州名字相近，不看坐标；+ realtycheck 坐标 1.2 km 内的 high-rise）→ 339 个完全没有候选（非 Klang Valley）维持 NONE；574 个打包成 `review/n_kv_01..11`（Klang Valley 242）+ `n_rest_01..14`（332），每包 24 个，附 realtycheck 页面地址 + 楼层、候选的最高/最低成交记录、自动「同一笔成交」标记（34 个）。研究员每次 5 个并行（避免再撞 limit），每 2 个案例存一次；high-rise LINK 由 Claude 裁决；最后用 `apply_none_recheck.py` 写回 | Owner |
| D18 | 09-25 | high-rise NONE 第二轮完成：574 个 → **293 LINK / 65 RELATED / 216 NONE**；另外 337 个没有候选的维持 NONE。Klang Valley 242 个 → 179 LINK / 39 RELATED / 24 NONE。LINK 仍须 ≥ 90%；574 个结论 Claude **逐个看过**才接受（记录在 `review/_none_adjudication.json`，只记 decision + uuid，apply 时发现 verdict 被改过就拒绝执行）。Claude 推翻 2 个（n548 PUSAT PERNIAGAAN DESA RIA：11 笔里 4 笔是店铺 → RELATED；n792 SS 16 SUBANG JAYA：唯一有价值的一笔和已链接的 SAUJANA RESIDENCY 是同一笔 → RELATED，避免重复计算），1 个改 segment（n752 The Gardens @ Polo Park 其实是 semi-D → landed）。同时修正 4 个旧链接（`review/_none_link_corrections.json`）：DESA TUN RAZAK 和两个 D'SECRET GARDEN 从没数据的 Google 行改到 EdgeProp 行；PANGSAPURI CANTIK PERMAI 从旧的 Pangsapuri Cantik（1990 flat）改到 **Marminton Homes**（隔壁 14 层新 condo，户型 1,130/1,300/1,323 sf 对上） | Claude（按 D4 / D17 授权） |
| D19 | 09-27 | Explorer 的地图 / 列表**只显示 catalogue 项目**（有 active realtycheck 来源 + 有坐标）。没挂上 catalogue 的 realtycheck scheme（NONE / RELATED / 小镇 taman 等）不再出现在 Explorer | Owner |
| D20 | 09-27 | Explorer 开放给会员后，会员**只看得到已发布的项目**（`published_at` + 在本站 listed，和项目页同一规则）。未发布的只有有 `view-projects` 权限的人看得到，Explorer 里标「Unpublished」 | Owner |
| D21 | 09-27 | 两个**临时**开关，默认锁住：`SALE_HISTORY_LOCKED=true`（项目页「成交记录」tab 只给 admin）、`PROPERTY_EXPLORER_LOCKED=true`（Explorer + Heat map 只给 admin）。没设或乱填 = 锁住。解锁：`.env` 改成 `false` → `php artisan config:cache`。**AI Advisor 不管开关永远 admin-only**（它还在读 `propertylab_*` 原始 archive，里面有 catalogue 没有的 scheme）。这取代 D1 的「解除 admin-only」做法 | Owner 要 env 开关；Advisor 规则 Claude 定 |
| D22 | 09-27 | 成交图规则（D11 落实）：high-rise 默认看**每尺中位价**，landed 默认看**总价**（两个都能切，landed 切到呎价时有提示）；一张图只有一条 y 轴，成交笔数另画一张小柱状图；呎价跟其他月份差 2 倍以上（或不到一半）的「异常」月份**不画在线上、留在表格**并标「异常」；少于 5 笔显示「仅供参考」；一个项目挂多个 scheme 时每个 scheme 一条线（最多 8 条），同一笔成交登记在两个名字下的按 月份+价格+呎价(+面积) 去重 | Claude（按 D4） |
| D23 | 09-28 | 三个 tab 各自一个开关（默认锁、读不到 = 锁）：`PROPERTY_EXPLORER_LOCKED`、`HEAT_MAP_LOCKED`、`AI_ADVISOR_LOCKED`；admin 永远三个都有。`SALE_HISTORY_LOCKED` **删除**，项目页「成交记录」tab 跟着 `PROPERTY_EXPLORER_LOCKED`。取代 D21 的「Advisor 永远 admin-only」：Advisor 现在可以开给会员 —— 对话按会员自己的 lead 隔离、每次回答扣会员的 AI credits，但它读的是 realtycheck 原始 archive（有 catalogue 没有的 scheme、不看发布状态）。wk 当前值：Explorer `false`、Heat Map `false`、Advisor `true`（保持改之前的效果） | Owner |

---

## 5. Matching

### 5.1 第一版 v1（09-23 16:47，已被 FINAL 取代，文件在 `superseded/`）

SAME confident 4,841 · probable 764 · PART OF 363 · NEEDS HUMAN CHECK 208 · 类型不同 849 · 不同 phase 65 · NOT IN CATALOGUE 11,827。
v1 的类别覆盖率（SAME）：Serviced Apt 79% · Flat 70% · Condo 65% · **Landed 27%** · Office 22% · Shop/Industrial 1–2%。
（4,370 / 656 这两个数字是 v1 的子集，已不再使用。）

### 5.2 FINAL（09-23 18:33，**当前有效**）

| 决定 | High-rise | Landed | Commercial | 合计 |
|---|---|---|---|---|
| ✅ **LINK**（≥ 90%，自动链接） | **1,883** | 3,281 | 74 | **5,238** |
| 🟡 **HOLD**（同一项目，但 < 90%） | 42 | 218 | 17 | **277** |
| 🔗 **RELATED**（phase / block，或混合 taman 里的 flat） | 227 | 289 | 19 | **535** |
| ❌ **NONE**（不在 catalogue / 别处同名 / 类型不同 / 不同 phase） | 909 | 8,673 | 3,285 | **12,867** |

- high-rise = Condo/Apartment + Flat + Serviced Apartment；commercial = Shop + Industrial + Office/SOHO。
- LINK 对到 **4,865 个不同 project**；其中 289 个 project 被 ≥2 个 scheme 对到（high-rise 59 个）。
- **验证链**：规则匹配（名字 normalize、JPPH 缩写、地契号、phase 号、按类型的距离、property class、median price、high-rise PSF）
  → 两轮 AI 复核（3,482 case）→ AI 盲审规则 100/100 一致 → **第二位独立 AI reviewer 盲审 1,761 case**（160 个做 web search）
  → high-rise 241 个分歧由 Claude 逐个裁决（**纠正了 10 个错误的 confident**，例如 Akasia Apartment Seksyen 32 = Berjaya Park 不是 Setia Alam；
  Sri Ara Kayu Ara 是 low-cost，不是 Ara Damansara 的 Sri Ara；Residensi Tun Razak = MKH TR Residence 第 1 期，不是 1 Razak Mansion）
  → landed/commercial：两次都说 SAME **且** median 价差 ±15% 内才 LINK，否则 HOLD
  → **estate-level 规则**：realtycheck 的 flat/condo 对上 catalogue「整个 taman」（landed + flat 混合、价格以 landed 为主）时不 LINK，改 RELATED（78 个）。
- 中途有 5 批盲审因额度/登录中断，都已救回。high-rise 第 1 批救回的是 reviewer 的**初判**（没做 web search），和之前不一致的都由 Claude 裁决。
- 人工抽查：规则匹配 170 个、AI confident 70 个、价格规则 LINK 15 个全部正确；probable 25 个约 22 个对。

### 5.2b HOLD 复核后（2026-09-24，**当前有效**，上面 5.2 的表是复核前）

| 决定 | High-rise | Landed | Commercial | 合计 |
|---|---|---|---|---|
| ✅ **LINK** | **1,908** | 3,377 | 79 | **5,364** |
| 🟡 HOLD | 0 | 0 | 0 | **0** |
| 🔗 RELATED | 242 | 319 | 31 | **592** |
| ❌ NONE | 911 | 8,765 | 3,285 | **12,961** |

LINK 对到 **4,979** 个不同 project（high-rise 1,846 个）；297 个 project 被 ≥2 个 scheme 对到（high-rise 60 个）。
HOLD 复核的证据：realtycheck 页面的地址（路名 / postcode / 镇 · district）、realtycheck 数据 API 给的 JPPH mukim、
portal 挂牌（面积 / 楼层 / 价格）、EdgeProp 记录的最高/最低成交（有几例和 realtycheck 同一笔成交：Kg Budiman RM625k 2021-12、
Taman Belimbing RM678k 2023-07、Taman Bukit Bayu RM680k 2024-10、Taman Karas RM220k 2023-08）。
Landed LINK 另外用脚本 `scripts/pipeline/rc_page_check.py` 把 realtycheck 页面地址和 catalogue 所在地逐个比对（结果 `review/_rc_page_check.json`）。

### 5.2c high-rise NONE 第二轮复核后（2026-09-25，**当前有效**，上面 5.2b 是复核前）

| 决定 | High-rise | Landed | Commercial | 合计 |
|---|---|---|---|---|
| ✅ **LINK** | **2,200** | 3,378 | 79 | **5,657** |
| 🟡 HOLD | 0 | 0 | 0 | **0** |
| 🔗 RELATED | 307 | 319 | 31 | **657** |
| ❌ NONE | 553 | 8,765 | 3,285 | **12,603** |

（landed +1 = n752 从 high-rise 改成 landed。）LINK 对到 **5,252** 个 project（high-rise 2,119 个，比复核前多 273 个）；316 个 project 被 ≥2 个 scheme 对到（high-rise 78 个）。新 LINK 背后的成交共 8,063 笔（`source_n` 合计）。

第一轮为什么漏：realtycheck 的坐标很多是**按名字 geocode** 的，离真楼几公里，所以按距离找候选找不到；拼写差异（Greenlane / Green Lane、Oren = Orange、Mozek = Mosaic、Desahill、Pandanmas、QuayWest）；catalogue 行没有类型（untyped）或类型写错。第二轮靠 realtycheck API 给的 **JPPH mukim**、单位面积、楼层、价格、**同一笔成交**来判。

Klang Valley 以外 NONE 多（332 个里 192 个 NONE），因为 catalogue 在那些州几乎只有新盘：Melaka、Perak 没有一个 EdgeProp 二手行，Pahang 1 个，Negeri Sembilan 4 个，Sabah 总共 2 行。已完工、成交好几年的楼（Port Dickson 度假公寓、Genting、Cameron、Ipoh、Melaka、Kota Kinabalu）没有 project 可以挂。

跨 scheme 重复成交检查（`scripts/tools/dup_sales.php`，全量用 `dup_sales_all.php`；环境变量 `SCHEME_GROUPS`）：316 个多 scheme project、7,987 个月度数据里，只有 **5 笔**同月同价同 psf 的重复（例：KENANGA POINT / MENARA KENANGA 2024-01 RM430k）——链接没错，但以后前端合并多条 series 时要按 月份 + 价格 + psf 去重。

### 5.3 四种状态在数据库里的样子

- ✅ LINK：**写入 master**。在已有 project 下挂一行 `catalog_project_sources`（provider `realtycheck`，`external_id` = scheme_id），
  再写 `catalog_project_price_months`（每月）和 `catalog_project_transactions`（单笔）。project 本身**一个字都不改**。
- 🟡 HOLD / 🔗 RELATED / ❌ NONE：**不写入**，只在 `FINAL-crosswalk.csv`。HOLD 和 RELATED 的每一行都已经写好候选 project 的 uuid。

### 5.4 各组的成交量（`source_n` 合计，2026-09-24 查）

| | High-rise | Landed | Commercial |
|---|---|---|---|
| LINK | 59,903 笔（857 个 scheme ≥20 笔） | 141,541（1,159） | 2,229（36） |
| HOLD | 1,272（20） | 4,118（48） | 260（6） |
| RELATED | **14,369（133）** | 8,956（83） | 379（7） |
| NONE | **20,499（307）** | 114,359（1,352） | 26,103（290） |

按成交量算，high-rise 已经 LINK 的约 62%；RELATED 和 NONE 的 high-rise 加起来还有约 35k 笔，值得第二步处理。

---

## 6. 已完成的实现（dev-wk）

### 6.1 Commits

| Commit | 内容 |
|---|---|
| `a5ed67ea4` | `/property/research` admin-only（`EnsureResearchEnabled`、`HandleInertiaRequests` 的 `features.propertylab_research`、test、handbook） |
| `7c65c3035` | realtycheck → master catalogue 全部代码 + 文档 |
| `ac59d35d9` | runbook：Hub deploy 后立刻跑 master migration |

2026-09-24 查：三个 commit 已在 `origin/dev-wk`（被别的 session 的 `/sync` push 上去），**不在 `origin/master`**。

### 6.2 代码

- 新 master 表（`database/migrations/catalogue/2026_09_23_2000*`）：
  - `catalog_project_price_months`：(source, month) unique；`median_price`、`median_psf`、`transactions`
  - `catalog_project_transactions`：(source, source_key) unique；`month`、`area_sqft`、`area_basis`（全部 `unknown`）、`price`、`psf`
  - `data_providers` 加 `realtycheck`（migration 会复制 master 的 id；现在两边下一个 id 都是 9）
- Models：`src/Analysis/Reference/CatalogProjectPriceMonth.php`、`CatalogProjectTransaction.php`；`CatalogProject::priceMonths()` / `saleTransactions()`
- 导出：`php artisan propertylab:export-catalogue-package --crosswalk=… --segment=all|high-rise|landed|commercial`（`app/Console/Commands/PropertyLab/ExportCataloguePackage.php`，在 wk 跑，只读）
- 导入：`php artisan catalogue:sync realtycheck --file=<package dir>`（`RealtycheckAdapter`；校验 sha256；uuid 找不到就跳过并计数 `skipped_unknown_project`）
- `config/project_catalogue.php` 的 `providers.realtycheck`：
  - `attach_only => true`：不会新建 project；`--full` 也不会删 project
  - `canonical_facts => false`：**不 resolve** 碰到的 project（resolve 会重算 floor plan、把不完整的已发布项目 **unpublish**、在没有其他来源的旧项目上**清空** `market_segments` / `name_translations`）；不刷新 AI 内容；realtycheck 的 `data_scraped_at` 不会变成 project 的
- 删除保护：有成交历史的 project，admin delete 会被拒（*recorded sale history*），orphan cleanup 也不删；Review & combine（`attachSource`）会把成交历史一起搬走
- 已知行为：已存在的 source link 优先（`upsertSource`），新数据包**不能把 scheme 挪到别的 project**；要挪就用 Review & combine
- 读取规则（给以后写前端的人）：只读 **active** source 的成交历史；一个 project 可能有多条曲线（多个 scheme），要么每个 source 一条线，要么合并，不能随便挑一条

### 6.3 数据包（wk 的 `storage/app/`）

| 数据包 | Schemes | Projects | 每月行 | 单笔 | complete |
|---|---|---|---|---|---|
| `realtycheck-package-20260924-all` | 5,238 | 4,865 | 78,306 | 27,765 | true |
| `realtycheck-package-20260923-high-rise` | 1,883 | 1,822 | 30,103 | 10,947 | false |

两个都用 adapter 对 live master 只读预跑过：**每一行都对得上，0 跳过，全部是 Malaysia project**。单笔成交确认是每个 scheme 最近的几笔（最多 8 笔）。

**⚠️ 2026-09-24 HOLD 复核后重新导出，上面两个旧包已移到 `superseded/packages/`，不要再用。当前有效：**

| 数据包 | Schemes | Projects | 每月行 | 单笔 | complete |
|---|---|---|---|---|---|
| `realtycheck-package-20260925-all` | 5,364 | 4,979 | 79,657 | 28,281 | true |
| `realtycheck-package-20260925-high-rise` | 1,908 | 1,846 | 30,361 | 11,052 | false |

同样对 live master 只读预跑过：0 跳过、全部 MY。

**⚠️ 2026-09-25 high-rise NONE 第二轮后再重新导出，上面 `…-20260925-*` 两个包已移到 `superseded/packages/`，不要再用。当前有效：**

| 数据包 | Schemes | Projects | 每月行 | 单笔 | complete |
|---|---|---|---|---|---|
| `realtycheck-package-20260925b-all` | 5,657 | 5,252 | 84,098 | 29,976 | true |
| `realtycheck-package-20260925b-high-rise` | 2,200 | 2,119 | 34,799 | 12,746 | false |

对 live master 只读预跑（`scripts/tools/dry_run_pkg.php`，`PKG=<包目录> php artisan tinker --execute="require …"`：adapter 读完整个包、数 skipped、查 country）：两个包 0 跳过、全部 MY（country_id 1）。

### 6.4 测试

- 新测试全过：`RealtycheckIngestionTest` 11 个、`PropertyLabAdvisorTest` 19 个；`SplitCatalogueSourcesMigrationTest`（fresh DB + rollback）通过。
- 339 个 catalogue 相关测试：16 个失败，**和不含改动的 baseline 完全一样**（HK adapter、没有 `pdo_sqlite`、测试文件缺 helper 等旧问题）。
- `CatalogAttachConcurrencyTest` 1 个失败：用 query log 证明第三个锁来自现有的 `enforcePublishedCompleteness`，不是新代码。
- **没跑** `PropertySifuAdapterTest`：每个 test 都重建数据库，约 45 分钟，当时 disk 只剩约 1 GB。

---

## 7. HOLD / RELATED / NONE 的计划（2026-09-24，**等 owner 决定**）

原则：**这次导入 master 只放 LINK。** 另外三组都不进 master，这一点不变。但三组不是都「忽略」：

### 🟡 HOLD 277 —— 建议：**不忽略，下一步就处理**（小、快、值钱）
- 组成：high-rise 42、landed 218、commercial 17。原因多半是 catalogue 那边**没有价格可以对比**、或价格差太多。
  - 129 个：两个独立 AI 都说 SAME，只是缺价格佐证
  - 98 个：第一轮不确定、第二轮说 SAME
  - 42 个：两轮 AI + Claude 裁决后仍 < 90%
- 每一行已经写好候选 project uuid，**确认 = 在 CSV 把 `decision` 改成 `LINK` → 重新导出 → 同一个命令导入**。
- 建议做法：先做 **high-rise 42 个**（其中 20 个有 ≥20 笔成交）：Claude 再做一次 web search + 地图/名字/价格核对，给 owner 一份清单；landed 218 之后再做。

### 🔗 RELATED 535 —— 建议：**这次先不导，Explorer 改读 catalogue 之前再决定**
- 不能直接挂：它是某个项目的一期/一栋，或混合 taman 里的 flat，挂上去价格会错。
- 但 **high-rise 227 个有 14,369 笔成交（133 个 ≥20 笔）**，不少其实是 catalogue 里**缺了的那栋楼 / 那一期**（例：MOLEK PINE 1 & 2 ↔ catalogue 只有 Molek Pine 2；flat 挂在整个 taman 那一行下）。
- 两个选项：
  - **(a)** 在 catalogue 补建那栋楼 / 那一期为新 project（用 `parent_catalog_project_id` 挂在上一级），再 LINK。需要一个「可以新建 project」的导入模式（现在 `attach_only` 不允许），而且要有人核对。
  - **(b)** 不补建，在上一级 project 页显示为「同一 estate 的相关成交」，并清楚标明。
- 建议：high-rise 227 个做 (a)；landed / commercial 的 RELATED 暂时不管。

### ❌ NONE 12,867 —— 建议：**这次忽略；但 Explorer 改读 catalogue 之前必须先定**
- 组成：landed 8,673、shop 2,378、industrial 701、condo 557、flat 242、office 206、serviced apt 110。大多是小镇 taman、kampung、店屋、工业，catalogue 本来就没有。
- **影响**：NONE 占 realtycheck 总成交量约 40%（约 16 万笔）。如果 Explorer **只**读 catalogue，这些会从 Explorer 消失。
- 选项：
  - **(a)** Explorer 用 **hybrid**：有 catalogue 的读 catalogue，NONE 的继续读 `propertylab_*`。不改 master、覆盖不丢。
  - **(b)** 在 catalogue **新建** project：会是只有名字、坐标、成交数据的「瘦」项目（没有户型图、照片），不会达到 publish 标准；而且要先防重复（有些可能是 catalogue 里名字不同的同一项目）。
- 建议：landed / commercial 的 NONE 用 **(a)**；high-rise NONE 909 个（307 个 ≥20 笔）以后可以分批做 (b)，因为目标文档以 high-rise 为主。

**待 owner 回答**：同不同意上面三组的建议？特别是 NONE 选 (a) 还是 (b)。

---

### 7.1 RELATED 按地区（2026-09-24）

> **2026-09-25 更新（NONE 第二轮后，RELATED 657 个）**：Klang Valley 315 · Kuala Lumpur 57 · Selangor 282 · Kajang 21 · Semenyih 10 · Seremban 11；high-rise 307 个，其中 Klang Valley 208。重算脚本 `scripts/pipeline/related_by_region.py`，下面是 09-24 的旧表。

定义：**Klang Valley** = KL + Putrajaya + Selangor 的 Petaling / Klang / Gombak / Hulu Langat / Sepang 五个 district；
**Kajang、Semenyih** 属于 Hulu Langat，所以也算在 Selangor 和 Klang Valley 里面（会重复计算）；**Seremban** = 森美兰 Seremban district。
地区优先用 realtycheck 的 state / district / mukim，缺的用 catalogue project 的 area 补（535 个全部定位得到）。

| 地区 | High-rise | Landed | Commercial | 合计 | 成交笔数（high-rise / 全部） |
|---|---|---|---|---|---|
| Kuala Lumpur | 33 | 3 | 1 | **37** | 1,347 / 1,462 |
| Selangor | 118 | 86 | 14 | **218** | 6,927 / 8,620 |
| Klang Valley | 148 | 74 | 14 | **236** | 8,051 / 9,301 |
| Kajang | 14 | 2 | 0 | **16** | 1,470 / 1,476 |
| Semenyih | 6 | 3 | 0 | **9** | 94 / 197 |
| Seremban | 0 | 11 | 0 | **11** | 0 / 521 |
| （全部 RELATED） | 227 | 289 | 19 | 535 | |

**HOLD 复核后（当前有效，RELATED 共 592 个）：**

| 地区 | High-rise | Landed | Commercial | 合计 | 成交笔数（high-rise / 全部） |
|---|---|---|---|---|---|
| Kuala Lumpur | 34 | 6 | 2 | **42** | 1,353 / 1,650 |
| Selangor | 127 | 94 | 21 | **242** | 7,355 / 10,091 |
| Klang Valley | 157 | 85 | 22 | **264** | 8,465 / 10,940 |
| Kajang | 16 | 4 | 0 | **20** | 1,566 / 1,661 |
| Semenyih | 6 | 3 | 0 | **9** | 94 / 197 |
| Seremban | 0 | 11 | 0 | **11** | 0 / 521 |

典型例子（Klang Valley high-rise，按成交量）：TMN PUTRA PERDANA（Sepang，643 笔，中位 RM 200k）↔ catalogue「Taman Putra Perdana」整个 township（flat + 排屋 + 半独立，中位 RM 416k）；
BDR SG LONG（Kajang，615 笔）↔「Sungai Long」；DESA AMAN PURI（Gombak，394 笔）↔「Desa Aman Puri」（condo + flat + 排屋混合）。
都是 realtycheck 的「某 taman 里的 flat/condo」对上 catalogue 的「整个 taman」，所以不能直接挂。
完整名单（535 行，Klang Valley 和 Seremban 排前面，high-rise 优先、按成交量排序）：`RELATED-by-region.csv`。

### 7.2 HOLD 再查一轮（2026-09-24 开始）

- 277 个案例打包在 `review/h_*_in.jsonl`：7 批（high-rise 2 批 × 21、landed 4 批 × 53–55、commercial 1 批 × 17），
  每个案例带 realtycheck 页面链接、最近 8 笔成交的面积和价格、catalogue 候选的 EdgeProp 链接，以及 3 km 内名字相近的其他候选。
- 7 个 AI 研究员并行做 web 复核，结果写到 `review/h_*_out.jsonl`；high-rise 的结论由 Claude 逐个裁决（`review/_hold_adjudication.json`）。
- 用 `scripts/pipeline/apply_hold_recheck.py` 把结论写回 `final_links.json`（旧版备份在 `superseded/`），
  再用 `scripts/pipeline/report_final.py` 重建 FINAL xlsx + CSV，然后重新导出数据包。
- **结果（2026-09-24 完成）**：high-rise 42 → 25 LINK / 15 RELATED / 2 NONE；landed 218 → 96 / 30 / 92；commercial 17 → 5 / 12 / 0。
  中途两次中断（session limit、account on hold），重开后续跑完成。Claude 推翻或亲自决定的 high-rise：THE MAPLE（NONE → LINK 到 Sentul 的
  "The Maple Condominium"，1,569 sqft 户型吻合）、KOI PRIMA SUITE（混了 Koi Prima + Koi Suites → RELATED）、MKH BOULEVARD JALAN BUKIT
  （2025 年前的整区标签，含两栋 + 商铺 → RELATED）、PUCHONG PERMAI JLN 2/x（区内多栋同价 → RELATED）、PANGSAPURI BUKIT BARU / SERI WARISAN（LINK）。
- **顺带发现的 catalogue 问题**：Pangsapuri Sentral 的 RM645k 中位价是错的（实际约 RM250k）；Seasons Tower、PPR Desa Petaling、Taman Ikan Emas
  等坐标错；Vogue Suites One、GEM Residences、Taman SA、Northam 有重复行。
- **范围外的候选**（未验证到 90%，没动）：VILLA HEIGHT 2 → "Taman Villa Heights 2"（RM350k vs RM337.5k）。

## 8. 以后 Explorer 改读 catalogue（B 部分）

| Explorer 功能 | 现在读 | 改成 |
|---|---|---|
| 地图、列表、搜索、比较（`allSchemes` / `searchSchemes` / `compareSchemes`） | `propertylab_schemes`（18,917） | `catalog_projects`：MY、有 EdgeProp subsale 来源、有坐标，约 15,470 个（+ NONE 的处理见 §7） |
| 每月价格图（`getSchemeEvidence`） | `propertylab_monthly_observations` | `catalog_project_price_months`；没有的用 EdgeProp `quarterly_sale_psf` 顶上（10,517 个 project 有） |
| 单笔成交、价格位置分析（`getSaleSample` / `analysePricePosition`） | `propertylab_sale_observations` | `catalog_project_transactions` |
| 附近设施、通勤（`getNearbyPlaces` / `calculateJourneys`） | `propertylab_places` | 不用改；⚠️ 要确认 **production 有没有这份数据**（import 只在 wk 跑过） |

前端几乎不用改：只要后端输出格式不变（id 改用 catalogue uuid）。改完测试后，**解除 admin-only（D1 的两处一起改）**。

---

## 9. 进度 & 下一步（只追加）

| 日期 | 状态 |
|---|---|
| 09-23 | admin-only 上线；FINAL crosswalk 完成；代码 + 两个数据包 + 文档完成并 commit |
| 09-24 | commit 已在 `origin/dev-wk`；master 还**没有**新表和 `realtycheck` provider（Hub import 未做）；wk 上 3 个 catalogue migration 故意保持 pending（规则：Hub 先跑）|
| 09-24 | 写了本文件；matcher / 复核脚本复制到 `scripts/` |
| 09-24 | Owner 定 D12；Claude 解释 HOLD / RELATED，提出 D13 |
| 09-24 | Owner 确认 D14；RELATED 分区统计完成（§7.1）；HOLD 277 个开始 web 复核（§7.2） |
| 09-24 | HOLD 复核中断：研究员先被 session limit（429）打断，重开后又遇到 **account on hold**（Claude Code 账号被暂停，见 https://claude.ai/restricted）。已完成的结论在 `review/h_*_out.jsonl`（含 `*r_out` 续跑），未完成的名单在 `review/_hold_recheck_missing.json`；high-rise 已裁决的记录在 `review/_hold_adjudication.json`，地址核对在 `review/_rc_page_check.json`。续跑：为 missing 名单建 `h_*r2_in.jsonl` 再派研究员，或直接人工查；全部有结论后跑 `scripts/pipeline/apply_hold_recheck.py` → `report_final.py` → 重新导出数据包 |
| 09-24 | 续跑：high-rise 42 个**全部完成**（最后 5 个由 Claude 亲自查：h019、h021 LINK；h017、h018、h020 RELATED，文件 `review/h_hr_01c_out.jsonl`）；landed 剩 54 个交给 2 个研究员（`review/h_ld_r2a/r2b_*`）。收尾步骤：① `apply_hold_recheck.py` ② 备份旧 FINAL xlsx/csv 到 `superseded/` 后跑 `report_final.py` ③ 重算 `RELATED-by-region.csv` ④ 重新导出 all / high-rise 数据包，旧包移到 `superseded/packages/` ⑤ 对 live master 只读预跑 ⑥ 更新 repo 文档 + 本 log + memory 并 commit |
| 09-24 | **HOLD 复核全部完成**（D15）；FINAL xlsx/csv 重建（旧版在 `superseded/`）；RELATED 分区重算；数据包重新导出为 `…-20260925-all / -high-rise` 并预跑通过；repo 文档更新 |
| 09-25 | Owner 定 D16：prod → wk 刷新必须保留 `propertylab_*`；已备份 12 张表并写进 memory |
| 09-25 | Owner 问：为什么不新建项目？NONE 都不是 high-rise 吗？→ 查了：realtycheck high-rise 3,061 个 = LINK 1,908（62%）/ RELATED 242 / **NONE 911**（condo 558、flat 243、serviced 110；Sabah+Sarawak 221；Klang Valley 242；≥20 笔成交 307 个）。⚠️ **NONE 里有漏配**：第一轮的「不在 catalogue」大多由规则判定、只盲审了 30 个。例：MENTARI COURT PJS 8（299 笔）→ catalogue 有 "Mentari Court, Bandar Sunway"；PELANGI DAMANSARA FASA 1A（256 笔）→ 有多个 "Pelangi Damansara" 行；CENTRAL PARK @ LAMAN GLASIER（294 笔）→ "Laman Glasier @ Country Garden Central Park"。粗筛：911 个里 252 个在同州 catalogue 有名字相近的行（很多是误报），真正漏配的估计几十到一百多个。建议下一步：high-rise NONE 第二轮复核（先 Klang Valley + 成交多的） |
| 09-25 | high-rise NONE 第二轮开始（D17）：候选 + 574 个案例包就绪，第 1 波 Klang Valley 1–5 包已派出 |
| 09-25 | 第二轮进行中：Klang Valley 11 包全部完成（242 → 179 LINK / 39 RELATED / 24 NONE）；rest 14 包每次 5 个研究员并行；每包回来 Claude 逐个看、接受或推翻。研究员 WebSearch 每人约 200 次上限（kv_08、kv_09 用完后改直接 curl：iProperty、Mudah、StarProperty、DurianProperty、Waze 能开；EdgeProp、PropertyGuru、brickz、拍卖网站 403） |
| 09-25 | 中途 session 中断一次：rest_14 做到 17/20，恢复后补完。**D18 完成**：apply → FINAL xlsx/csv 重建（旧版 `superseded/before-none-recheck-*`）→ RELATED 分区重算（657）→ 数据包重新导出为 `…-20260925b-all / -high-rise` 并对 live master 预跑通过 → repo 文档（realtycheck-transactions.md）更新 |
| 09-27 | **Hub 导入完成**：Run #12 success，realtycheck/MY **5,657 created / 0 failed**（commit `54b3a49e8` 记录）。production 的 `catalogue` connection 读 master **LIVE**，不需要 mirror。下方「下一步」1–4 完成 |
| 09-27 | **前端完成（锁在开关后面）**：项目页新增「成交记录」tab（Overview 后面）+ Explorer 改读 catalogue（点项目 → 右边抽屉打开同一个项目页，直接停在「成交记录」）。wk 已 live-build；等 owner 检查后才解锁。数字：有成交且有坐标的 catalogue 项目 **5,247** 个，其中**已发布只有 3 个**（Pavilion Damansara Heights、Laurel Residence、Oxley Towers @ KLCC）→ 开放给会员后会员只看到 3 个项目，要先发布 |
| 09-27 | ⚠️ **事故（约 30 分钟，没人受影响）**：新开关写进 `config/features.php` 后没有马上 `config:cache`；cached config 里没有这两个 key → `config()` 回 null → 锁**失效 = 开放**（16:25–16:55 UTC），route cache 也是旧的，那段时间 AI Advisor 路由还没有新的 admin 限制。修复：代码默认值改成 `config(key, true)`（读不到 = 锁住）+ 重跑 `config:cache` / `route:cache`，验证两个开关都读 true。查 Apache access log：那段时间**没有任何** `/property/research` 或 `/sale-history` 请求 |
| 09-28 | D23 完成：三个开关 + 「成交记录」跟 Explorer。wk `.env`：删 `SALE_HISTORY_LOCKED`，加 `HEAT_MAP_LOCKED=false`、`AI_ADVISOR_LOCKED=true`（`PROPERTY_EXPLORER_LOCKED=false` 不变），先 `config:cache` 再上代码，`route:cache`，live-build。live 验证：admin 三个 tab、5,247 个项目；会员 Explorer + Heat Map、3 个项目、Advisor 404；假设只开 Heat Map → 会员只有 Heat Map、点项目在原地开抽屉、「成交记录」403；假设只开 Advisor → 只有 Advisor、没有项目列表、抽屉 404 |

**下一步（按顺序）：**
1. ~~GitHub 网页开 PR：`dev-wk` → `master`~~ ✅ 已合并并在 Hub deploy。
2. ~~Hub deploy 后**立刻**~~ ✅ 已跑：
   ```bash
   php artisan migrate --path=database/migrations/catalogue --database=catalogue
   php artisan migrate --path=database/migrations/catalogue
   ```
   ⚠️ 在这之前，Hub 上的 Review & combine、admin delete、`--full` sync 清理都会报错。
3. ~~把数据包 `scp` 到 Hub，导入~~ ✅ 09-27 Run #12（第一次**不要** `--full`）：
   ```bash
   php artisan catalogue:sync realtycheck --file=/path/to/realtycheck-package-20260925b-all
   ```
   预期：`5657 created, 0 failed`；`price_months` 84,098、`transactions` 29,976、`skipped_unknown_project` 0。
4. ~~Production：先确认 COPY 还是 LIVE~~ ✅ 09-27 确认是 **LIVE**，不需要 mirror。
5. ~~HOLD 复核~~ ✅ 2026-09-24 完成（§7.2）。
6. ~~前端：project 页加成交走势图 + 最近成交列表~~ ✅ 09-27（在 `SALE_HISTORY_LOCKED` 后面；另写了 `SaleHistoryTab.vue`，没复用 `EvidenceChart.vue`，见 §12）。
7. ~~Explorer 改读 catalogue（§8）~~ ✅ 09-27（D19 只显示 catalogue 项目；在 `PROPERTY_EXPLORER_LOCKED` 后面；AI Advisor 仍读 `propertylab_*`）。
8. （可选）清理 catalogue：337 个 Action to Combine 排队建议、约 1,050 组 subsale 重复项。

**下一步（09-27 更新，按顺序）：**
1. Owner 在 wk 用 admin 帐号检查：有成交的项目页 →「成交记录」tab（例：`/my/projects/28-boulevard-28-blvd`，2 个 scheme、81 笔）；`/property/research` 的 Explorer 和 Heat map；项目页加 `?preview=locked` 看会员视角（tab 会消失）。
2. 满意后解锁：wk `.env` 改 `SALE_HISTORY_LOCKED=false`、`PROPERTY_EXPLORER_LOCKED=false` → `php artisan config:cache`。**不用 build、不用 deploy**。
3. 发布项目：5,247 个有成交的项目只发布了 3 个，不发布的话会员在 Explorer 几乎看不到东西（D20）。
4. Production：PR `dev-wk` → `master` + deploy；production `.env` 不加这两行也安全（默认锁住），要开时再加 `=false` + `config:cache`。
5. 待决定：来源怎么写（现在是「与此项目配对的成交登记记录」，没写 realtycheck / JPPH）；mobile app 要不要也加这个 tab（API 还没有）；AI Advisor 改读 catalogue 后才能开放给会员。

---

## 10. 陷阱（踩过或差点踩的）

- **prod → wk 数据库刷新会清空 `propertylab_*`**：production 那边是空表，realtycheck 数据只在 wk。刷新时必须 `--ignore-table` 全部 `propertylab_*` 表（D16）；万一被覆盖，用 `backup/propertylab_tables_20260925.sql.gz` 还原（`zcat … | mysql petav3wk`）。
- wk 的 `catalogue` connection 是 **read-only**（`petav3_read`），写 master 会 `1142 command denied`，所有写 master 的动作都在 **Hub**。
- 不能写进 `catalog_projects` 字段：master sync 会覆盖（之前吃掉过 80 条 `key_sqft`）。
- 不要在 realtycheck source 的 `fields` 里放 canonical key（尤其 `project_name`），否则没有 EdgeProp 来源的项目会被改名成 JPPH 大写名。
- ingestion 没有 `catalogProjectId` 会**新建** project —— 所以 adapter 对不上的一律跳过，provider 设 `attach_only`。
- 单个 segment 的数据包**不能**用 `--full`（adapter 会拒绝；all 包允许，但只在 crosswalk 有删减时才用）。
- 这台机器的 LibreOffice 没有 Calc，xlsx 都是写死数值、没有公式。
- **Disk**：09-23 一度只剩约 1 GB（99%），主要是 mobile app 的 build 工具（`.gradle` 7.2 GB、`android-sdk` 4.9 GB 等）和 MySQL binlog。跑会重建数据库的 phpunit 前先看 `df -h /`。
- 这个 repo 有多个 Claude session 同时在用，别人会 `git add -A`；commit 前只 stage 自己的 hunk。
- **realtycheck 的坐标常是按名字 geocode 的**（页面地址是由坐标反查出来的），会落在同名的别处甚至别州；判断位置要用 `https://realtycheck.my/api/schemes?q=<name>` 给的 **mukim**。没有页面的 scheme，`/scheme/<slug>` 会打开**另一个**同名 scheme 的页面，别信。
- **wk 的 realtycheck 快照不完整**：有些 scheme 在 realtycheck 网站上有、在 `propertylab_*` 里没有（PANGSAPURI BAYU PUTERI-PJU 3、RESIDENSI SUTERA 7、P/PURI SERI JASA - SG BESI INDAH）。
- **两个 scheme 可能含同一笔成交**（JPPH 用两个名字登记）：挂到同一 project 时会重复，合并 series 要按 月份+价格+psf 去重。
- **catalogue 行可能带着别栋楼的数据或地址**（Seri Nilam 槟城用了 Ampang 的地址和 "Johor"；Pangsapuri Cantik 混了隔壁 Marminton 的成交 + KL 同名楼的来源；Greenlane Heights Block G 带着 Block F 的成交）。链接跟着**楼**走，不跟数据走；问题清单在 `review/_none_round_catalogue_issues.md`。
- 研究员 agent 的 WebSearch 每个约 200 次上限；同时开 >5 个会撞 session limit。
- 自动接受脚本只接受**明确列出的 case**（`scripts/tools/accept_n.py`，在本文件夹里跑），记录 reviewed 的 decision+uuid；apply 发现 verdict 在 review 后被改就拒绝。bash 的 `GROUPS` 是保留变量，别拿来当环境变量名。
- **新加的 config key 要马上 `php artisan config:cache`**：wk 的 config 是 cache 的，新 key 在 cache 里不存在 → `config()` 回 null。开关类的 key 一律写成 `config('features.x_locked', true)`（读不到 = 锁住），否则会像 09-27 那样锁失效 30 分钟。路由改了要 `route:cache`。
- **`scripts/live-build.sh` 的 FRESH 判断会漏**：它拿源码 mtime 和 `public/build/manifest.json` 的 mtime 比；别的 session 的 build 正在跑时你改的文件，mtime 比 manifest 早、却没被编进去 → 用 `--force`，然后 grep 线上 chunk 确认改动真的在里面。
- 截图 harness 如果直接拿线上 build 的 CSS，新写的 Tailwind class 不会生效（CSS 是按源码扫描生成的）；要在 harness 里用 `@tailwindcss/vite` 编 `resources/css/app.css`。

---

## 11. 本文件夹里有什么

| 文件 / 目录 | 用途 |
|---|---|
| `DISCUSSION-LOG.md` | 本文件 |
| `FINAL-crosswalk.csv` | **导出数据包用的就是它**：`scheme_id, scheme_name, category, segment, decision, confidence_pct, import_phase, catalog_project_id, catalog_project_uuid, catalog_project_name, decided_by, reason` |
| `FINAL-propertylab-catalogue-match.xlsx` | 给人看的报告：Summary、Phase 1 high-rise links、Phase 2 landed + commercial、Hold、Related、All schemes、Catalogue duplicates |
| `final_links.json`、`final.json`、`match_v3.json` 等 | matching 中间结果 |
| `catalogue_my_projects.json`、`realtycheck_*.json` | 两边数据的快照（matching 的输入） |
| `report_stats.json` | 报告统计 |
| `review/` | 所有 AI verdict、`_key*.json`、`v_*` 最终验证文件 |
| `scripts/pipeline/` | matcher 脚本（`normalize.py`、`match_v3.py`、`finalize.py`、`report_final.py` …），2026-09-24 从 session scratchpad 复制过来 |
| `scripts/review-decisions/` | 每批 AI / Claude 复核的决定脚本（`vhr*` = high-rise 最终验证，`vrest*` = 其余，`r2b*` = 第二轮） |
| `RELATED-by-region.csv` | RELATED 名单（657 行，2026-09-25 重算；09-24 版在 `superseded/`）+ 地区、成交量；Klang Valley / Seremban、high-rise 优先 |
| `review/h_*_in.jsonl` / `h_*_out.jsonl` | HOLD 复核的案例包和结论（`*r_*` / `r2*` 是中断后的续跑，`h_hr_01c_out` 是 Claude 亲自决定的 5 个） |
| `review/_hold_adjudication.json` | Claude 对 42 个 high-rise 的裁决（接受 / 推翻 / 亲自决定） |
| `review/_link_corrections.json` | 对旧链接的修正（MENARA NORTHAM → Mansion One） |
| `review/_rc_page_check.json`、`review/_rc_pages/` | realtycheck 页面地址比对结果和页面缓存 |
| `scripts/pipeline/apply_hold_recheck.py` | 把复核结论写回 `final_links.json`（缺案例会拒绝执行） |
| `scripts/pipeline/rc_page_check.py` | LINK 的地址比对 |
| `backup/propertylab_tables_20260925.sql.gz` | wk 全部 12 张 `propertylab_*` 表的备份（数据只在 wk，production 是空表） |
| `review/n_candidates.json`、`review/n_kv_*` / `n_rest_*` `_in/_out.jsonl` | high-rise NONE 第二轮：候选、案例包（574 个）、研究员结论 |
| `review/_none_adjudication.json` | Claude 对 574 个结论的接受 / 推翻记录 |
| `review/_none_link_corrections.json` | 第二轮顺带修正的 4 个旧链接（带 from_uuid 防护） |
| `review/_none_round_catalogue_issues.md` | 第二轮发现的 catalogue 重复行 / 错数据 / 覆盖缺口清单（给 Hub 清理用，约 400 个行 id） |
| `scripts/tools/` | 第二轮用的小工具：研究员 prompt `n_prompt.txt`、`accept_n.py` / `shown.py`（review + 接受）、`dry_run_pkg.php`（数据包预跑）、`dup_sales*.php`（跨 scheme 重复成交）、`src_one.php` / `qp.php` / `sales_cmp.php`（只读查 catalogue 行和成交） |
| `scripts/pipeline/none_hr_candidates.py`、`none_hr_packets.py`、`apply_none_recheck.py`、`none_link_collisions.py`、`related_by_region.py`、`region.py` | 第二轮的候选、打包、写回、「一个 project 多个 scheme」检查、RELATED 分区 |
| `superseded/` | 旧版本，不要再用：v1 的 crosswalk / xlsx、review-for-humans.xlsx、复核前的 FINAL xlsx / csv、`final_links-before-hold-recheck.json`、旧数据包 `packages/` |

当前有效的数据包在上一层：`storage/app/realtycheck-package-20260925b-all/`、`storage/app/realtycheck-package-20260925b-high-rise/`（2026-09-25 NONE 第二轮后；`…-20260925-*` 已移到 `superseded/packages/`）。

---

## 12. Explorer + 成交记录（2026-09-27，在开关后面）

**会员看到什么（解锁后）**
- 项目页（`/my/projects/{slug}`，也就是 admin 编辑页 Live preview、Sales Projects 的 Property Preview、Area Guide 抽屉里的同一个 `ProjectDetailContent.vue`）：项目有 active realtycheck 来源时，Overview 后面多一个「成交记录」tab：三格摘要（成交笔数、涵盖期间、最近有成交的月份）→ 每月中位价折线（每尺 / 总价切换，可切换成表格）→ 每月成交笔数柱状图 →「最近成交」表（最新在前，异常的标「异常」）。没登录 = 锁卡片，不发请求。
- Explorer（`/property/research`）：地图 / 列表 / 搜索 / Heat map 都读 catalogue 项目（D19），会员只有已发布的（D20）；点项目 → 右边抽屉打开项目页，直接停在「成交记录」。Compare 和收藏在 Explorer 模式下关掉（那两个功能还绑着旧的 scheme id）。
- AI Advisor tab：永远只有 admin 看得到（D21）。

**开关**
| `.env` | 默认 | 锁住时 | 解锁 |
|---|---|---|---|
| `PROPERTY_EXPLORER_LOCKED` | `true` | 会员没有 Property Explorer tab；**项目页「成交记录」tab 也只有 admin 看得到**（admin 加 `?preview=locked` 看会员视角） | `false` → `php artisan config:cache` |
| `HEAT_MAP_LOCKED` | `true` | 会员没有 Heat Map tab | `false` → `php artisan config:cache` |
| `AI_ADVISOR_LOCKED` | `true` | 会员没有 AI Advisor tab | `false` → `php artisan config:cache` |

（09-28 起，D23。`SALE_HISTORY_LOCKED` 已删除。锁住 = tab 不显示**而且**那个 tab 的网址 404；三个都锁 → 会员打开 `/property/research` = 404。admin 永远三个都有。）

**代码在哪**（repo 文档：`docs/modules_handbook/shared/project-catalogue/realtycheck-transactions.md` 的 *Where it shows*、`docs/modules_handbook/main/property-research/readMe.md`）
- 数据：`app/Services/Property/CatalogueSaleHistoryService.php`（读 `catalog_project_price_months` + `catalog_project_transactions`，去重、薄数据、landed 判断）
- 权限：`src/Analysis/Support/SaleHistoryAccess.php`、`src/PropertyLab/Advisor/Http/ResearchAccess.php`（+ `EnsureResearchEnabled` / `EnsureAdvisorAccess`）
- 端点：`GET /{country}/projects/{slug}/sale-history`（登录 + 开关 + 发布规则）；`GET /property/research/projects/{uuid}`（Explorer 抽屉）
- Explorer 列表：`src/PropertyLab/Research/CatalogueExplorerIndex.php`（cache 10 分钟）
- 前端：`resources/js/Components/ProjectDetail/SaleHistoryTab.vue`、`resources/js/utils/saleHistory.js`
- 测试：`tests/Feature/Property/CatalogueSaleHistoryTest.php`、`tests/Feature/Portal/PropertyLabAdvisorTest.php`、`SaleHistoryTab.test.js`、`saleHistory.test.js`

**用 live 数据验证过**（全部在 rollback 的 transaction 里）：admin Explorer 5,247 个项目；锁住时会员 404、会员 / 访客拿 `sale-history` = 403；28 Boulevard 两个 scheme、81 笔成交；假设解锁后会员只看到 3 个已发布项目、AI Advisor 仍 404、未发布项目抽屉 404。

