feat: add tenant semantic documentation lifecycle

This commit is contained in:
2026-08-21 15:37:01 +02:00
parent 77eb7339e6
commit d6db344d81
22 changed files with 3717 additions and 8 deletions
+147
View File
@@ -32,12 +32,19 @@ from govoplan_core.core.versioning import (
from govoplan_core.db.session import get_database
from govoplan_docs.backend.manifest import DOCS_ADMIN_READ_SCOPES, DOCS_READ_SCOPES
from govoplan_docs.backend.api.v1.semantic_routes import router as semantic_router
from govoplan_docs.backend.sources import (
RegisteredDocumentationSource,
build_documentation_source_registry,
)
from govoplan_docs.backend.semantic_service import (
list_semantic_entries,
select_locale_entries,
semantic_entry_payload,
)
router = APIRouter(prefix="/docs", tags=["docs"])
router.include_router(semantic_router)
TOPIC_KINDS = ("workflow", "reference", "pattern", "system")
_CONFIGURATION_ACTIVE_STATES = frozenset({"enabled", "inherited"})
@@ -958,9 +965,149 @@ def _collect_documentation_topics(
for topic in provided_topics:
if not user_workflow_scope_condition_issues(topic):
topics.append((manifest.id, topic))
if session is not None:
try:
topics.extend(
("docs", topic)
for topic in _semantic_documentation_topics(
registry,
principal,
session=session,
locale=locale,
)
)
except Exception as exc:
if documentation_type == "admin":
topics.append(
(
"docs",
DocumentationTopic(
id="docs.semantic-runtime-unavailable",
title="Tenant semantic documentation unavailable",
summary="Stored tenant semantics could not be projected for this request.",
body="Static module documentation remains available. Check the Docs database migration, subject providers, and application logs.",
layer="evidence",
documentation_types=("admin",),
source_module_id="docs",
metadata={
"kind": "system",
"error_type": type(exc).__name__,
},
),
)
)
return topics
def _semantic_documentation_topics(
registry: PlatformRegistry,
principal: ApiPrincipal,
*,
session: object,
locale: str,
) -> tuple[DocumentationTopic, ...]:
entries = select_locale_entries(
list_semantic_entries(session, principal),
locale=locale,
)
topics: list[DocumentationTopic] = []
for entry in entries:
payload = semantic_entry_payload(
session,
registry,
principal,
entry=entry,
editor=False,
requested_locale=locale,
)
if payload is None:
continue
content = payload["content"]
resolution = payload["subject_resolution"]
subject = resolution.get("subject") if isinstance(resolution, Mapping) else None
route = subject.get("route") if isinstance(subject, Mapping) else None
route_anchor = (
subject.get("route_anchor") if isinstance(subject, Mapping) else None
)
links = [
DocumentationLink(
label=str(item["label"]),
href=str(item["href"]),
kind="runtime" if str(item["href"]).startswith("/") else "external",
)
for item in content.get("links", ())
if isinstance(item, Mapping) and item.get("label") and item.get("href")
]
if isinstance(route, str) and route.startswith("/"):
links.insert(
0,
DocumentationLink(
label="Open configured subject",
href=(f"{route}#{route_anchor}" if route_anchor else route),
kind="runtime",
),
)
body_parts = [
str(content.get(key) or "").strip()
for key in (
"meaning",
"body",
"intended_use",
"non_intended_use",
)
]
topics.append(
DocumentationTopic(
id=f"docs.semantic.{entry.id}",
title=str(content.get("title") or "Semantic documentation"),
summary=str(content.get("summary") or "Tenant semantic guidance"),
body="\n\n".join(part for part in body_parts if part),
layer="configured",
documentation_types=("admin", "user"),
source_module_id="docs",
order=200,
links=tuple(links),
related_modules=(entry.subject_module_id,),
metadata={
"kind": "reference",
"source_badge": "tenant_semantic",
"semantic_entry_id": entry.id,
"semantic_subject": payload["subject"],
"semantic_subject_stable_key": entry.subject_stable_key,
"subject_availability": resolution.get("availability"),
"locale": payload["locale"],
"requested_locale": payload["requested_locale"],
"locale_fallback": payload["locale_fallback"],
"lifecycle_state": payload["lifecycle_state"],
"pending_draft": payload["pending_draft"],
"route": route,
"route_anchor": route_anchor,
"help_contexts": [
_semantic_help_context(entry),
],
},
)
)
return tuple(topics)
def _semantic_help_context(entry: object) -> str:
values = [
"semantic",
str(getattr(entry, "subject_module_id")),
str(getattr(entry, "subject_kind")),
str(getattr(entry, "subject_id")),
]
if getattr(entry, "anchor_kind", None) and getattr(entry, "anchor_id", None):
values.extend(
(
str(getattr(entry, "anchor_kind")),
str(getattr(entry, "anchor_id")),
)
)
return ".".join(values)
def _topic_matches_documentation_type(topic: DocumentationTopic, documentation_type: DocumentationType) -> bool:
return documentation_type in (topic.documentation_types or ("admin",))