@@ -14,7 +14,7 @@ class GuidelineComposer
1414{
1515 protected string $ userGuidelineDir = '.ai/guidelines ' ;
1616
17- /** @var Collection<string, string > */
17+ /** @var Collection<string, array > */
1818 protected Collection $ guidelines ;
1919
2020 protected GuidelineConfig $ config ;
@@ -42,18 +42,24 @@ public function compose(): string
4242 return self ::composeGuidelines ($ this ->guidelines ());
4343 }
4444
45+ public function customGuidelinePath (string $ path = '' ): string
46+ {
47+ return base_path ($ this ->userGuidelineDir .'/ ' .ltrim ($ path , '/ ' ));
48+ }
49+
4550 /**
4651 * Static method to compose guidelines from a collection.
4752 * Can be used without Laravel dependencies.
4853 *
49- * @param Collection<string, string> $guidelines
54+ * @param Collection<string, array{content: string, name: string, path: ?string, custom: bool} > $guidelines
5055 */
5156 public static function composeGuidelines (Collection $ guidelines ): string
5257 {
5358 return str_replace ("\n\n\n\n" , "\n\n" , trim ($ guidelines
54- ->filter (fn ($ content ) => ! empty (trim ($ content )))
55- ->map (fn ($ content , $ key ) => "\n=== {$ key } rules === \n\n" .trim ($ content ))
56- ->join ("\n\n" )));
59+ ->filter (fn ($ guideline ) => ! empty (trim ($ guideline ['content ' ])))
60+ ->map (fn ($ guideline , $ key ) => "\n=== {$ key } rules === \n\n" .trim ($ guideline ['content ' ]))
61+ ->join ("\n\n" ))
62+ );
5763 }
5864
5965 /**
@@ -65,7 +71,7 @@ public function used(): array
6571 }
6672
6773 /**
68- * @return Collection<string, string >
74+ * @return Collection<string, array >
6975 */
7076 public function guidelines (): Collection
7177 {
@@ -79,14 +85,13 @@ public function guidelines(): Collection
7985 /**
8086 * Key is the 'guideline key' and value is the rendered blade.
8187 *
82- * @return \Illuminate\Support\Collection<string, string >
88+ * @return \Illuminate\Support\Collection<string, array >
8389 */
8490 protected function find (): Collection
8591 {
8692 $ guidelines = collect ();
8793 $ guidelines ->put ('foundation ' , $ this ->guideline ('foundation ' ));
8894 $ guidelines ->put ('boost ' , $ this ->guideline ('boost/core ' ));
89-
9095 $ guidelines ->put ('php ' , $ this ->guideline ('php/core ' ));
9196
9297 // TODO: AI-48: Use composer target version, not PHP version. Production could be 8.1, but local is 8.4
@@ -119,49 +124,39 @@ protected function find(): Collection
119124 $ guidelineDir .'/core ' ,
120125 $ this ->guideline ($ guidelineDir .'/core ' )
121126 ); // Always add package core
122-
123- $ guidelines ->put (
124- $ guidelineDir .'/v ' .$ package ->majorVersion (),
125- $ this ->guidelinesDir ($ guidelineDir .'/ ' .$ package ->majorVersion ())
126- );
127+ $ packageGuidelines = $ this ->guidelinesDir ($ guidelineDir .'/ ' .$ package ->majorVersion ());
128+ foreach ($ packageGuidelines as $ guideline ) {
129+ $ suffix = $ guideline ['name ' ] == 'core ' ? '' : '/ ' .$ guideline ['name ' ];
130+ $ guidelines ->put (
131+ $ guidelineDir .'/v ' .$ package ->majorVersion ().$ suffix ,
132+ $ guideline
133+ );
134+ }
127135 }
128136
129137 if ($ this ->config ->enforceTests ) {
130138 $ guidelines ->put ('tests ' , $ this ->guideline ('enforce-tests ' ));
131139 }
132140
133- $ userGuidelines = $ this ->guidelineFilesInDir (base_path ($ this ->userGuidelineDir ));
141+ $ userGuidelines = $ this ->guidelinesDir ($ this ->customGuidelinePath ());
142+ $ pathsUsed = $ guidelines ->pluck ('path ' );
134143
135144 foreach ($ userGuidelines as $ guideline ) {
136- $ guidelineKey = '.ai/ ' .$ guideline ->getBasename ('.blade.php ' );
137- $ guidelines ->put ($ guidelineKey , $ this ->guideline ($ guideline ->getPathname ()));
145+ if ($ pathsUsed ->contains ($ guideline ['path ' ])) {
146+ continue ; // Don't include this twice if it's an override
147+ }
148+ $ guidelines ->put ('.ai/ ' .$ guideline ['name ' ], $ guideline );
138149 }
139150
140151 return $ guidelines
141- ->whereNotNull ()
142- ->where (fn (string $ guideline ) => ! empty (trim ($ guideline )));
152+ ->where (fn (array $ guideline ) => ! empty (trim ($ guideline ['content ' ])));
143153 }
144154
145155 /**
146- * @return Collection<string, \Symfony\Component\Finder\SplFileInfo>
156+ * @param string $dirPath
157+ * @return array<array{content: string, name: string, path: ?string, custom: bool}>
147158 */
148- protected function guidelineFilesInDir (string $ dirPath ): Collection
149- {
150- if (! is_dir ($ dirPath )) {
151- $ dirPath = str_replace ('/ ' , DIRECTORY_SEPARATOR , __DIR__ .'/../../.ai/ ' .$ dirPath );
152- }
153-
154- try {
155- return collect (iterator_to_array (Finder::create ()
156- ->files ()
157- ->in ($ dirPath )
158- ->name ('*.blade.php ' )));
159- } catch (DirectoryNotFoundException $ e ) {
160- return collect ();
161- }
162- }
163-
164- protected function guidelinesDir (string $ dirPath ): ?string
159+ protected function guidelinesDir (string $ dirPath ): array
165160 {
166161 if (! is_dir ($ dirPath )) {
167162 $ dirPath = str_replace ('/ ' , DIRECTORY_SEPARATOR , __DIR__ .'/../../.ai/ ' .$ dirPath );
@@ -173,27 +168,21 @@ protected function guidelinesDir(string $dirPath): ?string
173168 ->in ($ dirPath )
174169 ->name ('*.blade.php ' );
175170 } catch (DirectoryNotFoundException $ e ) {
176- return null ;
171+ return [] ;
177172 }
178173
179- $ guidelines = '' ;
180- foreach ($ finder as $ file ) {
181- $ guidelines .= $ this ->guideline ($ file ->getRealPath ()) ?? '' ;
182- $ guidelines .= PHP_EOL ;
183- }
184-
185- return $ guidelines ;
174+ return array_map (fn ($ file ) => $ this ->guideline ($ file ->getRealPath ()), iterator_to_array ($ finder ));
186175 }
187176
188- protected function guideline (string $ path ): ?string
177+ /**
178+ * @param string $path
179+ * @return array{content: string, name: string, path: ?string, custom: bool}
180+ */
181+ protected function guideline (string $ path ): array
189182 {
190- if (! file_exists ($ path )) {
191- $ path = preg_replace ('/\.blade\.php$/ ' , '' , $ path );
192- $ path = str_replace ('/ ' , DIRECTORY_SEPARATOR , __DIR__ .'/../../.ai/ ' .$ path .'.blade.php ' );
193- }
194-
195- if (! file_exists ($ path )) {
196- return null ;
183+ $ path = $ this ->guidelinePath ($ path );
184+ if (is_null ($ path )) {
185+ return ['content ' => '' , 'name ' => '' , 'path ' => null , 'custom ' => false ];
197186 }
198187
199188 $ content = file_get_contents ($ path );
@@ -214,7 +203,12 @@ protected function guideline(string $path): ?string
214203 $ rendered = str_replace (array_keys ($ this ->storedSnippets ), array_values ($ this ->storedSnippets ), $ rendered );
215204 $ this ->storedSnippets = []; // Clear for next use
216205
217- return trim ($ rendered );
206+ return [
207+ 'content ' => trim ($ rendered ),
208+ 'name ' => str_replace ('.blade.php ' , '' , basename ($ path )),
209+ 'path ' => $ path ,
210+ 'custom ' => str_contains ($ path , $ this ->customGuidelinePath ()),
211+ ];
218212 }
219213
220214 private array $ storedSnippets = [];
@@ -233,4 +227,44 @@ private function processBoostSnippets(string $content): string
233227 return $ placeholder ;
234228 }, $ content );
235229 }
230+
231+ protected function prependPackageGuidelinePath (string $ path ): string
232+ {
233+ $ path = preg_replace ('/\.blade\.php$/ ' , '' , $ path );
234+ $ path = str_replace ('/ ' , DIRECTORY_SEPARATOR , __DIR__ .'/../../.ai/ ' .$ path .'.blade.php ' );
235+
236+ return $ path ;
237+ }
238+
239+ protected function prependUserGuidelinePath (string $ path ): string
240+ {
241+ $ path = preg_replace ('/\.blade\.php$/ ' , '' , $ path );
242+ $ path = str_replace ('/ ' , DIRECTORY_SEPARATOR , $ this ->customGuidelinePath ($ path .'.blade.php ' ));
243+
244+ return $ path ;
245+ }
246+
247+ protected function guidelinePath (string $ path ): ?string
248+ {
249+ // Relative path, prepend our package path to it
250+ if (! file_exists ($ path )) {
251+ $ path = $ this ->prependPackageGuidelinePath ($ path );
252+ if (! file_exists ($ path )) {
253+ return null ;
254+ }
255+ }
256+
257+ $ path = realpath ($ path );
258+
259+ // If this is a custom guideline, return it unchanged
260+ if (str_contains ($ path , $ this ->customGuidelinePath ())) {
261+ return $ path ;
262+ }
263+
264+ // The path is not a custom guideline, check if the user has an override for this
265+ $ relativePath = ltrim (str_replace ([realpath (__DIR__ .'/../../ ' ), '.ai/ ' ], '' , $ path ), '/ ' );
266+ $ customPath = $ this ->prependUserGuidelinePath ($ relativePath );
267+
268+ return file_exists ($ customPath ) ? $ customPath : $ path ;
269+ }
236270}
0 commit comments