Current section

42 Versions

Jump to

Compare versions

10 files changed
+803 additions
-344 deletions
  @@ -5,6 +5,28 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
5 5
6 6 <!-- changelog -->
7 7
8 + ## v0.1.15 (2025-07-02)
9 +
10 +
11 +
12 +
13 + ### Improvements:
14 +
15 + * support sub-rules, in a usage-rules folder by Zach Daniel
16 +
17 + * add --remove-missing option by Zach Daniel
18 +
19 + * add notice when using --all about the dangers of it by Zach Daniel
20 +
21 + * update usage rules for Elixir by Zach Daniel
22 +
23 + ## [Unreleased]
24 +
25 + ### Features:
26 +
27 + * add sub-rules support - Packages can now have a `usage_rules/` folder with multiple sub-rule files that can be included individually using `package:rule` syntax or all at once with `package:all`
28 + * add `--inline` option - Force specific packages to be inlined even when using `--link-to-folder`. Supports special `usage_rules:all` spec to inline all sub-rules while linking main packages
29 +
8 30 ## v0.1.14 (2025-06-26)
  @@ -5,12 +5,14 @@
5 5 ## Quickstart
6 6
7 7 ```sh
8 - # swap AGENTS.md out for any file you like, e.g `CLAUDE.md`
8 + # install usage_rules into your project
9 + mix igniter.install usage_rules
9 10
11 + # swap AGENTS.md out for any file you like, e.g `CLAUDE.md`
10 12 # sync projects as links to their usage rules
11 13 # to save tokens. Agent can view them on demand
12 14 mix usage_rules.sync AGENTS.md --all \
13 - --builtins elixir,otp \
15 + --inline usage_rules:all \
14 16 --link-to-folder deps
15 17 ```
16 18
  @@ -32,6 +34,24 @@ One quick tip is to have an agent begin the work of writing rules for you, by po
32 34
33 35 Make sure that your `usage-rules.md` file is included in your hex package's `files` option, so that it is distributed with your package.
34 36
37 + ### Sub rules
38 +
39 + A package can have a `package-rules.md` and/or sub-rule files, each of which is referred to separately.
40 + For example:
41 +
42 + ```
43 + package-rules.md # general rules
44 + package-rules/
45 + html.md # html specific rules
46 + database.md # database specific rules
47 + ```
48 +
49 + When synchronizing, these are stated separately, like so:
50 +
51 + ```
52 + mix usage_rules.sync AGENTS.md package package:html package:database
53 + ```
54 +
35 55 ## Key Features
36 56
37 57 1. **Dependency Rules Collection**: Automatically discovers and collects usage rules from dependencies that provide `usage-rules.md` files in their package directory
  @@ -6,17 +6,17 @@
6 6 {<<"GitHub">>,<<"https://github.com/ash-project/usage_rules">>},
7 7 {<<"Website">>,<<"https://ash-hq.org">>}]}.
8 8 {<<"name">>,<<"usage_rules">>}.
9 - {<<"version">>,<<"0.1.14">>}.
9 + {<<"version">>,<<"0.1.15">>}.
10 10 {<<"description">>,
11 11 <<"A dev tool for Elixir projects to gather LLM usage rules from dependencies">>}.
12 12 {<<"elixir">>,<<"~> 1.18">>}.
13 13 {<<"files">>,
14 14 [<<"lib">>,<<"lib/mix">>,<<"lib/mix/tasks">>,
15 15 <<"lib/mix/tasks/usage_rules.install.ex">>,
16 - <<"lib/mix/tasks/usage_rules.sync.ex">>,<<"lib/usage_rules.ex">>,<<"priv">>,
17 - <<"priv/builtins">>,<<"priv/builtins/elixir.md">>,
18 - <<"priv/builtins/otp.md">>,<<".formatter.exs">>,<<"mix.exs">>,
19 - <<"README.md">>,<<"LICENSE">>,<<"CHANGELOG.md">>]}.
16 + <<"lib/mix/tasks/usage_rules.sync.ex">>,<<"lib/usage_rules.ex">>,
17 + <<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>,
18 + <<"CHANGELOG.md">>,<<"usage-rules">>,<<"usage-rules/elixir.md">>,
19 + <<"usage-rules/otp.md">>]}.
20 20 {<<"app">>,<<"usage_rules">>}.
21 21 {<<"licenses">>,[<<"MIT">>]}.
22 22 {<<"requirements">>,
  @@ -57,8 +57,8 @@ if Code.ensure_loaded?(Igniter) do
57 57 "--all",
58 58 "--link-to-folder",
59 59 "deps",
60 - "--builtins",
61 - "elixir,otp"
60 + "--inline",
61 + "usage_rules:all"
62 62 ])
63 63 end
64 64 end
  @@ -16,15 +16,26 @@ defmodule Mix.Tasks.UsageRules.Sync.Docs do
16 16 """
17 17 #{short_doc()}
18 18
19 + ## Package Specifications
20 +
21 + Packages can be specified in the following formats:
22 + * `package_name` - Include the main usage-rules.md file for the package
23 + * `package_name:sub_rule` - Include a specific sub-rule from the package's usage-rules/ folder
24 + * `package_name:all` - Include all sub-rules from the package's usage-rules/ folder
25 +
26 + Sub-rules are discovered from `usage-rules/` folders within package directories. For example:
27 + * `deps/ash/usage-rules/testing.md` can be included with `ash:testing`
28 + * `deps/phoenix/usage-rules/views.md` can be included with `phoenix:views`
29 +
19 30 ## Options
20 31
21 - * `--all` - Gather usage rules from all dependencies that have them
32 + * `--all` - Gather usage rules from all dependencies that have them (includes both main rules and all sub-rules)
22 33 * `--list` - List all dependencies with usage rules. If a file is provided, shows status (present, missing, stale)
23 34 * `--remove` - Remove specified packages from the target file instead of adding them
35 + * `--remove-missing` - Remove any packages from the target file that are not listed in the command
24 36 * `--link-to-folder <folder>` - Save usage rules for each package in separate files within the specified folder and create links to them
25 37 * `--link-style <style>` - Style of links to create when using --link-to-folder (markdown|at). Defaults to 'markdown'
26 - * `--builtins <builtins>` - Include built-in usage rules (comma-separated: elixir,otp)
27 - * `--builtins-link` - Make built-in usage rules honor the --link-to-folder option (default: false, builtins are inlined)
38 + * `--inline <specs>` - Force specific packages to be inlined even when using --link-to-folder. Supports same specs as packages (comma-separated)
28 39
29 40 ## Examples
30 41
  @@ -83,14 +94,36 @@ defmodule Mix.Tasks.UsageRules.Sync.Docs do
83 94 mix usage_rules.sync CLAUDE.md ash phoenix --remove --link-to-folder rules
84 95 ```
85 96
86 - Include built-in Elixir and OTP usage rules:
97 + Include specific sub-rules:
87 98 ```sh
88 - mix usage_rules.sync CLAUDE.md --all --link-to-folder deps --builtins elixir,otp
99 + mix usage_rules.sync CLAUDE.md ash:testing phoenix:views
89 100 ```
90 - Include built-in usage rules with links to folder:
101 +
102 + Include all sub-rules from a package:
91 103 ```sh
92 - mix usage_rules.sync CLAUDE.md --all --link-to-folder docs --builtins elixir,otp --builtins-link
104 + mix usage_rules.sync CLAUDE.md ash:all
93 105 ```
106 +
107 + Mix main package rules with sub-rules:
108 + ```sh
109 + mix usage_rules.sync CLAUDE.md ash ash:testing phoenix:views
110 + ```
111 +
112 + Inline all sub-rules while linking main packages (recommended for agents):
113 + ```sh
114 + mix usage_rules.sync AGENTS.md --all --inline usage_rules:all --link-to-folder deps
115 + ```
116 +
117 + Inline specific packages while linking others:
118 + ```sh
119 + mix usage_rules.sync CLAUDE.md ash:testing phoenix --inline ash:testing --link-to-folder docs
120 + ```
121 +
122 + Remove unused packages that are no longer dependencies:
123 + ```sh
124 + mix usage_rules.sync CLAUDE.md ash phoenix --remove-missing
125 + ```
126 +
94 127 """
95 128 end
96 129 end
  @@ -118,10 +151,10 @@ if Code.ensure_loaded?(Igniter) do
