@@ -36,6 +36,23 @@ def sync_plugin_metadata(version: str) -> None:
3636 print (f"✅ Synced plugin metadata to { version } " )
3737
3838
39+ def matches_tag_filter (rule_tags : list [str ], filter_tags : list [str ]) -> bool :
40+ """
41+ Check if rule has all required tags (case-insensitive AND logic).
42+
43+ Args:
44+ rule_tags: List of tags from the rule (already lowercase from parsing)
45+ filter_tags: List of tags to filter by
46+
47+ Returns:
48+ True if rule has all filter tags (or no filter), False otherwise
49+ """
50+ if not filter_tags :
51+ return True # No filter means all pass
52+
53+ return all (tag .lower () in rule_tags for tag in filter_tags )
54+
55+
3956def update_skill_md (language_to_rules : dict [str , list [str ]], skill_path : str ) -> None :
4057 """
4158 Update SKILL.md with language-to-rules mapping table.
@@ -81,7 +98,7 @@ def update_skill_md(language_to_rules: dict[str, list[str]], skill_path: str) ->
8198 print (f"Updated SKILL.md with language mappings" )
8299
83100
84- def convert_rules (input_path : str , output_dir : str = "dist" , include_claudecode : bool = True , version : str = None ) -> dict [str , list [str ]]:
101+ def convert_rules (input_path : str , output_dir : str = "dist" , include_claudecode : bool = True , version : str = None , filter_tags : list [ str ] = None ) -> dict [str , list [str ]]:
85102 """
86103 Convert rule file(s) to all supported IDE formats using RuleConverter.
87104
@@ -90,6 +107,7 @@ def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode:
90107 output_dir: Output directory (default: 'dist/')
91108 include_claudecode: Whether to generate Claude Code plugin (default: True, only for core rules)
92109 version: Version string to use (default: read from pyproject.toml)
110+ filter_tags: Optional list of tags to filter by (AND logic, case-insensitive)
93111
94112 Returns:
95113 Dictionary with 'success' and 'errors' lists:
@@ -138,14 +156,19 @@ def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode:
138156 # Setup output directory
139157 output_base = Path (output_dir )
140158
141- results = {"success" : [], "errors" : []}
159+ results = {"success" : [], "errors" : [], "skipped" : [] }
142160 language_to_rules = defaultdict (list )
143161
144162 # Process each file
145163 for md_file in md_files :
146164 try :
147165 # Convert the file (raises exceptions on error)
148166 result = converter .convert (md_file )
167+
168+ # Apply tag filter if specified
169+ if filter_tags and not matches_tag_filter (result .tags , filter_tags ):
170+ results ["skipped" ].append (result .filename )
171+ continue
149172
150173 # Write each format
151174 output_files = []
@@ -192,9 +215,14 @@ def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode:
192215 results ["errors" ].append (error_msg )
193216
194217 # Summary
195- print (
196- f"\n Results: { len (results ['success' ])} success, { len (results ['errors' ])} errors"
197- )
218+ if filter_tags :
219+ print (
220+ f"\n Results: { len (results ['success' ])} success, { len (results ['skipped' ])} skipped (tag filter), { len (results ['errors' ])} errors"
221+ )
222+ else :
223+ print (
224+ f"\n Results: { len (results ['success' ])} success, { len (results ['errors' ])} errors"
225+ )
198226
199227 # Generate SKILL.md with language mappings (only if Claude Code is included)
200228 if include_claudecode and language_to_rules :
@@ -256,6 +284,12 @@ def _resolve_source_paths(args) -> list[Path]:
256284 default = "dist" ,
257285 help = "Output directory for generated bundles (default: dist)." ,
258286 )
287+ parser .add_argument (
288+ "--tag" ,
289+ "--tags" ,
290+ dest = "tags" ,
291+ help = "Filter rules by tags (comma-separated, case-insensitive, AND logic). Example: --tag api,web-security" ,
292+ )
259293
260294 cli_args = parser .parse_args ()
261295 source_paths = _resolve_source_paths (cli_args )
@@ -316,7 +350,16 @@ def _resolve_source_paths(args) -> list[Path]:
316350 print ()
317351
318352 # Convert all sources
319- aggregated = {"success" : [], "errors" : []}
353+ aggregated = {"success" : [], "errors" : [], "skipped" : []}
354+ # Parse comma-separated tags
355+ filter_tags = None
356+ if cli_args .tags :
357+ filter_tags = [tag .strip () for tag in cli_args .tags .split ("," ) if tag .strip ()]
358+
359+ # Print tag filter info if active
360+ if filter_tags :
361+ print (f"Tag filter active: { ', ' .join (filter_tags )} (AND logic - rules must have all tags)\n " )
362+
320363 for source_path in source_paths :
321364 is_core = source_path == Path ("sources/core" )
322365
@@ -325,11 +368,14 @@ def _resolve_source_paths(args) -> list[Path]:
325368 str (source_path ),
326369 cli_args .output_dir ,
327370 include_claudecode = is_core ,
328- version = version
371+ version = version ,
372+ filter_tags = filter_tags
329373 )
330374
331375 aggregated ["success" ].extend (results ["success" ])
332376 aggregated ["errors" ].extend (results ["errors" ])
377+ if "skipped" in results :
378+ aggregated ["skipped" ].extend (results ["skipped" ])
333379 print ("" )
334380
335381 if aggregated ["errors" ]:
0 commit comments