@agile-team/wl-skills-ui 1.20.0 → 1.21.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 +21 -0
- package/README.md +38 -13
- package/bin/__tests__/dependency-security.test.mjs +78 -0
- package/bin/__tests__/protocol-cli.test.mjs +3 -2
- package/bin/__tests__/protocol-execute.test.mjs +168 -0
- package/bin/__tests__/readiness-compatibility.test.mjs +133 -0
- package/bin/executor-cli.mjs +13 -0
- package/bin/executor-invoke.cjs +129 -0
- package/bin/executor-process.cjs +51 -0
- package/bin/executors.json +1 -0
- package/bin/integration-protocol.cjs +50 -5
- package/bin/protocol-cli.mjs +11 -2
- package/bin/shared-support-check.cjs +40 -0
- package/bin/shared-support-manifest.json +25 -0
- package/bin/task-integration.mjs +11 -4
- package/bin/task-observability.cjs +26 -2
- package/bin/wl-ui.js +1 -1
- package/docs/maintenance-contract.md +46 -0
- package/docs/task-observability.md +10 -0
- package/package.json +9 -4
- package/scanner/engine.mjs +17 -8
- package/scanner/fix.mjs +2 -2
- package/scanner/index.mjs +2 -2
- package/scanner/rule-selector.mjs +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.21.0] — 2026-10-11
|
|
4
|
+
|
|
5
|
+
- 协议参数校验运行依赖固定升级 Ajv 8.20.0,消除 GHSA-2g4f-4pwh-qvx6 已知版本风险;保留共享 Schema 与未受影响的依赖版本。
|
|
6
|
+
- 定向修复 fast-uri 与开发工具传递依赖的已知公告;开发验证 Vue 链更新为 3.5.42 并保留生产 peer 范围,tmp 兼容性独立验证,主 CI 增加运行依赖审计门禁。
|
|
7
|
+
- CI 兼容矩阵先冻结安装依赖并生成运行产物,再打包候选;验证与发布工作流显式固定 pnpm 版本,避免全新 checkout 缺少构建或包管理器版本。
|
|
8
|
+
- 增加 executor-invoke-v1 结构化检查入口,复用本包原执行器并保留旧五操作。
|
|
9
|
+
- 完整参数 Schema、任务准入和路径边界校验;共享快照来源及离线防漂移门禁。
|
|
10
|
+
- 更新独立维护、Harness 兼容与持续验收说明。
|
|
11
|
+
|
|
12
|
+
- 公开协议新增 `executor-invoke-v1` 与结构化 `execute`;UI 在包内维护 scan 参数映射,复用原 scanner CLI 和同 runId 回执,无兄弟包或 Harness 运行时依赖。
|
|
13
|
+
- 执行前核对本项目有效计划、动作许可、目标及规则子集与参数 Schema;非法输入、计划外范围、解释、过期和禁用请求不启动检查。业务失败保留真实非零退出,调用成功不冒充验证通过。
|
|
14
|
+
- execute 省略 rules 时只执行计划内 R 规则;CLI/MCP 的 only/skip 在扫描前选择实际回调,SFC_PARSE 保持独立且不可隐藏。
|
|
15
|
+
- 原 scanner/修复器读取 Vue 叶子前核验实际项目根,拒绝目录内外部文件链接;保留本项目内部合法链接。
|
|
16
|
+
- 异步执行直接复用 scanner,避免同步包装器形成二次子进程;协议等待检查结束,复用公共输出预算与进程树取消,并增加真实 R001 成功/违规及拒绝零记录回归。
|
|
17
|
+
|
|
18
|
+
## [1.20.1] - 2026-10-11
|
|
19
|
+
|
|
20
|
+
- 修复 task 在版本漂移后旧顶层 status/ready 与嵌套 decision、notice、公开协议准入不一致;从校正后的记录生成旧字段,gap 始终 ready=false。
|
|
21
|
+
- task/route/explain/status 的旧顶层 ready 复用公开结果核心判定,并核对当前适用 scope;缺资产、歧义、未接入/移动项目、缺上下文或计划过期均不准入。
|
|
22
|
+
- 增加真实 CLI 和协议版本漂移、恢复后重判、缺口/范围/新鲜度回归;ready 不证明宿主读取、代码执行或业务验证通过。
|
|
23
|
+
|
|
3
24
|
## [1.20.0] - 2026-10-11
|
|
4
25
|
|
|
5
26
|
- scan 严格校验已注册规则、范围与参数,CLI/MCP 使用同源选择器;非法请求零检查零任务记录。
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 项目范围:只对目标自身已接入且平台适用的项目判定规则;父工作区安装不启用子项目。移动端与未接入开源项目不套用。详见 [精准触发与范围配置](docs/project-scope.md)。
|
|
4
4
|
|
|
5
|
-
**企业级 UI 一致性与 AI 精确治理工具包 v1.
|
|
5
|
+
**企业级 UI 一致性与 AI 精确治理工具包 v1.21.0** — 面向 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
|
| 目标 | 包提供的能力 | 带来的效果 |
|
|
@@ -106,20 +108,41 @@ CLI、安装器和 MCP 无需安装其他 wl-skills 包或统一客户端即可
|
|
|
106
108
|
|
|
107
109
|
## 公开集成协议 v1
|
|
108
110
|
|
|
109
|
-
供 harness、客户端或其他编排层以统一 JSON
|
|
111
|
+
供 harness、客户端或其他编排层以统一 JSON 入口判定、查询并调用本包声明的业务只读检查器。业务执行复用本包原 CLI,参数映射由本包维护;不新增兄弟包或 Harness 运行时依赖。
|
|
110
112
|
|
|
111
113
|
```bash
|
|
112
|
-
wl-ui protocol describe --json #
|
|
114
|
+
wl-ui protocol describe --json # 能力目录、六操作映射、请求/响应 Schema、错误码
|
|
113
115
|
wl-ui protocol request --input-file request.json --json
|
|
114
116
|
```
|
|
115
117
|
|
|
116
|
-
- 操作:`route` / `explain`(只读判定)、`task`(判定并持久化计划,尚未执行)、`status`(回查本包执行回执)、`doctor-host
|
|
117
|
-
-
|
|
118
|
-
- 统一信封:`{ ok, result | error, diagnostics }
|
|
119
|
-
-
|
|
118
|
+
- 操作:`route` / `explain`(只读判定)、`task`(判定并持久化计划,尚未执行)、`status`(回查本包执行回执)、`doctor-host`(宿主入口静态诊断)、`execute`(按有效计划调用本包声明的业务只读检查器)。
|
|
119
|
+
- 判定与查询请求字段:`protocolVersion`(1)、`operation`、`requestId`、`runId`、`projectRoot`、`task`、`targets`、`skill`、`host`+ `profile`。
|
|
120
|
+
- 统一信封:`{ ok, result | error, diagnostics }`;机器结果与诊断分离;非法输入在触达执行器前拒绝(基础请求为 `invalid-input`,执行参数为 `invalid-executor-input`),不写入任何记录。
|
|
121
|
+
- 基础错误码:`unsupported-protocol` / `unknown-operation` / `missing-input` / `invalid-input` / `internal-error`;执行请求另区分计划、授权、范围与参数拒绝,完整错误码见 `describe.errorCodes`。请求失败(exit 2)时 stdout 仍为可解析 JSON 信封。
|
|
120
122
|
- **`ok=true` 只表示协议调用成功,不代表业务验证通过**;业务状态以 `result` 内 `validationStatus` / `executionStatus` 为准(仅计划时为 `unverified` / `not-executed`)。
|
|
121
|
-
-
|
|
122
|
-
- 协议实现为本包内置的同源快照 `integration-protocol.cjs
|
|
123
|
+
- 副作用:`task` 保存计划,`execute` 复用原执行器写本包自己的 runs 回执并输出报告;均不通过本接口修改业务文件。其他操作只读,见 describe 中各操作 `sideEffects`。
|
|
124
|
+
- 协议实现为本包内置的同源快照 `integration-protocol.cjs`,当前源码分发一致性由 conformance 验收。各包独立保留自己的快照与版本;已发布版本按协议、feature 和 Schema 判断兼容,不要求每次同步发版。
|
|
125
|
+
|
|
126
|
+
### 稳定执行入口
|
|
127
|
+
|
|
128
|
+
先用 `task` 创建当前项目的计划,核对 `result.integration.allowedExecutors`,再通过同一个 `protocol request` 发送:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"protocolVersion": 1,
|
|
133
|
+
"operation": "execute",
|
|
134
|
+
"projectRoot": "/absolute/path/to/adopted-pc-project",
|
|
135
|
+
"runId": "task-returned-run-id",
|
|
136
|
+
"executorId": "scan",
|
|
137
|
+
"parameters": {"targets": ["src/views/example/index.vue"], "rules": ["R001"]}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`projectRoot`、`runId`、`executorId`、`parameters` 必填;执行目标放在 `parameters.targets`,不接受顶层 `targets`。UI 的 `rules` 只接受本次计划内的规则子集;省略时执行当前计划的 R 规则,不扩大到全量规则。SFC 解析检查独立执行且不能被隐藏。未知执行器、非法参数、计划外目标/规则、未接入或不适用项目、禁止检查、过期或版本漂移的计划在启动业务 CLI 前拒绝。当前本包执行器为 `scan`;以 `describe.inventory.executors` 的实际目录与 Schema 为准。
|
|
142
|
+
|
|
143
|
+
返回 `result.execution` 的真实 `exitCode`、`signal`、stdout/stderr,并重新读取同 runId 的包内状态形成 `result.integration`。`ok=true` 表示调用已完成,即使业务检查退出非零仍保留真实失败;宿主须查看执行与验证状态、实际检查文件和未验证项。输出有界,取消会终止本次业务 CLI 及其进程树。协议不代替真实宿主加载、模型读取或界面展示证据。
|
|
144
|
+
|
|
145
|
+
适配器只发送执行器 ID 与结构化参数,无需维护本包 CLI argv。增加检查器时由本包更新目录、Schema 与原 CLI 映射;兼容接口下其他包与 Harness 不必同步发版。原 CLI/MCP 继续可独立使用。
|
|
123
146
|
|
|
124
147
|
## 能力地图
|
|
125
148
|
|
|
@@ -510,7 +533,7 @@ npx wl-ui doctor --project . --print-overrides
|
|
|
510
533
|
|
|
511
534
|
## 版本与进一步阅读
|
|
512
535
|
|
|
513
|
-
当前 v1.
|
|
536
|
+
当前 v1.21.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
537
|
|
|
515
538
|
- [版本摘要](docs/version-history.md)
|
|
516
539
|
- [完整 Changelog](CHANGELOG.md)
|
|
@@ -537,13 +560,13 @@ npm run pack:dry
|
|
|
537
560
|
|
|
538
561
|
任务判定保留原始文本,并将当前请求、历史引用和明确否定分开。自然语言判定覆盖已公开语料与规则,不能承诺任意表述都自动命中;遇到歧义或缺口应补充目标/领域或显式指定本包能力,不能静默选择。编辑前显示真实 `notice`,包括版本、技能、规则、目标、runId、动作与待检查项。
|
|
539
562
|
|
|
540
|
-
`protocol describe --json` 声明 `task-intent-v1`、`result-core-v1`、`executor-catalog-v1`;`schemas.resultCore`
|
|
563
|
+
`protocol describe --json` 声明 `task-intent-v1`、`result-core-v1`、`executor-catalog-v1`、`executor-invoke-v1`;`schemas.resultCore` 与六操作 `schemas.results` 可供宿主验证。原有结果字段保留,新增 `result.integration`:项目适用范围、动作模式、规则身份、目标、就绪状态、允许执行器和真实执行/验证状态。`ok` 只表示调用成功。判定与查询的未声明字段进入 diagnostics,不会转成授权依据;execute 的额外字段按 Schema 拒绝。
|
|
541
564
|
|
|
542
565
|
`action.mode` 为 explain/plan/inspect/apply/unspecified;解释和规划不自动启动检查。`businessWritesAuthorized=false`,任务意图不能替代业务写入授权。`ruleRefs` 为包自有规则身份(包名、规则 ID、版本、来源、性质),自然语言约束有稳定 ID;原规则名称与列表保留。所有规则的机械/语义验证仍以实际回执为准。
|
|
543
566
|
|
|
544
|
-
`inventory.executors` 声明本包业务只读检查器、目标与参数 Schema;其适用条件由本包判定。外部适配器须核对同项目、同包、同版本、同 runId
|
|
567
|
+
`inventory.executors` 声明本包业务只读检查器、目标与参数 Schema;其适用条件由本包判定。外部适配器须核对同项目、同包、同版本、同 runId 的真实计划、新鲜度与允许动作,指定一个目标,不能扩大范围;支持规则子集的执行器不得选择计划外规则。当前执行入口一次只接受与本包计划一致的一个目标;多目标应先拆分计划,再逐个执行。五包和 Harness 均为可选组合,不新增兄弟包或 Harness 运行时依赖;卸下适配器后原 CLI/MCP 继续独立使用。
|
|
545
568
|
|
|
546
|
-
样式规则检查使用 `scan
|
|
569
|
+
样式规则检查使用 `scan`(支持计划内规则子集;`--only/--skip` 在扫描前选择实际规则回调);`check` 只核验接入完整性。技能触发词由本包 `bin/task-triggers.json` 维护并接受目录一致性检查。
|
|
547
570
|
|
|
548
571
|
`scan/audit/fix` 的未知参数、重复值选项、空值、多余位置参数和不存在的规则选择器在检查或写入前拒绝,机读输出返回 `invalid-input`,不会形成成功检查记录。CLI 与 MCP 共用注册规则选择器,支持单个 ID、逗号列表与三位编号范围(例如 `R031-R037`);用 `wl-ui rules list` 核对实际规则。Vue 解析错误仍不能被 `--only/--skip` 隐藏。`wl-ui scan --help` 只输出帮助,不启动扫描。
|
|
549
572
|
|
|
@@ -552,3 +575,5 @@ npm run pack:dry
|
|
|
552
575
|
编辑前展示实际 `notice` 的包版本、判定、动作、技能/规则、目标、runId、待检查项与版本对齐信息;解释/规划和明确禁止检查时不自动执行检查。`runtime=mismatch/invalid` 或 `planStale=true` 必须先同步本包或重新判定。只读 `inspect` 输入变化后重新 `task`;`apply` 完成正常业务修改后可用同 runId 做实际复扫,但规则、配置或版本变化仍需重新判定。
|
|
553
576
|
|
|
554
577
|
单包只读取自身公开能力;多包复用同 runId 并各存各的证据。`matched/ready/ok=true` 仅说明判定或调用成功,是否完成以实际 `executionStatus/validationStatus/checkedFiles` 为准。未接入或不适用的项目不借父目录规则;宿主发现、模型读取、反馈展示没有真实证据时继续标记未验证。
|
|
578
|
+
|
|
579
|
+
维护边界、结构化执行和防漂移验收:[维护契约](docs/maintenance-contract.md)。
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdtempSync, writeFileSync, existsSync, readdirSync, rmSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import test from 'node:test'
|
|
7
|
+
|
|
8
|
+
// Commitizen and cz-customizable still use external-editor 3.x. Verify the
|
|
9
|
+
// patched tmp dependency against their real sync, callback and cleanup APIs.
|
|
10
|
+
const require = createRequire(import.meta.url)
|
|
11
|
+
const commitizenRequire = createRequire(require.resolve('commitizen'))
|
|
12
|
+
const inquirerRequire = createRequire(commitizenRequire.resolve('inquirer'))
|
|
13
|
+
const editorEntry = inquirerRequire.resolve('external-editor')
|
|
14
|
+
const editorRequire = createRequire(editorEntry)
|
|
15
|
+
const { ExternalEditor, editAsync } = editorRequire(editorEntry)
|
|
16
|
+
const tmp = editorRequire('tmp')
|
|
17
|
+
|
|
18
|
+
function fixture() {
|
|
19
|
+
const root = mkdtempSync(join(tmpdir(), 'wl-ui-editor-api-'))
|
|
20
|
+
const editor = join(root, 'editor.cjs')
|
|
21
|
+
writeFileSync(editor, "require('node:fs').writeFileSync(process.argv[2], 'edited value', 'utf8')\n")
|
|
22
|
+
return { root, editor, cleanup: () => rmSync(root, { recursive: true, force: true }) }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('patched tmp keeps external-editor sync creation, editing and cleanup', () => {
|
|
26
|
+
const f = fixture()
|
|
27
|
+
try {
|
|
28
|
+
assert.equal(editorRequire('tmp/package.json').version, '0.2.7')
|
|
29
|
+
const editor = new ExternalEditor('initial value', { dir: f.root, prefix: 'input-' })
|
|
30
|
+
editor.editor = { bin: process.execPath, args: [f.editor] }
|
|
31
|
+
assert.equal(existsSync(editor.tempFile), true)
|
|
32
|
+
assert.equal(editor.run(), 'edited value')
|
|
33
|
+
assert.equal(editor.lastExitStatus, 0)
|
|
34
|
+
editor.cleanup()
|
|
35
|
+
assert.equal(existsSync(editor.tempFile), false)
|
|
36
|
+
} finally {
|
|
37
|
+
f.cleanup()
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('patched tmp keeps external-editor async callback and automatic cleanup', async () => {
|
|
42
|
+
const f = fixture()
|
|
43
|
+
const previousVisual = process.env.VISUAL
|
|
44
|
+
try {
|
|
45
|
+
process.env.VISUAL = `${process.execPath} ${f.editor}`
|
|
46
|
+
const value = await new Promise((resolve, reject) => {
|
|
47
|
+
editAsync('initial value', (error, result) => error ? reject(error) : resolve(result), {
|
|
48
|
+
dir: f.root,
|
|
49
|
+
prefix: 'input-',
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
assert.equal(value, 'edited value')
|
|
53
|
+
assert.deepEqual(readdirSync(f.root), ['editor.cjs'])
|
|
54
|
+
} finally {
|
|
55
|
+
if (previousVisual === undefined) delete process.env.VISUAL
|
|
56
|
+
else process.env.VISUAL = previousVisual
|
|
57
|
+
f.cleanup()
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('patched tmp file callback retains idempotent cleanup within the fixture', async () => {
|
|
62
|
+
const f = fixture()
|
|
63
|
+
try {
|
|
64
|
+
const result = await new Promise((resolve, reject) => {
|
|
65
|
+
tmp.file({ dir: f.root, discardDescriptor: true }, (error, name, fd, cleanup) => {
|
|
66
|
+
if (error) reject(error)
|
|
67
|
+
else resolve({ name, fd, cleanup })
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
assert.equal(existsSync(result.name), true)
|
|
71
|
+
assert.equal(result.fd, undefined)
|
|
72
|
+
await new Promise((resolve, reject) => result.cleanup(error => error ? reject(error) : resolve()))
|
|
73
|
+
result.cleanup()
|
|
74
|
+
assert.equal(existsSync(result.name), false)
|
|
75
|
+
} finally {
|
|
76
|
+
f.cleanup()
|
|
77
|
+
}
|
|
78
|
+
})
|
|
@@ -22,12 +22,13 @@ function tempRoot() {
|
|
|
22
22
|
return root;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
test("describe
|
|
25
|
+
test("describe 返回协议版本、能力目录与六个统一操作", () => {
|
|
26
26
|
const described = protocol.describe();
|
|
27
27
|
assert.equal(described.protocolVersion, 1);
|
|
28
28
|
assert.equal(described.package, pkg.name);
|
|
29
29
|
assert.ok(Array.isArray(described.capabilities) && described.capabilities.length > 0);
|
|
30
|
-
assert.deepEqual(described.operations.map((operation) => operation.id), ["route", "explain", "task", "status", "doctor-host"]);
|
|
30
|
+
assert.deepEqual(described.operations.map((operation) => operation.id), ["route", "explain", "task", "status", "doctor-host", "execute"]);
|
|
31
|
+
assert.ok(described.features.includes("executor-invoke-v1"));
|
|
31
32
|
assert.ok(described.errorCodes.includes("unsupported-protocol"));
|
|
32
33
|
});
|
|
33
34
|
|
|
@@ -0,0 +1,168 @@
|
|
|
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 selected = "src/views/Selected/index.vue";
|
|
12
|
+
|
|
13
|
+
function fixture(callback) {
|
|
14
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "wl-ui-public-execute-"));
|
|
15
|
+
try {
|
|
16
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ private: true, devDependencies: { [pkg.name]: pkg.version, vue: "^3.4.0" } }));
|
|
17
|
+
fs.mkdirSync(path.join(root, "node_modules/@agile-team"), { recursive: true });
|
|
18
|
+
fs.symlinkSync(repo, path.join(root, "node_modules", pkg.name), "dir");
|
|
19
|
+
fs.mkdirSync(path.dirname(path.join(root, selected)), { recursive: true });
|
|
20
|
+
fs.writeFileSync(path.join(root, selected), '<template><el-table><el-table-column prop="name" align="center"/></el-table></template>\n');
|
|
21
|
+
const installed = spawnSync(process.execPath, [cli, "init", "--editor", "all", "--profile", "native-element"], { cwd: root, encoding: "utf8", timeout: 30000 });
|
|
22
|
+
assert.equal(installed.status, 0, installed.stderr || installed.stdout);
|
|
23
|
+
callback(root);
|
|
24
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function request(root, input) {
|
|
28
|
+
const file = path.join(root, "request.json");
|
|
29
|
+
fs.writeFileSync(file, JSON.stringify({ projectRoot: root, ...input }));
|
|
30
|
+
const called = spawnSync(process.execPath, [cli, "protocol", "request", "--input-file", file, "--json"], { cwd: root, encoding: "utf8", timeout: 30000 });
|
|
31
|
+
assert.equal(called.signal, null, called.stderr);
|
|
32
|
+
return { ...JSON.parse(called.stdout), cliExitCode: called.status };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function plan(root, runId, task = "检查当前 Vue 代码") {
|
|
36
|
+
const planned = request(root, { operation: "task", task, runId, targets: [selected] });
|
|
37
|
+
assert.equal(planned.ok, true);
|
|
38
|
+
return planned.result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function execute(root, runId, extra = {}) {
|
|
42
|
+
return request(root, { operation: "execute", runId, executorId: "scan", parameters: { targets: [selected], rules: ["R001"] }, ...extra });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test("公开 execute 复用原 scan:规则子集与真实失败保留,同 runId 状态回查", () => fixture(root => {
|
|
46
|
+
const source = fs.readFileSync(path.join(root, selected));
|
|
47
|
+
assert.ok(plan(root, "passed").integration.allowedExecutors.includes("scan"));
|
|
48
|
+
const passed = execute(root, "passed");
|
|
49
|
+
assert.equal(passed.ok, true, JSON.stringify(passed));
|
|
50
|
+
assert.equal(passed.cliExitCode, 0);
|
|
51
|
+
assert.equal(passed.result.execution.exitCode, 0);
|
|
52
|
+
assert.equal(passed.result.execution.signal, null);
|
|
53
|
+
assert.equal(passed.result.integration.runId, "passed");
|
|
54
|
+
assert.equal(passed.result.integration.executionStatus, "completed");
|
|
55
|
+
assert.deepEqual(fs.readFileSync(path.join(root, selected)), source);
|
|
56
|
+
const report = JSON.parse(passed.result.execution.stdout);
|
|
57
|
+
assert.equal(report.summary.fileCount, 1);
|
|
58
|
+
assert.equal(report.issues.length, 0);
|
|
59
|
+
const status = request(root, { operation: "status", runId: "passed" });
|
|
60
|
+
assert.equal(status.result.integration.executionStatus, passed.result.integration.executionStatus);
|
|
61
|
+
assert.equal(status.result.tools[0].tool, "scan");
|
|
62
|
+
assert.deepEqual(status.result.tools[0].checkedFiles.map(file => file.path), [selected]);
|
|
63
|
+
assert.deepEqual(status.result.tools[0].summary.rules.map(rule => rule.id), ["R001"]);
|
|
64
|
+
fs.writeFileSync(path.join(root, selected), '<template><el-table><el-table-column prop="name"/></el-table></template>\n');
|
|
65
|
+
plan(root, "failed");
|
|
66
|
+
const failed = execute(root, "failed");
|
|
67
|
+
assert.equal(failed.ok, true, JSON.stringify(failed));
|
|
68
|
+
assert.equal(failed.result.execution.exitCode, 1);
|
|
69
|
+
assert.equal(failed.result.integration.executionStatus, "failed");
|
|
70
|
+
assert.equal(failed.result.integration.validationStatus, "failed");
|
|
71
|
+
assert.ok(JSON.parse(failed.result.execution.stdout).issues.some(issue => issue.rule === "R001"));
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
test("execute 省略 rules 沿用计划 R 子集,不调用或显示计划外 R038", () => fixture(root => {
|
|
75
|
+
fs.writeFileSync(path.join(root, selected), '<template><el-table><el-table-column prop="name" align="center"/></el-table><el-button>新增</el-button></template>\n');
|
|
76
|
+
const planned = plan(root, "default-plan-rules");
|
|
77
|
+
const expected = planned.integration.ruleRefs.map(rule => rule.legacyId).filter(id => /^R\d{3}$/.test(id)).sort();
|
|
78
|
+
assert.ok(expected.includes("R001"));
|
|
79
|
+
assert.equal(expected.includes("R038"), false);
|
|
80
|
+
const called = execute(root, "default-plan-rules", { parameters: { targets: [selected] } });
|
|
81
|
+
assert.equal(called.ok, true, JSON.stringify(called));
|
|
82
|
+
assert.equal(called.result.execution.exitCode, 0);
|
|
83
|
+
const report = JSON.parse(called.result.execution.stdout);
|
|
84
|
+
assert.equal(report.issues.some(issue => issue.rule === "R038"), false);
|
|
85
|
+
assert.deepEqual(called.result.tools[0].summary.rules.map(rule => rule.id).sort(), expected);
|
|
86
|
+
assert.equal(called.result.tools[0].checks.some(check => check.id === "R038"), false);
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
test("execute 未知执行器、计划外规则/目标与额外字段在原 scanner 前拒绝,计划不变", () => fixture(root => {
|
|
90
|
+
const planned = plan(root, "rejected");
|
|
91
|
+
const planFile = path.join(root, planned.recordPath);
|
|
92
|
+
const files = fs.readdirSync(path.dirname(planFile));
|
|
93
|
+
const before = fs.readFileSync(planFile);
|
|
94
|
+
const cases = [
|
|
95
|
+
[{ executorId: "unknown" }, "unsupported-executor"],
|
|
96
|
+
[{ parameters: { targets: [] } }, "invalid-executor-input"],
|
|
97
|
+
[{ parameters: { targets: [selected], rules: ["R999"] } }, "rule-mismatch"],
|
|
98
|
+
[{ parameters: { targets: [selected], rules: ["R038"] } }, "rule-mismatch"],
|
|
99
|
+
[{ parameters: { targets: ["src/views/Other/index.vue"] } }, "target-mismatch"],
|
|
100
|
+
[{ parameters: { targets: [selected], command: "fix" } }, "invalid-executor-input"],
|
|
101
|
+
[{ targets: [selected] }, "invalid-executor-input"],
|
|
102
|
+
];
|
|
103
|
+
for (const [input, code] of cases) {
|
|
104
|
+
const rejected = execute(root, "rejected", input);
|
|
105
|
+
assert.equal(rejected.ok, false, JSON.stringify(input));
|
|
106
|
+
assert.equal(rejected.cliExitCode, 2);
|
|
107
|
+
assert.equal(rejected.error.code, code);
|
|
108
|
+
assert.deepEqual(fs.readFileSync(planFile), before);
|
|
109
|
+
assert.deepEqual(fs.readdirSync(path.dirname(planFile)), files);
|
|
110
|
+
assert.equal(request(root, { operation: "status", runId: "rejected" }).result.executionStatus, "not-executed");
|
|
111
|
+
}
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
test("execute 需要本项目有效检查计划:解释、过期、禁用与未知 runId 均拒绝", () => fixture(root => {
|
|
115
|
+
plan(root, "explain", "解释当前 Vue 样式,不要检查");
|
|
116
|
+
assert.equal(execute(root, "explain").ok, false);
|
|
117
|
+
plan(root, "stale");
|
|
118
|
+
fs.appendFileSync(path.join(root, selected), "<!-- changed after inspect -->\n");
|
|
119
|
+
assert.equal(execute(root, "stale").ok, false);
|
|
120
|
+
assert.equal(request(root, { operation: "status", runId: "stale" }).result.executionStatus, "not-executed");
|
|
121
|
+
plan(root, "disabled");
|
|
122
|
+
fs.writeFileSync(path.join(root, ".wl-skills-scope.json"), JSON.stringify({ schemaVersion: 1, projectType: "pc", packages: { ui: false } }));
|
|
123
|
+
assert.equal(execute(root, "disabled").ok, false);
|
|
124
|
+
assert.equal(execute(root, "unknown-run").ok, false);
|
|
125
|
+
assert.equal(fs.existsSync(path.join(root, ".wl-skills-ui/runs/unknown-run")), false);
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
test("公开 execute 在原 scanner 前拒绝目录中越界的 Vue 叶子,任务回执零新增", () => fixture(root => {
|
|
129
|
+
const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), "wl-ui-external-source-"));
|
|
130
|
+
try {
|
|
131
|
+
const external = path.join(outsideRoot, "Outside.vue");
|
|
132
|
+
const source = '<template><el-table-column prop="external"/></template>';
|
|
133
|
+
fs.writeFileSync(external, source);
|
|
134
|
+
fs.symlinkSync(external, path.join(root, "src/views/Selected/Outside.vue"));
|
|
135
|
+
const target = "src/views/Selected";
|
|
136
|
+
const planned = request(root, { operation: "task", task: "检查当前 Vue 代码", targets: [target], runId: "external-leaf" });
|
|
137
|
+
assert.equal(planned.ok, true);
|
|
138
|
+
const record = path.join(root, planned.result.recordPath);
|
|
139
|
+
const bytes = fs.readFileSync(record);
|
|
140
|
+
const files = fs.readdirSync(path.dirname(record));
|
|
141
|
+
const rejected = execute(root, "external-leaf", { parameters: { targets: [target] } });
|
|
142
|
+
assert.equal(rejected.ok, false, JSON.stringify(rejected));
|
|
143
|
+
assert.equal(rejected.error.code, "invalid-executor-input");
|
|
144
|
+
assert.deepEqual(fs.readFileSync(record), bytes);
|
|
145
|
+
assert.deepEqual(fs.readdirSync(path.dirname(record)), files);
|
|
146
|
+
assert.equal(fs.readFileSync(external, "utf8"), source);
|
|
147
|
+
} finally { fs.rmSync(outsideRoot, { recursive: true, force: true }); }
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
test("复制项目不能重用原根任务计划:status 过期、execute 身份拒绝且零新回执", () => fixture(root => {
|
|
151
|
+
const planned = plan(root, "copied-plan");
|
|
152
|
+
const copy = fs.mkdtempSync(path.join(os.tmpdir(), "wl-ui-copied-project-"));
|
|
153
|
+
try {
|
|
154
|
+
fs.cpSync(root, copy, { recursive: true, verbatimSymlinks: true });
|
|
155
|
+
const record = path.join(copy, planned.recordPath);
|
|
156
|
+
const bytes = fs.readFileSync(record);
|
|
157
|
+
const files = fs.readdirSync(path.dirname(record));
|
|
158
|
+
const status = request(copy, { operation: "status", runId: "copied-plan" });
|
|
159
|
+
assert.equal(status.result.planStale, true);
|
|
160
|
+
assert.equal(status.result.ready, false);
|
|
161
|
+
assert.equal(status.result.integration.ready, false);
|
|
162
|
+
const rejected = execute(copy, "copied-plan");
|
|
163
|
+
assert.equal(rejected.ok, false);
|
|
164
|
+
assert.equal(rejected.error.code, "identity-mismatch");
|
|
165
|
+
assert.deepEqual(fs.readFileSync(record), bytes);
|
|
166
|
+
assert.deepEqual(fs.readdirSync(path.dirname(record)), files);
|
|
167
|
+
} finally { fs.rmSync(copy, { recursive: true, force: true }); }
|
|
168
|
+
}));
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import processCore from "./executor-process.cjs";
|
|
3
|
+
|
|
4
|
+
// 复用原 scanner CLI,避免 wl-ui 包装器的同步二次子进程。
|
|
5
|
+
// 规则与参数归本包维护;共享协议先验证计划、规则子集与目标范围。
|
|
6
|
+
export function invokeExecutor({ executor, parameters, projectRoot, runId, plan }) {
|
|
7
|
+
if (executor.id !== "scan") throw new Error(`UI 未实现执行器:${executor.id}`);
|
|
8
|
+
const rules = parameters.rules || plan.ruleRefs.map(rule => rule.legacyId).filter(id => /^R\d{3}$/.test(id));
|
|
9
|
+
if (!rules.length) throw new Error("当前计划没有可执行的 UI R 规则,不能默认扩大扫描范围");
|
|
10
|
+
const args = ["scan", "--project", projectRoot, `--target=${parameters.targets[0]}`, "--run-id", runId, "--output", "json", "--fail-on-error"];
|
|
11
|
+
args.push("--only", rules.join(","));
|
|
12
|
+
return processCore.invokeCli({ cli: path.resolve(import.meta.dirname, "../scanner/index.mjs"), args, projectRoot });
|
|
13
|
+
}
|