Quellcode durchsuchen

docs(kb): ORC 走默认 + raw EXTERNAL + dt STRING 规范 + UDF 自查表计划 + 里程碑

tianyu.chu vor 2 Wochen
Ursprung
Commit
eb86fe4256
4 geänderte Dateien mit 40 neuen und 13 gelöschten Zeilen
  1. 4 5
      kb/00-项目架构.md
  2. 3 7
      kb/20-数仓分层与建模.md
  3. 27 1
      kb/90-重构路线.md
  4. 6 0
      kb/92-重构进度.md

+ 4 - 5
kb/00-项目架构.md

@@ -614,13 +614,13 @@ bin/datax-multiple-hive-job-starter.sh -gcd jobs/raw/trd -start-date 20260415 -e
 **核心原则:DDL 与计算 SQL 物理分离,DDL 全部在 `manual/ddl/` 下单一来源。**
 
 - `manual/ddl/` 存放**所有 DDL**(首次建表 + 后续 ALTER),采用 **migration 模式**:每次 DDL 操作是一个不可变文件,**禁止回头改老文件**
-- `jobs/` 存放每日调度执行的采集 / 计算任务,只做 `INSERT OVERWRITE` 或数据同步,不写 CREATE TABLE
+- `jobs/` 存放调度执行的采集 / 计算任务,只做 `INSERT OVERWRITE` 或数据同步,不写 CREATE TABLE
 
 
 一张表的完整生命周期涉及:
 - `manual/ddl/{layer}/{domain}/{表名}_create.sql` —— 首次建表,永久保留
 - 若干 `manual/ddl/{layer}/{domain}/{表名}_{yyyymmdd}_{描述}_change.sql` —— 之后每次 ALTER,独立文件
-- `jobs/{layer}/{domain}/{表名}.sql` 或 `jobs/{layer}/{domain}/{表名}/{表名}-{NN}-{描述}.sql` —— 每日调度的计算 SQL(不含建表),详见 §9.2
+- `jobs/{layer}/{domain}/{表名}.sql` 或 `jobs/{layer}/{domain}/{表名}/{表名}-{NN}-{描述}.sql` —— 调度执行的计算 SQL(不含建表),详见 §9.2
 
 ### 9.1 `manual/ddl/` —— DDL 唯一来源
 
