Sync main: everything from v0.1.3 to v0.2.3 (1920 commits) - #20
Merged
Merged
Conversation
richardosborne14
added a commit
that referenced
this pull request
Aug 7, 2026
…s the sibling assertion PR #20 CI (2026-08-07, seed 82518): "Project import and export unit tests re-keys imported node ids while reusing the target component id" failed on one of two parallel runs, same commit, different random seed -- confirmed flaky, not a regression. This exact test's own comments already document three prior rounds of diagnosing identical order-dependent-identity flakiness through NodeLibrary.instance (a singleton shared across the whole suite), each resolved by asserting a stable property instead of raw object identity -- e.g. `instanceNode.type.name === '/comp1'` a few lines above this one, explicitly captioned "asserted, unlike identity, because it is order-independent". That caption turned out to be wrong for THIS assertion too: `NodeLibrary.instance.typeCache.get('/comp1')` toBe(importedComp1) is the last remaining raw-identity check in the spec, and seed 82518 is the seed that proves it isn't order-independent either. Same fix as its siblings: assert the name resolves to a real component, not that it's the literal same object. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…p Component Stack: a push is a call, a pop is its return Tier 2.8 row 11, the component-stack trio, §16's deferred pair landing with the container it drives. Picker 105 → 108 on this branch. - src/lib/pageStack.ts (src/emit/pageStackLib.ts): navigation-handler.ts transcribed — a module-level registry keyed by name (`name || 'Main'`), the navigation queue replayed on registration, `settle`'s one-report-per-press; navigation-stack.tsx's navigate/replace/back/reset minus the transition and the url (`_isAlreadyShowing` verbatim, the three failure sentences verbatim, no way back on a replaced or reset entry); navigate-back.ts's no-stack-in-scope failure; `usePageStack` registers in the effect and shows nothing before it, as the runtime does. - `Page Stack` is a visual role: the row renders the TOP entry only, one `&&` line per page, the params spread as the page's own Props, `key` fresh per push, the reserved `pageStackEntry` prop where the page keeps a Pop; Top Component Name / Stack Depth off the handle; Reset then its Done chain; the class carries the runtime's defaultCss. - The pusher prints as navigate.ts's own call — target, params typed against the target's declared inputs, the back callback writing the results row BEFORE testing the action (the runtime flags the outputs before the signal), hasNavigated/hasUnchanged; replaceComponent in Replace mode with no callback. The Pop prints per trigger port as a two-arm return; the Failure arm raises pop-component-stack/no-stack-in-scope. - Refused by name (§61.0, all graded by mutation): Use Routes; wired Name/Components/Start Page/Stack/Mode/Target/Transition/tr-*; an empty list, a page naming nothing, a routed page, an absent component, a Start Page off the list; no stack of the name; a stack that did not translate; two same-named stacks disagreeing; a target off the list; a parameter typed against the target's interface; the back channel in Replace mode; an undeclared back action or result; Completed on all three; the pusher's Error and a Done consumed as a value; a Pop no stack lists; the reserved prop claimed; a Pop's Unchanged, and its Error outside the Failure arm; a Back Result read while nothing fires or attaches the push. - Recorded divergence: no transition (the runtime animates by default and keeps both components mounted until it ends). - Fixture tests/fixtures/wizard-desk: a tab bar in Replace mode and a two-step wizard with a back result. Reverted arm 15 refusals; built 0, 23 files, the real tsc clean. - Gates: pkg tsc 0 · component-stack-trio 50/50 · component-stack-pair 11/11 (the three deferred pins flipped to positive rows) · neighbours green one at a time · whole suite 72 files / 2512 rows, exit 0 · ledger check OK · picker 108/127 · arms 15/15 killed, sources restored md5-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tack trio beside §60/§62/§63; floor reconciled 111+3 → 114 Thirteen conflicts, all additive: the ledger's floor and ratchet sentence (108 on the branch → 114 here), the eight shared floor pins plus the four newer specs' own, the hook-gap and action chains in component.ts (a break restored between the component-object and stack cases of the dependency switch), the resolveExpr ladder and the maybeUndefined switch in plan.ts (the stack cases printed before the component-object case so both still fall to return true), and the two roles' if-blocks sharing one tail. Package tsc 0, ledger check OK 121 translated, picker 114/127, the thirteen pinned specs 641/641. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… row 11 ticked; PROGRESS and the alpha notice at 114/89% Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ow the top, as the runtime keeps every page's node alive
Reading navigation-stack.tsx before the drive: navigateAsync keeps the outgoing page as top.from and backAsync re-adds
it, so a Text or an input in the pusher's own component keeps its state across a push and a pop, and the Back Results
the pusher renders arrive in a component that is still mounted. The top-entry-only row of session 86 unmounted the pusher
under the pushed page and remounted it on the pop — its back-results row booted {} and the rendered Back Result was
always empty. The handle now exposes entries; the row maps them into a wrapper that is display: contents on top (a
direct flex child, as the runtime's page is) and display: none below. A6 pins the new row; package tsc 0, the trio 50/50,
the pair 11/11. Drive of both arms owed (EXPECTED61-drive.md written first).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…named answers Lesson 5's app plus the first decision it makes on its own. `Is Happy` (Expression, `pokes > 4`) turns the poke count into a boolean, `Mood Check` (Condition) forks on it, and `Mood` (States) holds the two sentences — one of which reaches a new `Mood Line` Text on the card. Gated: create_lesson F1-F4 all pass with allow_unrendered NOT used; lessons:check exit 0 over 7 bundles; starter(6) byte-identical to solution(5) on all four graph files, first attempt. The chain now reads 3n → 6n → 11n → 15n → 18n → 21n → 25n. 🟢 First spine lesson whose behaviour was DRIVEN rather than only rendered. Served with render-from-disk and clicked with drive-page: 4 pokes → "Nibbles is dozing", the 5th → "Nibbles is delighted", Rest → back to dozing, errors: []. A single static frame cannot tell a live branch from a dead one, because a States node starts in its first state either way. 🔴 The curriculum entry named `Switch`, which cannot do this job — it is a one-bit memory, not a branch — and omitted `Expression`, which is unavoidable because nothing in the Logic category compares two numbers. Third correction in six entries; a curriculum edit is owed in nodegx-community. 🔴 Lesson 5's plural is deliberately NOT fixed. Its outro promises the move, not the repair, and no spine lesson can repair it: the words live in Caption's graded `format`, so re-grading it would make derive_starter retract it and break the chain. The intro says so out loud. Four new rows in DEFECTS-LESSON-6-FOUND.md, all owner NONE. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… two conditions at once Lesson 6's app plus the first thing in it that no click causes. `Patience` (Delay, 5s) is started by the Page's Did Mount and restarted by every poke; `Ignored` (Switch) turns its one Finished signal into a fact an And can read; `Demanding` (And) requires that fact AND Mood Check's Is False; `Fade the nag` carries the answer to a new `Nag` text's opacity. Gated: create_lesson F1-F4 all pass with allow_unrendered NOT used; lessons:check exit 0 over 8 bundles; starter(7) byte-identical to solution(6) on all four graph files, first attempt. Chain: 3n → 6n → 11n → 15n → 18n → 21n → 25n → 30n. 🔴 A static render cannot see this lesson: the shipped solution and one with all eight of its wires deleted render identically, because the delay has not fired when the harness screenshots. Registered as I1 — every gate the programme has is blind to a time-delayed lesson. 🔴 The first drive nearly produced a false pass. Shots at t≈0 and t≈8s BOTH showed the nag, because starting Chrome takes longer than five seconds — a reading that fits "the timer works" and does not exclude "visible at load". The control that separates them is a second arm at duration 60000, which stays clear at t≈12s. Then the loop was clicked: 5 pokes + 8s → delay fired and the And still refused; Rest → nag appears; Poke → nag fades out. errors: [] throughout. The middle arm is the only one in which the two And inputs disagree. 🔴 Curriculum: `Timer`'s picker name is `Delay`, and `Switch` — unnamed in the entry — is the lesson's hinge, because an And cannot read a signal. `Or` and `Inverter` are named in the prose and deliberately not built; an Inverter here would duplicate an Is False that already exists. ⬜ tests-unit/rel-012 + tut-004 NOT run: a peer held the box with a 30-minute browser drive. Next session's first job; the bundle is committed so the shipped-lessons check can see it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…h; session 87 hand-off at 114/127 with every gate on the merged tree Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he gate had a second red from another phase projectFileWatcher.test.ts slept a flat 600ms for a 60ms-debounced fs.watch event and asserted afterwards, which grades the box rather than the watcher. The number nobody had taken: the batch lands at 73ms (5 runs, 72-74ms), so the budget was ~8x headroom on a chain of timers and two concurrent suites ate it. It now waits on the EVENT. 278ms, was 607ms. Four arms, all reading 16 tests that ran so the counts reconcile. The defect is reproduced DETERMINISTICALLY by stretching the watcher's own debounce to 900ms past the 600ms budget -- no CPU contention needed, no peer's drive disturbed. Arm A (original spec, stretched) RED. Arm B (fix, same stretch) GREEN at 1123ms, which is the proof the wait is real. Arm C (fix, ceiling below the latency) RED. Arm D GREEN. Three of my own arms were wrong before they were right, and each correction came from reading a control instead of adjusting it: - Arm C PASSED at first: the 200ms settle is itself a wait and supplied what the ceiling refused, so the ceiling was decorative. Fixed by latching the answer before the settle is spent. - The first latch was a bare expect() before watcher.stop(), so a failure leaked the fs.watch handle and jest never exited -- failed correctly in 35ms, then sat 7 minutes until killed. EXIT=143, not 1. A spec that HANGS the gate is worse than one that reddens it. Every assertion is now after stop(). - Arm A briefly read -- built by surgery it stripped the latch declaration and left its assertion, grading nothing while looking like a red. And the fix was WRONG until the gate was read TWICE. jest's default per-test timeout is 5000ms against a 10s ceiling, so the ceiling could never be reached: the stopwatch had merely moved from the spec to the runner. Run #1 passed at 6.18s of suite time, run #2 died on the runner timeout. Ceiling is now 30s with an explicit 35s it() timeout above it. sb-007/site-template.test.ts: a literal count gate reading 401 against a template holding 406. Reconciled exactly, not adjusted to fit -- P77/SBR-007 AC3 (f77e664, 13:37 same day) adds five ids to /Admin/SectionRow: dropZone, dropHint, dropWords, dropRefused, dropRefusal. 401 + 5 = 406. Nobody caught it because the gate lives in noodl-editor and the nodes live in noodl-mcp, so a per-package run on either side is green. The count was reconciled; the FEATURE was not reviewed. The sweep that found it also found the defect class is not unique: bld-004's reasoningChannel -- the OTHER red s40 wrote off as a lone flake -- and aib-009/turnDeadline both run a 60ms stall window against a sleep(STALL_MS/2) heartbeat, a 2x margin. Registered, owner NONE, untouched: they belong to BLD-004 and AIB-009, and turnDeadline.ts reads Date.now() deliberately so the source is not at fault. test:main #3: 423 suites / 7086 tests, EXIT=0, ZERO reds -- the first fully green reading in this phase's record, and taken WITH a peer's 7-suite jest overlapping it. Not a clean-HEAD baseline: the tree carried another lane's uncommitted SBR-011/D46 realtime work throughout. TASKS.md also carries sessions 42-43's uncommitted board corrections (REL-012/013/014/016), verified off commits at the time and left unsaved for hours -- included here rather than swept silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d the app can now be driven Both new lessons' jest gate recorded: tests-unit/rel-012 + tut-004, 109 passed, 7 suites, exit 0, run after both bundles were committed so shipped-lessons-reach-the-artefact could see them. The next-session prompt is re-derived from the task files and carries the serve-and-click recipe (render-from-disk + drive-page), which is the thing this session added to the phase's toolkit: the lesson runner is still the first job, but the app the lessons build can now be observed doing what the prose claims. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… were one fault test:main reads 423 suites / 7086 tests EXIT=0 with zero reds for the first time in this phase's record, taken with a peer's 7-suite jest overlapping it. Three faults, not the one registered: projectFileWatcher's stopwatch (fixed, and the fix was itself wrong until the gate was read twice -- jest's 5000ms per-test timeout made a 10s ceiling unreachable); sb-007's literal count gate, red at HEAD since P77's f77e664 and invisible to any per-package run because the gate and the nodes live in different packages; and bld-004/aib-009's 2x stall margin, registered with owner NONE. The register row for the stopwatch race is struck and replaced with its sibling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed in a browser SBR-003 §2 refused to close this on the legend: "verify with a rendered probe (a maxWidth from a token actually constrains a box in the viewer), not by quoting the legend". At s4 nothing consumed the token so the probe had nowhere to stand. /Pages/Site's shell consumes it now. sb008 §7, two arms, 1280x900, anonymous, enforcement on: shipped graph --site-measure=44rem max-width 704px shell 704 frame 1280 unknown token --site-measure=44rem max-width none shell 1232 frame 1280 The clamp BINDS (704 inside 1280, not a box that was that size anyway), and the token is DEFINED on :root in both arms — the control varies what the port references, never what the token is, which is what makes it a probe of the port rather than of the token. The first run read max-width:none in BOTH arms and fitted "the dimension port drops var(--token)" perfectly — the exact defect being hunted. It was the fixture: authorSiteTemplate never performs the editor's install step, and render-from-disk takes CUSTOM tokens from metadata.designTokens alone, so every token the template OVERRIDES still resolved (the page looked themed) and only the one it MINTS was missing. applyTemplateDesignTokens was private to vib001-site.look.ts and was its only caller — the look harness's photographs were right and thirteen drives beside it were not looking at the same page. Moved to helpers/site-drive.ts and imported there; the other twelve drives are a sweep with its own before/after and are NOT done. Registered as D57 (register file left uncommitted — it carries a peer's SBR-011 hunks). Two assertions were predicted wrong and reconciled rather than edited to fit: the control was predicted to fill the frame at 1280 and measures 1232, the pair at 576px and is 528. shell states width:100% so unclamped it fills its parent's CONTENT box and frame carries 48px padding — so the probe gained framePaddingX and the control asserts shell = frame - padding, not a literal. §7 reads on COPIES of the project: customTokens carries the whole Studio palette, so installing it into the shared project would have moved every colour the other 24 tests read. Gates: sb008 31/31 EXIT=0 (24 pre-existing unchanged, 7 new), 41.5s; the red arms recorded too (run 1 5-failed on the fixture gap, run 2 2-failed on the mis-predictions). typecheck:mcp EXIT=0. vib001-site.look.ts loads and collects its 2 tests; its photographs were NOT re-taken. typecheck:backend-tests is unrunnable on this box and ts-jest here is isolatedModules, so the green suite is not a typecheck of the two backend-test files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ent both ways); a stretched flex item had collapsed the parent bound to one value The board-desk drive measured a 320 px wrapper around an 80 px card: an unstyled div inside a column flex container stretches across the cross axis, and react-draggable's getBoundPosition then reads innerWidth − outerWidth − offsetLeft = −offsetLeft on both sides, pinning x. The interpreter drags the child's own root, which never stretches on its behalf. Pinned as drag.test.ts F3; the §B style rows read the pair through wrapperStyle(). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… run: panel-desk clean bar the registered unseeded Variable, link-desk 1 GET + 3 PUT against a fake backend, board-desk's stretched wrapper found and fixed Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…at was missing was the gate 1e41503's message reads as though the var(--token) rendered reading were first taken there. It was not. s5 (2026-08-28) took it and wrote it into SBR-004 §140-141 with the identical structure — known token 704px computed and 704px rendered, var(--site-measure-typo) control none and 940px in a 988px frame — and TASKS.md's s5 entry says "✅ SBR-003's carried probe answered". Two files in this phase disagreed: SBR-003 §5 says "still owed", TASKS.md and SBR-004 say answered. This session read §5 only. The re-measure-before-you- inherit check WAS run, but against the wrong question: "does a spec exist?" (no, and that grep was right) instead of "has this been answered?" (yes). AC5's actual words are "the probe specs of §2 exist and are green — each one paired with a control". s5's reading was a one-off drive: nothing asserted it and nothing would have caught a regression. So the contribution is the standing gate plus D57, not the discovery, and the s51 reading REPRODUCES s5's table at a second viewport rather than replacing it. History not rewritten — a peer committed on top of 1e41503 before it could be amended. SBR-003 §6 carries the correct statement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…as never what stood in the way
Finding 6 of Richard's 2026-09-04 testing pass ("create a component, open the workbench
dropdown, it is not there") shipped in 16f38e7 with no gate, and the reason written into
that commit was: "@testing-library/react is not installed, so the menu cannot be opened in a
spec, and a source-text assertion would pass on dead code."
The second half was right. The first half was the wrong cause, and a probe measured it before
anything was written: PreviewChrome.tsx cannot be graded by this runner because it imports
@noodl-core-ui/.../Icon, whose require.context call ts-jest rejects outright — Icon.tsx:207,
TS2339 — so the module fails the suite TO RUN. The dev dependency would not have helped. The
same probe read previewScope.ts clean, 17 exports.
But the decision was never in the rendering. "What list does the menu read when it opens" is a
function from a getter to an array, so it moved to previewScope.readMenuComponents and open()
calls it. tests-unit/ben-004/previewScopeMenuRead.test.ts then grades four things:
§1 the premise — a model shaped exactly like ProjectModel (getComponents() returns
this.components, addComponent pushes in place) hands out ONE object across two reads.
A known-firing control: false here would mean ProjectModel started copying.
§2 the fix — the read is not the live array, each open has its own identity, and each
snapshot is of the moment it was taken.
§3 the symptom — Richard's bug reproduced through the real benchTargets behind a faithful
useMemo (Object.is per dep): the live-array arm computes once and /Cards/New is not in
the menu; the fixed arm computes twice and it is.
§4 the wire — open() calls it, read off source with comments stripped.
Four arms, and 13 total in every one of them is what says each graded something:
Arm A readMenuComponents returns getComponents() 5 red of 13
Arm B open() reverted to setComponents(getComponents()) 2 red of 13, EXIT=1
Arm C1 the reverted line named in open()'s comment, fix in place 13/13, EXIT=0
Arm C2 stripComments() removed from the spec's own reader, nothing else
1 red of 13
Arm C2 is the one that answers the original commit's warning. The fix's own prose quotes
setComponents(getComponents()) — the reverted form — so without the strip the documentation
reddens the gate, and with a differently-worded comment it could equally have passed on behalf
of a line that had been undone. C1 and C2 together say the strip is load-bearing rather than
decorative, which is why the reverted form is now named in that comment on purpose.
Gates:
tests-unit/ben-004 alone 13/13, EXIT=0
npm run test:main 425 suites / 7115 tests, EXIT=0, zero reds
npm run typecheck:editor 0 error TS, EXIT=0
test:main reconciles against s44's 423/7086 baseline as +1 suite/+13 tests mine (measured
alone) and +1 suite/+16 from peers' untracked lanes in the same tree — syl-j1/lessonProse
appeared between the two runs and reads 19 tests now; the arithmetic closes if it held 16
during this run, which was not read directly.
NOT run, and why: the editor's test:ci is unreadable today — a peer lane holds uncommitted
specs under packages/nodegx-export/tests, which reddens that gate for everyone (registered
trap). The two jasmine specs that touch previewScope (tests/canvas/preview-scope.test.ts,
bench-frame-default.test.ts) live there; this change to previewScope.ts is purely additive —
one new exported function — and their subjects are untouched.
What this still does not establish: no menu was opened. This holds the identity contract and
the wire; it cannot see focus, the search field, the scroll, or whether the row is clickable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…urth member, sse-connection.ts transcribed into src/lib/sse.ts; picker 114 → 115/127 net.noodl.SSE joins STREAM_NODES as kind 'sse' (the data port made optional, a lib + sourceFile per spec, one kind→type ladder). src/lib/sse.ts transcribes the node, both transports (fetch with a streaming body and Last-Event-ID; EventSource with the browser's own retry), the backoff, the 512-id dedupe window and stream-parsers' SSE half; useServerSentEvents(source, options, on, env) puts the two Actions on the handle as verbs, the four Events and the outcome trio as listeners, every Status/Data output as a live getter; the Failure outcome raises sse/connect-failed. component.ts groups hook imports by module; emitApp ships sse.ts and lets it earn streaming.ts and errors.ts. Fixture token-desk (SSE Text → a Text Accumulator's Chunk, On Message → Add — the streaming-LLM shape); spec 35 rows with a scripted fetch, a scripted EventSource and the runtime's timer seam; 14/16 arms killed, one equivalent, one closed by a new row. The three 'x' in options seam reads take ?? null: the emitted app's tsconfig is stricter than the package's. EXP-013's scheduled-badge pin moves to net.noodl.WebSocket. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…arms, the drive against a fake event stream; §3 row 11 ticked; PROGRESS at 115/90.6%; session 88 hand-off Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ort (91%), 12 badged, after EXP-011 §64 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… make — and a stale row struck Two corrections, both found by asking what a reading would say if the thing it describes had moved. 1. ben-004 §1 called a STUB a known-firing control. `LiveArrayModel` restates ProjectModel's two lines (getComponents() returns this.components, addComponent pushes in place) and the comment on it said: "if this ever reads false, ProjectModel started copying." It cannot say that. A stub does not notice when the thing it imitates changes, so if ProjectModel ever started handing out a copy, §1 would carry on green describing a world that had moved and §2 would keep passing on a property the product no longer needs. §1.3 now reads those two lines off projectmodel.ts itself, with comments stripped, and returns null rather than asserting so a deleted subject is a named red instead of Tests: 0 total. Driven red by making the real getter copy — 1 of 14, EXIT=1 — and projectmodel.ts restored md5-identical (b8b51edc28386bfd5cb90c959f79b8aa). All four arms re-taken at fourteen rows so one table holds: green (the reverted line named in open()'s comment) 14/14, EXIT=0 Arm A readMenuComponents returns getComponents() 5 red of 14 Arm B open() reverted 2 red of 14 Arm C2 stripComments() removed from the spec's reader 1 red of 14 Arm D ProjectModel.getComponents() copies 1 red of 14 2. REL-002c §9.7 item 2 was stale by fifty-three minutes. "The site-builder template still has no outline gate. It ships 26 as tags and nothing holds them there. Owner: NONE." It was true when typed, at 22:30 on 09-03, and 373375f landed at 23:23 the same evening with that sentence's own numbers in its subject line. sb007Template.test.ts §12 is a full peer of the members' area census, not a subset: one h1 per page over seven pages, one main per page, the h1 INSIDE the main, the nav resolved from the artefact rather than named, no node carrying an `as` its type has no port for, and the same near-miss control. Read green rather than inferred from the commit message — 9 passed, EXIT=0, including two mutant rows reproducing the template as it shipped. The row's claim was also not what a skim would take it for: the heading-ORDER check is a different gate (headingOrder.test.ts, s41, both artefacts). What was missing was the landmark census. Both now exist, for both templates. Gates: tests-unit/ben-004 14/14 EXIT=0; npm run test:main 426 suites / 7136 tests, EXIT=0, zero reds; npm run typecheck:editor 0 error TS, EXIT=0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d one stale line The ungated lane: the workbench dropdown fix from Richard's testing pass is gated (14/14, red on four separate reverts), and REL-002c §9.7 item 2 is struck because the site-builder outline gate had landed fifty-three minutes after the row was typed. The §B register row is struck in place with the measurement under it, so the strike is auditable rather than a status flip.⚠️ This commit also carries session 45's CUT LANE, which was written into this file at 15:30 and left uncommitted. It is not this session's work and is not edited here — there is no way to commit the s46 lane without it, and leaving the file uncommitted indefinitely is the worse option. The only line of s45's that changed is the §B row this session struck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e name on the node
Sweep of the phase-79 defect registers (21 rows, every one owner NONE). Four rows
closed and two narrowed, each gated and mutant-checked.
J1 — a blockquote rendered as a literal `>` in 5 of the 8 shipped lessons. The row's
observation reproduced at HEAD character for character; its diagnosis did not. It is
three independent defects, not one:
(a) renderMarkdown had no blockquote branch, so `> ...` fell to the paragraph branch
and escapeHtml made the marker a literal >.
(b) the bold rule matched content with [^*]+, which forbids the character that opens
a nested emphasis — so `**bold with *nested* inside**` never matched as bold, and
the single-star rule then paired across the wrong spans: the emphasised words
rendered plain, the plain words italic, two stray asterisks left over.
(c) found by the corpus arm and recorded by nobody — emphasis ran after code spans
without treating them as opaque. `moods` ships `min(96 + pokes * 8, 200)`, so that
multiplication paired with the `*how much*` after it and opened an <em> INSIDE the
code and closed it outside: mismatched tags, corrupted code sample, italics on the
wrong words — on the lesson that teaches the Expression node.
J4 — the multi-key paramsEqual arm named the parameters it graded but not the values.
The single-key arm always had, so the sentence got less useful the more the step asked
for. Dimensions now render as `560px`, not {"value":560,"unit":"px"}.
D2 — the "Looking for..." line repeated the internal type id at the learner. Wider than
recorded: eight lesson types do not read as their own name, and three are ordinary words
that are the wrong word (Circle is Shape, Timer is Delay, Logic Builder is Visual
Function). Now resolved through the node picker's own getItemLabel, injected from the
view, so the sentence and the picker cannot disagree.
E4/H3 (Timer vs Delay) are NARROWED, not closed — the runner half is fixed; the
curriculum entry naming `Timer` lives in the other repo and is untouched.
Gates: tests-unit/syl-j1/lessonProse.test.ts (23 rows, incl. an 8-lesson corpus arm and
the 8 pre-existing renderMarkdown assertions restated in the cheap runner) and 15 new
rows in tests-unit/fix-025/check-my-work-copy.test.ts. Each of the five fixes reverted
alone reddens only its own rows. 1053/1053 across the 59 lesson-touching suites.
Note: DEFECTS-THE-RUNNER-DRIVE-FOUND.md was untracked — session 8 wrote it and never
committed it; it lands here because this commit's write-up references it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…whole pkg 76/76, editor tsc 0, exp-012/013 157, test:ci at the floor), M5 killed by the Auto-Connect-while-live row Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… wrong Every shipped lesson read "Written locally" on the shelf. All eight declare authoredBy: "ai", so resolveProvenance downgrades them from the seed's `curated` to `local-ai` — correctly, because that downgrade buys the stricter install gate the seed explicitly refuses to dodge. The defect was that the shelf read the gate's field as an authorship claim, denying in the UI what the seed says of these lessons: "they are editorial, and a person stands behind them." LearningEntry gains `origin` — the installing caller's word, recorded before the manifest's claim is folded in. `provenance` is untouched and still downgrades; the gate is exactly as strict as it was. Only the card moved. The migration arm is the part that would have been missed: writing `origin` at install fixes nothing for lessons already on a shelf, because the seed skips an id it has already installed and would never rewrite them. So badgeProvenance falls back to isShippedLessonId before falling back to the gate class — three inputs, each with its own spec row. noodl-core-ui is unchanged: the card's `provenance` is read in exactly one place (PROVENANCE_LABEL), so it is the badge input and the fix belongs editor-side. Gates: 5 new rows in tests-unit/uni-007/learningstate.test.ts (21 total). Reverting the whole fix reddens 3; removing only the migration arm reddens 2; the 16 pre-existing rows stay green in both. 1058/1058 across the 59 lesson-touching suites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Expression` declares fourteen ports and flagged four: result, isTrue, isFalse,
and error on the failure path. asString, asNumber and asBoolean were declared
with getters and flagged nowhere in the file, so a wire leaving one delivered
whatever the getter returned when the connection was made and never delivered
again.
`As Number` is the port an author reaches for when the target is a number input,
which is exactly when the failure is silent and total: an unevaluated expression
reads 0, 0 into a dimension renders nothing, and the parameter that would have
saved it is overridden by the wire. Building spine lesson 5, `As Number → size`
drew no circle at all with every gate green — F1-F4 pass, placeholders 0,
consoleErrors [].
The three now flag alongside result, INSIDE the existing
`!hadEvaluated || lastValue !== cachedValue` guard: all three are pure functions
of cachedValue, so a result that has not moved must not wake their consumers
either. That is a control row in the spec, not an implementation detail.
The gate measures a WIRE, not a getter. getOutput('asNumber').value calls the
getter directly and is correct at HEAD, defect and all — a spec written that way
passes against the broken runtime and grades nothing. Every row drives a real
graph through the corpus harness with a sink recording what it received. Built
as the reverted arm first: at HEAD the four typed-output rows are red and the
`result` control is green.
Full noodl-runtime suite after the fix: 2662 passed, 0 failed, 154 suites.
Two corrections to the register, made rather than left: the row's `NaN` is stale
(the getter now ends `Number(val) || 0`, so it reads 0 — equally dead, equally
invisible in a dimension), and the code export never shared the defect —
nodegx-export lists all six in EXPRESSION_VALUE_OUTPUTS, so an exported app
would have animated the creature the editor's own runtime could not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
setRGB was three blind parseInt(hex.substring(i, i+2), 16) calls. Handed
var(--primary) it read "ar", "--" and "pr" — all NaN — and rgbToHex stringified
them. No warning, no fallback, no console error. Since every project the
authoring tools build styles on design tokens, the node was unusable in all of
them: the corpus idiom for a smooth interaction is
Switch → Animate To Value → Color Blend → backgroundColor, and spine lesson 3
animates opacity instead because of this.
parseColor now reads #RGB, #RRGGBB, #RRGGBBAA, rgb()/rgba() and var(--token),
the last resolved against getComputedStyle(document.documentElement) — the only
honest source, since a token can be redefined per theme, per component or per
media query and only the browser knows which definition won. Token-to-token is
followed, bounded at 8 so a self-referential definition cannot hang a render;
var(--x, #fff) uses the author's own fallback the way CSS would.
A colour it still cannot read no longer produces a colour at all: it shows the
nearest authored endpoint verbatim and raises color-blend/unreadable-color,
deduped per distinct value. The dedupe is load-bearing — this node re-blends on
every frame an animation touches it.
Three-digit hex was broken in the same function and no row had recorded it:
#abc read "ab", "c" and "", so the blue channel alone came back NaN — wrong
rather than absent, the harder kind to see.
The `result` port description used to document the defect ("the inputs must be
6-digit hex, since any other notation yields nonsense") and now describes what
the node does.
12 rows incl. three controls; the reverted arm reddens 8 and leaves the controls
green. Full noodl-viewer-react suite: 1283 passed, 0 failed, 97 suites.
Registers E5, owner P18: nodegx-export's emitted blendColor is "a transcription
of colorblend.ts, holes and all" and carries the same blind parse. Both copies
were wrong and agreed before today; they now disagree, so exported apps still
paint #NaNNaNNaN. NOT fixed here on purpose — a peer was editing
nodegx-export/src/emit/ throughout this session and the emitted-code goldens
would have moved under them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… space The port's own `description` was exact — "Hides the element while keeping the space it occupies in the layout" — but the SUMMARY an authoring tool sees first read "Boolean level; hides/shows without removing layout logic elsewhere", which sounds like a note about side effects rather than "this element keeps its box". Lesson 6's first design was two Texts sharing a slot, one hidden; it leaves a permanent blank line and was abandoned only because someone read the long description before building. Rendered instead of read, it would have looked like a spacing bug rather than a wrong choice of port. Fixed in docs/node-catalog/enrichment/text.json — the single authored source; the string occurred exactly once across all 176 enrichment files. Also regenerates the two committed catalogs, which the E2 commit earlier in this session left stale: changing Color Blend's `result` port description changes node-catalog.json and node-catalog-enriched.json, both of which have CI gates (catalog:check, catalog:merge:check), and nothing said so at the time. Measured rather than assumed — both catalogs were backed up, regenerated and diffed against the committed copies. The combined diff is exactly four lines (the Color Blend description in both, this summary in the enriched one), so no unrelated drift was swept in. Both gates now report up to date. A port description is not a comment: it is an input to two committed artefacts and two CI gates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine rows closed across five commits (J1a/b/c, J4, D2, J3, G1, E2, H4), two narrowed rather than closed (E4/H3 — their editor half rode in on D2, but the curriculum entry lives in the other repo). The first job named for the next session is E5, which this session CAUSED: nodegx-export's emitted blendColor is a transcription of colorblend.ts "holes and all", so fixing the runtime made the two copies disagree and exported apps still paint #NaNNaNNaN. Left deliberately — a peer was editing that package's emit/ directory throughout the session. Also records the caveat that the editor's test:ci (webpack+Electron) was never run, because a peer held the box; the eight renderMarkdown assertions that live there are restated in the cheap runner and pass, which is not the same thing. NOTE: this file also carries session 8's own uncommitted edit — my section is appended below theirs, and their text is left intact. Their four modified SYL-* files and the untracked drive write-up are deliberately NOT in this commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rd the report The E2 commit fixed Color Blend in the runtime and not in nodegx-export's emitted copy, so exported apps still painted #NaNNaNNaN where a token was blended. utilLib.ts now emits a readColor alongside blendColor — the same notations, var() resolved against the document the same way. An exported app has no error bus, so the fallback to the nearest authored endpoint is the whole of the behaviour there and the doc comment says so. The package was re-checked before starting rather than assumed: the P18 peer had committed their session-88 hand-off and git status over packages/nodegx-export was clean, so the collision risk the hand-off warned about had gone. WHAT THIS EXPOSED, which matters more than E5: The E2 commit left nodegx-export's suite RED for forty minutes and I did not know. small-utilities.test.ts §A does not compare the emitted helper against a description of the runtime — it loads colorblend.ts FROM SOURCE and runs it. It is a real parity gate and it failed the moment the runtime changed. I never ran that package, having reasoned that Color Blend lives in noodl-viewer-react and run noodl-viewer-react. A node's blast radius is not its package. Two other packages compile or execute colorblend.ts — nodegx-export grades against it, and the catalogs are generated from its port descriptions — and one commit broke both, in different ways, neither visible from the directory the file sits in. The failure was also briefly worse than the defect: the suite died on `this.raiseRuntimeError is not a function`, because the node is loaded as a bare definition object with no Node.prototype under it, so the new report THREW where the old code merely returned nonsense. Now guarded — reporting must never be the thing that throws. That guard exists only because a suite I had not thought to run was watching. tests/small-utilities 20/20; full nodegx-export 2780/2780 across 76 suites; noodl-viewer-react 1283/1283. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… job E5 was raised and closed in the same session once the P18 peer committed their hand-off and nodegx-export went clean, so the hand-off's first job moves to J2. Keeps the part that outlives the row: a node's blast radius is not its package. Editing colorblend.ts broke nodegx-export (which loads it from source as a parity oracle) and both generated catalogs (built from its port descriptions), neither visible from the directory the file sits in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…at the drive measured Missed by the previous commit's pathspec: an untracked file is not picked up by `git commit <paths>`, only by `git add` first. Same trap this repo has filed before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…'s code executed for the first time 94 operators in Bubble's own words at docs-site/docs/coming-from-bubble.md, generated from scripts/bubble-phrasebook/rows.js. 32 rows were blank in both answer columns and none is now: 89 have an answer and 5 are refused in plain words with what to do instead, because a silently omitted row and an unanswerable one look identical to a reader. AC4 asked that every code cell be executed rather than read, and that is the whole task. All 77 run under `npm run docs:bubble:check`, in the product's own two sandboxes rather than an invented one — Function cells through `new AsyncFunction(Inputs, Outputs, Noodl, Component, prefix + body)` with the prefix read out of javascriptnodeparser.js, Expression cells through the real preamble, with a guard that fails if the runtime's pre-defined names ever stop matching this file's. Running the community table found 18 demonstrably wrong rows, in three kinds the page keeps apart because they are not the same finding. Eleven return the wrong answer when executed: JSON-safe wraps stringify in quotes a second time, "+2 days" returns 2025-09-08 after adding six units to a Date it mutates, "extract month" calls getDay() and answers 4, ":unique elements" dedupes colour strings by item.id and returns one element where the row says ten, two assign no Outputs at all. Three have a wrong stated ANSWER with fine code. Two claim an answer they never shipped — one code cell is the literal text "See above", and ":group by" names a Function node with an empty code cell, so it never counted among the 32 blanks and a reader scanning the node column sees a row that was answered. The accusations are executed too. Each of the eleven carries the original code verbatim and the checker runs it: a row accusing the table of being broken fails the build if the old code turns out to work. It fired on two, and both were demoted from "broken" to "right answer, wrong reason" — the count is 11 and not 13 because of it. 🔴 A product defect nothing else would have found: an Expression whose text ends in a `//` comment does not compile. expression.ts wraps the body as `return ( text );` on one line, so the comment swallows the closing `);` and it dies with `Unexpected token '}'`, naming nothing the author wrote. Three community rows are written that way, which means all three have provably never been run by anyone. Not owned here — nominated to the runtime, with no test covering it today. AC3's premise was wrong about where the Parse-era rows live: the dictionary has zero, and the three quoted strings are in Components.csv. Marked and re-tested in corpus/components/PARSE-ERA-ROWS.md — and "Parse backend" is not a death sentence, since parse.ts is kept by decision with cells probed against a live parse-server 7.3.0. Also regenerates docs-site/docs/nodes: COM-001's examples now reach the published node reference.⚠️ That tree was ALREADY stale before this session — P85 added comp-variant-badge-states in b438e8c, after the last regeneration in 2ae2379 — so this clears a backlog it did not create. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
COM-002 is 🟢 and hands COM-003 the 14 snippets and the Parse-era marker. Carries the Expression trailing-comment defect this phase found and does not own, and the s3 correction to the Directus ask: we already speak Directus at the backend-contract level, so the question to put to its author changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… a nav that scrolls, and a form that speaks before Send AC1-AC3. The shipped landing-page template held 0 States, 0 CSS Definition, 0 Expression and 0 popups across 375 nodes: three pages that rendered and did not respond to anything. This is the first three criteria of TPL-004. - App gains one `CSS Definition` (hover, press, photo zoom, a reduced-motion block). Deliberately NO `var(--shadow-*)`: this template sets all five shadow tokens to `none`, so a shadow hover would be a rule that does nothing. - `Site/ScrollTo` replaces `Scroll To Element`. The reason is a port count, not taste: a Group holds exactly ONE `scrollToElement.element`, which is why the header could offer one destination. Targets are class names now, so it offers three nav links per page. - `Site/IsValidEmail` + three Expressions + an `And` gate: Send is disabled and dimmed until the name, the address and twenty characters of message pass, the address box goes red with a reason, the message helper counts down, and the line under Send says what is still missing instead of nothing. 🔴 The new gate found two dead links on its first run. `flAbout` and `bzVisit` are hand-built bands, so `section()` never gave them a class — the freelancer nav's third link and the business hero's "Find us" both scrolled nowhere. That failure renders perfectly and reports nothing, which is why the gate asserts every target resolves to a class that exists rather than that a wire is present. The component count assertions now derive from `TPL003_COMPONENTS` instead of carrying the literal 21 — a number that has to be bumped grades whoever bumped it. Gate: 42/42 (was 40/40). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hes — one States node each AC5 (in part) and AC6 (in part). - `Site/FaqRow` was a question with its answer permanently under it, which is a wall of paragraphs with some words in bold. It is now a disclosure: the question and a chevron, the answer `mounted` on open. - `Site/ServiceCard` is new — the freelancer's three services were `Site/Feature` (a glyph, a title, nine words) and a service is the one thing on that page somebody actually has more to say about. It carries a price line and a detail that opens. `Site/Feature` is untouched and still serves the reasons and facts that genuinely are one line. - `Site/PhotoCard` gains the same disclosure, plus `card-lift photo-zoom` — the `card` composition already sets `clip: true`, which is what keeps the scaling photograph inside the rounded corners. - The launch page's plans switch between monthly and yearly from ONE `States` node carrying seven values: both prices, the line under the table and both pills' colours. Split into a Condition per colour and an Expression per price that is nine nodes that can disagree; here no arrangement can show a yearly price beside a lit "Monthly". 🔴 `mounted`, never `visible`, on every one of them: `visible` sets `visibility: hidden` and keeps the space, so four closed questions would be as tall as four open ones and the accordion would read as broken rather than closed. The door refused two things and both were real: `padding` is not a Group port (there are four), and `borderWidth`/`borderColor` are inert on a button whose `borderStyle` is `none` — so the period pills are built from their own parameters instead of spreading the outline composition. The marker gate now admits `Static Data` and `States` as word-bearing, with the reason written where the assertion is: a `States` node holding two prices is something a person must open to change what the page says, so it has to be marked and has to reach START-HERE.md. Gate: 42/42. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r and a story you can open AC4. Three hand-placed photo cards become a `Static Data` list of six, narrowed by a `Filter Collection` and drawn by a `For Each` — so the page has as many pieces of work as the list has rows, and adding a seventh is a row rather than a copied subtree and four rewired parameters. - `Site/FilterPill` writes an app-wide `workFilter` variable and lights from a `States` node driven by `currentState`, which the door's own States documentation describes as an enum input that picks a state in one wire. The `|| ''` on BOTH sides of the comparison is what lights "Everything" on first load, when nobody has written the variable yet — otherwise the page opens with no pill lit and reads as broken. - The filter is nodes, not `items.filter(...)`. `filterFilter` mints a connectable `filterFilterValue-category`, so which field the pills filter on stays on the canvas instead of inside a code editor. An empty value with op `regex` matches everything, which is the "Everything" pill for free. - `Site/WorkCard` owns its own `NavigationShowPopup`, fed from its OWN Component Inputs — not from repeater outputs. A signal is not a promise that the values beside it have arrived, and this design has no ordering to be wrong about. - `Site/CaseStudy` is the popup: brief, what was done, what happened. 🔴 Two defects designed out rather than shipped and found later: - The nine popup parameters are `popupParam-<name>`. The node says its inputs "mirror the component inputs of the target popup", the write gate accepts the bare name in silence, and nine undefined values render perfectly. - `clickBubbling: 'never'` on the panel. The backdrop closes on a click and the panel is inside it, so without this the popup shuts the moment somebody clicks the text they opened it to read. `auto` does not save it — nothing on the panel is itself wired. The door refused an absolutely-positioned close button (`unsized-absolute-box`), and the float-preserving fix is an icon-only button — which this Button node has no way to label, so a screen reader would announce nothing. It is in flow and it says "Close". 🔴 The photograph gate had gone blind and its own floor is what caught it: it read parameter values, and six photographs moved inside a Static Data JSON string where it could not see them. The collector now reads inside the JSON; the floor is a control on the collector, not a target. Gate: 49/49 (was 42/42). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sh, and a browser found what four readers could not `nodegx deploy` copied whatever build sat in `src/external/deploy/` and said nothing about which one. On any checkout where `npm run dev` has run that is the DEVELOPMENT bundle: 14.95 MB, of which 9.88 MB is an inline source map of the NodeGX viewer's own source, welded on as a trailing comment. The run reported 39 entries and exit 0, and a contributor who built in the morning published that source in the afternoon. The engine now reads the artefact — the trailing `//# sourceMappingURL=data:` and its size, the mean bytes per line, a `.LICENSE.txt` sibling — and refuses a development build with its own exit code (11) BEFORE a byte is written, naming the source map rather than the size. `--allow-development-engine` ships it knowingly, and every run, refused or not, prints which engine it picked up. Three readings, and they do not weigh the same: - The production bundle contains `sourceMappingURL=data:` once, inside css-loader's runtime, which BUILDS such a URL at run time from a `/*#` prefix. A substring count refuses every clean production deploy there is. - "Minified means one line" is already wrong about the file it would be written for: webpack's production build emits a license banner, so the real artefact is two lines. A mean does not care how many banner lines there are. - `licenseSibling` votes on nothing, in either direction. It is a real signal of the minifier having run AND a file that survives beside a bundle a later dev build overwrote — this checkout was in exactly that state. The armed loser is the map stripped off: `node --check` passes, 9.4 MB smaller, no `sourceMappingURL` anywhere, and it is still the unminified development build. So production is the conjunction, and a real deploy runs against it in a temp checkout. ## Inter shipped twice, as TTF, and the woff2 objection was answerable POL-006 refused woff2 because "there is no woff2 encoder in this repo, and a font nobody can regenerate is worse than a larger one" — a fair rule, answered by writing the encoder rather than arguing with it. `scripts/library/ttf-to-woff2.js`: zlib only, null transform, it rewrites a container and never touches a glyph. The four faces every project and every deployed app carries go from 1,256,396 B to 488,132 B. 🔴 A browser found two defects that four readers could not. The encoder's own check passed; fontTools opened the file and reported every table byte-identical to the source, 2,547 glyphs, 2,505 cmap entries; and Chrome refused it with one sentence and no detail. The WOFF2 table directory must be sorted by tag, and a WOFF2 file must be a multiple of four bytes long — and of Inter's four faces, Regular and Medium came out 4-aligned BY LUCK and loaded perfectly while SemiBold and Bold were invisible on every page. A gate written against "the font" would have been green with three quarters of the encoder's output unusable. ## The duplicate, and how far the rule is allowed to reach A file is left out when another file the deploy ships is byte-for-byte identical, one of the two is under `noodl_modules/`, and the project's source text never mentions it — reported like every other exclusion, with the survivor named. The `noodl_modules/` half was learned by breaking a shipped spec. The rule first deduplicated any two identical files, and DEP-008's fixture holds `assets/logo.png` and `pre.gitlab-assets/logo.png` with the same three bytes: two copies of a user's own picture, in two folders they chose. It dropped one. `nodegx deploy`'s contract is that everything ships unless a named rule excludes it, and somebody with two copies of their logo has two URLs this deploy cannot read.⚠️ A duplicate the project REFERS to still ships, and the run says so by name. Dropping it needs the reference repointed, and those live in node parameters, Static Data JSON, script strings and the project's own stylesheets — a rewrite that reaches four of five leaves a stranger's website with a missing typeface and a clean report. Gates: 8 + 5 in noodl-preview, 12 in nodegx-export, 17 in the editor's tests-unit, 5 in the jasmine suite. `test:ci` 2983 specs / 4 failures — the AIX-006 floor, unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AC5 completed, AC7. - `Site/QuoteCarousel`: both pages ended on two quotes side by side, which is the most a row holds and exactly the number that reads as "we could only find two". One at a time now, from a list of four on each page, with Back and Next disabled at the ends and an n-of-m readout. 🔴 There is no "item at index N" node. `Filter Collection`'s `filterEnableLimit` mints `filterSkip`, so a `Counter` driving it with `filterLimit: 1` is a one-item repeater that steps.⚠️ `count` is an input rather than `(items || []).length` in an Expression: `parsePorts` is a text scan that would mint a second port called `length` and then wait for a value nobody sends it. - START-HERE.md gains "The parts of it that move" — six paragraphs on the nav, the disclosures, the work list, the carousel, the price toggle and the form, each naming the node to open. It also warns that a pill's value must match a category string exactly, which is the one pairing in this template a person can break without seeing anything break. 🔴 A marked `Static Data` node ended the note's table at the first newline and printed its JSON as loose text, exactly where the note starts describing the biggest thing a person edits. A list is now summarised by what it is — how many rows, and what a row carries — because the rows are in the node and the note's job is to send somebody to it. Gate: 50/50 — including a new one that follows `items` forward across a component boundary and asserts every repeated component is fed rows carrying every field it reads. A missing field renders clean, on every row, forever. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ol that says what it is not evidence of 28 components (was 21), 494 nodes, 0 validator errors, 0 door warnings, gate 50 (was 40). All three pages render clean at 1280x900 and 390x844: 0 broken images, 0 placeholder texts, 3 pages routed, every new mechanism present and publishing. 🔴 The render path stamps `0 shipped defaults + 34 project override(s)` — the ~200 product default tokens are absent, so --text-lg, --space-6 and --gradient-deep resolve to nothing and every text renders at 16px/400. Before attributing that to this task, the control: the template at 192cf8d^ rendered through the same instrument in the same directory measures identically flat. The exporter merges the defaults (parseProject.ts:effectiveTokens carries the comment recording the day it did not), so a deployed page is unaffected. Nothing in that render is a statement about how the template looks, and it is written into the task file that way. AC8 — the click-drive — is NOT met and is the only criterion left. A clean render is not it: nobody has pressed anything. Three findings registered rather than chased, including that Landing page test V2's own popup closes when you click inside it (clickBubbling defaults to auto and nothing on its panel is wired), which is what put `clickBubbling: 'never'` on ours. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he 44 the write-up guessed The figure was typed from memory of an earlier measurement and the file was committed before the correction landed. Counted from landing-pages.content.json: 34 nodes, 20 distinct class names, 6 of them interaction classes and 14 scroll targets. The AC's own '≥ 20 nodes' threshold is replaced by the measurement, since a floor nobody has read against is a number that grades nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…se board carries the row The description a person reads before picking the template described three documents; it now says the pages are not flyers and names what moves. The template's version moves to 1.1.0 — one that never moves is a claim that nothing did. Phase 78's scope table gains TPL-004 at 🟡: AC1-AC7 built and gated, AC8 (the click-drive) open and named as the one a render cannot meet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… already prepared TPL-004 shipped AC1-AC7; AC8 is the click-drive and it is the only criterion a render cannot meet. The handoff names the prepared copy, the exact list of things to press, the render finding that must NOT be read as a verdict on the look, and the five noodl-mcp suites that were already red at HEAD and import nothing this task touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e gate that refused seven learns one thing it was missing COM-003, 5 of 5 ACs. The gate went 5/12 → 12/12 without being loosened once. AC1 — 101/101 (89 + 12), exit 0. 31 citations across 21 enrichment files; the 21 regenerated node pages are exactly the 21 cited from. AC2 — 33 raw-spacing-literal → 0. 32 tokenised, and the 33rd DROPPED instead because it sat on a port the runtime never reads. The scale is parsed out of parameterValues.ts at run time rather than copied: that file's own header says a second copy of a palette drifts silently, and a third copy in a Python script would be graded by nothing. One literal (1200px) is off the scale and is left raw and reported on every run — rounding is the only change here that would alter what a graph renders. Inert parameters are dropped BEFORE tokenising, or a port nothing consults gets a tidy var(--space-5) that reads as on-system. AC3 — `requiresModules` joins the example format, checked and never believed: the module must ship, its library.json must say type "module", and its own index.js must name the type. Four mutations each take 12/12 → 11/12. The `unknown-type-check-skipped` notices are deliberately LEFT IN — the declaration proves the type exists and says nothing about its ports. The same question was being asked by a second pipeline (catalog:merge refused the identical three examples from a different file), so the rule lives once in moduleNodeTypes.js and both gates read it. AC4 — re-measured as the AC instructed, and it inverted the task file's headline. `Open File Picker.success` was a REAL port from the initial commit until a139a3c (erg-001 §4, 2026-08-02) renamed it to `done`. The community graph was correct when written; our own rename six weeks ago made it wrong, and it shipped no alias, no migration and no warning — so every user project that wired it (or Send Event.sent, Unique Id.generated, Send Email.sent/failed, all moved in that one commit) holds a dead wire and is told nothing. Filed as D1, owner the runtime. AC5 — 14 snippets: 1 documented, 1 verified, 1 routed to COM-005, 11 discarded with a reason. The one that earned an artefact named a real, undocumented, DYNAMIC port: For Each's inputMappingScript now has an enrichment note. The snippet itself does not compile (one missing comma) — found by executing it, and the same run cleared it of a second accusation that was unfounded.⚠️ An example's description is PUBLISHED onto docs-site/docs/nodes/**. Three were written for a corpus reader and shipped referencing "the corpus" and "COM-004" before a grep caught them. title/description are also required by merge.js, so the export's truncated directory name would have become the public title. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s 13 meta-tag ports COM-004 §2 measured "catalog types matching seo, or display name containing meta = 0" and concluded there is no way to set meta tags without JavaScript. True count, wrong population: the authoring surface is a PORT GROUP on the Page node, named for the page it belongs to rather than the tags it sets. Page.tsx:32 declares META_TAGS; page.ts:208 folds all 13 into inputProps with propPath 'metatags'. All 13 are in node-catalog.json. So 11 of the community graph's 13 fields already ship as parameters. What is actually missing is smaller and sharper than a new node: og:type and twitter:card are plain strings where the community constrained them with States (AC4 unmet), og:image:width/height must be filled by hand where the community computed them (AC5), og:url has no location.href default, and the group is still labelled "Experimental SEO" for two of the thirteen. AC3 is also in better shape than feared and its dependency is now named: Page.tsx applies tags in the render body on the SERVER (effects never run under renderToString) so injectSeo can build the served <head>; whether a crawler sees them depends on the project being built SSR/SSG, which is P16 RUN-002. 🔴 Next session must not build a node or prefab before reading §7 — either would duplicate 11 ports that already ship. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s task did not need An agent about to apply a plan can ask the running editor whether a person has the project open and is mid-edit, and say so instead of racing them. #41's feature half. 🔴 THREE of the task file's own claims were wrong, and the biggest would have had this task edit the relay. "The routing is wrong for this use" is true of BROADCAST and irrelevant: `target` routing matches on clientId, ignores peer type entirely, and HLS-009 has carried an agent→editor command over exactly that branch since it shipped. The task file never mentions HLS-009. Following it would have meant changing the fan-out rule nodegx-observe rides on — the change the task's own trap warns about — to reach a door that was already open. relay-server.js is untouched, which is what makes AC5 true by construction. §3's "register as a viewer-typed client" was wrong too: a viewer peer appears in the editor as a preview arriving and leaving, dropping its export cache on the way out. `service` is the type, and openInEditor.ts already says why. Fifth task in five in this phase whose own file was wrong in more than one place. The property the design is built around: it must never lie about a person. The dangerous answer is not "I don't know", it is a confident editorAttached:false from a transport that failed. So editorAttached is boolean|null and `false` is reachable from exactly one place — nothing listening on the relay port, conclusive because the relay runs inside the editor's own process. A stale token (the editor never unlinks it on quit) against a LIVE relay reads unknown, not "nobody home". unsavedComponents is null, never [], whenever the baselines cannot be trusted. Editor: models/sessionStatus/{collect,index}.ts, one inbound ViewerConnection branch and sendSessionStatusResult, hasPendingProjectSave(), baselineProjectDirectory, installed once per window beside HLS-009's. MCP: tools/sessionStatus.ts, appended to the deferred `project` group. Token budget measured on BOTH arms, not reasoned about: 8,275/8,280 with the tool and 8,275 without it — 0 resident tokens, against 5 of headroom. (Memory's remembered 8,272/8 had already moved; a remembered baseline would have reported a 3-token cost that is not there.) Gates: 5/5 ACs. Driven in the real editor — attached, /App, unsavedBuffers true→false naming Site/FaqRow, with a no-edit negative control reading false→false, and the label verified on disk. 13 spec arms against the real relay + 11 editor-side, 7 mutants armed and all 7 caught. typecheck editor 0, noodl-mcp src 0. noodl-mcp suite 109/114 suites — the 5 red are 2 known drive reds plus 3 proved pre-existing against THIS change (reverted to HEAD, identical 3 failures, restored md5-identical). #41 replied; it STAYS OPEN — of its three asks, one ships, the advisory lock is refused with the measurement (R6), and the reverse save channel is not built. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…h it rather than build a second one 5 of 5 ACs. Richard's AC1 call, taken against the corrected premise: neither a node nor a prefab. 11 of the community graph's 13 fields already ship as Page node parameters, so either would duplicate them and leave two writers for the same <head>. AC2/AC3 — the chain from Page parameters to the served <head> had never been tested; each piece had a test and the seam had none. The new test runs parameters -> Page.tsx render -> Noodl.SEO.setMeta -> injectSeo -> HTML with testEnvironment 'node', so `document` is genuinely undefined — the SSR condition itself. It asserts Open Graph is emitted on `property` (a name-only tag is ignored by Facebook's scraper), that a blank tag is not emitted as content="", that values are escaped, and that the per-process SEO buffer does not bleed one page's preview onto the next. Armed: flipping Page.tsx's server branch to `if (false)` — the exact regression nda-012 warns about — turns 5 of 6 red, while ssr-inject-seo.test.js stays green through the same mutation, which is why this test had to exist.⚠️ It grades the chain, not the deployment choice: a client-only deploy still shows a crawler nothing. That is P16 RUN-002, now named. AC4 — og:type and twitter:card were plain strings; they are now enums, values taken verbatim from the community's own States nodes. Control pair, both arms: before, og:type "artical" was ACCEPTED by every gate we have; after, it is rejected and "article" still passes. Deliberately not allowEditOnly — a CMS drives og:type from data. The spec pins the sets verbatim rather than by count, and asserts the counterpart that only these two are constrained (og:title is prose). AC5 — the automatic image dimensions are LEFT OUT, which is the AC's own second branch, and the reason is AC3 wearing a different hat: the community computes them by loading the image, so the value can never be in the render-time buffer injectSeo reads. It would work in the editor preview and show a crawler nothing. The lead for closing it properly is recorded: Noodl.Env['BaseUrl'] plus the Page node's own getUrlPath() are both render-time on either path. "Experimental SEO" revisited and KEPT: the popouts declare it as parentGroup, so it labels the whole surface rather than two ports, and the qualifier is honest while the crawler path depends on RUN-002. viewer-react 107 suites / 1410 tests (was 105 / 1399). catalog:check, catalog:merge:check, docs:nodes:check, catalog:examples, catalog:tokens, typecheck:viewer all exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… one COM-003 and COM-004 both closed 5/5. The handoff carries COM-005 two things: the masonry CSS routed to it by COM-003 AC5 (⚠️ it is flex-wrap, NOT masonry — no column balancing, and AC4 already says "under an honest name"), and both recorders already landed as gated examples, which is AC5's raw material. 🔴 It also carries the warning COM-004 earned the hard way: measure library/modules/web-camera BEFORE deciding AC1. COM-004 asserted a gap without checking the surface that already existed and lost its entire premise. Traps recorded: an example description is PUBLISHED onto docs-site; catalog: examples green is not the pipeline (.filter(Boolean) silently skips an example the enriched catalog lacks); the same question gets asked by two pipelines; a JSON round-trip reformats a shared file (182 insertions vs 28 for the same 31 citations). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ere red before this session
`45929d94c` (P85 CMP-004/CMP-005) landed format-date, format-full-name and
sanitise-email without an `icon`, which is a HARD FAIL in `library:verify-dist`
— *"ModuleCard destructures it unguarded"* — and left `library:icons:check` red
at 75/78. Both gates were already red at HEAD; this is not COM-005's doing and
is committed on its own so it is not smuggled into that task's diff.
`npm run library:icons` (the repo's own `--all-missing` generator) produces the
three monograms; the matching `"icon": "icon.png"` line is what the generator
writes beside each.
library:icons:check 75/78 → 78/78, exit 0
library:verify-dist 3 prefab FAILs → 0, exit 0 (after `library:build`)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… two defects only a running browser showed
The last task in phase 86. AC1 and AC4 were both decided by MEASURING FIRST, which
is what COM-004 failed to do, and in both cases the measurement changed the answer.
**AC1 — one module, beside `web-camera`, not extending it.** Five facts from the
artefact: it is a vendored MINIFIED 1.0.4 bundle with no source here; it requests
VIDEO ONLY (the string `audio` does not occur in it), so it cannot source half the
task; its failure path is a `console.log` with no port on it, so AC3 is unreachable
through it; it has NO unmount teardown; and the two community recorders are the
same script twice, differing only in `{audio:true}` vs `{audio:true,video:true}`.
The shelf stays coherent because the overlap is REMOVED, not documented away:
`Record Media` takes an optional `Media Stream` of the same port type `Web Camera`
outputs, records that one without a second prompt, and never stops tracks it did
not open.
**AC4 — no masonry, no new entry, and no `card-grid` variant either.** All 830
characters were checked declaration by declaration: every one is an existing
`Group` port or a no-op (`break-inside: avoid` does nothing to a flex item, and
`min-width: 0` is inert beside `flex-shrink: 0`). ⚠️ And the idiom ALREADY SHIPS —
`prefabs/tags` is `flexDirection: row` + `flexWrap: wrap` with items at
`sizeMode: contentWidth`, which `layout.ts:82` renders as exactly `flex: 0 0 auto`.
So this ships a worked example under an honest name and nothing else. There is no
column balancing anywhere in that CSS; "masonry" would have been a lie.
**AC2/AC3 — driven, both modes, 34 checks, exit 0.** Audio 11,567 bytes of
`audio/webm`, video 93,970 bytes of `video/webm`, each played back from the blob
URL it published. Permission denied, device busy and navigate-away each reach
their own port, with 0 live tracks after every one — graded on
`MediaStreamTrack.readyState`, which is the thing the camera indicator reflects,
not on the source.
🔴 **Two defects the drive found and reading could not.**
1. `Failure` fired AFTER the specific signal and overwrote it. The demo wires both
— the obvious wiring — so a refused prompt reported "Failed" and the `Denied`
branch was dead on arrival. The general signal now fires first.
2. **A hidden Group is not an unmounted node.** The teardown arm first used
`mounted: false` and measured a MICROPHONE STILL LIVE after the component had
vanished. Correctly: hiding a group leaves the component's non-visual nodes
alive and recording. Only a route change deletes the node scope. The drive
harness gained an `/Away` page so any drive can measure this properly.
⚠️ And the instrument lied first: a fixed 1.6s sleep read the status BEFORE the
fake device handed over its track and graded nine later arms one step out of
phase — not one of those nine failures was about the recorder.
🔴 **D5 filed, unowned:** the SDK shim's own `methods.onNodeDeleted` THROWS at
unmount — it calls `.value` on what `prototypeExtensions` stores as a bare
function, and `nodedefinition.ts` deliberately stopped rewriting that object in
place. Nothing in the repo uses the hook, which is why it has never been seen.
This module declares `_onNodeDeleted` directly, the path every built-in uses.
⚠️ NOT driven: the cloud round trip. `Noodl.Files.upload` needs a live backend,
and the `file-upload` drive declines the same thing for the same reason. What is
driven is the blob's bytes and content type — the `File` that `Upload File` takes.
Shared-file changes are additive and backwards compatible: `withRenderedPage`
gains `chromeArgs` (the fake media device is what needed it), and the drive
harness resolves a `modules/…` slug, accepts `subjectParent`, and ships `/Away`.
library:check 78/78 clean, exit 0
library:drive 7 drives, 0 failing checks, exit 0
library:verify-dist 0 problems, exit 0
catalog:examples 104/104 strict, exit 0
catalog:check · catalog:merge:check · docs:nodes:check · catalog:tokens · catalog:groups:check all exit 0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t does not own COM-005 was the last one. The README's status line and NEXT-SESSION-PROMPT are re-derived from the task files, and both name the three things that outlive this phase: Richard's Directus ask (COM-006 AC3), the five unowned defects (D1 is the one that matters), and the two library gates that were red at HEAD before this session touched anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y what git tracks `Lint` has been red for weeks and it was never ESLint — that passes with 3038 errors to spare. It was the tsfixme ratchet, on a single number that had drifted +237 TSFixme / +423 any / +4 @ts-nocheck since 2026-08-07. Measured before deciding anything: 503 of those 664 markers are in test files and 159 are in shipped source. One number meant the honest signal about the product was buried under drift nobody intended to pay down, so the gate could not be held and was being routed around instead of read. Two populations, two baselines. `src` keeps full teeth on a real and tractable number; `tests` gets its own, because a stub cast to the interface it stands in for is not the same debt as an untyped value riding into the product. Neither is an exemption — both may fall, never rise. The split is STRICTER in the case that matters. Armed three mutants, all caught, exit 1 each: 1. one `any` added to src -> "any rose by 1 in src", names the file 2. one `any` added to a spec -> "any rose by 1 in tests" 3. src +1 while tests -1 -> still fails, naming src (3) is the case the old gate got wrong: the counts netted to zero and it passed. Both mutated files restored md5-identical; clean tree exits 0. Counting is now restricted to files git tracks. CI counts a fresh checkout where everything is tracked; a working tree also carries untracked scratch work, so the two were measuring different populations. On this tree that was 12 files and 11 `any` in a peer's `scripts/tmp/` probes, which `--update` would have written into a baseline that names a commit those files were never part of. The warning about modified *tracked* files is unchanged and still fires. Baseline regenerated at 6288f05: src 1098, tests 641. Per PLAT-004's own instruction, this raise is deliberate and recorded rather than silent. Richard chose the split over a re-baseline of all 664 and over dropping tests from the population entirely, on the measurement above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…as hiding
The `Typecheck` gate has been red with exit 134 — 8 GB and 417 seconds, killed
before it reported a single error. It was read as a runner-size problem. It is
not: `src` alone typechecks in 1.7 seconds, and raising the heap to 8 GB locally
fails the same way.
The cause is `moduleResolution: "node"` meeting `@modelcontextprotocol/sdk`.
The specs reach it through `tests/helpers/site-drive.ts` -> `noodl-mcp/src/server`
(and `judge.ts`/`members-drive.ts` import `site-drive`, which is why removing any
one spec never helped — every subset still contained it). node10 resolution
ignores the SDK's `exports` map and the type graph it lands in instead explodes.
`noodl-mcp` typechecks that same server in 5.7s under `node16`.
Bisection found a single spec first and that was misleading: removing it left the
other 156 failing too. The shared helper was the answer, not the file the search
happened to stop on.
Overridden in `tsconfig.tests.json` and NOT in the base config, because
`jest.config.js` points ts-jest at `<rootDir>/tsconfig.json` — changing `module`
there changes how 157 specs are transformed at runtime. The tests config is read
by `--noEmit` and nothing else.
Also added the editor's `lib`/`paths`, because the specs pull editor source in
transitively and it is written against them. `DOM` is a type lib, not an
environment: `testEnvironment: node` is unchanged and the specs still have no DOM
globals.
The gate now runs in 4.6 seconds and reported 16 errors. All 16 fixed:
- sb016 `new ExecutionHistory(dataDir)` against a ZERO-ARGUMENT constructor. The
history was never opened at that path. Now `open(dataDir)`, as every other
spec does. 🔴 The one change here with runtime meaning — spec re-run, 30/30.
- sb016 `makeDataDir`/`tryStart` typed `Record<string, unknown>` while every arm
passes a `SecurityConfig`, which is an interface and so has no index signature.
Both now take the union they are actually given; several arms deliberately pass
a partial or malformed config and that is the point of them.
- ac2 four casts to `{ x: number }` / `{ y: number }` on a type that had neither.
The browser payload carries x/y exactly when `ok` is true; the type never said
so. Replaced with a discriminated union — all four casts deleted, not moved.
- sbr015 `readRun` promised `stored` and never returned it; both call sites
supply it themselves. Narrowed to `Omit<Arm, 'http' | 'body' | 'stored'>`.
- sb017 `flatten` already guarded with `roots || []`; the parameter type did not
admit `undefined`.
- helpers/http.ts Buffer is not in DOM's `BodyInit`; undici accepts it. Cast at
the boundary.
- noodl-runtime `eventsender.ts` `.get(ref)` after the line that sets it.
Every step of the `Typecheck` job now exits 0: typecheck, :editor-tests,
:backend-tests, :nodegx-core, :mcp.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Version bumped 0.2.2 -> 0.2.3 in packages/noodl-editor/package.json. The tag must match it prefixed with `v`, because electron-builder names the release from the package version rather than from the ref. 🔴 And the policy change Richard asked for. Every release so far was tagged on `cline-dev`: `v0.2.0` and `v0.2.2` are reachable only from that branch, and `main` has been 1920 commits behind since 2026-08-07. That was survivable while nothing pointed at `main`. It stopped being survivable once six public issue replies named PR #20 as the thing that puts the exporter there, and once the release notes started describing a tree only we could see. From 0.2.3: a release lands on `main` first and is tagged there. The measurement that makes this a real gate rather than a ceremony: `main` is protected with SIX required checks and `enforce_admins: true`, so a red one stops everybody including the repository owner. At the time of writing four of the six were red — Typecheck (OOM), Lint (the tsfixme ratchet) and Test (editor) — which is exactly the point. Two are fixed in the commits before this one; the editor suite is the one left. `main`'s tree is byte-identical to the merge base, so the merge itself is conflict-free: `git diff origin/cline-dev...origin/main` is empty and the only commit `main` carries alone is an empty merge commit. Also documented the escape hatch, because an undocumented exception is how the August `main` happened: a release that genuinely cannot wait for a green gate may still be tagged off `cline-dev`, but the release notes have to say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pushing the ratchet and OOM fixes let `Lint` and `Typecheck` reach steps that had not run in weeks. Both were red, and neither was caused by those fixes. **Lint — a hex literal in noodl-core-ui.** The colour ratchet never ran before, because the tsfixme ratchet exits first. `Community.module.scss` was carrying three colour values written as `var(--token, fallback)` against tokens that DO NOT EXIST: `--base-color-black` and `--base-color-white-transparent-80` are declared nowhere, so the fallback was the only value ever used and the `var()` was decoration. Introduced by `3f3753c63` (REL-019, 2026-09-06) — pre-existing, and invisible because the gate in front of it was already failing. Fixed at the source of truth rather than at the call site: both values are now real tokens in `custom-properties/colors.css` (which the ratchet excludes, deliberately — a token has to be spelled somewhere), named for what they are, and defined to the EXACT values the fallbacks used. `--base-color-ink: #111111` for a mark drawn on media rather than on a themed surface, beside the existing theme-invariant `--base-color-white`; `--base-color-white-transparent-85` on the existing transparency ladder, at 0.85 rather than being filed under an "80" that would have been a second lie. 🔴 Zero visual change — the point was to stop the literal, not to bend it until the regex stopped matching. **Typecheck — `@nodegx/export` does not resolve on a clean checkout.** With the OOM gone, `typecheck:backend-tests` ran for the first time and reported four TS2307s in `noodl-mcp/src`. It passed locally and failed on CI for one reason: `node16` resolution goes through the package's `exports`/`types`, which point into `dist/` — built on this machine, absent on a fresh runner. The old `node` resolution never got that far. Aliased to source in the tests config, mirroring the root tsconfig. Repeated rather than inherited because a `paths` block REPLACES the inherited one. Verified by resolution target, not by exit code: `--traceResolution` now reports `@nodegx/export` resolving to `packages/nodegx-export/src/index.ts`, which is what a runner with no `dist/` will do. Local: typecheck, :editor-tests, :backend-tests, :nodegx-core, tsfixme and colors all exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`tokens:css` is the fifth and last step of the `Lint` job, and the first time it
has been reached. Three custom properties were used and never defined anywhere:
--duration-100 TokenCategorySection.module.scss (2 sites)
--ease-out TokenCategorySection.module.scss (same 2 declarations)
--font-mono iconpicker.css
Same shape as the hex fix before it, and the gate's own wording is the reason it
matters: *a var() naming nothing renders its fallback silently — it looks like a
token and behaves like a hardcoded value that no theme can reach*. Two of these
had no fallback at all, so `transition: background-color var(--duration-100)
var(--ease-out)` parsed to an invalid value and the transition simply never ran,
on a panel whose entire subject is design tokens.
Repointed at the tokens that already exist rather than defining new ones — this
was three misspellings, not a gap in the system:
--duration-100 -> --transition-fast (100ms; the name was already the value)
--ease-out -> --transition-ease-out (cubic-bezier(0, 0, 0.2, 1))
--font-mono -> --font-family-code (ends in `monospace`, so the dropped
fallback changes nothing)
🔴 Read the job definition instead of discovering the next step from another CI
round-trip: `Lint` is `lint:ci`, `tsfixme`, `colors`, `icons:css`, `tokens:css`.
All five exit 0 locally. Four of them had never run in CI, because each one only
ever reported the step in front of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Test (editor)`'s whole floor was these four, and the cause is one word in a
fixture: the styled Group was given `color`. A Group is a container — text
colour belongs to Text — so `color` is not one of its input ports, and
`unknown-parameter` blocked the authored output. The loop then did the correct
thing with a wrong candidate: fed the rejection back, got the same candidate
again, and ran out of turns. The specs read `exhausted` (and `error` for the
stall one) where they expect `authored`.
🔴 The product is right and stays untouched. A parameter nothing reads should
not ship, and `isBlockingForAuthoredOutput` blocks it deliberately. `git diff`
here is one test file.
The replacement has to satisfy BOTH sides or the specs go green while measuring
nothing:
- a real `Group` port, or validation rejects it, and
- a member of `COLOR_PROPERTIES` (`StyleAnalyzerCore.ts`), or the style lint
never fires and the advisory turn these specs exist to test never happens.
Measured every candidate rather than picking one that looked right — `authored`
alone would have passed with the lint silent:
borderTopColor / borderRightColor / borderBottomColor / borderLeftColor
Group port ✅ COLOR_PROPERTIES ✅
outlineColor, shadowColor, caretColor COLOR_PROPERTIES ✅ Group port ❌
boxShadowColor Group port ✅ COLOR_PROPERTIES ❌
`boxShadowColor` is the trap: it is accepted, every spec reaches `authored`, and
the advisory never fires — so the two specs that count turns would have gone
green having stopped testing their own subject. ⚠️ Note the near-miss pair,
`boxShadowColor` (the port) against `shadowColor` (the property); they are not
the same name and only one of them is in each list.
`borderTopColor` it is, at the four submit sites and at the on-system resubmit on
what is now line 196 — that one still carried `color` too, so the resubmit was
rejected in its turn and the round count stayed wrong after the first fix.
⚠️ Deliberately NOT changed: the `filesFor(...)` case in the pure-lint spec keeps
`color`. That one never builds a session, so no port validation runs, and `color`
being counted as a colour property is exactly what it asserts.
Verified outside Electron, where these run in ~200ms instead of a 10-minute
suite: 3 of the 4 pass outright. The stall spec measures `authored` with
`turns: 2` — its own assertions — and then dies in the harness on
`StorageWeb.get → "Method not implemented"`, which is a plain-jest run having no
Electron platform storage. CI runs it under Electron.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ct, so the PR must be up to date No content: main's tree is identical to the merge base, and the only commit it holds alone is an empty merge commit from PR #19. This exists solely to satisfy 'Require branches to be up to date before merging' on main. Co-Authored-By: Claude Opus 5 (1M context) <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
This is the first sync of
mainsince 2026-08-07. It carries 1920 commits— everything from v0.1.3 up to and including the v0.2.3 preparation.
The PR was opened in August to land three packaging fixes and a README rewrite.
It has tracked
cline-devever since, so its contents are now very much largerthan its original description. That description is preserved at the bottom.
Why it matters that this lands
mainhas been 1920 commits behind, which means the branch a reader can clonehas not matched what the release notes describe. Several public issue replies
point at this PR as the thing that puts the code export work on
main. Mergingit is what makes those replies true.
What it carries, in broad strokes
nodegx export/nodegx deploy,@nodegx/coreon npm, anda report naming every node that will not translate and why. Alpha.
no-backend landing-page set.
which is why The preview server listens on every interface with no authentication #31 stays open until a release carries it.
FUSE 2, the
--no-sandboxdefault, the no-DISPLAYabort, and an.rpmtarget.app.asar283,432,139 → 168,143,884 B (−40.7%).phases 73–86.
Merge policy
From v0.2.3, releases are cut from
mainrather than fromcline-dev— seedev-docs/guidelines/RELEASE-PROCESS.md§3.Tags
v0.2.0andv0.2.2are reachable only fromcline-dev; that is the thingbeing corrected.
Please merge, do not squash. Squashing 1920 commits discards the history that
is the actual record of this work.
Conflicts
None by construction.
main's tree is byte-identical to the merge base —git diff origin/cline-dev...origin/mainis empty, and the only commitmainholds alone is an empty merge commit.
Original August description
eslint-linter-browserify,globals,lodash— all the same webpack-externals/electron-builder-pruning gap across a workspace boundary).v0.1.3 is already built, signed, notarized, and published as the release — this PR just brings the commits that produced it onto
main.🤖 Generated with Claude Code