Skip to content

Commit ed48192

Browse files
committed
fix: only generate reference links for deps with actual content
Composed skills were generating broken reference links in two ways: 1. Packages with only sub-rules (no main usage-rules.md) got a [pkg_name](references/pkg_name.md) link despite no file being created 2. Search docs and mix tasks were excluding deps without usage rules, even though hexdocs and mix tasks exist regardless Now reference links are only emitted when the corresponding file is actually created, while search docs and mix tasks include all matched deps. Fixes: #69
1 parent 93bcf2f commit ed48192

2 files changed

Lines changed: 73 additions & 17 deletions

File tree

lib/mix/tasks/usage_rules.sync.ex

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -808,10 +808,10 @@ if Code.ensure_loaded?(Igniter) do
808808
custom_description = skill_opts[:description]
809809

810810
# Resolve which packages to include in this skill (supports atoms and regexes)
811+
all_expanded = expand_dep_specs(usage_rule_specs, all_deps)
812+
811813
resolved_packages =
812-
usage_rule_specs
813-
|> expand_dep_specs(all_deps)
814-
|> Enum.filter(fn {_pkg_name, package_path, _mode} ->
814+
Enum.filter(all_expanded, fn {_pkg_name, package_path, _mode} ->
815815
package_has_usage_rules?(igniter, package_path)
816816
end)
817817

@@ -821,6 +821,7 @@ if Code.ensure_loaded?(Igniter) do
821821
skill_name,
822822
skill_dir,
823823
resolved_packages,
824+
all_expanded,
824825
custom_description
825826
)
826827
else
@@ -833,10 +834,11 @@ if Code.ensure_loaded?(Igniter) do
833834
skill_name,
834835
skill_dir,
835836
resolved_packages,
837+
all_expanded,
836838
custom_description
837839
) do
838840
skill_md =
839-
build_skill_md(igniter, skill_name, resolved_packages, custom_description)
841+
build_skill_md(igniter, skill_name, resolved_packages, all_expanded, custom_description)
840842

841843
igniter =
842844
Igniter.create_or_update_file(
@@ -886,7 +888,7 @@ if Code.ensure_loaded?(Igniter) do
886888
end)
887889
end
888890

