ADR-032: Missing alt texts are fixed in a draft workspace 

Status: Accepted

Date: 2026-11-05

Amends: ADR-026: A Site-Wide Check for Missing Alt Texts (the refusal of a draft workspace, the decorative mark written in live only).

Context 

"Optimieren" in the list of missing alt texts (ADR-026: A Site-Wide Check for Missing Alt Texts) refused an editor working in a draft workspace: the start, the switch to another image, "Nächsten Treffer optimieren", "Erneut anbieten" and "Als dekorativ markieren" answered 409 workspace, the list showed no button, and set_file_reference_decorative refused every workspace but live. The reason was nr-llm: its writers wrote the live workspace only.

That has changed. A conversation records the workspace its turn was handed over in, the worker sets the user up in it, and the run's actor carries it (nr-llm ADR-260, this extension since 0.33.0). nr-llm's set_file_reference_alternative_text writes a version of the reference in the workspace the run recorded (nr-llm #1294), and refuses a draft workspace the run did not record. The list itself already read the editor's workspace (ADR-026: A Site-Wide Check for Missing Alt Texts, Workspaces).

What core does with the one write involved, a datamap of sys_file_reference by the live uid, as a backend user in a draft workspace (DataHandler::process_datamap(), TYPO3 v14.3.10 lines 921-945, v13.4.37 lines 909-933): a live record of a workspace-aware table that the workspace may not edit live (workspaceCannotEditRecord(), v14.3.10 lines 9564-9584, v13.4.37 lines 9430-9471) gets its existing version in the user's workspace written, or a version created by a version/new command and then written. A row of another workspace is refused (v14.3.10: "Workspace ID of record does not match current user workspace", v13.4.37: "Workspace ID of record didn't match current workspace"). sys_file_reference is workspace-aware (versioningWS) in both versions.

Decision 

A draft workspace is no refusal. AltTextGate::refusal() answers workspace only for a user who may work in no workspace (core sets a negative workspace): there a turn records none, and nothing could be saved. The label error.altTextWorkspace says that. In a draft workspace it answers liveOnly where the installation's TCA turns the versioning of sys_file_reference off or lets it be edited live (see below): both writers refuse there, and the label error.altTextLiveOnly says so. Every other step of the flow works in the workspace the editor works in when they take it, and the turn records it.

The image is decided on as the workspace shows it. ReferenceWriteAccess reads the reference and its record as nr-llm's writer reads them (nr-llm ADR-260): in a draft workspace the live row overlaid with its version there, or a row new in that workspace; a version row named by its own uid and another workspace's row are not there. The pages and the inline owners are read live, as nr-llm reads them, so the gate lets through what the writer accepts and nothing else (ImageReferenceRepository::findAsTheUserSees()).

The decorative mark writes into the workspace the run recorded. DecorativeImageWriter writes through nr-llm's ToolDataHandler as before. In a draft workspace it writes only where the run's actor recorded that workspace (ToolExecutionContext::$actor->workspaceId, nr-llm @api), and only where core keeps a draft of sys_file_reference (versioningWS set, versioningWS_alwaysAllowLiveEdit not, which TYPO3 13.4 also honours for a versioned table). The read-back reads the version through the live uid. The tool's answer ends with "Stored as a draft in the workspace …", so the model does not report a draft as live.

The card names the workspace. set_file_reference_decorative adds "Workspace: “…” — saved there as a draft, live stays unchanged until the workspace is published" (the acting user's workspace, the one the run writes into), shows the record as the workspace shows it, and shows only the refusal for a call its write will refuse for the workspace, so no card promises a draft that is not stored. Who may read the card is nr-llm's decision (nr-llm ADR-136, ADR-260); the tool's own check runs against the viewer in the viewer's workspace.

Amended 2026-11-08: the proposal's card names the workspace too. It shows the proposed text instead of the preview lines of nr-llm's set_file_reference_alternative_text, and with them it hid nr-llm's line naming the workspace. ApprovalCallPresenter hands that line over as the call's workspace, recognised by nr-llm's own label (approvalPreview.workspace.target) in the preview's language and, without one, by nr-llm's source text; the proposal's card and the proposal block of a process card that shows nr-llm's structured values show it. In live nr-llm writes no such line, and the card shows none. The decorative mark's line is built from the same nr-llm labels (with approvalPreview.value.quoted for the quotation marks) instead of a translation of this extension's own, which read the same in English and German.

The model reads the texts of the workspace. AltTextContext reads the page, the content element, the elements around it and a record's text fields as the workspace of the turn shows them.

Provenance needs nothing here. nr-llm dispatches AfterAiRecordWrittenEvent for the tool's write target with the acting user's workspace; its MarkAiWrittenRecord marks pages and tt_content only, not references.

Consequences 

  • An editor in a draft workspace gets "Optimieren" and the whole flow; the alt text and the decorative mark are stored as a version of the reference in that workspace, and live stays as it is until the workspace is published.
  • nr-llm must be a version that contains #1294 (the first release after 0.43.17); the release of this extension requires it. With 0.43.17, nr-llm's alt text writer still refuses a draft workspace: for a live reference after its card, for a reference new in the workspace already on the card. The decorative mark works with 0.43.16 and later.
  • ✗ A reference on a record whose page, or on an inline record whose owner, is new in the workspace is in the list but refused at "Optimieren" with the neutral 403, because nr-llm reads those pages and owners live. In a page's own file field the page is the record and is read as the workspace shows it, as nr-llm reads it.
  • ✗ An alt text that is set live and emptied only in the workspace is still not listed there (ADR-026: A Site-Wide Check for Missing Alt Texts).
  • ✗ An element moved onto the page in the workspace is not among the elements around the image that the model reads. On a page with more than 500 elements in the image's language, the elements around it are picked from the first 500 by sorting.
  • ✗ Writing the alt text or the mark on a reference whose draft was already sent to review sets its stage back to "editing", as every DataHandler write in a workspace does and as nr-llm's field tools do.
  • ✗ The scaled image the worker sends to the model is cropped as the live reference says: outside a backend request, core's ResourceFactory reads the reference without the workspace overlay.