16 Commits
Author SHA1 Message Date
zemion 8cbab781bf Release v0.1.16
Module Package Release / publish-packages (push) Successful in 12s
2026-08-05 19:52:06 +02:00
zemion 2d18ecc9b6 feat: document contextual and temporal help 2026-08-05 00:03:32 +02:00
zemion 1f50505305 Release v0.1.15
Module Package Release / publish-packages (push) Successful in 11s
2026-08-04 15:18:12 +02:00
zemion 6c9ff55d27 Make package publication retries hash-safe 2026-08-04 14:32:19 +02:00
zemion 64df8b0032 Harden module package publication 2026-08-04 14:02:40 +02:00
zemion 1a9e83ce0c Add protected package release workflow 2026-08-04 04:14:04 +02:00
zemion abe2f78fc3 Document contextual help link contract 2026-08-03 07:35:42 +02:00
zemion 6607a3eeae feat: explain configured institutional architecture 2026-08-01 17:48:31 +02:00
zemion 0d8a49c8af Add version-aware documentation resolution 2026-07-31 22:48:07 +02:00
zemion c1ea7bb8f1 refactor: target workflow engine runtime 2026-07-31 16:59:22 +02:00
zemion e2bf104c53 feat: expose capability and catalog documentation 2026-07-31 04:21:34 +02:00
zemion af18f072d8 Align docs WebUI runtime dependencies 2026-07-31 02:48:56 +02:00
zemion da15645dba feat: complete governed documentation sources 2026-07-30 14:26:43 +02:00
zemion 511930ea56 perf: bound documentation route assembly 2026-07-29 14:16:29 +02:00
zemion 4d36ab2747 docs: update GovOPlaN repository links 2026-07-27 15:46:51 +02:00
zemion 94c000f351 fix(docs): fail closed on unscoped workflows 2026-07-22 01:55:54 +02:00
17 changed files with 2866 additions and 156 deletions
+270
View File
@@ -0,0 +1,270 @@
name: Module Package Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
release_tag:
description: Existing protected version tag to publish
required: true
type: string
jobs:
publish-packages:
runs-on: ubuntu-latest
env:
GITEA_REPOSITORY: ${{ gitea.repository }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
with:
python-version: "3.12"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: "22"
- name: Select and validate protected release tag
shell: bash
env:
REQUESTED_TAG: ${{ inputs.release_tag }}
TRIGGER_TAG: ${{ gitea.ref_name }}
run: |
set -euo pipefail
tag="${REQUESTED_TAG:-$TRIGGER_TAG}"
case "$tag" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "Release tag must start with a SemVer-shaped vX.Y.Z value" >&2; exit 1 ;;
esac
git fetch --force origin "refs/tags/$tag:refs/tags/$tag" refs/heads/main:refs/remotes/origin/main
tag_commit="$(git rev-list -n 1 "$tag")"
git merge-base --is-ancestor "$tag_commit" refs/remotes/origin/main || {
echo "Release tag is not contained in main" >&2
exit 1
}
git checkout --detach "$tag"
printf 'RELEASE_TAG=%s\n' "$tag" >> "$GITEA_ENV"
printf 'SOURCE_DATE_EPOCH=%s\n' "$(git show -s --format=%ct HEAD)" >> "$GITEA_ENV"
- name: Validate package versions
run: |
python - <<'PY'
import json
from pathlib import Path
import os
import re
import tomllib
tag = os.environ["RELEASE_TAG"]
expected = tag.removeprefix("v")
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]
if project.get("version") != expected:
raise SystemExit(f"pyproject version {project.get('version')!r} does not match {tag}")
if re.fullmatch(r"govoplan-[a-z0-9-]+", str(project.get("name", ""))) is None:
raise SystemExit("Python distribution name must use the govoplan-* namespace")
webui = Path("webui/package.json")
if webui.is_file():
package = json.loads(webui.read_text(encoding="utf-8"))
if package.get("version") != expected:
raise SystemExit(f"WebUI version {package.get('version')!r} does not match {tag}")
if re.fullmatch(r"@govoplan/[a-z0-9-]+-webui", str(package.get("name", ""))) is None:
raise SystemExit("WebUI package name must use the @govoplan/*-webui namespace")
release = Path("webui/package.release.json")
if release.is_file():
release_package = json.loads(release.read_text(encoding="utf-8"))
if (
release_package.get("name") != package.get("name")
or release_package.get("version") != expected
):
raise SystemExit("WebUI release package identity does not match package.json and the release tag")
PY
- name: Build immutable package artifacts
shell: bash
run: |
set -euo pipefail
python -m pip install --disable-pip-version-check build==1.5.0 twine==7.0.0
rm -rf dist .package-webui
python -m build --wheel --outdir dist
python -m twine check dist/*.whl
if [[ -f webui/package.json ]]; then
mkdir .package-webui
cp -a webui/. .package-webui/
rm -rf .package-webui/node_modules .package-webui/dist
if [[ -f .package-webui/package.release.json ]]; then
cp .package-webui/package.release.json .package-webui/package.json
fi
node <<'NODE'
const fs = require("node:fs");
const path = ".package-webui/package.json";
const packageJson = JSON.parse(fs.readFileSync(path, "utf8"));
const groups = ["dependencies", "optionalDependencies", "peerDependencies"];
for (const group of groups) {
for (const [name, specifier] of Object.entries(packageJson[group] || {})) {
if (!name.startsWith("@govoplan/")) continue;
if (typeof specifier !== "string") {
throw new Error(`${group}.${name} must use a string version`);
}
const packageSlug = name.slice("@govoplan/".length);
if (!packageSlug.endsWith("-webui")) {
throw new Error(`${group}.${name} is outside the WebUI package namespace`);
}
const repository = `govoplan-${packageSlug.slice(0, -"-webui".length)}`;
const escapedRepository = repository.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const gitTag = specifier.match(
new RegExp(
`^git\\+(?:ssh://git@|https://)git\\.add-ideas\\.de/(?:GovOPlaN|add-ideas)/${escapedRepository}\\.git#v([0-9]+\\.[0-9]+\\.[0-9]+)$`,
),
);
if (gitTag) {
packageJson[group][name] = gitTag[1];
continue;
}
if (specifier.startsWith("file:") || specifier.startsWith("git+")) {
throw new Error(
`${group}.${name} must resolve to an exact registry version for publication`,
);
}
}
}
delete packageJson.private;
fs.writeFileSync(path, `${JSON.stringify(packageJson, null, 2)}\n`);
NODE
npm pkg delete private --prefix .package-webui
(cd .package-webui && npm pack --ignore-scripts --pack-destination ../dist)
fi
python - <<'PY'
import hashlib
import json
from pathlib import Path
import os
import subprocess
artifacts = []
for path in sorted(Path("dist").iterdir()):
if path.suffix not in {".whl", ".tgz"}:
continue
digest = hashlib.sha256(path.read_bytes()).hexdigest()
artifacts.append({"filename": path.name, "sha256": digest, "size": path.stat().st_size})
payload = {
"schema_version": "1",
"repository": os.environ["GITEA_REPOSITORY"],
"tag": os.environ["RELEASE_TAG"],
"commit": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
"artifacts": artifacts,
}
Path("dist/package-artifacts.json").write_text(
json.dumps(payload, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
PY
- name: Retain package hash evidence
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32
with:
name: module-packages-${{ gitea.ref_name }}
path: dist/package-artifacts.json
- name: Check immutable registry state
shell: bash
env:
PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
run: |
set -euo pipefail
test -n "$PACKAGE_TOKEN"
python - <<'PY'
import hashlib
import json
import os
from pathlib import Path
import tomllib
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
api_root = "https://git.add-ideas.de/api/v1/packages/GovOPlaN"
token = os.environ["PACKAGE_TOKEN"]
def should_publish(kind, name, version, path):
package_url = "/".join(
(api_root, kind, quote(name, safe=""), quote(version, safe=""), "files")
)
request = Request(
package_url,
headers={"Accept": "application/json", "Authorization": f"token {token}"},
)
try:
with urlopen(request, timeout=30) as response:
files = json.load(response)
except HTTPError as exc:
if exc.code == 404:
print(f"{kind} package {name}=={version} is not published yet")
return True
raise
if not isinstance(files, list) or len(files) != 1:
raise SystemExit(
f"immutable {kind} package {name}=={version} has an unexpected file set"
)
expected_sha256 = hashlib.sha256(path.read_bytes()).hexdigest()
if files[0].get("sha256") != expected_sha256:
raise SystemExit(
f"immutable {kind} package {name}=={version} already exists with a different SHA-256"
)
print(f"verified existing {kind} package {name}=={version} ({expected_sha256})")
return False
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]
wheels = tuple(Path("dist").glob("*.whl"))
if len(wheels) != 1:
raise SystemExit("release build must contain exactly one wheel")
publish_pypi = should_publish(
"pypi", str(project["name"]), str(project["version"]), wheels[0]
)
tarballs = tuple(Path("dist").glob("*.tgz"))
if len(tarballs) > 1:
raise SystemExit("release build must contain at most one npm package")
publish_npm = False
if tarballs:
webui = json.loads(
Path(".package-webui/package.json").read_text(encoding="utf-8")
)
publish_npm = should_publish(
"npm", str(webui["name"]), str(webui["version"]), tarballs[0]
)
with Path(os.environ["GITEA_ENV"]).open("a", encoding="utf-8") as env_file:
env_file.write(f"PUBLISH_PYPI={int(publish_pypi)}\n")
env_file.write(f"PUBLISH_NPM={int(publish_npm)}\n")
PY
- name: Publish wheel and WebUI package
shell: bash
env:
PACKAGE_USERNAME: ${{ secrets.GOVOPLAN_PACKAGE_USERNAME }}
PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
run: |
set -euo pipefail
test -n "$PACKAGE_USERNAME"
test -n "$PACKAGE_TOKEN"
if [[ "$PUBLISH_PYPI" == 1 ]]; then
TWINE_USERNAME="$PACKAGE_USERNAME" TWINE_PASSWORD="$PACKAGE_TOKEN" \
python -m twine upload --non-interactive \
--repository-url https://git.add-ideas.de/api/packages/GovOPlaN/pypi \
dist/*.whl
else
echo "Exact wheel is already present; skipping immutable retry."
fi
shopt -s nullglob
webui_packages=(dist/*.tgz)
if (( ${#webui_packages[@]} )) && [[ "$PUBLISH_NPM" == 1 ]]; then
npmrc="$(mktemp)"
trap 'rm -f "$npmrc"' EXIT
chmod 600 "$npmrc"
printf '%s\n' \
'@govoplan:registry=https://git.add-ideas.de/api/packages/GovOPlaN/npm/' \
"//git.add-ideas.de/api/packages/GovOPlaN/npm/:_authToken=$PACKAGE_TOKEN" \
> "$npmrc"
NPM_CONFIG_USERCONFIG="$npmrc" npm publish "./${webui_packages[0]}" \
--ignore-scripts --access public \
--registry https://git.add-ideas.de/api/packages/GovOPlaN/npm/
elif (( ${#webui_packages[@]} )); then
echo "Exact WebUI package is already present; skipping immutable retry."
fi
+2
View File
@@ -27,5 +27,7 @@ tools/checks/check-focused.sh
- Keep documentation-layer behavior in this module, not core. - Keep documentation-layer behavior in this module, not core.
- Read configuration, module manifests, routes, permissions, and capability metadata through core contracts. - Read configuration, module manifests, routes, permissions, and capability metadata through core contracts.
- Do not import feature-module internals. Modules should contribute documentation metadata through manifests, capabilities, generated docs, or typed DTOs. - Do not import feature-module internals. Modules should contribute documentation metadata through manifests, capabilities, generated docs, or typed DTOs.
- Treat documentation as part of every behavior change. Update the owning module's manifest-driven `DocumentationTopic` contributions for each affected user and administrator workflow, setting, permission, limitation, and operational consequence.
- Require every module manifest to provide a static user and administrator documentation baseline, even when configured-state details come from `documentation_providers`. Validate the complete workspace with `/mnt/DATA/git/govoplan/tools/checks/check-manifest-shapes.py`.
- Prefer role-aware and configuration-aware documentation over global manuals. - Prefer role-aware and configuration-aware documentation over global manuals.
- Keep active backlog state in Gitea issues; keep durable context in repository docs and synced wiki pages. - Keep active backlog state in Gitea issues; keep durable context in repository docs and synced wiki pages.
+56
View File
@@ -55,8 +55,64 @@ 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/`.
Every module manifest must contribute a static documentation baseline for both
the `user` and `admin` projections through `ModuleManifest.documentation`.
Runtime providers may add actor- and configuration-specific detail, but they do
not replace that baseline. A behavior change is complete only when the owning
module updates the affected workflows, settings, permissions, limitations, and
operational consequences. Validate workspace coverage with:
```sh
cd /mnt/DATA/git/govoplan
./tools/checks/check-manifest-shapes.py
```
Feature content remains in the owning module. Docs indexes and renders the
contributions without importing feature implementations.
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
context together with `fallback_context` and `module`; Docs selects the first
visible exact, page-level, or module-level topic after applying audience and
permission filtering. Modules announce exact route and control identities in
static topic `metadata.help_contexts`.
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.
Module-owned external-provider runtime state is also projected through the
Core contract. Administrative documentation may show sanitized binding-level
state; ordinary-user documentation receives only aggregate configured, active,
authority, health, freshness, conflict, recovery, and observation fields.
URLs, credential references, provider error text, and binding identifiers are
excluded from the user projection.
## 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.
- `docs/INSTANCE_AWARE_DOCUMENTATION.md` defines the runtime context and condition model. - `docs/INSTANCE_AWARE_DOCUMENTATION.md` defines the runtime context and condition model.
- `docs/DOCUMENTATION_EXPERIENCE_CONCEPT.md` defines the workflow, structure/reference, and design-pattern documentation experience. - `docs/DOCUMENTATION_EXPERIENCE_CONCEPT.md` defines the workflow, structure/reference, and design-pattern documentation experience.
- Core's `DocumentationHelpLink` is the stable route/field/blocker entry point
into a topic or help context, with hosted fallback when this module is not
active.
+13 -1
View File
@@ -26,7 +26,7 @@ It should not start from a full product manual. The current tenant, installed
modules, enabled routes, permissions, and configuration decide what is shown as modules, enabled routes, permissions, and configuration decide what is shown as
the default path. the default path.
Tracking issue: `add-ideas/govoplan-docs#15`. Tracking issue: `GovOPlaN/govoplan-docs#15`.
## Editorial Pillars ## Editorial Pillars
@@ -184,6 +184,15 @@ Each topic should support stable anchors and related-topic links:
- pattern topics link to examples and the components that implement them - pattern topics link to examples and the components that implement them
- unavailable topics link to the blocker and the actor who can resolve it - unavailable topics link to the blocker and the actor who can resolve it
Core exposes `DocumentationHelpLink` for those entry points. A caller supplies
either a stable topic id (`/docs?type=user&topic=...`) or help-context id
(`/docs?type=user&context=...`), plus an optional topic anchor. Route help uses
the context form; field labels and blocker callouts can use either form. The
component opens the configured Help Center when Docs is active and falls back
to the hosted documentation site when the optional module is absent. Modules
must reference topic/context ids, not reproduce Docs routing or inspect another
module's implementation.
## Source Contract Direction ## Source Contract Direction
The current `DocumentationTopic` contract can carry the first version through The current `DocumentationTopic` contract can carry the first version through
@@ -255,6 +264,9 @@ Workflow topics:
- keep each step actionable - keep each step actionable
- mention blockers where the user would encounter them - mention blockers where the user would encounter them
- link to field/reference topics instead of repeating field tables - link to field/reference topics instead of repeating field tables
- for every user-facing workflow, declare one or more conditions and put
`required_scopes` or `any_scopes` on every condition alternative; the release
gate rejects an unscoped alternative
Reference topics: Reference topics:
+19
View File
@@ -93,6 +93,25 @@ register durable topics directly in its `ModuleManifest.documentation` tuple.
Use this for stable explanations such as the module purpose, common workflows, Use this for stable explanations such as the module purpose, common workflows,
policy hierarchy, route meaning, and links to public docs or repository docs. policy hierarchy, route meaning, and links to public docs or repository docs.
Every manifest must retain at least one static topic for each of the `user` and
`admin` projections. A shared topic may serve both only when its language and
disclosure level are appropriate for both audiences. A module that is still a
seed should state that limitation plainly rather than documenting an unfinished
screen as available. `documentation_providers` enrich this baseline; they do
not replace it because a provider may be unavailable before configuration or
database access succeeds.
Documentation is part of a behavior change's completion criteria. The owning
module updates affected workflows, fields and settings, permissions, optional
integration behavior, failure or limitation explanations, and operator
consequences in the same change. The workspace manifest-shape check enforces
the static audience baseline:
```sh
cd /mnt/DATA/git/govoplan
./tools/checks/check-manifest-shapes.py
```
When the text depends on active configuration, a module registers a provider in When the text depends on active configuration, a module registers a provider in
`ModuleManifest.documentation_providers`. The provider receives a `ModuleManifest.documentation_providers`. The provider receives a
`DocumentationContext` with the active registry, principal, settings, and a `DocumentationContext` with the active registry, principal, settings, and a
+8
View File
@@ -34,6 +34,14 @@ opens the user projection: each contributed workflow still needs its own
module, capability, permission, and runtime-policy conditions so baseline Help module, capability, permission, and runtime-policy conditions so baseline Help
Center access does not imply authority to perform every documented task. Center access does not imply authority to perform every documented task.
This permission binding is a strict source contract. Every user workflow topic
must declare at least one `DocumentationCondition`, and every alternative in
its `conditions` tuple must include `required_scopes` or `any_scopes`. Module,
capability, or configuration conditions alone are not sufficient because topic
conditions are alternatives: one unscoped alternative would bypass all scoped
ones. Manifest validation blocks a release containing such a topic, and Docs
omits a non-compliant runtime-provider topic from the user projection.
## Conditions ## Conditions
Documentation topics can declare: Documentation topics can declare:
+7 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/docs-webui", "name": "@govoplan/docs-webui",
"version": "0.1.10", "version": "0.1.16",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "webui/src/index.ts", "main": "webui/src/index.ts",
@@ -17,14 +17,14 @@
"README.md" "README.md"
], ],
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.10", "@govoplan/core-webui": "^0.1.16",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": "^19.0.0", "react": ">=19.2.7 <20",
"react-dom": "^19.0.0", "react-dom": ">=19.2.7 <20",
"react-router-dom": "^7.1.1", "react-router": ">=8.3.0 <9",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^5.2.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^6.0.6" "vite": "^7.3.6"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@govoplan/core-webui": { "@govoplan/core-webui": {
+3 -3
View File
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-docs" name = "govoplan-docs"
version = "0.1.10" version = "0.1.16"
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.10", "govoplan-core>=0.1.16",
"govoplan-access>=0.1.10", "govoplan-access>=0.1.16",
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
+1 -1
View File
@@ -2,4 +2,4 @@
__all__ = ["__version__"] __all__ = ["__version__"]
__version__ = "0.1.10" __version__ = "0.1.16"
File diff suppressed because it is too large Load Diff
+216 -14
View File
@@ -1,9 +1,13 @@
from __future__ import annotations from __future__ import annotations
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER from govoplan_core.core.access import (
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
)
from govoplan_core.core.modules import ( from govoplan_core.core.modules import (
DocumentationCondition, DocumentationCondition,
DocumentationLink, DocumentationLink,
DocumentationSourceDefinition,
DocumentationTopic, DocumentationTopic,
FrontendModule, FrontendModule,
FrontendRoute, FrontendRoute,
@@ -13,12 +17,48 @@ from govoplan_core.core.modules import (
PermissionDefinition, PermissionDefinition,
RoleTemplate, RoleTemplate,
) )
from govoplan_core.core.provider_governance import (
ModuleArchitectureDeclaration,
ModuleArchitectureDocumentation,
ModuleMaturityEvidence,
)
DOCS_READ_SCOPE = "docs:documentation:read" DOCS_READ_SCOPE = "docs:documentation:read"
DOCS_ADMIN_READ_SCOPE = "docs:documentation:admin" DOCS_ADMIN_READ_SCOPE = "docs:documentation:admin"
DOCS_ADMIN_READ_SCOPES = (DOCS_ADMIN_READ_SCOPE, "system:settings:read", "admin:settings:read") DOCS_ADMIN_READ_SCOPES = (
DOCS_ADMIN_READ_SCOPE,
"system:settings:read",
"admin:settings:read",
)
DOCS_READ_SCOPES = (DOCS_READ_SCOPE, *DOCS_ADMIN_READ_SCOPES) DOCS_READ_SCOPES = (DOCS_READ_SCOPE, *DOCS_ADMIN_READ_SCOPES)
ARCHITECTURE = ModuleArchitectureDeclaration(
layer="governance_accountability",
kind="presentation",
maturity="vertical_slice",
evidence=(
ModuleMaturityEvidence(
kind="test",
reference="tests/test_docs_context.py",
summary="Tests audience-safe configured documentation and architecture projections.",
),
ModuleMaturityEvidence(
kind="documentation",
reference="docs/DOCUMENTATION_LAYER_CONCEPT.md",
summary="Defines the manifest-driven documentation boundary.",
),
),
known_limits=(
"Architecture declarations are in staged adoption, so undeclared modules remain visible as pending.",
),
owned_concepts=("configured documentation projection", "documentation audience filtering"),
non_owned_concepts=("module feature behavior", "module evidence generation"),
documentation=ModuleArchitectureDocumentation(
security=("docs/DOCUMENTATION_LAYER_CONCEPT.md",),
operations=("docs/DOCUMENTATION_LAYER_CONCEPT.md",),
),
)
def _permission(scope: str, label: str, description: str) -> PermissionDefinition: def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
module_id, resource, action = scope.split(":", 2) module_id, resource, action = scope.split(":", 2)
@@ -44,9 +84,12 @@ def _route_factory(context: ModuleContext):
manifest = ModuleManifest( manifest = ModuleManifest(
id="docs", id="docs",
name="Docs", name="Docs",
version="0.1.10", version="0.1.16",
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), required_capabilities=(
optional_dependencies=("policy", "audit", "ops", "workflow", "search"), CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
),
optional_dependencies=("policy", "audit", "ops", "workflow_engine", "search"),
permissions=( permissions=(
_permission( _permission(
DOCS_READ_SCOPE, DOCS_READ_SCOPE,
@@ -75,12 +118,35 @@ manifest = ModuleManifest(
), ),
), ),
route_factory=_route_factory, route_factory=_route_factory,
nav_items=(NavItem(path="/docs", label="Docs", icon="reports", required_any=DOCS_READ_SCOPES, order=880),), nav_items=(
NavItem(
path="/docs",
label="Docs",
icon="reports",
required_any=DOCS_READ_SCOPES,
order=880,
),
),
frontend=FrontendModule( frontend=FrontendModule(
module_id="docs", module_id="docs",
package_name="@govoplan/docs-webui", package_name="@govoplan/docs-webui",
routes=(FrontendRoute(path="/docs", component="DocsPage", required_any=DOCS_READ_SCOPES, order=880),), routes=(
nav_items=(NavItem(path="/docs", label="Docs", icon="reports", required_any=DOCS_READ_SCOPES, order=880),), FrontendRoute(
path="/docs",
component="DocsPage",
required_any=DOCS_READ_SCOPES,
order=880,
),
),
nav_items=(
NavItem(
path="/docs",
label="Docs",
icon="reports",
required_any=DOCS_READ_SCOPES,
order=880,
),
),
), ),
documentation=( documentation=(
DocumentationTopic( DocumentationTopic(
@@ -119,6 +185,49 @@ manifest = ModuleManifest(
), ),
metadata={"kind": "system"}, metadata={"kind": "system"},
), ),
DocumentationTopic(
id="docs.reference.institutional-governance-architecture",
title="Institutional governance architecture",
summary="GovOPlaN models institutional responsibility, governed work, formal outcomes, evidence, and external-system authority without turning every concept into Core or one monolithic application.",
body=(
"Organizations, Identity, IDM, Access, and Policy answer different parts of who may act. "
"Mandate, service, procedure-party, and formal-decision semantics are being introduced as shared contracts and become modules only after independent lifecycle and reuse are proven. "
"External integrations separately declare technical maturity and whether GovOPlaN is authoritative, mirrors an external source, synchronizes under governance, adds an overlay, or retains only a link."
),
layer="always",
documentation_types=("admin",),
audience=("tenant_admin", "operator", "module_admin", "product_owner"),
order=15,
translations={
"de": {
"title": "Architektur der institutionellen Steuerung",
"summary": "GovOPlaN modelliert institutionelle Verantwortung, gesteuerte Arbeit, formale Ergebnisse, Nachweise und die Datenhoheit externer Systeme, ohne alle Begriffe in den Kern oder eine monolithische Anwendung zu ziehen.",
"body": "Organisationen, Identitaeten, IDM, Zugriff und Richtlinien beantworten unterschiedliche Teile der Frage, wer handeln darf. Mandate, Leistungen, Verfahrensbeteiligte und formale Entscheidungen beginnen als gemeinsame Vertraege und werden erst bei nachgewiesenem eigenstaendigem Lebenszyklus zu Modulen. Integrationen erklaeren technische Reife und Datenhoheit getrennt.",
},
},
links=(
DocumentationLink(
label="Institutional governance target architecture",
href="govoplan/docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md",
kind="repository",
),
DocumentationLink(
label="Core module architecture",
href="govoplan-core/docs/MODULE_ARCHITECTURE.md",
kind="repository",
),
),
metadata={
"kind": "reference",
"architecture_topics": [
"institutional context",
"module ownership",
"source authority",
"integration maturity",
"product packages",
],
},
),
DocumentationTopic( DocumentationTopic(
id="docs.pattern.field-help", id="docs.pattern.field-help",
title="Field help marker", title="Field help marker",
@@ -154,6 +263,69 @@ manifest = ModuleManifest(
], ],
}, },
), ),
DocumentationTopic(
id="docs.pattern.contextual-help",
title="Context-sensitive help",
summary="Press F1 on a page, field, action, or dialog to open help for the current interface context.",
body=(
"GovOPlaN first resolves help for the focused field or action, then its dialog or section, "
"the current page, and the owning module. Exact documentation is shown when available; "
"otherwise the page or module documentation is used. The titlebar help button opens the "
"current page context. Documentation remains filtered by the current account's audience, "
"permissions, and configured modules."
),
layer="always",
documentation_types=("admin", "user"),
audience=("user", "tenant_admin", "operator", "module_admin"),
order=21,
i18n_key="docs.topic.pattern.contextual_help",
links=(
DocumentationLink(
label="Contextual help contract",
href="govoplan-core/docs/CONTEXTUAL_HELP_CONTRACT.md",
kind="repository",
),
),
metadata={
"kind": "pattern",
"pattern_id": "contextual-f1-help",
"help_contexts": [
"core.contextual-help",
"core.titlebar.language",
],
"component_refs": [
"govoplan-core/webui/src/layout/HelpMenu.tsx",
"govoplan-core/webui/src/utils/helpContext.ts",
],
},
),
DocumentationTopic(
id="docs.reference.temporal-data-context",
title="Temporal data context",
summary="Choose whether pages show currently valid records, records valid at a selected time, or all valid-time states.",
body=(
"The titlebar calendar controls valid time across participating modules. Current is the neutral "
"default. At time selects records valid at the chosen instant, while All includes historical and "
"future valid-time states. Recorded time remains separate: it describes when the platform learned "
"or stored a fact. Permissions are evaluated now, so temporal selection never restores historical access."
),
layer="always",
documentation_types=("admin", "user"),
audience=("user", "tenant_admin", "operator", "module_admin"),
order=22,
i18n_key="docs.topic.reference.temporal_data_context",
links=(
DocumentationLink(
label="Temporal data read contract",
href="govoplan-core/docs/TEMPORAL_DATA_CONTEXT.md",
kind="repository",
),
),
metadata={
"kind": "reference",
"help_contexts": ["core.temporal-data-context"],
},
),
DocumentationTopic( DocumentationTopic(
id="docs.reference.organization-identity-idm-access-boundary", id="docs.reference.organization-identity-idm-access-boundary",
title="Organization, identity, IDM, and access boundary", title="Organization, identity, IDM, and access boundary",
@@ -169,30 +341,60 @@ manifest = ModuleManifest(
documentation_types=("admin", "user"), documentation_types=("admin", "user"),
audience=("tenant_admin", "access_admin", "operator", "user"), audience=("tenant_admin", "access_admin", "operator", "user"),
related_modules=("organizations", "identity", "idm", "access"), related_modules=("organizations", "identity", "idm", "access"),
order=21, order=23,
conditions=( conditions=(
DocumentationCondition( DocumentationCondition(
required_modules=("organizations", "identity", "idm", "access"), required_modules=("organizations", "identity", "idm", "access"),
), ),
), ),
links=( links=(
DocumentationLink(label="Organizations", href="/organizations", kind="runtime"), DocumentationLink(
label="Organizations", href="/organizations", kind="runtime"
),
DocumentationLink(label="IDM assignments", href="/idm", kind="runtime"), DocumentationLink(label="IDM assignments", href="/idm", kind="runtime"),
DocumentationLink(label="Access administration", href="/admin", kind="runtime"), DocumentationLink(
label="Access administration", href="/admin", kind="runtime"
),
), ),
metadata={ metadata={
"kind": "reference", "kind": "reference",
"admin_explanation": "Function-to-role effects are owned by Access. IDM assignment changes can be governed independently from organization model changes.", "admin_explanation": "Function-to-role effects are owned by Access. IDM assignment changes can be governed independently from organization model changes.",
"user_explanation": "A person can hold a function because IDM links their identity to the organization function. Access decides which application permissions that function gives.", "user_explanation": "A person can hold a function because IDM links their identity to the organization function. Access decides which application permissions that function gives.",
"module_boundaries": [ "module_boundaries": [
{"module": "organizations", "owns": "unit types, structures, relations, units, and function definitions"}, {
"module": "organizations",
"owns": "unit types, structures, relations, units, and function definitions",
},
{"module": "identity", "owns": "identities and account links"}, {"module": "identity", "owns": "identities and account links"},
{"module": "idm", "owns": "identity-to-function assignments, delegation, acting-for links, and synchronization mapping"}, {
{"module": "access", "owns": "roles, permissions, and accepted function-to-role mappings"}, "module": "idm",
"owns": "identity-to-function assignments, delegation, acting-for links, and synchronization mapping",
},
{
"module": "access",
"owns": "roles, permissions, and accepted function-to-role mappings",
},
], ],
}, },
), ),
), ),
documentation_sources=(
DocumentationSourceDefinition(
id="docs.project.wiki",
kind="wiki",
label="GovOPlaN Docs wiki",
provenance={"source": "gitea_wiki"},
link=DocumentationLink(
label="GovOPlaN Docs wiki",
href="https://git.add-ideas.de/GovOPlaN/govoplan-docs/wiki",
kind="wiki",
),
inspection={
"href": "https://git.add-ideas.de/GovOPlaN/govoplan-docs/wiki",
},
),
),
architecture=ARCHITECTURE,
) )
+616
View File
@@ -0,0 +1,616 @@
from __future__ import annotations
import hashlib
from dataclasses import dataclass
from typing import Annotated, Any, Literal, Mapping
from pydantic import BaseModel, ConfigDict, Field
from govoplan_core.core.modules import (
DocumentationCondition,
DocumentationSourceDefinition,
DocumentationType,
ModuleManifest,
)
from govoplan_core.security.redaction import redact_secret_values
class _SourceModel(BaseModel):
model_config = ConfigDict(extra="forbid")
class DocumentationSourceProvenance(_SourceModel):
source: str
version: str | None = None
revision: str | None = None
published_at: str | None = None
checksum: str | None = None
class DocumentationSourceVisibility(_SourceModel):
documentation_types: list[DocumentationType]
required_modules: list[str] = Field(default_factory=list)
any_modules: list[str] = Field(default_factory=list)
missing_modules: list[str] = Field(default_factory=list)
required_capabilities: list[str] = Field(default_factory=list)
required_scopes: list[str] = Field(default_factory=list)
any_scopes: list[str] = Field(default_factory=list)
configuration_keys: list[str] = Field(default_factory=list)
class ManifestInspection(_SourceModel):
kind: Literal["manifest"] = "manifest"
module_id: str
name: str
version: str
dependencies: list[str]
optional_dependencies: list[str]
class RouteInspection(_SourceModel):
kind: Literal["route"] = "route"
path: str
component: str | None = None
order: int
class CapabilityInspection(_SourceModel):
kind: Literal["capability"] = "capability"
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):
kind: Literal["policy"] = "policy"
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):
kind: Literal["configuration_package"] = "configuration_package"
package_id: str
name: str | None = None
version: str | None = None
schema_version: 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):
kind: Literal["wiki"] = "wiki"
href: str
published_at: str | None = None
revision: str | None = None
class RepositoryInspection(_SourceModel):
kind: Literal["repository"] = "repository"
href: str
revision: str | None = None
DocumentationSourceInspection = Annotated[
ManifestInspection
| RouteInspection
| CapabilityInspection
| PolicyInspection
| ReleaseCatalogInspection
| ConfigurationPackageInspection
| WikiInspection
| RepositoryInspection,
Field(discriminator="kind"),
]
class DocumentationSourceItem(_SourceModel):
id: str
kind: str
owner_module_id: str
label: str
state: Literal["configured", "disabled", "unavailable"]
state_reason: str | None = None
inspection_url: str
provenance: DocumentationSourceProvenance
visibility: DocumentationSourceVisibility
inspection: DocumentationSourceInspection
def summary(self) -> dict[str, Any]:
return self.model_dump(mode="json", exclude={"inspection"})
@dataclass(frozen=True, slots=True)
class RegisteredDocumentationSource:
item: DocumentationSourceItem
condition: DocumentationCondition
documentation_types: tuple[DocumentationType, ...]
configuration_key: str | None = None
class DocumentationSourceRegistry:
def __init__(self) -> None:
self._sources: dict[str, RegisteredDocumentationSource] = {}
def register(self, source: RegisteredDocumentationSource) -> None:
if source.item.id in self._sources:
raise ValueError(f"Duplicate documentation source id: {source.item.id}")
self._sources[source.item.id] = source
def sources(self) -> tuple[RegisteredDocumentationSource, ...]:
return tuple(
self._sources[source_id]
for source_id in sorted(self._sources)
)
def get(self, source_id: str) -> RegisteredDocumentationSource | None:
return self._sources.get(source_id)
def build_documentation_source_registry(
manifests: tuple[ModuleManifest, ...],
*,
include_runtime_catalogs: bool = True,
) -> DocumentationSourceRegistry:
registry = DocumentationSourceRegistry()
for manifest in manifests:
registry.register(_manifest_source(manifest))
for source in _route_sources(manifest):
registry.register(source)
for source in _capability_sources(manifest):
registry.register(source)
for definition in manifest.documentation_sources:
registry.register(_defined_source(manifest, definition))
for source in _linked_sources(manifest):
if registry.get(source.item.id) is None:
registry.register(source)
if include_runtime_catalogs:
for source in _catalog_sources():
registry.register(source)
return registry
def _manifest_source(manifest: ModuleManifest) -> RegisteredDocumentationSource:
return _registered_source(
source_id=f"{manifest.id}.manifest",
kind="manifest",
owner_module_id=manifest.id,
label=f"{manifest.name} module manifest",
provenance={"source": "module_manifest", "version": manifest.version},
inspection=ManifestInspection(
module_id=manifest.id,
name=manifest.name,
version=manifest.version,
dependencies=list(manifest.dependencies),
optional_dependencies=list(manifest.optional_dependencies),
),
)
def _route_sources(manifest: ModuleManifest) -> tuple[RegisteredDocumentationSource, ...]:
if manifest.frontend is None:
return ()
return tuple(
_registered_source(
source_id=_derived_source_id(manifest.id, "route", route.path),
kind="route",
owner_module_id=manifest.id,
label=f"{manifest.name} route {route.path}",
provenance={"source": "frontend_manifest", "version": manifest.version},
inspection=RouteInspection(
path=route.path,
component=route.component,
order=route.order,
),
condition=DocumentationCondition(
required_modules=(manifest.id,),
required_scopes=route.required_all,
any_scopes=route.required_any,
),
)
for route in manifest.frontend.routes
)
def _capability_sources(manifest: ModuleManifest) -> tuple[RegisteredDocumentationSource, ...]:
sources: list[RegisteredDocumentationSource] = []
for capability in sorted(manifest.capability_factories):
metadata = manifest.capability_documentation.get(capability)
is_policy = capability.startswith("policy.")
kind = "policy" if is_policy else "capability"
inspection: DocumentationSourceInspection = (
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
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(
source_id=_derived_source_id(manifest.id, kind, capability),
kind=kind,
owner_module_id=manifest.id,
label=metadata.label if metadata else f"{manifest.name} {kind} {capability}",
provenance={"source": "module_manifest", "version": manifest.version},
inspection=inspection,
condition=DocumentationCondition(required_modules=(manifest.id,)),
documentation_types=metadata.documentation_types if metadata else ("admin",),
))
return tuple(sources)
def _defined_source(
manifest: ModuleManifest,
definition: DocumentationSourceDefinition,
) -> RegisteredDocumentationSource:
inspection = _defined_inspection(definition)
return _registered_source(
source_id=definition.id,
kind=definition.kind,
owner_module_id=manifest.id,
label=definition.label,
provenance={
"source": str(definition.provenance.get("source") or "module_manifest"),
**_safe_source_fields(definition.provenance),
},
inspection=inspection,
condition=definition.condition,
documentation_types=definition.documentation_types,
state=definition.state,
state_reason=definition.state_reason,
configuration_key=definition.configuration_key,
)
def _defined_inspection(definition: DocumentationSourceDefinition) -> DocumentationSourceInspection:
safe = _safe_source_fields(definition.inspection)
if definition.kind == "configuration_package":
return ConfigurationPackageInspection(
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")),
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 "")
if definition.kind == "wiki":
return WikiInspection(
href=href,
published_at=_optional_text(safe.get("published_at")),
revision=_optional_text(safe.get("revision")),
)
if definition.kind == "repository":
return RepositoryInspection(
href=href,
revision=_optional_text(safe.get("revision")),
)
if definition.kind == "route":
return RouteInspection(
path=str(safe.get("path") or ""),
component=_optional_text(safe.get("component")),
order=int(safe.get("order") or 0),
)
if definition.kind == "policy":
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":
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(
module_id=str(safe.get("module_id") or definition.id.split(".", 1)[0]),
name=str(safe.get("name") or definition.label),
version=str(safe.get("version") or ""),
dependencies=_string_list(safe.get("dependencies")),
optional_dependencies=_string_list(safe.get("optional_dependencies")),
)
def _linked_sources(manifest: ModuleManifest) -> tuple[RegisteredDocumentationSource, ...]:
sources: list[RegisteredDocumentationSource] = []
for topic in manifest.documentation:
for link in topic.links:
if link.kind not in {"wiki", "repository"}:
continue
source_id = _derived_source_id(manifest.id, link.kind, link.href)
inspection: DocumentationSourceInspection = (
WikiInspection(href=link.href)
if link.kind == "wiki"
else RepositoryInspection(href=link.href)
)
sources.append(_registered_source(
source_id=source_id,
kind=link.kind,
owner_module_id=manifest.id,
label=link.label,
provenance={"source": "documentation_link", "version": manifest.version},
inspection=inspection,
condition=topic.conditions[0] if len(topic.conditions) == 1 else DocumentationCondition(),
documentation_types=topic.documentation_types,
))
return tuple(sources)
def _registered_source(
*,
source_id: str,
kind: str,
owner_module_id: str,
label: str,
provenance: Mapping[str, Any],
inspection: DocumentationSourceInspection,
condition: DocumentationCondition | None = None,
documentation_types: tuple[DocumentationType, ...] = ("admin",),
state: Literal["configured", "disabled", "unavailable"] = "configured",
state_reason: str | None = None,
configuration_key: str | None = None,
) -> RegisteredDocumentationSource:
clean_provenance = _safe_source_fields(provenance)
return RegisteredDocumentationSource(
item=DocumentationSourceItem(
id=source_id,
kind=kind,
owner_module_id=owner_module_id,
label=label,
state=state,
state_reason=state_reason,
inspection_url=f"/api/v1/docs/sources/{source_id}",
provenance=DocumentationSourceProvenance(
source=str(clean_provenance.get("source") or "unknown"),
version=_optional_text(clean_provenance.get("version")),
revision=_optional_text(clean_provenance.get("revision")),
published_at=_optional_text(clean_provenance.get("published_at")),
checksum=_optional_text(clean_provenance.get("checksum")),
),
visibility=_visibility_payload(condition or DocumentationCondition(), documentation_types),
inspection=inspection,
),
condition=condition or DocumentationCondition(),
documentation_types=documentation_types,
configuration_key=configuration_key,
)
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(
condition: DocumentationCondition,
documentation_types: tuple[DocumentationType, ...],
) -> DocumentationSourceVisibility:
return DocumentationSourceVisibility(
documentation_types=list(documentation_types),
required_modules=list(condition.required_modules),
any_modules=list(condition.any_modules),
missing_modules=list(condition.missing_modules),
required_capabilities=list(condition.required_capabilities),
required_scopes=list(condition.required_scopes),
any_scopes=list(condition.any_scopes),
configuration_keys=list(condition.configuration_keys),
)
def _derived_source_id(module_id: str, kind: str, value: str) -> str:
digest = hashlib.sha256(value.encode("utf-8")).hexdigest()[:16]
return f"{module_id}.{kind}.{digest}"
def _safe_source_fields(value: Mapping[str, Any]) -> dict[str, Any]:
redacted = redact_secret_values(dict(value))
if not isinstance(redacted, Mapping):
return {}
return {
str(key): item
for key, item in redacted.items()
if isinstance(item, (str, int, float, bool, list, tuple)) or item is None
}
def _optional_text(value: object) -> str | None:
if value is None:
return None
text = str(value).strip()
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]:
if not isinstance(value, (list, tuple)):
return []
return [str(item)[:500] for item in value[:100]]
+499 -2
View File
@@ -9,16 +9,35 @@ 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 (
CapabilityDocumentation,
DocumentationConfigurationDecision,
DocumentationConfigurationProviderRegistration,
DocumentationSourceDefinition,
FrontendModule,
FrontendRoute,
)
from govoplan_core.core.registry import PlatformRegistry from govoplan_core.core.registry import PlatformRegistry
from govoplan_core.core.provider_governance import (
ModuleArchitectureDeclaration,
ModuleMaturityEvidence,
)
from govoplan_tenancy.backend.manifest import get_manifest as get_tenancy_manifest from govoplan_tenancy.backend.manifest import get_manifest as get_tenancy_manifest
from govoplan_docs.backend.api.v1.routes import ( from govoplan_docs.backend.api.v1.routes import (
_classify_documentation, _classify_documentation,
_condition_visibility, _condition_visibility,
_documentation_topic_anchor, _documentation_topic_anchor,
_documentation_topic_groups, _documentation_topic_groups,
_documentation_provider_state,
_module_payload,
_visible_documentation_sources,
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 (
_release_catalog_sources,
build_documentation_source_registry,
)
class FakePrincipal: class FakePrincipal:
@@ -33,6 +52,121 @@ class FakePrincipal:
class DocsContextTests(unittest.TestCase): class DocsContextTests(unittest.TestCase):
def test_user_provider_state_omits_binding_details(self) -> None:
state = {
"configured": True,
"active": True,
"health": "healthy",
"freshness": "current",
"conflict": "clear",
"recovery": "ready",
"binding_ref": "calendar:sync-source:secret-context",
"bindings": [{"binding_ref": "calendar:sync-source:secret-context"}],
}
user = _documentation_provider_state(state, technical=False)
technical = _documentation_provider_state(state, technical=True)
self.assertNotIn("binding_ref", user)
self.assertNotIn("bindings", user)
self.assertEqual("healthy", user["health"])
self.assertIn("bindings", technical)
def test_module_architecture_projection_hides_evidence_from_user_docs(self) -> None:
manifest = ModuleManifest(
id="example",
name="Example",
version="1.0.0",
architecture=ModuleArchitectureDeclaration(
layer="governance_accountability",
kind="governance",
maturity="vertical_slice",
evidence=(
ModuleMaturityEvidence(
kind="test",
reference="tests/test_example.py",
summary="Private implementation evidence.",
),
ModuleMaturityEvidence(
kind="documentation",
reference="docs/EXAMPLE.md",
summary="Architecture explanation.",
),
),
known_limits=("Example limit.",),
supported_authority_modes=("linked_reference",),
owned_concepts=("examples",),
),
)
technical = _module_payload(manifest, technical=True)
user = _module_payload(manifest, technical=False)
self.assertEqual("vertical_slice", technical["architecture"]["maturity"])
self.assertEqual(2, len(technical["architecture"]["evidence"]))
self.assertEqual([], user["architecture"]["evidence"])
self.assertEqual(["Example limit."], user["architecture"]["known_limits"])
def test_topics_are_filtered_by_installed_or_selected_version(self) -> None:
registry = PlatformRegistry()
registry.register(ModuleManifest(
id="example",
name="Example",
version="2.1.0",
documentation=(
DocumentationTopic(
id="example.legacy",
title="Legacy",
summary="Legacy behavior",
version_max_exclusive="2.0.0",
),
DocumentationTopic(
id="example.current",
title="Current",
summary="Current behavior",
version_min="2.0.0",
),
DocumentationTopic(
id="example.universal",
title="Universal",
summary="All versions",
),
),
))
principal = FakePrincipal({"docs:documentation:read"})
installed = _classify_documentation(
registry,
principal,
settings=None,
session=None,
documentation_type="admin",
locale="en",
)
selected = _classify_documentation(
registry,
principal,
settings=None,
session=None,
documentation_type="admin",
locale="en",
target_version="1.9.0",
)
self.assertEqual(
{topic["id"] for topic in installed["configured"]},
{"example.current", "example.universal"},
)
self.assertEqual(
{topic["id"] for topic in selected["configured"]},
{"example.legacy", "example.universal"},
)
universal = next(
topic for topic in selected["configured"]
if topic["id"] == "example.universal"
)
self.assertEqual(universal["version"]["fallback"], "unversioned")
def test_docs_reader_is_the_managed_authenticated_tenant_default(self) -> None: def test_docs_reader_is_the_managed_authenticated_tenant_default(self) -> None:
manifest = get_docs_manifest() manifest = get_docs_manifest()
roles = {template.slug: template for template in manifest.role_templates} roles = {template.slug: template for template in manifest.role_templates}
@@ -98,6 +232,55 @@ class DocsContextTests(unittest.TestCase):
self.assertNotIn("access.workflow.grant-user-access", workflow_ids) self.assertNotIn("access.workflow.grant-user-access", workflow_ids)
self.assertNotIn("access.workflow.grant-user-access", {topic["id"] for topic in layers["available"]}) self.assertNotIn("access.workflow.grant-user-access", {topic["id"] for topic in layers["available"]})
def test_user_projection_fails_closed_for_unscoped_workflow_topics(self) -> None:
unscoped_static_topic = DocumentationTopic(
id="example.workflow.unscoped-static",
title="Unscoped static task",
summary="This topic must not be rendered.",
documentation_types=("user",),
metadata={"kind": "workflow"},
)
unscoped_runtime_topic = DocumentationTopic(
id="example.workflow.unscoped-runtime",
title="Unscoped runtime task",
summary="This provider topic must not be rendered.",
documentation_types=("user",),
conditions=(DocumentationCondition(required_modules=("example",)),),
metadata={"kind": "workflow"},
)
scoped_runtime_topic = DocumentationTopic(
id="example.workflow.scoped-runtime",
title="Scoped runtime task",
summary="This provider topic remains visible.",
documentation_types=("user",),
conditions=(DocumentationCondition(required_scopes=("docs:documentation:read",)),),
metadata={"kind": "workflow"},
)
registry = PlatformRegistry()
registry.register(ModuleManifest(
id="example",
name="Example",
version="9.9.9",
documentation=(unscoped_static_topic,),
documentation_providers=(
lambda context: (unscoped_runtime_topic, scoped_runtime_topic),
),
))
layers = _classify_documentation(
registry,
FakePrincipal({"docs:documentation:read"}),
settings=None,
session=None,
documentation_type="user",
locale="en",
)
self.assertEqual(
[topic["id"] for topic in _documentation_topic_groups(layers)["workflow"]],
["example.workflow.scoped-runtime"],
)
def test_user_projection_is_a_bounded_safe_whitelist(self) -> None: def test_user_projection_is_a_bounded_safe_whitelist(self) -> None:
registry = PlatformRegistry() registry = PlatformRegistry()
registry.register(ModuleManifest( registry.register(ModuleManifest(
@@ -159,7 +342,12 @@ class DocsContextTests(unittest.TestCase):
) )
self.assertEqual(topic["conditions"], []) self.assertEqual(topic["conditions"], [])
self.assertEqual(topic["configuration_keys"], []) self.assertEqual(topic["configuration_keys"], [])
self.assertEqual(topic["blockers"], {"modules": [], "capabilities": [], "scopes": []}) self.assertEqual(topic["blockers"], {
"modules": [],
"capabilities": [],
"scopes": [],
"configuration": [],
})
self.assertNotIn("raw_policy", topic["metadata"]) self.assertNotIn("raw_policy", topic["metadata"])
self.assertNotIn("api_path", topic["metadata"]) self.assertNotIn("api_path", topic["metadata"])
self.assertEqual(topic["metadata"]["help_contexts"], ["example.list"]) self.assertEqual(topic["metadata"]["help_contexts"], ["example.list"])
@@ -278,6 +466,7 @@ class DocsContextTests(unittest.TestCase):
self.assertEqual(blockers["modules"], ["mail", "files", "campaigns", "legacy"]) self.assertEqual(blockers["modules"], ["mail", "files", "campaigns", "legacy"])
self.assertEqual(blockers["capabilities"], ["mail.delivery"]) self.assertEqual(blockers["capabilities"], ["mail.delivery"])
self.assertEqual(blockers["scopes"], ["mail:profile:read", "admin:policies:read"]) self.assertEqual(blockers["scopes"], ["mail:profile:read", "admin:policies:read"])
self.assertEqual(blockers["configuration"], [])
def test_condition_visibility_accepts_any_module_scope_and_capability(self) -> None: def test_condition_visibility_accepts_any_module_scope_and_capability(self) -> None:
registry = PlatformRegistry() registry = PlatformRegistry()
@@ -293,7 +482,315 @@ class DocsContextTests(unittest.TestCase):
self.assertTrue(active) self.assertTrue(active)
self.assertEqual(reason, "conditions satisfied") self.assertEqual(reason, "conditions satisfied")
self.assertEqual(blockers, {"modules": [], "capabilities": [], "scopes": []}) self.assertEqual(blockers, {
"modules": [],
"capabilities": [],
"scopes": [],
"configuration": [],
})
def test_configuration_states_gate_conditions_without_disclosing_values(self) -> None:
states = {
"feature.enabled": "enabled",
"feature.inherited": "inherited",
"feature.disabled": "disabled",
"feature.unavailable": "unavailable",
}
def resolve(_context, keys):
return {
key: DocumentationConfigurationDecision(
key=key,
state=states[key], # type: ignore[arg-type]
source="tenant" if states[key] == "enabled" else "system",
reason="State metadata only.",
)
for key in keys
}
topics = tuple(
DocumentationTopic(
id=f"example.{key}",
title=key,
summary="Configuration-aware topic.",
conditions=(
DocumentationCondition(
required_scopes=("docs:documentation:read",),
configuration_keys=(key,),
),
),
configuration_keys=(key,),
)
for key in states
)
registry = PlatformRegistry()
registry.register(ModuleManifest(
id="example",
name="Example",
version="1.0.0",
documentation=topics,
documentation_configuration_providers=(
DocumentationConfigurationProviderRegistration(
keys=tuple(states),
resolve=resolve,
),
),
))
layers = _classify_documentation(
registry,
FakePrincipal({"docs:documentation:read"}),
settings=None,
session=None,
documentation_type="admin",
locale="en",
)
self.assertEqual(
{topic["id"] for topic in layers["configured"]},
{"example.feature.enabled", "example.feature.inherited"},
)
self.assertEqual(
[topic["id"] for topic in layers["available"]],
["example.feature.disabled"],
)
self.assertEqual(
[topic["id"] for topic in layers["evidence"]],
["example.feature.unavailable"],
)
rendered = repr(layers)
self.assertNotIn("raw value", rendered.casefold())
self.assertNotIn("password", rendered.casefold())
def test_source_registry_covers_every_kind_and_redacts_explicit_payloads(self) -> None:
manifest = ModuleManifest(
id="example",
name="Example",
version="1.2.3",
capability_factories={
"example.lookup": 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(
module_id="example",
routes=(
FrontendRoute(
path="/example",
component="ExamplePage",
required_all=("example:item:read",),
),
),
),
documentation_sources=(
DocumentationSourceDefinition(
id="example.configuration.package",
kind="configuration_package",
label="Example package",
inspection={
"package_id": "example.package",
"schema_version": "2",
"password": "must-not-leak",
},
),
DocumentationSourceDefinition(
id="example.project.wiki",
kind="wiki",
label="Example wiki",
link=DocumentationLink(
label="Wiki",
href="https://example.invalid/wiki",
kind="wiki",
),
),
DocumentationSourceDefinition(
id="example.repository.handbook",
kind="repository",
label="Example handbook",
link=DocumentationLink(
label="Handbook",
href="example/docs/HANDBOOK.md",
kind="repository",
),
),
),
)
sources = build_documentation_source_registry(
(manifest,),
include_runtime_catalogs=False,
).sources()
self.assertEqual(
{source.item.kind for source in sources},
{
"manifest",
"route",
"capability",
"policy",
"configuration_package",
"wiki",
"repository",
},
)
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:
self.assertTrue(source.item.id.startswith("example."))
self.assertEqual("example", source.item.owner_module_id)
self.assertEqual(
f"/api/v1/docs/sources/{source.item.id}",
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:
manifest = ModuleManifest(
id="example",
name="Example",
version="1.0.0",
documentation_sources=(
DocumentationSourceDefinition(
id="example.protected.handbook",
kind="repository",
label="Protected handbook",
condition=DocumentationCondition(
required_scopes=("example:handbook:read",),
),
link=DocumentationLink(
label="Protected",
href="example/docs/PROTECTED.md",
kind="repository",
),
),
),
)
registry = PlatformRegistry()
registry.register(manifest)
sources = build_documentation_source_registry(
(manifest,),
include_runtime_catalogs=False,
).sources()
denied = _visible_documentation_sources(
sources,
registry,
FakePrincipal({"docs:documentation:admin"}),
settings=None,
session=None,
documentation_type="admin",
locale="en",
)
allowed = _visible_documentation_sources(
sources,
registry,
FakePrincipal({
"docs:documentation:admin",
"example:handbook:read",
}),
settings=None,
session=None,
documentation_type="admin",
locale="en",
)
denied_ids = {source.item.id for source in denied}
allowed_ids = {source.item.id for source in allowed}
self.assertNotIn("example.protected.handbook", denied_ids)
self.assertIn("example.protected.handbook", allowed_ids)
if __name__ == "__main__": if __name__ == "__main__":
+7 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/docs-webui", "name": "@govoplan/docs-webui",
"version": "0.1.10", "version": "0.1.16",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
@@ -13,14 +13,14 @@
} }
}, },
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.10", "@govoplan/core-webui": "^0.1.16",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": "^19.0.0", "react": ">=19.2.7 <20",
"react-dom": "^19.0.0", "react-dom": ">=19.2.7 <20",
"react-router-dom": "^7.1.1", "react-router": ">=8.3.0 <9",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^5.2.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^6.0.6" "vite": "^7.3.6"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@govoplan/core-webui": { "@govoplan/core-webui": {
+110 -3
View File
@@ -1,5 +1,42 @@
import { apiFetch, type ApiSettings } from "@govoplan/core-webui"; import { apiFetch, type ApiSettings } from "@govoplan/core-webui";
export type DocsModuleArchitecture = {
contract_version: string;
layer: string;
kind: string;
maturity: string;
evidence: Array<{ kind: string; reference: string; summary: string }>;
known_limits: string[];
supported_authority_modes: string[];
owned_concepts: string[];
non_owned_concepts: string[];
reference_packages: string[];
target_tested_providers: string[];
documentation: Record<string, string[]>;
};
export type DocsExternalProvider = {
id: string;
module_id: string;
label: string;
maturity: string;
operations: string[];
authority_modes: string[];
behavior?: Record<string, unknown>;
known_outage_behavior?: string | null;
runtime_state?: {
configured?: boolean;
active?: boolean;
authority_mode?: string | null;
authority_modes?: string[];
health?: string;
freshness?: string;
conflict?: string;
recovery?: string;
observed_at?: string;
} | null;
};
export type DocsModule = { export type DocsModule = {
id: string; id: string;
name: string; name: string;
@@ -16,6 +53,9 @@ export type DocsModule = {
capabilities: string[]; capabilities: string[];
documentation_count: number; documentation_count: number;
documentation_provider_count: number; documentation_provider_count: number;
architecture?: DocsModuleArchitecture | null;
external_provider_count: number;
external_providers: DocsExternalProvider[];
}; };
export type DocsRoute = { export type DocsRoute = {
@@ -54,9 +94,34 @@ export type DocsOptionalModuleEvidence = {
}; };
export type DocsSource = { export type DocsSource = {
id: string;
kind: string;
owner_module_id: string;
label: string; label: string;
state: "configured" | "disabled" | "unavailable";
state_reason?: string | null;
inspection_url: string;
provenance: {
source: string; source: string;
layer: string; version?: string | null;
revision?: string | null;
published_at?: string | null;
checksum?: string | null;
};
};
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 = {
@@ -94,21 +159,45 @@ export type DocsDocumentationTopic = {
modules: string[]; modules: string[];
capabilities: string[]; capabilities: string[];
scopes: string[]; scopes: string[];
configuration: string[];
}; };
audience: string[]; audience: string[];
order: number; order: number;
i18n_key: string; i18n_key: string;
locale: string; locale: string;
translation_locale: string; translation_locale: string;
version: {
resolved: string;
minimum?: string | null;
maximum_exclusive?: string | null;
range: string;
fallback: "unversioned" | "matching_range" | string;
};
conditions: DocsDocumentationCondition[]; conditions: DocsDocumentationCondition[];
links: DocsDocumentationLink[]; links: DocsDocumentationLink[];
related_modules: string[]; related_modules: string[];
unlocks: string[]; unlocks: string[];
configuration_keys: string[]; configuration_keys: string[];
configuration_states: Array<{
key: string;
state: "enabled" | "disabled" | "inherited" | "unavailable";
source?: string | null;
reason?: string | null;
}>;
metadata: Record<string, unknown>; metadata: Record<string, unknown>;
}; };
export type DocsContext = { export type DocsContext = {
versions: {
mode: "installed" | "selected";
selected_version?: string | null;
status: "installed" | "stable" | "older_supported" | "unsupported" | string;
latest_version?: string | null;
stable_version?: string | null;
supported_versions: string[];
installed_versions: Record<string, string>;
fallback_policy: string;
};
actor: { actor: {
tenant_id?: string; tenant_id?: string;
user_id?: string; user_id?: string;
@@ -119,6 +208,8 @@ export type DocsContext = {
}; };
summary: { summary: {
module_count: number; module_count: number;
architecture_declared_module_count: number;
external_provider_count: number;
visible_route_count: number; visible_route_count: number;
available_route_count: number; available_route_count: number;
permission_count: number; permission_count: number;
@@ -161,10 +252,26 @@ export type DocsContext = {
}; };
}; };
export function fetchDocsContext(settings: ApiSettings, options: { documentationType?: "admin" | "user"; locale?: string } = {}): Promise<DocsContext> { export function fetchDocsContext(settings: ApiSettings, options: { documentationType?: "admin" | "user"; locale?: string; version?: string | null } = {}): Promise<DocsContext> {
const params = new URLSearchParams();
if (options.documentationType) params.set("type", options.documentationType);
if (options.locale) params.set("locale", options.locale);
if (options.version) params.set("version", options.version);
const query = params.toString();
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(); const params = new URLSearchParams();
if (options.documentationType) params.set("type", options.documentationType); if (options.documentationType) params.set("type", options.documentationType);
if (options.locale) params.set("locale", options.locale); if (options.locale) params.set("locale", options.locale);
const query = params.toString(); const query = params.toString();
return apiFetch(settings, `/api/v1/docs/context${query ? `?${query}` : ""}`); return apiFetch(
settings,
`/api/v1/docs/sources/${encodeURIComponent(sourceId)}${query ? `?${query}` : ""}`
);
} }
+179 -14
View File
@@ -1,9 +1,10 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Link, useLocation } from "react-router-dom"; 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";
@@ -71,6 +74,7 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
const [documentationType, setDocumentationType] = useState<DocumentationType>(() => documentationTypeFromSearch(location.search)); const [documentationType, setDocumentationType] = useState<DocumentationType>(() => documentationTypeFromSearch(location.search));
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set()); const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set());
const locale = localeFromSearch(location.search) ?? language; const locale = localeFromSearch(location.search) ?? language;
const selectedVersion = versionFromSearch(location.search);
const adminDocs = documentationType === "admin"; const adminDocs = documentationType === "admin";
async function load() { async function load() {
@@ -79,7 +83,7 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
setError(""); setError("");
setContext(null); setContext(null);
try { try {
const nextContext = await fetchDocsContext(settings, { documentationType, locale }); const nextContext = await fetchDocsContext(settings, { documentationType, locale, version: selectedVersion });
if (sequence !== loadSequence.current) return; if (sequence !== loadSequence.current) return;
if (nextContext.actor.documentation_type !== documentationType) { if (nextContext.actor.documentation_type !== documentationType) {
throw new Error("Documentation response type did not match the requested projection."); throw new Error("Documentation response type did not match the requested projection.");
@@ -98,7 +102,7 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
setDocumentationType((current) => current === nextType ? current : nextType); setDocumentationType((current) => current === nextType ? current : nextType);
}, [location.search]); }, [location.search]);
useEffect(() => { void load(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken, documentationType, locale]); useEffect(() => { void load(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken, documentationType, locale, selectedVersion]);
const treeNodes = useMemo(() => docsTreeNodes(context, adminDocs), [context, adminDocs]); const treeNodes = useMemo(() => docsTreeNodes(context, adminDocs), [context, adminDocs]);
const pages = useMemo(() => flattenTreePages(treeNodes), [treeNodes]); const pages = useMemo(() => flattenTreePages(treeNodes), [treeNodes]);
@@ -137,6 +141,21 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
onSelect={selectDocumentationType} onSelect={selectDocumentationType}
canViewAdmin={context?.actor.available_documentation_types.includes("admin") ?? documentationType === "admin"} canViewAdmin={context?.actor.available_documentation_types.includes("admin") ?? documentationType === "admin"}
/> />
<label className="docs-version-selector" title={context?.versions.fallback_policy}>
<span>Version</span>
<select
value={selectedVersion ?? ""}
onChange={(event) => selectVersion(event.target.value || null)}
>
<option value="">Installed versions</option>
{selectedVersion && !context?.versions.supported_versions.includes(selectedVersion) &&
<option value={selectedVersion}>{selectedVersion} (unsupported)</option>
}
{(context?.versions.supported_versions ?? []).map((version) => (
<option key={version} value={version}>{version}</option>
))}
</select>
</label>
</div> </div>
<nav className="docs-tree" aria-label="i18n:govoplan-docs.documentation_outline.6f836b99"> <nav className="docs-tree" aria-label="i18n:govoplan-docs.documentation_outline.6f836b99">
<ExplorerTree <ExplorerTree
@@ -189,6 +208,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} />
@@ -205,6 +226,13 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) {
navigate(`${location.pathname}?${params.toString()}`, { replace: true }); navigate(`${location.pathname}?${params.toString()}`, { replace: true });
} }
function selectVersion(version: string | null) {
const params = new URLSearchParams(location.search);
if (version) params.set("version", version);
else params.delete("version");
navigate(`${location.pathname}?${params.toString()}`, { replace: true });
}
function selectPage(page: DocsPageNode) { function selectPage(page: DocsPageNode) {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
params.set("topic", page.id); params.set("topic", page.id);
@@ -269,7 +297,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 +311,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 +342,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>
); );
@@ -559,7 +597,8 @@ function UnavailableDocumentationReason({ topic }: { topic: DocsDocumentationTop
const rows = [ const rows = [
["i18n:govoplan-docs.modules.04e9462c", topic.blockers.modules], ["i18n:govoplan-docs.modules.04e9462c", topic.blockers.modules],
["i18n:govoplan-docs.capabilities.ca09c54b", topic.blockers.capabilities], ["i18n:govoplan-docs.capabilities.ca09c54b", topic.blockers.capabilities],
["i18n:govoplan-docs.permissions.d06d5557", topic.blockers.scopes] ["i18n:govoplan-docs.permissions.d06d5557", topic.blockers.scopes],
["Configuration", topic.blockers.configuration]
].filter(([, values]) => Array.isArray(values) && values.length); ].filter(([, values]) => Array.isArray(values) && values.length);
return ( return (
<div className="docs-unavailable-reason"> <div className="docs-unavailable-reason">
@@ -818,9 +857,12 @@ function selectedPageFromSearch(search: string, pages: DocsPageNode[]): DocsPage
if (requested) return pages.find((page) => page.id === requested) ?? pages[0]; if (requested) return pages.find((page) => page.id === requested) ?? pages[0];
const helpContext = params.get("context") || ""; const helpContext = params.get("context") || "";
if (helpContext) { if (helpContext) {
const exact = pages.find((page) => page.kind === "topic" && metadataList(page.topic.metadata, "help_contexts").includes(helpContext)); const fallbackContext = params.get("fallback_context") || "";
for (const contextId of [helpContext, fallbackContext].filter(Boolean)) {
const exact = pages.find((page) => page.kind === "topic" && metadataList(page.topic.metadata, "help_contexts").includes(contextId));
if (exact) return exact; if (exact) return exact;
const moduleId = helpContextModuleId(helpContext); }
const moduleId = params.get("module") || helpContextModuleId(fallbackContext || helpContext);
const moduleTopic = pages.find((page) => page.kind === "topic" && page.topic.source_module_id === moduleId); const moduleTopic = pages.find((page) => page.kind === "topic" && page.topic.source_module_id === moduleId);
if (moduleTopic) return moduleTopic; if (moduleTopic) return moduleTopic;
} }
@@ -879,6 +921,11 @@ function documentationTypeFromSearch(search: string): DocumentationType {
return new URLSearchParams(search).get("type") === "admin" ? "admin" : "user"; return new URLSearchParams(search).get("type") === "admin" ? "admin" : "user";
} }
function versionFromSearch(search: string): string | null {
const value = new URLSearchParams(search).get("version")?.trim();
return value || null;
}
function localeFromSearch(search: string): string | null { function localeFromSearch(search: string): string | null {
const value = new URLSearchParams(search).get("locale"); const value = new URLSearchParams(search).get("locale");
if (!value) return null; if (!value) return null;
@@ -925,6 +972,8 @@ function ModuleTable({ modules }: { modules: DocsModule[] }) {
if (!modules.length) return <p className="muted">i18n:govoplan-docs.no_configured_modules_found.f6f9ce24</p>; if (!modules.length) return <p className="muted">i18n:govoplan-docs.no_configured_modules_found.f6f9ce24</p>;
const columns: DataGridColumn<DocsModule>[] = [ const columns: DataGridColumn<DocsModule>[] = [
{ id: "module", header: "i18n:govoplan-docs.module.b8ff0289", width: "minmax(220px, 1fr)", minWidth: 190, resizable: true, sortable: true, filterable: true, value: (module) => `${module.name} ${module.id} ${module.version}`, render: (module) => <div><strong>{module.name}</strong><span className="muted block">{module.id} {module.version}</span></div> }, { id: "module", header: "i18n:govoplan-docs.module.b8ff0289", width: "minmax(220px, 1fr)", minWidth: 190, resizable: true, sortable: true, filterable: true, value: (module) => `${module.name} ${module.id} ${module.version}`, render: (module) => <div><strong>{module.name}</strong><span className="muted block">{module.id} {module.version}</span></div> },
{ id: "architecture", header: "i18n:govoplan-docs.architecture.4ca303a3", width: "minmax(210px, .9fr)", minWidth: 190, resizable: true, sortable: true, filterable: true, value: (module) => module.architecture ? `${module.architecture.layer} ${module.architecture.kind} ${module.architecture.maturity}` : "undeclared", render: (module) => module.architecture ? <div><strong>{module.architecture.maturity}</strong><span className="muted block">{module.architecture.layer} · {module.architecture.kind}</span>{module.architecture.known_limits.length ? <span className="muted block">{module.architecture.known_limits.length} i18n:govoplan-docs.known_limits.31871a6b</span> : null}</div> : <span className="muted">i18n:govoplan-docs.staged_declaration_pending.7ce9c1a1</span> },
{ id: "authority", header: "i18n:govoplan-docs.source_authority.0a863835", width: "minmax(260px, 1fr)", minWidth: 230, resizable: true, filterable: true, value: (module) => `${module.architecture?.supported_authority_modes.join(" ") ?? ""} ${module.external_provider_count} ${module.external_providers.map((provider) => `${provider.runtime_state?.health ?? "unobserved"} ${provider.runtime_state?.freshness ?? ""} ${provider.runtime_state?.conflict ?? ""} ${provider.runtime_state?.recovery ?? ""}`).join(" ")}`, render: (module) => <div>{module.architecture?.supported_authority_modes.length ? module.architecture.supported_authority_modes.join(", ") : "-"}<span className="muted block">{module.external_provider_count} i18n:govoplan-docs.external_providers.d618fc54</span>{module.external_providers.map((provider) => <span className="muted block" key={provider.id}>{provider.label}: {provider.runtime_state ? `${provider.runtime_state.health ?? "unknown"} · ${provider.runtime_state.freshness ?? "unknown"} · ${provider.runtime_state.conflict ?? "unknown"} · ${provider.runtime_state.recovery ?? "unknown"}` : "unobserved"}</span>)}</div> },
{ id: "routes", header: "i18n:govoplan-docs.routes.03730e58", width: 170, sortable: true, value: (module) => module.route_count, render: (module) => <>{module.route_count} i18n:govoplan-docs.route.200e2a66 {module.nav_count} nav</> }, { id: "routes", header: "i18n:govoplan-docs.routes.03730e58", width: 170, sortable: true, value: (module) => module.route_count, render: (module) => <>{module.route_count} i18n:govoplan-docs.route.200e2a66 {module.nav_count} nav</> },
{ id: "permissions", header: "i18n:govoplan-docs.permissions.d06d5557", width: 130, sortable: true, value: (module) => module.permission_count }, { id: "permissions", header: "i18n:govoplan-docs.permissions.d06d5557", width: 130, sortable: true, value: (module) => module.permission_count },
{ id: "frontend", header: "i18n:govoplan-docs.frontend.152d1cf2", width: "minmax(180px, .8fr)", minWidth: 160, resizable: true, sortable: true, filterable: true, value: (module) => module.frontend_package || "-", render: (module) => module.frontend_package || "-" }, { id: "frontend", header: "i18n:govoplan-docs.frontend.152d1cf2", width: "minmax(180px, .8fr)", minWidth: 160, resizable: true, sortable: true, filterable: true, value: (module) => module.frontend_package || "-", render: (module) => module.frontend_package || "-" },
@@ -964,9 +1013,40 @@ 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 (
<>
{sourceError && <DismissibleAlert tone="danger" resetKey={sourceError}>{sourceError}</DismissibleAlert>}
<dl className="detail-list"> <dl className="detail-list">
{modules.map((item) => {modules.map((item) =>
<div key={`${item.source_module_id}-${item.module_id}`}> <div key={`${item.source_module_id}-${item.module_id}`}>
@@ -975,15 +1055,100 @@ function EvidenceList({ modules, sources }: { modules: DocsOptionalModuleEvidenc
</div> </div>
)} )}
{sources.map((item) => {sources.map((item) =>
<div key={item.source}> <div key={item.id}>
<dt>{item.layer}</dt> <dt><StatusBadge status={item.state === "configured" ? "success" : item.state === "disabled" ? "inactive" : "warning"} label={item.state} /></dt>
<dd><strong>{item.label}</strong><span className="muted"> · {item.source}</span></dd> <dd>
<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> </div>
)} )}
</dl> </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(", ")}`);
+22
View File
@@ -9,6 +9,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-docs.advanced.203a8d6b": "Advanced", "i18n:govoplan-docs.advanced.203a8d6b": "Advanced",
"i18n:govoplan-docs.available_documentation.db8bcc42": "Available documentation", "i18n:govoplan-docs.available_documentation.db8bcc42": "Available documentation",
"i18n:govoplan-docs.available_routes.c2635868": "Available routes", "i18n:govoplan-docs.available_routes.c2635868": "Available routes",
"i18n:govoplan-docs.architecture.4ca303a3": "Architecture",
"i18n:govoplan-docs.basics.5fcebeef": "Basics", "i18n:govoplan-docs.basics.5fcebeef": "Basics",
"i18n:govoplan-docs.capabilities.ca09c54b": "Capabilities", "i18n:govoplan-docs.capabilities.ca09c54b": "Capabilities",
"i18n:govoplan-docs.common_tasks.9f825c48": "Common tasks", "i18n:govoplan-docs.common_tasks.9f825c48": "Common tasks",
@@ -25,10 +26,12 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-docs.evidence.7ea014de": "Evidence", "i18n:govoplan-docs.evidence.7ea014de": "Evidence",
"i18n:govoplan-docs.field.7558c082": "Field", "i18n:govoplan-docs.field.7558c082": "Field",
"i18n:govoplan-docs.frontend.152d1cf2": "Frontend", "i18n:govoplan-docs.frontend.152d1cf2": "Frontend",
"i18n:govoplan-docs.external_providers.d618fc54": "external providers",
"i18n:govoplan-docs.granted_permissions.0a232e78": "Granted permissions", "i18n:govoplan-docs.granted_permissions.0a232e78": "Granted permissions",
"i18n:govoplan-docs.guidance_for_the_functions_available_in_this_ins.723b7cad": "Guidance for the functions available in this installation.", "i18n:govoplan-docs.guidance_for_the_functions_available_in_this_ins.723b7cad": "Guidance for the functions available in this installation.",
"i18n:govoplan-docs.help_center.f3f3a34b": "Help Center", "i18n:govoplan-docs.help_center.f3f3a34b": "Help Center",
"i18n:govoplan-docs.loading_documentation_context.1c091645": "Loading documentation context...", "i18n:govoplan-docs.loading_documentation_context.1c091645": "Loading documentation context...",
"i18n:govoplan-docs.known_limits.31871a6b": "known limits",
"i18n:govoplan-docs.meaning.584d8aa0": "Meaning", "i18n:govoplan-docs.meaning.584d8aa0": "Meaning",
"i18n:govoplan-docs.module.b8ff0289": "Module", "i18n:govoplan-docs.module.b8ff0289": "Module",
"i18n:govoplan-docs.modules.04e9462c": "Modules", "i18n:govoplan-docs.modules.04e9462c": "Modules",
@@ -74,7 +77,15 @@ 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_authority.0a863835": "Source authority",
"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.staged_declaration_pending.7ce9c1a1": "Staged declaration pending",
"i18n:govoplan-docs.steps.6041435e": "Steps", "i18n:govoplan-docs.steps.6041435e": "Steps",
"i18n:govoplan-docs.summary.d6b9936d": "Summary", "i18n:govoplan-docs.summary.d6b9936d": "Summary",
"i18n:govoplan-docs.technical_documentation_for_the_modules_and_conf.267e739e": "Technical documentation for the modules and configuration active in this installation.", "i18n:govoplan-docs.technical_documentation_for_the_modules_and_conf.267e739e": "Technical documentation for the modules and configuration active in this installation.",
@@ -100,6 +111,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-docs.advanced.203a8d6b": "Fortgeschritten", "i18n:govoplan-docs.advanced.203a8d6b": "Fortgeschritten",
"i18n:govoplan-docs.available_documentation.db8bcc42": "Verfügbare Dokumentation", "i18n:govoplan-docs.available_documentation.db8bcc42": "Verfügbare Dokumentation",
"i18n:govoplan-docs.available_routes.c2635868": "Verfügbare Routen", "i18n:govoplan-docs.available_routes.c2635868": "Verfügbare Routen",
"i18n:govoplan-docs.architecture.4ca303a3": "Architektur",
"i18n:govoplan-docs.basics.5fcebeef": "Grundlagen", "i18n:govoplan-docs.basics.5fcebeef": "Grundlagen",
"i18n:govoplan-docs.capabilities.ca09c54b": "Fähigkeiten", "i18n:govoplan-docs.capabilities.ca09c54b": "Fähigkeiten",
"i18n:govoplan-docs.common_tasks.9f825c48": "Häufige Aufgaben", "i18n:govoplan-docs.common_tasks.9f825c48": "Häufige Aufgaben",
@@ -116,10 +128,12 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-docs.evidence.7ea014de": "Evidence", "i18n:govoplan-docs.evidence.7ea014de": "Evidence",
"i18n:govoplan-docs.field.7558c082": "Feld", "i18n:govoplan-docs.field.7558c082": "Feld",
"i18n:govoplan-docs.frontend.152d1cf2": "Frontend", "i18n:govoplan-docs.frontend.152d1cf2": "Frontend",
"i18n:govoplan-docs.external_providers.d618fc54": "externe Anbieter",
"i18n:govoplan-docs.granted_permissions.0a232e78": "Gewährte Berechtigungen", "i18n:govoplan-docs.granted_permissions.0a232e78": "Gewährte Berechtigungen",
"i18n:govoplan-docs.guidance_for_the_functions_available_in_this_ins.723b7cad": "Anleitung für die in dieser Installation verfügbaren Funktionen.", "i18n:govoplan-docs.guidance_for_the_functions_available_in_this_ins.723b7cad": "Anleitung für die in dieser Installation verfügbaren Funktionen.",
"i18n:govoplan-docs.help_center.f3f3a34b": "Hilfezentrum", "i18n:govoplan-docs.help_center.f3f3a34b": "Hilfezentrum",
"i18n:govoplan-docs.loading_documentation_context.1c091645": "Dokumentationskontext wird geladen...", "i18n:govoplan-docs.loading_documentation_context.1c091645": "Dokumentationskontext wird geladen...",
"i18n:govoplan-docs.known_limits.31871a6b": "bekannte Einschränkungen",
"i18n:govoplan-docs.meaning.584d8aa0": "Bedeutung", "i18n:govoplan-docs.meaning.584d8aa0": "Bedeutung",
"i18n:govoplan-docs.module.b8ff0289": "Modul", "i18n:govoplan-docs.module.b8ff0289": "Modul",
"i18n:govoplan-docs.modules.04e9462c": "Module", "i18n:govoplan-docs.modules.04e9462c": "Module",
@@ -165,7 +179,15 @@ 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_authority.0a863835": "Quellenhoheit",
"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.staged_declaration_pending.7ce9c1a1": "Deklaration steht noch aus",
"i18n:govoplan-docs.steps.6041435e": "Schritte", "i18n:govoplan-docs.steps.6041435e": "Schritte",
"i18n:govoplan-docs.summary.d6b9936d": "Zusammenfassung", "i18n:govoplan-docs.summary.d6b9936d": "Zusammenfassung",
"i18n:govoplan-docs.technical_documentation_for_the_modules_and_conf.267e739e": "Technische Dokumentation für die in dieser Installation aktiven Module und Konfiguration.", "i18n:govoplan-docs.technical_documentation_for_the_modules_and_conf.267e739e": "Technische Dokumentation für die in dieser Installation aktiven Module und Konfiguration.",