# 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` |

---

## 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 |

---

## 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.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。

### 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）

定义：**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 包已派出 |

**下一步（按顺序）：**
1. GitHub 网页开 PR：`dev-wk` → `master`（这台机器没有 `gh`）。
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，导入（第一次**不要** `--full`）：
   ```bash
   php artisan catalogue:sync realtycheck --file=/path/to/realtycheck-package-20260925-all
   ```
   预期：`5364 created, 0 failed`；`price_months` 79,657、`transactions` 28,281、`skipped_unknown_project` 0。
4. Production：先确认 COPY 还是 LIVE（`.env` 的 `CATALOGUE_USE_DEFAULT_CONNECTION`）；COPY → deploy 后 `catalogue:mirror-from-master`（先不带 `--apply` 看报告，再 `--apply`）。
5. ~~HOLD 复核~~ ✅ 2026-09-24 完成（§7.2）。
6. 前端：project 页加成交走势图 + 最近成交列表（复用 `EvidenceChart.vue` / `historyAnalysis.js`）。
7. Explorer 改读 catalogue（§8），按 §7 的 NONE 决定处理覆盖，然后解除 admin-only。
8. （可选）清理 catalogue：337 个 Action to Combine 排队建议、约 1,050 组 subsale 重复项。

---

## 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。

---

## 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 名单（592 行）+ 地区、成交量；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 是空表） |
| `superseded/` | 旧版本，不要再用：v1 的 crosswalk / xlsx、review-for-humans.xlsx、复核前的 FINAL xlsx / csv、`final_links-before-hold-recheck.json`、旧数据包 `packages/` |

当前有效的数据包在上一层：`storage/app/realtycheck-package-20260925-all/`、`storage/app/realtycheck-package-20260925-high-rise/`。
