diff --git a/scripts/snippets/model.py b/scripts/snippets/model.py
index 7ab2b3608..b49f6dad9 100644
--- a/scripts/snippets/model.py
+++ b/scripts/snippets/model.py
@@ -227,3 +227,20 @@ class ConditionPageValidation:
tags: tuple[IfVersionTag | ElseTag, ...]
conditions: tuple[IfVersionCondition, ...]
diagnostics: tuple[Diagnostic, ...]
+
+
+@dataclass(frozen=True)
+class ValidatedSnippet:
+ tag: SnippetTag
+ source: (
+ ImmutableSourceReference
+ | PullRequestSnippetSource
+ | LocalSourceReference
+ )
+ condition: IfVersionCondition | None
+
+
+@dataclass(frozen=True)
+class SnippetPageValidation:
+ snippets: tuple[ValidatedSnippet, ...]
+ diagnostics: tuple[Diagnostic, ...]
diff --git a/scripts/snippets/page_validation.py b/scripts/snippets/page_validation.py
index 10afe3aec..e98cd73c8 100644
--- a/scripts/snippets/page_validation.py
+++ b/scripts/snippets/page_validation.py
@@ -3,16 +3,38 @@
from pathlib import Path
from .diagnostics import (
+ candidate_condition_diagnostics,
directive_syntax_diagnostic,
if_version_attribute_diagnostics,
if_version_structure_diagnostics,
+ local_source_policy_diagnostic,
+ snippet_attribute_diagnostics,
+ snippet_source_attribute_diagnostics,
+ snippet_source_safety_diagnostics,
+)
+from .model import (
+ ConditionPageValidation,
+ IfVersionTag,
+ SnippetPageValidation,
+ ValidatedSnippet,
)
-from .model import ConditionPageValidation, IfVersionTag
from .registry import RepositoryRegistry
-from .syntax import DirectiveSyntaxError, parse_if_version_tags
+from .semantics import (
+ map_snippet_condition_contexts,
+ resolve_snippet_source_attributes,
+ validate_candidate_condition,
+ validate_snippet_basic_attributes,
+)
+from .syntax import (
+ DirectiveSyntaxError,
+ parse_if_version_tags,
+ parse_snippet_tags,
+)
from .validation import (
validate_if_version_attributes,
validate_if_version_structure,
+ validate_local_source_policy,
+ validate_snippet_source_safety,
)
@@ -48,3 +70,73 @@ def validate_condition_page(
conditions=tuple(conditions),
diagnostics=tuple(diagnostics),
)
+
+
+def validate_snippet_page(
+ text: str,
+ *,
+ path: Path,
+ registry: RepositoryRegistry,
+ conditions: ConditionPageValidation,
+ allow_local: bool = False,
+) -> SnippetPageValidation:
+ """Parse and validate snippets against prevalidated condition context."""
+
+ try:
+ tags = parse_snippet_tags(text)
+ except DirectiveSyntaxError as error:
+ return SnippetPageValidation(
+ snippets=(),
+ diagnostics=(directive_syntax_diagnostic(path, error),),
+ )
+
+ diagnostics = []
+ snippets = []
+ contexts = map_snippet_condition_contexts(
+ tags, conditions.tags, conditions.conditions
+ )
+ for context in contexts:
+ tag = context.snippet
+ tag_diagnostics = list(
+ snippet_attribute_diagnostics(
+ path, validate_snippet_basic_attributes(tag)
+ )
+ )
+ source_result = resolve_snippet_source_attributes(tag)
+ tag_diagnostics.extend(
+ snippet_source_attribute_diagnostics(path, source_result.issues)
+ )
+ source = source_result.source
+ if source is not None:
+ tag_diagnostics.extend(
+ snippet_source_safety_diagnostics(
+ path,
+ validate_snippet_source_safety(
+ source, span=tag.span, registry=registry
+ ),
+ )
+ )
+ local_issues = validate_local_source_policy(
+ source, span=tag.span, allow_local=allow_local
+ )
+ tag_diagnostics.extend(
+ local_source_policy_diagnostic(path, issue)
+ for issue in local_issues
+ )
+ tag_diagnostics.extend(
+ candidate_condition_diagnostics(
+ path, validate_candidate_condition(source, context)
+ )
+ )
+ diagnostics.extend(tag_diagnostics)
+ if source is not None and not tag_diagnostics:
+ snippets.append(
+ ValidatedSnippet(
+ tag=tag,
+ source=source,
+ condition=context.condition,
+ )
+ )
+ return SnippetPageValidation(
+ snippets=tuple(snippets), diagnostics=tuple(diagnostics)
+ )
diff --git a/tests/test_snippet_page_validation.py b/tests/test_snippet_page_validation.py
new file mode 100644
index 000000000..34042f5d6
--- /dev/null
+++ b/tests/test_snippet_page_validation.py
@@ -0,0 +1,98 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from scripts.snippets.model import SnippetPageValidation
+from scripts.snippets.page_validation import (
+ validate_condition_page,
+ validate_snippet_page,
+)
+from scripts.snippets.registry import (
+ RepositoryConfig,
+ RepositoryRegistry,
+ RepositoryVisibility,
+)
+
+PATH = Path("docs-main/validator.source.mdx")
+REGISTRY = RepositoryRegistry(
+ (
+ RepositoryConfig(
+ name="canton-network/splice",
+ url="https://github.com/canton-network/splice",
+ default_branch="main",
+ visibility=RepositoryVisibility.PUBLIC,
+ ),
+ )
+)
+COMMIT = "2c941ea9e834d7602d388f3271c0f864025ea756"
+
+
+def validate(
+ text: str, *, allow_local: bool = False
+) -> SnippetPageValidation:
+ conditions = validate_condition_page(text, path=PATH, registry=REGISTRY)
+ return validate_snippet_page(
+ text,
+ path=PATH,
+ registry=REGISTRY,
+ conditions=conditions,
+ allow_local=allow_local,
+ )
+
+
+def test_returns_validated_immutable_snippet() -> None:
+ result = validate(
+ f''
+ )
+
+ assert result.diagnostics == ()
+ assert len(result.snippets) == 1
+ assert result.snippets[0].condition is None
+
+
+def test_returns_candidate_matching_condition() -> None:
+ result = validate(
+ """
+
+"""
+ )
+
+ assert result.diagnostics == ()
+ assert result.snippets[0].condition is not None
+
+
+def test_reports_candidate_identity_mismatch() -> None:
+ result = validate(
+ """
+
+"""
+ )
+
+ assert "SNIP028" in {
+ diagnostic.code for diagnostic in result.diagnostics
+ }
+
+
+def test_reports_local_ref_remediation_unless_preview_enabled() -> None:
+ text = (
+ ''
+ )
+
+ committed = validate(text)
+ preview = validate(text, allow_local=True)
+
+ assert committed.diagnostics[0].code == "SNIP007"
+ remediation = committed.diagnostics[0].remediation
+ assert remediation is not None
+ assert "snippets:resolve-local" in remediation
+ assert preview.diagnostics == ()
+
+
+def test_reports_snippet_syntax_error() -> None:
+ result = validate('')
+
+ assert [diagnostic.code for diagnostic in result.diagnostics] == [
+ "SNIP014"
+ ]