// THIS FILE IS AUTO-GENERATED.
// Edit docs/CONTRIBUTING-source.adoc instead.

// ============================================================
// This is the source file for CONTRIBUTING.adoc.
// CONTRIBUTING.adoc is AUTO-GENERATED by the GitHub
// workflow .github/workflows/build-docs.yml.
// NEVER edit CONTRIBUTING.adoc manually — edit this file instead.
// ============================================================

:project_name: Semantic Drilldown
:project_type: mediawiki
:phan:

= Contributing

We welcome contributions from the community to help make {project_name} better!
Whether you're fixing bugs, adding new features, improving documentation, or anything else, your efforts are valuable.

== Step 1: Clone the Repository

Fork and clone the repository to your local machine:

[source,console]
----
git clone https://github.com/username/projectname.git --recursive
----

== Step 2: Ensure the test container is running

This repository supports https://github.com/gesinn-it-pub/docker-compose-ci[_docker-compose-ci_-based CI and testing for MediaWiki extensions].

The _docker-compose-ci_ repository is integrated as a Git submodule (`build/`).
It uses https://en.wikipedia.org/wiki/Make_(software)[Make] as the main entry point.

Ensure `Make` and `Docker` are installed:

[source,console]
----
make --version
docker --version
----

Run lint, phpcs, and tests:

[source,console]
----
make ci
----

== Step 3: Start coding

Make sure there is an issue describing your changes.
Create a new branch (replace _1234_ with your issue number):

[source,console]
----
git checkout -b issue-1234
----

'''

*Coding Conventions — MediaWiki*

All source files regardless of language must follow these baseline rules.
They are enforced by `make ci` (lint + phpcs + eslint).

* Encoding: UTF-8 without BOM
* Line endings: Unix-style LF (not CR+LF)
* Indentation: tabs, not spaces
* Maximum line length: 120 characters
* No trailing whitespace
* Newline at end of file

'''

*Coding Conventions — PHP*

*File structure*

* Every file starts with `declare( strict_types=1 );`
* No closing `?>` tag
* One class per file; filename matches class name (UpperCamelCase, e.g. `MyClass.php`)

*Namespaces and autoloading*

* PSR-4 via Composer (`autoload.psr-4` in `composer.json`)
* Acronyms treated as single words: `HtmlId`, not `HTMLId`

*Naming*

[cols="1,1,1"]
|===
| Element | Convention | Example

| Classes, interfaces, traits
| UpperCamelCase
| `PageFormParser`

| Methods, variables
| lowerCamelCase
| `getFormContent()`

| Constants
| UPPER_CASE
| `MAX_FORM_SIZE`
|===

*Type system*

* Use native type declarations on all parameters, properties, and return types
* PHPDoc only when native types are insufficient (e.g. `string[]`, `array<string, Foo>`)
* Nullable parameters: `?Type`, not `Type $x = null`
* Prefer `??` (null coalescing) and `??=` over ternary isset checks
* Use arrow functions `+fn( $x ) => $x * 2+` for single-expression closures

*Modern PHP features (target: PHP 8.1+)*

* Constructor property promotion
* `readonly` properties for immutable value objects
* `enum` instead of class constant groups
* `match()` instead of `switch` when returning a value

*Code style*

* Indentation: tabs, not spaces
* 1TBS brace style — opening brace on same line, `else`/`elseif` on closing brace line
* Always use braces, even for single-line blocks
* Spaces inside parentheses: `getFoo( $bar )`, empty: `getBar()`
* Spaces around binary operators: `$a = $b + $c`
* Single quotes preferred; double quotes for string interpolation
* `===` strict equality; `==` only when type coercion is intentional
* No Yoda conditions: `$a === 'foo'`, not `'foo' === $a`
* `elseif` not `else if`
* `true`, `false`, `null` always lowercase

*Architecture*

* `private` by default; `protected` only when subclass access is needed
* Dependency injection over direct instantiation — delegate `new Foo()` to factories
* Single Responsibility: one class, one concern
* Order class members: `public` → `protected` → `private`

