@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/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +16 -0
- package/README.md +7 -4
- package/capability-roadmap.json +1 -1
- package/component-decisions.json +1 -1
- package/component-guides.json +42 -42
- package/contract.json +4 -3
- package/package.json +1 -1
- package/semantics-data.json +1 -1
- package/server.js +22 -11
- package/smoke.mjs +12 -0
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.
|
|
4
|
+
"frameworkVersion": "0.12.0",
|
|
5
5
|
"skillVersion": "0.7.0",
|
|
6
|
-
"matchingTag": "v0.
|
|
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": "
|
|
28
|
+
"sourceHash": "270b7d8fd0ec2f1174cba3f2793c061d3b4f494903520e2eaaa7fa640a680859"
|
|
28
29
|
}
|
package/package.json
CHANGED
package/semantics-data.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedBy": "mcp/expressivecss/scripts/sync-guides.mjs",
|
|
4
|
-
"frameworkVersion": "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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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');
|