wiki/c-work/4-done/2026-04-trustee-actionresult-schema-and-datapicker-crash.md
ValueOn AG d4095db4f2 fixes
2026-04-25 01:13:24 +02:00

109 lines
5.6 KiB
Markdown

# Trustee ActionResult schema alignment + DataPicker white-screen fix
Status: done
Date: 2026-04-24
Scope: gateway (trustee adapter, method definition, port catalog, tests),
frontend_nyla (FlowEditor / DataPicker)
## Symptoms reported
- Trustee nodes in the expense-receipt workflow ("Spesenbeleg") could not
recognise their input data: opening the DataRef picker for
`processDocuments.documentList` or `syncToAccounting.documentList` showed no
compatible upstream candidates even though `extractFromFiles` /
`processDocuments` were obviously connected upstream.
- Setting any parameter on the `trustee.syncToAccounting` node caused the
whole graph editor to white-screen.
## Root causes
Two independent bugs collided on the same UI path.
### 1. Adapter drift around `ActionResult.documents`
Every trustee action returns `ActionResult.isSuccess(documents=[...])` at
runtime (see `gateway/modules/datamodels/datamodelChat.py` and the three
action implementations). However the typed-action surface disagreed in three
places:
| Layer | What it said | What it should say |
| --- | --- | --- |
| `nodeDefinitions/trustee.py::extractFromFiles.outputPorts[0].schema` | `DocumentList` | `ActionResult` |
| `methodTrustee.py::extractFromFiles.outputType` | `DocumentList` | `ActionResult` |
| `methodTrustee.py::processDocuments.outputType` | `TrusteeProcessResult` | `ActionResult` |
| `methodTrustee.py::syncToAccounting.outputType` | `TrusteeSyncResult` | `ActionResult` |
| `portTypes.py::PORT_TYPE_CATALOG['ActionResult']` | only `success / error / data` | also `documents: List[ActionDocument]` |
| `portTypes.py::PORT_TYPE_CATALOG['ActionDocument']` | not registered | new `PortSchema` with `documentName / documentData / mimeType / fileId / fileName` |
| `nodeDefinitions/trustee.py::processDocuments.parameters.documentList.type` | `DocumentList` | `List[ActionDocument]` (the concrete shape `_resolveDocumentList` consumes) |
| `nodeDefinitions/trustee.py::syncToAccounting.parameters.documentList.type` | `DocumentList` | `List[ActionDocument]` |
| `nodeDefinitions/trustee.py::processDocuments.inputPorts[0].accepts` | `DocumentList / Transit` | also `ActionResult` |
Because the catalog `ActionResult` schema had no `documents` field, the
DataPicker's `_buildPathsFromSchema` could not surface the canonical
`upstream → documents → * → documentName` path. Strict-filter then
correctly rejected every other candidate, so the user saw "no input data".
### 2. Rules-of-Hooks violation in `DataPicker`
`frontend_nyla/src/components/FlowEditor/nodes/shared/DataPicker.tsx` called
`useMemo(... loopAncestorIds ...)` *after* the `if (!open) return null;`
early return. As soon as the picker opened (e.g. when clicking a parameter
on the sync node), React saw "rendered more hooks than during the previous
render", threw, and there is no `ErrorBoundary` around the canvas, so the
whole tree unmounted to a white screen.
## Fix
- `frontend_nyla/src/components/FlowEditor/nodes/shared/DataPicker.tsx`
- Moved both `useMemo` calls (`connections`, `loopAncestorIds`) **above**
the `if (!open) return null;` guard.
- Added a comment block explaining why hooks must stay above the early
return so the next refactor doesn't reintroduce the bug.
- `gateway/modules/features/graphicalEditor/portTypes.py`
- Added `ActionDocument` `PortSchema` (mirrors
`datamodelChat.ActionDocument`).
- Added `documents: List[ActionDocument]` field on the `ActionResult`
schema so the DataPicker can drill into it.
- `gateway/modules/features/graphicalEditor/nodeDefinitions/trustee.py`
- `extractFromFiles.outputPorts[0].schema = "ActionResult"`.
- Both consumer params (`processDocuments.documentList`,
`syncToAccounting.documentList`) typed as `List[ActionDocument]`.
- `processDocuments.inputPorts[0].accepts` includes `ActionResult`.
- `gateway/modules/workflows/methods/methodTrustee/methodTrustee.py`
- `extractFromFiles.outputType = "ActionResult"`.
- `processDocuments.outputType = "ActionResult"`,
`documentList.type = "List[ActionDocument]"`.
- `syncToAccounting.outputType = "ActionResult"`,
`documentList.type = "List[ActionDocument]"`.
## Verification
- `gateway/tests/unit/nodeDefinitions/test_trustee_schema_compliance.py`
rewritten to enforce the new alignment plus two new regressions
(`test_catalog_ActionResult_exposes_documents_field`,
`test_catalog_ActionDocument_is_registered`).
- `python -m pytest gateway/tests/unit/graphicalEditor/
gateway/tests/unit/nodeDefinitions/
gateway/tests/unit/workflow/test_trusteeQueryData.py` → 112 passed.
Includes `test_staticNodesHaveNoDriftAgainstLiveMethods`, the strict
drift gate added during the Phase-4 cleanup.
- `npx vitest run src/components/FlowEditor/` → 32 passed (DataPicker,
RequiredAttributePicker, paramValidation, CanvasHeader).
## Why the existing adapterValidator did not catch this
Adapter rules 1-5 only check that adapter-declared parameters / output
types resolve in the catalog and that required action params are covered.
They do **not** check whether the catalog schema is structurally complete
(e.g. "ActionResult declares `documents` because the runtime emits it").
Capturing that would require a runtime/return-value introspection pass —
tracked as a follow-up in `local/notes/issues.md` if the picker keeps
losing fields.
## Pick-not-Push contract recap
Producer always emits `ActionResult { success, error, documents, data }`.
Consumers bind their `documentList`-style param via DataRef to
`upstream → documents` (or `documents → *` when iterating in a loop). No
auto-wire, no implicit aliasing — the binding is fully visible in the
editor via `DataRefRenderer`.