Публічний реєстр
Звіт про здоров'я програмного забезпеченнясхема 0.27.0 · метрики 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 зірок⑂ 0 форківз черв. 2026 р.Переглянути на GitHub ↗

middag-io/middag-php-moodle має індекс здоров’я 60 зі 100, що відповідає смузі «Помірний». Найвищий показник — Engineering Quality (88/100), найнижчий — Security (33/100). Останнє оновлення — сьогодні. Більшість нещодавньої роботи виконує один учасник.

60
загалом / 100
Помірний

Індекс здоров'я програмного забезпечення

Метрики згруповано у зважені категорії на шкалі 1–100. Загальна оцінка починається як їхнє середнє; коли публічні дані активують Політику юрисдикцій високого ризику, рейтинг коригується й отримує верхню межу 49 («Під ризиком»). Готовність до ШІ не входить до індексу.

60
Відмінний85-100Зразковий; відповідає практично всім перевіреним критеріям
Добрий70-84Здоровий; незначні прогалини
Помірний50-69Прийнятний, але з помітними прогалинами; рекомендовано перевірку
У зоні ризику30-49Суттєві слабкі місця; впровадження потребує обережності
Критичний1-29Серйозні проблеми (покинутий, єдиний мейнтейнер, без базової гігієни)
ЖиттєздатністьСпільнота тавпровадженняСталість таврядуванняІнженернаякістьБезпекаГотовність доШІ

Профіль оцінок

Кожна вісь — окрема категорія. Форма важить більше, ніж середнє: здоровий об'єкт заповнює всю фігуру, тоді як профіль із піками та провалами означає, що сила в одному вимірі маскує ризик в іншому.

Власність

MIDDAGОрганізація
0 підписників12 публічних репозиторіївз квіт. 2026 р.

За цим репозиторієм стоїть організація — спільна, підзвітна опіка, здатна пережити будь-якого окремого мейнтейнера.

Пакетні екосистеми

РеєстрПакетВерсіяЗавантажень / місВерсіїОстання публікаціяТеги
Packagistmiddag-io/moodlev1.11.12 038221 день томуpluginadaptermoodleinertiaxmldbhost-adaptermiddagmform

Метрики за категоріями

Життєздатність

Чи живий проєкт — чи пишеться код і чи виходять релізи?

75Добрий · 22% загального індексу
Як обчислюється оцінка
36/36Свіжість push — останній push 0 дн. тому
3.5/36Ритм комітів — 5/52 тижнів із комітами
18/18Обсяг комітів — 254 комітів за останній рік
0/10OpenSSF Scorecard: Maintained — project was created within the last 90 days. Please review its contents carefully
Використані вхідні дані
commits_last_year254
human_commit_share0,94
days_since_last_push0
active_weeks_last_year5
Як обчислюється оцінка
27/27Випускає релізи — опубліковано 22 релізів
36/36Свіжість релізів — останній реліз 1 дн. тому
27/27Ритм релізів — реліз кожні ~2,1 дн.
0/10OpenSSF Scorecard: Signed-Releases — немає даних
Використані вхідні дані
releases_count22
latest_release_tagv1.11.1
releases_from_tagsні
days_since_latest_release1
mean_days_between_releases2,1
Виключено з оцінювання (немає даних або не застосовно): OpenSSF Scorecard: Signed-Releases. Залишкові ваги перенормовано.

Спільнота та впровадження

Чи має проєкт користувачів, завантаження, увагу та влаштовані умови для контриб’юторів?

41У зоні ризику · 18% загального індексу
Як обчислюється оцінка
0/60Зірки — 0 зірок
0/25Форки — 0 форків
0/15Спостерігачі — 0 спостерігачів
Використані вхідні дані
forks0
stars0
watchers0
growth_stateunverified
growth_factor_pct100
growth_unverified_reasonno_history
Як обчислюється оцінка
22.5/22.5README
22.5/22.5Ліцензія — визнана ліцензія (Apache-2.0)
18/18Настанови CONTRIBUTING
13.5/13.5Кодекс поведінки
0/7.2Шаблон issue
0/6.3Шаблон PR
Використані вхідні дані
has_readmeтак
has_licenseтак
has_contributingтак
has_issue_templateні
has_code_of_conductтак
has_pull_request_templateні
Як обчислюється оцінка
44.1/80Щомісячні завантаження — 2 038 завантажень/місяць у packagist
0/20Залежні пакети в реєстрі — залежних пакетів: 0
Використані вхідні дані
packagesmiddag-io/moodle
dependents0
ecosystemspackagist
total_downloads2 041
monthly_downloads2 038

