|
| 1 | +# Walkthrough — Parts 2 and 14 (2026-07-27) |
| 2 | + |
| 3 | +Continuation of `WALKTHROUGH_PARTS_3_9.md`, which covered Parts 3–9 only. Parts |
| 4 | +2, 4, 10–16 had **never been walked in a browser**. This pass takes Part 2 |
| 5 | +(smoke) and Part 14 (dynamic nodes) — the part that produced the |
| 6 | +`/dynamic-nodes` admin breakage Alex hit on 2026-07-27. |
| 7 | + |
| 8 | +## Environment |
| 9 | + |
| 10 | +Full local stack: backend + Temporal server + **Temporal worker** + postgres + |
| 11 | +minio + deno-runner (`{"ok":true,"denoVersion":"2.1.4"}`). Everything in Parts 2 |
| 12 | +and 14 was runnable. |
| 13 | + |
| 14 | +## Part 2 — Smoke |
| 15 | + |
| 16 | +| # | Verdict | Evidence | |
| 17 | +|---|---|---| |
| 18 | +| 2.1 API reachable | ✅ PASS | `200`, 25 workflows. *Plan nit: it returns `{workflows:[…]}`, not a bare JSON array.* | |
| 19 | +| 2.2 deno-runner health | ✅ PASS | `{"ok":true,"denoVersion":"2.1.4"}` verbatim | |
| 20 | +| 2.3 IDIR login | ⏭ NOT AUTOMATABLE | needs real IDIR credentials; the mock-auth bypass reaches the app shell, which is as close as automation gets | |
| 21 | +| 2.4 Temporal UI | ✅ PASS | `200`; namespaces `["temporal-system","default"]` | |
| 22 | +| 2.5 Workflows list + kind filter | ✅ PASS | 25 rows; segmented control reads `Workflows / Libraries / All` | |
| 23 | + |
| 24 | +## Part 14 — Dynamic (custom-code) nodes |
| 25 | + |
| 26 | +| # | Verdict | Evidence | |
| 27 | +|---|---|---| |
| 28 | +| 14.1 Publish (create) | ⚠️ **PASS only after fixing the plan** | see D-9 | |
| 29 | +| 14.2 Publish negatives | ✅ PASS | `jsdoc-parse` "Missing @workflow-node marker"; `signature-semantics` "Unknown kind: NotAKind"; `ts-check` line 15; `409 DUPLICATE_SLUG` | |
| 30 | +| 14.3 New version | ✅ PASS | `200 {version:2}`; `@name` mismatch → `409 NAME_MISMATCH`; unknown slug → `404` | |
| 31 | +| 14.4 List / detail | ✅ PASS | slug-sorted; `versionCount:2`, `head:2`; detail newest-first `[2,1]` | |
| 32 | +| 14.5 Soft-delete | ✅ PASS | `200`, byte-identical `deletedAt` on the second call (idempotent), excluded from list | |
| 33 | +| 14.6 Merged catalog | ✅ PASS (one half) | both `dyn.*` entries carry `dynamicNodeSlug/Version` + `colorHint:"dyn"` and sit after all 41 static entries. **Cross-group isolation not provable with one API key** — the negative is: a non-admin naming a foreign group is refused outright | |
| 34 | +| 14.7 Management page | ✅ PASS | boilerplate prefilled; strip green→red→green; Publish **disabled** while JSDoc broken; on `400` a toast *"Publish failed (1 error) — see error markers"* plus a line-anchored Monaco marker (`line 14: Type 'Document' is not assignable to type 'number'`); on success *"Published v1"* → `/dynamic-nodes/:slug` | |
| 35 | +| 14.8 In-canvas custom node | ✅ PASS after a fix / ⚠️ one gap | auto-drop was broken — see D-10. Deleted lineage: red **DELETED** badge, settings *"(deleted dynamic node)"*, validator *"Activity type 'dyn.walk-14-8-node' is not registered"* — but **Try is not disabled**, see D-11 | |
| 36 | +| 14.9 Execute (Try) | ⚠️ PARTIAL | the node really ran — both nodes `succeeded` through Temporal + deno-runner via the UI. The **preview does not show the value**, and says something untrue — see D-12 | |
| 37 | +| 14.10 Runtime errors | ✅ PASS | full typed-error matrix green against the live runner (14/14, `RUN_INTEGRATION=1`): timeout → `DynamicNodeTimeoutError`, 6MB stdout → `StdoutTooLarge`, throw → `RuntimeError` w/ stderrTail, bad JSON → `OutputInvalidJson`, missing port → `OutputShapeError` | |
| 38 | +| 14.11 🔒 Net egress | ✅ PASS | `allowNet:[]` → `NotCapable: Requires net access to "example.com:443"`; allowlist the host → `exit 0 {"status":200}`. The allowlist **is** the gate | |
| 39 | +| 14.12 🔒 Remote import | ✅ PASS | `Requires import access to "blocked.example.com:443"` — no outbound fetch | |
| 40 | +| 14.13 🔒 Env isolation | ✅ PASS | `Deno.env.get("PATH")` → `NotCapable: Requires env access to "PATH"` | |
| 41 | +| 14.14 Restore-on-republish | ✅ PASS | re-POST after soft-delete → `201 v3` (history continued, not a fresh v1), `deletedAt:null`, versions `[3,2,1]`; re-POST while live → `409 DUPLICATE_SLUG` | |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## D-9 — the plan's own 14.1 example cannot publish (doc defect, fixed) |
| 46 | + |
| 47 | +The `curl` body in 14.1 declares `dynamicNode(ctx, params)` with no parameter |
| 48 | +types. `deno check` runs under `noImplicitAny`, so the very first thing anyone |
| 49 | +following Part 14 does fails: |
| 50 | + |
| 51 | +``` |
| 52 | +400 ts-check line 8: Parameter 'ctx' implicitly has an 'any' type. |
| 53 | +``` |
| 54 | + |
| 55 | +Everything downstream cascades — 14.3 `400`, 14.4/14.5 `404`, 14.14 unreachable |
| 56 | +— because the lineage is never created. Both real sources of truth (the editor |
| 57 | +boilerplate and `seed-feature-demos.mjs`) type their parameters; only the plan |
| 58 | +does not. With a correctly typed script the whole chain passes on the first run. |
| 59 | + |
| 60 | +## D-10 — publishing a custom node never dropped it on the canvas (fixed, `64d86d73`) |
| 61 | + |
| 62 | +14.8's headline behaviour. The palette modal publishes, closes, toasts |
| 63 | +*"Published v1"* — and the canvas is unchanged. `addDynamicNode` looks the new |
| 64 | +`dyn.<slug>` up in the merged catalog and returns silently when absent, and it |
| 65 | +was absent for two independent reasons: the publish mutation fired |
| 66 | +`invalidateQueries` without returning the promise (so `mutateAsync` resolved a |
| 67 | +round-trip early), and even once awaited, React has not re-rendered at that |
| 68 | +instant — so a closed-over array *and* a ref updated during render are both a |
| 69 | +commit behind. Reads the TanStack cache first now. Verified live: 4 → 5 nodes. |
| 70 | + |
| 71 | +## D-11 — Try stays enabled on a graph that cannot run (open — needs a ruling) |
| 72 | + |
| 73 | +A workflow whose `dyn.*` lineage has been soft-deleted is correctly diagnosed: |
| 74 | +red **DELETED** badge, settings alert, and `1 error — Activity type |
| 75 | +"dyn.walk-14-8-node" is not registered`. But **Try and Run remain enabled**, and |
| 76 | +clicking Try opens the drawer as usual. Running it fails at |
| 77 | +`dynamicNode.resolveLineage` with `DynamicNodeDeletedError` — knowable at author |
| 78 | +time, which is what the *"Fail before the run"* invariant asks for. |
| 79 | + |
| 80 | +14.8 states the criterion as "Try disabled". Not fixed unilaterally because the |
| 81 | +scope is a product decision: **does any validation error disable Try/Run, or |
| 82 | +only errors that make the graph structurally unrunnable?** Those are very |
| 83 | +different products. Needs Alex. |
| 84 | + |
| 85 | +> ⚠️ Method note: my first reading of this said "Valid, Try enabled" and was |
| 86 | +> **wrong** — I sampled at 3.2s while the activity catalog was still loading, |
| 87 | +> and `isRegisteredActivityType` deliberately gives `dyn.*` the benefit of the |
| 88 | +> doubt until it resolves. At 6.5s the error is there. Any check touching |
| 89 | +> `dyn.*` validation must wait for the catalog. |
| 90 | +
|
| 91 | +## D-12 — a succeeded dynamic node previews an untrue message (open) |
| 92 | + |
| 93 | +After a green run, the dyn node's preview reads: |
| 94 | + |
| 95 | +> Preview unavailable — cache evicted. Re-run to repopulate. |
| 96 | +
|
| 97 | +Both halves are wrong. Nothing was evicted, and re-running will *never* |
| 98 | +repopulate it: the node is `@deterministic: false` (the default — the signature |
| 99 | +card even says **NON-DETERMINISTIC (NOT CACHED)**), and §3.3 says such scripts |
| 100 | +must re-execute every run and are deliberately not cached. So the widget offers |
| 101 | +an action that cannot work, for a reason that never happened. |
| 102 | + |
| 103 | +14.9's "preview shows the uppercased URL" therefore holds only for a |
| 104 | +`@deterministic: true` node. The honest message is "not cached — this node is |
| 105 | +non-deterministic", which needs the widget to see the entry's `deterministic` |
| 106 | +flag. |
| 107 | + |
| 108 | +## D-13 — the script editor loads Monaco from a public CDN (open) |
| 109 | + |
| 110 | +The only external request the app makes: |
| 111 | + |
| 112 | +``` |
| 113 | +GET https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs/loader.js |
| 114 | +``` |
| 115 | + |
| 116 | +In this sandbox it fails with `ERR_CERT_AUTHORITY_INVALID` — TLS interception, |
| 117 | +exactly what a government network does — and the editor sits on **"Loading…" |
| 118 | +forever**: no error, no timeout, no fallback, and **Publish stays enabled**, so |
| 119 | +you can publish the untouched boilerplate without noticing. I did that twice by |
| 120 | +accident before spotting it. |
| 121 | + |
| 122 | +Nothing blocks it in deployment (the frontend nginx sets no CSP), so whether the |
| 123 | +authoring surface works depends entirely on each user's network reaching |
| 124 | +jsdelivr. `monaco-editor@0.55.1` is already in `node_modules` at the exact |
| 125 | +version served, so the fix is `loader.config({ monaco })` against the local |
| 126 | +package. Not done here because it changes the build and needs the dependency |
| 127 | +declared — Alex's call. |
| 128 | + |
| 129 | +For the walkthrough I served the byte-identical local copy via a Playwright |
| 130 | +route so the 14.7/14.8 editor checks could run. |
| 131 | + |
| 132 | +--- |
| 133 | + |
| 134 | +## Artifacts created (safe to delete) |
| 135 | + |
| 136 | +| Kind | Name / id | |
| 137 | +|---|---| |
| 138 | +| Library workflow | `WALK-7.8 typed ports` — `cms3tqu3z0000f2gc1b04jxz2` | |
| 139 | +| Workflow | `WALK-14.8 deleted-badge probe` — `cms3v1qds000hf2gcesg9ucy3` | |
| 140 | +| Workflow | `WALK-14.9 dyn execute probe` — `cms3v9c0x000kf2gcn1w3dtqn` | |
| 141 | +| Dynamic node | `uppercase-url` (v3, live) | |
| 142 | +| Dynamic node | `walk-14-7-node` (v1, live) | |
| 143 | +| Dynamic node | `walk-14-8-node`, `my-custom-node` (soft-deleted) | |
| 144 | + |
| 145 | +## Still unwalked |
| 146 | + |
| 147 | +Parts **4, 10, 11, 12, 13, 15, 16** in full, plus the open checks in Parts 3, 5, |
| 148 | +6, 7, 8, 9. Part 15 additionally needs cloud credentials. |
0 commit comments