Öffentliches Register
Software-GesundheitsberichtSchema 0.27.0 · Metriken 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 Sterne⑂ 0 Forksseit Juni 2026Auf GitHub ansehen ↗

middag-io/middag-php-moodle erreicht einen Gesundheitsindex von 60 von 100 und liegt damit im Bereich Mittel. Am stärksten schneidet es bei Engineering Quality (88/100) ab, am schwächsten bei Security (33/100). Zuletzt heute aktualisiert. Ein einzelner Mitwirkender trägt den Großteil der jüngsten Arbeit.

60
gesamt / 100
Mittel

Software-Gesundheitsindex

Metriken werden auf einer Skala von 1–100 in gewichtete Kategorien gruppiert. Der Gesamtwert beginnt als ihr Mittel; sobald öffentliche Evidenz die Richtlinie für Hochrisikojurisdiktionen auslöst, wird die Bewertung angepasst und erhält die Obergrenze 49 (Gefährdet). AI Readiness liegt außerhalb.

60
Exzellent85-100Vorbildlich; erfüllt im Wesentlichen alle geprüften Kriterien
Gut70-84Gesund; geringfügige Lücken
Mittel50-69Akzeptabel mit deutlichen Lücken; Überprüfung empfohlen
Gefährdet30-49Erhebliche Schwächen; eine Übernahme erfordert Vorsicht
Kritisch1-29Schwerwiegende Probleme (aufgegeben, nur ein Maintainer, keine Hygiene)
VitalitätCommunity &VerbreitungNachhaltigkeit &GovernanceEngineering-QualitätSicherheitAI Readiness

Bewertungsprofil

Jede Achse ist eine Kategorie. Die Form zählt mehr als der Durchschnitt — ein gesundes Projekt füllt die gesamte Fläche, während ein Profil aus Spitzen und Kratern bedeutet, dass Stärke in einer Dimension Risiken in einer anderen verdeckt.

Eigentümerschaft

MIDDAGOrganisation
0 Follower12 öffentliche Reposseit Apr. 2026

Dieses Repository wird von einer Organisation getragen — geteilte, rechenschaftspflichtige Trägerschaft, die jeden einzelnen Maintainer überdauern kann.

Paket-Ökosysteme

RegistryPaketVersionDownloads / MonatVersionenZuletzt veröffentlichtTags
Packagistmiddag-io/moodlev1.11.12.03822vor 1 Tagpluginadaptermoodleinertiaxmldbhost-adaptermiddagmform

Metriken nach Kategorie

Vitalität

Lebt das Projekt — wird Code geschrieben und werden Releases ausgeliefert?

75Gut · 22 % des Gesamtindex
Wie die Bewertung erfolgt
36/36Push-Aktualität — letzter Push vor 0 Tagen
3.5/36Commit-Rhythmus — 5/52 Wochen mit Commits
18/18Commit-Volumen — 254 Commits im letzten Jahr
0/10OpenSSF Scorecard: Maintained — project was created within the last 90 days. Please review its contents carefully
Verwendete Eingangsdaten
commits_last_year254
human_commit_share0,94
days_since_last_push0
active_weeks_last_year5

Release-Disziplin

100Exzellent
Wie die Bewertung erfolgt
27/27Liefert Releases aus — 22 Releases veröffentlicht
36/36Release-Aktualität — letztes Release vor 1 Tagen
27/27Release-Rhythmus — ein Release etwa alle 2,1 Tage
0/10OpenSSF Scorecard: Signed-Releases — keine Daten
Verwendete Eingangsdaten
releases_count22
latest_release_tagv1.11.1
releases_from_tagsnein
days_since_latest_release1
mean_days_between_releases2,1
Von der Bewertung ausgeschlossen (keine Daten oder nicht anwendbar): OpenSSF Scorecard: Signed-Releases. Die verbleibenden Gewichte wurden renormalisiert.

Community & Verbreitung

Hat das Projekt Nutzer, Downloads, Aufmerksamkeit und ein einladendes Umfeld für Beitragende?

41Gefährdet · 18 % des Gesamtindex
Wie die Bewertung erfolgt
0/60Stars — 0 Stars
0/25Forks — 0 Forks
0/15Watcher — 0 Watcher
Verwendete Eingangsdaten
forks0
stars0
watchers0
growth_stateunverified
growth_factor_pct100
growth_unverified_reasonno_history
Wie die Bewertung erfolgt
22.5/22.5README
22.5/22.5Lizenz — anerkannte Lizenz (Apache-2.0)
18/18CONTRIBUTING-Leitfaden
13.5/13.5Verhaltenskodex
0/7.2Issue-Vorlage
0/6.3PR-Vorlage
Verwendete Eingangsdaten
has_readmeja
has_licenseja
has_contributingja
has_issue_templatenein
has_code_of_conductja
has_pull_request_templatenein
Wie die Bewertung erfolgt
44.1/80Downloads pro Monat — 2.038 Downloads/Monat über packagist
0/20Abhängige in der Registry — 0 Pakete hängen davon ab
Verwendete Eingangsdaten
packagesmiddag-io/moodle
dependents0
ecosystemspackagist
total_downloads2.041
monthly_downloads2.038

