@agile-team/wl-skills-ui 1.18.0 → 1.20.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
@@ -1,4 +1,21 @@
1
- # Changelog
1
+ # Changelog
2
+
3
+ ## [1.20.0] - 2026-10-11
4
+
5
+ - scan 严格校验已注册规则、范围与参数,CLI/MCP 使用同源选择器;非法请求零检查零任务记录。
6
+ - 合法 pnpm/npm-link 本包规则与执行器纳入新鲜度证据,不扩展兄弟包或业务路径权限。
7
+ - 解释、方案/步骤、背景引用与禁止检查的动作判定收敛;任务意图仍不授予业务写入权限。
8
+ - notice 与 result.integration 暴露实际版本对齐,漂移返回 gap/ready=false;只读任务输入变化需重新判定,修改任务允许同 runId 复扫。
9
+ - 本包外部依赖的只读证据核实身份与真实边界,累计快照预算和 Schema 同步,保持零兄弟包/Harness 运行时依赖。
10
+
11
+
12
+ ## [1.19.0] - 2026-10-11
13
+
14
+ - 区分当前请求、否定动作与历史引用,增加动作模式;解释/规划不自动检查,意图不作为业务写入授权。
15
+ - 公开协议保持 v1 和原有字段,增加带 Schema 的 result.integration、规则身份与包自有只读执行器目录。
16
+ - 编辑器 gateway 展示真实动作与规则回执;补充独立包语料、统一契约和执行边界回归,无兄弟包/Harness 依赖。
17
+ - 触发词迁至独立目录并双向核验,明确 scan 样式检查与 check 接入检查的职责。
18
+
2
19
 
3
20
  ## [1.18.0] - 2026-10-11
4
21
 
package/README.md CHANGED
@@ -510,7 +510,7 @@ npx wl-ui doctor --project . --print-overrides
510
510
 
511
511
  ## 版本与进一步阅读
512
512
 
513
- 当前 v1.18.0:公开集成协议 describe/request 统一信封(能力清单与路由语料经真实打包产物验证)、输入校验收口与 MCP 目录对齐真实 tools/list。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 依赖识别。
513
+ 当前 v1.20.0:公开集成协议 describe/request 统一信封(能力清单与路由语料经真实打包产物验证)、输入校验收口与 MCP 目录对齐真实 tools/list。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 依赖识别。
514
514
 
515
515
  - [版本摘要](docs/version-history.md)
516
516
  - [完整 Changelog](CHANGELOG.md)
