@agile-team/wl-skills-ui 1.19.0 → 1.20.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 CHANGED
@@ -1,4 +1,19 @@
1
- # Changelog
1
+ # Changelog
2
+
3
+ ## [1.20.1] - 2026-10-11
4
+
5
+ - 修复 task 在版本漂移后旧顶层 status/ready 与嵌套 decision、notice、公开协议准入不一致;从校正后的记录生成旧字段,gap 始终 ready=false。
6
+ - task/route/explain/status 的旧顶层 ready 复用公开结果核心判定,并核对当前适用 scope;缺资产、歧义、未接入/移动项目、缺上下文或计划过期均不准入。
7
+ - 增加真实 CLI 和协议版本漂移、恢复后重判、缺口/范围/新鲜度回归;ready 不证明宿主读取、代码执行或业务验证通过。
8
+
9
+ ## [1.20.0] - 2026-10-11
10
+
11
+ - scan 严格校验已注册规则、范围与参数,CLI/MCP 使用同源选择器;非法请求零检查零任务记录。
12
+ - 合法 pnpm/npm-link 本包规则与执行器纳入新鲜度证据,不扩展兄弟包或业务路径权限。
13
+ - 解释、方案/步骤、背景引用与禁止检查的动作判定收敛;任务意图仍不授予业务写入权限。
14
+ - notice 与 result.integration 暴露实际版本对齐,漂移返回 gap/ready=false;只读任务输入变化需重新判定,修改任务允许同 runId 复扫。
15
+ - 本包外部依赖的只读证据核实身份与真实边界,累计快照预算和 Schema 同步,保持零兄弟包/Harness 运行时依赖。
16
+
2
17
 
3
18
  ## [1.19.0] - 2026-10-11
4
19
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 项目范围:只对目标自身已接入且平台适用的项目判定规则;父工作区安装不启用子项目。移动端与未接入开源项目不套用。详见 [精准触发与范围配置](docs/project-scope.md)。
4
4
 
5
- **企业级 UI 一致性与 AI 精确治理工具包 v1.17.0** — 面向 Vue 3 + Element Plus 的设计令牌、能力 Profile、样式皮肤、页面骨架、运行时能力、静态扫描、确定性修复、AI Skill、MCP 与脱敏 UI 模板契约。
5
+ **企业级 UI 一致性与 AI 精确治理工具包 v1.20.1** — 面向 Vue 3 + Element Plus 的设计令牌、能力 Profile、样式皮肤、页面骨架、运行时能力、静态扫描、确定性修复、AI Skill、MCP 与脱敏 UI 模板契约。
6
6
 
7
7
  它把“视觉统一、问题识别、机械修复、项目适配和样例沉淀”拆成可验证、可组合的工程能力。脚本、规则和 MCP 先完成确定性工作,AI 只处理业务语义与人工决策,从而减少重复读源码、上下文 token 和误修改。
8
8
 
@@ -17,6 +17,8 @@
17
17
 
18
18
  `wl-ui doctor-host --json` 对比已分发规范、本地执行器和正在运行的版本,漂移会显式报告。规范更新不会替代依赖升级:同步本包依赖、锁文件和受管入口;未使用的兄弟包无需安装。重开/刷新宿主加载后仍需观察真实任务调用,不能宣称所有 AI 自动触发。
19
19
 
20
+ `task/route/explain/status` 的旧顶层 `ready` 与公开协议的 `result.integration.ready` 表示本次工作流的有效就绪状态:当前项目必须已接入且 `scope=applicable`,判定为 matched/baseline,所需资产完整、版本没有已知漂移、计划尚未过期。gap、歧义、缺上下文、未接入/移动项目均为 false,不能只因规范文件存在就称 ready。漂移时旧顶层 task 的 status/ready 与嵌套 decision、notice 保持一致;状态回查以当前 scope 和新鲜度为准。ready=true 不证明宿主读取、自动检查权限、工具执行或业务验证通过,执行仍需核对动作许可和 allowedExecutors。
21
+
20
22
  ## 你能获得什么
21
23
 
22
24
  | 目标 | 包提供的能力 | 带来的效果 |
@@ -510,7 +512,7 @@ npx wl-ui doctor --project . --print-overrides
510
512
 
511
513
  ## 版本与进一步阅读
512
514
 
513
- 当前 v1.19.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 依赖识别。
515
+ 当前 v1.20.1:公开集成协议 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
516
 
515
517
  - [版本摘要](docs/version-history.md)
516
518
  - [完整 Changelog](CHANGELOG.md)
