1 Commits
Author SHA1 Message Date
zemion 12fe709a34 feat(docs): localize structured metadata and gate coverage
Module Package Release / publish-packages (push) Successful in 11s
2026-08-22 20:25:37 +02:00
9 changed files with 370 additions and 19 deletions
+11 -1
View File
@@ -82,7 +82,8 @@ module, or every module checkout in a workspace, with:
govoplan-docs-export-public \ govoplan-docs-export-public \
--workspace-root /mnt/DATA/git \ --workspace-root /mnt/DATA/git \
--output public/docs/v1/catalog.json \ --output public/docs/v1/catalog.json \
--coverage-output docs/DOCUMENTATION_COVERAGE.md --coverage-output docs/DOCUMENTATION_COVERAGE.md \
--coverage-baseline docs/DOCUMENTATION_COVERAGE_BASELINE.json
``` ```
Use `--check` in publication CI to reject a stale checked-in catalog. Dynamic Use `--check` in publication CI to reject a stale checked-in catalog. Dynamic
@@ -90,6 +91,15 @@ Use `--check` in publication CI to reject a stale checked-in catalog. Dynamic
on permissions, policy, configuration, and live provider state; the export on permissions, policy, configuration, and live provider state; the export
records which modules have such additional documentation. records which modules have such additional documentation.
Static topics localize title, summary, and body through `translations`.
Rendered metadata such as steps, fields, limitations, consequences, and
verification uses Core's opt-in `structured_translation_version="1"` plus
`structured_translations` contract. The registry validates exact shape before
Docs overlays the requested locale. The public catalog reports structured
adoption separately. A reviewed coverage-baseline file sets monotonic minima
and maxima so publication CI also rejects localization or coverage regressions
after generated output is refreshed.
Pressing `F1` resolves the focused field or action first, then its containing Pressing `F1` resolves the focused field or action first, then its containing
dialog or section, current page, and owning module. The shell sends the focused dialog or section, current page, and owning module. The shell sends the focused
context together with `fallback_context` and `module`; Docs selects the first context together with `fallback_context` and `module`; Docs selects the first
+2 -2
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-docs" name = "govoplan-docs"
version = "0.1.20" version = "0.1.21"
description = "GovOPlaN documentation module for configured-system, available, and evidence documentation." description = "GovOPlaN documentation module for configured-system, available, and evidence documentation."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }] authors = [{ name = "GovOPlaN" }]
dependencies = [ dependencies = [
"govoplan-core>=0.1.18", "govoplan-core>=0.1.37",
"govoplan-access>=0.1.18", "govoplan-access>=0.1.18",
] ]
+22 -2
View File
@@ -17,6 +17,7 @@ from govoplan_core.core.modules import (
ModuleManifest, ModuleManifest,
NavItem, NavItem,
PermissionDefinition, PermissionDefinition,
localized_documentation_metadata,
user_workflow_scope_condition_issues, user_workflow_scope_condition_issues,
) )
from govoplan_core.core.registry import PlatformRegistry from govoplan_core.core.registry import PlatformRegistry
@@ -1432,6 +1433,10 @@ def _documentation_topic_payload(
) -> dict[str, Any]: ) -> dict[str, Any]:
module_id = topic.source_module_id or source_module_id module_id = topic.source_module_id or source_module_id
translation_locale, translation = _translation_for_locale(topic, locale) translation_locale, translation = _translation_for_locale(topic, locale)
structured_translation_locale = _structured_translation_locale(topic, locale)
localized_metadata = localized_documentation_metadata(
topic, structured_translation_locale
)
kind = _documentation_topic_kind(topic) kind = _documentation_topic_kind(topic)
payload = { payload = {
"id": topic.id, "id": topic.id,
@@ -1459,6 +1464,8 @@ def _documentation_topic_payload(
"i18n_key": topic.i18n_key or topic.id, "i18n_key": topic.i18n_key or topic.id,
"locale": locale, "locale": locale,
"translation_locale": translation_locale, "translation_locale": translation_locale,
"structured_translation_locale": structured_translation_locale,
"structured_translation_version": topic.structured_translation_version,
"version": { "version": {
"resolved": resolved_version, "resolved": resolved_version,
"minimum": topic.version_min, "minimum": topic.version_min,
@@ -1483,7 +1490,7 @@ def _documentation_topic_payload(
_documentation_configuration_payload(configuration[key]) _documentation_configuration_payload(configuration[key])
for key in sorted(configuration) for key in sorted(configuration)
], ],
"metadata": dict(topic.metadata), "metadata": localized_metadata,
} }
if documentation_type == "admin": if documentation_type == "admin":
return payload return payload
@@ -1511,6 +1518,12 @@ def _documentation_topic_payload(
"i18n_key": "", "i18n_key": "",
"locale": locale, "locale": locale,
"translation_locale": payload["translation_locale"], "translation_locale": payload["translation_locale"],
"structured_translation_locale": payload[
"structured_translation_locale"
],
"structured_translation_version": payload[
"structured_translation_version"
],
"conditions": [], "conditions": [],
"links": [ "links": [
_documentation_link_payload(link) _documentation_link_payload(link)
@@ -1521,7 +1534,7 @@ def _documentation_topic_payload(
"unlocks": list(topic.unlocks), "unlocks": list(topic.unlocks),
"configuration_keys": [], "configuration_keys": [],
"configuration_states": [], "configuration_states": [],
"metadata": _user_topic_metadata(kind, topic.metadata), "metadata": _user_topic_metadata(kind, localized_metadata),
} }
@@ -1662,6 +1675,13 @@ def _translation_for_locale(topic: DocumentationTopic, locale: str) -> tuple[str
return "source", {} return "source", {}
def _structured_translation_locale(topic: DocumentationTopic, locale: str) -> str:
for candidate in _locale_candidates(locale):
if candidate in topic.structured_translations:
return candidate
return "source"
def _locale_candidates(locale: str) -> tuple[str, ...]: def _locale_candidates(locale: str) -> tuple[str, ...]:
normalized = _normalize_locale(locale) normalized = _normalize_locale(locale)
base = normalized.split("-", 1)[0] base = normalized.split("-", 1)[0]
+33 -3
View File
@@ -131,7 +131,7 @@ def _dsar_provider(_context: ModuleContext) -> DocsDsarProvider:
manifest = ModuleManifest( manifest = ModuleManifest(
id="docs", id="docs",
name="Docs", name="Docs",
version="0.1.20", version="0.1.21",
required_capabilities=( required_capabilities=(
CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PERMISSION_EVALUATOR,
@@ -458,7 +458,8 @@ manifest = ModuleManifest(
"The public exporter reads DocumentationTopic contributions from all installed packages or sibling module checkouts, " "The public exporter reads DocumentationTopic contributions from all installed packages or sibling module checkouts, "
"projects German and English content, and records documentation coverage. Runtime providers remain in the authenticated " "projects German and English content, and records documentation coverage. Runtime providers remain in the authenticated "
"Docs surface because their output can depend on the current actor, policy, configuration, and live service state. " "Docs surface because their output can depend on the current actor, policy, configuration, and live service state. "
"Publication CI should run the export check and reject a stale source digest." "Rendered steps, fields, limitations, consequences, and verification use Core's versioned same-shape structured-translation contract. "
"Publication CI runs both the source-digest check and a reviewed monotonic coverage baseline, so regenerating the catalog cannot conceal a regression."
), ),
layer="always", layer="always",
documentation_types=("admin", "user"), documentation_types=("admin", "user"),
@@ -472,7 +473,8 @@ manifest = ModuleManifest(
"Der öffentliche Export liest die DocumentationTopic-Beiträge aus allen installierten Paketen oder benachbarten Modulquellen, " "Der öffentliche Export liest die DocumentationTopic-Beiträge aus allen installierten Paketen oder benachbarten Modulquellen, "
"projiziert deutsche und englische Inhalte und weist Dokumentationslücken aus. Laufzeit-Provider verbleiben in der authentifizierten " "projiziert deutsche und englische Inhalte und weist Dokumentationslücken aus. Laufzeit-Provider verbleiben in der authentifizierten "
"Dokumentationsoberfläche, da ihre Ausgabe von Rolle, Richtlinie, Konfiguration und Dienstzustand abhängen kann. " "Dokumentationsoberfläche, da ihre Ausgabe von Rolle, Richtlinie, Konfiguration und Dienstzustand abhängen kann. "
"Die Veröffentlichungs-CI soll den Quelldigest prüfen und veraltete Exporte ablehnen." "Gerenderte Schritte, Felder, Einschränkungen, Folgen und Prüfhinweise verwenden den versionierten, formgleichen Vertrag für strukturierte Übersetzungen in Core. "
"Die Veröffentlichungs-CI prüft sowohl den Quelldigest als auch einen freigegebenen monotonen Abdeckungsstand, damit das Neuerzeugen des Katalogs keine Verschlechterung verdecken kann."
), ),
} }
}, },
@@ -573,6 +575,23 @@ manifest = ModuleManifest(
"access.workflow.grant-user-access", "access.workflow.grant-user-access",
], ],
}, },
structured_translation_version="1",
structured_translations={
"de": {
"purpose": (
"Beschriftungen bleiben schnell erfassbar, während kurze Erläuterungen bei Bedarf verfügbar sind."
),
"when_used": (
"Formular- und Umschalterbeschriftungen, Zeilen mit wirksamen Werten und kompakte Verwaltungsbegriffe."
),
"user_explanation": (
"Öffnen Sie die Markierung, wenn eine Beschriftung unklar ist. Sie erläutert die lokale Auswahl in ein oder zwei Sätzen."
),
"admin_explanation": (
"Feldhilfe bleibt am Feld. Längere Verfahrens-, API- oder Richtlinienerläuterungen gehören in verknüpfte Ablauf- oder Referenzthemen."
),
}
},
), ),
DocumentationTopic( DocumentationTopic(
id="docs.pattern.contextual-help", id="docs.pattern.contextual-help",
@@ -725,6 +744,17 @@ manifest = ModuleManifest(
}, },
], ],
}, },
structured_translation_version="1",
structured_translations={
"de": {
"admin_explanation": (
"Auswirkungen von Funktionen auf Rollen gehören Access. Änderungen an IDM-Zuordnungen können unabhängig von Änderungen am Organisationsmodell gesteuert werden."
),
"user_explanation": (
"Eine Person kann eine Funktion innehaben, weil IDM ihre Identität mit der Organisationsfunktion verknüpft. Access entscheidet, welche Anwendungsberechtigungen diese Funktion gewährt."
),
}
},
), ),
), ),
documentation_sources=( documentation_sources=(
+177 -8
View File
@@ -15,10 +15,12 @@ from typing import Any
from govoplan_core.core.discovery import discover_module_manifests from govoplan_core.core.discovery import discover_module_manifests
from govoplan_core.core.modules import ( from govoplan_core.core.modules import (
DOCUMENTATION_STRUCTURED_TRANSLATION_VERSION,
DocumentationCondition, DocumentationCondition,
DocumentationLink, DocumentationLink,
DocumentationTopic, DocumentationTopic,
ModuleManifest, ModuleManifest,
localizable_documentation_metadata_keys,
) )
@@ -102,12 +104,12 @@ def documentation_coverage_markdown(catalog: Mapping[str, Any]) -> str:
"2. Give every user-facing module at least one scope-conditioned workflow topic and one field/consequence reference.", "2. Give every user-facing module at least one scope-conditioned workflow topic and one field/consequence reference.",
"3. Give every configurable module an administrator topic covering permissions, policy provenance, retention, and operational consequences.", "3. Give every configurable module an administrator topic covering permissions, policy provenance, retention, and operational consequences.",
"4. Keep live provider-state and instance-specific limitations in `documentation_providers`; do not publish them as generic facts.", "4. Keep live provider-state and instance-specific limitations in `documentation_providers`; do not publish them as generic facts.",
"5. Add a versioned localization contract for structured metadata such as steps, fields, limitations, and verification; these values currently retain their manifest source language.", "5. Adopt the versioned structured-localization contract for steps, fields, limitations, consequences, and verification; the coverage column tracks this migration independently from title/body completeness.",
"", "",
"## Module gaps", "## Module gaps",
"", "",
"| Module | Topics | Missing German | Missing coverage |", "| Module | Topics | Missing German | Structured German | Missing coverage |",
"| --- | ---: | ---: | --- |", "| --- | ---: | ---: | ---: | --- |",
] ]
gaps_by_id = {str(item.get("module_id")): item for item in gaps} gaps_by_id = {str(item.get("module_id")): item for item in gaps}
for module in modules: for module in modules:
@@ -117,7 +119,9 @@ def documentation_coverage_markdown(catalog: Mapping[str, Any]) -> str:
missing = ", ".join(str(item) for item in gap.get("missing", ())) or "-" missing = ", ".join(str(item) for item in gap.get("missing", ())) or "-"
lines.append( lines.append(
f"| `{module_id}` | {coverage.get('topic_count', 0)} | " f"| `{module_id}` | {coverage.get('topic_count', 0)} | "
f"{coverage.get('missing_german_topic_count', 0)} | {missing} |" f"{coverage.get('missing_german_topic_count', 0)} | "
f"{coverage.get('german_structured_complete_topic_count', 0)}/"
f"{coverage.get('structured_localizable_topic_count', 0)} | {missing} |"
) )
lines.extend(("", "Generated file. Edit module manifests, then regenerate this report.", "")) lines.extend(("", "Generated file. Edit module manifests, then regenerate this report.", ""))
return "\n".join(lines) return "\n".join(lines)
@@ -238,6 +242,7 @@ def _topic_payload(module_id: str, topic: DocumentationTopic) -> dict[str, Any]:
if key != "kind" if key != "kind"
} }
), ),
**_structured_content_payload(topic),
} }
@@ -258,6 +263,36 @@ def _localized_topic(topic: DocumentationTopic, locale: str) -> dict[str, Any]:
} }
def _localized_content(topic: DocumentationTopic, locale: str) -> dict[str, Any]:
localizable_keys = localizable_documentation_metadata_keys(topic)
translation = topic.structured_translations.get(locale, {})
translated_fields = sorted(set(localizable_keys).intersection(translation))
complete = locale == "en" or not localizable_keys or (
topic.structured_translation_version
== DOCUMENTATION_STRUCTURED_TRANSLATION_VERSION
and len(translated_fields) == len(localizable_keys)
)
return {
"content": _public_value(dict(translation)),
"source_locale": locale if translated_fields else "en",
"translated_fields": translated_fields,
"complete": complete,
}
def _structured_content_payload(topic: DocumentationTopic) -> dict[str, Any]:
if not localizable_documentation_metadata_keys(topic):
return {}
return {
"structured_translation_version": topic.structured_translation_version,
"content_localizations": {
locale: _localized_content(topic, locale)
for locale in SUPPORTED_LOCALES
if locale != "en"
},
}
def _module_coverage( def _module_coverage(
manifest: ModuleManifest, manifest: ModuleManifest,
topics: Sequence[Mapping[str, Any]], topics: Sequence[Mapping[str, Any]],
@@ -270,6 +305,21 @@ def _module_coverage(
for topic in topics for topic in topics
if not _mapping(_mapping(topic.get("localizations")).get("de")).get("complete") if not _mapping(_mapping(topic.get("localizations")).get("de")).get("complete")
] ]
structured_localizable = [
topic
for topic in manifest.documentation
if localizable_documentation_metadata_keys(topic)
]
german_structured_complete = [
topic
for topic in structured_localizable
if _localized_content(topic, "de")["complete"]
]
missing_german_structured = [
topic.id
for topic in structured_localizable
if not _localized_content(topic, "de")["complete"]
]
missing: list[str] = [] missing: list[str] = []
if not has_user: if not has_user:
missing.append("user documentation") missing.append("user documentation")
@@ -291,6 +341,15 @@ def _module_coverage(
), ),
"missing_german_topic_count": len(missing_german), "missing_german_topic_count": len(missing_german),
"missing_german_topic_ids": missing_german, "missing_german_topic_ids": missing_german,
"structured_localizable_topic_count": len(structured_localizable),
"structured_translation_contract_topic_count": sum(
topic.structured_translation_version
== DOCUMENTATION_STRUCTURED_TRANSLATION_VERSION
for topic in structured_localizable
),
"german_structured_complete_topic_count": len(german_structured_complete),
"missing_german_structured_topic_count": len(missing_german_structured),
"missing_german_structured_topic_ids": missing_german_structured,
"kinds": sorted(kinds), "kinds": sorted(kinds),
"missing": missing, "missing": missing,
} }
@@ -314,6 +373,30 @@ def _coverage_summary(modules: Sequence[Mapping[str, Any]]) -> dict[str, Any]:
"module_count": len(modules), "module_count": len(modules),
"topic_count": topic_count, "topic_count": topic_count,
"german_complete_topic_count": topic_count - missing_german, "german_complete_topic_count": topic_count - missing_german,
"structured_localizable_topic_count": sum(
int(
_mapping(module.get("coverage")).get(
"structured_localizable_topic_count", 0
)
)
for module in modules
),
"structured_translation_contract_topic_count": sum(
int(
_mapping(module.get("coverage")).get(
"structured_translation_contract_topic_count", 0
)
)
for module in modules
),
"german_structured_complete_topic_count": sum(
int(
_mapping(module.get("coverage")).get(
"german_structured_complete_topic_count", 0
)
)
for module in modules
),
"runtime_provider_module_count": sum( "runtime_provider_module_count": sum(
int(module.get("runtime_documentation_provider_count", 0)) > 0 int(module.get("runtime_documentation_provider_count", 0)) > 0
for module in modules for module in modules
@@ -323,6 +406,79 @@ def _coverage_summary(modules: Sequence[Mapping[str, Any]]) -> dict[str, Any]:
} }
def coverage_baseline(catalog: Mapping[str, Any]) -> dict[str, Any]:
"""Build the monotonic public-documentation coverage baseline."""
summary = _mapping(catalog.get("summary"))
topic_count = int(summary.get("topic_count", 0))
german_complete = int(summary.get("german_complete_topic_count", 0))
structured_localizable = int(
summary.get("structured_localizable_topic_count", 0)
)
structured_complete = int(
summary.get("german_structured_complete_topic_count", 0)
)
return {
"schema_version": "1",
"minimum": {
"german_complete_topic_count": german_complete,
"structured_translation_contract_topic_count": int(
summary.get("structured_translation_contract_topic_count", 0)
),
"german_structured_complete_topic_count": structured_complete,
},
"maximum": {
"missing_german_topic_count": topic_count - german_complete,
"gap_module_count": int(summary.get("gap_module_count", 0)),
"missing_german_structured_topic_count": (
structured_localizable - structured_complete
),
},
}
def coverage_regression_issues(
catalog: Mapping[str, Any], baseline: Mapping[str, Any]
) -> tuple[str, ...]:
"""Return monotonic coverage failures against a reviewed baseline."""
if str(baseline.get("schema_version")) != "1":
return ("unsupported coverage baseline schema_version",)
summary = _mapping(catalog.get("summary"))
minimum = _mapping(baseline.get("minimum"))
maximum = _mapping(baseline.get("maximum"))
actual = {
"german_complete_topic_count": int(
summary.get("german_complete_topic_count", 0)
),
"structured_translation_contract_topic_count": int(
summary.get("structured_translation_contract_topic_count", 0)
),
"german_structured_complete_topic_count": int(
summary.get("german_structured_complete_topic_count", 0)
),
"missing_german_topic_count": int(summary.get("topic_count", 0))
- int(summary.get("german_complete_topic_count", 0)),
"gap_module_count": int(summary.get("gap_module_count", 0)),
"missing_german_structured_topic_count": int(
summary.get("structured_localizable_topic_count", 0)
)
- int(summary.get("german_structured_complete_topic_count", 0)),
}
issues: list[str] = []
for key, expected in minimum.items():
if key in actual and actual[key] < int(expected):
issues.append(
f"coverage {key} regressed: {actual[key]} is below {int(expected)}"
)
for key, expected in maximum.items():
if key in actual and actual[key] > int(expected):
issues.append(
f"coverage {key} regressed: {actual[key]} exceeds {int(expected)}"
)
return tuple(issues)
def _topic_kind(topic: DocumentationTopic) -> str: def _topic_kind(topic: DocumentationTopic) -> str:
raw = topic.metadata.get("kind") raw = topic.metadata.get("kind")
if isinstance(raw, str): if isinstance(raw, str):
@@ -394,6 +550,7 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument("--output", type=Path, required=True) parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--coverage-output", type=Path) parser.add_argument("--coverage-output", type=Path)
parser.add_argument("--workspace-root", type=Path) parser.add_argument("--workspace-root", type=Path)
parser.add_argument("--coverage-baseline", type=Path)
parser.add_argument( parser.add_argument(
"--check", "--check",
action="store_true", action="store_true",
@@ -408,10 +565,22 @@ def main(argv: Iterable[str] | None = None) -> int:
collect_manifest_sources(workspace_root=args.workspace_root) collect_manifest_sources(workspace_root=args.workspace_root)
) )
if args.check: if args.check:
if catalog_matches_sources(args.output, catalog): failed = False
return 0 if not catalog_matches_sources(args.output, catalog):
print(f"Public documentation catalog is stale: {args.output}", file=sys.stderr) print(f"Public documentation catalog is stale: {args.output}", file=sys.stderr)
return 1 failed = True
if args.coverage_baseline is not None:
try:
baseline = json.loads(
args.coverage_baseline.read_text(encoding="utf-8")
)
except (OSError, json.JSONDecodeError) as exc:
print(f"Invalid coverage baseline: {exc}", file=sys.stderr)
return 1
for issue in coverage_regression_issues(catalog, _mapping(baseline)):
print(f"Public documentation {issue}", file=sys.stderr)
failed = True
return int(failed)
write_public_catalog( write_public_catalog(
args.output, args.output,
catalog, catalog,
+44
View File
@@ -52,6 +52,50 @@ class FakePrincipal:
class DocsContextTests(unittest.TestCase): class DocsContextTests(unittest.TestCase):
def test_structured_topic_metadata_uses_requested_locale(self) -> None:
registry = PlatformRegistry()
registry.register(
ModuleManifest(
id="example",
name="Example",
version="1.0.0",
documentation=(
DocumentationTopic(
id="example.reference.localized",
title="Reference",
summary="Reference summary.",
documentation_types=("admin",),
metadata={
"kind": "reference",
"limitations": ["English limitation."],
},
structured_translation_version="1",
structured_translations={
"de": {
"limitations": ["Deutsche Einschränkung."]
}
},
),
),
)
)
layers = _classify_documentation(
registry,
FakePrincipal({"docs:documentation:read"}),
settings=None,
session=None,
documentation_type="admin",
locale="de-DE",
)
topic = layers["configured"][0]
self.assertEqual("de", topic["structured_translation_locale"])
self.assertEqual("1", topic["structured_translation_version"])
self.assertEqual(
["Deutsche Einschränkung."], topic["metadata"]["limitations"]
)
def test_user_provider_state_omits_binding_details(self) -> None: def test_user_provider_state_omits_binding_details(self) -> None:
state = { state = {
"configured": True, "configured": True,
+78 -2
View File
@@ -11,6 +11,8 @@ from govoplan_docs.public_export import (
ManifestSource, ManifestSource,
build_public_catalog, build_public_catalog,
catalog_matches_sources, catalog_matches_sources,
coverage_baseline,
coverage_regression_issues,
documentation_coverage_markdown, documentation_coverage_markdown,
write_public_catalog, write_public_catalog,
) )
@@ -25,6 +27,13 @@ class PublicDocumentationExportTests(unittest.TestCase):
self.assertEqual(9, module["coverage"]["topic_count"]) self.assertEqual(9, module["coverage"]["topic_count"])
self.assertEqual(0, module["coverage"]["missing_german_topic_count"]) self.assertEqual(0, module["coverage"]["missing_german_topic_count"])
self.assertEqual([], module["coverage"]["missing_german_topic_ids"]) self.assertEqual([], module["coverage"]["missing_german_topic_ids"])
self.assertEqual(2, module["coverage"]["structured_localizable_topic_count"])
self.assertEqual(
2, module["coverage"]["german_structured_complete_topic_count"]
)
self.assertEqual(
[], module["coverage"]["missing_german_structured_topic_ids"]
)
self.assertEqual([], module["coverage"]["missing"]) self.assertEqual([], module["coverage"]["missing"])
def test_catalog_projects_localized_manifest_topics_and_gaps(self) -> None: def test_catalog_projects_localized_manifest_topics_and_gaps(self) -> None:
@@ -39,7 +48,11 @@ class PublicDocumentationExportTests(unittest.TestCase):
summary="Perform the example workflow.", summary="Perform the example workflow.",
body="Open and finish it.", body="Open and finish it.",
documentation_types=("user", "admin"), documentation_types=("user", "admin"),
metadata={"kind": "workflow"}, metadata={
"kind": "workflow",
"steps": ["Review", "Execute"],
"verification": "Confirm completion.",
},
translations={ translations={
"de": { "de": {
"title": "Beispiel verwenden", "title": "Beispiel verwenden",
@@ -47,6 +60,13 @@ class PublicDocumentationExportTests(unittest.TestCase):
"body": "Öffnen und abschließen.", "body": "Öffnen und abschließen.",
} }
}, },
structured_translation_version="1",
structured_translations={
"de": {
"steps": ["Prüfen", "Ausführen"],
"verification": "Den Abschluss bestätigen.",
}
},
), ),
), ),
) )
@@ -59,7 +79,12 @@ class PublicDocumentationExportTests(unittest.TestCase):
self.assertEqual("Beispiel verwenden", topic["localizations"]["de"]["title"]) self.assertEqual("Beispiel verwenden", topic["localizations"]["de"]["title"])
self.assertTrue(topic["localizations"]["de"]["complete"]) self.assertTrue(topic["localizations"]["de"]["complete"])
self.assertEqual("workflow", topic["kind"]) self.assertEqual("workflow", topic["kind"])
self.assertEqual({}, topic["content"]) self.assertEqual(["Review", "Execute"], topic["content"]["steps"])
self.assertEqual(
["Prüfen", "Ausführen"],
topic["content_localizations"]["de"]["content"]["steps"],
)
self.assertTrue(topic["content_localizations"]["de"]["complete"])
self.assertIn( self.assertIn(
"field/consequence reference", "field/consequence reference",
catalog["modules"][0]["coverage"]["missing"], catalog["modules"][0]["coverage"]["missing"],
@@ -159,6 +184,57 @@ class PublicDocumentationExportTests(unittest.TestCase):
self.assertEqual("operator-workflow", topic["kind"]) self.assertEqual("operator-workflow", topic["kind"])
def test_coverage_baseline_rejects_text_and_structured_regressions(self) -> None:
healthy = build_public_catalog(
(
ManifestSource(
ModuleManifest(
id="example",
name="Example",
version="1.0.0",
documentation=(
DocumentationTopic(
id="example.reference",
title="Reference",
summary="Reference summary.",
body="Reference body.",
translations={
"de": {
"title": "Referenz",
"summary": "Referenzzusammenfassung.",
"body": "Referenzinhalt.",
}
},
metadata={
"kind": "reference",
"limitations": ["Source limitation."],
},
structured_translation_version="1",
structured_translations={
"de": {
"limitations": [
"Einschränkung der Quelle."
]
}
},
),
),
)
),
)
)
baseline = coverage_baseline(healthy)
self.assertEqual((), coverage_regression_issues(healthy, baseline))
regressed = json.loads(json.dumps(healthy))
regressed["summary"]["german_complete_topic_count"] = 0
regressed["summary"]["german_structured_complete_topic_count"] = 0
issues = coverage_regression_issues(regressed, baseline)
self.assertTrue(any("german_complete_topic_count" in issue for issue in issues))
self.assertTrue(
any("german_structured_complete_topic_count" in issue for issue in issues)
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/docs-webui", "name": "@govoplan/docs-webui",
"version": "0.1.20", "version": "0.1.21",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+2
View File
@@ -166,6 +166,8 @@ export type DocsDocumentationTopic = {
i18n_key: string; i18n_key: string;
locale: string; locale: string;
translation_locale: string; translation_locale: string;
structured_translation_locale: string;
structured_translation_version?: string | null;
version: { version: {
resolved: string; resolved: string;
minimum?: string | null; minimum?: string | null;