Skip to content
TYPO3Dev Companion

TYPO3 Core Issue Triage

Skill: typo3-core-issue-triage

Find the issues worth working on in an area of the forge.typo3.org backlog — old or untouched ones, and whether anybody is on one already — or say what is still true about one issue: whether it still happens against the core checkout, was fixed, or was never a defect. A task that ends in a patch starts here; the patch is typo3-core-patch-development's.

Markdown source#

markdown
---
name: typo3-core-issue-triage
description: 'Find the issues worth working on in an area of the forge.typo3.org backlog — old or untouched ones, and whether anybody is on one already — or say what is still true about one issue: whether it still happens against the core checkout, was fixed, or was never a defect. A task that ends in a patch starts here; the patch is typo3-core-patch-development''s.'
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 Issue Triage

Take one open issue and say what is still true about it. Or take an area of the
backlog and hand back the candidates worth work. Keep this skill as routing and
working order. The tracker, the review server and the checkout's own commands
are lookups. A copy of what they answer goes stale here, and nothing reports it.

Triage is not a smaller version of the patch. What it produces is a statement
somebody can act on. This still happens, this is gone, this was never a defect,
nobody can settle this without X. The outcome that ends the work early is the
valuable one.

## Find the candidates

1. Work through [references/base.md](references/base.md), which fixes the order
   every task here starts in. It establishes the checkout you stand in, which
   the verification below runs against.
2. `typo3_forge_lookup` with `backlog` to get it rather than one issue. `oldest`
   and `stale` are two different questions, and the second is usually the one
   the user asks. Filed long ago is about the report. Untouched for years is
   about the attention it got. Somebody works on an issue filed in 2009 with a
   comment from last month.

   Narrow with `category` in the user's own words, and with `tracker`. An old
   Bug and an old Feature are two different findings. One claims something is
   broken today, the other that somebody wanted something once.

   Read the count that comes back against the number of entries. A page is not
   the set. A triage that takes thirty of two thousand for the problem has
   measured the limit rather than the backlog.

**Nothing in that list is a finding.** Age makes an issue a candidate and says
nothing about whether it is right. A report from 2011 can describe behaviour the
branch still has. One from last year can be about code that no longer exists.
The rest of this order separates them.

**The list is the first deliverable, and the choice from it is not yours.**
Triaging a backlog and triaging an issue are two different jobs. The step below
is the second one: it takes a number. Hand the backlog over first, one row per
candidate with what the user chooses on. That is the number, the area, the
subject, and how long it has sat untouched. It is whether the code its text
names is still installed here.

Let whoever asked pick. A session that picks for itself reports on four issues
out of thirty-nine. It has silently answered a question nobody asked. Where the
request really was "just find me something", say which rows you would take and
why. Let that be the choice.

**Where you do pick, pick on where the symptom is visible and on how much the
checkout already models it.** Age is not it, and neither is the subject matter.
Read in this order and stop at the first that decides:

- **What has already happened to it.** A change on the review server is the
  cheapest description of what a fix looks like. One somebody abandoned is a
  verdict somebody wrote down. A relation to an epic, or to an accepted parent,
  says the report is one strand of a larger piece. That piece's decision is not
  a session's to take.
- **Whether the code it names is still there.** Every row carries the classes,
  methods and core files its own text cites. Each comes with where it stands in
  the packages installed here. So a report whose names are all gone settles
  without one open file. A name the answer could not place decides nothing, and
  neither does one that stands. A class that is still there is a candidate to
  read rather than a defect that still reproduces.
- **The category, against the branch you stand on.** One that names a subsystem
  the branch no longer ships settles the issue before you read the report. The
  tracker keeps a category long after the code goes. Most of an old backlog
  still names subsystems that are there.
- **Where the symptom appears.** A rendered fragment, a stored row, a resolved
  value: you reach anything a process produces in the cheap layers. Those need
  neither an installation nor a browser. One that appears only after an
  interaction in the backend needs both, and that is most of the session.
- **How far the mechanism reaches.** A report that names one class and the
  behaviour in it is the shape you can settle. One that names several and the
  order between them has already said it is an interaction. A reporter who
  worked that out is usually right.
- **What the suite already models.** Look for a test over the class the report
  is about, at the level the symptom appears at. A case added to a file that
  exists is a reproduction with no fixture to build. The level is the whole of
  it. A component tested on its own cannot see an order between components. The
  core models more constellations than a category suggests.