118 151 all: :boolean,
119 152 list: :boolean,
120 153 remove: :boolean,
154 + remove_missing: :boolean,
121 155 link_to_folder: :string,
122 156 link_style: :string,
123 - builtins: :string,
124 - builtins_link: :boolean
157 + inline: :string
125 158 ]
126 159 }
127 160 end
  @@ -137,8 +170,11 @@ if Code.ensure_loaded?(Igniter) do
137 170 igniter
138 171 end
139 172
140 - # Add all usage-rules.md files from deps directory to igniter
141 - igniter = Igniter.include_glob(igniter, "deps/*/usage-rules.md")
173 + # Add all usage-rules.md files and usage_rules/ folders from deps directory to igniter
174 + igniter =
175 + igniter
176 + |> Igniter.include_glob("deps/*/usage-rules.md")
177 + |> Igniter.include_glob("deps/*/usage_rules/*.md")
142 178
143 179 top_level_deps =
144 180 Mix.Project.get().project()[:deps] |> Enum.map(&elem(&1, 0))
  @@ -159,23 +195,13 @@ if Code.ensure_loaded?(Igniter) do
159 195 all_option = igniter.args.options[:all]
160 196 list_option = igniter.args.options[:list]
161 197 remove_option = igniter.args.options[:remove]
198 + remove_missing_option = igniter.args.options[:remove_missing]
162 199 link_to_folder = igniter.args.options[:link_to_folder]
163 200 link_style = igniter.args.options[:link_style] || "markdown"
201 + inline_specs = parse_inline_specs(igniter.args.options[:inline])
164 202 provided_packages = igniter.args.positional.packages
165 - builtins = parse_builtins(igniter.args.options[:builtins])
166 - builtins_link = igniter.args.options[:builtins_link]
167 203
168 204 cond do
169 - # If --builtins contains invalid values, add error
170 - igniter.args.options[:builtins] &&
171 - parse_invalid_builtins(igniter.args.options[:builtins]) != [] ->
172 - invalid = parse_invalid_builtins(igniter.args.options[:builtins])
173 -
174 - Igniter.add_issue(
175 - igniter,
176 - "Invalid builtins: #{Enum.join(invalid, ", ")}. Valid options are: elixir, otp"
177 - )
178 -
179 205 # If --link-style is used with invalid value, add error
180 206 link_style && link_style not in ["markdown", "at"] ->
181 207 Igniter.add_issue(igniter, "--link-style must be either 'markdown' or 'at'")
  @@ -188,6 +214,14 @@ if Code.ensure_loaded?(Igniter) do
188 214 remove_option && (all_option || list_option) ->
189 215 Igniter.add_issue(igniter, "Cannot use --remove with --all or --list options")
190 216
217 + # If --remove-missing is used without a file, add error
218 + remove_missing_option && is_nil(igniter.args.positional[:file]) ->
219 + Igniter.add_issue(igniter, "--remove-missing option requires a file to modify")
220 +
221 + # If --remove-missing is used with --list, add error
222 + remove_missing_option && list_option ->
223 + Igniter.add_issue(igniter, "Cannot use --remove-missing with --list option")
224 +
191 225 # If --remove is used without a file, add error
192 226 remove_option && is_nil(igniter.args.positional[:file]) ->
193 227 Igniter.add_issue(igniter, "--remove option requires a file to remove from")
  @@ -208,8 +242,9 @@ if Code.ensure_loaded?(Igniter) do
208 242 link_to_folder && is_nil(igniter.args.positional[:file]) ->
209 243 Igniter.add_issue(igniter, "--link-to-folder option requires a file to write to")
210 244
211 - # If no packages are given and neither --list nor --all nor --remove is set, add error
212 - Enum.empty?(provided_packages) && !all_option && !list_option && !remove_option ->
245 + # If no packages are given and neither --list nor --all nor --remove nor --remove-missing is set, add error
246 + Enum.empty?(provided_packages) && !all_option && !list_option && !remove_option &&
247 + !remove_missing_option ->
213 248 add_usage_error(igniter)
214 249
215 250 # Handle --remove option
  @@ -223,13 +258,13 @@ if Code.ensure_loaded?(Igniter) do
223 258 all_deps,
224 259 link_to_folder,
225 260 link_style,
226 - builtins,
227 - builtins_link
261 + inline_specs,
262 + remove_missing_option
228 263 )
229 264
230 265 # Handle --list option
231 266 list_option ->
232 - handle_list_option(igniter, all_deps, link_to_folder)
267 + handle_list_option(igniter, all_deps, link_to_folder, link_style, inline_specs)
233 268
234 269 # Handle specific packages
235 270 true ->
  @@ -239,56 +274,37 @@ if Code.ensure_loaded?(Igniter) do
