@agile-team/wl-skills-ui 1.7.1 → 1.8.1
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 +32 -0
- package/README.md +10 -1
- package/docs/governance-long-term.md +191 -0
- package/mcp/server.js +130 -0
- package/package.json +1 -1
- package/scanner/drift.mjs +179 -0
- package/scanner/index.mjs +25 -0
- 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,38 @@ 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.1] - 2026-05-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 新增 **`scanner/drift.mjs`** 漂移检测模块:对比基线与当前扫描 JSON,输出 gained / fixed / regressed 结构化报告。
|
|
12
|
+
- 新增 CLI `wl-scan drift --baseline <f> --current <f>` 子命令,支持 `--output json` 和 `--fail-on-error` 门槛。
|
|
13
|
+
- 新增 MCP 工具 **`wl_ui_drift`**:AI 可直接传入两份扫描 JSON 拿漂移报告。
|
|
14
|
+
|
|
15
|
+
## [1.8.0] - 2026-05-12
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- 新增 **`standards/rules.json`** R-rule 单一事实源:29 条规则按 `id / category / severity / appliesTo / autoFixable / scanner / skills` 结构化注册,所有 standards 文档、SKILL.md、scanner、MCP、未来 ESLint 插件均从此派生。
|
|
20
|
+
- 新增 **`standards/rules-loader.mjs`** 共享加载器:暴露 `loadRules / listRules / getRule / groupByCategory / buildRuleSummary`,scanner / MCP / check-docs / Vite 插件统一读取。
|
|
21
|
+
- 新增 MCP 工具 **`wl_ui_list_rules`**:按 `category / severity / autoFixable` 过滤返回规则摘要。
|
|
22
|
+
- 新增 MCP 工具 **`wl_ui_describe_rule`**:按 ID 返回单条 R-rule 完整定义(含 aliases 兼容旧 ID)。
|
|
23
|
+
- 新增 **`docs/governance-long-term.md`**:业务项目长效治理方案(基线 / 豁免 / 漂移看板 / 版本钉死 / 写作期 AI 守护五机制)。
|
|
24
|
+
- `scripts/check-docs.mjs` 扩展:校验 `scanner/rules/*.mjs` 中所有 `id` 必须在 `rules.json` 注册、SKILL.md 引用的 R-id 必须存在、`_registry.md` 引用的 skill 目录必须真实存在。
|
|
25
|
+
|
|
26
|
+
### Changed (Breaking-ish)
|
|
27
|
+
|
|
28
|
+
- **修复 R011 逻辑反转 bug**:之前 scanner 检测分页"不在 #footer 报错",与 `standards/ui/05` "分页必须放内容区,不得放 #footer" 相互矛盾。v1.8.0 反转 scanner 检测语义,与 standards 对齐。
|
|
29
|
+
- **`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。
|
|
30
|
+
- **`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。
|
|
31
|
+
- **`skills/runtime/style-align/SKILL.md` 改为指针式引用**:不再复述 17 条 R-rule 内容,仅保留分类→编号→SKILL 映射表,规则细节统一查 `standards/rules.json` 或 `wl_ui_describe_rule`。
|
|
32
|
+
- `tsup.config.ts` `clean: true`:每次构建清空 `es/` 目录,避免 hash 残留。
|
|
33
|
+
|
|
34
|
+
### Notes
|
|
35
|
+
|
|
36
|
+
- R013(Upload 嵌入 operations[])由文档约束晋升为 `rules.json` 正式条目(`severity: review`,无 scanner 实现)。
|
|
37
|
+
- 推荐业务项目跟进:跑一次 `npx wl-ui audit --target src --outFile .wl-baseline.json` 建立基线,配合 `--baseline` 增量门槛使用(详见 `docs/governance-long-term.md`)。
|
|
38
|
+
|
|
7
39
|
## [1.7.1] - 2026-05-12
|
|
8
40
|
|
|
9
41
|
### 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.1 版本完成 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,60 @@ 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
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "wl_ui_drift",
|
|
138
|
+
description:
|
|
139
|
+
"对比基线与当前扫描 JSON,输出漂移报告(gained / fixed / regressed)。用于 PR 级增量门槛。",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
baselineJson: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description: "基线扫描 JSON 字符串(.wl-baseline.json 的内容)",
|
|
146
|
+
},
|
|
147
|
+
currentJson: {
|
|
148
|
+
type: "string",
|
|
149
|
+
description: "当前扫描 JSON 字符串",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
required: ["baselineJson", "currentJson"],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
101
155
|
{
|
|
102
156
|
name: "wl_ui_recommend_flow",
|
|
103
157
|
description:
|
|
@@ -409,6 +463,82 @@ async function dispatchTool(id, name, args) {
|
|
|
409
463
|
});
|
|
410
464
|
return;
|
|
411
465
|
}
|
|
466
|
+
if (name === "wl_ui_list_rules") {
|
|
467
|
+
const loader = await import("../standards/rules-loader.mjs");
|
|
468
|
+
const rules = loader.listRules({
|
|
469
|
+
category: args.category,
|
|
470
|
+
severity: args.severity,
|
|
471
|
+
autoFixable: args.autoFixable,
|
|
472
|
+
});
|
|
473
|
+
const summary = rules.map((r) => ({
|
|
474
|
+
id: r.id,
|
|
475
|
+
severity: r.severity,
|
|
476
|
+
category: r.category,
|
|
477
|
+
title: r.title,
|
|
478
|
+
autoFixable: !!r.autoFixable,
|
|
479
|
+
}));
|
|
480
|
+
sendResult(id, {
|
|
481
|
+
content: [
|
|
482
|
+
{
|
|
483
|
+
type: "text",
|
|
484
|
+
text: JSON.stringify(
|
|
485
|
+
{ total: summary.length, rules: summary },
|
|
486
|
+
null,
|
|
487
|
+
2,
|
|
488
|
+
),
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (name === "wl_ui_describe_rule") {
|
|
495
|
+
const loader = await import("../standards/rules-loader.mjs");
|
|
496
|
+
const rule = loader.getRule(args.id);
|
|
497
|
+
if (!rule) {
|
|
498
|
+
sendResult(id, {
|
|
499
|
+
content: [
|
|
500
|
+
{
|
|
501
|
+
type: "text",
|
|
502
|
+
text: JSON.stringify(
|
|
503
|
+
{ ok: false, reason: `规则 ${args.id} 未注册` },
|
|
504
|
+
null,
|
|
505
|
+
2,
|
|
506
|
+
),
|
|
507
|
+
},
|
|
508
|
+
],
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
sendResult(id, {
|
|
513
|
+
content: [{ type: "text", text: JSON.stringify(rule, null, 2) }],
|
|
514
|
+
});
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (name === "wl_ui_drift") {
|
|
518
|
+
const { drift, formatDriftJson } = await import("../scanner/drift.mjs");
|
|
519
|
+
try {
|
|
520
|
+
const baseline = JSON.parse(args.baselineJson);
|
|
521
|
+
const current = JSON.parse(args.currentJson);
|
|
522
|
+
const result = drift(baseline, current);
|
|
523
|
+
sendResult(id, {
|
|
524
|
+
content: [{ type: "text", text: formatDriftJson(result) }],
|
|
525
|
+
});
|
|
526
|
+
} catch (e) {
|
|
527
|
+
sendResult(id, {
|
|
528
|
+
content: [
|
|
529
|
+
{
|
|
530
|
+
type: "text",
|
|
531
|
+
text: JSON.stringify(
|
|
532
|
+
{ ok: false, reason: String(e.message) },
|
|
533
|
+
null,
|
|
534
|
+
2,
|
|
535
|
+
),
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
412
542
|
if (name === "wl_ui_recommend_flow") {
|
|
413
543
|
sendResult(id, {
|
|
414
544
|
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.1",
|
|
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",
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scanner/drift.mjs — 漂移检测
|
|
3
|
+
*
|
|
4
|
+
* 对比 baseline(基线 JSON)与 current(当前扫描 JSON),输出结构化漂移报告。
|
|
5
|
+
* 指纹策略:file + rule(行号不稳定,不参与指纹)。同一 file+rule 多条则按数量 diff。
|
|
6
|
+
*
|
|
7
|
+
* 用法:
|
|
8
|
+
* import { drift, formatDriftText } from '@agile-team/wl-skills-ui/scanner/drift.mjs'
|
|
9
|
+
* const result = drift(baselineJson, currentJson)
|
|
10
|
+
* console.log(formatDriftText(result))
|
|
11
|
+
*
|
|
12
|
+
* CLI(由 scanner/index.mjs 调用):
|
|
13
|
+
* npx wl-ui drift --baseline .wl-baseline.json --current .wl-current.json
|
|
14
|
+
*
|
|
15
|
+
* 事实源:standards/rules.json
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
18
|
+
|
|
19
|
+
// ── 指纹 ────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function fingerprint(issue) {
|
|
22
|
+
return `${issue.file}\x00${issue.rule}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildCountMap(issues) {
|
|
26
|
+
const map = new Map();
|
|
27
|
+
for (const iss of issues) {
|
|
28
|
+
const key = fingerprint(iss);
|
|
29
|
+
map.set(key, (map.get(key) || 0) + 1);
|
|
30
|
+
}
|
|
31
|
+
return map;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── 核心 diff ───────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {object} baseline JSON 扫描结果(含 issues 数组)
|
|
38
|
+
* @param {object} current JSON 扫描结果(含 issues 数组)
|
|
39
|
+
* @returns {DriftResult}
|
|
40
|
+
*/
|
|
41
|
+
export function drift(baseline, current) {
|
|
42
|
+
const baseIssues = baseline.issues || [];
|
|
43
|
+
const curIssues = current.issues || [];
|
|
44
|
+
const baseMap = buildCountMap(baseIssues);
|
|
45
|
+
const curMap = buildCountMap(curIssues);
|
|
46
|
+
|
|
47
|
+
const allKeys = new Set([...baseMap.keys(), ...curMap.keys()]);
|
|
48
|
+
|
|
49
|
+
const gained = []; // 新增(当前有、基线无 或 当前 > 基线)
|
|
50
|
+
const fixed = []; // 消化(基线有、当前无 或 当前 < 基线)
|
|
51
|
+
const regressed = []; // 回归预留(后续可扩展 fix-then-break 检测)
|
|
52
|
+
|
|
53
|
+
for (const key of allKeys) {
|
|
54
|
+
const [file, rule] = key.split("\x00");
|
|
55
|
+
const baseCount = baseMap.get(key) || 0;
|
|
56
|
+
const curCount = curMap.get(key) || 0;
|
|
57
|
+
const delta = curCount - baseCount;
|
|
58
|
+
if (delta > 0) {
|
|
59
|
+
gained.push({ file, rule, delta, baseCount, curCount });
|
|
60
|
+
} else if (delta < 0) {
|
|
61
|
+
fixed.push({ file, rule, delta, baseCount, curCount });
|
|
62
|
+
}
|
|
63
|
+
// delta === 0 → 无变化,跳过
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 按规则 & 目录 聚合
|
|
67
|
+
const byRule = aggregate(gained, "rule");
|
|
68
|
+
const byDir = aggregateDir(gained);
|
|
69
|
+
const fixedByRule = aggregate(fixed, "rule");
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
baselineTotal: baseIssues.length,
|
|
73
|
+
currentTotal: curIssues.length,
|
|
74
|
+
netDelta: curIssues.length - baseIssues.length,
|
|
75
|
+
gained: { count: sumDelta(gained), items: gained },
|
|
76
|
+
fixed: { count: Math.abs(sumDelta(fixed)), items: fixed },
|
|
77
|
+
regressed: { count: 0, items: regressed },
|
|
78
|
+
topGainedByRule: sortDesc(byRule).slice(0, 5),
|
|
79
|
+
topGainedByDir: sortDesc(byDir).slice(0, 5),
|
|
80
|
+
topFixedByRule: sortDesc(fixedByRule).slice(0, 5),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sumDelta(arr) {
|
|
85
|
+
return arr.reduce((s, i) => s + Math.abs(i.delta), 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function aggregate(items, field) {
|
|
89
|
+
const map = new Map();
|
|
90
|
+
for (const it of items) {
|
|
91
|
+
const key = it[field];
|
|
92
|
+
map.set(key, (map.get(key) || 0) + Math.abs(it.delta));
|
|
93
|
+
}
|
|
94
|
+
return [...map.entries()].map(([key, count]) => ({ key, count }));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function aggregateDir(items) {
|
|
98
|
+
const map = new Map();
|
|
99
|
+
for (const it of items) {
|
|
100
|
+
const parts = it.file.split("/");
|
|
101
|
+
const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : ".";
|
|
102
|
+
map.set(dir, (map.get(dir) || 0) + Math.abs(it.delta));
|
|
103
|
+
}
|
|
104
|
+
return [...map.entries()].map(([key, count]) => ({ key, count }));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sortDesc(arr) {
|
|
108
|
+
return arr.sort((a, b) => b.count - a.count);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── 文本格式化 ──────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export function formatDriftText(result) {
|
|
114
|
+
const lines = [];
|
|
115
|
+
lines.push("╭──────────────────────────────────────╮");
|
|
116
|
+
lines.push("│ wl-skills-ui 漂移报告 │");
|
|
117
|
+
lines.push("╰──────────────────────────────────────╯");
|
|
118
|
+
lines.push("");
|
|
119
|
+
lines.push(`基线违规: ${result.baselineTotal} → 当前违规: ${result.currentTotal} (${result.netDelta >= 0 ? "+" : ""}${result.netDelta})`);
|
|
120
|
+
lines.push("");
|
|
121
|
+
lines.push(` 🔴 gained +${result.gained.count} 新增违规`);
|
|
122
|
+
lines.push(` 🟢 fixed -${result.fixed.count} 消化历史`);
|
|
123
|
+
if (result.regressed.count > 0) {
|
|
124
|
+
lines.push(` 🟡 regressed +${result.regressed.count} 回归`);
|
|
125
|
+
}
|
|
126
|
+
lines.push("");
|
|
127
|
+
|
|
128
|
+
if (result.topGainedByRule.length > 0) {
|
|
129
|
+
lines.push("新增 Top 规则:");
|
|
130
|
+
for (const { key, count } of result.topGainedByRule) {
|
|
131
|
+
lines.push(` ${key} +${count}`);
|
|
132
|
+
}
|
|
133
|
+
lines.push("");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (result.topGainedByDir.length > 0) {
|
|
137
|
+
lines.push("新增 Top 目录:");
|
|
138
|
+
for (const { key, count } of result.topGainedByDir) {
|
|
139
|
+
lines.push(` ${key} +${count}`);
|
|
140
|
+
}
|
|
141
|
+
lines.push("");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (result.topFixedByRule.length > 0) {
|
|
145
|
+
lines.push("消化 Top 规则:");
|
|
146
|
+
for (const { key, count } of result.topFixedByRule) {
|
|
147
|
+
lines.push(` ${key} -${count}`);
|
|
148
|
+
}
|
|
149
|
+
lines.push("");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── JSON 格式化 ─────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
export function formatDriftJson(result) {
|
|
158
|
+
return JSON.stringify(result, null, 2);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── 文件级便捷 API ──────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 从两个 JSON 文件路径生成漂移报告
|
|
165
|
+
* @param {string} baselinePath
|
|
166
|
+
* @param {string} currentPath
|
|
167
|
+
* @returns {DriftResult}
|
|
168
|
+
*/
|
|
169
|
+
export function driftFromFiles(baselinePath, currentPath) {
|
|
170
|
+
if (!existsSync(baselinePath)) {
|
|
171
|
+
throw new Error(`基线文件不存在: ${baselinePath}`);
|
|
172
|
+
}
|
|
173
|
+
if (!existsSync(currentPath)) {
|
|
174
|
+
throw new Error(`当前扫描结果不存在: ${currentPath}`);
|
|
175
|
+
}
|
|
176
|
+
const baseline = JSON.parse(readFileSync(baselinePath, "utf8"));
|
|
177
|
+
const current = JSON.parse(readFileSync(currentPath, "utf8"));
|
|
178
|
+
return drift(baseline, current);
|
|
179
|
+
}
|
package/scanner/index.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* wl-scan fix --target <path> --dry-run
|
|
12
12
|
* wl-scan all --project <path> # 接入检查 + 风格扫描 + 报告
|
|
13
13
|
* wl-scan init # 打印接入指引
|
|
14
|
+
* wl-scan drift --baseline <f> --current <f> # 漂移检测
|
|
14
15
|
* wl-scan snapshot list # 列出快照
|
|
15
16
|
* wl-scan snapshot rollback [--id <id>] # 回退到快照(默认最新)
|
|
16
17
|
* wl-scan snapshot diff [--id <id>] # 查看快照与当前差异
|
|
@@ -43,6 +44,7 @@ const SUBCOMMANDS = new Set([
|
|
|
43
44
|
"fix",
|
|
44
45
|
"all",
|
|
45
46
|
"init",
|
|
47
|
+
"drift",
|
|
46
48
|
"snapshot",
|
|
47
49
|
]);
|
|
48
50
|
let subcommand = "scan";
|
|
@@ -67,6 +69,8 @@ const { values } = parseArgs({
|
|
|
67
69
|
keep: { type: "string", default: "5" },
|
|
68
70
|
"no-snapshot": { type: "boolean", default: false },
|
|
69
71
|
exempt: { type: "string", default: "" },
|
|
72
|
+
baseline: { type: "string", default: "" },
|
|
73
|
+
current: { type: "string", default: "" },
|
|
70
74
|
},
|
|
71
75
|
strict: false,
|
|
72
76
|
});
|
|
@@ -263,6 +267,27 @@ if (subcommand === "check") {
|
|
|
263
267
|
process.exit(values["fail-on-error"] && hasError ? 1 : 0);
|
|
264
268
|
}
|
|
265
269
|
|
|
270
|
+
if (subcommand === "drift") {
|
|
271
|
+
const { driftFromFiles, formatDriftText, formatDriftJson } =
|
|
272
|
+
await import("./drift.mjs");
|
|
273
|
+
const baselinePath = resolve(values.baseline || ".wl-baseline.json");
|
|
274
|
+
const currentPath = resolve(values.current);
|
|
275
|
+
if (!values.current) {
|
|
276
|
+
console.error(
|
|
277
|
+
"用法: wl-scan drift --baseline .wl-baseline.json --current .wl-current.json",
|
|
278
|
+
);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
const result = driftFromFiles(baselinePath, currentPath);
|
|
282
|
+
if (values.output === "json") {
|
|
283
|
+
console.log(formatDriftJson(result));
|
|
284
|
+
} else {
|
|
285
|
+
console.log(formatDriftText(result));
|
|
286
|
+
}
|
|
287
|
+
const hasNew = result.gained.count > 0;
|
|
288
|
+
process.exit(values["fail-on-error"] && hasNew ? 1 : 0);
|
|
289
|
+
}
|
|
290
|
+
|
|
266
291
|
if (subcommand === "snapshot") {
|
|
267
292
|
const projectRoot = resolve(values.project);
|
|
268
293
|
const sub = args[0] || "list";
|