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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ user.bazelrc
# docs build artifacts
/_build*
docs/ubproject.toml
docs/schemas.json

# Vale - editorial style guide
.vale.ini
Expand Down
2 changes: 1 addition & 1 deletion docs/internals/requirements/requirements.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,6 @@ Grouped Requirements
.. needextend:: c.this_doc() and type == 'tool_req' and not status
:status: valid

.. needextend:: "metamodel.yaml" in source_code_link
.. needextend:: source_code_link is not None and "metamodel.yaml" in source_code_link
:+satisfies: tool_req__docs_metamodel
:+tags: config
94 changes: 94 additions & 0 deletions src/extensions/score_metamodel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# score_metamodel

Sphinx extension that enforces the S-CORE metamodel on sphinx-needs documents.

It reads `metamodel.yaml` (the single source of truth for all need types, fields,
links, and constraints) and validates every need in the documentation against
those rules.

## What it does

1. **Registers need types** with sphinx-needs (directives like `feat_req`, `comp`,
`workflow`, etc.) including their fields, links, and extra options.
2. **Generates `schemas.json`** from the metamodel so that sphinx-needs 6 can
validate needs at parse time (required fields, regex patterns, link
constraints).
3. **Runs post-build checks** that go beyond what JSON Schema can express
(graph traversals, prohibited words, ID format rules).

## Metamodel overview

`metamodel.yaml` defines:

| Section | Purpose |
|---|---|
| `needs_types` | All need types (e.g. `feat_req`, `comp`, `document`) with their mandatory/optional fields and links |
| `needs_types_base_options` | Global optional fields applied to every type (e.g. `source_code_link`, `testlink`) |
| `needs_extra_links` | Custom link types (e.g. `satisfies`, `implements`, `mitigated_by`) |
| `prohibited_words_checks` | Forbidden words in titles/descriptions (e.g. "shall", "must") |
| `graph_checks` | Cross-need constraints (e.g. safety level decomposition rules) |

Each need type can specify:

- **`mandatory_options`** -- fields that must be present, with a regex pattern
the value must match (e.g. `status: ^(valid|invalid)$`).
- **`optional_options`** -- fields that, if present, must match a pattern.
- **`mandatory_links`** -- links that must have at least one target. The value
is either a plain type name (`stkh_req`) or a regex (`^logic_arc_int__.+$`).
- **`optional_links`** -- links that are allowed but not required.

## Validation layers

### Schema validation (sphinx-needs >6)

`sn_schemas.py` translates the metamodel into a `schemas.json` file that
sphinx-needs evaluates at parse time. Each schema entry has:

- **`select`** -- matches needs by their `type` field.
- **`validate.local`** -- JSON Schema checking the need's own properties
(required fields, regex patterns on option values, mandatory links with
`minItems: 1`). Regex patterns on **link IDs** (e.g. checking that
`includes` entries match `^logic_arc_int(_op)*__.+$`) are not yet
validated here; the schema only enforces that at least one link exists.
ID-pattern checking is still done by the Python `validate_links()` in
`check_options.py`.
- **`validate.network`** -- validates that linked needs have the expected
`type` (e.g. `satisfies` targets must be `stkh_req`). Uses the
sphinx-needs `items.local` format so each linked need is checked
individually. Only **mandatory** links are checked here; optional link
type violations are left to the Python `validate_links()` check, which
treats them as informational (`treat_as_info=True`) rather than errors.
Fields that mix regex and plain targets (e.g.
`complies: std_wp, ^std_req__aspice_40__iic.*$`) are also excluded
because the `items` schema would incorrectly require all linked needs
to match the plain type.

### Post-build Python checks

Checks in `checks/` run after the Sphinx build and cover rules that
JSON Schema cannot express:

| Check | File | What it validates |
|---|---|---|
| `check_options` | `check_options.py` | Mandatory/optional field presence and patterns (legacy, overlaps with schema validation) |
| `check_extra_options` | `check_options.py` | Warns about fields not defined in the metamodel |
| `check_id_format` | `attributes_format.py` | ID structure (`<type>__<abbrev>__<element>`, part count) |
| `check_for_prohibited_words` | `attributes_format.py` | Forbidden words in titles |
| `check_metamodel_graph` | `graph_checks.py` | Cross-need constraints (e.g. ASIL_B needs must link to non-QM requirements) |
| `check_id_contains_feature` | `id_contains_feature.py` | Need IDs must contain the feature abbreviation from the file path |
| `check_standards` | `standards.py` | Standard compliance link validation |

