From 489912c682cddaefc9b757091147ca96b4370a05 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Wed, 26 Aug 2026 12:13:42 +0000 Subject: [PATCH 1/5] requirements: expose package override for RST-derived TRLC conversion component_requirements/feature_requirements/assumed_system_requirements derive the TRLC package name from the .rst source file's basename stem when no explicit override is given (see rst_to_trlc.py). Multiple requirement targets converted from same-named .rst files (e.g. 'index.rst' in different packages) therefore collide on the same default package name, which can lead to invalid self-imports when one target's ref_package points at another target that resolves to the same name. Thread a new 'package' parameter through score_requirements_rule and the three public macros down to the existing (but previously unexposed) rst_to_trlc 'package' attribute, so callers can pick an explicit, collision-free package name. Only affects the .rst conversion path; raw .trlc srcs are unaffected and keep declaring their package name in the source file as before. --- .../rules_score/private/assumed_system_requirements.bzl | 7 +++++++ .../rules/rules_score/private/component_requirements.bzl | 7 +++++++ bazel/rules/rules_score/private/feature_requirements.bzl | 7 +++++++ bazel/rules/rules_score/private/requirements.bzl | 9 +++++++++ 4 files changed, 30 insertions(+) diff --git a/bazel/rules/rules_score/private/assumed_system_requirements.bzl b/bazel/rules/rules_score/private/assumed_system_requirements.bzl index ec044f9c..13fc5238 100644 --- a/bazel/rules/rules_score/private/assumed_system_requirements.bzl +++ b/bazel/rules/rules_score/private/assumed_system_requirements.bzl @@ -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. @@ -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: @@ -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 ) diff --git a/bazel/rules/rules_score/private/component_requirements.bzl b/bazel/rules/rules_score/private/component_requirements.bzl index fd138eff..f163e90d 100644 --- a/bazel/rules/rules_score/private/component_requirements.bzl +++ b/bazel/rules/rules_score/private/component_requirements.bzl @@ -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. @@ -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: @@ -86,6 +92,7 @@ def component_requirements( lobster_config = lobster_config, spec = spec, ref_package = ref_package, + package = package, image_srcs = image_srcs, **kwargs ) diff --git a/bazel/rules/rules_score/private/feature_requirements.bzl b/bazel/rules/rules_score/private/feature_requirements.bzl index 15adafbf..6800bdec 100644 --- a/bazel/rules/rules_score/private/feature_requirements.bzl +++ b/bazel/rules/rules_score/private/feature_requirements.bzl @@ -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. @@ -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: @@ -86,6 +92,7 @@ def feature_requirements( lobster_config = lobster_config, spec = spec, ref_package = ref_package, + package = package, image_srcs = image_srcs, **kwargs ) diff --git a/bazel/rules/rules_score/private/requirements.bzl b/bazel/rules/rules_score/private/requirements.bzl index b7a96171..6a4111c4 100644 --- a/bazel/rules/rules_score/private/requirements.bzl +++ b/bazel/rules/rules_score/private/requirements.bzl @@ -196,6 +196,7 @@ def score_requirements_rule( deps = [], spec = [], ref_package = "", + package = "", **kwargs): """Macro wrapper around _score_requirements_rule with RST support. @@ -215,6 +216,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 @@ -233,6 +241,7 @@ def score_requirements_rule( name = gen_name, srcs = [src], ref_package = ref_package, + package = package, ) trlc_srcs.append(":" + gen_name) resolved_srcs.append(":" + gen_name) From 0e971d190c2a344370fad4d2d2aa63e57499956a Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Wed, 26 Aug 2026 12:36:25 +0000 Subject: [PATCH 2/5] rst_to_trlc: honor explicit [version==N] qualifier in cross-references derived_from/satisfies fields previously had the record's [version==N] qualifier (if written by the RST author) silently discarded: render_trlc always hardcoded '@1' regardless of what version was actually requested, producing wrong (or, when combined with the literal bracket text, plain invalid) TRLC syntax such as 'Pkg.name[version==2]@1'. Add _split_ref() to parse the optional qualifier out of each reference token and use the parsed version number (defaulting to 1, matching prior behavior for bare ids) when building the derived_from list. --- bazel/rules/rules_score/src/rst_to_trlc.py | 22 ++++++++++++++++++- .../rules_score/test/rst_to_trlc_test.py | 16 ++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/bazel/rules/rules_score/src/rst_to_trlc.py b/bazel/rules/rules_score/src/rst_to_trlc.py index 59e429d3..05d3d4d2 100644 --- a/bazel/rules/rules_score/src/rst_to_trlc.py +++ b/bazel/rules/rules_score/src/rst_to_trlc.py @@ -83,6 +83,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[\w]+)(?:\[version==(?P\d+)\])?$") _TRLC_HEADER = """\ /******************************************************************************** @@ -140,6 +141,23 @@ 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 _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) -> list[dict[str, Any]]: """Parse supported requirement directives from RST content.""" results: list[dict[str, Any]] = [] @@ -187,7 +205,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: diff --git a/bazel/rules/rules_score/test/rst_to_trlc_test.py b/bazel/rules/rules_score/test/rst_to_trlc_test.py index 2b6da09d..3f07fed7 100644 --- a/bazel/rules/rules_score/test/rst_to_trlc_test.py +++ b/bazel/rules/rules_score/test/rst_to_trlc_test.py @@ -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): From 103b6f78dda3c624d86c75187ccbb4a942dd6279 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Wed, 26 Aug 2026 15:52:22 +0000 Subject: [PATCH 3/5] rst_to_trlc: add only_types filter for multi-directive-type RST sources Lets a single RST source file that contains multiple directive types (e.g. both comp_req and aou_req in one requirements/index.rst) be converted differently by different rule invocations, each picking up only its relevant subset. - rst_to_trlc.py: parse_directives()/convert() gain an optional only_types set; CLI gains --only-types (comma-separated). - rst_to_trlc.bzl: rst_to_trlc rule gains an only_types string_list attr, forwarded to the Python CLI; rst_srcs_to_trlc() gains a matching only_types param. Also: declare_file output is now namespaced under ctx.label.name instead of sibling=src, fixing an output-path collision when two rules (e.g. component_requirements and assumptions_of_use) both convert the same source RST file. - requirements.bzl: score_requirements_rule() maps req_kind to the relevant directive set (_REQ_KIND_TO_DIRECTIVES) and passes it as only_types. - assumptions_of_use.bzl: assumptions_of_use() passes only_types=["aou_req"] so it only ever picks up AoU directives, even when srcs is shared with a component_requirements() target. --- .../rules_score/private/requirements.bzl | 16 ++++++++ .../rules/rules_score/private/rst_to_trlc.bzl | 28 ++++++++++++- bazel/rules/rules_score/src/rst_to_trlc.py | 39 ++++++++++++++++--- 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/bazel/rules/rules_score/private/requirements.bzl b/bazel/rules/rules_score/private/requirements.bzl index 6a4111c4..1c4ffa8a 100644 --- a/bazel/rules/rules_score/private/requirements.bzl +++ b/bazel/rules/rules_score/private/requirements.bzl @@ -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 # ============================================================================ @@ -231,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 = [] @@ -242,6 +257,7 @@ def score_requirements_rule( srcs = [src], ref_package = ref_package, package = package, + only_types = only_types, ) trlc_srcs.append(":" + gen_name) resolved_srcs.append(":" + gen_name) diff --git a/bazel/rules/rules_score/private/rst_to_trlc.bzl b/bazel/rules/rules_score/private/rst_to_trlc.bzl index 196b8c3f..a3c01c36 100644 --- a/bazel/rules/rules_score/private/rst_to_trlc.bzl +++ b/bazel/rules/rules_score/private/rst_to_trlc.bzl @@ -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: @@ -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. @@ -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, @@ -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() @@ -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)) @@ -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 ), diff --git a/bazel/rules/rules_score/src/rst_to_trlc.py b/bazel/rules/rules_score/src/rst_to_trlc.py index 05d3d4d2..55ab2584 100644 --- a/bazel/rules/rules_score/src/rst_to_trlc.py +++ b/bazel/rules/rules_score/src/rst_to_trlc.py @@ -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 @@ -158,14 +165,26 @@ def _split_ref(ref: str) -> tuple[str, str]: return m.group("id"), m.group("version") or _DEFAULT_VERSION -def parse_directives(content: str) -> list[dict[str, Any]]: - """Parse supported requirement directives from RST content.""" +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 @@ -226,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) @@ -246,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"], @@ -258,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) From 9a90e17c936ca9bd11978a50f2a2654c4d13edde Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Thu, 27 Aug 2026 14:06:23 +0000 Subject: [PATCH 4/5] fmea_assembler: local workaround for missing TRLCRST helper API LOCAL WORKAROUND: the pinned trlc v3.0.0 module TRLCRST class does not implement objects_by_fqn()/field_value_for()/render_table_to_string() that fmea_assembler.py expects. Add a local reimplementation driven by the already-parsed Symbol_Table so the fmea() pipeline can be exercised end-to-end. Remove once upstream trlc_rst gains this API. --- bazel/rules/rules_score/src/fmea_assembler.py | 75 ++++++++++++++++++- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/bazel/rules/rules_score/src/fmea_assembler.py b/bazel/rules/rules_score/src/fmea_assembler.py index 655b2a58..51dd4fc1 100644 --- a/bazel/rules/rules_score/src/fmea_assembler.py +++ b/bazel/rules/rules_score/src/fmea_assembler.py @@ -34,6 +34,70 @@ logger = logging.getLogger(__name__) +# --------------------------------------------------------------------------- +# LOCAL, UNCOMMITTED WORKAROUND -- DO NOT SUBMIT +# +# trlc (Bazel module, pinned at v3.0.0 in MODULE.bazel) ships a TRLCRST class +# that only implements parse_trlc_files()/convert_symbols_to_tree()/ +# render_to_file(). The functions below reimplement the +# objects_by_fqn()/field_value_for()/render_table_to_string() API that the +# rest of this file expects, driven directly by the already-parsed TRLC +# Symbol_Table (renderer._symbols), so the fmea() pipeline can be exercised +# end-to-end locally. Remove once the real trlc_rst gains this API (or once +# this module is rewritten against the shipped one). +# --------------------------------------------------------------------------- + + +def _objects_by_fqn(self): + return { + obj.fully_qualified_name(): obj for obj in self._symbols.iter_record_objects() + } + + +def _field_value_for(self, fqn, field_name): + obj = self.objects_by_fqn().get(fqn) + if obj is None: + return None + field = obj.field.get(field_name) + value = field.to_python_object() if field is not None else None + if isinstance(value, str) and hasattr(field, "has_references") and field.has_references: + return self._resolve_markup_references(value, field.references) + if isinstance(value, list): + return ", ".join(str(v) for v in value) + return value + + +def _render_table_to_string(self, columns, fqns, name_header="Name", link_fn=None): + obj_map = self.objects_by_fqn() + headers = [name_header] + list(columns.values()) + + def _cell(value): + if isinstance(value, list): + return ", ".join(str(v) for v in value) + return "" if value is None else str(value) + + def _row_lines(cells): + lines = [f" * - {cells[0]}"] + lines += [f" - {cell}" for cell in cells[1:]] + return lines + + lines = [".. list-table::", " :header-rows: 1", ""] + lines += _row_lines(headers) + for fqn in fqns: + obj = obj_map.get(fqn) + if obj is None: + continue + fields = obj.to_python_dict() + name_cell = link_fn(fqn, obj.name) if link_fn else obj.name + row = [name_cell] + [_cell(fields.get(name)) for name in columns] + lines += _row_lines(row) + return "\n".join(lines) + "\n" + + +TRLCRST.objects_by_fqn = _objects_by_fqn +TRLCRST.field_value_for = _field_value_for +TRLCRST.render_table_to_string = _render_table_to_string + _LEVEL_MAP = { "error": logging.ERROR, "warn": logging.WARNING, @@ -45,7 +109,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", } @@ -150,9 +214,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) From a429b1cbfdefe8a2b6d54284640eaeefab0b27b5 Mon Sep 17 00:00:00 2001 From: Anton Krivoborodov Date: Fri, 28 Aug 2026 09:56:10 +0000 Subject: [PATCH 5/5] fmea_assembler: remove obsolete TRLCRST monkeypatch main bumped the pinned trlc module to v3.0.1, which now ships objects_by_fqn()/field_value_for()/render_table_to_string() natively on TRLCRST. The local workaround added in 9a90e17 is therefore no longer needed. Also fixes test_fmea_assembler.py fixtures that still used the old singular 'guideword' field instead of the plural 'guidewords' list. --- bazel/rules/rules_score/src/fmea_assembler.py | 64 ------------------- .../rules_score/test/test_fmea_assembler.py | 8 +-- 2 files changed, 4 insertions(+), 68 deletions(-) diff --git a/bazel/rules/rules_score/src/fmea_assembler.py b/bazel/rules/rules_score/src/fmea_assembler.py index 51dd4fc1..78f39173 100644 --- a/bazel/rules/rules_score/src/fmea_assembler.py +++ b/bazel/rules/rules_score/src/fmea_assembler.py @@ -34,70 +34,6 @@ logger = logging.getLogger(__name__) -# --------------------------------------------------------------------------- -# LOCAL, UNCOMMITTED WORKAROUND -- DO NOT SUBMIT -# -# trlc (Bazel module, pinned at v3.0.0 in MODULE.bazel) ships a TRLCRST class -# that only implements parse_trlc_files()/convert_symbols_to_tree()/ -# render_to_file(). The functions below reimplement the -# objects_by_fqn()/field_value_for()/render_table_to_string() API that the -# rest of this file expects, driven directly by the already-parsed TRLC -# Symbol_Table (renderer._symbols), so the fmea() pipeline can be exercised -# end-to-end locally. Remove once the real trlc_rst gains this API (or once -# this module is rewritten against the shipped one). -# --------------------------------------------------------------------------- - - -def _objects_by_fqn(self): - return { - obj.fully_qualified_name(): obj for obj in self._symbols.iter_record_objects() - } - - -def _field_value_for(self, fqn, field_name): - obj = self.objects_by_fqn().get(fqn) - if obj is None: - return None - field = obj.field.get(field_name) - value = field.to_python_object() if field is not None else None - if isinstance(value, str) and hasattr(field, "has_references") and field.has_references: - return self._resolve_markup_references(value, field.references) - if isinstance(value, list): - return ", ".join(str(v) for v in value) - return value - - -def _render_table_to_string(self, columns, fqns, name_header="Name", link_fn=None): - obj_map = self.objects_by_fqn() - headers = [name_header] + list(columns.values()) - - def _cell(value): - if isinstance(value, list): - return ", ".join(str(v) for v in value) - return "" if value is None else str(value) - - def _row_lines(cells): - lines = [f" * - {cells[0]}"] - lines += [f" - {cell}" for cell in cells[1:]] - return lines - - lines = [".. list-table::", " :header-rows: 1", ""] - lines += _row_lines(headers) - for fqn in fqns: - obj = obj_map.get(fqn) - if obj is None: - continue - fields = obj.to_python_dict() - name_cell = link_fn(fqn, obj.name) if link_fn else obj.name - row = [name_cell] + [_cell(fields.get(name)) for name in columns] - lines += _row_lines(row) - return "\n".join(lines) + "\n" - - -TRLCRST.objects_by_fqn = _objects_by_fqn -TRLCRST.field_value_for = _field_value_for -TRLCRST.render_table_to_string = _render_table_to_string - _LEVEL_MAP = { "error": logging.ERROR, "warn": logging.WARNING, diff --git a/bazel/rules/rules_score/test/test_fmea_assembler.py b/bazel/rules/rules_score/test/test_fmea_assembler.py index db98ace4..8cb2636a 100644 --- a/bazel/rules/rules_score/test/test_fmea_assembler.py +++ b/bazel/rules/rules_score/test/test_fmea_assembler.py @@ -67,14 +67,14 @@ def _objs(): "FM_A", "FailureMode", { - "guideword": "LossOfFunction", + "guidewords": ["LossOfFunction"], "safety": "B", "interface": "Lib.Api", "failureeffect": "world ends", "description": "fm a description", }, ), - "Lib.FM_Orphan": _Obj("FM_Orphan", "FailureMode", {"safety": "QM", "guideword": "TooLate"}), + "Lib.FM_Orphan": _Obj("FM_Orphan", "FailureMode", {"safety": "QM", "guidewords": ["TooLate"]}), "Lib.CM_1": _Obj("CM_1", "ControlMeasure", {"safety": "B", "description": "cm one"}), "Lib.CM_Orphan": _Obj("CM_Orphan", "ControlMeasure", {"safety": "D"}), } @@ -200,7 +200,7 @@ def test_chain_missing_puml_raises_valueerror(self): package TestFmea type FailureMode { - guideword optional String + guidewords String [0 .. *] safety optional String interface optional String failureeffect optional String @@ -217,7 +217,7 @@ def test_chain_missing_puml_raises_valueerror(self): package TestFmea FailureMode FmA { - guideword = "TooLate" + guidewords = ["TooLate"] safety = "ASIL_D" interface = "Lib.Api" failureeffect = "downstream timeout"