Say which of those decided. Say of the rows you passed over that you passed over
them. A skip is not a triage, and the list is still what the user asked for.

## Establish what the issue claims

3. `typo3_forge_lookup` with the number. Read what comes back as a report rather
   than as a specification. Three parts of it are not in the description a
   session otherwise starts from. Those are the **status and target version as
   they stand today**, the **relations**, and the **notes**. The relations are
   one hop from the change that introduced the behaviour. The notes are where a
   maintainer said why.

   The assignee is the fourth. On an old issue it usually names who last touched
   it rather than who is on it. An assignee is not evidence that anybody works
   on it, and an unassigned issue is not evidence that nobody minds.

Separate the three claims the report mixes before you verify any of them. Those
are what the reporter saw, what they believed caused it, and what they wanted
instead. The first is the only one a checkout can settle. A report is regularly
right about the symptom and wrong about the cause. To verify the cause and
report the issue as invalid is the most common way this work goes wrong.

Where the issue quotes a rule, verify it in the checkout. A rule is "do not use
an API this way" or "this is not supported". Do not carry it at the strength the
reporter put on it. Enforced in code, warned about in a docblock and advised in
prose are three different claims. The reporter's word for all three is the same.

## Ask what happened since the report

4. `typo3_gerrit_lookup` with the issue number, **before you open the
   checkout**. Its cheapest outcome is the one that ends the work. Somebody has
   a patch up, and the triage is that it is under review rather than
   unaddressed. An answer of nothing is a result, and a narrow one. The server
   reads the review server without a credential. So nothing public names the
   issue, which is not that nobody fixed it.
5. `typo3_changelog_lookup` with the words the report uses. It says whether the
   core deprecated, removed or reworked the area since the report. A rework
   turns a valid report into one about code that is gone. It also makes the
   reproduction below fail for a reason that has nothing to do with the defect.

A changelog records change events. So an area nobody has touched has no entry at
all. An empty answer is not evidence that the behaviour stayed the same.

## Verify against the checkout you stand in

Reproduce against what the branch does today, never against the version in the
report. Half of what an old issue describes is usually gone, and the half that
remains is the finding.

Establish the code path first. Find the class the report is about and read
whether the behaviour it describes is still there. That separates "still
happens" from "cannot happen any more, the method is gone". The second is a
verdict that needs no reproduction at all.

**Before you write a test, look for the one the core already wrote and switched
off.** Where people knew a defect and nobody fixed it, the suite regularly
carries it as a commented-out data-provider row. The reason stands beside it.
The fixture under it already models the constellation.

`grep -rn "@todo" <sysext>/Tests` narrowed to the subsystem the report is about
is the whole of the search. The reason text says whether a hit is this report.
"Fails, not expanded to sub-pages" is one, and "wrong assertion" is a note to
whoever wrote the test. There are few of them and they are worth the one grep.
To remove a comment is a reproduction with no fixture to build and no harness to
prove.

`markTestSkipped` is a different thing and rarely this one. Most of them are
about the machine: no APCu, no Redis, no ImageMagick, a case-sensitive
filesystem. A test skipped for the environment says nothing about the report.

`typo3_test_run_guide` with the paths you have just read says which suites can
fail on them. It says whether a test can pin the behaviour at all. Where it can,
a failing test is the strongest thing a triage produces. It survives a handover
to somebody else, and it is the patch's first half already written. Where no
layer can hold it — backend markup, a build step, shipped JavaScript — say so.
Reproduce by hand instead, and write down the steps and what you saw.

That test is a throwaway until a patch adopts it, and it has three rules of its
own. It goes where the suite already looks. It mirrors the path of the class it
is about, because a file the runner does not collect proves nothing.

**Watch it fail before you believe it.** A reproduction that is green on its
first run tests nothing until you have shown it red. A first run may fail for a
reason that is not the issue. That is a field the type does not show, or a
fixture nobody loaded. That is a result about your harness and not about the
report. And it comes out again when the triage ends, unless the work carries on
into the patch that keeps it.

**The core's suites are not the ones a manifest here declares. You do not run
them the way you run an extension's.** They belong to the core's own runner,
which no `composer` script names. `typo3_test_run_guide` gives the targeted
invocation for the paths in hand rather than a suite name to guess at.

`typo3_script_lookup` is the rest of what that runner offers. That is the
options that decide which PHP and which database a suite runs against. An old
report turns on exactly that when it says the behaviour depends on either.

