Skip to content

Drop the retired draw node from the agent-facing docs - #372

Merged
mikkel merged 4 commits into
mainfrom
fix/drop-retired-draw-refs
Jul 30, 2026
Merged

Drop the retired draw node from the agent-facing docs#372
mikkel merged 4 commits into
mainfrom
fix/drop-retired-draw-refs

Conversation

@mikkel

@mikkel mikkel commented Jul 29, 2026

Copy link
Copy Markdown
Member

Two separate untruths on the same agent-facing pages, plus the guard that stops them coming back.

1. The retired draw node

The 🎨 Draw node retired on 2026-07-22. Its Gemini omni model left the NanoGPT catalog.

The runtime is already correct. The editor does not register the type. Both headless executors treat it as an unknown type: they warn on load and throw UnsupportedNodeError at run(), before any network call. The documentation was not correct. Four live surfaces still presented draw as a current, usable node type. An agent that reads those surfaces writes a graph that cannot run.

  • llms-full.txt:158 listed draw in the node catalog.
  • llms-full.txt:259 and llms-full.txt:378 listed draw in both NanoGPT node tables.
  • guide/graph-format.html:132 said "(LLM/Draw vision references)".
  • guide/graph-format.html:158 was a full table row for the draw type and its fields.
  • guide/run-headless.html:135 listed draw among the nodes that run headlessly.
  • growth/i18n-drafts/zenn-comfyui-ja.md:81 advertised Draw. That draft is unposted, so the claim never shipped. It also described Draw wrongly, as a hand-drawing input.

(Line numbers are as of 552b41f^, the tip before this branch.)

llms-full.txt is generated by scripts/gen-llms-full.mjs, and the header says "do not edit by hand". The file was stale. All 3 draw mentions came from sources that dropped draw already: index.html for the node catalog, and the nanoodle-js and nanoodle-py READMEs for the 2 tables. I regenerated the file instead of editing it. The regeneration also picks up unrelated README drift that built up since the last run, so the diff is larger than the draw fix alone.

guide/graph-format.html loses the draw row and the "LLM/Draw" port note. It gains a "Retired type" bullet under Loader rules. That was a deliberate call. The page documents the file format, not the menu. An author who holds a graph saved before 2026-07-22 opens the JSON, finds a draw node, and comes to this page for an answer. Deletion alone gives no answer. The new bullet states the retirement date, states that old graphs still load, and names the repair: image to generate, edit to edit a wired image.

growth/i18n-drafts/zenn-comfyui-ja.md names Join instead of Draw.

Historical records stay untouched. changelog.html, updates.json, feed.xml, docs/ and proof/ still record that the node existed and then retired. The changelog entry that announces the retirement is correct and stays.

Legacy compatibility is untouched. No runtime code changed.

2. "Not supported headlessly" was false

Review of the first commit caught a second untruth in the same paragraphs, and llms-full.txt in that same commit contradicted it.

guide/run-headless.html: Browser-only media-processing nodes (resize, vframes, combine, soundtrack, trim, extractaudio) are not supported headlessly: workflows containing them load with a warning and fail fast at run(), before any network call.

guide/graph-format.html: the headless executors reject them up front, before any paid call.

Both are false. The truth, read off the executors:

  • nanoodle-js/src/local-media.mjs implements all 6 ops headlessly. It runs a pure-JS path first (PNG resize through its own PNG codec, PCM-WAV trim, MP4CAT lossless mp4 concat) and shells out to ffmpeg/ffprobe on PATH for everything else.
  • nanoodle-py/src/nanoodle/local_media.py has no pure path. All 6 shell out to ffmpeg.
  • Neither library marks any node type unsupported. nanoodle-py's graph.UNSUPPORTED_TYPES evaluates to the empty tuple; nanoodle-js's graph.mjs lists all 6 as local: true. UnsupportedNodeError is raised only for an unknown node type (workflow.mjs:183-188, workflow.py:208-215) — which is what a legacy draw node is, and the only case the up-front refusal covers.
  • nanoodle-mcp/src/ffmpeg-check.mjs warns at startup when ffmpeg is missing, for exactly these 6 types. It would have nothing to warn about if they could not run.

A false "not supported" costs an agent a capability it has. A false "supported" costs a failed paid run. So the fix states it per node and per language:

node JavaScript Python
resize pure JS for a PNG source; ffmpeg for any other format ffmpeg
trim pure JS for a PCM WAV source; ffmpeg for any other format ffmpeg
combine pure JS lossless mp4 remux when every clip is mp4 with matching stream parameters; ffmpeg otherwise ffmpeg
vframes ffmpeg ffmpeg
soundtrack ffmpeg ffmpeg
extractaudio ffmpeg ffmpeg