Сталість та врядування

Чи переживе проєкт своїх людей — бас-фактор, реактивність, хто за ним стоїть і як супроводжуються пакети?

54Помірний · 24% загального індексу
Як обчислюється оцінка
9/54Бас-фактор — на 1 контриб’ютор(ів) припадає половина всіх комітів
0.1/22.5Розподіл комітів — головний контриб’ютор — автор 100% комітів
2.7/13.5Широта контриб’юторів — 2 контриб’юторів
6/10OpenSSF Scorecard: Contributors — project has 2 contributing companies or organizations -- score normalized to 6
Використані вхідні дані
bus_factor1
contributors_sampled2
top_contributor_share0,996
Як обчислюється оцінка
37.4/46.8Вирішення issue — закрито 80% issue
38.2/38.3Прийняття PR — злито 47/47 вирішених PR
0/15OpenSSF Scorecard: Code-Review — Found 1/25 approved changesets -- score normalized to 0
Використані вхідні дані
merged_prs47
open_issues1
closed_issues4
issue_closed_ratio0,8
closed_unmerged_prs0

Власність та опіка

39У зоні ризику
Як обчислюється оцінка
30/30Підтримка власника — у власності організації
0/20Верифікований домен
0/25Охоплення власника — 0 підписників у middag-io
8.7/25Послужний список — 12 публічних репозиторіїв, вік облікового запису ~0 р.
Використані вхідні дані
followers0
owner_typeOrganization
is_verified
owner_loginmiddag-io
public_repos12
account_age_days115

Супровід пакетів

100Відмінний
Як обчислюється оцінка
25/25Опубліковано й доступно — 1 пакет(ів) у packagist
35/35Свіжість публікацій — остання публікація 1 дн. тому
20/20Історія версій — 22 опублікованих версій
20/20Не застарілий — активний, не deprecated і не yanked
Використані вхідні дані
packagesmiddag-io/moodle
ecosystemspackagist
any_deprecatedні
min_days_since_publish1

Інженерна якість

Чи наявні базові інженерні практики та документація?

88Відмінний · 20% загального індексу
Як обчислюється оцінка
24/24Процеси CI — 2 процес(ів) CI
24/24Наявні тести
16/16Конфігурація лінтера — .php-cs-fixer.dist.php
0/9.6Pre-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
Використані вхідні дані
has_ciтак
has_testsтак
has_editorconfigні
has_linter_configтак
has_precommit_configні

Документація

100Відмінний
Як обчислюється оцінка
30/30README
25/25Каталог документації
15/15Сайт документації / домашня сторінка — https://docs.middag.dev
10/10Опис репозиторію
10/10Теми — 4 тем
10/10Wiki
Використані вхідні дані
topicsclient-middag, platform-php, type-library, status-active
has_wikiтак
homepagehttps://docs.middag.dev
has_readmeтак
has_docs_dirтак
has_descriptionтак

Безпека

Чи міцні видимі практики безпеки й ланцюга постачання, без непослабленої пов’язаності з юрисдикціями високого ризику?

33У зоні ризику · 16% загального індексу

Стан безпеки

33У зоні ризику
Як обчислюється оцінка
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 — немає даних
0/7.5Dependency-Update-Tool — no update tool detected
0/5Fuzzing — project is not fuzzed
2.5/2.5Ліцензія — license file detected
0/7.5Maintained — project was created within the last 90 days. Please review its contents carefully
0/5Packaging — немає даних
0/5Pinned-Dependencies — немає даних
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 — немає даних
0/7.5Token-Permissions — немає даних
7.5/7.5Vulnerabilities — 0 existing vulnerabilities detected
Використані вхідні дані
sourceopenssf_scorecard
checks_evaluated13
scorecard_versionv5.5.0
checks_inconclusive5
scorecard_aggregate3,3
Виключено з оцінювання (немає даних або не застосовно): dangerous_workflow, packaging, pinned_dependencies, signed_releases, token_permissions. Залишкові ваги перенормовано.