Where the symptom is in the rendered output, the throwaway has to produce it
before it can assert anything. The value is the unknown rather than the
expectation. `typo3_rule_lookup` with
`documentId="core/testing/proving-a-rendering"` is that harness. It says how the
snippet goes into TypoScript and which operator forms silently do something
else. It says how you print the rendered HTML at all.

Two of those decide whether a reproduction means anything. The step the base
opens with already answered both:

- **Where the checkout has a DDEV project, the suites and the console run inside
  it.** The same command in your own shell runs on whatever PHP the machine
  carries and against whatever database it has. That reproduces something else
  and looks identical in the output. `typo3_project_describe` says whether this
  checkout is one of those and what the form is. Take it from there rather than
  from what worked in another repository.
- **A green that ran over no files is not a green.** Where a suite reports
  success, confirm it inspected something — the count of tests or files it
  names. Do that before you read it as the behaviour being gone. That is the
  failure mode a triage is most exposed to. "The suite passes" is the evidence
  it is about to write a verdict on.
- **Once you commit the change, `git stash` measures nothing.** The same failure
  in a second costume. The stash finds nothing to save on a clean tree. The run
  that follows is the patched code, and the result reads as a without-patch
  measurement.

  Compare against the parent instead: a worktree on `HEAD~1`, or
  `git revert --no-commit` with a restore after. Confirm that the tree changed
  before you believe the run. `git stash list` that names nothing new should
  stop you.

An old report frequently names the versions the reporter saw it on. Those are
what the reporter had, not what it still reproduces on. The version the suites
run against here is a property of this checkout. Say which one the verification
used. A verdict that names no version and no branch is one nobody can repeat.

**A reproduction that fails to reproduce is a result and not a dead end.** Say
which of the three it is. The behaviour is gone, the steps were insufficient, or
the report never contained enough to try. They lead to opposite outcomes. They
look identical in a session that only writes down "could not reproduce".

## Where the finding is a vulnerability

**Ask it of every finding before you write the verdict, not when one happens to
look alarming.** A triage produces what makes up a vulnerability report: a
step-by-step reproduction against a branch people run. It produces it for the
tracker. Nothing else in this order asks the question. So the step that was
meant to report the finding would disclose it.

The stopping point is the verified reproduction. It stands, and you take no
public step. Nothing about the finding goes into the issue, onto the review
server or into a chat. Not the reproduction, not the failing test, not the
verdict.

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.

Hand over what that report needs: the branch, the code path, the reproduction
and the version it ran on. Say that you withhold the ordinary verdict and why.
The user files it. This workflow supplies what the report rests on and takes no
step of its own.

## Say what the triage found

[references/checklist.md](references/checklist.md) carries the verdicts, what
evidence each one owes, and the questions that decide between them. Read it
before you write the answer rather than after. The verdicts are not degrees of
confidence in one finding. The one you pick first decides what you still have to
establish.

Report what you did not establish beside what you did. A triage whose reading
stopped at the code path says so. The next person's work is exactly the part you
left.

**The verdict is markdown the reader can copy, and the answer is where it
goes.** You write it for the person who will act on it, 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. This workflow leaves that tree as it found it.

**A verdict that ends the issue carries the comment that closes it.** Three of
them do. The checklist says which, what that comment holds and which markup the
tracker renders. Forge is not markdown. The comment stands in the answer beside
the verdict it rests on. Filing it is the maintainer's act. This workflow holds
no credential and comments on nothing.

## What a previous attempt cost

Where somebody fixed the issue once and reverted the fix, the verdict is not the
answer people wait for. "Still happens" and "somebody tried and reverted it" are
the same verdict and opposite propositions. What separates them is why they
reverted it and whether that reason still holds.

The trigger is in the issue answer rather than in the reading. A relation marked
`precedes` or `duplicates` carries its subject, and `reviews` names every change
the journal mentions. So an issue whose history is a merged-then-reverted fix
says so before you open the checkout.

- Read the related issue somebody filed the revert under. The reason lives there
  and nowhere else. The commit that reverts says what it reverted, and the issue
  says what it cost.
