Registro público
Informe de salud del softwareesquema 0.23.0 · métricas 1.13.0 · 2026-07-21 21:02 UTC

Betterment / delayed

a multi-threaded, SQL-driven ActiveJob backend used at Betterment to process millions of background jobs per day

RubyMIT★ 175 estrellas⑂ 12 forksdesde ago 2021Ver en GitHub ↗

Betterment/delayed 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 Sustainability & Governance (82/100) y la más baja, AI Readiness (51/100). Se actualizó por última vez hace 8 días. 3 personas concentran 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

BettermentOrganización
105 seguidores91 repositorios públicosdesde jun 2011

Este repositorio está respaldado por una organización: una custodia compartida y responsable que puede sobrevivir a cualquier mantenedor individual.

Ecosistemas de paquetes

RegistroPaqueteVersiónDescargas / mesVersionesÚltima publicación
RubyGemsdelayed3.1.0-29hace 8 días

Métricas por categoría

Vitalidad

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

78Bueno · 22% del índice global
Cómo se puntúa
28.8/36Recencia de push — último push hace 8 días
12.5/36Cadencia de commits — 18/52 semanas con commits
15.5/18Volumen de commits — 52 commits en el último año
7/10OpenSSF Scorecard: Maintained — 9 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 7
Datos de entrada utilizados
commits_last_year52
human_commit_share1
days_since_last_push8
active_weeks_last_year18
Cómo se puntúa
27/27Publica versiones — 26 versiones publicadas
36/36Recencia de las versiones — última versión hace 8 días
27/27Cadencia de publicación — una versión cada ~31,1 días
0/10OpenSSF Scorecard: Signed-Releases — sin datos
Datos de entrada utilizados
releases_count26
latest_release_tagv3.1.0
releases_from_tagsno
days_since_latest_release8
mean_days_between_releases31,1
Excluidos de la puntuación (sin datos o no aplicable): OpenSSF Scorecard: Signed-Releases. Los pesos restantes se han renormalizado.

Comunidad y Adopción

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

59Moderado · 18% del índice global
Cómo se puntúa
36.3/60Estrellas — 175 estrellas
8.7/25Forks — 12 forks
9.1/15Observadores — 44 observadores
Datos de entrada utilizados
forks12
stars175
watchers44
growth_stateorganic
growth_factor_pct100
Cómo se puntúa
22.5/22.5README
22.5/22.5Licencia — licencia reconocida (MIT)
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
62.9/80Descargas totales — 1.132.655 descargas históricas en rubygems
0/20Dependientes en el registro — no lo informa este ecosistema
Datos de entrada utilizados
packagesdelayed
dependents
ecosystemsrubygems
total_downloads1.132.655
monthly_downloads
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?

82Bueno · 24% del índice global
Cómo se puntúa
36/54Factor bus — la mitad de los commits recae en 3 contribuyente(s)
16.3/22.5Distribución de commits — el principal contribuyente firma el 28% de los commits
13.5/13.5Amplitud de contribuyentes — 100 contribuyentes
10/10OpenSSF Scorecard: Contributors — project has 34 contributing companies or organizations
Datos de entrada utilizados
bus_factor3
contributors_sampled100
top_contributor_share0,275
Cómo se puntúa
35.5/46.8Resolución de issues — 76% de issues cerradas
35.2/38.3Aceptación de PR — 81/88 PR decididos fusionados
15/15OpenSSF Scorecard: Code-Review — all changesets reviewed
Datos de entrada utilizados
merged_prs81
open_issues6
closed_issues19
issue_closed_ratio0,76
closed_unmerged_prs7
Cómo se puntúa
30/30Respaldo de la propiedad — propiedad de una organización
0/20Dominio verificado
14.6/25Alcance del propietario — 105 seguidores de Betterment
25/25Trayectoria — 91 repos públicos, cuenta de ~15 años
Datos de entrada utilizados
followers105
owner_typeOrganization
is_verified
owner_loginBetterment
public_repos91
account_age_days5507
Cómo se puntúa
25/25Publicado y resoluble — 1 paquete(s) en rubygems
35/35Recencia de publicación — última publicación hace 8 días
20/20Historial de versiones — 29 versiones en el registro
20/20No obsoleto — activo, ni obsoleto ni retirado
Datos de entrada utilizados
packagesdelayed
ecosystemsrubygems
any_deprecatedno
min_days_since_publish8

Calidad de Ingeniería

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

70Bueno · 20% del índice global
Cómo se puntúa
24/24Flujos de trabajo de CI — 2 flujo(s) de trabajo
24/24Pruebas presentes
16/16Configuración de linter — .rubocop.yml
0/9.6Hooks de pre-commit
0/6.4.editorconfig
20/20OpenSSF Scorecard: CI-Tests — 30 out of 30 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_configno

Documentación

50Moderado
Cómo se puntúa
30/30README
0/25Directorio de documentación
0/15Sitio de documentación / página del proyecto
10/10Descripción del repositorio
0/10Topics
10/10Wiki
Datos de entrada utilizados
topics
has_wiki
homepage
has_readme
has_docs_dirno
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?

59Moderado · 16% del índice global
Cómo se puntúa
7.5/7.5Binary-Artifacts — no binaries found in the repo
6/7.5Branch-Protection — branch protection is not maximal on development and all release branches
2.5/2.5CI-Tests — 30 out of 30 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
7.5/7.5Code-Review — all changesets reviewed
2.5/2.5Contributors — project has 34 contributing companies or organizations
10/10Dangerous-Workflow — no dangerous workflow patterns detected
0/7.5Dependency-Update-Tool — no update tool detected
0/5Fuzzing — project is not fuzzed
2.5/2.5Licencia — license file detected
5.2/7.5Maintained — 9 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 7
0/5Packaging — sin datos
5/5Pinned-Dependencies — all dependencies are pinned
0/5SAST — SAST tool is not run on all commits -- score normalized to 0
0/5Security-Policy — security policy file not detected
0/7.5Signed-Releases — sin datos
0/7.5Token-Permissions — detected GitHub workflow tokens with excessive permissions
0/7.5Vulnerabilities — 53 existing vulnerabilities detected
Datos de entrada utilizados
sourceopenssf_scorecard
checks_evaluated16
scorecard_versionv5.5.0
checks_inconclusive2
scorecard_aggregate5,3
Excluidos de la puntuación (sin datos o no aplicable): packaging, signed_releases. Los pesos restantes se han renormalizado.
Cómo se puntúa
26.6/35Dependencias directas libres de avisos conocidos — 1 afectados: activerecord 8.0.2 (moderate)
0/25Dependencias indirectas libres de avisos conocidos — el conjunto transitivo no es separable de las dependencias de desarrollo y prueba en este alcance
37.2/40Sin avisos pendientes — 1 paquete(s) con avisos sin atender más allá de 90 días; el más antiguo publicado hace 341 días
Datos de entrada utilizados
sourceosv
advisories53
affected_packages8
assessed_packages78
unassessed_packages2
affected_by_severitycritical 1, high 4, moderate 2, low 1
direct_affected_packages1
Excluidos de la puntuación (sin datos o no aplicable): Dependencias indirectas libres de avisos conocidos. Los pesos restantes se han renormalizado. Se cotejaron 78 dependencias resueltas con OSV. 2 no pudieron evaluarse: sin versión resuelta, ecosistema no admitido o fuera de la lista de paquetes informada. 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.

51Moderado · 0% del índice global
Cómo se puntúa
0/45Instrucciones para agentes — sin CLAUDE.md / AGENTS.md / reglas de editor
0/15Documentación legible por máquinas (llms.txt)
40/40Historial de commits legible — 97 de 100 commits humanos declaran su intención (asunto estructurado o cuerpo explicativo)
Datos de entrada utilizados
has_llms_txtno
legible_history_share0,97
agent_instruction_files
agent_instruction_max_bytes
Cómo se puntúa
0/18Arranque con un solo comando
22/22Pruebas automatizadas
11/11Configuración de lint / formato — .rubocop.yml
0/11Verificación estática de tipos
10/10Entorno reproducible — lockfile
4/10Práctica demostrada con agentes — 2 de los últimos 100 commits con autoría o crédito de agente
0/8Mantenimiento automatizado — no se observan actualizaciones automáticas de dependencias
10/10OpenSSF Scorecard: Pinned-Dependencies — all dependencies are pinned
Datos de entrada utilizados
has_nixno
has_tests
lockfilesGemfile.lock
has_dockerfileno
typed_languageno
bootstrap_files
has_devcontainerno
has_linter_config
typecheck_configs
agent_commit_share0,02
toolchain_manifests
dependency_bot_commit_share0
Cómo se puntúa
0/45Código verificable por tipos — Ruby sin configuración de verificación de tipos
55/55Tamaños de archivo manejables — 0/58 archivos fuente de más de 60 KB
Datos de entrada utilizados
primary_languageRuby
largest_source_bytes48.940
source_files_sampled58
oversized_source_files0

Datos clave

175estrellas de GitHub
100contribuidores
52commits en los últimos 12 meses
8días desde el último push
26versiones publicadas
3factor bus
6issues abiertas
RubyGemsecosistemas de paquetes

Más detalle

Historial de estrellas y forks 175 ★ / 12 ⇿
175Estrellas
12Forks
25Versiones

Cuándo se añadió cada estrella y fork, recopilado de GitHub y agrupado por día. El crecimiento acumulado se sitúa justo encima de las adiciones diarias que lo componen, de modo que ambos se leen en conjunto: la acumulación orgánica sostenida no se parece en nada a un pico abrupto y efímero. Cuando esa diferencia es medible, se informa como autenticidad del crecimiento.

0408012016020017512162021-082024-012026-07
Mayor 3Menor 12Parche 10

Cada punto abarca 5 días.

OpenSSF Scorecard 5.3 / 10
5.3agregado

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-21 21:02 UTC

10Binary-Artifactsno binaries found in the repo
8Branch-Protectionbranch protection is not maximal on development and all release branches
10CI-Tests30 out of 30 merged PRs checked by a CI test -- score normalized to 10
0CII-Best-Practicesno effort to earn an OpenSSF best practices badge detected
10Code-Reviewall changesets reviewed
10Contributorsproject has 34 contributing companies or organizations
10Dangerous-Workflowno dangerous workflow patterns detected
0Dependency-Update-Toolno update tool detected
0Fuzzingproject is not fuzzed
10Licenselicense file detected
7Maintained9 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 7
n/dPackagingpackaging workflow not detected
10Pinned-Dependenciesall dependencies are pinned
0SASTSAST tool is not run on all commits -- score normalized to 0
0Security-Policysecurity policy file not detected
n/dSigned-Releasesno releases found
0Token-Permissionsdetected GitHub workflow tokens with excessive permissions
0Vulnerabilities53 existing vulnerabilities detected
Dependencias directas 13
RegistroPaqueteRestricción de versiónManifiesto
RubyGemsactionmailerGemfile
RubyGemsactivejobGemfile
RubyGemsactiverecordGemfile
RubyGemsappraisalGemfile
RubyGemsbetterlintGemfile
RubyGemsmysql2Gemfile
RubyGemspgGemfile
RubyGemsrakeGemfile
RubyGemsrspecGemfile
RubyGemstimecopGemfile
RubyGemszeitwerkGemfile
RubyGemssqlite3>= 1.4Gemfile
RubyGemssnapshot_testingGemfile
Todas las dependencias 80

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

RegistroPaqueteVersiónRelación
RubyGemsactionmailer8.0.2directa
RubyGemsactivejob8.0.2directa
RubyGemsactiverecorddirecta
RubyGemsactiverecord8.0.2directa
RubyGemsappraisal2.5.0directa
RubyGemsbetterlint1.19.0directa
RubyGemsmysql20.5.6directa
RubyGemspg1.5.9directa
RubyGemsrake13.2.1directa
RubyGemsrspec3.13.0directa
RubyGemssnapshot_testing0.3.4directa
RubyGemssqlite32.6.0directa
RubyGemstimecop0.9.10directa
RubyGemszeitwerk2.7.2directa
RubyGemsactionpack8.0.2indirecta
RubyGemsactionview8.0.2indirecta
RubyGemsactivemodel8.0.2indirecta
RubyGemsactivesupport8.0.2indirecta
RubyGemsast2.4.3indirecta
RubyGemsbase640.2.0indirecta
RubyGemsbenchmark0.4.0indirecta
RubyGemsbigdecimal3.1.9indirecta
RubyGemsbuilder3.3.0indirecta
RubyGemsconcurrent-rubyindirecta
RubyGemsconcurrent-ruby1.3.5indirecta
RubyGemsconnection_pool2.5.0indirecta
RubyGemscrass1.0.6indirecta
RubyGemsdate3.4.1indirecta
RubyGemsdiff-lcs1.6.1indirecta
RubyGemsdrb2.2.1indirecta
RubyGemserubi1.13.1indirecta
RubyGemsglobalid1.2.1indirecta
RubyGemsi18n1.14.7indirecta
RubyGemsjson2.10.2indirecta
RubyGemslanguage_server-protocol3.17.0.4indirecta
RubyGemslint_roller1.1.0indirecta
RubyGemslogger1.7.0indirecta
RubyGemsloofah2.24.0indirecta
RubyGemsmail2.8.1indirecta
RubyGemsmini_mime1.1.5indirecta
RubyGemsminitest5.25.5indirecta
RubyGemsnet-imap0.5.6indirecta
RubyGemsnet-pop0.1.2indirecta
RubyGemsnet-protocol0.2.2indirecta
RubyGemsnet-smtp0.5.1indirecta
RubyGemsnokogiri1.18.6indirecta
RubyGemsparallel1.26.3indirecta
RubyGemsparser3.3.7.3indirecta
RubyGemsprism1.4.0indirecta
RubyGemsracc1.8.1indirecta
RubyGemsrack3.1.12indirecta
RubyGemsrack-session2.1.0indirecta
RubyGemsrack-test2.2.0indirecta
RubyGemsrails-dom-testing2.2.0indirecta
RubyGemsrails-html-sanitizer1.6.2indirecta
RubyGemsrainbow3.1.1indirecta
RubyGemsregexp_parser2.10.0indirecta
RubyGemsrspec-core3.13.3indirecta
RubyGemsrspec-expectations3.13.3indirecta
RubyGemsrspec-mocks3.13.2indirecta
RubyGemsrspec-support3.13.2indirecta
RubyGemsrubocop1.75.1indirecta
RubyGemsrubocop-ast1.43.0indirecta
RubyGemsrubocop-capybara2.22.1indirecta
RubyGemsrubocop-factory_bot2.27.1indirecta
RubyGemsrubocop-graphql1.5.5indirecta
RubyGemsrubocop-performance1.24.0indirecta
RubyGemsrubocop-rails2.30.3indirecta
RubyGemsrubocop-rake0.7.1indirecta
RubyGemsrubocop-rspec2.31.0indirecta
RubyGemsrubocop-rspec_rails2.29.1indirecta
RubyGemsruby-progressbar1.13.0indirecta
RubyGemssecurerandom0.4.1indirecta
RubyGemsthor1.3.2indirecta
RubyGemstimeout0.4.3indirecta
RubyGemstzinfo2.0.6indirecta
RubyGemsunicode-display_width3.1.4indirecta
RubyGemsunicode-emoji4.0.4indirecta
RubyGemsuri1.1.1indirecta
RubyGemsuseragent0.16.11indirecta
Avisos de dependencias 8

Este repositorio no publica ningún paquete que el índice resuelva, así que se evaluó su propio grafo de dependencias — 78 paquetes, que incluyen también fijaciones de desarrollo y prueba que nunca se distribuyen: 8 tienen avisos conocidos, de los cuales 1 son directas. 2 no pudieron evaluarse: sin versión resuelta, ecosistema no admitido, o fuera de la lista de paquetes informada.

