Skip to content
Merged
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
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,6 @@ http_file(
)

bazel_dep(name = "score_process", version = "1.6.0")

# Provide the tools from the devcontainer to Bazel
bazel_dep(name = "score_devcontainer", version = "1.7.0")
2 changes: 2 additions & 0 deletions MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions docs/how-to/write_docs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,47 @@ For further documentation on needextends please `look here <https://sphinx-needs

In the future we will enable a check that needextends will only modify needs in the current document.
You can ensure this by adding `c.this_doc()` to the filter string of the need.


Requirement ID Feature Part
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Requirement IDs with 3 parts (defined by the Metamodel) follow the format ``<Type>__<feature>__<Title>``.
The ``<feature>`` part must relate to where the requirement lives in the documentation,
so that IDs stay meaningful and traceable as the project evolves.

The feature part is validated by checking that at least one of the following is true:

* A segment of the feature part (split on ``_`` and ``-``) appears in the document's directory path
* The initials of the feature part's segments appear in the document's directory path
* The feature part contains a string explicitly allowed via ``required_in_id`` in ``conf.py``

**Examples** β€” given a requirement in ``internals/safety/fmea/requirements.rst``:

.. list-table::
:header-rows: 1
:widths: 45 10 45

* - ID
-
- Reason
* - ``feat_saf__fmea__late_message``
- βœ…
- ``fmea`` is in the path
* - ``feat_saf__safety_fmea__late_message``
- βœ…
- ``safety`` and ``fmea`` are in the path
* - ``feat_saf__sf__late_message``
- βœ…
- ``sf`` are the initials of ``safety_fmea``, which is in the path
* - ``feat_saf__blabla__late_message``
- ❌
- ``blabla`` has no relation to the path ``internals/safety/fmea``

To explicitly allow a feature part that intentionally doesn't match the path
(e.g. in a single module repository), add a matching string to ``required_in_id`` in ``conf.py``:

.. code-block:: python

# conf.py
required_in_id = ["persistenc"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review (Claude):

"persistenc" is a partial string that would match persistence, persistency, etc. If this substring match is intentional, it should be explained in the docs β€” readers will assume it's a typo for "persistence" and either copy it incorrectly or fix it in their own conf.py without understanding why the truncation was used.

If the intent is an exact feature name, use the full string. If partial matching is a deliberate affordance, document it explicitly here.

1 change: 1 addition & 0 deletions src/extensions/score_metamodel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ def _clear_needs_defaults(app: Sphinx):
def setup(app: Sphinx) -> dict[str, str | bool]:
app.add_config_value("external_needs_source", "", rebuild="env")
app.add_config_value("score_metamodel_yaml", "", rebuild="env")
app.add_config_value("required_in_id", [], rebuild="env")
config_setdefault(app.config, "needs_id_required", True)
config_setdefault(app.config, "needs_id_regex", "^[A-Za-z0-9_-]{6,}")

Expand Down
33 changes: 25 additions & 8 deletions src/extensions/score_metamodel/checks/id_contains_feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,36 @@ def id_contains_feature(app: Sphinx, need: NeedItem, log: CheckLogger):
for featurepart in featureparts
if featureparts and featurepart and docname
)
allowed_parts_from_config = app.config.required_in_id
found_part_from_config = any(
part_from_config.lower() in feature.lower()
for part_from_config in allowed_parts_from_config

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review (Claude):

The match is against the full need ID (e.g. feat_req__blabla__testing) rather than the feature part alone (parts[1]). This means a config entry like "feat" or "req" would silently suppress the check for every 3-part ID, regardless of the actual feature value.

The check should compare against feature (i.e. parts[1]) to match the documented intent: "strings that are also allowed in the feature_part of the id".

Suggested change
for part_from_config in allowed_parts_from_config
found_part_from_config = any(
part_from_config.lower() in feature.lower()
for part_from_config in allowed_parts_from_config
if allowed_parts_from_config
)

if allowed_parts_from_config
)

# allow abbreviation of the feature
initials = (
"".join(fp[0].lower() for fp in featureparts) if len(featureparts) > 1 else ""
)
foundinitials = bool(initials) and docname and initials in docname.lower()
if not (foundfeatpart or foundinitials or found_part_from_config):
parts_display = ", ".join(f"'{p}'" for p in featureparts)
config_display = (
", ".join(f"'{p}'" for p in allowed_parts_from_config)
if allowed_parts_from_config
else "[]"
)

fix_options = [f"rename the feature part to match a segment of '{docname}'"]
if initials:
fix_options.append(f"use correct abbreviation '{initials}'")
fix_options.append(
f"Add an allowed part to `required_in_id` in conf.py (currently: {config_display})"
)

if not (foundfeatpart or foundinitials):
log.warning_for_option(
need,
"id",
(
f"Featurepart '{featureparts}' not in path '{docname}' "
f"or abbreviation not ok, expected: '{initials}'."
),
combined_msg = (
f"Feature part {parts_display} not found in path '{docname}'. "
"\nHow can you fix this:\n=>"
f"{'\n=> '.join(fix_options)}.\n"
)
log.warning_for_option(need, "id", combined_msg)
2 changes: 1 addition & 1 deletion src/extensions/score_metamodel/tests/rst/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"sphinx_needs",
"score_metamodel",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review (Claude):

Adding required_in_id = ["blabla"] to the shared conf.py affects every test that uses this Sphinx build, not just the new id_contains_feature tests. Any test whose need ID happens to contain the string "blabla" would silently bypass the id_contains_feature check.

More importantly, this is a globally-permissive sentinel value chosen purely for testing. When reviewing or debugging failures, it's non-obvious why blabla is allowed globally. A more precise approach: add a dedicated conf.py under the id_contains_feature/ subdirectory so test isolation is explicit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a non issue as we only tests single checks in each RST file.
This will not interfeer.

]

required_in_id = ["blabla"]
needs_external_needs = [
{
"base_url": "https://eclipse-score.github.io/process_description/main/",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

#CHECK: id_contains_feature


.. Feature is in the path of the RST file
#EXPECT-NOT[+2]: Feature 'id_contains_feature' not in path

Expand All @@ -34,3 +36,33 @@

.. stkh_req:: This is a test
:id: stkh_req__test__abce



.. Check if feature is correctly found to not be in path
#EXPECT[+2]: Feature part 'abcabc' not found in path 'id_contains_feature'.

.. feat_req:: Testing if warning correctly triggers
:id: feat_req__abcabc__testing



.. Check if feature is correctly found to be in path
#EXPECT-NOT[+2]: Feature part

.. feat_req:: Testing if warning correctly triggers
:id: feat_req__id_contains__testing


.. Testing if additional allowed param in conf.py is valid
#EXPECT-NOT[+2]: Feature part

.. feat_req:: Testing conf.py parameter
:id: feat_req__blabla__testing

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-assisted review (Claude):

This test verifies that feat_req__blabla__testing does not produce a warning when required_in_id = ["blabla"] is set β€” good. But there's no test for the negative boundary: a need whose ID contains blabla only in the type or title segment (e.g. feat_req__other__blabla_testing) should still trigger a warning.

Once the match is narrowed to the feature part (see comment on id_contains_feature.py), a test like this is needed to confirm the fix doesn't over-suppress.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems plausible. Will add this.



.. Testing if additional allowed param in conf.py is valid
#EXPECT[+2]: Feature part 'abcabcabc' not found in path 'id_contains_feature'.

.. feat_req:: Testing conf.py parameter
:id: feat_req__abcabcabc__blabla_testing
Loading