You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(workflow-builder): never-cached copy must not tell built-in authors to edit a script (D-18a)
Found walking 9.5. document.updateStatus is nonCacheable in the catalog, so my
D-12 copy told its author to 'Tag it @deterministic true' — there is no script
and no tag. Every nonCacheable built-in hits it: azureOcr.submit,
document.storeRejection, every benchmark writer.
describeNoOutput now splits on isDynamicNode: the dynamic-node text keeps the
actionable advice, a built-in gets copy about the activity itself. Verified
live on two nodes; test falsified against the fix removed.
Also records the Part 9 walk it came out of. Azure OCR works — the standard-OCR
chain finishes in under 12s on the sample invoice — which unblocked 9.4a, 9.5a
and 9.10 (all pass) and the verified half of 9.5/9.5b/9.5c/9.10a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* D-18a — only a dynamic node's author can change whether its output is
112
+
* cached (by tagging the script `@deterministic true`). A built-in
113
+
* activity is `nonCacheable` by catalog design — `document.updateStatus`,
114
+
* `azureOcr.submit`, every `benchmark.*` writer — and telling that author
115
+
* to edit a script they do not have is an instruction they cannot follow.
116
+
*/
117
+
isDynamicNode?: boolean;
118
+
},
119
+
): NoOutputCopy{
108
120
switch(reason){
109
121
case"no-run":
110
122
return{
@@ -165,7 +177,9 @@ export function describeNoOutput(reason: NoOutputReason): NoOutputCopy {
165
177
return{
166
178
reason,
167
179
message:
168
-
"This step ran, but its output isn't cached: the script is marked non-deterministic, so it re-executes every run instead of being stored. Tag it `@deterministic true` to make its output previewable.",
180
+
options?.isDynamicNode===true
181
+
? "This step ran, but its output isn't cached: the script is marked non-deterministic, so it re-executes every run instead of being stored. Tag it `@deterministic true` to make its output previewable."
182
+
: "This step ran, but this activity never caches its output — it re-executes on every run instead of being stored, so there's nothing here to preview.",
Copy file name to clipboardExpand all lines: docs-md/workflows/MANUAL_TEST_PLAN.md
+7-7Lines changed: 7 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -466,13 +466,13 @@ Requires Temporal server + **worker** + visibility store + `activity_output_cach
466
466
-[x]**9.3 Upload & Try.** On a saved source.upload workflow → the source node’s **Upload & Try** → drop a PDF. **Pass:** file commits to blob, a run starts, source node’s DocumentPreview shows the doc, canvas animates.
467
467
-[x]**9.4 Status badges + active edges.** Watch a Try. **Pass:** per-node badge progresses pending→running→succeeded/failed/skipped on a ~1.5s poll (pauses on tab blur); active edge animates blue while source running/target pending.
-[]**9.4a The path the run took (G-014).** Use a switch-heavy workflow (`multi-page-report-workflow`). Watch a Try, then let it finish. **Pass:** while running, the already-walked hops render in the lighter **taken-path** blue *behind* the animated in-flight edge (both cues visible at once — the animation is not replaced by the trail); once the run finishes nothing animates but the **full path it took stays drawn**. On the switch node, exactly ONE outgoing edge is on the path — the branch that was not chosen stays in its resting style. Repeat via **9.9 Replay** on a finished run: the path is still drawn, even though nothing is running. *(Before G-014 an edge was only marked while its source was CURRENTLY running, so a replay showed no path at all.)*
469
+
-[x]**9.4a The path the run took (G-014).** Use a switch-heavy workflow (`multi-page-report-workflow`). Watch a Try, then let it finish. **Pass:** while running, the already-walked hops render in the lighter **taken-path** blue *behind* the animated in-flight edge (both cues visible at once — the animation is not replaced by the trail); once the run finishes nothing animates but the **full path it took stays drawn**. On the switch node, exactly ONE outgoing edge is on the path — the branch that was not chosen stays in its resting style. Repeat via **9.9 Replay** on a finished run: the path is still drawn, even though nothing is running. *(Before G-014 an edge was only marked while its source was CURRENTLY running, so a replay showed no path at all.)*
470
470
- API: `GET /api/workflows/<WF>/runs/<RUN>/node-statuses` → a switch / humanGate-fallback / errorPolicy-fallback node carries `selectedEdgeId`; every other node omits it (meaning all outgoing normal edges were taken).
471
-
-[ ]**9.5 Preview widgets.****Pass:** Document → thumbnail strip; Segment[] → polygon overlays; OcrResult → K/V table + “View raw”; Classification → label pill + confidence bar. A kind with **no** dedicated widget (`ValidationResult`, `Reference`, bare `Artifact`, `Document[]`, scalar `Segment`, …) must render the **generic view**: a dimmed caption naming the kind (“ValidationResult — no dedicated preview, showing the raw value”) above a JSON snippet with **View raw**. **No node may render an empty card.** (G-011)
471
+
-[ ]**9.5 Preview widgets.***(generic-view + no-empty-card clauses verified 2026-07-27; the four kind-specific widgets still need a graph that produces them)***Pass:** Document → thumbnail strip; Segment[] → polygon overlays; OcrResult → K/V table + “View raw”; Classification → label pill + confidence bar. A kind with **no** dedicated widget (`ValidationResult`, `Reference`, bare `Artifact`, `Document[]`, scalar `Segment`, …) must render the **generic view**: a dimmed caption naming the kind (“ValidationResult — no dedicated preview, showing the raw value”) above a JSON snippet with **View raw**. **No node may render an empty card.** (G-011)
-[]**9.5a Multi-output preview.** Use a node with more than one output port (`ocr.spellcheck` — `correctedResult` / `corrections` / `metadata`; or `document.classify`, `azureOcr.poll`). After a Try, look at that node’s preview. **Pass:** a row of small port chips appears above the preview, labelled with the catalog port labels; the first port is selected and its value is shown; clicking another chip switches the pane to **that port’s** value, styled by that port’s own kind. A single-output node shows **no** chip row (unchanged). (G-011)
474
-
-[ ]**9.5b Bound-but-empty and unbound outputs.** On a node whose output binding points at a ctx key the run never wrote: **Pass:** “No value was recorded for this output (expected `<Kind>`).” On a node with no output binding at all: **Pass:** “This step’s output isn’t bound to a workflow value yet, so there’s nothing to read.” Neither may be a blank card. (G-011)
475
-
-[ ]**9.5c OCR previews show values, not a blob pointer.** On an OCR step (`azureOcr.extract`, `mistral-ocr.process`, any `ocr.*` correction). **Pass:** the K/V table shows the **payload’s** keys (e.g. `fileName`, `status`, `extractedText`, `pages`, `documents`) — **not**`blobPath` / `storage` / `byteLength`. For a custom-model run, drilling into `documents → fields` via **View raw** shows the extracted field values. If the payload was bounded, a dimmed line reads “Truncated preview — pages: showing the first 5 of N items; …”, naming every omission. If the blob is gone (delete it from MinIO and re-open the preview), **Pass:** the pointer is shown **with** “The full OCR payload is no longer in storage — showing the reference only.” — never a bare blob key with no explanation. (G-022)
473
+
-[x]**9.5a Multi-output preview.** Use a node with more than one output port (`ocr.spellcheck` — `correctedResult` / `corrections` / `metadata`; or `document.classify`, `azureOcr.poll`). After a Try, look at that node’s preview. **Pass:** a row of small port chips appears above the preview, labelled with the catalog port labels; the first port is selected and its value is shown; clicking another chip switches the pane to **that port’s** value, styled by that port’s own kind. A single-output node shows **no** chip row (unchanged). (G-011)
474
+
-[ ]**9.5b Bound-but-empty and unbound outputs.***(unbound clause verified verbatim 2026-07-27; bound-but-empty not yet)*On a node whose output binding points at a ctx key the run never wrote: **Pass:** “No value was recorded for this output (expected `<Kind>`).” On a node with no output binding at all: **Pass:** “This step’s output isn’t bound to a workflow value yet, so there’s nothing to read.” Neither may be a blank card. (G-011)
475
+
-[ ]**9.5c OCR previews show values, not a blob pointer.***(payload-values + truncation clauses verified 2026-07-27; the blob-deleted clause not yet)*On an OCR step (`azureOcr.extract`, `mistral-ocr.process`, any `ocr.*` correction). **Pass:** the K/V table shows the **payload’s** keys (e.g. `fileName`, `status`, `extractedText`, `pages`, `documents`) — **not**`blobPath` / `storage` / `byteLength`. For a custom-model run, drilling into `documents → fields` via **View raw** shows the extracted field values. If the payload was bounded, a dimmed line reads “Truncated preview — pages: showing the first 5 of N items; …”, naming every omission. If the blob is gone (delete it from MinIO and re-open the preview), **Pass:** the pointer is shown **with** “The full OCR payload is no longer in storage — showing the reference only.” — never a bare blob key with no explanation. (G-022)
476
476
- API: `GET /api/workflows/<WF>/preview-cache?nodeId=<NODE>` → response carries `blobExcerpts` keyed by `blobPath`, each with `status`, `truncated`, `omissions[]` and the `limits` applied.
477
477
-[x]**9.6 Incremental re-run (cache).** Run → tweak one node param (e.g. `confidenceThreshold`) → Try again same input. **Pass:** unchanged upstream nodes flash **violet (cache hit/skipped)**; tweaked node + downstream re-execute; that preview updates. ⚠️ To force a miss there’s no UI — `DELETE FROM activity_output_cache WHERE node_id='<id>'`.
478
478
-[x]**9.7 Cancel-on-new-Try.** Start a Try, then Try again mid-run. **Pass:** prior run cancelled server-side (shows **cancelled** in Run history); exactly one active run.
@@ -482,11 +482,11 @@ Requires Temporal server + **worker** + visibility store + `activity_output_cach
482
482
-[x]**9.9a Replay shows the graph that RAN (G-004).** Run a workflow, then **change it** — add a node, delete one, save (creating a new version). Now **More ▸ Run history ▸ Replay** the earlier run. **Pass:** the canvas renders the **older version's** graph: the node you added is **not** on it, and the node you deleted **is** (wearing its result). The replay chip names that version (`v2`, not head). *(Before G-004 statuses were matched by id and painted onto whatever config was on screen — you read yesterday's results on today's diagram.)* Click **Clear**: the canvas returns to the live graph, including the added node.
483
483
-[x]**9.9b Replay never risks unsaved work (G-004).** Make an **unsaved** edit (rename a node — do NOT save). Enter replay per 9.9a, try to edit something on the historical graph, **press Undo while still in replay**, then **Clear**. **Pass:** your unsaved rename is still there, the replayed version's content did **not** leak into the editing config, the leave-page guard still warns, Undo/Redo are **disabled** while replaying, and once you leave replay **Undo** steps back through your own edits only (replay adds no history entries).
484
484
-[x]**9.9c Version that can't be loaded.** Replay a run whose version was removed (or stop the backend mid-replay). **Pass:** the replay chip turns orange and reads **"v{n} unavailable, showing current graph"** — it never silently pretends the current graph is the one that ran.
485
-
-[]**9.10 Cache-evicted preview.** Replay a run whose cache row was deleted **for a node that SUCCEEDED** (`DELETE FROM "ActivityOutputCache" WHERE "nodeId"='<id>'`). **Pass:** red alert “Preview unavailable — cache evicted. Re-run **v{n}** (the version you are viewing) to repopulate.” + a **Re-run v{n}** button that fetches the original `initialCtx` and starts a fresh Try. This must remain distinct from 9.10a — eviction is the ONLY no-output state that offers a Re-run.
485
+
-[x]**9.10 Cache-evicted preview.** Replay a run whose cache row was deleted **for a node that SUCCEEDED** (`DELETE FROM "ActivityOutputCache" WHERE "nodeId"='<id>'`). **Pass:** red alert “Preview unavailable — cache evicted. Re-run **v{n}** (the version you are viewing) to repopulate.” + a **Re-run v{n}** button that fetches the original `initialCtx` and starts a fresh Try. This must remain distinct from 9.10a — eviction is the ONLY no-output state that offers a Re-run.
486
486
-[x]**9.10b Re-run targets the version being viewed (G-024).** With 9.9a's setup (a run on an older version, plus newer edits saved as head), trigger the Re-run from 9.10 while replaying. **Pass:** the POST body carries `workflowVersionId` for the **replayed** version, and the resulting run's history row pins that version — not head. Outside replay (no version pin) the button reads plain **Re-run** and targets head, which is correct there. *(Before G-024 the offered remedy silently ran a different graph and filed the result as if it were the same thing.)*
-[x]**9.10c Retention (G-024).** Intermediate values are kept **14 days** by default, not 24 hours — "the run happened yesterday" is the most common debugging situation there is and used to land exactly on the old boundary. **Pass:**`SELECT "createdAt", "expiresAt" FROM "ActivityOutputCache" ORDER BY "createdAt" DESC LIMIT 5;` shows `expiresAt ≈ createdAt + 14 days`, and replaying a run from **two days ago** still shows its previews rather than the cache-evicted alert. Set `ACTIVITY_OUTPUT_CACHE_TTL_MS=3600000` in the worker/backend env and restart: freshly written rows now expire in an hour, proving the window is tunable without a code change. ⚠️ The GC sweep is operator-started (`cache-gc-singleton`); expiry is enforced lazily on read regardless, so an unswept row past `expiresAt` still reads as evicted.
489
-
-[ ]**9.10a Distinct no-output states.** Each situation below must show its **own** copy, and none may offer a Re-run button. (G-012)
489
+
-[ ]**9.10a Distinct no-output states.***(control-flow clause verified 2026-07-27 on 3 nodes; the other five states not yet)*Each situation below must show its **own** copy, and none may offer a Re-run button. (G-012)
490
490
-**Mid-Try, node not reached yet:** “Waiting — the run hasn’t reached this step yet.” (Previously this was a blank card — the live run showed nothing at all.)
491
491
-**Mid-Try, node executing:** “Running now — output appears when this step finishes.”
492
492
-**Replay, branch not taken:** on a switch-heavy workflow, replay a run and look at a node on the untaken branch → “This step was never reached — the run took a different branch.”
0 commit comments