chore: snapshot WIP previo + flow 0008 + 7 sub-issues (0112-0119)

Snapshot de WIP acumulado de sesiones previas antes de merge wave 1
del flow 0008 (kanban_cpp + agent_runner_api + DoD schema).

Incluye:
- dev/flows/0008-kanban-cpp-and-agent-workflows.md
- dev/issues/0112-0119*.md (7 sub-issues)
- WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 18:17:08 +02:00
parent ddb5366884
commit b9716a7cd6
119 changed files with 14929 additions and 3084 deletions
+117 -13
View File
@@ -54,6 +54,24 @@ def _hex_to_rgb(h: str) -> tuple[int, int, int]:
return tuple(int(h[i: i + 2], 16) for i in (0, 2, 4))
def _luminance(accent_hex: str) -> float:
"""Luminancia percibida Rec.601 normalizada [0,1]."""
r, g, b = _hex_to_rgb(accent_hex)
return (0.299 * r + 0.587 * g + 0.114 * b) / 255.0
def _render_glyph_colored(svg_path: Path, size: int, fill: str) -> Image.Image:
"""Renderiza un SVG Phosphor reemplazando currentColor por `fill`."""
svg = svg_path.read_text(encoding="utf-8")
svg = svg.replace('fill="currentColor"', f'fill="{fill}"')
png_bytes = cairosvg.svg2png(
bytestring=svg.encode("utf-8"),
output_width=size,
output_height=size,
)
return Image.open(io.BytesIO(png_bytes)).convert("RGBA")
def _render_glyph_white(svg_path: Path, size: int) -> Image.Image:
"""Renderiza el SVG Phosphor como glyph blanco sobre fondo transparente.
@@ -105,6 +123,59 @@ def _make_icon_image(svg_path: Path, accent_hex: str, size: int) -> Image.Image:
return canvas
ADAPTIVE_LUMINANCE_THRESHOLD = 0.5
ADAPTIVE_DARK_GLYPH = "#0f0f12"
ADAPTIVE_LIGHT_GLYPH = "#f4f4f5"
WHITE_DUOTONE_GLYPH = "#ffffff"
def _make_icon_image_duotone(
phosphor_icon_name: str,
accent_hex: str,
size: int,
phosphor_assets: Path,
glyph_color: str,
) -> Image.Image:
"""Compone bg accent + glyph duotone Phosphor con `glyph_color` como fill.
Usa la variante `duotone` de Phosphor cuando exista (el path con `opacity=0.2`
crea automaticamente el segundo tono atenuado al mezclarse con el accent).
Fallback a `fill` si no hay duotone.
"""
bg_color = _hex_to_rgb(accent_hex) + (255,)
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(canvas)
radius = max(2, size // 6)
draw.rounded_rectangle([(0, 0), (size - 1, size - 1)], radius=radius, fill=bg_color)
glyph_size = int(size * 0.7)
if glyph_size < 8:
glyph_size = max(8, size - 2)
svg_duo = phosphor_assets / "duotone" / f"{phosphor_icon_name}-duotone.svg"
svg_fill = phosphor_assets / "fill" / f"{phosphor_icon_name}-fill.svg"
svg_src = svg_duo if svg_duo.exists() else svg_fill
if not svg_src.exists():
raise FileNotFoundError(f"Icono Phosphor no encontrado: {svg_src}")
glyph = _render_glyph_colored(svg_src, glyph_size, glyph_color)
off = ((size - glyph_size) // 2, (size - glyph_size) // 2)
canvas.alpha_composite(glyph, dest=off)
return canvas
def _make_icon_image_adaptive(
phosphor_icon_name: str,
accent_hex: str,
size: int,
phosphor_assets: Path,
threshold: float = ADAPTIVE_LUMINANCE_THRESHOLD,
) -> Image.Image:
"""Adaptive duotone: tono claro/oscuro segun luminancia del accent."""
glyph_color = ADAPTIVE_DARK_GLYPH if _luminance(accent_hex) >= threshold else ADAPTIVE_LIGHT_GLYPH
return _make_icon_image_duotone(phosphor_icon_name, accent_hex, size, phosphor_assets, glyph_color)
def generate_app_icon(
phosphor_icon_name: str,
accent_hex: str,
@@ -113,6 +184,7 @@ def generate_app_icon(
weight: str = "fill",
sizes: list[int] = None,
phosphor_root: str = None,
style: str = "fill_white",
) -> str:
"""Genera un icono .ico multi-resolucion a partir de un SVG Phosphor.
@@ -137,17 +209,32 @@ def generate_app_icon(
phosphor_root: Ruta a la carpeta raiz de assets de phosphor-core
(la que contiene subdirectorios "fill", "regular", etc.).
Default: <registry_root>/sources/phosphor-core/assets.
style: Estilo de render. Valores:
- "fill_white" (default, backwards-compat): fondo accent + glyph
blanco solido usando `weight`.
- "adaptive_duotone": fondo accent + glyph duotone con tono claro/
oscuro elegido por luminancia del accent. Glyph oscuro
`#0f0f12` si L>=0.5; glyph claro `#f4f4f5` si L<0.5. Usa
variante `duotone` de Phosphor (el path con opacity=0.2 da el
segundo tono). Fallback a `fill` si no hay duotone.
- "white_duotone": fondo accent + glyph duotone Phosphor con fill
`#ffffff`. Path bg al opacity=0.2 deja translucir el accent y
produce un segundo tono mas suave. Legible en cualquier accent
(claro u oscuro). Fallback a `fill` si no hay duotone.
Returns:
Ruta absoluta del archivo .ico generado.
Raises:
FileNotFoundError: Si el SVG del icono no existe en phosphor_root.
ValueError: Si accent_hex no tiene el formato "#RRGGBB".
ValueError: Si accent_hex no tiene el formato "#RRGGBB" o style invalido.
"""
if sizes is None:
sizes = DEFAULT_SIZES
if style not in ("fill_white", "adaptive_duotone", "white_duotone"):
raise ValueError(f"style invalido: {style!r}. Valores: fill_white | adaptive_duotone | white_duotone")
# Resolver raiz de phosphor
if phosphor_root is None:
_root = _find_registry_root()
@@ -155,22 +242,39 @@ def generate_app_icon(
else:
phosphor_assets = Path(phosphor_root)
svg_file = phosphor_assets / weight / f"{phosphor_icon_name}-{weight}.svg"
if not svg_file.exists():
raise FileNotFoundError(
f"Icono Phosphor no encontrado: {svg_file}\n"
f"Asegurate de que sources/phosphor-core/ existe. Si no:\n"
f" git clone --depth=1 https://github.com/phosphor-icons/core.git "
f"sources/phosphor-core"
)
# Validar formato del color
# Validar formato del color (antes de tocar disco)
h = accent_hex.lstrip("#")
if len(h) != 6:
raise ValueError(f"accent_hex debe tener formato #RRGGBB, recibido: {accent_hex!r}")
# Renderizar cada resolucion individualmente para crispness en tamanos pequeños
images = {s: _make_icon_image(svg_file, accent_hex, s) for s in sorted(sizes, reverse=True)}
if style in ("adaptive_duotone", "white_duotone"):
svg_duo = phosphor_assets / "duotone" / f"{phosphor_icon_name}-duotone.svg"
svg_fill = phosphor_assets / "fill" / f"{phosphor_icon_name}-fill.svg"
if not svg_duo.exists() and not svg_fill.exists():
raise FileNotFoundError(
f"Icono Phosphor no encontrado: {svg_duo} ni {svg_fill}\n"
f"Asegurate de que sources/phosphor-core/ existe."
)
if style == "adaptive_duotone":
images = {
s: _make_icon_image_adaptive(phosphor_icon_name, accent_hex, s, phosphor_assets)
for s in sorted(sizes, reverse=True)
}
else: # white_duotone
images = {
s: _make_icon_image_duotone(phosphor_icon_name, accent_hex, s, phosphor_assets, WHITE_DUOTONE_GLYPH)
for s in sorted(sizes, reverse=True)
}
else:
svg_file = phosphor_assets / weight / f"{phosphor_icon_name}-{weight}.svg"
if not svg_file.exists():
raise FileNotFoundError(
f"Icono Phosphor no encontrado: {svg_file}\n"
f"Asegurate de que sources/phosphor-core/ existe. Si no:\n"
f" git clone --depth=1 https://github.com/phosphor-icons/core.git "
f"sources/phosphor-core"
)
images = {s: _make_icon_image(svg_file, accent_hex, s) for s in sorted(sizes, reverse=True)}
out = Path(out_ico_path)
if not out.is_absolute():