Health Report: discourse

2026-02-01T11:19:01.309090Z 6m history
84Health Score
Stable
0.0 pts/mo
11638
Files Analyzed
662
Hotspot Files
49
Critical Issues
2.4%
Duplication

Quick Insights

Key findings from the analysis - prioritized by impact

Technical Debt Hotspot

Found 49 critical and 77 high-priority debt items. These are areas where the team has explicitly noted problems. Consider scheduling time to address the most impactful items.

Low Cohesion Classes

Detected 47 classes with low cohesion (LCOM > 50). These classes may be doing too many unrelated things. Consider splitting them into more focused components.

Active Development

28242 commits across 15222 files.

Healthy Codebase

Overall health score of 84 indicates a well-maintained codebase. Continue current practices and address issues as they arise.

Hotspots

Files with both high complexity and frequent changes - highest risk for bugs

662 hotspots detected (141 critical, 191 high). Two areas dominate: plugins/discourse-ai (140 hotspots, 39 critical) and frontend/discourse (152 hotspots, 35 critical), together accounting for 44% of all hotspots and 52% of criticals. Within discourse-ai, the lib/completions subsystem is the worst cluster (22 hotspots, 13 critical), indicating the LLM integration layer is rapidly accruing debt. The core app/models layer has 30 hotspots with 6 files scoring above 0.96 -- User, Topic, Post, Group, Category, and Reviewable are classic god models with extreme complexity percentiles (all 99.5+). The lib/ directory averages the highest score (0.712) of any area, driven by long-lived utility files like site_setting_extension.rb, search.rb, post_destroyer.rb, and post_revisor.rb that are both highly complex and frequently changed. Several JavaScript files exhibit exceptionally high average cyclomatic complexity (lightbox.js at 54.0, chat-sidebar.js at 46.3, extend-for-assigns.js at 21.1), suggesting monolithic functions that should be decomposed. The top 10 hotspots all score above 0.975, indicating files that are simultaneously in the top 1% for both complexity and churn -- these are the highest-leverage refactoring targets in the codebase.

662
Hotspot Files
1.0
Highest Risk
0.65
Average Risk

Risk Landscape

Files in the upper-right are both complex and frequently changed -- the most likely sources of bugs. Bubble size reflects overall risk score. Green is low risk, yellow moderate, red high.

FileLangRiskChangesComplexity

Low Cohesion Classes

Classes doing too many unrelated things - candidates for splitting

Cohesion is the second-weakest component at 55/100 and is declining. It dropped from 57 to 56 in mid-November 2025, then from 56 to 55 in early January 2026. Across 10,580 analyzed classes, 6,709 (63%) have low cohesion. The worst offenders by LCOM are User (222), Plugin::Instance (145), TopicController (135), Topic (119), and BulkImport::Base (108). These are god-class patterns where a single class handles too many unrelated responsibilities. The User model alone has CBO of 126 and WMC of 405. Trend slope is -0.09 with 0.90 correlation, making this the most reliably declining component. The November drop coincides with SelectKit modernization and new feature additions; the January drop coincides with the Rewind feature and tag group changes.

ClassFileLanguageLCOMWMCCBO

Architectural Smells

Structural problems that make the codebase harder to change safely

Across 11,638 components, 97 architectural smells were detected: 29 critical, 49 high, and 19 medium severity. The breakdown is 49 hub-like dependencies, 27 cyclic dependencies, 19 unstable dependency violations, and 2 central connectors.

CYCLIC DEPENDENCIES (27 total, highest priority):

The most dangerous cycle spans 13 components in the frontend core, linking get-url.js, source-identifier.js, user-presence.js, the discourse route, transformer.js, get-owner.js, ajax-error.js, preferences/tracking.js, prosemirror glimmer-node-view.js, prosemirror plugin.js, config/application.rb, config/environment.rb, and deprecated.js. This mega-cycle crosses the frontend/backend boundary (config/application.rb and config/environment.rb appear alongside JS modules), which suggests the dependency analysis is tracing build-time or configuration edges alongside runtime imports. Regardless, the frontend portion of this cycle -- particularly the chain through get-url.js, ajax-error.js, transformer.js, and the discourse route -- represents a tightly coupled core that is difficult to test or refactor in isolation. Breaking this cycle should start by extracting shared primitives (get-url, get-owner, deprecated) into a leaf utility layer with zero inward dependencies on application-level modules.

Three additional multi-component cycles deserve attention: (1) lib/content_security_policy/default.rb -> builder.rb -> content_security_policy.rb (length 3) -- a Ruby-side cycle that can be broken by having builder.rb accept a configuration object rather than importing the parent module; (2) session.js -> ajax.js -> rest.js (length 3) -- the classic circular auth/network problem where the session model depends on the AJAX layer which depends on the REST adapter which depends back on session; extract an auth-token provider that both session and ajax consume; (3) test helpers forming a cycle between notifications-tracking-assertions.js, form-kit-assertions.js, and qunit-helpers.js -- resolve by having each assertion module import qunit-helpers directly without qunit-helpers importing them back (register assertions via a plugin/registration pattern instead).