guide/run-headless.html carries that table plus one thing the old text got backwards in the reader's favour: a missing-ffmpeg error does not arrive before any paid call. It arrives when that node's turn comes, so paid nodes upstream have already spent. The page says so and tells you to prove your setup with a media-only graph first. guide/graph-format.html's "runs" column now reads local media instead of browser-only and links to that table.

3. Nothing stopped the staleness returning

No guard checked llms-full.txt: none of the 51 scripts/check-*.mjs mentioned it, and scripts/gen-llms-full.mjs was not in .githooks/pre-commit. Worse, its clone() returned the cached .tmp-libs/<repo> whenever README.md existed and never re-fetched, so a future regeneration could silently reproduce a months-old library README. The file was correct by luck.

  • scripts/gen-llms-full.mjs now hard-resets the cached clone to the remote default branch on every run, and fails loudly (naming the fix) when it cannot. --offline reuses the cache and says so. --check regenerates and diffs instead of writing. NANOODLE_JS / NANOODLE_PY read a local checkout instead of cloning. Its section builders are exported so a guard can rebuild them.
  • scripts/check-llms-full.mjs (new) is offline and deterministic. It regenerates every section sourced from this repo (llms.txt, README.md, index.html) and diffs byte for byte, then asserts every node type named in the two libraries' supported-node tables is still registered in index.html — the exact check the three stale draw rows would have failed.
  • .githooks/pre-commit runs it when llms-full.txt, llms.txt, README.md, index.html or either script is staged.
  • .github/workflows/checks.yml checks out nanoodle-py as well and runs gen-llms-full.mjs --check against both fresh siblings. That covers the library sections the offline guard cannot rebuild, and it needs no network beyond the checkouts.