889-
defp build_skill_md(igniter, skill_name, resolved_packages, custom_description) do
891+
defp build_skill_md(igniter, skill_name, resolved_packages, all_expanded, custom_description) do
890892
description =
891893
(custom_description || build_skill_description(skill_name, resolved_packages))
892894
|> truncate_description()
@@ -904,7 +906,7 @@ if Code.ensure_loaded?(Igniter) do
904906
"""
905907
|> String.trim_trailing()
906908

907-
body = build_skill_body(igniter, skill_name, resolved_packages)
909+
body = build_skill_body(igniter, skill_name, resolved_packages, all_expanded)
908910

909911
frontmatter <>
910912
"\n\n" <>
@@ -929,18 +931,23 @@ if Code.ensure_loaded?(Igniter) do
929931
end
930932
end
931933

932-
defp build_skill_body(igniter, _skill_name, resolved_packages) do
934+
defp build_skill_body(igniter, _skill_name, resolved_packages, all_expanded) do
933935
sections = []
934936

935-
# Sub-rules as references
937+
# Sub-rules as references (only from packages with usage rules)
936938
all_sub_rules =
937939
Enum.flat_map(resolved_packages, fn {_pkg_name, package_path, _mode} ->
938940
find_available_sub_rules(igniter, package_path)
939941
end)
940942

941-
# All packages are references
943+
# Only include main rule links for packages that have a main usage-rules.md
944+
# (a package may pass the filter via sub-rules alone, with no main file)
942945
all_main_rules =
943-
Enum.map(resolved_packages, fn {pkg_name, _path, _mode} -> pkg_name end)
946+
resolved_packages
947+
|> Enum.filter(fn {_pkg_name, package_path, _mode} ->
948+
read_dep_content(igniter, Path.join(package_path, "usage-rules.md")) != ""
949+
end)
950+
|> Enum.map(fn {pkg_name, _path, _mode} -> pkg_name end)
944951

945952
all_references =
946953
Enum.map(all_sub_rules, fn sub_rule ->
@@ -960,8 +967,8 @@ if Code.ensure_loaded?(Igniter) do
960967
sections
961968
end
962969

963-
# Search docs for all packages
964-
package_names = Enum.map(resolved_packages, &elem(&1, 0))
970+
# Search docs for all matched packages (hexdocs exist regardless of usage-rules)
971+
package_names = Enum.map(all_expanded, &elem(&1, 0))
965972

966973
search_flags = Enum.map_join(package_names, " ", &"-p #{&1}")
967974

@@ -978,9 +985,9 @@ if Code.ensure_loaded?(Igniter) do
978985
|> String.trim_trailing()
979986
]
980987

981-
# Mix tasks from all packages (at the bottom)
988+
# Mix tasks from all matched packages (at the bottom)
982989
all_mix_tasks =
983-
Enum.flat_map(resolved_packages, fn {pkg_name, _path, _mode} ->
990+
Enum.flat_map(all_expanded, fn {pkg_name, _path, _mode} ->
984991
discover_mix_tasks(pkg_name)
985992
|> Enum.map(fn {task, doc} -> {pkg_name, task, doc} end)
986993
end)

test/mix/tasks/usage_rules.sync_test.exs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,7 @@ defmodule Mix.Tasks.UsageRules.SyncTest do
945945
refute content =~ "Req"
946946
end
947947

948-
test "regex build skips deps without usage rules when rendering references and search docs" do
948+
test "regex build skips reference links for deps without usage rules but includes them in search docs" do
949949
igniter =
950950
project_with_deps(%{
951951
"deps/phoenix/usage-rules/ecto.md" => "# Phoenix Ecto",
@@ -967,12 +967,61 @@ defmodule Mix.Tasks.UsageRules.SyncTest do
967967

968968
content = file_content(igniter, ".claude/skills/phoenix-framework/SKILL.md")
969969

970+
# Sub-rule references are included
970971
assert content =~ "[ecto](references/ecto.md)"
971972
assert content =~ "[liveview](references/liveview.md)"
973+
974+
# Deps without usage rules should NOT have reference links
972975
refute content =~ "references/phoenix_ecto.md"
973976
refute content =~ "references/phoenix_html.md"
974-
refute content =~ "-p phoenix_ecto"
975-
refute content =~ "-p phoenix_html"
977+
978+
# Package with only sub-rules (no main usage-rules.md) should NOT have a main reference link
979+
refute content =~ "[phoenix](references/phoenix.md)"
980+
981+
# But search docs should include ALL matched deps (they have hexdocs regardless)
982+
assert content =~ "-p phoenix_ecto"
983+
assert content =~ "-p phoenix_html"
984+
assert content =~ "-p phoenix"
985+
end
986+
987+
test "deps with main usage-rules.md get reference links, sub-rules-only deps do not" do
988+
igniter =
989+
project_with_deps(%{
990+
# ash has a main usage-rules.md
991+
"deps/ash/usage-rules.md" => "# Ash Framework",
992+
# ash_postgres has only sub-rules, no main usage-rules.md
993+
"deps/ash_postgres/usage-rules/migrations.md" => "# Migrations",
994+
# ash_oban has no usage rules at all
995+
"deps/ash_oban/mix.exs" => "defmodule AshOban.MixProject, do: nil"
996+
})
997+
|> sync(
998+
skills: [
999+
location: ".claude/skills",
1000+
build: [
1001+
"ash-framework": [usage_rules: [:ash, ~r/^ash_/]]
1002+
]
1003+
]
1004+
)
1005+
|> assert_creates(".claude/skills/ash-framework/SKILL.md")
1006+
|> assert_creates(".claude/skills/ash-framework/references/ash.md")
1007+
|> assert_creates(".claude/skills/ash-framework/references/migrations.md")
1008+
1009+
content = file_content(igniter, ".claude/skills/ash-framework/SKILL.md")
1010+
1011+
# ash has main usage-rules.md → gets a reference link
1012+
assert content =~ "[ash](references/ash.md)"
1013+
1014+
# ash_postgres has sub-rules → sub-rule link present, but no main link
1015+
assert content =~ "[migrations](references/migrations.md)"
1016+
refute content =~ "[ash_postgres](references/ash_postgres.md)"
1017+
1018+
# ash_oban has no usage rules → no reference link at all
1019+
refute content =~ "references/ash_oban.md"
1020+
1021+
# Search docs include all matched deps
1022+
assert content =~ "-p ash"
1023+
assert content =~ "-p ash_postgres"
1024+
assert content =~ "-p ash_oban"
9761025
end
9771026

9781027
test "removes stale managed skills no longer in build list" do

0 commit comments

Comments
 (0)