@@ -532,3 +532,23 @@ npm run pack:dry
532
532
  ```
533
533
 
534
534
  视觉测试基线按操作系统管理;如果本机没有对应平台截图,完整 `test:visual` 会明确报告缺失,不应自动生成并提交一套未经评审的新基线。发布门禁使用跨浏览器计算样式契约 `test:visual:contracts`;真实浏览器兼容契约也可以单独执行 `browser-compat.spec.ts`。
535
+
536
+ ### 当前请求、动作边界与 Harness 集成
537
+
538
+ 任务判定保留原始文本,并将当前请求、历史引用和明确否定分开。自然语言判定覆盖已公开语料与规则,不能承诺任意表述都自动命中;遇到歧义或缺口应补充目标/领域或显式指定本包能力,不能静默选择。编辑前显示真实 `notice`,包括版本、技能、规则、目标、runId、动作与待检查项。
539
+
540
+ `protocol describe --json` 声明 `task-intent-v1`、`result-core-v1`、`executor-catalog-v1`;`schemas.resultCore` 与五操作 `schemas.results` 可供宿主验证。原有结果字段保留,新增 `result.integration`:项目适用范围、动作模式、规则身份、目标、就绪状态、允许执行器和真实执行/验证状态。`ok` 只表示调用成功。未声明使用的输入字段进入 diagnostics,不会转成授权依据。
541
+
542
+ `action.mode` 为 explain/plan/inspect/apply/unspecified;解释和规划不自动启动检查。`businessWritesAuthorized=false`,任务意图不能替代业务写入授权。`ruleRefs` 为包自有规则身份(包名、规则 ID、版本、来源、性质),自然语言约束有稳定 ID;原规则名称与列表保留。所有规则的机械/语义验证仍以实际回执为准。
543
+
544
+ `inventory.executors` 声明本包业务只读检查器、目标与参数 Schema;其适用条件由本包判定。外部适配器须核对同项目、同包、同版本、同 runId 的真实计划、新鲜度与允许动作,指定一个目标,不能扩大范围;支持规则子集的执行器不得选择计划外规则。多目标需拆分调用。五包和 Harness 均为可选组合,不新增兄弟包或 Harness 运行时依赖;卸下适配器后原 CLI/MCP 继续独立使用。
545
+
546
+ 样式规则检查使用 `scan`(支持计划内规则子集);`check` 只核验接入完整性。技能触发词由本包 `bin/task-triggers.json` 维护并接受目录一致性检查。
547
+
548
+ `scan/audit/fix` 的未知参数、重复值选项、空值、多余位置参数和不存在的规则选择器在检查或写入前拒绝,机读输出返回 `invalid-input`,不会形成成功检查记录。CLI 与 MCP 共用注册规则选择器,支持单个 ID、逗号列表与三位编号范围(例如 `R031-R037`);用 `wl-ui rules list` 核对实际规则。Vue 解析错误仍不能被 `--only/--skip` 隐藏。`wl-ui scan --help` 只输出帮助,不启动扫描。
549
+
550
+ ### 任务反馈与执行边界
551
+
552
+ 编辑前展示实际 `notice` 的包版本、判定、动作、技能/规则、目标、runId、待检查项与版本对齐信息;解释/规划和明确禁止检查时不自动执行检查。`runtime=mismatch/invalid` 或 `planStale=true` 必须先同步本包或重新判定。只读 `inspect` 输入变化后重新 `task`;`apply` 完成正常业务修改后可用同 runId 做实际复扫,但规则、配置或版本变化仍需重新判定。
553
+
554
+ 单包只读取自身公开能力;多包复用同 runId 并各存各的证据。`matched/ready/ok=true` 仅说明判定或调用成功,是否完成以实际 `executionStatus/validationStatus/checkedFiles` 为准。未接入或不适用的项目不借父目录规则;宿主发现、模型读取、反馈展示没有真实证据时继续标记未验证。
@@ -0,0 +1,42 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { appendFileSync, cpSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join, relative, resolve } from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+
8
+ const repo = resolve(import.meta.dirname, "../..");
9
+ test("external workspace store keeps own rule and execution evidence fresh or explicitly stale", () => {
10
+ const temporary = realpathSync(mkdtempSync(join(tmpdir(), "ui-external-store-")));
11
+ const project = join(temporary, "workspace/apps/admin");
12
+ const physical = join(temporary, "workspace/node_modules/.pnpm/ui-fixture/node_modules/@agile-team/wl-skills-ui");
13
+ try {
14
+ mkdirSync(project, { recursive: true });mkdirSync(physical, { recursive: true });
15
+ for (const file of ["bin", "scanner", "standards", "skills", "mcp", "SKILL.md", "package.json"]) cpSync(join(repo, file), join(physical, file), { recursive: true });
16
+ const pkg = JSON.parse(readFileSync(join(repo, "package.json"), "utf8"));
17
+ writeFileSync(join(project, "package.json"), JSON.stringify({ private: true, devDependencies: { [pkg.name]: pkg.version, vue: "^3.4.0" } }));
18
+ const logical = join(project, "node_modules", pkg.name);
19
+ mkdirSync(dirname(logical), { recursive: true });symlinkSync(relative(dirname(logical), physical), logical, "dir");
20
+ mkdirSync(join(project, "src"));writeFileSync(join(project, "src/Form.vue"), '<template><el-input size="small" /></template>');
21
+ const run = args => {
22
+ const result = spawnSync(process.execPath, [join(physical, "bin/wl-ui.js"), ...args], { cwd: project, encoding: "utf8", timeout: 30000 });
23
+ assert.equal(result.status, 0, result.stderr);return result.stdout;
24
+ };
25
+ run(["init", "--editor", "agents-generic", "--profile", "native-element", "--skills-only"]);
26
+ assert.equal(JSON.parse(run(["doctor-host", "--host", "codex", "--json"])).entryReadiness, "ready");
27
+ const plan = JSON.parse(run(["task", "检查表单样式", "--target", "src/Form.vue", "--json", "--run-id", "external-store"]));
28
+ assert.equal(plan.ready, true);assert.deepEqual(plan.selectedSkills, ["element/el-form"]);
29
+ const status = () => JSON.parse(run(["status", "--run-id", plan.runId, "--json"]));
30
+ const before = status();assert.equal(before.planStale, false);
31
+ const snapshot = before.records.find(record => record.kind === "task-decision").ruleSnapshot;
32
+ assert.ok(snapshot.targets.includes(`node_modules/${pkg.name}/standards/rules.json`));
33
+ assert.ok(snapshot.targets.includes(`node_modules/${pkg.name}/scanner/rules`));
34
+ run(["scan", "--target", "src/Form.vue", "--run-id", plan.runId, "--output", "summary"]);
35
+ assert.equal(status().stale, false);
36
+ const rules = join(physical, "standards/rules.json");const original = readFileSync(rules);
37
+ appendFileSync(rules, "\n");assert.equal(status().planStale, true);assert.equal(status().stale, true);
38
+ writeFileSync(rules, original);assert.equal(status().stale, false);
39
+ appendFileSync(join(physical, "scanner/rules/form.mjs"), "\n// isolated fixture change\n");
40
+ assert.equal(status().planStale, true);assert.equal(status().stale, true);
41
+ } finally { rmSync(temporary, { recursive: true, force: true }); }
42
+ });
@@ -0,0 +1,34 @@
1
+ [
2
+ {
3
+ "task": "旧需求写过“UI迁移迁移tokens”,本次只审查表单尺寸",
4
+ "status": "matched",
5
+ "skills": [
6
+ "element/el-form"
7
+ ],
8
+ "mode": "inspect"
9
+ },
10
+ {
11
+ "task": "不要迁移tokens,只检查现有表单样式",
12
+ "status": "matched",
13
+ "skills": [
14
+ "element/el-form"
15
+ ],
16
+ "mode": "inspect"
17
+ },
18
+ {
19
+ "task": "只说明 UI修复 的影响,不执行修改",
20
+ "status": "matched",
21
+ "skills": [
22
+ "ops/fix"
23
+ ],
24
+ "mode": "explain"
25
+ },
26
+ {
27
+ "task": "迁移tokens",
28
+ "status": "matched",
29
+ "skills": [
30
+ "ops/migrate"
31
+ ],
32
+ "mode": "apply"
33
+ }
34
+ ]
@@ -0,0 +1,41 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import {spawnSync} from 'node:child_process';
7
+ const dirname = import.meta.dirname;
8
+ const repo = path.resolve(dirname, "../..");
9
+ const pkg = JSON.parse(fs.readFileSync(path.join(repo, 'package.json'), 'utf8'));
10
+ const cli = path.join(repo, 'bin/wl-ui.js');
11
+ const cases = JSON.parse(fs.readFileSync(path.join(dirname, 'task-intent-cases.json'), 'utf8'));
12
+ const key = "ui";
13
+ test('当前请求、否定、历史引用与只讨论:公开协议精确回归', () => {
14
+ const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'wl-intent-ui-'));
15
+ const project = path.join(temporary, 'project');fs.mkdirSync(project);
16
+ const run = args => {
17
+ const result = spawnSync(process.execPath,[cli,...args],{cwd:project,encoding:'utf8',timeout:30000});
18
+ assert.equal(result.status,0,result.stderr || result.stdout);return result.stdout;
19
+ };
20
+ try {
21
+ fs.writeFileSync(path.join(project,'package.json'),JSON.stringify({private:true,devDependencies:{[pkg.name]:'*'}}));
22
+ const scoped = path.join(project,'node_modules/@agile-team');fs.mkdirSync(scoped,{recursive:true});fs.symlinkSync(repo,path.join(scoped,'wl-skills-ui'),'dir');
23
+ run(['init','--project',project,'--editor','agents-generic','--profile','native-element','--skills-only']);
24
+ for(const row of cases){
25
+ if(row.javaProject)fs.writeFileSync(path.join(project,'pom.xml'),'<project><modelVersion>4.0.0</modelVersion><groupId>audit</groupId><artifactId>fixture</artifactId><version>1</version></project>');
26
+ const file = path.join(temporary,'request.json');
27
+ fs.writeFileSync(file,JSON.stringify({operation:'route',projectRoot:project,task:row.task,...(row.domain ? {domain:row.domain} : {})}));
28
+ const envelope = JSON.parse(run(['protocol','request','--input-file',file,'--json']));
29
+ assert.equal(envelope.ok,true,row.task);const d = envelope.result.decision || envelope.result;
30
+ assert.equal(d.status,row.status,row.task);assert.deepEqual(d.selectedSkills,row.skills,row.task);
31
+ if(row.rules)assert.deepEqual(d.requiredRules,row.rules,row.task);
32
+ assert.equal(d.action.mode,row.mode,row.task);assert.equal(d.action.businessWritesAuthorized,false);
33
+ if(row.mode === 'explain')assert.equal(d.action.checksAllowed,false);
34
+ const core = envelope.result.integration;assert.equal(core.packageName,pkg.name);assert.equal(core.packageVersion,pkg.version);
35
+ assert.equal(core.contractVersion,1);assert.equal(core.runId,null);
36
+ assert.ok(core.ruleRefs.every(rule => rule.packageName === pkg.name && rule.ruleVersion === pkg.version));
37
+ }
38
+ const storage = {kit:'.wl-skills',ui:'.wl-skills-ui',bd:'.wl-skills-bd',design:'.wl-skills-design',test:'.wl-skills-test'}[key];
39
+ assert.equal(fs.existsSync(path.join(project,storage,'runs')),false,'route 不写任务记录');
40
+ } finally {fs.rmSync(temporary,{recursive:true,force:true});}
41
+ });
@@ -0,0 +1,7 @@
1
+ import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
2
+ import {taskCatalog} from '../task-integration.mjs';
3
+ test('触发词清单与本包技能目录双向完整且不含空词',() => {
4
+ const triggers = JSON.parse(fs.readFileSync(new URL('../task-triggers.json',import.meta.url),'utf8'));const catalog = taskCatalog();
5
+ assert.deepEqual(Object.keys(triggers).sort(),catalog.map(s => s.id).sort());
6
+ for(const skill of catalog){assert.ok(skill.triggers.length,skill.id);assert.ok(skill.triggers.every(p => typeof p === 'string' && p.trim()));assert.deepEqual(skill.triggers,triggers[skill.id]);}
7
+ });
@@ -0,0 +1,48 @@
1
+ [
2
+ {
3
+ "id": "scan",
4
+ "command": [
5
+ "scan"
6
+ ],
7
+ "when": {
8
+ "checks": [
9
+ "scan"
10
+ ]
11
+ },
12
+ "parameters": {
13
+ "type": "object",
14
+ "required": [
15
+ "targets"
16
+ ],
17
+ "properties": {
18
+ "targets": {
19
+ "type": "array",
20
+ "items": {
21
+ "type": "string",
22
+ "minLength": 1,
23
+ "pattern": "\\S"
24
+ },
25
+ "minItems": 1,
26
+ "maxItems": 1
27
+ },
28
+ "rules": {
29
+ "type": "array",
30
+ "items": {
31
+ "type": "string",
32
+ "pattern": "^R[0-9]{3}$"
33
+ },
34
+ "minItems": 1,
35
+ "uniqueItems": true
36
+ }
37
+ },
38
+ "additionalProperties": false
39
+ },
40
+ "sideEffects": {
41
+ "business": "none",
42
+ "records": true,
43
+ "reports": "stdout"
44
+ },
45
+ "authorization": "read-only-business-check",
46
+ "receipt": "package-execution-record-v1"
47
+ }
48
+ ]
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  const PROTOCOL_VERSION = 1;
19
+ const resultCore = require("./integration-result.cjs");
19
20
  const SUPPORTED_VERSIONS = [1];
20
21
  const ERROR_CODES = ["unsupported-protocol", "unknown-operation", "missing-input", "invalid-input", "internal-error"];
21
22
  const OPTIONAL_STRING_FIELDS = ["requestId", "runId", "projectRoot", "skill", "host", "type", "domain", "profile"];
@@ -233,6 +234,7 @@ function validateFieldTypes(input) {
233
234
  commands: inventory.commands || [],
234
235
  mcpTools: inventory.mcpTools || [],
235
236
  loadErrors: inventory.loadErrors || [],
237
+ executors: config.executors || [],
236
238
  note: "业务执行仍走各包原 CLI/MCP;inventory 只声明公开调用映射与入口,不改变执行归属;loadErrors 非空表示目录派生存在异常,不得忽略",
237
239
  };
238
240
  }
@@ -255,9 +257,11 @@ function validateFieldTypes(input) {
255
257
  return {
256
258
  ...base(null, null),
257
259
  capabilities: config.capabilities || [],
260
+ features: ["task-intent-v1", "result-core-v1", "executor-catalog-v1"],
258
261
  operations: config.operations.map((operation) => ({ ...operation })),
259
262
  inventory: buildInventory(config),
260
- schemas: { request: requestSchema(config), envelope: envelopeSchema(config.packageName) },
263
+ schemas: { request: requestSchema(config), envelope: envelopeSchema(config.packageName), resultCore: resultCore.SCHEMA,
264
+ results: Object.fromEntries(config.operations.map((operation) => [operation.id, { type: "object", required: ["integration"], properties: { integration: resultCore.SCHEMA }, additionalProperties: true }])) },
261
265
  constraints: buildConstraints(config),
262
266
  errorCodes: [...ERROR_CODES],
263
267
  };
@@ -267,8 +271,13 @@ function validateFieldTypes(input) {
267
271
  const { operation, error } = validateRequest(input);
268
272
  if (error) return error;
269
273
  const diagnostics = [];
274
+ const supportedFields = new Set(["operation", "protocolVersion", "requestId", ...operation.required, ...(operation.requireAny || []), ...operation.optional]);
275
+ for (const field of Object.keys(input)) {
276
+ if (!supportedFields.has(field)) diagnostics.push(`操作 ${operation.id} 未声明使用字段 ${field};该字段不构成执行或授权依据`);
277
+ }
270
278
  try {
271
- const result = runOperation(operation.id, input, diagnostics) ?? {};
279
+ const original = runOperation(operation.id, input, diagnostics) ?? {};
280
+ const result = resultCore.projectResult(original, input, config);
272
281
  return { ...base(operation.id, input.requestId), ok: true, result, diagnostics };
273
282
  } catch (thrown) {
274
283
  diagnostics.push(String((thrown && thrown.message) || thrown));
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+
3
+ // Additive, typed core: original package-specific result fields are preserved.
4
+ const path = require("node:path");
5
+ const DECISIONS = ["matched", "baseline", "gap", "ambiguous", "not-applicable", "needs-context"];
6
+ const strings = (items) => Array.isArray(items) ? items.filter((value) => typeof value === "string") : [];
7
+ const nullableString = { type: ["string", "null"] };
8
+ const SCHEMA = {
9
+ type: "object", additionalProperties: false,
10
+ required: ["contractVersion", "packageName", "packageVersion", "projectRoot", "runId", "decisionStatus", "scope", "action", "selectedSkills", "ruleRefs", "requiredChecks", "targets", "ready", "executionStatus", "validationStatus", "stale", "planStale", "allowedExecutors"],
11
+ properties: {
12
+ contractVersion: { const: 1 }, packageName: { type: "string", minLength: 1 }, packageVersion: { type: "string", minLength: 1 },
13
+ projectRoot: { type: "string", minLength: 1 }, runId: nullableString,
14
+ decisionStatus: { enum: [...DECISIONS, null] }, scope: { oneOf: [{type:"null"}, {type:"object",required:["schemaVersion","status","reason","adopted","projectType","inherited"],
15
+ properties:{schemaVersion:{const:1},status:{enum:["applicable","not-applicable","needs-context"]},reason:{type:"string"},adopted:{type:"boolean"},projectType:{type:"string"},inherited:{const:false}},additionalProperties:true}] },
16
+ action: { oneOf: [{ type: "null" }, { type: "object", required: ["mode", "checksAllowed", "businessWritesAuthorized", "authorization"],
17
+ properties: { mode: { enum: ["explain", "inspect", "plan", "apply", "unspecified"] }, checksAllowed: { type: "boolean" }, businessWritesAuthorized: { const: false }, authorization: { type: "string" } }, additionalProperties: false }] },
18
+ selectedSkills: { type: "array", items: { type: "string" } }, requiredChecks: { type: "array", items: { type: "string" } },
19
+ targets: { type: "array", items: { type: "string" } }, allowedExecutors: { type: "array", items: { type: "string" } },
20
+ ruleRefs: { type: "array", items: { type: "object", required: ["packageName", "ruleId", "legacyId", "name", "ruleVersion", "source", "kind", "verification"],
21
+ properties: { packageName: nullableString, ruleId: { type: "string" }, legacyId: { type: "string" }, name: { type: "string" }, ruleVersion: nullableString, source: nullableString, kind: { type: "string" }, verification: { const: "unverified" } }, additionalProperties: false } },
22
+ ready: { type: ["boolean", "null"] }, executionStatus: nullableString, validationStatus: nullableString,
23
+ stale: { type: ["boolean", "null"] }, planStale: { type: ["boolean", "null"] },
24
+ runtime:{oneOf:[{type:'null'},{type:'object',required:['status'],properties:{status:{enum:['aligned','unverified','mismatch','invalid']}},additionalProperties:true}]},
25
+ },
26
+ };
27
+
28
+ function allowedExecutors(decision, executors) {
29
+ if (!["matched", "baseline"].includes(decision.status) || !decision.action?.checksAllowed) return [];
30
+ return executors.filter((executor) => {
31
+ const when = executor.when || {};
32
+ return strings(when.skills).some((id) => decision.selectedSkills?.includes(id))
33
+ || strings(when.checks).some((id) => decision.requiredChecks?.includes(id))
34
+ || strings(when.rulePrefixes).some((prefix) => decision.requiredRules?.some((id) => id.startsWith(prefix)));
35
+ }).map((executor) => executor.id);
36
+ }
37
+
38
+ function resultReadiness(result, decision, status) {
39
+ if (result.planStale || decision.ready === false || status && !["matched", "baseline"].includes(status)) return false;
40
+ if (typeof result.ready === "boolean") return result.ready;
41
+ if (typeof decision.ready === "boolean") return decision.ready;
42
+ return status ? ["matched", "baseline"].includes(status) && !(decision.missingInputs || []).length : null;
43
+ }
44
+
45
+ function executionState(result) {
46
+ return {
47
+ runId: result.runId || null,
48
+ executionStatus: result.executionStatus || null,
49
+ validationStatus: result.validationStatus || null,
50
+ stale: typeof result.stale === "boolean" ? result.stale : null,
51
+ planStale: typeof result.planStale === "boolean" ? result.planStale : null,
52
+ };
53
+ }
54
+
55
+ function decisionReadout(decision) {
56
+ return {action:decision.action || null, selectedSkills:strings(decision.selectedSkills), ruleRefs:decision.ruleRefs || [], requiredChecks:strings(decision.requiredChecks)};
57
+ }
58
+ function targetReadout(result,input) { return strings(result.targets || input.targets || result.notice?.targets); }
59
+ function projectIdentity(scope,result,input) { return path.resolve(scope?.projectRoot || result.projectRoot || result.root || input.projectRoot || process.cwd()); }
60
+
61
+ function runtimeReadout(result) { return result.runtime || result.notice?.runtime || null; }
62
+
63
+ function projectResult(result, input, config) {
64
+ const decision = result.decision || result;
65
+ const scope = result.currentScope || decision.scope || result.scope || null;
66
+ const status = DECISIONS.includes(decision.status) ? decision.status : null;
67
+ const ready = resultReadiness(result, decision, status);
68
+ return {
69
+ ...result,
70
+ integration: {
71
+ contractVersion: 1, packageName: config.packageName, packageVersion: config.packageVersion,
72
+ projectRoot: projectIdentity(scope,result,input),
73
+ ...executionState(result), decisionStatus: status, scope,
74
+ ...decisionReadout(decision), targets: targetReadout(result,input), ready,
75
+ runtime: runtimeReadout(result),
76
+ allowedExecutors: ready && scope?.status === "applicable" ? allowedExecutors(decision, config.executors || []) : [],
77
+ },
78
+ };
79
+ }
80
+
81
+ module.exports = { SCHEMA, projectResult };
@@ -16,9 +16,9 @@ const { createProtocol } = require("./integration-protocol.cjs");
16
16
 
17
17
  const BIN = "wl-ui";
18
18
  const OPERATIONS = [
19
- { id: "route", summary: "只读任务判定:视觉/样式域技能、约束、歧义与缺口", readOnly: true, required: ["task"], optional: ["targets", "profile", "skill", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} route --text "<task>"` },
20
- { id: "explain", summary: "解释本次任务判定(只读,不记录)", readOnly: true, required: ["task"], optional: ["targets", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} explain --text "<task>"` },
21
- { id: "task", summary: "判定并持久化任务计划(尚未执行扫描或修复)", readOnly: false, required: ["task"], optional: ["runId", "targets", "profile", "projectRoot"], sideEffects: "写入 .wl-skills-ui/runs/ 下本包任务记录", mapping: `${BIN} task --text "<task>" [--run-id <id>]` },
19
+ { id: "route", summary: "只读任务判定:视觉/样式域技能、约束、歧义与缺口", readOnly: true, required: ["task"], optional: ["context", "targets", "profile", "skill", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} route --text "<task>"` },
20
+ { id: "explain", summary: "解释本次任务判定(只读,不记录)", readOnly: true, required: ["task"], optional: ["context", "targets", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} explain --text "<task>"` },
21
+ { id: "task", summary: "判定并持久化任务计划(尚未执行扫描或修复)", readOnly: false, required: ["task"], optional: ["context", "runId", "targets", "profile", "projectRoot"], sideEffects: "写入 .wl-skills-ui/runs/ 下本包任务记录", mapping: `${BIN} task --text "<task>" [--run-id <id>]` },
22
22
  { id: "status", summary: "读取本包执行/校验记录与新鲜度", readOnly: true, required: [], optional: ["runId", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} status [--run-id <id>]` },
