Current section

42 Versions

Jump to

Compare versions

5 files changed
+255 additions
-31 deletions
  @@ -5,6 +5,15 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
5 5
6 6 <!-- changelog -->
7 7
8 + ## v0.1.4 (2025-06-06)
9 +
10 +
11 +
12 +
13 + ### Improvements:
14 +
15 + * add `--link-to-folder` option
16 +
8 17 ## v0.1.3 (2025-05-24)
  @@ -4,10 +4,16 @@
4 4
5 5 You'll note this package itself doesn't have a usage-rules.md. Its a simple tool that likely would not benefit from having a usage-rules.md file.
6 6
7 + `usage-rules.md` is not an existing standard, rather it is a community initiative that may evolve over time as adoption grows and feedback is gathered. We encourage experimentation and welcome input on how to make this approach more useful for the broader Elixir ecosystem.
8 +
7 9 ## For Package Authors
8 10
9 11 Even if you don't want to use LLMs, its very possible that your users will, and they will often come to you with hallucinations from their LLMs and try to get your help with it. Writing a `usage-rules.md` file is a great way to stop this sort of thing 😁
10 12
13 + We don't really know what makes great usage-rules.md files yet. Ash Framework is experimenting with quite fleshed out usage rules which seems to be working quite well. See [Ash Framework's usage-rules.md](https://github.com/ash-project/ash/blob/main/usage-rules.md) for one such large example. Perhaps for your package or framework only a few lines are necessary. We will all have to adjust over time.
14 +
15 + One quick tip is to have an agent begin the work of writing rules for you, by pointing it at your docs and asking it to write a `usage-rules.md` file in a condensed format that would be useful for agents to work with your tool. Then, aggressively prune and edit it to your taste.
16 +
11 17 ## Key Features
12 18
13 19 1. **Dependency Rules Collection**: Automatically discovers and collects usage rules from dependencies that provide `usage-rules.md` files in their package directory
  @@ -6,7 +6,7 @@
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.3">>}.
9 + {<<"version">>,<<"0.1.4">>}.
10 10 {<<"description">>,
11 11 <<"A dev tool for Elixir projects to gather LLM usage rules from dependencies">>}.
12 12 {<<"elixir">>,<<"~> 1.18">>}.
  @@ -22,6 +22,6 @@
22 22 [[{<<"name">>,<<"igniter">>},
23 23 {<<"app">>,<<"igniter">>},
24 24 {<<"optional">>,true},
25 - {<<"requirement">>,<<"~> 0.6 and >= 0.6.2">>},
25 + {<<"requirement">>,<<"~> 0.6 and >= 0.6.6">>},
26 26 {<<"repository">>,<<"hexpm">>}]]}.
27 27 {<<"build_tools">>,[<<"mix">>]}.
  @@ -21,6 +21,7 @@ defmodule Mix.Tasks.UsageRules.Sync.Docs do
21 21 * `--all` - Gather usage rules from all dependencies that have them
22 22 * `--list` - List all dependencies with usage rules. If a file is provided, shows status (present, missing, stale)
23 23 * `--remove` - Remove specified packages from the target file instead of adding them
24 + * `--link-to-folder <folder>` - Save usage rules for each package in separate files within the specified folder and create CLAUDE.md style links
24 25
25 26 ## Examples
26 27
  @@ -48,6 +49,26 @@ defmodule Mix.Tasks.UsageRules.Sync.Docs do
