Sync main: packaging fixes (v0.1.1-v0.1.3) + README rewrite - #20
Open
richardosborne14 wants to merge 974 commits into
Open
Sync main: packaging fixes (v0.1.1-v0.1.3) + README rewrite#20richardosborne14 wants to merge 974 commits into
richardosborne14 wants to merge 974 commits into
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>
… never belonged to the client it was filed against s49 built, gated, drove and committed FIX-023 (551a944). Fifteen tasks closed. The headline is a correction to the task file: FIX-023 was scoped as an MCP-server defect, but isComponentRef is shared noodl-editor code and the repo's own `npm run validate:project` crashed identically. "The editor tolerates this node" was a fact about the editor's runtime graph, not about its validator — two code paths in one process, disagreeing about one file. Also recorded: the guard existed in two call sites and not one, with ~17 unguarded and seven of those in the editor's own rules; and the corpus is 56 project directories, not 28, whose legacy half reaches a second normalisation boundary the scoping never named. test:ci is still not taken, but for a new reason — this is the first P66 change in four sessions that the jasmine suite would actually have graded. Flagged rather than glossed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ved its blocker Both were recorded as blocked on a viewer build. A build carrying the fix had existed since 2026-08-16 21:57 — five minutes after the fix committed at 21:52 (2d8f8e0) — and three sessions inherited the blocker without checking either the artefact's mtime or its contents. - kit-app.editor-nodelibrary.json re-recorded from a running editor. Compared by NAME the capture is identical to its predecessor except for two values: demo.kit.Badge and demo.kit.Meter go module 'Unknown Module' -> 'Demo Kit'. Ports byte-identical, 177-name set unchanged. The ~400-line diff is pure reordering (kit entries now sort last); the JSON header says so, so the line count is not mistaken for a rewrite. - kitAgreement.test.ts asserts ['Demo Kit', 'Demo Kit'] on the editor route. The describe block is renamed off "the one thing the two routes do not agree about", and the fossil comment now records why it was a fossil rather than restating the defect as current. kitAgreement 9 passed; noodl-editor tests-unit/cn-003 22 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… that could not fail CN-018 CLOSED and CN-007's D8 obligation driven, on the first free 9222 in three sessions. - CN-018 AC4/AC5: three kits draw three separately-named picker headings; the two Stat Tile cards are distinguishable without data-test. The two modes carry the kit name on DIFFERENT elements — browsing on the heading, searching on the card's second line — and reading only browse-mode cards nearly produced a false defect report. AC5 measured, not argued: kit and built-in headings are identical in tag, class and every computed typographic property. - CN-007 D8: positive pills paint rgb(22,163,74) with no colour parameter set — not the old #1F8A4C, and not rgba(0,0,0,0), which is the exclusion that mattered. s14 removed the in-JSX || fallbacks on the reasoning that a declared default reaches props at initialize; had that been wrong, every pill, band and banner would have rendered blank. Both banner arms measured. - Observation O3 RETIRED as unmeasurable. A planted unresolvable token computes to rgba(0,0,0,0), never literal text, so "0 var( in computed colours" is equally true of a perfect page and a wholly broken one. Its liveness control did not catch this: a control has to be a known-broken input fed to the instrument itself, not a sign of life taken from nearby. - The mid-session pickup question, carried since s11, is answered: no watcher delivers a newly scaffolded kit (absent at every poll to t+30s); a viewer reload does. CN-014's spec hypothesis is false. - CN-008 has a false premise: the scaffold emits no docs string at all, so "the docs string authors already write" is not written on the path this phase sends authors down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nothing `AuthoringContextBuilder.nodeKitOverview()` and a `THIS PROJECT'S NODE KITS` block in the cache-stable half, immediately after the catalog, wired at both `AuthoringSession` call sites. Absent-means-omitted and charged, following `libraryOverview` exactly. 17 tests, all seven mutations killed. Two of the task's own clauses were false, measured before building: 1. "the AI does not know the lane exists" — it does. A kit node is `inNodePicker`, so `catalogOverview()` already named it and `nodeTypeDetails()` already rendered its full ports and defaults. What was missing is attribution and salience: it arrived buried in ~30 names on the `- Visual:` line with no kit named anywhere in the prompt. So the handout names ports rather than describing them. 2. "the ports an instance would actually set — the inputs with no default" selects ZERO ports on either kit in this phase, because an author defaults every port and D8 pushes them harder that way. Following it prints a node name with an empty list under it while `charge()` reports a cost and AC2/AC3/AC4 all pass. The discriminator is the runtime-added base set from `react-component-node.ts`, which takes `demo.kit.Badge` from 12 in / 9 out to the 8 and 1 its author declared. Not derived, because both derivations are wrong: intersecting the shipped catalog's visual nodes gives the empty set (`Component Children` has zero inputs), and intersecting the project's own kit nodes also swallows `radius`, which both authors meant. `docs` turned out to be one field over two vocabularies: on a shipped node it is a URL (158 of 175 carry one; 158 of 158 are docs.noodl.net, zero prose), on a kit node it is prose. Fixed a real defect on the way past — `renderNodeType` sourced its summary from `enrichedNode()`, which is keyed by type name and generated at repo-build time, so a kit type can never be in it; the author's `docs` was carried into the overlay by `@nodegx/kit-catalog` and then dropped, and every kit node reached the model with no statement of what it is for. Budget measured: 383 chars fixed + ~261/node; demo kit 934, cashflow-shaped ~2.2k, a 40-node kit capped at 30 gives 8,869 — ~7% of the 120k context budget, in the cached half, and none of CN-009's 8,280 bar. Overflow is stated, never silent. AC1 is the consequence and is NOT met: it needs a live model. Editor jest: 3555 passed / 229 suites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The last open FIX-016 build — the script-mode mining slice.
Two surfaces still mined `Inputs.`/`Outputs.` out of a Script node's text, and
the Script node mines nothing from its text: it is compiled
`Function('define','script','Node','Component', …)` and takes its ports from
`parser.getPorts()`. So the help bar told a correctly-written Script node to
"Read price with Inputs.price", and completion offered to INSERT that same
notation — the exact line s44's message 6 was underlining as throwing, in the
same popout. A completion arrives first and looks like knowledge.
The root cause was one predicate answering two questions. `modeHasDeclaredPorts`
is true for 'script' and correctly so: its scriptInputs/scriptOutputs proplists
are real. Consumers reached for it when they meant "is Inputs.x how ports are
written here". Adds `modeUsesPortNotation` (['function']) beside it and gates the
bar and both completion call sites on it; corrects the `unionPorts` module doc
that had named the wrong gate and so sent all three consumers wrong.
The tempting fix was one edit, not four: drop 'script' from
`modeHasDeclaredPorts`. That deletes message 6, which is gated on it. There is a
spec row and a mutant for exactly that.
The bar is silenced rather than fed an empty mined list — with nothing mined a
correct Script node falls to `unused-ports` and the bar nags the author who did
the right thing. A bar that teaches `define()` is deliberately not built: picking
a row needs the node's real port list, which means running the author's code.
Censused first, and the scoping was wrong in both directions: FUN-005's rail does
not exist (four doc comments, no component), FUN-004's diagnostics was already
fixed by s44, and FUN-008's completions — two call sites — were not mentioned.
Gates: noodl-core-ui jest 27 suites / 498 tests; root typecheck exit 0;
typecheck:core-ui 44 errors, unchanged, none on the changed path; lint:ci exit 0.
Spec: 21 tests, every behavioural row paired across the two modes. Three mutants:
bar gate removed 4 failed, completion gates reverted 5 failed, the "tidy" 2
failed including "message 6 still fires".
Driven 2 arms x 6 rows in a live editor on a copy of fix016-msg6-drive, whose
PriceDiscount carries both node types. The bar: 0 PortHintText on the Script node
across four documents, 1 on the Function node. Completions: 0 with no tooltip on
the Script node, `Inputs.price` with a rendered tooltip on the Function node.
`define` still completes in script mode with its info text — the row that shows
the suppression is of the notation, not of the feature. Message 6 still fires.
Two existing CN-019 rows in portBar.test.ts asserted the old behaviour and are
changed, not deleted: they used 'script' as a stand-in for "a mode with declared
ports".
The drive's own correction: the first completion probes used a bare prefix at
position 0, which is an isDeclarationPosition where ports are correctly refused
in BOTH modes. The Function control came back empty too, which is how it was
caught; re-run at an expression position the pair separates. A one-armed probe
would have recorded a pass that was true and proved nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… scoping wrong in both directions s50 closed FIX-016's last open build. The census found FUN-005's rail does not exist, FUN-004's diagnostics was already fixed by s44, and FUN-008's completions — two call sites, the half that mattered — were never named. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nd a query that found nothing
AC1–AC4 met, AC5's consequence needs a live model. `src/catalog.ts` only; 14 tests,
8/8 mutations killed, noodl-mcp jest 599 passed / 51 suites.
AC3 first, because the task says to measure before designing:
before: [surface] 8223 tokens / 20 resident tools — 57 under the 8280 budget
after: [surface] 8223 tokens / 20 resident tools — 57 under the 8280 budget
Unchanged because this spends nothing: no description, schema or group purpose
was touched — every fact added travels in a response, which the gate does not
measure. So there is no third renegotiation and the $ref move stays available.
Two criteria were already met, confirmed against the real cashflow project rather
than inherited: all five nodegx.cashflow.* types listed, and Pill returning
23 in / 14 out exactly. Provenance was the missing half — both projections
dropped providedBy/kitModule, so an agent could not tell an author's node from a
shipped one.
The finding is bigger than the task. `summary` was read from `enrichment`, which
is generated at repo-build time and keyed by type name — a kit type can never be
in it — so the author's `docs` sentence was carried faithfully into the overlay
by @nodegx/kit-catalog and then dropped. A kit node reached the model as a name,
a category and a port list, with nothing saying what it does.
It also cost the kit its only free-text handle. Measured on the cashflow kit:
query "draggable" [] -> nodegx.cashflow.Pill
query "snaps to whole days" [] -> nodegx.cashflow.Pill
query "pill" Pill -> Pill (matched the NAME)
That third row is why nobody noticed: every query you would try while already
knowing the node existed worked. Only discovery failed.
This is CN-008 finding 3 in the other consumer — two bugs, not one reachable
twice, since the editor reads the repo-build enrichedNode() table and this reads
the merged catalog document. Each side's comment now points at the other.
The gate on that fallback is load-bearing and its obvious test cannot fail:
`docs` is prose on a kit node and a URL on a shipped one (158/175, all
docs.noodl.net), but all 175 built-ins also have an enrichment.summary, so the
fallback is never reached for them gated or not. The control is therefore a
hand-built node shaped so it would fire, and it dies when the gate is removed.
Found by mutating: an empty `docs` string survived the first version, because
getNodeTypeDetail guards with `if (summary)` while listNodeTypes assigns it
unconditionally — so the row, not the detail, is where it breaks.
One scope reading, stated rather than taken quietly. "find_tools matches kit node
names" is a no-op as written: list_node_types and get_node_type are in `core` and
are resident, so there is no deferred tool to reveal. Built on
list_node_types({query}) instead — zero tokens, and kitTools.test.ts' deliberate
find_tools control is untouched and green.
AC5's caller is built and driven: a real session searches by subject, reads
provenance off the row, fetches ports, places the node and validates clean, with
a control that mistypes the port and gets `unknown-parameter … did you mean
value?` — the diagnostic, not just a refusal. But the choice to search for
"percentage" is mine, not a model's, so the consequence half is NOT met.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…u already knew the answer s17 built CN-009. AC1-AC4 met for zero tokens; AC5 needs a live model, and it is the same live-model drive CN-008 AC1 needs — one drive closes both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… azure Added out of Richard's read of the running site — "a bit simple and sad right now … I don't think it fits our sleek new NodeGX style in the editor." ## The diagnosis is not the complaint It is not a weak design. There is NO design system. globals.css declares SEVEN custom properties and all fourteen pages are built on them, while the editor ships a canonical, documented two-tier system whose own header calls itself "the only definition of colour tokens". Measured 08-17 from the three stylesheets: 7 tokens against the landing page's 24; 2 surface steps against the editor's 5; no mono face, no display face, no light-theme convention. 🔴 The detail that diagnoses it: the site's accent is #4b9fff and the editor's is #4da3ff. Four hex digits apart — close enough that someone was aiming at the editor, far enough that they were doing it by eye. Nothing was imported, so nothing stayed in sync. That makes "paste better values in" the one fix guaranteed to fail the same way, which is why the drift test is the deliverable rather than the paint. ## D18 — ruled azure, AGAINST the recommendation The pitch argued teal, reasoning that the landing page is what a stranger meets first and the community site sits directly behind it. Richard ruled azure: one hue across the two PRODUCTS, which is the pair a user moves between and the pair UNI-011 puts inside one window. Recorded with its consequence rather than as a bare verdict — the landing page becomes the odd one out, chosen and not inherited, so D18 is now the reason nodegx-web moves rather than the community site if anyone reopens it.⚠️ This register carries a standing note that "a recommendation adopted wholesale is weaker evidence of a good decision than one argued down". D18 is the first ruling in this phase that was argued down, and the file says so. ## The engineering question the task actually turns on Two repos, one token set, no build step between them and no registry. Copy (what produced #4b9fff), publish a package (real infrastructure), or vendor with a sync script and a DRIFT TEST. Chose the third: a vendored copy with no check is the first option wearing a hat; what makes it honest is that divergence becomes a failing test.⚠️ And it inherits a known hole from the pattern it borrows: the platform's schema drift spec compares tables and columns AND NOT enum names, so a half-done rename passed it. The token test must compare every declared property and its value — two files with the same number of different colours pass a count. ## Also recorded, and it is not cosmetic Deployment is owned by no task. Measured 08-17: nodegx-community has no Dockerfile, no CI workflow, no ops/, no fly.toml, and a grep across all twelve task files for deploying the platform returns zero hits. UNI-008 is about hosting users' apps, not this. Same shape as slice 1's finding — D14 called the API a deliverable, no task owned it, so it did not exist. The next-session prompt leads with an unrelated launcher tweak Richard asked to have done first, and flags that it is not phase 67 work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… 'learning'
Richard, using the app: "you should see your projects first when you open the
launcher, not the learning bit which takes up the whole top of it."
D5's Learning section moves out of the Projects view and into its own header
tab. The section itself is unchanged — D5 chose the visible option because
visible progress motivates, and a permanent tab is visible; what it does not
survive is being the launcher's opening screen.
The tab affordance already existed. HEADER_TABS, the accent underline, the
persisted active tab and the deep-link parser all shipped with PAR-001, so this
is one table entry plus a view, not phase 37's project tabs arriving early.
The trap is the naming. There were already two things called learning:
'learn' POL-002's retired catalogue of hosted lessons, whose content
predates every project-format change since LEARN-001. Kept
compiled, reachable from nothing.
'learning' UNI-007 / D5's installed lessons. The new tab.
Reusing 'learn' would have put the dead catalogue behind the live tab. They are
one letter apart, and usePersistentTab's isValidPageId is the single place a
*stored* string decides which page you land on — so the spec asserts both
directions, accepts 'learning' and still rejects 'learn'. A spec checking only
the first would pass just as happily with both accepted; the control run proves
it does not (1 of 3 red with 'learn' accepted).
Driven, and the consequence checked rather than the mechanism: "Recent
projects" is the first heading at y=81 with cards on screen, the learning
section selector is absent on Projects while firing on the Learning tab, the
tab persists across a reload.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r places Discharges FIX-004's redaction ruling (b), s42: a `noodl_log` block could print a provisioned secret to stdout in the clear, where the `Log` node could not. The ruling read as "make the generator emit something other than console.log". Rejected on population: `generatedCode` is a string saved in every existing project and every one already says `console.log`, so a new emission would fix programs saved from tomorrow and leave the ones on disk leaking. Instead the compiled function gets an 11th parameter named `console`, which shadows the global inside a `new Function` body — both populations, one edit, and the editor↔runtime contract untouched (the three specs pinning `console.log(…)` stay true rather than being rewritten). No sink means the REAL global console, by identity, not a forwarding shim — so the browser is the same call it always was, objects included. The parameter list turned out to have FOUR spellings. The census found one; the other three were found by `vfn-011/drift-gate.spec.ts` failing on arity, because it reads `compiled.length` off the runtime's own compile rather than off its source. The bench is a third executor of block programs and would have gone on running them against a ten-parameter contract. That gate had never fired before. Measured through a real BackendService over real HTTP: the row that recorded "a block-logged secret reaches stdout in the clear: true" now records `false`, with `toContain(REDACTED)` beside it so the absence has a firing signal — the secret being absent also passes when nothing was printed at all. The suite's own stated premise had to be repaired: "a console.log from generated code does not go through the logger" was true when written and false after this. Left alone, all four presence assertions failed for a reason unrelated to the block. Note the env var beats the configure option (ops/logger.ts:78), so NODEGX_LOG_LEVEL is deleted before the level is set. Four mutants, each red, each reporting a real test count: the parameter dropped (leak returns, by name, while the derived-value control still passes); `console` inserted before `__p` (the positional near-miss); the no-sink shim; and String() instead of JSON folding.⚠️ A block's console.log is now subject to the service log level, where before it always printed. Consistent with the Log node and the ruling; production default is info. Gates: noodl-runtime 137/2510, nodegx-backend 100/1085, test:main 229/230 (the one failure is the bld-004 load flake, 8/8 alone), viewer-react 71/910, cloud-runtime 7/172, typecheck exit 0, lint:ci 876 vs 3916. test:ci NOT taken: peers hold uncommitted noodl-core-ui edits, one saved 35s before the run would have started, and noodl-core-ui is bundled into the renderer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… a list that had four FIX-004's redaction (b) is closed, so FIX-004 has no open build left. Seventeen tasks now closed outright, one of them a peer's FIX-024 landed mid-session. Records three things for the next session. That the census read _compileFunction, found 'the' parameter list and was wrong by three — the other spellings live in evaluateFragment, in the editor's BenchRunner, and in the drift gate's own literal, and a gate in a package the change never touched is what said so. That the ruling named a mechanism which was the wrong half of the problem: changing the generator would have fixed programs saved from tomorrow and left every generatedCode string already on disk leaking. And that a suite's stated premise expired, so four presence assertions failed for a reason unrelated to the feature. Also marks the seven gates taken, the two exit codes read wrong before being read right, and why test:ci was declined — the process table said clear and the tree did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… half is a session's Rewrites the next-session prompt around a measurement rather than a lane list: of what is left in the phase, three items are buildable by a session and seven are blocked on a purchase, an account, an artwork or a decision. Six of the seven trace to four asks, and a forum gates three of them on its own. Re-measured rather than inherited: community.nodegx.io still resolves to nexus-1, has_pages is still false, the sibling checkout is clean at 7193f92. Two corrections to the last handover, both in the direction that misleads. It listed the deployment absences accurately and omitted that docker-compose.yml exists — it is dev/test Postgres only, and a reader checking for a compose file finds one and distrusts the whole list. And the platform does carry a lint script while carrying no lint config, so a reader who checks package.json concludes the handover was wrong about the gate. Records the launcher tweak (FIX-024, 7b7a778) as phase 66 work, and the unowned hole it exposed: noodl-core-ui cannot be eslinted at all, so "eslint clean on every touched file" was never true of a core-ui file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…skip
CN-010's AC2 and AC3. The spec's premise for AC3 was false three ways, and the
third was the defect: `get_node_type` read `dynamicPorts.note`, a field that
exists on no node in the product. The catalog's field is `description`, and it is
non-optional on both `DynamicPortInfo` (88 shipped types) and
`OverlayDynamicPortInfo` (every kit node) — so every dynamic type answered "this
port list is incomplete" with no statement of how. It typechecked because the MCP
server declared its own local shape for a document it does not own. The editor's
`CatalogIndex.dynamicPortNote()` reads `description` and was right all along.
That is the third such field in three consecutive tasks, after CN-008's `docs`
and CN-009's `summary`, and the common cause is not that kits were forgotten: a
second consumer re-declares the shape it reads and nothing types the seam.
AC3 (`noodl-mcp/src/catalog.ts`):
- `description` reaches full detail, for shipped types and kits alike
- `declaredPortGroups` too — 34 types, 165 groups, 158 conditions — projected
to {condition, inputs, outputs}, exporter internals dropped
- a kit's `hasDynamicPorts` flag finally carries a sentence; it never could,
since `runtimeBehavior` comes from enrichment, keyed by type name at
repo-build time (88/88 built-ins have one, 0 kits can, ever)
AC2 (`noodl-editor/.../validation/parameterValues.ts`): the runtime-dynamic
carve-out was a bare `continue` — 947 set parameters across 321 nodes of the 29
real test projects, 6.0% of every parameter in them, reported as a clean pass.
Now an `info` per node, reusing the `DynamicPortSkipped` code `nonexistentPort`
already emitted for connections; one of two sibling call sites had the notice.
Emitted unconditionally rather than behind `emitDynamicPortInfo`, because no
production caller can set that flag — justified by measurement instead (median 0,
p90 2, max 17 per component).
Four silence baselines were replaced rather than deleted, per CN-002's rule; each
now asserts the guarantee (not accused) instead of the shape (empty array).
The full-detail response ratchet moves 12,500 -> 13,500 with its arithmetic
recorded: 283,483 -> 294,894 tokens across 142 types (+4.0%), worst single
12,069 -> 12,899, headroom 431 -> 601. The ceiling case is no longer `Group`.
The cheaper-looking `activeWhen`-per-port encoding was measured and is not
cheaper (29,014 vs 22,911 bytes).
23 tests, 15/15 real mutations killed. noodl-mcp jest 613/52 suites, editor jest
3,567/231 suites, resident surface 8,223/8,280 unchanged, tsc 8 pre-existing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t had stopped existing Session 42's ruling, built and measured. `NODES_BEFORE_CODE` is a fourth prompt block carried by both channels: weigh the library heavier where one node does the whole step, and — the half that stops the first one making graphs worse — once a calculation needs code at all, do all of it in that one code node. The forbidden shape is named in the concrete. Separate from THREE_WAYS_TO_COMPUTE on purpose, in both directions. Session 39's control removed the two older blocks together, so it could say the pair moved `Substring` from 10/10 to 3/10 and nothing about which block did it. The harness gets a second arm, --node-weighting=off, which subtracts this block alone. n=10 per arm, two requests, arms interleaved, 40 sessions all valid on first submit: fix006-string-math (simple) Substring 8/10 ON vs 0/10 OFF; 8x `Substring -> Expression` fix006-price-line (complex) a single JavaScriptFunction 10/10 in BOTH arms alternation (>=2 class crossings) 0/40 The second table does not show the exception clause preventing alternation — the hazard never appeared in the control either, so it is unfalsified rather than proven. It shows the thing the ruling warned about: a rule that only pushed "use the node" would manufacture that chain, and this one did not. Two instrument repairs, both of the same shape: - The control arm's marker `Reach for the Script node LAST` appears nowhere in the product. Session 43's AC4 edit reworded that line and did not touch the harness; because the check only asked whether a marker SURVIVED the strip, a marker that had stopped existing passed silently. Markers are now asserted present before the strip as well as absent after — which caught this session's own first choice of marker, a phrase that falls across a line break. - The grader scored an `Expression` as a JavaScript body, so the arm that reached for a node read as 1/10 "const/let" against 4/10. An Expression holds one expression and can contain neither `const` nor `var`. Style is graded over statement bodies now; the rate is 1/1 and 4/4. And the copy is graded at last. Session 43 recorded that nothing anywhere asserted this block; `tests-unit/fix-006/promptGuidance.test.ts` is 11 specs in the plain-Node runner, checking every node name against the shipped catalog rather than a second hand-kept list, plus four more assertions over real MCP stdio. Gates: typecheck 0 · test:main 232/3578 · noodl-mcp 52/613 · lint:ci 876 of a 3916 baseline. Measurement cost $0.89; both arms archived so the A/B can be re-read without re-spending it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hecks that could only pass Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ight and failed AA Records session 24 in TASKS.md, the UNI-013 task file and the handover. The slice landed in the platform repo (`f64f138`). What is worth carrying back here is what AC2 cost: the obvious token by name, `--theme-color-fg-muted`, is an editor CHROME label colour and scores 3.19:1 on the light page ground — below AA, on the lede of every page. Two screenshots of the two themes said the site was fine. A criterion that is a number caught what a criterion that is a look could not. Three more, all filed where the next person meets them: - A control pair needs its arms PINNED, not merely required to differ. Every contrast ratio passes in dark, so a light arm that silently returned the dark palette would have gone green while measuring one theme twice. - A Map-based diff collapses a duplicate key silently, and the probe that found it did so by landing on an occupied name — which measures the collision, not the mutation. - `pkill -f "next start"` matches nothing (Next renames to `next-server`), so a stale server served a CSS hash that no longer existed and the page rendered completely unstyled while build, sweep and 516 specs all stayed green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…frozen First live-editor measurement of CN-010 AC1, with the observations written down before the editor was launched. The conditional-group form works end to end and nothing on the path is kit-aware: `inputs: ['itemCount']` arrives in the editor's own format with the condition intact, the row appears at `mode = list` and vanishes at `mode = grid` reversibly, it is connectable through the real connection popup, and the wire is drawn, saved and live at runtime. The `channelPort` form is erased in every surface and every state, including `getPorts()` with the runtime live and the node mounted. A census over the live library found `channelPort` in exactly one of 177 types — the fixture's own kit node — which is why nothing ever went red: the exporter strips the port expecting an editor-side manager, and that manager is commented out. The finding that outlives the criterion: a kit node's definition is delivered once and then frozen. One write flipped a condition and added a new node; one reload delivered the new node and never the changed condition, and a full editor reload delivered both. Same file, same write, same reload — so the only variable is whether the editor already knew the name. The cause is a TODO on the line, in the function s12 flagged as wanting a look. Two instruments nearly produced false findings and both are recorded: `setParameter` does not re-render the property panel, and `WarningsModel` returns 0 beside a deliberately bogus node type. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… like the defect Session 43 named the missing control: every FIX-022 cell measured over-decomposition where reuse is IMPOSSIBLE, so nothing would notice a "don't factor for a single use" rule damaging the case Richard builds on purpose. This is that cell. - `reuse-available`: the one corpus request where creating a component is correct — the same badge wanted at three existing sites. - `expect.minPlacementSites`: the reuse half of the oracle, set only where reuse is actually available. - `plan-grade.ts`: the grader, extracted so a spec can hold it (the harness calls main() at import). Placement counts on `instantiates` OR on the intent naming it, and reports the split — the structured field was filled on only 5 of s41's 11 real placements, so a structural-only grade would have scored 6 real placements as never placed. - 15 specs in test:main; three mutants each kill named specs. Measured n=10 per arm, interleaved: all 17 planned sessions factored one shared component and placed it at all three sites. 0 single-use. The guard has a clean baseline and the doctrine moves nothing here either. Three sessions died on an exhausted API credit balance. A session that never planned grades as 0 creates and fails the oracle — indistinguishable from "the planner refused to factor". summarise() now grades planned sessions only and prints the provider's note; verified against a real failing run. ✅ The mechanical grade reproduces s43's hand re-grade exactly: 11 creates, 11 single-use, 0 reused, same runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ths that graded as findings Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tate nothing could express
FIX-013 ruling 1(c) says the shim should serve zero rows. Both earlier readings
of the task reached for `useSampleData: false` to get there. That flag
uninstalls the network shim and points the preview at the project's real
backend -- it is AC3's violation, not its implementation.
The other half is worse: an empty dataset is not an empty sandbox.
`SandboxStore.list()` invents five records for any class it has never heard of,
deliberately, so a preview never strands a graph. Shipping `{ classes: {} }`
therefore serves five synthesized rows per class queried, not zero. The
behaviour the ruling rules against was already pinned by a spec, which is how
it was found.
So the third state is built here, and switched on nowhere:
- `SandboxDataset.synthesizeMissing`, defaulting to true, so `undefined`,
`true` and no-dataset-at-all are all the behaviour that shipped before it.
- `SandboxStore.list()` honours it and still caches, because a form with no
rows behind it has to stay writable.
- `buildSandboxDataset({ emptyState: true })` ships the class list named and
empty -- a named class is what stops the store inventing it -- drops the
unknown-shape caveat, and says the emptiness is the point.
`buildBenchExport` is untouched on purpose: flipping the bench without removing
the toolbar in the same commit would offer an empty Data panel on every
component instead of one, which is the reported screenshot spread wider.
Gates: typecheck 0 errors; noodl-runtime 137 suites / 2515; test:ci 2849 specs
/ 6 failures, the same six by name; lint:ci exit 0 at 877, measured identical
with the change reverted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…thing could express Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records `d205b47` in TASKS.md, the task file and the handover. Lane A is closed — only slice 4 (the badge artworks) remains, and it is Richard's. The sequencing lesson is filed where the next person will meet it, because it is the kind that repeats: slice 1 was scoped as "most of the win" and is nearly invisible, and it was landed first against a complaint that was explicitly about appearance. Richard's reaction on seeing it was "I thought the style was redone??". The ordering was defensible on engineering grounds and wrong on the grounds that mattered. When an ask is about how something looks, ship something that looks different in the same session. Two more defects AC2's arithmetic caught that rendering could not, both now in the handover's findings list: - A theme-INVARIANT ground given theme-DEPENDENT ink passes in one theme and fails in the other. `--theme-color-on-primary` flips; `--theme-color-avatar-gradient` does not, so the avatar was white-on-azure at 2.63:1 in light only. - `bg-page` is DARKER than `bg-0` in the light theme — it is the page's floor, not a chrome surface, and a sticky header on it cost 0.03 of the AA bar. And one that needs no contrast maths: a token can name an asset that is not served. `--font-family-display` resolved to its system fallback on the web for as long as the site existed. Correct-looking source, wrong render, no error anywhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eaten 84 built-ins
s19 found that a kit node's definition is frozen after first delivery: one write
that both changed a node and added one delivered only the addition, and a restart
was the only recovery. That finding is CN-014 AC1 verbatim -- not a new task, and
two sessions had already done CN-014's mandatory measure-first pass without the
task being open.
Two causes, both in NodeLibraryImporter:
- mergeUpdates' known-name branch carried `// TODO: Update the node data?` and
discarded the new definition;
- mergeInByName DID replace the picker-index entry but never set `updated`, so
updateIndex published nothing and NodeLibrary.reload() never ran -- which is
why adding a node to a kit refreshed its group and editing one did not.
The obvious fix corrupts 84 built-ins. The generated cloud library merges once per
session, shares ALL 84 of its type names with the browser library, and all 84
definitions differ -- so an unconditional replace hands Expression, REST2, Model2
and 81 others to the cloud library and inverts a first-writer-wins precedence.
Three candidate keys, measured before writing anything:
- `runtimeTypes` contains the runtime -> it is a UNION; cloud captures the 84;
- `runtimeTypes.length === 1` -> the mirror failure, stops refreshing those 84;
- `clientId` -> a viewer mints a fresh guid() per socket open, so it never fires.
Hence a separate dataOwner map: whose report the data came from, which is a
different question from where the node can run.
8 tests, 5/5 mutations killed. The precedence control is built from the two real
payloads on disk and asserts they DISAGREE before asserting the browser's survived
-- overlap alone would have made it vacuous.
Driven: one write, one viewer reload took the library 177 -> 178 AND
Title -> Panel Heading, the combination that was impossible before.
Residual, reproduced twice: an open property panel does not re-render on
libraryUpdated (six polls over 30s untouched, then updated on one re-selection),
so the variable is the selection change and not elapsed time. Recovers by clicking
any other node and back. AC1's second clause is recorded UNMEASURED, not working.
editor test:main 3,601 / 234 suites; typecheck:editor 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UNI-013 was scoped from a "Seven Tokens" artifact — a NOW/PROPOSED profile card in full CSS. I built slices 2 and 3 from the task file`s prose and never opened it. The prose carries one of the five moves (the mono face); it does not carry the dot grid, the tier colour on the badge`s edge rather than a decorative disc, the headline points figure and meter, or the eyebrow/wire/mono-label hierarchy — and those four are what make it look like the product. A written summary of a visual decision loses the visual decisions, and it reads complete because every sentence in it is true. The artifact URL is now at the top of the task file and in the handover, with the two caveats that matter: it is drawn in teal and says so (D18 ruled azure — take the forms, not the hue), and it is a mock, so its "Coach · Acme" eyebrow names an org PublicProfile does not carry. Also recorded: a sweep is only as wide as the surface it enumerates. The AC1 sweep quantifies over stylesheets, so the site`s first inline style was outside it entirely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eady hiding
FIX-013's UI slice: the bench serves no rows, and the toolbar that would have
advertised an empty Data panel goes in the same commit — flipping one without
the other would have spread the reported defect to every benched component
rather than fixing it on one.
buildBenchExport passes emptyState: true, hard-coded rather than offered. A
bench that can be switched back needs a control to switch it with, and the
control IS the reported defect. SandboxToolbar and SandboxDataEditor lose their
bench mounts along with the four pieces of state they owned; useSandboxViewer is
now called with literal trues, which is AC3 structurally — it is the only writer
of noodl-sandbox-data, the runtime disables the shim for the literal string
"real" alone, and nothing the bench renders can now produce it.
The summary did NOT go where the Fix direction said. It called for the chrome
strip beside BenchCaption; that placement satisfies the letter of the task and
defeats AC2. describe() appends the backwards-ports sentence LAST, and
BenchCaption is a single nowrap line whose own comment names it the shrink zone
because BEN-004 measured the strip clipping at 640px. A sentence appended last,
dropped into the element designated to truncate first, is not relocated — it is
hidden. So it lives in the bench, full width and wrapping, with no height cap.
That is strictly better than the toolbar it replaced: a 30px nowrap row with an
ellipsis, which was already clipping the diagnostic. AC2 is met here for the
first time, not preserved.
Ruling 2 is answered as the two surfaces diverge, recorded in the doc as the
ruling required: the toolbar, the data editor and sandboxDataDraft all keep
their owner in SandboxPreview, so nothing is orphaned and nothing left disk.
Rulings 3 and 4 take their non-destructive branch — signedIn and useSampleData
survive as programmatic options with no UI, so the Real-backend assertions
needed no edit at all.
The first spec written for this failed, and the failure was the finding:
ShareItem reads no collections, so the class map is EMPTY — the identical shape
to the bug report's CategoryCard. That makes synthesizeMissing: false the only
thing doing any work on this component, since with classes {} and synthesis left
on the bench would serve five invented rows per class queried, more fabricated
data than before the change meant to remove it. It also caught a spec that could
not fail: a loop over Object.values(classes) asserting records.length === 0
iterated zero times and passed without measuring anything. Both were rewritten —
the empty map asserted directly, the populated case pinned by supplying a class
rather than hoping the fixture had one.
⚠️ Built and gated, NOT yet driven: AC1 and AC2 want a running editor and a peer
holds the checkout. Fixture is ready at NodeGX test projects/fix013-drive, whose
/Probe carries a pBackwards port plugged "input" for AC2.
Gates, taken this session on a tree measured clean of a peer's seven uncommitted
paths (test bundle 23:41:30, earliest peer edit 23:42:39):
root typecheck exit 0, zero error TS
test:ci @ 39393 2849 specs / 6 failures — the floor, the same six by name
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… and a spec that could not fail s55 built FIX-013's UI slice and did not drive it; AC1/AC2 are the whole of the open work and the fixture is already waiting at fix013-drive. Records the two things a builder would otherwise repeat. The Fix direction's chrome-strip placement would have hidden the diagnostic it was meant to preserve — describe() appends the backwards-ports sentence last, and BenchCaption is the strip's designated shrink zone because BEN-004 measured it clipping at 640px. And a new spec passed by looping over an empty collection zero times, which is why the spec that FAILED was the one carrying the finding. Also corrects a claim s55 nearly shipped: tests/ IS typechecked, by typecheck:editor-tests. The bare typecheck covers src/ only, and there are nine typecheck:* scripts that it is not the union of. Gate taken, exit 0. lint:ci re-measured rather than inherited: 877, unmoved by the deletion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CN-006b AC1-AC4, built and driven. The list, removal and the property-panel provenance row; item 2 (the create entry point) turned out to be already built by CN-006, which reading the file first is what established. Three of the spec's own premises were false: - "the right node count and VERSION" — no manifest in any of the 29 real projects has a version field, the scaffold writes none, and the manifest schema has no version property. The row reads one when present and omits it rather than defaulting; CN-016 owns what a kit version means. - "with a link to its docs — the field already exists" — it exists and it is PROSE. docs is one field over two vocabularies (CN-008's finding, third consecutive task): on the payload a real viewer sent, both kit types carry the author's sentence and none of the 175 built-ins carries the field at all. An href would have opened nothing. - provenance "already carried" — module was already on the exported type and needed a reader, not plumbing. A P1 capability gap closed in passing: getNodeDocs reads the build-time enriched catalog, keyed by type name, so a kit type can never be in it — the header's help button was gated on that lookup and a kit node had 2 action buttons to a built-in's 3. Driven, and the old gap reproduced live to prove the fix caused the equality rather than merely coinciding with it. There is no kind:'node-kit' marker; the rule is subtractive and was built from a census of all four manifest shapes on disk, because a fixture with only a kit in it passes against a list that returns every module it finds. 29 tests, 15/15 mutations killed (two of which exposed tests that could not fail, not code that was wrong). test:main 3,630/236 re-measured; typecheck:editor 0.
CN-006b is built and driven; the tier-2 authoring arc is complete. The handover leads with the part that generalises: three of the task's own acceptance criteria rested on premises that a census contradicted, and two of them would have shipped a wrong surface (a version field no manifest has, and an href over prose). Also records the four CDP traps the drive turned up — the native confirm modal that takes CDP down with it, hover not opening the Tooltip, the fiber walk that reports the last element in the container for every element, and cdp click taking the first of ~400 matches, which opened a peer's project before it was caught.
…d gate Bug 6 closed, FIX-028 recorded, commit hashes corrected, nine FIX-027 reports rather than seven. 🔴 The one that matters: the test:ci floor in the appendix was measured on 3d8b162, BEFORE FIX-028 — and FIX-028 edited packages/noodl-editor/tests/canvas/CanvasThemeNodeSchemes.test.ts. `tests/` is the electron suite that only test:ci runs; `jest --listTests` does not list it, so test:main never saw the change. The edit looks safe (a token fixture whose assertions are about node-category colours, which did not move) but that is a reading and not a measurement, and the prompt now says so rather than quoting a floor that no longer applies to the tree. Also recorded from FIX-028: palette-copies.spec.ts caught 15 stale copies across 5 files, so it is the thing to run after any palette change; and only tokens moved, so hard-coded greys in the editor's legacy style.css did not follow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nvisible
Bug 21's editor half. `useLearnerPath` surfaced only `refused` and dropped
`absent`, `unauthenticated` and `unreachable` on the floor, which is why Richard
saw a console 404 and nothing on screen. The field mismatch behind that 404 is
fixed on the platform — but a client that says nothing when a write fails would
have hidden the next cause just as completely.
`projectionFailure` now words all four, distinctly. Two corrections fall out
that were not in the report:
The hook's comment claimed a refusal is "worded by the platform and shown
as-is". It was not — it was routed into `{ kind: 'failed' }`, whose note is a
fixed "could not be written, and it will not be retried". Wrong twice for a 409
the learner can fix and retry themselves.
And `absent` is narrated here, deliberately. D15's silence protects a surface
the viewer must not learn exists; this one is already on their screen with a
button on it, so silence is not privacy, it is a dead control. It is worded as
a fact about the step, never as an error.
UNI-001 AC4 caught the first placement and the boundary was right. The helper
went into `learnerpathview.ts` first because that module is tested, and AC4
asserts that the module deciding what is DRAWN never sees a session — which the
`unauthenticated` sentence trips. The answer was the boundary, not a reword:
`projectionNote` maps the five outcome kinds inside a 200 and is a drawing
decision; this maps the transport outcomes of a write and knows about
credentials. It stays in the hook, exported for the spec.
Graded by mutation twice — collapsed to one sentence, and reverted to the
pre-fix behaviour — 4 of 6 arms go red each time. The two survivors are the two
that should. typecheck:editor, typecheck:editor-tests, eslint all exit 0;
test:main 300 suites / 4871 tests / 0 failures.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…harness
The release's one open engineering question, and the answer was one line below
where every previous session stopped reading the log:
Test run timed out after 900s without reporting results.
The cap is not the job's — that really does set no timeout-minutes, which is
what made this invisible. It is DEFAULT_TIMEOUT_MS in packages/noodl-editor/
test.js: 15 minutes, overridable by NOODL_TEST_TIMEOUT_MINUTES. The suite needs
about 15 minutes. Runner variance alone decided whether a run graded anything —
one finished of four, and the three that did not were killed at 2766, 2264 and
2194 specs with no summary line, which reads as a failing gate rather than an
unfinished one.
Three earlier readings were wrong and all three fit the evidence. "Degradation,
not a wall-clock cap" is backwards. "Both incomplete runs died in AIX-011, that
is the death zone" — this one died in AAQ-011; there is no death zone, a run
dies wherever 900s falls. And the listener-flood comparison was confounded: the
completed run carried more warnings because it ran more specs, and per-spec the
rates are indistinguishable, so that count could never have decided it.
Measured over all 2264 spec starts: the per-spec median is 0.001s and flat from
first decile to last, so nothing degrades. 59 specs take 17-20s each and eat
12.4 of the 14.9 minutes, all authoring specs (AAQ-*, AIX-*, AIB-004, BEN-001).
Making those faster is the real work; this only stops the cap deciding.
Raised to 30 minutes with the measurement recorded beside it, which is the check
test.js's own comment asks for before raising. Verified the variable arrives:
both spawners use { ...process.env } and strip only ELECTRON_RUN_AS_NODE.
test.js already documented this on 2026-08-12, in the comment above the
constant. Three sessions rediscovered it from scratch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r key Found while preparing the drive. nexus-1's env carries no ANTHROPIC_API_KEY, so defaultProjector() returns UnconfiguredProjector and projectConcept answers `unavailable` before the insert — whose own comment says it never claims a row, so configuring a key later still works. Two consequences are good news: the drive is free and consequence-free (no model call, no money, no burnt (learner, concept) pair), and the before/after is still unambiguous — 404 drawing nothing becomes 200 saying "not switched on for this community yet". Silence becoming an honest sentence is what both halves of the fix were for. The third is not: the button still cannot explain anything until a key is configured. That is a spend decision for Richard, not a defect to patch. Also corrects something I had implied: concept_projections = 0 does NOT prove the 404, because an unconfigured call leaves zero rows too. The 0-of-15 slug/teaches measurement is what proves it; the row count only shows no projection was ever stored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bug 21 is fixed in both halves and deployed, and it exposed a third defect that is Richard's spend decision: production has no ANTHROPIC_API_KEY, so the button still cannot explain anything even though the mismatch and the silence are both fixed. Costs measured from the real prompts: ~$0.03 per learner for all 11 steps, once ever, ~$30 per thousand. Test (editor) is a 900s cap inside test.js, not degradation and not the listener leak. Three earlier readings all fit the evidence and were wrong; the file that prints the message had documented it on 2026-08-12. The handover leads with a measurement that is easy to destroy by accident: run 32468326220 was still testing that fix, and pr.yml sets cancel-in-progress, so a push kills it. That is why the previous commit is deliberately unpushed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the learner for the lesson Bug 18, two halves of three. Richard finished every step of *State on a page* and was told "All 3 checked steps are done" and "the project has problems (21 reported)" in one breath, over 26 diagnostics that ship inside the lesson. THE NUMBER WAS NEVER A COUNT. `capFindingLines` keeps MAX_FINDING_LINES = 20 and appends one line announcing the overflow — and that line is in the array. So `findings.length` saturates: 26 problems print 21, and so would 1,000. The `info` filter was a red herring; re-measuring the bundle gives 26 diagnostics of which 26 are non-`info` (20 warnings, 6 errors). 21 was the cap, only the cap. `buildLessonEvidence` carried the same saturated number to the platform as `findingCount`, so the stored row was wrong too. `WholeSolutionResult` gains `findingTotal`, counted before the cap in both adapters — they have to agree, which is why they already cap at one number. `findingTotalOf()` owns the read and falls back to the list length when an adapter reports none: absent means "not reported", never "none found", and under-reporting a broken project beats calling it clean. `normalise` bumps the tally when it adds the empty-page finding and does not invent one when the adapter was silent. THE SENTENCE. With every checked step done it is now an observation about the project beside the lesson's own verdict, not one verdict reading as failure. The findings are still counted and still listed — the verdict is softened, the finding is not, and a learner with a step outstanding gets the plain sentence. Graded by mutation six ways; each goes red and restores byte-identical. test:main 300 suites / 4884 tests / 0 failures (floor 4871 + 13 new), mcp 55 suites / 651 tests, four typechecks exit 0. Still open in bug 18: the 26 diagnostics are in `state-on-a-page`, which is in neither checkout and whose source path is gone — where it ships from has to be answered first. And no gate validates a shipped lesson bundle at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ode halves closed The measurement the last session held a commit to collect is taken: 2849 markers, a real summary line, ten failures matching the floor by name, zero timeouts. Records what is actually left in FIX-027, which is mostly not code — four of the six open items are decisions for Richard, and both remaining code items need the editor. Adds the two traps this session paid for: a capped list's length is not a count, and a reading that fits is not one that excludes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The runs quoted a moment earlier were cancelled by the push that added the document quoting them — cancel-in-progress, the trap recorded two sections above. Points at the live runs on a3a9be9 instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…already moved Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing in scripts/ or .github/ had ever looked at project-examples/ — the only reference to it was the comment-legibility scanner. Bug 18 was found by a person opening a lesson that carried 26 diagnostics they had no way to fix, because the diagnostics were in the lesson's own graph rather than in anything it asked them to build. Fourth time in this repo a checker existed and had never been pointed at the corpus that ships. npm run lessons:check walks every bundle under project-examples/lessons/<slug>/: lesson.json parses and verifies against the node catalog, and BOTH projects — the starter at the bundle root and solution/ — load and validate clean. log-a-thing measures 0 manifest findings and 0 diagnostics across starter 3c/12n and solution 3c/16n. The cardinality is printed on every run: a checker reporting clean over an empty read scores identically to one over a perfect bundle. Warnings fail this gate, unlike library:check, and that is the point. An unknown node type arrives as severity: warning — exactly how the sibling gate reported 58/58 clean while nine shipped prefabs had no type at all. Gating on errors alone would have left a hole shaped precisely like the defect. The strict bar is what the corpus already meets. Graded by mutation, in CI: --self-test copies the real bundle, breaks it seven ways, and requires each break to be caught in the arm it NAMES rather than merely to exit non-zero — a harness that threw would exit non-zero too. Verified by meta-mutation that the reason check can fail. Control runs first; the shipped corpus is never written to. knownCollections is deliberately not supplied: omitted and supplied-empty are different answers, and [] would fire unreachable-collection against every correct data lesson. Deriving it properly is TUT-002 AC3, which is open. Its own CI job rather than a step on library: three times this release a red step was hidden behind a green one earlier in the same job. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ading it taught A content gate's SEVERITY THRESHOLD is where its hole goes: unknownNodeType is a warning, so an errors-only gate reproduces bug 18's blind spot exactly — which is what library:check's own header records as 58/58 clean over nine typeless prefabs. And 'exited non-zero' is not 'caught': a mutation harness that throws reads as a successful catch. The self-test asserts the arm each mutation was aimed at, and was meta-mutated to prove that check can fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… withdrawn The one shipped-source cluster the handover pointed at — +32 in nodegx-node-kit-types/src/index.d.ts — cannot be typed. It is a hand-written mirror whose any-sites are byte-identical to react-component-node.ts, and its two stated rules (self-contained; must not lie) each forbid a different half of the fix. Nothing would catch a mistake either: drift.test.js records members[name] = isOptional, a boolean, so it is blind to member types. Also: Test (editor) is at the floor on a THIRD seed (72336), 2849 specs, ten failures matching by name; and the slow-spec count was measured on a truncated run — it is 82, not 59, with seven suites at 79% of the wall. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…found again Model.prototype.on warns on '>' 10000, so every on() past the line prints — the 12,636 lines are one crossed threshold, not 12,636 leaks. The cost is real: notifyListeners walks the whole array per event, so past 10,000 every event is an O(10,000) scan. And a model holding >10,000 listeners survives from spec #1219 to spec #2848, so it is never torn down between specs. But causation is still unestablished. The mean triples across the boundary while the MEDIAN STAYS FLAT, the flood begins inside BEN-001 (so "after" and "slow suite" are the same population), and the within-suite trend contradicts itself — 384x slower for AIX-011, 0.07x for AIX-002. The control that would settle it needs a quiet machine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… on a 4th seed Lesson bundles (FIX-027) ran on CI for the first time and passed — and it printed its cardinality (starter 3c/12n, solution 3c/16n; 6 components, 28 nodes), which is what separates a gate that checked something from one that found nothing. Its self-test caught all seven mutations in CI, each in the arm it names, including the empty-corpus exit 2. Test (editor) on the pushed tree: 2849 specs, 10 failures, 0 timeouts, seed 68471 — a fourth unrelated seed, same four suites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ere loaded Test (editor) came back 13 against a floor of 10, and the three extra were all BEN-001 harness-export specs reading `inputs.length = 0`. Not a regression: the PR merge tree and the branch tree are the same object (b9ce3f9), and the two runs differ only in the order jasmine drew. `benchInterface` resolves `node.type` against the global NodeLibrary, so a `Component Inputs` node the library has never heard of resolves to the unknown type, carries no `haveComponentPorts`, and `ComponentModel.getPorts()` walks past it — an empty interface, and therefore no parameters on the mounted node. Nothing loads that library at start-up; fourteen suites each install the fixture blob in their own beforeEach, and this file relied on one of them being drawn first. The control is inside a single run. At seed 22715 the harness-export specs ran at ordinal 208 and failed; the earliest library-loading suite ran at 508; and the SAME assertion passed at ordinal 574 from the interface describe, which had only ever passed because it was drawn late. At seed 69883, same tree, BEN-001 landed at 1009 and the run was the floor. Both interface-deriving describes now load the library themselves, and the helper asserts the type is there so an empty interface fails where the cause is rather than in an assertion about ports. The seed is pinned for ONE run to grade this against the order that broke it; the next commit removes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… comes out Removes NOODL_SPEC_SEED from pr.yml — it was pinned for exactly one run, which graded the BEN-001 fix against the order that broke it: same seed 22715, same ordinals (harness-export 208, interface 574, earliest library load 508), 13 failures before and 10 after, the floor name for name. Records it as §2b⁷. The part worth carrying: "2849 / 10" was quoted across four seeds as the floor, and a spec depending on ambient global state is invisible until the draw exposes it — three of those four seeds simply drew kindly. Read the failure NAMES; a count that matches is not a set that matches. Also corrects two things I nearly left overstated: thirteen suites load the library in this bundle, not fourteen (the fourteenth is a jest file in another process), and the four tests/components/*ports.js files carrying the identical hazard are not "checked and safe" — every spec in all four is xit, 0 active of 13. They cannot fire, which is a different fact and the true one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ince D13
SUB-011's three "the validator stays silent" specs are three of the floor's ten
failures, and they are the guard catching a real regression rather than stale
expectations.
The `fx` toggle stores `{mode:'expression',expression,fallback,version}` on a
port still typed string/number, and the typed runtime evaluates it and coerces
the result (`noodl-runtime/src/node.ts:32,60`). D13 (`c1c0b5b5`, 2026-08-18)
registered `rules/parameterValue`, so `SemanticValidator` began checking those
objects against the port's primitive shape: the corpus fixture went from silent
to 5 errors + 9 warnings, and `InvalidParameterValue` is the code that blocks.
SUB-011 predicted this on 2026-07-24, three weeks before the check existed:
"if it ever grows parameter/type checking, the object form reads as invalid
without an explicit carve-out (the same shape as the existing dynamic-port
carve-outs)". This is that carve-out, placed beside the connection-only skip
for the same stated reason — the value's shape is beside the point — and using
`ExpressionParameter.ts`'s own guard, which that task says to reuse rather than
reimplement. The port-existence check deliberately still runs first.
Measured, not inferred: the fixture reads 0/0 on all three spec paths
(plain, strict, after a round-trip). Graded by mutation — with 13 expression
objects made malformed (`expression: 42`, so the guard correctly declines
them) the reading returns to 5 errors + 8 warnings, so the carve-out skips
only well-formed expressions.
The fixture's `textAlign` was a real defect and is now `textAlignX`: Text
declares textAlignX/textAlignY and `textAlign` is only the CSS property the
node sets internally, so it stood in for "a plain parameter" while naming
nothing. Same dead parameter noodl-preview's fixture carried.
D13 is in v0.2.0 and was not in v0.1.7. The unpublished draft ships this.
typecheck:editor 0 · typecheck:editor-tests 0 · eslint 0 · test:main 300
suites / 4884 tests, 0 failures · MCP gate tests 34/34. The three jasmine
specs are CI-graded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d ruled out
Two more of the floor's ten, and the failing spec was right: F35 fixed the
list and left the default behind.
LAS-011 / F35 gave `openai-compatible` its own entries so a DeepInfra or vLLM
user is no longer offered `gpt-4.1`, an id their gateway does not serve. But
`getDefaultModel` is `find(isDefault) || models[0]`, and the OpenAI entries
that follow carry `isDefault` — so the `models[0]` fallback never ran and the
PRESELECTED model was `gpt-4.1` anyway. Measured before the change:
`getDefaultModel('openai-compatible')` → `gpt-4.1`, and the provider owned 0
defaults where every other provider owned exactly 1.
`deepseek-ai/DeepSeek-V4-Pro` is now the gateway default — DeepSeek rather than
Qwen because Richard's recorded verdict names it first ("DeepSeek v4 or the
latest Qwen, the big ones") and it carries four times the context window. So
"exactly one default per provider that owns models" now reads 1, which is the
invariant that was pointing at this.
The catalogue spec asserted plain equality with OpenAI's list — the behaviour
F35 deliberately ended, so it had become an assertion that the bug was still
there. Rewritten to the real contract: own entries FIRST, the OpenAI catalogue
after (Azure and similar shims do serve those ids), plus the half that matters
to a gateway user — what they are given by default is one of their own ids.
Measured after: all four providers own exactly one default, the list is
[...own, ...openai], and the default is DeepSeek. typecheck:editor 0 ·
typecheck:editor-tests 0 · eslint 0 · test:main 300 suites / 4884 tests, 0
failures. The two jasmine specs are CI-graded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ent" was too strong Records §2b⁸: the ten failures are not ten independent items. SUB-011 ×3 is D13 reaching the shipped fx expression form with no carve-out (and it ships in 0.2.0); AI model registry ×2 is F35's surviving half, where a gateway user's DEFAULT was still gpt-4.1. Both suites were the guard working. Also reconciles phase-74 §6a, flagged by a peer session: "these ten are deterministic and order-independent across four seeds" is true of these ten but must not be read as "order does not matter here" — 1877dbd found BEN-001 depending on ambient global state that three of four seeds drew kindly around. Adds a fifth seed (16562, run 32479979252) and the warning that the floor is now moving. Both of my own wrong readings are recorded beside the right one, including a control that applied 0 mutations and read exactly like the valid case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-measured the installed state-on-a-page bundle on a tree carrying dae76da, which stopped the validator reading the shipped fx expression form as invalid. The count drops 26 -> 25: the one that went is the lone invalid-parameter-value WARNING. The six errors are untouched, so the section's argument is unchanged — 21 was the display cap and not a count, and the bundle still ships dirty. Added as a dated note rather than by rewriting the numbers, because the 20 + 6 = 26 arithmetic is load-bearing in the reasoning above it and was correct when it was taken. Worth carrying: a corpus measurement inherits the defects of the instrument that took it. My original 26 was measured with a validator that had a real bug in it, and that bug was one of test:ci's ten floor failures — which had been read as accepted noise rather than as a guard that was firing correctly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…three The remaining five floor failures are one defect. Since D13 (08-18) both the semantic validator's report and `authoredPreconditionDiagnostics` run `checkParameterValues`, so every caller that MERGES them doubles every parameter-value finding. A rejection naming one mistake twice reads as two mistakes: `AIX-011` counted 2 blocking warnings where the agent caused 1, and `AIX-006`'s candidate burned its repair rounds being rejected for "2 problem(s)" that were the same problem printed twice. CN-009 AC5's drive found this on the day D13 landed and fixed it — in `noodl-mcp/src/validate.ts` only (`810478ce`). Its own comment names the editor's authoring loop as a caller that runs the preconditions, and the editor's loop and `planTools.ts` kept doubling for three days. So `dedupeDiagnostics` moves to `validation/authoredCandidate.ts`, beside the `diagnosticKey` it is defined in terms of and under that file's own stated rule — "three copies that agree are still three copies; this is the one" — and all three callers now use it. The MCP copy is deleted rather than left to drift. Also corrects the comment above the editor's join, which still justified it with "the semantic validator has no view of parameter VALUES — its normalized model does not carry them". D13 ended that; it is the same expired sentence `parameterValues.ts` already carries a correction for. Both baseline paths were checked and left alone: they collapse into a Set, so duplicates never reached them. typecheck:mcp 0 (the stricter one) · typecheck:editor 0 · typecheck:editor-tests 0 · test:main 300 suites / 4884 tests, 0 failures · noodl-mcp 55 suites / 651 tests, 0 failures. The five jasmine specs are CI-graded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… are D13 Records §2b⁹: the ten Test (editor) failures reduce to four causes, three of them D13. Names what is still open — ea40285 is not CI-graded yet, and SUB-011's posture decision is still an empty placeholder — and the method notes that cost this session time, including a mutation control that applied zero mutations and read exactly like the valid case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Run 32482182225 (seed 93591) on a46b52b: 2849 specs, 6 failures. The 3x SUB-011 and 2x AI model registry are gone by name. The sixth was never in the ten: AIX-011 criterion 7 (createPlanDocWriter). It passed in the two prior runs, uses a fresh mkdtemp dir, and settles async undo writes with a fixed 30ms setTimeout that a loaded runner loses. None of its imports is in the diff. Recorded as a real fragility to fix at the constant, not at the assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`log-a-thing` was unfinishable by construction, and the first actionable step was where it stopped. Found by driving it, 2026-08-21. The chain: steps 1 and 6 grade against the built-in database (`collectionExists`, `hasColumns`, `rowCountAtLeast`); nothing bound a backend to an installed lesson; and `Backend Services` — the only surface that can create or bind one, and the only route to the Schema and Data surfaces (`backendSurfaces.tsx`) — was registered `isDisabled: isLesson === true`. So grading answered "this project is not bound to a backend, so there is no built-in database to read", and the panel that would have fixed it was greyed out. The learner met what reads as a malfunction on step 1 of 8. Two halves, because either alone leaves it broken: 1. `models/lessonbackend` gives the lesson its database on open — create (or adopt), start, and report the binding, which `EditorPage` applies through `setCloudServices` so the panel's `cloudServicesChanged` listeners see it. 2. `backend-services` is no longer disabled for a lesson that grades against the database. It is the only lesson exception in the rail, and by the time a learner opens it they meet their own lesson database rather than an empty panel. Which lessons need one is DERIVED from their own grading conditions (`lessonObservesDatabase`), never a hand-written `requiresBackend:` field — `lessonprotection.ts` records the reason: a second statement of the same fact drifts on the first edit, and here it would drift into a lesson grading against a database it was never given. 🔴 It deliberately does NOT create the collections, and there is a spec whose whole job is to say so. Step 1 *is* "make somewhere to put them"; a starter shipping `LogEntries` pre-made makes it already-complete before the learner arrives — the F2 defect `lessonbundleverify` exists to catch, arriving through the one blind spot it admits to, since it reports database steps `not-checked`. A green bundle gate would not have caught it. `findReusableBackend` and `LocalBackendMeta` move to `BackendServices/backendReuse`, a leaf, because `provisionBackend` reaches `projectmodel.editor` → `PopupLayer` → React and cannot be imported from a plain-Node runner. Extracted, not copied: `noodl-mcp` already carries a third implementation, documented as verbatim. The lesson text was also wrong and is corrected: it promised the Data panel "offers to make one. Say yes", an affordance that does not exist — the empty state says "Create one in Backend Services". It now names the real path. DRIVEN, not read. On a real editor with the project deliberately unbound: `[lesson] database created at http://localhost:8587`, the port is LISTENING, the panel opens on "Log a thing database · Built-in • Port 8587 · ACTIVE · Running · This project uses this backend", Schema and Data are both on the card, and the lesson bar moved from the not-bound error to "Looking for records in LogEntries". typecheck:editor 0 · typecheck:editor-tests 0 · typecheck:mcp 0 · lint:ci 0 · tsfixme unmoved at +37/+125 · test:main 301 suites / 4890 tests, 0 failures · noodl-mcp 55 suites / 651 tests, 0 failures (serially; the parallel red was contention with the live editor, and passes 12/12 alone). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… in the repo 41 commits have landed since the v0.2.0 tag, 14 of them fixes, and none is in the draft's artifacts. Publishing it would ship a first-time user none of the twelve bugs Richard filed, the dark-theme inks, the "21 problems" cap, the fx false positive — or a Log a thing that can be finished at all. PUBLISH-0.2.0.md leads with that decision and the evidence, then the re-cut procedure, the 15 assets to check directly, clean-machine verification, and what is red on purpose (Test (editor) at its floor of 4, Lint on the tsfixme decision) so neither is misread as a blocker. RELEASE-NOTES-0.2.0.md moves into the repo. The previous copy lived in a session scratchpad and survived only by luck; it gains a section for the fixes that came out of using the alpha, and two honest additions to Known limitations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…an-machine check is left The old draft was built from 6fdf9de and was 42 commits stale. Deleted, re-tagged at the head of cline-dev, rebuilt: all six release jobs green, 15 assets, latest-mac.yml carrying all four mac files under one version, both mac legs notarised and stapled. Still isDraft. Also corrects §1a. It told the next person to run `ci:build:editor` locally because `pr.yml` had not run for a week. It runs now, as the `Build (viewer + editor bundles)` job, on the exact commit being tagged — so the measurement already existed remotely and the local run was redundant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It describes a draft that no longer exists. Points at PUBLISH-0.2.0.md and says which half of it is still true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… update modal too The launcher's update dialog renders `release.body` verbatim as markdown in a 520px window (autoupdater.js sets `notes: release.body`; UpdateDialog reads `chosen?.notes || state.releaseNotes`). There is no second field to author, so these notes serve both surfaces and the opening is written to stand alone as the modal's first screenful: summary, then the changelog/Discord/YouTube links, then the detail. Adds the plain-language changelog artifact link in the v0.1.7 house pattern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewritten from a survey of the actual v0.1.7..v0.2.0 range rather than from the earlier draft, which covered roughly half of it. Restored: - NodeGX Community / the university (UNI, 78 commits): one account that gates nothing, intake and personalised path, the tutor overlay, "check my work" handing work to the platform, the people directory, badges - MCP lesson authoring (UNI-010): get_lesson_brief / create_lesson / check_lesson / derive_starter, and why derive_starter's subtraction makes ghostwriting structurally hard rather than merely checked for - Phase 66's 23-of-24 user-test fixes, and its finding that most of the list was built machinery the user could not reach - Phase 69's two stated principles, and what "20 of 20" does not mean Adds to Known-and-open the two gaps the survey surfaced: the library is not licence-audited, and the style-token editor is deliberately absent. 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
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