239 274 provided_packages,
240 275 link_to_folder,
241 276 link_style,
242 - builtins,
243 - builtins_link
277 + inline_specs,
278 + remove_missing_option
244 279 )
245 280 end
281 + |> notice_about_all_option(all_option)
246 282 end
247 283
248 - defp parse_builtins(nil), do: []
284 + defp notice_about_all_option(igniter, all_option) do
285 + file = igniter.args.positional[:file]
249 286
250 - defp parse_builtins(builtins_string) do
251 - builtins_string
252 - |> String.split(",")
253 - |> Enum.map(&String.trim/1)
254 - |> Enum.filter(fn builtin ->
255 - builtin in ["elixir", "otp"]
256 - end)
257 - end
287 + if all_option do
288 + Igniter.add_warning(igniter, """
289 + Usage Rules:
258 290
259 - defp parse_invalid_builtins(nil), do: []
291 + We've synchronized usage rules for all of your direct
292 + dependencies into #{file}. When working with agents, it
293 + is important to manage your context window. Consider
294 + which packages you wish to have present. You can use
295 + the `--remove-missing` flag to select exactly what to sync.
260 296
261 - defp parse_invalid_builtins(builtins_string) do
262 - builtins_string
263 - |> String.split(",")
264 - |> Enum.map(&String.trim/1)
265 - |> Enum.reject(fn builtin ->
266 - builtin in ["", "elixir", "otp"]
267 - end)
268 - end
297 + For example:
269 298
270 - defp get_builtin_contents(builtins) do
271 - builtins
272 - |> Enum.map(fn builtin ->
273 - builtin_path = Path.join([:code.priv_dir(:usage_rules), "builtins", "#{builtin}.md"])
274 - content = File.read!(builtin_path)
275 -
276 - description =
277 - case builtin do
278 - "elixir" -> "Core Elixir language features and standard library"
279 - "otp" -> "OTP (Open Telecom Platform) behaviors and patterns"
280 - _ -> ""
281 - end
282 -
283 - description_part = if description == "", do: "", else: "_#{description}_\n\n"
284 -
285 - {String.to_atom(builtin),
286 - "<!-- #{builtin}-start -->\n" <>
287 - "## #{builtin} usage\n" <>
288 - description_part <>
289 - content <>
290 - "\n<!-- #{builtin}-end -->"}
291 - end)
299 + mix usage_rules.sync #{file} pkg1 pkg2 \\
300 + usage_rules:all \\
301 + --inline usage_rules:all \\
302 + --link-to-folder deps \\
303 + --remove-missing
304 + """)
305 + else
306 + igniter
307 + end
292 308 end
293 309
294 310 defp usage_rules_header do
  @@ -314,18 +330,19 @@ if Code.ensure_loaded?(Igniter) do
314 330 if igniter.assigns[:test_mode?] do
315 331 igniter.rewrite.sources
316 332 |> Enum.filter(fn {path, _source} ->
317 - String.match?(path, ~r|^deps/[^/]+/usage-rules\.md$|)
333 + String.match?(path, ~r|^deps/[^/]+/usage-rules\.md$|) ||
334 + String.match?(path, ~r|^deps/[^/]+/usage-rules/[^/]+\.md$|)
318 335 end)
319 336 |> Enum.map(fn {path, _source} ->
320 - # Extract package name from deps/package_name/usage-rules.md
337 + # Extract package name from deps/package_name/usage-rules.md or deps/package_name/usage-rules/sub-rule.md
321 338 package_name =
322 339 path
323 340 |> String.split("/")
324 341 |> Enum.at(1)
325 342 |> String.to_atom()
326 343
327 - # Extract package path from deps/package_name/usage-rules.md
328 - package_path = Path.dirname(path)
344 + # Extract package path from deps/package_name/...
345 + package_path = Path.join("deps", to_string(package_name))
329 346
330 347 {package_name, package_path}
331 348 end)
  @@ -335,6 +352,118 @@ if Code.ensure_loaded?(Igniter) do
