Registro público
Informe de salud del softwareesquema 0.27.0 · métricas 1.13.0 · 2026-07-28 22:14 UTC

middag-io / middag-php-moodle

MIDDAG Moodle adapter — platform bindings, ACL layer, and Moodle-specific implementations for middag-framework

PHPApache-2.0★ 0 estrellas⑂ 0 forksdesde jun 2026Ver en GitHub ↗

middag-io/middag-php-moodle tiene un índice de salud de 60 sobre 100, lo que lo sitúa en la banda Moderado. Su puntuación más alta es Engineering Quality (88/100) y la más baja, Security (33/100). Se actualizó por última vez hoy. Una sola persona concentra la mayor parte del trabajo reciente.

60
global / 100
Moderado

Í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.

60
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

MIDDAGOrganización
0 seguidores12 repositorios públicosdesde abr 2026

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ónEtiquetas
Packagistmiddag-io/moodlev1.11.1203822hace 1 díapluginadaptermoodleinertiaxmldbhost-adaptermiddagmform

Métricas por categoría

Vitalidad

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

75Bueno · 22% del índice global
Cómo se puntúa
36/36Recencia de push — último push hace 0 días
3.5/36Cadencia de commits — 5/52 semanas con commits
18/18Volumen de commits — 254 commits en el último año
0/10OpenSSF Scorecard: Maintained — project was created within the last 90 days. Please review its contents carefully
Datos de entrada utilizados
commits_last_year254
human_commit_share0,94
days_since_last_push0
active_weeks_last_year5
Cómo se puntúa
27/27Publica versiones — 22 versiones publicadas
36/36Recencia de las versiones — última versión hace 1 días
27/27Cadencia de publicación — una versión cada ~2,1 días
0/10OpenSSF Scorecard: Signed-Releases — sin datos
Datos de entrada utilizados
releases_count22
latest_release_tagv1.11.1
releases_from_tagsno
days_since_latest_release1
mean_days_between_releases2,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?

41En riesgo · 18% del índice global
Cómo se puntúa
0/60Estrellas — 0 estrellas
0/25Forks — 0 forks
0/15Observadores — 0 observadores
Datos de entrada utilizados
forks0
stars0
watchers0
growth_stateunverified
growth_factor_pct100
growth_unverified_reasonno_history
Cómo se puntúa
22.5/22.5README
22.5/22.5Licencia — licencia reconocida (Apache-2.0)
18/18Guía CONTRIBUTING
13.5/13.5Código de conducta
0/7.2Plantilla de issues
0/6.3Plantilla de PR
Datos de entrada utilizados
has_readme
has_license
has_contributing
has_issue_templateno
has_code_of_conduct
has_pull_request_templateno
Cómo se puntúa
44.1/80Descargas mensuales — 2038 descargas/mes en packagist
0/20Dependientes en el registro — 0 paquetes dependen de él
Datos de entrada utilizados
packagesmiddag-io/moodle
dependents0
ecosystemspackagist
total_downloads2041
monthly_downloads2038

Sostenibilidad y Gobernanza

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

54Moderado · 24% del índice global
Cómo se puntúa
9/54Factor bus — la mitad de los commits recae en 1 contribuyente(s)
0.1/22.5Distribución de commits — el principal contribuyente firma el 100% de los commits
2.7/13.5Amplitud de contribuyentes — 2 contribuyentes
6/10OpenSSF Scorecard: Contributors — project has 2 contributing companies or organizations -- score normalized to 6
Datos de entrada utilizados
bus_factor1
contributors_sampled2
top_contributor_share0,996
Cómo se puntúa
37.4/46.8Resolución de issues — 80% de issues cerradas
38.2/38.3Aceptación de PR — 47/47 PR decididos fusionados
0/15OpenSSF Scorecard: Code-Review — Found 1/25 approved changesets -- score normalized to 0
Datos de entrada utilizados
merged_prs47
open_issues1
closed_issues4
issue_closed_ratio0,8
closed_unmerged_prs0
Cómo se puntúa
30/30Respaldo de la propiedad — propiedad de una organización
0/20Dominio verificado
0/25Alcance del propietario — 0 seguidores de middag-io
8.7/25Trayectoria — 12 repos públicos, cuenta de ~0 años
Datos de entrada utilizados
followers0
owner_typeOrganization
is_verified
owner_loginmiddag-io
public_repos12
account_age_days115
Cómo se puntúa
25/25Publicado y resoluble — 1 paquete(s) en packagist
35/35Recencia de publicación — última publicación hace 1 días
20/20Historial de versiones — 22 versiones en el registro
20/20No obsoleto — activo, ni obsoleto ni retirado
Datos de entrada utilizados
packagesmiddag-io/moodle
ecosystemspackagist
any_deprecatedno
min_days_since_publish1

Calidad de Ingeniería

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

88Excelente · 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 — .php-cs-fixer.dist.php
0/9.6Hooks de pre-commit
0/6.4.editorconfig
16/20OpenSSF Scorecard: CI-Tests — 6 out of 7 merged PRs checked by a CI test -- score normalized to 8
Datos de entrada utilizados
has_ci
has_tests
has_editorconfigno
has_linter_config
has_precommit_configno

Documentación

100Excelente
Cómo se puntúa
30/30README
25/25Directorio de documentación
15/15Sitio de documentación / página del proyecto — https://docs.middag.dev
10/10Descripción del repositorio
10/10Topics — 4 topics
10/10Wiki
Datos de entrada utilizados
topicsclient-middag, platform-php, type-library, status-active
has_wiki
homepagehttps://docs.middag.dev
has_readme
has_docs_dir
has_description

Seguridad

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

33En riesgo · 16% del índice global
Cómo se puntúa
7.5/7.5Binary-Artifacts — no binaries found in the repo
2.2/7.5Branch-Protection — branch protection is not maximal on development and all release branches
2/2.5CI-Tests — 6 out of 7 merged PRs checked by a CI test -- score normalized to 8
0/2.5CII-Best-Practices — no effort to earn an OpenSSF best practices badge detected
0/7.5Code-Review — Found 1/25 approved changesets -- score normalized to 0
1.5/2.5Contributors — project has 2 contributing companies or organizations -- score normalized to 6
0/10Dangerous-Workflow — sin datos
0/7.5Dependency-Update-Tool — no update tool detected
0/5Fuzzing — project is not fuzzed
2.5/2.5Licencia — license file detected
0/7.5Maintained — project was created within the last 90 days. Please review its contents carefully
0/5Packaging — sin datos
0/5Pinned-Dependencies — sin datos
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 — sin datos
7.5/7.5Vulnerabilities — 0 existing vulnerabilities detected
Datos de entrada utilizados
sourceopenssf_scorecard
checks_evaluated13
scorecard_versionv5.5.0
checks_inconclusive5
scorecard_aggregate3,3
Excluidos de la puntuación (sin datos o no aplicable): dangerous_workflow, packaging, pinned_dependencies, signed_releases, token_permissions. Los pesos restantes se han renormalizado.

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.

57Moderado · 0% del índice global
Cómo se puntúa
45/45Instrucciones para agentes — CLAUDE.md
0/15Documentación legible por máquinas (llms.txt)
40/40Historial de commits legible — 94 de 94 commits humanos declaran su intención (asunto estructurado o cuerpo explicativo)
Datos de entrada utilizados
has_llms_txtno
legible_history_share1
agent_instruction_filesCLAUDE.md
agent_instruction_max_bytes13.720
Cómo se puntúa
0/18Arranque con un solo comando
22/22Pruebas automatizadas
11/11Configuración de lint / formato — .php-cs-fixer.dist.php
0/11Verificación estática de tipos
0/10Entorno reproducible
0/10Práctica demostrada con agentes — ningún commit con autoría de agente entre los últimos 100
0/8Mantenimiento automatizado — no se observan actualizaciones automáticas de dependencias
0/10OpenSSF Scorecard: Pinned-Dependencies — sin datos
Datos de entrada utilizados
has_nixno
has_tests
lockfiles
has_dockerfileno
typed_languageno
bootstrap_files
has_devcontainerno
has_linter_config
typecheck_configs
agent_commit_share0
toolchain_manifests
dependency_bot_commit_share0
Excluidos de la puntuación (sin datos o no aplicable): OpenSSF Scorecard: Pinned-Dependencies. Los pesos restantes se han renormalizado.
Cómo se puntúa
0/45Código verificable por tipos — PHP sin configuración de verificación de tipos
54.9/55Tamaños de archivo manejables — 1/479 archivos fuente de más de 60 KB
Datos de entrada utilizados
primary_languagePHP
largest_source_bytes61.160
source_files_sampled479
oversized_source_files1

Datos clave

0estrellas de GitHub
2contribuidores
254commits en los últimos 12 meses
0días desde el último push
22versiones publicadas
1factor bus
1issues abiertas
Packagistecosistemas de paquetes

Advertencias de recopilación de datos

  • GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository

Más detalle

OpenSSF Scorecard 3.3 / 10
3.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-28 22:13 UTC

10Binary-Artifactsno binaries found in the repo
3Branch-Protectionbranch protection is not maximal on development and all release branches
8CI-Tests6 out of 7 merged PRs checked by a CI test -- score normalized to 8
0CII-Best-Practicesno effort to earn an OpenSSF best practices badge detected
0Code-ReviewFound 1/25 approved changesets -- score normalized to 0
6Contributorsproject has 2 contributing companies or organizations -- score normalized to 6
n/dDangerous-Workflowno workflows found
0Dependency-Update-Toolno update tool detected
0Fuzzingproject is not fuzzed
10Licenselicense file detected
0Maintainedproject was created within the last 90 days. Please review its contents carefully
n/dPackagingpackaging workflow not detected
n/dPinned-Dependenciesno dependencies found
0SASTSAST tool is not run on all commits -- score normalized to 0
0Security-Policysecurity policy file not detected
n/dSigned-Releasesno releases found
n/dToken-PermissionsNo tokens found
10Vulnerabilities0 existing vulnerabilities detected
Dependencias directas 12
RegistroPaqueteRestricción de versiónManifiesto
Packagistfirebase/php-jwt^7.0composer.json
Packagistmiddag-io/framework^1.9composer.json
Packagistmiddag-io/ui^1.3composer.json
Packagistnyholm/psr7^1.8composer.json
Packagistpsr/container^2.0composer.json
Packagistpsr/log^3.0composer.json
Packagistpsr/simple-cache^3.0composer.json
Packagistsymfony/dependency-injection^7.0composer.json
Packagistsymfony/event-dispatcher-contracts^3.0composer.json
Packagistsymfony/http-client^7.0composer.json
Packagistsymfony/http-foundation^7.0composer.json
Packagistsymfony/routing^7.0composer.json
Todas las dependencias no recopilado

No fue posible recopilar el conjunto de dependencias resuelto para este informe: GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository

