cli.meta.roles.applications.complexity package

Submodules

cli.meta.roles.applications.complexity.cli module

Argument parsing, sorting/filtering and the main entry point.

cli.meta.roles.applications.complexity.cli.build_parser() → ArgumentParser
cli.meta.roles.applications.complexity.cli.main(argv: list[str] | None = None) → int
cli.meta.roles.applications.complexity.cli.parse_lifecycles(tokens: list[str] | None) → set[str] | None

Normalise --lifecycles tokens into a lowercase set, splitting each on commas and whitespace so 'alpha,beta' and 'alpha beta' are equivalent. None (flag absent) stays None so the model falls back to its built-in default envelope.

cli.meta.roles.applications.complexity.cli.parse_sort_spec(spec: str) → list[tuple[str, bool]]

Parse a --sort value into an ordered [(column, reverse), ...].

Parameters:

spec – Comma-separated clauses, each a column optionally prefixed or suffixed with a direction, e.g. "desc embeds, asc total". Direction defaults to asc.

Returns:

Clauses in significance order (first = primary sort key). reverse is True for desc.

Raises:

ValueError – An unknown token, or a clause naming no column.

cli.meta.roles.applications.complexity.filter module

A tiny boolean filter language over ComplexityRow fields.

Grammar (precedence low → high):

or_expr   := xor_expr ('or'  xor_expr)*
xor_expr  := and_expr ('xor' and_expr)*
and_expr  := not_expr ('and' not_expr)*
not_expr  := 'not' not_expr | atom
atom      := '(' or_expr ')' | comparison
comparison:= FIELD OP value
OP        := '%%' | '==' | '!=' | '<=' | '>=' | '<' | '>'
value     := scalar | '{' scalar (',' scalar)* '}'
scalar    := NUMBER | WORD | QUOTED

%% is “contains” for a scalar string value and “is a member of” for a set value; == / != against a set mean membership / non-membership. String comparisons (== != %%) are case-insensitive; < > <= >= are numeric. A bare expression with no operator (e.g. nextcloud) is shorthand for name %% nextcloud.

exception cli.meta.roles.applications.complexity.filter.FilterError

Bases: ValueError

A malformed filter expression.

cli.meta.roles.applications.complexity.filter.compile_predicate(expr: str, fields: frozenset[str]) → Callable[[dict[str, Any]], bool]

Compile expr into a predicate over a field dict. A bare expression with no operator is treated as name %% <expr> (substring on name).

cli.meta.roles.applications.complexity.graph module

Role discovery and the shared-service dependency graph it induces.

cli.meta.roles.applications.complexity.graph.build_graphs(roles_dir: Path, *, truth: TruthFn) → tuple[dict[str, list[str]], dict[str, list[str]]]

Return (forward, reverse) adjacency maps over all role dirs.

forward[A] is the list of provider roles that A directly depends on (services it embeds). reverse[B] is the list of roles that directly depend on B (consumers that embed B).

cli.meta.roles.applications.complexity.graph.build_service_registry(roles_dir: Path) → dict[str, dict[str, Any]]
cli.meta.roles.applications.complexity.graph.direct_dep_roles(services_map: dict[str, Any], registry: dict[str, dict[str, Any]], *, truth: Callable[[Any], bool]) → list[str]

Provider roles a services map directly embeds (enabled AND shared truthy under truth), deduped in first-seen order.

cli.meta.roles.applications.complexity.graph.is_application_role(role_dir: Path) → bool
cli.meta.roles.applications.complexity.graph.resolve_transitively(start_role: str, forward_graph: dict[str, list[str]], *, max_level: int | None = None) → list[str]

BFS over an adjacency map. max_level caps recursion depth: 1 returns direct neighbours only, 2 adds their direct neighbours, etc. None walks the full closure. The start role itself is never included in the result.

cli.meta.roles.applications.complexity.graph.truth_predicate(*, include_group_names: bool) → Callable[[Any], bool]

cli.meta.roles.applications.complexity.model module

The per-role complexity score, its dna cluster key and siblings.

class cli.meta.roles.applications.complexity.model.ComplexityRow(name: str, embeds: int, services: list[str], consumers: int, consumed_by: list[str], embeds_direct: int, services_direct: list[str], consumers_direct: int, consumed_by_direct: list[str], weight: int, dna: str, siblings: list[str], random: int = 0, variant: int | None = None, id: int = 0, covered_by: int = 0, row: int = -1, variants: int = 1, lifecycle: str = '', compose: bool = False, swarm: bool = False, host: bool = False, stack: bool = False, test_compose: bool = False, test_swarm: bool = False, test_host: bool = False, integrated: bool = True, in_main: bool = True, clone: bool = False)

Bases: NamedTuple

One application role. The field order is the column order; legacy positional access (row[0] … row[11]) still resolves.

