1111Schichten:
1212 1. Scanner: Sammelt Rohdaten (z. B. aus gap_scanner_v2, analyze_code_maturity)
1313 2. Dispatcher: Entscheidet, welche Dateien/Module aktualisiert werden
14- 3. Updater: Schreibt Header/Reports (delegiert aktuell an Legacy-Logik)
14+ 3. Updater: Schreibt Header/Reports inkl. Header-Vollständigkeitsvalidierung
1515
16- Aktuell: Nur Klassengerüst, keine Verhaltensänderung. Die eigentliche Logik bleibt vorerst im Legacy-Skript .
16+ Aktuell: Enthält die kanonische Header-Generierung für Workflow und lokale Läufe .
1717"""
1818
1919import sys
@@ -83,9 +83,9 @@ class CodeMaturityScanner:
8383 'production' : (3 , 9 ), 'tests' : (2 , 20 ), 'docs' : (1 , 15 ), 'documented_stub' : (1 , 10 ),
8484 }
8585
86- def scan (self , repo_root : str = "." ):
87- repo_root = Path (repo_root )
88- files = list (self ._find_files (repo_root ))
86+ def scan (self , repo_root : str = "." , target_paths : Optional [ List [ str ]] = None ):
87+ repo_root = Path (repo_root ). resolve ()
88+ files = list (self ._find_files (repo_root , target_paths = target_paths ))
8989 _debug_log ('scanner' , f'Found { len (files )} candidate files under { repo_root } ' )
9090 results = []
9191 for index , file in enumerate (files , start = 1 ):
@@ -96,16 +96,41 @@ def scan(self, repo_root: str = "."):
9696 _debug_log ('scanner' , f'Analyzed { index } /{ len (files )} files' )
9797 return results
9898
99- def _find_files (self , repo_root : Path ):
100- # Ziel: src/, include/, tests/, benchmarks/ rekursiv durchsuchen
101- code_dirs = [repo_root / d for d in ("src" , "include" , "tests" , "benchmarks" ) if (repo_root / d ).exists ()]
102- for base_dir in code_dirs :
103- for root , dirs , files in os .walk (base_dir ):
99+ def _resolve_scan_roots (self , repo_root : Path , target_paths : Optional [List [str ]]) -> List [Path ]:
100+ if not target_paths :
101+ return [repo_root / d for d in ("src" , "include" , "tests" , "benchmarks" ) if (repo_root / d ).exists ()]
102+ roots : List [Path ] = []
103+ for raw in target_paths :
104+ candidate = Path (raw )
105+ resolved = candidate .resolve () if candidate .is_absolute () else (repo_root / candidate ).resolve ()
106+ if resolved .exists ():
107+ roots .append (resolved )
108+ else :
109+ _debug_log ('scanner' , f'Skip missing target path: { raw } ' )
110+ return roots
111+
112+ def _find_files (self , repo_root : Path , target_paths : Optional [List [str ]] = None ):
113+ roots = self ._resolve_scan_roots (repo_root , target_paths )
114+ seen : set = set ()
115+ for root_path in roots :
116+ if root_path .is_file ():
117+ ext = root_path .suffix .lower ()
118+ if ext in self .SUPPORTED_EXTENSIONS :
119+ file_key = root_path .as_posix ()
120+ if file_key not in seen :
121+ seen .add (file_key )
122+ yield root_path
123+ continue
124+ for root , dirs , files in os .walk (root_path ):
104125 dirs [:] = [d for d in dirs if d not in self .EXCLUDE_DIRS ]
105126 for file in files :
106127 ext = os .path .splitext (file )[1 ]
107128 if ext in self .SUPPORTED_EXTENSIONS :
108- yield Path (root ) / file
129+ candidate = Path (root ) / file
130+ file_key = candidate .as_posix ()
131+ if file_key not in seen :
132+ seen .add (file_key )
133+ yield candidate
109134
110135 def _analyze_file (self , file_path : Path ):
111136 try :
@@ -164,15 +189,26 @@ def _get_maturity_level(self, score: float) -> str:
164189# --- Dispatcher ---
165190class CodeMaturityDispatcher :
166191 """Entscheidet, welche Dateien/Module aktualisiert werden sollen (Policy-gesteuert)"""
167- def __init__ (self , scanner : CodeMaturityScanner , min_score : int = 80 ):
192+ def __init__ (
193+ self ,
194+ scanner : CodeMaturityScanner ,
195+ min_score : int = 80 ,
196+ include_all_files : bool = False ,
197+ target_paths : Optional [List [str ]] = None ,
198+ ):
168199 self .scanner = scanner
169200 self .min_score = min_score
201+ self .include_all_files = include_all_files
202+ self .target_paths = target_paths or []
170203
171204 def dispatch (self , repo_root : str = "." ):
172- _debug_log ('dispatcher' , f'Start dispatch with min_score={ self .min_score } ' )
173- scan_results = self .scanner .scan (repo_root )
174- # Policy: Nur Dateien mit Score < min_score (nicht production-ready)
175- to_update = [r for r in scan_results if r ['score' ] < self .min_score ]
205+ _debug_log ('dispatcher' , f'Start dispatch with min_score={ self .min_score } , include_all_files={ self .include_all_files } ' )
206+ scan_results = self .scanner .scan (repo_root , target_paths = self .target_paths )
207+ if self .include_all_files :
208+ to_update = scan_results
209+ else :
210+ # Policy: Nur Dateien mit Score < min_score (nicht production-ready)
211+ to_update = [r for r in scan_results if r ['score' ] < self .min_score ]
176212 _debug_log ('dispatcher' , f'Selected { len (to_update )} files for header updates out of { len (scan_results )} scanned files' )
177213 return to_update
178214
@@ -188,8 +224,14 @@ class CodeMaturityUpdater:
188224 * @version {version}
189225 * @note Maturity: {level}
190226 * @note Score: {score}/100
227+ * @note Module Context: {module_context}
228+ * @note Ownership Scope: {ownership_scope}
229+ * @note Primary Symbols: {primary_symbols}
191230 * @note Gap Summary: total={gaps}; TODO={todo}, Stub={stub}, Unimpl={unimpl}, Mock={mock}, Sim={sim}, Debt={debt}, C={ext_critical}, H={ext_high}, M={ext_medium}, L={ext_low}
231+ * @note Governance: {governance_context}
232+ * @note Release Context: {release_context}
192233 * @note Status: {status}{maturity_gate}
234+ * @note Generator: .github/scripts/code_maturity_header_writer.py
193235 * @note This block is auto-generated and will be overwritten.
194236 */"""
195237 )
@@ -204,9 +246,15 @@ class CodeMaturityUpdater:
204246 * @note Maturity: {level}
205247 * @note Score: {score}/100
206248 * @note Lines: {total_lines}
249+ * @note Module Context: {module_context}
250+ * @note Ownership Scope: {ownership_scope}
251+ * @note Primary Symbols: {primary_symbols}
207252 * @note Gap Summary: total={gaps}; TODO={todo}, Stub={stub}, Unimpl={unimpl}, Mock={mock}, Sim={sim}, Debt={debt}, C={ext_critical}, H={ext_high}, M={ext_medium}, L={ext_low}
208253 * @note PR History (last 5): {pr_info}
254+ * @note Governance: {governance_context}
255+ * @note Release Context: {release_context}
209256 * @note Status: {status}{maturity_gate}
257+ * @note Generator: .github/scripts/code_maturity_header_writer.py
210258 * @note This block is auto-generated and will be overwritten.
211259 */"""
212260 )
@@ -285,29 +333,56 @@ def _detect_maturity_gates(self, file_path: Path, repo_root: Path) -> str:
285333 _debug_log ('maturity_gate_detection' , f'Warning: { e } ' )
286334 return ''
287335
288- def update (self , repo_root : str = "." ):
289-
336+ def update (
337+ self ,
338+ repo_root : str = "." ,
339+ no_headers : bool = False ,
340+ report_path : Optional [str ] = None ,
341+ validate_headers : bool = True ,
342+ fail_on_validation : bool = False ,
343+ ):
290344 repo_root_path = Path (repo_root ).resolve ()
291345 _debug_log ('updater' , f'Load external gap details from { repo_root_path / "ai_working" } ' )
292346 self .external_gap_details = self ._load_external_gap_details (repo_root_path )
293347 _debug_log ('updater' , f'Loaded external gap details for { len (self .external_gap_details )} files' )
294348 to_update = self .dispatcher .dispatch (str (repo_root_path ))
295- _debug_log ('updater' , f'Start header updates for { len (to_update )} files' )
349+ _debug_log ('updater' , f'Start header updates for { len (to_update )} files (no_headers= { no_headers } ) ' )
296350 updated = []
297351 total = len (to_update )
298- for index , entry in enumerate (to_update , start = 1 ):
299- file_path = entry ['file' ]
300- score = entry ['score' ]
301- level = entry ['level' ]
302- try :
303- self ._write_header (repo_root_path , Path (file_path ), score , level )
304- updated .append (file_path )
305- if index == 1 or index % 100 == 0 or index == total :
306- _debug_log ('updater' , f'Updated { index } /{ total } : { Path (file_path ).name } ' )
307- except Exception as e :
308- print (f"[FAIL] Header-Update für { file_path } : { e } " )
352+ if not no_headers :
353+ for index , entry in enumerate (to_update , start = 1 ):
354+ file_path = entry ['file' ]
355+ score = entry ['score' ]
356+ level = entry ['level' ]
357+ try :
358+ self ._write_header (repo_root_path , Path (file_path ), score , level )
359+ updated .append (file_path )
360+ if index == 1 or index % 100 == 0 or index == total :
361+ _debug_log ('updater' , f'Updated { index } /{ total } : { Path (file_path ).name } ' )
362+ except Exception as e :
363+ print (f"[FAIL] Header-Update für { file_path } : { e } " )
364+ else :
365+ _debug_log ('updater' , 'Header write disabled via --no-headers' )
309366 print (f"[OK] { len (updated )} Header aktualisiert." )
310- return updated
367+ validation_summary = {'checked' : 0 , 'failed' : 0 , 'failures' : []}
368+ if validate_headers :
369+ validation_summary = self ._validate_headers (repo_root_path , to_update )
370+ print (f"[OK] Header validation checked={ validation_summary ['checked' ]} failed={ validation_summary ['failed' ]} " )
371+
372+ summary = {
373+ 'scanned_candidates' : total ,
374+ 'updated' : len (updated ),
375+ 'no_headers' : no_headers ,
376+ 'validation' : validation_summary ,
377+ 'header_mode' : self ._resolve_header_mode ('' ),
378+ }
379+ if report_path :
380+ self ._write_report (Path (report_path ), summary )
381+ if fail_on_validation and validation_summary ['failed' ] > 0 :
382+ summary ['exit_code' ] = 2
383+ else :
384+ summary ['exit_code' ] = 0
385+ return summary
311386
312387 def _write_header (self , repo_root : Path , file_path : Path , score : int , level : str ):
313388 # Lese Originalinhalt
@@ -340,6 +415,14 @@ def _write_header(self, repo_root: Path, file_path: Path, score: int, level: str
340415 'debt' : metrics ['debt' ],
341416 'status' : status ,
342417 }
418+ module_context , ownership_scope = self ._derive_module_context (repo_root , file_path )
419+ template_data .update ({
420+ 'module_context' : module_context ,
421+ 'ownership_scope' : ownership_scope ,
422+ 'primary_symbols' : self ._extract_primary_symbols (content_ohne_header ),
423+ 'governance_context' : 'BranchModel=develop-first; CanonicalBranches=develop,community,military' ,
424+ 'release_context' : 'GateModel=WaveA→B→C→D on develop' ,
425+ })
343426
344427 gap_corr = self ._build_gap_correlation (repo_root , file_path , metrics ['gaps' ])
345428 template_data .update (gap_corr )
@@ -366,6 +449,41 @@ def _write_header(self, repo_root: Path, file_path: Path, score: int, level: str
366449 with open (file_path , 'w' , encoding = 'utf-8' ) as f :
367450 f .write (new_content )
368451
452+ def _derive_module_context (self , repo_root : Path , file_path : Path ) -> Tuple [str , str ]:
453+ rel = file_path .relative_to (repo_root )
454+ parts = rel .parts
455+ if len (parts ) < 2 :
456+ return (parts [0 ] if parts else 'root' , 'global' )
457+ context = parts [0 ]
458+ module = parts [1 ]
459+ ownership = {
460+ 'src' : 'production-code' ,
461+ 'include' : 'public-api' ,
462+ 'tests' : 'test-suite' ,
463+ 'benchmarks' : 'benchmark-suite' ,
464+ }.get (context , 'global' )
465+ return (f'{ context } /{ module } ' , ownership )
466+
467+ def _extract_primary_symbols (self , content : str , max_symbols : int = 6 ) -> str :
468+ class_pattern = re .compile (r'^\s*(?:template\s*<[^>]+>\s*)?(?:class|struct|enum(?:\s+class)?)\s+([A-Za-z_]\w*)' , re .MULTILINE )
469+ function_pattern = re .compile (
470+ r'^\s*(?:inline\s+|static\s+|virtual\s+|constexpr\s+|friend\s+)*'
471+ r'(?:[\w:\<\>\,\s\*&~]+)\s+([A-Za-z_~]\w*)\s*\([^;{}]*\)\s*(?:const)?\s*(?:noexcept)?\s*(?:\{|$)' ,
472+ re .MULTILINE ,
473+ )
474+ blocked = {'if' , 'for' , 'while' , 'switch' , 'return' , 'catch' }
475+ symbols : List [str ] = []
476+ for pattern in (class_pattern , function_pattern ):
477+ for match in pattern .finditer (content ):
478+ name = match .group (1 )
479+ if not name or name in blocked :
480+ continue
481+ if name not in symbols :
482+ symbols .append (name )
483+ if len (symbols ) >= max_symbols :
484+ return ', ' .join (symbols )
485+ return ', ' .join (symbols ) if symbols else 'none-detected'
486+
369487 def _derive_score_and_level (self , content : str ) -> Tuple [int , str ]:
370488 content = strip_generated_header (content )
371489 scanner = self .dispatcher .scanner
@@ -608,9 +726,78 @@ def _resolve_header_mode(self, level: str) -> str:
608726 return 'extended'
609727 return self .header_mode
610728
729+ def _required_header_fragments (self , mode : str ) -> List [str ]:
730+ base = [
731+ '@file' ,
732+ '@brief' ,
733+ '@version' ,
734+ '@note Maturity:' ,
735+ '@note Score:' ,
736+ '@note Module Context:' ,
737+ '@note Ownership Scope:' ,
738+ '@note Primary Symbols:' ,
739+ '@note Gap Summary:' ,
740+ '@note Governance:' ,
741+ '@note Release Context:' ,
742+ '@note Status:' ,
743+ '@note Generator:' ,
744+ ]
745+ if mode == 'extended' :
746+ return ['@author' , '@date' , '@note Lines:' , '@note PR History (last 5):' ] + base
747+ return base
748+
749+ def _validate_headers (self , repo_root : Path , entries : List [Dict [str , Any ]]) -> Dict [str , Any ]:
750+ mode = self ._resolve_header_mode ('' )
751+ required = self ._required_header_fragments (mode )
752+ failures : List [Dict [str , Any ]] = []
753+ checked = 0
754+ for entry in entries :
755+ file_path = Path (entry ['file' ])
756+ try :
757+ content = file_path .read_text (encoding = 'utf-8' , errors = 'ignore' )
758+ except Exception :
759+ failures .append ({'file' : file_path .as_posix (), 'missing' : ['<read_failed>' ]})
760+ continue
761+ match = self ._RE_EXISTING_HEADER .match (content )
762+ checked += 1
763+ if not match :
764+ failures .append ({'file' : file_path .relative_to (repo_root ).as_posix (), 'missing' : ['<header_block_missing>' ]})
765+ continue
766+ header = match .group (0 )
767+ missing = [fragment for fragment in required if fragment not in header ]
768+ if missing :
769+ failures .append ({'file' : file_path .relative_to (repo_root ).as_posix (), 'missing' : missing })
770+ return {'checked' : checked , 'failed' : len (failures ), 'failures' : failures }
771+
772+ def _write_report (self , report_path : Path , summary : Dict [str , Any ]) -> None :
773+ report_path .parent .mkdir (parents = True , exist_ok = True )
774+ now = datetime .now (timezone .utc ).strftime ('%Y-%m-%d %H:%M:%S UTC' )
775+ validation = summary .get ('validation' , {})
776+ lines = [
777+ '# Code Maturity Header Report' ,
778+ '' ,
779+ f'- Generated: { now } ' ,
780+ f'- Header mode: { summary .get ("header_mode" , "unknown" )} ' ,
781+ f'- Scan candidates: { summary .get ("scanned_candidates" , 0 )} ' ,
782+ f'- Headers updated: { summary .get ("updated" , 0 )} ' ,
783+ f'- Header write mode: { "check-only" if summary .get ("no_headers" ) else "rewrite" } ' ,
784+ f'- Validation checked: { validation .get ("checked" , 0 )} ' ,
785+ f'- Validation failed: { validation .get ("failed" , 0 )} ' ,
786+ '' ,
787+ ]
788+ failures = validation .get ('failures' , [])
789+ if failures :
790+ lines .extend (['## Missing required header fragments' , '' ])
791+ for item in failures [:200 ]:
792+ missing = ', ' .join (item .get ('missing' , []))
793+ lines .append (f"- `{ item .get ('file' , '<unknown>' )} ` → { missing } " )
794+ else :
795+ lines .append ('All validated files contain required header fragments.' )
796+ report_path .write_text ('\n ' .join (lines ).rstrip () + '\n ' , encoding = 'utf-8' )
797+
611798
612799def main ():
613- parser = argparse .ArgumentParser (description = 'Write/update compact code maturity headers.' )
800+ parser = argparse .ArgumentParser (description = 'Write/update canonical code maturity Doxygen headers.' )
614801 parser .add_argument ('--root' , default = '.' , help = 'Repository root path' )
615802 parser .add_argument ('--min-score' , type = int , default = 80 , help = 'Only files with score < min-score are updated' )
616803 parser .add_argument (
@@ -619,13 +806,31 @@ def main():
619806 default = 'auto' ,
620807 help = 'Header detail level (auto=extended until production-ready, then lean)' ,
621808 )
809+ parser .add_argument ('--target-paths' , default = '' , help = 'Comma-separated list of paths to scan (files or directories)' )
810+ parser .add_argument ('--include-all-files' , action = 'store_true' , help = 'Update all scanned files regardless of score' )
811+ parser .add_argument ('--no-headers' , action = 'store_true' , help = 'Do not rewrite headers; run scan/validation/report only' )
812+ parser .add_argument ('--report-path' , default = '' , help = 'Optional markdown report output path' )
813+ parser .add_argument ('--no-validate-headers' , action = 'store_true' , help = 'Disable required-header validation' )
814+ parser .add_argument ('--fail-on-validation' , action = 'store_true' , help = 'Return non-zero exit code if validation fails' )
622815 args = parser .parse_args ()
623816
817+ target_paths = [p .strip () for p in args .target_paths .split (',' ) if p .strip ()]
624818 scanner = CodeMaturityScanner ()
625- dispatcher = CodeMaturityDispatcher (scanner , min_score = args .min_score )
819+ dispatcher = CodeMaturityDispatcher (
820+ scanner ,
821+ min_score = args .min_score ,
822+ include_all_files = args .include_all_files ,
823+ target_paths = target_paths ,
824+ )
626825 updater = CodeMaturityUpdater (dispatcher , header_mode = args .header_mode )
627- updater .update (args .root )
628- return 0
826+ summary = updater .update (
827+ args .root ,
828+ no_headers = args .no_headers ,
829+ report_path = args .report_path or None ,
830+ validate_headers = not args .no_validate_headers ,
831+ fail_on_validation = args .fail_on_validation ,
832+ )
833+ return int (summary .get ('exit_code' , 0 ))
629834
630835
631836if __name__ == '__main__' :
0 commit comments