How I verified it

  • grep -rniE "🎨|\bdraw\b" . over the whole repo, minus .git, vendor/ and proof/. The only remaining hits are canvas code (drawImage, drawFinderPattern, "draw rect"), the image node's search keyword string at index.html:6042, and the historical records listed above.
  • node scripts/gen-llms-full.mjs, then grep -n draw llms-full.txt returns nothing.
  • node scripts/gen-llms-full.mjs --check passes against freshly fetched clones of both libraries, and again through the NANOODLE_JS / NANOODLE_PY local-checkout path that CI uses.
  • The new guard was proved to fail, not just to pass. Injecting a draw cell into a supported-node row fails it with the node-key message. Renaming a catalog entry fails it with a byte diff naming the line. Editing a cached library README and regenerating fails both the generator's --check and the guard.
  • All 53 guards run clean on the updated branch: for f in scripts/check-*.mjs; do node "$f"; done → 53 pass, 0 fail. (51 at the branch point, 52 after growth: refresh the 4 slipped launches, remove the stale r/mcp gate, add a launch checklist #373 added check-launch-facts.mjs, 53 with check-llms-full.mjs.)
  • scripts/check-js-parity.mjs runs, and does not skip, against nanoodle-js at its main tip: 24 scenarios match.
  • The pre-commit hook passed on every commit without --no-verify.

I did not touch index.html, play.html, growth/shares.md, growth/launch-checklist.md, README.md, or the built-with-nanoodle links.

Updated to main before merge

main moved to 89f998c (#373) after this branch was written. I merged it in (no rebase, no force-push). Two resolutions:

.githooks/pre-commit#373 and this branch both edited the same one-line early-exit chain, and both appended a guard numbered 41). I unioned them, so the chain now carries touches_launchfacts and touches_llmsfull, and #373's guard keeps 41) while this branch's becomes 42). #373's 3-line comment above the chain is kept.

A dropped clause is a guard that never runs, so I proved the union instead of reading it. Staging one modified file into a scratch index and running the real hook with a node that only names the script it was handed:

staged file guards that ran
growth/launch-checklist.md check-launch-facts.mjs
llms-full.txt check-llms-full.mjs
README.md (in both patterns) check-launch-facts.mjs, check-llms-full.mjs

llms-full.txtnanoodle-py 0.5.0 landed (nanoodle-py#11, nanoodle-py#12) after this branch generated the file, and it rewrote two sections the generator copies verbatim: ## Quickstart (library) and ## CLI. The committed file described 0.4.0, so the new CI step would have gone red on merge. Regenerating adds 88 lines: the breaking input-key rename in 0.5.0 with its per-workflow key table, optional input nodes, run-wide timeout= releasing its workers, prompt length caps, and --json printing the same envelope on a failed run.

That regeneration is also the proof the clone() fix works. The cache held stale checkouts, and the run refreshed both before reading them — nanoodle-py 91741de → deea945, nanoodle-js a900ed1 → bff73bb. The old clone() would have re-emitted 0.4.0 and said nothing.

Related

Same false claim, different repo: nanoodlecom/nanoodle-skill#5 fixes references/graph-format.md, which called the 6 media nodes browser-only while that skill's own SKILL.md said the opposite.

The 🎨 Draw node retired on 2026-07-22 when its Gemini omni model left the
NanoGPT catalog. The runtime is correct: the editor skips the type and both
headless executors warn on load, then refuse the run. The documentation was
not correct. It still listed `draw` as a current node type, so an agent that
read these pages wrote a graph that cannot run.

- llms-full.txt is generated by scripts/gen-llms-full.mjs and was stale. The
  3 `draw` mentions came from index.html and from the 2 library READMEs.
  All 3 sources dropped `draw` already. Regenerated the file. The regeneration
  also picks up unrelated README drift that had built up since the last run.
- guide/graph-format.html: removed the `draw` table row and the "LLM/Draw"
  port note. Added a "Retired type" bullet under Loader rules instead. That
  page documents the file format, so an author who holds a graph from before
  2026-07-22 needs to know what the `draw` node in the JSON is and how to
  repair it. Deletion alone would leave that author with no answer.
- guide/run-headless.html: removed `draw` from the runnable node list.
- growth/i18n-drafts/zenn-comfyui-ja.md: the unposted Japanese draft
  advertised Draw, and described it wrongly as a hand-drawing input. Replaced
  it with Join.

Historical records stay untouched: changelog.html, updates.json, feed.xml,
docs/ and proof/ still record that the node existed and then retired.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
nanoodle 571cedf Commit Preview URL

Branch Preview URL
Jul 30 2026, 12:15 AM

…ness

Two agent-facing guide pages said the opposite of what the executors do, and
llms-full.txt in the same commit said the truth.

  guide/run-headless.html: "Browser-only media-processing nodes (resize,
  vframes, combine, soundtrack, trim, extractaudio) are not supported
  headlessly: workflows containing them load with a warning and fail fast at
  run(), before any network call."

  guide/graph-format.html: "the headless executors reject them up front, before
  any paid call."

Both are false. The truth, read off the executors:

- nanoodle-js src/local-media.mjs implements all 6 ops headlessly: a pure-JS
  path first (PNG resize through its own PNG codec, PCM-WAV trim, MP4CAT
  lossless mp4 concat), then ffmpeg/ffprobe on PATH for everything else.
- nanoodle-py src/nanoodle/local_media.py has no pure path. All 6 shell out to
  ffmpeg.
- Neither library marks any node type unsupported. nanoodle-py's
  graph.UNSUPPORTED_TYPES evaluates to the empty tuple; nanoodle-js lists all 6
  as local:true in graph.mjs. UnsupportedNodeError is raised only for an
  UNKNOWN node type (workflow.mjs:183-188, workflow.py:208-215) — which is what
  a legacy `draw` node is, and the only case the up-front refusal covers.

A false "not supported" costs an agent a capability it has, so run-headless.html
now carries a per-node, per-language table (which of the 6 are pure, which need
ffmpeg), and says when the missing-ffmpeg error actually arrives: at that node's
turn, after any upstream paid node has already spent. graph-format.html's "runs"
column reads "local media" instead of "browser-only" and points at that table.

Freshness guard. Nothing stopped llms-full.txt from going stale again: no
check-*.mjs mentioned it and gen-llms-full.mjs was not in the hook. Worse, its
clone() returned the cached .tmp-libs/<repo> whenever README.md existed and
never re-fetched, so a regeneration could silently reproduce a months-old
library README. Today's file was correct by luck.

- gen-llms-full.mjs now hard-resets the cached clone to the remote default
  branch on every run, and fails loudly (naming the fix) when it cannot.
  --offline reuses the cache and says so; --check regenerates and diffs instead
  of writing; NANOODLE_JS / NANOODLE_PY read a local checkout instead of
  cloning. Its parts are exported so a guard can rebuild them.
- scripts/check-llms-full.mjs is offline and deterministic. It regenerates
  every section sourced from this repo (llms.txt, README.md, index.html) and
  diffs byte for byte, then asserts every node type named in the two libraries'
  supported-node tables is still registered in index.html — the exact check the
  three stale `draw` rows would have failed.
- .githooks/pre-commit runs it when llms-full.txt, llms.txt, README.md,
  index.html or either script is staged.
- .github/workflows/checks.yml checks out nanoodle-py as well and runs
  gen-llms-full.mjs --check against both fresh siblings, covering the library
  sections the offline guard cannot rebuild.

llms-full.txt itself is unchanged: it already matched. That is now proven, not
lucky — `node scripts/gen-llms-full.mjs --check` passes against freshly fetched
clones of both libraries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mikkel added a commit that referenced this pull request Jul 29, 2026
The other four places this branch touches — the Examples note, the guide's
Elsewhere list, the share-links guide and the README ecosystem row — all say
that every app on the showcase is still a nanoodle-team demo, and all but the
README also say the entry needs the full link. llms.txt said neither.

llms.txt is the agent-facing index, so an agent reading only that file would
describe the showcase as a collection of third-party apps, and would hand a
reader the shortened link the showcase cannot decode. Say both there too.

scripts/gen-llms-full.mjs does not read llms.txt, so llms-full.txt is
byte-identical after this commit and the merge with #372 is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mikkel and others added 2 commits July 29, 2026 18:09
…-refs

* origin/main:
  growth: refresh the 4 slipped launches, remove the stale r/mcp gate, add a launch checklist (#373)

# Conflicts:
#	.githooks/pre-commit
nanoodle-py 0.5.0 landed (#11, #12) after this branch generated the file, and it
rewrote two README sections the generator copies verbatim: "Quickstart (library)"
and "CLI". The committed file described 0.4.0, so the new CI step would have gone
red the moment this branch merged.

Regenerated with `node scripts/gen-llms-full.mjs`, which re-fetched both cached
clones first (nanoodle-py 91741de -> deea945, nanoodle-js a900ed1 -> bff73bb),
so this is the current content of both default branches, not the cache.

New content, all from the nanoodle-py README:
- the breaking input-key rename in 0.5.0, with the per-workflow key table
- optional input nodes
- run-wide `timeout=` releasing its workers, and media staying fetchable after it
- prompt length caps (UTF-16 code units, learned caps, no rewriting)
- `--json` printing the same envelope on a failed run, and on a pre-run failure

No hand edits. `node scripts/gen-llms-full.mjs --check` passes, both against
fresh clones and through the NANOODLE_JS / NANOODLE_PY paths CI uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mikkel
mikkel merged commit 6d7c065 into main Jul 30, 2026
3 checks passed
@mikkel
mikkel deleted the fix/drop-retired-draw-refs branch July 30, 2026 00:17
mikkel added a commit that referenced this pull request Jul 30, 2026
Conflict: llms-full.txt line 144, the built-with-nanoodle ecosystem row.
Kept this branch's wording, because that row is copied verbatim from
README.md and this branch is the one that changes README.md line 140.
#372 leaves README.md alone, so its row was the pre-change text.

Re-ran scripts/gen-llms-full.mjs on the merged tree to prove it: the
generator reproduces the kept row, and it also pulls in the nanoodle-py
0.5.0 README sections that landed after this branch was measured. The
merged file now differs from origin/main on that one line only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mikkel added a commit that referenced this pull request Jul 30, 2026
* Link the built-with-nanoodle showcase from the product

The showcase had no inbound link from nanoodle.com. Its only mention in this
repo was one row of the README ecosystem table, which no visitor reads. The
best proof that the app builder makes shippable things sold nothing to anybody.

Two in-product links, at the 2 moments where they do work.

1. play.html share popover, last line, after the link and the share targets.
   That is the one moment the reader already holds the #a= link a showcase
   entry needs. The line is owner-only: body.recipient hides it, because a
   recipient did not build the app they are re-sharing. The popover is the
   same one the editor's Create app modal embeds, so the create-app moment is
   covered by the same element.
2. index.html Examples panel note, beside the existing awesome-noodles link.
   The gallery answers "what can I start from". The showcase answers "does
   this make anything real".

Both new strings go through i18n like their neighbours: 2 PLAY_I18N_B keys and
2 index.html keys, each in all 5 languages. Verified in a real browser in en,
de and ja.

Also indexed for readers who are not in the app:
- llms.txt gains an Examples section naming both the gallery and the showcase.
- guide/index.html "Elsewhere" and guide/share-links.html get the same 2 links.
- llms-full.txt is regenerated. It is generated, never hand-edited, and it had
  drifted: it was missing the whole Ecosystem table (where the showcase row
  lives), and it still listed the retired Draw node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Say what the showcase actually is, and fix the German word for it

Two copy defects in the previous commit.

- The in-product copy sold the showcase as work other people shipped
  ("finished apps people shipped", "Want to see finished apps built from
  graphs like these?"). Every entry there is a team demo, and the showcase
  README says so. A reader who clicked through from the editor landed on our
  own seed apps. The Examples note, the guide's ecosystem list and the
  share-links guide now say the entries are still all ours, and turn that
  into the invitation it is: yours would be the first from outside.
- German translated "showcase" as "Galerie" (gallery), the word the same
  sentence already uses for awesome-noodles — one word for two repos. Both
  German strings now say "Schaufenster", matching es "vitrina", fr/pt
  "vitrine" and ja "ショーケース".

The 2 new Examples-note keys replace the 2 old ones in all 5 language maps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Say what the showcase is in the README ecosystem table too

The table row still read "Showcase of apps people shipped". No third-party app
has landed there yet, and the showcase README says so itself. This is the same
overclaim this branch already removed from the in-product copy, the guide and
llms.txt, so the last place it survived now matches: the apps are all still
ours, and the row invites the reader to add the first one that is not.

llms-full.txt is regenerated from the README, so it carries the same one-line
change and nothing else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Say which link form the showcase takes, next to the shorten button

The share popover reads top to bottom: copy the link, "Make it shorter —
optional", share to a social site, then this branch's showcase invite. A
contributor who works down that list clicks nanolink first, and setShareUrl()
replaces the link box with the short link. The showcase decodes the app out of
the #a= fragment, so a shortened entry is rejected — the popover walked the
reader straight into the one link form the showcase cannot take.

Name the form in the invite itself, before the copy happens: "it needs the
full link, not a shortened one". Own text node, own PLAY_I18N_B key, all 5
languages, exactly like its neighbours. guide/share-links.html says the same
thing in its showcase paragraph, which also sits right under a shortening
section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Say in llms.txt too that every showcase entry is still ours

The other four places this branch touches — the Examples note, the guide's
Elsewhere list, the share-links guide and the README ecosystem row — all say
that every app on the showcase is still a nanoodle-team demo, and all but the
README also say the entry needs the full link. llms.txt said neither.

llms.txt is the agent-facing index, so an agent reading only that file would
describe the showcase as a collection of third-party apps, and would hand a
reader the shortened link the showcase cannot decode. Say both there too.

scripts/gen-llms-full.mjs does not read llms.txt, so llms-full.txt is
byte-identical after this commit and the merge with #372 is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
mikkel added a commit that referenced this pull request Jul 30, 2026
* origin/main:
  Link the built-with-nanoodle showcase from the product (#374)
  Drop the retired draw node from the agent-facing docs (#372)
  growth: refresh the 4 slipped launches, remove the stale r/mcp gate, add a launch checklist (#373)
mikkel added a commit that referenced this pull request Jul 30, 2026
…e moved surfaces

#374 and #376 both changed index.html and play.html, so the two artifacts this guard pins had to be
re-derived rather than merged. Nothing here loosens a rule.

Baseline. The guard reported exactly one change and it is #376's: `async genVideo(model, prompt,
opts, imageDataUrl, onProgress){` gained a second copy in index.html at 9046, the DEMO_CTX override
that lets a signed-out sample reach a video result. The distinct count held at 893, nothing entered
the shared set and nothing left it, so the only ratchet movement is index.html occurrences 957 -> 958
(and multi-occurrence lines 74 -> 75). The refresh also recorded 2 new look-alike hashes, both from
the same commit: index.html:9022 `if(!r.ok) throw new Error(String(r.status));` and index.html:9025's
FileReader line. 796 -> 798 hashes over 219 lines. Every delta is attributable to #376; none is a
one-sided edit.

Sandbox matrix. Its ranges are absolute and #376 pushed index.html down by up to 111 lines and #374
pushed play.html down by 19, so all 25 ranges moved. Each was re-derived and checked byte for byte
against its pre-merge content. Left stale, 9 of the 16 cases return the wrong verdict — measured, not
assumed. All 16 now pass, row 8 still reporting the same 5 one-sided deletions and 1 occurrence drift
at their new positions. docs/twin-drift.md and scripts/twin-drift-worklist.mjs carry the same ranges,
so all 216 file:line references in them moved too; the work-list table is unchanged (893, and rows 1
to 6 still free 255 lines).

Hook. #372 landed touches_llmsfull and #373 landed touches_launchfacts on the same early-exit line
this PR adds touches_twindrift and touches_twindriftcases to. All 4 are unioned in, and each was
proved to fire by staging one file for it and watching which check ran; an unrelated file still
early-exits. The 2 new checks are renumbered 43 and 44, main's launch-facts and llms-full keeping 41
and 42.

Re-measured, and figures corrected where the merge moved them: timings (30 samples per case, load
average 3.19-5.68), the baseline byte sizes, the raw unblanked occurrence count, and the guard-suite
total, now 55 of 55 passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant