@microi.net/cli 6.1.7 → 6.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.codebuddy-plugin/marketplace.json +2 -2
  2. package/.codebuddy-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.mcp.json +1 -1
  5. package/.workbuddy-plugin/marketplace.json +2 -2
  6. package/.workbuddy-plugin/plugin.json +1 -1
  7. package/assets/build-meta.json +4 -4
  8. package/cordis.patch.yml +1 -1
  9. package/package.json +1 -1
  10. package/scripts/codex-marketplace.json +20 -20
  11. package/scripts/mcp-codex-stdio-adapter.js +189 -189
  12. package/scripts/mcp-server.js +101 -96
  13. package/scripts/mcp-trae-windows-launcher.cmd +21 -21
  14. package/scripts/microi-cli-mcp.js +7 -7
  15. package/scripts/microi-skills.meta.json +257 -239
  16. package/skills/.microi-skills-version.json +2 -2
  17. package/skills/.progressive-disclosure-manifest.json +90 -68
  18. package/skills/ai-engine/SKILL.md +303 -293
  19. package/skills/ai-engine/references/ai-employees.md +4 -0
  20. package/skills/app-store/SKILL.md +3 -1
  21. package/skills/job-engine/SKILL.md +12 -0
  22. package/skills/message-notification/SKILL.md +2 -0
  23. package/skills/microi-client-frontend/SKILL.md +260 -252
  24. package/skills/microi-codex/SKILL.md +8 -0
  25. package/skills/microi-datasource-mapping/SKILL.md +4 -0
  26. package/skills/microi-deployment/SKILL.md +85 -5
  27. package/skills/microi-deployment/references/deployment-matrix.md +27 -0
  28. package/skills/microi-deployment/references/offline-installer.md +64 -0
  29. package/skills/microi-docs-coverage/references/capability-map.md +3 -3
  30. package/skills/microi-form-engine/SKILL.md +13 -0
  31. package/skills/microi-form-engine/references/component-catalog.md +1 -1
  32. package/skills/microi-frontend-sdk/SKILL.md +12 -0
  33. package/skills/microi-microservice/SKILL.md +18 -1
  34. package/skills/microi-system-delivery/SKILL.md +8 -6
  35. package/skills/microi-system-delivery/references/delivery-pitfalls.md +50 -0
  36. package/skills/microi-system-delivery/references/progressive-01-/346/240/207/345/207/206/345/267/245/344/275/234/346/265/201.md +11 -9
  37. package/skills/microi-uniapp-frontend/SKILL.md +18 -3
  38. package/skills/microi.v8.js +445 -0
  39. package/skills/module-engine/SKILL.md +4 -5
  40. package/skills/page-engine/SKILL.md +233 -215
  41. package/skills/print-engine/SKILL.md +5 -0
  42. package/skills/report-engine/SKILL.md +9 -0
  43. package/skills/report-engine/scripts/check-demo-coverage.mjs +88 -0
  44. package/skills/report-engine/scripts/check-demo-coverage.test.mjs +52 -0
  45. package/skills/scripts/sync-embedded-skills.mjs +4 -1
  46. package/skills/scripts/sync-public-resource-router.mjs +29 -0
  47. package/skills/system-observability/SKILL.md +18 -2
  48. package/skills/ui-design/SKILL.md +1 -1
  49. package/skills/uniapp-mall-assets/SKILL.md +5 -0
  50. package/skills/v8-file-upload/SKILL.md +32 -31
  51. package/skills/v8-file-upload/references/hotspot-offload-operations.md +118 -0
  52. package/skills/v8-saas-multi-tenant/SKILL.md +17 -4
  53. package/skills/v8-security/SKILL.md +20 -5
  54. package/skills/vision-engine/SKILL.md +114 -3
  55. package/skills/vision-engine/references/architecture-and-acceptance.md +8 -0
  56. package/skills/workspace-conventions/SKILL.md +9 -3
  57. package/skills/workspace-conventions/references/progressive-01-/347/211/210/346/234/254/346/233/264/346/226/260/346/227/245/345/277/227/344/277/235/346/212/244/350/247/204/345/210/231-/345/274/272/345/210/266.md +4 -4
  58. package/skills/workspace-conventions/references/progressive-04-/345/267/245/344/275/234/345/214/272/344/270/216/345/271/263/345/217/260/344/270/223/351/241/271/344/277/235/351/232/234.md +22 -1
