@agile-team/wl-skills-ui 1.14.0 → 1.15.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 CHANGED
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.15.0] - 2026-10-07
10
+
11
+ ### Added
12
+
13
+ - 新增 task/route/explain/status/doctor-host CLI 与 5 个 MCP 工具,任务计划与真实执行回执分开;支持跨已安装适用包复用 runId,无兄弟包运行依赖。
14
+ - 新增本包独有 Codex `.agents/skills/wl-skills-ui/SKILL.md` 薄入口,按需读取 canonical Skill;缺资产或未发布显式技能报告 gap,静态入口诊断不冒充宿主实际发现。
15
+ - scan/check/fix/all 与 MCP、Vite 依赖检查记录执行/验证状态、实际检查文件、规则回调次数、规则/配置快照与过期状态。未执行、零文件、跳过或人工复核不会假绿。
16
+
17
+ ### Fixed
18
+
19
+ - 实际 Vue SFC 解析错误形成不可被 only/skip 隐藏的 SFC_PARSE 诊断;fast/fallback 不声称语法验证,脚本类型编译仍需项目独立 typecheck。
20
+ - 不适用/缺上下文任务不附加全量 UI 约束;Markdown 无问题文件占比不再称为规范覆盖率,零文件明确未检查。Vite 插件成功也明确仅验证依赖组合、源码规则未运行。
21
+
9
22
  ## [1.14.0] - 2026-10-07
10
23
 
11
24
  ### Added
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @agile-team/wl-skills-ui
2
2
 
3
- **企业级 UI 一致性与 AI 精确治理工具包 v1.14.0** — 面向 Vue 3 + Element Plus 的设计令牌、能力 Profile、样式皮肤、页面骨架、运行时能力、静态扫描、确定性修复、AI Skill、MCP 与脱敏 UI 模板契约。
3
+ **企业级 UI 一致性与 AI 精确治理工具包 v1.15.0** — 面向 Vue 3 + Element Plus 的设计令牌、能力 Profile、样式皮肤、页面骨架、运行时能力、静态扫描、确定性修复、AI Skill、MCP 与脱敏 UI 模板契约。
4
4
 
5
5
  它把“视觉统一、问题识别、机械修复、项目适配和样例沉淀”拆成可验证、可组合的工程能力。脚本、规则和 MCP 先完成确定性工作,AI 只处理业务语义与人工决策,从而减少重复读源码、上下文 token 和误修改。
6
6
 
@@ -15,13 +15,16 @@
15
15
  | 低成本治理老项目 | Skin 模式、Base*/jh-*/C_*/AG Grid 兼容层、定制页豁免 | 不改封装源码也能统一大部分视觉,避免一次性重构风险 |
16
16
  | 阻止 UI 回归 | 40 条 R-rule、接入检查、漂移基线、Git 增量扫描、CI 门禁 | 表头/内容不共轴、按钮无图标、日期弹层全屏、密集表单失衡等问题在提交前暴露 |
17
17
  | 安全机械修复 | Profile、only/skip、dry-run 计划哈希、快照、复检与回滚 | 自动化只处理可证明安全且已批准的动作,不让 AI 猜测批改 |
18
- | 降低 AI token | summary、compact-v2 分页、changed-only、Skill 路由、13 个 MCP Tool | AI 先读总览和结构化事实,只在必要时获取有界明细或局部源码 |
18
+ | 降低 AI token | summary、compact-v2 分页、changed-only、Skill 路由、18 个 MCP Tool | AI 先读总览和结构化事实,只在必要时获取有界明细或局部源码 |
19
19
  | 沉淀领域样例 | `ui-contract` extract / validate / match | 页面转为脱敏语义 JSON,不复制整页代码、接口、字段值或原始文案 |
20
20
  | 兼容多代项目 | EP 2.2+jh-ui 与原生 EP 2.7+ DOM 契约、Vite/doctor 检查 | 支持声明有 fixture 和真实浏览器证据,不靠文档口头约定 |