335 352 end
336 353 end
337 354
355 + defp parse_package_spec(package_spec) when is_binary(package_spec) do
356 + case String.split(package_spec, ":", parts: 2) do
357 + [package_name] ->
358 + {String.to_atom(package_name), nil}
359 +
360 + [package_name, sub_rule] ->
361 + {String.to_atom(package_name), sub_rule}
362 + end
363 + end
364 +
365 + defp find_available_sub_rules(igniter, package_path) do
366 + usage_rules_dir = Path.join(package_path, "usage-rules")
367 +
368 + # Try to find sub-rules from igniter sources first (works in both test and regular mode)
369 + source_sub_rules =
370 + igniter.rewrite.sources
371 + |> Enum.filter(fn {path, _source} ->
372 + String.starts_with?(path, usage_rules_dir <> "/") &&
373 + String.ends_with?(path, ".md")
374 + end)
375 + |> Enum.map(fn {path, _source} ->
376 + path
377 + |> Path.basename()
378 + |> Path.rootname()
379 + end)
380 + |> Enum.sort()
381 +
382 + # If we found sub-rules in sources, return them
383 + if Enum.any?(source_sub_rules) do
384 + source_sub_rules
385 + else
386 + # Otherwise, try file system
387 + case File.ls(usage_rules_dir) do
388 + {:ok, files} ->
389 + files
390 + |> Enum.filter(&String.ends_with?(&1, ".md"))
391 + |> Enum.map(&Path.rootname/1)
392 + |> Enum.sort()
393 +
394 + {:error, _} ->
395 + []
396 + end
397 + end
398 + end
399 +
400 + defp parse_inline_specs(nil), do: []
401 +
402 + defp parse_inline_specs(inline_string) do
403 + inline_string
404 + |> String.split(",")
405 + |> Enum.map(&String.trim/1)
406 + |> Enum.reject(&(&1 == ""))
407 + end
408 +
409 + defp should_inline_package?(package_name, sub_rule, inline_specs) do
410 + package_name_str = to_string(package_name)
411 +
412 + section_name =
413 + case sub_rule do
414 + nil -> package_name_str
415 + sub_rule_name -> "#{package_name_str}:#{sub_rule_name}"
416 + end
417 +
418 + Enum.any?(inline_specs, fn inline_spec ->
419 + case String.split(inline_spec, ":", parts: 2) do
420 + [^package_name_str] when sub_rule == nil ->
421 + true
422 +
423 + [^package_name_str, "all"] ->
424 + true
425 +
426 + [^package_name_str, sub_rule_spec] when sub_rule == sub_rule_spec ->
427 + true
428 +
429 + [^section_name] ->
430 + true
431 +
432 + # Special case: "usage_rules:all" means inline all sub-rules
433 + ["usage_rules", "all"] when sub_rule != nil ->
434 + true
435 +
436 + _ ->
437 + false
438 + end
439 + end)
440 + end
441 +
442 + defp expand_wildcard_specs(igniter, all_deps, provided_packages) do
443 + Enum.flat_map(provided_packages, fn package_spec ->
444 + {package_name, sub_rule} = parse_package_spec(package_spec)
445 +
446 + case sub_rule do
447 + "all" ->
448 + # Find the package path
449 + case Enum.find(all_deps, fn {name, _path} -> name == package_name end) do
450 + {_name, package_path} ->
451 + available_sub_rules = find_available_sub_rules(igniter, package_path)
452 +
453 + Enum.map(available_sub_rules, fn sub_rule_name ->
454 + "#{package_name}:#{sub_rule_name}"
455 + end)
456 +
457 + nil ->
458 + [package_spec]
459 + end
460 +
461 + _ ->
462 + [package_spec]
463 + end
464 + end)
465 + end
466 +
338 467 defp add_usage_error(igniter) do
339 468 Igniter.add_issue(igniter, """
340 469 Usage:
  @@ -364,37 +493,63 @@ if Code.ensure_loaded?(Igniter) do
364 493 """)
365 494 end
366 495
367 - defp handle_all_option(igniter, all_deps, link_to_folder, link_style, builtins, builtins_link) do
496 + defp handle_all_option(
497 + igniter,
498 + all_deps,
499 + link_to_folder,
500 + link_style,
501 + inline_specs,
502 + remove_missing
503 + ) do
368 504 all_packages_with_rules = get_packages_with_usage_rules(igniter, all_deps)
369 505
506 + # Discover all package rules including sub-rules
507 + all_package_rules =
508 + all_packages_with_rules
509 + |> Enum.flat_map(fn {package_name, package_path} ->
510 + # Check for main usage-rules.md file
511 + main_rules =
512 + if Igniter.exists?(igniter, Path.join(package_path, "usage-rules.md")) do
513 + [{package_name, package_path, nil}]
514 + else
515 + []
516 + end
517 +
518 + # Check for sub-rules in usage_rules/ folder
519 + sub_rules =
520 + find_available_sub_rules(igniter, package_path)
521 + |> Enum.map(fn sub_rule_name ->
522 + {package_name, package_path, sub_rule_name}
523 + end)
524 +
525 + main_rules ++ sub_rules
526 + end)
527 +
370 528 igniter
371 529 |> Igniter.add_notice(
372 530 "Found #{length(all_packages_with_rules)} dependencies with usage rules"
373 531 )
374 532 |> then(fn igniter ->
375 - Enum.reduce(all_packages_with_rules, igniter, fn {name, _path}, acc ->
376 - Igniter.add_notice(acc, "Including usage rules for: #{name}")
533 + Enum.reduce(all_package_rules, igniter, fn {name, _path, sub_rule}, acc ->
534 + case sub_rule do
535 + nil ->
536 + Igniter.add_notice(acc, "Including usage rules for: #{name}")
537 +
538 + sub_rule_name ->
539 + Igniter.add_notice(acc, "Including usage rules for: #{name}:#{sub_rule_name}")
540 + end
377 541 end)
378 542 end)
379 - |> maybe_add_builtin_notices(builtins)
380 543 |> generate_usage_rules_file(
381 - all_packages_with_rules,
544 + all_package_rules,
382 545 link_to_folder,
383 546 link_style,
384 - builtins,
385 - builtins_link
547 + inline_specs,
548 + remove_missing
386 549 )
387 550 end
388 551
389 - defp maybe_add_builtin_notices(igniter, []), do: igniter
390 -
391 - defp maybe_add_builtin_notices(igniter, builtins) do
392 - Enum.reduce(builtins, igniter, fn builtin, acc ->
393 - Igniter.add_notice(acc, "Including built-in usage rules for: #{builtin}")
394 - end)
395 - end
396 -
397 - defp handle_list_option(igniter, all_deps, link_to_folder) do
552 + defp handle_list_option(igniter, all_deps, link_to_folder, link_style, inline_specs) do
398 553 packages_with_rules = get_packages_with_usage_rules(igniter, all_deps)
399 554
400 555 if Enum.empty?(packages_with_rules) do
  @@ -407,7 +562,9 @@ if Code.ensure_loaded?(Igniter) do
407 562 igniter,
408 563 packages_with_rules,
409 564 file_path,
410 - link_to_folder
565 + link_to_folder,
566 + link_style,
567 + inline_specs
411 568 )
412 569 else
413 570 list_packages_without_comparison(igniter, packages_with_rules)
  @@ -421,27 +578,55 @@ if Code.ensure_loaded?(Igniter) do
