@expressivecss/mcp-server 0.2.1 → 0.2.2

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/contract.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generatedBy": "scripts/gen-expressivecss-skill.mjs",
4
- "frameworkVersion": "0.11.0",
4
+ "frameworkVersion": "0.12.0",
5
5
  "skillVersion": "0.7.0",
6
- "matchingTag": "v0.11.0",
6
+ "matchingTag": "v0.12.0",
7
7
  "releaseTags": [
8
8
  "v0.10.0",
9
9
  "v0.10.1",
10
10
  "v0.11.0",
11
+ "v0.12.0",
11
12
  "v0.5.0",
12
13
  "v0.6.0",
13
14
  "v0.7.0",
@@ -24,5 +25,5 @@
24
25
  "package.json",
25
26
  "CHANGELOG.md"
26
27
  ],
27
- "sourceHash": "425faa2b1b68a0dd95d0718e81b94405ff8ab3b34a2c95e5cfc1b71ef2894a65"
28
+ "sourceHash": "270b7d8fd0ec2f1174cba3f2793c061d3b4f494903520e2eaaa7fa640a680859"
28
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expressivecss/mcp-server",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "ExpressiveCSS MCP server with Design→QA workflow tools.",
5
5
  "homepage": "https://www.expressivecss.com",
6
6
  "repository": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generatedBy": "mcp/expressivecss/scripts/sync-guides.mjs",