21
21
  | 降低运行时开销 | Shared Observer Hub、可选 guard、幂等安装与完整卸载 | 全部 guard 共用 1 个 MutationObserver + 1 个 ResizeObserver |
22
22
 
23
23
  适合多项目 Vue 3 企业后台、存量系统视觉治理、AI 辅助开发和领域页面模式沉淀。运行时最低要求:Node.js 22、Vue 3.2、Element Plus 2.2。
24
24
 
25
+
26
+ 每次任务先运行 `wl-ui task "任务" --target <path>`,按选定技能执行工具,最后用 `wl-ui status --run-id <id>` 查看实际执行与验证状态;安装或匹配不会自动变成检查通过。详见[任务判定与真实检查回执](docs/task-observability.md)。
27
+
25
28
  ## 快速开始
26
29
 
27
30
  ### 1. 安装
@@ -480,7 +483,7 @@ npx wl-ui doctor --project . --print-overrides
480
483
 
481
484
  ## 版本与进一步阅读
482
485
 
483
- 当前 v1.14.0:明确独立能力边界,安装、更新、清理按本包贡献保护用户及其他包内容;共享文件采用单文件原子替换,支持历史路由与 `.clinerules` 目录迁移。v1.13.2:R044 审查多列表单超宽标签,R040 识别自定义 `stepper`,均供人工复核。v1.13.0:新增 `native-jh-ag` Profile、显式 Profile 兼容承诺与联邦 AG 依赖识别。
486
+ 当前 v1.15.0:任务判定、真实执行/验证回执、实际检查范围和过期检测;Codex 原生薄入口按需加载 canonical Skill。确定性路由覆盖已发布目录与显式缺口,不保证识别所有自然语言。v1.14.0:明确独立能力边界,安装、更新、清理按本包贡献保护用户及其他包内容;共享文件采用单文件原子替换,支持历史路由与 `.clinerules` 目录迁移。v1.13.2:R044 审查多列表单超宽标签,R040 识别自定义 `stepper`,均供人工复核。v1.13.0:新增 `native-jh-ag` Profile、显式 Profile 兼容承诺与联邦 AG 依赖识别。
484
487
 
485
488
  - [版本摘要](docs/version-history.md)
486
489
  - [完整 Changelog](CHANGELOG.md)
package/SKILL.md CHANGED
@@ -14,6 +14,8 @@ do not reconstruct them from memory or duplicate their wording.
14
14
 
15
15
  ## Start here
16
16
 