23
23
  { id: "doctor-host", summary: "宿主入口静态诊断(不证明宿主已加载)", readOnly: true, required: [], optional: ["host", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} doctor-host [--host <host>]` },
24
24
  ];
@@ -31,7 +31,7 @@ function buildInventory() {
31
31
  { name: "clean", args: "[--project <path>] [--dry-run]", summary: "清理本包安装文件", execution: "programmatic", sideEffects: "删除本包登记文件" },
32
32
  { name: "scan", args: "[--project <path>] [--profile <id>]", summary: "R 规则扫描", execution: "programmatic", sideEffects: "无(报告输出)" },
33
33
  { name: "audit", args: "[--project <path>]", summary: "审计汇总", execution: "programmatic", sideEffects: "无" },
34
- { name: "check", args: "[--project <path>]", summary: "扫描+回执记录", execution: "programmatic", sideEffects: "写 .wl-skills-ui/runs/" },
34
+ { name: "check", args: "[--project <path>]", summary: "接入完整性检查+回执记录(样式规则使用 scan)", execution: "programmatic", sideEffects: "写 .wl-skills-ui/runs/" },
35
35
  { name: "fix", args: "--run-id <id> --rule <id> [--confirm]", summary: "确定性修复(快照/回滚)", execution: "programmatic", sideEffects: "写源文件(确认制+快照回滚)" },
36
36
  { name: "snapshot", args: "/ drift / exempt", summary: "漂移基线管理", execution: "programmatic", sideEffects: "写基线文件" },
37
37
  { name: "contract", args: "<extract|validate|match>", summary: "ui-contract 提取/校验/匹配", execution: "programmatic", sideEffects: "extract 写契约文件" },
@@ -82,6 +82,7 @@ export const protocol = createProtocol({
82
82
  capabilities: capabilitiesDocument.capabilities || [],
83
83
  constraints: { projectScope: { config: ".wl-skills-scope.json", schema: "bin/project-scope.schema.json", adoption: "own-manifest-or-direct-dependency-or-explicit-enable", inheritance: "never-across-project-boundaries", excluded: ["mobile", "unadopted-project", "aggregate-workspace"] }, node: (pkg.engines && pkg.engines.node) || null, boundaryVersion: capabilitiesDocument.boundaryVersion || null },
84
84
  operations: OPERATIONS,
85
+ executors: require("./executors.json"),
85
86
  inventory: buildInventory(),
86
87
  });
87
88
 
@@ -11,6 +11,8 @@ For one user task reuse the same \`--run-id\` or \`WL_TASK_RUN_ID\` across insta
11
11
 
12
12
  先确认目标项目自身的接入证据(本包安装清单、直接依赖或 \`.wl-skills-scope.json\` 显式启用),再判定任务。只在目标已接入且平台适用时读取规则;父目录安装、兄弟包、Vue 文件或触发词不能证明接入。未接入的开源项目不套用规则、不建议自动安装;UniApp/小程序/App/PDA/移动 H5 不适用。跨项目目标分别判定,沿最近项目边界停止,不能越过未接入子项目找父清单。工具返回 \`scope\` 作为静态范围证据,宿主加载仍需实际事件。
13
13
 
14
+ 动作边界:显示真实回执中的 \`action.mode\`、\`checksAllowed\` 和 \`ruleRefs\`。历史引用与明确禁止的动作不参与技能评分。只解释或只列计划时可以读取相关规范,不能因为命中了生成/修复技能就自动检查或改写业务文件。检查许可仅表示任务动作;业务写入仍遵循用户授权和本包原有流程。非适用项目不加载规范。
15
+
14
16
  多项目工作区先沿目标路径向上找到本包安装清单与项目 AGENTS,切到该项目根再调用工具;不要以聚合工作区根代替 projectRoot,也不要在聚合根安装。不同项目分别保存项目身份,同一用户任务复用 runId。
15
17
 
16
18
  1. Start each relevant task from its project root with \`node node_modules/@agile-team/wl-skills-ui/bin/wl-ui.js task "<task>" --target <path> --json\`. Before editing, visibly show the returned \`notice\`: actual package/version, decision, Skill or baseline, rule IDs/names, target, runId and unverified checks. Include baseline and gaps. Report missing/unsupported local commands and version drift explicitly. A routing decision does not execute checks.
@@ -3,6 +3,7 @@ import { join, relative, resolve, isAbsolute } from "node:path";
3
3
  import { parseArgs } from "node:util";
4
4
  import core from "./task-observability.cjs";
5
5
  import projectScope from "./project-scope.cjs";
6
+ import taskIntent from "./task-intent.cjs";
6
7
  import { GATEWAY_PATH } from "./task-gateway.mjs";
7
8
  import { resolveProjectProfile } from "../standards/profiles-loader.mjs";
8
9
  import { getRules } from "../scanner/rules/index.mjs";
@@ -15,18 +16,7 @@ const POLICY = {
15
16
  baselineRules: ["R001", "R006", "R043"], baselineChecks: ["scan"], minimumScore: 1, minimumMargin: 1,
16
17
  unsupportedIntents: ["生成图像", "生成图片", "Figma设计稿导出"],
17
18
  };
18
- const TRIGGERS = {
19
- "element/el-table": ["表格", "table", "el-table"], "element/el-form": ["表单", "查询表单", "el-form"],
20
- "element/el-dialog": ["弹窗", "dialog", "el-dialog"], "element/el-tag": ["标签颜色", "状态标签", "el-tag"],
21
- "element/component-family": ["卡片", "页签", "抽屉", "el-tabs", "上传控件"],
22
- "runtime/design-tokens": ["设计令牌", "design tokens", "颜色硬编码", "tokens"],
23
- "runtime/style-align": ["样式统一", "风格对齐", "样式不生效"], "runtime/migration": ["runtime迁移", "样式迁移"],
24
- "ops/scan": ["UI扫描", "UI检查", "样式扫描"], "ops/audit": ["UI审计", "视觉审计"],
25
- "ops/fix": ["UI修复", "修复样式", "统一视觉"], "ops/migrate": ["UI迁移", "迁移tokens"],
26
- "layouts/list-page": ["列表布局"], "layouts/detail-page": ["详情布局"], "layouts/form-dialog": ["表单弹窗布局"], "layouts/tree-list": ["树列表布局"],
27
- "vendors/ag-grid": ["AG Grid", "ag-grid"], "vendors/base-table": ["BaseTable"], "vendors/jh-components": ["jh组件"],
28
- "vendors/c-components": ["C组件"], "vendors/custom-wrappers": ["自定义封装组件"], "vendors/unknown-wrapper": ["未知封装组件"],
29
- };
19
+ const TRIGGERS = JSON.parse(readFileSync(join(root, "bin/task-triggers.json"), "utf8"));
30
20
 
31
21
  function catalogFiles(dir, result = []) {
32
22
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -54,15 +44,30 @@ export function taskOptions(projectRoot, extra = {}) {
54
44
  }
55
45
 
56
46
  function projectEvidencePaths(projectRoot, candidates) {
57
- return candidates.filter((candidate) => existsSync(candidate)).map((candidate) => relative(projectRoot, realpathSync(candidate)).replace(/\\/g, "/"))
58
- .filter((rel) => rel && !rel.startsWith("../") && !isAbsolute(rel));
47
+ return candidates.filter((candidate) => existsSync(candidate)).map((candidate) => projectEvidencePath(projectRoot, candidate)).filter(Boolean);
48
+ }
49
+
50
+ function evidenceRelative(projectRoot, target) {
51
+ const value = relative(projectRoot, target);
52
+ return value && value !== ".." && !value.startsWith("../") && !value.startsWith("..\\") && !isAbsolute(value) ? value.replace(/\\/g, "/") : null;
53
+ }
54
+
55
+ function projectEvidencePath(projectRoot, candidate) {
56
+ const physical = realpathSync(candidate);
57
+ const local = evidenceRelative(projectRoot, physical);
58
+ if (local) return local;
59
+ const dependency = join(projectRoot, "node_modules", pkg.name);
60
+ if (!existsSync(dependency)) return null;
61
+ const internal = evidenceRelative(realpathSync(dependency), physical);
62
+ // The shared reader still verifies package identity and its own realpath boundary.
63
+ return internal ? `node_modules/${pkg.name}/${internal}` : null;
59
64
  }
60
65
 
61
66
  export function decideTask(input) {
62
67
  const catalog = taskCatalog();
63
68
  const decision = core.evaluateTask({ ...input, catalog, policy: POLICY });
64
69
  if (input.skill && !catalog.some((skill) => skill.id === input.skill)) return capabilityGap(decision, `本包未发布专项 Skill:${input.skill}`);
65
- if (POLICY.unsupportedIntents.some((intent) => String(input.task).includes(intent))) return capabilityGap(decision, "本包没有图像生成或设计稿导出的执行器");
70
+ if (POLICY.unsupportedIntents.some((intent) => taskIntent.phraseMatches(input.task, intent))) return capabilityGap(decision, "本包没有图像生成或设计稿导出的执行器");
66
71
  return decision;
67
72
  }
68
73
 
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Dependency-free task evidence snapshot. Domain routing remains in each package.
5
+ * Preserve the prompt; exclude explicit prohibitions and historical clauses from
6
+ * capability scoring. Intent never substitutes for business write authorization.
7
+ */
8
+ const HISTORY = /^(?:旧(?:需求|文档|任务|方案)|历史(?:需求|任务|记录)|上(?:次|一轮|轮)|之前|以前|(?:用户|客户).{0,8}(?:曾|原先)(?:说|要求|提)|曾(?:说|要求|提到)|previous(?:\s+(?:request|task))?|previously|used to|historical)/iu;
9
+ const CURRENT = /(?:本次|这次|此次|现在|当前任务|实际要求|改为|而是|this time|now|instead)/iu;
10
+ const NEGATIVE = /^(?:(?:请|本次|这次|此次|本轮|现在|这里|暂时|暂|先|也|并且|且|同时|还)\s*)*(?:不要|不用|无需|禁止|不得|不可|不能|切勿|别|不再|不需要|不必|不(?:做|用|画|生成|执行|运行|跑|检查|校验|扫描|测试|新增|实现|修改|改写|迁移|创建|编写|修复|自动)|跳过|do not\b|don't\b|don’t\b|without\b|skip\b)/iu;
11
+ const APPLY = /生成|绘制|新增|增加|实现|修改|改写|编写|修复|整改|创建|迁移|重构|同步|统一|调整|优化|完善|补充|补[^,。;\n]{0,12}注释|\b(?:generate|draw|create|implement|modify|write|fix|repair|migrate|refactor|adjust|optimize|improve)\b/iu;
12
+ const INSPECT = /检查|校验|验证|审查|审计|扫描|评审|核对|走查|梳理|(?:运行|执行|跑)(?:自动)?(?:测试|质量门)|\b(?:check|validate|verify|inspect|review|audit|scan|run\s+(?:tests?|checks?|quality\s+gates?))\b/iu;
13
+ const PREFIX = /^(?:(?:本次|这次|此次|现在|先|暂时|请问|请|麻烦你?|帮我|给我|我(?:希望|想要|想)|你(?:能否|可以|能不能)?|能否|可以|能不能|只|仅|需要)|(?:please|can you|could you|would you|only|just|first|this time|now|instead)\b)\s*/iu;
14
+ const EXPLAIN = /^(?:解释|说明|介绍|讨论|分析|了解|咨询|如何|怎么|为什么|explain\b|describe\b|discuss\b|analy[sz]e\b|how\b|why\b)/iu;
15
+ const BACKGROUND = /^(?:为了|为(?:了)?(?=修复|生成|实现|修改)|目的是|背景是|in order to\b|for the purpose of\b)/iu;
16
+ const PLAN = /^(?:(?:列|制定|提供|给出|做)(?:一个|一份|个)?[^,。;\n]{0,40}(?:计划|步骤|方案)|(?:一个|一份)?[^,。;\n]{0,40}(?:计划|步骤)\s*$|[^,。;\n]{1,40}(?:做|拟定|制定)(?:一个|一份|个)?方案\s*$|plan\b)/iu;
17
+ const QUOTES = new Map([['"','"'],["'","'"],["“","”"],["‘","’"],["「","」"],["『","』"]]);
18
+ const CONTRAST = /^(?:但是|但|而是|改为|(?:本次|这次|此次|现在)(?=\s*(?:只|仅|请|要|需要|检查|审查|生成|修改|补充))|(?:but|instead|this time|now)\b)/iu;
19
+
20
+ function normalized(text) { return String(text || "").normalize("NFKC").toLowerCase().trim(); }
21
+
22
+ function quoteEnd(text,index,stack) {
23
+ const character = text[index];
24
+ if (stack.at(-1) === character) { stack.pop(); return true; }
25
+ if (!QUOTES.has(character) || character === "'" && /[a-z]/iu.test(text[index - 1] || '') && /[a-z]/iu.test(text[index + 1] || '')) return false;
26
+ stack.push(QUOTES.get(character)); return true;
27
+ }
28
+
29
+ function clausesFor(text) {
30
+ const parts = []; const stack = []; let raw = ''; let visible = '';
31
+ const finish = () => {if(raw.trim())parts.push({raw:raw.trim(),visible:visible.trim()});raw = '';visible = '';};
32
+ for(let index = 0;index < text.length;index += 1){
33
+ const character = text[index]; const outside = stack.length === 0;
34
+ if(outside && /[,,。;;!?!?\n]/u.test(character)){finish();continue;}
35
+ if(outside && raw.trim() && CONTRAST.test(text.slice(index)))finish();
36
+ const delimiter = quoteEnd(text,index,stack);
37
+ raw += character; visible += outside && !delimiter ? character : ' ';
38
+ }
39
+ finish();return parts;
40
+ }
41
+
42
+ function analyzeTask(task) {
43
+ const original = String(task || "").normalize("NFKC");
44
+ const clauses = clausesFor(original);
45
+ const excluded = [];
46
+ const active = [];
47
+ for (const {raw,visible} of clauses) {
48
+ const clause = raw.replace(/^(?:但是|不过|但|而是|改为)\s*/u, "").trim();
49
+ if (BACKGROUND.test(visible)) {
50
+ excluded.push({ kind: "background", text: clause });
51
+ } else if (HISTORY.test(visible) && !CURRENT.test(visible)) {
52
+ excluded.push({ kind: "historical", text: clause });
53
+ } else if (NEGATIVE.test(clause)) {
54
+ excluded.push({ kind: "prohibited", text: clause });
55
+ } else active.push(clause);
56
+ }
57
+ const activeText = active.join(",");
58
+ return {schemaVersion:1,activeText,excluded,action:actionEvidence(activeText,excluded)};
59
+ }
60
+
61
+ function requestText(text) {
62
+ let value = text.trim(); let next = value.replace(PREFIX,'');
63
+ while(next !== value){value = next;next = value.replace(PREFIX,'');}
64
+ return value;
65
+ }
66
+
67
+ function clauseMode(text) {
68
+ const request = requestText(text);
69
+ if(EXPLAIN.test(request))return 'explain';
70
+ if(PLAN.test(request))return 'plan';
71
+ if(APPLY.test(text))return 'apply';
72
+ if(INSPECT.test(text))return 'inspect';
73
+ return 'unspecified';
74
+ }
75
+
76
+ function stopsChecks(text) {
77
+ const chinese = /^(?:(?:请|本次|这次|此次|本轮|现在|这里|暂时|暂|先|也|并且|且|同时|还)\s*)*(?:不要|不用|无需|禁止|不得|不可|不能|切勿|别|不再|不需要|不必|跳过|不)(?:再|自动|进行|做)?(?:执行|运行|跑)?(?:任何|所有|自动)?(?:工具|命令|检查|校验|测试|扫描|审查)?(?:代码|规范|质量门|任何检查|自动检查)?\s*$/u;
78
+ const english = /^(?:please\s+)?(?:do not|don't|don’t|skip|without)\s+(?:execute|run|check|validate|verify|inspect|review|audit|scan)(?:\s+(?:any|all|automatic|the))*(?:\s+(?:tools?|commands?|checks?|tests?|code))?\s*$/iu;
79
+ return chinese.test(text) || english.test(text);
80
+ }
81
+
82
+ function actionEvidence(activeText, excluded) {
83
+ const modes = clausesFor(activeText).map(({raw}) => clauseMode(raw));
84
+ const mode = ['apply','inspect','plan','explain'].find(value => modes.includes(value)) || 'unspecified';
85
+ const genericStop = excluded.some((item) => item.kind === "prohibited" && stopsChecks(item.text));
86
+ return {
87
+ mode, checksAllowed: !genericStop && ["inspect", "apply"].includes(mode), businessWritesAuthorized: false,
88
+ authorization: "intent-is-not-business-write-authorization",
89
+ };
90
+ }
91
+
92
+ function phraseMatches(text, phrase) {
93
+ const haystack = normalized(analyzeTask(text).activeText);
94
+ const needle = normalized(phrase);
95
+ if (!needle) return false;
96
+ let at = haystack.indexOf(needle);
97
+ while (at !== -1) {
98
+ const before = haystack.slice(Math.max(0, at - 32), at);
99
+ const boundary = !/^[a-z0-9_.+-]+$/iu.test(needle)
100
+ || !/[a-z0-9_-]/iu.test((haystack[at - 1] || "") + (haystack[at + needle.length] || ""));
101
+ const negated = /(?:不要|不用|无需|禁止|别|不做|不|do not|don't|without)\s*(?:生成|创建|做|画|绘制|进行|设计|用|使用|执行|运行)?\s*$/iu.test(before);
102
+ if (boundary && !negated) return true;
103
+ at = haystack.indexOf(needle, at + needle.length);
104
+ }
105
+ return false;
106
+ }
107
+
108
+ module.exports = { analyzeTask, phraseMatches };
@@ -5,6 +5,7 @@ const fs = require("node:fs");
5
5
  const path = require("node:path");
6
6
  const crypto = require("node:crypto");
7
7
  const projectScope = require("./project-scope.cjs");
8
+ const taskIntent = require("./task-intent.cjs");
8
9
 
9
10
  const RECORD_VERSION = 1;
10
11
  const STORAGE = {
@@ -48,18 +49,7 @@ function digest(value) {
48
49
  }
49
50
 
50
51
  function phraseMatches(text, phrase) {
51
- const needle = normalized(phrase);
52
- if (!needle) return false;
53
- let index = text.indexOf(needle);
54
- while (index !== -1) {
55
- const before = text.slice(Math.max(0, index - 16), index);
56
- const after = text[index + needle.length] || "";
57
- const boundary = !/^[a-z0-9_-]+$/i.test(needle) || !/[a-z0-9_-]/i.test((text[index - 1] || "") + after);
58
- const negated = /(?:不要|不用|无需|禁止|别|不是|不做|不|do not|don't|without)\s*$/i.test(before);
59
- if (boundary && !negated) return true;
60
- index = text.indexOf(needle, index + needle.length);
61
- }
62
- return false;
52
+ return taskIntent.phraseMatches(text, phrase);
63
53
  }
64
54
 
65
55
  function matches(text, words) {
@@ -84,7 +74,7 @@ function targetRelevance(targets, policy) {
84
74
 
85
75
  function taskSignals(options) {
86
76
  const policy = options.policy || {};
87
- const text = normalized(options.task);
77
+ const text = normalized(taskIntent.analyzeTask(options.task).activeText);
88
78
  const context = options.context || {};
89
79
  const contextText = `${text} ${strings(options.targets).join(" ")} ${strings(context.signals).join(" ")}`;
90
80
  const relevant = matches(text, policy.domainKeywords).length > 0 || targetRelevance(options.targets, policy) || context.domainRelevant === true;
@@ -138,7 +128,8 @@ function evaluateTask(options = {}) {
138
128
  const signals = taskSignals(options);
139
129
  const candidates = (options.catalog || []).map((skill) => scoreSkill(skill, signals.text, signals.contextText))
140
130
  .filter((skill) => skill.score > 0).sort((left, right) => right.score - left.score || left.id.localeCompare(right.id));
141
- return selectDecision(candidates, signals, options) || fallbackDecision(candidates, signals, options);
131
+ const result = selectDecision(candidates, signals, options) || fallbackDecision(candidates, signals, options);
132
+ return { ...result, taskEvidence: taskIntent.analyzeTask(options.task), action: taskIntent.analyzeTask(options.task).action };
142
133
  }
143
134
 
144
135
  function assertRunId(value) {
@@ -169,7 +160,31 @@ function projectPath(root, relative) {
169
160
  return inspectAncestors(root, path.resolve(root, relative));
170
161
  }
171
162
 
172
- function readEvidencePath(root, relative) {
163
+ function insidePath(root,target) {
164
+ const relative = path.relative(root,target);
165
+ return !path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`);
166
+ }
167
+
168
+ function ownDependencyRoot(root,packageName) {
169
+ if(!Object.hasOwn(STORAGE,packageName || ''))throw new Error('WL read evidence escapes project root');
170
+ const installed = fs.realpathSync(path.join(root,'node_modules',packageName));
171
+ const manifest = fs.realpathSync(path.join(installed,'package.json'));
172
+ if(!insidePath(installed,manifest))throw new Error('WL dependency metadata escapes its own package');
173
+ const metadata = JSON.parse(fs.readFileSync(manifest,'utf8'));
174
+ if(metadata.name !== packageName || typeof metadata.version !== 'string' || !metadata.version.trim())throw new Error('WL dependency identity is invalid');
175
+ return installed;
176
+ }
177
+
178
+ function authorizeRead(root,requested,physical,packageName) {
179
+ if(insidePath(root,physical))return root;
180
+ const logical = path.join(root,'node_modules',packageName || '__unowned__');
181
+ if(!insidePath(logical,requested))throw new Error('WL read evidence escapes project root');
182
+ const dependency = ownDependencyRoot(root,packageName);
183
+ if(!insidePath(dependency,physical))throw new Error('WL read evidence escapes its own dependency');
184
+ return dependency;
185
+ }
186
+
187
+ function readEvidencePath(root, relative,packageName) {
173
188
  if (typeof relative !== "string" || !relative) throw new Error("WL evidence path is required");
174
189
  const requested = path.resolve(root, relative);
175
190
  const lexical = path.relative(root, requested);
@@ -181,16 +196,44 @@ function readEvidencePath(root, relative) {
181
196
  ancestor = path.dirname(ancestor);
182
197
  }
183
198
  const physical = path.resolve(fs.realpathSync(ancestor), ...suffix);
184
- const resolved = path.relative(root, physical);
185
- if (resolved.startsWith("..") || path.isAbsolute(resolved)) throw new Error("WL read evidence escapes project root");
199
+ authorizeRead(root,requested,physical,packageName);
186
200
  return physical;
187
201
  }
