Skip to content

Commit a5019c3

Browse files
committed
score_metamodel: autodiscover requirement types for metrics
1 parent acc8ea9 commit a5019c3

3 files changed

Lines changed: 379 additions & 21 deletions

File tree

docs/conf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@
1818
extensions = [
1919
"score_sphinx_bundle",
2020
]
21+
22+
# Configure traceability metrics explicitly for this repository.
23+
score_metamodel_requirement_types = "tool_req"
24+
score_metamodel_include_external_needs = False

src/extensions/score_metamodel/__init__.py

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
150207
def _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

Comments
 (0)