@agile-team/wl-skills-ui 1.19.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 +10 -1
- package/README.md +9 -1
- package/bin/__tests__/external-store-evidence.test.mjs +42 -0
- package/bin/integration-result.cjs +5 -0
- package/bin/task-integration.mjs +17 -2
- package/bin/task-intent.cjs +59 -15
- package/bin/task-observability.cjs +103 -21
- package/bin/task-observability.schema.json +1 -1
- package/docs/task-observability.md +2 -0
- package/package.json +1 -1
- package/scanner/cli-input.mjs +46 -0
- package/scanner/engine.mjs +4 -14
- package/scanner/index.mjs +17 -22
- package/scanner/rule-selector.mjs +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
|
|
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
|
+
|
|
2
11
|
|
|
3
12
|
## [1.19.0] - 2026-10-11
|
|
4
13
|
|
package/README.md
CHANGED
|
@@ -510,7 +510,7 @@ npx wl-ui doctor --project . --print-overrides
|
|
|
510
510
|
|
|
511
511
|
## 版本与进一步阅读
|
|
512
512
|
|
|
513
|
-
当前 v1.
|
|
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)
|
|
@@ -544,3 +544,11 @@ npm run pack:dry
|
|
|
544
544
|
`inventory.executors` 声明本包业务只读检查器、目标与参数 Schema;其适用条件由本包判定。外部适配器须核对同项目、同包、同版本、同 runId 的真实计划、新鲜度与允许动作,指定一个目标,不能扩大范围;支持规则子集的执行器不得选择计划外规则。多目标需拆分调用。五包和 Harness 均为可选组合,不新增兄弟包或 Harness 运行时依赖;卸下适配器后原 CLI/MCP 继续独立使用。
|
|
545
545
|
|
|
546
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
|
+
});
|
|
@@ -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
|
};
|
package/bin/task-integration.mjs
CHANGED
|
@@ -44,8 +44,23 @@ export function taskOptions(projectRoot, extra = {}) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function projectEvidencePaths(projectRoot, candidates) {
|
|
47
|
-
return candidates.filter((candidate) => existsSync(candidate)).map((candidate) =>
|
|
48
|
-
|
|
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;
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
export function decideTask(input) {
|
package/bin/task-intent.cjs
CHANGED
|
@@ -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 =
|
|
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 = /^(?:(
|
|
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 =
|
|
13
|
-
const
|
|
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
|
-
|
|
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(/^(
|
|
26
|
-
if (
|
|
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
|
|
38
|
-
const mode =
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
184
|
-
|
|
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 >=
|
|
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
|
|
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
|
|
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) {
|
|
@@ -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.
|
|
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
|
+
}
|
package/scanner/engine.mjs
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
+
}
|