Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ffdb23f69 | ||
|
|
14b19fbead | ||
|
|
845dcbafdb | ||
|
|
58d320d9b3 | ||
|
|
9554657bb5 | ||
|
|
88b685ff5e | ||
|
|
be57a1823a | ||
|
|
6bcb75f577 | ||
|
|
32fe4b7238 | ||
|
|
6a8f53b87d | ||
|
|
1cec4ee1d8 | ||
|
|
29d03aa2ca | ||
|
|
6fb928d6cf | ||
|
|
6edaaadf37 | ||
|
|
0b171fbdd4 | ||
|
|
ed6790c057 | ||
|
|
3766e26377 | ||
|
|
6c2b36af0f | ||
|
|
3f75ca8e48 | ||
|
|
a886a9b3de | ||
|
|
fe83290d56 | ||
|
|
c50f699399 | ||
|
|
59b45a0829 | ||
|
|
861abcc573 | ||
|
|
5e995fed88 | ||
|
|
41ca242004 | ||
|
|
85caa8d337 | ||
|
|
64640327ae | ||
|
|
cc7c2a91ee | ||
|
|
7c92565d9d | ||
|
|
23bfe5e2f8 | ||
|
|
cf2f7f6890 | ||
|
|
23b601bc0d | ||
|
|
99c52c2153 | ||
|
|
ca68d98806 | ||
|
|
79c4cb067a | ||
|
|
cd498dc1d8 | ||
|
|
e07b3487e3 | ||
|
|
72279de2c0 | ||
|
|
88543ab115 | ||
|
|
81fe0f4680 | ||
|
|
fe784cc562 | ||
|
|
9fe7ad2cb4 | ||
|
|
f1eebd849c | ||
|
|
4eb90079d5 | ||
|
|
628714804b | ||
|
|
ff9fa37a88 | ||
|
|
4c7552f0dd | ||
|
|
a20e02291d | ||
|
|
b6452c6f53 | ||
|
|
75103d49af | ||
|
|
a60b8b0752 | ||
|
|
7f2f896a0f | ||
|
|
60e04a324c | ||
|
|
6a74e53a1c | ||
|
|
6517b6ac27 | ||
|
|
26a66814b4 | ||
|
|
d08f9f0f2d | ||
|
|
47c90400af | ||
|
|
5d4535f7b5 | ||
|
|
83ccb7f198 | ||
|
|
f407419d25 | ||
|
|
e88dceb639 | ||
|
|
ef8fd45457 | ||
|
|
d0ff2f1510 | ||
|
|
f8b06887d2 | ||
|
|
69519a92b4 | ||
|
|
e2f505eeab | ||
|
|
a1b80eda27 | ||
|
|
5bb8028147 | ||
|
|
5efb0eea6f | ||
|
|
5e9234d4b6 | ||
|
|
b76581a89a | ||
|
|
bec62f38d1 | ||
|
|
c66e1b768d | ||
|
|
209a43592f | ||
|
|
50b81c9ca7 | ||
|
|
612a44bc8e | ||
|
|
dce725636d | ||
|
|
78811f7f6e | ||
|
|
09046e6e62 | ||
|
|
f3cfd1bccc | ||
|
|
241db623c7 | ||
|
|
b269791c48 | ||
|
|
69ba1037bf | ||
|
|
8bdf7b5f7e | ||
|
|
d6fdd7ddf5 | ||
|
|
7b0ab31adf | ||
|
|
389df7c3d5 | ||
|
|
3b9ae901dd | ||
|
|
bf2f02891f | ||
|
|
7ec0bbb826 | ||
|
|
3ca068f76a | ||
|
|
61463a24cb | ||
|
|
8262215fcd | ||
|
|
8aba74e01e |
+4
-2
@@ -15,12 +15,14 @@ GOVOPLAN_DB_MAX_OVERFLOW=10
|
||||
GOVOPLAN_DB_POOL_TIMEOUT_SECONDS=30
|
||||
GOVOPLAN_DB_POOL_RECYCLE_SECONDS=1800
|
||||
|
||||
ENABLED_MODULES=tenancy,organizations,identity,idm,access,admin,dashboard,policy,audit,files,templates,mail,campaigns,calendar,poll,scheduling,connectors,datasources,dataflow,dist_lists,workflow_engine,workflow,views,search,risk_compliance,postbox,notifications,services,parties,mandates,decisions,portal,cases,committee,docs,ops
|
||||
ENABLED_MODULES=tenancy,organizations,identity,idm,access,admin,dashboard,policy,audit,files,templates,mail,campaigns,calendar,poll,scheduling,connectors,datasources,dataflow,dist_lists,workflow_engine,workflow,tasks,views,quick_access,search,risk_compliance,postbox,notifications,services,parties,mandates,decisions,portal,cases,committee,docs,ops
|
||||
|
||||
CELERY_ENABLED=true
|
||||
REDIS_URL=redis://127.0.0.1:6379/0
|
||||
CELERY_QUEUES=send_email,append_sent,notifications,calendar,dataflow,events,default
|
||||
CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90
|
||||
SCHEDULING_PUBLIC_SELF_ENROLLMENT_ENABLED=true
|
||||
SCHEDULING_PUBLIC_SELF_ENROLLMENT_MAX_CAPACITY=10000
|
||||
|
||||
GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS=false
|
||||
GOVOPLAN_CONNECTOR_MAX_STRUCTURED_RESPONSE_BYTES=16777216
|
||||
@@ -57,4 +59,4 @@ DEV_MAILBOX_API_ENABLED=false
|
||||
|
||||
GOVOPLAN_MODULE_PACKAGE_CATALOG_URL=https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json
|
||||
GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS_FILE=/etc/govoplan/catalog-keyring.json
|
||||
GOVOPLAN_MODULE_PACKAGE_CATALOG_APPROVED_CHANNEL=stable
|
||||
GOVOPLAN_MODULE_PACKAGE_CATALOG_APPROVED_CHANNELS=stable
|
||||
|
||||
@@ -23,6 +23,9 @@ jobs:
|
||||
- name: Test declarative deployment bundle
|
||||
working-directory: govoplan
|
||||
run: python -m unittest -v tests.test_deployment_installer
|
||||
- name: Test WebUI installer retry failures
|
||||
working-directory: govoplan
|
||||
run: python -m unittest -v tests.test_webui_release_dependency_retries
|
||||
- name: Build single-file deployer artifact
|
||||
working-directory: govoplan
|
||||
run: |
|
||||
|
||||
@@ -4,12 +4,20 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Existing protected release version without leading v
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish-package:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
TRIGGER_TAG: ${{ gitea.ref_name }}
|
||||
REQUESTED_VERSION: ${{ inputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
@@ -25,14 +33,134 @@ jobs:
|
||||
import subprocess
|
||||
import tomllib
|
||||
|
||||
tag = os.environ["GITEA_REF_NAME"]
|
||||
project = tomllib.loads(Path("packages/govoplan-meta/pyproject.toml").read_text(encoding="utf-8"))["project"]
|
||||
requested_version = os.environ.get("REQUESTED_VERSION", "").strip()
|
||||
tag = f"v{requested_version}" if requested_version else os.environ["TRIGGER_TAG"]
|
||||
if not tag.startswith("v") or not tag[1:]:
|
||||
raise SystemExit("release tag is missing")
|
||||
project_text = subprocess.check_output(
|
||||
["git", "show", f"{tag}:packages/govoplan-meta/pyproject.toml"],
|
||||
text=True,
|
||||
)
|
||||
project = tomllib.loads(project_text)["project"]
|
||||
if tag != f"v{project['version']}":
|
||||
raise SystemExit("meta-package version does not match the release tag")
|
||||
if subprocess.run(["git", "merge-base", "--is-ancestor", "HEAD", "origin/main"]).returncode:
|
||||
tag_commit = subprocess.check_output(
|
||||
["git", "rev-parse", f"refs/tags/{tag}^{{commit}}"], text=True
|
||||
).strip()
|
||||
if subprocess.run(
|
||||
["git", "merge-base", "--is-ancestor", tag_commit, "origin/main"]
|
||||
).returncode:
|
||||
raise SystemExit("release tag is not contained in main")
|
||||
if not requested_version:
|
||||
head_commit = subprocess.check_output(
|
||||
["git", "rev-parse", "HEAD"], text=True
|
||||
).strip()
|
||||
if head_commit != tag_commit:
|
||||
raise SystemExit("tag-triggered checkout does not match the release tag")
|
||||
with Path(os.environ["GITEA_ENV"]).open("a", encoding="utf-8") as env_file:
|
||||
env_file.write(f"RELEASE_TAG={tag}\n")
|
||||
subprocess.run(["git", "checkout", "--detach", tag_commit], check=True)
|
||||
PY
|
||||
- name: Build and publish developer package
|
||||
- name: Build developer package
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python -m pip install --disable-pip-version-check build==1.5.0 twine==7.0.0
|
||||
python -m build --wheel --outdir dist packages/govoplan-meta
|
||||
python -m twine check dist/*.whl
|
||||
python - <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
wheels = tuple(Path("dist").glob("*.whl"))
|
||||
if len(wheels) != 1:
|
||||
raise SystemExit("meta release must contain exactly one wheel")
|
||||
wheel = wheels[0]
|
||||
evidence = {
|
||||
"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": [
|
||||
{
|
||||
"filename": wheel.name,
|
||||
"sha256": hashlib.sha256(wheel.read_bytes()).hexdigest(),
|
||||
"size": wheel.stat().st_size,
|
||||
}
|
||||
],
|
||||
}
|
||||
Path("dist/package-artifacts.json").write_text(
|
||||
json.dumps(evidence, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
PY
|
||||
- name: Retain package hash evidence
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32
|
||||
with:
|
||||
name: developer-meta-package
|
||||
path: dist/package-artifacts.json
|
||||
- name: Check immutable registry state
|
||||
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
|
||||
|
||||
project = tomllib.loads(
|
||||
Path("packages/govoplan-meta/pyproject.toml").read_text(encoding="utf-8")
|
||||
)["project"]
|
||||
wheels = tuple(Path("dist").glob("*.whl"))
|
||||
if len(wheels) != 1:
|
||||
raise SystemExit("meta release must contain exactly one wheel")
|
||||
wheel = wheels[0]
|
||||
digest = hashlib.sha256(wheel.read_bytes()).hexdigest()
|
||||
package_url = "/".join(
|
||||
(
|
||||
"https://git.add-ideas.de/api/v1/packages/GovOPlaN",
|
||||
"pypi",
|
||||
quote(str(project["name"]), safe=""),
|
||||
quote(str(project["version"]), safe=""),
|
||||
"files",
|
||||
)
|
||||
)
|
||||
request = Request(
|
||||
package_url,
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"Authorization": f"token {os.environ['PACKAGE_TOKEN']}",
|
||||
},
|
||||
)
|
||||
publish = True
|
||||
try:
|
||||
with urlopen(request, timeout=30) as response:
|
||||
files = json.load(response)
|
||||
except HTTPError as exc:
|
||||
if exc.code != 404:
|
||||
raise
|
||||
else:
|
||||
if not isinstance(files, list) or len(files) != 1:
|
||||
raise SystemExit("immutable meta-package has an unexpected file set")
|
||||
if files[0].get("sha256") != digest:
|
||||
raise SystemExit(
|
||||
"immutable meta-package already exists with a different SHA-256"
|
||||
)
|
||||
publish = False
|
||||
with Path(os.environ["GITEA_ENV"]).open("a", encoding="utf-8") as env_file:
|
||||
env_file.write(f"PUBLISH_PYPI={int(publish)}\n")
|
||||
PY
|
||||
- name: Publish developer package
|
||||
env:
|
||||
PACKAGE_USERNAME: ${{ secrets.GOVOPLAN_PACKAGE_USERNAME }}
|
||||
PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
|
||||
@@ -40,10 +168,11 @@ jobs:
|
||||
set -euo pipefail
|
||||
test -n "$PACKAGE_USERNAME"
|
||||
test -n "$PACKAGE_TOKEN"
|
||||
python -m pip install --disable-pip-version-check build==1.5.0 twine==7.0.0
|
||||
python -m build --wheel --outdir dist packages/govoplan-meta
|
||||
python -m twine check dist/*.whl
|
||||
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
|
||||
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 developer meta-package is already present; skipping immutable retry."
|
||||
fi
|
||||
|
||||
@@ -92,6 +92,17 @@ jobs:
|
||||
if image_pattern.fullmatch(os.environ[name]) is None:
|
||||
raise SystemExit(f"{name} must be an exact sha256 image reference")
|
||||
PY
|
||||
- name: Resolve immutable release source
|
||||
working-directory: govoplan
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
git fetch --force --no-tags origin "refs/tags/v$VERSION:refs/tags/v$VERSION"
|
||||
mkdir -p runtime-output
|
||||
git rev-parse "v$VERSION^{commit}" > runtime-output/release-source-commit
|
||||
grep -Eq '^[0-9a-f]{40}$' runtime-output/release-source-commit
|
||||
git show "v$VERSION:requirements-release.txt" > runtime-output/requirements-release.source.txt
|
||||
git show "v$VERSION:packages/govoplan-meta/pyproject.toml" > runtime-output/govoplan-meta.source.toml
|
||||
- name: Use HTTPS for GovOPlaN repositories
|
||||
run: |
|
||||
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "git@git.add-ideas.de:GovOPlaN/govoplan"
|
||||
@@ -110,6 +121,9 @@ jobs:
|
||||
.runtime-build/bin/python -m pip install --upgrade pip cryptography
|
||||
.runtime-build/bin/python tools/release/generate-release-package-set.py \
|
||||
--version "$VERSION" \
|
||||
--profile full \
|
||||
--requirements runtime-output/requirements-release.source.txt \
|
||||
--meta-package runtime-output/govoplan-meta.source.toml \
|
||||
--output runtime-output/release-packages.json
|
||||
.runtime-build/bin/python tools/release/resolve-package-artifacts.py \
|
||||
--package-set runtime-output/release-packages.json \
|
||||
@@ -118,9 +132,10 @@ jobs:
|
||||
--lock-output runtime-output/package-artifacts.lock.json \
|
||||
--requirements-output runtime-output/requirements-release.packages.txt \
|
||||
--python .runtime-build/bin/python
|
||||
PYTHON=.runtime-build/bin/python \
|
||||
PYTHON="$PWD/.runtime-build/bin/python" \
|
||||
GOVOPLAN_WEBUI_PACKAGE_LOCK="$PWD/runtime-output/package-artifacts.lock.json" \
|
||||
GOVOPLAN_WEBUI_PACKAGE_DIR="$PWD/runtime-output/webui-packages" \
|
||||
GOVOPLAN_WEBUI_INSTALL_ALL_PACKAGES=true \
|
||||
bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
|
||||
npm --prefix ../govoplan-core/webui run build
|
||||
.runtime-build/bin/python tools/release/prepare-runtime-context.py \
|
||||
@@ -255,7 +270,6 @@ jobs:
|
||||
working-directory: govoplan
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
SOURCE_COMMIT: ${{ gitea.sha }}
|
||||
SIGNING_KEY: ${{ secrets.RUNTIME_DISTRIBUTION_SIGNING_KEY }}
|
||||
SIGNING_KEY_ID: ${{ secrets.RUNTIME_DISTRIBUTION_SIGNING_KEY_ID }}
|
||||
TRUSTED_KEYRING: ${{ secrets.RUNTIME_DISTRIBUTION_KEYRING }}
|
||||
@@ -266,6 +280,7 @@ jobs:
|
||||
GARAGE_IMAGE: ${{ inputs.garage_image }}
|
||||
TEST_MAIL_IMAGE: ${{ inputs.test_mail_image }}
|
||||
run: |
|
||||
SOURCE_COMMIT="$(cat runtime-output/release-source-commit)"
|
||||
test -n "$SIGNING_KEY"
|
||||
test -n "$SIGNING_KEY_ID"
|
||||
test -n "$TRUSTED_KEYRING"
|
||||
@@ -363,9 +378,9 @@ jobs:
|
||||
working-directory: govoplan
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
SOURCE_COMMIT: ${{ gitea.sha }}
|
||||
GITEA_RELEASE_TOKEN: ${{ secrets.GOVOPLAN_RELEASE_TOKEN }}
|
||||
run: |
|
||||
SOURCE_COMMIT="$(cat runtime-output/release-source-commit)"
|
||||
python tools/release/publish-runtime-release.py \
|
||||
--tag "v$VERSION" \
|
||||
--target-commit "$SOURCE_COMMIT" \
|
||||
|
||||
@@ -9,6 +9,7 @@ tools/release/runtime/*
|
||||
!tools/release/runtime/Dockerfile.api
|
||||
!tools/release/runtime/Dockerfile.web
|
||||
!tools/release/runtime/nginx.conf
|
||||
!tools/release/runtime/web-entrypoint.sh
|
||||
__pycache__/
|
||||
build/
|
||||
dist/
|
||||
|
||||
@@ -15,6 +15,13 @@ platform behavior remains owned by the corresponding module repository.
|
||||
|
||||
## Working Rules
|
||||
|
||||
- Start repeated workflows with `./devkit commands`; use `context --changed`,
|
||||
`check --profile quick --changed --dry-run`, and `review MODULE` instead of
|
||||
reconstructing repository/check inventories. See `docs/operations/DEVKIT.md`.
|
||||
- Prefer compact check receipts (`status`, `summary`, `logs`) to repeated full
|
||||
log dumps. `check --profile full` retains the required focused gate; targeted
|
||||
profiles or cached results do not waive required verification or approvals.
|
||||
|
||||
- Treat Gitea issues as the canonical backlog and state log.
|
||||
- Preserve optional module boundaries and use Core contracts or capabilities for integrations.
|
||||
- Prefer targeted checks before full workspace scans.
|
||||
|
||||
@@ -19,6 +19,17 @@ installed modules/connectors discovered by core.
|
||||
|
||||
## Common Commands
|
||||
|
||||
For repeated development and review workflows, start with the unified command
|
||||
suite. [Developer command guide](docs/operations/DEVKIT.md) documents profiles,
|
||||
evidence, safe Git/release operations and reuse in other projects.
|
||||
|
||||
```sh
|
||||
./devkit commands
|
||||
./devkit context --changed
|
||||
./devkit check --profile quick --changed --dry-run
|
||||
./devkit review campaign
|
||||
```
|
||||
|
||||
Create the whole-product development virtualenv in this meta repository:
|
||||
|
||||
```sh
|
||||
@@ -123,7 +134,7 @@ Synchronize module package workflows and inspect the registry release contract:
|
||||
|
||||
Package publication, exact artifact locking, and the optional `govoplan`
|
||||
developer meta-package are documented in
|
||||
[Package Registry Releases](docs/PACKAGE_REGISTRY_RELEASES.md).
|
||||
[Package Registry Releases](docs/operations/PACKAGE_REGISTRY_RELEASES.md).
|
||||
|
||||
For reproducible release artifacts, set `SOURCE_DATE_EPOCH` to the release
|
||||
commit timestamp (or pass an explicit timezone-qualified `--timestamp`):
|
||||
@@ -170,12 +181,19 @@ Create and validate a private, declarative installation bundle:
|
||||
```
|
||||
|
||||
The current executable slice and remaining production gates are documented in
|
||||
[Installation and Deployment Architecture](docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md).
|
||||
[Installation and Deployment Architecture](docs/operations/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md).
|
||||
The canonical distinction between local source development, split source
|
||||
integration, immutable single-host rehearsal, one-host production and
|
||||
multi-host Kubernetes production is in
|
||||
[Deployment Profiles](docs/operations/DEPLOYMENT_PROFILES.md).
|
||||
Same-host replica balancing and the multi-host promotion boundary are documented
|
||||
in [Scaling and Multi-Host Deployment](docs/SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
|
||||
in [Scaling and Multi-Host Deployment](docs/operations/SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
|
||||
Create, update, pause, resume, verify and remove a local or multi-hypervisor K3s
|
||||
VM target with the guarded lifecycle documented in
|
||||
[Kubernetes VM Test Lab](docs/operations/KUBERNETES_TEST_LAB.md).
|
||||
The recovery state machine, migration rollback boundary, and required restore
|
||||
drills are documented in
|
||||
[Recovery and Rollback Guarantees](docs/RECOVERY_AND_ROLLBACK_GUARANTEES.md).
|
||||
[Recovery and Rollback Guarantees](docs/operations/RECOVERY_AND_ROLLBACK_GUARANTEES.md).
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -188,47 +206,14 @@ such as `~/.config/gitea/gitea.env` and be passed with `--env-file`.
|
||||
|
||||
## Structure
|
||||
|
||||
The repository categories are documented in
|
||||
`docs/REPOSITORY_STRUCTURE.md`. The machine-readable list lives in
|
||||
`repositories.json`; the clickable human-readable index is
|
||||
`docs/REPOSITORY_INDEX.md`.
|
||||
Start with the [documentation map](docs/README.md). It separates stable
|
||||
strategy, architecture, operations, project reference, pinned evidence, and
|
||||
historical records and identifies the canonical source for each question.
|
||||
|
||||
Meta ownership and module install/contract boundaries are documented in
|
||||
`docs/META_REPO_SCAN.md` and `docs/MODULE_CONTRACTS_AND_INSTALLS.md`.
|
||||
Frontend layout principles for module pages are documented in
|
||||
`docs/FRONTEND_LAYOUT_PRINCIPLES.md`.
|
||||
The provider-neutral datasource boundary and reusable Dataflow/Workflow graph
|
||||
contract are documented in
|
||||
`docs/DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md`.
|
||||
The cross-product destination, stakeholder visions, configuration archetypes,
|
||||
connected outcome stories, and capability horizons are documented in
|
||||
the [Connected Governance Platform Roadmap](docs/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md).
|
||||
The reconciled institutional semantics, source-authority modes, module layers,
|
||||
candidate Mandates/Services/Parties/Decisions boundaries, and migration
|
||||
sequence are documented in the
|
||||
[Institutional Governance Target Architecture](docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).
|
||||
The selected Campaign-to-Postbox-to-data-to-collaboration implementation path,
|
||||
including stage gates and shared documentation expectations, is in the
|
||||
[Reference Journey Program](docs/REFERENCE_JOURNEY_PROGRAM.md).
|
||||
The administrator journey from Core-only bootstrap through online module
|
||||
installation, scale-out, and reversible environment promotion is defined in
|
||||
[System Administrator Lifecycle User Story](docs/SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md).
|
||||
The corresponding host deployment compiler, managed/external component choices,
|
||||
reconfiguration semantics, and safe Web update boundary are defined in
|
||||
[Installation and Deployment Architecture](docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md).
|
||||
The concrete replica, worker-node, load-balancer, and shared-state topology is
|
||||
defined in [Scaling and Multi-Host Deployment](docs/SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
|
||||
Durable deployment journals, Core recovery evidence, and the distinction
|
||||
between pre-migration configuration restore and post-migration forward recovery
|
||||
are defined in
|
||||
[Recovery and Rollback Guarantees](docs/RECOVERY_AND_ROLLBACK_GUARANTEES.md).
|
||||
The first Campaign-centric capability and infrastructure fit assessment is in
|
||||
`docs/CAPABILITY_AND_INFRASTRUCTURE_FIT.md`. Its rerun tooling can collect and
|
||||
verify a bounded installed composition; target, provider and production claims
|
||||
remain separate, expiring attestations signed by independently scoped proof
|
||||
authorities. The operational issuance, target-run, recovery-measurement, key
|
||||
custody, and promotion-gate procedure is in
|
||||
[Target Maturity Evidence Runbook](docs/TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
|
||||
The machine-readable repository list lives in `repositories.json`; the
|
||||
clickable directory is the
|
||||
[Repository Index](docs/project/REPOSITORY_INDEX.md), and ownership boundaries
|
||||
are in [Repository Structure](docs/project/REPOSITORY_STRUCTURE.md).
|
||||
|
||||
# GovOPlaN Docker
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
This profile runs the shared services that production depends on while keeping
|
||||
API, worker, scheduler, and WebUI code in the editable local repositories.
|
||||
It is the **split source integration** profile defined in
|
||||
[`docs/operations/DEPLOYMENT_PROFILES.md`](../../docs/operations/DEPLOYMENT_PROFILES.md). It does not
|
||||
exercise signed application images. Use an installer-generated evaluation
|
||||
Compose bundle for an immutable Dockerized whole-product rehearsal.
|
||||
|
||||
It provides:
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# One stable entry point; no environment activation, installation or server startup.
|
||||
set -eu
|
||||
DEVKIT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
if [ -n "${PYTHON:-}" ]; then
|
||||
DEVKIT_PYTHON=$(command -v "$PYTHON") || { echo "devkit: configured PYTHON is unavailable" >&2; exit 127; }
|
||||
elif [ -x "$DEVKIT_ROOT/.venv/bin/python" ]; then
|
||||
DEVKIT_PYTHON="$DEVKIT_ROOT/.venv/bin/python"
|
||||
else
|
||||
DEVKIT_PYTHON=$(command -v python3)
|
||||
fi
|
||||
exec "$DEVKIT_PYTHON" "$DEVKIT_ROOT/tools/devkit/devkit.py" "$@"
|
||||
@@ -1,77 +0,0 @@
|
||||
# GovOPlaN Frontend Layout Principles
|
||||
|
||||
GovOPlaN modules should choose their page layout by the kind of work the user is
|
||||
doing, not by the repository that owns the feature.
|
||||
|
||||
These concise layout choices are one canonical input to the broader
|
||||
[`INTERFACE_PATTERN_LANGUAGE.md`](INTERFACE_PATTERN_LANGUAGE.md). The current
|
||||
route and rollout evidence lives in
|
||||
[`INTERFACE_SURFACE_INVENTORY.md`](INTERFACE_SURFACE_INVENTORY.md).
|
||||
|
||||
## Structured Data Directories
|
||||
|
||||
Use a full-available-space workspace for structured data directories: files,
|
||||
addresses, calendars, records, mailboxes, document stores, and similar domains
|
||||
where the primary task is browsing, selecting, filtering, inspecting, and acting
|
||||
on related objects.
|
||||
|
||||
Principles:
|
||||
|
||||
- The module route should use the full available content area.
|
||||
- Do not add a separate page heading row above the main workspace.
|
||||
- Prefer persistent navigation panes, such as tree panels, source panels, folder
|
||||
panels, calendar list panels, or mailbox folder panels.
|
||||
- Keep collection navigation and collection-level actions close to the relevant
|
||||
pane header.
|
||||
- In a list-detail workspace such as Scheduling, keep related lists stacked in
|
||||
the left pane and use the remaining main pane for view/create/edit. A single
|
||||
Add action stays in the relevant list-pane header and opens the common main
|
||||
editor; it does not create an additional menu or launcher.
|
||||
- Use bounded widths for navigation/list panes and let the main detail/content
|
||||
pane take the remaining space.
|
||||
- Keep filtering controls inside the pane they affect.
|
||||
- Use overlays, toasts, or floating alerts for transient messages so the
|
||||
workspace height does not change.
|
||||
|
||||
This pattern is appropriate when the user is working inside one coherent data
|
||||
domain and needs spatial continuity.
|
||||
|
||||
## Workflow And Configuration Surfaces
|
||||
|
||||
Use the standard heading/menu/card visual language for workflow structures,
|
||||
settings, administration, dashboards, and pages that collect essentially
|
||||
unrelated areas.
|
||||
|
||||
Principles:
|
||||
|
||||
- A page heading and subnavigation are appropriate when the page explains a
|
||||
task, workflow stage, or administrative area.
|
||||
- Cards are appropriate for repeated independent panels, settings groups,
|
||||
summaries, and dashboard widgets.
|
||||
- Collapsible panels and segmented controls are appropriate when a dense
|
||||
configuration area needs controlled disclosure.
|
||||
- A collapsible card whose sole content is a table gives that table the full
|
||||
available card body; avoid nested cards, duplicate padding, inner max-widths,
|
||||
and nested scrolling.
|
||||
- Avoid forcing workflow/configuration pages into a file-explorer style unless
|
||||
the primary interaction is genuinely directory browsing.
|
||||
|
||||
This pattern is appropriate when the user is comparing or configuring separate
|
||||
concerns rather than navigating one structured object space.
|
||||
|
||||
## Shared Components
|
||||
|
||||
Reusable layout components belong in `govoplan-core` WebUI. Modules may consume
|
||||
shared components from core, but must not import another module's private UI
|
||||
components directly.
|
||||
|
||||
When a module-specific component becomes generally useful, promote it to core
|
||||
with a parameterized API before reusing it elsewhere.
|
||||
|
||||
Non-self-explanatory fields use Core `FieldLabel`; documented omissions must
|
||||
name their accessible-label source. Explicit Discard and dirty navigation use
|
||||
the same Core unsaved-changes dialog. Table action sets retain unavailable row
|
||||
actions as disabled controls and reserve empty-state slots so Add remains
|
||||
aligned. Use central feedback/dialog components; `window.alert` is not an
|
||||
authorized product surface unless a product-owner-approved exception is first
|
||||
recorded in the Core decision ledger.
|
||||
@@ -1,184 +0,0 @@
|
||||
# Package Registry Releases
|
||||
|
||||
GovOPlaN publishes reusable module artifacts through Gitea's native PyPI and
|
||||
npm registries. These packages improve developer installation, release
|
||||
resolution, cacheability, and artifact inspection. They do not replace the
|
||||
signed runtime distribution: the signed manifest and digest-pinned OCI images
|
||||
remain the production deployment authority.
|
||||
|
||||
## Publication boundary
|
||||
|
||||
Every repository with a `pyproject.toml` contains
|
||||
`.gitea/workflows/module-package-release.yml`. The meta repository owns the
|
||||
canonical template and installs it with:
|
||||
|
||||
```bash
|
||||
python tools/repo/sync-module-package-workflows.py --write
|
||||
python tools/repo/sync-module-package-workflows.py --check
|
||||
```
|
||||
|
||||
The workflow runs for `v*` tags and may be dispatched manually for an existing
|
||||
tag. The organization preflight verifies that every package repository protects
|
||||
the `v*` namespace. Before building, the workflow itself verifies that:
|
||||
|
||||
- the tagged commit is contained in `main`;
|
||||
- the tag, Python project version, and optional WebUI package version agree;
|
||||
- package names remain in the `govoplan-*` and `@govoplan/*-webui` namespaces.
|
||||
|
||||
The workflow binds the repository explicitly from the Gitea Actions context.
|
||||
Do not rely on GitHub-compatible environment variables being injected by the
|
||||
runner image; Gitea runners may expose only the context values. Gitea 1.24 job
|
||||
tokens cannot read repository tag-protection settings, so package jobs must not
|
||||
receive a broad administrator token merely to repeat the organization preflight.
|
||||
Run the following before the first publication and after repository or tag-rule
|
||||
changes:
|
||||
|
||||
```bash
|
||||
python tools/gitea/gitea-configure-package-releases.py
|
||||
```
|
||||
|
||||
Preview and dispatch the exact wheel/WebUI versions selected by the developer
|
||||
meta-package with:
|
||||
|
||||
```bash
|
||||
python tools/gitea/gitea-dispatch-package-set.py \
|
||||
--env-file ~/.config/gitea/gitea.env
|
||||
python tools/gitea/gitea-dispatch-package-set.py \
|
||||
--env-file ~/.config/gitea/gitea.env \
|
||||
--apply
|
||||
```
|
||||
|
||||
The dispatcher reads exact versions from `packages/govoplan-meta/pyproject.toml`,
|
||||
inspects the selected tag to determine whether a WebUI package is expected,
|
||||
skips complete registry pairs and does not duplicate an active workflow. Use
|
||||
`--repository govoplan-core` for a bounded dispatch or `--verify-existing` to
|
||||
rebuild and hash-verify versions already present in both registries.
|
||||
|
||||
For coordinated lockstep tags, `push-release-tag.sh` pushes module tags first,
|
||||
Core next, and the meta tag last. This is a dependency guarantee for a
|
||||
single-capacity Actions runner: the developer package cannot run before its
|
||||
exact Core and module versions have entered the queue.
|
||||
|
||||
The same release entry point first validates the migration graph, then records
|
||||
the reviewed current Alembic heads under the target release version and reruns
|
||||
the strict migration audit before it changes package versions, commits, or
|
||||
tags. The default preflight intentionally does not require those heads to exist
|
||||
in the previous release baseline. A failed candidate-baseline check therefore
|
||||
cannot produce a protected package release.
|
||||
|
||||
The source gate validates `pyproject.toml`, the module version declaration
|
||||
(`MODULE_VERSION` or the top-level `ModuleManifest.version`), public package
|
||||
`__version__`, and WebUI metadata before creating tags. Release-tag artifact
|
||||
checks run only after the candidate tags and immutable WebUI lock have been
|
||||
created locally.
|
||||
|
||||
Release-lock regeneration resolves a fresh immutable lock from the reviewed
|
||||
candidate manifests; it does not seed resolution from the previous release
|
||||
lock. This prevents removed transitive packages and stale peer metadata from
|
||||
blocking or contaminating the new release. Candidate resolution also uses an
|
||||
isolated temporary npm cache, so a locally replaced tag cannot reuse metadata
|
||||
from a failed, unpushed release attempt.
|
||||
|
||||
Modules that retain the same WebUI package identity in both a root publish
|
||||
manifest and `webui/package.json` use the WebUI manifest as the canonical peer
|
||||
contract. The coordinated release synchronizes `peerDependencies` and
|
||||
`peerDependenciesMeta` into the publish manifest before creating the module
|
||||
tag, then synchronizes each lockfile root from the final package metadata. A
|
||||
distinct root package remains independent.
|
||||
|
||||
It builds one wheel and, where applicable, one npm tarball. The workflow records
|
||||
the source tag, source commit, filename, size, and SHA-256 in
|
||||
`package-artifacts.json` before publishing. Gitea rejects a second upload of the
|
||||
same package version, so correction requires a new version rather than artifact
|
||||
replacement.
|
||||
|
||||
A retry after partial publication is safe. Before upload, the workflow reads the
|
||||
native package registry file record and compares its SHA-256 with the artifact
|
||||
rebuilt from the protected tag. An exact existing artifact is skipped; a
|
||||
same-version artifact with another digest or an unexpected file set fails
|
||||
closed. This permits a failed npm publication to resume without weakening
|
||||
package immutability or accepting `--skip-existing` blindly.
|
||||
|
||||
The npm tarball is always published through an explicit local `./dist/...`
|
||||
path. Without that prefix, npm may interpret a relative tarball name as a Git
|
||||
package shorthand before it ever contacts the configured registry.
|
||||
|
||||
Published WebUI packages contain registry-compatible dependencies only. The
|
||||
workflow converts an internal dependency pinned to a protected `vX.Y.Z` Git tag
|
||||
into the exact `X.Y.Z` registry version and rejects unresolved `file:` or Git
|
||||
dependencies. Repository development metadata may therefore keep local or Git
|
||||
references without leaking them into the published package contract.
|
||||
Historical `add-ideas` and current `GovOPlaN` organization URLs are accepted
|
||||
for immutable tagged releases; both normalize to the same exact registry
|
||||
dependency and no branch or unversioned Git reference is accepted.
|
||||
|
||||
## One-time Gitea setup
|
||||
|
||||
Protect `v*` tags in every package repository and the meta repository. Allow
|
||||
only the `Owners` team to create or delete those tags.
|
||||
|
||||
```bash
|
||||
set -a
|
||||
. ~/.config/gitea/gitea.env
|
||||
set +a
|
||||
python tools/gitea/gitea-configure-package-releases.py --apply
|
||||
```
|
||||
|
||||
Create a dedicated personal access token with only `write:package` scope and
|
||||
store these organization-level Actions secrets on `GovOPlaN`:
|
||||
|
||||
- `GOVOPLAN_PACKAGE_USERNAME`: account owning the package token;
|
||||
- `GOVOPLAN_PACKAGE_TOKEN`: dedicated package-write token.
|
||||
|
||||
Do not use an administrator or general release token. Gitea 1.24 does not grant
|
||||
package publication to the automatic Actions job token. Organization secrets
|
||||
allow the same least-privilege credential to serve every module workflow.
|
||||
|
||||
## Exact release consumption
|
||||
|
||||
`tools/release/generate-release-package-set.py` translates the reviewed Git
|
||||
source refs in `requirements-release.txt` into an exact registry package set.
|
||||
It resolves each version tag to its commit and verifies the package metadata in
|
||||
that tag.
|
||||
|
||||
`tools/release/resolve-package-artifacts.py` then downloads exactly those wheel
|
||||
and WebUI versions from Gitea. It reads the identity embedded in every wheel and
|
||||
npm tarball, rejects missing, duplicate, unexpected, or oversized artifacts,
|
||||
and writes `package-artifacts.lock.json` with SHA-256 values and npm integrity
|
||||
values. Credentials are accepted only through environment variables and are
|
||||
never written to the lock. Python resolution ignores ambient pip configuration
|
||||
and extra indexes for GovOPlaN roots, preventing an internal package name from
|
||||
being selected from an undeclared registry.
|
||||
|
||||
The runtime distribution workflow uses the verified wheelhouse directly and
|
||||
installs module WebUI tarballs only after matching them to the lock. It publishes
|
||||
the package set, package lock, and hash-locked requirements as release assets.
|
||||
The package-lock SHA-256 is part of the signed distribution manifest. Runtime
|
||||
finalization also requires the lock's package versions and hashes to match the
|
||||
wheel composition embedded in the images. OCI assembly remains network-free
|
||||
after package and third-party dependency resolution.
|
||||
|
||||
The source refs remain in the module catalog for source provenance and release
|
||||
planning. Production installation consumes the signed runtime images rather
|
||||
than invoking `pip`, `npm`, or Git on the target host.
|
||||
|
||||
## Developer meta-package
|
||||
|
||||
`packages/govoplan-meta` builds the optional `govoplan` package. Its default
|
||||
dependencies mirror the reviewed runtime roots; `govoplan[full]` adds all
|
||||
currently packageable workspace modules. Regenerate it after changing release
|
||||
requirements or package versions:
|
||||
|
||||
```bash
|
||||
python tools/release/generate-developer-meta-package.py
|
||||
python tools/release/generate-developer-meta-package.py --check
|
||||
```
|
||||
|
||||
`push-release-tag.sh` performs this synchronization before release commits and
|
||||
tags. The meta-package is for editable/developer setup and composition tests. It
|
||||
does not enable modules, apply migrations, provision services, or establish
|
||||
backup and recovery evidence.
|
||||
|
||||
Generic Packages are intentionally not used. Add that transport only when a
|
||||
consumer needs an artifact format unsupported by PyPI, npm, Gitea Releases, or
|
||||
the OCI registry.
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
# GovOPlaN Documentation
|
||||
|
||||
This directory contains cross-repository product, architecture, delivery, and
|
||||
project documentation. Start here instead of browsing every file.
|
||||
|
||||
## Read First
|
||||
|
||||
| Need | Source |
|
||||
| --- | --- |
|
||||
| Understand the platform in ten minutes | [Platform Core Ideas](strategy/PLATFORM_CORE_IDEAS.md) |
|
||||
| See the intended product sequence | [Roadmap](strategy/ROADMAP.md) |
|
||||
| Check the reconciled state and material gaps | [Strategy Status](strategy/STRATEGY_STATUS.md) |
|
||||
| Find active work, priority, or ownership | [Gitea issue workflow](project/GITEA_ISSUES.md) and Gitea issues |
|
||||
| Understand the selected end-to-end proofs | [Reference Journey Program](strategy/REFERENCE_JOURNEY_PROGRAM.md) |
|
||||
|
||||
The first three documents are the normal entry points. Detailed architecture,
|
||||
runbooks, evidence, and historical assessments support them; they are not
|
||||
parallel roadmaps.
|
||||
|
||||
## Strategy
|
||||
|
||||
| Document | Role |
|
||||
| --- | --- |
|
||||
| [Platform Core Ideas](strategy/PLATFORM_CORE_IDEAS.md) | Stable purpose, principles, planes, distinctions, and non-goals |
|
||||
| [Roadmap](strategy/ROADMAP.md) | Concise product outcomes, horizons, and current sequence |
|
||||
| [Strategy Status](strategy/STRATEGY_STATUS.md) | Only prose source for current cross-product status |
|
||||
| [Reference Journey Program](strategy/REFERENCE_JOURNEY_PROGRAM.md) | Acceptance journeys and their gates |
|
||||
| [Product Input Register](strategy/PRODUCT_INPUT_REGISTER.md) | Normalized ideas and user-story source material |
|
||||
| [System Administrator Lifecycle](strategy/SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md) | Installation and lifecycle outcome story |
|
||||
| [Detailed Connected-Platform Vision](strategy/reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md) | Long-form stakeholder, configuration, and outcome catalogue |
|
||||
|
||||
## Architecture
|
||||
|
||||
| Topic | Canonical source |
|
||||
| --- | --- |
|
||||
| Institutional model and ownership | [Institutional Governance Target Architecture](architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md) |
|
||||
| Product experience and technical boundaries | [Product Experience and Module Boundaries](architecture/PRODUCT_EXPERIENCE_AND_MODULE_BOUNDARIES.md) |
|
||||
| Shared interface and layout rules | [Interface Pattern Language](architecture/INTERFACE_PATTERN_LANGUAGE.md) |
|
||||
| Focused task views | [Views Architecture](architecture/VIEWS_ARCHITECTURE.md) |
|
||||
| Product areas and task-local tools | [Quick Access and Product Areas](architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md) |
|
||||
| Platform self-description and configuration | [Platform Control Plane](architecture/PLATFORM_CONTROL_PLANE.md) |
|
||||
| Data sources, definitions, and graph execution | [Datasource and Definition Graph Architecture](architecture/DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md) |
|
||||
| Federation between autonomous installations | [Federated GovOPlaN Architecture](architecture/FEDERATED_GOVOPLAN_ARCHITECTURE.md) |
|
||||
| Institutional digital twin | [Institutional Digital Twin](architecture/INSTITUTIONAL_DIGITAL_TWIN.md) |
|
||||
| Assisted and non-digital participation | [Assisted and Non-Digital Channels](architecture/ASSISTED_AND_NON_DIGITAL_CHANNELS.md) |
|
||||
|
||||
Module-specific architecture remains in the owning repository. In particular,
|
||||
information-governance adoption is in
|
||||
`govoplan-core/docs/INFORMATION_GOVERNANCE_ADOPTION.md`, and the eAkte model is
|
||||
in `govoplan-records/docs/EAKTE_ARCHITECTURE.md`.
|
||||
|
||||
## Operations
|
||||
|
||||
| Need | Source |
|
||||
| --- | --- |
|
||||
| Installation model and managed components | [Installation and Deployment Architecture](operations/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md) |
|
||||
| Supported operating modes | [Deployment Profiles](operations/DEPLOYMENT_PROFILES.md) |
|
||||
| Horizontal scaling and multi-host topology | [Scaling and Multi-Host Deployment](operations/SCALING_AND_MULTI_HOST_DEPLOYMENT.md) |
|
||||
| Local Kubernetes evidence target | [Kubernetes VM Test Lab](operations/KUBERNETES_TEST_LAB.md) |
|
||||
| Recovery guarantees and state machine | [Recovery and Rollback Guarantees](operations/RECOVERY_AND_ROLLBACK_GUARANTEES.md) |
|
||||
| Recovery-ledger rollout | [Recovery Ledger Adoption](operations/RECOVERY_LEDGER_ADOPTION.md) |
|
||||
| Backup evidence contract | [Backup and Restore Evidence](operations/BACKUP_AND_RESTORE_EVIDENCE.md) |
|
||||
| Target handoff and independent evidence | [Production Target Handoff](operations/PRODUCTION_TARGET_HANDOFF.md) |
|
||||
| Evidence collection and promotion | [Target Maturity Evidence Runbook](operations/TARGET_MATURITY_EVIDENCE_RUNBOOK.md) |
|
||||
| Package publication and consumption | [Package Registry Releases](operations/PACKAGE_REGISTRY_RELEASES.md) |
|
||||
| Release-console operation | [Release Console](operations/RELEASE_CONSOLE.md) |
|
||||
| Repeated development, verification and evidence commands | [Developer Command Suite](operations/DEVKIT.md) |
|
||||
| Module compatibility and install behavior | [Module Contracts and Installs](operations/MODULE_CONTRACTS_AND_INSTALLS.md) |
|
||||
| Security-audit toolchain | [Security Audit](operations/SECURITY_AUDIT.md) |
|
||||
|
||||
## Project Reference
|
||||
|
||||
- [Repository Index](project/REPOSITORY_INDEX.md) is the human-readable module
|
||||
and repository directory; `../repositories.json` is authoritative for tools.
|
||||
- [Repository Structure](project/REPOSITORY_STRUCTURE.md) defines ownership of
|
||||
meta, module, deployment, and website content.
|
||||
- [Gitea Issues](project/GITEA_ISSUES.md) defines labels, templates, import, and
|
||||
state-update conventions.
|
||||
- [UI Review Program](project/UI_REVIEW_PROGRAM.md) defines the principle-led,
|
||||
per-module review process and links the canonical Gitea review inventory.
|
||||
|
||||
## Evidence And Archive
|
||||
|
||||
Pinned evidence is retained under `evidence/`; completed reviews and migration
|
||||
inventories are under `archive/`. They explain or prove a dated state and must
|
||||
not be read as current product status.
|
||||
|
||||
- [Generated Campaign capability and infrastructure fit, 2026-07-22](evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md)
|
||||
- [Supporting narrative for the 2026-07-22 assessment](evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.md)
|
||||
- [Interface surface inventory, 2026-08-03](evidence/snapshots/INTERFACE_SURFACE_INVENTORY.md)
|
||||
- [Strategic review, 2026-08-05](archive/2026-08/STRATEGIC_REVIEW_2026-08-05.md)
|
||||
- [Meta repository scan, 2026-07-13](archive/2026-07/META_REPO_SCAN.md)
|
||||
- [Meta repository migration audit](archive/2026-07/META_REPOSITORY_MIGRATION_AUDIT.md)
|
||||
|
||||
The JSON files at the root of this directory are machine-readable schemas,
|
||||
evidence inputs, and project configuration. Their paths are intentionally
|
||||
stable because tools and published schema identifiers consume them; they are
|
||||
not additional reading-list entries.
|
||||
|
||||
Regenerate and verify the human fit report from its JSON input with:
|
||||
|
||||
```sh
|
||||
./.venv/bin/python tools/assessments/generate-capability-fit-report.py
|
||||
./.venv/bin/python tools/assessments/generate-capability-fit-report.py --check
|
||||
```
|
||||
|
||||
## Maintenance Rules
|
||||
|
||||
1. Gitea issues are the only live source for work state, priority, and owner.
|
||||
2. `strategy/STRATEGY_STATUS.md` is the only prose reconciliation of current
|
||||
portfolio state. Do not copy its volatile counts into durable documents.
|
||||
3. Durable documents state decisions, invariants, ownership, and acceptance
|
||||
gates. They link to Gitea for implementation detail.
|
||||
4. Dated evidence and archive documents retain their original composition and
|
||||
conclusion. Add a snapshot notice instead of silently modernizing them.
|
||||
5. Module-specific behavior and user/admin documentation stay in the owning
|
||||
repository. Meta documentation covers cross-module outcomes and contracts.
|
||||
6. Do not add another top-level Markdown file. Place new content in the
|
||||
appropriate directory and add it to this map only when it has a distinct
|
||||
canonical purpose.
|
||||
7. A new strategy document must replace, narrow, or become a reference for an
|
||||
existing source; it must not introduce a parallel roadmap.
|
||||
8. The Product Input Register preserves source ideas. Only a named journey,
|
||||
package, or Gitea issue turns an idea into implementation work.
|
||||
|
||||
After moving or adding documentation, run:
|
||||
|
||||
```sh
|
||||
./.venv/bin/python -m unittest tests.test_documentation_structure
|
||||
```
|
||||
@@ -0,0 +1,177 @@
|
||||
# Assisted and Non-Digital Channels
|
||||
|
||||
## Purpose
|
||||
|
||||
GovOPlaN must support people who cannot or do not use a self-service portal.
|
||||
Telephone, paper, in-person service, authorized representation, mobile staff,
|
||||
interpreters, and temporary offline work are not exceptional side systems.
|
||||
They are governed channels into the same service, case, workflow, record, and
|
||||
decision.
|
||||
|
||||
The goal is equivalent institutional treatment, not forced channel identity.
|
||||
The system preserves which channel was used and which evidence is available
|
||||
without giving digitally confident users stronger substantive rights.
|
||||
|
||||
The first end-to-end journey is tracked in
|
||||
[GovOPlaN #42](https://git.add-ideas.de/GovOPlaN/govoplan/issues/42).
|
||||
|
||||
## Actor Model
|
||||
|
||||
Every assisted interaction distinguishes:
|
||||
|
||||
- the affected person or organization;
|
||||
- the real staff member or external helper entering information;
|
||||
- the represented party and representation basis;
|
||||
- an interpreter, witness, guardian, or support person where relevant;
|
||||
- the responsible institutional function;
|
||||
- the channel and location;
|
||||
- the person who reviewed or confirmed the captured information.
|
||||
|
||||
"Entered by" is not "declared by". "Declared by" is not "verified by".
|
||||
Authentication assurance, representation authority, and evidence quality are
|
||||
separate fields.
|
||||
|
||||
## Channel-Neutral Intake Contract
|
||||
|
||||
All channels create the same versioned service/form submission contract with
|
||||
additional provenance:
|
||||
|
||||
- service, form, schema, language, and accessibility version;
|
||||
- valid and recorded time;
|
||||
- channel (`portal`, `counter`, `telephone`, `paper`, `email`, `mobile`,
|
||||
`representative`, `offline_import`, or configured extension);
|
||||
- affected and represented parties;
|
||||
- capture actor and responsible function;
|
||||
- consent, notice, purpose, legal basis, and information source;
|
||||
- field-level source and confidence where staff transcribed or inferred data;
|
||||
- attachments, scans, originals, signatures, recordings, and attestations as
|
||||
governed evidence references;
|
||||
- read-back/confirmation result and correction path;
|
||||
- receipt and chosen return channels;
|
||||
- duplicate/matching assessment and any manual resolution.
|
||||
|
||||
Forms Runtime owns the submission lifecycle. Parties owns procedural capacity
|
||||
and representation. Identity/Addresses own subject and contact references.
|
||||
Cases owns the matter. Records owns filing and retention. Audit preserves the
|
||||
action/effect evidence.
|
||||
|
||||
## Assisted Session
|
||||
|
||||
An assisted session is a resumable work item, not a privileged bypass. It:
|
||||
|
||||
1. selects service, language, channel, affected party, and represented capacity;
|
||||
2. shows the staff member only fields and evidence relevant to the service;
|
||||
3. explains why sensitive data is requested and what evidence quality is
|
||||
required;
|
||||
4. records source per value when information comes from speech, paper, an
|
||||
existing register, or staff observation;
|
||||
5. validates and previews consequences before submission;
|
||||
6. supports read-back, correction, confirmation, and a second-person check
|
||||
where policy requires it;
|
||||
7. generates an accessible receipt through the requested channel;
|
||||
8. creates follow-up tasks when original documents, signatures, translation,
|
||||
or verification remain outstanding.
|
||||
|
||||
The first executable slice is implemented in Forms Runtime for authenticated
|
||||
assisted sessions. Administrators enable an exact published Form revision;
|
||||
operators then record channel, party and representation references, authority,
|
||||
purpose, notice, responsible function, language, accessibility needs, and
|
||||
field-level source/confidence provenance. Read-back outcomes are append-only and
|
||||
payload-bound. A draft correction changes the Form revision and invalidates the
|
||||
prior confirmation for submission. The resident-parking-permit fixture proves
|
||||
resume and submission enforcement; browser accessibility and target archive
|
||||
evidence remain acceptance work.
|
||||
|
||||
The helper's normal account and represented function remain in the audit
|
||||
chain. Assistance never grants access to unrelated records about the person.
|
||||
|
||||
## Paper And Scanning
|
||||
|
||||
- Register receipt before scanning so custody and deadlines do not depend on
|
||||
successful OCR.
|
||||
- Store the original scan or external archive reference with digest, pages,
|
||||
capture device/provider, time, operator, and quality assessment.
|
||||
- Treat OCR and extracted fields as derived data with confidence and source
|
||||
coordinates. A person confirms consequential values.
|
||||
- Support separation, ordering, missing-page, duplicate, malware, and
|
||||
readability review.
|
||||
- File the resulting document and submission into the appropriate eAkte;
|
||||
retain or return the physical original according to policy.
|
||||
- Produce cover sheets, barcodes, and return instructions through Templates,
|
||||
not a separate print domain.
|
||||
|
||||
## Telephone And In-Person Handling
|
||||
|
||||
- Show a scripted but adaptable interview from the same Form definition.
|
||||
- Record how identity and representation were checked; do not equate caller ID
|
||||
with identity proof.
|
||||
- Require explicit confirmation of consequential declarations and capture the
|
||||
method (read-back, signed summary, one-time code, witness, later letter).
|
||||
- Record call audio only when a lawful, declared profile permits it; an
|
||||
interaction note is the default.
|
||||
- Make interrupted sessions resumable without exposing prior answers to an
|
||||
unauthorized caller or visitor.
|
||||
|
||||
## Offline And Mobile Work
|
||||
|
||||
Offline packages are encrypted, device-bound, time-limited, purpose-limited,
|
||||
and contain only the required forms/reference data. Synchronization uses
|
||||
idempotent intents and exposes conflicts rather than last-write-wins. Device
|
||||
loss, expiry, revocation, duplicate submission, clock drift, and outcome
|
||||
unknown have explicit recovery paths.
|
||||
|
||||
## Outbound Non-Digital Delivery
|
||||
|
||||
Campaign and Postbox model one delivery intent with channel choices and policy:
|
||||
|
||||
- portal/postbox delivery;
|
||||
- email;
|
||||
- print and postal fulfillment through a managed provider or local handoff;
|
||||
- in-person collection;
|
||||
- telephone notification followed by durable confirmation;
|
||||
- accessible or language-specific variants.
|
||||
|
||||
Distribution preferences are purpose- and service-specific, effective-dated,
|
||||
and may be overridden only by a documented legal or urgent-delivery rule. A
|
||||
fallback occurs only before a channel has accepted the effect unless policy
|
||||
explicitly authorizes duplicate delivery. Receipts distinguish creation,
|
||||
provider acceptance, dispatch, delivery, return, and acknowledgement.
|
||||
|
||||
## Accessibility And Equality
|
||||
|
||||
- The person can request language, easy-language, large-print, screen-reader,
|
||||
sign-language, relay, interpreter, or representative support without those
|
||||
preferences becoming a general-purpose profile visible everywhere.
|
||||
- Staff interfaces support keyboard-only capture, clear focus, error summary,
|
||||
read-back, and printable/offline alternatives.
|
||||
- Channel choice and need for assistance must not be used as an adverse risk
|
||||
signal.
|
||||
- Reports compare completion, wait, correction, abandonment, and outcome by
|
||||
channel only under a declared equality/service-quality purpose and with
|
||||
privacy thresholds.
|
||||
|
||||
## Security And Abuse Controls
|
||||
|
||||
- purpose-aware field access and session timeout;
|
||||
- current authority checks for every read and effect;
|
||||
- dual control for high-risk identity, payment, address, or representation
|
||||
changes;
|
||||
- immutable source/attestation evidence and correction history;
|
||||
- rate and anomaly controls that do not silently reject a person;
|
||||
- explicit safe handling of domestic-abuse, protected-address, witness, or
|
||||
sealed-record cases;
|
||||
- no secret answers or full documents in ordinary operational logs.
|
||||
|
||||
## First Reference Journey
|
||||
|
||||
Implement the permit-to-payment/service-to-decision journey through three
|
||||
equivalent starts:
|
||||
|
||||
1. self-service portal submission;
|
||||
2. staff-assisted counter/telephone submission;
|
||||
3. paper receipt, scan, extraction, confirmation, and filing.
|
||||
|
||||
All three must create the same Case and Workflow contract, preserve different
|
||||
provenance, support correction, produce a receipt, file an eAkte, reach the same
|
||||
decision rules, and prove accessibility, privacy, recovery, and channel
|
||||
fallback in browser and operator tests.
|
||||
@@ -0,0 +1,155 @@
|
||||
# Federated GovOPlaN Architecture
|
||||
|
||||
## Purpose
|
||||
|
||||
Federation lets autonomous GovOPlaN installations exchange data,
|
||||
configuration, work, messages, records, and evidence without sharing a database
|
||||
or surrendering local policy. It is institution-to-institution cooperation,
|
||||
not multi-tenancy across an untrusted network.
|
||||
|
||||
The first implementation should prove a bounded exchange between two
|
||||
installations. A new federation module is not justified until the shared
|
||||
protocol has at least two independent consumers. Core owns neutral envelopes
|
||||
and trust contracts; Connectors owns transport providers; domain modules own
|
||||
the objects and effects they exchange.
|
||||
|
||||
Implementation is tracked in
|
||||
[GovOPlaN #41](https://git.add-ideas.de/GovOPlaN/govoplan/issues/41).
|
||||
|
||||
## Invariants
|
||||
|
||||
1. Every installation remains authoritative for its tenants, identities,
|
||||
policies, keys, records, and local mappings.
|
||||
2. A remote identity or permission never becomes a local authorization claim.
|
||||
3. Every exchange declares purpose, legal/organizational basis, classification,
|
||||
minimization, retention expectation, and permitted onward use.
|
||||
4. Every object reference identifies origin instance, owner tenant, object type,
|
||||
object ID, exact revision, and source-authority mode.
|
||||
5. Payloads and receipts are signed; sensitive transports use mutually
|
||||
authenticated encrypted channels.
|
||||
6. Acceptance, rejection, outcome unknown, retry, revocation, correction, and
|
||||
reconciliation are durable states.
|
||||
7. Local policy may reject or narrow a remote request. It cannot silently claim
|
||||
to have accepted an effect that did not occur.
|
||||
8. Federation works asynchronously and can exchange signed offline bundles
|
||||
where continuous connectivity is unavailable.
|
||||
|
||||
## Trust Domains
|
||||
|
||||
An instance publishes a signed, versioned federation descriptor containing:
|
||||
|
||||
- stable instance and operator identity;
|
||||
- supported protocol and schema versions;
|
||||
- signing and transport key identifiers with rotation history;
|
||||
- accepted object and exchange profiles;
|
||||
- endpoint locations and size/rate limits;
|
||||
- support, incident, revocation, and data-protection contacts;
|
||||
- evidence and conformance references.
|
||||
|
||||
Pairing is a two-sided administrative workflow. Each side verifies the other,
|
||||
maps the remote institution to a local trusted-party record, selects permitted
|
||||
profiles and purposes, sets policy ceilings, and records approvals. Trust is
|
||||
directional and profile-specific; trusting signed Postbox delivery does not
|
||||
automatically permit case transfer or configuration import.
|
||||
|
||||
## Exchange Envelope
|
||||
|
||||
Every request, response, receipt, correction, and revocation uses one neutral
|
||||
envelope with:
|
||||
|
||||
- message ID, correlation ID, causation ID, creation and expiry;
|
||||
- origin and destination instance/institution/tenant references;
|
||||
- real actor and represented institutional capacity where disclosure is
|
||||
permitted;
|
||||
- exchange profile and semantic schema version;
|
||||
- exact domain object references and content digests;
|
||||
- purpose, legal basis, classification, data categories, retention expectation,
|
||||
onward-transfer constraint, and subject notice status;
|
||||
- requested action and idempotency key;
|
||||
- encryption recipients and signature chain;
|
||||
- attachment/object manifests rather than unbounded embedded blobs;
|
||||
- previous-envelope references for correction, replacement, or revocation.
|
||||
|
||||
The envelope is evidence, not a universal domain object. Each owner validates
|
||||
and imports or links its own payload.
|
||||
|
||||
## Exchange Profiles
|
||||
|
||||
| Profile | First owners | Behavior |
|
||||
| --- | --- | --- |
|
||||
| Postbox delivery | Postbox, Campaign, Notifications | Address or derive a remote function-bound postbox, obtain acceptance receipt, and track acknowledgement where permitted |
|
||||
| Case handoff | Cases, Parties, Services, Workflow Engine | Offer exact context and evidence; destination accepts into a new local case and returns the mapping |
|
||||
| Record transfer | Records, Files, DMS, Audit | Transfer or offer a signed record package with file-plan, metadata, content digests, holds, and disposition constraints |
|
||||
| Decision/evidence reference | Decisions, Committee, Audit | Publish a protected exact outcome or verifiable reference without transferring unrelated case content |
|
||||
| Data product publication | Datasources, Dataflow, Reporting | Publish immutable governed materializations with schema, quality, freshness, lineage, and use constraints |
|
||||
| Configuration package | Core, Policy, Views, Workflow, Forms, Templates | Exchange signed definitions; destination assesses compatibility, maps values, derives locally, and never imports secrets |
|
||||
| Search discovery | Search and domain providers | Return permission-filtered metadata or a handoff link; never expose raw remote indexes as local authority |
|
||||
|
||||
## State Machine
|
||||
|
||||
```text
|
||||
draft -> authorized -> queued -> transmitted -> received
|
||||
| |
|
||||
v v
|
||||
outcome_unknown rejected
|
||||
|
|
||||
received -> validating -> accepted -> applied -> acknowledged
|
||||
| | |
|
||||
v v v
|
||||
rejected accepted_ reconciled
|
||||
pending
|
||||
```
|
||||
|
||||
Acceptance means the destination durably owns the received intent. It does not
|
||||
mean the requested domain effect completed. Receipts distinguish transport,
|
||||
validation, acceptance, application, and human acknowledgement.
|
||||
|
||||
## Conflict And Autonomy
|
||||
|
||||
- Incoming native objects become local references, mirrors, or newly owned
|
||||
objects according to the profile. They do not overwrite local authority by
|
||||
ID coincidence.
|
||||
- Local mappings are effective-dated and auditable.
|
||||
- Corrections create a linked revision. They do not erase what the destination
|
||||
previously observed.
|
||||
- Revocation is a request and evidence event; the destination applies its own
|
||||
legal and retention rules.
|
||||
- Configuration imports use assessment and derivation. A remote package cannot
|
||||
weaken local policy or install code implicitly.
|
||||
- A disconnected partner remains a visible pending/failed state; work can be
|
||||
rerouted through an approved alternative channel.
|
||||
|
||||
## Security And Privacy
|
||||
|
||||
- Use mTLS for paired online transports and signed envelopes for end-to-end
|
||||
origin evidence.
|
||||
- Encrypt payload objects for the destination, with key rotation and outcome-
|
||||
unknown recovery; transport encryption alone is insufficient for queued
|
||||
bundles.
|
||||
- Do not put bearer credentials, local permission scopes, or reusable secrets
|
||||
in an exchange.
|
||||
- Rate-limit and size-bound discovery and transfer; quarantine unknown schemas
|
||||
and active content.
|
||||
- Evaluate current local authorization at every effect even when the envelope
|
||||
describes historical authority.
|
||||
- Log metadata separately from protected content so operators can reconcile
|
||||
without broad content access.
|
||||
- Subject access, correction, restriction, legal hold, and deletion requests
|
||||
become federated workflows with local decisions and receipts, not remote
|
||||
direct database operations.
|
||||
|
||||
## First Reference Proof
|
||||
|
||||
1. Pair two disposable installations with independent tenants, keys, and
|
||||
policies.
|
||||
2. Exchange signed descriptors and approve only the Postbox delivery profile.
|
||||
3. Deliver one Campaign message to a remote function-bound Postbox.
|
||||
4. Prove replay safety, rejection, timeout/outcome unknown, retry,
|
||||
acknowledgement, correction, key rotation, and revoked trust.
|
||||
5. Export the complete evidence bundle and restore both sides from backup.
|
||||
6. Add configuration-package exchange only after the delivery proof passes.
|
||||
|
||||
The result is a provider-neutral federation contract. A future dedicated
|
||||
module becomes appropriate only when pairing, trust administration, exchange
|
||||
queues, and evidence have a lifecycle independent of Connectors and the first
|
||||
domain owner.
|
||||
@@ -0,0 +1,149 @@
|
||||
# Institutional Digital Twin
|
||||
|
||||
## Definition
|
||||
|
||||
The institutional digital twin is a governed, time-aware projection of how an
|
||||
institution is constituted and operates. It connects structure, authority,
|
||||
services, work, information, technology, obligations, controls, evidence, and
|
||||
outcomes without becoming a second source of truth.
|
||||
|
||||
The twin is not one editable graph database and not an employee-surveillance
|
||||
system. Domain modules and external systems keep ownership. The twin stores or
|
||||
materializes exact references, declared relationships, provenance, confidence,
|
||||
and projection versions. Changes flow through owner actions.
|
||||
|
||||
Implementation is tracked in
|
||||
[GovOPlaN #43](https://git.add-ideas.de/GovOPlaN/govoplan/issues/43).
|
||||
|
||||
## Questions It Should Answer
|
||||
|
||||
- Which unit and function is responsible for a service, decision, record,
|
||||
system, dataset, control, or risk at a given valid and recorded time?
|
||||
- Which mandates and policies permit or constrain an action?
|
||||
- Which processes, providers, staff capacities, data sources, and records are
|
||||
required to deliver a service?
|
||||
- What is affected if a system, provider, organizational unit, role, package,
|
||||
or legal rule changes?
|
||||
- Where are responsibilities missing, conflicting, expired, or concentrated?
|
||||
- Which controls are evidenced, stale, failed, or dependent on an unverified
|
||||
assertion?
|
||||
- How do actual process traces differ from defined workflows?
|
||||
- Which public outcomes can be explained from protected internal evidence?
|
||||
|
||||
## Projection Planes
|
||||
|
||||
| Plane | Meaning |
|
||||
| --- | --- |
|
||||
| Current | Valid now, reconstructed from owner projections and current provider state |
|
||||
| Historical | Valid at and recorded by selected instants, with present-day security enforced |
|
||||
| Planned | Approved or proposed future structures, services, policies, projects, and package changes |
|
||||
| Observed | Events, process traces, service measures, incidents, effects, and evidence actually recorded |
|
||||
| Scenario | Non-authoritative simulation of a proposed change and its estimated consequences |
|
||||
|
||||
The UI must label these planes unambiguously. Scenario output never becomes an
|
||||
institutional fact until an authorized owner action accepts it.
|
||||
|
||||
## Canonical Graph
|
||||
|
||||
Nodes are stable institutional references, including institution, tenant,
|
||||
unit, function, assignment, mandate, jurisdiction, service, case, party, task,
|
||||
workflow, approval, decision, record, file, message, appointment, dataset,
|
||||
report, provider, system, control, risk, project, asset, and configuration
|
||||
package.
|
||||
|
||||
Edges have:
|
||||
|
||||
- owner and source authority;
|
||||
- relationship type and direction;
|
||||
- valid-from/valid-to and recorded/superseded times;
|
||||
- exact source revision and evidence digest;
|
||||
- institution/tenant boundary;
|
||||
- purpose and visibility classification;
|
||||
- confidence and derivation method for inferred relationships;
|
||||
- correction and replacement references.
|
||||
|
||||
Inferred edges are never displayed as owner assertions. They remain
|
||||
explainable analytical products with source lineage.
|
||||
|
||||
## Ownership And Implementation
|
||||
|
||||
- Core owns neutral institutional references, temporal context, provider
|
||||
registration, and graph projection contracts.
|
||||
- Domain modules publish bounded nodes and edges through provider interfaces.
|
||||
- Search indexes discoverable identities and links.
|
||||
- Reporting materializes governed analytical projections.
|
||||
- Dataflow computes derived relationships, quality checks, and scenarios.
|
||||
- Policy evaluates visibility, purpose, retention, and allowed scenario/action
|
||||
transitions.
|
||||
- Audit supplies observed events and evidence references.
|
||||
- Projects supplies planned change and benefit relationships.
|
||||
- Views renders role- and task-focused twin perspectives.
|
||||
- Workflow Engine coordinates accepted changes but does not edit owner tables.
|
||||
|
||||
No new digital-twin module is required for the first slice. A dedicated owner
|
||||
is justified later if persisted scenario models, graph revisions, and
|
||||
cross-domain projection lifecycle become independent product objects.
|
||||
|
||||
## Beyond The Current Platform
|
||||
|
||||
### Continuous assurance
|
||||
|
||||
Controls become versioned assertions with evidence requirements, evaluation
|
||||
frequency, responsible function, exception workflow, and freshness. Dataflow
|
||||
and provider checks evaluate them continuously; Policy decides whether a stale
|
||||
or failed control advises, requires review, or blocks an effect.
|
||||
|
||||
### Process mining and conformance
|
||||
|
||||
Governed event histories can derive actual paths, wait times, rework, and
|
||||
exceptions. Comparison to Workflow definitions should improve procedures, not
|
||||
rank individuals. Access to personal or small-cohort detail is purpose-limited
|
||||
and separately governed.
|
||||
|
||||
### Change-impact simulation
|
||||
|
||||
A proposed organizational, provider, policy, or package change can be assessed
|
||||
against dependencies, mandates, open work, records, controls, capacity, and
|
||||
recovery plans before activation. Results identify uncertainty rather than
|
||||
inventing precision.
|
||||
|
||||
### Federated institutional models
|
||||
|
||||
Installations can exchange signed public or partner-specific subsets of their
|
||||
service, mandate, provider, and evidence graph. Every side maps the references
|
||||
locally and retains autonomy. Federation does not create one supranational
|
||||
master graph.
|
||||
|
||||
### Accountable assistance
|
||||
|
||||
Assistance may summarize context, identify missing evidence, draft a decision
|
||||
or workflow, propose mappings, and explain policy. Every output records model,
|
||||
inputs, constraints, uncertainty, human review, and accepted edits. Assistance
|
||||
does not become the acting authority.
|
||||
|
||||
### Public evidence chains
|
||||
|
||||
Transparency packages can publish a minimized chain from rule and aggregate
|
||||
facts to decision and observed outcome, with digests proving relation to
|
||||
protected evidence. Public verification does not require disclosure of the
|
||||
underlying personal data.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Do not infer competence, misconduct, intent, or personal performance from
|
||||
graph proximity or incomplete events.
|
||||
- Do not centralize protected content merely to make graph queries easier.
|
||||
- Do not use historical authorization to expose data now prohibited.
|
||||
- Do not let a scenario engine write domain state directly.
|
||||
- Do not hide source authority, freshness, uncertainty, or missing evidence.
|
||||
- Do not retain analytical detail longer than the declared purpose requires.
|
||||
|
||||
## Delivery Slices
|
||||
|
||||
1. Publish exact institutional reference/edge providers for the service-to-
|
||||
decision and monthly-data journeys.
|
||||
2. Build a current/historical dependency explorer with source and access
|
||||
explanations.
|
||||
3. Add planned Project/package changes and bounded impact reports.
|
||||
4. Add control evidence/freshness and process conformance for one journey.
|
||||
5. Prove a minimized federated projection and a public evidence package.
|
||||
+52
-15
@@ -9,20 +9,24 @@ concepts prepared outside the repositories:
|
||||
- `software_big_picture.md`
|
||||
|
||||
The source concepts describe GovOPlaN as an operational governance platform for
|
||||
public institutions. This document merges that direction with the implemented
|
||||
platform state as of 2026-08-01. It is the canonical repository version of the
|
||||
direction. Gitea issues remain the source of truth for delivery state.
|
||||
public institutions. This document is the canonical repository version of that
|
||||
durable architectural direction. Its implementation table records the accepted
|
||||
2026-08-01 baseline; it is not a rolling status report. Current reconciliation
|
||||
lives in [Strategy Status](../strategy/STRATEGY_STATUS.md), and Gitea issues remain the
|
||||
source of truth for delivery state.
|
||||
|
||||
Read this together with:
|
||||
|
||||
- [Connected Governance Platform Roadmap](CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md)
|
||||
- [Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md)
|
||||
- [Module Contracts and Install Boundaries](MODULE_CONTRACTS_AND_INSTALLS.md)
|
||||
- [Connected Governance Platform Roadmap](../strategy/reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md)
|
||||
- [Platform Core Ideas](../strategy/PLATFORM_CORE_IDEAS.md)
|
||||
- [Strategy Status](../strategy/STRATEGY_STATUS.md)
|
||||
- [Reference Journey Program](../strategy/REFERENCE_JOURNEY_PROGRAM.md)
|
||||
- [Module Contracts and Install Boundaries](../operations/MODULE_CONTRACTS_AND_INSTALLS.md)
|
||||
- [Datasource and Definition Graph Architecture](DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md)
|
||||
- [Capability and Infrastructure Fit](CAPABILITY_AND_INFRASTRUCTURE_FIT.md)
|
||||
- [Core Module Architecture](../../govoplan-core/docs/MODULE_ARCHITECTURE.md)
|
||||
- [Core External References and Integration Maturity](../../govoplan-core/docs/EXTERNAL_REFERENCES_AND_INTEGRATION_MATURITY.md)
|
||||
- [Core Action, Effect, and Automation Layer](../../govoplan-core/docs/ACTION_EFFECT_AUTOMATION_LAYER.md)
|
||||
- [Generated Capability and Infrastructure Fit](../evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md)
|
||||
- [Core Module Architecture](../../../govoplan-core/docs/MODULE_ARCHITECTURE.md)
|
||||
- [Core External References and Integration Maturity](../../../govoplan-core/docs/EXTERNAL_REFERENCES_AND_INTEGRATION_MATURITY.md)
|
||||
- [Core Action, Effect, and Automation Layer](../../../govoplan-core/docs/ACTION_EFFECT_AUTOMATION_LAYER.md)
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -70,7 +74,12 @@ compositions described here are now implemented. Subsequent work is
|
||||
**product depth and stronger maturity evidence**, not another runtime rewrite
|
||||
or an unimplemented architecture boundary.
|
||||
|
||||
## Implementation status (2026-08-01)
|
||||
## Accepted implementation baseline (2026-08-01)
|
||||
|
||||
This section is retained as the dated baseline against which the architecture
|
||||
decision was accepted. Later implementation must be reconciled in
|
||||
`STRATEGY_STATUS.md` rather than editing individual rows here into a competing
|
||||
status report.
|
||||
|
||||
The architecture contract is implemented as a bounded, executable vertical
|
||||
slice. The portfolio declarations and provider governance gates apply to the
|
||||
@@ -79,7 +88,7 @@ were proven now have independent persistent owners:
|
||||
|
||||
| Area | Implemented state | Remaining rollout |
|
||||
| --- | --- | --- |
|
||||
| Module portfolio metadata | Core validates versioned architecture layer/kind, maturity evidence, known limits, ownership boundaries, authority modes, reference packages, target-tested providers, and migration/upgrade/recovery/security/operations documentation. | Complete for all 62 source manifests. Focused and release checks enforce `--require-architecture`; a new module cannot enter the workspace without truthful declaration and repository-local evidence. |
|
||||
| Module portfolio metadata | Core validates versioned architecture layer/kind, maturity evidence, known limits, ownership boundaries, authority modes, reference packages, target-tested providers, and migration/upgrade/recovery/security/operations documentation. | Complete for the source manifests in the 2026-08-01 snapshot. Focused and release checks enforce `--require-architecture`; current portfolio counts belong in `STRATEGY_STATUS.md`. |
|
||||
| External providers | Core validates provider objects/field groups, operations, integration maturity, source authority, bounded reads, freshness/health, idempotency, conflicts, outcome-unknown handling, evidence, correction, reconciliation, outage, classification, purpose, retention, and secret handling. Addresses/CardDAV, Files remote storage, Mail SMTP/IMAP, Calendar CalDAV/ICS/Graph/EWS, and Connectors tabular/sanctions providers declare the contract and tenant-bounded secret-free runtime state. | Registry validation rejects any declared external provider without a sanitized state provider. Future adapters must cross the same gate before activation. |
|
||||
| Institutional context | Core provides versioned temporal, actor/representation, institution/unit/function/task/mandate/jurisdiction/service/case/party/work-item/workflow/approval/decision/record, legal-basis, evidence, information-governance, external-source, presentation, and geographic references. Events, automation actions, audit records, and the transactional Audit outbox preserve the envelope. | Owning modules must progressively require the relevant subset for consequential operations. |
|
||||
| Semantic provider contracts | Provider-neutral DTOs and protocols cover Mandate resolution, versioned Service definitions, procedure Parties/representation, and formal Decisions. `govoplan-mandates`, `govoplan-services`, `govoplan-parties`, and `govoplan-decisions` now persist immutable revisions behind those contracts with tenant isolation, bounded reads, replay safety, OCC, migrations, uninstall guards, permissions, APIs, capability documentation, and recovery documentation. | The owners are deliberately headless. Procedure-specific UI remains with consuming modules. |
|
||||
@@ -391,7 +400,9 @@ submodule, configuration fragment, package, or profile.
|
||||
|
||||
- This reconciliation is canonical in the meta repository and mirrored to the
|
||||
Gitea wiki.
|
||||
- All 62 source manifests carry validated evidence-based architecture metadata.
|
||||
- All source manifests in the accepted 2026-08-01 baseline carried validated
|
||||
evidence-based architecture metadata; current counts belong in
|
||||
`STRATEGY_STATUS.md`.
|
||||
- External-reference, action/effect, operational-health, ownership, policy,
|
||||
audit, and documentation primitives compose into one enforced provider
|
||||
declaration and sanitized runtime-state contract.
|
||||
@@ -446,7 +457,7 @@ submodule, configuration fragment, package, or profile.
|
||||
recovery, accessibility, privacy, security, and operator evidence. This is a
|
||||
maturity gate, not missing architecture implementation.
|
||||
|
||||
## What remains after the executable architecture slice
|
||||
## What remains within the accepted 2026-08-01 architecture slice
|
||||
|
||||
The remaining work is not another Core or cross-module architecture rewrite.
|
||||
It falls into two explicitly different categories, neither of which can be
|
||||
@@ -487,12 +498,38 @@ persistence, migrations, recovery/disable semantics, documentation and focused
|
||||
tests. Their remaining tickets concern concrete providers, deeper adapters and
|
||||
target evidence, not an unresolved institutional architecture boundary.
|
||||
|
||||
Everything else described as architecture in this document now has a
|
||||
Everything else described in the accepted baseline of this document now has a
|
||||
repository owner, versioned contract, bounded implementation, migration and
|
||||
recovery boundary where state exists, documentation, and executable evidence.
|
||||
Further work in those modules is product breadth, UX depth, provider adoption,
|
||||
and evidence renewal.
|
||||
|
||||
## Strategic extensions accepted after the baseline
|
||||
|
||||
The completed baseline does not imply that institutional product architecture
|
||||
can no longer grow. The 2026-08-05 strategic review accepted four extensions
|
||||
that consume the existing contracts without reopening the kernel or moving
|
||||
domain ownership into Core:
|
||||
|
||||
- [Product Experience and Module Boundaries](PRODUCT_EXPERIENCE_AND_MODULE_BOUNDARIES.md)
|
||||
separates technical package topology from stable task/object/product
|
||||
surfaces; implementation is tracked in Core #283.
|
||||
- [Federated GovOPlaN Architecture](FEDERATED_GOVOPLAN_ARCHITECTURE.md)
|
||||
defines governed exchange between autonomous installations; implementation
|
||||
is tracked in GovOPlaN #41.
|
||||
- [Assisted and Non-Digital Channels](ASSISTED_AND_NON_DIGITAL_CHANNELS.md)
|
||||
makes channel inclusion part of the service-to-decision journey; the first
|
||||
reference proof is tracked in GovOPlaN #42.
|
||||
- [Institutional Digital Twin](INSTITUTIONAL_DIGITAL_TWIN.md) defines a
|
||||
time-aware, policy-filtered projection over owner data; implementation is
|
||||
tracked in GovOPlaN #43.
|
||||
|
||||
The eAkte depth required by those journeys is owned by Records and specified in
|
||||
`govoplan-records/docs/EAKTE_ARCHITECTURE.md`, tracked in Records #1. These are
|
||||
new product-depth programs with bounded contracts and acceptance journeys, not
|
||||
evidence that the original institutional semantics or module architecture
|
||||
failed.
|
||||
|
||||
## Delivery tracking
|
||||
|
||||
The completed cross-repository architecture epic is
|
||||
+162
-14
@@ -2,12 +2,13 @@
|
||||
|
||||
This document is the cross-repository pattern language for GovOPlaN user
|
||||
interfaces. It turns the existing ethical doctrine, binding UI/UX decisions,
|
||||
layout principles, and module boundary into a common composition and review
|
||||
grammar. It does not replace those sources.
|
||||
layout rules, and module boundary into a common composition and review grammar.
|
||||
This document also owns the former standalone frontend-layout principles.
|
||||
|
||||
The companion [interface surface inventory](INTERFACE_SURFACE_INVENTORY.md)
|
||||
records which surfaces the current code contributes and where each surface
|
||||
enters the rollout.
|
||||
The dated [interface surface inventory](../evidence/snapshots/INTERFACE_SURFACE_INVENTORY.md)
|
||||
records the 2026-08-03 rollout snapshot. Current implementation state belongs
|
||||
in Gitea and generated inventory evidence, not in this durable pattern
|
||||
language.
|
||||
|
||||
## Source Of Truth And Precedence
|
||||
|
||||
@@ -19,14 +20,11 @@ Use the narrowest owning document when changing a rule:
|
||||
2. `govoplan-core/docs/UI_UX_DECISION_LEDGER.md` owns accepted product decisions
|
||||
such as progressive disclosure, adaptive forms, blocker language, guided
|
||||
operations, and the platform theme contract.
|
||||
3. `docs/FRONTEND_LAYOUT_PRINCIPLES.md` owns the high-level choice between a
|
||||
full-space structured-data workspace and a heading/menu/card workflow or
|
||||
configuration surface.
|
||||
4. `govoplan-core/docs/MODULE_ARCHITECTURE.md` owns the shell, route, navigation,
|
||||
3. `govoplan-core/docs/MODULE_ARCHITECTURE.md` owns the shell, route, navigation,
|
||||
UI-capability, and shared-component boundaries.
|
||||
5. This document owns the common pattern names, placement grammar, wording and
|
||||
state conventions, focused-view composition, and definition of done across
|
||||
those sources.
|
||||
4. This document owns the high-level layout choice, common pattern names,
|
||||
placement grammar, wording and state conventions, focused-view composition,
|
||||
and definition of done across those sources.
|
||||
|
||||
If two rules appear to conflict, do not create a third local convention. Record
|
||||
the conflict in the owning decision ledger, resolve it there, and update the
|
||||
@@ -53,6 +51,148 @@ rules:
|
||||
- Preserve a stable way back to the containing object and the broader system.
|
||||
- Do not let navigation, selection, or a view switch imply consent.
|
||||
|
||||
## Shared Component And Layout Architecture
|
||||
|
||||
Core owns the reusable WebUI vocabulary; modules own domain composition and
|
||||
behavior. Centralization follows four layers:
|
||||
|
||||
| Layer | Owner | Examples | Rule |
|
||||
| --- | --- | --- | --- |
|
||||
| Foundation | Core | theme tokens, spacing, typography, focus and responsive breakpoints | Modules consume the contract and do not redefine it. |
|
||||
| Primitives | Core | buttons, fields, dialogs, alerts, cards, tables, loading, empty and blocked states | A matching primitive is reused rather than copied locally. |
|
||||
| Structural layouts | Core | page frame and header, action region, workspace panes, toolbars, grids, form sections and dialog anatomy | Layout owns geometry, scroll, responsive collapse and accessibility, but no domain decisions. |
|
||||
| Domain compositions | Owning module | a campaign review, mailbox, records explorer or operations dashboard | Modules select shared pieces, bind data and permissions, and retain domain wording and consequences. |
|
||||
|
||||
A component belongs in Core when it is used or expected in more than one
|
||||
module and central ownership materially protects accessibility, responsive
|
||||
behavior, localization, contextual help, theming, or interaction consistency.
|
||||
A component stays module-owned when its API would otherwise encode a domain
|
||||
entity, permission, workflow state, endpoint, or policy decision. Reuse does
|
||||
not justify moving domain semantics into Core.
|
||||
|
||||
`PageLayout` is the standard frame for headed workflow, dashboard,
|
||||
configuration, monitoring and explanatory pages. It owns the content inset,
|
||||
sticky responsive header, title and rich-description geometry, route-action
|
||||
placement, transient and custom notices, loading boundary and page help
|
||||
identity. Its modes make scroll ownership explicit: `standalone` owns a page
|
||||
viewport, `workspace` defers scrolling to a full-canvas content pane while
|
||||
retaining the standard inset, and `embedded` owns neither scroll nor inset.
|
||||
|
||||
`WorkspaceLayout` is the standard full-canvas shell. Its `navigation` variant
|
||||
owns module/resource subnavigation plus content; its `split` variant owns
|
||||
collection/detail panes. It centralizes pane sizing, internal scroll,
|
||||
responsive collapse/stacking, accessible pane labels and workspace help
|
||||
identity. `WorkspaceFrame` is the outer full-height module frame and owns
|
||||
container or application-viewport height, overflow, surface, landmark, help,
|
||||
and accessible-name behavior. `PageHeader` remains available when an
|
||||
exceptional canvas needs only the shared heading. Specialized layouts such as
|
||||
`AdminPageLayout` compose these lower-level Core contracts; they do not repeat
|
||||
markup or responsive CSS.
|
||||
|
||||
`PageActionBar` is the semantic action contract for headed pages;
|
||||
`WorkspaceActionBar` applies the identical ordering and lifecycle rules to a
|
||||
full canvas and its collection, detail, and editor panes. Reload is always the
|
||||
leading action on a refreshable projection. Help and ordinary task actions
|
||||
follow contextual controls; Create is the far-right collection action;
|
||||
destructive actions occupy a named separated group; an editor ends with
|
||||
Discard and Save, with Save at the far right. Editor state is explicit:
|
||||
`clean`, `dirty`, `invalid`, `saving`, `save-failed`, or `conflict`. Lower-level
|
||||
`ActionToolbar` remains appropriate for a section-local view switch or compact
|
||||
control group, but it must not recreate page or pane action placement.
|
||||
|
||||
Composite workspaces whose selected contribution supplies its own semantic
|
||||
heading may use `PageLayout` with its visible header delegated. This preserves
|
||||
the central inset, loading boundary, help identity, and content frame without
|
||||
adding a duplicate heading. It is not permission to recreate the page header
|
||||
locally on ordinary headed pages.
|
||||
|
||||
Module CSS may arrange domain content inside a shared layout. It must not
|
||||
override Core layout internals or copy the outer page, dialog, toolbar, form or
|
||||
state skeleton under a module-prefixed name. If an archetype cannot be
|
||||
expressed by the central API, extend the central contract or record a bounded
|
||||
exception before introducing local structure.
|
||||
|
||||
Migration is incremental and enforceable:
|
||||
|
||||
1. inventory copied structures and register existing debt;
|
||||
2. introduce the smallest domain-neutral Core contract with accessibility,
|
||||
help, localization, theme and narrow-layout tests;
|
||||
3. migrate representative Core and optional-module consumers;
|
||||
4. reject new copies while removing registered debt in bounded module batches;
|
||||
5. promote the next repeated structure only after its variants and extension
|
||||
points are understood.
|
||||
|
||||
The current page-frame and workspace migration has no legacy exceptions. New
|
||||
raw frames fail the focused layout contract instead of entering a new baseline.
|
||||
|
||||
The current structural vocabulary is:
|
||||
|
||||
- `ActionToolbar`, `ToolbarGroup`, and `ToolbarSpacer` own action alignment,
|
||||
distribution, density, grouping, panel/section surfaces, accessible toolbar
|
||||
naming, help identity, and responsive wrapping. Modules may add
|
||||
domain-specific presentation; they do not recreate the flex/wrap skeleton.
|
||||
- `PageActionBar` and `WorkspaceActionBar` own semantic ordering, Reload,
|
||||
editor persistence state, destructive separation, and page/pane scope. A
|
||||
module supplies action behavior, authority, blocker reasons, and wording;
|
||||
it does not assemble another panel-header action convention.
|
||||
- `WorkspaceFrame` and `WorkspaceLayout` own application-viewport framing,
|
||||
surfaces, overflow, list/detail and navigation/content pane geometry,
|
||||
accessible region identity, and responsive pane behavior. Modules own only
|
||||
the domain regions placed inside those contracts.
|
||||
- `FilterBar` owns submitted or live filter/search arrangement, wrapping,
|
||||
width and surface. `SelectionList`, `SelectionListItem`, and
|
||||
`SelectionListItemContent` own selectable resource navigation and its
|
||||
title/description/leading-icon geometry. `CountBadge` owns compact numeric
|
||||
emphasis. Modules retain filter behavior, selection state, and count meaning.
|
||||
- `StatePanel` owns whole-surface, compact, inline and fill state presentation
|
||||
for empty, unavailable, blocked, warning and recoverable-error compositions.
|
||||
Modules provide the cause, consequence, permitted action and authority.
|
||||
- `ContentGrid`, `FormGrid`, `FormLayout`, and `GridItem` own equal-column
|
||||
geometry, standard gaps, alignment, spans, native form semantics, and named
|
||||
responsive collapse points. A module-local grid remains appropriate only
|
||||
when unequal tracks or domain visualization semantics are material.
|
||||
- `ContentSection` owns repeated bordered or subtle content-section surfaces,
|
||||
density, stacked flow and surrounding rhythm without prescribing a domain
|
||||
heading or body schema.
|
||||
- `FormSection` owns form-section heading, description, actions, content flow,
|
||||
separation, and panel presentation. It does not own field values,
|
||||
validation, permissions, or domain wording.
|
||||
- `MetricGrid` owns the responsive grouping around `MetricCard`: fixed one-to-five
|
||||
columns or auto-fit, minimum card width, density, surrounding rhythm, and a
|
||||
named collapse point. `MetricCard.drilldown` provides an explicit link or
|
||||
in-page action when an authorized underlying detail helps the user act; it
|
||||
names that destination and preserves the current scope and filters. The card
|
||||
itself is never the hidden click target. Derived, privacy-suppressed,
|
||||
non-enumerable, and purely informational aggregates remain inert. Modules
|
||||
provide the metric, tone, destination, and consequence; they do not recreate
|
||||
the group grid or reach across module CSS to size it.
|
||||
- `DescriptionList` and `DescriptionItem` own semantic property presentation.
|
||||
The stacked variant supports compact multi-column facts; the inline variant
|
||||
supports one-column term/value rows with a standard term width. Both own
|
||||
density, wrapping, and responsive collapse while modules retain the terms,
|
||||
values, provenance, and actions.
|
||||
- `Dialog` owns size and administration variants, body padding, description,
|
||||
notices, and fixed footer placement. `DialogActions`, `DialogForm`, and
|
||||
`DialogSection` own the footer action flow, native form flow, and body
|
||||
grouping used inside it. Modules compose fields and consequences rather than
|
||||
recreating dialog anatomy.
|
||||
- `DefinitionPalette`, `DefinitionPaletteGroup`, `DefinitionPaletteItem`, and
|
||||
`DefinitionNodeIcon`, together with the shared definition-canvas classes,
|
||||
own reusable graph-editor palette, canvas-control, node-icon, port and empty
|
||||
overlay visuals. Workflow/Dataflow retain node types, shapes, edges,
|
||||
validation and execution semantics. `FloatingStatus` owns the common
|
||||
non-shifting activity overlay.
|
||||
|
||||
Raw toolbar tags, the former generic grid and property-list classes, retired
|
||||
module-local shells/states/metrics/badges, raw dialog-form wrappers, and
|
||||
module-local definitions of these contracts are rejected by the focused
|
||||
workspace checks. Dialog widths matching the Core size scale must use `Dialog
|
||||
size`; other local widths require a reviewed exception and may only decrease.
|
||||
Remaining local layout is acceptable only for unequal-track domain editors,
|
||||
visualizations, trees, timelines, data tables, or domain-specific multi-pane
|
||||
interaction. Generic resemblance alone is not a reason to create one oversized
|
||||
page template, while exact repeated structural anatomy must be promoted.
|
||||
|
||||
## Surface Archetypes
|
||||
|
||||
Choose an archetype from the task, then specialize it for the domain. A route
|
||||
@@ -95,15 +235,23 @@ one.
|
||||
|
||||
- Structured directories use the full available content space and persistent
|
||||
panes. They do not add a decorative heading row that reduces working height.
|
||||
Give navigation and list panes bounded widths and let the main content or
|
||||
detail pane consume the remaining space.
|
||||
- In a list-detail workspace, related lists may be stacked in the left pane
|
||||
while the main pane owns view, create, and edit. Keep one create action in
|
||||
the relevant list heading instead of adding a second launcher or permanent
|
||||
creation panel.
|
||||
- Workflow, configuration, dashboard, and explanatory pages may use a heading.
|
||||
The heading names the task or scoped object and contains only route-level
|
||||
actions.
|
||||
actions. Use the Core `PageLayout` contract for the frame and `PageHeader`
|
||||
only when a full-canvas archetype owns its own scrolling.
|
||||
- Put a collection-wide create action in the heading of the collection it
|
||||
affects. Use a short, specific label such as `Add` when the heading already
|
||||
names the object. Do not duplicate that action in a permanently visible side
|
||||
panel. A side panel used as the creation surface appears for creation and is
|
||||
otherwise absent or returns to its documented non-creation purpose.
|
||||
- Put filters beside the list or pane they affect. Put bulk actions immediately
|
||||
- Put filters beside the list or pane they affect. Put collection, detail, and
|
||||
editor-pane actions in `WorkspaceActionBar` with the matching scope. Put bulk actions immediately
|
||||
above or beside the current selection. Put object actions with the object
|
||||
detail, not in the global title bar.
|
||||
- Full-page create and edit surfaces put their persistent action cluster in the
|
||||
@@ -0,0 +1,211 @@
|
||||
# Product Experience and Module Boundaries
|
||||
|
||||
## Problem
|
||||
|
||||
GovOPlaN's runtime modularity is a strength, but the implementation structure
|
||||
is exposed too directly in the product. Ordinary users encounter module names,
|
||||
one top-level route per module, one navigation item per repository, package and
|
||||
provider identifiers, and errors framed as missing modules. This makes the
|
||||
system look like a toolbox of adjacent applications instead of one operating
|
||||
environment for institutional work.
|
||||
|
||||
The correction is not a monolithic frontend and not hidden provenance. It is a
|
||||
separate product information architecture assembled from typed module
|
||||
contributions.
|
||||
|
||||
Implementation is tracked in
|
||||
[Core #283](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/283).
|
||||
The accepted configurable product-area and task-local tool design is defined
|
||||
in [Quick Access And Product Areas](QUICK_ACCESS_AND_PRODUCT_AREAS.md).
|
||||
|
||||
## Current Exposure Inventory
|
||||
|
||||
| Surface | Direct exposure | Appropriate audience | Product-facing alternative |
|
||||
| --- | --- | --- | --- |
|
||||
| Side rail | One icon and route for many installed modules | Administrators and power users | Work areas, services, inboxes, records, communication, data and assurance |
|
||||
| Route paths | Technical owners such as `/dataflow`, `/forms`, or `/postbox` | Deep links and diagnostics | Stable product aliases and journey routes that resolve to owner surfaces |
|
||||
| Dashboard | Installed module count and module-owned widget library | Operators | Outcome, obligation, work, exception, and service widgets |
|
||||
| Administration | Package names, database state, capabilities, providers | Module and system administrators | Guided product/package configuration with technical details on demand |
|
||||
| Errors | "Module/capability not installed" | Diagnostics | Explain the unavailable outcome, responsible administrator, and enabling path |
|
||||
| Documentation | Topics grouped primarily by module | Administrators | Task, role, service, and object documentation with module provenance secondary |
|
||||
| Permissions | Module-namespaced scopes | Access administrators | Human-readable responsibility bundles; exact scopes remain inspectable |
|
||||
| Search | Provider/module as a result facet | Advanced filtering | Object type, institution, time, purpose, case/service, and source authority |
|
||||
| Workflow | Steps can expose target route/module details | Workflow designers | User-facing action and expected result; technical binding in definition details |
|
||||
| Connector state | Provider IDs and source types | Integration owners | Named source, authority, freshness, health, last effect, and recovery state |
|
||||
|
||||
## Boundary Decision
|
||||
|
||||
Three layers remain distinct:
|
||||
|
||||
1. **Technical module layer:** package ownership, dependencies, capabilities,
|
||||
permissions, migrations, routes, and provider identifiers.
|
||||
2. **Product composition layer:** work areas, object types, journeys, commands,
|
||||
inboxes, configuration packages, and role-based defaults.
|
||||
3. **Presentation projection:** active view, tenant policy, current task,
|
||||
temporal context, language, accessibility preferences, and device layout.
|
||||
|
||||
Modules own implementation and contribute typed product metadata. Core
|
||||
assembles it. Views filters it. Policy constrains it. Access authorizes the
|
||||
underlying actions. No consumer imports another optional module's UI directly.
|
||||
|
||||
## Product Surface Contract
|
||||
|
||||
Each WebUI module should be able to announce:
|
||||
|
||||
- `product_areas`: stable areas to which a route, command, widget, or object
|
||||
belongs;
|
||||
- `object_types`: user-facing nouns, icons, search context, detail route, and
|
||||
owner provenance;
|
||||
- `work_item_sources`: open work, exceptions, deadlines, and responsible
|
||||
capacity;
|
||||
- `journey_actions`: launch, resume, review, correct, decide, publish, and
|
||||
reconcile commands;
|
||||
- `workspace_surfaces`: embeddable but owner-rendered list, detail, editor, and
|
||||
status surfaces;
|
||||
- `configuration_contributions`: guided settings with consequence and
|
||||
prerequisite metadata;
|
||||
- `help_contexts`: user/admin documentation for the product identity as well as
|
||||
the technical owner;
|
||||
- `technical_provenance`: module, interface version, capability, and provider
|
||||
identifiers shown only in details and evidence.
|
||||
|
||||
The contract references surfaces. It does not permit Core or a product package
|
||||
to import their implementation.
|
||||
|
||||
The versioned `product_surfaces` slice is implemented in Core. It
|
||||
binds a stable product identity and entry path to one or more owner routes,
|
||||
View surfaces, presentations, capabilities, search sources, help contexts and
|
||||
documentation topics. It also carries standard unavailable/degraded
|
||||
explanations and migration aliases. Mail and Postbox contribute the first
|
||||
shared identity, `communication.messages`: `/messages` and the migration alias
|
||||
`/inbox` select the first currently authorized, View-visible owner while the
|
||||
underlying `/mail` and `/postbox` deep links, custody and permissions remain
|
||||
unchanged. Tasks, Calendar and Files contribute the corresponding single-owner
|
||||
identities:
|
||||
|
||||
| Product identity | Stable destination | Compatible owner route |
|
||||
| --- | --- | --- |
|
||||
| Work | `/work` | `/tasks` |
|
||||
| Calendar | `/agenda` | `/calendar` |
|
||||
| Messages | `/messages` (`/inbox` alias) | `/mail`, `/postbox` |
|
||||
| Files | `/documents` | `/files` |
|
||||
|
||||
Core replaces those owner entries in the ordinary rail with the stable product
|
||||
destinations. A collapsed **All available tools** catalogue retains every
|
||||
authorized technical owner route independently of View focus; unauthorized
|
||||
entries are never disclosed. The original deep links remain valid, and all
|
||||
contributing owner paths keep the corresponding product entry active. Alias
|
||||
resolution emits a bounded client telemetry event before the redirect.
|
||||
|
||||
Core's `ProductAvailabilityState` is the shared presentation primitive for
|
||||
authorization, Policy, configuration, disabled, missing-capability, offline and
|
||||
provider-degraded states. Product language is primary; exact module,
|
||||
capability, provider and correlation provenance is available only in an
|
||||
expandable technical section.
|
||||
|
||||
## Navigation Model
|
||||
|
||||
The default shell should prioritize:
|
||||
|
||||
1. global search and create/resume commands;
|
||||
2. personal and function-bound work;
|
||||
3. configured product areas;
|
||||
4. pinned user destinations;
|
||||
5. administration and technical module inspection when authorized.
|
||||
|
||||
The baseline product areas are Work, Services and Cases, Records and
|
||||
Documents, Communication, Meetings and Decisions, Data and Assurance, and
|
||||
People and Responsibility. They are configurable system/tenant defaults and
|
||||
Views projections, not hard-coded repository groups. Empty areas disappear;
|
||||
single-destination areas may link directly; familiar tools may remain pinned.
|
||||
|
||||
The complete permission-derived module rail is available as the collapsed
|
||||
**All available tools** escape. It is deliberately independent of the active
|
||||
View while still enforcing authorization. Its ability to scroll is useful and
|
||||
is not itself the product defect. The defect is requiring people to infer a
|
||||
task or outcome from repository topology.
|
||||
|
||||
Task-local Work, Calendar, Messages and Files tools may be contributed to the
|
||||
optional `govoplan-quick-access` rail. Messages composes Mail, Postbox and
|
||||
future governed chat presentation without merging their channel semantics or
|
||||
state.
|
||||
|
||||
A module route remains a valid deep link. A product area may combine links and
|
||||
owner-rendered surfaces from several modules. When a required contribution is
|
||||
absent, the area explains the missing outcome rather than rendering a broken
|
||||
placeholder.
|
||||
|
||||
Views remain the projection mechanism. They may select product areas, routes,
|
||||
sections, commands, widgets, and fields. A view must not grant a permission or
|
||||
change data semantics. Policy can force, allow, or prohibit a surface at system,
|
||||
tenant, group, or user scope.
|
||||
|
||||
Core browser conformance exercises the German Anwohnerparkausweis reference
|
||||
context with Work, Calendar, Messages and Files entries, verifies that package
|
||||
owner labels are absent from the primary rail, expands the technical catalogue,
|
||||
and runs WCAG 2 A/AA checks over the result. Unit permutations cover two-owner,
|
||||
one-owner, unauthorized-owner and focused-View compositions.
|
||||
|
||||
## Error And Provenance Language
|
||||
|
||||
Normal errors answer:
|
||||
|
||||
- what the person was trying to achieve;
|
||||
- why it is unavailable or failed;
|
||||
- whether data was saved or an external effect may have occurred;
|
||||
- who can resolve it and where;
|
||||
- the correlation/evidence reference.
|
||||
|
||||
An expandable technical section may then identify the module, capability,
|
||||
provider, request, and version. This keeps the product intelligible without
|
||||
hiding operational truth.
|
||||
|
||||
## Migration
|
||||
|
||||
Core's product-area and Quick Access contracts, the optional Quick Access
|
||||
module, the first five providers and immutable View presentation revisions are
|
||||
implemented. The migration below now concerns broader classification and
|
||||
product-language adoption; it is not a prerequisite for safely enabling the
|
||||
first rail slice.
|
||||
|
||||
### Slice 1: inventory and aliases
|
||||
|
||||
- continue classifying every route, navigation item, widget, setting, search object, and
|
||||
help context by product area and object type;
|
||||
- extend the implemented product-surface aliases without removing existing deep links;
|
||||
- flag raw module IDs in ordinary-user labels and errors.
|
||||
|
||||
### Slice 2: work-first shell
|
||||
|
||||
- provide a generic work/exception/deadline aggregation capability;
|
||||
- make work areas and configured packages the default navigation;
|
||||
- move the complete module catalogue to administration and an optional power-
|
||||
user surface.
|
||||
- implement the configurable Quick Access rail through Core-mediated
|
||||
contributions, system/tenant/user resolution and View/Policy ceilings.
|
||||
|
||||
### Slice 3: composite journeys
|
||||
|
||||
- let product packages define journey launch/resume actions and default views;
|
||||
- let Workflow Engine activate a view and focus an owner surface without
|
||||
controlling authorization;
|
||||
- expose provider provenance and technical bindings on demand.
|
||||
|
||||
### Slice 4: enforceability
|
||||
|
||||
- make product classification mandatory for user-visible manifest surfaces;
|
||||
- reject duplicate product identities and missing owner routes in CI;
|
||||
- add browser tests proving that reference users can complete a journey without
|
||||
knowing module names.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- An ordinary user can describe every primary navigation item as work or an
|
||||
institutional object, not as a package.
|
||||
- A product package can remove irrelevant navigation while retaining deep-link
|
||||
and help integrity.
|
||||
- Missing optional modules produce an actionable product explanation.
|
||||
- Administrators can still inspect exact module, capability, provider, schema,
|
||||
and evidence provenance.
|
||||
- Module permutation tests prove that no product surface assumes an optional
|
||||
owner is installed.
|
||||
@@ -0,0 +1,241 @@
|
||||
# Quick Access And Product Areas
|
||||
|
||||
## Purpose
|
||||
|
||||
GovOPlaN presents institutional work without requiring ordinary users to
|
||||
understand the installed package graph. Two complementary projections provide
|
||||
that experience:
|
||||
|
||||
- **product areas** group destinations, objects, work and actions by the
|
||||
outcome a person recognizes;
|
||||
- **Quick Access** keeps a small set of task-local tools available without
|
||||
leaving the current page, case, record or Workflow context.
|
||||
|
||||
Technical modules remain the implementation, release and provenance boundary.
|
||||
Product areas and Quick Access are presentation contracts over those owners;
|
||||
they do not copy domain state or bypass authorization.
|
||||
|
||||
Implementation is tracked by Core #283 and #285, GovOPlaN's product-experience
|
||||
umbrella, Views, Policy and `govoplan-quick-access`.
|
||||
|
||||
The repository and product name is `govoplan-quick-access`, with module id
|
||||
`quick_access`. `govoplan-qar` was rejected because the abbreviation hides the
|
||||
purpose in package catalogues, diagnostics, permissions and operations.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
The first production-shaped slice is implemented:
|
||||
|
||||
- Core validates and publishes versioned `product_areas` and
|
||||
`quick_access_tools` manifest contracts;
|
||||
- `govoplan-quick-access` derives its live catalogue from installed modules,
|
||||
persists optimistic-concurrency-protected system, tenant and user profiles,
|
||||
and resolves blocked, forced, ordered and stale preferences;
|
||||
- the shell hosts the optional right rail and one composed drawer with keyboard
|
||||
dismissal, focus return, responsive mobile behavior and full-page fallbacks;
|
||||
- Tasks, Calendar, Mail, Postbox and Files contribute the first owner-rendered
|
||||
tools; Mail and Postbox remain separate sections inside Messages;
|
||||
- immutable View revisions now carry grouped/flat navigation, product-area
|
||||
order and optional labels. Scoped Views therefore configure product
|
||||
presentation for system, tenant, group, user and Workflow contexts;
|
||||
- the expanded left rail groups classified destinations while retaining
|
||||
Dashboard and every authorized unclassified destination under More tools;
|
||||
- Core promotes Work (`/work`), Calendar (`/agenda`), Messages (`/messages`)
|
||||
and Files (`/documents`) into stable primary destinations and collapses the
|
||||
compatible owner routes under **All available tools**;
|
||||
- **All available tools** is permission-derived but independent of the active
|
||||
View, providing a deliberate escape without granting access or discarding
|
||||
the original `/tasks`, `/calendar`, `/mail`, `/postbox` and `/files` links.
|
||||
|
||||
The baseline classification and the four initial stable destinations are now
|
||||
manifest-declared. The area classification covers every ordinary user-facing
|
||||
module and is enforced by the workspace manifest check. A separately
|
||||
versioned launch-context contract carries bounded active-object, acting,
|
||||
temporal, View and return references into full-page Quick Access fallbacks;
|
||||
Cases publishes the first active-object reference. The remaining rollout is to
|
||||
add useful bounded tools and active-object publishers only where a maintained
|
||||
journey benefits. The pinned German Anwohnerparkausweis browser composition
|
||||
verifies stable product labels, technical escape, keyboard access and WCAG
|
||||
conformance. Authorized global and technical routes remain visible through
|
||||
their dedicated shell entry or **All available tools**.
|
||||
|
||||
## Quick Access Boundary
|
||||
|
||||
Core owns a versioned contribution contract. Feature modules may register a
|
||||
tool when they have a useful bounded surface. They do not import Quick Access.
|
||||
`govoplan-quick-access` owns configuration, effective resolution, ordering,
|
||||
the right-side rail and its drawer. Views may narrow tools for the current
|
||||
task. Policy may constrain availability and customization. Access and each
|
||||
owner's backend remain authoritative.
|
||||
|
||||
The initial categories are:
|
||||
|
||||
| Category | Typical contributions |
|
||||
| --- | --- |
|
||||
| Work | Explicit Tasks, Workflow handoffs, approvals, deadlines and exceptions |
|
||||
| Calendar | Today/upcoming agenda, event creation and scheduling launch |
|
||||
| Messages | Mail, function-bound Postbox messages and future governed chat providers |
|
||||
| Files | Contextual/recent files, attachment selection and upload |
|
||||
|
||||
Messages is one shell category but not one data model. Mail, Postbox and future
|
||||
chat providers retain their channel semantics, custody, policy, audit and
|
||||
delivery behavior. The drawer identifies the channel where that distinction
|
||||
matters.
|
||||
|
||||
## Contribution Contract
|
||||
|
||||
A Quick Access contribution declares:
|
||||
|
||||
- contract version 1, a stable id, category and human label;
|
||||
- icon, order and optional badge/summary provider;
|
||||
- required permissions and optional dependencies;
|
||||
- global or active-object availability, accepted context-reference kinds and
|
||||
produced result-reference kinds;
|
||||
- an owner-rendered bounded WebUI surface and full-page fallback route;
|
||||
- View surface, help context and availability explanation;
|
||||
- whether the contribution supports preview, create, select or resume.
|
||||
|
||||
The shell passes only bounded references: tenant, acting context, temporal
|
||||
read context, active task/Workflow, current institutional object, selected
|
||||
resources and a safe return location. The owner reauthorizes every read and
|
||||
effect. Credentials, protected content and permission decisions are never
|
||||
embedded in launch context.
|
||||
|
||||
Launch-context version 2 identifies reference contract version 1 and carries
|
||||
the exact resolved View revision plus optional recommended and focused tool
|
||||
ids. Recommendations affect order and emphasis only. Focus narrows the rail
|
||||
only when at least one focused contribution survives module enablement,
|
||||
configuration, context compatibility and authorization; otherwise the normal
|
||||
effective rail remains available. Workflow gets the same behavior by resolving
|
||||
the exact View revision instead of acquiring separate presentation authority.
|
||||
|
||||
An owner-rendered tool explicitly returns result contract version 1 as either
|
||||
`completed` with an action and typed owner reference, or `cancelled` with a
|
||||
reason. The shell correlates the result with the source and tool, rejects
|
||||
cross-tenant or undeclared reference kinds, and does not interpret closing the
|
||||
drawer as completion. Owner modules validate, persist, recover and audit their
|
||||
own effects. The overlay leaves the host route mounted, so unsaved host-page
|
||||
state is preserved; the full-page route remains the bounded-work fallback.
|
||||
|
||||
## Effective Configuration
|
||||
|
||||
The effective rail is resolved from:
|
||||
|
||||
1. installed and enabled modules and their registered contributions;
|
||||
2. system availability, forced entries and ordering defaults;
|
||||
3. tenant availability, forced entries and ordering defaults;
|
||||
4. group and user View/Policy ceilings where configured;
|
||||
5. the user's enabled categories, entries and ordering;
|
||||
6. the active View and optional Workflow-step narrowing overlay;
|
||||
7. current authorization and contribution availability.
|
||||
|
||||
Lower scopes may narrow or reorder allowed entries but cannot enable a tool
|
||||
blocked above them. A forced entry cannot be removed below its source. User
|
||||
configuration stores stable contribution ids; unavailable or retired ids are
|
||||
retained as explained stale preferences without rendering broken controls.
|
||||
|
||||
Configuration screens derive their available choices from the live registry.
|
||||
Installing or enabling a contributing module adds its permitted choices;
|
||||
disabling it removes the runtime tool while preserving harmless preferences.
|
||||
If Quick Access is absent, contributors behave exactly as before.
|
||||
|
||||
## Interaction Model
|
||||
|
||||
Desktop uses a narrow right-side rail with at most four initial category
|
||||
buttons and an overflow when an administrator or user adds more categories.
|
||||
Selecting a category opens one fixed, owner-neutral drawer. Contributions are
|
||||
shown inside that drawer as tabs, sections or commands according to the
|
||||
category contract. The default drawer overlays content so DataGrid and fixed
|
||||
workspace layouts do not resize unexpectedly; a later explicit pinned mode may
|
||||
reserve layout width on sufficiently wide screens.
|
||||
|
||||
The drawer preserves host-page state, has a deterministic focus return, closes
|
||||
with Escape, supports keyboard traversal, and provides explicit completion,
|
||||
cancellation and full-page actions. Mobile and narrow layouts use the same
|
||||
category/configuration semantics in a bottom sheet or compact menu.
|
||||
|
||||
## Product Areas
|
||||
|
||||
Product areas are stable configurable identities, not repositories. The
|
||||
recommended baseline is:
|
||||
|
||||
- Work;
|
||||
- Services and Cases;
|
||||
- Records and Documents;
|
||||
- Communication;
|
||||
- Meetings and Decisions;
|
||||
- Data and Assurance;
|
||||
- People and Responsibility.
|
||||
|
||||
Modules contribute routes, objects, actions, widgets, work sources and help to
|
||||
one or more areas. Product packages and administrators may define sensible
|
||||
system and tenant defaults. Views select, order, rename or narrow allowed
|
||||
areas, and users may personalize them within Policy ceilings. An empty area is
|
||||
omitted. An area with one destination may open it directly. A multi-destination
|
||||
area provides a useful work/recent/action surface rather than another menu.
|
||||
|
||||
Familiar product nouns such as Calendar or Files remain direct product
|
||||
destinations. The objective is not to hide every implementation name from
|
||||
administrators; it is to prevent repository topology from determining a
|
||||
person's workflow.
|
||||
|
||||
The initial module classification is deliberately outcome-oriented:
|
||||
|
||||
| Product area | Contributing user-facing modules |
|
||||
| --- | --- |
|
||||
| Work | Approvals, Projects, Tasks, Workflow |
|
||||
| Services and Cases | Cases, Forms, Forms Runtime, Portal |
|
||||
| Records and Documents | Files, Records, Templates |
|
||||
| Communication | Campaigns, Distribution Lists, Mail, Notifications, Postbox |
|
||||
| Meetings and Decisions | Calendar, Committee, Scheduling, Voting |
|
||||
| Data and Assurance | Dataflow, Datasources, Reporting, Risk Compliance |
|
||||
| People and Responsibility | Address Book, IDM, Organizations |
|
||||
|
||||
Dashboard, Search, Documentation and Quick Access remain global shell
|
||||
affordances. Access, Administration, Audit, Encryption, Identity Trust,
|
||||
Operations, Policy, Tenancy and Views remain administrative or platform
|
||||
surfaces available through their dedicated entry point or **All available
|
||||
tools**. The manifest-shape check enforces both this explicit exception set and
|
||||
the shared label, icon, description and ordering of every canonical area.
|
||||
|
||||
## Full Access And Provenance
|
||||
|
||||
The existing permission-derived module rail remains available as **All
|
||||
available tools** for power users and deliberate escape from a focused View.
|
||||
It contains only currently authorized destinations. Technical module,
|
||||
capability, provider and package provenance remains visible in administration,
|
||||
diagnostics, evidence and expandable details.
|
||||
|
||||
Search, deep links and help distinguish three states:
|
||||
|
||||
- available in the active View;
|
||||
- authorized but outside the active View, with a temporary escape or View
|
||||
switch;
|
||||
- unavailable because of authorization, Policy, configuration or a missing
|
||||
capability, with an actionable explanation.
|
||||
|
||||
## Delivery Order
|
||||
|
||||
1. Define Core product-area and Quick Access contracts and validation.
|
||||
2. Implement `govoplan-quick-access` configuration, effective resolution and
|
||||
shell capability.
|
||||
3. Contribute Work, Calendar, Messages and Files bounded surfaces.
|
||||
4. Add configurable product-area defaults through Views and product packages.
|
||||
5. Migrate navigation, breadcrumbs, search, errors, documentation, dashboard
|
||||
and administration toward product terminology.
|
||||
6. Prove keyboard, focus, responsive, optional-module and reference-journey
|
||||
behavior before making it the ordinary-user default.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- A user can configure allowed Quick Access categories and ordering without
|
||||
gaining authority.
|
||||
- System and tenant administrators can make entries available, forced or
|
||||
unavailable with provenance.
|
||||
- Mail, Postbox and another future channel can share Messages presentation
|
||||
while retaining independent state and channel semantics.
|
||||
- A reference journey can use a bounded tool and return without losing host
|
||||
state or Workflow context.
|
||||
- Product areas remain useful under sparse and rich permission sets and under
|
||||
optional-module permutations.
|
||||
- All available tools and technical provenance remain deliberately reachable.
|
||||
@@ -7,6 +7,11 @@ responsibility, or workflow step. A View can reduce the visible modules,
|
||||
navigation entries, routes, page sections, and commands to the interface
|
||||
needed for the current job.
|
||||
|
||||
Views also project configurable product areas and Quick Access contributions.
|
||||
They may select, order, rename or hide permitted presentation identities but
|
||||
do not move ownership or merge Mail, Postbox, Files, Calendar, Tasks or other
|
||||
domain state.
|
||||
|
||||
Views are optional. If `govoplan-views` is not installed or enabled, the normal
|
||||
permission-derived interface remains unchanged.
|
||||
|
||||
@@ -119,6 +124,12 @@ Implemented in the initial Views slice:
|
||||
prevention
|
||||
- surface declarations for every currently installed module that contributes a
|
||||
WebUI, including finer-grained shared administration and settings surfaces
|
||||
- immutable presentation settings for grouped or flat navigation, product-area
|
||||
order and product-area labels; the shell resolves these settings through the
|
||||
same system, tenant, group, user and Workflow-aware View projection
|
||||
- live product-area identities from module manifests, with authorized
|
||||
unclassified destinations retained under More tools during incremental
|
||||
adoption
|
||||
|
||||
Still intentionally separate:
|
||||
|
||||
@@ -127,6 +138,11 @@ Still intentionally separate:
|
||||
- read-only and layout-replacement projections beyond the version `1`
|
||||
visible/hidden contract
|
||||
|
||||
Quick Access ordering and availability remain owned by
|
||||
`govoplan-quick-access`; Views only narrow its declared surfaces for the active
|
||||
task. Neither contract permits arbitrary layout or styling. See
|
||||
`docs/architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md` in the meta repository.
|
||||
|
||||
## Gitea Work Packages
|
||||
|
||||
- `govoplan#17`: task-focused Views user story
|
||||
+5
-1
@@ -1,5 +1,9 @@
|
||||
# Meta Repository Migration Audit
|
||||
|
||||
> **Archived migration record:** The ownership migration described here is
|
||||
> complete. Current boundaries are defined by Repository Structure, module
|
||||
> manifests, and the owning repositories.
|
||||
|
||||
This audit records which existing GovOPlaN files should move toward the
|
||||
`govoplan` meta repository and which should remain with their current runtime
|
||||
owner.
|
||||
@@ -148,7 +152,7 @@ It should not own:
|
||||
Known references reviewed after the server-side rename:
|
||||
|
||||
- `govoplan/repositories.json`
|
||||
- `govoplan/docs/REPOSITORY_STRUCTURE.md`
|
||||
- `govoplan/docs/project/REPOSITORY_STRUCTURE.md`
|
||||
- `govoplan/docker/README.md`
|
||||
- `govoplan-core/docs/RELEASE_DEPENDENCIES.md`
|
||||
- `govoplan-core/docs/MODULE_ARCHITECTURE.md`
|
||||
@@ -1,5 +1,8 @@
|
||||
# Meta Repository Scan
|
||||
|
||||
> **Archived assessment:** This file records the 2026-07-13 repository state.
|
||||
> Use `repositories.json` and the current documentation map for present state.
|
||||
|
||||
Scan date: 2026-07-13.
|
||||
|
||||
This scan checked local repositories under `/mnt/DATA/git` listed in
|
||||
@@ -13,7 +16,7 @@ Checked-out repositories not listed in `repositories.json`: none.
|
||||
|
||||
Repositories listed in `repositories.json` but not checked out locally: none.
|
||||
|
||||
The human-readable link index is `docs/REPOSITORY_INDEX.md`; the JSON file
|
||||
The human-readable link index is `docs/project/REPOSITORY_INDEX.md`; the JSON file
|
||||
remains the machine-readable source of truth.
|
||||
|
||||
## Meta-Owned Content
|
||||
@@ -0,0 +1,135 @@
|
||||
# Strategic Review - 2026-08-05
|
||||
|
||||
> **Archived assessment:** This review explains the 2026-08-05 strategy reset.
|
||||
> It is not updated with later implementation or portfolio state.
|
||||
|
||||
## Assessment
|
||||
|
||||
GovOPlaN has not lost its central direction. The architecture now expresses a
|
||||
coherent institutional governance platform, but architecture and repository
|
||||
breadth have advanced faster than complete, usable outcomes. The immediate
|
||||
need is convergence: fewer simultaneous fronts, stronger cross-cutting
|
||||
adoption, and end-to-end reference journeys that non-developers can complete.
|
||||
|
||||
This is a dated review. Current status belongs in
|
||||
[Strategy Status](../../strategy/STRATEGY_STATUS.md); stable direction belongs in
|
||||
[Platform Core Ideas](../../strategy/PLATFORM_CORE_IDEAS.md).
|
||||
|
||||
## What Is Already Strong
|
||||
|
||||
- A modular runtime with manifests, capabilities, interfaces, migrations,
|
||||
optional integrations, signed releases, and permutation checks.
|
||||
- Explicit institutional semantics for identity, representation,
|
||||
organization, function, mandate, service, case, party, approval, decision,
|
||||
evidence, and record references.
|
||||
- Governed communication foundations spanning Campaign, Mail, Files, Postbox,
|
||||
Addresses, Distribution Lists, Templates, Audit, and Policy.
|
||||
- Governed data foundations spanning Connectors, Datasources, Dataflow,
|
||||
Reporting, Search, and immutable provenance.
|
||||
- Bitemporal browsing, views, contextual documentation, action/effect
|
||||
contracts, event delivery, recovery ledgers, and stateless deployment
|
||||
contracts.
|
||||
- A credible deployment and release foundation with signed artifacts and
|
||||
reproducible composition evidence.
|
||||
|
||||
## Where The Program Veered
|
||||
|
||||
### Repository breadth preceded product proof
|
||||
|
||||
Logical modularity often became a repository before a reference journey proved
|
||||
that an independent release boundary was required. Scaffolds are useful as
|
||||
ownership markers, but their number makes the product appear broader and more
|
||||
complete than its supported outcomes.
|
||||
|
||||
### Foundations outran reference gates
|
||||
|
||||
Later-stage contracts such as federation, encryption, formal governance,
|
||||
deployment evidence, and broad module metadata were developed while basic
|
||||
human-work and records journeys remained incomplete. Those foundations are not
|
||||
wasted; they now need to be consumed by a small number of demonstrable
|
||||
products.
|
||||
|
||||
### The module graph leaked into the experience
|
||||
|
||||
Navigation, routes, administration, errors, documentation, and configuration
|
||||
often present module names and package structure directly. This is appropriate
|
||||
for operators, but ordinary users should see work, services, records, and
|
||||
outcomes.
|
||||
|
||||
This is not primarily a rail-length or scrolling problem. Sparse permissions
|
||||
already reduce navigation and the complete technical rail remains useful for
|
||||
power users. The correction is configurable product areas, task-focused Views
|
||||
and a bounded Quick Access rail, while preserving deliberate access to every
|
||||
authorized tool and technical provenance. The accepted design is maintained in
|
||||
[Quick Access And Product Areas](../../architecture/QUICK_ACCESS_AND_PRODUCT_AREAS.md).
|
||||
|
||||
### Status became duplicated
|
||||
|
||||
Roadmaps, target architecture, fit assessments, issue comments, and release
|
||||
documents each contained partial implementation snapshots. Their stable
|
||||
decisions remain valuable, but volatile counts and maturity claims diverged.
|
||||
|
||||
### Too much work remained active simultaneously
|
||||
|
||||
The issue portfolio had many high-priority and in-progress items without
|
||||
milestones. This reduces the signal of both labels and roadmap order and makes
|
||||
completion harder to demonstrate.
|
||||
|
||||
## Where GovOPlaN Has Not Gone Far Enough
|
||||
|
||||
1. No composition has yet crossed the full `reference_ready` gate.
|
||||
2. The human-work spine is incomplete: work queues, tasks, handoffs, deadlines,
|
||||
reminders, escalation, and resumption need a coherent user experience.
|
||||
3. Records and document management remain too shallow for a public-sector
|
||||
operating platform.
|
||||
4. Real target integrations and GovOPlaN-to-GovOPlaN federation are not yet
|
||||
proven.
|
||||
5. Temporal browsing, purpose-aware access, retention, and institutional
|
||||
context exist as contracts but are not adopted uniformly by domain reads
|
||||
and effects.
|
||||
6. German completeness, contextual help, accessibility, responsive behavior,
|
||||
and browser-level journey testing are not yet release gates everywhere.
|
||||
7. Multi-host, backup/restore, provider interoperability, and independent
|
||||
signed target evidence still require real environments and operators.
|
||||
|
||||
## Important Omissions
|
||||
|
||||
- a named first institution, bounded users, volumes, and operating constraints;
|
||||
- measurable usability outcomes, not only functional tests;
|
||||
- installable sector packages and migration/exit demonstrations;
|
||||
- support, upgrade, deprecation, and LTS promises;
|
||||
- complete assisted, paper, telephone, and in-person channel handling;
|
||||
- a native eAkte/records model that can also overlay an external DMS or archive.
|
||||
|
||||
## Opportunities Beyond The Original Idea
|
||||
|
||||
- an institutional digital twin that exposes responsibilities, dependencies,
|
||||
obligations, services, work, data, controls, and change impact over time;
|
||||
- continuous assurance that evaluates controls and evidence as work happens;
|
||||
- process mining and conformance analysis over governed event histories;
|
||||
- federated product packages and inter-institution case/evidence exchange;
|
||||
- accountable assistance that drafts and explains without obscuring authority;
|
||||
- public evidence chains that disclose decisions and provenance without
|
||||
exposing protected source data.
|
||||
|
||||
## Recommended Reset
|
||||
|
||||
1. Freeze new repositories unless a real journey proves an independent owner,
|
||||
release lifecycle, security boundary, or optional installation need.
|
||||
2. Use one generated maturity/status dashboard and one current status document.
|
||||
3. Complete governed communication and function-bound Postbox against a real
|
||||
target.
|
||||
4. Complete the monthly-data journey, then sanctions screening on the same
|
||||
data foundations.
|
||||
5. Complete one browser-driven service-to-decision journey, including assisted
|
||||
intake and records.
|
||||
6. Make eAkte/records the next major product-depth program.
|
||||
7. Tie feature work to a reference journey, a security/recovery gate, or a
|
||||
measured usability defect.
|
||||
|
||||
## Success Criterion
|
||||
|
||||
The reset succeeds when a public institution can install a signed composition,
|
||||
configure a named procedure, complete it through digital and assisted channels,
|
||||
connect an external source, reconstruct the authority and evidence, recover it
|
||||
after failure, and transfer or retire it without custom code.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "./capability-fit.schema.json",
|
||||
"schema_version": "0.1.0",
|
||||
"schema_version": "0.2.0",
|
||||
"assessment_id": "campaign-reference-2026-07-22",
|
||||
"assessed_at": "2026-07-22",
|
||||
"scope": {
|
||||
@@ -13,16 +13,30 @@
|
||||
"Workflow and workflow-driven user stories"
|
||||
]
|
||||
},
|
||||
"facts": [
|
||||
"The assessment is pinned to signed stable catalog sequence 202607220843 and the exact module commits listed below.",
|
||||
"The Campaign authoring, validation, build, mock-delivery, managed-file, local-access, and local-audit paths have direct test or contract evidence.",
|
||||
"The production-like development profile runs PostgreSQL and Redis in containers while application processes use editable source trees.",
|
||||
"No installed-target, external-provider, reference-readiness, recovery, or production-approval evidence bundle is attached to this assessment."
|
||||
],
|
||||
"decisions": [
|
||||
"Use Campaign as the first reference journey and flagship pilot scenario.",
|
||||
"Keep Workflow and workflow-driven user stories planned and explicitly postponed for this assessment.",
|
||||
"Use local GovOPlaN accounts for the bounded pilot; do not claim federated identity support.",
|
||||
"Do not approve small production until installed-artifact, target mail, monitoring, backup/restore, and recovery proof checks pass."
|
||||
],
|
||||
"release": {
|
||||
"kind": "tagged_release",
|
||||
"ref": "stable-catalog-202607220843",
|
||||
"meta_commit": "5447299289a1",
|
||||
"reproducible": true,
|
||||
"configuration_packages": [],
|
||||
"configuration_packages": [
|
||||
"none: environment-profile basis only"
|
||||
],
|
||||
"notes": [
|
||||
"The live stable catalog has a valid Ed25519 signature trusted through release-key-1.",
|
||||
"Core v0.1.13 and Campaign v0.1.10 are tagged and package-integrated; this is not target-environment or production approval.",
|
||||
"No configuration revision or configuration package is pinned yet."
|
||||
"The absence of a configuration package is pinned explicitly as an environment-profile-only basis; this remains a promotion gap."
|
||||
]
|
||||
},
|
||||
"composition": [
|
||||
@@ -188,6 +202,125 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"scenarios": [
|
||||
{
|
||||
"id": "campaign-pilot",
|
||||
"label": "Controlled Campaign pilot",
|
||||
"status": "partial",
|
||||
"recommendation": "Proceed with a bounded internal pilot after its provider, privacy, workload, and recovery proof checks are assigned and passed.",
|
||||
"composition": [
|
||||
"core",
|
||||
"tenancy",
|
||||
"organizations",
|
||||
"identity",
|
||||
"access",
|
||||
"admin",
|
||||
"dashboard",
|
||||
"policy",
|
||||
"audit",
|
||||
"campaigns",
|
||||
"files",
|
||||
"mail",
|
||||
"docs",
|
||||
"ops"
|
||||
],
|
||||
"topology": [
|
||||
"One supervised GovOPlaN API process and one immutable built WebUI behind deployment-owned TLS termination",
|
||||
"One PostgreSQL database and a durable single-node or shared managed-file path",
|
||||
"One persistent private Redis broker and one supervised Celery worker when asynchronous delivery is enabled",
|
||||
"One dedicated non-production SMTP/IMAP account with a restricted safe-recipient policy",
|
||||
"External health checks, centralized logs, protected secret injection, and coordinated backup storage"
|
||||
],
|
||||
"conditions": [
|
||||
"Use one internal tenant or office and controlled operators.",
|
||||
"Keep recipient volume non-critical until measured.",
|
||||
"Enable Addresses only when reusable recipient lists or CardDAV are explicitly in scope.",
|
||||
"Do not enable or claim Workflow from this assessment."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "small-production-candidate",
|
||||
"label": "Small-production candidate",
|
||||
"status": "partial",
|
||||
"recommendation": "Do not approve production until every listed operational gate has target evidence and the residual risks have named owners.",
|
||||
"composition": [
|
||||
"core",
|
||||
"tenancy",
|
||||
"organizations",
|
||||
"identity",
|
||||
"access",
|
||||
"admin",
|
||||
"dashboard",
|
||||
"policy",
|
||||
"audit",
|
||||
"campaigns",
|
||||
"files",
|
||||
"mail",
|
||||
"docs",
|
||||
"ops"
|
||||
],
|
||||
"topology": [
|
||||
"Immutable separately supervised WebUI, API, and worker artifacts behind monitored reverse-proxy TLS",
|
||||
"Dedicated or managed PostgreSQL with measured coordinated backup and isolated restore",
|
||||
"Persistent authenticated Redis with queue-age, queue-depth, and worker-health alerts",
|
||||
"Durable shared or S3-compatible object storage with versioning, lifecycle, and restore evidence",
|
||||
"Target-native secret management, centralized monitoring/logging/audit export, and an exercised incident and disaster-recovery procedure"
|
||||
],
|
||||
"conditions": [
|
||||
"Pin and promote a configuration package instead of relying on an environment-only basis.",
|
||||
"Pass installed-release, target SMTP/IMAP, accessibility, privacy, security, operations, and recovery evidence gates.",
|
||||
"Agree availability, RPO, RTO, retention, support, and procurement requirements.",
|
||||
"Run only one scheduler unless distributed leadership or locking is proved."
|
||||
]
|
||||
}
|
||||
],
|
||||
"functional_context": {
|
||||
"required_modules": [
|
||||
"core",
|
||||
"tenancy",
|
||||
"organizations",
|
||||
"identity",
|
||||
"access",
|
||||
"admin",
|
||||
"dashboard",
|
||||
"policy",
|
||||
"audit",
|
||||
"campaigns",
|
||||
"files",
|
||||
"mail",
|
||||
"docs",
|
||||
"ops"
|
||||
],
|
||||
"optional_modules": [
|
||||
"addresses"
|
||||
],
|
||||
"external_systems": [
|
||||
"Deployment-owned reverse proxy and TLS certificate lifecycle",
|
||||
"Target SMTP/IMAP service and its DNS, certificate, throttling, bounce, and reply policies",
|
||||
"Target-native secret store, monitoring/logging platform, backup storage, and incident-response process"
|
||||
],
|
||||
"missing_contracts": [
|
||||
"End-to-end federated identity provider and lifecycle contract",
|
||||
"Target monitoring, alert delivery, and central audit/SIEM acceptance contract",
|
||||
"Production configuration-package promotion and approval evidence"
|
||||
],
|
||||
"policy_decisions": [
|
||||
"Recipient allow-list, permitted sender, attachment, retention, and external-disclosure policy",
|
||||
"Identity, MFA, break-glass, service-account, and joiner/mover/leaver policy",
|
||||
"Availability, RPO, RTO, support, procurement, and residual-risk ownership"
|
||||
],
|
||||
"manual_workarounds": [
|
||||
"Use controlled local accounts while federation remains outside the verified slice",
|
||||
"Use one supervised scheduler where periodic work is unavoidable",
|
||||
"Keep provider reconciliation and production promotion under explicit operator review"
|
||||
],
|
||||
"blockers": [
|
||||
"No promoted configuration package is pinned",
|
||||
"No installed-target or target SMTP/IMAP proof is attached",
|
||||
"No coherent target backup/restore or disaster-recovery drill with measured RPO/RTO is attached",
|
||||
"No target privacy, security, accessibility, operations, or production-approval evidence is attached"
|
||||
]
|
||||
},
|
||||
"questionnaire": {
|
||||
"scope_outcomes": [
|
||||
{
|
||||
@@ -203,6 +336,20 @@
|
||||
"state": "answered",
|
||||
"answer": "No; Workflow is planned and explicitly postponed.",
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "scope.users_tenants_organizations",
|
||||
"question": "Which users, roles, tenants, organization units, and delegated functions participate?",
|
||||
"state": "assumed",
|
||||
"answer": "One internal tenant or office with controlled Campaign operators; detailed organization and delegation shape remains target-specific.",
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "outcome.acceptance",
|
||||
"question": "What constitutes pilot success and production acceptance?",
|
||||
"state": "answered",
|
||||
"answer": "Pilot success requires the bounded Campaign journey and proof checks; production additionally requires installed-artifact, provider, privacy, security, operations, recovery, and approval evidence.",
|
||||
"evidence": []
|
||||
}
|
||||
],
|
||||
"data_policy": [
|
||||
@@ -219,6 +366,13 @@
|
||||
"state": "not_assessed",
|
||||
"answer": null,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "data.privacy_security_disclosure",
|
||||
"question": "Which privacy, security, residency, minimization, access, and external-disclosure constraints apply?",
|
||||
"state": "not_assessed",
|
||||
"answer": null,
|
||||
"evidence": []
|
||||
}
|
||||
],
|
||||
"identity_integrations": [
|
||||
@@ -235,22 +389,50 @@
|
||||
"state": "not_assessed",
|
||||
"answer": null,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "identity.protocols_lifecycle",
|
||||
"question": "Which identity protocols, MFA, joiner/mover/leaver, service-account, and break-glass rules are mandatory?",
|
||||
"state": "not_assessed",
|
||||
"answer": null,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "integration.protocols_network",
|
||||
"question": "Which connector protocols, versions, directions, authentication, certificate, rate-limit, egress, and degraded-mode requirements apply?",
|
||||
"state": "not_assessed",
|
||||
"answer": null,
|
||||
"evidence": []
|
||||
}
|
||||
],
|
||||
"workload_growth": [
|
||||
{
|
||||
"id": "workload.campaign",
|
||||
"id": "workload.campaign_volume_peaks",
|
||||
"question": "What are Campaign frequency, recipients per Campaign, send window, import size and attachment volume?",
|
||||
"state": "not_assessed",
|
||||
"answer": null,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "workload.platform",
|
||||
"id": "workload.tenants_users_concurrency",
|
||||
"question": "What are tenant, named-user, active-user, concurrent-user, and peak-request assumptions?",
|
||||
"state": "not_assessed",
|
||||
"answer": null,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "workload.files_jobs_audit_growth_retention",
|
||||
"question": "What are tenant, user, concurrency, file, database, queue and audit growth assumptions?",
|
||||
"state": "not_assessed",
|
||||
"answer": null,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "workload.connector_traffic_batches",
|
||||
"question": "What connector traffic, scheduled-job, batch, queue-depth, queue-age, and external-rate-limit peaks apply?",
|
||||
"state": "not_assessed",
|
||||
"answer": null,
|
||||
"evidence": []
|
||||
}
|
||||
],
|
||||
"availability_operations": [
|
||||
@@ -267,6 +449,13 @@
|
||||
"state": "not_assessed",
|
||||
"answer": null,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "hosting.network_constraints",
|
||||
"question": "Which hosting, network-zone, egress, proxy, DNS, NTP, certificate-authority, residency, or disconnected-operation constraints apply?",
|
||||
"state": "not_assessed",
|
||||
"answer": null,
|
||||
"evidence": []
|
||||
}
|
||||
],
|
||||
"procurement_decisions": [
|
||||
@@ -754,6 +943,63 @@
|
||||
"recommendation": "Use target-native secret injection and document rotation/recovery.",
|
||||
"proof_check": "Rotate a non-production credential and recover from a protected backup."
|
||||
},
|
||||
{
|
||||
"id": "identity.access",
|
||||
"requirement": "Authenticate users and enforce tenant-scoped authorization through the selected identity mode.",
|
||||
"status": "verified",
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "test",
|
||||
"scope": "committed_source",
|
||||
"locator": "govoplan-access/tests/test_auth_dependencies.py"
|
||||
},
|
||||
{
|
||||
"kind": "test",
|
||||
"scope": "committed_source",
|
||||
"locator": "govoplan-core/tests/test_api_smoke.py#cookie-session-csrf"
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
"The bounded pilot accepts local GovOPlaN accounts."
|
||||
],
|
||||
"gaps": [
|
||||
"Target MFA, federation, provisioning, and joiner/mover/leaver requirements are not assessed."
|
||||
],
|
||||
"risks": [
|
||||
"A local-only identity topology may not satisfy institutional production policy."
|
||||
],
|
||||
"recommendation": "Use controlled local pilot accounts and assess the mandatory production identity topology separately.",
|
||||
"proof_check": "Exercise login, role change, account suspension, protected bootstrap, and break-glass recovery in the target."
|
||||
},
|
||||
{
|
||||
"id": "connectors.mail",
|
||||
"requirement": "Reach the selected SMTP/IMAP and other external connector endpoints under explicit network and provider policy.",
|
||||
"status": "available_unconfigured",
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "test",
|
||||
"scope": "current_workspace",
|
||||
"locator": "govoplan-mail/tests",
|
||||
"note": "Protocol adapters have direct tests; no target provider was exercised"
|
||||
},
|
||||
{
|
||||
"kind": "documentation",
|
||||
"scope": "documented_model",
|
||||
"locator": "govoplan-campaign/docs/CAMPAIGN_DELIVERY_RUNBOOK.md"
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
"The deployment supplies DNS, egress, proxy, CA trust, scoped service accounts, and provider limits."
|
||||
],
|
||||
"gaps": [
|
||||
"No target endpoint, TLS chain, throttling, sender policy, bounce/reply path, or disclosure agreement is assessed."
|
||||
],
|
||||
"risks": [
|
||||
"Provider rejection, delay, or ambiguous outcomes can affect delivery and evidence completeness."
|
||||
],
|
||||
"recommendation": "Use a dedicated safe provider account for the pilot and require target interoperability evidence before production.",
|
||||
"proof_check": "Exercise target-like SMTP acceptance, IMAP append, throttling, outage, retry, and reconciliation through the approved network path."
|
||||
},
|
||||
{
|
||||
"id": "operations.monitoring",
|
||||
"requirement": "Detect API, database, worker, queue, storage and delivery degradation.",
|
||||
@@ -780,6 +1026,30 @@
|
||||
"recommendation": "Integrate external monitoring before small production.",
|
||||
"proof_check": "Trigger each readiness/delivery failure and verify an actionable alert."
|
||||
},
|
||||
{
|
||||
"id": "operations.audit",
|
||||
"requirement": "Retain, monitor, review, and where required export security and business audit evidence.",
|
||||
"status": "partial",
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "test",
|
||||
"scope": "current_workspace",
|
||||
"locator": "govoplan-audit/tests",
|
||||
"note": "Local audit persistence and retry behavior are exercised"
|
||||
}
|
||||
],
|
||||
"conditions": [
|
||||
"Local database audit evidence is part of coordinated backup and access review."
|
||||
],
|
||||
"gaps": [
|
||||
"Target retention enforcement, tamper-evident export, SIEM integration, alerting, and privileged review are not verified."
|
||||
],
|
||||
"risks": [
|
||||
"Local evidence alone may not meet institutional security, records, or incident-response requirements."
|
||||
],
|
||||
"recommendation": "Define the target audit retention, export, monitoring, and review controls before production approval.",
|
||||
"proof_check": "Exercise privileged-event review, retention, export failure/retry, and target SIEM or archive ingestion."
|
||||
},
|
||||
{
|
||||
"id": "operations.backup_restore",
|
||||
"requirement": "Back up and restore database, files, configuration and keys as a coherent service.",
|
||||
|
||||
@@ -9,9 +9,13 @@
|
||||
"assessment_id",
|
||||
"assessed_at",
|
||||
"scope",
|
||||
"facts",
|
||||
"decisions",
|
||||
"release",
|
||||
"composition",
|
||||
"deployment_profile",
|
||||
"scenarios",
|
||||
"functional_context",
|
||||
"questionnaire",
|
||||
"capabilities",
|
||||
"infrastructure",
|
||||
@@ -28,7 +32,7 @@
|
||||
"format": "uri-reference"
|
||||
},
|
||||
"schema_version": {
|
||||
"const": "0.1.0"
|
||||
"const": "0.2.0"
|
||||
},
|
||||
"assessment_id": {
|
||||
"$ref": "#/$defs/non_empty_string"
|
||||
@@ -54,6 +58,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"facts": { "$ref": "#/$defs/string_list" },
|
||||
"decisions": { "$ref": "#/$defs/string_list" },
|
||||
"release": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -95,6 +101,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scenarios": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"items": { "$ref": "#/$defs/scenario" }
|
||||
},
|
||||
"functional_context": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"required_modules",
|
||||
"optional_modules",
|
||||
"external_systems",
|
||||
"missing_contracts",
|
||||
"policy_decisions",
|
||||
"manual_workarounds",
|
||||
"blockers"
|
||||
],
|
||||
"properties": {
|
||||
"required_modules": { "$ref": "#/$defs/string_list" },
|
||||
"optional_modules": { "$ref": "#/$defs/string_list" },
|
||||
"external_systems": { "$ref": "#/$defs/string_list" },
|
||||
"missing_contracts": { "$ref": "#/$defs/string_list" },
|
||||
"policy_decisions": { "$ref": "#/$defs/string_list" },
|
||||
"manual_workarounds": { "$ref": "#/$defs/string_list" },
|
||||
"blockers": { "$ref": "#/$defs/string_list" }
|
||||
}
|
||||
},
|
||||
"questionnaire": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -239,6 +272,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scenario": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"label",
|
||||
"status",
|
||||
"recommendation",
|
||||
"composition",
|
||||
"topology",
|
||||
"conditions"
|
||||
],
|
||||
"properties": {
|
||||
"id": { "$ref": "#/$defs/non_empty_string" },
|
||||
"label": { "$ref": "#/$defs/non_empty_string" },
|
||||
"status": { "$ref": "#/$defs/status" },
|
||||
"recommendation": { "$ref": "#/$defs/non_empty_string" },
|
||||
"composition": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "$ref": "#/$defs/non_empty_string" }
|
||||
},
|
||||
"topology": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/non_empty_string" }
|
||||
},
|
||||
"conditions": { "$ref": "#/$defs/string_list" }
|
||||
}
|
||||
},
|
||||
"assessed_item": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -259,6 +323,7 @@
|
||||
"status": { "$ref": "#/$defs/status" },
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/evidence" }
|
||||
},
|
||||
"conditions": { "$ref": "#/$defs/string_list" },
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Shared WebUI Primitive Inventory
|
||||
|
||||
This 2026-08-18 inventory records the implementation state after the
|
||||
product-wide structural consolidation and second duplicate-rule audit. It is
|
||||
evidence for enforcement, not a substitute for the normative
|
||||
[interface pattern language](../architecture/INTERFACE_PATTERN_LANGUAGE.md).
|
||||
|
||||
## Implemented And Enforced
|
||||
|
||||
| Contract | Adoption evidence | Ownership now enforced |
|
||||
| --- | ---: | --- |
|
||||
| `ActionToolbar` and groups | 50 source files | Raw module-prefixed toolbar elements and local toolbar definitions are rejected. Distribution, wrapping, density, grouping and panel/section surfaces are Core-owned. |
|
||||
| `PageLayout` / `WorkspaceLayout` / `WorkspaceFrame` | 25 / 16 / 17 source files | Headed page anatomy, full-height viewport frames and navigation/list-detail panes no longer repeat inset, heading, notices, loading, shell height, surface, overflow or pane geometry. The raw page-frame and raw workspace exception baselines are both empty. |
|
||||
| `FilterBar` | 14 source files | Catalogue and pane search/filter rows share width, surface, layout and wrapping. |
|
||||
| `SelectionList` family | 19 source files | Resource navigation shares selection, title/description, leading-icon and truncation anatomy. |
|
||||
| `StatePanel` | 25 source files | Whole-surface, compact and fill empty/blocked/error states replace module-local state shells. |
|
||||
| `CountBadge` | 8 source files | Notification, folder, search, postbox and graph counts use one compact badge contract. |
|
||||
| `ContentSection` | 5 source files | Repeated bordered/subtle editor sections and compact provenance panels share surface, density, flow and rhythm. |
|
||||
| `ContentGrid` | 22 source files | Equal-column content geometry and former dashboard/settings/assignment copies are Core-owned. |
|
||||
| `FormGrid` and `FormLayout` | 53 source files | Former generic/admin grids and equal-column dialog/editor copies use named collapse points and native form semantics. |
|
||||
| `MetricGrid` / `MetricCard` | 31 / 33 source files | Module-local metric helpers, grids and card visual definitions were removed. |
|
||||
| `DescriptionList` and `DescriptionItem` | 28 source files | Former generic property grids use semantic `dl`/`dt`/`dd` composition with central density and collapse. |
|
||||
| `DefinitionPalette`, node/canvas visuals and `FloatingStatus` | 2 Dataflow/Workflow consumers each | The copied graph palette, canvas controls, minimap, node icon/port, empty overlay and activity overlay definitions are Core-owned; graph semantics remain local. |
|
||||
| `DialogActions`, `DialogForm`, `DialogSection` | every Core footer / 6 / 6 source files | Footer action flow, native dialog form flow and dialog content grouping are Core-owned. |
|
||||
| Standard dialog sizing | 61 reviewed specialized selectors | Any width matching the Core 460/560/680/1040/1440px scale must use `Dialog size`; the remaining decrease-only exceptions are explicit. |
|
||||
|
||||
`tools/checks/check-shared-webui-primitives.py` verifies Core exports and
|
||||
ownership, representative consumers, the absence of the retired raw anatomy,
|
||||
and composition of every `Dialog` footer through `DialogActions`.
|
||||
`tools/checks/check-shared-webui-layouts.py` additionally requires the reviewed
|
||||
Core and module consumers and rejects any raw page or workspace frame; there
|
||||
are no remaining allow-listed layout exceptions.
|
||||
|
||||
## Dialog Width Classification
|
||||
|
||||
The remaining 61 width selectors do not duplicate the Core 460/560/680/1040/
|
||||
1440px scale. They cover bounded editor widths between scale steps, high-density
|
||||
definition and governance editors, preview/chooser canvases, message and file
|
||||
overlays with coupled height behavior, and responsive full-canvas workflows.
|
||||
Their exact selector set lives in
|
||||
`tools/checks/shared-webui-dialog-width-exceptions.txt`. The focused check fails
|
||||
for a new selector, a stale baseline entry, or any local width that duplicates
|
||||
the Core scale.
|
||||
|
||||
## Audit Result And Deliberate Local Ownership
|
||||
|
||||
The second scan compared exact CSS declaration bodies and JSX anatomy across
|
||||
every WebUI module after migration. All repeated generic structural candidates
|
||||
found in that pass were promoted: viewport frames, catalogue/list shells,
|
||||
filters, selectable lists, state panels, count badges, section frames,
|
||||
equal-column grids, section headers, metrics, and definition-editor chrome.
|
||||
The final legacy-baseline pass also migrated Access administration, Core
|
||||
Settings, Docs, Mail bounce processing, and Organizations to the shared page
|
||||
and workspace layouts and removed their copied responsive geometry.
|
||||
|
||||
The remaining cross-module declaration matches are not independent component
|
||||
anatomy. They are small token-based rules such as ellipsis, muted captions,
|
||||
uppercase terms, or flex-column containment applied to different semantic
|
||||
elements. Moving those rules into a component would erase meaning; their
|
||||
visual values already come from Core tokens. Remaining larger local layouts
|
||||
are deliberately domain-owned:
|
||||
|
||||
- unequal-track editors, import mappings and schema/data tables;
|
||||
- calendar time grids, charts, graph node shapes and graph edge semantics;
|
||||
- file/mail/postbox/records explorer panes whose interaction contracts differ;
|
||||
- timelines, evidence histories, recipient compositions and policy-specific
|
||||
detail sections;
|
||||
- compact list-row internals that cannot preserve their semantics through
|
||||
`SelectionListItemContent`.
|
||||
|
||||
A future candidate is promoted only when a new audit identifies repeated
|
||||
structure plus the same responsive, accessibility and interaction contract.
|
||||
The enforcement script prevents regression for the patterns centralized in
|
||||
this pass and maintains the reviewed dialog-width baseline.
|
||||
@@ -0,0 +1,323 @@
|
||||
# GovOPlaN Capability and IT-Infrastructure Fit Assessment
|
||||
|
||||
> Generated from [`capability-fit-current.json`](../../capability-fit-current.json).
|
||||
> Edit and validate the machine-readable assessment, then regenerate this file;
|
||||
> do not maintain conclusions independently in Markdown.
|
||||
|
||||
This is an evidence-based fit assessment, not a production approval or
|
||||
security certification. Repository or manifest existence alone never counts
|
||||
as an implemented capability. Unknown target requirements remain explicitly
|
||||
`not_assessed`.
|
||||
|
||||
## Assessment record
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Assessment ID | `campaign-reference-2026-07-22` |
|
||||
| Schema version | `govoplan.fit-assessment/0.2.0` |
|
||||
| Assessed on | 2026-07-22 |
|
||||
| Scope | Campaign-centric internal pilot and small-production candidate |
|
||||
| Release | `stable-catalog-202607220843` (tagged_release) |
|
||||
| Meta commit | `5447299289a1` |
|
||||
| Deployment profile | `production-like-dev` · `partial` |
|
||||
| Configuration packages | `none: environment-profile basis only` |
|
||||
| Canonical input SHA-256 | `5a23f17c5289c5a89d2e92445f2c8b2eef54e3753f1392ebf300aff5508f0bfe` |
|
||||
|
||||
## Controlled status vocabulary
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| `verified` | Implemented and directly exercised by evidence appropriate to the stated scope. |
|
||||
| `available_unconfigured` | Implemented with supporting evidence, but not configured and exercised in the target. |
|
||||
| `partial` | A useful subset exists, but a material part of the requirement is missing or unproved. |
|
||||
| `scaffold` | Contracts or structure exist, but the end-to-end capability is not usable. |
|
||||
| `external_system` | The deployment or another system must supply the capability. |
|
||||
| `planned` | Only a concept, backlog item, or design direction exists. |
|
||||
| `not_fit` | Evidence shows that the assessed composition cannot meet the requirement. |
|
||||
| `not_assessed` | The requirement or target environment is not sufficiently known. |
|
||||
|
||||
## Scope and reference journeys
|
||||
|
||||
Reference journeys:
|
||||
|
||||
- Internal operator authors, validates, builds, queues, sends and reconciles an email Campaign with managed attachments
|
||||
- Operator inspects delivery and audit evidence
|
||||
|
||||
Explicitly postponed:
|
||||
|
||||
- Workflow and workflow-driven user stories
|
||||
|
||||
## Facts
|
||||
|
||||
- The assessment is pinned to signed stable catalog sequence 202607220843 and the exact module commits listed below.
|
||||
- The Campaign authoring, validation, build, mock-delivery, managed-file, local-access, and local-audit paths have direct test or contract evidence.
|
||||
- The production-like development profile runs PostgreSQL and Redis in containers while application processes use editable source trees.
|
||||
- No installed-target, external-provider, reference-readiness, recovery, or production-approval evidence bundle is attached to this assessment.
|
||||
|
||||
## Decisions
|
||||
|
||||
- Use Campaign as the first reference journey and flagship pilot scenario.
|
||||
- Keep Workflow and workflow-driven user stories planned and explicitly postponed for this assessment.
|
||||
- Use local GovOPlaN accounts for the bounded pilot; do not claim federated identity support.
|
||||
- Do not approve small production until installed-artifact, target mail, monitoring, backup/restore, and recovery proof checks pass.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- The pilot can use local accounts and one internal tenant or office.
|
||||
- A dedicated non-production SMTP/IMAP account and safe recipients are available.
|
||||
- Pilot load fits one API and one worker until measured otherwise.
|
||||
- Durable local storage is acceptable for the pilot.
|
||||
|
||||
## Unresolved decisions
|
||||
|
||||
- What are the target organization's data classes, legal bases, retention and external-disclosure rules?
|
||||
- Which identity, mail, file, address and monitoring systems are mandatory?
|
||||
- What are Campaign volume, concurrency, growth, availability, RPO and RTO?
|
||||
- Who owns each external runtime component and operational control?
|
||||
- Which accessibility, security, support and procurement constraints are mandatory?
|
||||
|
||||
## Pinned release and composition
|
||||
|
||||
Release reproducible: **yes**.
|
||||
|
||||
Release notes:
|
||||
|
||||
- The live stable catalog has a valid Ed25519 signature trusted through release-key-1.
|
||||
- Core v0.1.13 and Campaign v0.1.10 are tagged and package-integrated; this is not target-environment or production approval.
|
||||
- The absence of a configuration package is pinned explicitly as an environment-profile-only basis; this remains a promotion gap.
|
||||
|
||||
| Module | Repository and commit | Manifest version | Enabled | Role |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `core` | `govoplan-core` @ `d487726f4d2c` | `0.1.13` | yes | API, registry, migrations, sessions, kernel contracts and shared WebUI |
|
||||
| `tenancy` | `govoplan-tenancy` @ `efbec827616b` | `0.1.8` | yes | Tenant context and lifecycle |
|
||||
| `organizations` | `govoplan-organizations` @ `39c081c4fb8f` | `0.1.8` | yes | Organization model |
|
||||
| `identity` | `govoplan-identity` @ `7a1710af896f` | `0.1.8` | yes | Normalized internal identity directory |
|
||||
| `access` | `govoplan-access` @ `f1d64d247e12` | `0.1.11` | yes | Local authentication, sessions, API keys and RBAC |
|
||||
| `admin` | `govoplan-admin` @ `11ecf362a36d` | `0.1.8` | yes | Administration surfaces |
|
||||
| `dashboard` | `govoplan-dashboard` @ `4b960ad37f0d` | `0.1.8` | yes | Module-aware home surface |
|
||||
| `policy` | `govoplan-policy` @ `1063622d311a` | `0.1.9` | yes | Policy explanation and configuration boundary |
|
||||
| `audit` | `govoplan-audit` @ `d3d2c60d7dc1` | `0.1.8` | yes | Database audit records and retrying audit outbox |
|
||||
| `campaigns` | `govoplan-campaign` @ `735e874bd03c` | `0.1.10` | yes | Campaign authoring, build, delivery control and reporting |
|
||||
| `files` | `govoplan-files` @ `2b34f6e30578` | `0.1.9` | yes | Managed files and Campaign attachments |
|
||||
| `mail` | `govoplan-mail` @ `3e2302909022` | `0.1.10` | yes | SMTP and IMAP profiles and transports |
|
||||
| `calendar` | `govoplan-calendar` @ `9bcf41bb1fbb` | `0.1.8` | yes | Optional calendar outside the Campaign pilot minimum |
|
||||
| `docs` | `govoplan-docs` @ `be52b716caed` | `0.1.10` | yes | Configured-system documentation |
|
||||
| `ops` | `govoplan-ops` @ `341773a4ff8a` | `0.1.8` | yes | Readiness and deployment-profile visibility |
|
||||
| `addresses` | `govoplan-addresses` @ `93dddbb8c52a` | `0.1.9` | no | Optional reusable recipient sources and CardDAV |
|
||||
|
||||
## Deployment profile
|
||||
|
||||
Status: `partial`
|
||||
|
||||
PostgreSQL and Redis run in containers while API, WebUI, worker and scheduler run from editable source trees.
|
||||
|
||||
Evidence:
|
||||
|
||||
- configuration/current_workspace: govoplan/dev/production-like/docker-compose.yml
|
||||
- documentation/documented_model: govoplan/dev/production-like/README.md
|
||||
|
||||
## Recommended scenarios
|
||||
|
||||
### Controlled Campaign pilot
|
||||
|
||||
Status: `partial`
|
||||
|
||||
Proceed with a bounded internal pilot after its provider, privacy, workload, and recovery proof checks are assigned and passed.
|
||||
|
||||
Composition: `core`, `tenancy`, `organizations`, `identity`, `access`, `admin`, `dashboard`, `policy`, `audit`, `campaigns`, `files`, `mail`, `docs`, `ops`.
|
||||
|
||||
Topology:
|
||||
|
||||
- One supervised GovOPlaN API process and one immutable built WebUI behind deployment-owned TLS termination
|
||||
- One PostgreSQL database and a durable single-node or shared managed-file path
|
||||
- One persistent private Redis broker and one supervised Celery worker when asynchronous delivery is enabled
|
||||
- One dedicated non-production SMTP/IMAP account with a restricted safe-recipient policy
|
||||
- External health checks, centralized logs, protected secret injection, and coordinated backup storage
|
||||
|
||||
Conditions:
|
||||
|
||||
- Use one internal tenant or office and controlled operators.
|
||||
- Keep recipient volume non-critical until measured.
|
||||
- Enable Addresses only when reusable recipient lists or CardDAV are explicitly in scope.
|
||||
- Do not enable or claim Workflow from this assessment.
|
||||
|
||||
### Small-production candidate
|
||||
|
||||
Status: `partial`
|
||||
|
||||
Do not approve production until every listed operational gate has target evidence and the residual risks have named owners.
|
||||
|
||||
Composition: `core`, `tenancy`, `organizations`, `identity`, `access`, `admin`, `dashboard`, `policy`, `audit`, `campaigns`, `files`, `mail`, `docs`, `ops`.
|
||||
|
||||
Topology:
|
||||
|
||||
- Immutable separately supervised WebUI, API, and worker artifacts behind monitored reverse-proxy TLS
|
||||
- Dedicated or managed PostgreSQL with measured coordinated backup and isolated restore
|
||||
- Persistent authenticated Redis with queue-age, queue-depth, and worker-health alerts
|
||||
- Durable shared or S3-compatible object storage with versioning, lifecycle, and restore evidence
|
||||
- Target-native secret management, centralized monitoring/logging/audit export, and an exercised incident and disaster-recovery procedure
|
||||
|
||||
Conditions:
|
||||
|
||||
- Pin and promote a configuration package instead of relying on an environment-only basis.
|
||||
- Pass installed-release, target SMTP/IMAP, accessibility, privacy, security, operations, and recovery evidence gates.
|
||||
- Agree availability, RPO, RTO, retention, support, and procurement requirements.
|
||||
- Run only one scheduler unless distributed leadership or locking is proved.
|
||||
|
||||
## Functional matrix context
|
||||
|
||||
### Required modules
|
||||
|
||||
- core
|
||||
- tenancy
|
||||
- organizations
|
||||
- identity
|
||||
- access
|
||||
- admin
|
||||
- dashboard
|
||||
- policy
|
||||
- audit
|
||||
- campaigns
|
||||
- files
|
||||
- mail
|
||||
- docs
|
||||
- ops
|
||||
|
||||
### Optional modules
|
||||
|
||||
- addresses
|
||||
|
||||
### External systems and connectors
|
||||
|
||||
- Deployment-owned reverse proxy and TLS certificate lifecycle
|
||||
- Target SMTP/IMAP service and its DNS, certificate, throttling, bounce, and reply policies
|
||||
- Target-native secret store, monitoring/logging platform, backup storage, and incident-response process
|
||||
|
||||
### Missing contracts
|
||||
|
||||
- End-to-end federated identity provider and lifecycle contract
|
||||
- Target monitoring, alert delivery, and central audit/SIEM acceptance contract
|
||||
- Production configuration-package promotion and approval evidence
|
||||
|
||||
### Policy decisions
|
||||
|
||||
- Recipient allow-list, permitted sender, attachment, retention, and external-disclosure policy
|
||||
- Identity, MFA, break-glass, service-account, and joiner/mover/leaver policy
|
||||
- Availability, RPO, RTO, support, procurement, and residual-risk ownership
|
||||
|
||||
### Manual workarounds
|
||||
|
||||
- Use controlled local accounts while federation remains outside the verified slice
|
||||
- Use one supervised scheduler where periodic work is unavoidable
|
||||
- Keep provider reconciliation and production promotion under explicit operator review
|
||||
|
||||
### Blockers
|
||||
|
||||
- No promoted configuration package is pinned
|
||||
- No installed-target or target SMTP/IMAP proof is attached
|
||||
- No coherent target backup/restore or disaster-recovery drill with measured RPO/RTO is attached
|
||||
- No target privacy, security, accessibility, operations, or production-approval evidence is attached
|
||||
|
||||
## Assessment questionnaire
|
||||
|
||||
Every required area remains visible even when its target answer is unknown.
|
||||
|
||||
| Area | Question | State | Answer | Evidence |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Scope Outcomes | Which journey is assessed? | `answered` | An internal operator authors, validates, builds, queues, sends and reconciles a Campaign with managed attachments. | — |
|
||||
| Scope Outcomes | Is Workflow in scope? | `answered` | No; Workflow is planned and explicitly postponed. | — |
|
||||
| Scope Outcomes | Which users, roles, tenants, organization units, and delegated functions participate? | `assumed` | One internal tenant or office with controlled Campaign operators; detailed organization and delegation shape remains target-specific. | — |
|
||||
| Scope Outcomes | What constitutes pilot success and production acceptance? | `answered` | Pilot success requires the bounded Campaign journey and proof checks; production additionally requires installed-artifact, provider, privacy, security, operations, recovery, and approval evidence. | — |
|
||||
| Data Policy | Which data classes and legal bases apply? | `not_assessed` | — | — |
|
||||
| Data Policy | What retention, deletion, archive and legal-hold rules apply? | `not_assessed` | — | — |
|
||||
| Data Policy | Which privacy, security, residency, minimization, access, and external-disclosure constraints apply? | `not_assessed` | — | — |
|
||||
| Identity Integrations | May the pilot use local GovOPlaN accounts? | `assumed` | Yes; federation is outside the verified composition. | — |
|
||||
| Identity Integrations | Which target SMTP/IMAP service and policy apply? | `not_assessed` | — | — |
|
||||
| Identity Integrations | Which identity protocols, MFA, joiner/mover/leaver, service-account, and break-glass rules are mandatory? | `not_assessed` | — | — |
|
||||
| Identity Integrations | Which connector protocols, versions, directions, authentication, certificate, rate-limit, egress, and degraded-mode requirements apply? | `not_assessed` | — | — |
|
||||
| Workload Growth | What are Campaign frequency, recipients per Campaign, send window, import size and attachment volume? | `not_assessed` | — | — |
|
||||
| Workload Growth | What are tenant, named-user, active-user, concurrent-user, and peak-request assumptions? | `not_assessed` | — | — |
|
||||
| Workload Growth | What are tenant, user, concurrency, file, database, queue and audit growth assumptions? | `not_assessed` | — | — |
|
||||
| Workload Growth | What connector traffic, scheduled-job, batch, queue-depth, queue-age, and external-rate-limit peaks apply? | `not_assessed` | — | — |
|
||||
| Availability Operations | What availability, RPO and RTO are required? | `not_assessed` | — | — |
|
||||
| Availability Operations | Who operates database, queue, storage, TLS, secrets, monitoring, backup and incident response? | `not_assessed` | — | — |
|
||||
| Availability Operations | Which hosting, network-zone, egress, proxy, DNS, NTP, certificate-authority, residency, or disconnected-operation constraints apply? | `not_assessed` | — | — |
|
||||
| Procurement Decisions | Which licensing, accessibility, security, certification, support and procurement conditions are mandatory? | `not_assessed` | — | — |
|
||||
|
||||
## Functional capability matrix
|
||||
|
||||
| Requirement | Status | Evidence | Conditions and gaps | Recommendation and proof |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **platform.composition**<br>Compose enabled backend and WebUI modules without hard optional-module dependencies. | `verified` | test/committed_source: govoplan-core/tests/test_module_system.py; contract/current_workspace: govoplan/tools/checks/check-contracts.py (43 modules, 33 providers, 19 requirements, no issues) | Condition: Package integration is verified; repeat checks on the installed target composition.; Gap: No target deployment acceptance is recorded.; Risk: A reproducible module graph can still be installed or configured incorrectly. | Use the signed stable catalog and verify the minimal Campaign composition after installation.<br>**Proof:** Run contract, migration, API and WebUI module-permutation gates on the installed release. |
|
||||
| **access.local**<br>Provide tenant-scoped local accounts, sessions, API keys and RBAC. | `verified` | test/committed_source: govoplan-access/tests/test_auth_dependencies.py; test/committed_source: govoplan-core/tests/test_api_smoke.py#cookie-session-csrf | Condition: Pilot accepts local accounts.; Gap: MFA and federated lifecycle are not part of this conclusion.; Risk: Manual account lifecycle may not satisfy production identity policy. | Use controlled local pilot accounts and define break-glass/bootstrap rules.<br>**Proof:** Exercise joiner, role change, suspension and protected-owner recovery. |
|
||||
| **campaign.journey**<br>Author, validate, build, queue, send, reconcile and report a Campaign with frozen execution evidence. | `verified` | test/committed_source: govoplan-core/tests/test_api_smoke.py#campaign-create-validate-build-mock-send; test/committed_source: govoplan-campaign/tests (Campaign v0.1.10 is exactly the catalog-selected tagged source); configuration/committed_source: https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json#sequence-202607220843 (Core v0.1.13 and Campaign v0.1.10 have matching catalogued Python and WebUI refs) | Condition: This verifies implementation paths, not target-provider delivery.; Gap: Usability and target-provider acceptance remain separate.; Risk: Package integration does not prove provider behavior or production operations. | Use the catalogued Campaign release for usability and target-provider acceptance.<br>**Proof:** Run the complete journey with safe data and the target-like mail service. |
|
||||
| **files.managed_attachments**<br>Store and resolve managed Campaign attachments on durable storage. | `verified` | test/current_workspace: govoplan-files/tests (14 tests passed); test/current_workspace: govoplan-campaign/tests/test_attachment_building.py | Condition: Deployment provides a durable storage root.; Gap: Target backup and restore are not verified.; Risk: Node-local storage prevents safe independent API scaling. | Use durable local storage for the pilot and assess object/shared storage before scaling.<br>**Proof:** Back up and restore files together with database references. |
|
||||
| **mail.smtp_imap**<br>Send Campaign mail through SMTP and optionally append sent messages through IMAP. | `available_unconfigured` | test/current_workspace: govoplan-mail/tests (22 tests passed); documentation/documented_model: govoplan-campaign/docs/CAMPAIGN_DELIVERY_RUNBOOK.md | Condition: Use a dedicated non-production service account and safe recipients.; Gap: No target provider, TLS chain, throttling or bounce/reply process was exercised.; Risk: Ambiguous provider outcomes can cause duplicate-send risk if reconciled incorrectly. | Run target-like interoperability and failure drills before production use.<br>**Proof:** Prove SMTP acceptance, IMAP append, throttling and outcome reconciliation. |
|
||||
| **addresses.recipient_sources**<br>Select reusable address lists as Campaign recipient sources. | `available_unconfigured` | test/current_workspace: govoplan-addresses/tests (14 tests passed) | Condition: Enable the Addresses module explicitly.; Gap: Addresses is disabled in the pinned root profile.; Risk: Recipient governance may differ between source data and frozen Campaign evidence. | Enable only when reusable lists are a pilot requirement.<br>**Proof:** Build a Campaign from a source list and verify immutable recipient provenance. |
|
||||
| **audit.local**<br>Retain tenant/system audit evidence and retry governed audit events. | `verified` | test/current_workspace: govoplan-audit/tests (5 tests passed) | Condition: Conclusion covers local database evidence only.; Gap: No central sink, retention enforcement or tamper-evident archive is verified.; Risk: Local audit evidence may not satisfy organizational records or SIEM requirements. | Define retention and export requirements before production approval.<br>**Proof:** Exercise privileged-event review, retention and any required external export. |
|
||||
| **identity.federation**<br>Integrate external LDAP/AD, OIDC/SAML or SCIM identity infrastructure. | `scaffold` | documentation/documented_model: govoplan-idm/README.md | Gap: No end-to-end provider connector or federated login is verified.; Risk: Federation-dependent organizations cannot use the current pilot composition without extra implementation. | Use local pilot accounts or assess and implement the selected provider path.<br>**Proof:** Run provider metadata, login/provisioning, deprovisioning and failure tests. |
|
||||
| **compliance.export_control**<br>Screen persons and organizations against embargo/sanctions lists with review evidence. | `planned` | issue/documented_model: https://git.add-ideas.de/GovOPlaN/govoplan/issues/12 | Gap: No provider, list provenance, match policy, review flow or legal evidence exists.; Risk: The current composition must not be represented as performing export-control screening. | Keep outside pilot claims until the user story is implemented and legally validated.<br>**Proof:** Validate list ingestion, versioning, matching, false-positive review and audit evidence. |
|
||||
| **workflow**<br>Orchestrate the journey through Workflow. | `planned` | observation/documented_model: Assessment scope (Explicitly postponed) | Gap: Workflow is outside this assessment.; Risk: Including it would overstate the assessed composition. | Do not enable or claim Workflow for this reference pilot.<br>**Proof:** Reassess in a later Workflow-focused composition. |
|
||||
|
||||
## Infrastructure matrix
|
||||
|
||||
| Requirement | Status | Evidence | Conditions and gaps | Recommendation and proof |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **runtime.web_api**<br>Serve matching WebUI and API artifacts with health endpoints. | `verified` | test/committed_source: govoplan-core/tests/test_module_system.py; route/committed_source: govoplan-core/src/govoplan_core/server/fastapi.py#/health | Condition: Materialize the matching catalogued artifacts in the target.; Gap: No production image or service bundle is supplied by the profile.; Risk: Editable source processes are unsuitable as a production artifact. | Install matching catalogued WebUI/API refs and supervise them as immutable artifacts.<br>**Proof:** Deploy the built artifacts and run health/module-route checks. |
|
||||
| **runtime.worker**<br>Run durable asynchronous Campaign jobs. | `available_unconfigured` | configuration/current_workspace: govoplan/tools/launch/launch-production-like-dev.sh | Condition: Redis and a supervised worker are required when Celery is enabled.; Gap: Target heartbeat, restart and queue-age alerting are not proved.; Risk: Queued work can stall silently without monitoring. | Start one worker for the pilot and split queues only after measurement.<br>**Proof:** Interrupt and restart a worker while preserving job/reconciliation safety. |
|
||||
| **runtime.scheduler**<br>Run periodic recovery and cleanup safely. | `partial` | test/committed_source: govoplan-calendar/tests/test_outbox.py (Committed and pushed after the catalogued Calendar v0.1.8 tag) | Condition: Calendar outbox and recovery work is remote-integrated source but not stable-package-integrated.; Gap: No distributed leader election or target supervision is established.; Risk: Multiple schedulers can duplicate periodic dispatch without locking. | Omit from the Campaign-only pilot or run one supervised instance.<br>**Proof:** Prove missed-schedule recovery and single-leader behavior. |
|
||||
| **data.postgresql**<br>Persist application state in PostgreSQL with explicit migrations. | `verified` | configuration/committed_source: govoplan/dev/postgres; test/committed_source: govoplan/tools/checks/postgres-integration-check.py | Condition: Target database remains deployment-owned.; Gap: HA, patching, WAL policy and capacity are not assessed.; Risk: A single unprotected database is a system-wide failure point. | Use managed or dedicated PostgreSQL with explicit migration and backup controls.<br>**Proof:** Run migrations and restore a target-like database. |
|
||||
| **queue.redis**<br>Provide the Celery broker and queue persistence. | `available_unconfigured` | configuration/current_workspace: govoplan/dev/production-like/docker-compose.yml#redis | Gap: Authentication, TLS, eviction, HA and queue-loss policy are not assessed.; Risk: Broker loss or eviction can delay work even when database business state survives. | Configure private persistent Redis and monitor queue age/depth.<br>**Proof:** Exercise broker interruption and worker recovery. |
|
||||
| **storage.local**<br>Persist managed files on a durable single-node/shared path. | `verified` | contract/committed_source: govoplan-files/src/govoplan_files/backend/storage/backends.py | Condition: Path is durable, private, writable and backed up.; Gap: Node-local storage cannot support independent API replicas.; Risk: Files can be lost or become inconsistent with database state. | Use for a bounded pilot only with coordinated backup.<br>**Proof:** Restore files and verify all database references. |
|
||||
| **storage.object**<br>Use S3-compatible storage for independently scalable file persistence. | `partial` | test/current_workspace: govoplan-files/tests/test_connector_providers.py | Gap: No chosen target service or storage-backend interoperability drill.; Risk: Provider semantics, CA or lifecycle mismatch can break file access/retention. | Select and exercise the target object store before horizontal scaling.<br>**Proof:** Upload, retrieve, version, back up and restore representative objects. |
|
||||
| **edge.proxy_tls**<br>Terminate HTTPS and enforce proxy/security policy. | `external_system` | route/committed_source: govoplan-ops/src/govoplan_ops/backend/api/v1/routes.py#deployment-security | Gap: No proxy, certificates, renewal, header or request-limit configuration is shipped here.; Risk: Incorrect proxy/cookie/CORS configuration can expose sessions or block legitimate use. | Supply and monitor the edge through the target platform.<br>**Proof:** Run external TLS/header/cookie/CORS and upload-limit tests. |
|
||||
| **security.secret_store**<br>Inject and rotate master, database, mail and connector secrets. | `external_system` | configuration/committed_source: govoplan/.env.example | Gap: No target secret manager or rotation drill is selected.; Risk: Loss of the master key makes encrypted credentials unavailable; leakage compromises connectors. | Use target-native secret injection and document rotation/recovery.<br>**Proof:** Rotate a non-production credential and recover from a protected backup. |
|
||||
| **identity.access**<br>Authenticate users and enforce tenant-scoped authorization through the selected identity mode. | `verified` | test/committed_source: govoplan-access/tests/test_auth_dependencies.py; test/committed_source: govoplan-core/tests/test_api_smoke.py#cookie-session-csrf | Condition: The bounded pilot accepts local GovOPlaN accounts.; Gap: Target MFA, federation, provisioning, and joiner/mover/leaver requirements are not assessed.; Risk: A local-only identity topology may not satisfy institutional production policy. | Use controlled local pilot accounts and assess the mandatory production identity topology separately.<br>**Proof:** Exercise login, role change, account suspension, protected bootstrap, and break-glass recovery in the target. |
|
||||
| **connectors.mail**<br>Reach the selected SMTP/IMAP and other external connector endpoints under explicit network and provider policy. | `available_unconfigured` | test/current_workspace: govoplan-mail/tests (Protocol adapters have direct tests; no target provider was exercised); documentation/documented_model: govoplan-campaign/docs/CAMPAIGN_DELIVERY_RUNBOOK.md | Condition: The deployment supplies DNS, egress, proxy, CA trust, scoped service accounts, and provider limits.; Gap: No target endpoint, TLS chain, throttling, sender policy, bounce/reply path, or disclosure agreement is assessed.; Risk: Provider rejection, delay, or ambiguous outcomes can affect delivery and evidence completeness. | Use a dedicated safe provider account for the pilot and require target interoperability evidence before production.<br>**Proof:** Exercise target-like SMTP acceptance, IMAP append, throttling, outage, retry, and reconciliation through the approved network path. |
|
||||
| **operations.monitoring**<br>Detect API, database, worker, queue, storage and delivery degradation. | `partial` | route/committed_source: govoplan-ops/src/govoplan_ops/backend/api/v1/routes.py#/ops/readiness; contract/committed_source: govoplan-core/src/govoplan_core/server/fastapi.py#slow-request-logging | Gap: No metrics exporter, log collector, dashboards, alert routes or SLO is verified.; Risk: Failures and queue backlog can remain unnoticed. | Integrate external monitoring before small production.<br>**Proof:** Trigger each readiness/delivery failure and verify an actionable alert. |
|
||||
| **operations.audit**<br>Retain, monitor, review, and where required export security and business audit evidence. | `partial` | test/current_workspace: govoplan-audit/tests (Local audit persistence and retry behavior are exercised) | Condition: Local database audit evidence is part of coordinated backup and access review.; Gap: Target retention enforcement, tamper-evident export, SIEM integration, alerting, and privileged review are not verified.; Risk: Local evidence alone may not meet institutional security, records, or incident-response requirements. | Define the target audit retention, export, monitoring, and review controls before production approval.<br>**Proof:** Exercise privileged-event review, retention, export failure/retry, and target SIEM or archive ingestion. |
|
||||
| **operations.backup_restore**<br>Back up and restore database, files, configuration and keys as a coherent service. | `partial` | documentation/documented_model: govoplan-core/docs/DEPLOYMENT_OPERATOR_GUIDE.md; issue/documented_model: https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/29 | Gap: No target full-service restore drill or measured RPO/RTO exists.; Risk: Partial restore can produce missing files, unusable secrets or inconsistent evidence. | Treat Core #29 and a target restore drill as a production gate.<br>**Proof:** Restore the whole service into an isolated environment and measure it. |
|
||||
| **operations.disaster_recovery**<br>Recover the service after site or dependency loss within agreed RPO/RTO. | `not_assessed` | absence/current_workspace: No target DR plan or exercise evidence supplied | Gap: RPO/RTO, off-site copies, recovery order, failover, communications and exercise schedule are unknown.; Risk: Service and evidence may be unrecoverable after a major incident. | Define and exercise DR before any availability commitment.<br>**Proof:** Run a documented end-to-end recovery exercise. |
|
||||
## Data flows and trust boundaries
|
||||
|
||||
| Flow | From → to | Data | Trust boundary | Controls |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `browser.api` | User browser → Reverse proxy and GovOPlaN WebUI/API | Session and CSRF cookies; Campaign content; Recipient personal data; Managed files | Client/public to application | HTTPS; Exact CORS origins; Secure cookies; Tenant and RBAC enforcement; Request limits |
|
||||
| `api.database` | GovOPlaN API and workers → PostgreSQL | Tenant and identity records; Campaign drafts, snapshots and jobs; Connector metadata; Audit evidence | Application to primary state store | Dedicated database identity; Private or encrypted transport; Migrations; Backup and retention |
|
||||
| `api.queue.worker` | GovOPlaN API → Redis and Celery worker | Job identifiers; Queue routing and retry metadata | Request plane to asynchronous processing plane | Private authenticated broker; Bounded payloads; Idempotent claims; Queue monitoring |
|
||||
| `worker.mail` | GovOPlaN Campaign worker → External SMTP and IMAP services | Recipient addresses; Message bodies; Attachments; Sent-message copy | GovOPlaN to external communication provider | Scoped service account; TLS and CA policy; Sender and recipient policy; Rate limits; Outcome reconciliation |
|
||||
| `worker.connectors` | GovOPlaN connector worker → External address, file, object or calendar service | Addresses; Files and provenance; Calendar resources | GovOPlaN to organizational/external content systems | Explicit sync direction; Scoped credentials; Endpoint allow-list; Provenance; Conflict and reconciliation policy |
|
||||
|
||||
## Risks and residual risks
|
||||
|
||||
| Risk | Impact | Treatment | Owner | Residual risk |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **risk.reproducibility**<br>The signed package selection is reproducible but has not been accepted as an installed target composition. | Installation or configuration drift can still produce uncertain deployed behavior. | Materialize the signed catalog in an isolated target and run installed-artifact acceptance gates. | unassigned | Module and environment differences still require release-environment verification. |
|
||||
| **risk.delivery_provider**<br>Target SMTP/IMAP behavior and failure modes are unproved. | Failed, delayed or duplicate communication and incomplete evidence. | Run target-like interoperability, throttling and uncertainty drills. | unassigned | External provider outages and ambiguous outcomes remain operational risks. |
|
||||
| **risk.recovery**<br>Backup/restore and disaster recovery are not demonstrated across all state and keys. | Irrecoverable or inconsistent service after loss. | Complete Core #29 and an isolated full-service restore/DR exercise. | unassigned | Recovery time and data loss remain bounded by the selected external infrastructure. |
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Proceed only with a controlled internal Campaign pilot after the bounded proof checks pass.
|
||||
- Use the minimal composition and enable Addresses only for an explicit reusable-recipient journey.
|
||||
- Do not claim Workflow, export-control screening, identity federation or production DR as implemented.
|
||||
- Treat installed-release acceptance, target mail proof, monitoring and a coherent restore drill as production gates.
|
||||
|
||||
## Proof-of-concept and promotion checks
|
||||
|
||||
1. Materialize the signed catalog into an isolated installation and rerun contract, migration and module-permutation gates against the installed artifacts.
|
||||
2. Collect the isolated installation with the bounded installed-composition evidence contract; require exact enabled package/module versions, complete RECORD verification and immutable provenance anchored to this assessment.
|
||||
3. Run a safe target-like Campaign through SMTP acceptance, IMAP append, reporting and audit.
|
||||
4. Drill worker, Redis and ambiguous-delivery failures without duplicate sends.
|
||||
5. Restore PostgreSQL, managed files, configuration and encrypted credentials and measure RPO/RTO.
|
||||
6. Validate proxy/TLS, cookies/CORS, account bootstrap, secret redaction, monitoring and alert delivery.
|
||||
7. Measure representative Campaign/file/queue/database load and external throttling.
|
||||
8. Require separately issued, expiring and independently scope-authorized evidence before marking target environment, external provider or production approval proof as checked.
|
||||
|
||||
## Generation contract
|
||||
|
||||
This report is deterministic output from the schema-validated JSON companion.
|
||||
The generator rejects duplicate JSON keys, schema drift, secret-bearing field
|
||||
names, stale checked-in output, and oversized inputs. A new assessment or
|
||||
release changes the canonical input hash and requires review of the affected
|
||||
evidence and conclusions through the release-aware reassessment tool.
|
||||
+26
-14
@@ -1,4 +1,16 @@
|
||||
# GovOPlaN Capability and IT-Infrastructure Fit Assessment
|
||||
# Supporting Narrative: 2026-07-22 Capability and Infrastructure Assessment
|
||||
|
||||
> **Canonical report:** The schema-validated human report is generated from the
|
||||
> machine-readable input at
|
||||
> [`CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md`](CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md).
|
||||
> This file retains the original hand-authored evidence narrative and operator
|
||||
> guidance; it is not maintained as a second set of conclusions.
|
||||
|
||||
> **Pinned historical evidence:** This document assesses the exact 2026-07-22
|
||||
> Campaign composition below. It is intentionally not updated to describe later
|
||||
> main-branch work. Use [Strategy Status](../../strategy/STRATEGY_STATUS.md) for the current
|
||||
> cross-product reconciliation and create a new dated fit assessment for a new
|
||||
> target composition.
|
||||
|
||||
## Assessment record
|
||||
|
||||
@@ -11,16 +23,16 @@
|
||||
| Configuration basis | Root `.env.example` and the production-like development profile |
|
||||
| Scope | Campaign-centric internal pilot and small-production candidate |
|
||||
| Explicitly postponed | Workflow and workflow-driven user stories |
|
||||
| Machine-readable companion | [`capability-fit-current.json`](capability-fit-current.json) |
|
||||
| Input schema | [`capability-fit.schema.json`](capability-fit.schema.json) |
|
||||
| Machine-readable companion | [`capability-fit-current.json`](../../capability-fit-current.json) |
|
||||
| Input schema | [`capability-fit.schema.json`](../../capability-fit.schema.json) |
|
||||
|
||||
**Snapshot notice:** this assessment remains valid only for the pinned
|
||||
2026-07-22 composition above. Workflow Engine, the optional Workflow editor,
|
||||
Datasources, Dataflow, Search, encryption contracts, and other later main-branch
|
||||
work must not be inferred into this evidence record. The current product
|
||||
direction and implemented-state reconciliation are documented separately in
|
||||
the
|
||||
[Institutional Governance Target Architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).
|
||||
the [Institutional Governance Target Architecture](../../architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md)
|
||||
and [Strategy Status](../../strategy/STRATEGY_STATUS.md).
|
||||
|
||||
This is a fit assessment, not a production approval or security certification.
|
||||
It deliberately does not infer implementation from a repository, issue, or
|
||||
@@ -398,7 +410,7 @@ that observation to the assessment and signed catalog:
|
||||
```
|
||||
|
||||
The collector follows the strict version `0.4.0`
|
||||
[`installed-composition-evidence.schema.json`](installed-composition-evidence.schema.json)
|
||||
[`installed-composition-evidence.schema.json`](../../installed-composition-evidence.schema.json)
|
||||
contract. It enumerates all installed distributions whose normalized name starts
|
||||
with `govoplan-`, compares the enabled assessed package and module-manifest
|
||||
versions, and identifies missing, duplicate and extra GovOPlaN distributions.
|
||||
@@ -481,14 +493,14 @@ unchecked boundary.
|
||||
Installed evidence cannot establish target acceptance, accessibility, privacy,
|
||||
security, operations, recovery, an external provider, or production use. These
|
||||
scopes use a separate, expiring
|
||||
[`capability-fit-boundary-evidence.schema.json`](capability-fit-boundary-evidence.schema.json)
|
||||
[`capability-fit-boundary-evidence.schema.json`](../../capability-fit-boundary-evidence.schema.json)
|
||||
bundle. The bundle is bound to the assessment ID, assessment release and exact
|
||||
installed-evidence SHA-256 digest. It contains only opaque subject/control/result
|
||||
IDs and content hashes, not endpoints, credentials, people or raw result files.
|
||||
|
||||
Boundary evidence is accepted only when at least one Ed25519 signature validates
|
||||
against a separately provisioned
|
||||
[`capability-fit-proof-authority-keyring.schema.json`](capability-fit-proof-authority-keyring.schema.json).
|
||||
[`capability-fit-proof-authority-keyring.schema.json`](../../capability-fit-proof-authority-keyring.schema.json).
|
||||
Each authority key explicitly lists the scopes it may attest. Target,
|
||||
accessibility, privacy, security, operations, recovery, and provider claims use
|
||||
`passed` or `failed`; production claims use `approved` or `rejected`.
|
||||
@@ -505,7 +517,7 @@ the tool's deterministic canonicalization.
|
||||
|
||||
`tools/assessments/boundary-evidence.py` is the bounded issuance path. It
|
||||
accepts a private target-run manifest conforming to
|
||||
[`capability-fit-boundary-run.schema.json`](capability-fit-boundary-run.schema.json),
|
||||
[`capability-fit-boundary-run.schema.json`](../../capability-fit-boundary-run.schema.json),
|
||||
hashes each retained result file without following a final-component symlink,
|
||||
and excludes all paths and raw results from the signed receipt. Issuance is
|
||||
refused unless an independently trusted catalog, exact installed payload,
|
||||
@@ -515,7 +527,7 @@ is authorized for the full proof interval; catalog and installer key reuse is
|
||||
rejected. The command immediately verifies its own result and atomically writes
|
||||
both the proof and a sanitized review. The complete operator procedure and
|
||||
recovery measurement definition are in
|
||||
[`TARGET_MATURITY_EVIDENCE_RUNBOOK.md`](TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
|
||||
[`TARGET_MATURITY_EVIDENCE_RUNBOOK.md`](../../operations/TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
|
||||
|
||||
```bash
|
||||
./.venv/bin/python tools/assessments/capability-fit.py \
|
||||
@@ -593,7 +605,7 @@ separate from production approval and from provider-specific acceptance.
|
||||
|
||||
Both authority keyrings are governance trust roots. Installer receipt keys use
|
||||
the strict
|
||||
[`installer-receipt-authority-keyring.schema.json`](installer-receipt-authority-keyring.schema.json)
|
||||
[`installer-receipt-authority-keyring.schema.json`](../../installer-receipt-authority-keyring.schema.json)
|
||||
contract and may attest only `installed_release_origin`; their public material
|
||||
must not be reused by catalog or boundary-proof authorities. Do not download or
|
||||
generate them from the proof bundle being checked. The checker rejects
|
||||
@@ -617,9 +629,9 @@ journey, source tests, or signed release metadata.
|
||||
|
||||
## Evidence used in this slice
|
||||
|
||||
- [Production-like profile](../dev/production-like/README.md) and
|
||||
[Compose dependencies](../dev/production-like/docker-compose.yml)
|
||||
- [Module contracts and install boundaries](MODULE_CONTRACTS_AND_INSTALLS.md)
|
||||
- [Production-like profile](../../../dev/production-like/README.md) and
|
||||
[Compose dependencies](../../../dev/production-like/docker-compose.yml)
|
||||
- [Module contracts and install boundaries](../../operations/MODULE_CONTRACTS_AND_INSTALLS.md)
|
||||
- [Core deployment operator guide](https://git.add-ideas.de/GovOPlaN/govoplan-core/src/branch/main/docs/DEPLOYMENT_OPERATOR_GUIDE.md)
|
||||
- [Ops scalability profiles](https://git.add-ideas.de/GovOPlaN/govoplan-ops/src/branch/main/docs/SCALABILITY_PROFILES.md)
|
||||
- Actual module manifests in the pinned repositories and the static contract
|
||||
@@ -0,0 +1,91 @@
|
||||
# DSAR Provider Coverage
|
||||
|
||||
This generated matrix is enforced by `tools/checks/check-dsar-coverage.py`.
|
||||
A migration-owning module must register and document its canonical DSAR provider.
|
||||
Every other active module requires a reviewed explanation of why it owns no
|
||||
persistent subject-data store. Adding a migration invalidates that explanation.
|
||||
|
||||
- Active modules: 72
|
||||
- Registered and documented DSAR providers: 49
|
||||
- Reviewed no-store rationales: 23
|
||||
- Unexplained coverage gaps: 0
|
||||
|
||||
| Module | Repository | Persistence | Coverage | Rationale |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `access` | `govoplan-access` | Migration-owned | Provider | Provider `privacy.dsar.access` is registered and documented. |
|
||||
| `addresses` | `govoplan-addresses` | Migration-owned | Provider | Provider `privacy.dsar.addresses` is registered and documented. |
|
||||
| `admin` | `govoplan-admin` | Migration-owned | Provider | Provider `privacy.dsar.admin` is registered and documented. |
|
||||
| `approvals` | `govoplan-approvals` | Migration-owned | Provider | Provider `privacy.dsar.approvals` is registered and documented. |
|
||||
| `assets` | `govoplan-assets` | No module migration | Reviewed no-store rationale | Contract-only module: asset persistence and lifecycle APIs are not implemented; reassess before adding a migration-owned store. |
|
||||
| `audit` | `govoplan-audit` | Migration-owned | Provider | Provider `privacy.dsar.audit` is registered and documented. |
|
||||
| `booking` | `govoplan-booking` | No module migration | Reviewed no-store rationale | Contract-only module: booking persistence and reservation workflows are not implemented; reassess before adding a migration-owned store. |
|
||||
| `calendar` | `govoplan-calendar` | Migration-owned | Provider | Provider `privacy.dsar.calendar` is registered and documented. |
|
||||
| `campaigns` | `govoplan-campaign` | Migration-owned | Provider | Provider `privacy.dsar.campaigns` is registered and documented. |
|
||||
| `cases` | `govoplan-cases` | Migration-owned | Provider | Provider `privacy.dsar.cases` is registered and documented. |
|
||||
| `certificates` | `govoplan-certificates` | No module migration | Reviewed no-store rationale | Contract-only module: certificate issuance and revocation persistence are not implemented; reassess before adding a migration-owned store. |
|
||||
| `committee` | `govoplan-committee` | Migration-owned | Provider | Provider `privacy.dsar.committee` is registered and documented. |
|
||||
| `connectors` | `govoplan-connectors` | Migration-owned | Provider | Provider `privacy.dsar.connectors` is registered and documented. |
|
||||
| `consultation` | `govoplan-consultation` | No module migration | Reviewed no-store rationale | Contract-only module: consultation submissions and evaluation persistence are not implemented; reassess before adding a migration-owned store. |
|
||||
| `contracts` | `govoplan-contracts` | No module migration | Reviewed no-store rationale | Contract-only module: contract, amendment, and obligation persistence are not implemented; reassess before adding a migration-owned store. |
|
||||
| `dashboard` | `govoplan-dashboard` | Migration-owned | Provider | Provider `privacy.dsar.dashboard` is registered and documented. |
|
||||
| `dataflow` | `govoplan-dataflow` | Migration-owned | Provider | Provider `privacy.dsar.dataflow` is registered and documented. |
|
||||
| `datasources` | `govoplan-datasources` | Migration-owned | Provider | Provider `privacy.dsar.datasources` is registered and documented. |
|
||||
| `decisions` | `govoplan-decisions` | Migration-owned | Provider | Provider `privacy.dsar.decisions` is registered and documented. |
|
||||
| `dist_lists` | `govoplan-dist-lists` | Migration-owned | Provider | Provider `privacy.dsar.dist_lists` is registered and documented. |
|
||||
| `dms` | `govoplan-dms` | No module migration | Reviewed no-store rationale | Stateless integration-preview module: DMS retains no document, person, credential, or provider-response store; Files and Records remain the subject-data owners. Reassess before persisting a target binding, plan, receipt, or diagnostic. |
|
||||
| `docs` | `govoplan-docs` | Migration-owned | Provider | Provider `privacy.dsar.docs` is registered and documented. |
|
||||
| `encryption` | `govoplan-encryption` | Migration-owned | Provider | Provider `privacy.dsar.encryption` is registered and documented. |
|
||||
| `erp` | `govoplan-erp` | No module migration | Reviewed no-store rationale | Stateless integration-contract module: ERP retains no invoice, payable, plan, booking observation, provider response, or credential store; Procurement, Payments, Ledger, Files, and Audit remain the subject-data owners. Reassess before persisting a target binding, plan, receipt, reconciliation decision, or diagnostic. |
|
||||
| `evaluation` | `govoplan-evaluation` | No module migration | Reviewed no-store rationale | Contract-only module: evaluation runs, responses, and scores are not persisted; reassess before adding a migration-owned store. |
|
||||
| `facilities` | `govoplan-facilities` | No module migration | Reviewed no-store rationale | Contract-only module: facility and maintenance persistence are not implemented; reassess before adding a migration-owned store. |
|
||||
| `files` | `govoplan-files` | Migration-owned | Provider | Provider `privacy.dsar.files` is registered and documented. |
|
||||
| `fit_connect` | `govoplan-fit-connect` | No module migration | Reviewed no-store rationale | Stateless transport-contract module: FIT-Connect retains no submission, attachment, receipt, acknowledgement plan, key, provider response, or diagnostic store; the owning Service, Forms, Cases, Files, and Audit workflows remain responsible for subject data. Reassess before persisting any ingress or event-log evidence. |
|
||||
| `forms` | `govoplan-forms` | Migration-owned | Provider | Provider `privacy.dsar.forms` is registered and documented. |
|
||||
| `forms_runtime` | `govoplan-forms-runtime` | Migration-owned | Provider | Provider `privacy.dsar.forms_runtime` is registered and documented. |
|
||||
| `grants` | `govoplan-grants` | No module migration | Reviewed no-store rationale | Contract-only module: grant applications, awards, and monitoring are not persisted; reassess before adding a migration-owned store. |
|
||||
| `helpdesk` | `govoplan-helpdesk` | Migration-owned | Provider | Provider `privacy.dsar.helpdesk` is registered and documented. |
|
||||
| `identity` | `govoplan-identity` | Migration-owned | Provider | Provider `privacy.dsar.identity` is registered and documented. |
|
||||
| `identity_trust` | `govoplan-identity-trust` | Migration-owned | Provider | Provider `privacy.dsar.identity_trust` is registered and documented. |
|
||||
| `idm` | `govoplan-idm` | Migration-owned | Provider | Provider `privacy.dsar.idm` is registered and documented. |
|
||||
| `inspections` | `govoplan-inspections` | No module migration | Reviewed no-store rationale | Contract-only module: inspections, findings, and measures are not persisted; reassess before adding a migration-owned store. |
|
||||
| `learning` | `govoplan-learning` | No module migration | Reviewed no-store rationale | Contract-only module: learning offers, enrollment, and completion are not persisted; reassess before adding a migration-owned store. |
|
||||
| `mail` | `govoplan-mail` | Migration-owned | Provider | Provider `privacy.dsar.mail` is registered and documented. |
|
||||
| `mandates` | `govoplan-mandates` | Migration-owned | Provider | Provider `privacy.dsar.mandates` is registered and documented. |
|
||||
| `notifications` | `govoplan-notifications` | Migration-owned | Provider | Provider `privacy.dsar.notifications` is registered and documented. |
|
||||
| `ops` | `govoplan-ops` | No module migration | Reviewed no-store rationale | Projection-only module: Ops reads bounded platform and provider status; durable recovery evidence remains owned by Core and domain modules. |
|
||||
| `organizations` | `govoplan-organizations` | Migration-owned | Provider | Provider `privacy.dsar.organizations` is registered and documented. |
|
||||
| `parties` | `govoplan-parties` | Migration-owned | Provider | Provider `privacy.dsar.parties` is registered and documented. |
|
||||
| `payments` | `govoplan-payments` | Migration-owned | Provider | Provider `privacy.dsar.payments` is registered and documented. |
|
||||
| `permits` | `govoplan-permits` | No module migration | Reviewed no-store rationale | Contract-only module: permit applications, assessments, and decisions are not persisted; reassess before adding a migration-owned store. |
|
||||
| `policy` | `govoplan-policy` | Migration-owned | Provider | Provider `privacy.dsar.policy` is registered and documented. |
|
||||
| `poll` | `govoplan-poll` | Migration-owned | Provider | Provider `privacy.dsar.poll` is registered and documented. |
|
||||
| `portal` | `govoplan-portal` | No module migration | Reviewed no-store rationale | Projection-only module: Portal stores no applicant records; Services, Forms Runtime, Cases, and Postbox own and export authoritative subject data. |
|
||||
| `postbox` | `govoplan-postbox` | Migration-owned | Provider | Provider `privacy.dsar.postbox` is registered and documented. |
|
||||
| `procurement` | `govoplan-procurement` | No module migration | Reviewed no-store rationale | Contract-only module: procurement procedures, tenders, and awards are not persisted; reassess before adding a migration-owned store. |
|
||||
| `projects` | `govoplan-projects` | Migration-owned | Provider | Provider `privacy.dsar.projects` is registered and documented. |
|
||||
| `quick_access` | `govoplan-quick-access` | Migration-owned | Provider | Provider `privacy.dsar.quick_access` is registered and documented. |
|
||||
| `records` | `govoplan-records` | Migration-owned | Provider | Provider `privacy.dsar.records` is registered and documented. |
|
||||
| `reporting` | `govoplan-reporting` | Migration-owned | Provider | Provider `privacy.dsar.reporting` is registered and documented. |
|
||||
| `resources` | `govoplan-resources` | No module migration | Reviewed no-store rationale | Contract-only module: resource catalog and allocation persistence are not implemented; reassess before adding a migration-owned store. |
|
||||
| `rest` | `govoplan-rest` | No module migration | Reviewed no-store rationale | Transport-only module: REST binds explicitly published functions and owns no domain or subject-data store. |
|
||||
| `risk_compliance` | `govoplan-risk-compliance` | Migration-owned | Provider | Provider `privacy.dsar.risk_compliance` is registered and documented. |
|
||||
| `scheduling` | `govoplan-scheduling` | Migration-owned | Provider | Provider `privacy.dsar.scheduling` is registered and documented. |
|
||||
| `search` | `govoplan-search` | Migration-owned | Provider | Provider `privacy.dsar.search` is registered and documented. |
|
||||
| `services` | `govoplan-services` | Migration-owned | Provider | Provider `privacy.dsar.services` is registered and documented. |
|
||||
| `soap` | `govoplan-soap` | No module migration | Reviewed no-store rationale | Transport-only module: SOAP binds explicitly published operations and owns no domain or subject-data store. |
|
||||
| `tasks` | `govoplan-tasks` | Migration-owned | Provider | Provider `privacy.dsar.tasks` is registered and documented. |
|
||||
| `templates` | `govoplan-templates` | Migration-owned | Provider | Provider `privacy.dsar.templates` is registered and documented. |
|
||||
| `tenancy` | `govoplan-tenancy` | Migration-owned | Provider | Provider `privacy.dsar.tenancy` is registered and documented. |
|
||||
| `tickets` | `govoplan-tickets` | Migration-owned | Provider | Provider `privacy.dsar.tickets` is registered and documented. |
|
||||
| `transparency` | `govoplan-transparency` | No module migration | Reviewed no-store rationale | Contract-only module: requests, disclosure reviews, and publications are not persisted; reassess before adding a migration-owned store. |
|
||||
| `views` | `govoplan-views` | Migration-owned | Provider | Provider `privacy.dsar.views` is registered and documented. |
|
||||
| `voting` | `govoplan-voting` | Migration-owned | Provider | Provider `privacy.dsar.voting` is registered and documented. |
|
||||
| `wiki` | `govoplan-wiki` | Migration-owned | Provider | Provider `privacy.dsar.wiki` is registered and documented. |
|
||||
| `workflow` | `govoplan-workflow` | No module migration | Reviewed no-store rationale | Presentation-only module: Workflow edits and projects Workflow Engine state; Workflow Engine owns persistence and DSAR coverage. |
|
||||
| `workflow_engine` | `govoplan-workflow-engine` | Migration-owned | Provider | Provider `privacy.dsar.workflow_engine` is registered and documented. |
|
||||
| `xrechnung` | `govoplan-xrechnung` | No module migration | Reviewed no-store rationale | Stateless validation-contract module: XRechnung persists no invoice, report, diagnostic, or handoff; the invoking Files, Procurement, or Payments workflow remains the subject-data owner. Reassess before adding a validation store. |
|
||||
|
||||
Provider search, export minimization, retention, and erasure behavior remains
|
||||
documented and tested by each owning module. This matrix verifies adoption and
|
||||
ownership coverage; Core continues to test disabled providers, partial failure,
|
||||
retry, authorization evidence, and horizontally coordinated execution.
|
||||
+13
-6
@@ -1,19 +1,24 @@
|
||||
# GovOPlaN Interface Surface Inventory And Rollout
|
||||
|
||||
> **Pinned snapshot:** This inventory records the source-derived state reviewed
|
||||
> on 2026-08-03. It is retained as evidence, not maintained as the current
|
||||
> rollout ledger. Generate a new inventory and use Gitea issues for current
|
||||
> implementation state.
|
||||
|
||||
This is the initial evidence inventory for the product-wide interface pattern
|
||||
language. It records code contributions, not an assertion that every listed
|
||||
surface is complete, enabled in a deployment, usable, or compliant.
|
||||
|
||||
The applicable design contract is
|
||||
[`INTERFACE_PATTERN_LANGUAGE.md`](INTERFACE_PATTERN_LANGUAGE.md).
|
||||
[`INTERFACE_PATTERN_LANGUAGE.md`](../../architecture/INTERFACE_PATTERN_LANGUAGE.md).
|
||||
|
||||
## Snapshot And Method
|
||||
|
||||
The source-derived inventory command is documented in
|
||||
[`PLATFORM_CONTROL_PLANE.md`](PLATFORM_CONTROL_PLANE.md). It produces
|
||||
[`PLATFORM_CONTROL_PLANE.md`](../../architecture/PLATFORM_CONTROL_PLANE.md). It produces
|
||||
machine-readable field, label, translation, route, API-reference, and module
|
||||
manifest evidence. This hand-maintained document remains the reviewed product
|
||||
interpretation and rollout ledger; generated evidence does not replace it.
|
||||
manifest evidence. This hand-maintained document is the reviewed interpretation
|
||||
of that snapshot; generated evidence does not retroactively change it.
|
||||
|
||||
Snapshot refreshed: 2026-08-03.
|
||||
|
||||
@@ -104,7 +109,9 @@ semantics as authenticated navigation routes.
|
||||
| `/scheduling/public/:requestId/:token` | Scheduling | Public signed token | Public participation | Scheduling #8 complete in `c17cbda` |
|
||||
| `/search` | Search | `search:result:read` | Keyboard-first global/context overlay and full results fallback | Search pattern migration complete in [Search #4](https://git.add-ideas.de/GovOPlaN/govoplan-search/issues/4); durable evidence in `govoplan-search/docs/INTERFACE_PATTERN_MIGRATION.md` |
|
||||
| `/templates` | Templates | Template read/write/publish/render/admin | Governed library, immutable-revision editor, compatibility preview, and render evidence | Templates pattern migration complete in [Templates #5](https://git.add-ideas.de/GovOPlaN/govoplan-templates/issues/5), commit `72fafa2` |
|
||||
| `/tickets` | Tickets | `tickets:ticket:read` | Governed operational queue/detail workspace with distinct report, triage, assignment, resolution, comment, reference and removal boundaries | Tickets vertical slice and pattern migration complete in [Tickets #1](https://git.add-ideas.de/GovOPlaN/govoplan-tickets/issues/1), release `v0.1.20` |
|
||||
| `/voting` | Voting | `voting:ballot:read` | Governed ballot workspace | Voting pattern migration complete in [Voting #1](https://git.add-ideas.de/GovOPlaN/govoplan-voting/issues/1), commit `2625990` |
|
||||
| `/wiki` | Wiki | `wiki:page:read` | Governed space-tree/page workspace with draft editing, immutable revision comparison, publication, comments, typed references and archival | Native Wiki vertical slice and pattern migration implemented in [Wiki #1](https://git.add-ideas.de/GovOPlaN/govoplan-wiki/issues/1), release `v0.1.20` |
|
||||
| `/workflow` | Workflow | Definition read or instance admin | Native BPMN editor, governed revision actions and execution evidence | Workflow pattern migration complete in [Workflow #15](https://git.add-ideas.de/GovOPlaN/govoplan-workflow/issues/15); durable evidence in `govoplan-workflow/docs/INTERFACE_PATTERN_MIGRATION.md` |
|
||||
|
||||
## Final Module Closure Evidence
|
||||
@@ -318,8 +325,8 @@ The generated manifest snapshot reports no WebUI package for:
|
||||
`govoplan-learning`, `govoplan-mandates`, `govoplan-parties`,
|
||||
`govoplan-permits`, `govoplan-poll`, `govoplan-procurement`,
|
||||
`govoplan-records`, `govoplan-resources`, `govoplan-rest`,
|
||||
`govoplan-services`, `govoplan-soap`, `govoplan-tickets`,
|
||||
`govoplan-transparency`, `govoplan-wiki`, and `govoplan-workflow-engine`.
|
||||
`govoplan-services`, `govoplan-soap`, `govoplan-transparency`, and
|
||||
`govoplan-workflow-engine`.
|
||||
|
||||
Tenancy does provide composed administration surfaces despite having no direct
|
||||
route. This section is only negative package evidence; connector-only,
|
||||
@@ -353,6 +353,12 @@
|
||||
"description": "GovOPlaN Projects module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/records",
|
||||
"color": "0052cc",
|
||||
"description": "GovOPlaN Records and eAkte lifecycle behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/reporting",
|
||||
"color": "c2e0c6",
|
||||
@@ -365,6 +371,12 @@
|
||||
"description": "GovOPlaN Risk Compliance module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/quick-access",
|
||||
"color": "c5def5",
|
||||
"description": "GovOPlaN configurable task-local Quick Access behavior and integrations.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/search",
|
||||
"color": "bfdadc",
|
||||
|
||||
+2
-2
@@ -16,8 +16,8 @@ deployment/evidence boundary.
|
||||
|
||||
The machine-readable contracts are:
|
||||
|
||||
- [`backup-evidence.schema.json`](backup-evidence.schema.json);
|
||||
- [`backup-evidence-keyring.schema.json`](backup-evidence-keyring.schema.json).
|
||||
- [`backup-evidence.schema.json`](../backup-evidence.schema.json);
|
||||
- [`backup-evidence-keyring.schema.json`](../backup-evidence-keyring.schema.json).
|
||||
|
||||
One evidence document is bound to the installation id, deployment profile,
|
||||
topology subject, exact signed release manifest, image digests, and composition
|
||||
@@ -0,0 +1,120 @@
|
||||
# GovOPlaN Deployment Profiles
|
||||
|
||||
## Purpose
|
||||
|
||||
GovOPlaN distinguishes how code is executed, where it is placed, and how mature
|
||||
the target is. These are separate concerns:
|
||||
|
||||
- **execution basis:** editable source trees or an immutable signed release;
|
||||
- **topology:** local processes, one-host containers, or a multi-host
|
||||
orchestrator;
|
||||
- **component ownership:** installer-managed or externally supplied state and
|
||||
infrastructure services; and
|
||||
- **assurance state:** development, rehearsal/acceptance, or approved
|
||||
production.
|
||||
|
||||
PostgreSQL, Redis, object storage, mail and ingress choices are component
|
||||
bindings inside a profile. They do not create a new application topology by
|
||||
themselves.
|
||||
|
||||
## Canonical Profiles
|
||||
|
||||
| Profile | Entry point | Application execution | State services | Intended use | Explicit boundary |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Local source development | `tools/launch/launch-dev.sh` | Editable Uvicorn/Vite processes with reload | Local development bindings, optionally the shared PostgreSQL helper | Fast module and UI work | No production packaging, isolation, availability or capacity claim |
|
||||
| Split source integration | `tools/launch/launch-production-like-dev.sh` | Editable API, WebUI, worker and scheduler processes | Containerized PostgreSQL/Redis by default; environment bindings may point at developer-owned services | Queue, migration, Redis and split-role integration while retaining source reload | “Production-like” describes behavior, not immutable artifacts or a production security boundary |
|
||||
| Immutable single-host rehearsal | `govoplan-deploy init/apply --profile evaluation` | Signed API/WebUI images and generated Compose roles | Bounded managed components or explicit external bindings | Test the downloadable artifacts, installer, migrations, load balancer and component choices | All containers and managed services may share one host and failure domain; evaluation conveniences are not production controls |
|
||||
| Single-host production | `govoplan-deploy init/apply --profile self-hosted` | Signed API/WebUI images behind generated HAProxy and selected TLS ingress | Durable local/single-node managed services where accepted, or external services | Small and medium installations whose accepted availability boundary is one host | Multiple containers add capacity and rolling-process resilience, but do not survive host loss |
|
||||
| Multi-host Kubernetes production | `govoplan-deploy render-kubernetes` or the guarded K3s lab/acceptance workflow | Immutable API, WebUI and queue-specific worker Deployments across failure domains | External PostgreSQL, Redis and S3-compatible storage; external secret and ingress control | Institution-scale availability and horizontal application-tier capacity | Production claims require independent nodes, HA state services, load/capacity evidence and signed recovery evidence |
|
||||
|
||||
Docker Compose services are containers or replicas, not Kubernetes pods. The
|
||||
immutable single-host rehearsal is the appropriate Dockerized whole-product
|
||||
test when source reload is not required.
|
||||
|
||||
The K3s VM lab has two modes over the same Kubernetes profile:
|
||||
|
||||
- `rehearsal` may place VMs on one physical hypervisor and proves bounded
|
||||
orchestration behavior;
|
||||
- `acceptance` requires independently controlled worker failure domains and can
|
||||
contribute target evidence.
|
||||
|
||||
## Module composition and availability
|
||||
|
||||
Official immutable API and WebUI images carry the verified `full` package
|
||||
profile. This is package availability, not runtime activation and not a license
|
||||
or tenant entitlement. The signed distribution manifest records the complete
|
||||
package composition; the desired module graph selects which installed modules
|
||||
are active; tenant module policy applies unavailable/available/forced ceilings;
|
||||
and Views/Policy control group and user presentation.
|
||||
|
||||
Local and single-host profiles may use the supervised installer to download a
|
||||
signed catalog artifact into a private digest cache and mutate the local package
|
||||
environment during maintenance. A multi-host/shared-state profile must never
|
||||
change one replica in place. Its Admin install plan is a composition request:
|
||||
publish and roll out a new signed image whose package lock contains the target,
|
||||
then activate the module graph after all replicas report the same composition.
|
||||
|
||||
## Component Choices
|
||||
|
||||
The installer may manage a component where its bounded profile is appropriate,
|
||||
or consume an operator-provided service:
|
||||
|
||||
| Component | Managed boundary | External/BYO boundary |
|
||||
| --- | --- | --- |
|
||||
| PostgreSQL | Single-host Compose database | Stable primary-aware endpoint supplied by a PostgreSQL provider/operator |
|
||||
| Redis | Single-host persistent Redis | Tested HA Redis endpoint compatible with queues, throttling and coordination |
|
||||
| File/object storage | Durable local storage or single-node Garage | Shared, redundant S3-compatible storage |
|
||||
| Mail | Development GreenMail only | Institution/provider SMTP and IMAP services |
|
||||
| Ingress/TLS | Generated Caddy on one host | Existing reverse proxy or Kubernetes ingress and secret management |
|
||||
|
||||
Switching to an external component changes ownership and evidence requirements;
|
||||
it does not remove GovOPlaN's health, capacity, backup and recovery checks.
|
||||
|
||||
## Scaling Responsibilities
|
||||
|
||||
GovOPlaN scales application roles, while the infrastructure control plane owns
|
||||
machines and state-service replication:
|
||||
|
||||
| Concern | Scaling model | Owner |
|
||||
| --- | --- | --- |
|
||||
| API and WebUI | Increase replicas behind health-aware Services/Ingress | GovOPlaN deployment desired state, reconciled by Compose or Kubernetes |
|
||||
| Background work | Add queue-specific worker replicas and bounded concurrency | GovOPlaN deployment desired state and worker-pool configuration |
|
||||
| Scheduler, migrations and module lifecycle | Singleton execution protected by database leases/fencing | GovOPlaN; these roles are never scaled as unfenced active-active workers |
|
||||
| Kubernetes worker/control nodes | Add, drain, replace and upgrade machines; optionally use a cluster autoscaler | Kubernetes/platform operator, not the GovOPlaN application |
|
||||
| PostgreSQL | Replication, failover, backups, connection pooling and stable writer endpoint | Database operator/provider; GovOPlaN currently consumes the stable endpoint and does not route arbitrary reads to replicas |
|
||||
| Redis | Replication/failover, persistence, eviction and TLS/authentication | Redis operator/provider |
|
||||
| S3-compatible storage | Placement, replication, repair and capacity | Storage operator/provider |
|
||||
|
||||
Administrators should eventually be able to review and change permitted
|
||||
application replica and worker-pool desired state through the Ops surface.
|
||||
Creating physical machines, database replicas or storage members remains an
|
||||
orchestrator/provider action. GovOPlaN must observe their health and block unsafe
|
||||
changes rather than becoming a second infrastructure scheduler.
|
||||
|
||||
Every scale change must recalculate the database connection budget, preserve
|
||||
queue coverage, verify software/module-composition consistency and respect
|
||||
drain and fencing state.
|
||||
|
||||
## What Has Been Proven
|
||||
|
||||
The current implementation and the signed `v0.1.18` rehearsal prove that the
|
||||
application tier can run as stateless API, WebUI and worker replicas against
|
||||
logically shared state. Two Kubernetes worker VMs hosted API and WebUI replicas,
|
||||
and an API pod was replaced without an observed public-readiness failure.
|
||||
|
||||
This is not yet proof of general “large organization fit.” That claim also
|
||||
requires:
|
||||
|
||||
- representative concurrent-user, dataset, report and background-job load
|
||||
tests with latency and saturation budgets;
|
||||
- independent physical failure domains and real ingress/network behavior;
|
||||
- HA PostgreSQL, Redis and object storage with failover drills;
|
||||
- session, accepted-job and provider-effect continuity under node and service
|
||||
loss;
|
||||
- coordinated backup/isolated restore, measured RTO/RPO and semantic recovery;
|
||||
- observability, alerting, capacity forecasting and sustained soak evidence.
|
||||
|
||||
The profile therefore proves the architecture is horizontally deployable. A
|
||||
specific institution is production-fit only after its target topology and load
|
||||
envelope have produced the governed evidence described in
|
||||
`TARGET_MATURITY_EVIDENCE_RUNBOOK.md`.
|
||||
Executable
+300
@@ -0,0 +1,300 @@
|
||||
# Developer command suite
|
||||
|
||||
`./devkit` is the maintained entry point for repeated development work. It uses
|
||||
ordinary local programs, not an AI service. People, CI and coding agents use the
|
||||
same commands and results. It composes existing GovOPlaN checks and release
|
||||
services rather than defining parallel product rules.
|
||||
|
||||
## Start here
|
||||
|
||||
From the Meta checkout:
|
||||
|
||||
```sh
|
||||
./devkit commands
|
||||
./devkit doctor --repo core
|
||||
./devkit context --changed
|
||||
./devkit check --profile quick --repo campaign --dry-run
|
||||
./devkit coverage --profile ui --repo campaign
|
||||
./devkit check --profile quick --repo campaign
|
||||
./devkit latest
|
||||
./devkit review campaign
|
||||
```
|
||||
|
||||
Use `--help` on each command. `--json` (or `--format json`) returns structured
|
||||
results; the default summary intentionally omits full logs and source contents.
|
||||
Global options work before or after the command. `--workspace-root` is the parent
|
||||
directory of the registered repositories, not the Core checkout. Unknown
|
||||
repository filters fail rather than silently widening/narrowing scope.
|
||||
|
||||
The launcher prefers Meta's `.venv/bin/python`, or the explicit `PYTHON`
|
||||
executable, with `python3` as the no-venv fallback. Checks resolve `NODE` and
|
||||
`NPM` from explicit environment settings or `PATH`; no username-specific Node
|
||||
installation path is required. Doctor reports repairs but never installs
|
||||
packages, changes configuration, kills an occupied port or starts a server.
|
||||
|
||||
## Command contracts
|
||||
|
||||
| Command | Purpose | Effects |
|
||||
| --- | --- | --- |
|
||||
| `context [--changed] [--repo NAME]` | Offline Git state, instruction/documentation paths and review links | Reads only; upstream counts are not freshly fetched |
|
||||
| `doctor [--repo NAME] [--profile PROFILE]` | Interpreter, dependency, browser and test-resource preflight | Reads only; repair suggestions are not executed |
|
||||
| `check --profile PROFILE` | Registered checks with explanations, bounded logs and a receipt | Runs trusted tests/builds; `--dry-run` only plans |
|
||||
| `coverage --profile PROFILE [--repo NAME]` | Declared suite dispositions and explicit coverage limits | Reads only; not execution evidence |
|
||||
| `runs [--limit 10] [--before RUN]`, `latest` | Discover recent runs without looking up state-directory paths | Reads only; malformed latest evidence is not replaced with an older pass |
|
||||
| `status RUN`, `summary RUN` | Stage counts and failure log locations | Reads only |
|
||||
| `logs RUN --stage STAGE --tail 40 [--final-only]` | Provisional live output or a hash-verified final stage log | Reads only; provisional output never proves a pass |
|
||||
| `resume RUN` | Replan the previous check selection and reuse eligible successful stages | Runs remaining checks, creating a new receipt |
|
||||
| `recover RUN [--apply]` | Recover an abandoned check-run record after acquiring its OS lock | No test replay; apply changes only the local record |
|
||||
| `review MODULE` | Source inventory, principle revision, issue links, check plan and walkthrough checklist | Does not perform or complete a module review |
|
||||
| `docs audit [--repo NAME] [--changed]` | Owning manifest/help/translation checks and visible-label gaps | Local checks only; never translates or edits content |
|
||||
| `issues note …` | Preview and explicitly append deduplicated evidence | Remote comment creation only with `--apply` |
|
||||
| `release …` | Existing durable release planning/execution/recovery | Explicit `--apply`, request IDs and existing step confirmations |
|
||||
| `git …` | Frozen, explicit-path maintenance commit and exact branch push | Preview first; mutations require `--apply`; no stage-all, force or tags |
|
||||
|
||||
Git maintenance is a separate, selected-path workflow; see
|
||||
[Git maintenance](DEVKIT_MAINTENANCE.md). Issue notes and module bundles are
|
||||
documented in [Evidence and review commands](DEVKIT_EVIDENCE.md). The complete
|
||||
release command syntax, limitations and examples are in
|
||||
[Headless release operations](DEVKIT_RELEASE.md).
|
||||
|
||||
## Check profiles and their limits
|
||||
|
||||
- `quick`: existing manifest/interface invariants, import/help guards and selected
|
||||
module source/structure tests. It does not compile component suites or run the
|
||||
full browser matrix.
|
||||
- `ui`: quick/source contracts plus the shared Core component batch, compiled
|
||||
once in an isolated directory. This is not a full visual/usability review.
|
||||
- `backend`: existing manifest/interface checks and selected repository backend
|
||||
test suites. Shared backend-test state is serialized.
|
||||
- `full`: the canonical `tools/checks/check-focused.sh`, including optional-module
|
||||
build permutations and integrated browser conformance. Repository filters do
|
||||
not reduce this required cross-module completion gate.
|
||||
|
||||
`--changed` includes uncommitted work, commits ahead of the locally cached
|
||||
upstream and repositories with commits but no configured upstream.
|
||||
Missing/unreadable repositories remain visible as errors. For the
|
||||
GovOPlaN profiles, Core or Meta changes conservatively select every registered
|
||||
consumer; other changes expand through declared interface consumers. This is
|
||||
not complete semantic dependency analysis. Use the full gate for cross-module
|
||||
completion as required by `AGENTS.md`.
|
||||
|
||||
The module catalog reuses declared direct-Node tests and established structural
|
||||
script names. Arbitrary shell chains are not guessed or rewritten. Unsupported
|
||||
declared scripts remain visible with a reason; only exact known Core component
|
||||
aliases are covered by the shared component runner. Invalid package metadata is
|
||||
an error, not an empty test inventory.
|
||||
|
||||
`coverage` inventories declared suites as `planned`, `covered_elsewhere`,
|
||||
`excluded` or `unsupported`, naming their covering stage where applicable. The
|
||||
same inventory accompanies check dry runs and saved receipts. **Full means the
|
||||
canonical focused gate, not every test in every package.** Its inventory follows
|
||||
explicit canonical commands conservatively; it does not infer arbitrary nested
|
||||
shell commands or npm hooks. Add new deterministic checks to the owning
|
||||
package/canonical gate and the catalog's tests as appropriate. Coverage planning
|
||||
does not execute the scripts it inventories or assert that they passed. See
|
||||
[Coverage and validation details](DEVKIT_COVERAGE.md) for examples and boundaries.
|
||||
|
||||
Independent checks can run with `--jobs 1..8` (default 2). Shared resources use
|
||||
OS locks for the current user, including across runs with different evidence
|
||||
directories. Browser/port locks are shared across that user's workspaces. Full runs
|
||||
reserve the shared WebUI/backend/browser resources. Standalone Core component
|
||||
commands use isolated temporary build directories; `npm run test:components --
|
||||
page-layout documentation-help` compiles once for that batch. Managed temporary
|
||||
output is cleaned; no user worktree is reset or restored.
|
||||
|
||||
## Evidence, interruption and safe reuse
|
||||
|
||||
A check announces its run ID and saves a `preparing` receipt before source and
|
||||
environment fingerprinting. Progress on stderr shows elapsed time, stage counts
|
||||
and active stages; it also distinguishes `checking` from final snapshot
|
||||
verification (`finalizing`). `--json` keeps stdout as one final JSON result and
|
||||
emits structured progress events on stderr; `--quiet` suppresses these events.
|
||||
Use `latest`, `status RUN` and `logs RUN --stage STAGE` from another terminal
|
||||
while a check runs. Counts are stages, not a guessed percentage of test effort.
|
||||
|
||||
Live snapshots are private, bounded, explicitly provisional and not accepted for
|
||||
reuse or issue evidence. A completed stage log can be hash-verified before the
|
||||
whole run finishes, but only the final run snapshot verifies the overall result.
|
||||
`--final-only` rejects unfinished logs. Run history supports cursor pagination
|
||||
(up to 100 rows per page), bounds directory scanning, and never deletes evidence.
|
||||
An explicitly selected `--project` also filters run discovery by project file.
|
||||
|
||||
Default records are private below
|
||||
`$XDG_STATE_HOME/govoplan/devkit/workspace-<identity>/runs/<run>/` (fallback
|
||||
`~/.local/state`). `--state-dir` selects another base; workspace scoping still
|
||||
applies. Status changes are atomically persisted with restrictive file modes;
|
||||
symlinked evidence paths are rejected. A record contains source/environment/plan
|
||||
fingerprints, stage reasons, status, exit code, timing and log identities.
|
||||
|
||||
Stages distinguish pending, running, passed, failed, timed out, blocked, skipped,
|
||||
stale and interrupted. A skipped or unexecuted stage is never a pass. A source or
|
||||
environment change during verification marks the overall result **stale**, even
|
||||
when individual subprocesses returned zero. Such a result is not passing evidence.
|
||||
|
||||
`resume RUN` replans the current selection and compares each phase independently.
|
||||
Matching **verified checkpoints** are reused; changed/new phases and phases
|
||||
without a checkpoint run again. A checkpoint binds the exact command and working
|
||||
directory, declared repository inputs, transitive data-dependency identities,
|
||||
devkit implementation/schema, tool/dependency metadata and environment. Logs must
|
||||
retain their recorded hashes; a missing or changed cached log fails closed.
|
||||
The summary reports how many checkpoints were reused; JSON records `input_scope`,
|
||||
`cache_key`, `checkpoint_verified`, `reused_from` and the reuse/rerun reason.
|
||||
|
||||
Each checkpoint is saved while its resource locks remain held, only after exit
|
||||
zero, a verified final log, and matching before/after inputs and environment.
|
||||
A phase that changes its own inputs is stale, even if another phase later restores
|
||||
those bytes. Final verification rechecks all passed/reused phases against current
|
||||
inputs before the new aggregate can pass. An interrupted, failed or stale run may
|
||||
donate an independently verified matching checkpoint, but never becomes passing
|
||||
evidence itself. Old receipts remain readable: legacy monolithic logs and receipts
|
||||
without these checkpoints cannot be retroactively split or reused as verified phases.
|
||||
|
||||
After a hard crash, `recover RUN` shows the abandoned owner and guidance. Inspect the
|
||||
recorded commands and manually stop any surviving test/build processes before
|
||||
using `recover RUN --apply --confirm-processes-stopped`. The command verifies the
|
||||
run lock is free before marking the record interrupted. A free parent lock alone
|
||||
does **not** prove children stopped after a hard kill. Recovery does not replay
|
||||
processes or infer that a partially completed operation succeeded.
|
||||
|
||||
### Canonical phases and narrower inputs
|
||||
|
||||
The full gate has seven explicit phases: `preflight`, `tooling`, `backend`,
|
||||
`core-ui`, `module-builds`, `browser`, and `module-ui`. Devkit stage IDs are
|
||||
`focused.<phase>`. The authoritative Bash bodies remain in
|
||||
`tools/checks/check-focused.sh`; the bounded metadata in
|
||||
`tools/checks/focused-phases.json` names their order and resources. The direct
|
||||
shell command still runs every phase in the original fail-fast order. For inspection:
|
||||
|
||||
```sh
|
||||
tools/checks/check-focused.sh --list-phases
|
||||
./devkit check --profile full --jobs 1
|
||||
./devkit resume RUN_ID --jobs 1
|
||||
```
|
||||
|
||||
For example, a browser failure no longer forces successful backend/build phases
|
||||
to run again if their inputs still match. Standalone `--phase browser` is available
|
||||
for diagnostics, but is not a substitute for the complete gate or its receipt.
|
||||
|
||||
Scopes are whole repositories, not inferred file globs. A trusted check may declare
|
||||
`"inputs": {"repos": ["canonical-repository-name"]}`; no declaration means the whole
|
||||
registered workspace. Dry runs show that boundary. Native source-discovery checks
|
||||
retain broad scopes where cross-module dependencies cannot safely be narrowed.
|
||||
Changes outside an explicit scope do not invalidate it, but tool/environment
|
||||
changes remain conservatively global. Scope declarations are a correctness
|
||||
contract: include every source repository that the check reads, not only its cwd.
|
||||
Native discovery ownership/presence is rechecked at phase boundaries, so a newly
|
||||
appearing module or WebUI directory invalidates the in-flight environment identity.
|
||||
Unregistered native sibling sources disable reuse with an explicit coverage note;
|
||||
register them before relying on cached results. Ordinary generated files inside
|
||||
existing source directories do not change this directory-shape identity.
|
||||
|
||||
Every snapshot rereads repository presence, HEAD, index/flags and tracked plus
|
||||
non-ignored untracked membership, including assume-unchanged files. File hashes
|
||||
use a bounded **in-process-only** memo with file identity/ctime/mtime/size checks
|
||||
before and after an open file descriptor; no persistent mtime cache is trusted.
|
||||
In-repository regular-file symlink targets are included; escaping or directory
|
||||
symlinks fail closed. Ignored files, undeclared external data and service state
|
||||
are not source evidence and need explicit verification.
|
||||
|
||||
Environment probes stream bounded regular-file contents and reject concurrent
|
||||
replacement, growth and symlink retargeting. Stable venv executable and dependency
|
||||
directory symlinks remain supported without changing the executable path; absent
|
||||
optional metadata remains optional. Environment identities are not persistently
|
||||
cached and remain deliberately global.
|
||||
|
||||
These checkpoints cache **verification results, not output artifacts**. The full
|
||||
gate's phases were audited so later phases do not require an earlier phase's
|
||||
retained build output. A custom setup/build that produces files consumed later
|
||||
must declare `"reuse": "never"`; its consumers must use `deps`. Until output
|
||||
manifests/restoration exist, do not assume a cached successful build recreates
|
||||
deleted ignored artifacts.
|
||||
|
||||
Check output is drained without unbounded memory growth. Stored output retains
|
||||
the beginning and actual final tail within an 8 MiB bound per stage, with a small
|
||||
truncation marker and an omitted-byte count; output truncation does not change
|
||||
the subprocess exit result. `logs` exposes at most 200 lines and 16 KiB. Live
|
||||
snapshots retain at most 64 KiB after redaction. Incomplete live lines and cut
|
||||
retention-boundary lines are withheld to avoid exposing fragments of secrets;
|
||||
very long single-line output may therefore be absent from live views.
|
||||
Known environment credentials and common authorization patterns are
|
||||
redacted as display hygiene. **Never pass credentials as check arguments or print
|
||||
them from tests.** Redaction is not a general secret-classification guarantee.
|
||||
JSON command output is a redacted presentation; the receipt file at the reported
|
||||
path is the canonical local record. Cancellation and deadlines terminate the
|
||||
owned process group; a parent leaving running descendants does not pass.
|
||||
During source/environment fingerprinting, cancellation is observed between
|
||||
probes; the current bounded probe may finish first. Cancellation during final
|
||||
verification is recorded as interrupted, never as a passing run.
|
||||
Deliberately detached processes are outside that group: this is not a sandbox.
|
||||
|
||||
These local hash-bound records detect accidental changes; they are not signed
|
||||
attestations, a security certification, proof of complete test coverage, or
|
||||
permission to publish. External service state and undeclared dependencies still
|
||||
need explicit verification. No automated result closes a Gitea issue or marks a
|
||||
module reviewed. Detailed logs remain local unless deliberately shared. Old run
|
||||
directories are not automatically deleted; review retention before removing any
|
||||
evidence referenced by an issue.
|
||||
|
||||
## Reuse in another project
|
||||
|
||||
The runner, repository context, doctor and evidence primitives are usable with an
|
||||
explicit local JSON project manifest. The example at
|
||||
`tools/devkit/examples/project.json` registers ordinary Python tests and a Git
|
||||
whitespace check; the format is described by
|
||||
`tools/devkit/project.schema.json`.
|
||||
|
||||
```sh
|
||||
/path/to/govoplan/devkit --workspace-root /path/to/project \
|
||||
--project /path/to/project/devkit-project.json check --profile quick --dry-run
|
||||
```
|
||||
|
||||
Repository paths and check working directories must remain inside the selected
|
||||
workspace. Check arguments are arrays, not shell-evaluated strings. The supported
|
||||
tool placeholders are `{python}`, `{node}`, `{npm}` and `{workspace}`. Check
|
||||
dependencies form an acyclic graph; resources serialize incompatible tasks.
|
||||
`deps` declares actual data dependencies: consumers rerun whenever a dependency
|
||||
runs again. `after` declares only fail-fast execution ordering: a failed predecessor
|
||||
skips the follower, but a successfully rerun independent predecessor does not
|
||||
invalidate the follower's inputs. Both edge types are validated together for
|
||||
cycles, missing IDs and overlap. `reuse` is `verified` by default or `never` for
|
||||
setup/output-producing checks. Repository `inputs` names are canonical, unique,
|
||||
nonempty and validated even for unselected checks.
|
||||
The complete custom manifest is validated, including unselected checks/profiles:
|
||||
unknown fields, misspellings, invalid bounds/types and unresolved dependencies
|
||||
fail early. This prevents silently ignored resource or timeout declarations.
|
||||
|
||||
Portable `doctor` always requires its Python runtime, plus tools needed by the
|
||||
selected checks and their dependencies and tools explicitly configured in the
|
||||
manifest. Without `--profile`, all declared profiles are considered. Unused
|
||||
Node/npm are marked `not_required` and do not block Python-only projects. Declare
|
||||
indirect tool dependencies explicitly: arbitrary script contents are not
|
||||
analyzed. Native GovOPlaN retains its Python/Node/npm requirements.
|
||||
Only use project manifests and scripts you trust: running a registered test is
|
||||
ordinary code execution, not a sandbox. Platform-specific release and Docs
|
||||
commands reject generic project manifests; put another project's checks in its
|
||||
own profiles rather than pretending its release policy is GovOPlaN's.
|
||||
|
||||
The initial runtime targets POSIX environments with Python 3.11+ and OS advisory
|
||||
locks; it does not claim Windows support. No globally installed service or Codex
|
||||
plugin is needed. Keep this implementation versioned and reuse it; avoid copying
|
||||
diverging helper implementations into each project.
|
||||
|
||||
Devkit is not a complete build system: it does not infer semantic dependencies,
|
||||
restore output artifacts, sandbox checks, or share signed remote caches. Repository
|
||||
scopes intentionally stop short of file/glob narrowing; environment identities
|
||||
remain global. These are explicit boundaries, not claims of exhaustive coverage.
|
||||
|
||||
## Development and conformance
|
||||
|
||||
```sh
|
||||
./.venv/bin/python -m pytest -q tests/test_devkit_*.py
|
||||
./.venv/bin/python -m pytest -q tests/test_focused_phases.py
|
||||
node --test tests/test-devkit-display-labels.mjs
|
||||
./.venv/bin/python -m unittest tests.test_documentation_structure
|
||||
```
|
||||
|
||||
The focused gate includes the Python command-suite regression tests. Fixture
|
||||
tests use temporary repositories/processes and mocked issue/release transports;
|
||||
they do not commit user work, send mail, publish artifacts or change live issues.
|
||||
Documentation is part of each command change: update `--help`, this guide and the
|
||||
corresponding targeted contract tests together.
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
# Check configuration and suite coverage
|
||||
|
||||
`devkit coverage` explains what a selected plan intends to run. It does not run
|
||||
tests, start servers, or report successful verification:
|
||||
|
||||
```sh
|
||||
./devkit coverage --profile quick --repo campaign
|
||||
./devkit coverage --profile ui --repo portal --json
|
||||
./devkit coverage --profile full --json
|
||||
./devkit --project /path/to/project.json coverage --profile quick
|
||||
```
|
||||
|
||||
The inventory accounts for declared `test` and `test:*` scripts in each
|
||||
registered repository's root and `webui/package.json`, discovered UI structural
|
||||
checks, and explicitly configured project checks. It is a suite inventory, not
|
||||
an enumeration of every test function or recursive dependency. Unselected suites
|
||||
remain visible; every row has a disposition and reason:
|
||||
|
||||
| Disposition | Meaning |
|
||||
| --- | --- |
|
||||
| `planned` | A selected stage directly invokes the suite. |
|
||||
| `covered_elsewhere` | An exact equivalent invocation or an explicitly selected shared batch owns it; the covering stage is named. |
|
||||
| `excluded` | The suite is outside the actual selected plan. |
|
||||
| `unsupported` | Scoped discovery cannot safely interpret or locate the command; no command is guessed or executed. |
|
||||
|
||||
`full` means the existing canonical `check-focused.sh` gate. It does **not**
|
||||
mean every package script or every component suite. Coverage reads the current
|
||||
script's explicit npm/Node commands inside its seven registered marked phase
|
||||
bodies, without executing shell code or inferring arbitrary functions, nested
|
||||
scripts, npm hooks, branches or here-document contents. Each covered suite names
|
||||
the `focused.<phase>` that owns it. The
|
||||
current gate names four of the sixteen Core component suites: layout primitives,
|
||||
page layout, DataGrid actions/sizing, and Mail components. The other twelve stay
|
||||
explicitly excluded from that gate's component batch. `ui` runs the shared
|
||||
sixteen-suite batch once; `quick` does not compile component tests.
|
||||
|
||||
Only Core's exact known component aliases receive that shared-batch treatment.
|
||||
A different repository using the same filename, an unknown alias, or a compound
|
||||
command containing the runner is not silently credited or launched. Ordinary
|
||||
scoped source discovery accepts bounded direct `node`/`node --test` commands
|
||||
targeting regular package-owned `.mjs` files in `scripts` or `tests`; shell
|
||||
chains, extra flags, missing targets and symlinked targets are not rewritten.
|
||||
An explicit invocation already present in the canonical full gate remains part
|
||||
of that gate, even when the narrower discovery profile does not support it.
|
||||
|
||||
Coverage rows retain a command hash and, for supported commands, redacted argv.
|
||||
Unsupported shell bodies are not copied into the inventory. This is display
|
||||
hygiene, not permission to put secrets into project commands. Coverage is
|
||||
attached to check plans/receipts; it never turns an excluded suite into a pass.
|
||||
|
||||
## Portable-project validation
|
||||
|
||||
The runtime validates custom project metadata against the published
|
||||
[`project.schema.json`](../../tools/devkit/project.schema.json) using a small
|
||||
dependency-free validator. All checks and profiles are validated before
|
||||
selection, including checks the chosen profile does not execute. Unknown nested
|
||||
fields such as `resource` or `timout_seconds`, malformed types, oversized values,
|
||||
duplicate references, missing dependencies, unknown profiles and dependency
|
||||
cycles fail with controlled errors. Repository aliases and resolved paths must
|
||||
be unambiguous and confined to the workspace. Check `cwd` defaults to `.` when
|
||||
omitted; configured paths cannot escape the workspace.
|
||||
|
||||
Package JSON is bounded to 1 MiB and 512 scripts; duplicate JSON keys, excessive
|
||||
nesting and malformed script objects fail before planning. Command strings are
|
||||
bounded to 8,192 characters. This validates configuration, not the safety of its
|
||||
code: run only project manifests and test scripts you trust.
|
||||
|
||||
Checks may declare repository-scoped `inputs`, true data `deps`, order-only
|
||||
`after`, and `reuse: "never"` for setup/output-producing work. Unknown repositories,
|
||||
empty/duplicate scopes, overlapping edge types and cycles fail before execution.
|
||||
Missing inputs deliberately fall back to all registered repositories. See
|
||||
[checkpoint and input contracts](DEVKIT.md#canonical-phases-and-narrower-inputs).
|
||||
Executable
+171
@@ -0,0 +1,171 @@
|
||||
# Devkit review bundles and issue evidence
|
||||
|
||||
Gitea remains the canonical backlog and review state log. These commands assemble
|
||||
guidance and append evidence; they do not create a second progress tracker, close
|
||||
issues, rewrite issue bodies, tick checkboxes, or certify a module review.
|
||||
|
||||
## Prepare a module review
|
||||
|
||||
```sh
|
||||
./devkit review campaign
|
||||
./devkit review campaigns --json --output /tmp/campaign-review.json
|
||||
./devkit review campaign --evidence RUN_ID
|
||||
```
|
||||
|
||||
Repository names, repository aliases and scope IDs from the
|
||||
[issue discovery inventory](../project/ui-review-issue-inventory.json) are accepted.
|
||||
`review bundle campaign` is an equivalent spelling. A bundle contains the current
|
||||
source inventory and fingerprint, canonical module/central issue links, the
|
||||
Core principle revision and content hash, applicable **planned** check stages,
|
||||
and a manual checklist. No checks or application servers are started. The
|
||||
optional output file is a local snapshot, not an authoritative state record.
|
||||
|
||||
The filename inventory is a starting point, not proof that all runtime surfaces
|
||||
were found. It does not import application manifests or optional modules. Review
|
||||
routes, panes, dialogs, settings, widgets, public forms, contributed/headless
|
||||
interfaces, missing-module behavior, permissions, English/German and narrow/wide
|
||||
layouts manually. Missing source, no UI files or no planned checks is never an
|
||||
automatic pass or N/A. Record exceptions and remaining work in the module issue.
|
||||
|
||||
Apply [UI-01 and UI-02, and the other Core principles](../../../govoplan-core/docs/UI_DESIGN_PRINCIPLES.md):
|
||||
books belong beside meaningful visible text; normal screens display compact
|
||||
data, with intentional scoped dialog editing. A bulk editing mode needs a
|
||||
documented reason. Record new principles and their back-propagation to previously
|
||||
reviewed modules through the [central review program](../project/UI_REVIEW_PROGRAM.md).
|
||||
|
||||
An explicit portable `--project project.json` does not inherit GovOPlaN issue
|
||||
links or principles. Its optional configuration is:
|
||||
|
||||
```json
|
||||
{
|
||||
"review": {
|
||||
"issue_inventory": "meta/docs/review-issues.json",
|
||||
"principles": "core/docs/UI_DESIGN_PRINCIPLES.md"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Paths are relative to `--workspace-root` and cannot escape it or traverse
|
||||
symlinks. The inventory uses schema version 1, `issues[]` with `scope_id`,
|
||||
`repository`, `number`, `url`, and optional `name`/`kind`; an optional `epic`
|
||||
contains `repository`, `number`, `url`. Snapshot issue-state fields are not
|
||||
reported as current state. Custom checks come from the project's selected
|
||||
profile (`--profile ui` by default).
|
||||
|
||||
## Preview and append evidence
|
||||
|
||||
```sh
|
||||
./devkit issues note --root govoplan-campaign --issue 103 \
|
||||
--evidence RUN_ID --summary 'Targeted checks passed.' \
|
||||
--next 'Manual module review and remaining fixes are still required.' --json
|
||||
|
||||
./devkit issues note --root govoplan-campaign --issue 103 \
|
||||
--evidence RUN_ID --summary 'Targeted checks passed.' \
|
||||
--next 'Manual module review and remaining fixes are still required.' \
|
||||
--env-file /private/gitea.env --apply
|
||||
```
|
||||
|
||||
`--root` is resolved relative to the selected **workspace root**, not the shell's
|
||||
current directory. In the normal siblings workspace, use `--root govoplan-campaign`
|
||||
or an absolute checkout path; do not use a path escaping the workspace.
|
||||
|
||||
The default is a completely offline dry run; it does not load credentials or
|
||||
contact Gitea. `--apply` explicitly authorizes comment creation. Tokens are read
|
||||
from `GITEA_TOKEN` or the explicit `--env-file` (environment wins); there is no
|
||||
implicit `.env` search. The credential file is parsed, never sourced, and is not
|
||||
printed or uploaded. Git's selected remote (default `origin`) determines the
|
||||
exact target: ambient `GITEA_OWNER`, `GITEA_REPO` and `GITEA_URL` cannot retarget
|
||||
the request. Credential-bearing HTTP Git remote URLs are rejected.
|
||||
|
||||
Use repeatable `--summary`/`--next`, `--body-file` for Markdown, or `--note-file`
|
||||
with structured JSON `{ "summary": ["..."], "next": ["..."], "body": "..." }`.
|
||||
Do not put secrets in notes, paths, check arguments or logs. Display redaction is
|
||||
best-effort protection, not permission to include confidential data.
|
||||
|
||||
For a bounded multi-issue operation, replace `--root`/`--issue` with
|
||||
`--target-plan targets.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"targets": [
|
||||
{
|
||||
"root": "govoplan-campaign",
|
||||
"issue": 103,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/103"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Targets must be unique, inside the workspace and bound exactly to their Git
|
||||
remote/issue URLs, all on one exact Gitea base URL. Optional `issue_id` binds
|
||||
Gitea's immutable numeric ID as well. Every target, complete comment history,
|
||||
marker collision and local journal binding is checked before the first POST.
|
||||
Comments are then posted serially and read back against the exact issue. A
|
||||
closed issue stays closed; existing bodies, comments and checklists are untouched.
|
||||
|
||||
Each note has a stable hidden marker binding its issue, `--key` purpose
|
||||
(`verification` by default), and evidence run ID. Identical already-published
|
||||
notes are read back instead of reposted; changed content with the same marker
|
||||
is a collision, not an update. Use a distinct intentional `--key` for a separate
|
||||
follow-up. Without a receipt, the note's structured content determines identity.
|
||||
|
||||
A lost POST response is **not** retried automatically. The publisher scans all
|
||||
comment pages and reads back any matching comment. If it cannot confirm the
|
||||
result, it records `uncertain`, stops later posts, and exits nonzero. The next
|
||||
invocation reconciles again without replaying the POST. Only after inspecting
|
||||
the issue and local result, use `--apply --retry-uncertain` to permit one new
|
||||
attempt when the marker is still absent. Local journals and OS-released locks
|
||||
live in the workspace-scoped devkit state directory; do not delete them to bypass
|
||||
uncertainty. These are retry receipts, not issue state. Independent machines or
|
||||
different state directories cannot share a lock: coordinate publishers, because
|
||||
Gitea comment creation does not provide an atomic idempotency key.
|
||||
|
||||
## What a receipt does and does not prove
|
||||
|
||||
`--evidence RUN_ID` reads the workspace-scoped runner receipt and checks local
|
||||
integrity. `--evidence /path/to/receipt.json` accepts bounded validated external
|
||||
receipt metadata, explicitly labeled `external-unverified`. A self-consistent
|
||||
digest is not a signature or independent verification. Foreign-workspace,
|
||||
malformed and inconsistent successful receipts are rejected. Receipt-provided
|
||||
commands are never run; log files are not opened or uploaded, only referenced.
|
||||
Successful receipts must explicitly record a verified source snapshot; an
|
||||
external file's flag is still a reported claim, not an independent attestation.
|
||||
An aggregate containing a skipped stage cannot claim success. New versioned
|
||||
receipts additionally require independent verified checkpoints for passed phases.
|
||||
Failed/interrupted/stale aggregates remain labeled as such even when they contain
|
||||
reusable successful phases; reuse creates a new run and preserves the original.
|
||||
Stage coverage limitations are validated and retained in the note, including
|
||||
checks omitted by scoped profiles. Compact review output shows the first eight
|
||||
limitations and points to the complete list in its JSON bundle. A passing stage
|
||||
never implies that omitted compiler, browser, permutation or manual checks ran.
|
||||
|
||||
Documentation audit limitations are saved with their stage receipts, so later
|
||||
review bundles and issue notes retain the same scope as the original command.
|
||||
Managed inventory collectors use the explicit selected workspace throughout;
|
||||
an incomplete checkout never borrows sources or missing module imports from a
|
||||
fuller default workspace. Missing required Core/parser dependencies fail the
|
||||
audit. Legacy direct inventory commands without `--workspace-root` retain their
|
||||
discovery behavior; use the explicit option for reproducible evidence.
|
||||
|
||||
The note distinguishes `matches-current`, `historical-source-differs`,
|
||||
`different-project-not-compared` and unavailable comparisons. Matching source
|
||||
is a comparison at bundle/note preparation time, not a live test run or proof
|
||||
that a person completed review. If the source or environment has changed, run
|
||||
the appropriate checks again and keep historical evidence labeled honestly.
|
||||
|
||||
Versioned repository-input receipts compare the recorded repository scope, not a
|
||||
legacy whole-workspace hash. Partial scopes remain explicitly labeled in evidence
|
||||
notes. This comparison reads source only; receipt commands are never executed.
|
||||
Legacy receipts retain their original comparison format. A matching source scope
|
||||
does not prove that ignored build outputs, the environment or live services match.
|
||||
|
||||
Fixtures cover dry-run isolation, exact target bindings, capped/repeated
|
||||
pagination, collisions, append-only idempotency, uncertain POST reconciliation,
|
||||
secret-safe errors, receipt validation, portable projects and optional-module
|
||||
review boundaries:
|
||||
|
||||
```sh
|
||||
python3 -m pytest -q tests/test_devkit_issues.py tests/test_devkit_review.py
|
||||
```
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
# Selected-path Git maintenance
|
||||
|
||||
The optional `git` namespace of `tools/devkit/devkit.py` is a deliberately
|
||||
narrow maintenance workflow. It is not a replacement for normal Git, the
|
||||
release executor, required human review, or Gitea's canonical issue state.
|
||||
|
||||
From the Meta repository, preview and then save a plan for explicit files:
|
||||
|
||||
```sh
|
||||
python3 tools/devkit/devkit.py git plan --repo core --path webui/src/example.ts --message "Fix the reviewed example"
|
||||
python3 tools/devkit/devkit.py git plan --repo core --path webui/src/example.ts --message "Fix the reviewed example" --apply
|
||||
```
|
||||
|
||||
Use the saved `git-…` plan ID for the remaining commands. Each mutation needs
|
||||
its own `--apply`; an unqualified command only previews the operation:
|
||||
|
||||
```sh
|
||||
python3 tools/devkit/devkit.py git commit git-PLAN_ID
|
||||
python3 tools/devkit/devkit.py git commit git-PLAN_ID --apply
|
||||
python3 tools/devkit/devkit.py git push git-PLAN_ID
|
||||
python3 tools/devkit/devkit.py git push git-PLAN_ID --apply
|
||||
python3 tools/devkit/devkit.py git status git-PLAN_ID
|
||||
```
|
||||
|
||||
The immutable local plan binds the registered repository, HEAD, branch, origin
|
||||
fetch/push URL hashes, Git configuration, complete index identity, explicit
|
||||
selected file paths, working-tree hashes, expected Git blobs, and commit message.
|
||||
Raw remote URLs and file contents are not stored. A changed input blocks the
|
||||
operation; inspect it and create another plan instead of weakening the check.
|
||||
Receipts are private local records with integrity checks, not signed approval
|
||||
or a security attestation.
|
||||
|
||||
Commit captures each selected file once, verifies the captured bytes and Git
|
||||
blob against the plan, and builds an isolated index/tree from the frozen HEAD
|
||||
plus only those blobs. Git's normal identity rules create the commit object;
|
||||
an atomic branch compare-and-swap publishes it only if the planned parent is
|
||||
still current. Active hooks/signing remain refused rather than bypassed.
|
||||
An editor racing with this operation cannot substitute newer working-tree
|
||||
bytes: those edits remain uncommitted, and the helper never overwrites files.
|
||||
|
||||
The real index is protected by Git's standard index lock. Only selected entries
|
||||
are updated from a prepared private copy; unrelated staging and index flags are
|
||||
preserved. If the index changes independently, its new data is not overwritten
|
||||
and the receipt requires reconciliation. The resulting parent, message,
|
||||
changed paths, blobs/modes and unrelated index entries are verified.
|
||||
Git refs and index files are separately atomic, not one filesystem transaction.
|
||||
Independent writers that ignore the standard index lock can still change refs
|
||||
during publication. The helper rechecks the exact recorded candidate before
|
||||
index recovery and before recording success; it refuses an uncertain result
|
||||
instead of adopting a newer HEAD or claiming that competing work was its own.
|
||||
Directories, noncanonical paths, implicit globs, staging-all, amend,
|
||||
force-push and extra-tag publication are not supported. A selected file with
|
||||
different staged and working-tree changes is refused; decide explicitly which
|
||||
version to commit using normal Git first.
|
||||
|
||||
Push requires the recorded commit to remain current, an unchanged origin and
|
||||
one standard-transport push destination. It requests a normal, non-forced push
|
||||
of that commit to the recorded branch and checks the remote branch afterward.
|
||||
The push preview does not contact a remote. A normal push may run the remote's
|
||||
usual CI or server-side hooks; this is an external effect of the separately
|
||||
authorized push, not of planning or committing.
|
||||
|
||||
Local active hooks (including index-change hooks), filter/encoding attributes,
|
||||
signing, external fsmonitor/SSH configuration, Git environment overrides,
|
||||
in-progress merge/rebase/cherry-pick operations, submodules, symlinks and
|
||||
assume-unchanged/skip-worktree entries, split indexes and replacement/graft
|
||||
history are deliberately
|
||||
unsupported. The helper
|
||||
refuses these cases instead of disabling hooks, signatures or filters. An
|
||||
unused globally installed filter definition alone does not block maintenance;
|
||||
active attributes are checked across tracked files as well as selected files.
|
||||
Custom remote receive-pack/upload-pack/helper commands, recursive submodule
|
||||
pushes and partial-clone lazy fetch are also refused. Of inherited `GIT_*`
|
||||
variables, only `GIT_OPTIONAL_LOCKS`, `GIT_TERMINAL_PROMPT` and `GIT_PAGER` are
|
||||
allowed (every command explicitly passes `--no-pager`, so the pager is inert);
|
||||
namespace, identity, alternate-index/object-directory and unknown overrides
|
||||
are not silently removed. Git subprocesses have bounded input/output and
|
||||
deadlines; cancellation terminates their owned process group, including
|
||||
transport helpers. Network failures can still leave a remote effect uncertain,
|
||||
which is why receipts require explicit reconciliation rather than blind retry.
|
||||
|
||||
An interruption or uncertain failure is recorded before any retry. Use
|
||||
`git reconcile git-PLAN_ID` to preview, then add `--apply` to verify an
|
||||
already-existing result. Reconciliation never makes a commit or push. It may
|
||||
finish the selected real-index update after an interrupted commit publication,
|
||||
but only when the original index fingerprint still matches; independent staged
|
||||
work is preserved and requires normal Git resolution. For an
|
||||
uncertain push it may read the frozen remote, but only with `--apply`. If no
|
||||
planned commit is current, the helper reports that state and requires a fresh
|
||||
plan after inspection. A forcibly killed process may leave its private scratch
|
||||
index or an owned Git index lock; the helper never guesses that an existing
|
||||
lock is safe to delete. Blob/commit preparation can leave unreferenced Git
|
||||
objects for ordinary Git garbage collection. It never automatically resets,
|
||||
restores, deletes, or rolls back user work.
|
||||
|
||||
The maintenance tests use disposable local repositories and local bare remotes.
|
||||
They never commit or push the user's workspace repositories.
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
# Headless durable releases
|
||||
|
||||
The devkit `release` commands use the **same release application, request
|
||||
validation, run store and bounded executors** as the
|
||||
[release console](RELEASE_CONSOLE.md). They invoke its ASGI application inside
|
||||
the current process, with an ephemeral internal token. No listening socket,
|
||||
background server, browser session or externally supplied console URL is used.
|
||||
The development environment's FastAPI/HTTPX dependencies load only when a
|
||||
release command runs; ordinary devkit help does not import them.
|
||||
|
||||
This is a GovOPlaN-specific provider. Repository and origin authority still comes
|
||||
from the release service's registered catalog. `--project` is explicitly rejected
|
||||
for release commands; a portable project configuration must not silently override
|
||||
that authority. A different workspace path does not authorize arbitrary release
|
||||
repositories, remotes, commands, signing policies or source bindings.
|
||||
|
||||
The default compact output includes the overall status, selected repository
|
||||
states and versions, source-preflight readiness, bounded gate findings and the
|
||||
recommended next action. Run inspection also shows step-state counts and the
|
||||
first outstanding steps. These are projections of the service response, not new
|
||||
readiness checks. Use `--json` for the complete existing plan or receipt; compact
|
||||
output deliberately omits executor commands, arguments and source bindings.
|
||||
|
||||
## Commands and boundaries
|
||||
|
||||
| Command | Purpose | Effect boundary |
|
||||
| --- | --- | --- |
|
||||
| `release status` | Current release dashboard | Offline by default; explicit remote/catalog flags enable their checks |
|
||||
| `release plan` | Selective repository/version plan | Inspection only; does not create a run |
|
||||
| `release list` | Bounded run history, with `--limit` and `--cursor` | Existing workspace-scoped store |
|
||||
| `release show RUN_ID` | Exact verified run, steps, receipts and required confirmations | No release execution |
|
||||
| `release create` | Freeze a selected repository/version plan | Preview by default; `--apply` persists a run |
|
||||
| `release preview RUN_ID STEP_ID` | Frozen step and its current constraints | No step claim; catalog publication additionally uses the existing validated-candidate preview |
|
||||
| `release execute RUN_ID STEP_ID` | One available durable step | Preview by default; `--apply`, explicit request ID and the step's exact confirmation are required |
|
||||
| `release resume RUN_ID` | Mark persisted running attempts interrupted | Requires `--apply`; never assumes an effect succeeded |
|
||||
| `release retry RUN_ID STEP_ID` | Prepare an eligible failed/read-only-interrupted step | Requires `--apply`; does not execute it |
|
||||
| `release reconcile RUN_ID STEP_ID` | Record a proven uncertain-write outcome | Requires `--apply`, `--confirm RECONCILE` and an explicit outcome |
|
||||
|
||||
Create and transition commands require caller-chosen `--request-id` values.
|
||||
Retain the same ID for an uncertain replay of the **same** command; changing
|
||||
inputs under an existing ID fails closed. No command automatically retries a
|
||||
mutation. These commands perform one explicit transition, not an implicit
|
||||
"release everything" loop.
|
||||
|
||||
Read commands do not commit, tag, push, publish or apply database migrations.
|
||||
The existing private run store can initialize its lock directory or upgrade a
|
||||
legacy record while inspecting it; this does not advance release steps.
|
||||
`--include-migrations` requests the existing audits, never migration application.
|
||||
`--online`, `--remote-tags` and `--public-catalog` are explicit network-check
|
||||
choices. A generic step preview describes frozen intent; it is not a claim that
|
||||
live preflight, remote identity or release acceptance has passed.
|
||||
|
||||
## Example: review, freeze and execute one step
|
||||
|
||||
Run the devkit through the workspace `./devkit` entrypoint. These examples put
|
||||
global options before `release`:
|
||||
|
||||
```sh
|
||||
./devkit --workspace-root /path/to/workspace --format json release plan \
|
||||
--repo-version govoplan-files=0.1.9
|
||||
|
||||
./devkit --workspace-root /path/to/workspace release create \
|
||||
--repo-version govoplan-files=0.1.9 --request-id files-release-create-0001
|
||||
|
||||
./devkit --workspace-root /path/to/workspace release create \
|
||||
--repo-version govoplan-files=0.1.9 --request-id files-release-create-0001 --apply
|
||||
|
||||
./devkit --workspace-root /path/to/workspace release show RUN_ID
|
||||
./devkit --workspace-root /path/to/workspace release preview RUN_ID STEP_ID
|
||||
./devkit --workspace-root /path/to/workspace release execute RUN_ID STEP_ID \
|
||||
--request-id files-release-step-0001 --confirm REQUIRED_CONFIRMATION --apply
|
||||
```
|
||||
|
||||
`RUN_ID`, `STEP_ID` and `REQUIRED_CONFIRMATION` stand for the actual values
|
||||
returned by the frozen run. For read-only preflight/alignment/install-verification
|
||||
steps, omit `--confirm` when the service reports an empty confirmation. Metadata,
|
||||
commit, release-lock, source-tag, source-push, candidate-generation and catalog
|
||||
publication steps keep the existing `UPDATE`, `COMMIT`, `LOCK`, `TAG`, `PUBLISH`,
|
||||
`GENERATE` and `PUSH` confirmations respectively. Prerequisite ordering is enforced
|
||||
by the same store; a later step cannot be forced through this adapter.
|
||||
|
||||
Version selection supports repeated `--repo-version REPO=VERSION`, or repeated
|
||||
`--repo REPO` with `--target-version VERSION`. Every created run needs explicit
|
||||
versions for all selected repositories. Conflicting version assignments are
|
||||
rejected. Planning is selective; repositories are never silently selected merely
|
||||
because their worktrees are dirty.
|
||||
|
||||
## Candidate publication and recovery
|
||||
|
||||
Use the frozen run's `catalog:selective-generator` and
|
||||
`catalog:validate-sign-publish` steps, not the disabled legacy mutation endpoints.
|
||||
Generation accepts repeated `--signing-key KEY_ID=PRIVATE_KEY_FILE`; these are
|
||||
operator-owned key-file references, never inline key material. Signing arguments
|
||||
are not emitted in devkit JSON or persisted by the adapter. Publication consumes
|
||||
the candidate receipt created by that run, not an arbitrary candidate directory.
|
||||
The existing source/runtime trust, immutable tag/remote identity, private
|
||||
candidate checks and exact commit-delta checks remain authoritative.
|
||||
|
||||
After an interrupted write, inspect the run and the actual local/remote effect.
|
||||
Use `resume --apply` if an attempt was left running. Then use `reconcile` with
|
||||
`effect_absent`, `effect_succeeded` or `unresolved`, `--confirm RECONCILE`, a new
|
||||
reconciliation request ID and `--apply`. Successful reconciliation still performs
|
||||
the service's independent receipt checks. An unresolved effect must not be
|
||||
retried or papered over by generating a new request ID. A failed/read-only
|
||||
attempt may use `retry --apply` and a new explicit execute attempt only when
|
||||
the existing lifecycle makes that safe.
|
||||
|
||||
## State and limitations
|
||||
|
||||
Every successful response exposes `state_location` and `candidate_location`.
|
||||
Without `--state-dir`, the adapter uses the console's existing default private,
|
||||
workspace-fingerprinted state directory. With `--state-dir PATH`, it uses
|
||||
`PATH/release-console/workspace-<fingerprint>/release-runs`. Workspaces cannot
|
||||
read or resume one another's runs. Existing ownership, symlink, permission,
|
||||
retention and record-integrity checks are unchanged.
|
||||
|
||||
Release preparation can commit only the recognized, receipt-bound metadata
|
||||
changes its executors produced. This command does **not** stage arbitrary source
|
||||
changes, use `git add -A`, force-push, retarget tags or enable the disabled generic
|
||||
prepare/sync/push endpoints. Normal selected-path maintenance requires its own
|
||||
explicit reviewed-change workflow; it is not silently folded into release.
|
||||
@@ -0,0 +1,138 @@
|
||||
# Full registry candidates / Vollständige Registry-Kandidaten
|
||||
|
||||
## Operator workflow (EN)
|
||||
|
||||
The canonical `release-catalog.py full-registry` command takes `--package-set`,
|
||||
`--package-lock`, `--wheelhouse`, `--webui-packages`, `--output-dir`, and a
|
||||
configured `--catalog-signing-key`. Generate the package set with
|
||||
`generate-release-package-set.py --profile full` and download its exact artifacts
|
||||
with `resolve-package-artifacts.py`; do not substitute locally rebuilt wheels.
|
||||
The candidate compares the package set with the exact developer meta-package
|
||||
pins, checks archive bytes and package metadata against the lock, and synthesizes
|
||||
every entry from its immutable tagged manifest. Native package publication and
|
||||
its CI authority remain trusted: this verifies the published artifact identity,
|
||||
not independent reproducible-build equivalence to source.
|
||||
|
||||
Pass `--selected-repository` once for each newly released repository, including
|
||||
Core when it changes. These selected units must have clean, version-aligned
|
||||
named branches whose HEAD equals the annotated local and remote release tag.
|
||||
Other full-profile packages retain their exact older annotated tags; a later
|
||||
workflow-only commit on `main` does not relabel those package contents or force
|
||||
a version bump. Every source fetch/push endpoint must match the registered
|
||||
origin, and all entries bind their source commit and annotated tag object.
|
||||
Git replacement objects, caller Git configuration, and executable-path
|
||||
redirection cannot substitute another tagged manifest tree.
|
||||
|
||||
The fixed existing website catalog and keyring are authenticated before signing.
|
||||
An older catalog without a signed keyring hash can be migrated only through this
|
||||
complete rebuild, only when its signature verifies and its entire keyring
|
||||
exactly matches the configured known signers. No old entries or artifact hashes
|
||||
are reused. The new catalog signs the exact unchanged website keyring hash;
|
||||
key rotation remains a separate reviewed operation. Selective candidates still
|
||||
reject unpinned base keyrings. New candidate directories are private and
|
||||
exclusive: a retry must choose a new directory, not overwrite a reviewed one.
|
||||
|
||||
Run these commands on the trusted host, with an operator-private source workspace
|
||||
and artifact directory. `RELEASE_PYTHON` must select its private environment and
|
||||
`RELEASE_NPM` an absolute npm executable with a trusted sibling Node 22 binary.
|
||||
In Flatpak, execute host commands through `flatpak-spawn --host`; sandbox and
|
||||
host UID mappings are not interchangeable. Do not weaken trust gates or change
|
||||
system-wide permissions.
|
||||
|
||||
```sh
|
||||
# These paths identify previously prepared private operator resources.
|
||||
RELEASE_WORKSPACE=/path/to/private/workspace
|
||||
RELEASE_PYTHON="$RELEASE_WORKSPACE/govoplan/.host-venv/bin/python"
|
||||
RELEASE_NPM=/path/to/private/node22/bin/npm
|
||||
ARTIFACT_ROOT=/path/to/private/artifacts
|
||||
RELEASE_CANDIDATE=/path/to/private/new-candidate
|
||||
RELEASE_VERSION=0.1.45
|
||||
RELEASE_TOOLS="$RELEASE_WORKSPACE/govoplan/tools/release"
|
||||
|
||||
umask 077
|
||||
"$RELEASE_PYTHON" "$RELEASE_TOOLS/generate-release-package-set.py" \
|
||||
--version "$RELEASE_VERSION" --profile full \
|
||||
--workspace "$RELEASE_WORKSPACE" --output "$ARTIFACT_ROOT/packages.json"
|
||||
PATH="$(dirname "$RELEASE_NPM"):/usr/bin:/bin" \
|
||||
"$RELEASE_PYTHON" "$RELEASE_TOOLS/resolve-package-artifacts.py" \
|
||||
--package-set "$ARTIFACT_ROOT/packages.json" \
|
||||
--wheelhouse "$ARTIFACT_ROOT/wheels" \
|
||||
--webui-packages "$ARTIFACT_ROOT/webui" \
|
||||
--lock-output "$ARTIFACT_ROOT/artifacts.lock.json" \
|
||||
--python "$RELEASE_PYTHON" --npm "$RELEASE_NPM"
|
||||
|
||||
# Repeat --selected-repository for EVERY newly released unit, not just Core.
|
||||
"$RELEASE_PYTHON" "$RELEASE_TOOLS/release-catalog.py" full-registry \
|
||||
--workspace-root "$RELEASE_WORKSPACE" \
|
||||
--package-set "$ARTIFACT_ROOT/packages.json" \
|
||||
--package-lock "$ARTIFACT_ROOT/artifacts.lock.json" \
|
||||
--wheelhouse "$ARTIFACT_ROOT/wheels" --webui-packages "$ARTIFACT_ROOT/webui" \
|
||||
--output-dir "$RELEASE_CANDIDATE" --selected-repository govoplan-core \
|
||||
--catalog-signing-key known-key=/path/to/private/known-key.pem --json
|
||||
|
||||
"$RELEASE_PYTHON" "$RELEASE_TOOLS/release-catalog.py" publish-candidate \
|
||||
--workspace-root "$RELEASE_WORKSPACE" --candidate-dir "$RELEASE_CANDIDATE" \
|
||||
--channel stable --npm "$RELEASE_NPM" --build-web \
|
||||
--commit --tag --push --tag-name "catalog-v$RELEASE_VERSION" --json
|
||||
```
|
||||
|
||||
The last command is a strict non-mutating preview because `--apply` is absent.
|
||||
Review its output, then repeat it with `--apply` to publish. The website's locked
|
||||
build dependencies must already be installed before `--build-web`. The publisher
|
||||
sanitizes the build and Git environments and pushes the verified immutable
|
||||
website commit/tag. Source tag publication and registry package availability
|
||||
must be complete before candidate generation.
|
||||
|
||||
Source tags, registry packages, and a signed module catalog do not imply that a
|
||||
new runtime distribution exists. While runtime images are held, leave the Meta
|
||||
Gitea runtime Release held too: a normal source-only Release can replace Gitea's
|
||||
`releases/latest` discovery result despite having no deployment assets. The
|
||||
deployer still requires an explicit signed manifest, digest, and trusted
|
||||
keyring; it does not deploy a tag or module catalog directly.
|
||||
|
||||
## Betriebsablauf (DE)
|
||||
|
||||
`release-catalog.py full-registry` übernimmt den vollständigen Paketbestand,
|
||||
die Registry-Sperrdatei, das Wheel-Verzeichnis, die WebUI-Archive und den
|
||||
konfigurierten Signaturschlüssel. Zuerst mit
|
||||
`generate-release-package-set.py --profile full` die exakten Meta-Paketversionen
|
||||
ermitteln und mit `resolve-package-artifacts.py` die veröffentlichten Artefakte
|
||||
herunterladen. Lokal neu gebaute Wheels sind kein Ersatz. Der Kandidat prüft
|
||||
Paketidentitäten, Dateigrößen und Hashes und erzeugt alle Einträge aus den
|
||||
unveränderlichen getaggten Manifesten. Die Registry und ihre veröffentlichende
|
||||
CI bleiben eine Vertrauensgrundlage; dies ist kein unabhängiger Nachweis eines
|
||||
reproduzierbaren Builds aus dem Quellcode.
|
||||
|
||||
Jedes neu veröffentlichte Repository wird mit `--selected-repository`
|
||||
angegeben. Nur diese Auswahl muss mit dem sauberen, versionsgleichen HEAD eines
|
||||
benannten Branches und dem annotierten lokalen und entfernten Tag übereinstimmen.
|
||||
Unveränderte Pakete behalten ihren ursprünglichen Tag, auch wenn auf `main`
|
||||
bereits eine spätere Workflow-Korrektur liegt. Alle Quelladressen müssen dem
|
||||
registrierten Ursprung entsprechen; Commit und annotiertes Tag-Objekt werden
|
||||
für jeden Eintrag gebunden. Git-Ersetzungsobjekte oder fremde Git-Konfiguration
|
||||
können dabei keinen anderen Manifestbaum unterschieben.
|
||||
|
||||
Vor dem Signieren werden der bestehende Website-Katalog und sein Schlüsselbund
|
||||
geprüft. Ein alter Katalog ohne signierten Schlüsselbund-Hash darf ausschließlich
|
||||
durch diesen vollständigen Neuaufbau migriert werden: Seine Signatur muss gültig
|
||||
sein und der gesamte Schlüsselbund exakt den konfigurierten bekannten Signierern
|
||||
entsprechen. Alte Einträge oder Artefakt-Hashes werden nicht übernommen. Der neue
|
||||
Katalog bindet den unveränderten Schlüsselbund-Hash; ein Schlüsselwechsel bleibt
|
||||
ein eigener geprüfter Vorgang. Selektive Kandidaten verlangen weiterhin einen
|
||||
bereits gebundenen Schlüsselbund. Kandidaten werden nur in neuen privaten
|
||||
Verzeichnissen erzeugt und niemals überschrieben.
|
||||
|
||||
Das obige Befehlsbeispiel wird auf dem vertrauenswürdigen Host ausgeführt. Dafür
|
||||
die private Python-Umgebung und einen absoluten `--npm`-Pfad zu Node 22 verwenden;
|
||||
unter Flatpak die Host-Werkzeuge über `flatpak-spawn --host` aufrufen. Die
|
||||
gesperrten Website-Build-Abhängigkeiten vorher installieren. Keine
|
||||
Vertrauensprüfung umgehen und keine globalen Rechte ändern. Die Artefaktordner
|
||||
müssen privat und bei der Auflösung leer sein. Vor der Kandidatenerzeugung
|
||||
müssen Quell-Tags und Registry-Pakete vollständig veröffentlicht sein.
|
||||
|
||||
Die Veröffentlichung zunächst mit `publish-candidate --commit --tag --push
|
||||
--build-web` ohne `--apply` prüfen und erst nach Prüfung mit `--apply` ausführen.
|
||||
Solange Laufzeit-Images zurückgestellt sind, bleibt auch das Meta-Gitea-Runtime-
|
||||
Release zurückgestellt: Ein reines Quellcode-Release könnte sonst als neuestes
|
||||
Release erscheinen. Eine Installation benötigt weiterhin ein signiertes
|
||||
Laufzeitmanifest, dessen Digest und einen explizit vertrauenswürdigen Schlüsselbund.
|
||||
+83
-5
@@ -8,8 +8,12 @@ receives a working base system. Re-running the same tool repairs or
|
||||
reconfigures that installation instead of creating unrelated state.
|
||||
|
||||
The canonical product journey remains
|
||||
[System Administrator Lifecycle User Story](SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md).
|
||||
[System Administrator Lifecycle User Story](../strategy/SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md).
|
||||
This document defines the deployer boundary and the first executable slice.
|
||||
The execution, topology, component-ownership and assurance modes are defined
|
||||
canonically in [Deployment Profiles](DEPLOYMENT_PROFILES.md). In particular,
|
||||
the editable production-like developer launcher is distinct from both an
|
||||
immutable Compose rehearsal and a supported one-host production deployment.
|
||||
|
||||
## First Executable Slice
|
||||
|
||||
@@ -95,6 +99,8 @@ The private installation directory contains:
|
||||
| `existing-proxy.json` | Exact upstream, trusted-source, header, and health contract for an operator-owned proxy |
|
||||
| `plan.json` | Latest desired-state diff and readiness findings |
|
||||
| `receipt.json` | Last successfully applied immutable identities |
|
||||
| `infrastructure-capabilities.json` | Deterministic non-secret capability states, endpoint metadata, secret references, consumers, and resumable post-install tasks |
|
||||
| `infrastructure-dependency-inventory.json` | Owner-only, short-lived Ops evidence of actual module-owned configuration and data that depend on infrastructure capabilities |
|
||||
| `distribution-manifest.json` | Canonical signed runtime/image selection adopted by the installer |
|
||||
| `distribution-keyring.json` | Explicitly installed public trust anchor for runtime releases |
|
||||
| `backup-evidence.json` | Signed provider-neutral coordinated backup and isolated-restore receipt |
|
||||
@@ -106,7 +112,27 @@ The private installation directory contains:
|
||||
| `.deployment.lock` | Same-host operation exclusion |
|
||||
|
||||
The specification contract is
|
||||
[`installation-spec.schema.json`](installation-spec.schema.json).
|
||||
[`installation-spec.schema.json`](../installation-spec.schema.json).
|
||||
|
||||
The API, workers, scheduler, and Ops read the capability receipt through the
|
||||
same bounded Core validator. Configuration-package providers receive that typed
|
||||
receipt in preflight context. Mail uses `mail.smtp` to offer an idempotent SMTP
|
||||
profile plan and accepts only an existing credential-envelope reference; Files
|
||||
uses `files.storage` to prove that the deployment-owned local/S3 runtime binding
|
||||
already matches. Files deliberately blocks drift instead of rewriting process
|
||||
environment or initiating an implicit object migration. Invalid receipts fail
|
||||
closed, while a deployment without a mounted receipt continues to run but
|
||||
cannot apply receipt-bound configuration fragments.
|
||||
|
||||
Enabled modules may also register a Core infrastructure-dependency provider.
|
||||
The authorized Ops endpoint aggregates those providers without importing their
|
||||
tables. Mail reports persisted SMTP endpoints, credential-binding counts and
|
||||
legacy profiles; Files reports its runtime storage binding plus persisted blob
|
||||
counts and byte totals grouped by backend. Ops reports the active PostgreSQL,
|
||||
Redis coordination, ingress, and load-balancing runtime bindings. Provider output contains stable
|
||||
references, bounded numeric metrics and required migration actions, never
|
||||
credentials, endpoint secrets, tenant identifiers or file keys. A provider
|
||||
failure makes the entire inventory incomplete.
|
||||
|
||||
Build the same dependency-free tool as one downloadable artifact:
|
||||
|
||||
@@ -195,9 +221,9 @@ it can initialize a new volume; the actual HAProxy process retains the image's
|
||||
non-root identity and runs read-only with all capabilities dropped.
|
||||
|
||||
The manifest contract is
|
||||
[`runtime-distribution-manifest.schema.json`](runtime-distribution-manifest.schema.json),
|
||||
[`runtime-distribution-manifest.schema.json`](../runtime-distribution-manifest.schema.json),
|
||||
and its separately distributed trust-anchor contract is
|
||||
[`runtime-distribution-keyring.schema.json`](runtime-distribution-keyring.schema.json).
|
||||
[`runtime-distribution-keyring.schema.json`](../runtime-distribution-keyring.schema.json).
|
||||
Publication is immutable: an existing Gitea release asset must have the same
|
||||
size and SHA-256 digest or publication fails.
|
||||
|
||||
@@ -335,6 +361,15 @@ and WebUI API proxy traffic across API replicas. The WebUI and API services do
|
||||
not publish host ports. HAProxy has no Docker socket and discovers only the
|
||||
bounded replica slots rendered into `load-balancer.cfg`.
|
||||
|
||||
New installation specifications default to HAProxy `3.2.23-alpine`, pinned to
|
||||
registry index `sha256:6343ce34a132a5dceaa24767d739df2bd519f8f7c1079ae39e4821334e8eb42e`.
|
||||
This patch remains on HAProxy 3.2 LTS and Alpine 3.24.1. Loading an existing
|
||||
specification preserves its explicit image; it does not perform an upgrade.
|
||||
The [runtime remediation evidence](../security/RUNTIME_IMAGE_REMEDIATION_2026-09-08.md)
|
||||
records both architecture scans and the pending binary/configuration, runtime,
|
||||
inventory and final-image checks. The source default is not a release approval:
|
||||
runtime publication remains held in Meta #52.
|
||||
|
||||
Replica counts are desired state:
|
||||
|
||||
```sh
|
||||
@@ -404,16 +439,30 @@ the supported topology and promotion path.
|
||||
## Reconfiguration Semantics
|
||||
|
||||
`installation.json` is desired state. `receipt.json` is the last successfully
|
||||
applied state. `plan` compares their canonical hashes and service sets.
|
||||
applied state. `plan` compares their canonical hashes, service sets, and
|
||||
infrastructure capability projections.
|
||||
|
||||
- Adding a managed component creates its service and persistent volume.
|
||||
- Removing a component removes its service container on apply.
|
||||
- Reconfiguring, replacing or removing a capability adds a review action that
|
||||
names the prior and desired state/source, declared consumers, actual
|
||||
provider-reported dependency records and each required migration action.
|
||||
- The deployer blocks that change when provider inventory is missing,
|
||||
incomplete, more than five minutes old, from another installation, timestamped
|
||||
in the future, or does not cover every impacted capability. It never treats
|
||||
installer-declared consumers as proof that persisted module state is absent.
|
||||
- The inventory reports impact; it does not migrate or delete module-owned
|
||||
configuration or data. Complete the reported preparation and collect again
|
||||
immediately before apply.
|
||||
- Volumes are retained by default; deleting data requires a separate,
|
||||
deliberately destructive workflow.
|
||||
- Existing generated credentials are retained unless an explicit future rotate
|
||||
operation is requested.
|
||||
- Private configuration changes are represented by a keyed fingerprint in the
|
||||
plan and receipt; plaintext values are never copied there.
|
||||
- Capability documents contain sanitized scheme/host/port metadata and stable
|
||||
`env:` references only. Credential values and secret-bearing URLs remain in
|
||||
`secrets.env` or module-owned credential envelopes.
|
||||
- Managed-to-external transitions require the new endpoint in the same
|
||||
operation.
|
||||
- Migrations run as a one-shot service before API/worker replacement.
|
||||
@@ -426,6 +475,35 @@ applied state. `plan` compares their canonical hashes and service sets.
|
||||
- Health must recover before a new receipt and applied-state snapshot are
|
||||
committed.
|
||||
|
||||
Compose mounts the capability document read-only into API and worker runtime
|
||||
containers. The Kubernetes export projects the same document through a
|
||||
dedicated ConfigMap and read-only file mount. Ops validates the bounded schema
|
||||
before displaying configured, externally supplied, available-unconfigured, or
|
||||
unavailable states and any pending post-install tasks.
|
||||
|
||||
Collect current dependency evidence with an API key whose principal has one of
|
||||
the Ops read scopes:
|
||||
|
||||
```sh
|
||||
export GOVOPLAN_OPS_API_KEY='<short-lived operator API key>'
|
||||
python3 govoplan-deploy.pyz collect-infrastructure-inventory \
|
||||
--directory /srv/govoplan/example
|
||||
python3 govoplan-deploy.pyz doctor \
|
||||
--directory /srv/govoplan/example
|
||||
python3 govoplan-deploy.pyz apply \
|
||||
--directory /srv/govoplan/example
|
||||
unset GOVOPLAN_OPS_API_KEY
|
||||
```
|
||||
|
||||
The command defaults to
|
||||
`<public-url>/api/v1/ops/infrastructure/dependencies`; `--ops-url` may select an
|
||||
explicit HTTPS endpoint (plain HTTP is accepted only on loopback). `apply`
|
||||
refreshes the inventory automatically when `GOVOPLAN_OPS_API_KEY` is present.
|
||||
Otherwise an already collected, current inventory may be used. The API key is
|
||||
sent only as `X-API-Key`, is never written to the bundle, and the inventory file
|
||||
is owner-readable only. Because it contains operational references and counts,
|
||||
handle it as private evidence even though it contains no secret material.
|
||||
|
||||
Every apply operation is journalled before image pulls or runtime mutation. A
|
||||
failure before migration may restore a verified previous bundle. Once migration
|
||||
starts, recovery is forward-only unless an independently verified database
|
||||
@@ -0,0 +1,91 @@
|
||||
# Integrity and performance source release — September 2026
|
||||
|
||||
Coordinated tracking: [Core #298](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/298).
|
||||
This source publication advances only the affected packages; it is not a signed
|
||||
catalog publication, runtime-image release, or remote production rollout.
|
||||
|
||||
## Package set
|
||||
|
||||
| Package | Version |
|
||||
| --- | --- |
|
||||
| Core / developer meta-package | 0.1.46 |
|
||||
| Addresses | 0.1.23 |
|
||||
| Calendar | 0.1.24 |
|
||||
| Campaign | 0.1.29 |
|
||||
| Cases | 0.1.25 |
|
||||
| Committee | 0.1.22 |
|
||||
| Connectors | 0.1.27 |
|
||||
| Dataflow | 0.1.25 |
|
||||
| Datasources | 0.1.26 |
|
||||
| Files | 0.1.27 |
|
||||
| Forms Runtime | 0.1.22 |
|
||||
| IDM | 0.1.26 |
|
||||
| Mail | 0.1.28 |
|
||||
| Reporting | 0.1.22 |
|
||||
| Tickets | 0.1.23 |
|
||||
|
||||
Consumers of new Core helpers require Core 0.1.46 or later. Dataflow and
|
||||
Datasources also require the matching Core WebUI contract. The release manifests,
|
||||
immutable Git lock, and developer package describe this coordinated composition.
|
||||
Unchanged packages retain their independent versions.
|
||||
|
||||
## Database and data integrity
|
||||
|
||||
The reviewed additive heads are Connectors `d2a4c6e8f0b1`, Datasources
|
||||
`e2b8d4a0f6c3`, Files `a2b3c4d5e701`, and Mail `b5d6e7f8091a`.
|
||||
Back up the target deployment and rehearse its normal upgrade before rollout.
|
||||
Never downgrade away retained CSV originals without a separate recovery plan.
|
||||
Files preserves historical duplicate copies; Mail leaves legacy maildrop identity
|
||||
unset rather than guessing an account. A schema upgrade does not authorize POP3
|
||||
retrieval/reconciliation, provider deletion, or message resending.
|
||||
|
||||
Development startup may automatically apply pending migrations after a watched
|
||||
source change. Therefore, inspect actual Alembic heads and schema before assuming
|
||||
a live development database is still at its pre-change state. A backup taken
|
||||
after such an upgrade is a current-state recovery copy, not a pre-upgrade backup.
|
||||
|
||||
On 8 September, the local PostgreSQL development database already contained all
|
||||
four heads. Schema and constraint inspection passed; recomputing the Files
|
||||
identity backfill checked 7,385 rows with zero mismatches. A private current-state
|
||||
database backup was created. Per-instance backup paths and row contents are not
|
||||
published in this repository. The implementation's initial receipt claiming no
|
||||
live migration occurred was incorrect: watched development-server restarts had
|
||||
applied the migration files automatically. Restoring the backup into an isolated
|
||||
PostgreSQL 16 cluster and running the normal upgrade preserved all 281 public
|
||||
tables, 142,287 rows, and migration heads exactly. Both Datasources PostgreSQL
|
||||
concurrency regressions passed. The test-only cluster was then stopped.
|
||||
|
||||
Release preparation additionally corrected Alembic's ConfigParser handling of
|
||||
percent-escaped connection URLs, preserving the exact database URL. The static
|
||||
migration auditor now recognizes the existing reviewed development-wrapper
|
||||
aliases and peer filenames without executing migration code; historical
|
||||
migrations are unchanged.
|
||||
|
||||
## Verification and boundaries
|
||||
|
||||
The implementation passed the required focused workspace gate, including 63
|
||||
frontend build configurations and 230 browser conformance tests. Owning-module
|
||||
regressions cover exact import/rollback evidence, authorization-before-pagination,
|
||||
bounded recurrence and response processing, collision-safe attachment naming,
|
||||
and stale asynchronous UI completion. English/German feature documentation stays
|
||||
in each owning module; Core documents shared integrity contracts.
|
||||
|
||||
Mock-provider tests and local work-count measurements do not establish production
|
||||
throughput or provider race behavior. Real S3/SMB/Seafile and POP3 acceptance,
|
||||
representative load testing, and deployment authentication checks remain separate
|
||||
operational validation. Source tags trigger package workflows; a pushed source
|
||||
tag alone does not prove a registry artifact or signed catalog is published.
|
||||
|
||||
## Deutsch
|
||||
|
||||
Dieses koordinierte Quellrelease veröffentlicht nur die betroffenen Pakete.
|
||||
Produktions-Images, signierter Modulkatalog und entfernte Produktivinstanzen
|
||||
werden dadurch nicht ausgerollt. Die vier Migrationen bewahren bestehende Daten;
|
||||
historische POP3-Zuordnungen werden nicht geraten. POP3 ist ein eigener
|
||||
Import-Arbeitsablauf innerhalb von **Mail**, kein separat installierbares Modul.
|
||||
|
||||
Der Entwicklungsserver kann Migrationen beim automatischen Neustart nach einer
|
||||
Quelländerung bereits anwenden. Tatsächliche Schema-Stände prüfen; eine danach
|
||||
erstellte Sicherung enthält den aktuellen Stand und ist keine Sicherung vor dem
|
||||
Upgrade. Externe Transportaktionen, erneutes Versenden und Löschungen werden
|
||||
durch die Migration oder Quellveröffentlichung nicht ausgelöst.
|
||||
@@ -0,0 +1,343 @@
|
||||
# Kubernetes VM Test Lab
|
||||
|
||||
`tools/lab/govoplan-lab.py` creates and operates an amd64 Ubuntu/K3s test
|
||||
environment on local or SSH-accessible libvirt hypervisors. It provides the
|
||||
commands requested for the complete VM lifecycle:
|
||||
|
||||
| Command | Effect |
|
||||
| --- | --- |
|
||||
| `doctor` | Validate the strict inventory and, with `--online`, every hypervisor. |
|
||||
| `create --apply` | Download checksum-pinned cloud images, create VM overlays and boot the declared VMs. |
|
||||
| `deploy --apply` | Verify the signed GovOPlaN release, deploy shared state, install pinned K3s and apply GovOPlaN. |
|
||||
| `update --apply` | Pull newly pinned state images, update K3s serially and roll the selected GovOPlaN release. |
|
||||
| `status` | Show libvirt VM state, Kubernetes nodes and GovOPlaN pods. |
|
||||
| `pause --apply` | Gracefully shut down workers, control planes and shared state while retaining disks. |
|
||||
| `resume --apply` | Start the retained environment in dependency order and wait for readiness. |
|
||||
| `verify` | Collect sanitized live-cluster evidence and optionally perform the API-pod-loss drill. |
|
||||
| `destroy --apply --confirm <lab>` | Delete only the lab-owned domains and overlays; local evidence is retained by default. |
|
||||
|
||||
Every mutating command is a dry run unless `--apply` is present. Destruction
|
||||
also requires the exact lab name. Generated credentials, CA keys, manifests and
|
||||
evidence are written below the configured `state_directory` with owner-only
|
||||
permissions. Keep that directory outside the repository and include it in the
|
||||
workstation backup policy. Existing domains are reused or removed only when
|
||||
their GovOPlaN ownership description and both expected lab disk paths match.
|
||||
|
||||
## What The Lab Proves
|
||||
|
||||
The supplied inventories describe two different assurance levels:
|
||||
|
||||
- `tools/lab/govoplan-lab.example.toml` creates four VMs on one libvirt host.
|
||||
It is suitable for development, deployment rehearsal, migration testing,
|
||||
application-pod replacement and recovery-tool exercises. It cannot close
|
||||
GovOPlaN #27 because one physical host remains one failure domain.
|
||||
- `tools/lab/govoplan-lab.acceptance.example.toml` places the two workers on
|
||||
different hypervisors and puts the control and state VMs on a third. It can
|
||||
produce the bounded stateless application-tier evidence required by #27 when
|
||||
the declared hypervisors are genuinely independent physical failure domains.
|
||||
|
||||
Both examples use one control-plane VM and one state VM. This keeps the bounded
|
||||
#27 target economical, but it does not prove control-plane or state-service
|
||||
high availability. For control-plane failover, declare exactly three control
|
||||
nodes on independent hosts. PostgreSQL, Redis and object-storage failover must
|
||||
be tested against independently operated HA services; the lab's single state
|
||||
VM is intentionally a replaceable integration fixture.
|
||||
|
||||
Approximate minimum capacity for the four-VM profile is 10 vCPUs, 16 GiB RAM
|
||||
and 192 GiB of thin-provisioned disk. A six-VM profile with three controls needs
|
||||
additional capacity. Do not overcommit memory on an acceptance target.
|
||||
|
||||
## 1. Prepare The Hypervisors
|
||||
|
||||
On each Ubuntu/Debian libvirt host:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
qemu-kvm libvirt-daemon-system libvirt-clients virtinst cloud-image-utils curl
|
||||
sudo systemctl enable --now libvirtd
|
||||
```
|
||||
|
||||
Use a dedicated lab-administration account. Remote hypervisors are managed over
|
||||
SSH and the lifecycle invokes `sudo -n` there, so that account needs bounded
|
||||
non-interactive permission for libvirt, image and cloud-init operations.
|
||||
`NOPASSWD: ALL` is acceptable only on isolated lab hypervisors.
|
||||
|
||||
On a local hypervisor, put the workstation account in the `libvirt` group and
|
||||
point `vm_image_directory` at a directory writable by that account and
|
||||
traversable by `libvirt-qemu`. The lifecycle connects explicitly to
|
||||
`qemu:///system` and does not require passwordless local sudo. Log out and back
|
||||
in after a new group assignment before running `doctor --online`. Create the
|
||||
configured image directory before running the doctor; it deliberately rejects
|
||||
a missing or non-writable storage root instead of silently falling back to a
|
||||
different filesystem.
|
||||
|
||||
Create a dedicated SSH key on the management workstation:
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -f "$HOME/.ssh/govoplan-lab" \
|
||||
-C "GovOPlaN Kubernetes lab"
|
||||
```
|
||||
|
||||
Install its public key for every remote hypervisor account. The same public key
|
||||
is injected into the VMs. The lifecycle keeps its own `ssh_known_hosts` file,
|
||||
uses `accept-new` for first contact, and rejects changed host keys until a
|
||||
lab-owned VM is deliberately recreated.
|
||||
|
||||
### Network contract
|
||||
|
||||
The configured `bridge` must exist on every selected hypervisor. All VM
|
||||
addresses are static. Reserve them outside DHCP allocation and ensure that the
|
||||
management workstation can route directly to every VM address; the lifecycle
|
||||
does not tunnel VM traffic through the hypervisor SSH connection.
|
||||
|
||||
Permit only these flows inside the lab network:
|
||||
|
||||
| Port | Source and destination | Purpose |
|
||||
| --- | --- | --- |
|
||||
| TCP 22 | management workstation to every VM/hypervisor | Provisioning and evidence collection |
|
||||
| TCP 6443 | all K3s nodes and management path to controls | Kubernetes API |
|
||||
| UDP 8472 | K3s node to K3s node | Default Flannel VXLAN; never expose publicly |
|
||||
| TCP 10250 | K3s node to K3s node | Kubelet metrics and API |
|
||||
| TCP 2379-2380 | control to control, only with three controls | Embedded etcd |
|
||||
| TCP 80/443 | test clients to K3s nodes | Traefik/ServiceLB ingress |
|
||||
| TCP 5432/6379/9443 | K3s nodes to the state VM | PostgreSQL, Redis and TLS-protected Garage S3 |
|
||||
| TCP 3025/3143 | approved test clients/workers to the state VM | GreenMail SMTP/IMAP test endpoints |
|
||||
|
||||
The official
|
||||
[K3s networking requirements](https://docs.k3s.io/installation/requirements#networking)
|
||||
remain authoritative. Restrict state ports to the lab network even though the
|
||||
generated integration stack binds them on the state VM.
|
||||
|
||||
## 2. Create The Inventory
|
||||
|
||||
Start with the one-host rehearsal:
|
||||
|
||||
```bash
|
||||
install -d -m 0700 "$HOME/.config/govoplan/labs"
|
||||
cp tools/lab/govoplan-lab.example.toml \
|
||||
"$HOME/.config/govoplan/labs/development.toml"
|
||||
chmod 0600 "$HOME/.config/govoplan/labs/development.toml"
|
||||
```
|
||||
|
||||
Edit at least the bridge, network, static addresses and SSH key paths. For a
|
||||
multi-host run, copy the acceptance example and replace every example hostname,
|
||||
failure-domain declaration and network value. Strict parsing rejects unknown
|
||||
keys, mutable HTTP inputs, malformed checksums, duplicate addresses/MACs and an
|
||||
acceptance inventory that collapses workers onto one declared hypervisor or
|
||||
failure domain.
|
||||
|
||||
Cloud image, K3s binary, K3s installer and GovOPlaN release inputs are URL plus
|
||||
SHA-256 pairs. Updating means changing those reviewed pins and then running the
|
||||
`update` command; the tool deliberately does not follow `latest` aliases.
|
||||
|
||||
The one-host example uses the dedicated `govoplan-lab` NAT network. Its DHCP
|
||||
pool ends at `192.168.123.99`; the static lab addresses start at
|
||||
`192.168.123.201`. Define and start it once on the local hypervisor:
|
||||
|
||||
```bash
|
||||
virsh --connect qemu:///system net-define \
|
||||
tools/lab/libvirt/govoplan-lab-network.xml
|
||||
virsh --connect qemu:///system net-autostart govoplan-lab
|
||||
virsh --connect qemu:///system net-start govoplan-lab
|
||||
```
|
||||
|
||||
Re-running those commands is unnecessary when `virsh net-info govoplan-lab`
|
||||
already reports an active, persistent network. The lab destroy command leaves
|
||||
this reusable network in place.
|
||||
|
||||
## 3. Validate And Create The VMs
|
||||
|
||||
```bash
|
||||
LAB="$HOME/.config/govoplan/labs/development.toml"
|
||||
PYTHON="/mnt/DATA/git/govoplan/.venv/bin/python"
|
||||
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" doctor --online
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" create
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" create --apply
|
||||
```
|
||||
|
||||
The preview is safe to run repeatedly. Creation reuses a domain whose exact
|
||||
lab-owned name already exists and otherwise creates a thin qcow2 overlay under
|
||||
`vm_image_directory/<lab>/<node>`.
|
||||
|
||||
## 4. Deploy GovOPlaN
|
||||
|
||||
If `git.add-ideas.de` requires authentication for release images, export a
|
||||
read-only package/container-registry identity for this shell. A Gitea package
|
||||
token can be used as the password:
|
||||
|
||||
```bash
|
||||
export GOVOPLAN_LAB_REGISTRY_USERNAME='package-reader'
|
||||
read -r -s GOVOPLAN_LAB_REGISTRY_PASSWORD
|
||||
export GOVOPLAN_LAB_REGISTRY_PASSWORD
|
||||
```
|
||||
|
||||
Then preview and apply:
|
||||
|
||||
```bash
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" deploy
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" deploy --apply
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" status
|
||||
unset GOVOPLAN_LAB_REGISTRY_PASSWORD
|
||||
```
|
||||
|
||||
Deployment verifies the downloaded release manifest and keyring by pinned
|
||||
digest and by the existing GovOPlaN signature policy. It deploys PostgreSQL,
|
||||
Redis, single-node Garage and GreenMail on the state VM. The API, WebUI, workers
|
||||
and scheduler run in K3s from digest-pinned release images. A private lab CA
|
||||
protects both ingress and S3; backend pods receive only the CA Secret and keep
|
||||
TLS verification enabled. The CA profile carries critical `CA:TRUE` and
|
||||
`keyCertSign,cRLSign` constraints. `deploy` and `update` rotate older lab CAs
|
||||
that do not satisfy that profile and reissue the ingress/S3 certificate.
|
||||
|
||||
The final output identifies two local files below `state_directory`:
|
||||
|
||||
- `hosts` maps the public GovOPlaN and S3 test names to their VM addresses;
|
||||
- `pki/ca.crt` is the private lab CA certificate.
|
||||
|
||||
Add the host mappings to the test client's resolver and trust the CA only on
|
||||
devices used for this lab. On Debian/Ubuntu:
|
||||
|
||||
```bash
|
||||
STATE="$HOME/.local/share/govoplan/labs/govoplan-k8s-lab"
|
||||
cat "$STATE/hosts"
|
||||
sudo install -m 0644 "$STATE/pki/ca.crt" \
|
||||
/usr/local/share/ca-certificates/govoplan-k8s-lab.crt
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
|
||||
Review mappings before adding them to `/etc/hosts`; the lifecycle does not edit
|
||||
the workstation's trust or resolver configuration. Reinstall `pki/ca.crt` in
|
||||
the client trust store after an automatic CA rotation.
|
||||
|
||||
### Enroll the first administrator
|
||||
|
||||
The production runtime does not create a default password. Issue one expiring,
|
||||
single-use first-administrator credential inside an API pod and copy its
|
||||
owner-only artifact out immediately:
|
||||
|
||||
```bash
|
||||
KUBECTL="$STATE/bin/kubectl"
|
||||
POD="$($KUBECTL -n govoplan get pods \
|
||||
-l app.kubernetes.io/component=api \
|
||||
-o jsonpath='{.items[0].metadata.name}')"
|
||||
ARTIFACT="$STATE/first-admin-enrollment.json"
|
||||
umask 077
|
||||
|
||||
$KUBECTL -n govoplan exec "$POD" -- \
|
||||
python -m govoplan_core.commands.first_admin issue \
|
||||
--reason 'initial Kubernetes lab enrollment' \
|
||||
--output /tmp/first-admin-enrollment.json
|
||||
$KUBECTL -n govoplan exec "$POD" -- \
|
||||
cat /tmp/first-admin-enrollment.json > "$ARTIFACT"
|
||||
$KUBECTL -n govoplan exec "$POD" -- \
|
||||
rm -f /tmp/first-admin-enrollment.json
|
||||
chmod 0600 "$ARTIFACT"
|
||||
```
|
||||
|
||||
Submit the token from that artifact once to
|
||||
`/api/v1/bootstrap/first-admin` with the administrator email, display name,
|
||||
password, tenant slug and tenant name. The password must contain at least 12
|
||||
characters. The lab command performs that exchange without placing either the
|
||||
token or password in process arguments, rejects redirects, and removes the
|
||||
artifact only after HTTP 201:
|
||||
|
||||
```bash
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" enroll-admin \
|
||||
--email 'owner@example.org' \
|
||||
--display-name 'System Owner' \
|
||||
--tenant-slug default \
|
||||
--tenant-name 'Default Tenant'
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" enroll-admin \
|
||||
--email 'owner@example.org' \
|
||||
--display-name 'System Owner' \
|
||||
--tenant-slug default \
|
||||
--tenant-name 'Default Tenant' \
|
||||
--apply
|
||||
```
|
||||
|
||||
The public lab hostname must already resolve on the management workstation;
|
||||
the command verifies TLS through the generated private CA directly.
|
||||
|
||||
## 5. Collect #27 Evidence
|
||||
|
||||
Create a short-lived API key authorized to read the Ops status endpoint. In the
|
||||
current Access administration UI, open **Tenant API keys** and select only
|
||||
**View tenant settings** (`admin:settings:read`); the Ops endpoint explicitly
|
||||
accepts that compatibility scope. A dedicated operator credential may instead
|
||||
use `ops:operations:read`. Then run:
|
||||
|
||||
```bash
|
||||
export GOVOPLAN_OPS_API_KEY='short-lived-value'
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" verify \
|
||||
--exercise-api-pod-loss
|
||||
unset GOVOPLAN_OPS_API_KEY
|
||||
```
|
||||
|
||||
The verifier requires ready API and WebUI pods across at least two Kubernetes
|
||||
nodes, all Deployments available, consistent runtime composition, queue
|
||||
coverage and a valid database-connection budget. During the optional drill it
|
||||
deletes one ready API pod, probes public readiness and waits for replacement.
|
||||
It writes sanitized output to
|
||||
`state_directory/evidence/kubernetes-multi-host.json` and never stores the API
|
||||
key. A rehearsal inventory prints an explicit warning that its result is not
|
||||
independent-failure-domain evidence.
|
||||
|
||||
Retain these private artifacts together for review:
|
||||
|
||||
1. `inventory.json` and the reviewed inventory TOML;
|
||||
2. the adopted release manifest/keyring and installation receipt;
|
||||
3. `kubernetes.json`;
|
||||
4. the Kubernetes verifier output;
|
||||
5. private cluster logs for the approved drill window;
|
||||
6. the operator's out-of-band evidence that the worker hypervisors are
|
||||
independent physical hosts or availability zones.
|
||||
|
||||
GovOPlaN #37 additionally requires independent assessment and production
|
||||
approval keys. Running its evidence jobs in containers is supported, but a
|
||||
container does not create an independent authority. Follow
|
||||
`TARGET_MATURITY_EVIDENCE_RUNBOOK.md` after the #27 drill passes.
|
||||
|
||||
## 6. Update, Pause, Resume And Remove
|
||||
|
||||
After reviewing and changing pinned image/K3s/release values in the inventory:
|
||||
|
||||
```bash
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" update
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" update --apply
|
||||
```
|
||||
|
||||
Workers are cordoned, drained, updated and uncordoned one at a time. K3s
|
||||
controls are reconciled serially. The release-specific migration Job remains
|
||||
subject to GovOPlaN's signed backup-evidence gate. The lab update command is not
|
||||
a substitute for creating recovery evidence before a destructive state-schema
|
||||
change.
|
||||
|
||||
To stop compute use without deleting disks:
|
||||
|
||||
```bash
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" pause --apply
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" resume --apply
|
||||
```
|
||||
|
||||
To remove VM resources while preserving local evidence:
|
||||
|
||||
```bash
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" destroy
|
||||
"$PYTHON" tools/lab/govoplan-lab.py --config "$LAB" destroy \
|
||||
--apply --confirm govoplan-k8s-lab
|
||||
```
|
||||
|
||||
Add `--purge-local-state` only after evidence and recovery material have been
|
||||
retained elsewhere. That option deletes the generated local CA, secrets,
|
||||
manifests and evidence as well as the VMs.
|
||||
|
||||
## Acceptance Boundary
|
||||
|
||||
This tool supplies reproducible infrastructure and executes the bounded
|
||||
stateless-node drill. It does not certify the truth of operator-entered failure
|
||||
domains, provide HA PostgreSQL/Redis/Garage, create production backup evidence,
|
||||
or approve its own results. Those boundaries are deliberate: #27 can close
|
||||
after a passing run on independently controlled hosts; broader production
|
||||
maturity remains governed by #35, #37 and the target evidence runbook.
|
||||
@@ -0,0 +1,465 @@
|
||||
# Package Registry Releases
|
||||
|
||||
GovOPlaN publishes reusable module artifacts through Gitea's native PyPI and
|
||||
npm registries. These packages improve developer installation, release
|
||||
resolution, cacheability, and artifact inspection. They do not replace the
|
||||
signed runtime distribution: the signed manifest and digest-pinned OCI images
|
||||
remain the production deployment authority.
|
||||
|
||||
## Publication boundary
|
||||
|
||||
Every repository with a `pyproject.toml` contains
|
||||
`.gitea/workflows/module-package-release.yml`. The meta repository owns the
|
||||
canonical template and installs it with:
|
||||
|
||||
```bash
|
||||
python tools/repo/sync-module-package-workflows.py --write
|
||||
python tools/repo/sync-module-package-workflows.py --check
|
||||
```
|
||||
|
||||
The workflow runs for `v*` tags and may be dispatched manually for an existing
|
||||
tag. The organization preflight verifies that every package repository protects
|
||||
the `v*` namespace. Before building, the workflow itself verifies that:
|
||||
|
||||
- the tagged commit is contained in `main`;
|
||||
- the tag, Python project version, and optional WebUI package version agree;
|
||||
- package names remain in the `govoplan-*` and `@govoplan/*-webui` namespaces.
|
||||
|
||||
The workflow binds the repository explicitly from the Gitea Actions context.
|
||||
Do not rely on GitHub-compatible environment variables being injected by the
|
||||
runner image; Gitea runners may expose only the context values. Gitea 1.24 job
|
||||
tokens cannot read repository tag-protection settings, so package jobs must not
|
||||
receive a broad administrator token merely to repeat the organization preflight.
|
||||
Run the following before the first publication and after repository or tag-rule
|
||||
changes:
|
||||
|
||||
```bash
|
||||
python tools/gitea/gitea-configure-package-releases.py
|
||||
```
|
||||
|
||||
Preview and dispatch the exact wheel/WebUI versions selected by the developer
|
||||
meta-package with:
|
||||
|
||||
```bash
|
||||
python tools/gitea/gitea-dispatch-package-set.py \
|
||||
--env-file ~/.config/gitea/gitea.env
|
||||
python tools/gitea/gitea-dispatch-package-set.py \
|
||||
--env-file ~/.config/gitea/gitea.env \
|
||||
--apply
|
||||
```
|
||||
|
||||
The dispatcher reads exact versions from `packages/govoplan-meta/pyproject.toml`,
|
||||
inspects the selected tag to determine whether a WebUI package is expected,
|
||||
skips complete registry pairs and does not duplicate an active workflow. Use
|
||||
`--repository govoplan-core` for a bounded dispatch or `--verify-existing` to
|
||||
rebuild and hash-verify versions already present in both registries.
|
||||
|
||||
For coordinated lockstep tags, `push-release-tag.sh` pushes module tags first,
|
||||
Core next, and the meta tag last. This is a dependency guarantee for a
|
||||
single-capacity Actions runner: the developer package cannot run before its
|
||||
exact Core and module versions have entered the queue.
|
||||
|
||||
The same release entry point first validates the migration graph, then records
|
||||
the reviewed current Alembic heads under the target release version and reruns
|
||||
the strict migration audit before it changes package versions, commits, or
|
||||
tags. The default preflight intentionally does not require those heads to exist
|
||||
in the previous release baseline. A failed candidate-baseline check therefore
|
||||
cannot produce a protected package release.
|
||||
|
||||
The source gate validates `pyproject.toml`, the module version declaration
|
||||
(`MODULE_VERSION` or the top-level `ModuleManifest.version`), public package
|
||||
`__version__`, and WebUI metadata before creating tags. Release-tag artifact
|
||||
checks run only after the candidate tags and immutable WebUI lock have been
|
||||
created locally.
|
||||
|
||||
Release-lock regeneration resolves a fresh immutable lock from the reviewed
|
||||
candidate manifests; it does not seed resolution from the previous release
|
||||
lock. This prevents removed transitive packages and stale peer metadata from
|
||||
blocking or contaminating the new release. Candidate resolution also uses an
|
||||
isolated temporary npm cache, so a locally replaced tag cannot reuse metadata
|
||||
from a failed, unpushed release attempt.
|
||||
|
||||
Modules that retain the same WebUI package identity in both a root publish
|
||||
manifest and `webui/package.json` use the WebUI manifest as the canonical peer
|
||||
contract. The coordinated release synchronizes `peerDependencies` and
|
||||
`peerDependenciesMeta` into the publish manifest before creating the module
|
||||
tag, then synchronizes each lockfile root from the final package metadata. A
|
||||
distinct root package remains independent.
|
||||
|
||||
Every module referenced by Core's Git-based `package.release.json` must expose
|
||||
its WebUI identity at the repository root, including matching peer requirements
|
||||
and `webui/`-prefixed entry exports (also CSS subpaths). npm resolves Git
|
||||
dependencies from the repository root, while the native-package workflow packs
|
||||
`webui/`; success in one path does not verify the other. Run
|
||||
`python tools/checks/check-webui-package-facades.py` after changing either
|
||||
manifest or the release composition. The focused gate also runs this check.
|
||||
Adding or correcting a facade in an already published repository requires a
|
||||
new patch tag; never repair an existing immutable tag in place.
|
||||
|
||||
It builds one wheel and, where applicable, one npm tarball. The workflow records
|
||||
the source tag, source commit, filename, size, and SHA-256 in
|
||||
`package-artifacts.json` before publishing. Gitea rejects a second upload of the
|
||||
same package version, so correction requires a new version rather than artifact
|
||||
replacement.
|
||||
|
||||
A retry after partial publication is safe. Before upload, the workflow reads the
|
||||
native package registry file record and compares its SHA-256 with the artifact
|
||||
rebuilt from the protected tag. An exact existing artifact is skipped; a
|
||||
same-version artifact with another digest or an unexpected file set fails
|
||||
closed. This permits a failed npm publication to resume without weakening
|
||||
package immutability or accepting `--skip-existing` blindly.
|
||||
|
||||
The npm tarball is always published through an explicit local `./dist/...`
|
||||
path. Without that prefix, npm may interpret a relative tarball name as a Git
|
||||
package shorthand before it ever contacts the configured registry.
|
||||
|
||||
Published WebUI packages contain registry-compatible dependencies only. The
|
||||
workflow converts an internal dependency pinned to a protected `vX.Y.Z` Git tag
|
||||
into the exact `X.Y.Z` registry version and rejects unresolved `file:` or Git
|
||||
dependencies. Repository development metadata may therefore keep local or Git
|
||||
references without leaking them into the published package contract.
|
||||
Historical `add-ideas` and current `GovOPlaN` organization URLs are accepted
|
||||
for immutable tagged releases; both normalize to the same exact registry
|
||||
dependency and no branch or unversioned Git reference is accepted.
|
||||
|
||||
## One-time Gitea setup
|
||||
|
||||
Protect `v*` tags in every package repository and the meta repository. Allow
|
||||
only the `Owners` team to create or delete those tags.
|
||||
|
||||
```bash
|
||||
set -a
|
||||
. ~/.config/gitea/gitea.env
|
||||
set +a
|
||||
python tools/gitea/gitea-configure-package-releases.py --apply
|
||||
```
|
||||
|
||||
Create a dedicated personal access token with only `write:package` scope and
|
||||
store these organization-level Actions secrets on `GovOPlaN`:
|
||||
|
||||
- `GOVOPLAN_PACKAGE_USERNAME`: account owning the package token;
|
||||
- `GOVOPLAN_PACKAGE_TOKEN`: dedicated package-write token.
|
||||
|
||||
Do not use an administrator or general release token. Gitea 1.24 does not grant
|
||||
package publication to the automatic Actions job token. Organization secrets
|
||||
allow the same least-privilege credential to serve every module workflow.
|
||||
|
||||
## Exact release consumption
|
||||
|
||||
`tools/release/generate-release-package-set.py` supports two explicit package
|
||||
profiles. `base` translates the reviewed roots in `requirements-release.txt`;
|
||||
`full` reads the exact `govoplan[full]` dependency set from the developer
|
||||
meta-package. Both profiles resolve every version tag to its commit and verify
|
||||
the package metadata from that exact Git tree. The official module directory
|
||||
and immutable runtime distribution use `full`, so every publicly released
|
||||
module can be discovered without rebuilding the application image.
|
||||
|
||||
`tools/release/resolve-package-artifacts.py` then downloads exactly those wheel
|
||||
and WebUI versions from Gitea. It reads the identity embedded in every wheel and
|
||||
npm tarball, rejects missing, duplicate, unexpected, or oversized artifacts,
|
||||
and writes `package-artifacts.lock.json` with credential-free HTTPS download
|
||||
URLs, SHA-256 values, and npm registry integrity values. The resolver verifies
|
||||
that the bytes downloaded by `npm pack` match the registry's own integrity
|
||||
record. Credentials are accepted only through environment variables and are
|
||||
never written to the lock. Python resolution ignores ambient pip configuration
|
||||
and extra indexes for GovOPlaN roots, preventing an internal package name from
|
||||
being selected from an undeclared registry.
|
||||
|
||||
The runtime distribution workflow uses the verified full-profile wheelhouse
|
||||
directly and installs every selected module WebUI tarball only after matching
|
||||
it to the lock. It publishes the package set, package lock, and hash-locked
|
||||
requirements as release assets.
|
||||
The WebUI installer receives the absolute runtime-build interpreter path so its
|
||||
directory changes cannot escape the isolated release environment.
|
||||
Gitea 1.24 dispatches this workflow from a branch, but that branch is only the
|
||||
workflow implementation. The job fetches and peels the protected `v<version>`
|
||||
tag explicitly and materializes both `requirements-release.txt` and the
|
||||
developer meta-package from that Git tree. It then binds the signed distribution
|
||||
source and Gitea release assets to the same exact commit. A post-tag workflow
|
||||
repair can therefore retry publication without changing the released package
|
||||
composition or relabelling the later branch commit as released source.
|
||||
The package-lock SHA-256 is part of the signed distribution manifest. Runtime
|
||||
finalization also requires the lock's package versions and hashes to match the
|
||||
wheel composition embedded in the images. OCI assembly remains network-free
|
||||
after package and third-party dependency resolution.
|
||||
|
||||
The source refs remain in the module catalog for source provenance and release
|
||||
planning. Production installation consumes the signed runtime images rather
|
||||
than invoking `pip`, `npm`, or Git on the target host.
|
||||
|
||||
## Public module directory
|
||||
|
||||
For an operator-reviewed full publication, use
|
||||
`tools/release/release-catalog.py full-registry` followed by the same tool's
|
||||
`publish-candidate` command. Resolve the package set and registry lock first;
|
||||
the older direct-write shell wrapper is not the strict candidate publication
|
||||
path. See [Full registry candidates / Vollständige Registry-Kandidaten](FULL_REGISTRY_CANDIDATES.md)
|
||||
for the private host runtime, exact artifact checks, and legacy keyring transition.
|
||||
Catalog entries are synthesized from
|
||||
the exact tagged module manifests, never from a hand-maintained module list or
|
||||
the current workspace. Each entry binds its Python wheel and optional WebUI
|
||||
tarball to the registry URL, filename, size, SHA-256, package identity, source
|
||||
tag, and source commit before the complete catalog is signed.
|
||||
|
||||
The same publication transaction regenerates and prunes the browsable static
|
||||
directory under `public/catalogs/v1/modules/`. It writes a global
|
||||
`modules/index.json`, one `<module>/index.json`, and one
|
||||
`<module>/<version>/manifest.json` for every entry in the signed channel.
|
||||
These files are derived from that exact signed payload and keyring; stale JSON
|
||||
from an older partial catalog is removed while unrelated static assets are left
|
||||
untouched. The signed channel remains the trust anchor, while the module
|
||||
directory provides stable discovery URLs for browsers and external tooling.
|
||||
|
||||
Official GovOPlaN modules are open-source directory entries and do not require
|
||||
license entitlements. The generic `license_features` contract remains available
|
||||
for third-party package directories, support/configuration packages, or future
|
||||
deployment-specific presets. A catalog entry is gated only when that entry
|
||||
explicitly declares such features.
|
||||
|
||||
Core carries the public stable catalog URL and its independently pinned trust
|
||||
anchor. In the absence of an operator-configured catalog, Admin discovers the
|
||||
official directory automatically. Selecting an entry creates a reviewed
|
||||
install/update plan; the trusted installer downloads the exact signed artifacts
|
||||
into a private digest cache, verifies size and hash, and installs only from that
|
||||
cache. A saved plan is rejected if any package ref, artifact identity, catalog
|
||||
channel, sequence, or signing-key identity differs from the currently validated
|
||||
catalog.
|
||||
|
||||
The Admin directory can be searched by module, package, repository, or tag and
|
||||
filtered by available, installed, update, and blocked/withdrawn states. It
|
||||
shows the source revision, artifact digest, release notes, and configuration
|
||||
requirements. Missing dependency/interface providers and unsupported update
|
||||
windows are surfaced before an operator adds the entry to a plan; installer
|
||||
preflight remains authoritative.
|
||||
|
||||
Catalog entries also carry the permission definitions declared by the tagged
|
||||
module manifest. Admin groups and exposes their scopes before an install or
|
||||
update is planned. This is disclosure only: installing a module does not grant
|
||||
its permissions to an account, role, group, tenant, or service account.
|
||||
|
||||
Package lifecycle and availability are intentionally separate:
|
||||
|
||||
- install, update, and uninstall change the instance-wide package composition;
|
||||
- enable and disable change the active instance runtime graph;
|
||||
- tenant module entitlements define unavailable, available, and forced modules;
|
||||
- group/user presentation is governed through Views and Policy; and
|
||||
- enabling a capability module does not opt data into that capability.
|
||||
|
||||
Single-process or single-host installations may execute a supervised package
|
||||
plan locally. Shared-state and Kubernetes profiles reject node-local package
|
||||
mutation: operators compose and roll out a new signed full-profile runtime image
|
||||
instead. This prevents replicas from drifting while retaining the same Admin
|
||||
catalog and preflight experience.
|
||||
|
||||
## Developer meta-package
|
||||
|
||||
`packages/govoplan-meta` builds the optional `govoplan` package. Its default
|
||||
dependencies mirror the reviewed runtime roots; `govoplan[full]` adds all
|
||||
currently packageable workspace modules. Regenerate it after changing release
|
||||
requirements or package versions:
|
||||
|
||||
```bash
|
||||
python tools/release/generate-developer-meta-package.py
|
||||
python tools/release/generate-developer-meta-package.py --check
|
||||
```
|
||||
|
||||
The direct generator is a development synchronization tool, not a receipt-gated
|
||||
release executor. For release preparation, use the guarded out-of-run stage below.
|
||||
|
||||
`push-release-tag.sh` performs this synchronization before release commits and
|
||||
tags. The meta-package is for editable/developer setup and composition tests. It
|
||||
does not enable modules, apply migrations, provision services, or establish
|
||||
backup and recovery evidence.
|
||||
|
||||
### Shared source-tag contract and Meta composition
|
||||
|
||||
The shared version collector names Meta's real
|
||||
`packages/govoplan-meta/pyproject.toml` separately from root `pyproject.toml`.
|
||||
Only the registered `govoplan` system/meta repository with nested project name
|
||||
`govoplan` receives this contract. Missing, unknown, or misidentified metadata
|
||||
does not become a versionless exception. Version alignment compares the complete
|
||||
nested file with the canonical operator-tool generator output: its version must
|
||||
match Core, and dependencies and `full` composition must match the reviewed
|
||||
requirements and workspace package versions. Validation never executes a
|
||||
generator from a selected checkout. The shared trusted manifest checker can
|
||||
load reviewed application manifests; these checks are not a code sandbox.
|
||||
|
||||
Meta's complete generated file is recognized by shared version-mutation discovery,
|
||||
but the generic durable version executor deliberately cannot write it. A durable
|
||||
run freezes the release console's own Meta checkout as trusted runtime code;
|
||||
changing it in place would invalidate that run. The planner therefore places Meta
|
||||
after Core and exposes only non-executable support preparation/publication steps,
|
||||
not misleading automatic Meta version, commit, tag, or push actions. A missing or
|
||||
different Core target produces an actionable preparation prerequisite.
|
||||
|
||||
Prepare Core and the intended module inputs first, commit their reviewed state,
|
||||
then stop active durable runs for the target workspace. Use trusted operator tools
|
||||
against a separate registered, private source checkout, never the running operator
|
||||
Meta directory. Preview outside any selected source checkout, for example:
|
||||
|
||||
```sh
|
||||
python tools/release/prepare-developer-meta-package.py \
|
||||
--workspace /private/release-workspace --target-version X.Y.Z \
|
||||
> /private/operator/meta-preview.json
|
||||
python tools/release/prepare-developer-meta-package.py \
|
||||
--workspace /private/release-workspace --target-version X.Y.Z \
|
||||
--receipt /private/operator/meta-preview.json --apply --confirm-out-of-run
|
||||
```
|
||||
|
||||
The explicit confirmation attests that no durable run is active for that target
|
||||
workspace; the helper does not discover or stop other processes. Preview/apply
|
||||
requires registered clean main sources, matching origins and live-main ancestry,
|
||||
Core already aligned at the target, the exact nested identity, and no existing or
|
||||
unverifiable target Meta tag. Frozen receipts cover source HEADs/filesystem
|
||||
identities, release requirements, every discovered registered full-composition
|
||||
pyproject, the trusted generator snapshot, and the resulting full-file hash.
|
||||
Inputs are limited to 128 selected files, 2 MiB per file and 16 MiB aggregate.
|
||||
The canonical generator renders copied bounded data in a temporary directory;
|
||||
no generator from the selected checkout executes. Changed receipts block before
|
||||
the file effect. Apply writes only `packages/govoplan-meta/pyproject.toml`, then
|
||||
rechecks the other sources and exact output. A write or post-check failure that
|
||||
may have changed the file reports `needs-reconciliation` and leaves that bounded
|
||||
delta for explicit review; it never retries, rolls back, commits or publishes.
|
||||
|
||||
Review the complete generated composition and manually commit the resulting file.
|
||||
Complete matching Core publication before guarded Meta source tagging/publication,
|
||||
then start a fresh durable run from reviewed, clean, published operator tooling.
|
||||
Hot self-updating durable Meta release execution remains explicitly unsupported;
|
||||
this out-of-run preparation is the existing developer-meta support contract.
|
||||
|
||||
For every `tag_repositories` batch, strict checks apply to every selected
|
||||
repository before any tag creation, fetch, or push: registered checkout and
|
||||
origin/push URL, a clean `main` tracking `origin/main`, live remote-main ancestry,
|
||||
exact frozen HEADs, and immutable annotated local/remote tag objects. Git metadata
|
||||
must remain inside the operator's trusted workspace. Missing local knowledge of
|
||||
live remote main is a blocker; fetch and review it separately. Unknown repositories
|
||||
and non-registered remote aliases fail closed. If selected, Meta runs last.
|
||||
For Meta only, the matching annotated Core release must exist before its effect; Core may be
|
||||
an earlier selected repository, or an already tagged dependency. Publication
|
||||
requires that Core's exact tag and main commit are already remote.
|
||||
|
||||
Non-Meta selections do not acquire Meta's composition or Core-tag prerequisite.
|
||||
Local module-candidate tags still work before Core's final release lock or tag.
|
||||
The existing Core WebUI bundle gate still applies to module publication and
|
||||
batches selecting Core: relevant Core release-package and release-lock inputs
|
||||
must be operator-owned regular files, at most 16 MiB each, and their identities
|
||||
and content hashes are frozen before preflight and rechecked before every effect.
|
||||
When Core is unselected, this does not require its checkout to be clean or tagged;
|
||||
reviewed pending composition inputs retain their previous meaning. Backend-only
|
||||
selections never read irrelevant Core WebUI files.
|
||||
|
||||
Before even read-only Git commands, source ancestry must be owned by root or the
|
||||
current operator and must not be group/world writable. A sticky shared ancestor
|
||||
such as `/tmp` is permitted only above an owned, protected child; the workspace
|
||||
and checkouts receive no writable-directory exception. The current operator must
|
||||
own source inputs and actual Git/worktree/common metadata, which must be regular
|
||||
files/directories, non-symlinked, and non-writable by other users. Metadata walks
|
||||
are bounded to 500,000 entries and 128 levels, and tracked inputs to 100,000 paths
|
||||
and 16 MiB of listing text. Read-only Git targets, object alternates/grafts and
|
||||
hidden/sparse/unmerged index entries are blocked. Frozen receipts include actual
|
||||
checkout/Git directory paths, devices, inodes, owners and modes, so replacing Git
|
||||
metadata with the same HEAD is still detected. All selected version/composition
|
||||
inputs must be tracked, including root and WebUI package/lock metadata, discovered
|
||||
module manifests and package initializers, and Meta's nested package and release
|
||||
requirements; ignored working files cannot supply declarations absent from a tag.
|
||||
No chmod, ownership repair or
|
||||
global Git trust change is performed. A shared writable workspace must first be
|
||||
recreated or reviewed in the operator's protected release area by an explicitly
|
||||
authorized preparation workflow.
|
||||
|
||||
Preview is read-only. Local-tag mode creates only pinned annotated tags (or
|
||||
retrieves an identical published annotation); it does not publish main or tags.
|
||||
Publish mode atomically pushes the frozen main commit and annotation object,
|
||||
without force, retagging, fallback, or automatic retry. The complete source
|
||||
receipt is rechecked before every effect and afterward; remote main and the
|
||||
exact annotated tag must both match, not merely the Git exit status. Changes
|
||||
after preflight stop the remaining batch. Atomicity is per repository, not
|
||||
across repositories: earlier successful publications and a newly created local
|
||||
tag can remain after a later failure. Inspect reported receipts and obtain a new
|
||||
review before retrying; do not move immutable tags.
|
||||
|
||||
Whole-batch revalidation deliberately repeats source and live-remote checks around
|
||||
each repository effect; the number of checks can grow quadratically with batch
|
||||
size. Plan release time accordingly rather than bypassing trust checks. The
|
||||
shared internal preflight is read-only and exposes no legacy mutation path.
|
||||
The fixture suite covers Meta and non-Meta preview/local-tag/
|
||||
publication using temporary local bare remotes, including stale compositions,
|
||||
unsafe origins, divergent branches, damaged tag identity, changed receipts and
|
||||
false publication success. This is local tooling evidence, not a real release
|
||||
publication or production permission check.
|
||||
|
||||
Deutsch: Die gemeinsamen Helfer erkennen ausschließlich das registrierte
|
||||
Meta-Repository mit dem echten Paket `packages/govoplan-meta/pyproject.toml`
|
||||
(Projektname `govoplan`). Version und vollständige Zusammensetzung müssen dem
|
||||
kanonischen Generator, Core und den geprüften Anforderungen entsprechen; der
|
||||
Generator stammt niemals aus dem ausgewählten Checkout. Der gemeinsame
|
||||
Manifestprüfer kann geprüften Anwendungscode laden und ist keine Sandbox.
|
||||
Die gemeinsame Änderungsplanung erkennt die vollständig generierte Paketdatei,
|
||||
aber der dauerhafte Versionsausführer darf Meta nicht selbst verändern: sein
|
||||
eingefrorener Lauf bindet den Meta-Checkout als vertrauenswürdigen Programmstand.
|
||||
Meta erscheint deshalb nach Core ausschließlich mit nicht automatisch ausführbaren
|
||||
Vorbereitungs-/Veröffentlichungsschritten. Zuerst Core und Modulquellen vorbereiten
|
||||
und geprüft committen; bei abweichender Core-Zielversion nennt der Plan diese
|
||||
Voraussetzung ausdrücklich. Aktive dauerhafte Läufe des Ziel-Workspaces beenden.
|
||||
Mit `prepare-developer-meta-package.py` zunächst eine Vorschau außerhalb der
|
||||
Quell-Checkouts speichern, dann deren JSON über `--receipt` zusammen mit `--apply`
|
||||
und `--confirm-out-of-run` bestätigen. Das Ziel muss ein separater registrierter
|
||||
privater Checkout sein, niemals das laufende Operator-Meta. Die Bestätigung ist
|
||||
eine Betreibererklärung; der Helfer sucht oder beendet keine fremden Prozesse.
|
||||
Quell-HEADs, Pfadidentitäten, Anforderungen, alle registrierten vollständigen
|
||||
Paket-Eingaben, der vertrauenswürdige Generator und der vollständige Ausgabehash
|
||||
werden eingefroren. Es gelten höchstens 128 Quelldateien, 2 MiB je Datei und
|
||||
16 MiB insgesamt. Core muss bereits vollständig zur Zielversion passen; vorhandene
|
||||
oder nicht verifizierbare Meta-Zieltags sperren die Vorbereitung. Geänderte
|
||||
Nachweise stoppen vor dem Schreiben. Ausschließlich die verschachtelte Paketdatei
|
||||
wird vollständig generiert und danach geprüft; ein Fehler nach dem Schreiben
|
||||
meldet `needs-reconciliation` und erfordert die manuelle Prüfung dieser begrenzten
|
||||
Änderung, ohne automatisches Zurücksetzen. Kein automatischer
|
||||
Commit, Push oder Wiederholungsversuch findet statt. Zusammensetzung prüfen,
|
||||
manuell committen, Core zuerst veröffentlichen, dann die geschützte Meta-Tag-Route
|
||||
verwenden und einen neuen dauerhaften Lauf starten. Eine Selbstaktualisierung
|
||||
des aktiven dauerhaften Meta-Laufs bleibt ausdrücklich nicht unterstützt.
|
||||
Für jeden Tag-Stapel, auch ohne Meta, gelten
|
||||
Vertrauens-, Origin-, saubere Main- und Live-Abstammungsprüfungen für die gesamte
|
||||
Auswahl vor jeder Änderung. Unbekannte Repositories und nicht registrierte
|
||||
Remote-Aliase sind gesperrt. Nur bei ausgewähltem Meta gelten zusätzlich dessen
|
||||
Zusammensetzungsprüfung und der passende annotierte Core-Tag als Voraussetzung;
|
||||
Meta folgt zuletzt. Für Metas Veröffentlichung müssen Core-Tag und Main-Commit
|
||||
bereits auf dem Remote vorliegen. Lokale Modul-Kandidatentags bleiben vor Cores
|
||||
abschließendem Release-Lock und Tag möglich. Die vorhandene Core-WebUI-Bundleprüfung
|
||||
bleibt bei Modulveröffentlichung und Core-Auswahl erhalten. Relevante Core-Paket-
|
||||
und Lockdateien müssen eigene reguläre Dateien mit höchstens je 16 MiB sein;
|
||||
Identität und Inhaltshash werden eingefroren und vor jeder Aktion erneut geprüft.
|
||||
Nicht ausgewähltes Core benötigt dafür weder einen sauberen Checkout noch einen
|
||||
Tag. Reine Backend-Auswahlen lesen keine irrelevanten Core-WebUI-Dateien.
|
||||
Vor Git-Aufrufen werden Eigentümer, Schreibrechte, sichere
|
||||
Pfadabstammung und echte Git-/Worktree-Metadaten geprüft; veränderbare gemeinsame
|
||||
Verzeichnisse, fremde Eigentümer, Alternates, Grafts und versteckte Indexeinträge
|
||||
sind gesperrt. Ein Sticky-Bit-Vorfahr wie `/tmp` ist nur oberhalb eines eigenen
|
||||
geschützten Unterverzeichnisses zulässig. Es erfolgen weder Rechtereparaturen
|
||||
noch globale Git-Vertrauensänderungen. Ausgewählte Versions- und Zusammensetzungs-
|
||||
dateien müssen versioniert sein: Paket-/Lockdateien, Modulmanifeste und
|
||||
Paketinitialisierer sowie Metas verschachteltes Paket und Release-Anforderungen.
|
||||
Ignorierte Arbeitsdateien dürfen keine vom Tag abweichenden Angaben liefern.
|
||||
Die Vorschau schreibt nichts, lokale Tags veröffentlichen nichts,
|
||||
und die Veröffentlichung überträgt Main und den exakten annotierten Tag atomar
|
||||
je Repository. Unmittelbar vor und nach den Aktionen werden die eingefrorenen
|
||||
Quellnachweise erneut geprüft, einschließlich entferntem Main und Tag-Objekt.
|
||||
Bei Änderungen oder Fehlern stoppt der Rest des Stapels ohne automatischen
|
||||
Wiederholungsversuch. Frühere Veröffentlichungen und neu erzeugte lokale Tags
|
||||
können bestehen bleiben: vor einem neuen Versuch Nachweise prüfen und erneut
|
||||
freigeben, niemals unveränderliche Tags verschieben. Die vollständigen Quell- und
|
||||
Live-Remote-Prüfungen werden um jede Aktion wiederholt; bei großen Stapeln kann
|
||||
deren Anzahl quadratisch wachsen. Diese konservativen Prüfkosten gehören zur
|
||||
Release-Planung. Der interne Vorprüfer ist ausschließlich lesend und besitzt
|
||||
keinen alten Änderungspfad. Tests für Auswahlen mit und ohne Meta verwenden
|
||||
nur temporäre lokale Remotes und ersetzen keine echte Veröffentlichungsprüfung.
|
||||
|
||||
If the tag-triggered developer meta-package job fails before publication, rerun
|
||||
`publish-developer-meta-package.yml` with the existing protected version. The
|
||||
manual path validates that tag against `main`, checks out its exact commit, and
|
||||
publishes only when the registry does not already contain the same wheel hash.
|
||||
|
||||
Generic Packages are intentionally not used. Add that transport only when a
|
||||
consumer needs an artifact format unsupported by PyPI, npm, Gitea Releases, or
|
||||
the OCI registry.
|
||||
@@ -49,6 +49,13 @@ firewall ports. Those inputs are sufficient to provision a k3s target. They are
|
||||
not sufficient to claim control-plane HA unless three control-plane failure
|
||||
domains are present.
|
||||
|
||||
The repository now supplies the strict libvirt/K3s lifecycle and example
|
||||
inventories for this handoff in
|
||||
[`KUBERNETES_TEST_LAB.md`](KUBERNETES_TEST_LAB.md). Its `acceptance` mode
|
||||
rejects a declared topology unless the workers and shared-state fixture occupy
|
||||
different hypervisor and failure-domain identifiers. Reviewers must still
|
||||
verify that those identifiers correspond to genuinely independent hosts.
|
||||
|
||||
### Separate deployment and evidence authorities
|
||||
|
||||
The deployment identity may create and update the namespace, Secret,
|
||||
@@ -74,6 +81,7 @@ python tools/deployment/govoplan-deploy.py render-kubernetes \
|
||||
--namespace govoplan \
|
||||
--secret-name govoplan-runtime \
|
||||
--tls-secret-name govoplan-tls \
|
||||
--s3-ca-secret-name govoplan-s3-ca \
|
||||
--ingress-class-name nginx \
|
||||
--output /srv/govoplan/<installation-id>/kubernetes.json
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
The Core recovery ledger is a platform primitive, not automatic protection for
|
||||
module-owned effects. The canonical, machine-checked inventory is
|
||||
[`recovery-operation-inventory.json`](recovery-operation-inventory.json).
|
||||
[`recovery-operation-inventory.json`](../recovery-operation-inventory.json).
|
||||
|
||||
## Classification Rules
|
||||
|
||||
@@ -58,6 +58,45 @@ checkouts remain usable for read-only planning, but every durable executor
|
||||
fails closed there; clone the registered origins into a private workspace
|
||||
before releasing.
|
||||
|
||||
For a host with a confirmed IPv6 connection timeout, set
|
||||
`GOVOPLAN_RELEASE_SSH_ADDRESS_FAMILY=inet` only for the release-tool invocation.
|
||||
When unset, the original SSH command is preserved, including the trusted
|
||||
operator's per-host `AddressFamily` configuration (normally `any`). Explicit
|
||||
values accepted by the shared source/tag Git helper are exactly `any`, `inet`
|
||||
(IPv4 only), and `inet6` (IPv6 only). Empty, misspelled, whitespace-padded, or
|
||||
injected values fail before Git starts. The selector only adds the corresponding
|
||||
fixed SSH `AddressFamily` option: it does not change DNS, host-key verification,
|
||||
the registered remote, authentication, `BatchMode=yes`, or `ConnectTimeout=8`.
|
||||
Arbitrary `GIT_SSH_COMMAND` overrides remain ignored. For example, start a
|
||||
single local console invocation with:
|
||||
|
||||
```sh
|
||||
GOVOPLAN_RELEASE_SSH_ADDRESS_FAMILY=inet \
|
||||
./.venv/bin/python tools/release/release-console.py
|
||||
```
|
||||
|
||||
The same process-scoped setting applies to canonical source/tag readbacks and
|
||||
registry-candidate source verification. Under Flatpak, pass it explicitly to
|
||||
the host invocation with `flatpak-spawn --host /usr/bin/env
|
||||
GOVOPLAN_RELEASE_SSH_ADDRESS_FAMILY=inet ...`. It is not a global SSH setting
|
||||
and does not affect the website publisher's separate transport sanitizer, npm,
|
||||
or HTTP downloads. An IPv4-only setting cannot reach IPv6-only hosts; omit it
|
||||
or use `any` when the diagnosed restriction no longer applies.
|
||||
|
||||
Deutsch: Bei einem bestätigten IPv6-Verbindungs-Timeout kann für genau einen
|
||||
Release-Werkzeugaufruf `GOVOPLAN_RELEASE_SSH_ADDRESS_FAMILY=inet` gesetzt werden.
|
||||
Ohne diese Variable bleibt der bisherige SSH-Befehl einschließlich der
|
||||
vertrauenswürdigen Host-Konfiguration unverändert (normalerweise `any`).
|
||||
Explizit zulässig sind ausschließlich `any`, `inet` (nur IPv4) und `inet6`
|
||||
(nur IPv6). Andere oder leere Werte werden vor dem Git-Aufruf abgewiesen.
|
||||
DNS, Hostschlüsselprüfung, registrierte Quelladresse, Authentifizierung und
|
||||
Zeitlimit bleiben unverändert; frei vorgegebene SSH-Befehle bleiben gesperrt.
|
||||
Unter Flatpak die Variable ausdrücklich an den Host-Aufruf übergeben. Die
|
||||
Auswahl gilt für den gemeinsamen Git-Helfer der Quell-/Tag-Prüfungen, nicht
|
||||
für den separaten Website-Publisher, npm oder HTTP-Downloads. Sie ändert keine
|
||||
globale Konfiguration. Nach Behebung des Netzwerkproblems die Variable
|
||||
weglassen oder auf `any` setzen; IPv4-only erreicht keine IPv6-only-Ziele.
|
||||
|
||||
The runtime itself is part of the authority boundary. Durable run creation
|
||||
verifies the meta checkout, release/check tooling, repository registry, Python
|
||||
environment, loaded `govoplan_core` and cryptography packages, and Git/SSH
|
||||
@@ -221,6 +260,9 @@ Repository capabilities are frozen into each plan unit (`python-package`,
|
||||
`core-release-bundle`, and the universal `git-source`) and determine which
|
||||
steps appear. Internally aligned version changes are rendered deterministically
|
||||
from recognized TOML, JSON, lockfile, manifest, and package declarations.
|
||||
The manifest may use a literal version or a top-level literal `MODULE_VERSION`;
|
||||
the latter is updated without rewriting independently versioned interfaces.
|
||||
Computed or missing version declarations fail before any metadata is written.
|
||||
Pre-existing dirty worktrees remain visible but have no commit executor; the
|
||||
console never absorbs unrelated operator changes.
|
||||
|
||||
@@ -232,6 +274,17 @@ runs a receipt-bound alignment gate before exposing any atomic branch/tag push.
|
||||
A failed step stops later steps while preserving prior receipts for explicit
|
||||
retry or reconciliation.
|
||||
|
||||
Local module candidate creation deliberately does not require those candidates
|
||||
to be resolved already in Core's release lock: their annotated tags are inputs
|
||||
to the next lock-generation step. The internal tag helper applies this ordering
|
||||
only when no Core repository is selected and remote publication is disabled.
|
||||
Module version/lock consistency, manifest validity, clean/non-behind worktrees,
|
||||
and local/remote tag immutability checks still apply. Core candidate tagging
|
||||
continues to validate its own complete bundle, and every remote-publication
|
||||
preview and execution requires the selected modules to match Core's release
|
||||
input and resolved lock. A local candidate is therefore not publication
|
||||
approval; a stale Core lock blocks publication without changing remote refs.
|
||||
|
||||
The browser likewise retains the request identifier for an uncertain
|
||||
resume/retry/reconciliation response and replays it after reload. A successful
|
||||
replay selects the returned run state. Transport and server failures retain the
|
||||
@@ -301,8 +354,17 @@ such as `0.2.0` or `0.2.0-alpha1`, but requires the first three version numbers
|
||||
to move forward.
|
||||
|
||||
Plain repository pushes are separate from catalog publication. `Preview Push`
|
||||
shows the selected repository push commands. `Push Selected` requires `PUSH` in
|
||||
the repository push confirmation field.
|
||||
shows the selected repository push commands, but generic push, sync and prepare
|
||||
mutation endpoints are disabled: they require a separate durable, receipt-bound
|
||||
maintenance workflow and cannot be enabled by typing a confirmation. Source
|
||||
release branch/tag publication uses the durable release-run steps described
|
||||
above; do not route ordinary dirty worktrees through the legacy all-repository
|
||||
stage/commit/tag helper.
|
||||
|
||||
The [headless devkit release commands](DEVKIT_RELEASE.md) invoke this same
|
||||
application in-process without starting a server. They expose selective planning,
|
||||
bounded status/history, create/show/preview/execute and explicit recovery while
|
||||
retaining the same request IDs, confirmations, source bindings and receipts.
|
||||
|
||||
The source release panel retains `Preview Tag + Publish` as a non-mutating
|
||||
inspection. Its legacy `Create Tags` and `Publish Tags` controls stay visible
|
||||
@@ -503,6 +565,15 @@ tree and requires byte-for-byte equality with those validated objects. Tags and
|
||||
remote branch updates then reference that exact commit SHA rather than the
|
||||
mutable worktree `HEAD`.
|
||||
|
||||
For a full registry-backed release, first build a fresh private candidate using
|
||||
`release-catalog.py full-registry`. Pass `--selected-repository` for newly
|
||||
released HEAD-bound units, not every unchanged package in the full profile.
|
||||
The command independently checks all full-profile registry bytes and annotated
|
||||
tag provenance, then feeds this same strict `publish-candidate` transaction.
|
||||
It does not create Gitea runtime Releases or dispatch image builds. See
|
||||
[Full registry candidates / Vollständige Registry-Kandidaten](FULL_REGISTRY_CANDIDATES.md)
|
||||
for the complete EN/DE workflow and the narrowly scoped legacy keyring transition.
|
||||
|
||||
Published channels are expected below the public catalog base URL:
|
||||
|
||||
- `https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json`
|
||||
@@ -515,8 +586,9 @@ updated catalog, and keep the published keyring healthy.
|
||||
|
||||
When a selected module exposes a WebUI package, its requested version must also
|
||||
match Core's `webui/package.release.json` input and the resolved
|
||||
`package-lock.release.json` entry. The source-tag preflight, selective plan, and
|
||||
catalog-candidate writer all enforce this composition boundary. Pins for modules
|
||||
`package-lock.release.json` entry. The source-publication preflight, selective
|
||||
plan, and catalog-candidate writer all enforce this composition boundary;
|
||||
module-only local candidate tags use the staged order described above. Pins for modules
|
||||
that are not part of the selective release remain unchanged.
|
||||
|
||||
Release integration also enforces repository and composition version alignment
|
||||
+26
-5
@@ -3,6 +3,8 @@
|
||||
For the exact external handoff, least-privilege collector permissions and live
|
||||
two-node acceptance procedure, see
|
||||
[`PRODUCTION_TARGET_HANDOFF.md`](PRODUCTION_TARGET_HANDOFF.md).
|
||||
For a reproducible local or multi-hypervisor libvirt/K3s target, use
|
||||
[`KUBERNETES_TEST_LAB.md`](KUBERNETES_TEST_LAB.md).
|
||||
|
||||
## Implemented Contract
|
||||
|
||||
@@ -74,6 +76,7 @@ python tools/deployment/govoplan-deploy.py render-kubernetes \
|
||||
--namespace govoplan \
|
||||
--secret-name govoplan-runtime \
|
||||
--tls-secret-name govoplan-tls \
|
||||
--s3-ca-secret-name govoplan-s3-ca \
|
||||
--ingress-class-name nginx \
|
||||
--output /srv/govoplan/default/kubernetes.json
|
||||
```
|
||||
@@ -90,11 +93,25 @@ command prints the exact required key contract. Review the generated
|
||||
`FORWARDED_ALLOW_IPS` value and replace it with the exact ingress-proxy network
|
||||
before production use.
|
||||
|
||||
When an external S3 endpoint is signed by a private CA, create the optional CA
|
||||
Secret with a `ca.crt` key and pass `--s3-ca-secret-name`. The renderer mounts
|
||||
that Secret read-only and sets `AWS_CA_BUNDLE` for API, worker, scheduler,
|
||||
migration and database-wait containers. It does not disable certificate
|
||||
verification or replace the WebUI trust store.
|
||||
|
||||
The generated containers run as non-root with a read-only root filesystem and
|
||||
an ephemeral `/tmp`. Runtime Deployments wait for the exact configured database
|
||||
migration heads before starting. The API exposes `/health/ready`, which fails
|
||||
while that API node is draining or cannot prove its runtime-coordination
|
||||
heartbeat.
|
||||
an ephemeral `/tmp`. Celery Beat keeps its replaceable schedule database there;
|
||||
durable schedule definitions remain in shared state. The WebUI resolves its
|
||||
configured API Service when the container starts, so Kubernetes deployments do
|
||||
not inherit the Compose-only `load-balancer` hostname. Runtime Deployments wait
|
||||
for the exact dependency-resolved database migration heads before starting.
|
||||
The API exposes `/health/ready`, which fails while that API node is draining or
|
||||
cannot prove its runtime-coordination heartbeat.
|
||||
|
||||
Replicated API, WebUI, and worker Deployments use a hard hostname-spread
|
||||
constraint scoped to the current pod-template hash. A rollout therefore keeps
|
||||
each replica set distributed across independently schedulable nodes instead of
|
||||
allowing all replacement pods to settle on one node after the old set exits.
|
||||
|
||||
## Runtime Coordination
|
||||
|
||||
@@ -253,7 +270,11 @@ record under the installation evidence directory and never retains the API key.
|
||||
|
||||
Use `--exercise-api-pod-loss` in an approved drill window to delete one API pod,
|
||||
observe the public readiness path continuously, and record its replacement.
|
||||
Generated API workloads use a ten-second pre-stop drain so Kubernetes can remove
|
||||
the terminating endpoint from ingress and service routing before Uvicorn exits.
|
||||
Do not remove or shorten this drain without repeating the public-path pod-loss
|
||||
test against the target ingress controller and network implementation.
|
||||
This proves the bounded stateless-node-loss slice only. Session continuity,
|
||||
accepted-job redelivery, state-service failover, and coordinated restore remain
|
||||
separate target exercises whose signed evidence is governed by
|
||||
`docs/TARGET_MATURITY_EVIDENCE_RUNBOOK.md` and GovOPlaN #37.
|
||||
`docs/operations/TARGET_MATURITY_EVIDENCE_RUNBOOK.md` and GovOPlaN #37.
|
||||
@@ -0,0 +1,91 @@
|
||||
# WebUI release dependency installer retries
|
||||
|
||||
## English
|
||||
|
||||
This operational note covers
|
||||
[`install-webui-release-dependencies.sh`](../../tools/release/install-webui-release-dependencies.sh)
|
||||
and the exit-status repair tracked in
|
||||
[Meta #54](https://git.add-ideas.de/GovOPlaN/govoplan/issues/54).
|
||||
It applies to release administrators using the legacy runtime WebUI installer;
|
||||
there are no new application settings, permissions, or end-user workflows.
|
||||
|
||||
Each retried npm install or Git clone has at most three attempts. The installer
|
||||
waits 10 seconds after the first failure and 20 seconds after the second, and
|
||||
continues immediately after success. If all attempts fail, it exits with the
|
||||
last command's nonzero status. Its `set -e` execution stops before subsequent
|
||||
installation stages; callers using `set -e` also stop before subsequent work.
|
||||
Previously, the retry helper could report success after three failures because
|
||||
it captured the status of a completed `if` statement instead of the command.
|
||||
|
||||
On exhaustion, inspect the npm or Git error and correct the reported cause
|
||||
before rerunning the installation. The temporary dependency workspace is
|
||||
removed on exit. Earlier changes to `package.json`, removal of `package-lock.json`,
|
||||
cache cleaning, and completed dependency installations are not rolled back;
|
||||
prepare a fresh disposable release workspace when a clean retry is required.
|
||||
|
||||
The repair preserves the existing retry count, backoff, cache behavior, and
|
||||
peer-resolution flags. It does not lift the runtime publication hold tracked in
|
||||
[Meta #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52).
|
||||
Review the historical `--legacy-peer-deps` workaround separately before lifting
|
||||
that hold. Strict disposable Git-release and signed catalog verification do not
|
||||
use this installer; strict release verification must not bypass peer checks.
|
||||
See [Package Registry Releases](PACKAGE_REGISTRY_RELEASES.md) for release context.
|
||||
|
||||
Run the isolated regression suite from the meta repository:
|
||||
|
||||
```sh
|
||||
python3 -m unittest -v tests.test_webui_release_dependency_retries
|
||||
```
|
||||
|
||||
The suite executes the actual Bash installer and a caller using `set -e`, with
|
||||
local npm, Git, Node, and sleep stubs. It covers success on attempts one, two, and
|
||||
three, final failure status, backoff, and termination at each retry call site.
|
||||
It performs no network access, real waiting, or changes to the real npm cache.
|
||||
It checks shell control flow, not package resolution or runtime publication.
|
||||
|
||||
## Deutsch
|
||||
|
||||
Dieser Betriebshinweis beschreibt
|
||||
[`install-webui-release-dependencies.sh`](../../tools/release/install-webui-release-dependencies.sh)
|
||||
und die unter [Meta #54](https://git.add-ideas.de/GovOPlaN/govoplan/issues/54)
|
||||
erfasste Korrektur des Rückgabestatus. Er richtet sich an Release-Administratoren,
|
||||
die den bisherigen WebUI-Installer für Laufzeit-Releases verwenden. Neue
|
||||
Anwendungseinstellungen, Berechtigungen oder Endanwenderabläufe entstehen nicht.
|
||||
|
||||
Jede wiederholte npm-Installation und jeder Git-Klon erhält höchstens drei
|
||||
Versuche. Nach dem ersten Fehlschlag wartet der Installer 10 Sekunden, nach dem
|
||||
zweiten 20 Sekunden; nach einem Erfolg fährt er sofort fort. Scheitern alle
|
||||
Versuche, endet er mit dem letzten von null verschiedenen Rückgabestatus.
|
||||
Durch `set -e` werden nachfolgende Installationsschritte nicht ausgeführt;
|
||||
auch aufrufende Skripte mit `set -e` brechen vor ihren nächsten Schritten ab.
|
||||
Bisher konnte die Hilfsfunktion nach drei Fehlschlägen Erfolg melden, weil sie
|
||||
den Status der abgeschlossenen `if`-Anweisung statt des Befehls übernahm.
|
||||
|
||||
Prüfen Sie nach dem Abbruch die npm- oder Git-Fehlermeldung und beheben Sie deren
|
||||
Ursache vor einem erneuten Installationslauf. Das temporäre Verzeichnis für
|
||||
Abhängigkeiten wird beim Beenden entfernt. Vorherige Änderungen an `package.json`,
|
||||
das Entfernen von `package-lock.json`, die Cache-Bereinigung und abgeschlossene
|
||||
Installationen werden nicht zurückgerollt. Bereiten Sie bei Bedarf einen neuen
|
||||
temporären Release-Arbeitsbereich für einen sauberen Wiederholungslauf vor.
|
||||
|
||||
Die Korrektur erhält Anzahl und Wartezeiten der Versuche, Cache-Verhalten und
|
||||
Optionen zur Peer-Auflösung. Die Sperre für Laufzeitveröffentlichungen aus
|
||||
[Meta #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52) bleibt bestehen.
|
||||
Der bisherige Einsatz von `--legacy-peer-deps` muss vor ihrer Aufhebung gesondert
|
||||
geprüft werden. Die strenge Git-Release-Prüfung in einem temporären Arbeitsbereich
|
||||
und die Prüfung signierter Kataloge verwenden diesen Installer nicht; die strenge
|
||||
Release-Prüfung darf Peer-Prüfungen nicht umgehen. Weitere Zusammenhänge erläutert
|
||||
[Package Registry Releases](PACKAGE_REGISTRY_RELEASES.md).
|
||||
|
||||
Führen Sie die isolierten Regressionstests im Meta-Repository aus:
|
||||
|
||||
```sh
|
||||
python3 -m unittest -v tests.test_webui_release_dependency_retries
|
||||
```
|
||||
|
||||
Die Tests führen den tatsächlichen Bash-Installer und ein aufrufendes Skript mit
|
||||
`set -e` aus. Lokale Testprogramme ersetzen npm, Git, Node und sleep. Geprüft werden
|
||||
Erfolge im ersten, zweiten und dritten Versuch, der letzte Fehlerstatus,
|
||||
Warteintervalle und der Abbruch an jeder Aufrufstelle. Es gibt keine
|
||||
Netzwerkzugriffe, echten Wartezeiten oder Änderungen am tatsächlichen npm-Cache.
|
||||
Die Tests prüfen den Shell-Ablauf, nicht die Paketauflösung oder Veröffentlichung.
|
||||
@@ -79,7 +79,7 @@ development depend on submodule updates.
|
||||
Module release tags also publish wheels and WebUI tarballs to the organization
|
||||
PyPI/npm registries. The meta release resolves exact versions into a hash-bound
|
||||
package lock before producing the signed OCI runtime. See
|
||||
`docs/PACKAGE_REGISTRY_RELEASES.md`. Git tags remain source provenance; package
|
||||
`docs/operations/PACKAGE_REGISTRY_RELEASES.md`. Git tags remain source provenance; package
|
||||
registries are reusable artifact transport; the signed runtime manifest and
|
||||
digest-pinned images remain production authority.
|
||||
|
||||
Executable
+164
@@ -0,0 +1,164 @@
|
||||
# Product-wide UI review program
|
||||
|
||||
The [central Gitea epic](https://git.add-ideas.de/GovOPlaN/govoplan/issues/56)
|
||||
coordinates the review; its linked module issues are the canonical backlog,
|
||||
progress and evidence log. The [issue inventory](ui-review-issue-inventory.json)
|
||||
is a discovery/link snapshot, **not a second progress tracker**. All reviews were
|
||||
initialized pending. Implementing a shared component or moving a help icon does
|
||||
not by itself complete a module review.
|
||||
|
||||
The visual and interaction rules live in Core's
|
||||
[UI design principles](https://git.add-ideas.de/GovOPlaN/govoplan-core/src/branch/main/docs/UI_DESIGN_PRINCIPLES.md).
|
||||
This document defines how to apply and verify those principles across repositories;
|
||||
it does not fork their definitions. The usual [Gitea issue contract](GITEA_ISSUES.md)
|
||||
still applies. At the user's request, this cross-product program is coordinated in
|
||||
Meta; shared primitives and their implementation remain owned by Core.
|
||||
|
||||
## Scope and initial inventory
|
||||
|
||||
The September 2026 source/catalog baseline contains **77 review tracks**:
|
||||
|
||||
- **73 implemented scopes:** Core/shared shell and 72 manifest-backed modules.
|
||||
- **4 registered placeholders:** Appointments, Ledger, XÖV and XTA/OSCI. They have
|
||||
README-only repositories at initialization. Their separate readiness tracks
|
||||
remain pending for future interfaces; they are not counted as implemented or
|
||||
reviewed modules.
|
||||
|
||||
Meta and the public website are not business-module review scopes. Module IDs
|
||||
come from actual manifests, not repository-name guesses: for example,
|
||||
`govoplan-campaign` owns `campaigns` and `govoplan-dist-lists` owns `dist_lists`.
|
||||
The inventory is derived from `repositories.json`, trusted module manifests and
|
||||
the owning repository's source files. Changes to the catalog must be reconciled
|
||||
with the central epic rather than silently dropping a module.
|
||||
|
||||
A module with no standalone WebUI is still in scope. Review its configuration
|
||||
and administrator experience, contributed operator/public surfaces, widgets,
|
||||
documentation, permissions, errors and interactions exposed through other
|
||||
modules. A missing standalone page is not evidence of conformance. A reviewer
|
||||
may mark a particular principle not applicable only with a concrete explanation
|
||||
and source/runtime evidence. A placeholder likewise needs an explicit future-UI
|
||||
gate, not a fabricated page inventory or an automatic green review.
|
||||
|
||||
## Work order and ownership
|
||||
|
||||
Prioritize user-visible defects, usability and consistent interactions before
|
||||
broader features. Start with Core/shared-shell contracts and Campaign's complete
|
||||
workflow, then Files, Mail, Templates, Notifications, Docs/Help, Dashboard, Quick
|
||||
Access and Views. The remaining queue is ordered by observed defects, shared
|
||||
dependencies and risk. This is a starting order, not a claim that those reviews
|
||||
have started or finished; the central issue records the current focus.
|
||||
|
||||
For Campaign, use a compact, read-only campaign settings overview with explicit,
|
||||
scoped edit dialogs. Large recipient or attachment grids may use the UI-02 bulk
|
||||
editing exception: clearly entered edit mode, explicit Save and Cancel, dirty
|
||||
navigation protection, and no silent loss or accidental save. This direction
|
||||
must be implemented and verified in the Campaign review, not marked complete by
|
||||
the program bootstrap.
|
||||
|
||||
Fix a repeated problem in the owning shared primitive or semantic contract first,
|
||||
then migrate consumers and verify representative actual-module fixtures. Keep
|
||||
module business behavior in its own repository and preserve optional module
|
||||
boundaries. Do not replace concrete user evidence with a generic fixture alone.
|
||||
Parallel reviewers may own different modules, but they must coordinate shared
|
||||
Core files and browser fixtures instead of racing the same resources.
|
||||
|
||||
## Per-module review workflow
|
||||
|
||||
1. **Inventory.** Verify and extend the issue's source-derived seed: navigation,
|
||||
pages, nested routes, dialogs, labels/forms, tables/trees, admin/system/tenant/
|
||||
user settings, public/operator interfaces, widgets and optional-module
|
||||
contributions. Follow actual module entrypoints and runtime contributions;
|
||||
filename heuristics and manifest routes are a starting point, not an exhaustive
|
||||
runtime audit. Note roles, permission boundaries and installed optional modules.
|
||||
2. **Observe.** Exercise realistic narrow and wide viewports, German and English,
|
||||
keyboard navigation and focus, empty/loading/error/success states, clean/dirty
|
||||
edit modes, reload, navigation away and return, and restoration of personal
|
||||
preferences. Include realistic row counts, long labels, horizontal overflow,
|
||||
pagination and two-way column resizing with fixed columns between text fields.
|
||||
3. **Record.** Keep a findings/TODO/done ledger in the module issue. Every finding
|
||||
names the surface, reproduction, expected principle, impact, owner and linked
|
||||
implementation/evidence. A source inventory, planned fix or green generic
|
||||
test is not a completed finding. State product decisions and manual checks
|
||||
explicitly with enough context to resolve them.
|
||||
4. **Implement.** Prefer reusable shared primitives and action/page contracts.
|
||||
Keep display mode readable and editing intentional. Preserve authorization,
|
||||
server validation, save/cancel behavior and data integrity. Never send mail,
|
||||
delete records, change live configuration or trigger other irreversible effects
|
||||
merely to obtain UI evidence; use authorized fixtures or a safe test context.
|
||||
5. **Verify.** Run proportionate unit, structure and actual-module browser tests.
|
||||
Record commands, results, source revision and safe runtime evidence. Update the
|
||||
owning module's manifest-driven EN/DE documentation for behavior changes; run
|
||||
`tools/checks/check-manifest-shapes.py` and relevant cross-module checks.
|
||||
6. **Conclude honestly.** Complete the principle matrix, evidence and manual
|
||||
checks. Link unresolved follow-ups and blockers; do not close a supposedly
|
||||
complete review while required work is still pending. Closing an issue requires
|
||||
explicit reviewed scope and the applicable design-principle revision.
|
||||
|
||||
## Principle ledger and back-propagation
|
||||
|
||||
Use the stable IDs from Core in each issue. The initial revision covers UI-01
|
||||
heading/label help placement; UI-02 display-first/scoped editing; UI-03 actions;
|
||||
UI-04 table/card geometry; UI-05 loading/error/progress; UI-06 tree interaction;
|
||||
UI-07 accessibility and German; UI-08 data integrity; and UI-09 propagation of
|
||||
revised principles. Core remains the authoritative definition.
|
||||
|
||||
Each module maintains this matrix, initially entirely pending:
|
||||
|
||||
| Principle/revision | Applicable surfaces / justified N/A | Applied / remaining work | Evidence | Exception / owner / follow-up |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| UI-01 … UI-09 | Pending inventory | Pending review | Not yet recorded | None approved |
|
||||
|
||||
Whenever a principle is added or changed:
|
||||
|
||||
1. Update Core's rule with its rationale, stable ID and revision/change reference.
|
||||
2. List **all already-reviewed modules**, not just the current implementation's
|
||||
consumers. Check whether the changed rule applies to each.
|
||||
3. Record unchanged conformance with evidence, or reopen the review / create a
|
||||
linked follow-up with owner, exact affected surfaces and required checks.
|
||||
4. Update the central epic's propagation ledger. Previously reviewed modules with
|
||||
outstanding applicable work are **follow-up required**, not silently green.
|
||||
5. Resolve the propagation sweep only after every affected module has current
|
||||
evidence or an explicit, owned and justified exception. Exceptions do not
|
||||
silently change the shared rule.
|
||||
|
||||
Suggested central ledger:
|
||||
|
||||
| Principle change | Already-reviewed modules checked | Conformant evidence | Reopened / follow-up required | Exceptions / owner | Sweep state |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Initial UI-01–UI-09 baseline | None reviewed at initialization | None | All module reviews pending | None approved | Pending |
|
||||
|
||||
## Safe inventory/bootstrap automation
|
||||
|
||||
`tools/gitea/gitea-ui-review-program.py` derives the current scopes from the
|
||||
catalog and source manifests, inventories routes and UI source entrypoints,
|
||||
then deduplicates issues by a stable hidden marker and normalized exact title.
|
||||
It includes closed issues in duplicate checks and never reopens, closes or
|
||||
overwrites existing module issues. Ambiguous or unmanaged duplicates stop the
|
||||
operation for review. Existing issue bodies, labels and review progress are
|
||||
preserved. A failed POST is not automatically retried; a subsequent run checks
|
||||
the marker again before deciding whether another create is needed.
|
||||
|
||||
Dry-run is the default. An authenticated dry-run performs only Gitea reads:
|
||||
|
||||
```sh
|
||||
.venv/bin/python tools/gitea/gitea-ui-review-program.py \
|
||||
--env-file /home/zemion/.config/gitea/gitea.env --epic 56
|
||||
```
|
||||
|
||||
Creating missing review issues requires explicit `--apply`. Initial population
|
||||
of the central epic's module-link block additionally requires
|
||||
`--initialize-links`; it is accepted only for the untouched bootstrap placeholder.
|
||||
Later runs must not rewrite checkboxes or human-maintained progress. Existing
|
||||
complete links are verified read-only. The token is loaded through the shared
|
||||
Gitea helpers and is never emitted in reports. A scoped `--ipv4` option works
|
||||
around host-specific IPv6 connectivity while retaining HTTPS certificate and
|
||||
hostname verification.
|
||||
|
||||
The tool emits a JSON result to stdout with source counts and issue links. The
|
||||
checked-in inventory is a reviewed initialization snapshot of that result;
|
||||
refreshing links does not authorize replacing live review status with the
|
||||
snapshot. Test the automation offline with:
|
||||
|
||||
```sh
|
||||
.venv/bin/python -m pytest tests/test_ui_review_program.py -q
|
||||
```
|
||||
Executable
+866
@@ -0,0 +1,866 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"snapshot_purpose": "Issue discovery links; live Gitea issues own review state and evidence.",
|
||||
"epic": {
|
||||
"repository": "govoplan",
|
||||
"number": 56,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan/issues/56"
|
||||
},
|
||||
"scope_count": 77,
|
||||
"manifest_modules": 72,
|
||||
"implemented_scopes": 73,
|
||||
"catalogued_placeholders": 4,
|
||||
"created": 77,
|
||||
"missing": 0,
|
||||
"issues": [
|
||||
{
|
||||
"scope_id": "core",
|
||||
"name": "Core / shared shell",
|
||||
"repository": "govoplan-core",
|
||||
"kind": "core",
|
||||
"ui_source_count": 111,
|
||||
"number": 301,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/301",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "access",
|
||||
"name": "Access",
|
||||
"repository": "govoplan-access",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 18,
|
||||
"number": 23,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-access/issues/23",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "addresses",
|
||||
"name": "Addresses",
|
||||
"repository": "govoplan-addresses",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 1,
|
||||
"number": 26,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-addresses/issues/26",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "admin",
|
||||
"name": "Admin",
|
||||
"repository": "govoplan-admin",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 9,
|
||||
"number": 11,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-admin/issues/11",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "approvals",
|
||||
"name": "Approvals",
|
||||
"repository": "govoplan-approvals",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 4,
|
||||
"number": 5,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-approvals/issues/5",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "assets",
|
||||
"name": "Assets",
|
||||
"repository": "govoplan-assets",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-assets/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "audit",
|
||||
"name": "Audit",
|
||||
"repository": "govoplan-audit",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 1,
|
||||
"number": 10,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-audit/issues/10",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "booking",
|
||||
"name": "Booking",
|
||||
"repository": "govoplan-booking",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-booking/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "calendar",
|
||||
"name": "Calendar",
|
||||
"repository": "govoplan-calendar",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 10,
|
||||
"number": 26,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-calendar/issues/26",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "campaigns",
|
||||
"name": "Campaigns",
|
||||
"repository": "govoplan-campaign",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 60,
|
||||
"number": 103,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/103",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "cases",
|
||||
"name": "Cases",
|
||||
"repository": "govoplan-cases",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 4,
|
||||
"number": 8,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-cases/issues/8",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "certificates",
|
||||
"name": "Certificates",
|
||||
"repository": "govoplan-certificates",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-certificates/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "committee",
|
||||
"name": "Committee",
|
||||
"repository": "govoplan-committee",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 3,
|
||||
"number": 5,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-committee/issues/5",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "connectors",
|
||||
"name": "Connectors",
|
||||
"repository": "govoplan-connectors",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 3,
|
||||
"number": 20,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/20",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "consultation",
|
||||
"name": "Consultation",
|
||||
"repository": "govoplan-consultation",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-consultation/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "contracts",
|
||||
"name": "Contracts",
|
||||
"repository": "govoplan-contracts",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-contracts/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "dashboard",
|
||||
"name": "Dashboard",
|
||||
"repository": "govoplan-dashboard",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 5,
|
||||
"number": 6,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-dashboard/issues/6",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "dataflow",
|
||||
"name": "Dataflow",
|
||||
"repository": "govoplan-dataflow",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 5,
|
||||
"number": 24,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/24",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "datasources",
|
||||
"name": "Datasources",
|
||||
"repository": "govoplan-datasources",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 1,
|
||||
"number": 10,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-datasources/issues/10",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "decisions",
|
||||
"name": "Decisions",
|
||||
"repository": "govoplan-decisions",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 2,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-decisions/issues/2",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "dist_lists",
|
||||
"name": "Distribution Lists",
|
||||
"repository": "govoplan-dist-lists",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 2,
|
||||
"number": 10,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-dist-lists/issues/10",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "dms",
|
||||
"name": "DMS",
|
||||
"repository": "govoplan-dms",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 3,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-dms/issues/3",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "docs",
|
||||
"name": "Docs",
|
||||
"repository": "govoplan-docs",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 2,
|
||||
"number": 23,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-docs/issues/23",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "encryption",
|
||||
"name": "Encryption",
|
||||
"repository": "govoplan-encryption",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 1,
|
||||
"number": 7,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-encryption/issues/7",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "erp",
|
||||
"name": "ERP",
|
||||
"repository": "govoplan-erp",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 2,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-erp/issues/2",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "evaluation",
|
||||
"name": "Evaluation",
|
||||
"repository": "govoplan-evaluation",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-evaluation/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "facilities",
|
||||
"name": "Facilities",
|
||||
"repository": "govoplan-facilities",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-facilities/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "files",
|
||||
"name": "Files",
|
||||
"repository": "govoplan-files",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 8,
|
||||
"number": 48,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/48",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "fit_connect",
|
||||
"name": "FIT-Connect",
|
||||
"repository": "govoplan-fit-connect",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 2,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-fit-connect/issues/2",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "forms",
|
||||
"name": "Forms",
|
||||
"repository": "govoplan-forms",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 2,
|
||||
"number": 7,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-forms/issues/7",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "forms_runtime",
|
||||
"name": "Forms Runtime",
|
||||
"repository": "govoplan-forms-runtime",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 6,
|
||||
"number": 7,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime/issues/7",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "grants",
|
||||
"name": "Grants",
|
||||
"repository": "govoplan-grants",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-grants/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "helpdesk",
|
||||
"name": "Helpdesk",
|
||||
"repository": "govoplan-helpdesk",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 1,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-helpdesk/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "identity",
|
||||
"name": "Identity",
|
||||
"repository": "govoplan-identity",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 1,
|
||||
"number": 5,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-identity/issues/5",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "identity_trust",
|
||||
"name": "Identity Trust",
|
||||
"repository": "govoplan-identity-trust",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 1,
|
||||
"number": 4,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-identity-trust/issues/4",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "idm",
|
||||
"name": "IDM",
|
||||
"repository": "govoplan-idm",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 3,
|
||||
"number": 14,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/14",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "inspections",
|
||||
"name": "Inspections",
|
||||
"repository": "govoplan-inspections",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-inspections/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "learning",
|
||||
"name": "Learning",
|
||||
"repository": "govoplan-learning",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-learning/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "mail",
|
||||
"name": "Mail",
|
||||
"repository": "govoplan-mail",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 6,
|
||||
"number": 25,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/25",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "mandates",
|
||||
"name": "Mandates",
|
||||
"repository": "govoplan-mandates",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 2,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-mandates/issues/2",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "notifications",
|
||||
"name": "Notifications",
|
||||
"repository": "govoplan-notifications",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 3,
|
||||
"number": 7,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-notifications/issues/7",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "ops",
|
||||
"name": "Ops",
|
||||
"repository": "govoplan-ops",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 2,
|
||||
"number": 5,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-ops/issues/5",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "organizations",
|
||||
"name": "Organizations",
|
||||
"repository": "govoplan-organizations",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 4,
|
||||
"number": 9,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-organizations/issues/9",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "parties",
|
||||
"name": "Parties",
|
||||
"repository": "govoplan-parties",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 2,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-parties/issues/2",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "payments",
|
||||
"name": "Payments",
|
||||
"repository": "govoplan-payments",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 3,
|
||||
"number": 3,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-payments/issues/3",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "permits",
|
||||
"name": "Permits",
|
||||
"repository": "govoplan-permits",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-permits/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "policy",
|
||||
"name": "Policy",
|
||||
"repository": "govoplan-policy",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 3,
|
||||
"number": 14,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-policy/issues/14",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "poll",
|
||||
"name": "Poll",
|
||||
"repository": "govoplan-poll",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 5,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-poll/issues/5",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "portal",
|
||||
"name": "Portal",
|
||||
"repository": "govoplan-portal",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 2,
|
||||
"number": 4,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-portal/issues/4",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "postbox",
|
||||
"name": "Postbox",
|
||||
"repository": "govoplan-postbox",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 4,
|
||||
"number": 29,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-postbox/issues/29",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "procurement",
|
||||
"name": "Procurement",
|
||||
"repository": "govoplan-procurement",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-procurement/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "projects",
|
||||
"name": "Projects",
|
||||
"repository": "govoplan-projects",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 1,
|
||||
"number": 4,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-projects/issues/4",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "quick_access",
|
||||
"name": "Quick Access",
|
||||
"repository": "govoplan-quick-access",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 2,
|
||||
"number": 3,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-quick-access/issues/3",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "records",
|
||||
"name": "Records",
|
||||
"repository": "govoplan-records",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 4,
|
||||
"number": 10,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-records/issues/10",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "reporting",
|
||||
"name": "Reporting",
|
||||
"repository": "govoplan-reporting",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 3,
|
||||
"number": 11,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-reporting/issues/11",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "resources",
|
||||
"name": "Resources",
|
||||
"repository": "govoplan-resources",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-resources/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "rest",
|
||||
"name": "REST Connector",
|
||||
"repository": "govoplan-rest",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-rest/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "risk_compliance",
|
||||
"name": "Risk Compliance",
|
||||
"repository": "govoplan-risk-compliance",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 1,
|
||||
"number": 10,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-risk-compliance/issues/10",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "scheduling",
|
||||
"name": "Scheduling",
|
||||
"repository": "govoplan-scheduling",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 4,
|
||||
"number": 10,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-scheduling/issues/10",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "search",
|
||||
"name": "Search",
|
||||
"repository": "govoplan-search",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 4,
|
||||
"number": 6,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-search/issues/6",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "services",
|
||||
"name": "Services",
|
||||
"repository": "govoplan-services",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 2,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-services/issues/2",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "soap",
|
||||
"name": "SOAP Connector",
|
||||
"repository": "govoplan-soap",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-soap/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "tasks",
|
||||
"name": "Tasks",
|
||||
"repository": "govoplan-tasks",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 3,
|
||||
"number": 5,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-tasks/issues/5",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "templates",
|
||||
"name": "Templates",
|
||||
"repository": "govoplan-templates",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 1,
|
||||
"number": 8,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-templates/issues/8",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "tenancy",
|
||||
"name": "Tenancy",
|
||||
"repository": "govoplan-tenancy",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 2,
|
||||
"number": 7,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-tenancy/issues/7",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "tickets",
|
||||
"name": "Tickets",
|
||||
"repository": "govoplan-tickets",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 1,
|
||||
"number": 3,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-tickets/issues/3",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "transparency",
|
||||
"name": "Transparency",
|
||||
"repository": "govoplan-transparency",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-transparency/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "views",
|
||||
"name": "Views",
|
||||
"repository": "govoplan-views",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 3,
|
||||
"number": 6,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-views/issues/6",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "voting",
|
||||
"name": "Voting",
|
||||
"repository": "govoplan-voting",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 2,
|
||||
"number": 8,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-voting/issues/8",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "wiki",
|
||||
"name": "Wiki",
|
||||
"repository": "govoplan-wiki",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 1,
|
||||
"number": 2,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-wiki/issues/2",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "workflow",
|
||||
"name": "Workflow",
|
||||
"repository": "govoplan-workflow",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 6,
|
||||
"number": 17,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-workflow/issues/17",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "workflow_engine",
|
||||
"name": "Workflow Engine",
|
||||
"repository": "govoplan-workflow-engine",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 4,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine/issues/4",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "xrechnung",
|
||||
"name": "XRechnung",
|
||||
"repository": "govoplan-xrechnung",
|
||||
"kind": "manifest",
|
||||
"ui_source_count": 0,
|
||||
"number": 3,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-xrechnung/issues/3",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "catalog:govoplan-appointments",
|
||||
"name": "Appointments",
|
||||
"repository": "govoplan-appointments",
|
||||
"kind": "placeholder",
|
||||
"ui_source_count": 0,
|
||||
"number": 2,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-appointments/issues/2",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "catalog:govoplan-ledger",
|
||||
"name": "Ledger",
|
||||
"repository": "govoplan-ledger",
|
||||
"kind": "placeholder",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-ledger/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "catalog:govoplan-xoev",
|
||||
"name": "Xoev",
|
||||
"repository": "govoplan-xoev",
|
||||
"kind": "placeholder",
|
||||
"ui_source_count": 0,
|
||||
"number": 2,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-xoev/issues/2",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
},
|
||||
{
|
||||
"scope_id": "catalog:govoplan-xta-osci",
|
||||
"name": "Xta Osci",
|
||||
"repository": "govoplan-xta-osci",
|
||||
"kind": "placeholder",
|
||||
"ui_source_count": 0,
|
||||
"number": 1,
|
||||
"url": "https://git.add-ideas.de/GovOPlaN/govoplan-xta-osci/issues/1",
|
||||
"state_at_verification": "open",
|
||||
"operation": "created"
|
||||
}
|
||||
],
|
||||
"snapshot_date": "2026-09-08",
|
||||
"initial_review_status": "pending for every scope; this snapshot does not track subsequent issue progress"
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# GovOPlaN 0.1.45 — usability, reliability and security hardening
|
||||
|
||||
Release coordination: [GovOPlaN #51](https://git.add-ideas.de/GovOPlaN/govoplan/issues/51).
|
||||
The exact independently versioned composition is recorded in
|
||||
`packages/govoplan-meta/pyproject.toml`; unchanged modules retain their versions.
|
||||
This source release does not by itself establish a deployed or independently
|
||||
approved production environment. Package, signed catalog and runtime publication
|
||||
results are recorded separately in the coordination issue.
|
||||
|
||||
## Runtime publication hold
|
||||
|
||||
The [runtime image audit](../security/RUNTIME_IMAGE_AUDIT_2026-09-08.md) completed
|
||||
eleven registry-only amd64 scans, but found unresolved vulnerabilities and
|
||||
inventory gaps. Runtime publication remains held separately from this source
|
||||
release. Patch-only image updates are insufficient; maintained minor-line
|
||||
changes, narrowly evidenced finding decisions, arm64/final-layer scans and
|
||||
deployment checks remain necessary. No audited candidate was automatically
|
||||
adopted and no image was executed during those scans.
|
||||
The remaining gates are tracked in
|
||||
[GovOPlaN #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52).
|
||||
|
||||
## Included changes
|
||||
|
||||
- Shared page/action placement, reusable navigation grouping/editing, table and
|
||||
dialog sizing, field alignment, multi-select filters and predictable tree
|
||||
selection. Files, Mail, Search, Notifications and domain pages use the same
|
||||
contracts, with browser regression coverage.
|
||||
- Campaign draft saving and independent Mail/ZIP-policy repair, persistent and
|
||||
bulk message review, clearer delivery eligibility, bounded configurable
|
||||
synchronous delivery, guarded workerless recovery, lightweight SMTP/IMAP
|
||||
progress, reused IMAP connections and recipient-complete reporting.
|
||||
- Files archive staging/reuse, unpacking previously uploaded archives, numeric
|
||||
progress and bounded traversal. Optional native archive acceleration retains
|
||||
the same validation rules; portable fallbacks remain available.
|
||||
- Mail credential references and IMAP folder-name decoding; help topics can be
|
||||
found by area and tags without expanding every occurrence of the same topic.
|
||||
- Authentication provenance/scope and browser-cache hardening, patched rich-text
|
||||
dependencies, spreadsheet/archive/template/Dataflow resource limits, batched
|
||||
Docs/Notifications queries and safe Reporting bind names. See the
|
||||
[security/performance review](../security/SECURITY_PERFORMANCE_REVIEW_2026-09-08.md)
|
||||
for measurements, test evidence and remaining limitations.
|
||||
- A deterministic governance-journey clock fixture, fresh-process Campaign
|
||||
import coverage, and a new Cases patch aligning its root npm facade with its
|
||||
Python/WebUI package. Historical published tags are not rewritten.
|
||||
- Git-root WebUI package facades are aligned with their owning packages, with
|
||||
a cross-composition parity check. Tasks is included in default module
|
||||
discovery; it remains subject to enabled modules and normal permissions.
|
||||
|
||||
## Upgrade and verification
|
||||
|
||||
Back up the database and file storage before upgrading. Apply the complete
|
||||
selected migration graph before starting the new API/workers. This release
|
||||
includes additive repair migrations `c58a2d7e9f10` (Core ownership history) and
|
||||
`d8f1b4e7a0c3` (Access external-function mappings), plus Campaign delivery-state
|
||||
migrations. Existing business evidence is retained; a schema downgrade is not
|
||||
a substitute for a reviewed backup/restore plan. Restart API and worker
|
||||
processes together after upgrading their matching packages.
|
||||
|
||||
Updated UI consumers require Core 0.1.45 where they use its new shared contracts.
|
||||
Tenant keys that previously relied on unintended system permissions/wildcards
|
||||
must be corrected; the release does not preserve that unsafe behavior. Extremely
|
||||
sparse spreadsheets, oversized generated output and excessive archive paths
|
||||
can now fail early with a diagnostic.
|
||||
|
||||
For archive staging across multiple hosts, provide shared POSIX storage with
|
||||
working locks or sticky routing. Background delivery still needs configured
|
||||
workers; increasing the synchronous limit does not create a worker or guarantee
|
||||
delivery after a process failure. An unknown SMTP outcome must be reconciled,
|
||||
not automatically resent.
|
||||
|
||||
After deployment, manually verify login/logout and least-privilege API keys,
|
||||
Campaign Settings and independent Mail/ZIP saves, archive upload/unpack,
|
||||
recipient-complete reports, and SMTP/IMAP progress with an explicitly approved
|
||||
test mailbox. No release verification sends real campaign mail automatically.
|
||||
|
||||
Hard process isolation, forced-password-change/recovery enforcement, bounded
|
||||
Xrechnung subprocess output and large-history pagination remain separate open
|
||||
issues. This release is not a claim that all security or performance debt is
|
||||
resolved. Production-image scans and multi-host evidence must refer to the
|
||||
actual signed runtime being deployed.
|
||||
@@ -0,0 +1,125 @@
|
||||
# Runtime image candidate audit — 8 September 2026
|
||||
|
||||
Release coordination: [GovOPlaN #51](https://git.add-ideas.de/GovOPlaN/govoplan/issues/51).
|
||||
Canonical remediation: [GovOPlaN #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52).
|
||||
This follow-up to the [source security/performance review](SECURITY_PERFORMANCE_REVIEW_2026-09-08.md)
|
||||
records registry-only scans of nine proposed runtime dependencies and two
|
||||
same-minor patch candidates. **Runtime publication is held:** patch-only updates
|
||||
do not resolve the baseline. Source/package publication is a separate outcome.
|
||||
No images were executed, rebuilt, selected for CI, or published by this audit.
|
||||
|
||||
## Method and reproducible evidence
|
||||
|
||||
Official Trivy **0.74.0** was installed only in a private local task directory,
|
||||
without sudo or Docker access. Its Linux-64bit release archive matched both the
|
||||
official checksums file and GitHub release asset metadata:
|
||||
|
||||
- Archive SHA256: `2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a`.
|
||||
- Checksums-file SHA256: `bc701c3c3ee8b9acbea2c23257e41381e3854888f51281616a6ba5dc96963821`.
|
||||
- Vulnerability database schema 2, updated `2026-09-07T19:06:01.154199452Z`,
|
||||
downloaded from `mirror.gcr.io/aquasec/trivy-db:2`.
|
||||
- Scan flags: `--image-src remote --platform linux/amd64 --scanners vuln
|
||||
--format json --no-progress --timeout 8m --max-image-size 2GB --exit-code 0`.
|
||||
Findings were counted from validated JSON; exit zero did not mean clean.
|
||||
- Existing Docker credentials were not read; no private keys or secrets were
|
||||
used. Checksums over official HTTPS metadata were verified, not independent
|
||||
Sigstore signatures. See the [official release](https://github.com/aquasecurity/trivy/releases/tag/v0.74.0)
|
||||
and [registry-only scan documentation](https://trivy.dev/docs/latest/target/container_image/).
|
||||
|
||||
Raw evidence is retained locally, not committed:
|
||||
`/home/zemion/.cache/govoplan-trivy-remote.yKZgjDOg/scan/`.
|
||||
It contains eleven `reports/*-amd64.json` reports/logs, scanner scripts,
|
||||
`patch-candidate-inspection.json`, exact successor registry indices, and
|
||||
`evidence-checksums.json`. Summary SHA256 values:
|
||||
|
||||
- `summary.json`: `f2785a731d637452ab9c0b1f5399772c0f8828a63ca83d5fa7496abdad1c757a`.
|
||||
- `patch-summary.json`: `b3fc6273fcdad98864040ccdf3477ecf379afd46e9f94444b1f2910f48c1d85b`.
|
||||
|
||||
All eleven executions succeeded without timeout/rate-limit failure. Initial
|
||||
summary fields distinguish `scan_execution_complete: true` from
|
||||
`coverage_complete: false`: Garage has no detectable package inventory.
|
||||
Checksums preserve evidence identity, not indefinite storage availability.
|
||||
|
||||
## Exact requested pins and results
|
||||
|
||||
All references below use `docker.io/`. Counts are package-vulnerability records,
|
||||
not distinct CVEs or confirmed exploitable application defects. A vulnerability
|
||||
can appear against several installed packages. Unfixed/unknown records remain.
|
||||
|
||||
| Image tag | Exact index SHA256 | Critical / High / Medium / Low / Unknown | Fixable C/H |
|
||||
| --- | --- | --- | ---: |
|
||||
| `library/python:3.12-slim-bookworm` | `782412e85d0f0984994c290652577d4018aff08145c85b262bb63dc0c7522254` | 5 / 55 / 102 / 103 / 5 | 0 |
|
||||
| `library/postgres:16-alpine` | `cf78e76683b9ca8c5733cbbdce6c9262b45b6767934dd0a95e671f9a0fc20685` | 1 / 30 / 28 / 14 / 1 | 31 |
|
||||
| `library/redis:7-alpine` | `ff02b58f971e7d7d156a1267e283fcbbeee91773b6aa36c49dac28ecfe28eadf` | 0 / 0 / 0 / 0 / 0 | 0 |
|
||||
| `nginxinc/nginx-unprivileged:1.29-alpine` | `0c79d56aee561a1d81c63f00eee5fb5fe29279560cdc55e91425133104c7fbe6` | 0 / 33 / 74 / 37 / 20 | 33 |
|
||||
| `library/haproxy:3.2.21-alpine` | `66e25cc9a8332635f4e897f7f4b1e5622c25f09f0ee23cddc6ce9bdb3a24772a` | 0 / 2 / 6 / 12 / 0 | 2 |
|
||||
| `library/caddy:2.10.2-alpine` | `4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d` | 7 / 75 / 67 / 37 / 4 | 82 |
|
||||
| `dxflrs/garage:v2.3.0` | `866bd13ed2038ba7e7190e840482bc27234c4afaf77be8cfa439ae088c1e4690` | **Unknown: no inventory** | — |
|
||||
| `greenmail/standalone:2.1.9` | `3ac5a83dd6727cf95e4d50e18907fb8ee7bbf5f67e8534714dee2fb1b5b2e1d4` | 0 / 0 / 116 / 35 / 0 | 0 |
|
||||
| `tonistiigi/binfmt:qemu-v10.2.3-68` | `400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0` | 0 / 9 / 2 / 1 / 1 | 9 |
|
||||
|
||||
## Patch-only options and limits
|
||||
|
||||
Complete publisher tag listings were inspected for nginx 1.29, Caddy 2.10,
|
||||
HAProxy 3.2, GreenMail 2.1 and binfmt qemu10.2. Two newer candidates were
|
||||
scanned; their registry index bytes matched both registry and publisher digests,
|
||||
and contained amd64 and arm64 manifests:
|
||||
|
||||
- `library/haproxy:3.2.23-alpine@sha256:6343ce34a132a5dceaa24767d739df2bd519f8f7c1079ae39e4821334e8eb42e`:
|
||||
same Alpine 3.24.1, 24 detected OS packages, zero reported findings. This is
|
||||
a useful candidate, not a completed compatibility test or application audit.
|
||||
- `greenmail/standalone:2.1.13@sha256:3df66b7edd01c8a301343ca5e3601d8674760d4708655573560c24745e624fb2`:
|
||||
upstream changes Ubuntu 22.04 to Debian 13.6; **3 C / 80 H / 98 M / 85 L /
|
||||
5 unknown**, 30 fixable C/H records. Not selected as a no-base-change update.
|
||||
- nginx's newest matching Alpine patch is already 1.29.8 at the scanned pin;
|
||||
Caddy 2.10 remains 2.10.2; binfmt qemu10.2 remains 10.2.3-68. No newer matching
|
||||
publisher images were found. The current [official Caddy image catalogue](https://raw.githubusercontent.com/docker-library/official-images/master/library/caddy)
|
||||
uses 2.11.4; switching minor lines requires new scans and compatibility checks.
|
||||
|
||||
Priority remediation: Caddy's own seven HIGH records require fixes through
|
||||
2.11.4, with additional bundled Go/library fixes that must be re-scanned;
|
||||
nginx's packages include curl/libcurl fixes through 8.22.0-r0, OpenSSL 3.5.8-r0,
|
||||
c-ares 1.34.8-r0, expat 2.8.1-r0 and libuuid 2.41.6-r1. PostgreSQL's OS records
|
||||
require OpenSSL 3.5.8-r0 and libuuid 2.42.3-r1; its CRITICAL plus 21 HIGH Go
|
||||
records concern the **gosu helper**, not PostgreSQL server code. binfmt's nine
|
||||
HIGH records concern its Go 1.26.4 build, with fixes through 1.26.6. Package
|
||||
presence does not establish vulnerable-symbol reachability. No unscanned tag
|
||||
is claimed to meet every fix requirement.
|
||||
|
||||
## Python triage and coverage caveats
|
||||
|
||||
Python image metadata identifies CPython 3.12.14, but Trivy inventories only
|
||||
Debian packages and pip, **not CPython/stdlib**. All 60 C/H records concern
|
||||
Debian packages: 21 CVEs, 50 `affected` records, 9 `fix_deferred`, 1
|
||||
`will_not_fix`, without a recorded fixed Bookworm version. Five util-linux CVEs
|
||||
repeat across eight binary packages. These remain installed; they are not all
|
||||
removed build dependencies. Pip 25.0.1 separately has five MEDIUM/one LOW
|
||||
records, with fixes through 26.2.0; it is install tooling, and the API image uses
|
||||
an offline `--no-index` wheelhouse rather than an arbitrary package index.
|
||||
|
||||
Narrow triage examples, **not blanket exemptions**:
|
||||
|
||||
- Debian states [CVE-2023-45853](https://security-tracker.debian.org/tracker/CVE-2023-45853)
|
||||
does not affect the built Bookworm zlib binaries because vulnerable minizip
|
||||
code is not included. Other bundled minizip implementations are separate.
|
||||
- [CVE-2026-8376](https://security-tracker.debian.org/tracker/CVE-2026-8376)
|
||||
explicitly requires 32-bit Perl; this scan targets amd64.
|
||||
- [CVE-2025-7458](https://security-tracker.debian.org/tracker/CVE-2025-7458)
|
||||
requires crafted arbitrary SQLite SQL; the managed runtime uses PostgreSQL,
|
||||
but alternate SQLite use must be reviewed.
|
||||
- Perl's regex and Archive::Tar records need exact binary/module applicability
|
||||
checks; vendor-deferred status alone is not a finding dismissal.
|
||||
|
||||
Only amd64 was scanned. arm64, newly built GovOPlaN API/Web layers and optional
|
||||
dependency combinations remain unverified. Garage has no inventory; Redis,
|
||||
HAProxy and PostgreSQL source-built executables, CPython and QEMU static
|
||||
binaries need supplemental SBOM/source coverage. Zero detected OS findings is
|
||||
not zero application vulnerabilities. Trivy also lacks Alpine 3.24 EOL metadata
|
||||
and nginx CVE-2026-80256 detail; unknowns are retained. There were no runtime,
|
||||
exploitability, secret, misconfiguration, malware or signature-policy checks.
|
||||
|
||||
Before lifting the runtime hold: approve and test maintained image-line changes
|
||||
where necessary, fix or narrowly disposition findings with evidence, close
|
||||
inventory gaps, scan both architectures and final runtime layers, then run
|
||||
deployment/ingress smoke checks. Do not silently change base OS, use unpinned
|
||||
`latest`, rebuild third-party images, or accept all HIGH/CRITICAL findings.
|
||||
@@ -0,0 +1,139 @@
|
||||
# Runtime image remediation follow-up — 8 September 2026
|
||||
|
||||
Canonical tracking: [Meta #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52)
|
||||
and [website #9](https://git.add-ideas.de/add-ideas/addideas-govoplan-website/issues/9).
|
||||
This addendum supplements the [original audit](RUNTIME_IMAGE_AUDIT_2026-09-08.md);
|
||||
it does not replace that historical baseline or lift either publication or
|
||||
deployment gate. The immutable 0.1.45 release and `catalog-v0.1.45` are unchanged.
|
||||
|
||||
## Source change and candidate decisions
|
||||
|
||||
New installer specifications now use
|
||||
`haproxy:3.2.23-alpine@sha256:6343ce34a132a5dceaa24767d739df2bd519f8f7c1079ae39e4821334e8eb42e`.
|
||||
This is a patch update from 3.2.21 within the supported
|
||||
[3.2 LTS branch](https://www.haproxy.org/), keeping Alpine 3.24.1.
|
||||
The [publisher's exact build source](https://github.com/docker-library/haproxy/blob/7a5c202cde713867a737033dca56e7a211a8b8df/3.2/alpine/Dockerfile)
|
||||
and scanned image configuration retain the non-root `haproxy` user,
|
||||
`/usr/local/etc/haproxy/haproxy.cfg`, entrypoint and graceful-stop signal.
|
||||
The [upstream changelog](https://www.haproxy.org/download/3.2/src/CHANGELOG)
|
||||
includes HTTP parsing, TLS and memory-safety fixes in 3.2.22/3.2.23.
|
||||
Existing specifications retain their explicit image, including an older pin;
|
||||
this source change does not update a running installation.
|
||||
|
||||
| Candidate | OS | C / H / M / L / Unknown, per architecture | Disposition |
|
||||
| --- | --- | --- | --- |
|
||||
| HAProxy `3.2.23-alpine` | Alpine 3.24.1 | 0 / 0 / 0 / 0 / 0 | Installer source default updated; binary/runtime checks pending |
|
||||
| nginx-unprivileged `1.30.4-alpine` | Alpine 3.24.1 | 0 / 0 / 0 / 0 / 0 | Candidate only; compatibility and website OS upgrade review pending |
|
||||
| Caddy `2.11.4-alpine` | Alpine 3.23.5 | 1 / 38 / 41 / 12 / 23 | Not selected; all 39 C/H records have recorded fixes |
|
||||
| Node `24-alpine` (24.20.0) | Alpine 3.24.1 | 0 / 6 / 11 / 12 / 0 | Not selected; major change and six fixable HIGH records |
|
||||
|
||||
Each row was scanned separately for **linux/amd64 and linux/arm64**, with the
|
||||
same counts on both. Counts are package-vulnerability records, not distinct
|
||||
CVEs or proven exploits. The [machine-readable evidence](runtime-image-candidates-2026-09-08.json)
|
||||
contains exact index, platform-manifest, config and report digests, inventory
|
||||
counts, scanner bounds and decisions. It is audit data, not an accepted release
|
||||
manifest or an installer input.
|
||||
|
||||
nginx's candidate reference is
|
||||
`docker.io/nginxinc/nginx-unprivileged:1.30.4-alpine@sha256:442753882674b49ae2c1de83ed67896131c0777f56df5005e356e62bc3f7e7ce`.
|
||||
It inventories 70 Alpine packages including nginx/NJS, uses UID 101 and exposes
|
||||
8080. The [upstream stable release](https://nginx.org/en/download.html) and
|
||||
[security advisories](https://nginx.org/en/security_advisories.html) include the
|
||||
1.30.4 fixes. The publisher retains its
|
||||
[unprivileged port and temporary-path contract](https://github.com/nginx/docker-nginx-unprivileged).
|
||||
For the website, this changes nginx 1.27.5 to 1.30.4, NJS 0.8.10 to 1.0.1 and
|
||||
Alpine 3.21.3 to 3.24.1. These changes are explicit review items; the website
|
||||
Dockerfile has not been changed. The GovOPlaN Web image still requires an
|
||||
explicit verified `NGINX_IMAGE` build argument.
|
||||
|
||||
Caddy's candidate reference is
|
||||
`docker.io/library/caddy:2.11.4-alpine@sha256:5f5c8640aae01df9654968d946d8f1a56c497f1dd5c5cda4cf95ab7c14d58648`.
|
||||
Although this is the current
|
||||
[official image line](https://raw.githubusercontent.com/docker-library/official-images/master/library/caddy),
|
||||
its inventory still includes Go 1.26.3, `x/crypto` 0.52.0, `x/net` 0.55.0,
|
||||
`x/text` 0.37.0 and gRPC 1.81.0. Recorded fixes include Go 1.26.6,
|
||||
`x/crypto` 0.55.0, `x/net` 0.56.0, `x/text` 0.39.0 and gRPC 1.83.1; Alpine
|
||||
findings also remain in c-ares, curl/libcurl and OpenSSL. The CRITICAL
|
||||
`CVE-2026-56854` concerns `x/crypto/ssh` source-address enforcement. A module
|
||||
record alone does not establish that this binary exposes that SSH path; exact
|
||||
binary symbol/reachability analysis is still required for a disposition.
|
||||
|
||||
The [official Node image catalogue](https://raw.githubusercontent.com/docker-library/official-images/master/library/node)
|
||||
still maps Node 22 Alpine to 22.23.2 and the previously scanned digest. Node 24's
|
||||
candidate is
|
||||
`docker.io/library/node:24-alpine@sha256:e67514e5d0f6c46656005e1b693b2ec9d52e80b641307de684d4a015ba7a4eaf`.
|
||||
Its HIGH records remain in two OpenSSL packages and npm dependencies
|
||||
`brace-expansion`, `ip-address` and `tar`; fixing the earlier critical tar
|
||||
record alone is insufficient. The website builder stays on Node 22 pending
|
||||
a reviewed build-tool remedy and a final builder scan.
|
||||
|
||||
## Method, verification and retained evidence
|
||||
|
||||
The existing Trivy 0.74.0 executable was rehashed against the previously verified
|
||||
archive member: `d89bcc6510a267f11b773398cbf1be5520ce39f9e8b6633178c4487f05b7d791`.
|
||||
The same schema-2 vulnerability database was used, updated
|
||||
`2026-09-07T19:06:01.154199452Z`. No tool installation or database refresh occurred.
|
||||
Index bytes matched both the registry digest header and Docker Hub publisher
|
||||
metadata; both platform-manifest byte hashes matched the index. All eight
|
||||
registry-only scans completed successfully with validated JSON, `--list-all-pkgs`,
|
||||
`--scanners vuln`, an eight-minute/2GB image bound, an empty Docker configuration
|
||||
and no inherited credentials. Exit zero means execution succeeded. Private
|
||||
temporary paths and in-memory artifact cache isolated this follow-up from the
|
||||
earlier scanner's artifact cache; its vulnerability database was read only.
|
||||
|
||||
Raw reports, logs, manifests, publisher metadata and the scanner script are in
|
||||
`/home/zemion/.cache/govoplan-runtime-remediation.qfDSWHJh/`:
|
||||
|
||||
- `summary.json` SHA-256: `0d44390408ab35270e4430516f77bf11aa7877334eff2ef19e11e9a863fe5c56`.
|
||||
- `frozen-images.json` SHA-256: `d0cb156f4a88998531ec55ab950067a3f1350ded648f07650c463af101dad467`.
|
||||
- `scan_successors.py` SHA-256: `f64c69e06efc2ad7b5a657a25aa73f9ff586e3685737d525456bcecbe3ab5f07`.
|
||||
|
||||
Local retention is not permanent artifact hosting; preserve this evidence with
|
||||
the eventual reviewed release. The JSON evidence records compressed registry
|
||||
layer sizes; these are not expanded filesystem limits or final GovOPlaN sizes.
|
||||
|
||||
Installer regression checks cover the new generated image pin, legacy
|
||||
specification fallback, preserved explicit images, generated topology and
|
||||
configuration: `python -I -m unittest discover -s tests -p
|
||||
test_deployment_installer.py` ran 45 tests successfully with one skip because
|
||||
Core was not importable in that isolated test environment. The skipped Core
|
||||
startup-configuration integration was subsequently rerun in the shared development
|
||||
environment with Core available: all 45 installer tests passed with no skips,
|
||||
including generated-environment startup validation. This is configuration
|
||||
validation, not execution of the candidate image.
|
||||
Both repositories passed `git diff --check`; the audit JSON and all eight
|
||||
report hashes were checked against the retained evidence.
|
||||
**Docker, Podman and HAProxy executables are unavailable on
|
||||
this host**, so no image or HAProxy configuration was executed and no daemon was
|
||||
installed. Publisher metadata and installer tests support the scoped source
|
||||
patch; they do not establish binary or deployed compatibility.
|
||||
|
||||
## Gates that remain open
|
||||
|
||||
- Validate `haproxy -c` on generated local, existing-proxy and managed-ingress
|
||||
configurations using the exact pinned image and target architectures. Run
|
||||
bounded isolated checks without live mounts, secrets, privilege or external
|
||||
network access. Then verify DNS discovery, readiness, forwarded headers,
|
||||
replica routing and graceful termination in the intended runtime.
|
||||
- Test the nginx candidate with both the website configuration and GovOPlaN
|
||||
WebUI entrypoint/proxy configuration, including UID 101, writable temporary
|
||||
paths, health paths, cache headers and static catalog bytes. Approve the
|
||||
website nginx/NJS/Alpine version changes before changing its Dockerfile.
|
||||
- Resolve Caddy, Node build-tool and all unchanged baseline dependencies with
|
||||
updated publisher images or narrow reviewed applicability evidence. No
|
||||
severity-wide exceptions or custom third-party rebuilds were introduced.
|
||||
- Close the original source-built/static inventory gaps. HAProxy's 24-package
|
||||
OS inventory still omits the source-built HAProxy executable. Node's npm
|
||||
inventory still omits the Node executable/stdlib. Garage, CPython, Redis,
|
||||
PostgreSQL and QEMU gaps are unchanged. Alpine 3.24 EOL metadata is still
|
||||
missing from this scanner; zero findings is not complete coverage.
|
||||
- Scan **final built** API/Web/website layers and the selected managed
|
||||
dependencies on both architectures, then perform migration, worker,
|
||||
readiness and ingress smoke checks. Record failure and unknown states.
|
||||
Secrets, misconfiguration and image signature policy need separate checks.
|
||||
- Obtain the website deployment host/operator and rebuild/restart authority,
|
||||
preserving the exact immutable catalog/keyring/module-directory bytes and
|
||||
verifying fresh public responses after an authorized rollout.
|
||||
|
||||
No images were built, executed, published or deployed; no running service,
|
||||
release tag, signed manifest, CI image input or live infrastructure was changed.
|
||||
@@ -0,0 +1,219 @@
|
||||
# Security and performance follow-up — 8 September 2026
|
||||
|
||||
This follows the [original review](SECURITY_PERFORMANCE_REVIEW_2026-09-08.md)
|
||||
and its post-release issue reconciliation. It describes new source work after
|
||||
the frozen 0.1.45 release; it does not change published tags, packages, signed
|
||||
catalogs or deployed images. Gitea remains the canonical state log.
|
||||
|
||||
## Implemented source slices
|
||||
|
||||
- [Core #297](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/297):
|
||||
a shared disposable-process runner enforces wall/CPU/address-space/input/output
|
||||
limits, bounded stderr, process-group cleanup and non-queuing per-process
|
||||
admission. A private binary codec bounds decoding before allocating a full
|
||||
object graph and preserves explicitly supported data types without pickle.
|
||||
Read the owning Core `docs/BOUNDED_PROCESS_CONTRACT.md` before adding callers.
|
||||
- Connectors XLSX parsing, Templates rendering, Files ZIP/TAR inspection and
|
||||
extraction, and Dataflow reference previews/development execution now use
|
||||
that boundary. Existing authorization, sessions, provider credentials,
|
||||
idempotency and persistence remain in the parent. No unprotected inline
|
||||
fallback is used. Each module contributes static EN/DE user/admin limits and
|
||||
operational consequences through its manifest.
|
||||
- Files snapshots authorized sources inside shared admission, validates private
|
||||
staged members, and acknowledges each persisted member before decoding the
|
||||
next. Numeric progress remains available. The acknowledgement is event-driven,
|
||||
not a fixed sleep per member. Reads allocate by validated actual file size,
|
||||
not by the configured ceiling. Failures reap children, clear private staging
|
||||
and retain the existing transaction/blob cleanup and explicit retry behavior.
|
||||
- Dataflow's normal reference preview formerly bypassed the backend wrapper;
|
||||
it now enters the worker too. Nested source configurations cannot collide
|
||||
merely because subflows reuse node IDs. Combined reference-source data is
|
||||
checked before creating further columnar copies, while individual providers
|
||||
retain their own authorized-read bounds. Staging/production still require
|
||||
DuckDB; this change does not replace that separate backend.
|
||||
- [Access #22](https://git.add-ideas.de/GovOPlaN/govoplan-access/issues/22):
|
||||
current-password change, session/CSRF rotation, cross-tenant session and human
|
||||
API-key revocation, and optional administrator-assisted recovery. Recovery
|
||||
codes are hashed, single-use, expire after 15 minutes, require a current local
|
||||
System owner and explicit identity verification, and recheck current account,
|
||||
membership, tenant and issuer authority at redemption. A password change also
|
||||
invalidates outstanding codes issued by that account for other people. Audit
|
||||
evidence and validation/error responses do not contain passwords or codes.
|
||||
External-provider and service-account rules remain separate.
|
||||
- The Access UI provides first-login/required change, self-service change,
|
||||
policy-aware sign-in help, public code redemption and eligible owner issuance.
|
||||
Core consumes an optional lazy auth-action capability rather than importing
|
||||
Access internals. The required-action gate fails closed if its UI is missing.
|
||||
- [Workflow Engine #3](https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine/issues/3):
|
||||
full-history lists batch pinned revisions, while new summary and bounded
|
||||
step/event endpoints preserve authorization and explicit pagination. Existing
|
||||
full-history responses are not silently truncated. Exact inbox total semantics
|
||||
are retained and their counting cost is documented.
|
||||
- [Meta #55](https://git.add-ideas.de/GovOPlaN/govoplan/issues/55):
|
||||
shared version/planning helpers recognize the existing nested developer
|
||||
package, not invented root metadata. All tag batches enforce trusted private
|
||||
source ownership, registered origins and clean main/upstream state. Meta
|
||||
batches additionally require exact composition and matching Core evidence.
|
||||
Whole-batch preflight,
|
||||
frozen source receipts, annotated immutable tags, object-pinned atomic
|
||||
publication and post-effect remote checks are covered with temporary local
|
||||
repositories. Hidden Git index flags, unsafe ancestry, alternates and changed
|
||||
Git-directory identities are rejected. Selected version/composition metadata
|
||||
must be tracked, so ignored files cannot describe bytes absent from a tag.
|
||||
Applicable unselected Core WebUI inputs have bounded, frozen read receipts;
|
||||
backend-only releases do not read them. The existing local module-candidate
|
||||
exception remains intact. The weaker legacy mutation path was removed.
|
||||
Canonical whole-package preview and receipt-bound apply now cover Meta's
|
||||
version preparation too. Core must already match the target. Preparation
|
||||
requires a separate trusted checkout, explicit out-of-run confirmation and
|
||||
unchanged source/tooling receipts; it cannot rewrite the running operator.
|
||||
Plans place Meta after Core and explain the manual preparation/publication
|
||||
steps instead of promising a durable self-update. Ambiguous partial writes
|
||||
require reconciliation, without automatic rollback or retry.
|
||||
- [Runtime-image follow-up](RUNTIME_IMAGE_REMEDIATION_2026-09-08.md): eight new
|
||||
registry-only scans cover four exact candidates on amd64 and arm64. New
|
||||
installer specifications select the patched same-line HAProxy digest;
|
||||
existing specifications retain their explicit image. Other candidates and
|
||||
unresolved inventory/deployment gates remain visible, not blanket-approved.
|
||||
|
||||
## Verification record
|
||||
|
||||
Targeted checks include actual child execution, catastrophic regex CPU,
|
||||
aggregate memory exhaustion, TAR extension metadata, noisy output, malformed
|
||||
transport/staging data, Unicode allocation limits, cancellation/callback
|
||||
failures, descendant cleanup, rollback and explicit retries. Local mixed-owner
|
||||
composition tests completed nine real children, rejected six overlapping
|
||||
requests as busy, observed at most one unreaped child and recovered all slots.
|
||||
This is local admission evidence, not a target deployment load certification.
|
||||
|
||||
Workflow fixtures serialize 40 different pinned revisions with five SQL reads;
|
||||
summary lists use one query for 40 ordinary rows. Exact inbox totals for
|
||||
40/400/4,000 candidates used one query, with measured local costs approximately
|
||||
0.011/0.057/0.492 seconds. These are fixture measurements, not production SLOs.
|
||||
|
||||
The broader Core API smoke suite exposed three stale campaign assertions.
|
||||
All three failures were reproduced against the unchanged private frozen 0.1.45
|
||||
sources. Updated fixtures verify recipient-summary projection, detailed payload
|
||||
separation and explicit fenced recovery of a confirmed stopped runtime; observing
|
||||
SENDING alone must not make a claim recoverable. All 76 smoke tests then passed.
|
||||
No production Campaign behavior was changed to satisfy these tests.
|
||||
|
||||
The final release-tool suite passed 279 tests and 68 subtests, including
|
||||
temporary local remotes and adversarial source/tag/receipt changes. Rechecking
|
||||
the whole batch before effects is deliberately conservative: its repeated
|
||||
filesystem/Git/remote work grows quadratically with batch size. It is not a
|
||||
new unattended publication path or permission to execute unreviewed source.
|
||||
|
||||
Strict interface inventory now reports no unclassified endpoints and exact
|
||||
contextual help for all 133 high-risk controls. Seventeen password browser cases
|
||||
include actual F1 help from the restricted screen, empty workspace scopes,
|
||||
EN/DE layouts, Unicode boundaries and no credential values in help URLs.
|
||||
The initial production bundle remains within the unchanged limits (512,036
|
||||
raw bytes and 162,415 gzip bytes; 1,713 gzip bytes below its ceiling), with
|
||||
46 optional descriptors and no eager optional-module imports.
|
||||
|
||||
The focused checker now includes the new Core process, mixed-owner admission,
|
||||
Access password, Templates and Files worker tests, the repaired campaign smoke
|
||||
cases, and browser-side auth/password transport contracts. The full focused run
|
||||
passed, including 63 production module/build permutations and all 230 browser
|
||||
cases. Its two opt-in Datasources PostgreSQL cases were skipped in that run
|
||||
and subsequently passed against the isolated real database described below.
|
||||
The final Meta preparation gate was added after that full run and verified
|
||||
with the owning release-tool suite and the focused release-gate command.
|
||||
Manifest validation passed for all 72 modules. The full focused log is
|
||||
`/mnt/DATA/tmp/govoplan-security-followup-20260908-focused.log`.
|
||||
|
||||
The first follow-up quick audit captured an unchanged 79-repository snapshot
|
||||
in `/mnt/DATA/tmp/govoplan-security-followup-quick-20260908-7s8Sgh/`.
|
||||
All four required scanners completed, with zero missing/execution reports;
|
||||
all 168 report checksums and 163 machine-readable reports were validated.
|
||||
Gitleaks found no secrets in all 79 histories and 79 worktrees. Local Semgrep
|
||||
rules reported zero findings. Production Bandit reported 65 low and four medium
|
||||
warnings, and production Ruff retained 54 warnings. The two added Bandit
|
||||
warnings identify the new Core subprocess import and invocation: trusted
|
||||
server-owned arguments, no shell, and the documented resource/process boundary
|
||||
were reviewed; warnings remain visible. This is report-only evidence, not a
|
||||
warning-free audit or a penetration test. A final snapshot follows the
|
||||
cross-module declaration/contextual-help corrections and release-tool checks.
|
||||
|
||||
That final audit completed on 8 September, 05:59:46–06:02:18 UTC, in
|
||||
`/mnt/DATA/tmp/govoplan-security-final-quick-20260908-vAwQIh/`. All 79 start/end
|
||||
source fingerprints were identical; all four scanners completed, all 168
|
||||
registered report checksums matched, and all 163 JSON/SARIF reports parsed.
|
||||
There were no missing reports or scanner execution errors. Semgrep and both
|
||||
Gitleaks scopes again reported zero findings. Production counts were unchanged
|
||||
from the first follow-up: Bandit 65 low/four medium and Ruff 54. Test-only
|
||||
counts were Bandit 140 low/34 medium and Ruff 136. A separate frozen scan of
|
||||
all ten changed Meta release/deployment Python files reported seven low Bandit
|
||||
and four Ruff S603 warnings, with no execution errors. Its four argv-only
|
||||
subprocess sites were reviewed; the preparation additions introduced no new
|
||||
warnings. No findings were hidden or severity-wide exceptions added.
|
||||
The audit manifest SHA-256 is
|
||||
`a997b786239cd11443cb665d5f9041a968cc38f9d49171e68bb868bf2bd73310`;
|
||||
its report-checksum list SHA-256 is
|
||||
`dc590ca5b0a4e445019a05536d410226088d67b40d61cd7657bdef4a4eae56d8`.
|
||||
|
||||
The audit includes the eight committed feature/website source changes and the
|
||||
final uncommitted Meta source. Only this evidence document was updated after
|
||||
the source freeze ended; the final Meta commit and remote publication are
|
||||
recorded in the linked Gitea issues, not inferred from local audit completion.
|
||||
|
||||
Fresh dependency audits are retained in
|
||||
`/mnt/DATA/tmp/govoplan-dependency-final-20260908-d24LEK/`: all four full npm
|
||||
lockfile audits (Core WebUI, Mail root/WebUI and website) report zero known
|
||||
vulnerabilities. Installed Python auditing covers 137 distributions with zero
|
||||
known vulnerabilities; 51 local GovOPlaN distributions lack PyPI advisory
|
||||
coverage. Core's 46 linked packages are likewise not claimed covered by public
|
||||
registry advisories. All 12 dependency-file hashes and the installed inventory
|
||||
were unchanged. No packages were installed or automatically fixed.
|
||||
|
||||
Managed PostgreSQL 16.15 fixtures used private Unix sockets, synthetic roles
|
||||
and databases, no TCP listener, per-case schemas and bounded SQL/lock waits.
|
||||
Both previously skipped Datasources races passed. Twenty-one existing Access
|
||||
password HTTP tests and four additional races passed on PostgreSQL: single-use
|
||||
redemption, stale-session/password replacement, competing issuance, and issuer
|
||||
password revocation during redemption. Four release/development migration checks
|
||||
also passed for Access and Workflow, including credential preservation and
|
||||
idempotent indexes. The four races are now owning opt-in Access regressions;
|
||||
see `govoplan-access/docs/PASSWORD_RECOVERY_POSTGRES_TESTS.md`. These local
|
||||
database checks do not certify a deployment, fleet load or external recovery
|
||||
handover. With both explicit PostgreSQL test URLs enabled, the full Access suite
|
||||
passed 122 tests and 18 subtests, and the full Datasources suite passed 57 tests,
|
||||
without skips. Access retained 12 existing SQLite datetime-adapter warnings in
|
||||
its separate SQLite migration cases. Both temporary PostgreSQL fixtures were
|
||||
stopped and independently verified: no server process, private socket,
|
||||
generated schema or synthetic cluster remains. Scripts, logs and shutdown
|
||||
receipts are retained under `/home/zemion/.cache/govoplan-pg-security-20260908.RAUitg/`
|
||||
and `/home/zemion/.cache/govoplan-pg-promoted-20260908.JZcIKL/`.
|
||||
|
||||
## Adoption and remaining gates
|
||||
|
||||
1. `AUTH_LOCAL_PASSWORD_RECOVERY_ENABLED` remains **false** by default. The
|
||||
existing flag is still advisory until an operator explicitly adopts and
|
||||
enables the complete recovery policy. Confirm who verifies identity and how
|
||||
the one-time code is handed over; automated email recovery is not enabled.
|
||||
Test first-login, lost-password, code expiry and administrator availability
|
||||
in the target environment before enforcement.
|
||||
2. Access migration `e9a2c5f8b1d4` adds recovery evidence; Workflow migration
|
||||
`9e6b3f8a2c7d` adds summary-pagination indexes. Use normal backed-up upgrade
|
||||
procedures and account for index-build cost. No manual live migration or
|
||||
server restart was performed during this work. The user's existing devserver
|
||||
has automatic reload, so live schema state must not be assumed unchanged.
|
||||
3. Release preparation must assign new source/package versions and require a
|
||||
Core version containing the new worker/auth contracts in the affected module
|
||||
metadata, including matching WebUI assets. The old immutable release must
|
||||
not be relabelled or treated as containing these APIs.
|
||||
4. Resource limits are not an arbitrary-code, filesystem or network sandbox.
|
||||
Admission is per API/worker process, not fleet-wide. Validate Linux/cgroup
|
||||
memory, disk quotas, process counts, cancellation and legitimate large-file
|
||||
workloads on the intended runtime before increasing concurrency. Core #297
|
||||
retains this target-evidence follow-up.
|
||||
5. Meta #52 and website #9 retain runtime-image/publication/deployment holds.
|
||||
Docker/Podman/HAProxy executables are unavailable here. Final built images,
|
||||
binary/source inventories, ingress behavior, migration/readiness/worker
|
||||
smoke checks and the website's target/operator authority remain outstanding.
|
||||
Zero findings in a detected package inventory is not full image coverage.
|
||||
|
||||
No real messages, IMAP appends, password resets, provider operations or deployment
|
||||
actions were used as test fixtures. Development tests use temporary databases,
|
||||
private temporary files, mock transports and managed test-browser servers.
|
||||
@@ -0,0 +1,207 @@
|
||||
# Security and performance review — 8 September 2026
|
||||
|
||||
Coordinated status: [Core #296](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/296).
|
||||
This records a workspace-wide automated scan, targeted manual boundary review,
|
||||
and a verified implementation pass. It is not a penetration test, an exhaustive
|
||||
line-by-line review, or a security certification. The audit was completed on
|
||||
local, unpublished changes, preserving existing worktree changes. Subsequent
|
||||
release preparation/publication is tracked in
|
||||
[GovOPlaN #51](https://git.add-ideas.de/GovOPlaN/govoplan/issues/51) and the
|
||||
[0.1.45 release notes](../releases/0.1.45.md).
|
||||
|
||||
## Implemented findings
|
||||
|
||||
| Area | Finding and change | Evidence / ownership |
|
||||
| --- | --- | --- |
|
||||
| Authentication — high | Preserve service-account provenance and current scope ceilings instead of recalculating them as ordinary membership permissions. Tenant API keys cannot retain canonical system permissions or unsafe wildcard grants. | Previously failing isolated regressions; [Access #21](https://git.add-ideas.de/GovOPlaN/govoplan-access/issues/21). |
|
||||
| Authentication — medium | Warm-cache API keys must follow the same explicit-header credential rules as cold authentication. A session cookie cannot turn an API key into a session credential. | Regression covering source-dependent authentication. |
|
||||
| Browser authority/cache — medium | Clear reusable data on auth changes and write settlement; fence late 200/304 writes and obsolete 401 side effects. Honor server no-store/no-cache and explicit fresh-read requests. Interactive login/logout remove retained automation keys that could shadow cookie-session identity. | 23 real-client regressions. Unchanged settings keep their object identity, preventing profile-fetch loops. Core `docs/API_CLIENT_CACHE_CONTRACT.md`; owning Access EN/DE session/field documentation. |
|
||||
| Spreadsheet resource exhaustion | Validate actual XLSX coordinates before openpyxl traversal; ignore misleading declared dimensions; count blank row gaps toward the existing limits. | [Connectors #18](https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/18), 13 tests and 2 subtests. |
|
||||
| Template resource exhaustion | Enforce the existing 5 MiB output budget during substitution and item construction, including UTF-8, HTML escaping and separators. | [Templates #7](https://git.add-ideas.de/GovOPlaN/govoplan-templates/issues/7), full 20 tests; independent 3,000-case valid-output comparison. |
|
||||
| Archive resource exhaustion | Inspect regular TAR member limits before traversing payloads. Limit archive paths to 4,096 UTF-8 bytes / 128 components and count derived directories against entry limits. | [Files #46](https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/46), 55 archive and 15 documentation tests. Extension-header decoding still needs stronger isolation. |
|
||||
| Dataflow resource exhaustion | Reject LPAD/RPAD target lengths above the existing 1,000,000-byte preview budget before fill evaluation/allocation. Preserve final serialized-byte checks. | [Dataflow #22](https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/22), full 104 tests and 39 subtests; 7 new guard tests independently rerun. |
|
||||
| Docs performance / defense in depth | Batch revision reads per request, avoid loading pending draft bodies for readers, and validate tenant/entry/publication consistency while retaining owner/audience checks. | [Docs #22](https://git.add-ideas.de/GovOPlaN/govoplan-docs/issues/22), full 39 tests. |
|
||||
| Notifications performance / defense in depth | Batch delivery-attempt loading while preserving recipient checks and rejecting inconsistent attempt references, including already-loaded relationships. | [Notifications #6](https://git.add-ideas.de/GovOPlaN/govoplan-notifications/issues/6), full 23 tests. |
|
||||
| Session-list performance | Apply active/expiry predicates and the existing 100-row cap in SQL, before loading session history. | Query-shape regression in Access. |
|
||||
| Reporting correctness | Use structural bind-name suffixes for recursive calculated measures, preserving valid dotted/hyphenated public keys and parameter uniqueness. | [Reporting #10](https://git.add-ideas.de/GovOPlaN/govoplan-reporting/issues/10), full 29 tests. |
|
||||
| Audit hygiene | Redact Gitleaks logs and machine reports on current, history and legacy scanner paths. | 13 audit-wrapper tests enforce the flag. |
|
||||
|
||||
All changed module workflows/limits have owning EN/DE DocumentationTopic updates.
|
||||
Independent review found no concrete regression in the backend changes.
|
||||
|
||||
## Measured performance changes
|
||||
|
||||
These are SQL-query counts in isolated 40-item fixtures, not production latency
|
||||
or throughput claims. Authorization is still evaluated for each request.
|
||||
|
||||
| Projection | Before | After |
|
||||
| --- | ---: | ---: |
|
||||
| Docs reader entries | 41 SELECTs | 2 SELECTs |
|
||||
| Docs editor entries | 81 SELECTs | 2 SELECTs |
|
||||
| Notification list with attempts | 41 SELECTs | 2 SELECTs |
|
||||
|
||||
The Docs 401-entry batching regression uses 3 SELECTs. Resource guards reject
|
||||
oversized work before the formerly expensive allocation/traversal. This does
|
||||
not make every legitimate upload or campaign faster. Honoring no-cache can
|
||||
increase server validation requests; ETags still avoid retransmitting unchanged
|
||||
bodies. That authorization/freshness trade-off is deliberate.
|
||||
|
||||
The original audit snapshot measured 517,380 initial JavaScript bytes and
|
||||
164,119 gzip bytes. Release preparation's pure-defaults split reduces this to
|
||||
516,730 initial bytes and 163,908 gzip bytes. Restoring the missing Tasks
|
||||
descriptor then measures 516,987 initial / 163,976 gzip bytes with all 46 module
|
||||
descriptors lazy, within the unchanged 524,288 / 164,128 caps. The gzip margin is still small; future
|
||||
startup work should reduce eager dependencies, not raise the cap automatically.
|
||||
The full 209-case browser suite passed before the split, followed by 13 focused
|
||||
browser checks after it. Radon recorded 238 rank-D-or-higher entries; complexity
|
||||
is a review-priority signal, not a performance measurement.
|
||||
|
||||
## Dependency remediation
|
||||
|
||||
Core's full npm audit went from 30 affected package entries to zero. Most initial
|
||||
entries were transitive effects of the same Tiptap advisory, not 30 independent
|
||||
application exploits. The website went from two affected entries to zero; both
|
||||
Mail lockfiles also report zero.
|
||||
|
||||
- Tiptap packages are aligned at 3.31.3, with direct minimum ranges raised to
|
||||
3.30.4 in both development and release manifests, with a parity regression.
|
||||
Added an actual installed-library prototype-attribute regression for
|
||||
the [maintainer's security advisory](https://github.com/ueberdosis/tiptap/security/advisories/GHSA-cp6q-959q-f8rh).
|
||||
- Core now resolves xmldom 0.9.12, browserslist 4.28.9 and nanoid 3.3.18.
|
||||
The website's affected browserslist/nanoid dependencies are patched too.
|
||||
- Development/audit requirements now require pip >=26.2; the local development
|
||||
environment uses 26.2.1. The installed audit originally flagged
|
||||
[CVE-2026-13346](https://github.com/advisories/GHSA-qwm4-qh6w-59xr), requiring an
|
||||
attacker-controlled package index. This is an installation-tool vulnerability,
|
||||
not evidence of an exposed application endpoint.
|
||||
|
||||
The final installed Python audit enumerated 188 distributions: 137 were
|
||||
auditable with zero known vulnerabilities, and 51 local distributions were not
|
||||
available in PyPI. Those skips are covered by source review, not by a claim of
|
||||
dependency-advisory coverage. Production images and every optional dependency
|
||||
combination were not independently resolved or scanned.
|
||||
|
||||
## Scan coverage and limitations
|
||||
|
||||
Evidence directory:
|
||||
`/mnt/DATA/tmp/govoplan-security-performance-20260908-gsk8jn/`.
|
||||
|
||||
The final `final-quick/manifest.json` captures 79 repositories, tool versions,
|
||||
start/end repository fingerprints, report checksums, 168 report artifacts and
|
||||
163 validated JSON/SARIF reports. It records an unchanged workspace, complete
|
||||
coverage for its four required scanners, no execution errors and no missing
|
||||
reports. It ran in report-only mode: exit zero does **not** mean zero warnings.
|
||||
|
||||
- Final production Bandit: 447,008 Python lines; 67 warnings (63 low, 4 medium),
|
||||
no high findings. Ruff security rules: 54 warnings. SQL-construction warnings
|
||||
were reviewed against identifier/operator validation and bound values in
|
||||
DuckDB/Reporting; no injection fix was warranted there. XML import warnings
|
||||
were checked: feed/BPMN input parsing uses defusedxml; stdlib imports support
|
||||
types/output construction. Operator-owned fenced-run argv is not a public
|
||||
arbitrary-command endpoint. Xrechnung output buffering remains a follow-up.
|
||||
Assertions and error-swallowing markers remain review/maintenance warnings,
|
||||
not proof that all such code is harmless.
|
||||
- Final local Semgrep rules: no findings. The broader OWASP-rule pass applied
|
||||
272 rules to 4,169 tracked targets. Its seven warnings recommended weakening
|
||||
owner-only 0700 permissions; they were rejected as false positives. One
|
||||
Calendar rule timeout was rerun with a 60-second budget: zero findings/errors.
|
||||
Bash and conformance TypeScript checks passed despite two scanner-specific
|
||||
parser limitations. Ignored/dependency/generated paths are not a complete
|
||||
line-by-line source audit.
|
||||
- Gitleaks: 79 Git histories plus 79 worktrees, 158 redacted reports, zero
|
||||
detected secrets. This does not establish that deployed credentials are safe
|
||||
or that formerly exposed credentials have been rotated.
|
||||
- Tool versions included Semgrep 1.176.1, Bandit 1.9.4, Ruff 0.15.21 and
|
||||
Gitleaks 8.30.1. The downloaded Gitleaks binary archive matched the official
|
||||
release SHA-256 before execution.
|
||||
- The containerized full-toolbox path could not access Docker's daemon. Its
|
||||
full-mode Trivy/misconfiguration and additional OSV scans were **not** run.
|
||||
A subsequent [registry-only runtime image audit](RUNTIME_IMAGE_AUDIT_2026-09-08.md)
|
||||
successfully scanned nine pinned candidates and two same-minor successors
|
||||
for amd64 without Docker. It found unresolved vulnerabilities and inventory
|
||||
gaps; runtime publication is held. This does not complete full-toolbox,
|
||||
arm64, final-runtime-image or deployment coverage.
|
||||
|
||||
No live application probes, database changes, file operations, mail sends,
|
||||
IMAP appends, imports, notification delivery, deployments, commits or pushes
|
||||
were performed. Browser tests used isolated mocked fixtures. Package installs,
|
||||
builds and temporary audit-tool installation were local development operations.
|
||||
|
||||
## Verification and remaining work
|
||||
|
||||
- 209/209 browser conformance tests pass; production Core/website builds,
|
||||
conformance TypeScript, 24 Core client/dependency regressions, 4 real-client
|
||||
Files reload checks, and 72/72 manifest checks pass.
|
||||
- Access's full 91-test suite passed before the final documentation-only update;
|
||||
the final documentation suite passed all 4 tests. Other module counts appear
|
||||
above. The new authentication/resource tests include demonstrated pre-fix
|
||||
failures rather than only structural assertions.
|
||||
- The original focused workspace run stopped at the institutional
|
||||
governance/Portal fixture (`tests/test_institutional_governance_journey.py:223`,
|
||||
`IndexError`). Release preparation fixes its mixed clocks using the existing
|
||||
temporal context, retaining validity-boundary exclusions; 7 journey tests and
|
||||
ambient-year checks pass. Tracked in
|
||||
[Meta #50](https://git.add-ideas.de/GovOPlaN/govoplan/issues/50).
|
||||
- Campaign's apparent host-path issue was ruled out by existing tracked
|
||||
API/build/snapshot guards and 11 passing tests under normal initialization.
|
||||
Release preparation fixes the standalone import cycle through a deferred
|
||||
resolver import without changing validation rules. Fresh-process coverage,
|
||||
all 11 path tests and Campaign's full 611-test suite pass.
|
||||
|
||||
Next coordinated work:
|
||||
|
||||
1. [Hard resource isolation — Core #297](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/297):
|
||||
regex CPU, aggregate allocation, TAR extension metadata, bounded workers and
|
||||
cancellation, followed by production-like concurrent load tests.
|
||||
2. [Forced password change/recovery — Access #22](https://git.add-ideas.de/GovOPlaN/govoplan-access/issues/22):
|
||||
the current flag is advisory only. Do not enable enforcement without a usable
|
||||
local-password/recovery flow and external-provider rules.
|
||||
3. [Bound subprocess output — Xrechnung #2](https://git.add-ideas.de/GovOPlaN/govoplan-xrechnung/issues/2):
|
||||
enforce the existing 2 MiB limit while draining stdout/stderr, not afterwards.
|
||||
4. [Workflow revision batching/history projection — Workflow Engine #3](https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine/issues/3):
|
||||
batch evidence lookups; separately define explicit history pagination and
|
||||
authorized-total semantics. Docs/notification history volumes also remain.
|
||||
5. Resolve the [runtime image audit](RUNTIME_IMAGE_AUDIT_2026-09-08.md) findings
|
||||
tracked in [Meta #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52)
|
||||
and coverage gaps before lifting its publication hold; complete deployment
|
||||
audits, review exposed development credentials and worker quotas, and
|
||||
benchmark realistic tenant sizes/concurrency. The sanctions
|
||||
transport's fixed HTTPS/redirect allowlist is not a demonstrated arbitrary-URL
|
||||
issue, but migration to Core's pinned egress transport remains desirable.
|
||||
|
||||
Operational compatibility: tenant keys relying on accidental system/wildcard
|
||||
permissions must be corrected rather than weakening the guard. Extreme sparse
|
||||
spreadsheets, overly deep/long archive paths and oversized padding intermediates
|
||||
can now fail early with diagnostics. No stored documents or configurations were
|
||||
deleted or silently migrated.
|
||||
|
||||
## Post-release follow-up — 2026-09-08
|
||||
|
||||
The findings and scanner counts above describe the original audit snapshot.
|
||||
The following source fixes are subsequent to the frozen `0.1.45` composition;
|
||||
they do not change its immutable tags or published package bytes.
|
||||
|
||||
- [Xrechnung #2](https://git.add-ideas.de/GovOPlaN/govoplan-xrechnung/issues/2)
|
||||
now enforces the existing shared 2 MiB stdout/stderr limit during execution
|
||||
and kills/reaps the direct validator on overflow, timeout or cancellation.
|
||||
Report reads are bounded to 16 MiB plus one probe byte before interpretation.
|
||||
The 30-test module suite passes; noisy-child and report-read regressions were
|
||||
also demonstrated to fail against the previous source. Owning EN/DE static
|
||||
documentation is updated. POSIX pipe capture is required; disk quotas,
|
||||
descendant isolation and process-level CPU/memory limits remain separate work.
|
||||
- [Meta #54](https://git.add-ideas.de/GovOPlaN/govoplan/issues/54) now preserves
|
||||
the last command's failure status after exhausted installer retries. Twelve
|
||||
isolated stage/scenario combinations cover every retry call site, success,
|
||||
backoff and caller termination under `set -e`. The test is included in the
|
||||
focused checks and installer CI. See the bilingual
|
||||
[installer retry note](../operations/WEBUI_RELEASE_DEPENDENCY_RETRIES.md).
|
||||
|
||||
These are unreleased follow-up source changes, not a new runtime release or
|
||||
deployment. The runtime-image hold under Meta #52 remains in force; the
|
||||
historical peer-dependency workaround still needs its separate review.
|
||||
|
||||
Further implementation and adoption gates are tracked in the
|
||||
[security follow-up](SECURITY_FOLLOWUP_2026-09-08.md), including disposable
|
||||
parsing/execution workers, opt-in password recovery, workflow read projections
|
||||
and the newer runtime-image evidence. The original scanner counts above remain
|
||||
historical and are not silently replaced by later test results.
|
||||
@@ -0,0 +1,305 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"purpose": "Audit evidence only; not a release manifest or active installer configuration.",
|
||||
"observed_at": "2026-09-08T03:56:47.666808+00:00",
|
||||
"runtime_publication_held": true,
|
||||
"website_deployment_held": true,
|
||||
"scan_execution_complete": true,
|
||||
"coverage_complete": false,
|
||||
"scanner": {
|
||||
"name": "Trivy",
|
||||
"version": "0.74.0",
|
||||
"binary_sha256": "d89bcc6510a267f11b773398cbf1be5520ce39f9e8b6633178c4487f05b7d791",
|
||||
"database_metadata": {
|
||||
"Version": 2,
|
||||
"NextUpdate": "2026-09-08T19:06:01.154199291Z",
|
||||
"UpdatedAt": "2026-09-07T19:06:01.154199452Z",
|
||||
"DownloadedAt": "2026-09-07T23:30:53.198039224Z"
|
||||
},
|
||||
"source": "remote",
|
||||
"scanners": [
|
||||
"vuln"
|
||||
],
|
||||
"list_all_packages": true,
|
||||
"images_executed": false,
|
||||
"existing_docker_credentials_used": false,
|
||||
"timeout": "8m",
|
||||
"maximum_image_size": "2GB"
|
||||
},
|
||||
"evidence": {
|
||||
"private_directory": "/home/zemion/.cache/govoplan-runtime-remediation.qfDSWHJh",
|
||||
"summary_sha256": "0d44390408ab35270e4430516f77bf11aa7877334eff2ef19e11e9a863fe5c56",
|
||||
"frozen_images_sha256": "d0cb156f4a88998531ec55ab950067a3f1350ded648f07650c463af101dad467",
|
||||
"scanner_script_sha256": "f64c69e06efc2ad7b5a657a25aa73f9ff586e3685737d525456bcecbe3ab5f07"
|
||||
},
|
||||
"candidates": [
|
||||
{
|
||||
"name": "haproxy",
|
||||
"image": "docker.io/library/haproxy:3.2.23-alpine@sha256:6343ce34a132a5dceaa24767d739df2bd519f8f7c1079ae39e4821334e8eb42e",
|
||||
"disposition": "source_default_updated_binary_runtime_verification_pending",
|
||||
"platforms": [
|
||||
{
|
||||
"platform": "linux/amd64",
|
||||
"manifest_digest": "sha256:0666a2c2f41d341084ed2da85392b48cdcd766adfa28231f31305724ed5c6ea5",
|
||||
"config_digest": "sha256:9621d75e50a8f26d3738ae3cdbf15e98c6aa5cd48e6baca0699492de502156f5",
|
||||
"compressed_layer_bytes": 20516844,
|
||||
"scan_exit_code": 0,
|
||||
"os": {
|
||||
"Family": "alpine",
|
||||
"Name": "3.24.1"
|
||||
},
|
||||
"inventory": [
|
||||
{
|
||||
"type": "alpine",
|
||||
"packages": 24
|
||||
}
|
||||
],
|
||||
"counts": {
|
||||
"CRITICAL": 0,
|
||||
"HIGH": 0,
|
||||
"MEDIUM": 0,
|
||||
"LOW": 0,
|
||||
"UNKNOWN": 0
|
||||
},
|
||||
"fixable_high_critical": 0,
|
||||
"unique_cves": 0,
|
||||
"report_sha256": "40cfc3db74e29f09723f38698660ccdd50ac935c8d6e1e75c6e0555b3e9361fb",
|
||||
"log_sha256": "46881f695780f89c037d5b0b4dc0ded9ea4e459077c7658d80b786efc5754084"
|
||||
},
|
||||
{
|
||||
"platform": "linux/arm64",
|
||||
"manifest_digest": "sha256:cd20b9dc6b4713956a2a043997001a1948167d345e7c8d5bd5ff2e667166651f",
|
||||
"config_digest": "sha256:19796bff8905d4a46c9463c576203b20cac8984d41b7b01ea9cbff55e8422c34",
|
||||
"compressed_layer_bytes": 20970772,
|
||||
"scan_exit_code": 0,
|
||||
"os": {
|
||||
"Family": "alpine",
|
||||
"Name": "3.24.1"
|
||||
},
|
||||
"inventory": [
|
||||
{
|
||||
"type": "alpine",
|
||||
"packages": 24
|
||||
}
|
||||
],
|
||||
"counts": {
|
||||
"CRITICAL": 0,
|
||||
"HIGH": 0,
|
||||
"MEDIUM": 0,
|
||||
"LOW": 0,
|
||||
"UNKNOWN": 0
|
||||
},
|
||||
"fixable_high_critical": 0,
|
||||
"unique_cves": 0,
|
||||
"report_sha256": "0e1326c58abf358d592fa55c94333228ce1094edf4e489fcc4ece6e32d3320f6",
|
||||
"log_sha256": "397c2fbf1044cf50733d68b4b9cc4eb68211d96f1f302d5e9f8af0d11fb0de1c"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "nginx-stable",
|
||||
"image": "docker.io/nginxinc/nginx-unprivileged:1.30.4-alpine@sha256:442753882674b49ae2c1de83ed67896131c0777f56df5005e356e62bc3f7e7ce",
|
||||
"disposition": "candidate_pending_compatibility",
|
||||
"platforms": [
|
||||
{
|
||||
"platform": "linux/amd64",
|
||||
"manifest_digest": "sha256:b8c179cd3c2ae222a873dd59fbae240fadc03836cae5198afc9e9c19919c3880",
|
||||
"config_digest": "sha256:8b5953dae38d27a76bca22373bb920fd6ce8d9d7da21578d2926e678002de8a0",
|
||||
"compressed_layer_bytes": 25526590,
|
||||
"scan_exit_code": 0,
|
||||
"os": {
|
||||
"Family": "alpine",
|
||||
"Name": "3.24.1"
|
||||
},
|
||||
"inventory": [
|
||||
{
|
||||
"type": "alpine",
|
||||
"packages": 70
|
||||
}
|
||||
],
|
||||
"counts": {
|
||||
"CRITICAL": 0,
|
||||
"HIGH": 0,
|
||||
"MEDIUM": 0,
|
||||
"LOW": 0,
|
||||
"UNKNOWN": 0
|
||||
},
|
||||
"fixable_high_critical": 0,
|
||||
"unique_cves": 0,
|
||||
"report_sha256": "3ad164dae3cdf891b12e41451b8a8e65a5e1688824a7c01d848e1274363e6bf9",
|
||||
"log_sha256": "0f99ff3d9b4396de48655bf8299df30c14ba0c579f480d37baa7d2f6a4c11f1d"
|
||||
},
|
||||
{
|
||||
"platform": "linux/arm64",
|
||||
"manifest_digest": "sha256:b6742a0cbd749add25346658991c3da06a8e38796949df120fed78db8c512576",
|
||||
"config_digest": "sha256:4d8b10f5d2ff99e7aa5f161930d8a4693a86a26f288ec8c0d4cd47f2ef5af179",
|
||||
"compressed_layer_bytes": 25892370,
|
||||
"scan_exit_code": 0,
|
||||
"os": {
|
||||
"Family": "alpine",
|
||||
"Name": "3.24.1"
|
||||
},
|
||||
"inventory": [
|
||||
{
|
||||
"type": "alpine",
|
||||
"packages": 70
|
||||
}
|
||||
],
|
||||
"counts": {
|
||||
"CRITICAL": 0,
|
||||
"HIGH": 0,
|
||||
"MEDIUM": 0,
|
||||
"LOW": 0,
|
||||
"UNKNOWN": 0
|
||||
},
|
||||
"fixable_high_critical": 0,
|
||||
"unique_cves": 0,
|
||||
"report_sha256": "506fdeb97525ed8e4d9ae538c42bab7aa2217f662a7135f0f12d16d20410c7a6",
|
||||
"log_sha256": "c41ef9ea091d00f18c7a097404672591bee85e7477ae17d8f5ca771d8e42b2bb"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "caddy",
|
||||
"image": "docker.io/library/caddy:2.11.4-alpine@sha256:5f5c8640aae01df9654968d946d8f1a56c497f1dd5c5cda4cf95ab7c14d58648",
|
||||
"disposition": "not_selected_remaining_fixable_findings",
|
||||
"platforms": [
|
||||
{
|
||||
"platform": "linux/amd64",
|
||||
"manifest_digest": "sha256:98eb57d882ccd5213d1688764db10c1ca2c58a1ca3a6717a3411ad798f7a423a",
|
||||
"config_digest": "sha256:af555904a0961945f16bb323a501457b13a4f7e9bde969b145b97da80b38ecbe",
|
||||
"compressed_layer_bytes": 23907283,
|
||||
"scan_exit_code": 0,
|
||||
"os": {
|
||||
"Family": "alpine",
|
||||
"Name": "3.23.5"
|
||||
},
|
||||
"inventory": [
|
||||
{
|
||||
"type": "alpine",
|
||||
"packages": 32
|
||||
},
|
||||
{
|
||||
"type": "gobinary",
|
||||
"packages": 146
|
||||
}
|
||||
],
|
||||
"counts": {
|
||||
"CRITICAL": 1,
|
||||
"HIGH": 38,
|
||||
"MEDIUM": 41,
|
||||
"LOW": 12,
|
||||
"UNKNOWN": 23
|
||||
},
|
||||
"fixable_high_critical": 39,
|
||||
"unique_cves": 68,
|
||||
"report_sha256": "e483352a1d5b9b97950dcf92e4dfcf4600f5b09306af3d0640b10ca229a07779",
|
||||
"log_sha256": "9cd599d6dd8c101b421fdeb57036cec1f69f815d765a8bf1f850ec60061036f4"
|
||||
},
|
||||
{
|
||||
"platform": "linux/arm64",
|
||||
"manifest_digest": "sha256:1172d4213087d3fc30bafc7ff2c2896180eb0c41ff7f75f315568fb36cabdcba",
|
||||
"config_digest": "sha256:6b08c1b9858ca9a7d99c1da13c3695081e0e604c6cf214ca26a7ce0e2c4fd9b4",
|
||||
"compressed_layer_bytes": 22722712,
|
||||
"scan_exit_code": 0,
|
||||
"os": {
|
||||
"Family": "alpine",
|
||||
"Name": "3.23.5"
|
||||
},
|
||||
"inventory": [
|
||||
{
|
||||
"type": "alpine",
|
||||
"packages": 32
|
||||
},
|
||||
{
|
||||
"type": "gobinary",
|
||||
"packages": 146
|
||||
}
|
||||
],
|
||||
"counts": {
|
||||
"CRITICAL": 1,
|
||||
"HIGH": 38,
|
||||
"MEDIUM": 41,
|
||||
"LOW": 12,
|
||||
"UNKNOWN": 23
|
||||
},
|
||||
"fixable_high_critical": 39,
|
||||
"unique_cves": 68,
|
||||
"report_sha256": "20e04d820e3b27d57575ea177e523e23109fd83344cdf57e184a4d2fad27e803",
|
||||
"log_sha256": "a1d9c37aa7948c137db64040d95e5930c5859a8acd71ac5bc41ff168e2b270c5"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "node-lts",
|
||||
"image": "docker.io/library/node:24-alpine@sha256:e67514e5d0f6c46656005e1b693b2ec9d52e80b641307de684d4a015ba7a4eaf",
|
||||
"disposition": "not_selected_remaining_fixable_findings",
|
||||
"platforms": [
|
||||
{
|
||||
"platform": "linux/amd64",
|
||||
"manifest_digest": "sha256:4caaaf42195bcd6f6f3559a413b20cb8f8ad089e231ee874cf7701643966689f",
|
||||
"config_digest": "sha256:ee289c69ed1ac50a5a042112ea97f132800e2dd53e832da27784f00e45b3289c",
|
||||
"compressed_layer_bytes": 58486244,
|
||||
"scan_exit_code": 0,
|
||||
"os": {
|
||||
"Family": "alpine",
|
||||
"Name": "3.24.1"
|
||||
},
|
||||
"inventory": [
|
||||
{
|
||||
"type": "alpine",
|
||||
"packages": 18
|
||||
},
|
||||
{
|
||||
"type": "node-pkg",
|
||||
"packages": 146
|
||||
}
|
||||
],
|
||||
"counts": {
|
||||
"CRITICAL": 0,
|
||||
"HIGH": 6,
|
||||
"MEDIUM": 11,
|
||||
"LOW": 12,
|
||||
"UNKNOWN": 0
|
||||
},
|
||||
"fixable_high_critical": 6,
|
||||
"unique_cves": 19,
|
||||
"report_sha256": "c927d995dde700c92027f6328dc6c273f0f1445cd8154301480757cb962e02b2",
|
||||
"log_sha256": "c7dc1900cbe39c9f91e228b2770bcbd394ec2914d2b3879478295fc7f8f77ab3"
|
||||
},
|
||||
{
|
||||
"platform": "linux/arm64",
|
||||
"manifest_digest": "sha256:d3724e44ee368606d753e0027eb8d2a94fc1f275e5d9e4620178a12edb655f5f",
|
||||
"config_digest": "sha256:722cc1507731edf58a4c0bc3e29553c44ce5774d0849242c1b237abc79926a0a",
|
||||
"compressed_layer_bytes": 58935654,
|
||||
"scan_exit_code": 0,
|
||||
"os": {
|
||||
"Family": "alpine",
|
||||
"Name": "3.24.1"
|
||||
},
|
||||
"inventory": [
|
||||
{
|
||||
"type": "alpine",
|
||||
"packages": 18
|
||||
},
|
||||
{
|
||||
"type": "node-pkg",
|
||||
"packages": 146
|
||||
}
|
||||
],
|
||||
"counts": {
|
||||
"CRITICAL": 0,
|
||||
"HIGH": 6,
|
||||
"MEDIUM": 11,
|
||||
"LOW": 12,
|
||||
"UNKNOWN": 0
|
||||
},
|
||||
"fixable_high_critical": 6,
|
||||
"unique_cves": 19,
|
||||
"report_sha256": "e5f7799761f23cefc36929381b4186eeb3afb46a2a559c789d12a7eea5dc30d0",
|
||||
"log_sha256": "fd24671f0da4d3b20dc8bcc2718531e6f6e19155d49bed15958965e21dd10813"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
# GovOPlaN Platform Core Ideas
|
||||
|
||||
## Purpose
|
||||
|
||||
GovOPlaN is an institutional governance and operations layer. Its central
|
||||
promise is:
|
||||
|
||||
> Model the institution, orchestrate its work, connect its systems, and
|
||||
> preserve why and under whose authority it acted.
|
||||
|
||||
The platform should let people complete a real task without understanding its
|
||||
repository or module graph. It should let institutions retain control over
|
||||
their data, procedures, providers, and deployment while still sharing
|
||||
interoperable definitions and evidence.
|
||||
|
||||
This document is the stable summary of the ideas that every product package,
|
||||
module, interface, and integration must preserve. Current implementation state
|
||||
lives in [Strategy Status](STRATEGY_STATUS.md).
|
||||
|
||||
## Ten Core Ideas
|
||||
|
||||
### 1. Institutional context before application context
|
||||
|
||||
Work happens for a tenant, institution, organizational unit, function,
|
||||
mandate, jurisdiction, service, case, and represented party. The real actor
|
||||
and represented capacity remain distinct. Application permissions alone do not
|
||||
prove institutional competence.
|
||||
|
||||
### 2. Governance is executable
|
||||
|
||||
Policy is not explanatory prose around an operation. Consequential actions
|
||||
must expose applicable rules, authority, purpose, expected effects, review
|
||||
requirements, recovery behavior, and evidence. Inheritance may tighten a rule
|
||||
but must not silently loosen an upstream constraint.
|
||||
|
||||
### 3. Time has two independent meanings
|
||||
|
||||
Valid time answers when a fact applied. Recorded time answers what the system
|
||||
knew at a point in history. Historical browsing changes the business-data
|
||||
projection, never the current authorization context. Corrections and
|
||||
supersession remain visible rather than rewriting history.
|
||||
|
||||
### 4. One context, many owners
|
||||
|
||||
Cases, tasks, decisions, records, messages, files, appointments, reports, and
|
||||
external objects remain owned by their domain modules or source systems. Stable
|
||||
references create one navigable context without a universal copied master
|
||||
record or cross-module table access.
|
||||
|
||||
### 5. Native and connected operation are peers
|
||||
|
||||
For every integration, GovOPlaN states whether it is authoritative, mirrors an
|
||||
external source, synchronizes governed fields, adds a governance overlay, or
|
||||
keeps a link only. An external system can be used today and replaced later
|
||||
without losing provenance or institutional control.
|
||||
|
||||
### 6. Human work is a first-class system object
|
||||
|
||||
An intake becomes owned, reviewable work. A person can see the current context,
|
||||
next responsible action, reason, deadline, consequence, and completion
|
||||
evidence. Workflow Engine coordinates machine and human transitions; focused
|
||||
views guide people through the relevant platform surfaces.
|
||||
|
||||
Tasks owns explicit work items and the unified work inbox. Workflow Engine owns
|
||||
process execution and resumable handoffs. Notifications attract attention, and
|
||||
domain modules retain their business objects. These boundaries prevent an
|
||||
inbox, workflow, or notification from becoming a second copy of institutional
|
||||
state.
|
||||
|
||||
### 7. Views reduce complexity without changing authority
|
||||
|
||||
The interface is a task- and role-sensitive projection of installed
|
||||
capabilities. Views, dashboards, search, documentation, and workflow-guided
|
||||
surfaces may hide irrelevant functions, but they never grant access. Users can
|
||||
escape a focused mode when policy permits and can always understand why
|
||||
something is unavailable.
|
||||
|
||||
Configurable product areas organize authorized capabilities around work,
|
||||
services, records, communication, meetings, data and institutional
|
||||
responsibility. The optional Quick Access rail presents task-local Work,
|
||||
Calendar, Messages and Files contributions without merging their owners or
|
||||
turning presentation settings into permissions.
|
||||
|
||||
### 8. Evidence and recovery are part of the operation
|
||||
|
||||
Intent, exact input versions, approvals, external effects, receipts,
|
||||
outcome-unknown states, reconciliation, corrections, retention, and recovery
|
||||
belong to one evidence chain. A retry must be idempotent; rollback claims must
|
||||
distinguish reversible local state from effects already observed elsewhere.
|
||||
|
||||
### 9. Inclusion is multi-channel, not portal-only
|
||||
|
||||
Public portal, postbox, mail, telephone, paper, in-person assistance, APIs, and
|
||||
external systems are channels around the same governed work. Assisted entry
|
||||
records who entered information, for whom, from which source, with which
|
||||
attestation, and how the affected person receives a usable receipt and
|
||||
correction path.
|
||||
|
||||
Responsive, mobile, desktop, and embedded launch surfaces are additional ways
|
||||
to enter the same governed context, not separate products with weaker authority
|
||||
or evidence. Common task-local actions may open in bounded overlays while their
|
||||
owning modules retain validation, policy, and persistence.
|
||||
|
||||
### 10. Successful configurations are portable products
|
||||
|
||||
Modules are ingredients. A usable product is a signed configuration package
|
||||
with terminology, forms, policies, workflows, views, reports, provider
|
||||
profiles, documentation, migration rules, and evidence. Institutions derive
|
||||
local packages without forking code or weakening inherited constraints.
|
||||
|
||||
## Platform Planes
|
||||
|
||||
The planes below are ownership lenses, not navigation groups or mandatory
|
||||
deployment tiers.
|
||||
|
||||
| Plane | Responsibility |
|
||||
| --- | --- |
|
||||
| Experience | Shell, views, dashboard, search, help, accessibility, and task-focused composition |
|
||||
| Participation and channels | Portal, postbox, mail, campaigns, calendar, scheduling, consultation, and assisted channels |
|
||||
| Human work and procedure | Services, forms/runtime, cases, tasks, approvals, workflow execution, and domain procedures |
|
||||
| Content, records, and evidence | Files, templates, DMS, eAkte/records, audit, reporting, transparency, and publication |
|
||||
| Institutional governance | Identity, access, tenancy, organizations, functions, mandates, policy, trust, and formal decisions |
|
||||
| Data and integration | Connectors, datasources, dataflow, search, external references, provider health, and reconciliation |
|
||||
| Runtime and assurance | Module composition, operations, deployment, recovery, security evidence, and signed packages |
|
||||
|
||||
Collected product ideas and normalized actor outcomes are preserved in the
|
||||
[Product Input Register](PRODUCT_INPUT_REGISTER.md). They enter implementation
|
||||
only through a named journey, package, or explicit discovery issue.
|
||||
|
||||
## Canonical Distinctions
|
||||
|
||||
The platform must not collapse these pairs:
|
||||
|
||||
- identity vs account vs represented capacity;
|
||||
- role/permission vs function/mandate/competence;
|
||||
- valid time vs recorded time;
|
||||
- purpose for use vs general technical access;
|
||||
- document content vs managed file bytes vs institutional record;
|
||||
- task vs workflow definition vs workflow instance;
|
||||
- approval vs formal decision;
|
||||
- message intent vs transport delivery vs recipient acknowledgement;
|
||||
- source authority vs connector maturity;
|
||||
- current state vs historical evidence;
|
||||
- correction/compensation vs erasure of an observed effect;
|
||||
- a module boundary vs a user-visible product boundary.
|
||||
|
||||
## Product Experience Rule
|
||||
|
||||
The normal user interface speaks in services, work, records, messages,
|
||||
meetings, decisions, and outcomes. Module names, provider IDs, capability names,
|
||||
package coordinates, and schema details are technical provenance. They are
|
||||
visible to administrators and in expandable diagnostics, but they are not the
|
||||
primary information architecture for ordinary work.
|
||||
|
||||
The complete permission-derived tool catalogue remains deliberately available
|
||||
to power users. Product areas and Quick Access provide sensible system and
|
||||
tenant defaults plus governed user personalization; they do not make familiar
|
||||
tools harder to reach merely to conceal modular implementation.
|
||||
|
||||
## Maturity Rule
|
||||
|
||||
A repository, route, model, or unit test does not make a capability complete.
|
||||
Claims advance only with evidence appropriate to the claim:
|
||||
|
||||
1. `scaffold`: boundary and documentation exist;
|
||||
2. `vertical_slice`: useful behavior has focused tests;
|
||||
3. `reference_ready`: an end-to-end reference journey passed target,
|
||||
accessibility, privacy, security, operations, and recovery evidence;
|
||||
4. `supported`: upgrades, interoperability, support procedures, and release
|
||||
guarantees are defined;
|
||||
5. `lts`: compatibility and maintenance windows are contractual.
|
||||
|
||||
## Deliberate Non-Goals
|
||||
|
||||
GovOPlaN does not aim to:
|
||||
|
||||
- replace every specialist system, ERP, DMS, groupware, or data tool;
|
||||
- make one database authoritative for every connected fact;
|
||||
- expose every installed capability to every person;
|
||||
- infer authority from organizational membership alone;
|
||||
- make historical browsing weaken current security;
|
||||
- treat AI output as an unaccountable institutional decision;
|
||||
- create a repository for every noun in the information model;
|
||||
- claim production maturity from local development evidence.
|
||||
|
||||
## Decision Test
|
||||
|
||||
A proposed feature fits the platform when it improves at least one real
|
||||
institutional journey and can answer:
|
||||
|
||||
1. Who owns the object and source of truth?
|
||||
2. In which institutional and temporal context does it apply?
|
||||
3. For which declared purpose may it be used?
|
||||
4. Which policy and authority permit the action?
|
||||
5. What effect, evidence, retention, and recovery behavior result?
|
||||
6. How can it operate with an external owner without losing autonomy?
|
||||
7. How will a person discover and complete it without learning the module
|
||||
graph?
|
||||
@@ -0,0 +1,203 @@
|
||||
# GovOPlaN Product Input Register
|
||||
|
||||
## Purpose
|
||||
|
||||
This document preserves and normalizes product ideas and user-story notes that
|
||||
inform GovOPlaN without turning a private note file into a second backlog.
|
||||
Gitea issues remain the source of live work state; the stable platform direction
|
||||
remains in [Platform Core Ideas](PLATFORM_CORE_IDEAS.md), the
|
||||
[Connected Governance Platform Roadmap](reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md),
|
||||
and the [Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md).
|
||||
|
||||
The register was reconciled on 2026-08-06 from:
|
||||
|
||||
- `/mnt/DATA/Nextcloud/ADD ideas UG/Products/govoplan/ideas.md`;
|
||||
- `/mnt/DATA/Nextcloud/ADD ideas UG/Products/govoplan/user_stories.txt`.
|
||||
|
||||
The source notes remain useful as the original capture. This maintained version
|
||||
uses consistent terminology, makes ownership explicit, and records where an
|
||||
idea enters the product program.
|
||||
|
||||
## Product Themes
|
||||
|
||||
### Operable and scalable installation
|
||||
|
||||
An operator should be able to install, update, reconfigure, scale, back up,
|
||||
restore, pause, and retire GovOPlaN through one explainable control plane.
|
||||
Existing infrastructure may be reused or managed components may be provisioned.
|
||||
The WebUI and CLI must invoke the same governed operations, show the planned and
|
||||
completed effects, preserve recovery evidence, and never claim rollback for an
|
||||
external effect that cannot actually be reversed.
|
||||
|
||||
This theme is owned by Core, Admin, Ops, Policy, Files, and the signed product
|
||||
package. It is tracked primarily by GovOPlaN #13 and the production evidence
|
||||
issues. It advances in parallel with, but does not replace, actor-facing
|
||||
reference journeys.
|
||||
|
||||
### Focused, consistent work
|
||||
|
||||
People should see the work and tools relevant to the current task, not the
|
||||
installed module graph. Views may be defined by administrators, groups, or
|
||||
users within policy. Workflow instances may pin a governed View. Contextual
|
||||
help, predictable action placement, consistent central components, visible
|
||||
intermediate results, and plain institutional terminology are product
|
||||
requirements.
|
||||
|
||||
Small task-local actions such as writing a Mail or Postbox message, completing
|
||||
a Template, or manipulating Files should be launchable without abandoning the
|
||||
current context. These actions remain owned by their modules and use bounded
|
||||
overlays or workspaces; the shell supplies discovery and return context rather
|
||||
than reimplementing them.
|
||||
|
||||
The accepted first presentation is the optional, configurable Quick Access
|
||||
rail: Work, Calendar, Messages and Files. Messages may compose Mail, Postbox
|
||||
and future chat contributions while preserving their separate authority and
|
||||
channel semantics. System and tenant administrators govern availability and
|
||||
forced entries; users select categories and ordering within those ceilings.
|
||||
Modules register typed contributions through Core and continue to work when
|
||||
Quick Access is absent.
|
||||
|
||||
This theme is owned by Core experience contracts, Views, Dashboard, Tasks,
|
||||
Workflow Engine, Quick Access, Docs, and the contributing feature modules. The
|
||||
first proof is the resumable service-to-decision/eAkte journey in GovOPlaN #42.
|
||||
|
||||
### Governed human work
|
||||
|
||||
An intake or event becomes owned work with a responsible actor or function,
|
||||
priority, deadline, current action, consequence, source context, and completion
|
||||
evidence. Tasks owns explicit work items and the unified work inbox. Workflow
|
||||
Engine owns process execution, waits, retries, and handoffs. Domain modules own
|
||||
the business objects and commands. Notifications attract attention but do not
|
||||
replace durable work state.
|
||||
|
||||
This distinction applies to service requests, technical support, approvals,
|
||||
data reconciliation, campaigns, meetings, decisions, records, and failed
|
||||
automation. It is the immediate shared implementation priority because users
|
||||
must be able to leave work and resume it safely.
|
||||
|
||||
### Institutional responsibility and workforce context
|
||||
|
||||
Organization units, functions, mandates, assignments, delegations, and acting
|
||||
context determine institutional responsibility. Presence, absence, illness,
|
||||
availability, and similar status are effective-dated operational facts used to
|
||||
route work, suppress or redirect notifications, explain planning, and trigger
|
||||
policy. They are not merely profile decorations and they do not replace the IDM
|
||||
lifecycle status of an identity or account.
|
||||
|
||||
Time recording, absence management, sickness reporting, return-to-work
|
||||
management, and applicant management form a possible workforce package. The
|
||||
first implementation must be driven by a real journey and legal/privacy
|
||||
profile; no new module boundary is implied solely by this register.
|
||||
|
||||
### Integration-first and provider-neutral operation
|
||||
|
||||
GovOPlaN should integrate tightly with software already used by an institution
|
||||
and offer native alternatives only where that produces a better governed
|
||||
outcome. Core-mediated provider contracts expose stable, vendor-neutral
|
||||
capabilities; adapters encapsulate specific products. Authority, synchronized
|
||||
fields, conflict behavior, health, credential custody, provenance, and
|
||||
retirement must be explicit.
|
||||
|
||||
The LBV Baden-Wuerttemberg idea is retained as a candidate workforce/payroll
|
||||
integration profile and as a test of provider-neutral contracts. Desktop and
|
||||
groupware integration for Microsoft Office, Outlook, LibreOffice, Thunderbird,
|
||||
file managers, Windows, Unix, and macOS should use standards, deep links,
|
||||
protocol handlers, synchronization, and governed connectors before custom
|
||||
desktop software is introduced.
|
||||
|
||||
### Inclusive channels and device surfaces
|
||||
|
||||
Portal, Postbox, Mail, telephone, paper, in-person assistance, API, desktop,
|
||||
and mobile are channels around the same governed work. A responsive or native
|
||||
mobile surface must not create a second authority or data model. Assisted work
|
||||
records representation, source, attestation, receipt, correction, and delivery
|
||||
choice. People may opt into permitted distribution channels while policy keeps
|
||||
mandatory channels and legal delivery requirements explicit.
|
||||
|
||||
Video meetings, chat, instant messaging, and forums are retained as governed
|
||||
collaboration-channel candidates. The default direction is integration with an
|
||||
established provider through typed message, meeting, participant, evidence, and
|
||||
retention contracts before building another communications stack.
|
||||
|
||||
### Meetings, deliberation, decisions, and voting
|
||||
|
||||
An institutional meeting spans scheduling, participants and mandates,
|
||||
documents, agenda, discussion, formal motions, votes, decisions, minutes,
|
||||
follow-up work, publication, and eligible expense settlement. Committee owns
|
||||
the meeting and deliberation semantics while Calendar, Scheduling, Files,
|
||||
Templates, Decisions, Tasks, Reporting, Ledger, and Voting contribute optional
|
||||
capabilities.
|
||||
|
||||
Voting requiring certified assurance remains a provider program. POLYAS is the
|
||||
first external profile; a native provider may progress only through the
|
||||
controlled assurance and certification program already tracked in Voting.
|
||||
|
||||
### Controlled data work and understandable reporting
|
||||
|
||||
People should manipulate data through immutable inputs, previewed operations,
|
||||
intermediate materializations, reversible definition changes, durable review
|
||||
decisions, quality rules, and complete lineage. Reports expose their definitions
|
||||
and source revisions so controllers can understand and change how a result is
|
||||
produced. Technical support may package controlled workflows that let
|
||||
non-technical users safely operate otherwise hidden data.
|
||||
|
||||
The monthly-data journey is the first proof. Sanctions screening follows on the
|
||||
same source, snapshot, transformation, review, reporting, workflow, and
|
||||
delivery contracts.
|
||||
|
||||
### Institutional memory and consequence
|
||||
|
||||
Decisions should be prepared, discussed, made, communicated, implemented, and
|
||||
filed with their authority and consequences visible. A record/eAkte provides
|
||||
the familiar administrative context across exact source revisions without
|
||||
copying ownership from Cases, Decisions, Files, Forms, Campaign, Postbox, or
|
||||
other modules. The institutional digital twin may later use governed
|
||||
projections to model and simulate organizational change, but simulation output
|
||||
never becomes authority without an explicit adoption decision.
|
||||
|
||||
## Normalized Story Catalogue
|
||||
|
||||
The following catalogue preserves the intent of the collected notes. It is an
|
||||
orientation index, not a completion checklist.
|
||||
|
||||
| Actor and desired outcome | Product owner or composition | First proof |
|
||||
| --- | --- | --- |
|
||||
| Operator installs, updates, scales, backs up, restores, and rolls back through one explainable workflow | Core, Admin, Ops, signed package | GovOPlaN #13 and target-evidence lane |
|
||||
| System and tenant module administrators govern module availability and lifecycle | Core, Admin, Policy, Tenancy | Module entitlement and lifecycle composition |
|
||||
| User works in a decluttered, consistent and task-sensitive interface | Views, Core, Dashboard, Workflow, Docs | Service-to-decision workspace |
|
||||
| Policy maker defines inherited, explainable and enforced rules | Policy plus every consequential owner | Information-governance adoption gate |
|
||||
| Controller and auditor reconstruct results, rules, evidence and correction paths | Audit, Reporting, Records, Dataflow | Monthly-data and eAkte journeys |
|
||||
| User sees institutional terminology, current progress, intermediate results and consequences | Domain owner, Tasks, Workflow, Views | All reference journey acceptance tests |
|
||||
| Voting body and voter obtain independently assured democratic voting | Voting, Committee, Identity Trust, Encryption | POLYAS profile and controlled native-provider program |
|
||||
| Support staff packages safe guided manipulation of hidden data | Workflow, Dataflow, Tasks, Views | Monthly reconciliation workflow |
|
||||
| Data worker performs controlled, understandable and recoverable transformations | Datasources, Connectors, Dataflow, Reporting | GovOPlaN #8 |
|
||||
| Decision maker prepares, deliberates, decides, records and follows consequences | Committee, Decisions, Tasks, Records, Reporting | Service-to-decision journey |
|
||||
| Management delegates responsibility and receives governed activity reports | Organizations, IDM, Access, Policy, Reporting | Function-bound Postbox and work inbox |
|
||||
| Institution models and simulates organizational change | Organizations, Policy, Dataflow, Reporting, Digital Twin | Later governed digital-twin package |
|
||||
| Sender distributes generated files to functions without knowing incumbents | Campaign, Distribution Lists, Postbox, Organizations, IDM | Governed communication package |
|
||||
| Function holder receives current and policy-selected historical work and information | IDM, Access, Postbox, Tasks, Records | Postbox reassignment/history tests |
|
||||
| Administrative worker accesses one familiar eAkte context across exact owned objects | Records and record-source providers | GovOPlaN #42 and Records #8 |
|
||||
| User invokes common message, template and file actions without leaving the current task | Core shell, Views, Workflow and contributing modules | Task-local action contract and service workspace |
|
||||
|
||||
## Idea Preservation Map
|
||||
|
||||
| Original idea cluster | Preserved direction |
|
||||
| --- | --- |
|
||||
| Time recording, absence, sickness, reintegration, applicant management | Governed workforce-context journey; effective-dated status and privacy profile before module expansion |
|
||||
| LBV BW interface | Candidate provider-neutral workforce/payroll connector profile |
|
||||
| Abstract interfaces | Versioned Core contracts with product adapters and explicit source authority |
|
||||
| Desktop and groupware integration | Standards, connectors, deep launch and synchronization before custom clients |
|
||||
| GovOPlaN app/mobile-first pages | Responsive shared semantics; native shell only when a proven journey needs device capabilities |
|
||||
| Video, chat, instant messaging and forum | Optional governed collaboration providers with retention/evidence contracts |
|
||||
| Somacos Session-style meeting management | Committee-led meeting composition across Calendar, Files, Decisions, Templates, Tasks, Reporting and Ledger |
|
||||
| Stronger software integration | Integration-first roadmap rule and first full external product connectors |
|
||||
|
||||
## Maintenance
|
||||
|
||||
When a source idea becomes actionable:
|
||||
|
||||
1. link it to a named reference journey or explicit discovery issue;
|
||||
2. identify the owning module and external authority;
|
||||
3. create or update the Gitea issue with acceptance criteria;
|
||||
4. keep live status out of this document;
|
||||
5. update this register only when the durable interpretation changes.
|
||||
@@ -11,13 +11,77 @@ is not an automatic dependency of every journey.
|
||||
|
||||
The institutional semantics and source-authority model applied to these stages
|
||||
are defined in the
|
||||
[Institutional Governance Target Architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).
|
||||
[Institutional Governance Target Architecture](../architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).
|
||||
|
||||
The stages are ordered, but they are not monolithic releases. Each stage is
|
||||
delivered as small, reviewable, green increments and is complete only when its
|
||||
user journey, failure behavior, documentation, and operator evidence work in a
|
||||
pinned composition.
|
||||
|
||||
## 2026 outcome reset
|
||||
|
||||
Repository completion is not product completion. From 2026-08-05 onward, work
|
||||
is accepted primarily through three maintained real-life journeys:
|
||||
|
||||
1. **Governed communication:** select accountable recipients, prepare content
|
||||
and attachments, approve, deliver through Mail and/or a function-bound
|
||||
Postbox, reconcile uncertain outcomes, and file the evidence.
|
||||
2. **Inclusive service-to-decision:** accept a request through a digital or
|
||||
assisted channel, establish identity and purpose, guide the case through
|
||||
human and automatic work, decide, notify, and file the resulting eAkte.
|
||||
3. **Monthly data and sanctions:** acquire immutable source snapshots, validate
|
||||
and reconcile them interactively, preserve decisions and lineage, produce
|
||||
reports and files, and deliver the accepted result through Campaign.
|
||||
|
||||
The staged program below remains the architectural build order. These journeys
|
||||
are the acceptance lens across those stages. Every significant feature should
|
||||
identify the journey it improves, or provide security, operability, recovery,
|
||||
accessibility, or usability evidence that those journeys require. Work that
|
||||
does neither stays in the backlog until a concrete consumer exists.
|
||||
|
||||
The maintained service-to-decision scenario is the German resident parking
|
||||
permit (`Anwohnerparkausweis`), pinned by
|
||||
`tests/fixtures/resident_parking_permit_journey.json`. It replaces generic
|
||||
permit examples as acceptance evidence and fixes the service, exact Form
|
||||
revision, digital and assisted intake, Case and Workflow handoff, formal
|
||||
Decision, Postbox delivery, and Records target. Changing this flagship scenario
|
||||
is a product decision; implementations may add further scenarios without
|
||||
weakening or silently replacing its acceptance gates.
|
||||
|
||||
The reference fixes an email-link applicant-status profile. The exact
|
||||
published Form revision names the linked email field and bounded expiry/request
|
||||
limits. Submission issues a tracking grant, a matching request delegates mail
|
||||
delivery to Notifications using a hash-only short-lived secret, and Portal
|
||||
presents only the public lifecycle projection. Forms Runtime's module tests
|
||||
also cover authenticated-only and permanent-link variants; the flagship keeps
|
||||
email-link mode because it exercises identity minimization, delivery,
|
||||
revocation, resend, expiry, and non-enumerating failure behavior in one slice.
|
||||
|
||||
The Case-to-payment handoff now has an executable first contract as well. The
|
||||
flagship requests a fixed EUR obligation through `payments.requests`, retains
|
||||
the Case and Workflow context references, proves exact replay, and reconciles a
|
||||
full offline receipt against a Files-owned immutable evidence reference. This
|
||||
does not simulate online checkout or accounting: provider callbacks, partial
|
||||
payments, corrections, refunds, Ledger posting, and XRechnung remain separate
|
||||
governed slices.
|
||||
|
||||
The Records vertical now supplies the journey's native file plan, immutable
|
||||
record and item revisions, chronology, close/reopen, retention calculation,
|
||||
holds, appraisal, independent disposition approval, recovery-ledger evidence,
|
||||
and archive-neutral package simulation. Forms Runtime, Cases, and Decisions
|
||||
expose exact, permission-rechecked source revisions for explicit filing, and
|
||||
all three contribute metadata-only native Search projections that can be
|
||||
rebuilt from authoritative state. The executable fixtures prove those native
|
||||
transitions without claiming archival custody. A persisted Workflow Engine
|
||||
handoff is now reloaded through the Tasks aggregation surface and remains
|
||||
visible until the authoritative Workflow transition completes. Authenticated
|
||||
assisted intake now uses the same exact Form revision and validation as digital
|
||||
intake while retaining purpose, authority, party, channel, accessibility,
|
||||
source, correction, and payload-bound read-back evidence across a session
|
||||
restart. The journey still needs browser accessibility evidence for both
|
||||
channels, pinned-composition reconstruction evidence, and one target-tested
|
||||
archive profile.
|
||||
|
||||
## Why this sequence
|
||||
|
||||
The sequence grows one connected product rather than advancing repositories in
|
||||
@@ -86,6 +150,23 @@ journey needs and supplies contracts shared by all five stages.
|
||||
execution. Database, broker, cache, and worker channels are constrained by
|
||||
deployment network policy and authenticated transport rather than treated as
|
||||
tenant connector profiles.
|
||||
10. **Information governance.** Temporal browsing, purpose-aware access,
|
||||
retention/legal-hold behavior, and institutional acting context are applied
|
||||
to every owned object type. Historical reads use current authorization.
|
||||
Module manifests state `contract_only`, `partial`, `enforced`, or
|
||||
`not_applicable` adoption with evidence; supported maturity is blocked until
|
||||
every applicable dimension is enforced.
|
||||
11. **Durable human work.** Tasks aggregates explicit work and module-owned
|
||||
attention items; Workflow Engine persists process state and handoffs;
|
||||
Notifications attracts attention; Views focuses the relevant surfaces.
|
||||
Leaving or refreshing the browser never becomes the only record that work
|
||||
remains unfinished.
|
||||
12. **Task-local tools.** Mail, Postbox, Templates, Files, and other common
|
||||
actions may contribute bounded launch surfaces with return context. The
|
||||
shell and Workflow compose them without copying their data or validation.
|
||||
The optional Quick Access module presents configurable Work, Calendar,
|
||||
Messages and Files categories; system/tenant/user settings and View/Policy
|
||||
ceilings resolve their availability and ordering.
|
||||
|
||||
## Documentation contract for every reference stage
|
||||
|
||||
@@ -108,6 +189,9 @@ Every demonstrated journey provides:
|
||||
provenance, evidence, retention, and destructive actions.
|
||||
- **Acceptance view:** runnable examples, expected results, failure injection,
|
||||
and release gates.
|
||||
- **Channel and records view:** assisted/non-digital intake and output,
|
||||
representation, provenance, filing, retention, legal hold, and archive
|
||||
consequences where the journey creates evidence or a record.
|
||||
|
||||
The Docs module selects and links these views according to installed
|
||||
capabilities and actor context. Feature repositories remain the source of
|
||||
@@ -436,6 +520,9 @@ or the external editor the document-lifecycle owner.
|
||||
link, callback, webhook, file, identity, or data row.
|
||||
- Do not claim a stage complete from local unit tests. Use pinned composition,
|
||||
target integration, failure drills, adaptive docs, and operator evidence.
|
||||
- Do not claim a module complete while its relevant information-governance
|
||||
dimensions remain `contract_only` or while the reference journey lacks an
|
||||
assisted-channel and records outcome where those are applicable.
|
||||
- A later stage may prototype contracts while the preceding gate is being
|
||||
proven, but it may not redefine an owning module's boundary by convenience.
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# GovOPlaN Roadmap
|
||||
|
||||
## Purpose
|
||||
|
||||
GovOPlaN should become the connective, governance-aware operating layer of an
|
||||
institution: people complete services and work without learning the module
|
||||
graph, while the institution can explain authority, policy, source data,
|
||||
effects, evidence, and recovery.
|
||||
|
||||
This is the concise product roadmap. It states durable outcomes and sequence,
|
||||
not release dates or issue state. Use [Strategy Status](STRATEGY_STATUS.md) for
|
||||
the current reconciliation and Gitea issues for active work. The
|
||||
[detailed connected-platform vision](reference/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md)
|
||||
retains stakeholder perspectives, configuration archetypes, and the complete
|
||||
outcome-story catalogue.
|
||||
|
||||
## Product Promise
|
||||
|
||||
GovOPlaN will:
|
||||
|
||||
1. model institutional context, responsibility, authority, and time;
|
||||
2. turn incoming information into owned, reviewable human and machine work;
|
||||
3. connect native and external systems without obscuring the source of truth;
|
||||
4. preserve decisions, effects, records, corrections, and recovery evidence;
|
||||
5. support digital, assisted, paper, message, calendar, and system channels as
|
||||
paths through the same governed work; and
|
||||
6. package successful configurations so institutions can adopt them without
|
||||
code forks or loss of local autonomy.
|
||||
|
||||
It will not replace every specialist system, copy all data into one master
|
||||
database, infer authority from membership, or claim production maturity from
|
||||
repository breadth.
|
||||
|
||||
## Outcome Horizons
|
||||
|
||||
| Horizon | Outcome | Completion evidence |
|
||||
| --- | --- | --- |
|
||||
| Trustworthy baseline | A pinned composition can be installed, upgraded, operated, explained, and recovered. | Signed artifacts, clean install/upgrade, provider failure tests, restore drill, coherent UI, and target evidence |
|
||||
| Connected work | Intake becomes accountable work with context, assignment, review, communication, and evidence. | One digital and assisted service reaches a decision and eAkte without losing responsibility or state |
|
||||
| Reusable products | Complete service, communication, and data outcomes ship as governed configuration packages. | Two materially different deployments adapt packages without code forks |
|
||||
| Institutional assurance | Records, transparency, privacy, risk, regulated review, and reporting connect to real operations. | A consequential decision can be reconstructed, corrected, retained, and disclosed under policy |
|
||||
| Federated ecosystem | Autonomous installations exchange signed data and configuration across explicit trust boundaries. | Paired-instance exchange, reconciliation, supported deployment profiles, and independent evidence |
|
||||
|
||||
## Current Sequence
|
||||
|
||||
The sequence is outcome-led. Shared foundation work enters when one of these
|
||||
proofs needs it.
|
||||
|
||||
1. **Enforce the platform quality contract.** German is the reference locale;
|
||||
help, accessibility, temporal browsing, purpose-aware access, retention,
|
||||
institutional context, optional-module combinations, and recovery behavior
|
||||
become measurable release gates.
|
||||
2. **Complete governed communication.** Prove recipient selection, Campaign,
|
||||
Files, Mail, function-bound Postbox delivery, acknowledgement, uncertain
|
||||
outcomes, correction, filing, and recovery against a named target.
|
||||
3. **Complete the monthly-data and sanctions journey.** Acquire immutable
|
||||
source snapshots, validate and reconcile data interactively, preserve
|
||||
lineage and review, publish reports and files, and deliver accepted results.
|
||||
4. **Complete inclusive service to decision.** Accept digital or assisted
|
||||
input, establish actor and purpose, persist human handoffs, decide, notify,
|
||||
and reconstruct the exact eAkte under current authorization.
|
||||
5. **Complete discovery and external coexistence.** Finish native PostgreSQL
|
||||
search coverage, prove reauthorization and reindexing, then prove one
|
||||
external product connector and one paired GovOPlaN federation exchange.
|
||||
6. **Prove production operation.** Complete multi-host, provider, restore,
|
||||
accessibility, volume, key-custody, and independently signed target
|
||||
evidence before raising maturity claims.
|
||||
|
||||
## Continuous Foundation
|
||||
|
||||
Every journey applies the same boundaries:
|
||||
|
||||
- modules cooperate through versioned Core contracts and typed references;
|
||||
- permissions, policy, institutional context, purpose, and current authority
|
||||
are evaluated before presenting or acting on data;
|
||||
- requested actions, durable intent, observed effects, unknown outcomes,
|
||||
retries, reconciliation, and correction remain distinct;
|
||||
- Workflow Engine coordinates stable module-owned actions and human handoffs;
|
||||
it does not become a second owner of domain state;
|
||||
- Files owns managed bytes, Records owns institutional filing and retention,
|
||||
and source systems retain explicitly declared authority;
|
||||
- focused views and product areas reduce interface complexity without granting
|
||||
access or hiding material consequences;
|
||||
- configuration packages include terminology, forms, policies, workflows,
|
||||
views, reports, providers, documentation, migration, and evidence; and
|
||||
- maturity advances from scaffold to vertical slice, reference-ready,
|
||||
supported, and LTS only with evidence appropriate to each claim.
|
||||
|
||||
## Decision Rule
|
||||
|
||||
A roadmap item should answer all of the following before implementation:
|
||||
|
||||
1. Which real journey and actor outcome does it improve?
|
||||
2. Which module or external system owns each object and source of truth?
|
||||
3. Which institutional, temporal, purpose, and policy context applies?
|
||||
4. Which effects, evidence, retention, failure, and recovery states result?
|
||||
5. Which package and target evidence will prove the outcome?
|
||||
|
||||
If those answers are missing, retain the idea in the Product Input Register or
|
||||
Gitea discovery work rather than opening an unbounded implementation program.
|
||||
@@ -0,0 +1,137 @@
|
||||
# GovOPlaN Strategy Status
|
||||
|
||||
## Status Record
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Reconciled on | 2026-08-17 |
|
||||
| Source scope | Local workspace manifests, source inventory, focused journey checks, signed release evidence, and live Gitea issue state |
|
||||
| Stable direction | [Platform Core Ideas](PLATFORM_CORE_IDEAS.md) and [Roadmap](ROADMAP.md) |
|
||||
| Collected product input | [Product Input Register](PRODUCT_INPUT_REGISTER.md) |
|
||||
| Delivery source | Gitea issues |
|
||||
|
||||
This is the only prose source for current cross-product status. It is a
|
||||
reconciliation, not a release certification. Module manifests and target
|
||||
evidence remain authoritative for specific maturity claims.
|
||||
|
||||
## Portfolio Snapshot
|
||||
|
||||
- 67 source module manifests were loadable and architecture-declared.
|
||||
- 50 modules declared `vertical_slice`; 17 declared `scaffold`.
|
||||
- No module declared `reference_ready`, `supported`, or `lts`.
|
||||
- The coordinated package version was `0.1.18`, with version alignment passing
|
||||
across all 78 release repositories.
|
||||
- The live portfolio had 137 open issues: 42 priority-P1, 92 priority-P2, and
|
||||
3 priority-P3 items. Every open issue had labels.
|
||||
- 129 open issues had no milestone, so issue labels do not yet express a
|
||||
reliable completion sequence on their own.
|
||||
- Three product package manifests existed: governed communication, governed
|
||||
data and assurance, and service to decision. None had crossed the complete
|
||||
target-evidence gate.
|
||||
|
||||
These counts are dated. Refresh them rather than copying them into another
|
||||
document.
|
||||
|
||||
## Interface And Contract Evidence
|
||||
|
||||
The 2026-08-17 source inventory found:
|
||||
|
||||
- 1,344 UI fields and 1,331 UI actions;
|
||||
- 8,412 stable interface declarations with no duplicate IDs;
|
||||
- 43 frontend routes and 943 backend endpoints;
|
||||
- no public WebUI surfaces missing runtime declarations;
|
||||
- no stale runtime route declarations;
|
||||
- no unclassified endpoint without a static UI reference;
|
||||
- all 1,344 fields with a resolvable F1 context; 175 have statically specific
|
||||
help and 1,169 remain candidates for richer field-specific content beyond
|
||||
page/module fallback;
|
||||
- German (`de`) as the complete reference locale and no used key missing from
|
||||
the required German or English catalogs;
|
||||
- 3 module information-governance dimensions classified as `enforced`, 1 as
|
||||
`partial`, and 264 as `contract_only`.
|
||||
This is an honest platform-wide baseline, not a claim that temporal,
|
||||
purpose, retention, and institutional-context adoption is complete.
|
||||
|
||||
## Credible Current Outcomes
|
||||
|
||||
### Platform foundation
|
||||
|
||||
Module discovery, optional dependency validation, migrations, shared WebUI,
|
||||
tenant and access foundations, signed catalogs/packages, event delivery,
|
||||
recovery contracts, contextual help, views, temporal titlebar context, and
|
||||
stateless-runtime patterns are implemented and tested at varying depths.
|
||||
|
||||
### Governed communication
|
||||
|
||||
Campaign authoring, recipient data, attachments, templates, mail profiles,
|
||||
mock/real delivery paths, audit evidence, reporting, distribution-list
|
||||
composition, and optional Postbox delivery form the deepest product cluster.
|
||||
Target provider, accessibility, recovery, and high-volume evidence still
|
||||
prevent a reference-ready claim.
|
||||
|
||||
### Institutional service and decision
|
||||
|
||||
Services, Forms, Forms Runtime, Cases, Parties, Mandates, Approvals, Committee,
|
||||
Voting, Decisions, Portal, Postbox, and Audit have an executable service-to-
|
||||
decision fixture. Public and invitation intake can retain Files-backed
|
||||
evidence; Forms submissions, Cases, and formal Decisions can be explicitly
|
||||
filed as exact eAkte source revisions and reconstructed through permission-
|
||||
rechecked native Search projections. A durable Workflow Engine handoff now
|
||||
survives session restart and appears through the Tasks work inbox until the
|
||||
authoritative transition completes. Browser-complete assisted intake, broader
|
||||
work projections and escalation, production identity and delivery, a named
|
||||
archive profile, and target evidence remain.
|
||||
|
||||
### Governed data and assurance
|
||||
|
||||
Connectors, Datasources, Dataflow, Reporting, Search, Policy, Risk Compliance,
|
||||
and Workflow provide source governance, immutable snapshots, transformation,
|
||||
quality, semantic reporting, and provenance foundations. The monthly-data and
|
||||
sanctions compositions now prove immutable connector snapshots, pinned
|
||||
Dataflow publication, Risk Compliance review, and rescreening in process. The
|
||||
journeys still need target connector profiles, complete interactive
|
||||
reconciliation, governed export/delivery, and browser-level handoff evidence.
|
||||
|
||||
## Material Gaps
|
||||
|
||||
| Gap | Consequence | Next proof |
|
||||
| --- | --- | --- |
|
||||
| No reference-ready product package | The platform cannot yet make a bounded supported-product claim | Complete one named target composition and evidence bundle |
|
||||
| Human-work spine is only an MVP | Tasks aggregates explicit work plus Workflow, Approval, and unread Postbox projections, but broad domain coverage, deadline escalation, assignment lifecycle, and focused product UX remain | Extend source providers through the three reference journeys and prove overdue/reassignment behavior in browser tests |
|
||||
| Records/eAkte target integration incomplete | Native lifecycle, retention, holds, approval, recovery, and transfer simulation are implemented, but real custody is not proved | Target-test one archive/xdomea profile and browser-test the now server-enforced assisted reference journey |
|
||||
| Cross-cutting governance adoption uneven | Historical and purpose-sensitive behavior varies by module | Enforced adoption declarations and route/query/effect migration |
|
||||
| Explicit help/accessibility depth incomplete | German/reference and F1 association gates now pass, but generic fallback remains too common | High-risk German help content and browser/a11y matrix |
|
||||
| Real federation absent | Cross-institution exchange remains connector-specific | Paired-instance signed exchange and reconciliation proof |
|
||||
| External production evidence incomplete | Scale, restore, interoperability and custody claims remain conditional | Real target drills and independent signed evidence |
|
||||
|
||||
## Active Strategic Order
|
||||
|
||||
1. Establish German, help, temporal, purpose, retention, and institutional
|
||||
context as enforceable platform quality contracts.
|
||||
2. Complete governed communication and Postbox against a named target.
|
||||
3. Complete the monthly-data flow and use it as the data foundation for
|
||||
sanctions screening.
|
||||
4. Complete the browser proof for the digital and assisted service-to-decision
|
||||
journey; server-side assisted resume, provenance, correction, and read-back
|
||||
enforcement now complement its existing exact eAkte filing contracts.
|
||||
5. Complete native PostgreSQL search coverage for remaining journey-owned
|
||||
objects and prove reauthorization and reindex operations at target volume;
|
||||
keep OpenSearch optional. Communication, Records, service-to-decision,
|
||||
Dataflow, Reporting, Risk Compliance, and Datasource catalogue sources now
|
||||
exist.
|
||||
6. Prove one external product connector and one GovOPlaN federation exchange.
|
||||
7. Finish multi-host, restore, provider, accessibility, and independent signed
|
||||
target evidence before increasing maturity claims.
|
||||
|
||||
## Refresh Procedure
|
||||
|
||||
Refresh this page only from evidence:
|
||||
|
||||
1. run `tools/checks/check-manifest-shapes.py`;
|
||||
2. run `tools/inventory/platform-interface-inventory.py --strict
|
||||
--strict-declarations --strict-endpoints`;
|
||||
3. run the selected reference-journey checks;
|
||||
4. inspect signed release and target evidence;
|
||||
5. query live Gitea issue/milestone state;
|
||||
6. update the dated values and material gaps here;
|
||||
7. retain prior assessments as dated evidence rather than rewriting them.
|
||||
+23
-13
@@ -4,7 +4,8 @@
|
||||
|
||||
> As a system administrator, I can execute one shell command that downloads a
|
||||
> verified GovOPlaN distribution and starts a completely configured Core control
|
||||
> plane without optional modules. In the WebUI I can browse compatible signed
|
||||
> plane with the official package directory available but only the protected
|
||||
> baseline active. In the WebUI I can browse compatible signed
|
||||
> module releases, select the modules for this installation, and follow every
|
||||
> download, validation, migration, installation, activation, and health-check
|
||||
> step. When an update is available, I can review its impact and confirm it.
|
||||
@@ -25,7 +26,8 @@ The canonical backlog item is
|
||||
|
||||
- **Core control plane:** the smallest bootable distribution: Core API, Core
|
||||
WebUI, PostgreSQL, Redis, installer worker, migration runner, and durable
|
||||
storage configuration. No optional GovOPlaN module package is installed.
|
||||
storage configuration. An immutable image may carry the full verified package
|
||||
profile, but optional modules are not active or tenant-entitled by implication.
|
||||
- **Bootstrap administrator:** a single-use, time-limited installation identity
|
||||
that may access only first-run and module-lifecycle functions. It is retired
|
||||
when the selected identity/access configuration becomes healthy.
|
||||
@@ -55,7 +57,9 @@ The canonical backlog item is
|
||||
5. It prints the local URL and one-time bootstrap credential. Re-running the
|
||||
command is idempotent and shows or repairs the existing installation rather
|
||||
than creating another identity or database.
|
||||
6. No optional module is installed or enabled at this point.
|
||||
6. Only the protected baseline is enabled. Installed package availability does
|
||||
not grant permissions, tenant entitlement, View visibility, or capability
|
||||
opt-in.
|
||||
|
||||
### Module selection, installation, and update
|
||||
|
||||
@@ -160,19 +164,24 @@ Implementation status as of the current source tree:
|
||||
ledger and deployment operation journal. Automatic database backup and broad
|
||||
adoption by module-owned external effects remain open work.
|
||||
|
||||
1. **Reproducible Core-only distribution.** Publish pinned multi-architecture
|
||||
images, signed distribution manifest, Core-only Compose profile, bootstrap
|
||||
1. **Reproducible Core-baseline distribution.** Publish pinned multi-architecture
|
||||
full-package images, signed distribution manifest, Core-baseline Compose profile, bootstrap
|
||||
preflight, generated secrets, readiness, and idempotent rerun/repair.
|
||||
2. **First-run control plane.** Add the restricted bootstrap administrator,
|
||||
one-time enrollment, initial catalog/keyring configuration, and retirement
|
||||
after durable administrator access is established.
|
||||
3. **Read-only online module directory.** Move the existing catalog and module
|
||||
directory contracts into the installed Core WebUI with compatibility,
|
||||
provenance, release-note, and update-state presentation.
|
||||
4. **Durable module plan and install.** Reuse the existing installer queue,
|
||||
locks, signed-package validator, rollback drill, and run evidence behind a
|
||||
plan/confirm/progress UI. Add initial catalog-entry synthesis and artifact
|
||||
acquisition where the current release console still assumes local sources.
|
||||
3. **Read-only online module directory (implemented foundation).** Admin falls
|
||||
back to the signed public stable directory, presents installed/update state,
|
||||
searchable availability/blocker filters, immutable source/artifact
|
||||
provenance, configuration requirements, release notes, and technical
|
||||
compatibility. Withdrawn releases remain visible but cannot be planned.
|
||||
Operator-configured catalogs remain an explicit override.
|
||||
4. **Durable module plan and install (implemented local boundary).** Catalog
|
||||
selection creates a reviewed plan; the installer queue, lock, preflight,
|
||||
maintenance gate, digest-verified artifact cache, rollback drill, and run
|
||||
evidence remain separate from the API process. Shared deployments convert
|
||||
the same intent into a new immutable release composition instead of mutating
|
||||
one replica.
|
||||
5. **Safe module update.** Add drain/maintenance coordination, backup gate,
|
||||
migration compatibility window, reconnectable progress, health verification,
|
||||
retry/recovery, and update notification.
|
||||
@@ -194,7 +203,8 @@ Implementation status as of the current source tree:
|
||||
|
||||
## Explicit non-goals for the first distribution slice
|
||||
|
||||
- Shipping optional modules in the Core image.
|
||||
- Activating, tenant-entitling, or exposing optional modules merely because the
|
||||
immutable image carries their verified packages.
|
||||
- Exporting secrets or production business data with configuration.
|
||||
- Pretending every schema migration can be reversed automatically.
|
||||
- Building a proprietary orchestrator instead of supporting Compose and a
|
||||
+38
-94
@@ -1,29 +1,32 @@
|
||||
# GovOPlaN Connected Governance Platform Roadmap
|
||||
# GovOPlaN Detailed Connected-Platform Vision
|
||||
|
||||
## Purpose and status
|
||||
|
||||
This document describes the long-term product destination for GovOPlaN from an
|
||||
outcome and stakeholder perspective. It answers what a completely connected
|
||||
governance platform should enable, how the same platform can be configured for
|
||||
different institutions, and which capability horizons lead from the current
|
||||
baseline to that destination.
|
||||
This reference catalogue describes the long-term product destination from an
|
||||
outcome and stakeholder perspective. It preserves the detailed perspectives,
|
||||
configuration archetypes, stories, horizons, and maturity notes behind the
|
||||
concise [Roadmap](../ROADMAP.md).
|
||||
|
||||
It is a durable direction, not a release promise or a substitute for issue
|
||||
tracking. Live work state belongs in Gitea issues. The
|
||||
It is not a release promise, live plan, or second status source. The concise
|
||||
roadmap owns the current durable sequence, Strategy Status owns the reconciled
|
||||
state, and Gitea issues own work state. Where dated detail here differs from
|
||||
those sources, those sources take precedence. The
|
||||
[Core master roadmap](https://git.add-ideas.de/GovOPlaN/govoplan-core/src/branch/main/docs/GOVOPLAN_MASTER_ROADMAP.md)
|
||||
remains the technical module and wave sequence; this document supplies the
|
||||
cross-product vision that sequence serves.
|
||||
|
||||
Read it together with:
|
||||
|
||||
- the [institutional governance target architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md)
|
||||
- the [selected reference-journey program](REFERENCE_JOURNEY_PROGRAM.md)
|
||||
- the [current capability and infrastructure fit assessment](CAPABILITY_AND_INFRASTRUCTURE_FIT.md)
|
||||
- the [interface pattern language](INTERFACE_PATTERN_LANGUAGE.md)
|
||||
- the [interface surface inventory](INTERFACE_SURFACE_INVENTORY.md)
|
||||
- the [module contract and install model](MODULE_CONTRACTS_AND_INSTALLS.md)
|
||||
- the [repository and module index](REPOSITORY_INDEX.md)
|
||||
- the [Gitea issue workflow](GITEA_ISSUES.md)
|
||||
- the [concise product roadmap](../ROADMAP.md)
|
||||
- the [institutional governance target architecture](../../architecture/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md)
|
||||
- the [selected reference-journey program](../REFERENCE_JOURNEY_PROGRAM.md)
|
||||
- the [current strategy status](../STRATEGY_STATUS.md)
|
||||
- the [generated, pinned Campaign capability and infrastructure fit assessment](../../evidence/snapshots/CAPABILITY_AND_INFRASTRUCTURE_FIT.generated.md)
|
||||
- the [interface pattern language](../../architecture/INTERFACE_PATTERN_LANGUAGE.md)
|
||||
- the [interface surface inventory](../../evidence/snapshots/INTERFACE_SURFACE_INVENTORY.md)
|
||||
- the [module contract and install model](../../operations/MODULE_CONTRACTS_AND_INSTALLS.md)
|
||||
- the [repository and module index](../../project/REPOSITORY_INDEX.md)
|
||||
- the [Gitea issue workflow](../../project/GITEA_ISSUES.md)
|
||||
|
||||
### How to read this roadmap
|
||||
|
||||
@@ -41,16 +44,17 @@ Read it together with:
|
||||
- Use [Near-term portfolio order](#near-term-portfolio-order) for the bridge to
|
||||
implementation and [Product decisions](#product-decisions-to-make-progressively)
|
||||
for choices that can remain deferred.
|
||||
- Use the [dated snapshot appendix](#snapshot-appendix-2026-07-20) only to
|
||||
understand which live backlog and release facts informed this revision.
|
||||
- Use the [dated strategic review](../../archive/2026-08/STRATEGIC_REVIEW_2026-08-05.md) to understand
|
||||
why the current convergence and reference-journey order was chosen.
|
||||
|
||||
### Planning ownership
|
||||
|
||||
| Question | Canonical source |
|
||||
| --- | --- |
|
||||
| What product should GovOPlaN become, for whom, in which configurations, and through which outcome horizons? | This meta roadmap |
|
||||
| What product should GovOPlaN become and in which durable sequence? | The concise Roadmap |
|
||||
| Which stakeholder perspectives, configuration archetypes, and detailed outcome stories inform that direction? | This reference catalogue |
|
||||
| Which module owns a capability, which technical wave should deliver it, and what implementation gates apply? | The Core master roadmap and owning-module concepts |
|
||||
| What is actively planned, blocked, implemented, or closed now? | Gitea issues |
|
||||
| What is actively planned, blocked, implemented, or closed now? | Gitea issues and the dated reconciliation in `STRATEGY_STATUS.md` |
|
||||
| What can a named composition credibly claim in a target environment? | A dated capability/infrastructure fit assessment |
|
||||
|
||||
The horizons and near-term order below express product outcomes and portfolio
|
||||
@@ -109,7 +113,7 @@ safe modules -> connected work -> reusable services -> institutional assurance -
|
||||
```
|
||||
|
||||
The active implementation path is the
|
||||
[Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md), selected on
|
||||
[Reference Journey Program](../REFERENCE_JOURNEY_PROGRAM.md), selected on
|
||||
2026-07-21. Its five stages do not replace these product horizons: they are the
|
||||
ordered demonstrations through which the shared platform contracts and horizon
|
||||
gates are to be proved. Connector safety, identity/function semantics,
|
||||
@@ -265,8 +269,8 @@ Diagnostics minimize personal data and link to governed evidence when deeper
|
||||
inspection is authorized.
|
||||
|
||||
The complete installation and lifecycle journey is specified in the
|
||||
[System Administrator Lifecycle User Story](SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md):
|
||||
one-command Core-only bootstrap, signed online module installation and updates,
|
||||
[System Administrator Lifecycle User Story](../SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md):
|
||||
one-command Core-baseline bootstrap, signed online module installation and updates,
|
||||
stateless scale-out, versioned configuration transfer, undo, and reproducible
|
||||
environment-promotion recipes.
|
||||
|
||||
@@ -949,7 +953,7 @@ first analytical product prove Horizons 2 and 3; governed BI adds assurance and
|
||||
ecosystem capabilities across Horizons 3–5; collaborative documents combine
|
||||
the evidence spine, service packages, and records assurance across Horizons
|
||||
2–4. The detailed mapping and gates are in the
|
||||
[Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md).
|
||||
[Reference Journey Program](../REFERENCE_JOURNEY_PROGRAM.md).
|
||||
|
||||
### Current baseline: modular pilot foundations
|
||||
|
||||
@@ -975,8 +979,9 @@ checkouts.
|
||||
Priorities:
|
||||
|
||||
1. Deliver the first slices of the
|
||||
[System Administrator Lifecycle User Story](SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md):
|
||||
a verified Core-only distribution, first-run control plane, read-only online
|
||||
[System Administrator Lifecycle User Story](../SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md):
|
||||
a verified full-package distribution with only the Core baseline active,
|
||||
first-run control plane, read-only online
|
||||
module directory, and durable plan/confirm/install progress.
|
||||
2. Pin and publish a compatible Core/WebUI/module composition and first
|
||||
reference configuration package.
|
||||
@@ -1218,7 +1223,7 @@ provides all applicable evidence below.
|
||||
## Near-term portfolio order
|
||||
|
||||
This order is now selected. Detailed slices and gates are in the
|
||||
[Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md). Workflow Engine and
|
||||
[Reference Journey Program](../REFERENCE_JOURNEY_PROGRAM.md). Workflow Engine and
|
||||
the optional editor may support these stages, but Workflow work enters the
|
||||
portfolio only through an explicit bounded package or reference journey.
|
||||
|
||||
@@ -1351,71 +1356,10 @@ language, what service it configured, who can act, which systems participate,
|
||||
what happens when they fail, how a decision can be reviewed, and where the
|
||||
evidence remains—and the product can prove that explanation at runtime.
|
||||
|
||||
## Snapshot appendix: 2026-07-20
|
||||
## Dated Context
|
||||
|
||||
This appendix records volatile facts that informed this revision. It is not a
|
||||
second source of truth and should be refreshed or removed when a later roadmap
|
||||
review uses a new release/backlog snapshot.
|
||||
|
||||
### Composition and release snapshot
|
||||
|
||||
The cross-repository contract scan found 43 module manifest contracts, 29
|
||||
provided interface names, 16 requirements, and no contract error across 65
|
||||
scanned repositories. That is meaningful composition evidence, but the release
|
||||
metadata trailed the integrated code: Core, Policy, Poll, and Scheduling
|
||||
declared `0.1.9` while the whole-product release requirements remained on
|
||||
module tag `v0.1.8`; the root self-hosted `.env.example` and release smoke
|
||||
composition did not yet exercise all installed release modules. Other
|
||||
development compositions already included some of those modules. This was a
|
||||
release/composition gap, not evidence that the underlying slices did not exist.
|
||||
|
||||
### Backlog snapshot
|
||||
|
||||
The Gitea audit found 206 open issues across 36 of 66 catalogued repositories
|
||||
and 362 closed issues. Campaign had 51 open issues and Core 44; together they
|
||||
held 46% of current work. This reflected substantial completed kernel,
|
||||
security, and platform work and a deliberate concentration on the first usable
|
||||
vertical, but also risked crowding out production evidence and the shared
|
||||
process spine.
|
||||
|
||||
The issue workflow needed a reconciliation pass before another delivery
|
||||
program could be inferred from labels: 119 open issues remained in triage, 116
|
||||
had no milestone, and several recently pushed Calendar, Scheduling, Poll,
|
||||
Campaign, and Files slices still described themselves as local or awaiting
|
||||
integration. Conversely, 30 repositories had no open issue; for many
|
||||
later-wave modules this meant no implementation program had been opened, not
|
||||
that the capability was complete.
|
||||
|
||||
[Poll #2](https://git.add-ideas.de/GovOPlaN/govoplan-poll/issues/2) was a clear
|
||||
tracker-drift example: its configurable transition engine, agreed transition
|
||||
matrix/history, idempotent keyed retries, re-decision audit, archive/unarchive,
|
||||
and preservation behavior were implemented and pushed while the issue still
|
||||
reported `needs-info`.
|
||||
|
||||
Issue anchors that informed the bridge from the baseline into this roadmap:
|
||||
|
||||
- [Meta #10](https://git.add-ideas.de/GovOPlaN/govoplan/issues/10) for the
|
||||
capability/infrastructure assessment and its target proof;
|
||||
- [Meta #11](https://git.add-ideas.de/GovOPlaN/govoplan/issues/11) for the
|
||||
universal interface and focused-view direction;
|
||||
- [Core #225](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/225) for
|
||||
guided, safe configuration;
|
||||
- [Core #29](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/29) for the
|
||||
backup/restore production gate;
|
||||
- [Core #263](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/263) and
|
||||
[Campaign #63](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/63),
|
||||
[#62](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/62),
|
||||
[#65](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/65), and
|
||||
[#69](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/69) for the
|
||||
reference interface/delivery vocabulary and behavior;
|
||||
- [Poll #1](https://git.add-ideas.de/GovOPlaN/govoplan-poll/issues/1) for the
|
||||
database-enforced respondent invariant exposed by Scheduling;
|
||||
- [Connectors #6](https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/6)
|
||||
for the governed connector configuration/simulation foundation;
|
||||
- [Meta #9](https://git.add-ideas.de/GovOPlaN/govoplan/issues/9) for the first
|
||||
permit-to-payment reference process; and
|
||||
- [Meta #12](https://git.add-ideas.de/GovOPlaN/govoplan/issues/12) for the
|
||||
deliberately deferred, consumer-independent export-control story.
|
||||
|
||||
Live Gitea issue state remains canonical. These dated facts explain the roadmap
|
||||
sequence only.
|
||||
The volatile release and backlog appendix that originally accompanied this
|
||||
roadmap has been removed so the durable direction cannot become a competing
|
||||
status source. The [Strategic Review 2026-08-05](../../archive/2026-08/STRATEGIC_REVIEW_2026-08-05.md)
|
||||
retains the dated assessment and reasoning. Current reconciliation belongs in
|
||||
[Strategy Status](../STRATEGY_STATUS.md), and live work state belongs in Gitea.
|
||||
@@ -4,82 +4,89 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan"
|
||||
version = "0.1.15"
|
||||
version = "0.1.46"
|
||||
description = "Developer convenience package for a versioned GovOPlaN composition"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
dependencies = [
|
||||
"govoplan-core[server]==0.1.15",
|
||||
"govoplan-tenancy==0.1.15",
|
||||
"govoplan-organizations==0.1.15",
|
||||
"govoplan-identity==0.1.15",
|
||||
"govoplan-idm==0.1.15",
|
||||
"govoplan-access==0.1.15",
|
||||
"govoplan-admin==0.1.15",
|
||||
"govoplan-policy==0.1.15",
|
||||
"govoplan-audit==0.1.15",
|
||||
"govoplan-dashboard==0.1.15",
|
||||
"govoplan-files==0.1.15",
|
||||
"govoplan-mail==0.1.15",
|
||||
"govoplan-campaign==0.1.15",
|
||||
"govoplan-calendar==0.1.15",
|
||||
"govoplan-docs==0.1.15",
|
||||
"govoplan-ops==0.1.15",
|
||||
"govoplan-core[server]==0.1.46",
|
||||
"govoplan-tenancy==0.1.22",
|
||||
"govoplan-organizations==0.1.21",
|
||||
"govoplan-identity==0.1.21",
|
||||
"govoplan-idm==0.1.26",
|
||||
"govoplan-access==0.1.25",
|
||||
"govoplan-admin==0.1.23",
|
||||
"govoplan-policy==0.1.23",
|
||||
"govoplan-audit==0.1.20",
|
||||
"govoplan-dashboard==0.1.20",
|
||||
"govoplan-files==0.1.27",
|
||||
"govoplan-mail==0.1.28",
|
||||
"govoplan-campaign==0.1.29",
|
||||
"govoplan-calendar==0.1.24",
|
||||
"govoplan-docs==0.1.23",
|
||||
"govoplan-ops==0.1.22",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
full = [
|
||||
"govoplan-addresses==0.1.15",
|
||||
"govoplan-approvals==0.1.15",
|
||||
"govoplan-assets==0.1.15",
|
||||
"govoplan-booking==0.1.15",
|
||||
"govoplan-cases==0.1.15",
|
||||
"govoplan-certificates==0.1.15",
|
||||
"govoplan-committee==0.1.15",
|
||||
"govoplan-connectors==0.1.15",
|
||||
"govoplan-consultation==0.1.15",
|
||||
"govoplan-contracts==0.1.15",
|
||||
"govoplan-dataflow==0.1.15",
|
||||
"govoplan-datasources==0.1.15",
|
||||
"govoplan-decisions==0.1.15",
|
||||
"govoplan-dist-lists==0.1.15",
|
||||
"govoplan-encryption==0.1.15",
|
||||
"govoplan-evaluation==0.1.15",
|
||||
"govoplan-facilities==0.1.15",
|
||||
"govoplan-forms==0.1.15",
|
||||
"govoplan-forms-runtime==0.1.15",
|
||||
"govoplan-grants==0.1.15",
|
||||
"govoplan-helpdesk==0.1.15",
|
||||
"govoplan-identity-trust==0.1.15",
|
||||
"govoplan-inspections==0.1.15",
|
||||
"govoplan-learning==0.1.15",
|
||||
"govoplan-mandates==0.1.15",
|
||||
"govoplan-notifications==0.1.15",
|
||||
"govoplan-parties==0.1.15",
|
||||
"govoplan-permits==0.1.15",
|
||||
"govoplan-poll==0.1.15",
|
||||
"govoplan-portal==0.1.15",
|
||||
"govoplan-postbox==0.1.15",
|
||||
"govoplan-procurement==0.1.15",
|
||||
"govoplan-projects==0.1.15",
|
||||
"govoplan-records==0.1.15",
|
||||
"govoplan-reporting==0.1.15",
|
||||
"govoplan-resources==0.1.15",
|
||||
"govoplan-rest==0.1.15",
|
||||
"govoplan-risk-compliance==0.1.15",
|
||||
"govoplan-scheduling==0.1.15",
|
||||
"govoplan-search==0.1.15",
|
||||
"govoplan-services==0.1.15",
|
||||
"govoplan-soap==0.1.15",
|
||||
"govoplan-templates==0.1.15",
|
||||
"govoplan-tickets==0.1.15",
|
||||
"govoplan-transparency==0.1.15",
|
||||
"govoplan-views==0.1.15",
|
||||
"govoplan-voting==0.1.15",
|
||||
"govoplan-wiki==0.1.15",
|
||||
"govoplan-workflow==0.1.15",
|
||||
"govoplan-workflow-engine==0.1.15",
|
||||
"govoplan-addresses==0.1.23",
|
||||
"govoplan-approvals==0.1.21",
|
||||
"govoplan-assets==0.1.20",
|
||||
"govoplan-booking==0.1.20",
|
||||
"govoplan-cases==0.1.25",
|
||||
"govoplan-certificates==0.1.20",
|
||||
"govoplan-committee==0.1.22",
|
||||
"govoplan-connectors==0.1.27",
|
||||
"govoplan-consultation==0.1.20",
|
||||
"govoplan-contracts==0.1.20",
|
||||
"govoplan-dataflow==0.1.25",
|
||||
"govoplan-datasources==0.1.26",
|
||||
"govoplan-decisions==0.1.19",
|
||||
"govoplan-dist-lists==0.1.21",
|
||||
"govoplan-dms==0.1.20",
|
||||
"govoplan-encryption==0.1.20",
|
||||
"govoplan-erp==0.1.20",
|
||||
"govoplan-evaluation==0.1.20",
|
||||
"govoplan-facilities==0.1.20",
|
||||
"govoplan-fit-connect==0.1.20",
|
||||
"govoplan-forms==0.1.23",
|
||||
"govoplan-forms-runtime==0.1.22",
|
||||
"govoplan-grants==0.1.20",
|
||||
"govoplan-helpdesk==0.1.21",
|
||||
"govoplan-identity-trust==0.1.21",
|
||||
"govoplan-inspections==0.1.20",
|
||||
"govoplan-learning==0.1.20",
|
||||
"govoplan-mandates==0.1.19",
|
||||
"govoplan-notifications==0.1.20",
|
||||
"govoplan-parties==0.1.19",
|
||||
"govoplan-payments==0.1.22",
|
||||
"govoplan-permits==0.1.20",
|
||||
"govoplan-poll==0.1.20",
|
||||
"govoplan-portal==0.1.22",
|
||||
"govoplan-postbox==0.1.23",
|
||||
"govoplan-procurement==0.1.20",
|
||||
"govoplan-projects==0.1.20",
|
||||
"govoplan-quick-access==0.1.21",
|
||||
"govoplan-records==0.1.24",
|
||||
"govoplan-reporting==0.1.22",
|
||||
"govoplan-resources==0.1.20",
|
||||
"govoplan-rest==0.1.19",
|
||||
"govoplan-risk-compliance==0.1.21",
|
||||
"govoplan-scheduling==0.1.22",
|
||||
"govoplan-search==0.1.20",
|
||||
"govoplan-services==0.1.19",
|
||||
"govoplan-soap==0.1.19",
|
||||
"govoplan-tasks==0.1.23",
|
||||
"govoplan-templates==0.1.22",
|
||||
"govoplan-tickets==0.1.23",
|
||||
"govoplan-transparency==0.1.20",
|
||||
"govoplan-views==0.1.22",
|
||||
"govoplan-voting==0.1.21",
|
||||
"govoplan-wiki==0.1.22",
|
||||
"govoplan-workflow==0.1.23",
|
||||
"govoplan-workflow-engine==0.1.21",
|
||||
"govoplan-xrechnung==0.1.21",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -28,5 +28,7 @@ The artifact remains a `product` package. Promotion to `reference` requires:
|
||||
access; and
|
||||
- version-pinned user and administrator documentation.
|
||||
|
||||
Optional Notifications, Portal, Reporting, and Workflow Engine integrations do
|
||||
not change the package boundary when absent.
|
||||
Optional Notifications, Portal, Reporting, Tasks, and Workflow Engine
|
||||
integrations do not change the package boundary when absent. When Tasks is
|
||||
present, acknowledgement, reconciliation, and operator intervention remain
|
||||
owned by their source modules and are projected into the common work inbox.
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
{"module_id": "notifications"},
|
||||
{"module_id": "portal"},
|
||||
{"module_id": "reporting"},
|
||||
{"module_id": "tasks"},
|
||||
{"module_id": "workflow_engine"}
|
||||
],
|
||||
"evidence": [
|
||||
|
||||
@@ -12,7 +12,7 @@ review.
|
||||
1. Register a typed datasource with source authority, purpose, classification,
|
||||
owner, freshness, and correction policy.
|
||||
2. Acquire or upload an immutable source state.
|
||||
3. execute a versioned flow and retain intermediate materializations and
|
||||
3. Execute a versioned flow and retain intermediate materializations and
|
||||
provenance;
|
||||
4. publish a report or decision input against exact source and flow revisions;
|
||||
5. link obligation, governed object, risk, control, evidence, finding,
|
||||
@@ -37,5 +37,28 @@ The artifact remains a `product` package. Promotion to `reference` requires:
|
||||
aggregate disclosure; and
|
||||
- version-pinned user and administrator documentation.
|
||||
|
||||
Optional Connectors, Files, Notifications, and Workflow Engine integrations
|
||||
must remain capability-based and absence-safe.
|
||||
Optional Connectors, Files, Notifications, Tasks, and Workflow Engine
|
||||
integrations must remain capability-based and absence-safe. Tasks may present
|
||||
review and recovery handoffs, but Dataflow and Risk Compliance remain the
|
||||
authoritative owners of run and screening state.
|
||||
|
||||
## Executable evidence
|
||||
|
||||
- `tools/checks/check-datasource-composition.py` composes Connector snapshots,
|
||||
governed Datasources, queued Dataflow execution, frozen publication,
|
||||
idempotent replay, and recovery evidence.
|
||||
- `govoplan-dataflow/fixtures/golden/monthly-reconciliation` pins synthetic
|
||||
monthly inputs, stable reconciliation hashes, reviewed decisions, expected
|
||||
output, source fingerprints, and output hashes.
|
||||
- `tools/checks/check-sanctions-screening-composition.py` composes an immutable
|
||||
Connector acquisition, idempotent Risk Compliance import and screening,
|
||||
independent disposition, a cleared gate, changed-source invalidation, and
|
||||
the rescreening queue through the registered versioned capabilities.
|
||||
- `govoplan-dataflow/fixtures/golden/sanctions-screening` independently proves
|
||||
the deterministic normalization and matching graph with exact expected
|
||||
output.
|
||||
|
||||
These checks use synthetic data and run without network access. They prove the
|
||||
module contracts and durable state transitions; they do not replace the
|
||||
deployment, security, privacy, accessibility, and operator evidence still
|
||||
listed above.
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
{"module_id": "connectors"},
|
||||
{"module_id": "files"},
|
||||
{"module_id": "notifications"},
|
||||
{"module_id": "tasks"},
|
||||
{"module_id": "workflow_engine"}
|
||||
],
|
||||
"evidence": [
|
||||
@@ -28,7 +29,17 @@
|
||||
"kind": "documentation",
|
||||
"reference": "packages/product/governed-data-assurance/README.md",
|
||||
"summary": "Defines the package boundary, provenance chain, and reference-readiness gates."
|
||||
},
|
||||
{
|
||||
"kind": "target_test",
|
||||
"reference": "tools/checks/check-datasource-composition.py",
|
||||
"summary": "Proves governed Connector acquisition, Datasource registration, queued Dataflow execution, frozen publication, idempotency, and recovery evidence."
|
||||
},
|
||||
{
|
||||
"kind": "target_test",
|
||||
"reference": "tools/checks/check-sanctions-screening-composition.py",
|
||||
"summary": "Proves immutable sanctions acquisition, import, screening replay, independent review, freshness gates, and rescreening across module capabilities."
|
||||
}
|
||||
],
|
||||
"tags": ["datasources", "dataflow", "reporting", "assurance"]
|
||||
"tags": ["datasources", "dataflow", "reporting", "sanctions", "assurance"]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,34 @@ resolution -> approval/deliberation -> formal Decision -> observed delivery
|
||||
effect -> record and review references
|
||||
```
|
||||
|
||||
The maintained concrete scenario is a German resident parking permit
|
||||
(`Anwohnerparkausweis`). Its versioned fixture is
|
||||
`tests/fixtures/resident_parking_permit_journey.json`. It pins the service,
|
||||
exact Form revision, resident inputs, digital and assisted channels, Case type,
|
||||
human review handoff, formal outcome, Postbox delivery channel, and Records
|
||||
filing/retention target. Generic permit wording is no longer acceptance
|
||||
evidence for this package.
|
||||
|
||||
The package is now executable rather than metadata-only. Its Access fragments
|
||||
create the bounded resident-permit clerk role, collect only the tenant-local
|
||||
responsibility group key and name, create that group, and bind the role. The
|
||||
Forms-owned fragment carries a digest-bound German-reference application schema
|
||||
and imports it as a tenant-local draft with source provenance. Reapplying the
|
||||
same source digest is a no-op; replacing an unrelated local definition remains
|
||||
blocked unless the reviewed package explicitly selects a new revision. Normal
|
||||
Forms review and publication are still required before the definition can serve
|
||||
new applications. The Workflow Engine-owned fragment materializes and activates
|
||||
the tenant review baseline, resolves the chosen responsibility group into each
|
||||
human handoff, and preserves the evidence, decision, and EUR 30 payment-review
|
||||
steps as a replay-safe contributed definition.
|
||||
|
||||
Services, Cases, Payments, Tasks, and the optional delivery and Records modules
|
||||
already execute the pinned journey through their runtime
|
||||
contracts, but their reusable configuration fragments are not yet claimed by
|
||||
this package. Until those module-owned configuration providers are added, the
|
||||
package preflight deliberately distinguishes the installed runtime composition
|
||||
from the Access, Forms, and Workflow configurations it can currently materialize.
|
||||
|
||||
An installed Forms and Forms Runtime pair adds an alternative governed entry
|
||||
path before case/workflow handoff:
|
||||
|
||||
@@ -17,6 +45,14 @@ Service discovery -> exact Form revision -> validated draft/submission
|
||||
-> receipt and handoff evidence -> Case or Workflow owner
|
||||
```
|
||||
|
||||
The assisted path now creates an authenticated, resumable session against that
|
||||
same exact Form revision. It records channel, affected and represented parties,
|
||||
authority, purpose, notice, responsible function, language, accessibility
|
||||
support, and field provenance. Submission fails closed until an immutable
|
||||
read-back outcome matches the current revision, values, attachments, and
|
||||
signatures. Saving a correction therefore requires a fresh confirmation rather
|
||||
than silently reusing old evidence.
|
||||
|
||||
Services, Cases, Parties, Mandates, Committee, and Decisions retain immutable
|
||||
provider-owned revisions for the parts they own. Portal, Cases, and Committee
|
||||
consume capabilities for cross-module semantics only. The package does not
|
||||
@@ -24,19 +60,53 @@ grant cross-module table access and can omit optional presentation, work,
|
||||
deliberation, delivery, or records modules while retaining explicit references
|
||||
to externally performed steps.
|
||||
|
||||
When Records is present, Forms Runtime, Cases, and Decisions expose exact,
|
||||
digest-bound source snapshots for explicit filing. The source module rechecks
|
||||
current access, Records chooses the destination and preserves chronology, and
|
||||
the filed reference never becomes an editable copy. When Search is present,
|
||||
the same three owners contribute rebuildable metadata-only projections. Form
|
||||
values, evidence payloads, Decision reasoning, operative results, and
|
||||
conditions are excluded; every candidate is authorized again before it is
|
||||
shown.
|
||||
|
||||
When Tasks is present, explicit work and source-owned Workflow handoffs appear
|
||||
in one resumable inbox with typed account, group, role, function, or assignment
|
||||
responsibility. Workflow Engine retains process state and completion commands;
|
||||
Tasks retains only explicit tasks and the aggregation surface.
|
||||
|
||||
## Security And Recovery
|
||||
|
||||
Every provider is tenant-bound. Missing or conflicting authority fails closed.
|
||||
Protected Decision content has a separate permission. Writes are replay-safe
|
||||
and OCC-guarded. Database restore is the semantic-state recovery unit; file and
|
||||
communication effects remain governed by their owning providers and are linked
|
||||
through requested/observed effect, evidence, and audit references.
|
||||
through requested/observed effect, evidence, and audit references. Search is a
|
||||
derived recovery unit and can be rebuilt from authoritative module state.
|
||||
|
||||
The executable fixture in
|
||||
`tests/test_institutional_governance_journey.py` proves SQL-backed Service,
|
||||
Case, Party, Mandate, Committee meeting/agendum/vote/minute, and Decision state.
|
||||
`tests/test_institutional_service_journey.py` separately proves exact Portal
|
||||
Form launch, persisted submission provenance, and idempotent replay.
|
||||
Target-environment accessibility, security, operator, privacy,
|
||||
delivery-provider, and recovery evidence are still required before this product
|
||||
package may claim `reference_ready` maturity.
|
||||
Form launch, persisted submission provenance, idempotent replay, resumable
|
||||
assisted intake with enforced read-back evidence, and a durable Workflow handoff
|
||||
that remains visible through Tasks after the database session is reopened and
|
||||
disappears only after the Workflow Engine records completion.
|
||||
Core's production-component browser conformance suite additionally executes the
|
||||
German self-service and assisted Anwohnerparkausweis paths at desktop and mobile
|
||||
widths. It proves native keyboard order, accessible names and landmarks, WCAG
|
||||
2.1 A/AA automation, responsive geometry, first-draft persistence, and mixed
|
||||
per-field person/document/system provenance. Physical screen-reader spot checks
|
||||
remain target-environment release evidence.
|
||||
Module-level Records source tests prove exact Form submission, Case revision,
|
||||
and Decision revision filing. Target-environment browser accessibility,
|
||||
production identity and delivery, a named archive profile, and recovery evidence
|
||||
are still required before this product package may claim `reference_ready`
|
||||
maturity.
|
||||
|
||||
The generic package orchestrator stops at the first provider apply or health
|
||||
blocker. Access and Forms may commit in separate provider transactions, so the
|
||||
operator must retain the reviewed pre-apply database snapshot until verification
|
||||
is complete. The Admin result reports no-op, snapshot-required, or partial-apply
|
||||
recovery state and never describes this as atomic cross-module undo. Exported
|
||||
fragments carry source/module/operator/scope provenance; supplied values and
|
||||
credentials are not serialized into that provenance.
|
||||
|
||||
@@ -8,35 +8,366 @@
|
||||
"category": "institutional-governance",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"required_modules": [
|
||||
{"module_id": "access"},
|
||||
{"module_id": "audit"},
|
||||
{"module_id": "cases"},
|
||||
{"module_id": "decisions"},
|
||||
{"module_id": "forms"},
|
||||
{"module_id": "forms_runtime"},
|
||||
{"module_id": "mandates"},
|
||||
{"module_id": "parties"},
|
||||
{"module_id": "payments"},
|
||||
{"module_id": "policy"},
|
||||
{"module_id": "portal"},
|
||||
{"module_id": "services"}
|
||||
{"module_id": "services"},
|
||||
{"module_id": "tasks"},
|
||||
{"module_id": "workflow_engine"}
|
||||
],
|
||||
"required_capabilities": [
|
||||
"access.configuration",
|
||||
"cases.party_context",
|
||||
"cases.service_intake",
|
||||
"decisions.registry",
|
||||
"forms.configuration",
|
||||
"forms.definitions",
|
||||
"mandates.resolver",
|
||||
"parties.resolver",
|
||||
"payments.requests",
|
||||
"portal.service_directory",
|
||||
"services.availability",
|
||||
"services.definitions"
|
||||
"services.definitions",
|
||||
"workflow.configuration"
|
||||
],
|
||||
"optional_modules": [
|
||||
{"module_id": "approvals"},
|
||||
{"module_id": "committee"},
|
||||
{"module_id": "files"},
|
||||
{"module_id": "forms"},
|
||||
{"module_id": "forms_runtime"},
|
||||
{"module_id": "postbox"},
|
||||
{"module_id": "records"},
|
||||
{"module_id": "tasks"},
|
||||
{"module_id": "workflow_engine"}
|
||||
{"module_id": "search"}
|
||||
],
|
||||
"data_requirements": [
|
||||
{
|
||||
"key": "responsible_group_slug",
|
||||
"label": "Responsible permit group key",
|
||||
"data_type": "string",
|
||||
"required": true,
|
||||
"secret": false,
|
||||
"description": "Tenant-local stable key for the group that reviews resident parking permit applications."
|
||||
},
|
||||
{
|
||||
"key": "responsible_group_name",
|
||||
"label": "Responsible permit group name",
|
||||
"data_type": "string",
|
||||
"required": true,
|
||||
"secret": false,
|
||||
"description": "Human-readable tenant-local name shown for the responsible permit group."
|
||||
}
|
||||
],
|
||||
"fragments": [
|
||||
{
|
||||
"module_id": "access",
|
||||
"fragment_type": "roles",
|
||||
"fragment_id": "resident-parking-permit-clerk",
|
||||
"payload": {
|
||||
"items": [
|
||||
{
|
||||
"slug": "resident-parking-permit-clerk",
|
||||
"name": "Resident parking permit clerk",
|
||||
"description": "Reviews resident parking permit submissions, workflow handoffs, cases, decisions, and payment evidence.",
|
||||
"permissions": [
|
||||
"cases:case:read",
|
||||
"cases:case:create",
|
||||
"cases:case:update",
|
||||
"decisions:decision:read",
|
||||
"decisions:decision:write",
|
||||
"forms:definition:read",
|
||||
"forms_runtime:workspace:read",
|
||||
"forms_runtime:workspace:write",
|
||||
"payments:payment:read",
|
||||
"payments:payment:write",
|
||||
"tasks:item:read",
|
||||
"tasks:item:write",
|
||||
"workflow:definition:read",
|
||||
"workflow:instance:read",
|
||||
"workflow:instance:start",
|
||||
"workflow:instance:transition"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"module_id": "access",
|
||||
"fragment_type": "groups",
|
||||
"fragment_id": "resident-parking-permit-responsibility",
|
||||
"payload": {
|
||||
"items": [
|
||||
{
|
||||
"slug": {"$data": "responsible_group_slug"},
|
||||
"name": {"$data": "responsible_group_name"},
|
||||
"description": "Tenant-local responsibility group for the resident parking permit reference journey."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"module_id": "access",
|
||||
"fragment_type": "group_role_assignments",
|
||||
"fragment_id": "resident-parking-permit-clerk-assignment",
|
||||
"payload": {
|
||||
"items": [
|
||||
{
|
||||
"group": {"$data": "responsible_group_slug"},
|
||||
"role": "resident-parking-permit-clerk"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"module_id": "forms",
|
||||
"fragment_type": "definition",
|
||||
"fragment_id": "resident-parking-permit-application",
|
||||
"payload": {
|
||||
"on_conflict": "new_revision",
|
||||
"change_reason": "Install the reviewed resident parking permit reference form.",
|
||||
"fragment": {
|
||||
"kind": "govoplan.forms.definition",
|
||||
"contract_version": "0.1.0",
|
||||
"definition": {
|
||||
"reference": {
|
||||
"kind": "form",
|
||||
"owner_module": "forms",
|
||||
"object_id": "resident-parking-permit-application",
|
||||
"tenant_id": "reference-package",
|
||||
"version": "3",
|
||||
"valid_at": null,
|
||||
"label": null
|
||||
},
|
||||
"key": "resident-parking-permit-application",
|
||||
"temporal": {
|
||||
"revision": "3",
|
||||
"valid_from": null,
|
||||
"valid_to": null,
|
||||
"recorded_at": "2026-08-22T00:00:00+00:00",
|
||||
"superseded_at": null,
|
||||
"change_reason": "Reference package revision."
|
||||
},
|
||||
"title": "Resident parking permit",
|
||||
"description": "Apply for a resident parking permit through a digital or assisted channel.",
|
||||
"fields": [
|
||||
{
|
||||
"key": "applicant_name",
|
||||
"label": "Name",
|
||||
"value_type": "text",
|
||||
"required": true,
|
||||
"help_text": null,
|
||||
"options": [],
|
||||
"constraints": {"min_length": 2, "max_length": 200},
|
||||
"default_value": null
|
||||
},
|
||||
{
|
||||
"key": "applicant_email",
|
||||
"label": "Email",
|
||||
"value_type": "text",
|
||||
"required": true,
|
||||
"help_text": null,
|
||||
"options": [],
|
||||
"constraints": {"format": "email"},
|
||||
"default_value": null
|
||||
},
|
||||
{
|
||||
"key": "residence_address",
|
||||
"label": "Primary residence",
|
||||
"value_type": "text",
|
||||
"required": true,
|
||||
"help_text": null,
|
||||
"options": [],
|
||||
"constraints": {"max_length": 500},
|
||||
"default_value": null
|
||||
},
|
||||
{
|
||||
"key": "licence_plate",
|
||||
"label": "Licence plate",
|
||||
"value_type": "text",
|
||||
"required": true,
|
||||
"help_text": null,
|
||||
"options": [],
|
||||
"constraints": {"max_length": 20},
|
||||
"default_value": null
|
||||
}
|
||||
],
|
||||
"publication_state": "published",
|
||||
"allow_drafts": true,
|
||||
"max_attachments": 4,
|
||||
"signature_requirement": "none",
|
||||
"policy_refs": [
|
||||
"law:resident-parking-permit",
|
||||
"records:resident-parking-permit"
|
||||
],
|
||||
"handoff_kinds": ["case", "workflow"],
|
||||
"metadata": {},
|
||||
"pages": [
|
||||
{
|
||||
"key": "application",
|
||||
"title": "Application",
|
||||
"description": null,
|
||||
"sections": [
|
||||
{
|
||||
"key": "applicant-and-vehicle",
|
||||
"title": "Applicant and vehicle",
|
||||
"description": null,
|
||||
"field_keys": [
|
||||
"applicant_name",
|
||||
"applicant_email",
|
||||
"residence_address",
|
||||
"licence_plate"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"localizations": [
|
||||
{
|
||||
"locale": "de",
|
||||
"title": "Anwohnerparkausweis beantragen",
|
||||
"description": "Einen Anwohnerparkausweis digital oder mit Unterstützung beantragen.",
|
||||
"field_labels": {
|
||||
"applicant_name": "Name",
|
||||
"applicant_email": "E-Mail-Adresse",
|
||||
"residence_address": "Hauptwohnsitz",
|
||||
"licence_plate": "Kennzeichen"
|
||||
},
|
||||
"field_help_texts": {},
|
||||
"option_labels": {},
|
||||
"page_titles": {"application": "Antrag"},
|
||||
"section_titles": {
|
||||
"applicant-and-vehicle": "Antragstellende Person und Fahrzeug"
|
||||
}
|
||||
}
|
||||
],
|
||||
"fallback_locale": "de"
|
||||
},
|
||||
"definition_sha256": "7dc108002d532c07e5e7f3b14029a9d4deb3836ebb65d97fb6b51166a70e0ed4",
|
||||
"provenance": {
|
||||
"owner_module": "forms",
|
||||
"tenant_id": "reference-package",
|
||||
"form_id": "resident-parking-permit-application",
|
||||
"revision": "3",
|
||||
"exported_at": "2026-08-22T12:00:00+00:00",
|
||||
"exported_by": "GovOPlaN reference package"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"module_id": "workflow_engine",
|
||||
"fragment_type": "workflow_definitions",
|
||||
"fragment_id": "resident-parking-permit-workflow",
|
||||
"payload": {
|
||||
"schema_version": 1,
|
||||
"origin_module_id": "configuration_package.service_to_decision",
|
||||
"origin_module_version": "0.1.0",
|
||||
"items": [
|
||||
{
|
||||
"definition_key": "resident-parking-permit-review",
|
||||
"name": "Resident parking permit review",
|
||||
"description": "Review evidence, record the formal decision, and verify payment evidence for the resident parking permit reference journey.",
|
||||
"scope_type": "tenant",
|
||||
"allow_start": true,
|
||||
"allow_reuse": true,
|
||||
"allow_automation": false,
|
||||
"execution_mode": "guided",
|
||||
"activate_on_install": true,
|
||||
"graph": {
|
||||
"schema_version": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"type": "workflow.start.manual",
|
||||
"label": "Application received",
|
||||
"config": {"input_schema_ref": "form:resident-parking-permit-application"}
|
||||
},
|
||||
{
|
||||
"id": "review-evidence",
|
||||
"type": "workflow.review",
|
||||
"label": "Review application evidence",
|
||||
"config": {
|
||||
"title": "Review resident parking permit evidence",
|
||||
"reviewer": {
|
||||
"kind": "group",
|
||||
"id": {"$data": "responsible_group_slug"},
|
||||
"label": {"$data": "responsible_group_name"}
|
||||
},
|
||||
"due_after": "P14D",
|
||||
"required_evidence": [
|
||||
"identity",
|
||||
"primary_residence",
|
||||
"vehicle_registration"
|
||||
],
|
||||
"view_surface_ids": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "record-decision",
|
||||
"type": "workflow.activity",
|
||||
"label": "Record formal decision",
|
||||
"config": {
|
||||
"title": "Record the resident parking permit decision",
|
||||
"instructions": "Record the operative result, reasoning, legal basis, remedy, and exact evidence references through the Decisions capability.",
|
||||
"assignee": {
|
||||
"kind": "group",
|
||||
"id": {"$data": "responsible_group_slug"},
|
||||
"label": {"$data": "responsible_group_name"}
|
||||
},
|
||||
"due_after": "P7D",
|
||||
"view_surface_ids": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "verify-payment",
|
||||
"type": "workflow.activity",
|
||||
"label": "Verify payment evidence",
|
||||
"config": {
|
||||
"title": "Verify the resident parking permit fee",
|
||||
"instructions": "Verify the EUR 30.00 obligation, immutable receipt evidence, currency, amount, and transaction reference before completion.",
|
||||
"assignee": {
|
||||
"kind": "group",
|
||||
"id": {"$data": "responsible_group_slug"},
|
||||
"label": {"$data": "responsible_group_name"}
|
||||
},
|
||||
"due_after": "P14D",
|
||||
"view_surface_ids": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "completed",
|
||||
"type": "workflow.end.completed",
|
||||
"label": "Permit journey complete",
|
||||
"config": {"output_mapping": {}}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{"id": "start-review", "source": "start", "target": "review-evidence"},
|
||||
{"id": "review-decision", "source": "review-evidence", "source_port": "approved", "target": "record-decision"},
|
||||
{"id": "decision-payment", "source": "record-decision", "target": "verify-payment"},
|
||||
{"id": "payment-completed", "source": "verify-payment", "target": "completed"}
|
||||
],
|
||||
"metadata": {
|
||||
"reference_journey": "resident-parking-permit",
|
||||
"locale": "de-DE",
|
||||
"payment_amount_minor": 3000,
|
||||
"payment_currency": "EUR"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"reference_package": "product.service-to-decision",
|
||||
"form_id": "resident-parking-permit-application"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
@@ -44,6 +375,11 @@
|
||||
"reference": "packages/product/service-to-decision/README.md",
|
||||
"summary": "Defines the package boundary, authority path, recovery contract, and known operational limits."
|
||||
},
|
||||
{
|
||||
"kind": "target_test",
|
||||
"reference": "tests/fixtures/resident_parking_permit_journey.json",
|
||||
"summary": "Pins the resident parking permit actors, channels, exact inputs, work item, formal outcome, filing target, and remaining manual acceptance gates."
|
||||
},
|
||||
{
|
||||
"kind": "target_test",
|
||||
"reference": "tests/test_institutional_governance_journey.py",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
{"name": "govoplan-postbox", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-postbox.git", "path": "govoplan-postbox"},
|
||||
{"name": "govoplan-procurement", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-procurement.git", "path": "govoplan-procurement"},
|
||||
{"name": "govoplan-projects", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-projects.git", "path": "govoplan-projects"},
|
||||
{"name": "govoplan-quick-access", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-quick-access.git", "path": "govoplan-quick-access"},
|
||||
{"name": "govoplan-records", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-records.git", "path": "govoplan-records"},
|
||||
{"name": "govoplan-reporting", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-reporting.git", "path": "govoplan-reporting"},
|
||||
{"name": "govoplan-resources", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-resources.git", "path": "govoplan-resources"},
|
||||
|
||||
@@ -2,7 +2,7 @@ bandit>=1.8,<2
|
||||
click>=8.3.3
|
||||
filelock>=3.20.3
|
||||
idna>=3.15
|
||||
pip>=26.1.2
|
||||
pip>=26.2
|
||||
pip-audit>=2.9,<3
|
||||
python-multipart>=0.0.31
|
||||
radon>=6,<7
|
||||
|
||||
@@ -30,11 +30,14 @@
|
||||
-e ../govoplan-parties
|
||||
-e ../govoplan-mandates
|
||||
-e ../govoplan-decisions
|
||||
-e ../govoplan-payments
|
||||
-e ../govoplan-connectors
|
||||
-e ../govoplan-datasources
|
||||
-e ../govoplan-dataflow
|
||||
-e ../govoplan-workflow-engine
|
||||
-e ../govoplan-workflow
|
||||
-e ../govoplan-tasks
|
||||
-e ../govoplan-quick-access
|
||||
-e ../govoplan-views
|
||||
-e ../govoplan-voting
|
||||
-e ../govoplan-search
|
||||
@@ -53,7 +56,7 @@ httpx2>=2.5,<3
|
||||
filelock>=3.20.3
|
||||
idna>=3.15
|
||||
jsonschema>=4,<5
|
||||
pip>=26.1.2
|
||||
pip>=26.2
|
||||
pip-audit>=2.9,<3
|
||||
pytest>=9.0.3,<10
|
||||
pygments>=2.20,<3
|
||||
|
||||
+15
-15
@@ -1,18 +1,18 @@
|
||||
# Whole-product release install from immutable, independently versioned module tags.
|
||||
# Only add a module after its referenced tag has been published.
|
||||
../govoplan-core[server]
|
||||
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.15
|
||||
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.15
|
||||
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.15
|
||||
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.15
|
||||
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.15
|
||||
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.15
|
||||
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.15
|
||||
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.15
|
||||
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.15
|
||||
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.15
|
||||
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.15
|
||||
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.15
|
||||
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.15
|
||||
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.15
|
||||
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.15
|
||||
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.22
|
||||
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.21
|
||||
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.21
|
||||
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.26
|
||||
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.25
|
||||
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.23
|
||||
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.23
|
||||
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.20
|
||||
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.20
|
||||
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.27
|
||||
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.28
|
||||
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.29
|
||||
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.24
|
||||
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.23
|
||||
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.22
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"id": "resident-parking-permit-berlin-style-reference",
|
||||
"title": "Resident parking permit",
|
||||
"title_de": "Anwohnerparkausweis",
|
||||
"locale": "de-DE",
|
||||
"service": {
|
||||
"object_id": "resident-parking-permit",
|
||||
"key": "resident_parking_permit.apply",
|
||||
"version": "6",
|
||||
"audience": "resident",
|
||||
"required_evidence_types": [
|
||||
"application",
|
||||
"identity",
|
||||
"primary_residence",
|
||||
"vehicle_registration"
|
||||
],
|
||||
"channels": ["portal", "assisted"]
|
||||
},
|
||||
"form": {
|
||||
"object_id": "resident-parking-permit-application",
|
||||
"version": "3",
|
||||
"fields": {
|
||||
"applicant_name": "Ada Lovelace",
|
||||
"applicant_email": "ada.lovelace@example.test",
|
||||
"residence_address": "Musterstrasse 17, 10115 Berlin",
|
||||
"licence_plate": "B-AL 1843"
|
||||
}
|
||||
},
|
||||
"status_access": {
|
||||
"mode": "email_link",
|
||||
"email_field_key": "applicant_email",
|
||||
"token_ttl_seconds": 1800,
|
||||
"request_limit_per_hour": 3
|
||||
},
|
||||
"assisted_intake": {
|
||||
"channel": "counter",
|
||||
"affected_party_ref": "party:resident-ada-lovelace",
|
||||
"represented_party_ref": null,
|
||||
"authority_basis": "self",
|
||||
"purpose": "Apply for a resident parking permit.",
|
||||
"legal_basis_ref": "law:resident-parking-permit",
|
||||
"consent_basis": "in-person-confirmation",
|
||||
"notice_given": true,
|
||||
"responsible_function_ref": "function:parking-permits",
|
||||
"language": "de",
|
||||
"accessibility_needs": ["plain-language"],
|
||||
"confirmation_method": "written_preview",
|
||||
"confirmation_outcome": "confirmed"
|
||||
},
|
||||
"case": {
|
||||
"type_key": "resident-parking-permit-application",
|
||||
"number": "RPP-2026-0001",
|
||||
"initial_status": "intake",
|
||||
"decided_status": "decided",
|
||||
"deadline_days": 30
|
||||
},
|
||||
"workflow": {
|
||||
"definition_name": "Resident parking permit decision",
|
||||
"work_item_title": "Decide the resident parking permit application",
|
||||
"instructions": "Review identity, primary residence, vehicle evidence, and the effective local rule before recording the decision."
|
||||
},
|
||||
"decision": {
|
||||
"type": "resident-parking-permit",
|
||||
"operative_result": "Resident parking permit granted.",
|
||||
"reasoning": "Identity, primary residence, vehicle registration, and the effective local rule were verified.",
|
||||
"delivery_channel": "postbox",
|
||||
"remedy": "review:administrative-court"
|
||||
},
|
||||
"payment": {
|
||||
"mode": "manual",
|
||||
"amount_minor": 3000,
|
||||
"currency": "EUR",
|
||||
"subject": "Resident parking permit fee",
|
||||
"due_days": 14,
|
||||
"evidence_owner": "files"
|
||||
},
|
||||
"records": {
|
||||
"file_plan_key": "traffic.resident-parking-permits",
|
||||
"retention_policy_ref": "records:resident-parking-permit"
|
||||
},
|
||||
"acceptance": {
|
||||
"automated": [
|
||||
"The published service and exact form revision drive digital intake.",
|
||||
"An authenticated assisted session uses the same exact form and validation rules while retaining purpose, authority, channel, party, accessibility, source, correction, and read-back provenance.",
|
||||
"The configured applicant email issues a short-lived, hash-only status link through Notifications and exposes only the bounded status timeline.",
|
||||
"An idempotent replay returns the same persisted submission.",
|
||||
"The human review handoff survives a database-session restart and remains visible in Tasks until completion.",
|
||||
"The production self-service and assisted WebUI paths preserve keyboard order, accessible names, WCAG 2.1 A/AA automation, and responsive geometry at desktop and mobile widths.",
|
||||
"The assisted operator can assign independent source, confidence, and governed declaring-party, document, or system references to every populated field before immutable read-back.",
|
||||
"The formal decision retains party, mandate, legal-basis, evidence, delivery, review, and exact revision references.",
|
||||
"The Case-bound payment handoff creates a replay-safe obligation and accepts a full manual receipt only with exact amount, currency, transaction reference, and immutable evidence.",
|
||||
"Forms Runtime, Cases, and Decisions can expose exact snapshots for explicit eAkte filing."
|
||||
],
|
||||
"manual_or_target": [
|
||||
"Perform physical screen-reader spot checks for the digital journey at desktop and mobile widths.",
|
||||
"Perform physical screen-reader spot checks for the assisted operator journey at desktop and mobile widths.",
|
||||
"Open, resend, expire, and revoke the applicant status link with keyboard and screen reader at desktop and mobile widths.",
|
||||
"Verify the configured Postbox or external delivery provider, including unknown outcome and reconciliation.",
|
||||
"Restore the pinned composition and reconstruct the exact form, case, decision, delivery evidence, and eAkte chronology.",
|
||||
"Transfer through a named archive profile and retain independently signed target evidence."
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { auditRepository } from "../tools/devkit/audit-display-labels.mjs";
|
||||
|
||||
const require = createRequire(resolve(import.meta.dirname, "../../govoplan-core/webui/package.json"));
|
||||
const ts = require("typescript");
|
||||
function fixture(fn) {
|
||||
const root = mkdtempSync(join(tmpdir(), "govoplan-label-audit-"));
|
||||
const write = (relative, content) => { const target = join(root, relative); mkdirSync(dirname(target), { recursive: true }); writeFileSync(target, content); };
|
||||
try {
|
||||
write("core/webui/src/i18n/generatedTranslations.ts", 'export const generatedTranslations = { en: { Shared: "Shared" }, de: { Shared: "Gemeinsam" } };');
|
||||
return fn(root, write);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
}
|
||||
|
||||
test("plain labels resolve owning registered catalogs, aliases, constants and Core defaults", () => fixture((root, write) => {
|
||||
write("module/webui/src/i18n/generatedTranslations.ts", 'const en = { Projects: "Projects" }; const de = { Projects: "Projekte" }; export const generatedTranslations = { en, de };');
|
||||
write("module/webui/src/module.ts", 'import { generatedTranslations as words } from "./i18n/generatedTranslations"; export const exampleModule: PlatformWebModule = { translations: words };');
|
||||
write("module/webui/src/Page.tsx", 'import { PageLayout as Layout, PageTitle } from "@govoplan/core-webui"; const title = "Projects"; export const page = <><Layout title={title} /><PageTitle>Shared</PageTitle></>;');
|
||||
const result = auditRepository(ts, join(root, "module"), join(root, "core"));
|
||||
assert.equal(result.registration, "registered");
|
||||
assert.equal(result.labels.length, 2);
|
||||
assert.deepEqual(result.findings, []);
|
||||
}));
|
||||
|
||||
test("a catalog file without module registration cannot mask untranslated titles", () => fixture((root, write) => {
|
||||
write("module/webui/src/i18n/generatedTranslations.ts", 'export const generatedTranslations = { en: { Projects: "Projects" }, de: { Projects: "Projekte" } };');
|
||||
write("module/webui/src/module.ts", 'export const exampleModule = { id: "example" };');
|
||||
write("module/webui/src/Page.tsx", 'import * as Core from "@govoplan/core-webui"; export const page = <Core.PageLayout title="Projects" />;');
|
||||
const result = auditRepository(ts, join(root, "module"), join(root, "core"));
|
||||
assert(result.findings.some((item) => item.code === "catalog-not-registered"));
|
||||
assert.deepEqual(result.labels[0].missing_locales, ["en", "de"]);
|
||||
}));
|
||||
|
||||
test("only known static display slots fail; markers and runtime data remain separate", () => fixture((root, write) => {
|
||||
write("module/webui/src/Page.tsx", 'import { PageLayout } from "@govoplan/core-webui"; export const page = <><PageLayout title="Untranslated" /><PageLayout title={campaign.name} /><PageLayout title="i18n:known.key" /><CustomThing title="Not a known slot" /></>;');
|
||||
const result = auditRepository(ts, join(root, "module"), join(root, "core"));
|
||||
assert.equal(result.findings.length, 1);
|
||||
assert.equal(result.findings[0].text, "Untranslated");
|
||||
assert.equal(result.review.length, 1);
|
||||
assert.equal(result.review[0].code, "dynamic-display-slot");
|
||||
}));
|
||||
|
||||
test("dynamic translation registration is reported for review, not falsely missing", () => fixture((root, write) => {
|
||||
write("module/webui/src/module.ts", 'export const exampleModule: PlatformWebModule = { translations: configuredCatalog() };');
|
||||
write("module/webui/src/Page.tsx", 'export const page = <PageTitle>Example</PageTitle>;');
|
||||
const result = auditRepository(ts, join(root, "module"), join(root, "core"));
|
||||
assert.equal(result.registration, "dynamic");
|
||||
assert.deepEqual(result.findings, []);
|
||||
assert(result.review.some((item) => item.code === "dynamic-catalog-review"));
|
||||
}));
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { findDetachedDocumentation } from "../tools/checks/check-heading-help.mjs";
|
||||
import "./test-heading-translations.mjs";
|
||||
|
||||
const inspect = (source) => findDetachedDocumentation([{ path: "/fixture/Page.tsx", source }]).findings;
|
||||
const book = '<DocumentationHelpLink reference={topic} />';
|
||||
|
||||
test("heading and text contracts accept contextual help", () => {
|
||||
for (const component of ["PageHeader", "PageLayout", "AdminPageLayout", "Card", "Dialog", "PageActionBar", "WorkspaceActionBar"]) {
|
||||
assert.equal(inspect(`const Page=()=> <${component} title="Topic" titleHelp={${book}} />`).length, 0, component);
|
||||
}
|
||||
assert.equal(inspect(`const Page=()=> <PageTitle titleHelp={${book}}>Topic</PageTitle>`).length, 0);
|
||||
assert.equal(inspect(`const Page=()=> <TextWithHelp help={${book}}>Existing label</TextWithHelp>`).length, 0);
|
||||
});
|
||||
|
||||
test("detached action and body links are rejected", () => {
|
||||
for (const source of [`<PageActionBar helpAction={${book}} />`, `<Card title="Topic" actions={${book}} />`, `<div>${book}</div>`, `<Card title={${book}} />`]) {
|
||||
assert.equal(inspect(`const Page=()=> ${source}`).length, 1, source);
|
||||
}
|
||||
});
|
||||
|
||||
test("aliases, conditional help and simple local references remain checked", () => {
|
||||
assert.equal(inspect(`import {DocumentationHelpLink as Book, Card as Box} from '@govoplan/core-webui'; const Page=()=> <Box title="Topic" titleHelp={<Book reference={topic}/>} />`).length, 0);
|
||||
assert.equal(inspect(`import {DocumentationHelpLink as Book} from '@govoplan/core-webui'; const Page=()=> <div><Book reference={topic}/></div>`).length, 1);
|
||||
assert.equal(inspect(`const help=${book}; const Page=()=> <Card title="Topic" titleHelp={enabled ? help : null}/>`).length, 0);
|
||||
assert.equal(inspect(`const help=${book}; const Page=()=> <><Card title="Topic" titleHelp={help}/><PageActionBar helpAction={help}/></>`).length, 1);
|
||||
assert.equal(inspect(`const help=${book}; const again=help; const Page=()=> <Card title="Topic" titleHelp={again}/>`).length, 0);
|
||||
});
|
||||
|
||||
test("empty anchors, unknown contracts and nested interactive elements fail", () => {
|
||||
for (const source of [`<Card titleHelp={${book}}/>`, `<TextWithHelp help={${book}}/>`, `<TextWithHelp help={${book}}> </TextWithHelp>`, `<Unknown title="Topic" titleHelp={${book}}/>`, `<Card title="Topic" titleHelp={<button>${book}</button>}/>`]) {
|
||||
assert.equal(inspect(`const Page=()=> ${source}`).length, 1, source);
|
||||
}
|
||||
});
|
||||
|
||||
test("namespace and explicit component default imports cannot bypass placement", () => {
|
||||
for (const source of [
|
||||
`import * as UI from '@govoplan/core-webui'; const Page=()=> <div><UI.DocumentationHelpLink reference={topic}/></div>`,
|
||||
`import Book from './components/help/DocumentationHelpLink'; const Page=()=> <div><Book reference={topic}/></div>`,
|
||||
]) assert.equal(inspect(source).length, 1, source);
|
||||
assert.equal(inspect(`import * as UI from '@govoplan/core-webui'; const Page=()=> <UI.Card title="Topic" titleHelp={<UI.DocumentationHelpLink reference={topic}/>}/>`).length, 0);
|
||||
assert.equal(inspect(`import Book from './components/help/DocumentationHelpLink'; import Label from './components/help/TextWithHelp'; const Page=()=> <Label help={<Book reference={topic}/>}>Topic</Label>`).length, 0);
|
||||
assert.equal(findDetachedDocumentation([{ path: "/fixture/Page.tsx", source: `import Book from './business/Book'; const Page=()=> <div><Book/></div>` }]).links, 0);
|
||||
});
|
||||
|
||||
test("outer interactive containers and statically absent text are rejected", () => {
|
||||
for (const source of [
|
||||
`<button><TextWithHelp help={${book}}>Topic</TextWithHelp></button>`,
|
||||
`<a href="/other"><Card title="Topic" titleHelp={${book}}/></a>`,
|
||||
`<Card title="" titleHelp={${book}}/>`,
|
||||
`<Card title={false} titleHelp={${book}}/>`,
|
||||
`<PageTitle titleHelp={${book}}>{null}</PageTitle>`,
|
||||
`<TextWithHelp help={${book}}>{/* Topic */}</TextWithHelp>`,
|
||||
`<TextWithHelp help={${book}}>{undefined}</TextWithHelp>`,
|
||||
`<TextWithHelp help={${book}}><span hidden>Topic</span></TextWithHelp>`,
|
||||
`<TextWithHelp hidden help={${book}}>Topic</TextWithHelp>`,
|
||||
]) assert.equal(inspect(`const Page=()=> ${source}`).length, 1, source);
|
||||
assert.equal(inspect(`const help=${book}; const Page=()=> <Button><TextWithHelp help={help}>Topic</TextWithHelp></Button>`).length, 1);
|
||||
assert.equal(inspect(`const title=null; const Page=()=> <Card title={title} titleHelp={${book}}/>`).length, 1);
|
||||
assert.equal(inspect(`const Page=()=> <Card title={translateText(title)} titleHelp={${book}}/>`).length, 0);
|
||||
assert.equal(inspect(`const Page=()=> <TextWithHelp help={${book}}><TranslatedTitle/></TextWithHelp>`).length, 0);
|
||||
assert.equal(inspect(`let title=""; title=translateText(key); const Page=()=> <Card title={title} titleHelp={${book}}/>`).length, 0);
|
||||
});
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { resolve } from "node:path";
|
||||
import { test } from "node:test";
|
||||
|
||||
const root = resolve(import.meta.dirname, "../..");
|
||||
const require = createRequire(resolve(root, "govoplan-core/webui/package.json"));
|
||||
const { buildSync } = require("esbuild");
|
||||
const labels = {
|
||||
approvals: { "Approval requests": "Genehmigungsanträge" },
|
||||
forms: { "Form meaning": "Formularbedeutung" },
|
||||
"forms-runtime": { "Form instance": "Formularinstanz", "Forms runtime": "Formularlaufzeit" },
|
||||
helpdesk: { "Helpdesk profiles": "Helpdesk-Profile" },
|
||||
portal: { "Service directory": "Leistungsverzeichnis" },
|
||||
projects: { Projects: "Projekte" },
|
||||
reporting: { Reporting: "Reporting" },
|
||||
"risk-compliance": { "Risk Compliance": "Risiko und Compliance" },
|
||||
scheduling: { Scheduling: "Terminplanung" },
|
||||
tickets: { Tickets: "Tickets" },
|
||||
voting: { Ballots: "Abstimmungen" },
|
||||
wiki: { Wiki: "Wiki" },
|
||||
workflow: { Workflows: "Workflows", Workflow: "Workflow" },
|
||||
idm: {
|
||||
"Direct changes to this governed function are": "Direkte Änderungen an dieser gesteuerten Funktion sind",
|
||||
"emergency overrides": "Notfallübersteuerungen",
|
||||
". Use a request or grant above for the normal process.": ". Verwenden Sie für den regulären Prozess einen Antrag oder eine Vergabe.",
|
||||
},
|
||||
};
|
||||
|
||||
// Render the actual Core heading/locale contract with one owning module at a
|
||||
// time. The in-memory bundle creates no shared component-build artifacts.
|
||||
const names = Object.keys(labels);
|
||||
const imports = names.map((name, index) => `import { generatedTranslations as t${index} } from ${JSON.stringify(resolve(root, `govoplan-${name}/webui/src/i18n/generatedTranslations.ts`))};`).join("\n");
|
||||
const { outputFiles } = buildSync({
|
||||
stdin: {
|
||||
contents: `${imports}
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { PlatformLanguageProvider } from ${JSON.stringify(resolve(root, "govoplan-core/webui/src/i18n/LanguageContext.tsx"))};
|
||||
import PageTitle from ${JSON.stringify(resolve(root, "govoplan-core/webui/src/components/PageTitle.tsx"))};
|
||||
import DocumentationHelpLink from ${JSON.stringify(resolve(root, "govoplan-core/webui/src/components/help/DocumentationHelpLink.tsx"))};
|
||||
export const catalogs = [${names.map((_, index) => `t${index}`).join(",")}];
|
||||
export function heading(index, language, label) {
|
||||
return renderToStaticMarkup(<PlatformLanguageProvider preferredLanguageCode={language} moduleTranslations={[catalogs[index]]}>
|
||||
<PageTitle titleHelp={<DocumentationHelpLink reference={{ contextId: "heading-test" }} />}>{label}</PageTitle>
|
||||
</PlatformLanguageProvider>);
|
||||
}`,
|
||||
loader: "tsx",
|
||||
resolveDir: resolve(root, "govoplan-core/webui"),
|
||||
},
|
||||
bundle: true,
|
||||
write: false,
|
||||
platform: "node",
|
||||
format: "cjs",
|
||||
jsx: "automatic",
|
||||
external: ["react", "react-dom/server"],
|
||||
logLevel: "silent",
|
||||
});
|
||||
const compiled = { exports: {} };
|
||||
new Function("require", "module", "exports", outputFiles[0].text)(require, compiled, compiled.exports);
|
||||
const { catalogs, heading } = compiled.exports;
|
||||
|
||||
test("new contextual labels render from their owning EN/DE catalogues", async (context) => {
|
||||
for (const [index, name] of names.entries()) {
|
||||
await context.test(name, () => {
|
||||
const moduleSource = readFileSync(resolve(root, `govoplan-${name}/webui/src/module.ts`), "utf8");
|
||||
assert.match(moduleSource, /import\s*\{\s*generatedTranslations\s*\}\s*from\s*["']\.\/i18n\/generatedTranslations["']/);
|
||||
assert.match(moduleSource, /\btranslations(?:\s*:\s*generatedTranslations)?\s*,/);
|
||||
for (const [english, german] of Object.entries(labels[name])) {
|
||||
assert.equal(catalogs[index].en[english], english);
|
||||
assert.equal(catalogs[index].de[english], german);
|
||||
for (const [language, expected] of [["en", english], ["de", german]]) {
|
||||
const markup = heading(index, language, english);
|
||||
assert(markup.includes(`<h1>${expected}</h1>`), `${name}: ${language} contextual heading`);
|
||||
assert(markup.includes(language === "de" ? "Benutzerdokumentation öffnen" : "Open user documentation"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { findTypeOnlyJsxImports } from "../tools/checks/check-jsx-value-imports.mjs";
|
||||
|
||||
const findings = (source) => findTypeOnlyJsxImports([{ path: "/fixture.tsx", source }]).map((item) => item.component);
|
||||
assert.deepEqual(findings('import type { FormGrid, AuthInfo } from "@govoplan/core-webui"; const page = <Dialog><FormGrid /></Dialog>;'), ["FormGrid"]);
|
||||
assert.deepEqual(findings('import { type FormGrid as Layout } from "ui"; const page = <Layout>Content</Layout>;'), ["Layout"]);
|
||||
assert.deepEqual(findings('import type Layout from "ui"; const page = <Layout />;'), ["Layout"]);
|
||||
assert.deepEqual(findings('import type * as ui from "ui"; const page = <ui.Layout />;'), ["ui.Layout"]);
|
||||
assert.deepEqual(findings('import type { FormGrid } from "ui"; const page = <div title={<FormGrid />} />;'), ["FormGrid"]);
|
||||
assert.deepEqual(findings('import { FormGrid, type AuthInfo } from "ui"; const page = <FormGrid />;'), []);
|
||||
assert.deepEqual(findings('import type { FormGrid } from "ui"; function Page({ FormGrid }: Props) { return <FormGrid />; }'), []);
|
||||
assert.deepEqual(findings('import type * as ui from "ui"; function Page(ui: RuntimeControls) { return <ui.Layout />; }'), []);
|
||||
assert.deepEqual(findings('import type { Layout } from "ui"; const page: Layout = {};'), []);
|
||||
assert.deepEqual(findings('import type { input } from "ui"; const page = <input />;'), []);
|
||||
console.log("JSX runtime-import AST regression tests passed (10 cases).");
|
||||
@@ -0,0 +1,124 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
ASSESSMENT_TOOLS_ROOT = META_ROOT / "tools" / "assessments"
|
||||
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
|
||||
for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
|
||||
if str(tools_root) not in sys.path:
|
||||
sys.path.insert(0, str(tools_root))
|
||||
|
||||
from govoplan_assessment.report_generator import ( # noqa: E402
|
||||
AssessmentGenerationError,
|
||||
load_bounded_json,
|
||||
render_assessment_markdown,
|
||||
validate_report_input,
|
||||
)
|
||||
|
||||
|
||||
class CapabilityFitGenerationTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.assessment = json.loads(
|
||||
(META_ROOT / "docs" / "capability-fit-current.json").read_text("utf-8")
|
||||
)
|
||||
cls.schema = json.loads(
|
||||
(META_ROOT / "docs" / "capability-fit.schema.json").read_text("utf-8")
|
||||
)
|
||||
|
||||
def test_current_assessment_renders_every_conclusion_from_one_input(self) -> None:
|
||||
validate_report_input(assessment=self.assessment, schema=self.schema)
|
||||
|
||||
first = render_assessment_markdown(self.assessment)
|
||||
second = render_assessment_markdown(deepcopy(self.assessment))
|
||||
|
||||
self.assertEqual(first, second)
|
||||
self.assertIn("## Facts", first)
|
||||
self.assertIn("## Decisions", first)
|
||||
self.assertIn("## Unresolved decisions", first)
|
||||
self.assertIn("## Risks and residual risks", first)
|
||||
self.assertIn("## Proof-of-concept and promotion checks", first)
|
||||
self.assertIn("### Controlled Campaign pilot", first)
|
||||
self.assertIn("### Small-production candidate", first)
|
||||
self.assertIn("## Functional matrix context", first)
|
||||
self.assertIn("### Manual workarounds", first)
|
||||
self.assertIn("### Blockers", first)
|
||||
self.assertIn("Workflow and workflow-driven user stories", first)
|
||||
for status in self.schema["$defs"]["status"]["enum"]:
|
||||
self.assertIn(f"`{status}`", first)
|
||||
for collection in ("capabilities", "infrastructure", "data_flows"):
|
||||
for item in self.assessment[collection]:
|
||||
self.assertIn(item["id"], first)
|
||||
infrastructure_ids = {
|
||||
item["id"] for item in self.assessment["infrastructure"]
|
||||
}
|
||||
self.assertTrue(
|
||||
{
|
||||
"runtime.web_api",
|
||||
"runtime.worker",
|
||||
"runtime.scheduler",
|
||||
"data.postgresql",
|
||||
"queue.redis",
|
||||
"storage.local",
|
||||
"storage.object",
|
||||
"edge.proxy_tls",
|
||||
"identity.access",
|
||||
"security.secret_store",
|
||||
"connectors.mail",
|
||||
"operations.monitoring",
|
||||
"operations.audit",
|
||||
"operations.backup_restore",
|
||||
"operations.disaster_recovery",
|
||||
}.issubset(infrastructure_ids)
|
||||
)
|
||||
|
||||
def test_questionnaire_retains_all_required_fit_dimensions(self) -> None:
|
||||
ids = {
|
||||
item["id"]
|
||||
for answers in self.assessment["questionnaire"].values()
|
||||
for item in answers
|
||||
}
|
||||
self.assertTrue(
|
||||
{
|
||||
"outcome.reference_journey",
|
||||
"scope.users_tenants_organizations",
|
||||
"data.classification",
|
||||
"data.retention",
|
||||
"data.privacy_security_disclosure",
|
||||
"identity.protocols_lifecycle",
|
||||
"integration.protocols_network",
|
||||
"workload.tenants_users_concurrency",
|
||||
"workload.campaign_volume_peaks",
|
||||
"workload.files_jobs_audit_growth_retention",
|
||||
"workload.connector_traffic_batches",
|
||||
"availability.rto_rpo",
|
||||
"hosting.network_constraints",
|
||||
"operations.ownership",
|
||||
"procurement.constraints",
|
||||
}.issubset(ids)
|
||||
)
|
||||
|
||||
def test_duplicate_keys_and_sensitive_fields_fail_closed(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
duplicate = Path(directory) / "duplicate.json"
|
||||
duplicate.write_text('{"id": 1, "id": 2}', encoding="utf-8")
|
||||
with self.assertRaisesRegex(AssessmentGenerationError, "Duplicate JSON key"):
|
||||
load_bounded_json(duplicate, label="assessment")
|
||||
|
||||
unsafe = deepcopy(self.assessment)
|
||||
unsafe["password"] = "must-not-render"
|
||||
permissive = deepcopy(self.schema)
|
||||
permissive["additionalProperties"] = True
|
||||
with self.assertRaisesRegex(AssessmentGenerationError, "sensitive field"):
|
||||
validate_report_input(assessment=unsafe, schema=permissive)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -5,9 +5,17 @@ import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.core.configuration_packages import (
|
||||
ConfigurationApplyResult,
|
||||
ConfigurationExportResult,
|
||||
ConfigurationPackageManifest,
|
||||
ConfigurationPlanItem,
|
||||
ConfigurationPreflightContext,
|
||||
ConfigurationPreflightResult,
|
||||
ConfigurationProviderDescription,
|
||||
configuration_package_claim_issues,
|
||||
dry_run_configuration_package,
|
||||
)
|
||||
@@ -54,10 +62,19 @@ class ConfigurationPackageArtifactTests(unittest.TestCase):
|
||||
f"Missing repository for {requirement.module_id}",
|
||||
)
|
||||
|
||||
provider_module_ids = tuple(
|
||||
sorted({fragment.module_id for fragment in manifest.fragments})
|
||||
)
|
||||
providers = tuple(_ArtifactProvider(module_id) for module_id in provider_module_ids)
|
||||
supplied_data = {
|
||||
str(item["key"]): _sample_value(item)
|
||||
for item in manifest.data_requirements
|
||||
}
|
||||
result = dry_run_configuration_package(
|
||||
manifest,
|
||||
(),
|
||||
providers,
|
||||
ConfigurationPreflightContext(
|
||||
supplied_data=supplied_data,
|
||||
installed_modules={
|
||||
item.module_id: item.version or "workspace"
|
||||
for item in manifest.required_modules
|
||||
@@ -74,6 +91,155 @@ class ConfigurationPackageArtifactTests(unittest.TestCase):
|
||||
self.assertIn("product.governed-data-assurance", package_ids)
|
||||
self.assertIn("product.service-to-decision", package_ids)
|
||||
|
||||
def test_service_to_decision_package_imports_its_form_as_an_idempotent_local_draft(self) -> None:
|
||||
from govoplan_forms.backend.configuration_provider import (
|
||||
_apply_definition,
|
||||
_preflight_definition,
|
||||
)
|
||||
from govoplan_forms.backend.db.models import FormDefinitionRevision
|
||||
from govoplan_forms.backend.service import get_form_definition
|
||||
|
||||
package = ConfigurationPackageManifest.from_mapping(json.loads(
|
||||
(PACKAGE_ROOT / "product/service-to-decision/package.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
))
|
||||
fragment = next(
|
||||
item
|
||||
for item in package.fragments
|
||||
if item.module_id == "forms" and item.fragment_type == "definition"
|
||||
)
|
||||
context = ConfigurationPreflightContext(
|
||||
tenant_id="tenant-reference-test",
|
||||
operator_user_id="operator-1",
|
||||
operator_scopes=frozenset({"system:governance:write"}),
|
||||
)
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
FormDefinitionRevision.__table__.create(engine)
|
||||
session = Session(engine)
|
||||
try:
|
||||
preflight = _preflight_definition(session, fragment, context)
|
||||
applied = _apply_definition(session, fragment, context)
|
||||
session.commit()
|
||||
replay = _apply_definition(session, fragment, context)
|
||||
imported = get_form_definition(
|
||||
session,
|
||||
type("Principal", (), {"tenant_id": "tenant-reference-test"})(),
|
||||
form_id="resident-parking-permit-application",
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
self.assertFalse(any(item.severity == "blocker" for item in preflight.diagnostics))
|
||||
self.assertEqual("create", preflight.plan[0].action)
|
||||
self.assertEqual(1, len(applied.created_refs))
|
||||
self.assertEqual({}, replay.created_refs)
|
||||
self.assertIsNotNone(imported)
|
||||
assert imported is not None
|
||||
self.assertEqual("tenant-reference-test", imported.reference.tenant_id)
|
||||
self.assertEqual("draft", imported.publication_state)
|
||||
self.assertEqual("de", imported.fallback_locale)
|
||||
|
||||
def test_service_to_decision_package_materializes_its_tenant_workflow_idempotently(self) -> None:
|
||||
from govoplan_core.core.configuration_packages import _resolve_fragment_data_references
|
||||
from govoplan_workflow_engine.backend.configuration_provider import (
|
||||
apply_workflow_definitions,
|
||||
preflight_workflow_definitions,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.db.models import (
|
||||
WorkflowDefinition,
|
||||
WorkflowDefinitionRevision,
|
||||
)
|
||||
|
||||
package = ConfigurationPackageManifest.from_mapping(json.loads(
|
||||
(PACKAGE_ROOT / "product/service-to-decision/package.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
))
|
||||
fragment = next(
|
||||
item
|
||||
for item in package.fragments
|
||||
if item.module_id == "workflow_engine"
|
||||
)
|
||||
context = ConfigurationPreflightContext(
|
||||
tenant_id="tenant-reference-test",
|
||||
supplied_data={
|
||||
"responsible_group_slug": "traffic-permits",
|
||||
"responsible_group_name": "Traffic permits",
|
||||
},
|
||||
)
|
||||
resolved = _resolve_fragment_data_references(
|
||||
fragment,
|
||||
context.supplied_data,
|
||||
)
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
WorkflowDefinition.__table__.create(engine)
|
||||
WorkflowDefinitionRevision.__table__.create(engine)
|
||||
session = Session(engine)
|
||||
try:
|
||||
preflight = preflight_workflow_definitions(session, resolved, context)
|
||||
applied = apply_workflow_definitions(
|
||||
session,
|
||||
resolved,
|
||||
context,
|
||||
registry=None,
|
||||
)
|
||||
replay = apply_workflow_definitions(
|
||||
session,
|
||||
resolved,
|
||||
context,
|
||||
registry=None,
|
||||
)
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
self.assertFalse(any(item.severity == "blocker" for item in preflight.diagnostics))
|
||||
self.assertEqual("create", preflight.plan[0].action)
|
||||
self.assertIn("resident-parking-permit-review", applied.created_refs)
|
||||
self.assertEqual({}, replay.created_refs)
|
||||
self.assertEqual({}, replay.updated_refs)
|
||||
|
||||
|
||||
class _ArtifactProvider:
|
||||
def __init__(self, module_id: str) -> None:
|
||||
self.module_id = module_id
|
||||
|
||||
def describe(self) -> ConfigurationProviderDescription:
|
||||
return ConfigurationProviderDescription(module_id=self.module_id)
|
||||
|
||||
def preflight(self, fragment, context) -> ConfigurationPreflightResult:
|
||||
del context
|
||||
return ConfigurationPreflightResult(plan=(ConfigurationPlanItem(
|
||||
action="create",
|
||||
module_id=fragment.module_id,
|
||||
fragment_type=fragment.fragment_type,
|
||||
fragment_id=fragment.fragment_id,
|
||||
),))
|
||||
|
||||
def apply(self, fragment, supplied_data, context) -> ConfigurationApplyResult:
|
||||
del fragment, supplied_data, context
|
||||
return ConfigurationApplyResult()
|
||||
|
||||
def export(self, selection, context) -> ConfigurationExportResult:
|
||||
del selection, context
|
||||
return ConfigurationExportResult()
|
||||
|
||||
def health(self, import_result, context):
|
||||
del import_result, context
|
||||
return ()
|
||||
|
||||
|
||||
def _sample_value(requirement: dict[str, object]) -> object:
|
||||
data_type = str(requirement.get("data_type") or requirement.get("type") or "string")
|
||||
if data_type == "boolean":
|
||||
return False
|
||||
if data_type in {"integer", "number"}:
|
||||
return 1
|
||||
return f"fixture-{requirement['key']}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import subprocess
|
||||
@@ -33,6 +35,11 @@ from govoplan_deploy.bundle import ( # noqa: E402
|
||||
)
|
||||
from govoplan_deploy.cli import _receipt_uses_direct_web_port, main # noqa: E402
|
||||
import govoplan_deploy.cli as deployment_cli # noqa: E402
|
||||
from govoplan_deploy.capabilities import ( # noqa: E402
|
||||
capability_change_impacts,
|
||||
infrastructure_capability_document,
|
||||
infrastructure_dependency_inventory_from_mapping,
|
||||
)
|
||||
from govoplan_deploy.cluster_evidence import ( # noqa: E402
|
||||
collect_kubernetes_evidence,
|
||||
)
|
||||
@@ -83,6 +90,41 @@ def _kubernetes_test_deployment(component: str, replicas: int) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _dependency_inventory(
|
||||
installation_id: str,
|
||||
*,
|
||||
generated_at: datetime | None = None,
|
||||
) -> dict:
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"installation_id": installation_id,
|
||||
"generated_at": (generated_at or datetime.now(UTC)).isoformat(),
|
||||
"complete": True,
|
||||
"inspected_capability_ids": ["coordination.redis", "mail.smtp"],
|
||||
"providers": [
|
||||
{
|
||||
"module_id": "mail",
|
||||
"state": "complete",
|
||||
"capability_ids": ["mail.smtp"],
|
||||
"dependency_count": 1,
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"capability_id": "mail.smtp",
|
||||
"module_id": "mail",
|
||||
"dependency_type": "smtp_endpoint",
|
||||
"dependency_ref": "endpoint:17",
|
||||
"state": "active",
|
||||
"scope": "system",
|
||||
"summary": "Persisted SMTP endpoint has one credential binding.",
|
||||
"metrics": {"credential_binding_count": 1},
|
||||
"required_action": "Rebind or migrate this SMTP endpoint.",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class DeploymentInstallerTests(unittest.TestCase):
|
||||
def test_kubernetes_evidence_requires_two_node_spread_and_safe_runtime(
|
||||
self,
|
||||
@@ -158,6 +200,81 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
evidence["snapshot"]["ready_node_names"],
|
||||
)
|
||||
|
||||
def test_kubernetes_api_loss_uses_a_non_json_mutation_command(self) -> None:
|
||||
initial_pods = [
|
||||
_kubernetes_test_pod("api-a", "api", "node-a"),
|
||||
_kubernetes_test_pod("api-b", "api", "node-b"),
|
||||
_kubernetes_test_pod("web-a", "web", "node-a"),
|
||||
_kubernetes_test_pod("web-b", "web", "node-b"),
|
||||
]
|
||||
replacement_pods = [
|
||||
_kubernetes_test_pod("api-b", "api", "node-b"),
|
||||
_kubernetes_test_pod("api-c", "api", "node-a"),
|
||||
]
|
||||
deleted = False
|
||||
actions: list[tuple[str, ...]] = []
|
||||
|
||||
def run(arguments):
|
||||
if "nodes" in arguments:
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"metadata": {"name": name},
|
||||
"spec": {},
|
||||
"status": {
|
||||
"conditions": [
|
||||
{"type": "Ready", "status": "True"}
|
||||
]
|
||||
},
|
||||
}
|
||||
for name in ("node-a", "node-b")
|
||||
]
|
||||
}
|
||||
if "deployments" in arguments:
|
||||
return {
|
||||
"items": [
|
||||
_kubernetes_test_deployment("api", 2),
|
||||
_kubernetes_test_deployment("web", 2),
|
||||
]
|
||||
}
|
||||
return {"items": replacement_pods if deleted else initial_pods}
|
||||
|
||||
def act(arguments):
|
||||
nonlocal deleted
|
||||
actions.append(tuple(arguments))
|
||||
deleted = True
|
||||
|
||||
evidence = collect_kubernetes_evidence(
|
||||
installation_id="govoplan-cluster",
|
||||
namespace="govoplan",
|
||||
ops_url="https://govoplan.example.test/api/v1/ops/status",
|
||||
api_key="not-retained",
|
||||
exercise_api_pod_loss=True,
|
||||
command_runner=run,
|
||||
action_runner=act,
|
||||
json_fetcher=lambda _url, _key: {
|
||||
"readiness": {"ready": True},
|
||||
"runtime_cluster": {
|
||||
"composition": {"skewed": False},
|
||||
"software_versions": {"skewed": False},
|
||||
"queues": {"missing": []},
|
||||
},
|
||||
"checks": [
|
||||
{
|
||||
"id": "database_capacity",
|
||||
"state": "ok",
|
||||
"detail": "Within budget",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual("passed", evidence["api_pod_loss"]["state"])
|
||||
self.assertEqual(1, len(actions))
|
||||
self.assertIn("delete", actions[0])
|
||||
self.assertNotIn("-o", actions[0])
|
||||
self.assertNotIn("not-retained", json.dumps(evidence))
|
||||
|
||||
def test_pre_migration_failure_restores_checksum_verified_applied_bundle(
|
||||
self,
|
||||
) -> None:
|
||||
@@ -339,6 +456,27 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
self.assertNotIn("db-secret", rendered)
|
||||
self.assertNotIn("redis-secret", rendered)
|
||||
self.assertNotIn("object-secret", rendered)
|
||||
capability_config = next(
|
||||
item
|
||||
for item in manifest["items"]
|
||||
if item["kind"] == "ConfigMap"
|
||||
and item["metadata"]["name"].endswith("infrastructure-capabilities")
|
||||
)
|
||||
capability_payload = json.loads(
|
||||
capability_config["data"]["infrastructure-capabilities.json"]
|
||||
)
|
||||
self.assertEqual(1, capability_payload["schema_version"])
|
||||
self.assertNotIn("db-secret", json.dumps(capability_payload))
|
||||
api_container = deployments["govoplan-cluster-api"]["spec"]["template"]["spec"]["containers"][0]
|
||||
self.assertIn(
|
||||
{
|
||||
"name": "deployment-capabilities",
|
||||
"mountPath": "/etc/govoplan/deployment/infrastructure-capabilities.json",
|
||||
"subPath": "infrastructure-capabilities.json",
|
||||
"readOnly": True,
|
||||
},
|
||||
api_container["volumeMounts"],
|
||||
)
|
||||
self.assertNotIn("PersistentVolumeClaim", kinds)
|
||||
self.assertNotIn("StatefulSet", kinds)
|
||||
self.assertEqual(3, deployments["govoplan-cluster-api"]["spec"]["replicas"])
|
||||
@@ -347,6 +485,10 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
"template"
|
||||
]["spec"]["containers"][0]["command"]
|
||||
self.assertIn("govoplan_core.commands.fenced_run", scheduler_command)
|
||||
self.assertEqual(
|
||||
["--schedule", "/tmp/celerybeat-schedule"],
|
||||
scheduler_command[-2:],
|
||||
)
|
||||
api_init_command = deployments["govoplan-cluster-api"]["spec"]["template"][
|
||||
"spec"
|
||||
]["initContainers"][0]["command"]
|
||||
@@ -381,10 +523,32 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
"containers"
|
||||
][0]["readinessProbe"]["httpGet"]["httpHeaders"],
|
||||
)
|
||||
api_pod_spec = deployments["govoplan-cluster-api"]["spec"]["template"][
|
||||
"spec"
|
||||
]
|
||||
self.assertEqual(30, api_pod_spec["terminationGracePeriodSeconds"])
|
||||
self.assertEqual(
|
||||
["/bin/sh", "-c", "sleep 10"],
|
||||
api_pod_spec["containers"][0]["lifecycle"]["preStop"]["exec"][
|
||||
"command"
|
||||
],
|
||||
)
|
||||
self.assertNotIn(
|
||||
"lifecycle",
|
||||
deployments["govoplan-cluster-worker"]["spec"]["template"]["spec"][
|
||||
"containers"
|
||||
][0],
|
||||
)
|
||||
worker_command = deployments["govoplan-cluster-worker"]["spec"]["template"][
|
||||
"spec"
|
||||
]["containers"][0]["command"]
|
||||
self.assertIn("--concurrency", worker_command)
|
||||
for deployment in deployments.values():
|
||||
spread = deployment["spec"]["template"]["spec"][
|
||||
"topologySpreadConstraints"
|
||||
][0]
|
||||
self.assertEqual("DoNotSchedule", spread["whenUnsatisfiable"])
|
||||
self.assertEqual(["pod-template-hash"], spread["matchLabelKeys"])
|
||||
self.assertEqual(
|
||||
"62",
|
||||
manifest["metadata"]["annotations"][
|
||||
@@ -392,6 +556,99 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_kubernetes_export_mounts_an_optional_s3_ca_on_backend_roles(
|
||||
self,
|
||||
) -> None:
|
||||
spec = default_spec(
|
||||
installation_id="govoplan-cluster",
|
||||
postgres_mode="external",
|
||||
redis_mode="external",
|
||||
storage_mode="s3",
|
||||
api_replicas=2,
|
||||
web_replicas=2,
|
||||
worker_replicas=2,
|
||||
api_image="registry.example.test/govoplan-api@sha256:" + "a" * 64,
|
||||
web_image="registry.example.test/govoplan-web@sha256:" + "b" * 64,
|
||||
)
|
||||
environment = initial_secrets(
|
||||
spec,
|
||||
supplied={
|
||||
"DATABASE_URL": "postgresql+psycopg://user:secret@postgres.example.test/govoplan",
|
||||
"GOVOPLAN_DATABASE_URL_PGTOOLS": "postgresql://user:secret@postgres.example.test/govoplan",
|
||||
"REDIS_URL": "rediss://:secret@redis.example.test/0",
|
||||
"FILE_STORAGE_S3_ENDPOINT_URL": "https://s3.example.test",
|
||||
"FILE_STORAGE_S3_REGION": "eu-test-1",
|
||||
"FILE_STORAGE_S3_ACCESS_KEY_ID": "object-key",
|
||||
"FILE_STORAGE_S3_SECRET_ACCESS_KEY": "object-secret",
|
||||
"FILE_STORAGE_S3_BUCKET": "govoplan",
|
||||
"GOVOPLAN_DB_CONNECTION_LIMIT": "100",
|
||||
},
|
||||
)
|
||||
|
||||
manifest = render_kubernetes(
|
||||
spec,
|
||||
environment,
|
||||
s3_ca_secret_name="govoplan-s3-ca",
|
||||
backup_required=False,
|
||||
)
|
||||
backend_pods = [
|
||||
item["spec"]["template"]["spec"]
|
||||
for item in manifest["items"]
|
||||
if item["kind"] in {"Deployment", "Job"}
|
||||
and item["metadata"]["labels"].get("app.kubernetes.io/component")
|
||||
in {"api", "worker", "scheduler", "migration"}
|
||||
]
|
||||
web = next(
|
||||
item
|
||||
for item in manifest["items"]
|
||||
if item["kind"] == "Deployment"
|
||||
and item["metadata"]["labels"].get("app.kubernetes.io/component")
|
||||
== "web"
|
||||
)
|
||||
|
||||
self.assertTrue(backend_pods)
|
||||
for pod in backend_pods:
|
||||
self.assertIn(
|
||||
{
|
||||
"name": "s3-ca",
|
||||
"secret": {
|
||||
"secretName": "govoplan-s3-ca",
|
||||
"items": [{"key": "ca.crt", "path": "s3-ca.crt"}],
|
||||
},
|
||||
},
|
||||
pod["volumes"],
|
||||
)
|
||||
for container in [*pod.get("initContainers", []), *pod["containers"]]:
|
||||
self.assertIn(
|
||||
{
|
||||
"name": "AWS_CA_BUNDLE",
|
||||
"value": "/etc/govoplan/trust/s3-ca.crt",
|
||||
},
|
||||
container["env"],
|
||||
)
|
||||
self.assertIn(
|
||||
{
|
||||
"name": "s3-ca",
|
||||
"mountPath": "/etc/govoplan/trust",
|
||||
"readOnly": True,
|
||||
},
|
||||
container["volumeMounts"],
|
||||
)
|
||||
self.assertNotIn(
|
||||
"s3-ca",
|
||||
{
|
||||
volume["name"]
|
||||
for volume in web["spec"]["template"]["spec"]["volumes"]
|
||||
},
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "S3 CA secret"):
|
||||
render_kubernetes(
|
||||
spec,
|
||||
environment,
|
||||
s3_ca_secret_name="INVALID_NAME",
|
||||
backup_required=False,
|
||||
)
|
||||
|
||||
def test_kubernetes_export_splits_worker_queues_and_rejects_capacity_overrun(
|
||||
self,
|
||||
) -> None:
|
||||
@@ -523,12 +780,21 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
self.assertIn("redis", compose["services"])
|
||||
self.assertIn("worker", compose["services"])
|
||||
self.assertIn("load-balancer", compose["services"])
|
||||
self.assertEqual(
|
||||
"haproxy:3.2.23-alpine@sha256:"
|
||||
"6343ce34a132a5dceaa24767d739df2bd519f8f7c1079ae39e4821334e8eb42e",
|
||||
compose["services"]["load-balancer"]["image"],
|
||||
)
|
||||
self.assertNotIn("test-mail", compose["services"])
|
||||
self.assertEqual(
|
||||
["127.0.0.1:8080:8080"],
|
||||
compose["services"]["load-balancer"]["ports"],
|
||||
)
|
||||
self.assertNotIn("ports", compose["services"]["web"])
|
||||
self.assertIn(
|
||||
"./infrastructure-capabilities.json:/etc/govoplan/deployment/infrastructure-capabilities.json:ro",
|
||||
compose["services"]["api"]["volumes"],
|
||||
)
|
||||
self.assertEqual(1, compose["services"]["api"]["scale"])
|
||||
self.assertEqual(1, compose["services"]["web"]["scale"])
|
||||
|
||||
@@ -754,6 +1020,85 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
reconciled["GARAGE_RPC_SECRET"],
|
||||
)
|
||||
|
||||
def test_infrastructure_capability_document_exposes_refs_not_secrets(self) -> None:
|
||||
spec = default_spec(
|
||||
installation_id="govoplan-shared",
|
||||
postgres_mode="external",
|
||||
redis_mode="external",
|
||||
storage_mode="s3",
|
||||
mail_mode="external-relay",
|
||||
module_set="full",
|
||||
)
|
||||
values = initial_secrets(
|
||||
spec,
|
||||
supplied={
|
||||
"DATABASE_URL": "postgresql+psycopg://user:database-secret@db.example.test/govoplan",
|
||||
"REDIS_URL": "rediss://:redis-secret@redis.example.test/0",
|
||||
"FILE_STORAGE_S3_ENDPOINT_URL": "https://s3.example.test",
|
||||
"FILE_STORAGE_S3_REGION": "eu-test-1",
|
||||
"FILE_STORAGE_S3_ACCESS_KEY_ID": "object-key",
|
||||
"FILE_STORAGE_S3_SECRET_ACCESS_KEY": "object-secret",
|
||||
"FILE_STORAGE_S3_BUCKET": "govoplan",
|
||||
},
|
||||
)
|
||||
|
||||
document = infrastructure_capability_document(spec, values)
|
||||
rendered = json.dumps(document, sort_keys=True)
|
||||
capabilities = {item["id"]: item for item in document["capabilities"]}
|
||||
|
||||
self.assertNotIn("database-secret", rendered)
|
||||
self.assertNotIn("redis-secret", rendered)
|
||||
self.assertNotIn("object-secret", rendered)
|
||||
self.assertNotIn("object-key", rendered)
|
||||
self.assertEqual("externally_supplied", capabilities["database.postgresql"]["state"])
|
||||
self.assertEqual("db.example.test", capabilities["database.postgresql"]["endpoint"]["host"])
|
||||
self.assertEqual(["env:DATABASE_URL"], capabilities["database.postgresql"]["secret_refs"])
|
||||
self.assertEqual("available_unconfigured", capabilities["mail.smtp"]["state"])
|
||||
self.assertEqual("mail.smtp-profile", document["post_install_tasks"][0]["id"])
|
||||
|
||||
def test_capability_impact_detects_external_endpoint_rebinding(self) -> None:
|
||||
spec = default_spec(postgres_mode="external", module_set="full")
|
||||
previous = infrastructure_capability_document(
|
||||
spec,
|
||||
{"DATABASE_URL": "postgresql://user:old-secret@old-db.example.test/govoplan"},
|
||||
)
|
||||
desired = infrastructure_capability_document(
|
||||
spec,
|
||||
{"DATABASE_URL": "postgresql://user:new-secret@new-db.example.test/govoplan"},
|
||||
)
|
||||
|
||||
impacts = {
|
||||
item.capability_id: item
|
||||
for item in capability_change_impacts(previous, desired)
|
||||
}
|
||||
|
||||
self.assertEqual("reconfigure", impacts["database.postgresql"].action)
|
||||
self.assertIn("changed endpoint binding", impacts["database.postgresql"].detail)
|
||||
self.assertNotIn("old-secret", impacts["database.postgresql"].detail)
|
||||
self.assertNotIn("new-secret", impacts["database.postgresql"].detail)
|
||||
|
||||
def test_capability_impact_includes_provider_dependency_evidence(self) -> None:
|
||||
previous_spec = default_spec(mail_mode="test-mail", module_set="full")
|
||||
desired_spec = default_spec(mail_mode="disabled", module_set="full")
|
||||
inventory = infrastructure_dependency_inventory_from_mapping(
|
||||
_dependency_inventory(previous_spec.installation_id)
|
||||
)
|
||||
|
||||
impacts = {
|
||||
item.capability_id: item
|
||||
for item in capability_change_impacts(
|
||||
infrastructure_capability_document(previous_spec, {}),
|
||||
infrastructure_capability_document(desired_spec, {}),
|
||||
dependency_inventory=inventory,
|
||||
)
|
||||
}
|
||||
|
||||
mail = impacts["mail.smtp"]
|
||||
self.assertTrue(mail.inventory_inspected)
|
||||
self.assertEqual("endpoint:17", mail.actual_dependencies[0].dependency_ref)
|
||||
self.assertIn("mail:endpoint:17", mail.detail)
|
||||
self.assertIn("Rebind or migrate", mail.required_action)
|
||||
|
||||
def test_replica_counts_drive_compose_and_load_balancer_discovery(self) -> None:
|
||||
spec = default_spec(
|
||||
storage_mode="garage",
|
||||
@@ -805,8 +1150,26 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
self.assertEqual(1, parsed.replicas.web)
|
||||
self.assertEqual(1, parsed.replicas.worker)
|
||||
self.assertEqual("managed", parsed.components.load_balancer.mode)
|
||||
self.assertEqual(
|
||||
default_spec().components.load_balancer.image,
|
||||
parsed.components.load_balancer.image,
|
||||
)
|
||||
self.assertEqual("local", parsed.ingress.mode)
|
||||
|
||||
def test_load_balancer_patch_does_not_rewrite_an_existing_image(self) -> None:
|
||||
for image in (
|
||||
"haproxy:3.2.21-alpine",
|
||||
"registry.example.test/haproxy@sha256:" + "a" * 64,
|
||||
):
|
||||
with self.subTest(image=image):
|
||||
saved = default_spec(load_balancer_image=image).to_dict()
|
||||
|
||||
restored = parse_spec(json.loads(json.dumps(saved)))
|
||||
compose = render_compose(restored)
|
||||
|
||||
self.assertEqual(image, restored.components.load_balancer.image)
|
||||
self.assertEqual(image, compose["services"]["load-balancer"]["image"])
|
||||
|
||||
def test_compose_contains_no_secret_values(self) -> None:
|
||||
spec = default_spec()
|
||||
values = initial_secrets(spec)
|
||||
@@ -883,10 +1246,14 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
|
||||
paths = bundle_paths(Path(directory))
|
||||
paths.root.chmod(0o700)
|
||||
first_spec = default_spec(mail_mode="test-mail")
|
||||
first_spec = default_spec(mail_mode="test-mail", module_set="full")
|
||||
first_environment = initial_secrets(first_spec)
|
||||
write_env(paths.env, first_environment)
|
||||
first_plan = build_plan(first_spec, paths, include_host_checks=False)
|
||||
first_capabilities = infrastructure_capability_document(
|
||||
first_spec,
|
||||
first_environment,
|
||||
)
|
||||
atomic_write(
|
||||
paths.receipt,
|
||||
canonical_json(
|
||||
@@ -897,12 +1264,17 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
first_plan.desired_environment_fingerprint
|
||||
),
|
||||
"services": list(render_compose(first_spec)["services"]),
|
||||
"infrastructure_capabilities": first_capabilities,
|
||||
}
|
||||
),
|
||||
mode=0o600,
|
||||
)
|
||||
|
||||
second_spec = default_spec(redis_mode="disabled", mail_mode="disabled")
|
||||
second_spec = default_spec(
|
||||
redis_mode="disabled",
|
||||
mail_mode="disabled",
|
||||
module_set="full",
|
||||
)
|
||||
write_env(
|
||||
paths.env,
|
||||
reconcile_runtime_environment(second_spec, first_environment),
|
||||
@@ -918,6 +1290,79 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
{"redis", "worker", "scheduler", "test-mail"},
|
||||
removed,
|
||||
)
|
||||
impacts = {
|
||||
item.capability_id: item
|
||||
for item in second_plan.capability_impacts
|
||||
}
|
||||
self.assertEqual("remove", impacts["coordination.redis"].action)
|
||||
self.assertEqual("remove", impacts["mail.smtp"].action)
|
||||
self.assertIn("mail", impacts["mail.smtp"].dependent_modules)
|
||||
self.assertTrue(
|
||||
any(
|
||||
check.id == "capability.change.mail.smtp"
|
||||
and check.level == "warning"
|
||||
for check in second_plan.checks
|
||||
)
|
||||
)
|
||||
self.assertTrue(second_plan.blocked)
|
||||
self.assertTrue(
|
||||
any(
|
||||
check.id == "capability.dependency_inventory.missing"
|
||||
and check.level == "error"
|
||||
for check in second_plan.checks
|
||||
)
|
||||
)
|
||||
|
||||
atomic_write(
|
||||
paths.dependency_inventory,
|
||||
canonical_json(_dependency_inventory(second_spec.installation_id)),
|
||||
mode=0o600,
|
||||
)
|
||||
evidenced_plan = build_plan(
|
||||
second_spec,
|
||||
paths,
|
||||
include_host_checks=False,
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
any(
|
||||
check.level == "error"
|
||||
and check.id.startswith("capability.dependency_inventory.")
|
||||
for check in evidenced_plan.checks
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
"endpoint:17",
|
||||
{
|
||||
item.capability_id: item
|
||||
for item in evidenced_plan.capability_impacts
|
||||
}["mail.smtp"].actual_dependencies[0].dependency_ref,
|
||||
)
|
||||
self.assertTrue(
|
||||
any(
|
||||
check.id == "capability.dependency_inventory.current"
|
||||
and check.level == "ok"
|
||||
for check in evidenced_plan.checks
|
||||
)
|
||||
)
|
||||
|
||||
stale = _dependency_inventory(
|
||||
second_spec.installation_id,
|
||||
generated_at=datetime.now(UTC) - timedelta(minutes=6),
|
||||
)
|
||||
atomic_write(
|
||||
paths.dependency_inventory,
|
||||
canonical_json(stale),
|
||||
mode=0o600,
|
||||
)
|
||||
stale_plan = build_plan(second_spec, paths, include_host_checks=False)
|
||||
self.assertTrue(stale_plan.blocked)
|
||||
self.assertTrue(
|
||||
any(
|
||||
check.id == "capability.dependency_inventory.stale"
|
||||
for check in stale_plan.checks
|
||||
)
|
||||
)
|
||||
|
||||
def test_secret_change_is_planned_without_exposing_secret_values(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
|
||||
@@ -1027,6 +1472,54 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
)[0],
|
||||
)
|
||||
|
||||
def test_cli_collects_bounded_private_dependency_inventory(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
|
||||
root = Path(directory) / "installation"
|
||||
self.assertEqual(
|
||||
0,
|
||||
run_cli(
|
||||
[
|
||||
"init",
|
||||
"--non-interactive",
|
||||
"--directory",
|
||||
str(root),
|
||||
]
|
||||
)[0],
|
||||
)
|
||||
payload = _dependency_inventory("govoplan-local")
|
||||
response = MagicMock()
|
||||
response.__enter__.return_value = response
|
||||
response.geturl.return_value = "https://ops.example.test/inventory"
|
||||
response.read.return_value = json.dumps(payload).encode("utf-8")
|
||||
fetch = MagicMock(return_value=response)
|
||||
|
||||
with (
|
||||
patch.dict(os.environ, {"TEST_OPS_KEY": "secret-api-key"}),
|
||||
patch.object(deployment_cli, "urlopen", fetch),
|
||||
):
|
||||
result, stdout, stderr = run_cli(
|
||||
[
|
||||
"collect-infrastructure-inventory",
|
||||
"--directory",
|
||||
str(root),
|
||||
"--ops-url",
|
||||
"https://ops.example.test/inventory",
|
||||
"--api-key-env",
|
||||
"TEST_OPS_KEY",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(0, result, stderr)
|
||||
self.assertIn("1 record(s)", stdout)
|
||||
evidence_path = root / "infrastructure-dependency-inventory.json"
|
||||
self.assertEqual(0o600, stat.S_IMODE(evidence_path.stat().st_mode))
|
||||
self.assertNotIn(
|
||||
"secret-api-key",
|
||||
evidence_path.read_text(encoding="utf-8"),
|
||||
)
|
||||
request = fetch.call_args.args[0]
|
||||
self.assertEqual("secret-api-key", request.get_header("X-api-key"))
|
||||
|
||||
def test_cli_requires_external_url_when_switching_from_managed(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
|
||||
root = Path(directory) / "installation"
|
||||
@@ -1286,6 +1779,11 @@ class DeploymentInstallerTests(unittest.TestCase):
|
||||
receipt["listen"],
|
||||
)
|
||||
self.assertNotIn("installer", receipt["services"])
|
||||
self.assertEqual(
|
||||
1,
|
||||
receipt["infrastructure_capabilities"]["schema_version"],
|
||||
)
|
||||
self.assertTrue((root / "infrastructure-capabilities.json").is_file())
|
||||
|
||||
def test_installation_root_symlink_is_rejected(self) -> None:
|
||||
if not hasattr(Path, "symlink_to"):
|
||||
|
||||
Executable
+438
@@ -0,0 +1,438 @@
|
||||
"""Audit the selected fixture workspace, never a fuller neighboring checkout."""
|
||||
|
||||
from argparse import Namespace
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from types import ModuleType, SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = META_ROOT / "tools/inventory/platform-interface-inventory.py"
|
||||
WEBUI_SCRIPT = META_ROOT / "tools/inventory/extract-webui-structure.mjs"
|
||||
SPEC = importlib.util.spec_from_file_location("audit_scope_inventory", SCRIPT)
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
inventory = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(inventory)
|
||||
|
||||
sys.path.insert(0, str(META_ROOT / "tools/devkit"))
|
||||
from govoplan_devkit import docs, issues, runner # noqa: E402
|
||||
from govoplan_devkit.workspace import Project, Repository # noqa: E402
|
||||
|
||||
|
||||
def write(path, content):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspaces(tmp_path):
|
||||
selected = tmp_path / "selected"
|
||||
default = tmp_path / "fuller-default"
|
||||
meta = tmp_path / "legacy-siblings/govoplan"
|
||||
selected.mkdir()
|
||||
meta.mkdir(parents=True)
|
||||
catalog = {
|
||||
"default_parent": str(default),
|
||||
"repositories": [
|
||||
{"name": "govoplan-core", "path": "govoplan-core"},
|
||||
{"name": "govoplan-example", "path": "nested/example"},
|
||||
{"name": "govoplan-optional", "path": "govoplan-optional"},
|
||||
],
|
||||
}
|
||||
write(meta / "repositories.json", json.dumps(catalog))
|
||||
write(
|
||||
selected / "nested/example/webui/src/Page.tsx",
|
||||
"export const page = <h1>SELECTED_ONLY</h1>;\n",
|
||||
)
|
||||
write(
|
||||
default / "nested/example/webui/src/Page.tsx",
|
||||
"export const page = <h1>DEFAULT_ONLY</h1>;\n",
|
||||
)
|
||||
write(
|
||||
default / "govoplan-optional/webui/src/Page.tsx",
|
||||
"export const page = <h1>DEFAULT_OPTIONAL</h1>;\n",
|
||||
)
|
||||
for item in catalog["repositories"]:
|
||||
(default / item["path"] / "src").mkdir(parents=True)
|
||||
return selected, default, meta, catalog
|
||||
|
||||
|
||||
def test_explicit_partial_root_beats_fuller_legacy_discovery(workspaces, monkeypatch):
|
||||
selected, default, meta, catalog = workspaces
|
||||
monkeypatch.setattr(inventory, "META_ROOT", meta)
|
||||
assert inventory._resolve_workspace_root(catalog) == default
|
||||
assert inventory._resolve_workspace_root(catalog, selected) == selected
|
||||
with pytest.raises(ValueError, match="existing directory"):
|
||||
inventory._resolve_workspace_root(catalog, selected / "missing")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("relative", ["../fuller-default", "/absolute/path"])
|
||||
def test_repository_paths_cannot_escape_explicit_root(workspaces, relative):
|
||||
selected, _, _, _ = workspaces
|
||||
with pytest.raises(ValueError, match="inside the selected workspace"):
|
||||
inventory._validate_repository_roots(
|
||||
{"repositories": [{"path": relative}]}, selected
|
||||
)
|
||||
|
||||
|
||||
def test_linked_repository_cannot_borrow_default_sources(workspaces):
|
||||
selected, default, _, catalog = workspaces
|
||||
(selected / "govoplan-core").symlink_to(
|
||||
default / "govoplan-core", target_is_directory=True
|
||||
)
|
||||
with pytest.raises(ValueError, match="escapes the selected workspace"):
|
||||
inventory._validate_repository_roots(catalog, selected)
|
||||
|
||||
|
||||
def test_python_forwards_selected_root_and_configured_node(workspaces, monkeypatch):
|
||||
selected, _, meta, _ = workspaces
|
||||
monkeypatch.setattr(inventory, "META_ROOT", meta)
|
||||
monkeypatch.setenv("NODE", "/selected/toolchain/node")
|
||||
calls = []
|
||||
|
||||
def run(argv, **kwargs):
|
||||
calls.append((argv, kwargs))
|
||||
return SimpleNamespace(stdout=json.dumps({"workspaceRoot": str(selected)}))
|
||||
|
||||
monkeypatch.setattr(inventory.subprocess, "run", run)
|
||||
assert inventory._extract_webui(selected)["workspaceRoot"] == str(selected)
|
||||
argv, options = calls[0]
|
||||
assert argv == [
|
||||
"/selected/toolchain/node",
|
||||
str(meta / "tools/inventory/extract-webui-structure.mjs"),
|
||||
str(meta),
|
||||
"--workspace-root",
|
||||
str(selected),
|
||||
]
|
||||
assert options == {"check": True, "capture_output": True, "text": True}
|
||||
|
||||
|
||||
def test_python_rejects_webui_result_from_another_root(workspaces, monkeypatch):
|
||||
selected, default, _, _ = workspaces
|
||||
monkeypatch.setattr(
|
||||
inventory.subprocess,
|
||||
"run",
|
||||
lambda *a, **k: SimpleNamespace(
|
||||
stdout=json.dumps({"workspaceRoot": str(default)})
|
||||
),
|
||||
)
|
||||
with pytest.raises(ValueError, match="did not confirm"):
|
||||
inventory._extract_webui(selected)
|
||||
|
||||
|
||||
def test_main_forwards_same_root_to_every_collector(workspaces, monkeypatch):
|
||||
selected, _, meta, catalog = workspaces
|
||||
monkeypatch.setattr(inventory, "META_ROOT", meta)
|
||||
roots = []
|
||||
monkeypatch.setattr(
|
||||
inventory, "_extract_webui", lambda root: roots.append(root) or {}
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
inventory,
|
||||
"_extract_backend_endpoints",
|
||||
lambda data, root: roots.append(root) or [],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
inventory, "_extract_manifests", lambda data, root: roots.append(root) or []
|
||||
)
|
||||
monkeypatch.setattr(inventory, "_load_endpoint_declarations", lambda _: {})
|
||||
monkeypatch.setattr(inventory, "_load_high_risk_help_baseline", lambda _: {})
|
||||
monkeypatch.setattr(inventory, "_assemble_inventory", lambda **kwargs: {})
|
||||
monkeypatch.setattr(inventory, "_render_markdown", lambda data: "fixture\n")
|
||||
output = selected / "output"
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
[str(SCRIPT), "--workspace-root", str(selected), "--output-dir", str(output)],
|
||||
)
|
||||
assert inventory.main() == 0
|
||||
assert roots == [selected, selected, selected]
|
||||
report = json.loads((output / "platform-interface-inventory.json").read_text())
|
||||
assert report["workspace_root"] == str(selected)
|
||||
assert report["workspace_selection"] == "explicit"
|
||||
|
||||
|
||||
def install_fixture_compiler(root):
|
||||
compiler = META_ROOT.parent / "govoplan-core/webui/node_modules/typescript"
|
||||
if not compiler.is_dir() or not shutil.which("node"):
|
||||
pytest.skip("Node and the installed Core TypeScript parser are required")
|
||||
target = root / "govoplan-core/webui/node_modules/typescript"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Parser dependencies may be shared; audited source checkouts may not.
|
||||
target.symlink_to(compiler, target_is_directory=True)
|
||||
|
||||
|
||||
def collect_webui(meta, selected=None):
|
||||
argv = [shutil.which("node") or "node", str(WEBUI_SCRIPT), str(meta)]
|
||||
if selected is not None:
|
||||
argv += ["--workspace-root", str(selected)]
|
||||
return subprocess.run(argv, capture_output=True, text=True, timeout=30)
|
||||
|
||||
|
||||
def test_javascript_explicit_root_is_authoritative_and_legacy_cli_still_works(
|
||||
workspaces,
|
||||
):
|
||||
selected, default, meta, _ = workspaces
|
||||
install_fixture_compiler(selected)
|
||||
install_fixture_compiler(default)
|
||||
explicit = collect_webui(meta, selected)
|
||||
assert explicit.returncode == 0, explicit.stderr
|
||||
report = json.loads(explicit.stdout)
|
||||
assert report["workspaceRoot"] == str(selected)
|
||||
assert [item["value"] for item in report["visibleText"]] == ["SELECTED_ONLY"]
|
||||
assert report["visibleText"][0]["file"] == "webui/src/Page.tsx"
|
||||
legacy = collect_webui(meta)
|
||||
assert legacy.returncode == 0, legacy.stderr
|
||||
assert json.loads(legacy.stdout)["workspaceRoot"] == str(default)
|
||||
assert {item["value"] for item in json.loads(legacy.stdout)["visibleText"]} == {
|
||||
"DEFAULT_ONLY",
|
||||
"DEFAULT_OPTIONAL",
|
||||
}
|
||||
|
||||
|
||||
def test_javascript_does_not_borrow_missing_core_dependencies(workspaces):
|
||||
selected, default, meta, _ = workspaces
|
||||
install_fixture_compiler(default)
|
||||
result = collect_webui(meta, selected)
|
||||
assert result.returncode != 0
|
||||
assert (
|
||||
str(selected / "govoplan-core/webui/node_modules/typescript") in result.stderr
|
||||
)
|
||||
assert not result.stdout
|
||||
|
||||
|
||||
def test_javascript_rejects_source_root_linked_outside_workspace(workspaces):
|
||||
selected, default, meta, _ = workspaces
|
||||
install_fixture_compiler(selected)
|
||||
linked = selected / "govoplan-optional/webui/src"
|
||||
linked.parent.mkdir(parents=True)
|
||||
linked.symlink_to(default / "govoplan-optional/webui/src", target_is_directory=True)
|
||||
result = collect_webui(meta, selected)
|
||||
assert result.returncode != 0
|
||||
assert "source root escapes the selected workspace" in result.stderr
|
||||
|
||||
|
||||
def test_backend_does_not_borrow_optional_endpoints(workspaces):
|
||||
selected, default, _, catalog = workspaces
|
||||
source = "from fastapi import APIRouter\nrouter = APIRouter()\n@router.get('/selected')\ndef endpoint(): pass\n"
|
||||
write(selected / "nested/example/src/routes.py", source)
|
||||
write(
|
||||
default / "govoplan-optional/src/routes.py", source.replace("selected", "other")
|
||||
)
|
||||
endpoints = inventory._extract_backend_endpoints(catalog, selected)
|
||||
assert [item["path"] for item in endpoints] == ["/selected"]
|
||||
(selected / "nested/example/src/foreign.py").symlink_to(
|
||||
default / "govoplan-optional/src/routes.py"
|
||||
)
|
||||
with pytest.raises(ValueError, match="Backend source path escapes"):
|
||||
inventory._extract_backend_endpoints(catalog, selected)
|
||||
|
||||
|
||||
def test_manifests_require_selected_core_sources(workspaces, monkeypatch):
|
||||
selected, default, _, catalog = workspaces
|
||||
write(
|
||||
default / "govoplan-core/src/govoplan_core/core/platform_interfaces.py",
|
||||
"raise AssertionError('foreign source imported')",
|
||||
)
|
||||
monkeypatch.syspath_prepend(str(default / "govoplan-core/src"))
|
||||
with pytest.raises(ValueError, match="requires Core interface sources"):
|
||||
inventory._extract_manifests(catalog, selected)
|
||||
|
||||
|
||||
def test_cached_application_import_cannot_replace_missing_checkout(
|
||||
workspaces, monkeypatch
|
||||
):
|
||||
selected, default, _, _ = workspaces
|
||||
# Meta's own devkit/release packages may audit another workspace.
|
||||
tooling = ModuleType("govoplan_release")
|
||||
tooling.__file__ = str(default / "tools/release/govoplan_release/__init__.py")
|
||||
modules = {"govoplan_release": tooling, "govoplan_devkit.docs": docs}
|
||||
# Isolate the fixture from application packages collected by unrelated
|
||||
# suites, without removing or replacing those real cached imports.
|
||||
monkeypatch.setattr(inventory, "sys", SimpleNamespace(modules=modules))
|
||||
inventory._assert_workspace_imports(selected)
|
||||
application = ModuleType("govoplan_audit_fixture")
|
||||
application.__file__ = str(
|
||||
default / "govoplan-optional/src/govoplan_audit_fixture/__init__.py"
|
||||
)
|
||||
modules["govoplan_audit_fixture"] = application
|
||||
with pytest.raises(ValueError, match="outside the selected inventory workspace"):
|
||||
inventory._assert_workspace_imports(selected)
|
||||
|
||||
|
||||
def test_partial_manifest_collection_uses_selected_sources_in_fresh_process(workspaces):
|
||||
selected, default, _, catalog = workspaces
|
||||
core = selected / "govoplan-core/src/govoplan_core"
|
||||
write(core / "__init__.py", "")
|
||||
write(core / "core/__init__.py", "")
|
||||
write(
|
||||
core / "core/platform_interfaces.py",
|
||||
"def manifest_interface_catalog(manifest): return {'selected': True}\n",
|
||||
)
|
||||
write(
|
||||
default / "govoplan-core/src/govoplan_core/__init__.py",
|
||||
"raise AssertionError('foreign Core loaded')\n",
|
||||
)
|
||||
module = selected / "nested/example/src/govoplan_example"
|
||||
write(module / "__init__.py", "")
|
||||
write(module / "backend/__init__.py", "")
|
||||
manifest = (
|
||||
"from types import SimpleNamespace as S\n"
|
||||
"def get_manifest():\n"
|
||||
" return S(id='selected', name='Selected', version='1', dependencies=(), "
|
||||
"optional_dependencies=(), required_capabilities=(), provides_interfaces=(), "
|
||||
"capability_factories={}, permissions=(), documentation=(), architecture=None, "
|
||||
"information_governance=S(to_dict=lambda: {}), frontend=None)\n"
|
||||
)
|
||||
write(module / "backend/manifest.py", manifest)
|
||||
write(
|
||||
default / "govoplan-optional/src/govoplan_optional/backend/manifest.py",
|
||||
manifest.replace("selected", "foreign"),
|
||||
)
|
||||
code = (
|
||||
"import importlib.util, json; from pathlib import Path; "
|
||||
f"spec=importlib.util.spec_from_file_location('fixture', {str(SCRIPT)!r}); "
|
||||
"module=importlib.util.module_from_spec(spec); spec.loader.exec_module(module); "
|
||||
f"print(json.dumps(module._extract_manifests({catalog!r}, Path({str(selected)!r}))))"
|
||||
)
|
||||
env = {
|
||||
**os.environ,
|
||||
"PYTHONPATH": os.pathsep.join(
|
||||
str(default / item["path"] / "src") for item in catalog["repositories"]
|
||||
),
|
||||
}
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", code],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
manifests = json.loads(result.stdout)
|
||||
assert [manifest["id"] for manifest in manifests] == ["selected"]
|
||||
assert manifests[0]["interface_catalog"] == {"selected": True}
|
||||
|
||||
|
||||
def doc_plan(workspace, monkeypatch):
|
||||
repos = tuple(
|
||||
Repository(name, workspace / name)
|
||||
for name in ("govoplan", "govoplan-core", "govoplan-example")
|
||||
)
|
||||
project = Project("fixture", repos, {})
|
||||
monkeypatch.setattr(docs, "load_project", lambda *a: project)
|
||||
args = Namespace(
|
||||
workspace_root=workspace,
|
||||
project=None,
|
||||
state_dir=workspace.parent / "state",
|
||||
repo=["govoplan-example"],
|
||||
changed=False,
|
||||
)
|
||||
return docs.build_doc_stages(args)
|
||||
|
||||
|
||||
def test_docs_plan_forwards_root_and_persists_all_limitations(workspaces, monkeypatch):
|
||||
selected, _, _, _ = workspaces
|
||||
stages = doc_plan(selected, monkeypatch)
|
||||
by_id = {stage["id"]: stage for stage in stages}
|
||||
for stage_id in (
|
||||
"docs.manifests",
|
||||
"docs.interface-inventory",
|
||||
"docs.plain-display-labels",
|
||||
):
|
||||
argv = by_id[stage_id]["argv"]
|
||||
assert argv[argv.index("--workspace-root") + 1] == str(selected)
|
||||
assert by_id["docs.translation-structure"]["argv"][1] == str(
|
||||
selected / "govoplan-core/webui/scripts/audit-i18n-structural.mjs"
|
||||
)
|
||||
assert issues.coverage_notes(stages) == docs.LIMITATIONS
|
||||
assert by_id["docs.plain-display-labels"]["argv"][-2:] == [
|
||||
"--repo",
|
||||
"govoplan-example",
|
||||
]
|
||||
|
||||
|
||||
def test_docs_limitations_survive_real_receipt_and_issue_evidence(
|
||||
workspaces, monkeypatch, tmp_path
|
||||
):
|
||||
selected, _, _, _ = workspaces
|
||||
stages = doc_plan(selected, monkeypatch)
|
||||
repo = selected / "example"
|
||||
repo.mkdir()
|
||||
subprocess.run(["git", "init", "-q", str(repo)], check=True, timeout=10)
|
||||
write(repo / "source.txt", "fixture\n")
|
||||
subprocess.run(
|
||||
["git", "-C", str(repo), "add", "source.txt"], check=True, timeout=10
|
||||
)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(repo),
|
||||
"-c",
|
||||
"user.name=Fixture",
|
||||
"-c",
|
||||
"user.email=fixture@example.invalid",
|
||||
"commit",
|
||||
"-qm",
|
||||
"fixture",
|
||||
],
|
||||
check=True,
|
||||
timeout=10,
|
||||
)
|
||||
project = tmp_path / "project.json"
|
||||
write(
|
||||
project,
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"name": "fixture",
|
||||
"repositories": [{"name": "example", "path": "example"}],
|
||||
"checks": [],
|
||||
"profiles": {},
|
||||
}
|
||||
),
|
||||
)
|
||||
args = Namespace(
|
||||
workspace_root=selected,
|
||||
project=project,
|
||||
state_dir=tmp_path / "state",
|
||||
dry_run=False,
|
||||
jobs=2,
|
||||
profile="docs",
|
||||
resume=None,
|
||||
)
|
||||
for stage in stages:
|
||||
stage.update(
|
||||
argv=[sys.executable, "-c", "print('fixture audit')"],
|
||||
cwd=str(repo),
|
||||
deps=[],
|
||||
resources=[],
|
||||
timeout_seconds=10,
|
||||
)
|
||||
monkeypatch.setattr(docs, "build_doc_stages", lambda _: stages)
|
||||
monkeypatch.setattr(
|
||||
runner, "environment_fingerprint", lambda *a, **k: "fixture-env"
|
||||
)
|
||||
result = docs.audit(args)
|
||||
assert result["status"] == "passed", result
|
||||
assert all(result["summary"].count(note) == 1 for note in docs.LIMITATIONS)
|
||||
receipt = runner.read_receipt(selected, args.state_dir, result["run_id"])
|
||||
assert issues.coverage_notes(receipt["stages"]) == docs.LIMITATIONS
|
||||
evidence = issues.evidence_record(result["run_id"], args)
|
||||
assert evidence["coverage_notes"] == docs.LIMITATIONS
|
||||
assert evidence["source_state"] == "matches-current"
|
||||
target = issues.NoteTarget(
|
||||
repo, "https://gitea.example.invalid", "fixture", "example", 1
|
||||
)
|
||||
_, body = issues.render_note(
|
||||
{"summary": [], "next": [], "body": ""}, evidence, target, "fixture"
|
||||
)
|
||||
assert all(note in body for note in docs.LIMITATIONS)
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
"""Signals during snapshot probes must not become passing run evidence."""
|
||||
|
||||
import os
|
||||
import signal
|
||||
|
||||
import pytest
|
||||
|
||||
import test_devkit_runner as fixtures
|
||||
from govoplan_devkit import runner
|
||||
from govoplan_devkit.checkpoints import Checkpoints
|
||||
|
||||
example = fixtures.example
|
||||
|
||||
|
||||
@pytest.mark.parametrize("interruption", [signal.SIGINT, signal.SIGTERM])
|
||||
@pytest.mark.parametrize("probe", ["source", "environment"])
|
||||
def test_interrupt_during_final_snapshot_is_not_a_passing_run(
|
||||
example, monkeypatch, interruption, probe
|
||||
):
|
||||
args, repo = example
|
||||
calls = {"source": 0, "environment": 0}
|
||||
events, interruptions = [], []
|
||||
args.on_progress = events.append
|
||||
original = Checkpoints.source
|
||||
|
||||
def reached(kind):
|
||||
calls[kind] += 1
|
||||
if kind == probe and events and events[-1]["phase"] == "finalizing":
|
||||
# Intermediate checkpoint probes are also real verification. Target
|
||||
# finalization explicitly, while the runner owns signal handlers.
|
||||
interruptions.append(kind)
|
||||
os.kill(os.getpid(), interruption)
|
||||
|
||||
def source(self, *values, **kwargs):
|
||||
reached("source")
|
||||
return original(self, *values, **kwargs)
|
||||
|
||||
def environment(*_args, **_kwargs):
|
||||
reached("environment")
|
||||
return "fixture-environment"
|
||||
|
||||
monkeypatch.setattr(Checkpoints, "source", source)
|
||||
monkeypatch.setattr(runner, "environment_fingerprint", environment)
|
||||
result = runner.run_checks(args, [fixtures.stage(repo)])
|
||||
assert interruptions == [probe]
|
||||
assert calls[probe] >= 2
|
||||
assert result["stages"][0]["status"] == "passed"
|
||||
assert result["status"] == "interrupted"
|
||||
assert result["_exit_code"] != 0
|
||||
receipt = runner.read_receipt(args.workspace_root, args.state_dir, result["run_id"])
|
||||
assert receipt["status"] == "interrupted"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("interruption", [signal.SIGINT, signal.SIGTERM])
|
||||
@pytest.mark.parametrize("probe", ["source", "environment"])
|
||||
def test_preparation_interrupt_stops_at_probe_boundary_without_running_checks(
|
||||
example, monkeypatch, interruption, probe
|
||||
):
|
||||
args, repo = example
|
||||
calls = []
|
||||
original = Checkpoints.source
|
||||
|
||||
def reached(kind):
|
||||
calls.append(kind)
|
||||
if kind == probe:
|
||||
os.kill(os.getpid(), interruption)
|
||||
|
||||
def source(self, *values, **kwargs):
|
||||
reached("source")
|
||||
return original(self, *values, **kwargs)
|
||||
|
||||
def environment(*_args, **_kwargs):
|
||||
reached("environment")
|
||||
return "fixture-environment"
|
||||
|
||||
def must_not_execute(*args, **kwargs):
|
||||
pytest.fail("A cancelled preparation must not start a check")
|
||||
|
||||
monkeypatch.setattr(Checkpoints, "source", source)
|
||||
monkeypatch.setattr(runner, "environment_fingerprint", environment)
|
||||
monkeypatch.setattr(runner, "execute_stage", must_not_execute)
|
||||
result = runner.run_checks(args, [fixtures.stage(repo)])
|
||||
assert calls == (["source"] if probe == "source" else ["source", "environment"])
|
||||
assert result["status"] == "interrupted"
|
||||
assert result["snapshot_verified"] is False
|
||||
assert result["_exit_code"] != 0
|
||||
receipt = runner.read_receipt(args.workspace_root, args.state_dir, result["run_id"])
|
||||
assert receipt["status"] == "interrupted"
|
||||
Executable
+451
@@ -0,0 +1,451 @@
|
||||
"""Bounded planning fixtures: no application imports, compilers or servers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
|
||||
from govoplan_devkit.catalog import (
|
||||
_expanded_repositories,
|
||||
build_stages,
|
||||
module_ui_stages,
|
||||
)
|
||||
from govoplan_devkit.docs import build_doc_stages
|
||||
from govoplan_devkit.workspace import Project, Repository
|
||||
|
||||
|
||||
class CatalogTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory(prefix="govoplan-devkit-catalog-")
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.core = Repository("govoplan-core", self.root / "govoplan-core", ("core",))
|
||||
self.meta = Repository("govoplan", self.root / "govoplan", ("meta",))
|
||||
self.module = Repository(
|
||||
"govoplan-example", self.root / "govoplan-example", ("example",)
|
||||
)
|
||||
self.project = Project("fixture", (self.meta, self.core, self.module), {})
|
||||
for repo in self.project.repositories:
|
||||
repo.path.mkdir()
|
||||
(self.meta.path / "tools/checks").mkdir(parents=True)
|
||||
shutil.copyfile(
|
||||
Path(__file__).resolve().parents[1] / "tools/checks/focused-phases.json",
|
||||
self.meta.path / "tools/checks/focused-phases.json",
|
||||
)
|
||||
|
||||
def write(self, relative, content):
|
||||
path = self.root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
def plan(self, profile, repos=None, changed=False):
|
||||
with patch("govoplan_devkit.workspace.load_project", return_value=self.project):
|
||||
return build_stages(self.root, profile, repos or [], changed)
|
||||
|
||||
def test_ui_compiles_once_and_quick_does_not_compile(self):
|
||||
self.write(
|
||||
"govoplan-core/webui/package.json",
|
||||
json.dumps(
|
||||
{"scripts": {"test:components": "node scripts/run-component-tests.mjs"}}
|
||||
),
|
||||
)
|
||||
self.write("govoplan-core/webui/scripts/run-component-tests.mjs", "// fixture")
|
||||
self.assertFalse(
|
||||
any(
|
||||
"run-component-tests.mjs" in " ".join(item["argv"])
|
||||
for item in self.plan("quick")
|
||||
)
|
||||
)
|
||||
matches = [
|
||||
item
|
||||
for item in self.plan("ui")
|
||||
if "run-component-tests.mjs" in " ".join(item["argv"])
|
||||
]
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertEqual(matches[0]["id"], "core.component-batch")
|
||||
|
||||
def test_module_script_metadata_is_bounded_and_deduplicated(self):
|
||||
self.write(
|
||||
"govoplan-example/webui/package.json",
|
||||
json.dumps(
|
||||
{
|
||||
"scripts": {
|
||||
"test:interface-pattern": "node scripts/test-interface-pattern-language.mjs",
|
||||
"test:source": "node --test tests/source.test.mjs",
|
||||
"test:dangerous-chain": "node tests/source.test.mjs && npm run dev",
|
||||
"test:flags": "node --eval 'startServer()'",
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
self.write(
|
||||
"govoplan-example/webui/scripts/test-interface-pattern-language.mjs",
|
||||
"// source-only fixture",
|
||||
)
|
||||
self.write(
|
||||
"govoplan-example/webui/tests/source.test.mjs", "// source-only fixture"
|
||||
)
|
||||
stages = module_ui_stages(self.module, reason="fixture")
|
||||
self.assertEqual(len(stages), 2)
|
||||
self.assertEqual(len({item["id"] for item in stages}), 2)
|
||||
self.assertTrue(all(item["argv"][0] == "{node}" for item in stages))
|
||||
self.assertFalse(any("&&" in item["argv"] for item in stages))
|
||||
|
||||
def test_full_is_never_narrowed_by_repo_filter(self):
|
||||
result = self.plan("full", ["example"])
|
||||
self.assertEqual(
|
||||
[item["id"] for item in result],
|
||||
[
|
||||
"focused." + identity
|
||||
for identity in (
|
||||
"preflight",
|
||||
"tooling",
|
||||
"backend",
|
||||
"core-ui",
|
||||
"module-builds",
|
||||
"browser",
|
||||
"module-ui",
|
||||
)
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
[item["after"] for item in result],
|
||||
[[], *[[item["id"]] for item in result[:-1]]],
|
||||
)
|
||||
self.assertTrue(all(item["deps"] == [] for item in result))
|
||||
self.assertTrue(
|
||||
all(
|
||||
item["argv"]
|
||||
== [
|
||||
"bash",
|
||||
str(self.meta.path / "tools/checks/check-focused.sh"),
|
||||
"--phase",
|
||||
item["id"].removeprefix("focused."),
|
||||
]
|
||||
for item in result
|
||||
)
|
||||
)
|
||||
self.assertIn("webui:govoplan-example", result[-1]["resources"])
|
||||
self.assertIn("backend:test-state", result[0]["resources"])
|
||||
|
||||
def test_changed_empty_is_not_full_verification(self):
|
||||
with (
|
||||
patch("govoplan_devkit.workspace.load_project", return_value=self.project),
|
||||
patch("govoplan_devkit.workspace.selected_repositories", return_value=[]),
|
||||
):
|
||||
self.assertEqual(build_stages(self.root, "quick", [], True), [])
|
||||
self.assertEqual(len(build_stages(self.root, "full", [], True)), 7)
|
||||
|
||||
def test_full_ui_scope_excludes_backend_only_but_includes_all_ui_owners(self):
|
||||
backend = Repository(
|
||||
"govoplan-backend-only", self.root / "govoplan-backend-only"
|
||||
)
|
||||
backend.path.mkdir()
|
||||
(self.module.path / "webui").mkdir()
|
||||
(self.core.path / "webui").mkdir()
|
||||
project = Project("fixture", (*self.project.repositories, backend), {})
|
||||
with patch("govoplan_devkit.workspace.load_project", return_value=project):
|
||||
checks = build_stages(self.root, "full", ["example"], False)
|
||||
for check in checks[:3]:
|
||||
self.assertNotIn("inputs", check)
|
||||
for check in checks[3:]:
|
||||
self.assertEqual(
|
||||
check["inputs"]["repos"],
|
||||
["govoplan", "govoplan-core", "govoplan-example"],
|
||||
)
|
||||
(backend.path / "webui").mkdir()
|
||||
with patch("govoplan_devkit.workspace.load_project", return_value=project):
|
||||
replanned = build_stages(self.root, "full", [], False)
|
||||
self.assertIn(backend.name, replanned[3]["inputs"]["repos"])
|
||||
|
||||
def test_full_missing_checkout_explicitly_falls_back_to_workspace_inputs(self):
|
||||
missing = Repository("govoplan-missing", self.root / "govoplan-missing")
|
||||
project = Project("fixture", (*self.project.repositories, missing), {})
|
||||
with patch("govoplan_devkit.workspace.load_project", return_value=project):
|
||||
checks = build_stages(self.root, "full", [], False)
|
||||
self.assertTrue(all("inputs" not in check for check in checks))
|
||||
self.assertTrue(
|
||||
all(
|
||||
any("workspace-wide" in note for note in check["coverage_notes"])
|
||||
for check in checks[3:]
|
||||
)
|
||||
)
|
||||
|
||||
def test_full_requires_authoritative_phase_metadata(self):
|
||||
(self.meta.path / "tools/checks/focused-phases.json").write_text("{}")
|
||||
with self.assertRaisesRegex(ValueError, "phase metadata"):
|
||||
self.plan("full")
|
||||
|
||||
def test_native_ui_guards_and_component_batch_share_safe_ui_owner_scope(self):
|
||||
backend = Repository(
|
||||
"govoplan-backend-only", self.root / "govoplan-backend-only"
|
||||
)
|
||||
backend.path.mkdir()
|
||||
(self.module.path / "webui").mkdir()
|
||||
(self.core.path / "webui").mkdir()
|
||||
project = Project("fixture", (*self.project.repositories, backend), {})
|
||||
with patch("govoplan_devkit.workspace.load_project", return_value=project):
|
||||
checks = build_stages(self.root, "ui", ["example"], False)
|
||||
for check in checks:
|
||||
if check["id"] in {
|
||||
"jsx-value-imports",
|
||||
"heading-help",
|
||||
"core.component-batch",
|
||||
}:
|
||||
self.assertEqual(
|
||||
check["inputs"]["repos"],
|
||||
["govoplan", "govoplan-core", "govoplan-example"],
|
||||
)
|
||||
else:
|
||||
self.assertNotIn("inputs", check)
|
||||
|
||||
def test_unregistered_src_or_webui_disables_all_native_reuse(self):
|
||||
for directory in ("src", "webui"):
|
||||
with self.subTest(directory=directory):
|
||||
unknown = self.root / "govoplan-unregistered" / directory
|
||||
unknown.mkdir(parents=True)
|
||||
try:
|
||||
for profile in ("quick", "ui", "backend", "full"):
|
||||
checks = self.plan(profile)
|
||||
self.assertTrue(checks)
|
||||
self.assertTrue(
|
||||
all(check["reuse"] == "never" for check in checks)
|
||||
)
|
||||
self.assertTrue(
|
||||
all(
|
||||
any(
|
||||
"Unregistered sibling" in note
|
||||
for note in check["coverage_notes"]
|
||||
)
|
||||
for check in checks
|
||||
)
|
||||
)
|
||||
finally:
|
||||
unknown.rmdir()
|
||||
unknown.parent.rmdir()
|
||||
(self.root / "govoplan-unused-empty").mkdir()
|
||||
self.assertTrue(all("reuse" not in check for check in self.plan("full")))
|
||||
|
||||
def test_unregistered_broken_source_symlink_does_not_allow_reuse(self):
|
||||
unknown = self.root / "govoplan-unregistered"
|
||||
unknown.mkdir()
|
||||
(unknown / "src").symlink_to(self.root / "missing")
|
||||
self.assertTrue(all(check["reuse"] == "never" for check in self.plan("full")))
|
||||
|
||||
def test_unknown_repo_is_not_silently_ignored(self):
|
||||
with self.assertRaisesRegex(ValueError, "Unknown repository"):
|
||||
self.plan("ui", ["not-a-repo"])
|
||||
|
||||
def test_changed_provider_selects_transitive_declared_consumers(self):
|
||||
other = Repository("govoplan-other", self.root / "govoplan-other")
|
||||
final = Repository("govoplan-final", self.root / "govoplan-final")
|
||||
project = Project("fixture", (self.meta, self.module, other, final), {})
|
||||
self.write("govoplan/tools/release/govoplan_release/contracts.py", "# fixture")
|
||||
for name in ("example", "other", "final"):
|
||||
self.write(f"govoplan-{name}/src/fixture/backend/manifest.py", "# fixture")
|
||||
|
||||
def contract(_path, repo_name):
|
||||
gives = {
|
||||
self.module.name: ["first"],
|
||||
other.name: ["second"],
|
||||
final.name: [],
|
||||
}[repo_name]
|
||||
needs = {
|
||||
self.module.name: [],
|
||||
other.name: ["first"],
|
||||
final.name: ["second"],
|
||||
}[repo_name]
|
||||
return SimpleNamespace(
|
||||
repo=repo_name,
|
||||
provides_interfaces=[SimpleNamespace(name=value) for value in gives],
|
||||
requires_interfaces=[SimpleNamespace(name=value) for value in needs],
|
||||
)
|
||||
|
||||
with patch.dict(
|
||||
sys.modules,
|
||||
{
|
||||
"govoplan_release.contracts": SimpleNamespace(
|
||||
parse_manifest_contract=contract
|
||||
)
|
||||
},
|
||||
):
|
||||
selected, reason = _expanded_repositories(
|
||||
project, [self.module], changed=True
|
||||
)
|
||||
self.assertEqual(
|
||||
[repo.name for repo in selected], [self.module.name, other.name, final.name]
|
||||
)
|
||||
self.assertIn("declared interface consumers", reason)
|
||||
|
||||
def test_generic_dependency_closure_includes_filtered_prerequisites(self):
|
||||
config = {
|
||||
"schema_version": 1,
|
||||
"name": "generic",
|
||||
"repositories": [
|
||||
{"name": "one", "path": "one"},
|
||||
{"name": "two", "path": "two"},
|
||||
],
|
||||
"checks": [
|
||||
{
|
||||
"id": "compile",
|
||||
"argv": ["{node}", "compile.mjs"],
|
||||
"cwd": "one",
|
||||
"repos": ["two"],
|
||||
},
|
||||
{
|
||||
"id": "test",
|
||||
"argv": ["{node}", "test.mjs"],
|
||||
"cwd": "one",
|
||||
"deps": ["compile"],
|
||||
"repos": ["one"],
|
||||
},
|
||||
{
|
||||
"id": "other",
|
||||
"argv": ["{python}", "test.py"],
|
||||
"cwd": "two",
|
||||
"repos": ["two"],
|
||||
},
|
||||
],
|
||||
"profiles": {"quick": ["test", "other"]},
|
||||
}
|
||||
self.write("project.json", json.dumps(config))
|
||||
result = build_stages(
|
||||
self.root, "quick", ["one"], False, self.root / "project.json"
|
||||
)
|
||||
self.assertEqual([item["id"] for item in result], ["compile", "test"])
|
||||
self.assertEqual(result[1]["deps"], ["compile"])
|
||||
self.assertEqual(result[1]["cwd"], str(self.root / "one"))
|
||||
|
||||
def test_generic_cycles_and_duplicate_ids_fail(self):
|
||||
config = {
|
||||
"schema_version": 1,
|
||||
"name": "generic",
|
||||
"repositories": [{"name": "one", "path": "one"}],
|
||||
"checks": [
|
||||
{"id": "one", "argv": ["test"], "deps": ["two"]},
|
||||
{"id": "two", "argv": ["test"], "deps": ["one"]},
|
||||
],
|
||||
"profiles": {"quick": ["one"]},
|
||||
}
|
||||
self.write("project.json", json.dumps(config))
|
||||
with self.assertRaisesRegex(ValueError, "Cyclic"):
|
||||
build_stages(self.root, "quick", [], False, self.root / "project.json")
|
||||
config["checks"][1]["id"] = "one"
|
||||
self.write("project.json", json.dumps(config))
|
||||
with self.assertRaisesRegex(ValueError, "Duplicate"):
|
||||
build_stages(self.root, "quick", [], False, self.root / "project.json")
|
||||
|
||||
def test_generic_order_prerequisites_inputs_and_reuse_survive_planning(self):
|
||||
config = {
|
||||
"schema_version": 1,
|
||||
"name": "generic",
|
||||
"repositories": [
|
||||
{"name": "one", "path": "one"},
|
||||
{"name": "two", "path": "two"},
|
||||
],
|
||||
"checks": [
|
||||
{
|
||||
"id": "prepare",
|
||||
"argv": ["prepare"],
|
||||
"repos": ["two"],
|
||||
"reuse": "never",
|
||||
"inputs": {"repos": ["two"]},
|
||||
},
|
||||
{
|
||||
"id": "test",
|
||||
"argv": ["test"],
|
||||
"repos": ["one"],
|
||||
"after": ["prepare"],
|
||||
"reuse": "verified",
|
||||
"inputs": {"repos": ["one"]},
|
||||
},
|
||||
{"id": "broad", "argv": ["check"], "repos": ["one"]},
|
||||
],
|
||||
"profiles": {"quick": ["test", "broad"]},
|
||||
}
|
||||
self.write("project.json", json.dumps(config))
|
||||
result = build_stages(
|
||||
self.root, "quick", ["one"], False, self.root / "project.json"
|
||||
)
|
||||
self.assertEqual([item["id"] for item in result], ["prepare", "test", "broad"])
|
||||
self.assertEqual(result[1]["after"], ["prepare"])
|
||||
self.assertEqual(result[1]["deps"], [])
|
||||
self.assertEqual(result[0]["reuse"], "never")
|
||||
self.assertEqual(result[1]["reuse"], "verified")
|
||||
self.assertEqual(result[0]["inputs"], {"repos": ["two"]})
|
||||
self.assertEqual(result[1]["inputs"], {"repos": ["one"]})
|
||||
self.assertNotIn("inputs", result[2])
|
||||
|
||||
def test_generic_original_types_are_validated_before_coercion(self):
|
||||
for field, invalid in (
|
||||
("argv", "false"),
|
||||
("argv", None),
|
||||
("resources", "shared"),
|
||||
("cwd", None),
|
||||
("cwd", "../outside"),
|
||||
("title", 5),
|
||||
("timeout_seconds", True),
|
||||
):
|
||||
with self.subTest(field=field, invalid=invalid):
|
||||
config = {
|
||||
"schema_version": 1,
|
||||
"name": "generic",
|
||||
"repositories": [{"name": "one", "path": "one"}],
|
||||
"checks": [{"id": "test", "argv": ["test"], field: invalid}],
|
||||
"profiles": {"quick": ["test"]},
|
||||
}
|
||||
self.write("project.json", json.dumps(config))
|
||||
with self.assertRaises(ValueError):
|
||||
build_stages(
|
||||
self.root, "quick", [], False, self.root / "project.json"
|
||||
)
|
||||
|
||||
def test_ui_coverage_notes_make_excluded_suites_explicit_and_discover_tests_folder(
|
||||
self,
|
||||
):
|
||||
self.write(
|
||||
"govoplan-example/webui/package.json",
|
||||
json.dumps({"scripts": {"test:full-ui": "tsc && node tests/full-ui.js"}}),
|
||||
)
|
||||
self.write(
|
||||
"govoplan-example/webui/tests/aggregate-report-structure.test.mjs",
|
||||
"// fixture",
|
||||
)
|
||||
stages = self.plan("ui", ["example"])
|
||||
self.assertTrue(
|
||||
any("aggregate-report-structure" in item["id"] for item in stages)
|
||||
)
|
||||
self.assertTrue(
|
||||
any("test:full-ui" in note for note in stages[0]["coverage_notes"])
|
||||
)
|
||||
|
||||
def test_docs_reuses_existing_guards_and_only_narrows_plain_labels(self):
|
||||
args = argparse.Namespace(
|
||||
workspace_root=self.root,
|
||||
state_dir=self.root / "state",
|
||||
project=None,
|
||||
repo=["example"],
|
||||
changed=False,
|
||||
)
|
||||
with patch("govoplan_devkit.docs.load_project", return_value=self.project):
|
||||
stages = build_doc_stages(args)
|
||||
self.assertEqual(len(stages), 4)
|
||||
self.assertIn("check-manifest-shapes.py", " ".join(stages[0]["argv"]))
|
||||
self.assertIn("platform-interface-inventory.py", " ".join(stages[1]["argv"]))
|
||||
self.assertIn("--strict-declarations", stages[1]["argv"])
|
||||
self.assertEqual(stages[3]["argv"][-2:], ["--repo", "govoplan-example"])
|
||||
self.assertFalse(
|
||||
(self.root / "state").exists(), "Planning must not create artifacts"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Executable
+158
@@ -0,0 +1,158 @@
|
||||
"""Public CLI contracts and an executable portable-project smoke fixture."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
|
||||
from govoplan_devkit import cli, doctor, runner
|
||||
from govoplan_devkit.common import META_ROOT
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arguments",
|
||||
[["--json", "commands"], ["commands", "--json"], ["commands", "--format", "json"]],
|
||||
)
|
||||
def test_global_output_flags_work_on_either_side_of_command(arguments, capsys):
|
||||
assert cli.main(arguments) == 0
|
||||
output = json.loads(capsys.readouterr().out)
|
||||
names = {item["command"] for item in output["commands"]}
|
||||
assert {"context", "check", "doctor", "docs", "issues", "release", "git"} <= names
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"command",
|
||||
[
|
||||
"context",
|
||||
"doctor",
|
||||
"check",
|
||||
"resume",
|
||||
"recover",
|
||||
"docs",
|
||||
"review",
|
||||
"issues",
|
||||
"release",
|
||||
"git",
|
||||
],
|
||||
)
|
||||
def test_command_help_does_not_require_live_services(command, capsys):
|
||||
with pytest.raises(SystemExit) as stopped:
|
||||
cli.main([command, "--help"])
|
||||
assert stopped.value.code == 0
|
||||
assert "usage:" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_portable_project_executes_registered_commands_and_reads_receipt(
|
||||
tmp_path, capsys, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "state"))
|
||||
repo = tmp_path / "example"
|
||||
repo.mkdir()
|
||||
subprocess.run(["git", "init", "-q", str(repo)], check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(repo),
|
||||
"-c",
|
||||
"user.name=Fixture",
|
||||
"-c",
|
||||
"user.email=fixture@example.invalid",
|
||||
"commit",
|
||||
"--allow-empty",
|
||||
"-qm",
|
||||
"fixture",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
project = tmp_path / "project.json"
|
||||
project.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"name": "Portable",
|
||||
"repositories": [{"name": "example", "path": "example"}],
|
||||
"checks": [
|
||||
{
|
||||
"id": "test",
|
||||
"argv": ["{python}", "-c", "print('portable ok')"],
|
||||
"cwd": "example",
|
||||
}
|
||||
],
|
||||
"profiles": {"quick": ["test"]},
|
||||
}
|
||||
)
|
||||
)
|
||||
common = ["--workspace-root", str(tmp_path), "--project", str(project), "--json"]
|
||||
with patch.object(runner, "environment_fingerprint", return_value="fixture"):
|
||||
assert cli.main(common + ["check", "--profile", "quick"]) == 0
|
||||
result = json.loads(capsys.readouterr().out)
|
||||
assert result["status"] == "passed"
|
||||
assert cli.main(["status", result["run_id"], *common]) == 0
|
||||
status = json.loads(capsys.readouterr().out)
|
||||
assert status["snapshot_verified"] is True
|
||||
assert cli.main(["logs", result["run_id"], "--stage", "test", *common]) == 0
|
||||
assert "portable ok" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_portable_example_matches_published_schema():
|
||||
import jsonschema
|
||||
|
||||
jsonschema.validate(
|
||||
json.loads((META_ROOT / "tools/devkit/examples/project.json").read_text()),
|
||||
json.loads((META_ROOT / "tools/devkit/project.schema.json").read_text()),
|
||||
)
|
||||
|
||||
|
||||
def test_malformed_project_is_a_controlled_json_error(tmp_path, capsys):
|
||||
project = tmp_path / "bad.json"
|
||||
project.write_text('{"schema_version":true,"repositories":[]}')
|
||||
assert (
|
||||
cli.main(
|
||||
[
|
||||
"context",
|
||||
"--workspace-root",
|
||||
str(tmp_path),
|
||||
"--project",
|
||||
str(project),
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
== 2
|
||||
)
|
||||
assert json.loads(capsys.readouterr().out)["status"] == "error"
|
||||
|
||||
|
||||
def test_doctor_is_read_only_and_preserves_dependency_warnings(tmp_path, capsys):
|
||||
project = tmp_path / "project.json"
|
||||
project.write_text(
|
||||
json.dumps(
|
||||
{"schema_version": 1, "repositories": [{"name": "example", "path": "."}]}
|
||||
)
|
||||
)
|
||||
(tmp_path / "package.json").write_text("{}")
|
||||
before = set(tmp_path.iterdir())
|
||||
with (
|
||||
patch.object(doctor, "tool_version", return_value="fixture"),
|
||||
patch.object(doctor, "inspect_repository", return_value={"errors": []}),
|
||||
):
|
||||
assert (
|
||||
cli.main(
|
||||
[
|
||||
"doctor",
|
||||
"--workspace-root",
|
||||
str(tmp_path),
|
||||
"--project",
|
||||
str(project),
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
== 0
|
||||
)
|
||||
output = json.loads(capsys.readouterr().out)
|
||||
assert any(item["status"] == "warning" for item in output["checks"])
|
||||
assert set(tmp_path.iterdir()) == before
|
||||
Executable
+339
@@ -0,0 +1,339 @@
|
||||
"""Coverage inventory is explicit intent, never execution or guessed completion."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "tools/devkit"))
|
||||
from govoplan_devkit.catalog import build_coverage, build_stages, module_ui_stages # noqa: E402
|
||||
from govoplan_devkit.coverage import canonical_invocations # noqa: E402
|
||||
from govoplan_devkit.package_tests import ( # noqa: E402
|
||||
CORE_COMPONENT_SUITES,
|
||||
declared_tests,
|
||||
read_package,
|
||||
)
|
||||
from govoplan_devkit.workspace import Project, Repository # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fixture(tmp_path):
|
||||
core = Repository("govoplan-core", tmp_path / "govoplan-core", ("core",))
|
||||
meta = Repository("govoplan", tmp_path / "govoplan", ("meta",))
|
||||
module = Repository("govoplan-example", tmp_path / "govoplan-example", ("example",))
|
||||
for repo in (core, meta, module):
|
||||
(repo.path / "webui/scripts").mkdir(parents=True)
|
||||
(repo.path / "webui/tests").mkdir()
|
||||
scripts = {
|
||||
"test:components": "node scripts/run-component-tests.mjs",
|
||||
**{
|
||||
f"test:{name}": f"node scripts/run-component-tests.mjs {name}"
|
||||
for name in CORE_COMPONENT_SUITES
|
||||
},
|
||||
}
|
||||
(core.path / "webui/package.json").write_text(json.dumps({"scripts": scripts}))
|
||||
(core.path / "webui/scripts/run-component-tests.mjs").write_text("// fixture")
|
||||
(module.path / "webui/package.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"scripts": {
|
||||
"test:safe": "node --test tests/source.test.mjs",
|
||||
"test:compound": "tsc && node test.js",
|
||||
"test:bad-quote": "node 'broken",
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
(module.path / "webui/tests/source.test.mjs").write_text("// fixture")
|
||||
script = meta.path / "tools/checks/check-focused.sh"
|
||||
script.parent.mkdir(parents=True)
|
||||
phase_metadata = ROOT / "tools/checks/focused-phases.json"
|
||||
shutil.copyfile(phase_metadata, script.with_name("focused-phases.json"))
|
||||
bodies = {
|
||||
"core-ui": 'cd "$ROOT/webui"\n"$NPM" run test:components -- layout-primitives page-layout data-grid-actions mail-components\n',
|
||||
"module-ui": 'cd "$WORKSPACE_ROOT/govoplan-example/webui"\n"$NPM" run test:compound\n',
|
||||
}
|
||||
script.write_text(
|
||||
"\n".join(
|
||||
f"focused_phase_{phase['id'].replace('-', '_')}() {{\n# devkit-phase: {phase['id']} begin\n"
|
||||
+ bodies.get(phase["id"], 'cd "$ROOT"\n')
|
||||
+ f"# devkit-phase: {phase['id']} end\n}}\n"
|
||||
for phase in json.loads(phase_metadata.read_text())["phases"]
|
||||
)
|
||||
)
|
||||
project = Project("Fixture", (meta, core, module), {})
|
||||
with (
|
||||
patch("govoplan_devkit.workspace.load_project", return_value=project),
|
||||
patch("govoplan_devkit.coverage.load_project", return_value=project),
|
||||
):
|
||||
yield tmp_path, core, module, project
|
||||
|
||||
|
||||
def rows(coverage, repo="govoplan-core"):
|
||||
return {row["name"]: row for row in coverage["suites"] if row["repo"] == repo}
|
||||
|
||||
|
||||
def test_core_aliases_are_explicitly_excluded_in_quick_and_covered_by_one_ui_stage(
|
||||
fixture,
|
||||
):
|
||||
root, _, _, _ = fixture
|
||||
quick = rows(build_coverage(root, "quick", [], False))
|
||||
ui = rows(build_coverage(root, "ui", [], False))
|
||||
for suite in CORE_COMPONENT_SUITES:
|
||||
assert quick["test:" + suite]["disposition"] == "excluded"
|
||||
assert "quick" in quick["test:" + suite]["reason"]
|
||||
assert ui["test:" + suite]["disposition"] == "covered_elsewhere"
|
||||
assert ui["test:" + suite]["covering_stage"] == "core.component-batch"
|
||||
assert ui["test:components"]["covered_components"] == list(CORE_COMPONENT_SUITES)
|
||||
|
||||
|
||||
def test_full_reports_only_four_of_sixteen_components_and_exact_shell_suite(fixture):
|
||||
root, _, _, _ = fixture
|
||||
result = build_coverage(root, "full", ["example"], False)
|
||||
core = rows(result)
|
||||
aliases = [core["test:" + suite] for suite in CORE_COMPONENT_SUITES]
|
||||
assert sum(item["disposition"] == "covered_elsewhere" for item in aliases) == 4
|
||||
assert sum(item["disposition"] == "excluded" for item in aliases) == 12
|
||||
assert len(core["test:components"]["covered_components"]) == 4
|
||||
assert "4/16" in core["test:components"]["reason"]
|
||||
module = rows(result, "govoplan-example")
|
||||
assert module["test:compound"]["disposition"] == "planned"
|
||||
assert module["test:safe"]["disposition"] == "excluded"
|
||||
assert result["stages"] == [
|
||||
"focused." + phase["id"]
|
||||
for phase in json.loads(
|
||||
(ROOT / "tools/checks/focused-phases.json").read_text()
|
||||
)["phases"]
|
||||
]
|
||||
assert core["test:components"]["covering_stage"] == "focused.core-ui"
|
||||
assert module["test:compound"]["covering_stage"] == "focused.module-ui"
|
||||
|
||||
|
||||
def test_prebuilt_plan_avoids_replanning(fixture):
|
||||
root, _, _, _ = fixture
|
||||
stages = build_stages(root, "quick", [], False)
|
||||
with patch(
|
||||
"govoplan_devkit.catalog.build_stages",
|
||||
side_effect=AssertionError("must not replan"),
|
||||
):
|
||||
result = build_coverage(root, "quick", [], False, stages=stages)
|
||||
assert rows(result, "govoplan-example")["test:safe"]["disposition"] == "planned"
|
||||
assert sum(result["counts"].values()) == result["suite_count"]
|
||||
assert all(item["reason"] for item in result["suites"])
|
||||
|
||||
|
||||
def test_missing_or_spoofed_phase_stage_does_not_grant_component_coverage(fixture):
|
||||
root, _, _, _ = fixture
|
||||
stages = build_stages(root, "full", [], False)
|
||||
without_core = [item for item in stages if item["id"] != "focused.core-ui"]
|
||||
result = build_coverage(root, "full", [], False, stages=without_core)
|
||||
assert rows(result)["test:components"]["covered_components"] == []
|
||||
assert (
|
||||
rows(result, "govoplan-example")["test:compound"]["covering_stage"]
|
||||
== "focused.module-ui"
|
||||
)
|
||||
next(item for item in stages if item["id"] == "focused.core-ui")["argv"] = ["true"]
|
||||
spoofed = build_coverage(root, "full", [], False, stages=stages)
|
||||
assert rows(spoofed)["test:components"]["covered_components"] == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("wrapper", ["heredoc", "function", "conditional"])
|
||||
def test_lookalike_phase_wrappers_outside_top_level_do_not_grant_coverage(
|
||||
fixture, wrapper
|
||||
):
|
||||
root, core, _, project = fixture
|
||||
meta = next(repo.path for repo in project.repositories if repo.name == "govoplan")
|
||||
path = meta / "tools/checks/check-focused.sh"
|
||||
fake = 'focused_phase_core_ui() {\n# devkit-phase: core-ui begin\ncd "$ROOT/webui"\n"$NPM" run test:spoof\n# devkit-phase: core-ui end\n}\n'
|
||||
prefix = {
|
||||
"heredoc": "cat <<'BODY'\n" + fake + "BODY\n",
|
||||
"function": "unused() {\n" + fake + "}\n",
|
||||
"conditional": "if false; then\n" + fake + "fi\n",
|
||||
}[wrapper]
|
||||
path.write_text(prefix + path.read_text())
|
||||
result = canonical_invocations(root, meta, core.path)
|
||||
assert result["notes"] == []
|
||||
assert [item["name"] for item in result["npm"]] == [
|
||||
"test:components",
|
||||
"test:compound",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("damage", ["missing", "duplicate", "bad-end"])
|
||||
def test_invalid_marked_phase_bodies_do_not_infer_coverage(fixture, damage):
|
||||
root, core, _, project = fixture
|
||||
meta = next(repo.path for repo in project.repositories if repo.name == "govoplan")
|
||||
path = meta / "tools/checks/check-focused.sh"
|
||||
text = path.read_text()
|
||||
if damage == "missing":
|
||||
text = text.replace("focused_phase_core_ui()", "unregistered_core_ui()")
|
||||
elif damage == "duplicate":
|
||||
text += text
|
||||
else:
|
||||
text = text.replace(
|
||||
"# devkit-phase: core-ui end", "# devkit-phase: another end"
|
||||
)
|
||||
path.write_text(text)
|
||||
result = canonical_invocations(root, meta, core.path)
|
||||
assert result["npm"] == [] and result["node"] == []
|
||||
assert "no phase coverage inferred" in result["notes"][0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"repo_name,name,command",
|
||||
[
|
||||
("govoplan-example", "test:components", "node scripts/run-component-tests.mjs"),
|
||||
(
|
||||
"govoplan-core",
|
||||
"test:dialog-focus",
|
||||
"node scripts/run-component-tests.mjs dialog-focus && npm run dev",
|
||||
),
|
||||
(
|
||||
"govoplan-core",
|
||||
"test:unrecognized",
|
||||
"node scripts/run-component-tests.mjs dialog-focus",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_component_alias_exemption_is_exact_and_core_only(
|
||||
tmp_path, repo_name, name, command
|
||||
):
|
||||
repo = Repository(repo_name, tmp_path / repo_name)
|
||||
package = repo.path / "webui/package.json"
|
||||
package.parent.mkdir(parents=True)
|
||||
package.write_text(json.dumps({"scripts": {name: command}}))
|
||||
declared = declared_tests(repo, package)
|
||||
assert declared[0]["component_suite"] is None
|
||||
assert declared[0]["_argv"] is None
|
||||
assert module_ui_stages(repo, reason="fixture") == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"body",
|
||||
[
|
||||
"[]",
|
||||
"null",
|
||||
'{"scripts":[]}',
|
||||
'{"scripts":{"test:bad":42}}',
|
||||
'{"scripts":{"test:one":"node a","test:one":"node b"}}',
|
||||
"[" * 2000 + "]" * 2000,
|
||||
],
|
||||
)
|
||||
def test_malformed_package_metadata_is_a_controlled_error(tmp_path, body):
|
||||
package = tmp_path / "package.json"
|
||||
package.write_text(body)
|
||||
with pytest.raises(ValueError):
|
||||
read_package(package)
|
||||
|
||||
|
||||
def test_oversize_package_and_symlinked_test_are_not_discovered(tmp_path):
|
||||
repo = Repository("example", tmp_path)
|
||||
webui = tmp_path / "webui"
|
||||
(webui / "tests").mkdir(parents=True)
|
||||
package = webui / "package.json"
|
||||
package.write_text(" " * (1024 * 1024 + 1))
|
||||
with pytest.raises(ValueError):
|
||||
read_package(package)
|
||||
package.write_text(
|
||||
json.dumps({"scripts": {"test:escape": "node tests/escape.mjs"}})
|
||||
)
|
||||
(webui / "tests/escape.mjs").symlink_to(tmp_path / "outside.mjs")
|
||||
(tmp_path / "outside.mjs").write_text("// fixture")
|
||||
assert module_ui_stages(repo, reason="fixture") == []
|
||||
|
||||
|
||||
def test_unparseable_and_sensitive_commands_do_not_leak_into_coverage(fixture):
|
||||
root, _, module, _ = fixture
|
||||
package = module.path / "webui/package.json"
|
||||
package.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"scripts": {
|
||||
"test:secret": "node tests/source.test.mjs --token unknown-private-value",
|
||||
"test:broken": "node 'unknown-other-secret",
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
result = build_coverage(root, "quick", [], False)
|
||||
encoded = json.dumps(result)
|
||||
assert (
|
||||
"unknown-private-value" not in encoded and "unknown-other-secret" not in encoded
|
||||
)
|
||||
assert "_argv" not in encoded and '"command"' not in encoded
|
||||
assert all(
|
||||
item["disposition"] == "unsupported"
|
||||
for item in rows(result, "govoplan-example").values()
|
||||
)
|
||||
|
||||
|
||||
def test_custom_check_coverage_redacts_separate_token_and_includes_unselected(tmp_path):
|
||||
project = tmp_path / "project.json"
|
||||
project.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"repositories": [{"name": "app", "path": "."}],
|
||||
"checks": [
|
||||
{
|
||||
"id": "one",
|
||||
"argv": ["check", "--token", "unknown-private-value"],
|
||||
},
|
||||
{"id": "two", "argv": ["true"]},
|
||||
],
|
||||
"profiles": {"quick": ["one"]},
|
||||
}
|
||||
)
|
||||
)
|
||||
result = build_coverage(tmp_path, "quick", [], False, project)
|
||||
assert "unknown-private-value" not in json.dumps(result)
|
||||
assert result["counts"]["planned"] == 1 and result["counts"]["excluded"] == 1
|
||||
|
||||
|
||||
def test_canonical_parser_does_not_credit_comments_heredocs_conditionals_or_chains(
|
||||
tmp_path,
|
||||
):
|
||||
meta, core = tmp_path / "govoplan", tmp_path / "govoplan-core"
|
||||
path = meta / "tools/checks/check-focused.sh"
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_text(
|
||||
'cd "$ROOT/webui"\n# "$NPM" run test:comment\n"$PYTHON" - <<\'PY\'\n"$NPM" run test:heredoc\nPY\nif false; then\n"$NPM" run test:conditional\nfi\n"$NPM" run test:compound && true\n"$NPM" run test:real\n'
|
||||
)
|
||||
result = canonical_invocations(tmp_path, meta, core)
|
||||
assert [item["name"] for item in result["npm"]] == ["test:real"]
|
||||
|
||||
|
||||
def test_real_canonical_gate_has_four_explicit_component_suites():
|
||||
result = canonical_invocations(ROOT.parent, ROOT, ROOT.parent / "govoplan-core")
|
||||
calls = [item for item in result["npm"] if item["name"] == "test:components"]
|
||||
assert len(calls) == 1
|
||||
assert calls[0]["args"] == [
|
||||
"layout-primitives",
|
||||
"page-layout",
|
||||
"data-grid-actions",
|
||||
"mail-components",
|
||||
]
|
||||
assert calls[0]["phase"] == "core-ui"
|
||||
for name, phase in (
|
||||
("test:module-permutations", "module-builds"),
|
||||
("test:conformance", "browser"),
|
||||
):
|
||||
matching = [item for item in result["npm"] if item["name"] == name]
|
||||
assert len(matching) == 1 and matching[0]["phase"] == phase
|
||||
|
||||
|
||||
def test_known_core_component_aliases_match_the_owned_runner_registry():
|
||||
runner = (
|
||||
ROOT.parent / "govoplan-core/webui/scripts/run-component-tests.mjs"
|
||||
).read_text()
|
||||
block = runner.split("export const componentSuites = Object.freeze({", 1)[1].split(
|
||||
"});", 1
|
||||
)[0]
|
||||
assert set(re.findall(r'^ "([a-z0-9-]+)":', block, re.M)) == set(
|
||||
CORE_COMPONENT_SUITES
|
||||
)
|
||||
Executable
+164
@@ -0,0 +1,164 @@
|
||||
"""Portable preflight inference uses fixtures only and never installs or starts tools."""
|
||||
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
|
||||
from govoplan_devkit import doctor
|
||||
from govoplan_devkit.workspace import Project, Repository
|
||||
|
||||
|
||||
def project(tmp_path, checks, *, tools=None, profiles=None):
|
||||
return Project(
|
||||
"Fixture",
|
||||
(Repository("app", tmp_path / "app"), Repository("other", tmp_path / "other")),
|
||||
{
|
||||
"checks": checks,
|
||||
"profiles": profiles
|
||||
if profiles is not None
|
||||
else {"quick": [item["id"] for item in checks]},
|
||||
"tools": tools or {},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def check(identity, executable, *, repos=None, deps=None):
|
||||
return {
|
||||
"id": identity,
|
||||
"argv": [executable, "--version"],
|
||||
"cwd": ".",
|
||||
"repos": repos or [],
|
||||
"deps": deps or [],
|
||||
}
|
||||
|
||||
|
||||
def diagnose(tmp_path, configured, *, profile=None, repos=None, versions=None):
|
||||
args = Namespace(
|
||||
workspace_root=tmp_path,
|
||||
project=tmp_path / "not-read.json",
|
||||
repo=repos or [],
|
||||
profile=profile,
|
||||
)
|
||||
tools = {"python": sys.executable, "node": "fixture-node", "npm": "fixture-npm"}
|
||||
version_map = versions or {
|
||||
sys.executable: "Python 3",
|
||||
"fixture-node": "unavailable",
|
||||
"fixture-npm": "unavailable",
|
||||
}
|
||||
with (
|
||||
patch.object(doctor, "load_project", return_value=configured),
|
||||
patch.object(doctor, "resolve_tools", return_value=tools),
|
||||
patch.object(
|
||||
doctor,
|
||||
"tool_version",
|
||||
side_effect=lambda executable, _env: version_map[executable],
|
||||
) as probe,
|
||||
patch.object(doctor, "inspect_repository", return_value={"errors": []}),
|
||||
):
|
||||
result = doctor.diagnose(args)
|
||||
return result, probe
|
||||
|
||||
|
||||
def test_python_only_portable_project_does_not_probe_or_block_on_unused_node(tmp_path):
|
||||
configured = project(tmp_path, [check("python-test", "{python}")])
|
||||
result, probe = diagnose(tmp_path, configured)
|
||||
assert result["_exit_code"] == 0
|
||||
assert result["required_tools"] == ["python"]
|
||||
assert probe.call_count == 1
|
||||
assert {
|
||||
item["id"] for item in result["checks"] if item["status"] == "not_required"
|
||||
} == {"node", "npm"}
|
||||
|
||||
|
||||
def test_selected_profile_excludes_other_profile_tool_requirements(tmp_path):
|
||||
configured = project(
|
||||
tmp_path,
|
||||
[check("py", "{python}"), check("js", "{npm}")],
|
||||
profiles={"quick": ["py"], "ui": ["js"]},
|
||||
)
|
||||
result, _ = diagnose(tmp_path, configured, profile="quick")
|
||||
assert result["required_tools"] == ["python"]
|
||||
result, _ = diagnose(tmp_path, configured)
|
||||
assert result["required_tools"] == ["node", "npm", "python"]
|
||||
assert result["_exit_code"] == 1
|
||||
|
||||
|
||||
def test_selected_repository_includes_dependency_tool_requirements(tmp_path):
|
||||
configured = project(
|
||||
tmp_path,
|
||||
[
|
||||
check("build", "{npm}", repos=["other"]),
|
||||
check("test", "{python}", repos=["app"], deps=["build"]),
|
||||
],
|
||||
)
|
||||
result, _ = diagnose(tmp_path, configured, repos=["app"])
|
||||
assert result["required_tools"] == ["node", "npm", "python"]
|
||||
|
||||
|
||||
def test_unselected_repository_does_not_require_its_tool(tmp_path):
|
||||
configured = project(
|
||||
tmp_path,
|
||||
[check("js", "{npm}", repos=["other"]), check("py", "{python}", repos=["app"])],
|
||||
)
|
||||
result, _ = diagnose(tmp_path, configured, repos=["app"])
|
||||
assert result["required_tools"] == ["python"]
|
||||
|
||||
|
||||
def test_explicit_tool_configuration_declares_indirect_script_dependency(tmp_path):
|
||||
configured = project(tmp_path, [check("shell", "sh")], tools={"npm": "fixture-npm"})
|
||||
result, _ = diagnose(tmp_path, configured)
|
||||
assert result["required_tools"] == ["node", "npm", "python"]
|
||||
assert result["_exit_code"] == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"executable,expected",
|
||||
[
|
||||
("/opt/node/bin/node", ["node", "python"]),
|
||||
("npm", ["node", "npm", "python"]),
|
||||
("npx", ["node", "npm", "python"]),
|
||||
],
|
||||
)
|
||||
def test_direct_tool_names_are_inferred(tmp_path, executable, expected):
|
||||
result, _ = diagnose(tmp_path, project(tmp_path, [check("test", executable)]))
|
||||
assert result["required_tools"] == expected
|
||||
|
||||
|
||||
def test_context_only_project_needs_no_node_and_makes_no_files(tmp_path):
|
||||
before = set(tmp_path.iterdir())
|
||||
result, _ = diagnose(tmp_path, project(tmp_path, [], profiles={}))
|
||||
assert result["required_tools"] == ["python"]
|
||||
assert set(tmp_path.iterdir()) == before
|
||||
|
||||
|
||||
def test_missing_profile_is_not_silently_widened(tmp_path):
|
||||
with pytest.raises(ValueError, match="does not declare profile"):
|
||||
diagnose(tmp_path, project(tmp_path, []), profile="ui")
|
||||
|
||||
|
||||
def test_native_govoplan_still_requires_all_three_tools(tmp_path):
|
||||
configured = project(tmp_path, [])
|
||||
args = Namespace(workspace_root=tmp_path, project=None, repo=[], profile=None)
|
||||
with (
|
||||
patch.object(doctor, "load_project", return_value=configured),
|
||||
patch.object(
|
||||
doctor,
|
||||
"resolve_tools",
|
||||
return_value={"python": sys.executable, "node": "node", "npm": "npm"},
|
||||
),
|
||||
patch.object(
|
||||
doctor,
|
||||
"tool_version",
|
||||
side_effect=lambda executable, _env: (
|
||||
"Python" if executable == sys.executable else "unavailable"
|
||||
),
|
||||
),
|
||||
patch.object(doctor, "inspect_repository", return_value={"errors": []}),
|
||||
):
|
||||
result = doctor.diagnose(args)
|
||||
assert result["required_tools"] == ["node", "npm", "python"]
|
||||
assert result["_exit_code"] == 1
|
||||
Executable
+378
@@ -0,0 +1,378 @@
|
||||
"""Environment probes read bounded stable files; all fixtures are local."""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
|
||||
from govoplan_devkit import environment
|
||||
from govoplan_devkit.common import digest
|
||||
from govoplan_devkit.workspace import Project, Repository
|
||||
|
||||
|
||||
def mutate_during_read(monkeypatch, callback):
|
||||
original = hashlib.sha256
|
||||
mutated = False
|
||||
|
||||
class MutatingHasher:
|
||||
def __init__(self):
|
||||
self.hasher = original()
|
||||
|
||||
def update(self, chunk):
|
||||
nonlocal mutated
|
||||
self.hasher.update(chunk)
|
||||
if not mutated:
|
||||
mutated = True
|
||||
callback()
|
||||
|
||||
def hexdigest(self):
|
||||
return self.hasher.hexdigest()
|
||||
|
||||
monkeypatch.setattr(environment.hashlib, "sha256", MutatingHasher)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("content", [b"", b"stable", b"x" * (1024 * 1024 + 3)])
|
||||
def test_file_hash_keeps_existing_digest_and_exact_size_boundary(tmp_path, content):
|
||||
path = tmp_path / "input"
|
||||
path.write_bytes(content)
|
||||
assert (
|
||||
environment._environment_file_hash(path, len(content))
|
||||
== hashlib.sha256(content).hexdigest()
|
||||
)
|
||||
|
||||
|
||||
def test_missing_optional_file_remains_optional_without_a_persistent_cache(tmp_path):
|
||||
path = tmp_path / "input"
|
||||
assert environment._environment_file_hash(path, 10) is None
|
||||
path.write_bytes(b"first")
|
||||
first = environment._environment_file_hash(path, 10)
|
||||
path.write_bytes(b"other")
|
||||
assert environment._environment_file_hash(path, 10) != first
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["fifo", "directory", "oversized", "dangling"])
|
||||
def test_present_unsafe_input_is_rejected_without_blocking(tmp_path, kind):
|
||||
path = tmp_path / "input"
|
||||
if kind == "fifo":
|
||||
os.mkfifo(path)
|
||||
elif kind == "directory":
|
||||
path.mkdir()
|
||||
elif kind == "oversized":
|
||||
path.write_bytes(b"too large")
|
||||
else:
|
||||
path.symlink_to(tmp_path / "missing")
|
||||
with pytest.raises(ValueError, match="Environment input"):
|
||||
environment._environment_file_hash(path, 2)
|
||||
|
||||
|
||||
def test_stable_venv_executable_symlink_keeps_original_path_and_identity(tmp_path):
|
||||
target = tmp_path / "real-python"
|
||||
target.write_bytes(b"fixture binary")
|
||||
executable = tmp_path / "venv" / "bin" / "python"
|
||||
executable.parent.mkdir(parents=True)
|
||||
executable.symlink_to(target)
|
||||
repo = tmp_path / "repo"
|
||||
metadata = repo / "node_modules" / ".package-lock.json"
|
||||
metadata.parent.mkdir(parents=True)
|
||||
metadata.write_bytes(b'{"fixture":true}')
|
||||
project = Project("Fixture", (Repository("repo", repo),), {})
|
||||
tools = {"python": str(executable)}
|
||||
env = {"PATH": "fixture", "PWD": "ignored"}
|
||||
distributions = b'[["fixture", "1"]]\n'
|
||||
with (
|
||||
patch.object(environment, "tool_version", return_value="fixture-version"),
|
||||
patch.object(
|
||||
environment,
|
||||
"require_capture",
|
||||
return_value=SimpleNamespace(returncode=0, stdout=distributions),
|
||||
) as capture,
|
||||
):
|
||||
actual = environment.environment_fingerprint(tmp_path, project, tools, env)
|
||||
assert actual == digest(
|
||||
{
|
||||
"environment": {"PATH": "fixture"},
|
||||
"tools": {
|
||||
"python": {
|
||||
"path": str(executable),
|
||||
"version": "fixture-version",
|
||||
"sha256": hashlib.sha256(target.read_bytes()).hexdigest(),
|
||||
}
|
||||
},
|
||||
"installed": {
|
||||
str(metadata): hashlib.sha256(metadata.read_bytes()).hexdigest(),
|
||||
"python_distributions": hashlib.sha256(distributions).hexdigest(),
|
||||
},
|
||||
}
|
||||
)
|
||||
assert capture.call_args.args[0][0] == str(executable)
|
||||
|
||||
|
||||
def test_symlinked_package_directory_is_allowed_when_stable(tmp_path):
|
||||
actual = tmp_path / "packages"
|
||||
actual.mkdir()
|
||||
(actual / ".package-lock.json").write_bytes(b"fixture")
|
||||
link = tmp_path / "node_modules"
|
||||
link.symlink_to(actual, target_is_directory=True)
|
||||
assert environment._environment_file_hash(link / ".package-lock.json", 10)
|
||||
|
||||
|
||||
def test_fifo_replacement_between_inspection_and_open_does_not_block(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
path = tmp_path / "input"
|
||||
path.write_bytes(b"fixture")
|
||||
real_open = os.open
|
||||
|
||||
def replace_before_open(target, flags):
|
||||
path.unlink()
|
||||
os.mkfifo(path)
|
||||
assert flags & os.O_NONBLOCK
|
||||
return real_open(target, flags)
|
||||
|
||||
monkeypatch.setattr(environment.os, "open", replace_before_open)
|
||||
with pytest.raises(ValueError, match="bounded regular"):
|
||||
environment._environment_file_hash(path, 10)
|
||||
|
||||
|
||||
def test_growth_between_inspection_and_open_cannot_bypass_size_bound(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
path = tmp_path / "input"
|
||||
path.write_bytes(b"a")
|
||||
real_open = os.open
|
||||
|
||||
def grow_before_open(target, flags):
|
||||
path.write_bytes(b"x" * 11)
|
||||
return real_open(target, flags)
|
||||
|
||||
monkeypatch.setattr(environment.os, "open", grow_before_open)
|
||||
with pytest.raises(ValueError, match="bounded regular"):
|
||||
environment._environment_file_hash(path, 10)
|
||||
|
||||
|
||||
def test_growth_during_read_cannot_bypass_size_bound(tmp_path, monkeypatch):
|
||||
path = tmp_path / "input"
|
||||
path.write_bytes(b"a")
|
||||
mutate_during_read(monkeypatch, lambda: path.write_bytes(b"x" * 11))
|
||||
with pytest.raises(ValueError, match="grew beyond"):
|
||||
environment._environment_file_hash(path, 10)
|
||||
|
||||
|
||||
def test_same_size_edit_with_restored_mtime_is_not_a_stable_identity(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
path = tmp_path / "input"
|
||||
path.write_bytes(b"first")
|
||||
metadata = path.stat()
|
||||
|
||||
def mutate():
|
||||
path.write_bytes(b"other")
|
||||
os.utime(path, ns=(metadata.st_atime_ns, metadata.st_mtime_ns))
|
||||
|
||||
mutate_during_read(monkeypatch, mutate)
|
||||
with pytest.raises(ValueError, match="changed during"):
|
||||
environment._environment_file_hash(path, 10)
|
||||
|
||||
|
||||
def test_replacement_with_identical_bytes_is_not_a_stable_identity(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
path = tmp_path / "input"
|
||||
path.write_bytes(b"fixture")
|
||||
replacement = tmp_path / "replacement"
|
||||
replacement.write_bytes(b"fixture")
|
||||
mutate_during_read(monkeypatch, lambda: replacement.replace(path))
|
||||
with pytest.raises(ValueError, match="changed during"):
|
||||
environment._environment_file_hash(path, 10)
|
||||
|
||||
|
||||
def test_symlink_retarget_with_identical_bytes_is_not_a_stable_identity(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
first, second, link = (tmp_path / name for name in ("first", "second", "python"))
|
||||
first.write_bytes(b"fixture")
|
||||
second.write_bytes(b"fixture")
|
||||
link.symlink_to(first)
|
||||
|
||||
def retarget():
|
||||
link.unlink()
|
||||
link.symlink_to(second)
|
||||
|
||||
mutate_during_read(monkeypatch, retarget)
|
||||
with pytest.raises(ValueError, match="changed during"):
|
||||
environment._environment_file_hash(link, 10)
|
||||
|
||||
|
||||
def test_source_disappearing_during_read_fails_closed(tmp_path, monkeypatch):
|
||||
path = tmp_path / "input"
|
||||
path.write_bytes(b"fixture")
|
||||
mutate_during_read(monkeypatch, path.unlink)
|
||||
with pytest.raises(ValueError, match="Environment input"):
|
||||
environment._environment_file_hash(path, 10)
|
||||
|
||||
|
||||
def test_parent_symlink_retarget_is_rejected_even_for_the_same_target_inode(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
first, second, link = (tmp_path / name for name in ("first", "second", "bin"))
|
||||
first.mkdir()
|
||||
second.mkdir()
|
||||
(first / "python").write_bytes(b"fixture")
|
||||
os.link(first / "python", second / "python")
|
||||
link.symlink_to(first, target_is_directory=True)
|
||||
|
||||
def retarget():
|
||||
link.unlink()
|
||||
link.symlink_to(second, target_is_directory=True)
|
||||
|
||||
mutate_during_read(monkeypatch, retarget)
|
||||
with pytest.raises(ValueError, match="changed during"):
|
||||
environment._environment_file_hash(link / "python", 10)
|
||||
|
||||
|
||||
def test_invalid_executable_is_rejected_before_any_version_process(tmp_path):
|
||||
executable = tmp_path / "python"
|
||||
os.mkfifo(executable)
|
||||
project = Project("Fixture", (), {})
|
||||
with patch.object(environment, "tool_version") as version:
|
||||
with pytest.raises(ValueError, match="bounded regular"):
|
||||
environment.environment_fingerprint(
|
||||
tmp_path, project, {"python": str(executable)}, {}
|
||||
)
|
||||
version.assert_not_called()
|
||||
|
||||
|
||||
def discovery_project(root, *, path="govoplan-backend"):
|
||||
repo = root / path
|
||||
(repo / "src").mkdir(parents=True)
|
||||
return Project(
|
||||
"GovOPlaN",
|
||||
(Repository("govoplan-backend", repo),),
|
||||
{"organization": "GovOPlaN"},
|
||||
)
|
||||
|
||||
|
||||
def test_native_shape_ignores_build_files_and_directory_timestamps(tmp_path):
|
||||
project = discovery_project(tmp_path)
|
||||
repo = project.repositories[0].path
|
||||
(repo / "webui").mkdir()
|
||||
before = environment._native_discovery_fingerprint(tmp_path, project)
|
||||
for directory in (repo, repo / "src", repo / "webui"):
|
||||
(directory / "temporary-build-output").write_text("changed")
|
||||
(directory / "temporary-build-directory").mkdir()
|
||||
assert environment._native_discovery_fingerprint(tmp_path, project) == before
|
||||
(repo / "webui" / "temporary-build-output").unlink()
|
||||
assert environment._native_discovery_fingerprint(tmp_path, project) == before
|
||||
|
||||
|
||||
def test_native_shape_detects_unknown_sibling_and_source_addition_removal(tmp_path):
|
||||
project = discovery_project(tmp_path)
|
||||
baseline = environment._native_discovery_fingerprint(tmp_path, project)
|
||||
sibling = tmp_path / "govoplan-unregistered"
|
||||
sibling.mkdir()
|
||||
empty = environment._native_discovery_fingerprint(tmp_path, project)
|
||||
assert empty != baseline
|
||||
for name in ("src", "webui"):
|
||||
(sibling / name).mkdir()
|
||||
assert environment._native_discovery_fingerprint(tmp_path, project) != empty
|
||||
(sibling / name).rmdir()
|
||||
assert environment._native_discovery_fingerprint(tmp_path, project) == empty
|
||||
sibling.rmdir()
|
||||
assert environment._native_discovery_fingerprint(tmp_path, project) == baseline
|
||||
|
||||
|
||||
@pytest.mark.parametrize("repo_path", ["govoplan-backend", "nonstandard-layout"])
|
||||
def test_native_shape_detects_registered_backend_gaining_webui(tmp_path, repo_path):
|
||||
project = discovery_project(tmp_path, path=repo_path)
|
||||
baseline = environment._native_discovery_fingerprint(tmp_path, project)
|
||||
webui = project.repositories[0].path / "webui"
|
||||
webui.mkdir()
|
||||
assert environment._native_discovery_fingerprint(tmp_path, project) != baseline
|
||||
webui.rmdir()
|
||||
assert environment._native_discovery_fingerprint(tmp_path, project) == baseline
|
||||
|
||||
|
||||
def test_native_shape_binds_registered_ownership_names(tmp_path):
|
||||
project = discovery_project(tmp_path, path="nonstandard-layout")
|
||||
renamed = Project(
|
||||
project.name,
|
||||
(Repository("govoplan-renamed", project.repositories[0].path),),
|
||||
project.config,
|
||||
)
|
||||
assert environment._native_discovery_fingerprint(
|
||||
tmp_path, project
|
||||
) != environment._native_discovery_fingerprint(tmp_path, renamed)
|
||||
|
||||
|
||||
def test_native_shape_detects_source_link_retarget_and_dangling_target(tmp_path):
|
||||
project = discovery_project(tmp_path)
|
||||
first, second = tmp_path / "first", tmp_path / "second"
|
||||
first.mkdir()
|
||||
second.mkdir()
|
||||
link = project.repositories[0].path / "webui"
|
||||
link.symlink_to(first, target_is_directory=True)
|
||||
baseline = environment._native_discovery_fingerprint(tmp_path, project)
|
||||
link.unlink()
|
||||
link.symlink_to(second, target_is_directory=True)
|
||||
changed = environment._native_discovery_fingerprint(tmp_path, project)
|
||||
assert changed != baseline
|
||||
second.rmdir()
|
||||
assert environment._native_discovery_fingerprint(tmp_path, project) != changed
|
||||
|
||||
|
||||
def test_native_shape_encodes_non_directory_source_entries(tmp_path):
|
||||
project = discovery_project(tmp_path)
|
||||
webui = project.repositories[0].path / "webui"
|
||||
baseline = environment._native_discovery_fingerprint(tmp_path, project)
|
||||
webui.write_text("not a directory")
|
||||
file_shape = environment._native_discovery_fingerprint(tmp_path, project)
|
||||
assert file_shape != baseline
|
||||
webui.unlink()
|
||||
webui.mkdir()
|
||||
assert environment._native_discovery_fingerprint(tmp_path, project) != file_shape
|
||||
|
||||
|
||||
def test_native_shape_audit_is_bounded_including_nonmatching_children(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
project = discovery_project(tmp_path)
|
||||
(tmp_path / "unrelated-one").mkdir()
|
||||
(tmp_path / "unrelated-two").mkdir()
|
||||
monkeypatch.setattr(environment, "MAX_DISCOVERY_CHILDREN", 2)
|
||||
with pytest.raises(ValueError, match="bounded ownership"):
|
||||
environment._native_discovery_fingerprint(tmp_path, project)
|
||||
|
||||
|
||||
def test_native_shape_resolution_loop_fails_closed(tmp_path):
|
||||
project = discovery_project(tmp_path)
|
||||
link = project.repositories[0].path / "webui"
|
||||
link.symlink_to(link)
|
||||
with pytest.raises(ValueError, match="discovery cannot be resolved"):
|
||||
environment._native_discovery_fingerprint(tmp_path, project)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("portable", [False, True])
|
||||
def test_only_native_environment_identity_binds_discovery_shape(tmp_path, portable):
|
||||
project = discovery_project(tmp_path)
|
||||
if portable:
|
||||
project.config["schema_version"] = 1
|
||||
executable = tmp_path / "fixture-python"
|
||||
executable.write_text("not executed")
|
||||
tools = {"python": str(executable)}
|
||||
with (
|
||||
patch.object(environment, "tool_version", return_value="fixture"),
|
||||
patch.object(
|
||||
environment,
|
||||
"require_capture",
|
||||
return_value=SimpleNamespace(returncode=0, stdout=b"[]"),
|
||||
),
|
||||
):
|
||||
baseline = environment.environment_fingerprint(tmp_path, project, tools, {})
|
||||
(tmp_path / "govoplan-new" / "src").mkdir(parents=True)
|
||||
changed = environment.environment_fingerprint(tmp_path, project, tools, {})
|
||||
assert (baseline == changed) is portable
|
||||
Executable
+601
@@ -0,0 +1,601 @@
|
||||
"""Incremental checkpoints use isolated local repositories, never product or remote state."""
|
||||
|
||||
from argparse import Namespace
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
|
||||
from govoplan_devkit import runner
|
||||
from govoplan_devkit.checkpoints import Checkpoints
|
||||
from govoplan_devkit.common import atomic_json, read_json, resource_lock, state_root
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "xdg"))
|
||||
root = tmp_path / "workspace"
|
||||
repos = {}
|
||||
for name in ("alpha", "beta"):
|
||||
repo = root / name
|
||||
repo.mkdir(parents=True)
|
||||
subprocess.run(["git", "init", "-q", str(repo)], check=True)
|
||||
(repo / "source.txt").write_text(name + " original\n")
|
||||
subprocess.run(["git", "-C", str(repo), "add", "source.txt"], check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(repo),
|
||||
"-c",
|
||||
"user.name=Fixture",
|
||||
"-c",
|
||||
"user.email=fixture@example.invalid",
|
||||
"commit",
|
||||
"-qm",
|
||||
"fixture",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
repos[name] = repo
|
||||
config = tmp_path / "project.json"
|
||||
config.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"name": "Incremental fixture",
|
||||
"repositories": [{"name": name, "path": name} for name in repos],
|
||||
"checks": [],
|
||||
"profiles": {},
|
||||
}
|
||||
)
|
||||
)
|
||||
traces = tmp_path / "traces"
|
||||
traces.mkdir()
|
||||
args = Namespace(
|
||||
workspace_root=root,
|
||||
project=config,
|
||||
state_dir=tmp_path / "state",
|
||||
dry_run=False,
|
||||
jobs=2,
|
||||
profile="quick",
|
||||
resume=None,
|
||||
repo=[],
|
||||
changed=False,
|
||||
)
|
||||
return args, repos, traces
|
||||
|
||||
|
||||
def stage(workspace, identity, repo="alpha", *, inputs=True, body="", **extra):
|
||||
_, repos, traces = workspace
|
||||
counter = traces / identity
|
||||
code = (
|
||||
"from pathlib import Path; "
|
||||
f"counter=Path({str(counter)!r}); "
|
||||
"counter.write_text(str(int(counter.read_text())+1 if counter.exists() else 1)); "
|
||||
f"print({identity!r},flush=True); " + body
|
||||
)
|
||||
result = {
|
||||
"id": identity,
|
||||
"title": identity,
|
||||
"argv": [sys.executable, "-c", code],
|
||||
"cwd": str(repos[repo]),
|
||||
"timeout_seconds": 5,
|
||||
**extra,
|
||||
}
|
||||
if inputs:
|
||||
result["inputs"] = {"repos": [repo]}
|
||||
return result
|
||||
|
||||
|
||||
def run(workspace, stages, *, environment="e" * 64):
|
||||
args, _, _ = workspace
|
||||
with patch.object(runner, "environment_fingerprint", return_value=environment):
|
||||
return runner.run_checks(args, stages)
|
||||
|
||||
|
||||
def count(workspace, identity):
|
||||
path = workspace[2] / identity
|
||||
return int(path.read_text()) if path.exists() else 0
|
||||
|
||||
|
||||
def stages_by_id(result):
|
||||
return {item["id"]: item for item in result["stages"]}
|
||||
|
||||
|
||||
def test_checkpoint_probes_and_durable_persistence_hold_stage_resource_lock(workspace):
|
||||
args, _, _ = workspace
|
||||
locks = state_root(args.workspace_root) / "resource-locks"
|
||||
original_identity, original_write = Checkpoints.identity, runner.atomic_json
|
||||
probes, persisted = [], []
|
||||
|
||||
def assert_held():
|
||||
with pytest.raises(RuntimeError, match="busy"):
|
||||
with resource_lock(locks, "checkpoint-fixture"):
|
||||
pass
|
||||
|
||||
def identity(self, selected):
|
||||
assert_held()
|
||||
probes.append(selected["id"])
|
||||
return original_identity(self, selected)
|
||||
|
||||
def write(path, payload):
|
||||
original_write(path, payload)
|
||||
if (
|
||||
payload.get("phase") == "checking"
|
||||
and payload["stages"][0].get("checkpoint_verified") is True
|
||||
and not persisted
|
||||
):
|
||||
assert_held()
|
||||
assert read_json(path)["stages"][0]["checkpoint_verified"] is True
|
||||
persisted.append(path)
|
||||
|
||||
with (
|
||||
patch.object(Checkpoints, "identity", identity),
|
||||
patch.object(runner, "atomic_json", side_effect=write),
|
||||
):
|
||||
result = run(
|
||||
workspace, [stage(workspace, "a", resources=["checkpoint-fixture"])]
|
||||
)
|
||||
assert result["status"] == "passed"
|
||||
assert len(probes) >= 2 and len(persisted) == 1
|
||||
|
||||
|
||||
def test_checkpoint_save_failure_never_releases_dependent_execution(workspace):
|
||||
args, _, _ = workspace
|
||||
original_write, original_command, original_wait = (
|
||||
runner.atomic_json,
|
||||
runner._execute_stage_command,
|
||||
runner.wait,
|
||||
)
|
||||
scheduler_waiting = threading.Event()
|
||||
checkpoint_stalled = threading.Event()
|
||||
scheduler_rechecked = threading.Event()
|
||||
release_failure = threading.Event()
|
||||
failed_write_paths, results, errors = [], [], []
|
||||
|
||||
def command(selected, *values, **kwargs):
|
||||
if selected["id"] == "producer":
|
||||
# Let the scheduler finish its initial running-state save and enter
|
||||
# its wait loop before the command publishes a passing checkpoint.
|
||||
assert scheduler_waiting.wait(timeout=5)
|
||||
return original_command(selected, *values, **kwargs)
|
||||
|
||||
def wait(*values, **kwargs):
|
||||
scheduler_waiting.set()
|
||||
result = original_wait(*values, **kwargs)
|
||||
if checkpoint_stalled.is_set():
|
||||
scheduler_rechecked.set()
|
||||
return result
|
||||
|
||||
def write(path, payload):
|
||||
producer = next(
|
||||
(item for item in payload.get("stages", []) if item["id"] == "producer"),
|
||||
None,
|
||||
)
|
||||
if (
|
||||
producer
|
||||
and producer.get("checkpoint_verified") is True
|
||||
and not failed_write_paths
|
||||
):
|
||||
failed_write_paths.append(path)
|
||||
checkpoint_stalled.set()
|
||||
assert release_failure.wait(timeout=5)
|
||||
raise OSError("fixture checkpoint persistence failed")
|
||||
return original_write(path, payload)
|
||||
|
||||
def execute():
|
||||
try:
|
||||
results.append(
|
||||
run(
|
||||
workspace,
|
||||
[
|
||||
stage(workspace, "producer"),
|
||||
stage(workspace, "dependent", "beta", deps=["producer"]),
|
||||
],
|
||||
)
|
||||
)
|
||||
except BaseException as exc:
|
||||
errors.append(exc)
|
||||
|
||||
worker = threading.Thread(target=execute)
|
||||
with (
|
||||
patch.object(runner, "atomic_json", side_effect=write),
|
||||
patch.object(runner, "_execute_stage_command", side_effect=command),
|
||||
patch.object(runner, "wait", side_effect=wait),
|
||||
):
|
||||
worker.start()
|
||||
try:
|
||||
assert checkpoint_stalled.wait(timeout=5)
|
||||
assert scheduler_rechecked.wait(timeout=5)
|
||||
# Keep persistence blocked across a scheduling turn: the in-memory
|
||||
# producer status must not grant authority to start a consumer.
|
||||
time.sleep(0.15)
|
||||
assert count(workspace, "dependent") == 0
|
||||
durable = read_json(failed_write_paths[0])
|
||||
assert (
|
||||
stages_by_id(durable)["producer"].get("checkpoint_verified") is not True
|
||||
)
|
||||
finally:
|
||||
release_failure.set()
|
||||
worker.join(timeout=8)
|
||||
assert not worker.is_alive() and not errors
|
||||
assert len(results) == 1 and results[0]["status"] == "failed"
|
||||
assert count(workspace, "producer") == 1 and count(workspace, "dependent") == 0
|
||||
by_id = stages_by_id(results[0])
|
||||
assert by_id["producer"]["status"] == "failed"
|
||||
assert by_id["producer"]["checkpoint_verified"] is False
|
||||
assert "fixture checkpoint persistence failed" in by_id["producer"]["error"]
|
||||
assert by_id["dependent"]["status"] == "skipped"
|
||||
persisted = runner.read_receipt(
|
||||
args.workspace_root, args.state_dir, results[0]["run_id"]
|
||||
)
|
||||
assert stages_by_id(persisted)["producer"]["checkpoint_verified"] is False
|
||||
|
||||
|
||||
def test_verified_checkpoint_reuses_unchanged_stage_after_other_repo_changes(workspace):
|
||||
args, repos, _ = workspace
|
||||
plan = [stage(workspace, "a"), stage(workspace, "b", "beta")]
|
||||
first = run(workspace, plan)
|
||||
old_bytes = Path(first["receipt_path"]).read_bytes()
|
||||
(repos["beta"] / "source.txt").write_text("beta changed\n")
|
||||
args.resume = first["run_id"]
|
||||
second = run(workspace, plan)
|
||||
by_id = stages_by_id(second)
|
||||
assert second["status"] == "passed" and second["snapshot_verified"] is True
|
||||
assert count(workspace, "a") == 1 and count(workspace, "b") == 2
|
||||
assert by_id["a"]["reused_from"] == first["run_id"]
|
||||
assert "reused_from" not in by_id["b"]
|
||||
assert by_id["a"]["checkpoint_verified"] is True
|
||||
assert by_id["a"]["checkpoint_version"] == 1
|
||||
assert isinstance(by_id["a"]["cache_key"], str) and by_id["a"]["cache_key"]
|
||||
assert second["source_fingerprint"] != first["source_fingerprint"]
|
||||
assert Path(first["receipt_path"]).read_bytes() == old_bytes
|
||||
|
||||
|
||||
def test_failed_post_execution_probe_preserves_actual_log_without_certifying_it(
|
||||
workspace,
|
||||
):
|
||||
from govoplan_devkit.checkpoints import Checkpoints
|
||||
|
||||
identity = Checkpoints.identity
|
||||
calls = 0
|
||||
|
||||
def probe(self, selected):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
if calls == 2:
|
||||
raise ValueError("fixture input became unreadable")
|
||||
return identity(self, selected)
|
||||
|
||||
with patch.object(Checkpoints, "identity", probe):
|
||||
result = run(workspace, [stage(workspace, "a")])
|
||||
selected = result["stages"][0]
|
||||
assert result["status"] == "stale"
|
||||
assert selected["status"] == "stale" and selected["exit_code"] == 0
|
||||
assert selected["checkpoint_verified"] is False
|
||||
assert Path(selected["log_path"]).read_text() == "a\n"
|
||||
assert "fixture input became unreadable" in selected["error"]
|
||||
|
||||
|
||||
def test_changed_and_new_commands_run_without_discarding_unrelated_checkpoint(
|
||||
workspace,
|
||||
):
|
||||
args, _, _ = workspace
|
||||
first = run(workspace, [stage(workspace, "a"), stage(workspace, "b", "beta")])
|
||||
args.resume = first["run_id"]
|
||||
second = run(
|
||||
workspace,
|
||||
[
|
||||
stage(workspace, "a"),
|
||||
stage(workspace, "b", "beta", body="print('changed command')"),
|
||||
stage(workspace, "new", "beta"),
|
||||
],
|
||||
)
|
||||
assert second["status"] == "passed"
|
||||
assert count(workspace, "a") == 1
|
||||
assert count(workspace, "b") == 2
|
||||
assert count(workspace, "new") == 1
|
||||
assert first["plan_fingerprint"] != second["plan_fingerprint"]
|
||||
|
||||
|
||||
def test_unrelated_profile_edit_does_not_change_existing_stage_execution_identity(
|
||||
workspace,
|
||||
):
|
||||
args, _, _ = workspace
|
||||
plan = [stage(workspace, "a")]
|
||||
first = run(workspace, plan)
|
||||
config = json.loads(args.project.read_text())
|
||||
config["checks"] = [
|
||||
{
|
||||
"id": "extra",
|
||||
"argv": [sys.executable, "-c", "print('extra')"],
|
||||
"cwd": "beta",
|
||||
"repos": ["beta"],
|
||||
}
|
||||
]
|
||||
config["profiles"] = {"backend": ["extra"]}
|
||||
args.project.write_text(json.dumps(config))
|
||||
args.resume = first["run_id"]
|
||||
second = run(workspace, plan)
|
||||
assert second["status"] == "passed"
|
||||
assert count(workspace, "a") == 1
|
||||
|
||||
|
||||
def test_added_dependency_edge_invalidates_consumer_even_when_both_repo_bytes_match(
|
||||
workspace,
|
||||
):
|
||||
args, _, _ = workspace
|
||||
a, b = stage(workspace, "a"), stage(workspace, "b", "beta")
|
||||
first = run(workspace, [a, b])
|
||||
args.resume = first["run_id"]
|
||||
second = run(workspace, [a, {**b, "deps": ["a"]}])
|
||||
assert second["status"] == "passed"
|
||||
assert count(workspace, "a") == 1 and count(workspace, "b") == 2
|
||||
|
||||
|
||||
def test_failed_run_reuses_successful_phase_but_retries_failed_phase(workspace):
|
||||
args, _, traces = workspace
|
||||
ready = traces / "ready"
|
||||
plan = [
|
||||
stage(workspace, "a"),
|
||||
stage(
|
||||
workspace,
|
||||
"b",
|
||||
"beta",
|
||||
after=["a"],
|
||||
body=f"raise SystemExit(0 if Path({str(ready)!r}).exists() else 3)",
|
||||
),
|
||||
]
|
||||
first = run(workspace, plan)
|
||||
assert first["status"] == "failed"
|
||||
assert stages_by_id(first)["a"]["checkpoint_verified"] is True
|
||||
ready.touch()
|
||||
args.resume = first["run_id"]
|
||||
second = run(workspace, plan)
|
||||
assert second["status"] == "passed"
|
||||
assert count(workspace, "a") == 1 and count(workspace, "b") == 2
|
||||
|
||||
|
||||
def test_changed_dependency_invalidates_transitive_consumers_but_not_independent_stage(
|
||||
workspace,
|
||||
):
|
||||
args, repos, _ = workspace
|
||||
plan = [
|
||||
stage(workspace, "a"),
|
||||
stage(workspace, "b", "beta", deps=["a"]),
|
||||
stage(workspace, "c", "beta", deps=["b"]),
|
||||
stage(workspace, "independent", "beta"),
|
||||
]
|
||||
first = run(workspace, plan)
|
||||
(repos["alpha"] / "source.txt").write_text("alpha changed\n")
|
||||
args.resume = first["run_id"]
|
||||
second = run(workspace, plan)
|
||||
assert second["status"] == "passed"
|
||||
assert {
|
||||
identity: count(workspace, identity)
|
||||
for identity in ("a", "b", "c", "independent")
|
||||
} == {"a": 2, "b": 2, "c": 2, "independent": 1}
|
||||
|
||||
|
||||
def test_order_only_predecessor_change_does_not_invalidate_independent_stage(workspace):
|
||||
args, repos, _ = workspace
|
||||
plan = [stage(workspace, "a"), stage(workspace, "b", "beta", after=["a"])]
|
||||
first = run(workspace, plan)
|
||||
(repos["alpha"] / "source.txt").write_text("alpha changed\n")
|
||||
args.resume = first["run_id"]
|
||||
second = run(workspace, plan)
|
||||
assert second["status"] == "passed"
|
||||
assert count(workspace, "a") == 2 and count(workspace, "b") == 1
|
||||
assert stages_by_id(second)["b"]["reused_from"] == first["run_id"]
|
||||
|
||||
|
||||
def test_order_only_failure_prevents_new_downstream_execution(workspace):
|
||||
plan = [
|
||||
stage(workspace, "a", body="raise SystemExit(8)"),
|
||||
stage(workspace, "b", "beta", after=["a"]),
|
||||
]
|
||||
result = run(workspace, plan)
|
||||
assert result["status"] == "failed"
|
||||
assert count(workspace, "a") == 1 and count(workspace, "b") == 0
|
||||
assert stages_by_id(result)["b"]["status"] == "skipped"
|
||||
|
||||
|
||||
def test_never_reused_stage_and_actual_consumers_rerun_but_order_only_stage_can_reuse(
|
||||
workspace,
|
||||
):
|
||||
args, _, _ = workspace
|
||||
plan = [
|
||||
stage(workspace, "producer", reuse="never"),
|
||||
stage(workspace, "consumer", "beta", deps=["producer"]),
|
||||
stage(workspace, "independent", "beta", after=["producer"]),
|
||||
]
|
||||
first = run(workspace, plan)
|
||||
args.resume = first["run_id"]
|
||||
second = run(workspace, plan)
|
||||
assert second["status"] == "passed"
|
||||
assert count(workspace, "producer") == 2
|
||||
assert count(workspace, "consumer") == 2
|
||||
assert count(workspace, "independent") == 1
|
||||
|
||||
|
||||
def test_unspecified_input_scope_stays_conservatively_workspace_wide(workspace):
|
||||
args, repos, _ = workspace
|
||||
plan = [stage(workspace, "broad", "beta", inputs=False)]
|
||||
first = run(workspace, plan)
|
||||
(repos["alpha"] / "source.txt").write_text("alpha changed\n")
|
||||
args.resume = first["run_id"]
|
||||
second = run(workspace, plan)
|
||||
assert second["status"] == "passed"
|
||||
assert count(workspace, "broad") == 2
|
||||
|
||||
|
||||
def test_global_environment_change_invalidates_all_scoped_checkpoints(workspace):
|
||||
args, _, _ = workspace
|
||||
plan = [stage(workspace, "a"), stage(workspace, "b", "beta")]
|
||||
first = run(workspace, plan)
|
||||
args.resume = first["run_id"]
|
||||
second = run(workspace, plan, environment="f" * 64)
|
||||
assert second["status"] == "passed"
|
||||
assert count(workspace, "a") == 2 and count(workspace, "b") == 2
|
||||
assert second["environment_fingerprint"] != first["environment_fingerprint"]
|
||||
|
||||
|
||||
def test_changed_declared_scope_prevents_same_id_reuse(workspace):
|
||||
args, _, _ = workspace
|
||||
selected = stage(workspace, "a")
|
||||
first = run(workspace, [selected])
|
||||
args.resume = first["run_id"]
|
||||
selected = {**selected, "inputs": {"repos": ["alpha", "beta"]}}
|
||||
second = run(workspace, [selected])
|
||||
assert second["status"] == "passed" and count(workspace, "a") == 2
|
||||
|
||||
|
||||
def test_input_mutation_during_passing_command_never_creates_reusable_checkpoint(
|
||||
workspace,
|
||||
):
|
||||
args, repos, _ = workspace
|
||||
plan = [
|
||||
stage(
|
||||
workspace,
|
||||
"mutates",
|
||||
body="Path('source.txt').write_text('mutated during stage\\n')",
|
||||
)
|
||||
]
|
||||
first = run(workspace, plan)
|
||||
assert first["status"] != "passed"
|
||||
assert stages_by_id(first)["mutates"].get("checkpoint_verified") is not True
|
||||
args.resume = first["run_id"]
|
||||
# The same command is now stable against the already-mutated current bytes;
|
||||
# its unverified first result still cannot be skipped.
|
||||
second = run(workspace, plan)
|
||||
assert count(workspace, "mutates") == 2
|
||||
assert second["status"] == "passed"
|
||||
assert (repos["alpha"] / "source.txt").read_text() == "mutated during stage\n"
|
||||
|
||||
|
||||
def test_later_restoration_of_bytes_cannot_turn_invalid_phase_into_overall_pass(
|
||||
workspace,
|
||||
):
|
||||
_, repos, _ = workspace
|
||||
source = repos["alpha"] / "source.txt"
|
||||
original = source.read_text()
|
||||
plan = [
|
||||
stage(
|
||||
workspace,
|
||||
"mutates",
|
||||
body="Path('source.txt').write_text('temporary change\\n')",
|
||||
),
|
||||
stage(
|
||||
workspace,
|
||||
"restores",
|
||||
"beta",
|
||||
after=["mutates"],
|
||||
body=f"Path({str(source)!r}).write_text({original!r})",
|
||||
),
|
||||
]
|
||||
result = run(workspace, plan)
|
||||
assert result["status"] != "passed"
|
||||
assert stages_by_id(result)["mutates"].get("checkpoint_verified") is not True
|
||||
|
||||
|
||||
def test_recovered_interruption_reuses_only_verified_durable_checkpoint(workspace):
|
||||
args, _, _ = workspace
|
||||
plan = [stage(workspace, "a"), stage(workspace, "b", "beta", after=["a"])]
|
||||
first = run(workspace, plan)
|
||||
receipt = read_json(Path(first["receipt_path"]))
|
||||
receipt.update(
|
||||
status="running", phase="checking", snapshot_verified=False, finished_at=None
|
||||
)
|
||||
unfinished = stages_by_id(receipt)["b"]
|
||||
unfinished.update(status="running", exit_code=None, checkpoint_verified=False)
|
||||
atomic_json(Path(first["receipt_path"]), runner._seal(receipt))
|
||||
parser = argparse.ArgumentParser()
|
||||
runner.register(parser.add_subparsers(dest="command", required=True))
|
||||
recovered = parser.parse_args(
|
||||
["recover", first["run_id"], "--apply", "--confirm-processes-stopped"],
|
||||
namespace=Namespace(**vars(args)),
|
||||
)
|
||||
recovery = recovered.handler(recovered)
|
||||
assert recovery["status"] == "interrupted"
|
||||
args.resume = first["run_id"]
|
||||
second = run(workspace, plan)
|
||||
assert second["status"] == "passed"
|
||||
assert count(workspace, "a") == 1 and count(workspace, "b") == 2
|
||||
assert stages_by_id(second)["a"]["reused_from"] == first["run_id"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["tampered", "missing"])
|
||||
def test_cached_log_tamper_or_loss_is_not_accepted_as_verified_evidence(
|
||||
workspace, change
|
||||
):
|
||||
args, _, _ = workspace
|
||||
plan = [stage(workspace, "a")]
|
||||
first = run(workspace, plan)
|
||||
path = Path(first["stages"][0]["log_path"])
|
||||
if change == "missing":
|
||||
path.unlink()
|
||||
else:
|
||||
path.write_text("tampered evidence\n")
|
||||
args.resume = first["run_id"]
|
||||
result = run(workspace, plan)
|
||||
assert result["status"] == "failed"
|
||||
assert (
|
||||
result["snapshot_verified"] is not True
|
||||
or result["stages"][0]["status"] == "failed"
|
||||
)
|
||||
assert result["stages"][0].get("checkpoint_verified") is not True
|
||||
assert "reused_from" not in result["stages"][0]
|
||||
assert "error" in result["stages"][0]
|
||||
assert count(workspace, "a") == 1
|
||||
|
||||
|
||||
def test_stale_run_donates_only_checkpoints_matching_final_current_inputs(workspace):
|
||||
args, repos, _ = workspace
|
||||
source = repos["alpha"] / "source.txt"
|
||||
plan = [
|
||||
stage(workspace, "a"),
|
||||
stage(
|
||||
workspace,
|
||||
"changes-alpha",
|
||||
"beta",
|
||||
after=["a"],
|
||||
body=f"Path({str(source)!r}).write_text('new alpha bytes\\n')",
|
||||
),
|
||||
]
|
||||
first = run(workspace, plan)
|
||||
assert first["status"] != "passed"
|
||||
args.resume = first["run_id"]
|
||||
second = run(workspace, plan)
|
||||
assert second["status"] == "passed"
|
||||
assert count(workspace, "a") == 2
|
||||
# This order-only phase did not consume alpha and its own scoped inputs are
|
||||
# still identical; it may retain its independently verified checkpoint.
|
||||
assert count(workspace, "changes-alpha") == 1
|
||||
|
||||
|
||||
def test_legacy_checkpointless_receipt_does_not_gain_incremental_authority(workspace):
|
||||
args, repos, _ = workspace
|
||||
plan = [stage(workspace, "a")]
|
||||
first = run(workspace, plan)
|
||||
receipt = read_json(Path(first["receipt_path"]))
|
||||
receipt.pop("fingerprint_version", None)
|
||||
for item in receipt["stages"]:
|
||||
for field in ("checkpoint_version", "checkpoint_verified", "cache_key"):
|
||||
item.pop(field, None)
|
||||
atomic_json(Path(first["receipt_path"]), runner._seal(receipt))
|
||||
(repos["beta"] / "source.txt").write_text("unrelated changed source\n")
|
||||
args.resume = first["run_id"]
|
||||
try:
|
||||
second = run(workspace, plan)
|
||||
except ValueError:
|
||||
return # Rejecting legacy incremental reuse is also safely fail-closed.
|
||||
assert second["status"] == "passed"
|
||||
assert count(workspace, "a") == 2
|
||||
Executable
+393
@@ -0,0 +1,393 @@
|
||||
"""Repository input identities use disposable Git fixtures, never remote effects."""
|
||||
|
||||
from copy import deepcopy
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools/devkit"))
|
||||
from govoplan_devkit import inputs
|
||||
from govoplan_devkit.inputs import InputSnapshotter, validate_input_declaration
|
||||
from govoplan_devkit.workspace import Project, Repository, source_fingerprint
|
||||
|
||||
|
||||
def git(repo, *arguments):
|
||||
return subprocess.run(
|
||||
["git", "-C", str(repo), *arguments], capture_output=True, check=True
|
||||
).stdout
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fixture(tmp_path, monkeypatch):
|
||||
repositories = []
|
||||
for name in ("alpha", "beta"):
|
||||
path = tmp_path / name
|
||||
path.mkdir()
|
||||
git(path, "init", "-q")
|
||||
(path / "source.txt").write_text("initial\n")
|
||||
git(path, "add", "source.txt")
|
||||
git(
|
||||
path,
|
||||
"-c",
|
||||
"user.name=Fixture",
|
||||
"-c",
|
||||
"user.email=fixture@example.invalid",
|
||||
"commit",
|
||||
"-qm",
|
||||
"fixture",
|
||||
)
|
||||
repositories.append(Repository(name, path, ("alias-" + name,)))
|
||||
project = Project(
|
||||
"Fixture",
|
||||
tuple(repositories),
|
||||
{"tools": {}, "profiles": {"quick": ["one"]}, "checks": []},
|
||||
)
|
||||
# Avoid incidental edits by other agents affecting these source-input tests.
|
||||
# The actual tool-source hashing implementation is checked separately.
|
||||
monkeypatch.setattr(
|
||||
InputSnapshotter, "_tooling_identity", lambda *_: "fixture-devkit-source"
|
||||
)
|
||||
return tmp_path, project, InputSnapshotter(project, workspace_root=tmp_path)
|
||||
|
||||
|
||||
def stage(identity="one", repos=None, **extra):
|
||||
return {
|
||||
"id": identity,
|
||||
"argv": ["true"],
|
||||
**({"inputs": {"repos": repos}} if repos is not None else {}),
|
||||
**extra,
|
||||
}
|
||||
|
||||
|
||||
def fingerprint(snapshot, name="one"):
|
||||
return snapshot["stages"][name]["fingerprint"]
|
||||
|
||||
|
||||
def test_undeclared_inputs_remain_whole_workspace(fixture):
|
||||
root, _, engine = fixture
|
||||
result = engine.snapshot([stage()])
|
||||
assert result["complete_workspace"] is True
|
||||
assert result["observed_scope"]["repos"] == ["alpha", "beta"]
|
||||
assert result["stages"]["one"]["scope"] == {
|
||||
"version": 1,
|
||||
"kind": "workspace",
|
||||
"declared": False,
|
||||
"repos": ["alpha", "beta"],
|
||||
}
|
||||
(root / "beta/source.txt").write_text("changed\n")
|
||||
assert fingerprint(engine.snapshot([stage()])) != fingerprint(result)
|
||||
|
||||
|
||||
def test_scoped_input_does_not_read_unrelated_repository_bytes(fixture, monkeypatch):
|
||||
root, _, engine = fixture
|
||||
original = inputs.os.open
|
||||
|
||||
def guarded(path, *args, **kwargs):
|
||||
if Path(path).is_relative_to(root / "beta"):
|
||||
raise AssertionError("Unrelated repository input was opened")
|
||||
return original(path, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(inputs.os, "open", guarded)
|
||||
first = engine.snapshot([stage(repos=["alpha"])])
|
||||
(root / "beta/source.txt").write_text("unrelated change")
|
||||
second = engine.snapshot([stage(repos=["alpha"])])
|
||||
assert fingerprint(first) == fingerprint(second)
|
||||
assert second["scan_stats"]["repositories"] == 1
|
||||
assert second["scan_stats"]["git_calls"] == 2
|
||||
assert second["complete_workspace"] is False
|
||||
|
||||
|
||||
def test_repository_union_scans_once_per_snapshot(fixture):
|
||||
_, _, engine = fixture
|
||||
result = engine.snapshot(
|
||||
[
|
||||
stage("one", ["alpha"]),
|
||||
stage("two", ["alpha"]),
|
||||
stage("three", ["beta", "alpha"]),
|
||||
]
|
||||
)
|
||||
assert result["scan_stats"]["repositories"] == 2
|
||||
assert result["scan_stats"]["git_calls"] == 4
|
||||
assert result["stages"]["three"]["scope"]["repos"] == ["alpha", "beta"]
|
||||
|
||||
|
||||
def test_session_rechecks_metadata_but_reuses_stable_file_content(fixture):
|
||||
_, _, engine = fixture
|
||||
plan = [stage(repos=["alpha"])]
|
||||
before = engine.snapshot(plan)
|
||||
after = engine.snapshot(plan)
|
||||
assert fingerprint(before) == fingerprint(after)
|
||||
assert before["scan_stats"]["bytes"] == len("initial\n")
|
||||
assert after["scan_stats"]["bytes"] == 0
|
||||
assert after["scan_stats"]["cache_hits"] == 1
|
||||
assert after["scan_stats"]["git_calls"] == 2
|
||||
|
||||
|
||||
def test_changed_bytes_with_restored_mtime_are_not_reused(fixture):
|
||||
root, _, engine = fixture
|
||||
plan = [stage(repos=["alpha"])]
|
||||
before = engine.snapshot(plan)
|
||||
path = root / "alpha/source.txt"
|
||||
metadata = path.stat()
|
||||
path.write_text("changed\n")
|
||||
os.utime(path, ns=(metadata.st_atime_ns, metadata.st_mtime_ns))
|
||||
after = engine.snapshot(plan)
|
||||
assert fingerprint(after) != fingerprint(before)
|
||||
assert after["scan_stats"]["bytes"] == len("changed\n")
|
||||
|
||||
|
||||
def test_inode_replacement_with_same_size_and_mtime_is_not_reused(fixture):
|
||||
root, _, engine = fixture
|
||||
plan = [stage(repos=["alpha"])]
|
||||
before = engine.snapshot(plan)
|
||||
path = root / "alpha/source.txt"
|
||||
metadata = path.stat()
|
||||
replacement = root / "replacement.txt"
|
||||
replacement.write_text("changed\n")
|
||||
os.utime(replacement, ns=(metadata.st_atime_ns, metadata.st_mtime_ns))
|
||||
replacement.replace(path)
|
||||
after = engine.snapshot(plan)
|
||||
assert fingerprint(before) != fingerprint(after)
|
||||
assert after["scan_stats"]["cache_hits"] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("flag", ["assume-unchanged", "skip-worktree"])
|
||||
def test_hidden_index_flags_do_not_hide_worktree_changes(fixture, flag):
|
||||
root, _, engine = fixture
|
||||
git(root / "alpha", "update-index", "--" + flag, "source.txt")
|
||||
before = engine.source_snapshot(["alpha"])
|
||||
(root / "alpha/source.txt").write_text("hidden change\n")
|
||||
after = engine.source_snapshot(["alpha"])
|
||||
assert before["observed_source_fingerprint"] != after["observed_source_fingerprint"]
|
||||
|
||||
|
||||
def test_index_head_and_new_deleted_files_are_bound(fixture):
|
||||
root, _, engine = fixture
|
||||
repo = root / "alpha"
|
||||
identities = []
|
||||
|
||||
def record():
|
||||
identities.append(
|
||||
engine.source_snapshot(["alpha"])["observed_source_fingerprint"]
|
||||
)
|
||||
|
||||
record()
|
||||
(repo / "source.txt").write_text("changed\n")
|
||||
record()
|
||||
git(repo, "add", "source.txt")
|
||||
record()
|
||||
git(
|
||||
repo,
|
||||
"-c",
|
||||
"user.name=Fixture",
|
||||
"-c",
|
||||
"user.email=fixture@example.invalid",
|
||||
"commit",
|
||||
"-qm",
|
||||
"change",
|
||||
)
|
||||
record()
|
||||
(repo / "new.txt").write_text("new")
|
||||
record()
|
||||
(repo / "source.txt").unlink()
|
||||
record()
|
||||
assert len(set(identities)) == len(identities)
|
||||
|
||||
|
||||
def test_missing_registered_repository_becoming_present_invalidates(fixture):
|
||||
root, project, _ = fixture
|
||||
extra = Repository("missing", root / "missing")
|
||||
extended = Project("Fixture", (*project.repositories, extra), project.config)
|
||||
engine = InputSnapshotter(extended, workspace_root=root)
|
||||
before = engine.source_snapshot(["missing"])
|
||||
extra.path.mkdir()
|
||||
git(extra.path, "init", "-q")
|
||||
after = engine.source_snapshot(["missing"])
|
||||
assert before["observed_source_fingerprint"] != after["observed_source_fingerprint"]
|
||||
|
||||
|
||||
def test_unrelated_plan_config_edits_do_not_invalidate_scoped_stage(fixture):
|
||||
root, project, engine = fixture
|
||||
plan = [stage(repos=["alpha"])]
|
||||
before = engine.snapshot(plan)
|
||||
updated = deepcopy(project.config)
|
||||
updated["profiles"]["full"] = ["unrelated"]
|
||||
updated["checks"].append({"id": "unrelated", "argv": ["false"]})
|
||||
other = InputSnapshotter(
|
||||
Project("Changed label", project.repositories, updated), workspace_root=root
|
||||
)
|
||||
assert fingerprint(other.snapshot(plan)) == fingerprint(before)
|
||||
|
||||
|
||||
def test_scope_command_tools_and_tooling_change_invalidate(fixture):
|
||||
root, project, engine = fixture
|
||||
first = engine.snapshot([stage(repos=["alpha"])])
|
||||
assert fingerprint(engine.snapshot([stage(repos=["beta"])])) != fingerprint(first)
|
||||
assert fingerprint(
|
||||
engine.snapshot([stage(repos=["alpha"], argv=["false"])])
|
||||
) != fingerprint(first)
|
||||
assert fingerprint(
|
||||
engine.snapshot([stage(repos=["alpha"])], tooling_fingerprint="new-env")
|
||||
) != fingerprint(first)
|
||||
changed = deepcopy(project.config)
|
||||
changed["tools"] = {"node": "/another/node"}
|
||||
other = InputSnapshotter(
|
||||
Project("Fixture", project.repositories, changed), workspace_root=root
|
||||
)
|
||||
assert fingerprint(other.snapshot([stage(repos=["alpha"])])) != fingerprint(first)
|
||||
|
||||
|
||||
def test_source_only_attestation_is_independent_of_plan_and_tooling(fixture):
|
||||
_, _, engine = fixture
|
||||
one = engine.snapshot([stage(repos=["alpha"])], tooling_fingerprint="env-one")
|
||||
two = engine.snapshot(
|
||||
[stage("different", ["alpha"], argv=["false"])], tooling_fingerprint="env-two"
|
||||
)
|
||||
source = engine.source_snapshot(["alpha"])
|
||||
assert (
|
||||
one["observed_source_fingerprint"]
|
||||
== two["observed_source_fingerprint"]
|
||||
== source["observed_source_fingerprint"]
|
||||
)
|
||||
assert (
|
||||
one["stages"]["one"]["source_fingerprint"]
|
||||
== source["observed_source_fingerprint"]
|
||||
)
|
||||
assert source["fingerprint_version"] == inputs.FINGERPRINT_VERSION
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
[
|
||||
None,
|
||||
{},
|
||||
{"paths": ["src/**"]},
|
||||
{"repos": []},
|
||||
{"repos": "alpha"},
|
||||
{"repos": ["unknown"]},
|
||||
{"repos": ["alias-alpha"]},
|
||||
{"repos": ["alpha", "alpha"]},
|
||||
{"repos": [""]},
|
||||
{"repos": ["alpha"], "extra": True},
|
||||
],
|
||||
)
|
||||
def test_synthetic_stage_scopes_fail_closed(fixture, value):
|
||||
_, _, engine = fixture
|
||||
with pytest.raises(ValueError):
|
||||
engine.snapshot([stage(inputs=value)])
|
||||
|
||||
|
||||
def test_duplicate_stage_ids_and_escaped_repository_paths_fail(fixture):
|
||||
root, project, engine = fixture
|
||||
with pytest.raises(ValueError, match="Duplicate"):
|
||||
engine.snapshot([stage(), stage()])
|
||||
bad = Project("Bad", (Repository("outside", root.parent),), project.config)
|
||||
with pytest.raises(ValueError, match="escapes"):
|
||||
InputSnapshotter(bad, workspace_root=root)
|
||||
|
||||
|
||||
def test_declaration_validation_is_pure_before_dry_run(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
inputs,
|
||||
"git_bytes",
|
||||
lambda *_a, **_k: pytest.fail("Planning validation ran Git"),
|
||||
)
|
||||
assert validate_input_declaration(
|
||||
{"repos": ["beta", "alpha"]}, {"alpha", "beta"}
|
||||
) == {"repos": ["alpha", "beta"]}
|
||||
with pytest.raises(ValueError):
|
||||
validate_input_declaration({"repos": ["unknown"]}, {"alpha", "beta"})
|
||||
|
||||
|
||||
def test_runtime_records_are_not_hashed_as_execution_plans(fixture):
|
||||
_, _, engine = fixture
|
||||
with pytest.raises(ValueError, match="freshly planned"):
|
||||
engine.snapshot([stage(repos=["alpha"], status="passed")])
|
||||
|
||||
|
||||
def test_membership_count_is_bounded_before_entry_hashing(fixture, monkeypatch):
|
||||
root, _, engine = fixture
|
||||
(root / "alpha/new.txt").write_text("extra")
|
||||
monkeypatch.setattr(inputs, "MAX_REPOSITORY_ENTRIES", 1)
|
||||
with pytest.raises(ValueError, match="entry count"):
|
||||
engine.source_snapshot(["alpha"])
|
||||
|
||||
|
||||
def test_symlink_to_ignored_file_inside_repository_binds_target(fixture):
|
||||
root, _, engine = fixture
|
||||
repo = root / "alpha"
|
||||
(repo / ".gitignore").write_text("ignored.txt\n")
|
||||
(repo / "ignored.txt").write_text("first")
|
||||
(repo / "linked.txt").symlink_to("ignored.txt")
|
||||
before = engine.source_snapshot(["alpha"])
|
||||
(repo / "ignored.txt").write_text("other")
|
||||
assert (
|
||||
engine.source_snapshot(["alpha"])["observed_source_fingerprint"]
|
||||
!= before["observed_source_fingerprint"]
|
||||
)
|
||||
|
||||
|
||||
def test_dangling_symlink_target_creation_is_observed(fixture):
|
||||
root, _, engine = fixture
|
||||
repo = root / "alpha"
|
||||
(repo / "linked.txt").symlink_to("future.txt")
|
||||
before = engine.source_snapshot(["alpha"])
|
||||
(repo / "future.txt").write_text("created")
|
||||
assert (
|
||||
engine.source_snapshot(["alpha"])["observed_source_fingerprint"]
|
||||
!= before["observed_source_fingerprint"]
|
||||
)
|
||||
|
||||
|
||||
def test_cross_repository_and_directory_symlinks_are_not_silently_reused(fixture):
|
||||
root, _, engine = fixture
|
||||
link = root / "alpha/linked.txt"
|
||||
link.symlink_to(root / "beta/source.txt")
|
||||
with pytest.raises(ValueError, match="escapes"):
|
||||
engine.source_snapshot(["alpha"])
|
||||
link.unlink()
|
||||
link.symlink_to(".")
|
||||
with pytest.raises(ValueError, match="regular file"):
|
||||
engine.source_snapshot(["alpha"])
|
||||
|
||||
|
||||
def test_file_change_during_hash_is_rejected(fixture, monkeypatch):
|
||||
root, _, engine = fixture
|
||||
path = root / "alpha/source.txt"
|
||||
original = inputs.os.fstat
|
||||
changed = False
|
||||
|
||||
def mutate(descriptor):
|
||||
nonlocal changed
|
||||
metadata = original(descriptor)
|
||||
if not changed:
|
||||
changed = True
|
||||
path.write_text("changed during read")
|
||||
return metadata
|
||||
|
||||
monkeypatch.setattr(inputs.os, "fstat", mutate)
|
||||
with pytest.raises(ValueError, match="changed"):
|
||||
engine._file_hash(path, inputs._stats())
|
||||
|
||||
|
||||
def test_legacy_whole_project_api_remains_independent(fixture):
|
||||
_, project, engine = fixture
|
||||
legacy = source_fingerprint(project)
|
||||
engine.snapshot([stage(repos=["alpha"])])
|
||||
assert source_fingerprint(project) == legacy
|
||||
|
||||
|
||||
def test_real_tooling_inventory_is_bound_and_memoized(tmp_path):
|
||||
repo = Repository("example", tmp_path)
|
||||
project = Project("Fixture", (repo,), {"tools": {}})
|
||||
engine = InputSnapshotter(project, workspace_root=tmp_path)
|
||||
before_stats, after_stats = inputs._stats(), inputs._stats()
|
||||
before = engine._tooling_identity(before_stats)
|
||||
after = engine._tooling_identity(after_stats)
|
||||
assert before == after
|
||||
assert before_stats["tooling_files"] > 15 and before_stats["tooling_bytes"] > 0
|
||||
assert after_stats["tooling_files"] == after_stats["tooling_cache_hits"]
|
||||
assert after_stats["tooling_bytes"] == 0
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user