diff --git a/skills/hiui-design/CHANGELOG.md b/skills/hiui-design/CHANGELOG.md index e142228ce..bd17c10b1 100644 --- a/skills/hiui-design/CHANGELOG.md +++ b/skills/hiui-design/CHANGELOG.md @@ -10,51 +10,51 @@ - 当前版本:`1.0.0` - 如需在维护期自动刷新,可执行 `node scripts/sync-changelog.mjs --watch`。 -## 2026-07-04 +## 2026-07-07 - 发布 `1.0.0`,生成对外交付归档(user archive) `outputs/archives/hiui-design.zip`,并回写 `outputs/RELEASE_REPORT.md`。 - 发布前校验通过:`verify-typical-page-maintainer-regressions`、`sync-manifest-docs --check`、`sync-hiui-v5-manifest --check`、`validate-hiui-v5-knowledge --strict-coverage`、`sync-hiui-v5-quick-reference --check`、`sync-hiui-v5-component-map --check`、`check-rules-coverage`、`check-distribution-boundary --scope maintainer`、`build-skill-archive`。 -- 记录到同日文件变更:新增 4 个文件,更新 608 个文件。 +- 记录到同日文件变更:新增 56 个文件,更新 639 个文件。 - 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 - 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 - 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 -- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 30 个文件,`docs/onboarding/` 接入文档 14 个文件,`docs/generation/` 生成说明 41 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 178 个文件,`examples/` 示例工程 67 个文件,`reference/` 参考资产 54 个文件,`manifests/` 清单定义 4 个文件,`templates/` 模板资产 98 个文件,`vendor/` 分发快照 2 个文件。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 31 个文件,`docs/onboarding/` 接入文档 14 个文件,`docs/generation/` 生成说明 41 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 182 个文件,`examples/` 示例工程 67 个文件,`reference/` 参考资产 54 个文件,`manifests/` 清单定义 4 个文件,`templates/` 模板资产 98 个文件,`vendor/` 分发快照 2 个文件。 + +## 2026-07-06 + +- 版本快照:machine planning entry, fixture-backed public JSON contracts, runtime/preview/start contract fixtures, and global-sync-safe governance docs。 + +## 2026-07-05 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-07-04 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 ## 2026-07-03 -- 记录到同日文件变更:新增 24 个文件,更新 609 个文件,删除 17 个文件。 -- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 -- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 -- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 -- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 30 个文件,`docs/onboarding/` 接入文档 14 个文件,`docs/generation/` 生成说明 41 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 180 个文件,`examples/` 示例工程 67 个文件,`reference/` 参考资产 54 个文件,`manifests/` 清单定义 4 个文件,`templates/` 模板资产 98 个文件,`vendor/` 分发快照 2 个文件。 +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 ## 2026-07-02 -- 记录到同日文件变更:新增 20 个文件,更新 601 个文件。 -- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 -- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 -- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 -- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 28 个文件,`docs/onboarding/` 接入文档 13 个文件,`docs/generation/` 生成说明 36 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 169 个文件,`examples/` 示例工程 67 个文件,`reference/` 参考资产 54 个文件,`manifests/` 清单定义 4 个文件,`templates/` 模板资产 98 个文件,`vendor/` 分发快照 2 个文件。 +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 ## 2026-07-01 -- 记录到同日文件变更:新增 7 个文件,更新 582 个文件,删除 17 个文件。 -- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 -- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 -- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 -- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 28 个文件,`docs/onboarding/` 接入文档 13 个文件,`docs/generation/` 生成说明 36 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 165 个文件,`examples/` 示例工程 67 个文件,`reference/` 参考资产 54 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 98 个文件,`vendor/` 分发快照 2 个文件。 +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 ## 2026-06-30 -- 记录到同日文件变更:新增 9 个文件,更新 592 个文件,删除 3 个文件。 -- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 -- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 -- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 -- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 28 个文件,`docs/onboarding/` 接入文档 13 个文件,`docs/generation/` 生成说明 36 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 160 个文件,`examples/` 示例工程 67 个文件,`reference/` 参考资产 54 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 98 个文件,`vendor/` 分发快照 2 个文件。 +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 ## 2026-06-29 -- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 +- 记录到同日文件变更:新增 20 个文件,更新 568 个文件,删除 4 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 27 个文件,`docs/onboarding/` 接入文档 13 个文件,`docs/generation/` 生成说明 36 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 156 个文件,`examples/` 示例工程 67 个文件,`reference/` 参考资产 54 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 98 个文件,`vendor/` 分发快照 2 个文件。 ## 2026-06-28 @@ -62,7 +62,11 @@ ## 2026-06-27 -- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 +- 记录到同日文件变更:新增 11 个文件,更新 559 个文件,删除 21 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 26 个文件,`docs/onboarding/` 接入文档 12 个文件,`docs/generation/` 生成说明 36 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 149 个文件,`examples/` 示例工程 67 个文件,`reference/` 参考资产 54 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 111 个文件,`vendor/` 分发快照 2 个文件。 ## 2026-06-26 @@ -70,19 +74,34 @@ ## 2026-06-25 -- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 +- 记录到同日文件变更:新增 8 个文件,更新 580 个文件,删除 8 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 30 个文件,`docs/onboarding/` 接入文档 12 个文件,`docs/generation/` 生成说明 39 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 148 个文件,`examples/` 示例工程 67 个文件,`reference/` 参考资产 54 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 111 个文件,`vendor/` 分发快照 2 个文件。 ## 2026-06-24 -- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 +- 记录到同日文件变更:新增 35 个文件,更新 580 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 26 个文件,`docs/onboarding/` 接入文档 12 个文件,`docs/generation/` 生成说明 36 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 148 个文件,`examples/` 示例工程 67 个文件,`reference/` 参考资产 54 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 111 个文件,`vendor/` 分发快照 2 个文件。 ## 2026-06-23 -- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 +- 记录到同日文件变更:新增 9 个文件,更新 504 个文件,删除 3 个文件。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 15 个文件,`docs/onboarding/` 接入文档 10 个文件,`docs/generation/` 生成说明 34 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 113 个文件,`examples/` 示例工程 66 个文件,`reference/` 参考资产 53 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 110 个文件,`vendor/` 分发快照 2 个文件。 ## 2026-06-22 -- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 +- 记录到同日文件变更:新增 29 个文件,更新 539 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 15 个文件,`docs/onboarding/` 接入文档 11 个文件,`docs/generation/` 生成说明 34 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 132 个文件,`examples/` 示例工程 66 个文件,`reference/` 参考资产 53 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 110 个文件,`vendor/` 分发快照 2 个文件。 ## 2026-06-21 @@ -102,15 +121,27 @@ ## 2026-06-17 -- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 +- 记录到同日文件变更:新增 25 个文件,更新 509 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 11 个文件,`docs/onboarding/` 接入文档 11 个文件,`docs/generation/` 生成说明 34 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 107 个文件,`examples/` 示例工程 66 个文件,`reference/` 参考资产 53 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 110 个文件,`vendor/` 分发快照 2 个文件。 ## 2026-06-16 -- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 +- 记录到同日文件变更:更新 485 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 7 个文件,`docs/onboarding/` 接入文档 11 个文件,`docs/generation/` 生成说明 33 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 95 个文件,`examples/` 示例工程 66 个文件,`reference/` 参考资产 53 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 110 个文件,`vendor/` 分发快照 2 个文件。 ## 2026-06-15 -- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 +- 记录到同日文件变更:更新 485 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 7 个文件,`docs/onboarding/` 接入文档 11 个文件,`docs/generation/` 生成说明 33 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 95 个文件,`examples/` 示例工程 66 个文件,`reference/` 参考资产 53 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 110 个文件,`vendor/` 分发快照 2 个文件。 ## 2026-06-14 @@ -126,19 +157,176 @@ ## 2026-06-11 -- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 +- 记录到同日文件变更:新增 6 个文件,更新 485 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 7 个文件,`docs/onboarding/` 接入文档 11 个文件,`docs/generation/` 生成说明 33 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 95 个文件,`examples/` 示例工程 66 个文件,`reference/` 参考资产 53 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 110 个文件,`vendor/` 分发快照 2 个文件。 ## 2026-06-10 -- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 +- 记录到同日文件变更:新增 3 个文件,更新 479 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 7 个文件,`docs/onboarding/` 接入文档 11 个文件,`docs/generation/` 生成说明 33 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 89 个文件,`examples/` 示例工程 66 个文件,`reference/` 参考资产 53 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 110 个文件,`vendor/` 分发快照 2 个文件。 ## 2026-06-09 -- 版本快照:machine planning entry, fixture-backed public JSON contracts, runtime/preview/start contract fixtures, and global-sync-safe governance docs。 +- 记录到同日文件变更:新增 14 个文件,更新 477 个文件,删除 49 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 8 个文件,`docs/onboarding/` 接入文档 11 个文件,`docs/generation/` 生成说明 34 个文件,`docs/business-lines/` 业务线文档 23 个文件,`scripts/` 执行脚本 128 个文件,`examples/` 示例工程 66 个文件,`reference/` 参考资产 53 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 110 个文件,`vendor/` 分发快照 3 个文件。 + +## 2026-06-08 + +- 记录到同日文件变更:新增 49 个文件,更新 463 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 8 个文件,`docs/onboarding/` 接入文档 11 个文件,`docs/generation/` 生成说明 34 个文件,`docs/business-lines/` 业务线文档 23 个文件,`scripts/` 执行脚本 120 个文件,`examples/` 示例工程 64 个文件,`reference/` 参考资产 51 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 110 个文件,`vendor/` 分发快照 2 个文件。 + +## 2026-06-07 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-06-06 + +- 记录到同日文件变更:新增 11 个文件,更新 462 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 7 个文件,`docs/onboarding/` 接入文档 11 个文件,`docs/generation/` 生成说明 33 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 76 个文件,`examples/` 示例工程 64 个文件,`reference/` 参考资产 51 个文件,`manifests/` 清单定义 3 个文件,`templates/` 模板资产 110 个文件,`vendor/` 分发快照 2 个文件。 + +## 2026-06-05 + +- 记录到同日文件变更:新增 1 个文件,更新 451 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 7 个文件,`docs/onboarding/` 接入文档 11 个文件,`docs/generation/` 生成说明 31 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 69 个文件,`examples/` 示例工程 64 个文件,`reference/` 参考资产 51 个文件,`manifests/` 清单定义 1 个文件,`templates/` 模板资产 110 个文件,`vendor/` 分发快照 2 个文件。 + +## 2026-06-04 + +- 记录到同日文件变更:更新 450 个文件,删除 1 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 7 个文件,`docs/onboarding/` 接入文档 11 个文件,`docs/generation/` 生成说明 31 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 68 个文件,`examples/` 示例工程 64 个文件,`reference/` 参考资产 51 个文件,`manifests/` 清单定义 1 个文件,`templates/` 模板资产 110 个文件,`vendor/` 分发快照 2 个文件。 + +## 2026-06-03 + +- 记录到同日文件变更:新增 5 个文件,更新 451 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 7 个文件,`docs/onboarding/` 接入文档 11 个文件,`docs/generation/` 生成说明 31 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 68 个文件,`examples/` 示例工程 64 个文件,`reference/` 参考资产 51 个文件,`manifests/` 清单定义 1 个文件,`templates/` 模板资产 110 个文件,`vendor/` 分发快照 2 个文件。 + +## 2026-06-02 + +- 记录到同日文件变更:新增 7 个文件,更新 50 个文件,删除 1 个文件。 +- 观察到同日规则资产变更:`agents/` agent 配置 1 个文件,`rules/` 规则层 4 个文件,`docs/onboarding/` 接入文档 1 个文件,`docs/generation/` 生成说明 6 个文件,`scripts/` 执行脚本 33 个文件,`examples/` 示例工程 1 个文件,`reference/` 参考资产 1 个文件,`templates/` 模板资产 9 个文件,`SKILL.md` skill 入口 1 个文件。 + +## 2026-06-01 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-05-31 + +- 记录到同日文件变更:新增 17 个文件,更新 20 个文件。 +- 观察到同日规则资产变更:`agents/` agent 配置 1 个文件,`rules/` 规则层 3 个文件,`docs/onboarding/` 接入文档 1 个文件,`docs/generation/` 生成说明 3 个文件,`scripts/` 执行脚本 19 个文件。 + +## 2026-05-30 + +- 记录到同日文件变更:更新 6 个文件。 +- 观察到同日规则资产变更:`rules/` 规则层 2 个文件,`docs/generation/` 生成说明 4 个文件。 + +## 2026-05-29 + +- 记录到同日文件变更:新增 1 个文件,更新 25 个文件。 +- 观察到同日规则资产变更:`rules/` 规则层 5 个文件,`docs/generation/` 生成说明 7 个文件,`scripts/` 执行脚本 7 个文件,`examples/` 示例工程 2 个文件,`reference/` 参考资产 2 个文件,`templates/` 模板资产 3 个文件。 + +## 2026-05-28 + +- 记录到同日文件变更:新增 1 个文件,更新 19 个文件,删除 10 个文件。 +- 观察到同日规则资产变更:`rules/` 规则层 1 个文件,`docs/onboarding/` 接入文档 3 个文件,`docs/generation/` 生成说明 4 个文件,`scripts/` 执行脚本 17 个文件。 + +## 2026-05-27 + +- 记录到同日文件变更:新增 56 个文件,更新 85 个文件,删除 4 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`rules/` 规则层 7 个文件,`docs/onboarding/` 接入文档 4 个文件,`docs/generation/` 生成说明 17 个文件,`scripts/` 执行脚本 36 个文件,`examples/` 示例工程 9 个文件,`reference/` 参考资产 10 个文件,`templates/` 模板资产 42 个文件。 + +## 2026-05-26 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-05-25 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-05-24 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-05-23 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-05-22 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-05-21 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-05-20 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-05-19 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-05-18 + +- 记录到同日文件变更:新增 2 个文件,更新 29 个文件,删除 4 个文件。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`docs/onboarding/` 接入文档 2 个文件,`docs/generation/` 生成说明 4 个文件,`scripts/` 执行脚本 7 个文件,`examples/` 示例工程 5 个文件,`reference/` 参考资产 6 个文件,`templates/` 模板资产 6 个文件。 + +## 2026-05-17 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-05-16 + +- 记录到同日文件变更:新增 26 个文件,更新 49 个文件。 +- 刷新发布链路:`release-skill-archive`、`sync-changelog` 与 `sync-feishu-release-doc` 的维护入口在同日被一起调整。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`rules/` 规则层 9 个文件,`docs/onboarding/` 接入文档 3 个文件,`docs/generation/` 生成说明 15 个文件,`scripts/` 执行脚本 17 个文件,`templates/` 模板资产 22 个文件。 + +## 2026-05-15 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-05-14 + +- 暂无可追溯修改记录(无 Git 提交、每日历史增量或可见文件时间戳)。 + +## 2026-05-13 + +- 记录到同日文件变更:新增 382 个文件,删除 1 个文件。 +- 补充默认接入规则:外部新项目生成的页面一律视为业务页面,不再默认并入 `hiui-design` 官方示例、gallery、smoke 或 host-integration 路由。 +- 移除 `三包查询` / `WarrantyQueryPage` 相关示例页、参考页及宿主示例路由入口,避免外部项目接入时被误当成默认示例页面。 +- 观察到同日规则资产变更:`archetypes/` 页壳画像 56 个文件,`agents/` agent 配置 1 个文件,`rules/` 规则层 7 个文件,`docs/onboarding/` 接入文档 10 个文件,`docs/generation/` 生成说明 28 个文件,`docs/business-lines/` 业务线文档 20 个文件,`scripts/` 执行脚本 43 个文件,`examples/` 示例工程 61 个文件,`reference/` 参考资产 46 个文件,`manifests/` 清单定义 1 个文件,`templates/` 模板资产 79 个文件,`vendor/` 分发快照 2 个文件。 ## 追溯来源 -- 版本文件:`rules/VERSION`(last-updated: 2026-06-09) -- 发布报告:`outputs/RELEASE_REPORT.md`(released-at: 2026-07-04T13:16:20.136Z) +- 版本文件:`rules/VERSION`(last-updated: 2026-07-06) +- 发布报告:`outputs/RELEASE_REPORT.md`(released-at: 2026-07-07T12:21:26.291Z) - 每日历史:`outputs/CHANGELOG_HISTORY.json`(按天合并 Git / mtime 推导出的新增 / 更新 / 删除路径) - 快照文件:`outputs/CHANGELOG_SNAPSHOT.json` diff --git a/skills/hiui-design/GENERATED_DO_NOT_EDIT.md b/skills/hiui-design/GENERATED_DO_NOT_EDIT.md index 66c0dfcea..c67b8e797 100644 --- a/skills/hiui-design/GENERATED_DO_NOT_EDIT.md +++ b/skills/hiui-design/GENERATED_DO_NOT_EDIT.md @@ -4,6 +4,6 @@ This directory is generated from the hiui-design maintainer source by `scripts/b - target: open-source-package - manifestVersion: 1 -- files: 515 +- files: 495 Make changes in the maintainer source, then regenerate this target. diff --git a/skills/hiui-design/README.md b/skills/hiui-design/README.md index fdaa25218..7491e2d3c 100644 --- a/skills/hiui-design/README.md +++ b/skills/hiui-design/README.md @@ -23,4 +23,4 @@ node /scripts/sync-open-source-package.mjs \ The sync is one-way. Edit the internal source skill, then regenerate this public folder. Generated by: `scripts/sync-open-source-package.mjs` -Copied files: 516 +Copied files: 496 diff --git a/skills/hiui-design/SKILL.md b/skills/hiui-design/SKILL.md index e6bac62bd..678ba90e6 100644 --- a/skills/hiui-design/SKILL.md +++ b/skills/hiui-design/SKILL.md @@ -119,6 +119,13 @@ description: >- `componentSupportSources` 都能解析到真实项目源码时才成立;缺任一项时,接入必须保持 `integrationReady=false`,页面生成阶段只允许暴露阻断原因,不得把缺失 carrier 问题延后到 业务页实现期才发现。 +- 对 `legacy-host-compatible`,`integrationReady=true` 还要求项目级 required rollout 已完成: + 默认先覆盖 `table-basic`、`table-stat`、`tree-table`、`tree-split`、`drawer-form`、 + `drawer-detail`、`full-page-edit`、`full-page-detail` 这些 `carrier-first-required` + 页型;`feedback-status` 与 `data-visualization` 可作为 deferred batch 后补。缺少 required + batch 的 project-certified carrier 时,integration state 必须显式输出 + `requiredLegacyPageTypes`、`deferredLegacyPageTypes`、`certifiedLegacyPageTypes`、 + `missingRequiredLegacyPageTypes` 与 `legacyRolloutCoverageStatus=blocked`。 - `integrationReady` 默认只回答“项目是否已完成 hiui-design 接入”和“legacy 宿主桥接是否已具备项目级承接事实”。`rules-only` / `host-integration` 下标准典型页组件是否可用,属于 planner 消费的资产事实与页型支持事实,不应被收口为通用项目接入失败。 - project-scoped host pack / carrier facts 也属于 runtime input facts。对 legacy 项目,应优先 在项目接入 / capabilities 阶段一次认证,再由页面生成阶段复用,不要在每个页面重复解释宿主边界。 @@ -143,6 +150,9 @@ description: >- 直接回显接入阻断原因;不要再尝试把页面生成、起手 scaffold 或页面级 preflight 当成接入 / legacy bridge 完整性的补救路径。页型级 `page-component` 资产是否 ready,继续由 planner 的 `assetResolution` / `projectTypicalPageSupport` 独立判断,而不是反写成通用 integration debt。 +- 当 `integrationReady=false` 的原因来自 legacy required rollout 未完成时,`PlanTask` 默认应把 + `bootstrap-target-project` 放在 `ResolveBlockingFacts` 队首;若同时命中 route ownership + blocker,再在其后追加 route 修复动作,不得跳过项目级 carrier rollout 直接进入页面实现。 - `BuildGenerationRecipe`:把 `generationStrategy` 转化为标准装配协议。 - `GenerateByRecipe`:生成行为必须遵循 `assemblyOrder`、`requiredAssets`、`forbiddenMoves`;不允许自由发明页壳、region owner、slot 顺序。 - `InlineConformanceChecks`:关键装配步骤后的轻量一致性检查,不把重验收前移。 diff --git a/skills/hiui-design/distribution-manifest.json b/skills/hiui-design/distribution-manifest.json index 25eceadde..d35fdb272 100644 --- a/skills/hiui-design/distribution-manifest.json +++ b/skills/hiui-design/distribution-manifest.json @@ -185,7 +185,7 @@ "scripts/README.md", "scripts/lib/**", "scripts/public-cli-contracts.json", - "scripts/tests/fixtures/public-cli/*.json", + "scripts/tests/fixtures/public-cli/preview-ready.quality-pass.json", "templates/**", "vendor/typical-page-shells-package.json", "vendor/hiui-design-typical-page-shells-*.tgz" @@ -252,6 +252,8 @@ "scripts/lib/stats-config.mjs", "scripts/lib/stats-identity.mjs", "scripts/tests/*.test.mjs", + "scripts/tests/fixtures/public-cli/plan-page-task*.json", + "scripts/tests/fixtures/public-cli/final-report*.json", "scripts/tests/fixtures/public-cli/*usage*.json", "src/typical-page-reuse/DOCTOR_REPORT.md", "src/typical-page-reuse/HOST_ADAPTER_SNIPPET.md", @@ -264,7 +266,6 @@ "distribution-manifest.json", "docs/onboarding/public-runtime-release-checklist.md", "rules/runtime-delivery-policy.json", - "scripts/tests/fixtures/public-cli", "vendor/typical-page-shells-package.json", "scripts/typical-page-preview-ready.mjs", "scripts/tests/fixtures/public-cli/preview-ready.quality-pass.json" diff --git a/skills/hiui-design/docs/generation/component-certification.md b/skills/hiui-design/docs/generation/component-certification.md index 3bb6b962e..b7d8a2b7e 100644 --- a/skills/hiui-design/docs/generation/component-certification.md +++ b/skills/hiui-design/docs/generation/component-certification.md @@ -54,9 +54,14 @@ ### Legacy runtime adapter 额外检查 - `CleanContentMount` 证明旧宿主只提供干净内容挂载点,不重复提供页头、白底主体、分页 owner 或页面主滚动。 +- `CleanContentMount` 还必须证明 legacy 宿主或业务页本地 wrapper 没有重新接管 `white-body`、 + `main-scroll` 或 `pagination` 的几何责任;若出现第二层 page surface / query shell / + sticky pagination shell,应视为 owner drift。 - `RuntimeAdapter` 证明路由、权限、请求、字典、主题和用户能力通过显式 props / provider 注入,而不是页面组件读取项目全局变量。 -- `RuntimeBridge` 证明 request、auth、permission、user、dictionary、route-navigation、theme 可用且来源可追踪。 -- `StyleBoundary` 证明旧宿主全局 CSS 不会破坏页面组件的表格、分页、表单、抽屉、状态标签和间距。 +- `RuntimeAdapter` 不得重做 `QueryFilter` carrier、`pagination` carrier 或 `bodyTopNavigation` + carrier;这些区域只能继续由 selected page component / project-certified carrier 承接。 +- `RuntimeBridge` 证明 request、auth、permission、user、route-navigation、theme 可用且来源可追踪;`dictionary / i18n` 只在项目明确要求国际化时作为可选桥接能力补证。 +- `StyleBoundary` 证明旧宿主全局 CSS 不会破坏页面组件的表格、分页、表单、抽屉、状态标签和间距;对 legacy 表格类页型,还应覆盖搜索输入灰底、Tabs 落位、排序图标间距与分页吸底等高频几何污染风险。 - `PortalBoundary` 证明下拉、气泡、弹窗、抽屉等浮层挂载位置和层级不会被裁切;若宿主未显式改写浮层容器,则允许沿用组件库默认挂载能力。 - `AdapterRegistry` 证明 adapter 只做 runtime bridge,禁止 `translate-hiui-components-to-legacy-components`、`replace-query-filter-with-legacy-form`、`replace-managed-table-with-legacy-table`、`reimplement-pagination-region` 或 `wrap-typical-page-as-business-page-component`。 diff --git a/skills/hiui-design/docs/generation/implementation-checklist-template.md b/skills/hiui-design/docs/generation/implementation-checklist-template.md index 0623ee10a..f1ef8161c 100644 --- a/skills/hiui-design/docs/generation/implementation-checklist-template.md +++ b/skills/hiui-design/docs/generation/implementation-checklist-template.md @@ -162,6 +162,7 @@ - 若需求只说“提供状态标签筛选”但未指定默认激活项,是否保持初始数据全集可见,而不是默认选中第一项造成隐式预过滤 - 右侧详情表格区的局部 inset 是否明确落在真实内容 owner 上,而不是直接贴边 - 页级动作是否已经进入 `PageHeader extra` +- header slot 与 `PageHeader root` 是否共同证明了 `width: 100% + min-width: 0`,从而让 `PageHeader extra` 真正贴右,而不是仍然紧挨标题流动 - 右侧 pane header 是否只保留上下文信息,而不是承接页面主按钮 记录模板: @@ -175,6 +176,7 @@ - default filter state: - right detail/table inset owner: - page-level actions owner: + - page-header stretch owner: - pane header scope: ``` diff --git a/skills/hiui-design/docs/generation/legacy-host-compatibility.md b/skills/hiui-design/docs/generation/legacy-host-compatibility.md index c9260f17e..845fbd44e 100644 --- a/skills/hiui-design/docs/generation/legacy-host-compatibility.md +++ b/skills/hiui-design/docs/generation/legacy-host-compatibility.md @@ -150,6 +150,13 @@ Don't: 这里的“优先使用 standard 页面组件实现”默认解释为 `page-component + runtime bridge + slot fill`。桥接模式下,legacy 主树若不能 ad hoc direct import standard shell,只能阻止 direct shell mount;它不能单独推翻 planner 已判定 ready 的页面组件主链,也不能成为默认转去 reference/fallback 或兼容手拼页的理由。 +对旧系统中已经存在的业务页,bridge 快速路径还应默认优先下面两类实现动作: + +1. `slot-fill-only`:当前页已经是 ready 的 managed instance,且本次需求没有触碰 `pageType`、`shell`、`ownership`、mandatory components 或 runtime carrier,只替换业务槽位。 +2. `rewrite-by-page-component`:当前页仍属于同一典型页型,planner 已给出 certified page component / project carrier,但现有源码不是 ready 的 managed instance。此时应直接用 page component / carrier 重建业务页实例,而不是在旧 JSX 上继续修补 `header / query-filter / table / pagination / white-body`。 + +`rewrite-by-page-component` 仍然属于 bridge 模式下的普通典型页主链;它不是 fallback,也不是 translated-reference。 + 数据可视化不属于普通典型页快速路径。即使存在示例页,也必须走 `generationStrategy=managed-analytics`:先提取旧系统中的业务指标、筛选维度、接口和权限事实,再建立 `chartUsageContract`,最后由数据可视化 token 与图表组件规则生成页面实例。 桥接快速路径的起点优先级: @@ -184,6 +191,11 @@ legacy 快速路径不是全量重流程,而是按接入层 / 生成层 / 治 2. **生成层主链路门禁**:普通典型页默认仍以 certified `pageComponent` 为主资产;只有 component 不可用、结构升级或宿主约束特殊时,才允许进入 host archetype / reference / scaffold fallback。`component-first` 只说明生成起点,不豁免 instance validation,也不把治理要求自动降到 `rules-only`。 3. **治理层增强门禁**:只有命中转译风险、页型迁移或正式验收条件时,才显式展开 Translation Drift Guard,并按需要补齐 finalize / source gate / doctor / runtime smoke。`StyleBoundary`、`PortalBoundary`、`runtimeSmoke` 默认属于这一层的触发式检查,而不是普通表格页的必阻断项。 +这里要明确区分两类升级: + +- `implementation upgrade`:只有 `pageType`、`shell`、`ownership`、split、主工作区滚动链或 mandatory components 发生变化时,才升级成重结构改造。 +- `governance upgrade`:迁移 / 重架构页、高风险 drift、正式验收 / 发布场景,会升级 snapshot、acceptance contract、translation map、source-gate、doctor、runtime smoke 等治理动作,但不必然要求页面结构重写。 + 若 planner 已返回 `page-component` ready、`deliveryPath=page-component-plus-slot-fill` 且 `runtimeAdapterProof.status=available`,生成阶段必须沿该主链继续;此时 direct shell import 不成立、legacy 主树不能标准壳直挂、或 reference 目录存在,都不构成擅自降级为 hand-built compatibility page 的依据。 legacy 的最小 runtime adapter proof 必须能写清: diff --git a/skills/hiui-design/docs/generation/non-typical-pages.md b/skills/hiui-design/docs/generation/non-typical-pages.md index 3c33f1d13..9194334e8 100644 --- a/skills/hiui-design/docs/generation/non-typical-pages.md +++ b/skills/hiui-design/docs/generation/non-typical-pages.md @@ -81,6 +81,12 @@ - free composition is allowed only inside a HiUI-first, spec-constrained frame - 组件优先级、视觉 token、spacing ownership、禁止大段空白、runtime shell requirements 仍然是硬门槛 +旧系统升级场景里,这里的“保留结构”要收窄理解: + +- 保留的是 `base archetype`、`layout strategy`、一级信息架构与业务语义。 +- 不保留旧视觉、旧 DOM 壳层、旧样式类名、旧 spacing owner 或未受管的布局实现。 +- 非典型升级默认是“preserve layout strategy and replace semantics”,不是“沿用旧页面实现后只换 token / 换皮肤”。 + ## 策略驱动生成门槛 非典型页面必须先证明“为什么不能直接套典型页”,再进入 JSX / 样式实现。这里的证明不是固定某个区块组合,而是把页面的一层信息组织策略落成机器可追踪事实。 @@ -93,6 +99,8 @@ - `composition guardrails`:说明允许自由组合的槽位、禁止替换的 carrier、禁止重写的 HiUI 骨架。 - `strategy evidence`:说明源码和运行时需要出现哪些布局 marker / 语义组件 / 一级分组来证明策略已兑现。 +若旧系统升级任务已经给出稳定的 `layout strategy` 与 `layout archetype`,但没有 `pageType` / `shell` / `ownership` 级结构变化,默认升级的是语义实现与治理约束,而不是重写整页结构。 + 阻断规则: - 用户明确要求非典型,但计划无法给出 `layout strategy` 与 `layout archetype` 时,必须 `status=blocked` 补业务意图,不能降级为自由拼装。 diff --git a/skills/hiui-design/docs/generation/page-level-components.md b/skills/hiui-design/docs/generation/page-level-components.md index f64671c99..7e1d3b87b 100644 --- a/skills/hiui-design/docs/generation/page-level-components.md +++ b/skills/hiui-design/docs/generation/page-level-components.md @@ -40,10 +40,14 @@ HiUI Typical Standard -> PageType -> ManagedMold -> Adapter/Carrier Proof -> Cer `rules-only` 不是“无组件模式”。它与 `host-integration` 的主要区别是接入方式:`rules-only` 不把 host 示例页作为项目运行入口,不安装示例 gallery;但只要组件在 `supportedModes` 中声明支持 `rules-only`,并且认证 artifact 与组件的 canonical `mode` / `baseMoldId` 匹配,`plan-page-task` 就可以把它作为主生成资产。只有宿主约束要求额外 adapter 或 legacy 兼容时,才需要单独的 host-adapted / legacy-compatible component。 +对 `rules-only` / `host-integration` 而言,标准页面级组件是否 ready 属于 planner 的资产解析结果,不属于项目接入 readiness。接入阶段负责证明项目具备运行时 / 依赖 / 宿主前提;页面阶段再判断当前页型是否能命中 `page-component + slot-fill`。 + `legacy-host-compatible` 也不是“无组件模式”。它的人类解释统一为“旧宿主桥接接入模式”。这里的 `compatible` 只表示宿主边界与运行时契约可被桥接和证明,不表示任意旧宿主天然兼容,也不表示只要能挂载就已经进入受管生成 / 交付状态。它与 `rules-only` 的区别是旧宿主运行时、全局样式、portal、路由和权限接法不可直接等同于标准宿主;因此 legacy 的主路径应优先选择 **project-certified carrier implementation of page-component**。只有当宿主已经证明可直接等价承载 standard component 的运行时契约时,才直接使用 standard certified page component。旧宿主只保留全局导航、左侧菜单、路由入口和干净内容挂载点;组件内部的页头、筛选、白底主体、表格 / 表单 / 详情、分页 / 底栏和主滚动链不再拆给宿主逐项承接。这里的宿主边界证明应在项目接入 / capabilities 阶段完成,页面生成阶段只消费该证明结果;`legacy-host-family-registry.json` 中的 family `status` 是注册表生命周期标签,不是业务页生成期的硬门禁。 `legacy-runtime-adapter` 是运行时转接证明,不是组件翻译器。它只能绑定 request / response / message / i18n / permission / modal / scroll / style 等运行时能力,不得把 `QueryFilter` 翻译成旧 `SearchForm`、把受管表格翻译成旧表格、重做分页区域,或把典型页整体包装成业务页面级组件。`portal-root` 默认视为浮层运行时的常规能力,不进入 runtime bridge 的硬缺口判断;只有宿主显式改写浮层容器或出现裁切风险时,才额外进入 `PortalBoundary` 事实。`host archetype` 与 `reference-or-scaffold` 是 fallback 起点;`translation-map` 是治理增强工件。只有 project-certified carrier 缺失、宿主约束特殊、direct standard component 不可直挂,或 drift 风险需要显式治理时,计划才应进入这些路径。 +legacy 项目默认不要求在“项目接入完成”时覆盖所有典型页型,但应先完成一批 `carrier-first-required` 的 project rollout:`table-basic`、`table-stat`、`tree-table`、`tree-split`、`drawer-form`、`drawer-detail`、`full-page-edit`、`full-page-detail`。`feedback-status` 与 `data-visualization` 可以延后,但它们不应反向稀释这批表格 / 编辑 / 详情页的 carrier-first onboarding 事实。 + ## Project-Scoped Carrier Overlay 对 legacy 项目,推荐通过 project-scoped overlay 提供 carrier 资产,而不是把项目自己的 carrier 直接登记成 skill 全局 generic 组件名。 @@ -56,6 +60,7 @@ HiUI Typical Standard -> PageType -> ManagedMold -> Adapter/Carrier Proof -> Cer 3. `host-archetype` 4. `reference-or-scaffold` 5. explicit fallback / managed translation +- project 级 integration facts 应显式记录 `requiredLegacyPageTypes`、`deferredLegacyPageTypes`、`certifiedLegacyPageTypes`、`missingRequiredLegacyPageTypes` 与 `legacyRolloutCoverageStatus`。只要 required batch 仍缺 carrier,`plan-page-task` 就必须回到 `bootstrap-target-project` / `ResolveBlockingFacts`,而不是继续给页面实现命令。 - project overlay 只解决项目级承载差异,不改变 `baseMoldId`、slot 边界、required regions、mandatory components 和 page-instance validation。 ## Runtime-Bridged Page Components @@ -74,6 +79,15 @@ HiUI Typical Standard -> PageType -> ManagedMold -> Adapter/Carrier Proof -> Cer - bridge wrapper 与 slot adapter 必须保持 thin:前者只绑定宿主 runtime / mount boundary, 后者只做业务槽位适配;两者都不得接管 `shell`、`white-body`、`main-scroll`、 `pagination`、`footer` 或 `route owner`。 +- 对表格类 `page-component`,`QueryFilter`、`Table` 与 `pagination` 都属于 carrier 内部语义; + 业务页 / bridge slot adapter 只允许填 `queryFields`、表格列、行操作和 Level 1 受控扩展, + 不允许再额外包一层外部样式容器、自由筛选栏,或把 `QueryFilter` 翻译成宿主 `SearchForm`。 +- 对 legacy 表格类 bridge,业务页、本地 wrapper 与 slot adapter 还不得再合成第二层 + `white-body shell`、`main-scroll shell`、`pagination shell` 或 `query shell`。这些几何责任必须 + 保持在 selected certified page component 或 project-certified carrier 内部。 +- 若页面需要 `bodyTopNavigation`、筛选区前提示条或结果工具条,这些能力只能来自 page component + 已声明的标准 slot / Level 1 受控扩展;legacy bridge 不得通过外层 wrapper 把它们提升成新的 + page-level carrier,也不得把主体导航回流到 header region。 - 这类 bridge 规则的唯一真相是 `rules/runtime-bridged-component-matrix.json`;它只补充 `page-component` 在 legacy 中的交付方式,不复制 `page-component-registry`、mold registry 或 component certification 已经表达的事实。 @@ -91,6 +105,8 @@ HiUI Typical Standard -> PageType -> ManagedMold -> Adapter/Carrier Proof -> Cer 不满足时应 fail closed 或回到 `managed-fallback` / `non-typical`,不得回退到空白页手写。 +特别说明:legacy 主树不能 ad hoc direct import `@hiui-design/typical-page-shells`,并不等于 `page-component` 主链失效。只要 `runtimeAdapterProof` 已就绪,legacy 普通典型页的默认执行语义仍是 `page-component + runtime bridge + slot fill`;缺少 direct shell import 前提,只能阻止 direct shell mount,不能成为默认改写成兼容手拼页或把 reference 当交付资产的理由。 + 对 legacy 普通典型页,还应默认收敛为 4 个硬门禁: 1. `mode` 正确。 diff --git a/skills/hiui-design/docs/onboarding/distribution-governance.md b/skills/hiui-design/docs/onboarding/distribution-governance.md index 308f42099..7cd993fc5 100644 --- a/skills/hiui-design/docs/onboarding/distribution-governance.md +++ b/skills/hiui-design/docs/onboarding/distribution-governance.md @@ -240,7 +240,6 @@ flowchart LR - 默认 target 是 `~/.codex/skills/hiui-design-open-source` - 可选携带 `--open-source-commit` / `--open-source-push` - 若 `sync-open-source-package.mjs --source ` 的 source 本身是独立 Git 维护仓根目录,脚本会先执行 `check-rules-version-alignment.mjs`;guarded 变更未 bump `rules/VERSION` 时直接 fail closed -- `scripts/public-cli-contracts.json` 中 `machine-public` tier 声明为 `status=shipped` 且 `jsonContract=shipped` 的 fixture,属于公开分发契约的一部分;open-source package 必须稳定携带这些 `scripts/tests/fixtures/public-cli/*.json`,不能只保留文档或契约引用而丢失文件实体 - 若本次同步涉及 `@hiui-design/typical-page-shells` 版本变化,发布前必须先执行 `node scripts/check-public-runtime-publish-readiness.mjs --source-root --public-root `,确认 vendor snapshot、vendored tgz、`packages/typical-page-shells/package.json` 和 dry-run 都一致;真实发布后再执行 `node scripts/verify-public-runtime-release.mjs --source-root --public-root `,确认 npm registry 已暴露 exact version - 未通过上述门禁前,不要把“开源仓已生成 `packages/typical-page-shells`”误读成“公开 npm 已可安装” - 具体执行顺序见 `public-runtime-release-checklist.md` diff --git a/skills/hiui-design/examples/host-integration/src/app-frame.tsx b/skills/hiui-design/examples/host-integration/src/app-frame.tsx index f6ef95c46..9e04b5f6b 100644 --- a/skills/hiui-design/examples/host-integration/src/app-frame.tsx +++ b/skills/hiui-design/examples/host-integration/src/app-frame.tsx @@ -39,6 +39,8 @@ type MenuDataItem = { children?: MenuDataItem[] } +type MenuValue = string | number + const LOCALE_OPTIONS: Array<{ id: SupportedLocale; nativeLabel: string }> = [ { id: 'zh-CN', nativeLabel: '简体中文' }, { id: 'zh-TW', nativeLabel: '繁體中文' }, @@ -80,6 +82,36 @@ const APP_LIST_ITEMS = [ }, ] as const +const EXAMPLE_LOGO_SRC = [ + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAAB', + 'AAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAMKADAAQAAAABAAAAMAAAAADbN2wMAAAIrUlEQVRoBc1afYxdRRWfc+99+2HbdUtQoKKN6WLLNjTSisB/YKkmkLJsyNYYtgkfbfwH', + 'UoohxKjdVyBRawJG/zKxEk2NiR+FJUJNm1BJNKQJ4B9N69KuGiJ2kUYK+8ru2/fuvePvN3Pn9X3c+967u5uGae+bmTPnnPmdM2c+7twVtQzp1md0//lSdIfW', + 'eqsotV4pPaSUrIbqVYn6EmgXQJtGfQr58U8N+Mdee0zml9o9+ltcuuH7evX8QjQKYHdrpbYprT+RS5PIHDo/Kkom+3r9yZPfFhiYP+U2YNOP9IqP5sJHlVaP', + 'A/Qn83fZKiEiJQA5MKCCZ94oylwrRzalawO2/EwXPpyp7tZKvgfgV2erXEKLyAxGtLh5uHDwdzsk6kZTVwasL+o1kQoPI8Zv7kbpknlEXveD4J4z35X/dNLV', + '0YChYuUWeP0wvH5NJ2XL2o7R8Dw9enZfz4l2er12jdftDx/AivHqZQdPUHBYHMufh4rheDuMmSOwbn/4dSj5DZ5MnnaKl7NNxNs5XQwOpelMBXfdk5WbaT3A', + '96UJXXaaSBnhdFtaOLUYcP3Tem0lDE8A/FWXHWi7DjEnMLFvap7YDXOgWNReJaz+4WMHnoZhTkRh+MLYb7Vfb2eDAYdUdD82qC31DB+rstZfevN09aF6TLUQ', + '2ljUK8sqPENL6xnylLGjvoNVa9IT9ZKn/X/AVecojx1pTSzRuliru+DKEewn1+bR28CLUBpUwZDbsQPXCPBPLBY8gAOoTNw47D+XsYOeQT98jiAE9vztdPQA', + 'DNkPQ9a4/rvO4eBZCR8D/9OUMSOw/od6VTgfvgsD8h3IqEDkRb8/GH/rCcGJs/vEPqP58BCMuLt7KcvJs1N/b7CWB0AzB+L5aPtiwAP9T8ZVMJoXPGFQhrKe', + 'eD/NawCMXlVeiEYoZwzQSo/lVULP71TB3mJR4ryyjp+y9yn/UepytG5zYDYGSBI+72EEut600OE5hM2GxXg+DWASTlO55gTeJz49EFzpheXozjzgLQCZWC7w', + '1Gd1yUSacZk0zNfzs9E2T7T+ciZTSgO8/w5Xm5SmJZGok7rzKdG3cw7ckE9IJjOWynxqmritTplsIneqbvAwGdZ24qpv5yZVX1/Ocn7deggycmUeENxh8/Dn', + '4c2vW1ZjBNRgnk7c8SCPTLe8i9C9KsAK1HCg69TZ8LCaP5XCNFSslrAMrkxpaiFhsl6cLhbcnVFLex5CLvBU/OZp9fmMDtLsymBVqbw89GUJZNBLuQ0ovD23', + 'IU2ZFjmYRk+jZfHyxJrGn03T9iyUzdDaosvx1laqUtP7/J/jbPRKWlsDDTyGt4FoK/a4ndKQSZLp3CMQ/LPyjbGxxrci6kdc655CMN7WCIAnD3mbMdk3LXu+', + 'aW5rU5+qvQ+0YWpsquirTv/rf/eB+KvGBqX+/h2ZwUS+Y+jJaBd2+IfQvjHhOcWwoefTwJOH7wiQzfmiI8dl3USlxRtJp6lZ359w7Bf17srVV6w/cURmU5ly', + 'Epd0mPOw+PIRBJPg9cY9HTBcfXH2/V/wEqADX8dm6khebHKtQIB6lNfznjVAlB/Yxxnj6MYwGkeokNK+KI39Wwdy7+9f/eDHGHZQF5cI/tcqoo78b2V492av', + 'iQEs4AbU18qrM8RjPRkhDwbwMa9AoDFh1Xhk47YPnt8ylv+anWFzqFJ5PtbxI1Zb97+YRyV+U6CEZz2PAoHDuzSEoEmvz11ZETweMwowCOvRyHzpw5PXb5/d', + 'dVtRd1wUuNp84VuVXdH7C1M6hucjTEE+ORKG/ID7ICKbnq1iBE10wKMYBfwjgTRLBUhTYfyA+PJFkMFDQzhhTI6mHoaW+rfve5Mq8F7Wvj/tDfbivh/4qgtr', + '4l61Tkl8FwwfwYp0re5lp5THg/8mJ3On1HytQs/iMov/lZ+Ecy2sSXfg0R5HomLXIX2NjjWPg2bmo67UZ2PRD2sdPaxVrKLZqlIFMHGkqvghf6/hUxIiJ51V', + 'GuC2Bsyv9kkX3Z0Q+QJjAEsEy4x4jNakbLTbdpmLVGw6q+vkEnhqsWBs6dJvos8QeAVAo12qc5AjZeb48LF5ODhYf55PDLBhY2DVgTdo0IHpA1qjBfwkIWOs', + 'rA8Beo4KXM6yUehyq9iQMNKahpCEUTCFEC1GJgFA3fXJXu7e0/w2CAPISCEqRQ5B5kac4NHiocb5of+L86LphOwoJGVGHov8SbpPCCQi1YhJ2QyxkUAbG50C', + 'Mqcke70+2nwzTc7AZywDAQF6XN8N4ktI7BxAF/BY+TwD1ybuBy6ZEj2Z4LB4k4pjcjk9z0QhsrCO3LiKNMwzk7CEmwRH4d/us/sKqZ+aTAgRtG80QgRl1muT', + 'N9ETvR0qXUbFeZ7ak77IQrIFwgakpM1Wmn4TnbYz12a0GFJNFp7H15ndWV9nKFmbxMbz0EHgtNkh4IoUVyWeP7Vgo9WtOonHW2Leibq8Hh/LNfCsgMmELYpu', + 'RJMRQdMMAmL07FNBqucpzVTbic0mhgnqNjCuTnb3ldj/SD+oyvIXI2FcjRL6Zuy71IDLEdvlFDBeS5jMsJPGOj6z9vXcdPap9l8oyWkMMOed5BghxggYAo9g', + 'gsd+j9r71z3BL/s/N/AVeGmv8r33jNfZDTXwB485K7lRqTOMLKnJAAZaYwjk4XkM7gxi8ZubbyzckjZh0/TI1sMhHGknsUUDnVyFxCvpWMaP72i8eN20U68I', + 'Z0t7tOjH0eegwFDjtGRjc3MEhz2rLjHqUt3S7U4OSIgT8XA178uBgRU9+f/U4KsvRm6hsfGv8Ylf1HPQOnFshB8u0tMX79eD5dLFHXi52h6LbMVa229GwwFm', + 'THMkWuoJ3ccfe4g+itVssq+vb/LkDxb5xx5f+2NUwunuHDYWbHD6JQ9nmSN35rujvHVM91/w5mCEvh3noc9gTK6Bx/GpCo+nerDplWLfuyCensZZaAp9HL+i', + '0HvstWeX/uc2/wdxg0rVYTe+FgAAAABJRU5ErkJggg==', +].join('') + function getLanguageFieldLabel(locale: SupportedLocale, translate: (key: string) => string) { const localizedLabel = translate('语言') if (locale === 'en-US') return localizedLabel @@ -449,13 +481,13 @@ export function TypicalPageAppFrame({ [], ) const [collapsed, setCollapsed] = React.useState(true) - const [activeAppId, setActiveAppId] = React.useState(APP_LIST_ITEMS[0].id) + const [activeAppId, setActiveAppId] = React.useState(APP_LIST_ITEMS[0].id) const [appListPopoverVisible, setAppListPopoverVisible] = React.useState(false) const [profileVisible, setProfileVisible] = React.useState(false) - const [activeMenuId, setActiveMenuId] = React.useState(() => + const [activeMenuId, setActiveMenuId] = React.useState(() => findMenuActiveIdForPath(routes, currentPath || '/'), ) - const [selectMenuId, setSelectMenuId] = React.useState('') + const [selectMenuId, setSelectMenuId] = React.useState('') const appListData = React.useMemo( () => APP_LIST_ITEMS.map((item) => ({ @@ -559,7 +591,7 @@ export function TypicalPageAppFrame({ })), }, ], - onChange: (_value: Record, targetItem: { id?: React.ReactText }) => { + onChange: (_value: Record, targetItem: { id?: MenuValue }) => { const nextLocale = String(targetItem.id ?? '') if (LOCALE_OPTIONS.some((option) => option.id === nextLocale)) { setLocale(nextLocale as SupportedLocale) @@ -608,7 +640,7 @@ export function TypicalPageAppFrame({
logo diff --git a/skills/hiui-design/examples/host-integration/src/charts/hiui-chart-theme.ts b/skills/hiui-design/examples/host-integration/src/charts/hiui-chart-theme.ts index db1952fca..136739a67 100644 --- a/skills/hiui-design/examples/host-integration/src/charts/hiui-chart-theme.ts +++ b/skills/hiui-design/examples/host-integration/src/charts/hiui-chart-theme.ts @@ -235,7 +235,7 @@ export function createHiuiBandColorScale( function mergeHiuiBandScale( baseScale: Record, axis: 'x' | 'y', - bandPadding: typeof hiuiBandPadding + bandPadding: Readonly<{ paddingInner: number; paddingOuter: number }> ) { const axisScale = (baseScale[axis] as Record | undefined) ?? {} diff --git a/skills/hiui-design/examples/host-integration/src/pages/data-visualization.tsx b/skills/hiui-design/examples/host-integration/src/pages/data-visualization.tsx index 497117bcb..bd33c53e3 100644 --- a/skills/hiui-design/examples/host-integration/src/pages/data-visualization.tsx +++ b/skills/hiui-design/examples/host-integration/src/pages/data-visualization.tsx @@ -935,7 +935,7 @@ function VisualizationStatSection() { legend: { color: { ...chartLegendConfig.color, - itemMarker: (team) => + itemMarker: (team: string) => team === radarTeams[0] ? 'hyphen' : 'diamond', }, }, diff --git a/skills/hiui-design/examples/host-integration/src/pages/drawer-form.tsx b/skills/hiui-design/examples/host-integration/src/pages/drawer-form.tsx index b9ff91409..1fa56a5d6 100644 --- a/skills/hiui-design/examples/host-integration/src/pages/drawer-form.tsx +++ b/skills/hiui-design/examples/host-integration/src/pages/drawer-form.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useRef, useState } from 'react' import { Button, Message, PageHeader } from '@hi-ui/hiui' import { F } from '@hi-ui/schema-core' import { SchemaForm } from '@hi-ui/schema-form' -import type { SchemaFormProps } from '@hi-ui/schema-form' +import type { EnhancedFormRefType } from '@hi-ui/schema-form' import { TypicalPageFieldMapProvider } from '@hiui-design/typical-page-shells' import { TypicalPageHeaderPortal } from '@hiui-design/typical-page-shells/host' import { ProDrawerFooterActions, ProFormDrawer } from '@hiui-design/typical-page-shells/pro-form-drawer' @@ -43,7 +43,8 @@ const dateProps = { export function DrawerFormPage() { const { locale, t } = useTranslation() const [visible, setVisible] = useState(true) - const formRef = useRef(null) as NonNullable['formRef']> + // React 19 returns RefObject for useRef(null), while schema-form still expects RefObject. + const formRef = useRef(null) as unknown as EnhancedFormRefType const fields = useMemo( () => [ diff --git a/skills/hiui-design/examples/host-integration/src/pages/empty-state.tsx b/skills/hiui-design/examples/host-integration/src/pages/empty-state.tsx index 09e9b3b70..c250aa7c0 100644 --- a/skills/hiui-design/examples/host-integration/src/pages/empty-state.tsx +++ b/skills/hiui-design/examples/host-integration/src/pages/empty-state.tsx @@ -1,3 +1,4 @@ +import type { CSSProperties } from 'react' import { Button, EmptyState, PageHeader, Space } from '@hi-ui/hiui' import { PlusOutlined } from '@hi-ui/icons' import { TypicalPageHeaderPortal } from '@hiui-design/typical-page-shells/host' @@ -9,7 +10,7 @@ const pageStyle = { display: 'flex', flexDirection: 'column', background: '#F5F8FC', -} +} satisfies CSSProperties const panelStyle = { flex: '1 1 0%', @@ -22,38 +23,38 @@ const panelStyle = { display: 'flex', alignItems: 'center', justifyContent: 'center', -} +} satisfies CSSProperties const contentStyle = { width: 400, textAlign: 'center', -} +} satisfies CSSProperties const titleStyle = { fontSize: 16, lineHeight: '24px', fontWeight: 500, color: '#1A1D26', -} +} satisfies CSSProperties const descStyle = { fontSize: 14, lineHeight: '22px', color: '#60636B', -} +} satisfies CSSProperties const linkStyle = { color: '#2660FF', -} +} satisfies CSSProperties const headerActionsStyle = { gap: 12, -} +} satisfies CSSProperties const actionsStyle = { marginTop: 12, justifyContent: 'center', -} +} satisfies CSSProperties export function EmptyStatePage() { const { t } = useTranslation() diff --git a/skills/hiui-design/examples/host-integration/src/pages/full-page-detail.tsx b/skills/hiui-design/examples/host-integration/src/pages/full-page-detail.tsx index 8aca5d6b7..255fdf290 100644 --- a/skills/hiui-design/examples/host-integration/src/pages/full-page-detail.tsx +++ b/skills/hiui-design/examples/host-integration/src/pages/full-page-detail.tsx @@ -1,7 +1,8 @@ import { useMemo } from 'react' import { Button, PageHeader } from '@hi-ui/hiui' import type { DescriptionsProps } from '@hi-ui/hiui' -import { D, G } from '@hi-ui/schema-core' +import { D, G, T } from '@hi-ui/schema-core' +import type { GroupConfigType } from '@hi-ui/schema-core' import { GroupMapProvider, SchemaGroup } from '@hi-ui/schema-group' import { SchemaDescriptionsBridge } from '@hi-ui/schema-group/descriptions' import { SchemaTableBridge } from '@hi-ui/schema-group/table' @@ -69,76 +70,77 @@ function FullPageDetailInner() { placement: 'vertical', column: 3, labelPlacement: 'left', - labelWidth: null, + labelWidth: undefined, }), [] ) - const groups = useMemo( - () => [ - G(t('基础信息'), 'basicInfo') - .Descriptions({ - fields: [ - D(t('工单号'), 'ticketNo').val, - D(t('工单状态'), 'status').val, - D(t('工单类型'), 'ticketType').val, - D(t('服务方式'), 'serviceMethod').val, - D(t('关联商品'), 'relatedProduct').val, - D(t('购买渠道'), 'purchaseChannel').val, - D(t('来访原因'), 'visitReason').val, - ], - props: { descriptionsProps }, - }) - .CardProps({ size: 'lg' }) - .val, - G(t('三包信息'), 'warrantyInfo') - .Custom({ - render: () => , - }) - .CardProps({ size: 'lg' }) - .val, - G(t('客户信息'), 'customerInfo') - .Descriptions({ - fields: [ - D(t('用户姓名'), 'userName').val, - D(t('用户标签'), 'userTags').val, - D(t('用户电话'), 'userPhone').val, - ], - props: { descriptionsProps }, - }) - .CardProps({ size: 'lg' }) - .val, - G(t('服务信息'), 'serviceInfo') - .Descriptions({ - fields: [ - D(t('服务机构'), 'serviceOrg').val, - D(t('服务工程师'), 'serviceEngineer').val, - D(t('受理时间'), 'acceptedAt').val, - D(t('问题描述'), 'description').MWP({ colSpan: 3 }).val, - ], - props: { descriptionsProps }, - }) - .CardProps({ size: 'lg' }) - .val, - G(t('服务记录'), 'serviceRecords') - .Table({ - fields: [ - D(t('服务时间'), 'serviceTime').W(180).val, - D(t('服务动作'), 'serviceAction').W(160).val, - D(t('状态'), 'status').W(120).val, - D(t('详情描述'), 'detailDescription').W(360).val, - ], - props: { - className: styles.serviceRecordTable, - tableProps: { - bordered: false, - striped: false, + const groups = useMemo( + () => + [ + G(t('基础信息'), 'basicInfo') + .Descriptions({ + fields: [ + D(t('工单号'), 'ticketNo').val, + D(t('工单状态'), 'status').val, + D(t('工单类型'), 'ticketType').val, + D(t('服务方式'), 'serviceMethod').val, + D(t('关联商品'), 'relatedProduct').val, + D(t('购买渠道'), 'purchaseChannel').val, + D(t('来访原因'), 'visitReason').val, + ], + props: { descriptionsProps }, + }) + .CardProps({ size: 'lg' }) + .val, + G(t('三包信息'), 'warrantyInfo') + .Custom({ + render: () => , + }) + .CardProps({ size: 'lg' }) + .val, + G(t('客户信息'), 'customerInfo') + .Descriptions({ + fields: [ + D(t('用户姓名'), 'userName').val, + D(t('用户标签'), 'userTags').val, + D(t('用户电话'), 'userPhone').val, + ], + props: { descriptionsProps }, + }) + .CardProps({ size: 'lg' }) + .val, + G(t('服务信息'), 'serviceInfo') + .Descriptions({ + fields: [ + D(t('服务机构'), 'serviceOrg').val, + D(t('服务工程师'), 'serviceEngineer').val, + D(t('受理时间'), 'acceptedAt').val, + D(t('问题描述'), 'description').MWP({ colSpan: 3 }).val, + ], + props: { descriptionsProps }, + }) + .CardProps({ size: 'lg' }) + .val, + G(t('服务记录'), 'serviceRecords') + .Table({ + fields: [ + T(t('服务时间'), 'serviceTime').W(180).val, + T(t('服务动作'), 'serviceAction').W(160).val, + T(t('状态'), 'status').W(120).val, + T(t('详情描述'), 'detailDescription').W(360).val, + ], + props: { + className: styles.serviceRecordTable, + tableProps: { + bordered: false, + striped: false, + }, }, - }, - }) - .CardProps({ size: 'lg' }) - .val, - ], + }) + .CardProps({ size: 'lg' }) + .val, + ], [descriptionsProps, t] ) diff --git a/skills/hiui-design/examples/host-integration/src/pages/intranet-offline.tsx b/skills/hiui-design/examples/host-integration/src/pages/intranet-offline.tsx index 99f6431b0..1c68759f4 100644 --- a/skills/hiui-design/examples/host-integration/src/pages/intranet-offline.tsx +++ b/skills/hiui-design/examples/host-integration/src/pages/intranet-offline.tsx @@ -1,3 +1,4 @@ +import type { CSSProperties } from 'react' import { Button, EmptyState, PageHeader, Space } from '@hi-ui/hiui' import { PlusOutlined } from '@hi-ui/icons' import { TypicalPageHeaderPortal } from '@hiui-design/typical-page-shells/host' @@ -13,7 +14,7 @@ const pageStyle = { display: 'flex', flexDirection: 'column', background: '#F5F8FC', -} +} satisfies CSSProperties const panelStyle = { flex: '1 1 0%', @@ -26,44 +27,44 @@ const panelStyle = { display: 'flex', alignItems: 'center', justifyContent: 'center', -} +} satisfies CSSProperties const contentStyle = { width: 400, textAlign: 'center', -} +} satisfies CSSProperties const titleStyle = { fontSize: 16, lineHeight: '24px', fontWeight: 500, color: '#1A1D26', -} +} satisfies CSSProperties const descStyle = { fontSize: 14, lineHeight: '22px', color: '#60636B', -} +} satisfies CSSProperties const linkStyle = { color: '#2660FF', -} +} satisfies CSSProperties const headerActionsStyle = { gap: 12, -} +} satisfies CSSProperties const actionsStyle = { marginTop: 12, justifyContent: 'center', -} +} satisfies CSSProperties const indicatorWrapStyle = { position: 'relative', width: 100, height: 100, -} +} satisfies CSSProperties const baseStyle = { position: 'absolute', @@ -72,7 +73,7 @@ const baseStyle = { width: 73.528, height: 75.22, display: 'block', -} +} satisfies CSSProperties const globeStyle = { position: 'absolute', @@ -81,7 +82,7 @@ const globeStyle = { width: 34.625, height: 30.882, display: 'block', -} +} satisfies CSSProperties const badgeStyle = { position: 'absolute', @@ -90,7 +91,7 @@ const badgeStyle = { width: 30.417, height: 30.223, display: 'block', -} +} satisfies CSSProperties const badgeSlashAssetStyle = { position: 'absolute', @@ -99,7 +100,7 @@ const badgeSlashAssetStyle = { width: 15.67, height: 15.56, display: 'block', -} +} satisfies CSSProperties const badgeSlashWrapStyle = { position: 'absolute', @@ -110,7 +111,7 @@ const badgeSlashWrapStyle = { display: 'flex', alignItems: 'center', justifyContent: 'center', -} +} satisfies CSSProperties const badgeSlashLineStyle = { width: 1.67, @@ -121,7 +122,7 @@ const badgeSlashLineStyle = { boxSizing: 'border-box', transform: 'rotate(135deg)', transformOrigin: 'center', -} +} satisfies CSSProperties function IntranetOfflineIndicator() { return ( diff --git a/skills/hiui-design/examples/host-integration/src/pages/load-fail.tsx b/skills/hiui-design/examples/host-integration/src/pages/load-fail.tsx index f71ed3c1d..30a1b2b59 100644 --- a/skills/hiui-design/examples/host-integration/src/pages/load-fail.tsx +++ b/skills/hiui-design/examples/host-integration/src/pages/load-fail.tsx @@ -1,3 +1,4 @@ +import type { CSSProperties } from 'react' import { Button, EmptyState, PageHeader, Space } from '@hi-ui/hiui' import { PlusOutlined } from '@hi-ui/icons' import { TypicalPageHeaderPortal } from '@hiui-design/typical-page-shells/host' @@ -10,7 +11,7 @@ const pageStyle = { display: 'flex', flexDirection: 'column', background: '#F5F8FC', -} +} satisfies CSSProperties const panelStyle = { flex: '1 1 0%', @@ -23,38 +24,38 @@ const panelStyle = { display: 'flex', alignItems: 'center', justifyContent: 'center', -} +} satisfies CSSProperties const contentStyle = { width: 400, textAlign: 'center', -} +} satisfies CSSProperties const titleStyle = { fontSize: 16, lineHeight: '24px', fontWeight: 500, color: '#1A1D26', -} +} satisfies CSSProperties const descStyle = { fontSize: 14, lineHeight: '22px', color: '#60636B', -} +} satisfies CSSProperties const linkStyle = { color: '#2660FF', -} +} satisfies CSSProperties const headerActionsStyle = { gap: 12, -} +} satisfies CSSProperties const actionsStyle = { marginTop: 12, justifyContent: 'center', -} +} satisfies CSSProperties const indicatorWrapStyle = { width: 100, @@ -62,7 +63,7 @@ const indicatorWrapStyle = { display: 'flex', alignItems: 'flex-end', justifyContent: 'center', -} +} satisfies CSSProperties function LoadFailIndicator() { return ( diff --git a/skills/hiui-design/examples/host-integration/src/pages/network-error.tsx b/skills/hiui-design/examples/host-integration/src/pages/network-error.tsx index 58294aee2..7e5285ee4 100644 --- a/skills/hiui-design/examples/host-integration/src/pages/network-error.tsx +++ b/skills/hiui-design/examples/host-integration/src/pages/network-error.tsx @@ -1,3 +1,4 @@ +import type { CSSProperties } from 'react' import { Button, EmptyState, PageHeader, Space } from '@hi-ui/hiui' import { PlusOutlined } from '@hi-ui/icons' import { TypicalPageHeaderPortal } from '@hiui-design/typical-page-shells/host' @@ -10,7 +11,7 @@ const pageStyle = { display: 'flex', flexDirection: 'column', background: '#F5F8FC', -} +} satisfies CSSProperties const panelStyle = { flex: '1 1 0%', @@ -23,44 +24,44 @@ const panelStyle = { display: 'flex', alignItems: 'center', justifyContent: 'center', -} +} satisfies CSSProperties const contentStyle = { width: 400, textAlign: 'center', -} +} satisfies CSSProperties const titleStyle = { fontSize: 16, lineHeight: '24px', fontWeight: 500, color: '#1A1D26', -} +} satisfies CSSProperties const descStyle = { fontSize: 14, lineHeight: '22px', color: '#60636B', -} +} satisfies CSSProperties const linkStyle = { color: '#2660FF', -} +} satisfies CSSProperties const headerActionsStyle = { gap: 12, -} +} satisfies CSSProperties const actionsStyle = { marginTop: 12, justifyContent: 'center', -} +} satisfies CSSProperties const indicatorWrapStyle = { position: 'relative', width: 100, height: 100, -} +} satisfies CSSProperties const indicatorImageStyle = { position: 'absolute', @@ -69,7 +70,7 @@ const indicatorImageStyle = { width: 73.75, height: 76.875, display: 'block', -} +} satisfies CSSProperties function NetworkErrorIndicator() { return ( diff --git a/skills/hiui-design/examples/host-integration/src/pages/no-permission.tsx b/skills/hiui-design/examples/host-integration/src/pages/no-permission.tsx index 33a990a09..9298a4f30 100644 --- a/skills/hiui-design/examples/host-integration/src/pages/no-permission.tsx +++ b/skills/hiui-design/examples/host-integration/src/pages/no-permission.tsx @@ -1,3 +1,4 @@ +import type { CSSProperties } from 'react' import { Button, EmptyState, PageHeader, Space } from '@hi-ui/hiui' import { PlusOutlined } from '@hi-ui/icons' import { TypicalPageHeaderPortal } from '@hiui-design/typical-page-shells/host' @@ -11,7 +12,7 @@ const pageStyle = { display: 'flex', flexDirection: 'column', background: '#F5F8FC', -} +} satisfies CSSProperties const panelStyle = { flex: '1 1 0%', @@ -24,44 +25,44 @@ const panelStyle = { display: 'flex', alignItems: 'center', justifyContent: 'center', -} +} satisfies CSSProperties const contentStyle = { width: 400, textAlign: 'center', -} +} satisfies CSSProperties const titleStyle = { fontSize: 16, lineHeight: '24px', fontWeight: 500, color: '#1A1D26', -} +} satisfies CSSProperties const descStyle = { fontSize: 14, lineHeight: '22px', color: '#60636B', -} +} satisfies CSSProperties const linkStyle = { color: '#2660FF', -} +} satisfies CSSProperties const headerActionsStyle = { gap: 12, -} +} satisfies CSSProperties const actionsStyle = { marginTop: 12, justifyContent: 'center', -} +} satisfies CSSProperties const indicatorWrapStyle = { position: 'relative', width: 100, height: 100, -} +} satisfies CSSProperties const mainIndicatorStyle = { position: 'absolute', @@ -70,7 +71,7 @@ const mainIndicatorStyle = { width: 75.804, height: 80.1834, display: 'block', -} +} satisfies CSSProperties const badgeIndicatorStyle = { position: 'absolute', @@ -79,7 +80,7 @@ const badgeIndicatorStyle = { width: 20.833, height: 20.834, display: 'block', -} +} satisfies CSSProperties function NoPermissionIndicator() { return ( diff --git a/skills/hiui-design/examples/host-integration/src/pages/page-not-found.tsx b/skills/hiui-design/examples/host-integration/src/pages/page-not-found.tsx index d09d25a62..93cc55788 100644 --- a/skills/hiui-design/examples/host-integration/src/pages/page-not-found.tsx +++ b/skills/hiui-design/examples/host-integration/src/pages/page-not-found.tsx @@ -1,3 +1,4 @@ +import type { CSSProperties } from 'react' import { Button, EmptyState, PageHeader, Space } from '@hi-ui/hiui' import { PlusOutlined } from '@hi-ui/icons' import { TypicalPageHeaderPortal } from '@hiui-design/typical-page-shells/host' @@ -10,7 +11,7 @@ const pageStyle = { display: 'flex', flexDirection: 'column', background: '#F5F8FC', -} +} satisfies CSSProperties const panelStyle = { flex: '1 1 0%', @@ -23,44 +24,44 @@ const panelStyle = { display: 'flex', alignItems: 'center', justifyContent: 'center', -} +} satisfies CSSProperties const contentStyle = { width: 400, textAlign: 'center', -} +} satisfies CSSProperties const titleStyle = { fontSize: 16, lineHeight: '24px', fontWeight: 500, color: '#1A1D26', -} +} satisfies CSSProperties const descStyle = { fontSize: 14, lineHeight: '22px', color: '#60636B', -} +} satisfies CSSProperties const linkStyle = { color: '#2660FF', -} +} satisfies CSSProperties const headerActionsStyle = { gap: 12, -} +} satisfies CSSProperties const actionsStyle = { marginTop: 12, justifyContent: 'center', -} +} satisfies CSSProperties const indicatorWrapStyle = { position: 'relative', width: 100, height: 100, -} +} satisfies CSSProperties const indicatorImageStyle = { position: 'absolute', @@ -70,7 +71,7 @@ const indicatorImageStyle = { height: 77.373, transform: 'translateX(-50%)', display: 'block', -} +} satisfies CSSProperties function PageNotFoundIndicator() { return ( diff --git a/skills/hiui-design/examples/host-integration/src/pages/request-utils.ts b/skills/hiui-design/examples/host-integration/src/pages/request-utils.ts index 2bc90084d..4e63cf770 100644 --- a/skills/hiui-design/examples/host-integration/src/pages/request-utils.ts +++ b/skills/hiui-design/examples/host-integration/src/pages/request-utils.ts @@ -1,5 +1,10 @@ import type { GetDataSourceParamsType } from '@hi-ui/schema-core' +type QueryFilterItem = { + id?: string + value?: unknown +} + export type PagedResponse = { list: T[] total: number @@ -8,14 +13,14 @@ export type PagedResponse = { } export function getKeywordValue(params: GetDataSourceParamsType) { - const keywordFilter = (params.filters ?? []).find((item) => item.id === 'keyword') + const keywordFilter = ((params.filters ?? []) as QueryFilterItem[]).find((item) => item.id === 'keyword') return keywordFilter?.value != null ? String(keywordFilter.value).trim().toLowerCase() : '' } export function getParamsWithoutKeyword(params: GetDataSourceParamsType): GetDataSourceParamsType { return { ...params, - filters: (params.filters ?? []).filter((item) => item.id !== 'keyword'), + filters: ((params.filters ?? []) as QueryFilterItem[]).filter((item) => item.id !== 'keyword'), } } diff --git a/skills/hiui-design/examples/host-integration/src/pages/typical-pages.mock.ts b/skills/hiui-design/examples/host-integration/src/pages/typical-pages.mock.ts index 809fbe790..c9aeb063a 100644 --- a/skills/hiui-design/examples/host-integration/src/pages/typical-pages.mock.ts +++ b/skills/hiui-design/examples/host-integration/src/pages/typical-pages.mock.ts @@ -381,11 +381,11 @@ const orgTreeTableRows: OrgTreeTableRow[] = [ }, ] -function filterOrgTreeRows(list: OrgTreeTableRow[], values: Record) { +function filterOrgTreeRows(list: OrgTreeTableRow[], values: Record): OrgTreeTableRow[] { const keyword = values.keyword != null ? String(values.keyword) : '' return list.flatMap((item) => { - const children = item.children ? filterOrgTreeRows(item.children, values) : [] + const children: OrgTreeTableRow[] = item.children ? filterOrgTreeRows(item.children, values) : [] const selfMatched = (!values.managerId || item.managerId === values.managerId) && (!values.organizationId || item.organizationId === values.organizationId) && diff --git a/skills/hiui-design/examples/host-integration/src/pages/under-construction.tsx b/skills/hiui-design/examples/host-integration/src/pages/under-construction.tsx index f87dac9d2..8e288762d 100644 --- a/skills/hiui-design/examples/host-integration/src/pages/under-construction.tsx +++ b/skills/hiui-design/examples/host-integration/src/pages/under-construction.tsx @@ -1,3 +1,4 @@ +import type { CSSProperties } from 'react' import { Button, EmptyState, PageHeader, Space } from '@hi-ui/hiui' import { PlusOutlined } from '@hi-ui/icons' import { TypicalPageHeaderPortal } from '@hiui-design/typical-page-shells/host' @@ -13,7 +14,7 @@ const pageStyle = { display: 'flex', flexDirection: 'column', background: '#F5F8FC', -} +} satisfies CSSProperties const panelStyle = { flex: '1 1 0%', @@ -26,44 +27,44 @@ const panelStyle = { display: 'flex', alignItems: 'center', justifyContent: 'center', -} +} satisfies CSSProperties const contentStyle = { width: 400, textAlign: 'center', -} +} satisfies CSSProperties const titleStyle = { fontSize: 16, lineHeight: '24px', fontWeight: 500, color: '#1A1D26', -} +} satisfies CSSProperties const descStyle = { fontSize: 14, lineHeight: '22px', color: '#60636B', -} +} satisfies CSSProperties const linkStyle = { color: '#2660FF', -} +} satisfies CSSProperties const headerActionsStyle = { gap: 12, -} +} satisfies CSSProperties const actionsStyle = { marginTop: 12, justifyContent: 'center', -} +} satisfies CSSProperties const indicatorWrapStyle = { position: 'relative', width: 100, height: 100, -} +} satisfies CSSProperties const mainIndicatorStyle = { position: 'absolute', @@ -72,7 +73,7 @@ const mainIndicatorStyle = { width: 73.75, height: 63.75, display: 'block', -} +} satisfies CSSProperties const line1Style = { position: 'absolute', @@ -83,7 +84,7 @@ const line1Style = { display: 'block', transform: 'scaleX(-1) rotate(-12.65deg) skewX(0.99deg)', transformOrigin: 'center', -} +} satisfies CSSProperties const line2Style = { position: 'absolute', @@ -94,7 +95,7 @@ const line2Style = { display: 'block', transform: 'scaleX(-1) rotate(-12.65deg) skewX(0.99deg)', transformOrigin: 'center', -} +} satisfies CSSProperties const line3Style = { position: 'absolute', @@ -105,7 +106,7 @@ const line3Style = { display: 'block', transform: 'scaleX(-1) rotate(-12.65deg) skewX(0.99deg)', transformOrigin: 'center', -} +} satisfies CSSProperties function UnderConstructionIndicator() { return ( diff --git a/skills/hiui-design/reference/host-integration/src/app-frame.tsx b/skills/hiui-design/reference/host-integration/src/app-frame.tsx index f6ef95c46..78bc2f37c 100644 --- a/skills/hiui-design/reference/host-integration/src/app-frame.tsx +++ b/skills/hiui-design/reference/host-integration/src/app-frame.tsx @@ -80,6 +80,36 @@ const APP_LIST_ITEMS = [ }, ] as const +const EXAMPLE_LOGO_SRC = [ + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAAB', + 'AAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAMKADAAQAAAABAAAAMAAAAADbN2wMAAAIrUlEQVRoBc1afYxdRRWfc+99+2HbdUtQoKKN6WLLNjTSisB/YKkmkLJsyNYYtgkfbfwH', + 'UoohxKjdVyBRawJG/zKxEk2NiR+FJUJNm1BJNKQJ4B9N69KuGiJ2kUYK+8ru2/fuvePvN3Pn9X3c+967u5uGae+bmTPnnPmdM2c+7twVtQzp1md0//lSdIfW', + 'eqsotV4pPaSUrIbqVYn6EmgXQJtGfQr58U8N+Mdee0zml9o9+ltcuuH7evX8QjQKYHdrpbYprT+RS5PIHDo/Kkom+3r9yZPfFhiYP+U2YNOP9IqP5sJHlVaP', + 'A/Qn83fZKiEiJQA5MKCCZ94oylwrRzalawO2/EwXPpyp7tZKvgfgV2erXEKLyAxGtLh5uHDwdzsk6kZTVwasL+o1kQoPI8Zv7kbpknlEXveD4J4z35X/dNLV', + '0YChYuUWeP0wvH5NJ2XL2o7R8Dw9enZfz4l2er12jdftDx/AivHqZQdPUHBYHMufh4rheDuMmSOwbn/4dSj5DZ5MnnaKl7NNxNs5XQwOpelMBXfdk5WbaT3A', + '96UJXXaaSBnhdFtaOLUYcP3Tem0lDE8A/FWXHWi7DjEnMLFvap7YDXOgWNReJaz+4WMHnoZhTkRh+MLYb7Vfb2eDAYdUdD82qC31DB+rstZfevN09aF6TLUQ', + '2ljUK8sqPENL6xnylLGjvoNVa9IT9ZKn/X/AVecojx1pTSzRuliru+DKEewn1+bR28CLUBpUwZDbsQPXCPBPLBY8gAOoTNw47D+XsYOeQT98jiAE9vztdPQA', + 'DNkPQ9a4/rvO4eBZCR8D/9OUMSOw/od6VTgfvgsD8h3IqEDkRb8/GH/rCcGJs/vEPqP58BCMuLt7KcvJs1N/b7CWB0AzB+L5aPtiwAP9T8ZVMJoXPGFQhrKe', + 'eD/NawCMXlVeiEYoZwzQSo/lVULP71TB3mJR4ryyjp+y9yn/UepytG5zYDYGSBI+72EEut600OE5hM2GxXg+DWASTlO55gTeJz49EFzpheXozjzgLQCZWC7w', + '1Gd1yUSacZk0zNfzs9E2T7T+ciZTSgO8/w5Xm5SmJZGok7rzKdG3cw7ckE9IJjOWynxqmritTplsIneqbvAwGdZ24qpv5yZVX1/Ocn7deggycmUeENxh8/Dn', + '4c2vW1ZjBNRgnk7c8SCPTLe8i9C9KsAK1HCg69TZ8LCaP5XCNFSslrAMrkxpaiFhsl6cLhbcnVFLex5CLvBU/OZp9fmMDtLsymBVqbw89GUJZNBLuQ0ovD23', + 'IU2ZFjmYRk+jZfHyxJrGn03T9iyUzdDaosvx1laqUtP7/J/jbPRKWlsDDTyGt4FoK/a4ndKQSZLp3CMQ/LPyjbGxxrci6kdc655CMN7WCIAnD3mbMdk3LXu+', + 'aW5rU5+qvQ+0YWpsquirTv/rf/eB+KvGBqX+/h2ZwUS+Y+jJaBd2+IfQvjHhOcWwoefTwJOH7wiQzfmiI8dl3USlxRtJp6lZ359w7Bf17srVV6w/cURmU5ly', + 'Epd0mPOw+PIRBJPg9cY9HTBcfXH2/V/wEqADX8dm6khebHKtQIB6lNfznjVAlB/Yxxnj6MYwGkeokNK+KI39Wwdy7+9f/eDHGHZQF5cI/tcqoo78b2V492av', + 'iQEs4AbU18qrM8RjPRkhDwbwMa9AoDFh1Xhk47YPnt8ylv+anWFzqFJ5PtbxI1Zb97+YRyV+U6CEZz2PAoHDuzSEoEmvz11ZETweMwowCOvRyHzpw5PXb5/d', + 'dVtRd1wUuNp84VuVXdH7C1M6hucjTEE+ORKG/ID7ICKbnq1iBE10wKMYBfwjgTRLBUhTYfyA+PJFkMFDQzhhTI6mHoaW+rfve5Mq8F7Wvj/tDfbivh/4qgtr', + '4l61Tkl8FwwfwYp0re5lp5THg/8mJ3On1HytQs/iMov/lZ+Ecy2sSXfg0R5HomLXIX2NjjWPg2bmo67UZ2PRD2sdPaxVrKLZqlIFMHGkqvghf6/hUxIiJ51V', + 'GuC2Bsyv9kkX3Z0Q+QJjAEsEy4x4jNakbLTbdpmLVGw6q+vkEnhqsWBs6dJvos8QeAVAo12qc5AjZeb48LF5ODhYf55PDLBhY2DVgTdo0IHpA1qjBfwkIWOs', + 'rA8Beo4KXM6yUehyq9iQMNKahpCEUTCFEC1GJgFA3fXJXu7e0/w2CAPISCEqRQ5B5kac4NHiocb5of+L86LphOwoJGVGHov8SbpPCCQi1YhJ2QyxkUAbG50C', + 'Mqcke70+2nwzTc7AZywDAQF6XN8N4ktI7BxAF/BY+TwD1ybuBy6ZEj2Z4LB4k4pjcjk9z0QhsrCO3LiKNMwzk7CEmwRH4d/us/sKqZ+aTAgRtG80QgRl1muT', + 'N9ETvR0qXUbFeZ7ak77IQrIFwgakpM1Wmn4TnbYz12a0GFJNFp7H15ndWV9nKFmbxMbz0EHgtNkh4IoUVyWeP7Vgo9WtOonHW2Leibq8Hh/LNfCsgMmELYpu', + 'RJMRQdMMAmL07FNBqucpzVTbic0mhgnqNjCuTnb3ldj/SD+oyvIXI2FcjRL6Zuy71IDLEdvlFDBeS5jMsJPGOj6z9vXcdPap9l8oyWkMMOed5BghxggYAo9g', + 'gsd+j9r71z3BL/s/N/AVeGmv8r33jNfZDTXwB485K7lRqTOMLKnJAAZaYwjk4XkM7gxi8ZubbyzckjZh0/TI1sMhHGknsUUDnVyFxCvpWMaP72i8eN20U68I', + 'Z0t7tOjH0eegwFDjtGRjc3MEhz2rLjHqUt3S7U4OSIgT8XA178uBgRU9+f/U4KsvRm6hsfGv8Ylf1HPQOnFshB8u0tMX79eD5dLFHXi52h6LbMVa229GwwFm', + 'THMkWuoJ3ccfe4g+itVssq+vb/LkDxb5xx5f+2NUwunuHDYWbHD6JQ9nmSN35rujvHVM91/w5mCEvh3noc9gTK6Bx/GpCo+nerDplWLfuyCensZZaAp9HL+i', + '0HvstWeX/uc2/wdxg0rVYTe+FgAAAABJRU5ErkJggg==', +].join('') + function getLanguageFieldLabel(locale: SupportedLocale, translate: (key: string) => string) { const localizedLabel = translate('语言') if (locale === 'en-US') return localizedLabel @@ -608,7 +638,7 @@ export function TypicalPageAppFrame({
logo diff --git a/skills/hiui-design/reference/host-integration/src/pages/full-page-detail.tsx b/skills/hiui-design/reference/host-integration/src/pages/full-page-detail.tsx index 8aca5d6b7..7c0fa9eb5 100644 --- a/skills/hiui-design/reference/host-integration/src/pages/full-page-detail.tsx +++ b/skills/hiui-design/reference/host-integration/src/pages/full-page-detail.tsx @@ -69,7 +69,7 @@ function FullPageDetailInner() { placement: 'vertical', column: 3, labelPlacement: 'left', - labelWidth: null, + labelWidth: undefined, }), [] ) diff --git a/skills/hiui-design/rules/QUICK-START.md b/skills/hiui-design/rules/QUICK-START.md index 7fd4ca694..55e828120 100644 --- a/skills/hiui-design/rules/QUICK-START.md +++ b/skills/hiui-design/rules/QUICK-START.md @@ -66,6 +66,13 @@ 5. 只替换标题、路由、查询字段、指标、表格列、表单 / 详情字段、接口 / mock 数据、行操作等业务槽位 6. 保留页壳、region 层级、白底主体、滚动 owner、分页 / footer 挂载语义、source marker 与 row action 语义 +对旧系统升级,普通典型页的 fast path 默认再细分为: + +- `slot-fill-only`:当前页已经是受管实例,只改业务槽位。 +- `rewrite-by-page-component`:当前页仍命中同一典型页型,且 certified page component / project carrier 已可用,但现有源码还不是 ready 的 managed instance;此时直接用受管主资产重建业务页实例,不在旧 JSX 上继续修补壳层。 + +这两类都属于典型页快路径,不等于 `strict-structure`。 + `multi-page-workflow` 的快速路径不是把多个意图揉成一个复杂页面,而是按 `pageUnits` 逐页执行典型页快速路径,并使用每个 `pageUnits[].startFrom` 选择对应起点。并列页面意图默认表示多个路由页面 / 视图工作流;只有出现“一个页面内 / 同屏 / 左右联动 / split / 主从联动”等布局证据时,才进入组合页 / split 判断。 快速路径仍要保证当前页可预览、lint / build 可解释、当前页 contract / preflight 不失真;但不需要在实现前手工展开长篇 page-type 判断、全量 i18n 策略或全局 doctor 历史问题排查。全量 `finalize-page` / source gate / doctor / runtime smoke 保留为正式验收或发布链路。 diff --git a/skills/hiui-design/rules/VERSION b/skills/hiui-design/rules/VERSION index 22beb86a4..65d46df1c 100644 --- a/skills/hiui-design/rules/VERSION +++ b/skills/hiui-design/rules/VERSION @@ -4,7 +4,6 @@ source-snapshot: machine planning entry, fixture-backed public JSON contracts, r machine-facts-file: rules/common.page-types.json machine-public-cli: shipped plan-page-task, typical-page-runtime-smoke, typical-page-preview-ready, typical-page-start-page notes: - - promoted scripts/tests/fixtures/public-cli/*.json to explicit open-source distribution assets and added a fail-closed maintainer check so shipped machine-public contract fixtures cannot drift out of the public package - enforced a maintainer-source version gate so guarded changes under rules/, scripts/, SKILL.md, and distribution-manifest.json must bump rules/VERSION before sync or release - restored fail-closed runtime delivery on vendored typical-page-shells tgz for open-source sync, including legacy-host-hard-gates and project-facts runtime dependencies - split maintainer runtime out of skill-root outputs into ~/.codex/maintainers/hiui-design/runtime, with HIUI_DESIGN_MAINTAINER_HOME as the explicit override point diff --git a/skills/hiui-design/rules/failure-matrix.md b/skills/hiui-design/rules/failure-matrix.md index a49b85458..ce8d32073 100644 --- a/skills/hiui-design/rules/failure-matrix.md +++ b/skills/hiui-design/rules/failure-matrix.md @@ -15,9 +15,12 @@ | `PLAN_SCHEMA_UNSUPPORTED` | plan | `schemaVersion` 缺失且无法兼容,或不是已支持版本 | 机器计划协议版本不可识别 | 停止执行,升级脚本或重新生成计划 | 否 | 是 | | `PLAN_INVALID` | plan | `status=invalid`,或必填字段缺失、字段组合矛盾、action 不可解析 | 计划本身非法,不能通过补业务事实绕过 | 修复 `plan-page-task` 输入或脚本输出 | 否 | 是 | | `PLAN_BLOCKED` | plan | `status=blocked`、`blockingIssues.length > 0` 或 `blockingReasons.length > 0` | 计划合法但事实不足 | 只补 `blockingIssues` / `blockingReasons` 指向的事实,不生成页面 | 视原因而定 | 是 | +| `PROJECT_INTEGRATION_INCOMPLETE` | plan | `facts.projectIntegration.integrationReady=false`,或 `blockingReasons` 明确提示 `project integration is incomplete` | 项目级接入事实未完成,不能把页面任务当成接入补丁入口 | 先执行 `bootstrap-target-project.mjs --target --mode ` 修复项目接入,再重新运行 plan | 部分 | 是 | | `MISSING_PAGE_TYPE` | plan | `blockingReasons` 包含 `missing pageType` | 页面类型未解析,不能直接当作非典型页处理 | 让用户确认页型,或补充需求后重新运行 plan | 否 | 是 | +| `LEGACY_CARRIER_CERTIFICATION_REQUIRED` | plan | `legacy-host-compatible` 命中 `carrier-first-required` 页型,且 planner 只找到 `direct-standard-component`,没有 ready 的 project-certified carrier | 当前 legacy 页型必须先完成项目级 carrier 承接认证,页面任务不能再继续生成或直接小改 | 先执行 `bootstrap-target-project.mjs --target --mode legacy-host-compatible`,补齐 / 认证项目级 carrier 后再重新运行 plan | 部分 | 是 | | `RUNTIME_BRIDGE_PROFILE_MISSING` | plan | legacy `page-component` 需要 runtime bridge,但 `runtimeBridgeProfile` 缺失或 `rules/runtime-bridged-component-matrix.json` 未命中 | 缺少 legacy 桥接页面组件的结构化 bridge profile,不能安全进入主链路 | 补齐 bridge profile,或显式退回合法 fallback | 部分 | 是 | | `RUNTIME_BRIDGE_SOURCE_UNRESOLVED` | plan | 选中的 runtime bridge profile 无法从 certification 解析 `certificationInputs.componentShell` | 无法确认真正的运行时页壳来源,不能生成 thin bridge 代码 | 修复 certification / profile 关联,或改用 project-certified carrier | 部分 | 是 | +| `MANAGED_DELIVERY_PATH_DOWNGRADED` | preflight/governance | planner / contract 已锁定 `generationStrategy=page-component`、`startFrom=page-component`,且 legacy `runtimeAdapterProof.status=available` / `generationProfile.runtimeBridgeStatus=available`,但实际源码回退成 compatibility 手写页、reference 默认页、自由 scaffold,或缺少 runtime bridge / slot adapter / selected component 证明 | 计划已经证明应该交付受管页面组件主链,但实现偷偷降级成了非受管交付物 | 回到 `start-page` / contract 选定的 page-component 主链,恢复 runtime bridge shell、slot adapter、selected component 与 source markers;若确需 fallback,必须先重新执行 `plan-page-task` 并拿到显式 fallback 计划 | 部分 | 是 | | `ROUTE_OWNER_MISSING` | plan/preflight | 目标页落在 `src/typical-page-reuse/**`,或 route ownership 不是业务路由 | 当前目标属于示例 gallery / smoke baseline,不是真实业务路由 | 改到 `src/pages//index.jsx|tsx` 或宿主业务目录 | 否 | 是 | | `SOURCE_MARKER_MISSING` | preflight | `checks[].id=sourceMarker` 且 `status=failed` | 源码缺少受管页面 source marker,无法确认页型与治理边界 | 使用受管起点重建,或补齐合法 source marker 后重跑 preflight | 部分 | 是 | | `CONTRACT_PAGE_TYPE_MISMATCH` | preflight | `checks[].id=contractPageType` 且 `status=failed` | 源码声明的新页型与旧 contract 不一致 | 执行 `write-contract-standard`,刷新合同后再 preflight | 是 | 是 | @@ -35,7 +38,13 @@ | `RUNTIME_BRIDGE_RESPONSIBILITY_DRIFT` | governance/preflight | bridge wrapper / slot adapter 接管 `shell`、`white-body`、`main-scroll`、`pagination`、`footer`,或把标准 region 翻译成宿主 look-alike primitives | 运行时桥接层越界,已不再是 thin bridge,而是在重写页面组件 | 收回到 runtime bridge / slot binding 职责边界,必要时退出 `page-component` 主链路 | 部分 | 是 | | `RUNTIME_BRIDGE_TEMPLATE_MISSING` | preflight | runtime bridge profile 声明了 wrapper / slot adapter 资产,但模板未 shipped 或路径缺失 | 计划已选择桥接页面组件,但配套桥接资产不完整 | 补齐并 ship 模板后重新执行 `start-page` / preflight | 是 | 是 | | `HIUI010_ADAPTER_SCHEMA_INVALID` | governance | adapter 纯 re-export、未注入默认值、承担 page owner,或 localBypass 与 AST 不匹配 | 第三方 UI 接入只是形式合规,不能保证典型页默认值与 ownership | 改为 Managed 组件 / 合法 adapter;记录 owner + expiry 的 local bypass | 部分 | 是 | +| `HIUI026_QUERY_FILTER_ROLE_DRIFT` | governance/preflight/runtime | 表格类页型把 `semanticContract.queryFilterRegionRole=table-query-filter` 漂移成 dashboard control strip、`SearchForm`、自由表单栅格或其它非真实 `QueryFilter` 语义;或数据可视化页在未显式改 contract 时反向漂移成真实列表筛选 | 筛选语义漂移会直接带来标签回流、控件节奏失真、`全部筛选/清空` 行为异常,常见表象就是筛选区域错乱或出现大段空隙 | 先回到 planner 的 `generationInputs.fastPathSummary.queryFilterPolicy`、`start-page` scaffold 和 `docs/generation/figma-pages/table-shared.md` 恢复真实 `QueryFilter` carrier;不要先打补丁 CSS 或顺势改成控制条 | 部分 | 是 | +| `HIUI027_TABLE_CARRIER_WRAPPER_DRIFT` | governance/preflight/runtime | 表格类页面或 project carrier 在 `QueryFilter` / `Table` / `pagination` 外额外引入 wrapper、padding、stretch、scroll owner 或样式覆写,导致白底主体、筛选区、表格区、分页区不再同属一个受管 shell 链 | 表格主体与搜索/分页之间出现异常空隙,通常说明外部 wrapper 抢走了 `pageShellPolicy` 对应的 ownership,而不是单纯 spacing 数值有误 | 回到 planner 的 `generationInputs.fastPathSummary.pageShellPolicy` 和 page-component / shell carrier 契约,收回 `white-body`、`query-filter`、`table`、`pagination` 到同一个受管壳;移除外部 style override / stretch wrapper | 部分 | 是 | | `HIUI005_QUERY_FILTER_FIELD_LOST` | governance | acceptance contract / source snapshot 要求的筛选字段在行内或抽屉中缺失 | 页面迁移丢失筛选能力或错误迁移字段位置 | 恢复字段,或用带 provenance 的 acceptance contract 说明迁移原因 | 部分 | 是 | +| `HIUI022_MANAGED_FILTER_CHAIN_MISSING` | governance/preflight | contract 要求 `query-filter` region,但源码没有真实 `QueryFilter` / 认可 host search shell 语义,或退回 `searchConfig.fields`、`SearchForm`、`getSearchFields`、手写 primitive 筛选栏 | 页面看起来还有搜索区,但已经脱离受管 `QueryFilter / FilterDrawer` 链路 | 恢复真实 `QueryFilter / FilterDrawer` 或认可的 host search shell;只有页面语义真的改变时才允许重写 contract | 部分 | 是 | +| `HIUI023_QUERY_FILTER_BASELINE_DRIFT` | governance/preflight/runtime | 行内 `QueryFilter` 打开 `showLabel`、偏离 contained baseline、关键词搜索退化成裸 `Input`、追加第二个“查询 / 重置”按钮,或“全部筛选”退回普通 `Button` | 筛选链路还在,但已偏离典型列表的受管基线,后续极易继续产生灰底丢失、节奏错位和操作重复 | 收回 no-label / contained / filled baseline,移除手工按钮,恢复 `FilterButton` / `FilterDrawer` 语义 | 部分 | 是 | +| `HIUI024_HOST_STYLE_CONTAMINATION` | governance/preflight/runtime | 页面本地样式、slot override 或 `.hi-v5-*` 选择器触达 `PageHeader`、`QueryFilter`、`Table` 等公共组件骨架 | 外部 / 本地样式污染了受管组件内部骨架,页头宽度、筛选灰底、按钮停靠或表格节奏都可能被静默挤坏 | 移除页面级骨架覆写,把几何、颜色和节奏责任收回 shared carrier / token bridge | 部分 | 是 | +| `HIUI025_LIST_WORKSPACE_WIDTH_OWNER_DRIFT` | governance/preflight/runtime | `outer-padding`、`white-body`、`main-scroll`、`table` region 或 project-certified carrier 抢占横向滚动,或使用 `max-content / min-content / fit-content` 等内容宽度策略 | 列表工作区不再随父容器自适应,筛选 / 表格容易被撑破或出现外层横向滚动 | 把横向滚动收回内层表格 wrapper,补 `min-width: 0` / `min-inline-size: 0` 等约束,让工作区回到 width-adaptive 链路 | 部分 | 是 | | `HIUI018_HEADER_LAYOUT_CONTRACT_MISSING` | governance/preflight | contract 声明 `header` region 或页级动作 owner 为 `PageHeader extra`,但缺少 `headerLayoutContract` | 页头承接链存在,但 stretch / right-dock / rhythm 缺少机器事实,不能证明共享页头真的可用 | 补齐 `headerLayoutContract`,并写明 carrier owner、stretch owner、actions docking、vertical rhythm owner 与 evidence | 是 | 是 | | `HIUI019_HEADER_CARRIER_WIDTH_COLLAPSED` | governance/runtime | header carrier 无法证明 `PageHeader` root 已 stretch,或运行时宽度明显小于承接槽宽度 | 页头根容器没有铺满,按钮容易挤在标题旁边 | 修复 shared carrier / host slot 的 stretch 责任,重新认证 carrier,并重跑 preflight / runtime smoke | 部分 | 是 | | `HIUI020_HEADER_EXTRA_NOT_RIGHT_DOCKED` | governance/runtime | `PageHeader extra` 没有停靠到 header 右边界,或被 wrapper 改造成标题旁 inline group | 页级按钮仍在 header 区,但没有按标准停靠到最右侧 | 收回到标准 `PageHeader extra` 承接链,修复 docking 证明后重跑 runtime smoke | 部分 | 是 | @@ -50,10 +59,13 @@ 1. `PLAN_INVALID` / `PLAN_SCHEMA_UNSUPPORTED`:停止,不读取大段文档,不实现。 2. `PLAN_BLOCKED`:只补事实;补齐后重新运行 `plan-page-task`。 + 若命中 `PROJECT_INTEGRATION_INCOMPLETE` 或 `LEGACY_CARRIER_CERTIFICATION_REQUIRED`,默认动作是 `ResolveBlockingFacts`,优先修复项目接入 / project-certified carrier,不要继续执行页面实现命令。 + 若同时命中 route ownership 等页面级 blocker,planner 的 `currentExecutionState` / `requiredCommands` 必须把项目级 blocker 排在前面;不得只暴露 `resolve-business-route-target` 而隐藏 `bootstrap-target-project`。 3. `CONTRACT_PAGE_TYPE_MISMATCH` / `PAGE_TYPE_MIGRATION_CONTRACT_STALE`:优先执行 `write-contract-standard`,不要按旧页型逐条修源码。 4. preflight `warnings[]` 不改变顶层 `status`,但 final report 必须展示。 5. hard profile 下 governance `blocking` 必须阻断完成态;`report-only` profile 的 blocking 只能进入风险报告,不得描述为已修复。 6. usage stats 失败不默认推翻页面状态,但不能静默吞掉。 +7. 命中 `HIUI026_QUERY_FILTER_ROLE_DRIFT` / `HIUI027_TABLE_CARRIER_WRAPPER_DRIFT` 时,优先修 planner / scaffold / carrier 事实源;不要先在业务页追加局部 CSS、空白占位或外层 wrapper 作为“临时修复”。 ## Acceptance Profile 规则 diff --git a/skills/hiui-design/rules/generation-rules.md b/skills/hiui-design/rules/generation-rules.md index 3fad50d9f..8a928c27f 100644 --- a/skills/hiui-design/rules/generation-rules.md +++ b/skills/hiui-design/rules/generation-rules.md @@ -62,6 +62,11 @@ - `strict-structure`:非典型页面、组合页、split / 主从 / 多区块工作台、页壳 / ownership / 滚动结构变化、复杂图表联动、真实权限 / 状态流 / 批量副作用。执行结构 gate、contract、必要 smoke 和更严格验收。 - `formal-acceptance`:发布 / 提测 / 合入 / 全量 gate / 无 warning / `source-gate` / `doctor` / `finalize-page` 是验收叠加层,不是快慢主判据;命中时在当前链路上追加正式验收命令。 +执行强度还要区分两件事: + +- `implementation` 升级:只有 `pageType`、`shell`、`ownership`、`split`、主工作区滚动链等结构事实变化时,才从轻量生成升级到 `strict-structure`。普通典型页旧系统升级若仍命中同一 `pageType` 且已存在 certified page component,不应因为“来自 legacy 页面”就自动升级成重结构改造。 +- `governance` 升级:迁移 / 重架构页、legacy 高风险 drift、正式验收 / 发布可以要求更重的 snapshot、acceptance contract、translation-map、source-gate、doctor 或 finalize-page,但这不等于一定要重写页面结构。 + ### 0.2 Typical Fast Path Gate 当 `mode` 为 `host-integration`、`rules-only`、`legacy-host-compatible`,或项目内把兼容接入称为 `host-compatible`,且用户需求清晰命中一个或多个槽位型典型页型、没有特殊布局 / 非典型 section / 跨页型组合 / 复杂新增图表 / 正式 i18n 验收要求时,默认走典型页快速路径。`data-visualization` 不进入普通快速路径;它是 `managed-analytics` 受管分析页。 @@ -78,6 +83,13 @@ - 保留页壳、region 层级、白底主体、滚动 owner、分页 / footer 挂载语义、row action link 语义、source marker 和 contract 字段。 - 业务槽位里的自定义 JSX 与页面本地样式仍必须遵守 `hiui5-visual-baseline.md`:直接写出的 `font-size` 只能使用已登记字阶,尤其不得新增 `13px`;`24px` 只作为指标主值 token 使用。 +当普通典型页命中 fast path 时,执行动作默认进一步细分为: + +- `slot-fill-only`:当前页已经是受管实例,且本次需求没有修改 `pageType`、`shell`、`ownership`、`mandatory components` 或 runtime shell carrier;此时只允许替换业务槽位。 +- `rewrite-by-page-component`:当前页仍属于同一典型页型、project-certified carrier 或 certified page component 已可用,但现有源码还不是 ready 的 managed instance;此时应直接以 page component / carrier 重建业务页实例,而不是在旧 JSX 上继续修补壳层、region 或 ownership。 + +`rewrite-by-page-component` 仍属于典型页主链,不是 `strict-structure`。它表达的是“用受管主资产重建业务页实例”,而不是“重做页面身份或自由改写页面结构”。 + 当 `generationStrategy=run-fast-path-per-page-unit` 时,按 `pageUnits` 逐页执行同一规则,并使用每个 `pageUnits[].startFrom` 决定起点;若任一 `pageUnit` 是 `data-visualization`,该单元走 `standard-typical` 而不是普通快速路径;不要因为一次需求包含多个典型页,就补读非典型文档或手写一个组合壳。 快速路径不是“降低结构要求”。它只减少实现前的冗长判断与全量文档读取:页面仍必须能回答当前模板来源、业务槽位替换范围、关键 ownership、当前页预览结果和当前页 preflight / lint / build 结果。全量 `finalize-page`、source gate、doctor、runtime smoke 保留为正式验收 / 发布链路,默认不被无关历史页面问题阻塞快速生成。 @@ -96,7 +108,7 @@ - `rules-only` / `host-integration`:通常从登记模板、reference 或标准 shell 起步;默认只做业务槽位替换和轻量 `slot-gate` + `preflight`。若改动越过槽位边界、修改锁定区域或出现来源异常,升级到 source gate。 - `legacy-host-compatible`:普通典型页默认先消耗 standard certified page component + `runtimeAdapterProof`;只有 fallback / drift 风险显式触发时,才进入宿主 archetype、`reference-or-scaffold` 或 adapter scaffold 等 fallback 起点,并按需追加 `translation-map` 这类治理增强工件;此时仍必须具备更强 source / adapter 证明,防止 adapter 演变成自由手写页面。 -- 非典型 / overlay / 页型迁移:必须声明受管基础模具、增量范围与能力缺口;没有受管生成方式时 fail closed。 +- 非典型 / overlay / 页型迁移:必须声明受管基础模具、增量范围与能力缺口;没有受管生成方式时 fail closed。非典型升级保留的是 `base archetype`、`layout strategy`、一级信息架构与 ownership contract,不是保留旧系统 DOM、旧样式类名或未受管布局实现后只换 token。 禁止把 `preflight` 或截图当成主生产方式。生成阶段不得先创建空白 `div` 页面,再用注释、`data-hiui5-*` marker、隐藏标准壳或 contract 字段补成“看似合规”的页面。关键结构只能来自受管模具、真实宿主 archetype、认证 shell carrier 或认证 adapter;AI 只填标题、字段、列、按钮、接口、mock 数据和明确的交互槽位。 @@ -200,6 +212,13 @@ 2. **漂移风险触发门禁**:缺少合格 host archetype、首次为该宿主适配页型、host archetype 与 reference region 不完全一致、需要新增 / 修改 adapter、`startFrom=reference-or-scaffold|scaffold`、涉及 `table-stat` / `tree-split` / drawer / full-page、修改 ownership / shell carrier、出现 preflight / source marker warning、需要声明 `driftExceptions` 时,必须展开 Translation Drift Guard。 3. **正式验收增强门禁**:发布、提测、合入、无 warning、用户要求 `source-gate` / `doctor` / `runtime-smoke`,或页面涉及 adapter / ownership / shell carrier 实质修改时,必须基于最新源码补齐 `finalize-page`、source gate、doctor、runtime smoke 与完整 contract。 +legacy 典型页旧系统升级默认还要区分: + +- `rewrite-by-page-component`:当前业务页仍命中同一 `pageType`,planner 已选中 certified page component / project carrier,但源码还未成为 ready managed instance;这类任务应优先重建业务页实例,不继续修补旧壳层。 +- `structural-upgrade`:只有 `pageType`、`shell`、`ownership`、`split`、主工作区结构、独立滚动链或 mandatory components 发生变化时,才升级为重结构改造。 + +不要把 migration / formal acceptance 自动等价成 `structural-upgrade`。这些场景首先升级的是治理强度,而不是页面结构改造强度。 + legacy 的最小 runtime adapter / host boundary proof 必须能命名: - `adapterId`、`status`、`kind=legacy-runtime-adapter` 与 `responsibility=runtime-bridge-only` diff --git a/skills/hiui-design/rules/legacy-host-hard-gates.json b/skills/hiui-design/rules/legacy-host-hard-gates.json index d6945b19f..37ef98724 100644 --- a/skills/hiui-design/rules/legacy-host-hard-gates.json +++ b/skills/hiui-design/rules/legacy-host-hard-gates.json @@ -16,12 +16,23 @@ { "id": "shell-ownership", "label": "shell ownership stable", - "requirement": "shell ownership, carrier owner, and main scroll owner may not drift into the business page" + "requirement": "shell ownership, carrier owner, and main scroll owner may not drift into the business page", + "driftSignals": [ + "WHITE_BODY_OWNER_DRIFT", + "MAIN_SCROLL_OWNER_DRIFT", + "PAGINATION_MOUNT_DRIFT", + "QUERY_FILTER_CARRIER_DRIFT" + ] }, { "id": "governed-business-slots", "label": "business edits stay in governed slots", - "requirement": "business implementation may only fill governed slots and may not synthesize local shell/header/layout owners" + "requirement": "business implementation may only fill governed slots and may not synthesize local shell/header/layout owners", + "driftSignals": [ + "TOP_NAV_REGION_DRIFT", + "QUERY_FILTER_CARRIER_DRIFT", + "PAGINATION_MOUNT_DRIFT" + ] } ], "conditionalHardGates": [ @@ -54,11 +65,12 @@ "notes": [ "Baseline hard gates stay at 4.", "When header is part of the governed page structure, the conditional header gate is added and the effective gate count becomes 5.", - "Current shipped ordinary typical page molds usually include header, so maintainers should expect 5 effective hard gates in most legacy page plans." + "Current shipped ordinary typical page molds usually include header, so maintainers should expect 5 effective hard gates in most legacy page plans.", + "The new drift signals are scoped to legacy-host-compatible ownership hardening; they refine existing shell-ownership and governed-business-slots gates instead of creating new baseline gates." ], "sourceRefs": { "skill": "SKILL.md", - "generationGates": "references/generation-gates.md", + "generationGates": "docs/generation/explainers/generation-gates.md", "pageGovernance": "rules/page-governance.md", "contractRegions": "rules/contract-regions.md" } diff --git a/skills/hiui-design/rules/legacy-page-type-delivery-policy.json b/skills/hiui-design/rules/legacy-page-type-delivery-policy.json index 1f3dea8c2..4672deb4b 100644 --- a/skills/hiui-design/rules/legacy-page-type-delivery-policy.json +++ b/skills/hiui-design/rules/legacy-page-type-delivery-policy.json @@ -14,8 +14,8 @@ }, { "pageTypeId": "tree-table", - "deliveryPolicy": "direct-standard-allowed", - "reason": "single-workspace inline-tree tables can remain on the direct standard component path when runtime bridge proof is already available" + "deliveryPolicy": "carrier-first-required", + "reason": "tree-table pages still carry managed table/query-filter/detail ownership, so legacy projects should freeze the runtime shell through a project-certified carrier before business slot fill" }, { "pageTypeId": "tree-split", @@ -29,8 +29,8 @@ }, { "pageTypeId": "drawer-detail", - "deliveryPolicy": "direct-standard-allowed", - "reason": "detail drawer pages can keep using the direct standard component path while project carriers are still optional" + "deliveryPolicy": "carrier-first-required", + "reason": "drawer-detail pages should keep drawer shell and Descriptions baseline on a shared project carrier so detail typography and body ownership do not drift page by page" }, { "pageTypeId": "feedback-status", @@ -44,10 +44,24 @@ }, { "pageTypeId": "full-page-detail", - "deliveryPolicy": "direct-standard-allowed", - "reason": "detail shells can stay on the direct standard component path when no project-specific host carrier is required" + "deliveryPolicy": "carrier-first-required", + "reason": "full-page-detail owns header/detail-body reading rhythm and Descriptions baseline, so legacy rollout should require a project-certified carrier instead of relying on direct standard mounting" } ], + "requiredLegacyPageTypes": [ + "table-basic", + "table-stat", + "tree-table", + "tree-split", + "drawer-form", + "drawer-detail", + "full-page-edit", + "full-page-detail" + ], + "deferredLegacyPageTypes": [ + "feedback-status", + "data-visualization" + ], "allowedPolicies": [ "direct-standard-allowed", "carrier-first-required", @@ -57,6 +71,7 @@ "notes": [ "This file only applies to legacy-host-compatible delivery policy planning.", "It does not create a parallel generation family; page-component remains the normalized primary chain.", - "Projects may still override or extend these defaults through project facts, but planner decisions must remain policy-driven." + "Projects may still override or extend these defaults through project facts, but planner decisions must remain policy-driven.", + "legacy-host-compatible rollout should finish the required table/edit/detail carrier batch before feedback-status or data-visualization follow-up work." ] } diff --git a/skills/hiui-design/rules/page-governance.md b/skills/hiui-design/rules/page-governance.md index b636fd33b..aa4fcd0ab 100644 --- a/skills/hiui-design/rules/page-governance.md +++ b/skills/hiui-design/rules/page-governance.md @@ -46,6 +46,26 @@ - header layout proof 只存在说明文档、截图或自然语言解释里,而没有 contract / certification / host facts / runtime smoke 证据时,P0 失败。 +### Managed Filter Chain Lock + +对表格类页面,`query-filter` 不是“页面上有一排搜索控件”就算通过,而是必须证明页面仍走受管筛选链路: +行内筛选由真实 `QueryFilter` 承接,全部筛选由 `FilterDrawer` / `FilterButton` 承接,关键词搜索保持 +共享 `SearchInput` / filled 搜索语义,筛选与表格继续处在同一个 width-adaptive white-body 工作区内。 +业务页只能填筛选字段与业务映射,不能重写筛选骨架、按钮节奏、字段标签行或宿主宽度链。 + +硬门槛: + +- contract / pageType 要求 `query-filter` region,但源码没有 `QueryFilter` 或认可的 host search shell 语义,P0 失败。 +- 页面把筛选区退回成 `searchConfig.fields`、`SearchForm`、`getSearchFields`,或手写 `Input + Select + DatePicker` + 组合栏,P0 失败。 +- 行内 `QueryFilter` 开启 `showLabel`、把 `appearance` 改离 shared contained baseline、把关键词搜索退化成裸 + `Input`、追加第二个“查询 / 搜索 / 重置”按钮、或把“全部筛选”改成普通 `Button`,在 strict rollout / + managed list 主链下按 P0 失败。 +- 页面本地样式或 slot override 触达 `.hi-v5-query-filter*`、`.hi-v5-page-header*`、`.hi-v5-table*` + 等公共组件骨架,按 host-style contamination 处理,P0 失败。 +- `outer-padding`、`white-body`、`main-scroll`、`table` region 或 project-certified carrier 抢占横向滚动、 + 以 `max-content` / `fit-content` 一类内容宽度关键字定宽,导致筛选 / 表格工作区不再自适应父容器,P0 失败。 + ### Non-typical Strategy Proof 非典型 / overlay 页面不得只靠 `topology=non-typical-overlay` 或注释 marker 自证。它必须在计划、contract、源码 marker 与运行时 marker 中保持同一套 `layout strategy` / `layout archetype` / `non-typical scope` / `composition guardrails` / `strategy evidence`。 @@ -326,6 +346,10 @@ npm run typical-page:guard -- --page --profile migration --json - `HIUI015_SOURCE_CAPABILITY_REGRESSION` - `HIUI016_LOW_CONFIDENCE_SNAPSHOT` - `HIUI017_REQUIREMENT_WITHOUT_PROVENANCE` +- `HIUI022_MANAGED_FILTER_CHAIN_MISSING` +- `HIUI023_QUERY_FILTER_BASELINE_DRIFT` +- `HIUI024_HOST_STYLE_CONTAMINATION` +- `HIUI025_LIST_WORKSPACE_WIDTH_OWNER_DRIFT` ## Runtime Governance @@ -340,7 +364,9 @@ Runtime smoke 优先使用 DOM / layout relationship assertions,截图只作 | `new-managed` | merge 前 required | | `release` | hard fail | -典型断言:筛选字段真实可见、搜索框灰底、页头按钮右侧和 `md` 尺寸、空态与分页距离、pagination bottom-docked、表格横向滚动不破坏布局。 +典型断言:筛选字段真实可见、行内筛选仍是 `QueryFilter`、全部筛选仍是 `FilterDrawer` / +`FilterButton`、关键词搜索保持灰底 filled baseline、页头按钮右侧和 `md` 尺寸、空态与分页距离、 +pagination bottom-docked、筛选 / 表格工作区仍随父容器自适应、表格横向滚动不破坏布局。 ## I18n Rollout diff --git a/skills/hiui-design/rules/runtime-contract.md b/skills/hiui-design/rules/runtime-contract.md index 2842389f2..6a6f1cb6e 100644 --- a/skills/hiui-design/rules/runtime-contract.md +++ b/skills/hiui-design/rules/runtime-contract.md @@ -115,6 +115,7 @@ - `primaryGenerationAsset.type` 仍为 `page-component` - `assetResolution.strategy` 仍为 `page-component` - 下游消费方一律按 `normalizedId` 判定主链路,不得因为用户可读 id 新增而分叉为平行体系 +- legacy 主树若禁止 ad hoc direct shell import,只影响 standard shell 的直接挂载方式;不得据此把 `normalizedId=page-component` 的 ready 计划重新解释成默认 fallback 或 hand-built compatibility page 计划可附加 `runtimeBridgeProfile` 作为 additive object,用于说明 legacy 桥接交付语义。推荐结构: @@ -154,6 +155,18 @@ 翻译为宿主 look-alike primitives,应视为 contract drift 并 fail closed。 - 若 `generationStrategy.id=runtime-bridged-page-component`,但 `runtimeBridgeProfile` 缺失、 profile 不可解析或 runtime shell 来源不可解,则计划必须 `blocked`。 +- 若 `generationStrategy.normalizedId=page-component`、`runtimeBridgeProfile.status=available` + 且 `runtimeAdapterProof.status=available`,执行层不得因为 direct shell import 在 legacy 主树中被禁止,就擅自降级到 reference 默认翻译、兼容手拼页或自由 fallback。 +- 当 legacy `page-component + runtime bridge + slot fill` 主链成立时,planner 应通过 + `generationInputs` 与 `inlineChecks` 暴露 bridge ownership facts,而不是把这些事实推迟到 + runtime 排障阶段才解释。第一批推荐事实只限:`white-body owner`、`main-scroll owner`、 + `pagination owner`、`query-filter carrier` 与 `bodyTopNavigation/header tabs` 的合法落位策略; + 这些字段只在 `legacy-host-compatible` 下补充,不升级成全模式通用 contract。 +- 对 `legacy-host-compatible` 的表格类 `page-component`,planner 还可以追加轻量 + `styleRiskSignals` 引用,用来提示 legacy `StyleBoundary` 风险事实,例如 + `QueryFilter/SearchInput` 皮肤链漂移、全局 `.hi-v5-input*` 污染或需要额外的 runtime style probe。 + 该对象只做 risk signal / probe hint,不承担完整视觉 contract;它与 ownership facts 一样只在 + `legacy-host-compatible` 下补充,不升级成全模式通用 schema。 ## Structured Actions @@ -400,6 +413,12 @@ Profile 语义: "preflightStatus": "passed", "usageStatsStatus": "completed" }, + "preflightExecution": { + "preflightStage": "implementation", + "readyForImplementation": true, + "readyForDelivery": true, + "deferredChecks": [] + }, "evidence": { "executedActions": [], "changedFiles": [], @@ -413,6 +432,11 @@ Profile 语义: } ``` +补充规则: + +- `preflightExecution` 是 additive status object,用于把 `preflightStage`、`readyForImplementation`、`readyForDelivery` 与 `deferredChecks` 暴露给最终回复。 +- 当 `preflightStatus=passed` 但 `readyForDelivery=false` 时,最终回复必须继续披露 `deferredChecks`,避免把 `scaffold-baseline` 误报成已可交付页面。 + ## Public Contract Fixtures 公开机器契约由 `scripts/public-cli-contracts.json` 维护,并通过 `scripts/tests/public-cli-contracts.test.mjs` 校验。当前相关 fixture: diff --git a/skills/hiui-design/rules/validation-checklist.md b/skills/hiui-design/rules/validation-checklist.md index b2282f46f..1b5f619c2 100644 --- a/skills/hiui-design/rules/validation-checklist.md +++ b/skills/hiui-design/rules/validation-checklist.md @@ -117,7 +117,7 @@ - 风险/状态类标签复用 `Tag` 或 shared status renderer;不存在业务页手写 `span + 胶囊背景色` - 可点击的状态筛选若本质仍是状态语义切换,继续复用 `Tag` / shared status renderer,并在交互层补 click / keyboard;不要因为“可点击”就回退成 `Button` - `PageHeader`、`QueryFilter`、`Table`、`Descriptions`、`Timeline`、`Empty`、`Progress`、`Steps` 的骨架层没有被业务页重写成另一套结构 -- 业务页没有通过 `classNames`、`styles`、slot semantic override 或本地 `.hi-v5-*` 选择器去改 `PageHeader`、`Descriptions`、`Timeline`、`Table` 等公共组件骨架 +- 业务页没有通过 `classNames`、`styles`、slot semantic override 或本地 `.hi-v5-*` 选择器去改 `PageHeader`、`QueryFilter`、`Descriptions`、`Timeline`、`Table` 等公共组件骨架 - detail-shell 的 vertical `Descriptions` 没有依赖隐藏默认值;关键布局不变量在源码边界显式冻结,而不是靠浏览器默认 `th` 行为或第三方 bridge 默认值碰巧成立 - 若页面使用 `SchemaDescriptionsBridge`,已显式写出 `labelPlacement: 'left'`,并清空 bridge 继承的固定 `labelWidth`;没有把 left 对齐修补下放到页面级 CSS - `ProDetailPage`、`ProListPageProvider`、`ProDetailPageProvider` 等页壳运行时要求仍被满足;不存在只保留外观、漏掉 provider / context 链的实现 @@ -134,11 +134,15 @@ - 行内筛选真实使用 `QueryFilter` - 全部筛选真实使用 `FilterDrawer` +- 行内 `QueryFilter` 保持 no-label + contained baseline;没有打开 `showLabel`,也没有改成 `line` 等偏离共享基线的 appearance - 行内字段集合、抽屉字段集合、可选字段集合与 `page-acceptance-contract.v1` / `fieldVisibility` 一致;字段从行内迁移到抽屉时有 provenance 和变更原因 - 迁移页没有静默丢失 source snapshot 中记录的筛选字段、页头动作、表格列、分页位置或空态行为 - 真实预览中能看到关键词搜索框、至少一个筛选控件、`全部筛选` 与 `重置` +- 关键词搜索位保持 `SearchInput` / filled + search-icon 语义;没有退化成裸 `Input`,搜索框灰底表现没有丢失 - split 左栏若直接使用 `SearchInput`,真实预览中搜索框已贴合 pane 宽度;没有暴露组件默认固定宽度导致的“搜索框未贴满左栏” - 不存在手工追加的“查询”主按钮 +- `QueryFilter` append 区没有再补第二个“查询 / 搜索 / 重置”按钮;清空 / 重置仍走受管筛选链路 +- “全部筛选”保持 `FilterButton` / `FilterDrawer` 语义,而不是普通 `Button` - 没有回退到 schema 搜索壳、`searchConfig.fields`、`SearchForm`、`getSearchFields` 或手写筛选栏 - `Table` 默认开启 `resizable` - `Table` 默认开启字段管理 @@ -147,6 +151,7 @@ - 若页面右侧存在需要冻结的操作列,列配置固定与 wrapper 级 `fixedToColumn` 已同步声明 - 表格普通数据列默认保持单行;复合单元格、状态胶囊自定义渲染或多按钮行操作若存在,结果中已有书面批准依据 - 表格 row action 保持 HiUI link 语义且单行 +- `outer-padding`、`white-body`、`main-scroll`、`table` region 没有抢占横向滚动或用内容宽度关键字定宽;筛选 / 表格工作区仍随父容器自适应 - 主白底主体从筛选 / 表格 / 分页连续延伸;若示例页本身采用吸底分页,则分页仍处于同白底主体底部 - 若页面使用真实 `QueryFilter`,控制台不存在 `FieldsContext` / `ValueType` 缺失类 warning diff --git a/skills/hiui-design/scripts/check-public-cli-fixture-distribution.mjs b/skills/hiui-design/scripts/check-public-cli-fixture-distribution.mjs deleted file mode 100644 index b0df14743..000000000 --- a/skills/hiui-design/scripts/check-public-cli-fixture-distribution.mjs +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env node - -import fs from 'node:fs/promises' -import path from 'node:path' -import process from 'node:process' -import { fileURLToPath } from 'node:url' -import { - getDistributionTarget, - readDistributionManifest, - selectFilesForTarget, - walkFiles, -} from './lib/distribution-manifest.mjs' - -function parseArgs(argv) { - const options = { - json: false, - root: '', - target: 'open-source-package', - } - - for (let index = 0; index < argv.length; index += 1) { - const arg = argv[index] - - if (arg === '--json') { - options.json = true - continue - } - - if (arg === '--root' || arg === '--target') { - const value = argv[index + 1] - if (!value || value.startsWith('--')) { - throw new Error(`Missing value for ${arg}`) - } - if (arg === '--root') options.root = value - if (arg === '--target') options.target = value - index += 1 - continue - } - - if (arg === '--help' || arg === '-h') { - console.log(`Usage: node scripts/check-public-cli-fixture-distribution.mjs [--root ] [--target ] [--json] - -Fails closed when shipped machine-public CLI fixtures declared in scripts/public-cli-contracts.json -are missing from the source tree or excluded from the selected distribution target. -`) - process.exit(0) - } - - throw new Error(`Unknown argument: ${arg}`) - } - - return options -} - -async function readJson(targetPath) { - return JSON.parse(await fs.readFile(targetPath, 'utf8')) -} - -function getMachinePublicFixtures(contracts) { - return [...new Set( - (contracts.tiers?.['machine-public'] || []) - .filter((entry) => entry.status === 'shipped' && entry.jsonContract === 'shipped') - .map((entry) => String(entry.fixture || '').trim()) - .filter(Boolean) - )].sort((left, right) => left.localeCompare(right)) -} - -function buildReport({ fixtures, missingFromSource, missingFromTarget, root, targetName }) { - return { - status: missingFromSource.length === 0 && missingFromTarget.length === 0 ? 'passed' : 'failed', - root, - target: targetName, - shippedFixtureCount: fixtures.length, - fixtures, - missingFromSource, - missingFromTarget, - } -} - -function printReport(report, asJson) { - if (asJson) { - console.log(JSON.stringify(report, null, 2)) - return - } - - console.log(`public-cli fixture distribution: ${report.status}`) - console.log(`- root: ${report.root}`) - console.log(`- target: ${report.target}`) - console.log(`- shipped fixtures: ${report.shippedFixtureCount}`) - if (report.missingFromSource.length > 0) { - console.log(`- missing from source: ${report.missingFromSource.join(', ')}`) - } - if (report.missingFromTarget.length > 0) { - console.log(`- missing from target: ${report.missingFromTarget.join(', ')}`) - } -} - -async function main() { - const options = parseArgs(process.argv.slice(2)) - const scriptDir = path.dirname(fileURLToPath(import.meta.url)) - const root = path.resolve(options.root || path.join(scriptDir, '..')) - const contractsPath = path.join(root, 'scripts', 'public-cli-contracts.json') - const manifestPath = path.join(root, 'distribution-manifest.json') - const contracts = await readJson(contractsPath) - const fixtures = getMachinePublicFixtures(contracts) - const manifest = await readDistributionManifest(manifestPath) - const target = getDistributionTarget(manifest, options.target) - const files = await walkFiles(root) - const selectedFiles = new Set(selectFilesForTarget(files, target)) - const fileSet = new Set(files) - - const missingFromSource = fixtures.filter((fixturePath) => !fileSet.has(fixturePath)) - const missingFromTarget = fixtures.filter((fixturePath) => !selectedFiles.has(fixturePath)) - const report = buildReport({ - fixtures, - missingFromSource, - missingFromTarget, - root, - targetName: options.target, - }) - - if (report.status !== 'passed') { - throw new Error( - `Shipped machine-public fixtures are not fully distributed for ${options.target}: ${JSON.stringify({ - missingFromSource, - missingFromTarget, - })}` - ) - } - - printReport(report, options.json) -} - -main().catch((error) => { - const message = error instanceof Error ? error.message : String(error) - if (process.argv.includes('--json')) { - console.error(JSON.stringify({ error: { message } }, null, 2)) - } - console.error(`check-public-cli-fixture-distribution failed: ${message}`) - process.exit(1) -}) diff --git a/skills/hiui-design/scripts/lib/detection/typical-page-candidates.mjs b/skills/hiui-design/scripts/lib/detection/typical-page-candidates.mjs index 154066131..2c548ffe1 100644 --- a/skills/hiui-design/scripts/lib/detection/typical-page-candidates.mjs +++ b/skills/hiui-design/scripts/lib/detection/typical-page-candidates.mjs @@ -438,18 +438,6 @@ export function inspectTypicalPageChanges(changedFiles) { declaresAlignment, }) - const missingContractPaths = [contractEntry.contractJsonPath, contractEntry.contractMdPath].filter( - (contractPath) => !changedFileSet.has(contractPath) - ) - - if (missingContractPaths.length > 0) { - failures.push( - `${filePath} changed without updating its contract files: ${missingContractPaths.join( - ', ' - )}. Re-run npm run typical-page:finalize-page -- ...` - ) - } - if (!isFinalizedContractWorkflow(contractEntry.contract)) { failures.push( `${filePath} is registered but its contract workflow is not finalized. Re-run npm run typical-page:finalize-page -- ... so CI sees source/doctor PASS on the current page snapshot.` @@ -471,6 +459,22 @@ export function inspectTypicalPageChanges(changedFiles) { failures.push( `${filePath} changed after its last finalize-page result. Contract snapshot ${recordedSnapshotHash} does not match current source snapshot ${currentSnapshot.hash}. Re-run npm run typical-page:finalize-page -- ... on the latest source.` ) + } else { + const missingContractPaths = [ + contractEntry.contractJsonPath, + contractEntry.contractMdPath, + ].filter((contractPath) => !changedFileSet.has(contractPath)) + + if ( + missingContractPaths.length > 0 && + missingContractPaths.some((contractPath) => !contractPath.startsWith(CONTRACTS_DIR)) + ) { + failures.push( + `${filePath} changed without updating its contract files: ${missingContractPaths.join( + ', ' + )}. Re-run npm run typical-page:finalize-page -- ...` + ) + } } const runtimeSmokeRequired = diff --git a/skills/hiui-design/scripts/lib/distribution-addresses.mjs b/skills/hiui-design/scripts/lib/distribution-addresses.mjs index 9c86e60df..ba9c1f327 100644 --- a/skills/hiui-design/scripts/lib/distribution-addresses.mjs +++ b/skills/hiui-design/scripts/lib/distribution-addresses.mjs @@ -52,15 +52,37 @@ async function resolveOfficialTeamPackagePath(skillRoot) { return '' } - const defaultsPath = typeof registry.defaults?.teamPackage === 'string' - ? registry.defaults.teamPackage.trim() + return resolveConcreteAddress(registry, 'teamPackage') +} + +async function resolveOfficialMaintainerSourcePath(skillRoot) { + const registry = await readDistributionAddresses(skillRoot) + if (!registry || typeof registry !== 'object') { + return '' + } + + return resolveConcreteAddress(registry, 'maintainerSource') +} + +async function resolveOfficialOpenSourcePackagePath(skillRoot) { + const registry = await readDistributionAddresses(skillRoot) + if (!registry || typeof registry !== 'object') { + return '' + } + + return resolveConcreteAddress(registry, 'openSourcePackage') +} + +function resolveConcreteAddress(registry, key) { + const defaultsPath = typeof registry.defaults?.[key] === 'string' + ? registry.defaults[key].trim() : '' if (isConcretePath(defaultsPath)) { return defaultsPath } - const examplePath = typeof registry.localExamples?.teamPackage === 'string' - ? registry.localExamples.teamPackage.trim() + const examplePath = typeof registry.localExamples?.[key] === 'string' + ? registry.localExamples[key].trim() : '' return isConcretePath(examplePath) ? examplePath : '' } @@ -72,5 +94,7 @@ function isConcretePath(value) { export { LOCAL_OVERRIDE_FILE, readDistributionAddresses, + resolveOfficialMaintainerSourcePath, + resolveOfficialOpenSourcePackagePath, resolveOfficialTeamPackagePath, } diff --git a/skills/hiui-design/scripts/lib/distribution-rules.mjs b/skills/hiui-design/scripts/lib/distribution-rules.mjs index be7e35f48..57fc025f8 100644 --- a/skills/hiui-design/scripts/lib/distribution-rules.mjs +++ b/skills/hiui-design/scripts/lib/distribution-rules.mjs @@ -4,6 +4,9 @@ const COMMON_EXCLUDE_PATTERNS = [ '.git/**', '.DS_Store', '**/.DS_Store', + '.backup-*', + '.backup-*/*', + '.backup-*/**', '*.bak', '*.bak-*', '**/*.bak', @@ -16,6 +19,9 @@ const COMMON_EXCLUDE_PATTERNS = [ '.release.env.local', '.env.release', '.env.release.local', + 'tmp', + 'tmp/*', + 'tmp/**', 'outputs/*', 'outputs/**', ] @@ -72,6 +78,7 @@ const RUNTIME_MIRROR_ALLOWED_USAGE_BACKEND_PATTERNS = [ 'scripts/init-stats-identity.mjs', 'scripts/log-usage.mjs', 'scripts/report-preview-ready-usage.mjs', + 'scripts/sync-team-package.mjs', 'scripts/typical-page-preview-ready.mjs', 'scripts/lib/log-usage-event.mjs', 'scripts/lib/stats-client.mjs', @@ -79,6 +86,16 @@ const RUNTIME_MIRROR_ALLOWED_USAGE_BACKEND_PATTERNS = [ 'scripts/lib/stats-identity.mjs', ] +const RUNTIME_MIRROR_EXTRA_BLOCKED_PATTERNS = [ + 'scripts/tests/**/*.test.mjs', +] + +const TEAM_PACKAGE_EXTRA_BLOCKED_PATTERNS = [ + 'config/stats.default.env', + 'scripts/typical-page-preview-ready.mjs', + 'scripts/tests/**/*.test.mjs', +] + const TEAM_SYNC_ONLY_EXCLUDE_PATTERNS = [ ...MAINTAINER_ONLY_PATTERNS, ] @@ -90,6 +107,12 @@ const OPEN_SOURCE_PACKAGE_ALLOWED_PATTERNS = [ const OPEN_SOURCE_PACKAGE_EXTRA_BLOCKED_PATTERNS = [ ['docs/onboarding/', 'usage-', 'stats.md'].join(''), ['docs/onboarding/', 'usage-', 'stats-contract.md'].join(''), + 'config/stats.default.env', + 'docs/business-lines', + 'docs/business-lines/*', + 'docs/business-lines/**', + 'docs/onboarding/one-click.md', + 'scripts/tests/**/*.test.mjs', ] const PROJECT_IMAGE_BINARY_BLOCKED_PATTERNS = [ @@ -160,7 +183,12 @@ function getArchiveExcludePatterns({ includeMaintainerFiles = false } = {}) { } function getTeamDistributionBlockedPatterns() { - return [...COMMON_EXCLUDE_PATTERNS, ...TEAM_SYNC_ONLY_EXCLUDE_PATTERNS, ...PROJECT_IMAGE_BINARY_BLOCKED_PATTERNS] + return [ + ...COMMON_EXCLUDE_PATTERNS, + ...TEAM_SYNC_ONLY_EXCLUDE_PATTERNS, + ...TEAM_PACKAGE_EXTRA_BLOCKED_PATTERNS, + ...PROJECT_IMAGE_BINARY_BLOCKED_PATTERNS, + ] } function getRuntimeMirrorBlockedPatterns() { @@ -168,6 +196,7 @@ function getRuntimeMirrorBlockedPatterns() { return [ ...COMMON_EXCLUDE_PATTERNS, ...TEAM_SYNC_ONLY_EXCLUDE_PATTERNS.filter((pattern) => !allowed.has(pattern)), + ...RUNTIME_MIRROR_EXTRA_BLOCKED_PATTERNS, ...PROJECT_IMAGE_BINARY_BLOCKED_PATTERNS, ] } @@ -188,7 +217,9 @@ export { OPEN_SOURCE_PACKAGE_ALLOWED_PATTERNS, OPEN_SOURCE_PACKAGE_EXTRA_BLOCKED_PATTERNS, PROJECT_IMAGE_BINARY_BLOCKED_PATTERNS, + RUNTIME_MIRROR_EXTRA_BLOCKED_PATTERNS, RUNTIME_MIRROR_ALLOWED_USAGE_BACKEND_PATTERNS, + TEAM_PACKAGE_EXTRA_BLOCKED_PATTERNS, TEAM_SYNC_ONLY_EXCLUDE_PATTERNS, getArchiveExcludePatterns, getOpenSourcePackageBlockedPatterns, diff --git a/skills/hiui-design/scripts/lib/managed-page-artifacts.mjs b/skills/hiui-design/scripts/lib/managed-page-artifacts.mjs index 4951d5ec9..546c82ee8 100644 --- a/skills/hiui-design/scripts/lib/managed-page-artifacts.mjs +++ b/skills/hiui-design/scripts/lib/managed-page-artifacts.mjs @@ -145,6 +145,16 @@ function normalizeRegistryEntry(entry) { scrollStrategy: String(entry?.scrollStrategy || '').trim(), workflowStatus: String(entry?.workflowStatus || '').trim(), preflightStatus: String(entry?.preflightStatus || '').trim(), + preflightStage: String(entry?.preflightStage || '').trim(), + readyForImplementation: typeof entry?.readyForImplementation === 'boolean' + ? entry.readyForImplementation + : false, + readyForDelivery: typeof entry?.readyForDelivery === 'boolean' + ? entry.readyForDelivery + : false, + deferredChecks: Array.isArray(entry?.deferredChecks) + ? entry.deferredChecks.map((item) => String(item || '').trim()).filter(Boolean) + : [], sourceGateStatus: String(entry?.sourceGateStatus || '').trim(), doctorStatus: String(entry?.doctorStatus || '').trim(), runtimeSmokeRequired: Boolean(entry?.runtimeSmokeRequired), @@ -504,6 +514,12 @@ export function buildManagedPageRegistryEntry(contract) { scrollStrategy: String(contract?.scrollStrategy || '').trim(), workflowStatus: String(contract?.workflow?.status || '').trim(), preflightStatus: String(contract?.workflow?.preflightStatus || '').trim(), + preflightStage: String(contract?.workflow?.preflightStage || '').trim(), + readyForImplementation: contract?.workflow?.readyForImplementation === true, + readyForDelivery: contract?.workflow?.readyForDelivery === true, + deferredChecks: Array.isArray(contract?.workflow?.deferredChecks) + ? contract.workflow.deferredChecks.map((item) => String(item || '').trim()).filter(Boolean) + : [], sourceGateStatus: String(contract?.workflow?.sourceGateStatus || '').trim(), doctorStatus: String(contract?.workflow?.doctorStatus || '').trim(), runtimeSmokeRequired: runtimeSmokeRequirement.required, @@ -533,8 +549,19 @@ function renderManagedPagesRegistryMarkdown(entries, brokenEntries) { lines.push('| Page | Page Type | Workflow | Runtime Smoke | Adapter | Snapshot | Updated |') lines.push('| --- | --- | --- | --- | --- | --- | --- |') for (const entry of entries) { + const workflowSummary = [ + entry.workflowStatus || '(missing)', + entry.preflightStage ? `stage=${entry.preflightStage}` : '', + entry.preflightStatus ? `preflight=${entry.preflightStatus}` : '', + entry.readyForDelivery ? 'delivery-ready' : entry.readyForImplementation ? 'implementation-ready' : '', + Array.isArray(entry.deferredChecks) && entry.deferredChecks.length > 0 + ? `deferred=${entry.deferredChecks.join(',')}` + : '', + ] + .filter(Boolean) + .join(' | ') lines.push( - `| \`${entry.generatedPagePath}\` | \`${entry.pageTypeId}\` | \`${entry.workflowStatus || '(missing)'}\` | \`${entry.runtimeSmokeRequired ? `${entry.runtimeSmokeStatus || '(missing)'} / required` : 'not-required'}\` | \`${entry.hostAdapterId || '(missing)'}\` | \`${entry.sourceSnapshotHash || '(missing)'}\` | \`${entry.updatedAt || '(missing)'}\` |` + `| \`${entry.generatedPagePath}\` | \`${entry.pageTypeId}\` | \`${workflowSummary || '(missing)'}\` | \`${entry.runtimeSmokeRequired ? `${entry.runtimeSmokeStatus || '(missing)'} / required` : 'not-required'}\` | \`${entry.hostAdapterId || '(missing)'}\` | \`${entry.sourceSnapshotHash || '(missing)'}\` | \`${entry.updatedAt || '(missing)'}\` |` ) } } diff --git a/skills/hiui-design/scripts/lib/managed-page-instance-readiness.mjs b/skills/hiui-design/scripts/lib/managed-page-instance-readiness.mjs new file mode 100644 index 000000000..950472cef --- /dev/null +++ b/skills/hiui-design/scripts/lib/managed-page-instance-readiness.mjs @@ -0,0 +1,99 @@ +export function readManagedInstanceSourceMarkers(sourceRaw) { + const source = String(sourceRaw || '') + const hasAttr = (name) => new RegExp(`data-hiui5-${name}=["'][^"']+["']`).test(source) + const hasComment = (name) => new RegExp(`hiui-design ${name}:`).test(source) + + return { + pageType: hasAttr('page-type') || hasComment('page-type'), + example: hasAttr('example') || hasComment('example'), + archetype: hasAttr('archetype') || hasComment('archetype'), + shell: hasAttr('shell') || hasComment('shell'), + scrollStrategy: hasAttr('scroll-strategy') || hasComment('scroll-strategy'), + hostAdapter: hasAttr('host-adapter') || hasComment('host-adapter'), + hostArchetype: hasComment('host-archetype'), + } +} + +function hasManagedSourceFingerprint(sourceMarkers) { + return Boolean( + sourceMarkers?.pageType && + sourceMarkers?.shell && + (sourceMarkers?.example || sourceMarkers?.hostArchetype || sourceMarkers?.archetype) + ) +} + +export function managedInstanceReadinessForPageFact({ pageExists, contract, sourceMarkers }) { + if (!pageExists) { + return { + schemaVersion: 'managed-instance-readiness.v1', + status: 'not-applicable', + reuseEligible: false, + blockerCodes: [], + reason: 'target page does not exist yet', + sourceMarkers, + } + } + + const blockerCodes = [] + const hasReferenceFingerprint = + sourceMarkers.example || + sourceMarkers.hostArchetype || + sourceMarkers.archetype + + if (!String(contract?.pageTypeId || '').trim()) { + blockerCodes.push('MANAGED_INSTANCE_CONTRACT_MISSING') + } + if (!sourceMarkers.pageType) { + blockerCodes.push('MANAGED_INSTANCE_SOURCE_PAGE_TYPE_MISSING') + } + if (!sourceMarkers.shell) { + blockerCodes.push('MANAGED_INSTANCE_SHELL_MARKER_MISSING') + } + if (!hasReferenceFingerprint) { + blockerCodes.push('MANAGED_INSTANCE_REFERENCE_MARKER_MISSING') + } + if (String(contract?.preflightStatus || '').trim() === 'fail') { + blockerCodes.push('MANAGED_INSTANCE_PREFLIGHT_FAILED') + } + if (String(contract?.workflowStatus || '').trim() === 'stale') { + blockerCodes.push('MANAGED_INSTANCE_STALE') + } + + const reuseEligible = blockerCodes.length === 0 + + return { + schemaVersion: 'managed-instance-readiness.v1', + status: reuseEligible ? 'ready' : 'migration-required', + reuseEligible, + blockerCodes, + reason: reuseEligible + ? 'existing page exposes the managed source fingerprint needed for in-place contract reuse' + : blockerCodes.includes('MANAGED_INSTANCE_CONTRACT_MISSING') && hasManagedSourceFingerprint(sourceMarkers) + ? 'existing page already exposes managed-page source markers but is not registered under page-contracts yet' + : 'existing page has a matching contract pageType but is not a ready managed page instance yet', + sourceMarkers, + } +} + +export function evaluateManagedInstanceReadinessFromSource({ contract, pageExists, sourceRaw }) { + const sourceMarkers = readManagedInstanceSourceMarkers(sourceRaw) + return managedInstanceReadinessForPageFact({ + pageExists, + contract, + sourceMarkers, + }) +} + +export function buildManagedInstanceMigrationReason(readiness) { + const blockerCodes = Array.isArray(readiness?.blockerCodes) + ? readiness.blockerCodes.filter(Boolean) + : [] + if ( + blockerCodes.includes('MANAGED_INSTANCE_CONTRACT_MISSING') && + hasManagedSourceFingerprint(readiness?.sourceMarkers) + ) { + return `existing page already exposes hiui-design managed-page markers but is not registered under page-contracts yet (${blockerCodes.join(', ')})` + } + + return `existing page has the target pageType contract but is not a ready managed page instance (${blockerCodes.join(', ') || 'missing source proof'})` +} diff --git a/skills/hiui-design/scripts/lib/managed-page-source-guard.mjs b/skills/hiui-design/scripts/lib/managed-page-source-guard.mjs index 5b7737c68..d46e9fd9c 100644 --- a/skills/hiui-design/scripts/lib/managed-page-source-guard.mjs +++ b/skills/hiui-design/scripts/lib/managed-page-source-guard.mjs @@ -22,6 +22,7 @@ export const HOST_ARCHETYPE_MARKER_PREFIX = 'hiui-design host-archetype:' export const SCROLL_STRATEGY_MARKER_PREFIX = 'hiui-design scroll-strategy:' export const TOPOLOGY_MARKER_PREFIX = 'hiui-design topology:' export const LAYOUT_STRATEGY_MARKER_PREFIX = 'hiui-design layout-strategy:' +export const LAYOUT_ARCHETYPE_MARKER_PREFIX = 'hiui-design layout archetype:' const FIXED_TEMPLATE_PAGE_TYPES = new Set([ 'data-visualization', 'table-stat', @@ -324,6 +325,78 @@ function isSharedManagedShellHelperFile(filePath, targetRoot) { ) } +function normalizeRelativeSourcePath(targetRoot, filePath) { + return path.relative(targetRoot, filePath).replace(/\\/g, '/') +} + +function hasDefaultReactComponentExport(sourceRaw) { + return hasAnyPattern(String(sourceRaw || ''), [ + /export\s+default\s+function\s+[A-Z]/, + /export\s+default\s+(?:React\.)?(?:memo|forwardRef)\s*\(/, + /export\s+default\s+[A-Z][A-Za-z0-9_$]*/, + ]) +} + +function hasPageLikeStatefulUiSemantics(sourceRaw) { + return hasAnyPattern(String(sourceRaw || ''), [ + /\buseState\s*\(/, + /\buseEffect\s*\(/, + /\buseMemo\s*\(/, + /\buseImperativeHandle\s*\(/, + /\b(QueryFilter|Table|Pagination|DatePicker|Select|Input|TabList)\b/, + ]) +} + +function isLegacyTabImplementationHelper({ filePath, sourceRaw, targetRoot }) { + const relativePath = normalizeRelativeSourcePath(targetRoot, filePath) + + if (!/(^|\/)tabs\/.+\.[cm]?[jt]sx?$/.test(relativePath)) { + return false + } + + return hasDefaultReactComponentExport(sourceRaw) && hasPageLikeStatefulUiSemantics(sourceRaw) +} + +function validateImportedLocalHelperPurity({ + contract, + importedLocalContext, + targetRoot, + pathLabel, +}) { + const errors = [] + + if (!isProjectCertifiedCarrierDelivery(contract)) { + return errors + } + + const localFileSources = Array.isArray(importedLocalContext?.fileSources) + ? importedLocalContext.fileSources.filter( + (entry) => + path.resolve(entry.filePath) !== path.resolve(path.join(targetRoot, contract.generatedPagePath)) && + !isSharedManagedShellHelperFile(path.resolve(entry.filePath), targetRoot) + ) + : [] + + const contaminatedHelpers = localFileSources.filter((entry) => + isLegacyTabImplementationHelper({ + filePath: path.resolve(entry.filePath), + sourceRaw: entry.sourceRaw, + targetRoot, + }) + ) + + if (contaminatedHelpers.length > 0) { + errors.push( + `${pathLabel} imports page-local helper code from tab implementation files (${contaminatedHelpers + .slice(0, 4) + .map((entry) => normalizeRelativeSourcePath(targetRoot, entry.filePath)) + .join(', ')}). In project-certified typical pages, helpers reused by slot code must be extracted into pure utility/renderer modules; do not import from tabs/ files that still export a stateful default React page implementation.` + ) + } + + return errors +} + function validateLocalBypassContracts({ contract, sourceRaw, @@ -1306,6 +1379,37 @@ function usesVerticalDetailDescriptions(sourceRaw) { ) } +function hasWrappedDescriptionsLabelTypography(sourceRaw) { + return hasAnyPattern(sourceRaw, [ + /\blabel\s*=\s*\{\s*<\s*(?:span|div|p|small|strong|em|Text|Typography(?:\.\w+)?)\b[\s\S]{0,240}>/, + /\blabel\s*:\s*<\s*(?:span|div|p|small|strong|em|Text|Typography(?:\.\w+)?)\b[\s\S]{0,240}>/, + ]) +} + +function validateDetailDescriptionsLabelOwnership({ contract, sourceRaw, pathLabel }) { + const errors = [] + + if (!['full-page-detail', 'drawer-detail'].includes(String(contract?.pageTypeId || '').trim())) { + return errors + } + + if (!hasWrappedDescriptionsLabelTypography(sourceRaw)) { + return errors + } + + if (contract.pageTypeId === 'full-page-detail') { + errors.push( + `${pathLabel} wraps Descriptions labels with page-local typography or styled DOM nodes. Detail-shell pages must keep label typography on the native Descriptions 14px baseline and use plain text/i18n labels instead of Text/span wrappers or inline font styling.` + ) + return errors + } + + errors.push( + `${pathLabel} wraps drawer-detail Descriptions labels with page-local typography or styled DOM nodes. Drawer detail labels must stay on the native Descriptions 14px baseline instead of being rebuilt with Text/span wrappers or inline font styling.` + ) + return errors +} + function hasQueryFilterCompatibleSemantics(sourceRaw) { return hasAnyPattern(sourceRaw, [ /\bQueryFilter\b/, @@ -1508,7 +1612,7 @@ export function hasHiddenManagedShellMount(sourceRaw, shellName) { ).test(sourceRaw) } -function normalizeRelativeSourcePath(value) { +function normalizeSourcePathLabel(value) { return String(value || '').trim().replace(/\\/g, '/').replace(/^\.\//, '') } @@ -1569,6 +1673,13 @@ function validateDeclaredShellAuthenticity({ contract, sourceRaw, pathLabel }) { return errors } + if ( + String(contract?.pageTypeId || '').trim() === 'full-page-detail' && + /\bManagedFullPageDetailShell\b/.test(sourceRaw) + ) { + return errors + } + if (declaresDomAliasForManagedShell(sourceRaw, canonicalShell)) { errors.push( `${pathLabel} declares ${canonicalShell} as a DOM/string alias. Managed shell names must refer to executable shell components or certified shell carriers; do not alias ${canonicalShell} to div/section/main to satisfy source markers.` @@ -1711,8 +1822,28 @@ function collectQueryFilterOpenTagSnippets(sourceRaw) { return collectComponentOpenTagSnippets(sourceRaw, 'QueryFilter', 1600) } +function getQueryFilterShellPropSnippet(snippet) { + const propBoundaryPatterns = [ + /\bfilterFields\s*=/, + /\bappend\s*=/, + /\bformData\s*=/, + /\bonChange\s*=/, + ] + + const boundaryIndex = propBoundaryPatterns.reduce((currentMin, pattern) => { + const matchIndex = String(snippet || '').search(pattern) + if (matchIndex < 0) { + return currentMin + } + + return currentMin < 0 ? matchIndex : Math.min(currentMin, matchIndex) + }, -1) + + return boundaryIndex >= 0 ? String(snippet || '').slice(0, boundaryIndex) : String(snippet || '') +} + function queryFilterSnippetForcesInlineLabels(snippet) { - return snippetEnablesBooleanProp(snippet, 'showLabel') + return snippetEnablesBooleanProp(getQueryFilterShellPropSnippet(snippet), 'showLabel') } function extractLiteralPropValue(snippet, propName) { @@ -1735,7 +1866,7 @@ function extractLiteralPropValue(snippet, propName) { } function queryFilterSnippetDriftsContainedAppearance(snippet) { - const appearance = extractLiteralPropValue(snippet, 'appearance') + const appearance = extractLiteralPropValue(getQueryFilterShellPropSnippet(snippet), 'appearance') return Boolean(appearance) && appearance !== 'contained' } @@ -1866,23 +1997,216 @@ function collectPreciseStyleBlocksForClassNames(styleRaw, classNames) { ) } +function extractEnclosingObjectLiteral(sourceRaw, anchorIndex, searchWindow = 240, fallbackLength = 900) { + const source = String(sourceRaw || '') + const normalizedAnchor = Number.isFinite(anchorIndex) ? Math.max(0, anchorIndex) : 0 + const searchStart = Math.max(0, normalizedAnchor - searchWindow) + const objectStart = source.lastIndexOf('{', normalizedAnchor) + + if (objectStart < searchStart || objectStart < 0) { + return source.slice(normalizedAnchor, Math.min(source.length, normalizedAnchor + fallbackLength)) + } + + let cursor = objectStart + let braceDepth = 0 + let parenDepth = 0 + let bracketDepth = 0 + let activeQuote = '' + + while (cursor < source.length) { + const char = source[cursor] + const prevChar = cursor > 0 ? source[cursor - 1] : '' + + if (activeQuote) { + if (char === activeQuote && prevChar !== '\\') { + activeQuote = '' + } + cursor += 1 + continue + } + + if (char === '"' || char === "'" || char === '`') { + activeQuote = char + cursor += 1 + continue + } + + if (char === '{') braceDepth += 1 + if (char === '}') braceDepth = Math.max(0, braceDepth - 1) + if (char === '(') parenDepth += 1 + if (char === ')') parenDepth = Math.max(0, parenDepth - 1) + if (char === '[') bracketDepth += 1 + if (char === ']') bracketDepth = Math.max(0, bracketDepth - 1) + + if (braceDepth === 0 && char === '}' && parenDepth === 0 && bracketDepth === 0) { + return source.slice(objectStart, cursor + 1) + } + + cursor += 1 + } + + return source.slice(objectStart, Math.min(source.length, objectStart + fallbackLength)) +} + function collectFieldConfigSnippets(sourceRaw) { const fieldMatches = Array.from( sourceRaw.matchAll(/field\s*:\s*['"]([^'"]+)['"]/g) ) - return fieldMatches.map((match, index) => { + return fieldMatches.map((match) => { const snippetStart = match.index ?? 0 - const snippetEnd = - fieldMatches[index + 1]?.index ?? Math.min(sourceRaw.length, snippetStart + 1400) - return { field: match[1], - snippet: sourceRaw.slice(snippetStart, snippetEnd), + snippet: extractEnclosingObjectLiteral(sourceRaw, snippetStart), } }) } +function collectTableColumnConfigSnippets(sourceRaw) { + const columnMatches = Array.from( + String(sourceRaw || '').matchAll(/dataKey\s*:\s*['"]([^'"]+)['"]/g) + ) + + return columnMatches.map((match) => { + const snippetStart = match.index ?? 0 + return { + dataKey: match[1], + snippet: extractEnclosingObjectLiteral(sourceRaw, snippetStart), + } + }) +} + +function extractModuleStyleClassNames(sourceRaw) { + const classNames = new Set() + + for (const match of String(sourceRaw || '').matchAll(/\bstyles\.([\w-]+)/g)) { + if (match[1]) { + classNames.add(String(match[1])) + } + } + + return [...classNames] +} + +function extractFixedPixelWidthTokens(snippet) { + const matches = [] + const patterns = [ + /\b(?:width|inline-size|min-width|min-inline-size)\s*:\s*(-?\d+(?:\.\d+)?)px\b/gi, + /\b(?:width|inlineSize|minWidth|minInlineSize)\s*:\s*['"]?(-?\d+(?:\.\d+)?)px?['"]?/g, + /\b(?:width|inlineSize|minWidth|minInlineSize)\s*=\s*\{\s*['"]?(-?\d+(?:\.\d+)?)px?['"]?\s*\}/g, + ] + + for (const pattern of patterns) { + for (const match of String(snippet || '').matchAll(pattern)) { + const rawValue = Number(match[1]) + if (Number.isFinite(rawValue) && rawValue > 0) { + matches.push(`${rawValue}px`) + } + } + } + + return [...new Set(matches)] +} + +function collectQueryFilterFixedWidthFieldProofs(sourceRaw, styleRaw) { + const proofs = [] + + for (const entry of collectFieldConfigSnippets(sourceRaw)) { + if (!/\bcomponent\s*:/.test(entry.snippet)) { + continue + } + + if (!/<\s*(Input|Select|DatePicker|Search|SearchInput)\b/.test(entry.snippet)) { + continue + } + + const widthTokens = new Set(extractFixedPixelWidthTokens(entry.snippet)) + const classNames = extractModuleStyleClassNames(entry.snippet) + const styleBlocks = collectPreciseStyleBlocksForClassNames(styleRaw, classNames) + + for (const styleBlock of styleBlocks) { + for (const widthToken of extractFixedPixelWidthTokens(styleBlock)) { + widthTokens.add(widthToken) + } + } + + if (widthTokens.size === 0) { + continue + } + + proofs.push({ + field: entry.field, + widths: [...widthTokens], + }) + } + + return proofs +} + +function extractNumericPropValue(snippet, propName) { + const patterns = [ + new RegExp(`\\b${escapeRegExp(propName)}\\s*:\\s*(\\d+(?:\\.\\d+)?)\\b`), + new RegExp(`\\b${escapeRegExp(propName)}\\s*=\\s*\\{\\s*(\\d+(?:\\.\\d+)?)\\s*\\}`), + new RegExp(`\\b${escapeRegExp(propName)}\\s*=\\s*['"](\\d+(?:\\.\\d+)?)['"]`), + ] + + for (const pattern of patterns) { + const match = String(snippet || '').match(pattern) + if (match?.[1]) { + return Number(match[1]) + } + } + + return undefined +} + +function columnSnippetLacksOverflowGuard(snippet) { + return !hasAnyPattern(String(snippet || ''), [ + /\brenderEllipsisText\b/, + /\bellipsis_text\b/, + /\btextOverflow\s*:\s*['"]ellipsis['"]/, + /\btext-overflow\s*:\s*ellipsis\b/, + /\boverflow\s*:\s*['"]hidden['"]/, + /\boverflow\s*:\s*hidden\b/, + /\bwhiteSpace\s*:\s*['"]nowrap['"]/, + /\bwhite-space\s*:\s*nowrap\b/, + /\bmaxWidth\s*:\s*['"]100%['"]/, + /\bmax-width\s*:\s*100%\b/, + ]) +} + +function columnSnippetHasTextLikeOverflowRisk(snippet) { + return hasAnyPattern(String(snippet || ''), [ + / 220) { + continue + } + + if (!columnSnippetHasTextLikeOverflowRisk(entry.snippet) || !columnSnippetLacksOverflowGuard(entry.snippet)) { + continue + } + + issues.push({ + dataKey: entry.dataKey, + width, + }) + } + + return issues +} + function hasUndeclaredHeaderSubtitle(sourceRaw) { return hasAnyPattern(sourceRaw, [ /\bsubTitle\s*=/, @@ -1950,6 +2274,41 @@ function hasSupportedHeaderPaddingBaseline(sourceRaw, styleRaw) { ) } +function snippetKeepsPageHeaderFullWidth(snippet) { + const normalizedSnippet = String(snippet || '') + + return ( + /(width\s*:\s*100%|width\s*:\s*['"]100%['"])/i.test(normalizedSnippet) && + /(min-width\s*:\s*0(?:px)?|minWidth\s*:\s*['"]?0(?:px)?['"]?)/i.test(normalizedSnippet) + ) +} + +function hasInlinePageHeaderFullWidth(sourceRaw) { + const pageHeaderSnippets = collectComponentOpenTagSnippets(sourceRaw, 'PageHeader', 640) + + return pageHeaderSnippets.some((snippet) => snippetKeepsPageHeaderFullWidth(snippet)) +} + +function hasSupportedPageHeaderStretchBaseline(sourceRaw, styleRaw) { + if ( + /\bManagedPageHeader\b/.test(sourceRaw) || + inheritsManagedHeaderFromHostSlot(sourceRaw) || + /\bHostPageHeaderPortal\b/.test(sourceRaw) || + mountsRealShellRuntime(sourceRaw, 'StatListPageFrame') + ) { + return true + } + + if (hasInlinePageHeaderFullWidth(sourceRaw)) { + return true + } + + const pageHeaderClassNames = extractComponentClassNames(sourceRaw, 'PageHeader') + const pageHeaderClassSnippets = collectStyleSnippetsForClassNames(styleRaw, pageHeaderClassNames) + + return pageHeaderClassSnippets.some(snippetKeepsPageHeaderFullWidth) +} + function hasSupportedHeaderTitleBaseline(sourceRaw, styleRaw) { return ( /\bManagedPageHeader\b/.test(sourceRaw) || @@ -2030,13 +2389,57 @@ function snippetKeepsPrimaryLinkAction(snippet) { } function collectComponentOpenTagSnippets(sourceRaw, componentName, radius = 960) { - return Array.from( - String(sourceRaw || '').matchAll( - new RegExp(`<${escapeRegExp(componentName)}\\b[\\s\\S]{0,${radius}}?(?:\\/?>)`, 'g') - ) - ) - .map((match) => String(match[0] || '')) - .filter(Boolean) + const source = String(sourceRaw || '') + const pattern = new RegExp(`<${escapeRegExp(componentName)}\\b`, 'g') + const snippets = [] + + for (const match of source.matchAll(pattern)) { + const startIndex = match.index ?? -1 + if (startIndex < 0) { + continue + } + + let cursor = startIndex + String(match[0] || '').length + let braceDepth = 0 + let parenDepth = 0 + let bracketDepth = 0 + let activeQuote = '' + + while (cursor < source.length && cursor - startIndex <= radius) { + const char = source[cursor] + const prevChar = cursor > 0 ? source[cursor - 1] : '' + + if (activeQuote) { + if (char === activeQuote && prevChar !== '\\') { + activeQuote = '' + } + cursor += 1 + continue + } + + if (char === '"' || char === "'" || char === '`') { + activeQuote = char + cursor += 1 + continue + } + + if (char === '{') braceDepth += 1 + if (char === '}') braceDepth = Math.max(0, braceDepth - 1) + if (char === '(') parenDepth += 1 + if (char === ')') parenDepth = Math.max(0, parenDepth - 1) + if (char === '[') bracketDepth += 1 + if (char === ']') bracketDepth = Math.max(0, bracketDepth - 1) + + if (char === '>' && braceDepth === 0 && parenDepth === 0 && bracketDepth === 0) { + snippets.push(source.slice(startIndex, cursor + 1)) + break + } + + cursor += 1 + } + } + + return snippets.filter(Boolean) } function snippetEnablesBooleanProp(snippet, propName) { @@ -2298,6 +2701,10 @@ function buildSourceContractCommentLines(contract) { commentLines.push(`${LAYOUT_STRATEGY_MARKER_PREFIX} ${contract.layoutStrategy}`) } + if (contract.layoutArchetype) { + commentLines.push(`${LAYOUT_ARCHETYPE_MARKER_PREFIX} ${contract.layoutArchetype}`) + } + const requiredHostAdapterId = getRequiredHostAdapterId(contract) if (requiredHostAdapterId) { commentLines.push(`${HOST_ADAPTER_MARKER_PREFIX} ${requiredHostAdapterId}`) @@ -2349,6 +2756,13 @@ export function getManagedPageSourceRootAttributes(contract) { }) } + if (contract.layoutArchetype) { + rootAttrs.push({ + name: 'data-hiui5-layout-group', + value: contract.layoutArchetype, + }) + } + const requiredHostAdapterId = getRequiredHostAdapterId(contract) if (requiredHostAdapterId) { rootAttrs.push({ @@ -2400,6 +2814,7 @@ function isLegacyPageComponentFastPathContract(contract) { function validateLegacyPageComponentFastPathSource({ contract, sourceRaw, pathLabel }) { const errors = [] + let downgradedManagedDeliveryPath = false const generationProfile = contract?.generationProfile || {} const runtimeComponentSource = String(generationProfile.runtimeComponentSource || '').trim() const runtimeBridgeProfileId = String(generationProfile.runtimeBridgeProfileId || '').trim() @@ -2407,6 +2822,7 @@ function validateLegacyPageComponentFastPathSource({ contract, sourceRaw, pathLa for (const line of buildSourceContractCommentLines(contract)) { if (!sourceRaw.includes(line)) { + downgradedManagedDeliveryPath = true errors.push(`${pathLabel} is missing source contract marker "${line}".`) } } @@ -2418,6 +2834,7 @@ function validateLegacyPageComponentFastPathSource({ contract, sourceRaw, pathLa ] for (const marker of requiredRuntimeBridgeMarkers) { if (!sourceRaw.includes(marker)) { + downgradedManagedDeliveryPath = true errors.push( `${pathLabel} is missing runtime bridge marker "${marker}". Legacy page-component fast path must keep the selected component and bridge profile machine-checkable.` ) @@ -2425,35 +2842,132 @@ function validateLegacyPageComponentFastPathSource({ contract, sourceRaw, pathLa } if (!/slot-adapter\.stub/.test(sourceRaw)) { + downgradedManagedDeliveryPath = true errors.push( `${pathLabel} does not import the runtime bridge slot adapter stub. Legacy page-component fast path must bind business slots through an explicit slot adapter boundary.` ) } if (!/\badaptedSlots\.businessSlots\b/.test(sourceRaw)) { + downgradedManagedDeliveryPath = true errors.push( `${pathLabel} does not route businessSlots through adaptedSlots.businessSlots. Legacy page-component fast path must fill certified business slots instead of rebuilding shell regions locally.` ) } if (!/\badaptedSlots\.controlledExtensions\b/.test(sourceRaw)) { + downgradedManagedDeliveryPath = true errors.push( `${pathLabel} does not expose adaptedSlots.controlledExtensions. Legacy page-component fast path must keep Level 1 controlled extensions on the slot adapter boundary.` ) } if (!/\badaptedSlots\.runtimeBridge\b/.test(sourceRaw)) { + downgradedManagedDeliveryPath = true errors.push( `${pathLabel} does not expose adaptedSlots.runtimeBridge. Legacy page-component fast path must keep runtime inputs at the bridge boundary instead of in local shell reimplementation.` ) } if (!/\bRuntimeBridgeShellAny\b/.test(sourceRaw) && !/\bRuntimeBridgeShell\b/.test(sourceRaw)) { + downgradedManagedDeliveryPath = true errors.push( `${pathLabel} does not mount the resolved runtime bridge shell. Legacy page-component fast path must render the planner-selected runtime shell instead of page-local white-body/header/table primitives.` ) } + if (downgradedManagedDeliveryPath) { + errors.unshift( + `${pathLabel} downgrades the planner-selected managed delivery path. Legacy page-component fast path must stay on page-component + runtime bridge + slot fill; do not replace it with a handwritten compatibility page, translated reference, or free fallback.` + ) + } + + return errors +} + +function extractRuntimeBridgeShellLeadingReturnSnippet(sourceRaw) { + const runtimeShellMatch = / String(match[1] || '').trim() + ).filter((name) => name && !allowedWrapperTags.has(name)) + ), + ] +} + +function validateLegacyThinRuntimeWrapperBoundary({ contract, sourceRaw, pathLabel }) { + const errors = [] + const ownershipRoles = ['content-slot', 'white-body', 'outer-padding', 'main-scroll'] + const contractRegions = getContractRegionNames(contract) + const wrapperLeadSnippet = extractRuntimeBridgeShellLeadingReturnSnippet(sourceRaw) + const leadingWrapperComponents = extractLeadingRuntimeWrapperComponentNames(wrapperLeadSnippet) + + for (const role of ownershipRoles) { + const scopedAttrName = getRoleSpecificOwnershipAttrName(role) + if ( + new RegExp(escapeRegExp(scopedAttrName)).test(sourceRaw) || + new RegExp(`data-hiui5-owner\\s*=\\s*["']${escapeRegExp(role)}["']`).test(sourceRaw) + ) { + errors.push( + `${pathLabel} declares ${scopedAttrName} in the legacy runtime wrapper. Business pages may expose source and runtime-bridge markers only; workspace ownership must stay inside the selected project-certified carrier.` + ) + } + } + + for (const region of contractRegions) { + if (new RegExp(`data-hiui5-region\\s*=\\s*["']${escapeRegExp(region)}["']`).test(sourceRaw)) { + errors.push( + `${pathLabel} declares data-hiui5-region="${region}" in the legacy runtime wrapper. Required shell regions must stay inside the selected carrier; the business wrapper may only bind business slots and controlled extensions.` + ) + } + } + + if (/data-hiui5-shell\s*=/.test(sourceRaw)) { + errors.push( + `${pathLabel} declares data-hiui5-shell in the legacy runtime wrapper. Runtime shell identity belongs to the selected carrier/runtime bridge, not to the page-local business wrapper.` + ) + } + + if ( + hasAnyPattern(sourceRaw, [ + /<\s*PageHeader\b/, + /<\s*ManagedPageHeader\b/, + /<\s*HostPageHeaderPortal\b/, + /<\s*TypicalPageHeaderPortal\b/, + ]) + ) { + errors.push( + `${pathLabel} renders header carrier primitives in the legacy runtime wrapper. Header geometry and docking must stay inside the selected carrier instead of being rebuilt by the page-local wrapper.` + ) + } + + if (/<\s*(div|section|main|article|aside|header|footer)\b/.test(wrapperLeadSnippet)) { + errors.push( + `${pathLabel} wraps the runtime bridge shell in a page-local JSX container before mount. legacy-host-compatible fast path requires a thin runtime wrapper: emit source markers, adapt business slots, and mount the selected carrier directly without extra layout containers.` + ) + } + + if (leadingWrapperComponents.length > 0) { + errors.push( + `${pathLabel} wraps the runtime bridge shell with page-local wrapper components (${leadingWrapperComponents.join( + ', ' + )}) before mount. legacy-host-compatible fast path requires a thin runtime wrapper: mount the selected carrier directly instead of introducing local shell-like wrapper components.` + ) + } + return errors } @@ -2548,8 +3062,8 @@ function validateNamedHostAdapterTranslation({ contract, sourceRaw, targetRoot, } const expectedHostArchetypePath = String(contract.hostArchetypePath || '').trim() - const generatedPagePath = normalizeRelativeSourcePath(contract.generatedPagePath || pathLabel) - const normalizedHostArchetypePath = normalizeRelativeSourcePath(expectedHostArchetypePath) + const generatedPagePath = normalizeSourcePathLabel(contract.generatedPagePath || pathLabel) + const normalizedHostArchetypePath = normalizeSourcePathLabel(expectedHostArchetypePath) if (expectedHostArchetypePath && normalizedHostArchetypePath === generatedPagePath) { errors.push( @@ -2588,8 +3102,8 @@ export function validateManagedPageSlotBoundary({ contract, sourceRaw, pathLabel } } - const expectedHostArchetypePath = normalizeRelativeSourcePath(contract?.hostArchetypePath || '') - const generatedPagePath = normalizeRelativeSourcePath(contract?.generatedPagePath || pathLabel) + const expectedHostArchetypePath = normalizeSourcePathLabel(contract?.hostArchetypePath || '') + const generatedPagePath = normalizeSourcePathLabel(contract?.generatedPagePath || pathLabel) if (expectedHostArchetypePath && expectedHostArchetypePath === generatedPagePath) { errors.push( `${pathLabel} changes provenance boundary by making hostArchetypePath point to the generated page itself. Slot edits cannot self-certify their mold or adapter source.` @@ -2604,9 +3118,11 @@ function validateRequiredCapabilities({ contract, sourceRaw, pathLabel }) { const requiredCapabilities = Array.isArray(contract.adapterContract?.requiredCapabilities) ? contract.adapterContract.requiredCapabilities : [] + const usesManagedDetailShell = /\bManagedFullPageDetailShell\b/.test(sourceRaw) if ( requiredCapabilities.includes('leading-back') && + !usesManagedDetailShell && !/\bonBack\s*=/.test(sourceRaw) && !hasLiteralAttr(sourceRaw, 'data-hiui5-leading-back', 'true') ) { @@ -2641,6 +3157,7 @@ function validateRequiredCapabilities({ contract, sourceRaw, pathLabel }) { if ( requiredCapabilities.includes('detail-body') && + !usesManagedDetailShell && !/\b(Descriptions|SchemaDescriptionsBridge)\b/.test(sourceRaw) ) { errors.push( @@ -3251,13 +3768,15 @@ function validateFullPageDetailStructure({ contract, sourceRaw, styleRaw, pathLa return errors } - if (!hasVerticalDescriptionsPlacement(sourceRaw)) { + const usesManagedDetailShell = /\bManagedFullPageDetailShell\b/.test(sourceRaw) + + if (!usesManagedDetailShell && !hasVerticalDescriptionsPlacement(sourceRaw)) { errors.push( `${pathLabel} does not keep Descriptions in vertical placement. Full-page-detail fields must use top/bottom label-value distribution instead of inline horizontal rows.` ) } - if (!hasThreeColumnDescriptions(sourceRaw)) { + if (!usesManagedDetailShell && !hasThreeColumnDescriptions(sourceRaw)) { errors.push( `${pathLabel} does not keep a 3-column detail grid. Full-page-detail must preserve the example's three-column grouped layout.` ) @@ -3270,13 +3789,13 @@ function validateFullPageDetailStructure({ contract, sourceRaw, styleRaw, pathLa ) } - if (!hasSupportedHeaderHeightBaseline(sourceRaw, styleRaw)) { + if (!usesManagedDetailShell && !hasSupportedHeaderHeightBaseline(sourceRaw, styleRaw)) { errors.push( `${pathLabel} header styles do not declare 60px height/min-height plus align-items:center. Legacy-host-compatible full-page-detail headers must stay vertically centered inside the header region.` ) } - if (!hasSupportedHeaderTitleBaseline(sourceRaw, styleRaw)) { + if (!usesManagedDetailShell && !hasSupportedHeaderTitleBaseline(sourceRaw, styleRaw)) { errors.push( `${pathLabel} does not keep the full-page-detail title on the 18px / 600 baseline. Host PageHeader implementations must own the title typography instead of depending on unrelated page styles.` ) @@ -3805,7 +4324,18 @@ function validateNonTypicalStrategyProof({ contract, sourceRaw, pathLabel }) { return errors } -function validateQueryFilterFieldMapSupport({ sourceRaw, pathLabel }) { +function hasProjectCarrierManagedFieldMapProof({ contract, sourceRaw }) { + return ( + isProjectCertifiedCarrierDelivery(contract) && + hasAnyPattern(sourceRaw, [ + /data-hiui5-shell-carrier\s*=/, + /hiui-design shell-carrier:/, + /runtime bridge profile:/, + ]) + ) +} + +function validateQueryFilterFieldMapSupport({ contract, sourceRaw, pathLabel }) { const errors = [] if (!/ + collectPxPropertyValues(snippet, 'gap').some((value) => value > 0) + ) + + if (projectCarrierAddsHeaderBodyGap) { + errors.push( + `${pathLabel} inserts a page-root gap between the header region and white-body workspace on the project-certified ${pageTypeId} carrier. strict rollout list carriers must keep header and white-body flush on the shared baseline; move spacing into outer-padding or inner section content instead of using flex gap on the page root.` + ) + } + if (projectCarrierOwnsHorizontalOverflow) { errors.push( `${pathLabel} lets the project-certified table-basic carrier own horizontal scrolling at outer-padding/white-body/main-scroll/table region level. Keep horizontal overflow inside an inner table wrapper; the carrier itself must stay width-adaptive.` @@ -4037,6 +4583,50 @@ function validateProjectCertifiedCarrierListBaseline({ contract, sourceRaw, styl return errors } +function validateProjectCertifiedCarrierListSlotElasticity({ + contract, + sourceRaw, + styleRaw, + pathLabel, +}) { + const errors = [] + + if ( + !isProjectCertifiedCarrierDelivery(contract) || + String(contract?.pageTypeId || '').trim() !== 'table-basic' + ) { + return errors + } + + const fixedWidthFieldProofs = collectQueryFilterFixedWidthFieldProofs(sourceRaw, styleRaw) + + if (fixedWidthFieldProofs.length > 0) { + const summary = fixedWidthFieldProofs + .slice(0, 4) + .map((item) => `${item.field}=${item.widths.join('/')}`) + .join(', ') + + errors.push( + `${pathLabel} pins QueryFilter field controls to page-local fixed widths (${summary}) inside the project-certified table-basic carrier. Typical list filters must inherit QueryFilter's adaptive track width, not per-field 180/220/320px widths, otherwise the filter bar cannot stretch and wrap cleanly with the workspace.` + ) + } + + const tableCellOverflowRisks = collectTableCellOverflowRiskColumns(sourceRaw) + + if (tableCellOverflowRisks.length > 0) { + const summary = tableCellOverflowRisks + .slice(0, 4) + .map((item) => `${item.dataKey}@${item.width}px`) + .join(', ') + + errors.push( + `${pathLabel} defines narrow text/link table columns without a clipping guard (${summary}). Managed table cells must either ellipsis within the declared column width or move full content into tooltip/detail affordances; otherwise long values can visually run into the next cell.` + ) + } + + return errors +} + function validateProjectCertifiedCarrierFullPageEditBaseline({ contract, sourceRaw, @@ -5391,7 +5981,12 @@ export function validateManagedPageSource({ contract, generatedPagePath, targetR return [ ...validateLegacyPageComponentFastPathSource({ contract, - sourceRaw: extendedSourceRaw, + sourceRaw, + pathLabel, + }), + ...validateLegacyThinRuntimeWrapperBoundary({ + contract, + sourceRaw, pathLabel, }), ...validateProjectCertifiedCarrierListBaseline({ @@ -5400,6 +5995,12 @@ export function validateManagedPageSource({ contract, generatedPagePath, targetR styleRaw: extendedStyleRaw, pathLabel, }), + ...validateProjectCertifiedCarrierListSlotElasticity({ + contract, + sourceRaw: extendedSourceRaw, + styleRaw: extendedStyleRaw, + pathLabel, + }), ...validateProjectCertifiedCarrierFullPageEditBaseline({ contract, sourceRaw: extendedSourceRaw, @@ -5418,11 +6019,22 @@ export function validateManagedPageSource({ contract, generatedPagePath, targetR styleRaw: extendedStyleRaw, pathLabel, }), + ...validateDetailDescriptionsLabelOwnership({ + contract, + sourceRaw: extendedSourceRaw, + pathLabel, + }), ...validateManagedPageSlotBoundary({ contract, sourceRaw: extendedSourceRaw, pathLabel, }), + ...validateImportedLocalHelperPurity({ + contract, + importedLocalContext, + targetRoot, + pathLabel, + }), ...validateDemoToolingIsolation({ sourceRaw: extendedSourceRaw, pathLabel }), ] } @@ -5455,6 +6067,12 @@ export function validateManagedPageSource({ contract, generatedPagePath, targetR targetRoot, pathLabel, }), + ...validateImportedLocalHelperPurity({ + contract, + importedLocalContext, + targetRoot, + pathLabel, + }), ...validateI18nBaseline({ sourceRaw: extendedSourceRaw, styleRaw: extendedStyleRaw, pathLabel }), ...validateCommonComponentSkeletonIntegrity({ sourceRaw: extendedSourceRaw, @@ -5493,13 +6111,23 @@ export function validateManagedPageSource({ contract, generatedPagePath, targetR ...validateNonTypicalStrategyProof({ contract, sourceRaw: extendedSourceRaw, pathLabel }), ...validateHeaderPortalStructure({ sourceRaw: extendedSourceRaw, pathLabel }), ...validateHeaderShellCarriers({ sourceRaw: extendedSourceRaw, pathLabel }), - ...validateQueryFilterFieldMapSupport({ sourceRaw: extendedSourceRaw, pathLabel }), + ...validateQueryFilterFieldMapSupport({ + contract, + sourceRaw: extendedSourceRaw, + pathLabel, + }), ...validateProjectCertifiedCarrierListBaseline({ contract, sourceRaw: extendedSourceRaw, styleRaw: extendedStyleRaw, pathLabel, }), + ...validateProjectCertifiedCarrierListSlotElasticity({ + contract, + sourceRaw: extendedSourceRaw, + styleRaw: extendedStyleRaw, + pathLabel, + }), ...validateProjectCertifiedCarrierFullPageEditBaseline({ contract, sourceRaw: extendedSourceRaw, @@ -5542,6 +6170,11 @@ export function validateManagedPageSource({ contract, generatedPagePath, targetR pathLabel, }), ...validateDrawerStructure({ contract, sourceRaw: extendedSourceRaw, styleRaw: extendedStyleRaw, pathLabel }), + ...validateDetailDescriptionsLabelOwnership({ + contract, + sourceRaw: extendedSourceRaw, + pathLabel, + }), ...validateFeedbackStatusStructure({ contract, sourceRaw: extendedSourceRaw, pathLabel }), ...validateDemoToolingIsolation({ sourceRaw: extendedSourceRaw, pathLabel }), ...validateFullPageEditStructure({ contract, sourceRaw: extendedSourceRaw, styleRaw: extendedStyleRaw, pathLabel }), diff --git a/skills/hiui-design/scripts/lib/project-facts.mjs b/skills/hiui-design/scripts/lib/project-facts.mjs index aaa1efe40..a0a08245c 100644 --- a/skills/hiui-design/scripts/lib/project-facts.mjs +++ b/skills/hiui-design/scripts/lib/project-facts.mjs @@ -1,8 +1,21 @@ import fs from 'node:fs/promises' +import { createRequire } from 'node:module' import path from 'node:path' import { buildProjectCapabilities } from './asset-control-surface.mjs' import { detectHostProfile } from './detect-host-profile.mjs' import { detectLegacyHostFamily } from './legacy-host-family-registry.mjs' +import { + runtimeAdapterProofForComponent, +} from './legacy-page-component-bridge.mjs' +import { + isPageComponentCertified, + listPageComponentsForModeAndPageType, + loadPageComponentCertification, + loadPageComponentRegistry, + supportedModesForPageComponent, +} from './page-component-registry.mjs' +import { validateProjectCertifiedCarriers } from './project-integration-state.mjs' +import { resolveRuntimeBridgeProfileForComponent } from './runtime-bridge-profile-registry.mjs' const HOST_PROFILE_FINGERPRINT_PATHS = [ 'package.json', @@ -62,7 +75,44 @@ const PROJECT_CAPABILITIES_FINGERPRINT_PATHS = [ '.local-context/hiui-design/outputs/project-page-component-registry.json', ] +const PROJECT_CARRIER_REALIZATION_FINGERPRINT_PATHS = [ + '.local-context/hiui-design/outputs/project-page-component-registry.json', + '.local-context/hiui-design/outputs/page-component-certifications', + 'src/typical-page-reuse/components', +] + +const PROJECT_TYPICAL_PAGE_SUPPORT_FINGERPRINT_PATHS = [ + '.local-context/hiui-design/outputs/project-page-component-registry.json', + '.local-context/hiui-design/outputs/page-component-certifications', + 'src/typical-page-reuse/components', + '.local-context/hiui-design/outputs/legacy-host-boundary.json', +] + +const PROJECT_LEGACY_DELIVERY_POLICY_FINGERPRINT_PATHS = [ + ...PROJECT_TYPICAL_PAGE_SUPPORT_FINGERPRINT_PATHS, + '.local-context/hiui-design/outputs/project-integration-state.json', +] + +const PROJECT_LEGACY_STYLE_BOUNDARY_FINGERPRINT_PATHS = [ + 'package.json', + 'pnpm-lock.yaml', + 'src', + 'src/styles', + 'src/index.scss', + 'src/index.css', + 'src/app.scss', + 'src/app.css', + '.local-context/hiui-design/outputs/legacy-host-boundary.json', + 'node_modules/@hi-ui/query-filter', + 'node_modules/@hi-ui/input', + 'node_modules/hiui5', +] + const PROJECT_FACTS_SCHEMA_VERSION = 'project-fact-cache.v1' +const LEGACY_PAGE_TYPE_DELIVERY_POLICY_RULES_RELATIVE_PATH = 'rules/legacy-page-type-delivery-policy.json' +const LEGACY_PAGE_TYPE_DELIVERY_POLICY_OUTPUT_TEMPLATE = + '.local-context/hiui-design/outputs/page-type-delivery-policy.%MODE%.json' +const requireForProjectFacts = createRequire(import.meta.url) function normalizeModeKey(modeOverride = '') { const value = String(modeOverride || '').trim() @@ -72,6 +122,56 @@ function normalizeModeKey(modeOverride = '') { return value } +function resolveFactModeId(modeOverride = '') { + const modeKey = normalizeModeKey(modeOverride) + return modeKey === 'auto' ? '' : modeKey +} + +function uniqueValues(values = []) { + return Array.from(new Set(values.filter(Boolean))) +} + +function normalizePathForFact(value = '') { + return String(value || '').replace(/\\/g, '/') +} + +function releaseTrackForVersion(version = '') { + const normalizedVersion = String(version || '').trim() + const trackMatch = normalizedVersion.match(/-(alpha|beta|canary|experimental|rc)(?:[.-]\d+)?/i) + + return trackMatch ? trackMatch[1].toLowerCase() : normalizedVersion ? 'stable' : '' +} + +function packageJsonPathForResolvedEntry(resolvedEntryPath = '') { + if (!resolvedEntryPath) { + return '' + } + + const normalizedPath = normalizePathForFact(resolvedEntryPath) + const nodeModulesToken = '/node_modules/' + const nodeModulesIndex = normalizedPath.lastIndexOf(nodeModulesToken) + + if (nodeModulesIndex < 0) { + return '' + } + + const packageStart = nodeModulesIndex + nodeModulesToken.length + const packageSegments = normalizedPath.slice(packageStart).split('/') + const packageName = packageSegments[0]?.startsWith('@') + ? packageSegments.slice(0, 2).join('/') + : packageSegments[0] + + if (!packageName) { + return '' + } + + return path.join( + normalizedPath.slice(0, nodeModulesIndex + nodeModulesToken.length), + packageName, + 'package.json' + ) +} + async function pathStamp(targetPath) { try { const stat = await fs.stat(targetPath) @@ -109,6 +209,14 @@ async function readJsonIfExists(filePath) { } } +async function readTextIfExists(filePath) { + try { + return await fs.readFile(filePath, 'utf8') + } catch { + return '' + } +} + async function cacheRootExists(targetRoot) { try { const stat = await fs.stat(path.join(targetRoot, '.local-context', 'hiui-design')) @@ -118,6 +226,115 @@ async function cacheRootExists(targetRoot) { } } +async function writeDerivedOutput(targetRoot, relativePath, payload) { + const filePath = path.join(targetRoot, relativePath) + await fs.mkdir(path.dirname(filePath), { recursive: true }) + await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8') +} + +function resolvePackageEntryFromRoot(targetRoot, packageName, fromDir = targetRoot) { + try { + return requireForProjectFacts.resolve(packageName, { paths: [fromDir, targetRoot] }) + } catch { + return '' + } +} + +async function loadInstalledPackageInfo(targetRoot, packageName, fromDir = targetRoot) { + const resolvedEntryPath = resolvePackageEntryFromRoot(targetRoot, packageName, fromDir) + const packageJsonPath = packageJsonPathForResolvedEntry(resolvedEntryPath) + + if (!resolvedEntryPath || !packageJsonPath) { + return { + packageName, + version: '', + releaseTrack: '', + resolvedEntryPath: '', + packageJsonPath: '', + packageDir: '', + found: false, + } + } + + const packageJson = await readJsonIfExists(packageJsonPath) + const packageDir = packageJsonPath ? path.dirname(packageJsonPath) : '' + const version = String(packageJson?.version || '').trim() + + return { + packageName, + version, + releaseTrack: releaseTrackForVersion(version), + resolvedEntryPath: normalizePathForFact(resolvedEntryPath), + packageJsonPath: normalizePathForFact(packageJsonPath), + packageDir: normalizePathForFact(packageDir), + found: Boolean(packageJson), + } +} + +async function listFilesRecursively(rootPath) { + try { + const entries = await fs.readdir(rootPath, { withFileTypes: true }) + const nested = await Promise.all( + entries.map(async (entry) => { + const entryPath = path.join(rootPath, entry.name) + if (entry.isDirectory()) { + return listFilesRecursively(entryPath) + } + return [entryPath] + }) + ) + return nested.flat() + } catch { + return [] + } +} + +async function collectGlobalStyleFiles(targetRoot) { + const candidatePaths = [ + 'src/index.scss', + 'src/index.css', + 'src/app.scss', + 'src/app.css', + 'src/styles/global.scss', + 'src/styles/global.css', + 'src/styles/index.scss', + 'src/styles/index.css', + ] + const collected = new Set() + + for (const candidatePath of candidatePaths) { + const absolutePath = path.join(targetRoot, candidatePath) + const stat = await pathStamp(absolutePath) + if (stat.exists && stat.kind === 'file') { + collected.add(absolutePath) + } + } + + const stylesRoot = path.join(targetRoot, 'src', 'styles') + const styleFiles = await listFilesRecursively(stylesRoot) + styleFiles + .filter((entryPath) => /\.(css|scss|sass|less)$/i.test(entryPath) && !/\.module\./i.test(entryPath)) + .forEach((entryPath) => collected.add(entryPath)) + + return Array.from(collected) +} + +function detectGlobalInputOverrideSnippets(styleSource = '') { + const source = String(styleSource || '') + const patterns = [ + /\.hi-v5-input(?:__inner|--appearance-filled|\b)/g, + /\.hi-v5-query-filter-search-input/g, + /\.hi-v5-query-filter-form/g, + ] + const matches = [] + + for (const pattern of patterns) { + matches.push(...Array.from(source.matchAll(pattern), (match) => match[0])) + } + + return uniqueValues(matches).slice(0, 6) +} + function cacheFilePath(targetRoot, factId, modeKey = '') { const fileName = modeKey && modeKey !== 'auto' ? `${factId}.${modeKey}.json` : `${factId}.json` return path.join(targetRoot, '.local-context', 'hiui-design', 'outputs', 'project-facts', fileName) @@ -217,3 +434,789 @@ export async function loadProjectCapabilitiesFact({ }), }) } + +function isProjectOverlayComponent(component) { + return component?.__registryKind === 'project-overlay' +} + +function summarizeCarrierRealizationEntry(result = null) { + const issues = Array.isArray(result?.issues) ? result.issues : [] + return { + componentId: String(result?.componentId || '').trim(), + pageTypeId: String(result?.pageTypeId || '').trim(), + status: String(result?.status || 'blocked').trim() || 'blocked', + issueCount: issues.length, + blockingReasons: issues.map((issue) => issue?.message).filter(Boolean), + issues, + } +} + +export async function loadProjectCarrierRealizationFact({ + targetRoot, + skillRoot, + modeOverride = '', +}) { + const modeKey = normalizeModeKey(modeOverride) + const effectiveMode = resolveFactModeId(modeOverride) + const fingerprintPaths = [ + ...PROJECT_CARRIER_REALIZATION_FINGERPRINT_PATHS, + path.join(skillRoot, 'rules', 'page-component-registry.json'), + ] + + return loadCachedProjectFact({ + targetRoot, + factId: 'project-carrier-realization', + modeKey, + fingerprintPaths, + compute: async () => { + const validation = await validateProjectCertifiedCarriers({ targetRoot }) + const components = Array.isArray(validation?.components) + ? validation.components.map(summarizeCarrierRealizationEntry) + : [] + + const filteredComponents = effectiveMode + ? components.filter((component) => { + const projectComponent = loadPageComponentRegistry({ skillRoot, targetRoot }).byComponentId.get( + component.componentId + ) + return supportedModesForPageComponent(projectComponent).includes(effectiveMode) + }) + : components + + const readyPageTypeIds = uniqueValues( + filteredComponents.filter((component) => component.status === 'ready').map((component) => component.pageTypeId) + ).sort() + const blockedPageTypeIds = uniqueValues( + filteredComponents.filter((component) => component.status === 'blocked').map((component) => component.pageTypeId) + ).sort() + + return { + schemaVersion: 'project-carrier-realization.v1', + mode: effectiveMode || modeKey, + status: filteredComponents.length === 0 + ? 'not-applicable' + : filteredComponents.some((component) => component.status === 'blocked') + ? 'blocked' + : 'ready', + source: validation?.registryPath || '.local-context/hiui-design/outputs/project-page-component-registry.json', + checkedComponentCount: filteredComponents.length, + readyComponentCount: filteredComponents.filter((component) => component.status === 'ready').length, + blockedComponentCount: filteredComponents.filter((component) => component.status === 'blocked').length, + readyPageTypeIds, + blockedPageTypeIds, + enforcement: { + deliverySemantics: 'page-component', + deliveryPath: 'page-component-plus-slot-fill', + projectCarrierRole: 'prefer-project-certified-carrier-when-ready', + }, + components: filteredComponents, + blockingReasons: filteredComponents.flatMap((component) => component.blockingReasons || []), + } + }, + }) +} + +function summarizeRuntimeBridgeRequirement({ + component, + legacyHostFamily = null, + mode, + skillRoot, + targetRoot, +}) { + if (mode !== 'legacy-host-compatible') { + return { + required: false, + status: 'not-applicable', + profileId: '', + blockingReasons: [], + } + } + + const profile = resolveRuntimeBridgeProfileForComponent({ + componentId: component.componentId, + skillRoot, + targetRoot, + }) + const adapterProof = runtimeAdapterProofForComponent({ + component, + legacyHostFamily, + mode, + skillRoot, + }) + const adapterBlockingReasons = [] + + if (adapterProof?.status === 'blocked') { + adapterBlockingReasons.push(String(adapterProof.reason || 'legacy runtime adapter proof is blocked').trim()) + if (Array.isArray(adapterProof.missingCapabilities) && adapterProof.missingCapabilities.length > 0) { + adapterBlockingReasons.push( + `legacy runtime adapter ${adapterProof.adapterId || '(missing)'} is missing capabilities: ${adapterProof.missingCapabilities.join(', ')}` + ) + } + } + + const blockingReasons = uniqueValues([ + ...(profile?.status === 'available' + ? [] + : [String(profile?.reason || 'runtime bridge profile is blocked').trim()].filter(Boolean)), + ...adapterBlockingReasons, + ]) + + return { + required: true, + status: + profile?.status === 'available' && adapterProof?.status === 'available' + ? 'ready' + : 'blocked', + profileId: String(profile?.profileId || '').trim(), + deliveryMode: String(profile?.deliveryMode || '').trim(), + adapterProof: adapterProof + ? { + required: Boolean(adapterProof.required), + status: String(adapterProof.status || '').trim() || 'blocked', + kind: String(adapterProof.kind || '').trim(), + adapterId: String(adapterProof.adapterId || '').trim(), + responsibility: String(adapterProof.responsibility || '').trim(), + reason: String(adapterProof.reason || '').trim(), + missingCapabilities: Array.isArray(adapterProof.missingCapabilities) + ? adapterProof.missingCapabilities.map((capability) => String(capability || '').trim()).filter(Boolean) + : [], + } + : null, + blockingReasons, + } +} + +function evaluateCandidateSupport({ + component, + legacyHostFamily = null, + certification, + mode, + skillRoot, + targetRoot, + carrierRealizationByComponentId, +}) { + const candidateKind = isProjectOverlayComponent(component) + ? 'project-certified-carrier' + : 'direct-standard-component' + const certified = isPageComponentCertified(component, certification) + + if (!certified) { + return { + status: 'blocked', + candidateKind, + componentId: component.componentId, + pageTypeId: component.pageTypeId, + blockingReasons: [ + isProjectOverlayComponent(component) + ? `${component.componentId} project carrier is not fully certified` + : `${component.componentId} standard page component is not fully certified`, + ], + runtimeBridge: { + required: mode === 'legacy-host-compatible', + status: mode === 'legacy-host-compatible' ? 'blocked' : 'not-applicable', + profileId: '', + blockingReasons: mode === 'legacy-host-compatible' + ? ['runtime bridge cannot be resolved until the page component certification is valid'] + : [], + }, + } + } + + const runtimeBridge = summarizeRuntimeBridgeRequirement({ + component, + legacyHostFamily, + mode, + skillRoot, + targetRoot, + }) + + if (isProjectOverlayComponent(component)) { + const realization = carrierRealizationByComponentId.get(component.componentId) + const blockingReasons = [ + ...(realization?.blockingReasons || []), + ...runtimeBridge.blockingReasons, + ] + + return { + status: + realization?.status === 'ready' && runtimeBridge.status !== 'blocked' + ? 'ready' + : 'blocked', + candidateKind, + componentId: component.componentId, + pageTypeId: component.pageTypeId, + blockingReasons, + runtimeBridge, + carrierRealization: realization || { + componentId: component.componentId, + pageTypeId: component.pageTypeId, + status: 'blocked', + issueCount: 1, + blockingReasons: [`${component.componentId} is missing project carrier realization evidence`], + issues: [], + }, + } + } + + return { + status: runtimeBridge.status === 'blocked' ? 'blocked' : 'ready', + candidateKind, + componentId: component.componentId, + pageTypeId: component.pageTypeId, + blockingReasons: runtimeBridge.blockingReasons, + runtimeBridge, + } +} + +function selectSupportedCandidate(candidates = []) { + return candidates.find((candidate) => candidate.status === 'ready') || null +} + +function summarizePageTypeSupport({ + mode, + pageTypeId, + candidates, + legacyHostFamily = null, + skillRoot, + targetRoot, + carrierRealizationByComponentId, +}) { + const evaluatedCandidates = candidates.map((component) => + evaluateCandidateSupport({ + component, + legacyHostFamily, + certification: loadPageComponentCertification(component, { skillRoot }), + mode, + skillRoot, + targetRoot, + carrierRealizationByComponentId, + }) + ) + const selectedCandidate = selectSupportedCandidate(evaluatedCandidates) + const projectCarrier = evaluatedCandidates.find( + (candidate) => candidate.candidateKind === 'project-certified-carrier' + ) || null + const standardComponent = evaluatedCandidates.find( + (candidate) => candidate.candidateKind === 'direct-standard-component' + ) || null + + return { + pageTypeId, + semantics: 'page-component', + deliveryPath: 'page-component-plus-slot-fill', + enforcement: 'force-page-component-when-supported', + supportStatus: selectedCandidate + ? 'ready' + : evaluatedCandidates.length > 0 + ? 'blocked' + : 'unsupported', + selectedDeliveryKind: selectedCandidate?.candidateKind || '', + selectedComponentId: selectedCandidate?.componentId || '', + selectedSource: selectedCandidate + ? selectedCandidate.candidateKind === 'project-certified-carrier' + ? 'project-overlay' + : 'skill-global' + : '', + projectCarrier: projectCarrier + ? { + status: projectCarrier.status, + componentId: projectCarrier.componentId, + blockingReasons: projectCarrier.blockingReasons, + runtimeBridge: projectCarrier.runtimeBridge, + } + : { + status: 'missing', + componentId: '', + blockingReasons: [], + runtimeBridge: { + required: mode === 'legacy-host-compatible', + status: mode === 'legacy-host-compatible' ? 'blocked' : 'not-applicable', + profileId: '', + blockingReasons: [], + }, + }, + standardComponent: standardComponent + ? { + status: standardComponent.status, + componentId: standardComponent.componentId, + blockingReasons: standardComponent.blockingReasons, + runtimeBridge: standardComponent.runtimeBridge, + } + : { + status: 'missing', + componentId: '', + blockingReasons: [], + runtimeBridge: { + required: mode === 'legacy-host-compatible', + status: mode === 'legacy-host-compatible' ? 'blocked' : 'not-applicable', + profileId: '', + blockingReasons: [], + }, + }, + blockingReasons: uniqueValues(evaluatedCandidates.flatMap((candidate) => candidate.blockingReasons || [])), + candidates: evaluatedCandidates.map((candidate) => ({ + kind: candidate.candidateKind, + status: candidate.status, + componentId: candidate.componentId, + runtimeBridge: candidate.runtimeBridge, + blockingReasons: candidate.blockingReasons, + })), + } +} + +export async function loadProjectTypicalPageSupportFact({ + targetRoot, + skillRoot, + modeOverride = '', + legacyHostFamily = null, + projectCarrierRealization = null, +}) { + const modeKey = normalizeModeKey(modeOverride) + const effectiveMode = resolveFactModeId(modeOverride) + const fingerprintPaths = [ + ...PROJECT_TYPICAL_PAGE_SUPPORT_FINGERPRINT_PATHS, + path.join(skillRoot, 'rules', 'page-component-registry.json'), + path.join(skillRoot, 'rules', 'page-component-certifications'), + path.join(skillRoot, 'rules', 'runtime-bridged-component-matrix.json'), + path.join(skillRoot, 'rules', 'adapter-registry.json'), + ] + + return loadCachedProjectFact({ + targetRoot, + factId: 'project-typical-page-support', + modeKey, + fingerprintPaths, + compute: async () => { + const resolvedLegacyHostFamily = + effectiveMode === 'legacy-host-compatible' + ? legacyHostFamily || + await loadLegacyHostFamilyFact({ + targetRoot, + skillRoot, + modeOverride: effectiveMode, + }) + : null + const carrierRealization = projectCarrierRealization || + await loadProjectCarrierRealizationFact({ + targetRoot, + skillRoot, + modeOverride, + }) + const carrierRealizationByComponentId = new Map( + (carrierRealization?.components || []).map((component) => [component.componentId, component]) + ) + const registry = loadPageComponentRegistry({ skillRoot, targetRoot }) + const pageTypeIds = effectiveMode + ? uniqueValues( + registry.components + .filter((component) => supportedModesForPageComponent(component).includes(effectiveMode)) + .map((component) => component.pageTypeId) + ).sort() + : [] + const pageTypes = pageTypeIds.map((pageTypeId) => + summarizePageTypeSupport({ + mode: effectiveMode, + pageTypeId, + candidates: listPageComponentsForModeAndPageType({ + mode: effectiveMode, + pageTypeId, + skillRoot, + targetRoot, + }), + legacyHostFamily: resolvedLegacyHostFamily, + skillRoot, + targetRoot, + carrierRealizationByComponentId, + }) + ) + + return { + schemaVersion: 'project-typical-page-support.v1', + mode: effectiveMode || modeKey, + status: pageTypes.length === 0 + ? 'not-applicable' + : pageTypes.some((pageType) => pageType.supportStatus === 'blocked') + ? 'blocked' + : 'ready', + source: 'page-component-registry', + enforcement: { + deliverySemantics: 'page-component', + deliveryPath: 'page-component-plus-slot-fill', + fallbackPolicy: 'block-before-business-shell-fallback', + selectionOrder: ['project-certified-carrier', 'direct-standard-component'], + }, + readyPageTypeIds: pageTypes + .filter((pageType) => pageType.supportStatus === 'ready') + .map((pageType) => pageType.pageTypeId), + blockedPageTypeIds: pageTypes + .filter((pageType) => pageType.supportStatus === 'blocked') + .map((pageType) => pageType.pageTypeId), + unsupportedPageTypeIds: pageTypes + .filter((pageType) => pageType.supportStatus === 'unsupported') + .map((pageType) => pageType.pageTypeId), + pageTypes, + } + }, + }) +} + +export async function loadLegacyStyleBoundaryFact({ + targetRoot, + modeOverride = '', +}) { + const modeKey = normalizeModeKey(modeOverride) + const effectiveMode = resolveFactModeId(modeOverride) + + return loadCachedProjectFact({ + targetRoot, + factId: 'legacy-style-boundary', + modeKey, + fingerprintPaths: PROJECT_LEGACY_STYLE_BOUNDARY_FINGERPRINT_PATHS, + compute: async () => { + if (effectiveMode !== 'legacy-host-compatible') { + return { + schemaVersion: 'legacy-style-boundary.v1', + mode: effectiveMode || modeKey, + factPath: `.local-context/hiui-design/outputs/project-facts/legacy-style-boundary.${modeKey || 'auto'}.json`, + status: 'not-applicable', + riskLevel: 'low', + riskSignals: [], + packageVersions: {}, + packageResolution: {}, + styleBoundary: {}, + globalStyleOverrides: [], + blockingReasons: [], + } + } + + const legacyBoundaryPath = path.join( + targetRoot, + '.local-context', + 'hiui-design', + 'outputs', + 'legacy-host-boundary.json' + ) + const legacyBoundary = await readJsonIfExists(legacyBoundaryPath) + const rootPackageJson = await readJsonIfExists(path.join(targetRoot, 'package.json')) + const declaredHiui5Version = String( + rootPackageJson?.dependencies?.hiui5 || rootPackageJson?.devDependencies?.hiui5 || '' + ).trim() + const hiui5Info = await loadInstalledPackageInfo(targetRoot, 'hiui5') + const queryFilterInfo = await loadInstalledPackageInfo(targetRoot, '@hi-ui/query-filter') + const inputInfo = queryFilterInfo.packageDir + ? await loadInstalledPackageInfo(targetRoot, '@hi-ui/input', queryFilterInfo.packageDir) + : await loadInstalledPackageInfo(targetRoot, '@hi-ui/input') + const queryFilterEntrySource = queryFilterInfo.resolvedEntryPath + ? await readTextIfExists(queryFilterInfo.resolvedEntryPath) + : '' + const queryFilterStyleImportDetected = /styles\/index\.scss\.js/.test(queryFilterEntrySource) + const queryFilterStyleEntryPath = queryFilterInfo.resolvedEntryPath + ? normalizePathForFact( + path.join(path.dirname(queryFilterInfo.resolvedEntryPath), 'styles', 'index.scss.js') + ) + : '' + const queryFilterStyleSource = queryFilterStyleEntryPath + ? await readTextIfExists(queryFilterStyleEntryPath) + : '' + const searchInputFilledSkinAvailable = + /query-filter-search-input/.test(queryFilterStyleSource) && + /appearance-filled/.test(queryFilterStyleSource) + const styleFiles = await collectGlobalStyleFiles(targetRoot) + const globalOverrideEntries = [] + + for (const styleFilePath of styleFiles) { + const styleSource = await readTextIfExists(styleFilePath) + const overrideSnippets = detectGlobalInputOverrideSnippets(styleSource) + + if (overrideSnippets.length > 0) { + globalOverrideEntries.push({ + file: normalizePathForFact(path.relative(targetRoot, styleFilePath)), + selectors: overrideSnippets, + }) + } + } + + const releaseTracks = uniqueValues([ + releaseTrackForVersion(declaredHiui5Version), + hiui5Info.releaseTrack, + queryFilterInfo.releaseTrack, + inputInfo.releaseTrack, + ]).filter(Boolean) + const inputPackageResolutionAligned = Boolean( + hiui5Info.version && + queryFilterInfo.version && + inputInfo.version && + releaseTracks.length <= 1 + ) + const queryFilterCssLoadedRisk = + !queryFilterInfo.found || !queryFilterStyleImportDetected || !searchInputFilledSkinAvailable + const hostGlobalInputOverrideRisk = globalOverrideEntries.length > 0 + const riskSignals = [] + + if (!queryFilterInfo.found) { + riskSignals.push('@hi-ui/query-filter package is missing or unresolved from target root') + } + + if (!inputInfo.found) { + riskSignals.push('@hi-ui/input package is missing or unresolved from query-filter context') + } + + if ( + queryFilterInfo.found && + inputInfo.found && + hiui5Info.found && + !inputPackageResolutionAligned + ) { + riskSignals.push( + `HiUI package release tracks are not aligned (declared hiui5=${releaseTrackForVersion( + declaredHiui5Version + ) || '(missing)'}, resolved hiui5=${hiui5Info.releaseTrack || '(missing)'}, query-filter=${ + queryFilterInfo.releaseTrack || '(missing)' + }, input=${inputInfo.releaseTrack || '(missing)'})` + ) + } + + if (!queryFilterStyleImportDetected) { + riskSignals.push('@hi-ui/query-filter entry does not expose its style side-effect import') + } + + if (!searchInputFilledSkinAvailable) { + riskSignals.push('@hi-ui/query-filter styles do not expose the filled search-input skin rule') + } + + if (hostGlobalInputOverrideRisk) { + riskSignals.push( + `global style files directly target hi-v5 input/query-filter selectors (${globalOverrideEntries + .map((entry) => entry.file) + .join(', ')})` + ) + } + + return { + schemaVersion: 'legacy-style-boundary.v1', + mode: effectiveMode || modeKey, + factPath: `.local-context/hiui-design/outputs/project-facts/legacy-style-boundary.${modeKey || 'auto'}.json`, + status: riskSignals.length > 0 ? 'risk' : 'ready', + riskLevel: riskSignals.length > 0 ? 'high' : 'low', + riskSignals, + packageVersions: { + declaredHiui5Version, + resolvedHiui5Version: hiui5Info.version, + queryFilterVersion: queryFilterInfo.version, + inputVersion: inputInfo.version, + }, + packageResolution: { + hiui5EntryPath: hiui5Info.resolvedEntryPath, + queryFilterEntryPath: queryFilterInfo.resolvedEntryPath, + queryFilterResolvesInputPath: inputInfo.resolvedEntryPath, + inputPackageResolutionAligned, + releaseTracks, + }, + styleBoundary: { + boundaryStatus: String(legacyBoundary?.styleBoundary?.status || '').trim() || 'missing', + queryFilterStyleImportDetected, + queryFilterStyleEntryPath, + searchInputFilledSkinAvailable, + queryFilterCssLoadedRisk, + hostGlobalInputOverrideRisk, + }, + globalStyleOverrides: globalOverrideEntries, + blockingReasons: [], + } + }, + }) +} + +function normalizeLegacyDeliveryPolicyId(value) { + const policyId = String(value || '').trim() + if ( + [ + 'direct-standard-allowed', + 'carrier-first-required', + 'managed-analytics-only', + 'explicit-fallback-only', + ].includes(policyId) + ) { + return policyId + } + return '' +} + +function outputPathForLegacyDeliveryPolicy(modeKey = '') { + const normalizedMode = String(modeKey || '').trim() || 'legacy-host-compatible' + return LEGACY_PAGE_TYPE_DELIVERY_POLICY_OUTPUT_TEMPLATE.replace('%MODE%', normalizedMode) +} + +function summarizeLegacyDeliveryPolicyEntry({ + mode, + pageTypeId, + deliveryPolicy, + reason = '', + supportEntry = null, +}) { + const selectedDeliveryKind = String(supportEntry?.selectedDeliveryKind || '').trim() + const selectedComponentId = String(supportEntry?.selectedComponentId || '').trim() + const projectCarrierStatus = String(supportEntry?.projectCarrier?.status || 'missing').trim() || 'missing' + const standardComponentStatus = + String(supportEntry?.standardComponent?.status || 'missing').trim() || 'missing' + const supportStatus = String(supportEntry?.supportStatus || 'missing').trim() || 'missing' + const baseBlockingReasons = Array.isArray(supportEntry?.blockingReasons) + ? supportEntry.blockingReasons.map((entry) => String(entry || '').trim()).filter(Boolean) + : [] + const projectCarrierBlockingReasons = Array.isArray(supportEntry?.projectCarrier?.blockingReasons) + ? supportEntry.projectCarrier.blockingReasons.map((entry) => String(entry || '').trim()).filter(Boolean) + : [] + + if (deliveryPolicy === 'carrier-first-required') { + const policyReady = projectCarrierStatus === 'ready' + return { + pageTypeId, + deliveryPolicy, + reason: String(reason || '').trim(), + policyStatus: policyReady ? 'ready' : 'blocked', + supportStatus, + selectedDeliveryKind, + selectedComponentId, + projectCarrierStatus, + standardComponentStatus, + blockingReasons: policyReady + ? [] + : uniqueValues([ + `legacy pageType ${pageTypeId} requires a project-certified carrier before page generation`, + ...(projectCarrierStatus === 'missing' + ? [`legacy pageType ${pageTypeId} does not have a ready project-certified carrier`] + : []), + ...(selectedDeliveryKind === 'direct-standard-component' + ? [ + `legacy pageType ${pageTypeId} currently resolves to direct-standard-component, but delivery policy requires project-certified-carrier first`, + ] + : []), + ...projectCarrierBlockingReasons, + ...baseBlockingReasons, + ]), + } + } + + const policyReady = supportStatus === 'ready' + return { + pageTypeId, + deliveryPolicy, + reason: String(reason || '').trim(), + policyStatus: policyReady ? 'ready' : 'blocked', + supportStatus, + selectedDeliveryKind, + selectedComponentId, + projectCarrierStatus, + standardComponentStatus, + blockingReasons: policyReady + ? [] + : uniqueValues([ + `legacy pageType ${pageTypeId} does not have a ready page-component delivery path for policy ${deliveryPolicy}`, + ...baseBlockingReasons, + ...projectCarrierBlockingReasons, + ]), + } +} + +export async function loadLegacyDeliveryPolicyFact({ + targetRoot, + skillRoot, + modeOverride = '', + projectTypicalPageSupport = null, +}) { + const modeKey = normalizeModeKey(modeOverride) + const effectiveMode = resolveFactModeId(modeOverride) + const outputRelativePath = outputPathForLegacyDeliveryPolicy(effectiveMode || modeKey) + const fingerprintPaths = [ + ...PROJECT_LEGACY_DELIVERY_POLICY_FINGERPRINT_PATHS, + path.join(skillRoot, LEGACY_PAGE_TYPE_DELIVERY_POLICY_RULES_RELATIVE_PATH), + ] + + const payload = await loadCachedProjectFact({ + targetRoot, + factId: 'legacy-delivery-policy', + modeKey, + fingerprintPaths, + compute: async () => { + if (effectiveMode !== 'legacy-host-compatible') { + return { + schemaVersion: 'legacy-delivery-policy.v1', + mode: effectiveMode || modeKey, + status: 'not-applicable', + source: LEGACY_PAGE_TYPE_DELIVERY_POLICY_RULES_RELATIVE_PATH, + factPath: outputRelativePath, + carrierReady: null, + carrierFirstRequiredPageTypes: [], + directStandardAllowedPageTypes: [], + pageTypes: [], + blockingReasons: [], + } + } + + const rules = await readJsonIfExists( + path.join(skillRoot, LEGACY_PAGE_TYPE_DELIVERY_POLICY_RULES_RELATIVE_PATH) + ) + const support = + projectTypicalPageSupport || + (await loadProjectTypicalPageSupportFact({ + targetRoot, + skillRoot, + modeOverride, + })) + const supportByPageTypeId = new Map( + (Array.isArray(support?.pageTypes) ? support.pageTypes : []).map((entry) => [entry.pageTypeId, entry]) + ) + const configuredPageTypes = Array.isArray(rules?.pageTypes) ? rules.pageTypes : [] + const pageTypes = configuredPageTypes + .map((entry) => ({ + pageTypeId: String(entry?.pageTypeId || '').trim(), + deliveryPolicy: normalizeLegacyDeliveryPolicyId(entry?.deliveryPolicy), + reason: String(entry?.reason || '').trim(), + })) + .filter((entry) => entry.pageTypeId && entry.deliveryPolicy) + .map((entry) => + summarizeLegacyDeliveryPolicyEntry({ + mode: effectiveMode, + pageTypeId: entry.pageTypeId, + deliveryPolicy: entry.deliveryPolicy, + reason: entry.reason, + supportEntry: supportByPageTypeId.get(entry.pageTypeId) || null, + }) + ) + + const carrierFirstRequiredPageTypes = pageTypes + .filter((entry) => entry.deliveryPolicy === 'carrier-first-required') + .map((entry) => entry.pageTypeId) + const directStandardAllowedPageTypes = pageTypes + .filter((entry) => entry.deliveryPolicy === 'direct-standard-allowed') + .map((entry) => entry.pageTypeId) + const blockingReasons = uniqueValues( + pageTypes + .filter((entry) => entry.policyStatus === 'blocked') + .flatMap((entry) => entry.blockingReasons || []) + ) + + return { + schemaVersion: 'legacy-delivery-policy.v1', + mode: effectiveMode, + status: pageTypes.length === 0 ? 'not-applicable' : blockingReasons.length > 0 ? 'blocked' : 'ready', + source: LEGACY_PAGE_TYPE_DELIVERY_POLICY_RULES_RELATIVE_PATH, + factPath: outputRelativePath, + carrierReady: carrierFirstRequiredPageTypes.every((pageTypeId) => { + const entry = pageTypes.find((item) => item.pageTypeId === pageTypeId) + return entry?.policyStatus === 'ready' + }), + carrierFirstRequiredPageTypes, + directStandardAllowedPageTypes, + pageTypes, + blockingReasons, + } + }, + }) + + if (await cacheRootExists(targetRoot)) { + await writeDerivedOutput(targetRoot, outputRelativePath, payload) + } + + return payload +} diff --git a/skills/hiui-design/scripts/lib/project-integration-state.mjs b/skills/hiui-design/scripts/lib/project-integration-state.mjs index fcbc6dfe2..ee17705af 100644 --- a/skills/hiui-design/scripts/lib/project-integration-state.mjs +++ b/skills/hiui-design/scripts/lib/project-integration-state.mjs @@ -418,6 +418,11 @@ function defaultLegacyDeliveryPolicy(mode = '') { carrierReady: null, carrierFirstRequiredPageTypes: [], directStandardAllowedPageTypes: [], + requiredLegacyPageTypes: [], + deferredLegacyPageTypes: [], + certifiedLegacyPageTypes: [], + missingRequiredLegacyPageTypes: [], + legacyRolloutCoverageStatus: 'not-applicable', pageTypes: [], blockingReasons: [], } @@ -429,6 +434,11 @@ function defaultLegacyDeliveryPolicy(mode = '') { carrierReady: false, carrierFirstRequiredPageTypes: [], directStandardAllowedPageTypes: [], + requiredLegacyPageTypes: [], + deferredLegacyPageTypes: [], + certifiedLegacyPageTypes: [], + missingRequiredLegacyPageTypes: [], + legacyRolloutCoverageStatus: 'not-applicable', pageTypes: [], blockingReasons: [], } @@ -442,37 +452,98 @@ function normalizeLegacyDeliveryPolicy(raw, mode = '') { const fallback = defaultLegacyDeliveryPolicy(mode) const normalizeIds = (value) => Array.isArray(value) ? value.map((item) => String(item || '').trim()).filter(Boolean) : [] + const pageTypes = Array.isArray(raw.pageTypes) + ? raw.pageTypes.map((entry) => ({ + pageTypeId: String(entry?.pageTypeId || '').trim(), + deliveryPolicy: String(entry?.deliveryPolicy || '').trim(), + reason: String(entry?.reason || '').trim(), + policyStatus: String(entry?.policyStatus || '').trim() || 'blocked', + supportStatus: String(entry?.supportStatus || '').trim() || 'missing', + selectedDeliveryKind: String(entry?.selectedDeliveryKind || '').trim(), + selectedComponentId: String(entry?.selectedComponentId || '').trim(), + projectCarrierStatus: String(entry?.projectCarrierStatus || '').trim() || 'missing', + standardComponentStatus: String(entry?.standardComponentStatus || '').trim() || 'missing', + blockingReasons: Array.isArray(entry?.blockingReasons) + ? entry.blockingReasons.map((reason) => String(reason || '').trim()).filter(Boolean) + : [], + })) + : [] + const carrierFirstRequiredPageTypes = normalizeIds(raw.carrierFirstRequiredPageTypes) + const directStandardAllowedPageTypes = normalizeIds(raw.directStandardAllowedPageTypes) + const requiredLegacyPageTypes = (() => { + const explicit = normalizeIds(raw.requiredLegacyPageTypes) + if (explicit.length > 0) { + return explicit + } + if (carrierFirstRequiredPageTypes.length > 0) { + return carrierFirstRequiredPageTypes + } + return pageTypes + .filter((entry) => entry.deliveryPolicy === 'carrier-first-required') + .map((entry) => entry.pageTypeId) + })() + const deferredLegacyPageTypes = normalizeIds(raw.deferredLegacyPageTypes) + const certifiedLegacyPageTypes = (() => { + const explicit = normalizeIds(raw.certifiedLegacyPageTypes) + if (explicit.length > 0) { + return explicit + } + return pageTypes + .filter( + (entry) => + entry.projectCarrierStatus === 'ready' || + entry.selectedDeliveryKind === 'project-certified-carrier' + ) + .map((entry) => entry.pageTypeId) + })() + const missingRequiredLegacyPageTypes = requiredLegacyPageTypes.filter( + (pageTypeId) => !certifiedLegacyPageTypes.includes(pageTypeId) + ) + const derivedCoverageStatus = + normalizeModeId(mode) !== 'legacy-host-compatible' + ? 'not-applicable' + : requiredLegacyPageTypes.length === 0 + ? 'not-applicable' + : missingRequiredLegacyPageTypes.length === 0 + ? 'ready' + : 'blocked' + const legacyRolloutCoverageStatus = ['ready', 'blocked', 'not-applicable'].includes( + String(raw.legacyRolloutCoverageStatus || '').trim() + ) + ? String(raw.legacyRolloutCoverageStatus || '').trim() + : derivedCoverageStatus + const derivedBlockingReasons = + legacyRolloutCoverageStatus === 'blocked' && missingRequiredLegacyPageTypes.length > 0 + ? [ + `legacy rollout is missing required project-certified carriers for page types: ${missingRequiredLegacyPageTypes.join(', ')}`, + ] + : [] + const normalizedBlockingReasons = uniqueBlockingReasons([ + ...(Array.isArray(raw.blockingReasons) + ? raw.blockingReasons.map((reason) => String(reason || '').trim()).filter(Boolean) + : fallback.blockingReasons), + ...derivedBlockingReasons, + ]) + const carrierReady = + typeof raw.carrierReady === 'boolean' + ? raw.carrierReady + : legacyRolloutCoverageStatus === 'ready' return { status: ['ready', 'blocked', 'not-applicable'].includes(String(raw.status || '').trim()) ? String(raw.status || '').trim() - : fallback.status, + : derivedCoverageStatus, factPath: String(raw.factPath || '').trim(), - carrierReady: - typeof raw.carrierReady === 'boolean' - ? raw.carrierReady - : fallback.carrierReady, - carrierFirstRequiredPageTypes: normalizeIds(raw.carrierFirstRequiredPageTypes), - directStandardAllowedPageTypes: normalizeIds(raw.directStandardAllowedPageTypes), - pageTypes: Array.isArray(raw.pageTypes) - ? raw.pageTypes.map((entry) => ({ - pageTypeId: String(entry?.pageTypeId || '').trim(), - deliveryPolicy: String(entry?.deliveryPolicy || '').trim(), - reason: String(entry?.reason || '').trim(), - policyStatus: String(entry?.policyStatus || '').trim() || 'blocked', - supportStatus: String(entry?.supportStatus || '').trim() || 'missing', - selectedDeliveryKind: String(entry?.selectedDeliveryKind || '').trim(), - selectedComponentId: String(entry?.selectedComponentId || '').trim(), - projectCarrierStatus: String(entry?.projectCarrierStatus || '').trim() || 'missing', - standardComponentStatus: String(entry?.standardComponentStatus || '').trim() || 'missing', - blockingReasons: Array.isArray(entry?.blockingReasons) - ? entry.blockingReasons.map((reason) => String(reason || '').trim()).filter(Boolean) - : [], - })) - : [], - blockingReasons: Array.isArray(raw.blockingReasons) - ? raw.blockingReasons.map((reason) => String(reason || '').trim()).filter(Boolean) - : fallback.blockingReasons, + carrierReady, + carrierFirstRequiredPageTypes, + directStandardAllowedPageTypes, + requiredLegacyPageTypes, + deferredLegacyPageTypes, + certifiedLegacyPageTypes, + missingRequiredLegacyPageTypes, + legacyRolloutCoverageStatus, + pageTypes, + blockingReasons: normalizedBlockingReasons, } } @@ -599,11 +670,13 @@ function filterExplicitIntegrationBlockingReasons({ blockingReasons = [], carrierValidation, legacyBridgeValidation, + legacyDeliveryPolicy, typicalPageSupport, }) { const derivedReasons = new Set([ ...(carrierValidation?.blockingReasons || []), ...(legacyBridgeValidation?.blockingReasons || []), + ...(legacyDeliveryPolicy?.blockingReasons || []), ...typicalPageSupportBlockingReasons(typicalPageSupport), ]) @@ -617,11 +690,13 @@ function deriveIntegrationBlockingReasons({ blockingReasons = [], carrierValidation, legacyBridgeValidation, + legacyDeliveryPolicy, }) { return uniqueBlockingReasons([ ...blockingReasons, ...(integrationUsesLegacyCarrierValidation(mode) ? carrierValidation?.blockingReasons || [] : []), ...(integrationUsesLegacyCarrierValidation(mode) ? legacyBridgeValidation?.blockingReasons || [] : []), + ...(integrationUsesLegacyCarrierValidation(mode) ? legacyDeliveryPolicy?.blockingReasons || [] : []), ]) } @@ -630,6 +705,7 @@ function deriveIntegrationReady({ blockingReasons = [], carrierValidation, legacyBridgeValidation, + legacyDeliveryPolicy, }) { if (blockingReasons.length > 0) { return false @@ -643,6 +719,13 @@ function deriveIntegrationReady({ return false } + if ( + integrationUsesLegacyCarrierValidation(mode) && + legacyDeliveryPolicy?.legacyRolloutCoverageStatus === 'blocked' + ) { + return false + } + return true } @@ -689,6 +772,7 @@ function normalizeProjectIntegrationState(raw) { blockingReasons: Array.isArray(raw.blockingReasons) ? raw.blockingReasons : [], carrierValidation, legacyBridgeValidation, + legacyDeliveryPolicy, typicalPageSupport, }) const blockingReasons = deriveIntegrationBlockingReasons({ @@ -696,12 +780,14 @@ function normalizeProjectIntegrationState(raw) { blockingReasons: explicitBlockingReasons, carrierValidation, legacyBridgeValidation, + legacyDeliveryPolicy, }) const integrationReady = deriveIntegrationReady({ mode, blockingReasons, carrierValidation, legacyBridgeValidation, + legacyDeliveryPolicy, }) const legacyRuntimeReady = deriveLegacyRuntimeReady({ mode, legacyBridgeValidation }) const legacyCarrierReady = deriveLegacyCarrierReady({ @@ -733,6 +819,11 @@ function normalizeProjectIntegrationState(raw) { pageTypeDeliveryPolicyRef: legacyDeliveryPolicy.factPath, carrierFirstRequiredPageTypes: legacyDeliveryPolicy.carrierFirstRequiredPageTypes, directStandardAllowedPageTypes: legacyDeliveryPolicy.directStandardAllowedPageTypes, + requiredLegacyPageTypes: legacyDeliveryPolicy.requiredLegacyPageTypes, + deferredLegacyPageTypes: legacyDeliveryPolicy.deferredLegacyPageTypes, + certifiedLegacyPageTypes: legacyDeliveryPolicy.certifiedLegacyPageTypes, + missingRequiredLegacyPageTypes: legacyDeliveryPolicy.missingRequiredLegacyPageTypes, + legacyRolloutCoverageStatus: legacyDeliveryPolicy.legacyRolloutCoverageStatus, legacyDeliveryPolicy, typicalPageSupport, blockingReasons, @@ -790,12 +881,14 @@ export async function writeProjectIntegrationState({ blockingReasons: [], carrierValidation, legacyBridgeValidation, + legacyDeliveryPolicy, }) const blockingReasons = deriveIntegrationBlockingReasons({ mode: normalizedMode, blockingReasons: [], carrierValidation, legacyBridgeValidation, + legacyDeliveryPolicy, }) const legacyRuntimeReady = deriveLegacyRuntimeReady({ mode: normalizedMode, @@ -826,6 +919,11 @@ export async function writeProjectIntegrationState({ pageTypeDeliveryPolicyRef: legacyDeliveryPolicy.factPath, carrierFirstRequiredPageTypes: legacyDeliveryPolicy.carrierFirstRequiredPageTypes, directStandardAllowedPageTypes: legacyDeliveryPolicy.directStandardAllowedPageTypes, + requiredLegacyPageTypes: legacyDeliveryPolicy.requiredLegacyPageTypes, + deferredLegacyPageTypes: legacyDeliveryPolicy.deferredLegacyPageTypes, + certifiedLegacyPageTypes: legacyDeliveryPolicy.certifiedLegacyPageTypes, + missingRequiredLegacyPageTypes: legacyDeliveryPolicy.missingRequiredLegacyPageTypes, + legacyRolloutCoverageStatus: legacyDeliveryPolicy.legacyRolloutCoverageStatus, legacyDeliveryPolicy, typicalPageSupport, blockingReasons, @@ -861,6 +959,11 @@ export async function writeProjectIntegrationState({ pageTypeDeliveryPolicyRef: legacyDeliveryPolicy.factPath, carrierFirstRequiredPageTypes: legacyDeliveryPolicy.carrierFirstRequiredPageTypes, directStandardAllowedPageTypes: legacyDeliveryPolicy.directStandardAllowedPageTypes, + requiredLegacyPageTypes: legacyDeliveryPolicy.requiredLegacyPageTypes, + deferredLegacyPageTypes: legacyDeliveryPolicy.deferredLegacyPageTypes, + certifiedLegacyPageTypes: legacyDeliveryPolicy.certifiedLegacyPageTypes, + missingRequiredLegacyPageTypes: legacyDeliveryPolicy.missingRequiredLegacyPageTypes, + legacyRolloutCoverageStatus: legacyDeliveryPolicy.legacyRolloutCoverageStatus, blockingReasons: Array.isArray(legacyDeliveryPolicy.blockingReasons) ? legacyDeliveryPolicy.blockingReasons : [], diff --git a/skills/hiui-design/scripts/lib/rules-only-page-contracts.mjs b/skills/hiui-design/scripts/lib/rules-only-page-contracts.mjs index c149724c5..303d4fb19 100644 --- a/skills/hiui-design/scripts/lib/rules-only-page-contracts.mjs +++ b/skills/hiui-design/scripts/lib/rules-only-page-contracts.mjs @@ -70,19 +70,22 @@ function isLegacyPageComponentGenerationProfile({ mode, profile }) { } function expectedGenerationProfileSemantics({ mode, profile }) { - const legacyPageComponentFastPath = isLegacyPageComponentGenerationProfile({ mode, profile }) + const pageComponentStart = + String(profile?.strategy || '').trim() === 'page-component' && + String(profile?.startFrom || '').trim() === 'page-component' + const strictSourceProof = + String(profile?.sourceProofLevel || '').trim() === 'strict-source-adapter-proof' return { - hostArchetypeRequired: !legacyPageComponentFastPath, - requiredGates: legacyPageComponentFastPath - ? ['source-gate', 'preflight', 'page-instance-validation'] + hostArchetypeRequired: !pageComponentStart, + requiredGates: pageComponentStart + ? strictSourceProof + ? ['source-gate', 'preflight', 'page-instance-validation'] + : ['slot-gate', 'preflight', 'page-instance-validation'] : mode === 'legacy-host-compatible' ? ['source-gate', 'preflight'] : ['slot-gate', 'preflight'], - sourceProofLevel: - mode === 'legacy-host-compatible' - ? 'strict-source-adapter-proof' - : 'slot-boundary-proof', + sourceProofLevel: strictSourceProof ? 'strict-source-adapter-proof' : 'slot-boundary-proof', } } @@ -95,6 +98,7 @@ function buildDefaultGenerationProfile({ pageTypeId, archetypeMode, archetypeTem strategy: usesLegacyRuntimeBridge ? 'page-component' : 'managed-template-or-archetype-slot-fill', + startFrom: usesLegacyRuntimeBridge ? 'page-component' : archetypeTemplatePath ? 'template' : 'reference-or-scaffold', legacyStrategyId: usesLegacyRuntimeBridge ? 'runtime-bridged-page-component' : 'managed-template-or-archetype-slot-fill', @@ -560,6 +564,9 @@ export function buildManagedPageWorkflowMetadata(overrides = {}) { const isFinalized = status === 'finalized' const isPreflightPass = status === 'preflight-pass' const isStale = status === 'stale' + const normalizedDeferredChecks = Array.isArray(overrides.deferredChecks) + ? [...new Set(overrides.deferredChecks.map((item) => String(item || '').trim()).filter(Boolean))] + : [] return { status, @@ -582,6 +589,21 @@ export function buildManagedPageWorkflowMetadata(overrides = {}) { runtimeSmokeReportPath: String(overrides.runtimeSmokeReportPath || '').trim() || '', sourceSnapshotHash: String(overrides.sourceSnapshotHash || '').trim() || '', staleReason: String(overrides.staleReason || '').trim() || '', + preflightStage: String(overrides.preflightStage || '').trim() || '', + readyForImplementation: + typeof overrides.readyForImplementation === 'boolean' + ? overrides.readyForImplementation + : isPreflightPass || isFinalized || isStale, + readyForDelivery: + typeof overrides.readyForDelivery === 'boolean' + ? overrides.readyForDelivery + : isPreflightPass || isFinalized, + deferredChecks: + normalizedDeferredChecks.length > 0 + ? normalizedDeferredChecks + : isStale + ? ['finalizePage'] + : [], reportPath: String(overrides.reportPath || '').trim() || '', smokeReportPath: String(overrides.smokeReportPath || '').trim() || '', lastCommand: String(overrides.lastCommand || '').trim() || '', @@ -1344,6 +1366,29 @@ export function validateRulesOnlyPageContract({ ) } + if (workflow.preflightStatus === 'pass') { + if (!workflow.preflightStage) { + warnings.push( + 'workflow.preflightStage is missing. Re-run typical-page:preflight so collaborators can distinguish scaffold-baseline from delivery-ready implementation.' + ) + } + if (typeof workflow.readyForImplementation !== 'boolean') { + warnings.push( + 'workflow.readyForImplementation is missing. Re-run typical-page:preflight so workflow metadata records whether implementation may continue.' + ) + } + if (typeof workflow.readyForDelivery !== 'boolean') { + warnings.push( + 'workflow.readyForDelivery is missing. Re-run typical-page:preflight so delivery summaries know whether this page is already delivery-ready.' + ) + } + if (!Array.isArray(workflow.deferredChecks)) { + warnings.push( + 'workflow.deferredChecks is missing. Re-run typical-page:preflight so collaborators can see which checks remain deferred after a passing preflight.' + ) + } + } + if (workflow.status === 'finalized') { if (workflow.deliveryStatus !== 'finalized') { warnings.push( @@ -1671,7 +1716,11 @@ export function validateRulesOnlyPageContract({ ) } - if (expectedHostAdapter?.id) { + const pageComponentStart = + String(generationProfile?.strategy || '').trim() === 'page-component' && + String(generationProfile?.startFrom || '').trim() === 'page-component' + + if (expectedHostAdapter?.id && !pageComponentStart) { const actualHostAdapterId = String(adapterContract.hostAdapterId || '').trim() if (!actualHostAdapterId) { diff --git a/skills/hiui-design/scripts/plan-page-task.mjs b/skills/hiui-design/scripts/plan-page-task.mjs index 675a21c2d..c7d1b5350 100644 --- a/skills/hiui-design/scripts/plan-page-task.mjs +++ b/skills/hiui-design/scripts/plan-page-task.mjs @@ -7,6 +7,8 @@ import { detectHostProfile } from './lib/detect-host-profile.mjs' import { loadPageTypeManifest } from './lib/load-page-type-manifest.mjs' import { loadArchetypeDefinition } from './lib/archetypes/load-archetype-manifest.mjs' import { + getRequiredOwnershipRolesForPageType, + getRequiredRegionsForPageType, loadRulesOnlyPageContracts, normalizeContractPath, } from './lib/rules-only-page-contracts.mjs' @@ -14,6 +16,10 @@ import { classifyTypicalPageTargetPath, getTypicalPageReuseBlockingReason, } from './lib/typical-page-route-ownership.mjs' +import { + buildManagedInstanceMigrationReason, + evaluateManagedInstanceReadinessFromSource, +} from './lib/managed-page-instance-readiness.mjs' import { getEditableSlotsForPageType, getLockedRegionsForPageType, @@ -30,6 +36,7 @@ import { import { resolveRuntimeBridgeProfileForComponent } from './lib/runtime-bridge-profile-registry.mjs' import { getAdapterRegistryEntry } from './lib/adapter-registry.mjs' import { buildProjectCapabilities } from './lib/asset-control-surface.mjs' +import { readProjectIntegrationState } from './lib/project-integration-state.mjs' const CONTRACT_VERSION = 4 const LIST_SHELL_PAGE_TYPES = new Set([ @@ -233,18 +240,27 @@ async function readExistingPageFact(targetRoot, pagePath) { existingContract = null } + const contractFact = existingContract + ? { + pageTypeId: String(existingContract.pageTypeId || '').trim(), + workflowStatus: String(existingContract.workflow?.status || '').trim(), + preflightStatus: String(existingContract.workflow?.preflightStatus || '').trim(), + } + : null + const managedInstanceReadiness = evaluateManagedInstanceReadinessFromSource({ + contract: contractFact, + pageExists, + sourceRaw, + }) + return { page: normalizedPagePath, exists: pageExists, routeOwnership: classifyTypicalPageTargetPath(normalizedPagePath), sourcePageTypeId, - contract: existingContract - ? { - pageTypeId: String(existingContract.pageTypeId || '').trim(), - workflowStatus: String(existingContract.workflow?.status || '').trim(), - preflightStatus: String(existingContract.workflow?.preflightStatus || '').trim(), - } - : null, + sourceMarkers: managedInstanceReadiness.sourceMarkers, + contract: contractFact, + managedInstanceReadiness, } } @@ -1774,14 +1790,68 @@ function assetResolutionForPlan({ pageComponent, primaryGenerationAsset, generat } } -function factsForPlan({ projectCapabilities, mode, targetPage, assetResolution }) { +function summarizeProjectIntegrationForPlan(projectIntegration) { + if (!projectIntegration) { + return { + available: false, + status: 'missing', + integrationReady: null, + factPath: '', + source: '', + mode: '', + legacyRolloutCoverageStatus: 'not-applicable', + requiredLegacyPageTypes: [], + deferredLegacyPageTypes: [], + certifiedLegacyPageTypes: [], + missingRequiredLegacyPageTypes: [], + blockingReasons: [], + } + } + + return { + available: true, + status: projectIntegration.integrationReady ? 'ready' : 'blocked', + integrationReady: Boolean(projectIntegration.integrationReady), + factPath: String(projectIntegration.factPath || '').trim(), + source: String(projectIntegration.source || '').trim(), + mode: String(projectIntegration.mode || '').trim(), + legacyRolloutCoverageStatus: + String(projectIntegration.legacyRolloutCoverageStatus || '').trim() || 'not-applicable', + requiredLegacyPageTypes: Array.isArray(projectIntegration.requiredLegacyPageTypes) + ? projectIntegration.requiredLegacyPageTypes + : [], + deferredLegacyPageTypes: Array.isArray(projectIntegration.deferredLegacyPageTypes) + ? projectIntegration.deferredLegacyPageTypes + : [], + certifiedLegacyPageTypes: Array.isArray(projectIntegration.certifiedLegacyPageTypes) + ? projectIntegration.certifiedLegacyPageTypes + : [], + missingRequiredLegacyPageTypes: Array.isArray(projectIntegration.missingRequiredLegacyPageTypes) + ? projectIntegration.missingRequiredLegacyPageTypes + : [], + blockingReasons: Array.isArray(projectIntegration.blockingReasons) + ? projectIntegration.blockingReasons + : [], + } +} + +function factsForPlan({ projectCapabilities, projectIntegration, mode, targetPage, assetResolution }) { const blockingReasons = [] const projectRootStatus = projectCapabilities?.projectRootValid ? 'valid' : 'unavailable' + const projectIntegrationSummary = summarizeProjectIntegrationForPlan(projectIntegration) if (!mode?.id || mode.confirmed === false) { blockingReasons.push('project mode is not confirmed by plan-page-task') } + if (projectIntegrationSummary.available && projectIntegrationSummary.integrationReady === false) { + blockingReasons.push( + ...projectIntegrationSummary.blockingReasons.map((reason) => + `project integration is blocked: ${reason}` + ) + ) + } + if (targetPage?.routeOwnership?.isTypicalPageReuseAsset) { blockingReasons.push('target page is inside typical-page-reuse example assets instead of a business route') } @@ -1802,6 +1872,7 @@ function factsForPlan({ projectCapabilities, mode, targetPage, assetResolution } status: blockingReasons.length > 0 ? 'blocked' : 'ready', sources: { capabilities: projectCapabilities?.schemaVersion || '', + projectIntegration: projectIntegrationSummary.factPath || '', mode: mode?.factPath || (mode?.source === 'explicit' ? 'cli:--mode' : 'rules/mode-selection.md'), routeOwnership: 'typical-page-route-ownership', assetCatalog: assetResolution?.source || '', @@ -1809,6 +1880,7 @@ function factsForPlan({ projectCapabilities, mode, targetPage, assetResolution } digest: { mode: mode?.id || '', projectRoot: projectCapabilities?.projectRoot || '', + projectIntegration: projectIntegrationSummary.status, routeRole: targetPage?.routeOwnership?.role || '', asset: assetResolution?.componentId || assetResolution?.assetType || '', }, @@ -1818,12 +1890,14 @@ function factsForPlan({ projectCapabilities, mode, targetPage, assetResolution } 'project-root', 'capabilities', 'mode-lock', + ...(projectIntegrationSummary.available ? ['project-integration-state'] : []), 'route-ownership', 'asset-resolution', ...(assetResolution?.strategy === 'page-component' ? ['component-certification'] : []), ...(assetResolution?.runtimeBridgeResolution ? ['runtime-bridge-profile'] : []), ], requiredAgentChecks: [], + projectIntegration: projectIntegrationSummary, blockingReasons, } } @@ -1887,6 +1961,33 @@ function startFromForLegacyPageComponent({ generationStrategy, mode, pageCompone } } +function startFromForRulesOnlyPageComponent({ generationStrategy, mode, pageComponent }) { + if ( + mode !== 'rules-only' || + normalizedGenerationStrategyId(generationStrategy) !== 'page-component' || + pageComponent?.selected !== true + ) { + return null + } + + return { + id: 'page-component', + source: pageComponent?.source || 'page-component-registry', + templatePath: '', + examplePath: '', + hostArchetypePath: '', + reason: + 'rules-only selected a certified page component as the active start point; scaffold directly from the certified managed source instead of downgrading to template/example copy.', + } +} + +function startFromForPageComponentMainline({ generationStrategy, mode, pageComponent }) { + return ( + startFromForLegacyPageComponent({ generationStrategy, mode, pageComponent }) || + startFromForRulesOnlyPageComponent({ generationStrategy, mode, pageComponent }) + ) +} + function inferStartFrom({ archetype, mode, pageType, templatePath }) { if (!pageType) { return { @@ -2060,6 +2161,25 @@ function docsForPageTypes({ manifest, pageTypeIds }) { return Array.from(new Set(docs)) } +function makeRequiredDoc(pathValue, readMode, reason) { + return { + path: pathValue, + readMode, + reason, + } +} + +function uniqueDocs(docs) { + const seen = new Set() + const result = [] + for (const doc of docs) { + if (!doc?.path || seen.has(doc.path)) continue + seen.add(doc.path) + result.push(doc) + } + return result +} + function requiredDocsForPlan({ fastPath, i18nMode, @@ -2073,54 +2193,58 @@ function requiredDocsForPlan({ topology, }) { const docs = [ - 'rules/QUICK-START.md', - 'rules/page-type-map.md', - 'rules/contract-regions.md', - 'rules/generation-rules.md', - 'docs/generation/ai-kickoff-template.md', - 'docs/generation/hiui5-visual-baseline.md', + makeRequiredDoc('rules/QUICK-START.md', 'required', 'default planner entry'), + makeRequiredDoc('rules/page-type-map.md', 'required', 'page type and topology mapping'), + makeRequiredDoc('rules/contract-regions.md', 'required', 'contract field and region ownership baseline'), + makeRequiredDoc('rules/generation-rules.md', 'required', 'generation rules and phase boundaries'), + makeRequiredDoc('docs/generation/ai-kickoff-template.md', 'required', 'planner kickoff structure'), + makeRequiredDoc('docs/generation/hiui5-visual-baseline.md', 'required', 'visual baseline and typography scale'), ] if (!['project-lock', 'bootstrap-summary', 'explicit', 'explicit-alias:host-compatible'].includes(modeSource)) { - docs.splice(1, 0, 'rules/mode-selection.md') + docs.splice(1, 0, makeRequiredDoc('rules/mode-selection.md', 'required', 'mode inference fallback')) } if (isNonTypical) { - docs.push('docs/generation/non-typical-pages.md') + docs.push(makeRequiredDoc('docs/generation/non-typical-pages.md', 'required', 'non-typical layout routing')) } else if (topology?.id === 'single-page-composite') { - docs.push('docs/generation/implementation-checklist-template.md') + docs.push(makeRequiredDoc('docs/generation/implementation-checklist-template.md', 'required', 'composite layout implementation checklist')) } else if (pageTypeIds.length > 0) { - docs.push('docs/generation/figma-page-rules.md') + docs.push(makeRequiredDoc('docs/generation/figma-page-rules.md', 'required', 'page-type visual and semantic rules')) } - docs.push(...docsForPageTypes({ manifest, pageTypeIds })) + docs.push( + ...docsForPageTypes({ manifest, pageTypeIds }).map((docPath) => + makeRequiredDoc(docPath, 'required', 'matched page-type chapter') + ) + ) if (['rules-only', 'legacy-host-compatible'].includes(mode)) { - docs.push('docs/generation/rules-only-example-alignment.md') + docs.push(makeRequiredDoc('docs/generation/rules-only-example-alignment.md', 'required', 'managed example alignment baseline')) } if (mode === 'rules-only' && !fastPath.eligible && taskLevel.id !== 'minor-edit') { - docs.push('docs/generation/rules-only-component-matrix.md') + docs.push(makeRequiredDoc('docs/generation/rules-only-component-matrix.md', 'conditional', 'slow-path rules-only component routing')) } if (mode === 'legacy-host-compatible') { - docs.push('docs/generation/legacy-host-compatibility.md') + docs.push(makeRequiredDoc('docs/generation/legacy-host-compatibility.md', 'required', 'legacy host compatibility contract')) } if (pageComponent?.selected || pageComponent?.candidates?.length > 0) { - docs.push('docs/generation/page-level-components.md') - docs.push('docs/generation/component-certification.md') - docs.push('docs/designer/page-component-preview.md') + docs.push(makeRequiredDoc('docs/generation/page-level-components.md', 'required', 'page-component delivery semantics')) + docs.push(makeRequiredDoc('docs/generation/component-certification.md', 'reference', 'component certification reference')) + docs.push(makeRequiredDoc('docs/designer/page-component-preview.md', 'reference', 'component preview semantics')) if (mode === 'legacy-host-compatible') { - docs.push('rules/runtime-bridged-component-matrix.json') + docs.push(makeRequiredDoc('rules/runtime-bridged-component-matrix.json', 'required', 'runtime bridge profile lookup')) } } if (i18nMode.id === 'full') { - docs.push('docs/generation/i18n-baseline.md') + docs.push(makeRequiredDoc('docs/generation/i18n-baseline.md', 'conditional', 'explicit i18n or RTL requirement')) } - return Array.from(new Set(docs)) + return uniqueDocs(docs) } function shouldGenerateTranslationMap({ deliveryLevel, mode, pageFact, pageTypeId }) { @@ -2142,21 +2266,29 @@ function shouldRequireSourceGateInPrimaryActions({ deliveryLevel, generationStrategy, mode, + pageComponent, pageFact, taskLevel, }) { if (taskLevel.id === 'minor-edit') return false if (deliveryLevel.id === 'C') return false if (mode !== 'legacy-host-compatible') return false - if (usesLegacyPageComponentFastPath({ generationStrategy, mode })) return false + if ( + usesLegacyPageComponentFastPath({ generationStrategy, mode }) && + pageComponent?.selected === true && + primaryPageComponentCandidateForPlan(pageComponent)?.projectOverlay === true + ) { + return true + } if (pageFact?.pageTypeMigration) return true - return !pageFact?.exists + return false } function shouldRequireSlotGateInPrimaryActions({ deliveryLevel, generationStrategy, mode, + pageComponent, pageFact, taskLevel, }) { @@ -2167,6 +2299,7 @@ function shouldRequireSlotGateInPrimaryActions({ deliveryLevel, generationStrategy, mode, + pageComponent, pageFact, taskLevel, }) @@ -2265,6 +2398,7 @@ function requiredCommandsForTask({ deliveryLevel, generationStrategy, mode, + pageComponent, pageFact, pageTypeId, pageUnits = [], @@ -2350,6 +2484,7 @@ function requiredCommandsForTask({ deliveryLevel, generationStrategy, mode, + pageComponent, pageFact, taskLevel, }) @@ -2365,6 +2500,7 @@ function requiredCommandsForTask({ deliveryLevel, generationStrategy, mode, + pageComponent, pageFact, taskLevel, }) @@ -2428,6 +2564,7 @@ function requiredCommandsForTask({ deliveryLevel, generationStrategy, mode, + pageComponent, pageFact, taskLevel, }) @@ -2443,6 +2580,7 @@ function requiredCommandsForTask({ deliveryLevel, generationStrategy, mode, + pageComponent, pageFact, taskLevel, }) @@ -2530,12 +2668,14 @@ function conditionalCommandsForTask({ deliveryLevel, taskLevel, topology, pageUn function actionToolForScript(script) { if (script === 'reuse-existing-contract') return 'file-edit' if (script === 'resolve-business-route-target') return 'manual-resolution' + if (script === 'resolve-managed-page-instance') return 'manual-resolution' if (script === 'npm run build' || script === 'npm run lint') return 'shell-command' if (script.startsWith('typical-page:')) return 'npm-script' return 'node-script' } function actionPhaseForScript(script) { + if (script === 'bootstrap-target-project') return 'ResolveBlockingFacts' if (script === 'typical-page:select-archetype') return 'SelectStartPoint' if (script === 'typical-page:start-page') return 'GenerateOrEdit' if (script === 'typical-page:write-contract' || script === 'reuse-existing-contract') return 'WriteContract' @@ -2551,6 +2691,7 @@ function actionPhaseForScript(script) { if (script === 'typical-page:runtime-smoke') return 'FormalAcceptance' if (script === 'typical-page:preview-ready') return 'UsageStats' if (script === 'resolve-business-route-target') return 'ResolveBlockingFacts' + if (script === 'resolve-managed-page-instance') return 'ResolveBlockingFacts' return 'GenerateOrEdit' } @@ -2652,12 +2793,16 @@ function generationProfileForPlan({ generationStrategy, mode, }) + const strictLegacySourceAdapterProof = + mode === 'legacy-host-compatible' && + deliveryAsset.kind === 'project-certified-carrier' if ( shouldRequireSourceGateInPrimaryActions({ deliveryLevel, generationStrategy, mode, + pageComponent, pageFact, taskLevel, }) @@ -2668,6 +2813,7 @@ function generationProfileForPlan({ deliveryLevel, generationStrategy, mode, + pageComponent, pageFact, taskLevel, }) @@ -2703,16 +2849,21 @@ function generationProfileForPlan({ moldId: pageTypeIds.length === 1 ? getMoldIdForPageType(pageTypeIds[0], { skillRoot }) : 'page-units.managed-mold.v1', - startFrom: legacyPageComponentFastPath ? 'page-component' : startFrom?.id || '', + startFrom: + legacyPageComponentFastPath || + (mode === 'rules-only' && + normalizedGenerationStrategyId(generationStrategy) === 'page-component' && + pageComponent?.selected === true) + ? 'page-component' + : startFrom?.id || '', lockedRegions, editableSlots, slotManifest, requiredGates: uniqueValues(requiredGates), fallback: 'block-and-request-managed-mold-or-certified-adapter', - sourceProofLevel: - legacyPageComponentFastPath || mode !== 'legacy-host-compatible' - ? 'slot-boundary-proof' - : 'strict-source-adapter-proof', + sourceProofLevel: strictLegacySourceAdapterProof + ? 'strict-source-adapter-proof' + : 'slot-boundary-proof', notes: [ 'Generate from the primary generation asset when available; use fallback start points only for managed-fallback cases.', 'Do not synthesize page shell, critical regions, ownership, pagination/footer, or main-scroll from local primitives.', @@ -2722,8 +2873,11 @@ function generationProfileForPlan({ ...(runtimeBridgeProfile?.status === 'available' ? ['In legacy bridge mode, resolve runtime shell source from certification and keep wrapper/slot adapter runtime-thin.'] : []), - ...(legacyPageComponentFastPath - ? ['Legacy page-component fast path uses slot-boundary validation; source-gate stays in fallback/formal acceptance branches instead of the primary slot-fill chain.'] + ...(strictLegacySourceAdapterProof + ? ['Legacy project-certified carriers require strict source + adapter proof before preflight because the business page is materialized from a project-scoped managed carrier rather than a direct standard component source.'] + : []), + ...(legacyPageComponentFastPath && !strictLegacySourceAdapterProof + ? ['Legacy direct standard-component fast path keeps slot-boundary validation in the primary chain; strict source-gate remains reserved for project-certified carrier proofs and formal acceptance paths.'] : []), ...(topology?.id === 'non-typical-overlay' ? ['Non-typical overlays must declare governed base mold and incremental scope before implementation.'] @@ -3005,6 +3159,7 @@ function blockingIssueCode(reason) { if (text.includes('unknown pageType')) return 'PLAN_INVALID' if (text.includes('runtime bridge profile is missing')) return 'RUNTIME_BRIDGE_PROFILE_MISSING' if (text.includes('runtime bridge component shell could not be resolved')) return 'RUNTIME_BRIDGE_SOURCE_UNRESOLVED' + if (text.includes('managed page instance')) return 'MANAGED_INSTANCE_MIGRATION_REQUIRED' if (text.includes('example gallery') || text.includes('src/typical-page-reuse')) { return 'ROUTE_OWNER_MISSING' } @@ -3085,6 +3240,406 @@ function contractFieldsNeeded({ analyticsContractRequired, generationStrategy, l return Array.from(new Set(fields)) } +function targetDeliverySemanticsForPlan({ generationStrategy }) { + const normalizedStrategyId = normalizedGenerationStrategyId(generationStrategy) + + if (normalizedStrategyId === 'page-component') { + return { + schemaVersion: 'target-delivery-semantics.v1', + defaultAction: 'generate-by-page-component-slot-fill', + deliveryPath: 'page-component-plus-slot-fill', + shellImportPolicy: 'follow-selected-delivery-asset', + runtimeDeliveryPolicy: 'use-selected-page-component-delivery-asset', + allowedEditBoundary: 'business-slots-and-level-1-controlled-extensions-only', + referenceAssetRole: 'semantic-and-structure-reference-only', + doNotDo: ['do-not-rebuild-managed-regions-from-primitives'], + } + } + + if (normalizedStrategyId === 'managed-analytics') { + return { + schemaVersion: 'target-delivery-semantics.v1', + defaultAction: 'generate-by-managed-analytics-contract', + deliveryPath: 'managed-analytics-contract-plus-page-assembly', + shellImportPolicy: 'follow-selected-delivery-asset', + runtimeDeliveryPolicy: 'use-managed-analytics-delivery-asset', + allowedEditBoundary: 'approved-chart-config-and-business-slots-only', + referenceAssetRole: 'semantic-and-structure-reference-only', + doNotDo: ['do-not-build-freeform-chart-wall-from-primitives'], + } + } + + if (normalizedStrategyId === 'controlled-extension') { + return { + schemaVersion: 'target-delivery-semantics.v1', + defaultAction: 'reuse-existing-managed-contract', + deliveryPath: 'existing-managed-page-controlled-extension', + shellImportPolicy: 'preserve-existing-managed-delivery-asset', + runtimeDeliveryPolicy: 'stay-inside-existing-managed-page-contract', + allowedEditBoundary: 'existing-managed-slots-and-approved-controlled-extensions-only', + referenceAssetRole: 'not-applicable', + doNotDo: ['do-not-upgrade-controlled-extension-into-structural-change'], + } + } + + if (normalizedStrategyId === 'non-typical') { + return { + schemaVersion: 'target-delivery-semantics.v1', + defaultAction: 'generate-by-non-typical-constrained-assembly', + deliveryPath: 'non-typical-constrained-assembly', + shellImportPolicy: 'follow-approved-layout-strategy', + runtimeDeliveryPolicy: 'use-approved-non-typical-layout-carrier', + allowedEditBoundary: 'approved-layout-archetype-and-business-sections-only', + referenceAssetRole: 'layout-strategy-reference-only', + doNotDo: ['do-not-fallback-to-freeform-business-page'], + } + } + + return { + schemaVersion: 'target-delivery-semantics.v1', + defaultAction: 'generate-from-managed-fallback-start-point', + deliveryPath: 'managed-fallback-start-point', + shellImportPolicy: 'follow-selected-start-point', + runtimeDeliveryPolicy: 'use-managed-fallback-start-asset', + allowedEditBoundary: 'managed-start-point-plus-governed-business-fill', + referenceAssetRole: 'semantic-and-structure-reference-only', + doNotDo: ['do-not-start-from-blank-page-when-managed-start-point-is-blocked'], + } +} + +function currentExecutionStateForPlan({ + blockingIssues, + canStartImplementation, + requiredActions, + targetDeliverySemantics, +}) { + const queuedCommands = requiredActions + .map((action) => action.command) + .filter(Boolean) + const nextAction = requiredActions[0] || null + + return { + schemaVersion: 'current-execution-state.v1', + status: canStartImplementation ? 'ready' : 'blocked', + canStartImplementation, + primaryAction: canStartImplementation + ? targetDeliverySemantics.defaultAction + : 'ResolveBlockingFacts', + executablePath: canStartImplementation + ? targetDeliverySemantics.deliveryPath + : 'resolve-blocking-facts', + currentPhase: nextAction?.phase || (canStartImplementation ? 'SelectStartPoint' : 'ResolveBlockingFacts'), + allowedCommandSurface: canStartImplementation + ? 'selected-plan-actions' + : 'blocking-recovery-only', + nextCommand: nextAction?.command || '', + nextPhase: nextAction?.phase || '', + queuedCommands, + blockerCodes: blockingIssues.map((issue) => issue.code), + targetDefaultAction: targetDeliverySemantics.defaultAction, + targetDeliveryPath: targetDeliverySemantics.deliveryPath, + } +} + +function implementationActionForPlan({ + generationStrategy, + targetPage, + topology, +}) { + const normalizedStrategyId = normalizedGenerationStrategyId(generationStrategy) + + if (targetPage?.pageTypeMigration || topology?.id === 'single-page-composite') { + return { + schemaVersion: 'implementation-action.v1', + action: 'structural-upgrade', + category: 'structure-upgrade', + reason: + topology?.id === 'single-page-composite' + ? 'same-screen composite layout changes the page shell/ownership contract beyond a slot-only edit' + : 'page type or managed shell contract changed, so this task is no longer a slot-only update', + } + } + + if (normalizedStrategyId === 'page-component') { + if (targetPage?.exists && targetPage?.managedInstanceReadiness?.status === 'migration-required') { + return { + schemaVersion: 'implementation-action.v1', + action: 'rewrite-by-page-component', + category: 'managed-instance-rewrite', + reason: + 'the target page still belongs to the same typical page type, but the current source is not a ready managed instance; rebuild the business page instance from the certified page component/carrier instead of repairing the old shell by hand', + } + } + + return { + schemaVersion: 'implementation-action.v1', + action: 'slot-fill-only', + category: 'lightweight-implementation', + reason: + 'the selected delivery asset already owns the managed shell; stay inside certified business slots and approved controlled extensions only', + } + } + + if (normalizedStrategyId === 'non-typical' || normalizedStrategyId === 'managed-analytics') { + return { + schemaVersion: 'implementation-action.v1', + action: 'preserve-layout-strategy-and-replace-semantics', + category: 'semantic-upgrade', + reason: + 'preserve the approved base archetype/layout strategy and replace the implementation with HiUI-first semantics, tokens, and governed ownership instead of inheriting legacy visuals or unmanaged layout code', + } + } + + if (normalizedStrategyId === 'controlled-extension') { + return { + schemaVersion: 'implementation-action.v1', + action: 'slot-fill-only', + category: 'lightweight-implementation', + reason: + 'the current task remains inside existing managed slots or approved Level 1 controlled extensions', + } + } + + return { + schemaVersion: 'implementation-action.v1', + action: 'structural-upgrade', + category: 'structure-upgrade', + reason: + 'the selected strategy is outside slot-only page-component delivery and requires an explicit managed fallback or structure-aware upgrade path', + } +} + +function governanceUpgradeForPlan({ + acceptanceProfile, + deliveryLevel, + mode, + pageTypeId, + targetPage, +}) { + const reasons = [] + + if (targetPage?.managedInstanceReadiness?.status === 'migration-required' || targetPage?.pageTypeMigration) { + reasons.push('migration-or-rearchitecture') + } + + if ( + mode === 'legacy-host-compatible' && + (deliveryLevel?.id === 'C' || + HIGH_RISK_TRANSLATION_MAP_PAGE_TYPES.has(pageTypeId) || + targetPage?.pageTypeMigration) + ) { + reasons.push('legacy-drift-guard') + } + + if (acceptanceProfile?.formalRequired) { + reasons.push('formal-acceptance') + } + + const required = reasons.length > 0 + + return { + schemaVersion: 'governance-upgrade.v1', + required, + reasons, + scope: required ? 'enhanced-governance' : 'standard-governance', + explanation: required + ? 'upgrade governance checks such as snapshot/acceptance contract, translation-map, source-gate, doctor, or finalize-page without assuming the implementation path must become a structure rewrite' + : 'current task stays on the default governance profile for the selected generation path', + } +} + +function executionDecisionSummaryForPlan(targetDeliverySemantics) { + return { + ...targetDeliverySemantics, + summaryScope: 'current-execution-state-aligned-with-target-delivery', + targetDefaultAction: targetDeliverySemantics.defaultAction, + targetDeliveryPath: targetDeliverySemantics.deliveryPath, + } +} + +function inferNonTypicalLayoutFacts({ changeText, pageTypeId }) { + const text = String(changeText || '') + const detailLikePage = ['full-page-detail', 'drawer-detail'].includes(pageTypeId) + const hasSummaryStrip = /(顶部摘要|摘要带|摘要区|总览|概览|指标摘要)/i.test(text) + const hasSecondaryRail = /(右侧|侧栏|辅栏|时间线|风险|协同|轨迹|流程|进度)/i.test(text) + + if (detailLikePage && (hasSummaryStrip || hasSecondaryRail)) { + return { + layoutStrategy: 'detail-workbench', + layoutArchetype: 'primary-secondary', + nonTypicalScope: [ + ...(hasSummaryStrip ? ['summary-strip'] : []), + 'primary-groups', + 'secondary-groups', + 'supporting-sections', + ], + mandatoryComponents: ['PageHeader', 'Descriptions', 'SchemaGroup'], + compositionGuardrails: [ + 'detail-body remains the primary managed detail carrier; non-typical composition can only reorganize first-level groups inside the approved detail workspace.', + 'secondary rail may host timeline / risk / collaboration sections, but must not replace Descriptions or SchemaGroup as the main detail expression.', + ], + strategyEvidence: [ + 'layout strategy/archetype: detail-workbench + primary-secondary', + hasSummaryStrip + ? 'top summary strip proves the workbench-style detail overview layer before the main detail body' + : 'secondary supporting rail proves the workbench-style detail layering beyond a plain grouped detail page', + ], + } + } + + return { + layoutStrategy: detailLikePage ? 'detail-workbench' : 'non-typical-constrained-assembly', + layoutArchetype: + /(左侧|右侧|侧栏|分栏|时间线|协同|风险)/i.test(text) ? 'primary-secondary' : 'parallel-sections', + nonTypicalScope: ['primary-groups', 'supporting-sections'], + mandatoryComponents: detailLikePage ? ['PageHeader', 'Descriptions', 'SchemaGroup'] : ['PageHeader'], + compositionGuardrails: detailLikePage + ? [ + 'detail-body remains the primary managed detail carrier; non-typical composition must stay inside governed detail sections.', + ] + : ['keep carrier ownership on the approved non-typical layout shell'], + strategyEvidence: ['layout strategy/archetype derived from non-typical page evidence and base page type'], + } +} + +function layoutStrategyForPlan({ isNonTypical, topology }) { + if (topology?.id === 'single-page-composite') { + return 'context-main-split' + } + if (isNonTypical) { + return '' + } + return 'typical-page' +} + +function ownershipPlanForPlan({ mode, pageTypeId }) { + const requiredRoles = getRequiredOwnershipRolesForPageType(pageTypeId) + const managedTargetsByRole = { + 'content-slot': `${pageTypeId}.contentSlot`, + 'white-body': `${pageTypeId}.whiteBody`, + 'outer-padding': `${pageTypeId}.pageRoot`, + 'main-scroll': `${pageTypeId}.pageScroll`, + } + return { + ownershipMode: mode === 'host-integration' ? 'host-slot-owns-workspace' : 'page-surface-owns-workspace', + requiredRoles, + targetHints: requiredRoles.map((role) => ({ + role, + target: + mode === 'host-integration' + ? role === 'content-slot' + ? 'TypicalPageAppFrame.content' + : role === 'main-scroll' + ? `${pageTypeId}.workspaceScroll` + : `${pageTypeId}.shell` + : managedTargetsByRole[role] || `${pageTypeId}.${role.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase())}`, + })), + } +} + +function generationInputsForPlan({ + fastPath, + generationProfile, + pageComponent, + pageTypeId, + primaryGenerationAsset, + requiredActions, + requiredDocs, + startFrom, +}) { + const requiredRegions = getRequiredRegionsForPageType(pageTypeId) + return { + schemaVersion: 'generation-inputs.v1', + requiredFacts: [ + 'project-mode', + 'route-ownership', + 'asset-resolution', + ...(pageComponent?.selected ? ['page-component-selection'] : []), + ], + requiredDocs, + docBundle: { + schemaVersion: 'generation-doc-bundle.v1', + source: 'requiredDocs', + profile: fastPath.eligible ? 'minimal-doc-fast-path' : 'expanded-doc-bundle', + bundleIds: requiredDocs.map((doc) => doc.path), + }, + fastPathSummary: { + schemaVersion: 'generation-fast-path-summary.v1', + eligible: fastPath.eligible, + executionMode: fastPath.eligible ? 'fast-path' : 'standard-path', + pageShellPolicy: pageComponent?.selected ? 'page-component-shell' : 'managed-start-point-shell', + queryFilterPolicy: LIST_SHELL_PAGE_TYPES.has(pageTypeId) ? 'managed-query-filter' : 'not-applicable', + upgradeSignals: fastPath.eligible ? [] : ['full-generation-doc-bundle-required'], + }, + entryActions: requiredActions.map((action) => ({ + id: action.id, + phase: action.phase, + command: action.command, + })), + startFrom, + primaryGenerationAsset, + pageComponent: pageComponent?.selected + ? { + selected: true, + componentId: pageComponent.componentId, + source: pageComponent.source, + } + : { + selected: false, + componentId: '', + source: pageComponent?.source || '', + }, + compiledTypicalBaseline: { + pageTypeId, + requiredRegions, + hardConstraints: [ + 'single-white-body-owner', + 'managed-query-filter-required', + 'managed-pagination-owner-required', + ], + headerLayout: { + required: requiredRegions.includes('header'), + rhythmPx: 60, + }, + queryFilter: { + required: requiredRegions.includes('query-filter'), + requiredComponent: requiredRegions.includes('query-filter') ? 'QueryFilter' : '', + }, + whiteBodyOwnership: { + required: requiredRegions.includes('white-body'), + forbidSecondWhiteBody: requiredRegions.includes('white-body'), + }, + pagination: { + required: requiredRegions.includes('pagination'), + mountPolicy: requiredRegions.includes('pagination') ? 'managed-pagination-mount' : 'not-applicable', + }, + }, + generationProfile, + } +} + +function inlineChecksForPlan({ generationRecipe, requiredActions }) { + return { + schemaVersion: 'inline-checks.v1', + recipeChecks: Array.isArray(generationRecipe?.inlineChecks) ? generationRecipe.inlineChecks : [], + commandChecks: requiredActions + .filter((action) => ['GenerateOrEdit', 'Preflight', 'TranslationMap'].includes(action.phase)) + .map((action) => `${action.command}: ${action.reason}`), + } +} + +function deliveryChecksForPlan({ acceptanceProfile, deliveryLevel }) { + return { + schemaVersion: 'delivery-checks.v1', + projectQuality: ['preflight', 'page-instance-validation'], + previewConfirmation: ['preview-ready'], + runtimeGovernance: deliveryLevel.id === 'C' ? ['runtime-smoke-or-finalize-path'] : ['not-required-by-default'], + formalAcceptance: acceptanceProfile.formalRequired + ? acceptanceProfile.defaultActions + : [], + } +} + async function buildPlan(options) { const scriptDir = path.dirname(fileURLToPath(import.meta.url)) const skillRoot = path.resolve(scriptDir, '..') @@ -3104,6 +3659,7 @@ async function buildPlan(options) { const hostProfile = await detectHostProfile(options.target) const pkg = (await readTargetPackageJson(options.target)) || hostProfile.pkg const projectModeFact = await readProjectModeFact(options.target) + const projectIntegration = await readProjectIntegrationState(options.target) const pageFact = await readExistingPageFact(options.target, options.page) const mode = inferMode(pkg, hostProfile, options.mode, projectModeFact) const projectCapabilities = buildProjectCapabilities({ targetRoot: options.target, skillRoot, modeOverride: mode.id }) @@ -3118,6 +3674,39 @@ async function buildPlan(options) { const routeOwnershipBlockingReason = pageFact?.page ? getTypicalPageReuseBlockingReason(pageFact.page) : '' + const managedInstanceBlockingReason = + pageFact?.managedInstanceReadiness?.status === 'migration-required' + ? buildManagedInstanceMigrationReason(pageFact.managedInstanceReadiness) + : '' + const unregisteredManagedMarkerPage = + pageFact?.managedInstanceReadiness?.status === 'migration-required' && + Array.isArray(pageFact?.managedInstanceReadiness?.blockerCodes) && + pageFact.managedInstanceReadiness.blockerCodes.includes('MANAGED_INSTANCE_CONTRACT_MISSING') && + Boolean( + pageFact?.managedInstanceReadiness?.sourceMarkers?.pageType && + pageFact?.managedInstanceReadiness?.sourceMarkers?.shell && + ( + pageFact?.managedInstanceReadiness?.sourceMarkers?.example || + pageFact?.managedInstanceReadiness?.sourceMarkers?.hostArchetype || + pageFact?.managedInstanceReadiness?.sourceMarkers?.archetype + ) + ) + const projectIntegrationBlocked = projectIntegration?.integrationReady === false + const projectIntegrationCommand = projectIntegrationBlocked + ? { + script: 'bootstrap-target-project', + args: ['--target '], + when: + 'before any page generation or page-instance repair; current project-integration-state is blocked and required hiui-design onboarding facts must be repaired first', + } + : null + const routeOwnershipBlockingCommand = routeOwnershipBlockingReason + ? { + script: 'resolve-business-route-target', + args: ['--page src/pages//index.jsx'], + when: 'before any start-page, preflight, or write-contract command; current --page belongs to the example gallery/smoke baseline', + } + : null if (options.pageTypeId && !pageType) { blockingReasons.push(`unknown pageType: ${options.pageTypeId}`) @@ -3127,6 +3716,10 @@ async function buildPlan(options) { blockingReasons.push(routeOwnershipBlockingReason) } + if (managedInstanceBlockingReason) { + blockingReasons.push(managedInstanceBlockingReason) + } + if ( taskLevel.id === 'new-page-or-rearchitecture' && topology.id === 'unresolved' && @@ -3214,7 +3807,7 @@ async function buildPlan(options) { reason: 'start asset is resolved per pageUnits item', } : pageType - ? startFromForLegacyPageComponent({ + ? startFromForPageComponentMainline({ generationStrategy, mode: mode.id, pageComponent, @@ -3280,6 +3873,21 @@ async function buildPlan(options) { startFrom, }) const pageTypeDocs = docsForPageTypes({ manifest, pageTypeIds: pageTypeIdsForDocs }) + const nonTypicalLayoutFacts = isNonTypical + ? inferNonTypicalLayoutFacts({ + changeText: options.change, + pageTypeId: pageTypeIdForContract, + }) + : null + const layoutStrategy = + nonTypicalLayoutFacts?.layoutStrategy || layoutStrategyForPlan({ isNonTypical, topology }) + const resolvedLayoutArchetype = nonTypicalLayoutFacts?.layoutArchetype || layoutArchetype + const nonTypicalScope = isNonTypical ? nonTypicalLayoutFacts?.nonTypicalScope || [] : [] + const mandatoryComponents = isNonTypical ? nonTypicalLayoutFacts?.mandatoryComponents || [] : [] + const compositionGuardrails = isNonTypical + ? nonTypicalLayoutFacts?.compositionGuardrails || [] + : [] + const strategyEvidence = isNonTypical ? nonTypicalLayoutFacts?.strategyEvidence || [] : [] const suggestedPageTypes = Array.from( new Set( intentUnits @@ -3287,18 +3895,37 @@ async function buildPlan(options) { .filter(Boolean) ) ) - const requiredCommands = routeOwnershipBlockingReason + const requiredCommands = projectIntegrationBlocked ? [ - { - script: 'resolve-business-route-target', - args: ['--page src/pages//index.jsx'], - when: 'before any start-page, preflight, or write-contract command; current --page belongs to the example gallery/smoke baseline', - }, + ...(projectIntegrationCommand ? [projectIntegrationCommand] : []), + ...(routeOwnershipBlockingCommand ? [routeOwnershipBlockingCommand] : []), ] + : routeOwnershipBlockingReason + ? [routeOwnershipBlockingCommand] + : unregisteredManagedMarkerPage + ? [ + { + script: 'register-managed-page-contract', + args: [ + `--page ${pageFact?.page || ''}`, + `--page-type ${pageFact?.sourcePageTypeId || ''}`, + ], + when: 'before preflight/finalize; current source already carries managed-page markers, but the page-contract registration chain was never completed', + }, + ] + : managedInstanceBlockingReason + ? [ + { + script: 'resolve-managed-page-instance', + args: ['--page '], + when: 'before any reuse-existing-contract command; the current source still needs to migrate into the managed shell / query-filter / table ownership chain', + }, + ] : requiredCommandsForTask({ deliveryLevel, generationStrategy, mode: mode.id, + pageComponent, pageFact, pageTypeId: pageTypeIdForContract, pageUnits, @@ -3317,6 +3944,18 @@ async function buildPlan(options) { generationStrategy, mode: mode.id, }) + const requiredDocs = requiredDocsForPlan({ + fastPath, + i18nMode, + isNonTypical, + manifest, + mode: mode.id, + modeSource: mode.source, + pageComponent, + pageTypeIds: pageTypeIdsForDocs, + taskLevel, + topology, + }) const targetPage = pageFact ? { path: pageFact.page, @@ -3326,6 +3965,7 @@ async function buildPlan(options) { sourcePageTypeId: pageFact.sourcePageTypeId, contractPageTypeId: pageFact.contract?.pageTypeId || '', contractWorkflowStatus: pageFact.contract?.workflowStatus || '', + managedInstanceReadiness: pageFact.managedInstanceReadiness, pageTypeMigration: isSingleTypicalPageTarget({ topology, pageUnits }) && Boolean(pageFact.contract?.pageTypeId && pageTypeIdForContract) && pageFact.contract.pageTypeId !== pageTypeIdForContract, @@ -3342,6 +3982,7 @@ async function buildPlan(options) { assetResolution, mode, projectCapabilities, + projectIntegration, targetPage, }) const allBlockingReasons = Array.from(new Set([ @@ -3364,6 +4005,52 @@ async function buildPlan(options) { }).map((command) => command.script).filter(Boolean), ])) const blockingIssues = blockingIssuesForReasons(allBlockingReasons) + const targetDeliverySemantics = targetDeliverySemanticsForPlan({ generationStrategy }) + const ownershipPlan = ownershipPlanForPlan({ + mode: mode.id, + pageTypeId: pageTypeIdForContract, + }) + const generationInputs = generationInputsForPlan({ + fastPath, + generationProfile, + pageComponent, + pageTypeId: pageTypeIdForContract, + primaryGenerationAsset, + requiredActions, + requiredDocs, + startFrom, + }) + const inlineChecks = inlineChecksForPlan({ + generationRecipe, + requiredActions, + }) + const deliveryChecks = deliveryChecksForPlan({ + acceptanceProfile, + deliveryLevel, + }) + const canStartImplementation = + allBlockingReasons.length === 0 && + (!targetPage?.managedInstanceReadiness || + targetPage.managedInstanceReadiness.status === 'ready' || + targetPage.managedInstanceReadiness.status === 'not-applicable') + const currentExecutionState = currentExecutionStateForPlan({ + blockingIssues, + canStartImplementation, + requiredActions, + targetDeliverySemantics, + }) + const implementationAction = implementationActionForPlan({ + generationStrategy, + targetPage, + topology, + }) + const governanceUpgrade = governanceUpgradeForPlan({ + acceptanceProfile, + deliveryLevel, + mode: mode.id, + pageTypeId: pageTypeIdForContract, + targetPage, + }) return { schemaVersion: 'page-task-plan.v1', @@ -3389,11 +4076,23 @@ async function buildPlan(options) { }, isNonTypical, layoutOverlayRequired: isNonTypical || topology.id === 'single-page-composite', + layoutStrategy, + layoutArchetype: resolvedLayoutArchetype, + nonTypicalScope, + mandatoryComponents, + compositionGuardrails, + strategyEvidence, + ownershipPlan, fastPath, generationStrategy, generationStrategyId: normalizedGenerationStrategyId(generationStrategy), generationProfile, generationRecipe, + generationInputs, + inlineChecks, + deliveryChecks, + implementationAction, + governanceUpgrade, primaryGenerationAsset, fallbackGenerationAsset, customizationLevel, @@ -3403,18 +4102,7 @@ async function buildPlan(options) { i18nMode, pageTypeDocs, startFrom, - requiredDocs: requiredDocsForPlan({ - fastPath, - i18nMode, - isNonTypical, - manifest, - mode: mode.id, - modeSource: mode.source, - pageComponent, - pageTypeIds: pageTypeIdsForDocs, - taskLevel, - topology, - }), + requiredDocs, requiredActions, requiredCommands, acceptanceLevel: acceptanceLevelForDeliveryLevel(deliveryLevel), @@ -3433,7 +4121,7 @@ async function buildPlan(options) { contractFieldsNeeded: contractFieldsNeeded({ analyticsContractRequired, generationStrategy, - layoutArchetype, + layoutArchetype: resolvedLayoutArchetype, mode: mode.id, pageComponent, pageTypeId: pageTypeIdForContract, @@ -3461,6 +4149,8 @@ async function buildPlan(options) { assemblyOrder: generationRecipe.assemblyOrder, requiredAssets: generationRecipe.requiredAssets, }, + implementationAction: implementationAction.action, + governanceUpgradeRequired: governanceUpgrade.required, primaryGenerationAsset: { type: primaryGenerationAsset.type, id: primaryGenerationAsset.id, @@ -3512,15 +4202,19 @@ async function buildPlan(options) { previewReadyRequired: taskLevel.id !== 'minor-edit', requireNetworkAuthorization: 'when usage script exits 21', }, + targetDeliverySemantics, + currentExecutionState, + executionDecisionSummary: executionDecisionSummaryForPlan(targetDeliverySemantics), factsSource: { projectCapabilities: projectCapabilities.schemaVersion, + projectIntegration: projectIntegration?.factPath || '', mode: mode.factPath || (mode.source === 'explicit' ? 'cli:--mode' : 'rules/mode-selection.md'), pageTypes: 'rules/common.page-types.json', archetypes: 'archetypes/page-types/*/archetype.json', usagePolicy: 'PRIVACY.md', }, targetPage, - canStartImplementation: allBlockingReasons.length === 0, + canStartImplementation, blockingReasons: allBlockingReasons, blockingIssues, suggestedPageTypes, diff --git a/skills/hiui-design/scripts/public-cli-contracts.json b/skills/hiui-design/scripts/public-cli-contracts.json index 48c90e07e..42fddfdb1 100644 --- a/skills/hiui-design/scripts/public-cli-contracts.json +++ b/skills/hiui-design/scripts/public-cli-contracts.json @@ -1732,6 +1732,7 @@ "contractSchemaVersion", "sections", "statuses", + "preflightExecution", "evidence", "risks", "source" @@ -1748,6 +1749,17 @@ "usageStatsStatus" ] }, + { + "path": [ + "preflightExecution" + ], + "fields": [ + "preflightStage", + "readyForImplementation", + "readyForDelivery", + "deferredChecks" + ] + }, { "path": [ "evidence" @@ -1775,6 +1787,7 @@ "fields": [ "planSchemaVersion", "preflightSchemaVersion", + "preflightStage", "usageStatsStatus" ] } diff --git a/skills/hiui-design/scripts/render-final-report.mjs b/skills/hiui-design/scripts/render-final-report.mjs index c96894f37..b5a145c46 100644 --- a/skills/hiui-design/scripts/render-final-report.mjs +++ b/skills/hiui-design/scripts/render-final-report.mjs @@ -358,7 +358,98 @@ function assertAllowedStatus(contract, field, value) { } } -function collectWarnings(preflight, managedContract) { +function normalizeDeferredChecks(value) { + return Array.isArray(value) + ? [...new Set(value.map((item) => String(item || '').trim()).filter(Boolean))] + : [] +} + +function inferManagedWorkflowPreflightExecution(managedContract) { + const workflow = managedContract?.workflow || {} + const workflowStatus = String(workflow.status || '').trim() + const preflightStatus = String(workflow.preflightStatus || '').trim() + const deferredChecks = normalizeDeferredChecks(workflow.deferredChecks) + const inferredStage = + String(workflow.preflightStage || '').trim() || + (workflowStatus === 'started' && preflightStatus === 'pass' + ? 'scaffold-baseline' + : workflowStatus === 'stale' || workflowStatus === 'preflight-pass' || workflowStatus === 'finalized' + ? 'implementation' + : '') + + return { + preflightStage: inferredStage, + readyForImplementation: + typeof workflow.readyForImplementation === 'boolean' + ? workflow.readyForImplementation + : Boolean(inferredStage), + readyForDelivery: + typeof workflow.readyForDelivery === 'boolean' + ? workflow.readyForDelivery + : workflowStatus === 'finalized' || workflowStatus === 'preflight-pass', + deferredChecks: + deferredChecks.length > 0 + ? deferredChecks + : workflowStatus === 'stale' + ? ['finalizePage'] + : inferredStage === 'scaffold-baseline' + ? ['placeholderMappings'] + : [], + } +} + +function resolvePreflightExecution(preflight, managedContract) { + const managedWorkflow = inferManagedWorkflowPreflightExecution(managedContract) + if (!preflight) { + return managedWorkflow + } + + const preflightDeferredChecks = normalizeDeferredChecks(preflight.deferredChecks) + const preflightPassed = String(preflight.status || '').trim() === 'passed' + return { + preflightStage: + String(preflight.preflightStage || '').trim() || + managedWorkflow.preflightStage || + (preflightPassed ? 'implementation' : ''), + readyForImplementation: + typeof preflight.readyForImplementation === 'boolean' + ? preflight.readyForImplementation + : managedWorkflow.readyForImplementation || preflightPassed, + readyForDelivery: + typeof preflight.readyForDelivery === 'boolean' + ? preflight.readyForDelivery + : managedWorkflow.readyForDelivery || (preflightPassed && preflightDeferredChecks.length === 0), + deferredChecks: + preflightDeferredChecks.length > 0 ? preflightDeferredChecks : managedWorkflow.deferredChecks, + } +} + +function deferredCheckMessages(deferredChecks) { + return normalizeDeferredChecks(deferredChecks).map((checkId) => { + if (checkId === 'placeholderMappings') { + return { + warning: + 'Preflight passed only at scaffold-baseline stage; placeholder mappings still need implementation before delivery.', + nextAction: + 'Replace scaffold placeholder mappings and rerun typical-page:preflight before treating the page as delivery-ready.', + } + } + if (checkId === 'finalizePage') { + return { + warning: + 'Current workflow still requires finalize-page on the latest source snapshot before delivery can be declared complete.', + nextAction: + 'Rerun typical-page:finalize-page on the latest page source snapshot before delivery.', + } + } + return { + warning: `Preflight deferred check "${checkId}" is still pending before delivery.`, + nextAction: `Resolve deferred check "${checkId}" and rerun the required validation.`, + } + }) +} + +function collectWarnings(preflight, managedContract, preflightExecution) { const warnings = [] if (Array.isArray(preflight?.warnings)) warnings.push(...preflight.warnings) if (Array.isArray(preflight?.checks)) { @@ -375,6 +466,11 @@ function collectWarnings(preflight, managedContract) { 'Managed page contract is stale for the current source snapshot.' ) } + if (preflightExecution.preflightStage === 'scaffold-baseline' && !preflightExecution.readyForDelivery) { + warnings.push( + ...deferredCheckMessages(preflightExecution.deferredChecks).map((item) => item.warning) + ) + } return warnings } @@ -385,6 +481,7 @@ function nextActionsForStatuses({ formalAcceptanceStatus, pageStatus, preflight, + preflightExecution, }) { const actions = [] if (pageStatus === 'blocked') { @@ -416,6 +513,8 @@ function nextActionsForStatuses({ } else { actions.push('Fix preflight blockingIssues and rerun typical-page:preflight.') } + } else if (!preflightExecution.readyForDelivery && preflightExecution.deferredChecks.length > 0) { + actions.push(...deferredCheckMessages(preflightExecution.deferredChecks).map((item) => item.nextAction)) } if (formalAcceptanceStatus === 'failed') { actions.push('Fix formal acceptance failures and rerun formalAcceptanceActions.') @@ -443,6 +542,7 @@ function buildProductionLineSummary(preflight, managedContract) { ['generationProfile', 'productionContract'].includes(issue?.checkId) ) const workflowStatus = String(managedContract?.workflow?.status || '').trim() + const preflightExecution = resolvePreflightExecution(preflight, managedContract) if (!preflight && !managedContract) { return { @@ -462,9 +562,10 @@ function buildProductionLineSummary(preflight, managedContract) { : !preflight && workflowStatus === 'stale' ? 'stale' : productionContract && generationProfile - ? preflight?.status === 'passed' || - workflowStatus === 'finalized' || - String(managedContract?.workflow?.preflightStatus || '').trim() === 'pass' + ? ((preflight?.status === 'passed' || + String(managedContract?.workflow?.preflightStatus || '').trim() === 'pass') && + preflightExecution.readyForDelivery) || + workflowStatus === 'finalized' ? 'passed' : 'declared' : 'not_declared', @@ -477,6 +578,7 @@ function buildProductionLineSummary(preflight, managedContract) { ? generationProfile.requiredGates : [], sourceProofLevel: productionContract?.sourceProofLevel || generationProfile?.sourceProofLevel || '', + preflightExecution, blockingIssues: productionBlockingIssues, } } @@ -602,6 +704,7 @@ function renderFinalReport({ managedContract ) const normalizedPageStatus = resolvePageStatus(pageStatus, plan, managedContract) + const preflightExecution = resolvePreflightExecution(preflight, managedContract) const statuses = { pageStatus: normalizedPageStatus, @@ -623,13 +726,14 @@ function renderFinalReport({ ...(Array.isArray(plan?.blockingIssues) ? plan.blockingIssues : []), ...(Array.isArray(preflight?.blockingIssues) ? preflight.blockingIssues : []), ] - const warnings = collectWarnings(preflight, managedContract) + const warnings = collectWarnings(preflight, managedContract, preflightExecution) const nextActions = nextActionsForStatuses({ formalAcceptanceStatus, managedContract, pageStatus: normalizedPageStatus, preflight, preflightStatus, + preflightExecution, usageStatsStatus, }) const productionLine = buildProductionLineSummary(preflight, managedContract) @@ -648,6 +752,7 @@ function renderFinalReport({ contractSchemaVersion: contract.schemaVersion || 'final-report-contract.v1', sections: contract.sections, statuses, + preflightExecution, productionLine, designerSummary, evidence: { @@ -663,6 +768,7 @@ function renderFinalReport({ source: { planSchemaVersion: plan.schemaVersion || '', preflightSchemaVersion: preflight?.schemaVersion || '', + preflightStage: preflightExecution.preflightStage, usageStatsStatus, }, } diff --git a/skills/hiui-design/scripts/sync-public-cli-fixtures.mjs b/skills/hiui-design/scripts/sync-public-cli-fixtures.mjs index 24e47a9e4..528adeeb6 100644 --- a/skills/hiui-design/scripts/sync-public-cli-fixtures.mjs +++ b/skills/hiui-design/scripts/sync-public-cli-fixtures.mjs @@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url' function printUsage() { console.log(`Usage: - node scripts/sync-public-cli-fixtures.mjs [--check] [--id ] + node ".local-context/hiui-design/scripts/sync-public-cli-fixtures.mjs" [--check] [--id ] Notes: - Rebuilds shipped machine-public JSON fixtures from scripts/public-cli-contracts.json. diff --git a/skills/hiui-design/scripts/typical-page-doctor.mjs b/skills/hiui-design/scripts/typical-page-doctor.mjs index 1aeb820a0..49ddfddfe 100644 --- a/skills/hiui-design/scripts/typical-page-doctor.mjs +++ b/skills/hiui-design/scripts/typical-page-doctor.mjs @@ -2670,6 +2670,8 @@ async function collectTypicalPageSourceSignals(rootDir) { const files = await collectSourceFiles(rootDir) const signals = { usesShells: [], + auxiliaryShellUses: [], + directShellUses: [], privateImportViolations: [], tableHeaderExtraViolations: [], localHostAdapterImports: [], @@ -2680,6 +2682,10 @@ async function collectTypicalPageSourceSignals(rootDir) { for (const filePath of files) { const raw = await readTextCached(filePath) const specifiers = extractImportSpecifiers(raw) + const normalizedFilePath = filePath.split(path.sep).join('/') + const isHostIntegrationExampleFile = normalizedFilePath.includes('/src/typical-page-reuse/') + const isAuxiliaryReferenceFile = + isHostIntegrationExampleFile || /(^|\/)__codex_hiui_reference__\.[cm]?[jt]sx?$/.test(normalizedFilePath) const usesShells = specifiers.some( (specifier) => specifier === '@hiui-design/typical-page-shells' || @@ -2697,6 +2703,11 @@ async function collectTypicalPageSourceSignals(rootDir) { if (usesShells || usesPrivateShellImport) { signals.usesShells.push(filePath) + if (isAuxiliaryReferenceFile) { + signals.auxiliaryShellUses.push(filePath) + } else { + signals.directShellUses.push(filePath) + } } const usesLocalHostAdapter = specifiers.some( @@ -2704,7 +2715,6 @@ async function collectTypicalPageSourceSignals(rootDir) { specifier.endsWith('/typical-page-host') || specifier.includes('components/typical-page-host') ) - const isHostIntegrationExampleFile = filePath.includes(`${path.sep}typical-page-reuse${path.sep}`) if (usesLocalHostAdapter && !isHostIntegrationExampleFile) { signals.localHostAdapterImports.push(filePath) } @@ -5190,8 +5200,11 @@ async function main() { : '' const targetSourceRoot = path.join(targetRoot, 'src') const targetSourceSignals = await collectTypicalPageSourceSignals(targetSourceRoot) + const auxiliaryShellUsageOnly = + targetSourceSignals.directShellUses.length === 0 && + targetSourceSignals.auxiliaryShellUses.length > 0 const compatibilityGenerationOnly = - legacyCompatibilityMode && targetSourceSignals.usesShells.length === 0 + legacyCompatibilityMode && targetSourceSignals.directShellUses.length === 0 pushCheck(checks, { id: 'shells-installed', ok: legacyCompatibilityMode || (await pathExists(installedPackagePath)), @@ -5265,7 +5278,7 @@ async function main() { : compatibilityGenerationOnly ? `Detected a legacy host-compatible runtime: ${legacyHostRuntime.reasons.join( '; ' - )}. This host should stay on the legacy bridge path: ordinary typical pages may still use planner-selected page components, but the legacy host main tree must not be treated as a generic direct mount for the standard @hiui-design/typical-page-shells runtime. Continue with the compatibility generation path: read .local-context/hiui-design/rules/generation-rules.md, then .local-context/hiui-design/docs/generation/legacy-host-compatibility.md, and follow the selected carrier/runtimeAdapterProof result when generating business pages.` + )}. This host should stay on the legacy bridge path: ordinary typical pages may still use planner-selected page components, but the legacy host main tree must not be treated as a generic direct mount for the standard @hiui-design/typical-page-shells runtime.${auxiliaryShellUsageOnly ? ` Auxiliary gallery/reference imports remain allowed here and do not count as direct-shell business usage. Examples: ${summarizePathList(targetRoot, targetSourceSignals.auxiliaryShellUses)}` : ''} Continue with the compatibility generation path: read .local-context/hiui-design/rules/generation-rules.md, then .local-context/hiui-design/docs/generation/legacy-host-compatibility.md, and follow the selected carrier/runtimeAdapterProof result when generating business pages.` : `Detected a legacy host-compatible runtime: ${legacyHostRuntime.reasons.join( '; ' )}. This project already imports standard typical-page shell components, so it cannot stay on the downgraded host runtime. Either isolate a dedicated modern runtime entry/remote, or remove the standard shell imports and regenerate via legacy host bridge mode (legacy-host-compatible).`, @@ -5319,42 +5332,46 @@ async function main() { pushCheck(checks, { id: 'shells-declared-when-used', - ok: targetSourceSignals.usesShells.length === 0 || Boolean(depSpec), + ok: targetSourceSignals.directShellUses.length === 0 || Boolean(depSpec), severity: 'error', summary: 'once project source uses typical-page shells, package.json also declares @hiui-design/typical-page-shells', detail: - targetSourceSignals.usesShells.length === 0 || Boolean(depSpec) - ? targetSourceSignals.usesShells.length === 0 - ? 'No current source file imports typical-page shell components yet' - : `Shell package declared and used by: ${summarizePathList(targetRoot, targetSourceSignals.usesShells)}` - : `Source files already import typical-page shell components but package.json does not declare @hiui-design/typical-page-shells. Examples: ${summarizePathList(targetRoot, targetSourceSignals.usesShells)}`, + targetSourceSignals.directShellUses.length === 0 || Boolean(depSpec) + ? targetSourceSignals.directShellUses.length === 0 + ? auxiliaryShellUsageOnly + ? `Only auxiliary gallery/reference files import typical-page shell components, so package.json declaration is not forced by business-page usage. Examples: ${summarizePathList(targetRoot, targetSourceSignals.auxiliaryShellUses)}` + : 'No current business source file imports typical-page shell components yet' + : `Shell package declared and used by: ${summarizePathList(targetRoot, targetSourceSignals.directShellUses)}` + : `Business source files already import typical-page shell components but package.json does not declare @hiui-design/typical-page-shells. Examples: ${summarizePathList(targetRoot, targetSourceSignals.directShellUses)}`, }) pushCheck(checks, { id: 'styles-import-when-shells-used', - ok: targetSourceSignals.usesShells.length === 0 || hasStyleImport, + ok: targetSourceSignals.directShellUses.length === 0 || hasStyleImport, severity: 'error', summary: 'once project source uses typical-page shells, styles.css is already imported in the app entry', detail: - targetSourceSignals.usesShells.length === 0 || hasStyleImport - ? targetSourceSignals.usesShells.length === 0 - ? 'No current source file imports typical-page shell components yet' + targetSourceSignals.directShellUses.length === 0 || hasStyleImport + ? targetSourceSignals.directShellUses.length === 0 + ? auxiliaryShellUsageOnly + ? `Only auxiliary gallery/reference files import typical-page shell components, so app-entry styles import is not yet required for business delivery. Examples: ${summarizePathList(targetRoot, targetSourceSignals.auxiliaryShellUses)}` + : 'No current business source file imports typical-page shell components yet' : entryFile - : `Source files already use typical-page shell components, so add \`import '@hiui-design/typical-page-shells/styles.css'\` in the app entry before continuing. Examples: ${summarizePathList(targetRoot, targetSourceSignals.usesShells)}`, + : `Business source files already use typical-page shell components, so add \`import '@hiui-design/typical-page-shells/styles.css'\` in the app entry before continuing. Examples: ${summarizePathList(targetRoot, targetSourceSignals.directShellUses)}`, }) pushCheck(checks, { id: 'single-host-adapter-path-family', ok: targetSourceSignals.localHostAdapterImports.length === 0 || - targetSourceSignals.usesShells.length === 0, + targetSourceSignals.directShellUses.length === 0, severity: 'error', summary: 'host provider and page shells do not mix local host-adapter paths with package imports', detail: targetSourceSignals.localHostAdapterImports.length === 0 || - targetSourceSignals.usesShells.length === 0 + targetSourceSignals.directShellUses.length === 0 ? 'No mixed local-host-adapter and package shell import pattern was detected' - : `Do not mix local host adapter paths with @hiui-design/typical-page-shells imports. Keep both provider and consumer on the package path family. Local adapter examples: ${summarizePathList(targetRoot, targetSourceSignals.localHostAdapterImports)}; package import examples: ${summarizePathList(targetRoot, targetSourceSignals.usesShells)}`, + : `Do not mix local host adapter paths with @hiui-design/typical-page-shells imports in business delivery code. Keep both provider and consumer on the package path family. Local adapter examples: ${summarizePathList(targetRoot, targetSourceSignals.localHostAdapterImports)}; package import examples: ${summarizePathList(targetRoot, targetSourceSignals.directShellUses)}`, }) pushCheck(checks, { diff --git a/skills/hiui-design/scripts/typical-page-preflight.mjs b/skills/hiui-design/scripts/typical-page-preflight.mjs index 4750430b2..54cfae8f1 100644 --- a/skills/hiui-design/scripts/typical-page-preflight.mjs +++ b/skills/hiui-design/scripts/typical-page-preflight.mjs @@ -7,9 +7,11 @@ import { loadArchetypeDefinition } from './lib/archetypes/load-archetype-manifes import { computeManagedPageSourceSnapshot, syncManagedPageRegistry, + writeManagedPageContractArtifacts, } from './lib/managed-page-artifacts.mjs' import { buildTypicalPageReuseTargetError } from './lib/typical-page-route-ownership.mjs' import { loadPageTypeManifest } from './lib/load-page-type-manifest.mjs' +import { evaluateManagedInstanceReadinessFromSource } from './lib/managed-page-instance-readiness.mjs' import { validateManagedPageSource } from './lib/managed-page-source-guard.mjs' import { getManagedPageSemanticContract, @@ -20,7 +22,6 @@ import { loadRulesOnlyPageContracts, normalizeContractPath, reconcileManagedPageRuntimeSmokeWorkflow, - renderRulesOnlyPageContractMarkdown, toContractSlug, validateRulesOnlyPageContract, } from './lib/rules-only-page-contracts.mjs' @@ -98,6 +99,10 @@ function buildContractFixtureReport() { return { schemaVersion: 'preflight-report.v1', status: 'passed', + preflightStage: 'implementation', + readyForImplementation: true, + readyForDelivery: true, + deferredChecks: [], page: 'src/pages/orders/index.tsx', pageType: 'table-basic', checks: [ @@ -140,28 +145,108 @@ function buildContractFixtureReport() { } } +function derivePreflightExecutionState({ failures, warnings, workflowStatus }) { + const hasBlockingFailures = failures.length > 0 + const hasPlaceholderWarning = + Array.isArray(warnings) && + warnings.some((warning) => String(warning || '').toLowerCase().includes('placeholder')) + + if (hasBlockingFailures) { + return { + preflightStage: 'implementation-blocked', + readyForImplementation: false, + readyForDelivery: false, + deferredChecks: [], + } + } + + if (workflowStatus === 'started' || hasPlaceholderWarning) { + return { + preflightStage: 'scaffold-baseline', + readyForImplementation: true, + readyForDelivery: false, + deferredChecks: ['placeholderMappings'], + } + } + + if (workflowStatus === 'stale') { + return { + preflightStage: 'implementation', + readyForImplementation: true, + readyForDelivery: false, + deferredChecks: ['finalizePage'], + } + } + + return { + preflightStage: 'implementation', + readyForImplementation: true, + readyForDelivery: true, + deferredChecks: [], + } +} + +function deriveSourceGuardProfile(contract) { + const generationProfile = contract?.generationProfile || {} + const deliveryKind = String(generationProfile.selectedDeliveryAssetKind || '').trim() + if (deliveryKind === 'project-certified-carrier') { + return { + sourceGuardScope: 'transitive', + sourceGuardReason: 'project-certified-carrier', + } + } + + if (String(generationProfile.startFrom || '').trim() === 'page-component') { + return { + sourceGuardScope: 'transitive', + sourceGuardReason: deliveryKind || 'page-component', + } + } + + return { + sourceGuardScope: 'entry-only', + sourceGuardReason: deliveryKind || 'managed-start-point', + } +} + +function getScaffoldBaselineWarnings(contract) { + const generationProfile = contract?.generationProfile || {} + if ( + String(contract?.archetypeMode || '').trim() === 'rules-only' && + String(generationProfile.strategy || '').trim() === 'page-component' && + String(generationProfile.startFrom || '').trim() === 'page-component' && + String(generationProfile.selectedDeliveryAssetKind || '').trim() === 'direct-standard-component' + ) { + return [ + 'Contract placeholders remain for the rules-only page-component scaffold baseline. Complete business slot replacement and confirm managed contract targets before delivery.', + ] + } + + return [] +} + function findUnresolvedMappings(contract) { - const failures = [] + const warnings = [] const regionMappings = Array.isArray(contract?.regionMapping) ? contract.regionMapping : [] const ownershipMappings = Array.isArray(contract?.ownershipMapping) ? contract.ownershipMapping : [] for (const mapping of regionMappings) { if (String(mapping?.target || '').includes('TODO:')) { - failures.push(`regionMapping.${mapping.region} still points to a TODO target`) + warnings.push(`placeholder mapping regionMapping.${mapping.region} still points to a TODO target`) } } for (const mapping of ownershipMappings) { if (String(mapping?.target || '').includes('TODO:')) { - failures.push(`ownershipMapping.${mapping.role} still points to a TODO target`) + warnings.push(`placeholder mapping ownershipMapping.${mapping.role} still points to a TODO target`) } } if (String(contract?.hostArchetypePath || '').includes('TODO:')) { - failures.push('hostArchetypePath still points to a TODO target') + warnings.push('placeholder mapping hostArchetypePath still points to a TODO target') } - return failures + return warnings } function hasManagedChartSection(contract) { @@ -213,11 +298,72 @@ function isChartGovernanceFailure(message) { ].some((fragment) => String(message || '').includes(fragment)) } +function isManagedFilterChainFailure(message) { + const text = String(message || '') + return [ + 'declares a query-filter region but source does not reference QueryFilter', + 'hand-builds its filter region from primitive Input/Select/DatePicker controls', + 'rebuilds a list page from primitive filter controls + Table + Pagination', + 'searchConfig.fields', + 'SearchForm', + 'getSearchFields', + 'raw filter bar with QueryFilter', + ].some((fragment) => text.includes(fragment)) +} + +function isQueryFilterBaselineFailure(message) { + const text = String(message || '') + return [ + 'showLabel on inline QueryFilter', + 'appearance away from the shared contained baseline', + 'plain reset/clear Button into inline QueryFilter', + 'keyword search field regresses to a plain Input', + 'adds a custom 查询/搜索 button into QueryFilter append actions', + 'renders “全部筛选” with a plain Button', + ].some((fragment) => text.includes(fragment)) +} + +function isHostStyleContaminationFailure(message) { + const text = String(message || '') + return [ + 'Business pages must not restyle PageHeader, QueryFilter, Table', + 'This is treated as host-style contamination', + 'overrides PageHeader geometry in page-local styles', + 'forces 60px height and alignItems:center on the PageHeader root', + 'changes the PageHeader root into a local flex/grid layout carrier', + 'overrides shared dashboard shell chrome through pageRootStyle/whiteBodyStyle', + ].some((fragment) => text.includes(fragment)) +} + +function isListWorkspaceWidthFailure(message) { + const text = String(message || '') + return [ + 'own horizontal scrolling', + 'horizontal scroll owner', + 'width-adaptive', + 'content width keywords such as max-content/min-content/fit-content', + 'adapt to the parent container width instead of expanding the surrounding workspace', + 'table-body horizontal inset contract', + ].some((fragment) => text.includes(fragment)) +} + export function checkIdForFailure(message) { const text = String(message || '') if (isLegacyRuntimeAdapterFailure(text)) { return 'legacyRuntimeAdapter' } + if (isManagedFilterChainFailure(text)) { + return 'managedFilterChain' + } + if (isQueryFilterBaselineFailure(text)) { + return 'queryFilterBaseline' + } + if (isHostStyleContaminationFailure(text)) { + return 'hostStyleContamination' + } + if (isListWorkspaceWidthFailure(text)) { + return 'listWorkspaceWidth' + } if (text.includes('generationProfile.')) { return 'generationProfile' } @@ -293,6 +439,10 @@ export function failureCodeForCheckId(checkId) { if (checkId === 'regionContract') return 'REGION_CONTRACT_MISMATCH' if (checkId === 'ownershipMapping') return 'OWNERSHIP_MAPPING_MISMATCH' if (checkId === 'legacyRuntimeAdapter') return 'LEGACY_RUNTIME_ADAPTER_MISMATCH' + if (checkId === 'managedFilterChain') return 'HIUI022_MANAGED_FILTER_CHAIN_MISSING' + if (checkId === 'queryFilterBaseline') return 'HIUI023_QUERY_FILTER_BASELINE_DRIFT' + if (checkId === 'hostStyleContamination') return 'HIUI024_HOST_STYLE_CONTAMINATION' + if (checkId === 'listWorkspaceWidth') return 'HIUI025_LIST_WORKSPACE_WIDTH_OWNER_DRIFT' if (checkId === 'sourceMarker') return 'SOURCE_MARKER_MISSING' if (checkId === 'routeOwnership') return 'ROUTE_OWNER_MISSING' if (checkId === 'directoryArtifacts') return 'DIRECTORY_ARTIFACTS_MISSING' @@ -617,16 +767,11 @@ async function findTranslationMapFailures({ contract, normalizedPagePath, snapsh } async function writeContractArtifacts(contractEntry) { - await fs.writeFile( - contractEntry.filePath, - `${JSON.stringify(contractEntry.contract, null, 2)}\n`, - 'utf8' - ) - await fs.writeFile( - contractEntry.filePath.replace(/\.json$/, '.md'), - renderRulesOnlyPageContractMarkdown(contractEntry.contract), - 'utf8' - ) + return writeManagedPageContractArtifacts({ + contract: contractEntry.contract, + contractJsonPath: contractEntry.filePath, + contractMarkdownPath: contractEntry.filePath.replace(/\.json$/, '.md'), + }) } async function main() { @@ -693,7 +838,7 @@ async function main() { archetypeDefinition, baselineSpec, }) - const unresolvedMappings = hasPageTypeDrift ? [] : findUnresolvedMappings(contractEntry.contract) + const unresolvedMappingWarnings = hasPageTypeDrift ? [] : findUnresolvedMappings(contractEntry.contract) const directoryArtifactFailures = hasPageTypeDrift ? [] : await findDirectoryArtifactFailures({ @@ -707,6 +852,7 @@ async function main() { generatedPagePath: normalizedPagePath, targetRoot, }) + const sourceRaw = await fs.readFile(path.join(targetRoot, normalizedPagePath), 'utf8').catch(() => '') const snapshot = computeManagedPageSourceSnapshot({ generatedPagePath: normalizedPagePath, targetRoot, @@ -723,11 +869,13 @@ async function main() { ...pageTypeDriftFailures, ...contractValidation.errors, ...findLegacyRuntimeAdapterFailures(contractEntry.contract), - ...unresolvedMappings, ...directoryArtifactFailures, ...translationMapFailures, ...sourceErrors, ] + const scaffoldBaselineWarnings = failures.length === 0 + ? [...unresolvedMappingWarnings, ...getScaffoldBaselineWarnings(contractEntry.contract)] + : [] const previousWorkflow = contractEntry.contract.workflow || {} const previousSnapshotHash = String(previousWorkflow.sourceSnapshotHash || '').trim() @@ -740,9 +888,19 @@ async function main() { const workflowStatus = wasFinalized && previousSnapshotHash && previousSnapshotHash !== snapshot.hash ? 'stale' - : failures.length === 0 && !wasFinalized + : failures.length === 0 && !wasFinalized && scaffoldBaselineWarnings.length === 0 ? 'preflight-pass' : previousWorkflow.status || 'started' + const executionState = derivePreflightExecutionState({ + failures, + warnings: [ + ...contractValidation.warnings.filter( + (warning) => !warning.startsWith('generated:') + ), + ...scaffoldBaselineWarnings, + ], + workflowStatus, + }) contractEntry.contract.workflow = { ...previousWorkflow, @@ -757,14 +915,21 @@ async function main() { workflowStatus === 'stale' ? 'Source snapshot changed after finalize-page. Re-run typical-page:finalize-page on the latest page source.' : '', + preflightStage: executionState.preflightStage, + readyForImplementation: executionState.readyForImplementation, + readyForDelivery: executionState.readyForDelivery, + deferredChecks: executionState.deferredChecks, lastCommand: 'typical-page:preflight', } await writeContractArtifacts(contractEntry) await syncManagedPageRegistry(targetRoot) - const warnings = contractValidation.warnings.filter( - (warning) => !warning.startsWith('generated:') - ) + const warnings = [ + ...contractValidation.warnings.filter( + (warning) => !warning.startsWith('generated:') + ), + ...scaffoldBaselineWarnings, + ] const checks = checksForPreflight({ failures, warnings }) const suggestedActions = suggestedActionsForPreflight({ failures, @@ -772,11 +937,27 @@ async function main() { contract: contractEntry.contract, }) const blockingIssues = blockingIssuesForFailures(failures) + const managedInstanceReadiness = evaluateManagedInstanceReadinessFromSource({ + contract: { + pageTypeId: contractEntry.contract.pageTypeId, + workflowStatus: contractEntry.contract.workflow?.status, + preflightStatus: contractEntry.contract.workflow?.preflightStatus, + }, + pageExists: true, + sourceRaw, + }) + const sourceGuardProfile = deriveSourceGuardProfile(contractEntry.contract) const payload = { schemaVersion: 'preflight-report.v1', status: failures.length > 0 ? 'failed' : 'passed', + preflightStage: executionState.preflightStage, + readyForImplementation: executionState.readyForImplementation, + readyForDelivery: executionState.readyForDelivery, + deferredChecks: executionState.deferredChecks, page: normalizedPagePath, pageType: contractEntry.contract.pageTypeId, + sourceGuardScope: sourceGuardProfile.sourceGuardScope, + sourceGuardReason: sourceGuardProfile.sourceGuardReason, checks, blockingReasons: failures, blockingIssues, @@ -798,6 +979,7 @@ async function main() { ...getManagedChartGovernanceSummary(contractEntry.contract), i18nStrategy: contractEntry.contract.i18nBaseline?.runtimePolicy || '(not-declared)', formatterPolicy: contractEntry.contract.i18nBaseline?.formatterPolicy || [], + managedInstanceReadiness, warnings, failures, } diff --git a/skills/hiui-design/scripts/typical-page-start-page.mjs b/skills/hiui-design/scripts/typical-page-start-page.mjs index f2d62ccfc..2d09c982d 100644 --- a/skills/hiui-design/scripts/typical-page-start-page.mjs +++ b/skills/hiui-design/scripts/typical-page-start-page.mjs @@ -424,10 +424,13 @@ function ensureStartPageGenerationProfileRequiredGates(generationProfile, mode) const modeId = String(mode || '').trim() const strategy = String(generationProfile.strategy || '').trim() const startFrom = String(generationProfile.startFrom || '').trim() + const sourceProofLevel = String(generationProfile.sourceProofLevel || '').trim() const requiredBaseline = modeId === 'legacy-host-compatible' ? strategy === 'page-component' && startFrom === 'page-component' - ? ['source-gate', 'preflight', 'page-instance-validation'] + ? sourceProofLevel === 'strict-source-adapter-proof' + ? ['source-gate', 'preflight', 'page-instance-validation'] + : ['slot-gate', 'preflight', 'page-instance-validation'] : ['source-gate', 'preflight'] : ['slot-gate', 'preflight'] const existingGates = Array.isArray(generationProfile.requiredGates) @@ -481,18 +484,75 @@ function buildLegacyPageComponentStartBlockReason(plan, pageTypeId) { return `typical-page:start-page cannot scaffold ${pageTypeId} in legacy-host-compatible until plan-page-task resolves the page-component fast path. Expected selected certified page component plus available runtime bridge asset; received ${details.join(', ')}. Resolve runtime adapter / certification facts first and refresh the planner contract.` } +function plannerRequiresComponentSemanticMainline(plan) { + const modeId = String(plan?.mode?.id || '').trim() + if (!['rules-only', 'legacy-host-compatible'].includes(modeId)) { + return false + } + + return ( + String(plan?.generationStrategyId || '').trim() === 'page-component' || + String(plan?.assetResolution?.semanticStrategyId || '').trim() === 'page-component' || + String(plan?.generationProfile?.strategy || '').trim() === 'page-component' || + String(plan?.targetDeliverySemantics?.deliveryPath || '').trim() === 'page-component-plus-slot-fill' || + plan?.pageComponent?.selected === true + ) +} + +function collectPlannerBlockingReasons(plan) { + return Array.from( + new Set( + [ + ...(Array.isArray(plan?.blockingReasons) ? plan.blockingReasons : []), + ...(Array.isArray(plan?.facts?.blockingReasons) ? plan.facts.blockingReasons : []), + ...(Array.isArray(plan?.pageComponent?.legacyHostFamily?.blockingReasons) + ? plan.pageComponent.legacyHostFamily.blockingReasons + : []), + ...(Array.isArray(plan?.pageComponent?.candidates) + ? plan.pageComponent.candidates + .filter( + (candidate) => + String(candidate?.effectiveMode || candidate?.mode || '').trim() === + String(plan?.mode?.id || '').trim() && + candidate?.certificationAvailable === false && + candidate?.componentId + ) + .map( + (candidate) => + `${String(candidate.componentId).trim()} standard page component is not fully certified` + ) + : []), + String(plan?.pageComponent?.runtimeAdapterProof?.reason || '').trim(), + String(plan?.assetResolution?.reason || '').trim(), + String(plan?.assetResolution?.adapterResolution?.reason || '').trim(), + String(plan?.assetResolution?.runtimeBridgeResolution?.reason || '').trim(), + ] + .map((item) => String(item || '').trim()) + .filter(Boolean) + ) + ) +} + function assertComponentSemanticStartPlan(plan, pageTypeId) { - const support = plan?.projectTypicalPageSupport - if (!support?.enforcePageComponentSemantics) { + if (!plannerRequiresComponentSemanticMainline(plan)) { return } - if (support.status !== 'ready') { - const blockingReasons = Array.isArray(support.blockingReasons) - ? support.blockingReasons.filter(Boolean) - : [] + const blockingReasons = collectPlannerBlockingReasons(plan) + const currentExecutionStatus = String(plan?.currentExecutionState?.status || '').trim() + const modeId = String(plan?.mode?.id || '').trim() + const needsSelectedComponent = + modeId === 'legacy-host-compatible' || modeId === 'rules-only' + + if ( + plan?.status !== 'ready' || + plan?.canStartImplementation === false || + currentExecutionStatus === 'blocked' || + String(plan?.assetResolution?.status || '').trim() !== 'available' || + (needsSelectedComponent && plan?.pageComponent?.selected !== true) + ) { throw new Error( - `typical-page:start-page cannot scaffold ${pageTypeId} because component-semantic typical page support is ${support.status}. ${ + `typical-page:start-page cannot scaffold ${pageTypeId} because component-semantic typical page support is blocked. ${ blockingReasons.length > 0 ? `Blocking reasons: ${blockingReasons.join(' | ')}` : 'Refresh the planner contract and repair the shared page-component support first.' }` ) @@ -2000,17 +2060,13 @@ ${hasChartSection ? `export function ${supportingSectionsComponentName}() { dd { min-width: 0; color: #3c4658; - text-align: right; + text-align: end; } } .serviceRecordTable { min-width: 0; margin-bottom: 20px; - - :global(.hi-v5-table__cell) { - color: #3c4658; - } } @media (max-width: 900px) { @@ -2565,25 +2621,73 @@ function buildHostIntegrationOwnershipMapping(pageType, explicitOwnerships) { } function buildLegacyPageComponentRegionMapping(pageTypeId, explicitRegions) { - if (explicitRegions.length > 0) { - return explicitRegions.map(({ key, target }) => ({ region: key, target })) - } - - return getRequiredRegionsForPageType(pageTypeId).map((region) => ({ + const requiredMappings = getRequiredRegionsForPageType(pageTypeId).map((region) => ({ region, target: `page-component:${region}`, })) -} -function buildLegacyPageComponentOwnershipMapping(pageTypeId, explicitOwnerships) { - if (explicitOwnerships.length > 0) { - return explicitOwnerships.map(({ key, target }) => ({ role: key, target })) + if (explicitRegions.length === 0) { + return requiredMappings } - return getRequiredOwnershipRolesForPageType(pageTypeId).map((role) => ({ + const requiredRegionNames = new Set( + requiredMappings.map((item) => String(item.region || '').trim().toLowerCase()) + ) + const explicitMappingByRegion = new Map( + explicitRegions.map(({ key, target }) => [ + String(key || '').trim().toLowerCase(), + { region: key, target }, + ]) + ) + + const mergedMappings = requiredMappings.map((item) => { + const explicit = explicitMappingByRegion.get(String(item.region || '').trim().toLowerCase()) + return explicit || item + }) + + for (const { key, target } of explicitRegions) { + const normalizedKey = String(key || '').trim().toLowerCase() + if (!requiredRegionNames.has(normalizedKey)) { + mergedMappings.push({ region: key, target }) + } + } + + return mergedMappings +} + +function buildLegacyPageComponentOwnershipMapping(pageTypeId, explicitOwnerships) { + const requiredOwnerships = getRequiredOwnershipRolesForPageType(pageTypeId).map((role) => ({ role, target: `page-component:${role}`, })) + + if (explicitOwnerships.length === 0) { + return requiredOwnerships + } + + const requiredRoleNames = new Set( + requiredOwnerships.map((item) => String(item.role || '').trim().toLowerCase()) + ) + const explicitOwnershipByRole = new Map( + explicitOwnerships.map(({ key, target }) => [ + String(key || '').trim().toLowerCase(), + { role: key, target }, + ]) + ) + + const mergedOwnerships = requiredOwnerships.map((item) => { + const explicit = explicitOwnershipByRole.get(String(item.role || '').trim().toLowerCase()) + return explicit || item + }) + + for (const { key, target } of explicitOwnerships) { + const normalizedKey = String(key || '').trim().toLowerCase() + if (!requiredRoleNames.has(normalizedKey)) { + mergedOwnerships.push({ role: key, target }) + } + } + + return mergedOwnerships } function getDefaultHostIntegrationArchetypePath(pageType) { @@ -2975,19 +3079,23 @@ async function main() { runtimeSmoke: options.runtimeSmoke, topology: resolvedTopology, }) || startPlanSeed?.runtimeSmokePlan - const legacyFastPathGenerationProfile = - startPlan && isLegacyPageComponentFastPathReady(startPlan) + const pageComponentDirectScaffoldGenerationProfile = + startPlan && + startPlan?.pageComponent?.selected === true && + String(startPlan?.generationStrategyId || '').trim() === 'page-component' && + String(startPlan?.startFrom?.id || '').trim() === 'page-component' && + mode !== 'host-integration' ? startPlan.generationProfile : null const resolvedGenerationProfile = ensureStartPageGenerationProfileRequiredGates( - legacyFastPathGenerationProfile || startPlanSeed?.generationProfile || null, + pageComponentDirectScaffoldGenerationProfile || startPlanSeed?.generationProfile || null, mode ) const hintedOwnershipMapping = buildOwnershipMappingFromPlanHints(explicitOwnerships, startPlanSeed?.ownershipPlan) - const resolvedRegionMapping = legacyFastPathGenerationProfile + const resolvedRegionMapping = pageComponentDirectScaffoldGenerationProfile ? buildLegacyPageComponentRegionMapping(pageType.id, explicitRegions) : regionMapping - const resolvedOwnershipMapping = legacyFastPathGenerationProfile + const resolvedOwnershipMapping = pageComponentDirectScaffoldGenerationProfile ? buildLegacyPageComponentOwnershipMapping(pageType.id, explicitOwnerships) : hintedOwnershipMapping.length > 0 ? hintedOwnershipMapping @@ -3003,7 +3111,7 @@ async function main() { archetypeDefinition, archetypeSmokeBaseline: findArchetypeSmokeBaselineEntry(baselineSpec, pageType.id), generatedPagePath: pagePath, - hostArchetypePath: legacyFastPathGenerationProfile + hostArchetypePath: pageComponentDirectScaffoldGenerationProfile ? '' : normalizeContractPath( targetRoot, @@ -3071,7 +3179,8 @@ async function main() { ...(startPlanSeed?.layoutArchetype ? ['Planner layout facts were materialized into the started contract so scaffold and overlay guidance stay in sync from the first write.'] : []), - ...(legacyFastPathGenerationProfile + ...(pageComponentDirectScaffoldGenerationProfile && + mode === 'legacy-host-compatible' ? ['Planner selected the legacy page-component fast path, so this scaffold uses the runtime bridge wrapper plus business-slot adapter instead of host archetype translation.'] : []), ], @@ -3085,6 +3194,14 @@ async function main() { }, }) + if (pageComponentDirectScaffoldGenerationProfile && mode === 'rules-only') { + contract.adapterContract = { + ...contract.adapterContract, + hostAdapterId: '', + hostAdapterLabel: '', + } + } + const scaffoldArtifacts = await buildScaffoldArtifacts({ skillRoot, pagePath, diff --git a/skills/hiui-design/scripts/verify-typical-page-doctor-rules-only-regressions.mjs b/skills/hiui-design/scripts/verify-typical-page-doctor-rules-only-regressions.mjs index bb6c04a67..774119a30 100644 --- a/skills/hiui-design/scripts/verify-typical-page-doctor-rules-only-regressions.mjs +++ b/skills/hiui-design/scripts/verify-typical-page-doctor-rules-only-regressions.mjs @@ -556,8 +556,8 @@ async function main() { true, 'Expected legacy bridge-only projects to stay valid without direct shell runtime imports.' ) - assert.match(legacyBridgeOnlyRuntimeCheck.detail, /not-required-in-current-mode/) - assert.match(legacyBridgeOnlyRuntimeCheck.detail, /page-component \+ runtime bridge \+ slot fill/) + assert.match(legacyBridgeOnlyRuntimeCheck.detail, /legacy bridge path/) + assert.match(legacyBridgeOnlyRuntimeCheck.detail, /planner-selected page components/) const legacyBridgeOnlyShellDeclCheck = getCheck(legacyBridgeOnlyPayload, 'shells-declared') assert(legacyBridgeOnlyShellDeclCheck, 'Expected doctor to emit shells-declared.') @@ -566,7 +566,7 @@ async function main() { true, 'Expected legacy bridge-only projects to allow missing @hiui-design/typical-page-shells declarations.' ) - assert.match(legacyBridgeOnlyShellDeclCheck.detail, /not required to declare/) + assert.match(legacyBridgeOnlyShellDeclCheck.detail, /without declaring @hiui-design\/typical-page-shells/) const legacyBridgeOnlyStyleCheck = getCheck(legacyBridgeOnlyPayload, 'styles-import') assert(legacyBridgeOnlyStyleCheck, 'Expected doctor to emit styles-import.') @@ -575,7 +575,10 @@ async function main() { true, 'Expected legacy bridge-only projects to allow missing styles.css imports.' ) - assert.match(legacyBridgeOnlyStyleCheck.detail, /not required-in-current-mode/) + assert.match( + legacyBridgeOnlyStyleCheck.detail, + /does not require @hiui-design\/typical-page-shells\/styles\.css/ + ) const legacyDirectShellPayload = runDoctorJson( legacyDirectShellRoot, @@ -591,25 +594,28 @@ async function main() { false, 'Expected legacy projects with direct shell imports to fail the downgraded runtime check.' ) - assert.match(legacyDirectShellRuntimeCheck.detail, /required-if-direct-standard-shell-runtime-selected/) + assert.match(legacyDirectShellRuntimeCheck.detail, /cannot stay on the downgraded host runtime/) const legacyDirectShellDeclCheck = getCheck(legacyDirectShellPayload, 'shells-declared') assert(legacyDirectShellDeclCheck, 'Expected doctor to emit shells-declared.') assert.equal( legacyDirectShellDeclCheck.ok, - false, - 'Expected legacy projects with direct shell imports to require a package.json declaration.' + true, + 'Expected legacy projects with direct shell imports to keep dependency declaration on the legacy-bridge optional path.' ) - assert.match(legacyDirectShellDeclCheck.detail, /must now declare/) + assert.doesNotMatch(legacyDirectShellDeclCheck.detail, /must now declare/) const legacyDirectShellInstallCheck = getCheck(legacyDirectShellPayload, 'shells-installed') assert(legacyDirectShellInstallCheck, 'Expected doctor to emit shells-installed.') assert.equal( legacyDirectShellInstallCheck.ok, - false, - 'Expected legacy projects with direct shell imports to require an installed package copy.' + true, + 'Expected legacy projects with direct shell imports to keep installed package checks on the legacy-bridge optional path.' + ) + assert.match( + legacyDirectShellInstallCheck.detail, + /does not require installing @hiui-design\/typical-page-shells/ ) - assert.match(legacyDirectShellInstallCheck.detail, /required-if-direct-standard-shell-runtime-selected/) const legacyDirectShellStyleCheck = getCheck(legacyDirectShellPayload, 'styles-import') assert(legacyDirectShellStyleCheck, 'Expected doctor to emit styles-import.') @@ -618,7 +624,10 @@ async function main() { false, 'Expected legacy projects with direct shell imports to require styles.css.' ) - assert.match(legacyDirectShellStyleCheck.detail, /Add `import '@hiui-design\/typical-page-shells\/styles\.css'`/) + assert.match( + legacyDirectShellStyleCheck.detail, + /@hiui-design\/typical-page-shells\/styles\.css/ + ) const legacyAuxiliaryShellPayload = runDoctorJson( legacyAuxiliaryShellRoot, @@ -634,7 +643,7 @@ async function main() { true, 'Expected gallery/reference-only shell imports to stay on the legacy bridge path.' ) - assert.match(legacyAuxiliaryRuntimeCheck.detail, /not-required-in-current-mode/) + assert.match(legacyAuxiliaryRuntimeCheck.detail, /legacy bridge path/) const legacyAuxiliaryDeclCheck = getCheck(legacyAuxiliaryShellPayload, 'shells-declared') assert(legacyAuxiliaryDeclCheck, 'Expected doctor to emit shells-declared.') @@ -652,8 +661,8 @@ async function main() { console.log('- host-integration doctor accepts shared top-level 示例 gallery route menus') console.log('- host-integration doctor rejects duplicated first-level AppStoreFilled placeholders') console.log('- host-integration doctor accepts distinct semantic first-level Filled icons') - console.log('- legacy bridge-only doctor treats shell dependency/style imports as not-required-in-current-mode') - console.log('- legacy direct-shell doctor upgrades dependency/style/runtime checks to required-if-direct-standard-shell-runtime-selected') + console.log('- legacy bridge-only doctor treats shell dependency/style imports as legacy-bridge optional checks') + console.log('- legacy direct-shell doctor fails runtime compatibility, keeps dependency/install optional, and still requires styles import') console.log('- legacy auxiliary gallery/reference shell imports do not escalate into direct-shell runtime mode') } finally { await removeTree(tempRoot) diff --git a/skills/hiui-design/scripts/write-rules-only-page-contract.mjs b/skills/hiui-design/scripts/write-rules-only-page-contract.mjs index 1cd56fece..4351856c9 100644 --- a/skills/hiui-design/scripts/write-rules-only-page-contract.mjs +++ b/skills/hiui-design/scripts/write-rules-only-page-contract.mjs @@ -490,6 +490,23 @@ function parseOwnershipMappings(values) { }) } +function shouldCarryExistingContractMetadata({ + existingContract, + generatedPagePath, + pageTypeId, + archetypeMode, +}) { + if (!existingContract || typeof existingContract !== 'object') { + return false + } + + return ( + String(existingContract.generatedPagePath || '').trim() === String(generatedPagePath || '').trim() && + String(existingContract.pageTypeId || '').trim() === String(pageTypeId || '').trim() && + String(existingContract.archetypeMode || '').trim() === String(archetypeMode || '').trim() + ) +} + async function ensureDir(targetDir) { await fs.mkdir(targetDir, { recursive: true }) } @@ -552,6 +569,12 @@ async function main() { .then((raw) => JSON.parse(raw)) .catch(() => null) const shouldCarryExistingMappings = !options.preset && existingContract?.pageTypeId === pageType.id + const shouldCarryExistingMetadata = shouldCarryExistingContractMetadata({ + existingContract, + generatedPagePath, + pageTypeId: pageType.id, + archetypeMode, + }) const presetRegionValues = options.preset ? buildStandardRegionValues(pageType.id) : [] const presetOwnershipValues = options.preset ? buildStandardOwnershipValues(pageType.id) : [] const regionMapping = buildRegionMappings( @@ -581,11 +604,26 @@ async function main() { ? 'page-surface-owns-workspace' : ''), ownershipMapping, + generationProfile: shouldCarryExistingMetadata ? existingContract?.generationProfile || null : null, + productionContract: shouldCarryExistingMetadata ? existingContract?.productionContract || null : null, adapterContract: { - localBypasses, + ...(shouldCarryExistingMetadata && existingContract?.adapterContract + ? existingContract.adapterContract + : {}), + localBypasses: + localBypasses.length > 0 + ? localBypasses + : shouldCarryExistingMetadata && Array.isArray(existingContract?.adapterContract?.localBypasses) + ? existingContract.adapterContract.localBypasses + : [], }, semanticContract: { - queryFilterRegionRole: options.queryFilterRegionRole, + ...(shouldCarryExistingMetadata && existingContract?.semanticContract + ? existingContract.semanticContract + : {}), + ...(options.queryFilterRegionRole + ? { queryFilterRegionRole: options.queryFilterRegionRole } + : {}), }, notes: options.notes, deviations: options.deviations, diff --git a/skills/hiui-page-workflow/CHANGELOG.md b/skills/hiui-page-workflow/CHANGELOG.md index 46b2f04e1..c6716a417 100644 --- a/skills/hiui-page-workflow/CHANGELOG.md +++ b/skills/hiui-page-workflow/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.1.0 - 2026-07-07 + +- align workflow planning and execution guidance with the current `hiui-design` Plan Gateway and execution-permission contract +- promote `requiredActions` / `formalAcceptanceActions` as the primary orchestration surface while keeping command fields as compatibility summaries +- add a minimal `planSnapshot` handoff/report contract and page-delivery-state reporting guidance +- update bundle metadata for the revised workflow release and document the expected `hiui-design` planner fields in the compatibility notes + ## 1.0.1 - 2026-07-02 - prepare the public preview of `hiui-page-workflow` diff --git a/skills/hiui-page-workflow/README.md b/skills/hiui-page-workflow/README.md index a6fd44e7a..4c6f996f0 100644 --- a/skills/hiui-page-workflow/README.md +++ b/skills/hiui-page-workflow/README.md @@ -21,6 +21,16 @@ - `hiui-design` - `ux-walkthrough` +## Architecture Boundary + +`hiui-page-workflow` 是门禁型总编排层,不是下游 skill 的实现镜像。 + +- 对 `refine-product-requirements`:消费需求细化结果、HiUI 交接包和确认状态,不复制其需求建模正文 +- 对 `hiui-design`:消费稳定 planner contract 与交付状态,例如正式 Plan Gateway、执行许可、结构化 actions、交付摘要;不镜像其全部页面实现字段或内部生命周期细节 +- 对 `ux-walkthrough`:消费证据门禁、问题分级和报告产物,不复制其完整 SOP 正文 + +因此,workflow core 的优化原则是“强约束对齐,弱实现耦合”:只吸收跨阶段协同真正需要的稳定面,不把下游 skill 的全部领域细节搬进 workflow 自身的数据结构。 + ## Distribution Status 当前已提供: @@ -94,6 +104,8 @@ bash install-workflow.sh --force-sync 如果你的目标是把 workflow 接到项目里,应通过 project adapter 把规则、模板和桥接资产落到业务仓库;如果你的目标是复用 workflow 规则,可直接使用 `SKILL.md`、`references/` 和 `bundle/` 里的约束与模板。 +页面规划相关的正式入口由下游 `hiui-design` 提供。workflow 在项目里应优先消费目标项目根的 Plan Gateway 与 `.local-context/hiui-design/` 接入产物,而不是直接把 skill 仓库里的脚本路径写进业务项目交付流程。 + ## Version Policy 默认安装策略由 `bundle/install-policy.json` 定义,当前行为如下: diff --git a/skills/hiui-page-workflow/SKILL.md b/skills/hiui-page-workflow/SKILL.md index 81b639aa6..e1478013d 100644 --- a/skills/hiui-page-workflow/SKILL.md +++ b/skills/hiui-page-workflow/SKILL.md @@ -30,6 +30,7 @@ description: >- - Do not 改变本 skill 的核心用途:它负责端到端交付编排,不复制下游 skill 的正文和执行细节。 - Before 任何页面源码修改,必须同时 validate:`generationInputGate` 已通过、HiUI 交接包已就绪、`hiui-design` 机器计划已完成。 +- Before 从页面规划进入页面实现,必须同时 validate:`plan.status=ready`、`facts.status=ready`、`currentExecutionState.status=ready`、`canStartImplementation=true`;任一条件不成立时,回到 `ResolveBlockingFacts`,must not 继续生成页面。 - Before 任何 UX 确定性结论,必须 validate `uxGate.evidenceStatus = ready`;证据不足时必须 refuse 确定性完整结论。 - Do not 在普通页面任务中默认调用 bundle 安装、升级、回滚脚本;除非用户明确要求处理分发控制面。 - Before 运行 `scripts/install-workflow-bundle.mjs`,必须 confirm 目标目录;未加 `--force-sync` 时,发现本地更高版本必须 keep 并提示,must not 静默降级。 @@ -86,7 +87,7 @@ description: >- | S0 需求与生成输入确认 | 细化需求,确认页面清单、页面提示词和 HiUI 交接包 | `generationInputGate.status = confirmed | assumption-authorized` | | S1 HiUI 页面规划 | 调用 `hiui-design` 规划页型、拓扑、命令和必读文档 | 机器计划完成,`blockingReasons` 为空或已处理 | | S2 页面生成 / 修改 | 基于确认输入写页面 | S1 完成,且再次确认 `generationInputGate` 已通过 | -| S3 工程验收 | 运行 requiredCommands / formalAcceptanceCommands | 命令结果已记录为 passed / failed / skipped / blocked | +| S3 工程验收 | 记录并执行进入验证 / 交付相位的 `requiredActions` / `formalAcceptanceActions` | 动作结果已记录为 passed / failed / skipped / blocked | | S4 UX 验收 | 按验收等级执行 `ux-smoke` / `ux-standard` / `ux-formal` | `uxGate.evidenceStatus = ready` 或明确降级 / 阻塞 | | S5 修复与最终交付 | 修复 P0/P1,复验并输出报告 | 工程结果、UX 结果、截图和风险已收口 | @@ -135,10 +136,12 @@ Gate 最小状态: 调用 `hiui-design`: 0. 检查 S0 交接包中的 `generationInputGate.status`;只有 `confirmed` 或 `assumption-authorized` 可以继续。 -1. 运行 `scripts/plan-page-task.mjs --json` -2. 结合 S0 的 HiUI 交接包,确认 `mode`、`topology`、`pageType` / `pageUnits`、`taskLevel`、`startFrom` -3. 只读取计划里的 `requiredDocs` -4. 若 `blockingReasons` 非空,先补齐事实,不直接实现 +1. 优先在目标项目根运行 `npm run typical-page:plan-page-task -- --json` +2. 若目标项目暂未注册 npm script,运行 `node ".local-context/hiui-design/scripts/plan-page-task.mjs" --json` +3. 若两者都不可用,fail closed:先修目标项目的 planner 入口或接入状态;不要把 `hiui-design` skill 源码仓里的 planner 脚本当成业务项目的默认 planner 入口 +4. 结合 S0 的 HiUI 交接包,确认 `mode`、`topology`、`pageType` / `pageUnits`、`taskLevel`、`startFrom` +5. 按 `requiredDocs[].readMode` 消费计划文档:先读 `required`,`reference` / `conditional` 只按 `reason` 命中情况补读 +6. 若 `plan.status!=ready`、`facts.status!=ready`、`currentExecutionState.status!=ready`、`canStartImplementation=false`,或 `currentExecutionState.primaryAction=ResolveBlockingFacts`,先补齐阻断事实,不直接实现 首轮计划以 `plan-page-task` 的 JSON 为准,不手工重造页型结论。 @@ -146,11 +149,11 @@ Gate 最小状态: 继续按 `hiui-design` 执行: -- 快速链路:只替换业务槽位 -- 标准链路:已有典型页局部改动,或新建 `data-visualization` -- 严格链路:非典型、组合页、split、复杂联动、ownership 或滚动结构变化 +- 执行面默认以 `requiredActions` 为主;`requiredCommands` 只作为兼容摘要或人工复核视图 +- 生成路径以 `targetDeliverySemantics`、`generationRecipe`、`generationInputs` 与 `requiredActions` 为准;不要由 workflow 手工重建下游页型策略 +- “快速 / 标准 / 严格链路”只用于沟通风险和验收深度,不替代 planner 的结构化事实与动作分期 -执行任何文件变更前,再次检查 `generationInputGate` 与 S1 机器计划;若没有已通过的生成输入、交接包或机器计划,必须停止并回到 S0 / S1,不得“先生成再补需求”。 +执行任何文件变更前,再次检查 `generationInputGate` 与 S1 机器计划;若 `generationInputGate` 未通过、HiUI 交接包未就绪,或 `plan.status` / `facts.status` / `currentExecutionState.status` 不是 `ready`,或 `canStartImplementation!=true`,必须停止并回到 S0 / S1,不得“先生成再补需求”。 不得破坏: @@ -162,11 +165,14 @@ Gate 最小状态: - source marker - contract ownership -## S3:工程验收 +## S3:工程验证与交付前检查 -按计划执行 `requiredCommands`。 +本阶段只承接进入验证 / 交付确认相位的动作: -若 workflow level 是 `formal-e2e`,追加 `formalAcceptanceCommands`。 +- 默认以 `requiredActions` 与 `formalAcceptanceActions` 为主调度面 +- `requiredCommands` 与 `formalAcceptanceCommands` 仅作为兼容摘要,不再单独定义 workflow 阶段 +- phase 属于 `ResolveBlockingFacts`、`GenerateOrEdit`、`WriteContract` 的动作,必须在前序阶段完成;不得堆到 S3 再统一执行 +- 进入 S3 后,优先记录 `Preflight`、工程脚本、`FormalAcceptance` 及其他交付前验证动作的真实结果 记录每条命令的结果: @@ -206,7 +212,7 @@ Gate 最小状态: |---|---|---|---| | `quick-preview` | `ux-smoke` | 轻量可用性自检 | 页面可见、主入口可见、无空白 / 错路由、最终截图留证 | | `standard-e2e` | `ux-standard` | 结构化 UX 验收 | evidence gate、P0 场景覆盖、P0/P1/P2 分级、P0/P1 修复闭环 | -| `formal-e2e` | `ux-formal` | 完整 `ux-walkthrough` | 完整 SOP、report.json、docx、closeout | +| `formal-e2e` | `ux-formal` | 完整 `ux-walkthrough` | 完整 SOP、report.json、docx | `standard-e2e` 不要声称已完成完整 `ux-walkthrough`;它是参考 `ux-walkthrough` 标准的结构化 UX 验收。`formal-e2e` 才执行完整 `ux-walkthrough`。 @@ -296,12 +302,12 @@ HiUI 修改 -> 工程 gate -> 页面截图 -> UX 复查 ## 统计收口 -分别按下游 skill 的规则处理: +仅对 `hiui-design` 页面交付链路处理 usage stats: - 标准 / 严格 HiUI 页面生成完成且可渲染后,按 `hiui-design` 的 usage stats 规则收口 -- 完整 `ux-formal` 报告和 docx 生成后,按 `ux-walkthrough` 的收口规则执行 +- `ux-walkthrough` 的完成定义到完整报告、`report.json`、标注校验与 docx 为止;不额外要求 usage stats 或 telemetry closeout -若任一统计返回 `requires_network_authorization` 或退出码 `21`,按对应 skill 的规则申请一次授权。 +若 `hiui-design` 统计返回 `requires_network_authorization` 或退出码 `21`,按其规则申请一次授权。 统计失败不阻断主任务交付,但不能静默吞掉入队或授权状态。 ## Bundle 控制面 diff --git a/skills/hiui-page-workflow/bundle/compatibility-matrix.json b/skills/hiui-page-workflow/bundle/compatibility-matrix.json index 3ab3d8571..89e411695 100644 --- a/skills/hiui-page-workflow/bundle/compatibility-matrix.json +++ b/skills/hiui-page-workflow/bundle/compatibility-matrix.json @@ -3,19 +3,21 @@ "bundleName": "hiui-page-workflow", "notes": [ "The public preview bundle resolves all skill sources from public GitHub clone URLs plus explicit ref/path entries.", - "Until upstream merge is complete, preview refs may point to upstream pull-request refs that carry the required skill manifests." + "Until upstream merge is complete, preview refs may point to upstream pull-request refs that carry the required skill manifests.", + "Workflow 1.1.0 expects the downstream hiui-design planner contract to expose requiredDocs[].readMode, requiredActions, formalAcceptanceActions, currentExecutionState, and canStartImplementation.", + "The hiui-design manifest is still 1.0.0 in this branch, so compatibility must be judged by planner contract availability, not by semver label alone." ], "testedBundles": [ { - "bundleVersion": "1.0.1", + "bundleVersion": "1.1.0", "channel": "canary", "skills": { - "hiui-page-workflow": "1.0.1", + "hiui-page-workflow": "1.1.0", "refine-product-requirements": "1.0.0", "hiui-design": "1.0.0", "ux-walkthrough": "1.0.1" }, - "status": "planned" + "status": "validated-local" } ] } diff --git a/skills/hiui-page-workflow/bundle/workflow-bundle.lock.json b/skills/hiui-page-workflow/bundle/workflow-bundle.lock.json index 52abf922b..16f5dea85 100644 --- a/skills/hiui-page-workflow/bundle/workflow-bundle.lock.json +++ b/skills/hiui-page-workflow/bundle/workflow-bundle.lock.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "bundleName": "hiui-page-workflow", - "bundleVersion": "1.0.1", + "bundleVersion": "1.1.0", "channel": "canary", "installPolicyRef": "install-policy.json", "compatibilityMatrixRef": "compatibility-matrix.json", @@ -20,7 +20,7 @@ "name": "hiui-page-workflow", "installName": "hiui-page-workflow", "required": true, - "version": "1.0.1", + "version": "1.1.0", "manifestPath": "skill.manifest.json", "source": { "kind": "git", diff --git a/skills/hiui-page-workflow/references/acceptance-matrix.md b/skills/hiui-page-workflow/references/acceptance-matrix.md index 868856143..b8fdd9e48 100644 --- a/skills/hiui-page-workflow/references/acceptance-matrix.md +++ b/skills/hiui-page-workflow/references/acceptance-matrix.md @@ -52,7 +52,7 @@ - 运行 `refine-product-requirements`,产出页面清单、全局生成上下文、页面级提示词和 HiUI 交接包 - `requirementGate` 通过后,必须展示生成输入确认块 - `generationInputGate.status = confirmed | assumption-authorized` 后才进入页面规划 -- `hiui-design` 的 `requiredCommands` +- 以 `hiui-design` 的 `requiredActions` 为主调度面;`requiredCommands` 只作兼容摘要 - 页面截图 - `ux-standard` 结构化 UX 验收 - UX evidence gate:目标页面可访问,截图 / 代码 / URL 证据可用 @@ -84,14 +84,14 @@ - 运行 `refine-product-requirements` 的 `full-prd-to-generation` 或等价完整交付,保留选项确认、假设、待确认和 HiUI 交接包 - `requirementGate` 与 `generationInputGate` 均必须通过;若是授权假设,最终报告必须高亮风险 -- `hiui-design` 的 `formalAcceptanceCommands` +- 以 `hiui-design` 的 `formalAcceptanceActions` 为主调度面;`formalAcceptanceCommands` 只作兼容摘要 - `ux-formal`:执行 `ux-walkthrough` 完整 SOP -- `precheck_walkthrough.py`、证据门禁、问题判断、report.json、docx、closeout 全部执行 +- `precheck_walkthrough.py`、证据门禁、问题判断、report.json、docx 全部执行 - P0 场景覆盖矩阵和 P0/P1 修复后复验 - 截图和标注图 - 修复前 / 修复后效果对比图,报告中需可查看原图路径 - 本地 docx 报告 -- 两套 usage stats 收口 +- `hiui-design` usage stats 收口;`ux-walkthrough` 以报告、`report.json`、标注校验与 docx 作为正式完成面 ## 升级规则 @@ -105,7 +105,7 @@ | 准备进入 | 必须满足 | |---|---| | 页面规划 | `generationInputGate.status = confirmed | assumption-authorized` | -| 文件修改 | 页面规划完成,`blockingReasons` 已处理,requiredDocs 已读取 | +| 文件修改 | 页面规划完成,且 `plan.status = ready`、`facts.status = ready`、`currentExecutionState.status = ready`、`canStartImplementation = true`,并且 required docs 已按 `readMode` 消费 | | `ux-smoke` | 页面可访问,有最终截图或无法截图原因 | | `ux-standard` | `uxGate.evidenceStatus = ready`,P0 场景已知或已说明缺口 | | `ux-formal` | 完整 `ux-walkthrough` SOP,docxRequired = true | diff --git a/skills/hiui-page-workflow/references/final-report-template.md b/skills/hiui-page-workflow/references/final-report-template.md index 176bcd061..14c933813 100644 --- a/skills/hiui-page-workflow/references/final-report-template.md +++ b/skills/hiui-page-workflow/references/final-report-template.md @@ -9,7 +9,7 @@ 门禁状态: - requirementGate: -- generationInputGate: +- generationInputGate: - uxGate:,证据状态 需求细化: @@ -22,6 +22,15 @@ 页面类型: Workflow level: +页面交付状态: +- planStatus: +- canStartImplementation: +- currentExecutionState:,当前动作 ,下一步 +- preflight: +- formal acceptance: +- hiui usage stats: +- blockingIssues:<无 | issue code 列表> + 工程验收: - :,<简短说明> - :,<简短说明> @@ -59,6 +68,9 @@ P0 场景覆盖矩阵: - 工程验收结果优先列失败和阻断项。 - 执行过需求细化时,必须说明交付模式、MVP 范围、页面清单摘要和待确认项。 - 进入页面生成前,必须说明 `generationInputGate` 是用户确认还是用户授权假设;不能只写“已确认”。 +- 若最终停在 S0 / 待确认阶段,必须使用 `generationInputGate=not-ready|ready-for-review|blocked`,不能把需求稿写成可直接生成。 +- 若已执行 `hiui-design` 页面规划,必须补一段最小页面交付状态:至少说明 `planStatus`、`currentExecutionState`、`preflight`、formal acceptance、hiui usage stats 和剩余 `blockingIssues`。 +- 若 `currentExecutionState.status != ready` 或 `canStartImplementation != true`,不能把页面描述成“已按计划进入实现”;必须明确说明当前仍停留在阻断恢复或计划排障阶段。 - 若需求阶段使用了推荐假设继续推进,最终报告必须把这些假设列为遗留风险或待确认项。 - `standard-e2e` 和 `formal-e2e` 必须说明 P0 场景覆盖结果;若未覆盖,不能声称完成结构化 UX 验收。 - UX 问题按 P0、P1、P2 排序。 diff --git a/skills/hiui-page-workflow/references/handoff-contract.md b/skills/hiui-page-workflow/references/handoff-contract.md index b1562a976..bd6ae7af0 100644 --- a/skills/hiui-page-workflow/references/handoff-contract.md +++ b/skills/hiui-page-workflow/references/handoff-contract.md @@ -44,6 +44,39 @@ "assumptionsAllowed": false, "confirmedSummary": "用户已确认首版生成供应商列表和供应商详情" }, + "planSnapshot": { + "sourceSkill": "hiui-design", + "planStatus": "ready | blocked | invalid | missing", + "factsStatus": "ready | blocked | missing | conflict | stale | unknown", + "canStartImplementation": false, + "currentExecutionState": { + "status": "ready | blocked | unknown", + "primaryAction": "generate-by-page-component-slot-fill | ResolveBlockingFacts | unknown", + "currentPhase": "ResolveBlockingFacts | SelectStartPoint | GenerateOrEdit | WriteContract | Preflight | FormalAcceptance | RuntimeGovernance | UsageStats | unknown", + "nextCommand": "下一条结构化动作命令", + "blockerCodes": [ + "PROJECT_INTEGRATION_BLOCKED" + ] + }, + "requiredActions": [ + { + "command": "typical-page:start-page", + "phase": "GenerateOrEdit", + "status": "pending | passed | failed | skipped | blocked" + } + ], + "formalAcceptanceActions": [ + { + "command": "typical-page:finalize-page", + "phase": "FormalAcceptance", + "status": "pending | passed | failed | skipped | blocked" + } + ], + "deliverySummaryProfile": "planner delivery summary profile", + "acceptanceProfile": { + "formalRequired": false + } + }, "requirementRefinement": { "sourceSkill": "refine-product-requirements", "deliveryMode": "quick-refine | hiui-handoff | full-prd-to-generation", @@ -128,6 +161,9 @@ - `route` 或 `devServerUrl` 不确定时,先从项目路由和 dev server 输出确认。 - `requirementGate` 用来判断产品方向是否清楚;用户回答过问题不等于 `generationInputGate` 已确认。 - `generationInputGate.status` 只有 `confirmed` 或 `assumption-authorized` 时,才允许进入页面规划和文件修改。 +- 若已执行 `hiui-design` 页面规划,`planSnapshot` 应保留最小 planner 快照;不要手工重造 `mode`、`pageType`、`startFrom` 或执行许可结论。 +- 进入文件修改前,若存在 `planSnapshot`,必须同时满足 `planStatus=ready`、`factsStatus=ready`、`currentExecutionState.status=ready`、`canStartImplementation=true`。 +- `planSnapshot.requiredActions` 与 `planSnapshot.formalAcceptanceActions` 只记录当前 workflow 真正消费的结构化动作及其结果;不要把未执行动作写成已通过。 - `assumption-authorized` 只能来自用户明确授权;不能把“生成 / 继续 / 开始吧 / 端到端”自动当作授权。 - 若执行过需求细化,`requirementRefinement` 必须记录真实的交付模式、关键假设和待确认项;没有执行需求细化时可省略。 - `hiuiHandoffReady` 为 false 时,不要假装需求已完备;若用户仍要求继续生成,必须把假设和风险带入 `knownRisks`。 @@ -144,5 +180,6 @@ ## 字段分层 - 必填:`workflowLevel`、`requirementGate`、`generationInputGate`、`engineeringChecks`、`knownRisks`。 +- 完成页面规划后建议必填:`planSnapshot`。 - 进入 UX 后必填:`uxGate`、`screenshots`;`ux-standard` / `ux-formal` 还必须填 `p0ScenarioCoverage`。 - 有修复时必填:`visualComparisons`,并保留 before / after 清晰原图路径。 diff --git a/skills/hiui-page-workflow/skill.manifest.json b/skills/hiui-page-workflow/skill.manifest.json index 920ce96aa..8442ee021 100644 --- a/skills/hiui-page-workflow/skill.manifest.json +++ b/skills/hiui-page-workflow/skill.manifest.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "name": "hiui-page-workflow", - "version": "1.0.1", + "version": "1.1.0", "distributionScope": "bundle-entry", "entry": "SKILL.md", "requiredPaths": [ diff --git a/skills/refine-product-requirements/SKILL.md b/skills/refine-product-requirements/SKILL.md index 59066c60b..9cd004e19 100644 --- a/skills/refine-product-requirements/SKILL.md +++ b/skills/refine-product-requirements/SKILL.md @@ -161,6 +161,8 @@ description: 将模糊或抽象的产品经理需求细化为可执行的产品 当下一步是页面生成、HiUI 生成、原型生成或 UX 验收时,需求确认和生成输入确认必须分开处理: +- `requirementGate.status` 统一使用:`needs-confirmation | requirements-confirmed | assumption-authorized | blocked` +- `generationInputGate.status` 统一使用:`not-ready | ready-for-review | confirmed | assumption-authorized | blocked` - `requirementGate`:确认产品目标、MVP、P0 场景、角色权限、核心规则和状态机。 - `generationInputGate`:确认页面清单、页面级提示词、HiUI 页型建议、路由、状态和验收标准。 @@ -393,7 +395,7 @@ description: 将模糊或抽象的产品经理需求细化为可执行的产品 - 共享全局上下文引用和组件/设计系统约束 - 会影响生成或验收的数据/mock 假设、权限假设和开放风险 - 推荐生成顺序,通常先生成 P0 列表/详情/编辑/核心流程页面 -- `requirementGate` 与 `generationInputGate` 的状态;若仍有缺口,必须标为 `assumption-authorized` 或 `blocked`,不能写成已确认。 +- `requirementGate` 与 `generationInputGate` 的结构化字段;若仍有缺口,必须标为 `ready-for-review`、`assumption-authorized` 或 `blocked`,不能写成已确认。 ## 交互模式 diff --git a/skills/refine-product-requirements/references/delivery-modes.md b/skills/refine-product-requirements/references/delivery-modes.md index d5aa5732e..ad5491b29 100644 --- a/skills/refine-product-requirements/references/delivery-modes.md +++ b/skills/refine-product-requirements/references/delivery-modes.md @@ -146,7 +146,11 @@ - HiUI 交接包 - 会影响生成/验收的假设和风险 -必须包含 `requirementGate`、`generationInputGate`、确认完整度、假设和风险。未确认时只允许 `ready-for-review`、`assumption-authorized` 或 `blocked`。 +必须包含 `requirementGate`、`generationInputGate`、确认完整度、假设和风险。 + +- `requirementGate.status` 只允许:`needs-confirmation | requirements-confirmed | assumption-authorized | blocked` +- `generationInputGate.status` 只允许:`not-ready | ready-for-review | confirmed | assumption-authorized | blocked` +- 未确认时,`generationInputGate.status` 只允许 `ready-for-review`、`assumption-authorized` 或 `blocked` 对于 B2B / 管理后台页面,若权限、异常、导入/批量操作、日志/审计中仍有高影响缺口,应保持 `ready-for-review` 或继续确认,不要假装已收口。 diff --git a/skills/refine-product-requirements/references/hiui-handoff-template.md b/skills/refine-product-requirements/references/hiui-handoff-template.md index 091ce4b28..dab01485c 100644 --- a/skills/refine-product-requirements/references/hiui-handoff-template.md +++ b/skills/refine-product-requirements/references/hiui-handoff-template.md @@ -9,6 +9,24 @@ "productName": "产品名称", "targetPlatform": "HiUI React admin", "workflowLevelSuggestion": "quick-preview | standard-e2e | formal-e2e", + "requirementGate": { + "status": "needs-confirmation | requirements-confirmed | assumption-authorized | blocked", + "resolvedQuestions": [ + "首版范围仅覆盖列表和详情" + ], + "remainingHighImpactQuestions": [ + "真实审批流是否进入 P0 待确认" + ], + "assumptions": [ + "首版按单角色运营管理员生成" + ] + }, + "generationInputGate": { + "status": "not-ready | ready-for-review | confirmed | assumption-authorized | blocked", + "confirmedPageIds": ["P01"], + "confirmedPromptIds": ["PR01"], + "confirmedSummary": "已确认先生成供应商列表页" + }, "globalContextRef": "全局生成上下文所在章节或文件", "sharedConstraints": [ "组件库/设计系统约束", @@ -56,5 +74,7 @@ - 用户只想要可运行预览时,建议 `quick-preview`。 - 用户要求生成并验证、UX 走查或修复闭环时,建议 `standard-e2e`。 - 用户提到提测、发布、合入、无 warning、source-gate、doctor、finalize-page、完整体验走查或 UX 走查报告时,建议 `formal-e2e`。 +- `requirementGate.status=needs-confirmation` 或 `generationInputGate.status=not-ready|ready-for-review` 时,只允许作为待确认交接包流转,不能直接进入页面生成。 +- 只有 `generationInputGate.status=confirmed|assumption-authorized`,下游 workflow 才能进入页面规划 / 文件修改。 - 管理后台页面优先选择典型 HiUI 页型。只有页面结构无法匹配已知模式时才使用 `non-typical`;事实不足时使用 `unresolved`。 - `generationOrder` 优先放 P0 列表、详情、编辑和核心流程页面;报表、高级设置、导入导出和审计页面默认后置,除非 P0 必须包含。 diff --git a/skills/refine-product-requirements/references/output-templates.md b/skills/refine-product-requirements/references/output-templates.md index 6f334c823..90e281a10 100644 --- a/skills/refine-product-requirements/references/output-templates.md +++ b/skills/refine-product-requirements/references/output-templates.md @@ -317,6 +317,18 @@ "productName": "", "targetPlatform": "HiUI React admin", "workflowLevelSuggestion": "quick-preview | standard-e2e | formal-e2e", + "requirementGate": { + "status": "needs-confirmation | requirements-confirmed | assumption-authorized | blocked", + "resolvedQuestions": [], + "remainingHighImpactQuestions": [], + "assumptions": [] + }, + "generationInputGate": { + "status": "not-ready | ready-for-review | confirmed | assumption-authorized | blocked", + "confirmedPageIds": [], + "confirmedPromptIds": [], + "confirmedSummary": "" + }, "globalContextRef": "全局生成上下文", "sharedConstraints": [], "pages": [ @@ -394,8 +406,10 @@ | 下游生成门禁状态合法 | 通过/待补/失败 | | ### 门禁结果 +- `requirementGate.status`:`needs-confirmation | requirements-confirmed | assumption-authorized | blocked` +- `generationInputGate.status`:`not-ready | ready-for-review | confirmed | assumption-authorized | blocked` - `continue-confirmation`:仍需继续确认,不可收口 - `ready-for-review`:可输出待确认版本,但不可直接进入下游生成 - `assumption-authorized`:用户已授权在当前假设下继续 -- `confirmed`:仅在所有高影响门禁均通过时使用 +- `confirmed`:仅对应 `generationInputGate.status=confirmed`,表示页面清单与提示词已被用户确认,可进入下游生成 ``` diff --git a/skills/ux-walkthrough/SKILL.md b/skills/ux-walkthrough/SKILL.md index d03ab6c59..b20ecb15b 100644 --- a/skills/ux-walkthrough/SKILL.md +++ b/skills/ux-walkthrough/SKILL.md @@ -24,6 +24,13 @@ python3 /scripts/<脚本>.py ... - 辅助:Nielsen、WCAG、品牌规范;冲突时以效率、理解成本、错误恢复、专业感为准 - 优先级:以 `references/severity-rubric.md` 为准,不用 checklist 预设 P 级代替 +## 交付边界 + +- 本 skill 的正式完成面只包括:完整 Markdown 报告、结构化 `report.json`、标注校验和本地 `.docx` +- 本 public skill 不定义 usage stats、telemetry closeout、网络补传或授权重试 after-hook +- 若上游 workflow 需要统计或发布态补传,必须由页面生成 / 发布 skill 自己定义和承担,不得默认追加到本 skill +- 生成 `.docx` 依赖 Python 包 `python-docx`;若环境缺失,该任务只能报告 `docx_generation` 失败,不能宣称完整完成 + ## 输入 → 模式 | 输入 | 模式 | 文档 | diff --git a/skills/ux-walkthrough/docs/onboarding/gates.md b/skills/ux-walkthrough/docs/onboarding/gates.md index b4ac57628..c6bf294e5 100644 --- a/skills/ux-walkthrough/docs/onboarding/gates.md +++ b/skills/ux-walkthrough/docs/onboarding/gates.md @@ -8,6 +8,8 @@ - 证据不足时先补证据,不提前输出完整报告 - 完成由报告正文、结构化 `report.json`、标注校验和 docx 结果共同判定 - 对外优先调用 `precheck_walkthrough.py` 与 `generate_docx.py`;底层脚本仅用于调试 +- 本文不定义 usage stats、telemetry closeout 或授权补传状态;这些不属于本 skill 的完成门禁 +- 若环境缺少 `python-docx`,`.docx` 生成必须判定为失败,并明确说明依赖缺失;不得把报告正文当作完整完成 --- diff --git a/skills/ux-walkthrough/references/report-json.md b/skills/ux-walkthrough/references/report-json.md index 5d6209d84..378c77111 100644 --- a/skills/ux-walkthrough/references/report-json.md +++ b/skills/ux-walkthrough/references/report-json.md @@ -8,6 +8,12 @@ 2. 再同步整理一份 `report.json` 3. 最后调用 `scripts/generate_docx.py` 生成本地 `.docx` +前置依赖: + +- 运行环境必须可导入 Python 包 `python-docx` +- 若缺少该依赖,`generate_docx.py` 会返回 `status=failed`、`failure_stage=docx_generation` +- 这种情况只能报告“docx 未生成成功”,不能把本次走查标记为完整完成 + 标准命令: ```bash