The remaining 20 cycles are self-referential (cycle length 1), indicating modules that import themselves or have internal circular references. These are low risk individually but include notable modules like the composer controller and several instance-initializers.

HUB MODULES (49 total):

The top three hubs by fan-in are pure dependency sinks: object.js (524 dependents), lib/tasks/qunit.rake (497 dependents), and lib/service.rb (471 dependents). These have zero or near-zero fan-out, making them maximally stable (instability = 0.0). They are low-risk hubs because changes to them are rare and they do not propagate dependency chains outward.

The more concerning hubs are those with both high fan-in and meaningful fan-out, as changes to them ripple in both directions: discourse.js route (273 in, 3 out), ajax.js (239 in, 6 out), decorators.js (233 in, 10 out), and utilities.js (113 in, 7 out). decorators.js is particularly risky with 10 outgoing dependencies -- any change to those 10 upstream modules can destabilize 233 downstream consumers.

On the Ruby side, User (60 dependents), Category (52), Topic (49), Site (43), and Bookmark (22) are hub models. These are typical for a Rails application of this size and represent inherent domain complexity rather than a design flaw.

CENTRAL CONNECTORS (2, critical severity):

qunit-helpers.js is the worst offender: 328 fan-in, 61 fan-out, acting as a god module for the test infrastructure. It is also involved in a cyclic dependency and 14 of the 19 unstable-dependency violations. This single file is the primary source of architectural coupling in the test layer. It should be decomposed into focused test-setup modules (e.g., separate modules for pretender setup, site settings stubs, user session stubs, component rendering helpers) so that individual test files only import what they need.

select-kit.js (55 fan-in, 11 fan-out) is the second central connector. As the base component for all select-kit dropdowns, this is somewhat expected, but the 11 outgoing dependencies could be reduced by injecting configuration rather than importing it.

UNSTABLE DEPENDENCY VIOLATIONS (19 total):

14 of the 19 violations originate from qunit-helpers.js depending on unstable application modules (notification-types-manager, sidebar components, card-container, post-stream, nav-item, and others). This is a direct consequence of its god-module nature -- it pre-registers or stubs too many application concerns. The remaining violations are: config/environment.rb depending on the unstable config/application.rb, utilities.js depending on the unstable capabilities service, component-test.js depending on unstable topic-tracking, and the discourse-ai streamer cycle.

PRIORITIZED REFACTORING ACTIONS:

  1. Break the 13-component mega-cycle by extracting get-url, get-owner, and deprecated into a zero-dependency utility package that other frontend modules import without creating back-edges.
  2. Decompose qunit-helpers.js (328 fan-in, 61 fan-out) into focused test-setup modules to eliminate it as a central connector and resolve 14 unstable-dependency violations simultaneously.
  3. Break the session.js -> ajax.js -> rest.js cycle by extracting an auth-token provider module.
  4. Break the content_security_policy Ruby cycle by passing configuration as data rather than importing the parent module.
  5. Reduce decorators.js fan-out (10 outgoing dependencies) to limit the blast radius of upstream changes affecting its 233 consumers.
