feat: expose capability and catalog documentation
This commit is contained in:
@@ -55,6 +55,30 @@ Frontend package:
|
|||||||
|
|
||||||
Platform module manifests, configuration packages, release catalogs, and governance rules are documented in `govoplan-core/docs/`.
|
Platform module manifests, configuration packages, release catalogs, and governance rules are documented in `govoplan-core/docs/`.
|
||||||
|
|
||||||
|
Capabilities can provide generic documentation without exposing their runtime
|
||||||
|
provider implementation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ModuleManifest(
|
||||||
|
capability_factories={"example.lookup": build_lookup},
|
||||||
|
capability_documentation={
|
||||||
|
"example.lookup": CapabilityDocumentation(
|
||||||
|
label="Example lookup",
|
||||||
|
summary="Resolves records through the versioned lookup contract.",
|
||||||
|
contract_version="2",
|
||||||
|
stability="stable",
|
||||||
|
audience=("module_admin",),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Docs also projects the configured module release catalog and configuration
|
||||||
|
package catalog through the public Core catalog contracts. Catalog trust,
|
||||||
|
freshness, provenance, descriptions, and package requirements remain visible
|
||||||
|
as typed evidence; provider objects, credentials, and secret configuration are
|
||||||
|
never imported into the Docs WebUI.
|
||||||
|
|
||||||
## Concept documents
|
## Concept documents
|
||||||
|
|
||||||
- `docs/DOCUMENTATION_LAYER_CONCEPT.md` defines the configured, available, and evidence documentation model.
|
- `docs/DOCUMENTATION_LAYER_CONCEPT.md` defines the configured, available, and evidence documentation model.
|
||||||
|
|||||||
@@ -195,23 +195,6 @@ manifest = ModuleManifest(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
documentation_sources=(
|
documentation_sources=(
|
||||||
DocumentationSourceDefinition(
|
|
||||||
id="docs.configuration.packages",
|
|
||||||
kind="configuration_package",
|
|
||||||
label="Configuration package catalog",
|
|
||||||
condition=DocumentationCondition(
|
|
||||||
any_scopes=("system:settings:read", "admin:settings:read"),
|
|
||||||
),
|
|
||||||
provenance={
|
|
||||||
"source": "core_configuration_package_contract",
|
|
||||||
"version": "1",
|
|
||||||
},
|
|
||||||
inspection={
|
|
||||||
"package_id": "govoplan.configuration-packages",
|
|
||||||
"schema_version": "1",
|
|
||||||
"description": "Signed, preflighted platform configuration packages.",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
DocumentationSourceDefinition(
|
DocumentationSourceDefinition(
|
||||||
id="docs.project.wiki",
|
id="docs.project.wiki",
|
||||||
kind="wiki",
|
kind="wiki",
|
||||||
|
|||||||
@@ -58,18 +58,58 @@ class RouteInspection(_SourceModel):
|
|||||||
class CapabilityInspection(_SourceModel):
|
class CapabilityInspection(_SourceModel):
|
||||||
kind: Literal["capability"] = "capability"
|
kind: Literal["capability"] = "capability"
|
||||||
capability: str
|
capability: str
|
||||||
|
label: str | None = None
|
||||||
|
summary: str | None = None
|
||||||
|
contract_version: str | None = None
|
||||||
|
stability: Literal["experimental", "stable", "deprecated"] | None = None
|
||||||
|
audience: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class PolicyInspection(_SourceModel):
|
class PolicyInspection(_SourceModel):
|
||||||
kind: Literal["policy"] = "policy"
|
kind: Literal["policy"] = "policy"
|
||||||
capability: str
|
capability: str
|
||||||
|
label: str | None = None
|
||||||
|
summary: str | None = None
|
||||||
|
contract_version: str | None = None
|
||||||
|
stability: Literal["experimental", "stable", "deprecated"] | None = None
|
||||||
|
audience: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseCatalogEntryInspection(_SourceModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
version: str | None = None
|
||||||
|
description: str | None = None
|
||||||
|
action: str | None = None
|
||||||
|
tags: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseCatalogInspection(_SourceModel):
|
||||||
|
kind: Literal["release_catalog"] = "release_catalog"
|
||||||
|
catalog_type: Literal["modules", "configuration_packages"]
|
||||||
|
channel: str | None = None
|
||||||
|
sequence: int | None = None
|
||||||
|
generated_at: str | None = None
|
||||||
|
entry_count: int
|
||||||
|
signed: bool
|
||||||
|
trusted: bool
|
||||||
|
cache_used: bool
|
||||||
|
warnings: list[str] = Field(default_factory=list)
|
||||||
|
entries: list[ReleaseCatalogEntryInspection] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationPackageInspection(_SourceModel):
|
class ConfigurationPackageInspection(_SourceModel):
|
||||||
kind: Literal["configuration_package"] = "configuration_package"
|
kind: Literal["configuration_package"] = "configuration_package"
|
||||||
package_id: str
|
package_id: str
|
||||||
|
name: str | None = None
|
||||||
|
version: str | None = None
|
||||||
schema_version: str | None = None
|
schema_version: str | None = None
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
|
publisher: str | None = None
|
||||||
|
category: str | None = None
|
||||||
|
tags: list[str] = Field(default_factory=list)
|
||||||
|
required_modules: list[str] = Field(default_factory=list)
|
||||||
|
required_capabilities: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class WikiInspection(_SourceModel):
|
class WikiInspection(_SourceModel):
|
||||||
@@ -90,6 +130,7 @@ DocumentationSourceInspection = Annotated[
|
|||||||
| RouteInspection
|
| RouteInspection
|
||||||
| CapabilityInspection
|
| CapabilityInspection
|
||||||
| PolicyInspection
|
| PolicyInspection
|
||||||
|
| ReleaseCatalogInspection
|
||||||
| ConfigurationPackageInspection
|
| ConfigurationPackageInspection
|
||||||
| WikiInspection
|
| WikiInspection
|
||||||
| RepositoryInspection,
|
| RepositoryInspection,
|
||||||
@@ -142,6 +183,8 @@ class DocumentationSourceRegistry:
|
|||||||
|
|
||||||
def build_documentation_source_registry(
|
def build_documentation_source_registry(
|
||||||
manifests: tuple[ModuleManifest, ...],
|
manifests: tuple[ModuleManifest, ...],
|
||||||
|
*,
|
||||||
|
include_runtime_catalogs: bool = True,
|
||||||
) -> DocumentationSourceRegistry:
|
) -> DocumentationSourceRegistry:
|
||||||
registry = DocumentationSourceRegistry()
|
registry = DocumentationSourceRegistry()
|
||||||
for manifest in manifests:
|
for manifest in manifests:
|
||||||
@@ -155,6 +198,9 @@ def build_documentation_source_registry(
|
|||||||
for source in _linked_sources(manifest):
|
for source in _linked_sources(manifest):
|
||||||
if registry.get(source.item.id) is None:
|
if registry.get(source.item.id) is None:
|
||||||
registry.register(source)
|
registry.register(source)
|
||||||
|
if include_runtime_catalogs:
|
||||||
|
for source in _catalog_sources():
|
||||||
|
registry.register(source)
|
||||||
return registry
|
return registry
|
||||||
|
|
||||||
|
|
||||||
@@ -203,21 +249,37 @@ def _route_sources(manifest: ModuleManifest) -> tuple[RegisteredDocumentationSou
|
|||||||
def _capability_sources(manifest: ModuleManifest) -> tuple[RegisteredDocumentationSource, ...]:
|
def _capability_sources(manifest: ModuleManifest) -> tuple[RegisteredDocumentationSource, ...]:
|
||||||
sources: list[RegisteredDocumentationSource] = []
|
sources: list[RegisteredDocumentationSource] = []
|
||||||
for capability in sorted(manifest.capability_factories):
|
for capability in sorted(manifest.capability_factories):
|
||||||
|
metadata = manifest.capability_documentation.get(capability)
|
||||||
is_policy = capability.startswith("policy.")
|
is_policy = capability.startswith("policy.")
|
||||||
kind = "policy" if is_policy else "capability"
|
kind = "policy" if is_policy else "capability"
|
||||||
inspection: DocumentationSourceInspection = (
|
inspection: DocumentationSourceInspection = (
|
||||||
PolicyInspection(capability=capability)
|
PolicyInspection(
|
||||||
|
capability=capability,
|
||||||
|
label=metadata.label if metadata else None,
|
||||||
|
summary=metadata.summary if metadata else None,
|
||||||
|
contract_version=metadata.contract_version if metadata else None,
|
||||||
|
stability=metadata.stability if metadata else None,
|
||||||
|
audience=list(metadata.audience) if metadata else [],
|
||||||
|
)
|
||||||
if is_policy
|
if is_policy
|
||||||
else CapabilityInspection(capability=capability)
|
else CapabilityInspection(
|
||||||
|
capability=capability,
|
||||||
|
label=metadata.label if metadata else None,
|
||||||
|
summary=metadata.summary if metadata else None,
|
||||||
|
contract_version=metadata.contract_version if metadata else None,
|
||||||
|
stability=metadata.stability if metadata else None,
|
||||||
|
audience=list(metadata.audience) if metadata else [],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
sources.append(_registered_source(
|
sources.append(_registered_source(
|
||||||
source_id=_derived_source_id(manifest.id, kind, capability),
|
source_id=_derived_source_id(manifest.id, kind, capability),
|
||||||
kind=kind,
|
kind=kind,
|
||||||
owner_module_id=manifest.id,
|
owner_module_id=manifest.id,
|
||||||
label=f"{manifest.name} {kind} {capability}",
|
label=metadata.label if metadata else f"{manifest.name} {kind} {capability}",
|
||||||
provenance={"source": "module_manifest", "version": manifest.version},
|
provenance={"source": "module_manifest", "version": manifest.version},
|
||||||
inspection=inspection,
|
inspection=inspection,
|
||||||
condition=DocumentationCondition(required_modules=(manifest.id,)),
|
condition=DocumentationCondition(required_modules=(manifest.id,)),
|
||||||
|
documentation_types=metadata.documentation_types if metadata else ("admin",),
|
||||||
))
|
))
|
||||||
return tuple(sources)
|
return tuple(sources)
|
||||||
|
|
||||||
@@ -240,6 +302,7 @@ def _defined_source(
|
|||||||
condition=definition.condition,
|
condition=definition.condition,
|
||||||
documentation_types=definition.documentation_types,
|
documentation_types=definition.documentation_types,
|
||||||
state=definition.state,
|
state=definition.state,
|
||||||
|
state_reason=definition.state_reason,
|
||||||
configuration_key=definition.configuration_key,
|
configuration_key=definition.configuration_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -249,8 +312,15 @@ def _defined_inspection(definition: DocumentationSourceDefinition) -> Documentat
|
|||||||
if definition.kind == "configuration_package":
|
if definition.kind == "configuration_package":
|
||||||
return ConfigurationPackageInspection(
|
return ConfigurationPackageInspection(
|
||||||
package_id=str(safe.get("package_id") or definition.id),
|
package_id=str(safe.get("package_id") or definition.id),
|
||||||
|
name=_optional_text(safe.get("name")),
|
||||||
|
version=_optional_text(safe.get("version")),
|
||||||
schema_version=_optional_text(safe.get("schema_version")),
|
schema_version=_optional_text(safe.get("schema_version")),
|
||||||
description=_optional_text(safe.get("description")),
|
description=_optional_text(safe.get("description")),
|
||||||
|
publisher=_optional_text(safe.get("publisher")),
|
||||||
|
category=_optional_text(safe.get("category")),
|
||||||
|
tags=_string_list(safe.get("tags")),
|
||||||
|
required_modules=_string_list(safe.get("required_modules")),
|
||||||
|
required_capabilities=_string_list(safe.get("required_capabilities")),
|
||||||
)
|
)
|
||||||
href = definition.link.href if definition.link else str(safe.get("href") or "")
|
href = definition.link.href if definition.link else str(safe.get("href") or "")
|
||||||
if definition.kind == "wiki":
|
if definition.kind == "wiki":
|
||||||
@@ -271,9 +341,23 @@ def _defined_inspection(definition: DocumentationSourceDefinition) -> Documentat
|
|||||||
order=int(safe.get("order") or 0),
|
order=int(safe.get("order") or 0),
|
||||||
)
|
)
|
||||||
if definition.kind == "policy":
|
if definition.kind == "policy":
|
||||||
return PolicyInspection(capability=str(safe.get("capability") or definition.id))
|
return PolicyInspection(
|
||||||
|
capability=str(safe.get("capability") or definition.id),
|
||||||
|
label=_optional_text(safe.get("label")),
|
||||||
|
summary=_optional_text(safe.get("summary")),
|
||||||
|
contract_version=_optional_text(safe.get("contract_version")),
|
||||||
|
stability=_capability_stability(safe.get("stability")),
|
||||||
|
audience=_string_list(safe.get("audience")),
|
||||||
|
)
|
||||||
if definition.kind == "capability":
|
if definition.kind == "capability":
|
||||||
return CapabilityInspection(capability=str(safe.get("capability") or definition.id))
|
return CapabilityInspection(
|
||||||
|
capability=str(safe.get("capability") or definition.id),
|
||||||
|
label=_optional_text(safe.get("label")),
|
||||||
|
summary=_optional_text(safe.get("summary")),
|
||||||
|
contract_version=_optional_text(safe.get("contract_version")),
|
||||||
|
stability=_capability_stability(safe.get("stability")),
|
||||||
|
audience=_string_list(safe.get("audience")),
|
||||||
|
)
|
||||||
return ManifestInspection(
|
return ManifestInspection(
|
||||||
module_id=str(safe.get("module_id") or definition.id.split(".", 1)[0]),
|
module_id=str(safe.get("module_id") or definition.id.split(".", 1)[0]),
|
||||||
name=str(safe.get("name") or definition.label),
|
name=str(safe.get("name") or definition.label),
|
||||||
@@ -319,6 +403,7 @@ def _registered_source(
|
|||||||
condition: DocumentationCondition | None = None,
|
condition: DocumentationCondition | None = None,
|
||||||
documentation_types: tuple[DocumentationType, ...] = ("admin",),
|
documentation_types: tuple[DocumentationType, ...] = ("admin",),
|
||||||
state: Literal["configured", "disabled", "unavailable"] = "configured",
|
state: Literal["configured", "disabled", "unavailable"] = "configured",
|
||||||
|
state_reason: str | None = None,
|
||||||
configuration_key: str | None = None,
|
configuration_key: str | None = None,
|
||||||
) -> RegisteredDocumentationSource:
|
) -> RegisteredDocumentationSource:
|
||||||
clean_provenance = _safe_source_fields(provenance)
|
clean_provenance = _safe_source_fields(provenance)
|
||||||
@@ -329,6 +414,7 @@ def _registered_source(
|
|||||||
owner_module_id=owner_module_id,
|
owner_module_id=owner_module_id,
|
||||||
label=label,
|
label=label,
|
||||||
state=state,
|
state=state,
|
||||||
|
state_reason=state_reason,
|
||||||
inspection_url=f"/api/v1/docs/sources/{source_id}",
|
inspection_url=f"/api/v1/docs/sources/{source_id}",
|
||||||
provenance=DocumentationSourceProvenance(
|
provenance=DocumentationSourceProvenance(
|
||||||
source=str(clean_provenance.get("source") or "unknown"),
|
source=str(clean_provenance.get("source") or "unknown"),
|
||||||
@@ -346,6 +432,130 @@ def _registered_source(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _catalog_sources() -> tuple[RegisteredDocumentationSource, ...]:
|
||||||
|
from govoplan_core.core.configuration_packages import validate_configuration_package_catalog
|
||||||
|
from govoplan_core.core.module_package_catalog import validate_module_package_catalog
|
||||||
|
|
||||||
|
return (
|
||||||
|
*_release_catalog_sources(
|
||||||
|
"modules",
|
||||||
|
"Module release catalog",
|
||||||
|
validate_module_package_catalog(),
|
||||||
|
),
|
||||||
|
*_release_catalog_sources(
|
||||||
|
"configuration_packages",
|
||||||
|
"Configuration package catalog",
|
||||||
|
validate_configuration_package_catalog(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _release_catalog_sources(
|
||||||
|
catalog_type: Literal["modules", "configuration_packages"],
|
||||||
|
label: str,
|
||||||
|
validation: Mapping[str, object],
|
||||||
|
) -> tuple[RegisteredDocumentationSource, ...]:
|
||||||
|
entry_key = "modules" if catalog_type == "modules" else "packages"
|
||||||
|
raw_entries = validation.get(entry_key)
|
||||||
|
entries = [item for item in raw_entries if isinstance(item, Mapping)] if isinstance(raw_entries, list) else []
|
||||||
|
configured = bool(validation.get("configured"))
|
||||||
|
valid = bool(validation.get("valid"))
|
||||||
|
state: Literal["configured", "disabled", "unavailable"] = (
|
||||||
|
"configured" if configured and valid else "unavailable" if configured else "disabled"
|
||||||
|
)
|
||||||
|
reason = _optional_text(validation.get("error"))
|
||||||
|
if state == "disabled":
|
||||||
|
reason = "No catalog source is configured."
|
||||||
|
catalog_id = f"docs.release-catalog.{catalog_type.replace('_', '-')}"
|
||||||
|
catalog_source = _registered_source(
|
||||||
|
source_id=catalog_id,
|
||||||
|
kind="release_catalog",
|
||||||
|
owner_module_id="docs",
|
||||||
|
label=label,
|
||||||
|
provenance={
|
||||||
|
"source": "core_catalog_contract",
|
||||||
|
"published_at": validation.get("generated_at"),
|
||||||
|
},
|
||||||
|
inspection=ReleaseCatalogInspection(
|
||||||
|
catalog_type=catalog_type,
|
||||||
|
channel=_optional_text(validation.get("channel")),
|
||||||
|
sequence=_optional_int(validation.get("sequence")),
|
||||||
|
generated_at=_optional_text(validation.get("generated_at")),
|
||||||
|
entry_count=len(entries),
|
||||||
|
signed=bool(validation.get("signed")),
|
||||||
|
trusted=bool(validation.get("trusted")),
|
||||||
|
cache_used=bool(validation.get("cache_used")),
|
||||||
|
warnings=_string_list(validation.get("warnings")),
|
||||||
|
entries=[_release_catalog_entry(item, catalog_type) for item in entries],
|
||||||
|
),
|
||||||
|
state=state,
|
||||||
|
state_reason=reason,
|
||||||
|
)
|
||||||
|
if catalog_type == "modules" or not valid:
|
||||||
|
return (catalog_source,)
|
||||||
|
return (
|
||||||
|
catalog_source,
|
||||||
|
*(
|
||||||
|
_configuration_package_source(item, validation)
|
||||||
|
for item in entries
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _release_catalog_entry(
|
||||||
|
item: Mapping[str, object],
|
||||||
|
catalog_type: Literal["modules", "configuration_packages"],
|
||||||
|
) -> ReleaseCatalogEntryInspection:
|
||||||
|
item_id = (
|
||||||
|
_optional_text(item.get("module_id"))
|
||||||
|
if catalog_type == "modules"
|
||||||
|
else _optional_text(item.get("package_id"))
|
||||||
|
)
|
||||||
|
return ReleaseCatalogEntryInspection(
|
||||||
|
id=item_id or "unknown",
|
||||||
|
name=_optional_text(item.get("name")) or item_id or "Unknown",
|
||||||
|
version=_optional_text(item.get("version")),
|
||||||
|
description=_optional_text(item.get("description")),
|
||||||
|
action=_optional_text(item.get("action")),
|
||||||
|
tags=_string_list(item.get("tags")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _configuration_package_source(
|
||||||
|
item: Mapping[str, object],
|
||||||
|
validation: Mapping[str, object],
|
||||||
|
) -> RegisteredDocumentationSource:
|
||||||
|
package_id = _optional_text(item.get("package_id")) or "unknown"
|
||||||
|
required_modules = [
|
||||||
|
str(requirement.get("module_id"))
|
||||||
|
for requirement in item.get("required_modules", ())
|
||||||
|
if isinstance(requirement, Mapping) and requirement.get("module_id")
|
||||||
|
] if isinstance(item.get("required_modules"), (list, tuple)) else []
|
||||||
|
return _registered_source(
|
||||||
|
source_id=_derived_source_id("docs", "configuration-package", package_id),
|
||||||
|
kind="configuration_package",
|
||||||
|
owner_module_id="docs",
|
||||||
|
label=_optional_text(item.get("name")) or package_id,
|
||||||
|
provenance={
|
||||||
|
"source": "configuration_package_catalog",
|
||||||
|
"version": item.get("version"),
|
||||||
|
"published_at": validation.get("generated_at"),
|
||||||
|
},
|
||||||
|
inspection=ConfigurationPackageInspection(
|
||||||
|
package_id=package_id,
|
||||||
|
name=_optional_text(item.get("name")),
|
||||||
|
version=_optional_text(item.get("version")),
|
||||||
|
schema_version=_optional_text(item.get("schema_version")),
|
||||||
|
description=_optional_text(item.get("description")),
|
||||||
|
publisher=_optional_text(item.get("publisher")),
|
||||||
|
category=_optional_text(item.get("category")),
|
||||||
|
tags=_string_list(item.get("tags")),
|
||||||
|
required_modules=required_modules,
|
||||||
|
required_capabilities=_string_list(item.get("required_capabilities")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _visibility_payload(
|
def _visibility_payload(
|
||||||
condition: DocumentationCondition,
|
condition: DocumentationCondition,
|
||||||
documentation_types: tuple[DocumentationType, ...],
|
documentation_types: tuple[DocumentationType, ...],
|
||||||
@@ -385,6 +595,22 @@ def _optional_text(value: object) -> str | None:
|
|||||||
return text[:2_000] if text else None
|
return text[:2_000] if text else None
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_int(value: object) -> int | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _capability_stability(
|
||||||
|
value: object,
|
||||||
|
) -> Literal["experimental", "stable", "deprecated"] | None:
|
||||||
|
text = _optional_text(value)
|
||||||
|
return text if text in {"experimental", "stable", "deprecated"} else None
|
||||||
|
|
||||||
|
|
||||||
def _string_list(value: object) -> list[str]:
|
def _string_list(value: object) -> list[str]:
|
||||||
if not isinstance(value, (list, tuple)):
|
if not isinstance(value, (list, tuple)):
|
||||||
return []
|
return []
|
||||||
|
|||||||
+109
-3
@@ -10,6 +10,7 @@ from fastapi import HTTPException
|
|||||||
from govoplan_access.backend.manifest import get_manifest as get_access_manifest
|
from govoplan_access.backend.manifest import get_manifest as get_access_manifest
|
||||||
from govoplan_core.core.modules import DocumentationCondition, DocumentationLink, DocumentationTopic, ModuleManifest, NavItem
|
from govoplan_core.core.modules import DocumentationCondition, DocumentationLink, DocumentationTopic, ModuleManifest, NavItem
|
||||||
from govoplan_core.core.modules import (
|
from govoplan_core.core.modules import (
|
||||||
|
CapabilityDocumentation,
|
||||||
DocumentationConfigurationDecision,
|
DocumentationConfigurationDecision,
|
||||||
DocumentationConfigurationProviderRegistration,
|
DocumentationConfigurationProviderRegistration,
|
||||||
DocumentationSourceDefinition,
|
DocumentationSourceDefinition,
|
||||||
@@ -27,7 +28,10 @@ from govoplan_docs.backend.api.v1.routes import (
|
|||||||
docs_context,
|
docs_context,
|
||||||
)
|
)
|
||||||
from govoplan_docs.backend.manifest import get_manifest as get_docs_manifest
|
from govoplan_docs.backend.manifest import get_manifest as get_docs_manifest
|
||||||
from govoplan_docs.backend.sources import build_documentation_source_registry
|
from govoplan_docs.backend.sources import (
|
||||||
|
_release_catalog_sources,
|
||||||
|
build_documentation_source_registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakePrincipal:
|
class FakePrincipal:
|
||||||
@@ -446,6 +450,14 @@ class DocsContextTests(unittest.TestCase):
|
|||||||
"example.lookup": lambda _context: object(),
|
"example.lookup": lambda _context: object(),
|
||||||
"policy.example": lambda _context: object(),
|
"policy.example": lambda _context: object(),
|
||||||
},
|
},
|
||||||
|
capability_documentation={
|
||||||
|
"example.lookup": CapabilityDocumentation(
|
||||||
|
label="Example lookup",
|
||||||
|
summary="Resolves example records through a stable provider contract.",
|
||||||
|
contract_version="2",
|
||||||
|
audience=("module_admin",),
|
||||||
|
),
|
||||||
|
},
|
||||||
frontend=FrontendModule(
|
frontend=FrontendModule(
|
||||||
module_id="example",
|
module_id="example",
|
||||||
routes=(
|
routes=(
|
||||||
@@ -490,7 +502,10 @@ class DocsContextTests(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
sources = build_documentation_source_registry((manifest,)).sources()
|
sources = build_documentation_source_registry(
|
||||||
|
(manifest,),
|
||||||
|
include_runtime_catalogs=False,
|
||||||
|
).sources()
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{source.item.kind for source in sources},
|
{source.item.kind for source in sources},
|
||||||
@@ -505,6 +520,14 @@ class DocsContextTests(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertNotIn("must-not-leak", repr(sources))
|
self.assertNotIn("must-not-leak", repr(sources))
|
||||||
|
capability_source = next(
|
||||||
|
source for source in sources
|
||||||
|
if source.item.kind == "capability"
|
||||||
|
)
|
||||||
|
self.assertEqual(capability_source.item.label, "Example lookup")
|
||||||
|
self.assertEqual(capability_source.item.inspection.summary, "Resolves example records through a stable provider contract.")
|
||||||
|
self.assertEqual(capability_source.item.inspection.contract_version, "2")
|
||||||
|
self.assertEqual(capability_source.item.inspection.audience, ["module_admin"])
|
||||||
for source in sources:
|
for source in sources:
|
||||||
self.assertTrue(source.item.id.startswith("example."))
|
self.assertTrue(source.item.id.startswith("example."))
|
||||||
self.assertEqual("example", source.item.owner_module_id)
|
self.assertEqual("example", source.item.owner_module_id)
|
||||||
@@ -513,6 +536,86 @@ class DocsContextTests(unittest.TestCase):
|
|||||||
source.item.inspection_url,
|
source.item.inspection_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_release_and_configuration_catalogs_become_typed_sources(self) -> None:
|
||||||
|
release_sources = _release_catalog_sources(
|
||||||
|
"modules",
|
||||||
|
"Module release catalog",
|
||||||
|
{
|
||||||
|
"configured": True,
|
||||||
|
"valid": True,
|
||||||
|
"channel": "stable",
|
||||||
|
"sequence": 12,
|
||||||
|
"generated_at": "2026-07-31T10:00:00Z",
|
||||||
|
"signed": True,
|
||||||
|
"trusted": True,
|
||||||
|
"cache_used": False,
|
||||||
|
"warnings": [],
|
||||||
|
"modules": [{
|
||||||
|
"module_id": "files",
|
||||||
|
"name": "Files",
|
||||||
|
"version": "0.1.10",
|
||||||
|
"description": "Managed files.",
|
||||||
|
"action": "install",
|
||||||
|
"tags": ["official"],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(len(release_sources), 1)
|
||||||
|
release = release_sources[0].item
|
||||||
|
self.assertEqual(release.kind, "release_catalog")
|
||||||
|
self.assertEqual(release.state, "configured")
|
||||||
|
self.assertEqual(release.inspection.entry_count, 1)
|
||||||
|
self.assertEqual(release.inspection.entries[0].description, "Managed files.")
|
||||||
|
|
||||||
|
configuration_sources = _release_catalog_sources(
|
||||||
|
"configuration_packages",
|
||||||
|
"Configuration package catalog",
|
||||||
|
{
|
||||||
|
"configured": True,
|
||||||
|
"valid": True,
|
||||||
|
"channel": "stable",
|
||||||
|
"signed": True,
|
||||||
|
"trusted": True,
|
||||||
|
"packages": [{
|
||||||
|
"package_id": "public-service.base",
|
||||||
|
"name": "Public service base",
|
||||||
|
"version": "3",
|
||||||
|
"description": "Baseline configuration for a public service.",
|
||||||
|
"required_modules": [{"module_id": "access"}],
|
||||||
|
"required_capabilities": ["policy.retention"],
|
||||||
|
"tags": ["reference"],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(len(configuration_sources), 2)
|
||||||
|
package = next(
|
||||||
|
source.item for source in configuration_sources
|
||||||
|
if source.item.kind == "configuration_package"
|
||||||
|
)
|
||||||
|
self.assertEqual(package.inspection.package_id, "public-service.base")
|
||||||
|
self.assertEqual(package.inspection.required_modules, ["access"])
|
||||||
|
self.assertEqual(package.inspection.required_capabilities, ["policy.retention"])
|
||||||
|
|
||||||
|
def test_docs_manifest_links_repository_and_wiki_sources(self) -> None:
|
||||||
|
sources = build_documentation_source_registry(
|
||||||
|
(get_docs_manifest(),),
|
||||||
|
include_runtime_catalogs=False,
|
||||||
|
).sources()
|
||||||
|
|
||||||
|
source_kinds = {source.item.kind for source in sources}
|
||||||
|
self.assertIn("repository", source_kinds)
|
||||||
|
self.assertIn("wiki", source_kinds)
|
||||||
|
self.assertTrue(any(
|
||||||
|
source.item.inspection.href.endswith("/govoplan-docs/wiki")
|
||||||
|
for source in sources
|
||||||
|
if source.item.kind == "wiki"
|
||||||
|
))
|
||||||
|
self.assertTrue(any(
|
||||||
|
"DOCUMENTATION_LAYER_CONCEPT.md" in source.item.inspection.href
|
||||||
|
for source in sources
|
||||||
|
if source.item.kind == "repository"
|
||||||
|
))
|
||||||
|
|
||||||
def test_source_visibility_hides_unauthorized_ids(self) -> None:
|
def test_source_visibility_hides_unauthorized_ids(self) -> None:
|
||||||
manifest = ModuleManifest(
|
manifest = ModuleManifest(
|
||||||
id="example",
|
id="example",
|
||||||
@@ -536,7 +639,10 @@ class DocsContextTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
registry = PlatformRegistry()
|
registry = PlatformRegistry()
|
||||||
registry.register(manifest)
|
registry.register(manifest)
|
||||||
sources = build_documentation_source_registry((manifest,)).sources()
|
sources = build_documentation_source_registry(
|
||||||
|
(manifest,),
|
||||||
|
include_runtime_catalogs=False,
|
||||||
|
).sources()
|
||||||
|
|
||||||
denied = _visible_documentation_sources(
|
denied = _visible_documentation_sources(
|
||||||
sources,
|
sources,
|
||||||
|
|||||||
@@ -70,6 +70,20 @@ export type DocsSource = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DocsSourceDetail = DocsSource & {
|
||||||
|
visibility: {
|
||||||
|
documentation_types: string[];
|
||||||
|
required_modules: string[];
|
||||||
|
any_modules: string[];
|
||||||
|
missing_modules: string[];
|
||||||
|
required_capabilities: string[];
|
||||||
|
required_scopes: string[];
|
||||||
|
any_scopes: string[];
|
||||||
|
configuration_keys: string[];
|
||||||
|
};
|
||||||
|
inspection: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
export type DocsDocumentationCondition = {
|
export type DocsDocumentationCondition = {
|
||||||
required_modules: string[];
|
required_modules: string[];
|
||||||
any_modules: string[];
|
any_modules: string[];
|
||||||
@@ -186,3 +200,18 @@ export function fetchDocsContext(settings: ApiSettings, options: { documentation
|
|||||||
const query = params.toString();
|
const query = params.toString();
|
||||||
return apiFetch(settings, `/api/v1/docs/context${query ? `?${query}` : ""}`);
|
return apiFetch(settings, `/api/v1/docs/context${query ? `?${query}` : ""}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchDocsSource(
|
||||||
|
settings: ApiSettings,
|
||||||
|
sourceId: string,
|
||||||
|
options: { documentationType?: "admin" | "user"; locale?: string } = {}
|
||||||
|
): Promise<DocsSourceDetail> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.documentationType) params.set("type", options.documentationType);
|
||||||
|
if (options.locale) params.set("locale", options.locale);
|
||||||
|
const query = params.toString();
|
||||||
|
return apiFetch(
|
||||||
|
settings,
|
||||||
|
`/api/v1/docs/sources/${encodeURIComponent(sourceId)}${query ? `?${query}` : ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Link, useLocation } from "react-router";
|
import { Link, useLocation } from "react-router";
|
||||||
import { ChevronDown, ChevronRight, RefreshCw } from "lucide-react";
|
import { ChevronDown, ChevronRight, Eye, RefreshCw } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DataGrid,
|
DataGrid,
|
||||||
|
Dialog,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
ExplorerTree,
|
ExplorerTree,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
@@ -18,12 +19,14 @@ import {
|
|||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import {
|
import {
|
||||||
fetchDocsContext,
|
fetchDocsContext,
|
||||||
|
fetchDocsSource,
|
||||||
type DocsContext,
|
type DocsContext,
|
||||||
type DocsDocumentationTopic,
|
type DocsDocumentationTopic,
|
||||||
type DocsModule,
|
type DocsModule,
|
||||||
type DocsOptionalModuleEvidence,
|
type DocsOptionalModuleEvidence,
|
||||||
type DocsRoute,
|
type DocsRoute,
|
||||||
type DocsSource
|
type DocsSource,
|
||||||
|
type DocsSourceDetail
|
||||||
} from "../../api/docs";
|
} from "../../api/docs";
|
||||||
|
|
||||||
type DocumentationType = "admin" | "user";
|
type DocumentationType = "admin" | "user";
|
||||||
@@ -189,6 +192,8 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
|
|||||||
grantedPermissions={grantedPermissions}
|
grantedPermissions={grantedPermissions}
|
||||||
evidenceModules={context?.layers.evidence.optional_modules ?? []}
|
evidenceModules={context?.layers.evidence.optional_modules ?? []}
|
||||||
evidenceSources={context?.layers.evidence.sources ?? []}
|
evidenceSources={context?.layers.evidence.sources ?? []}
|
||||||
|
settings={settings}
|
||||||
|
locale={locale}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
<PageOutline items={outlineItems} />
|
<PageOutline items={outlineItems} />
|
||||||
@@ -269,7 +274,9 @@ function SelectedPageContent({
|
|||||||
availableRoutes,
|
availableRoutes,
|
||||||
grantedPermissions,
|
grantedPermissions,
|
||||||
evidenceModules,
|
evidenceModules,
|
||||||
evidenceSources
|
evidenceSources,
|
||||||
|
settings,
|
||||||
|
locale
|
||||||
}: {
|
}: {
|
||||||
page: DocsPageNode | null;
|
page: DocsPageNode | null;
|
||||||
adminDocs: boolean;
|
adminDocs: boolean;
|
||||||
@@ -281,6 +288,8 @@ function SelectedPageContent({
|
|||||||
grantedPermissions: Array<{ scope: string; label: string; category: string }>;
|
grantedPermissions: Array<{ scope: string; label: string; category: string }>;
|
||||||
evidenceModules: DocsOptionalModuleEvidence[];
|
evidenceModules: DocsOptionalModuleEvidence[];
|
||||||
evidenceSources: DocsSource[];
|
evidenceSources: DocsSource[];
|
||||||
|
settings: ApiSettings;
|
||||||
|
locale: string;
|
||||||
}) {
|
}) {
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return (
|
return (
|
||||||
@@ -310,7 +319,13 @@ function SelectedPageContent({
|
|||||||
<section id="docs-admin-permissions" className="docs-reference-block">
|
<section id="docs-admin-permissions" className="docs-reference-block">
|
||||||
<h3>i18n:govoplan-docs.granted_permissions.0a232e78</h3>
|
<h3>i18n:govoplan-docs.granted_permissions.0a232e78</h3>
|
||||||
<PermissionList permissions={grantedPermissions} />
|
<PermissionList permissions={grantedPermissions} />
|
||||||
<EvidenceList modules={evidenceModules} sources={evidenceSources} />
|
<EvidenceList
|
||||||
|
modules={evidenceModules}
|
||||||
|
sources={evidenceSources}
|
||||||
|
settings={settings}
|
||||||
|
documentationType={documentationType}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -965,26 +980,142 @@ function PermissionList({ permissions }: { permissions: Array<{ scope: string; l
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EvidenceList({ modules, sources }: { modules: DocsOptionalModuleEvidence[]; sources: DocsSource[] }) {
|
function EvidenceList({
|
||||||
|
modules,
|
||||||
|
sources,
|
||||||
|
settings,
|
||||||
|
documentationType,
|
||||||
|
locale
|
||||||
|
}: {
|
||||||
|
modules: DocsOptionalModuleEvidence[];
|
||||||
|
sources: DocsSource[];
|
||||||
|
settings: ApiSettings;
|
||||||
|
documentationType: DocumentationType;
|
||||||
|
locale: string;
|
||||||
|
}) {
|
||||||
|
const [selected, setSelected] = useState<DocsSourceDetail | null>(null);
|
||||||
|
const [loadingSourceId, setLoadingSourceId] = useState("");
|
||||||
|
const [sourceError, setSourceError] = useState("");
|
||||||
|
|
||||||
if (!modules.length && !sources.length) return <p className="muted">i18n:govoplan-docs.no_evidence_sources_found.be3bb2f6</p>;
|
if (!modules.length && !sources.length) return <p className="muted">i18n:govoplan-docs.no_evidence_sources_found.be3bb2f6</p>;
|
||||||
|
|
||||||
|
async function inspectSource(source: DocsSource) {
|
||||||
|
setLoadingSourceId(source.id);
|
||||||
|
setSourceError("");
|
||||||
|
try {
|
||||||
|
setSelected(await fetchDocsSource(settings, source.id, { documentationType, locale }));
|
||||||
|
} catch (error) {
|
||||||
|
setSourceError(adminErrorMessage(error));
|
||||||
|
} finally {
|
||||||
|
setLoadingSourceId("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dl className="detail-list">
|
<>
|
||||||
{modules.map((item) =>
|
{sourceError && <DismissibleAlert tone="danger" resetKey={sourceError}>{sourceError}</DismissibleAlert>}
|
||||||
<div key={`${item.source_module_id}-${item.module_id}`}>
|
<dl className="detail-list">
|
||||||
<dt><StatusBadge status={item.status === "installed" ? "success" : "inactive"} label={item.status} /></dt>
|
{modules.map((item) =>
|
||||||
<dd><strong>{item.module_id}</strong><span className="muted"> · {item.reason}</span></dd>
|
<div key={`${item.source_module_id}-${item.module_id}`}>
|
||||||
</div>
|
<dt><StatusBadge status={item.status === "installed" ? "success" : "inactive"} label={item.status} /></dt>
|
||||||
)}
|
<dd><strong>{item.module_id}</strong><span className="muted"> · {item.reason}</span></dd>
|
||||||
{sources.map((item) =>
|
</div>
|
||||||
<div key={item.id}>
|
)}
|
||||||
<dt><StatusBadge status={item.state === "configured" ? "success" : item.state === "disabled" ? "inactive" : "warning"} label={item.state} /></dt>
|
{sources.map((item) =>
|
||||||
<dd><strong>{item.label}</strong><span className="muted"> · {item.owner_module_id} · {item.kind} · {item.provenance.source}</span></dd>
|
<div key={item.id}>
|
||||||
</div>
|
<dt><StatusBadge status={item.state === "configured" ? "success" : item.state === "disabled" ? "inactive" : "warning"} label={item.state} /></dt>
|
||||||
)}
|
<dd>
|
||||||
</dl>
|
<strong>{item.label}</strong>
|
||||||
|
<span className="muted"> · {item.owner_module_id} · {item.kind} · {item.provenance.source}</span>
|
||||||
|
{item.state_reason && <span className="muted block">{item.state_reason}</span>}
|
||||||
|
</dd>
|
||||||
|
<dd>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="icon-button"
|
||||||
|
title="i18n:govoplan-docs.inspect_source.bcc1739d"
|
||||||
|
aria-label="i18n:govoplan-docs.inspect_source.bcc1739d"
|
||||||
|
disabled={loadingSourceId === item.id}
|
||||||
|
onClick={() => void inspectSource(item)}
|
||||||
|
>
|
||||||
|
<Eye size={16} />
|
||||||
|
</Button>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
<Dialog
|
||||||
|
open={selected !== null}
|
||||||
|
title={selected?.label ?? "i18n:govoplan-docs.source_details.6dc79c75"}
|
||||||
|
onClose={() => setSelected(null)}
|
||||||
|
footer={<Button onClick={() => setSelected(null)}>i18n:govoplan-docs.close.87b84f71</Button>}
|
||||||
|
>
|
||||||
|
{selected && <SourceInspection source={selected} />}
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SourceInspection({ source }: { source: DocsSourceDetail }) {
|
||||||
|
const provenance = compactSourceRecord(source.provenance);
|
||||||
|
const visibility = compactSourceRecord(source.visibility);
|
||||||
|
const inspection = compactSourceRecord(source.inspection);
|
||||||
|
return (
|
||||||
|
<div className="stack">
|
||||||
|
<div>
|
||||||
|
<StatusBadge
|
||||||
|
status={source.state === "configured" ? "success" : source.state === "disabled" ? "inactive" : "warning"}
|
||||||
|
label={source.state}
|
||||||
|
/>
|
||||||
|
{source.state_reason && <p className="muted">{source.state_reason}</p>}
|
||||||
|
</div>
|
||||||
|
<SourceInspectionGroup title="i18n:govoplan-docs.provenance.73e80298" values={provenance} />
|
||||||
|
<SourceInspectionGroup title="i18n:govoplan-docs.visibility.80ab5798" values={visibility} />
|
||||||
|
<SourceInspectionGroup title="i18n:govoplan-docs.inspection.83dc17ca" values={inspection} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SourceInspectionGroup({ title, values }: { title: string; values: Array<[string, unknown]> }) {
|
||||||
|
if (!values.length) return null;
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<dl className="detail-list">
|
||||||
|
{values.map(([key, value]) =>
|
||||||
|
<div key={key}>
|
||||||
|
<dt>{humanizeSourceKey(key)}</dt>
|
||||||
|
<dd>{formatSourceValue(value)}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactSourceRecord(value: object): Array<[string, unknown]> {
|
||||||
|
return Object.entries(value).filter(([, item]) => {
|
||||||
|
if (item === null || item === undefined || item === "") return false;
|
||||||
|
return !Array.isArray(item) || item.length > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function humanizeSourceKey(value: string): string {
|
||||||
|
const words = value.replaceAll("_", " ");
|
||||||
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSourceValue(value: unknown): string {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.every((item) => ["string", "number", "boolean"].includes(typeof item))) {
|
||||||
|
return value.join(", ");
|
||||||
|
}
|
||||||
|
return JSON.stringify(value, null, 2);
|
||||||
|
}
|
||||||
|
if (value && typeof value === "object") return JSON.stringify(value, null, 2);
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
function routeRequirements(route: DocsRoute): string {
|
function routeRequirements(route: DocsRoute): string {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (route.required_all.length) parts.push(`all: ${route.required_all.join(", ")}`);
|
if (route.required_all.length) parts.push(`all: ${route.required_all.join(", ")}`);
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-docs.screen.c4878ec4": "Screen",
|
"i18n:govoplan-docs.screen.c4878ec4": "Screen",
|
||||||
"i18n:govoplan-docs.section.5e498158": "Section",
|
"i18n:govoplan-docs.section.5e498158": "Section",
|
||||||
"i18n:govoplan-docs.source.6da13add": "Source",
|
"i18n:govoplan-docs.source.6da13add": "Source",
|
||||||
|
"i18n:govoplan-docs.source_details.6dc79c75": "Source details",
|
||||||
|
"i18n:govoplan-docs.inspect_source.bcc1739d": "Inspect source",
|
||||||
|
"i18n:govoplan-docs.inspection.83dc17ca": "Inspection",
|
||||||
|
"i18n:govoplan-docs.provenance.73e80298": "Provenance",
|
||||||
|
"i18n:govoplan-docs.visibility.80ab5798": "Visibility",
|
||||||
|
"i18n:govoplan-docs.close.87b84f71": "Close",
|
||||||
"i18n:govoplan-docs.status.bae7d5be": "Status",
|
"i18n:govoplan-docs.status.bae7d5be": "Status",
|
||||||
"i18n:govoplan-docs.steps.6041435e": "Steps",
|
"i18n:govoplan-docs.steps.6041435e": "Steps",
|
||||||
"i18n:govoplan-docs.summary.d6b9936d": "Summary",
|
"i18n:govoplan-docs.summary.d6b9936d": "Summary",
|
||||||
@@ -165,6 +171,12 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-docs.screen.c4878ec4": "Ansicht",
|
"i18n:govoplan-docs.screen.c4878ec4": "Ansicht",
|
||||||
"i18n:govoplan-docs.section.5e498158": "Bereich",
|
"i18n:govoplan-docs.section.5e498158": "Bereich",
|
||||||
"i18n:govoplan-docs.source.6da13add": "Quelle",
|
"i18n:govoplan-docs.source.6da13add": "Quelle",
|
||||||
|
"i18n:govoplan-docs.source_details.6dc79c75": "Quelldetails",
|
||||||
|
"i18n:govoplan-docs.inspect_source.bcc1739d": "Quelle anzeigen",
|
||||||
|
"i18n:govoplan-docs.inspection.83dc17ca": "Prüfdaten",
|
||||||
|
"i18n:govoplan-docs.provenance.73e80298": "Herkunft",
|
||||||
|
"i18n:govoplan-docs.visibility.80ab5798": "Sichtbarkeit",
|
||||||
|
"i18n:govoplan-docs.close.87b84f71": "Schließen",
|
||||||
"i18n:govoplan-docs.status.bae7d5be": "Status",
|
"i18n:govoplan-docs.status.bae7d5be": "Status",
|
||||||
"i18n:govoplan-docs.steps.6041435e": "Schritte",
|
"i18n:govoplan-docs.steps.6041435e": "Schritte",
|
||||||
"i18n:govoplan-docs.summary.d6b9936d": "Zusammenfassung",
|
"i18n:govoplan-docs.summary.d6b9936d": "Zusammenfassung",
|
||||||
|
|||||||
Reference in New Issue
Block a user