Registro público
Informe de salud del softwareesquema 0.27.0 · métricas 1.13.0 · 2026-07-31 09:06 UTC

Djelibeybi / lifx-async

A modern, type-safe, async Python library for controlling LIFX lights

PythonUPL-1.0★ 5 estrellas⑂ 1 forkdesde nov 2025Ver en GitHub ↗

Djelibeybi/lifx-async tiene un índice de salud de 71 sobre 100, lo que lo sitúa en la banda Bueno. Su puntuación más alta es Engineering Quality (92/100) y la más baja, Community & Adoption (36/100). Se actualizó por última vez hoy. Una sola persona concentra la mayor parte del trabajo reciente.

71
global / 100
Bueno

Índice de salud del software

Las métricas se agrupan en categorías ponderadas sobre una escala de 1 a 100. El resultado global parte de su media; cuando la evidencia pública activa la Política de Jurisdicciones de Alto Riesgo, la calificación se ajusta y recibe el límite 49 (En riesgo). Preparación para IA queda fuera.

71
Excelente85-100Ejemplar; cumple prácticamente todos los criterios evaluados
Bueno70-84Saludable; carencias menores
Moderado50-69Aceptable con carencias notables; se recomienda revisión
En riesgo30-49Debilidades significativas; su adopción exige cautela
Crítico1-29Problemas graves (proyecto abandonado, un solo mantenedor, sin higiene)
VitalidadComunidad yAdopciónSostenibilidady GobernanzaCalidad deIngenieríaSeguridadPreparaciónpara IA

Perfil de puntuación

Cada eje es una categoría. La forma importa más que la media: un proyecto sano llena toda la figura, mientras que un perfil de picos y cráteres indica que la fortaleza en una dimensión enmascara el riesgo en otra.

Titularidad

Avi MillerCuenta personal
187 seguidores46 repositorios públicosdesde jul 2009

Este repositorio pertenece a una cuenta personal. Un proyecto con un único propietario conlleva más riesgo de continuidad que uno respaldado por una organización.

Ecosistemas de paquetes

RegistroPaqueteVersiónDescargas / mesVersionesÚltima publicación
PyPIlifx-async6.1.1396375hace 0 días

Métricas por categoría

Vitalidad

¿Está vivo el proyecto: se escribe código y se publican versiones?

90Excelente · 22% del índice global
Cómo se puntúa
36/36Recencia de push — último push hace 0 días
25.6/36Cadencia de commits — 37/52 semanas con commits
18/18Volumen de commits — 499 commits en el último año
10/10OpenSSF Scorecard: Maintained — 30 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 10
Datos de entrada utilizados
commits_last_year499
human_commit_share0,89
days_since_last_push0
active_weeks_last_year37
Cómo se puntúa
27/27Publica versiones — 75 versiones publicadas
36/36Recencia de las versiones — última versión hace 0 días
27/27Cadencia de publicación — una versión cada ~5,3 días
0/10OpenSSF Scorecard: Signed-Releases — Project has not signed or included provenance with any releases.
Datos de entrada utilizados
releases_count75
latest_release_tagv6.1.1
releases_from_tagsno
days_since_latest_release0
mean_days_between_releases5,3

Comunidad y Adopción

¿Tiene el proyecto usuarios, descargas, atención y unas condiciones acogedoras para quienes contribuyen?

36En riesgo · 18% del índice global
Cómo se puntúa
9.8/60Estrellas — 5 estrellas
0/25Forks — 1 forks
0/15Observadores — 1 observadores
Datos de entrada utilizados
forks1
stars5
watchers1
growth_stateunverified
growth_factor_pct100
growth_unverified_reasonno_history
Cómo se puntúa
22.5/22.5README
22.5/22.5Licencia — licencia reconocida (UPL-1.0)
0/18Guía CONTRIBUTING
0/13.5Código de conducta
0/7.2Plantilla de issues
0/6.3Plantilla de PR
Datos de entrada utilizados
has_readme
has_license
has_contributingno
has_issue_templateno
has_code_of_conductno
has_pull_request_templateno
Cómo se puntúa
48/80Descargas mensuales — 3963 descargas/mes en pypi
0/20Dependientes en el registro — no lo informa este ecosistema
Datos de entrada utilizados
packageslifx-async
dependents
ecosystemspypi
total_downloads
monthly_downloads3963
Excluidos de la puntuación (sin datos o no aplicable): Dependientes en el registro. Los pesos restantes se han renormalizado.

Sostenibilidad y Gobernanza

¿Sobrevivirá el proyecto a sus personas: factor bus, capacidad de respuesta, quién lo respalda y mantenimiento del paquete?

56Moderado · 24% del índice global
Cómo se puntúa
9/54Factor bus — la mitad de los commits recae en 1 contribuyente(s)
0/22.5Distribución de commits — el principal contribuyente firma el 100% de los commits
1.4/13.5Amplitud de contribuyentes — 1 contribuyentes
0/10OpenSSF Scorecard: Contributors — project has 0 contributing companies or organizations -- score normalized to 0
Datos de entrada utilizados
bus_factor1
contributors_sampled1
top_contributor_share1
Cómo se puntúa
31.2/46.8Resolución de issues — 67% de issues cerradas
37.2/38.3Aceptación de PR — 177/182 PR decididos fusionados
0/15OpenSSF Scorecard: Code-Review — Found 0/9 approved changesets -- score normalized to 0
Datos de entrada utilizados
merged_prs177
open_issues1
closed_issues2
issue_closed_ratio0,667
closed_unmerged_prs5
Cómo se puntúa
10/30Respaldo de la propiedad — cuenta personal (usuario)
0/20Dominio verificado — no aplicable a cuentas de usuario
16.4/25Alcance del propietario — 187 seguidores de Djelibeybi
24.2/25Trayectoria — 46 repos públicos, cuenta de ~17 años
Datos de entrada utilizados
followers187
owner_typeUser
is_verified
owner_loginDjelibeybi
public_repos46
account_age_days6230
Excluidos de la puntuación (sin datos o no aplicable): Dominio verificado. Los pesos restantes se han renormalizado.
Cómo se puntúa
25/25Publicado y resoluble — 1 paquete(s) en pypi
35/35Recencia de publicación — última publicación hace 0 días
20/20Historial de versiones — 75 versiones en el registro
20/20No obsoleto — activo, ni obsoleto ni retirado
Datos de entrada utilizados
packageslifx-async
ecosystemspypi
any_deprecatedno
min_days_since_publish0

Calidad de Ingeniería

¿Existen unas prácticas mínimas de ingeniería y documentación?

92Excelente · 20% del índice global
Cómo se puntúa
24/24Flujos de trabajo de CI — 3 flujo(s) de trabajo
24/24Pruebas presentes
16/16Configuración de linter
9.6/9.6Hooks de pre-commit
0/6.4.editorconfig
20/20OpenSSF Scorecard: CI-Tests — 5 out of 5 merged PRs checked by a CI test -- score normalized to 10
Datos de entrada utilizados
has_ci
has_tests
has_editorconfigno
has_linter_config
has_precommit_config

Documentación

90Excelente
Cómo se puntúa
30/30README
25/25Directorio de documentación
15/15Sitio de documentación / página del proyecto — https://djelibeybi.github.io/lifx-async/
10/10Descripción del repositorio
10/10Topics — 7 topics
0/10Wiki
Datos de entrada utilizados
topicsasyncio, asyncio-python, lifx, lifx-bulbs, lifx-lan-protocol, lifx-lights, llms-txt
has_wikino
homepagehttps://djelibeybi.github.io/lifx-async/
has_readme
has_docs_dir
has_description

Seguridad

¿Son sólidas las prácticas visibles de seguridad y de cadena de suministro, sin exposición jurisdiccional de alto riesgo sin resolver?

78Bueno · 16% del índice global
Cómo se puntúa
7.5/7.5Binary-Artifacts — no binaries found in the repo
3/7.5Branch-Protection — branch protection is not maximal on development and all release branches
2.5/2.5CI-Tests — 5 out of 5 merged PRs checked by a CI test -- score normalized to 10
0/2.5CII-Best-Practices — no effort to earn an OpenSSF best practices badge detected
0/7.5Code-Review — Found 0/9 approved changesets -- score normalized to 0
0/2.5Contributors — project has 0 contributing companies or organizations -- score normalized to 0
10/10Dangerous-Workflow — no dangerous workflow patterns detected
7.5/7.5Dependency-Update-Tool — update tool detected
0/5Fuzzing — project is not fuzzed
2.5/2.5Licencia — license file detected
7.5/7.5Maintained — 30 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 10
5/5Packaging — packaging workflow detected
5/5Pinned-Dependencies — all dependencies are pinned
5/5SAST — SAST tool is run on all commits
5/5Security-Policy — security policy file detected
0/7.5Signed-Releases — Project has not signed or included provenance with any releases.
7.5/7.5Token-Permissions — GitHub workflow tokens follow principle of least privilege
7.5/7.5Vulnerabilities — 0 existing vulnerabilities detected
Datos de entrada utilizados
sourceopenssf_scorecard
checks_evaluated18
scorecard_versionv5.5.0
checks_inconclusive0
scorecard_aggregate7,2
Cómo se puntúa
35/35Dependencias directas libres de avisos conocidos — ninguna dependencia directa tiene un aviso conocido
0/25Dependencias indirectas libres de avisos conocidos — el conjunto transitivo no es separable de las dependencias de desarrollo y prueba en este alcance
0/40Sin avisos pendientes — ningún aviso tiene fecha de publicación
Datos de entrada utilizados
sourceosv
advisories0
affected_packages0
assessed_packages67
unassessed_packages0
affected_by_severitynone
direct_affected_packages0
Excluidos de la puntuación (sin datos o no aplicable): Dependencias indirectas libres de avisos conocidos, Sin avisos pendientes. Los pesos restantes se han renormalizado. Se cotejaron 67 dependencias resueltas con OSV. Este repositorio no publica ningún paquete que el índice resuelva, por lo que se evaluó en su lugar el grafo de dependencias del repositorio. Ese grafo mezcla fijaciones de desarrollo y prueba con las dependencias distribuidas, de modo que solo se puntúan las dependencias declaradas en tiempo de ejecución; los hallazgos transitivos se informan como contexto y quedan excluidos de la puntuación. No se analiza la alcanzabilidad.

Preparación para IA

¿Hasta qué punto está el repositorio preparado para desarrollarse y mantenerse con agentes de codificación de IA? Es una insignia independiente y experimental — peso 0,0, de modo que se presenta por separado y no afecta a la puntuación de salud global.

76Bueno · 0% del índice global
Cómo se puntúa
45/45Instrucciones para agentes — AGENTS.md, CLAUDE.md
0/15Documentación legible por máquinas (llms.txt)
40/40Historial de commits legible — 78 de 89 commits humanos declaran su intención (asunto estructurado o cuerpo explicativo)
Datos de entrada utilizados
has_llms_txtno
legible_history_share0,876
agent_instruction_filesAGENTS.md, CLAUDE.md
agent_instruction_max_bytes22.347
Cómo se puntúa
0/18Arranque con un solo comando
22/22Pruebas automatizadas
11/11Configuración de lint / formato
11/11Verificación estática de tipos — src/lifx/py.typed
10/10Entorno reproducible — lockfile
10/10Práctica demostrada con agentes — 21 de los últimos 100 commits con autoría o crédito de agente
8/8Mantenimiento automatizado — 11 de los últimos 100 commits son actualizaciones automáticas de dependencias
10/10OpenSSF Scorecard: Pinned-Dependencies — all dependencies are pinned
Datos de entrada utilizados
has_nixno
has_tests
lockfilesuv.lock
has_dockerfileno
typed_languageno
bootstrap_files
has_devcontainerno
has_linter_config
typecheck_configssrc/lifx/py.typed
agent_commit_share0,21
toolchain_manifests
dependency_bot_commit_share0,11
Cómo se puntúa
27/45Código verificable por tipos — Python con configuración de verificación de tipos (src/lifx/py.typed)
54.1/55Tamaños de archivo manejables — 4/237 archivos fuente de más de 60 KB
Datos de entrada utilizados
primary_languagePython
largest_source_bytes119.553
source_files_sampled237
oversized_source_files4
Cómo se puntúa
0/40Esquema de API (OpenAPI/GraphQL/proto)
0/20Servidor MCP
40/40Ejemplos ejecutables — examples
Datos de entrada utilizados
example_dirsexamples
has_mcp_signalno
api_schema_files

Datos clave

5estrellas de GitHub
1contribuidores
499commits en los últimos 12 meses
0días desde el último push
75versiones publicadas
1factor bus
1issues abiertas
PyPIecosistemas de paquetes

Advertencias de recopilación de datos

  • Star history unavailable: GitHub GraphQL error: Resource not accessible by personal access token
  • deps.dev does not index pypi:lifx-async@6.1.1; advisories assessed against the repository dependency graph instead

Más detalle

OpenSSF Scorecard 7.2 / 10
7.2agregado

Evaluación de seguridad independiente y agnóstica en cuanto a herramientas, procedente del proyecto de código abierto OpenSSF Scorecard. Cada comprobación premia una práctica de seguridad, no la herramienta de un proveedor concreto. Las comprobaciones que Scorecard no pudo determinar se marcan como n/d y se excluyen de la puntuación de seguridad (nunca se cuentan como cero).Scorecard v5.5.0 · 2026-07-31 09:06 UTC

10Binary-Artifactsno binaries found in the repo
4Branch-Protectionbranch protection is not maximal on development and all release branches
10CI-Tests5 out of 5 merged PRs checked by a CI test -- score normalized to 10
0CII-Best-Practicesno effort to earn an OpenSSF best practices badge detected
0Code-ReviewFound 0/9 approved changesets -- score normalized to 0
0Contributorsproject has 0 contributing companies or organizations -- score normalized to 0
10Dangerous-Workflowno dangerous workflow patterns detected
10Dependency-Update-Toolupdate tool detected
0Fuzzingproject is not fuzzed
10Licenselicense file detected
10Maintained30 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 10
10Packagingpackaging workflow detected
10Pinned-Dependenciesall dependencies are pinned
10SASTSAST tool is run on all commits
10Security-Policysecurity policy file detected
0Signed-ReleasesProject has not signed or included provenance with any releases.
10Token-PermissionsGitHub workflow tokens follow principle of least privilege
10Vulnerabilities0 existing vulnerabilities detected
Todas las dependencias 67

Conjunto completo de dependencias resueltas según el grafo de dependencias de GitHub: 0 paquetes directos y 67 indirectos (transitivos). El cierre transitivo es completo cuando el repositorio incluye un lockfile.

