@monoharada/wcf-mcp 0.6.0 → 0.7.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.
Files changed (3) hide show
  1. package/core.mjs +103 -6
  2. package/package.json +1 -1
  3. package/validator.mjs +54 -25
package/core.mjs CHANGED
@@ -1132,6 +1132,20 @@ export function serializeApi(decl, modulePath, prefix) {
1132
1132
  };
1133
1133
  }
1134
1134
 
1135
+ /**
1136
+ * Generic fallback values for common attributes when CEM default is missing.
1137
+ * Attribute-name-based (not component-specific). `type` is excluded to avoid
1138
+ * conflicts between button (type="button") and input (type="text").
1139
+ * `variant` is also excluded — its valid values differ per component,
1140
+ * so the first enum value is used instead (see generateSnippet).
1141
+ */
1142
+ const SNIPPET_FALLBACK_VALUES = {
1143
+ label: 'ラベル',
1144
+ name: 'field1',
1145
+ value: 'サンプル値',
1146
+ 'support-text': '説明テキスト',
1147
+ };
1148
+
1135
1149
  export function generateSnippet(api, prefix) {
1136
1150
  // Use custom snippet if injected by CEM plugin (e.g. data-* driven components)
1137
1151
  const customSnippet = api.custom?.usageSnippet;
@@ -1174,7 +1188,28 @@ export function generateSnippet(api, prefix) {
1174
1188
  if (isBoolean) {
1175
1189
  lines.push(` ${name}`);
1176
1190
  } else {
1177
- const defaultVal = typeof a.default === 'string' ? a.default.replace(/^['"]|['"]$/g, '') : '';
1191
+ let defaultVal;
1192
+ if (typeof a.default === 'string') {
1193
+ defaultVal = a.default.replace(/^['"]|['"]$/g, '');
1194
+ } else if (SNIPPET_FALLBACK_VALUES[name] !== undefined) {
1195
+ defaultVal = SNIPPET_FALLBACK_VALUES[name];
1196
+ } else {
1197
+ // For enum types, use the first enum value as fallback
1198
+ const enumMatch = t.match(/^'([^']+)'/);
1199
+ if (enumMatch) {
1200
+ defaultVal = enumMatch[1];
1201
+ } else {
1202
+ // Fallback: extract first value from description pattern like "(solid | outlined | text)"
1203
+ const desc = String(a.description ?? '');
1204
+ const descEnum = desc.match(/\(([^)]+)\)/);
1205
+ if (descEnum) {
1206
+ const first = descEnum[1].split(/\s*[||]\s*/)[0]?.trim();
1207
+ defaultVal = first || '';
1208
+ } else {
1209
+ defaultVal = '';
1210
+ }
1211
+ }
1212
+ }
1178
1213
  lines.push(` ${name}="${defaultVal}"`);
1179
1214
  }
1180
1215
  if (lines.length >= 4) break;
@@ -1264,7 +1299,36 @@ export function resolveComponentClosure({ installRegistry }, componentIds) {
1264
1299
  return [...out];
1265
1300
  }
1266
1301
 
1267
- export function buildComponentSummaries(indexes, { category, query, limit, offset, prefix } = {}) {
1302
+ /**
1303
+ * Build a frequency map: componentId → count of patterns that require it.
1304
+ * Counts from pattern-registry.json `requires` arrays only.
1305
+ */
1306
+ export function buildPatternFrequencyMap(patterns) {
1307
+ const freq = new Map();
1308
+ if (!patterns || typeof patterns !== 'object') return freq;
1309
+ for (const pat of Object.values(patterns)) {
1310
+ const requires = Array.isArray(pat?.requires) ? pat.requires : [];
1311
+ for (const id of requires) {
1312
+ const key = String(id ?? '').trim();
1313
+ if (key) freq.set(key, (freq.get(key) ?? 0) + 1);
1314
+ }
1315
+ }
1316
+ return freq;
1317
+ }
1318
+
1319
+ /**
1320
+ * Convert a tag from the current prefix to canonical prefix using string ops
1321
+ * (no regex, safe for arbitrary prefix values).
1322
+ */
1323
+ function toCanonicalTag(tag, currentPrefix) {
1324
+ const cp = `${currentPrefix}-`;
1325
+ if (tag.startsWith(cp)) {
1326
+ return `${CANONICAL_PREFIX}-${tag.slice(cp.length)}`;
1327
+ }
1328
+ return tag;
1329
+ }
1330
+
1331
+ export function buildComponentSummaries(indexes, { category, query, limit, offset, prefix, patternId, sort, patterns, installRegistry, patternFrequency } = {}) {
1268
1332
  const p = normalizePrefix(prefix);
1269
1333
  const q = typeof query === 'string' ? query.trim().toLowerCase() : '';
1270
1334
  const limitExplicit = Number.isInteger(limit);
@@ -1279,6 +1343,24 @@ export function buildComponentSummaries(indexes, { category, query, limit, offse
1279
1343
  modulePath,
1280
1344
  }));
1281
1345
 
1346
+ // patternId filter: restrict to components required by a specific pattern
1347
+ if (typeof patternId === 'string' && patternId.trim()) {
1348
+ const pats = patterns && typeof patterns === 'object' ? patterns : {};
1349
+ const pat = pats[patternId.trim()];
1350
+ if (pat && Array.isArray(pat.requires)) {
1351
+ const requiredIds = new Set(pat.requires.map((r) => String(r ?? '').trim()).filter(Boolean));
1352
+ const tags = installRegistry?.tags && typeof installRegistry.tags === 'object' ? installRegistry.tags : {};
1353
+ items = items.filter((item) => {
1354
+ // Map tagName to componentId via install registry
1355
+ const canonicalTag = toCanonicalTag(item.tagName, p);
1356
+ const componentId = tags[canonicalTag];
1357
+ return componentId && requiredIds.has(componentId);
1358
+ });
1359
+ } else {
1360
+ items = [];
1361
+ }
1362
+ }
1363
+
1282
1364
  if (category) {
1283
1365
  items = items.filter((item) => item.category === category);
1284
1366
  }
@@ -1296,6 +1378,18 @@ export function buildComponentSummaries(indexes, { category, query, limit, offse
1296
1378
  });
1297
1379
  }
1298
1380
 
1381
+ // frequency sort: order by pattern usage count descending
1382
+ if (sort === 'frequency') {
1383
+ const freq = patternFrequency instanceof Map ? patternFrequency : new Map();
1384
+ const tags = installRegistry?.tags && typeof installRegistry.tags === 'object' ? installRegistry.tags : {};
1385
+ items = items.map((item) => {
1386
+ const canonicalTag = toCanonicalTag(item.tagName, p);
1387
+ const componentId = tags[canonicalTag] ?? '';
1388
+ return { ...item, frequency: freq.get(componentId) ?? 0 };
1389
+ });
1390
+ items.sort((a, b) => b.frequency - a.frequency);
1391
+ }
1392
+
1299
1393
  const total = items.length;
1300
1394
  const paged = items.slice(pageOffset, pageOffset + pageSize);
1301
1395
 
@@ -1886,6 +1980,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
1886
1980
  const patternRegistry = await loadJson('pattern-registry.json');
1887
1981
  const { patterns } = loadPatternRegistryShape(patternRegistry);
1888
1982
  const relatedComponentMap = buildRelatedComponentMap(installRegistry, patterns);
1983
+ const patternFrequency = buildPatternFrequencyMap(patterns);
1889
1984
 
1890
1985
  // Load optional data files (design tokens, guidelines index)
1891
1986
  let designTokensData = null;
@@ -1916,7 +2011,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
1916
2011
 
1917
2012
  const server = new McpServer({
1918
2013
  name: 'web-components-factory-design-system',
1919
- version: '0.6.0',
2014
+ version: '0.7.0',
1920
2015
  });
1921
2016
 
1922
2017
  server.registerPrompt(
@@ -2066,7 +2161,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2066
2161
 
2067
2162
  const overview = {
2068
2163
  name: 'DADS Web Components (wcf)',
2069
- version: '0.6.0',
2164
+ version: '0.7.0',
2070
2165
  prefix: detectedPrefix,
2071
2166
  totalComponents: indexes.decls.length,
2072
2167
  componentsByCategory: categoryCount,
@@ -2221,10 +2316,12 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
2221
2316
  limit: z.number().int().min(1).max(200).optional().describe('Maximum items to return (default: 20; set 200 for all results)'),
2222
2317
  offset: z.number().int().min(0).optional().describe('Pagination offset (default: 0)'),
2223
2318
  prefix: z.string().optional(),
2319
+ patternId: z.string().optional().describe('Filter to components required by this pattern'),
2320
+ sort: z.enum(['default', 'frequency']).optional().describe('Sort order: "default" (CEM declaration order) or "frequency" (pattern usage count, descending)'),
2224
2321
  },
2225
2322
  },
2226
- async ({ category, query, limit, offset, prefix }) => {
2227
- const page = buildComponentSummaries(indexes, { category, query, limit, offset, prefix });
2323
+ async ({ category, query, limit, offset, prefix, patternId, sort }) => {
2324
+ const page = buildComponentSummaries(indexes, { category, query, limit, offset, prefix, patternId, sort, patterns, installRegistry, patternFrequency });
2228
2325
  const payload = {
2229
2326
  items: page.items,
2230
2327
  total: page.total,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoharada/wcf-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.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": {
package/validator.mjs CHANGED
@@ -676,9 +676,18 @@ const PARENT_CHILD_CONSTRAINTS = new Map([
676
676
  ['dads-menu-list-item', 'dads-menu-list'],
677
677
  ]);
678
678
 
679
+ /**
680
+ * HTML void elements that never have a closing tag.
681
+ */
682
+ const HTML_VOID_ELEMENTS = new Set([
683
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
684
+ 'link', 'meta', 'source', 'track', 'wbr',
685
+ ]);
686
+
679
687
  /**
680
688
  * Detect orphaned child components (child appears without expected parent).
681
- * Uses regex/text scan (DIG-13) lower confidence, severity: warning.
689
+ * Uses a lightweight tag-stack approach: only prefix-matching tags (e.g. dads-*)
690
+ * are tracked on the stack; all other HTML elements are ignored.
682
691
  * @param {{
683
692
  * filePath?: string;
684
693
  * text: string;
@@ -697,7 +706,7 @@ export function detectOrphanedChildComponents({
697
706
  const p = prefix.toLowerCase();
698
707
  const canonicalPrefix = 'dads';
699
708
 
700
- // Build prefix-aware constraint map
709
+ // Build prefix-aware constraint map (child → parent)
701
710
  const constraints = new Map();
702
711
  for (const [child, parent] of PARENT_CHILD_CONSTRAINTS.entries()) {
703
712
  const mappedChild = p !== canonicalPrefix ? child.replace(canonicalPrefix, p) : child;
@@ -705,36 +714,56 @@ export function detectOrphanedChildComponents({
705
714
  constraints.set(mappedChild, mappedParent);
706
715
  }
707
716
 
708
- const textLower = text.toLowerCase();
717
+ const prefixDash = `${p}-`;
718
+ // Stack of currently open prefix-matching tags
719
+ const stack = [];
709
720
 
710
- const tagRe = /<([a-z][a-z0-9-]*)\b([^<>]*?)>/gi;
721
+ // Regex matches opening tags, closing tags, and self-closing tags
722
+ const tagRe = /<\/?([a-z][a-z0-9-]*)\b[^<>]*?\/?>/gi;
711
723
  let m;
712
724
 
713
725
  while ((m = tagRe.exec(text))) {
726
+ const fullMatch = m[0];
714
727
  const tag = String(m[1] ?? '').toLowerCase();
715
- const expectedParent = constraints.get(tag);
716
- if (!expectedParent) continue;
717
-
718
- // Check if the expected parent tag appears before this child in the text
719
- const precedingText = textLower.slice(0, m.index);
720
- const parentOpenPattern = `<${expectedParent}`;
721
- const parentClosePattern = `</${expectedParent}`;
728
+ const isClosing = fullMatch.startsWith('</');
729
+ const isSelfClosing = fullMatch.endsWith('/>');
730
+
731
+ // Only track prefix-matching tags on the stack
732
+ if (!tag.startsWith(prefixDash)) continue;
733
+
734
+ if (isClosing) {
735
+ // Pop the matching opening tag from the stack (search from top)
736
+ for (let i = stack.length - 1; i >= 0; i--) {
737
+ if (stack[i] === tag) {
738
+ stack.splice(i, 1);
739
+ break;
740
+ }
741
+ }
742
+ continue;
743
+ }
722
744
 
723
- const lastParentOpen = precedingText.lastIndexOf(parentOpenPattern);
724
- const lastParentClose = precedingText.lastIndexOf(parentClosePattern);
745
+ // Check if this is a child that needs a parent
746
+ const expectedParent = constraints.get(tag);
747
+ if (expectedParent) {
748
+ const hasParent = stack.includes(expectedParent);
749
+ if (!hasParent) {
750
+ const tagOffset = m.index + 1;
751
+ const range = makeRange(lineStarts, tagOffset, tagOffset + tag.length);
752
+ diagnostics.push({
753
+ file: filePath,
754
+ range,
755
+ severity,
756
+ code: 'orphanedChildComponent',
757
+ message: `<${tag}> should be a child of <${expectedParent}>.`,
758
+ tagName: tag,
759
+ hint: `Wrap <${tag}> inside <${expectedParent}>...</${expectedParent}>.`,
760
+ });
761
+ }
762
+ }
725
763
 
726
- if (lastParentOpen === -1 || lastParentClose > lastParentOpen) {
727
- const tagOffset = m.index + 1;
728
- const range = makeRange(lineStarts, tagOffset, tagOffset + tag.length);
729
- diagnostics.push({
730
- file: filePath,
731
- range,
732
- severity,
733
- code: 'orphanedChildComponent',
734
- message: `<${tag}> should be a child of <${expectedParent}>.`,
735
- tagName: tag,
736
- hint: `Wrap <${tag}> inside <${expectedParent}>...</${expectedParent}>.`,
737
- });
764
+ // Push opening tag to stack (skip void elements and self-closing)
765
+ if (!isSelfClosing && !HTML_VOID_ELEMENTS.has(tag)) {
766
+ stack.push(tag);
738
767
  }
739
768
  }
740
769