---
title: "TYPO3 Core Patch Development"
description: "Skill: typo3-core-patch-development"
canonical: index.html
navigation-title: "TYPO3 Core Patch Development"
---

<a id="typo3-core-patch-development"></a>

# TYPO3 Core Patch Development

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

**Skill:** `typo3-core-patch-development`

Write a TYPO3 core patch and carry it to review: the changelog entry, the
project's checks, the push to Gerrit. Also amending after review and backporting
to a release branch.

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

## Markdown source

```markdown
---
name: typo3-core-patch-development
description: 'Write a TYPO3 core patch and carry it to review: the changelog entry, the project''s checks, the push to Gerrit. Also amending after review and backporting to a release branch.'
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 Core Patch Development

Carry one change from an issue to a patch somebody can review. Keep this skill
as routing and working order. The suites, the scripts, the contribution rules
and the Gerrit commands are lookups. A copy of them here goes stale in somebody
else's checkout, and nothing reports it.

## Establish the issue before you believe it

1. Work through [references/base.md](references/base.md), which fixes the order
   every task here starts in.
2. `typo3_rule_lookup` for what this kind of change owes. A bugfix, a feature, a
   deprecation and a removal are four different sets of obligations. Which one
   you are in decides the changelog entry, the commit subject and the target
   branch.
3. `typo3_forge_lookup` with the issue number. Read what comes back as a report
   rather than as a specification. An issue can be stale, half fixed, or right
   about the symptom and wrong about the cause. The maintainers' comments on it
   can be product judgement rather than an API fact.

   Three parts of that answer are not in the description a session otherwise
   starts from:

   - The **status and target version as they stand today**. That is where a
     closure or a reassignment shows without a rewrite of the report.
   - The **relations**. They are one hop from the change that introduced the
     behaviour under complaint. They reach it where a query on the wording does
     not.
   - The **notes**, where a maintainer said why.

   Establish which of those you have before you write code. That is what the
   reporter saw, what the branch does today, and what the project intends the
   API for. Three parts of that reading are acts. What they produce goes into
   the assessment before any code:
   - **Read the closure reason and the target version for what the conversation
     decided. Write that down rather than what the report is worth.** A closure
     for lack of feedback after a long silence has two readings. The reporter
     could not use the answer, or the reporter gave up. A target version says
     which branch a fix was still expected on.

     Say what the closure settles and what it leaves open. A closed issue is not a finding that the need is absent.
   - **Where a comment names an alternative, write out what the alternative
     drops against what the reported code did.** Name the arguments and the
     behaviour the reported code had and the replacement does not. An
     alternative closes an issue only if it does the same work. What it drops is
     usually the capability the reporter was reaching for.
   - **Enumerate the points the issue requires, the ones only a comment names
     included.** A subject that names two things over comments that name three
     is the ordinary case. The comment is the list. One patch covers all of
     them, or each point it leaves gets its own issue here, before any code. A
     split part needs a number. The `Resolves:` trailer and the changelog file
     name each take one.

     A point that is riskier to change is an argument for an issue of its own, not for a drop. What that issue carries — the tracker, the fields, the markup its description renders as — is `typo3_rule_lookup` with `documentId="core/contribution/reporting-an-issue"`. The user files it, because that takes an account and this server holds none.

4. `typo3_gerrit_lookup` with the same issue number, **before any code exists**.
   Its cheapest outcome is the one that cancels the work, and it costs one call.
   An answer of nothing is a result, and a narrow one. The server reads the
   review server without a credential. So the answer says that nothing public
   names the issue, not that nobody has fixed it. A change pushed unlisted is
   invisible to it.

5. **Verify in the checkout every rule the issue quotes.** A rule about what an
   API is for is a claim, the way a path or an identifier is. Read the class it
   names, its docblock and the core's own tests for the form under dispute. Read
   them and say which of the three carries the rule.

   Enforced in code, warned about as fragile and advised in prose are three
   different claims. Two neighbouring APIs regularly make different ones. Carry
   it at the strength its own source puts on it. An assessment that hardens "may
   change in a future version" into "must not" argues the patch away on nothing.

6. **Reproduce against the branch you fix**, not against the version in the
   report. Half of what a stale issue describes is usually gone, and the half
   that remains is the patch.

Whether that reproduction can be a test is a property of what you change.
`typo3_test_run_guide` with the paths you are about to touch says so. It names
the suites that can fail on them. A change to backend markup, a build step or
shipped JavaScript may have none that can hold the bug.

Where there is a layer, write the test first. Prove that it fails before the fix
and passes after it, in that order. A test written afterwards asserts what the
code now does, which is true of any code. Where there is none, reproduce by
hand. Write down the steps and what you saw, because that is what a reviewer
repeats. An unreproducible claim sends a patch back whether or not it is right.

A reason not to do something has a date, and the API it rested on does not. The
issue may carry a decision to defer. It needs an event that does not exist, or
there is no API for this yet. Check that blocker against what the branch has
today before you treat it as standing. An expired objection uses the same words
as one that still holds, and nothing in the notes separates them.

The argument that carries a bugfix is the same inconsistency inside one version.
"The branch handles the same input one way here and another there" is a defect a
reviewer can act on. "This would be better if it also did that" is a wish.
Agreement with it does not make it a bug.

To find the place where the system already does the right thing turns the second
into the first. To find none is an answer as well. It says the change is a
feature, and step 2 has already priced what that owes.

Establish the blast radius here rather than meet it while you work. How much of
the behaviour the suites already pin down has to change with it decides the
kind. It is a quiet bugfix, a change that has to announce itself, or a breaking
one. That decision sits upstream of the target branch, the commit subject and
the entry. Discovered step by step, it arrives after you have characterised the
change. Then you have to take the characterisation back.

Its other half is who may already extend what you are about to edit. No suite in
the checkout answers it. A green run says no core class overrides the method,
never that no extension does. The shape you have in mind may touch a public or
protected member's declaration. That is a parameter, a type, a visibility, a
`final`. Settle what that commits the patch to before you write it.

Use `typo3_hint_lookup` for the id `public-api-surface`. It decides the target
branch, so a fix you owe to a maintained release line has to know it first.

## Where the finding is a vulnerability

**Ask it once the reproduction stands and before you write any code, not when a
defect happens to look alarming.** This workflow ends in a push, and a push is
publication. It puts the diff, the test that proves the defect and a message
about both in front of every reader. Nothing else in this order asks the
question, so the fix would disclose the defect it fixes.

The stopping point is the verified reproduction. It stands, and you do nothing
after it here. No patch, no test pushed, no entry written, no comment on the
issue.

Where it goes instead is `typo3_rule_lookup` with
`documentId="any/security/reporting-a-vulnerability"`. That is the whole
procedure, and it also stands as
`typo3://guides/any/security/reporting-a-vulnerability`. Read the address there
and never from here. A contact route is the fact that moves, and this file is a
copy no release of this server corrects.

