feat(docs): define semantic subject contract

This commit is contained in:
2026-08-21 14:55:58 +02:00
parent 4b0737e1cd
commit 925dc33696
6 changed files with 1176 additions and 0 deletions
@@ -0,0 +1,378 @@
from __future__ import annotations
import unittest
from types import SimpleNamespace
from govoplan_core.core.modules import (
CapabilityDocumentation,
DocumentationTopic,
ModuleContext,
ModuleManifest,
)
from govoplan_core.core.registry import PlatformRegistry, RegistryError
from govoplan_core.core.semantic_documentation import (
SEMANTIC_DOCUMENTATION_SUBJECT_CONTRACT_VERSION,
SemanticDocumentationBreadcrumb,
SemanticDocumentationContractError,
SemanticDocumentationSubjectAnchor,
SemanticDocumentationSubjectDescriptor,
SemanticDocumentationSubjectPage,
SemanticDocumentationSubjectQuery,
SemanticDocumentationSubjectReference,
SemanticDocumentationSubjectResolution,
list_semantic_documentation_subjects,
resolve_semantic_documentation_subject,
semantic_documentation_fingerprint,
semantic_documentation_subject_capability,
semantic_documentation_subject_provider_names,
semantic_documentation_subject_providers,
)
def reference(
*,
tenant_id: str = "tenant-1",
revision: str | None = "4",
fingerprint: str | None = None,
anchor: SemanticDocumentationSubjectAnchor | None = None,
) -> SemanticDocumentationSubjectReference:
return SemanticDocumentationSubjectReference(
module_id="forms",
tenant_id=tenant_id,
subject_kind="form",
subject_id="permit-application",
anchor=anchor,
observed_revision=revision,
observed_fingerprint=fingerprint
or semantic_documentation_fingerprint({"revision": revision}),
)
def descriptor(
*,
tenant_id: str = "tenant-1",
revision: str = "4",
fingerprint: str | None = None,
label: str = "Permit application",
anchor: SemanticDocumentationSubjectAnchor | None = None,
) -> SemanticDocumentationSubjectDescriptor:
return SemanticDocumentationSubjectDescriptor(
reference=reference(
tenant_id=tenant_id,
revision=revision,
fingerprint=fingerprint,
anchor=anchor,
),
labels={"de": "Antrag auf Parkerlaubnis", "en": label},
descriptions={"de": "Konfiguriertes Antragsformular."},
breadcrumbs=(
SemanticDocumentationBreadcrumb(
label="Forms",
subject_kind="form",
subject_id="permit-application",
),
),
route="/forms/permit-application",
route_anchor=anchor.id if anchor else None,
audience=("case_worker",),
classification="internal",
required_scopes=("forms:definition:read",),
)
class Provider:
provider_id = "forms.semantic_subjects"
module_id = "forms"
contract_version = SEMANTIC_DOCUMENTATION_SUBJECT_CONTRACT_VERSION
def __init__(self) -> None:
self.list_calls = 0
self.resolve_calls = 0
def list_subjects(self, _session, principal, *, request):
self.list_calls += 1
if getattr(principal, "account_id", "") == "denied":
return SemanticDocumentationSubjectPage()
return SemanticDocumentationSubjectPage(subjects=(descriptor(),))
def resolve_subject(self, _session, principal, *, reference):
self.resolve_calls += 1
if getattr(principal, "account_id", "") == "denied":
return None
current = descriptor()
availability = (
"changed"
if reference.observed_fingerprint
!= current.reference.observed_fingerprint
else "available"
)
return SemanticDocumentationSubjectResolution(
requested_reference=reference,
availability=availability,
subject=current,
)
class SemanticDocumentationContractTests(unittest.TestCase):
def test_reference_identity_is_stable_across_labels_and_revisions(self) -> None:
original = descriptor(label="Old label")
renamed = descriptor(revision="5", label="New label")
self.assertEqual(
original.reference.stable_key,
renamed.reference.stable_key,
)
self.assertNotEqual(
original.reference.observed_fingerprint,
renamed.reference.observed_fingerprint,
)
def test_nested_anchor_is_typed_and_changes_subject_identity(self) -> None:
form = reference()
field = reference(
anchor=SemanticDocumentationSubjectAnchor(
kind="field",
id="vehicle-registration",
)
)
self.assertNotEqual(form.stable_key, field.stable_key)
restored = SemanticDocumentationSubjectReference.from_mapping(
field.to_dict()
)
self.assertEqual(restored, field)
def test_reference_mapping_rejects_unbounded_provider_metadata(self) -> None:
payload = reference().to_dict()
payload["credentials"] = {"token": "must-not-cross"}
with self.assertRaisesRegex(
SemanticDocumentationContractError,
"unsupported fields: credentials",
):
SemanticDocumentationSubjectReference.from_mapping(payload)
def test_descriptors_reject_remote_routes_and_missing_localization(self) -> None:
with self.assertRaisesRegex(
SemanticDocumentationContractError,
"local absolute paths",
):
SemanticDocumentationSubjectDescriptor(
reference=reference(),
labels={"de": "Formular"},
route="https://provider.example/form",
)
with self.assertRaisesRegex(
SemanticDocumentationContractError,
"labels are required",
):
SemanticDocumentationSubjectDescriptor(
reference=reference(),
labels={},
)
def test_resolution_distinguishes_change_supersession_and_absence(self) -> None:
old = reference(
revision="3",
fingerprint=semantic_documentation_fingerprint({"revision": "3"}),
)
current = descriptor()
changed = SemanticDocumentationSubjectResolution(
requested_reference=old,
availability="changed",
subject=current,
)
superseded = SemanticDocumentationSubjectResolution(
requested_reference=old,
availability="superseded",
superseded_by=SemanticDocumentationSubjectReference(
module_id="forms",
tenant_id="tenant-1",
subject_kind="form",
subject_id="permit-application-v2",
),
)
missing = SemanticDocumentationSubjectResolution(
requested_reference=old,
availability="missing",
reason_code="subject_removed",
)
self.assertEqual(changed.availability, "changed")
self.assertEqual(superseded.superseded_by.subject_id, "permit-application-v2")
self.assertEqual(missing.reason_code, "subject_removed")
with self.assertRaisesRegex(
SemanticDocumentationContractError,
"require a revision or fingerprint change",
):
SemanticDocumentationSubjectResolution(
requested_reference=current.reference,
availability="changed",
subject=current,
)
def test_subject_pages_reject_duplicate_stable_identities(self) -> None:
with self.assertRaisesRegex(
SemanticDocumentationContractError,
"duplicate identities",
):
SemanticDocumentationSubjectPage(
subjects=(descriptor(), descriptor(label="Renamed")),
)
def test_registry_discovers_optional_providers_without_direct_imports(self) -> None:
provider = Provider()
registry = provider_registry(provider)
self.assertEqual(
semantic_documentation_subject_provider_names(registry),
("documentation.semantic_subjects.forms",),
)
self.assertEqual(
semantic_documentation_subject_providers(registry),
(("forms", provider),),
)
def test_listing_and_resolution_are_tenant_and_principal_aware(self) -> None:
provider = Provider()
registry = provider_registry(provider)
allowed = SimpleNamespace(tenant_id="tenant-1", account_id="account-1")
wrong_tenant = SimpleNamespace(
tenant_id="tenant-2", account_id="account-1"
)
denied = SimpleNamespace(tenant_id="tenant-1", account_id="denied")
pages = list_semantic_documentation_subjects(
registry,
object(),
allowed,
request=SemanticDocumentationSubjectQuery(tenant_id="tenant-1"),
)
self.assertEqual(len(pages), 1)
self.assertEqual(pages[0][1].subjects[0].reference.tenant_id, "tenant-1")
self.assertEqual(
list_semantic_documentation_subjects(
registry,
object(),
wrong_tenant,
request=SemanticDocumentationSubjectQuery(tenant_id="tenant-1"),
),
(),
)
self.assertIsNone(
resolve_semantic_documentation_subject(
registry,
object(),
denied,
reference=reference(),
)
)
self.assertEqual(provider.list_calls, 1)
self.assertEqual(provider.resolve_calls, 1)
def test_missing_optional_module_resolves_without_loading_feature_code(self) -> None:
principal = SimpleNamespace(tenant_id="tenant-1", account_id="account-1")
result = resolve_semantic_documentation_subject(
PlatformRegistry(),
object(),
principal,
reference=reference(),
)
self.assertEqual(result.availability, "temporarily_unavailable")
self.assertEqual(result.reason_code, "provider_unavailable")
def test_manifest_requires_exact_capability_docs_and_static_baselines(self) -> None:
provider = Provider()
capability = semantic_documentation_subject_capability("forms")
missing_baseline = PlatformRegistry()
missing_baseline.register(
ModuleManifest(
id="forms",
name="Forms",
version="1.0.0",
capability_factories={capability: lambda _context: provider},
capability_documentation={
capability: CapabilityDocumentation(
label="Form semantics",
summary="Lists documentation-safe configured form subjects.",
contract_version=(
SEMANTIC_DOCUMENTATION_SUBJECT_CONTRACT_VERSION
),
)
},
documentation=(static_topic("admin"),),
)
)
with self.assertRaisesRegex(RegistryError, "static user and administrator"):
missing_baseline.validate()
wrong_capability = PlatformRegistry()
wrong_capability.register(
ModuleManifest(
id="forms",
name="Forms",
version="1.0.0",
capability_factories={
semantic_documentation_subject_capability("workflow"): (
lambda _context: provider
)
},
)
)
with self.assertRaisesRegex(RegistryError, "must be"):
wrong_capability.validate()
def test_fingerprint_is_canonical_and_rejects_non_json_values(self) -> None:
self.assertEqual(
semantic_documentation_fingerprint({"b": 2, "a": 1}),
semantic_documentation_fingerprint({"a": 1, "b": 2}),
)
with self.assertRaisesRegex(
SemanticDocumentationContractError,
"canonical JSON",
):
semantic_documentation_fingerprint({"unsafe": object()})
def static_topic(documentation_type: str) -> DocumentationTopic:
return DocumentationTopic(
id=f"forms.semantic.{documentation_type}",
title="Form semantics",
summary="Static form semantics baseline.",
documentation_types=(documentation_type,), # type: ignore[arg-type]
)
def provider_registry(provider: Provider) -> PlatformRegistry:
capability = semantic_documentation_subject_capability("forms")
registry = PlatformRegistry()
registry.register(
ModuleManifest(
id="forms",
name="Forms",
version="1.0.0",
capability_factories={capability: lambda _context: provider},
capability_documentation={
capability: CapabilityDocumentation(
label="Form semantic subjects",
summary="Lists safe configured forms and fields for Docs.",
contract_version=(
SEMANTIC_DOCUMENTATION_SUBJECT_CONTRACT_VERSION
),
documentation_types=("admin", "user"),
)
},
documentation=(static_topic("admin"), static_topic("user")),
)
)
registry.validate()
registry.configure_capability_context(
ModuleContext(registry=registry, settings=object())
)
return registry
if __name__ == "__main__":
unittest.main()