autodoc documents whatever __all__ lists, an imported name included, and apidoc also writes a page for the submodule the name comes from. An annotation naming such a type resolves to two targets, which -W turns fatal: the documentation catalogs could not be extracted at all. Only a type reaches that failure, because only a type appears in the annotations sphinx resolves. mesh re-exported Mesh, MeshMember and MeshSpec; libretranslate re-exported LibreTranslate and Outcome; the development inventory package still listed PlanEntry beside the DevInventorySpec that7fee974c7fremoved for this reason. Each now uses the redundant alias the repository already established, which keeps the name importable without listing it. Label in variant/axes.py had no importer and is gone. Re-exporting a function stays allowed: the development inventory package re-exports its helpers so unit tests can patch them under the package path, and no function has ever produced a second target. A lint pins the class, because the docstring7fee974c7frelied on did not hold a third time. It reads the trees apidoc skips from APIDOC_EXCLUDED, which commands.py now names once instead of repeating the three paths inside the argv. locale/ is the extract this unblocks: 26144 messages over 183 catalogs. 987761 translations carry over, 8893 lose a msgid that no longer exists, and 1136 keep their msgid but come back empty for the next i18n-translate to refill. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Development matrix deploy
Iterates per-role variants against a development inventory. See below for
how the include set is resolved and where disable= prunes it.
Include resolution, disable, and orphan pruning
How a primary app list becomes the per-round inventory include set, and
where disable= prunes the transitive closure.
Deploy entry points
flowchart TD
MK["make compose-deploy apps=… disable=…"] --> MAIN["scripts/.../deploy/main.sh"]
MAIN -->|mode=initialize / reinstall| INITSEL["apps/initialize/selection.sh"]
MAIN -->|mode=update| UPDSEL["apps/update/selection.sh"]
INITSEL --> INIT["cli … deploy development init --apps … (disable via env)"]
INIT --> DEPLOY["cli … deploy development deploy --apps …"]
UPDSEL --> UPDEXEC["container/update/selection.sh (existing inventory)"]
INIT -.->|builds & prunes inventory| INV[("per-round inventory dir(s)")]
UPDEXEC -.->|reuses inventory, disable = ansible extra-var only| INV
initialize/reinstallrebuild the inventory throughinit.py; this is the only path that prunes the include set.deployre-derives the same pruned set for its deploy ids (see below).updatereuses the existing inventory;disablereaches ansible as an extra-var that renders services disabled, but does not touch the include.
init.py: plan then prune
flowchart TD
A["--apps → primary_apps"] --> B{"disable set?"}
B -->|yes| C["parse_services_disabled → find_provider_roles<br/>→ disabled_app_ids"]
C --> D["primary_apps −= disabled_app_ids"]
B -->|no| E
D --> E["plan_dev_inventory_matrix(primary_apps)"]
E --> F["per round: include_R = variant-merged closure"]
F --> G{"disable set?"}
G -->|no| H["round_include = include_R"]
G -->|yes| I["prune_orphans_after_disable(include_R,<br/>primary_apps, disabled_app_ids, round_overrides)"]
I --> J["(kept, pruned)"]
J --> K["round_include = kept<br/>log pruned"]
H --> L["build_dev_inventory(round_include)"]
K --> L
deploy/cli.py: re-derive the same include
init.py writes the inventory; deploy/cli.py decides which application
ids to hand to cli.administration.deploy.dedicated. Both derive their
per-round set from the same plan_dev_inventory_matrix closure and MUST
apply the same prune_orphans_after_disable cut with the same
round-merged services map. validate_application_ids rejects any id that
is absent from the inventory, so a deploy-side set wider than the
init-side set aborts the run before the first role deploys.
flowchart TD
PLAN["plan_dev_inventory_matrix(primary_apps)"] --> IR["include_R (per round)"]
IR --> INITP{"disable set?"}
INITP -->|yes| IP["prune_orphans_after_disable"]
INITP -->|no| IK["include_R"]
IP --> IW["build_dev_inventory → inventory dir"]
IK --> IW
IR --> DEPP{"disable set?"}
DEPP -->|yes| DP["prune_orphans_after_disable<br/>(same round_overrides)"]
DEPP -->|no| DK["include_R"]
DP --> DD["round_deploy_ids"]
DK --> DD
DD --> VAL{"validate_application_ids<br/>vs inventory"}
IW --> VAL
VAL -->|equal sets| RUN["ansible-playbook"]
VAL -->|deploy ⊃ inventory| ABORT["exit 1: not present in inventory"]:::abort
classDef abort fill:#fee,stroke:#c00;
Both sides pass primary_apps (not the round closure) as the BFS seed and
the variant-merged services map for that round, so the reachability walk
sees the same topology the inventory baked.
Closure resolution (per round)
plan_dev_inventory_matrix → _resolve_round_include runs
CombinedResolver(follow_run_after=False) over each primary app and unions
the results.
flowchart LR
subgraph edges["prerequisites(role)"]
RA["run_after<br/>(NOT followed for inclusion)"]
DEP["dependencies<br/>(app roles only)"]
SVC["services<br/>(meta/services.yml flags)"]
end
SVC --> GATE{"_is_enabled_shared:<br/>is_explicit_truth(enabled)<br/>AND is_explicit_truth(shared)"}
GATE -->|edge exists| PROV["provider role pulled"]
GATE -->|else| DROP["not included"]
is_explicit_truth treats both forms as truth at resolve time:
enabled / shared value |
resolve-time truth |
|---|---|
literal true |
yes |
"{{ '<role>' in group_names }}" |
yes |
"{{ '<role>' not in group_names }}" |
yes |
literal false |
no |
So a conditional "{{ 'web-app-matomo' in group_names }}" edge pulls its
provider into the static closure regardless of whether web-app-matomo is
actually selected. The closure is the co-deploy superset.
Example: web-svc-cdn closure
web-svc-cdn's only truthy service roots are matomo and prometheus
(both conditional). Everything else hangs beneath them.
flowchart TD
CDN["web-svc-cdn (primary)"] --> MAT["web-app-matomo"]
CDN --> PRM["web-app-prometheus"]
MAT --> KC["web-app-keycloak"]
PRM --> KC
MAT --> MAILU["web-app-mailu"]
KC --> PG["svc-db-postgres"]
KC --> MDB["svc-db-mariadb"]
KC --> LDAP["svc-db-openldap"]
MAILU --> MDB
KC --> CSS["web-svc-design"]
KC --> LOGOUT["web-svc-logout"]
MAT --> DASH["web-app-dashboard"]
CDN --> FILE["web-svc-file"]
CDN --> ASSET["web-svc-asset"]
CDN --> SIMPLE["web-svc-simpleicons"]
Orphan pruning (prune_orphans_after_disable)
BFS from surviving primary apps over dependencies + services edges,
cutting every role in disabled_app_ids. Reachable → kept; the rest of
include_R (disabled roots excluded) → pruned.
flowchart TD
SEED["stack = primary_apps − disabled"] --> POP{"pop node"}
POP -->|in disabled or seen| POP
POP -->|else| MARK["reachable += node"]
MARK --> EDGES["push deps + services<br/>(skip disabled, skip seen)"]
EDGES --> POP
POP -->|empty| SPLIT["kept = include ∩ reachable<br/>pruned = include − reachable − disabled"]
disable=matomo,prometheus on web-svc-cdn cuts both roots; the whole
subtree beneath them is unreachable from the surviving primary (cdn), so
kept = (web-svc-cdn,).
flowchart TD
CDN["web-svc-cdn (survives)"]
MAT["web-app-matomo (cut)"]:::cut
PRM["web-app-prometheus (cut)"]:::cut
KC["web-app-keycloak"]:::orphan
MDB["svc-db-mariadb"]:::orphan
PG["svc-db-postgres"]:::orphan
CDN -.-> MAT
CDN -.-> PRM
MAT --> KC
PRM --> KC
KC --> MDB
KC --> PG
classDef cut fill:#fee,stroke:#c00,stroke-dasharray:4;
classDef orphan fill:#eee,stroke:#999,stroke-dasharray:2;
Reachability, not syntax
Pruning walks whatever edges the resolver reports; it never inspects
enabled for the hard-vs-conditional distinction. A literal enabled: true dependency (e.g. web-app-keycloak → svc-db-postgres) is kept only
while its declaring node stays reachable. If the declaring node is cut or
orphaned and no surviving node pulls the dependency, it is pruned too.
flowchart LR
Q{"dep reachable from a<br/>surviving primary?"}
Q -->|yes| KEEP["kept (hard or conditional)"]
Q -->|no| PRUNE["pruned (hard or conditional)"]
Scope
| Path | include pruned? | disable effect |
|---|---|---|
compose initialize / reinstall |
yes | named roots + orphaned closure |
compose update |
no | ansible extra-var only |
compose CI matrix (legacy_resolver direct) |
no | co-deploy superset, no prune |
swarm (utils.tests.swarm.derive_includes) |
yes | named roots + orphaned closure |
The swarm path applies the same cut through the blocked parameter of
applications_if_group_and_all_deps; disable roles are cut from the dep
walk, so orphaned providers never enter the provisioned inventory and
swarm_deploy_targets (bounded by inventory presence) cannot re-add them.