109 lines
5.6 KiB
Markdown
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),
|
|
ui-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 `platform-core/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`
|
|
|
|
`ui-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
|
|
|
|
- `ui-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.
|
|
- `platform-core/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.
|
|
- `platform-core/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`.
|
|
- `platform-core/modules/workflows/methods/methodTrustee/methodTrustee.py`
|
|
- `extractFromFiles.outputType = "ActionResult"`.
|
|
- `processDocuments.outputType = "ActionResult"`,
|
|
`documentList.type = "List[ActionDocument]"`.
|
|
- `syncToAccounting.outputType = "ActionResult"`,
|
|
`documentList.type = "List[ActionDocument]"`.
|
|
|
|
## Verification
|
|
|
|
- `platform-core/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 platform-core/tests/unit/graphicalEditor/
|
|
platform-core/tests/unit/nodeDefinitions/
|
|
platform-core/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`.
|