- Read the attempt itself, which is the one thing no lookup here returns. The
  issue answer carries the change numbers, and the Gerrit search by issue number
  their state. So what remains is the diff: fetch the patch set into the
  checkout and read it. The ref it is under and the remote it is on are two
  sections of one page. That remote is not the one a core checkout fetches from.
  So where the fetch is the task, read it whole: `typo3_rule_lookup` with
  `documentId="core/contribution/gerrit-workflow"`, which also stands as
  `typo3://guides/core/contribution/gerrit-workflow`.

  Abandoned is a verdict somebody wrote down. The diff under it is the cheapest
  description of what a fix looks like against a modern core. Read the patch
  set; put it onto no branch. To rebase an abandoned attempt is to write the
  patch rather than judge the issue.

  `typo3_gerrit_lookup` with a change number earns a call of its own for a
  `reviews` entry the issue search left out. That search finds a change whose
  commit message names the issue. So an entry missing from it is the one whose
  branch, patch set and status nothing else has stated.
- Find every production caller of the method the reverted patch touched, in the
  checkout. A fix scoped to one call site is a different proposition from one
  that changes a shared path.
- Establish whether the path the revert names still routes through that method.
  A subsystem rebuilt since is what turns the old objection into history.

**A reverted core fix becomes re-attemptable when somebody has rebuilt the
shared consumer that made it expensive. It also does when the caller set has
shrunk to the one site the fix needs.** That is the form that transfers. Neither
half is readable off the issue, and both are one grep and one file in the
checkout.

What comes out of this is what a maintainer needs before the issue can move.
That is what the answer owes. It is not a design and not a patch. To name the
constraint the last attempt broke is the deliverable. To propose the code that
respects it is the next workflow's.

## Where the triage ends and the patch begins

**When it still happens and the user asks for the fix, invoke
`typo3-core-patch-development` before making the change.** That is a step, not a
note about ownership. Load the skill by name and work from it. What crosses over
is the issue number, the verdict, the established code path and any failing
test.

It stands as a step because it did not fire as anything else. A session read
this paragraph and held exactly that handoff. The user asked for the patch, and
the session wrote it over forty more turns without the skill. It decided for
itself the changelog obligation, the suites and databases to run, the commit
trailers and the release branches. Nothing it decided was wrong in the report;
it reconstructed all of it.

This skill owns the statement of what is still true about an issue. That is the
choice out of the backlog and the report read against the branch. It is the
reproduction or the failure to reproduce, and the verdict that comes out.

It stops at the tracker. Nothing here comments, assigns, closes or reopens
anything. You write the verdict, with the comment that closes the issue, for the
person who will. The judgement of a patch somebody pushed is
`typo3-core-patch-review`, which reads the diff rather than the report.

References#

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.

The verdicts, and what each one owes#

markdown
# The verdicts, and what each one owes

One issue gets one verdict. They are not degrees of confidence in a single
finding. Each names a different thing that is true, and each owes different
evidence. Pick the verdict first, and the missing evidence names itself.

Ask the seventh before the other six, of every finding rather than of the ones
that look alarming. It decides where the answer goes rather than what it says:
is what you established a security defect?

## Still happens

The behaviour the report describes is what the branch does today.

Owes: the code path, at its file and line, or the steps that reproduced it and
what you saw. A failing test where a layer can hold it. The branch the
reproduction ran on.

Not enough on its own: that the code looks like it would still do this. A read
of a method is evidence about the method, and the report is usually about the
interaction of two.

## Gone

The behaviour cannot happen any more, and the reason is in the checkout.

Owes: what changed, by name. That is the method that no longer exists, the
branch nobody takes any more, or the entry about the rework. A reproduction that
came out clean is not this verdict. It is the one below.

Fixed by accident is as final as fixed on purpose, but only where you name the
mechanism. "It works for me now" is not a mechanism.

It ends the issue, so it also owes the comment that closes it, below.

## Not reproducible as written

The steps in the report do not produce what it describes. Nothing says whether
that is the report or the branch.

Owes: what you tried, what happened instead, and which of the two it is. The
steps were incomplete, the environment differs, or the report never carried
enough to try. Say which parts of the report you could verify and which you did
not test at all.

This is the verdict people most often write as "gone". They are opposite
outcomes. One closes the issue, the other asks the reporter a question.

## Superseded

Something else already covers this. That is a patch under review, a merged
change, a duplicate, a rework that made the request moot.

Owes: the change or issue number, its state, and whether it does the same work.
An alternative closes an issue only where what it drops is nothing the reporter
was reaching for. Name the arguments and the behaviour the original had and the
replacement does not.

It ends the issue, so it also owes the comment that closes it, below.

## Not a defect

