Skip to content

S005: Plugin System

FieldValue
SpecS005
FeaturePlugin System
Date2026-04-23
StatusDraft
Authorspec-writer-agent

Overview

The Plugin System enables users and package authors to extend Parity with custom rules without modifying Parity's core codebase. It provides three discovery locations -- project-local, global user, and Composer packages -- each targeting a different distribution scope. The PluginLoader service scans all three locations, loads PHP files or instantiates declared classes, and registers any valid RuleInterface implementations into the RuleRegistry.

Plugin files loaded from the project-local and global-user directories are plain PHP files that return either a single RuleInterface instance or an array of instances. Composer-distributed plugins declare their rule class FQCNs in their package's composer.json under extra.parity.rules, and the loader instantiates them via the project's Composer autoloader. Once registered, plugin rules are indistinguishable from built-in rules -- they participate identically in configuration resolution, evaluation, table output, and enforcement (see S002).

The three discovery locations are loaded in a deterministic order: project-local first, then global-user, then Composer packages. Because RuleRegistry uses last-write-wins semantics (S002-FR-004.e), later sources override earlier ones when name collisions occur. This means Composer package rules override global-user rules, and global-user rules override project-local rules. However, all three override built-in rules that were registered before plugin loading. Plugin loading errors are captured as warnings rather than fatal errors, ensuring a single broken plugin does not prevent Parity from running.

User Scenarios

S005-US-001 [P1] As a developer, I want to write a custom rule in .parity/plugins/ that only applies to my current project, so that I can enforce project-specific conventions.

S005-US-002 [P2] As a developer working on multiple projects, I want to place shared custom rules in ~/.parity/plugins/ so that they are available across all my projects without duplication.

S005-US-003 [P2] As a package author, I want to distribute custom Parity rules via Composer so that teams can composer require my package and immediately use my rules.

S005-US-004 [P1] As a developer, I want plugin loading errors to produce warnings instead of fatal errors so that one broken plugin does not prevent me from running Parity.

S005-US-005 [P2] As a developer, I want to override a global plugin with a project-local one of the same name so that I can customize behavior per-project.

S005-US-006 [P1] As a developer, I want my custom rule to appear in the check output table and participate in pass/fail evaluation exactly like built-in rules.

Requirements Summary

IDTypePriorityTitleStatus
S005-FR-001FunctionalP1Three-location plugin discoveryDraft
S005-FR-002FunctionalP1Discovery order and precedenceDraft
S005-FR-003FunctionalP1Project-local plugin loadingDraft
S005-FR-004FunctionalP2Global-user plugin loadingDraft
S005-FR-005FunctionalP2Composer package plugin discoveryDraft
S005-FR-006FunctionalP1Plugin file return contractDraft
S005-FR-007FunctionalP1RuleInterface compliance requiredDraft
S005-FR-008FunctionalP1Error handling and warningsDraft
S005-FR-009FunctionalP1Name collision resolutionDraft
S005-FR-010FunctionalP1Plugin-to-registry integrationDraft
S005-FR-011FunctionalP2Composer autoloader bootstrappingDraft
S005-FR-012FunctionalP2Composer v1 and v2 installed.json compatibilityDraft
S005-FR-013FunctionalP1Alphabetical file loading within a directoryDraft
S005-FR-014FunctionalP1No sandboxing -- same-process executionDraft
S005-IF-001InterfaceP1PluginLoader public APIDraft
S005-IF-002InterfaceP1Plugin file return type contractDraft
S005-IF-003InterfaceP2Composer extra.parity.rules schemaDraft
S005-IF-004InterfaceP2Plugin authoring minimal exampleDraft
S005-AS-001AcceptanceP1Project-local plugin discovered and registeredDraft
S005-AS-002AcceptanceP2Global-user plugin discovered and registeredDraft
S005-AS-003AcceptanceP2Composer plugin discovered and registeredDraft
S005-AS-004AcceptanceP1Plugin returning array registers multiple rulesDraft
S005-AS-005AcceptanceP1Invalid plugin produces warning, not fatal errorDraft
S005-AS-006AcceptanceP1Name collision resolved by last-write-winsDraft
S005-AS-007AcceptanceP1Plugin rule evaluated like built-in ruleDraft
S005-AS-008AcceptanceP1Plugin rule appears in table outputDraft
S005-AS-009AcceptanceP2Missing plugin directory silently skippedDraft
S005-AS-010AcceptanceP2Plugin with throwing constructor produces warningDraft
S005-AS-011AcceptanceP2Composer plugin with missing class produces warningDraft
S005-SC-001SuccessP1Plugin isolation from coreDraft
S005-SC-002SuccessP1All three discovery paths functionalDraft
S005-SC-003SuccessP1Error resilience verifiedDraft
S005-SC-004SuccessP1Name collision semantics verifiedDraft
S005-SC-005SuccessP2Composer distribution end-to-endDraft
S005-EC-001Edge CaseP1Plugin directory does not existDraft
S005-EC-002Edge CaseP1Plugin file returns non-RuleInterface valueDraft
S005-EC-003Edge CaseP1Plugin file throws exception on requireDraft
S005-EC-004Edge CaseP2Plugin file returns empty arrayDraft
S005-EC-005Edge CaseP2Plugin file returns array with mixed valid and invalid itemsDraft
S005-EC-006Edge CaseP1Composer class does not implement RuleInterfaceDraft
S005-EC-007Edge CaseP1Composer class does not existDraft
S005-EC-008Edge CaseP2Composer installed.json is malformed JSONDraft
S005-EC-009Edge CaseP2Composer installed.json is missing entirelyDraft
S005-EC-010Edge CaseP2HOME environment variable is unsetDraft
S005-EC-011Edge CaseP2Plugin name collides with built-in ruleDraft
S005-EC-012Edge CaseP2Plugin directory contains non-PHP filesDraft
S005-EC-013Edge CaseP2Composer extra.parity.rules contains non-string entryDraft
S005-EC-014Edge CaseP2Plugin file returns nullDraft
S005-EC-015Edge CaseP2Composer package declares empty rules arrayDraft
S005-NF-001Non-FunctionalP2Plugin loading performanceDraft
S005-NF-002Non-FunctionalP2Security considerationsDraft

Cross-Spec Dependencies

  • Depends on: S002 (Rules System provides RuleInterface, RuleResult, RuleContext, and RuleRegistry that plugins implement and register into), S006 (Configuration provides parity.yaml where custom rules are referenced by name)
  • Required by: S001 (CLI check command triggers PluginLoader::loadAll() before rule evaluation), S002-AS-015 (plugin rules evaluated identically to built-in rules)