188
202
 
189
203
  function captureReadEvidence(options, targets) {
190
204
  const root = fs.realpathSync(path.resolve(options.projectRoot || process.cwd()));
191
205
  const requested = strings(targets);
192
- const snapshot = captureSnapshot({ projectRoot: root, targets: requested.map((target) => path.relative(root, readEvidencePath(root, target)) || ".") });
193
- return { ...snapshot, targets: requested, resolution: "project-internal-realpath" };
206
+ const batches = [];let fileCount = 0;let bytes = 0;const seen = new Set();
207
+ for(const target of requested){
208
+ const physical = readEvidencePath(root,target,options.packageName);
209
+ if(seen.has(physical))continue;
210
+ seen.add(physical);
211
+ const batch = readSnapshot(root,target,options.packageName,{maxFiles:1000 - fileCount,maxBytes:16 * 1024 * 1024 - bytes});
212
+ batches.push(batch);fileCount += batch.files.length;bytes += batch.files.reduce((sum,file) => sum + file.bytes,0);
213
+ }
214
+ const snapshot = mergeReadSnapshots(batches);
215
+ return { ...snapshot, targets: requested, resolution: "authorized-read-realpath" };
216
+ }
217
+
218
+ function readSnapshot(root,target,packageName,limits) {
219
+ const physical = readEvidencePath(root,target,packageName);
220
+ const base = authorizeRead(root,path.resolve(root,target),physical,packageName);
221
+ const snapshot = captureSnapshot({projectRoot:base,targets:[path.relative(base,physical) || '.'],...limits});
222
+ if(base !== root){
223
+ const prefix = `node_modules/${packageName}/`;
224
+ for(const item of [...snapshot.files,...snapshot.omitted])item.path = prefix + item.path;
225
+ }
226
+ return snapshot;
227
+ }
228
+
229
+ function mergeReadSnapshots(batches) {
230
+ const files = [...new Map(batches.flatMap(batch => batch.files).map(file => [file.path,file])).values()].sort((left,right) => left.path.localeCompare(right.path));
231
+ const omitted = batches.flatMap(batch => batch.omitted); let bytes = 0;
232
+ const bounded = files.filter((file,index) => {
233
+ if(index >= 1000 || bytes + file.bytes > 16 * 1024 * 1024){omitted.push({path:file.path,reason:'snapshot-budget'});return false;}
234
+ bytes += file.bytes;return true;
235
+ });
236
+ return {files:bounded,omitted,complete:omitted.length === 0,sha256:digest(JSON.stringify({files:bounded,omitted}))};
194
237
  }
