TYPO3 Extension Testing
Skill: typo3-extension-testing
Set up, extend, repair or run tests and static quality checks for a TYPO3 project or extension: missing test infrastructure, PHPUnit unit and functional tests, fixtures, Playwright browser and accessibility tests, PHPStan, php-cs-fixer, baselines and a failing check.
Markdown source#
---
name: typo3-extension-testing
description: 'Set up, extend, repair or run tests and static quality checks for a TYPO3 project or extension: missing test infrastructure, PHPUnit unit and functional tests, fixtures, Playwright browser and accessibility tests, PHPStan, php-cs-fixer, baselines and a failing check.'
compatibility: Needs the typo3-dev-companion MCP server, which owns every lookup this workflow routes to and publishes this skill together with the references/base.md it opens on. Install it from github.com/TYPO3/dev-companion and run typo3-dev-companion install in the project. A copy taken out of that repository's skills directory alone has neither the tools nor that base file.
---
# TYPO3 Extension Testing
Establish or grow the smallest useful test and static-quality surface at the
correct layer. Make the first green run part of the setup, and run only commands
the checkout supports. Keep this skill as routing and workflow. Never keep
version-specific APIs, paths, dependency constraints, or commands that the
installation or the checkout owns.
## Establish the test surface
Work through [references/base.md](references/base.md) first. It fixes the order
every task here starts in and says why that order is not interchangeable. Two of
its answers decide this workflow before you write any test. The commands
`typo3_project_describe` reports are the only ones that exist in this
repository.
The layers `typo3_extension_describe` reports below `Tests/` are what the
extension has today.
An empty list is the answer that there is no harness yet.
Then, for this workflow:
- `typo3_documentation_lookup` with several short English queries and the target
TYPO3 version. Use it when dependency setup, bootstrap, fixtures, browser
configuration, or an API needs confirmation.
- Verify that the harness for every relevant layer can discover and run its
tests. Treat missing or broken infrastructure as a prerequisite of the
requested work. It is not a separate kind of task, and it is no reason to
force the behavior into another layer.
- Read the target code, existing tests and fixtures, test configuration,
dependency manifests, CI, and the development environment.
If a lookup is unavailable, state the gap apart from a lookup that found no
match. Do not replace current project evidence with a TYPO3 setup you recall.
## Choose the layer and its owner
Read [references/checklist.md](references/checklist.md) when you select layers,
establish missing infrastructure, choose commands, or audit coverage. After you
select a layer, read only its implementation guide:
- [references/phpunit.md](references/phpunit.md) for unit or functional tests.
- [references/playwright.md](references/playwright.md) for browser,
accessibility, or visual tests.
- [references/static-quality.md](references/static-quality.md) for static
analysis, coding standards, and the commands that run them.
- Prefer a unit test for isolated logic without TYPO3 state or persistence.
- Use a functional test when the behavior involves TYPO3 bootstrap,
configuration, database schema, DataHandler, repositories or services. Use one
for integration between framework components.
- Use a browser test for rendered user journeys, backend interaction,
JavaScript, or accessibility behavior nothing below the UI can establish.
- Use static analysis and coding standards for the defects and the style no test
observes. A task that asks for them establishes them whether or not the
project already runs them. A task that does not ask extends what is there and
reports what is missing. It introduces no check nobody requested.
- Keep unit and functional infrastructure with the extension whose PHP it
exercises. Keep browser infrastructure with the runnable project, because it
needs a served site rather than an extension package alone.
- Establish only the layers the task can justify. A setup request does not
require every possible test runner.
## Establish or repair the required harness
Before you add or extend coverage, fix any missing or broken prerequisite for
the selected layer. For an explicit setup request, this is the requested work.
For a review-only request, report the defect without changing it.
1. Determine compatible development dependencies from the project's constraints,
the installed packages, the Composer resolution, and the versioned
documentation. Add a dependency only when changes are in scope and the
selected layer requires it. Never guess its version.
2. Take configuration and bootstrap templates from the installed dependency or
the source `typo3_hint_lookup` names. Copy and adapt templates that say they
are examples. Do not point extension suites into a core checkout.
`typo3_reference_list` says which extensions the core ships as worked
examples of its own conventions. It says what each one is a reference for.
The browser suite and the static analysis setup are among them. One of those
is the form of the harness you establish here that passes today. A template
copied out of a manual is not.
3. Preserve configuration, scripts, and CI that work. Extend them instead of a
parallel harness.
4. Give each selected layer one stable local command before you add CI. Derive
functional database settings and browser URLs from the project's environment.
Do not commit credentials or machine-specific hosts.
5. For unit or functional tests, establish what the returned guidance requires.
That is the suite configuration, the bootstrap, the test directories, the
extension load, and the environment. Never translate a core-only
`runTests.sh` command into an extension command.
6. For browser tests, require a runnable site. Establish project-owned runner
configuration, scripts, artifacts, and one real target. Choose host,
container, or dedicated browser image from the project. Do not impose one
topology.
7. For static analysis and coding standards, establish one project-owned command
per check. Keep the command that reports apart from the one that writes. Fix
a new finding rather than record it in a baseline. Keep automatic formatting
inside the first-party paths the project intends it to touch.
8. Make CI call the same commands that passed locally. Add a version matrix only
for combinations the package declares and the dependency solver accepts.
## Add or extend tests
- Follow nearby tests that pass and the established harness. If the required
layer is missing, establish it first. Do not force the behavior into a cheaper
layer.
- Keep a regression test that fails for the observed defect before you apply its
fix, when practical.
- Keep fixtures minimal and deterministic. Avoid unrelated site data, execution
order, wall-clock time, and external services.
- Put reusable setup at the narrowest scope that removes meaningful duplication.
- Test observable behavior and public contracts. Avoid assertions tied only to
implementation details.
- Tell a broken runner, a missing environment prerequisite, and a failing
assertion apart before you change production code.
**Where the failing assertion is a defect in another workflow's code, invoke the
skill that owns it before the fix.** That is a step, not a note about ownership.
Load the skill by name and work from it. What crosses over is the failing test,
what it establishes and the paths it runs over. The test itself stays here and
runs again on what comes back.
## Prove the result
1. Prove the setup with a meaningful test at every layer the task established.
Do not add `assertTrue(true)` or production code whose only purpose is to
give the harness something to test. If no unit-testable behavior exists,
prove discovery and report the unit suite as empty.
2. Run the narrowest relevant test first, then its containing local suite.
3. Run the declared CI-equivalent commands after the local commands pass.
4. For browser work, execute at least one real spec. Confirm that it produces
its expected artifact or report.
5. For a static check, run it again after its fix command. Inspect the tree for
files the fixer touched outside the intended scope.
6. Report the exact commands you ran, the results, and the files you added or
changed. Report the checks you did not run, with the reason.
7. Draft the message for each commit with `typo3_commit_message_guide` and
`workflow="project"`.
[references/static-quality.md](references/static-quality.md) says where you
split a formatting pass off and in which order the commits go. What each of
them says is this tool's answer.
This skill owns testing and static-quality infrastructure, the changes they
require, and the execution of both. A broad conformance audit is
`typo3-extension-health`. A documentation rewrite is
`typo3-extension-documentation`. A backend module is
`typo3-backend-module-development`, and a content element is
`typo3-content-element-development`. Hand that work to its owner at the verified
point. Stop before you edit its files, and keep only the testing part.
markdown
---
name: typo3-extension-testing
description: 'Set up, extend, repair or run tests and static quality checks for a TYPO3 project or extension: missing test infrastructure, PHPUnit unit and functional tests, fixtures, Playwright browser and accessibility tests, PHPStan, php-cs-fixer, baselines and a failing check.'
compatibility: Needs the typo3-dev-companion MCP server, which owns every lookup this workflow routes to and publishes this skill together with the references/base.md it opens on. Install it from github.com/TYPO3/dev-companion and run typo3-dev-companion install in the project. A copy taken out of that repository's skills directory alone has neither the tools nor that base file.
---
# TYPO3 Extension Testing
Establish or grow the smallest useful test and static-quality surface at the
correct layer. Make the first green run part of the setup, and run only commands
the checkout supports. Keep this skill as routing and workflow. Never keep
version-specific APIs, paths, dependency constraints, or commands that the
installation or the checkout owns.
## Establish the test surface
Work through [references/base.md](references/base.md) first. It fixes the order
every task here starts in and says why that order is not interchangeable. Two of
its answers decide this workflow before you write any test. The commands
`typo3_project_describe` reports are the only ones that exist in this
repository.
The layers `typo3_extension_describe` reports below `Tests/` are what the
extension has today.
An empty list is the answer that there is no harness yet.
Then, for this workflow:
- `typo3_documentation_lookup` with several short English queries and the target
TYPO3 version. Use it when dependency setup, bootstrap, fixtures, browser
configuration, or an API needs confirmation.
- Verify that the harness for every relevant layer can discover and run its
tests. Treat missing or broken infrastructure as a prerequisite of the
requested work. It is not a separate kind of task, and it is no reason to
force the behavior into another layer.
- Read the target code, existing tests and fixtures, test configuration,
dependency manifests, CI, and the development environment.
If a lookup is unavailable, state the gap apart from a lookup that found no
match. Do not replace current project evidence with a TYPO3 setup you recall.
## Choose the layer and its owner
Read [references/checklist.md](references/checklist.md) when you select layers,
establish missing infrastructure, choose commands, or audit coverage. After you
select a layer, read only its implementation guide:
- [references/phpunit.md](references/phpunit.md) for unit or functional tests.
- [references/playwright.md](references/playwright.md) for browser,
accessibility, or visual tests.
- [references/static-quality.md](references/static-quality.md) for static
analysis, coding standards, and the commands that run them.
- Prefer a unit test for isolated logic without TYPO3 state or persistence.
- Use a functional test when the behavior involves TYPO3 bootstrap,
configuration, database schema, DataHandler, repositories or services. Use one
for integration between framework components.
- Use a browser test for rendered user journeys, backend interaction,
JavaScript, or accessibility behavior nothing below the UI can establish.
- Use static analysis and coding standards for the defects and the style no test
observes. A task that asks for them establishes them whether or not the
project already runs them. A task that does not ask extends what is there and
reports what is missing. It introduces no check nobody requested.
- Keep unit and functional infrastructure with the extension whose PHP it
exercises. Keep browser infrastructure with the runnable project, because it
needs a served site rather than an extension package alone.
- Establish only the layers the task can justify. A setup request does not
require every possible test runner.
## Establish or repair the required harness
Before you add or extend coverage, fix any missing or broken prerequisite for
the selected layer. For an explicit setup request, this is the requested work.
For a review-only request, report the defect without changing it.
1. Determine compatible development dependencies from the project's constraints,
the installed packages, the Composer resolution, and the versioned
documentation. Add a dependency only when changes are in scope and the
selected layer requires it. Never guess its version.
2. Take configuration and bootstrap templates from the installed dependency or
the source `typo3_hint_lookup` names. Copy and adapt templates that say they
are examples. Do not point extension suites into a core checkout.
`typo3_reference_list` says which extensions the core ships as worked
examples of its own conventions. It says what each one is a reference for.
The browser suite and the static analysis setup are among them. One of those
is the form of the harness you establish here that passes today. A template
copied out of a manual is not.
3. Preserve configuration, scripts, and CI that work. Extend them instead of a
parallel harness.
4. Give each selected layer one stable local command before you add CI. Derive
functional database settings and browser URLs from the project's environment.
Do not commit credentials or machine-specific hosts.
5. For unit or functional tests, establish what the returned guidance requires.
That is the suite configuration, the bootstrap, the test directories, the
extension load, and the environment. Never translate a core-only
`runTests.sh` command into an extension command.
6. For browser tests, require a runnable site. Establish project-owned runner
configuration, scripts, artifacts, and one real target. Choose host,
container, or dedicated browser image from the project. Do not impose one
topology.
7. For static analysis and coding standards, establish one project-owned command
per check. Keep the command that reports apart from the one that writes. Fix
a new finding rather than record it in a baseline. Keep automatic formatting
inside the first-party paths the project intends it to touch.
8. Make CI call the same commands that passed locally. Add a version matrix only
for combinations the package declares and the dependency solver accepts.
## Add or extend tests
- Follow nearby tests that pass and the established harness. If the required
layer is missing, establish it first. Do not force the behavior into a cheaper
layer.
- Keep a regression test that fails for the observed defect before you apply its
fix, when practical.
- Keep fixtures minimal and deterministic. Avoid unrelated site data, execution
order, wall-clock time, and external services.
- Put reusable setup at the narrowest scope that removes meaningful duplication.
- Test observable behavior and public contracts. Avoid assertions tied only to
implementation details.
- Tell a broken runner, a missing environment prerequisite, and a failing
assertion apart before you change production code.
**Where the failing assertion is a defect in another workflow's code, invoke the
skill that owns it before the fix.** That is a step, not a note about ownership.
Load the skill by name and work from it. What crosses over is the failing test,
what it establishes and the paths it runs over. The test itself stays here and
runs again on what comes back.
## Prove the result
1. Prove the setup with a meaningful test at every layer the task established.
Do not add `assertTrue(true)` or production code whose only purpose is to
give the harness something to test. If no unit-testable behavior exists,
prove discovery and report the unit suite as empty.
2. Run the narrowest relevant test first, then its containing local suite.
3. Run the declared CI-equivalent commands after the local commands pass.
4. For browser work, execute at least one real spec. Confirm that it produces
its expected artifact or report.
5. For a static check, run it again after its fix command. Inspect the tree for
files the fixer touched outside the intended scope.
6. Report the exact commands you ran, the results, and the files you added or
changed. Report the checks you did not run, with the reason.
7. Draft the message for each commit with `typo3_commit_message_guide` and
`workflow="project"`.
[references/static-quality.md](references/static-quality.md) says where you
split a formatting pass off and in which order the commits go. What each of
them says is this tool's answer.
This skill owns testing and static-quality infrastructure, the changes they
require, and the execution of both. A broad conformance audit is
`typo3-extension-health`. A documentation rewrite is
`typo3-extension-documentation`. A backend module is
`typo3-backend-module-development`, and a content element is
`typo3-content-element-development`. Hand that work to its owner at the verified
point. Stop before you edit its files, and keep only the testing part.
References#
Where every task starts#
# Where every task starts
## Nothing starts until the server answers
A skill is a file the installer left behind. It loads and reads the same whether
the tools behind it are there or not, and neither side notices. So the first
call below is also the check.
- A client may carry this server's name in each tool's name:
`mcp__<server>__typo3_project_describe`. So a search for the bare name comes
back empty where the server is there. A search for a tool's schema needs the
same form. A `select:` on the bare names returns nothing where the tools are
there. Look for the qualified form before you read an empty result as an
answer about the server.
- No `typo3_` tool in this session, or a first call that errors: stop. Say that
this workflow needs the server and it is not there, and name what came back.
- Do not fall back to general TYPO3 knowledge, and do not start to read the
checkout. That answer carries this workflow's order and confidence and none of
its evidence. Nothing in it says which of the two it is.
- Continue only when the user asks you to after you said so. Repeat it in the
answer and in every finding a lookup would have carried.
## The order
This is an order rather than a list. Each step decides what the next one is
worth. Where a step below carries a condition to skip it, that condition is
narrow on purpose. A skipped prescription teaches the next reader to skip the
ones that matter too.
1. **`typo3_project_describe`** — the repository and whether it holds an
installation yet. It reports the TYPO3 and PHP version, the project's own
extensions, its sites, and the commands this repository declares. That
version filters every later answer. A check the repository does not declare
is a wrong answer however sensible it sounds.
The answer ends with the whole procedures this server carries, as ids. That
list is the only place a client that renders no resource list sees their
names. Each one is a `typo3_rule_lookup` with that `documentId` rather than a
search.
2. **`typo3_extension_describe`** for each extension in scope. It says what the
extension registers, and what it ships beside that. That is its manual, its
README, its test layers, and its XLF files with the source language each one
declares. It also says what the extension does *not* ship, and that is the
half no file listing gives you.
Where step 1 reported no extension, that answer is this step, and there is
nothing to call. Say so. A core checkout is that case, because step 1 names
the project's own extensions and not TYPO3's.
3. **`typo3_task_guide`** with a short English task, the paths it touches, the
target version and the change type. It answers the workflow this task belongs
to and the checks that come with it.
Run it in every session, this skill's own tasks included. The guide builds
the brief from the paths as well as the task text. No skill knows which paths
the caller holds.
A skill that covers the task is not that brief. A skipped step costs the
hints and the core checks those paths match. Where the guide's own answer
named this skill, this is one call for an answer already in the session. The
price of a step there is nothing to decide about.
4. **`typo3_hint_lookup`** for each subsystem in scope, with its concrete paths.
One query per subsystem. A single broad query is not subsystem evidence.
Where step 3 ran with those paths, its answer says whether you still owe this
step. A brief that carried everything the lookup matched says so: "these are
everything typo3_hint_lookup matches for these paths". There the guide made
the call, and the same query returns the same hints. A brief that stopped
short says that instead and names the ids it left. You owe those: fetch them
by id rather than repeat the query.
Read the sentence rather than the populated `hints` key. That key is present
either way and does not tell the two apart. `omittedHints` is that sentence
as data. It is empty where the brief carried everything, and it holds the ids
the brief left where it stopped short.
5. **`typo3_changelog_lookup` with `type: deprecation`**, at each major the
package declares. Omit the query and raise `limit` to carry that major whole.
Those two are the changelog's own axes, and the extension's vocabulary is not
among them. An entry carries a query only when its title carries every word
of it at once. The core titled those entries about its own code.
That is one call per declared major, and what comes back is the major. Every
entry carries its own index tags. `ext:core`, `ext:frontend`, `ext:form` and
the rest name the system extension a change is **in**. `TCA`, `TypoScript`,
`Fluid`, `YAML`, `Backend`, `Frontend` name the surface.
Step 2 picks the package's entries out of that answer by those tags. The tags
are the system extensions it requires, renders through or registers into, and
the kinds of file it ships. That costs no further call. An extension key of
your own is not among them and matches nothing. `tag` narrows one question
inside a major rather than composes the sweep out of eleven.
You check the answers against step 2, which is the other half the words did.
Verify each identifier that comes back in the checkout. A deprecation nothing
here calls is not a finding.
Carry the `FullyScanned` / `PartiallyScanned` tag into the answer. It says
whether the Extension Scanner can find the remaining call sites or whether
that reading is yours. Bounded this way, you can write the sweep before you
open a file. That is why it is a step of the order rather than something the
reading stumbles into.
**What its silence is worth.** A changelog records change events. So a
pattern nothing has touched for ten majors has no entry at all. An empty
sweep is therefore not an answer about what still works. "Does this still
work in version N" goes to `typo3_documentation_lookup` at that version. Ask
it here, and whenever the reading raises it again.
That is a question for a documented surface: a ViewHelper, a TCA type, a
TypoScript setting. The manual matches page titles, section paths and what
each manual declares by name, never the text of a page. Declared is a
property, a class or method the manual documents, a console command. You
reach one by its own name where the query writes that name the way code
does. You also reach it where the query is nothing but the name. A PHP
identifier the manual does not declare has no page named after it.
An identifier goes to `typo3_changelog_lookup` under its own name. That
reaches the entries that write it, however the core titled the change. Then
it goes to the class below. Where the manual has no page for a surface
either, that is a result and not an answer. Undocumented is not unsupported.
**A second declared major.** A package that declares more than one asks a
second question of every deprecation the sweep returns. Is the replacement on
the lower one? The entry's `issue` is a query of its own, and it reaches
every entry filed under that number. The Feature the core announced the
replacement in is among them.
The version the core released it in settles that question. Where the number
reaches no sibling, nobody wrote an entry for the replacement.
`typo3_rule_lookup` with
`documentId="extension/compatibility/a-declared-major-that-is-not-installed"`
is the reading that closes it.
**Where you do not owe the sweep.** A task that produces no change does not
reach this step at all. The property is what the task produces. A triage, a
reproduction and a review illustrate it; they are not the list you read it
off. The sweep asks what a package will have to stop calling. A task that
writes nothing is not going to call anything.
The exemption ends where the workflow produces a change. A review asked to
make the change is that other workflow. It starts this order again with the
files it is about to write. To carry somebody else's patch onto current code
is on the same side. It writes commits. The sweep says whether the code that
moved under the patch deprecated something the patch calls.
Skip the sweep only where the change touches no TYPO3 API: a code style
fixer, a CI file, an `.editorconfig`. A deprecation is a statement about API
the package calls. So a change that calls none has nothing for the sweep to
land on. The sweep is empty before it runs.
That condition is worth a statement, because this step is the largest answer
the order asks for. It is one call per declared major, with that major's
deprecations whole. You read which side a change falls on off the files it
touches, never off the task it started as. One PHP file edited along the way
puts it back among the ordinary ones.
A skip there costs the deprecation no finding would have walked into. How
small the change is decides nothing either. Three statements can call a
deprecated API as easily as three hundred.
A test file is one of those wherever it sits. It calls the API it exercises
and the framework around it, and both deprecate. A fixture is exempt where it
is data the suite reads, and not where it is a class.
**Before the reading**, write down what the order established. That is the
version that filters every later answer, the packages in scope, and the commands
this repository declares. Write down which steps what discharged. Those are
answers already in the session rather than a second reading. What the files show
belongs to the report at the other end. A caller who cannot see what an answer
rests on cannot tell it from one that rests on nothing.
**Then** read the checkout. Not before. A file list first makes everything after
the list look optional. The conventions then arrive as a footnote to a verdict
that has already formed.
**Before the first edit**, name the files this change will create, change or
delete. A deletion is the caller's to ask for, and this is somebody else's
checkout. It is the one act nothing here can put back.
**Last**, the report names every step of this order it did not reach, and what
stood in for it. That is an answer already in the session, a condition that made
the step empty, or an exemption. A reader cannot tell a step passed over in
silence from one somebody dropped.
## When the lookups run out
A behaviour question that survives the lookups above is one you read out of the
installed source. Do not guess at it. The class that implements the behaviour
and the one it inherits from answer it. That reading is the step after the
lookups. It replaces a change to the code until it works.
A first change that did not work is evidence about the reading. So the second
attempt at one failure reads the source rather than changes the code again.
What it settles is what this installation does and never what TYPO3 supports. So
a finding says that you could not settle the question beyond the version
installed. An answer built on the reading names the version it holds for.
## What each runtime lookup adds after the extension answer
`typo3_extension_describe` in step 2 says what one package registers. The
lookups below say what the installation resolved. That is a different fact even
where the words are the same. So step 2 has made none of these calls:
- `typo3_backend_module_lookup` — the tree position, the labels, the access
level, the routes and the navigation component the parent module supplies.
Step 2 lists the modules the package declares. A declaration cannot show that
inheritance.
- `typo3_icon_lookup` — whether any installed package registers an identifier.
That validates the ones a template uses. Step 2 lists the identifiers this
package contributes.
- `typo3_label_lookup` — the labels as the installation resolves them, with its
overrides applied. Step 2 lists the package's XLF files and the source
language each declares, never what a unit says here.
- `typo3_fluid_namespace_list` — the prefixes any template may use without a
declaration, from every package at once. Step 2 lists the package's own
declarations. So an empty list there is no evidence that no package registers
a prefix globally.
- `typo3_configuration_lookup` — the resolved configuration value, after every
extension has had its say. For a form data group it gives the order the
providers really run in. Step 2 answers nothing about that surface at all.
What a registration declares is not what the installation resolves.
- `typo3_service_lookup` — the class the container really injects for a service
id, an interface or a tag. Decorations and overrides count. Step 2 lists what
the package's own `Services.yaml` declares, never what won.
- `typo3_schema_lookup` — the columns TYPO3 derives for a table from its TCA. It
gives the type, the nullability and the default each one gets. Step 2 lists
the tables the package registers and nothing about their shape.
- `typo3_flexform_lookup` — the data structure the installation resolves a
`type=flex` field to, sheet by sheet, with listeners and migrations applied.
Step 2 lists the content elements a package registers, never the structure
each one's form builds.
- `typo3_record_lookup` — the rows of any table the installation has TCA for. It
says how many there are and where they sit. It says what one column holds
across them and which rows depart from its default. Step 2 has no row in its
answer at all.
None of these says whether what it reports is right. `typo3_hint_lookup` and
`typo3_documentation_lookup` do. A subsystem its own runtime lookup confirmed
can still break every rule that governs it. So it is not established until you
asked both.
## A rule reads in both directions
It says what new code should do, and it says what this checkout already does
wrong. A file that settled into the opposite of a rule is a finding, not a local
style to preserve. Consistency with a project's own habit establishes nothing
about whether the habit is right.
## What the code is for is evidence, and the repository states it
A mechanism that costs something is not a defect because it costs. Before you
report one, find what it is there for and say so. That is the manual, the
README, the changelog, the setting that drives it, or the declared versions.
Where the documentation states a purpose, what you have is a trade-off, not a
defect. Name it with its cost and its alternative. Where you cannot find one,
the finding says that you could not establish one, not that none exists. If you
skip this, your review is a list of everything the author did on purpose.
## What a finding rests on is part of the finding
Three things carry one. A file you read, at its path and its line. A command you
ran, with what it printed. A mechanism you traced into an installed package. Say
which of the three it is. If you leave it unsaid, a finding from a CI file
weighs as much as one with a verified line.
Where one of the project's own commands would settle it, run it.
`typo3_project_describe` marks each command it lists **check**, **change** or
**unknown**, read off the declared body. A check reports and hands the code back
as it was. So even a task told not to change files runs it. The linter the
repository already declares is the cheapest evidence in it.
Do not run a change under that instruction. Name an unknown in the answer as
evidence that is available, and do not run it unasked. An unknown is a test
suite, a shell pipeline, a console command.
What a check prints is not the finding. The configuration that makes it fail is
still what the finding is about. The run takes that finding from derived to
established.
## What this server does not know
It does not read your working tree. You establish which files changed, which
branch you are on, and whether a path or an identifier still exists there. Then
pass the concrete paths back, because that turns a general convention into an
answer about this code.
## Query it in English
The knowledge is English and the match is lexical. So a query in another
language reaches the loanwords the two happen to share and nothing else.
Translate the subject before the call and the answer back afterwards, whatever
language you speak with the user.
markdown
# Where every task starts
## Nothing starts until the server answers
A skill is a file the installer left behind. It loads and reads the same whether
the tools behind it are there or not, and neither side notices. So the first
call below is also the check.
- A client may carry this server's name in each tool's name:
`mcp__<server>__typo3_project_describe`. So a search for the bare name comes
back empty where the server is there. A search for a tool's schema needs the
same form. A `select:` on the bare names returns nothing where the tools are
there. Look for the qualified form before you read an empty result as an
answer about the server.
- No `typo3_` tool in this session, or a first call that errors: stop. Say that
this workflow needs the server and it is not there, and name what came back.
- Do not fall back to general TYPO3 knowledge, and do not start to read the
checkout. That answer carries this workflow's order and confidence and none of
its evidence. Nothing in it says which of the two it is.
- Continue only when the user asks you to after you said so. Repeat it in the
answer and in every finding a lookup would have carried.
## The order
This is an order rather than a list. Each step decides what the next one is
worth. Where a step below carries a condition to skip it, that condition is
narrow on purpose. A skipped prescription teaches the next reader to skip the
ones that matter too.
1. **`typo3_project_describe`** — the repository and whether it holds an
installation yet. It reports the TYPO3 and PHP version, the project's own
extensions, its sites, and the commands this repository declares. That
version filters every later answer. A check the repository does not declare
is a wrong answer however sensible it sounds.
The answer ends with the whole procedures this server carries, as ids. That
list is the only place a client that renders no resource list sees their
names. Each one is a `typo3_rule_lookup` with that `documentId` rather than a
search.
2. **`typo3_extension_describe`** for each extension in scope. It says what the
extension registers, and what it ships beside that. That is its manual, its
README, its test layers, and its XLF files with the source language each one
declares. It also says what the extension does *not* ship, and that is the
half no file listing gives you.
Where step 1 reported no extension, that answer is this step, and there is
nothing to call. Say so. A core checkout is that case, because step 1 names
the project's own extensions and not TYPO3's.
3. **`typo3_task_guide`** with a short English task, the paths it touches, the
target version and the change type. It answers the workflow this task belongs
to and the checks that come with it.
Run it in every session, this skill's own tasks included. The guide builds
the brief from the paths as well as the task text. No skill knows which paths
the caller holds.
A skill that covers the task is not that brief. A skipped step costs the
hints and the core checks those paths match. Where the guide's own answer
named this skill, this is one call for an answer already in the session. The
price of a step there is nothing to decide about.
4. **`typo3_hint_lookup`** for each subsystem in scope, with its concrete paths.
One query per subsystem. A single broad query is not subsystem evidence.
Where step 3 ran with those paths, its answer says whether you still owe this
step. A brief that carried everything the lookup matched says so: "these are
everything typo3_hint_lookup matches for these paths". There the guide made
the call, and the same query returns the same hints. A brief that stopped
short says that instead and names the ids it left. You owe those: fetch them
by id rather than repeat the query.
Read the sentence rather than the populated `hints` key. That key is present
either way and does not tell the two apart. `omittedHints` is that sentence
as data. It is empty where the brief carried everything, and it holds the ids
the brief left where it stopped short.
5. **`typo3_changelog_lookup` with `type: deprecation`**, at each major the
package declares. Omit the query and raise `limit` to carry that major whole.
Those two are the changelog's own axes, and the extension's vocabulary is not
among them. An entry carries a query only when its title carries every word
of it at once. The core titled those entries about its own code.
That is one call per declared major, and what comes back is the major. Every
entry carries its own index tags. `ext:core`, `ext:frontend`, `ext:form` and
the rest name the system extension a change is **in**. `TCA`, `TypoScript`,
`Fluid`, `YAML`, `Backend`, `Frontend` name the surface.
Step 2 picks the package's entries out of that answer by those tags. The tags
are the system extensions it requires, renders through or registers into, and
the kinds of file it ships. That costs no further call. An extension key of
your own is not among them and matches nothing. `tag` narrows one question
inside a major rather than composes the sweep out of eleven.
You check the answers against step 2, which is the other half the words did.
Verify each identifier that comes back in the checkout. A deprecation nothing
here calls is not a finding.
Carry the `FullyScanned` / `PartiallyScanned` tag into the answer. It says
whether the Extension Scanner can find the remaining call sites or whether
that reading is yours. Bounded this way, you can write the sweep before you
open a file. That is why it is a step of the order rather than something the
reading stumbles into.
**What its silence is worth.** A changelog records change events. So a
pattern nothing has touched for ten majors has no entry at all. An empty
sweep is therefore not an answer about what still works. "Does this still
work in version N" goes to `typo3_documentation_lookup` at that version. Ask
it here, and whenever the reading raises it again.
That is a question for a documented surface: a ViewHelper, a TCA type, a
TypoScript setting. The manual matches page titles, section paths and what
each manual declares by name, never the text of a page. Declared is a
property, a class or method the manual documents, a console command. You
reach one by its own name where the query writes that name the way code
does. You also reach it where the query is nothing but the name. A PHP
identifier the manual does not declare has no page named after it.
An identifier goes to `typo3_changelog_lookup` under its own name. That
reaches the entries that write it, however the core titled the change. Then
it goes to the class below. Where the manual has no page for a surface
either, that is a result and not an answer. Undocumented is not unsupported.
**A second declared major.** A package that declares more than one asks a
second question of every deprecation the sweep returns. Is the replacement on
the lower one? The entry's `issue` is a query of its own, and it reaches
every entry filed under that number. The Feature the core announced the
replacement in is among them.
The version the core released it in settles that question. Where the number
reaches no sibling, nobody wrote an entry for the replacement.
`typo3_rule_lookup` with
`documentId="extension/compatibility/a-declared-major-that-is-not-installed"`
is the reading that closes it.
**Where you do not owe the sweep.** A task that produces no change does not
reach this step at all. The property is what the task produces. A triage, a
reproduction and a review illustrate it; they are not the list you read it
off. The sweep asks what a package will have to stop calling. A task that
writes nothing is not going to call anything.
The exemption ends where the workflow produces a change. A review asked to
make the change is that other workflow. It starts this order again with the
files it is about to write. To carry somebody else's patch onto current code
is on the same side. It writes commits. The sweep says whether the code that
moved under the patch deprecated something the patch calls.
Skip the sweep only where the change touches no TYPO3 API: a code style
fixer, a CI file, an `.editorconfig`. A deprecation is a statement about API
the package calls. So a change that calls none has nothing for the sweep to
land on. The sweep is empty before it runs.
That condition is worth a statement, because this step is the largest answer
the order asks for. It is one call per declared major, with that major's
deprecations whole. You read which side a change falls on off the files it
touches, never off the task it started as. One PHP file edited along the way
puts it back among the ordinary ones.
A skip there costs the deprecation no finding would have walked into. How
small the change is decides nothing either. Three statements can call a
deprecated API as easily as three hundred.
A test file is one of those wherever it sits. It calls the API it exercises
and the framework around it, and both deprecate. A fixture is exempt where it
is data the suite reads, and not where it is a class.
**Before the reading**, write down what the order established. That is the
version that filters every later answer, the packages in scope, and the commands
this repository declares. Write down which steps what discharged. Those are
answers already in the session rather than a second reading. What the files show
belongs to the report at the other end. A caller who cannot see what an answer
rests on cannot tell it from one that rests on nothing.
**Then** read the checkout. Not before. A file list first makes everything after
the list look optional. The conventions then arrive as a footnote to a verdict
that has already formed.
**Before the first edit**, name the files this change will create, change or
delete. A deletion is the caller's to ask for, and this is somebody else's
checkout. It is the one act nothing here can put back.
**Last**, the report names every step of this order it did not reach, and what
stood in for it. That is an answer already in the session, a condition that made
the step empty, or an exemption. A reader cannot tell a step passed over in
silence from one somebody dropped.
## When the lookups run out
A behaviour question that survives the lookups above is one you read out of the
installed source. Do not guess at it. The class that implements the behaviour
and the one it inherits from answer it. That reading is the step after the
lookups. It replaces a change to the code until it works.
A first change that did not work is evidence about the reading. So the second
attempt at one failure reads the source rather than changes the code again.
What it settles is what this installation does and never what TYPO3 supports. So
a finding says that you could not settle the question beyond the version
installed. An answer built on the reading names the version it holds for.
## What each runtime lookup adds after the extension answer
`typo3_extension_describe` in step 2 says what one package registers. The
lookups below say what the installation resolved. That is a different fact even
where the words are the same. So step 2 has made none of these calls:
- `typo3_backend_module_lookup` — the tree position, the labels, the access
level, the routes and the navigation component the parent module supplies.
Step 2 lists the modules the package declares. A declaration cannot show that
inheritance.
- `typo3_icon_lookup` — whether any installed package registers an identifier.
That validates the ones a template uses. Step 2 lists the identifiers this
package contributes.
- `typo3_label_lookup` — the labels as the installation resolves them, with its
overrides applied. Step 2 lists the package's XLF files and the source
language each declares, never what a unit says here.
- `typo3_fluid_namespace_list` — the prefixes any template may use without a
declaration, from every package at once. Step 2 lists the package's own
declarations. So an empty list there is no evidence that no package registers
a prefix globally.
- `typo3_configuration_lookup` — the resolved configuration value, after every
extension has had its say. For a form data group it gives the order the
providers really run in. Step 2 answers nothing about that surface at all.
What a registration declares is not what the installation resolves.
- `typo3_service_lookup` — the class the container really injects for a service
id, an interface or a tag. Decorations and overrides count. Step 2 lists what
the package's own `Services.yaml` declares, never what won.
- `typo3_schema_lookup` — the columns TYPO3 derives for a table from its TCA. It
gives the type, the nullability and the default each one gets. Step 2 lists
the tables the package registers and nothing about their shape.
- `typo3_flexform_lookup` — the data structure the installation resolves a
`type=flex` field to, sheet by sheet, with listeners and migrations applied.
Step 2 lists the content elements a package registers, never the structure
each one's form builds.
- `typo3_record_lookup` — the rows of any table the installation has TCA for. It
says how many there are and where they sit. It says what one column holds
across them and which rows depart from its default. Step 2 has no row in its
answer at all.
None of these says whether what it reports is right. `typo3_hint_lookup` and
`typo3_documentation_lookup` do. A subsystem its own runtime lookup confirmed
can still break every rule that governs it. So it is not established until you
asked both.
## A rule reads in both directions
It says what new code should do, and it says what this checkout already does
wrong. A file that settled into the opposite of a rule is a finding, not a local
style to preserve. Consistency with a project's own habit establishes nothing
about whether the habit is right.
## What the code is for is evidence, and the repository states it
A mechanism that costs something is not a defect because it costs. Before you
report one, find what it is there for and say so. That is the manual, the
README, the changelog, the setting that drives it, or the declared versions.
Where the documentation states a purpose, what you have is a trade-off, not a
defect. Name it with its cost and its alternative. Where you cannot find one,
the finding says that you could not establish one, not that none exists. If you
skip this, your review is a list of everything the author did on purpose.
## What a finding rests on is part of the finding
Three things carry one. A file you read, at its path and its line. A command you
ran, with what it printed. A mechanism you traced into an installed package. Say
which of the three it is. If you leave it unsaid, a finding from a CI file
weighs as much as one with a verified line.
Where one of the project's own commands would settle it, run it.
`typo3_project_describe` marks each command it lists **check**, **change** or
**unknown**, read off the declared body. A check reports and hands the code back
as it was. So even a task told not to change files runs it. The linter the
repository already declares is the cheapest evidence in it.
Do not run a change under that instruction. Name an unknown in the answer as
evidence that is available, and do not run it unasked. An unknown is a test
suite, a shell pipeline, a console command.
What a check prints is not the finding. The configuration that makes it fail is
still what the finding is about. The run takes that finding from derived to
established.
## What this server does not know
It does not read your working tree. You establish which files changed, which
branch you are on, and whether a path or an identifier still exists there. Then
pass the concrete paths back, because that turns a general convention into an
answer about this code.
## Query it in English
The knowledge is English and the match is lexical. So a query in another
language reaches the loanwords the two happen to share and nothing else.
Translate the subject before the call and the answer back afterwards, whatever
language you speak with the user.
Test strategy checklist#
# Test strategy checklist
Read this when you choose test layers, set up or repair their harnesses, review
coverage, or select commands. Use only the rows the task and the checkout
support.
## Verify the starting point
- Identify the layers the requested behavior needs.
- Run or inspect each layer's discovery command before you rely on it.
- Establish or repair missing prerequisites before you add coverage.
- Preserve a review-only boundary: report setup defects without changing them.
- Do not force behavior into an existing lower layer merely because the correct
layer has no harness yet.
## Select the layer
- Unit: isolated logic whose behavior needs neither TYPO3 bootstrap nor
persistence.
- Functional: services, configuration, database schema, repositories,
DataHandler, fixtures, or interaction between TYPO3 components.
- Browser: rendered journeys, backend interaction, JavaScript, or accessibility
behavior nothing below the UI can establish.
- Static quality: defects and style no test observes — static analysis, coding
standards, structural rules. Establish them when the task asks for them.
Extend them rather than invent them when it does not.
## Respect the functional frontend boundary
- A TYPO3 functional frontend subrequest proves server-side routing, rendering
and the generated response.
- An asset URL or AssetCollector tag in that response proves registration, not
that the browser loaded or executed the asset.
- A functional test does not execute JavaScript, apply CSS, calculate layout,
exercise focus behavior or inspect the browser accessibility tree.
- Use a browser test for an interactive claim. Describe untested interaction as
unverified rather than as frontend-tested.
- Documentation and configuration: validate examples and declarations directly.
Do not disguise a file-presence assertion as a behavioral test.
Prefer the lowest layer that observes the contract and would fail for the
reported defect. Record why a higher layer is necessary.
## Complete setup
- Resolve development dependencies against the package's declared TYPO3 and PHP
constraints. Do not copy a version from another project.
- Reuse installed template configuration and existing project conventions.
- Keep extension-owned unit and functional tests in the extension. Keep
browser-owned configuration in the project that serves the tested site.
- Establish a stable local command before CI, then make CI call that command.
- Keep a check command apart from the command that writes. Keep automatic
formatting inside the first-party paths the project intends it to touch.
- Fix a new static-analysis finding. A baseline records what was already there.
- Keep credentials and machine-specific URLs out of tracked configuration.
- Prove every newly established layer with a meaningful test or a real browser
spec. When no unit-testable behavior exists, report an empty discovered suite.
Do not manufacture behavior or a vacuous assertion.
- Treat a missing runner or environment as an infrastructure failure, not as a
failed product assertion.
## Select commands
1. Prefer the narrowest existing command that names the affected test or suite.
2. During setup, introduce one project-owned command per selected layer. Run it
before you wire CI.
3. Use a direct PHPUnit or browser-runner invocation only when its executable,
configuration, and target path exist in the checkout.
4. Then run the containing command `typo3_project_describe` declares, or the
repository's documented test setup.
5. Never translate TYPO3 core commands into extension commands by analogy.
6. Separate missing infrastructure or environment prerequisites from a failing
test.
## Coverage review
- Trace every claimed behavior to an existing test or a concrete missing case.
- Include success, boundary, failure, authorization, and persistence behavior
only where the production code exposes those branches.
- Check that fixtures are minimal, deterministic, and independent of local site
data, time, execution order, and external services.
- Name exact files and commands. Label anything you did not execute as
unverified.
markdown
# Test strategy checklist
Read this when you choose test layers, set up or repair their harnesses, review
coverage, or select commands. Use only the rows the task and the checkout
support.
## Verify the starting point
- Identify the layers the requested behavior needs.
- Run or inspect each layer's discovery command before you rely on it.
- Establish or repair missing prerequisites before you add coverage.
- Preserve a review-only boundary: report setup defects without changing them.
- Do not force behavior into an existing lower layer merely because the correct
layer has no harness yet.
## Select the layer
- Unit: isolated logic whose behavior needs neither TYPO3 bootstrap nor
persistence.
- Functional: services, configuration, database schema, repositories,
DataHandler, fixtures, or interaction between TYPO3 components.
- Browser: rendered journeys, backend interaction, JavaScript, or accessibility
behavior nothing below the UI can establish.
- Static quality: defects and style no test observes — static analysis, coding
standards, structural rules. Establish them when the task asks for them.
Extend them rather than invent them when it does not.
## Respect the functional frontend boundary
- A TYPO3 functional frontend subrequest proves server-side routing, rendering
and the generated response.
- An asset URL or AssetCollector tag in that response proves registration, not
that the browser loaded or executed the asset.
- A functional test does not execute JavaScript, apply CSS, calculate layout,
exercise focus behavior or inspect the browser accessibility tree.
- Use a browser test for an interactive claim. Describe untested interaction as
unverified rather than as frontend-tested.
- Documentation and configuration: validate examples and declarations directly.
Do not disguise a file-presence assertion as a behavioral test.
Prefer the lowest layer that observes the contract and would fail for the
reported defect. Record why a higher layer is necessary.
## Complete setup
- Resolve development dependencies against the package's declared TYPO3 and PHP
constraints. Do not copy a version from another project.
- Reuse installed template configuration and existing project conventions.
- Keep extension-owned unit and functional tests in the extension. Keep
browser-owned configuration in the project that serves the tested site.
- Establish a stable local command before CI, then make CI call that command.
- Keep a check command apart from the command that writes. Keep automatic
formatting inside the first-party paths the project intends it to touch.
- Fix a new static-analysis finding. A baseline records what was already there.
- Keep credentials and machine-specific URLs out of tracked configuration.
- Prove every newly established layer with a meaningful test or a real browser
spec. When no unit-testable behavior exists, report an empty discovered suite.
Do not manufacture behavior or a vacuous assertion.
- Treat a missing runner or environment as an infrastructure failure, not as a
failed product assertion.
## Select commands
1. Prefer the narrowest existing command that names the affected test or suite.
2. During setup, introduce one project-owned command per selected layer. Run it
before you wire CI.
3. Use a direct PHPUnit or browser-runner invocation only when its executable,
configuration, and target path exist in the checkout.
4. Then run the containing command `typo3_project_describe` declares, or the
repository's documented test setup.
5. Never translate TYPO3 core commands into extension commands by analogy.
6. Separate missing infrastructure or environment prerequisites from a failing
test.
## Coverage review
- Trace every claimed behavior to an existing test or a concrete missing case.
- Include success, boundary, failure, authorization, and persistence behavior
only where the production code exposes those branches.
- Check that fixtures are minimal, deterministic, and independent of local site
data, time, execution order, and external services.
- Name exact files and commands. Label anything you did not execute as
unverified.
PHPUnit unit and functional guidance#
# PHPUnit unit and functional guidance
Read this after you choose a PHPUnit layer. Let the checkout, the
`project-extension-tests` hint, and versioned documentation decide concrete
package versions, bootstrap APIs, configuration contents, and commands.
## Verify the harness
1. Inspect the harness before you change it. That is `composer.json`, the lock
file, the installed PHPUnit and `typo3/testing-framework`, and the PHPUnit
configuration. It is the bootstrap files, the Composer scripts, and CI.
2. Run the narrowest declared PHPUnit discovery or suite command. Tell a missing
executable, an invalid configuration, a bootstrap failure, and an unavailable
functional database apart from a failed assertion.
3. If configuration is absent or stale, take its current shape from the
installed testing framework or the versioned official documentation. Do not
copy another extension's dependency constraint. Do not point at the core mono
repository.
4. Preserve one command per useful layer. Give CI the same entrypoint that
passes locally rather than a second invocation you assemble apart.
5. When `typo3/testing-framework` is missing, use its official compatibility
information only to form candidates. Let Composer resolve the newest
candidate that intersects the package's TYPO3, PHP, PHPUnit,
minimum-stability, and lock constraints. Do not name or write a concrete
constraint until the solver has accepted it.
6. Select the functional database from existing project or CI infrastructure and
the behavior under test. SQLite is useful only when the production schema and
queries support it. It is not the default merely because it needs no service
container.
## Choose the folders
Keep an established repository layout. Where none exists, `typo3_rule_lookup`
with `documentId="extension/testing/phpunit"` holds the files, whole. Those are
`Build/UnitTests.xml`, `Build/FunctionalTests.xml` and the bootstrap beside
each. It says where they sit in a project, and what each needs after you write
it out.
- Keep runner configuration and bootstraps out of `Tests/`. They describe how
the suite runs rather than one tested behavior.
- Keep unit and functional trees separate, so each configuration discovers only
its own layer.
- Mirror the production subject below `Tests/Unit/` when that makes a test easy
to find. Organize functional tests by behavior or subsystem when several
production classes participate.
- Put fixtures beside the functional test or its small subsystem. Do not put
them in one extension-wide bucket whose ownership is unclear.
- If a project already uses another shape that works, adapt it. Do not move
files for cosmetic consistency. Calculate testsuite paths from the final
configuration location.
## Write the test
- Unit-test isolated behavior without a TYPO3 boot. Prefer real value objects
and collaborators. A test whose subject is only mocks is evidence about the
mock arrangement.
- Use a functional test for dependency injection, configuration, persistence,
repositories, TCA, DataHandler, routing, database schema, and server-side
frontend rendering.
- Load only the extensions the behavior needs, derived from the package
requirements and the subject under test.
- Keep fixtures beside the test, minimal, deterministic, and explicit about
their expected result. Avoid records from the developer's installation.
- Resolve services from the functional test container when the wiring is part of
the contract.
- Reset or isolate state that survives between tests. A test that passes alone
but fails in its containing suite is not finished.
- Add the smallest real assertion that proves the behavior. Never use a vacuous
assertion to certify the harness.
## Prove it
1. Run the new test or method alone.
2. Run its complete unit or functional suite.
3. Run both suites when shared bootstrap, dependencies, scripts, or CI changed.
4. Run the CI-equivalent command after local execution passes.
5. Report database or environment prerequisites apart from test results.
For a server-rendered frontend response, PHPUnit proves routing, configuration,
markup, and response behavior. It does not execute JavaScript, apply CSS, or
exercise a browser accessibility tree. Use the Playwright guide for those
claims.
markdown
# PHPUnit unit and functional guidance
Read this after you choose a PHPUnit layer. Let the checkout, the
`project-extension-tests` hint, and versioned documentation decide concrete
package versions, bootstrap APIs, configuration contents, and commands.
## Verify the harness
1. Inspect the harness before you change it. That is `composer.json`, the lock
file, the installed PHPUnit and `typo3/testing-framework`, and the PHPUnit
configuration. It is the bootstrap files, the Composer scripts, and CI.
2. Run the narrowest declared PHPUnit discovery or suite command. Tell a missing
executable, an invalid configuration, a bootstrap failure, and an unavailable
functional database apart from a failed assertion.
3. If configuration is absent or stale, take its current shape from the
installed testing framework or the versioned official documentation. Do not
copy another extension's dependency constraint. Do not point at the core mono
repository.
4. Preserve one command per useful layer. Give CI the same entrypoint that
passes locally rather than a second invocation you assemble apart.
5. When `typo3/testing-framework` is missing, use its official compatibility
information only to form candidates. Let Composer resolve the newest
candidate that intersects the package's TYPO3, PHP, PHPUnit,
minimum-stability, and lock constraints. Do not name or write a concrete
constraint until the solver has accepted it.
6. Select the functional database from existing project or CI infrastructure and
the behavior under test. SQLite is useful only when the production schema and
queries support it. It is not the default merely because it needs no service
container.
## Choose the folders
Keep an established repository layout. Where none exists, `typo3_rule_lookup`
with `documentId="extension/testing/phpunit"` holds the files, whole. Those are
`Build/UnitTests.xml`, `Build/FunctionalTests.xml` and the bootstrap beside
each. It says where they sit in a project, and what each needs after you write
it out.
- Keep runner configuration and bootstraps out of `Tests/`. They describe how
the suite runs rather than one tested behavior.
- Keep unit and functional trees separate, so each configuration discovers only
its own layer.
- Mirror the production subject below `Tests/Unit/` when that makes a test easy
to find. Organize functional tests by behavior or subsystem when several
production classes participate.
- Put fixtures beside the functional test or its small subsystem. Do not put
them in one extension-wide bucket whose ownership is unclear.
- If a project already uses another shape that works, adapt it. Do not move
files for cosmetic consistency. Calculate testsuite paths from the final
configuration location.
## Write the test
- Unit-test isolated behavior without a TYPO3 boot. Prefer real value objects
and collaborators. A test whose subject is only mocks is evidence about the
mock arrangement.
- Use a functional test for dependency injection, configuration, persistence,
repositories, TCA, DataHandler, routing, database schema, and server-side
frontend rendering.
- Load only the extensions the behavior needs, derived from the package
requirements and the subject under test.
- Keep fixtures beside the test, minimal, deterministic, and explicit about
their expected result. Avoid records from the developer's installation.
- Resolve services from the functional test container when the wiring is part of
the contract.
- Reset or isolate state that survives between tests. A test that passes alone
but fails in its containing suite is not finished.
- Add the smallest real assertion that proves the behavior. Never use a vacuous
assertion to certify the harness.
## Prove it
1. Run the new test or method alone.
2. Run its complete unit or functional suite.
3. Run both suites when shared bootstrap, dependencies, scripts, or CI changed.
4. Run the CI-equivalent command after local execution passes.
5. Report database or environment prerequisites apart from test results.
For a server-rendered frontend response, PHPUnit proves routing, configuration,
markup, and response behavior. It does not execute JavaScript, apply CSS, or
exercise a browser accessibility tree. Use the Playwright guide for those
claims.
Playwright browser guidance#
# Playwright browser guidance
Read this after you choose a browser layer. Keep the runner with the project
that serves the TYPO3 site. Let the checkout and the current official Playwright
documentation decide dependency versions, supported configuration fields, and
browser installation commands.
## Verify the harness
1. Inspect the harness before you touch it. That is the project and frontend
package manifests, the lock file, the Playwright configuration, and the test
directories. It is the scripts, the ignored artifacts, and CI. Inspect the
command that serves the site.
2. Establish the real mounted URL and whether the journey needs backend or
frontend authentication. Do not invent a page identifier. Do not certify an
unmounted content element.
3. Run test discovery and one existing spec before you edit the harness. Tell
runner installation, browser availability, site reachability, authentication,
and assertion failures apart.
4. Choose where browsers execute from evidence in the project: host, development
container, or dedicated browser image. Keep the site URL and the artifact
paths reachable from that execution environment.
5. If the checkout does not decide browser execution, leave the topology open.
Local and CI reachability, browser persistence, and the project's container
policy decide it later. Do not turn a generic host or DDEV preference into
project evidence.
## Choose the folders
Keep an established repository layout. Where none exists, `typo3_rule_lookup`
with `documentId="project/testing/playwright"` holds the files, whole. Those are
`Build/playwright.config.ts`, the login setup and a spec per project. The page
adds the environment they read the site from, and what you do not commit.
- Project-owned describes the suite's lifecycle, not necessarily the repository
root. Reuse an existing frontend package manifest when it belongs to this
deployed project and can own browser commands. Do not create a second manifest
only to move Playwright closer to the root. If the only manifest belongs to a
reusable extension, establish an explicit project test package instead.
- Point `testDir` at the chosen browser-test root. Do not rely on accidental
discovery across PHP or frontend unit tests.
- Keep setup files distinct from journey specs. Then express their order through
Playwright project dependencies.
- Keep reusable browser fixtures and page objects apart from test cases only
after a second spec needs them.
- Store accepted visual baselines beside their specs or under an explicit
snapshot path. Commit only those baselines.
- If the repository already uses `Tests/Browser/`, `e2e/`, or a frontend-package
directory, keep it and map the same ownership boundaries there.
## Establish or repair it
- Put configuration and specs at the project level unless the repository already
owns them elsewhere. A standalone extension cannot prove a rendered journey
without a site that mounts it.
- Read the base URL from the environment or the project configuration. Do not
commit a developer-specific host.
- Add stable package scripts for a targeted run, the normal suite, and any
accepted snapshot-update workflow before you wire CI.
- Keep reports, traces, screenshots, videos, and temporary authentication state
in declared artifact or ignored paths. Commit reference snapshots only when
the project uses visual regression tests on purpose.
- Use setup dependencies or fixtures for shared authenticated state. Do not log
in on its own in every spec.
- Enable only the browsers and projects the task or the support policy requires.
More combinations are not evidence when nobody runs or maintains them.
## Write the spec
- Test a user-visible journey or a browser-only contract. That is navigation,
authentication, form behavior, JavaScript interaction, focus, responsive
behavior, visual output, or accessibility.
- Prefer role, label, and other user-facing locators over DOM structure or CSS
implementation details.
- Wait for observable conditions rather than fixed timeouts.
- Keep test data deterministic. Clean up the state the spec creates.
- Add an accessibility scan to the relevant mounted page or journey when the
task makes an accessibility claim. Do not disable a rule merely to make the
first run green.
- Review visual differences before you update snapshots. Never accept baselines
blindly.
## Prove it
1. Run the new spec alone against the real served site.
2. Confirm that the expected report, trace, screenshot, or snapshot path works.
3. Run the containing browser project or suite.
4. Run the same script CI will call.
5. Report untested browsers, unavailable URLs, and environment prerequisites as
unverified rather than passed.
markdown
# Playwright browser guidance
Read this after you choose a browser layer. Keep the runner with the project
that serves the TYPO3 site. Let the checkout and the current official Playwright
documentation decide dependency versions, supported configuration fields, and
browser installation commands.
## Verify the harness
1. Inspect the harness before you touch it. That is the project and frontend
package manifests, the lock file, the Playwright configuration, and the test
directories. It is the scripts, the ignored artifacts, and CI. Inspect the
command that serves the site.
2. Establish the real mounted URL and whether the journey needs backend or
frontend authentication. Do not invent a page identifier. Do not certify an
unmounted content element.
3. Run test discovery and one existing spec before you edit the harness. Tell
runner installation, browser availability, site reachability, authentication,
and assertion failures apart.
4. Choose where browsers execute from evidence in the project: host, development
container, or dedicated browser image. Keep the site URL and the artifact
paths reachable from that execution environment.
5. If the checkout does not decide browser execution, leave the topology open.
Local and CI reachability, browser persistence, and the project's container
policy decide it later. Do not turn a generic host or DDEV preference into
project evidence.
## Choose the folders
Keep an established repository layout. Where none exists, `typo3_rule_lookup`
with `documentId="project/testing/playwright"` holds the files, whole. Those are
`Build/playwright.config.ts`, the login setup and a spec per project. The page
adds the environment they read the site from, and what you do not commit.
- Project-owned describes the suite's lifecycle, not necessarily the repository
root. Reuse an existing frontend package manifest when it belongs to this
deployed project and can own browser commands. Do not create a second manifest
only to move Playwright closer to the root. If the only manifest belongs to a
reusable extension, establish an explicit project test package instead.
- Point `testDir` at the chosen browser-test root. Do not rely on accidental
discovery across PHP or frontend unit tests.
- Keep setup files distinct from journey specs. Then express their order through
Playwright project dependencies.
- Keep reusable browser fixtures and page objects apart from test cases only
after a second spec needs them.
- Store accepted visual baselines beside their specs or under an explicit
snapshot path. Commit only those baselines.
- If the repository already uses `Tests/Browser/`, `e2e/`, or a frontend-package
directory, keep it and map the same ownership boundaries there.
## Establish or repair it
- Put configuration and specs at the project level unless the repository already
owns them elsewhere. A standalone extension cannot prove a rendered journey
without a site that mounts it.
- Read the base URL from the environment or the project configuration. Do not
commit a developer-specific host.
- Add stable package scripts for a targeted run, the normal suite, and any
accepted snapshot-update workflow before you wire CI.
- Keep reports, traces, screenshots, videos, and temporary authentication state
in declared artifact or ignored paths. Commit reference snapshots only when
the project uses visual regression tests on purpose.
- Use setup dependencies or fixtures for shared authenticated state. Do not log
in on its own in every spec.
- Enable only the browsers and projects the task or the support policy requires.
More combinations are not evidence when nobody runs or maintains them.
## Write the spec
- Test a user-visible journey or a browser-only contract. That is navigation,
authentication, form behavior, JavaScript interaction, focus, responsive
behavior, visual output, or accessibility.
- Prefer role, label, and other user-facing locators over DOM structure or CSS
implementation details.
- Wait for observable conditions rather than fixed timeouts.
- Keep test data deterministic. Clean up the state the spec creates.
- Add an accessibility scan to the relevant mounted page or journey when the
task makes an accessibility claim. Do not disable a rule merely to make the
first run green.
- Review visual differences before you update snapshots. Never accept baselines
blindly.
## Prove it
1. Run the new spec alone against the real served site.
2. Confirm that the expected report, trace, screenshot, or snapshot path works.
3. Run the containing browser project or suite.
4. Run the same script CI will call.
5. Report untested browsers, unavailable URLs, and environment prerequisites as
unverified rather than passed.
Static analysis and coding standards guidance#
# Static analysis and coding standards guidance
Read this after you choose the static-quality layer. It covers analysis, coding
standards, and the lint or normalisation steps that run beside them. The
checkout, the package's declared TYPO3 and PHP range, and versioned
documentation decide the concrete facts. Those are package versions, rule sets,
configuration contents, and commands.
## Verify what is already there
1. Inspect what is there before you change any of it. That is the package
manifests, the lock file, the installed analysers and fixers, and their
configuration and rule sets. It is the existing baselines, the Composer
scripts, the development environment, and CI. Half an infrastructure is the
ordinary case. That is a fixer without an analyser, a lint step without
either, a configuration nothing calls.
2. Run every check that already exists, unchanged, and record its output. You
measure every later claim against that run. A first analyser report on a
project that never ran one is a list of findings rather than a regression.
3. Separate a missing executable, an unreadable configuration, and a real
finding. Only the third says anything about the code.
4. Read what the existing configuration excludes on purpose before you widen it.
A generated directory, a vendored library, or a fixture tree of broken code
is a decision, not an oversight.
5. For a review-only request this is the whole workflow. Report what is missing
or unenforced and change nothing.
## What a complete surface covers
This is the expectation the checkout is measured against. It names each check by
what it establishes rather than by the tool behind it. What the package ships
decides which of them apply. A check whose subject the extension does not ship
is absent for a reason rather than missing.
- **Syntax** — every shipped PHP file parses on every PHP version the package
declares. Run `php -l` through a lint runner such as `overtrue/phplint` or
`php-parallel-lint/php-parallel-lint`, so one command covers the tree.
- **Static analysis** — types, unreachable code, and calls that cannot succeed.
Run `phpstan/phpstan` against the extension's own paths, which is what the
core runs on itself. `GeneralUtility::makeInstance()` and its neighbours carry
`@template` annotations. So the analyser gets its types from the installed
core rather than from a TYPO3-specific analyser extension.
Beside it, `phpstan/extension-installer` wires up whatever extensions the
project installed. `phpstan/phpstan-phpunit` belongs beside a PHPUnit suite.
`bnf/phpstan-psr-container` types the container's `get()` calls, and
`phpstan/phpstan-deprecation-rules` reports use of deprecated API. Establish
that a TYPO3-specific analyser extension is still maintained before adding
one. Several are not, and an abandoned extension on a current core produces
false findings instead of types. `vimeo/psalm` is the alternative where a
project already runs it.
Which packages to require is the whole of what this page decides about the
analyser. What goes into its configuration is not. Ask `typo3_hint_lookup`
with `id=extension-static-analysis`. It answers where the file belongs, which
include it carries, and the constants an extension's analysis never sees. It
answers which manifest you exclude rather than fix, the cache directory, the
level, and what a baseline is for. It reads that off the packages that
configure themselves this way. So ask it rather than recall a configuration
from another project.
- **Coding standards** — the TYPO3 coding guidelines as the project applies
them. Run `friendsofphp/php-cs-fixer` with the `typo3/coding-standards` rule
set, which also owns the file header the guidelines require. Run
`editorconfig-checker` where the repository ships an `.editorconfig`. Run
`squizlabs/php_codesniffer` with `phpcompatibility/php-compatibility` where
you have to prove the declared PHP range without a matrix.
- **Manifests and dependencies** — `composer validate` on the extension's own
manifest. It also checks the manifest against what the extension declares
about itself elsewhere. `composer audit` for advisories against what it
requires. `ergebnis/composer-normalize` where the project keeps its manifest
normalised.
- **Shipped configuration and data** — the files the package ships. The XLIFF
linter `symfony/translation` ships validates against the schema rather than
only parses the XML. A TYPO3 project usually has it installed. A YAML lint
such as `j13k/yaml-lint` or the framework's own `lint:yaml` covers
configuration. `helmich/typo3-typoscript-lint` covers TypoScript.
Two of them have to learn about this project before their verdict is worth
anything. `typo3_hint_lookup` with `id=language-files` says what the XLIFF
linter needs before it accepts a file named the way TYPO3 names one. A linter
ships a configuration of its own and merges yours over it. So what it reports
and what it advises are its defaults rather than this project's conventions.
Advice written for an older TYPO3 is still advice the tool gives today.
Fluid templates have no established linter. The functional tests that render
them prove them. Say so rather than invent a check for them.
- **Shipped frontend assets** — where the package ships JavaScript, TypeScript
or CSS. Run `eslint` for the scripts, with `@typescript-eslint` where the
sources are TypeScript. Run `stylelint` for the stylesheets, with
`stylelint-scss` and `stylelint-order` where Sass compiles. A formatter such
as `prettier` goes on the fix side and never into the check. Lint the sources
the repository maintains rather than the compiled bundle below
`Resources/Public/`. A finding in generated output is a finding about the
build step that produced it.
Declare each of them as a script in the package's own `package.json`. Then the
same one command exists locally and in CI. Take the Node version from what the
project declares rather than from the machine that happens to run it. Run each
one over the files it guards before you report the entry as covered. A linter
that finds nothing in the only sources the package ships is a gap dressed as
coverage. That is the standard this page already applies to a matrix whose
cells run only version-independent steps.
Read the names as the default per check where the checkout covers it with
nothing, never as a replacement for what it already runs. A project with another
analyser, another lint runner or its own wrapper around one has answered that
question already. A second tool for the same check is a second answer to
maintain. Versions come from the solver and from what the package's range
allows, never from this list.
`rector/rector` with `ssch/typo3-rector` reads like a check in its dry-run mode
and is not one. It proposes migrations, which belong to an upgrade task rather
than to the surface CI has to keep green.
Then say which axis each one has, because that decides where it runs. Syntax and
analysis depend on the PHP and TYPO3 combination and belong in the matrix. A
standards, manifest or format check is version-independent, and one run of it
proves as much as sixteen. A matrix whose every cell runs only
version-independent steps proves that the files parse and nothing more. Say so,
with what it costs and what it does not buy.
## Resolve the dependencies
1. Form candidates from the package's own declared TYPO3 and PHP range. Never
form them from another project's manifest or from the core's build tooling.
2. Let Composer resolve the newest candidate that intersects those constraints
together with what is already installed. Do not write a concrete constraint
until the solver has accepted it.
3. Take a rule set from a package the project already requires before you add
another one. Every added package is a further thing that has to stay current
across the whole declared range. An abandoned one stops doing that.
4. Keep every one of them in the development requirements.
5. Where the solver refuses one, read what it refused rather than which TYPO3
version the project runs. A check tool has to meet what this project
resolved. A package nothing here pins can sit a major ahead of what the
framework asks for. So the same tool goes into one project and not into the
next.
Where it cannot meet it, the tool gets an installation of its own. That is a
second manifest below the build directory with its own vendor directory. The
project-owned command calls that binary. The check exists either way. What
moves is where the tool's own dependencies live.
## Establish one command per check
1. Give each check one stable project-owned command. Declare it where the
project already declares its commands. Name it for what it checks rather than
for the binary behind it.
2. Keep the check and the fix apart. A check reports and fails; a fix writes.
One command that rewrites the tree on the way to its verdict is not a check.
CI cannot call it.
3. Keep automatic formatting inside the first-party paths the project intends it
to touch. Vendored code, generated output, other packages' files, and
fixtures that assert exact bytes stay outside the fixer's paths. Confirm that
with the fixer's own dry run before the first write and with the tree's
status afterwards.
4. Point analysis at the paths the extension owns, at the level the project can
hold today. A level chosen for the eventual state produces a wall of findings
nobody works off.
5. Run each command locally until it passes. Then make CI call that same
command. A CI step assembled apart proves something the developer cannot
reproduce.
## Work the findings off
- Fix the finding. A baseline records what was already there on the day somebody
wrote it. It never receives an error the change in hand introduced.
- Where a baseline exists, read it as a work list with an owner and a horizon.
The horizon is usually the release that drops the oldest supported version.
Say which of its entries the current change retires.
- A suppression that is correct while the package supports an older version is
evidence rather than debt. Establish what it is there for, and what would
remove it, before you propose that it goes.
- Keep a formatting pass in its own commit, apart from a behavioural change.
Nobody can review a diff that mixes both.
- Where you introduce a check onto a repository that does not yet pass it, the
conformance commits come first. The commit that adds the check comes last.
Then no commit fails the check it introduces. The obvious split does the
opposite. Tooling first leaves the new check on a tree the conformance pass
has not reached yet. Verify it: run the check at the new HEAD.
- Report a finding in code the task does not touch. Do not fix it quietly beside
the requested work.
## Prove it
1. Run each check on the narrowest scope it supports, then on its full target.
2. Run the fix command, run the check again, and inspect the tree for files
outside the intended scope.
3. Run the CI-equivalent commands after the local commands pass.
4. Report the exact commands, their results, and the files the fixer changed.
Report every check you did not run, with the reason.
markdown
# Static analysis and coding standards guidance
Read this after you choose the static-quality layer. It covers analysis, coding
standards, and the lint or normalisation steps that run beside them. The
checkout, the package's declared TYPO3 and PHP range, and versioned
documentation decide the concrete facts. Those are package versions, rule sets,
configuration contents, and commands.
## Verify what is already there
1. Inspect what is there before you change any of it. That is the package
manifests, the lock file, the installed analysers and fixers, and their
configuration and rule sets. It is the existing baselines, the Composer
scripts, the development environment, and CI. Half an infrastructure is the
ordinary case. That is a fixer without an analyser, a lint step without
either, a configuration nothing calls.
2. Run every check that already exists, unchanged, and record its output. You
measure every later claim against that run. A first analyser report on a
project that never ran one is a list of findings rather than a regression.
3. Separate a missing executable, an unreadable configuration, and a real
finding. Only the third says anything about the code.
4. Read what the existing configuration excludes on purpose before you widen it.
A generated directory, a vendored library, or a fixture tree of broken code
is a decision, not an oversight.
5. For a review-only request this is the whole workflow. Report what is missing
or unenforced and change nothing.
## What a complete surface covers
This is the expectation the checkout is measured against. It names each check by
what it establishes rather than by the tool behind it. What the package ships
decides which of them apply. A check whose subject the extension does not ship
is absent for a reason rather than missing.
- **Syntax** — every shipped PHP file parses on every PHP version the package
declares. Run `php -l` through a lint runner such as `overtrue/phplint` or
`php-parallel-lint/php-parallel-lint`, so one command covers the tree.
- **Static analysis** — types, unreachable code, and calls that cannot succeed.
Run `phpstan/phpstan` against the extension's own paths, which is what the
core runs on itself. `GeneralUtility::makeInstance()` and its neighbours carry
`@template` annotations. So the analyser gets its types from the installed
core rather than from a TYPO3-specific analyser extension.
Beside it, `phpstan/extension-installer` wires up whatever extensions the
project installed. `phpstan/phpstan-phpunit` belongs beside a PHPUnit suite.
`bnf/phpstan-psr-container` types the container's `get()` calls, and
`phpstan/phpstan-deprecation-rules` reports use of deprecated API. Establish
that a TYPO3-specific analyser extension is still maintained before adding
one. Several are not, and an abandoned extension on a current core produces
false findings instead of types. `vimeo/psalm` is the alternative where a
project already runs it.
Which packages to require is the whole of what this page decides about the
analyser. What goes into its configuration is not. Ask `typo3_hint_lookup`
with `id=extension-static-analysis`. It answers where the file belongs, which
include it carries, and the constants an extension's analysis never sees. It
answers which manifest you exclude rather than fix, the cache directory, the
level, and what a baseline is for. It reads that off the packages that
configure themselves this way. So ask it rather than recall a configuration
from another project.
- **Coding standards** — the TYPO3 coding guidelines as the project applies
them. Run `friendsofphp/php-cs-fixer` with the `typo3/coding-standards` rule
set, which also owns the file header the guidelines require. Run
`editorconfig-checker` where the repository ships an `.editorconfig`. Run
`squizlabs/php_codesniffer` with `phpcompatibility/php-compatibility` where
you have to prove the declared PHP range without a matrix.
- **Manifests and dependencies** — `composer validate` on the extension's own
manifest. It also checks the manifest against what the extension declares
about itself elsewhere. `composer audit` for advisories against what it
requires. `ergebnis/composer-normalize` where the project keeps its manifest
normalised.
- **Shipped configuration and data** — the files the package ships. The XLIFF
linter `symfony/translation` ships validates against the schema rather than
only parses the XML. A TYPO3 project usually has it installed. A YAML lint
such as `j13k/yaml-lint` or the framework's own `lint:yaml` covers
configuration. `helmich/typo3-typoscript-lint` covers TypoScript.
Two of them have to learn about this project before their verdict is worth
anything. `typo3_hint_lookup` with `id=language-files` says what the XLIFF
linter needs before it accepts a file named the way TYPO3 names one. A linter
ships a configuration of its own and merges yours over it. So what it reports
and what it advises are its defaults rather than this project's conventions.
Advice written for an older TYPO3 is still advice the tool gives today.
Fluid templates have no established linter. The functional tests that render
them prove them. Say so rather than invent a check for them.
- **Shipped frontend assets** — where the package ships JavaScript, TypeScript
or CSS. Run `eslint` for the scripts, with `@typescript-eslint` where the
sources are TypeScript. Run `stylelint` for the stylesheets, with
`stylelint-scss` and `stylelint-order` where Sass compiles. A formatter such
as `prettier` goes on the fix side and never into the check. Lint the sources
the repository maintains rather than the compiled bundle below
`Resources/Public/`. A finding in generated output is a finding about the
build step that produced it.
Declare each of them as a script in the package's own `package.json`. Then the
same one command exists locally and in CI. Take the Node version from what the
project declares rather than from the machine that happens to run it. Run each
one over the files it guards before you report the entry as covered. A linter
that finds nothing in the only sources the package ships is a gap dressed as
coverage. That is the standard this page already applies to a matrix whose
cells run only version-independent steps.
Read the names as the default per check where the checkout covers it with
nothing, never as a replacement for what it already runs. A project with another
analyser, another lint runner or its own wrapper around one has answered that
question already. A second tool for the same check is a second answer to
maintain. Versions come from the solver and from what the package's range
allows, never from this list.
`rector/rector` with `ssch/typo3-rector` reads like a check in its dry-run mode
and is not one. It proposes migrations, which belong to an upgrade task rather
than to the surface CI has to keep green.
Then say which axis each one has, because that decides where it runs. Syntax and
analysis depend on the PHP and TYPO3 combination and belong in the matrix. A
standards, manifest or format check is version-independent, and one run of it
proves as much as sixteen. A matrix whose every cell runs only
version-independent steps proves that the files parse and nothing more. Say so,
with what it costs and what it does not buy.
## Resolve the dependencies
1. Form candidates from the package's own declared TYPO3 and PHP range. Never
form them from another project's manifest or from the core's build tooling.
2. Let Composer resolve the newest candidate that intersects those constraints
together with what is already installed. Do not write a concrete constraint
until the solver has accepted it.
3. Take a rule set from a package the project already requires before you add
another one. Every added package is a further thing that has to stay current
across the whole declared range. An abandoned one stops doing that.
4. Keep every one of them in the development requirements.
5. Where the solver refuses one, read what it refused rather than which TYPO3
version the project runs. A check tool has to meet what this project
resolved. A package nothing here pins can sit a major ahead of what the
framework asks for. So the same tool goes into one project and not into the
next.
Where it cannot meet it, the tool gets an installation of its own. That is a
second manifest below the build directory with its own vendor directory. The
project-owned command calls that binary. The check exists either way. What
moves is where the tool's own dependencies live.
## Establish one command per check
1. Give each check one stable project-owned command. Declare it where the
project already declares its commands. Name it for what it checks rather than
for the binary behind it.
2. Keep the check and the fix apart. A check reports and fails; a fix writes.
One command that rewrites the tree on the way to its verdict is not a check.
CI cannot call it.
3. Keep automatic formatting inside the first-party paths the project intends it
to touch. Vendored code, generated output, other packages' files, and
fixtures that assert exact bytes stay outside the fixer's paths. Confirm that
with the fixer's own dry run before the first write and with the tree's
status afterwards.
4. Point analysis at the paths the extension owns, at the level the project can
hold today. A level chosen for the eventual state produces a wall of findings
nobody works off.
5. Run each command locally until it passes. Then make CI call that same
command. A CI step assembled apart proves something the developer cannot
reproduce.
## Work the findings off
- Fix the finding. A baseline records what was already there on the day somebody
wrote it. It never receives an error the change in hand introduced.
- Where a baseline exists, read it as a work list with an owner and a horizon.
The horizon is usually the release that drops the oldest supported version.
Say which of its entries the current change retires.
- A suppression that is correct while the package supports an older version is
evidence rather than debt. Establish what it is there for, and what would
remove it, before you propose that it goes.
- Keep a formatting pass in its own commit, apart from a behavioural change.
Nobody can review a diff that mixes both.
- Where you introduce a check onto a repository that does not yet pass it, the
conformance commits come first. The commit that adds the check comes last.
Then no commit fails the check it introduces. The obvious split does the
opposite. Tooling first leaves the new check on a tree the conformance pass
has not reached yet. Verify it: run the check at the new HEAD.
- Report a finding in code the task does not touch. Do not fix it quietly beside
the requested work.
## Prove it
1. Run each check on the narrowest scope it supports, then on its full target.
2. Run the fix command, run the check again, and inspect the tree for files
outside the intended scope.
3. Run the CI-equivalent commands after the local commands pass.
4. Report the exact commands, their results, and the files the fixer changed.
Report every check you did not run, with the reason.