RegistroPaqueteVersiónRelación
PyPIannotated-doc0.0.4indirecta
PyPIannotated-types0.7.0indirecta
PyPIbackports-asyncio-runner1.2.0indirecta
PyPIbeautifulsoup44.15.0indirecta
PyPIclick8.4.2indirecta
PyPIcolorama0.4.6indirecta
PyPIcoverage7.15.0indirecta
PyPIdeepmerge2.1.0indirecta
PyPIexceptiongroup1.3.1indirecta
PyPIghp-import2.1.0indirecta
PyPIgriffelib2.1.0indirecta
PyPIhatchling1.30.1indirecta
PyPIiniconfig2.3.0indirecta
PyPIjinja23.1.6indirecta
PyPIlifx-async6.1.1indirecta
PyPIlifx-emulator-core3.7.0indirecta
PyPIllmstxt-standalone0.2.0indirecta
PyPImarkdown3.10.2indirecta
PyPImarkdown-it-py3.0.0indirecta
PyPImarkdownify1.2.3indirecta
PyPImarkupsafe3.0.3indirecta
PyPImdformat0.7.22indirecta
PyPImdformat-tables1.0.0indirecta
PyPImdurl0.1.2indirecta
PyPImergedeep1.3.4indirecta
PyPImkdocs1.6.1indirecta
PyPImkdocs-autorefs1.4.4indirecta
PyPImkdocs-get-deps0.2.2indirecta
PyPImkdocstrings1.0.4indirecta
PyPImkdocstrings-python2.0.5indirecta
PyPInodeenv1.10.0indirecta
PyPIpackaging26.2indirecta
PyPIpathspec1.1.1indirecta
PyPIplatformdirs4.10.0indirecta
PyPIpluggy1.6.0indirecta
PyPIprek0.4.8indirecta
PyPIpy-cpuinfo9.0.0indirecta
PyPIpydantic2.13.4indirecta
PyPIpydantic-core2.46.4indirecta
PyPIpygments2.20.0indirecta
PyPIpymdown-extensions11.0.1indirecta
PyPIpyright1.1.411indirecta
PyPIpytest9.1.1indirecta
PyPIpytest-asyncio1.4.0indirecta
PyPIpytest-benchmark5.2.3indirecta
PyPIpytest-cov7.1.0indirecta
PyPIpytest-retry1.7.0indirecta
PyPIpytest-sugar1.1.1indirecta
PyPIpytest-timeout2.4.0indirecta
PyPIpython-dateutil2.9.0.post0indirecta
PyPIpyyaml6.0.3indirecta
PyPIpyyaml-env-tag1.1indirecta
PyPIrich15.0.0indirecta
PyPIruamel-yaml0.19.1indirecta
PyPIruff0.15.20indirecta
PyPIshellingham1.5.4indirecta
PyPIsix1.17.0indirecta
PyPIsoupsieve2.8.4indirecta
PyPItermcolor3.3.0indirecta
PyPItomli2.4.1indirecta
PyPItrove-classifiers2026.6.1.19indirecta
PyPItyper0.26.8indirecta
PyPItyping-extensions4.16.0indirecta
PyPItyping-inspection0.4.2indirecta
PyPIwatchdog6.0.0indirecta
PyPIwcwidth0.8.2indirecta
PyPIzensical0.0.47indirecta
Avisos de dependencias 0

Este repositorio no publica ningún paquete que el índice resuelva, así que se evaluó su propio grafo de dependencias — 67 paquetes, que incluyen también fijaciones de desarrollo y prueba que nunca se distribuyen: 0 tienen avisos conocidos, de los cuales 0 son directas.

Ningún aviso conocido afecta a las dependencias evaluadas.

Un aviso significa que la versión registrada en el grafo de dependencias cae dentro del rango afectado de un aviso. No se analiza la alcanzabilidad, y el grafo incluye fijaciones de desarrollo y prueba: un hallazgo puede referirse al utillaje y no al software distribuido.

