mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
lifecycle.md said a deprecated role is still covered by the test suite while the tested envelope already left it out. A deprecated role is now untested: the matrix-deploy MUST NOT exercise it and a failure in it MUST NOT block a release, while planned, pre-alpha and eol stay open to best-effort runs. The complexity legend says untested too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
296 lines
10 KiB
Python
296 lines
10 KiB
Python
"""Render complexity rows as a counts table, JSON, or bare role names."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import unicodedata
|
|
from typing import TYPE_CHECKING
|
|
|
|
from utils.cache.yaml import dump_yaml_str
|
|
from utils.symbol_glossary import to_emoji
|
|
|
|
if TYPE_CHECKING:
|
|
from .model import ComplexityRow
|
|
|
|
DNA_DISPLAY_LEN = 10
|
|
|
|
|
|
def _char_width(ch: str) -> int:
|
|
"""Terminal cell width of one character: 0 for combining marks / variation
|
|
selectors / ZWJ, 2 for emoji and East-Asian wide glyphs, else 1."""
|
|
o = ord(ch)
|
|
if o == 0x200D or 0xFE00 <= o <= 0xFE0F or 0x1F3FB <= o <= 0x1F3FF:
|
|
return 0
|
|
if (
|
|
0x1F000 <= o <= 0x1FAFF
|
|
or 0x2600 <= o <= 0x27BF
|
|
or 0x2B00 <= o <= 0x2BFF
|
|
or unicodedata.east_asian_width(ch) in ("W", "F")
|
|
):
|
|
return 2
|
|
return 1
|
|
|
|
|
|
def _dwidth(text: str) -> int:
|
|
"""Display width of *text* in terminal cells (emoji count as 2)."""
|
|
return sum(_char_width(ch) for ch in text)
|
|
|
|
|
|
def _variant_cell(row: ComplexityRow) -> str:
|
|
return "" if row.variant is None else str(row.variant)
|
|
|
|
|
|
_LIFECYCLE_STAGES = (
|
|
"planned",
|
|
"pre-alpha",
|
|
"alpha",
|
|
"beta",
|
|
"rc",
|
|
"stable",
|
|
"maintenance",
|
|
"deprecated",
|
|
"eol",
|
|
)
|
|
_LIFECYCLE_SYMBOLS: dict[str, str] = {s: to_emoji(s) for s in _LIFECYCLE_STAGES}
|
|
|
|
_HEADER_NAMES = (
|
|
"row",
|
|
"id",
|
|
"name",
|
|
"in_main",
|
|
"lifecycle",
|
|
"variant",
|
|
"integrated",
|
|
"variants",
|
|
"compose",
|
|
"swarm",
|
|
"stack",
|
|
"host",
|
|
"test_compose",
|
|
"test_swarm",
|
|
"test_host",
|
|
"embeds_direct",
|
|
"embeds",
|
|
"consumers_direct",
|
|
"consumers",
|
|
"weight",
|
|
"random",
|
|
"dna",
|
|
"clone",
|
|
"siblings",
|
|
"covered_by",
|
|
)
|
|
_HEADER_SYMBOLS: dict[str, str] = {name: to_emoji(name) for name in _HEADER_NAMES}
|
|
|
|
|
|
def _bool_cell(value: bool, *, symbol: bool = False) -> str:
|
|
if symbol:
|
|
return to_emoji("enabled") if value else to_emoji("disabled")
|
|
return "true" if value else "false"
|
|
|
|
|
|
def _lifecycle_cell(stage: str, *, symbol: bool = False) -> str:
|
|
return _LIFECYCLE_SYMBOLS.get(stage, stage) if symbol else stage
|
|
|
|
|
|
def _header(name: str, *, symbol: bool) -> str:
|
|
"""With ``symbol`` the header is the emoji alone (compact); the column name
|
|
falls back in only when no emoji is mapped."""
|
|
if not symbol:
|
|
return name
|
|
return _HEADER_SYMBOLS.get(name, name)
|
|
|
|
|
|
_COLUMN_DOC: dict[str, str] = {
|
|
"row": "line number in this rendering",
|
|
"id": "position in sort order",
|
|
"name": "role id",
|
|
"lifecycle": "maturity stage (see Lifecycle below)",
|
|
"variant": "meta/variants.yml index of this row",
|
|
"integrated": "row's service map keeps at least one foreign service enabled",
|
|
"variants": "number of variants the role has",
|
|
"compose": "exercised by the compose test-deploy matrix",
|
|
"swarm": "exercised by the swarm test-deploy matrix",
|
|
"stack": "ships its own container stack (compose template)",
|
|
"host": "configures the host instead of shipping a stack",
|
|
"test_compose": "compose minus meta/tests.yml skip",
|
|
"test_swarm": "swarm minus meta/tests.yml skip",
|
|
"test_host": "host minus meta/tests.yml skip",
|
|
"embeds_direct": "service roles it embeds directly",
|
|
"embeds": "service roles it embeds transitively",
|
|
"consumers_direct": "roles that embed it directly",
|
|
"consumers": "roles that embed it transitively",
|
|
"weight": "sum of the four count columns",
|
|
"random": "per-row display nonce",
|
|
"in_main": "role directory also present on origin/main (else new on this branch)",
|
|
"dna": "cluster key shared by same-service-set roles",
|
|
"clone": "another role with the same dna carries more weight",
|
|
"siblings": "other roles sharing the dna",
|
|
"covered_by": "id of an earlier row that already embeds it (0 = green)",
|
|
}
|
|
|
|
_LIFECYCLE_DOC: dict[str, str] = {
|
|
"planned": "on the roadmap, no code yet",
|
|
"pre-alpha": "scaffolding, too unstable to test",
|
|
"alpha": "deploys end-to-end with a smoke spec",
|
|
"beta": "documented integrations tested",
|
|
"rc": "burn-in on production, intends stable",
|
|
"stable": "shipped in a release without a hotfix",
|
|
"maintenance": "stable coverage, feature-frozen",
|
|
"deprecated": "kept for compatibility, untested, do not adopt",
|
|
"eol": "end of life: shipped but not tested or maintained",
|
|
}
|
|
|
|
|
|
def _symbol_legend(names: list[str], rows: list[ComplexityRow]) -> list[str]:
|
|
"""Grouped legend, one entry per line: symbol, label, explanation. The
|
|
symbol is placed LAST so the label and doc columns (both pure ASCII) stay
|
|
flush regardless of how wide the terminal renders each emoji; only the
|
|
trailing symbol can shift a cell, at the line end where it does not disturb
|
|
the grid. Only the columns rendered and lifecycle stages present are listed."""
|
|
columns = [
|
|
(_HEADER_SYMBOLS[name], name, _COLUMN_DOC.get(name, ""))
|
|
for name in names
|
|
if name in _HEADER_SYMBOLS
|
|
]
|
|
flags = [("✅", "true", "the flag is set"), ("❌", "false", "the flag is not set")]
|
|
present = {r.lifecycle for r in rows}
|
|
lifecycle = [
|
|
(symbol, stage, _LIFECYCLE_DOC.get(stage, ""))
|
|
for stage, symbol in _LIFECYCLE_SYMBOLS.items()
|
|
if stage in present
|
|
]
|
|
|
|
groups = [("Columns", columns), ("Flags", flags)]
|
|
if lifecycle:
|
|
groups.append(("Lifecycle", lifecycle))
|
|
|
|
entries = [entry for _, group in groups for entry in group]
|
|
label_w = max(len(label) for _, label, _ in entries)
|
|
doc_w = max(len(doc) for _, _, doc in entries)
|
|
|
|
lines = ["", "Legend:"]
|
|
for title, group in groups:
|
|
lines.append(f" {title}:")
|
|
lines.extend(
|
|
f" {label.ljust(label_w)} {doc.ljust(doc_w)} {sym}"
|
|
for sym, label, doc in group
|
|
)
|
|
return lines
|
|
|
|
|
|
def 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."""
|
|
if not rows:
|
|
return ""
|
|
|
|
raw = [
|
|
("row", ">", [str(r.row) for r in rows]),
|
|
("id", ">", [str(r.id) for r in rows]),
|
|
("name", "<", [r.name for r in rows]),
|
|
("in_main", ">", [_bool_cell(r.in_main, symbol=symbol) for r in rows]),
|
|
("lifecycle", "<", [_lifecycle_cell(r.lifecycle, symbol=symbol) for r in rows]),
|
|
*(
|
|
[("variant", ">", [_variant_cell(r) for r in rows])]
|
|
if any(r.variant is not None for r in rows)
|
|
else []
|
|
),
|
|
("integrated", ">", [_bool_cell(r.integrated, symbol=symbol) for r in rows]),
|
|
("variants", ">", [str(r.variants) for r in rows]),
|
|
("compose", ">", [_bool_cell(r.compose, symbol=symbol) for r in rows]),
|
|
("swarm", ">", [_bool_cell(r.swarm, symbol=symbol) for r in rows]),
|
|
("host", ">", [_bool_cell(r.host, symbol=symbol) for r in rows]),
|
|
("stack", ">", [_bool_cell(r.stack, symbol=symbol) for r in rows]),
|
|
(
|
|
"test_compose",
|
|
">",
|
|
[_bool_cell(r.test_compose, symbol=symbol) for r in rows],
|
|
),
|
|
("test_swarm", ">", [_bool_cell(r.test_swarm, symbol=symbol) for r in rows]),
|
|
("test_host", ">", [_bool_cell(r.test_host, symbol=symbol) for r in rows]),
|
|
("embeds_direct", ">", [str(r.embeds_direct) for r in rows]),
|
|
("embeds", ">", [str(r.embeds) for r in rows]),
|
|
("consumers_direct", ">", [str(r.consumers_direct) for r in rows]),
|
|
("consumers", ">", [str(r.consumers) for r in rows]),
|
|
("weight", ">", [str(r.weight) for r in rows]),
|
|
("random", ">", [str(r.random) for r in rows]),
|
|
("dna", "<", [r.dna[:DNA_DISPLAY_LEN] for r in rows]),
|
|
("clone", ">", [_bool_cell(r.clone, symbol=symbol) for r in rows]),
|
|
("siblings", ">", [str(len(r.siblings)) for r in rows]),
|
|
("covered_by", ">", [str(r.covered_by) for r in rows]),
|
|
]
|
|
cols = [(_header(name, symbol=symbol), align, cells) for name, align, cells in raw]
|
|
widths = [
|
|
max(_dwidth(title), *(_dwidth(c) for c in cells)) for title, _, cells in cols
|
|
]
|
|
|
|
def _line(values: list[str]) -> str:
|
|
parts = []
|
|
for v, (_, align, _), w in zip(values, cols, widths, strict=True):
|
|
gap = " " * max(w - _dwidth(v), 0)
|
|
parts.append(v + gap if align == "<" else gap + v)
|
|
return " ".join(parts)
|
|
|
|
lines = [
|
|
_line([title for title, _, _ in cols]),
|
|
" ".join("-" * w for w in widths),
|
|
]
|
|
lines.extend(_line([cells[i] for _, _, cells in cols]) for i in range(len(rows)))
|
|
if symbol:
|
|
lines.extend(_symbol_legend([name for name, _, _ in raw], rows))
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _payload(rows: list[ComplexityRow]) -> list[dict]:
|
|
return [
|
|
{
|
|
"row": r.row,
|
|
"id": r.id,
|
|
"name": r.name,
|
|
"in_main": r.in_main,
|
|
"lifecycle": r.lifecycle,
|
|
"variant": r.variant,
|
|
"integrated": r.integrated,
|
|
"variants": r.variants,
|
|
"compose": r.compose,
|
|
"swarm": r.swarm,
|
|
"host": r.host,
|
|
"stack": r.stack,
|
|
"test_compose": r.test_compose,
|
|
"test_swarm": r.test_swarm,
|
|
"test_host": r.test_host,
|
|
"embeds_direct": r.embeds_direct,
|
|
"services_direct": r.services_direct,
|
|
"embeds": r.embeds,
|
|
"services": r.services,
|
|
"consumers_direct": r.consumers_direct,
|
|
"consumed_by_direct": r.consumed_by_direct,
|
|
"consumers": r.consumers,
|
|
"consumed_by": r.consumed_by,
|
|
"weight": r.weight,
|
|
"random": r.random,
|
|
"dna": r.dna,
|
|
"clone": r.clone,
|
|
"siblings": r.siblings,
|
|
"covered_by": r.covered_by,
|
|
}
|
|
for r in rows
|
|
]
|
|
|
|
|
|
def render_json(rows: list[ComplexityRow]) -> str:
|
|
return json.dumps(_payload(rows), indent=2)
|
|
|
|
|
|
def render_yaml(rows: list[ComplexityRow]) -> str:
|
|
return dump_yaml_str(_payload(rows)).rstrip("\n")
|
|
|
|
|
|
def render_string(rows: list[ComplexityRow]) -> str:
|
|
return "\n".join(
|
|
r.name if r.variant is None else f"{r.name}#{r.variant}" for r in rows
|
|
)
|