'''

*Coding Conventions — PHP · MediaWiki*

Tooling: https://github.com/wikimedia/mediawiki-tools-codesniffer[mediawiki-codesniffer] via PHPCS.
Run locally: `make composer-phpcs` (or `make ci`).

*Source directories*

* New code belongs in `src/` following PSR-4; `includes/` is legacy and should be migrated incrementally

*Namespaces*

* Top-level namespace = extension name (e.g. `MediaWiki\Extension\FooBar\...`)

*Global variable prefix*

* Global variables: `$wg` prefix (e.g. `$wgPageFormsSettings`)

*Request handling*

* No superglobals (`$_GET`, `$_POST`) — use `WebRequest` via `RequestContext`
* No new global functions — use static utility classes (`Html`, `IP`) if needed

'''

*Static Analysis — Phan · PHP*

Tooling: https://github.com/phan/phan[Phan] with https://github.com/wikimedia/mediawiki-phan-config[mediawiki-phan-config].
Run locally: `make composer-phan` (or `make dev-test`).

*Setup*

Add the Phan script to `composer.json`:

[source,json]
----
"scripts": {
    "phan": "phan --allow-polyfill-parser"
}
----

NOTE: `--allow-polyfill-parser` activates a pure-PHP AST fallback.
Required when the native `php-ast` extension is not available (e.g. Debian trixie / PHP 8.3 where `php-ast` has no apt package).
Without this flag Phan exits immediately if `php-ast` is absent.

Add the following targets to the extension `Makefile`:

[source,makefile]
----
composer-phan: .init ## Run Phan static analysis
	$(compose-exec-wiki) bash -c "cd $(EXTENSION_FOLDER) && composer phan $(COMPOSER_PARAMS)"

composer-phan-update-baseline: .init ## Re-generate baseline and fix indentation for PHPCS
	$(compose-exec-wiki) bash -c "cd $(EXTENSION_FOLDER) && composer phan -- --save-baseline=.phan/baseline.php"
	unexpand --first-only -t 4 .phan/baseline.php > /tmp/baseline.php && mv /tmp/baseline.php .phan/baseline.php
----

NOTE: The `unexpand` post-processing step is required because Phan hardcodes 4-space indentation in `BaselineSavingPlugin.php` — this cannot be configured via CLI or `config.php`.
MediaWiki PHPCS enforces tabs, so committing the unmodified baseline will cause PHPCS failures.
On macOS where `unexpand --first-only` is unavailable, use `sed` instead: +
`sed -i 's/    /\t/g' .phan/baseline.php`

*Configuration*

`.phan/config.php` inherits from `mediawiki-phan-config`:

[source,php]
----
$cfg = require __DIR__ . '/../vendor/mediawiki/mediawiki-phan-config/src/config.php';

$cfg['baseline_path'] = __DIR__ . '/baseline.php';

$cfg['directory_list'] = array_merge(
    $cfg['directory_list'],
    ['src', 'includes', 'specials']
);

$cfg['exclude_analysis_directory_list'] = array_merge(
    $cfg['exclude_analysis_directory_list'],
    ['vendor/']
);

return $cfg;
----

*Baseline*

* `.phan/baseline.php` is auto-generated — do not edit it manually
* New code must not introduce Phan issues beyond the current baseline
* When deliberately deferring a pre-existing issue, update the baseline via the dedicated target: +
  `make composer-phan-update-baseline` +
  This re-generates `.phan/baseline.php` and converts Phan's hardcoded 4-space indentation to tabs (required by MediaWiki PHPCS). Never run `--save-baseline` directly without this post-processing step.
* When suppressing with `@suppress`, always add an explanatory comment