48 49 ```sh
49 50 mix usage_rules.sync rules.md ash phoenix --remove
50 51 ```
52 +
53 + Save usage rules to individual files in a folder with links to individual files:
54 + ```sh
55 + mix usage_rules.sync rules.md ash phoenix --link-to-folder rules
56 + ```
57 +
58 + Combine all dependencies with folder links with links to individual files:
59 + ```sh
60 + mix usage_rules.sync rules.md --all --link-to-folder docs
61 + ```
62 +
63 + Check status of packages using folder links:
64 + ```sh
65 + mix usage_rules.sync rules.md --list --link-to-folder rules
66 + ```
67 +
68 + Remove packages and their folder files:
69 + ```sh
70 + mix usage_rules.sync rules.md ash phoenix --remove --link-to-folder rules
71 + ```
51 72 """
52 73 end
53 74 end
  @@ -74,7 +95,8 @@ if Code.ensure_loaded?(Igniter) do
74 95 schema: [
75 96 all: :boolean,
76 97 list: :boolean,
77 - remove: :boolean
98 + remove: :boolean,
99 + link_to_folder: :string
78 100 ]
79 101 }
80 102 end
  @@ -112,6 +134,7 @@ if Code.ensure_loaded?(Igniter) do
112 134 all_option = igniter.args.options[:all]
113 135 list_option = igniter.args.options[:list]
114 136 remove_option = igniter.args.options[:remove]
137 + link_to_folder = igniter.args.options[:link_to_folder]
115 138 provided_packages = igniter.args.positional.packages
116 139
117 140 cond do
  @@ -131,29 +154,33 @@ if Code.ensure_loaded?(Igniter) do
131 154 (all_option || list_option) && !Enum.empty?(provided_packages) ->
132 155 Igniter.add_issue(igniter, "Cannot specify packages when using --all or --list options")
133 156
134 - # If no packages are given and neither --list nor --all nor --remove is set, add error
135 - Enum.empty?(provided_packages) && !all_option && !list_option && !remove_option ->
136 - add_usage_error(igniter)
137 -
138 157 # If --all is used without a file, add error
139 158 all_option && is_nil(igniter.args.positional[:file]) ->
140 159 Igniter.add_issue(igniter, "--all option requires a file to write to")
141 160
161 + # If --link-to-folder is used without a file, add error
162 + link_to_folder && is_nil(igniter.args.positional[:file]) ->
163 + Igniter.add_issue(igniter, "--link-to-folder option requires a file to write to")
164 +
165 + # If no packages are given and neither --list nor --all nor --remove is set, add error
166 + Enum.empty?(provided_packages) && !all_option && !list_option && !remove_option ->
167 + add_usage_error(igniter)
168 +
142 169 # Handle --remove option
143 170 remove_option ->
144 - handle_remove_packages(igniter, provided_packages)
171 + handle_remove_packages(igniter, provided_packages, link_to_folder)
145 172
146 173 # Handle --all option
147 174 all_option ->
148 - handle_all_option(igniter, all_deps)
175 + handle_all_option(igniter, all_deps, link_to_folder)
149 176
150 177 # Handle --list option
151 178 list_option ->
152 - handle_list_option(igniter, all_deps)
179 + handle_list_option(igniter, all_deps, link_to_folder)
153 180
154 181 # Handle specific packages
155 182 true ->
156 - handle_specific_packages(igniter, all_deps, provided_packages)
183 + handle_specific_packages(igniter, all_deps, provided_packages, link_to_folder)
157 184 end
158 185 end
159 186
  @@ -196,10 +223,19 @@ if Code.ensure_loaded?(Igniter) do
196 223
197 224 mix usage_rules.sync <file> <packages...> --remove
198 225 Remove specific packages from the target file
226 +
227 + mix usage_rules.sync <file> <packages...> --link-to-folder <folder>
228 + Save usage rules for each package in separate files within the specified folder and create CLAUDE.md style links
229 +
230 + mix usage_rules.sync <file> --list --link-to-folder <folder>
231 + List packages with usage rules and check status against folder links
232 +
233 + mix usage_rules.sync <file> <packages...> --remove --link-to-folder <folder>
234 + Remove specific packages from the target file and delete their folder files
199 235 """)
200 236 end
201 237
202 - defp handle_all_option(igniter, all_deps) do
238 + defp handle_all_option(igniter, all_deps, link_to_folder) do
203 239 all_packages_with_rules = get_packages_with_usage_rules(igniter, all_deps)
204 240
205 241 igniter
  @@ -211,10 +247,10 @@ if Code.ensure_loaded?(Igniter) do
