@@ -111,29 +111,36 @@ def _write_metrics_json(app: Sphinx, exception: Exception | None) -> None:
111111 return
112112
113113 all_needs : list [Any ] = list (SphinxNeedsData (app .env ).get_needs_view ().values ())
114-
115- raw = str (getattr (app .config , "score_metamodel_requirement_types" , "tool_req" ))
116- requirement_types = {t .strip () for t in raw .split ("," ) if t .strip ()} or {"tool_req" }
117- include_not_implemented = True
118114 include_external : bool = bool (
119115 getattr (app .config , "score_metamodel_include_external_needs" , False )
120116 )
121117
118+ raw = str (getattr (app .config , "score_metamodel_requirement_types" , "" )).strip ()
119+ requirement_types = {t .strip () for t in raw .split ("," ) if t .strip ()}
120+ if not requirement_types :
121+ requirement_types = _discover_requirement_types (app , all_needs , include_external )
122+ include_not_implemented = True
123+
122124 metrics_by_type : dict [str , Any ] = {}
123- for req_type in sorted (requirement_types ):
124- type_summary = compute_traceability_summary (
125- all_needs = all_needs ,
126- requirement_types = {req_type },
127- include_not_implemented = include_not_implemented ,
128- filtered_test_types = set (),
129- include_external = include_external ,
125+ if not requirement_types :
126+ logger .info (
127+ "No requirement types configured or discovered; writing empty metrics.json."
130128 )
131- metrics_by_type [req_type ] = {
132- "include_not_implemented" : type_summary ["include_not_implemented" ],
133- "include_external" : type_summary ["include_external" ],
134- "requirements" : type_summary ["requirements" ],
135- "tests" : type_summary ["tests" ],
136- }
129+ else :
130+ for req_type in sorted (requirement_types ):
131+ type_summary = compute_traceability_summary (
132+ all_needs = all_needs ,
133+ requirement_types = {req_type },
134+ include_not_implemented = include_not_implemented ,
135+ filtered_test_types = set (),
136+ include_external = include_external ,
137+ )
138+ metrics_by_type [req_type ] = {
139+ "include_not_implemented" : type_summary ["include_not_implemented" ],
140+ "include_external" : type_summary ["include_external" ],
141+ "requirements" : type_summary ["requirements" ],
142+ "tests" : type_summary ["tests" ],
143+ }
137144
138145 output : dict [str , Any ] = {
139146 "schema_version" : "1" ,
@@ -147,6 +154,56 @@ def _write_metrics_json(app: Sphinx, exception: Exception | None) -> None:
147154 logger .info (f"Traceability metrics written to: { out_path } " )
148155
149156
157+ def _get_need_value (need : Any , key : str , default : Any = None ) -> Any :
158+ getter = getattr (need , "get" , None )
159+ if callable (getter ):
160+ return getter (key , default )
161+ try :
162+ return need [key ]
163+ except Exception :
164+ return default
165+
166+
167+ def _discover_requirement_types (
168+ app : Sphinx , all_needs : list [Any ], include_external : bool
169+ ) -> set [str ]:
170+ """Discover requirement directives that are both tagged and present."""
171+ tagged_requirements : set [str ] = set ()
172+ needs_types = getattr (app .config , "needs_types" , [])
173+ for need_type in needs_types or []:
174+ if not isinstance (need_type , dict ):
175+ continue
176+ directive = need_type .get ("directive" )
177+ tags = need_type .get ("tags" , [])
178+ if not isinstance (directive , str ):
179+ continue
180+ if not isinstance (tags , list ):
181+ continue
182+ normalized = {str (tag ).strip () for tag in tags }
183+ if "requirement_excl_process" in normalized or "requirement" in normalized :
184+ tagged_requirements .add (directive )
185+
186+ present_types : set [str ] = set ()
187+ for need in all_needs :
188+ is_external = bool (_get_need_value (need , "is_external" , False ))
189+ if not include_external and is_external :
190+ continue
191+ need_type : Any = _get_need_value (need , "type" , None )
192+ if isinstance (need_type , str ):
193+ present_types .add (need_type )
194+ discovered = tagged_requirements .intersection (present_types )
195+ if not discovered :
196+ # Fallback for repositories that use *_req directives but do not tag
197+ # requirement types in needs_types.
198+ discovered = {t for t in present_types if t .endswith ("_req" )}
199+ if discovered :
200+ logger .info (
201+ "score_metamodel_requirement_types is not configured; "
202+ f"using discovered requirement types: { ', ' .join (sorted (discovered ))} "
203+ )
204+ return discovered
205+
206+
150207def _run_checks (app : Sphinx , exception : Exception | None ) -> None :
151208 # Do not run checks if an exception occurred during build
152209 if exception :
@@ -334,11 +391,12 @@ def setup(app: Sphinx) -> dict[str, str | bool]:
334391
335392 app .add_config_value (
336393 "score_metamodel_requirement_types" ,
337- "tool_req " ,
394+ "" ,
338395 rebuild = "env" ,
339396 description = (
340397 "Comma-separated list of need types treated as requirements for "
341- "traceability metrics (default: tool_req)."
398+ "traceability metrics. If empty, requirement types are autodiscovered "
399+ "from needs_types tags (requirement, requirement_excl_process)."
342400 ),
343401 )
344402
0 commit comments