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