Bring main up to date with the NodeGX fork (alpha launch work) - #19
Merged
Conversation
…an see TALK-002 had: no editor switch. The premise held under re-verification — ten @codemirror/* packages, one EditorView, a stock extension list — so FH-017 is the whole answer, and it is mostly deletion. Slice 1. `Noodl.` completes while typing. The guard `word.from === word.to && !context.explicit` is true of every position after a dot, so the `Noodl.` branch below it was unreachable except by Ctrl-Space. CED-001's QA only ever typed `Noodl.V`, where the word is one character long — which is why nobody saw it. The distinction the guard wanted was member position vs top level, now in utils/completionPosition.ts and shared with the library twin. Slices 2+3. One error system. jsValidator.ts ran `new Function()` synchronously on every keystroke and could say "✗ Error" over text with no squiggle in it; it is deleted, along with the panel that could only ever render one problem. Diagnostics come from ESLint (Richard's call: take the dependency rather than hand-roll parse-error anchoring), the toolbar reads the same lint state through `forEachDiagnostic`, and the verdict is now a button onto CodeMirror's own diagnostics panel — whose keybinding has been in our keymap since CED-001 with zero callers. Measured before building on it: an unterminated block is reported at end-of-input by acorn exactly as by Lezer — no parser points at the opening brace, and syntax errors are fatal-and-first. The value is semantic: `no-undef` catches `totl` at its column, `no-dupe-keys` reports twice in one pass. `no-undef` is off in expression mode, where an undeclared identifier *becomes an input port*, and a warning elsewhere until FH-019 threads project globals in. Slice 4. The gutter dot opens the panel. `EditorView.domEventHandlers` registers on the content element, and gutters are siblings of it — the obvious implementation typechecked, read correctly and did nothing. Only driving it caught that. Slice 5. `dark: true` was hardcoded, so CodeMirror's base dark selection (#233) painted behind light-mode syntax colours. The flag now follows the document root and reconfigures through a compartment on a live editor. The corrected flag was not enough: CM's 4-class focused-selection rule out-specifies a 2-class one, and measurement showed our token had never reached the selection in dark mode either. Light 5.48:1, dark 4.95:1. POL-017 folded in: gutter numbers 4.67:1 / 4.82:1. Driven live in both themes through the real popout. Editor suite 2202 specs, 0 failures; core-ui jest 148; root typecheck clean. package-lock.json is deliberately not in this commit — it carries another session's pruning, and CI runs `npm install`, not `npm ci`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ace has no owners Alpha, not fast-follow. Record leaves the Provenance panel for a canvas HUD; badges scoped to the open canvas with an honest app-wide counter; Q4 goes the expensive way (per-peer trace ownership). Four tasks, all gated on FH-011. Three of this doc's own premises were wrong or missing: - the execution-overlay slot is already owned by ExecutionOverlay, so the HUD needs its own layer; - trace events are `from`/`to`, not `fromNode`/`toNode`; - Q4 has no signal at all — the relay forwards verbatim and NEITHER editor peer registers a clientId, so there is no identity to own a switch with. And the Q4 research turned up live data loss: setTraceEnabled(true) replaces the buffer unconditionally, so an agent's start_trace destroys a recording a human is in the middle of. FH-011 slice 2 amended: the poll moves into TraceSession. A sidebar panel is not constructed until it is first opened, so with Record on the canvas a whole session can be recorded with the panel never existing — and a poll living there would never run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…remembered
Two probes through the real BackendService, then deleted:
- A JavaScript Function node inside a cloud function reaches process (79
env keys, cwd, exit), Buffer, crypto.subtle, randomUUID, global fetch,
FormData, Blob, URL, structuredClone, setInterval. It does NOT reach
require. The script is compiled with `new AsyncFunction`, whose body runs
in global scope, and the CloudRunner runs in the backend's own process —
sandbox.isolate.js, which blocks require and bridges fetch, is Parse-era
dead code referenced by nothing.
- A REST2 node in a cloud function made a real GET against a live upstream.
So hashing, JWT, base64, date maths, CSV parsing and third-party API calls are
all writable in a function today: the wish list is a node-and-door question,
never a runtime one. §6 is annotated for all 14 prompts rather than left blank.
CWF-003's first check item ("establish which fetch the node gets") is answered
and struck. One trap recorded: a REST node's outputs are minted by its Response
script at run time, so a pm- wire drawn at export time is dropped — the probe
returned 200 with an empty body while the upstream call had succeeded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…expires Settled: a "Connect an AI agent" section in Editor Settings beside the AI one, two captioned Copy buttons (the phase-36 merge question stays closed), and Q3 resolves to (a) — the editor creates the project, the agent builds in it. Verifying the doc found three things its own research did not. nodegx-observe never reconnects (relayClient.ts:129-171 puts every handler behind `if (settled) return`) and the relay token is minted per editor launch, so a copied command dies at the next restart and every tool then throws "Not connected to the NodeGX relay." forever. The proposed commands hard-code the server name, so a second project overwrites the first. And @noodl/observe is in no gate — test:packages scopes @noodl/mcp and not it. Q3's premise was also inverted: TAB-006 records that tabs affect nodegx-observe (bound to a port) and explicitly not noodl-mcp (bound to the path in argv, "none — it works unchanged"), because ProjectStore is constructed once at spawn and nothing rebinds it. So list_projects is observe-side discovery, and the path in the command already answers "which project". Registrations are now named after the project — slugging the directory basename, because ProjectModel.name is optional and falls back to 'Untitled'. Four tasks, ordered so the door works before it opens: MCP-003 (reconnect, gates the observe button) → MCP-002 (ship both .cjs via extraResources, on the exact nodegx-backend precedent) → MCP-001 (the section) → MCP-004 (the page, which belongs in the docs repo — docs/ here is reference material, not the user-facing tree). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…WebSocket to bind to
Q1 yes: build the standalone node on the BCN-008 contract; Query Records'
checkbox stays as the query-refreshing form. FH-021 written, unblocked.
Four corrections the research forced on the doc's own proposals:
- It asked for `realtimeSupportFor`-driven port gating. That is the exact
opposite of what shipped — the checkbox's port is deliberately never gated,
because a capability that vanishes when the picker moves is worse than one
that says why it cannot connect. The new node copies the shipped pattern.
- The filter reaches a server in one dialect of five (`NODEGX_SSE` alone sets
`subscription.filter`). Shipped anyway on Richard's call, disclosed — never
as a client-side fallback, which would be a fourth filter twin.
- The fold comment argues *against* a standalone node ("a subscription without
a query is a stream of ids nobody can render"). Weaker since the fold added
the record outputs, still true for a Directus delete. Missed counterweight:
`ssr: { compat: 'client-only' }` still works, so the standalone node is
strictly simpler on SSR than the checkbox was.
- Q2 was understated. Our backend has no WebSocket server at all — no upgrade
handling, no `ws` dep, SSE `/realtime` only per BAK-001. A "connect to my
backend" mode was unbuildable for the one backend Richard wanted it for.
The `_active_` picker default is what makes ours the easy path instead.
Also closes CWF-007 Q3: two deliberate nodes sharing `RealtimeSubscription`
but not a node definition — different payloads, different auth postures.
Recorded as a decision, not a deferral, so the pair is not later collapsed.
Numbered FH-021: a parallel session claimed FH-020 for the Ports tab.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ir own Every count re-derived from the generated catalog rather than the prose that produced it: 82 adopters, 34 with Unchanged, 48 without, 8 with neither, 4 with Treat Unchanged as. All held. Three of the doc's own claims did not. - Pruning Done from the 8 would have restored nothing: the proxy ERG-001 killed reads COMPLETION_SIGNAL_NAMES, which contains 'completed', the one port with no exemption. - "Build the canvas port hover" is not a rendering gap. There is no per-port hit-testing on the canvas at all; the only hover is node-level. Meanwhile the connection popup already has one, and ConnectionBar._getPorts hand-copies eight fields and drops `description` one function before the component that could render it. - Q3 was two questions. displayName: 'Done' is set in exactly one place, so a display-only rename was one line; the expensive one is the internal name, which lives in every saved project's connections. Decisions: keep both ports on all 82 and have outcomeOutputs say when Completed is redundant (FH-022); no rename; and the surface is a read-only Ports tab in the property panel (FH-020), because the connection popup filters by what you dragged from — right for wiring, useless for discovery. Both mechanisms the tab needs already exist and neither is where it needs to be: the tab strip is in this panel on the AI path only, and the connected-source chip is on 5 row classes of ~29 and reads inputs only. Counter.Reset rides along in FH-022 slice 3, with the warning the original note lacked: the obvious repair is also wrong, because the guard compares against 0 and Reset sets the count to startValue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…07 Pile B)
Richard's call, 2026-08-05: the nine browser-state nodes AIX-005 leaked into the
cloud vocabulary go. Global Store x3, Optimistic Update, State History,
Undo/Redo, State Snapshot, Action Dispatcher, Action Handler — all model client
session state, and a cloud function is one request that answers once and is torn
down. The Streaming six stay: calling an upstream streaming API server-side is
CWF-007's whole case.
The mechanism is the point. `registerNodes` reached EVERY runtime and the viewer
could only add to it, so a browser node put in the shared list silently became
part of the cloud function vocabulary with nobody choosing it. There is now a
`type !== 'cloud'` block at the foot of that function — the subtraction that did
not exist — and adding to it is the documented way to say "browser only".
Component Children is the tenth and works differently: it is seeded into the
export by hand, because it has no node definition at all — it is a marker
NodeScope interprets structurally. `generateNodeLibrary` takes an optional
`{ runtimeType }`, passed from `getNodeLibrary` (live clients) and from the
snapshot generator (the committed file), so both describe the same vocabulary.
Nothing behavioural changes: a graph carrying the marker still works.
Measured, not assumed: 58 -> 48 types, removals exactly the ten, every surviving
type byte-identical, typecasts/dynamicports/colors/nodeIndex untouched. Nothing
in nodegx-backend or noodl-viewer-cloud referenced any of the nine.
The node catalog carries the change as `availableIn: ["browser"]` for the nine.
Regenerating it also picks up two `prop-*` dynamic-port entries on Create/Update
Record that were already stale in HEAD, from the phase-40 opts-guard fix.
Gates: cloud-library:check, catalog:check, catalog:merge:check,
catalog:encoding:selftest, typecheck:{runtime,cloud,viewer,editor}, and the
runtime (2144), viewer (853), backend (743), mcp (161), backend-contract (199)
suites. The two email-flows Send Email timeouts are PRE-EXISTING — verified by
running that suite with these files reverted to HEAD.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… validated
Richard walked the node list. The validation found more than the list did: five
of the six Tier-1 additions he asked for were ALREADY registered — the object
trio, Expression/Function/Logic Builder, all 11 data nodes, and Run Tasks. The
cloud picker's contents are not legible from the picker, which is the same
finding as TALK-007 §2 in a different costume.
Two were shaped differently than assumed, and both are recorded rather than
quietly implemented:
- Run Tasks does NOT run another cloud function. `taskTemplate` is
type: 'component' and the picker deliberately excludes cloud functions —
"don't allow references to cloud functions, only cloud components". It runs
a cloud helper component per item. Left open for Richard; my read is that
"run this function per item" is a workflow For Each.
- The Cloud Function node is not a free move: cloudfunction2.ts uses
XMLHttpRequest and reads the browser session store for its token. Needs the
typeof-window branch restnode already carries, plus an answer to what
identity a server-to-server call carries — the same hole as the workflow
join, and it should get one answer, not two.
Value Changed struck (a one-pass runtime has no "last time"). Component Object
added: per-instance state in a runtime where the instance IS the request.
Ten tasks: CWF-008 the vocabulary moves, 009 Secret, 010 crypto/JWT, 011 date,
012 CSV, 013 Log, 014 typed Request bodies, 015 server-side users, 016
idempotency, 017 access rule + per-function rate limit. Four rows deliberately
have no task, with reasons recorded: scheduling, queue, PDF, payments.
Every file:line link in the ten resolves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… action colour FH-009. `ListItem`'s active variant painted the row with `--theme-color-secondary` — the neutral ACTION colour, which is inverted relative to the surface by construction (#18212b near-black on light, #eef2f6 near-white on dark). The icon got a half-correction (`--theme-color-on-secondary`) but the label kept `TextType.Default`, so the selected row was dark-on-dark in light mode and light-on-light in dark mode. Measured, that label was 2.17:1 (light) and 1.95:1 (dark) — below even the 3:1 non-text floor. The active row now takes a real surface pair: `--theme-color-primary-bg` plus `TextType.Proud` (`--theme-color-fg-highlight`). Measured over the plausible parent surfaces bg-0…bg-3: light: label 12.69–14.43:1, secondary fg-default text 5.85–6.65:1 dark: label 10.49–14.42:1, secondary fg-default text 5.37–7.38:1 The tint alone is only a ~1.12:1 step off the panel, so an inset 2px left rule in `--theme-color-primary` carries the "selected" signal (3.57–4.06:1 light, 4.49–6.18:1 dark against the tinted row — clears the 3:1 non-text threshold). The prefix icon takes the same primary. Hover on an active row goes to `--theme-color-primary-25` (label still 11.0–12.4:1 light, 8.4–11.4:1 dark). The `!important`s stay: `.is-active` (bg-1) and `.is-variant-active` have equal specificity and the variant wins on source order either way, but the flags are also beating panel-level rules outside this file, so dropping them would be an unmeasured change. All three call sites — DocsPanel.tsx:277, DiffList.tsx:141 and :165 — are fixed by this one component; neither panel's own source is touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Richard's scenario, driven end to end: a cloud function takes {users: [...]},
Run Tasks iterates it, a cloud HELPER COMPONENT runs per item with that item's
own data, and the function answers when the run is Done. Concurrency honoured,
empty list handled. Kept as a spec because "a registered node is not a working
node" is exactly the claim TALK-007 exists to stop people asserting — and
because it guards CWF-008's vocabulary moves.
So there is nothing to build for the loop. What is missing is the NAME (nobody
searching for a loop searches for "Run Tasks") and any sign that the per-item
unit is a helper component, not a cloud function — the picker's refusal of
functions is correct and silent. CWF-008 slice 4b.
Two things cost a wrong conclusion while writing the fixture, both recorded in
the spec's header:
- A component's ports come from the COMPONENT-level `ports` array
(componentmodel.ts:456-463), not from the Component Inputs/Outputs nodes'
own `ports`. With them on the nodes, Run Tasks correctly reported
run-tasks/no-completion-output.
- That failure then HUNG THE REQUEST instead of answering. CloudRunner.run
settles only when a Response node fires, and POST /functions/:name awaits it
with no timeout — while workflows have had per-step and per-run timeouts all
along. Wiring only the happy path off an outcome node is enough. It leaks the
component instance and its model scope too, because teardown lives in the
same callback as resolve. Filed as CWF-018, not fixed.
Also settled: the Script node stays out of the cloud for now (Richard).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… one-unit ports stop pretending to be a menu
FH-014. `PropertyPanelSelectInput` filtered the current value out of its own
option list (`if (option.value === value) return null`). For a multi-option
select that just meant the value you were looking at was missing from the
list. For the ~27 unit-bearing ports that declare exactly one unit (21x
['px'], 4x ['%'], 1x ['deg'] — Font Size, the four Paddings, Border Width,
the four Shadow numbers, Spacing, Size, Rotation) it meant the list was
empty: `NumberUnitInput` builds a one-option list, that option equals the
current unit, the filter deleted it, and what opened was the Options card's
border plus 2px of padding at the trigger's 40px width — a ~40x6px empty
box. Multi-unit ports (Width/Height, Margins) worked, which is why it looked
random.
Two changes:
(a) The selected option renders, marked with the same selection pair the
active list row uses — `--theme-color-primary-bg` + `fg-highlight`,
measured 14.43:1 on light bg-1 and 13.24:1 on dark bg-1 (the Options card
is bg-1). It also carries `aria-selected`. Clicking it closes the popup
without firing `onChange`, so re-picking the current value no longer
rewrites the parameter or pushes an undo entry. Every other select in the
panel — enums, the theme picker, the runtime picker — gains its current
value back.
(b) `NumberUnitInput` renders a static unit label instead of a select when
the port declares fewer than two units. A menu with one immutable choice
is noise. The label uses `fg-default` (6.5-8.3:1 on bg-1..bg-3, both
themes) rather than `fg-muted`, which only measures 2.7-3.6:1. When a
single-unit port has no unit string at all, nothing renders and the
number input takes the full width.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…on, not [object Object]
Setting fx on a label port stores { mode: 'expression', expression, fallback,
version } as the parameter value. BasicNodeType.labelForNode handed that object
straight back — typed TSFixme, so nothing caught it — and the canvas painter
stringified it, so every node whose label port carried an expression was called
"[object Object]" on the card. The property panel looked fine only because
NodeLabel already ran the label through ParameterValueResolver.
The fix is at the one place the label is produced: labelForNode resolves the
parameter to the expression TEXT (Noodl.Variables.foo — the name the author
typed, and now what the panel header shows too, since it reads the same label),
falls back to the display name when the resolved string is empty (an expression
object is truthy even when its expression is not) or when the value is some
other object, and truncates through the existing 'length' branch. That also
disarms the 'filename' branch, whose labelName.split('/') would have thrown on
the first node type to declare it.
Three consequences of the object reaching the view are fixed with it, via a new
NodeGraphEditorNode.labelText() that every title consumer now goes through: the
wrap-height cache was keyed on the label, so every expression-labelled node
shared the key "[object Object]" and its cached height; the label !==
typeDisplayName() tests in NodeGraphEditorNode and the painter can never be
equal for an object, so such nodes always painted a sub-label as if the author
had renamed them; and the comment popup titled itself with the same object.
NodeGraphNode.getParameterDisplayValue, which had zero call sites, is renamed to
getParameterAsString: it resolves to the fallback, the opposite of the
identically-named function in ExpressionParameter that the label path wants.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Block Pointer Events" (blockTouch) works by having pointerProps wrap every
pointer handler in stopPropagation. Button, Slider and the deprecated Button
spread that result and then wrote onClick again from the raw sender — as a
trailing `onClick={props.onClick}` in Button, and as an `inputProps` object
spread after `controlEvents` in Slider. JSX later-wins, so the wrapper was
discarded for the one event authors turn the option on for: mousedown, mouseup
and touchstart were blocked and the click sailed through to the parent Group.
That is the reported "the option is shit and doesn't work".
controlEvents already supplies onClick — pointerProps picks `props.onClick` up
off the props root — so the extra assignment was pure loss. Removing it also
restores the `updateDirtyNodes()` flush that the same wrapper carries, which a
Button click had never been getting.
Regression test drives pointerProps directly and calls Button as the plain
function it is (there is no jsdom jest environment in this repo); it fails on
both counts with the line put back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…placed, and a popout that would hang off an edge now flips
FH-005. Clicking a JSON or code editor button low in the property panel opened
the editor with its top edge at the button's Y, mostly below the fold.
Two stacked causes:
(a) The measurement race. PopupLayer.showPopout measures the content the
instant it is appended, but CodeEditorType and ListValueEditor render it
with a React 18 root and no flushSync, so the box was still 0x0. The
formula then reduces to y = anchor.top, and every viewport clamp is a no-op
against height 0. Both also pass disableDynamicPositioning: true, so the
ResizeObserver never revisited the top edge once the content arrived; the
box simply grew downward from a Y nobody re-evaluated. Five other popouts
already wrap their render in flushSync for exactly this reason (DEBT-010) —
these two were missed. Both keep disableDynamicPositioning: their size is
an inline width/height on the editor root, so one flushed commit is the
whole box, and re-positioning on every ResizeObserver tick would fight the
user's own resize handle.
(b) _positionPopout had no flip logic at all — only clamps. It now computes the
origin for the side it was asked for, and if that side hangs off the edge it
tries the opposite side, taking it only when it overflows strictly less. A
popout larger than the window therefore does not flip (both sides are just as
cut off) and the clamp still has the last word. The arrow follows the side
actually used, and is recoloured onto the correct border when it flips —
Popout now carries effectivePosition and arrowColor for that.
disableCentering is marked deprecated: it never centred anything, its only
effect was to skip clearing the arrow's direction class, which the flip needs.
No caller has ever set it.
Verified: npm run typecheck:editor is clean. Placement itself needs eyes on the
running editor — jasmine cannot measure layout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FH-013. Widening the properties panel did nothing for a cut-off label because the label column was a hard 62px in BOTH label implementations. A. The React row (PropertyPanelInput.module.scss). `.Label` carried `text-overflow: ellipsis` + `overflow: hidden` + `line-clamp: 2` + `max-height: 28px` — the unprefixed `line-clamp` does apply on our Electron, so line two was clipped by the max-height and line one ellipsised. All four are gone; the label is `white-space: normal` with `overflow-wrap: anywhere` so long unbroken identifiers break instead of setting a min-content width that shoves the input out of the row. The `.sidebar-property-editor` override no longer pins 62px: the column is `width: 37%` with a 62px floor and a 160px ceiling, so it still starts narrow, grows with the panel, and never takes the majority of the row. Every row shares the one column, so inputs stay aligned. The toggle-row `width: auto` special case is preserved (and now clears the floor/ceiling too). Cloud-workflow step rows use this same component, so item 11's cut-off step labels are covered. B. The legacy row (propertyeditor.css). `.property-label` was `white-space: nowrap` + ellipsis inside an absolutely-positioned 62px box, with `.property-value` pinned at `left: 72px` — this is where "Custom CSS" became "Custom C...". The label is now flow text that wraps, and the rows that pair a label with a value opt into a new `.property-row` flex row (the code-editor property row and the curve editor's Duration/Delay). `.property-value` keeps its absolute geometry in the base rule because MarginPaddingInput's edit box positions it itself with inline left/top/bottom/right — only the `.property-row` scope resets that to flow, and it keeps `position: relative` plus a definite 28px height so the `position: absolute; width/height: 100%` inputs inside still resolve. `.property-label-col` exists because <Tooltip> inserts two divs between the row and the label, so the flex item cannot always be the label itself. The other two `.property-label` consumers (ResizingInput's compact label and NumberUnitInput's "Fixed") already inline-override position and width, so they do not double-shift; they now inherit a 1.35 line-height instead of the old 35px one and centre in their flex rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ut it in your font picker
Reported as "Roboto Medium is still in there, all by itself, can we remove it?"
Measured against the Puppy test project: its lone text style "Label Small"
points at fonts/Roboto/Roboto-Medium.ttf and its fonts/ holds exactly that one
file, both copied in by an imported Date Picker prefab. Nothing in the editor
seeds Roboto — the seeded prefab/module library did.
The mechanism: the font picker lists every ttf/otf/woff/woff2 file it finds
anywhere under the project directory, grouped by folder (fontItems.ts ->
loadFontItems). Ship one Roboto file in a prefab and importing it creates a
fonts/Roboto group with a single entry, alone. That is the whole report.
Written first, per the phase-39 rule, and red before the fix: library:check
now fails any entry that references a font file it does not ship, or that
ships or names a retired family (the design system is Inter; Roboto is
retired). It was red at 19/58 entries. It had to be widened twice, and each
widening found content the previous pass called clean:
- simple-tooltips names the family in a JS assignment inside a minified
bundle, not in CSS. The first regex for that missed it anyway, because
the family list contains the other quote character ('Roboto, ...,
"Open Sans", sans-serif') and one capture group plus a backreference
truncated at it. Each quote style now gets its own group.
- After the sweep the check went green while Roboto was still sitting in
four project.json files: font-family: Roboto inside a CSS string in a
Javascript node's code parameter — a JSON string *value*, invisible to a
walk that only looks at fontFamily *keys*. .json and .map are now scanned
as raw text too. That put 5 entries back to red.
Sweep: 19 entries, 46 Roboto TTFs out, 27 Inter TTFs in (only the weights each
entry actually references, so image-cropper drops from 12 to 3), Inter's
LICENSE alongside each as SIL OFL requires, 39 fontFamily path references and
8 bare CSS/JS family names rewritten. Inter came from
packages/noodl-core-ui/src/assets/fonts/Inter/, which was not modified.
Every changed entry's library.json version is bumped, and that bump is the
entire fix for the phase-21 stale-zip trap: the zip filename carries the
version, so date-picker-1.3.0.zip becomes date-picker-1.4.0.zip — a new URL.
A same-URL republish would have silently done nothing for existing users,
because the editor never re-downloads a non-empty cache folder.
library:check is 58/58 and exits 0; library:build and library:verify-dist are
green; a content-level scan of all 58 rebuilt zips finds zero Roboto bytes.
Not verified: anything requiring the editor — no live import was driven.
Filed, not fixed: a bare `font-family: Inter, ...` in an injected CSS string
still does not resolve to the bundled TTF, and never did with Roboto either.
fontloader.ts derives the family from the filename, so Inter-Medium.ttf
registers as "Inter-Medium", not "Inter". Those CSS blocks have always fallen
back to the system font. This change fixes which family is named, not whether
a bare name resolves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Click-through was not a bug in "Block Pointer Events", it was the only behaviour there was. Every visual node and control gets a live DOM onClick installed at init whether or not its Click port is wired, and nothing in the runtime called stopPropagation unless blockTouch was on — so a Favourite button inside a clickable card always ran the card's Click too. pointerProps now stops the click when the node's own Click output has connections, governed by a new Click Bubbling port: Automatic (the default), Always (the old behaviour, for a project that relied on bubbling) and Never. The predicate is read when the click happens, not when the element renders, because wiring a connection in the editor does not re-render the node — a render-time answer would be stale exactly while an author is testing it. Only the click is stopped, so the parent's Hover Start and Pointer Down keep firing; that is what blockTouch cannot do, since it blocks fifteen of the sixteen pointer events at once. A node with no Click port, or one with nothing wired to it, is untouched. Also fixes the blockTouch descriptions, which said "elements underneath this one" — z-order — while what it stops is the event reaching this element's ancestors. Catalogs regenerated; both catalog checks green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ts name
Reported in the first-hour pass and decided by Richard on 2026-08-02 (ERG-004
§7.4). `Object Changed` shipped in phase 35 with an `object`-typed input and no
producer anywhere in the node library: the whole Data category names an object by
string id, so the obvious wire — Object.Id into Object Changed.Object — went
through the declared string→object typecast, which `eval`s the id as a JavaScript
literal, throws a ReferenceError on the hyphen, catches it, substitutes `{}` and
leaves the watcher watching nothing for the life of the app. Until now the only
working path was a Script node returning `Noodl.Object.get(id)`.
Model2 gains an `object` output carrying the live Model. It is a getter reading
`_internal.model`, not a setter side effect, because a declared default never runs
its setter and anything produced only when an input arrives is absent on a graph
that never touches that input. `setModel` — the single place the bound object
changes — flags it beside `id`, and before `setModelID` announces Fetched, so the
value cannot arrive behind the signal that describes it.
It emits `null`, never `undefined`, when nothing is bound. `Node.sendValue`
returns early on undefined, so an unbound Object emitting undefined would send
nothing at all and a downstream Object Changed would go on watching the object it
was given before, forever, with no way to be told the binding was cleared.
Slice 0 verified first, as the task required, and it is why Collection2 gained
nothing: `Array.Items` already serves `Array Changed`. `items` returns the live
Collection proxy, `Array.isArray` is true through it and `on`/`off` come from
`Array.prototype`, so `isWatchableArray` accepts it — measured end to end in row
FH004-5, adding and removing an item and asserting Index and Count.
The typecast is deliberately untouched: `string → object` still silently accepts
the wrong wiring, which is the blast radius Richard accepted and its own
conversation. Row FH004-3 records that behaviour rather than asserting it is right.
node-catalog.json needed no change here — a concurrent session regenerated it and
swept this port into f35601c before this commit was made. cloud-node-library.json
is regenerated here; catalog:check, cloud-library:check and catalog:merge:check are
all green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…to that workflow, and the node picker draws over the pin
FH-012. Two independent defects behind one report.
(1) The pin captured its identity from the OPEN CANVAS. POL-009 hid a pin on
any canvas but its own, and its slice-2 warning — put an identity on the
pin EVENT rather than reading one off the viewport — was not honoured. So
pinning workflow B's run while A was open tagged the pin as A's: it drew
over A, where no step resolves (the "Nothing to show on this graph"
notice), and vanished the moment you opened B. Pinned to the wrong canvas
and invisible where it belonged.
`execution:pinToCanvas` now carries `componentName`, and the Execution
History panel derives it from the RUN — `workflowComponentName(workflowId)`,
the same `/#__workflow__/<id>` fullName the guard compares — after taking
the canvas there with `WorkflowEditorService.open`. Navigation is awaited
before the event goes out, so no pin flashes on the canvas being left.
Only a run with `metadata.kind === 'workflow'` navigates. `WorkflowEngine`
is the sole writer of that key and the sole thing with a canvas to go to; a
cloud function CALL is logged by `WorkflowRunner.run` as
`workflowId = functionName` with no kind, and a rejected trigger has a
workflow id but never ran. Those, and a workflow whose backend will not
answer, emit no identity and fall back to the old behaviour — the pin lands
on the canvas in front of you, where the overlay's own notice explains
itself. The Workflows panel's run-and-pin is untouched for the same reason:
it is already standing on the right canvas.
(2) The overlay outranked every popup, not just the picker. Its header, notice
and timeline are `z-index: 200`, and nothing between them and <body> was a
stacking context — not the clipping wrapper (positioned, z-index auto), not
the shell root (unpositioned). Those 200s were therefore competing directly
with `.popup-layer` (z-index 10, appended to <body> by router.tsx) and
winning, so the pin bars drew over the node picker AND over its dimmed
backdrop.
The execution layer's clipping wrapper now carries `z-index: 5`. That makes
it a stacking context, so all three 200s — and the badges and the data
popup with them — resolve inside it and the layer competes as one number: 5
beats everything in the shell that paints at auto and would otherwise cover
the bars by document order (#nodegraph-dom-layer, .canvas-hud-root,
.nodegraph-component-trail-root under the timeline, #comment-layer-fg,
.help-center-layer), and loses to 10. `.popup-layer` is NOT bumped to 201:
that would fix the instance and leave the class. #canvas-tabs-root (100),
#editor-banner-root (1001) and HighlightOverlay's 999/1000/1001 still
escape to the body-level context and are filed, not fixed here; the tabs
root is empty unless a Logic Builder tab is open, and the highlight overlay
already outranked the pin bars.
Verified: `npm run typecheck:editor` and `npm run typecheck:editor-tests` are
both clean. Navigation and stacking are invisible to jasmine — both halves need
eyes on the running editor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… not only where it overlaps the wire CAN-001 built the whole label drag — bounds cache, `isPointInLabel`, clamped `labelT`, one undo entry, persisted — and then gated it on the wire-stroke hover, which is the exact ordering its own spec warned would "ship an infuriating regression". Two mechanisms, both in `NodeGraphEditorConnection.mouse`: - The `move` path decided the hover with `isPointInStroke` alone (±5 graph units of the curve). So the grabbable part of a chip was the *intersection* of chip and stroke — for a two- or three-line label on a sloped wire, the middle quarter, with both wide ends dead. Worse, stepping onto a wide end left the stroke band and *cleared* the highlight, so the chip vanished from under the cursor mid-reach. `isPointInLabel` is now asked first, and independently. - The `down` path was gated on `highlightedConnection`, and only the stroke hover ever sets that. Every chip lit by something else — an author's label, a `connectionLabel` port type, the always-on setting, or either endpoint node being hovered or selected — was visible and completely inert. The chip test is hoisted above that gate: a press on a painted chip belongs to its wire however the chip became visible. Double-click-to-edit comes along for the ride. Plus the affordance it never had. `InteractionController.setCursor` is now the single writer for the canvas cursor — `grab` over a chip, `grabbing` for the length of the drag, back to `grab` on release — and it has to live on the controller rather than the connection, because a node card consumes every `move` it contains, so a connection that set the cursor itself would have no event left to unset it with. And the debug inspector no longer opens on top of the target: it is a DOM element attached to a point on the curve under a `translate(-50%, calc(-100% - 4px))`, so its bottom edge lands 4px above that point — the middle of the chip — where it covers the chip's top half and eats the press. `InspectorActions.setHighlightedConnection` skips it while the pointer is on a chip, and drops an unpinned one already showing. `tests/canvas/WireLabels.test.ts` had no drag or hit-region test at all — nothing could have caught any of this. It gains 18: the geometry of a wide chip on a 45° wire (the corner is on the chip and 14 units off the stroke), the highlight not being cleared by stepping onto the chip, a grab from a selection-lit chip, no grab where no chip was painted, read-only refusal, the cursor states, and the drag itself — clamped, one `updateConnection`, rewound to the pre-drag value first, and nothing pushed when it ends where it started. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FH-008. The panel never stole focus — the property editor does, by design. Explain then paid for it twice. 1. `EditorEventBindings` deselects every node on `SidebarModelEvent.activeChanged` for any panel outside a two-entry allow-list, so *opening* Explain destroyed the selection it was opened to explain. The allow-list is now `panelHoldsCanvasSelection()` — a function, not an exported array, because `ExplainPanel_ID` comes back through a module cycle and a top-level array would capture it at init. `EditorDocument` held a verbatim twin of the same two ids for the detached viewer's inspect highlight; it calls the shared predicate now, so the highlight and the canvas cannot disagree. 2. The panel read the selection once, at mount. Sidebar panels are hidden behind `display: none` and never unmounted, and are not constructed until first opened — so that one read answered forever. `useCanvasSelection` now takes whether it is the active panel (`useIsActivePanel`, the hook that existed for exactly this and was unused) and re-reads on every transition into active. It also re-reads on mouse-up while visible, because a marquee changes the selection without touching the sidebar and the canvas emits nothing for it; a value compare keeps a gesture that changed nothing from re-rendering. 3. The remembered target could outlive the selection it shadowed: clicking empty canvas left the panel offering "Explain this node" for a node nobody was pointing at. `SelectionActions.deselect` drops it now, excluding read-only editors for the same reason it leaves the sidebar alone. `forgetTarget` went quiet when there is nothing to forget — it is now on every canvas mouse-up that lands on nothing. Multi-select needs no memo any more: with (1) a marquee selection is still live when the panel opens, so it reads three nodes as three nodes. `tests/nodegraph/explain-selection.spec.ts` (jasmine, 11 specs) pins the allow-list, the memo's remember/forget/navigate-away paths, and that deselect drops the memo on the app's canvas but not in a read-only editor. The re-read-on-visible is a React effect on a `display: none` panel — only the running editor can show that one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…as Done TALK-006 decision 1, had 2026-08-05. On eight of the 82 outcome-contract nodes there is no Unchanged port and no Failure port, so Done and Completed fire on the same path every time with nothing between them. Richard hit it on Condition — a Logic node, one of the first anyone wires — and read two identically-behaving ports as the vocabulary doubling up. It is not doubling up; it is the contract's universality landing on a node with one outcome, which is the price of Completed being the one port with no exemption. Both ports stay and neither is renamed. What was missing is that nothing told the author the two are the same on this node: `done` carries 77 distinct descriptions across its 82 nodes and `completed` carried exactly one. So on those eight an author read two different sentences describing one pulse. Completed now says it has no other outcome and always fires together with Done. The other 74 are byte-identical to before.⚠️ The rule is NOT derived from OutcomePortOptions, and FH-022 specified that it should be. Measured: the options object and the port set already disagree on four nodes. expression.ts, modelcrudbase.ts's addFailure mixin, componentutils/base.ts's canFailToResolve and variablenode2.ts each declare `failure` beside outcomeOutputs rather than through it, and three of those really do report a failure outcome — so an options-derived branch printed "it always fires together with Done" on Expression, Set Object Properties, Variable and Set Parent Component Object Properties, where it is false. That was caught by regenerating the catalog and seeing twelve nodes change instead of eight. So the branch reads the assembled outputs, in defineNode, which cannot disagree with the port set because it is the port set — and which reaches the one divergent caller that lives in a package outcomeOutputs' options cannot. The wording and the predicate stay in outcome.ts; defineNode calls one function; zero node definitions were edited, which is the whole reason outcome.ts exists. It is guarded on the exact generic sentence, so a node that hand-rolls a port called `completed` with a different meaning — GlobalStore.Set and ActionDispatcher both did before ERG-001 — is left alone, and it is idempotent because defineNode runs twice on a module object whenever two graphs register the same node. The corpus rows pin the rule against port sets, never against a list of eight type names: the eight are a consequence, and a node gaining a Failure port later must lose the sentence with nothing to edit. FH022-3 derives the population from the generated catalog, so it doubles as a staleness check on it. Snapshots regenerated, and all three gates run explicitly rather than inferred: catalog:check green, cloud-library:check green (3 of the 8 are in the cloud library — NewModel, Condition, Unique Id, exactly as predicted), catalog:merge:check green after catalog:merge, which a changed description makes a changed merge input. Runtime suite 2158 passed, typecheck:runtime clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ved the count FH-022 slice 3, shipped alone because it is a behaviour change and not a rename. The guard in Reset has never once fired. The count lives at `_internal.currentValue`; the guard read `this.currentValue`, which is `undefined` on every pass, so the early return was dead code from the day it was written (PLAT-003 NOTES §25). ERG-001 §4 kept it verbatim on purpose and said why in the code: repairing it changes when Count Changed fires, and that had no business riding along inside a contract adoption.⚠️ The obvious repair is also wrong, and R-3 is the row that holds the line. Pointing the read at `_internal` while leaving the comparison against `0` would report Unchanged for "the count is zero" — a different condition, and wrong on every counter that starts anywhere else. Reset sets the count to `_internal.startValue`, so the post-condition that already holds is `currentValue === startValue`, and that is what is now tested. A counter walked down to zero with Start Value 5 still reports Done and still moves. What changes for a running project: Count Changed stops firing on a Reset that changes nothing, and Reset starts emitting an outcome the node has never emitted. Both are what the contract says should happen, and both revert with this one commit if a project depended on the old pulse.⚠️ One premise in the task doc is wrong and is recorded in the suite rather than quietly worked around. FH-022 says "Counter already has an unchanged port … so no port set changes and Treat Unchanged as already applies". The first half is true; the second is not, and it is not a Counter quirk. counter.ts spreads outcomeOutputs and never outcomeInputs, so the setting has no port here at all — and measured from the generated catalog, 30 of the 34 nodes with an Unchanged output carry no treatUnchangedAs input. ERG-001 §3's escape hatch reaches four nodes. Recorded, not fixed: closing it would change the port set on the one node this behaviour-change commit touches, which is exactly what shipping the slice alone is meant to avoid. R-4 goes red the day somebody closes the gap. Unchanged's own description now covers both ways to reach it. Catalog, cloud node library and enriched catalog regenerated — Counter is the only node that moves in any of the three — and catalog:check, cloud-library:check and catalog:merge:check were each run explicitly and are green. Runtime suite 2164 passed, typecheck:runtime clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing with it The relay token is minted per editor launch and RelayClient connected exactly once: every handler was guarded by `if (settled) return`, so a close after the initial promise did nothing at all. Quit the editor, start it again, and every one of the nine tools threw "Not connected to the NodeGX relay." forever — with no hint that the cause was the restart, or that the server would have to be restarted too. It now reconnects on any post-settle close, re-reading the token through the same --token / $NODEGX_RELAY_TOKEN / file precedence cli.ts used at startup, on a 500ms→10s backoff floored at the 250ms accept grace (there is no `registered` ack for an editor peer, so a faster retry would race its own success detection). Startup still refuses when the editor is absent — only an already-running server recovers. State a reconnect must not carry over, because it all describes a process that no longer exists: viewerClients, topology, the event buffer and highestSeq. The last one is the quiet failure — a fresh runtime builds a fresh TraceBuffer whose nextSeq starts at 1, so a retained high-water mark would discard the entire new session while the tools reported a healthy connection. The one thing kept is `recording`, which is what this server was asked to do rather than something the app told us, and is re-armed on reconnect. And @noodl/observe joins test:packages — the server we are about to point every new user at had a suite nothing ran. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the editor instead of hiding inside the panel
… a node means FH-020, out of TALK-006 decision 2 (had 2026-08-05). A signal port has no property row by design and the canvas has no per-port hover, so the ~1045 output and ~1656 input descriptions ERG-004 restored into the node library had, until now, nowhere to appear at all. The one place a port's meaning was legible was the connection popup — which filters by what you dragged from, correct for wiring and useless for discovery: you never learn a node's full output surface unless you happen to drag at something that accepts everything. The new tab lists every input and every output of the selected node, whatever the port is: signals, connection-only ports, and outputs, none of which have ever had a row. Each one shows its display name, its type with enum values spelled out, its own description as text rather than as a hover, what it can connect to, and every wire attached to it as a chip that selects the node at the other end. Outputs list all their targets individually — "where does Done go" is the question the tab is for, and one label plus a count does not answer it. Mostly promotion, not invention. The tab strip was already in this panel and was reachable only with the AI assistant on; the connected-source chip already existed and was on 5 property-row classes of ~29, inputs only. Three things the task doc specified were wrong:⚠️ "Filter nothing" drops one filter too many. `conditionalports/*` is a filter over statically declared ports, so a port whose condition is false is still in getPorts() while not being on the node. The tab makes the same applyPortConditionsFilterForNode call the property panel makes.⚠️ "What it accepts" cannot come from canCastPortTypes alone. The cast table declares signal -> boolean and signal -> number; the connection popup then refuses those wires with a rule of its own that is nowhere in the table. Built from the table alone, the tab would have said Done can drive a Number input — the exact wrong answer it exists to stop. Both halves now live in one pure module, with the non-symmetry preserved: the block is on signal sources, so a Boolean output can still pulse a signal input.⚠️ The selected tab cannot be useState. createPanel builds a NEW function component on every node selection, so React unmounts and remounts the whole panel each time you click a node — the tab would reset on every click, and not resetting is what makes walking chip to chip work. Read-only throughout: nothing writes a parameter, adds a connection or dirties the project. The only interaction is selectNode, which moves the canvas selection. Not driven live — a dev launch rewrites the example project and this checkout is shared. Whoever drives it must relaunch rather than rely on HMR; this panel is long-lived and mounted, and ERG-004 lost a restart to exactly that.
…often Every cloud function already had an access rule and nobody could see it: with nothing configured, the graph's Allow Unauthenticated port decides, and the only way to say otherwise was to hand-edit security.json on the server. The backend now answers 'who may call this' as one question. GET /admin/permissions/functions lists every function with the EFFECTIVE rule - resolved by the same function the gate calls, so a reader and the enforcement cannot disagree - plus where the rule came from, functions deployed but unconfigured, and rules guarding names nothing serves. PUT/DELETE set it and put it back, both audited, both refusing a body they would otherwise ignore. A function can also carry its own request budget, on top of the shared one for all functions rather than instead of it, so a number here can only ever tighten. No default changes: an undeclared function behaves exactly as it did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d function Every function gets a row with the rule that would actually decide a call, and where it came from - a function nobody has configured says 'from the graph', not a blank. Pick Anyone, Signed-in, Nobody, or back to the graph; type an exact rule for roles and the OR form; give one function its own request budget. The panel never works the rule out for itself. It asks the backend for the effective answer, so what is on screen is what the gate applies. It also names the one pair that genuinely contradicts: a rule that lets anonymous callers through while the function's Request node does not have Allow Unauthenticated ticked. That call reaches the graph and fails inside it, and the row says so and what to do about it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The workflow-step trap said the opposite of what happens: a call-function step runs the function in process, so no call rule applies to it at all, and the thing that fails a step is the Request node's own port - which no rule in the panel can rescue. Also: classifyRoute returns a closed union and cannot express a per-function bucket, and 'which wins' between the panel and the node port was already decided in code before the task was written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Richard hit exactly the gap AIB-005 slice 3 was named for: leaving the
scoping conversation before agreeing on any pages produces a project
with no build plan at all (by design — nothing was agreed), but every
signal in the wizard read the same as the full-plan case. "Continue"
on the scoping step claimed it creates the project, when it actually
only advances to review; the review screen's confirm button just said
"Create Project" either way.
Fix the copy at both points instead of the mechanism, which already
does the right thing: the scoping step's exit line now describes what
Continue actually does, and the review button names the empty-plan
case ("no build plan (unfinished)") the same way it already names the
full one.
… the sheet dropdown stays on screen
Two Components-panel defects reported live:
- The create menu's disabled "Create Cloud Function Component" row
named the fix ("choose Cloud Functions in the sheet selector") but
could not do it — clicking it did nothing, since a disabled
MenuDialog row never fires onClick. Wired an onGoToCloudSheet
handler through the two folder-context call sites (the panel's
empty-space/header menu and folder rows; the nested-in-component and
canvas-trail contexts still have no sheet to jump to, so they keep
the plain disabled row). The row is now clickable and switches to
the Cloud Functions sheet when the reason is "wrong sheet" rather
than "cannot nest here".
- SheetSelector's dropdown was positioned with a plain `right: 0`
relative to the trigger. In a narrowed Components panel that put the
trigger close enough to the panel's left edge that the ~160-200px
dropdown overflowed past it, drawing over the icon rail. Re-anchor
in viewport-fixed coordinates, measured and clamped to both edges on
open.
…who else uses it Four gaps in local-backend management, reported live against a list that had grown past one entry: - listBackends() returned filesystem readdir order, not creation order. Sort newest-first in the one place every consumer reads from. - No rename existed anywhere — a backend named in a hurry (or by an AI provision, which names it from the project) was stuck with that name forever. Added `backend:rename` (main process + IPC), a hook action, and a menu item using the same StringInputDialog the Components panel already uses for sheet rename. - The only signal that a backend was the open project's backend was a line of shy 11px text. Bolded it, colour-matched it to a new highlighted border on the card, so it reads before you read anything. - Nothing said whether a backend was actually in use elsewhere. `projectIds` on config.json is write-only (stamped by AI provisioning, read by nobody) and would go stale immediately for manual creation, so this reads each known project's own root file directly instead — cheaper than opening every project through projectFromDirectory just to check one field. Shown as "Used by: X, Y" / "Also used by: X, Y". Confirmed already working, not touched: naming is already required by every creation path (create is disabled on an empty name); a backend already auto-starts when a project bound to it opens (ProjectBackendLifecycle, installed in router.setup.ts) — it just never covered backends the open project isn't bound to, which was never the ask.
…deGX GitHub repo renamed The-Low-Code-Foundation/OpenNoodl -> NodeGX (old name now redirects). Updates the electron-builder publish target, the issue-report ISSUE_REPO constant, the legal-window fallback link, the issue-template contact link, the README badge/release link, and the label-creation script's REPO var and doc comments to match. Deliberately not touched: the README's product-name prose and the sibling opennoodl-hosting.com/opennoodl-cloudservice/opennoodl-better-backend repo names, which are a separate content decision already made in REV-007 (rename scope was branding + packaging identity only). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Record the repo rename, the label creation, and Richard's Discord confirmation against HUMAN-GATED-ITEMS.md and PROGRESS.md, so the register doesn't outlive its own fixes. Also notes a new small find: the issue template's Discussions contact link is dead (Discussions is off), unowned rather than filed as an F-number. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bug_report.yml has had a severity dropdown since c57f729 (ALPHA-007 §2-4, 2026-08-03); the "there is no severity field" note was written 2026-08-06 without having seen that commit yet (it landed on an unmerged branch until 80d221c). Re-ran the prefill probe against the renamed NodeGX repo to confirm it still builds a valid URL. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ooting Writes ALPHA-004 §1/§2/§4 — the authored prose ahead of ALPHA-006's docs site — grounded in REACTIVITY-CONTRACT, PORT-TYPE-CONTRACT and a live read of the editor source, not invented. The node reference stays generated, per the "derive, never author twice" rule. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
§2/§3 turn out already discharged by LIB-005's live QA; §4/§6 not reached; §5 (PLAT-005 A/B/D) still blocked on canvas hit-testing, now with the graph-to-screen transform confirmed but forEachNode's gap against what's visibly rendered still unexplained. Also logs the ALPHA-006 §1 re-verification (already done, untouched) and the ALPHA-004 content drop this session's agents produced. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records what's actually done (B3/B4/B7, ALPHA-006 §1 re-verified, ALPHA-004 drafted, ALPHA-001 §1-3), what's left (§4/§5/§6, then ALPHA-006 §2+), the graph-to-screen transform math for the next §5 attempt plus the real blocker (forEachNode not matching what's rendered), and that gates weren't fully re-swept this session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
§4 (app-name round trip) PASSes: a real keystroke + graceful quit without blur + reopen round-trips correctly. §5 (PLAT-005) moves from "blocked" to root-caused: the forEachNode mystery was a stale/timing artifact, and the suggestion banner's real blocker is F102 (StyleAnalyzerCore.scanNode silently drops object-typed borderRadius overrides) plus F103 (a deprecated Button node type that ElementConfigRegistry doesn't recognize, silently losing the whole style-suggestion system). §6 (AI, no provider) PASSes via hybrid live+static verification after discovering this dev profile already has a working Anthropic key configured — an accidental ~$0.06 API call surfaced it, immediately rejected with no trace left. Full gate sweep re-run clean: Jasmine 2418/0, test:main 933/933 in isolation, all typechecks and package test suites green. One pre-existing gate gap noted (catalog enrichment coverage), not caused this session.
The docs origin doubles as the editor's content CDN for six non-documentation payload types (library, lessons, project templates, tutorials, what's-new). Splits the ambiguous call sites apart: getContentEndpoint() for the six payload consumers, getDocsEndpoint() left only on the three genuine docs-page links (NodeLabel/NodePicker's "read more", McpSettingsSection's docs-page probe). Both still resolve to the same origin — this is the call-site split only, so §2/§3 can be built next without risking a non-doc payload silently breaking. The disposition half of §5 (stripping opennoodl-docs to just the six payloads, the 413 MB asset question) is unbuilt — that's B5, still unresolved and needs a human decision. Gates: typecheck:editor clean, jest 933/933, lint:ci 860 vs 3916 baseline, test:ci (Jasmine) 2418/0 — matching the same-day ALPHA-001 baseline exactly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ence
§2: a fresh Docusaurus 3 site at docs-site/, wired as an actual workspace
member (root workspaces + lerna.json, not just files on disk) so npm install
and lerna/nx both see it. Carries over the local search plugin from
opennoodl-docs; does not carry over its SCSS, which was built for Noodl's
retired branding and a multi-plugin site structure this task doesn't rebuild.
custom.css is a light-touch pass overriding Infima's primary color with the
editor's own azure token. ALPHA-004's staged content (docs-site-content/)
lands in docs-site/docs/ close to verbatim, one broken link fixed. url/
baseUrl/org/project in docusaurus.config.js are marked provisional — where
this actually publishes is entangled with B5, a still-open human decision.
§3: scripts/generate-node-docs.js (npm run docs:nodes / docs:nodes:check),
one page per catalog node grouped by the picker's own category field, with
inputs/outputs split into values/signals/failure-outputs, dynamic-port
mechanisms, examples, and cross-linked related nodes. The staleness gate
deletes and regenerates docs/nodes/ from scratch so a renamed/removed node
can't leave an orphan page, then in --check mode diffs the fresh output
against disk. Generated pages are committed and gated, not gitignored.
Caught by actually running `docusaurus build`, not by reading the generator's
output: Docusaurus 3 parses .md as MDX by default, and the catalog's own
prose contains MDX-breaking sequences (<name> placeholders, {count} template
fragments) it was never written to avoid. Fixed with one config line
(markdown.format: 'detect') rather than escaping 55 generated files.
Gates: typecheck:runtime/cloud/viewer/editor/editor-tests clean, catalog:check
and cloud-library:check clean, library:check 58/58, docs:nodes:check clean,
jest 933/933, lint:ci 860 vs 3916 baseline (docs-site outside its target
glob), test:ci (Jasmine) 2418/0 — matching every other measurement taken
today.
Not built: §4 (the opennoodl-docs migration table) and §5's disposition half
(stripping that repo to six payload types) — both need B5, still unresolved.
docs-site-content/ is now a stale duplicate; deleting it was blocked by the
permission classifier as destructive and needs a human `git rm -r`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Richard asked whether every node was actually documented in the new
docs-site. Checking properly (not just re-citing the earlier-noted gap)
found two separate problems in the enrichment pipeline, both predating this
session:
1. noodl.cloud.addusertorole/getuserroles/removeuserfromrole (the F86 role
family) had no enrichment authored at all, so they didn't even appear in
node-catalog-enriched.json's node list. §3's generator only iterates that
file, so it silently produced zero pages for them.
2. The committed enriched catalog was separately stale relative to its own
authored inputs: docs/node-catalog/enrichment/*.json had an updated
accessControl port description (the NodeGX backend actually enforces
per-record rules, not the old generic "some backends ignore them") that
had never been landed via catalog:merge. Same failure mode as F76 — a
generation script exists and nobody re-ran it after the source changed.
Fixed by authoring the three missing enrichment files, grounded in the
actual node source (packages/noodl-viewer-cloud/src/nodes/cloud/{add
usertorole,getuserroles,removeuserfromrole,system-roles}.ts) and checked
against the style guide's port-name-coverage requirement programmatically,
which caught a missing treatUnchangedAs port on all three on the first
draft. Ran catalog:merge for real once catalog:merge:check --require-
coverage passed, then npm run docs:nodes to regenerate (194 files / 175
nodes), then rebuilt and curl-verified the new Add User To Role page end to
end.
catalog:merge:check passes clean for the first time today. Full gate sweep
re-run: typecheck x5 clean, catalog:check/cloud-library:check clean,
library:check 58/58, docs:nodes:check clean, jest 933/933, lint:ci
unchanged, test:ci (Jasmine) 2418/0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…is repo Richard's decision, 2026-08-07: leave the ~336 MB of live payload (library zips, lesson project files, template zips) in its own repo rather than the main monorepo — measured that it's genuinely live content, not stale doc media (only ~174 MB of this repo's 518 MB is dead screenshots/images that get deleted regardless), and dragging 336 MB of zips into git history with no LFS configured here would be permanent, taxing every future clone and worktree. Also matches the repo's intended future as a target for community-contributed templates and lessons — a lighter, purpose-specific repo is an easier target for that than the full engineering monorepo. Two owed GitHub admin actions, recorded in HUMAN-GATED-ITEMS.md B5, in order: rename opennoodl-docs to reflect what it now is (e.g. nodegx-content) before getContentEndpoint() is repointed at it, then enable GitHub Pages on this repo so docs-site/ can publish here directly — no third repo, no cross-repo deploy credential, since a workflow here can use the repo's own built-in Actions token. docusaurus.config.js's url/baseUrl are no longer a placeholder guess (previously nodegx-docs) but the real target (the-low-code-foundation.github.io/NodeGX/) — still not live until the two actions above happen, but no longer provisional. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs-site/MIGRATION.md accounts for every authored file in opennoodl-docs
(against the local clone at 7489e81, 2025-12-06) — a summary table grouped
by fate and folder, plus a full 431-path appendix so the count is auditable
rather than asserted. Zero unclassified.
Follows the spec's own folder assignments where it gave them, and extends
its stated reasoning by analogy where it was silent (docs/guides/
visualizing-data/ gets the same salvage treatment as its sibling guide
folders; docs/guides/deploy/{deploying-to-ios-and-android,embedding,
favicon,overview,project-structure,pwa}.md get Delete because phases
18/26 are post-alpha, not because the content is wrong).
Adds a fifth fate the spec's "four fates" framing never named but its own
§5 text implies: 193 files (library/ prose, repo meta, a prefab-authoring
boilerplate) that are neither migrated nor deleted — they stay in the
renamed content repo because they're coupled to payload that also stays
there. One exact duplicate caught and excluded from double-counting:
static/docs/guides/navigation/encoding-parameters-in-urls/README.md is
byte-identical to the docs/ copy already marked for salvage.
Satisfies ALPHA-006 acceptance criterion 5. No opennoodl-docs file was
touched — this is classification against the local clone, not a
migration; the actual strip still waits on B5's two GitHub actions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… morning's Written at the end of a session that closed nearly all of ALPHA-006 (§2-§5 code half, §4 migration table on top of already-done §1/§6), fixed a real node-catalog enrichment gap (172/175 -> 175/175), and got B5 decided by Richard. Measured at e3922d5. Leads with the one thing that actually needs fixing first: HelpCenter.tsx and FailedStep.tsx both still hardcode the pre-rename OpenNoodl repo URL, which is why Richard's live click on "Report a bug" produced a blank untemplated issue (GitHub's rename redirect drops the ?template= query param) - a real regression a real user hit, not a hypothetical. Also corrects this session's own wrong assumption about where ALPHA-007's composer lives: not the in-app Help Center's "?" menu (those are deliberately plain GitHub template links), but a native OS menu item registered in main.js, invisible to CDP's DOM-only reach. Records the inspector-based route to actually trigger it next time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
B3's rename session grepped for OpenNoodl and missed these two files. GitHub's rename redirect carries the path but drops ?template=, which produced a blank issue with no template/labels when Richard clicked "Report a bug" live. Verified by intercepting shell.openExternal in a running editor and confirming the composed URL now targets NodeGX with the template param intact. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found live while driving ALPHA-007's report composer via the main-process inspector: with the "What happened" field filled in, the form's content exceeded 90vh and the Send/Cancel buttons were unreachable — .VisibleDialog set overflow: hidden with no inner scroll container. The already-authored ::-webkit-scrollbar rules on the same class were dead code, only ever mattering once this became auto. Affects every CoreBaseDialog-based dialog, not just this one. Verified live: after the fix, the dialog scrolls and the buttons are reachable, with the underlying popup still visible behind it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Findings register row for F104 (BaseDialog.module.scss clipping tall dialogs, fixed same session as 022fb20), the ALPHA-007 status row, and a log entry covering both the stale-URL fix verification and the composer drive: criterion 2 confirmed real, the field-prefill contract verified against real composed data, redaction spot-checked on live output, and the on-disk bundle confirmed on macOS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
B4 (issue-label permission) was done 2026-08-06 — gh label list confirms all nine labels live on the repo. My own ALPHA-007 log entry inherited the prior handover's "B4 owed" claim without re-checking it. Caught before it could outlive its own fix a second time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ng's Records the stale-URL fix, ALPHA-007's first successful live drive via the main-process inspector, and F104 (found and fixed). Corrects the prior handover's stale "B4 owed" claim. Full gate sweep re-run clean at 7d64049. Next session's priorities: chase the human-gated items (B6 is two minutes and unblocks criterion 3), redo ALPHA-007 criterion 4 live now that the composer is reachable, and the docs repo strip once B5's two GitHub actions land. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Built a real hostile-fixture project (API key in a node parameter, a backend endpoint, a client-named component, paths outside the project root) and drove the actual Report a problem composer end to end against it — the composed GitHub URL, report.md, and diagnostics.json all verified secret-free. Closes the last unblocked engineering item the prior handover flagged; what remains is human-gated (B5, B6, testers). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Supersedes the same-day 7d64049 handover now that ALPHA-007 criterion 4 is closed. Reframes the remaining work honestly: almost everything left is Richard's action, not more engineering, since the exit criterion itself requires a non-author tester. Sequences the human-gated queue by lead time, names the one engineering task each unlocks, flags D4 as the one unblocked fix worth doing now, and bundles the A3 tester ask with ALPHA-004's criteria 1/4 so testers aren't recruited twice. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… by PR #19 Pushing cline-dev to trigger main-sync CI for the first time in 5 days (440 commits) surfaced real, pre-existing drift that had never been checked: - typecheck:backend-tests (~22 errors): a dual-declaration mismatch where noodl-viewer-cloud/src/index.ts mixes a dist-types-resolved `@noodl/runtime` import with source-resolved deep imports, producing two nominally distinct copies of the same runtime classes; plus nodegx-backend's strict:true surfacing real strictness gaps in noodl-runtime source (deliberately strict:false there per PLAT-003) and a handful of genuinely untyped test call sites. - ERG-003's list-port coverage census test was a stale snapshot (48/23 editable ports) against a catalog that grew to 54/32 — invariant check still passed, only the numbers needed updating, per the test's own documented procedure. - TSFixme ratchet (+43 TSFixme, +35 any): accumulated debt across 20 unrelated files from the same unpushed window, none from this fix. Baseline raised per the ratchet's own escape hatch rather than rewritten blind. All of typecheck, typecheck:editor-tests, typecheck:backend-tests, tsfixme, lint:ci now green; noodl-runtime (2298), noodl-viewer-cloud (172), and the three touched nodegx-backend suites all pass with no regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
461859b (2026-08-02) fixed the object->string cast the day after this row's own 2026-08-01 live check that filed it. Re-verified 2026-08-07: nda-014-outbound-string-cast.test.ts (9 cases) passes clean on HEAD. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s it @noodl/preview's suite drives the built CLI over HTTP and fails fast via assertPrerequisites() if packages/noodl-preview/dist or the deployed viewer runtime is missing. The test-packages job never built either — it runs independently of the build job (separate runners, no shared filesystem) — so every one of its 14 specs failed the first time this job actually reached @noodl/preview:test. That had never happened before: Nx bails the whole `lerna run test` on the first package failure, and something upstream (noodl-core-ui this session, almost certainly something else before it) has apparently always failed first, silently skipping preview's suite rather than running it. Verified locally: `npm run test:packages` end to end, 9 projects, all green with the two build steps added. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Builds docs-site/ (Docusaurus, regenerating the node-catalog pages first) and deploys via actions/deploy-pages using this repo's own built-in Actions token — no cross-repo credential needed, since docs-site/ already lives in this monorepo rather than the old opennoodl-docs arrangement. Triggers on push to main only (this publishes a public URL) plus manual dispatch. Verified docs-site:build succeeds locally (194 files / 175 nodes, Docusaurus build clean). Repo-side: GitHub Pages enabled via API with build_type=workflow, source now https://the-low-code-foundation.github.io/NodeGX/ — matches docusaurus.config.js's already-configured url/baseUrl. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
richardosborne14
added a commit
that referenced
this pull request
Aug 7, 2026
…, D4 correction Full account in PROGRESS.md's new 2026-08-07 (fourth session) entry and in memory (main-was-never-synced-2026-08-07). Short version: B6's "blank issue form" traced back to origin/main never having received fork work since 2025-09-09, not a prefill bug — fixed via PR #19 (cline-dev -> main, 6276 files), which also surfaced and fixed real CI drift hidden by 5 days of unpushed commits. B2 and B5's remaining GitHub actions are done; B6 needs a fresh run now that bug_report.yml actually exists on main. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
richardosborne14
added a commit
that referenced
this pull request
Aug 7, 2026
First release cut since origin/main was synced (PR #19) and since Apple signing credentials (A1) were added as GitHub secrets. v0.1.0 stays a draft with no macOS artifact; this is a fresh build to actually produce one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mainhas been the stock pre-fork Noodl checkout since 2025-09-09 while all rebrand and alpha-launch work (phases 14.5 through 33) happened oncline-dev. This PR bringsmainin line with reality: the rebrand to NodeGX, the alpha-launch legal/docs/report-a-problem/release infrastructure, and everything else built since the fork.Surfaced this session while investigating why GitHub's issue-template prefill (ALPHA-007 criterion 3) rendered a completely blank form:
mainhas no.github/ISSUE_TEMPLATE/bug_report.ymlat all (GitHub resolves issue templates from the default branch only), and also lacksrelease.yml,PRIVACY.md,TERMS.md, anddocs-site/.Test plan
🤖 Generated with Claude Code