Informe JSON sin procesar legible por máquina
{
  "data": {
    "repo": {
      "topics": [
        "asyncio",
        "asyncio-python",
        "lifx",
        "lifx-bulbs",
        "lifx-lan-protocol",
        "lifx-lights",
        "llms-txt"
      ],
      "is_fork": false,
      "size_kb": 23839,
      "has_wiki": false,
      "homepage": "https://djelibeybi.github.io/lifx-async/",
      "languages": {
        "HTML": 44167,
        "Python": 3449461
      },
      "pushed_at": "2026-07-31T09:02:03Z",
      "created_at": "2025-11-03T21:11:30Z",
      "owner_type": "User",
      "updated_at": "2026-07-31T09:02:41Z",
      "description": "A modern, type-safe, async Python library for controlling LIFX lights",
      "is_archived": false,
      "is_disabled": false,
      "license_spdx": "UPL-1.0",
      "default_branch": "main",
      "license_spdx_raw": "UPL-1.0",
      "primary_language": "Python",
      "significant_languages": [
        "Python"
      ]
    },
    "owner": {
      "blog": "https://omg.dje.li",
      "name": "Avi Miller",
      "type": "User",
      "login": "Djelibeybi",
      "company": null,
      "location": "Melbourne, Australia",
      "followers": 187,
      "avatar_url": "https://avatars.githubusercontent.com/u/103232?v=4",
      "created_at": "2009-07-09T10:53:07Z",
      "is_verified": null,
      "public_repos": 46,
      "account_age_days": 6230
    },
    "license": {
      "state": "standard",
      "spdx_id": "UPL-1.0",
      "raw_spdx": "UPL-1.0",
      "file_present": true,
      "scorecard_found": true,
      "profile_has_license": true
    },
    "activity": {
      "releases": [
        {
          "tag": "v6.1.1",
          "kind": "patch",
          "published_at": "2026-07-31T09:02:04Z"
        },
        {
          "tag": "v6.1.0",
          "kind": "minor",
          "published_at": "2026-07-31T04:40:19Z"
        },
        {
          "tag": "v6.0.1",
          "kind": "patch",
          "published_at": "2026-07-29T15:25:26Z"
        },
        {
          "tag": "v6.0.0",
          "kind": "major",
          "published_at": "2026-07-28T15:37:55Z"
        },
        {
          "tag": "v5.6.4",
          "kind": "patch",
          "published_at": "2026-07-26T12:23:55Z"
        },
        {
          "tag": "v5.6.3",
          "kind": "patch",
          "published_at": "2026-07-26T00:30:07Z"
        },
        {
          "tag": "v5.6.2",
          "kind": "patch",
          "published_at": "2026-07-25T20:55:13Z"
        },
        {
          "tag": "v5.6.1",
          "kind": "patch",
          "published_at": "2026-07-25T19:37:34Z"
        },
        {
          "tag": "v5.6.0",
          "kind": "minor",
          "published_at": "2026-07-18T01:18:16Z"
        },
        {
          "tag": "v5.5.1",
          "kind": "patch",
          "published_at": "2026-06-14T00:47:27Z"
        },
        {
          "tag": "v5.5.0",
          "kind": "minor",
          "published_at": "2026-06-13T17:59:47Z"
        },
        {
          "tag": "v5.4.9",
          "kind": "patch",
          "published_at": "2026-05-21T22:17:57Z"
        },
        {
          "tag": "v5.4.8",
          "kind": "patch",
          "published_at": "2026-04-17T11:33:21Z"
        },
        {
          "tag": "v5.4.7",
          "kind": "patch",
          "published_at": "2026-04-17T03:22:44Z"
        },
        {
          "tag": "v5.4.6",
          "kind": "patch",
          "published_at": "2026-03-22T13:08:28Z"
        },
        {
          "tag": "v5.4.5",
          "kind": "patch",
          "published_at": "2026-03-22T08:25:46Z"
        },
        {
          "tag": "v5.4.4",
          "kind": "patch",
          "published_at": "2026-03-21T19:40:00Z"
        },
        {
          "tag": "v5.4.3",
          "kind": "patch",
          "published_at": "2026-03-21T19:06:51Z"
        },
        {
          "tag": "v5.4.2",
          "kind": "patch",
          "published_at": "2026-03-21T07:05:12Z"
        },
        {
          "tag": "v5.4.1",
          "kind": "patch",
          "published_at": "2026-03-19T04:30:28Z"
        },
        {
          "tag": "v5.4.0",
          "kind": "minor",
          "published_at": "2026-03-18T06:32:40Z"
        },
        {
          "tag": "v5.3.0",
          "kind": "minor",
          "published_at": "2026-03-18T02:52:58Z"
        },
        {
          "tag": "v5.2.1",
          "kind": "patch",
          "published_at": "2026-03-18T01:59:53Z"
        },
        {
          "tag": "v5.2.0",
          "kind": "minor",
          "published_at": "2026-02-07T11:43:09Z"
        },
        {
          "tag": "v5.1.1",
          "kind": "patch",
          "published_at": "2026-02-06T04:15:18Z"
        },
        {
          "tag": "v5.1.0",
          "kind": "minor",
          "published_at": "2026-01-24T04:06:14Z"
        },
        {
          "tag": "v5.0.1",
          "kind": "patch",
          "published_at": "2026-01-14T09:20:46Z"
        },
        {
          "tag": "v5.0.0",
          "kind": "major",
          "published_at": "2026-01-12T06:11:04Z"
        },
        {
          "tag": "v4.9.0",
          "kind": "minor",
          "published_at": "2025-12-30T03:29:55Z"
        },
        {
          "tag": "v4.8.1",
          "kind": "patch",
          "published_at": "2025-12-24T22:16:47Z"
        },
        {
          "tag": "v4.8.0",
          "kind": "minor",
          "published_at": "2025-12-20T02:12:21Z"
        },
        {
          "tag": "v4.7.5",
          "kind": "patch",
          "published_at": "2025-12-16T12:28:50Z"
        },
        {
          "tag": "v4.7.4",
          "kind": "patch",
          "published_at": "2025-12-16T11:51:46Z"
        },
        {
          "tag": "v4.7.3",
          "kind": "patch",
          "published_at": "2025-12-16T06:49:49Z"
        },
        {
          "tag": "v4.7.2",
          "kind": "patch",
          "published_at": "2025-12-16T02:32:31Z"
        },
        {
          "tag": "v4.7.1",
          "kind": "patch",
          "published_at": "2025-12-13T10:54:04Z"
        },
        {
          "tag": "v4.7.0",
          "kind": "minor",
          "published_at": "2025-12-13T09:26:38Z"
        },
        {
          "tag": "v4.6.1",
          "kind": "patch",
          "published_at": "2025-12-12T08:23:38Z"
        },
        {
          "tag": "v4.6.0",
          "kind": "minor",
          "published_at": "2025-12-11T17:32:19Z"
        },
        {
          "tag": "v4.5.1",
          "kind": "patch",
          "published_at": "2025-12-11T14:37:14Z"
        },
        {
          "tag": "v4.5.0",
          "kind": "minor",
          "published_at": "2025-12-08T03:38:52Z"
        },
        {
          "tag": "v4.4.1",
          "kind": "patch",
          "published_at": "2025-12-03T18:19:53Z"
        },
        {
          "tag": "v4.4.0",
          "kind": "minor",
          "published_at": "2025-11-29T06:57:02Z"
        },
        {
          "tag": "v4.3.9",
          "kind": "patch",
          "published_at": "2025-11-27T04:46:19Z"
        },
        {
          "tag": "v4.3.8",
          "kind": "patch",
          "published_at": "2025-11-25T03:31:25Z"
        },
        {
          "tag": "v4.3.7",
          "kind": "patch",
          "published_at": "2025-11-25T01:24:51Z"
        },
        {
          "tag": "v4.3.6",
          "kind": "patch",
          "published_at": "2025-11-25T00:33:05Z"
        },
        {
          "tag": "v4.3.5",
          "kind": "patch",
          "published_at": "2025-11-22T17:52:03Z"
        },
        {
          "tag": "v4.3.4",
          "kind": "patch",
          "published_at": "2025-11-22T17:03:50Z"
        },
        {
          "tag": "v4.3.3",
          "kind": "patch",
          "published_at": "2025-11-22T15:28:05Z"
        },
        {
          "tag": "v4.3.2",
          "kind": "patch",
          "published_at": "2025-11-22T13:05:39Z"
        },
        {
          "tag": "v4.3.1",
          "kind": "patch",
          "published_at": "2025-11-22T11:06:05Z"
        },
        {
          "tag": "v4.3.0",
          "kind": "minor",
          "published_at": "2025-11-22T10:39:49Z"
        },
        {
          "tag": "v4.2.1",
          "kind": "patch",
          "published_at": "2025-11-21T02:34:29Z"
        },
        {
          "tag": "v4.2.0",
          "kind": "minor",
          "published_at": "2025-11-21T00:21:18Z"
        },
        {
          "tag": "v4.1.0",
          "kind": "minor",
          "published_at": "2025-11-20T11:49:03Z"
        },
        {
          "tag": "v4.0.2",
          "kind": "patch",
          "published_at": "2025-11-19T03:16:44Z"
        },
        {
          "tag": "v4.0.1",
          "kind": "patch",
          "published_at": "2025-11-18T18:37:32Z"
        },
        {
          "tag": "v4.0.0",
          "kind": "major",
          "published_at": "2025-11-18T11:52:42Z"
        },
        {
          "tag": "v3.1.0",
          "kind": "minor",
          "published_at": "2025-11-17T13:31:10Z"
        },
        {
          "tag": "v3.0.1",
          "kind": "patch",
          "published_at": "2025-11-17T03:48:14Z"
        },
        {
          "tag": "v3.0.0",
          "kind": "major",
          "published_at": "2025-11-16T23:55:04Z"
        },
        {
          "tag": "v2.2.2",
          "kind": "patch",
          "published_at": "2025-11-14T21:12:38Z"
        },
        {
          "tag": "v2.2.1",
          "kind": "patch",
          "published_at": "2025-11-14T18:55:39Z"
        },
        {
          "tag": "v2.2.0",
          "kind": "minor",
          "published_at": "2025-11-14T14:55:08Z"
        },
        {
          "tag": "v2.1.0",
          "kind": "minor",
          "published_at": "2025-11-14T05:02:39Z"
        },
        {
          "tag": "v2.0.0",
          "kind": "major",
          "published_at": "2025-11-14T03:04:49Z"
        },
        {
          "tag": "v1.3.1",
          "kind": "patch",
          "published_at": "2025-11-12T15:30:09Z"
        },
        {
          "tag": "v1.3.0",
          "kind": "minor",
          "published_at": "2025-11-10T14:57:22Z"
        },
        {
          "tag": "v1.2.1",
          "kind": "patch",
          "published_at": "2025-11-08T07:17:21Z"
        },
        {
          "tag": "v1.2.0",
          "kind": "minor",
          "published_at": "2025-11-07T12:46:55Z"
        },
        {
          "tag": "v1.1.3",
          "kind": "patch",
          "published_at": "2025-11-06T09:41:34Z"
        },
        {
          "tag": "v1.1.2",
          "kind": "patch",
          "published_at": "2025-11-05T12:26:26Z"
        },
        {
          "tag": "v1.1.1",
          "kind": "patch",
          "published_at": "2025-11-05T12:06:39Z"
        },
        {
          "tag": "v1.0.0",
          "kind": "major",
          "published_at": "2025-11-04T04:01:23Z"
        }
      ],
      "recent_commits": [
        {
          "oid": "e4f8c95f6500172276da2f45dc9b8a2e7e896ab0",
          "body": "Automatically generated by python-semantic-release",
          "is_bot": false,
          "headline": "6.1.1",
          "author_name": "semantic-release",
          "author_login": null,
          "committed_at": "2026-07-31T09:01:59Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0251e08a21c1d6f20410e3166c7e3fbfe0f4dd0f",
          "body": "Nothing propagated transport death up to the layers that decide whether to\nreopen. _UdpProtocol.connection_lost() cleared the protocol's own transport\nreference, but UdpTransport._transport/._protocol stayed populated, so\nis_open kept returning True, open() early-returned \"already_open\", and\nsend() \n[…]\nket itself is invalid, and asyncio never\ncalls connection_lost for them.\n\nRequest timeouts deliberately signal nothing. Only genuine endpoint death\nresets state.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(network): recover from UDP endpoint death instead of hanging forever",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-31T08:57:16Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "7e335439b90a4ea7296098d2d93d3bc195d59cd3",
          "body": "HSBK.__init__ applied user-input validation to values the firmware sent.\nvalidate_kelvin() rejected kelvin 0, which is what a fully saturated zone\ncarries on the wire, so get_all_tile_colors() crashed on a real Tile:\n\n    ValueError: Kelvin must be 1500-9000, got 0\n\nMIN_KELVIN-MAX_KELVIN is the whit\n[…]\nffect.__post_init__ (no inbound rule\ncan fire), TileInfo.from_protocol (no validation) and Serial.from_protocol\n(padding rejection is deliberate DoS protection).\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(color): accept device-reported kelvin 0 on inbound reads",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-31T08:57:16Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "8d9a666a8219040662eb61dea50530900938eaae",
          "body": "Add a [project.urls] table to pyproject.toml using the labels defined in\nthe PyPA well-known project URLs specification (Homepage, Documentation,\nSource, Changelog, Releasenotes, Issues) so PyPI renders them with the\ncorrect icons.\n\nCorrect several inaccurate claims in the feature lists:\n\n- Remove t\n[…]\ne\n  device classes documentation so it cannot drift again.\n\nAlso document features that were missing: mDNS discovery, the theme\nlibrary, and the animation layer.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs: add project URLs and correct stale feature claims",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-31T05:07:13Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "aa240a2ae43afb2488a1ac6944ebb88ba812a0f9",
          "body": "Automatically generated by python-semantic-release",
          "is_bot": false,
          "headline": "6.1.0",
          "author_name": "semantic-release",
          "author_login": null,
          "committed_at": "2026-07-31T04:40:14Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "d61f6e39071474baca2ca6bd6582295f49925534",
          "body": "- refresh_state() re-fetches the volatile state (label, power, colour, opt-in\n  readings, zones and tiles), not \"everything\": the semi-static fields stay as\n  initialisation left them\n- packets 401 and 402 are part of the published LAN protocol and are merely\n  absent from protocol.yml, so they are \n[…]\n clears its field, and that an ambient\n  reading taken while the light is on is stored as -1.0\n\nCo-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs: correct refresh_state and ambient light sensor descriptions",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-31T04:36:32Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "b85ab4a8e3c53769626eeb844ba5049d5c0c58e3",
          "body": "The integration imports these from their defining modules today. Export them\nfrom the package root so it can import them from lifx directly:\n\n- STATE_REFRESH_DEBOUNCE_MS and INVALID_AMBIENT_LIGHT_RESPONSE (lifx.const)\n- CollectionInfo and DeviceCapabilities (lifx.devices.base)\n- HevConfig and HevCyc\n[…]\n\n\nDeviceCapabilities is also exported from lifx.devices, alongside the other base\ndataclasses.\n\nCo-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "feat: export symbols used by the Home Assistant integration",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-31T04:36:32Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "118b6331b341b6e0d9926ab2975c0b17bfc4ad4e",
          "body": "Addresses the max-effort review of this branch:\n\n- set_group() wrote the new group UUID and label into state.location, leaving\n  state.group stale for the lifetime of the device\n- Device.refresh_state() stamped last_updated without re-reading anything, so\n  is_fresh() reported pre-change power and l\n[…]\non_requests() hold the batch shared\n  by Device and Light instead of two near-identical copies\n\nCo-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(devices): harden state fetches and correct set_group target",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-31T04:36:32Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "c6ad2c47ccfd2b7b816dd0ba58186af913ad611f",
          "body": "_initialize_state() schedules every request up front, so a failure\npart-way through awaiting them leaves the rest pending and relies on\n_discard_pending() to cancel and consume them. Nothing exercised that\ncancel: the existing failure test lets every other request complete\nimmediately, so each task was already done.\n\nFail one request while another is still in flight and assert the\npending one is cancelled, taking patch coverage of the branch to 100%.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "test(devices): cover cancellation of in-flight state requests",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-31T04:36:32Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "4aff9c633b1cf621789650adb75a4954b1fd1e80",
          "body": "Light.__init__ was an untyped *args/**kwargs passthrough that only\ncalled super(), which erased the constructor signature for Light and\nevery subclass: no call site could be type checked, including typos in\nthe new fetch flags. Deleting it inherits Device.__init__ as written.\n\nfetch_wifi_info and fe\n[…]\ne.py and\nlight.py. Unlike the deferred lifx imports around them, these are not\ncircular-import workarounds - time is already imported at the top of\nboth modules.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "refactor(devices): restore Light constructor typing, tidy imports",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-31T04:36:32Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "2df05cb04e2aa6c2930ef9f7b9995e5deeec7300",
          "body": "Device.refresh_state() claimed to fetch label and power, which it has\nnever done on an already-initialised device: it re-queries only the\nopt-in WiFi reading and stamps last_updated. Describe both paths - full\npopulation via _initialize_state() when there is no state yet, cached\nsemi-static fields l\n[…]\nso documented a RuntimeError for uninitialised\nstate that it cannot raise, because the uninitialised path initialises\ninstead. Drop it from all seven docstrings.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(devices): correct refresh_state docstrings",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-31T04:36:32Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "a7425b1e0174f7bfc677cb349c052b6d5a99a443",
          "body": "Discovery functions take no fetch_wifi_info or fetch_ambient_light\nargument, so discovered devices start with both flags off. The property\nhas to be set before `async with device`, because that is what runs\nstate initialisation - setting it inside the body silently misses the\nfirst fetch and leaves the field None until a full refresh.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs: explain opt-in state fields on discovered devices",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-31T04:36:32Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "23e27f7c8ba0df9a74630176da17b9a5f000e9bd",
          "body": "…ties\n\nAddress the PR #184 review and extend the same pattern to the ambient\nlight sensor.\n\nfetch_wifi_info and fetch_ambient_light are now settable properties on\nDevice as well as constructor arguments, so a caller starts or stops\ncollecting either reading by toggling the flag rather than passing a\n[…]\nlso re-fetches every zone or tile.\n- tests cover the wifi_info and ambient_light keys in as_dict, and the\n  already-initialized branch of Device.refresh_state().\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "feat(devices): add ambient light to state and make fetch flags proper…",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-31T04:36:32Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "aab64f7234f58a17fcc7ab74409e8818c59354fc",
          "body": "`refresh_state()` now takes an optional `fetch_wifi_info` argument on every\ndevice class, so an existing device can pick up a WiFi signal reading (or skip\none) without being reconstructed. None keeps the instance setting from the\nconstructor.\n\nWhen state has not been initialised yet, `refresh_state(\n[…]\nnce default; an override that\nenables the query then issues the single extra request it needs.\n\nCo-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "feat(devices): allow refresh_state to override fetch_wifi_info",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-31T04:36:32Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "3ddf7f03956c77cf9e0452baf2f5d95f6e448b77",
          "body": "Add a `wifi_info` field to `DeviceState` so WiFi signal strength is part of\nthe device state snapshot rather than requiring a separate `get_wifi_info()`\ncall.\n\nQuerying the device for the signal is opt-in via a new `fetch_wifi_info`\nparameter on `Device` (also plumbed through `from_ip()`, `connect()\n[…]\nre now optional, and\n`WifiInfo` gains an `as_dict` property consumed by `DeviceState.as_dict`.\n\nCo-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "feat(devices): add wifi_info to DeviceState",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-31T04:36:32Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "fbc7096d257e6c0fd805ed392a1a4913dd065b88",
          "body": "Automatically generated by python-semantic-release",
          "is_bot": false,
          "headline": "6.0.1",
          "author_name": "semantic-release",
          "author_login": null,
          "committed_at": "2026-07-29T15:25:23Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "6377cc2ad3e61064902b9411e9ecc23e91c233f2",
          "body": "Signed-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix: switch all examples to use Device.connect() instead of from_ip()",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-29T15:20:46Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "be2897610c6539e8f0feb5642e6ecc0d6453487d",
          "body": "SKY is not available on every matrix product: it ships with host firmware\n4.x, so matrix devices on earlier firmware reject or ignore it. It is\nconfirmed on Ceiling, Luna, Tube, Path and the E26 Candle, and the\ndiscontinued Tile is permanently excluded by its terminal 3.50 firmware.\nSending SKY to a\n[…]\nSKY set_effect() with LifxUnsupportedCommandError before any packet\nreaches the wire when the device lacks the matrix capability or runs\nfirmware older than 4.0.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(matrix): gate the SKY effect on capability and firmware",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-29T15:20:46Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "a7db2bfe87b179b31d725dbff59248a8775d0f31",
          "body": "Both generators emitted valid but unformatted Python, so regenerating\nfrom an unchanged upstream spec still produced a ~950 line diff until\nruff format was run by hand. Run ruff format and ruff check --fix over\nthe output instead, mirroring the CI generated-files job.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "build(generators): format generated code with ruff",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-29T15:20:46Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "04405bebbd49fe6c69f3012c001d182ded8331fd",
          "body": "Products 265 and 266 are the US and international LIFX Ceiling 13\", the\nsame 13x13 inch form factor as products 176 and 177, so they share the\n8x8 layout with the uplight on zone 63. Without the layout they fell\nthrough to MatrixLight and lost component control entirely.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(products): add the Ceiling 13\" component layout",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-29T15:20:46Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f4cf62326183e010f98d4c55999145bb550c7c82",
          "body": "theme_paint.py paints a built-in or custom palette onto bulbs, multizone\nstrips and matrix devices in a single DeviceGroup.apply_theme() call, and\nreports the paint geometry of each target before applying it. It also\nexercises the DeviceGroup.apply_theme() dispatch fix end to end.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(examples): add a theme painting example",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-29T15:20:46Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "2ee2f4cf4df053751633092bca7723d3f6e2309a",
          "body": "DeviceGroup.apply_theme() fanned out per device class: self.lights holds\nevery Light subclass, so multizone and matrix devices were painted twice\nconcurrently, racing a single random colour against their zone or tile\ngradient. CeilingLight missed the gradient entirely, because the\nmatrix_lights filter uses type() rather than isinstance().\n\nDispatch polymorphically over self.lights instead, so each device runs\nits own apply_theme() override exactly once.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(api): apply themes once per device",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-29T15:20:46Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "bb67c949247deeadd19590709b3a2bc10d8fb541",
          "body": "Automatically generated by python-semantic-release",
          "is_bot": false,
          "headline": "6.0.0",
          "author_name": "semantic-release",
          "author_login": null,
          "committed_at": "2026-07-28T15:37:52Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "d73a5302661bde33a3e3d5cb65587a38073bd471",
          "body": "The MultiZone Control example read every zone with no counterpart write,\nso a reader still reached for set_extended_color_zones -- the call that\nbreaks on legacy firmware and past 82 zones.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(devices): show set_all_color_zones in the multizone example",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f712d681a1220427d4ca225ab18177c61982595f",
          "body": "brightness_uint8 rounded, so every wire brightness below 129 of 65535\ncame back as 0. Home Assistant reads a light that is on at brightness 0\nas off and writes that 0 back, switching the light off -- reachable\nwhenever an effect fades through near-zero.\n\nAlso pin the hue quantisation with exact expe\n[…]\nion used approx(abs=1), a tolerance as wide as the one-LSB bias the\nscale-then-round fix removed, so the old expression could come back\nwithout failing anything.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(color): report at least brightness 1 for a lit light",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "4f561e2857d0a9cb3e31c566bcec3d42b8a9871c",
          "body": "_restore_zones still called set_extended_color_zones with every captured\nzone, so a strip longer than 82 zones raised ValueError. The broad\nhandler swallowed it, leaving the strip frozen on the effect's last frame\nwith only a warning logged.\n\nDelegating to set_all_color_zones picks the packet type from\ncapabilities and chunks past the extended limit, so the branch here goes\naway as well.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(effects): restore zones via set_all_color_zones",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "632bb82814cc7ff727051a67571bf3f9f8991c39",
          "body": "APPLY_ONLY tells the device to ignore the colours carried by the message\nand flush only what is already buffered. Forwarded to the final packet of\na split write, it discarded that chunk while every earlier chunk stayed\nbuffered, so a long strip came back half written with nothing raised.\n\nAlso state\n[…]\n only checked against a cached zone\ncount. The design spec deliberately never fetches one just to validate,\nbut the docstring read as an unconditional guarantee.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(multizone): reject APPLY_ONLY in set_all_color_zones",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "71d47e13b3cdef11e07f15ac93c49ec32e4bed89",
          "body": "LightState.as_dict called dataclasses.asdict, which bypassed\nDeviceState.as_dict entirely: light states lost has_variable_color_temp,\nkept None kelvin bounds instead of the 1500/9000 defaults, and leaked the\nfirmware build that DeviceState deliberately omits.\n\nEvery state now extends its parent and \n[…]\nndex. JSON object keys are always\nstrings, so the keys are stringified here rather than letting json.dumps\ncoerce them and break every lookup after a round trip.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(devices): build light state dicts from DeviceState.as_dict",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "c006899f8c904c3f18f97a5a196457bbbb7b78a8",
          "body": "as_dict became a property, but the discovery example still called it, so\nrunning it died with \"TypeError: 'dict' object is not callable\" before\nprinting a single device. CI never caught it: pyright only includes src\nand pytest only collects tests.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(examples): call HSBK.as_dict as a property",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "9cc7258e54f607cf0deb278f40a51c6423efb297",
          "body": "The counterpart to get_all_color_zones: writes zone colors using whichever\npacket the device supports, so callers no longer need to pick between\nSetColorZones and SetExtendedColorZones, know the 82-colour cap, or sequence\nNO_APPLY/APPLY themselves.\n\n    colors = await light.get_all_color_zones()\n   \n[…]\n hardcoded\nto start at zone 0. apply_theme now calls the public method.\n\nDesign and rejected alternatives:\n.claude/specs/2026-07-28-set-all-color-zones-design.md\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "feat(multizone): add set_all_color_zones",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "dd5e8e97e671b578088fbad5d2cb3c6162632a4a",
          "body": "Design for the public multizone setter that mirrors get_all_color_zones,\ngeneralising the private _write_zone_colors added in 05fcfba to a windowed\nwrite with a caller-supplied apply mode.\n\nRecords why the getter unified trivially and the setter does not: the two\nwrite packets take different shapes \n[…]\n38 below firmware 2.77.\n\nLives in .claude/ rather than docs/ so it is not published to the docs site,\nand uses chore: so it stays out of the generated changelog.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "chore: spec set_all_color_zones",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "8101d991cd8756a1b6b362ea910e16769549b888",
          "body": "apply_theme called set_extended_color_zones(0, colors) directly, with no\ncapability check and no chunking. That meant it raised\nLifxUnsupportedCommandError on firmware without extended multizone, and\nValueError (\"Too many colors\") on any strip with more than 82 zones, such as a\nchained LIFX Z run.\n\n\n[…]\nuct id and set\n_capabilities from the registry; the multizone fixture uses product 32\n(LIFX Z, extended) and the new fallback test uses product 31 (no extended).\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(multizone): make apply_theme work on legacy and long strips",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b4913394f681733f15dd18a853cd1ab1cb04144e",
          "body": "HSBK is a plain class, not a dataclass, so `dataclasses.asdict` left every\ncolor field as an HSBK object and its own `as_dict` was never consulted. No\ndevice state was JSON-serialisable as a result.\n\n`LightState.as_dict` now expands `color`, and the subclasses build on it via\n`super().as_dict` inste\n[…]\nxists there. They are replaced by test_state_serialisation.py,\nwhich asserts against real instances of all six state classes and checks each\nsurvives json.dumps.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(devices): expand HSBK colors in every state's as_dict",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "626f688fab353c2218731583dae6ee73c1ba8c8c",
          "body": "BREAKING CHANGE: HSBK.as_dict is now a property. Callers using `color.as_dict()`\nmust drop the parentheses; calling it raises\n`TypeError: 'dict' object is not callable`.\n\nEvery other class exposing as_dict — TileInfo, DeviceCapabilities,\nFirmwareInfo, CollectionInfo, DeviceState, MatrixLightState and LifxStruct —\nalready exposes it as a property. HSBK was the sole method.\n\nLedFx is the only known consumer and does not use as_dict on any state class.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "refactor(color)!: make HSBK.as_dict a property",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "5a96dc57d602ad0048086bea52dc10f4bf489ce4",
          "body": "`CeilingLightState.as_dict` returned `downlight_zones` as a raw `slice`, which\n`dataclasses.asdict` passes through untouched and which no serialiser can\nhandle. It is now expanded to `{\"start\": x, \"stop\": y, \"step\": z}`.\n\nThe layouts in quirks.py build `slice(0, 63)` and `slice(0, 127)`, so\n`slice.s\n[…]\ntionally, and the result compares unequal to a step-None original despite\nindexing identically.\n\nTwo existing tests asserted the old passthrough and are updated.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "refactor(ceiling): expand downlight_zones slice in as_dict()",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "c8653b0f61ad255b5769dde0fb3477159059e903",
          "body": "`round(x)` with no ndigits already returns an int; only `round(x, n)` returns\na float. The wrappers in framebuffer.py and infrared.py were no-ops.\n\nThese were the last two occurrences in the tree; the ones in color.py and\naurora.py were removed alongside the hue quantisation fix.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "refactor: drop redundant int() around round()",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "815f037fdee995f52b6ae06d8980ffb6768d6e43",
          "body": "Add pre-scaled views of the HSBK components for consumers that do not want\nthe internal 0.0-1.0 fractions:\n\n- `saturation_pct` / `brightness_pct` return unrounded percentages (0.0-100.0)\n- `brightness_uint8` returns an 8-bit value (0-255)\n\nHome Assistant wants `(hue, saturation_pct)` for a light's `\n[…]\nrate_frame()` and\n`generate_protocol_frame()`.\n\nThe redundant `int()` wrappers around `round()` in both files are dropped;\nbare `round()` already returns an int.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "feat(color): add scaled HSBK accessors and replace()",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T15:34:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "378e1e63d38995abacf657bd59305a84411f572b",
          "body": "Signed-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "chore: update copyright year to include 2026",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-28T09:03:01Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "c5191102786437c28fb4bf171c76454bc6727e6c",
          "body": "Automatically generated by python-semantic-release",
          "is_bot": false,
          "headline": "5.6.4",
          "author_name": "semantic-release",
          "author_login": null,
          "committed_at": "2026-07-26T12:23:53Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "55de0d247a5de2355104f28faaaa94053cedd2b3",
          "body": "Signed-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(devices): address multizone review feedback",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-26T12:20:31Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "64d6dfcdeaf1fae46e8ad91c16762c15bce0ea1e",
          "body": "Signed-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "test: cover Home Assistant migration fix paths",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-26T12:20:31Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "5507c41b87bd27ab7f159666c28cb7ebf779b1ca",
          "body": "Signed-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix: address Home Assistant migration issues",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-26T12:20:31Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "80d567da19109119c151e24c89bad5d369bbeea1",
          "body": "Automatically generated by python-semantic-release",
          "is_bot": false,
          "headline": "5.6.3",
          "author_name": "semantic-release",
          "author_login": null,
          "committed_at": "2026-07-26T00:30:02Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "83f3d256da5d179faf17ba97011b2659e07c7593",
          "body": "The v1.1 close-out planning docs recorded the idle-window fix under its\npre-rebase hash 3d16822, which no longer exists on main -- PR #176 landed as\ndca9e39. STATE.md, MILESTONES.md, the v1.1 milestone audit and the quick-task\nsummary now cite the real commit.\n\nThe quick-task summary also records the 9afe8df follow-up (PR #177): the\nreplacement docstring Note overstated the guarantee, and the two regression\ntests were hardened.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(planning): refresh the discovery idle-window commit refs",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-26T00:26:29Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "fe8effcd3d714bcf80347f90d207d4acce34707d",
          "body": "The generated entry pulled one bullet per SUMMARY.md, which included deviation headings\nand a stray 'H2' rather than accomplishments. Replaced with six requirement-grouped\noutcomes carrying their measured figures, plus the closeout type, override count and\ndeferred items.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs: curate the v1.1 milestone entry",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-26T00:26:29Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f8be2bb2220ff2f55b3898cf8781c95df063b015",
          "body": "Archived to .planning/milestones/v1.1-REQUIREMENTS.md with all 13 requirements marked\ncomplete. /gsd-new-milestone writes a fresh one for the next milestone; history is\npreserved via git rm rather than a plain delete.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "chore: remove REQUIREMENTS.md for the v1.1 milestone",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-26T00:26:29Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "93b8e88078fac2bb3db46080a500e19c8753d098",
          "body": "Archives the v1.1 roadmap, requirements, audit and phase directories to\n.planning/milestones/, collapses ROADMAP.md to a milestone-grouped summary, evolves\nPROJECT.md (v1.1 moved to shipped, carried-forward candidates listed under Active, two\nnew Key Decisions recorded), records the v1.1 retrospecti\n[…]\n with cross-milestone trends,\nand notes the acknowledged deferred items in STATE.md.\n\nSafety checkpoint before REQUIREMENTS.md is removed for the next milestone.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "chore: archive v1.1 milestone files",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-26T00:26:29Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "7337d0aee4bc1ec2804f5c7ae3c3dd4296dc2234",
          "body": "…e two audit items\n\nAdds the quick task's plan and summary, the STATE.md Quick Tasks row, and updates\nv1.1-MILESTONE-AUDIT.md: the api.discover() idle-window warning and phase 04's\nunreconciled VALIDATION.md are both resolved, so Nyquist coverage is now 4/4 compliant.\nThe missing 02-01 frontmatter and the dormant SEED-001 remain deferred.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(quick-260726-824): record the discovery idle-window fix and clos…",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-26T00:26:29Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f9929c3d7dc4b6192871a97407edbeee0f5b6680",
          "body": "…one audit\n\n04-VALIDATION.md still carried the plan-time seed: status draft, every row pending,\nnyquist_compliant false. Nothing had reconciled it after execution, so the milestone\naudit read phase 4 as NOT-VALIDATED even though the coverage exists.\n\nAudited retroactively: all four Wave 0 artefacts \n[…]\ns, the substantive one being that api.discover() awaits create_device() inside the\ndiscovery loop, letting one dead-but-discovered device starve the idle window.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(phase-04): reconcile validation strategy and add the v1.1 milest…",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-26T00:26:29Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "9afe8df68260c6940173a765306441f2568c5fa7",
          "body": "Follow-up to dca9e39 from a max-effort review of PR #176.\n\nThe docstring claimed a consumer 'cannot expire discovery early', which is\nfalse in the default configuration: DEFAULT_REQUEST_TIMEOUT (16 s) outlasts\nDISCOVERY_TIMEOUT (15 s), so a single unreachable device still ends the sweep\n-- via the o\n[…]\nidle-reset invariant next to the reset (sends deliberately do\nnot reset it), and replaces _make_recording_send([]) with an explicit\n_make_ignoring_send() helper.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(discovery): correct the idle-window docs and harden its tests",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-26T00:19:56Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "79814c8e2ef45ca48dbc3316ab35de60b12fd7ab",
          "body": "Automatically generated by python-semantic-release",
          "is_bot": false,
          "headline": "5.6.2",
          "author_name": "semantic-release",
          "author_login": null,
          "committed_at": "2026-07-25T20:55:09Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "dca9e39e3dc1a1fc684b8638597ff8219d1dbf2c",
          "body": "…indow\n\nThe idle timer was reset only before a response was yielded, so every second\nthe consumer spent counted against it. api.discover() constructs a Device per\nyielded response, and those requests carry a 16s wall deadline against a ~4s\nidle window: one device that answered GetService and then we\n[…]\nthat answers afterwards (verified to fail\nwithout the fix -- the second device is stranded), and one asserting the\noverall deadline still bounds a slow consumer.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(discovery): stop consumer time from expiring the discovery idle w…",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-25T20:51:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b635c5a28e18aff5a3952db3b3390dd2e9f197ba",
          "body": "Automatically generated by python-semantic-release",
          "is_bot": false,
          "headline": "5.6.1",
          "author_name": "semantic-release",
          "author_login": null,
          "committed_at": "2026-07-25T19:37:28Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "786ad344909a1192d8cebc46de5062005c681510",
          "body": "Signed-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(quick-260726-42c): ship quick task — PR #175 [ci skip]",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-25T19:34:56Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "bdb84f7739311291bf5f36b315211bf65e51b5a5",
          "body": "…it script\n\nget_mac_address() incremented the final octet of the serial for every\nversion_major == 3 firmware, so it returned a MAC no device had. A fleet sweep\nfound LIFX Tiles on firmware 3.50 whose real ARP MAC matched their serial\nexactly.\n\nThe offset now requires version_major == 3 and version_\n[…]\n removed\nmajor-only rule to the docs site and the context7 corpus, and AGENTS.md -- the\ntracked Codex mirror of CLAUDE.md -- still carried the superseded gotcha.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(devices): narrow the MAC offset to firmware 3.70+ and add the aud…",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-25T19:33:57Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "010e9d195b20594b6cc0d2dff03ad57aeef54afe",
          "body": null,
          "is_bot": true,
          "headline": "chore(deps): update actions/setup-python action to v7",
          "author_name": "renovate[bot]",
          "author_login": "renovate[bot]",
          "committed_at": "2026-07-25T19:13:11Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "681924522ac484678af6cdd4dbfb3bdd824ad0e3",
          "body": null,
          "is_bot": true,
          "headline": "chore(deps): update astral-sh/setup-uv action to v9",
          "author_name": "renovate[bot]",
          "author_login": "renovate[bot]",
          "committed_at": "2026-07-25T19:08:19Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "d52c856cf933e64d98393234be71995549f7c9c1",
          "body": "Signed-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "chore: commit and upload spike findings",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-25T16:15:54Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "fd2104f88dc97ef7b406509a29c8fafec4f00609",
          "body": null,
          "is_bot": true,
          "headline": "chore(deps): update pypa/gh-action-pypi-publish digest to ba38be9",
          "author_name": "renovate[bot]",
          "author_login": "renovate[bot]",
          "committed_at": "2026-07-22T22:13:24Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "71082bcf27929ac51a8991450519bc6e166cd429",
          "body": null,
          "is_bot": true,
          "headline": "chore(deps): update actions/checkout digest to 3d3c42e",
          "author_name": "renovate[bot]",
          "author_login": "renovate[bot]",
          "committed_at": "2026-07-22T20:44:42Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "cc34f64bca863adbf270442374d353da1cc4fef8",
          "body": null,
          "is_bot": true,
          "headline": "chore(deps): update actions/labeler action to v7",
          "author_name": "renovate[bot]",
          "author_login": "renovate[bot]",
          "committed_at": "2026-07-22T20:24:46Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f701a8fe64f8f3186019d0e94d5bd89a4b595bd8",
          "body": "Automatically generated by python-semantic-release",
          "is_bot": false,
          "headline": "5.6.0",
          "author_name": "semantic-release",
          "author_login": null,
          "committed_at": "2026-07-18T01:18:14Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "d99a501b2f896a596e45e71b57e1e06e1d3fa15c",
          "body": "Ack-gated animation flow control, retry/deadline hardening, discovery\nrebroadcast, reliability docs, and phase planning artifacts for the v1.1\nmilestone.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "feat: wire reliability improvements (v1.1)",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-07-18T01:14:55Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "2b9d41c78499161d1dfb7f4dee037eb97c4f488c",
          "body": null,
          "is_bot": true,
          "headline": "chore(deps): update astral-sh/setup-uv action to v8.3.2",
          "author_name": "renovate[bot]",
          "author_login": "renovate[bot]",
          "committed_at": "2026-07-11T22:31:51Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "fbc03af01137e41e6d486ce90a34a4bd3b9e92d5",
          "body": null,
          "is_bot": true,
          "headline": "chore(deps): update actions/labeler digest to b8dd2d9",
          "author_name": "renovate[bot]",
          "author_login": "renovate[bot]",
          "committed_at": "2026-07-11T22:20:57Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "eeb63a5862dc10adcd1979be7a47b81b8c07e2c8",
          "body": null,
          "is_bot": true,
          "headline": "chore(deps): update lock file",
          "author_name": "renovate[bot]",
          "author_login": "renovate[bot]",
          "committed_at": "2026-07-06T23:46:42Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "d70d0efedea5f63b369cb92d941df0ea25e4c4f3",
          "body": null,
          "is_bot": true,
          "headline": "chore(deps): update astral-sh/setup-uv action to v8.3.0",
          "author_name": "renovate[bot]",
          "author_login": "renovate[bot]",
          "committed_at": "2026-07-06T23:42:23Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "9b496df28a53ff0baa65a405ad7537b01510f3e2",
          "body": null,
          "is_bot": true,
          "headline": "chore(deps): update actions/setup-python digest to ece7cb0",
          "author_name": "renovate[bot]",
          "author_login": "renovate[bot]",
          "committed_at": "2026-06-26T12:12:39Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "eae31b6300891e838111afe469210b8508e4b9fc",
          "body": null,
          "is_bot": true,
          "headline": "chore(deps): update actions/checkout action to v7",
          "author_name": "renovate[bot]",
          "author_login": "renovate[bot]",
          "committed_at": "2026-06-20T02:18:46Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "cb30f424b717c9853105807866dea22ebad14b0e",
          "body": "Automatically generated by python-semantic-release",
          "is_bot": false,
          "headline": "5.5.1",
          "author_name": "semantic-release",
          "author_login": null,
          "committed_at": "2026-06-14T00:47:23Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "08b7e8d9c23708b10aa2ebde27e7765a3f487dd1",
          "body": "_is_stored_state_valid() compared stored vs current hue/saturation as\nraw floats. Now that those expose unrounded decoded values, a color read\nback from the device has the same wire tuple but slightly different raw\nfloats, so stored-state validity could incorrectly fail after a protocol\nround-trip.\n\n[…]\nound-trip regression tests for both uplight and downlight.\n\nReported in code review of #160.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(ceiling): compare stored state at uint16 wire granularity",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-14T00:44:21Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "515dd91d018ee5481602ee0ad9b270e0fe1eef8f",
          "body": "average(), lerp_hsb() and lerp_oklab() rounded their results to integer\nhue and 2dp saturation/brightness. Those outputs feed the effects layer\n(~10 effects via lerp_oklab) and the theme canvas/generators (average),\nall of which serialise to uint16 — so the 2dp rounding coarsened smooth\ngradients an\n[…]\nserted hue was an int (an artefact\nof the old rounding) now assert a numeric value in range.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(color): stop rounding computed lerp/average outputs",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-14T00:44:21Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "80b0b2f3a0289b7c635461dd5ae3b53cb8ca8f19",
          "body": "The hue/saturation/brightness properties rounded on read (integer hue,\n2dp sat/bri) and from_protocol() rounded at construction, so downstream\nconsumers could never obtain the device's full-precision values.\n\nReturn the raw stored floats from the getters and decode from_protocol()\nwithout rounding. \n[…]\nhrough that equality.\n\nDevice-state tests that asserted exact decoded values now use approx.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(color): expose raw HSBK values instead of rounded ones",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-14T00:44:21Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "f7e7c805bd7b90749505b9a69df512e8ce8a685d",
          "body": "Automatically generated by python-semantic-release",
          "is_bot": false,
          "headline": "5.5.0",
          "author_name": "semantic-release",
          "author_login": null,
          "committed_at": "2026-06-13T17:59:45Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "6c5933d426fb8b8f98d5b1822656491434f813ac",
          "body": "CeilingLight.__aexit__ ran the threaded save guarded only by\n`except Exception`. asyncio.CancelledError is a BaseException, so a\ncancellation landing while the save was pending bypassed the handler and\nskipped `super().__aexit__()`, leaking the connection despite the\ndocstring's \"always cleaned up\" \n[…]\ncancellation still propagates. Regression\ntest added.\n\nAddresses review feedback on PR #159.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(devices): guarantee ceiling connection cleanup if save is cancelled",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "415006a5bde8988dedad775e64095cfc63c02e93",
          "body": "Serial.from_protocol() keeps only target[:6], so a spoofed StateService\nwith valid-looking serial bytes but non-zero padding (e.g. d073d5010203ffff)\nnormalised to a clean serial and could win the per-serial dedup race\nagainst the real device, poisoning discovery with the attacker's IP/port.\nThe 8-by\n[…]\nero; reject when they are not. Regression test added.\n\nAddresses review feedback on PR #159.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(discovery): reject non-zero target padding in the serial guard",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "6d6d9b0398a417063d658dae8d348f0f315f3b18",
          "body": "codecov measures branch coverage; two changed conditionals had only one\nside exercised:\n- mdns discover_lifx_services idle_expired=True (idle-timeout break)\n- connection request_stream discovery-serial refresh (both sides of\n  serial != self.serial)\n\nFull suite 2522 passed; no missing lines or partial branches on the diff.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "test: cover remaining branch partials for 100% patch coverage",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "48c13ae10e0e7f250e3ce0b67d39f6f9c7395653",
          "body": "Adds targeted tests for the changed lines codecov/patch flagged:\n- IdleDeadline.expired convenience property\n- array-of-enums deserialisation tolerating unknown values (base.py)\n- CeilingLight atomic-write failure cleanup (temp unlink + warning)\n- request_stream DEBUG request/reply logging blocks (G\n[…]\n via a mocked IdleDeadline)\n\nFull suite 2519 passed; diff-cover vs origin/main reports 100%.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "test: reach 100% patch coverage for the discovery-unification branch",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "010967797649dd96a2412c411399c43fc157ca4d",
          "body": "Phase fully verified: review-fix 6/6, security 11/11 closed, UAT 4/4\nincluding real-hardware validation. Marked complete manually — the\nfree-form ROADMAP (this phase reuses \"Phase 1\" alongside the shipped\nv1.0 Ceiling phase) caused phase.complete to mis-resolve and touch the\nwrong row, so it was reverted. v1.0 shipped record left untouched.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(01): mark Unify-discovery-loops phase complete & verified",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "798c45b7a3b021dbfd3f1b7e5a25b485bef4c5ee",
          "body": "Test 4 (real-hardware discovery) confirmed passing by the user after the\nDeviceService fix (0d83deb). All Phase 1 UAT tests pass; status complete.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "test(01): UAT complete — 4/4 pass, hardware regression confirmed fixed",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "dbff129c7084af7dfec1162bed05fb09db40dd47",
          "body": "11 threats, all closed: 9 plan-time (T-01-01..T-01-09) verified in-tree,\nplus 2 DoS threats found post-plan and closed with regression coverage —\nT-01-10 (CR-01, size-invalid datagram abort) and T-01-11 (unknown\nDeviceService enum: log amplification + crash surface). threats_open: 0.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(phase-1): add security threat verification",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "244f4149d27b85c523ee78d93527c579dca16bff",
          "body": "Test 4 (real-hardware validation) found ValueError on unknown\nDeviceService(5); fixed in 0d83deb. Re-test against hardware pending.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "test(01): record hardware UAT regression + fix in 01-UAT",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "0cb5dc638e74ffe7c76ac6d31df66b99e6e60165",
          "body": "Real LIFX devices advertise one StateService per service they support and\nreport service identifiers (e.g. 5) that the bundled protocol.yml does not\ndefine as a DeviceService enum member. The Phase 1 refactor retired the raw\nstruct parser (which discarded the service byte) for StateService.unpack(),\n[…]\n not the scripts.\n\nGates: ruff clean, pyright 0/0/0, full suite 2511 passed / 12 deselected.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(01): tolerate unknown DeviceService values from real hardware",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "db9b7cc9a8dcd54a6c5750bfd5f1ce6d941686bd",
          "body": "Tests 1-3 (discovery via unified path, find_by_label truthful port WR-04,\nreceive_many DeprecationWarning WR-05) verified against the in-process\nemulator and full suite (2508 passed). Test 4 blocked on real-hardware\nvalidation at user request.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "test(01): UAT partial — 3/4 verified, hardware check pending",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "8e5f61bacdaf235d671e58b847e98294097d44dd",
          "body": "Review-fix pass resumed and completed; mark phase 1 ready for verifier.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "chore(01): resume bookkeeping — consume HANDOFF.json, update STATE",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "03aa5b6136e3220425831036a89b790d172f058a",
          "body": "All 6 in-scope findings (CR-01, WR-01..WR-05) resolved; 7 Info findings\nout of scope. Documents the resume reconciliation after the prior\nsession died mid-fix.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(01): add code review fix report",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "21c2b35e19f4c331c784af50ec655f36e35cdd8c",
          "body": "WR-03: rewrite _discover_with_packet docstring as an async-generator\ncontract (Yields, first-wins dedup, snake_case payload keys) and replace\nthe broken `await ... + sync for` example with `async for`.\n\nWR-04: set DiscoveryResponse.port to the device source port (addr[1])\ninstead of the broadcast de\n[…]\ned to match v6.0.\n\nGates: ruff clean, pyright 0/0/0, full suite 2508 passed / 12 deselected.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(01): finish review-fix pass — WR-03/WR-04/WR-05",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "ad35b4e906e0467522a43fdc6d0b2d1eace19fff",
          "body": "Signed-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "wip: phase 1 paused at review-fix (3/6 findings fixed)",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "74928f82e20e60ea7721c143a7750a5e3e30b47c",
          "body": "The mDNS loop only called deadline.mark_response() after a successful\nyield of a new serial, so duplicate re-announcements (normal mDNS\nbehaviour) never extended the idle window — the exact premature-expiry\nfailure mode D-04 fixed in the broadcast loop. Hoist mark_response() to\nrun on every valid LI\n[…]\ne honouring idle expiry).\n\nAdds a regression test asserting mark_response is called for both of two\nduplicate responses while dedup still yields a single record.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(01): WR-02 align mDNS idle-reset semantics with D-04",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "76b00efd5797b588254d40929651acd30e6a456d",
          "body": "The hoisted serial guard accepted b'\\x00' * 8 — the LIFX broadcast/\nall-devices target the discovery request itself uses, never a valid\ndevice serial. A spoofed response echoing a zero target produced a\nphantom DiscoveredDevice with serial 000000000000.\n\nReplace the unreachable all-0xff clause (0xff\n[…]\ntrips the\nmulticast-bit check) with an all-zeros check, so the guard now covers\nmulticast, all-0xff and all-zero targets. Adds a generator-level\nregression test.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(01): WR-01 reject all-zeros broadcast target in serial guard",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b33fde1464e73422cc260e649a75dfd77d79ffe8",
          "body": "UdpTransport.receive() raises LifxProtocolError for datagrams outside the\n36-1024 byte range, but the shared discovery loop only guarded the receive\ncall against LifxTimeoutError. A single hostile or malformed UDP datagram\nsent to the discovery socket terminated every discovery API with an\nuncaught \n[…]\nd and oversized datagrams through\nthe genuine UdpTransport.receive() size-validation path and assert the\nsubsequent valid StateService response is still yielded.\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "fix(01): CR-01 drop size-invalid datagrams instead of aborting discovery",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b9d7b8feda9c2601b102023ce85ac2705d7a2497",
          "body": "Signed-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(01): add code review report",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "e23cec0e646180ff7f5468ebcc657f222f8b994c",
          "body": "Signed-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(01-05): complete discovery error test gap plan",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "5c32b06eb5311d34eb04f1b312e5871c64685d2f",
          "body": "- Re-prove malformed StateService payload handling via the shared\n  _discover_with_packet path (D-10); no retired internal references remain\n- Add TestMalformedPayloadHandling: truncated payload yields zero devices\n  without propagating an exception through discover_devices\n- Add TestDiscoverWithPac\n[…]\ngenerator level (D-11)\n- Import _discover_with_packet and DevicePackets (used by new tests)\n- All existing validation and emulator dedup tests retained and green\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "test(01-05): close D-10/D-11 test gaps in discovery error suite",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "74aa4728b8fbb37de2443464624c3dfec9b008a0",
          "body": "Signed-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(01-03): complete mDNS IdleDeadline adoption plan",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "5a57443e6747a08a1c25aaf263d770b5fe124861",
          "body": "… discover_lifx_services\n\n- Replace inline idle/overall deadline arithmetic with IdleDeadline(timeout, idle_timeout)\n- Call deadline.mark_response() after each yielded record (was last_response_time = response_timestamp)\n- Replace bare except Exception around transport.receive() with typed clauses (\n[…]\neplace bare Exception(\"timeout\") side-effects with LifxTimeoutError\n- Add two new tests for LifxNetworkError (no-propagate) and unexpected error (re-raise) paths\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "refactor(01-03): adopt IdleDeadline and tighten exception handling in…",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "690fd207a77d0712fbbe7320ebd4e83698fb411b",
          "body": "Signed-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(01-02): complete thin-discover-devices-wrapper plan",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "2babaa18174ee500b014a9f0103b49b990efa7df",
          "body": "…ce_state_service\n\n- Remove top-level 'import struct' (only used by _parse_device_state_service)\n- Delete _parse_device_state_service function entirely (D-05): StateService.unpack()\n  via the protocol layer is now the single source of truth for payload parsing\n- Replace discover_devices body (~200 l\n[…]\nver_with_packet\n- Remove TestParseDeviceStateServiceErrors from test_discovery_errors.py (tests\n  pinned to retired internal; D-10); remaining 8 tests still pass\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "refactor(01-02): thin discover_devices to wrapper; delete _parse_devi…",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "fa0a1173e1451c54f0e5d562487c682ad92b9aaf",
          "body": "…packet; adopt IdleDeadline\n\n- Import IdleDeadline from lifx.network.utils alongside allocate_source\n- Replace inline idle/overall timeout arithmetic with IdleDeadline instance\n- Check deadline.idle_expired and deadline.overall_expired for distinct DEBUG log messages\n- Add unconditional serial valid\n[…]\nwas written-only)\n- Align LifxProtocolError handler to use exc_info=True (matching discover_devices)\n- devices_found in completion log now uses len(seen_serials)\n\nSigned-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "refactor(01-02): hoist serial validation + dedup into _discover_with_…",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "4345760e8151be97c88450b40157e45d4bb815bf",
          "body": "Signed-off-by: Avi Miller <me@dje.li>",
          "is_bot": false,
          "headline": "docs(01-04): complete deprecate-receive-many plan",
          "author_name": "Avi Miller",
          "author_login": "Djelibeybi",
          "committed_at": "2026-06-13T17:56:33Z",
          "body_truncated": false,
          "is_coding_agent": false
        }
      ],
      "releases_count": 75,
      "commits_last_year": 499,
      "latest_release_at": "2026-07-31T09:02:04Z",
      "latest_release_tag": "v6.1.1",
      "releases_from_tags": false,
      "days_since_last_push": 0,
      "active_weeks_last_year": 37,
      "days_since_latest_release": 0,
      "mean_days_between_releases": 5.3
    },
    "community": {
      "has_readme": true,
      "has_license": true,
      "has_description": true,
      "has_contributing": false,
      "health_percentage": 57,
      "has_issue_template": false,
      "has_code_of_conduct": false,
      "has_pull_request_template": false
    },
    "ecosystem": {
      "packages": [
        {
          "name": "lifx-async",
          "exists": true,
          "license": "UPL-1.0",
          "keywords": [
            "Framework :: AsyncIO",
            "Framework :: Pytest",
            "Intended Audience :: Developers",
            "Natural Language :: English",
            "Operating System :: OS Independent",
            "Programming Language :: Python :: 3.10",
            "Programming Language :: Python :: 3.11",
            "Programming Language :: Python :: 3.12",
            "Programming Language :: Python :: 3.13",
            "Programming Language :: Python :: 3.14",
            "Topic :: Software Development :: Libraries",
            "Topic :: Software Development :: Libraries :: Python Modules",
            "Typing :: Typed"
          ],
          "ecosystem": "pypi",
          "matches_repo": true,
          "registry_url": "https://pypi.org/project/lifx-async/",
          "is_deprecated": false,
          "latest_version": "6.1.1",
          "repository_url": "https://github.com/Djelibeybi/lifx-async",
          "versions_count": 75,
          "total_downloads": null,
          "dependents_count": null,
          "deprecation_note": null,
          "maintainers_count": null,
          "monthly_downloads": 3963,
          "first_published_at": "2025-11-04T04:01:44.942236Z",
          "latest_published_at": "2026-07-31T09:02:42.075413Z",
          "latest_version_yanked": null,
          "days_since_latest_publish": 0
        }
      ]
    },
    "popularity": {
      "forks": 1,
      "stars": 5,
      "watchers": 1,
      "fork_history": {
        "days": [
          {
            "date": "2026-07-15",
            "count": 1
          }
        ],
        "complete": true,
        "collected": 1,
        "total_forks": 1
      },
      "star_history": null,
      "open_issues_and_prs": 2
    },
    "ai_readiness": {
      "has_nix": false,
      "example_dirs": [
        "examples"
      ],
      "has_llms_txt": false,
      "has_dockerfile": false,
      "has_mcp_signal": false,
      "bootstrap_files": [],
      "api_schema_files": [],
      "has_devcontainer": false,
      "typecheck_configs": [
        "src/lifx/py.typed"
      ],
      "toolchain_manifests": [],
      "largest_source_bytes": 119553,
      "source_files_sampled": 237,
      "oversized_source_files": 4,
      "agent_instruction_files": [
        "AGENTS.md",
        "CLAUDE.md"
      ],
      "agent_instruction_max_bytes": 22347
    },
    "dependencies": {
      "manifests": [
        "pyproject.toml"
      ],
      "advisories": {
        "error": null,
        "scope": "repository_graph",
        "source": "osv",
        "findings": [],
        "collected": true,
        "malicious": [],
        "truncated": false,
        "by_severity": {},
        "advisory_count": 0,
        "affected_count": 0,
        "assessed_count": 67,
        "malicious_count": 0,
        "assessed_package": null,
        "unassessed_count": 0,
        "direct_affected_count": 0
      },
      "ecosystems": [
        "pypi"
      ],
      "dependencies": [],
      "all_dependencies": {
        "error": null,
        "source": "github-sbom",
        "packages": [
          {
            "name": "annotated-doc",
            "direct": false,
            "version": "0.0.4",
            "ecosystem": "pypi"
          },
          {
            "name": "annotated-types",
            "direct": false,
            "version": "0.7.0",
            "ecosystem": "pypi"
          },
          {
            "name": "backports-asyncio-runner",
            "direct": false,
            "version": "1.2.0",
            "ecosystem": "pypi"
          },
          {
            "name": "beautifulsoup4",
            "direct": false,
            "version": "4.15.0",
            "ecosystem": "pypi"
          },
          {
            "name": "click",
            "direct": false,
            "version": "8.4.2",
            "ecosystem": "pypi"
          },
          {
            "name": "colorama",
            "direct": false,
            "version": "0.4.6",
            "ecosystem": "pypi"
          },
          {
            "name": "coverage",
            "direct": false,
            "version": "7.15.0",
            "ecosystem": "pypi"
          },
          {
            "name": "deepmerge",
            "direct": false,
            "version": "2.1.0",
            "ecosystem": "pypi"
          },
          {
            "name": "exceptiongroup",
            "direct": false,
            "version": "1.3.1",
            "ecosystem": "pypi"
          },
          {
            "name": "ghp-import",
            "direct": false,
            "version": "2.1.0",
            "ecosystem": "pypi"
          },
          {
            "name": "griffelib",
            "direct": false,
            "version": "2.1.0",
            "ecosystem": "pypi"
          },
          {
            "name": "hatchling",
            "direct": false,
            "version": "1.30.1",
            "ecosystem": "pypi"
          },
          {
            "name": "iniconfig",
            "direct": false,
            "version": "2.3.0",
            "ecosystem": "pypi"
          },
          {
            "name": "jinja2",
            "direct": false,
            "version": "3.1.6",
            "ecosystem": "pypi"
          },
          {
            "name": "lifx-async",
            "direct": false,
            "version": "6.1.1",
            "ecosystem": "pypi"
          },
          {
            "name": "lifx-emulator-core",
            "direct": false,
            "version": "3.7.0",
            "ecosystem": "pypi"
          },
          {
            "name": "llmstxt-standalone",
            "direct": false,
            "version": "0.2.0",
            "ecosystem": "pypi"
          },
          {
            "name": "markdown",
            "direct": false,
            "version": "3.10.2",
            "ecosystem": "pypi"
          },
          {
            "name": "markdown-it-py",
            "direct": false,
            "version": "3.0.0",
            "ecosystem": "pypi"
          },
          {
            "name": "markdownify",
            "direct": false,
            "version": "1.2.3",
            "ecosystem": "pypi"
          },
          {
            "name": "markupsafe",
            "direct": false,
            "version": "3.0.3",
            "ecosystem": "pypi"
          },
          {
            "name": "mdformat",
            "direct": false,
            "version": "0.7.22",
            "ecosystem": "pypi"
          },
          {
            "name": "mdformat-tables",
            "direct": false,
            "version": "1.0.0",
            "ecosystem": "pypi"
          },
          {
            "name": "mdurl",
            "direct": false,
            "version": "0.1.2",
            "ecosystem": "pypi"
          },
          {
            "name": "mergedeep",
            "direct": false,
            "version": "1.3.4",
            "ecosystem": "pypi"
          },
          {
            "name": "mkdocs",
            "direct": false,
            "version": "1.6.1",
            "ecosystem": "pypi"
          },
          {
            "name": "mkdocs-autorefs",
            "direct": false,
            "version": "1.4.4",
            "ecosystem": "pypi"
          },
          {
            "name": "mkdocs-get-deps",
            "direct": false,
            "version": "0.2.2",
            "ecosystem": "pypi"
          },
          {
            "name": "mkdocstrings",
            "direct": false,
            "version": "1.0.4",
            "ecosystem": "pypi"
          },
          {
            "name": "mkdocstrings-python",
            "direct": false,
            "version": "2.0.5",
            "ecosystem": "pypi"
          },
          {
            "name": "nodeenv",
            "direct": false,
            "version": "1.10.0",
            "ecosystem": "pypi"
          },
          {
            "name": "packaging",
            "direct": false,
            "version": "26.2",
            "ecosystem": "pypi"
          },
          {
            "name": "pathspec",
            "direct": false,
            "version": "1.1.1",
            "ecosystem": "pypi"
          },
          {
            "name": "platformdirs",
            "direct": false,
            "version": "4.10.0",
            "ecosystem": "pypi"
          },
          {
            "name": "pluggy",
            "direct": false,
            "version": "1.6.0",
            "ecosystem": "pypi"
          },
          {
            "name": "prek",
            "direct": false,
            "version": "0.4.8",
            "ecosystem": "pypi"
          },
          {
            "name": "py-cpuinfo",
            "direct": false,
            "version": "9.0.0",
            "ecosystem": "pypi"
          },
          {
            "name": "pydantic",
            "direct": false,
            "version": "2.13.4",
            "ecosystem": "pypi"
          },
          {
            "name": "pydantic-core",
            "direct": false,
            "version": "2.46.4",
            "ecosystem": "pypi"
          },
          {
            "name": "pygments",
            "direct": false,
            "version": "2.20.0",
            "ecosystem": "pypi"
          },
          {
            "name": "pymdown-extensions",
            "direct": false,
            "version": "11.0.1",
            "ecosystem": "pypi"
          },
          {
            "name": "pyright",
            "direct": false,
            "version": "1.1.411",
            "ecosystem": "pypi"
          },
          {
            "name": "pytest",
            "direct": false,
            "version": "9.1.1",
            "ecosystem": "pypi"
          },
          {
            "name": "pytest-asyncio",
            "direct": false,
            "version": "1.4.0",
            "ecosystem": "pypi"
          },
          {
            "name": "pytest-benchmark",
            "direct": false,
            "version": "5.2.3",
            "ecosystem": "pypi"
          },
          {
            "name": "pytest-cov",
            "direct": false,
            "version": "7.1.0",
            "ecosystem": "pypi"
          },
          {
            "name": "pytest-retry",
            "direct": false,
            "version": "1.7.0",
            "ecosystem": "pypi"
          },
          {
            "name": "pytest-sugar",
            "direct": false,
            "version": "1.1.1",
            "ecosystem": "pypi"
          },
          {
            "name": "pytest-timeout",
            "direct": false,
            "version": "2.4.0",
            "ecosystem": "pypi"
          },
          {
            "name": "python-dateutil",
            "direct": false,
            "version": "2.9.0.post0",
            "ecosystem": "pypi"
          },
          {
            "name": "pyyaml",
            "direct": false,
            "version": "6.0.3",
            "ecosystem": "pypi"
          },
          {
            "name": "pyyaml-env-tag",
            "direct": false,
            "version": "1.1",
            "ecosystem": "pypi"
          },
          {
            "name": "rich",
            "direct": false,
            "version": "15.0.0",
            "ecosystem": "pypi"
          },
          {
            "name": "ruamel-yaml",
            "direct": false,
            "version": "0.19.1",
            "ecosystem": "pypi"
          },
          {
            "name": "ruff",
            "direct": false,
            "version": "0.15.20",
            "ecosystem": "pypi"
          },
          {
            "name": "shellingham",
            "direct": false,
            "version": "1.5.4",
            "ecosystem": "pypi"
          },
          {
            "name": "six",
            "direct": false,
            "version": "1.17.0",
            "ecosystem": "pypi"
          },
          {
            "name": "soupsieve",
            "direct": false,
            "version": "2.8.4",
            "ecosystem": "pypi"
          },
          {
            "name": "termcolor",
            "direct": false,
            "version": "3.3.0",
            "ecosystem": "pypi"
          },
          {
            "name": "tomli",
            "direct": false,
            "version": "2.4.1",
            "ecosystem": "pypi"
          },
          {
            "name": "trove-classifiers",
            "direct": false,
            "version": "2026.6.1.19",
            "ecosystem": "pypi"
          },
          {
            "name": "typer",
            "direct": false,
            "version": "0.26.8",
            "ecosystem": "pypi"
          },
          {
            "name": "typing-extensions",
            "direct": false,
            "version": "4.16.0",
            "ecosystem": "pypi"
          },
          {
            "name": "typing-inspection",
            "direct": false,
            "version": "0.4.2",
            "ecosystem": "pypi"
          },
          {
            "name": "watchdog",
            "direct": false,
            "version": "6.0.0",
            "ecosystem": "pypi"
          },
          {
            "name": "wcwidth",
            "direct": false,
            "version": "0.8.2",
            "ecosystem": "pypi"
          },
          {
            "name": "zensical",
            "direct": false,
            "version": "0.0.47",
            "ecosystem": "pypi"
          }
        ],
        "collected": true,
        "truncated": false,
        "total_count": 67,
        "direct_count": 0,
        "indirect_count": 67
      }
    },
    "maintainership": {
      "issues": {
        "open_prs": 1,
        "merged_prs": 177,
        "open_issues": 1,
        "closed_ratio": 0.667,
        "closed_issues": 2,
        "closed_unmerged_prs": 5
      },
      "bus_factor": 1,
      "bot_contributors": 3,
      "top_contributors": [
        {
          "type": "User",
          "login": "Djelibeybi",
          "commits": 381,
          "avatar_url": "https://avatars.githubusercontent.com/u/103232?v=4"
        }
      ],
      "contributors_sampled": 1,
      "top_contributor_share": 1
    },
    "quality_signals": {
      "has_ci": true,
      "has_tests": true,
      "ci_workflows": [
        "ci.yml",
        "docs.yml",
        "pr-automation.yml"
      ],
      "has_docs_dir": true,
      "linter_configs": [],
      "has_editorconfig": false,
      "has_linter_config": true,
      "has_precommit_config": true
    },
    "security_signals": {
      "lockfiles": [
        "uv.lock"
      ],
      "scorecard": {
        "checks": [
          {
            "name": "Binary-Artifacts",
            "score": 10,
            "reason": "no binaries found in the repo",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#binary-artifacts"
          },
          {
            "name": "Branch-Protection",
            "score": 4,
            "reason": "branch protection is not maximal on development and all release branches",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#branch-protection"
          },
          {
            "name": "CI-Tests",
            "score": 10,
            "reason": "5 out of 5 merged PRs checked by a CI test -- score normalized to 10",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#ci-tests"
          },
          {
            "name": "CII-Best-Practices",
            "score": 0,
            "reason": "no effort to earn an OpenSSF best practices badge detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#cii-best-practices"
          },
          {
            "name": "Code-Review",
            "score": 0,
            "reason": "Found 0/9 approved changesets -- score normalized to 0",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#code-review"
          },
          {
            "name": "Contributors",
            "score": 0,
            "reason": "project has 0 contributing companies or organizations -- score normalized to 0",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#contributors"
          },
          {
            "name": "Dangerous-Workflow",
            "score": 10,
            "reason": "no dangerous workflow patterns detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#dangerous-workflow"
          },
          {
            "name": "Dependency-Update-Tool",
            "score": 10,
            "reason": "update tool detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#dependency-update-tool"
          },
          {
            "name": "Fuzzing",
            "score": 0,
            "reason": "project is not fuzzed",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#fuzzing"
          },
          {
            "name": "License",
            "score": 10,
            "reason": "license file detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#license"
          },
          {
            "name": "Maintained",
            "score": 10,
            "reason": "30 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 10",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#maintained"
          },
          {
            "name": "Packaging",
            "score": 10,
            "reason": "packaging workflow detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#packaging"
          },
          {
            "name": "Pinned-Dependencies",
            "score": 10,
            "reason": "all dependencies are pinned",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#pinned-dependencies"
          },
          {
            "name": "SAST",
            "score": 10,
            "reason": "SAST tool is run on all commits",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#sast"
          },
          {
            "name": "Security-Policy",
            "score": 10,
            "reason": "security policy file detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#security-policy"
          },
          {
            "name": "Signed-Releases",
            "score": 0,
            "reason": "Project has not signed or included provenance with any releases.",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#signed-releases"
          },
          {
            "name": "Token-Permissions",
            "score": 10,
            "reason": "GitHub workflow tokens follow principle of least privilege",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#token-permissions"
          },
          {
            "name": "Vulnerabilities",
            "score": 10,
            "reason": "0 existing vulnerabilities detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#vulnerabilities"
          }
        ],
        "commit": "e4f8c95f6500172276da2f45dc9b8a2e7e896ab0",
        "ran_at": "2026-07-31T09:06:06Z",
        "aggregate_score": 7.2,
        "scorecard_version": "v5.5.0"
      },
      "has_codeql_workflow": false,
      "has_security_policy": true,
      "has_dependabot_config": true
    },
    "contribution_flow": {
      "collected": true,
      "ci_last_run_at": "2026-07-31T09:03:06Z",
      "oldest_open_prs": [
        {
          "number": 167,
          "created_at": "2026-07-15T12:04:10Z",
          "last_comment_at": "2026-07-15T12:33:42Z",
          "last_comment_author": "Djelibeybi"
        }
      ],
      "last_merged_pr_at": "2026-07-31T08:57:16Z",
      "ci_last_conclusion": "SUCCESS",
      "oldest_open_issues": [
        {
          "number": 1,
          "created_at": "2025-11-04T03:36:32Z",
          "last_comment_at": null,
          "last_comment_author": null
        }
      ]
    }
  },
  "config": {
    "disabled_metrics": [],
    "disabled_categories": [],
    "disabled_components": {}
  },
  "source": {
    "url": "https://github.com/Djelibeybi/lifx-async",
    "host": "github.com",
    "name": "lifx-async",
    "owner": "Djelibeybi"
  },
  "metrics": {
    "overall": {
      "key": "overall",
      "band": "good",
      "name": "Overall health",
      "note": null,
      "notes": [],
      "value": 71,
      "inputs": {
        "security": 78,
        "vitality": 90,
        "community": 36,
        "governance": 56,
        "engineering": 92
      },
      "components": []
    },
    "categories": [
      {
        "key": "vitality",
        "band": "excellent",
        "name": "Vitality",
        "value": 90,
        "weight": 0.22,
        "metrics": [
          {
            "key": "development_activity",
            "band": "excellent",
            "name": "Development activity",
            "note": null,
            "notes": [],
            "value": 90,
            "inputs": {
              "commits_last_year": 499,
              "human_commit_share": 0.89,
              "days_since_last_push": 0,
              "active_weeks_last_year": 37
            },
            "components": [
              {
                "key": "push_recency",
                "name": "Push recency",
                "detail": "last push 0 days ago",
                "points": 36,
                "status": "met",
                "details": [
                  {
                    "code": "push_recency",
                    "params": {
                      "days": 0
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "commit_cadence",
                "name": "Commit cadence",
                "detail": "37/52 weeks with commits",
                "points": 25.6,
                "status": "partial",
                "details": [
                  {
                    "code": "commit_cadence_weeks",
                    "params": {
                      "weeks": 37
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "commit_volume",
                "name": "Commit volume",
                "detail": "499 commits in the last year",
                "points": 18,
                "status": "met",
                "details": [
                  {
                    "code": "commits_last_year",
                    "params": {
                      "count": 499
                    }
                  }
                ],
                "max_points": 18
              },
              {
                "key": "openssf_scorecard_maintained",
                "name": "OpenSSF Scorecard: Maintained",
                "detail": "30 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 10",
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "release_discipline",
            "band": "excellent",
            "name": "Release discipline",
            "note": null,
            "notes": [],
            "value": 90,
            "inputs": {
              "releases_count": 75,
              "latest_release_tag": "v6.1.1",
              "releases_from_tags": false,
              "days_since_latest_release": 0,
              "mean_days_between_releases": 5.3
            },
            "components": [
              {
                "key": "ships_releases",
                "name": "Ships releases",
                "detail": "75 releases published",
                "points": 27,
                "status": "met",
                "details": [
                  {
                    "code": "releases_published",
                    "params": {
                      "count": 75
                    }
                  }
                ],
                "max_points": 27
              },
              {
                "key": "release_recency",
                "name": "Release recency",
                "detail": "latest release 0 days ago",
                "points": 36,
                "status": "met",
                "details": [
                  {
                    "code": "release_recency",
                    "params": {
                      "days": 0
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "release_cadence",
                "name": "Release cadence",
                "detail": "a release every ~5.3 days",
                "points": 27,
                "status": "met",
                "details": [
                  {
                    "code": "release_cadence",
                    "params": {
                      "gap": 5.3
                    }
                  }
                ],
                "max_points": 27
              },
              {
                "key": "openssf_scorecard_signed_releases",
                "name": "OpenSSF Scorecard: Signed-Releases",
                "detail": "Project has not signed or included provenance with any releases.",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "abandonment",
            "band": "excellent",
            "name": "Abandonment",
            "note": null,
            "notes": [],
            "value": 100,
            "inputs": {
              "cap": null,
              "state": "maintained",
              "guards": [],
              "signals": [],
              "red_flag": false,
              "multiplier_pct": 100,
              "declared_reason": null,
              "unverified_reason": null,
              "unanswered_open_prs": null,
              "unanswered_open_issues": null,
              "days_since_last_merged_pr": null,
              "days_since_last_human_commit": 0,
              "days_since_last_human_commit_is_floor": false
            },
            "components": [
              {
                "key": "project_is_still_maintained",
                "name": "Project is still maintained",
                "detail": "last human commit 0 days ago",
                "points": 100,
                "status": "met",
                "details": [
                  {
                    "code": "abandonment_maintained",
                    "params": {
                      "days": 0
                    }
                  }
                ],
                "max_points": 100
              }
            ]
          }
        ],
        "description": "Is the project alive — is code being written and are releases shipping?"
      },
      {
        "key": "community",
        "band": "at_risk",
        "name": "Community & Adoption",
        "value": 36,
        "weight": 0.18,
        "metrics": [
          {
            "key": "popularity",
            "band": "critical",
            "name": "Popularity & adoption",
            "note": null,
            "notes": [],
            "value": 10,
            "inputs": {
              "forks": 1,
              "stars": 5,
              "watchers": 1,
              "growth_state": "unverified",
              "growth_factor_pct": 100,
              "growth_unverified_reason": "no_history"
            },
            "components": [
              {
                "key": "stars",
                "name": "Stars",
                "detail": "5 stars",
                "points": 9.8,
                "status": "partial",
                "details": [
                  {
                    "code": "stars",
                    "params": {
                      "count": 5
                    }
                  }
                ],
                "max_points": 60
              },
              {
                "key": "forks",
                "name": "Forks",
                "detail": "1 forks",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "forks",
                    "params": {
                      "count": 1
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "watchers",
                "name": "Watchers",
                "detail": "1 watchers",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "watchers",
                    "params": {
                      "count": 1
                    }
                  }
                ],
                "max_points": 15
              }
            ]
          },
          {
            "key": "community_health",
            "band": "moderate",
            "name": "Community health",
            "note": null,
            "notes": [],
            "value": 50,
            "inputs": {
              "has_readme": true,
              "has_license": true,
              "has_contributing": false,
              "has_issue_template": false,
              "has_code_of_conduct": false,
              "has_pull_request_template": false
            },
            "components": [
              {
                "key": "readme",
                "name": "README",
                "detail": null,
                "points": 22.5,
                "status": "met",
                "details": [],
                "max_points": 22.5
              },
              {
                "key": "license",
                "name": "License",
                "detail": "recognized license (UPL-1.0)",
                "points": 22.5,
                "status": "met",
                "details": [
                  {
                    "code": "license_standard",
                    "params": {}
                  },
                  {
                    "code": "license_spdx",
                    "params": {
                      "spdx": "UPL-1.0"
                    }
                  }
                ],
                "max_points": 22.5
              },
              {
                "key": "contributing_guide",
                "name": "CONTRIBUTING guide",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 18
              },
              {
                "key": "code_of_conduct",
                "name": "Code of conduct",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 13.5
              },
              {
                "key": "issue_template",
                "name": "Issue template",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.2
              },
              {
                "key": "pr_template",
                "name": "PR template",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 6.3
              }
            ]
          },
          {
            "key": "ecosystem_adoption",
            "band": "moderate",
            "name": "Ecosystem adoption (downloads)",
            "note": "Excluded from scoring (no data or not applicable): Registry dependents. Remaining weights renormalized.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "registry_dependents"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              }
            ],
            "value": 60,
            "inputs": {
              "packages": [
                "lifx-async"
              ],
              "dependents": null,
              "ecosystems": "pypi",
              "total_downloads": null,
              "monthly_downloads": 3963
            },
            "components": [
              {
                "key": "monthly_downloads",
                "name": "Monthly downloads",
                "detail": "3,963 downloads/month across pypi",
                "points": 48,
                "status": "partial",
                "details": [
                  {
                    "code": "downloads_monthly",
                    "params": {
                      "count": 3963,
                      "ecosystems": "pypi"
                    }
                  }
                ],
                "max_points": 80
              },
              {
                "key": "registry_dependents",
                "name": "Registry dependents",
                "detail": "not reported by this ecosystem",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "not_reported_by_this_ecosystem",
                    "params": {}
                  }
                ],
                "max_points": 20
              }
            ]
          }
        ],
        "description": "Does the project have users, downloads, attention, and a welcoming setup for contributors?"
      },
      {
        "key": "governance",
        "band": "moderate",
        "name": "Sustainability & Governance",
        "value": 56,
        "weight": 0.24,
        "metrics": [
          {
            "key": "maintainer_resilience",
            "band": "critical",
            "name": "Maintainer resilience (bus factor)",
            "note": null,
            "notes": [],
            "value": 10,
            "inputs": {
              "bus_factor": 1,
              "contributors_sampled": 1,
              "top_contributor_share": 1
            },
            "components": [
              {
                "key": "bus_factor",
                "name": "Bus factor",
                "detail": "1 contributor(s) cover half of all commits",
                "points": 9,
                "status": "partial",
                "details": [
                  {
                    "code": "bus_factor",
                    "params": {
                      "count": 1
                    }
                  }
                ],
                "max_points": 54
              },
              {
                "key": "commit_distribution",
                "name": "Commit distribution",
                "detail": "top contributor authored 100% of commits",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "top_contributor_share",
                    "params": {
                      "share": 100
                    }
                  }
                ],
                "max_points": 22.5
              },
              {
                "key": "contributor_breadth",
                "name": "Contributor breadth",
                "detail": "1 contributors",
                "points": 1.4,
                "status": "partial",
                "details": [
                  {
                    "code": "contributors_sampled",
                    "params": {
                      "count": 1
                    }
                  }
                ],
                "max_points": 13.5
              },
              {
                "key": "openssf_scorecard_contributors",
                "name": "OpenSSF Scorecard: Contributors",
                "detail": "project has 0 contributing companies or organizations -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "responsiveness",
            "band": "moderate",
            "name": "Issue & PR responsiveness",
            "note": null,
            "notes": [],
            "value": 68,
            "inputs": {
              "merged_prs": 177,
              "open_issues": 1,
              "closed_issues": 2,
              "issue_closed_ratio": 0.667,
              "closed_unmerged_prs": 5
            },
            "components": [
              {
                "key": "issue_resolution",
                "name": "Issue resolution",
                "detail": "67% of issues closed",
                "points": 31.2,
                "status": "partial",
                "details": [
                  {
                    "code": "issues_closed_share",
                    "params": {
                      "share": 67
                    }
                  }
                ],
                "max_points": 46.75
              },
              {
                "key": "pr_acceptance",
                "name": "PR acceptance",
                "detail": "177/182 decided PRs merged",
                "points": 37.2,
                "status": "partial",
                "details": [
                  {
                    "code": "decided_prs_merged",
                    "params": {
                      "merged": 177,
                      "decided": 182
                    }
                  }
                ],
                "max_points": 38.25
              },
              {
                "key": "openssf_scorecard_code_review",
                "name": "OpenSSF Scorecard: Code-Review",
                "detail": "Found 0/9 approved changesets -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 15
              }
            ]
          },
          {
            "key": "stewardship",
            "band": "moderate",
            "name": "Ownership & stewardship",
            "note": "Excluded from scoring (no data or not applicable): Verified domain. Remaining weights renormalized.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "verified_domain"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              }
            ],
            "value": 63,
            "inputs": {
              "followers": 187,
              "owner_type": "User",
              "is_verified": null,
              "owner_login": "Djelibeybi",
              "public_repos": 46,
              "account_age_days": 6230
            },
            "components": [
              {
                "key": "ownership_backing",
                "name": "Ownership backing",
                "detail": "personal (user) account",
                "points": 10,
                "status": "partial",
                "details": [
                  {
                    "code": "owner_personal",
                    "params": {}
                  }
                ],
                "max_points": 30
              },
              {
                "key": "verified_domain",
                "name": "Verified domain",
                "detail": "not applicable to user accounts",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "not_applicable_to_user_accounts",
                    "params": {}
                  }
                ],
                "max_points": 20
              },
              {
                "key": "owner_reach",
                "name": "Owner reach",
                "detail": "187 followers of Djelibeybi",
                "points": 16.4,
                "status": "partial",
                "details": [
                  {
                    "code": "owner_followers",
                    "params": {
                      "count": 187,
                      "login": "Djelibeybi"
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "track_record",
                "name": "Track record",
                "detail": "46 public repos, account ~17 yr old",
                "points": 24.2,
                "status": "partial",
                "details": [
                  {
                    "code": "public_repos",
                    "params": {
                      "count": 46
                    }
                  },
                  {
                    "code": "account_age_years",
                    "params": {
                      "years": 17
                    }
                  }
                ],
                "max_points": 25
              }
            ]
          },
          {
            "key": "package_maintenance",
            "band": "excellent",
            "name": "Package maintenance",
            "note": null,
            "notes": [],
            "value": 100,
            "inputs": {
              "packages": [
                "lifx-async"
              ],
              "ecosystems": "pypi",
              "any_deprecated": false,
              "min_days_since_publish": 0
            },
            "components": [
              {
                "key": "published_resolvable",
                "name": "Published & resolvable",
                "detail": "1 package(s) on pypi",
                "points": 25,
                "status": "met",
                "details": [
                  {
                    "code": "packages_published",
                    "params": {
                      "count": 1,
                      "ecosystems": "pypi"
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "publish_recency",
                "name": "Publish recency",
                "detail": "latest publish 0 days ago",
                "points": 35,
                "status": "met",
                "details": [
                  {
                    "code": "publish_recency",
                    "params": {
                      "days": 0
                    }
                  }
                ],
                "max_points": 35
              },
              {
                "key": "version_history",
                "name": "Version history",
                "detail": "75 published versions",
                "points": 20,
                "status": "met",
                "details": [
                  {
                    "code": "published_versions",
                    "params": {
                      "count": 75
                    }
                  }
                ],
                "max_points": 20
              },
              {
                "key": "not_deprecated",
                "name": "Not deprecated",
                "detail": "active, not deprecated or yanked",
                "points": 20,
                "status": "met",
                "details": [
                  {
                    "code": "package_not_deprecated",
                    "params": {}
                  }
                ],
                "max_points": 20
              }
            ]
          }
        ],
        "description": "Will the project survive its people — bus factor, responsiveness, who backs it, and package upkeep?"
      },
      {
        "key": "engineering",
        "band": "excellent",
        "name": "Engineering Quality",
        "value": 92,
        "weight": 0.2,
        "metrics": [
          {
            "key": "engineering_practices",
            "band": "excellent",
            "name": "Engineering practices",
            "note": null,
            "notes": [],
            "value": 94,
            "inputs": {
              "has_ci": true,
              "has_tests": true,
              "has_editorconfig": false,
              "has_linter_config": true,
              "has_precommit_config": true
            },
            "components": [
              {
                "key": "ci_workflows",
                "name": "CI workflows",
                "detail": "3 workflow(s)",
                "points": 24,
                "status": "met",
                "details": [
                  {
                    "code": "ci_workflows",
                    "params": {
                      "count": 3
                    }
                  }
                ],
                "max_points": 24
              },
              {
                "key": "tests_present",
                "name": "Tests present",
                "detail": null,
                "points": 24,
                "status": "met",
                "details": [],
                "max_points": 24
              },
              {
                "key": "linter_config",
                "name": "Linter config",
                "detail": null,
                "points": 16,
                "status": "met",
                "details": [],
                "max_points": 16
              },
              {
                "key": "pre_commit_hooks",
                "name": "Pre-commit hooks",
                "detail": null,
                "points": 9.6,
                "status": "met",
                "details": [],
                "max_points": 9.6
              },
              {
                "key": "editorconfig",
                "name": ".editorconfig",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 6.4
              },
              {
                "key": "openssf_scorecard_ci_tests",
                "name": "OpenSSF Scorecard: CI-Tests",
                "detail": "5 out of 5 merged PRs checked by a CI test -- score normalized to 10",
                "points": 20,
                "status": "met",
                "details": [],
                "max_points": 20
              }
            ]
          },
          {
            "key": "documentation",
            "band": "excellent",
            "name": "Documentation",
            "note": null,
            "notes": [],
            "value": 90,
            "inputs": {
              "topics": [
                "asyncio",
                "asyncio-python",
                "lifx",
                "lifx-bulbs",
                "lifx-lan-protocol",
                "lifx-lights",
                "llms-txt"
              ],
              "has_wiki": false,
              "homepage": "https://djelibeybi.github.io/lifx-async/",
              "has_readme": true,
              "has_docs_dir": true,
              "has_description": true
            },
            "components": [
              {
                "key": "readme",
                "name": "README",
                "detail": null,
                "points": 30,
                "status": "met",
                "details": [],
                "max_points": 30
              },
              {
                "key": "documentation_directory",
                "name": "Documentation directory",
                "detail": null,
                "points": 25,
                "status": "met",
                "details": [],
                "max_points": 25
              },
              {
                "key": "documentation_homepage_site",
                "name": "Documentation / homepage site",
                "detail": "https://djelibeybi.github.io/lifx-async/",
                "points": 15,
                "status": "met",
                "details": [],
                "max_points": 15
              },
              {
                "key": "repository_description",
                "name": "Repository description",
                "detail": null,
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              },
              {
                "key": "topics",
                "name": "Topics",
                "detail": "7 topics",
                "points": 10,
                "status": "met",
                "details": [
                  {
                    "code": "topics_count",
                    "params": {
                      "count": 7
                    }
                  }
                ],
                "max_points": 10
              },
              {
                "key": "wiki",
                "name": "Wiki",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 10
              }
            ]
          }
        ],
        "description": "Are baseline engineering and documentation practices in place?"
      },
      {
        "key": "security",
        "band": "good",
        "name": "Security",
        "value": 78,
        "weight": 0.16,
        "metrics": [
          {
            "key": "security_posture",
            "band": "good",
            "name": "Security posture",
            "note": null,
            "notes": [],
            "value": 72,
            "inputs": {
              "source": "openssf_scorecard",
              "checks_evaluated": 18,
              "scorecard_version": "v5.5.0",
              "checks_inconclusive": 0,
              "scorecard_aggregate": 7.2
            },
            "components": [
              {
                "key": "binary_artifacts",
                "name": "Binary-Artifacts",
                "detail": "no binaries found in the repo",
                "points": 7.5,
                "status": "met",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "branch_protection",
                "name": "Branch-Protection",
                "detail": "branch protection is not maximal on development and all release branches",
                "points": 3,
                "status": "partial",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "ci_tests",
                "name": "CI-Tests",
                "detail": "5 out of 5 merged PRs checked by a CI test -- score normalized to 10",
                "points": 2.5,
                "status": "met",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "cii_best_practices",
                "name": "CII-Best-Practices",
                "detail": "no effort to earn an OpenSSF best practices badge detected",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "code_review",
                "name": "Code-Review",
                "detail": "Found 0/9 approved changesets -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "contributors",
                "name": "Contributors",
                "detail": "project has 0 contributing companies or organizations -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "dangerous_workflow",
                "name": "Dangerous-Workflow",
                "detail": "no dangerous workflow patterns detected",
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              },
              {
                "key": "dependency_update_tool",
                "name": "Dependency-Update-Tool",
                "detail": "update tool detected",
                "points": 7.5,
                "status": "met",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "fuzzing",
                "name": "Fuzzing",
                "detail": "project is not fuzzed",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 5
              },
              {
                "key": "license",
                "name": "License",
                "detail": "license file detected",
                "points": 2.5,
                "status": "met",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "maintained",
                "name": "Maintained",
                "detail": "30 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 10",
                "points": 7.5,
                "status": "met",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "packaging",
                "name": "Packaging",
                "detail": "packaging workflow detected",
                "points": 5,
                "status": "met",
                "details": [],
                "max_points": 5
              },
              {
                "key": "pinned_dependencies",
                "name": "Pinned-Dependencies",
                "detail": "all dependencies are pinned",
                "points": 5,
                "status": "met",
                "details": [],
                "max_points": 5
              },
              {
                "key": "sast",
                "name": "SAST",
                "detail": "SAST tool is run on all commits",
                "points": 5,
                "status": "met",
                "details": [],
                "max_points": 5
              },
              {
                "key": "security_policy",
                "name": "Security-Policy",
                "detail": "security policy file detected",
                "points": 5,
                "status": "met",
                "details": [],
                "max_points": 5
              },
              {
                "key": "signed_releases",
                "name": "Signed-Releases",
                "detail": "Project has not signed or included provenance with any releases.",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "token_permissions",
                "name": "Token-Permissions",
                "detail": "GitHub workflow tokens follow principle of least privilege",
                "points": 7.5,
                "status": "met",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "vulnerabilities",
                "name": "Vulnerabilities",
                "detail": "0 existing vulnerabilities detected",
                "points": 7.5,
                "status": "met",
                "details": [],
                "max_points": 7.5
              }
            ]
          },
          {
            "key": "dependency_advisories",
            "band": "excellent",
            "name": "Dependency advisories",
            "note": "Excluded from scoring (no data or not applicable): Indirect dependencies free of known advisories, No advisories left outstanding. Remaining weights renormalized. Matched 67 resolved dependencies against OSV. This repository publishes no package the index resolves, so the repository dependency graph was assessed instead. That graph mixes development and test pins with shipped dependencies, so only the declared runtime dependencies are scored; transitive findings are reported as context and excluded from the score. Reachability is not analyzed.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "indirect_dependencies_free_of_known_advisories",
                    "no_advisories_left_outstanding"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              },
              {
                "code": "advisories_scope_repository",
                "params": {
                  "assessed": 67
                }
              },
              {
                "code": "advisories_repo_graph_caveat",
                "params": {}
              },
              {
                "code": "advisories_reachability",
                "params": {}
              }
            ],
            "value": 100,
            "inputs": {
              "source": "osv",
              "advisories": 0,
              "affected_packages": 0,
              "assessed_packages": 67,
              "unassessed_packages": 0,
              "affected_by_severity": "none",
              "direct_affected_packages": 0
            },
            "components": [
              {
                "key": "direct_dependencies_free_of_known_advisories",
                "name": "Direct dependencies free of known advisories",
                "detail": "no direct dependency carries a known advisory",
                "points": 35,
                "status": "met",
                "details": [
                  {
                    "code": "no_direct_advisories",
                    "params": {}
                  }
                ],
                "max_points": 35
              },
              {
                "key": "indirect_dependencies_free_of_known_advisories",
                "name": "Indirect dependencies free of known advisories",
                "detail": "transitive set not separable from development and test dependencies in this scope",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "advisories_scope_not_separable",
                    "params": {}
                  }
                ],
                "max_points": 25
              },
              {
                "key": "no_advisories_left_outstanding",
                "name": "No advisories left outstanding",
                "detail": "no advisory carries a publication date",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "advisories_no_publication_date",
                    "params": {}
                  }
                ],
                "max_points": 40
              }
            ]
          },
          {
            "key": "malicious_dependencies",
            "band": "excellent",
            "name": "Malicious dependencies",
            "note": null,
            "notes": [],
            "value": 100,
            "inputs": {
              "source": "osv",
              "meaning": "reported as a malicious package by the OpenSSF corpus; the remedy is removal or moving off the compromised name, never an upgrade of the same artifact. Versions the registry has since pulled are listed but not scored",
              "packages": [],
              "red_flag": false,
              "assessed_packages": 67,
              "malicious_packages": 0,
              "direct_malicious_packages": 0,
              "withdrawn_malicious_packages": 0,
              "installable_malicious_packages": 0
            },
            "components": [
              {
                "key": "no_dependency_reported_as_a_malicious_package",
                "name": "No dependency reported as a malicious package",
                "detail": "no dependency is reported as a malicious package",
                "points": 100,
                "status": "met",
                "details": [
                  {
                    "code": "no_malicious_dependencies",
                    "params": {}
                  }
                ],
                "max_points": 100
              }
            ]
          },
          {
            "key": "high_risk_jurisdiction_exposure",
            "band": "excellent",
            "name": "High-Risk Jurisdiction Exposure",
            "note": "Only high-confidence self-published location evidence affects this multiplier. Ambiguous matches are review-only; country evidence is not proof of nationality, citizenship, legal registration, malicious intent, or sanctions status.",
            "notes": [
              {
                "code": "jurisdiction_evidence_limits",
                "params": {}
              }
            ],
            "value": 100,
            "inputs": {
              "meaning": "self-published location evidence; not nationality or citizenship",
              "red_flag": false,
              "exposures": [],
              "policy_countries": [
                "Russia",
                "Iran",
                "North Korea"
              ],
              "review_only_matches": 0,
              "assessed_self_published_locations": 2
            },
            "components": [
              {
                "key": "policy_exposure_multiplier",
                "name": "Policy exposure multiplier",
                "detail": "no confirmed policy-scope location match",
                "points": 100,
                "status": "met",
                "details": [
                  {
                    "code": "jurisdiction_no_match",
                    "params": {}
                  }
                ],
                "max_points": 100
              }
            ]
          }
        ],
        "description": "Are visible security and supply-chain practices strong, with no malicious dependency and no unresolved high-risk jurisdiction exposure?"
      },
      {
        "key": "ai_readiness",
        "band": "good",
        "name": "AI Readiness",
        "value": 76,
        "weight": 0,
        "metrics": [
          {
            "key": "ai_agent_context",
            "band": "excellent",
            "name": "Agent context & guidance",
            "note": null,
            "notes": [],
            "value": 85,
            "inputs": {
              "has_llms_txt": false,
              "legible_history_share": 0.876,
              "agent_instruction_files": [
                "AGENTS.md",
                "CLAUDE.md"
              ],
              "agent_instruction_max_bytes": 22347
            },
            "components": [
              {
                "key": "agent_instructions",
                "name": "Agent instructions",
                "detail": "AGENTS.md, CLAUDE.md",
                "points": 45,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "AGENTS.md, CLAUDE.md"
                    }
                  }
                ],
                "max_points": 45
              },
              {
                "key": "machine_readable_docs_llms_txt",
                "name": "Machine-readable docs (llms.txt)",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 15
              },
              {
                "key": "legible_commit_history",
                "name": "Legible commit history",
                "detail": "78 of 89 human commits state their intent (structured subject or explanatory body)",
                "points": 40,
                "status": "met",
                "details": [
                  {
                    "code": "legible_history",
                    "params": {
                      "legible": 78,
                      "sampled": 89
                    }
                  }
                ],
                "max_points": 40
              }
            ]
          },
          {
            "key": "ai_verify_loop",
            "band": "good",
            "name": "Verify loop (build / test / typecheck)",
            "note": null,
            "notes": [],
            "value": 82,
            "inputs": {
              "has_nix": false,
              "has_tests": true,
              "lockfiles": [
                "uv.lock"
              ],
              "has_dockerfile": false,
              "typed_language": false,
              "bootstrap_files": [],
              "has_devcontainer": false,
              "has_linter_config": true,
              "typecheck_configs": [
                "src/lifx/py.typed"
              ],
              "agent_commit_share": 0.21,
              "toolchain_manifests": [],
              "dependency_bot_commit_share": 0.11
            },
            "components": [
              {
                "key": "one_command_bootstrap",
                "name": "One-command bootstrap",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 18
              },
              {
                "key": "automated_tests",
                "name": "Automated tests",
                "detail": null,
                "points": 22,
                "status": "met",
                "details": [],
                "max_points": 22
              },
              {
                "key": "lint_format_config",
                "name": "Lint / format config",
                "detail": null,
                "points": 11,
                "status": "met",
                "details": [],
                "max_points": 11
              },
              {
                "key": "static_type_checking",
                "name": "Static type checking",
                "detail": "src/lifx/py.typed",
                "points": 11,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "src/lifx/py.typed"
                    }
                  }
                ],
                "max_points": 11
              },
              {
                "key": "reproducible_environment",
                "name": "Reproducible environment",
                "detail": "lockfile",
                "points": 10,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "lockfile"
                    }
                  }
                ],
                "max_points": 10
              },
              {
                "key": "demonstrated_agent_practice",
                "name": "Demonstrated agent practice",
                "detail": "21 of the last 100 commits agent-authored or agent-credited",
                "points": 10,
                "status": "met",
                "details": [
                  {
                    "code": "agent_authored_commits",
                    "params": {
                      "count": 21,
                      "sampled": 100
                    }
                  }
                ],
                "max_points": 10
              },
              {
                "key": "automated_maintenance",
                "name": "Automated maintenance",
                "detail": "11 of the last 100 commits are automated dependency updates",
                "points": 8,
                "status": "met",
                "details": [
                  {
                    "code": "dependency_bot_commits",
                    "params": {
                      "count": 11,
                      "sampled": 100
                    }
                  }
                ],
                "max_points": 8
              },
              {
                "key": "openssf_scorecard_pinned_dependencies",
                "name": "OpenSSF Scorecard: Pinned-Dependencies",
                "detail": "all dependencies are pinned",
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "ai_code_legibility",
            "band": "good",
            "name": "Code legibility for models",
            "note": null,
            "notes": [],
            "value": 81,
            "inputs": {
              "primary_language": "Python",
              "largest_source_bytes": 119553,
              "source_files_sampled": 237,
              "oversized_source_files": 4
            },
            "components": [
              {
                "key": "type_checkable_code",
                "name": "Type-checkable code",
                "detail": "Python with type-check config (src/lifx/py.typed)",
                "points": 27,
                "status": "partial",
                "details": [
                  {
                    "code": "typecheck_config_language",
                    "params": {
                      "files": "src/lifx/py.typed",
                      "language": "Python"
                    }
                  }
                ],
                "max_points": 45
              },
              {
                "key": "manageable_file_sizes",
                "name": "Manageable file sizes",
                "detail": "4/237 source files over 60KB",
                "points": 54.1,
                "status": "partial",
                "details": [
                  {
                    "code": "oversized_source_files",
                    "params": {
                      "kb": 60,
                      "sampled": 237,
                      "oversized": 4
                    }
                  }
                ],
                "max_points": 55
              }
            ]
          },
          {
            "key": "ai_interfaces",
            "band": "at_risk",
            "name": "Machine-readable interfaces",
            "note": null,
            "notes": [],
            "value": 40,
            "inputs": {
              "example_dirs": [
                "examples"
              ],
              "has_mcp_signal": false,
              "api_schema_files": []
            },
            "components": [
              {
                "key": "api_schema_openapi_graphql_proto",
                "name": "API schema (OpenAPI/GraphQL/proto)",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 40
              },
              {
                "key": "mcp_server",
                "name": "MCP server",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 20
              },
              {
                "key": "runnable_examples",
                "name": "Runnable examples",
                "detail": "examples",
                "points": 40,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "examples"
                    }
                  }
                ],
                "max_points": 40
              }
            ]
          }
        ],
        "description": "How well is the repo equipped to be developed and maintained with AI coding agents? An independent, experimental badge — weight 0.0, so it is surfaced on its own and does not affect the overall health score."
      }
    ],
    "metrics_version": "1.13.0"
  },
  "warnings": [
    "Star history unavailable: GitHub GraphQL error: Resource not accessible by personal access token",
    "deps.dev does not index pypi:lifx-async@6.1.1; advisories assessed against the repository dependency graph instead"
  ],
  "report_type": "repository",
  "generated_at": "2026-07-31T09:06:24.077753Z",
  "schema_version": "0.27.0",
  "badge_url": "https://raw.githubusercontent.com/inspect-software/badges/main/v1/d/Djelibeybi/lifx-async.svg",
  "full_name": "Djelibeybi/lifx-async",
  "license_state": "standard",
  "license_spdx": "UPL-1.0"
}

Las puntuaciones son señales, no garantías. Reflejan prácticas públicamente visibles en GitHub; no son una auditoría de código ni una garantía de seguridad.

Los datos ausentes se excluyen y los pesos se renormalizan; nunca se puntúan como cero. La metodología es versionada y abierta: métricas v1.13.0, esquema v0.27.0 — metodología completa · wiki de métricas.

Cómo se sitúa un resultado dentro del registro general: estadísticas agregadasPyPI.