---
title: "TYPO3 Extension Health"
description: "Skill: typo3-extension-health"
canonical: index.html
navigation-title: "TYPO3 Extension Health"
---

<a id="typo3-extension-health"></a>

# TYPO3 Extension Health

- [Markdown source](#markdown-source)
- [References](#references)
  - [Where every task starts](#where-every-task-starts)
  - [Conformance audit checklist](#conformance-audit-checklist)

**Skill:** `typo3-extension-health`

Review a TYPO3 project, sitepackage or extension against its checkout and active
installation and put it right — "look over my repository and fix it": TCA,
services, backend modules, content elements, site sets, TypoScript, Fluid,
labels, icons, security boundaries and deprecated APIs. The audit reports first;
nothing is changed before the list is agreed.

<a id="markdown-source"></a>

## Markdown source

```markdown
---
name: typo3-extension-health
description: 'Review a TYPO3 project, sitepackage or extension against its checkout and active installation and put it right — "look over my repository and fix it": TCA, services, backend modules, content elements, site sets, TypoScript, Fluid, labels, icons, security boundaries and deprecated APIs. The audit reports first; nothing is changed before the list is agreed.'
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 Health

Establish what is wrong with a package against evidence. Then work the agreed
list off until it is empty. Keep this skill as routing, assessment and
sequencing method. Do not embed versioned TYPO3 facts.

The two halves are one workflow, and the gate between them is step 5. The audit
answers a request that asked for a review. A request that asked for changes
passes through the same report on its way to them. Edit nothing before that
gate, whatever the request asked for.

## Establish scope and evidence

1. Work through [references/base.md](references/base.md). It fixes the order
   every task here starts in, and an assessment is where that order matters
   most. A rule you fetch after the reading confirms a verdict instead of a test
   of it.
2. Read [references/checklist.md](references/checklist.md) for the audit
   surfaces, the finding gate, and the severity rubric.
3. Write the surface list down before you open a single file. Take the
   checklist's surfaces, narrowed to the ones this kind of checkout can have. It
   is the work list. The coverage the report closes on is this same list with
   every entry answered.
4. Where the request names what it is about (security only, configuration only,
   one subsystem), read the surfaces it names. Mark the rest **not requested**
   on that same list. The request narrows the reading, never the list. An entry
   nobody asked about costs one line. A dropped entry is what leaves the reader
   unable to ask for the rest. A request that names no surface is not a focused
   one, and you read every entry.

A surface is in scope because the checklist names it, not because the file tree
shows it. A file list first inverts that. `find` cannot show a manual nobody
wrote, a test that does not exist, or a documentation tree that is absent. So
the surfaces it hides are exactly the ones whose absence is the finding. Derive
the list from the checklist and `typo3_extension_describe`. Then let the reading
answer it.

## Ask before you judge, on every surface in scope

Scope says which surfaces are in play, and the reading says what is there.
Neither says whether it is right. That comes from the owner of the convention.
Ask for it **before** you form a view of the subsystem, not to confirm one you
already have:

- `typo3_hint_lookup` with the subsystem's concrete paths and a short English
  description. One query per surface in scope. A single broad query is not
  subsystem evidence. Ask for a surface the checkout has no files for by its
  hint id instead. That is the surface whose absence is the finding, and the one
  whose paths you cannot pass.
- The runtime lookup that owns the surface, where one exists. The base names
  them and what each adds after the extension answer. An audit makes that call
  per surface in scope. An answer off what the package declares is about the
  package rather than about this installation.
- `typo3_documentation_lookup` with several short English queries and the target
  version, where an official API or configuration detail decides the finding.
  Ask it for every "does this still work here" a surface raises. The base says
  why the changelog cannot answer that one.
- `typo3_ter_lookup` with the extension key, on the package surface. What the
  Extension Repository has published is the one thing about this package the
  package cannot answer. `ext_emconf.php` names the version under release and
  goes on with that name afterwards. So a published checkout reads exactly like
  an unpublished one. A key with no publication under it is an answer rather
  than a finding. An extension distributed through Composer alone has no
  registration here.

Read the checkout for what none of those can know. That is the files themselves,
the registrations, the tests, the documentation, and the conventions the project
has settled into.

Then read every returned rule 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. The
project's own habits are part of what you assess, so consistency with them
establishes nothing.

Do not report the absence of an optional subsystem as a defect. But a surface
that is present and nobody asked about is **unassessed**, and unassessed is not
clean. Say so in the result. A defect nobody looked for and an absent defect
read the same in a report that does not separate them. Tell a verified violation
from a recommendation and from missing evidence.

Report the deprecation sweep the base fixes the same way. A review that found
nothing says the sweep ran and came back empty, with the majors it covered. A
reader cannot tell a sweep that shows only when it produces a finding from one
that never ran. The surface it covers is the one whose silence reads as a clean
bill for the next major.

A finding that depends on a version holds on the installed one, and the package
declares a range. The base's first step reports `coreConstraint` beside
`typo3Version`. So where the constraint names a major the installation does not
supply, that gap is on every such finding. To close it is a whole procedure this
skill does not repeat:

- `typo3_rule_lookup` with
  `documentId="extension/compatibility/a-declared-major-that-is-not-installed"`.
  It says whether the API the code calls is there on the other major. It is a
  reading of that branch, and the question is per symbol rather than per
  package.
- `typo3_rule_lookup` with
  `documentId="extension/compatibility/running-on-a-declared-major-that-is-not-installed"`.
  That is where you have to run the claim instead of read it. It says what the
  repository's own pipeline already covers, which you read before you install
  anything. It says how a Composer root of its own stands the other major up
  beside the installation.

An audit that runs neither reports the gap rather than the range. The finding
holds where you established it, and you name the majors you did not establish it
on. Where the constraint names the installed major alone, this is one line on
the coverage list and no call.

## Report

Order findings by severity and include:

1. the concrete file or runtime registration;
2. the observed behavior or configuration;
3. the applicable MCP or official-documentation evidence;
4. the consequence;
5. a scoped remediation and relevant project check.

Beside them, report what you raised while you read and dropped, with what
dropped it. A candidate let go in silence and a surface nobody opened leave the
same trace in the report. The checklist's *What a dropped candidate owes* is the
bar each one has to meet. That includes the one you could neither establish nor
disprove, which you report as open rather than dropped.

Close on coverage rather than on a summary. That is the surface list written in
step 3, every entry marked assessed, unassessed or not requested, clean ones
briefly. It is that list and not a recollection at the end. A summary from
memory reports what the session noticed it skipped, never what it never reached.

Without the list a thorough report and a narrow one look alike. The cheapest way
to look thorough is to examine less. Unassessed and not requested both mean that
you established nothing there, and they are not the same thing. One is this
review's gap, the other is what the request left out. Say which of the two per
entry, and let neither read as clean.

**The report is markdown the reader can copy, and the answer is where it goes.**
The findings and the coverage list together make it long, and length makes the
form matter. Somebody carries an audit into an issue, a ticket or a chat, and
rendered output does not survive the move. Write it to a file only where the
caller asks for one, at a path outside the checkout under assessment.

**A request that asked for a review ends here.** Report, name the owning
workflow per finding as step 10 says, and stop. An instruction to change the
package asks for the rest: "fix it", "put that right", "do the first three". It
arrives after the report, which is where you leave a review.

**A question about a finding is not that instruction.** "Is that really
breaking", "why is that one high", "are you sure": each asks you to defend the
report. It wants the evidence rather than a change. Where the sentence could be
either, ask which the user meant.

Stopping at findings is not stopping at reading. The commands
`typo3_project_describe` marks as checks hand the code back as it was. So an
audit told not to change files runs them. It reports what they printed.

## The list is written down and agreed before anything changes

5. Write one item per finding, in the report's own severity order. Each item
   carries what the finding is and the file or registration it is about. It
   carries its severity, the workflow that owns it, and a state. A finding the
   report left open, and a surface it reported unassessed, are items too. Their
   work is to establish what the audit could not.
6. Establish what the repository already carries against each item, and write it
   on the item before you show the list. A finding already fixed on a branch
   nobody merged reads on the list exactly like one nothing has touched. The
   maintainer is then the only party who can tell them apart.

   The surface is wider than the open pull requests: branches pushed without
   one, and the maintained release lines. The branch with no pull request is the
   one that gets missed. It is where a maintainer's own unfinished work sits.
   Beside its own state, the item carries one of three. It is **untouched**, or
   **carried** by a named branch or pull request. Or it is **colliding** with an
   unmerged change that reaches those places without a settled finding.
7. Show that list whole. Let the maintainer cut items, reorder them or stop,
   before you make a single change. That is what the whole order exists for, and
   it is the one step nothing downstream recovers. Do not begin the work while
   you show the list. A list that arrives together with the changes it produced
   is one nobody had the chance to disagree with.
8. Keep the list in the session rather than in the repository. A worklist
   committed into somebody's history is a file nobody asked for, in a project
   this workflow visits. Somebody has to take it out again afterwards. So report
   each item's state as the work goes. What the history keeps is the commits the
   items produced.

Git answers step 6, per branch, after a `git fetch --prune`. Read against the
base that branch targets rather than the one the audit ran on. Otherwise the
remote-tracking branches answer for a state that has moved.
`git diff --name-only <base>...<branch>` names the files the branch touches. A
branch that names none holds nothing the base does not already have.

Then run `git diff <base> <branch> -- <those files>`. Its empty answer settles
one reading: **the base already holds what the branch has in those files**.
Non-empty is not the opposite of that, which is where a reader goes wrong. A fix
that landed and a file the base edited afterwards produce the same non-empty
diff. So read it against the finding rather than count it.

Two shortcuts do not answer this at all. `git cherry` compares patch ids and
calls a squash-merged branch fully outstanding. An unrestricted two-dot diff
reports how far behind the branch is.

The branch listing covers the pull requests opened from the repository itself.
One opened from a fork is reachable only through the forge. That reading is
`gh pr list --json number,title,headRefName,files`, where the remote is GitHub
and the tool has a login. Assume none of the three. Where it does not answer,
say that you did not read the pull requests and ask the maintainer. Do not
report the branches as the whole surface.

An item you found already carried is not thereby dropped. An unmerged branch
holds a claim about the finding rather than the fix. So read it against the
finding the way you read the checkout. The checklist's *What a dropped candidate
owes* is the bar it has to clear before the item leaves the list.

Where the request arrives with a report from an earlier session, read it whole
rather than run the audit again. Say which of the two you built the list from. A
report from an earlier session is evidence about the checkout as it stood then.

## The list is worked off item by item

9. Take the items in the list's order, grouped by the workflow that owns them.
   One activation that covers that owner's items costs less than one per
   finding. The owner decides how its own area changes.

   Where the list came from a review, the base's sweep was exempt while you
   wrote nothing. You owe it now. Run it before the first item. That is one call
   per declared major, over the paths the items name. It is the one step of that
   order an audit skips. So to re-enter here with the document already read is
   what leaves it unrun.
10. **Invoke the skill that owns them** and carry across only the scope and the
    verified behaviour it needs. That is the finding, the evidence under it, the
    paths. Stop before you edit files another owner has. The crossing is the
    transition itself, not a detail of the item. Name that workflow in the
    report whether or not the user requested fixes. A reader who decides what to
    do next needs it as much as a session told to do it.
11. Where an item has no owning workflow, work it here only where the project's
    own checks prove the change. That is the change, the check that covers it,
    and nothing wider than the finding. An item nothing here can prove goes back
    unassigned in the closing report instead. A finding no workflow owns and no
    check covers is a hole in the map. A change on judgement is what hides the
    hole.
12. Settle where the change lands before the first commit. That is which branch
    these commits belong on, whether the pull request squashes, and which
    released lines carry the fix. That is the repository's own policy, and
    nothing here reads it.

    Ask the maintainer, before you push a branch and before you open a pull request. A branch listing and a tag scheme are not that answer. They say which branches exist, never which are still supported.

    What the core does is the core's own process and the default nowhere else. The core fixes on the main branch and cherry-picks down. Ask once and work from the answer. Keep it in the session the way you keep the list.
13. Commit per item, or per group of items in one owner's area. Say which item
    that commit closed, in the message from `typo3_commit_message_guide` with
    `workflow="project"`. A reader reads a session that ends halfway out of the
    log. That is why the state belongs in the commits rather than in the list
    alone. A log that says which finding each commit closed is what makes it
    readable.

## What closes it

14. Run the audit above again on the worked list. Do not only read the files it
    changed. The environment that owns a file can still rewrite one that reads
    correctly. The difference shows only once that environment runs again. Work
    that grades itself off its own diff has no evidence the finding is gone.
15. Report what remains: the items still open, the items dropped with what
    dropped them, and the ones sent back unassigned. Report every finding the
    audit left open or unassessed that this work did not settle. A finished list
    and an abandoned one read alike in a summary.

## Where this stops

This skill owns the state of a whole package. That is what is wrong with it,
what each finding is worth, and who takes it onward. It is the agreed list until
it is empty. It owns both halves of that one thing, and a request for either
arrives at the same door.

It does not own the changes in another workflow's area. Those cross to
`typo3-extension-testing`, `typo3-extension-documentation`,
`typo3-backend-module-development`, `typo3-content-element-development` or
`typo3-extension-upgrade`. What this skill carries across the crossing is the
item and not the work.

What the sweep returned goes to `typo3-extension-upgrade` whole. It owns the
move of the package to another supported range. A handover of one deprecation at
a time decides the order that workflow exists to establish.

It does not own one change proposed against the package either. You judge a pull
request, a patch or a branch somebody offers against that diff rather than
against the repository. To run this surface list on a one-line change is what
`typo3-extension-patch-review` exists instead of.
```

<a id="references"></a>

## References

<a id="where-every-task-starts"></a>

### Where every task starts

```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.
```

<a id="conformance-audit-checklist"></a>

### Conformance audit checklist

```markdown
# Conformance audit checklist

Read the relevant sections for a scoped review. Read all sections for a full
extension audit. Either way, write the surface list below whole. Report a
surface the request left out as not requested rather than drop it. The absence
of an optional subsystem is not a defect.

## Audit surfaces

- Package: identity, Composer constraints, autoloading, extension metadata, and
  the supported TYPO3 and PHP range.
- Registration and runtime: services, events, middleware, plugins, content
  elements, backend modules, routes, permissions, and effective configuration.
- Persistence: TCA, schema, relations, DataHandler use, repositories, fixtures,
  and upgrade paths.
- Rendering: site sets, TypoScript, TSconfig, Fluid roots and namespaces,
  templates, translations, and public assets.
- Security: authorization boundaries, state-changing requests, output-context
  escaping, raw rendering, query construction, user-controlled attributes, URLs
  and paths, and secret exposure. Every one of them is a value and a sink. The
  finding gate below says how you establish one.
- Quality: the test suite and the supported TYPO3 versions it runs on, the check
  layer, documentation, deprecations, and upgrade readiness. For documentation,
  `typo3_hint_lookup` with `id=extension-documentation` says what a manual
  consists of and that it ships with the package.
- Pinned versions: the Node, the actions under `.github/workflows/`, the
  container configuration and the declared dependencies. Read each against the
  release current on the day rather than against the file. One behind it is a
  finding that carries the raise. What speaks against the raise is a bound the
  package declares. The finding then names the newest release that bound allows.

## The check layer

The commands a repository declares are where you read this surface, never what
it is. Measure them against what a complete layer covers. Name each entry by
what the check establishes rather than by the tool behind it:

- **Syntax** — every shipped PHP file parses on every PHP version the package
  declares.
- **Static analysis** — types, unreachable code, and calls that cannot succeed.
  It runs over the paths the package owns, at a level the project can hold
  today.
- **Coding standards** — the TYPO3 coding guidelines as the project applies
  them. The editor-configuration and declared-PHP-range checks run beside them.
- **Manifests and dependencies** — the manifest validates and carries no open
  advisory. It agrees with what the package declares about itself elsewhere.
- **Shipped configuration and data** — the XLIFF, YAML and TypoScript the
  package ships are well-formed. Fluid templates have no established linter.

  The tests that render them prove them.
- **Shipped frontend assets** — the JavaScript, TypeScript and CSS sources the
  repository maintains. Never the bundle a build step produced from them.

What the package ships decides which of them apply. A check whose subject it
does not ship is absent for a reason. One whose subject it ships and no command
covers is a gap in the layer rather than an optional subsystem. That absence is
the finding. Ask the same of where each one runs. Syntax and analysis depend on
the PHP and TYPO3 combination and belong in a matrix.

A standards, manifest or format check is version-independent, and one run of it
proves as much as sixteen. So a matrix whose every cell runs only
version-independent steps establishes that the files parse and nothing more.

Run the checks that exist. What they printed is the ceiling of what this surface
is worth rather than its verdict. A green net proves the entries it covers and
nothing about the ones it has none for.

So say which of the entries above it leaves untouched. To establish a missing
one is `typo3-extension-testing`'s workflow, and it names the default tool per
check. A review names the gap, routes it there, and changes nothing.

For each surface, compare four sources. Those are the checkout declarations, the
runtime evidence when available, the architecture guidance, and the versioned
official documentation. When an installation parser misses a dynamic PHP
registration, report that limitation and inspect the checkout. Do not treat the
miss as absence.

## Severity

- Critical: exploitable security issue, destructive data loss, or release-wide
  outage with no practical containment.
- High: likely security boundary failure, data corruption, or a primary feature
  unusable in a supported setup.
- Medium: concrete incompatibility, unsafe rendering pattern, broken secondary
  behavior, missing regression coverage for risky code, or misleading operator
  documentation.
- Low: limited maintainability or convention issue with a concrete future cost.
- Recommendation: beneficial improvement without a verified violation.

Severity follows the demonstrated consequence, not the number of files involved.
State missing evidence instead of inflated severity.

## Finding gate

A finding needs a concrete location, observed evidence, and the applicable rule
or documentation. It needs the consequence, the remediation, and the relevant
project check. Otherwise record it as a question or an unverified category, not
a violation.

A finding about a user-controlled value is a claim about a **sink** rather than
about a call site. Escaping and injection are the same claim about different
sinks. A sink is the tag or attribute that prints the value, or the statement
that executes it. It is the header, path or process the value ends up in.

The claim stands only once you name that sink and read the code at it.
Everything before it is the path. An escaping opt-out or a quoting helper is on
the path, not at its end. So is a ViewHelper that hands its children to another
component. Where the sink protects the value on its own, that opt-out keeps the
value from a second encoding or quoting.

Ask `typo3_hint_lookup` for the sinks of the surface in hand. Follow the value
into the installed package that emits or executes it. Where you can render or
run the path, let the repository's own test settle it. Otherwise report the
finding as unverified and say which class you did not read.

A security verdict is the expensive kind to get wrong. Disprove it before you
dismiss it. A wrong dismissal costs the maintainer exactly the reading the
review skipped.

## What a dropped candidate owes

An audit drops more than it reports, and nothing records the drop. Name each
candidate you raised while you read and then let go, with what let it go. That
is the setting that was the core default after all, or the guard that was there.
It is the rule that does not govern this package, or the class you read. One
sentence each, beside the findings.

A subsystem the package does not ship never enters this list. Answer it on the
coverage list as not applicable, where it costs one line. What belongs here is
what you entertained as a defect and then was not one.

The two directions do not meet the same bar. To raise a candidate costs a
reading. To drop one costs the maintainer a finding, silently, and nothing
afterwards says it happened. So drop a candidate only where something concretely
disproves it. Report one you can neither establish nor disprove as open, with
the reading that would settle it named beside it. That is the finding gate's
question rather than violation, read from the other side.

Two dismissals go wrong reliably:

- Dropped because a comment, a docblock or an annotation says the code behaves
  that way. That is a sentence somebody wrote, not the behaviour. Read the
  implementation it describes. Where the two disagree, the disagreement is the
  finding.
- Dropped because it looks unlikely to happen. Unlikely is not disproved. What
  disproves a path is what makes it impossible. That is a guard nobody can pass
  or a caller that cannot exist, at a line.

The gate above states this bar for a security verdict, which is where it is
steepest. The bar is not that subject's. What makes a dismissal expensive is
that its cost falls on the maintainer rather than on the audit. It does that on
every surface here.
```