97
Total Smells
27
Cyclic Dependencies
49
Hub Dependencies
19
Unstable Dependencies
TypeSeverityFiles InvolvedSuggestion
Cyclic DependencyCriticallib/content_security_policy/default.rb, lib/content_security_policy/builder.rb, lib/content_security_policy.rbBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../tend/discourse/app/helpers/get-url.js, .../iscourse/app/lib/source-identifier.js, .../nd/discourse/app/lib/user-presence.js, .../end/discourse/app/routes/discourse.js, .../tend/discourse/app/lib/transformer.js, frontend/discourse/app/lib/get-owner.js, frontend/discourse/app/lib/ajax-error.js, .../p/controllers/preferences/tracking.js, ...//prosemirror/lib/glimmer-node-view.js, .../app/static/prosemirror/core/plugin.js, config/application.rb, config/environment.rb, frontend/discourse/app/lib/deprecated.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCriticalfrontend/discourse/app/models/session.js, frontend/discourse/app/lib/ajax.js, frontend/discourse/app/adapters/rest.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCriticallib/distributed_cache.rbBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../static/prosemirror/lib/markdown-it.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../e/app/instance-initializers/mobile.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCriticalfrontend/asset-processor/postcss.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../discourse/app/controllers/composer.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../e/app/instance-initializers/logout.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../nce-initializers/codeblock-buttons.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../stance-initializers/narrow-desktop.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../itializers/register-service-worker.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../ourse/app/lib/uppy-checksum-plugin.js, .../discourse/app/lib/uppy/uppy-upload.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../iscourse/app/lib/download-calendar.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../scourse/app/routes/forgot-password.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCriticalfrontend/discourse/app/routes/wizard.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical...//notifications-tracking-assertions.js, ...//tests/helpers/form-kit-assertions.js, .../course/tests/helpers/qunit-helpers.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCriticallib/compression/zip.rbBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCriticallib/tasks/annotate_rb.rakeBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCriticalmigrations/spec/rails_helper.rbBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../scourse/models/chat-direct-message.js, .../ipts/discourse/models/chat-channel.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical...//ai-streamer/updaters/post-updater.js, ...//lib/ai-streamer/progress-handlers.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../e/initializers/event-relative-date.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../alizers/discourse_chat_integration.rbBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../se/models/gamification-leaderboard.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCritical.../vascripts/discourse/lib/user-notes.jsBreak the cycle by introducing an interface or restructuring the dependency direction
Cyclic DependencyCriticalscript/import_scripts/nodebb/mongo.rbBreak the cycle by introducing an interface or restructuring the dependency direction
Central ConnectorCritical.../course/tests/helpers/qunit-helpers.jsDecompose into smaller components with single responsibility; extract interfaces for consumers
Central ConnectorCritical.../e/select-kit/components/select-kit.jsDecompose into smaller components with single responsibility; extract interfaces for consumers
Hub DependencyHighfrontend/discourse/app/lib/debounce.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../ourse/tests/helpers/component-test.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/ajax-error.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighapp/jobs/base.rbConsider splitting this component into smaller, more focused modules
Hub DependencyHighlib/tasks/qunit.rakeConsider splitting this component into smaller, more focused modules
Hub DependencyHighapp/models/topic.rbConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../d/discourse-markdown-it/src/engine.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/helpers.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../nd/discourse/app/lib/preload-store.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/decorators.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighapp/models/bookmark.rbConsider splitting this component into smaller, more focused modules
Hub DependencyHighlib/service.rbConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../nd/discourse/app/lib/tracked-tools.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/deprecated.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../tend/discourse/app/lib/transformer.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/api.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../se/tests/helpers/select-kit-helper.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../p/controllers/preferences/tracking.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/object.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighapp/models/user.rbConsider splitting this component into smaller, more focused modules
Hub DependencyHighlib/theme_settings_manager/string.rbConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../discourse/app/controllers/composer.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../app/lib/notification-types-manager.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/get-owner.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/adapters/rest.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../end/discourse/app/lib/icon-library.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/computed.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/ajax.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../iscourse/admin/models/site-setting.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/utilities.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../end/discourse/app/routes/discourse.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighapp/models/category.rbConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../d/discourse/app/models/user-action.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../course/app/lib/implicit-injections.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighconfig/environment.rbConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/url.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../outes/build-private-messages-route.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../tend/discourse/admin/lib/constants.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../iscourse/select-kit/lib/plugin-api.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../tend/discourse/app/lib/array-tools.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../tend/discourse/app/helpers/get-url.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighlib/onebox/engine/json.rbConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/later.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/formatter.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../rse/tests/helpers/create-pretender.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../ipts/discourse/models/chat-channel.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighapp/models/site.rbConsider splitting this component into smaller, more focused modules
Hub DependencyHigh.../ntrollers/admin-area-settings-base.jsConsider splitting this component into smaller, more focused modules
Hub DependencyHighfrontend/discourse/app/lib/text.jsConsider splitting this component into smaller, more focused modules
Unstable DependencyMediumconfig/environment.rb, config/application.rbIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMediumfrontend/discourse/app/lib/utilities.js, .../iscourse/app/services/capabilities.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../ourse/tests/helpers/component-test.js, .../se/app/models/topic-tracking-state.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, ...//app/components/card-contents-base.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, .../iscourse/app/components/d-document.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, .../tend/discourse/app/services/header.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, .../se/app/components/plugin-connector.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, ...//controllers/user-private-messages.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, .../nstance-initializers/auth-complete.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, .../nd/discourse/app/lib/link-mentions.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, .../app/lib/notification-types-manager.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, .../urse/app/lib/sidebar/admin-sidebar.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, .../se/app/lib/sidebar/custom-sections.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, .../tend/discourse/app/models/nav-item.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, .../d/discourse/app/models/post-stream.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, .../ct-kit/components/composer-actions.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, frontend/discourse/app/services/a11y.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium.../course/tests/helpers/qunit-helpers.js, ...//tests/helpers/form-kit-assertions.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)
Unstable DependencyMedium...//lib/ai-streamer/progress-handlers.js, ...//ai-streamer/updaters/post-updater.jsIntroduce an interface in the stable component that the unstable component implements (Dependency Inversion)

Dependency Structure

Import graph analysis - cycles, bottlenecks, and instability

The dependency graph contains 11,638 nodes and 9,606 edges with a low average degree of 1.65, indicating a relatively sparse graph overall. However, structural risks concentrate in a few areas.

Central files by PageRank: The highest-PageRank file is frontend/discourse/app/lib/deprecated.js (0.00498, 73 dependents, instability 0.039), which is appropriately stable as a utility. frontend/discourse/app/lib/object.js (493 dependents, zero out-degree) and lib/service.rb (471 dependents, zero out-degree) are pure foundations with instability 0.0, which is correct for files this heavily depended upon. app/jobs/base.rb (292 dependents) and app/serializers/application_serializer.rb (231 dependents) are also stable anchors. These top files are well-positioned: high fan-in, near-zero instability.