@@ -544,3 +546,11 @@ npm run pack:dry
544
546
  `inventory.executors` 声明本包业务只读检查器、目标与参数 Schema;其适用条件由本包判定。外部适配器须核对同项目、同包、同版本、同 runId 的真实计划、新鲜度与允许动作,指定一个目标,不能扩大范围;支持规则子集的执行器不得选择计划外规则。多目标需拆分调用。五包和 Harness 均为可选组合,不新增兄弟包或 Harness 运行时依赖;卸下适配器后原 CLI/MCP 继续独立使用。
545
547
 
546
548
  样式规则检查使用 `scan`(支持计划内规则子集);`check` 只核验接入完整性。技能触发词由本包 `bin/task-triggers.json` 维护并接受目录一致性检查。
549
+
550
+ `scan/audit/fix` 的未知参数、重复值选项、空值、多余位置参数和不存在的规则选择器在检查或写入前拒绝,机读输出返回 `invalid-input`,不会形成成功检查记录。CLI 与 MCP 共用注册规则选择器,支持单个 ID、逗号列表与三位编号范围(例如 `R031-R037`);用 `wl-ui rules list` 核对实际规则。Vue 解析错误仍不能被 `--only/--skip` 隐藏。`wl-ui scan --help` 只输出帮助,不启动扫描。
551
+
552
+ ### 任务反馈与执行边界
553
+
554
+ 编辑前展示实际 `notice` 的包版本、判定、动作、技能/规则、目标、runId、待检查项与版本对齐信息;解释/规划和明确禁止检查时不自动执行检查。`runtime=mismatch/invalid` 或 `planStale=true` 必须先同步本包或重新判定。只读 `inspect` 输入变化后重新 `task`;`apply` 完成正常业务修改后可用同 runId 做实际复扫,但规则、配置或版本变化仍需重新判定。
555
+
556
+ 单包只读取自身公开能力;多包复用同 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,133 @@
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
+
8
+ const repo = path.resolve(import.meta.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 key = 'ui';
12
+ const manifestName = key === "kit" ? ".wl-skills-manifest.json" : ".wl-skills-ui-manifest.json";
13
+ const selected = "src/views/Example/index.vue";
14
+
15
+ function fixture() {
16
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), `wl-${key}-ready-`));
17
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({private: true, devDependencies: {[pkg.name]: pkg.version, vue: "^3.4.0"}}));
18
+ fs.mkdirSync(path.join(root, "node_modules/@agile-team"), {recursive: true});
19
+ fs.symlinkSync(repo, path.join(root, "node_modules", pkg.name), "dir");
20
+ fs.mkdirSync(path.dirname(path.join(root, selected)), {recursive: true});
21
+ fs.writeFileSync(path.join(root, selected), "<template><div>example</div></template>\n");
22
+ const initialized = spawnSync(process.execPath, [cli, "init", ...(key === "ui" ? ["--editor", "all", "--profile", "native-element"] : [])], {cwd: root, encoding: "utf8", timeout: 30000});
23
+ assert.equal(initialized.status, 0, initialized.stderr || initialized.stdout);
24
+ return root;
25
+ }
26
+
27
+ function request(root, transport, operation, input = {}) {
28
+ let args;
29
+ if (transport === "protocol") {
30
+ const file = path.join(root, "request.json");
31
+ fs.writeFileSync(file, JSON.stringify({operation, projectRoot: root, ...input}));
32
+ args = ["protocol", "request", "--input-file", file, "--json"];
33
+ } else {
34
+ args = [operation, ...(input.task ? [input.task] : []), ...(input.targets || []).flatMap(target => ["--target", target]), ...(input.runId ? ["--run-id", input.runId] : []), "--json"];
35
+ }
36
+ const processResult = spawnSync(process.execPath, [cli, ...args], {cwd: root, encoding: "utf8", timeout: 30000});
37
+ assert.equal(processResult.status, 0, processResult.stderr || processResult.stdout);
38
+ const result = JSON.parse(processResult.stdout);
39
+ if (transport === "protocol") {assert.equal(result.ok, true);return result.result;}
40
+ return result;
41
+ }
42
+
43
+ function readiness(result, ready, status, runtime) {
44
+ const decision = result.decision || result;
45
+ assert.equal(result.ready, ready, "legacy ready must express effective admission");
46
+ if (status) assert.equal(decision.status, status);
47
+ if (result.status && result.decision) assert.equal(result.status, decision.status, "flattened task status agrees with its recorded decision");
48
+ if (runtime) assert.equal(result.notice.runtime.status, runtime);
49
+ if (result.integration) {
50
+ assert.equal(result.integration.ready, ready);
51
+ if (!ready) assert.deepEqual(result.integration.allowedExecutors, []);
52
+ }
53
+ }
54
+
55
+ test("legacy 与公开协议:真实版本漂移拒绝且 task/status/route/explain 的 ready 一致", () => {
56
+ const root = fixture();
57
+ try {
58
+ for (const transport of ["legacy", "protocol"]) {
59
+ for (const operation of ["route", "explain", "task"]) {
60
+ const input = {task: "检查当前 Vue 代码", targets: [selected], ...(operation === "task" ? {runId: `aligned-${transport}`} : {})};
61
+ readiness(request(root, transport, operation, input), true, "baseline", "aligned");
62
+ }
63
+ readiness(request(root, transport, "status", {runId: `aligned-${transport}`}), true, "baseline", "aligned");
64
+ }
65
+ const manifest = path.join(root, manifestName);
66
+ const bytes = fs.readFileSync(manifest);
67
+ const old = JSON.parse(bytes);old.version = key === "kit" ? "2.30.0" : "1.19.0";
68
+ fs.writeFileSync(manifest, JSON.stringify(old));
69
+ for (const transport of ["legacy", "protocol"]) {
70
+ for (const operation of ["route", "explain", "task"]) {
71
+ const input = {task: "检查当前 Vue 代码", targets: [selected], ...(operation === "task" ? {runId: `drift-${transport}`} : {})};
72
+ const result = request(root, transport, operation, input);
73
+ readiness(result, false, "gap", "mismatch");
74
+ assert.equal((result.decision || result).scope.status, "applicable");
75
+ assert.ok((result.decision || result).gaps.some(gap => gap.code === "runtime-mismatch"));
76
+ }
77
+ readiness(request(root, transport, "status", {runId: `aligned-${transport}`}), false, "gap", "mismatch");
78
+ readiness(request(root, transport, "status", {runId: `drift-${transport}`}), false, "gap", "mismatch");
79
+ }
80
+ fs.writeFileSync(manifest, bytes);
81
+ readiness(request(root, "protocol", "task", {task: "检查当前 Vue 代码", targets: [selected], runId: "replanned"}), true, "baseline", "aligned");
82
+ readiness(request(root, "protocol", "status", {runId: "replanned"}), true, "baseline", "aligned");
83
+ } finally {fs.rmSync(root, {recursive: true, force: true});}
84
+ });
85
+
86
+ test("规则资产存在不能替代适用scope、无歧义与计划新鲜度", () => {
87
+ const root = fixture();
88
+ try {
89
+ for (const transport of ["legacy", "protocol"]) {
90
+ readiness(request(root, transport, "route", {task: "帮我写诗"}), false, "not-applicable");
91
+ readiness(request(root, transport, "route", {task: "闲聊"}), false, "needs-context");
92
+ readiness(request(root, transport, "route", {task: key === "kit" ? "创建页面并自动修复" : "检查表格和表单", targets: [selected]}), false, "ambiguous");
93
+ readiness(request(root, transport, "status", {runId: "absent"}), false);
94
+ }
95
+ const owned = key === "kit" ? path.join(root, ".wl-skills/standards/03-comments.md") : path.join(root, "node_modules", pkg.name);
96
+ const bytes = key === "kit" ? fs.readFileSync(owned) : null;
97
+ fs.unlinkSync(owned);
98
+ for (const transport of ["legacy", "protocol"]) {
99
+ const result = request(root, transport, "route", {task: "检查当前 Vue 代码", targets: [selected]});
100
+ readiness(result, false, "gap");
101
+ assert.ok((result.decision || result).missingInputs.length > 0);
102
+ }
103
+ if (key === "kit") fs.writeFileSync(owned, bytes);
104
+ else fs.symlinkSync(repo, owned, "dir");
105
+ readiness(request(root, "legacy", "task", {task: "检查当前 Vue 代码", targets: [selected], runId: "freshness"}), true, "baseline");
106
+ fs.appendFileSync(path.join(root, selected), "<!-- modified after inspect plan -->\n");
107
+ for (const transport of ["legacy", "protocol"]) {
108
+ const result = request(root, transport, "status", {runId: "freshness"});
109
+ assert.equal(result.planStale, true);readiness(result, false, "baseline");
110
+ }
111
+ fs.writeFileSync(path.join(root, ".wl-skills-scope.json"), JSON.stringify({schemaVersion: 1, projectType: "pc", packages: {[key]: false}}));
112
+ for (const transport of ["legacy", "protocol"]) {
113
+ const result = request(root, transport, "status", {runId: "freshness"});
114
+ assert.equal(result.currentScope.status, "not-applicable");readiness(result, false);
115
+ readiness(request(root, transport, "task", {task: "检查当前 Vue 代码", targets: [selected], runId: "disabled"}), false, "not-applicable");
116
+ }
117
+ } finally {fs.rmSync(root, {recursive: true, force: true});}
118
+ });
119
+
120
+ test("未接入和移动项目即使有Vue任务对象也不准入,不新建任务记录", () => {
121
+ for (const mobile of [false, true]) {
122
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), `wl-${key}-excluded-`));
123
+ try {
124
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({private: true, devDependencies: {vue: "^3.4.0", ...(mobile ? {[pkg.name]: pkg.version, "@dcloudio/uni-app": "*"} : {})}}));
125
+ for (const transport of ["legacy", "protocol"]) {
126
+ const result = request(root, transport, "task", {task: "检查当前 Vue 代码", targets: [selected], runId: "excluded"});
127
+ readiness(result, false, "not-applicable");
128
+ assert.deepEqual(result.requiredRules, []);
129
+ }
130
+ assert.equal(fs.existsSync(path.join(root, key === "kit" ? ".wl-skills/runs" : ".wl-skills-ui/runs")), false);
131
+ } finally {fs.rmSync(root, {recursive: true, force: true});}
132
+ }
133
+ });
@@ -21,6 +21,7 @@ const SCHEMA = {
21
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
22
  ready: { type: ["boolean", "null"] }, executionStatus: nullableString, validationStatus: nullableString,
23
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}]},
24
25
  },