@@ -698,9 +698,9 @@ ADD COLUMNS (
 );
 ```
 
-**存储格式约定**:所有分层一律 `STORED AS ORC` + `orc.compress=NONE`。策略详见 `20-数仓分层与建模.md` §7。
+**存储格式约定**:所有分层一律 `STORED AS ORC`。策略详见 `20-数仓分层与建模.md` §7。
 
-### 9.2 `jobs/` 层 —— 每日调度的计算 SQL
+### 9.2 `jobs/` 层 —— 调度执行的计算 SQL
 
 **文件粒度:一张目标表对应一套 SQL 文件**,按复杂度两档:
 
@@ -828,7 +828,6 @@ DROP TABLE IF EXISTS raw.raw_trd_legacy_order_his_o;
 
 CREATE TABLE raw.raw_trd_legacy_order_his_o
 USING ORC
-OPTIONS ('orc.compress'='NONE')
 COMMENT '交易域-历史订单CSV导入(一次性,全STRING)'
 AS
 SELECT

+ 3 - 7
kb/20-数仓分层与建模.md

@@ -183,9 +183,8 @@ RDS PG / ES ──DataX──▶ RAW ──SparkSQL──▶ ODS ──▶ DWD 
 
 ## 7. 分区与存储策略
 
-- **分区字段**:`dt`(日期,必须);`hr`(小时,按需)
+- **分区字段**:`dt`(必须),`STRING` 类型,格式 `YYYYMMDD`(如 `20260101`);`hr`(小时,按需)
 - **存储格式**:ORC(列存)
-- **压缩**:`orc.compress=NONE`(咱不压缩,放弃一部分磁盘换 CPU/查询速度与 debug 友好度 ,后期做冷热数据时再考虑压缩)
 - **纠删码**:当前关闭(保持 3 副本),等 Worker 节点扩容后对冷数据启用
 
 ### 7.1 组合快照示例
@@ -198,12 +197,9 @@ RDS PG / ES ──DataX──▶ RAW ──SparkSQL──▶ ODS ──▶ DWD 
 
 ### 8.1 raw 层:schema-on-read landing
 
-- **全字段 STRING**:raw 层所有表的业务字段(不含 `dt` 分区)一律 `STRING` 类型,源库是什么类型、含不含空值、超不超长,raw 全部原样落盘
+- **全字段 STRING**:raw 层所有表的业务字段一律 `STRING` 类型
 - **同步任务不做类型转换**:DataX ini 里不写 `columnType` 的类型映射(或统一填 `string`),CSV 导入时 SparkSQL 读取后也不 `CAST`
-- **为什么**:
-  - 类型转换失败会中断同步链路,而 raw 是链路出入口,必须稳定
-  - 源库偶发脏数据(超长、空串、非预期类型)不会丢失,可追溯
-  - 同一份 raw 可以被 ods 重建多次(用不同清洗逻辑),而不需要回源库再同步
+- **外部表兜底**:raw 层建表一律用 `CREATE EXTERNAL TABLE`,DROP TABLE 只删元数据,HDFS 数据保留;raw 作为链路兜底层,误删元数据时数据仍可 `MSCK REPAIR` / 重建元数据恢复,无需回源库重同步
 
 ### 8.2 ods 层:类型转换与脏数据识别
 

+ 27 - 1
kb/90-重构路线.md

@@ -445,7 +445,7 @@ dfs.client.failover.proxy.provider.nameservice1 = org.apache.hadoop.hdfs.server.
 | `dw_base/datax/datasources/hdfs_data_source.py` | 覆写 `get_datasource_dict()`:父类逻辑外,检测 `[hadoop_config]` 节是否存在;存在则把整节作为 dict 塞进 `ds_dict['hadoopConfig']` |
 | `dw_base/datax/plugins/plugin.py` | **不用改** —— `load_data_source()` 的 `for key, value in ds_dict.items()` 里,value 是 dict 时走 Python 原生赋值,json 序列化自然成立 |
 | `dw_base/datax/plugins/reader/hdfs_reader.py` / `writer/hdfs_writer.py` | **不用改**,`defaultFS + hadoopConfig` 由 `load_data_source()` 自动注入到 `parameter` |
-| `dw_base/__init__.py` | **保留** `os.environ['HADOOP_CONF_DIR']` + `os.environ['HIVE_CONF_DIR']`(Spark on YARN 启动强校验 + Spark Hive metastore 定位;DataX JVM 不读,HA 靠 ini `[hadoop_config]`) |
+| `dw_base/__init__.py` | **保留** `os.environ['HADOOP_CONF_DIR']`(Spark on YARN 启动强校验)+ `os.environ.setdefault('SPARK_CONF_DIR', '/etc/spark/conf')`(pip pyspark 默认指向自身空 conf/,需显式指到集群配置才能加载 hive-site.xml);DataX JVM 不读这些,HA 靠 ini `[hadoop_config]`。详见 `01-运行环境.md §4` |
 | `bin/datax-gc-generator.py` | §2.7 从零重写时一并处理,这里不单独列 |
 | `datasource/hdfs/{env}/*.ini` | 按新 schema 新建(prod 带 `[hadoop_config]`;dev/test 只写 `[base] defaultFS`) |
 
@@ -539,6 +539,31 @@ record_per_channel = 100000
 
 **标准**:每个占位模块必须带 README,4 节(职责 / 接口 / 依赖 / 状态)—— 规则见 `kb/30-开发规范.md §4.5`。空 `__init__.py` + 无 README 直接删除,不保留"暂留"。
 
+### 2.12 通用 UDF 注释完整化 + 自查表(B 延伸) [聚簇 B]
+
+**现状**:`dw_base/udf/common/spark_common_udf.py` 500 行 40 函数由 6 份老文件合并而来(见 `kb/92` 2026-04-20 UDF 重组条目),函数签名齐全但注释粗细不一:部分函数只有函数名,部分 docstring 写了逻辑但没写入参约束 / 返回类型 / 异常路径 / SQL 调用示例。`common/` 目录 UDF 由 `bin/spark-sql-starter.py` 自动 `ADD FILE` 注册(启动日志 40 条 `注册 Python UDF xxx` 可证),理论上任意 SQL 都能用,但"能用"不等于"知道怎么用"。
+
+**问题**:
+- 注释不一致导致业务 SQL 开发时需要反复读源码猜参数语义,违背 common/ auto-load 的"开箱即用"初衷
+- 未来新增通用 UDF 如果没有登记表,规模大了之后"这个函数有没有 / 叫什么 / 谁加的"全靠 grep
+- 业务 UDF 进 common 的准入标准缺失,容易出现"为了省事放 common"与"只在一处用也塞 common"两类误伤
+
+**目标态**:
+
+1. **注释完整化**:40 函数全部补齐 docstring,统一 5 段模板 —— 一句话摘要 / 入参(名 · 类型 · 约束 / 可空 / 单位) / 返回(类型 · 语义 · 空值场景) / 异常与边界 / SQL 调用示例。按分类分批推进:JSON 段 → Array 段 → String 段 → Numeric-Date-Hash 段 → Cross-type 段,5 批独立 commit。
+
+2. **UDF 自查表**:新建 `kb/31-UDF 手册.md`(与 `30-开发规范.md` 同级独立文档;40 函数规模独立成文更稳,新增 UDF 需要稳定引用锚点)。表头 `函数名 | 分类 | 入参 | 返回 | 摘要 | 代码位置 | 补注释状态`。初版把现有 40 函数全量登记,后续新增通用 UDF(进 `common/`)必须同步登记此表;只进 `business/` 的 UDF 不走此表,在 `business/` 子目录 README 维护。
+
+3. **注册准入规则**:`kb/30-开发规范.md` 新增一节或 `CLAUDE.md` 一行硬约束 —— 向 `common/` 增删 UDF 前,先读 `kb/31` 自查表;新增必须同步更新表(函数名 / 分类 / 示例),否则 commit 不过。与 `tests/unit/udf/test_spark_common_udf.py`(阶段 4 首批单测目标,见 §2.11 占位 registry)是配套的:自查表为开发者服务,单测为回归服务。
+
+**回归检验**:
+- 任意 SQL 文件直接 `SELECT my_udf(col)` 能跑通(common auto-load 链路未变,保留现状)
+- 自查表行数 = `spark_common_udf.py` 中 `@udf` / `def` 导出函数数(启动日志中 `注册 Python UDF` 的条数即真值)
+
+**与其他条目的关系**:
+- 2026-04-20 UDF 模块重组(kb/92)已完成重组动作,本节是其延伸(补注释 + 登记表 + 准入规则)
+- 不动 auto-load 机制(`bin/spark-sql-starter.py` + `dw_base/__init__.py:29 COMMON_SPARK_UDF_FILE` 常量),只补文档与规则
+
 ## 三、`__init__.py` 瘦身(高优先级) [聚簇 B(B1)]
 
 **现状:** `tendata/__init__.py` 约 120 行,import 即执行以下操作:
@@ -671,6 +696,7 @@ tests/
 - LAZY 类依赖关联的老代码:`get_oldmongo_*` / `mg2es/` / `ent_interface_dingtalk*` 于 2026-04-20 第一批提前清理;同日第二批清理 `dw_base/oss/` 整目录、`dw_base/scheduler/` 整目录(含 polling_scheduler / drop_partitions / drop_daily_full_snapshot_tbls)、`dw_base/hive/` 整目录、`dw_base/utils/` 7 文件(data_distinct / diff_utils / excel_to_hive_utils / hive_diff_database / hive_to_excel_utils / pdt_check_table\*);剩余 `customs/similarity.py` 等在阶段 4 / 阶段 5 一并清理
 - 不需要 `requirements-base.txt` / `requirements-dev.txt` 分文件——当前依赖规模下单文件已经足够
 - pyspark==2.4.0 固定(对齐 CDH 6.3.2 parcel 自带版本):`pip install` 把 pyspark 装进解释器 site-packages 解决 PyCharm 远程解释器静态索引红线 + 本地 pytest;运行时 `findspark.init()` 指向集群 `$SPARK_HOME/python/`,两条链路同版本不冲突
+- **TODO 评估 `findspark` 移除**:`findspark` 原本解决的是 Spark Python 代码不在 `sys.path` 的问题(HDP 时代常见);CDH + pip 安装的 pyspark 情境下,解释器 site-packages 自带 pyspark 包已能直接 `import pyspark`,再配合 `SPARK_CONF_DIR=/etc/spark/conf`(`01-运行环境.md §4`)完成 spark-env.sh / hive-site.xml 加载,`findspark.init()` 的价值趋近于零。评估时机:当前阶段 1-3 以稳定为先,保留不拆;阶段 5 老残留清理时一并 drop 依赖 + `dw_base/__init__.py:9` 的 import 与 `:127` 的 `findspark.init()` 调用
 
 ### 7.2 日志改进
 

+ 6 - 0
kb/92-重构进度.md

@@ -172,4 +172,10 @@
 | 2026-04-20 | **删除 `geo_hash` UDF + `pygeohash` 依赖(重构计划外)**:`spark-sql-starter.py` 在新 CDH 环境启动时报 `ModuleNotFoundError: No module named 'pygeohash'`(`dw_base/udf/common/spark_common_udf.py:19 import pygeohash`)。根因:2026-04-20 UDF 重组(kb/92:165)把 `geo_hash(lat, lng, precision)` 保留进 `common/`(auto-load),但 `pygeohash` 在 kb/90 §7.1 还标 LAZY 且注释"只被即将清理的老业务代码引用"—— UDF 进 common 后该分类失效。当前阶段业务 SQL 尚未开始,`geo_hash` 零现成消费者;真需要 geo 分析再从老项目 copy + 放 `business/`。整改:(a) 删 `spark_common_udf.py:19 import pygeohash` + `:375-376 def geo_hash(...)`;(b) kb/90 §7.1 LAZY 行的 `pygeohash` 移除(仓库零引用,不需要 LAZY 档) | — |
 | 2026-04-20 | **kb/00 目录树同步真实状态 + §2/§3 收敛**:`dw_base/` 实际子目录已变(2026-04-20 删 `scheduler/` / `hive/`,新建占位 `io/` / `ops/` / `dq/` / `pm/` / `sync/`),kb/00 §1 目录树仍停留在旧态;§2 核心模块职责表 + §3 Mermaid 均与 §1 同一信息三视图重复。整改:(a) §1 目录树同步真实子目录,职责并入每行行尾注释;顺带补漏 `publish.sh` 已挪入 `bin/`(`6936460`)+ 新增 `tests/` 条目 + `conf/` 注释更新为"非敏感入库"口径;(b) §2 表删除,替换为一行"职责已并入 §1"指引;(c) §3 Mermaid 删除,保留标题 + 一行"待基础模块实装后重绘"说明。保留 §2/§3 heading 保证节号稳定,外部引用(kb/90 §4.3/§5/§6.4/§9、kb/30 §8/§9.6)零更新 | — |
 | 2026-04-20 | **恢复 `dw_base/__init__.py:16` 的 `HADOOP_CONF_DIR` export(反转 2026-04-18 第四轮决策的一小部分)**:`spark-sql-starter.py -f workspace/20260420/select_dim_calendar_dw2.sql` 在新 CDH 环境报 `When running with master 'yarn' either HADOOP_CONF_DIR or YARN_CONF_DIR must be set` —— Spark on YARN 启动时 `SparkSubmitArguments.validateSubmitArguments` 强校验该 env;bigdata 账号 shell 里未 export。2026-04-18 第四轮决策(`kb/92:158`)把这行当"死代码"删,判断只基于 DataX JVM(DataX 确实不靠它,HA 走 ini `[hadoop_config]` 注入),忽略了同一 `dw_base/__init__.py` 也是 Spark 入口的 bootstrap。本次恢复:`os.environ['HADOOP_CONF_DIR'] = '/etc/hadoop/conf'` 加回,注释更新为"Spark 需要;DataX 不读 classpath"。同步更新 `kb/90 §2.8` 末尾"死代码"段(重写为"仅对 Spark 生效")+ §2.8 代码改造清单附表对应行(从"删除"改为"保留",给出双路径理由)。DataX 侧结论(ini `[hadoop_config]` 注入 HA、`HADOOP_CONF_DIR` 对 DataX JVM 无效)不受影响 | — |
+| 2026-04-20 | **补 `SPARK_CONF_DIR` env 兜底 + 回退 `HIVE_CONF_DIR` 伪修复 + 删 HDP 残留注释**:昨日恢复 `HIVE_CONF_DIR` 后冒烟测试仍报 `Table or view not found`,说明 HIVE_CONF_DIR 不是真正触发点。对比老项目 `tendata-warehouse-release/tendata/__init__.py` 发现老代码同样把 `HIVE_CONF_DIR` 注释掉(反证此前上一条 changelog 的"伪修复"),且同样没有 `SPARK_CONF_DIR`。真正差异在环境:新 CDH 用 pip 安装的 pyspark 其 `SPARK_CONF_DIR` 默认指向自身空 `conf/`,不会加载集群 `/etc/spark/conf/hive-site.xml`,`.enableHiveSupport()` 静默回落 in-memory metastore。整改:(a) `dw_base/__init__.py` `os.environ.setdefault('SPARK_CONF_DIR', '/etc/spark/conf')`(用 setdefault 允许 shell 侧 export 覆盖),同时把 `HIVE_CONF_DIR` 重新注释掉,并删除历史遗留行 `# os.environ['SPARK_HOME'] = '/usr/hdp/3.1.5.0-152/spark2'`(过期 HDP 路径 + 本就是注释,无保留价值);多行 comment 合并为单行;(b) `kb/01-运行环境.md` 新增 §4 Spark 运行时环境变量(HADOOP_CONF_DIR / SPARK_CONF_DIR 表);(c) `kb/90-重构路线.md §2.8` 代码改造清单附表行回退 —— 从"保留 HADOOP_CONF_DIR + HIVE_CONF_DIR"改为"保留 HADOOP_CONF_DIR + 新增 SPARK_CONF_DIR.setdefault",附详情指向 `01 §4`;(d) `kb/90 §7.1` findspark 加 TODO:CDH + pip pyspark 环境下其功能可由 `SPARK_CONF_DIR + setdefault` 覆盖,evaluate 移除。教训:上一条 changelog(HIVE_CONF_DIR 恢复)是在没对比老项目的情况下拍板的,属于典型的"邻域扫但没找对根因";act-not-flatter memory 同步更新边界判定(同根因 yes/no) | — |
+| 2026-04-20 | **raw 层外部表规范 + "每日调度" 措辞收敛**:(a) `kb/20-数仓分层与建模.md §8.1` 新增 bullet:raw 层建表一律 `CREATE EXTERNAL TABLE`,DROP TABLE 只删元数据、HDFS 数据保留,raw 作为链路兜底层误删可恢复;(b) `kb/00-项目架构.md` 三处"每日调度"收敛为"调度执行":§9 目录说明 `jobs/` 用途行(L617)、§9 目录说明 jobs SQL 路径行(L623)、§9.2 标题(L703)—— 因为调度周期不一定是日(小时/周/月/ad-hoc 均可),"每日"措辞在通用性描述里不准确;同文档 L909 `ads_trd_gmv_d.sql` 注释 + `kb/23-标签体系.md` 两处 `tdm_*_ful_d` 场景描述保留"每日计算"(`_d` 后缀表明具体表就是日粒度)。**遗留冲突待拍**:`kb/00-项目架构.md §9.2` L829 的 raw 层 CTAS 样例(`CREATE TABLE raw.raw_trd_legacy_order_his_o ... AS SELECT`)是 managed table,与 §8.1 新规范的"raw 一律 EXTERNAL"冲突——`CREATE EXTERNAL TABLE ... AS SELECT` 在 Spark / Hive 语法上不支持。候选:① 样例拆两步(先 `CREATE EXTERNAL TABLE` + `LOCATION` 再 `INSERT OVERWRITE`)② §8.1 加 "CTAS 例外" 条款 ③ 删掉 §9.2 CTAS 路径只留 INSERT OVERWRITE 路径。待用户拍板 | — |
+| 2026-04-20 | **里程碑:DataX + Spark SQL 双入口在新 CDH 环境首次端到端冒烟跑通(tag `milestone/datax+spark-smoke-2026-04-20`)**:链路 DataX 导入 PG `dim_calendar` → Hive `test.dim_calendar_dw2`(`workspace/20260420/dim_calendar.ini`),Spark SQL 从 Hive 查回(`workspace/20260420/select_dim_calendar_dw2.sql`),端到端返回 10 行。新 CDH 环境适配前后共三处连锁修复:(a) `cf87744` 恢复 `HADOOP_CONF_DIR`(Spark on YARN 启动强校验);(b) `5b2569a` 删除 `geo_hash` UDF + `pygeohash` 依赖(common auto-load 链路无 fallback);(c) 本次 `SPARK_CONF_DIR=/etc/spark/conf` setdefault(pip pyspark 默认指向自身空 conf/,缺此项 `enableHiveSupport` 静默回落 in-memory metastore,看不到 HMS 真实库表)。tag 为 annotated tag 指向本次 commit,本地 + 远程均可见 | — |
+| 2026-04-20 | **存储格式约定反转:`orc.compress` 不显式写 `NONE`,走 ORC 默认(ZLIB)**:`workspace/20260420/dim_pub_calendar_ful_o.sql` 建表时写了 `TBLPROPERTIES ('orc.compress' = 'NONE')`,核查 ORC 默认压缩为 ZLIB,NONE 是显式关闭压缩(非 no-op)。前期 kb 三处口径"咱不压缩放弃磁盘换 CPU"属于过早决策,小表省 CPU 微乎其微、业务表规模上来后 ZLIB 更优,维护一套"显式写 NONE"的约束成本大于收益。改为走默认、建表不加 `TBLPROPERTIES`。文档同步三处:`kb/20-数仓分层与建模.md §7` 压缩行(`orc.compress=NONE` → 走 ORC 默认)、`kb/00-项目架构.md §9.1` 末尾存储格式约定行(删 `+ orc.compress=NONE`)、`kb/00-项目架构.md` raw 层 CTAS 样例(删 `OPTIONS ('orc.compress'='NONE')` 一行)。未在文档里加"为什么不显式写 NONE"的解释性注释(见用户反馈) | — |
+| 2026-04-20 | **kb/20 分区/raw STRING 口径收敛**:§7 分区字段补 `dt` 类型 `STRING` + 格式 `YYYYMMDD`(如 `20260101`);§8.1 "全字段 STRING" bullet 去掉 "(不含 `dt` 分区)"(dt 本身也是 STRING,括号让人误以为 dt 是另一种类型) | — |
+| 2026-04-20 | **kb/90 新增 §2.12 通用 UDF 注释完整化 + 自查表(聚簇 B 延伸)**:`dw_base/udf/common/spark_common_udf.py` 40 函数注释粗细不一,且当前 common/ auto-load 链路没有任何"新增 UDF 需要登记哪里"的准入规则。三档改造:(a) 40 函数 docstring 统一 5 段模板(摘要 / 入参 / 返回 / 异常与边界 / SQL 示例),按 JSON → Array → String → Numeric-Date-Hash → Cross-type 5 批分 commit;(b) 新建 `kb/31-UDF 手册.md`(与 `30-开发规范.md` 同级独立文档,方案 A 而非并入 30),表头 `函数名 / 分类 / 入参 / 返回 / 摘要 / 代码位置 / 补注释状态`,初版登记 40 函数全量,新增通用 UDF 进 common/ 时必须同步登记;business/ UDF 在自己的子目录 README 维护,不走此表;(c) `kb/30-开发规范.md` 或 `CLAUDE.md` 加硬约束"增删 common/ UDF 先读 kb/31 + 同步更新",与 `tests/unit/udf/test_spark_common_udf.py`(§2.11 占位 registry 登记的阶段 4 首批单测目标)配套(自查表为开发者服务,单测为回归服务)。本条是 2026-04-20 UDF 模块重组(本 changelog 之前记录的 UDF 6 文件合一 + business/common 分离)的延伸,不动 auto-load 机制,只补文档与规则 | — |
 | 2026-04-20 | **dw_base 占位模块骨架 + tests 骨架 + bin 收口(B4 提前 + C 起步)**:(a) 新建 5 个占位模块 `dw_base/io/{db,file,hdfs}/` + `dw_base/ops/` + `dw_base/pm/` + `dw_base/dq/` + `dw_base/sync/`,每个带 `__init__.py` + `README.md`(4 节:职责/接口/依赖/状态);实现留待后续阶段。(b) `tests/{unit,integration}/` 骨架 + `tests/README.md` + `.gitkeep`;首批单测目标 `tests/unit/udf/test_spark_common_udf.py`(40 函数)。(c) `bin/excel_to_hive.py` 删除(一次性工具,有需求重做);`publish.sh` 从项目根 `git mv` 到 `bin/publish.sh`(publish 是 DS 调度入口 = 和 bin 同类)。代码侧单次 commit `6936460`。(d) 文档侧同步:`kb/30-开发规范.md §4.5 占位模块规范`(4 节标准 + "空 __init__.py 无 README → 删"铁律);`kb/90-重构路线.md` 按聚簇 + DAG 重组(新增 §〇 全景与 DAG、§2.10 common/utils/io/ops 四模块律、§2.11 新占位 registry、§六.1 tests 骨架标注、§八 从 P0-P3 线性表替换为聚簇 A-F 推进视图;所有主章节加 `[聚簇 X]` 标签;§2.1 publish.sh 行改为 `bin/publish.sh`);本文档总览引入聚簇视图说明 + 阶段 1/2/4 状态改"推进中 / 部分提前完成" | — |