Bottleneck files by betweenness: config/environment.rb (betweenness 0.00069) and config/application.rb (0.00068) are the top two bottlenecks, sitting on the most shortest paths between modules. config/application.rb is particularly concerning: it has 21 outgoing dependencies (instability 0.91) yet acts as a structural bridge, meaning changes to it ripple widely in both directions. frontend/discourse/tests/helpers/qunit-helpers.js (betweenness 0.00056) is a critical test infrastructure bottleneck with 328 dependents and 61 dependencies, making it both heavily relied upon and fragile to upstream changes. frontend/discourse/app/lib/ajax.js (betweenness 0.00023, 239 dependents) is a communication bottleneck for the frontend networking layer.

Import cycles (11 total, 88 files involved): The most severe is Cycle 9, a 58-file cycle spanning the entire lib/onebox/engine/ directory through lib/onebox/engine.rb and lib/onebox/engine/allowlisted_generic_onebox.rb. This massive cycle means no onebox engine can be tested or deployed independently. Breaking it likely requires extracting the engine registry from engine.rb so individual engines do not circularly depend on the registry that depends on all of them. Other notable cycles: (1) topic.js <-> category.js, a 2-file domain model cycle that likely needs an extracted shared interface or merged module; (2) ajax.js -> rest.js -> session.js -> ajax.js, a networking cycle that complicates mocking in tests; (3) a 6-file cross-boundary cycle mixing frontend JS (transformer.js, tracking.js, prosemirror) with backend Ruby (application.rb, environment.rb), which is unusual and may indicate a tooling artifact rather than a real runtime cycle; (4) chat-channel.js <-> chat-direct-message.js in the chat plugin.

High fan-out fragile files: app/models/report.rb (47 outgoing, 0 incoming, instability 1.0) is the most change-sensitive file in the graph. Any change to its 47 dependencies could break it. frontend/discourse/app/models/user.js (35 out, 0 in), frontend/discourse/app/controllers/topic.js (29 out, 0 in), and frontend/discourse/app/services/composer.js (28 out, 0 in) are similarly fragile leaf nodes. While leaf files with zero fan-in are expected to have high instability, the magnitude of their fan-out (user.js importing 35 modules) makes them disproportionately exposed to breakage from upstream changes.

Key recommendations: (1) The 58-file onebox engine cycle is the single largest structural problem; extract a registry/loader pattern to decouple individual engines from engine.rb. (2) config/application.rb has high betweenness despite high instability, meaning it is both a bottleneck and fragile -- reduce its outgoing dependencies by moving configuration concerns into dedicated initializers. (3) qunit-helpers.js with 61 dependencies is a test utility acting as a God module; splitting it into focused helpers would reduce its fan-out and bottleneck effect. (4) The topic.js <-> category.js cycle is a domain modeling smell; consider whether one model should hold a reference without the reverse dependency.

11638
Modules
9606
Dependencies
1.7
Avg Degree
11
Dependency Cycles

Detected Cycles