421 578 provided_packages,
422 579 link_to_folder,
423 580 link_style,
424 - builtins,
425 - builtins_link
581 + inline_specs,
582 + remove_missing
426 583 ) do
427 - packages =
428 - all_deps
429 - |> Enum.filter(fn {name, _path} ->
430 - to_string(name) in provided_packages
431 - end)
432 - |> Enum.flat_map(fn {name, path} ->
433 - usage_rules_path = Path.join(path, "usage-rules.md")
584 + # Expand wildcard specs first
585 + expanded_packages = expand_wildcard_specs(igniter, all_deps, provided_packages)
434 586
435 - if Igniter.exists?(igniter, usage_rules_path) do
436 - [{name, path}]
437 - else
438 - []
587 + # Parse and process each package spec
588 + package_rules =
589 + expanded_packages
590 + |> Enum.flat_map(fn package_spec ->
591 + {package_name, sub_rule} = parse_package_spec(package_spec)
592 +
593 + case Enum.find(all_deps, fn {name, _path} -> name == package_name end) do
594 + {_name, package_path} ->
595 + case sub_rule do
596 + nil ->
597 + # Standard package without sub-rule - check for usage-rules.md
598 + usage_rules_path = Path.join(package_path, "usage-rules.md")
599 +
600 + if Igniter.exists?(igniter, usage_rules_path) do
601 + [{package_name, package_path, nil}]
602 + else
603 + []
604 + end
605 +
606 + sub_rule_name ->
607 + # Sub-rule specified - check for usage-rules/sub_rule.md
608 + sub_rule_path = Path.join([package_path, "usage-rules", "#{sub_rule_name}.md"])
609 +
610 + if Igniter.exists?(igniter, sub_rule_path) do
611 + [{package_name, package_path, sub_rule_name}]
612 + else
613 + []
614 + end
615 + end
616 +
617 + nil ->
618 + []
439 619 end
440 620 end)
441 621
442 622 igniter
443 - |> maybe_add_builtin_notices(builtins)
444 - |> generate_usage_rules_file(packages, link_to_folder, link_style, builtins, builtins_link)
623 + |> generate_usage_rules_file(
624 + package_rules,
625 + link_to_folder,
626 + link_style,
627 + inline_specs,
628 + remove_missing
629 + )
445 630 end
446 631
447 632 defp handle_remove_packages(igniter, provided_packages, link_to_folder) do
  @@ -455,10 +640,13 @@ if Code.ensure_loaded?(Igniter) do
455 640 end
456 641
457 642 defp get_packages_with_usage_rules(igniter, all_deps) do
458 - all_deps
459 - |> Enum.filter(fn {_name, path} ->
460 - usage_rules_path = Path.join(path, "usage-rules.md")
461 - Igniter.exists?(igniter, usage_rules_path)
643 + Enum.filter(all_deps, fn
644 + {_name, path} when is_binary(path) and path != "" ->
645 + Igniter.exists?(igniter, Path.join(path, "usage-rules.md")) ||
646 + Igniter.exists?(igniter, Path.join(path, "usage-rules"))
647 +
648 + _ ->
649 + false
462 650 end)
463 651 end
464 652
  @@ -466,35 +654,146 @@ if Code.ensure_loaded?(Igniter) do
466 654 igniter,
467 655 packages_with_rules,
468 656 file_path,
469 - link_to_folder
657 + link_to_folder,
658 + link_style,
659 + inline_specs
470 660 ) do
471 661 current_file_content = read_current_file_content(igniter, file_path)
472 662
473 663 Enum.reduce(packages_with_rules, igniter, fn {name, path}, acc ->
474 - usage_rules_path = Path.join(path, "usage-rules.md")
664 + # Ensure name is a string
665 + name = to_string(name)
475 666
476 - package_rules_content =
477 - case Rewrite.source(acc.rewrite, usage_rules_path) do
478 - {:ok, source} -> Rewrite.Source.get(source, :content)
479 - {:error, _} -> File.read!(usage_rules_path)
667 + # Check for main package and sub-rules
668 + usage_rules_path = Path.join(path, "usage-rules.md")
669 + has_main = Igniter.exists?(acc, usage_rules_path)
670 + sub_rules = find_available_sub_rules(igniter, path)
671 +
672 + # Build the notice message
673 + message_parts = []
674 +
675 + # Add main package status if exists
676 + message_parts =
677 + if has_main do
678 + package_rules_content =
679 + case Rewrite.source(acc.rewrite, usage_rules_path) do
680 + {:ok, source} -> Rewrite.Source.get(source, :content)
681 + {:error, _} -> File.read!(usage_rules_path)
682 + end
683 +
684 + status =
685 + get_package_status_in_file(
686 + acc,
687 + name,
688 + package_rules_content,
689 + current_file_content,
690 + link_to_folder,
691 + link_style,
692 + inline_specs
693 + )
694 +
695 + [" #{name} - #{colorize_status(status)}" | message_parts]
696 + else
697 + message_parts
480 698 end
481 699
482 - status =
483 - get_package_status_in_file(
484 - acc,
485 - name,
486 - package_rules_content,
487 - current_file_content,
488 - link_to_folder
489 - )
700 + # Add sub-rules status if any
701 + message_parts =
702 + if Enum.any?(sub_rules) do
703 + sub_rule_lines =
704 + sub_rules
705 + |> Enum.filter(fn sub_rule_name ->
706 + sub_rule_path = Path.join([path, "usage-rules", "#{sub_rule_name}.md"])
707 + Igniter.exists?(acc, sub_rule_path)
708 + end)
709 + |> Enum.map(fn sub_rule_name ->
710 + sub_rule_path = Path.join([path, "usage-rules", "#{sub_rule_name}.md"])
490 711
491 - Igniter.add_notice(acc, "#{name}: #{colorize_status(status)}")
712 + sub_rule_content =
713 + case Rewrite.source(acc.rewrite, sub_rule_path) do
714 + {:ok, source} -> Rewrite.Source.get(source, :content)
715 + {:error, _} -> File.read!(sub_rule_path)
716 + end
717 +
718 + sub_status =
719 + get_package_status_in_file(
720 + acc,
721 + "#{name}:#{sub_rule_name}",
722 + sub_rule_content,
723 + current_file_content,
724 + link_to_folder,
725 + link_style,
726 + inline_specs
727 + )
728 +
729 + " #{name}:#{sub_rule_name} - #{colorize_status(sub_status)}"
730 + end)
731 +
732 + message_parts ++ sub_rule_lines
733 + else
734 + message_parts
735 + end
736 +
737 + # Add the combined notice if we have anything to show
738 + if Enum.any?(message_parts) do
739 + # For file comparison, don't add the standalone package name
740 + full_message = Enum.join([name | Enum.reverse(message_parts)], "\n")
741 + Igniter.add_notice(acc, full_message)
742 + else
743 + acc
744 + end
492 745 end)
493 746 end
494 747
495 748 defp list_packages_without_comparison(igniter, packages_with_rules) do
496 - Enum.reduce(packages_with_rules, igniter, fn {name, _path}, acc ->
497 - Igniter.add_notice(acc, "#{name}: #{IO.ANSI.green()}has usage rules#{IO.ANSI.reset()}")
749 + Enum.reduce(packages_with_rules, igniter, fn {package_name, package_path}, acc ->
750 + # Ensure name is a string
751 + package_name = to_string(package_name)
752 +
753 + # Check for main package and sub-rules
754 + usage_rules_path = Path.join(package_path, "usage-rules.md")
755 + has_main = Igniter.exists?(acc, usage_rules_path)
756 + available_sub_rules = find_available_sub_rules(igniter, package_path)
757 +
758 + # Build message lines for this specific package
759 + lines = []
760 +
761 + # Add main package line if it exists
762 + lines =
763 + if has_main do
764 + [" #{package_name} - #{IO.ANSI.green()}has usage rules#{IO.ANSI.green()}"] ++ lines
765 + else
766 + lines
767 + end
768 +
769 + # Add sub-rules lines if they exist
770 + lines =
771 + if Enum.any?(available_sub_rules) do
772 + valid_sub_rules =
773 + available_sub_rules
774 + |> Enum.filter(fn sub_rule_name ->
775 + sub_rule_path = Path.join([package_path, "usage-rules", "#{sub_rule_name}.md"])
776 + Igniter.exists?(acc, sub_rule_path)
777 + end)
778 + |> Enum.sort()
779 +
780 + sub_rule_lines =
781 + Enum.map(valid_sub_rules, fn sub_rule_name ->
782 + " #{package_name}:#{sub_rule_name} - #{IO.ANSI.green()}has sub-rule#{IO.ANSI.green()}"
783 + end)
784 +
785 + lines ++ sub_rule_lines
786 + else
787 + lines
788 + end
789 +
790 + # Add notice for this package if we have anything to show
791 + if Enum.any?(lines) do
792 + message = Enum.join([package_name | Enum.reverse(lines)], "\n")
793 + Igniter.add_notice(acc, message)
794 + else
795 + acc
796 + end
498 797 end)
499 798 end
500 799
  @@ -520,8 +819,8 @@ if Code.ensure_loaded?(Igniter) do
520 819 packages,
521 820 link_to_folder,
522 821 link_style,
523 - builtins,
524 - builtins_link
822 + inline_specs,
823 + remove_missing
525 824 ) do
526 825 if link_to_folder do
527 826 generate_usage_rules_with_folder_links(
  @@ -529,19 +828,76 @@ if Code.ensure_loaded?(Igniter) do
529 828 packages,
530 829 link_to_folder,
531 830 link_style,
532 - builtins,
533 - builtins_link
831 + inline_specs,
832 + remove_missing
534 833 )
535 834 else
536 - generate_usage_rules_inline(igniter, packages, builtins)
835 + generate_usage_rules_inline(igniter, packages, remove_missing)
537 836 end
538 837 end
539 838
540 - defp generate_usage_rules_inline(igniter, packages, builtins) do
839 + defp extract_existing_package_names(content) do
840 + # Extract package names from <!-- package-name-start --> markers
841 + Regex.scan(~r/<!-- ([^-]+(?::[^-]+)?)-start -->/, content, capture: :all_but_first)
842 + |> Enum.map(fn [name] -> name end)
843 + end
844 +
845 + defp remove_missing_packages_from_content(content, packages_to_keep) do
846 + existing_packages = extract_existing_package_names(content)
847 + packages_to_remove = existing_packages -- packages_to_keep
848 +
849 + Enum.reduce(packages_to_remove, content, fn package_name, acc ->
850 + case String.split(acc, [
851 + "<!-- #{package_name}-start -->\n",
852 + "\n<!-- #{package_name}-end -->"
853 + ]) do
854 + [prelude, _, postlude] ->
855 + # Remove the package section, keeping proper spacing
856 + prelude <> postlude
857 +
858 + _ ->
859 + acc
860 + end
861 + end)
862 + end
863 +
864 + defp update_usage_rules_content(current_packages_contents, package_contents, remove_missing) do
865 + # Apply remove_missing logic if requested
866 + cleaned_content =
867 + if remove_missing do
868 + packages_to_keep = Enum.map(package_contents, fn {name, _} -> name end)
869 + remove_missing_packages_from_content(current_packages_contents, packages_to_keep)
870 + else
871 + current_packages_contents
872 + end
873 +
874 + Enum.reduce(package_contents, cleaned_content, fn {name, package_content}, acc ->
875 + case String.split(acc, [
876 + "<!-- #{name}-start -->\n",
877 + "\n<!-- #{name}-end -->"
878 + ]) do
879 + [prelude, _, postlude] ->
880 + prelude <> package_content <> postlude
881 +
882 + _ ->
883 + acc <> "\n" <> package_content
884 + end
885 + end)
886 + end
887 +
888 + defp generate_usage_rules_inline(igniter, packages, remove_missing) do
541 889 package_contents =
542 890 packages
543 - |> Enum.map(fn {name, path} ->
544 - usage_rules_path = Path.join(path, "usage-rules.md")
891 + |> Enum.map(fn {name, path, sub_rule} ->
892 + {usage_rules_path, section_name} =
893 + case sub_rule do
894 + nil ->
895 + {Path.join(path, "usage-rules.md"), to_string(name)}
896 +
897 + sub_rule_name ->
898 + {Path.join([path, "usage-rules", "#{sub_rule_name}.md"]),
899 + "#{name}:#{sub_rule_name}"}
900 + end
545 901
546 902 content =
547 903 case Rewrite.source(igniter.rewrite, usage_rules_path) do
  @@ -549,21 +905,24 @@ if Code.ensure_loaded?(Igniter) do
549 905 {:error, _} -> File.read!(usage_rules_path)
550 906 end
551 907
552 - description = get_package_description(name)
908 + description =
909 + case sub_rule do
910 + nil -> get_package_description(name)
911 + # Sub-rules don't get package descriptions
912 + _ -> ""
913 + end
914 +
553 915 description_part = if description == "", do: "", else: "_#{description}_\n\n"
554 916
555 - {name,
556 - "<!-- #{name}-start -->\n" <>
557 - "## #{name} usage\n" <>
917 + {section_name,
918 + "<!-- #{section_name}-start -->\n" <>
919 + "## #{section_name} usage\n" <>
558 920 description_part <>
559 921 content <>
560 - "\n<!-- #{name}-end -->"}
922 + "\n<!-- #{section_name}-end -->"}
561 923 end)
562 924
563 - builtin_contents = get_builtin_contents(builtins)
564 -
565 - all_contents = package_contents ++ builtin_contents
566 - all_rules_content = Enum.map_join(all_contents, "\n", &elem(&1, 1))
925 + all_rules_content = Enum.map_join(package_contents, "\n", &elem(&1, 1))
567 926
568 927 full_contents_for_new_file =
569 928 "<!-- usage-rules-start -->\n" <>
  @@ -585,19 +944,11 @@ if Code.ensure_loaded?(Igniter) do
585 944 "\n<!-- usage-rules-end -->"
586 945 ]) do
587 946 [prelude, current_packages_contents, postlude] ->
588 - Enum.reduce(all_contents, current_packages_contents, fn {name, package_content},
589 - acc ->
590 - case String.split(acc, [
591 - "<!-- #{name}-start -->\n",
592 - "\n<!-- #{name}-end -->"
593 - ]) do
594 - [prelude, _, postlude] ->
595 - prelude <> package_content <> postlude
596 -
597 - _ ->
598 - acc <> "\n" <> package_content
599 - end
600 - end)
947 + update_usage_rules_content(
948 + current_packages_contents,
949 + package_contents,
950 + remove_missing
951 + )
601 952 |> then(fn content ->
602 953 # Ensure header is present
603 954 content_with_header =
  @@ -633,125 +984,122 @@ if Code.ensure_loaded?(Igniter) do
633 984 packages,
634 985 folder_name,
635 986 link_style,
636 - builtins,
637 - builtins_link
987 + inline_specs,
988 + remove_missing
638 989 ) do
639 990 # Create individual files for each package in the folder (unless folder is "deps")
640 991 igniter =
641 992 if folder_name == "deps" do
642 993 igniter
643 994 else
644 - # Create builtin files in the target folder only if builtins_link is true
645 - igniter =
646 - if builtins_link do
647 - Enum.reduce(builtins, igniter, fn builtin, acc ->
648 - builtin_source_path =
649 - Path.join([:code.priv_dir(:usage_rules), "builtins", "#{builtin}.md"])
650 -
651 - builtin_file_path = Path.join(folder_name, "#{builtin}.md")
652 - content = File.read!(builtin_source_path)
653 -
654 - Igniter.create_or_update_file(
655 - acc,
656 - builtin_file_path,
657 - content,
658 - fn source ->
659 - Rewrite.Source.update(source, :content, content)
660 - end
661 - )
662 - end)
995 + Enum.reduce(packages, igniter, fn {name, path, sub_rule}, acc ->
996 + # Skip creating files for packages that should be inlined
997 + if should_inline_package?(name, sub_rule, inline_specs) do
998 + acc
663 999 else
664 - igniter
1000 + {usage_rules_path, target_file_name} =
1001 + case sub_rule do
1002 + nil ->
1003 + {Path.join(path, "usage-rules.md"), "#{name}.md"}
1004 +
1005 + sub_rule_name ->
1006 + {Path.join([path, "usage-rules", "#{sub_rule_name}.md"]),
1007 + "#{name}_#{sub_rule_name}.md"}
1008 + end
1009 +
1010 + content =
1011 + case Rewrite.source(acc.rewrite, usage_rules_path) do
1012 + {:ok, source} -> Rewrite.Source.get(source, :content)
1013 + {:error, _} -> File.read!(usage_rules_path)
1014 + end
1015 +
1016 + package_file_path = Path.join(folder_name, target_file_name)
1017 +
1018 + Igniter.create_or_update_file(
1019 + acc,
1020 + package_file_path,
1021 + content,
1022 + fn source ->
1023 + Rewrite.Source.update(source, :content, content)
1024 + end
1025 + )
1026 + end
1027 + end)
1028 + end
1029 +
1030 + # Then, create the main file with links or inline content
1031 + package_contents =
1032 + packages
1033 + |> Enum.map(fn {name, path, sub_rule} ->
1034 + section_name =
1035 + case sub_rule do
1036 + nil -> to_string(name)
1037 + sub_rule_name -> "#{name}:#{sub_rule_name}"
665 1038 end
666 1039
667 - Enum.reduce(packages, igniter, fn {name, path}, acc ->
668 - usage_rules_path = Path.join(path, "usage-rules.md")
1040 + description =
1041 + case sub_rule do
1042 + nil -> get_package_description(name)
1043 + # Sub-rules don't get package descriptions
1044 + _ -> ""
1045 + end
669 1046
670 - content =
671 - case Rewrite.source(acc.rewrite, usage_rules_path) do
1047 + description_part = if description == "", do: "", else: "_#{description}_\n\n"
1048 +
1049 + content =
1050 + if should_inline_package?(name, sub_rule, inline_specs) do
1051 + # Inline the actual content
1052 + {usage_rules_path, _} =
1053 + case sub_rule do
1054 + nil ->
1055 + {Path.join(path, "usage-rules.md"), "#{name}.md"}
1056 +
1057 + sub_rule_name ->
1058 + {Path.join([path, "usage-rules", "#{sub_rule_name}.md"]),
1059 + "#{name}_#{sub_rule_name}.md"}
1060 + end
1061 +
1062 + case Rewrite.source(igniter.rewrite, usage_rules_path) do
672 1063 {:ok, source} -> Rewrite.Source.get(source, :content)
673 1064 {:error, _} -> File.read!(usage_rules_path)
674 1065 end
1066 + else
1067 + # Create link
1068 + case sub_rule do
1069 + nil ->
1070 + case {link_style, folder_name} do
1071 + {"at", "deps"} -> "@deps/#{name}/usage-rules.md"
1072 + {"at", _} -> "@#{folder_name}/#{name}.md"
1073 + {_, "deps"} -> "[#{name} usage rules](deps/#{name}/usage-rules.md)"
1074 + _ -> "[#{name} usage rules](#{folder_name}/#{name}.md)"
1075 + end
675 1076
676 - package_file_path = Path.join(folder_name, "#{name}.md")
1077 + sub_rule_name ->
1078 + case {link_style, folder_name} do
1079 + {"at", "deps"} ->
1080 + "@deps/#{name}/usage-rules/#{sub_rule_name}.md"
677 1081
678 - Igniter.create_or_update_file(
679 - acc,
680 - package_file_path,
681 - content,
682 - fn source ->
683 - Rewrite.Source.update(source, :content, content)
1082 + {"at", _} ->
1083 + "@#{folder_name}/#{name}_#{sub_rule_name}.md"
1084 +
1085 + {_, "deps"} ->
1086 + "[#{section_name} usage rules](deps/#{name}/usage-rules/#{sub_rule_name}.md)"
1087 +
1088 + _ ->
1089 + "[#{section_name} usage rules](#{folder_name}/#{name}_#{sub_rule_name}.md)"
1090 + end
684 1091 end
685 - )
686 - end)
687 - end
688 -
689 - # Then, create the main file with links
690 - package_contents =
691 - packages
692 - |> Enum.map(fn {name, _path} ->
693 - link_content =
694 - case {link_style, folder_name} do
695 - {"at", "deps"} -> "@deps/#{name}/usage-rules.md"
696 - {"at", _} -> "@#{folder_name}/#{name}.md"
697 - {_, "deps"} -> "[#{name} usage rules](deps/#{name}/usage-rules.md)"
698 - _ -> "[#{name} usage rules](#{folder_name}/#{name}.md)"
699 1092 end
700 1093
701 - description = get_package_description(name)
702 - description_part = if description == "", do: "", else: "_#{description}_\n\n"
703 -
704 - {name,
705 - "<!-- #{name}-start -->\n" <>
706 - "## #{name} usage\n" <>
1094 + {section_name,
1095 + "<!-- #{section_name}-start -->\n" <>
1096 + "## #{section_name} usage\n" <>
707 1097 description_part <>
708 - link_content <>
709 - "\n<!-- #{name}-end -->"}
1098 + content <>
1099 + "\n<!-- #{section_name}-end -->"}
710 1100 end)
711 1101
712 - builtin_contents =
713 - if builtins_link do
714 - # If builtins_link is true, create links
715 - builtins
716 - |> Enum.map(fn builtin ->
717 - link_content =
718 - case {link_style, folder_name} do
719 - {"at", "deps"} ->
720 - "@deps/usage_rules/priv/builtins/#{builtin}.md"
721 -
722 - {"at", _} ->
723 - "@#{folder_name}/#{builtin}.md"
724 -
725 - {_, "deps"} ->
726 - "[#{builtin} usage rules](deps/usage_rules/priv/builtins/#{builtin}.md)"
727 -
728 - _ ->
729 - "[#{builtin} usage rules](#{folder_name}/#{builtin}.md)"
730 - end
731 -
732 - description =
733 - case builtin do
734 - "elixir" -> "Core Elixir language features and standard library"
735 - "otp" -> "OTP (Open Telecom Platform) behaviors and patterns"
736 - _ -> ""
737 - end
738 -
739 - description_part = if description == "", do: "", else: "_#{description}_\n\n"
740 -
741 - {String.to_atom(builtin),
742 - "<!-- #{builtin}-start -->\n" <>
743 - "## #{builtin} usage\n" <>
744 - description_part <>
745 - link_content <>
746 - "\n<!-- #{builtin}-end -->"}
747 - end)
748 - else
749 - # If builtins_link is false (default), inline the content
750 - get_builtin_contents(builtins)
751 - end
752 -
753 - all_contents = package_contents ++ builtin_contents
754 - all_rules_content = Enum.map_join(all_contents, "\n", &elem(&1, 1))
1102 + all_rules_content = Enum.map_join(package_contents, "\n", &elem(&1, 1))
755 1103
756 1104 full_contents_for_new_file =
757 1105 "<!-- usage-rules-start -->\n" <>
  @@ -773,19 +1121,11 @@ if Code.ensure_loaded?(Igniter) do
773 1121 "\n<!-- usage-rules-end -->"
774 1122 ]) do
775 1123 [prelude, current_packages_contents, postlude] ->
776 - Enum.reduce(all_contents, current_packages_contents, fn {name, package_content},
777 - acc ->
778 - case String.split(acc, [
779 - "<!-- #{name}-start -->\n",
780 - "\n<!-- #{name}-end -->"
781 - ]) do
782 - [prelude, _, postlude] ->
783 - prelude <> package_content <> postlude
784 -
785 - _ ->
786 - acc <> "\n" <> package_content
787 - end
788 - end)
1124 + update_usage_rules_content(
1125 + current_packages_contents,
1126 + package_contents,
1127 + remove_missing
1128 + )
789 1129 |> then(fn content ->
790 1130 # Ensure header is present
791 1131 content_with_header =
  @@ -908,7 +1248,9 @@ if Code.ensure_loaded?(Igniter) do
908 1248 name,
909 1249 package_rules_content,
910 1250 file_content,
911 - link_to_folder
1251 + link_to_folder,
1252 + link_style,
1253 + inline_specs
912 1254 ) do
913 1255 package_start_marker = "<!-- #{name}-start -->"
914 1256 package_end_marker = "<!-- #{name}-end -->"
  @@ -916,9 +1258,49 @@ if Code.ensure_loaded?(Igniter) do
