@xenonbyte/da-vinci-workflow 0.1.21 → 0.1.22

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.
@@ -0,0 +1,361 @@
1
+ const fs = require("fs");
2
+ const https = require("https");
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const {
6
+ MATERIAL_ROUNDED,
7
+ MATERIAL_OUTLINED,
8
+ MATERIAL_SHARP
9
+ } = require("./icon-search");
10
+
11
+ const MATERIAL_METADATA_URL = "https://fonts.google.com/metadata/icons";
12
+ const LUCIDE_TREE_URL = "https://api.github.com/repos/lucide-icons/lucide/git/trees/main?recursive=1";
13
+ const FEATHER_TREE_URL = "https://api.github.com/repos/feathericons/feather/git/trees/main?recursive=1";
14
+ const PHOSPHOR_TREE_URL = "https://api.github.com/repos/phosphor-icons/core/git/trees/main?recursive=1";
15
+ const DEFAULT_TIMEOUT_MS = 20000;
16
+
17
+ function toBoolean(value) {
18
+ if (value === true || value === false) {
19
+ return value;
20
+ }
21
+ if (typeof value === "string") {
22
+ const normalized = value.trim().toLowerCase();
23
+ if (["1", "true", "yes", "on"].includes(normalized)) {
24
+ return true;
25
+ }
26
+ if (["0", "false", "no", "off"].includes(normalized)) {
27
+ return false;
28
+ }
29
+ }
30
+ return Boolean(value);
31
+ }
32
+
33
+ function ensureDir(filePath) {
34
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
35
+ }
36
+
37
+ function normalizeName(name) {
38
+ return String(name || "").trim();
39
+ }
40
+
41
+ function makeIconRecord(family, name) {
42
+ const normalized = normalizeName(name);
43
+ if (!family || !normalized) {
44
+ return null;
45
+ }
46
+ return {
47
+ family,
48
+ name: normalized,
49
+ semantic: normalized.replace(/[_-]+/g, " "),
50
+ tags: []
51
+ };
52
+ }
53
+
54
+ function parseGoogleMaterialMetadata(raw) {
55
+ const text = String(raw || "");
56
+ const objectStart = text.indexOf("{");
57
+ if (objectStart < 0) {
58
+ throw new Error("Material metadata payload is not valid JSON.");
59
+ }
60
+ const parsed = JSON.parse(text.slice(objectStart));
61
+ const icons = Array.isArray(parsed.icons) ? parsed.icons : [];
62
+ const roundedFamilyKey = "Material Icons Round";
63
+ const outlinedFamilyKey = "Material Icons Outlined";
64
+ const sharpFamilyKey = "Material Icons Sharp";
65
+
66
+ return icons.flatMap((item) => {
67
+ const name = normalizeName(item && item.name);
68
+ if (!name) {
69
+ return [];
70
+ }
71
+ const unsupported = new Set(Array.isArray(item.unsupported_families) ? item.unsupported_families : []);
72
+ const records = [];
73
+
74
+ if (!unsupported.has(roundedFamilyKey)) {
75
+ records.push(makeIconRecord(MATERIAL_ROUNDED, name));
76
+ }
77
+ if (!unsupported.has(outlinedFamilyKey)) {
78
+ records.push(makeIconRecord(MATERIAL_OUTLINED, name));
79
+ }
80
+ if (!unsupported.has(sharpFamilyKey)) {
81
+ records.push(makeIconRecord(MATERIAL_SHARP, name));
82
+ }
83
+
84
+ return records.filter(Boolean);
85
+ });
86
+ }
87
+
88
+ function parseGitHubTreeIcons(raw, options) {
89
+ const { family, prefix, suffix } = options;
90
+ const parsed = JSON.parse(String(raw || ""));
91
+ const tree = Array.isArray(parsed.tree) ? parsed.tree : [];
92
+
93
+ return tree
94
+ .flatMap((node) => {
95
+ const nodePath = normalizeName(node && node.path);
96
+ if (!nodePath || !nodePath.startsWith(prefix) || !nodePath.endsWith(suffix)) {
97
+ return [];
98
+ }
99
+ const name = nodePath.slice(prefix.length, nodePath.length - suffix.length);
100
+ if (!name || name.includes("/")) {
101
+ return [];
102
+ }
103
+ const record = makeIconRecord(family, name);
104
+ return record ? [record] : [];
105
+ });
106
+ }
107
+
108
+ function dedupeIconRecords(records) {
109
+ const map = new Map();
110
+ for (const record of records) {
111
+ if (!record) {
112
+ continue;
113
+ }
114
+ const key = `${record.family}::${record.name}`;
115
+ if (!map.has(key)) {
116
+ map.set(key, record);
117
+ }
118
+ }
119
+ return Array.from(map.values());
120
+ }
121
+
122
+ function summarizeSourceResults(sourceResults = {}) {
123
+ const entries = Object.values(sourceResults);
124
+ const total = entries.length;
125
+ const errorCount = entries.filter((source) => source && source.status === "error").length;
126
+ const okCount = entries.filter((source) => source && source.status === "ok").length;
127
+ return {
128
+ total,
129
+ okCount,
130
+ errorCount,
131
+ degraded: errorCount > 0
132
+ };
133
+ }
134
+
135
+ function fetchText(url, options = {}) {
136
+ const timeoutMs = Number.isFinite(Number(options.timeoutMs))
137
+ ? Number(options.timeoutMs)
138
+ : DEFAULT_TIMEOUT_MS;
139
+
140
+ return new Promise((resolve, reject) => {
141
+ const request = https.get(
142
+ url,
143
+ {
144
+ headers: {
145
+ "User-Agent": "da-vinci-workflow/icon-sync",
146
+ Accept: "application/json,text/plain,*/*"
147
+ }
148
+ },
149
+ (response) => {
150
+ let data = "";
151
+ response.setEncoding("utf8");
152
+ response.on("data", (chunk) => {
153
+ data += chunk;
154
+ });
155
+ response.on("end", () => {
156
+ if (response.statusCode && response.statusCode >= 400) {
157
+ reject(new Error(`HTTP ${response.statusCode} for ${url}`));
158
+ return;
159
+ }
160
+ resolve(data);
161
+ });
162
+ }
163
+ );
164
+
165
+ request.on("error", (error) => {
166
+ reject(error);
167
+ });
168
+
169
+ request.setTimeout(timeoutMs, () => {
170
+ request.destroy(new Error(`Request timeout after ${timeoutMs}ms for ${url}`));
171
+ });
172
+ });
173
+ }
174
+
175
+ function getDefaultCatalogPath(homeDir) {
176
+ const root = homeDir ? path.resolve(homeDir) : os.homedir();
177
+ return path.join(root, ".da-vinci", "icon-catalog.json");
178
+ }
179
+
180
+ function resolveCatalogPath(options = {}) {
181
+ if (options.catalogPath) {
182
+ return path.resolve(options.catalogPath);
183
+ }
184
+ if (options.outputPath) {
185
+ return path.resolve(options.outputPath);
186
+ }
187
+ return path.resolve(getDefaultCatalogPath(options.homeDir));
188
+ }
189
+
190
+ function loadIconCatalog(options = {}) {
191
+ const catalogPath = resolveCatalogPath(options);
192
+ if (!fs.existsSync(catalogPath)) {
193
+ return {
194
+ catalogPath,
195
+ catalog: null
196
+ };
197
+ }
198
+
199
+ const parsed = JSON.parse(fs.readFileSync(catalogPath, "utf8"));
200
+ if (!parsed || !Array.isArray(parsed.icons)) {
201
+ throw new Error(`Invalid icon catalog format: ${catalogPath}`);
202
+ }
203
+
204
+ return {
205
+ catalogPath,
206
+ catalog: parsed
207
+ };
208
+ }
209
+
210
+ async function syncIconCatalog(options = {}) {
211
+ const catalogPath = resolveCatalogPath(options);
212
+ const timeoutMs = Number.isFinite(Number(options.timeoutMs))
213
+ ? Number(options.timeoutMs)
214
+ : DEFAULT_TIMEOUT_MS;
215
+ const strict = toBoolean(options.strict);
216
+ const fetchTextImpl = typeof options.fetchText === "function" ? options.fetchText : fetchText;
217
+
218
+ const sourceSpecs = [
219
+ {
220
+ key: "material",
221
+ url: MATERIAL_METADATA_URL,
222
+ parse: parseGoogleMaterialMetadata
223
+ },
224
+ {
225
+ key: "lucide",
226
+ url: LUCIDE_TREE_URL,
227
+ parse: (raw) =>
228
+ parseGitHubTreeIcons(raw, {
229
+ family: "lucide",
230
+ prefix: "icons/",
231
+ suffix: ".json"
232
+ })
233
+ },
234
+ {
235
+ key: "feather",
236
+ url: FEATHER_TREE_URL,
237
+ parse: (raw) =>
238
+ parseGitHubTreeIcons(raw, {
239
+ family: "feather",
240
+ prefix: "icons/",
241
+ suffix: ".svg"
242
+ })
243
+ },
244
+ {
245
+ key: "phosphor",
246
+ url: PHOSPHOR_TREE_URL,
247
+ parse: (raw) =>
248
+ parseGitHubTreeIcons(raw, {
249
+ family: "phosphor",
250
+ prefix: "assets/regular/",
251
+ suffix: ".svg"
252
+ })
253
+ }
254
+ ];
255
+
256
+ const sourceResults = {};
257
+ const collected = [];
258
+
259
+ await Promise.all(
260
+ sourceSpecs.map(async (spec) => {
261
+ try {
262
+ const raw = await fetchTextImpl(spec.url, {
263
+ timeoutMs
264
+ });
265
+ const records = spec.parse(raw);
266
+ sourceResults[spec.key] = {
267
+ status: "ok",
268
+ url: spec.url,
269
+ count: records.length
270
+ };
271
+ collected.push(...records);
272
+ } catch (error) {
273
+ sourceResults[spec.key] = {
274
+ status: "error",
275
+ url: spec.url,
276
+ count: 0,
277
+ error: error.message || String(error)
278
+ };
279
+ }
280
+ })
281
+ );
282
+
283
+ const icons = dedupeIconRecords(collected);
284
+ const sourceSummary = summarizeSourceResults(sourceResults);
285
+ if (icons.length === 0) {
286
+ throw new Error(
287
+ [
288
+ "icon-sync failed: no icon records were fetched.",
289
+ ...Object.entries(sourceResults).map(([key, value]) => `${key}: ${value.status}${value.error ? ` (${value.error})` : ""}`)
290
+ ].join("\n")
291
+ );
292
+ }
293
+ if (strict && sourceSummary.errorCount > 0) {
294
+ throw new Error(
295
+ [
296
+ "icon-sync strict mode failed: one or more upstream sources could not be fetched.",
297
+ ...Object.entries(sourceResults).map(([key, value]) => `${key}: ${value.status}${value.error ? ` (${value.error})` : ""}`)
298
+ ].join("\n")
299
+ );
300
+ }
301
+
302
+ const catalog = {
303
+ schema: 1,
304
+ generatedAt: new Date().toISOString(),
305
+ sourceResults,
306
+ sourceSummary,
307
+ syncStatus: sourceSummary.degraded ? "degraded" : "ok",
308
+ iconCount: icons.length,
309
+ icons
310
+ };
311
+
312
+ ensureDir(catalogPath);
313
+ fs.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2));
314
+
315
+ return {
316
+ catalogPath,
317
+ catalog
318
+ };
319
+ }
320
+
321
+ function formatIconSyncReport(result) {
322
+ const sourceSummary = result.catalog.sourceSummary || summarizeSourceResults(result.catalog.sourceResults || {});
323
+ const statusLine = sourceSummary.degraded
324
+ ? `DEGRADED (${sourceSummary.errorCount}/${sourceSummary.total} source failures)`
325
+ : "OK";
326
+ const lines = [
327
+ "Icon Sync",
328
+ `Catalog path: ${result.catalogPath}`,
329
+ `Generated at: ${result.catalog.generatedAt}`,
330
+ `Status: ${statusLine}`,
331
+ `Icon count: ${result.catalog.iconCount}`,
332
+ "Sources:"
333
+ ];
334
+
335
+ for (const [key, source] of Object.entries(result.catalog.sourceResults || {})) {
336
+ const base = `- ${key}: ${source.status} (${source.count})`;
337
+ if (source.error) {
338
+ lines.push(`${base} ${source.error}`);
339
+ continue;
340
+ }
341
+ lines.push(base);
342
+ }
343
+
344
+ return lines.join("\n");
345
+ }
346
+
347
+ module.exports = {
348
+ MATERIAL_METADATA_URL,
349
+ LUCIDE_TREE_URL,
350
+ FEATHER_TREE_URL,
351
+ PHOSPHOR_TREE_URL,
352
+ getDefaultCatalogPath,
353
+ resolveCatalogPath,
354
+ loadIconCatalog,
355
+ syncIconCatalog,
356
+ formatIconSyncReport,
357
+ parseGoogleMaterialMetadata,
358
+ parseGitHubTreeIcons,
359
+ dedupeIconRecords,
360
+ summarizeSourceResults
361
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenonbyte/da-vinci-workflow",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Requirement-to-design-to-code workflow skill for Codex, Claude, and Gemini",
5
5
  "bin": {
6
6
  "da-vinci": "bin/da-vinci.js"
@@ -30,7 +30,10 @@
30
30
  "test:persistence-flows": "node scripts/test-persistence-flows.js",
31
31
  "test:pencil-session": "node scripts/test-pencil-session.js",
32
32
  "test:pencil-preflight": "node scripts/test-pencil-preflight.js",
33
- "test:pen-persistence": "node scripts/test-pen-persistence.js"
33
+ "test:pen-persistence": "node scripts/test-pen-persistence.js",
34
+ "test:icon-search": "node scripts/test-icon-search.js",
35
+ "test:icon-sync": "node scripts/test-icon-sync.js",
36
+ "test:icon-aliases": "node scripts/test-icon-aliases.js"
34
37
  },
35
38
  "engines": {
36
39
  "node": ">=18"
@@ -166,6 +166,17 @@ Use this structure:
166
166
  - Require Adapter
167
167
  - Require Supervisor Review
168
168
 
169
+ ## Icon System Guidance (Advisory)
170
+ - Goal
171
+ - Source
172
+ - Allowed families
173
+ - Default family
174
+ - Default spec
175
+ - Reuse rule
176
+ - Placeholder policy
177
+ - Review requirement
178
+ - Checkpoint policy (`WARN` by default)
179
+
169
180
  ## Do
170
181
  - approved stylistic moves
171
182
 
@@ -189,6 +200,19 @@ Field meaning:
189
200
  - `Fallback`: what to do when preferred adapters are unavailable
190
201
  - `Require Adapter`: whether missing adapters should block the workflow
191
202
  - `Require Supervisor Review`: whether missing, blocked, or unaccepted `design-supervisor review` should block broad expansion, implementation-task handoff, or terminal completion
203
+ - `Icon System Guidance (Advisory)`: optional project-level icon consistency guidance; keep this non-blocking by default and escalate to hard gate only when explicit signoff is required
204
+
205
+ Icon System Guidance field meaning:
206
+
207
+ - `Goal`: icon-quality objective for this project (consistency, readability, brand fit)
208
+ - `Source`: expected icon source policy, typically preferring `icon_font` for functional icons
209
+ - `Allowed families`: approved icon families for this project
210
+ - `Default family`: preferred baseline family to reduce mixed-style drift
211
+ - `Default spec`: baseline size/weight/color-token rules for functional icons
212
+ - `Reuse rule`: whether icon variants should be consolidated as reusable components before broad expansion
213
+ - `Placeholder policy`: what counts as unacceptable placeholder icon usage
214
+ - `Review requirement`: icon checks reviewers must explicitly record per anchor review
215
+ - `Checkpoint policy`: whether unresolved icon issues default to `WARN` or `BLOCK`
192
216
 
193
217
  Use this artifact as a project-level visual contract. Generate it when the project does not already have one.
194
218
 
@@ -0,0 +1,12 @@
1
+ {
2
+ "aliases": {
3
+ "保险箱": ["vault", "safe box", "archive", "inventory_2"],
4
+ "解密": ["unlock", "lock_open", "key", "verified_user"],
5
+ "重试": ["refresh", "retry", "sync", "rotate-cw", "arrow-clockwise"],
6
+ "客服": ["headset", "support_agent", "message-circle", "chat"],
7
+ "工单": ["ticket", "file-text", "message-square"],
8
+ "风控": ["shield", "shield-check", "verified_user"],
9
+ "登录": ["login", "log-in", "sign-in", "person"],
10
+ "退出": ["logout", "log-out", "sign-out"]
11
+ }
12
+ }
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+
5
+ function getArg(flag) {
6
+ const index = process.argv.indexOf(flag);
7
+ if (index < 0) {
8
+ return undefined;
9
+ }
10
+ return process.argv[index + 1];
11
+ }
12
+
13
+ function safeReadJson(filePath, fallback) {
14
+ if (!filePath || !fs.existsSync(filePath)) {
15
+ return fallback;
16
+ }
17
+ try {
18
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
19
+ } catch (error) {
20
+ return fallback;
21
+ }
22
+ }
23
+
24
+ function main() {
25
+ const inputPath = getArg("-i");
26
+ const outputPath = getArg("-o");
27
+ const commandText = fs.readFileSync(0, "utf8");
28
+ const document = safeReadJson(inputPath, { children: [], variables: {} });
29
+
30
+ let payload;
31
+ if (/get_variables\s*\(/.test(commandText)) {
32
+ payload = {
33
+ variables: document && typeof document.variables === "object" ? document.variables : {}
34
+ };
35
+ } else {
36
+ payload = {
37
+ nodes: Array.isArray(document.children) ? document.children : []
38
+ };
39
+ }
40
+
41
+ if (outputPath) {
42
+ const data = inputPath && fs.existsSync(inputPath) ? fs.readFileSync(inputPath, "utf8") : '{"version":"2.9","children":[]}\n';
43
+ fs.writeFileSync(outputPath, data);
44
+ }
45
+
46
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
47
+ }
48
+
49
+ main();
@@ -0,0 +1,87 @@
1
+ const assert = require("assert/strict");
2
+ const fs = require("fs");
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const {
6
+ DEFAULT_ALIASES,
7
+ getDefaultAliasPath,
8
+ resolveAliasPath,
9
+ loadIconAliases,
10
+ expandQueryWithAliases,
11
+ normalizeAliasMap
12
+ } = require("../lib/icon-aliases");
13
+
14
+ function runTest(name, fn) {
15
+ try {
16
+ fn();
17
+ console.log(`PASS ${name}`);
18
+ } catch (error) {
19
+ console.error(`FAIL ${name}`);
20
+ throw error;
21
+ }
22
+ }
23
+
24
+ runTest("default aliases include vault semantics", () => {
25
+ assert.ok(Array.isArray(DEFAULT_ALIASES["保险箱"]));
26
+ assert.ok(DEFAULT_ALIASES["保险箱"].includes("vault"));
27
+ });
28
+
29
+ runTest("normalizeAliasMap normalizes keys and values", () => {
30
+ const map = normalizeAliasMap({
31
+ " 设 置 ": [" settings ", "tune"],
32
+ "": ["ignored"]
33
+ });
34
+ assert.ok(map["设 置"]);
35
+ assert.deepEqual(map["设 置"], ["settings", "tune"]);
36
+ });
37
+
38
+ runTest("default alias path resolves under home", () => {
39
+ const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-alias-home-"));
40
+ const aliasPath = getDefaultAliasPath(tempHome);
41
+ assert.match(aliasPath, /\.da-vinci[\/\\]icon-aliases\.json$/);
42
+ });
43
+
44
+ runTest("loadIconAliases merges user file over defaults", () => {
45
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-alias-file-"));
46
+ const aliasPath = path.join(tempDir, "icon-aliases.json");
47
+ fs.writeFileSync(
48
+ aliasPath,
49
+ JSON.stringify(
50
+ {
51
+ aliases: {
52
+ "保险箱": ["vault", "safe-box-custom"],
53
+ "工单": ["ticket", "message-square"]
54
+ }
55
+ },
56
+ null,
57
+ 2
58
+ )
59
+ );
60
+
61
+ const loaded = loadIconAliases({
62
+ aliasPath
63
+ });
64
+ assert.equal(loaded.loaded, true);
65
+ assert.equal(loaded.source, "file");
66
+ assert.ok(loaded.aliases["保险箱"].includes("safe-box-custom"));
67
+ assert.ok(loaded.aliases["工单"].includes("ticket"));
68
+ });
69
+
70
+ runTest("expandQueryWithAliases adds mapped extra tokens", () => {
71
+ const expansion = expandQueryWithAliases("保险箱 解密", {
72
+ ...normalizeAliasMap(DEFAULT_ALIASES),
73
+ 工单: ["ticket"]
74
+ });
75
+ assert.ok(expansion.extraTokens.includes("vault"));
76
+ assert.ok(expansion.extraTokens.includes("unlock"));
77
+ assert.ok(expansion.matchedAliases.length >= 2);
78
+ });
79
+
80
+ runTest("resolveAliasPath returns absolute path", () => {
81
+ const resolved = resolveAliasPath({
82
+ aliasPath: "./tmp/icon-aliases.json"
83
+ });
84
+ assert.ok(path.isAbsolute(resolved));
85
+ });
86
+
87
+ console.log("All icon-aliases tests passed.");
@@ -0,0 +1,72 @@
1
+ const assert = require("assert/strict");
2
+ const {
3
+ searchIconLibrary,
4
+ formatIconSearchReport
5
+ } = require("../lib/icon-search");
6
+
7
+ function runTest(name, fn) {
8
+ try {
9
+ fn();
10
+ console.log(`PASS ${name}`);
11
+ } catch (error) {
12
+ console.error(`FAIL ${name}`);
13
+ throw error;
14
+ }
15
+ }
16
+
17
+ runTest("exact settings query ranks material rounded first", () => {
18
+ const result = searchIconLibrary("settings", { top: 5 });
19
+ assert.ok(result.matches.length > 0);
20
+ assert.equal(result.matches[0].family, "Material Symbols Rounded");
21
+ assert.equal(result.matches[0].name, "settings");
22
+ });
23
+
24
+ runTest("chinese query can resolve settings candidates", () => {
25
+ const result = searchIconLibrary("设置", { top: 5 });
26
+ assert.ok(result.matches.some((match) => match.name === "settings"));
27
+ });
28
+
29
+ runTest("family filter returns only the selected family", () => {
30
+ const result = searchIconLibrary("lock", { family: "lucide", top: 6 });
31
+ assert.ok(result.matches.length > 0);
32
+ for (const match of result.matches) {
33
+ assert.equal(match.family, "lucide");
34
+ }
35
+ });
36
+
37
+ runTest("material family alias expands to all material variants", () => {
38
+ const result = searchIconLibrary("home", { family: "material", top: 6 });
39
+ const families = new Set(result.matches.map((match) => match.family));
40
+ assert.ok(families.has("Material Symbols Rounded"));
41
+ assert.ok(families.has("Material Symbols Outlined"));
42
+ assert.ok(families.has("Material Symbols Sharp"));
43
+ });
44
+
45
+ runTest("unknown family filter throws a clear error", () => {
46
+ assert.throws(() => searchIconLibrary("lock", { family: "unknown-family" }), /Unknown icon family filter/i);
47
+ });
48
+
49
+ runTest("formatted report contains node payload hints", () => {
50
+ const result = searchIconLibrary("vault", { top: 3 });
51
+ const report = formatIconSearchReport(result);
52
+ assert.match(report, /Icon Search/);
53
+ assert.match(report, /node:/);
54
+ assert.match(report, /iconFontFamily/);
55
+ });
56
+
57
+ runTest("external catalog records are merged into search candidates", () => {
58
+ const result = searchIconLibrary("launch rocket", {
59
+ top: 5,
60
+ catalog: [
61
+ {
62
+ family: "lucide",
63
+ name: "rocket",
64
+ semantic: "rocket",
65
+ tags: ["launch"]
66
+ }
67
+ ]
68
+ });
69
+ assert.ok(result.matches.some((match) => match.family === "lucide" && match.name === "rocket"));
70
+ });
71
+
72
+ console.log("All icon-search tests passed.");