25
26
  };
26
27
 
@@ -35,6 +36,7 @@ function allowedExecutors(decision, executors) {
35
36
  }
36
37
 
37
38
  function resultReadiness(result, decision, status) {
39
+ if (result.planStale || decision.ready === false || status && !["matched", "baseline"].includes(status)) return false;
38
40
  if (typeof result.ready === "boolean") return result.ready;
39
41
  if (typeof decision.ready === "boolean") return decision.ready;
40
42
  return status ? ["matched", "baseline"].includes(status) && !(decision.missingInputs || []).length : null;
@@ -56,6 +58,8 @@ function decisionReadout(decision) {
56
58
  function targetReadout(result,input) { return strings(result.targets || input.targets || result.notice?.targets); }
57
59
  function projectIdentity(scope,result,input) { return path.resolve(scope?.projectRoot || result.projectRoot || result.root || input.projectRoot || process.cwd()); }
58
60
 
61
+ function runtimeReadout(result) { return result.runtime || result.notice?.runtime || null; }
62
+
59
63
  function projectResult(result, input, config) {
60
64
  const decision = result.decision || result;
61
65
  const scope = result.currentScope || decision.scope || result.scope || null;
@@ -68,6 +72,7 @@ function projectResult(result, input, config) {
68
72
  projectRoot: projectIdentity(scope,result,input),
69
73
  ...executionState(result), decisionStatus: status, scope,
70
74
  ...decisionReadout(decision), targets: targetReadout(result,input), ready,
75
+ runtime: runtimeReadout(result),
71
76
  allowedExecutors: ready && scope?.status === "applicable" ? allowedExecutors(decision, config.executors || []) : [],
72
77
  },
73
78
  };
@@ -4,6 +4,7 @@ import { parseArgs } from "node:util";
4
4
  import core from "./task-observability.cjs";
5
5
  import projectScope from "./project-scope.cjs";
6
6
  import taskIntent from "./task-intent.cjs";
7
+ import resultCore from "./integration-result.cjs";
7
8
  import { GATEWAY_PATH } from "./task-gateway.mjs";
8
9
  import { resolveProjectProfile } from "../standards/profiles-loader.mjs";
9
10
  import { getRules } from "../scanner/rules/index.mjs";
@@ -44,8 +45,23 @@ export function taskOptions(projectRoot, extra = {}) {
44
45
  }
45
46
 
46
47
  function projectEvidencePaths(projectRoot, candidates) {
47
- return candidates.filter((candidate) => existsSync(candidate)).map((candidate) => relative(projectRoot, realpathSync(candidate)).replace(/\\/g, "/"))
48
- .filter((rel) => rel && !rel.startsWith("../") && !isAbsolute(rel));
48
+ return candidates.filter((candidate) => existsSync(candidate)).map((candidate) => projectEvidencePath(projectRoot, candidate)).filter(Boolean);
49
+ }
50
+
51
+ function evidenceRelative(projectRoot, target) {
52
+ const value = relative(projectRoot, target);
53
+ return value && value !== ".." && !value.startsWith("../") && !value.startsWith("..\\") && !isAbsolute(value) ? value.replace(/\\/g, "/") : null;
54
+ }
55
+
56
+ function projectEvidencePath(projectRoot, candidate) {
57
+ const physical = realpathSync(candidate);
58
+ const local = evidenceRelative(projectRoot, physical);
59
+ if (local) return local;
60
+ const dependency = join(projectRoot, "node_modules", pkg.name);
61
+ if (!existsSync(dependency)) return null;
62
+ const internal = evidenceRelative(realpathSync(dependency), physical);
63
+ // The shared reader still verifies package identity and its own realpath boundary.
64
+ return internal ? `node_modules/${pkg.name}/${internal}` : null;
49
65
  }
50
66
 
51
67
  export function decideTask(input) {
@@ -63,9 +79,9 @@ function capabilityGap(decision, reason) {
63
79
  export function runTaskAction(action, input = {}) {
64
80
  const resolved = projectScope.resolveScope({ projectRoot: input.projectRoot || process.env.WL_PROJECT_ROOT || process.cwd(), packageName: pkg.name, targets: input.targets || [] });
65
81
  const opts = taskOptions(resolved.projectRoot, { runId: input.runId, targets: resolved.targets });
66
- if (action === "status") return core.readStatus(opts);
82
+ if (action === "status") return taskResultReadiness(core.readStatus(opts), opts);
67
83
  if (action === "doctor-host") return hostDiagnostic({ ...opts, host: input.host || "unknown" });
68
- if (resolved.scope.status !== "applicable") return core.attachNotice(projectScope.excludedDecision(resolved.scope), opts);
84
+ if (resolved.scope.status !== "applicable") return taskResultReadiness(core.attachNotice(projectScope.excludedDecision(resolved.scope), opts), opts);
69
85
  const profile = resolveProjectProfile({ projectRoot: opts.projectRoot, profile: input.profile }).profile;
70
86
  const routed = decideTask({ task: input.task || "", targets: opts.targets, skill: input.skill, context: input.context });
71
87
  const applicable = getRules({ profile: profile.id });
@@ -75,8 +91,14 @@ export function runTaskAction(action, input = {}) {
75
91
  decision.requiredRules = decision.requiredRules.filter((id) => enabled.has(id));
76
92
  decision.ruleDetails = applicable.filter((rule) => decision.requiredRules.includes(rule.id)).map((rule) => ({ id: rule.id, name: rule.title, source: `node_modules/${pkg.name}/standards/rules.json` }));
77
93
  assetReadiness(decision, opts.projectRoot);
78
- if (action !== "task") return core.attachNotice(decision, opts);
79
- return core.attachNotice({ ...decision, ...core.startTask({ ...opts, task: input.task || "", decision }) }, opts);
94
+ if (action !== "task") return taskResultReadiness(core.attachNotice(decision, opts), opts);
95
+ const record = core.startTask({ ...opts, task: input.task || "", decision });
96
+ return taskResultReadiness(core.attachNotice({ ...record.decision, ...record }, opts), opts);
97
+ }
98
+
99
+ function taskResultReadiness(result, opts) {
100
+ const effective = resultCore.projectResult(result, opts, { packageName: pkg.name, packageVersion: pkg.version }).integration;
101
+ return { ...result, ready: effective.scope?.status === "applicable" && effective.ready === true };
80
102
  }
81
103
 
82
104
  function hostDiagnostic(opts) {
@@ -5,25 +5,50 @@
5
5
  * Preserve the prompt; exclude explicit prohibitions and historical clauses from
6
6
  * capability scoring. Intent never substitutes for business write authorization.
7
7
  */
8
- const HISTORY = /(?:旧(?:需求|文档|任务|方案)|历史(?:需求|任务|记录)|上(?:次|一轮|轮)(?:需求|任务|要求|说)|(?:用户|客户|之前|以前).{0,8}(?:曾|原先)(?:说|要求|提)|曾(?:说|要求|提到)|previous(?:\s+(?:request|task))?|previously|used to|historical)/iu;
8
+ const HISTORY = /^(?:旧(?:需求|文档|任务|方案)|历史(?:需求|任务|记录)|上(?:次|一轮|轮)|之前|以前|(?:用户|客户).{0,8}(?:曾|原先)(?:说|要求|提)|曾(?:说|要求|提到)|previous(?:\s+(?:request|task))?|previously|used to|historical)/iu;
9
9
  const CURRENT = /(?:本次|这次|此次|现在|当前任务|实际要求|改为|而是|this time|now|instead)/iu;
10
- const NEGATIVE = /^(?:(?:请|本次|这次|此次|暂时|暂|先|也|并且|且|同时|还)\s*)*(?:不要|不用|无需|禁止|别|不再|不需要|不必|不(?:做|用|画|生成|执行|运行|新增|实现|修改|改写|迁移|创建|编写|修复|自动)|do not\b|don't\b|without\b|skip\b)/iu;
10
+ const NEGATIVE = /^(?:(?:请|本次|这次|此次|本轮|现在|这里|暂时|暂|先|也|并且|且|同时|还)\s*)*(?:不要|不用|无需|禁止|不得|不可|不能|切勿|别|不再|不需要|不必|不(?:做|用|画|生成|执行|运行|跑|检查|校验|扫描|测试|新增|实现|修改|改写|迁移|创建|编写|修复|自动)|跳过|do not\b|don't\b|don’t\b|without\b|skip\b)/iu;
11
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)\b/iu;
13
- const DISCUSSION = /(?:(?:只|仅)(?:需要)?(?:分析|讨论|解释|说明|了解|咨询)|(?:先|暂时)(?:分析|讨论|解释|说明)|\b(?:only|just)\s+(?:explain|discuss|describe|analy[sz]e)\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;
14
19
 
15
20
  function normalized(text) { return String(text || "").normalize("NFKC").toLowerCase().trim(); }
16
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
+
17
42
  function analyzeTask(task) {
18
43
  const original = String(task || "").normalize("NFKC");
19
- // Current-request markers and contrast conjunctions start a new evidence clause.
20
- const clauses = original.replace(/(但(?:是)?|而是|改为|(?:本次|这次|此次|现在)(?=\s*(?:只|仅|请|要|需要|检查|审查|生成|修改|补充)))/gu, "\n$1")
21
- .split(/[,,。;;!?!?\n]+/u).map((part) => part.trim()).filter(Boolean);
44
+ const clauses = clausesFor(original);
22
45
  const excluded = [];
23
46
  const active = [];
24
- for (const raw of clauses) {
25
- const clause = raw.replace(/^(?:但是|但|而是|改为)\s*/u, "").trim();
26
- if (HISTORY.test(clause) && !CURRENT.test(clause)) {
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)) {
27
52
  excluded.push({ kind: "historical", text: clause });
28
53
  } else if (NEGATIVE.test(clause)) {
29
54
  excluded.push({ kind: "prohibited", text: clause });
@@ -33,12 +58,31 @@ function analyzeTask(task) {
33
58
  return {schemaVersion:1,activeText,excluded,action:actionEvidence(activeText,excluded)};
34
59
  }
35
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
+
36
82
  function actionEvidence(activeText, excluded) {
37
- const discussion = DISCUSSION.test(activeText);
38
- const mode = discussion ? "explain" : /(?:只|仅|先)?(?:列|制定|提供|给出|做)(?:一个|一份)?[^,。;\n]{0,40}计划|\bplan\b/iu.test(activeText) ? "plan"
39
- : APPLY.test(activeText) ? "apply" : INSPECT.test(activeText) ? "inspect"
40
- : /解释|说明|讨论|咨询|\b(?:explain|discuss|describe)\b/iu.test(activeText) ? "explain" : "unspecified";
41
- const genericStop = excluded.some((item) => item.kind === "prohibited" && /(?:暂|先|不要|不)(?:自动)?(?:执行|运行)(?:任何|所有)?(?:工具|命令|检查|校验)?\s*$|\b(?:do not|don't|skip)\s+(?:execute|run)\s+(?:(?:any|all)\s+)?(?:tools?|commands?|checks?)\s*$/iu.test(item.text));
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));
42
86
  return {
43
87
  mode, checksAllowed: !genericStop && ["inspect", "apply"].includes(mode), businessWritesAuthorized: false,
44
88
  authorization: "intent-is-not-business-write-authorization",
@@ -160,7 +160,31 @@ function projectPath(root, relative) {
160
160
  return inspectAncestors(root, path.resolve(root, relative));
161
161
  }
162
162
 
163
- 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) {
164
188
  if (typeof relative !== "string" || !relative) throw new Error("WL evidence path is required");
165
189
  const requested = path.resolve(root, relative);
166
190
  const lexical = path.relative(root, requested);
@@ -172,16 +196,44 @@ function readEvidencePath(root, relative) {
172
196
  ancestor = path.dirname(ancestor);
173
197
  }
174
198
  const physical = path.resolve(fs.realpathSync(ancestor), ...suffix);
175
- const resolved = path.relative(root, physical);
176
- if (resolved.startsWith("..") || path.isAbsolute(resolved)) throw new Error("WL read evidence escapes project root");
199
+ authorizeRead(root,requested,physical,packageName);
177
200
  return physical;
178
201
  }
179
202
 
180
203
  function captureReadEvidence(options, targets) {
181
204
  const root = fs.realpathSync(path.resolve(options.projectRoot || process.cwd()));
182
205
  const requested = strings(targets);
183
- const snapshot = captureSnapshot({ projectRoot: root, targets: requested.map((target) => path.relative(root, readEvidencePath(root, target)) || ".") });
184
- 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}))};
185
237
  }
186
238
 
187
239
  function snapshotFile(root, target, state) {
@@ -190,7 +242,7 @@ function snapshotFile(root, target, state) {
190
242
  if (stat.isSymbolicLink()) { state.omitted.push({ path: relative, reason: "symlink" }); return; }
191
243
  if (stat.isDirectory()) { snapshotDirectory(root, target, state); return; }
192
244
  if (!stat.isFile()) { state.omitted.push({ path: relative, reason: "not-file" }); return; }
193
- 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; }
194
246
  state.files.push({ path: relative, sha256: digest(fs.readFileSync(target)), bytes: stat.size });
195
247
  state.bytes += stat.size;
196
248
  }
@@ -207,7 +259,7 @@ function snapshotDirectory(root, target, state) {
207
259
 
208
260
  function captureSnapshot(options = {}) {
209
261
  const root = fs.realpathSync(path.resolve(options.projectRoot || process.cwd()));
210
- const state = { files: [], omitted: [], bytes: 0 };
262
+ const state = { files: [], omitted: [], bytes: 0, maxFiles: options.maxFiles ?? 1000, maxBytes: options.maxBytes ?? 16 * 1024 * 1024 };
211
263
  for (const relative of strings(options.targets)) {
212
264
  const target = projectPath(root, relative);
213
265
  if (!fs.existsSync(target)) { state.omitted.push({ path: relative, reason: "missing" }); continue; }
@@ -302,6 +354,10 @@ function checkedFileEvidence(options, files) {
302
354
  return captureReadEvidence(options, targets);
303
355
  }
304
356
 
357
+ function verificationCoverage(input, rules, config) {
358
+ return { ...input, complete: [input, rules, config].every(snapshot => snapshot.complete) };
359
+ }
360
+
305
361
  function finishExecution(handle, result = {}) {
306
362
  const { options, metadata } = handle;
307
363
  const checks = (result.checks || []).map(normalizeCheck);
@@ -316,7 +372,7 @@ function finishExecution(handle, result = {}) {
316
372
  const record = {
317
373
  ...baseRecord(metadata, "tool-finished"), executionId: handle.record.eventId, executionStartedAt: handle.record.recordedAt, tool: handle.record.tool, targets: handle.record.targets,
318
374
  executionStatus: exitCode === 0 ? "completed" : "failed", exitCode, durationMs: Date.now() - handle.startedAt,
319
- validationStatus: validationState(result.validationStatus, checks, after, changed), checks,
375
+ validationStatus: validationState(result.validationStatus, checks, verificationCoverage(after, ruleSnapshot, configSnapshot), changed), checks,
320
376
  checkedFiles: checkedSnapshot.files, checkedSnapshot, checkedScopeSource: Array.isArray(result.checkedFiles) ? "executor" : "unreported",
321
377
  summary: safeMetadata(result.summary || {}), inputSnapshot: after, inputBefore: handle.record.inputSnapshot,
322
378
  changedDuringCheck: changed, artifacts: captureSnapshot({ projectRoot: options.projectRoot, targets: result.artifacts || [] }).files,
@@ -376,7 +432,7 @@ function snapshotsFresh(snapshots, options) {
376
432
  }
377
433
 
378
434
  function currentSnapshot(snapshot, options) {
379
- 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 });
380
436
  }
381
437
 
382
438
  function pendingChecks(plan, tools) {
@@ -409,17 +465,19 @@ function statusForRecords(records, options) {
409
465
 
410
466
  function validationContext(plan, tools, options) {
411
467
  const pending = pendingChecks(plan, tools);
412
- 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);
413
470
  const planStale = Boolean(plan) && !planInputsFresh(plan, options);
414
471
  const stale = outdatedChecker || planStale || tools.some((record) => isFresh(record, options) === false);
415
472
  const verifiers = tools.filter((record) => record.checks.length || record.validationStatus !== "unverified");
416
473
  const scopeGaps = uncoveredScope(plan, verifiers, options);
417
474
  const unresolvedDecision = Boolean(plan) && ["gap", "ambiguous", "needs-context"].includes(plan.decision.status);
418
475
  const executionOrderUnverified = ambiguousExecutionOrder(tools);
419
- 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 };
420
477
  }
421
478
 
422
479
  function planInputsFresh(plan, options) {
480
+ if(plan.decision.action?.mode === 'inspect' && !snapshotsFresh([plan.inputSnapshot],options))return false;
423
481
  if (!snapshotsFresh([plan.ruleSnapshot, plan.configSnapshot], options)) return false;
424
482
  if (!plan.decision.scope?.fingerprint) return true;
425
483
  const current = projectScope.resolveScope({ projectRoot: options.projectRoot, packageName: options.packageName });
@@ -429,7 +487,7 @@ function planInputsFresh(plan, options) {
429
487
  function uncoveredScope(plan, tools, options) {
430
488
  if (!plan) return [];
431
489
  const root = fs.realpathSync(path.resolve(options.projectRoot || process.cwd()));
432
- 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)));
433
491
  const targets = plan.targets.filter((target) => !rulePaths.has(readEvidencePath(root, target)));
434
492
  if (!targets.length) return [];
435
493
  const current = captureSnapshot({ projectRoot: root, targets });
@@ -491,8 +549,8 @@ function listGaps(options) {
491
549
  return [...grouped.values()];
492
550
  }
493
551
 
494
- function inspectHostFile(root, relative, maximum) {
495
- const file = readEvidencePath(root, relative);
552
+ function inspectHostFile(root, relative, maximum,packageName) {
553
+ const file = readEvidencePath(root, relative,packageName);
496
554
  if (!fs.existsSync(file)) return { path: relative, status: "missing" };
497
555
  const stat = fs.statSync(file);
498
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 };
@@ -502,9 +560,9 @@ function doctorHost(options) {
502
560
  const root = fs.realpathSync(path.resolve(options.projectRoot || process.cwd()));
503
561
  const { scope } = projectScope.resolveScope({ projectRoot: root, packageName: options.packageName });
504
562
  const maximum = options.maxInstructionBytes || 32768;
505
- const entries = strings(options.entryFiles).map((relative) => inspectHostFile(root, relative, maximum));
506
- const skills = strings(options.skillPaths).map((relative) => inspectHostFile(root, relative, Infinity));
507
- 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;
508
566
  const warnings = entries.filter((entry) => entry.oversized).map((entry) => `${entry.path} 超过 ${maximum} 字节参考预算;请核对宿主实际配置与内容加载范围`);
509
567
  const runtime = inspectRuntime(root, options);
510
568
  warnings.push(...runtime.warnings);
@@ -515,8 +573,8 @@ function doctorHost(options) {
515
573
  function hostReady(gateway, skills, entries, runtime) {
516
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);
517
575
  }
518
- function runtimeVersion(root, relative, diagnostics) {
519
- 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; }
520
578
  catch (error) { if (error.code !== "ENOENT") diagnostics.push(`${relative}: ${error.message}`); return null; }
521
579
  }
522
580
  function declaredDependency(root, packageName) {
@@ -532,7 +590,7 @@ function inspectRuntime(root, options) {
532
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`;
533
591
  const diagnostics = [];
534
592
  const distributedVersion = runtimeVersion(root, manifestPath, diagnostics);
535
- 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);
536
594
  const declaredVersion = declaredDependency(root, options.packageName);
537
595
  const mismatch = runtimeMismatch([distributedVersion, localVersion, options.packageVersion], declaredVersion, localVersion);
538
596
  const state = distributedVersion ? "aligned" : "unverified";
@@ -560,6 +618,7 @@ function prepareNoticeDecision(record,options) {
560
618
  const item = record.decision || record;
561
619
  record.packageName ||= options.packageName;
562
620
  record.packageVersion ||= options.packageVersion;
621
+ prepareRuntimeNotice(record, item, options);
563
622
  item.action ||= taskIntent.analyzeTask(record.task || item.task || "").action;
564
623
  const details = new Map((item.ruleDetails || []).map((entry) => [entry.id, entry]));
565
624
  item.ruleRefs ||= strings([...strings(item.requiredRules), ...details.keys()]).map((id) => {
@@ -570,6 +629,23 @@ function prepareNoticeDecision(record,options) {
570
629
  });
571
630
  return item;
572
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
+ }
573
649
  function attachNotice(record, options = {}) {
574
650
  const item = prepareNoticeDecision(record,options);
575
651
  record.notice = {
@@ -580,6 +656,7 @@ function attachNotice(record, options = {}) {
580
656
  gaps: (item.gaps || []).map((gap) => ({ reason: safeText(gap.reason), suggestion: safeText(gap.suggestion) })),
581
657
  ...noticeScope(record, options, item), displayEvidence: "unverified",
582
658
  action: item.action, ruleRefs: item.ruleRefs,
659
+ runtime:record.runtime || null,warnings:record.runtime?.warnings || [],
583
660
  };
584
661
  return record;
585
662
  }
@@ -598,7 +675,12 @@ function formatDecision(record) {
598
675
  const notice = record.notice || attachNotice({ ...record }).notice;
599
676
  const label = { baseline: "基础约束", ambiguous: "候选需确认", gap: "规则缺口", "not-applicable": "不适用", "needs-context": "缺少上下文" }[notice.decision] || "待判定";
600
677
  const identity = notice.packageName ? `${notice.packageName.replace("@agile-team/wl-skills-", "")}@${notice.packageVersion || "未知"};` : "";
601
- 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);
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(';') : ''}`;
602
684
  }
603
685
 
604
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
  },
@@ -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.19.0",
3
+ "version": "1.20.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,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
+ }