@agile-team/wl-skills-ui 1.17.0 → 1.19.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 +14 -0
- package/README.md +15 -1
- package/SKILL.md +4 -0
- package/bin/__tests__/protocol-cli.test.mjs +4 -2
- package/bin/__tests__/protocol-evidence.test.mjs +4 -2
- package/bin/__tests__/protocol-routing.test.mjs +3 -2
- package/bin/__tests__/task-intent-cases.json +34 -0
- package/bin/__tests__/task-intent-routing.test.mjs +41 -0
- package/bin/__tests__/task-trigger-catalog.test.mjs +7 -0
- package/bin/executors.json +48 -0
- package/bin/integration-protocol.cjs +11 -2
- package/bin/integration-result.cjs +76 -0
- package/bin/project-scope.cjs +198 -0
- package/bin/project-scope.schema.json +48 -0
- package/bin/protocol-cli.mjs +6 -5
- package/bin/task-gateway.mjs +5 -1
- package/bin/task-integration.mjs +9 -16
- package/bin/task-intent.cjs +64 -0
- package/bin/task-observability.cjs +34 -20
- package/bin/task-observability.schema.json +407 -52
- package/bin/task-triggers.json +93 -0
- package/bin/wl-ui.js +2 -2
- package/docs/project-scope.md +44 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.19.0] - 2026-10-11
|
|
4
|
+
|
|
5
|
+
- 区分当前请求、否定动作与历史引用,增加动作模式;解释/规划不自动检查,意图不作为业务写入授权。
|
|
6
|
+
- 公开协议保持 v1 和原有字段,增加带 Schema 的 result.integration、规则身份与包自有只读执行器目录。
|
|
7
|
+
- 编辑器 gateway 展示真实动作与规则回执;补充独立包语料、统一契约和执行边界回归,无兄弟包/Harness 依赖。
|
|
8
|
+
- 触发词迁至独立目录并双向核验,明确 scan 样式检查与 check 接入检查的职责。
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [1.18.0] - 2026-10-11
|
|
12
|
+
|
|
13
|
+
- 任务路由前校验目标项目自身接入与平台;未接入/移动端零规则零任务写入,不继承父安装。
|
|
14
|
+
- 最近项目边界与跨项目拆分、独立包禁用、严格范围Schema、notice/doctor/status范围与过期证据。
|
|
15
|
+
- 原生及编辑器入口收窄适用描述;补充单包、移动端、开源未接入、工作区、异常配置和Maven模块回归。
|
|
16
|
+
|
|
3
17
|
All notable changes to **@agile-team/wl-skills-ui** will be documented in this file.
|
|
4
18
|
|
|
5
19
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @agile-team/wl-skills-ui
|
|
2
2
|
|
|
3
|
+
> 项目范围:只对目标自身已接入且平台适用的项目判定规则;父工作区安装不启用子项目。移动端与未接入开源项目不套用。详见 [精准触发与范围配置](docs/project-scope.md)。
|
|
4
|
+
|
|
3
5
|
**企业级 UI 一致性与 AI 精确治理工具包 v1.17.0** — 面向 Vue 3 + Element Plus 的设计令牌、能力 Profile、样式皮肤、页面骨架、运行时能力、静态扫描、确定性修复、AI Skill、MCP 与脱敏 UI 模板契约。
|
|
4
6
|
|
|
5
7
|
它把“视觉统一、问题识别、机械修复、项目适配和样例沉淀”拆成可验证、可组合的工程能力。脚本、规则和 MCP 先完成确定性工作,AI 只处理业务语义与人工决策,从而减少重复读源码、上下文 token 和误修改。
|
|
@@ -508,7 +510,7 @@ npx wl-ui doctor --project . --print-overrides
|
|
|
508
510
|
|
|
509
511
|
## 版本与进一步阅读
|
|
510
512
|
|
|
511
|
-
当前 v1.
|
|
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 依赖识别。
|
|
512
514
|
|
|
513
515
|
- [版本摘要](docs/version-history.md)
|
|
514
516
|
- [完整 Changelog](CHANGELOG.md)
|
|
@@ -530,3 +532,15 @@ npm run pack:dry
|
|
|
530
532
|
```
|
|
531
533
|
|
|
532
534
|
视觉测试基线按操作系统管理;如果本机没有对应平台截图,完整 `test:visual` 会明确报告缺失,不应自动生成并提交一套未经评审的新基线。发布门禁使用跨浏览器计算样式契约 `test:visual:contracts`;真实浏览器兼容契约也可以单独执行 `browser-compat.spec.ts`。
|
|
535
|
+
|
|
536
|
+
### 当前请求、动作边界与 Harness 集成
|
|
537
|
+
|
|
538
|
+
任务判定保留原始文本,并将当前请求、历史引用和明确否定分开。自然语言判定覆盖已公开语料与规则,不能承诺任意表述都自动命中;遇到歧义或缺口应补充目标/领域或显式指定本包能力,不能静默选择。编辑前显示真实 `notice`,包括版本、技能、规则、目标、runId、动作与待检查项。
|
|
539
|
+
|
|
540
|
+
`protocol describe --json` 声明 `task-intent-v1`、`result-core-v1`、`executor-catalog-v1`;`schemas.resultCore` 与五操作 `schemas.results` 可供宿主验证。原有结果字段保留,新增 `result.integration`:项目适用范围、动作模式、规则身份、目标、就绪状态、允许执行器和真实执行/验证状态。`ok` 只表示调用成功。未声明使用的输入字段进入 diagnostics,不会转成授权依据。
|
|
541
|
+
|
|
542
|
+
`action.mode` 为 explain/plan/inspect/apply/unspecified;解释和规划不自动启动检查。`businessWritesAuthorized=false`,任务意图不能替代业务写入授权。`ruleRefs` 为包自有规则身份(包名、规则 ID、版本、来源、性质),自然语言约束有稳定 ID;原规则名称与列表保留。所有规则的机械/语义验证仍以实际回执为准。
|
|
543
|
+
|
|
544
|
+
`inventory.executors` 声明本包业务只读检查器、目标与参数 Schema;其适用条件由本包判定。外部适配器须核对同项目、同包、同版本、同 runId 的真实计划、新鲜度与允许动作,指定一个目标,不能扩大范围;支持规则子集的执行器不得选择计划外规则。多目标需拆分调用。五包和 Harness 均为可选组合,不新增兄弟包或 Harness 运行时依赖;卸下适配器后原 CLI/MCP 继续独立使用。
|
|
545
|
+
|
|
546
|
+
样式规则检查使用 `scan`(支持计划内规则子集);`check` 只核验接入完整性。技能触发词由本包 `bin/task-triggers.json` 维护并接受目录一致性检查。
|
package/SKILL.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wl-skills-ui
|
|
3
3
|
description: |
|
|
4
|
+
Only for PC Vue admin projects that adopted wl-skills-ui; excludes mobile and unadopted projects.
|
|
4
5
|
Audit and align Vue 3 + Element Plus enterprise UI projects with deterministic
|
|
5
6
|
scanning, profile-aware fixes, design tokens, vendor adapters, and runtime guards.
|
|
6
7
|
Use for UI consistency, legacy skin migration, rule diagnostics, or safe UI fixes.
|
|
7
8
|
---
|
|
8
9
|
|
|
10
|
+
先确认目标项目自身的接入证据(本包安装清单、直接依赖或 `.wl-skills-scope.json` 显式启用),再判定任务。只在目标已接入且平台适用时读取规则;父目录安装、兄弟包、Vue 文件或触发词不能证明接入。未接入的开源项目不套用规则、不建议自动安装;UniApp/小程序/App/PDA/移动 H5 不适用。跨项目目标分别判定,沿最近项目边界停止,不能越过未接入子项目找父清单。工具返回 `scope` 作为静态范围证据,宿主加载仍需实际事件。
|
|
11
|
+
|
|
12
|
+
|
|
9
13
|
# wl-skills-ui
|
|
10
14
|
|
|
11
15
|
Use deterministic project facts before reading or editing business source. Rules,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { test } from "node:test";
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
|
-
import { mkdtempSync } from "node:fs";
|
|
6
|
+
import fs, { mkdtempSync } from "node:fs";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { spawnSync } from "node:child_process";
|
|
@@ -17,7 +17,9 @@ const pkg = require("../../package.json");
|
|
|
17
17
|
const BIN = path.join(here, "..", "wl-ui.js");
|
|
18
18
|
|
|
19
19
|
function tempRoot() {
|
|
20
|
-
|
|
20
|
+
const root = mkdtempSync(path.join(tmpdir(), "wl-ui-protocol-"));
|
|
21
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ devDependencies: { "@agile-team/wl-skills-ui": "*" } }));
|
|
22
|
+
return root;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
test("describe 返回协议版本、能力目录与五个统一操作", () => {
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { test } from "node:test";
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
|
-
import { mkdtempSync } from "node:fs";
|
|
6
|
+
import fs, { mkdtempSync } from "node:fs";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
|
|
10
10
|
const { protocol, runOperation } = await import("../protocol-cli.mjs");
|
|
11
11
|
|
|
12
12
|
function tempRoot() {
|
|
13
|
-
|
|
13
|
+
const root = mkdtempSync(path.join(tmpdir(), "wl-ui-evidence-"));
|
|
14
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ devDependencies: { "@agile-team/wl-skills-ui": "*" } }));
|
|
15
|
+
return root;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
test("status 严格回查同一 runId:字段精确、不兜底", () => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { test, before } from "node:test";
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
7
|
+
import fs, { mkdtempSync, rmSync } from "node:fs";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
@@ -22,6 +22,7 @@ function tempRoot() {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function routeAt(projectRoot, task) {
|
|
25
|
+
if (!fs.existsSync(path.join(projectRoot, "package.json"))) fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify({ devDependencies: { "@agile-team/wl-skills-ui": "*" } }));
|
|
25
26
|
return protocol.request({ operation: "route", projectRoot, task }, runOperation);
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -40,7 +41,7 @@ for (const item of cases) {
|
|
|
40
41
|
const expectedBare = item.skill ? "gap" : item.status;
|
|
41
42
|
const expectedInstalled = item.installedStatus || (item.skill ? "matched" : item.status);
|
|
42
43
|
|
|
43
|
-
test(
|
|
44
|
+
test(`已声明接入但未初始化:「${item.task}」→ ${expectedBare}${item.skill ? ` + ${item.skill}` : ""}`, () => {
|
|
44
45
|
const envelope = routeAt(tempRoot(), item.task);
|
|
45
46
|
assert.equal(envelope.ok, true);
|
|
46
47
|
const decision = envelope.result.decision || envelope.result;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"task": "旧需求写过“UI迁移迁移tokens”,本次只审查表单尺寸",
|
|
4
|
+
"status": "matched",
|
|
5
|
+
"skills": [
|
|
6
|
+
"element/el-form"
|
|
7
|
+
],
|
|
8
|
+
"mode": "inspect"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"task": "不要迁移tokens,只检查现有表单样式",
|
|
12
|
+
"status": "matched",
|
|
13
|
+
"skills": [
|
|
14
|
+
"element/el-form"
|
|
15
|
+
],
|
|
16
|
+
"mode": "inspect"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"task": "只说明 UI修复 的影响,不执行修改",
|
|
20
|
+
"status": "matched",
|
|
21
|
+
"skills": [
|
|
22
|
+
"ops/fix"
|
|
23
|
+
],
|
|
24
|
+
"mode": "explain"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"task": "迁移tokens",
|
|
28
|
+
"status": "matched",
|
|
29
|
+
"skills": [
|
|
30
|
+
"ops/migrate"
|
|
31
|
+
],
|
|
32
|
+
"mode": "apply"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {spawnSync} from 'node:child_process';
|
|
7
|
+
const dirname = import.meta.dirname;
|
|
8
|
+
const repo = path.resolve(dirname, "../..");
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(repo, 'package.json'), 'utf8'));
|
|
10
|
+
const cli = path.join(repo, 'bin/wl-ui.js');
|
|
11
|
+
const cases = JSON.parse(fs.readFileSync(path.join(dirname, 'task-intent-cases.json'), 'utf8'));
|
|
12
|
+
const key = "ui";
|
|
13
|
+
test('当前请求、否定、历史引用与只讨论:公开协议精确回归', () => {
|
|
14
|
+
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'wl-intent-ui-'));
|
|
15
|
+
const project = path.join(temporary, 'project');fs.mkdirSync(project);
|
|
16
|
+
const run = args => {
|
|
17
|
+
const result = spawnSync(process.execPath,[cli,...args],{cwd:project,encoding:'utf8',timeout:30000});
|
|
18
|
+
assert.equal(result.status,0,result.stderr || result.stdout);return result.stdout;
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
fs.writeFileSync(path.join(project,'package.json'),JSON.stringify({private:true,devDependencies:{[pkg.name]:'*'}}));
|
|
22
|
+
const scoped = path.join(project,'node_modules/@agile-team');fs.mkdirSync(scoped,{recursive:true});fs.symlinkSync(repo,path.join(scoped,'wl-skills-ui'),'dir');
|
|
23
|
+
run(['init','--project',project,'--editor','agents-generic','--profile','native-element','--skills-only']);
|
|
24
|
+
for(const row of cases){
|
|
25
|
+
if(row.javaProject)fs.writeFileSync(path.join(project,'pom.xml'),'<project><modelVersion>4.0.0</modelVersion><groupId>audit</groupId><artifactId>fixture</artifactId><version>1</version></project>');
|
|
26
|
+
const file = path.join(temporary,'request.json');
|
|
27
|
+
fs.writeFileSync(file,JSON.stringify({operation:'route',projectRoot:project,task:row.task,...(row.domain ? {domain:row.domain} : {})}));
|
|
28
|
+
const envelope = JSON.parse(run(['protocol','request','--input-file',file,'--json']));
|
|
29
|
+
assert.equal(envelope.ok,true,row.task);const d = envelope.result.decision || envelope.result;
|
|
30
|
+
assert.equal(d.status,row.status,row.task);assert.deepEqual(d.selectedSkills,row.skills,row.task);
|
|
31
|
+
if(row.rules)assert.deepEqual(d.requiredRules,row.rules,row.task);
|
|
32
|
+
assert.equal(d.action.mode,row.mode,row.task);assert.equal(d.action.businessWritesAuthorized,false);
|
|
33
|
+
if(row.mode === 'explain')assert.equal(d.action.checksAllowed,false);
|
|
34
|
+
const core = envelope.result.integration;assert.equal(core.packageName,pkg.name);assert.equal(core.packageVersion,pkg.version);
|
|
35
|
+
assert.equal(core.contractVersion,1);assert.equal(core.runId,null);
|
|
36
|
+
assert.ok(core.ruleRefs.every(rule => rule.packageName === pkg.name && rule.ruleVersion === pkg.version));
|
|
37
|
+
}
|
|
38
|
+
const storage = {kit:'.wl-skills',ui:'.wl-skills-ui',bd:'.wl-skills-bd',design:'.wl-skills-design',test:'.wl-skills-test'}[key];
|
|
39
|
+
assert.equal(fs.existsSync(path.join(project,storage,'runs')),false,'route 不写任务记录');
|
|
40
|
+
} finally {fs.rmSync(temporary,{recursive:true,force:true});}
|
|
41
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
|
|
2
|
+
import {taskCatalog} from '../task-integration.mjs';
|
|
3
|
+
test('触发词清单与本包技能目录双向完整且不含空词',() => {
|
|
4
|
+
const triggers = JSON.parse(fs.readFileSync(new URL('../task-triggers.json',import.meta.url),'utf8'));const catalog = taskCatalog();
|
|
5
|
+
assert.deepEqual(Object.keys(triggers).sort(),catalog.map(s => s.id).sort());
|
|
6
|
+
for(const skill of catalog){assert.ok(skill.triggers.length,skill.id);assert.ok(skill.triggers.every(p => typeof p === 'string' && p.trim()));assert.deepEqual(skill.triggers,triggers[skill.id]);}
|
|
7
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "scan",
|
|
4
|
+
"command": [
|
|
5
|
+
"scan"
|
|
6
|
+
],
|
|
7
|
+
"when": {
|
|
8
|
+
"checks": [
|
|
9
|
+
"scan"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"parameters": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"required": [
|
|
15
|
+
"targets"
|
|
16
|
+
],
|
|
17
|
+
"properties": {
|
|
18
|
+
"targets": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"items": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"pattern": "\\S"
|
|
24
|
+
},
|
|
25
|
+
"minItems": 1,
|
|
26
|
+
"maxItems": 1
|
|
27
|
+
},
|
|
28
|
+
"rules": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"pattern": "^R[0-9]{3}$"
|
|
33
|
+
},
|
|
34
|
+
"minItems": 1,
|
|
35
|
+
"uniqueItems": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"additionalProperties": false
|
|
39
|
+
},
|
|
40
|
+
"sideEffects": {
|
|
41
|
+
"business": "none",
|
|
42
|
+
"records": true,
|
|
43
|
+
"reports": "stdout"
|
|
44
|
+
},
|
|
45
|
+
"authorization": "read-only-business-check",
|
|
46
|
+
"receipt": "package-execution-record-v1"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const PROTOCOL_VERSION = 1;
|
|
19
|
+
const resultCore = require("./integration-result.cjs");
|
|
19
20
|
const SUPPORTED_VERSIONS = [1];
|
|
20
21
|
const ERROR_CODES = ["unsupported-protocol", "unknown-operation", "missing-input", "invalid-input", "internal-error"];
|
|
21
22
|
const OPTIONAL_STRING_FIELDS = ["requestId", "runId", "projectRoot", "skill", "host", "type", "domain", "profile"];
|
|
@@ -233,6 +234,7 @@ function validateFieldTypes(input) {
|
|
|
233
234
|
commands: inventory.commands || [],
|
|
234
235
|
mcpTools: inventory.mcpTools || [],
|
|
235
236
|
loadErrors: inventory.loadErrors || [],
|
|
237
|
+
executors: config.executors || [],
|
|
236
238
|
note: "业务执行仍走各包原 CLI/MCP;inventory 只声明公开调用映射与入口,不改变执行归属;loadErrors 非空表示目录派生存在异常,不得忽略",
|
|
237
239
|
};
|
|
238
240
|
}
|
|
@@ -255,9 +257,11 @@ function validateFieldTypes(input) {
|
|
|
255
257
|
return {
|
|
256
258
|
...base(null, null),
|
|
257
259
|
capabilities: config.capabilities || [],
|
|
260
|
+
features: ["task-intent-v1", "result-core-v1", "executor-catalog-v1"],
|
|
258
261
|
operations: config.operations.map((operation) => ({ ...operation })),
|
|
259
262
|
inventory: buildInventory(config),
|
|
260
|
-
schemas: { request: requestSchema(config), envelope: envelopeSchema(config.packageName)
|
|
263
|
+
schemas: { request: requestSchema(config), envelope: envelopeSchema(config.packageName), resultCore: resultCore.SCHEMA,
|
|
264
|
+
results: Object.fromEntries(config.operations.map((operation) => [operation.id, { type: "object", required: ["integration"], properties: { integration: resultCore.SCHEMA }, additionalProperties: true }])) },
|
|
261
265
|
constraints: buildConstraints(config),
|
|
262
266
|
errorCodes: [...ERROR_CODES],
|
|
263
267
|
};
|
|
@@ -267,8 +271,13 @@ function validateFieldTypes(input) {
|
|
|
267
271
|
const { operation, error } = validateRequest(input);
|
|
268
272
|
if (error) return error;
|
|
269
273
|
const diagnostics = [];
|
|
274
|
+
const supportedFields = new Set(["operation", "protocolVersion", "requestId", ...operation.required, ...(operation.requireAny || []), ...operation.optional]);
|
|
275
|
+
for (const field of Object.keys(input)) {
|
|
276
|
+
if (!supportedFields.has(field)) diagnostics.push(`操作 ${operation.id} 未声明使用字段 ${field};该字段不构成执行或授权依据`);
|
|
277
|
+
}
|
|
270
278
|
try {
|
|
271
|
-
const
|
|
279
|
+
const original = runOperation(operation.id, input, diagnostics) ?? {};
|
|
280
|
+
const result = resultCore.projectResult(original, input, config);
|
|
272
281
|
return { ...base(operation.id, input.requestId), ok: true, result, diagnostics };
|
|
273
282
|
} catch (thrown) {
|
|
274
283
|
diagnostics.push(String((thrown && thrown.message) || thrown));
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Additive, typed core: original package-specific result fields are preserved.
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const DECISIONS = ["matched", "baseline", "gap", "ambiguous", "not-applicable", "needs-context"];
|
|
6
|
+
const strings = (items) => Array.isArray(items) ? items.filter((value) => typeof value === "string") : [];
|
|
7
|
+
const nullableString = { type: ["string", "null"] };
|
|
8
|
+
const SCHEMA = {
|
|
9
|
+
type: "object", additionalProperties: false,
|
|
10
|
+
required: ["contractVersion", "packageName", "packageVersion", "projectRoot", "runId", "decisionStatus", "scope", "action", "selectedSkills", "ruleRefs", "requiredChecks", "targets", "ready", "executionStatus", "validationStatus", "stale", "planStale", "allowedExecutors"],
|
|
11
|
+
properties: {
|
|
12
|
+
contractVersion: { const: 1 }, packageName: { type: "string", minLength: 1 }, packageVersion: { type: "string", minLength: 1 },
|
|
13
|
+
projectRoot: { type: "string", minLength: 1 }, runId: nullableString,
|
|
14
|
+
decisionStatus: { enum: [...DECISIONS, null] }, scope: { oneOf: [{type:"null"}, {type:"object",required:["schemaVersion","status","reason","adopted","projectType","inherited"],
|
|
15
|
+
properties:{schemaVersion:{const:1},status:{enum:["applicable","not-applicable","needs-context"]},reason:{type:"string"},adopted:{type:"boolean"},projectType:{type:"string"},inherited:{const:false}},additionalProperties:true}] },
|
|
16
|
+
action: { oneOf: [{ type: "null" }, { type: "object", required: ["mode", "checksAllowed", "businessWritesAuthorized", "authorization"],
|
|
17
|
+
properties: { mode: { enum: ["explain", "inspect", "plan", "apply", "unspecified"] }, checksAllowed: { type: "boolean" }, businessWritesAuthorized: { const: false }, authorization: { type: "string" } }, additionalProperties: false }] },
|
|
18
|
+
selectedSkills: { type: "array", items: { type: "string" } }, requiredChecks: { type: "array", items: { type: "string" } },
|
|
19
|
+
targets: { type: "array", items: { type: "string" } }, allowedExecutors: { type: "array", items: { type: "string" } },
|
|
20
|
+
ruleRefs: { type: "array", items: { type: "object", required: ["packageName", "ruleId", "legacyId", "name", "ruleVersion", "source", "kind", "verification"],
|
|
21
|
+
properties: { packageName: nullableString, ruleId: { type: "string" }, legacyId: { type: "string" }, name: { type: "string" }, ruleVersion: nullableString, source: nullableString, kind: { type: "string" }, verification: { const: "unverified" } }, additionalProperties: false } },
|
|
22
|
+
ready: { type: ["boolean", "null"] }, executionStatus: nullableString, validationStatus: nullableString,
|
|
23
|
+
stale: { type: ["boolean", "null"] }, planStale: { type: ["boolean", "null"] },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function allowedExecutors(decision, executors) {
|
|
28
|
+
if (!["matched", "baseline"].includes(decision.status) || !decision.action?.checksAllowed) return [];
|
|
29
|
+
return executors.filter((executor) => {
|
|
30
|
+
const when = executor.when || {};
|
|
31
|
+
return strings(when.skills).some((id) => decision.selectedSkills?.includes(id))
|
|
32
|
+
|| strings(when.checks).some((id) => decision.requiredChecks?.includes(id))
|
|
33
|
+
|| strings(when.rulePrefixes).some((prefix) => decision.requiredRules?.some((id) => id.startsWith(prefix)));
|
|
34
|
+
}).map((executor) => executor.id);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resultReadiness(result, decision, status) {
|
|
38
|
+
if (typeof result.ready === "boolean") return result.ready;
|
|
39
|
+
if (typeof decision.ready === "boolean") return decision.ready;
|
|
40
|
+
return status ? ["matched", "baseline"].includes(status) && !(decision.missingInputs || []).length : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function executionState(result) {
|
|
44
|
+
return {
|
|
45
|
+
runId: result.runId || null,
|
|
46
|
+
executionStatus: result.executionStatus || null,
|
|
47
|
+
validationStatus: result.validationStatus || null,
|
|
48
|
+
stale: typeof result.stale === "boolean" ? result.stale : null,
|
|
49
|
+
planStale: typeof result.planStale === "boolean" ? result.planStale : null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function decisionReadout(decision) {
|
|
54
|
+
return {action:decision.action || null, selectedSkills:strings(decision.selectedSkills), ruleRefs:decision.ruleRefs || [], requiredChecks:strings(decision.requiredChecks)};
|
|
55
|
+
}
|
|
56
|
+
function targetReadout(result,input) { return strings(result.targets || input.targets || result.notice?.targets); }
|
|
57
|
+
function projectIdentity(scope,result,input) { return path.resolve(scope?.projectRoot || result.projectRoot || result.root || input.projectRoot || process.cwd()); }
|
|
58
|
+
|
|
59
|
+
function projectResult(result, input, config) {
|
|
60
|
+
const decision = result.decision || result;
|
|
61
|
+
const scope = result.currentScope || decision.scope || result.scope || null;
|
|
62
|
+
const status = DECISIONS.includes(decision.status) ? decision.status : null;
|
|
63
|
+
const ready = resultReadiness(result, decision, status);
|
|
64
|
+
return {
|
|
65
|
+
...result,
|
|
66
|
+
integration: {
|
|
67
|
+
contractVersion: 1, packageName: config.packageName, packageVersion: config.packageVersion,
|
|
68
|
+
projectRoot: projectIdentity(scope,result,input),
|
|
69
|
+
...executionState(result), decisionStatus: status, scope,
|
|
70
|
+
...decisionReadout(decision), targets: targetReadout(result,input), ready,
|
|
71
|
+
allowedExecutors: ready && scope?.status === "applicable" ? allowedExecutors(decision, config.executors || []) : [],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { SCHEMA, projectResult };
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Independent snapshot: project adoption is checked before skill intent or writes.
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const crypto = require("node:crypto");
|
|
7
|
+
const CONFIG = ".wl-skills-scope.json";
|
|
8
|
+
const MANIFESTS = {
|
|
9
|
+
kit: ".wl-skills-manifest.json", ui: ".wl-skills-ui-manifest.json",
|
|
10
|
+
bd: ".wl-skills-bd-manifest.json", design: ".wl-skills-design/state.json", test: ".wl-skills-test/manifest.json",
|
|
11
|
+
};
|
|
12
|
+
const TYPES = new Set(["pc", "backend", "documents", "testing", "mobile", "workspace", "unknown"]);
|
|
13
|
+
|
|
14
|
+
function inside(root, target) {
|
|
15
|
+
const rel = path.relative(root, target);
|
|
16
|
+
return !path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path.sep}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Resolve missing paths through their nearest existing ancestor, including symlinks.
|
|
20
|
+
function realTarget(target) {
|
|
21
|
+
if (fs.existsSync(target)) return fs.realpathSync(target);
|
|
22
|
+
const parent = path.dirname(target);
|
|
23
|
+
if (parent === target) throw new Error("Cannot resolve target ancestor");
|
|
24
|
+
return path.join(realTarget(parent), path.basename(target));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readJson(root, relative) {
|
|
28
|
+
const file = path.join(root, relative);
|
|
29
|
+
if (!fs.existsSync(file)) return { present: false, value: null };
|
|
30
|
+
try {
|
|
31
|
+
if (!inside(root, fs.realpathSync(file))) throw new Error("metadata path escapes project");
|
|
32
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
33
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected object");
|
|
34
|
+
return { present: true, value };
|
|
35
|
+
} catch (error) { return { present: true, value: null, error: `${relative}: ${error.message}` }; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function boundary(root, fallback) {
|
|
39
|
+
const markers = ["package.json", ".git", CONFIG, ...Object.values(MANIFESTS)];
|
|
40
|
+
// A Java project's Maven modules share its adoption; a nested independent repo still stops lookup.
|
|
41
|
+
if (root === fallback || !fs.existsSync(path.join(fallback, "pom.xml"))) markers.push("pom.xml");
|
|
42
|
+
return markers.some((file) => fs.existsSync(path.join(root, file)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function nearestRoot(target, fallback) {
|
|
46
|
+
let dir = fs.existsSync(target) && fs.statSync(target).isDirectory() ? target : path.dirname(target);
|
|
47
|
+
while (inside(fallback, dir)) {
|
|
48
|
+
if (boundary(dir, fallback) || dir === fallback) return dir;
|
|
49
|
+
dir = path.dirname(dir);
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function targetRoots(root, targets) {
|
|
55
|
+
const resolved = targets.map((target) => realTarget(path.resolve(root, target)));
|
|
56
|
+
if (resolved.some((target) => !inside(root, target))) return { error: "target-outside-authorized-project", roots: [], resolved };
|
|
57
|
+
return { roots: [...new Set(resolved.map((target) => nearestRoot(target, root)))], resolved };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function scopeConfig(root) {
|
|
61
|
+
const config = readJson(root, CONFIG);
|
|
62
|
+
if (!config.present || config.error) return config;
|
|
63
|
+
const { value } = config;
|
|
64
|
+
const allowed = ["schemaVersion", "projectType", "packages"];
|
|
65
|
+
if (value.schemaVersion !== 1 || !TYPES.has(value.projectType) || Object.keys(value).some((key) => !allowed.includes(key)) || !validPackageSwitches(value.packages)) {
|
|
66
|
+
return { ...config, error: `${CONFIG}: invalid schemaVersion/projectType/packages` };
|
|
67
|
+
}
|
|
68
|
+
return config;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function validPackageSwitches(packages) {
|
|
72
|
+
if (packages === undefined) return true;
|
|
73
|
+
return Boolean(packages && typeof packages === "object" && !Array.isArray(packages) && Object.entries(packages).every(([key, enabled]) => Object.hasOwn(MANIFESTS, key) && typeof enabled === "boolean"));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function mobileEvidence(root, pkg) {
|
|
77
|
+
const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
|
|
78
|
+
const mobile = deps.filter((name) => /^@dcloudio\/uni(?:-|$)|^@tarojs\/|^react-native$|^expo$|^@ionic\/|^@capacitor\//.test(name));
|
|
79
|
+
if (fs.existsSync(path.join(root, "pages.json")) && fs.existsSync(path.join(root, "manifest.json"))) mobile.push("pages.json+manifest.json");
|
|
80
|
+
return mobile;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function adoption(root, key, packageName, pkg, config) {
|
|
84
|
+
const manifest = readJson(root, MANIFESTS[key]);
|
|
85
|
+
validateAdoptionManifest(manifest, key, packageName);
|
|
86
|
+
const declared = [pkg.dependencies, pkg.devDependencies, pkg.optionalDependencies].some((deps) => deps?.[packageName]);
|
|
87
|
+
const explicit = config.value?.packages?.[key] === true;
|
|
88
|
+
const installed = manifest.present && !manifest.error;
|
|
89
|
+
return { adopted: explicit || declared || installed, sources: [
|
|
90
|
+
...(explicit ? [`${CONFIG}:packages.${key}`] : []), ...(declared ? ["package.json:direct-dependency"] : []),
|
|
91
|
+
...(installed ? [MANIFESTS[key]] : []),
|
|
92
|
+
], error: manifest.error || null };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validateAdoptionManifest(manifest, key, packageName) {
|
|
96
|
+
if (!manifest.present || manifest.error) return;
|
|
97
|
+
const { value } = manifest;
|
|
98
|
+
if (typeof value.version !== "string" || !value.version.trim() || value.package && value.package !== packageName) manifest.error = `${MANIFESTS[key]}: invalid package/version`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function verdict(status, reason, extra = {}) {
|
|
102
|
+
return { schemaVersion: 1, status, reason, adopted: false, projectType: "unknown", evidence: [], inherited: false, ...extra };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function inspectProject(root, key, packageName) {
|
|
106
|
+
const config = scopeConfig(root);
|
|
107
|
+
const packageFile = readJson(root, "package.json");
|
|
108
|
+
if (config.error || packageFile.error) return verdict("needs-context", "invalid-project-metadata", { diagnostics: [config.error || packageFile.error] });
|
|
109
|
+
const pkg = packageFile.value || {};
|
|
110
|
+
if (!["dependencies", "devDependencies", "optionalDependencies"].every((field) => validDependencies(pkg[field]))) {
|
|
111
|
+
return verdict("needs-context", "invalid-project-metadata", { diagnostics: ["package.json: dependency declarations must be nonempty strings"] });
|
|
112
|
+
}
|
|
113
|
+
const exclusion = platformExclusion(root, key, pkg, config);
|
|
114
|
+
if (exclusion) return exclusion;
|
|
115
|
+
const own = adoption(root, key, packageName, pkg, config);
|
|
116
|
+
if (own.error) return verdict("needs-context", "invalid-package-adoption", { diagnostics: [own.error] });
|
|
117
|
+
if (!own.adopted) return verdict("not-applicable", "package-not-adopted-in-target-project");
|
|
118
|
+
return adoptedScope(root, key, pkg, config, own);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validDependencies(deps) {
|
|
122
|
+
if (deps === undefined) return true;
|
|
123
|
+
return Boolean(deps && typeof deps === "object" && !Array.isArray(deps) && Object.values(deps).every((value) => typeof value === "string" && value.trim()));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function platformExclusion(root, key, pkg, config) {
|
|
127
|
+
const mobile = mobileEvidence(root, pkg);
|
|
128
|
+
const type = config.value?.projectType;
|
|
129
|
+
if (mobile.length || type === "mobile") return verdict("not-applicable", "mobile-project-excluded", { projectType: "mobile", evidence: mobile.length ? mobile : [CONFIG] });
|
|
130
|
+
if (type === "workspace") return verdict("not-applicable", "aggregate-workspace-is-not-project", { projectType: type, evidence: [CONFIG] });
|
|
131
|
+
if (config.value?.packages?.[key] === false) return verdict("not-applicable", "package-explicitly-disabled", { projectType: type || "unknown", evidence: [CONFIG] });
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function unsupportedFrontend(root, key, pkg, type) {
|
|
136
|
+
if (!["kit", "ui"].includes(key)) return false;
|
|
137
|
+
if (type && type !== "pc") return "pc-frontend-package-excluded";
|
|
138
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
139
|
+
return !deps.vue && (deps.react || deps.svelte || deps["@angular/core"] || fs.existsSync(path.join(root, "pom.xml"))) ? "unsupported-project-framework" : false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function defaultType(key) { return { kit: "pc", ui: "pc", bd: "backend", design: "documents", test: "testing" }[key]; }
|
|
143
|
+
|
|
144
|
+
function scopeFingerprint(root, key) {
|
|
145
|
+
const metadata = [CONFIG, "package.json", "pom.xml", MANIFESTS[key], "pages.json", "manifest.json"].map((file) => {
|
|
146
|
+
const full = path.join(root, file);
|
|
147
|
+
if (!fs.existsSync(full)) return [file, "missing"];
|
|
148
|
+
if (!inside(root, fs.realpathSync(full))) return [file, "external-metadata"];
|
|
149
|
+
try { return [file, crypto.createHash("sha256").update(fs.readFileSync(full)).digest("hex")]; }
|
|
150
|
+
catch (error) { return [file, `unreadable:${error.code}`]; }
|
|
151
|
+
});
|
|
152
|
+
return crypto.createHash("sha256").update(JSON.stringify(metadata)).digest("hex");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function adoptedScope(root, key, pkg, config, own) {
|
|
156
|
+
const type = config.value?.projectType;
|
|
157
|
+
if (type === "unknown") return verdict("needs-context", "project-platform-needs-context", { adopted: true, evidence: own.sources });
|
|
158
|
+
const unsupported = unsupportedFrontend(root, key, pkg, type);
|
|
159
|
+
if (unsupported) return verdict("not-applicable", unsupported, { adopted: true, projectType: type || "unknown", evidence: ["package.json/pom.xml", ...own.sources] });
|
|
160
|
+
return verdict("applicable", "target-project-explicitly-adopted-package", {
|
|
161
|
+
adopted: true, projectType: type || defaultType(key),
|
|
162
|
+
typeEvidence: type ? "explicit-config" : "adoption-default-not-host-observed", evidence: own.sources,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Parent installation, Vue files and keyword matches are never adoption evidence. */
|
|
167
|
+
function resolveScope({ projectRoot, packageName, targets = [] }) {
|
|
168
|
+
const root = fs.realpathSync(path.resolve(projectRoot || process.cwd()));
|
|
169
|
+
const key = packageName.replace("@agile-team/wl-skills-", "");
|
|
170
|
+
if (!Object.hasOwn(MANIFESTS, key)) throw new Error("Unknown WL package");
|
|
171
|
+
const located = targetRoots(root, targets.filter((item) => typeof item === "string" && item.trim()));
|
|
172
|
+
if (located.error || located.roots.length > 1) return { projectRoot: root, targets, scope: verdict("needs-context", located.error || "multiple-project-targets-split-required", { projectRoots: located.roots }) };
|
|
173
|
+
const actualRoot = located.roots[0] || root;
|
|
174
|
+
const actualTargets = located.resolved.map((target) => path.relative(actualRoot, target).split(path.sep).join("/") || ".");
|
|
175
|
+
return { projectRoot: actualRoot, targets: actualTargets, scope: { ...inspectProject(actualRoot, key, packageName), projectRoot: actualRoot, fingerprint: scopeFingerprint(actualRoot, key) } };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function excludedDecision(scope) {
|
|
179
|
+
const messages = {
|
|
180
|
+
"mobile-project-excluded": "目标为移动端/UniApp/小程序项目,不适用本轮WL项目约束",
|
|
181
|
+
"aggregate-workspace-is-not-project": "目标是聚合工作区,请指定实际已接入项目",
|
|
182
|
+
"package-explicitly-disabled": "目标项目已明确禁用本包",
|
|
183
|
+
"package-not-adopted-in-target-project": "目标项目未接入本包,不继承父目录或兄弟项目的安装",
|
|
184
|
+
"pc-frontend-package-excluded": "目标项目不是已接入的PC前端,PC规范不适用",
|
|
185
|
+
"unsupported-project-framework": "目标技术栈不是本包支持的Vue管理端,不套用PC Vue规范",
|
|
186
|
+
"multiple-project-targets-split-required": "目标跨越多个项目,请按项目分别判定并复用任务runId",
|
|
187
|
+
"target-outside-authorized-project": "目标超出当前授权项目边界,请从实际目标项目根调用",
|
|
188
|
+
"project-platform-needs-context": "项目已接入但平台未确认,请确认PC/后端/文档/测试或移动端范围",
|
|
189
|
+
};
|
|
190
|
+
const reason = messages[scope.reason] || "项目范围元数据无效,需要核对后再应用规则";
|
|
191
|
+
return { recordVersion: 1, status: scope.status, applicable: scope.status === "needs-context" ? null : false,
|
|
192
|
+
reason, reasons: [reason], scope, selectedSkills: [], candidates: [], baselineRules: [], requiredRules: [],
|
|
193
|
+
ruleDetails: [], requiredChecks: [], requiredFiles: [], missingInputs: scope.status === "needs-context" ? ["有效的目标项目范围"] : [],
|
|
194
|
+
gaps: [], constraints: [], ready: false, executionStatus: "not-executed", validationStatus: "unverified",
|
|
195
|
+
selectionEvidence: "project-scope-before-intent", hostDiscovery: "unverified", contentLoaded: "unverified", unverified: ["host-discovery", "model-read-canonical-files"] };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = { CONFIG, MANIFESTS, resolveScope, excludedDecision };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "urn:wl-skills:project-scope:1",
|
|
4
|
+
"title": "Explicit WL project scope and per-package adoption",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": [
|
|
7
|
+
"schemaVersion",
|
|
8
|
+
"projectType"
|
|
9
|
+
],
|
|
10
|
+
"additionalProperties": false,
|
|
11
|
+
"properties": {
|
|
12
|
+
"schemaVersion": {
|
|
13
|
+
"const": 1
|
|
14
|
+
},
|
|
15
|
+
"projectType": {
|
|
16
|
+
"enum": [
|
|
17
|
+
"pc",
|
|
18
|
+
"backend",
|
|
19
|
+
"documents",
|
|
20
|
+
"testing",
|
|
21
|
+
"mobile",
|
|
22
|
+
"workspace",
|
|
23
|
+
"unknown"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"packages": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"properties": {
|
|
30
|
+
"kit": {
|
|
31
|
+
"type": "boolean"
|
|
32
|
+
},
|
|
33
|
+
"ui": {
|
|
34
|
+
"type": "boolean"
|
|
35
|
+
},
|
|
36
|
+
"bd": {
|
|
37
|
+
"type": "boolean"
|
|
38
|
+
},
|
|
39
|
+
"design": {
|
|
40
|
+
"type": "boolean"
|
|
41
|
+
},
|
|
42
|
+
"test": {
|
|
43
|
+
"type": "boolean"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|