'''

*Coding Conventions — JavaScript · MediaWiki*

Tooling: https://eslint.org/[ESLint] with https://github.com/wikimedia/eslint-config-wikimedia[eslint-config-wikimedia].
Run locally: `npm run lint:js` (or `make ci`).

*ESLint configuration*

Every repository must have a `.eslintrc.json` at root with `"root": true`:

[source,json]
----
{
  "root": true,
  "extends": [
    "wikimedia/client/es2016",
    "wikimedia/jquery",
    "wikimedia/mediawiki"
  ],
  "env": { "commonjs": true }
}
----

*Module system*

* CommonJS modules: `require()` for imports, `module.exports` for exports
* Register modules with ResourceLoader; bundle name pattern: `ext.myExtension`
* JS class files match the class name exactly (`TitleWidget.js` for `TitleWidget`)

*Naming*

* Variables and methods: lowerCamelCase
* Constructors / classes: UpperCamelCase
* jQuery objects: `$`-prefix (`$button`, not `button`)
* Constants: `ALL_CAPS`
* Acronyms as single words: `getHtmlApiSource`, not `getHTMLAPISource`

*Code style*

* Tabs for indentation; single quotes for string literals
* `===` and `!==`; no Yoda conditions
* Spaces inside parentheses: `if ( foo )`, `getFoo( bar )`
* `const` and `let` — never `var` in new code
* Arrow functions for callbacks

*jQuery*

* Prefer ES6/DOM equivalents over deprecated jQuery methods (`.each` → `forEach`, etc.)
* Never search the full DOM with `$( '#id' )` or `$( '.selector' )`;
  use hook-provided `$content` and call `.find()` on it
  _(full-DOM queries match stale or foreign nodes, break hook-lifecycle isolation, and waste performance by traversing the entire document)_
* Prefer `$( '<div>' ).text( value )` over `$( '<div>text</div>' )` to avoid XSS

*MediaWiki APIs*

* Access configuration via `mw.config.get( 'wgFoo' )`, never direct globals
* Expose public API via `module.exports` or within the `mw` namespace (e.g. `mw.echo.Foo`)
* Use `mw.storage` / `mw.storage.session` for localStorage/sessionStorage
* Storage keys: `mw`-prefix + camelCase/hyphens (e.g. `mwedit-state-foo`)

'''

*Coding Conventions — CSS/LESS · MediaWiki*

Tooling: https://stylelint.io/[stylelint] via `npm run lint:styles` (or `make ci`).
ResourceLoader natively compiles `.less` files; prefer LESS over plain CSS.

*Class and ID naming*

* Classes and IDs: all-lowercase, hyphen-separated
* Use an extension-specific prefix to avoid conflicts (e.g. `pf-`, `smw-`, `mw-`)
* LESS mixin names: `mixin-` prefix + hyphen-case (e.g. `mixin-screen-reader-text`)

*Whitespace and formatting*

* One selector per line, one property per line
* Opening brace on the same line as the last selector
* Tab indentation for properties and nested rules
* Semicolon after every declaration, including the last
* Empty line between rule sets

*Colors*

* Lowercase hex shorthand preferred: `#fff`, `#252525`
* `rgba()` when alpha transparency is needed; `transparent` keyword otherwise
* No named color keywords (except `transparent`), no `rgb()`, `hsl()`, `hsla()`
* Ensure color contrast meets https://www.w3.org/TR/WCAG20/[WCAG 2.0 AA]

*LESS specifics*

* CSS custom properties (design tokens) preferred over LESS variables for new code
* `@import` only for mixins and variables (`variables.less`, `mixins.less`);
  do not use `@import` for bundling conceptually related files
* Omit `.less` extension in `@import` statements
* Bundle related files via the `styles` array in `skin.json` / `extension.json`

*Anti-patterns to avoid*

* `!important` — avoid except when overriding upstream code that also uses it
* `z-index` — use natural DOM stacking order where possible; document exceptions
* Inline `style` attributes — always use stylesheet classes instead
* `float` / `text-align: left` hardcoded — use `/* @noflip */` annotation when needed,
  otherwise ResourceLoader's CSSJanus handles RTL automatically

== Step 4: Test your changes

