@monoharada/wcf-mcp 0.8.0 → 0.9.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/README.md +3 -30
- package/core.mjs +47 -0
- package/package.json +1 -2
- package/server.mjs +1 -0
- package/wcf-mcp.config.example.json +0 -3
- package/plugins/design-system-skills/check-drift.mjs +0 -685
- package/plugins/design-system-skills/get-skill-manifest.mjs +0 -193
- package/plugins/design-system-skills/index.mjs +0 -20
- package/plugins/design-system-skills/list-skills.mjs +0 -78
- package/plugins/design-system-skills/shared.mjs +0 -75
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
|
|
|
78
78
|
}
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
## 提供機能(
|
|
81
|
+
## 提供機能(16 tools + 1 prompt + 5 resources)
|
|
82
82
|
|
|
83
83
|
### ガードレール
|
|
84
84
|
|
|
@@ -145,17 +145,6 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
|
|
|
145
145
|
| `get_accessibility_docs` | component/topic/wcagLevel で A11y チェックリストとガイドライン要点を検索(`topic=all` では両ソースを混在返却) |
|
|
146
146
|
| `search_guidelines` | ガイドライン(topic/query)をスコア付きで検索 |
|
|
147
147
|
|
|
148
|
-
### スキル管理(リポジトリ内専用)
|
|
149
|
-
|
|
150
|
-
| ツール | 説明 |
|
|
151
|
-
|--------|------|
|
|
152
|
-
| `list_skills` | 登録済みデザインシステムスキル一覧を取得 |
|
|
153
|
-
| `get_skill_manifest` | スキルの SKILL.md マニフェストを取得 |
|
|
154
|
-
| `check_drift` | スキルレジストリとリポジトリの整合性を検証 |
|
|
155
|
-
|
|
156
|
-
> **注意**: これらのツールはリポジトリ内でのみ有効です。npx 実行時は `SKILLS_REGISTRY_UNAVAILABLE` エラーを返します。
|
|
157
|
-
> 有効化には `wcf-mcp.config.json` で `design-system-skills` プラグインを設定してください(下記参照)。
|
|
158
|
-
|
|
159
148
|
#### `get_design_tokens` の使用例
|
|
160
149
|
|
|
161
150
|
**リクエスト:**
|
|
@@ -253,6 +242,7 @@ claude mcp add wcf -- npx @monoharada/wcf-mcp
|
|
|
253
242
|
| `wcf://tokens` | トークン summary(type/category/themes/sample) |
|
|
254
243
|
| `wcf://guidelines/{topic}` | topic 別ガイドライン要約(`accessibility`,`css`,`patterns`,`all`) |
|
|
255
244
|
| `wcf://llms-full` | `llms-full.txt` の全文 |
|
|
245
|
+
| `wcf://skills` | 登録済み Claude Code / Cursor / Codex スキルカタログ(skills-registry.json ベース) |
|
|
256
246
|
|
|
257
247
|
### Figma MCP との併用
|
|
258
248
|
|
|
@@ -339,7 +329,7 @@ npx @monoharada/wcf-mcp --transport=http --port=3100
|
|
|
339
329
|
※ `./plugins/custom-validation-plugin.mjs` は利用側プロジェクトに配置してください。
|
|
340
330
|
このリポジトリには参照用として `packages/mcp-server/examples/plugins/custom-validation-plugin.mjs` を同梱しています。
|
|
341
331
|
|
|
342
|
-
### plugin 契約(v1
|
|
332
|
+
### plugin 契約(v1)
|
|
343
333
|
|
|
344
334
|
詳細仕様: [docs/plugin-contract-v1.md](../../docs/plugin-contract-v1.md)
|
|
345
335
|
|
|
@@ -392,21 +382,6 @@ Claude Desktop 設定例:
|
|
|
392
382
|
prefix: "myui" → dads-button → myui-button
|
|
393
383
|
```
|
|
394
384
|
|
|
395
|
-
## v0.8.0 新機能
|
|
396
|
-
|
|
397
|
-
### 新ツール(スキル管理)
|
|
398
|
-
- **`list_skills`** — 登録済みデザインシステムスキルの一覧を取得
|
|
399
|
-
- **`get_skill_manifest`** — スキルの SKILL.md マニフェスト内容を取得
|
|
400
|
-
- **`check_drift`** — スキルレジストリとリポジトリの整合性を検証(ドリフト検出)
|
|
401
|
-
|
|
402
|
-
### 改善
|
|
403
|
-
- **Plugin 契約 v1.1** — module plugin の `dataSources` パス解決をモジュールディレクトリ基準に統一
|
|
404
|
-
- **Skills Registry plugin** — `plugins/design-system-skills/` として同梱。`wcf-mcp.config.json` で有効化
|
|
405
|
-
|
|
406
|
-
> スキル管理ツールはリポジトリ内でのみ動作します。npx 実行時は graceful に `SKILLS_REGISTRY_UNAVAILABLE` を返します。
|
|
407
|
-
|
|
408
|
-
---
|
|
409
|
-
|
|
410
385
|
## v0.4.0 新機能
|
|
411
386
|
|
|
412
387
|
### 新ツール
|
|
@@ -668,8 +643,6 @@ packages/mcp-server/
|
|
|
668
643
|
├── server.mjs # MCP サーバー本体
|
|
669
644
|
├── validator.mjs # HTML バリデーター
|
|
670
645
|
├── package.json # npm パッケージ定義
|
|
671
|
-
├── plugins/ # 同梱プラグイン
|
|
672
|
-
│ └── design-system-skills/ # スキル管理ツール (list_skills, get_skill_manifest, check_drift)
|
|
673
646
|
└── data/ # バンドルデータ (npm run mcp:build で生成)
|
|
674
647
|
├── custom-elements.json
|
|
675
648
|
├── install-registry.json
|
package/core.mjs
CHANGED
|
@@ -152,8 +152,24 @@ export const WCF_RESOURCE_URIS = Object.freeze({
|
|
|
152
152
|
tokens: 'wcf://tokens',
|
|
153
153
|
guidelinesTemplate: 'wcf://guidelines/{topic}',
|
|
154
154
|
llmsFull: 'wcf://llms-full',
|
|
155
|
+
skills: 'wcf://skills',
|
|
155
156
|
});
|
|
156
157
|
|
|
158
|
+
/** Normalize a skill entry to summary fields (omit compat/manifest for wcf://skills). */
|
|
159
|
+
function normalizeSkillSummary(s) {
|
|
160
|
+
return {
|
|
161
|
+
name: s.name,
|
|
162
|
+
description: s.description ?? '',
|
|
163
|
+
status: s.status ?? 'active',
|
|
164
|
+
path: s.path ?? '',
|
|
165
|
+
entry: s.entry ?? 'SKILL.md',
|
|
166
|
+
clients: Array.isArray(s.clients) ? s.clients : [],
|
|
167
|
+
tags: Array.isArray(s.tags) ? s.tags : [],
|
|
168
|
+
version: typeof s.version === 'string' ? s.version : '0.0.0',
|
|
169
|
+
dependencies: Array.isArray(s.dependencies) ? s.dependencies : [],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
157
173
|
// Unidirectional synonym table: key → expands to include these terms (DIG-09)
|
|
158
174
|
// Searching "keyboard" also matches "focus", "tab" etc. but NOT reverse.
|
|
159
175
|
const SYNONYM_TABLE = new Map([
|
|
@@ -2154,6 +2170,33 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2154
2170
|
},
|
|
2155
2171
|
);
|
|
2156
2172
|
|
|
2173
|
+
// -----------------------------------------------------------------------
|
|
2174
|
+
// Resource: wcf://skills
|
|
2175
|
+
// -----------------------------------------------------------------------
|
|
2176
|
+
server.registerResource(
|
|
2177
|
+
'wcf_skills',
|
|
2178
|
+
WCF_RESOURCE_URIS.skills,
|
|
2179
|
+
{
|
|
2180
|
+
title: 'WCF Skills Catalog',
|
|
2181
|
+
description: 'Registered Claude Code / Cursor / Codex skills from skills-registry.json.',
|
|
2182
|
+
mimeType: 'application/json',
|
|
2183
|
+
},
|
|
2184
|
+
async () => {
|
|
2185
|
+
const registry = await loadJsonData('skills-registry.json');
|
|
2186
|
+
if (!registry || !Array.isArray(registry.skills)) {
|
|
2187
|
+
throw new Error('SKILLS_REGISTRY_UNAVAILABLE: skills-registry.json is not available.');
|
|
2188
|
+
}
|
|
2189
|
+
const skills = registry.skills.map(normalizeSkillSummary);
|
|
2190
|
+
return {
|
|
2191
|
+
contents: [{
|
|
2192
|
+
uri: WCF_RESOURCE_URIS.skills,
|
|
2193
|
+
mimeType: 'application/json',
|
|
2194
|
+
text: JSON.stringify({ schemaVersion: registry.schemaVersion ?? 2, total: skills.length, skills }, null, 2),
|
|
2195
|
+
}],
|
|
2196
|
+
};
|
|
2197
|
+
},
|
|
2198
|
+
);
|
|
2199
|
+
|
|
2157
2200
|
// -----------------------------------------------------------------------
|
|
2158
2201
|
// Tool: get_design_system_overview
|
|
2159
2202
|
// -----------------------------------------------------------------------
|
|
@@ -2249,6 +2292,10 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
2249
2292
|
uri: WCF_RESOURCE_URIS.llmsFull,
|
|
2250
2293
|
purpose: 'Full LLM reference text for WCF',
|
|
2251
2294
|
},
|
|
2295
|
+
{
|
|
2296
|
+
uri: WCF_RESOURCE_URIS.skills,
|
|
2297
|
+
purpose: 'Skills catalog snapshot',
|
|
2298
|
+
},
|
|
2252
2299
|
],
|
|
2253
2300
|
availableTools: [
|
|
2254
2301
|
{ name: 'get_design_system_overview', purpose: 'This overview (start here)' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoharada/wcf-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "MCP server for the web-components-factory design system. Provides component discovery, validation, and pattern-based UI composition without cloning the repository.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"validator.mjs",
|
|
14
14
|
"data/",
|
|
15
15
|
"examples/",
|
|
16
|
-
"plugins/",
|
|
17
16
|
"wcf-mcp.config.example.json"
|
|
18
17
|
],
|
|
19
18
|
"keywords": [
|
package/server.mjs
CHANGED
|
@@ -25,6 +25,7 @@ const REPO_FILE_MAP = {
|
|
|
25
25
|
'design-tokens.json': 'design-tokens.json',
|
|
26
26
|
'guidelines-index.json': 'guidelines-index.json',
|
|
27
27
|
'llms-full.txt': 'llms-full.txt',
|
|
28
|
+
'skills-registry.json': 'registry/skills-registry.json',
|
|
28
29
|
};
|
|
29
30
|
export const DEFAULT_WCF_MCP_CONFIG = 'wcf-mcp.config.json';
|
|
30
31
|
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
"guidelines-index.json": "./packages/mcp-server/data/guidelines-index.json"
|
|
4
4
|
},
|
|
5
5
|
"plugins": [
|
|
6
|
-
{
|
|
7
|
-
"module": "./packages/mcp-server/plugins/design-system-skills/index.mjs"
|
|
8
|
-
},
|
|
9
6
|
{
|
|
10
7
|
"module": "./packages/mcp-server/examples/plugins/custom-validation-plugin.mjs"
|
|
11
8
|
},
|
|
@@ -1,685 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* check_drift plugin tool for wcf-mcp.
|
|
3
|
-
* Checks consistency across 4 data sources (CEM, install-registry,
|
|
4
|
-
* skills-registry, pattern-registry) and detects drift (divergence).
|
|
5
|
-
* Phase 1: JSON comparison only (no SKILL.md content analysis).
|
|
6
|
-
* Plugin Contract v1.0+
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { access } from 'node:fs/promises';
|
|
10
|
-
import { resolve } from 'node:path';
|
|
11
|
-
import { REPO_ROOT, loadRegistry } from './shared.mjs';
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Helper utilities
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Simple string hash for generating drift IDs.
|
|
19
|
-
* @param {string} str
|
|
20
|
-
* @returns {string} 8-char hex string
|
|
21
|
-
*/
|
|
22
|
-
function hashId(str) {
|
|
23
|
-
let hash = 0;
|
|
24
|
-
for (let i = 0; i < str.length; i++) {
|
|
25
|
-
const char = str.charCodeAt(i);
|
|
26
|
-
hash = ((hash << 5) - hash) + char;
|
|
27
|
-
hash |= 0;
|
|
28
|
-
}
|
|
29
|
-
return Math.abs(hash).toString(16).padStart(8, '0').slice(0, 8);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Generate a unique drift ID from rule + detail string.
|
|
34
|
-
* @param {string} ruleId
|
|
35
|
-
* @param {string} detail
|
|
36
|
-
* @returns {string}
|
|
37
|
-
*/
|
|
38
|
-
function driftId(ruleId, detail) {
|
|
39
|
-
return `DRIFT-${ruleId}-${hashId(detail)}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Create a drift report entry.
|
|
44
|
-
* @param {string} ruleId
|
|
45
|
-
* @param {string} severity HIGH | MEDIUM | LOW
|
|
46
|
-
* @param {string} source Data source name
|
|
47
|
-
* @param {string} target Target data source name
|
|
48
|
-
* @param {string} message Human-readable description
|
|
49
|
-
* @param {object} [details] Additional context
|
|
50
|
-
* @returns {object}
|
|
51
|
-
*/
|
|
52
|
-
function createDrift(ruleId, severity, source, target, message, details = {}) {
|
|
53
|
-
return {
|
|
54
|
-
id: driftId(ruleId, message),
|
|
55
|
-
ruleId,
|
|
56
|
-
severity,
|
|
57
|
-
source,
|
|
58
|
-
target,
|
|
59
|
-
message,
|
|
60
|
-
details,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Create a suggestion entry tied to a drift.
|
|
66
|
-
* @param {string} id The drift ID this suggestion relates to
|
|
67
|
-
* @param {string} action add | remove | update | document | investigate
|
|
68
|
-
* @param {string} description Human-readable suggestion
|
|
69
|
-
* @param {string} target File or data source to act on
|
|
70
|
-
* @param {string} [priority] recommended | optional
|
|
71
|
-
* @returns {object}
|
|
72
|
-
*/
|
|
73
|
-
function createSuggestion(id, action, description, target, priority = 'recommended') {
|
|
74
|
-
return { driftId: id, action, description, target, priority };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
// Data extraction helpers
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Extract custom-element-definition tags from CEM.
|
|
83
|
-
* @param {object|null} cem
|
|
84
|
-
* @returns {Map<string, string>} tagName -> modulePath
|
|
85
|
-
*/
|
|
86
|
-
function extractCemTags(cem) {
|
|
87
|
-
const tags = new Map();
|
|
88
|
-
if (!cem?.modules) return tags;
|
|
89
|
-
for (const mod of cem.modules) {
|
|
90
|
-
if (!Array.isArray(mod.exports)) continue;
|
|
91
|
-
for (const exp of mod.exports) {
|
|
92
|
-
if (exp.kind === 'custom-element-definition' && exp.name) {
|
|
93
|
-
tags.set(exp.name, mod.path);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return tags;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Extract dads-* tag names from an HTML string.
|
|
102
|
-
* @param {string} html
|
|
103
|
-
* @returns {string[]} Unique sorted tag names
|
|
104
|
-
*/
|
|
105
|
-
function extractDadsTags(html) {
|
|
106
|
-
const matches = [...String(html).matchAll(/<(dads-[a-z][a-z0-9-]*)/g)];
|
|
107
|
-
return [...new Set(matches.map((m) => m[1]))].sort();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
// Rule implementations
|
|
112
|
-
// Each returns { drifts: DriftReport[], suggestions: DriftSuggestion[] }
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* CIR01 - CEM component missing from install-registry.
|
|
117
|
-
* For each CEM dads-* tag, verify it exists in install-registry tags.
|
|
118
|
-
*/
|
|
119
|
-
function ruleCIR01(cemTags, irTags) {
|
|
120
|
-
const drifts = [];
|
|
121
|
-
const suggestions = [];
|
|
122
|
-
for (const [tag] of cemTags) {
|
|
123
|
-
if (!tag.startsWith('dads-')) continue;
|
|
124
|
-
if (!(tag in irTags)) {
|
|
125
|
-
const d = createDrift(
|
|
126
|
-
'CIR01',
|
|
127
|
-
'HIGH',
|
|
128
|
-
'custom-elements.json',
|
|
129
|
-
'install-registry.json',
|
|
130
|
-
`CEM tag "${tag}" is missing from install-registry tags`,
|
|
131
|
-
{ tag },
|
|
132
|
-
);
|
|
133
|
-
drifts.push(d);
|
|
134
|
-
suggestions.push(
|
|
135
|
-
createSuggestion(d.id, 'add', `Add "${tag}" to install-registry tags section`, 'registry/install-registry.json'),
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return { drifts, suggestions };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* CIR02 - Non-standard tags in CEM.
|
|
144
|
-
* CEM exports with kind=custom-element-definition where name does NOT match dads-*.
|
|
145
|
-
*/
|
|
146
|
-
function ruleCIR02(cemTags) {
|
|
147
|
-
const drifts = [];
|
|
148
|
-
const suggestions = [];
|
|
149
|
-
for (const [tag, modulePath] of cemTags) {
|
|
150
|
-
if (!tag.startsWith('dads-')) {
|
|
151
|
-
const d = createDrift(
|
|
152
|
-
'CIR02',
|
|
153
|
-
'LOW',
|
|
154
|
-
'custom-elements.json',
|
|
155
|
-
'custom-elements.json',
|
|
156
|
-
`CEM tag "${tag}" does not follow the dads-* naming convention`,
|
|
157
|
-
{ tag, modulePath },
|
|
158
|
-
);
|
|
159
|
-
drifts.push(d);
|
|
160
|
-
suggestions.push(
|
|
161
|
-
createSuggestion(d.id, 'investigate', `Verify if "${tag}" should follow the dads-* naming convention`, 'custom-elements.json', 'optional'),
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return { drifts, suggestions };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* CIT01 - install-registry tag missing from CEM.
|
|
170
|
-
* For each install-registry tags key, verify it exists in CEM exports.
|
|
171
|
-
*/
|
|
172
|
-
function ruleCIT01(cemTags, irTags) {
|
|
173
|
-
const drifts = [];
|
|
174
|
-
const suggestions = [];
|
|
175
|
-
for (const tag of Object.keys(irTags)) {
|
|
176
|
-
if (!cemTags.has(tag)) {
|
|
177
|
-
const d = createDrift(
|
|
178
|
-
'CIT01',
|
|
179
|
-
'HIGH',
|
|
180
|
-
'install-registry.json',
|
|
181
|
-
'custom-elements.json',
|
|
182
|
-
`install-registry tag "${tag}" is not defined in CEM`,
|
|
183
|
-
{ tag, componentId: irTags[tag] },
|
|
184
|
-
);
|
|
185
|
-
drifts.push(d);
|
|
186
|
-
suggestions.push(
|
|
187
|
-
createSuggestion(d.id, 'remove', `Remove "${tag}" from install-registry or add its CEM definition`, 'registry/install-registry.json'),
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return { drifts, suggestions };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* CIT02 - install-registry component tags mismatch with CEM.
|
|
196
|
-
* For each install-registry component, compare its tags array with CEM-derived
|
|
197
|
-
* tags for that component (by checking which CEM tags map to install-registry component IDs).
|
|
198
|
-
*/
|
|
199
|
-
function ruleCIT02(cemTags, irTags, irComponents) {
|
|
200
|
-
const drifts = [];
|
|
201
|
-
const suggestions = [];
|
|
202
|
-
|
|
203
|
-
// Build reverse map: componentId -> Set<tag> from irTags
|
|
204
|
-
const tagsByComponent = {};
|
|
205
|
-
for (const [tag, compId] of Object.entries(irTags)) {
|
|
206
|
-
if (!tagsByComponent[compId]) tagsByComponent[compId] = new Set();
|
|
207
|
-
tagsByComponent[compId].add(tag);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
for (const [compId, comp] of Object.entries(irComponents)) {
|
|
211
|
-
if (!Array.isArray(comp.tags)) continue;
|
|
212
|
-
const declaredTags = new Set(comp.tags);
|
|
213
|
-
const registeredTags = tagsByComponent[compId] ?? new Set();
|
|
214
|
-
|
|
215
|
-
// Check tags in component.tags that are not in the tags section
|
|
216
|
-
for (const tag of declaredTags) {
|
|
217
|
-
if (!registeredTags.has(tag)) {
|
|
218
|
-
const d = createDrift(
|
|
219
|
-
'CIT02',
|
|
220
|
-
'MEDIUM',
|
|
221
|
-
'install-registry.json',
|
|
222
|
-
'install-registry.json',
|
|
223
|
-
`Component "${compId}" declares tag "${tag}" but it is not in the tags section`,
|
|
224
|
-
{ componentId: compId, tag },
|
|
225
|
-
);
|
|
226
|
-
drifts.push(d);
|
|
227
|
-
suggestions.push(
|
|
228
|
-
createSuggestion(d.id, 'update', `Add "${tag}" to install-registry tags section mapping to "${compId}"`, 'registry/install-registry.json'),
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Check tags in tags section that are not in component.tags
|
|
234
|
-
for (const tag of registeredTags) {
|
|
235
|
-
if (!declaredTags.has(tag)) {
|
|
236
|
-
const d = createDrift(
|
|
237
|
-
'CIT02',
|
|
238
|
-
'MEDIUM',
|
|
239
|
-
'install-registry.json',
|
|
240
|
-
'install-registry.json',
|
|
241
|
-
`Tag "${tag}" maps to "${compId}" in tags section but is not in component.tags`,
|
|
242
|
-
{ componentId: compId, tag },
|
|
243
|
-
);
|
|
244
|
-
drifts.push(d);
|
|
245
|
-
suggestions.push(
|
|
246
|
-
createSuggestion(d.id, 'update', `Add "${tag}" to component "${compId}" tags array`, 'registry/install-registry.json'),
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return { drifts, suggestions };
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* IRD01 - Broken internal dependency in install-registry.
|
|
256
|
-
* For each component's deps[], verify dep ID exists in components.
|
|
257
|
-
*/
|
|
258
|
-
function ruleIRD01(irComponents) {
|
|
259
|
-
const drifts = [];
|
|
260
|
-
const suggestions = [];
|
|
261
|
-
const componentIds = new Set(Object.keys(irComponents));
|
|
262
|
-
|
|
263
|
-
for (const [compId, comp] of Object.entries(irComponents)) {
|
|
264
|
-
if (!Array.isArray(comp.deps)) continue;
|
|
265
|
-
for (const dep of comp.deps) {
|
|
266
|
-
if (!componentIds.has(dep)) {
|
|
267
|
-
const d = createDrift(
|
|
268
|
-
'IRD01',
|
|
269
|
-
'HIGH',
|
|
270
|
-
'install-registry.json',
|
|
271
|
-
'install-registry.json',
|
|
272
|
-
`Component "${compId}" depends on "${dep}" which does not exist in components`,
|
|
273
|
-
{ componentId: compId, dependency: dep },
|
|
274
|
-
);
|
|
275
|
-
drifts.push(d);
|
|
276
|
-
suggestions.push(
|
|
277
|
-
createSuggestion(d.id, 'update', `Fix dependency "${dep}" in component "${compId}" or add the missing component`, 'registry/install-registry.json'),
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
return { drifts, suggestions };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* IRT01 - install-registry tags/components inconsistency.
|
|
287
|
-
* tags keys must map to valid component IDs, and component tags must be in tags section.
|
|
288
|
-
*/
|
|
289
|
-
function ruleIRT01(irTags, irComponents) {
|
|
290
|
-
const drifts = [];
|
|
291
|
-
const suggestions = [];
|
|
292
|
-
const componentIds = new Set(Object.keys(irComponents));
|
|
293
|
-
|
|
294
|
-
// Check tags section: each value must be a valid component ID
|
|
295
|
-
for (const [tag, compId] of Object.entries(irTags)) {
|
|
296
|
-
if (!componentIds.has(compId)) {
|
|
297
|
-
const d = createDrift(
|
|
298
|
-
'IRT01',
|
|
299
|
-
'HIGH',
|
|
300
|
-
'install-registry.json',
|
|
301
|
-
'install-registry.json',
|
|
302
|
-
`Tag "${tag}" maps to component "${compId}" which does not exist`,
|
|
303
|
-
{ tag, componentId: compId },
|
|
304
|
-
);
|
|
305
|
-
drifts.push(d);
|
|
306
|
-
suggestions.push(
|
|
307
|
-
createSuggestion(d.id, 'update', `Fix tag "${tag}" mapping or add component "${compId}"`, 'registry/install-registry.json'),
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Check components section: each tag must be in tags section
|
|
313
|
-
for (const [compId, comp] of Object.entries(irComponents)) {
|
|
314
|
-
if (!Array.isArray(comp.tags)) continue;
|
|
315
|
-
for (const tag of comp.tags) {
|
|
316
|
-
if (!(tag in irTags)) {
|
|
317
|
-
const d = createDrift(
|
|
318
|
-
'IRT01',
|
|
319
|
-
'HIGH',
|
|
320
|
-
'install-registry.json',
|
|
321
|
-
'install-registry.json',
|
|
322
|
-
`Component "${compId}" tag "${tag}" is missing from tags section`,
|
|
323
|
-
{ componentId: compId, tag },
|
|
324
|
-
);
|
|
325
|
-
drifts.push(d);
|
|
326
|
-
suggestions.push(
|
|
327
|
-
createSuggestion(d.id, 'add', `Add "${tag}": "${compId}" to install-registry tags section`, 'registry/install-registry.json'),
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
return { drifts, suggestions };
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* CPR01 - pattern requires references missing component.
|
|
337
|
-
* pattern.requires[] components must exist in install-registry components.
|
|
338
|
-
*/
|
|
339
|
-
function ruleCPR01(patterns, irComponents) {
|
|
340
|
-
const drifts = [];
|
|
341
|
-
const suggestions = [];
|
|
342
|
-
const componentIds = new Set(Object.keys(irComponents));
|
|
343
|
-
|
|
344
|
-
for (const [patId, pat] of Object.entries(patterns)) {
|
|
345
|
-
if (!Array.isArray(pat.requires)) continue;
|
|
346
|
-
for (const req of pat.requires) {
|
|
347
|
-
if (!componentIds.has(req)) {
|
|
348
|
-
const d = createDrift(
|
|
349
|
-
'CPR01',
|
|
350
|
-
'HIGH',
|
|
351
|
-
'pattern-registry.json',
|
|
352
|
-
'install-registry.json',
|
|
353
|
-
`Pattern "${patId}" requires component "${req}" which is not in install-registry`,
|
|
354
|
-
{ patternId: patId, requirement: req },
|
|
355
|
-
);
|
|
356
|
-
drifts.push(d);
|
|
357
|
-
suggestions.push(
|
|
358
|
-
createSuggestion(d.id, 'add', `Add component "${req}" to install-registry or remove it from pattern "${patId}" requires`, 'registry/install-registry.json'),
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return { drifts, suggestions };
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* CPT01 - pattern HTML contains unknown CEM tags.
|
|
368
|
-
* Extract dads-* tags from pattern HTML, verify each exists in CEM.
|
|
369
|
-
*/
|
|
370
|
-
function ruleCPT01(patterns, cemTags) {
|
|
371
|
-
const drifts = [];
|
|
372
|
-
const suggestions = [];
|
|
373
|
-
|
|
374
|
-
for (const [patId, pat] of Object.entries(patterns)) {
|
|
375
|
-
if (!pat.html) continue;
|
|
376
|
-
const tags = extractDadsTags(pat.html);
|
|
377
|
-
for (const tag of tags) {
|
|
378
|
-
if (!cemTags.has(tag)) {
|
|
379
|
-
const d = createDrift(
|
|
380
|
-
'CPT01',
|
|
381
|
-
'HIGH',
|
|
382
|
-
'pattern-registry.json',
|
|
383
|
-
'custom-elements.json',
|
|
384
|
-
`Pattern "${patId}" HTML uses tag "${tag}" which is not defined in CEM`,
|
|
385
|
-
{ patternId: patId, tag },
|
|
386
|
-
);
|
|
387
|
-
drifts.push(d);
|
|
388
|
-
suggestions.push(
|
|
389
|
-
createSuggestion(d.id, 'investigate', `Verify tag "${tag}" in pattern "${patId}" HTML or add its CEM definition`, 'registry/pattern-registry.json'),
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
return { drifts, suggestions };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* CPT02 - pattern HTML uses tags not declared in requires.
|
|
399
|
-
* dads-* tags in pattern HTML -> reverse lookup to component ID -> must be in pattern.requires.
|
|
400
|
-
*/
|
|
401
|
-
function ruleCPT02(patterns, irTags) {
|
|
402
|
-
const drifts = [];
|
|
403
|
-
const suggestions = [];
|
|
404
|
-
|
|
405
|
-
for (const [patId, pat] of Object.entries(patterns)) {
|
|
406
|
-
if (!pat.html || !Array.isArray(pat.requires)) continue;
|
|
407
|
-
const tags = extractDadsTags(pat.html);
|
|
408
|
-
const requiresSet = new Set(pat.requires);
|
|
409
|
-
|
|
410
|
-
for (const tag of tags) {
|
|
411
|
-
const compId = irTags[tag];
|
|
412
|
-
if (compId && !requiresSet.has(compId)) {
|
|
413
|
-
const d = createDrift(
|
|
414
|
-
'CPT02',
|
|
415
|
-
'MEDIUM',
|
|
416
|
-
'pattern-registry.json',
|
|
417
|
-
'pattern-registry.json',
|
|
418
|
-
`Pattern "${patId}" HTML uses tag "${tag}" (component "${compId}") but "${compId}" is not in requires`,
|
|
419
|
-
{ patternId: patId, tag, componentId: compId },
|
|
420
|
-
);
|
|
421
|
-
drifts.push(d);
|
|
422
|
-
suggestions.push(
|
|
423
|
-
createSuggestion(d.id, 'add', `Add "${compId}" to pattern "${patId}" requires array`, 'registry/pattern-registry.json'),
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
return { drifts, suggestions };
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* CPC01 - Pattern coverage of components.
|
|
433
|
-
* Count how many install-registry components are referenced by at least one pattern's requires.
|
|
434
|
-
*/
|
|
435
|
-
function ruleCPC01(patterns, irComponents) {
|
|
436
|
-
const drifts = [];
|
|
437
|
-
const suggestions = [];
|
|
438
|
-
const componentIds = Object.keys(irComponents);
|
|
439
|
-
if (componentIds.length === 0) return { drifts, suggestions };
|
|
440
|
-
|
|
441
|
-
// Collect all component IDs referenced by any pattern
|
|
442
|
-
const coveredComponents = new Set();
|
|
443
|
-
for (const pat of Object.values(patterns)) {
|
|
444
|
-
if (!Array.isArray(pat.requires)) continue;
|
|
445
|
-
for (const req of pat.requires) {
|
|
446
|
-
coveredComponents.add(req);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const totalComponents = componentIds.length;
|
|
451
|
-
const uncoveredIds = [];
|
|
452
|
-
for (const id of componentIds) {
|
|
453
|
-
if (!coveredComponents.has(id)) uncoveredIds.push(id);
|
|
454
|
-
}
|
|
455
|
-
const coveredCount = totalComponents - uncoveredIds.length;
|
|
456
|
-
const coveragePercent = Math.round((coveredCount / totalComponents) * 100);
|
|
457
|
-
const severity = coveragePercent < 50 ? 'MEDIUM' : 'LOW';
|
|
458
|
-
|
|
459
|
-
if (uncoveredIds.length > 0) {
|
|
460
|
-
const d = createDrift(
|
|
461
|
-
'CPC01',
|
|
462
|
-
severity,
|
|
463
|
-
'pattern-registry.json',
|
|
464
|
-
'install-registry.json',
|
|
465
|
-
`Pattern coverage: ${coveredCount}/${totalComponents} components (${coveragePercent}%). ${uncoveredIds.length} components not used in any pattern.`,
|
|
466
|
-
{
|
|
467
|
-
coverage: coveragePercent,
|
|
468
|
-
coveredCount,
|
|
469
|
-
totalComponents,
|
|
470
|
-
uncoveredComponents: uncoveredIds,
|
|
471
|
-
},
|
|
472
|
-
);
|
|
473
|
-
drifts.push(d);
|
|
474
|
-
suggestions.push(
|
|
475
|
-
createSuggestion(d.id, 'document', `Consider adding patterns for uncovered components: ${uncoveredIds.slice(0, 5).join(', ')}${uncoveredIds.length > 5 ? '...' : ''}`, 'registry/pattern-registry.json', 'optional'),
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
return { drifts, suggestions };
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* SIR01 - skills-registry path existence.
|
|
483
|
-
* For each skill, verify skill.path + '/' + skill.entry file exists on disk.
|
|
484
|
-
*/
|
|
485
|
-
async function ruleSIR01(skillsRegistry) {
|
|
486
|
-
const drifts = [];
|
|
487
|
-
const suggestions = [];
|
|
488
|
-
if (!skillsRegistry?.skills) return { drifts, suggestions };
|
|
489
|
-
|
|
490
|
-
for (const skill of skillsRegistry.skills) {
|
|
491
|
-
const entry = skill.entry ?? 'SKILL.md';
|
|
492
|
-
const fullPath = resolve(REPO_ROOT, skill.path, entry);
|
|
493
|
-
try {
|
|
494
|
-
await access(fullPath);
|
|
495
|
-
} catch {
|
|
496
|
-
const d = createDrift(
|
|
497
|
-
'SIR01',
|
|
498
|
-
'HIGH',
|
|
499
|
-
'skills-registry.json',
|
|
500
|
-
'filesystem',
|
|
501
|
-
`Skill "${skill.name}" entry file not found at "${skill.path}/${entry}"`,
|
|
502
|
-
{ skillName: skill.name, path: skill.path, entry, expected: fullPath },
|
|
503
|
-
);
|
|
504
|
-
drifts.push(d);
|
|
505
|
-
suggestions.push(
|
|
506
|
-
createSuggestion(d.id, 'add', `Create "${entry}" at "${skill.path}/" or update the skill registry path`, 'registry/skills-registry.json'),
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return { drifts, suggestions };
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* SID01 - Skills dependency existence (v2 only).
|
|
515
|
-
* Each skill.dependencies[] name must exist as another skill.name in the registry.
|
|
516
|
-
*/
|
|
517
|
-
function ruleSID01(skillsRegistry) {
|
|
518
|
-
const drifts = [];
|
|
519
|
-
const suggestions = [];
|
|
520
|
-
if (!skillsRegistry?.skills) return { drifts, suggestions };
|
|
521
|
-
|
|
522
|
-
const skillNames = new Set(skillsRegistry.skills.map((s) => s.name));
|
|
523
|
-
|
|
524
|
-
for (const skill of skillsRegistry.skills) {
|
|
525
|
-
if (!Array.isArray(skill.dependencies)) continue;
|
|
526
|
-
for (const dep of skill.dependencies) {
|
|
527
|
-
const depName = typeof dep === 'string' ? dep : dep?.name;
|
|
528
|
-
if (depName && !skillNames.has(depName)) {
|
|
529
|
-
const d = createDrift(
|
|
530
|
-
'SID01',
|
|
531
|
-
'HIGH',
|
|
532
|
-
'skills-registry.json',
|
|
533
|
-
'skills-registry.json',
|
|
534
|
-
`Skill "${skill.name}" depends on "${depName}" which does not exist in the registry`,
|
|
535
|
-
{ skillName: skill.name, dependency: depName },
|
|
536
|
-
);
|
|
537
|
-
drifts.push(d);
|
|
538
|
-
suggestions.push(
|
|
539
|
-
createSuggestion(d.id, 'add', `Add skill "${depName}" to the registry or remove the dependency from "${skill.name}"`, 'registry/skills-registry.json'),
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
return { drifts, suggestions };
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// ---------------------------------------------------------------------------
|
|
548
|
-
// Scope mapping
|
|
549
|
-
// ---------------------------------------------------------------------------
|
|
550
|
-
|
|
551
|
-
const SCOPE_RULES = {
|
|
552
|
-
all: ['CIR01', 'CIR02', 'CIT01', 'CIT02', 'IRD01', 'IRT01', 'CPR01', 'CPT01', 'CPT02', 'CPC01', 'SIR01', 'SID01'],
|
|
553
|
-
cem: ['CIR01', 'CIR02', 'CIT01', 'CIT02', 'IRD01', 'IRT01'],
|
|
554
|
-
skills: ['SIR01', 'SID01'],
|
|
555
|
-
tokens: [], // Reserved for future Phase 2
|
|
556
|
-
patterns: ['CPR01', 'CPT01', 'CPT02', 'CPC01'],
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
// ---------------------------------------------------------------------------
|
|
560
|
-
// Main handler
|
|
561
|
-
// ---------------------------------------------------------------------------
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* @param {object} args
|
|
565
|
-
* @param {{ helpers: { loadJsonData: Function, buildJsonToolResponse: Function } }} ctx
|
|
566
|
-
*/
|
|
567
|
-
async function checkDriftHandler(args, { helpers }) {
|
|
568
|
-
const scope = args?.scope ?? 'all';
|
|
569
|
-
const activeRules = new Set(SCOPE_RULES[scope] ?? SCOPE_RULES.all);
|
|
570
|
-
const drifts = [];
|
|
571
|
-
const suggestions = [];
|
|
572
|
-
const rulesExecuted = [];
|
|
573
|
-
|
|
574
|
-
// -----------------------------------------------------------------------
|
|
575
|
-
// Load data sources
|
|
576
|
-
// -----------------------------------------------------------------------
|
|
577
|
-
const [cem, ir, pr, sr] = await Promise.all([
|
|
578
|
-
helpers.loadJsonData('custom-elements.json'),
|
|
579
|
-
helpers.loadJsonData('install-registry.json'),
|
|
580
|
-
helpers.loadJsonData('pattern-registry.json'),
|
|
581
|
-
loadRegistry(),
|
|
582
|
-
]);
|
|
583
|
-
|
|
584
|
-
// -----------------------------------------------------------------------
|
|
585
|
-
// Extract commonly used data
|
|
586
|
-
// -----------------------------------------------------------------------
|
|
587
|
-
const cemTags = extractCemTags(cem);
|
|
588
|
-
const irTags = ir?.tags ?? {};
|
|
589
|
-
const irComponents = ir?.components ?? {};
|
|
590
|
-
const patterns = pr?.patterns ?? {};
|
|
591
|
-
|
|
592
|
-
// -----------------------------------------------------------------------
|
|
593
|
-
// Execute rules based on scope
|
|
594
|
-
// -----------------------------------------------------------------------
|
|
595
|
-
|
|
596
|
-
/** Collect results from a synchronous rule function */
|
|
597
|
-
function runSync(ruleId, fn) {
|
|
598
|
-
if (!activeRules.has(ruleId)) return;
|
|
599
|
-
rulesExecuted.push(ruleId);
|
|
600
|
-
const result = fn();
|
|
601
|
-
drifts.push(...result.drifts);
|
|
602
|
-
suggestions.push(...result.suggestions);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
/** Collect results from an async rule function */
|
|
606
|
-
async function runAsync(ruleId, fn) {
|
|
607
|
-
if (!activeRules.has(ruleId)) return;
|
|
608
|
-
rulesExecuted.push(ruleId);
|
|
609
|
-
const result = await fn();
|
|
610
|
-
drifts.push(...result.drifts);
|
|
611
|
-
suggestions.push(...result.suggestions);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// CEM <-> install-registry rules
|
|
615
|
-
runSync('CIR01', () => ruleCIR01(cemTags, irTags));
|
|
616
|
-
runSync('CIR02', () => ruleCIR02(cemTags));
|
|
617
|
-
runSync('CIT01', () => ruleCIT01(cemTags, irTags));
|
|
618
|
-
runSync('CIT02', () => ruleCIT02(cemTags, irTags, irComponents));
|
|
619
|
-
runSync('IRD01', () => ruleIRD01(irComponents));
|
|
620
|
-
runSync('IRT01', () => ruleIRT01(irTags, irComponents));
|
|
621
|
-
|
|
622
|
-
// Pattern rules
|
|
623
|
-
runSync('CPR01', () => ruleCPR01(patterns, irComponents));
|
|
624
|
-
runSync('CPT01', () => ruleCPT01(patterns, cemTags));
|
|
625
|
-
runSync('CPT02', () => ruleCPT02(patterns, irTags));
|
|
626
|
-
runSync('CPC01', () => ruleCPC01(patterns, irComponents));
|
|
627
|
-
|
|
628
|
-
// Skills rules — require skills-registry for scopes that include skill rules
|
|
629
|
-
const hasSkillRules = ['SIR01', 'SID01'].some((id) => activeRules.has(id));
|
|
630
|
-
if (hasSkillRules && !sr) {
|
|
631
|
-
const d = createDrift(
|
|
632
|
-
'SIR01',
|
|
633
|
-
'HIGH',
|
|
634
|
-
'skills-registry.json',
|
|
635
|
-
'filesystem',
|
|
636
|
-
'skills-registry.json is missing or corrupted — all skill rules skipped',
|
|
637
|
-
{},
|
|
638
|
-
);
|
|
639
|
-
drifts.push(d);
|
|
640
|
-
suggestions.push(
|
|
641
|
-
createSuggestion(d.id, 'add', 'Create or restore registry/skills-registry.json', 'registry/skills-registry.json'),
|
|
642
|
-
);
|
|
643
|
-
rulesExecuted.push(...['SIR01', 'SID01'].filter((id) => activeRules.has(id)));
|
|
644
|
-
} else {
|
|
645
|
-
await runAsync('SIR01', () => ruleSIR01(sr));
|
|
646
|
-
runSync('SID01', () => ruleSID01(sr));
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// -----------------------------------------------------------------------
|
|
650
|
-
// Build output
|
|
651
|
-
// -----------------------------------------------------------------------
|
|
652
|
-
const summary = { total: drifts.length, high: 0, medium: 0, low: 0, ignored: 0 };
|
|
653
|
-
for (const d of drifts) {
|
|
654
|
-
if (d.severity === 'HIGH') summary.high++;
|
|
655
|
-
else if (d.severity === 'MEDIUM') summary.medium++;
|
|
656
|
-
else if (d.severity === 'LOW') summary.low++;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
const meta = {
|
|
660
|
-
phase: 1,
|
|
661
|
-
scope,
|
|
662
|
-
rulesExecuted,
|
|
663
|
-
timestamp: new Date().toISOString(),
|
|
664
|
-
};
|
|
665
|
-
|
|
666
|
-
return helpers.buildJsonToolResponse({ drifts, suggestions, summary, meta });
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// ---------------------------------------------------------------------------
|
|
670
|
-
// Plugin export
|
|
671
|
-
// ---------------------------------------------------------------------------
|
|
672
|
-
|
|
673
|
-
export default {
|
|
674
|
-
name: 'design-system-skills-drift',
|
|
675
|
-
version: '1.0.0',
|
|
676
|
-
tools: [
|
|
677
|
-
{
|
|
678
|
-
name: 'check_drift',
|
|
679
|
-
description:
|
|
680
|
-
'Check consistency across CEM, install-registry, skills-registry, and pattern-registry. Detects drift (divergence) between data sources. When: before PR, after registry updates, periodic audits. Returns: {drifts[], suggestions[], summary{total,high,medium,low,ignored}, meta{phase,scope,rulesExecuted,timestamp}}. Args: scope? (all|cem|skills|tokens|patterns, default: all). Phase 1: 12 JSON comparison rules.',
|
|
681
|
-
inputSchema: {},
|
|
682
|
-
handler: checkDriftHandler,
|
|
683
|
-
},
|
|
684
|
-
],
|
|
685
|
-
};
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* get_skill_manifest plugin tool for wcf-mcp.
|
|
3
|
-
* Returns skill metadata (manifest) from skills-registry v2.
|
|
4
|
-
* Supports metadata-only, full content, and section extraction modes.
|
|
5
|
-
* Plugin Contract v1.0+ (content modes require v1.1+)
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFile } from 'node:fs/promises';
|
|
9
|
-
import { resolve } from 'node:path';
|
|
10
|
-
import {
|
|
11
|
-
REPO_ROOT,
|
|
12
|
-
loadRegistry,
|
|
13
|
-
normalizeSkillEntry,
|
|
14
|
-
buildErrorResponse,
|
|
15
|
-
} from './shared.mjs';
|
|
16
|
-
|
|
17
|
-
/** Maximum safe response size in bytes (95KB) */
|
|
18
|
-
const MAX_SAFE_RESPONSE = 95 * 1024;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Normalize a heading text to a slug for section matching.
|
|
22
|
-
* @param {string} text
|
|
23
|
-
* @returns {string}
|
|
24
|
-
*/
|
|
25
|
-
function normalizeHeading(text) {
|
|
26
|
-
return text
|
|
27
|
-
.trim()
|
|
28
|
-
.toLowerCase()
|
|
29
|
-
.replace(/[^a-z0-9\s_]/g, '')
|
|
30
|
-
.replace(/\s+/g, '_');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Detect sections (H1/H2 headings) from markdown content.
|
|
35
|
-
* @param {string} content
|
|
36
|
-
* @returns {{ sections: Array<{level: number, text: string, normalized: string, line: number}>, lines: string[] }}
|
|
37
|
-
*/
|
|
38
|
-
function parseMarkdownSections(content) {
|
|
39
|
-
const lines = content.split('\n');
|
|
40
|
-
const sections = [];
|
|
41
|
-
for (let i = 0; i < lines.length; i++) {
|
|
42
|
-
const match = lines[i].match(/^(#{1,2})\s+(.+)/);
|
|
43
|
-
if (match) {
|
|
44
|
-
sections.push({
|
|
45
|
-
level: match[1].length,
|
|
46
|
-
text: match[2].trim(),
|
|
47
|
-
normalized: normalizeHeading(match[2]),
|
|
48
|
-
line: i,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return { sections, lines };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Extract a specific section from parsed markdown by normalized heading name.
|
|
57
|
-
* @param {{ sections: Array<{level: number, normalized: string, line: number}>, lines: string[] }} parsed
|
|
58
|
-
* @param {string} sectionName
|
|
59
|
-
* @returns {string|null}
|
|
60
|
-
*/
|
|
61
|
-
function extractSection(parsed, sectionName) {
|
|
62
|
-
const idx = parsed.sections.findIndex((s) =>
|
|
63
|
-
s.normalized === sectionName,
|
|
64
|
-
);
|
|
65
|
-
if (idx === -1) return null;
|
|
66
|
-
|
|
67
|
-
const startLine = parsed.sections[idx].line;
|
|
68
|
-
const startLevel = parsed.sections[idx].level;
|
|
69
|
-
const nextSection = parsed.sections
|
|
70
|
-
.slice(idx + 1)
|
|
71
|
-
.find((s) => s.level <= startLevel);
|
|
72
|
-
const endLine = nextSection ? nextSection.line : parsed.lines.length;
|
|
73
|
-
|
|
74
|
-
return parsed.lines.slice(startLine, endLine).join('\n').trim();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export default {
|
|
78
|
-
name: 'design-system-skills-manifest',
|
|
79
|
-
version: '1.0.0',
|
|
80
|
-
tools: [
|
|
81
|
-
{
|
|
82
|
-
name: 'get_skill_manifest',
|
|
83
|
-
description:
|
|
84
|
-
'Return skill metadata (manifest) from skills-registry v2. When: retrieving full skill details, reading SKILL.md content, extracting specific sections. Returns: {manifest, content?, section_content?, content_size?, available_sections?, truncated?}. After: use skill content to guide implementation. Args: skill_id (required, e.g. "css-writing-rules"), include_content? (boolean, requires Contract v1.1+), section? (overview|workflow|do_dont|references|quick_reference|token_architecture|error_contract|procedure).',
|
|
85
|
-
inputSchema: {},
|
|
86
|
-
async handler(args, { helpers }) {
|
|
87
|
-
// 1. Load registry
|
|
88
|
-
const registry = await loadRegistry();
|
|
89
|
-
if (!registry || !Array.isArray(registry.skills)) {
|
|
90
|
-
return buildErrorResponse(
|
|
91
|
-
'SKILLS_REGISTRY_UNAVAILABLE',
|
|
92
|
-
'Skills registry data not available.',
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// 2. Find skill by name
|
|
97
|
-
const { skill_id, include_content, section } = args;
|
|
98
|
-
const skill = registry.skills.find((s) => s.name === skill_id);
|
|
99
|
-
if (!skill) {
|
|
100
|
-
return buildErrorResponse(
|
|
101
|
-
'SKILL_NOT_FOUND',
|
|
102
|
-
`Skill '${skill_id}' not found in registry.`,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// 3. Build manifest with v2 defaults
|
|
107
|
-
const manifest = normalizeSkillEntry(skill);
|
|
108
|
-
|
|
109
|
-
// 4. Mode 1: Metadata Only
|
|
110
|
-
const wantsContent = include_content === true;
|
|
111
|
-
const wantsSection = typeof section === 'string' && section.length > 0;
|
|
112
|
-
|
|
113
|
-
if (!wantsContent && !wantsSection) {
|
|
114
|
-
return helpers.buildJsonToolResponse({ manifest });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// 5. Modes 2 & 3 require loadTextData (Contract v1.1+)
|
|
118
|
-
if (typeof helpers.loadTextData !== 'function') {
|
|
119
|
-
return buildErrorResponse(
|
|
120
|
-
'CONTRACT_VERSION_ERROR',
|
|
121
|
-
'Content retrieval requires Contract v1.1+. loadTextData helper is not available.',
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 6. Read SKILL.md from disk
|
|
126
|
-
const skillFilePath = resolve(REPO_ROOT, skill.path ?? '', skill.entry ?? 'SKILL.md');
|
|
127
|
-
let fileContent;
|
|
128
|
-
try {
|
|
129
|
-
fileContent = await readFile(skillFilePath, 'utf-8');
|
|
130
|
-
} catch {
|
|
131
|
-
const relPath = `${skill.path ?? ''}/${skill.entry ?? 'SKILL.md'}`;
|
|
132
|
-
return buildErrorResponse(
|
|
133
|
-
'SKILL_CONTENT_NOT_FOUND',
|
|
134
|
-
`SKILL.md not found at ${relPath}`,
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// 7. Parse sections once (reused for detection and extraction)
|
|
139
|
-
const parsed = parseMarkdownSections(fileContent);
|
|
140
|
-
const available_sections = parsed.sections.map((s) => s.normalized);
|
|
141
|
-
|
|
142
|
-
// 8. Build response payload
|
|
143
|
-
const payload = { manifest };
|
|
144
|
-
let totalContentSize = 0;
|
|
145
|
-
|
|
146
|
-
// Mode 2: Full content
|
|
147
|
-
if (wantsContent) {
|
|
148
|
-
const contentBytes = Buffer.byteLength(fileContent, 'utf8');
|
|
149
|
-
totalContentSize += contentBytes;
|
|
150
|
-
payload.content = fileContent;
|
|
151
|
-
payload.content_size = contentBytes;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Mode 3: Section extraction
|
|
155
|
-
if (wantsSection) {
|
|
156
|
-
const sectionContent = extractSection(parsed, section);
|
|
157
|
-
if (sectionContent === null) {
|
|
158
|
-
return buildErrorResponse(
|
|
159
|
-
'SECTION_NOT_FOUND',
|
|
160
|
-
`Section '${section}' not found in ${skill_id} SKILL.md. Available: [${available_sections.join(', ')}]`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
const sectionBytes = Buffer.byteLength(sectionContent, 'utf8');
|
|
164
|
-
totalContentSize += sectionBytes;
|
|
165
|
-
payload.section_content = sectionContent;
|
|
166
|
-
payload.content_size = payload.content_size ?? sectionBytes;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
payload.available_sections = available_sections;
|
|
170
|
-
|
|
171
|
-
// 9. Truncation check
|
|
172
|
-
const responseJson = JSON.stringify(payload);
|
|
173
|
-
const responseBytes = Buffer.byteLength(responseJson, 'utf8');
|
|
174
|
-
if (responseBytes > MAX_SAFE_RESPONSE) {
|
|
175
|
-
if (payload.content) {
|
|
176
|
-
const overhead = responseBytes - totalContentSize;
|
|
177
|
-
const availableBytes = MAX_SAFE_RESPONSE - overhead - 200;
|
|
178
|
-
// Estimate char limit from byte ratio
|
|
179
|
-
const ratio = Math.max(0, availableBytes) / Buffer.byteLength(payload.content, 'utf8');
|
|
180
|
-
const charLimit = Math.floor(payload.content.length * Math.min(1, ratio));
|
|
181
|
-
payload.content = payload.content.slice(0, charLimit);
|
|
182
|
-
payload.content_size = Buffer.byteLength(payload.content, 'utf8');
|
|
183
|
-
}
|
|
184
|
-
payload.truncated = true;
|
|
185
|
-
} else {
|
|
186
|
-
payload.truncated = false;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return helpers.buildJsonToolResponse(payload);
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
],
|
|
193
|
-
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Design System Skills plugin bundle for wcf-mcp.
|
|
3
|
-
* Aggregates list_skills, get_skill_manifest, and check_drift tools.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import listSkillsPlugin from './list-skills.mjs';
|
|
7
|
-
import getSkillManifestPlugin from './get-skill-manifest.mjs';
|
|
8
|
-
import checkDriftPlugin from './check-drift.mjs';
|
|
9
|
-
|
|
10
|
-
const allTools = [
|
|
11
|
-
...listSkillsPlugin.tools,
|
|
12
|
-
...getSkillManifestPlugin.tools,
|
|
13
|
-
...checkDriftPlugin.tools,
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
export default {
|
|
17
|
-
name: 'design-system-skills',
|
|
18
|
-
version: '1.0.0',
|
|
19
|
-
tools: allTools,
|
|
20
|
-
};
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* list_skills plugin tool for wcf-mcp.
|
|
3
|
-
* Lists registered skills from skills-registry.json with filtering support.
|
|
4
|
-
* Plugin Contract v1.0+
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { loadRegistry, normalizeSkillEntry, buildErrorResponse } from './shared.mjs';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Project a full skill entry to a lightweight summary (excludes compat/manifest).
|
|
11
|
-
*/
|
|
12
|
-
function toSummary(skill) {
|
|
13
|
-
const { compat, manifest, ...summary } = normalizeSkillEntry(skill);
|
|
14
|
-
return summary;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export default {
|
|
18
|
-
name: 'design-system-skills-list',
|
|
19
|
-
version: '1.0.0',
|
|
20
|
-
tools: [
|
|
21
|
-
{
|
|
22
|
-
name: 'list_skills',
|
|
23
|
-
description:
|
|
24
|
-
'List registered Claude Code / Cursor / Codex skills from skills-registry.json. When: discovering available skills, checking skill metadata, filtering skills by client/tags. Returns: {total, skills[]} where each skill has name, description, status, clients, tags, version, dependencies, path, entry. After: use get_skill_manifest for full content. Args: client? (claude_code|cursor|codex), tags? (spec|token|audit|drift|workflow, AND logic), status? (active|deprecated|experimental), query? (free-text substring search).',
|
|
25
|
-
inputSchema: {},
|
|
26
|
-
async handler(args = {}, { helpers }) {
|
|
27
|
-
const registry = await loadRegistry();
|
|
28
|
-
if (!registry || !Array.isArray(registry.skills)) {
|
|
29
|
-
return buildErrorResponse(
|
|
30
|
-
'SKILLS_REGISTRY_UNAVAILABLE',
|
|
31
|
-
'Skills registry data not available.',
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const { client, tags, status, query } = args;
|
|
36
|
-
let matched = registry.skills;
|
|
37
|
-
|
|
38
|
-
// Filter by status
|
|
39
|
-
if (status) {
|
|
40
|
-
matched = matched.filter(
|
|
41
|
-
(s) => String(s.status ?? 'active') === status,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Filter by client
|
|
46
|
-
if (client) {
|
|
47
|
-
matched = matched.filter(
|
|
48
|
-
(s) => Array.isArray(s.clients) && s.clients.includes(client),
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Filter by tags (AND logic)
|
|
53
|
-
if (Array.isArray(tags) && tags.length > 0) {
|
|
54
|
-
matched = matched.filter((s) => {
|
|
55
|
-
const skillTags = Array.isArray(s.tags) ? s.tags : [];
|
|
56
|
-
return tags.every((t) => skillTags.includes(t));
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Filter by free-text query
|
|
61
|
-
if (typeof query === 'string' && query.trim() !== '') {
|
|
62
|
-
const q = query.toLowerCase();
|
|
63
|
-
matched = matched.filter((s) => {
|
|
64
|
-
const name = String(s.name ?? '').toLowerCase();
|
|
65
|
-
const desc = String(s.description ?? '').toLowerCase();
|
|
66
|
-
return name.includes(q) || desc.includes(q);
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const skills = matched.map(toSummary);
|
|
71
|
-
return helpers.buildJsonToolResponse({
|
|
72
|
-
total: skills.length,
|
|
73
|
-
skills,
|
|
74
|
-
});
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared utilities for design-system-skills plugin.
|
|
3
|
-
* Single source of truth for registry loading, defaults, and error responses.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readFile } from 'node:fs/promises';
|
|
7
|
-
import { dirname, resolve } from 'node:path';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = dirname(__filename);
|
|
12
|
-
|
|
13
|
-
export const REPO_ROOT = resolve(__dirname, '..', '..', '..', '..');
|
|
14
|
-
export const REGISTRY_PATH = resolve(REPO_ROOT, 'registry', 'skills-registry.json');
|
|
15
|
-
|
|
16
|
-
/** v2 defaults for v1 skill entries */
|
|
17
|
-
export const V2_DEFAULTS = {
|
|
18
|
-
tags: [],
|
|
19
|
-
version: '0.0.0',
|
|
20
|
-
dependencies: [],
|
|
21
|
-
compat: {},
|
|
22
|
-
manifest: {},
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Load skills registry from disk.
|
|
27
|
-
* @returns {Promise<object|null>}
|
|
28
|
-
*/
|
|
29
|
-
export async function loadRegistry() {
|
|
30
|
-
try {
|
|
31
|
-
const raw = await readFile(REGISTRY_PATH, 'utf-8');
|
|
32
|
-
return JSON.parse(raw);
|
|
33
|
-
} catch {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Normalize a raw skill entry from the registry, applying v2 defaults.
|
|
40
|
-
* @param {object} skill - Raw skill entry
|
|
41
|
-
* @returns {object} Normalized skill fields
|
|
42
|
-
*/
|
|
43
|
-
export function normalizeSkillEntry(skill) {
|
|
44
|
-
return {
|
|
45
|
-
name: skill.name,
|
|
46
|
-
description: skill.description ?? '',
|
|
47
|
-
status: skill.status ?? 'active',
|
|
48
|
-
path: skill.path ?? '',
|
|
49
|
-
entry: skill.entry ?? 'SKILL.md',
|
|
50
|
-
clients: Array.isArray(skill.clients) ? skill.clients : [],
|
|
51
|
-
tags: Array.isArray(skill.tags) ? skill.tags : V2_DEFAULTS.tags,
|
|
52
|
-
version: typeof skill.version === 'string' ? skill.version : V2_DEFAULTS.version,
|
|
53
|
-
dependencies: Array.isArray(skill.dependencies) ? skill.dependencies : V2_DEFAULTS.dependencies,
|
|
54
|
-
compat: skill.compat ?? V2_DEFAULTS.compat,
|
|
55
|
-
manifest: skill.manifest ?? V2_DEFAULTS.manifest,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Build an MCP error response.
|
|
61
|
-
* @param {string} code
|
|
62
|
-
* @param {string} message
|
|
63
|
-
* @returns {object}
|
|
64
|
-
*/
|
|
65
|
-
export function buildErrorResponse(code, message) {
|
|
66
|
-
return {
|
|
67
|
-
content: [
|
|
68
|
-
{
|
|
69
|
-
type: 'text',
|
|
70
|
-
text: JSON.stringify({ error: { code, message } }, null, 2),
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
isError: true,
|
|
74
|
-
};
|
|
75
|
-
}
|