195
238
 
196
239
  function snapshotFile(root, target, state) {
@@ -199,7 +242,7 @@ function snapshotFile(root, target, state) {
199
242
  if (stat.isSymbolicLink()) { state.omitted.push({ path: relative, reason: "symlink" }); return; }
200
243
  if (stat.isDirectory()) { snapshotDirectory(root, target, state); return; }
201
244
  if (!stat.isFile()) { state.omitted.push({ path: relative, reason: "not-file" }); return; }
202
- if (state.files.length >= 1000 || state.bytes + stat.size > 16 * 1024 * 1024) { state.omitted.push({ path: relative, reason: "snapshot-budget" }); return; }
245
+ if (state.files.length >= state.maxFiles || state.bytes + stat.size > state.maxBytes) { state.omitted.push({ path: relative, reason: "snapshot-budget" }); return; }
203
246
  state.files.push({ path: relative, sha256: digest(fs.readFileSync(target)), bytes: stat.size });
204
247
  state.bytes += stat.size;
205
248
  }
@@ -216,7 +259,7 @@ function snapshotDirectory(root, target, state) {
216
259
 
217
260
  function captureSnapshot(options = {}) {
218
261
  const root = fs.realpathSync(path.resolve(options.projectRoot || process.cwd()));
219
- const state = { files: [], omitted: [], bytes: 0 };
262
+ const state = { files: [], omitted: [], bytes: 0, maxFiles: options.maxFiles ?? 1000, maxBytes: options.maxBytes ?? 16 * 1024 * 1024 };
220
263
  for (const relative of strings(options.targets)) {
221
264
  const target = projectPath(root, relative);
222
265
  if (!fs.existsSync(target)) { state.omitted.push({ path: relative, reason: "missing" }); continue; }
@@ -311,6 +354,10 @@ function checkedFileEvidence(options, files) {
311
354
  return captureReadEvidence(options, targets);
312
355
  }
313
356
 
357
+ function verificationCoverage(input, rules, config) {
358
+ return { ...input, complete: [input, rules, config].every(snapshot => snapshot.complete) };
359
+ }
360
+
314
361
  function finishExecution(handle, result = {}) {
315
362
  const { options, metadata } = handle;
316
363
  const checks = (result.checks || []).map(normalizeCheck);
@@ -325,7 +372,7 @@ function finishExecution(handle, result = {}) {
325
372
  const record = {
326
373
  ...baseRecord(metadata, "tool-finished"), executionId: handle.record.eventId, executionStartedAt: handle.record.recordedAt, tool: handle.record.tool, targets: handle.record.targets,
327
374
  executionStatus: exitCode === 0 ? "completed" : "failed", exitCode, durationMs: Date.now() - handle.startedAt,
328
- validationStatus: validationState(result.validationStatus, checks, after, changed), checks,
375
+ validationStatus: validationState(result.validationStatus, checks, verificationCoverage(after, ruleSnapshot, configSnapshot), changed), checks,
329
376
  checkedFiles: checkedSnapshot.files, checkedSnapshot, checkedScopeSource: Array.isArray(result.checkedFiles) ? "executor" : "unreported",
330
377
  summary: safeMetadata(result.summary || {}), inputSnapshot: after, inputBefore: handle.record.inputSnapshot,
331
378
  changedDuringCheck: changed, artifacts: captureSnapshot({ projectRoot: options.projectRoot, targets: result.artifacts || [] }).files,
@@ -385,7 +432,7 @@ function snapshotsFresh(snapshots, options) {
385
432
  }
386
433
 
387
434
  function currentSnapshot(snapshot, options) {
388
- return snapshot.resolution === "project-internal-realpath" ? captureReadEvidence(options, snapshot.targets) : captureSnapshot({ projectRoot: options.projectRoot, targets: snapshot.targets });
435
+ return ["project-internal-realpath","authorized-read-realpath"].includes(snapshot.resolution) ? captureReadEvidence(options, snapshot.targets) : captureSnapshot({ projectRoot: options.projectRoot, targets: snapshot.targets });
389
436
  }
390
437
 
391
438
  function pendingChecks(plan, tools) {
@@ -418,17 +465,19 @@ function statusForRecords(records, options) {
418
465
 
419
466
  function validationContext(plan, tools, options) {
420
467
  const pending = pendingChecks(plan, tools);
421
- const outdatedChecker = Boolean(options.packageVersion) && [...tools, ...plan ? [plan] : []].some((record) => record.packageVersion !== options.packageVersion);
468
+ const runtime = inspectRuntime(options.projectRoot,options);
469
+ const outdatedChecker = ['mismatch','invalid'].includes(runtime.status) || Boolean(options.packageVersion) && [...tools, ...plan ? [plan] : []].some((record) => record.packageVersion !== options.packageVersion);
422
470
  const planStale = Boolean(plan) && !planInputsFresh(plan, options);
423
471
  const stale = outdatedChecker || planStale || tools.some((record) => isFresh(record, options) === false);
424
472
  const verifiers = tools.filter((record) => record.checks.length || record.validationStatus !== "unverified");
425
473
  const scopeGaps = uncoveredScope(plan, verifiers, options);
426
474
  const unresolvedDecision = Boolean(plan) && ["gap", "ambiguous", "needs-context"].includes(plan.decision.status);
427
475
  const executionOrderUnverified = ambiguousExecutionOrder(tools);
428
- return { validationStatus: summarizeValidation(verifiers, pending, stale, unresolvedDecision || scopeGaps.length > 0 || executionOrderUnverified), pendingChecks: pending, stale, outdatedChecker, planStale, scopeGaps, unresolvedDecision, executionOrderUnverified };
476
+ return { validationStatus: summarizeValidation(verifiers, pending, stale, unresolvedDecision || scopeGaps.length > 0 || executionOrderUnverified), pendingChecks: pending, stale, outdatedChecker, planStale, scopeGaps, unresolvedDecision, executionOrderUnverified,runtime };
429
477
  }
430
478
 
431
479
  function planInputsFresh(plan, options) {
480
+ if(plan.decision.action?.mode === 'inspect' && !snapshotsFresh([plan.inputSnapshot],options))return false;
432
481
  if (!snapshotsFresh([plan.ruleSnapshot, plan.configSnapshot], options)) return false;
433
482
  if (!plan.decision.scope?.fingerprint) return true;
434
483
  const current = projectScope.resolveScope({ projectRoot: options.projectRoot, packageName: options.packageName });
@@ -438,7 +487,7 @@ function planInputsFresh(plan, options) {
438
487
  function uncoveredScope(plan, tools, options) {
439
488
  if (!plan) return [];
440
489
  const root = fs.realpathSync(path.resolve(options.projectRoot || process.cwd()));
441
- const rulePaths = new Set([...(plan.ruleSnapshot?.targets || []), ...strings(plan.decision.requiredFiles)].map((target) => readEvidencePath(root, target)));
490
+ const rulePaths = new Set([...(plan.ruleSnapshot?.targets || []), ...strings(plan.decision.requiredFiles)].map((target) => readEvidencePath(root, target,options.packageName)));
442
491
  const targets = plan.targets.filter((target) => !rulePaths.has(readEvidencePath(root, target)));
443
492
  if (!targets.length) return [];
444
493
  const current = captureSnapshot({ projectRoot: root, targets });
@@ -500,8 +549,8 @@ function listGaps(options) {
500
549
  return [...grouped.values()];
501
550
  }
502
551
 
503
- function inspectHostFile(root, relative, maximum) {
504
- const file = readEvidencePath(root, relative);
552
+ function inspectHostFile(root, relative, maximum,packageName) {
553
+ const file = readEvidencePath(root, relative,packageName);
505
554
  if (!fs.existsSync(file)) return { path: relative, status: "missing" };
506
555
  const stat = fs.statSync(file);
507
556
  return { path: relative, status: stat.isFile() ? "present" : "not-file", bytes: stat.size, oversized: stat.size > maximum, sha256: stat.isFile() ? digest(fs.readFileSync(file)) : null };
@@ -511,9 +560,9 @@ function doctorHost(options) {
511
560
  const root = fs.realpathSync(path.resolve(options.projectRoot || process.cwd()));
512
561
  const { scope } = projectScope.resolveScope({ projectRoot: root, packageName: options.packageName });
513
562
  const maximum = options.maxInstructionBytes || 32768;
514
- const entries = strings(options.entryFiles).map((relative) => inspectHostFile(root, relative, maximum));
515
- const skills = strings(options.skillPaths).map((relative) => inspectHostFile(root, relative, Infinity));
516
- const gateway = options.gatewayPath ? inspectHostFile(root, options.gatewayPath, Infinity) : null;
563
+ const entries = strings(options.entryFiles).map((relative) => inspectHostFile(root, relative, maximum,options.packageName));
564
+ const skills = strings(options.skillPaths).map((relative) => inspectHostFile(root, relative, Infinity,options.packageName));
565
+ const gateway = options.gatewayPath ? inspectHostFile(root, options.gatewayPath, Infinity,options.packageName) : null;
517
566
  const warnings = entries.filter((entry) => entry.oversized).map((entry) => `${entry.path} 超过 ${maximum} 字节参考预算;请核对宿主实际配置与内容加载范围`);
518
567
  const runtime = inspectRuntime(root, options);
519
568
  warnings.push(...runtime.warnings);
@@ -524,8 +573,8 @@ function doctorHost(options) {
524
573
  function hostReady(gateway, skills, entries, runtime) {
525
574
  return gateway?.status === "present" && skills.length > 0 && skills.every((skill) => skill.status === "present") && entries.every((entry) => entry.status === "present" && !entry.oversized) && ["aligned", "unverified"].includes(runtime.status);
526
575
  }
527
- function runtimeVersion(root, relative, diagnostics) {
528
- try { const value = JSON.parse(fs.readFileSync(readEvidencePath(root, relative), "utf8")).version; if (typeof value !== "string" || !value.trim()) throw new Error("version 必须是非空字符串"); return value; }
576
+ function runtimeVersion(root, relative, diagnostics,packageName) {
577
+ try { const value = JSON.parse(fs.readFileSync(readEvidencePath(root, relative,packageName), "utf8")).version; if (typeof value !== "string" || !value.trim()) throw new Error("version 必须是非空字符串"); return value; }
529
578
  catch (error) { if (error.code !== "ENOENT") diagnostics.push(`${relative}: ${error.message}`); return null; }
530
579
  }
531
580
  function declaredDependency(root, packageName) {
@@ -541,7 +590,7 @@ function inspectRuntime(root, options) {
541
590
  const manifestPath = { kit: ".wl-skills-manifest.json", design: ".wl-skills-design/state.json", test: ".wl-skills-test/manifest.json" }[key] || `.wl-skills-${key}-manifest.json`;
542
591
  const diagnostics = [];
543
592
  const distributedVersion = runtimeVersion(root, manifestPath, diagnostics);
544
- const localVersion = runtimeVersion(root, `node_modules/${options.packageName}/package.json`, diagnostics);
593
+ const localVersion = runtimeVersion(root, `node_modules/${options.packageName}/package.json`, diagnostics,options.packageName);
545
594
  const declaredVersion = declaredDependency(root, options.packageName);
546
595
  const mismatch = runtimeMismatch([distributedVersion, localVersion, options.packageVersion], declaredVersion, localVersion);
547
596
  const state = distributedVersion ? "aligned" : "unverified";
@@ -565,10 +614,40 @@ function noticeScope(record, options, item) {
565
614
  unverified: strings(item.unverified || ["host-discovery", "model-read-canonical-files", "planned-actions-not-executed"]) };
566
615
  }
567
616
  function noticeReason(item) { return safeText(item.reason || strings(item.reasons).join(";")); }
568
- function attachNotice(record, options = {}) {
617
+ function prepareNoticeDecision(record,options) {
569
618
  const item = record.decision || record;
570
619
  record.packageName ||= options.packageName;
571
620
  record.packageVersion ||= options.packageVersion;
621
+ prepareRuntimeNotice(record, item, options);
622
+ item.action ||= taskIntent.analyzeTask(record.task || item.task || "").action;
623
+ const details = new Map((item.ruleDetails || []).map((entry) => [entry.id, entry]));
624
+ item.ruleRefs ||= strings([...strings(item.requiredRules), ...details.keys()]).map((id) => {
625
+ const detail = details.get(id) || {};
626
+ return { packageName: record.packageName || null, ruleId: /^[a-zA-Z0-9_.:-]+$/.test(id) ? id : `constraint:${digest(id).slice(0, 16)}`,
627
+ legacyId: id, name: detail.name || id, ruleVersion: record.packageVersion || null,
628
+ source: detail.source || null, kind: detail.kind || "guidance", verification: "unverified" };
629
+ });
630
+ return item;
631
+ }
632
+
633
+ function prepareRuntimeNotice(record, item, options) {
634
+ if (!options.projectRoot || !options.packageName) return;
635
+ record.runtime = inspectRuntime(options.projectRoot, options);
636
+ runtimeReadiness(item, record.runtime);
637
+ }
638
+ function runtimeReadiness(item,runtime) {
639
+ if(item.scope?.status !== 'applicable' || !['mismatch','invalid'].includes(runtime.status))return;
640
+ const code = `runtime-${runtime.status}`;
641
+ item.routingStatus ||= item.status;
642
+ item.status = 'gap';item.ready = false;
643
+ const reason = '本包安装资产、本地依赖与当前执行器版本未对齐,暂不执行';
644
+ item.reason = reason;item.reasons = strings([...(item.reasons || []),reason]);
645
+ item.missingInputs = strings([...(item.missingInputs || []),'本包一致的安装资产与执行器版本']);
646
+ item.gaps ||= [];
647
+ if(!item.gaps.some(gap => gap.code === code))item.gaps.push({code,reason,suggestion:'在目标项目核对本包依赖和安装清单,正常 update 后重新判定;不需安装兄弟包'});
648
+ }
649
+ function attachNotice(record, options = {}) {
650
+ const item = prepareNoticeDecision(record,options);
572
651
  record.notice = {
573
652
  schemaVersion: 1, packageName: record.packageName || null, packageVersion: record.packageVersion || null,
574
653
  projectRoot: noticeProjectRoot(record, options), runId: record.runId || null, decision: item.status || "unverified",
@@ -576,6 +655,8 @@ function attachNotice(record, options = {}) {
576
655
  reason: noticeReason(item), missingInputs: strings(item.missingInputs),
577
656
  gaps: (item.gaps || []).map((gap) => ({ reason: safeText(gap.reason), suggestion: safeText(gap.suggestion) })),
578
657
  ...noticeScope(record, options, item), displayEvidence: "unverified",
658
+ action: item.action, ruleRefs: item.ruleRefs,
659
+ runtime:record.runtime || null,warnings:record.runtime?.warnings || [],
579
660
  };
580
661
  return record;
581
662
  }
@@ -594,7 +675,12 @@ function formatDecision(record) {
594
675
  const notice = record.notice || attachNotice({ ...record }).notice;
595
676
  const label = { baseline: "基础约束", ambiguous: "候选需确认", gap: "规则缺口", "not-applicable": "不适用", "needs-context": "缺少上下文" }[notice.decision] || "待判定";
596
677
  const identity = notice.packageName ? `${notice.packageName.replace("@agile-team/wl-skills-", "")}@${notice.packageVersion || "未知"};` : "";
597
- return `[WL ${notice.decision}] ${identity}${notice.skills.join("、") || label};${notice.reason}` + `\n适用规则:${noticeRuleText(notice)};范围:${notice.targets.slice(0, 3).join("、") || "未指定目标,检查范围待确认"}` + noticePlanText(notice) + noticeGapText(notice);
678
+ return `[WL ${notice.decision}] ${identity}${notice.skills.join("、") || label};${notice.reason}` + `\n动作=${notice.action.mode};允许自动检查=${notice.action.checksAllowed};业务写入遵循原授权契约` + `\n适用规则:${noticeRuleText(notice)};范围:${notice.targets.slice(0, 3).join("、") || "未指定目标,检查范围待确认"}` + noticePlanText(notice) + noticeGapText(notice) + runtimeText(notice.runtime);
679
+ }
680
+
681
+ function runtimeText(runtime) {
682
+ if(!runtime)return '';
683
+ return `\n版本对齐=${runtime.status};安装资产=${runtime.distributedVersion || '未分发'};本地依赖=${runtime.localVersion || '未安装'};当前执行器=${runtime.runningVersion || '未知'}${runtime.warnings.length ? ';' + runtime.warnings.join(';') : ''}`;
598
684
  }
599
685
 
600
686
  function formatStatus(status) {
@@ -397,7 +397,7 @@
397
397
  "pattern": "^[a-f0-9]{64}$"
398
398
  },
399
399
  "resolution": {
400
- "const": "project-internal-realpath"
400
+ "enum": ["project-internal-realpath", "authorized-read-realpath"]
401
401
  }
402
402
  }
403
403
  },
@@ -0,0 +1,93 @@
1
+ {
2
+ "element/el-table": [
3
+ "表格",
4
+ "table",
5
+ "el-table"
6
+ ],
7
+ "element/el-form": [
8
+ "表单",
9
+ "查询表单",
10
+ "el-form"
11
+ ],
12
+ "element/el-dialog": [
13
+ "弹窗",
14
+ "dialog",
15
+ "el-dialog"
16
+ ],
17
+ "element/el-tag": [
18
+ "标签颜色",
19
+ "状态标签",
20
+ "el-tag"
21
+ ],
22
+ "element/component-family": [
23
+ "卡片",
24
+ "页签",
25
+ "抽屉",
26
+ "el-tabs",
27
+ "上传控件"
28
+ ],
29
+ "runtime/design-tokens": [
30
+ "设计令牌",
31
+ "design tokens",
32
+ "颜色硬编码",
33
+ "tokens"
34
+ ],
35
+ "runtime/style-align": [
36
+ "样式统一",
37
+ "风格对齐",
38
+ "样式不生效"
39
+ ],
40
+ "runtime/migration": [
41
+ "runtime迁移",
42
+ "样式迁移"
43
+ ],
44
+ "ops/scan": [
45
+ "UI扫描",
46
+ "UI检查",
47
+ "样式扫描"
48
+ ],
49
+ "ops/audit": [
50
+ "UI审计",
51
+ "视觉审计"
52
+ ],
53
+ "ops/fix": [
54
+ "UI修复",
55
+ "修复样式",
56
+ "统一视觉"
57
+ ],
58
+ "ops/migrate": [
59
+ "UI迁移",
60
+ "迁移tokens"
61
+ ],
62
+ "layouts/list-page": [
63
+ "列表布局"
64
+ ],
65
+ "layouts/detail-page": [
66
+ "详情布局"
67
+ ],
68
+ "layouts/form-dialog": [
69
+ "表单弹窗布局"
70
+ ],
71
+ "layouts/tree-list": [
72
+ "树列表布局"
73
+ ],
74
+ "vendors/ag-grid": [
75
+ "AG Grid",
76
+ "ag-grid"
77
+ ],
78
+ "vendors/base-table": [
79
+ "BaseTable"
80
+ ],
81
+ "vendors/jh-components": [
82
+ "jh组件"
83
+ ],
84
+ "vendors/c-components": [
85
+ "C组件"
86
+ ],
87
+ "vendors/custom-wrappers": [
88
+ "自定义封装组件"
89
+ ],
90
+ "vendors/unknown-wrapper": [
91
+ "未知封装组件"
92
+ ]
93
+ }
@@ -23,3 +23,5 @@ wl-ui status --run-id <runId> --json
23
23
  UI scan 记录规则真实回调次数及 SFC_PARSE 错误;解析错误不受规则过滤隐藏。fast/fallback 仅提取区块,未验证 SFC 语法;compiler SFC 解析也不替代脚本 TypeScript 类型编译、浏览器视觉和业务语义复核。UI Vite 插件只检查依赖组合,明确列出源码 scan 尚未执行。运行时 guard 的存在不证明静态规范已通过。
24
24
 
25
25
  当前 CLI 检查回执覆盖 scan / audit / check / fix / all 与 Vite 依赖检查,本包 MCP 记录实际调用;其他 CLI snapshot / drift / exempt / contract / 脚手架保留既有产物与日志,不自动等同于 UI 源码检查。未由执行器报告实际文件的操作,其检查范围明确未知;不能用输入目录快照或成功退出补造文件覆盖。
26
+
27
+ 规则选择器来自实际注册表;未知 ID、格式错误和无界范围在扫描前拒绝。CLI 的未知参数、重复值选项、空值与额外位置参数同样不会启动检查或写入回执;JSON/summary/compact-v2 输出返回 `invalid-input` 与 `not-executed`。MCP 的错误调用返回 `isError=true` 与真实失败记录,保留同 runId,`checkedFiles` 与源码检查为空。已注册的规则选择和语法错误检查继续按原有契约执行。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agile-team/wl-skills-ui",
3
- "version": "1.18.0",
3
+ "version": "1.20.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",
@@ -0,0 +1,46 @@
1
+ import { parseArgs } from "node:util";
2
+ import { expandRuleSelector } from "./rule-selector.mjs";
3
+
4
+ const ACTIONS = { snapshot: ["list", "rollback", "diff", "clean"], exempt: ["init"] };
5
+ const MACHINE_OUTPUTS = new Set(["json", "summary", "compact-v2"]);
6
+
7
+ function validatePositionals(command, positionals) {
8
+ const actions = ACTIONS[command];
9
+ if ((!actions && positionals.length) || positionals.length > 1 || actions && positionals.length && !actions.includes(positionals[0])) {
10
+ throw new Error(`${command} 不接受位置参数 ${positionals.join(" ")};请使用该命令声明的选项或操作`);
11
+ }
12
+ }
13
+
14
+ function validateValues(values, tokens, options) {
15
+ const seen = new Set();
16
+ for (const token of tokens.filter(item => item.kind === "option")) {
17
+ if (options[token.name]?.type !== "string") continue;
18
+ if (seen.has(token.name)) throw new Error(`--${token.name} 只接受一个值,不静默覆盖前一个参数`);
19
+ seen.add(token.name);
20
+ if (!token.value.trim()) throw new Error(`--${token.name} 缺少有效值`);
21
+ }
22
+ for (const name of ["project", "target"]) {
23
+ if (values[name]?.startsWith("-")) throw new Error(`--${name} 缺少有效路径`);
24
+ }
25
+ }
26
+
27
+ function requestedOutput(args) {
28
+ const equals = args.find(argument => argument.startsWith("--output="));
29
+ const index = args.indexOf("--output");
30
+ return equals ? equals.slice("--output=".length) : index >= 0 ? args[index + 1] : undefined;
31
+ }
32
+
33
+ export function parseScannerInput(command, args, options, registered) {
34
+ try {
35
+ const parsed = parseArgs({ args, options, strict: true, allowPositionals: true, tokens: true });
36
+ validatePositionals(command, parsed.positionals);
37
+ validateValues(parsed.values, parsed.tokens, options);
38
+ for (const name of ["only", "skip"]) if (parsed.values[name]) expandRuleSelector(parsed.values[name], registered);
39
+ return parsed;
40
+ } catch (error) {
41
+ const message = error.message || String(error);
42
+ console.error(`[wl-scan] 无效参数:${message}`);
43
+ if (MACHINE_OUTPUTS.has(requestedOutput(args))) console.log(JSON.stringify({ ok: false, error: { code: "invalid-input", message }, executionStatus: "not-executed", validationStatus: "unverified", checkedFiles: [] }));
44
+ process.exit(1);
45
+ }
46
+ }
@@ -10,22 +10,10 @@ import { loadExemptConfig } from "./exempt.mjs";
10
10
  import { collectChangedVueFiles } from "./changed.mjs";
11
11
  import { parseVueSfc, SFC_PARSER_MODES } from "./sfc-parser.mjs";
12
12
  import { resolveProjectProfile } from "../standards/profiles-loader.mjs";
13
+ import { expandRuleSelector } from "./rule-selector.mjs";
13
14
 
14
15
  export function expandRuleRange(input = "") {
15
- const set = new Set();
16
- for (const part of input.split(",").map((value) => value.trim()).filter(Boolean)) {
17
- const match = part.match(/^(R)(\d+)-(R)?(\d+)$/i);
18
- if (!match) {
19
- set.add(part.toUpperCase());
20
- continue;
21
- }
22
- const start = Number.parseInt(match[2], 10);
23
- const end = Number.parseInt(match[4], 10);
24
- for (let id = Math.min(start, end); id <= Math.max(start, end); id++) {
25
- set.add(`R${String(id).padStart(3, "0")}`);
26
- }
27
- }
28
- return set;
16
+ return input === "" ? new Set() : expandRuleSelector(input, getRules().map(rule => rule.id));
29
17
  }
30
18
 
31
19
  /**
@@ -191,6 +179,8 @@ export function filterIssues(issues, { layer, vendor, mode, only, skip } = {}) {
191
179
  }
192
180
 
193
181
  export function scanProject(options = {}) {
182
+ // CLI and MCP share the same selector boundary; never scan first and discard errors later.
183
+ for (const selector of [options.only, options.skip]) if (selector) expandRuleRange(selector);
194
184
  const projectRoot = resolve(options.projectRoot || options.project || ".");
195
185
  const targetDir = resolve(projectRoot, options.target || "src");
196
186
  const changedFallback = options.changedFallback || "error";
package/scanner/index.mjs CHANGED
@@ -27,7 +27,8 @@
27
27
  */
28
28
  import { readFileSync, readdirSync, writeFileSync } from "node:fs";
29
29
  import { join, resolve, relative } from "node:path";
30
- import { parseArgs } from "node:util";
30
+ import { parseScannerInput } from "./cli-input.mjs";
31
+ import { expandRuleSelector } from "./rule-selector.mjs";
31
32
  import { getRules } from "./rules/index.mjs";
32
33
  import { generateReport } from "./report.mjs";
33
34
  import { checkIntegration } from "./integration.mjs";
@@ -67,9 +68,8 @@ if (args.length > 0 && SUBCOMMANDS.has(args[0])) {
67
68
  }
68
69
  const isAudit = subcommand === "audit";
69
70
 
70
- const { values } = parseArgs({
71
- args,
72
- options: {
71
+ const REGISTERED_RULES = getRules().map(rule => rule.id);
72
+ const { values, positionals } = parseScannerInput(subcommand, args, {
73
73
  target: { type: "string", default: "./src" },
74
74
  project: { type: "string", default: "." },
75
75
  output: { type: "string", default: "markdown" },
@@ -98,9 +98,17 @@ const { values } = parseArgs({
98
98
  limit: { type: "string", default: "100" },
99
99
  cursor: { type: "string", default: "0" },
100
100
  "run-id": { type: "string" },
101
+ help: { type: "boolean", short: "h" },
101
102
  },
102
- strict: false,
103
- });
103
+ REGISTERED_RULES);
104
+
105
+ if (values.help) {
106
+ console.log(`wl-ui ${subcommand} [--project <root>] [--target <file|directory>]\n` +
107
+ "scan/audit: --only R001,R006 | --skip R031-R037 | --output json|summary|compact-v2|markdown | --fail-on-error | --run-id <id>\n" +
108
+ "fix: --dry-run | --plan-hash <hash>; snapshot: list|rollback|diff|clean; exempt: init\n" +
109
+ "每个值选项只接受一个值;未知参数、操作和规则会在检查或写入前拒绝。");
110
+ process.exit(0);
111
+ }
104
112
 
105
113
  // ── 公共:扫描逻辑 ──────────────────────────────────────────────────────
106
114
  if (!SFC_PARSER_MODES.includes(values.parser)) {
@@ -155,20 +163,7 @@ function runScan(
155
163
 
156
164
  // ── 公共:规则范围展开(R031-R037 → R031,R032,...,R037)────────────────────
157
165
  function expandRuleRange(input) {
158
- const set = new Set();
159
- for (const part of input.split(",").map((s) => s.trim())) {
160
- const rangeMatch = part.match(/^(R)(\d+)-(R)?(\d+)$/i);
161
- if (rangeMatch) {
162
- const start = parseInt(rangeMatch[2]);
163
- const end = parseInt(rangeMatch[4]);
164
- for (let i = Math.min(start, end); i <= Math.max(start, end); i++) {
165
- set.add("R" + String(i).padStart(3, "0"));
166
- }
167
- } else {
168
- set.add(part.toUpperCase());
169
- }
170
- }
171
- return set;
166
+ return expandRuleSelector(input, REGISTERED_RULES);
172
167
  }
173
168
 
174
169
  // ── 公共:按 layer / vendor / mode 过滤 ─────────────────────────────────────
@@ -320,7 +315,7 @@ if (subcommand === "check") {
320
315
  }
321
316
 
322
317
  if (subcommand === "exempt") {
323
- const sub = args[0] || "init";
318
+ const sub = positionals[0] || "init";
324
319
  if (sub === "init") {
325
320
  const { existsSync } = await import("node:fs");
326
321
  const projectRoot = resolve(values.project);
@@ -412,7 +407,7 @@ if (subcommand === "drift") {
412
407
 
413
408
  if (subcommand === "snapshot") {
414
409
  const projectRoot = resolve(values.project);
415
- const sub = args[0] || "list";
410
+ const sub = positionals[0] || "list";
416
411
  if (sub === "list") {
417
412
  const snaps = listSnapshots(projectRoot);
418
413
  if (snaps.length === 0) {
@@ -0,0 +1,17 @@
1
+ /** Expand bounded, registered selectors before scans, records or writes begin. */
2
+ export function expandRuleSelector(input, registered) {
3
+ if (typeof input !== "string") throw new Error("规则选择器必须是字符串");
4
+ const selected = new Set();
5
+ const known = new Set([...registered, "SFC_PARSE"]);
6
+ for (const part of input.split(",").map(value => value.trim().toUpperCase())) {
7
+ const range = part.match(/^R(\d{3})-(?:R)?(\d{3})$/u);
8
+ if (range) {
9
+ const start = Number(range[1]);
10
+ const end = Number(range[2]);
11
+ for (let index = Math.min(start, end); index <= Math.max(start, end); index++) selected.add(`R${String(index).padStart(3, "0")}`);
12
+ } else selected.add(part);
13
+ }
14
+ const missing = [...selected].filter(id => !known.has(id));
15
+ if (missing.length) throw new Error(`不存在或格式无效的规则:${missing.join("、") || "空规则"};请用 wl-ui rules list 核对规则 ID`);
16
+ return selected;
17
+ }