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