Inventory module recovery ledger adoption
Dependency Audit / dependency-audit (push) Successful in 1m36s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 9m54s

This commit is contained in:
2026-08-03 03:02:53 +02:00
parent e8f7e2c194
commit 9370f501a0
4 changed files with 292 additions and 0 deletions
+4
View File
@@ -44,6 +44,10 @@ least one check. The ledger verifies its hash chain before evidence is trusted.
This is a platform contract, not an assertion that every existing module This is a platform contract, not an assertion that every existing module
operation has adopted it. Module operations with external or multi-resource operation has adopted it. Module operations with external or multi-resource
effects must be migrated to the ledger before claiming these guarantees. effects must be migrated to the ledger before claiming these guarantees.
The owning-module inventory and adoption state are maintained in
[Recovery Ledger Adoption](RECOVERY_LEDGER_ADOPTION.md); CI validates the
machine-readable inventory so newly identified boundaries cannot disappear from
the backlog silently.
## Deployment Journal ## Deployment Journal
+56
View File
@@ -0,0 +1,56 @@
# Recovery Ledger Adoption
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).
## Classification Rules
- Use `atomic` only when every mutation commits in one database transaction and
no external effect occurs.
- Use `compensation` when every completed effect has a bounded, verifiable
inverse action. A best-effort delete is not proof of compensation.
- Use `snapshot_restore` only with fresh, signed backup evidence that covers all
affected state services at one recovery point.
- Use `forward_recovery` for provider acceptance, queue publication, cursor
advancement, and other effects that may be resumable but cannot safely be
undone.
- Use `irreversible` for approved purge or destruction where no automated
recovery is claimed.
One feature may cross more than one boundary. Module installation is
compensatable before schema migration, forward-only after migration starts, and
snapshot-restorable for an approved destructive retirement. Mail submission is
forward recovery because losing the response after provider acceptance must not
cause an automatic resend.
## Adoption Order
1. Campaign build is the reference implementation for a database plus object
storage operation. Its operation reserves a build-specific object prefix,
persists request and precondition evidence before writes, records the final
object manifest, and verifies database/object state before success.
2. Campaign delivery and Mail provider effects adopt outcome-unknown semantics
without weakening their existing provider-specific idempotency records.
3. Files applies the same contract to uploads, purge, integrity reconciliation,
and writable connector synchronization.
4. Connectors, Dataflow, and Workflow Engine consume the contract at their
registry/capability boundaries so optional providers remain optional.
5. Core module lifecycle uses the ledger in addition to, not instead of, signed
deployment and backup evidence.
Every fenced operation uses a process incarnation and distributed lease. A
stale process cannot append a checkpoint or report success. An expired operation
is claimed for recovery through an explicit takeover that preserves the prior
fence in the checkpoint chain; it is never resumed as a normal retry.
## Operator Contract
Ops lists non-terminal and manual-intervention operations. Operators must verify
the checkpoint chain before trusting evidence, distinguish `outcome_unknown`
from rejection, and use the owning module's documented reconciliation action.
No evidence payload may contain credentials or resolved secrets.
The parent adoption issue remains open until all inventory rows are adopted and
the module matrix proves crash, retry, stale-fence, tamper, and optional-module
behavior for each consequential path.
+177
View File
@@ -0,0 +1,177 @@
{
"schema_version": 1,
"parent_issue": "https://git.add-ideas.de/GovOPlaN/govoplan/issues/36",
"operations": [
{
"id": "campaign.build.publish-artifacts",
"repository": "govoplan-campaign",
"resources": ["postgresql", "object-storage", "templates-capability", "files-capability"],
"mode": "compensation",
"fenced": true,
"adoption": "reference-implementation",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/92"
},
{
"id": "campaign.delivery.external-channels",
"repository": "govoplan-campaign",
"resources": ["postgresql", "queue", "smtp", "imap", "postbox", "print-provider"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/92"
},
{
"id": "campaign.retention.generated-artifacts",
"repository": "govoplan-campaign",
"resources": ["postgresql", "object-storage"],
"mode": "compensation",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/92"
},
{
"id": "files.upload.finalize",
"repository": "govoplan-files",
"resources": ["postgresql", "object-storage", "filesystem-staging"],
"mode": "compensation",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/41"
},
{
"id": "files.retention.purge",
"repository": "govoplan-files",
"resources": ["postgresql", "object-storage", "encryption-key-custody"],
"mode": "irreversible",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/41"
},
{
"id": "files.integrity.reconcile",
"repository": "govoplan-files",
"resources": ["postgresql", "object-storage"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/41"
},
{
"id": "files.connector.write-sync",
"repository": "govoplan-files",
"resources": ["postgresql", "object-storage", "external-connector"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/41"
},
{
"id": "mail.outbox.smtp-submit",
"repository": "govoplan-mail",
"resources": ["postgresql", "queue", "smtp"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/19"
},
{
"id": "mail.mailbox.imap-mutate",
"repository": "govoplan-mail",
"resources": ["postgresql", "imap"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/19"
},
{
"id": "mail.mailbox.sync-cursor",
"repository": "govoplan-mail",
"resources": ["postgresql", "imap", "object-storage"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/19"
},
{
"id": "connectors.sync.read-snapshot",
"repository": "govoplan-connectors",
"resources": ["postgresql", "external-provider"],
"mode": "atomic",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/15"
},
{
"id": "connectors.sync.external-mutation",
"repository": "govoplan-connectors",
"resources": ["postgresql", "queue", "external-provider"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/15"
},
{
"id": "dataflow.run.database-only",
"repository": "govoplan-dataflow",
"resources": ["postgresql"],
"mode": "atomic",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/19"
},
{
"id": "dataflow.run.publish-output",
"repository": "govoplan-dataflow",
"resources": ["postgresql", "queue", "object-storage", "external-sink"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/19"
},
{
"id": "workflow-engine.instance.state-transition",
"repository": "govoplan-workflow-engine",
"resources": ["postgresql"],
"mode": "atomic",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine/issues/1"
},
{
"id": "workflow-engine.activity.external-effect",
"repository": "govoplan-workflow-engine",
"resources": ["postgresql", "queue", "module-capability", "external-provider"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine/issues/1"
},
{
"id": "core.module-lifecycle.pre-migration",
"repository": "govoplan-core",
"resources": ["postgresql", "package-environment", "webui-bundle", "filesystem"],
"mode": "compensation",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/281"
},
{
"id": "core.module-lifecycle.post-migration",
"repository": "govoplan-core",
"resources": ["postgresql", "package-environment", "webui-bundle", "runtime-nodes"],
"mode": "forward_recovery",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/281"
},
{
"id": "core.module-retirement.destroy-data",
"repository": "govoplan-core",
"resources": ["postgresql", "object-storage", "package-environment"],
"mode": "snapshot_restore",
"fenced": true,
"adoption": "planned",
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/281"
}
]
}
@@ -0,0 +1,55 @@
from __future__ import annotations
import json
from pathlib import Path
from urllib.parse import urlparse
ROOT = Path(__file__).resolve().parents[1]
INVENTORY = ROOT / "docs" / "recovery-operation-inventory.json"
MODES = {
"atomic",
"compensation",
"snapshot_restore",
"forward_recovery",
"irreversible",
}
ADOPTION_STATES = {"planned", "reference-implementation", "adopted"}
REQUIRED_PREFIXES = {
"campaign.",
"files.",
"mail.",
"connectors.",
"dataflow.",
"workflow-engine.",
"core.module-lifecycle.",
}
def test_recovery_operation_inventory_is_complete_and_actionable() -> None:
payload = json.loads(INVENTORY.read_text(encoding="utf-8"))
assert payload["schema_version"] == 1
operations = payload["operations"]
ids = [item["id"] for item in operations]
assert len(ids) == len(set(ids))
assert all(any(item.startswith(prefix) for item in ids) for prefix in REQUIRED_PREFIXES)
for item in operations:
assert item["mode"] in MODES
assert item["adoption"] in ADOPTION_STATES
assert item["repository"].startswith("govoplan-")
assert item["resources"]
assert item["fenced"] is True
issue = urlparse(item["issue"])
assert issue.scheme == "https"
assert issue.netloc == "git.add-ideas.de"
assert issue.path.startswith(f"/GovOPlaN/{item['repository']}/issues/")
def test_non_atomic_operations_do_not_claim_plain_database_rollback() -> None:
operations = json.loads(INVENTORY.read_text(encoding="utf-8"))["operations"]
for item in operations:
if item["mode"] == "atomic":
assert item["resources"] == ["postgresql"] or item["id"] == (
"connectors.sync.read-snapshot"
)