@cyclonedx/cdxgen 12.3.1 → 12.3.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.
Files changed (56) hide show
  1. package/bin/cdxgen.js +1 -2
  2. package/data/rules/ai-agent-governance.yaml +43 -0
  3. package/data/rules/mcp-servers.yaml +36 -2
  4. package/lib/cli/index.js +295 -17
  5. package/lib/cli/index.poku.js +296 -1
  6. package/lib/helpers/agentFormulationParser.js +4 -1
  7. package/lib/helpers/aiInventory.js +262 -0
  8. package/lib/helpers/aiInventory.poku.js +111 -0
  9. package/lib/helpers/analyzer.js +375 -45
  10. package/lib/helpers/analyzer.poku.js +50 -0
  11. package/lib/helpers/auditCategories.js +76 -0
  12. package/lib/helpers/display.js +5 -2
  13. package/lib/helpers/display.poku.js +25 -0
  14. package/lib/helpers/formulationParsers.js +26 -6
  15. package/lib/helpers/jsonLike.js +21 -20
  16. package/lib/helpers/jsonLike.poku.js +34 -0
  17. package/lib/helpers/mcpConfigParser.js +11 -11
  18. package/lib/helpers/mcpConfigParser.poku.js +67 -0
  19. package/lib/helpers/mcpDiscovery.js +13 -23
  20. package/lib/helpers/mcpDiscovery.poku.js +21 -0
  21. package/lib/helpers/utils.js +2 -1
  22. package/lib/helpers/utils.poku.js +19 -1
  23. package/lib/stages/postgen/annotator.js +2 -1
  24. package/lib/stages/postgen/annotator.poku.js +15 -0
  25. package/lib/stages/postgen/auditBom.js +12 -6
  26. package/lib/stages/postgen/auditBom.poku.js +111 -4
  27. package/lib/stages/postgen/postgen.js +229 -6
  28. package/lib/stages/postgen/postgen.poku.js +180 -0
  29. package/package.json +1 -1
  30. package/types/lib/cli/index.d.ts +1 -0
  31. package/types/lib/cli/index.d.ts.map +1 -1
  32. package/types/lib/helpers/agentFormulationParser.d.ts +19 -0
  33. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -0
  34. package/types/lib/helpers/aiInventory.d.ts +23 -0
  35. package/types/lib/helpers/aiInventory.d.ts.map +1 -0
  36. package/types/lib/helpers/analyzer.d.ts +5 -0
  37. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  38. package/types/lib/helpers/auditCategories.d.ts +12 -0
  39. package/types/lib/helpers/auditCategories.d.ts.map +1 -0
  40. package/types/lib/helpers/communityAiConfigParser.d.ts +29 -0
  41. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -0
  42. package/types/lib/helpers/display.d.ts.map +1 -1
  43. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  44. package/types/lib/helpers/jsonLike.d.ts +4 -0
  45. package/types/lib/helpers/jsonLike.d.ts.map +1 -0
  46. package/types/lib/helpers/mcp.d.ts +29 -0
  47. package/types/lib/helpers/mcp.d.ts.map +1 -0
  48. package/types/lib/helpers/mcpConfigParser.d.ts +30 -0
  49. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -0
  50. package/types/lib/helpers/mcpDiscovery.d.ts +5 -0
  51. package/types/lib/helpers/mcpDiscovery.d.ts.map +1 -0
  52. package/types/lib/helpers/utils.d.ts +2 -0
  53. package/types/lib/helpers/utils.d.ts.map +1 -1
  54. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  55. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  56. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
@@ -16,7 +16,12 @@ import sinon from "sinon";
16
16
 
17
17
  import { auditBom } from "../stages/postgen/auditBom.js";
18
18
  import { postProcess } from "../stages/postgen/postgen.js";
19
- import { createBom, createChromeExtensionBom, createRustBom } from "./index.js";
19
+ import {
20
+ createBom,
21
+ createChromeExtensionBom,
22
+ createNodejsBom,
23
+ createRustBom,
24
+ } from "./index.js";
20
25
 