916 1258 case String.split(file_content, [package_start_marker, package_end_marker]) do
917 1259 [_, current_package_content, _] ->
918 1260 # Package is present in file, check if content matches
1261 + {package_name, sub_rule} =
1262 + case String.split(name, ":", parts: 2) do
1263 + [l, r] -> {l, r}
1264 + [l] -> {l, nil}
1265 + end
1266 +
919 1267 expected_content =
920 - if link_to_folder do
921 - "\n## #{name} usage\n@#{link_to_folder}/#{name}.md\n"
1268 + if link_to_folder && !should_inline_package?(package_name, sub_rule, inline_specs) do
1269 + # Generate the correct link format based on link_style
1270 + link_content =
1271 + case sub_rule do
1272 + nil ->
1273 + case {link_style, link_to_folder} do
1274 + {"at", "deps"} ->
1275 + "@deps/#{package_name}/usage-rules.md"
1276 +
1277 + {"at", _} ->
1278 + "@#{link_to_folder}/#{package_name}.md"
1279 +
1280 + {_, "deps"} ->
1281 + "[#{package_name} usage rules](deps/#{package_name}/usage-rules.md)"
1282 +
1283 + _ ->
1284 + "[#{package_name} usage rules](#{link_to_folder}/#{package_name}.md)"
1285 + end
1286 +
1287 + sub_rule_name ->
1288 + case {link_style, link_to_folder} do
1289 + {"at", "deps"} ->
1290 + "@deps/#{package_name}/usage-rules/#{sub_rule_name}.md"
1291 +
1292 + {"at", _} ->
1293 + "@#{link_to_folder}/#{package_name}_#{sub_rule_name}.md"
1294 +
1295 + {_, "deps"} ->
1296 + "[#{name} usage rules](deps/#{package_name}/usage-rules/#{sub_rule_name}.md)"
1297 +
1298 + _ ->
1299 + "[#{name} usage rules](#{link_to_folder}/#{package_name}_#{sub_rule_name}.md)"
1300 + end
1301 + end
1302 +
1303 + "\n## #{name} usage\n#{link_content}\n"
922 1304 else
923 1305 "\n## #{name} usage\n" <> package_rules_content <> "\n"
924 1306 end
  @@ -941,7 +1323,30 @@ if Code.ensure_loaded?(Igniter) do