Nachhaltigkeit & Governance

Überdauert das Projekt die Menschen, die es tragen — Bus-Faktor, Reaktionsfähigkeit, Trägerschaft und Paketpflege?

54Mittel · 24 % des Gesamtindex
Wie die Bewertung erfolgt
9/54Bus-Faktor — 1 Beitragende decken die Hälfte aller Commits ab
0.1/22.5Commit-Verteilung — wichtigste beitragende Person verfasste 100 % der Commits
2.7/13.5Breite der Beitragenden — 2 Beitragende
6/10OpenSSF Scorecard: Contributors — project has 2 contributing companies or organizations -- score normalized to 6
Verwendete Eingangsdaten
bus_factor1
contributors_sampled2
top_contributor_share0,996
Wie die Bewertung erfolgt
37.4/46.8Issue-Lösungsquote — 80 % der Issues geschlossen
38.2/38.3PR-Annahme — 47/47 entschiedene PRs gemergt
0/15OpenSSF Scorecard: Code-Review — Found 1/25 approved changesets -- score normalized to 0
Verwendete Eingangsdaten
merged_prs47
open_issues1
closed_issues4
issue_closed_ratio0,8
closed_unmerged_prs0
Wie die Bewertung erfolgt
30/30Organisatorische Trägerschaft — im Besitz einer Organisation
0/20Verifizierte Domain
0/25Reichweite des Inhabers — 0 Follower von middag-io
8.7/25Kontohistorie — 12 öffentliche Repos, Kontoalter ca. 0 Jahre
Verwendete Eingangsdaten
followers0
owner_typeOrganization
is_verified
owner_loginmiddag-io
public_repos12
account_age_days115

Paketpflege

100Exzellent
Wie die Bewertung erfolgt
25/25Veröffentlicht & auflösbar — 1 Paket(e) auf packagist
35/35Veröffentlichungsaktualität — letzte Veröffentlichung vor 1 Tagen
20/20Versionshistorie — 22 veröffentlichte Versionen
20/20Nicht veraltet — aktiv, nicht veraltet oder zurückgezogen
Verwendete Eingangsdaten
packagesmiddag-io/moodle
ecosystemspackagist
any_deprecatednein
min_days_since_publish1

Engineering-Qualität

Sind grundlegende Engineering- und Dokumentationspraktiken vorhanden?

88Exzellent · 20 % des Gesamtindex
Wie die Bewertung erfolgt
24/24CI-Workflows — 2 Workflow(s)
24/24Tests vorhanden
16/16Linter-Konfiguration — .php-cs-fixer.dist.php
0/9.6Pre-Commit-Hooks
0/6.4.editorconfig
16/20OpenSSF Scorecard: CI-Tests — 6 out of 7 merged PRs checked by a CI test -- score normalized to 8
Verwendete Eingangsdaten
has_cija
has_testsja
has_editorconfignein
has_linter_configja
has_precommit_confignein

Dokumentation

100Exzellent
Wie die Bewertung erfolgt
30/30README
25/25Dokumentationsverzeichnis
15/15Dokumentations-/Homepage-Site — https://docs.middag.dev
10/10Repository-Beschreibung
10/10Topics — 4 Topics
10/10Wiki
Verwendete Eingangsdaten
topicsclient-middag, platform-php, type-library, status-active
has_wikija
homepagehttps://docs.middag.dev
has_readmeja
has_docs_dirja
has_descriptionja

Sicherheit

Sind die sichtbaren Sicherheits- und Lieferkettenpraktiken belastbar, ohne ungeklärte Exposition gegenüber Hochrisikojurisdiktionen?

33Gefährdet · 16 % des Gesamtindex

Sicherheitslage

33Gefährdet
Wie die Bewertung erfolgt
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 — keine Daten
0/7.5Dependency-Update-Tool — no update tool detected
0/5Fuzzing — project is not fuzzed
2.5/2.5Lizenz — license file detected
0/7.5Maintained — project was created within the last 90 days. Please review its contents carefully
0/5Packaging — keine Daten
0/5Pinned-Dependencies — keine Daten
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 — keine Daten
0/7.5Token-Permissions — keine Daten
7.5/7.5Vulnerabilities — 0 existing vulnerabilities detected
Verwendete Eingangsdaten
sourceopenssf_scorecard
checks_evaluated13
scorecard_versionv5.5.0
checks_inconclusive5
scorecard_aggregate3,3
Von der Bewertung ausgeschlossen (keine Daten oder nicht anwendbar): dangerous_workflow, packaging, pinned_dependencies, signed_releases, token_permissions. Die verbleibenden Gewichte wurden renormalisiert.