'''

*Procedure — test:write · MediaWiki*

Before making any code changes to fix a bug or implement a feature:

. Check whether an existing test already covers the described behavior.
. If not, write or adapt a test that reproduces the issue — it must fail first.
. Only after a failing test exists, make the code changes.
. Re-run the test to confirm it passes (green).

*MediaWiki test base classes*

Use the appropriate base class:

* `MediaWikiUnitTestCase` — pure unit tests (no database, no service container); fastest
* `MediaWikiIntegrationTestCase` — integration tests that need the service container or database
* `MediaWikiLangTestCase` — when language handling is under test
* `SpecialPageTestBase` — extends `MediaWikiIntegrationTestCase`; use when testing Special Pages
* `HookRunnerTestBase` — unit test base for `HookRunner` classes; validates hook delegation automatically

Do *not* extend `MediaWikiIntegrationTestCase` by default — use `MediaWikiUnitTestCase` unless integration with MW services is required.

*Testing Special Pages with SpecialPageTestBase*

Extend `SpecialPageTestBase` and implement `newSpecialPage()` to return the page instance:

[source,php]
----
class SpecialFooTest extends SpecialPageTestBase {
    protected function newSpecialPage() {
        return MediaWikiServices::getInstance()
            ->getSpecialPageFactory()->getPage( 'Foo' );
    }
}
----

Key methods available in tests:

* `$this->executeSpecialPage( $subpage, new FauxRequest( $query, $isPosted ) )` — renders the special page and returns `[ $html, $context ]`
* `$this->setUserLang( 'qqx' )` — use `qqx` locale so message keys appear literally in output, making assertions locale-independent
* `$this->setGroupPermissions( '*', 'edit', false )` — adjust permissions; combine with `$this->expectException( PermissionsError::class )` to test access control
* `$this->insertPage( $title, $content )` — create a page as fixture
* `$this->getServiceContainer()` — access MW services

Annotate the class with `@group Database` when the test writes to the database.

If the special page requires a permission (e.g. `upload`), pass a matching performer as the fourth argument — otherwise `executeSpecialPage()` runs as an anonymous user and throws `PermissionsError`:

[source,php]
----
$performer = $this->getTestUser( [ 'sysop' ] )->getAuthority();
[ $html, ] = $this->executeSpecialPage( '', new FauxRequest( [] ), null, $performer );
----

If the page checks a feature flag via config (e.g. `UploadBase::isEnabled()` reads `wgEnableUploads`), set it before calling `executeSpecialPage()`:

[source,php]
----
$this->setMwGlobals( 'wgEnableUploads', true );
----

*Asserting HTML output*

Parse the returned `$html` with `DomDocument` and `DomXPath` to assert on form fields, links, or rendered content:

[source,php]
----
[ $html, ] = $this->executeSpecialPage( '', new FauxRequest( [] ) );
$dom = new DomDocument;
$dom->loadHTML( $html );
$xpath = new DomXpath( $dom );
$input = $xpath->query( '//input[@name="wpFoo"]' )->item( 0 );
$this->assertNotNull( $input );
----

*Testing HookRunner classes with HookRunnerTestBase*

If the extension has a `HookRunner` class (a class that implements hook interfaces and delegates to `HookContainer::run()`), add a unit test that extends `HookRunnerTestBase`. This test automatically verifies that every hook method delegates correctly — right hook name, right argument signature.

First check whether a `HookRunner` exists:

[source,console]
----
find includes -name "*HookRunner.php" | head -1
----

If one exists, the test is a one-liner:

[source,php]
----
use MediaWiki\Tests\HookContainer\HookRunnerTestBase;

/**
 * @covers \FooHookRunner
 */
class FooHookRunnerTest extends HookRunnerTestBase {
    public static function provideHookRunners() {
        yield FooHookRunner::class => [ FooHookRunner::class ];
    }
}
----

Place the test under `tests/phpunit/unit/` — no database needed.

Do *not* introduce a `HookRunner` class just to have this test. Only add the test when the extension already uses the Hook Runner pattern.

*Test fixtures*

* Use `setUp()` and `tearDown()` for test-scoped fixtures
* For database fixtures, use `addDBDataOnce()` (run once per class) or `addDBData()` (run per test)
* Use `getMockBuilder()` / `createMock()` for dependencies; prefer constructor injection so mocks can be passed in

*Running tests*

See the *Execution — Install Dependencies · MediaWiki* and *Execution — Run Tests (PHPUnit) · MediaWiki* reference files loaded by this skill.

'''

*Execution — Install Dependencies · MediaWiki*

