From a051825c9a407dbaa6fc7f02992ebecaffd21d42 Mon Sep 17 00:00:00 2001 From: rmaddikery Date: Thu, 10 Sep 2026 15:15:37 +0200 Subject: [PATCH 1/4] plumb package attr to all public macros --- .../rules_score/private/assumed_system_requirements.bzl | 2 ++ bazel/rules/rules_score/private/assumptions_of_use.bzl | 2 ++ bazel/rules/rules_score/private/component_requirements.bzl | 2 ++ bazel/rules/rules_score/private/feature_requirements.bzl | 2 ++ bazel/rules/rules_score/private/requirements.bzl | 7 +++++++ 5 files changed, 15 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..2ebcf378 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. @@ -87,6 +88,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/assumptions_of_use.bzl b/bazel/rules/rules_score/private/assumptions_of_use.bzl index 4eacad4b..4435e0cc 100644 --- a/bazel/rules/rules_score/private/assumptions_of_use.bzl +++ b/bazel/rules/rules_score/private/assumptions_of_use.bzl @@ -40,6 +40,7 @@ def assumptions_of_use( srcs, deps = [], ref_package = None, + package = "", lobster_config = Label("//bazel/rules/rules_score/lobster/config:aou_config"), **kwargs): """Define Assumptions of Use following S-CORE process guidelines. @@ -92,6 +93,7 @@ def assumptions_of_use( req_kind = "aou", lobster_config = lobster_config, ref_package = ref_package or "", + package = package, **kwargs ) trlc_requirements_test( diff --git a/bazel/rules/rules_score/private/component_requirements.bzl b/bazel/rules/rules_score/private/component_requirements.bzl index fd138eff..d5b27cf8 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. @@ -86,6 +87,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..abe09d3f 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. @@ -86,6 +87,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..a4427e3f 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,11 @@ 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: TRLC package name to use for any generated .trlc file(s) when + converting RST sources. Defaults to the input file's stem (e.g. + "index.rst" -> "Index") if left empty, which is rarely a + meaningful package name -- callers converting RST sources should + normally set this explicitly. Returns: List of resolved labels corresponding to srcs (after any .rst-to-.trlc @@ -233,6 +239,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 ff719bd4991d437248dbd603a3a9c036903b1028 Mon Sep 17 00:00:00 2001 From: rmaddikery Date: Thu, 10 Sep 2026 15:16:31 +0200 Subject: [PATCH 2/4] Fix declare_file(sibling=src) for cross repo .rst labels --- bazel/rules/rules_score/private/requirements.bzl | 4 +--- bazel/rules/rules_score/private/rst_to_trlc.bzl | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bazel/rules/rules_score/private/requirements.bzl b/bazel/rules/rules_score/private/requirements.bzl index a4427e3f..3596487b 100644 --- a/bazel/rules/rules_score/private/requirements.bzl +++ b/bazel/rules/rules_score/private/requirements.bzl @@ -218,9 +218,7 @@ def score_requirements_rule( feature requirements that derive from ASR). package: TRLC package name to use for any generated .trlc file(s) when converting RST sources. Defaults to the input file's stem (e.g. - "index.rst" -> "Index") if left empty, which is rarely a - meaningful package name -- callers converting RST sources should - normally set this explicitly. + "index.rst" -> "Index") if left empty. Returns: List of resolved labels corresponding to srcs (after any .rst-to-.trlc diff --git a/bazel/rules/rules_score/private/rst_to_trlc.bzl b/bazel/rules/rules_score/private/rst_to_trlc.bzl index 196b8c3f..19eecaf1 100644 --- a/bazel/rules/rules_score/private/rst_to_trlc.bzl +++ b/bazel/rules/rules_score/private/rst_to_trlc.bzl @@ -63,7 +63,10 @@ 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 in this rule's own package rather than next to `src` (via + # `sibling`), since `src` may come from a different repository (e.g. + # a cross-repo RST label), where declare_file(sibling=...) is invalid. + out = ctx.actions.declare_file(src.basename[:-4] + ".trlc") outs.append(out) args = ctx.actions.args() From 60b00b6b9690395baff065f2c12d3146b5194397 Mon Sep 17 00:00:00 2001 From: rmaddikery Date: Thu, 10 Sep 2026 15:17:31 +0200 Subject: [PATCH 3/4] Fixes rst_to_trlc parser - Strip triling sphinx filter suffixes - Consume multi-line directive arg lines before field list parsing --- .../rules/rules_score/private/rst_to_trlc.bzl | 4 +- bazel/rules/rules_score/src/rst_to_trlc.py | 24 ++++++++- .../rules_score/test/rst_to_trlc_test.py | 50 +++++++++++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/bazel/rules/rules_score/private/rst_to_trlc.bzl b/bazel/rules/rules_score/private/rst_to_trlc.bzl index 19eecaf1..7b2129cd 100644 --- a/bazel/rules/rules_score/private/rst_to_trlc.bzl +++ b/bazel/rules/rules_score/private/rst_to_trlc.bzl @@ -63,9 +63,7 @@ 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: - # Declared in this rule's own package rather than next to `src` (via - # `sibling`), since `src` may come from a different repository (e.g. - # a cross-repo RST label), where declare_file(sibling=...) is invalid. + # `src` may come from a different repository (E.g.: a cross-repo RST label) out = ctx.actions.declare_file(src.basename[:-4] + ".trlc") outs.append(out) diff --git a/bazel/rules/rules_score/src/rst_to_trlc.py b/bazel/rules/rules_score/src/rst_to_trlc.py index 59e429d3..ce111066 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_NEEDS_FILTER = re.compile(r"(\s*\[[^\[\]]*\])+\s*$") _TRLC_HEADER = """\ /******************************************************************************** @@ -136,8 +137,19 @@ def _escape(text: str) -> str: def _collect_refs(fields: dict[str, str]) -> list[str]: - """Extract all cross-reference IDs from relationship fields.""" - return [r.strip() for k in _REF_FIELDS if k in fields for r in fields[k].split(",") if r.strip()] + """Extract all cross-reference IDs from relationship fields. + + Strips one or more trailing sphinx-needs filter expressions (E.g.: + [version==1] or [version==1][status==valid]), which are not + valid in TRLC reference syntax. + """ + return [ + _RE_NEEDS_FILTER.sub("", r.strip()).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]]: @@ -154,6 +166,14 @@ def parse_directives(content: str) -> list[dict[str, Any]]: directive, title = m.group(1), m.group(2).strip() i += 1 + # RST directive arguments may wrap onto continuation lines (docutils + # allows this); consume any such lines here before the field list, + # otherwise they get mis-parsed as body text and the real :id: (and + # other fields) on the following lines are missed entirely. + while i < len(lines) and lines[i].strip() and not _RE_FIELD.match(lines[i]): + title += " " + lines[i].strip() + i += 1 + fields, i = _collect_fields(lines, i) raw_body, i = _collect_body(lines, i) body = _RE_MARKUP.sub(r"\1", raw_body).strip() 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..0a83a1fa 100644 --- a/bazel/rules/rules_score/test/rst_to_trlc_test.py +++ b/bazel/rules/rules_score/test/rst_to_trlc_test.py @@ -336,6 +336,37 @@ def test_strips_rst_bold_markup_from_body(self): self.assertNotIn("**", result[0]["body"]) self.assertIn("robust", result[0]["body"]) + def test_multi_line_title_is_joined_and_fields_still_parsed(self): + """docutils allows a directive's argument to wrap onto continuation + lines with no blank-line separator; the real field list must still + be found on the lines that follow.""" + rst = _rst( + ".. comp_req:: Logging During the C++ Static Storage", + " construction and destruction.", + " :id: comp_req__test__001", + " :safety: QM", + "", + " Body.", + ) + result = parse_directives(rst) + self.assertEqual(len(result), 1) + self.assertEqual( + result[0]["title"], + "Logging During the C++ Static Storage construction and destruction.", + ) + self.assertEqual(result[0]["fields"]["id"], "comp_req__test__001") + + def test_single_line_title_unaffected_by_continuation_handling(self): + rst = _rst( + ".. feat_req:: Single Line Title", + " :id: feat_req__test__001", + " :safety: QM", + "", + " Body.", + ) + result = parse_directives(rst) + self.assertEqual(result[0]["title"], "Single Line Title") + # --------------------------------------------------------------------------- # render_trlc – TRLC output for each S-CORE type @@ -545,6 +576,25 @@ def test_mitigates_is_not_a_ref_field(self): def test_returns_empty_when_no_ref_fields(self): self.assertEqual(_collect_refs({"safety": "QM", "reqtype": "Functional"}), []) + def test_strips_trailing_needs_filter(self): + """sphinx-needs filter suffixes are not valid TRLC reference syntax.""" + self.assertEqual( + _collect_refs({"satisfies": "req_001[version==1]"}), + ["req_001"], + ) + + def test_strips_multiple_trailing_needs_filters(self): + self.assertEqual( + _collect_refs({"satisfies": "req_001[version==1][status==valid]"}), + ["req_001"], + ) + + def test_strips_needs_filter_from_each_comma_separated_ref(self): + self.assertEqual( + _collect_refs({"satisfies": "req_001[version==1], req_002[status==valid]"}), + ["req_001", "req_002"], + ) + # --------------------------------------------------------------------------- # _collect_body – blank-line handling consistency From 3241f4338f1b26fb426cc772f72bb3d1b318d032 Mon Sep 17 00:00:00 2001 From: rmaddikery Date: Thu, 10 Sep 2026 15:59:51 +0200 Subject: [PATCH 4/4] fix(rst_to_trlc): avoid output path collision for same-named RST sources --- bazel/rules/rules_score/private/rst_to_trlc.bzl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bazel/rules/rules_score/private/rst_to_trlc.bzl b/bazel/rules/rules_score/private/rst_to_trlc.bzl index 7b2129cd..df111e1c 100644 --- a/bazel/rules/rules_score/private/rst_to_trlc.bzl +++ b/bazel/rules/rules_score/private/rst_to_trlc.bzl @@ -64,7 +64,9 @@ def _rst_to_trlc_impl(ctx): outs = [] for src in ctx.files.srcs: # `src` may come from a different repository (E.g.: a cross-repo RST label) - out = ctx.actions.declare_file(src.basename[:-4] + ".trlc") + # Nest under the target's own name so two rst_to_trlc targets in the + # same package converting same-named files don't clash on the output path. + out = ctx.actions.declare_file(ctx.label.name + "/" + src.basename[:-4] + ".trlc") outs.append(out) args = ctx.actions.args()