4
- "frameworkVersion": "0.11.0",
4
+ "frameworkVersion": "0.12.0",
5
5
  "semantics": {
6
6
  "about": "Normative HTML semantics for ExpressiveCSS. This data file is the single source of truth: SEMANTICS.md is generated from it by scripts/gen-semantics.mjs, and tests/semantics.test.js enforces it. One row per roster entry, each stating its kind - component, foundation or behavior, as CONTEXT.md defines them. CONTEXT.md defines the rest of the vocabulary.",
7
7
  "scope": "Element choice, landmark structure, and the ARIA that element choice implies. Keyboard interaction, focus management and colour contrast are deliberately out of scope.",
package/server.js CHANGED
@@ -24,6 +24,9 @@ const DEFAULT_QA_MAX_TOTAL_MB = 16;
24
24
  const MAX_STATIC_ISSUES = 200;
25
25
  const MAX_STATIC_ISSUES_PER_REQUEST = 1_000;
26
26
  const MAX_STATIC_MARKUP_DELIMITERS = 4_000;
27
+ // Source locations make parse5 copy a parent's children for each text run, so the cost grows with
28
+ // characters times tags. Past this product, semantics findings report 1:1 to stay within the budget.
29
+ const MAX_LOCATED_MARKUP_WORK = 100_000_000;
27
30
  const STATIC_INSPECTION_TIMEOUT_MS = 5_000;
28
31
  const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
29
32
  const MAX_PROJECT_ROOT_CHARS = 4_096;
@@ -96,19 +99,19 @@ const LegacyPatternList = [
96
99
  {
97
100
  id: 'legacy-nav-wrapper',
98
101
  severity: 'high',
99
- description: '`.nav-wrapper` is legacy Materialize structure; use ExpressiveCSS nav components directly.',
102
+ description: '`.nav-wrapper` is retired markup structure; use ExpressiveCSS nav components directly.',
100
103
  pattern: /\bnav-wrapper\b/g,
101
104
  },
102
105
  {
103
106
  id: 'legacy-brand-logo',
104
107
  severity: 'medium',
105
- description: '`.brand-logo` is Materialize-era naming; prefer native layout semantics in ExpressiveCSS pages.',
108
+ description: '`.brand-logo` is retired naming; prefer native layout semantics in ExpressiveCSS pages.',
106
109
  pattern: /\bbrand-logo\b/g,
107
110
  },
108
111
  {
109
112
  id: 'legacy-lever',
110
113
  severity: 'medium',
111
- description: '`.lever` is a Materialize switch token, replace with Expressive switches per component guide.',
114
+ description: '`.lever` is a retired switch token, replace with Expressive switches per component guide.',
112
115
  pattern: /\blever\b/g,
113
116
  },
114
117
  {
@@ -879,7 +882,15 @@ function inspectSemanticRules(snippet, maxIssues = MAX_STATIC_ISSUES, deadline =
879
882
  const { semantics, frameworkVersion } = loadSemanticsContract();
880
883
  if (maxIssues <= 0) return markInspectionTruncated([], 'issue limit reached');
881
884
  if (markupStructureExceedsLimit(snippet)) return markInspectionTruncated([], 'markup structure limit reached');
882
- const { document } = new JSDOM(`<!doctype html><body>${snippet}</body>`).window;
885
+ const prefix = '<!doctype html><body>';
886
+ const includeNodeLocations = snippet.length * (snippet.split('<').length - 1) <= MAX_LOCATED_MARKUP_WORK;
887
+ const dom = new JSDOM(`${prefix}${snippet}</body>`, { includeNodeLocations });
888
+ const { document } = dom.window;
889
+ // Elements the parser creates without a start tag have no source offset, so they report the start.
890
+ const locate = (element) => {
891
+ const offset = includeNodeLocations ? dom.nodeLocation(element)?.startOffset ?? -1 : -1;
892
+ return offset >= prefix.length ? lineForMatch(snippet, offset - prefix.length) : { line: 1, column: 1 };
893
+ };
883
894
  const issues = [];
884
895
 
885
896
  for (const rule of enforcedSemanticRules(semantics)) {
@@ -890,7 +901,7 @@ function inspectSemanticRules(snippet, maxIssues = MAX_STATIC_ISSUES, deadline =
890
901
  for (const element of hits) {
891
902
  const stop = inspectionStopReason(issues, maxIssues, deadline);
892
903
  if (stop) return markInspectionTruncated(issues, stop);
893
- issues.push(semanticIssue(rule, element, frameworkVersion));
904
+ issues.push(semanticIssue(rule, element, frameworkVersion, locate(element)));
894
905
  }
895
906
  } else if (rule.kind === 'require-attr') {
896
907
  for (const element of hits) {
@@ -899,7 +910,7 @@ function inspectSemanticRules(snippet, maxIssues = MAX_STATIC_ISSUES, deadline =
899
910
  const value = element.getAttribute(rule.attr);
900
911
  const valid = rule.equals ? value === rule.equals : value !== null && value !== '';
901
912
  if (!valid) {
902
- issues.push(semanticIssue(rule, element, frameworkVersion));
913
+ issues.push(semanticIssue(rule, element, frameworkVersion, locate(element)));
903
914
  }
904
915
  }
905
916
  } else if (rule.kind === 'require-accessible-name') {
@@ -907,7 +918,7 @@ function inspectSemanticRules(snippet, maxIssues = MAX_STATIC_ISSUES, deadline =
907
918
  const stop = inspectionStopReason(issues, maxIssues, deadline);
908
919
  if (stop) return markInspectionTruncated(issues, stop);
909
920
  if (!accessibleName(element, document)) {
910
- issues.push(semanticIssue(rule, element, frameworkVersion));
921
+ issues.push(semanticIssue(rule, element, frameworkVersion, locate(element)));
911
922
  }
912
923
  }
913
924
  }
@@ -928,7 +939,7 @@ function inspectSemanticRules(snippet, maxIssues = MAX_STATIC_ISSUES, deadline =
928
939
  component: 'landmarks',
929
940
  frameworkVersion,
930
941
  rule: `Navigation landmarks on one page need distinct names; "${name}" is repeated.`,
931
- location: { line: 1, column: 1 },
942
+ location: locate(nav),
932
943
  snippet: nav.outerHTML.slice(0, 140),
933
944
  });
934
945
  }
@@ -938,14 +949,14 @@ function inspectSemanticRules(snippet, maxIssues = MAX_STATIC_ISSUES, deadline =
938
949
  return issues;
939
950
  }
940
951
 
941
- function semanticIssue(rule, element, frameworkVersion) {
952
+ function semanticIssue(rule, element, frameworkVersion, location) {
942
953
  return {
943
954
  id: rule.id,
944
955
  severity: 'high',
945
956
  component: rule.component,
946
957
  frameworkVersion,
947
958
  rule: rule.message,
948
- location: { line: 1, column: 1 },
959
+ location,
949
960
  snippet: element.outerHTML.slice(0, 140),
950
961
  };
951
962
  }
@@ -1920,7 +1931,7 @@ async function rulesEnforcerHandler(args) {
1920
1931
  ],
1921
1932
  guidance: {
1922
1933
  alwaysPrefer: 'Read component guides before adding structure.',
1923
- compatibility: 'Avoid Materialize-era selectors and prefer source-of-truth component contracts.',
1934
+ compatibility: 'Avoid retired selectors and prefer source-of-truth component contracts.',
1924
1935
  },
1925
1936
  },
1926
1937
  workflowId,
package/smoke.mjs CHANGED
@@ -248,6 +248,18 @@ const transport = new StdioClientTransport({
248
248
  assert.equal(failing.status, 1, failing.stderr);
249
249
  assert.match(failing.stdout, /navigation-bar-marks-current/u);
250
250
  assert.match(failing.stdout, /retired-markup\.html:1:\d+ legacy-input-field/u);
251
+ // Semantics findings report the element's line and column, also in JSX after lint rewrites className.
252
+ const locatedMarkup = path.join(outsideDir, 'located.jsx');
253
+ await writeFile(locatedMarkup, 'export const Nav = () => (\n <>\n <nav className="navigation-bar"><a href="/">Home</a></nav>\n <nav aria-label="Main"><a href="/">Home</a></nav>\n <nav aria-label="Main"><a href="/a">A</a></nav>\n </>\n);\n');
254
+ const located = lint([locatedMarkup]);
255
+ assert.match(located.stdout, /located\.jsx:3:5 nav-needs-label/u);
256
+ assert.match(located.stdout, /located\.jsx:5:5 duplicate-navigation-landmark-name/u);
257
+ // Large files within the structure limit still finish inside the inspection budget.
258
+ const longMarkup = path.join(outsideDir, 'long.html');
259
+ await writeFile(longMarkup, `<nav><a href="/">Home</a></nav>${'<br>'.repeat(3_990)}${'x\n'.repeat(235_000)}`);
260
+ const long = lint([longMarkup]);
261
+ assert.match(long.stdout, /long\.html:1:1 nav-needs-label/u);
262
+ assert.doesNotMatch(long.stdout, /inspection-truncated/u);
251
263
  assert.equal(lint([outsideFile]).status, 0);
252
264
  // Files the bounded reader refuses, and inspections that stop early, fail instead of passing silently.
253
265
  const linkedMarkup = path.join(outsideDir, 'linked.html');