Cycle (3 files)
lib/backup_restore/s3_backup_store.rblib/backup_restore/local_backup_store.rblib/backup_restore/backup_store.rblib/backup_restore/s3_backup_store.rb
Cycle (4 files)
frontend/discourse/app/helpers/get-url.jsfrontend/discourse/app/lib/source-identifier.jsfrontend/discourse/app/lib/environment.jsfrontend/discourse/app/lib/deprecated.jsfrontend/discourse/app/helpers/get-url.js
Cycle (3 files)
lib/content_security_policy/default.rblib/content_security_policy/builder.rblib/content_security_policy.rblib/content_security_policy/default.rb
Cycle (6 files)
frontend/discourse/app/lib/transformer.js.../scourse/app/controllers/preferences/tracking.js.../app/static/prosemirror/lib/glimmer-node-view.js.../discourse/app/static/prosemirror/core/plugin.jsconfig/application.rbconfig/environment.rbfrontend/discourse/app/lib/transformer.js
Cycle (3 files)
frontend/discourse/app/lib/ajax.jsfrontend/discourse/app/adapters/rest.jsfrontend/discourse/app/models/session.jsfrontend/discourse/app/lib/ajax.js
Cycle (2 files)
frontend/discourse/app/models/topic.jsfrontend/discourse/app/models/category.jsfrontend/discourse/app/models/topic.js
Cycle (2 files)
frontend/discourse/app/lib/uppy-checksum-plugin.jsfrontend/discourse/app/lib/uppy/uppy-upload.jsfrontend/discourse/app/lib/uppy-checksum-plugin.js
Cycle (3 files)
.../ts/helpers/notifications-tracking-assertions.js...//discourse/tests/helpers/form-kit-assertions.jsfrontend/discourse/tests/helpers/qunit-helpers.js.../ts/helpers/notifications-tracking-assertions.js
Cycle (58 files)
lib/onebox/engine/loom_onebox.rblib/onebox/engine/tiktok_onebox.rblib/onebox/engine/motoko_onebox.rblib/onebox/engine/hackernews_onebox.rblib/onebox/engine/facebook_media_onebox.rblib/onebox/engine/google_drive_onebox.rblib/onebox/engine/reddit_media_onebox.rblib/onebox/engine/kaltura_onebox.rblib/onebox/engine/google_photos_onebox.rblib/onebox/engine/instagram_onebox.rblib/onebox/engine/simplecast_onebox.rblib/onebox/engine/wistia_onebox.rblib/onebox/engine/cloud_app_onebox.rblib/onebox/engine/trello_onebox.rblib/onebox/engine/pdf_onebox.rblib/onebox/engine/five_hundred_px_onebox.rblib/onebox/engine/flickr_shortened_onebox.rblib/onebox/engine/flickr_onebox.rblib/onebox/engine/coub_onebox.rblib/onebox/engine/band_camp_onebox.rblib/onebox/engine/mixcloud_onebox.rblib/onebox/engine/asciinema_onebox.rblib/onebox/engine/replit_onebox.rblib/onebox/engine/audioboom_onebox.rblib/onebox/engine/sketch_fab_onebox.rblib/onebox/engine/steam_store_onebox.rblib/onebox/engine/vimeo_onebox.rblib/onebox/engine/typeform_onebox.rblib/onebox/engine/gfycat_onebox.rblib/onebox/engine/animated_image_onebox.rblib/onebox/engine/xkcd_onebox.rblib/onebox/engine/slides_onebox.rblib/onebox/engine/pastebin_onebox.rblib/onebox/engine/imgur_onebox.rblib/onebox/engine/sound_cloud_onebox.rblib/onebox/engine/pubmed_onebox.rblib/onebox/engine/youku_onebox.rblib/onebox/engine/youtube_onebox.rblib/onebox/engine/wikipedia_onebox.rblib/onebox/engine/wikimedia_onebox.rblib/onebox/engine/twitter_status_onebox.rblib/onebox/engine/stack_exchange_onebox.rblib/onebox/engine/threads_status_onebox.rblib/onebox/engine/audio_onebox.rblib/onebox/engine/video_onebox.rblib/onebox/engine/image_onebox.rblib/onebox/engine/google_play_app_onebox.rblib/onebox/engine/google_maps_onebox.rblib/onebox/engine/google_docs_onebox.rblib/onebox/engine/google_calendar_onebox.rblib/onebox/engine/github_pull_request_onebox.rblib/onebox/engine/github_gist_onebox.rblib/onebox/engine/github_folder_onebox.rblib/onebox/engine/github_commit_onebox.rblib/onebox/engine/github_issue_onebox.rblib/onebox/engine/amazon_onebox.rblib/onebox/engine.rblib/onebox/engine/allowlisted_generic_onebox.rblib/onebox/engine/loom_onebox.rb
Cycle (2 files)
.../ts/javascripts/discourse/models/chat-channel.js.../scripts/discourse/models/chat-direct-message.js.../ts/javascripts/discourse/models/chat-channel.js
Cycle (2 files)
.../course/lib/ai-streamer/updaters/post-updater.js...//discourse/lib/ai-streamer/progress-handlers.js.../course/lib/ai-streamer/updaters/post-updater.js
FileLangPageRankBetweennessInOutInstability

Known Issues

Problems the team has documented but not yet fixed (TODO, FIXME, HACK)

622 SATD markers across the codebase (density 0.58 per 1K lines). The majority (271) are low-severity requirement TODOs that represent feature wishes rather than real risk. The actionable debt falls into four buckets: (1) Security-adjacent markers in controllers and auth code -- the path traversal concern in static_controller.rb and the mini_profiler security notes demand periodic audit since they guard live attack surfaces. (2) A cluster of 20+ FIXME markers in the vendored holidays library (discourse-calendar plugin) indicating a borrowed dependency with known structural defects that will resist upstream fixes. (3) Over 20 dated TODO removal comments that have long passed their stated deadlines (some from 2019-2022), meaning the context for why they exist is eroding. These should be resolved or the comments rewritten with current rationale. (4) Genuine FIXME/BUG markers in core services (backup restorer atomicity, upload_creator size checks, search scope leaks) that describe known defects left unpatched. The bulk importer and import_scripts directories are heavily annotated but are offline tooling with lower blast radius. Most SECURITY-tagged items are informational annotations (documenting why something is safe), not open vulnerabilities -- but the static_controller path traversal comment and the mini_profiler multi-tenant note are exceptions that warrant verification.

622
Total Issues
49
Critical
77
High
116
Medium

By Severity

By Category

SeverityCategoryFileLangLineComment

Duplicated Code

Code that appears in multiple places - bugs fixed in one spot may need fixing elsewhere

The codebase has 358 clone groups totaling 22,998 duplicated lines across 963,681 scanned lines (2.4% duplication ratio). Average similarity is 0.94, with type1 (exact) and type2 (parameterized) clones dominating (3,262 of 3,464 total clones). While 2.4% is low overall, the duplication that exists is highly concentrated and structurally significant.