PaqueteVersiónRelaciónGravedadAvisosCorregido en
rack-session2.1.0indirectacrítica22.1.2
activesupport8.0.2indirectaalta38.1.2.1
concurrent-ruby1.3.5indirectaalta31.3.7
nokogiri1.18.6indirectaalta131.19.4
rack3.1.12indirectaalta213.2.6
activerecord8.0.2directamoderada18.0.2.1
net-imap0.5.6indirectamoderada90.6.4.1
actionview8.0.2indirectabaja18.1.2.1

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": [],
      "is_fork": false,
      "size_kb": 1914,
      "has_wiki": true,
      "homepage": null,
      "languages": {
        "Ruby": 248958
      },
      "pushed_at": "2026-07-13T15:28:56Z",
      "created_at": "2021-08-18T14:13:01Z",
      "owner_type": "Organization",
      "updated_at": "2026-07-14T03:45:47Z",
      "description": "a multi-threaded, SQL-driven ActiveJob backend used at Betterment to process millions of background jobs per day",
      "is_archived": false,
      "is_disabled": false,
      "license_spdx": "MIT",
      "default_branch": "main",
      "license_spdx_raw": "MIT",
      "primary_language": "Ruby",
      "significant_languages": [
        "Ruby"
      ]
    },
    "owner": {
      "blog": "https://betterment.com",
      "name": "Betterment",
      "type": "Organization",
      "login": "Betterment",
      "company": null,
      "location": "New York, NY",
      "followers": 105,
      "avatar_url": "https://avatars.githubusercontent.com/u/871554?v=4",
      "created_at": "2011-06-23T18:42:51Z",
      "is_verified": null,
      "public_repos": 91,
      "account_age_days": 5507
    },
    "license": {
      "state": "standard",
      "spdx_id": "MIT",
      "raw_spdx": "MIT",
      "file_present": true,
      "scorecard_found": true,
      "profile_has_license": true
    },
    "activity": {
      "releases": [
        {
          "tag": "v3.1.0",
          "kind": "minor",
          "published_at": "2026-07-13T15:29:31Z"
        },
        {
          "tag": "v3.0.1",
          "kind": "patch",
          "published_at": "2026-06-15T15:23:19Z"
        },
        {
          "tag": "v3.0.0",
          "kind": "major",
          "published_at": "2026-06-08T17:13:25Z"
        },
        {
          "tag": "v2.2.0",
          "kind": "minor",
          "published_at": "2026-02-12T21:09:53Z"
        },
        {
          "tag": "v2.1.0",
          "kind": "minor",
          "published_at": "2026-02-05T16:54:20Z"
        },
        {
          "tag": "v2.0.3",
          "kind": "patch",
          "published_at": "2026-02-02T15:52:05Z"
        },
        {
          "tag": "v2.0.2",
          "kind": "patch",
          "published_at": "2026-01-05T22:09:33Z"
        },
        {
          "tag": "v2.0.1",
          "kind": "patch",
          "published_at": "2026-01-05T19:58:15Z"
        },
        {
          "tag": "v2.0.0",
          "kind": "major",
          "published_at": "2025-12-19T22:16:50Z"
        },
        {
          "tag": "v1.2.1",
          "kind": "patch",
          "published_at": "2025-10-06T19:23:42Z"
        },
        {
          "tag": "v1.2.0",
          "kind": "minor",
          "published_at": "2025-10-03T14:16:17Z"
        },
        {
          "tag": "v1.1.0",
          "kind": "minor",
          "published_at": "2025-09-12T21:13:58Z"
        },
        {
          "tag": "v1.0.0",
          "kind": "major",
          "published_at": "2025-05-08T20:17:13Z"
        },
        {
          "tag": "v0.8.0",
          "kind": "minor",
          "published_at": "2025-04-03T17:26:12Z"
        },
        {
          "tag": "v0.7.2",
          "kind": "patch",
          "published_at": "2025-04-02T22:22:27Z"
        },
        {
          "tag": "v0.7.1",
          "kind": "patch",
          "published_at": "2025-01-24T22:11:09Z"
        },
        {
          "tag": "v0.7.0",
          "kind": "minor",
          "published_at": "2025-01-24T22:11:00Z"
        },
        {
          "tag": "v0.6.0",
          "kind": "minor",
          "published_at": "2024-12-18T18:14:35Z"
        },
        {
          "tag": "v0.5.5",
          "kind": "patch",
          "published_at": "2024-08-13T21:59:24Z"
        },
        {
          "tag": "v0.5.4",
          "kind": "patch",
          "published_at": "2024-05-01T01:03:51Z"
        },
        {
          "tag": "v0.5.0",
          "kind": "minor",
          "published_at": "2023-01-20T22:07:16Z"
        },
        {
          "tag": "v0.4.0",
          "kind": "minor",
          "published_at": "2021-11-30T23:57:57Z"
        },
        {
          "tag": "v0.3.0",
          "kind": "minor",
          "published_at": "2021-10-27T16:09:21Z"
        },
        {
          "tag": "v0.2.0",
          "kind": "minor",
          "published_at": "2021-09-02T16:48:15Z"
        },
        {
          "tag": "v0.1.1",
          "kind": "patch",
          "published_at": "2021-08-19T20:35:00Z"
        },
        {
          "tag": "v0.1.0",
          "kind": "minor",
          "published_at": "2021-08-19T20:34:26Z"
        }
      ],
      "recent_commits": [
        {
          "oid": "c726753839bef4bec1b4d5dbe9945cb43d487edc",
          "body": "## What\n\nReplaces `method_missing` dispatch for named priority lookups with real\nmethod definitions:\n\n- **Class methods** — `Delayed::Priority.eventual`, `.interactive`, etc.\n- **Instance predicates** — `priority.eventual?`,\n`priority.interactive?`, etc.\n\nMethods are defined when the class loads (fo\n[…]\nn safety.\n- Full suite run locally on sqlite across multiple seeds; only failures\nare pre-existing on `main` in my local environment (Rails 8.0.2\nsnapshot/error-class drift, unrelated to this change).",
          "is_bot": false,
          "headline": "Define named priority methods as real methods (#115)",
          "author_name": "Corey Farwell",
          "author_login": "frewsxcv",
          "committed_at": "2026-07-13T15:28:26Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "a5375ed749a0fb318cfd3abf0d6b1b65231f5698",
          "body": "This pr https://github.com/Betterment/delayed/pull/96 made a change to\nhow max_age is calculated, where it started including the run_at's of\njobs that were currently locked. This made our max_age monitor go off\neven when we had a single long-running job in the queue, instead of\nshowing us when the queue was backed up.\n\nThis restores the previous behavior where max_age only includes jobs\nthat are pending, and not actively being worked off.",
          "is_bot": false,
          "headline": "fix: max_age metric excludes locked jobs (#114)",
          "author_name": "Harris Effron",
          "author_login": "effron",
          "committed_at": "2026-06-15T14:48:06Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "dbaf6a22f574f038ffa78b7220933af634890f3d",
          "body": "…ds without going through the adapter (#113)\n\nIntroduces new assertion that enforces that an ActiveJob cannot be\npassed into DJ enqueue methods, indicating to consumers that\n`perform_later` should be used instead in order to pipe through the\nadapter.",
          "is_bot": false,
          "headline": "fix: Assert ActiveJobs can not be passed directly to DJ enqueue metho…",
          "author_name": "Caleb Donoho",
          "author_login": "CelticMajora",
          "committed_at": "2026-06-02T19:48:04Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "564af1120a4b2b9a155377ebdbce9c468ffed1c1",
          "body": "- Fixes README to properly reflect the `enqueue` lifecycle event\n- Refactors unnecessary method nesting in the AJ adapter.",
          "is_bot": false,
          "headline": "fix: README to reflect accurate enqueue lifecycle shape (#112)",
          "author_name": "Caleb Donoho",
          "author_login": "CelticMajora",
          "committed_at": "2026-06-02T15:41:06Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "42e5eb9a54caea9ad1f4eb2ca9dd3f4071e90576",
          "body": "Adjusts the ActiveJob adapter to pipe all enqueues through enqueue_all.\nResult of convo here:\nhttps://github.com/Betterment/delayed/pull/103#discussion_r3335987931\n\nNote for release: This is a mildly breaking change. Previous to delayed\n3.0.0, a `provider_job_id` was _always_ attached when enqueuing jobs. In\ndelayed 3.0.0, `provider_job_id` will only be attached depending on your\ndatabase configuration(specifically\n`ActiveRecord::Base.connection.supports_insert_returning?` must be true)",
          "is_bot": false,
          "headline": "fix: switch aj adapter to pipe through enqueue_all (#111)",
          "author_name": "Caleb Donoho",
          "author_login": "CelticMajora",
          "committed_at": "2026-06-01T20:39:09Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "5023edb74eb12e50bb4bbef1280b850e06651f5a",
          "body": "Implements `enqueue_all` in order to support Rails 7.1 perform_all_later\n\nKey decisions:\n- Maintain safety against `enqueue_after_transaction_commit`\n- Adjusts shape of `enqueue` lifecycle hook\n- Keep enqueueing logic isolated to `Delayed::Job` so that `Delayed::ActiveJobAdapter` can stay pure to job setup and post-enqueue object manipulation.\n\n---------\n\nCo-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "feat: Implement enqueue_all (#103)",
          "author_name": "Caleb Donoho",
          "author_login": "CelticMajora",
          "committed_at": "2026-06-01T17:56:40Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "1f22eb506244bde11f4f2a7e8d69964133a1b675",
          "body": "Replacement for https://github.com/Betterment/delayed/pull/107 since I\nforgot to fork the repo before developing.",
          "is_bot": false,
          "headline": "refactor: Drop support for Delayed::Worker.delay_jobs being proc (#108)",
          "author_name": "Caleb Donoho",
          "author_login": "CelticMajora",
          "committed_at": "2026-05-21T17:10:52Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0d920e736e5f0281f8736908cbedfb5fe93ff065",
          "body": "Remove the job.hook(:enqueue) invocation from enqueue_job so payloads\ncan no longer mutate or observe the job at enqueue time. The hook method\nnow short-circuits on :enqueue with a deprecation warning for any direct\ncallers, and enqueue_job emits its own deprecation warning (naming the\npayload class\n[…]\ning the regression guard that ActiveJob JobWrapper payloads do not\ntrigger the deprecation warning.\n\ncc @effron @smudge\n\n---------\n\nCo-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "refactor: deprecate :enqueue job hook (#106)",
          "author_name": "Caleb Donoho",
          "author_login": "CelticMajora",
          "committed_at": "2026-05-21T15:39:48Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "4347124361f140ae2200b85ee55c14381141c78d",
          "body": "The before_save :set_default_run_at, :set_name callback is magical and doesn't actually prevent anyone from adding a new before_save that skips callbacks. Move the defaulting into Delayed::Backend::JobPreparer so both the single save path and any future bulk-insert path pick up defaults from the sam\n[…]\nun_at and name is now a DB-level NOT NULL violation -- callers should route through Delayed::Job.enqueue (which runs JobPreparer).\n\nBreaking change for anyone reaching past the documented enqueue API.",
          "is_bot": false,
          "headline": "refactor: Push run_at / name defaulting into JobPreparer (#104)",
          "author_name": "Caleb Donoho",
          "author_login": "CelticMajora",
          "committed_at": "2026-05-04T20:02:49Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "00d575c23b918cd79a174be7617c0847a40ab9cb",
          "body": "@medlefsen noticed that [ActiveJob states this in their\ndocs](https://apidock.com/rails/ActiveJob/Enqueuing/ClassMethods/perform_later)\nw.r.t. `perform_later`:\n\n> Returns an instance of the job class queued with arguments available\nin Job#arguments or false if the enqueue did not succeed.\n\nSince sil\n[…]\nlicit the current behavior, which\npresumably is specific to the AJ backend.\n\nThe `delayed` backend will not return `false` from `perform_later`, and\nwill instead bubble out an exception.\n\n/no-platform",
          "is_bot": false,
          "headline": "style: Add (passing) tests for regression coverage (#102)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-20T20:46:55Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "02864fbf8a1b8ecd2a46975add3330138ce72ce3",
          "body": "We previously [deprecated\nCHANGELOG.md](https://github.com/Betterment/delayed/commit/a8b6c9cd3aedc6f06e6e6421baa827aad7058c14#diff-06572a96a58dc510037d5efa622f9bec8519bc1beab13c9f251e97e657a9d4ed)\nin favor of GitHub release notes.\n\nThis updates the gemspec to properly reflect that change on sites that\nrender a link to the changelog (e.g. rubygems.org).",
          "is_bot": false,
          "headline": "fix: Update changelog_uri to GH releases (#101)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-13T16:53:49Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "1c6c122d00c9e69b9058dcbca338494e0c9b04ae",
          "body": "This releases the latest series of monitor changes as v2.2.0\n\n/no-platform",
          "is_bot": false,
          "headline": "release: v2.2.0 (#98)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-12T21:01:15Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "af898a0f604f6d6ee8ee31fdb5ed7541907e099f",
          "body": "Given that `live_count_grouped` is already necessary for computing all\nthe other live queue metrics, we can reduce the work on MySQL by running\n`failed_count_grouped` (and filtering the scan down to fewer rows)\ninstead of an entire count against the table.",
          "is_bot": false,
          "headline": "refactor: De-complexify 'count' metric (#97)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-12T20:49:20Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "d84ad27cbb0357c59e20f3bc169fc7d3f28b948e",
          "body": "This folds `max_lock_age` and `max_age` (and by extension,\n`age_alert_percent`) into the combined monitor queries introduced in the\nprior commit.\n\nAs before, I split these into two aggregate queries, hinging on whether\n`locked_at` (and therefore, a heap lookup) is required to return a\nresult.\n\n/no-platform",
          "is_bot": false,
          "headline": "perf(monitor): Fold max_* into combo queries (#96)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-12T20:46:22Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "1e3f706c67d38f903e6dbb6737c1fc6766df6236",
          "body": "This combines queries for `count` (live), `future_count`, and\n`erroring_count` into a combined query for \"live\" jobs.\n\nIt also combines `locked_count`/`working_count` and `workable_count`\ninto a combined query for \"pending\" jobs.\n\nThe reason this is still two queries and not one is because on\nPostgr\n[…]\ncan and can\ntheoretically benefit from not having to hit the heap. (Though\nunfortunately it sometimes does require heap visits, depending on how\nrecently a given page has been VACUUMed.)\n\n/no-platform",
          "is_bot": false,
          "headline": "perf(monitor): 4 fewer queries w/ equivalent scans (#95)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-12T20:32:06Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "2b6c0f63de9cf57e546b56d08ff51ca77921d349",
          "body": "After the change in #89 to have `delayed` surface DB-accurate max_age\nmetrics, it appears that PostgreSQL will occasionally report a `run_at`\nthat is milliseconds _after_ the current NOW() in UTC time.\n\nThis is more than just clock drift between the DB and the server that\ninserted the row -- in READ\n[…]\n\n- Switches PostgreSQL to `STATEMENT_TIMESTAMP()` to get even closer to\n  the time at which the `SELECT` actually began.\n- Clamps at 0 anyways because clock drift is still a possibility.\n\n/no-platform",
          "is_bot": false,
          "headline": "fix: clamp max_age metrics at 0 (#94)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-11T15:44:40Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d3bfb4cf6b99e7a886e2d2ac25c1eb095e229559",
          "body": "This is pretty self-explanatory -- the performance impact is probably\nde minimis, but the readability is certainly improved, IMO!\n\n/no-platform",
          "is_bot": false,
          "headline": "refactor: Simpler priority case statement (#93)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-11T14:50:42Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "c74f8e037fdcad8df1f68fb885c2006699d75c88",
          "body": "This only affects the rspec snapshot output, but I was finding that the\ndiffs were getting pretty noisy and wanted to split these out more\ncleanly before making structural changes to the query.\n\n/no-platform",
          "is_bot": false,
          "headline": "style: break aliased columns into multiple lines (#92)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-10T20:30:57Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "79425210b3504bc9340469c938f024273d019e65",
          "body": "…past (#99)\n\nThis catches potential code bugs that accidentally enqueue jobs with\n`run_at` set in the past. Often time we set `run_at` explicitly to\nforward date jobs, but bugs can lead us to accidentally enqueuing jobs\nin the past, which can lead to unnecessary max_age alarms and other\nconfusing metrics about queue max_age.\n\nBy default `deny_stale_enqueues` is false as to not introduce a breaking\nchange.\n\n/no-platform",
          "is_bot": false,
          "headline": "feat: Add config to raise exceptions when run_at sufficiently in the …",
          "author_name": "Harris Effron",
          "author_login": "effron",
          "committed_at": "2026-02-10T20:03:05Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0966d03417610c035f47101cc48e751ceff1b8f4",
          "body": "This is a test-only change that improves the way that query snapshotting\nis done for the monitor process.\n\nPreviously, each metric was given its own example context, testing its\nqueries in isolation. However, in practice, the monitor does not run\nthis way, and it intentionally memoizes query results\n[…]\nnt test coverage!\n\nWith this PR, monitor queries are dumped out into a single snapshot,\ndelineated by a few text dividers for readability by humans (& better\nhinting for the git differ).\n\n/no-platform",
          "is_bot": false,
          "headline": "test: Rework monitor coverage to avoid dups (#91)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-10T18:41:51Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "685c4fb37af235e43ce3b1f40f9cf0a3ec0b72fa",
          "body": "By inserting more rows up front, MySQL will behave more\ndeterministically.\n\n(In practice, I was finding that 10 was slightly too few, and doing it\non every test query is unnecessary if a single example covers multiple\nqueries.)",
          "is_bot": false,
          "headline": "fix: avoid test flakes on mysql (#90)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-10T18:06:46Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "107962162c0b831da5091610894d88bd16a1a0f2",
          "body": "By default, Rails wants to disable eager loading inside of `rake`\ncommands, even if `eager_load` is set to true. This is done to speed up\nthe boot time of rake tasks that don't need the entire application\nloaded.\n\nThe problem is that long-lived processes like `delayed` **do** want to\neager load the \n[…]\nn,\nwhich tells us whether the app has already eager loaded in a `rake`\ncontext.\n- If `rake_eager_loading` is not defined or `false`, we will then check\n`cache_classes` & explicitly eager load if true.",
          "is_bot": false,
          "headline": "fix: Eager load when 'rake_eager_load = false' (#88)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-05T16:50:29Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "c06c1978ef07a30534df4de1b911efc69f443000",
          "body": "This ensures that `max_lock_age` and `max_age` are not artificially\ninflated by slower query times. Previously, if the query took N seconds\nto perform, N seconds would be artificially added to `max_age` and\n`max_lock_age`. (Typically this is not a problem for sub-second queries\non small-medium table\n[…]\nction (as should the state of the rows it returns), and in other\nDB engines it will correspond to a clock time during execution, so there\nis still room for variance but it is much smaller than before.",
          "is_bot": false,
          "headline": "fix(monitor): DB-accurate max_age metrics (#89)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-05T16:49:09Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "ac916129facf873ce77f7d38e9cd28f10495ef47",
          "body": "Say you have a jobs table with 1M rows, 4 named priority ranges, and 2\nqueues.\n\nRather than:\n- `GROUP BY (CASE ... END) AS priority, queue, COUNT(*)` (1M rows)\n\nThis does:\n- [outer] `GROUP BY (CASE ... END) AS priority, queue, SUM(count)` (~8\nrows)\n  - [inner] `GROUP BY priority, queue, COUNT(*)` (1\n[…]\nthis shave ~2-3 seconds off of each\n`delayed:monitor` query in a production table with 10M+ rows. For tables\nwith far fewer rows, the difference is negligible. (And, as always, your\nmileage may vary!)",
          "is_bot": false,
          "headline": "perf(monitor): reduce per-tuple work in hot path (#87)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-02-02T15:50:42Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "838c5a590ab48908d713d3b38f99c432576a5c52",
          "body": "…ation wait timeout) (#86)\n\nPreviously, `with_retry_loop` had wrapped the \"drop\" & \"add\" operations\nseparately. This is fine in the case of lock timeouts, as it will retry\nuntil it acquires a lock as expected.\n\nHowever, if `add_index` experiences a statement timeout (and we have not\nyet exceeded the\n[…]\neSQL, w/ the `:concurrently` opt) an invalid index was left\nbehind. The operation should first remove the invalid index before\nattempting another `add_index` call, and that's what this commit changes.",
          "is_bot": false,
          "headline": "fix: Allow statement timeouts on index creation to retry (within migr…",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-01-05T22:07:34Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d8a44e12fd89266b8832351936ae805e9098c9a9",
          "body": "`@migration_start` was never getting set, and there wasn't any code exercising the non-happy path for these migrations.\n\nThis also bumps the version to 2.0.1",
          "is_bot": false,
          "headline": "fix: Ensure that migration lock timeout loop works as expected. (#85)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2026-01-05T19:57:04Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "62df90556503b0cd9a5d621d783037ae472eaed0",
          "body": "This releases `v2.0.0`, packed with improvements to `delayed`'s overall query performance and index coverage.\n\nNew indexes can be installed with `bundle exec rake delayed:install:migrations` (please review & test any migrations in your own application before shipping them!)",
          "is_bot": false,
          "headline": "build: Ship 2.0.0! (#71)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-19T21:49:20Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f7c87754abaf9cb7789d05d7401c1f23a853a1fa",
          "body": "…#82)\n\nThis does two things:\n1. Avoids querying both `locked_count` and `working_count` separately (since they are synonymous)\n2. Splits the (total) `count` metric into two: `failed_count` and `live_count`\n\nWorth noting that when a scan covers the entire table, **a sequential scan might actually be \n[…]\nurthermore, in postgres at least, these don't just become index scans -- they become _INDEX ONLY_ scans, which should be cheaper than table scans since the index should be significantly more compact!)",
          "is_bot": false,
          "headline": "perf(monitor): avoid querying the same thing twice, avoid seq scans (…",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-11T18:18:21Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "8413694b5a2e7d8fc0fe76a1341471c96872b5c9",
          "body": "… index (#84)\n\nThis PR aims to provide reasonable unit-level assurance that new code can be rolled out _before_ new indexes are applied.\n\nIt applies the same snapshotting strategy to all notable query plans, but generates the plans against a schema with the legacy index (and none of the new indexes)\n[…]\nt structurally the same (if not better). **This does not account for `cost` changes,** but should give us a good signal to ensure that we don't regress an `Index Scan` into a `Seq Scan` (for example).",
          "is_bot": false,
          "headline": "test(migration path): ensure that new queries are compatible with old…",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-11T17:06:13Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "07adf00c9e85bab79101fcda31f28f0981f8b17f",
          "body": "…ing & valid index (#83)\n\nThis allows for proactively adding indexes out of band, so that the migrator will no-op and complete quickly. For very large tables this may be necessary to stay within constraints of synchronous deployment steps (e.g. a timeout on migration processes).\n\nI also adjusted test logger setup so that it relies entirely on log levels, and will log out migration warnings even if debug logging is turned off for the rest of the test suite.",
          "is_bot": false,
          "headline": "fix: Improve `upsert_index` to only drop+rebuild if there is no match…",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-10T20:52:35Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "cc726776815c94954c8b0208fa4388989f28fec4",
          "body": "When using `ActiveRecord.default_timezone = :local` and a local timezone that has a daylight savings \"fall back\" hour, it was possible for future-scheduled jobs to be immediately claimable, producing a \"spinloop\" of retries until the job achieves exit velocity.\n\nThis results from the fact that the D\n[…]\n PR is that **future-scheduled jobs will now be deferred to immediately _after_ the \"fall back\" hour.**\n\n(Jobs that do not specify a `run_at` are not affected and will still be claimable immediately.)",
          "is_bot": false,
          "headline": "fix: prevent DST + `:local`  from breaking job backoff (#81)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-09T20:50:55Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b0a48ace98b85d998c473f4df67f7ad09a1048ae",
          "body": "(...which also includes `run_at` even though it's not in the name.)\n\nI'm relying on tests to prove that nothing this framework does relies _strictly_ on this index anymore. As such, none of the snapshotted query plans have changed! 🎉 \n\n**(Naturally, consumers of this gem can choose to keep any indexes they find useful.)**",
          "is_bot": false,
          "headline": "perf: remove legacy/unused \"delayed_jobs_priority\" index (#78)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-09T15:31:40Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0a3df1e744cce7c1564ad770df239c5de2819cb5",
          "body": "This adjusts pickup queries so that `run_at <= ?` can be included as an `Index Cond` (rather than as a `Filter`).\n\nIt also improves a few monitor queries in minor ways that signifcantly improve the `cost` (not visible in our snapshots, but observed by me in a live testing context).\n\nThe core conceit\n[…]\nd `workable_by` to `claimable_by`\n\nI did **not** adjust any `Delayed::Monitor` metric names. Several of these are already outdated and will merit a migration path, but for now I'm not addressing them.",
          "is_bot": false,
          "headline": "perf: Improve selectivity of pickup query / \"claimed\" states (#80)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-09T15:31:10Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "8a6dcb156f6ff3832d10b84d117de28f7a166719",
          "body": "As I've been testing this series of query/index changes, I noticed that things would hang everytime a new deploy went out. This is because when workers shut down, they perform a cleanup step to clear the locked state for any jobs that they had previously locked (and failed to complete, for whatever \n[…]\n (resulting in a full sequential scan) as there is no index that targets this column. By scoping the update to the `claimed_by(worker)` scope, we can turn it into an index scan against only live rows.",
          "is_bot": false,
          "headline": "perf(worker): avoid sequential scan during worker shutdown (#79)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-08T16:19:07Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "443edb0417810f07551d5113dd60bfa161911a65",
          "body": "…. (#77)\n\nOn PostgreSQL, we already do not index `locked_at` or `locked_by` to optimize for [HOT updates](https://www.postgresql.org/docs/current/storage-hot.html) during job pickup. However, this does not benefit other DBs that don't follow the same MVCC/page architecture.\n\nThis PR:\n- Indexes `lock\n[…]\nker can run it (one to insert the row, one to lock the row). But 50% is the upper bound, so we went with 33% instead to further reduce the odds of page thrashing when picking up multiple jobs at once.",
          "is_bot": false,
          "headline": "perf: HOT updates for PostgreSQL, index `locked_at` for everyone else…",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-05T19:28:38Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "575f92a8272b60edcfcce9f6783988e2163f7231",
          "body": "Postgres and Mysql both indicate that this enables them to use an index-only/covering scan for the `erroring` scope/metric 👍 \n\nIt would be perfectly possible to index on `last_error`, but indexing an `integer` column is a lot more straightforward than the strategies we'd need to employ for a `text` \n[…]\ng. we could index on `WHERE last_error IS NOT NULL` for DBs that support indexes on expressions, but we could also do that with `WHERE attempts > 0` just as effectively, so I'm going with `attempts`.)",
          "is_bot": false,
          "headline": "perf(monitor): use 'attempts > 0' for better indexability (#76)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-05T13:53:53Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b01d8fc9e896686e0b14fa0f489b6edd3cea11f1",
          "body": "Per [this](https://github.com/Betterment/delayed/pull/74#discussion_r2586219398) comment, I've included `WHERE locked_at >= ?` as part of the `claimed_by` scope, making it \"timeout aware\" rather than returning records that have long since had their claim expired.\n\nIn practice, this will have minimal impact on both correctness (slightly better but in the more unlikely cases) & performance (same or a hair worse), so it's more about avoiding unnecessary discrepancies than anything else.",
          "is_bot": false,
          "headline": "fix: Include 'claimed' as part of 'claimed_by' scope (#75)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-04T19:20:16Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "12148a75ea16a517df4609d7dc35716fd5ca8638",
          "body": "… (#74)\n\nSince we already don't index `locked_at` (optimizing for [HOT updates](https://www.postgresql.org/docs/current/storage-hot.html) in PG), this only impacts existing `Filter:` plans.\n\nI believe date comparisons are more expensive than `NOT NULL` checks, but the minor performance difference sh\n[…]\n for some reason we observe this change causing issues, an alternative would be to forego HOT updates and index `locked_at`. (I am already prepping a PR that indexes `locked_at` for non-PG databases.)",
          "is_bot": false,
          "headline": "fix(monitor): Obey lock timeout when reporting claimed/unclaimed rows…",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-03T22:26:50Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "e31fe6ac8533824c528f46fd5f6da183de75e7b6",
          "body": "…hem (#73)\n\nThis fixes a longstanding bug in the monitor queries that caused `future_count`, `locked_count`, and `erroring_count` to include failed jobs.\n\n(The latter sort of comes down to semantics, but \"erroring\" to me implies that it has not yet terminally failed. I also checked and it was out of\n[…]\nt stuck at a nonzero baseline.\n\n💪 Conveniently, this also improves the query plans for all three fields. (`future_count` even becomes an `Index Only` (postgres) / `Covering index` (mysql) scan/lookup!",
          "is_bot": false,
          "headline": "fix(monitor): exclude failed jobs from metrics that shouldn't count t…",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-03T18:26:11Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "bbd6815dfeb17ef0a65eb0bea6b8f4e313f8dac7",
          "body": "/no-platform\n\nThis also makes `AddIndexToDelayedJobsName` make use of it, and fixes a\nminor spec bug I found with Rails 6.0 appraisals where `drop_table` was\nnot getting run (because `:if_exists` wasn't added until 6.1)",
          "is_bot": false,
          "headline": "fix: Ensure that upsert_index is reversible (#72)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-03T17:27:02Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "17be23f401e0447611f56346de4a065a251ec7d1",
          "body": "This PR is the first of several query optimizations, and can largely be read in two parts:\n\n1. A pair of heavily-annotated migrations that add new index(es) as safely as I could muster generically (w/ timeouts and a lock retry loop). I also updated the `README` w/ some expanded instructions and ⚠️ s\n[…]\n `Table scan` to `Covering index range scan`\n        - `max_lock_age`, `max_age`, `working_count`, `workable_count`,  and `alert_age_percent` go from `Table scan` to `Index lookup` w/ index conditions",
          "is_bot": false,
          "headline": "feat(perf): New indexes for job pickup & monitoring (#70)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-02T18:53:25Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "dafa66ec0ca4ed42ab46a42bae135ac8e6d06842",
          "body": "This adds adapter-specific EXPLAIN outputs to the snapshot files introduced in #67.\n\nThe goal is to test that available indexes are usable, and enable us to flag changes/regressions in query plans going forward. **The goal is NOT to test or predict performance.** These are essentially \"unit\" tests f\n[…]\nstable to me, so I wasn't finding a need to attempt to trick it into doing anything different.\n\nThis depends on #68 being merged first (mostly because I want to use mysql's new \"TREE\" plan formatter).",
          "is_bot": false,
          "headline": "test: Snapshot test all EXPLAIN query plans (#69)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-01T21:35:24Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "710c9f9f1d8c1f4c461d20012c45f47f1e853c42",
          "body": "I'd like to get this change to our workflow merged so that subsequent\nPRs can run their tests w/ known-passing versions of their DB\ndependencies. As we are starting to more directly cover the raw adapter\noutputs, this will help us prevent CI from randomly breaking on its own.\n\n/no-platform",
          "is_bot": false,
          "headline": "test: Normalize DB versions used in CI (#68)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-01T20:15:06Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0c45793f4ff330264b3f3b44d741bf888cbbfac2",
          "body": "I'm introducing @rzane's `snapshot_testing` as a means to:\n- Make iterating on the queries a lot easier. (Removes all the manual work required in updating spec files, but still flags notable SQL differences in PRs so they can be reviewed before they ship.)\n- Removes the need to strip/normalize delim\n[…]\n instead of two now.\n\nThis might feel like a big diff, but it's really just capturing what's already there, and I very much intend to make use of this pattern to make several changes more confidently.",
          "is_bot": false,
          "headline": "test: Use snapshots for easier SQL testing/iteration. (#67)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-01T19:42:26Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "820ca77b7f3b15f1b8929617e7037512dfa8e1d3",
          "body": "…Error (#66)\n\nThis fixes a regression flagged by #64. It was originally addressed in\nddd7a1c288b6425cac320e3de410d2dea51fbb86 but got reverted by [a linter\nauto-correct](https://github.com/Betterment/delayed/commit/78bd49e75638bc40c6c437d2848bd5dc31767ad0)\n🤦.\n\nThis reverts the revert, and adds a regression test that proves that\n`rescue StandardError` does not inadvertently catch\n`Delayed::WorkerTimeout`, allowing it to bubble out and be handled by\nthe worker.",
          "is_bot": false,
          "headline": "fix: max_run_time timeout error should not be rescuable as a Standard…",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-01T19:07:24Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "4813b1a7501639d2b353691c38e7d7e8e5bf0a39",
          "body": "There are other ways to slice this, but because of the threaded nature\nof the job runners, `Timecop` wasn't cutting it. This gives us ~1.5\nseconds of tolerance rather than ~1 and seems to have fixed my local\nreproduction of the flake (running tests in a loop for a long time).",
          "is_bot": false,
          "headline": "fix(tests): timing issue on test flake (#65)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-12-01T18:42:36Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "ed8b6cebbdf4ee167aeb9bd0b8499895399f030e",
          "body": "The goal is to create some unifying structure around the way that scopes\nare defined and organized in the ActiveRecord `Job` class, without\nchanging the actual SQL in any meaningful way. (This sets me up to make\nmeaningful SQL changes in future PRs!)\n\nIn addition to using `Arel` for `gt`/`lt`/`gteq`\n[…]\n `pending` (where run_at <= now)\n\nAgain, this should **not** meaningfully change any SQL! Only minor\nformatting changes without changing the meaning of each query. (I'm\nusing the tests to prove this.)",
          "is_bot": false,
          "headline": "refactor: Remove 'ready_scope' and improve remaining scopes. (#63)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-11-24T22:46:32Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "f8dad798e68e6fd30d23f750434799416a02381f",
          "body": "…ss-wide config (#62)\n\nIdeally `max_run_time` is the same across an entire worker pool, because\notherwise workers might pick up jobs that are still held by other\nworkers in the pool.\n\nSo it has never made sense (based on our current pickup/monitor logic)\nthat this would be configurable on a per-worker-instance or per-query\nbasis.",
          "is_bot": false,
          "headline": "refactor: Add 'lock_timeout' & clarify that 'max_run_time' is a proce…",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-11-21T21:07:05Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "16c000c047df34018deb0a70cfc5a20c24d3f24d",
          "body": "This is the first in a series of PRs where I will aim to rework &\noptimize this gem's query logic. I ultimately aim to add new index(es),\nand generally improve the indexability & runtime performance of these\nqueries, and will produce a new major release at that point.\n\n👉 But step one is to **test th\n[…]\nrbose, but I argue that if the\nlibrary is capable of producing N distinct queries, we should have N\ntests, and look for ways to reduce the number of queries (rather than\nmaking the tests more clever).",
          "is_bot": false,
          "headline": "test: Add \"golden\" tests for worker/monitor SQL (#61)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-11-20T22:29:14Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "33a485b2063645f91d8951970d6f2d39b5cc115d",
          "body": "This updates the method & tests to properly ensure that the column value\ngets assigned before_save.\n\nReason: The `||=` operator results in no assignment, because `name` is\noverridden for backwards-compatibility if the column does not exist.",
          "is_bot": false,
          "headline": "fix: Fix set_name callback, and improve spec coverage (#60)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-10-06T19:18:32Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "8a3590393e3b668dec1627a2bed633a67a7f8cef",
          "body": "This adds a `delayed_jobs.name` column that will be populated by default\nwhen each job is enqueued. While this column starts as nullable (to\nensure zero downtime is required to make this change), it will be\npromoted in a future release to an expected column. **Update: this\nfuture release will be TBD\n[…]\nin the table, etc)\n\nThis is the first real schema change we've made, and as such I had to\nrewire a little bit of plumbing to make it easier to drop a new\nmigration into an existing app (see `README`).",
          "is_bot": false,
          "headline": "feat: add `delayed_jobs.name` column (#59)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-10-03T14:10:20Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d46a0fb3a4032af549d5226fa57c8ebe51665827",
          "body": "**Summary of changes:**\nThis pull request drops support for the oldest Rails and Ruby versions,\nin order to streamline the supportability bracket. By doing so, It also\ncuts a minor version release.",
          "is_bot": false,
          "headline": "Drop support for Rails 5.2 & Ruby 2.7 (#58)",
          "author_name": "Irving C.",
          "author_login": "Irving-Betterment",
          "committed_at": "2025-09-12T21:12:12Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "397dbe3176ddfd280be7b945e57f7725173f6cb8",
          "body": "This PR includes automated changes from running\n[frizbee](https://github.com/stacklok/frizbee) to pin Github Actions.\nFor more information on what this actually means, take a look at [our\ndocumentation on\nUnpinnedActions](https://github.com/betterment/claws?tab=readme-ov-file#unpinnedactions).\nBecau\n[…]\ncd6fda/.github/workflows/claws_fork_friendly.yml#L50-L54))\n\nFor other findings and how to remediate them, check the [Claws\ndocs!](https://github.com/betterment/claws?tab=readme-ov-file#built-in-rules)",
          "is_bot": false,
          "headline": "chore: Pin Github Actions to Commit Hashes (#56)",
          "author_name": "Omar A.",
          "author_login": "6f6d6172",
          "committed_at": "2025-07-01T22:24:59Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "7e5a1123414ec3265ff13fcc3e2fd164648813e7",
          "body": "This PR bumps us to `1.0.0` because in cleaning up our lifecycle hooks,\nI made a breaking change to remove the `job` argument from the `:thread`\nlifecycle callback. Coupling threads & jobs 1:1 was an implementation\ndecision, and we are restricting ourselves by baking that decision into\nthe callback \n[…]\nattempting to work\noff the same jobs over & over. (Even with `locked_at`, these workers can\nslowly degrade if enough jobs are consistently stuck in a locked state\npast the lock timeout.)\n\n/no-platform",
          "is_bot": false,
          "headline": "Delayed 1.0: Lifecycle & job cleanup improvements (#55)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-05-08T20:15:20Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "427bc304715961e5c56c3b08d90bfc6a43b0d87b",
          "body": "I'm adding regression coverage for the changes made in #53, and making\none additional change in line with the way that the `:perform` callback\n[works in the `delayed_job`\ngem](https://github.com/collectiveidea/delayed_job/blob/ea4879dd3c2f3f5aa7f616a6f0fd58f2bbc2a481/lib/delayed/worker.rb#L313)\n- th\n[…]\nast\npriority to a float, which was raising a method undefined error. So I\nadded `Delayed::Priority#to_f` and added a test to cover all the\nexplicit conversion methods (`to_s`, `to_i`, `to_f`, `to_d`).",
          "is_bot": false,
          "headline": "0.8.0 - `Priority#to_f`, plus some regression coverage (#54)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-04-03T17:24:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "2ea5f07954a89242d74af3b0990d3b1f0281b0df",
          "body": "/no-platform\n/domain @argvniyx-enroute @mavenraven \n\nPR https://github.com/Betterment/delayed/pull/42 inadvertently flipped the order of `:thread` and `:perform`, and also pushed `:thread` far enough in that cleanup steps would happen after the `with_connection`'s`end` block in the connection plugin.\n\nThis introduces the possibility of thread safety issues, or connections being held longer than intended (exhausting the connection pool / connection limits).",
          "is_bot": false,
          "headline": "[fix] Thread callback ordering (#53)",
          "author_name": "Nathan G",
          "author_login": "smudge",
          "committed_at": "2025-04-02T22:20:01Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "b5abf39a0b8fcc23c31534f85c65f14f4b1437e6",
          "body": "### Description\r\n\r\nUpgrade betterlint 1.18.0 dependencies and regenerate RuboCop todo.",
          "is_bot": false,
          "headline": "Upgrade betterlint to 1.18.0 (#52)",
          "author_name": "Dan Kubb",
          "author_login": "dkubb",
          "committed_at": "2025-02-03T18:35:47Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f0fcc71fac3ed3adecccdd917a41f769cb6f8460",
          "body": "I configured deprecations to raise exceptions.\r\n\r\nOnce I did that, I had to suppress deprecations from `enqueue_after_transaction_commit = :always` and `enqueue_after_transaction_commit = :never`.\r\n\r\nThose values are still supported, but deprecated. Rails 8.0 now expects `true` or `false`. I've updated delayed to ensure that `enqueue_after_transaction_commit` is not set to `true`.",
          "is_bot": false,
          "headline": "Fix Rails 8.0 deprecation (#51)",
          "author_name": "Ray Zane",
          "author_login": "rzane",
          "committed_at": "2025-01-24T21:10:27Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "7e184ba7f8207e78b6ac9be3bc3893b3bf5a2072",
          "body": "Adding 3.4 is a bit more complicated, because no versions of Nokogiri\r\nsupport 3.0 and 3.4. We'd need separate gemfiles, ro we'd need to only\r\ntest Ruby 3.4 against specific gemfiles.",
          "is_bot": false,
          "headline": "Add Ruby 3.3 to the CI matrix (#50)",
          "author_name": "Ray Zane",
          "author_login": "rzane",
          "committed_at": "2025-01-22T16:17:01Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "a8b6c9cd3aedc6f06e6e6421baa827aad7058c14",
          "body": "This PR runs our test suite against Rails 8 on CI.\r\n\r\nThere was only one major change. `ActiveSupport::ProxyObject` was\r\ndeprecated in 7.2 and removed in 8.0.",
          "is_bot": false,
          "headline": "Rails 8 support (#49)",
          "author_name": "Ray Zane",
          "author_login": "rzane",
          "committed_at": "2025-01-15T16:01:30Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "c8ac357f005081cad6076c605c7c81c6db03c055",
          "body": "… for now) (#48)\n\nThis is related to #41 and #42 insofar as a kind of DB-bound \"spinloop\"\r\nis still possible if a worker picks up jobs that take so little time\r\nthat the worker immediately turns around and asks for more. As of now,\r\nthere has been no way to tune the amount of time a worker should wa\n[…]\nracted from `min_reserve_interval` when\r\nthe worker sleeps, and if jobs take more than `min_reserve_interval` to\r\ncomplete than the worker will not sleep before the next reserve query.\r\n\r\n/no-platform",
          "is_bot": false,
          "headline": "Add a config for enforcing a minimum job reserve interval (default: 0…",
          "author_name": "smudge",
          "author_login": "smudge",
          "committed_at": "2024-12-18T18:12:39Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "c0c36c0bed9fc62ab0cb0790e786ba9e4e6ae9ed",
          "body": "/no-platform\r\n\r\nmore updates @smudge",
          "is_bot": false,
          "headline": "update rails deps to fix security alerts (#45)",
          "author_name": "Sam Moore",
          "author_login": "samandmoore",
          "committed_at": "2024-08-20T14:25:40Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "a352b1dec688fa9025ca1cb4532a9c2f73a53609",
          "body": "just what it says. we've got some dependabot alerts for these deps, so\r\nupgrading to fix that.",
          "is_bot": false,
          "headline": "update rexml + nokogiri to fix security alerts (#44)",
          "author_name": "Sam Moore",
          "author_login": "samandmoore",
          "committed_at": "2024-08-20T14:16:15Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "37dff237d53040abd6984990942f5c27f05db693",
          "body": "…or Rails 7.2 support (#43)\n\nHi Betterment friends!\r\n\r\nAs addressed in @smudge's PR\r\nhttps://github.com/Betterment/delayed/pull/40, Rails 7.2 introduces the\r\nconcept of `enqueue_after_transaction_commit`, which should not be used\r\nin conjunction with Delayed. The implementation, and the associated\r\n\n[…]\n13a4899c9f996ae51f54cdb8fa5f79f50efab872f9aR20)\r\nwhere `!!:never == true` and causes the condition to be true (and\r\nraising the exception) when `job.class.enqueue_after_transaction_commit\r\n== :never`.",
          "is_bot": false,
          "headline": "Handle proper values for ActiveJob.enqueue_after_transaction_commit f…",
          "author_name": "Chris Zega",
          "author_login": "warmerzega",
          "committed_at": "2024-08-13T21:55:22Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "fd97a38875cf3eaa2081972f2bbf4405bffc09af",
          "body": "This ensures that exceptions raised in thread callback hooks are rescued\r\nand properly mark jobs as failed.\r\n\r\nThis is also a good opportunity to change the `num` argument (of\r\n`work_off(num)`) to mean number of jobs (give or take a few due to\r\n`max_claims`), not number of iterations. Previously (be\n[…]\n we only\r\nincrement `success` and treat `total - success` as the \"failure\" number\r\nwhen we return from the method.\r\n\r\nFixes #23 and #41\r\n\r\nThis is also a prereq for a resolution I'm cooking up for #36",
          "is_bot": false,
          "headline": "Fix spin-loop/cleanup failure mode within run loop (#42)",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2024-06-27T16:35:43Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "24b75f56c43e470ecdec2d3afdcc11bb21a6e2e3",
          "body": "### Summary\r\nThis PR Adds a configuration value (`.assign_at_midpoint`) to `Delayed::Priority` \r\n\r\nThe intent is to allow users to change the default priority of a job (i.e. when using a named priority) to be the middle of the configured priority ranges -- derived from the default or custom priority\n[…]\n\n\r\n**Note**: since the last priority uses a range that is infinite, it's not possible to derive the midpoint. So, for now, I've defaulted to adding 5, since that matches our defaulted priority values.",
          "is_bot": false,
          "headline": "Allow configuring default priority value at midpoint of range (#39)",
          "author_name": "Terrance Koar",
          "author_login": "tkoar",
          "committed_at": "2024-04-29T17:16:29Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "9a328c6e0518bbd141980653140379a12d6f8c98",
          "body": "…commit) (#40)\n\nThis anticipates the release of a new feature in Rails 7.2 introduced here: https://github.com/rails/rails/pull/51426\r\n\r\nIn Rails 7.2, ActiveJob will default `enqueue_after_transaction_commit` to `true`, and we would like to explicitly set this to `false` (retaining transactional enq\n[…]\n I also:\r\n- Added a new Appraisal for the latest Rails main build on GitHub.\r\n- Re-generated the rubocop todo file. There are changes we can pull out & autocorrect, but we can do them in separate PRs.",
          "is_bot": false,
          "headline": "Prepare for Rails 7.2 support (and disable enqueue_after_transaction_…",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2024-04-29T17:15:40Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "77b97e9f540f0cce849de4c9665f9963d3a15c47",
          "body": "We commit the lockfiles now, so version changes need a re-bundle. (I\r\nmissed this in the last PR.) Once this is in I can cut a new release.",
          "is_bot": false,
          "headline": "Fix the lockfile / linter build step (#38)",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2024-01-31T03:45:34Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f2e5edc5d3f9481a9636d967d5cbe7ac191995eb",
          "body": "This PR introduces a change to avoid calling `ActiveJob::Base#deserialize` during queueing the job whilst maintaining existing functionality.\r\n\r\nThe change is in `JobWrapper`, which previously only took in a hash of `job_data` and knew how to deserialize it into a job object so we can interrogate it\n[…]\nialize` during the enqueuing, even those adapters that support invoking methods on the Job class instance like delayed does.\r\n\r\nFrom one angle it's an optimization, from another angle it's a bugfix (:",
          "is_bot": false,
          "headline": "Optimize JobWrapper queueing ActiveJobs (#35)",
          "author_name": "Caius Durling",
          "author_login": "caius",
          "committed_at": "2024-01-31T03:38:12Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "869c08a893b97d8fad51b2cc5c08e8e151c74b1b",
          "body": "Committing the lockfile triggered a bunch of dependabot errors, and I\r\nrealized that the versions committed were very out of date, so this\r\nupdates the lockfile with newer versions.\r\n\r\n/no-platform",
          "is_bot": false,
          "headline": "Address dependabot alerts (#34)",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2024-01-08T18:23:41Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "737f2fd4eaa149758bce30da299e07a09489c680",
          "body": "**Summary of changes:**\r\nCommit lockfile previously ignored (see\r\nhttps://bundler.io/guides/faq.html#using-gemfiles-inside-gems).\r\n\r\n/task https://app.asana.com/0/0/1206244989881229/f\r\n\r\n---------\r\n\r\nCo-authored-by: Irving Caro <112433591+betterment-coding-agent@users.noreply.github.com>",
          "is_bot": false,
          "headline": "Add lockfile (#32)",
          "author_name": "Irving Caro",
          "author_login": "Irving-Betterment",
          "committed_at": "2024-01-08T17:58:06Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "342acb982a575ecfbd5693bddbb1feab96b56589",
          "body": "This fixes #29, which can be reproduced on new Rails 7.1.1 apps in\r\ndevelopment.\r\n\r\nThe issue stems from the fact that Rails no longer loads ActiveJob at\r\nall until the first `ActiveJob::Base` class is referenced, but we\r\ndefined our `Delayed::JobWrapper` class in a file that only loads\r\n_after_ Act\n[…]\napper` out into its own file in `lib/`\r\nand load it when `lib/delayed.rb` loads. The class itself has no\r\ndependencies outside of `activesupport` (`delegate_missing_to`) so this\r\nis safe enough to do.",
          "is_bot": false,
          "headline": "Fix Rails 7.1 regression: Delayed::JobWrapper not defined (#30)",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2023-10-19T18:26:00Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "429e20f0baddb1205d5ee7be7082e2a4afcd1f0e",
          "body": "This adds Rails 7.1, Ruby 3.1, and Ruby 3.2 to the build matrix.\r\n\r\nThe gem already works with these versions, but now they are formally\r\ntested. (As such, I bumped the gem by a patch version, to signify that\r\nthis is the first verified-compatible release.)",
          "is_bot": false,
          "headline": "Rails 7.1, Ruby 3.1, and Ruby 3.2 (#28)",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2023-10-11T16:10:02Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "66125d123cff8742e729503dfdd1cc2dac7bb34a",
          "body": "…inconsistency (#24)\n\nThe issue here is that some ActiveJob-enqueued jobs would include the `@job` ivar in their serialized handler, which is redundant with `@job_data` and only existed for memoization purposes. We can use Ruby's `encode_with` API to ensure that we only encode `@job_data` into the h\n[…]\nsubsequent retries) occur. `DeserializationError` is really supposed to be only for cases where we fundamentally can't even attempt the job and allow the normal pattern of exception-handling to occur.",
          "is_bot": false,
          "headline": "Reduce handler size in some ActiveJob cases, and fix deserialization …",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2023-01-20T22:02:09Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "f7a27c8da949ec73f0fc0fe06182788544f497fc",
          "body": "There are a couple linter rules complaining. This will ensure that anyone who branches off of main won't have to fix them independently.\r\n\r\nThis also fixes the Ruby 3.0 build so that it doesn't inadvertently run with Ruby 3.1 in CI.",
          "is_bot": false,
          "headline": "Fix main builds: appease the linter (#14)",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2022-02-11T20:27:44Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "abad99fb2562a22e7d924c597b235dc4c74f1ae9",
          "body": null,
          "is_bot": false,
          "headline": "Move appraisal onto official Rails 7.0.0 release (#9)",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-12-16T03:43:39Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "6d9d831a731f20595f99655c3ae5436003caaeca",
          "body": "Resolves #6. While we already fully support Ruby 3.0 kwargs via ActiveJob, the legacy `delay` and `handle_asynchronously` APIs did not support the new [separation of positional and keyword arguments](https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-\n[…]\npt keyword arguments. It should also support jobs that were enqueued by the prior `delayed` gem version (where the new `kwargs` accessor would be nil, and `args` contains the kwargs as its last item).",
          "is_bot": false,
          "headline": "Support Ruby 3.0 kwarg-handling within `.delay` API (#7)",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-11-30T21:30:50Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "2d296b9f49c24c99b91d8bf78690a20436811bae",
          "body": "This:\r\n- Adds an `Appraisal` entry for `7.0.0.alpha2`. (I had to specify multiple gems to get the pre-release dependencies to resolve properly.)\r\n- Bumps `.ruby-version` to `2.7`, but keeps linting at `2.6`. (CI will still run against all of 2.6-3.0)\r\n- Fixes a deprecation warning about `ActiveRecor\n[…]\nassic` autoloader is no longer used/supported in Rails 7)\r\n- Autocorrects some new linter rules pulled from the latest version of the linter gem\r\n- Bumps the version to 0.3.0 and updates the CHANGELOG",
          "is_bot": false,
          "headline": "Add support for Rails 7 (alpha2) (#3)",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-10-26T19:52:48Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d82f789360a8c82404646189441b3739b545859d",
          "body": "I just realized that we hadn't cross-referenced this in the other direction!\r\n\r\nThe linter issues are fixed in #3",
          "is_bot": false,
          "headline": "Plug the blog post in the README (#4)",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-10-26T19:25:48Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "484d68a7a1eea573d52fe12a963002925b9171e9",
          "body": "This fixes an issue I was seeing on a very slim Rails 6.1 app -- essentially, `ActiveSupport.on_load(:active_record)` was not triggering at all during `rake delayed:work`, resulting in a missing constant error. Previous versions of rails seem to load `ActiveRecord::Base` during boot, but 6.1 seems t\n[…]\n to `syck` which dates back to Ruby 1.9, and we should consider removing support for it entirely. (It can still be installed via the `syck` gem, but `psych` has been the default for many, many years.)",
          "is_bot": false,
          "headline": "Minor Rails 6.1 fix, plus autoload Delayed::Job constant (#2)",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-09-02T16:45:07Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "941f02da326ffe13280d5a1cf4c1683a22f886d2",
          "body": "This fixes the gemspec description, which had previously been written in rdoc format (causing it to appear garbled on rubygems.org). It also adds a CHANGELOG and bumps the version to `0.1.1`.",
          "is_bot": false,
          "headline": "Fix rubygems description and add CHANGELOG (#1)",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-08-19T20:29:25Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "844bbca1e397b500f77eb9d2ecdfef94376cd766",
          "body": null,
          "is_bot": false,
          "headline": "Don't allow alert_age_percent to exceed 100",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-07-22T20:39:57Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "4c61327ec887d229c96e74fb227c2bb5040ca22e",
          "body": "Hello! Here's another stab at the `README` that attempts to incorporate\nall of the feedback I've received, plus describe the zillions of big and\nlittle things that have changed in the 30+ PRs since that initial\ndraft!!!\n\nThis also changes the default queue name to `'default'` (to align with\nActiveJob), and renames the install command from\n`rails g delayed_job:active_record` to `rails g delayed:migration`",
          "is_bot": false,
          "headline": "A README: Our vision for `delayed`",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-25T18:00:05Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "a65965a1a1b158772028fd9ffc2b3e0007ddee24",
          "body": "We were investigating a bug where jobs were not being enqueued with the\ncorrect priority. To rule out `ActiveJob`'s `queue_with_priority`\nconfig, I added this spec. It turns out that it works (the actual bug\nwas with `ActiveJob`'s `retry_on` behavior), but is helpful test\ncoverage to have!",
          "is_bot": false,
          "headline": "Add test for queue_with_priority",
          "author_name": "Harris Effron",
          "author_login": "effron",
          "committed_at": "2021-06-23T17:47:34Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "d8526e62fd31bdb71ff19c6bc704b9f507320d3d",
          "body": "This really only applies in legacy cases where we set DELAY_JOBS=0 -- if\njobs are run inline, they are never technically locked, so there is no\nway of tracking `run_time` via anything db-backed.",
          "is_bot": false,
          "headline": "Fix run_time_alert? when job has no run time",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-22T18:43:07Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "714ba2259806ffeef7808991a667f6ac573ff9f5",
          "body": "🤦 🤦 🤦\n\nI've been passively trying to figure out why a few of the metrics just\ndidn't seem to line up. Suddenly it struck me while I was making tea\nbefore bed, and so I felt compelled to get a fix up.\n\n...it's because we cached `@as_of = Job.db_time_now` when initializing\nthe monitor instance. I had \n[…]\noves the cleverness and goes back to the old strategy of\nusing an ever-changing `Time.zone.now` (provided by `Job.db_time_now` -\nto be consistent with the rest of the codebase) throughout the queries.",
          "is_bot": false,
          "headline": "Fix metrics that depend on db_time_now",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-22T13:02:52Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "4f92a30a54a105518fc278aa7002b413ce4f5974",
          "body": "I confirmed that this `:eager_load!` initializer is available across all\nof our supported Rails versions. It varies slightly depending on Rails\nversion, so this ensures that the gem's eager-loading behavior will use\nthe same code as whatever Rails version is running, because we call\ndirectly into Rails' eager_load initializer.",
          "is_bot": false,
          "headline": "Run the full eager-loading initializer",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-17T18:02:52Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "ca8a55b9efb3555f63c6cb1ff8bf267862c64df9",
          "body": "…a single value\n\nI baked this check into the matcher, so if it detects events with the\nsame payload but _different_ values, the tests will fail. I then used\nthis to fix the bug and get the tests green.\n\nThis should fix the weird issue where gauge metrics cycle between real\nvalues and zero with no rhyme or reason -- it's because we were emitting\nduplicate metrics with value zero!",
          "is_bot": false,
          "headline": "Ensure that events with matching payloads are only emitted ONCE with …",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-16T21:30:46Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "190a32b79d5ff62abc3ae93a2f13bbc16f689aa5",
          "body": "Rails always sets `eager_load` to `false` inside of rake tasks, which we\nnever noticed because either:\n\n- we were running the `:classic` autoloader with the single-threaded\n  implementation, so autoloading worked, or\n- we were running the `:zeitwerk` autoloader, which is thread-safe\n\nOn Rails 6.1, w\n[…]\nqueued JobClass (Job ID: SOME-UUID) to Delayed(default)\n```\n\nTo demonstrate the underlying reason:\n\n```\nclass B; end\nA = B\nA.name # => 'B'\n\nA = Class.new(B) # A now inherits from B\nA.name # => 'A'\n```",
          "is_bot": false,
          "headline": "Work around Rails' disabling of eager_load during rake tasks",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-14T17:31:22Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "c7fd37a643b4239113564b8a2c51a4b67d301f39",
          "body": "The way these ENV var handlers were set up was overriding the default\nvalue we set in our initializer (which runs as part of `:environment`).\nThis must be why we relied on a monkey patch (`def self.queues`) rather\nthan setting the queues config directly. Glad to finally have gotten to\nthe bottom of this 😌",
          "is_bot": false,
          "headline": "Fix 'queue(s)' opts when running via rake tasks",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-14T14:16:02Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "6a518c0b1e24467eef4c130caac9104795f3917e",
          "body": "During initialization, Rails uses the `cache_classes` and\n`reload_classes_only_on_change` config to set a \"`check`\" lambda on its\nreloader config:\n\n```ruby\n        if config.cache_classes\n          app.reloader.check = lambda { false }\n        elsif config.reload_classes_only_on_change\n          app\n[…]\n\n\nSo I'm replacing this gem's implementation of `reload_app?` with Rails'\nbuilt-in `.check!` method (which calls the lambda and caches `true`\nvalues until you call `reload!`, which we do immediately).",
          "is_bot": false,
          "headline": "Optimize 'reload!', prevent reloading app every time",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-14T14:15:53Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "6068ad9e7b09b1bc2a17233cdd7f117999391608",
          "body": "I assumed that since tests were passing, I had covered all of my bases.\nBut it turns out we pass opts into `Worker.new` inside of the rake\ntasks, and these were not tested. 🤦 🤕",
          "is_bot": false,
          "headline": "Fix rake tasks, and actually test them",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-08T21:14:15Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0e02ab44b3c90369ee55f3f77573bcb79180327a",
          "body": "We used the ability to override `attempt_alert_threshold` downstream in\na couple spots. I considered just ditching that functionality with\n`alert_attempts`, etc, but decided that it was easy enough to enable for\nnow.",
          "is_bot": false,
          "headline": "Allow overriding per-job alert thresholds",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-08T18:10:19Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "ebd8c14766057003a02b60da4839e3b055dbd56f",
          "body": null,
          "is_bot": false,
          "headline": "Fix test flake: SimpleJob is not threadsafe",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-08T16:54:08Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "cc63b47e373d5f11e6dba06219f04511085bf41c",
          "body": "I caught some issues in runtime relating to ActiveJob default job\npriority behavior.\n\nThis makes two changes:\n\n1. If a priority option is _not_ passed to `set`, this ensures that it\n   doesn't get overridden to `0`. (Essentially, it disobeys class-level\n   priorities that can be set via ActiveJob. -\n[…]\nPreparer` to watch for `payload_object.priority` if it\n   responds to it. (This seems to have been a pre-existing bug in DJ\n   that would prevent the use of ActiveJob class-level priority\n   configs.)",
          "is_bot": false,
          "headline": "Fix default priority behavior with ActiveJobs",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-08T16:52:47Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "1bebd6877bf3cb4f319564b98ad1f7753e97e002",
          "body": "This is my attempt to incorporate the scaling metric\n(`delayed.job.alert_age_percent`) into the monitor process, as well as\nmore general priority-based alert thresholds that can be used to\ndetermine whether or not to fire off alerts when subscribing to the\nActiveSupport notifications.\n\nAlert thresho\n[…]\nic to each priority name. (I\nconsidered adding global defaults too! But I wanted to see how this\nplayed out before making it more complicated.)\n\nCo-authored-by: Harris Effron <harris.effron@gmail.com>",
          "is_bot": false,
          "headline": "Priority based alerts and autoscaling",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-07T20:24:07Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b278de29fec7b0915dfc26c9668357951ac75246",
          "body": "…tate\n\nAfter chatting offline, we decided to ditch the idea that worker\ninstances could hold their own config state, and instead rely on global\nstate for all of these configs. Things behave mostly the way they did\nbefore now, except that worker instances _don't have setters_ for any of\nthese configs. The only way to change a config is to set it globally on\nthe class.",
          "is_bot": false,
          "headline": "Change default priority to 10, max_claims to 5, and simplify worker s…",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-07T18:02:39Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0f1638c6482303751d0c033090c21d2ddf4f9cb7",
          "body": "We had talked about these! Figured I'd take a stab at them.",
          "is_bot": false,
          "headline": "Add additional instrumentation around job pickup and enqueue",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-07T15:52:18Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "b0db8a46caa39c21f13fb641b3347c65f6d2bcc9",
          "body": "The other new defaults that I had already changed are `max_claims` at 2\n(up from 1) and `destroy_failed_jobs` (set to `false` now).",
          "is_bot": false,
          "headline": "Set max_run_time to 20 minutes by default",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-04T14:57:54Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "993b757b345a40d112f3c00533054cfc6b5cf02f",
          "body": "…pters\n\nI noticed this when running tests that use the `:test` queue adapter,\nbut it would be true of any other ActiveJob adapters.\n\nActiveJob tries to call `.to_i` on priority (which doesn't work on\nsymbols), which is why I had originally passed it through\n`provider_attributes` instead, but I realized that it's better to just\ntype-cast to a `Delayed::Priority` explicitly so that `to_i` works as\nexpected.",
          "is_bot": false,
          "headline": "Prevent adapter patches from hiding job priority from other queue ada…",
          "author_name": "Nathan Griffith",
          "author_login": "smudge",
          "committed_at": "2021-06-03T19:18:01Z",
          "body_truncated": false,
          "is_coding_agent": false
        }
      ],
      "releases_count": 26,
      "commits_last_year": 52,
      "latest_release_at": "2026-07-13T15:29:31Z",
      "latest_release_tag": "v3.1.0",
      "releases_from_tags": false,
      "days_since_last_push": 8,
      "active_weeks_last_year": 18,
      "days_since_latest_release": 8,
      "mean_days_between_releases": 31.1
    },
    "community": {
      "has_readme": true,
      "has_license": true,
      "has_description": true,
      "has_contributing": false,
      "health_percentage": 37,
      "has_issue_template": false,
      "has_code_of_conduct": false,
      "has_pull_request_template": false
    },
    "ecosystem": {
      "packages": [
        {
          "name": "delayed",
          "exists": true,
          "license": "MIT",
          "keywords": [],
          "ecosystem": "rubygems",
          "matches_repo": true,
          "registry_url": "https://rubygems.org/gems/delayed",
          "is_deprecated": false,
          "latest_version": "3.1.0",
          "repository_url": "https://github.com/betterment/delayed",
          "versions_count": 29,
          "total_downloads": 1132655,
          "dependents_count": null,
          "deprecation_note": null,
          "maintainers_count": null,
          "monthly_downloads": null,
          "first_published_at": "2021-08-17T14:19:16.612000Z",
          "latest_published_at": "2026-07-13T15:29:09.213000Z",
          "latest_version_yanked": null,
          "days_since_latest_publish": 8
        }
      ]
    },
    "popularity": {
      "forks": 12,
      "stars": 175,
      "watchers": 44,
      "fork_history": {
        "days": [
          {
            "date": "2021-08-19",
            "count": 1
          },
          {
            "date": "2022-08-17",
            "count": 1
          },
          {
            "date": "2022-10-02",
            "count": 1
          },
          {
            "date": "2023-03-02",
            "count": 1
          },
          {
            "date": "2023-10-15",
            "count": 1
          },
          {
            "date": "2023-12-20",
            "count": 1
          },
          {
            "date": "2024-04-02",
            "count": 1
          },
          {
            "date": "2024-08-13",
            "count": 1
          },
          {
            "date": "2024-10-21",
            "count": 1
          },
          {
            "date": "2026-02-12",
            "count": 1
          },
          {
            "date": "2026-05-21",
            "count": 1
          },
          {
            "date": "2026-05-28",
            "count": 1
          }
        ],
        "complete": true,
        "collected": 12,
        "total_forks": 12
      },
      "star_history": {
        "days": [
          {
            "date": "2021-08-18",
            "count": 1
          },
          {
            "date": "2021-08-19",
            "count": 5
          },
          {
            "date": "2021-08-26",
            "count": 1
          },
          {
            "date": "2021-08-29",
            "count": 1
          },
          {
            "date": "2021-09-09",
            "count": 2
          },
          {
            "date": "2021-09-11",
            "count": 1
          },
          {
            "date": "2021-09-13",
            "count": 1
          },
          {
            "date": "2021-10-05",
            "count": 1
          },
          {
            "date": "2021-10-07",
            "count": 1
          },
          {
            "date": "2021-10-27",
            "count": 1
          },
          {
            "date": "2021-11-08",
            "count": 1
          },
          {
            "date": "2021-11-09",
            "count": 3
          },
          {
            "date": "2021-11-10",
            "count": 1
          },
          {
            "date": "2021-11-23",
            "count": 2
          },
          {
            "date": "2021-11-24",
            "count": 3
          },
          {
            "date": "2021-11-29",
            "count": 1
          },
          {
            "date": "2021-12-10",
            "count": 1
          },
          {
            "date": "2021-12-16",
            "count": 1
          },
          {
            "date": "2021-12-17",
            "count": 2
          },
          {
            "date": "2021-12-20",
            "count": 1
          },
          {
            "date": "2021-12-24",
            "count": 2
          },
          {
            "date": "2021-12-25",
            "count": 1
          },
          {
            "date": "2021-12-26",
            "count": 1
          },
          {
            "date": "2021-12-30",
            "count": 1
          },
          {
            "date": "2022-01-02",
            "count": 1
          },
          {
            "date": "2022-01-06",
            "count": 1
          },
          {
            "date": "2022-01-07",
            "count": 1
          },
          {
            "date": "2022-01-15",
            "count": 1
          },
          {
            "date": "2022-01-22",
            "count": 1
          },
          {
            "date": "2022-01-29",
            "count": 1
          },
          {
            "date": "2022-02-13",
            "count": 1
          },
          {
            "date": "2022-02-14",
            "count": 1
          },
          {
            "date": "2022-02-16",
            "count": 13
          },
          {
            "date": "2022-02-17",
            "count": 1
          },
          {
            "date": "2022-02-18",
            "count": 1
          },
          {
            "date": "2022-02-19",
            "count": 1
          },
          {
            "date": "2022-02-20",
            "count": 1
          },
          {
            "date": "2022-02-21",
            "count": 1
          },
          {
            "date": "2022-02-23",
            "count": 1
          },
          {
            "date": "2022-02-24",
            "count": 1
          },
          {
            "date": "2022-02-28",
            "count": 1
          },
          {
            "date": "2022-04-11",
            "count": 2
          },
          {
            "date": "2022-05-03",
            "count": 1
          },
          {
            "date": "2022-05-09",
            "count": 1
          },
          {
            "date": "2022-05-17",
            "count": 1
          },
          {
            "date": "2022-05-18",
            "count": 1
          },
          {
            "date": "2022-05-24",
            "count": 1
          },
          {
            "date": "2022-05-27",
            "count": 1
          },
          {
            "date": "2022-05-29",
            "count": 1
          },
          {
            "date": "2022-05-30",
            "count": 3
          },
          {
            "date": "2022-06-03",
            "count": 1
          },
          {
            "date": "2022-06-07",
            "count": 1
          },
          {
            "date": "2022-06-20",
            "count": 3
          },
          {
            "date": "2022-06-27",
            "count": 1
          },
          {
            "date": "2022-07-07",
            "count": 1
          },
          {
            "date": "2022-07-28",
            "count": 1
          },
          {
            "date": "2022-08-15",
            "count": 1
          },
          {
            "date": "2022-08-21",
            "count": 1
          },
          {
            "date": "2022-08-24",
            "count": 1
          },
          {
            "date": "2022-08-26",
            "count": 1
          },
          {
            "date": "2022-09-02",
            "count": 1
          },
          {
            "date": "2022-09-04",
            "count": 1
          },
          {
            "date": "2022-09-15",
            "count": 1
          },
          {
            "date": "2022-09-16",
            "count": 1
          },
          {
            "date": "2022-10-15",
            "count": 3
          },
          {
            "date": "2022-10-21",
            "count": 1
          },
          {
            "date": "2022-11-04",
            "count": 1
          },
          {
            "date": "2022-11-07",
            "count": 1
          },
          {
            "date": "2022-11-09",
            "count": 1
          },
          {
            "date": "2022-11-28",
            "count": 1
          },
          {
            "date": "2022-12-07",
            "count": 1
          },
          {
            "date": "2023-01-26",
            "count": 1
          },
          {
            "date": "2023-02-03",
            "count": 1
          },
          {
            "date": "2023-02-06",
            "count": 1
          },
          {
            "date": "2023-02-07",
            "count": 1
          },
          {
            "date": "2023-03-08",
            "count": 1
          },
          {
            "date": "2023-03-09",
            "count": 1
          },
          {
            "date": "2023-04-05",
            "count": 1
          },
          {
            "date": "2023-04-10",
            "count": 1
          },
          {
            "date": "2023-05-02",
            "count": 1
          },
          {
            "date": "2023-05-03",
            "count": 1
          },
          {
            "date": "2023-05-05",
            "count": 1
          },
          {
            "date": "2023-05-09",
            "count": 1
          },
          {
            "date": "2023-06-09",
            "count": 1
          },
          {
            "date": "2023-06-11",
            "count": 1
          },
          {
            "date": "2023-06-12",
            "count": 1
          },
          {
            "date": "2023-06-20",
            "count": 1
          },
          {
            "date": "2023-07-22",
            "count": 1
          },
          {
            "date": "2023-08-04",
            "count": 1
          },
          {
            "date": "2023-08-08",
            "count": 1
          },
          {
            "date": "2023-08-19",
            "count": 1
          },
          {
            "date": "2023-09-17",
            "count": 2
          },
          {
            "date": "2023-10-02",
            "count": 1
          },
          {
            "date": "2023-10-12",
            "count": 1
          },
          {
            "date": "2023-10-13",
            "count": 1
          },
          {
            "date": "2023-10-14",
            "count": 1
          },
          {
            "date": "2023-10-18",
            "count": 1
          },
          {
            "date": "2023-11-18",
            "count": 1
          },
          {
            "date": "2023-11-26",
            "count": 1
          },
          {
            "date": "2023-12-03",
            "count": 1
          },
          {
            "date": "2023-12-18",
            "count": 1
          },
          {
            "date": "2023-12-22",
            "count": 1
          },
          {
            "date": "2024-01-10",
            "count": 1
          },
          {
            "date": "2024-01-13",
            "count": 1
          },
          {
            "date": "2024-01-18",
            "count": 1
          },
          {
            "date": "2024-02-09",
            "count": 1
          },
          {
            "date": "2024-02-18",
            "count": 1
          },
          {
            "date": "2024-03-25",
            "count": 1
          },
          {
            "date": "2024-04-15",
            "count": 1
          },
          {
            "date": "2024-04-18",
            "count": 1
          },
          {
            "date": "2024-05-08",
            "count": 1
          },
          {
            "date": "2024-05-31",
            "count": 1
          },
          {
            "date": "2024-06-13",
            "count": 1
          },
          {
            "date": "2024-07-17",
            "count": 1
          },
          {
            "date": "2024-07-18",
            "count": 1
          },
          {
            "date": "2024-08-21",
            "count": 1
          },
          {
            "date": "2024-10-18",
            "count": 1
          },
          {
            "date": "2024-10-27",
            "count": 1
          },
          {
            "date": "2024-11-20",
            "count": 1
          },
          {
            "date": "2025-05-15",
            "count": 3
          },
          {
            "date": "2025-05-20",
            "count": 1
          },
          {
            "date": "2025-05-25",
            "count": 1
          },
          {
            "date": "2025-09-16",
            "count": 1
          },
          {
            "date": "2025-09-18",
            "count": 1
          },
          {
            "date": "2025-09-19",
            "count": 2
          },
          {
            "date": "2025-09-25",
            "count": 1
          },
          {
            "date": "2025-09-27",
            "count": 1
          },
          {
            "date": "2025-10-07",
            "count": 1
          },
          {
            "date": "2025-10-28",
            "count": 1
          },
          {
            "date": "2025-12-09",
            "count": 1
          },
          {
            "date": "2026-01-08",
            "count": 1
          },
          {
            "date": "2026-01-09",
            "count": 1
          },
          {
            "date": "2026-02-19",
            "count": 1
          },
          {
            "date": "2026-03-04",
            "count": 1
          },
          {
            "date": "2026-03-11",
            "count": 1
          },
          {
            "date": "2026-03-18",
            "count": 1
          },
          {
            "date": "2026-05-26",
            "count": 1
          },
          {
            "date": "2026-07-01",
            "count": 1
          },
          {
            "date": "2026-07-09",
            "count": 1
          },
          {
            "date": "2026-07-14",
            "count": 1
          }
        ],
        "complete": true,
        "collected": 175,
        "total_stars": 175
      },
      "open_issues_and_prs": 9
    },
    "ai_readiness": {
      "has_nix": false,
      "example_dirs": [],
      "has_llms_txt": false,
      "has_dockerfile": false,
      "has_mcp_signal": false,
      "bootstrap_files": [],
      "api_schema_files": [],
      "has_devcontainer": false,
      "typecheck_configs": [],
      "toolchain_manifests": [],
      "largest_source_bytes": 48940,
      "source_files_sampled": 58,
      "oversized_source_files": 0,
      "agent_instruction_files": [],
      "agent_instruction_max_bytes": null
    },
    "dependencies": {
      "manifests": [
        "Gemfile"
      ],
      "advisories": {
        "error": null,
        "scope": "repository_graph",
        "source": "osv",
        "findings": [
          {
            "name": "rack-session",
            "direct": false,
            "version": "2.1.0",
            "severity": "critical",
            "ecosystem": "rubygems",
            "cvss_score": 9.1,
            "advisory_ids": [
              "GHSA-33qg-7wpp-89cq",
              "GHSA-9j94-67jr-4cqj"
            ],
            "fixed_version": "2.1.2",
            "advisory_count": 2,
            "oldest_advisory_days": 439
          },
          {
            "name": "activesupport",
            "direct": false,
            "version": "8.0.2",
            "severity": "high",
            "ecosystem": "rubygems",
            "cvss_score": 7.5,
            "advisory_ids": [
              "GHSA-2j26-frm8-cmj9",
              "GHSA-89vf-4333-qx8v",
              "GHSA-cg4j-q9v8-6v38"
            ],
            "fixed_version": "8.1.2.1",
            "advisory_count": 3,
            "oldest_advisory_days": 120
          },
          {
            "name": "concurrent-ruby",
            "direct": false,
            "version": "1.3.5",
            "severity": "high",
            "ecosystem": "rubygems",
            "cvss_score": 7.5,
            "advisory_ids": [
              "GHSA-6wx8-w4f5-wwcr",
              "GHSA-h8w8-99g7-qmvj",
              "GHSA-wv3x-4vxv-whpp"
            ],
            "fixed_version": "1.3.7",
            "advisory_count": 3,
            "oldest_advisory_days": 32
          },
          {
            "name": "nokogiri",
            "direct": false,
            "version": "1.18.6",
            "severity": "high",
            "ecosystem": "rubygems",
            "cvss_score": 7.5,
            "advisory_ids": [
              "GHSA-353f-x4gh-cqq8",
              "GHSA-5prr-v3j2-97mh",
              "GHSA-5v8h-3h3q-446p",
              "GHSA-5w6v-399v-w3cc",
              "GHSA-8678-w3jw-xfc2",
              "GHSA-9cv2-cfxc-v4v2",
              "GHSA-c4rq-3m3g-8wgx",
              "GHSA-p67v-3w7g-wjg7",
              "GHSA-phwj-rprq-35pp",
              "GHSA-v2fc-qm4h-8hqv"
            ],
            "fixed_version": "1.19.4",
            "advisory_count": 13,
            "oldest_advisory_days": 455
          },
          {
            "name": "rack",
            "direct": false,
            "version": "3.1.12",
            "severity": "high",
            "ecosystem": "rubygems",
            "cvss_score": 7.5,
            "advisory_ids": [
              "GHSA-47m2-26rw-j2jw",
              "GHSA-6xw4-3v39-52mm",
              "GHSA-7mqq-6cf9-v2qp",
              "GHSA-8vqr-qjwx-82mw",
              "GHSA-g2pf-xv49-m2h5",
              "GHSA-gjh7-p2fx-99vx",
              "GHSA-h2jq-g4cq-5ppq",
              "GHSA-mxw3-3hh2-x2mh",
              "GHSA-p543-xpfm-54cp",
              "GHSA-q2ww-5357-x388"
            ],
            "fixed_version": "3.2.6",
            "advisory_count": 21,
            "oldest_advisory_days": 439
          },
          {
            "name": "activerecord",
            "direct": true,
            "version": "8.0.2",
            "severity": "moderate",
            "ecosystem": "rubygems",
            "cvss_score": null,
            "advisory_ids": [
              "GHSA-76r7-hhxj-r776"
            ],
            "fixed_version": "8.0.2.1",
            "advisory_count": 1,
            "oldest_advisory_days": 341
          },
          {
            "name": "net-imap",
            "direct": false,
            "version": "0.5.6",
            "severity": "moderate",
            "ecosystem": "rubygems",
            "cvss_score": 5.3,
            "advisory_ids": [
              "GHSA-46q3-7gv7-qmgg",
              "GHSA-75xq-5h9v-w6px",
              "GHSA-87pf-fpwv-p7m7",
              "GHSA-8p34-64r3-mwg8",
              "GHSA-c4fp-cxrr-mj66",
              "GHSA-hm49-wcqc-g2xg",
              "GHSA-j3g3-5qv5-52mj",
              "GHSA-q2mw-fvj9-vvcw",
              "GHSA-vcgp-9326-pqcp"
            ],
            "fixed_version": "0.6.4.1",
            "advisory_count": 9,
            "oldest_advisory_days": 449
          },
          {
            "name": "actionview",
            "direct": false,
            "version": "8.0.2",
            "severity": "low",
            "ecosystem": "rubygems",
            "cvss_score": null,
            "advisory_ids": [
              "GHSA-v55j-83pf-r9cq"
            ],
            "fixed_version": "8.1.2.1",
            "advisory_count": 1,
            "oldest_advisory_days": 120
          }
        ],
        "collected": true,
        "truncated": false,
        "by_severity": {
          "low": 1,
          "high": 4,
          "critical": 1,
          "moderate": 2
        },
        "advisory_count": 53,
        "affected_count": 8,
        "assessed_count": 78,
        "assessed_package": null,
        "unassessed_count": 2,
        "direct_affected_count": 1
      },
      "ecosystems": [
        "rubygems"
      ],
      "dependencies": [
        {
          "name": "actionmailer",
          "manifest": "Gemfile",
          "ecosystem": "rubygems",
          "version_constraint": null
        },
        {
          "name": "activejob",
          "manifest": "Gemfile",
          "ecosystem": "rubygems",
          "version_constraint": null
        },
        {
          "name": "activerecord",
          "manifest": "Gemfile",
          "ecosystem": "rubygems",
          "version_constraint": null
        },
        {
          "name": "appraisal",
          "manifest": "Gemfile",
          "ecosystem": "rubygems",
          "version_constraint": null
        },
        {
          "name": "betterlint",
          "manifest": "Gemfile",
          "ecosystem": "rubygems",
          "version_constraint": null
        },
        {
          "name": "mysql2",
          "manifest": "Gemfile",
          "ecosystem": "rubygems",
          "version_constraint": null
        },
        {
          "name": "pg",
          "manifest": "Gemfile",
          "ecosystem": "rubygems",
          "version_constraint": null
        },
        {
          "name": "rake",
          "manifest": "Gemfile",
          "ecosystem": "rubygems",
          "version_constraint": null
        },
        {
          "name": "rspec",
          "manifest": "Gemfile",
          "ecosystem": "rubygems",
          "version_constraint": null
        },
        {
          "name": "timecop",
          "manifest": "Gemfile",
          "ecosystem": "rubygems",
          "version_constraint": null
        },
        {
          "name": "zeitwerk",
          "manifest": "Gemfile",
          "ecosystem": "rubygems",
          "version_constraint": null
        },
        {
          "name": "sqlite3",
          "manifest": "Gemfile",
          "ecosystem": "rubygems",
          "version_constraint": ">= 1.4"
        },
        {
          "name": "snapshot_testing",
          "manifest": "Gemfile",
          "ecosystem": "rubygems",
          "version_constraint": null
        }
      ],
      "all_dependencies": {
        "error": null,
        "source": "github-sbom",
        "packages": [
          {
            "name": "actionmailer",
            "direct": true,
            "version": "8.0.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "activejob",
            "direct": true,
            "version": "8.0.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "activerecord",
            "direct": true,
            "version": null,
            "ecosystem": "rubygems"
          },
          {
            "name": "activerecord",
            "direct": true,
            "version": "8.0.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "appraisal",
            "direct": true,
            "version": "2.5.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "betterlint",
            "direct": true,
            "version": "1.19.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "mysql2",
            "direct": true,
            "version": "0.5.6",
            "ecosystem": "rubygems"
          },
          {
            "name": "pg",
            "direct": true,
            "version": "1.5.9",
            "ecosystem": "rubygems"
          },
          {
            "name": "rake",
            "direct": true,
            "version": "13.2.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "rspec",
            "direct": true,
            "version": "3.13.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "snapshot_testing",
            "direct": true,
            "version": "0.3.4",
            "ecosystem": "rubygems"
          },
          {
            "name": "sqlite3",
            "direct": true,
            "version": "2.6.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "timecop",
            "direct": true,
            "version": "0.9.10",
            "ecosystem": "rubygems"
          },
          {
            "name": "zeitwerk",
            "direct": true,
            "version": "2.7.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "actionpack",
            "direct": false,
            "version": "8.0.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "actionview",
            "direct": false,
            "version": "8.0.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "activemodel",
            "direct": false,
            "version": "8.0.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "activesupport",
            "direct": false,
            "version": "8.0.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "ast",
            "direct": false,
            "version": "2.4.3",
            "ecosystem": "rubygems"
          },
          {
            "name": "base64",
            "direct": false,
            "version": "0.2.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "benchmark",
            "direct": false,
            "version": "0.4.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "bigdecimal",
            "direct": false,
            "version": "3.1.9",
            "ecosystem": "rubygems"
          },
          {
            "name": "builder",
            "direct": false,
            "version": "3.3.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "concurrent-ruby",
            "direct": false,
            "version": null,
            "ecosystem": "rubygems"
          },
          {
            "name": "concurrent-ruby",
            "direct": false,
            "version": "1.3.5",
            "ecosystem": "rubygems"
          },
          {
            "name": "connection_pool",
            "direct": false,
            "version": "2.5.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "crass",
            "direct": false,
            "version": "1.0.6",
            "ecosystem": "rubygems"
          },
          {
            "name": "date",
            "direct": false,
            "version": "3.4.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "diff-lcs",
            "direct": false,
            "version": "1.6.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "drb",
            "direct": false,
            "version": "2.2.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "erubi",
            "direct": false,
            "version": "1.13.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "globalid",
            "direct": false,
            "version": "1.2.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "i18n",
            "direct": false,
            "version": "1.14.7",
            "ecosystem": "rubygems"
          },
          {
            "name": "json",
            "direct": false,
            "version": "2.10.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "language_server-protocol",
            "direct": false,
            "version": "3.17.0.4",
            "ecosystem": "rubygems"
          },
          {
            "name": "lint_roller",
            "direct": false,
            "version": "1.1.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "logger",
            "direct": false,
            "version": "1.7.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "loofah",
            "direct": false,
            "version": "2.24.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "mail",
            "direct": false,
            "version": "2.8.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "mini_mime",
            "direct": false,
            "version": "1.1.5",
            "ecosystem": "rubygems"
          },
          {
            "name": "minitest",
            "direct": false,
            "version": "5.25.5",
            "ecosystem": "rubygems"
          },
          {
            "name": "net-imap",
            "direct": false,
            "version": "0.5.6",
            "ecosystem": "rubygems"
          },
          {
            "name": "net-pop",
            "direct": false,
            "version": "0.1.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "net-protocol",
            "direct": false,
            "version": "0.2.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "net-smtp",
            "direct": false,
            "version": "0.5.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "nokogiri",
            "direct": false,
            "version": "1.18.6",
            "ecosystem": "rubygems"
          },
          {
            "name": "parallel",
            "direct": false,
            "version": "1.26.3",
            "ecosystem": "rubygems"
          },
          {
            "name": "parser",
            "direct": false,
            "version": "3.3.7.3",
            "ecosystem": "rubygems"
          },
          {
            "name": "prism",
            "direct": false,
            "version": "1.4.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "racc",
            "direct": false,
            "version": "1.8.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "rack",
            "direct": false,
            "version": "3.1.12",
            "ecosystem": "rubygems"
          },
          {
            "name": "rack-session",
            "direct": false,
            "version": "2.1.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "rack-test",
            "direct": false,
            "version": "2.2.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "rails-dom-testing",
            "direct": false,
            "version": "2.2.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "rails-html-sanitizer",
            "direct": false,
            "version": "1.6.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "rainbow",
            "direct": false,
            "version": "3.1.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "regexp_parser",
            "direct": false,
            "version": "2.10.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "rspec-core",
            "direct": false,
            "version": "3.13.3",
            "ecosystem": "rubygems"
          },
          {
            "name": "rspec-expectations",
            "direct": false,
            "version": "3.13.3",
            "ecosystem": "rubygems"
          },
          {
            "name": "rspec-mocks",
            "direct": false,
            "version": "3.13.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "rspec-support",
            "direct": false,
            "version": "3.13.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "rubocop",
            "direct": false,
            "version": "1.75.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "rubocop-ast",
            "direct": false,
            "version": "1.43.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "rubocop-capybara",
            "direct": false,
            "version": "2.22.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "rubocop-factory_bot",
            "direct": false,
            "version": "2.27.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "rubocop-graphql",
            "direct": false,
            "version": "1.5.5",
            "ecosystem": "rubygems"
          },
          {
            "name": "rubocop-performance",
            "direct": false,
            "version": "1.24.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "rubocop-rails",
            "direct": false,
            "version": "2.30.3",
            "ecosystem": "rubygems"
          },
          {
            "name": "rubocop-rake",
            "direct": false,
            "version": "0.7.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "rubocop-rspec",
            "direct": false,
            "version": "2.31.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "rubocop-rspec_rails",
            "direct": false,
            "version": "2.29.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "ruby-progressbar",
            "direct": false,
            "version": "1.13.0",
            "ecosystem": "rubygems"
          },
          {
            "name": "securerandom",
            "direct": false,
            "version": "0.4.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "thor",
            "direct": false,
            "version": "1.3.2",
            "ecosystem": "rubygems"
          },
          {
            "name": "timeout",
            "direct": false,
            "version": "0.4.3",
            "ecosystem": "rubygems"
          },
          {
            "name": "tzinfo",
            "direct": false,
            "version": "2.0.6",
            "ecosystem": "rubygems"
          },
          {
            "name": "unicode-display_width",
            "direct": false,
            "version": "3.1.4",
            "ecosystem": "rubygems"
          },
          {
            "name": "unicode-emoji",
            "direct": false,
            "version": "4.0.4",
            "ecosystem": "rubygems"
          },
          {
            "name": "uri",
            "direct": false,
            "version": "1.1.1",
            "ecosystem": "rubygems"
          },
          {
            "name": "useragent",
            "direct": false,
            "version": "0.16.11",
            "ecosystem": "rubygems"
          }
        ],
        "collected": true,
        "truncated": false,
        "total_count": 80,
        "direct_count": 14,
        "indirect_count": 66
      }
    },
    "maintainership": {
      "issues": {
        "open_prs": 3,
        "merged_prs": 81,
        "open_issues": 6,
        "closed_ratio": 0.76,
        "closed_issues": 19,
        "closed_unmerged_prs": 7
      },
      "bus_factor": 3,
      "bot_contributors": 0,
      "top_contributors": [
        {
          "type": "User",
          "login": "albus522",
          "commits": 335,
          "avatar_url": "https://avatars.githubusercontent.com/u/7812?v=4"
        },
        {
          "type": "User",
          "login": "bkeepers",
          "commits": 228,
          "avatar_url": "https://avatars.githubusercontent.com/u/173?v=4"
        },
        {
          "type": "User",
          "login": "sferik",
          "commits": 131,
          "avatar_url": "https://avatars.githubusercontent.com/u/10308?v=4"
        },
        {
          "type": "User",
          "login": "bryckbost",
          "commits": 96,
          "avatar_url": "https://avatars.githubusercontent.com/u/692?v=4"
        },
        {
          "type": "User",
          "login": "smudge",
          "commits": 96,
          "avatar_url": "https://avatars.githubusercontent.com/u/83998?v=4"
        },
        {
          "type": "User",
          "login": "betamatt",
          "commits": 65,
          "avatar_url": "https://avatars.githubusercontent.com/u/23032?v=4"
        },
        {
          "type": "User",
          "login": "laserlemon",
          "commits": 34,
          "avatar_url": "https://avatars.githubusercontent.com/u/34264?v=4"
        },
        {
          "type": "User",
          "login": "lpetre",
          "commits": 12,
          "avatar_url": "https://avatars.githubusercontent.com/u/193260?v=4"
        },
        {
          "type": "User",
          "login": "gaffneyc",
          "commits": 10,
          "avatar_url": "https://avatars.githubusercontent.com/u/7095?v=4"
        },
        {
          "type": "User",
          "login": "bethesque",
          "commits": 8,
          "avatar_url": "https://avatars.githubusercontent.com/u/446228?v=4"
        }
      ],
      "contributors_sampled": 100,
      "top_contributor_share": 0.275
    },
    "quality_signals": {
      "has_ci": true,
      "has_tests": true,
      "ci_workflows": [
        "ci.yml",
        "linter.yml"
      ],
      "has_docs_dir": false,
      "linter_configs": [
        ".rubocop.yml"
      ],
      "has_editorconfig": false,
      "has_linter_config": true,
      "has_precommit_config": false
    },
    "security_signals": {
      "lockfiles": [
        "Gemfile.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": 8,
            "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": "30 out of 30 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": 10,
            "reason": "all changesets reviewed",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#code-review"
          },
          {
            "name": "Contributors",
            "score": 10,
            "reason": "project has 34 contributing companies or organizations",
            "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": 0,
            "reason": "no 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": 7,
            "reason": "9 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 7",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#maintained"
          },
          {
            "name": "Packaging",
            "score": null,
            "reason": "packaging workflow not 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": 0,
            "reason": "SAST tool is not run on all commits -- score normalized to 0",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#sast"
          },
          {
            "name": "Security-Policy",
            "score": 0,
            "reason": "security policy file not detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#security-policy"
          },
          {
            "name": "Signed-Releases",
            "score": null,
            "reason": "no releases found",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#signed-releases"
          },
          {
            "name": "Token-Permissions",
            "score": 0,
            "reason": "detected GitHub workflow tokens with excessive permissions",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#token-permissions"
          },
          {
            "name": "Vulnerabilities",
            "score": 0,
            "reason": "53 existing vulnerabilities detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#vulnerabilities"
          }
        ],
        "commit": "c726753839bef4bec1b4d5dbe9945cb43d487edc",
        "ran_at": "2026-07-21T21:02:01Z",
        "aggregate_score": 5.3,
        "scorecard_version": "v5.5.0"
      },
      "has_codeql_workflow": false,
      "has_security_policy": false,
      "has_dependabot_config": false
    }
  },
  "config": {
    "disabled_metrics": [],
    "disabled_categories": [],
    "disabled_components": {}
  },
  "source": {
    "url": "https://github.com/Betterment/delayed",
    "host": "github.com",
    "name": "delayed",
    "owner": "Betterment"
  },
  "metrics": {
    "overall": {
      "key": "overall",
      "band": "good",
      "name": "Overall health",
      "note": null,
      "notes": [],
      "value": 71,
      "inputs": {
        "security": 59,
        "vitality": 78,
        "community": 59,
        "governance": 82,
        "engineering": 70
      },
      "components": []
    },
    "categories": [
      {
        "key": "vitality",
        "band": "good",
        "name": "Vitality",
        "value": 78,
        "weight": 0.22,
        "metrics": [
          {
            "key": "development_activity",
            "band": "moderate",
            "name": "Development activity",
            "note": null,
            "notes": [],
            "value": 64,
            "inputs": {
              "commits_last_year": 52,
              "human_commit_share": 1,
              "days_since_last_push": 8,
              "active_weeks_last_year": 18
            },
            "components": [
              {
                "key": "push_recency",
                "name": "Push recency",
                "detail": "last push 8 days ago",
                "points": 28.8,
                "status": "partial",
                "details": [
                  {
                    "code": "push_recency",
                    "params": {
                      "days": 8
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "commit_cadence",
                "name": "Commit cadence",
                "detail": "18/52 weeks with commits",
                "points": 12.5,
                "status": "partial",
                "details": [
                  {
                    "code": "commit_cadence_weeks",
                    "params": {
                      "weeks": 18
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "commit_volume",
                "name": "Commit volume",
                "detail": "52 commits in the last year",
                "points": 15.5,
                "status": "partial",
                "details": [
                  {
                    "code": "commits_last_year",
                    "params": {
                      "count": 52
                    }
                  }
                ],
                "max_points": 18
              },
              {
                "key": "openssf_scorecard_maintained",
                "name": "OpenSSF Scorecard: Maintained",
                "detail": "9 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 7",
                "points": 7,
                "status": "partial",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "release_discipline",
            "band": "excellent",
            "name": "Release discipline",
            "note": "Excluded from scoring (no data or not applicable): OpenSSF Scorecard: Signed-Releases. Remaining weights renormalized.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "openssf_scorecard_signed_releases"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              }
            ],
            "value": 100,
            "inputs": {
              "releases_count": 26,
              "latest_release_tag": "v3.1.0",
              "releases_from_tags": false,
              "days_since_latest_release": 8,
              "mean_days_between_releases": 31.1
            },
            "components": [
              {
                "key": "ships_releases",
                "name": "Ships releases",
                "detail": "26 releases published",
                "points": 27,
                "status": "met",
                "details": [
                  {
                    "code": "releases_published",
                    "params": {
                      "count": 26
                    }
                  }
                ],
                "max_points": 27
              },
              {
                "key": "release_recency",
                "name": "Release recency",
                "detail": "latest release 8 days ago",
                "points": 36,
                "status": "met",
                "details": [
                  {
                    "code": "release_recency",
                    "params": {
                      "days": 8
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "release_cadence",
                "name": "Release cadence",
                "detail": "a release every ~31.1 days",
                "points": 27,
                "status": "met",
                "details": [
                  {
                    "code": "release_cadence",
                    "params": {
                      "gap": 31.1
                    }
                  }
                ],
                "max_points": 27
              },
              {
                "key": "openssf_scorecard_signed_releases",
                "name": "OpenSSF Scorecard: Signed-Releases",
                "detail": "no releases found",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "no_data",
                    "params": {}
                  }
                ],
                "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": 8,
              "days_since_last_human_commit_is_floor": false
            },
            "components": [
              {
                "key": "project_is_still_maintained",
                "name": "Project is still maintained",
                "detail": "last human commit 8 days ago",
                "points": 100,
                "status": "met",
                "details": [
                  {
                    "code": "abandonment_maintained",
                    "params": {
                      "days": 8
                    }
                  }
                ],
                "max_points": 100
              }
            ]
          }
        ],
        "description": "Is the project alive — is code being written and are releases shipping?"
      },
      {
        "key": "community",
        "band": "moderate",
        "name": "Community & Adoption",
        "value": 59,
        "weight": 0.18,
        "metrics": [
          {
            "key": "popularity",
            "band": "moderate",
            "name": "Popularity & adoption",
            "note": null,
            "notes": [],
            "value": 54,
            "inputs": {
              "forks": 12,
              "stars": 175,
              "watchers": 44,
              "growth_state": "organic",
              "growth_factor_pct": 100
            },
            "components": [
              {
                "key": "stars",
                "name": "Stars",
                "detail": "175 stars",
                "points": 36.3,
                "status": "partial",
                "details": [
                  {
                    "code": "stars",
                    "params": {
                      "count": 175
                    }
                  }
                ],
                "max_points": 60
              },
              {
                "key": "forks",
                "name": "Forks",
                "detail": "12 forks",
                "points": 8.7,
                "status": "partial",
                "details": [
                  {
                    "code": "forks",
                    "params": {
                      "count": 12
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "watchers",
                "name": "Watchers",
                "detail": "44 watchers",
                "points": 9.1,
                "status": "partial",
                "details": [
                  {
                    "code": "watchers",
                    "params": {
                      "count": 44
                    }
                  }
                ],
                "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 (MIT)",
                "points": 22.5,
                "status": "met",
                "details": [
                  {
                    "code": "license_standard",
                    "params": {}
                  },
                  {
                    "code": "license_spdx",
                    "params": {
                      "spdx": "MIT"
                    }
                  }
                ],
                "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": "good",
            "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": 79,
            "inputs": {
              "packages": [
                "delayed"
              ],
              "dependents": null,
              "ecosystems": "rubygems",
              "total_downloads": 1132655,
              "monthly_downloads": null
            },
            "components": [
              {
                "key": "total_downloads",
                "name": "Total downloads",
                "detail": "1,132,655 downloads all-time across rubygems",
                "points": 62.9,
                "status": "partial",
                "details": [
                  {
                    "code": "downloads_total",
                    "params": {
                      "count": 1132655,
                      "ecosystems": "rubygems"
                    }
                  }
                ],
                "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": "good",
        "name": "Sustainability & Governance",
        "value": 82,
        "weight": 0.24,
        "metrics": [
          {
            "key": "maintainer_resilience",
            "band": "good",
            "name": "Maintainer resilience (bus factor)",
            "note": null,
            "notes": [],
            "value": 76,
            "inputs": {
              "bus_factor": 3,
              "contributors_sampled": 100,
              "top_contributor_share": 0.275
            },
            "components": [
              {
                "key": "bus_factor",
                "name": "Bus factor",
                "detail": "3 contributor(s) cover half of all commits",
                "points": 36,
                "status": "partial",
                "details": [
                  {
                    "code": "bus_factor",
                    "params": {
                      "count": 3
                    }
                  }
                ],
                "max_points": 54
              },
              {
                "key": "commit_distribution",
                "name": "Commit distribution",
                "detail": "top contributor authored 28% of commits",
                "points": 16.3,
                "status": "partial",
                "details": [
                  {
                    "code": "top_contributor_share",
                    "params": {
                      "share": 28
                    }
                  }
                ],
                "max_points": 22.5
              },
              {
                "key": "contributor_breadth",
                "name": "Contributor breadth",
                "detail": "100 contributors",
                "points": 13.5,
                "status": "met",
                "details": [
                  {
                    "code": "contributors_sampled",
                    "params": {
                      "count": 100
                    }
                  }
                ],
                "max_points": 13.5
              },
              {
                "key": "openssf_scorecard_contributors",
                "name": "OpenSSF Scorecard: Contributors",
                "detail": "project has 34 contributing companies or organizations",
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "responsiveness",
            "band": "excellent",
            "name": "Issue & PR responsiveness",
            "note": null,
            "notes": [],
            "value": 86,
            "inputs": {
              "merged_prs": 81,
              "open_issues": 6,
              "closed_issues": 19,
              "issue_closed_ratio": 0.76,
              "closed_unmerged_prs": 7
            },
            "components": [
              {
                "key": "issue_resolution",
                "name": "Issue resolution",
                "detail": "76% of issues closed",
                "points": 35.5,
                "status": "partial",
                "details": [
                  {
                    "code": "issues_closed_share",
                    "params": {
                      "share": 76
                    }
                  }
                ],
                "max_points": 46.75
              },
              {
                "key": "pr_acceptance",
                "name": "PR acceptance",
                "detail": "81/88 decided PRs merged",
                "points": 35.2,
                "status": "partial",
                "details": [
                  {
                    "code": "decided_prs_merged",
                    "params": {
                      "merged": 81,
                      "decided": 88
                    }
                  }
                ],
                "max_points": 38.25
              },
              {
                "key": "openssf_scorecard_code_review",
                "name": "OpenSSF Scorecard: Code-Review",
                "detail": "all changesets reviewed",
                "points": 15,
                "status": "met",
                "details": [],
                "max_points": 15
              }
            ]
          },
          {
            "key": "stewardship",
            "band": "good",
            "name": "Ownership & stewardship",
            "note": null,
            "notes": [],
            "value": 70,
            "inputs": {
              "followers": 105,
              "owner_type": "Organization",
              "is_verified": null,
              "owner_login": "Betterment",
              "public_repos": 91,
              "account_age_days": 5507
            },
            "components": [
              {
                "key": "ownership_backing",
                "name": "Ownership backing",
                "detail": "organization-owned",
                "points": 30,
                "status": "met",
                "details": [
                  {
                    "code": "owner_organization",
                    "params": {}
                  }
                ],
                "max_points": 30
              },
              {
                "key": "verified_domain",
                "name": "Verified domain",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 20
              },
              {
                "key": "owner_reach",
                "name": "Owner reach",
                "detail": "105 followers of Betterment",
                "points": 14.6,
                "status": "partial",
                "details": [
                  {
                    "code": "owner_followers",
                    "params": {
                      "count": 105,
                      "login": "Betterment"
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "track_record",
                "name": "Track record",
                "detail": "91 public repos, account ~15 yr old",
                "points": 25,
                "status": "met",
                "details": [
                  {
                    "code": "public_repos",
                    "params": {
                      "count": 91
                    }
                  },
                  {
                    "code": "account_age_years",
                    "params": {
                      "years": 15
                    }
                  }
                ],
                "max_points": 25
              }
            ]
          },
          {
            "key": "package_maintenance",
            "band": "excellent",
            "name": "Package maintenance",
            "note": null,
            "notes": [],
            "value": 100,
            "inputs": {
              "packages": [
                "delayed"
              ],
              "ecosystems": "rubygems",
              "any_deprecated": false,
              "min_days_since_publish": 8
            },
            "components": [
              {
                "key": "published_resolvable",
                "name": "Published & resolvable",
                "detail": "1 package(s) on rubygems",
                "points": 25,
                "status": "met",
                "details": [
                  {
                    "code": "packages_published",
                    "params": {
                      "count": 1,
                      "ecosystems": "rubygems"
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "publish_recency",
                "name": "Publish recency",
                "detail": "latest publish 8 days ago",
                "points": 35,
                "status": "met",
                "details": [
                  {
                    "code": "publish_recency",
                    "params": {
                      "days": 8
                    }
                  }
                ],
                "max_points": 35
              },
              {
                "key": "version_history",
                "name": "Version history",
                "detail": "29 published versions",
                "points": 20,
                "status": "met",
                "details": [
                  {
                    "code": "published_versions",
                    "params": {
                      "count": 29
                    }
                  }
                ],
                "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": "good",
        "name": "Engineering Quality",
        "value": 70,
        "weight": 0.2,
        "metrics": [
          {
            "key": "engineering_practices",
            "band": "good",
            "name": "Engineering practices",
            "note": null,
            "notes": [],
            "value": 84,
            "inputs": {
              "has_ci": true,
              "has_tests": true,
              "has_editorconfig": false,
              "has_linter_config": true,
              "has_precommit_config": false
            },
            "components": [
              {
                "key": "ci_workflows",
                "name": "CI workflows",
                "detail": "2 workflow(s)",
                "points": 24,
                "status": "met",
                "details": [
                  {
                    "code": "ci_workflows",
                    "params": {
                      "count": 2
                    }
                  }
                ],
                "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": ".rubocop.yml",
                "points": 16,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": ".rubocop.yml"
                    }
                  }
                ],
                "max_points": 16
              },
              {
                "key": "pre_commit_hooks",
                "name": "Pre-commit hooks",
                "detail": null,
                "points": 0,
                "status": "missed",
                "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": "30 out of 30 merged PRs checked by a CI test -- score normalized to 10",
                "points": 20,
                "status": "met",
                "details": [],
                "max_points": 20
              }
            ]
          },
          {
            "key": "documentation",
            "band": "moderate",
            "name": "Documentation",
            "note": null,
            "notes": [],
            "value": 50,
            "inputs": {
              "topics": [],
              "has_wiki": true,
              "homepage": null,
              "has_readme": true,
              "has_docs_dir": false,
              "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": 0,
                "status": "missed",
                "details": [],
                "max_points": 25
              },
              {
                "key": "documentation_homepage_site",
                "name": "Documentation / homepage site",
                "detail": null,
                "points": 0,
                "status": "missed",
                "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": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 10
              },
              {
                "key": "wiki",
                "name": "Wiki",
                "detail": null,
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              }
            ]
          }
        ],
        "description": "Are baseline engineering and documentation practices in place?"
      },
      {
        "key": "security",
        "band": "moderate",
        "name": "Security",
        "value": 59,
        "weight": 0.16,
        "metrics": [
          {
            "key": "security_posture",
            "band": "moderate",
            "name": "Security posture",
            "note": "Excluded from scoring (no data or not applicable): Packaging, Signed-Releases. Remaining weights renormalized.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "packaging",
                    "signed_releases"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              }
            ],
            "value": 53,
            "inputs": {
              "source": "openssf_scorecard",
              "checks_evaluated": 16,
              "scorecard_version": "v5.5.0",
              "checks_inconclusive": 2,
              "scorecard_aggregate": 5.3
            },
            "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": 6,
                "status": "partial",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "ci_tests",
                "name": "CI-Tests",
                "detail": "30 out of 30 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": "all changesets reviewed",
                "points": 7.5,
                "status": "met",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "contributors",
                "name": "Contributors",
                "detail": "project has 34 contributing companies or organizations",
                "points": 2.5,
                "status": "met",
                "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": "no update tool detected",
                "points": 0,
                "status": "missed",
                "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": "9 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 7",
                "points": 5.2,
                "status": "partial",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "packaging",
                "name": "Packaging",
                "detail": "packaging workflow not detected",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "no_data",
                    "params": {}
                  }
                ],
                "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 not run on all commits -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 5
              },
              {
                "key": "security_policy",
                "name": "Security-Policy",
                "detail": "security policy file not detected",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 5
              },
              {
                "key": "signed_releases",
                "name": "Signed-Releases",
                "detail": "no releases found",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "no_data",
                    "params": {}
                  }
                ],
                "max_points": 7.5
              },
              {
                "key": "token_permissions",
                "name": "Token-Permissions",
                "detail": "detected GitHub workflow tokens with excessive permissions",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "vulnerabilities",
                "name": "Vulnerabilities",
                "detail": "53 existing vulnerabilities detected",
                "points": 0,
                "status": "missed",
                "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. Remaining weights renormalized. Matched 78 resolved dependencies against OSV; 2 could not be assessed (no resolved version, an unsupported ecosystem, or beyond the reported package list). 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"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              },
              {
                "code": "advisories_scope_repository",
                "params": {
                  "assessed": 78
                }
              },
              {
                "code": "advisories_unassessed",
                "params": {
                  "count": 2
                }
              },
              {
                "code": "advisories_repo_graph_caveat",
                "params": {}
              },
              {
                "code": "advisories_reachability",
                "params": {}
              }
            ],
            "value": 85,
            "inputs": {
              "source": "osv",
              "advisories": 53,
              "affected_packages": 8,
              "assessed_packages": 78,
              "unassessed_packages": 2,
              "affected_by_severity": "critical 1, high 4, moderate 2, low 1",
              "direct_affected_packages": 1
            },
            "components": [
              {
                "key": "direct_dependencies_free_of_known_advisories",
                "name": "Direct dependencies free of known advisories",
                "detail": "1 affected: activerecord 8.0.2 (moderate)",
                "points": 26.6,
                "status": "partial",
                "details": [
                  {
                    "code": "advisories_affected",
                    "params": {
                      "count": 1,
                      "packages": "activerecord 8.0.2 (moderate)"
                    }
                  }
                ],
                "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": "1 advisory-carrying package(s) unaddressed past 90 days; oldest published 341 days ago",
                "points": 37.2,
                "status": "partial",
                "details": [
                  {
                    "code": "advisories_stale",
                    "params": {
                      "days": 90,
                      "count": 1,
                      "oldest": 341
                    }
                  }
                ],
                "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": 78,
              "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": 19
            },
            "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": "moderate",
        "name": "AI Readiness",
        "value": 51,
        "weight": 0,
        "metrics": [
          {
            "key": "ai_agent_context",
            "band": "at_risk",
            "name": "Agent context & guidance",
            "note": null,
            "notes": [],
            "value": 40,
            "inputs": {
              "has_llms_txt": false,
              "legible_history_share": 0.97,
              "agent_instruction_files": [],
              "agent_instruction_max_bytes": null
            },
            "components": [
              {
                "key": "agent_instructions",
                "name": "Agent instructions",
                "detail": "no CLAUDE.md / AGENTS.md / editor rules",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "no_agent_instructions",
                    "params": {}
                  }
                ],
                "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": "97 of 100 human commits state their intent (structured subject or explanatory body)",
                "points": 40,
                "status": "met",
                "details": [
                  {
                    "code": "legible_history",
                    "params": {
                      "legible": 97,
                      "sampled": 100
                    }
                  }
                ],
                "max_points": 40
              }
            ]
          },
          {
            "key": "ai_verify_loop",
            "band": "moderate",
            "name": "Verify loop (build / test / typecheck)",
            "note": null,
            "notes": [],
            "value": 57,
            "inputs": {
              "has_nix": false,
              "has_tests": true,
              "lockfiles": [
                "Gemfile.lock"
              ],
              "has_dockerfile": false,
              "typed_language": false,
              "bootstrap_files": [],
              "has_devcontainer": false,
              "has_linter_config": true,
              "typecheck_configs": [],
              "agent_commit_share": 0.02,
              "toolchain_manifests": [],
              "dependency_bot_commit_share": 0
            },
            "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": ".rubocop.yml",
                "points": 11,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": ".rubocop.yml"
                    }
                  }
                ],
                "max_points": 11
              },
              {
                "key": "static_type_checking",
                "name": "Static type checking",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "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": "2 of the last 100 commits agent-authored or agent-credited",
                "points": 4,
                "status": "partial",
                "details": [
                  {
                    "code": "agent_authored_commits",
                    "params": {
                      "count": 2,
                      "sampled": 100
                    }
                  }
                ],
                "max_points": 10
              },
              {
                "key": "automated_maintenance",
                "name": "Automated maintenance",
                "detail": "no automated dependency updates observed",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "no_dependency_automation",
                    "params": {}
                  }
                ],
                "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": "moderate",
            "name": "Code legibility for models",
            "note": null,
            "notes": [],
            "value": 55,
            "inputs": {
              "primary_language": "Ruby",
              "largest_source_bytes": 48940,
              "source_files_sampled": 58,
              "oversized_source_files": 0
            },
            "components": [
              {
                "key": "type_checkable_code",
                "name": "Type-checkable code",
                "detail": "Ruby without a type-check config",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "no_typecheck_config_language",
                    "params": {
                      "language": "Ruby"
                    }
                  }
                ],
                "max_points": 45
              },
              {
                "key": "manageable_file_sizes",
                "name": "Manageable file sizes",
                "detail": "0/58 source files over 60KB",
                "points": 55,
                "status": "met",
                "details": [
                  {
                    "code": "oversized_source_files",
                    "params": {
                      "kb": 60,
                      "sampled": 58,
                      "oversized": 0
                    }
                  }
                ],
                "max_points": 55
              }
            ]
          }
        ],
        "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": [],
  "report_type": "repository",
  "generated_at": "2026-07-21T21:02:32.815288Z",
  "schema_version": "0.23.0",
  "badge_url": "https://raw.githubusercontent.com/inspect-software/badges/main/v1/b/Betterment/delayed.svg",
  "full_name": "Betterment/delayed",
  "license_state": "standard",
  "license_spdx": "MIT"
}

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.23.0 — metodología completa · wiki de métricas.

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