Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def assumed_system_requirements(
spec = Label("//bazel/rules/rules_score/trlc/config:score_requirements_model"),
lobster_config = Label("//bazel/rules/rules_score/lobster/config:assumed_system_requirement"),
ref_package = "",
package = "",
image_srcs = [],
**kwargs):
"""Define Assumed System Requirements following S-CORE process guidelines.
Expand All @@ -58,6 +59,11 @@ def assumed_system_requirements(
Override this when using a custom requirements model.
lobster_config: Optional Lobster extraction config label. Defaults to the
S-CORE assumed system requirement config.
package: Optional TRLC package name override, only used when srcs
contains .rst files (ignored for raw .trlc srcs). Defaults to a
name derived from the .rst file's stem; set this explicitly to
avoid collisions when multiple requirement targets are converted
from same-named .rst files (e.g. multiple "index.rst").
visibility: Bazel visibility specification for the generated targets.

Generated Targets:
Expand Down Expand Up @@ -87,6 +93,7 @@ def assumed_system_requirements(
lobster_config = lobster_config,
spec = spec,
ref_package = ref_package,
package = package,
image_srcs = image_srcs,
**kwargs
)
Expand Down
7 changes: 7 additions & 0 deletions bazel/rules/rules_score/private/component_requirements.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def component_requirements(
spec = Label("//bazel/rules/rules_score/trlc/config:score_requirements_model"),
lobster_config = Label("//bazel/rules/rules_score/lobster/config:component_requirement"),
ref_package = "",
package = "",
image_srcs = [],
**kwargs):
"""Define component requirements following S-CORE process guidelines.
Expand Down Expand Up @@ -62,6 +63,11 @@ def component_requirements(
include `derived_from` as a tracing target, including any AoU
entries within it (resolved only at the dependable_element level,
which has a "Received AoUs" level).
package: Optional TRLC package name override, only used when srcs
contains .rst files (ignored for raw .trlc srcs). Defaults to a
name derived from the .rst file's stem; set this explicitly to
avoid collisions when multiple requirement targets are converted
from same-named .rst files (e.g. multiple "index.rst").
visibility: Bazel visibility specification for the generated targets.

Generated Targets:
Expand All @@ -86,6 +92,7 @@ def component_requirements(
lobster_config = lobster_config,
spec = spec,
ref_package = ref_package,
package = package,
image_srcs = image_srcs,
**kwargs
)
Expand Down
7 changes: 7 additions & 0 deletions bazel/rules/rules_score/private/feature_requirements.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def feature_requirements(
spec = Label("//bazel/rules/rules_score/trlc/config:score_requirements_model"),
lobster_config = Label("//bazel/rules/rules_score/lobster/config:feature_requirement"),
ref_package = "",
package = "",
image_srcs = [],
**kwargs):
"""Define feature requirements following S-CORE process guidelines.
Expand All @@ -57,6 +58,11 @@ def feature_requirements(
(``@score_tooling//bazel/rules/rules_score/trlc/config:score_requirements_model``).
lobster_config: Optional Lobster extraction config label. Defaults to the
S-CORE feature requirement config.
package: Optional TRLC package name override, only used when srcs
contains .rst files (ignored for raw .trlc srcs). Defaults to a
name derived from the .rst file's stem; set this explicitly to
avoid collisions when multiple requirement targets are converted
from same-named .rst files (e.g. multiple "index.rst").
visibility: Bazel visibility specification for the generated targets.

Generated Targets:
Expand Down Expand Up @@ -86,6 +92,7 @@ def feature_requirements(
lobster_config = lobster_config,
spec = spec,
ref_package = ref_package,
package = package,
image_srcs = image_srcs,
**kwargs
)
Expand Down
25 changes: 25 additions & 0 deletions bazel/rules/rules_score/private/requirements.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ load("//bazel/rules/rules_score/private:rst_to_trlc.bzl", "rst_to_trlc")

_DEFAULT_SPEC = Label("//bazel/rules/rules_score/trlc/config:score_requirements_model")

# Restricts which RST directive names are converted per req_kind, so a
# shared .rst file (e.g. one that also carries aou_req directives consumed
# separately by assumptions_of_use()) doesn't leak unrelated directive types
# into a given requirements target. "assumed_system" additionally accepts
# stkh_req: Stakeholder Requirements have no TRLC representation of their
# own and are the intended real-world source for AssumedSystemReq records
# (see rst_to_trlc.py's DIRECTIVE_TO_TRLC mapping).
_REQ_KIND_TO_DIRECTIVES = {
"assumed_system": ["assumed_system_req", "stkh_req"],
"feature": ["feat_req"],
"component": ["comp_req"],
"aou": ["aou_req"],
}

# ============================================================================
# Private Rule Implementation
# ============================================================================
Expand Down Expand Up @@ -196,6 +210,7 @@ def score_requirements_rule(
deps = [],
spec = [],
ref_package = "",
package = "",
**kwargs):
"""Macro wrapper around _score_requirements_rule with RST support.

Expand All @@ -215,6 +230,13 @@ def score_requirements_rule(
ref_package: TRLC package prefix used for derived_from cross-references
when converting RST sources (e.g. "AssumedSystemRequirements" for
feature requirements that derive from ASR).
package: Optional TRLC package name override for the .trlc file(s)
generated from .rst srcs. Only applies to the RST conversion path;
.trlc sources are passed through unchanged and keep whatever
package name their own source declares. Defaults to a name derived
from the .rst file's stem (see rst_to_trlc.py), which can collide
when multiple requirement targets are converted from same-named
files (e.g. multiple "index.rst").

Returns:
List of resolved labels corresponding to srcs (after any .rst-to-.trlc
Expand All @@ -223,6 +245,7 @@ def score_requirements_rule(
"""
extra_spec = spec if type(spec) == type([]) else [spec]
merged_spec = [_DEFAULT_SPEC] + [s for s in extra_spec if s != _DEFAULT_SPEC]
only_types = _REQ_KIND_TO_DIRECTIVES.get(req_kind, [])
trlc_srcs = []
extra_deps = []
resolved_srcs = []
Expand All @@ -233,6 +256,8 @@ def score_requirements_rule(
name = gen_name,
srcs = [src],
ref_package = ref_package,
package = package,
only_types = only_types,
)
trlc_srcs.append(":" + gen_name)
resolved_srcs.append(":" + gen_name)
Expand Down
28 changes: 26 additions & 2 deletions bazel/rules/rules_score/private/rst_to_trlc.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
load("@trlc//:trlc.bzl", "trlc_requirements")
load("//bazel/rules/rules_score/private:verbosity.bzl", "VERBOSITY_ATTR", "get_log_level")

def rst_srcs_to_trlc(name, srcs, deps = [], ref_package = ""):
def rst_srcs_to_trlc(name, srcs, deps = [], ref_package = "", only_types = []):
"""Convert any .rst entries in srcs to trlc_requirements targets.

For each .rst entry a pair of intermediate targets is generated:
Expand All @@ -34,6 +34,12 @@ def rst_srcs_to_trlc(name, srcs, deps = [], ref_package = ""):
generated .trlc files (e.g. parent requirement packages).
ref_package: TRLC package prefix for derived_from cross-references
written into the generated .trlc content.
only_types: Optional list of RST directive names (e.g. ["aou_req"])
to restrict conversion to. Useful when the same .rst file is
shared as srcs by more than one rule (e.g. it contains both
comp_req and aou_req directives, consumed separately by
component_requirements() and assumptions_of_use()). Defaults to
all directive types supported by rst_to_trlc.py.

Returns:
List of srcs where .rst entries are replaced by generated trlc labels.
Expand All @@ -47,6 +53,7 @@ def rst_srcs_to_trlc(name, srcs, deps = [], ref_package = ""):
name = gen_name,
srcs = [src],
ref_package = ref_package,
only_types = only_types,
)
trlc_requirements(
name = trlc_name,
Expand All @@ -63,7 +70,17 @@ def _rst_to_trlc_impl(ctx):
"""Convert each .rst source file to a .trlc file via the Python converter."""
outs = []
for src in ctx.files.srcs:
out = ctx.actions.declare_file(src.basename[:-4] + ".trlc", sibling = src)
# Declared under this rule's own name (not sibling = src): a single
# .rst file can be the srcs of more than one rst_to_trlc rule (e.g.
# the same file supplying both comp_req and aou_req directives to
# component_requirements() and assumptions_of_use() respectively).
# Since sibling = src ties the output to the *source's* directory,
# two such rules would declare the exact same output path and
# conflict. Namespacing by rule name also sidesteps the constraint
# that a sibling-declared file must live in the same Bazel package
# as the rule (the source file's package and the rule's package can
# now differ).
out = ctx.actions.declare_file("{}/{}.trlc".format(ctx.label.name, src.basename[:-4]))
outs.append(out)

args = ctx.actions.args()
Expand All @@ -76,6 +93,9 @@ def _rst_to_trlc_impl(ctx):
if ctx.attr.package:
args.add("--package")
args.add(ctx.attr.package)
if ctx.attr.only_types:
args.add("--only-types")
args.add(",".join(ctx.attr.only_types))
args.add("--log-level")
args.add(get_log_level(ctx))

Expand Down Expand Up @@ -114,6 +134,10 @@ rst_to_trlc = rule(
default = "",
doc = "Optional TRLC package name override; defaults to the input file stem.",
),
"only_types": attr.string_list(
default = [],
doc = "Optional allowlist of RST directive names to convert; defaults to all supported types.",
),
},
**VERBOSITY_ATTR
),
Expand Down
11 changes: 7 additions & 4 deletions bazel/rules/rules_score/src/fmea_assembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
_CM_TABLE_COLUMNS = {"safety": "ASIL", "description": "Description"}
# Overview summary table columns (one row per failure mode).
_FM_TABLE_COLUMNS = {
"guideword": "Guideword",
"guidewords": "Guideword",
"safety": "ASIL",
"interface": "Interface",
}
Expand Down Expand Up @@ -150,9 +150,12 @@ def _attr_grid(obj: object) -> _Directive | None:
interface/failure-effect as titled cards; a gutter separates the rows."""
fields = obj.to_python_dict()
items = []
guideword = fields.get("guideword")
if guideword:
items.append(_grid_item(_badge(_GUIDEWORD_BADGE, guideword), {"class": "sd-text-center"}))
guidewords = fields.get("guidewords")
if guidewords:
guideword_text = (
", ".join(str(g) for g in guidewords) if isinstance(guidewords, list) else str(guidewords)
)
items.append(_grid_item(_badge(_GUIDEWORD_BADGE, guideword_text), {"class": "sd-text-center"}))
safety = fields.get("safety")
if safety:
role = _ASIL_BADGE.get(safety, _DEFAULT_BADGE)
Expand Down
61 changes: 55 additions & 6 deletions bazel/rules/rules_score/src/rst_to_trlc.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
DIRECTIVE_TO_TRLC: dict[str, str] = {
# Assumed System Requirements (root of the S-CORE traceability chain)
"assumed_system_req": "ScoreReq.AssumedSystemReq",
# Stakeholder Requirements (stkh_req) have no TRLC representation of their
# own. When used as srcs for assumed_system_requirements(), they are the
# TRLC-level stand-in for the wider operational/system context, so they
# are converted 1:1 into AssumedSystemReq records (same TRLC type as
# above, just sourced from the existing stkh_req directive instead of a
# duplicated one).
"stkh_req": "ScoreReq.AssumedSystemReq",
# Feature Requirements
"feat_req": "ScoreReq.FeatReq",
# Component Requirements
Expand Down Expand Up @@ -83,6 +90,7 @@
_RE_MARKUP = re.compile(r"\*\*?(.*?)\*\*?")
_RE_DIRECTIVE = re.compile(r"^\.\.\s+([\w]+)::\s*(.*)")
_RE_FIELD = re.compile(r"^\s+:([\w]+):\s*(.*)") # noqa: E501
_RE_REF = re.compile(r"^(?P<id>[\w]+)(?:\[version==(?P<version>\d+)\])?$")

_TRLC_HEADER = """\
/********************************************************************************
Expand Down Expand Up @@ -140,14 +148,43 @@ def _collect_refs(fields: dict[str, str]) -> list[str]:
return [r.strip() for k in _REF_FIELDS if k in fields for r in fields[k].split(",") if r.strip()]


def parse_directives(content: str) -> list[dict[str, Any]]:
"""Parse supported requirement directives from RST content."""
def _split_ref(ref: str) -> tuple[str, str]:
"""Split a raw reference token into (id, version).

Accepts either a bare id (e.g. "foo") or an id with an explicit
``[version==N]`` qualifier (e.g. "foo[version==2]"), as written in
``:derived_from:``/``:satisfies:`` RST fields. The version defaults to
``_DEFAULT_VERSION`` ("1") when no qualifier is present. Falls back to
treating the whole token as the id (version 1) if it doesn't match the
expected pattern, so malformed input degrades gracefully instead of
crashing the conversion.
"""
m = _RE_REF.match(ref)
if not m:
return ref, _DEFAULT_VERSION
return m.group("id"), m.group("version") or _DEFAULT_VERSION


def parse_directives(
content: str, only_types: set[str] | None = None
) -> list[dict[str, Any]]:
"""Parse supported requirement directives from RST content.

Args:
content: Raw RST source text.
only_types: If given, restrict parsing to these directive names
(e.g. {"aou_req"}), even if other supported directive types are
also present in the file (e.g. a shared file also containing
comp_req directives, converted separately by another rule).
Defaults to all directives known to DIRECTIVE_TO_TRLC.
"""
allowed = only_types if only_types is not None else set(DIRECTIVE_TO_TRLC)
results: list[dict[str, Any]] = []
lines = content.splitlines()
i = 0
while i < len(lines):
m = _RE_DIRECTIVE.match(lines[i])
if not m or m.group(1) not in DIRECTIVE_TO_TRLC:
if not m or m.group(1) not in allowed:
i += 1
continue

Expand Down Expand Up @@ -187,7 +224,9 @@ def render_trlc(directives: list[dict[str, Any]], package: str, ref_package: str

refs = _collect_refs(fields)
if refs:
ref_list = ", ".join(f"{ref_package}.{r}@1" for r in refs)
ref_list = ", ".join(
"{}.{}@{}".format(ref_package, *_split_ref(r)) for r in refs
)
lines_out.append(f" derived_from = [{ref_list}]")

if trlc_type in _ASSUMED_SYSTEM_REQ_TYPES:
Expand All @@ -206,10 +245,11 @@ def convert(
*,
package: str | None = None,
ref_package: str | None = None,
only_types: set[str] | None = None,
) -> int:
"""Convert one RST file to TRLC. Returns number of records written."""
pkg = package or "".join(w.capitalize() for w in re.split(r"[_\-\s]+", input_path.stem))
directives = parse_directives(input_path.read_text(encoding="utf-8"))
directives = parse_directives(input_path.read_text(encoding="utf-8"), only_types=only_types)
if not directives:
logging.warning("no supported requirement directives found in %s", input_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
Expand All @@ -226,6 +266,12 @@ def convert(
p.add_argument("--output-dir", type=Path, required=True)
p.add_argument("--package", default=None)
p.add_argument("--ref-package", default=None)
p.add_argument(
"--only-types",
default=None,
help="Comma-separated list of directive names to convert, e.g. "
"'aou_req'. Defaults to all supported directive types.",
)
p.add_argument(
"--log-level",
choices=["error", "warn", "info", "debug"],
Expand All @@ -238,5 +284,8 @@ def convert(
if not args.input_file.exists():
sys.exit(f"ERROR: file not found: {args.input_file}")
output_file = args.output_dir / (args.input_file.stem + ".trlc")
record_count = convert(args.input_file, output_file, package=args.package, ref_package=args.ref_package)
only_types = {t.strip() for t in args.only_types.split(",") if t.strip()} if args.only_types else None
record_count = convert(
args.input_file, output_file, package=args.package, ref_package=args.ref_package, only_types=only_types
)
logging.info("%s -> %s (%d record(s))", args.input_file, output_file, record_count)
16 changes: 16 additions & 0 deletions bazel/rules/rules_score/test/rst_to_trlc_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,22 @@ def test_feat_req_imports_ref_package_when_refs_present(self):
out = render_trlc(items, "FeatPkg", "AsrPkg")
self.assertIn("import AsrPkg", out)

def test_feat_req_derived_from_honors_explicit_version(self):
items = self._single(
"feat_req", {"derived_from": "asr_req__test__001[version==2]"}
)
out = render_trlc(items, "FeatPkg", "AsrPkg")
self.assertIn("AsrPkg.asr_req__test__001@2", out)

def test_feat_req_derived_from_mixed_explicit_and_default_version(self):
items = self._single(
"feat_req",
{"derived_from": "asr_req__a[version==2], asr_req__b"},
)
out = render_trlc(items, "FeatPkg", "AsrPkg")
self.assertIn("AsrPkg.asr_req__a@2", out)
self.assertIn("AsrPkg.asr_req__b@1", out)

# --- CompReq ---

def test_comp_req_produces_comp_req_type(self):
Expand Down
Loading
Loading