211 247 Igniter.add_notice(acc, "Including usage rules for: #{name}")
212 248 end)
213 249 end)
214 - |> generate_usage_rules_file(all_packages_with_rules)
250 + |> generate_usage_rules_file(all_packages_with_rules, link_to_folder)
215 251 end
216 252
217 - defp handle_list_option(igniter, all_deps) do
253 + defp handle_list_option(igniter, all_deps, link_to_folder) do
218 254 packages_with_rules = get_packages_with_usage_rules(igniter, all_deps)
219 255
220 256 if Enum.empty?(packages_with_rules) do
  @@ -223,14 +259,19 @@ if Code.ensure_loaded?(Igniter) do
223 259 file_path = igniter.args.positional[:file]
224 260
225 261 if file_path do
226 - list_packages_with_file_comparison(igniter, packages_with_rules, file_path)
262 + list_packages_with_file_comparison(
263 + igniter,
264 + packages_with_rules,
265 + file_path,
266 + link_to_folder
267 + )
227 268 else
228 269 list_packages_without_comparison(igniter, packages_with_rules)
229 270 end
230 271 end
231 272 end
232 273
233 - defp handle_specific_packages(igniter, all_deps, provided_packages) do
274 + defp handle_specific_packages(igniter, all_deps, provided_packages, link_to_folder) do
234 275 packages =
235 276 all_deps
236 277 |> Enum.filter(fn {name, _path} ->
  @@ -246,14 +287,14 @@ if Code.ensure_loaded?(Igniter) do
246 287 end
247 288 end)
248 289
249 - generate_usage_rules_file(igniter, packages)
290 + generate_usage_rules_file(igniter, packages, link_to_folder)
250 291 end
251 292
252 - defp handle_remove_packages(igniter, provided_packages) do
293 + defp handle_remove_packages(igniter, provided_packages, link_to_folder) do
253 294 file_path = igniter.args.positional[:file]
254 295
255 296 if Igniter.exists?(igniter, file_path) do
256 - remove_packages_from_file(igniter, file_path, provided_packages)
297 + remove_packages_from_file(igniter, file_path, provided_packages, link_to_folder)
257 298 else
258 299 Igniter.add_issue(igniter, "File #{file_path} does not exist")
259 300 end
  @@ -267,21 +308,33 @@ if Code.ensure_loaded?(Igniter) do
267 308 end)
268 309 end
269 310
270 - defp list_packages_with_file_comparison(igniter, packages_with_rules, file_path) do
311 + defp list_packages_with_file_comparison(
312 + igniter,
313 + packages_with_rules,
314 + file_path,
315 + link_to_folder
316 + ) do
271 317 current_file_content = read_current_file_content(igniter, file_path)
272 318
273 319 Enum.reduce(packages_with_rules, igniter, fn {name, path}, acc ->
274 320 usage_rules_path = Path.join(path, "usage-rules.md")
275 321
276 322 package_rules_content =
277 - case Rewrite.source(igniter.rewrite, usage_rules_path) do
323 + case Rewrite.source(acc.rewrite, usage_rules_path) do
278 324 {:ok, source} -> Rewrite.Source.get(source, :content)
279 325 {:error, _} -> File.read!(usage_rules_path)
280 326 end
281 327
282 - status = get_package_status_in_file(name, package_rules_content, current_file_content)
283 - colored_status = colorize_status(status)
284 - Igniter.add_notice(acc, "#{name}: #{colored_status}")
328 + status =
329 + get_package_status_in_file(
330 + acc,
331 + name,
332 + package_rules_content,
333 + current_file_content,
334 + link_to_folder
335 + )
336 +
337 + Igniter.add_notice(acc, "#{name}: #{colorize_status(status)}")
285 338 end)
286 339 end
287 340
  @@ -308,7 +361,15 @@ if Code.ensure_loaded?(Igniter) do
