diff --git a/strictdoc/core/file_traceability_index.py b/strictdoc/core/file_traceability_index.py
index e97854752..40b1ea7ef 100644
--- a/strictdoc/core/file_traceability_index.py
+++ b/strictdoc/core/file_traceability_index.py
@@ -47,6 +47,7 @@
from strictdoc.helpers.cast import assert_cast
from strictdoc.helpers.exception import StrictDocException
from strictdoc.helpers.google_test import convert_function_name_to_gtest_macro
+from strictdoc.helpers.mid import MID
from strictdoc.helpers.ordered_set import OrderedSet
if TYPE_CHECKING:
@@ -601,15 +602,36 @@ def validate_and_resolve(
continue
assert source_node_.entity_name is not None
+ sdoc_node = None
sdoc_node_uid = source_node_.get_sdoc_field(
"UID", relevant_source_node_entry
)
- if sdoc_node_uid is None:
- sdoc_node_uid = f"{document_uid}/{path_to_source_file_}/{source_node_.entity_name}"
- sdoc_node = traceability_index.get_node_by_uid_weak(
- sdoc_node_uid
+ mid = source_node_.get_sdoc_field(
+ "MID", relevant_source_node_entry
)
+ # First merge criterion: Merge if SDoc node with same MID exists.
+ if mid is not None:
+ sdoc_node_mid = MID(mid)
+ merge_candidate_sdoc_node = (
+ traceability_index.get_node_by_mid_weak(sdoc_node_mid)
+ )
+ if isinstance(merge_candidate_sdoc_node, SDocNode):
+ sdoc_node = merge_candidate_sdoc_node
+ sdoc_node_uid = sdoc_node.reserved_uid
+
+ if sdoc_node is None:
+ # If no UID from source code field or merge-by-MID, create UID by conventional scheme.
+ if sdoc_node_uid is None:
+ sdoc_node_uid = f"{document_uid}/{path_to_source_file_}/{source_node_.entity_name}"
+ # Second merge criterion: Merge if SDoc node with same UID exists.
+ tmp_sdoc_node = traceability_index.get_node_by_uid_weak(
+ sdoc_node_uid
+ )
+ if isinstance(tmp_sdoc_node, SDocNode):
+ sdoc_node = tmp_sdoc_node
+
+ assert sdoc_node_uid is not None
if sdoc_node is not None:
sdoc_node = assert_cast(sdoc_node, SDocNode)
self.merge_sdoc_node_with_source_node(
@@ -626,11 +648,6 @@ def validate_and_resolve(
document,
)
sdoc_node_uid = assert_cast(sdoc_node.reserved_uid, str)
- traceability_index.graph_database.create_link(
- link_type=GraphLinkType.UID_TO_NODE,
- lhs_node=sdoc_node_uid,
- rhs_node=sdoc_node,
- )
if current_top_node is None:
current_top_node = (
FileTraceabilityIndex.create_source_node_section(
@@ -998,6 +1015,25 @@ def merge_sdoc_node_with_source_node(
)
# Merge strategy: overwrite any field if there's a field with same name from custom tags.
sdoc_node_fields = source_node.get_sdoc_fields(source_node_config_entry)
+
+ # Sanity check: Nor UID neither MID must conflict (early auto-MID is allowed to be overwritten)
+ if (
+ "MID" in sdoc_node.ordered_fields_lookup
+ and "MID" in sdoc_node_fields
+ ):
+ sdoc_mid_field = sdoc_node.get_field_by_name("MID").get_text_value()
+ if sdoc_mid_field != sdoc_node_fields["MID"]:
+ raise StrictDocException(
+ f"Can't merge node by UID {sdoc_node.reserved_uid}: "
+ f"Conflicting MID: {sdoc_mid_field} != {sdoc_node_fields['MID']}"
+ )
+ if sdoc_node.reserved_uid is not None and "UID" in sdoc_node_fields:
+ if sdoc_node.reserved_uid != sdoc_node_fields["UID"]:
+ raise StrictDocException(
+ f"Can't merge node by MID {sdoc_node.reserved_mid}: "
+ f"Conflicting UID: {sdoc_node.reserved_uid} != {sdoc_node_fields['UID']}"
+ )
+
FileTraceabilityIndex.set_sdoc_node_fields(sdoc_node, sdoc_node_fields)
@staticmethod
@@ -1081,6 +1117,45 @@ def connect_source_node_requirements(
Here we link REQ and sdoc_node bidirectional.
"""
+ if (
+ sdoc_node.reserved_uid is not None
+ and not traceability_index.graph_database.has_link(
+ link_type=GraphLinkType.UID_TO_NODE,
+ lhs_node=sdoc_node.reserved_uid,
+ rhs_node=sdoc_node,
+ )
+ ):
+ traceability_index.graph_database.create_link(
+ link_type=GraphLinkType.UID_TO_NODE,
+ lhs_node=sdoc_node.reserved_uid,
+ rhs_node=sdoc_node,
+ )
+
+ # A merge procedure may have overwritten the MID,
+ # in which case the graph database and search index needs an update.
+ if "MID" in sdoc_node.ordered_fields_lookup != sdoc_node.reserved_mid:
+ sdoc_mid_field = sdoc_node.get_field_by_name("MID").get_text_value()
+ if sdoc_mid_field != sdoc_node.reserved_mid:
+ # TODO:
+ # If we really want to support changing the auto-assigned MID,
+ # at least the graph database and the document search index need an update (remove old MID, add new MID).
+ # I currently struggle to update the search index.
+ parent_document = sdoc_node.get_parent_or_including_document()
+ sdoc_node.reserved_mid = MID(sdoc_mid_field)
+ if parent_document.config.enable_mid:
+ sdoc_node.mid_permanent = True
+
+ if not traceability_index.graph_database.has_link(
+ link_type=GraphLinkType.MID_TO_NODE,
+ lhs_node=sdoc_node.reserved_mid,
+ rhs_node=sdoc_node,
+ ):
+ traceability_index.graph_database.create_link(
+ link_type=GraphLinkType.MID_TO_NODE,
+ lhs_node=sdoc_node.reserved_mid,
+ rhs_node=sdoc_node,
+ )
+
for marker_ in source_node.markers:
if not isinstance(marker_, FunctionRangeMarker):
continue
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_static_node/parent.sdoc b/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_sdoc_by_default_uid/parent.sdoc
similarity index 100%
rename from tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_static_node/parent.sdoc
rename to tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_sdoc_by_default_uid/parent.sdoc
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_static_node/source_node_base.sdoc b/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_sdoc_by_default_uid/source_node_base.sdoc
similarity index 93%
rename from tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_static_node/source_node_base.sdoc
rename to tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_sdoc_by_default_uid/source_node_base.sdoc
index 22caeec03..cd74e7c5b 100644
--- a/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_static_node/source_node_base.sdoc
+++ b/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_sdoc_by_default_uid/source_node_base.sdoc
@@ -39,7 +39,7 @@ ELEMENTS:
TITLE: Merge example.c into static nodes
[REQUIREMENT]
-UID: SRC-NODES-BASE/src/example/example.c/example_1
+UID: SRC-NODES-BASE/src/example.c/example_1
TITLE: TITLE from sdoc
FOO: FOO text from sdoc
BAR: BAR text from sdoc
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_static_node/src/example/example.c b/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_sdoc_by_default_uid/src/example.c
similarity index 100%
rename from tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_static_node/src/example/example.c
rename to tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_sdoc_by_default_uid/src/example.c
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_static_node/strictdoc.toml b/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_sdoc_by_default_uid/strictdoc.toml
similarity index 100%
rename from tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_static_node/strictdoc.toml
rename to tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_sdoc_by_default_uid/strictdoc.toml
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_sdoc_by_default_uid/test.itest b/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_sdoc_by_default_uid/test.itest
new file mode 100644
index 000000000..37db9c53f
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_sdoc_by_default_uid/test.itest
@@ -0,0 +1,30 @@
+#
+# This test verifies that a source nodes is merged with a static SDoc node if
+# - the source nodes is not marked up with a UID field (i.e., default UID is effective), and
+# - static SDoc node was explicitly given the default UID.
+#
+# @relation(SDOC-SRS-141, scope=file)
+#
+
+RUN: %strictdoc --debug export %S --output-dir %T | filecheck %s
+
+CHECK: Published: Hello world doc
+
+RUN: %check_exists --file "%T/html/_source_files/src/example.c.html"
+
+RUN: %cat %T/html/%THIS_TEST_FOLDER/source_node_base.html | filecheck %s --check-prefix CHECK-HTML
+CHECK-HTML: Requirements from Source Nodes
+CHECK-HTML: SRC-NODES-BASE/src/example.c/example_1
+CHECK-HTML: TITLE from sdoc
+CHECK-HTML: class="requirement__link-parent" href="../30_merge_with_sdoc_by_default_uid/parent.html#REQ-1"
+CHECK-HTML: src/example.c, lines: 3-14, function example_1()
+CHECK-HTML-NOT: FOO text from sdoc
+CHECK-HTML: FOO text from example.c
+CHECK-HTML-NOT: BAR text from sdoc
+CHECK-HTML: BAR text from example.c
+
+RUN: %cat %T/html/_source_files/src/example.c.html | filecheck %s --check-prefix CHECK-SOURCE-FILE
+CHECK-SOURCE-FILE: SRC-NODES-BASE/src/example.c/example_1
+
+RUN: %cat %T/html/source_coverage.html | filecheck %s --check-prefix CHECK-SOURCE-COVERAGE
+CHECK-SOURCE-COVERAGE: 100.0
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_static_node/test.itest b/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_static_node/test.itest
deleted file mode 100644
index edd3709f2..000000000
--- a/tests/integration/features/source_code_traceability/_source_nodes/30_merge_with_static_node/test.itest
+++ /dev/null
@@ -1,24 +0,0 @@
-#
-# @relation(SDOC-SRS-141, scope=file)
-#
-
-RUN: %strictdoc --debug export %S --output-dir %T | filecheck %s
-
-CHECK: Published: Hello world doc
-
-RUN: %check_exists --file "%T/html/_source_files/src/example/example.c.html"
-
-RUN: %cat %T/html/%THIS_TEST_FOLDER/source_node_base.html | filecheck %s --check-prefix CHECK-HTML
-CHECK-HTML: Requirements from Source Nodes
-CHECK-HTML: SRC-NODES-BASE/src/example/example.c/example_1
-CHECK-HTML: {{.*}}TITLE from sdoc
-CHECK-HTML: class="requirement__link-parent" href="../30_merge_with_static_node/parent.html#REQ-1"
-CHECK-HTML: src/example/example.c, lines: 3-14, function example_1()
-CHECK-HTML: FOO text from example.c
-CHECK-HTML: BAR text from example.c
-
-RUN: %cat %T/html/_source_files/src/example/example.c.html | filecheck %s --check-prefix CHECK-SOURCE-FILE
-CHECK-SOURCE-FILE: SRC-NODES-BASE/src/example/example.c/example_1
-
-RUN: %cat %T/html/source_coverage.html | filecheck %s --check-prefix CHECK-SOURCE-COVERAGE
-CHECK-SOURCE-COVERAGE: 100.0
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/parent.sdoc b/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/parent.sdoc
new file mode 100644
index 000000000..7eff27bf2
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/parent.sdoc
@@ -0,0 +1,17 @@
+[DOCUMENT]
+TITLE: Hello world doc
+
+[REQUIREMENT]
+UID: REQ-1
+TITLE: Requirement Title
+STATEMENT: Requirement Statement
+
+[REQUIREMENT]
+UID: REQ-2
+TITLE: Requirement Title #2
+STATEMENT: Requirement Statement #2
+
+[REQUIREMENT]
+UID: REQ-3
+TITLE: Requirement Title #3
+STATEMENT: Requirement Statement #3
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/source_node_base.sdoc b/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/source_node_base.sdoc
new file mode 100644
index 000000000..b27f68bbc
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/source_node_base.sdoc
@@ -0,0 +1,72 @@
+[DOCUMENT]
+MID: c2d4542d5f1741c88dfcb4f68ad7dcbd
+TITLE: Requirements from Source Nodes
+UID: SRC-NODES-BASE
+
+[GRAMMAR]
+ELEMENTS:
+- TAG: SECTION
+ PROPERTIES:
+ IS_COMPOSITE: True
+ FIELDS:
+ - TITLE: UID
+ TYPE: String
+ REQUIRED: False
+ - TITLE: TITLE
+ TYPE: String
+ REQUIRED: True
+- TAG: REQUIREMENT
+ PROPERTIES:
+ VIEW_STYLE: Narrative
+ FIELDS:
+ - TITLE: UID
+ TYPE: String
+ REQUIRED: False
+ - TITLE: MID
+ TYPE: String
+ REQUIRED: False
+ - TITLE: TITLE
+ TYPE: String
+ REQUIRED: False
+ - TITLE: FOO
+ TYPE: String
+ REQUIRED: False
+ - TITLE: BAR
+ TYPE: String
+ REQUIRED: False
+ RELATIONS:
+ - TYPE: Parent
+ - TYPE: File
+
+[[SECTION]]
+TITLE: Merge example.c into static nodes
+
+[REQUIREMENT]
+UID: REQ-SOURCE-1
+TITLE: TITLE1 from sdoc
+FOO: FOO1 text from sdoc
+BAR: BAR1 text from sdoc
+RELATIONS:
+- TYPE: Parent
+ VALUE: REQ-1
+
+[REQUIREMENT]
+UID: REQ-SOURCE-2
+MID: 80cd685d-0e18-44b8-9842-c1863a2eb9ec
+TITLE: TITLE2 from sdoc
+FOO: FOO2 text from sdoc
+BAR: BAR2 text from sdoc
+RELATIONS:
+- TYPE: Parent
+ VALUE: REQ-2
+
+[REQUIREMENT]
+UID: REQ-SOURCE-3
+TITLE: TITLE3 from sdoc
+FOO: FOO3 text from sdoc
+BAR: BAR3 text from sdoc
+RELATIONS:
+- TYPE: Parent
+ VALUE: REQ-3
+
+[[/SECTION]]
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/src/example.c b/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/src/example.c
new file mode 100644
index 000000000..fa900f484
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/src/example.c
@@ -0,0 +1,48 @@
+#include
+
+/**
+ * Some text.
+ *
+ * @relation(REQ-1, scope=function)
+ *
+ * UID: REQ-SOURCE-1
+ *
+ * FOO: FOO1 text from example.c
+ *
+ * BAR: BAR1 text from example.c
+ */
+void example_1(void) {
+ print("hello world\n");
+}
+
+/**
+ * Some text.
+ *
+ * @relation(REQ-2, scope=function)
+ *
+ * UID: REQ-SOURCE-2
+ *
+ * FOO: FOO2 text from example.c
+ *
+ * BAR: BAR2 text from example.c
+ */
+void example_2(void) {
+ print("hello world\n");
+}
+
+/**
+ * Some text.
+ *
+ * @relation(REQ-3, scope=function)
+ *
+ * UID: REQ-SOURCE-3
+ *
+ * MID: 1973a567-a109-491d-b7f0-6bb22eafa6ab
+ *
+ * FOO: FOO3 text from example.c
+ *
+ * BAR: BAR3 text from example.c
+ */
+void example_3(void) {
+ print("hello world\n");
+}
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/strictdoc.toml b/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/strictdoc.toml
new file mode 100644
index 000000000..f8688a0cd
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/strictdoc.toml
@@ -0,0 +1,14 @@
+[project]
+
+features = [
+ "REQUIREMENT_TO_SOURCE_TRACEABILITY",
+ "SOURCE_FILE_LANGUAGE_PARSERS",
+]
+
+source_nodes = [
+ { "src/" = { uid = "SRC-NODES-BASE", node_type = "REQUIREMENT" } }
+]
+
+exclude_source_paths = [
+ "test.itest"
+]
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/test.itest b/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/test.itest
new file mode 100644
index 000000000..6c426ff6b
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/31_merge_with_sdoc_by_uid/test.itest
@@ -0,0 +1,58 @@
+#
+# This test verifies that a source nodes is merged with a static SDoc node with same explicit UID annotated.
+# The following cases shall be handled:
+# - If MID is set at neither side, a random (but hidden) automatic MID is assigned by StrictDoc.
+# - If MID is set only at the sdoc side, the MID remains effective.
+# - If MID is set only at the source side, early random automatic MID is overwritten by source side MID.
+#
+# Cases tested elsewhere:
+# - If MID is set on both sides and is equal, this is considered "merge by mid".
+# - If MID is set on both sides but is different, this is considered a validation error.
+#
+# @relation(SDOC-SRS-141, scope=file)
+#
+
+RUN: %strictdoc --debug export %S --output-dir %T | filecheck %s
+
+CHECK: Published: Hello world doc
+
+RUN: %check_exists --file "%T/html/_source_files/src/example.c.html"
+
+RUN: %cat %T/html/%THIS_TEST_FOLDER/source_node_base.html | filecheck %s --check-prefix CHECK-HTML
+CHECK-HTML: Requirements from Source Nodes
+CHECK-HTML-NOT: SRC-NODES-BASE/src/example.c/example_1
+CHECK-HTML: REQ-SOURCE-1
+CHECK-HTML: TITLE1 from sdoc
+CHECK-HTML: src/example.c, lines: 3-16, function example_1()
+CHECK-HTML-NOT: FOO1 text from sdoc
+CHECK-HTML: FOO1 text from example.c
+CHECK-HTML-NOT: BAR1 text from sdoc
+CHECK-HTML: BAR1 text from example.c
+
+CHECK-HTML-NOT: SRC-NODES-BASE/src/example.c/example_2
+CHECK-HTML: REQ-SOURCE-2
+CHECK-HTML: TITLE2 from sdoc
+CHECK-HTML: src/example.c, lines: 18-31, function example_2()
+CHECK-HTML-NOT: FOO2 text from sdoc
+CHECK-HTML: FOO2 text from example.c
+CHECK-HTML-NOT: BAR2 text from sdoc
+CHECK-HTML: BAR2 text from example.c
+
+CHECK-HTML-NOT: SRC-NODES-BASE/src/example.c/example_3
+CHECK-HTML: REQ-SOURCE-3
+CHECK-HTML: TITLE3 from sdoc
+CHECK-HTML: 1973a567-a109-491d-b7f0-6bb22eafa6ab
+CHECK-HTML: src/example.c, lines: 33-48, function example_3()
+CHECK-HTML-NOT: FOO3 text from sdoc
+CHECK-HTML: FOO3 text from example.c
+CHECK-HTML-NOT: BAR3 text from sdoc
+CHECK-HTML: BAR3 text from example.c
+
+RUN: %cat %T/html/_source_files/src/example.c.html | filecheck %s --check-prefix CHECK-SOURCE-FILE
+CHECK-HTML-NOT: SRC-NODES-BASE/src/example.c/example_1
+CHECK-SOURCE-FILE: REQ-SOURCE-1
+CHECK-SOURCE-FILE: REQ-SOURCE-2
+CHECK-SOURCE-FILE: REQ-SOURCE-3
+
+RUN: %cat %T/html/source_coverage.html | filecheck %s --check-prefix CHECK-SOURCE-COVERAGE
+CHECK-SOURCE-COVERAGE: 100.0
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/parent.sdoc b/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/parent.sdoc
new file mode 100644
index 000000000..278b98a77
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/parent.sdoc
@@ -0,0 +1,12 @@
+[DOCUMENT]
+TITLE: Hello world doc
+
+[REQUIREMENT]
+UID: REQ-1
+TITLE: Requirement Title
+STATEMENT: Requirement Statement
+
+[REQUIREMENT]
+UID: REQ-2
+TITLE: Requirement Title #2
+STATEMENT: Requirement Statement #2
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/source_node_base.sdoc b/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/source_node_base.sdoc
new file mode 100644
index 000000000..c6c84f958
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/source_node_base.sdoc
@@ -0,0 +1,54 @@
+[DOCUMENT]
+MID: c2d4542d5f1741c88dfcb4f68ad7dcbd
+TITLE: Requirements from Source Nodes
+UID: SRC-NODES-BASE
+
+[GRAMMAR]
+ELEMENTS:
+- TAG: SECTION
+ PROPERTIES:
+ IS_COMPOSITE: True
+ FIELDS:
+ - TITLE: UID
+ TYPE: String
+ REQUIRED: False
+ - TITLE: TITLE
+ TYPE: String
+ REQUIRED: True
+- TAG: REQUIREMENT
+ PROPERTIES:
+ VIEW_STYLE: Narrative
+ FIELDS:
+ - TITLE: UID
+ TYPE: String
+ REQUIRED: False
+ - TITLE: MID
+ TYPE: String
+ REQUIRED: False
+ - TITLE: TITLE
+ TYPE: String
+ REQUIRED: False
+ - TITLE: FOO
+ TYPE: String
+ REQUIRED: False
+ - TITLE: BAR
+ TYPE: String
+ REQUIRED: False
+ RELATIONS:
+ - TYPE: Parent
+ - TYPE: File
+
+[[SECTION]]
+TITLE: Merge example.c into static nodes
+
+[REQUIREMENT]
+UID: REQ-SOURCE-1
+MID: 3bd0162d-d6d2-42a1-9324-ab8415190970
+TITLE: TITLE from sdoc
+FOO: FOO text from sdoc
+BAR: BAR text from sdoc
+RELATIONS:
+- TYPE: Parent
+ VALUE: REQ-1
+
+[[/SECTION]]
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/src/example.c b/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/src/example.c
new file mode 100644
index 000000000..0fe5bf23e
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/src/example.c
@@ -0,0 +1,16 @@
+#include
+
+/**
+ * Some text.
+ *
+ * @relation(REQ-1, scope=function)
+ *
+ * MID: 3bd0162d-d6d2-42a1-9324-ab8415190970
+ *
+ * FOO: FOO text from example.c
+ *
+ * BAR: BAR text from example.c
+ */
+void example_1(void) {
+ print("hello world\n");
+}
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/strictdoc.toml b/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/strictdoc.toml
new file mode 100644
index 000000000..f8688a0cd
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/strictdoc.toml
@@ -0,0 +1,14 @@
+[project]
+
+features = [
+ "REQUIREMENT_TO_SOURCE_TRACEABILITY",
+ "SOURCE_FILE_LANGUAGE_PARSERS",
+]
+
+source_nodes = [
+ { "src/" = { uid = "SRC-NODES-BASE", node_type = "REQUIREMENT" } }
+]
+
+exclude_source_paths = [
+ "test.itest"
+]
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/test.itest b/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/test.itest
new file mode 100644
index 000000000..aefd72bf4
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/32_merge_with_sdoc_by_mid/test.itest
@@ -0,0 +1,34 @@
+#
+# This test verifies that a source nodes is merged with a static SDoc node with same explicit MID annotated.
+# The following case shall be handled:
+# - If MID is set and equal at both sides and no UID update is required, merge the node.
+#
+# Cases tested elsewhere:
+# - If MID is set and equal on both sides but UID update would be required, this is considered a validation error.
+#
+# @relation(SDOC-SRS-141, scope=file)
+#
+
+RUN: %strictdoc --debug export %S --output-dir %T | filecheck %s
+
+CHECK: Published: Hello world doc
+
+RUN: %check_exists --file "%T/html/_source_files/src/example.c.html"
+
+RUN: %cat %T/html/%THIS_TEST_FOLDER/source_node_base.html | filecheck %s --check-prefix CHECK-HTML
+CHECK-HTML: Requirements from Source Nodes
+CHECK-HTML-NOT: SRC-NODES-BASE/src/example.c/example_1
+CHECK-HTML: 3bd0162d-d6d2-42a1-9324-ab8415190970
+CHECK-HTML: TITLE from sdoc
+CHECK-HTML: src/example.c, lines: 3-16, function example_1()
+CHECK-HTML-NOT: FOO text from sdoc
+CHECK-HTML: FOO text from example.c
+CHECK-HTML-NOT: BAR text from sdoc
+CHECK-HTML: BAR text from example.c
+
+RUN: %cat %T/html/_source_files/src/example.c.html | filecheck %s --check-prefix CHECK-SOURCE-FILE
+CHECK-HTML-NOT: SRC-NODES-BASE/src/example.c/example_1
+CHECK-SOURCE-FILE: 3bd0162d-d6d2-42a1-9324-ab8415190970
+
+RUN: %cat %T/html/source_coverage.html | filecheck %s --check-prefix CHECK-SOURCE-COVERAGE
+CHECK-SOURCE-COVERAGE: 100.0
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_validation/03_merge_by_uid_conflicting_mid/source_node_base.sdoc b/tests/integration/features/source_code_traceability/_source_nodes/_validation/03_merge_by_uid_conflicting_mid/source_node_base.sdoc
new file mode 100644
index 000000000..e00351a94
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/_validation/03_merge_by_uid_conflicting_mid/source_node_base.sdoc
@@ -0,0 +1,48 @@
+[DOCUMENT]
+MID: c2d4542d5f1741c88dfcb4f68ad7dcbd
+TITLE: Requirements from Source Nodes
+UID: SRC-NODES-BASE
+
+[GRAMMAR]
+ELEMENTS:
+- TAG: SECTION
+ PROPERTIES:
+ IS_COMPOSITE: True
+ FIELDS:
+ - TITLE: UID
+ TYPE: String
+ REQUIRED: False
+ - TITLE: TITLE
+ TYPE: String
+ REQUIRED: True
+- TAG: REQUIREMENT
+ PROPERTIES:
+ VIEW_STYLE: Narrative
+ FIELDS:
+ - TITLE: UID
+ TYPE: String
+ REQUIRED: False
+ - TITLE: MID
+ TYPE: String
+ REQUIRED: False
+ - TITLE: TITLE
+ TYPE: String
+ REQUIRED: False
+ - TITLE: FOO
+ TYPE: String
+ REQUIRED: False
+ - TITLE: BAR
+ TYPE: String
+ REQUIRED: False
+
+[[SECTION]]
+TITLE: Merge example.c into static nodes
+
+[REQUIREMENT]
+UID: REQ-SOURCE-1
+MID: 3bd0162d-d6d2-42a1-9324-ab8415190970
+TITLE: TITLE from sdoc
+FOO: FOO text from sdoc
+BAR: BAR text from sdoc
+
+[[/SECTION]]
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_validation/03_merge_by_uid_conflicting_mid/src/example.c b/tests/integration/features/source_code_traceability/_source_nodes/_validation/03_merge_by_uid_conflicting_mid/src/example.c
new file mode 100644
index 000000000..fcc707090
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/_validation/03_merge_by_uid_conflicting_mid/src/example.c
@@ -0,0 +1,14 @@
+#include
+
+/**
+ * UID: REQ-SOURCE-1
+ *
+ * MID: e7c09b63-2bed-4046-9511-a5b17978f152
+ *
+ * FOO: FOO text from example.c
+ *
+ * BAR: BAR text from example.c
+ */
+void example_1(void) {
+ print("hello world\n");
+}
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_validation/03_merge_by_uid_conflicting_mid/strictdoc.toml b/tests/integration/features/source_code_traceability/_source_nodes/_validation/03_merge_by_uid_conflicting_mid/strictdoc.toml
new file mode 100644
index 000000000..f8688a0cd
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/_validation/03_merge_by_uid_conflicting_mid/strictdoc.toml
@@ -0,0 +1,14 @@
+[project]
+
+features = [
+ "REQUIREMENT_TO_SOURCE_TRACEABILITY",
+ "SOURCE_FILE_LANGUAGE_PARSERS",
+]
+
+source_nodes = [
+ { "src/" = { uid = "SRC-NODES-BASE", node_type = "REQUIREMENT" } }
+]
+
+exclude_source_paths = [
+ "test.itest"
+]
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_validation/03_merge_by_uid_conflicting_mid/test.itest b/tests/integration/features/source_code_traceability/_source_nodes/_validation/03_merge_by_uid_conflicting_mid/test.itest
new file mode 100644
index 000000000..bc72f832e
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/_validation/03_merge_by_uid_conflicting_mid/test.itest
@@ -0,0 +1,9 @@
+#
+# This test verifies that if SDoc node and source node have same UID and thus are
+# subject to be merged, an error is thrown if both sides set also MID and the MIDs are not equal.
+#
+# @relation(SDOC-SRS-141, scope=file)
+#
+
+RUN: %expect_exit 1 %strictdoc export %S --output-dir %T | filecheck %s --dump-input=fail
+CHECK: error: Can't merge node by UID REQ-SOURCE-1: Conflicting MID: 3bd0162d-d6d2-42a1-9324-ab8415190970 != e7c09b63-2bed-4046-9511-a5b17978f152
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_validation/04_merge_by_mid_conflicting_uid/source_node_base.sdoc b/tests/integration/features/source_code_traceability/_source_nodes/_validation/04_merge_by_mid_conflicting_uid/source_node_base.sdoc
new file mode 100644
index 000000000..e00351a94
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/_validation/04_merge_by_mid_conflicting_uid/source_node_base.sdoc
@@ -0,0 +1,48 @@
+[DOCUMENT]
+MID: c2d4542d5f1741c88dfcb4f68ad7dcbd
+TITLE: Requirements from Source Nodes
+UID: SRC-NODES-BASE
+
+[GRAMMAR]
+ELEMENTS:
+- TAG: SECTION
+ PROPERTIES:
+ IS_COMPOSITE: True
+ FIELDS:
+ - TITLE: UID
+ TYPE: String
+ REQUIRED: False
+ - TITLE: TITLE
+ TYPE: String
+ REQUIRED: True
+- TAG: REQUIREMENT
+ PROPERTIES:
+ VIEW_STYLE: Narrative
+ FIELDS:
+ - TITLE: UID
+ TYPE: String
+ REQUIRED: False
+ - TITLE: MID
+ TYPE: String
+ REQUIRED: False
+ - TITLE: TITLE
+ TYPE: String
+ REQUIRED: False
+ - TITLE: FOO
+ TYPE: String
+ REQUIRED: False
+ - TITLE: BAR
+ TYPE: String
+ REQUIRED: False
+
+[[SECTION]]
+TITLE: Merge example.c into static nodes
+
+[REQUIREMENT]
+UID: REQ-SOURCE-1
+MID: 3bd0162d-d6d2-42a1-9324-ab8415190970
+TITLE: TITLE from sdoc
+FOO: FOO text from sdoc
+BAR: BAR text from sdoc
+
+[[/SECTION]]
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_validation/04_merge_by_mid_conflicting_uid/src/example.c b/tests/integration/features/source_code_traceability/_source_nodes/_validation/04_merge_by_mid_conflicting_uid/src/example.c
new file mode 100644
index 000000000..02a01bd76
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/_validation/04_merge_by_mid_conflicting_uid/src/example.c
@@ -0,0 +1,14 @@
+#include
+
+/**
+ * UID: REQ-SOURCE-2
+ *
+ * MID: 3bd0162d-d6d2-42a1-9324-ab8415190970
+ *
+ * FOO: FOO text from example.c
+ *
+ * BAR: BAR text from example.c
+ */
+void example_1(void) {
+ print("hello world\n");
+}
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_validation/04_merge_by_mid_conflicting_uid/strictdoc.toml b/tests/integration/features/source_code_traceability/_source_nodes/_validation/04_merge_by_mid_conflicting_uid/strictdoc.toml
new file mode 100644
index 000000000..f8688a0cd
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/_validation/04_merge_by_mid_conflicting_uid/strictdoc.toml
@@ -0,0 +1,14 @@
+[project]
+
+features = [
+ "REQUIREMENT_TO_SOURCE_TRACEABILITY",
+ "SOURCE_FILE_LANGUAGE_PARSERS",
+]
+
+source_nodes = [
+ { "src/" = { uid = "SRC-NODES-BASE", node_type = "REQUIREMENT" } }
+]
+
+exclude_source_paths = [
+ "test.itest"
+]
diff --git a/tests/integration/features/source_code_traceability/_source_nodes/_validation/04_merge_by_mid_conflicting_uid/test.itest b/tests/integration/features/source_code_traceability/_source_nodes/_validation/04_merge_by_mid_conflicting_uid/test.itest
new file mode 100644
index 000000000..48cb05075
--- /dev/null
+++ b/tests/integration/features/source_code_traceability/_source_nodes/_validation/04_merge_by_mid_conflicting_uid/test.itest
@@ -0,0 +1,9 @@
+#
+# This test verifies that if SDoc node and source node have same MID and thus are
+# subject to be merged, an error is thrown if both sides set also UID and the UID are not equal.
+#
+# @relation(SDOC-SRS-141, scope=file)
+#
+
+RUN: %expect_exit 1 %strictdoc export %S --output-dir %T | filecheck %s --dump-input=fail
+CHECK: error: Can't merge node by MID 3bd0162d-d6d2-42a1-9324-ab8415190970: Conflicting UID: REQ-SOURCE-1 != REQ-SOURCE-2