AI Readiness

Wie gut ist das Repository dafür ausgestattet, mit KI-Coding-Agenten entwickelt und gepflegt zu werden? Ein unabhängiges, experimentelles Badge — Gewicht 0,0, es wird eigenständig ausgewiesen und verändert den Gesamt-Gesundheitswert nicht.

57Mittel · 0 % des Gesamtindex
Wie die Bewertung erfolgt
45/45Agentenanweisungen — CLAUDE.md
0/15Maschinenlesbare Doku (llms.txt)
40/40Lesbare Commit-Historie — 94 von 94 menschlichen Commits benennen ihre Absicht (strukturierter Betreff oder erläuternder Text)
Verwendete Eingangsdaten
has_llms_txtnein
legible_history_share1
agent_instruction_filesCLAUDE.md
agent_instruction_max_bytes13.720
Wie die Bewertung erfolgt
0/18Bootstrap mit einem Befehl
22/22Automatisierte Tests
11/11Lint-/Format-Konfiguration — .php-cs-fixer.dist.php
0/11Statische Typprüfung
0/10Reproduzierbare Umgebung
0/10Belegte Agentenpraxis — keine von Agenten verfassten Commits unter den letzten 100
0/8Automatisierte Wartung — keine automatisierten Abhängigkeits-Updates beobachtet
0/10OpenSSF Scorecard: Pinned-Dependencies — keine Daten
Verwendete Eingangsdaten
has_nixnein
has_testsja
lockfiles
has_dockerfilenein
typed_languagenein
bootstrap_files
has_devcontainernein
has_linter_configja
typecheck_configs
agent_commit_share0
toolchain_manifests
dependency_bot_commit_share0
Von der Bewertung ausgeschlossen (keine Daten oder nicht anwendbar): OpenSSF Scorecard: Pinned-Dependencies. Die verbleibenden Gewichte wurden renormalisiert.
Wie die Bewertung erfolgt
0/45Typprüfbarer Code — PHP ohne Typprüfungs-Konfiguration
54.9/55Handhabbare Dateigrößen — 1/479 Quelldateien über 60 KB
Verwendete Eingangsdaten
primary_languagePHP
largest_source_bytes61.160
source_files_sampled479
oversized_source_files1

Eckdaten

0GitHub-Sterne
2Mitwirkende
254Commits, letzte 12 Monate
0Tage seit letztem Push
22Releases
1Bus-Faktor
1offene Issues
PackagistPaket-Ökosysteme

Warnungen zur Datenerhebung

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

Weitere Details

OpenSSF Scorecard 3.3 / 10
3.3Gesamtwert

Unabhängige, werkzeugneutrale Sicherheitsbewertung durch das quelloffene OpenSSF Scorecard. Jede Prüfung honoriert eine Sicherheits-Praxis, nicht das Werkzeug eines bestimmten Anbieters. Prüfungen, die Scorecard nicht ermitteln konnte, sind mit k. A. markiert und vom Sicherheitswert ausgeschlossen (nie als null gezählt).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
k. A.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
k. A.Packagingpackaging workflow not detected
k. A.Pinned-Dependenciesno dependencies found
0SASTSAST tool is not run on all commits -- score normalized to 0
0Security-Policysecurity policy file not detected
k. A.Signed-Releasesno releases found
k. A.Token-PermissionsNo tokens found
10Vulnerabilities0 existing vulnerabilities detected
Direkte Abhängigkeiten 12
RegistryPaketVersionsvorgabeManifest
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
Alle Abhängigkeiten nicht erhoben

Der aufgelöste Abhängigkeitssatz konnte für diesen Bericht nicht erhoben werden: GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository

JSON-Rohbericht maschinenlesbar
{
  "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"
}

Bewertungen sind Signale, keine Garantien. Sie spiegeln öffentlich sichtbare Praxis auf GitHub wider — kein Code-Audit und keine Sicherheitsgarantie.

Fehlende Daten werden ausgeschlossen und die Gewichte neu normiert, nie als null bewertet. Die Methodik ist versioniert und offen: Metriken v1.13.0, Schema v0.27.0 — vollständige Methodik · Metriken-Wiki.

Wie ein einzelnes Ergebnis im Gesamtregister steht: aggregierte StatistikenPackagist.