@fragments-sdk/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 +2 -0
- package/dist/bin.js +4 -3
- package/dist/bin.js.map +1 -1
- package/dist/chunk-7D4SUZUM.js +38 -0
- package/dist/{chunk-6JMX4AMO.js → chunk-YSNIGHNU.js} +952 -177
- package/dist/chunk-YSNIGHNU.js.map +1 -0
- package/dist/{constants-YXOTMY3I.js → constants-BLN4SSNH.js} +2 -1
- package/dist/dist-BDWAHJ4K.js +60258 -0
- package/dist/dist-BDWAHJ4K.js.map +1 -0
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/init.js +2 -0
- package/dist/init.js.map +1 -1
- package/dist/{rules-CKBRD3UL.js → rules-TN4KHFFG.js} +2 -1
- package/dist/rules-TN4KHFFG.js.map +1 -0
- package/dist/{sass.node-4XJK6YBF-2NJM7G64.js → sass.node-4XJK6YBF-CPK77BO6.js} +2 -1
- package/dist/{sass.node-4XJK6YBF-2NJM7G64.js.map → sass.node-4XJK6YBF-CPK77BO6.js.map} +1 -1
- package/dist/server.js +2 -1
- package/package.json +5 -6
- package/dist/chunk-6JMX4AMO.js.map +0 -1
- package/dist/dist-V7D67NXS.js +0 -1093
- package/dist/dist-V7D67NXS.js.map +0 -1
- /package/dist/{constants-YXOTMY3I.js.map → chunk-7D4SUZUM.js.map} +0 -0
- /package/dist/{rules-CKBRD3UL.js.map → constants-BLN4SSNH.js.map} +0 -0
|
@@ -470,14 +470,14 @@ function reciprocalRankFusion(resultSets, k = 60) {
|
|
|
470
470
|
const scoreMap = /* @__PURE__ */ new Map();
|
|
471
471
|
for (const { results } of resultSets) {
|
|
472
472
|
for (let rank = 0; rank < results.length; rank++) {
|
|
473
|
-
const
|
|
474
|
-
const key = `${
|
|
473
|
+
const result2 = results[rank];
|
|
474
|
+
const key = `${result2.kind}:${result2.name}`;
|
|
475
475
|
const rrfScore = 1 / (k + rank + 1);
|
|
476
476
|
const existing = scoreMap.get(key);
|
|
477
477
|
if (existing) {
|
|
478
478
|
existing.score += rrfScore;
|
|
479
479
|
} else {
|
|
480
|
-
scoreMap.set(key, { score: rrfScore, kind:
|
|
480
|
+
scoreMap.set(key, { score: rrfScore, kind: result2.kind, name: result2.name });
|
|
481
481
|
}
|
|
482
482
|
}
|
|
483
483
|
}
|
|
@@ -521,8 +521,8 @@ async function hybridSearch(query, data, limit = 10, kind, apiKey) {
|
|
|
521
521
|
const engine = new ComponentGraphEngine2(graph);
|
|
522
522
|
const topComponents = [...keywordResults, ...vectorResults].filter((r) => r.kind === "component").slice(0, 5);
|
|
523
523
|
const neighborSet = /* @__PURE__ */ new Set();
|
|
524
|
-
for (const
|
|
525
|
-
const neighbors = engine.neighbors(
|
|
524
|
+
for (const result2 of topComponents) {
|
|
525
|
+
const neighbors = engine.neighbors(result2.name, 1);
|
|
526
526
|
for (const n of neighbors.neighbors) {
|
|
527
527
|
if (!neighborSet.has(n.component)) {
|
|
528
528
|
neighborSet.add(n.component);
|
|
@@ -609,10 +609,10 @@ function buildBlockComponentFrequency(blocks) {
|
|
|
609
609
|
return freq;
|
|
610
610
|
}
|
|
611
611
|
function boostByBlockFrequency(results, freq) {
|
|
612
|
-
for (const
|
|
613
|
-
const count = freq.get(
|
|
612
|
+
for (const result2 of results) {
|
|
613
|
+
const count = freq.get(result2.name.toLowerCase()) ?? 0;
|
|
614
614
|
if (count > 0) {
|
|
615
|
-
|
|
615
|
+
result2.score += count * BLOCK_BOOST_PER_OCCURRENCE;
|
|
616
616
|
}
|
|
617
617
|
}
|
|
618
618
|
results.sort((a, b) => b.score - a.score);
|
|
@@ -843,7 +843,7 @@ var discoverHandler = async (args, ctx) => {
|
|
|
843
843
|
ctx.indexes.componentIndex ?? void 0
|
|
844
844
|
);
|
|
845
845
|
const allowedNames = new Set(components2.map((component) => component.name));
|
|
846
|
-
const sortedNames = scored.filter((
|
|
846
|
+
const sortedNames = scored.filter((result2) => allowedNames.has(result2.name)).map((result2) => result2.name.toLowerCase());
|
|
847
847
|
components2 = components2.filter((component) => sortedNames.includes(component.name.toLowerCase())).sort(
|
|
848
848
|
(a, b) => sortedNames.indexOf(a.name.toLowerCase()) - sortedNames.indexOf(b.name.toLowerCase())
|
|
849
849
|
);
|
|
@@ -898,12 +898,12 @@ var discoverHandler = async (args, ctx) => {
|
|
|
898
898
|
"component",
|
|
899
899
|
ctx.config.searchApiKey
|
|
900
900
|
);
|
|
901
|
-
const filteredSearchResults = searchResults.filter((
|
|
902
|
-
const component = componentsByName.get(
|
|
901
|
+
const filteredSearchResults = searchResults.filter((result2) => {
|
|
902
|
+
const component = componentsByName.get(result2.name.toLowerCase());
|
|
903
903
|
if (!component) return false;
|
|
904
904
|
if (category && !categoryMatches(component.category, category)) return false;
|
|
905
905
|
if (status && component.status !== status) return false;
|
|
906
|
-
|
|
906
|
+
result2.score += getRankingBonus(component);
|
|
907
907
|
return true;
|
|
908
908
|
});
|
|
909
909
|
const blockMatches = keywordScoreBlocks(
|
|
@@ -919,15 +919,15 @@ var discoverHandler = async (args, ctx) => {
|
|
|
919
919
|
boostByBlockFrequency(filteredSearchResults, blockFreq);
|
|
920
920
|
}
|
|
921
921
|
const maxScore = filteredSearchResults.length > 0 ? filteredSearchResults[0].score : 0;
|
|
922
|
-
const scored = filteredSearchResults.map((
|
|
923
|
-
const component = componentsByName.get(
|
|
922
|
+
const scored = filteredSearchResults.map((result2) => {
|
|
923
|
+
const component = componentsByName.get(result2.name.toLowerCase());
|
|
924
924
|
if (!component) return null;
|
|
925
925
|
return {
|
|
926
926
|
component: component.name,
|
|
927
927
|
category: component.category,
|
|
928
928
|
description: component.description,
|
|
929
|
-
confidence: assignConfidence(
|
|
930
|
-
reasons: [`Matched via hybrid search (score: ${
|
|
929
|
+
confidence: assignConfidence(result2.score, maxScore),
|
|
930
|
+
reasons: [`Matched via hybrid search (score: ${result2.score.toFixed(4)})`],
|
|
931
931
|
usage: {
|
|
932
932
|
when: getGuidanceWhen(component).slice(0, 3),
|
|
933
933
|
whenNot: getGuidanceWhenNot(component).slice(0, 2)
|
|
@@ -1009,13 +1009,13 @@ var discoverHandler = async (args, ctx) => {
|
|
|
1009
1009
|
]);
|
|
1010
1010
|
const topBlockScore = blockSearchResults.length > 0 ? blockSearchResults[0].score : 0;
|
|
1011
1011
|
const relevantBlockResults = blockSearchResults.filter(
|
|
1012
|
-
(
|
|
1012
|
+
(result2) => result2.score >= topBlockScore * 0.3
|
|
1013
1013
|
);
|
|
1014
1014
|
if (relevantBlockResults.length > 0) {
|
|
1015
1015
|
fullBlocks = (await Promise.all(
|
|
1016
|
-
relevantBlockResults.slice(0, 5).map(async (
|
|
1016
|
+
relevantBlockResults.slice(0, 5).map(async (result2) => {
|
|
1017
1017
|
const block = allBlocks.find(
|
|
1018
|
-
(entry) => entry.name.toLowerCase() ===
|
|
1018
|
+
(entry) => entry.name.toLowerCase() === result2.name.toLowerCase()
|
|
1019
1019
|
);
|
|
1020
1020
|
if (!block) return null;
|
|
1021
1021
|
const imports = await buildImportStatements(
|
|
@@ -1043,11 +1043,11 @@ var discoverHandler = async (args, ctx) => {
|
|
|
1043
1043
|
tokensByName.set(token.name, cat);
|
|
1044
1044
|
}
|
|
1045
1045
|
}
|
|
1046
|
-
for (const
|
|
1047
|
-
const cat = tokensByName.get(
|
|
1046
|
+
for (const result2 of tokenSearchResults) {
|
|
1047
|
+
const cat = tokensByName.get(result2.name);
|
|
1048
1048
|
if (cat) {
|
|
1049
1049
|
if (!fullTokens[cat]) fullTokens[cat] = [];
|
|
1050
|
-
fullTokens[cat].push(
|
|
1050
|
+
fullTokens[cat].push(result2.name);
|
|
1051
1051
|
}
|
|
1052
1052
|
}
|
|
1053
1053
|
if (Object.keys(fullTokens).length === 0) fullTokens = void 0;
|
|
@@ -1172,14 +1172,14 @@ var discoverHandler = async (args, ctx) => {
|
|
|
1172
1172
|
search2,
|
|
1173
1173
|
filteredComponents,
|
|
1174
1174
|
ctx.indexes.componentIndex ?? void 0
|
|
1175
|
-
).map((
|
|
1175
|
+
).map((result2) => {
|
|
1176
1176
|
const component = filteredComponents.find(
|
|
1177
|
-
(entry) => entry.name.toLowerCase() ===
|
|
1177
|
+
(entry) => entry.name.toLowerCase() === result2.name.toLowerCase()
|
|
1178
1178
|
);
|
|
1179
1179
|
if (!component) return null;
|
|
1180
1180
|
return {
|
|
1181
1181
|
component,
|
|
1182
|
-
score:
|
|
1182
|
+
score: result2.score + getRankingBonus(component)
|
|
1183
1183
|
};
|
|
1184
1184
|
}).filter(Boolean);
|
|
1185
1185
|
filteredComponents = scored.sort((a, b) => b.score - a.score).map((entry) => entry.component);
|
|
@@ -1248,11 +1248,11 @@ function projectFields(obj, fields) {
|
|
|
1248
1248
|
if (!fields || fields.length === 0) {
|
|
1249
1249
|
return obj;
|
|
1250
1250
|
}
|
|
1251
|
-
const
|
|
1251
|
+
const result2 = {};
|
|
1252
1252
|
for (const field of fields) {
|
|
1253
1253
|
const parts = field.split(".");
|
|
1254
1254
|
let source = obj;
|
|
1255
|
-
let target =
|
|
1255
|
+
let target = result2;
|
|
1256
1256
|
for (let i = 0; i < parts.length; i++) {
|
|
1257
1257
|
const part = parts[i];
|
|
1258
1258
|
const isLast = i === parts.length - 1;
|
|
@@ -1272,7 +1272,7 @@ function projectFields(obj, fields) {
|
|
|
1272
1272
|
}
|
|
1273
1273
|
}
|
|
1274
1274
|
}
|
|
1275
|
-
return
|
|
1275
|
+
return result2;
|
|
1276
1276
|
}
|
|
1277
1277
|
|
|
1278
1278
|
// src/tools/inspect.ts
|
|
@@ -1441,23 +1441,23 @@ var inspectHandler = async (args, ctx) => {
|
|
|
1441
1441
|
if (aliasMap[parts[0]]) parts[0] = aliasMap[parts[0]];
|
|
1442
1442
|
return parts.join(".");
|
|
1443
1443
|
});
|
|
1444
|
-
let
|
|
1444
|
+
let result2;
|
|
1445
1445
|
if (verbosity === "compact" && !resolvedFields?.length) {
|
|
1446
|
-
|
|
1446
|
+
result2 = {
|
|
1447
1447
|
meta: fullResult.meta,
|
|
1448
1448
|
propsSummary: component.propsSummary,
|
|
1449
1449
|
metadata: component.metadata
|
|
1450
1450
|
};
|
|
1451
1451
|
} else if (verbosity === "full") {
|
|
1452
|
-
|
|
1452
|
+
result2 = resolvedFields && resolvedFields.length > 0 ? projectFields(fullResult, resolvedFields) : fullResult;
|
|
1453
1453
|
} else if (resolvedFields && resolvedFields.length > 0) {
|
|
1454
|
-
|
|
1454
|
+
result2 = projectFields(fullResult, resolvedFields);
|
|
1455
1455
|
} else {
|
|
1456
1456
|
const { source: _source, ...withoutSource } = fullResult;
|
|
1457
|
-
|
|
1457
|
+
result2 = withoutSource;
|
|
1458
1458
|
}
|
|
1459
1459
|
return {
|
|
1460
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
1460
|
+
content: [{ type: "text", text: JSON.stringify(result2) }]
|
|
1461
1461
|
};
|
|
1462
1462
|
};
|
|
1463
1463
|
|
|
@@ -1876,45 +1876,45 @@ var renderHandler = async (args, ctx) => {
|
|
|
1876
1876
|
}
|
|
1877
1877
|
if (figmaUrl) {
|
|
1878
1878
|
try {
|
|
1879
|
-
const
|
|
1879
|
+
const result2 = await compareComponent(viewerUrl, {
|
|
1880
1880
|
component: componentName,
|
|
1881
1881
|
variant: variantName,
|
|
1882
1882
|
props,
|
|
1883
1883
|
figmaUrl,
|
|
1884
1884
|
threshold
|
|
1885
1885
|
}, ctx.config.fileConfig?.endpoints);
|
|
1886
|
-
if (
|
|
1886
|
+
if (result2.error) {
|
|
1887
1887
|
return {
|
|
1888
1888
|
content: [{
|
|
1889
1889
|
type: "text",
|
|
1890
|
-
text: `Compare error: ${
|
|
1891
|
-
Suggestion: ${
|
|
1890
|
+
text: `Compare error: ${result2.error}${result2.suggestion ? `
|
|
1891
|
+
Suggestion: ${result2.suggestion}` : ""}`
|
|
1892
1892
|
}],
|
|
1893
1893
|
isError: true
|
|
1894
1894
|
};
|
|
1895
1895
|
}
|
|
1896
1896
|
const content = [];
|
|
1897
|
-
const summaryText =
|
|
1897
|
+
const summaryText = result2.match ? `MATCH: ${componentName} matches Figma design (${result2.diffPercentage}% diff, threshold: ${result2.threshold}%)` : `MISMATCH: ${componentName} differs from Figma design by ${result2.diffPercentage}% (threshold: ${result2.threshold}%)`;
|
|
1898
1898
|
content.push({ type: "text", text: summaryText });
|
|
1899
|
-
if (
|
|
1899
|
+
if (result2.diff && !result2.match) {
|
|
1900
1900
|
content.push({
|
|
1901
1901
|
type: "image",
|
|
1902
|
-
data:
|
|
1902
|
+
data: result2.diff.replace("data:image/png;base64,", ""),
|
|
1903
1903
|
mimeType: "image/png"
|
|
1904
1904
|
});
|
|
1905
1905
|
content.push({
|
|
1906
1906
|
type: "text",
|
|
1907
|
-
text: `Diff image above shows visual differences (red highlights). Changed regions: ${
|
|
1907
|
+
text: `Diff image above shows visual differences (red highlights). Changed regions: ${result2.changedRegions?.length ?? 0}`
|
|
1908
1908
|
});
|
|
1909
1909
|
}
|
|
1910
1910
|
content.push({
|
|
1911
1911
|
type: "text",
|
|
1912
1912
|
text: JSON.stringify({
|
|
1913
|
-
match:
|
|
1914
|
-
diffPercentage:
|
|
1915
|
-
threshold:
|
|
1916
|
-
figmaUrl:
|
|
1917
|
-
changedRegions:
|
|
1913
|
+
match: result2.match,
|
|
1914
|
+
diffPercentage: result2.diffPercentage,
|
|
1915
|
+
threshold: result2.threshold,
|
|
1916
|
+
figmaUrl: result2.figmaUrl,
|
|
1917
|
+
changedRegions: result2.changedRegions
|
|
1918
1918
|
})
|
|
1919
1919
|
});
|
|
1920
1920
|
return { content };
|
|
@@ -1929,15 +1929,15 @@ Suggestion: ${result.suggestion}` : ""}`
|
|
|
1929
1929
|
}
|
|
1930
1930
|
}
|
|
1931
1931
|
try {
|
|
1932
|
-
const
|
|
1932
|
+
const result2 = await renderComponent(viewerUrl, {
|
|
1933
1933
|
component: componentName,
|
|
1934
1934
|
props,
|
|
1935
1935
|
variant: variantName,
|
|
1936
1936
|
viewport: viewport ?? { width: 800, height: 600 }
|
|
1937
1937
|
}, ctx.config.fileConfig?.endpoints);
|
|
1938
|
-
if (
|
|
1938
|
+
if (result2.error) {
|
|
1939
1939
|
return {
|
|
1940
|
-
content: [{ type: "text", text: `Render error: ${
|
|
1940
|
+
content: [{ type: "text", text: `Render error: ${result2.error}` }],
|
|
1941
1941
|
isError: true
|
|
1942
1942
|
};
|
|
1943
1943
|
}
|
|
@@ -1945,7 +1945,7 @@ Suggestion: ${result.suggestion}` : ""}`
|
|
|
1945
1945
|
content: [
|
|
1946
1946
|
{
|
|
1947
1947
|
type: "image",
|
|
1948
|
-
data:
|
|
1948
|
+
data: result2.screenshot.replace("data:image/png;base64,", ""),
|
|
1949
1949
|
mimeType: "image/png"
|
|
1950
1950
|
},
|
|
1951
1951
|
{
|
|
@@ -1992,16 +1992,16 @@ var fixHandler = async (args, ctx) => {
|
|
|
1992
1992
|
};
|
|
1993
1993
|
}
|
|
1994
1994
|
try {
|
|
1995
|
-
const
|
|
1995
|
+
const result2 = await fixComponent(viewerUrl, {
|
|
1996
1996
|
component: componentName,
|
|
1997
1997
|
variant: variantName,
|
|
1998
1998
|
fixType
|
|
1999
1999
|
}, ctx.config.fileConfig?.endpoints);
|
|
2000
|
-
if (
|
|
2000
|
+
if (result2.error) {
|
|
2001
2001
|
return {
|
|
2002
2002
|
content: [{
|
|
2003
2003
|
type: "text",
|
|
2004
|
-
text: `Fix generation error: ${
|
|
2004
|
+
text: `Fix generation error: ${result2.error}`
|
|
2005
2005
|
}],
|
|
2006
2006
|
isError: true
|
|
2007
2007
|
};
|
|
@@ -2013,10 +2013,10 @@ var fixHandler = async (args, ctx) => {
|
|
|
2013
2013
|
component: componentName,
|
|
2014
2014
|
variant: variantName ?? "all",
|
|
2015
2015
|
fixType,
|
|
2016
|
-
patches:
|
|
2017
|
-
summary:
|
|
2018
|
-
patchCount:
|
|
2019
|
-
nextStep:
|
|
2016
|
+
patches: result2.patches,
|
|
2017
|
+
summary: result2.summary,
|
|
2018
|
+
patchCount: result2.patches.length,
|
|
2019
|
+
nextStep: result2.patches.length > 0 ? `Apply patches using your editor or \`patch\` command, then run ${ctx.toolNames.render} to confirm fixes.` : void 0
|
|
2020
2020
|
})
|
|
2021
2021
|
}]
|
|
2022
2022
|
};
|
|
@@ -2061,25 +2061,25 @@ var a11yHandler = async (args, ctx) => {
|
|
|
2061
2061
|
};
|
|
2062
2062
|
}
|
|
2063
2063
|
try {
|
|
2064
|
-
const
|
|
2064
|
+
const result2 = await auditComponent(viewerUrl, {
|
|
2065
2065
|
component: componentName,
|
|
2066
2066
|
variant: variantName,
|
|
2067
2067
|
standard,
|
|
2068
2068
|
includeFixPatches
|
|
2069
2069
|
}, ctx.config.fileConfig?.endpoints);
|
|
2070
|
-
if (
|
|
2070
|
+
if (result2.error) {
|
|
2071
2071
|
return {
|
|
2072
2072
|
content: [{
|
|
2073
2073
|
type: "text",
|
|
2074
|
-
text: `A11y audit error: ${
|
|
2074
|
+
text: `A11y audit error: ${result2.error}`
|
|
2075
2075
|
}],
|
|
2076
2076
|
isError: true
|
|
2077
2077
|
};
|
|
2078
2078
|
}
|
|
2079
2079
|
let nextStep;
|
|
2080
|
-
if (
|
|
2080
|
+
if (result2.emptyAudit) {
|
|
2081
2081
|
nextStep = `No testable elements found for ${variantName ? `variant "${variantName}"` : componentName}. The variant may not exist or renders no accessible content. Check available variants with ${ctx.toolNames.inspect}("${componentName}").`;
|
|
2082
|
-
} else if (
|
|
2082
|
+
} else if (result2.passed) {
|
|
2083
2083
|
nextStep = 'All accessibility checks passed. Consider running with standard: "AAA" for enhanced compliance.';
|
|
2084
2084
|
} else {
|
|
2085
2085
|
nextStep = `Fix the violations above, then re-run ${ctx.toolNames.a11y} to verify. Use ${ctx.toolNames.fix} for automated fixes.`;
|
|
@@ -2091,12 +2091,12 @@ var a11yHandler = async (args, ctx) => {
|
|
|
2091
2091
|
component: componentName,
|
|
2092
2092
|
variant: variantName ?? "all",
|
|
2093
2093
|
standard,
|
|
2094
|
-
totalViolations:
|
|
2095
|
-
variantsPassingAA: `${
|
|
2096
|
-
variantsPassingAAA: `${
|
|
2097
|
-
passed:
|
|
2098
|
-
...
|
|
2099
|
-
results:
|
|
2094
|
+
totalViolations: result2.results.reduce((sum, r) => sum + r.summary.total, 0),
|
|
2095
|
+
variantsPassingAA: `${result2.aaPercent}%`,
|
|
2096
|
+
variantsPassingAAA: `${result2.aaaPercent}%`,
|
|
2097
|
+
passed: result2.passed,
|
|
2098
|
+
...result2.emptyAudit && { emptyAudit: true },
|
|
2099
|
+
results: result2.results,
|
|
2100
2100
|
nextStep
|
|
2101
2101
|
})
|
|
2102
2102
|
}]
|
|
@@ -2207,12 +2207,12 @@ function handleGraphTool(args, serializedGraph, blocks, componentNames2) {
|
|
|
2207
2207
|
const suggestion = closest ? ` Did you mean "${closest}"?` : "";
|
|
2208
2208
|
return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
|
|
2209
2209
|
}
|
|
2210
|
-
const
|
|
2210
|
+
const result2 = engine.impact(args.component, args.maxDepth ?? 3);
|
|
2211
2211
|
return {
|
|
2212
2212
|
text: JSON.stringify({
|
|
2213
2213
|
mode: "impact",
|
|
2214
|
-
...
|
|
2215
|
-
summary: `Changing ${args.component} affects ${
|
|
2214
|
+
...result2,
|
|
2215
|
+
summary: `Changing ${args.component} affects ${result2.totalAffected} component(s) and ${result2.affectedBlocks.length} block(s)`
|
|
2216
2216
|
})
|
|
2217
2217
|
};
|
|
2218
2218
|
}
|
|
@@ -2220,14 +2220,14 @@ function handleGraphTool(args, serializedGraph, blocks, componentNames2) {
|
|
|
2220
2220
|
if (!args.component || !args.target) {
|
|
2221
2221
|
return { text: JSON.stringify({ error: "component and target are required for path mode" }), isError: true };
|
|
2222
2222
|
}
|
|
2223
|
-
const
|
|
2223
|
+
const result2 = engine.path(args.component, args.target);
|
|
2224
2224
|
return {
|
|
2225
2225
|
text: JSON.stringify({
|
|
2226
2226
|
mode: "path",
|
|
2227
2227
|
from: args.component,
|
|
2228
2228
|
to: args.target,
|
|
2229
|
-
...
|
|
2230
|
-
edges:
|
|
2229
|
+
...result2,
|
|
2230
|
+
edges: result2.edges.map((e) => ({
|
|
2231
2231
|
source: e.source,
|
|
2232
2232
|
target: e.target,
|
|
2233
2233
|
type: e.type
|
|
@@ -2306,20 +2306,20 @@ var graphHandler = async (args, ctx) => {
|
|
|
2306
2306
|
};
|
|
2307
2307
|
const data = ctx.data;
|
|
2308
2308
|
const allNames = componentNames(data.snapshot);
|
|
2309
|
-
const
|
|
2309
|
+
const result2 = handleGraphTool(
|
|
2310
2310
|
graphArgs,
|
|
2311
2311
|
data.graph,
|
|
2312
2312
|
data.blocks,
|
|
2313
2313
|
allNames
|
|
2314
2314
|
);
|
|
2315
|
-
if (
|
|
2315
|
+
if (result2.isError) {
|
|
2316
2316
|
return {
|
|
2317
|
-
content: [{ type: "text", text:
|
|
2317
|
+
content: [{ type: "text", text: result2.text }],
|
|
2318
2318
|
isError: true
|
|
2319
2319
|
};
|
|
2320
2320
|
}
|
|
2321
2321
|
return {
|
|
2322
|
-
content: [{ type: "text", text:
|
|
2322
|
+
content: [{ type: "text", text: result2.text }]
|
|
2323
2323
|
};
|
|
2324
2324
|
};
|
|
2325
2325
|
|
|
@@ -2389,13 +2389,274 @@ var perfHandler = async (args, ctx) => {
|
|
|
2389
2389
|
};
|
|
2390
2390
|
};
|
|
2391
2391
|
|
|
2392
|
+
// src/tools/spec-govern.ts
|
|
2393
|
+
var SEVERITY_WEIGHTS = {
|
|
2394
|
+
critical: 10,
|
|
2395
|
+
serious: 5,
|
|
2396
|
+
moderate: 2,
|
|
2397
|
+
minor: 1
|
|
2398
|
+
};
|
|
2399
|
+
function isRuleEnabled(rule, defaultEnabled = true) {
|
|
2400
|
+
if (rule === void 0) return defaultEnabled;
|
|
2401
|
+
if (typeof rule === "boolean") return rule;
|
|
2402
|
+
return rule.enabled ?? defaultEnabled;
|
|
2403
|
+
}
|
|
2404
|
+
function ruleSeverity(rule, fallback) {
|
|
2405
|
+
return typeof rule === "object" && rule?.severity ? rule.severity : fallback;
|
|
2406
|
+
}
|
|
2407
|
+
function ruleOptions(rule) {
|
|
2408
|
+
return typeof rule === "object" && rule?.options ? rule.options : {};
|
|
2409
|
+
}
|
|
2410
|
+
function nodeId(node, fallback) {
|
|
2411
|
+
return typeof node.id === "string" ? node.id : `node-${fallback}`;
|
|
2412
|
+
}
|
|
2413
|
+
function nodeType(node) {
|
|
2414
|
+
return typeof node.type === "string" ? node.type : "_unknown";
|
|
2415
|
+
}
|
|
2416
|
+
function parentType(type) {
|
|
2417
|
+
const dotIndex = type.indexOf(".");
|
|
2418
|
+
return dotIndex > 0 ? type.slice(0, dotIndex) : type;
|
|
2419
|
+
}
|
|
2420
|
+
function walkNodes(nodes, visitor) {
|
|
2421
|
+
if (!Array.isArray(nodes)) return;
|
|
2422
|
+
for (const [index, node] of nodes.entries()) {
|
|
2423
|
+
if (typeof node !== "object" || node === null || Array.isArray(node)) {
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2426
|
+
const specNode = node;
|
|
2427
|
+
visitor(specNode, index);
|
|
2428
|
+
walkNodes(specNode.children, visitor);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
function flattenNodes(spec) {
|
|
2432
|
+
const flattened = [];
|
|
2433
|
+
walkNodes(spec.nodes, (node) => flattened.push(node));
|
|
2434
|
+
return flattened;
|
|
2435
|
+
}
|
|
2436
|
+
function worstSeverity(violations) {
|
|
2437
|
+
const order = ["critical", "serious", "moderate", "minor"];
|
|
2438
|
+
return violations.reduce(
|
|
2439
|
+
(worst, violation) => order.indexOf(violation.severity) < order.indexOf(worst) ? violation.severity : worst,
|
|
2440
|
+
"minor"
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
function result(validator, violations) {
|
|
2444
|
+
return {
|
|
2445
|
+
validator,
|
|
2446
|
+
severity: violations.length > 0 ? worstSeverity(violations) : "minor",
|
|
2447
|
+
passed: violations.length === 0,
|
|
2448
|
+
violations
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
function computeScore(violations) {
|
|
2452
|
+
const penalty = violations.reduce(
|
|
2453
|
+
(sum, violation) => sum + SEVERITY_WEIGHTS[violation.severity],
|
|
2454
|
+
0
|
|
2455
|
+
);
|
|
2456
|
+
return Math.max(0, 100 - penalty);
|
|
2457
|
+
}
|
|
2458
|
+
function validateComponents(nodes, options) {
|
|
2459
|
+
const rules = options.policy?.rules ?? {};
|
|
2460
|
+
const allowRule = rules["components/allow"];
|
|
2461
|
+
const denyRule = rules["components/deny"];
|
|
2462
|
+
const allowOptions = ruleOptions(allowRule);
|
|
2463
|
+
const denyOptions = ruleOptions(denyRule);
|
|
2464
|
+
const allowed = new Set(
|
|
2465
|
+
(allowOptions.components ?? options.allowedComponents).filter(Boolean)
|
|
2466
|
+
);
|
|
2467
|
+
const denied = new Set(denyOptions.components ?? []);
|
|
2468
|
+
const violations = [];
|
|
2469
|
+
for (const [index, node] of nodes.entries()) {
|
|
2470
|
+
const type = nodeType(node);
|
|
2471
|
+
const parent = parentType(type);
|
|
2472
|
+
if (allowed.size > 0 && isRuleEnabled(allowRule) && !allowed.has(type) && !allowed.has(parent)) {
|
|
2473
|
+
violations.push({
|
|
2474
|
+
nodeId: nodeId(node, index),
|
|
2475
|
+
nodeType: type,
|
|
2476
|
+
rule: "components/allow",
|
|
2477
|
+
severity: ruleSeverity(allowRule, "serious"),
|
|
2478
|
+
message: `Component "${type}" is not in the allowed list`,
|
|
2479
|
+
suggestion: `Use one of the allowed components: ${[...allowed].slice(0, 10).join(", ")}${allowed.size > 10 ? "..." : ""}`
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
if (denied.size > 0 && isRuleEnabled(denyRule) && (denied.has(type) || denied.has(parent))) {
|
|
2483
|
+
violations.push({
|
|
2484
|
+
nodeId: nodeId(node, index),
|
|
2485
|
+
nodeType: type,
|
|
2486
|
+
rule: "components/deny",
|
|
2487
|
+
severity: ruleSeverity(denyRule, "serious"),
|
|
2488
|
+
message: `Component "${type}" is blocked by the deny list`,
|
|
2489
|
+
suggestion: `Remove "${type}" or replace it with an allowed alternative`
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
return result("components", violations);
|
|
2494
|
+
}
|
|
2495
|
+
function isEventHandlerProp(prop) {
|
|
2496
|
+
return /^on[A-Z]/.test(prop);
|
|
2497
|
+
}
|
|
2498
|
+
function validateSafety(nodes) {
|
|
2499
|
+
const violations = [];
|
|
2500
|
+
const dangerousProps = /* @__PURE__ */ new Set(["dangerouslySetInnerHTML", "innerHTML"]);
|
|
2501
|
+
for (const [index, node] of nodes.entries()) {
|
|
2502
|
+
for (const [prop, value] of Object.entries(node.props ?? {})) {
|
|
2503
|
+
if (dangerousProps.has(prop)) {
|
|
2504
|
+
violations.push({
|
|
2505
|
+
nodeId: nodeId(node, index),
|
|
2506
|
+
nodeType: nodeType(node),
|
|
2507
|
+
rule: "safety/no-dangerous-props",
|
|
2508
|
+
severity: "critical",
|
|
2509
|
+
message: `Dangerous prop "${prop}" is not allowed`,
|
|
2510
|
+
suggestion: `Remove "${prop}" or use a safe rendering pattern`,
|
|
2511
|
+
prop
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
if (isEventHandlerProp(prop) && typeof value === "string") {
|
|
2515
|
+
violations.push({
|
|
2516
|
+
nodeId: nodeId(node, index),
|
|
2517
|
+
nodeType: nodeType(node),
|
|
2518
|
+
rule: "safety/no-string-handlers",
|
|
2519
|
+
severity: "serious",
|
|
2520
|
+
message: `Event handler prop "${prop}" must not be a string`,
|
|
2521
|
+
suggestion: `Remove "${prop}" from generated specs`,
|
|
2522
|
+
prop
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
return result("safety", violations);
|
|
2528
|
+
}
|
|
2529
|
+
function hasHardcodedCssValue(value) {
|
|
2530
|
+
if (value.includes("var(")) return false;
|
|
2531
|
+
return /#[0-9a-f]{3,8}\b/i.test(value) || /\b\d+(?:px|rem|em)\b/.test(value);
|
|
2532
|
+
}
|
|
2533
|
+
function validateTokens(nodes, options) {
|
|
2534
|
+
const rules = options.policy?.rules ?? {};
|
|
2535
|
+
const requireRule = rules["tokens/require-design-tokens"];
|
|
2536
|
+
const prefixRule = rules["tokens/allowed-prefixes"];
|
|
2537
|
+
const requireTokens = isRuleEnabled(requireRule);
|
|
2538
|
+
const defaultTokenPrefix = options.tokenPrefix?.replace(/^--/, "");
|
|
2539
|
+
const allowedPrefixes = ruleOptions(prefixRule).prefixes ?? (defaultTokenPrefix ? [defaultTokenPrefix] : []);
|
|
2540
|
+
const violations = [];
|
|
2541
|
+
for (const [index, node] of nodes.entries()) {
|
|
2542
|
+
for (const [prop, value] of Object.entries(node.props ?? {})) {
|
|
2543
|
+
if (typeof value !== "string") continue;
|
|
2544
|
+
if (requireTokens && hasHardcodedCssValue(value)) {
|
|
2545
|
+
violations.push({
|
|
2546
|
+
nodeId: nodeId(node, index),
|
|
2547
|
+
nodeType: nodeType(node),
|
|
2548
|
+
rule: "tokens/require-design-tokens",
|
|
2549
|
+
severity: ruleSeverity(requireRule, "moderate"),
|
|
2550
|
+
message: `Hardcoded CSS value "${value}" in prop "${prop}" - use a design token instead`,
|
|
2551
|
+
suggestion: "Use a design token instead of a hardcoded CSS value",
|
|
2552
|
+
prop,
|
|
2553
|
+
rawValue: value
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
if (allowedPrefixes.length > 0 && isRuleEnabled(prefixRule, Boolean(options.tokenPrefix))) {
|
|
2557
|
+
const matches = value.matchAll(/var\(--([^)]+)\)/g);
|
|
2558
|
+
for (const match of matches) {
|
|
2559
|
+
const tokenName = match[1];
|
|
2560
|
+
if (allowedPrefixes.some((prefix) => tokenName.startsWith(prefix))) {
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
violations.push({
|
|
2564
|
+
nodeId: nodeId(node, index),
|
|
2565
|
+
nodeType: nodeType(node),
|
|
2566
|
+
rule: "tokens/allowed-prefixes",
|
|
2567
|
+
severity: ruleSeverity(prefixRule, "moderate"),
|
|
2568
|
+
message: `Token "--${tokenName}" does not use an allowed prefix (${allowedPrefixes.join(", ")})`,
|
|
2569
|
+
suggestion: `Use a token with one of the allowed prefixes: ${allowedPrefixes.map((prefix) => `--${prefix}*`).join(", ")}`,
|
|
2570
|
+
prop,
|
|
2571
|
+
rawValue: value
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
return result("tokens", violations);
|
|
2578
|
+
}
|
|
2579
|
+
function textFromChildren(children) {
|
|
2580
|
+
if (!Array.isArray(children)) return "";
|
|
2581
|
+
return children.map((child) => {
|
|
2582
|
+
if (typeof child === "string") return child;
|
|
2583
|
+
if (typeof child === "object" && child !== null && !Array.isArray(child)) {
|
|
2584
|
+
return textFromChildren(child.children);
|
|
2585
|
+
}
|
|
2586
|
+
return "";
|
|
2587
|
+
}).join(" ").trim();
|
|
2588
|
+
}
|
|
2589
|
+
function validateA11y(nodes) {
|
|
2590
|
+
const violations = [];
|
|
2591
|
+
for (const [index, node] of nodes.entries()) {
|
|
2592
|
+
const type = nodeType(node);
|
|
2593
|
+
if (!/button/i.test(type)) continue;
|
|
2594
|
+
const props = node.props ?? {};
|
|
2595
|
+
const label = props["aria-label"] ?? props["aria-labelledby"] ?? props.title;
|
|
2596
|
+
const childText = textFromChildren(node.children);
|
|
2597
|
+
if (typeof label === "string" && label.trim().length > 0) continue;
|
|
2598
|
+
if (childText.length > 0) continue;
|
|
2599
|
+
violations.push({
|
|
2600
|
+
nodeId: nodeId(node, index),
|
|
2601
|
+
nodeType: type,
|
|
2602
|
+
rule: "button-name",
|
|
2603
|
+
severity: "serious",
|
|
2604
|
+
message: "Buttons must have discernible text (button-name)",
|
|
2605
|
+
suggestion: "Add visible text or an aria-label"
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
return result("a11y", violations);
|
|
2609
|
+
}
|
|
2610
|
+
function runSpecGovern(spec, options) {
|
|
2611
|
+
const startedAt = Date.now();
|
|
2612
|
+
const nodes = flattenNodes(spec);
|
|
2613
|
+
const results = [
|
|
2614
|
+
validateSafety(nodes),
|
|
2615
|
+
validateComponents(nodes, options),
|
|
2616
|
+
validateTokens(nodes, options),
|
|
2617
|
+
validateA11y(nodes)
|
|
2618
|
+
];
|
|
2619
|
+
const violations = results.flatMap((entry) => entry.violations);
|
|
2620
|
+
const componentTypes = Array.from(
|
|
2621
|
+
new Set(nodes.map((node) => nodeType(node)))
|
|
2622
|
+
);
|
|
2623
|
+
return {
|
|
2624
|
+
passed: results.every((entry) => entry.passed),
|
|
2625
|
+
score: computeScore(violations),
|
|
2626
|
+
results,
|
|
2627
|
+
metadata: {
|
|
2628
|
+
runner: "mcp",
|
|
2629
|
+
duration: Date.now() - startedAt,
|
|
2630
|
+
nodeCount: nodes.length,
|
|
2631
|
+
componentTypes
|
|
2632
|
+
}
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
function formatVerdict(verdict, format = "summary") {
|
|
2636
|
+
if (format === "json") {
|
|
2637
|
+
return JSON.stringify(verdict, null, 2);
|
|
2638
|
+
}
|
|
2639
|
+
const lines = [];
|
|
2640
|
+
const icon = verdict.passed ? "ok" : "fail";
|
|
2641
|
+
lines.push(`${icon} Governance check: score ${verdict.score}/100`);
|
|
2642
|
+
lines.push("");
|
|
2643
|
+
for (const entry of verdict.results) {
|
|
2644
|
+
const resultIcon = entry.passed ? "ok" : "fail";
|
|
2645
|
+
const count = entry.violations.length;
|
|
2646
|
+
lines.push(` ${resultIcon} ${entry.validator}: ${count === 0 ? "passed" : `${count} violation(s)`}`);
|
|
2647
|
+
for (const violation of entry.violations) {
|
|
2648
|
+
lines.push(` - [${violation.severity}] ${violation.nodeType}#${violation.nodeId}: ${violation.message}`);
|
|
2649
|
+
if (violation.suggestion) {
|
|
2650
|
+
lines.push(` -> ${violation.suggestion}`);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
lines.push("");
|
|
2655
|
+
lines.push(`Duration: ${verdict.metadata.duration.toFixed(0)}ms | Nodes: ${verdict.metadata.nodeCount}`);
|
|
2656
|
+
return lines.join("\n");
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2392
2659
|
// src/tools/govern.ts
|
|
2393
|
-
import {
|
|
2394
|
-
handleGovernTool,
|
|
2395
|
-
formatVerdict,
|
|
2396
|
-
universal,
|
|
2397
|
-
fragments as fragmentsPreset
|
|
2398
|
-
} from "@fragments-sdk/govern";
|
|
2399
2660
|
var governHandler = async (args, ctx) => {
|
|
2400
2661
|
const spec = args?.spec;
|
|
2401
2662
|
if (!spec || typeof spec !== "object") {
|
|
@@ -2416,23 +2677,12 @@ var governHandler = async (args, ctx) => {
|
|
|
2416
2677
|
const allowedComponents = Object.values(ctx.data.components).map(
|
|
2417
2678
|
(component) => component.name
|
|
2418
2679
|
);
|
|
2419
|
-
const tokenPrefix = ctx.data.tokens?.prefix;
|
|
2420
|
-
const basePolicy = tokenPrefix === "fui-" ? { rules: fragmentsPreset().rules } : { rules: universal().rules };
|
|
2421
|
-
basePolicy.rules["components/allow"] = {
|
|
2422
|
-
enabled: true,
|
|
2423
|
-
severity: "serious",
|
|
2424
|
-
options: {
|
|
2425
|
-
components: allowedComponents
|
|
2426
|
-
}
|
|
2427
|
-
};
|
|
2428
|
-
const engineOptions = ctx.data.tokens ? { tokenData: ctx.data.tokens } : void 0;
|
|
2429
|
-
const input = {
|
|
2430
|
-
spec,
|
|
2431
|
-
policy: policyOverrides,
|
|
2432
|
-
format
|
|
2433
|
-
};
|
|
2434
2680
|
try {
|
|
2435
|
-
const verdict =
|
|
2681
|
+
const verdict = runSpecGovern(spec, {
|
|
2682
|
+
allowedComponents,
|
|
2683
|
+
tokenPrefix: ctx.data.tokens?.prefix,
|
|
2684
|
+
policy: policyOverrides
|
|
2685
|
+
});
|
|
2436
2686
|
const text = format === "summary" ? formatVerdict(verdict, "summary") : JSON.stringify(verdict);
|
|
2437
2687
|
return {
|
|
2438
2688
|
content: [{ type: "text", text }],
|
|
@@ -2463,32 +2713,80 @@ var governHandler = async (args, ctx) => {
|
|
|
2463
2713
|
};
|
|
2464
2714
|
|
|
2465
2715
|
// src/tools/validate-and-fix.ts
|
|
2466
|
-
import {
|
|
2467
|
-
formatVerdict as formatVerdict2,
|
|
2468
|
-
fragments as fragmentsPreset2,
|
|
2469
|
-
handleGovernTool as handleGovernTool2,
|
|
2470
|
-
universal as universal2
|
|
2471
|
-
} from "@fragments-sdk/govern";
|
|
2472
2716
|
function classifyComponent(component) {
|
|
2473
2717
|
if (component.status === "deprecated") return "discouraged";
|
|
2474
2718
|
if (component.isCanonical && component.tier === "core") return "preferred";
|
|
2475
2719
|
return "allowed";
|
|
2476
2720
|
}
|
|
2477
2721
|
function buildEffectiveComponents(ctx) {
|
|
2478
|
-
const
|
|
2722
|
+
const validateFixByKey = new Map(
|
|
2479
2723
|
(ctx.data.validateFixContext?.components ?? []).map((component) => [
|
|
2480
2724
|
component.componentKey,
|
|
2481
|
-
component
|
|
2725
|
+
component
|
|
2482
2726
|
])
|
|
2483
2727
|
);
|
|
2484
2728
|
return Object.entries(ctx.data.components).map(([componentKey, component]) => ({
|
|
2485
2729
|
component,
|
|
2486
|
-
selection:
|
|
2730
|
+
selection: validateFixByKey.get(componentKey)?.selection ?? classifyComponent(component),
|
|
2731
|
+
isActive: validateFixByKey.get(componentKey)?.isActive ?? true,
|
|
2732
|
+
reasons: validateFixByKey.get(componentKey)?.reasons ?? []
|
|
2487
2733
|
}));
|
|
2488
2734
|
}
|
|
2489
2735
|
function cloneSpec(spec) {
|
|
2490
2736
|
return structuredClone(spec);
|
|
2491
2737
|
}
|
|
2738
|
+
function isPlainObject(value) {
|
|
2739
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2740
|
+
}
|
|
2741
|
+
function validateNodeShape(node, path) {
|
|
2742
|
+
if (!isPlainObject(node)) {
|
|
2743
|
+
return `${path} must be an object`;
|
|
2744
|
+
}
|
|
2745
|
+
if ("id" in node && node.id !== void 0 && typeof node.id !== "string") {
|
|
2746
|
+
return `${path}.id must be a string`;
|
|
2747
|
+
}
|
|
2748
|
+
if ("type" in node && node.type !== void 0 && typeof node.type !== "string") {
|
|
2749
|
+
return `${path}.type must be a string`;
|
|
2750
|
+
}
|
|
2751
|
+
if ("props" in node && node.props !== void 0 && !isPlainObject(node.props)) {
|
|
2752
|
+
return `${path}.props must be an object`;
|
|
2753
|
+
}
|
|
2754
|
+
if ("children" in node && node.children !== void 0) {
|
|
2755
|
+
if (!Array.isArray(node.children)) {
|
|
2756
|
+
return `${path}.children must be an array`;
|
|
2757
|
+
}
|
|
2758
|
+
for (const [index, child] of node.children.entries()) {
|
|
2759
|
+
if (!isPlainObject(child)) continue;
|
|
2760
|
+
const childError = validateNodeShape(
|
|
2761
|
+
child,
|
|
2762
|
+
`${path}.children[${index}]`
|
|
2763
|
+
);
|
|
2764
|
+
if (childError) return childError;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
return null;
|
|
2768
|
+
}
|
|
2769
|
+
function validateSpecShape(spec) {
|
|
2770
|
+
if (!isPlainObject(spec)) {
|
|
2771
|
+
return "spec must be an object";
|
|
2772
|
+
}
|
|
2773
|
+
if ("root" in spec && spec.root !== void 0 && typeof spec.root !== "string") {
|
|
2774
|
+
return "spec.root must be a string";
|
|
2775
|
+
}
|
|
2776
|
+
if ("metadata" in spec && spec.metadata !== void 0 && !isPlainObject(spec.metadata)) {
|
|
2777
|
+
return "spec.metadata must be an object";
|
|
2778
|
+
}
|
|
2779
|
+
if ("nodes" in spec && spec.nodes !== void 0) {
|
|
2780
|
+
if (!Array.isArray(spec.nodes)) {
|
|
2781
|
+
return "spec.nodes must be an array";
|
|
2782
|
+
}
|
|
2783
|
+
for (const [index, node] of spec.nodes.entries()) {
|
|
2784
|
+
const error = validateNodeShape(node, `spec.nodes[${index}]`);
|
|
2785
|
+
if (error) return error;
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
return null;
|
|
2789
|
+
}
|
|
2492
2790
|
function getSelectionNames(effectiveComponents, selection) {
|
|
2493
2791
|
const selections = Array.isArray(selection) ? selection : [selection];
|
|
2494
2792
|
return Array.from(
|
|
@@ -2497,27 +2795,146 @@ function getSelectionNames(effectiveComponents, selection) {
|
|
|
2497
2795
|
)
|
|
2498
2796
|
);
|
|
2499
2797
|
}
|
|
2500
|
-
function
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2798
|
+
function normalizeTokens(value) {
|
|
2799
|
+
return value.toLowerCase().split(/[^a-z0-9]+/g).map((token) => token.trim()).filter((token) => token.length >= 3);
|
|
2800
|
+
}
|
|
2801
|
+
function buildComponentText(component) {
|
|
2802
|
+
return [
|
|
2803
|
+
component.name,
|
|
2804
|
+
component.category,
|
|
2805
|
+
component.description,
|
|
2806
|
+
component.guidance.usageGuidance,
|
|
2807
|
+
...component.guidance.when,
|
|
2808
|
+
...component.guidance.whenNot,
|
|
2809
|
+
...component.guidance.dos,
|
|
2810
|
+
...component.guidance.donts
|
|
2811
|
+
].filter(Boolean).join(" ");
|
|
2812
|
+
}
|
|
2813
|
+
function rankReplacementCandidates(args) {
|
|
2814
|
+
const nodePropKeys = Object.keys(args.node.props ?? {});
|
|
2815
|
+
const sourcePropKeys = new Set(Object.keys(args.source.component.props ?? {}));
|
|
2816
|
+
const sourceTokens = new Set(normalizeTokens(buildComponentText(args.source.component)));
|
|
2817
|
+
return args.candidates.map((candidate) => {
|
|
2818
|
+
let score = 0;
|
|
2819
|
+
const reasons = [];
|
|
2820
|
+
const candidatePropKeys = new Set(Object.keys(candidate.component.props ?? {}));
|
|
2821
|
+
if (candidate.selection === "preferred") {
|
|
2822
|
+
score += 12;
|
|
2823
|
+
reasons.push("preferred selection");
|
|
2824
|
+
}
|
|
2825
|
+
if (candidate.component.isCanonical) {
|
|
2826
|
+
score += 6;
|
|
2827
|
+
reasons.push("canonical component");
|
|
2828
|
+
}
|
|
2829
|
+
if (candidate.component.tier === "core") {
|
|
2830
|
+
score += 4;
|
|
2831
|
+
reasons.push("core tier");
|
|
2832
|
+
}
|
|
2833
|
+
if (candidate.isActive) {
|
|
2834
|
+
score += 3;
|
|
2835
|
+
reasons.push("active component");
|
|
2836
|
+
}
|
|
2837
|
+
const supportedNodeProps = nodePropKeys.filter(
|
|
2838
|
+
(key) => candidatePropKeys.has(key)
|
|
2839
|
+
);
|
|
2840
|
+
if (supportedNodeProps.length > 0) {
|
|
2841
|
+
score += supportedNodeProps.length * 5;
|
|
2842
|
+
reasons.push(`supports node props: ${supportedNodeProps.join(", ")}`);
|
|
2509
2843
|
}
|
|
2510
|
-
|
|
2511
|
-
|
|
2844
|
+
const missingNodeProps = nodePropKeys.filter(
|
|
2845
|
+
(key) => !candidatePropKeys.has(key)
|
|
2846
|
+
);
|
|
2847
|
+
if (missingNodeProps.length > 0) {
|
|
2848
|
+
score -= missingNodeProps.length * 6;
|
|
2849
|
+
reasons.push(`missing node props: ${missingNodeProps.join(", ")}`);
|
|
2850
|
+
}
|
|
2851
|
+
const sharedContractProps = [...sourcePropKeys].filter(
|
|
2852
|
+
(key) => candidatePropKeys.has(key)
|
|
2853
|
+
);
|
|
2854
|
+
if (sharedContractProps.length > 0) {
|
|
2855
|
+
score += sharedContractProps.length * 2;
|
|
2856
|
+
reasons.push(`shares source props: ${sharedContractProps.join(", ")}`);
|
|
2857
|
+
}
|
|
2858
|
+
const candidateTokens = new Set(
|
|
2859
|
+
normalizeTokens(buildComponentText(candidate.component))
|
|
2860
|
+
);
|
|
2861
|
+
const tokenOverlap = [...candidateTokens].filter(
|
|
2862
|
+
(token) => sourceTokens.has(token)
|
|
2863
|
+
);
|
|
2864
|
+
if (tokenOverlap.length > 0) {
|
|
2865
|
+
score += Math.min(tokenOverlap.length, 4);
|
|
2866
|
+
reasons.push(`guidance overlap: ${tokenOverlap.join(", ")}`);
|
|
2867
|
+
}
|
|
2868
|
+
if (candidate.component.parentComponentId && candidate.component.parentComponentId === args.source.component.parentComponentId) {
|
|
2869
|
+
score += 2;
|
|
2870
|
+
reasons.push("matches parent component");
|
|
2871
|
+
}
|
|
2872
|
+
return {
|
|
2873
|
+
name: candidate.component.name,
|
|
2874
|
+
score,
|
|
2875
|
+
reasons
|
|
2876
|
+
};
|
|
2877
|
+
}).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
2512
2878
|
}
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2879
|
+
function chooseDeterministicCandidate(rankedCandidates) {
|
|
2880
|
+
if (rankedCandidates.length === 0) return null;
|
|
2881
|
+
if (rankedCandidates.length === 1) return rankedCandidates[0].name;
|
|
2882
|
+
const [first, second] = rankedCandidates;
|
|
2883
|
+
if (first.score <= 0) return null;
|
|
2884
|
+
if (first.score - second.score < 4) return null;
|
|
2885
|
+
return first.name;
|
|
2886
|
+
}
|
|
2887
|
+
function countViolations(verdict) {
|
|
2888
|
+
return verdict.results.reduce(
|
|
2889
|
+
(sum, result2) => sum + result2.violations.length,
|
|
2890
|
+
0
|
|
2891
|
+
);
|
|
2892
|
+
}
|
|
2893
|
+
function getNextAction(status, replacements, unresolvedAmbiguityCount) {
|
|
2894
|
+
if (status === "pass") return "none";
|
|
2895
|
+
if (status === "fixed") return "apply_fixed_spec";
|
|
2896
|
+
if (status === "partial_fix") return "review_partial_fix";
|
|
2897
|
+
if (unresolvedAmbiguityCount > 0) return "resolve_ambiguities";
|
|
2898
|
+
if (replacements.length > 0) return "review_partial_fix";
|
|
2899
|
+
return "revise_input";
|
|
2900
|
+
}
|
|
2901
|
+
async function emitValidateAndFixTelemetry(args) {
|
|
2902
|
+
if (!args.ctx.mcp?.server) return;
|
|
2903
|
+
try {
|
|
2904
|
+
await args.ctx.mcp.server.sendLoggingMessage({
|
|
2905
|
+
level: "info",
|
|
2906
|
+
data: JSON.stringify({
|
|
2907
|
+
tool: "validate_and_fix",
|
|
2908
|
+
status: args.status,
|
|
2909
|
+
nextAction: args.nextAction,
|
|
2910
|
+
resolutionPath: args.resolutionPath,
|
|
2911
|
+
evaluation: args.evaluation,
|
|
2912
|
+
attestation: args.attestation
|
|
2913
|
+
}),
|
|
2914
|
+
logger: "fragments-mcp"
|
|
2915
|
+
});
|
|
2916
|
+
} catch {
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
function buildAllowedComponents(ctx) {
|
|
2920
|
+
return buildEffectiveComponents(ctx).filter(({ selection }) => selection === "preferred" || selection === "allowed").map(({ component }) => component.name);
|
|
2921
|
+
}
|
|
2922
|
+
function runGovern(spec, ctx, policyOverrides) {
|
|
2923
|
+
return runSpecGovern(spec, {
|
|
2924
|
+
allowedComponents: buildAllowedComponents(ctx),
|
|
2925
|
+
tokenPrefix: ctx.data.tokens?.prefix,
|
|
2926
|
+
policy: policyOverrides
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2929
|
+
function walkNodes2(nodes, visitor, path = "nodes") {
|
|
2930
|
+
for (const [index, node] of nodes.entries()) {
|
|
2931
|
+
if (!isPlainObject(node)) continue;
|
|
2932
|
+
const nodeRef = `${path}[${index}]`;
|
|
2933
|
+
visitor(node, nodeRef);
|
|
2934
|
+
if (Array.isArray(node.children)) {
|
|
2935
|
+
walkNodes2(node.children, visitor, `${nodeRef}.children`);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2521
2938
|
}
|
|
2522
2939
|
function applyDeterministicReplacements(spec, ctx) {
|
|
2523
2940
|
const effectiveComponents = buildEffectiveComponents(ctx);
|
|
@@ -2538,39 +2955,241 @@ function applyDeterministicReplacements(spec, ctx) {
|
|
|
2538
2955
|
const fixedSpec = cloneSpec(spec);
|
|
2539
2956
|
const nodes = Array.isArray(fixedSpec.nodes) ? fixedSpec.nodes : [];
|
|
2540
2957
|
const replacements = [];
|
|
2541
|
-
|
|
2542
|
-
|
|
2958
|
+
const ambiguities = [];
|
|
2959
|
+
walkNodes2(nodes, (node, nodeRef) => {
|
|
2960
|
+
if (!node.type) return;
|
|
2543
2961
|
const componentEntries = byName.get(node.type) ?? [];
|
|
2544
2962
|
const component = componentEntries[0];
|
|
2545
2963
|
if (!component || component.selection !== "discouraged" && component.selection !== "forbidden") {
|
|
2546
|
-
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
const candidateEntries = (preferredByCategory.get(component.component.category ?? "uncategorized") ?? []).filter((candidate) => candidate !== component.component.name).map(
|
|
2967
|
+
(candidateName) => effectiveComponents.find(
|
|
2968
|
+
(entry) => entry.component.name === candidateName
|
|
2969
|
+
)
|
|
2970
|
+
).filter((entry) => Boolean(entry));
|
|
2971
|
+
const rankedCandidates = rankReplacementCandidates({
|
|
2972
|
+
source: component,
|
|
2973
|
+
candidates: candidateEntries,
|
|
2974
|
+
node
|
|
2975
|
+
});
|
|
2976
|
+
const replacement = chooseDeterministicCandidate(rankedCandidates);
|
|
2977
|
+
if (!replacement) {
|
|
2978
|
+
if (rankedCandidates.length > 1) {
|
|
2979
|
+
ambiguities.push({
|
|
2980
|
+
nodeId: node.id ?? node.type,
|
|
2981
|
+
nodeRef,
|
|
2982
|
+
from: component.component.name,
|
|
2983
|
+
candidates: rankedCandidates.map((candidate) => candidate.name),
|
|
2984
|
+
rankedCandidates,
|
|
2985
|
+
reason: "ambiguous_preferred_components"
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
return;
|
|
2547
2989
|
}
|
|
2548
|
-
const candidates = (preferredByCategory.get(component.component.category ?? "uncategorized") ?? []).filter((candidate) => candidate !== component.component.name);
|
|
2549
|
-
if (candidates.length !== 1) continue;
|
|
2550
|
-
const replacement = candidates[0];
|
|
2551
2990
|
replacements.push({
|
|
2552
2991
|
nodeId: node.id ?? node.type,
|
|
2553
2992
|
from: component.component.name,
|
|
2554
2993
|
to: replacement,
|
|
2555
|
-
reason: "deprecated_component"
|
|
2994
|
+
reason: "deprecated_component",
|
|
2995
|
+
mode: "deterministic"
|
|
2556
2996
|
});
|
|
2557
2997
|
node.type = replacement;
|
|
2558
|
-
}
|
|
2998
|
+
});
|
|
2559
2999
|
return {
|
|
2560
3000
|
fixedSpec,
|
|
2561
|
-
replacements
|
|
3001
|
+
replacements,
|
|
3002
|
+
ambiguities
|
|
2562
3003
|
};
|
|
2563
3004
|
}
|
|
3005
|
+
function applyReplacementAtRef(spec, nodeRef, replacement) {
|
|
3006
|
+
const nodes = Array.isArray(spec.nodes) ? spec.nodes : [];
|
|
3007
|
+
let applied = false;
|
|
3008
|
+
walkNodes2(nodes, (node, currentRef) => {
|
|
3009
|
+
if (applied || currentRef !== nodeRef) return;
|
|
3010
|
+
node.type = replacement;
|
|
3011
|
+
applied = true;
|
|
3012
|
+
});
|
|
3013
|
+
return applied;
|
|
3014
|
+
}
|
|
3015
|
+
function supportsFormElicitation(ctx) {
|
|
3016
|
+
const capabilities = ctx.mcp?.clientCapabilities?.elicitation;
|
|
3017
|
+
if (!capabilities) return false;
|
|
3018
|
+
return capabilities.form !== void 0 || capabilities.url === void 0;
|
|
3019
|
+
}
|
|
3020
|
+
function supportsSampling(ctx) {
|
|
3021
|
+
return Boolean(ctx.mcp?.clientCapabilities?.sampling);
|
|
3022
|
+
}
|
|
3023
|
+
function getSamplingText(response) {
|
|
3024
|
+
if (Array.isArray(response.content)) {
|
|
3025
|
+
return response.content.find((item) => item.type === "text")?.text;
|
|
3026
|
+
}
|
|
3027
|
+
return response.content.type === "text" ? response.content.text : void 0;
|
|
3028
|
+
}
|
|
3029
|
+
function buildComponentGuidanceMap(ctx) {
|
|
3030
|
+
return new Map(
|
|
3031
|
+
Object.values(ctx.data.components).map((component) => {
|
|
3032
|
+
const guidance = [];
|
|
3033
|
+
if (component.description) {
|
|
3034
|
+
guidance.push(`description: ${component.description}`);
|
|
3035
|
+
}
|
|
3036
|
+
if (component.guidance.usageGuidance) {
|
|
3037
|
+
guidance.push(`usage: ${component.guidance.usageGuidance}`);
|
|
3038
|
+
}
|
|
3039
|
+
if (component.guidance.dos.length > 0) {
|
|
3040
|
+
guidance.push(`dos: ${component.guidance.dos.join("; ")}`);
|
|
3041
|
+
}
|
|
3042
|
+
if (component.guidance.donts.length > 0) {
|
|
3043
|
+
guidance.push(`donts: ${component.guidance.donts.join("; ")}`);
|
|
3044
|
+
}
|
|
3045
|
+
return [component.name, guidance];
|
|
3046
|
+
})
|
|
3047
|
+
);
|
|
3048
|
+
}
|
|
3049
|
+
function buildAmbiguityGuidanceLines(candidates, guidanceByName) {
|
|
3050
|
+
return candidates.map((candidate) => {
|
|
3051
|
+
const guidance = guidanceByName.get(candidate) ?? [];
|
|
3052
|
+
return guidance.length > 0 ? `${candidate}: ${guidance.join(" | ")}` : `${candidate}: no additional guidance available`;
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
async function resolveAmbiguitiesWithElicitation(ambiguities, spec, ctx) {
|
|
3056
|
+
if (!ctx.mcp?.server || ambiguities.length === 0) return [];
|
|
3057
|
+
const replacements = [];
|
|
3058
|
+
const guidanceByName = buildComponentGuidanceMap(ctx);
|
|
3059
|
+
for (const ambiguity of ambiguities) {
|
|
3060
|
+
const guidanceLines = buildAmbiguityGuidanceLines(
|
|
3061
|
+
ambiguity.candidates,
|
|
3062
|
+
guidanceByName
|
|
3063
|
+
);
|
|
3064
|
+
const result2 = await ctx.mcp.server.elicitInput({
|
|
3065
|
+
mode: "form",
|
|
3066
|
+
message: [
|
|
3067
|
+
`Fragments found multiple safe replacements for ${ambiguity.from} on node ${ambiguity.nodeId}.`,
|
|
3068
|
+
"Choose the best replacement, or skip this change.",
|
|
3069
|
+
"Candidate guidance:",
|
|
3070
|
+
...guidanceLines
|
|
3071
|
+
].join("\n"),
|
|
3072
|
+
requestedSchema: {
|
|
3073
|
+
type: "object",
|
|
3074
|
+
properties: {
|
|
3075
|
+
replacement: {
|
|
3076
|
+
type: "string",
|
|
3077
|
+
title: "Replacement",
|
|
3078
|
+
description: [
|
|
3079
|
+
"Select the best replacement for this node.",
|
|
3080
|
+
...guidanceLines,
|
|
3081
|
+
"Choose Skip to leave this component unchanged."
|
|
3082
|
+
].join("\n"),
|
|
3083
|
+
oneOf: [
|
|
3084
|
+
...ambiguity.candidates.map((candidate) => ({
|
|
3085
|
+
const: candidate,
|
|
3086
|
+
title: candidate
|
|
3087
|
+
})),
|
|
3088
|
+
{
|
|
3089
|
+
const: "__skip__",
|
|
3090
|
+
title: "Skip"
|
|
3091
|
+
}
|
|
3092
|
+
],
|
|
3093
|
+
default: "__skip__"
|
|
3094
|
+
}
|
|
3095
|
+
},
|
|
3096
|
+
required: ["replacement"]
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3099
|
+
if (result2.action !== "accept" || !result2.content || typeof result2.content.replacement !== "string" || result2.content.replacement === "__skip__") {
|
|
3100
|
+
continue;
|
|
3101
|
+
}
|
|
3102
|
+
if (!ambiguity.candidates.includes(result2.content.replacement)) {
|
|
3103
|
+
continue;
|
|
3104
|
+
}
|
|
3105
|
+
if (!applyReplacementAtRef(spec, ambiguity.nodeRef, result2.content.replacement)) {
|
|
3106
|
+
continue;
|
|
3107
|
+
}
|
|
3108
|
+
replacements.push({
|
|
3109
|
+
nodeId: ambiguity.nodeId,
|
|
3110
|
+
from: ambiguity.from,
|
|
3111
|
+
to: result2.content.replacement,
|
|
3112
|
+
reason: "deprecated_component",
|
|
3113
|
+
mode: "elicitation"
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
return replacements;
|
|
3117
|
+
}
|
|
3118
|
+
async function resolveAmbiguitiesWithSampling(ambiguities, spec, ctx) {
|
|
3119
|
+
if (!ctx.mcp?.server || ambiguities.length === 0) return [];
|
|
3120
|
+
const guidanceByName = buildComponentGuidanceMap(ctx);
|
|
3121
|
+
const prompt = [
|
|
3122
|
+
"You are choosing design-system component replacements.",
|
|
3123
|
+
'Return strict JSON only in the form {"choices":[{"nodeId":"...","replacement":"...|__skip__"}]}.',
|
|
3124
|
+
"Only choose from the provided candidates.",
|
|
3125
|
+
"Prefer the option whose guidance best matches the original component intent.",
|
|
3126
|
+
JSON.stringify(
|
|
3127
|
+
{
|
|
3128
|
+
ambiguities: ambiguities.map((ambiguity) => ({
|
|
3129
|
+
nodeId: ambiguity.nodeId,
|
|
3130
|
+
from: ambiguity.from,
|
|
3131
|
+
candidates: ambiguity.candidates.map((candidate) => ({
|
|
3132
|
+
name: candidate,
|
|
3133
|
+
guidance: guidanceByName.get(candidate) ?? []
|
|
3134
|
+
}))
|
|
3135
|
+
}))
|
|
3136
|
+
},
|
|
3137
|
+
null,
|
|
3138
|
+
2
|
|
3139
|
+
)
|
|
3140
|
+
].join("\n\n");
|
|
3141
|
+
const response = await ctx.mcp.server.createMessage({
|
|
3142
|
+
messages: [
|
|
3143
|
+
{
|
|
3144
|
+
role: "user",
|
|
3145
|
+
content: {
|
|
3146
|
+
type: "text",
|
|
3147
|
+
text: prompt
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
],
|
|
3151
|
+
maxTokens: 600
|
|
3152
|
+
});
|
|
3153
|
+
const text = getSamplingText(response);
|
|
3154
|
+
if (!text) return [];
|
|
3155
|
+
let parsed;
|
|
3156
|
+
try {
|
|
3157
|
+
parsed = JSON.parse(text);
|
|
3158
|
+
} catch {
|
|
3159
|
+
return [];
|
|
3160
|
+
}
|
|
3161
|
+
const replacements = [];
|
|
3162
|
+
for (const choice of parsed.choices ?? []) {
|
|
3163
|
+
if (!choice.nodeId || !choice.replacement || choice.replacement === "__skip__") {
|
|
3164
|
+
continue;
|
|
3165
|
+
}
|
|
3166
|
+
const ambiguity = ambiguities.find((entry) => entry.nodeId === choice.nodeId);
|
|
3167
|
+
if (!ambiguity || !ambiguity.candidates.includes(choice.replacement)) {
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
if (!applyReplacementAtRef(spec, ambiguity.nodeRef, choice.replacement)) {
|
|
3171
|
+
continue;
|
|
3172
|
+
}
|
|
3173
|
+
replacements.push({
|
|
3174
|
+
nodeId: ambiguity.nodeId,
|
|
3175
|
+
from: ambiguity.from,
|
|
3176
|
+
to: choice.replacement,
|
|
3177
|
+
reason: "deprecated_component",
|
|
3178
|
+
mode: "sampling"
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
return replacements;
|
|
3182
|
+
}
|
|
2564
3183
|
function buildSummary(args) {
|
|
2565
3184
|
const lines = [
|
|
2566
3185
|
`status: ${args.status}`,
|
|
2567
|
-
`original: ${
|
|
3186
|
+
`original: ${formatVerdict(args.originalVerdict, "summary")}`
|
|
2568
3187
|
];
|
|
2569
3188
|
if (args.replacements.length > 0) {
|
|
2570
3189
|
lines.push(
|
|
2571
3190
|
`replacements: ${args.replacements.map((item) => `${item.from} -> ${item.to}`).join(", ")}`
|
|
2572
3191
|
);
|
|
2573
|
-
lines.push(`final: ${
|
|
3192
|
+
lines.push(`final: ${formatVerdict(args.finalVerdict, "summary")}`);
|
|
2574
3193
|
}
|
|
2575
3194
|
return lines.join("\n");
|
|
2576
3195
|
}
|
|
@@ -2589,43 +3208,166 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
2589
3208
|
isError: true
|
|
2590
3209
|
};
|
|
2591
3210
|
}
|
|
3211
|
+
const specError = validateSpecShape(spec);
|
|
3212
|
+
if (specError) {
|
|
3213
|
+
return {
|
|
3214
|
+
content: [
|
|
3215
|
+
{
|
|
3216
|
+
type: "text",
|
|
3217
|
+
text: JSON.stringify({
|
|
3218
|
+
error: `Invalid spec format: ${specError}. Expected: { nodes: [{ id?: string, type?: string, props?: object, children?: object[] }] }`
|
|
3219
|
+
})
|
|
3220
|
+
}
|
|
3221
|
+
],
|
|
3222
|
+
isError: true
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
2592
3225
|
const policyOverrides = args?.policy;
|
|
2593
3226
|
const applyFixes = args?.applyFixes !== false;
|
|
3227
|
+
const allowElicitation = args?.allowElicitation !== false;
|
|
3228
|
+
const allowSampling = args?.allowSampling !== false;
|
|
2594
3229
|
const format = args?.format ?? "json";
|
|
3230
|
+
const startedAt = Date.now();
|
|
2595
3231
|
try {
|
|
2596
3232
|
const effectiveComponents = buildEffectiveComponents(ctx);
|
|
2597
3233
|
const originalVerdict = await runGovern(spec, ctx, policyOverrides);
|
|
2598
3234
|
let finalVerdict = originalVerdict;
|
|
3235
|
+
let workingSpec;
|
|
2599
3236
|
let fixedSpec;
|
|
2600
3237
|
let replacements = [];
|
|
3238
|
+
let ambiguities = [];
|
|
3239
|
+
const resolutionPath = [];
|
|
2601
3240
|
if (!originalVerdict.passed && applyFixes) {
|
|
2602
|
-
const
|
|
3241
|
+
const result2 = applyDeterministicReplacements(
|
|
2603
3242
|
spec,
|
|
2604
3243
|
ctx
|
|
2605
3244
|
);
|
|
2606
|
-
replacements =
|
|
2607
|
-
|
|
3245
|
+
replacements = result2.replacements;
|
|
3246
|
+
ambiguities = result2.ambiguities;
|
|
2608
3247
|
if (replacements.length > 0) {
|
|
3248
|
+
workingSpec = result2.fixedSpec;
|
|
3249
|
+
resolutionPath.push("deterministic");
|
|
3250
|
+
}
|
|
3251
|
+
const unresolvedAmbiguities2 = ambiguities.filter(
|
|
3252
|
+
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
3253
|
+
);
|
|
3254
|
+
if (unresolvedAmbiguities2.length > 0 && allowElicitation && supportsFormElicitation(ctx)) {
|
|
3255
|
+
workingSpec ??= cloneSpec(spec);
|
|
3256
|
+
const elicitedReplacements = await resolveAmbiguitiesWithElicitation(
|
|
3257
|
+
unresolvedAmbiguities2,
|
|
3258
|
+
workingSpec,
|
|
3259
|
+
ctx
|
|
3260
|
+
);
|
|
3261
|
+
if (elicitedReplacements.length > 0) {
|
|
3262
|
+
resolutionPath.push("elicitation");
|
|
3263
|
+
}
|
|
3264
|
+
replacements.push(...elicitedReplacements);
|
|
3265
|
+
}
|
|
3266
|
+
const remainingAmbiguities = ambiguities.filter(
|
|
3267
|
+
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
3268
|
+
);
|
|
3269
|
+
if (remainingAmbiguities.length > 0 && allowSampling && supportsSampling(ctx)) {
|
|
3270
|
+
workingSpec ??= cloneSpec(spec);
|
|
3271
|
+
const sampledReplacements = await resolveAmbiguitiesWithSampling(
|
|
3272
|
+
remainingAmbiguities,
|
|
3273
|
+
workingSpec,
|
|
3274
|
+
ctx
|
|
3275
|
+
);
|
|
3276
|
+
if (sampledReplacements.length > 0) {
|
|
3277
|
+
resolutionPath.push("sampling");
|
|
3278
|
+
}
|
|
3279
|
+
replacements.push(...sampledReplacements);
|
|
3280
|
+
}
|
|
3281
|
+
fixedSpec = replacements.length > 0 ? workingSpec : void 0;
|
|
3282
|
+
if (fixedSpec) {
|
|
2609
3283
|
finalVerdict = await runGovern(fixedSpec, ctx, policyOverrides);
|
|
2610
3284
|
}
|
|
2611
3285
|
}
|
|
2612
|
-
const status = originalVerdict.passed ? "pass" : replacements.length > 0 ? "fixed" : "fail";
|
|
3286
|
+
const status = originalVerdict.passed ? "pass" : replacements.length > 0 ? finalVerdict.passed ? "fixed" : "partial_fix" : "fail";
|
|
3287
|
+
const unresolvedAmbiguities = ambiguities.filter(
|
|
3288
|
+
(ambiguity) => !replacements.some((replacement) => replacement.nodeId === ambiguity.nodeId)
|
|
3289
|
+
).map(({ nodeRef: _nodeRef, ...ambiguity }) => ambiguity);
|
|
3290
|
+
const attestation = {
|
|
3291
|
+
sourceType: ctx.data.snapshot.sourceType,
|
|
3292
|
+
sourceLabel: ctx.data.snapshot.sourceLabel,
|
|
3293
|
+
designSystemName: ctx.data.snapshot.metadata.designSystemName,
|
|
3294
|
+
packageName: ctx.data.snapshot.metadata.packageName,
|
|
3295
|
+
importPath: ctx.data.snapshot.metadata.importPath,
|
|
3296
|
+
catalogRevision: ctx.data.validateFixContext?.catalogRevision ?? ctx.data.snapshot.metadata.revision,
|
|
3297
|
+
catalogUpdatedAt: ctx.data.validateFixContext?.updatedAt ?? ctx.data.snapshot.metadata.updatedAt,
|
|
3298
|
+
policy: {
|
|
3299
|
+
mode: ctx.data.validateFixContext?.policy.mode ?? "embedded",
|
|
3300
|
+
endpoint: ctx.data.validateFixContext?.policy.endpoint,
|
|
3301
|
+
overrideApplied: Boolean(policyOverrides)
|
|
3302
|
+
},
|
|
3303
|
+
clientCapabilities: {
|
|
3304
|
+
sampling: supportsSampling(ctx),
|
|
3305
|
+
samplingTools: Boolean(ctx.mcp?.clientCapabilities?.sampling?.tools),
|
|
3306
|
+
elicitationForm: supportsFormElicitation(ctx),
|
|
3307
|
+
roots: Boolean(ctx.mcp?.clientCapabilities?.roots)
|
|
3308
|
+
},
|
|
3309
|
+
capabilitiesUsed: {
|
|
3310
|
+
deterministic: replacements.some((replacement) => replacement.mode === "deterministic"),
|
|
3311
|
+
elicitation: replacements.some((replacement) => replacement.mode === "elicitation"),
|
|
3312
|
+
sampling: replacements.some((replacement) => replacement.mode === "sampling")
|
|
3313
|
+
}
|
|
3314
|
+
};
|
|
3315
|
+
const evaluation = {
|
|
3316
|
+
durationMs: Date.now() - startedAt,
|
|
3317
|
+
originalViolationCount: countViolations(originalVerdict),
|
|
3318
|
+
finalViolationCount: countViolations(finalVerdict),
|
|
3319
|
+
originalPassed: originalVerdict.passed,
|
|
3320
|
+
finalPassed: finalVerdict.passed,
|
|
3321
|
+
replacementCount: replacements.length,
|
|
3322
|
+
replacementsByMode: {
|
|
3323
|
+
deterministic: replacements.filter((replacement) => replacement.mode === "deterministic").length,
|
|
3324
|
+
elicitation: replacements.filter((replacement) => replacement.mode === "elicitation").length,
|
|
3325
|
+
sampling: replacements.filter((replacement) => replacement.mode === "sampling").length
|
|
3326
|
+
},
|
|
3327
|
+
ambiguityCount: ambiguities.length,
|
|
3328
|
+
unresolvedAmbiguityCount: unresolvedAmbiguities.length,
|
|
3329
|
+
candidateRankingUsed: true
|
|
3330
|
+
};
|
|
3331
|
+
const nextAction = getNextAction(
|
|
3332
|
+
status,
|
|
3333
|
+
replacements,
|
|
3334
|
+
unresolvedAmbiguities.length
|
|
3335
|
+
);
|
|
2613
3336
|
const payload = {
|
|
2614
3337
|
status,
|
|
3338
|
+
nextAction,
|
|
2615
3339
|
applyFixes,
|
|
3340
|
+
allowElicitation,
|
|
3341
|
+
allowSampling,
|
|
3342
|
+
resolutionPath,
|
|
2616
3343
|
replacements,
|
|
2617
3344
|
originalVerdict,
|
|
2618
3345
|
finalVerdict,
|
|
2619
3346
|
...fixedSpec ? { fixedSpec } : {},
|
|
3347
|
+
attestation,
|
|
3348
|
+
evaluation,
|
|
2620
3349
|
preferredComponents: getSelectionNames(
|
|
2621
3350
|
effectiveComponents,
|
|
2622
3351
|
"preferred"
|
|
2623
3352
|
),
|
|
2624
|
-
discouragedComponents: getSelectionNames(
|
|
2625
|
-
|
|
3353
|
+
discouragedComponents: getSelectionNames(
|
|
3354
|
+
effectiveComponents,
|
|
3355
|
+
"discouraged"
|
|
3356
|
+
),
|
|
3357
|
+
forbiddenComponents: getSelectionNames(
|
|
3358
|
+
effectiveComponents,
|
|
2626
3359
|
"forbidden"
|
|
2627
|
-
|
|
3360
|
+
),
|
|
3361
|
+
unresolvedAmbiguities
|
|
2628
3362
|
};
|
|
3363
|
+
await emitValidateAndFixTelemetry({
|
|
3364
|
+
ctx,
|
|
3365
|
+
status,
|
|
3366
|
+
resolutionPath,
|
|
3367
|
+
evaluation,
|
|
3368
|
+
attestation,
|
|
3369
|
+
nextAction
|
|
3370
|
+
});
|
|
2629
3371
|
return {
|
|
2630
3372
|
content: [
|
|
2631
3373
|
{
|
|
@@ -2640,19 +3382,22 @@ var validateAndFixHandler = async (args, ctx) => {
|
|
|
2640
3382
|
],
|
|
2641
3383
|
_meta: {
|
|
2642
3384
|
status,
|
|
3385
|
+
nextAction,
|
|
2643
3386
|
replacementCount: replacements.length,
|
|
2644
|
-
passed: finalVerdict.passed
|
|
3387
|
+
passed: finalVerdict.passed,
|
|
3388
|
+
unresolvedAmbiguityCount: unresolvedAmbiguities.length,
|
|
3389
|
+
resolutionPath,
|
|
3390
|
+
catalogRevision: attestation.catalogRevision
|
|
2645
3391
|
}
|
|
2646
3392
|
};
|
|
2647
3393
|
} catch (error) {
|
|
2648
3394
|
const message = error instanceof Error ? error.message : String(error);
|
|
2649
|
-
const isSpecError = message.includes("Expected") || message.includes("Required");
|
|
2650
3395
|
return {
|
|
2651
3396
|
content: [
|
|
2652
3397
|
{
|
|
2653
3398
|
type: "text",
|
|
2654
3399
|
text: JSON.stringify({
|
|
2655
|
-
error:
|
|
3400
|
+
error: message
|
|
2656
3401
|
})
|
|
2657
3402
|
}
|
|
2658
3403
|
],
|
|
@@ -2764,10 +3509,10 @@ var findingsListHandler = async (args, ctx) => {
|
|
|
2764
3509
|
if (args.filePath) params.filePath = String(args.filePath);
|
|
2765
3510
|
if (args.limit != null) params.limit = Number(args.limit);
|
|
2766
3511
|
try {
|
|
2767
|
-
const
|
|
3512
|
+
const result2 = await fetchFindings(apiKey, params, cloudUrl);
|
|
2768
3513
|
return {
|
|
2769
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
2770
|
-
_meta: { count:
|
|
3514
|
+
content: [{ type: "text", text: JSON.stringify(result2) }],
|
|
3515
|
+
_meta: { count: result2.findings.length }
|
|
2771
3516
|
};
|
|
2772
3517
|
} catch (error) {
|
|
2773
3518
|
return {
|
|
@@ -2800,8 +3545,8 @@ var findingsForFileHandler = async (args, ctx) => {
|
|
|
2800
3545
|
}
|
|
2801
3546
|
const cloudUrl = resolveCloudUrl(ctx);
|
|
2802
3547
|
try {
|
|
2803
|
-
const
|
|
2804
|
-
const findings =
|
|
3548
|
+
const result2 = await fetchFindingsForFile(apiKey, filePath, cloudUrl);
|
|
3549
|
+
const findings = result2.findings.filter((f) => f.filePath === filePath);
|
|
2805
3550
|
return {
|
|
2806
3551
|
content: [
|
|
2807
3552
|
{
|
|
@@ -2995,9 +3740,9 @@ function executeWithMiddleware(middlewares, mCtx, handler) {
|
|
|
2995
3740
|
function telemetryMiddleware(logger) {
|
|
2996
3741
|
return async (mCtx, next) => {
|
|
2997
3742
|
const start = Date.now();
|
|
2998
|
-
const
|
|
3743
|
+
const result2 = await next();
|
|
2999
3744
|
logger(`${mCtx.toolKey}: ${Date.now() - start}ms`);
|
|
3000
|
-
return
|
|
3745
|
+
return result2;
|
|
3001
3746
|
};
|
|
3002
3747
|
}
|
|
3003
3748
|
|
|
@@ -3838,7 +4583,7 @@ Check that your tsconfig.json includes the component directories.`
|
|
|
3838
4583
|
var extractorModulePromise = null;
|
|
3839
4584
|
async function loadExtractorModule() {
|
|
3840
4585
|
if (!extractorModulePromise) {
|
|
3841
|
-
extractorModulePromise = import("./dist-
|
|
4586
|
+
extractorModulePromise = import("./dist-BDWAHJ4K.js");
|
|
3842
4587
|
}
|
|
3843
4588
|
return extractorModulePromise;
|
|
3844
4589
|
}
|
|
@@ -4122,6 +4867,19 @@ function readPackageName(projectRoot) {
|
|
|
4122
4867
|
|
|
4123
4868
|
// src/adapters/cloud-catalog.ts
|
|
4124
4869
|
var DEFAULT_CLOUD_URL2 = "https://app.usefragments.com/api/catalog";
|
|
4870
|
+
function mergeDesignSystemMetadata(catalogDesignSystem, contextDesignSystem) {
|
|
4871
|
+
return {
|
|
4872
|
+
name: catalogDesignSystem?.name ?? contextDesignSystem?.name,
|
|
4873
|
+
packageName: catalogDesignSystem?.packageName ?? contextDesignSystem?.packageName ?? null,
|
|
4874
|
+
importPath: catalogDesignSystem?.importPath ?? contextDesignSystem?.importPath ?? catalogDesignSystem?.packageName ?? contextDesignSystem?.packageName ?? null
|
|
4875
|
+
};
|
|
4876
|
+
}
|
|
4877
|
+
function chooseComponentSource(args) {
|
|
4878
|
+
if (args.contextComponents.length > args.catalogComponents.length) {
|
|
4879
|
+
return args.contextComponents;
|
|
4880
|
+
}
|
|
4881
|
+
return args.catalogComponents;
|
|
4882
|
+
}
|
|
4125
4883
|
var TOKEN_CATEGORY_ALIASES2 = {
|
|
4126
4884
|
color: ["color", "colors", "accent", "background", "foreground", "danger", "brand"],
|
|
4127
4885
|
spacing: ["spacing", "space", "padding", "margin", "gap", "inset"],
|
|
@@ -4246,8 +5004,8 @@ function mapComponent(component, designSystem) {
|
|
|
4246
5004
|
sourceType: "cloud",
|
|
4247
5005
|
sourcePath: component.sourcePath,
|
|
4248
5006
|
sourceRepoFullName: component.sourceRepoFullName ?? void 0,
|
|
4249
|
-
packageName: designSystem?.packageName ?? void 0,
|
|
4250
|
-
importPath: designSystem?.importPath ?? designSystem?.packageName ?? void 0,
|
|
5007
|
+
packageName: component.packageName ?? designSystem?.packageName ?? void 0,
|
|
5008
|
+
importPath: component.importPath ?? component.packageName ?? designSystem?.importPath ?? designSystem?.packageName ?? void 0,
|
|
4251
5009
|
publicRef: component.publicRef,
|
|
4252
5010
|
publicSlug: component.publicSlug ?? null,
|
|
4253
5011
|
isCanonical: component.isCanonical ?? false,
|
|
@@ -4314,21 +5072,32 @@ var CloudCatalogAdapter = class {
|
|
|
4314
5072
|
);
|
|
4315
5073
|
}
|
|
4316
5074
|
const raw = await response.json();
|
|
5075
|
+
const validateFixRaw = validateFixResponse && validateFixResponse.ok ? await validateFixResponse.json() : void 0;
|
|
5076
|
+
const designSystem = mergeDesignSystemMetadata(
|
|
5077
|
+
raw.designSystem,
|
|
5078
|
+
validateFixRaw?.content?.designSystem
|
|
5079
|
+
);
|
|
5080
|
+
const sourceComponents = chooseComponentSource({
|
|
5081
|
+
catalogComponents: raw.components ?? [],
|
|
5082
|
+
contextComponents: validateFixRaw?.content?.components ?? []
|
|
5083
|
+
});
|
|
4317
5084
|
const components = Object.fromEntries(
|
|
4318
|
-
|
|
5085
|
+
sourceComponents.map((component) => [
|
|
4319
5086
|
component.componentKey,
|
|
4320
|
-
mapComponent(component,
|
|
5087
|
+
mapComponent(component, designSystem)
|
|
4321
5088
|
])
|
|
4322
5089
|
);
|
|
4323
5090
|
const tokens = raw.tokens?.flat ? groupTokens(raw.tokens.flat) : void 0;
|
|
4324
|
-
const packageName =
|
|
4325
|
-
const importPath =
|
|
4326
|
-
const packageMap =
|
|
5091
|
+
const packageName = designSystem?.packageName ?? void 0;
|
|
5092
|
+
const importPath = designSystem?.importPath ?? designSystem?.packageName ?? void 0;
|
|
5093
|
+
const packageMap = Object.fromEntries(
|
|
4327
5094
|
Object.values(components).map((component) => [
|
|
4328
5095
|
component.name,
|
|
4329
|
-
packageName
|
|
4330
|
-
])
|
|
4331
|
-
|
|
5096
|
+
component.importPath ?? component.packageName ?? packageName
|
|
5097
|
+
]).filter(
|
|
5098
|
+
(entry) => typeof entry[1] === "string" && entry[1].length > 0
|
|
5099
|
+
)
|
|
5100
|
+
);
|
|
4332
5101
|
const snapshot = validateSnapshot({
|
|
4333
5102
|
schemaVersion: 1,
|
|
4334
5103
|
sourceType: "cloud",
|
|
@@ -4338,7 +5107,7 @@ var CloudCatalogAdapter = class {
|
|
|
4338
5107
|
tokens
|
|
4339
5108
|
}),
|
|
4340
5109
|
metadata: {
|
|
4341
|
-
designSystemName:
|
|
5110
|
+
designSystemName: designSystem?.name ?? raw.org.name,
|
|
4342
5111
|
packageName,
|
|
4343
5112
|
importPath,
|
|
4344
5113
|
revision: raw.revision,
|
|
@@ -4349,9 +5118,7 @@ var CloudCatalogAdapter = class {
|
|
|
4349
5118
|
packageMap,
|
|
4350
5119
|
defaultPackageName: packageName
|
|
4351
5120
|
});
|
|
4352
|
-
const validateFixContext =
|
|
4353
|
-
await validateFixResponse.json()
|
|
4354
|
-
) : void 0;
|
|
5121
|
+
const validateFixContext = validateFixRaw ? normalizeValidateFixContext(validateFixRaw) : void 0;
|
|
4355
5122
|
const hydratedComponents = Object.fromEntries(
|
|
4356
5123
|
Object.entries(snapshot.components).map(([componentId, component]) => [
|
|
4357
5124
|
componentId,
|
|
@@ -4690,7 +5457,8 @@ function createMcpServer(config) {
|
|
|
4690
5457
|
},
|
|
4691
5458
|
{
|
|
4692
5459
|
capabilities: {
|
|
4693
|
-
tools: { listChanged: true }
|
|
5460
|
+
tools: { listChanged: true },
|
|
5461
|
+
logging: {}
|
|
4694
5462
|
}
|
|
4695
5463
|
}
|
|
4696
5464
|
);
|
|
@@ -4728,9 +5496,9 @@ function createMcpServer(config) {
|
|
|
4728
5496
|
if (resolveProjectRootPromise) return resolveProjectRootPromise;
|
|
4729
5497
|
resolveProjectRootPromise = (async () => {
|
|
4730
5498
|
try {
|
|
4731
|
-
const
|
|
4732
|
-
if (
|
|
4733
|
-
const rootUri =
|
|
5499
|
+
const result2 = await server.listRoots();
|
|
5500
|
+
if (result2.roots?.length > 0) {
|
|
5501
|
+
const rootUri = result2.roots[0].uri;
|
|
4734
5502
|
resolvedRoot = fileURLToPath(rootUri);
|
|
4735
5503
|
return resolvedRoot;
|
|
4736
5504
|
}
|
|
@@ -4785,12 +5553,19 @@ function createMcpServer(config) {
|
|
|
4785
5553
|
data,
|
|
4786
5554
|
config: mergedConfig,
|
|
4787
5555
|
indexes: { componentIndex, blockIndex, tokenIndex },
|
|
5556
|
+
mcp: {
|
|
5557
|
+
server,
|
|
5558
|
+
clientCapabilities: server.getClientCapabilities()
|
|
5559
|
+
},
|
|
4788
5560
|
resolvePackageName: (name2) => {
|
|
4789
5561
|
if (name2) {
|
|
4790
5562
|
const pkg = data.packageMap[name2];
|
|
4791
5563
|
if (pkg) return pkg;
|
|
4792
5564
|
}
|
|
4793
5565
|
if (data.defaultPackageName) return data.defaultPackageName;
|
|
5566
|
+
if (data.snapshot.sourceType === "cloud") {
|
|
5567
|
+
return "your-component-library";
|
|
5568
|
+
}
|
|
4794
5569
|
const root = resolvedRoot ?? config.projectRoot;
|
|
4795
5570
|
const packageJsonPath = join8(root, "package.json");
|
|
4796
5571
|
if (existsSync9(packageJsonPath)) {
|
|
@@ -4882,4 +5657,4 @@ export {
|
|
|
4882
5657
|
startMcpServer,
|
|
4883
5658
|
createSandboxServer
|
|
4884
5659
|
};
|
|
4885
|
-
//# sourceMappingURL=chunk-
|
|
5660
|
+
//# sourceMappingURL=chunk-YSNIGHNU.js.map
|