feat: add tenant semantic documentation lifecycle
This commit is contained in:
@@ -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",))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user