21
26
  const fixtureDir = join(
22
27
  dirname(fileURLToPath(import.meta.url)),
@@ -54,6 +59,10 @@ const mcpFixtureDir = join(
54
59
  "mcp-repotest",
55
60
  );
56
61
 
62
+ function getProp(obj, name) {
63
+ return obj?.properties?.find((property) => property.name === name)?.value;
64
+ }
65
+
57
66
  describe("CLI tests", () => {
58
67
  describe("submitBom()", () => {
59
68
  it("should report blocked Dependency-Track submission during dry-run", async () => {
@@ -902,6 +911,32 @@ checksum = "${"a".repeat(64)}"
902
911
  assert.ok(findings.some((finding) => finding.ruleId === "MCP-003"));
903
912
  });
904
913
 
914
+ it("supports the ai-inventory audit category alias for MCP discovery", async () => {
915
+ const options = {
916
+ bomAudit: true,
917
+ bomAuditCategories: "ai-inventory",
918
+ bomAuditMinSeverity: "low",
919
+ failOnError: true,
920
+ installDeps: false,
921
+ multiProject: false,
922
+ projectType: ["js"],
923
+ specVersion: 1.7,
924
+ };
925
+ const bomNSData = await createBom(mcpFixtureDir, options);
926
+ const processedBomNSData = postProcess(bomNSData, options, mcpFixtureDir);
927
+ const bomJson = processedBomNSData?.bomJson || {};
928
+ assert.ok(
929
+ (bomJson.services || []).some(
930
+ (service) => service.name === "unsafe-http-server",
931
+ ),
932
+ );
933
+ const findings = await auditBom(bomJson, {
934
+ bomAuditCategories: "ai-inventory",
935
+ bomAuditMinSeverity: "low",
936
+ });
937
+ assert.ok(findings.some((finding) => finding.ruleId === "MCP-001"));
938
+ });
939
+
905
940
  it("supports the dedicated mcp project type alias", async () => {
906
941
  const options = {
907
942
  bomAudit: false,
@@ -927,5 +962,265 @@ checksum = "${"a".repeat(64)}"
927
962
  ),
928
963
  );
929
964
  });
965
+
966
+ it("supports exact AI skill scans and js exclude-type filtering for AI inventory", async () => {
967
+ const tmpDir = mkdtempSync(join(tmpdir(), "cdxgen-ai-inventory-"));
968
+ mkdirSync(join(tmpDir, ".claude", "skills", "release"), {
969
+ recursive: true,
970
+ });
971
+ mkdirSync(join(tmpDir, ".vscode"), { recursive: true });
972
+ writeFileSync(
973
+ join(tmpDir, "package.json"),
974
+ JSON.stringify(
975
+ {
976
+ dependencies: {
977
+ "left-pad": "1.3.0",
978
+ },
979
+ name: "ai-inventory-demo",
980
+ version: "1.0.0",
981
+ },
982
+ null,
983
+ 2,
984
+ ),
985
+ );
986
+ writeFileSync(
987
+ join(tmpDir, "package-lock.json"),
988
+ JSON.stringify(
989
+ {
990
+ lockfileVersion: 3,
991
+ name: "ai-inventory-demo",
992
+ packages: {
993
+ "": {
994
+ dependencies: {
995
+ "left-pad": "1.3.0",
996
+ },
997
+ name: "ai-inventory-demo",
998
+ version: "1.0.0",
999
+ },
1000
+ "node_modules/left-pad": {
1001
+ resolved:
1002
+ "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
1003
+ version: "1.3.0",
1004
+ },
1005
+ },
1006
+ requires: true,
1007
+ version: "1.0.0",
1008
+ },
1009
+ null,
1010
+ 2,
1011
+ ),
1012
+ );
1013
+ writeFileSync(
1014
+ join(tmpDir, "CLAUDE.md"),
1015
+ "Use the release skill before publishing artifacts.",
1016
+ );
1017
+ writeFileSync(
1018
+ join(tmpDir, ".claude", "skills", "release", "SKILL.md"),
1019
+ [
1020
+ "---",
1021
+ "name: release",
1022
+ "description: Prepare release artifacts",
1023
+ "---",
1024
+ "Use this skill before shipping.",
1025
+ ].join("\n"),
1026
+ );
1027
+ writeFileSync(
1028
+ join(tmpDir, ".vscode", "mcp.json"),
1029
+ JSON.stringify(
1030
+ {
1031
+ mcpServers: {
1032
+ releaseDocs: {
1033
+ endpoint: "https://example.com/mcp",
1034
+ transport: "http",
1035
+ },
1036
+ },
1037
+ },
1038
+ null,
1039
+ 2,
1040
+ ),
1041
+ );
1042
+ writeFileSync(
1043
+ join(tmpDir, "pyproject.toml"),
1044
+ [
1045
+ "[project]",
1046
+ 'name = "demo-python-app"',
1047
+ 'version = "0.1.0"',
1048
+ 'requires-python = ">=3.10"',
1049
+ ].join("\n"),
1050
+ );
1051
+ writeFileSync(
1052
+ join(tmpDir, "server.py"),
1053
+ [
1054
+ "import mcp.server.stdio",
1055
+ "import mcp.types as mtypes",
1056
+ "from mcp.server import Server",
1057
+ "",
1058
+ 'server = Server("python-release-docs", version="0.2.0")',
1059
+ "",
1060
+ "@server.list_tools()",
1061
+ "async def handle_list_tools():",
1062
+ ' return [mtypes.Tool(name="summarize_vulns", description="Summarize vulns", inputSchema={"type": "object"})]',
1063
+ "",
1064
+ "async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):",
1065
+ " await server.run(read_stream, write_stream, None)",
1066
+ ].join("\n"),
1067
+ );
1068
+ try {
1069
+ const baseOptions = {
1070
+ installDeps: false,
1071
+ multiProject: false,
1072
+ specVersion: 1.7,
1073
+ };
1074
+ const jsOptions = {
1075
+ ...baseOptions,
1076
+ projectType: ["js"],
1077
+ };
1078
+ const jsBomJson = postProcess(
1079
+ await createBom(tmpDir, jsOptions),
1080
+ jsOptions,
1081
+ tmpDir,
1082
+ ).bomJson;
1083
+ assert.ok(
1084
+ (jsBomJson.components || []).some(
1085
+ (component) =>
1086
+ getProp(component, "cdx:file:kind") === "skill-file" &&
1087
+ getProp(component, "cdx:skill:name") === "release",
1088
+ ),
1089
+ "expected skill file in js scan",
1090
+ );
1091
+ assert.ok(
1092
+ (jsBomJson.components || []).some(
1093
+ (component) =>
1094
+ component.name === "CLAUDE.md" &&
1095
+ getProp(component, "cdx:file:kind") === "agent-instructions",
1096
+ ),
1097
+ "expected CLAUDE.md in js scan",
1098
+ );
1099
+ assert.ok(
1100
+ (jsBomJson.components || []).some(
1101
+ (component) => getProp(component, "cdx:file:kind") === "mcp-config",
1102
+ ),
1103
+ "expected MCP config in js scan",
1104
+ );
1105
+ assert.ok(
1106
+ (jsBomJson.services || []).some(
1107
+ (service) =>
1108
+ service.name === "releaseDocs" &&
1109
+ getProp(service, "cdx:mcp:inventorySource") === "config-file",
1110
+ ),
1111
+ "expected MCP config service in js scan",
1112
+ );
1113
+
1114
+ const dockerOptions = {
1115
+ ...baseOptions,
1116
+ projectType: ["js", "docker"],
1117
+ };
1118
+ const dockerBomJson = postProcess(
1119
+ await createNodejsBom(tmpDir, dockerOptions),
1120
+ dockerOptions,
1121
+ tmpDir,
1122
+ ).bomJson;
1123
+ assert.ok(
1124
+ (dockerBomJson.components || []).some(
1125
+ (component) =>
1126
+ getProp(component, "cdx:file:kind") === "skill-file" &&
1127
+ getProp(component, "cdx:skill:name") === "release",
1128
+ ),
1129
+ "expected skill file in docker js scan",
1130
+ );
1131
+ assert.ok(
1132
+ (dockerBomJson.components || []).some(
1133
+ (component) => getProp(component, "cdx:file:kind") === "mcp-config",
1134
+ ),
1135
+ "expected MCP config in docker js scan",
1136
+ );
1137
+
1138
+ const exactAiSkillOptions = {
1139
+ ...baseOptions,
1140
+ projectType: ["ai-skill"],
1141
+ };
1142
+ const aiSkillBomJson = postProcess(
1143
+ await createBom(tmpDir, exactAiSkillOptions),
1144
+ exactAiSkillOptions,
1145
+ tmpDir,
1146
+ ).bomJson;
1147
+ assert.ok(
1148
+ (aiSkillBomJson.components || []).some(
1149
+ (component) =>
1150
+ component.name === "CLAUDE.md" &&
1151
+ getProp(component, "cdx:file:kind") === "agent-instructions",
1152
+ ),
1153
+ "expected CLAUDE.md in exact ai-skill scan",
1154
+ );
1155
+ assert.ok(
1156
+ !(aiSkillBomJson.components || []).some(
1157
+ (component) => getProp(component, "cdx:file:kind") === "mcp-config",
1158
+ ),
1159
+ "did not expect MCP configs in exact ai-skill scan",
1160
+ );
1161
+
1162
+ const filteredOptions = {
1163
+ ...baseOptions,
1164
+ excludeType: ["ai-skill", "mcp"],
1165
+ projectType: ["js"],
1166
+ };
1167
+ const filteredBomJson = postProcess(
1168
+ await createBom(tmpDir, filteredOptions),
1169
+ filteredOptions,
1170
+ tmpDir,
1171
+ ).bomJson;
1172
+ assert.ok(
1173
+ !(filteredBomJson.components || []).some((component) =>
1174
+ ["agent-instructions", "mcp-config", "skill-file"].includes(
1175
+ getProp(component, "cdx:file:kind"),
1176
+ ),
1177
+ ),
1178
+ "did not expect AI inventory components after exclude-type filtering",
1179
+ );
1180
+ assert.ok(
1181
+ !(filteredBomJson.services || []).some((service) =>
1182
+ service.properties?.some((property) =>
1183
+ property.name.startsWith("cdx:mcp:"),
1184
+ ),
1185
+ ),
1186
+ "did not expect MCP services after exclude-type filtering",
1187
+ );
1188
+
1189
+ const pyOptions = {
1190
+ ...baseOptions,
1191
+ projectType: ["py"],
1192
+ };
1193
+ const pyBomJson = postProcess(
1194
+ await createBom(tmpDir, pyOptions),
1195
+ pyOptions,
1196
+ tmpDir,
1197
+ ).bomJson;
1198
+ assert.ok(
1199
+ (pyBomJson.components || []).some(
1200
+ (component) =>
1201
+ getProp(component, "cdx:file:kind") === "skill-file" &&
1202
+ getProp(component, "cdx:skill:name") === "release",
1203
+ ),
1204
+ "expected skill file in python scan",
1205
+ );
1206
+ assert.ok(
1207
+ (pyBomJson.components || []).some(
1208
+ (component) => getProp(component, "cdx:file:kind") === "mcp-config",
1209
+ ),
1210
+ "expected MCP config in python scan",
1211
+ );
1212
+ assert.ok(
1213
+ (pyBomJson.services || []).some(
1214
+ (service) =>
1215
+ service.name === "python-release-docs" &&
1216
+ getProp(service, "cdx:mcp:inventorySource") ===
1217
+ "source-code-analysis",
1218
+ ),
1219
+ "expected Python MCP service in python scan",
1220
+ );
1221
+ } finally {
1222
+ rmSync(tmpDir, { force: true, recursive: true });
1223
+ }
1224
+ });
930
1225
  });
931
1226
  });