The transitive fields (services / consumed_by and their counts) are the BFS closure capped at max_level; the *_direct fields are always the one-hop neighbours. weight is the sum of the four numeric columns. dna is a hash of the role’s own name unioned with its embedded services (sorted), so two roles covering the same service set share a dna. siblings are the other roles sharing that dna; clone is True for every row of a dna group except the heaviest one (ties broken by name), so sorting clones last keeps one representative per service set ahead of the budget cut. random is a per-row 6-digit nonce, rolled fresh per invocation, so tie order deliberately varies between runs. variant is the meta/variants.yml index this row was computed against, or None for a whole-role row. id and covered_by are set by the CLI after sorting: id is the row’s numeric position in sort order (1-based), and covered_by is the id of the first earlier (green) row of a DIFFERENT role that embeds this row, or the sentinel 0 when this row is itself green (no real id is 0). Two variants of the same role never cover each other. row is the 1-based line number in the final rendered output (assigned last, after filtering), so it stays sequential even when sorting by covered_by scrambles id. variants is the number of meta/variants.yml variants of the role in whole-role mode; under --variant each row already is a single variant, so it is 1. lifecycle is the role’s meta/services.yml lifecycle stage (alpha/beta/pre/…). compose / swarm are True when the CI test-deploy matrix exercises the role in that mode, honouring the discovery skip logic: invokable + lifecycle in the tested envelope + the role’s skip list. Under --variant swarm is per-variant (every variant deploys, so it tracks the role-level swarm and stack). stack is True when the role renders its own container stack (ships a templates/*compose*.yml.j2); host-only roles (backup, wireguard, swapfile) and pure service-injectors are False. swarm implies stack: a role with no compose template of its own is never a swarm stack-deploy target, so swarm is forced False when stack is False. host mirrors the primary entity’s modes.host.enabled flag (default True) for non-stack roles, and is forced False for a stack role: it marks invokable roles that configure the host instead of shipping a stack. test_compose / test_swarm / test_host duplicate the mode columns minus the role’s meta/tests.yml skip list: modes states where a role RUNS, skip deactivates TESTING a mode, and CI discovery filters on these test_* columns. integrated is False when the row’s direct service map keeps no foreign provider enabled (the role deploys isolated): per variant on variant rows, from the base meta/services.yml on whole-role rows.

clone: bool

Alias for field number 28

compose: bool

Alias for field number 19

consumed_by: list[str]

Alias for field number 4

consumed_by_direct: list[str]

Alias for field number 8

consumers: int

Alias for field number 3

consumers_direct: int

Alias for field number 7

covered_by: int

Alias for field number 15

dna: str

Alias for field number 10

embeds: int

Alias for field number 1

embeds_direct: int

Alias for field number 5

host: bool

Alias for field number 21

id: int

Alias for field number 14

in_main: bool

Alias for field number 27

integrated: bool

Alias for field number 26

lifecycle: str

Alias for field number 18

name: str

Alias for field number 0

random: int

Alias for field number 12

row: int

Alias for field number 16

services: list[str]

Alias for field number 2

services_direct: list[str]

Alias for field number 6

siblings: list[str]

Alias for field number 11

stack: bool

Alias for field number 22

swarm: bool

Alias for field number 20

test_compose: bool

Alias for field number 23

test_host: bool

Alias for field number 25

test_swarm: bool

Alias for field number 24

variant: int | None

Alias for field number 13

variants: int

Alias for field number 17

weight: int

Alias for field number 9

cli.meta.roles.applications.complexity.model.TESTED_LIFECYCLES = frozenset({'alpha', 'beta', 'maintenance', 'rc', 'stable'})

Lifecycle stages the CI test-deploy discovery exercises, sourced from the INFINITO_LIFECYCLES value in default.env (the single source).

cli.meta.roles.applications.complexity.model.attach_siblings(rows: list[ComplexityRow]) → list[ComplexityRow]

Group rows by dna and elect the heaviest of each group the original, marking the rest clone so the budget cut keeps one representative.

Election is relative to the rows handed in, so a caller that filters MUST call this again on the survivors: a stand-in the filter dropped is not deployed, and the row deferring to it is cut in favour of nothing.

cli.meta.roles.applications.complexity.model.compute_complexity_rows(roles_dir: Path, *, include_group_names: bool = True, max_level: int | None = None, lifecycles: set[str] | None = None) → list[ComplexityRow]
cli.meta.roles.applications.complexity.model.compute_variant_complexity_rows(roles_dir: Path, *, include_group_names: bool = True, max_level: int | None = None, lifecycles: set[str] | None = None) → list[ComplexityRow]

One row per meta/variants.yml variant of every application role.

Each row recomputes the role’s embedded (forward) service deps from that variant’s merged services map, so toggling a sidecar off in a variant drops it from the variant’s embeds/weight. The consumer (reverse) side is the variant-independent role-level graph: who else in the catalog embeds this role does not change with the role’s own variant selection.

cli.meta.roles.applications.complexity.render module

Render complexity rows as a counts table, JSON, or bare role names.

cli.meta.roles.applications.complexity.render.render_json(rows: list[ComplexityRow]) → str
cli.meta.roles.applications.complexity.render.render_string(rows: list[ComplexityRow]) → str
cli.meta.roles.applications.complexity.render.render_table(rows: list[ComplexityRow], *, symbol: bool = False) → str

Counts only (plus the short dna and the sibling count). Use --format json for the full role and sibling name lists. With symbol=True the headers are emoji-only (compact), the boolean/lifecycle cells render as emojis, and a legend is appended.

cli.meta.roles.applications.complexity.render.render_yaml(rows: list[ComplexityRow]) → str

Module contents