941 1323 end
942 1324
943 1325 defp check_linked_file_status(igniter, name, expected_content, link_to_folder) do
944 - linked_file_path = Path.join(link_to_folder, "#{name}.md")
1326 + # Generate the correct file path based on package name and sub-rule
1327 + {package_name, sub_rule} =
1328 + case String.split(name, ":", parts: 2) do
1329 + [l, r] -> {l, r}
1330 + [l] -> {l, nil}
1331 + end
1332 +
1333 + linked_file_path =
1334 + case sub_rule do
1335 + nil ->
1336 + case link_to_folder do
1337 + "deps" -> Path.join(["deps", package_name, "usage-rules.md"])
1338 + _ -> Path.join(link_to_folder, "#{package_name}.md")
1339 + end
1340 +
1341 + sub_rule_name ->
1342 + case link_to_folder do
1343 + "deps" ->
1344 + Path.join(["deps", package_name, "usage-rules", "#{sub_rule_name}.md"])
1345 +
1346 + _ ->
1347 + Path.join(link_to_folder, "#{package_name}_#{sub_rule_name}.md")
1348 + end
1349 + end
945 1350
946 1351 if Igniter.exists?(igniter, linked_file_path) do
947 1352 actual_content =
  @@ -967,9 +1372,9 @@ if Code.ensure_loaded?(Igniter) do
967 1372 end
968 1373 end
969 1374
970 - defp colorize_status("present"), do: "#{IO.ANSI.green()}present#{IO.ANSI.reset()}"
971 - defp colorize_status("stale"), do: "#{IO.ANSI.yellow()}stale#{IO.ANSI.reset()}"
972 - defp colorize_status("missing"), do: "#{IO.ANSI.red()}missing#{IO.ANSI.reset()}"
1375 + defp colorize_status("present"), do: "#{IO.ANSI.green()}present#{IO.ANSI.green()}"
1376 + defp colorize_status("stale"), do: "#{IO.ANSI.yellow()}stale#{IO.ANSI.green()}"
1377 + defp colorize_status("missing"), do: "#{IO.ANSI.red()}missing#{IO.ANSI.green()}"
973 1378 end
974 1379 else
975 1380 defmodule Mix.Tasks.UsageRules.Sync do
Loading more files…