Highest-impact production clone: admin plugin configuration nav initializers (Group 249, 576 lines, 32 plugins). Every plugin contains a near-identical 18-line initializer (e.g., adplugin-admin-plugin-configuration-nav.js) that checks for admin, then calls api.setAdminPluginIcon(). This is the single largest clone group by file count and a textbook case of a missing framework-level declaration. A declarative plugin manifest or a single line in each plugin's metadata could replace all 32 copies. Estimated elimination: ~540 lines.

Chat subscription managers (Group 171, 84 lines, severity hotspot #1 and #2). chat-channel-subscription-manager.js and chat-channel-thread-subscription-manager.js share a nearly identical onMessage switch/case dispatcher (45 and 39 lines respectively). The channel version has two extra cases (update_thread_original_message, notice) but the core dispatch pattern is identical. Extract a shared base class ChatMessageBusSubscriptionManager with the common switch cases and handler delegation pattern. Both files are flagged as the top two duplication hotspots (severity 11.6 and 11.5). Estimated elimination: ~35 lines.

discourse-ai REST adapters (Group 50, 105 lines, 5 files). Five adapter files (ai-embedding.js, ai-feature.js, ai-llm.js, ai-persona.js, ai-tool.js) are identical except for the apiNameFor() return value. Extract a factory function or parameterized base class. Estimated elimination: ~80 lines.

IntermediateDB entity modules (Groups 43, 82, 305, 240 -- combined ~455 lines). The migrations/lib/database/intermediate_db/ directory contains many auto-generated modules (category_custom_field.rb, topic_tag.rb, tag_group_permission.rb, etc.) that follow an identical pattern: define an INSERT SQL constant, then a self.create method that calls IntermediateDB.insert. These are auto-generated from schema config, so the duplication is an intentional code generation tradeoff. No action needed unless the generator itself should be simplified.

Trust-level-to-group migration pattern (Groups 27 and 225, ~800 lines, 18+ files spanning core and plugins). Database migrations like fill_*_allowed_groups_based_on_deprecated_settings.rb and migrate_tl_to_group_settings_*.rb repeat the same migration logic with different setting names. These are frozen migrations and cannot be refactored retroactively, but for future similar migrations, extract a reusable migration helper (e.g., MigrateTrustLevelToGroupSetting) that accepts the setting name as a parameter.

Drop-column migration boilerplate (Group 315, 335 lines, 26 files across core and plugins). Migrations that drop columns follow an identical ~13-line pattern. Same note as above: frozen migrations, but a shared migration concern would prevent future repetition.

Cross-plugin admin nav configuration (Group 308, 106 lines, 4 plugins). A second variant of admin plugin nav setup exists in automation, chat, discourse-ai, and calendar plugins with slightly more complex logic than Group 249. These could share a utility if the framework-level solution for Group 249 also supports extended configuration.

Sidebar section links (Groups 353 and 340, ~188 lines). Four sidebar section link classes in frontend/discourse/app/lib/sidebar/ follow the same structural pattern with different display values. Extract a configurable base or factory.

Vendor holiday definitions (Groups 317, 208, 336 -- ~3,483 lines). The discourse-calendar plugin's vendored holidays library contains massive duplication across country-specific test and definition files. This is third-party vendored code and not directly actionable, but it accounts for the three largest clone groups by line count and inflates the overall duplication metric.

Key recommendations ranked by impact:

  1. Replace the 32-plugin admin-plugin-configuration-nav initializer pattern with a declarative plugin API (~540 lines saved).
  2. Extract a shared base class for chat channel/thread subscription managers (~35 lines saved, addresses top severity hotspots).
  3. Create a parameterized adapter factory for discourse-ai REST adapters (~80 lines saved).
  4. For future migrations, establish reusable helpers for trust-level-to-group and drop-column patterns to prevent further accumulation.
  5. Extract sidebar section link factory to reduce the boilerplate pattern across link classes (~120 lines saved).
2.4%
Duplication Rate
358
Clone Groups
22998
Duplicate Lines
963681
Total Lines

Duplication Level

Technical Debt Gradient

Per-file debt scores - prioritize cleanup where it matters most

The Discourse codebase is in strong overall health: average grade A, with 88.2% of 11,638 files at A or A-minus. Only 59 files (0.5%) fall at D or F, and 109 files (0.9%) sit at C-tier. Debt is not systemic -- it is concentrated in a small tail.

Grade distribution: A-tier: 10,270 (88.2%), B-tier: 1,200 (10.3%), C-tier: 109 (0.9%), D-tier: 5 (0.04%), F-tier: 54 (0.5%).

All 54 F-graded files score total=0.0 because they contain critical defects, which zeroes out the score regardless of other dimensions. Every file with critical defects is F-graded, and every F-graded file has critical defects. This is the single most important finding: the F tier is entirely defect-driven, not complexity-driven.

Highest-defect files requiring immediate attention: db/fixtures/006_badges.rb (27 critical defects), lib/pretty_text.rb (13 defects), spec/lib/discourse_redis_spec.rb (8 defects), spec/requests/extra_locales_controller_spec.rb (8 defects), plugins/discourse-ai/lib/personas/tool_runner.rb (7 defects), script/assemble_ember_build.rb (6 defects), and script/publish_built_assets.rb (5 defects). These seven files alone account for 74 of the 124 total critical defects across the codebase.

The 5 D-graded files are all JavaScript and share a common pattern: low structural complexity scores (all at 12.5/20) combined with low hotspot scores (near 0), indicating large, frequently-changing files. frontend/discourse/app/controllers/topic.js and frontend/discourse/app/services/composer.js are classic God-object controllers -- high coupling, low structure, and constant churn. plugins/chat/assets/javascripts/discourse/initializers/chat-sidebar.js compounds structural weakness with low semantic complexity and duplication. frontend/discourse/tests/fixtures/user-fixtures.js is dragged down by extreme duplication (3.1/15).

Among B-graded files (the 1,200-file middle tier), the dominant weakness is duplication (664 files, 55% of B-tier), followed by hotspot activity (434 files, 36%). Structural complexity is a distant third at 190 files. Coupling and temporal coupling are rarely weak. This means the primary path from B to A across the codebase is reducing code duplication.

Debt by area: tests carry the most non-A files (706, driven by duplication in spec files), followed by plugins (245), core app/lib/config (204), and frontend (158). Scripts have only 41 non-A files but 7 of them are F-graded, the highest F-rate of any area (4.6%). Ruby files dominate the non-A population (971 of 1,368 non-A files, 71%), reflecting the larger Ruby portion of the codebase.

Key actions: (1) Triage the 54 F-graded files for critical defects -- especially db/fixtures/006_badges.rb with 27 defects and lib/pretty_text.rb with 13 defects. These likely contain unsafe operations or missing error handling. (2) Break up the D-graded JavaScript controllers (topic.js, composer.js) which are large, structurally weak, and heavily churned. (3) Address duplication in test fixtures and spec files, which is the single largest source of B-tier scores. (4) Review build scripts under script/ for safety issues, as they have a disproportionate F-rate relative to their small file count.

11638
Files Scored
93.0
Average Score
A
Average Grade
54
Failing Files

Grade Distribution

FileLangScoreGradeStructuralSemanticDuplicationCoupling

Change Activity

How frequently code is being modified

Over the 180-day period, 15,222 files were touched across 28,242 file-level changes, but churn is broadly distributed: the top 10 files account for only 9.7% of all changed lines, and 75.7% of files were touched exactly once, indicating most changes are one-off rather than sustained rework. Churn concentration is low compared to typical projects. The highest-churn source files fall into three clear patterns. (1) Active feature development: lightbox.js (20 commits, relative churn 2.82) was being rebuilt with a new PhotoSwipe-based implementation, accumulating bug fixes and UX polish -- expected churn for a feature rewrite. The reviewable-refresh/item.gjs component (19 commits, relative churn 0.47) shows similar feature-build churn as the review queue was overhauled. chat-sidebar.js (14 commits, relative churn 1.34) reflects a steady stream of new chat features (starred channels, emoji, hover menus). (2) Infrastructure evolution: site_setting_extension.rb (20 commits, relative churn 0.28, active 170 of 180 days) is the most sustained churn in the codebase, driven by the 'upcoming changes' framework for deprecating and promoting site settings -- a deliberate, phased infrastructure change rather than instability. config/site_settings.yml (84 commits, active 177 days) follows the same pattern, with settings being added, deprecated, or migrated continuously. (3) Potential concern areas: topic.gjs (7 commits, relative churn 2.31 -- meaning the file was rewritten more than twice over) and composer-test.js (6 commits, relative churn 2.07) show high rewrite ratios. topic.gjs churn comes from the widget-system purge and template modernization, which is intentional refactoring. By directory, frontend/discourse (96,640 changed lines), plugins/discourse-ai (53,461), and app/assets (47,706) are the three largest churn centers. Plugins account for 41.4% of all source churn, with discourse-ai, discourse-math, chat, and discourse-calendar as the top contributors. The discourse-ai plugin in particular shows broad, sustained churn (755 files, 1,672 commits) consistent with rapid feature development. Overall, the churn profile is healthy: most high-churn files are explained by intentional feature builds or planned refactoring (widget removal, frontend directory rename, upcoming-changes framework), not by repeated bug-fix cycles indicating design problems.

28242
Total Changes
15222
Files Changed
356330
Lines Added
207286
Lines Deleted
FileLangChangesContributorsChurn Score

Code Ownership

How knowledge is distributed - concentrated knowledge is a risk

Of 11,638 analyzed files, 4,341 (37%) have a bus factor of 1 and 6,536 (56%) are rated high risk. Jarek Radosz is the sole owner of 1,807 files, overwhelmingly concentrated in plugins (discourse-ai: 470, discourse-calendar: 365, discourse-assign: 115, and 12+ more plugins). If Jarek becomes unavailable, knowledge of large swaths of the plugin ecosystem is lost with no backup. Beyond plugins, critical core paths have 262 silo files: 124 in lib/, 60 in app/services/, 52 in app/models/, and 19 in app/controllers/. On the opposite end, core model and controller files like user.rb (70 contributors), topic.rb (59), and users_controller.rb (65) suffer from fragmented ownership where no single person owns more than 26%, increasing defect risk due to diffused responsibility. Auth-related code has notable silos in oauth2 and openid-connect plugins (Jarek) and guardian specs (Ted Johansson, Natalie Tay). The migrations subsystem is almost entirely siloed to Gerhard Schlager. Recommended actions: (1) pair programming rotations on Jarek's plugin code, especially discourse-ai and discourse-calendar, (2) assign secondary reviewers to all silo files in app/services/ and lib/, (3) establish clear ownership for fragmented core files like user.rb and topic.rb, (4) cross-train on auth plugins and the migrations framework.

6
Bus Factor
4341
Knowledge Silos
11638
Total Files
37.3%
Files at Risk

Top Contributors

Temporal Coupling

Files that frequently change together - reveals hidden dependencies not in the import graph

Of 789 temporal couplings, 772 (98%) are locale file pairs from batch translation commits -- these are expected and not actionable. The 17 non-locale couplings reveal three findings worth attention.

  1. FormKit color control cluster (cross-boundary, strength 0.60-0.80): The stylesheet app/assets/stylesheets/common/form-kit/_control-color.scss is tightly coupled to three frontend files -- fk/control/color.gjs, fk/control-wrapper.gjs, and admin/templates/edit-category/tabs.gjs. This is a cross-boundary coupling between the legacy asset pipeline (app/assets/stylesheets/) and the new frontend directory. Four files across two directory trees change together in nearly every commit. Consider co-locating the color control SCSS with its component in the frontend tree, or at minimum documenting the dependency so it is not missed during refactors.

  2. discourse-ai preamble.js and tool_runner.rb (strength 1.0, 3/3 commits): A JavaScript preamble file and a Ruby runner class change in perfect lockstep. This is expected for a JS runtime embedded in Ruby, but the 1.0 coupling means any change to the JS contract requires a Ruby change. If the interface between them grows, extract a shared schema or version the protocol to allow independent evolution.

  3. site_setting_extension.rb and upcoming_changes.rb (strength 0.43, 3/7 commits): These lib/ files are coupled because upcoming_changes depends on site setting deprecation behavior. This coupling was recently addressed in commit b8672b3dfb which moved upcoming change tracking out of initializers. If the coupling persists after that refactor, consider making the dependency explicit through injection rather than implicit shared knowledge of setting lifecycle.

The config/locales/server.en.yml and config/site_settings.yml pair (strength 0.52, 11 co-changes) is expected and healthy -- adding a site setting requires adding its translation. No action needed there. The package.json/pnpm-lock.yaml coupling (strength 0.64, 14 co-changes) is similarly mechanical and expected.

789
Coupled Pairs
15
Strong Couplings
0.32
Avg Strength
1.0
Max Strength
File AFile BCo-changesStrength

Score Breakdown

How the health score is calculated from individual components

Complexity25%
95
Duplication20%
95
Cohesion15%
56
Debt Gradient15%
93
Known Debt10%
95
Coupling10%
43
Code Smells5%
95

Glossary of Terms

Health Score

A weighted composite metric (0-100) measuring overall code quality. Above 80 is good, 60-80 needs attention, below 60 is concerning.

Hotspot

A file that is both complex AND frequently changed. These are the highest-risk areas because they're hard to change safely but get changed often.

Bus Factor

The minimum number of people who would need to leave before critical knowledge is lost. Higher is better; 1 is a serious risk.

Knowledge Silo

A file that only one person has ever touched. If that person leaves, the knowledge goes with them.

Cyclomatic Complexity

The number of independent paths through code. More paths means more test cases needed and more ways things can go wrong.

Cognitive Complexity

How hard code is to understand. Accounts for nesting, breaks in flow, and things that make humans struggle to follow the logic.

LCOM (Cohesion)

How well a class's methods work together. High LCOM means methods don't share data - the class is probably doing too many unrelated things.

CBO (Coupling)

How many other classes a class depends on. High coupling means changes ripple through the codebase - everything is connected to everything.

Churn Rate

How frequently a file changes. High churn indicates instability - the code may be unclear, have bugs, or be undergoing active development.

Self-Admitted Technical Debt (SATD)

Code issues the team has documented (TODO, FIXME, HACK, XXX). These represent known shortcuts that need eventual attention.

Code Clone / Duplicate

Similar code appearing in multiple places. When a bug is fixed in one spot, it may need fixing elsewhere. Candidates for refactoring into shared functions.

Technical Debt Gradient

Per-file quality score (0-100) combining complexity, duplication, coupling, and other factors. Lower scores mean more accumulated debt.

Temporal Coupling

Files that frequently change together in the same commits. High temporal coupling suggests hidden dependencies not visible in the import graph.

Architectural Smell

A structural pattern that violates design principles: cyclic dependencies, hub-like modules, or stable code depending on unstable code.

Instability

Ratio of outgoing to total dependencies. 0 = maximally stable (many things depend on it), 1 = maximally unstable (depends on many things).

PageRank

Importance of a file in the dependency graph. Files with high PageRank are depended on by many other important files.