17
+ Start every task with `npx wl-ui task "<task>" --target <path>`. Read the selected canonical Skill and required rules only after resolving ambiguous/gap/needs-context or missing own assets. A match is a plan, not evidence of execution. Use the returned `--run-id` (or `WL_TASK_RUN_ID`) for real CLI/MCP tools and finish with `npx wl-ui status --run-id <id>`. Reuse the same ID across installed applicable packages without installing unused sibling packages. Report execution and validation separately, including skipped checks and stale evidence; host discovery/reading remain unverified without host events.
18
+
17
19
  ```bash
18
20
  # Small summary first; does not modify files
19
21
  npx wl-ui scan --project . --target src --output summary --parser auto
@@ -20,6 +20,27 @@
20
20
  "runtime-renderers",
21
21
  "visual-findings"
22
22
  ]
23
+ },
24
+ {
25
+ "id": "ui.task-observability",
26
+ "intents": [
27
+ "route",
28
+ "explain",
29
+ "record-execution",
30
+ "status",
31
+ "diagnose-host"
32
+ ],
33
+ "inputs": [
34
+ "task-description",
35
+ "target-paths",
36
+ "actual-tool-results"
37
+ ],
38
+ "outputs": [
39
+ "task-decision",
40
+ "execution-receipts",
41
+ "gap-proposals",
42
+ "host-entry-diagnostics"
43
+ ]
23
44
  }
24
45
  ],
25
46
  "standalone": {
@@ -38,6 +59,15 @@
38
59
  "optionalTraceInput": "design-model",
39
60
  "unknownProtocol": "reject-or-explicit-migration",
40
61
  "apiAuthority": "validated-effective-api-manifest",
41
- "sharedWrites": "own-block-or-json-entry-only"
62
+ "sharedWrites": "own-block-or-json-entry-only",
63
+ "observability": {
64
+ "recordVersion": 1,
65
+ "schema": "bin/task-observability.schema.json",
66
+ "storage": ".wl-skills-ui/runs",
67
+ "runCorrelation": "explicit-runId-or-WL_TASK_RUN_ID",
68
+ "eventPublication": "immutable-per-package-events",
69
+ "hostDiscovery": "unverified-without-host-events",
70
+ "gapPolicy": "propose-review-before-changing-business-rules"
71
+ }
42
72
  }
43
73
  }
@@ -0,0 +1,20 @@
1
+ export const GATEWAY_PATH = ".agents/skills/wl-skills-ui/SKILL.md";
2
+
3
+ export function taskGateway() {
4
+ return `---
5
+ name: wl-skills-ui
6
+ description: Vue and Element Plus UI consistency, design tokens, styles, component layout and visual rule checks. Decide the applicable UI workflow and verify actual source files using local package tools.
7
+ ---
8
+
9
+ # wl-skills-ui task gateway
10
+ For one user task reuse the same \`--run-id\` or \`WL_TASK_RUN_ID\` across installed applicable packages. Do not install unused sibling packages or create unrelated IDs.
11
+
12
+ 1. Start each task with \`node node_modules/@agile-team/wl-skills-ui/bin/wl-ui.js task "<task>" --target <path>\`. Retain the run ID and briefly explain the decision, applicable rules and unverified items. A routing decision does not execute checks.
13
+ 2. Read \`node_modules/@agile-team/wl-skills-ui/SKILL.md\`, then only the selected canonical Skill under \`node_modules/@agile-team/wl-skills-ui/skills\`. Resolve references relative to that Skill. Honor the actual project Profile; do not infer a table migration from a Skill match.
14
+ 3. Run the actual scan/check/fix tools with the same \`--run-id\`. For a code gate use \`wl-ui scan --target <path> --fail-on-error --run-id <id>\`. An ordinary Vue edit still receives UI baseline checks; tasks outside the domain are not applicable.
15
+ 4. End with \`wl-ui status --run-id <id>\` and report execution separately from validation. Zero files, skipped checks, dependency-only checks and unresolved review items are not proof of all UI constraints passing.
16
+
17
+ Installation and model declarations do not prove host discovery or reading. \`wl-ui doctor-host\` diagnoses static entries only. Report a missing local package before attempting tools; do not silently install another package or claim success.
18
+ For ambiguous, gap, needs-context or \`ready: false\`, resolve the reported uncertainty/missing own assets before implementing the selected workflow. Routing has a published catalog and explicit unsupported-intent boundary, rather than complete natural-language coverage.
19
+ `;
20
+ }
@@ -0,0 +1,156 @@
1
+ import { readFileSync, readdirSync, existsSync, realpathSync } from "node:fs";
2
+ import { join, relative, resolve, isAbsolute } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import core from "./task-observability.cjs";
5
+ import { GATEWAY_PATH } from "./task-gateway.mjs";
6
+ import { resolveProjectProfile } from "../standards/profiles-loader.mjs";
7
+ import { getRules } from "../scanner/rules/index.mjs";
8
+
9
+ const root = resolve(import.meta.dirname, "..");
10
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
11
+ const POLICY = {
12
+ domainKeywords: ["UI", "Vue", "前端", "样式", "布局", "颜色", "视觉", "Element Plus", "按钮", "表格", "表单"],
13
+ domainExtensions: [".vue", ".scss", ".css"], negativeKeywords: ["写诗", "诗歌", "旅游", "旅行", "天气", "做饭"],
14
+ baselineRules: ["R001", "R006", "R043"], baselineChecks: ["scan"], minimumScore: 1, minimumMargin: 1,
15
+ unsupportedIntents: ["生成图像", "生成图片", "Figma设计稿导出"],
16
+ };
17
+ const TRIGGERS = {
18
+ "element/el-table": ["表格", "table", "el-table"], "element/el-form": ["表单", "查询表单", "el-form"],
19
+ "element/el-dialog": ["弹窗", "dialog", "el-dialog"], "element/el-tag": ["标签颜色", "状态标签", "el-tag"],
20
+ "element/component-family": ["卡片", "页签", "抽屉", "el-tabs", "上传控件"],
21
+ "runtime/design-tokens": ["设计令牌", "design tokens", "颜色硬编码", "tokens"],
22
+ "runtime/style-align": ["样式统一", "风格对齐", "样式不生效"], "runtime/migration": ["runtime迁移", "样式迁移"],
23
+ "ops/scan": ["UI扫描", "UI检查", "样式扫描"], "ops/audit": ["UI审计", "视觉审计"],
24
+ "ops/fix": ["UI修复", "修复样式", "统一视觉"], "ops/migrate": ["UI迁移", "迁移tokens"],
25
+ "layouts/list-page": ["列表布局"], "layouts/detail-page": ["详情布局"], "layouts/form-dialog": ["表单弹窗布局"], "layouts/tree-list": ["树列表布局"],
26
+ "vendors/ag-grid": ["AG Grid", "ag-grid"], "vendors/base-table": ["BaseTable"], "vendors/jh-components": ["jh组件"],
27
+ "vendors/c-components": ["C组件"], "vendors/custom-wrappers": ["自定义封装组件"], "vendors/unknown-wrapper": ["未知封装组件"],
28
+ };
29
+
30
+ function catalogFiles(dir, result = []) {
31
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
32
+ if (entry.name.startsWith("_")) continue;
33
+ const full = join(dir, entry.name);
34
+ if (entry.isDirectory()) catalogFiles(full, result);
35
+ else if (entry.name === "SKILL.md") result.push(full);
36
+ }
37
+ return result;
38
+ }
39
+
40
+ export function taskCatalog() {
41
+ return catalogFiles(join(root, "skills")).map((file) => {
42
+ const id = relative(join(root, "skills"), file).replace(/\\/g, "/").replace(/\/SKILL\.md$/, "");
43
+ const content = readFileSync(file, "utf8");
44
+ return { id, path: `node_modules/${pkg.name}/skills/${id}/SKILL.md`, description: content.match(/^# (.+)$/m)?.[1] || id, triggers: TRIGGERS[id] || [], rules: POLICY.baselineRules, checks: ["scan"], status: "enabled" };
45
+ });
46
+ }
47
+
48
+ export function taskOptions(projectRoot, extra = {}) {
49
+ const actualRoot = realpathSync(resolve(projectRoot));
50
+ return { projectRoot: actualRoot, packageName: pkg.name, packageVersion: pkg.version, storageDir: ".wl-skills-ui/runs",
51
+ ruleFiles: projectEvidencePaths(actualRoot, [join(root, "standards/rules.json"), join(root, "scanner/rules")]),
52
+ configFiles: projectEvidencePaths(actualRoot, ["package.json", ".wl-ui-profile.json", ".wl-exempt.json"].map((rel) => join(actualRoot, rel))), ...extra };
53
+ }
54
+
55
+ function projectEvidencePaths(projectRoot, candidates) {
56
+ return candidates.filter((candidate) => existsSync(candidate)).map((candidate) => relative(projectRoot, realpathSync(candidate)).replace(/\\/g, "/"))
57
+ .filter((rel) => rel && !rel.startsWith("../") && !isAbsolute(rel));
58
+ }
59
+
60
+ export function decideTask(input) {
61
+ const catalog = taskCatalog();
62
+ const decision = core.evaluateTask({ ...input, catalog, policy: POLICY });
63
+ if (input.skill && !catalog.some((skill) => skill.id === input.skill)) return capabilityGap(decision, `本包未发布专项 Skill:${input.skill}`);
64
+ if (POLICY.unsupportedIntents.some((intent) => String(input.task).includes(intent))) return capabilityGap(decision, "本包没有图像生成或设计稿导出的执行器");
65
+ return decision;
66
+ }
67
+
68
+ function capabilityGap(decision, reason) {
69
+ return { ...decision, routingStatus: decision.status, status: "gap", applicable: true, ready: false, selectedSkills: [], requiredFiles: [], reasons: [...decision.reasons, reason], reason, gaps: [{ reason, suggestion: "提交本包专项技能/执行器提案并审阅验证,不自动修改技能或安装兄弟包" }] };
70
+ }
71
+
72
+ export function runTaskAction(action, input = {}) {
73
+ const opts = taskOptions(input.projectRoot || process.env.WL_PROJECT_ROOT || process.cwd(), { runId: input.runId, targets: input.targets || [] });
74
+ if (action === "status") return core.readStatus(opts);
75
+ if (action === "doctor-host") return hostDiagnostic({ ...opts, host: input.host || "unknown" });
76
+ const profile = resolveProjectProfile({ projectRoot: opts.projectRoot, profile: input.profile }).profile;
77
+ const routed = decideTask({ task: input.task || "", targets: opts.targets, skill: input.skill });
78
+ const decision = { ...routed, profile: profile.id, applicableConstraints: routed.applicable === true ? getRules({ profile: profile.id }).map((rule) => rule.id) : [] };
79
+ assetReadiness(decision, opts.projectRoot);
80
+ if (action !== "task") return decision;
81
+ return { ...decision, ...core.startTask({ ...opts, task: input.task || "", decision }) };
82
+ }
83
+
84
+ function hostDiagnostic(opts) {
85
+ const entries = new Map([["claude", ["CLAUDE.md"]], ["claude-code", ["CLAUDE.md"]], ["copilot", [".github/instructions/wk-skills/ops-scan.instructions.md"]], ["github-copilot", [".github/instructions/wk-skills/ops-scan.instructions.md"]], ["cursor", [".cursor/rules/ops-scan.mdc"]]]);
86
+ return core.doctorHost({ ...opts, gatewayPath: GATEWAY_PATH, entryFiles: entries.get(opts.host) || ["AGENTS.md"], skillPaths: [`node_modules/${pkg.name}/SKILL.md`] });
87
+ }
88
+
89
+ function assetReadiness(decision, projectRoot) {
90
+ if (decision.applicable !== true) return;
91
+ const required = [...decision.requiredFiles, `node_modules/${pkg.name}/SKILL.md`, `node_modules/${pkg.name}/standards/rules.json`];
92
+ const missing = required.filter((rel) => !existsSync(join(projectRoot, rel)));
93
+ decision.ready = decision.ready !== false && missing.length === 0;
94
+ decision.missingInputs = missing;
95
+ if (missing.length) {
96
+ decision.routingStatus ||= decision.status;
97
+ decision.status = "gap";
98
+ decision.reasons.push("本包所需 canonical Skill/规则入口未安装到目标项目");
99
+ decision.reason = decision.reasons.join(";");
100
+ decision.gaps.push({ reason: "本包所需 canonical Skill/规则入口未安装到目标项目", paths: missing, suggestion: "安装/更新本包项目依赖及本包入口后重新判定;不需要安装兄弟包" });
101
+ }
102
+ }
103
+
104
+ export function taskCli(action, args) {
105
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
106
+ target: { type: "string", multiple: true }, project: { type: "string" }, text: { type: "string" },
107
+ "run-id": { type: "string" }, host: { type: "string" }, skill: { type: "string" }, profile: { type: "string" }, json: { type: "boolean" },
108
+ } });
109
+ const result = runTaskAction(action, { projectRoot: values.project, task: values.text || positionals.join(" "), targets: values.target, runId: values["run-id"], host: values.host, profile: values.profile, skill: values.skill });
110
+ if (values.json || action === "doctor-host") console.log(JSON.stringify(result));
111
+ else if (action === "status") console.log(core.formatStatus(result));
112
+ else console.log(`${core.formatDecision(result)}${result.runId ? `\nrunId: ${result.runId}` : ""}`);
113
+ return result;
114
+ }
115
+
116
+ export function beginUiCheck(projectRoot, targets, tool, runId, extra = {}) {
117
+ const opts = taskOptions(projectRoot, { targets, tool, runId, readOnlyVerification: true, ...extra });
118
+ return { opts, handle: core.beginExecution(opts) };
119
+ }
120
+
121
+ export function finishUiCheck(execution, facts) {
122
+ const receipt = core.finishExecution(execution.handle, facts);
123
+ return { runId: receipt.runId, executionStatus: receipt.executionStatus, validationStatus: receipt.validationStatus, receiptPath: receipt.recordPath };
124
+ }
125
+
126
+ export function scanFacts(result, issues = result.issues || result.allIssues) {
127
+ const checked = result.fileCount - result.exemptFileCount;
128
+ const errors = issues.filter((issue) => issue.severity === "error").length;
129
+ return {
130
+ exitCode: 0, validationStatus: checked > 0 ? errors > 0 ? "failed" : "passed" : "unverified",
131
+ checkedFiles: result.checkedFiles || [],
132
+ checks: checked > 0 ? [{ id: "scan", status: errors > 0 ? "failed" : "passed", reason: `${checked} non-exempt Vue files actually scanned` }, parserFact(result.parsing), ...result.ruleExecutions.map((item) => ruleFact(item, issues))] : [{ id: "scan", status: "skipped", reason: "No non-exempt Vue file scanned" }],
133
+ summary: { files: result.fileCount, checkedFiles: checked, exemptFiles: result.exemptFileCount, errors, parsing: result.parsing, rules: result.ruleExecutions },
134
+ };
135
+ }
136
+
137
+ export function integrationFacts(checks, exitCode = 0) {
138
+ return { exitCode, validationStatus: checks.some((check) => !check.ok) ? "failed" : "passed", checks: checks.map((check) => ({ id: check.id, status: check.ok ? "passed" : "failed", reason: check.description })), summary: { scope: "dependency-and-runtime-integration-only", sourceRulesVerified: false } };
139
+ }
140
+
141
+ function parserFact(parsing) {
142
+ if (parsing.errors?.length) return { id: "sfc-parse", status: "failed", reason: `${parsing.errors.length} actual compiler parse errors` };
143
+ if (parsing.used.fast > 0) return { id: "sfc-parse", status: "skipped", reason: "Fast parser extracts blocks; it does not validate Vue syntax" };
144
+ return { id: "sfc-parse", status: "passed", reason: "Actual compiler SFC parse; script type correctness is outside this check" };
145
+ }
146
+
147
+ function ruleFact(item, issues) {
148
+ if (!item.calls) return { id: item.id, status: "skipped", reason: "No matching rule callback invoked" };
149
+ const active = issues.filter((issue) => issue.rule === item.id);
150
+ if (active.some((issue) => issue.severity === "error")) return { id: item.id, status: "failed", reason: `${item.calls} actual callbacks; active errors` };
151
+ if (item.errors > 0) return { id: item.id, status: "not-applicable", reason: "Callbacks executed; detected errors excluded from requested result by filters/exemptions" };
152
+ if (active.some((issue) => issue.severity === "review")) return { id: item.id, status: "unverified", reason: "Rule requires business/human review" };
153
+ return { id: item.id, status: "passed", reason: `${item.calls} actual callbacks; no active error; semantic applicability not asserted` };
154
+ }
155
+
156
+ export { core };