All tests run inside a containerized MediaWiki environment managed via
https://github.com/gesinn-it-pub/docker-compose-ci[docker-compose-ci] (the `build/` submodule).
Never run tests directly against a local PHP or Node.js installation.

Always run `make install` before executing tests to ensure that the latest file
changes are copied into the container. Changes to source or test files on the
host are *not* automatically reflected in a running container.

NOTE: When a `docker-compose.override.yml` with a bind-mount of the extension
source directory is active (local development setup), `make install` is only
required at the start of a new session or after dependency changes. For
iterative test runs, use `make php-test` or `make dev-test` directly.

[source,console]
----
make install
----

'''

*Execution — Run Tests (PHPUnit) · MediaWiki*

Run all PHPUnit tests:

[source,console]
----
make install composer-phpunit
----

Run a single test class or method (filtered):

[source,console]
----
make install composer-phpunit COMPOSER_PARAMS="-- --filter YourTestName"
----

Run a specific test suite:

[source,console]
----
make install composer-phpunit COMPOSER_PARAMS="-- --testsuite your-suite-name"
----

For interactive use, bash into the running container:

[source,console]
----
make bash
> composer phpunit -- --filter YourTestName
----

'''

*Execution — Run Tests (QUnit) · MediaWiki*

Run all JavaScript tests:

[source,console]
----
make install npm-test
----

There is no direct `make` target for filtering individual tests.
Bash into the running container to run a specific test file or test case:

[source,console]
----
make bash
> npm run node-qunit -- tests/node-qunit/yourtest.test.js
----

Filter by test description:

[source,console]
----
make bash
> npx qunit --require ./tests/node-qunit/setup.js 'tests/node-qunit/**/*.test.js' --filter "your test description"
----

'''

*Execution — Pre-Commit Gate · MediaWiki*

Before every commit, run the full CI suite to confirm nothing is broken:

[source,console]
----
make ci
----
For interactive use (volume-mounted extension, no container rebuild), use the faster pre-commit gate:

[source,console]
----
make dev-test
----

`dev-test` runs: lint → PHPCS → Phan → PHPUnit — without destroying Docker volumes.
Reserve `make ci` for the full pipeline verification.

== Step 5: Commit your changes

:leveloffset: +1

== Conventional Commits Policy

'''

*Commit Convention — Conventional Commits*

Commit messages follow the https://www.conventionalcommits.org/[Conventional Commits specification].

Commit format:

`type(scope): short description`

The scope is optional and should describe the affected subsystem, module, or dependency when useful.

Examples:

* feat(api): add autocomplete endpoint
* fix(parser): handle empty token lists
* docs(readme): explain input architecture
* refactor(parser): simplify token parsing
* deps(smw): bump from 5.1.0 to 5.2.0
* ci(github): update workflow configuration
* test(api): add autocomplete tests

Recommended commit types:

* `feat` — new functionality
* `fix` — bug fixes
* `deps` — dependency updates
* `docs` — documentation changes
* `refactor` — internal code changes without behavioral change
* `test` — tests added or updated
* `ci` — changes to continuous integration configuration
* `chore` — repository maintenance tasks without impact on runtime behavior

Dependency updates:

* Use the `deps` type for dependency upgrades
* The scope should identify the dependency being updated
* Include the version change when applicable

Example:

* deps(smw): bump from 5.1.0 to 5.2.0

Guidelines:

* Use the imperative mood (e.g. "add feature", not "added feature")
* Keep the subject line concise
* Use the commit body to explain *why*, not only *what*
* Scopes should be short, lowercase identifiers (e.g. `api`, `parser`, `smw`, `mediawiki`, `docker`)
* Use `chore` only for repository maintenance tasks that do not affect runtime behavior, dependencies, CI configuration, or tests
* Do *not* add a `Co-Authored-By:` trailer or any agent attribution line to the commit message

Changelog:

* After committing a `feat`, `fix`, `deps`, `refactor`, or `docs` change, add a corresponding entry to the `[Unreleased]` section of `CHANGELOG.md` — do not wait until release time.

:leveloffset!:

[source,console]
----
git commit -m "type(scope): description"
----

== Step 6: Push and open a Pull Request

[source,console]
----
git push origin issue-1234
----