308 361 end
309 362 end
310 363
311 - defp generate_usage_rules_file(igniter, packages) do
364 + defp generate_usage_rules_file(igniter, packages, link_to_folder) do
365 + if link_to_folder do
366 + generate_usage_rules_with_folder_links(igniter, packages, link_to_folder)
367 + else
368 + generate_usage_rules_inline(igniter, packages)
369 + end
370 + end
371 +
372 + defp generate_usage_rules_inline(igniter, packages) do
312 373 package_contents =
313 374 packages
314 375 |> Enum.map(fn {name, path} ->
  @@ -381,7 +442,112 @@ if Code.ensure_loaded?(Igniter) do
381 442 )
382 443 end
383 444
384 - defp remove_packages_from_file(igniter, file_path, packages_to_remove) do
445 + defp generate_usage_rules_with_folder_links(igniter, packages, folder_name) do
446 + # First, create individual files for each package in the folder
447 + igniter =
448 + Enum.reduce(packages, igniter, fn {name, path}, acc ->
449 + usage_rules_path = Path.join(path, "usage-rules.md")
450 +
451 + content =
452 + case Rewrite.source(acc.rewrite, usage_rules_path) do
453 + {:ok, source} -> Rewrite.Source.get(source, :content)
454 + {:error, _} -> File.read!(usage_rules_path)
455 + end
456 +
457 + package_file_path = Path.join(folder_name, "#{name}.md")
458 +
459 + Igniter.create_or_update_file(
460 + acc,
461 + package_file_path,
462 + content,
463 + fn source ->
464 + Rewrite.Source.update(source, :content, content)
465 + end
466 + )
467 + end)
468 +
469 + # Then, create the main file with links
470 + package_contents =
471 + packages
472 + |> Enum.map(fn {name, _path} ->
473 + {name,
474 + "<-- #{name}-start -->\n" <>
475 + "## #{name} usage\n" <>
476 + "@#{folder_name}/#{name}.md" <>
477 + "\n<-- #{name}-end -->"}
478 + end)
479 +
480 + package_rules_content = Enum.map_join(package_contents, "\n", &elem(&1, 1))
481 +
482 + full_contents_for_new_file =
483 + "<-- usage-rules-start -->\n" <>
484 + package_rules_content <>
485 + "\n<-- usage-rules-end -->"
486 +
487 + Igniter.create_or_update_file(
488 + igniter,
489 + igniter.args.positional[:file],
490 + full_contents_for_new_file,
491 + fn source ->
492 + current_contents = Rewrite.Source.get(source, :content)
493 +
494 + new_content =
495 + case String.split(current_contents, [
496 + "<-- usage-rules-start -->\n",
497 + "\n<-- usage-rules-end -->"
498 + ]) do
499 + [prelude, current_packages_contents, postlude] ->
500 + Enum.reduce(package_contents, current_packages_contents, fn {name,
501 + package_content},
502 + acc ->
503 + case String.split(acc, [
504 + "<-- #{name}-start -->\n",
505 + "\n<-- #{name}-end -->"
506 + ]) do
507 + [prelude, _, postlude] ->
508 + prelude <> package_content <> postlude
509 +
510 + _ ->
511 + acc <> "\n" <> package_content
512 + end
513 + end)
514 + |> then(fn content ->
515 + prelude <>
516 + "<-- usage-rules-start -->\n" <>
517 + content <>
518 + "\n<-- usage-rules-end -->" <>
519 + postlude
520 + end)
521 +
522 + _ ->
523 + current_contents <>
524 + "\n<-- usage-rules-start -->\n" <>
525 + package_rules_content <>
526 + "\n<-- usage-rules-end -->\n"
527 + end
528 +
529 + Rewrite.Source.update(source, :content, new_content)
530 + end
531 + )
532 + end
533 +
534 + defp remove_packages_from_file(igniter, file_path, packages_to_remove, link_to_folder) do
535 + # If using link-to-folder, also remove the individual package files
536 + igniter =
537 + if link_to_folder do
538 + Enum.reduce(packages_to_remove, igniter, fn package_name, acc ->
539 + package_file_path = Path.join(link_to_folder, "#{package_name}.md")
540 +
541 + if Igniter.exists?(acc, package_file_path) do
542 + Igniter.rm(acc, package_file_path)
543 + else
544 + acc
545 + end
546 + end)
547 + else
548 + igniter
549 + end
550 +
385 551 Igniter.update_file(igniter, file_path, fn source ->
386 552 current_contents = Rewrite.Source.get(source, :content)
387 553
  @@ -452,17 +618,33 @@ if Code.ensure_loaded?(Igniter) do
452 618 end
453 619 end
454 620
455 - defp get_package_status_in_file(name, package_rules_content, file_content) do
621 + defp get_package_status_in_file(
622 + igniter,
623 + name,
624 + package_rules_content,
625 + file_content,
626 + link_to_folder
627 + ) do
456 628 package_start_marker = "<-- #{name}-start -->"
457 629 package_end_marker = "<-- #{name}-end -->"
458 630
459 631 case String.split(file_content, [package_start_marker, package_end_marker]) do
460 632 [_, current_package_content, _] ->
461 633 # Package is present in file, check if content matches
462 - expected_content = "\n## #{name} usage\n" <> package_rules_content <> "\n"
634 + expected_content =
635 + if link_to_folder do
636 + "\n## #{name} usage\n@#{link_to_folder}/#{name}.md\n"
637 + else
638 + "\n## #{name} usage\n" <> package_rules_content <> "\n"
639 + end
463 640
464 641 if String.trim(current_package_content) == String.trim(expected_content) do
465 - "present"
642 + # If using link-to-folder, also check the linked file exists and matches
643 + if link_to_folder do
644 + check_linked_file_status(igniter, name, package_rules_content, link_to_folder)
645 + else
646 + "present"
647 + end
466 648 else
467 649 "stale"
468 650 end
  @@ -473,6 +655,33 @@ if Code.ensure_loaded?(Igniter) do
473 655 end
474 656 end
475 657
658 + defp check_linked_file_status(igniter, name, expected_content, link_to_folder) do
659 + linked_file_path = Path.join(link_to_folder, "#{name}.md")
660 +
661 + if Igniter.exists?(igniter, linked_file_path) do
662 + actual_content =
663 + case Rewrite.source(igniter.rewrite, linked_file_path) do
664 + {:ok, source} ->
665 + Rewrite.Source.get(source, :content)
666 +
667 + {:error, _} ->
668 + if File.exists?(linked_file_path) do
669 + File.read!(linked_file_path)
670 + else
671 + ""
672 + end
673 + end
674 +
675 + if String.trim(actual_content) == String.trim(expected_content) do
676 + "present"
677 + else
678 + "stale"
679 + end
680 + else
681 + "stale"
682 + end
683 + end
684 +
476 685 defp colorize_status("present"), do: "#{IO.ANSI.green()}present#{IO.ANSI.reset()}"
477 686 defp colorize_status("stale"), do: "#{IO.ANSI.yellow()}stale#{IO.ANSI.reset()}"
478 687 defp colorize_status("missing"), do: "#{IO.ANSI.red()}missing#{IO.ANSI.reset()}"
  @@ -1,7 +1,7 @@
1 1 defmodule UsageRules.MixProject do
2 2 use Mix.Project
3 3
4 - @version "0.1.3"
4 + @version "0.1.4"
5 5 @description """
6 6 A dev tool for Elixir projects to gather LLM usage rules from dependencies
7 7 """
  @@ -83,7 +83,7 @@ defmodule UsageRules.MixProject do
83 83
84 84 defp deps do
85 85 [
86 - {:igniter, "~> 0.6 and >= 0.6.2", optional: true},
86 + {:igniter, "~> 0.6 and >= 0.6.6", optional: true},
87 87 # dev dependencies
88 88 {:ex_doc, "~> 0.37-rc", only: [:dev, :test], runtime: false},
89 89 {:ex_check, "~> 0.12", only: [:dev, :test]},