Skip to content

Commit e41db41

Browse files
committed
refactor: unify header resolution for rule and instruction bundling
- Adds resolve_header_from_stem to consistently determine section headers. - Prefers canonical names, preserving acronyms from SECTION_GLOBS. - Improves maintainability of bundled output naming. Generated-by: aiautocommit
1 parent 29ddd58 commit e41db41

1 file changed

Lines changed: 62 additions & 24 deletions

File tree

implode.py

Lines changed: 62 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Bundle Cursor or Copilot instruction component files into a single instruction file.
44
Usage: python3 implode.py [cursor|github] [output_file]
55
"""
6+
67
import os
78
import sys
89
import argparse
@@ -12,57 +13,71 @@
1213
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
1314
from constants import SECTION_GLOBS, header_to_filename, filename_to_header
1415

16+
1517
def get_ordered_files(file_list, section_globs_keys):
1618
"""Order files based on SECTION_GLOBS key order, with unmapped files at the end."""
1719
file_dict = {f.stem: f for f in file_list}
1820
ordered_files = []
19-
21+
2022
# Add files in SECTION_GLOBS order
2123
for section_name in section_globs_keys:
2224
filename = header_to_filename(section_name)
2325
if filename in file_dict:
2426
ordered_files.append(file_dict[filename])
2527
del file_dict[filename]
26-
28+
2729
# Add any remaining files (not in SECTION_GLOBS) sorted alphabetically
2830
remaining_files = sorted(file_dict.values(), key=lambda p: p.name)
2931
ordered_files.extend(remaining_files)
30-
32+
3133
return ordered_files
3234

35+
3336
def get_ordered_files_github(file_list, section_globs_keys):
3437
"""Order GitHub instruction files based on SECTION_GLOBS key order, with unmapped files at the end.
3538
Handles .instructions suffix by stripping it for ordering purposes."""
3639
# Create dict mapping base filename (without .instructions) to the actual file
3740
file_dict = {}
3841
for f in file_list:
39-
base_stem = f.stem.replace('.instructions', '')
42+
base_stem = f.stem.replace(".instructions", "")
4043
file_dict[base_stem] = f
41-
44+
4245
ordered_files = []
43-
46+
4447
# Add files in SECTION_GLOBS order
4548
for section_name in section_globs_keys:
4649
filename = header_to_filename(section_name)
4750
if filename in file_dict:
4851
ordered_files.append(file_dict[filename])
4952
del file_dict[filename]
50-
53+
5154
# Add any remaining files (not in SECTION_GLOBS) sorted alphabetically
5255
remaining_files = sorted(file_dict.values(), key=lambda p: p.name)
5356
ordered_files.extend(remaining_files)
54-
57+
5558
return ordered_files
5659

60+
5761
def bundle_cursor_rules(rules_dir, output_file):
5862
rule_files = list(Path(rules_dir).glob("*.mdc"))
5963
general = [f for f in rule_files if f.stem == "general"]
6064
others = [f for f in rule_files if f.stem != "general"]
61-
65+
6266
# Order the non-general files based on SECTION_GLOBS
6367
ordered_others = get_ordered_files(others, SECTION_GLOBS.keys())
6468
ordered = general + ordered_others
65-
69+
70+
def resolve_header_from_stem(stem):
71+
"""Return the canonical header for a given filename stem.
72+
73+
Prefer exact header names from SECTION_GLOBS (preserves acronyms like FastAPI, TypeScript).
74+
Fallback to title-casing the filename when not found in SECTION_GLOBS.
75+
"""
76+
for section_name in SECTION_GLOBS.keys():
77+
if header_to_filename(section_name) == stem:
78+
return section_name
79+
return filename_to_header(stem)
80+
6681
with open(output_file, "w") as out:
6782
for rule_file in ordered:
6883
with open(rule_file, "r") as f:
@@ -71,41 +86,55 @@ def bundle_cursor_rules(rules_dir, output_file):
7186
continue
7287
content = strip_yaml_frontmatter(content)
7388
content = strip_header(content)
74-
# Convert dash-separated names to title case with spaces
75-
header = filename_to_header(rule_file.stem)
89+
# Use canonical header names from SECTION_GLOBS when available
90+
header = resolve_header_from_stem(rule_file.stem)
7691
if rule_file.stem != "general":
7792
out.write(f"## {header}\n\n")
7893
out.write(content)
7994
out.write("\n\n")
8095

96+
8197
def strip_yaml_frontmatter(text):
8298
lines = text.splitlines()
83-
if lines and lines[0].strip() == '---':
99+
if lines and lines[0].strip() == "---":
84100
# Find the next '---' after the first
85101
for i in range(1, len(lines)):
86-
if lines[i].strip() == '---':
87-
return '\n'.join(lines[i+1:]).lstrip('\n')
102+
if lines[i].strip() == "---":
103+
return "\n".join(lines[i + 1 :]).lstrip("\n")
88104
return text
89105

106+
90107
def strip_header(text):
91108
"""Remove the first markdown header (## Header) from text if present."""
92109
lines = text.splitlines()
93-
if lines and lines[0].startswith('## '):
110+
if lines and lines[0].startswith("## "):
94111
# Remove the header line and any immediately following empty lines
95112
remaining_lines = lines[1:]
96113
while remaining_lines and not remaining_lines[0].strip():
97114
remaining_lines = remaining_lines[1:]
98-
return '\n'.join(remaining_lines)
115+
return "\n".join(remaining_lines)
99116
return text
100117

118+
101119
def bundle_github_instructions(instructions_dir, output_file):
102120
copilot_general = Path(".github/copilot-instructions.md")
103121
instr_files = list(Path(instructions_dir).glob("*.instructions.md"))
104-
122+
105123
# Order the instruction files based on SECTION_GLOBS
106124
# We need to create a modified version that strips .instructions from stems for ordering
107125
ordered_files = get_ordered_files_github(instr_files, SECTION_GLOBS.keys())
108-
126+
127+
def resolve_header_from_stem(stem):
128+
"""Return the canonical header for a given filename stem.
129+
130+
Prefer exact header names from SECTION_GLOBS (preserves acronyms like FastAPI, TypeScript).
131+
Fallback to title-casing the filename when not found in SECTION_GLOBS.
132+
"""
133+
for section_name in SECTION_GLOBS.keys():
134+
if header_to_filename(section_name) == stem:
135+
return section_name
136+
return filename_to_header(stem)
137+
109138
with open(output_file, "w") as out:
110139
# Write general copilot instructions if present
111140
if copilot_general.exists():
@@ -120,16 +149,24 @@ def bundle_github_instructions(instructions_dir, output_file):
120149
continue
121150
content = strip_yaml_frontmatter(content)
122151
content = strip_header(content)
123-
# Convert dash-separated names to title case with spaces
124-
header = filename_to_header(instr_file.stem.replace('.instructions',''))
152+
# Use canonical header names from SECTION_GLOBS when available
153+
base_stem = instr_file.stem.replace(".instructions", "")
154+
header = resolve_header_from_stem(base_stem)
125155
out.write(f"## {header}\n\n")
126156
out.write(content)
127157
out.write("\n\n")
128158

159+
129160
def main():
130-
parser = argparse.ArgumentParser(description="Bundle Cursor or Copilot rules into a single file.")
131-
parser.add_argument("mode", choices=["cursor", "github"], help="Which rules to bundle")
132-
parser.add_argument("output", nargs="?", default="instructions.md", help="Output file")
161+
parser = argparse.ArgumentParser(
162+
description="Bundle Cursor or Copilot rules into a single file."
163+
)
164+
parser.add_argument(
165+
"mode", choices=["cursor", "github"], help="Which rules to bundle"
166+
)
167+
parser.add_argument(
168+
"output", nargs="?", default="instructions.md", help="Output file"
169+
)
133170
args = parser.parse_args()
134171

135172
if args.mode == "cursor":
@@ -138,5 +175,6 @@ def main():
138175
bundle_github_instructions(".github/instructions", args.output)
139176
print(f"Bundled {args.mode} rules into {args.output}")
140177

178+
141179
if __name__ == "__main__":
142180
main()

0 commit comments

Comments
 (0)