Informe JSON sin procesar legible por máquina
{
  "data": {
    "repo": {
      "topics": [
        "client-middag",
        "platform-php",
        "type-library",
        "status-active"
      ],
      "is_fork": false,
      "size_kb": 1333,
      "has_wiki": true,
      "homepage": "https://docs.middag.dev",
      "languages": {
        "PHP": 2356472,
        "Shell": 3228
      },
      "pushed_at": "2026-07-28T22:12:33Z",
      "created_at": "2026-06-26T05:57:36Z",
      "owner_type": "Organization",
      "updated_at": "2026-07-27T16:21:55Z",
      "description": "MIDDAG Moodle adapter — platform bindings, ACL layer, and Moodle-specific implementations for middag-framework",
      "is_archived": false,
      "is_disabled": false,
      "license_spdx": "Apache-2.0",
      "default_branch": "main",
      "license_spdx_raw": "Apache-2.0",
      "primary_language": "PHP",
      "significant_languages": [
        "PHP"
      ]
    },
    "owner": {
      "blog": "https://middag.io",
      "name": "MIDDAG",
      "type": "Organization",
      "login": "middag-io",
      "company": null,
      "location": null,
      "followers": 0,
      "avatar_url": "https://avatars.githubusercontent.com/u/273620894?v=4",
      "created_at": "2026-04-04T18:43:22Z",
      "is_verified": null,
      "public_repos": 12,
      "account_age_days": 115
    },
    "license": {
      "state": "standard",
      "spdx_id": "Apache-2.0",
      "raw_spdx": "Apache-2.0",
      "file_present": true,
      "scorecard_found": true,
      "profile_has_license": true
    },
    "activity": {
      "releases": [
        {
          "tag": "v1.11.1",
          "kind": "patch",
          "published_at": "2026-07-27T16:22:35Z"
        },
        {
          "tag": "v1.11.0",
          "kind": "minor",
          "published_at": "2026-07-25T14:28:09Z"
        },
        {
          "tag": "v1.10.0",
          "kind": "minor",
          "published_at": "2026-07-24T16:04:26Z"
        },
        {
          "tag": "v1.9.0",
          "kind": "minor",
          "published_at": "2026-07-16T04:33:26Z"
        },
        {
          "tag": "v1.8.0",
          "kind": "minor",
          "published_at": "2026-07-15T22:56:20Z"
        },
        {
          "tag": "v1.7.1",
          "kind": "patch",
          "published_at": "2026-07-14T04:12:54Z"
        },
        {
          "tag": "v1.7.0",
          "kind": "minor",
          "published_at": "2026-07-09T20:28:47Z"
        },
        {
          "tag": "v1.6.0",
          "kind": "minor",
          "published_at": "2026-07-09T07:28:40Z"
        },
        {
          "tag": "v1.5.0",
          "kind": "minor",
          "published_at": "2026-07-09T04:23:20Z"
        },
        {
          "tag": "v1.4.0",
          "kind": "minor",
          "published_at": "2026-07-08T14:22:10Z"
        },
        {
          "tag": "v1.3.0",
          "kind": "minor",
          "published_at": "2026-07-07T07:45:27Z"
        },
        {
          "tag": "v1.2.3",
          "kind": "patch",
          "published_at": "2026-07-05T20:12:54Z"
        },
        {
          "tag": "v1.2.2",
          "kind": "patch",
          "published_at": "2026-07-04T19:43:02Z"
        },
        {
          "tag": "v1.2.1",
          "kind": "patch",
          "published_at": "2026-07-04T18:09:00Z"
        },
        {
          "tag": "v1.2.0",
          "kind": "minor",
          "published_at": "2026-07-04T16:36:57Z"
        },
        {
          "tag": "v1.1.1",
          "kind": "patch",
          "published_at": "2026-07-03T18:52:41Z"
        },
        {
          "tag": "v1.1.0",
          "kind": "minor",
          "published_at": "2026-07-03T13:22:39Z"
        },
        {
          "tag": "v1.0.2",
          "kind": "patch",
          "published_at": "2026-06-30T15:26:03Z"
        },
        {
          "tag": "v1.0.1",
          "kind": "patch",
          "published_at": "2026-06-27T22:16:05Z"
        },
        {
          "tag": "v1.0.0",
          "kind": "major",
          "published_at": "2026-06-27T15:42:31Z"
        },
        {
          "tag": "v0.5.0",
          "kind": "minor",
          "published_at": "2026-06-26T10:47:22Z"
        },
        {
          "tag": "v0.4.0",
          "kind": "minor",
          "published_at": "2026-06-26T05:57:46Z"
        }
      ],
      "recent_commits": [
        {
          "oid": "76297c0046914464342d2953e421efc1668a96f0",
          "body": "Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>",
          "is_bot": true,
          "headline": "chore(main): release 1.11.1 (#52)",
          "author_name": "github-actions[bot]",
          "author_login": "github-actions[bot]",
          "committed_at": "2026-07-27T16:21:10Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "6a4313a438901be095b186e7a754f9ce3afee8ef",
          "body": "…ivo (#50)\n\n* fix(inertia): drenar o FlashBag do framework e corrigir a URL de arquivo\n\nHavia dois stores de flash desconectados: o kernel gravava as recusas de\ndomínio no FlashBag ($_SESSION['_middag_flash']) enquanto buildFlash() lia de\n$SESSION->middag_flash_*. A mensagem era escrita num e lida d\n[…]\nmorria, não só a do customform.\n\n* style: separação de atributos de classe em InertiaSharedProps\n\nphp-cs-fixer (class_attributes_separation) — o único arquivo dos 477 fora do\npadrão, apontado pelo CI.",
          "is_bot": false,
          "headline": "fix(inertia): drenar o FlashBag do framework e corrigir a URL de arqu…",
          "author_name": "Gabriel Sousa",
          "author_login": "gabrieldev-io",
          "committed_at": "2026-07-27T16:11:58Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "64e5fa3ee2f95ec50215d63318f7e4e459572fe6",
          "body": "A 2.5.8 (que também corrige a incompatibilidade com a phpstan 2.2.6,\nrectorphp/rector#9824) passou a propor a remoção de defaults em\npropriedades sempre atribuídas no construtor.\n\nAplicado e validado: o phpstan 2.2.6 acusaria qualquer propriedade que\nficasse sem inicialização em algum caminho, e não acusou nada.\n\ncomposer check completo verde: cs-fixer, rector, phpstan e PSR-4.",
          "is_bot": false,
          "headline": "refactor: aplica as regras novas do rector 2.5.8 (#51)",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-27T15:20:05Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "a83af50390398f0ed3aac40f04a2e6589c033fbd",
          "body": "Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>",
          "is_bot": true,
          "headline": "chore(main): release 1.11.0 (#48)",
          "author_name": "github-actions[bot]",
          "author_login": "github-actions[bot]",
          "committed_at": "2026-07-25T14:26:47Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "eb633a4c496e25012b4f3b18bad7e9581bd01d62",
          "body": "release: promote composition-root symbols consumed by core to @api",
          "is_bot": false,
          "headline": "Merge pull request #47 from middag-io/develop",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-25T14:25:10Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "5fc30d9ee0f1898ad760646186e209e89d457c78",
          "body": "This adapter deliberately ships no container builder (D-FACADE-SEAM):\nwhoever starts the kernel supplies one via ContainerFactory::setBuilder().\nA consumer therefore HAS to name the host-bridge concretes it registers or\ninstantiates at boot, so marking those @internal contradicted the design.\nIt als\n[…]\n docblock states the other tag as bare\nprose, so a consumer scanning docblocks for @api/@internal cannot misread\none for the other.\n\nDocblocks and documentation only — no behaviour change.\n\ncloses #45",
          "is_bot": false,
          "headline": "feat(api): promote the composition-root symbols core consumes to @api",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-25T11:27:53Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "643d5c10218f977b4f2a1e9bd968fa16fcd7a80c",
          "body": "ci: make Moodle stubs PHPStan matrix blocking (closes #39)",
          "is_bot": false,
          "headline": "Merge pull request #44 from middag-io/develop",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-24T22:01:48Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "897811111a5615db4ab86282a626d186ce3b8d77",
          "body": "All four supported stubs series (4.5 / 5.0 / 5.1 / 5.2) analyse clean at\nPHPStan L6, so the per-version class_alias shims (cm_info / modinfo /\nnavigation_node) are now backed by matrix evidence rather than manual\nreasoning. Drop continue-on-error and add the ~5.0 series explicitly so\nper-version coverage is enforced, not informative.",
          "is_bot": false,
          "headline": "ci: make Moodle stubs PHPStan matrix blocking (LB-CI-01, closes #39)",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-24T22:00:02Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0d8ef0f3f793ee714db755ad4b287a64ede8f2fa",
          "body": "Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>",
          "is_bot": true,
          "headline": "chore(main): release 1.10.0 (#40)",
          "author_name": "github-actions[bot]",
          "author_login": "github-actions[bot]",
          "committed_at": "2026-07-24T16:02:29Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f8ac931e26be6bc4d88e25386e877d87ebefa887",
          "body": "XmldbSchemaAdapter::dropIndex() now forwards the index field-set to the\nxmldb_index, which Moodle needs to locate the physical index via\nfind_index_name() (the xmldb_index name is arbitrary, not the DB\nidentifier). Previously the name-only index silently failed to drop, so a\ndependent column drop then errored on PostgreSQL. Requires framework ^1.9\nfor the widened dropIndex() signature.",
          "is_bot": false,
          "headline": "fix(database): drop xmldb index by field-set so column drop succeeds",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-24T15:55:07Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0ca992e7d8a6d3e5d369536d23dd64441f27fb32",
          "body": "…ag-dev\n\nSchema: adds deciders/enforced_by/decision one-liner; body reordered to\nContext -> Considered Options -> Decision -> Consequences -> Enforcement\n(old \"Out of scope\"/\"Links\" sections folded into Consequences prose and\nthe Enforcement table).\n\ndocs/ref/REF-MDL-*-01 (14 of 19 ADRs have a compa\n[…]\nis expected and correct here - this is the\nadapter repo, not the host-agnostic framework.\n\nSee tool-middag-planning TEMP-BACKLOG-ADR-REFORMAT-libs.md for the full\nWave 1 process (framework/moodle/ui).",
          "is_bot": false,
          "headline": "docs: reformat MDL-001..019 to MADR-compact, extract REF to docs-midd…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-21T17:04:52Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d6006afc53a027eb77054be3f0f9f15e8891b587",
          "body": "Enables consumers, like the native router bridge, to dispatch requests and get a PSR-7 response directly. This avoids header conflicts and unreliable output buffering that previously plagued proxying into the framework's HTTP kernel.\n\n*   A new public method is available to return the response objec\n[…]\nThe default request handling now uses this new method for emitting.\n*   The router integration is updated to process the returned response.\n*   Output buffering is removed from the router integration.",
          "is_bot": false,
          "headline": "feat(runtime): expose kernel method to return responses",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-17T18:01:26Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d1c2c4f0d25d2b3de9282e31d3822de26556198f",
          "body": "Adds tests for LangSupport::getStringOrIdentifier()'s catch path (reached\nvia ComponentContext::reset() making name() throw) and\nCompletionSupport::getCmTracking()'s missing-course-info guard. Lines\n99.53% -> 99.60%; CompletionSupport now 100%/100%.",
          "is_bot": false,
          "headline": "test: cover LangSupport and CompletionSupport error branches",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-17T10:54:10Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "1ca0980bda93ba098a91b608a2416a362cb7ec3c",
          "body": "getCssInjection() hardcoded the '--middag-brand' CSS custom property —\na MIDDAG-branded name inside the OSS adapter. Extract it into a\nprotected static brandCssVariable() seam resolved via late static\nbinding, with the value-free '--brand' as the OSS default, so a host\nsubclass can retarget the injected rule onto the property its design\nsystem actually reads.\n\nSame seam pattern as defaultMessageName()/extensionAliases() (SUP-051).\n\nRefs: SUPPORT-STATUS ESCOPO FECHADO item 6",
          "is_bot": false,
          "headline": "feat(support): extract brandCssVariable() seam in ThemeSupport",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T21:00:58Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "8e1d0a1866a7b23eda17217199cc4aa40868b935",
          "body": "sendSimple() hardcoded 'system_notification' as the message provider\nname. Extract it into a protected static defaultMessageName() seam\nresolved via late static binding, so a host subclass can reroute simple\nsends to the provider its product actually registers in db/messages.php.\nThe OSS default sta\n[…]\nendSimple() docblock ('local_example' — the code\nuses ComponentContext::name()). Mirrors the extensionAliases() seam\npattern from SettingsSupport (SUP-051).\n\nRefs: SUPPORT-STATUS ESCOPO FECHADO item 5",
          "is_bot": false,
          "headline": "feat(support): extract defaultMessageName() seam in NotificationSupport",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T20:54:40Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "da8e4caea849f529beb8d1cec892b1d2f71eef0b",
          "body": "The tests matrix (8.2/8.3/8.4) already resolves fresh per cell (no\ncomposer.lock committed), but nothing guarded the outcome: a future\nconstraint drift freezing Symfony 8.x (php >= 8.4.1) would silently mask\nthe ^8.2 floor. Add two assertions per cell:\n\n- composer check-platform-reqs: the resolved v\n[…]\ncap PHP at 8.3 and\nneed an 8.3-resolved vendor (a workstation vendor resolved on 8.4 fails\nplatform_check there — local artifact, not a support limitation).\n\nRefs: SUPPORT-STATUS ESCOPO FECHADO item 3",
          "is_bot": false,
          "headline": "ci: assert per-PHP resolution + document the no-lock convention",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T20:50:23Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "117f1d70248ce3f14ed4e40b373e8bd7a333deec",
          "body": "…eSupport\n\nDrop the hard use statements for core\\di and core\\hook\\di_configuration;\nonly docblocks referenced them. Docblock @see tags are replaced with prose\nmentions because php-cs-fixer (fully_qualified_strict_types) auto-imports\n@see targets, which would reintroduce the use statements.\n\nAligns D\n[…]\nrsion-sensitive classes are referenced via strings only, keeping the\nclass loadable on hosts without the DI hook (Moodle < 4.4).\n\nRefs: SUPPORT-DEPRECATION-MAP §7, SUPPORT-STATUS ESCOPO FECHADO item 2",
          "is_bot": false,
          "headline": "refactor(support): reference Moodle DI classes via strings in DiBridg…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T20:47:42Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b9d744a9b8e788fdb98b34fef693a8f8db98944d",
          "body": "Every @example still called the retired snake_case moodle_versions\nclass (version_semver/major_minor/at_least/assert_min), which does not\nexist in this codebase. All examples now call the actual\nVersionSupport camelCase API.\n\nRefs: LB-MDL-SUP-075",
          "is_bot": false,
          "headline": "docs(support): update VersionSupport examples to the real API",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:50:19Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "fb3275f01b3dff4150a25a80f14fa4e26c3c1fc1",
          "body": "The docblock omitted $fieldid entirely and mislabelled\n$comparison_sql as int — a copy/paste artifact from a prior signature.\nBoth now match the real buildUserSubquery(int, string, array) shape.\n\nRefs: LB-MDL-SUP-074",
          "is_bot": false,
          "headline": "docs(support): correct the buildUserSubquery docblock parameters",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:50:19Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "9c8726c1451a153096fb3c0dae1ce3caac56bee9",
          "body": "toAbsolute() only special-cased http(s) prefixes, so a\nprotocol-relative '//host/path' lost both slashes to the ltrim and got\nrewritten under the local wwwroot — silently pointing a foreign URL at\nthe site. It now completes such URLs with the site scheme.\n\nisExternal() compared hosts case-sensitively, misclassifying a\nsame-site URL with a differently-cased host as external; hostnames are\ncase-insensitive per RFC 3986 §3.2.2, so both sides are lowercased.\n\nRefs: LB-MDL-SUP-072, LB-MDL-SUP-073",
          "is_bot": false,
          "headline": "fix(support): handle protocol-relative and case-differing URLs",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:49:19Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "dd97dbfec6d0ddc1b5d8f4ae9d2e4dd3fb946235",
          "body": "getBrandColor() accepted any non-empty string, and getCssInjection()\ninterpolates the value verbatim into inline <style> content — a\nmalformed or hostile brandcolor setting ('#fff; } body {...') escaped\nstraight into the page's CSS. The value is now allow-listed to\nwell-formed color tokens (hex, rgb()/rgba()/hsl()/hsla(), plain\nidentifiers), mirroring Moodle's own colour-picker validation posture;\nanything else resolves null and the injection is skipped.\n\nRefs: LB-MDL-SUP-071",
          "is_bot": false,
          "headline": "fix(support): validate the theme brand color before CSS interpolation",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:49:19Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "19a7ff1e2ed78fb7059a64c54117a4516e374ddf",
          "body": "…ionBuilder\n\nThe class docblock claimed code-generation tooling consumes build(),\nbut no production caller delegates here — the codegen tool\nre-implements the identical mapping in its own repo (this package is\nonly a dev/suggested dependency there). The docblock now states the\nreal contract: this class is the canonical statement of the mapping,\nkept in lockstep by a parity test on the tooling side.\n\nRefs: LB-MDL-SUP-070",
          "is_bot": false,
          "headline": "docs(support): stop overclaiming a live codegen caller in TaskDefinit…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:47:31Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "3829ca1d201359d55b8a14bc1c1ffc9729d67d24",
          "body": "…pport::set\n\nMoodle's set_user_preference() treats a null $value as 'delete current\nvalue' (delegating to unset_user_preference) — a meaningfully different\nside effect from every other call. Documented at the $value param, the\nstub now mirrors the real delete branch, and a test pins the behaviour\nas intentional.\n\nRefs: LB-MDL-SUP-068",
          "is_bot": false,
          "headline": "fix(support): document and pin null-deletes semantics of PreferenceSu…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:34:26Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "03f598ac22e7e155e0270c0280a1095d6ed9b805",
          "body": "…aliases\n\nThe recording double echoes $type back, so SUP-046's regression test\ncould not catch a future default outside Moodle's recognised set\n('success'/'info'/'warning'/'error' — anything else silently falls\nthrough to the error template). The default parameter value is now\nasserted against that closed set via reflection.\n\nRefs: LB-MDL-SUP-067",
          "is_bot": false,
          "headline": "test(support): pin OutputSupport's default notification type to real …",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:34:26Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "e29c7cc943c317ca44c16b17070d2a7d83512835",
          "body": "…ecute\n\nexecute() returns null both for 'lock busy, skipped' and for a callback\nthat legitimately returned null, so a nullable T is ambiguous by\nconstruction. Documented loudly at @return with the disambiguation\npaths (non-nullable T, or manual acquire()/release()); no API change —\nno consumer needs the tri-state today.\n\nRefs: LB-MDL-SUP-066",
          "is_bot": false,
          "headline": "docs(support): call out the overloaded null return of LockSupport::ex…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:32:10Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "18fc560c5d039ad8c4c7434f1b42b599bdae5c0d",
          "body": "…events\n\nloadCoreEvents() silently dropped an otherwise-valid event when\nget_static_info() threw (empty safe-info + guard), while\nloadPluginEvents() kept the same event with the LEVEL_OTHER fallback —\nthe two loaders handled the identical failure in opposite ways. Core\nevents now degrade to LEVEL_OTHER exactly like plugin events and like\nthe missing-edulevel case both loaders already handled.\n\nRefs: LB-MDL-SUP-065",
          "is_bot": false,
          "headline": "fix(support): keep core events whose static info throws, like plugin …",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:32:10Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "7cb0aa539e9b6078249893435f07685106bc1585",
          "body": "DiBridgeSupport::configure() wrapped both export loops in one\ntry/catch, so a single definition the DI builder rejected aborted the\nregistration of every export after it in iteration order — and\ngetExportedServiceIds() then reported ids that never landed. Each\nadd_definition() is now guarded individually (traced per failure), and\nonly ids actually accepted by the builder are reported as exposed.\n\nRefs: LB-MDL-SUP-064",
          "is_bot": false,
          "headline": "fix(support): isolate DI export registration failures per id",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:30:12Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "a37fa4d618f4481b2fb97d1395b59910b48d80a1",
          "body": "…cords\n\nDbSupport::deleteRecords() narrowed Moodle's delete_records($table,\n?array $conditions = null) to a non-nullable array default, so callers\nomitting $conditions always sent [] and silently lost the TRUNCATE\nfast path moodle_database takes only when $conditions is null. The\nsignature now mirrors the host's nullable contract; a widened default\nis BC for every existing array-passing caller.\n\nRefs: LB-MDL-SUP-063",
          "is_bot": false,
          "headline": "fix(support): mirror the nullable delete_records contract in deleteRe…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:30:12Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "933389705de3aec109ec9105ff72eea52d1ddc74",
          "body": "Every other $SITE field in getSiteInfo() is defensively coalesced, but\nid was read raw: an unseeded/null $SITE raised a property-on-null\nwarning (a hard failure under this repo's failOnWarning=true) instead\nof the degraded DTO the rest of the method guarantees.\n\nRefs: LB-MDL-SUP-062",
          "is_bot": false,
          "headline": "fix(support): null-coalesce the site id in getSiteInfo",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:27:37Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "d18312cc2ef661b9b578a284391a9a7bdd99b904",
          "body": "getCategoryContextOptions() guarded the get_records() read but called\ncontext_coursecat::instance() (MUST_EXIST by default) unguarded on each\nrow: one category whose context row vanished (stale data, concurrent\ndelete mid-iteration) aborted the whole options list with an uncaught\nmoodle_exception in\n[…]\nve DB handling promises. The bad row is now traced and skipped.\nGlobal \\moodle_exception is caught on purpose — it is the base class\non the 4.x floor and the alias target on 5.x.\n\nRefs: LB-MDL-SUP-061",
          "is_bot": false,
          "headline": "fix(support): degrade gracefully when a category context is missing",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:27:37Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "e7077f92120a857b8417134d8761671bbb3ddb78",
          "body": "CacheSupport::setMany()/deleteMany() hardcoded 'return true' whenever\nno exception was thrown, discarding the count-actually-processed that\nMoodle's cache::set_many()/delete_many() return. A partial backend\nfailure (count < requested) therefore read as full success, so callers\nrelying on the documen\n[…]\ny or log never found out.\n\nThe delegate's count is now compared against the requested size, and\nthe stub gained result overrides so the partial-failure branch is\npinned by tests.\n\nRefs: LB-MDL-SUP-060",
          "is_bot": false,
          "headline": "fix(support): report partial bulk-cache failures from setMany/deleteMany",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:24:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "998bdbbe5914b42473b681f6ba6c3206926fe3c9",
          "body": "Moodle's complete_user_login() never returns a falsy value — every\nreturn path yields the populated $USER record, and its failure paths\nredirect (terminating the request) or throw. The bool contract here was\nunreachable fiction: consumers writing 'if (!completeUserLogin(...))'\ngot a permanently dead\n[…]\nmethod / SSO context for\naudit consumers). It is now forwarded, and the stub mirrors the real\nalways-object contract instead of injecting impossible false values.\n\nRefs: LB-MDL-SUP-058, LB-MDL-SUP-059",
          "is_bot": false,
          "headline": "fix(support): return the logged-in user from completeUserLogin",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:22:17Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "1efc9600fd32af72eba822a95965040e3487adbc",
          "body": "…ators\n\nVersionSupport::bootstrap() built the cached semantic as x.y.0 whenever\n$CFG->branch resolved — true on every real host — so a 5.0.7 install\nreported 5.0.0: atLeast('5.0.3') gates never activated and supports()\n'until' entries past x.y.0 never expired. The patch digit is now parsed\nfrom $CFG\n[…]\nrator failure. Consumers audited: no exact-match version gates rely\non the truncated x.y.0 (RouterBridge/mark_tables probe symbols, not\nversions).\n\nRefs: LB-MDL-SUP-055, LB-MDL-SUP-056, LB-MDL-SUP-057",
          "is_bot": false,
          "headline": "fix(support): adopt the release patch digit and type bad compare oper…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:19:27Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d35b908a2a4c3c0713dac1479e8174075c65db66",
          "body": "UserSupport::deleteUser() documents '@return bool True on success,\nfalse otherwise' but delegated straight to Moodle's delete_user(),\nwhich throws a coding_exception when the record lacks the id or\nusername property (moodlelib.php guard) — even though it re-fetches\nthe full row by id and discards th\n[…]\naught and surfaced as false (traced via Debug). The\ndelete_user() stub reproduces the real property_exists guard so the\nsuite can no longer pass a shape real Moodle would reject.\n\nRefs: LB-MDL-SUP-054",
          "is_bot": false,
          "headline": "fix(support): honour deleteUser's bool contract for malformed records",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:15:45Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "a7040c441283108ab21976b7be558b79386fe4dd",
          "body": "TimeSupport::userTimezone()/userTimezoneObject() fell through to the\nargument-less core_date call when core\\user::get_user() returned false\n(deleted/nonexistent id), silently returning the AMBIENT session user's\ntimezone attributed to the requested user — two admins running the same\nreport against t\n[…]\nmissing user now resolves the server timezone explicitly (a real,\ndocumented, deterministic default). Tests pin the fallback with ambient\nand server zones deliberately different.\n\nRefs: LB-MDL-SUP-053",
          "is_bot": false,
          "headline": "fix(support): fall back to server timezone for unresolvable user ids",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:13:59Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "301045eaf3a6407b7b26132e4dcc45998f3b0f17",
          "body": "…alias seam\n\nSettingsSupport::resolve() unconditionally rewrote the derived\n\"framework\" slug to \"core\", hardcoding a consumer-specific naming\nmigration into the generic slug resolver. The base resolver is now\nvalue-free: every {slug}_config enum maps onto its own slug, and a new\nprotected extensionA\n[…]\ne PascalCase-normalisation\nfootgun (mdg_FrameworkConfig_* dead keys) but now asserts the value-free\nmapping; new tests cover the alias seam for reads, writes and unaliased\nslugs.\n\nRefs: LB-MDL-SUP-051",
          "is_bot": false,
          "headline": "fix(support): replace hardcoded framework->core remap with extension-…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T19:06:48Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "a7482a37462215eb02acb990d78822131769408b",
          "body": "sesskey() declared a ': string' return but Moodle's sesskey() returns false\nwhen $_SESSION['USER'] isn't set yet (early bootstrap / CLI / webservice).\nUnder strict_types that false raised an uncaught TypeError instead of\nMoodle's intended soft-fail. Return sesskey() ?: '' so a downstream ===\ncomparison fails cleanly rather than crashing. The test stub was typed\n': string' and could not reproduce this; widen it to string|false and add a\nfalse-return test.\n\nRefs: LB-MDL-SUP-050",
          "is_bot": false,
          "headline": "fix(support): normalise SessionSupport::sesskey false into empty string",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T07:05:33Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "cd247bed6aac579c4ed5b188c1e361c3b113595b",
          "body": "isteacher() and getTeacher() identified the teacher role by shortname,\nwhile getTeachers()/getTeacherAssignment() use archetype. Shortname is\nadmin-editable (rename for i18n/branding is supported and common) while the\narchetype is fixed, so on a site with renamed roles the two paths disagreed:\nistea\n[…]\nfound. Switch both to match archetype IN\n('editingteacher', 'teacher'), using IGNORE_MULTIPLE for getTeacher() since\na custom install can have several roles sharing an archetype.\n\nRefs: LB-MDL-SUP-049",
          "is_bot": false,
          "headline": "fix(support): match teacher roles by archetype in RoleSupport",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T07:05:32Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "8585210399916e5ea5dd85b593dcb8dadcf02b8e",
          "body": "Two unguarded dereferences:\n\nadmin_root::locate() returns a reference to NULL for an unknown or\ncapability-gated section; the code read ->path on it and then count()'d the\nresult, which TypeErrors on null in PHP 8. Degrade to an empty path.\n\nInside the loop, $node = $node->get(array_pop($path)) can \n[…]\nr settingsnav, but ->text/->action were\nread in the same iteration before the next while-check — pushing a blank\nbreadcrumb. Re-check $node and break immediately.\n\nRefs: LB-MDL-SUP-047, LB-MDL-SUP-048",
          "is_bot": false,
          "headline": "fix(support): guard adminLoadNavigation against missing admin nodes",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:59:47Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "4637e5cf93ddf88d4715ef2da44eb4035cbb6479",
          "body": "The default $type was 'notifyinfo', which core_renderer::notification()\ndoes not recognise: 'issuccess'/'isinfo'/... are matched with === against\n'success'/'info'/..., and the legacy alias table has no 'notifyinfo' entry,\nso it falls through to the error template and red styling. Every call that\nomitted $type rendered as an error alert. Default to 'info'.\n\nRefs: LB-MDL-SUP-046",
          "is_bot": false,
          "headline": "fix(support): default OutputSupport::notification to a real info type",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:59:47Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "9c85c7f7e30f6ab2ec67702d24b2742051158989",
          "body": "send() hardcoded fullmessageformat = FORMAT_HTML even when the caller\nfollowed the DTO contract and put genuine plain text in fullMessage\n(distinct from fullMessageHtml). A FORMAT_HTML-aware reader then ran it\nthrough format_text() and mangled it (e.g. stripped '<needs review>').\nDerive the format: \n[…]\nread_popup_notifications(), whose empty() check substitutes $USER,\nleaking the session user's count for a call scoped to id 0. Return 0 for a\nnon-positive userid.\n\nRefs: LB-MDL-SUP-044, LB-MDL-SUP-045",
          "is_bot": false,
          "headline": "fix(support): correct notification message format and unread-count scope",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:53:59Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d8a4725cb4ee74406e0462e91c2d093037338926",
          "body": "getConversationId() dereferenced ->id on $message->userfrom/userto, but\ncore\\message\\message documents both as object|int and they default to\nnull. Passing a raw int id (which Moodle allows) read a property off an int\n— a PHP warning that coerced to 0, collapsing an individual conversation\ninto a bogus self-conversation on user 0. Normalise object|int before use.\n\nRefs: LB-MDL-SUP-043",
          "is_bot": false,
          "headline": "fix(support): normalise int user ids in getConversationId",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:50:26Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "40cb9c19d1c91825b90f11618a3824d4f1262b14",
          "body": "execute()/acquire() defaulted $maxlifetime to 600s. That value is only\nhonoured by the db_record_lock_factory backend (the DB-portable fallback),\nwhere it sets the stale-lock expiry; on it, a job running longer than 10\nminutes without an explicit override has its lock expire mid-run and a\nconcurrent\n[…]\nhe exact double-execution the wrapper\nprevents. Default to Moodle's own interface default (86400 = 24h) to err\nsafe, and document that the value is a no-op on the other backends.\n\nRefs: LB-MDL-SUP-042",
          "is_bot": false,
          "headline": "fix(support): default LockSupport maxlifetime to a safe 24h",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:50:26Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "5d527558788f68b851b603e481c4d129613ee879",
          "body": "getString()'s docblock claimed it was an 'alias for get', but the two\ndiverge by design: get() defaults an omitted component to the plugin via\nComponentContext, while getString() leaves it '' (Moodle-native), which\nnormalises to core. Core-string call sites (RoleSupport 'none', etc.) rely\non the cor\n[…]\n intentionally.\n\nCorrect the docblock to describe the core-first behaviour and point plugin\nlookups at get(), and add a test that pins the intentional divergence.\n\nRefs: LB-MDL-SUP-040, LB-MDL-SUP-041",
          "is_bot": false,
          "headline": "docs(support): document getString's core default, distinct from get()",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:45:00Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "c789a6c756e95d79a8503df44c232394c9bea91f",
          "body": "isMember() delegated to groups_is_member(), which scopes visibility to the\nEXECUTING $USER's moodle/course:viewhiddengroups capability rather than the\n$userid asked about. On a members-only-visibility course a caller lacking\nthat capability (cron, admin acting for a student) got a false negative for\n[…]\nmented '?int, null on failure' contract: a stale\ncourseid or a duplicate idnumber threw straight out to facade/API callers.\nWrap it in try/catch like addMember().\n\nRefs: LB-MDL-SUP-038, LB-MDL-SUP-039",
          "is_bot": false,
          "headline": "fix(support): make GroupSupport isMember unscoped and createGroup safe",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:33:17Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b30129c46c3d11079f2eaaf9cd58841e5b64bc48",
          "body": "getFileEntity() and getAreaFilesTyped() caught Throwable but returned\nnull/[] with no trace, unlike every other method in the class. A failure in\nthe stored_file getter mapping (e.g. version drift) was swallowed silently\nand indistinguishable from 'not found'. Trace before returning, and add\ntests that force the mapping step itself to fail.\n\nRefs: LB-MDL-SUP-037",
          "is_bot": false,
          "headline": "fix(support): trace mapping failures in FileSupport typed getters",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:26:03Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "ca6c0942b1a30aa35b5743b0ac44c622cae5f78a",
          "body": "getEventsByLevel() returned array_filter()'s result without reindexing, so\ndropping a middle event left a gap in the keys and json_encode() produced\nan object ({\"0\":...,\"2\":...}) instead of an array, breaking client code\nthat expects Array semantics. Wrap the filter in array_values().\n\nRefs: LB-MDL-SUP-036",
          "is_bot": false,
          "headline": "fix(support): reindex getEventsByLevel so it stays a JSON array",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:26:02Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0af923e40e1d367f0cb0fa6b5dc8a15b1df807fe",
          "body": "…cating\n\nenrolUser() searched instances with enrol_get_instances($courseid, true),\nwhich excludes disabled instances. A manual instance the admin merely\ndisabled was invisible, so every call inserted a brand-new duplicate\n{enrol} row (there is no courseid+enrol unique constraint). Fetch all\ninstances and re-enable the existing disabled one instead.\n\nRefs: LB-MDL-SUP-035",
          "is_bot": false,
          "headline": "fix(support): reuse a disabled manual enrol instance instead of dupli…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:26:02Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "4a25608fd32cb45c17c5ac0f0e9315baa8403963",
          "body": "getExportedServiceIds() returned the local registerExport() registry, not\nwhat configure() actually pushed into Moodle's DI builder, so it reported a\nservice as exposed even though core\\di::get() would throw (configure()\nnever ran, or the host predates the DI hook). Track a configured flag set\nonce configure() completes and return [] until then.\n\nRefs: LB-MDL-SUP-034",
          "is_bot": false,
          "headline": "fix(support): report only configured exports from DiBridgeSupport",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:26:02Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0aec76d2e82bd00c80d555161f98ee321a31060b",
          "body": "getCourseWithContextidOptions() called context_course::instance() with no\ntry/catch, unlike every sibling context method. A course mid-deletion or a\ncorrupted context table made instance() throw, crashing the whole admin\noptions build instead of skipping the bad course. Wrap the per-course\nlookup in try/catch + traceException + continue.\n\nRefs: LB-MDL-SUP-033",
          "is_bot": false,
          "headline": "fix(support): guard the context lookup in getCourseWithContextidOptions",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:26:01Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "8f04a4593109e4c71f72ef1e847e34ebd1c8e326",
          "body": "getCmTracking()/isEnabledCm() read the raw $cm->completion field, bypassing\ncompletion_info::is_enabled()'s site -> course -> module cascade. That field\nstays set even after completion is disabled higher up, so isEnabledCm()\nreported tracking as enabled while Moodle considered it disabled.\n\nRoute through infoForCourse() + is_enabled($cm), matching the rest of the\nclass, so the result reflects the same cascade Moodle enforces.\n\nRefs: LB-MDL-SUP-032",
          "is_bot": false,
          "headline": "fix(support): respect the completion enablement cascade in getCmTracking",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:26:01Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "608ed3b9c570836a717b5ceffe5e8043fb28d00d",
          "body": "…gging\n\ngetCohorts()'s docblock claimed a flat array<int, object> list, but\ncohort_get_cohorts() always returns a keyed structure (cohorts /\ntotalcohorts / allcohorts) across the supported version range; correct the\nannotation and the masking test.\n\ngetMembers() swallowed dml_exception with no trace, unlike getAll(), making\na DB failure indistinguishable from a genuinely empty cohort. Trace it\nbefore returning [].\n\nRefs: LB-MDL-SUP-030, LB-MDL-SUP-031",
          "is_bot": false,
          "headline": "fix(support): correct CohortSupport getCohorts docs and getMembers lo…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:26:01Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f5f1d096d6dcf5d8694029ab18035b972f5351dd",
          "body": "Two bugs in the per-check loop:\n\nget_result() was called with no per-item try/catch (unlike runCheck()), so\none misbehaving check propagated an uncaught exception and aborted the\nwhole catalog build for every consumer. Guard each check individually.\n\nThe result was keyed by the bare get_id(), which \n[…]\ne only within a\ncomponent, so two plugins' checks sharing an id silently overwrote each\nother. Key by get_ref() (component-qualified), as Moodle core itself does.\n\nRefs: LB-MDL-SUP-028, LB-MDL-SUP-029",
          "is_bot": false,
          "headline": "fix(support): guard and disambiguate CheckSupport::getCheckResults",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:26:00Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "f5ea23f56d20ac3ba6c89645da9e99db35020a54",
          "body": "…alendarSupport\n\nTwo calendar-write bugs:\n\nA site event with a null courseid persisted courseid = 0, but Moodle's\nsite-event handling and calendar_view_event_allowed()'s 'anyone can see\nsite events' shortcut key off courseid == SITEID. Default a null courseid\nto SITEID for site events.\n\ncreate()/upd\n[…]\n the blanket catch swallowed\ninto null. Add a $checkcapability parameter defaulting to false, matching\nMoodle's recommendation for module/system-triggered writes.\n\nRefs: LB-MDL-SUP-026, LB-MDL-SUP-027",
          "is_bot": false,
          "headline": "fix(support): correct site courseid and default capability check in C…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T06:26:00Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "53989df7c36126c1d7a5fce57023a4799a494254",
          "body": "get() treated a stored false or null the same as a miss and returned the\ndefault, violating PSR-16 CacheInterface::get, and getMultiple() disagreed\nwith get() on the same entry (it let a stored null through but not a stored\nfalse).\n\nReconcile both entry points on a single has()-based contract: a val\n[…]\nis returned as-is; only a\ngenuine miss falls back to the default. getMultiple() disambiguates a\nget_many() false via has() so it agrees with get() on every entry.\n\nRefs: LB-MDL-SUP-024, LB-MDL-SUP-025",
          "is_bot": false,
          "headline": "fix(support): return stored false/null from the PSR-16 cache adapter",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:36:27Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "2e749a2169e1d62590cf3375d8c090162d56395c",
          "body": "core_cache\\cache::get() returns false for both a miss and a genuinely\nstored false, so getOrSet()'s '!== false' hit test re-ran the resolver on\nevery call for any falsy result (a common shape for feature/permission\nflags), and its memoisation never engaged.\n\nUse has() to tell a real miss from a stor\n[…]\na passthrough to core_cache\\cache::get_many(),\nwhich returns every requested key with false marking a miss — missing keys\nare NOT omitted as the docblock claimed.\n\nRefs: LB-MDL-SUP-022, LB-MDL-SUP-023",
          "is_bot": false,
          "headline": "fix(support): serve stored falsy values from CacheSupport helpers",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:36:26Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "2edbccd0a8aab18b4bb7afe990c072fb2a64940a",
          "body": "getField()/saveUserData() used is_numeric() to tell a field id from a\nshortname, but Moodle allows purely-numeric profile-field shortnames\n(e.g. '007'), so such a shortname was misrouted to the id lookup — reading\nor writing an unrelated field silently, with no error.\n\nDisambiguate on the real PHP t\n[…]\nith is_int(): a string (even a\nnumeric-looking one) is always a shortname, only a real int is a field id.\nAdd regression tests with a numeric-string shortname at both call sites.\n\nRefs: LB-MDL-SUP-021",
          "is_bot": false,
          "headline": "fix(support): disambiguate UserFieldSupport id vs shortname by type",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:29:23Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "3cc3225e4244083c7cf56e39251f10fa8634170b",
          "body": "The unconditional preg_replace('#/+#', '/', $url) collapsed the '://' of\nany absolute URL down to ':/', so parse_url found no host and moodle_url\nrejected it — under the default IGNORE_MISSING strictness get() then\nsilently fell back to the site home, corrupting every absolute URL passed\nthrough this @api method.\n\nUse a negative lookbehind ('#(?<!:)/{2,}#') so only path slashes collapse\nand '://' is left intact. Add a test with a scheme'd URL.\n\nRefs: LB-MDL-SUP-020",
          "is_bot": false,
          "headline": "fix(support): preserve the scheme separator in UrlSupport::get",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:29:23Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "b0778a2aba044e3e59eb0b1cf64bbe58bd8fc197",
          "body": "resetScheduledTasks() never read db/tasks.php: it re-persisted each task's\ncurrent schedule via set_minute(get_minute()) (an identity write), so a\nplugin upgrade that changed a task's cron never took effect and the method\nalso touched admin-customised tasks it should have left alone.\n\nDelegate to co\n[…]\nr::reset_scheduled_tasks_for_component(), which\nloads defaults from db/tasks.php, respects customisations, and\nupserts/deletes tasks correctly across the supported version range.\n\nRefs: LB-MDL-SUP-019",
          "is_bot": false,
          "headline": "fix(support): delegate resetScheduledTasks to the host reset API",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:29:23Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "5f75e509f00a02b994bd9cb0e3644a3994eea0ac",
          "body": "getRoleOptions() dropped the $excluderoles filtering whenever a course\ncontext was supplied: get_assignable_roles() replaced the already-filtered\nlist with a fresh, unfiltered one, so excluded roles (guest by default,\nor any caller-specified role) silently reappeared in the picker.\n\nRecord the excluded role ids and re-apply them by id after the\nget_assignable_roles() branch. Add a test where an excluded role is\ncourse-assignable.\n\nRefs: LB-MDL-SUP-018",
          "is_bot": false,
          "headline": "fix(support): apply role exclusions to course-context role options",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:29:23Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "04e04e3d11e6e917cf679ec624d50da2d94e314b",
          "body": "getAll() guarded on is_object(), but get_user_preferences(null, ...)\nreturns a plain array (never a stdClass), so the method returned null for\nevery user regardless of how many preferences were set — it was dead in\nproduction.\n\nAccept the array shape Moodle actually returns, stripping the internal\n_lastloaded cache-freshness key so it does not leak as a fake preference,\nand cast to an object. Update the fixture to a real array.\n\nRefs: LB-MDL-SUP-017",
          "is_bot": false,
          "headline": "fix(support): return real preferences from PreferenceSupport::getAll",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:29:22Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "33a8480eacef1694f870eaa62f8400bcad9941ec",
          "body": "acquire()'s try wrapped self::lockType() (ComponentContext::name())\ntogether with the acquisition, so a MoodleConfigurationException thrown\nwhen the composition root never configured the adapter was caught and\nconverted into the same null as ordinary lock contention. execute()\nreturned null forever \n[…]\n coding exceptions propagate; only the acquisition itself degrades to\nnull. Update the factory-throws test to expect propagation and add a test\nfor the unconfigured-context case.\n\nRefs: LB-MDL-SUP-016",
          "is_bot": false,
          "headline": "fix(support): let LockSupport misconfiguration fail loud",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:19:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "98175e183d03fda731231386105e249507fda601",
          "body": "getStringOrIdentifier() defaulted $component to '' instead of null, so\nstringExists() and get() both bypassed their ComponentContext::name()\nfallback (which only coalesces on null). Moodle normalises '' to the core\ncomponent, so a string defined in the plugin's own lang file was never\nfound and the \n[…]\nn omitted component resolves via\nComponentContext, matching get()/stringExists(). NavbarService's explicit\nComponentContext::name() workaround args become redundant but harmless.\n\nRefs: LB-MDL-SUP-015",
          "is_bot": false,
          "headline": "fix(support): resolve getStringOrIdentifier component from context",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:19:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "857b272da5d62e18c782baae408ad6c44c30c90a",
          "body": "\"SELECT gg.{$fields}\" only prefixed the first field with the gg. alias.\ngrade_grades and grade_items share column names (id, hidden, locked,\ntimecreated, ...), so any such name in a non-first position of a\nmulti-field selector was left unqualified and the DB rejected it as an\nambiguous column — get_\n[…]\nws.\n\nQualify every token with gg. (leaving already-qualified tokens and '*'\nalone). Add a test asserting a multi-field selector including an\noverlapping column stays unambiguous.\n\nRefs: LB-MDL-SUP-014",
          "is_bot": false,
          "headline": "fix(support): qualify every field in GradeSupport::getGrade select",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:19:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "1446183a29455ea5bc66834e5c8ecd8c450d0578",
          "body": "…catalog\n\nloadPluginEvents()/loadCoreEvents() called $fqcn::get_name() with no\ntry/catch, unlike get_static_info() which is routed through\ngetStaticInfoSafe(). get_name() is equally plugin-overridable and can\nthrow (e.g. a get_string() against a missing lang string). Because the\nloaders iterate ever\n[…]\nevel().\n\nRoute both call sites through a getNameSafe() wrapper that catches\nThrowable and falls back to the class short name, and add a throwing\nget_name() fixture to lock it in.\n\nRefs: LB-MDL-SUP-013",
          "is_bot": false,
          "headline": "fix(support): guard event get_name() so one bad class can't kill the …",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:11:18Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "66aabbe6e85f272f62fbff28838ed008b248817a",
          "body": "…rrect\n\nTwo related enrolment bugs:\n\ngetEnrol() ran get_record_sql + IGNORE_MULTIPLE with no ORDER BY, so a\nuser holding several concurrent enrolments in a course (e.g. manual and\ncohort-sync) got an arbitrary, non-deterministic row. EnrolmentService's\nsuspend/reactivate then acted on whichever row \n[…]\nrue.\nCheck the exact requested outcome instead — the manual instance's\nuser_enrolments row plus user_has_role_assignment() — and only skip when\nboth already hold.\n\nRefs: LB-MDL-SUP-011, LB-MDL-SUP-012",
          "is_bot": false,
          "headline": "fix(support): make EnrolSupport enrolment selection and role grant co…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:11:18Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b18f5c069d3d5c6b4e495af56d080a92c1c6a88c",
          "body": "CustomFieldSupport called the handler with $returnall omitted (default\nfalse), so core_customfield filtered out any field whose per-field\nvisibility excludes the ambient $USER. In cron/CLI/webservice context a\nhidden-but-populated field became indistinguishable from an absent one:\ngetFieldValues/get\n[…]\nefined field. saveFieldData now also returns false\nwhen a requested shortname has no matching field, instead of reporting\nsuccess for a partially-dropped payload.\n\nRefs: LB-MDL-SUP-009, LB-MDL-SUP-010",
          "is_bot": false,
          "headline": "fix(support): read/write all custom fields regardless of visibility",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:02:10Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "314a5d3c4746c0ba2842cf32a55635c5d5396e98",
          "body": "capability() derived the suffix by rtrim()'ing every trailing 's' off the\npluralised slug. Inflector::slug() naively appends 's', so a basename that\nnatively ends in 's' (Status -> \"statuss\", Address -> \"addresss\")\nover-stripped to \"statu\"/\"addre\". The resulting capability matched\nnothing in db/acce\n[…]\ndy singular by convention, so lowercase it directly\ninstead of the pluralise/de-pluralise round-trip — identical output for\nevery other name. Add a case for an s-ending basename.\n\nRefs: LB-MDL-SUP-008",
          "is_bot": false,
          "headline": "fix(support): stop over-stripping trailing s in capability suffix",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:02:10Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b237e9952166ac46d4ff8b2d196f0728748cdd37",
          "body": "getCourseUrl($courseid, $sectionid) wrote the section id into the\n`section` query param, but course/view.php treats `section` as a raw\nsection NUMBER and only `sectionid` as a course_sections.id it resolves\nvia DB. Deep links built from a real section id therefore jumped to an\nunrelated section or fell through to section 0.\n\nSend the id through `sectionid`. The test asserted the wrong key, so it\ncodified the bug; assert sectionid and that section is absent.\n\nRefs: LB-MDL-SUP-007",
          "is_bot": false,
          "headline": "fix(support): route CourseSupport::getCourseUrl id through sectionid",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T05:02:10Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "614cc8fb591985eb5f20474aec944c64201a5161",
          "body": "Moodle's get_config() returns false (never null) for an absent key, so\nConfigSupport::get/getConfig could only ever yield false for absent and\nfalse for error, contradicting their own docblocks (null if not set).\nConsumers trusting the documented contract wrote get($k) ?? $default,\nwhich never fired\n[…]\n via the\ncatch). This amends the QG-MDL-06 'false for both' policy, which is\nsuperseded because the normalisation restores the distinction its own\n'?? default' consumers assumed.\n\nRefs: LB-MDL-SUP-006",
          "is_bot": false,
          "headline": "fix(support): distinguish absent config from read failure in getters",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T04:53:18Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "09cc8f57d1976d83664298955fca982f3c81a043",
          "body": "getCourseCriteria() cast the raw course_completion_criteria.criteriatype\ncolumn straight to a string, so it yielded '4' instead of 'activity'. The\ncolumn is an int (COMPLETION_CRITERIA_TYPE_*), never the label string that\nCompletionCriteriaDto documents, so every criteriaType === activity check\neval\n[…]\n constant mirroring Moodle's\n$COMPLETION_CRITERIA_TYPES global. The coverage fixture fed string values\n(impossible on a real int column), masking the bug; feed real ints instead.\n\nRefs: LB-MDL-SUP-005",
          "is_bot": false,
          "headline": "fix(support): map completion criteria type int to its Moodle label",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T04:53:18Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "7a14ccbca9d24c3c0b267e10c347cb725b15e735",
          "body": "CalendarSupport::create/update set only $data->repeats (the count), but\ncalendar_event::update() gates its repeat-generation loop on the separate\nboolean $data->repeat flag. Without it the count was silently ignored and\nonly a single event row was ever created, while create() still returned a\nnon-nu\n[…]\np that\nthe try/catch never engaged.\n\nDerive $data->repeat from the requested count in both methods. The stub\nnow records the data it received so tests can assert the flag is set.\n\nRefs: LB-MDL-SUP-003",
          "is_bot": false,
          "headline": "fix(support): set the calendar repeat flag so recurring events persist",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T04:53:17Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "1f7893f04686e105f30b906f1fd9b1dfd8b551e4",
          "body": "get_admin() returns stdClass|false (false when $CFG->siteadmins is empty\n— during install before an admin exists, or transiently in unit tests),\nnever null. Returning that false straight through the ?stdClass signature\nraised a TypeError in the caller instead of degrading to null.\n\nNormalise the hos\n[…]\n test stub returned null (never\nfalse) for the no-admin case, so the crash path had zero coverage; make\nthe stub default to false (matching real Moodle) and add an explicit test.\n\nRefs: LB-MDL-SUP-002",
          "is_bot": false,
          "headline": "fix(support): normalise AuthSupport::getAdmin false into null",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T04:53:17Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "173eb0cc431057208452df70425346127189091c",
          "body": "ACTION_COMPLETE and ACTION_OVERRIDE were 1 and 2, but the real\n\\core_competency\\evidence enum defines them as 2 and 3 (there is no\nvalue 1). Passing the wrong integer to api::add_evidence() lands on the\nswitch default arm and throws coding_exception, which addEvidence()\nswallows via catch(Throwable)\n[…]\nnstants. Guard the stub\nagainst invalid actions (mirroring the real switch default) and add an\nexplicit test pinning the three constant values to the Moodle enum.\n\nRefs: LB-MDL-SUP-001, LB-MDL-SUP-004",
          "is_bot": false,
          "headline": "fix(support): correct CompetencySupport evidence action constants",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T04:36:04Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d8b271a80d259f9ef0b04d93a9f5b05362b77c30",
          "body": "chore(main): release 1.9.0",
          "is_bot": false,
          "headline": "Merge pull request #38 from middag-io/release-please--branches--main",
          "author_name": "Michael Douglas Meneses de Souza",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T04:32:14Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "3019d3ea472651137def8f7f87a587215f0331b0",
          "body": null,
          "is_bot": true,
          "headline": "chore(main): release 1.9.0",
          "author_name": "github-actions[bot]",
          "author_login": "github-actions[bot]",
          "committed_at": "2026-07-16T04:31:58Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "1fe0ff9cc204f7773742e14d923b29f1236327c1",
          "body": "release: moodle 1.9.0 — condition-compiler impl + domain/database refactors",
          "is_bot": false,
          "headline": "Merge pull request #37 from middag-io/develop",
          "author_name": "Michael Douglas Meneses de Souza",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T04:30:40Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "e7fbcd92cff9cd68bb1e43225d3b25f1104ab90b",
          "body": "Hold the major despite the breaking domain/database refactors on develop: core pins middag-io/moodle ^1.8 and the develop-only / releases-1.x directive keeps the chain on 1.x.\n\nRelease-As: 1.9.0",
          "is_bot": false,
          "headline": "chore: release moodle 1.9.0",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T04:28:49Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "b899ba05625f065f33e46e40dc8b406be0ca52ad",
          "body": "MessageSupport::createTempAttachment() created its temp subdir as\n`middag/mtool_automessage` — a hard-coded product brand plus the `mtool_*`\nsubplugin-type slug from the previous local_middag architecture, both dead and\nout of place in an Apache-2.0 package. Resolve the subdir from the\nComponentContext composition-root seam instead (as FileSupport already does),\nyielding a component-scoped, brand-free path like `local_middag/message_attachments`.",
          "is_bot": false,
          "headline": "fix(support): drop legacy brand slug from message temp dir",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T03:07:07Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "856151b69467ff46c3ce214a4ad9efaaad829270",
          "body": "…tity\n\nAdd Course::isVisible() so the \"visible === 1\" rule lives on the entity\ninstead of the support helper. CourseSupport::isCourseVisible() now fetches and\ndelegates to it; behavior and signature are unchanged.\n\ngetCourseFormat is left as-is: the format already reads straight off the entity\n(get_format()), so there is nothing to relocate there.",
          "is_bot": false,
          "headline": "refactor(domain): move course-visibility predicate onto the Course en…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T02:21:29Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "58902f928064cc514f31ad1d81d53a0219f78d09",
          "body": "Move domain policy out of the low-level Support primitives into the domain\nservice layer, so the Supports stay thin host wrappers:\n\n- EnrolmentServiceInterface owns the default-role policy via a new\n  DEFAULT_STUDENT_ROLE_ID constant; EnrolmentService and its DTO fallback use\n  it. EnrolSupport::enr\n[…]\nolSupport::enrolUser() no longer defaults roleid — pass it\nexplicitly or use EnrolmentServiceInterface::enrol(). GroupSupport::addUserInGroup()\nis removed; use GroupServiceInterface::addUserToGroup().",
          "is_bot": false,
          "headline": "refactor(domain)!: extract enrolment/group domain policy from supports",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T02:12:59Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "8cfd624a39c89215c3619a6312d6b2cff9603195",
          "body": "Now that the query/transaction contracts live in the framework (OSS), the\nadapter implements them directly instead of duck-typing or re-declaring:\n\n- SqlGenerator implements the framework Persistence\\Contract\\\n  ConditionCompilerInterface directly. The core-side binding subclass is no\n  longer neede\n[…]\nct\\\n  TransactionManagerInterface.\n\nBREAKING CHANGE: Middag\\Moodle\\Database\\Contract\\TransactionManagerInterface\nis removed; use Middag\\Framework\\Database\\Contract\\TransactionManagerInterface\ninstead.",
          "is_bot": false,
          "headline": "refactor(database)!: consume framework query/transaction seams",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T01:49:49Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "c3e0348290785215b32f53b9aeaaf3e84ca65108",
          "body": "Delegate generic logic to the framework utils added upstream instead of\nre-implementing it in the adapter:\n\n- GradeSupport::getGrade() uses Typing::toNumber() for the single-field numeric\n  return (behavior identical to the previous toFloat/toInt comparison).\n- CrudConventionResolver slug/title/sing\n[…]\nr::mergePreferNonNull(), covered by the framework test suite.\n\nNo public contract change: getGrade() keeps its signature and return values;\nthe removed method and CrudConventionResolver are @internal.",
          "is_bot": false,
          "headline": "refactor(support): adopt framework Shared/Util helpers",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T01:35:51Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "87186e1d52afaa86fa77ca57b5c5be9b1241eae4",
          "body": "Environment::getEnvironment() resolves MIDDAG_ENV/APP_ENV (step 2) above the $CFG host hook (step 3). A container/CI MIDDAG_ENV=development leaked into PHPUnit and overrode the tests' intended environment, breaking the production cache-path assertions in FacadeLoaderCoverageTest and EventSupportCoverageTest. Neutralize both vars in the host and no-host bootstraps so $CFG-driven environment control is authoritative.",
          "is_bot": false,
          "headline": "test: neutralize ambient MIDDAG_ENV/APP_ENV in bootstraps",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T00:06:11Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "580e3a37ba70c38fd57ba3ccd512ccf95477a6af",
          "body": "DEBUG_DEVELOPER is defined as E_ALL on every supported Moodle branch, and E_ALL tracks the running PHP (30719 on 8.4, 32767 before). The hardcoded 32767 was stale on PHP 8.4. Builds on a9da3f3 (constant()/defined() guard), changing only the fallback literal.",
          "is_bot": false,
          "headline": "fix(shared): use E_ALL for the no-host DEBUG_DEVELOPER fallback",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-16T00:06:11Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "05089c9e279ecca5302ed363b9100222d51a2a68",
          "body": "chore(main): release 1.8.0",
          "is_bot": false,
          "headline": "Merge pull request #36 from middag-io/release-please--branches--main",
          "author_name": "Michael Douglas Meneses de Souza",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-15T22:55:00Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "1d9828362ba715197b93ce8759bd3123e5fd982e",
          "body": null,
          "is_bot": true,
          "headline": "chore(main): release 1.8.0",
          "author_name": "github-actions[bot]",
          "author_login": "github-actions[bot]",
          "committed_at": "2026-07-15T22:53:59Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "3152bb5b4af3173300e31d59abb8f83323b14336",
          "body": "release: promote develop for 1.8.0",
          "is_bot": false,
          "headline": "Merge pull request #35 from middag-io/develop",
          "author_name": "Michael Douglas Meneses de Souza",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-15T22:52:29Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "a9da3f31b818fb6d1c362cebaae8c3afc9c19803",
          "body": "The moodle-stubs 4.5 series types the DEBUG_DEVELOPER literal in a way\nPHPStan proves always-false against $CFG->debug, failing the matrix job.\nResolve the constant dynamically with a defined() guard (32767 fallback,\nMoodle's value on every supported branch) so environment inference stays\nidentical at runtime and analysable on every stubs series.",
          "is_bot": false,
          "headline": "fix(shared): keep DEBUG_DEVELOPER comparison opaque to per-stub analysis",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-15T22:50:30Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "a79432c2dcbe7f2dc73895faaf868c671c96c655",
          "body": "…e 4.5-5.2\n\nDbSupport now mirrors the full public moodle_database API instead of a\nhand-picked subset. Runtime incident: local_middag automessage handlers\ndispatch db::get_records_select_menu through the facade snake bridge and\nthe missing camelCase wrapper made AbstractFacade throw\nBadMethodCallExc\n[…]\n primary surface)\n- tests: one recording-double test per wrapper (106 total in the class),\n  false->null normalization covered, legacy-slave fallback and pre-5.2\n  no-op covered with dedicated doubles",
          "is_bot": false,
          "headline": "feat(support): complete the DbSupport delegation surface across Moodl…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-15T15:49:19Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "e8c0482676ff69ab0cf2a96e7f409ecba9db0bd8",
          "body": "Typed language-string definition in the Definition family: key +\nlocale=>string map ('en' mandatory — Moodle's fallback locale), with\nthe family's min/max Moodle version gating. Unlike the db/* definitions\nit is not collected from extensions: a catalog class passes them\nexplicitly to the statics generator, which renders a managed block\ninside the consumer plugin's lang/<locale>/<plugin>.php preserving\nhand-written strings. [LB-LANG-01]",
          "is_bot": false,
          "headline": "feat(definition): add LangDefinition for managed lang-file blocks",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-15T03:21:51Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f7f6f6f2847a68885f1a98cc1e0b747bb3156e18",
          "body": null,
          "is_bot": false,
          "headline": "chore: ignore the no-host phpunit cache directory",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-15T02:58:46Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "5f37a3a70934286814e2f35e6b842c9debe3eb56",
          "body": "The rebuilt stubs re-emit runtime class_alias() calls as declarations\n(bimoo 3ca7d7c), so the action_link and moodle_url compat shims are no\nlonger needed: tests/phpstan/moodle-compat.stub.php is deleted and the\nscanFiles block dropped. The two expects-direction exemptions stay —\na signature EXPECTI\n[…]\n their comment updated to the new stubs reality.\n\nThe lock is not versioned in this lib; consumers pick the tag up via\nthe existing ~5.0.0 constraint.\n\ncheck + both test suites green (2999 + 8 tests).",
          "is_bot": false,
          "headline": "build(stubs): adopt v5.0.8.2 and retire the class-alias compat shims",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-15T02:58:30Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b99ac11cbef5b7cc2cf0309ac2ece35dbc87914a",
          "body": "… property\n\nThe v5.0.8.1 rebuild is the first stubs tag whose files keep their use\nimports (bimoo fix); accurate resolution unmasked one real finding —\nsection_info::$section is the deprecated magic property, replaced by\nsectionnum (Moodle 4.4+, inside the 4.5 support floor). Fixtures\nupdated to the\n[…]\n shim for the receiving\nside, and the three expects-legacy-name signatures carry scoped,\ndocumented exemptions. Lock is gitignored here — consumers pick\nv5.0.8.1 up through ~5.0.0 on their own update.",
          "is_bot": false,
          "headline": "fix(support): adopt the rebuilt stubs and drop the deprecated section…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-15T01:43:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "ebee03981af88d224b4a2d60810305a36dbec9b9",
          "body": "Widget and the framework Kernel are this library's own hard\ndependencies — always autoloadable inside the suite, so their\nclass_exists() fallbacks can only fire on a broken host install.\nRuling: @codeCoverageIgnore over injectable seams.",
          "is_bot": false,
          "headline": "test(coverage): annotate the library-class guards as unreachable [R-05]",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-15T01:02:20Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "15f23caf9fda83ef8932572250a7062c5894d1b6",
          "body": "The main bootstrap defines stand-ins for every Moodle symbol, which\nmakes the library's own class_exists()/function_exists() absence guards\nunreachable — the branches that keep the adapter loadable outside a\nMoodle runtime had no coverage at all.\n\nAdd phpunit.no-host.xml + tests/bootstrap-no-host.ph\n[…]\nrmat_backtrace fallbacks, and MoodleOriginResolver.\n\ncomposer test now runs both suites; test:no-host / test:no-host:cov\nreport the suite separately (second coverage report instead of a\nphpcov merge).",
          "is_bot": false,
          "headline": "test(no-host): cover the host-absence guards with a stub-free suite",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-15T00:21:56Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "713368be675b7694b6dfada0a7a1a854ac0bc699",
          "body": "…EL_* constants\n\nThe enum shipped Teaching=0/Participating=1/Other=2 while \\core\\event\\base\ndefines LEVEL_OTHER=0/LEVEL_TEACHING=1/LEVEL_PARTICIPATING=2 — every event\ngenerated from a typed definition would log a wrong edulevel.",
          "is_bot": false,
          "headline": "fix(enum): align EventEdulevel backing values with the core event LEV…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-14T15:37:21Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "4da30de3eb14a06d7522ab1f608baf3b1243b9ed",
          "body": "…assnames\n\nMoodle plugin namespaces use the frankenstyle name as a single segment\n(\\local_middag\\event\\...); splitting it on underscores produced\n\\local\\middag\\event\\... which matches no autoloadable class.",
          "is_bot": false,
          "headline": "fix(definition): keep the frankenstyle plugin name intact in event cl…",
          "author_name": "Michael Meneses",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-14T15:37:21Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "9c7b16635b210afd098b5580d7ff4773bcba3d9b",
          "body": "chore(main): release 1.7.1",
          "is_bot": false,
          "headline": "Merge pull request #34 from middag-io/release-please--branches--main",
          "author_name": "Michael Douglas Meneses de Souza",
          "author_login": "michaelmeneses",
          "committed_at": "2026-07-14T04:11:46Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "1258e960359c8b2d2209a975f4c964cc7f869f6d",
          "body": null,
          "is_bot": true,
          "headline": "chore(main): release 1.7.1",
          "author_name": "github-actions[bot]",
          "author_login": "github-actions[bot]",
          "committed_at": "2026-07-14T04:09:37Z",
          "body_truncated": false,
          "is_coding_agent": false
        }
      ],
      "releases_count": 22,
      "commits_last_year": 254,
      "latest_release_at": "2026-07-27T16:22:35Z",
      "latest_release_tag": "v1.11.1",
      "releases_from_tags": false,
      "days_since_last_push": 0,
      "active_weeks_last_year": 5,
      "days_since_latest_release": 1,
      "mean_days_between_releases": 2.1
    },
    "community": {
      "has_readme": true,
      "has_license": true,
      "has_description": true,
      "has_contributing": true,
      "health_percentage": 87,
      "has_issue_template": false,
      "has_code_of_conduct": true,
      "has_pull_request_template": false
    },
    "ecosystem": {
      "packages": [
        {
          "name": "middag-io/moodle",
          "exists": true,
          "license": "Apache-2.0",
          "keywords": [
            "plugin",
            "adapter",
            "moodle",
            "inertia",
            "xmldb",
            "host-adapter",
            "middag",
            "mform"
          ],
          "ecosystem": "packagist",
          "matches_repo": true,
          "registry_url": "https://packagist.org/packages/middag-io/moodle",
          "is_deprecated": false,
          "latest_version": "v1.11.1",
          "repository_url": "https://github.com/middag-io/middag-php-moodle",
          "versions_count": 22,
          "total_downloads": 2041,
          "dependents_count": 0,
          "deprecation_note": null,
          "maintainers_count": null,
          "monthly_downloads": 2038,
          "first_published_at": null,
          "latest_published_at": "2026-07-27T16:21:10Z",
          "latest_version_yanked": null,
          "days_since_latest_publish": 1
        }
      ]
    },
    "popularity": {
      "forks": 0,
      "stars": 0,
      "watchers": 0,
      "fork_history": {
        "days": [],
        "complete": true,
        "collected": 0,
        "total_forks": 0
      },
      "star_history": {
        "days": [],
        "complete": true,
        "collected": 0,
        "total_stars": 0,
        "collected_at": null
      },
      "open_issues_and_prs": 1
    },
    "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": 61160,
      "source_files_sampled": 479,
      "oversized_source_files": 1,
      "agent_instruction_files": [
        "CLAUDE.md"
      ],
      "agent_instruction_max_bytes": 13720
    },
    "dependencies": {
      "manifests": [
        "composer.json"
      ],
      "advisories": {
        "error": null,
        "scope": null,
        "source": null,
        "findings": [],
        "collected": false,
        "malicious": [],
        "truncated": false,
        "by_severity": {},
        "advisory_count": 0,
        "affected_count": 0,
        "assessed_count": 0,
        "malicious_count": 0,
        "assessed_package": null,
        "unassessed_count": 0,
        "direct_affected_count": 0
      },
      "ecosystems": [
        "packagist"
      ],
      "dependencies": [
        {
          "name": "firebase/php-jwt",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^7.0"
        },
        {
          "name": "middag-io/framework",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^1.9"
        },
        {
          "name": "middag-io/ui",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^1.3"
        },
        {
          "name": "nyholm/psr7",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^1.8"
        },
        {
          "name": "psr/container",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^2.0"
        },
        {
          "name": "psr/log",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^3.0"
        },
        {
          "name": "psr/simple-cache",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^3.0"
        },
        {
          "name": "symfony/dependency-injection",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^7.0"
        },
        {
          "name": "symfony/event-dispatcher-contracts",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^3.0"
        },
        {
          "name": "symfony/http-client",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^7.0"
        },
        {
          "name": "symfony/http-foundation",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^7.0"
        },
        {
          "name": "symfony/routing",
          "manifest": "composer.json",
          "ecosystem": "packagist",
          "version_constraint": "^7.0"
        }
      ],
      "all_dependencies": {
        "error": "GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository",
        "source": null,
        "packages": [],
        "collected": false,
        "truncated": false,
        "total_count": null,
        "direct_count": null,
        "indirect_count": null
      }
    },
    "maintainership": {
      "issues": {
        "open_prs": 0,
        "merged_prs": 47,
        "open_issues": 1,
        "closed_ratio": 0.8,
        "closed_issues": 4,
        "closed_unmerged_prs": 0
      },
      "bus_factor": 1,
      "bot_contributors": 1,
      "top_contributors": [
        {
          "type": "User",
          "login": "michaelmeneses",
          "commits": 232,
          "avatar_url": "https://avatars.githubusercontent.com/u/6410303?v=4"
        },
        {
          "type": "User",
          "login": "gabrieldev-io",
          "commits": 1,
          "avatar_url": "https://avatars.githubusercontent.com/u/280796700?v=4"
        }
      ],
      "contributors_sampled": 2,
      "top_contributor_share": 0.996
    },
    "quality_signals": {
      "has_ci": true,
      "has_tests": true,
      "ci_workflows": [
        "ci.yml",
        "release.yml"
      ],
      "has_docs_dir": true,
      "linter_configs": [
        ".php-cs-fixer.dist.php"
      ],
      "has_editorconfig": false,
      "has_linter_config": true,
      "has_precommit_config": false
    },
    "security_signals": {
      "lockfiles": [],
      "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": 3,
            "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": 8,
            "reason": "6 out of 7 merged PRs checked by a CI test -- score normalized to 8",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#ci-tests"
          },
          {
            "name": "CII-Best-Practices",
            "score": 0,
            "reason": "no effort to earn an OpenSSF best practices badge detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#cii-best-practices"
          },
          {
            "name": "Code-Review",
            "score": 0,
            "reason": "Found 1/25 approved changesets -- score normalized to 0",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#code-review"
          },
          {
            "name": "Contributors",
            "score": 6,
            "reason": "project has 2 contributing companies or organizations -- score normalized to 6",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#contributors"
          },
          {
            "name": "Dangerous-Workflow",
            "score": null,
            "reason": "no workflows found",
            "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": 0,
            "reason": "project was created within the last 90 days. Please review its contents carefully",
            "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": null,
            "reason": "no dependencies found",
            "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": null,
            "reason": "No tokens found",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#token-permissions"
          },
          {
            "name": "Vulnerabilities",
            "score": 10,
            "reason": "0 existing vulnerabilities detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#vulnerabilities"
          }
        ],
        "commit": "76297c0046914464342d2953e421efc1668a96f0",
        "ran_at": "2026-07-28T22:13:50Z",
        "aggregate_score": 3.3,
        "scorecard_version": "v5.5.0"
      },
      "has_codeql_workflow": false,
      "has_security_policy": true,
      "has_dependabot_config": false
    },
    "contribution_flow": {
      "collected": true,
      "ci_last_run_at": "2026-07-27T16:22:53Z",
      "oldest_open_prs": [],
      "last_merged_pr_at": "2026-07-27T22:02:18Z",
      "ci_last_conclusion": "SUCCESS",
      "oldest_open_issues": [
        {
          "number": 49,
          "created_at": "2026-07-25T15:24:47Z",
          "last_comment_at": null,
          "last_comment_author": null
        }
      ]
    }
  },
  "config": {
    "disabled_metrics": [],
    "disabled_categories": [],
    "disabled_components": {}
  },
  "source": {
    "url": "https://github.com/middag-io/middag-php-moodle",
    "host": "github.com",
    "name": "middag-php-moodle",
    "owner": "middag-io"
  },
  "metrics": {
    "overall": {
      "key": "overall",
      "band": "moderate",
      "name": "Overall health",
      "note": null,
      "notes": [],
      "value": 60,
      "inputs": {
        "security": 33,
        "vitality": 75,
        "community": 41,
        "governance": 54,
        "engineering": 88
      },
      "components": []
    },
    "categories": [
      {
        "key": "vitality",
        "band": "good",
        "name": "Vitality",
        "value": 75,
        "weight": 0.22,
        "metrics": [
          {
            "key": "development_activity",
            "band": "moderate",
            "name": "Development activity",
            "note": null,
            "notes": [],
            "value": 58,
            "inputs": {
              "commits_last_year": 254,
              "human_commit_share": 0.94,
              "days_since_last_push": 0,
              "active_weeks_last_year": 5
            },
            "components": [
              {
                "key": "push_recency",
                "name": "Push recency",
                "detail": "last push 0 days ago",
                "points": 36,
                "status": "met",
                "details": [
                  {
                    "code": "push_recency",
                    "params": {
                      "days": 0
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "commit_cadence",
                "name": "Commit cadence",
                "detail": "5/52 weeks with commits",
                "points": 3.5,
                "status": "partial",
                "details": [
                  {
                    "code": "commit_cadence_weeks",
                    "params": {
                      "weeks": 5
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "commit_volume",
                "name": "Commit volume",
                "detail": "254 commits in the last year",
                "points": 18,
                "status": "met",
                "details": [
                  {
                    "code": "commits_last_year",
                    "params": {
                      "count": 254
                    }
                  }
                ],
                "max_points": 18
              },
              {
                "key": "openssf_scorecard_maintained",
                "name": "OpenSSF Scorecard: Maintained",
                "detail": "project was created within the last 90 days. Please review its contents carefully",
                "points": 0,
                "status": "missed",
                "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": 22,
              "latest_release_tag": "v1.11.1",
              "releases_from_tags": false,
              "days_since_latest_release": 1,
              "mean_days_between_releases": 2.1
            },
            "components": [
              {
                "key": "ships_releases",
                "name": "Ships releases",
                "detail": "22 releases published",
                "points": 27,
                "status": "met",
                "details": [
                  {
                    "code": "releases_published",
                    "params": {
                      "count": 22
                    }
                  }
                ],
                "max_points": 27
              },
              {
                "key": "release_recency",
                "name": "Release recency",
                "detail": "latest release 1 days ago",
                "points": 36,
                "status": "met",
                "details": [
                  {
                    "code": "release_recency",
                    "params": {
                      "days": 1
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "release_cadence",
                "name": "Release cadence",
                "detail": "a release every ~2.1 days",
                "points": 27,
                "status": "met",
                "details": [
                  {
                    "code": "release_cadence",
                    "params": {
                      "gap": 2.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": "unverified",
              "guards": [],
              "signals": [],
              "red_flag": false,
              "multiplier_pct": 100,
              "declared_reason": null,
              "unverified_reason": "repository_too_young",
              "unanswered_open_prs": null,
              "unanswered_open_issues": null,
              "days_since_last_merged_pr": null,
              "days_since_last_human_commit": null,
              "days_since_last_human_commit_is_floor": false
            },
            "components": [
              {
                "key": "project_is_still_maintained",
                "name": "Project is still maintained",
                "detail": "maintenance record not established from the collected data",
                "points": 100,
                "status": "met",
                "details": [
                  {
                    "code": "abandonment_unverified",
                    "params": {}
                  }
                ],
                "max_points": 100
              }
            ]
          }
        ],
        "description": "Is the project alive — is code being written and are releases shipping?"
      },
      {
        "key": "community",
        "band": "at_risk",
        "name": "Community & Adoption",
        "value": 41,
        "weight": 0.18,
        "metrics": [
          {
            "key": "popularity",
            "band": "critical",
            "name": "Popularity & adoption",
            "note": null,
            "notes": [],
            "value": 1,
            "inputs": {
              "forks": 0,
              "stars": 0,
              "watchers": 0,
              "growth_state": "unverified",
              "growth_factor_pct": 100,
              "growth_unverified_reason": "no_history"
            },
            "components": [
              {
                "key": "stars",
                "name": "Stars",
                "detail": "0 stars",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "stars",
                    "params": {
                      "count": 0
                    }
                  }
                ],
                "max_points": 60
              },
              {
                "key": "forks",
                "name": "Forks",
                "detail": "0 forks",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "forks",
                    "params": {
                      "count": 0
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "watchers",
                "name": "Watchers",
                "detail": "0 watchers",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "watchers",
                    "params": {
                      "count": 0
                    }
                  }
                ],
                "max_points": 15
              }
            ]
          },
          {
            "key": "community_health",
            "band": "excellent",
            "name": "Community health",
            "note": null,
            "notes": [],
            "value": 85,
            "inputs": {
              "has_readme": true,
              "has_license": true,
              "has_contributing": true,
              "has_issue_template": false,
              "has_code_of_conduct": true,
              "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 (Apache-2.0)",
                "points": 22.5,
                "status": "met",
                "details": [
                  {
                    "code": "license_standard",
                    "params": {}
                  },
                  {
                    "code": "license_spdx",
                    "params": {
                      "spdx": "Apache-2.0"
                    }
                  }
                ],
                "max_points": 22.5
              },
              {
                "key": "contributing_guide",
                "name": "CONTRIBUTING guide",
                "detail": null,
                "points": 18,
                "status": "met",
                "details": [],
                "max_points": 18
              },
              {
                "key": "code_of_conduct",
                "name": "Code of conduct",
                "detail": null,
                "points": 13.5,
                "status": "met",
                "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": "at_risk",
            "name": "Ecosystem adoption (downloads)",
            "note": null,
            "notes": [],
            "value": 44,
            "inputs": {
              "packages": [
                "middag-io/moodle"
              ],
              "dependents": 0,
              "ecosystems": "packagist",
              "total_downloads": 2041,
              "monthly_downloads": 2038
            },
            "components": [
              {
                "key": "monthly_downloads",
                "name": "Monthly downloads",
                "detail": "2,038 downloads/month across packagist",
                "points": 44.1,
                "status": "partial",
                "details": [
                  {
                    "code": "downloads_monthly",
                    "params": {
                      "count": 2038,
                      "ecosystems": "packagist"
                    }
                  }
                ],
                "max_points": 80
              },
              {
                "key": "registry_dependents",
                "name": "Registry dependents",
                "detail": "0 packages depend on it",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "registry_dependents",
                    "params": {
                      "count": 0
                    }
                  }
                ],
                "max_points": 20
              }
            ]
          }
        ],
        "description": "Does the project have users, downloads, attention, and a welcoming setup for contributors?"
      },
      {
        "key": "governance",
        "band": "moderate",
        "name": "Sustainability & Governance",
        "value": 54,
        "weight": 0.24,
        "metrics": [
          {
            "key": "maintainer_resilience",
            "band": "critical",
            "name": "Maintainer resilience (bus factor)",
            "note": null,
            "notes": [],
            "value": 18,
            "inputs": {
              "bus_factor": 1,
              "contributors_sampled": 2,
              "top_contributor_share": 0.996
            },
            "components": [
              {
                "key": "bus_factor",
                "name": "Bus factor",
                "detail": "1 contributor(s) cover half of all commits",
                "points": 9,
                "status": "partial",
                "details": [
                  {
                    "code": "bus_factor",
                    "params": {
                      "count": 1
                    }
                  }
                ],
                "max_points": 54
              },
              {
                "key": "commit_distribution",
                "name": "Commit distribution",
                "detail": "top contributor authored 100% of commits",
                "points": 0.1,
                "status": "partial",
                "details": [
                  {
                    "code": "top_contributor_share",
                    "params": {
                      "share": 100
                    }
                  }
                ],
                "max_points": 22.5
              },
              {
                "key": "contributor_breadth",
                "name": "Contributor breadth",
                "detail": "2 contributors",
                "points": 2.7,
                "status": "partial",
                "details": [
                  {
                    "code": "contributors_sampled",
                    "params": {
                      "count": 2
                    }
                  }
                ],
                "max_points": 13.5
              },
              {
                "key": "openssf_scorecard_contributors",
                "name": "OpenSSF Scorecard: Contributors",
                "detail": "project has 2 contributing companies or organizations -- score normalized to 6",
                "points": 6,
                "status": "partial",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "responsiveness",
            "band": "good",
            "name": "Issue & PR responsiveness",
            "note": null,
            "notes": [],
            "value": 76,
            "inputs": {
              "merged_prs": 47,
              "open_issues": 1,
              "closed_issues": 4,
              "issue_closed_ratio": 0.8,
              "closed_unmerged_prs": 0
            },
            "components": [
              {
                "key": "issue_resolution",
                "name": "Issue resolution",
                "detail": "80% of issues closed",
                "points": 37.4,
                "status": "partial",
                "details": [
                  {
                    "code": "issues_closed_share",
                    "params": {
                      "share": 80
                    }
                  }
                ],
                "max_points": 46.75
              },
              {
                "key": "pr_acceptance",
                "name": "PR acceptance",
                "detail": "47/47 decided PRs merged",
                "points": 38.2,
                "status": "met",
                "details": [
                  {
                    "code": "decided_prs_merged",
                    "params": {
                      "merged": 47,
                      "decided": 47
                    }
                  }
                ],
                "max_points": 38.25
              },
              {
                "key": "openssf_scorecard_code_review",
                "name": "OpenSSF Scorecard: Code-Review",
                "detail": "Found 1/25 approved changesets -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 15
              }
            ]
          },
          {
            "key": "stewardship",
            "band": "at_risk",
            "name": "Ownership & stewardship",
            "note": null,
            "notes": [],
            "value": 39,
            "inputs": {
              "followers": 0,
              "owner_type": "Organization",
              "is_verified": null,
              "owner_login": "middag-io",
              "public_repos": 12,
              "account_age_days": 115
            },
            "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": "0 followers of middag-io",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "owner_followers",
                    "params": {
                      "count": 0,
                      "login": "middag-io"
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "track_record",
                "name": "Track record",
                "detail": "12 public repos, account ~0 yr old",
                "points": 8.7,
                "status": "partial",
                "details": [
                  {
                    "code": "public_repos",
                    "params": {
                      "count": 12
                    }
                  },
                  {
                    "code": "account_age_years",
                    "params": {
                      "years": 0
                    }
                  }
                ],
                "max_points": 25
              }
            ]
          },
          {
            "key": "package_maintenance",
            "band": "excellent",
            "name": "Package maintenance",
            "note": null,
            "notes": [],
            "value": 100,
            "inputs": {
              "packages": [
                "middag-io/moodle"
              ],
              "ecosystems": "packagist",
              "any_deprecated": false,
              "min_days_since_publish": 1
            },
            "components": [
              {
                "key": "published_resolvable",
                "name": "Published & resolvable",
                "detail": "1 package(s) on packagist",
                "points": 25,
                "status": "met",
                "details": [
                  {
                    "code": "packages_published",
                    "params": {
                      "count": 1,
                      "ecosystems": "packagist"
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "publish_recency",
                "name": "Publish recency",
                "detail": "latest publish 1 days ago",
                "points": 35,
                "status": "met",
                "details": [
                  {
                    "code": "publish_recency",
                    "params": {
                      "days": 1
                    }
                  }
                ],
                "max_points": 35
              },
              {
                "key": "version_history",
                "name": "Version history",
                "detail": "22 published versions",
                "points": 20,
                "status": "met",
                "details": [
                  {
                    "code": "published_versions",
                    "params": {
                      "count": 22
                    }
                  }
                ],
                "max_points": 20
              },
              {
                "key": "not_deprecated",
                "name": "Not deprecated",
                "detail": "active, not deprecated or yanked",
                "points": 20,
                "status": "met",
                "details": [
                  {
                    "code": "package_not_deprecated",
                    "params": {}
                  }
                ],
                "max_points": 20
              }
            ]
          }
        ],
        "description": "Will the project survive its people — bus factor, responsiveness, who backs it, and package upkeep?"
      },
      {
        "key": "engineering",
        "band": "excellent",
        "name": "Engineering Quality",
        "value": 88,
        "weight": 0.2,
        "metrics": [
          {
            "key": "engineering_practices",
            "band": "good",
            "name": "Engineering practices",
            "note": null,
            "notes": [],
            "value": 80,
            "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": ".php-cs-fixer.dist.php",
                "points": 16,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": ".php-cs-fixer.dist.php"
                    }
                  }
                ],
                "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": "6 out of 7 merged PRs checked by a CI test -- score normalized to 8",
                "points": 16,
                "status": "partial",
                "details": [],
                "max_points": 20
              }
            ]
          },
          {
            "key": "documentation",
            "band": "excellent",
            "name": "Documentation",
            "note": null,
            "notes": [],
            "value": 100,
            "inputs": {
              "topics": [
                "client-middag",
                "platform-php",
                "type-library",
                "status-active"
              ],
              "has_wiki": true,
              "homepage": "https://docs.middag.dev",
              "has_readme": true,
              "has_docs_dir": true,
              "has_description": true
            },
            "components": [
              {
                "key": "readme",
                "name": "README",
                "detail": null,
                "points": 30,
                "status": "met",
                "details": [],
                "max_points": 30
              },
              {
                "key": "documentation_directory",
                "name": "Documentation directory",
                "detail": null,
                "points": 25,
                "status": "met",
                "details": [],
                "max_points": 25
              },
              {
                "key": "documentation_homepage_site",
                "name": "Documentation / homepage site",
                "detail": "https://docs.middag.dev",
                "points": 15,
                "status": "met",
                "details": [],
                "max_points": 15
              },
              {
                "key": "repository_description",
                "name": "Repository description",
                "detail": null,
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              },
              {
                "key": "topics",
                "name": "Topics",
                "detail": "4 topics",
                "points": 10,
                "status": "met",
                "details": [
                  {
                    "code": "topics_count",
                    "params": {
                      "count": 4
                    }
                  }
                ],
                "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": "at_risk",
        "name": "Security",
        "value": 33,
        "weight": 0.16,
        "metrics": [
          {
            "key": "security_posture",
            "band": "at_risk",
            "name": "Security posture",
            "note": "Excluded from scoring (no data or not applicable): Dangerous-Workflow, Packaging, Pinned-Dependencies, Signed-Releases, Token-Permissions. Remaining weights renormalized.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "dangerous_workflow",
                    "packaging",
                    "pinned_dependencies",
                    "signed_releases",
                    "token_permissions"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              }
            ],
            "value": 33,
            "inputs": {
              "source": "openssf_scorecard",
              "checks_evaluated": 13,
              "scorecard_version": "v5.5.0",
              "checks_inconclusive": 5,
              "scorecard_aggregate": 3.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": 2.2,
                "status": "partial",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "ci_tests",
                "name": "CI-Tests",
                "detail": "6 out of 7 merged PRs checked by a CI test -- score normalized to 8",
                "points": 2,
                "status": "partial",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "cii_best_practices",
                "name": "CII-Best-Practices",
                "detail": "no effort to earn an OpenSSF best practices badge detected",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "code_review",
                "name": "Code-Review",
                "detail": "Found 1/25 approved changesets -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "contributors",
                "name": "Contributors",
                "detail": "project has 2 contributing companies or organizations -- score normalized to 6",
                "points": 1.5,
                "status": "partial",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "dangerous_workflow",
                "name": "Dangerous-Workflow",
                "detail": "no workflows found",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "no_data",
                    "params": {}
                  }
                ],
                "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": "project was created within the last 90 days. Please review its contents carefully",
                "points": 0,
                "status": "missed",
                "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": "no dependencies found",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "no_data",
                    "params": {}
                  }
                ],
                "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": "No tokens found",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "no_data",
                    "params": {}
                  }
                ],
                "max_points": 7.5
              },
              {
                "key": "vulnerabilities",
                "name": "Vulnerabilities",
                "detail": "0 existing vulnerabilities detected",
                "points": 7.5,
                "status": "met",
                "details": [],
                "max_points": 7.5
              }
            ]
          },
          {
            "key": "high_risk_jurisdiction_exposure",
            "band": "excellent",
            "name": "High-Risk Jurisdiction Exposure",
            "note": "Only high-confidence self-published location evidence affects this multiplier. Ambiguous matches are review-only; country evidence is not proof of nationality, citizenship, legal registration, malicious intent, or sanctions status.",
            "notes": [
              {
                "code": "jurisdiction_evidence_limits",
                "params": {}
              }
            ],
            "value": 100,
            "inputs": {
              "meaning": "self-published location evidence; not nationality or citizenship",
              "red_flag": false,
              "exposures": [],
              "policy_countries": [
                "Russia",
                "Iran",
                "North Korea"
              ],
              "review_only_matches": 0,
              "assessed_self_published_locations": 2
            },
            "components": [
              {
                "key": "policy_exposure_multiplier",
                "name": "Policy exposure multiplier",
                "detail": "no confirmed policy-scope location match",
                "points": 100,
                "status": "met",
                "details": [
                  {
                    "code": "jurisdiction_no_match",
                    "params": {}
                  }
                ],
                "max_points": 100
              }
            ]
          }
        ],
        "description": "Are visible security and supply-chain practices strong, with no malicious dependency and no unresolved high-risk jurisdiction exposure?"
      },
      {
        "key": "ai_readiness",
        "band": "moderate",
        "name": "AI Readiness",
        "value": 57,
        "weight": 0,
        "metrics": [
          {
            "key": "ai_agent_context",
            "band": "excellent",
            "name": "Agent context & guidance",
            "note": null,
            "notes": [],
            "value": 85,
            "inputs": {
              "has_llms_txt": false,
              "legible_history_share": 1,
              "agent_instruction_files": [
                "CLAUDE.md"
              ],
              "agent_instruction_max_bytes": 13720
            },
            "components": [
              {
                "key": "agent_instructions",
                "name": "Agent instructions",
                "detail": "CLAUDE.md",
                "points": 45,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "CLAUDE.md"
                    }
                  }
                ],
                "max_points": 45
              },
              {
                "key": "machine_readable_docs_llms_txt",
                "name": "Machine-readable docs (llms.txt)",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 15
              },
              {
                "key": "legible_commit_history",
                "name": "Legible commit history",
                "detail": "94 of 94 human commits state their intent (structured subject or explanatory body)",
                "points": 40,
                "status": "met",
                "details": [
                  {
                    "code": "legible_history",
                    "params": {
                      "legible": 94,
                      "sampled": 94
                    }
                  }
                ],
                "max_points": 40
              }
            ]
          },
          {
            "key": "ai_verify_loop",
            "band": "at_risk",
            "name": "Verify loop (build / test / typecheck)",
            "note": "Excluded from scoring (no data or not applicable): OpenSSF Scorecard: Pinned-Dependencies. Remaining weights renormalized.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "openssf_scorecard_pinned_dependencies"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              }
            ],
            "value": 37,
            "inputs": {
              "has_nix": false,
              "has_tests": true,
              "lockfiles": [],
              "has_dockerfile": false,
              "typed_language": false,
              "bootstrap_files": [],
              "has_devcontainer": false,
              "has_linter_config": true,
              "typecheck_configs": [],
              "agent_commit_share": 0,
              "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": ".php-cs-fixer.dist.php",
                "points": 11,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": ".php-cs-fixer.dist.php"
                    }
                  }
                ],
                "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": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 10
              },
              {
                "key": "demonstrated_agent_practice",
                "name": "Demonstrated agent practice",
                "detail": "no agent-authored commits among the last 100",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "no_agent_authored_commits",
                    "params": {
                      "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": "no dependencies found",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "no_data",
                    "params": {}
                  }
                ],
                "max_points": 10
              }
            ]
          },
          {
            "key": "ai_code_legibility",
            "band": "moderate",
            "name": "Code legibility for models",
            "note": null,
            "notes": [],
            "value": 55,
            "inputs": {
              "primary_language": "PHP",
              "largest_source_bytes": 61160,
              "source_files_sampled": 479,
              "oversized_source_files": 1
            },
            "components": [
              {
                "key": "type_checkable_code",
                "name": "Type-checkable code",
                "detail": "PHP without a type-check config",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "no_typecheck_config_language",
                    "params": {
                      "language": "PHP"
                    }
                  }
                ],
                "max_points": 45
              },
              {
                "key": "manageable_file_sizes",
                "name": "Manageable file sizes",
                "detail": "1/479 source files over 60KB",
                "points": 54.9,
                "status": "partial",
                "details": [
                  {
                    "code": "oversized_source_files",
                    "params": {
                      "kb": 60,
                      "sampled": 479,
                      "oversized": 1
                    }
                  }
                ],
                "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": [
    "GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository"
  ],
  "report_type": "repository",
  "generated_at": "2026-07-28T22:14:00.939706Z",
  "schema_version": "0.27.0",
  "badge_url": "https://raw.githubusercontent.com/inspect-software/badges/main/v1/m/middag-io/middag-php-moodle.svg",
  "full_name": "middag-io/middag-php-moodle",
  "license_state": "standard",
  "license_spdx": "Apache-2.0"
}

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

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

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