The branch behaves as the project intends. What the report wants is a change of
intent.

Owes: where the intent stands. That is the documentation, the docblock, the test
that pins the behaviour, the changelog entry that introduced it. A verdict of
"works as designed" with no source is an opinion in a maintainer's voice.

This does not close the need. Say what it would take as a feature, and that the
argument for it is a different one. The argument that carries a bugfix is the
same inconsistency inside one version. To find the place where the system
already does the right thing is what turns a wish into a defect.

It ends the issue, so it also owes the comment that closes it, below.

## Cannot be settled here

The question is real, and this checkout cannot answer it.

Owes: what specifically is missing. That is an installation that runs, a
database that behaves differently, or a browser. Or it is the reporter's
configuration, or a version nobody covers any more. And what would settle it, so
the next person starts where this stopped.

Legitimate and underused. It is the honest end of a triage whose reading ran out
before the evidence did.

## A security defect

What the report describes, or what the reading turned up beside it, is something
an attacker can use. That is access to a record the user may not read, or a
value at a sink with no escape. Or it is a check somebody can walk around.

Owes: nothing to the tracker. This verdict is about where the answer goes. So
what it owes is the report the security team receives. The skill's own step says
what that is and which lookup carries the address.

Whichever of the six is also true stays true. You write it for that report
rather than for the issue. A defect that still happens and is exploitable is not
a "still happens" with a note attached. The note is the whole difference in who
may read the answer.

Do not wait until you are sure. A finding that might be exploitable is one the
team rates. The cost of a question to them is an email. The cost of a wrong
decision here is a public exploit against installations with no fix available.

# Before you write any of them

- Which of the report's three claims you verified. That is what the reporter
  saw, what they believed caused it, or what they wanted. Only the first is what
  a checkout settles. To verify the cause and report the issue invalid is the
  standard failure of this work.
- Which branch the verification ran on, said out loud. A verdict with no branch
  is one nobody can repeat.
- Where the suites ran. Inside the checkout's DDEV project or in your own shell
  are two different PHP versions and two different databases. An old report
  about behaviour that depends on either stays open if you leave this unsaid.
- Whether a suite that reported success inspected anything. A green over no
  files turns "not reproducible as written" into "gone" without anybody's
  notice.
- Whether you asked the review server. The cheapest outcome sits there, and it
  costs one call.
- What you did not establish. The part you skipped is the next person's whole
  task, and a verdict that reads as complete hides it.
- Whether the verdict ends the issue. Three of them do, and each hands over the
  comment that closes it, which is the section below. To close, reassign and
  reopen are the maintainer's acts. The text is the triage's.

# The comment that closes it

**Gone**, **Superseded** and **Not a defect** end the issue, and each hands over
the comment that closes it. A verdict that leaves the reason to somebody else
has stopped one step short of what it established.

The other three hand over nothing. **Not reproducible as written** and **Cannot
be settled here** ask the reporter a question instead. To write either up as a
closure is the trap the Gone verdict already names. **A security defect** owes
the tracker nothing. A closing comment is the public step that verdict exists to
prevent.

Forge carries no resolution field. The status is the whole of it, and the
comment is where the reason lives. So the comment says what the status cannot:

- What the verdict rests on: what you tried, on which branch, and what happened
  instead.
- The change that ends it, where you can name one. That is the issue somebody
  filed it under, and per branch the commit and its change on the review server.
  Then the first release each commit is in, which `git tag --contains <commit>`
  answers. The `Releases:` trailer names the branches the author wrote the
  change for, not the release that carries it.
- The branch the fix never reached, where it reached one and not another. A
  reader still on the older line is about to ask that.
- The status to set. Forge closes with three words. **Resolved** where the
  merged patch was filed under this issue. **Closed** where the behaviour went
  with a change filed under another one. **Rejected** where the branch behaves
  as the project intends. A duplicate is a relation on the issue rather than a
  sentence in the comment.

Where you can name no change, say so. "Not reproducible on `main`, verified by
the functional test that copies the record" is a reason to close. A commit
picked to fill the line is not.

A merged patch closes its own issue for a feature and a task, and not for a
bugfix. So a fixed bug whose change names the issue is still open, and this
comment is what ends it.

**You paste the comment into Forge, which renders Textile rather than
Markdown.** A heading, a bullet and a fenced block arrive as the characters you
typed. What survives is plain lines, `#12345` for an issue and a bare URL for a
change. The report around it stays markdown.