@@ -1,226 +1,244 @@
1
- ---
2
- name: page-engine
3
- description: 生成和审查 Microi 界面引擎 Page Engine 页面 JSON。用于创建仪表盘、图表、表格、地图、组件、页面布局,或校验 mic_page formData JSON。
4
- ---
5
-
6
- > **Microi吾码基础规范(强制):** 任何 AI 模型与宿主每次新建或接续吾码任务,先完整读取 `../workspace-conventions/SKILL.md`,必须执行版本播报、`@microi.net/cli` 后台自动升级、Skills/MCP 同步和进度播报。安装与诊断读取 `../microi-codex-installer/SKILL.md`;更新失败延后重试,不阻断当前工作。
7
-
8
- # Microi 界面引擎(Page Engine)页面 JSON 生成
9
-
10
- MCP 的生成/保存入口包含 `microi_build_page_design` 与 `microi_save_page_design`;版本治理入口见下文。保存工具写入后仍需回读 `mic_page`,不能把生成成功当作持久化成功。
11
-
12
- ## 完整 AI 开发默认报表(强制)
13
-
14
- 完整系统开发必须按需求主动设计基于界面引擎的业务统计、老板经营与领导管理报表,
15
- 先把相应入口纳入菜单骨架,再接通真实数据并验收。详细范围与菜单切换流程读取
16
- [菜单先行与经营报表增补要求](../microi-system-delivery/references/progressive-01-标准工作流.md#菜单先行与经营报表增补要求强制)。
17
- 局部修改只覆盖相关页面/指标,只读分析不创建页面。用户明确的范围优先。
18
-
19
- - 每个报表服务明确角色与决策任务;业务运营覆盖状态、趋势、效率与异常,经营视角覆盖
20
- 有真实来源的规模、收支/利润、回款及风险,管理视角覆盖部门/人员、任务进展、待办/逾期、
21
- 利用与达成。不存在数据来源的指标标为不适用并说明,不能编造成本、利润或业绩。
22
- - 每个指标定义来源、公式、单位/精度、状态、时间字段/时区/边界、比较基期、维度与角色。
23
- 当前租户真实数据通过服务端批量聚合,时间/维度筛选和用户数据权限必须在接口中生效。
24
- 禁止演示值、本页合计冒充全量或前端拉取全量明细再统计。
25
- - 页面包含周期/维度筛选、加载、零/空、无权限和失败状态;请求失败不能显示为正常零值。
26
- 指标/图表穿透到同一口径的授权明细并对账。界面引擎负责看板和图表,分页查询/导出明细
27
- 配合 `report-engine`;列表汇总、Hero 和角标不能替代决策报表。
28
- - 保存后回读 `mic_page`、菜单绑定、数据源/接口;以管理员、经营/管理角色、普通与无权限
29
- 角色验证时间边界、筛选、隔离、零/空/失败和明细对账,完成电脑/移动、明暗主题截图。
30
- 先从已授权候选入口独立预验收,再以回读后的真实页面 Id 切换同一菜单并按打开方式设置标记,
31
- 保留菜单身份与权限;从原 URL 最终验收,失败精确恢复本菜单原绑定并记录未完成。
32
-
33
- ## 紧凑报表与平台主题
34
-
35
- 需要“主指标 + 多行次指标 + 图表 + 排行表 + 资讯”布局时,可读取 [examples/compact-dashboard.json](examples/compact-dashboard.json) 作为原生组件示例。示例数值仅用于视觉排版,交付业务报表前必须替换为当前租户的真实数据源。
36
-
37
- 默认使用 `formConfig.themeMode="system"`、`density="compact"`,卡片背景使用 `var(--el-bg-color)`、页面背景使用 `var(--el-bg-color-page)`,正文使用平台文字变量。不要因为“驾驶舱/大屏”字样就强制深色。显式指定浅色或深色时才使用 `light/dark`。
38
-
39
- 运行容器默认 `wrapperOption.heightMode="content"`,`height` 只保留设计器布局尺寸。仅需固定图形画布时设为 `fixed`;图表自身可以设置 240–320px 高度。不要为一行指标、标题、三行表格预留 200–400px 空容器。紧凑布局的卡片间距建议 10–16px,面板内边距 8–16px;统计组件 `widgetParams[24].value` 支持 `summary`(图标和大数字)、`detail`(多行小指标)及兼容的 `cards`。
40
-
41
- 用户提供参考截图时,先拆出面板行列比例、指标主次、图表与表格高度、标题/筛选位置,再用原生 pannel/statistic/chart/tabel/list 组合配置。不能把所有指标替换成一排大色块,也不能用模板假数据声称已接通客户业务。必须用同一视口截图检查浅色和平台深色,检查页面缩放后的断行与纵向空白;布局接近度和真实数据绑定分别验收。
42
-
43
- ### 平台智能工作首页
44
-
45
- 保留 `aiengine` 原生 AI 创作中心,与 `homeoverview` 工作概览组成桌面首行 14/10 栅格;任务、月历与公告放在下一行,移动端自动切为 24 栅格。不能把任务的完整 15 行列表或未配置日程的大空白块直接堆在首屏。
46
-
47
- - `homeoverview.widgetParams[0]` 为真实个人统计接口 `platform-home-overview`,`[1].value="compact"` 启用紧凑指标、常用应用与趋势;所有数据仍受当前用户权限过滤。
48
- - `workcenter.widgetParams` 按索引依次为视图、工作菜单、流程菜单、公告菜单、日历菜单、分页条数;`[5].value=5` 用于首页任务,范围 5–20,不能影响独立“我的工作”页面。
49
- - `diycalendar.widgetParams[0].value` 应绑定当前账号可访问、关联 `microi_calendar` 的真实模块 Id;`[1].value="compact"` 只压缩月历密度,仍须支持点击日期新增及事件编辑、删除。备注字段为 `Remark`。隐藏菜单也可以是授权入口,旧模块可能没有 `DiyTableName`,此时必须显式绑定实际模块,不把只读日期回退当作日程验收成功。无模块时展示本月日期并明确提示未配置,不伪造任务。
50
- - `diytable.widgetParams[4].value="NoticeList"` 以只读公告列表展示真实授权公告;表、菜单仍在 `[0]/[1]`,模块缺失时使用 `referencePolicy.onMissing="RemoveWidget"` 移除可选组件及空容器。
51
- - 修改已有首页前通过 MCP 读取页面及历史 `CurrentHash`,保存传 `expectedCurrentHash`,再回读 JSON 和历史,保留可恢复版本。
52
-
53
- 你正在为 Microi 吾码平台生成界面引擎页面的 JSON 数据。界面引擎页面由 `formData` 对象描述,用户导入 JSON 即可使用。
54
-
1
+ ---
2
+ name: page-engine
3
+ description: 生成和审查 Microi 界面引擎 Page Engine 页面 JSON。用于创建仪表盘、图表、表格、地图、组件、页面布局,或校验 mic_page formData JSON。
4
+ ---
5
+
6
+ > **Microi吾码基础规范(强制):** 任何 AI 模型与宿主每次新建或接续吾码任务,先完整读取 `../workspace-conventions/SKILL.md`,必须执行版本播报、`@microi.net/cli` 后台自动升级、Skills/MCP 同步和进度播报。安装与诊断读取 `../microi-codex-installer/SKILL.md`;更新失败延后重试,不阻断当前工作。
7
+
8
+ # Microi 界面引擎(Page Engine)页面 JSON 生成
9
+
10
+ MCP 的生成/保存入口包含 `microi_build_page_design` 与 `microi_save_page_design`;版本治理入口见下文。保存工具写入后仍需回读 `mic_page`,不能把生成成功当作持久化成功。
11
+
12
+ ## 完整 AI 开发默认报表(强制)
13
+
14
+ 完整系统开发必须按需求主动设计业务统计、老板经营与领导管理报表,按微服务优先、原生能力满足时选界面引擎、HTML兼容最后的三方式规则选择展示技术,
15
+ 先把相应入口纳入菜单骨架,再接通真实数据并验收。详细范围与菜单切换流程读取
16
+ [菜单先行与经营报表增补要求](../microi-system-delivery/references/progressive-01-标准工作流.md#菜单先行与经营报表增补要求强制)。
17
+ 局部修改只覆盖相关页面/指标,只读分析不创建页面。用户明确的范围优先。
18
+
19
+ - 每个报表服务明确角色与决策任务;业务运营覆盖状态、趋势、效率与异常,经营视角覆盖
20
+ 有真实来源的规模、收支/利润、回款及风险,管理视角覆盖部门/人员、任务进展、待办/逾期、
21
+ 利用与达成。不存在数据来源的指标标为不适用并说明,不能编造成本、利润或业绩。
22
+ - 每个指标定义来源、公式、单位/精度、状态、时间字段/时区/边界、比较基期、维度与角色。
23
+ 当前租户真实数据通过服务端批量聚合,时间/维度筛选和用户数据权限必须在接口中生效。
24
+ 禁止演示值、本页合计冒充全量或前端拉取全量明细再统计。
25
+ - 页面包含周期/维度筛选、加载、零/空、无权限和失败状态;请求失败不能显示为正常零值。
26
+ 指标/图表穿透到同一口径的授权明细并对账。界面引擎负责看板和图表,分页查询/导出明细
27
+ 配合 `report-engine`;列表汇总、Hero 和角标不能替代决策报表。
28
+ - 保存后回读 `mic_page`、菜单绑定、数据源/接口;以管理员、经营/管理角色、普通与无权限
29
+ 角色验证时间边界、筛选、隔离、零/空/失败和明细对账,完成电脑/移动、明暗主题截图。
30
+ 页面可运行后立即以回读后的真实页面 Id 绑定原菜单供预览,保留菜单身份、URL 与权限,
31
+ 从原入口持续验收;候选入口不能替代原菜单。白屏/不可达/越权时恢复最近可运行且授权正确的绑定,
32
+ 确无可用版本才恢复占位。可预览与验收通过分别记录。
33
+ - 报表默认周期的模拟数据覆盖与独立对账按 `../report-engine/SKILL.md` 执行;每个组件的日期控件、
34
+ 请求参数、服务端区间和页面结果均需核对。不得只验证静态 JSON 或历史月份有数据。
35
+
36
+ ## 紧凑报表与平台主题
37
+
38
+ 需要“主指标 + 多行次指标 + 图表 + 排行表 + 资讯”布局时,可读取 [examples/compact-dashboard.json](examples/compact-dashboard.json) 作为原生组件示例。示例数值仅用于视觉排版,交付业务报表前必须替换为当前租户的真实数据源。
39
+
40
+ 默认使用 `formConfig.themeMode="system"`、`density="compact"`,卡片背景使用 `var(--el-bg-color)`、页面背景使用 `var(--el-bg-color-page)`,正文使用平台文字变量。不要因为“驾驶舱/大屏”字样就强制深色。显式指定浅色或深色时才使用 `light/dark`。
41
+
42
+ 运行容器默认 `wrapperOption.heightMode="content"`,`height` 只保留设计器布局尺寸。仅需固定图形画布时设为 `fixed`;图表自身可以设置 240–320px 高度。不要为一行指标、标题、三行表格预留 200–400px 空容器。紧凑布局的卡片间距建议 10–16px,面板内边距 8–16px;统计组件 `widgetParams[24].value` 支持 `summary`(图标和大数字)、`detail`(多行小指标)及兼容的 `cards`。
43
+
44
+ 用户提供参考截图时,先拆出面板行列比例、指标主次、图表与表格高度、标题/筛选位置,再用原生 pannel/statistic/chart/tabel/list 组合配置。不能把所有指标替换成一排大色块,也不能用模板假数据声称已接通客户业务。必须用同一视口截图检查浅色和平台深色,检查页面缩放后的断行与纵向空白;布局接近度和真实数据绑定分别验收。
45
+
46
+ ### 平台智能工作首页
47
+
48
+ 保留 `aiengine` 原生 AI 创作中心,与 `homeoverview` 工作概览组成桌面首行 14/10 栅格;任务、月历与公告放在下一行,移动端自动切为 24 栅格。不能把任务的完整 15 行列表或未配置日程的大空白块直接堆在首屏。
49
+
50
+ - `homeoverview.widgetParams[0]` 为真实个人统计接口 `platform-home-overview`,`[1].value="compact"` 启用紧凑指标、常用应用与趋势;所有数据仍受当前用户权限过滤。
51
+ - `workcenter.widgetParams` 按索引依次为视图、工作菜单、流程菜单、公告菜单、日历菜单、分页条数;`[5].value=5` 用于首页任务,范围 5–20,不能影响独立“我的工作”页面。
52
+ - `diycalendar.widgetParams[0].value` 应绑定当前账号可访问、关联 `microi_calendar` 的真实模块 Id;`[1].value="compact"` 只压缩月历密度,仍须支持点击日期新增及事件编辑、删除。备注字段为 `Remark`。隐藏菜单也可以是授权入口,旧模块可能没有 `DiyTableName`,此时必须显式绑定实际模块,不把只读日期回退当作日程验收成功。无模块时展示本月日期并明确提示未配置,不伪造任务。
53
+ - `diytable.widgetParams[4].value="NoticeList"` 以只读公告列表展示真实授权公告;表、菜单仍在 `[0]/[1]`,模块缺失时使用 `referencePolicy.onMissing="RemoveWidget"` 移除可选组件及空容器。
54
+ - 修改已有首页前通过 MCP 读取页面及历史 `CurrentHash`,保存传 `expectedCurrentHash`,再回读 JSON 和历史,保留可恢复版本。
55
+
56
+ 你正在为 Microi 吾码平台生成界面引擎页面的 JSON 数据。界面引擎页面由 `formData` 对象描述,用户导入 JSON 即可使用。
57
+
55
58
  <!-- microi-progressive:begin -->
56
- <!-- microi-progressive:chunk id=page-engine-000 sha256=44233416e71cc6ff3712347308dadd26e55b14938b81b8a2401478c6bc14921b -->
57
- ## 设计器源码事件
58
-
59
- 设计器选中事件只能在 `isDesignMode` 为真时执行 `preventDefault/stopPropagation` 和选中状态写入。运行和预览页面必须保留 HTML 链接、按钮与键盘激活的浏览器默认行为。链接使用实际授权菜单路由,保存后逐一点击验证,不以 JSON 中存在 href 代替跳转验收;回归入口为 `Microi.Client/tests/page-engine-runtime-click.spec.mjs`。
60
-
61
- 每个顶层运行路由必须创建并 provide 独立的 `PAGE_ENGINE_STORE_KEY`,其子组件通过 `usePageEngineStore` 使用当前渲染树的状态。不能让多个 KeepAlive 页面共用应用级 `formData`,否则迟到响应和页面激活会导致错页;验收需覆盖地图与看板往返、迟到响应、移动布局和主题隔离。
62
-
63
- 只有在扩展界面引擎设计器源码时才使用全局事件总线。用
64
- `EventBus.on(eventName, handler)` 监听保存、日期选择或组件跳转等事件,并在
65
- 组件卸载时逐项调用 `EventBus.off(eventName)`。重复挂载不解绑会造成一次操作
66
- 触发多次;普通页面 JSON 生成不需要注册事件总线。
67
-
59
+ <!-- microi-progressive:chunk id=page-engine-000 sha256=c13482f5007ea2aa645d659c95566e0ce9740608d4a615eb494e64fedc8c5108 -->
60
+ ## 设计器源码事件
61
+
62
+ 设计器选中事件只能在 `isDesignMode` 为真时执行 `preventDefault/stopPropagation` 和选中状态写入。运行和预览页面必须保留 HTML 链接、按钮与键盘激活的浏览器默认行为。链接使用实际授权菜单路由,保存后逐一点击验证,不以 JSON 中存在 href 代替跳转验收;回归入口为 `Microi.Client/tests/page-engine-runtime-click.spec.mjs`。
63
+
64
+ 每个顶层运行路由必须创建并 provide 独立的 `PAGE_ENGINE_STORE_KEY`,其子组件通过 `usePageEngineStore` 使用当前渲染树的状态。不能让多个 KeepAlive 页面共用应用级 `formData`,否则迟到响应和页面激活会导致错页;验收需覆盖地图与看板往返、迟到响应、移动布局和主题隔离。
65
+
66
+ 只有在扩展界面引擎设计器源码时才使用全局事件总线。用
67
+ `EventBus.on(eventName, handler)` 监听保存、日期选择或组件跳转等事件,并在
68
+ 组件卸载时逐项调用 `EventBus.off(eventName)`。重复挂载不解绑会造成一次操作
69
+ 触发多次;普通页面 JSON 生成不需要注册事件总线。
70
+
68
71
  <!-- /microi-progressive:chunk -->
69
- <!-- microi-progressive:chunk id=page-engine-001 sha256=57717991f9c7de557b524ec36fc9bdd716080b71b682439f1b6662b0b9b2f45c -->
70
- ## 核心数据结构
71
-
72
- ```
73
- formData(页面)
74
- ├── Id: string // 页面唯一ID(GUID)
75
- ├── Title: string // 页面标题
76
- ├── Number: string // 页面编号(如 PAGE1)
77
- ├── Desc: string // 页面描述
78
- └── JsonObj
79
- ├── formConfig // 页面全局配置
80
- └── wrapperList[] // 容器列表
81
- ├── wrapperOption // 容器配置
82
- └── widgetList[] // 组件列表
83
- ├── widgetOption // 组件通用配置
84
- └── widgetParams[] // 组件私有参数
85
- ```
86
-
72
+ <!-- microi-progressive:chunk id=page-engine-001 sha256=9749827d05043d169e0755e03de437b67eed6c6468a907942146a892176297dc -->
73
+ ## 核心数据结构
74
+
75
+ ### 原生视觉场景 scene
76
+
77
+ 需要精确还原渐变、透明重叠、玻璃材质、嵌套 Grid/Flex、悬停和鼠标高光时,可选原生 `scene`;复杂业务交互优先微服务。接口引擎返回 HTML 作为第三种兼容/分享选择。不能以 `browser/iframe/html` 包住完整看板后声称已交付原生界面引擎。选型需核对目标 PC 是否包含该组件,并以同视口截图验证,不承诺任意参考图自动一模一样。
78
+
79
+ - `widgetParams[0]` 仍为 JSON 数据来源,`typeOptions.dataJson` 为静态数据或初始数据;`widgetParams[1]={sort:1,type:"scene",value:节点树}` 保存布局。远端只替换数据域,布局不随请求覆盖。
80
+ - 原生节点:`block/grid/flex/text/icon/image/link/button/badge/metric/progress/chart/table/sparkline/divider`。`children` 嵌套最多 24 层;`repeat:"projects"` 按数组重复,子节点绑定 `$item/$index`;`text/value/option/style` 的 `{{path}}` 只读取自有数据路径,不执行表达式。`chart.option` 为 ECharts 声明或整个对象绑定。
81
+ - 重复节点应声明 `repeatKey:"Id"` 等条目路径(也支持 `$item.Id`);未声明时依次读取条目 `Id/id/key`,均无稳定身份才回退索引。VNode Key 同时包含设计节点身份和条目身份,重复数据键以发生序号消歧;不能让每个实例直接复用同一个设计节点 `id`。刷新与重排必须用真实 Vue renderer 验证原 DOM/组件身份随条目移动。
82
+ - `style` 支持渐变、布局、字体、边界、透明、玻璃与几何;`darkStyle/tabletStyle/mobileStyle` 覆盖主题和 <1100/<768 宽度,`responsive:[{maxWidth,style}]` 配置其它断点。断点依据场景实际容器宽度,不依据保存时设备。`surface:"card/glass/soft/transparent"`、`hover:"lift"`、`pointer:"glow"`、`enter:"rise"` 为通用效果;`motion:"scan/drift/marquee/pulse"` 使用有限 CSS 动效,必须保留静态降级。
83
+ - 场景根 `responsiveReference:"content"`(默认)使用组件内容宽度;需要与 iframe 原页面的 CSS media width 对齐时选 `scrollport`,以最近实际发生纵向滚动的 `overflow-y:auto/scroll` 祖先的 border-box 宽度判断断点,跳过只声明 auto 却随内容撑开的包装层,并在数据加载和尺寸变化后重新选择滚动口。实际节点仍按 100% 内容宽布局,不引入内层滚动、不把断点从 1024 随意改小;回归覆盖内容宽 1021/滚动口宽 1029 的滚动条临界情形。
84
+ - `image.src/link.href` 只接受安全 HTTP(S) 或站内路由,无凭据 URL;样式禁止 `url()`、脚本、HTML。`button.action` 只接受 `refresh/fullscreen`,普通业务动作不要自行扩成可执行字符串。图表提示使用画布富文本,不把业务值解释为 HTML。
85
+ - 需要与参考几何逐项对应时可选 `chart.renderer="vector"`,`option.type="donut/radar/line/hbars/vbars"`,输入 `data` 或 `xAxis/series`,以及可选 `valueScale/round/nameMap/centerLabel/legendA`。原生 SVG 全部由 Vue h 原语构建,不接收 SVG 正文;图标可用白名单 `shapes:[{type:"path",attrs:{d:"..."}}]`。表格列支持 `kind:"badge/code"` 和声明式 `cellStyle`;`dock:"content-bottom"` 限定吸底条在当前场景内容矩形内,不覆盖宿主导航。默认 ECharts 与矢量皮肤分别验收,不能仅凭数据相同声称视觉一致。
86
+ - 场景自己承担表面和留白时设 `wrapperOption.surface="transparent"`,`widgetOption.height=0`,沿用内容高度;旧 pannel/tabs 和既有组件保持兼容。设计器“原生视觉场景”设置器提供节点树、布局/材质/高光、移动覆盖样式与完整节点 JSON。
87
+ - MCP `microi_build_page_design` 的 `scene/dataApi` 参数可以构建完整外壳,`microi_validate_page_design` 校验节点与安全边界,保存仍使用 `microi_save_page_design` 及 CAS。保存成功不代表目标 PC 已升级;旧 PC 必须保留现有 `html` 短升级提示组件并设置 `widgetOption.fallbackFor:["scene"]`,新版能力存在时才隐藏提示。短升级提示不能承载看板正文。
88
+ - 数据失败显式显示错误并保留旧数据,零值/缺失值/失败分开;基于真实 DTO 验证指标、图表、明细和刷新。截图验收同视口对比原参考、桌面/手机、浅/暗主题、键盘、减少动态、隐藏恢复和网络失败。平台回归入口:`Microi.Client/tests/page-engine-scene.spec.mjs`;MCP 协议回归:`microi.mcp/src/page-scene.test.ts`。
89
+
90
+ ```
91
+ formData(页面)
92
+ ├── Id: string // 页面唯一ID(GUID)
93
+ ├── Title: string // 页面标题
94
+ ├── Number: string // 页面编号(如 PAGE1)
95
+ ├── Desc: string // 页面描述
96
+ └── JsonObj
97
+ ├── formConfig // 页面全局配置
98
+ └── wrapperList[] // 容器列表
99
+ ├── wrapperOption // 容器配置
100
+ └── widgetList[] // 组件列表
101
+ ├── widgetOption // 组件通用配置
102
+ └── widgetParams[] // 组件私有参数
103
+ ```
104
+
87
105
  <!-- /microi-progressive:chunk -->
88
- <!-- microi-progressive:chunk id=page-engine-002 sha256=6a1a85b40bcb5f8be694b0aad73a805083acd0d6fcd9cba36278dc9b51d24dcb -->
89
- ## formConfig 页面全局配置
90
-
91
- ```json
92
- {
93
- "gutter": 0, "mask": true, "drag": true, "left": true,
94
- "hover": true, "shadow": true, "link": false, "watermark": false,
95
- "mobile": false, "dark": false, "autoRefresh": 0, "lastRefreshTime": "",
96
- "watermarkStyle": {
97
- "content": "Microi吾码",
98
- "font": { "fontSize": 16, "color": "rgba(255, 0, 0, 0.15)" },
99
- "rotate": -22
100
- },
101
- "dynamicStyle": { "padding": "4px", "backgroundColor": "", "opacity": 1 }
102
- }
103
- ```
104
-
106
+ <!-- microi-progressive:chunk id=page-engine-002 sha256=227ecf87086231eee28a9d91fde6a178a437e9196dd8f2ef1d14d53e5c5858df -->
107
+ ## formConfig 页面全局配置
108
+
109
+ ```json
110
+ {
111
+ "gutter": 0, "mask": true, "drag": true, "left": true,
112
+ "hover": true, "shadow": true, "link": false, "watermark": false,
113
+ "mobile": false, "dark": false, "autoRefresh": 0, "lastRefreshTime": "",
114
+ "watermarkStyle": {
115
+ "content": "Microi吾码",
116
+ "font": { "fontSize": 16, "color": "rgba(255, 0, 0, 0.15)" },
117
+ "rotate": -22
118
+ },
119
+ "dynamicStyle": { "padding": "4px", "backgroundColor": "", "opacity": 1 }
120
+ }
121
+ ```
122
+
105
123
  <!-- /microi-progressive:chunk -->
106
- <!-- microi-progressive:chunk id=page-engine-003 sha256=8afb75b0e4d85ab9f56a7ba018e1cc50f758d3e604683d8c38d5f0a45d53f135 -->
107
- ## 容器类型
108
-
109
- | type | label | 说明 |
110
- |------|-------|------|
111
- | `pannel` | 卡片 | 标准容器,包含一个 `widgetList` |
112
- | `tabs` | 选项卡 | 多标签页容器,使用 `tabWidgetMap` 存储每个 tab 的组件 |
113
-
114
- ### 卡片容器(pannel)关键字段
115
-
116
- ```json
117
- {
118
- "type": "pannel",
119
- "label": "卡片",
120
- "hidden": false,
121
- "wrapperOption": {
122
- "number": 10001, // 随机5位整数,页面内唯一
123
- "span": 12, // 栅格宽度(1-24,24=满宽)
124
- "height": 300, // 容器高度(px)
125
- "margin": "0px 10px 10px 0px",
126
- "dynamicStyle": { "padding": "10px", "backgroundColor": "" },
127
- "titleOption": {
128
- "hidden": true, // true=隐藏标题
129
- "title": "未命名",
130
- "dynamicStyle": { "textAlign": "left", "padding": "0px", "height": "20px", "lineHeight": "20px", "fontSize": "14px", "color": "" },
131
- "moreOption": { "hidden": true, "icon": "More", "iconShow": false, "text": "更多", "linkurl": "/", "linktype": "router", "refresh": "0", "datetime": "0", "autotime": false, "autotimeval": 1, "dynamicStyle": { "color": "", "fontSize": "12px" } }
132
- }
133
- },
134
- "widgetList": []
135
- }
136
- ```
137
-
138
- ### 选项卡容器(tabs)
139
-
140
- 组件放在 `tabWidgetMap[tabKey][]` 中,**不**放在 `widgetList` 中。
141
-
142
- ```json
143
- {
144
- "type": "tabs",
145
- "wrapperOption": {
146
- "number": 10002, "span": 24, "height": 400,
147
- "tabType": "", // '' | 'card' | 'border-card'
148
- "tabPosition": "top", // 'top' | 'right' | 'bottom' | 'left'
149
- "tabs": [
150
- { "key": "tab_1", "label": "标签页1" },
151
- { "key": "tab_2", "label": "标签页2" }
152
- ],
153
- "activeTab": "tab_1"
154
- },
155
- "tabWidgetMap": { "tab_1": [], "tab_2": [] },
156
- "widgetList": []
157
- }
158
- ```
159
-
124
+ <!-- microi-progressive:chunk id=page-engine-003 sha256=7bb7cb860f9330002f60748e93de3c639b157cddfd77b63ac8b19f0535306e65 -->
125
+ ## 容器类型
126
+
127
+ | type | label | 说明 |
128
+ |------|-------|------|
129
+ | `pannel` | 卡片 | 标准容器,包含一个 `widgetList` |
130
+ | `tabs` | 选项卡 | 多标签页容器,使用 `tabWidgetMap` 存储每个 tab 的组件 |
131
+
132
+ ### 卡片容器(pannel)关键字段
133
+
134
+ ```json
135
+ {
136
+ "type": "pannel",
137
+ "label": "卡片",
138
+ "hidden": false,
139
+ "wrapperOption": {
140
+ "number": 10001, // 随机5位整数,页面内唯一
141
+ "span": 12, // 栅格宽度(1-24,24=满宽)
142
+ "height": 300, // 容器高度(px)
143
+ "margin": "0px 10px 10px 0px",
144
+ "dynamicStyle": { "padding": "10px", "backgroundColor": "" },
145
+ "titleOption": {
146
+ "hidden": true, // true=隐藏标题
147
+ "title": "未命名",
148
+ "dynamicStyle": { "textAlign": "left", "padding": "0px", "height": "20px", "lineHeight": "20px", "fontSize": "14px", "color": "" },
149
+ "moreOption": { "hidden": true, "icon": "More", "iconShow": false, "text": "更多", "linkurl": "/", "linktype": "router", "refresh": "0", "datetime": "0", "autotime": false, "autotimeval": 1, "dynamicStyle": { "color": "", "fontSize": "12px" } }
150
+ }
151
+ },
152
+ "widgetList": []
153
+ }
154
+ ```
155
+
156
+ ### 选项卡容器(tabs)
157
+
158
+ 组件放在 `tabWidgetMap[tabKey][]` 中,**不**放在 `widgetList` 中。
159
+
160
+ ```json
161
+ {
162
+ "type": "tabs",
163
+ "wrapperOption": {
164
+ "number": 10002, "span": 24, "height": 400,
165
+ "tabType": "", // '' | 'card' | 'border-card'
166
+ "tabPosition": "top", // 'top' | 'right' | 'bottom' | 'left'
167
+ "tabs": [
168
+ { "key": "tab_1", "label": "标签页1" },
169
+ { "key": "tab_2", "label": "标签页2" }
170
+ ],
171
+ "activeTab": "tab_1"
172
+ },
173
+ "tabWidgetMap": { "tab_1": [], "tab_2": [] },
174
+ "widgetList": []
175
+ }
176
+ ```
177
+
160
178
  <!-- /microi-progressive:chunk -->
161
- <!-- microi-progressive:chunk id=page-engine-004 sha256=9c7c85bb0d25267d07e45d9abeb5691c2f0ffd9da233351cf2f448a9b848cea5 -->
162
- ## 组件通用结构
163
-
164
- ```json
165
- {
166
- "type": "bar",
167
- "label": "柱状图",
168
- "category": 0,
169
- "show": 1,
170
- "widgetOption": {
171
- "number": 20001, // 随机5位整数,页面内唯一
172
- "wrapperNumber": 10001, // 必须等于所在容器的 wrapperOption.number
173
- "span": 24,
174
- "height": 280,
175
- "dynamicStyle": { "padding": "8px", "backgroundColor": "" }
176
- },
177
- "widgetParams": []
178
- }
179
- ```
180
-
179
+ <!-- microi-progressive:chunk id=page-engine-004 sha256=66261254d94455648441bf3519a5c810ede60c64f341dce035f931e284f762ba -->
180
+ ## 组件通用结构
181
+
182
+ ```json
183
+ {
184
+ "type": "bar",
185
+ "label": "柱状图",
186
+ "category": 0,
187
+ "show": 1,
188
+ "widgetOption": {
189
+ "number": 20001, // 随机5位整数,页面内唯一
190
+ "wrapperNumber": 10001, // 必须等于所在容器的 wrapperOption.number
191
+ "span": 24,
192
+ "height": 280,
193
+ "dynamicStyle": { "padding": "8px", "backgroundColor": "" }
194
+ },
195
+ "widgetParams": []
196
+ }
197
+ ```
198
+
181
199
  <!-- /microi-progressive:chunk -->
182
- <!-- microi-progressive:chunk id=page-engine-005 sha256=c1e31dede775f14761182fc197cf428d494806c4503a622ffdad3d16d091d3e0 -->
183
- ## widgetParams 参数类型
184
-
185
- | type | 说明 | value 类型 |
186
- |------|------|-----------|
187
- | `textarea` | 多行文本(数据来源) | `string` |
188
- | `input` | 单行文本 | `string` |
189
- | `number` | 数字 | `number` |
190
- | `switch` | 开关 | `boolean` |
191
- | `slider` | 滑块 | `number` |
192
- | `color` | 颜色选择 | `string` |
193
- | `select` | 下拉选择 | `string` |
194
- | `radio` | 单选组 | `string` |
195
-
200
+ <!-- microi-progressive:chunk id=page-engine-005 sha256=e9667b97462c9089a72d2a407cfde00e873afe7d64593bf33ad49a523255b38d -->
201
+ ## widgetParams 参数类型
202
+
203
+ | type | 说明 | value 类型 |
204
+ |------|------|-----------|
205
+ | `textarea` | 多行文本(数据来源) | `string` |
206
+ | `input` | 单行文本 | `string` |
207
+ | `number` | 数字 | `number` |
208
+ | `switch` | 开关 | `boolean` |
209
+ | `slider` | 滑块 | `number` |
210
+ | `color` | 颜色选择 | `string` |
211
+ | `select` | 下拉选择 | `string` |
212
+ | `radio` | 单选组 | `string` |
213
+
196
214
  <!-- /microi-progressive:chunk -->
197
- <!-- microi-progressive:chunk id=page-engine-006 sha256=2c021e43f4f0d730da1f5a5cf6a2d314589435e2bb9ff573148c787538a8cc86 -->
198
- ## 数据来源(widgetParams[0])
199
-
200
- 大多数组件的第一个参数(sort=0)是"数据来源":
201
- - **静态数据**:`value` 为空,数据在 `typeOptions.dataJson` 中
202
- - **动态接口**:`value` 设为接口地址,运行时请求替换 `dataJson`
203
-
204
- **接口引擎地址格式:**
205
- ```
206
- $ApiBase$/apiengine/{ApiEngineKey}
207
- ```
208
-
215
+ <!-- microi-progressive:chunk id=page-engine-006 sha256=7347b2b4290db41ae8c3ef128acc9854a5e17290268b8e274e771de47b8f752a -->
216
+ ## 数据来源(widgetParams[0])
217
+
218
+ 大多数组件的第一个参数(sort=0)是"数据来源":
219
+ - **静态数据**:`value` 为空,数据在 `typeOptions.dataJson` 中
220
+ - **动态接口**:`value` 设为接口地址,运行时请求替换 `dataJson`
221
+
222
+ **接口引擎地址格式:**
223
+ ```
224
+ $ApiBase$/apiengine/{ApiEngineKey}
225
+ ```
226
+
209
227
  <!-- /microi-progressive:chunk -->
210
- <!-- microi-progressive:chunk id=page-engine-007 sha256=4170c6e274fc773427057d8606c4800a5bbd5fabd62d63e931aaa388262a51e9 -->
211
- ## 运行态布局与滚动规范
212
-
213
- - 页面只保留一个主滚动容器:仪表盘、首页和嵌套界面优先继承最外层页面滚动;单个 `pannel`、`workcenter`、`diytable`、`diycalendar`、嵌套 `pageengine` 在运行态默认使用内容自适应高度和 `overflow: visible`,不得无条件设置 `overflow: auto`。
214
- - 经典桌面主滚动容器为路由内容区,导航与 Tab 留在视口内;移动端继续保留外层文档滚动。图表在异步挂载后绑定尺寸观察器,切换导航、收缩侧栏及改变窗口宽度都应重新测量,禁止仅隐藏溢出而保留错误画布宽度。真实回归覆盖两种导航、两种桌面宽度,以及日程创建后刷新、备注编辑和删除。
215
- - 固定高度只用于设计器拖拽预览、图表画布或确有虚拟滚动需求的组件。设计态可按 `widgetOption.height` 提供滚动,运行态必须先验证内容能完整展开;不能用双滚动条掩盖容器高度不足。
216
- - 表格型容器通常按 15 条数据验收,表头、工具栏、全部数据行和分页区应同时可见;首页紧凑表格可通过 `diy-table` 的 `PageSizeList` 追加 `[10]`,并在 `sys_menu.DefaultPageSize` 为空时默认选择候选页码中的最小值。空间不足时先压缩统计卡、工具栏、行高和间距,再考虑增加外层页面长度,不隐藏分页、不在卡片内部滚动。
217
- - 同一行的容器使用一致的左右边距、内边距、标题高度和顶部基线;同层卡片间距必须相同,左右边缘、标题、工具栏和内容区应形成稳定对齐线。推荐使用统一的 8px 基础间距和 8/12/16px 倍数。
218
- - 成对容器应设置相同的最小高度;内容较少的组件通过内容居中、合理留白或自适应布局消化空间,不能在底部留下明显的大块无意义空白。内容较多时允许容器自然增高并由页面继续向下滚动。
219
- - 日历和公告应完整展示工具栏、主体和底部操作区;日历优先使用 `height: auto` / `contentHeight: auto`,公告优先使用标准表单引擎分页与权限按钮。
220
- - 运行态移动端必须按实际视口自动切为 24 栅格,不能只依赖保存 JSON 中的 `formConfig.mobile`;窗口旋转或宽度变化时也要同步。嵌入的 `diytable` 不得渲染独立列表页的固定返回栏和全局 FAB,新增、页面按钮、批量操作应留在当前容器工具栏内;容器标题操作区允许紧凑换行,但不能绝对定位覆盖标题或其它卡片。
221
- - `aiengine` 在移动端嵌入首页时应使用内容自适应高度和外层页面滚动,指标卡与快捷入口优先两列紧凑排布,避免 4 个指标和全部快捷入口单列堆叠造成超长首屏;输入区和模型/推理设置必须保持可见且不横向溢出。
222
- - 交付前必须在真实运行页检查:是否存在内部纵向/横向滚动条、15 条表格分页是否可见、同排容器是否对齐、四周边距是否一致、组件底部是否有异常空白。仅检查设计器画布不算验收完成。
223
-
228
+ <!-- microi-progressive:chunk id=page-engine-007 sha256=5f2ed06aa1e0abbc5f968f026602d92f22f01836d76fdf502fd91ca0f945509b -->
229
+ ## 运行态布局与滚动规范
230
+
231
+ - 页面只保留一个主滚动容器:仪表盘、首页和嵌套界面优先继承最外层页面滚动;单个 `pannel`、`workcenter`、`diytable`、`diycalendar`、嵌套 `pageengine` 在运行态默认使用内容自适应高度和 `overflow: visible`,不得无条件设置 `overflow: auto`。
232
+ - 经典桌面主滚动容器为路由内容区,导航与 Tab 留在视口内;移动端继续保留外层文档滚动。图表在异步挂载后绑定尺寸观察器,切换导航、收缩侧栏及改变窗口宽度都应重新测量,禁止仅隐藏溢出而保留错误画布宽度。真实回归覆盖两种导航、两种桌面宽度,以及日程创建后刷新、备注编辑和删除。
233
+ - 固定高度只用于设计器拖拽预览、图表画布或确有虚拟滚动需求的组件。设计态可按 `widgetOption.height` 提供滚动,运行态必须先验证内容能完整展开;不能用双滚动条掩盖容器高度不足。
234
+ - 表格型容器通常按 15 条数据验收,表头、工具栏、全部数据行和分页区应同时可见;首页紧凑表格可通过 `diy-table` 的 `PageSizeList` 追加 `[10]`,并在 `sys_menu.DefaultPageSize` 为空时默认选择候选页码中的最小值。空间不足时先压缩统计卡、工具栏、行高和间距,再考虑增加外层页面长度,不隐藏分页、不在卡片内部滚动。
235
+ - 同一行的容器使用一致的左右边距、内边距、标题高度和顶部基线;同层卡片间距必须相同,左右边缘、标题、工具栏和内容区应形成稳定对齐线。推荐使用统一的 8px 基础间距和 8/12/16px 倍数。
236
+ - 成对容器应设置相同的最小高度;内容较少的组件通过内容居中、合理留白或自适应布局消化空间,不能在底部留下明显的大块无意义空白。内容较多时允许容器自然增高并由页面继续向下滚动。
237
+ - 日历和公告应完整展示工具栏、主体和底部操作区;日历优先使用 `height: auto` / `contentHeight: auto`,公告优先使用标准表单引擎分页与权限按钮。
238
+ - 运行态移动端必须按实际视口自动切为 24 栅格,不能只依赖保存 JSON 中的 `formConfig.mobile`;窗口旋转或宽度变化时也要同步。嵌入的 `diytable` 不得渲染独立列表页的固定返回栏和全局 FAB,新增、页面按钮、批量操作应留在当前容器工具栏内;容器标题操作区允许紧凑换行,但不能绝对定位覆盖标题或其它卡片。
239
+ - `aiengine` 在移动端嵌入首页时应使用内容自适应高度和外层页面滚动,指标卡与快捷入口优先两列紧凑排布,避免 4 个指标和全部快捷入口单列堆叠造成超长首屏;输入区和模型/推理设置必须保持可见且不横向溢出。
240
+ - 交付前必须在真实运行页检查:是否存在内部纵向/横向滚动条、15 条表格分页是否可见、同排容器是否对齐、四周边距是否一致、组件底部是否有异常空白。仅检查设计器画布不算验收完成。
241
+
224
242
  <!-- /microi-progressive:chunk -->
225
243
  ## 详细参考路由(渐进披露)
226
244
 
@@ -257,3 +257,8 @@ return {
257
257
  }
258
258
  };
259
259
  ```
260
+
261
+ ## 真实打印预览验收
262
+
263
+ - 查看/预览先以真实授权单据填充模板,明确显示加载、空数据和失败状态;由用户点击打印后再打开系统打印窗口,避免打开页面即打印而掩盖预览错误。明确的自动打印场景单独保留。
264
+ - 验证模板的实际输出,不把 design() 画布、testData 或接口成功当作打印通过。覆盖发货清单/标签、数量金额、二维码、长表分页、越权拒绝及关闭后清理;没有连接实体打印机时明确仅完成浏览器预览/PDF验收。
@@ -73,6 +73,14 @@ var result = await V8.ApiEngine.Run('report_adjust_inventory', {
73
73
 
74
74
  ## 验收清单
75
75
 
76
+ ### 模拟数据与当前周期(强制)
77
+
78
+ 1. 先读取实际业务时间字段的最小/最大值、各期数量和状态分布,再区分无数据、日期不匹配、权限过滤、DTO/绑定错误和请求失败。数据总量足够不代表默认“本月”有数据。
79
+ 2. 在已授权开发租户,以本次业务时区日期为 `asOf`,只补缺失的模拟订单及其主子表、履约/签收/退货/异常等适用流程。保留稳定种子标识、写前计划和实际 Id;通过正常表单事件和业务命令生成金额/库存,不直接改状态凑图表。日期时间应满足业务先后关系,不能把同日午夜承诺时间放在上午下单时间之前。
80
+ 3. 生成可核对的 JSON `{asOf,dateField,rows,periods,stateField,requiredStates}`,运行 `node microi.skills/report-engine/scripts/check-demo-coverage.mjs <文件路径>`。`asOf` 为声明业务时区的 `YYYY-MM-DD`;`periods` 指定需有演示数据的默认周期,支持 today/week/month/quarter/year/lastYear。该检查只验证夹具覆盖,不能代替服务端授权、聚合与 UI 验收。确应为空的周期用 `allowEmptyPeriods` 显式声明并在验收报告解释,禁止为通过检查伪造交易。
81
+ 4. 用同一日期、角色、维度从业务明细独立重算数量、金额、发运/签收和异常等指标,核对统计、分组、趋势、排行、分页明细与穿透;小数按业务精度比较。测试月末→月初、年末→年初、周一边界和明确空区间。时间区间采用约定的包含起点、不含终点语义,不能静默扩大范围掩盖空报表。
82
+ 5. 从每个原报表菜单真实打开并截图,检查所有组件与默认筛选;月初一天的趋势允许只有一个点,切本周再验证多日数据。模拟数据仅代表演示业务,不能称为真实交易,也不默认创建持续自动造数据的定时任务。
83
+
76
84
  - [ ] 指标口径、时区和小数精度明确
77
85
  - [ ] 当前租户与行级范围在数据库查询中生效
78
86
  - [ ] 虚拟字段与物理字段没有混用
@@ -81,3 +89,4 @@ var result = await V8.ApiEngine.Run('report_adjust_inventory', {
81
89
  - [ ] 缓存按租户和权限隔离
82
90
  - [ ] 普通角色、无权限角色、跨租户负向测试通过
83
91
  - [ ] 配套界面引擎看板与穿透明细/导出使用同一口径,时间边界、筛选、零/空/失败及明细对账通过
92
+ - [ ] 模拟数据覆盖实际验收日的默认周期和关键状态,跨月/跨年复测、各组件真实数据和原菜单截图均有回执
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ const dayMs = 86400000;
7
+ const supported = new Set(['today', 'week', 'month', 'quarter', 'year', 'lastYear']);
8
+
9
+ // 输入应先规范成业务时区;此处只比较业务日历日期,避免机器时区改变结果。
10
+ function calendarDay(value, dateOnly = false) {
11
+ if (typeof value !== 'string' || !(dateOnly
12
+ ? /^\d{4}-\d{2}-\d{2}$/
13
+ : /^\d{4}-\d{2}-\d{2}(?:$|[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})?$)/).test(value)) return null;
14
+ const parts = value.slice(0, 10).split('-').map(Number);
15
+ const d = new Date(0);
16
+ d.setUTCFullYear(parts[0], parts[1] - 1, parts[2]);
17
+ d.setUTCHours(0, 0, 0, 0);
18
+ if (d.toISOString().slice(0, 10) !== value.slice(0, 10)) return null;
19
+ if (value.length > 10) {
20
+ const time = value.slice(11).match(/^(\d{2}):(\d{2})(?::(\d{2}))?/);
21
+ if (!time || Number(time[1]) > 23 || Number(time[2]) > 59 || Number(time[3] || 0) > 59) return null;
22
+ const offset = value.match(/[+-](\d{2}):(\d{2})$/);
23
+ if (offset && (Number(offset[1]) > 23 || Number(offset[2]) > 59)) return null;
24
+ }
25
+ return d.getTime();
26
+ }
27
+
28
+ export function checkDemoCoverage(input) {
29
+ const errors = [];
30
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return { ok: false, errors: ['Expected an input object'] };
31
+ const anchor = calendarDay(input.asOf, true);
32
+ if (anchor === null) errors.push('asOf must be a valid business calendar date YYYY-MM-DD');
33
+ if (typeof input.dateField !== 'string' || !input.dateField.trim()) errors.push('dateField is required');
34
+ if (!Array.isArray(input.rows)) errors.push('rows must be an array');
35
+ const periods = input.periods ?? ['today', 'week', 'month'];
36
+ const allowEmpty = input.allowEmptyPeriods ?? [];
37
+ for (const [name, values] of [['periods', periods], ['allowEmptyPeriods', allowEmpty]]) {
38
+ if (!Array.isArray(values) || (name === 'periods' && values.length === 0) || values.some(p => !supported.has(p))) errors.push(`${name} must contain supported period names`);
39
+ }
40
+ if (input.requiredStates !== undefined && (!Array.isArray(input.requiredStates) || input.requiredStates.some(s => typeof s !== 'string'))) errors.push('requiredStates must be an array of strings');
41
+ if (input.requiredStates?.length && (typeof input.stateField !== 'string' || !input.stateField.trim())) errors.push('stateField is required with requiredStates');
42
+ if (errors.length) return { ok: false, errors };
43
+
44
+ const d = new Date(anchor);
45
+ const y = d.getUTCFullYear(), m = d.getUTCMonth();
46
+ const make = (year, month, day = 1) => {
47
+ const result = new Date(0);
48
+ result.setUTCFullYear(year, month, day); result.setUTCHours(0, 0, 0, 0); return result.getTime();
49
+ };
50
+ const bounds = {
51
+ today: [anchor, anchor + dayMs],
52
+ week: [anchor - ((d.getUTCDay() + 6) % 7) * dayMs, anchor + dayMs],
53
+ month: [make(y, m), anchor + dayMs],
54
+ quarter: [make(y, Math.floor(m / 3) * 3), anchor + dayMs],
55
+ year: [make(y, 0), anchor + dayMs],
56
+ lastYear: [make(y - 1, 0), make(y, 0)],
57
+ };
58
+ const counts = Object.fromEntries(periods.map(p => [p, 0]));
59
+ const states = Object.create(null);
60
+ let earliest = null, latest = null;
61
+ input.rows.forEach((row, index) => {
62
+ const stamp = calendarDay(row?.[input.dateField]);
63
+ if (stamp === null) { errors.push(`rows[${index}].${input.dateField} is missing or invalid`); return; }
64
+ earliest = earliest === null ? stamp : Math.min(earliest, stamp);
65
+ latest = latest === null ? stamp : Math.max(latest, stamp);
66
+ for (const p of periods) if (stamp >= bounds[p][0] && stamp < bounds[p][1]) counts[p]++;
67
+ if (input.stateField) {
68
+ const value = row?.[input.stateField];
69
+ if (typeof value !== 'string' || !value) errors.push(`rows[${index}].${input.stateField} is missing or invalid`);
70
+ else states[value] = (states[value] || 0) + 1;
71
+ }
72
+ });
73
+ for (const p of periods) if (counts[p] === 0 && !allowEmpty.includes(p)) errors.push(`No demo data in required period: ${p}`);
74
+ for (const state of input.requiredStates || []) if (!states[state]) errors.push(`No demo data in required state: ${state}`);
75
+ const date = stamp => stamp === null ? null : new Date(stamp).toISOString().slice(0, 10);
76
+ return { ok: errors.length === 0, asOf: input.asOf, totalRows: input.rows.length, minDate: date(earliest), maxDate: date(latest), periodCounts: counts, stateCounts: states, errors };
77
+ }
78
+
79
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
80
+ try {
81
+ if (!process.argv[2]) throw new Error('Usage: node check-demo-coverage.mjs <input.json>');
82
+ const result = checkDemoCoverage(JSON.parse(fs.readFileSync(process.argv[2], 'utf8')));
83
+ console.log(JSON.stringify(result, null, 2));
84
+ if (!result.ok) process.exitCode = 1;
85
+ } catch (error) {
86
+ console.error(JSON.stringify({ ok: false, errors: [error.message] })); process.exitCode = 1;
87
+ }
88
+ }