---
title: "TYPO3 Development Installation"
description: "Skill: typo3-development-installation"
canonical: index.html
navigation-title: "TYPO3 Development Installation"
---

<a id="typo3-development-installation"></a>

# TYPO3 Development Installation

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

**Skill:** `typo3-development-installation`

Bring the local development installation of a TYPO3 extension, sitepackage or
project package into existence, or boot and repair the one the repository
declares: the container, DDEV where it declares one, the unattended install,
seeded demo content, and a site that will not come up.

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

## Markdown source

```markdown
---
name: typo3-development-installation
description: 'Bring the local development installation of a TYPO3 extension, sitepackage or project package into existence, or boot and repair the one the repository declares: the container, DDEV where it declares one, the unattended install, seeded demo content, and a site that will not come up.'
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 Development Installation

Produce an installation the developer of this package can run. Each step decides
what the next one can be, so the order matters. Keep this skill as routing and
workflow. Never keep layout keys, environment defaults, command options or
package names. Each of those belongs to a tool that releases on its own cycle.
You cannot ask any of them again from here.

## Where this starts

Work through [references/base.md](references/base.md) first. One of its answers
is this workflow's entry condition rather than a failure. An installation lookup
that describes the repository and reports nothing installed in it is the task.
So is one that finds no repository to describe at all. Neither is the
disconnected server the base tells you to stop for. Stop for an error, continue
for either of those, and say which of the three came back.

Then, before you create anything:

- The base's `typo3_project_describe` discharges `typo3_server_scope`, whatever
  it answered. This step asks whether you can reach an installation and a
  console at all. That answer already states it. It says whether the repository
  it describes holds an installation. Where it could describe none, it gives the
  cause instead.

  The orientation tool is for a caller who does not know whether this server can
  answer at all. That is not this workflow's question.
- Ask the calls in the base that read the installation again once it exists.
  Asked before, they cannot answer. Asked after, they say whether the work
  succeeded.

The repository decides where this task starts. What decides it is the boot
procedure it declares rather than the traces an installation has left in it. A
procedure is a script its manifest declares, or a task its environment runs at
start. Or it is a sequence its own instructions write down. You boot a
repository that declares one from it. The describe answer carries what it
declares before any of it exists.

For one that declares none, you create an installation. One that declares an
environment and no procedure is both. Run what it declares, take every step
after that from the create branch, and change nothing it declares.

One that is already up is none of the three. You create nothing and boot
nothing, and you read it from **The installation that already answers** below.

## Boot what the repository already declares

- The run is a guide, and this page does not reconstruct it. `typo3_rule_lookup`
  with `documentId="project/installation/booting-a-clone"` carries the order the
  steps go in and why you start the environment twice. It says where the data
  comes from when the repository declares no import. It says what says the boot
  worked rather than that a command exited. Read it before you run the declared
  steps, not after one of them failed.
- `typo3_hint_lookup` with `id=installation-boot` owns what a clone lacks. That
  is the schema the dump owes the code, the caches inside it, and the backend
  user without a password. It owns the two things that make a booted clone
  answer nothing under the host you serve it on. That entry also holds the
  second boot's failures, and they are not the first boot's.
- Where the repository is an extension with TYPO3 installed beneath it,
  `typo3_hint_lookup` with `id=extension-repository-installation` owns that
  layout. Composer loads the root package from the Composer root itself. The
  `typo3conf/ext/` below the document root is empty on a Composer installation,
  not broken.
- Read the environment configuration whole before you run anything. The scope
  answer names the interpreter and the commands the manifests declare. The
  lifecycle the environment runs by itself lives in that file. That is the tasks
  bound to each stage, the configured data sources. Where the answer does not
  carry them, the file is the only place you can read them. A start of the
  environment runs them whether or not you read them.
- Read the versions that file pins against the release current on the day, from
  where its publisher announces it. Those are the container's own, the database,
  the Node. One behind it is a finding that carries the raise, not a raise you
  make here. A boot is not an upgrade, and what the installed TYPO3 requires
  speaks against one.
- Read the repository's own instructions beside it. A project that ships a boot
  procedure has usually written down which data it is meant to hold. You cannot
  derive that from the code.
- Run the declared steps in the declared order, and change nothing that already
  works. A boot is not a repair. A rewritten configuration that boots the same
  way is a change nobody asked for.
- Where a step fails, the finding is which declared step failed and on what. It
  is not a second procedure beside the one the repository has.

## Create one where none is declared

1. **Make the package's own manifest the Composer root package.** It has to
   install TYPO3 beneath itself, into a directory git ignores.
   `typo3_hint_lookup` owns what that takes:
   `id=extension-repository-installation` for which keys move the installation
   out of the way. It says where the console lands under them. It says which
   layout key Composer accepts and then reports rather than honours. It says
   which package the core brings that a root constraint for it cannot resolve.

   It says why the extension directory below the document root is empty rather
   than broken.

   The plugins the install has to run are Composer's own configuration, and
   Composer's documentation states them. Nothing exists yet, so the installation
   itself answers nothing at this step.
2. **Declare the container.** Its project type and its document root follow from
   the layout you decided above, not the other way round. You declare its
   interpreter here too, and nothing later asks that number again. Ask
   `typo3_hint_lookup` with `id=php-versions` for what the target version
   requires and what it resolves dependencies against. It says what the core
   runs its own suites on.

   Choose against that answer rather than against what the machine already has.
   Then verify two things rather than assume them. The environment fails its
   start when a provisioning task fails. An install that failed behind a green
   start is the expensive failure of this step. A command that rewrites the
   environment configuration has not dropped what you set by hand. That is why
   you read the file back after such a command.
3. **Install non-interactively.** The console's setup command answers its own
   questions from a fixed set of environment variables. `typo3_hint_lookup` with
   `id=environment-runtime-readers` names them. Read its option set off the
   installed console's own help, which is the binary the install runs through.
   From 14 on that help reports an option as disabled where a package it needs
   is inactive. The one it reports that way is `--distribution`, which step 4
   reaches for.

   Ask `typo3_documentation_lookup` for what an option means at the version
   installed. Check two things in what it answers. The value a connection option
   accepts is not necessarily the value written into the settings afterwards.
   The command refuses a database that already holds tables. The second decides
   whether you can run an install script twice. It needs its own guard on what a
   previous run left behind, and forced settings do not remove a schema.
4. **Seed the content the package's development runs against**, where the task
   needs one. The first question is by which mechanism this package fills an
   instance, and it has more than one answer. `typo3_hint_lookup` with
   `id=fresh-instance-seeding` says the ways a package declares one, and what
   holds when it declares none. Then only the package's own manual writes the
   procedure down. `typo3_extension_describe` reports where that manual is,
   beside the data files, the console commands and the site sets the package
   ships.

   Where the mechanism is a shipped data file, `typo3_hint_lookup` owns the
   rest. `id=sitepackage-initial-content` says which of the two setup commands
   imports it and what makes a package count as a distribution.
   `id=initial-content-import-once` says why a changed file does not arrive a
   second time. `id=initial-content-references` says what the import remaps and
   what it leaves as a pointer to a stranger.

   What this workflow adds is where you look when it lands. A seeded
   installation that answers not-found at the project root has a site base that
   is not this installation's URL. The importer did that, not the package. Read
   what landed with `typo3_configuration_lookup`. Correct it in the
   installation's own site configuration, and verify it again there.

   Content is the second question at this step and not the first. A package that
   renders into a page and defines none leaves the installation with something
   else to render. `typo3_hint_lookup` with
   `id=development-installation-page-object` owns where the page object that
   replaces it comes from. It says where it lives so nobody releases it.
5. **Decide what the install wrote into the repository.** The installation's
   configuration, its writable state and its document root land in the Composer
   root. That is the versioned repository itself. `typo3_hint_lookup` owns this:
   `id=project-configuration-files` for which of those files the project owns
   and which the environment generates.

   `id=project-build-and-scripts` for what surrounds the site rather than sits
   in it. That is where the tooling and the one-off scripts belong, how a
   colleague runs them, and what nobody commits. The ignore rules follow from
   both answers. Write them before the first commit, not after the first
   accidental one.

## The environment's settings against the installation's own

Where the local environment generates settings into the installation, two owners
share one boundary. That is the generated file and the installation's own.
`typo3_hint_lookup` with `id=project-configuration-files` owns it.

This workflow adds the case that breaks it. Such a generator knows only the
services it provides itself. So an installation you put on something else on
purpose gets the generated file merged over what the install wrote. That is a
database the environment does not run, or none. Then it can no longer connect.

To take the file over is the documented way out. It is a step of the install
rather than a repair afterwards. Establish what the merged result is with
`typo3_configuration_lookup` rather than from the files. The merge is what the
installation runs on.

## Prove it, and how far depends on what the run wrote

The site that answers is the proof in every case. That is the backend, and the
frontend on the URL the installation names for itself. Tear nothing down to
establish that, whoever wrote the sequence. An installation somebody asked for
and you then destroyed is a change nobody asked for.

**A status code is not what the site looks like, and you look at both sides.**
`typo3_rule_lookup` with `documentId="any/testing/browser-check"` carries which
installation shows a case, how a browser reaches it, and where the harness goes.
People skip the backend half. A page that answers 200 and renders unreadably
passes a frontend check. No frontend screenshot shows what an editor gets: the
element wizard, a preview, an icon, a record's own badges.

Where a side errors, read the failure from what the installation wrote down, not
from the page it rendered. `typo3_hint_lookup` with
`id=installation-exception-output` owns that. It says where TYPO3 writes an
uncaught exception, and which codes it shows and never writes at all. It says
what decides whether the page carries the message.

A fetch of the rendered error page is the detour this replaces wherever
something threw. It costs the whole document through the context and still holds
nothing where TYPO3 withheld the message. A side that answers something other
than what it should wrote nothing down at all. That is the section below rather
than this one.

Report the exact commands you ran, what each one printed, and what the
installation now is. That is the document root, the console that reaches it, the
URL that answered, and the database it is on. `typo3_task_guide` carries what a
finished setup owes its user beyond that, credentials included. Report what it
names rather than a second version of it.

What you owe past that follows from what the install wrote into the repository.
Read that off the ignore rules rather than off this session's account of itself.
Git may ignore every path it wrote: the document root, the installation's
configuration, its writable state. Then there is no sequence a clone would run
and nothing for a message to be about. Then you skip both steps below, and the
report names the two and says why. Where it left files the repository now
carries, both are part of the work:

1. Start from the state a colleague's clone is in: no installed dependencies, no
   installation, no container. Let the declared sequence run unattended.
   Anything that needed a hand is not part of the setup yet. Then start it a
   second time without a cleanup, because somebody will run a setup that is not
   idempotent twice.
2. Draft the message for what the setup added to the repository with
   `typo3_commit_message_guide` and `workflow="project"`. The manifest, the
   container declaration and the ignore rules are that repository's own files.
   That is the workflow the argument names.

## The installation that already answers

Here you build nothing. You read an installation that is up, and what it answers
is the evidence. That is why a session that arrives with one repairs it from the
same two facts that prove a build. In this order: what the installation
answered, and what it wrote down about it.

- **A log with an entry is an uncaught exception**, and you read it where
  **Prove it** above says.
- **A log that stayed empty is itself the finding.** A status code TYPO3 returns
  on purpose is a response rather than a failure. So nothing throws and nothing
  writes.

  The rendered page is then the only evidence the installation holds. Its fetch
  is right here where it was the detour above. The line it carries names the
  stage the answer came from. That separates a request that matched a site and
  failed inside it from one that matched no site at all.
- An empty log has one other cause, and you settle it before you fetch a page.
  TYPO3 never writes some exceptions down at all, and `typo3_hint_lookup` with
  `id=installation-exception-output` names them.

Where the line says the request reached a site and the page did not come, the
subject is the page. It is not the site configuration. `typo3_hint_lookup` with
`id=page-not-found-within-a-site` owns that half. It says which of those lines
is a path the router never resolved. It says which is a page it resolved and
then withheld.

It says how far up a tree a hidden or a deleted root page reaches. It says where
that line stops being readable at all. Read it before you touch the site
configuration. A page this site holds and refuses is not a base that is wrong.

Where the answer says the request reached the wrong site or none, the subject is
the installation's own site configuration. It is not the code in front of it.
Five lookups own what it can be, each a `typo3_hint_lookup` by id:

- `project-configuration-files` — which file TYPO3 reads, and why the copy a
  package ships is not that file.
- `installation-boot` — a base whose host is not the host you serve the site
  under. It matches no site, and the root answers not-found.
- `site-base-collision` — which of two sites answers where both bases fit the
  request.
- `initial-content-references` — a base an import rewrote to the identifier it
  landed under.
- `autogenerated-site-configuration` — a site nobody wrote. The core writes one
  when somebody creates a page at the root. It writes it on a sub-path of the
  URL the request that created it arrived on. Read off the identifier which of
  the last two left a site, not off the symptom. The two produce the same
  not-found.

  Correct it where TYPO3 reads it. Verify by asking the installation again
  rather than by another read of the file. `typo3_configuration_lookup` says
  which hosts the installation accepts at all and whether a page discloses its
  message. TYPO3 merges those at runtime, and the merged value is what answers.

To take over an installation somebody else built is the same reading with one
step in front of it. Establish what it runs on before what it answers means
anything. `typo3_hint_lookup` with `id=installation-boot` owns what such a
hand-over lacks. That is the schema a dump owes the code, the caches inside it,
and the backend user without a password.

The single verbs of an installation that runs have no order to keep and get none
here. Flush what a change invalidated, get into a user nobody has the password
for, add one. Each is one command, and the hint a query for it reaches carries
it.

A first boot that writes a deprecation log is a finding about the package's own
code, not about the installation. The installation is complete at that point
rather than broken. State that it is up and what it answers. Name the log and
the package whose code fills it. Change nothing in that package here. Invoke
`typo3-extension-health` with those lines as the evidence it starts from.

## When the task turns to a suite

**The moment this task grows a test, invoke `typo3-extension-testing`.** Do that
before you edit a test file or build the installation a suite boots. That is a
step, not a note about ownership. Load the skill by name and work from it. What
crosses over is the verified point this workflow reached, and the defect the
diagnosis here landed on. The point is the document root, the console that
reaches it, the URL that answered, the database it is on.

It stands as a step because the skill's name at the foot of this file did not
fire. A session read a 404 out of the log and fixed the exception behind it.
Forty minutes later it extended `Tests/Functional/` without the workflow that
owns it.

**A sentence about proof rather than about the site fires it.** "There is no
test for it", "prove it", "add a functional test": each turns the task over.
That holds whether or not the installation work here is complete. "The frontend
is still a 404", "the backend does not come up", "which site did it reach" are
this workflow's. A read of a log is not a suite.

## Where this stops

This skill owns the installation a developer works on a package in, from before
it exists until it answers. That is the Composer root package that installs
TYPO3 beneath it, and the container the repository declares. It is the
non-interactive install, and the content that seeds it. It is what the install
writes into the repository, and what a running one answers. That is which site a
request reached, what it wrote down, and the site configuration behind both.

It does not own hosting, deployment or backups. Nor the major upgrade of an
installation. That is a project of its own rather than a verb of the one
somebody develops in. `typo3_hint_lookup` with `id=installation-upgrade` carries
its order.

The installation a suite boots is not this one. The difference is what each is
for rather than how it is laid out. This workflow produces a site somebody opens
in a browser and clicks through. That is why the package's own manifest becomes
the Composer root.

A package with TYPO3 below a build directory, linked in and with no site to
visit, is a test fixture. It belongs to `typo3-extension-testing`. A repository
can have both. Which one the task needs is the first question, and it decides
the layout.

Tests and static checks are `typo3-extension-testing`'s, and the boundary runs
in both directions. On the way out, **When the task turns to a suite** above
states the verified point. It stops before you edit that owner's files. On the
way in, a suite that needs a served site and has none is this workflow first. It
runs up to that same verified point, and then back.
```

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