## File layout

```
score_metamodel/
__init__.py # Sphinx extension entry point (setup, check orchestration)
metamodel.yaml # The S-CORE metamodel definition
metamodel_types.py # Type definitions (ScoreNeedType, etc.)
yaml_parser.py # Parses metamodel.yaml into MetaModelData
sn_schemas.py # Generates schemas.json for sphinx-needs 6
log.py # CheckLogger for structured warning output
external_needs.py # External needs integration
checks/ # Post-build validation checks
tests/ # Unit and integration tests
```
27 changes: 26 additions & 1 deletion src/extensions/score_metamodel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import pkgutil
from collections.abc import Callable
from pathlib import Path
from typing import Any

from sphinx.application import Sphinx
from sphinx_needs import logging
Expand All @@ -31,6 +32,7 @@
from src.extensions.score_metamodel.metamodel_types import (
ScoreNeedType as ScoreNeedType,
)
from src.extensions.score_metamodel.sn_schemas import write_sn_schemas
from src.extensions.score_metamodel.yaml_parser import (
default_options as default_options,
)
Expand Down Expand Up @@ -237,10 +239,28 @@ def setup(app: Sphinx) -> dict[str, str | bool]:
# load metamodel.yaml via ruamel.yaml
metamodel = load_metamodel_data()

# Sphinx-Needs 6 requires extra options as dicts: {"name": ..., "schema": ...}
# Options WITH a schema get JSON schema validation (value must be a string).
# Options WITHOUT a schema are registered but not validated.
# non_schema_options = {"source_code_link", "testlink", "codelink"}
non_schema_options = {} # currently empty β†’ all options get schema validation
extra_options_schema: list[dict[str, Any]] = [
{"name": opt, "schema": {"type": "string"}}
for opt in metamodel.needs_extra_options
if opt not in non_schema_options
]
extra_options_wo_schema: list[dict[str, Any]] = [
{"name": opt}
for opt in metamodel.needs_extra_options
if opt in non_schema_options
]
# extra_options = [{"name": opt} for opt in metamodel.needs_extra_options]
extra_options = extra_options_schema + extra_options_wo_schema

# Assign everything to Sphinx config
app.config.needs_types = metamodel.needs_types
app.config.needs_extra_links = metamodel.needs_extra_links
app.config.needs_extra_options = metamodel.needs_extra_options
app.config.needs_extra_options = extra_options
app.config.graph_checks = metamodel.needs_graph_check
app.config.prohibited_words_checks = metamodel.prohibited_words_checks

Expand All @@ -251,6 +271,11 @@ def setup(app: Sphinx) -> dict[str, str | bool]:
app.config.needs_reproducible_json = True
app.config.needs_json_remove_defaults = True

# Generate schemas.json from the metamodel and register it with sphinx-needs.
# This enables sphinx-needs 6 schema validation: required fields, regex
# patterns on option values, and (eventually) link target type checks.
write_sn_schemas(app, metamodel)

# sphinx-collections runs on default prio 500.
# We need to populate the sphinx-collections config before that happens.
# --> 499
Expand Down
14 changes: 10 additions & 4 deletions src/extensions/score_metamodel/metamodel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,8 @@ needs_types:
testcovered: ^(YES|NO)$
hash: ^.*$
# req-Id: tool_req__docs_req_attr_validity_correctness
valid_from: ^v(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))?$
valid_until: ^v(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))?$
valid_from: ^v(0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?$
valid_until: ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?$
tags:
- requirement
- requirement_excl_process
Expand Down Expand Up @@ -299,8 +299,8 @@ needs_types:
testcovered: ^(YES|NO)$
hash: ^.*$
# req-Id: tool_req__docs_req_attr_validity_correctness
valid_from: ^v(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))?$
valid_until: ^v(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))?$
valid_from: ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?$
valid_until: ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?$
optional_links:
belongs_to: feat # for evaluation
tags:
Expand Down Expand Up @@ -971,6 +971,12 @@ needs_extra_links:
partially_verifies:
incoming: partially_verified_by
outgoing: partially_verifies

# Decision Records
affects:
incoming: affected by
outgoing: affects

##############################################################
# Graph Checks
# The graph checks focus on the relation of the needs and their attributes.
Expand Down
Loading
Loading