@@ -167,7 +167,10 @@ export const agentFormulationParser = {
167
167
  packageRefs.length > 0 ||
168
168
  /\bmcp\b/i.test(raw) ||
169
169
  /modelcontextprotocol/i.test(raw);
170
- if (!hiddenUnicodeScan.hasHiddenUnicode && !hasMcpReferences) {
170
+ // Inventory all matched agent/instruction files, even when they do not
171
+ // yet contain hidden Unicode or explicit MCP references, so shipped files
172
+ // such as CLAUDE.md and AGENTS.md still surface in build/post-build BOMs.
173
+ if (!raw.trim().length) {
171
174
  continue;
172
175
  }
173
176
  const hiddenComponentKinds = [];
@@ -0,0 +1,262 @@
1
+ import { agentFormulationParser } from "./agentFormulationParser.js";
2
+ import { communityAiConfigParser } from "./communityAiConfigParser.js";
3
+ import { mergeServices, trimComponents } from "./depsUtils.js";
4
+ import { classifyMcpReference } from "./mcp.js";
5
+ import { mcpConfigParser } from "./mcpConfigParser.js";
6
+ import { getAllFiles } from "./utils.js";
7
+
8
+ export const AI_INVENTORY_PROJECT_TYPES = ["mcp", "ai-skill"];
9
+ export const AI_INSTRUCTION_FILE_KINDS = new Set([
10
+ "agent-config",
11
+ "agent-definition",
12
+ "agent-instructions",
13
+ "ai-agent-file",
14
+ "copilot-instructions",
15
+ "copilot-setup-workflow",
16
+ "crew-agent",
17
+ "crew-task",
18
+ "crew-tool",
19
+ "custom-command",
20
+ "custom-tool",
21
+ "graph-definition",
22
+ ]);
23
+ export const AI_SKILL_FILE_KIND = "skill-file";
24
+ export const MCP_CONFIG_FILE_KIND = "mcp-config";
25
+
26
+ const AI_INVENTORY_FILE_KINDS = new Set([
27
+ "agent-config",
28
+ "agent-definition",
29
+ "agent-instructions",
30
+ "ai-agent-file",
31
+ "copilot-instructions",
32
+ "copilot-setup-workflow",
33
+ "crew-agent",
34
+ "crew-task",
35
+ "crew-tool",
36
+ "custom-command",
37
+ "custom-tool",
38
+ "graph-definition",
39
+ AI_SKILL_FILE_KIND,
40
+ ]);
41
+
42
+ const AI_INVENTORY_PARSERS = [
43
+ {
44
+ id: agentFormulationParser.id,
45
+ parser: agentFormulationParser,
46
+ types: ["mcp", "ai-skill"],
47
+ },
48
+ {
49
+ id: mcpConfigParser.id,
50
+ parser: mcpConfigParser,
51
+ types: ["mcp"],
52
+ },
53
+ {
54
+ id: communityAiConfigParser.id,
55
+ parser: communityAiConfigParser,
56
+ types: ["ai-skill"],
57
+ },
58
+ ];
59
+
60
+ export function inventoryPropertyValue(subject, name) {
61
+ return subject?.properties?.find((property) => property.name === name)?.value;
62
+ }
63
+
64
+ function hasPropertyPrefix(subject, prefix) {
65
+ return (subject?.properties || []).some((property) =>
66
+ property?.name?.startsWith(prefix),
67
+ );
68
+ }
69
+
70
+ function uniqueNonEmptyTypes(types) {
71
+ return [...new Set((types || []).filter(Boolean))];
72
+ }
73
+
74
+ export function optionIncludesAiInventoryProjectType(optionValue, type) {
75
+ const values = Array.isArray(optionValue)
76
+ ? optionValue
77
+ : optionValue
78
+ ? [optionValue]
79
+ : [];
80
+ return values.some((value) => {
81
+ const normalizedValue = String(value).toLowerCase();
82
+ if (type === "ai-skill") {
83
+ return ["ai-skill", "skill", "skills"].includes(normalizedValue);
84
+ }
85
+ return normalizedValue === type;
86
+ });
87
+ }
88
+
89
+ export function inventoryTypesForSubject(subject) {
90
+ const types = new Set();
91
+ const fileKind = inventoryPropertyValue(subject, "cdx:file:kind");
92
+ if (
93
+ subject?.group === "mcp" ||
94
+ classifyMcpReference(subject).isMcp ||
95
+ hasPropertyPrefix(subject, "cdx:mcp:") ||
96
+ (subject?.tags || []).some((tag) => String(tag || "").startsWith("mcp"))
97
+ ) {
98
+ types.add("mcp");
99
+ }
100
+ if (
101
+ AI_INVENTORY_FILE_KINDS.has(fileKind) ||
102
+ hasPropertyPrefix(subject, "cdx:agent:") ||
103
+ hasPropertyPrefix(subject, "cdx:skill:") ||
104
+ hasPropertyPrefix(subject, "cdx:tool:") ||
105
+ hasPropertyPrefix(subject, "cdx:langgraph:") ||
106
+ hasPropertyPrefix(subject, "cdx:crewai:")
107
+ ) {
108
+ types.add("ai-skill");
109
+ }
110
+ if (
111
+ inventoryPropertyValue(subject, "cdx:mcp:inventorySource") === "agent-file"
112
+ ) {
113
+ types.add("ai-skill");
114
+ }
115
+ return Array.from(types);
116
+ }
117
+
118
+ export function matchesAiInventoryType(subject, type) {
119
+ return inventoryTypesForSubject(subject).includes(type);
120
+ }
121
+
122
+ export function matchesAiInventoryExcludeType(subject, type) {
123
+ if (type === "mcp") {
124
+ const fileKind = inventoryPropertyValue(subject, "cdx:file:kind");
125
+ return (
126
+ fileKind === MCP_CONFIG_FILE_KIND ||
127
+ subject?.group === "mcp" ||
128
+ inventoryPropertyValue(subject, "cdx:mcp:inventorySource") !==
129
+ undefined ||
130
+ inventoryPropertyValue(subject, "cdx:mcp:role") !== undefined
131
+ );
132
+ }
133
+ return matchesAiInventoryType(subject, type);
134
+ }
135
+
136
+ export function filterInventorySubjectsByTypes(subjects, types) {
137
+ const allowedTypes = uniqueNonEmptyTypes(types);
138
+ if (!allowedTypes.length) {
139
+ return [];
140
+ }
141
+ return (subjects || []).filter((subject) =>
142
+ inventoryTypesForSubject(subject).some((type) =>
143
+ allowedTypes.includes(type),
144
+ ),
145
+ );
146
+ }
147
+
148
+ export function filterInventoryDependencies(
149
+ dependencies,
150
+ components,
151
+ services,
152
+ ) {
153
+ const allowedRefs = new Set(
154
+ []
155
+ .concat(components || [])
156
+ .concat(services || [])
157
+ .map((subject) => subject?.["bom-ref"])
158
+ .filter(Boolean),
159
+ );
160
+ return (dependencies || [])
161
+ .filter((dependency) => allowedRefs.has(dependency.ref))
162
+ .map((dependency) => {
163
+ const filteredDependency = {
164
+ ref: dependency.ref,
165
+ };
166
+ if (dependency.dependsOn?.length) {
167
+ filteredDependency.dependsOn = dependency.dependsOn.filter((ref) =>
168
+ allowedRefs.has(ref),
169
+ );
170
+ }
171
+ if (dependency.provides?.length) {
172
+ filteredDependency.provides = dependency.provides.filter((ref) =>
173
+ allowedRefs.has(ref),
174
+ );
175
+ }
176
+ return filteredDependency;
177
+ });
178
+ }
179
+
180
+ export function collectAiInventory(discoveryPath, options, types) {
181
+ const requestedTypes = uniqueNonEmptyTypes(types);
182
+ if (!requestedTypes.length) {
183
+ return { components: [], dependencies: [], services: [] };
184
+ }
185
+ let components = [];
186
+ const dependencies = [];
187
+ let services = [];
188
+ for (const parserEntry of AI_INVENTORY_PARSERS) {
189
+ if (!parserEntry.types.some((type) => requestedTypes.includes(type))) {
190
+ continue;
191
+ }
192
+ const matchedFiles = [];
193
+ for (const pattern of parserEntry.parser.patterns) {
194
+ const found = getAllFiles(discoveryPath, pattern, options);
195
+ if (found?.length) {
196
+ matchedFiles.push(...found);
197
+ }
198
+ }
199
+ const uniqueMatchedFiles = [...new Set(matchedFiles)];
200
+ if (!uniqueMatchedFiles.length) {
201
+ continue;
202
+ }
203
+ let result;
204
+ try {
205
+ result = parserEntry.parser.parse(uniqueMatchedFiles, options);
206
+ } catch (err) {
207
+ console.warn(
208
+ `[aiInventory] Parser "${parserEntry.id}" threw an error:`,
209
+ err.message,
210
+ );
211
+ continue;
212
+ }
213
+ if (result?.components?.length) {
214
+ components = components.concat(result.components);
215
+ }
216
+ if (result?.services?.length) {
217
+ services = mergeServices(services, result.services);
218
+ }
219
+ if (result?.dependencies?.length) {
220
+ dependencies.push(...result.dependencies);
221
+ }
222
+ }
223
+ components = trimComponents(
224
+ filterInventorySubjectsByTypes(components, requestedTypes),
225
+ );
226
+ services = mergeServices(
227
+ [],
228
+ filterInventorySubjectsByTypes(services, requestedTypes),
229
+ );
230
+ return {
231
+ components,
232
+ dependencies: filterInventoryDependencies(
233
+ dependencies,
234
+ components,
235
+ services,
236
+ ),
237
+ services,
238
+ };
239
+ }
240
+
241
+ export function summarizeAiInventory(inventory) {
242
+ const components = inventory?.components || [];
243
+ const services = inventory?.services || [];
244
+ return {
245
+ instructionCount: components.filter((component) =>
246
+ AI_INSTRUCTION_FILE_KINDS.has(
247
+ inventoryPropertyValue(component, "cdx:file:kind"),
248
+ ),
249
+ ).length,
250
+ mcpConfigCount: components.filter(
251
+ (component) =>
252
+ inventoryPropertyValue(component, "cdx:file:kind") ===
253
+ MCP_CONFIG_FILE_KIND,
254
+ ).length,
255
+ mcpServiceCount: services.length,
256
+ skillCount: components.filter(
257
+ (component) =>
258
+ inventoryPropertyValue(component, "cdx:file:kind") ===
259
+ AI_SKILL_FILE_KIND,
260
+ ).length,
261
+ };
262
+ }
@@ -0,0 +1,111 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import {
4
+ filterInventoryDependencies,
5
+ inventoryTypesForSubject,
6
+ matchesAiInventoryExcludeType,
7
+ matchesAiInventoryType,
8
+ summarizeAiInventory,
9
+ } from "./aiInventory.js";
10
+
11
+ describe("aiInventory", () => {
12
+ it("classifies agent-derived MCP services as both mcp and ai-skill", () => {
13
+ const service = {
14
+ "bom-ref": "urn:service:agent-mcp:demo:1",
15
+ group: "mcp",
16
+ properties: [
17
+ { name: "cdx:mcp:inventorySource", value: "agent-file" },
18
+ { name: "cdx:mcp:serviceType", value: "inferred-endpoint" },
19
+ ],
20
+ };
21
+ assert.deepStrictEqual(inventoryTypesForSubject(service).sort(), [
22
+ "ai-skill",
23
+ "mcp",
24
+ ]);
25
+ assert.strictEqual(matchesAiInventoryType(service, "mcp"), true);
26
+ assert.strictEqual(matchesAiInventoryType(service, "ai-skill"), true);
27
+ });
28
+
29
+ it("limits MCP exclusion matching to AI inventory services, files, and primitives", () => {
30
+ const mcpPackage = {
31
+ "bom-ref": "pkg:npm/@modelcontextprotocol/server-filesystem@1.0.0",
32
+ name: "@modelcontextprotocol/server-filesystem",
33
+ purl: "pkg:npm/%40modelcontextprotocol/server-filesystem@1.0.0",
34
+ };
35
+ const mcpPrimitive = {
36
+ "bom-ref": "urn:mcp:tool:docs:search",
37
+ properties: [{ name: "cdx:mcp:role", value: "tool" }],
38
+ tags: ["mcp", "mcp-tool"],
39
+ };
40
+ const mcpConfig = {
41
+ "bom-ref": "file:/repo/.vscode/mcp.json",
42
+ properties: [{ name: "cdx:file:kind", value: "mcp-config" }],
43
+ type: "file",
44
+ };
45
+ const mcpService = {
46
+ "bom-ref": "urn:service:mcp:docs:latest",
47
+ group: "mcp",
48
+ properties: [{ name: "cdx:mcp:inventorySource", value: "config-file" }],
49
+ };
50
+ assert.strictEqual(matchesAiInventoryExcludeType(mcpPackage, "mcp"), false);
51
+ assert.strictEqual(
52
+ matchesAiInventoryExcludeType(mcpPrimitive, "mcp"),
53
+ true,
54
+ );
55
+ assert.strictEqual(matchesAiInventoryExcludeType(mcpConfig, "mcp"), true);
56
+ assert.strictEqual(matchesAiInventoryExcludeType(mcpService, "mcp"), true);
57
+ });
58
+
59
+ it("filters dependencies to retained component and service refs", () => {
60
+ const components = [{ "bom-ref": "file:/repo/CLAUDE.md" }];
61
+ const services = [{ "bom-ref": "urn:service:mcp:docs:latest" }];
62
+ const filtered = filterInventoryDependencies(
63
+ [
64
+ {
65
+ ref: "urn:service:mcp:docs:latest",
66
+ provides: ["file:/repo/CLAUDE.md", "urn:service:mcp:other:latest"],
67
+ },
68
+ {
69
+ ref: "urn:service:mcp:missing:latest",
70
+ provides: ["file:/repo/CLAUDE.md"],
71
+ },
72
+ ],
73
+ components,
74
+ services,
75
+ );
76
+ assert.deepStrictEqual(filtered, [
77
+ {
78
+ ref: "urn:service:mcp:docs:latest",
79
+ provides: ["file:/repo/CLAUDE.md"],
80
+ },
81
+ ]);
82
+ });
83
+
84
+ it("summarizes AI inventory counts for instructions, skills, configs, and services", () => {
85
+ const summary = summarizeAiInventory({
86
+ components: [
87
+ {
88
+ properties: [{ name: "cdx:file:kind", value: "agent-instructions" }],
89
+ },
90
+ {
91
+ properties: [
92
+ { name: "cdx:file:kind", value: "copilot-instructions" },
93
+ ],
94
+ },
95
+ {
96
+ properties: [{ name: "cdx:file:kind", value: "skill-file" }],
97
+ },
98
+ {
99
+ properties: [{ name: "cdx:file:kind", value: "mcp-config" }],
100
+ },
101
+ ],
102
+ services: [{ name: "releaseDocs" }, { name: "deployBot" }],
103
+ });
104
+ assert.deepStrictEqual(summary, {
105
+ instructionCount: 2,
106
+ mcpConfigCount: 1,
107
+ mcpServiceCount: 2,
108
+ skillCount: 1,
109
+ });
110
+ });
111
+ });