Ask it again wherever the work turns into one, because the issue rarely says so.
A fix whose real effect is that a restriction now holds is this case under
another name. Where something is already up for review, an amend takes nothing
back. Every patch set stays fetchable. So name what is public in the report
rather than repair it.

## Make the change

Ask `typo3_hint_lookup` for the conventions of each subsystem you touch, before
you write rather than after. A convention you fetch afterwards confirms what you
already wrote. Ask by id for the hints the brief left out, which `omittedHints`
names, and with the concrete paths only for a path the brief did not see. The
same paths queried again answer the same hints.

Where the change touches a source below `Build/Sources/`, the generated file
beside it is part of the patch. `typo3_rule_lookup` with
`documentId="core/contribution/committed-build-output"` says which source
produces which committed file. It says how you rebuild one without risk to the
rest of the tree. It says what a backport that came back with conflict markers
in it needs.

Keep the patch one change. What else you noticed is another issue and another
patch. A diff that fixes two things is a diff a reviewer has to accept or reject
as one.

That narrows the work and never the points the issue lists. You settled those
while you assessed it. All of them are in this patch, or the ones that are not
already have issues of their own. A point you drop here drops invisibly.
`Resolves:` closes the issue on every point it names, and nobody reopens a
closed one.

Where the request widens after the patch is under way, re-establish three
things. That is what kind of change this is now, which branches it reaches, and
what it owes. Do it before you write the widened part, and say which of the
three moved.

Step 2 settled the first, the blast radius the second and the changelog section
the third. Each did so against the narrower request. To carry on derives none of
them again. A change that gains a second subsystem gains that subsystem's build,
its checks and its backport constraint with it.

Find out whether the area moves before you build on it. Fetch and rebase onto
the branch you target before you finalise. A patch written against code that
changed underneath it is not a patch that needs adjustment. The method it called
can be gone, and with it the reason the change looked right.

## Verify with the project's own commands

`typo3_test_run_guide` with the changed paths returns the suites that can fail
on this change, each with its targeted invocation. `typo3_script_lookup` returns
the scripts around them. Run the narrow ones while you iterate and the broad
ones before you push.

Two things decide whether that verification means anything:

- The runner is the project's. A suite run through the host's own PHP or an
  installed binary is a green nobody can reproduce.
- A green that ran over no files is not a green. Where a check reports success,
  confirm that it inspected something: the count of files, tests or fixtures it
  names. Do that before you treat it as evidence. A check that found nothing to
  check without a word is the failure mode that survives review.

## The changelog entry the change owes

The procedure is one page: `typo3_rule_lookup` with
`documentId="core/contribution/changelog"`, which also stands as
`typo3://guides/core/contribution/changelog`. It says which of the four types
the change owes and which release directory the file goes into. It says what its
name is and what checks it.

Decide the type from what the change does rather than from habit. An entry for a
change that owes none is as much a review finding as a missing one. Write the
file into the `<lts>.x` directory of the oldest branch the `Releases:` trailer
names. Write it into both `.x` directories where two maintained lines take the
change. The branches the patch reaches decide that. The branch you write it on
decides nothing.