Готовність до ШІ

Наскільки репозиторій оснащений для розробки та супроводу за участі ШІ-агентів? Незалежний, експериментальний бейдж — вага 0.0, тож він подається окремо і не впливає на загальний індекс здоров'я.

57Помірний · 0% загального індексу
Як обчислюється оцінка
45/45Інструкції для агентів — CLAUDE.md
0/15Машиночитана документація (llms.txt)
40/40Читабельна історія комітів — намір зазначено у 94 з 94 людських комітів (структурований заголовок або пояснювальний текст)
Використані вхідні дані
has_llms_txtні
legible_history_share1
agent_instruction_filesCLAUDE.md
agent_instruction_max_bytes13 720
Як обчислюється оцінка
0/18Розгортання однією командою
22/22Автоматизовані тести
11/11Конфігурація лінтера / форматера — .php-cs-fixer.dist.php
0/11Статична перевірка типів
0/10Відтворюване середовище
0/10Підтверджена практика роботи з агентами — серед останніх 100 комітів немає створених агентом
0/8Автоматизоване супроводження — автоматичних оновлень залежностей не виявлено
0/10OpenSSF Scorecard: Pinned-Dependencies — немає даних
Використані вхідні дані
has_nixні
has_testsтак
lockfiles
has_dockerfileні
typed_languageні
bootstrap_files
has_devcontainerні
has_linter_configтак
typecheck_configs
agent_commit_share0
toolchain_manifests
dependency_bot_commit_share0
Виключено з оцінювання (немає даних або не застосовно): OpenSSF Scorecard: Pinned-Dependencies. Залишкові ваги перенормовано.
Як обчислюється оцінка
0/45Типізований код — PHP без конфігурації перевірки типів
54.9/55Керовані розміри файлів — 1/479 файлів вихідного коду понад 60 КБ
Використані вхідні дані
primary_languagePHP
largest_source_bytes61 160
source_files_sampled479
oversized_source_files1

Ключові факти

0зірок GitHub
2контриб'юторів
254комітів за останні 12 місяців
0днів від останнього пушу
22релізів
1бас-фактор
1відкритих issue
Packagistпакетних екосистем

Попередження щодо збору даних

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

Докладніше

OpenSSF Scorecard 3.3 / 10
3.3сукупно

Незалежна, не прив'язана до інструментів оцінка безпеки від відкритого проєкту OpenSSF Scorecard. Кожна перевірка винагороджує практику безпеки, а не інструмент конкретного постачальника. Перевірки, які Scorecard не зміг визначити, позначено н/д і виключено з оцінки безпеки (вони ніколи не зараховуються як нуль).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
н/дDangerous-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
н/дPackagingpackaging workflow not detected
н/дPinned-Dependenciesno dependencies found
0SASTSAST tool is not run on all commits -- score normalized to 0
0Security-Policysecurity policy file not detected
н/дSigned-Releasesno releases found
н/дToken-PermissionsNo tokens found
10Vulnerabilities0 existing vulnerabilities detected
Прямі залежності 12
РеєстрПакетОбмеження версіїМаніфест
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
Усі залежності не зібрано

Не вдалося зібрати розв'язаний набір залежностей для цього звіту: GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository

Звіт у форматі JSON машиночитний
{
  "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"
}

Оцінки — це сигнали, а не гарантії. Вони відображають публічно видимі практики на GitHub — це не аудит коду й не гарантія безпеки.

Відсутні дані виключаються, а ваги перенормовуються — нуль за відсутність ніколи не ставиться. Методологія версіонована й відкрита: метрики v1.13.0, схема v0.27.0 — повна методологія · вікі метрик.

Як окремий результат виглядає на тлі всього реєстру: сукупна статистикаPackagist.