@@ -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 /**
@@ -86,7 +92,6 @@ protected function find(): Collection
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,26 @@ 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 = '' ;
174+ $ guidelines = [] ;
180175 foreach ($ finder as $ file ) {
181- $ guidelines .= $ this ->guideline ($ file ->getRealPath ()) ?? '' ;
182- $ guidelines .= PHP_EOL ;
176+ $ guidelines [] = $ this ->guideline ($ file ->getRealPath ());
183177 }
184178
185179 return $ guidelines ;
186180 }
187181
188- protected function guideline (string $ path ): ?string
182+ /**
183+ * @param string $path
184+ * @return array{content: string, name: string, path: ?string, custom: bool}
185+ */
186+ protected function guideline (string $ path ): array
189187 {
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 ;
188+ $ path = $ this ->guidelinePath ($ path );
189+ if (is_null ($ path )) {
190+ return ['content ' => '' , 'name ' => '' , 'path ' => null , 'custom ' => false ];
197191 }
198192
199193 $ content = file_get_contents ($ path );
@@ -214,7 +208,12 @@ protected function guideline(string $path): ?string
214208 $ rendered = str_replace (array_keys ($ this ->storedSnippets ), array_values ($ this ->storedSnippets ), $ rendered );
215209 $ this ->storedSnippets = []; // Clear for next use
216210
217- return trim ($ rendered );
211+ return [
212+ 'content ' => trim ($ rendered ),
213+ 'name ' => str_replace ('.blade.php ' , '' , basename ($ path )),
214+ 'path ' => $ path ,
215+ 'custom ' => str_contains ($ path , $ this ->customGuidelinePath ()),
216+ ];
218217 }
219218
220219 private array $ storedSnippets = [];
@@ -233,4 +232,44 @@ private function processBoostSnippets(string $content): string
233232 return $ placeholder ;
234233 }, $ content );
235234 }
235+
236+ protected function prependPackageGuidelinePath (string $ path ): string
237+ {
238+ $ path = preg_replace ('/\.blade\.php$/ ' , '' , $ path );
239+ $ path = str_replace ('/ ' , DIRECTORY_SEPARATOR , __DIR__ .'/../../.ai/ ' .$ path .'.blade.php ' );
240+
241+ return $ path ;
242+ }
243+
244+ protected function prependUserGuidelinePath (string $ path ): string
245+ {
246+ $ path = preg_replace ('/\.blade\.php$/ ' , '' , $ path );
247+ $ path = str_replace ('/ ' , DIRECTORY_SEPARATOR , $ this ->customGuidelinePath ($ path .'.blade.php ' ));
248+
249+ return $ path ;
250+ }
251+
252+ protected function guidelinePath (string $ path ): ?string
253+ {
254+ // Relative path, prepend our package path to it
255+ if (! file_exists ($ path )) {
256+ $ path = $ this ->prependPackageGuidelinePath ($ path );
257+ if (! file_exists ($ path )) {
258+ return null ;
259+ }
260+ }
261+
262+ $ path = realpath ($ path );
263+
264+ // If this is a custom guideline, return it unchanged
265+ if (str_contains ($ path , $ this ->customGuidelinePath ())) {
266+ return $ path ;
267+ }
268+
269+ // The path is not a custom guideline, check if the user has an override for this
270+ $ relativePath = ltrim (str_replace ([realpath (__DIR__ .'/../../ ' ), '.ai/ ' ], '' , $ path ), '/ ' );
271+ $ customPath = $ this ->prependUserGuidelinePath ($ relativePath );
272+
273+ return file_exists ($ customPath ) ? $ customPath : $ path ;
274+ }
236275}
0 commit comments