## Commit and push

`typo3_commit_message_guide` with `workflow="core"`, the drafted message and the
change type reports what is still wrong. It does so before the hook does. State
the workflow. Its default is a repository of your own. That demands neither the
Forge issue nor the `Releases:` trailer a patch here owes.

The rules behind it are one page: `typo3_rule_lookup` with
`documentId="core/contribution/commit-messages"`, which also stands as
`typo3://guides/core/contribution/commit-messages`. It says the subject, the
trailers, the release targets and the changelog entry the change type owes. One
read of it here is cheaper than the checks that teach it one call at a time.

Then the Gerrit workflow. It says what the push is. It says how you amend a
change into a new patch set rather than a second commit. It says what you must
not edit between patch sets. That procedure exists whole as `typo3_rule_lookup`
with `documentId="core/contribution/gerrit-workflow"`, which also stands as
`typo3://guides/core/contribution/gerrit-workflow`. Read it before the first
push rather than a section at a time. A search returns the part your words
matched, and everything below here is a different part of the same page.

Before you push, establish where you push to. A core checkout's remote is not
necessarily the one it fetches from. The answer is in the checkout's own git
configuration rather than in the repository's name.

### Rebase where the branch moved under you

A commit that sat while you verified it is behind `origin/main`. Its rebase is
part of the push rather than a thing of its own. Two parts of that are not
obvious. A session with no skill that told it either worked both out from
scratch:

- **Stop a running `runTests.sh` suite first.** The script mounts the tree and
  reads it as it goes. So a rebase underneath a run invalidates it without a
  failure. The run then reports about a tree that no longer exists. Clear the
  suite's leftover containers before you start.
- **Confirm that the `Change-Id` survived the rebase.** It makes the push a new
  patch set on the change you already have. Without it you open a second change
  instead, and another push does not undo that.

Then run the checks again on the new base. Inspect the commits you rebased over
where any of them touch the same files. A suite that passed before the rebase is
evidence about the old base.

**Where the commit to change is a patch set on the review server, invoke
`typo3-core-patch-checkout` for that one change.** Work from the copy it leaves.
Somebody else's change you pick up to finish arrives that way. So does your own
where this checkout no longer holds it. That workflow's whole subject is the
ref, the remote, and which of the three destinations the patch goes to. It is
also how you put the checkout back afterwards.

What crosses back here is the working copy the patch sits in and the patch set
you fetched. It is also whether you had to carry the patch onto current code to
apply.

The push is a step of its own, and you take it when the user asks for it.
Everything above is local and reversible. The push is neither.

**Ask whether the change goes up visible to everyone or unlisted, every time.**
The two are different refspecs, and the difference is not a preference. One
publishes the change to whoever watches the project and notifies reviewers.
Nothing quietly undoes the publication or the notification. `typo3_rule_lookup`
for the Gerrit workflow has both forms. Which one this change wants is the
user's decision and never a default read off what the session did last.

## Where the patch is finished and the review begins

**When the checks pass and you have written the commit, invoke
`typo3-core-patch-review` on the diff.** Do that before you push or hand the
patch over. That is a step, not a note about ownership. Load the skill by name
and work from it. What crosses over is the diff, the branch it targets, the
change type and what the checks reported. What comes back is the work list the
paragraph below already says to take it as.

It stands as a step because the ownership sentence did not fire as one. A
session finished a push-ready patch here and ran the project's checks. The patch
was three files, two functional tests, a commit message. It reported the patch
and never opened the review. That was twenty turns after it invoked this skill
out of a triage whose crossing stands as an act. The act fired and the boundary
did not, in one session on one task.

## Amending after review

A patch that came back is the same change, not a new one. Fetch the patch set
that exists, amend it, and keep the identifier that links it to its review.
`typo3_rule_lookup` for the Gerrit workflow says how you do each of those.
Address a reviewer's comment in the patch or answer it in the review. Never drop
one in silence. A comment nobody replied to is the reason a change sits
unmerged.

Where the change is somebody else's, those steps hold and two more come with
them. Ask the author before the patch set goes up. Say every decision you took
on their behalf in a comment on the change. The diff between two patch sets says
what moved and never why.

The amend leaves them the author and makes you the committer, which is the
intended shape. The same page carries it. Never overwrite the author line,
however much of the patch you wrote. `--reset-author` has no use here.

This skill owns the write and the delivery of a core patch. That is the change
itself, its tests, its changelog entry, and the checks it has to pass. It is its
commit message and its push.

It does not own the judgement of somebody else's patch, and it does not own the
judgement of its own. Where the request is to say what is wrong with a change
rather than to make one, `typo3-core-patch-review` owns that. It reads different
surfaces for it. Take its findings as a work list when it hands them over. A
change to an extension, a sitepackage or a site project belongs to the extension
skills. Their conventions are not the core's.
```

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