@agile-team/wl-skills-ui 1.7.1 → 1.8.0
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.
- package/CHANGELOG.md +24 -0
- package/README.md +10 -1
- package/docs/governance-long-term.md +191 -0
- package/mcp/server.js +86 -0
- package/package.json +1 -1
- package/scanner/rules/dialog.mjs +31 -15
- package/scanner/rules/tag.mjs +10 -8
- package/scripts/check-docs.mjs +91 -0
- package/skills/_meta/_registry.md +63 -59
- package/skills/runtime/style-align/SKILL.md +15 -42
- package/standards/rules-loader.mjs +66 -0
- package/standards/rules.json +392 -0
- package/es/chunk-GBB7LDKQ.js +0 -261
- package/es/chunk-MDI2SCUP.js +0 -281
- package/es/chunk-YLNAFAWE.js +0 -233
- package/es/renderers-DMZbi_Gs.d.ts +0 -51
- package/es/renderers-DWhUlI2y.d.ts +0 -47
- package/es/types-04qLKHMt.d.ts +0 -62
- package/es/types-BrCPjAH6.d.ts +0 -62
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to **@agile-team/wl-skills-ui** will be documented in this f
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [1.8.0] - 2026-05-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 新增 **`standards/rules.json`** R-rule 单一事实源:29 条规则按 `id / category / severity / appliesTo / autoFixable / scanner / skills` 结构化注册,所有 standards 文档、SKILL.md、scanner、MCP、未来 ESLint 插件均从此派生。
|
|
12
|
+
- 新增 **`standards/rules-loader.mjs`** 共享加载器:暴露 `loadRules / listRules / getRule / groupByCategory / buildRuleSummary`,scanner / MCP / check-docs / Vite 插件统一读取。
|
|
13
|
+
- 新增 MCP 工具 **`wl_ui_list_rules`**:按 `category / severity / autoFixable` 过滤返回规则摘要。
|
|
14
|
+
- 新增 MCP 工具 **`wl_ui_describe_rule`**:按 ID 返回单条 R-rule 完整定义(含 aliases 兼容旧 ID)。
|
|
15
|
+
- 新增 **`docs/governance-long-term.md`**:业务项目长效治理方案(基线 / 豁免 / 漂移看板 / 版本钉死 / 写作期 AI 守护五机制)。
|
|
16
|
+
- `scripts/check-docs.mjs` 扩展:校验 `scanner/rules/*.mjs` 中所有 `id` 必须在 `rules.json` 注册、SKILL.md 引用的 R-id 必须存在、`_registry.md` 引用的 skill 目录必须真实存在。
|
|
17
|
+
|
|
18
|
+
### Changed (Breaking-ish)
|
|
19
|
+
|
|
20
|
+
- **修复 R011 逻辑反转 bug**:之前 scanner 检测分页"不在 #footer 报错",与 `standards/ui/05` "分页必须放内容区,不得放 #footer" 相互矛盾。v1.8.0 反转 scanner 检测语义,与 standards 对齐。
|
|
21
|
+
- **`scanner/rules/tag.mjs` R017/R018 重号为 R019/R020**:原 ID 与 `color.mjs` 的 R017/R018 冲突。`rules.json` 通过 `aliases: [R017_TAG_LEGACY/R018_TAG_LEGACY]` 兼容历史引用;scanner 输出 `rule` 字段改为新 ID。
|
|
22
|
+
- **`skills/_meta/_registry.md` 清理 12 条幽灵条目**:删除 9 个不存在的 `element/*` SKILL 引用(el-card/el-tabs/el-descriptions/el-tree/el-drawer/el-upload/el-steps/el-overlay/el-navigation/el-feedback),声明已统一归入 `element/component-family`;删除 2 个 `ops/*` 引用(route-intent / recommend-flow),声明为 MCP 工具而非独立 SKILL。
|
|
23
|
+
- **`skills/runtime/style-align/SKILL.md` 改为指针式引用**:不再复述 17 条 R-rule 内容,仅保留分类→编号→SKILL 映射表,规则细节统一查 `standards/rules.json` 或 `wl_ui_describe_rule`。
|
|
24
|
+
- `tsup.config.ts` `clean: true`:每次构建清空 `es/` 目录,避免 hash 残留。
|
|
25
|
+
|
|
26
|
+
### Notes
|
|
27
|
+
|
|
28
|
+
- R013(Upload 嵌入 operations[])由文档约束晋升为 `rules.json` 正式条目(`severity: review`,无 scanner 实现)。
|
|
29
|
+
- 推荐业务项目跟进:跑一次 `npx wl-ui audit --target src --outFile .wl-baseline.json` 建立基线,配合 `--baseline` 增量门槛使用(详见 `docs/governance-long-term.md`)。
|
|
30
|
+
|
|
7
31
|
## [1.7.1] - 2026-05-12
|
|
8
32
|
|
|
9
33
|
### Added
|
package/README.md
CHANGED
|
@@ -213,7 +213,16 @@ yarn add @agile-team/wl-skills-ui
|
|
|
213
213
|
|
|
214
214
|
## 版本亮点
|
|
215
215
|
|
|
216
|
-
当前 v1.
|
|
216
|
+
当前 v1.8.0 版本完成 R-rule 治理体系单一事实源化,并落地业务项目长效治理方案:
|
|
217
|
+
|
|
218
|
+
- 新增 **`standards/rules.json`** 作为 29 条 R-rule 的单一事实源;所有 standards 文档、SKILL.md、scanner、MCP、未来 ESLint 插件均从此派生
|
|
219
|
+
- 新增 **`standards/rules-loader.mjs`** 共享加载器;新增 MCP 工具 **`wl_ui_list_rules`** / **`wl_ui_describe_rule`**,AI 写代码前一键查规则
|
|
220
|
+
- 修复历史 ID 冲突与逻辑反转 bug:scanner `tag.mjs` 原 R017/R018 重号为 R019/R020(与 `color.mjs` 解耦,旧 ID 通过 `aliases` 兼容);R011 由"必须放 #footer"反转为"不得放 #footer",与 standards/ui/05 对齐
|
|
221
|
+
- 清理 `_registry.md` 12 条幽灵 skill 引用;`runtime/style-align/SKILL.md` 改为指针式引用 rules.json,不再复述规则细节
|
|
222
|
+
- `scripts/check-docs.mjs` 扩展为 R-rule / scanner / SKILL / registry 四向一致性守卫,CI 不一致直接红灯
|
|
223
|
+
- 新增 **`docs/governance-long-term.md`**:业务项目长效治理方案(基线 / 豁免 / 漂移看板 / 版本钉死 / 写作期 AI 守护五机制),杜绝遗留累积与升级回归
|
|
224
|
+
|
|
225
|
+
历史亮点(v1.7.1):
|
|
217
226
|
|
|
218
227
|
- 新增 Vite 插件 `@agile-team/wl-skills-ui/vite`:消费方一行配置即可在每次启动 dev/build 时自动跑版本配对校验,偏离时彩色提示 + 修复片段
|
|
219
228
|
- 新增 `npx wl-ui doctor --print-overrides`:检测到偏离直接输出 pnpm/npm/yarn overrides JSON,复制粘贴即可修复
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# 长效治理:业务项目如何避免遗留与漂移污染
|
|
2
|
+
|
|
3
|
+
> 适用:使用 `@agile-team/wl-skills-ui` 的业务项目,长期迭代过程中**不让历史违规无限累积**、**不让新代码反向污染**、**不让升级回归打穿主干**。
|
|
4
|
+
> 事实源:本文档结合 `standards/rules.json` 单一事实源 + `scanner/snapshot.mjs` + `scanner/exempt.mjs` 共同提供。
|
|
5
|
+
|
|
6
|
+
## 一、问题画像
|
|
7
|
+
|
|
8
|
+
业务项目随时间会出现三种"污染":
|
|
9
|
+
|
|
10
|
+
| 类型 | 表现 | 根因 |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| **历史遗留**(legacy) | 旧页面违规但没人修,scanner 报错被忽略 | 一次性扫描门槛太高,团队选择 mute |
|
|
13
|
+
| **风格漂移**(drift) | 同一种场景不同写法,hex 颜色又冒出来 | 没有 PR 级别的增量校验 |
|
|
14
|
+
| **升级回归**(regression) | wl-skills-ui 升版后老页面视觉错乱 | vendor 版本配对没钉死、scoped 样式覆盖底层 |
|
|
15
|
+
|
|
16
|
+
下面三条机制对应三个根因。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 二、机制 1 · 基线快照(Baseline)
|
|
21
|
+
|
|
22
|
+
**目标**:把"历史欠债"冻结,新代码只对增量负责。
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# 在主干分支建立基线(团队第一次接入时执行一次)
|
|
26
|
+
npx wl-ui audit --target src --output json --outFile .wl-baseline.json
|
|
27
|
+
git add .wl-baseline.json && git commit -m "chore: wl-ui baseline snapshot"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`.wl-baseline.json` 记录当前所有违规条目的 `(file, line, ruleId)` 指纹。后续:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# PR 检查(仅报新增违规)
|
|
34
|
+
npx wl-ui scan --target src --baseline .wl-baseline.json --fail-on=new-issues
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **历史条目不再阻塞构建**(团队按计划逐步消化)
|
|
38
|
+
- **新代码引入新违规 → 立即红灯**
|
|
39
|
+
- 团队修复历史条目后跑 `npx wl-ui audit --refresh-baseline` 收敛基线
|
|
40
|
+
|
|
41
|
+
> 实现入口:`scanner/snapshot.mjs` 已具备文件级快照能力,issue 级 baseline 在 v1.8.0 起以 `audit --output json` 输出为契约,业务项目可自行 diff。
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 三、机制 2 · 豁免与隔离(Quarantine)
|
|
46
|
+
|
|
47
|
+
**目标**:大屏、地图、流程设计器等强个性化页面 + 不再迭代的旧模块,明确隔离,**不参与统一风格扫描**。
|
|
48
|
+
|
|
49
|
+
`.wl-exempt.json`:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"exemptPaths": [
|
|
54
|
+
"src/views/**/big-screen/**",
|
|
55
|
+
"src/views/**/dashboard/**",
|
|
56
|
+
"src/views/legacy/**"
|
|
57
|
+
],
|
|
58
|
+
"exemptRules": {
|
|
59
|
+
"R016": ["src/views/**/chart/**"]
|
|
60
|
+
},
|
|
61
|
+
"exemptCategories": ["dashboard", "big-screen", "report-designer"],
|
|
62
|
+
"description": "大屏与遗留模块豁免风格统一扫描;新功能不得新增豁免路径"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
约束(团队规约):
|
|
67
|
+
|
|
68
|
+
1. 豁免清单**只能减少**,不能增加(新增需架构组评审 PR)
|
|
69
|
+
2. 豁免路径**禁止承载新业务**;新业务必须落到非豁免目录
|
|
70
|
+
3. 每季度团队负责人审视豁免清单:能解开就解开
|
|
71
|
+
|
|
72
|
+
> 实现入口:`scanner/exempt.mjs::loadExemptConfig` 已支持 glob + 规则级豁免;CLI/MCP/Vite 插件统一读取。
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 四、机制 3 · 漂移看板(Drift Dashboard)
|
|
77
|
+
|
|
78
|
+
**目标**:让"逐渐变烂"可见,每次升级都能量化。
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# 每次 wl-skills-ui 升版后跑一次
|
|
82
|
+
npx wl-ui audit --target src --output json --outFile .wl-drift-after.json
|
|
83
|
+
node -e "console.log(require('@agile-team/wl-skills-ui/scanner/drift.mjs').report('.wl-baseline.json','.wl-drift-after.json'))"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
输出(示意):
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
变化(基线 → 当前)
|
|
90
|
+
gained +12 新增违规(红灯)
|
|
91
|
+
fixed -45 消化历史(绿灯)
|
|
92
|
+
regressed +3 曾修过又破的(黄灯,最高优先级追查)
|
|
93
|
+
exempt +0 豁免清单未扩张
|
|
94
|
+
按规则 Top3:R017(+8) R001(+3) R009(+1)
|
|
95
|
+
按目录 Top3:src/views/quote(+9) src/views/cost(+3)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> 实现入口:v1.8.0 起 `scanner/index.mjs` 已暴露 `--output json` 完整结构(含 `file/line/rule`),业务项目可基于该结构自实现 drift 报告;包内 `scanner/drift.mjs` 在 1.8.x 后续小版本提供。
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 五、机制 4 · 版本钉死(Lock & Detect)
|
|
103
|
+
|
|
104
|
+
**目标**:vendor(Element Plus / @jhlc/jh-ui)版本飘移导致老页面 DOM 不匹配的问题,**在启动期就发现**,而不是页面坏了才查。
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
// vite.config.ts
|
|
108
|
+
import { wlSkillsCheck } from '@agile-team/wl-skills-ui/runtime/vite/check.mjs'
|
|
109
|
+
|
|
110
|
+
export default defineConfig({
|
|
111
|
+
plugins: [
|
|
112
|
+
wlSkillsCheck({
|
|
113
|
+
enforce: 'warn', // 'warn' | 'error'
|
|
114
|
+
verbose: false,
|
|
115
|
+
}),
|
|
116
|
+
],
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
启动时输出(不匹配示意):
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
[wl-skills-ui] 版本偏离推荐组合:
|
|
124
|
+
element-plus@2.6.3 ≠ 推荐 2.2.6-prod.3(jh 集群)
|
|
125
|
+
@jhlc/jh-ui@3.1.0 ✓
|
|
126
|
+
修复片段:
|
|
127
|
+
"pnpm": { "overrides": { "element-plus": "2.2.6-prod.3" } }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
> 推荐组合 = `skills/_meta/_compat/vendors.json`(事实源)。CLI: `npx wl-ui doctor --print-overrides`。MCP: `wl_ui_detect_skin`。
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 六、机制 5 · 写作期 AI 守护(Editor Skill)
|
|
135
|
+
|
|
136
|
+
**目标**:在 AI 写出新代码的那一刻就让它**对齐 rules.json**,而不是事后扫描。
|
|
137
|
+
|
|
138
|
+
- 编辑器接入:`npx wl-ui init` 会把 `.cursor/rules` / `.windsurf/rules` 等指向 `skills/` 目录
|
|
139
|
+
- AI 强约束(写入 SKILL 头部):
|
|
140
|
+
- 任何样式相关结论必须先通过 MCP `wl_ui_describe_rule` 查 `standards/rules.json`
|
|
141
|
+
- **禁止**在业务 SFC 的 `<style scoped>` 中覆盖 `.el-*` / `.jh-*` 全局选择器;样式补丁全部归 wl-skills-ui 升级
|
|
142
|
+
- 新增组件族适配优先升级到 wl-skills-ui,而不是在业务侧封 `Base*` 二代
|
|
143
|
+
|
|
144
|
+
效果:AI 写出的代码 day-1 即合规,不再产生历史欠债。
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 七、典型业务项目接入清单
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# 1. 安装
|
|
152
|
+
pnpm add -D @agile-team/wl-skills-ui
|
|
153
|
+
|
|
154
|
+
# 2. 编辑器规则 + MCP + 启动期插件
|
|
155
|
+
npx wl-ui init --project . --mode skin
|
|
156
|
+
|
|
157
|
+
# 3. 基线
|
|
158
|
+
npx wl-ui audit --target src --outFile .wl-baseline.json --output json
|
|
159
|
+
git add .wl-baseline.json
|
|
160
|
+
|
|
161
|
+
# 4. 豁免(如有)
|
|
162
|
+
cp node_modules/@agile-team/wl-skills-ui/.wl-exempt.example.json .wl-exempt.json
|
|
163
|
+
# 编辑为本项目实际豁免路径
|
|
164
|
+
|
|
165
|
+
# 5. CI 加 PR gate
|
|
166
|
+
# npx wl-ui scan --target src --baseline .wl-baseline.json --fail-on=new-issues
|
|
167
|
+
|
|
168
|
+
# 6. 每季度 / 每次升级
|
|
169
|
+
npx wl-ui doctor --print-overrides # 版本对齐
|
|
170
|
+
npx wl-ui audit --refresh-baseline # 基线收敛
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 八、wl-skills-ui 自身的承诺
|
|
176
|
+
|
|
177
|
+
为了让业务项目"敢于"长期跟随:
|
|
178
|
+
|
|
179
|
+
1. **R-rule 不删只迁**:v1.8.0 起 `tag.mjs` 的旧 R017/R018 → R019/R020,旧 ID 在 `rules.json.aliases` 保留,scanner 输出兼容
|
|
180
|
+
2. **CSS Token 不破坏**:`design/tokens/base.css` 的变量名为长期契约,重命名走 deprecate → alias → remove 三段式
|
|
181
|
+
3. **vendor 适配可选**:业务项目偏离推荐组合时插件**只警告不阻塞**,由项目方决策升级节奏
|
|
182
|
+
4. **每个 minor 版本附带 compat-matrix 更新**:`docs/compat-matrix.md` 是版本与 vendor 的双向契约
|
|
183
|
+
5. **`docs:check` 守护文档/规则/scanner 一致性**:包级 CI 不通过则不发版
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 九、不做的事
|
|
188
|
+
|
|
189
|
+
- ❌ 不强行重写业务 SFC 结构(除非走 ops/migrate 显式确认)
|
|
190
|
+
- ❌ 不在业务项目里塞自动 commit hook(团队自行选 husky/lefthook)
|
|
191
|
+
- ❌ 不收集业务项目数据(所有扫描结果留在本地或 CI artifact)
|
package/mcp/server.js
CHANGED
|
@@ -98,6 +98,41 @@ const TOOLS = [
|
|
|
98
98
|
required: [],
|
|
99
99
|
},
|
|
100
100
|
},
|
|
101
|
+
{
|
|
102
|
+
name: "wl_ui_list_rules",
|
|
103
|
+
description:
|
|
104
|
+
"列出 wl-skills-ui R-rule 全集(事实源:standards/rules.json)。可按 category / severity / autoFixable 过滤。",
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
category: {
|
|
109
|
+
type: "string",
|
|
110
|
+
description:
|
|
111
|
+
"可选:table / button / form / dialog / tag / style / base / family / layout",
|
|
112
|
+
},
|
|
113
|
+
severity: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "可选:error / warning / info / suggestion / review",
|
|
116
|
+
},
|
|
117
|
+
autoFixable: {
|
|
118
|
+
type: "boolean",
|
|
119
|
+
description: "可选:仅返回可自动修复的规则",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
required: [],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "wl_ui_describe_rule",
|
|
127
|
+
description: "返回单条 R-rule 的完整定义(事实源:standards/rules.json)。",
|
|
128
|
+
inputSchema: {
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: {
|
|
131
|
+
id: { type: "string", description: "规则 ID,例如 R001、R019" },
|
|
132
|
+
},
|
|
133
|
+
required: ["id"],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
101
136
|
{
|
|
102
137
|
name: "wl_ui_recommend_flow",
|
|
103
138
|
description:
|
|
@@ -409,6 +444,57 @@ async function dispatchTool(id, name, args) {
|
|
|
409
444
|
});
|
|
410
445
|
return;
|
|
411
446
|
}
|
|
447
|
+
if (name === "wl_ui_list_rules") {
|
|
448
|
+
const loader = await import("../standards/rules-loader.mjs");
|
|
449
|
+
const rules = loader.listRules({
|
|
450
|
+
category: args.category,
|
|
451
|
+
severity: args.severity,
|
|
452
|
+
autoFixable: args.autoFixable,
|
|
453
|
+
});
|
|
454
|
+
const summary = rules.map((r) => ({
|
|
455
|
+
id: r.id,
|
|
456
|
+
severity: r.severity,
|
|
457
|
+
category: r.category,
|
|
458
|
+
title: r.title,
|
|
459
|
+
autoFixable: !!r.autoFixable,
|
|
460
|
+
}));
|
|
461
|
+
sendResult(id, {
|
|
462
|
+
content: [
|
|
463
|
+
{
|
|
464
|
+
type: "text",
|
|
465
|
+
text: JSON.stringify(
|
|
466
|
+
{ total: summary.length, rules: summary },
|
|
467
|
+
null,
|
|
468
|
+
2,
|
|
469
|
+
),
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (name === "wl_ui_describe_rule") {
|
|
476
|
+
const loader = await import("../standards/rules-loader.mjs");
|
|
477
|
+
const rule = loader.getRule(args.id);
|
|
478
|
+
if (!rule) {
|
|
479
|
+
sendResult(id, {
|
|
480
|
+
content: [
|
|
481
|
+
{
|
|
482
|
+
type: "text",
|
|
483
|
+
text: JSON.stringify(
|
|
484
|
+
{ ok: false, reason: `规则 ${args.id} 未注册` },
|
|
485
|
+
null,
|
|
486
|
+
2,
|
|
487
|
+
),
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
});
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
sendResult(id, {
|
|
494
|
+
content: [{ type: "text", text: JSON.stringify(rule, null, 2) }],
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
412
498
|
if (name === "wl_ui_recommend_flow") {
|
|
413
499
|
sendResult(id, {
|
|
414
500
|
content: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agile-team/wl-skills-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "企业级 UI 风格对齐框架 — Vue + Element Plus 项目通用化妆/原生双模式(tokens / element / vendors / layouts / runtime / scanner / fixer / skills)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./es/index.js",
|
package/scanner/rules/dialog.mjs
CHANGED
|
@@ -1,29 +1,44 @@
|
|
|
1
|
-
/** scanner/rules/dialog.mjs — 弹窗分页规则:R011
|
|
1
|
+
/** scanner/rules/dialog.mjs — 弹窗分页规则:R011
|
|
2
|
+
* v1.8.0 修复:之前逻辑反了(要求"必须放 #footer"),与 standards/ui/05-dialog-pagination.md
|
|
3
|
+
* 「分页必须放在内容区,不得放在 #footer」语义矛盾。本版反转为正确语义:检测到
|
|
4
|
+
* #footer 内含 el-pagination 即报错。
|
|
5
|
+
* 事实源:standards/rules.json (R011)
|
|
6
|
+
*/
|
|
2
7
|
import { lineOf, issue } from "./_shared.mjs";
|
|
3
8
|
|
|
4
9
|
export const dialogRules = [
|
|
5
|
-
// R011: pagination
|
|
10
|
+
// R011: el-pagination 不得位于 #footer 插槽中
|
|
6
11
|
{
|
|
7
12
|
id: "R011",
|
|
8
13
|
category: "dialog",
|
|
9
14
|
severity: "error",
|
|
10
|
-
name: "
|
|
15
|
+
name: "弹窗内 el-pagination 不得放在 #footer 插槽中",
|
|
11
16
|
check(template, file, lineOffset) {
|
|
12
17
|
const issues = [];
|
|
13
|
-
//
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
// 提取所有 <template #footer ...> ... </template> 段
|
|
19
|
+
const footerSegments = [];
|
|
20
|
+
const footerOpenRe = /<template\s+#footer\b[^>]*>/g;
|
|
21
|
+
let openMatch;
|
|
22
|
+
while ((openMatch = footerOpenRe.exec(template)) !== null) {
|
|
23
|
+
const start = openMatch.index;
|
|
24
|
+
const after = template.slice(start);
|
|
25
|
+
// 简易匹配最近的 </template>(v3 中 footer slot 不嵌套 template)
|
|
26
|
+
const closeRel = after.search(/<\/template>/);
|
|
27
|
+
if (closeRel === -1) continue;
|
|
28
|
+
footerSegments.push({ start, end: start + closeRel });
|
|
29
|
+
}
|
|
30
|
+
const inFooter = (idx) =>
|
|
31
|
+
footerSegments.some((s) => idx >= s.start && idx <= s.end);
|
|
32
|
+
|
|
33
|
+
const paginationRe = /<el-pagination\b/g;
|
|
18
34
|
let m;
|
|
19
|
-
while ((m =
|
|
20
|
-
//
|
|
35
|
+
while ((m = paginationRe.exec(template)) !== null) {
|
|
36
|
+
// 必须在 el-dialog 内
|
|
21
37
|
const before = template.slice(0, m.index);
|
|
22
38
|
const dialogOpens = (before.match(/<el-dialog\b/g) || []).length;
|
|
23
39
|
const dialogCloses = (before.match(/<\/el-dialog>/g) || []).length;
|
|
24
|
-
if (dialogOpens <= dialogCloses) continue;
|
|
25
|
-
|
|
26
|
-
if (footerIdx === -1 || m.index < footerIdx)
|
|
40
|
+
if (dialogOpens <= dialogCloses) continue;
|
|
41
|
+
if (inFooter(m.index)) {
|
|
27
42
|
issues.push(
|
|
28
43
|
issue(
|
|
29
44
|
file,
|
|
@@ -31,10 +46,11 @@ export const dialogRules = [
|
|
|
31
46
|
"R011",
|
|
32
47
|
"dialog",
|
|
33
48
|
"error",
|
|
34
|
-
"弹窗内 el-pagination
|
|
35
|
-
'将 el-pagination
|
|
49
|
+
"弹窗内 el-pagination 位于 #footer 插槽中(会逃出 dialog 容器漂浮在页面底部)",
|
|
50
|
+
'将 el-pagination 移到内容区,包裹 <div class="popup-pagination">;#footer 仅放操作按钮',
|
|
36
51
|
),
|
|
37
52
|
);
|
|
53
|
+
}
|
|
38
54
|
}
|
|
39
55
|
return issues;
|
|
40
56
|
},
|
package/scanner/rules/tag.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
/** scanner/rules/tag.mjs — 标签/状态规则:R009 R010 R012
|
|
1
|
+
/** scanner/rules/tag.mjs — 标签/状态规则:R009 R010 R012 R019 R020
|
|
2
|
+
* v1.8.0 重号:原 R017 → R019(与 color.mjs R017 冲突),原 R018 → R020(与 color.mjs R018 冲突)。
|
|
3
|
+
* 事实源:standards/rules.json
|
|
4
|
+
*/
|
|
2
5
|
import { lineOf, issue, findTags } from "./_shared.mjs";
|
|
3
6
|
|
|
4
7
|
/** 状态类字段名关键字(动态流转状态) */
|
|
@@ -124,15 +127,14 @@ export const tagRules = [
|
|
|
124
127
|
},
|
|
125
128
|
},
|
|
126
129
|
|
|
127
|
-
// R017
|
|
130
|
+
// R019(原 R017,v1.8.0 重号): 编号/工号/证件号列缺少 renderBadge
|
|
128
131
|
{
|
|
129
|
-
id: "
|
|
132
|
+
id: "R019",
|
|
130
133
|
category: "tag",
|
|
131
134
|
severity: "warning",
|
|
132
135
|
name: "编号/工号/证件号列缺少 renderBadge(脚本式 columnsDef)",
|
|
133
136
|
checkScript(script, file, lineOffset) {
|
|
134
137
|
const issues = [];
|
|
135
|
-
/** label 关键字:编号、工号、证件号、以及常见英文组合 */
|
|
136
138
|
const BADGE_LABEL_RE =
|
|
137
139
|
/编号|工号|证件号|驾驶员证件号|车主证件号|处置人工号/;
|
|
138
140
|
const lines = script.split("\n");
|
|
@@ -150,7 +152,7 @@ export const tagRules = [
|
|
|
150
152
|
issues.push({
|
|
151
153
|
file,
|
|
152
154
|
line: lineOffset + i + 1,
|
|
153
|
-
rule: "
|
|
155
|
+
rule: "R019",
|
|
154
156
|
category: "tag",
|
|
155
157
|
severity: "warning",
|
|
156
158
|
description: `编号列 "${label}" 未使用 renderBadge,纯文本无视觉区分`,
|
|
@@ -161,9 +163,9 @@ export const tagRules = [
|
|
|
161
163
|
},
|
|
162
164
|
},
|
|
163
165
|
|
|
164
|
-
// R018
|
|
166
|
+
// R020(原 R018,v1.8.0 重号): logicType:dict 列缺少 defaultSlot
|
|
165
167
|
{
|
|
166
|
-
id: "
|
|
168
|
+
id: "R020",
|
|
167
169
|
category: "tag",
|
|
168
170
|
severity: "warning",
|
|
169
171
|
name: "dict列缺少 defaultSlot/renderDictClassifyTag(脚本式 columnsDef)",
|
|
@@ -190,7 +192,7 @@ export const tagRules = [
|
|
|
190
192
|
issues.push({
|
|
191
193
|
file,
|
|
192
194
|
line: lineOffset + i + 1,
|
|
193
|
-
rule: "
|
|
195
|
+
rule: "R020",
|
|
194
196
|
category: "tag",
|
|
195
197
|
severity: "warning",
|
|
196
198
|
description: `"${label}" 使用 logicType:dict 但无 defaultSlot,纯文本展示枚举值`,
|
package/scripts/check-docs.mjs
CHANGED
|
@@ -145,6 +145,97 @@ for (const editor of editorConfig.editors) {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
149
|
+
// v1.8.0 新增校验:rules.json 单一事实源 + 幽灵 skill
|
|
150
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
151
|
+
const rulesJsonPath = join(root, "standards", "rules.json");
|
|
152
|
+
if (!existsSync(rulesJsonPath)) {
|
|
153
|
+
errors.push("standards/rules.json: 文件不存在(v1.8.0 起为 R-rule 单一事实源)");
|
|
154
|
+
} else {
|
|
155
|
+
const rulesData = JSON.parse(readFileSync(rulesJsonPath, "utf8"));
|
|
156
|
+
const rulesById = new Map(rulesData.rules.map((r) => [r.id, r]));
|
|
157
|
+
const aliasMap = new Map();
|
|
158
|
+
for (const r of rulesData.rules)
|
|
159
|
+
for (const a of r.aliases || []) aliasMap.set(a, r.id);
|
|
160
|
+
|
|
161
|
+
// (1) 校验 ID 唯一
|
|
162
|
+
if (rulesById.size !== rulesData.rules.length) {
|
|
163
|
+
errors.push("standards/rules.json: 存在重复 R-id");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// (2) 校验 scanner/rules/*.mjs 中所有 id: "Rxxx" 都在 rules.json 中存在且 scanner 字段一致
|
|
167
|
+
const scannerRulesDir = join(root, "scanner", "rules");
|
|
168
|
+
for (const f of readdirSync(scannerRulesDir)) {
|
|
169
|
+
if (!f.endsWith(".mjs") || f.startsWith("_") || f === "index.mjs") continue;
|
|
170
|
+
const code = readFileSync(join(scannerRulesDir, f), "utf8");
|
|
171
|
+
const idMatches = [...code.matchAll(/id:\s*["'](R\d{3})["']/g)].map(
|
|
172
|
+
(m) => m[1],
|
|
173
|
+
);
|
|
174
|
+
for (const rid of idMatches) {
|
|
175
|
+
const rule = rulesById.get(rid);
|
|
176
|
+
if (!rule) {
|
|
177
|
+
errors.push(
|
|
178
|
+
`scanner/rules/${f}: 规则 ${rid} 未在 standards/rules.json 注册`,
|
|
179
|
+
);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const expectedScanner = `scanner/rules/${f}`;
|
|
183
|
+
if (rule.scanner && rule.scanner !== expectedScanner) {
|
|
184
|
+
errors.push(
|
|
185
|
+
`standards/rules.json: ${rid}.scanner 应为 "${expectedScanner}"(当前 "${rule.scanner}")`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// (3) 校验 skills/**/SKILL.md 引用的 R-rule 必须在 rules.json 中存在
|
|
192
|
+
const skillsRoot = join(root, "skills");
|
|
193
|
+
function walkSkills(dir, list = []) {
|
|
194
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
195
|
+
const p = join(dir, e.name);
|
|
196
|
+
if (e.isDirectory()) walkSkills(p, list);
|
|
197
|
+
else if (e.name.endsWith(".md")) list.push(p);
|
|
198
|
+
}
|
|
199
|
+
return list;
|
|
200
|
+
}
|
|
201
|
+
for (const f of walkSkills(skillsRoot)) {
|
|
202
|
+
const rel = relative(root, f).replace(/\\/g, "/");
|
|
203
|
+
const content = readFileSync(f, "utf8");
|
|
204
|
+
const refs = [...new Set(
|
|
205
|
+
[...content.matchAll(/\b(R\d{3})\b/g)].map((m) => m[1]),
|
|
206
|
+
)];
|
|
207
|
+
for (const rid of refs) {
|
|
208
|
+
if (rulesById.has(rid)) continue;
|
|
209
|
+
if (aliasMap.has(rid)) continue;
|
|
210
|
+
errors.push(
|
|
211
|
+
`${rel}: 引用了未注册规则 ${rid}(应在 standards/rules.json 中定义或加 aliases)`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// (4) 校验 _registry.md 提到的 skill 目录必须真实存在
|
|
217
|
+
const registryPath = join(root, "skills", "_meta", "_registry.md");
|
|
218
|
+
if (existsSync(registryPath)) {
|
|
219
|
+
const reg = readFileSync(registryPath, "utf8");
|
|
220
|
+
const skillRefs = [
|
|
221
|
+
...reg.matchAll(/\b(element|vendors|layouts|runtime|ops)\/([a-z][a-z0-9-]*)\b/g),
|
|
222
|
+
];
|
|
223
|
+
const seen = new Set();
|
|
224
|
+
for (const m of skillRefs) {
|
|
225
|
+
const key = `${m[1]}/${m[2]}`;
|
|
226
|
+
if (seen.has(key)) continue;
|
|
227
|
+
seen.add(key);
|
|
228
|
+
const dir = join(skillsRoot, m[1], m[2]);
|
|
229
|
+
const skillFile = join(dir, "SKILL.md");
|
|
230
|
+
if (!existsSync(skillFile)) {
|
|
231
|
+
errors.push(
|
|
232
|
+
`skills/_meta/_registry.md: 引用的 skill "${key}" 不存在 (期望 ${relative(root, skillFile).replace(/\\/g, "/")})`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
148
239
|
if (errors.length > 0) {
|
|
149
240
|
console.error(
|
|
150
241
|
"docs:check failed:\n" + errors.map((error) => `- ${error}`).join("\n"),
|