Skip to content

diagram: editor bug burndown (caret mapping, Cmd+Enter, dblclick rename, connector sync, cloud drag)#831

Merged
bpowers merged 10 commits into
mainfrom
editor-bug-burndown-2026-07
Jul 3, 2026
Merged

diagram: editor bug burndown (caret mapping, Cmd+Enter, dblclick rename, connector sync, cloud drag)#831
bpowers merged 10 commits into
mainfrom
editor-bug-burndown-2026-07

Conversation

@bpowers

@bpowers bpowers commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Five user-reported editor bugs, fixed one commit each. Each fix was implemented and then adversarially reviewed (independent review pass verifying root cause, running tests against pre-fix code, and probing for regressions) before landing.

1. Caret placement on rendered fractions (04e588a)

Clicking the fraction bar or denominator of a KaTeX-rendered division placed the caret at a garbage offset. Root cause: the click handler resolved the annotated source span via Element.closest (ancestor-only), but KaTeX renders fraction operands inside unannotated .vlist/.frac-line layout chrome, so those clicks resolved to the whole-fraction span and interpolated across the entire equation. Now all annotated spans are gathered and the smallest box containing the click wins (pure chooseSpanForClick), generalizing to any nested expression.

Design note: a dead-center fraction-bar click maps into an adjacent operand rather than exactly at the / -- the bar carries no annotation and is geometrically ambiguous, so that is the principled behavior.

2. Cmd+Enter inserts a newline (f714c2c)

Cmd+Enter did nothing in the equation/documentation editors: modifier+Enter chords produce no beforeinput event, and Slate's legacy keydown fallback (is-hotkey) requires unnamed modifiers to be absent. New isNewlineChord routes Cmd+Enter through Editor.insertSoftBreak -- the identical operation Shift+Enter triggers.

Design notes: Ctrl+Enter is treated the same so the gesture works on non-mac keyboards (nothing else binds it in those fields). The units editor (semantically single-line) and the canvas name editor (Enter/Ctrl+Enter commit there) deliberately do not get the chord. Discovered along the way: the units editor already accepts plain-Enter newlines into a single-line field -- filed as #829 rather than scope-creeping here.

3. Reliable double-click rename (1942bdf)

Two root causes. Deterministic: a label double-click on an already-selected variable routed into the deferred-single-select path whose name-edit resolution only ran on pointer-up -- unreachable for a terminal dblclick event, so the editor never opened (the interaction union's deferredIsText field was provably dead code, now removed). Flaky: labels had no click-vs-drag threshold, so 1-2px of double-click wobble started a label drag and captured the pointer, suppressing the native dblclick. Labels now use the same 5-screen-pixel threshold as the rest of the canvas.

4. Connector/equation sync warnings (3791f79)

New feature: variables now show the existing warning-dot (plus explanatory text in the details panel) when the sketch and equations disagree -- an equation references a variable with no connector drawn from it, or a connector's source is no longer used by the equation.

Design decisions:

  • Dependency truth comes from the engine's existing Model.getIncomingLinks (no new FFI). Using getLinks/causal edges instead would be wrong: those add structural flow-to-stock integration edges (which no connector represents) and omit stock initial-equation deps.
  • Warning-level and non-blocking: never affects simulatability, never pins the equation editor open.
  • Missing-connector only flags when the referenced variable is actually drawable on the view; modules are exempt as targets (input wiring lives in the references table) and as connector sources; aliases resolve one hop; stocks are checked against their initial equation.
  • A freshly drawn connector warns until the equation references its source -- that is out-of-sync by definition, and the dot doubles as a "finish wiring this" nudge.
  • The annotation is display-only; new tests pin that serialization never emits it, so saved projects are untouched.
  • Known inefficiency (recompute on viewport-only settles) documented as tech-debt item 67 with fix options.

5. Live flow updates while dragging a cloud (524a4fe)

Dragging an existing flow's cloud endpoint moved the valve live but left the line/arrowhead stale until release, which then snapped. The preview used UpdateFlow (valve-centric) while the commit used UpdateCloudAndFlow. Fixed by unifying on the flow-creation codepath, per the reported expectation: growInCreationFlow is generalized into growEndpointDrag, and the canvas previews existing-endpoint drags through it -- preview and commit now share geometry, so release is snap-free by construction.

Design notes: UpdateFlow's valve/Z-routing math is deliberately not unified (it still serves stock-endpoint and stock-move drags, and past attempts to merge the two broke routing). Dragging a stock-attached endpoint retains the old stale-until-release preview -- scoped out and filed as #830.

Verification

Every commit passed the full pre-commit gamut (Rust fmt/clippy/tests, TS lint/tsc/build/tests, pysimlin). Diagram suite grew from 1322 to 1376 tests; each fix's new tests were verified to fail against the pre-fix code. Browser verification of the caret fix was done against the real KaTeX DOM pipeline; the dev-server API returns 401 to the automation browser session, so the remaining fixes rely on the gesture-suite tests driving real pointer/keyboard events through the full component tree -- worth a quick manual pass on /bpowers/population (fraction clicks, Cmd+Enter, double-click rename, connector warnings, cloud drags).

Generated with Claude Code
Session: https://claude.ai/code/session_01591tEkjitjUfd5wrtSmr6u

bpowers added 5 commits July 2, 2026 08:01
Clicking the fraction bar or the denominator of a KaTeX-rendered
division (e.g. population/average_lifespan) placed the editing caret at
a garbage offset, while numerator clicks worked. The click handler
resolved the annotated source span via Element.closest, an
ancestor-only walk: KaTeX renders a fraction's operand rows as
absolutely-positioned .vlist wrappers plus a .frac-line, none of which
carry data-eqnloc, so those clicks resolved to the whole-fraction span
and interpolated coarsely across the entire equation.

Replace ancestry lookup with geometry: gather every annotated span
under the preview and pick the smallest box containing the click (a new
pure chooseSpanForClick in equation-caret.ts, nearest-box fallback for
padding clicks), then map within the chosen span as before. This
generalizes to any nested expression (fractions, superscripts, calls)
because the click resolves to the operand the pixel actually sits in.
A dead-center fraction-bar click maps into an adjacent operand rather
than exactly at the '/' -- the bar is geometrically ambiguous and
carries no oploc annotation, so that is the principled behavior.
Cmd+Enter did nothing in the equation and documentation editors while
Shift+Enter inserted a line break. The chord falls through both of
Slate's input paths: modern browsers emit no beforeinput event for a
modifier+Enter chord, and the legacy keydown fallback's is-hotkey
bindings require unnamed modifiers to be absent, so meta/ctrl+Enter
matches neither the split-block nor soft-break binding.

Add a pure isNewlineChord predicate (keyboard-shortcuts.ts) and route
matching chords through Editor.insertSoftBreak -- the same operation
Shift+Enter triggers, so serialization and undo behavior are identical.
Ctrl+Enter is treated the same as Cmd+Enter so the gesture works on
non-mac keyboards; Alt combinations are left to the platform. The units
editor (semantically single-line) and the canvas name editor (Enter and
Ctrl+Enter commit there) deliberately do not get the chord.
Double-clicking a variable's name label failed to open the inline name
editor in two ways. Deterministically: a label dblclick on an
already-selected element routed into the deferred-single-select path,
whose name-edit resolution only ran on pointer-up -- but dblclick is a
terminal MouseEvent with no pointerId, so the resolver's pointer-id
guard could never pass and the editor never opened (the union's
deferredIsText field was unreachable). Flakily: Label had no
click-vs-drag threshold, so the 1-2px wobble of a physical double-click
started a label drag and captured the pointer, which suppresses the
native dblclick in real browsers.

Open the editor synchronously in handleSetSelection when the request
comes from a label double-click (collapsing selection to that element,
same rendering dependency the unselected path always used), delete the
now-dead deferredIsText plumbing from the interaction union, and gate
label drags (and their pointer capture) behind the same
5-screen-pixel threshold the rest of the canvas uses.
Equations are the source of truth for causal dependencies; connectors
are their visual representation, and nothing kept them consistent: an
equation could reference a variable with no connector drawn from it, or
a connector could linger after the equation stopped using its source.
Flag both on the dependent variable, with the existing warning-dot
idiom on the canvas and explanatory text in the variable details panel.

The check is a pure core (connector-sync.ts computeConnectorErrors)
fed by the engine's existing Model.getIncomingLinks -- no new FFI.
getIncomingLinks rather than getLinks is load-bearing: causal edges
add structural flow->stock integration edges (which no connector
represents) and omit a stock's initial-equation deps, so it would
produce false positives and negatives; incoming links already exclude
builtins/TIME, structural stock relationships, and dotted module
outputs. Notable semantics: only variables whose dependency is
actually drawable on the view are flagged; modules are exempt as
targets (input wiring lives in the references table) and as connector
sources; aliases resolve one hop; stocks are checked against their
initial equation. The annotation is display-only -- saves serialize
from the engine, and new tests pin that toJson never emits it.
Warnings never block simulation or pin the equation editor open.
Dragging an existing flow's cloud endpoint moved the valve live but
left the flow's line and arrowhead stale until pointer-up, which then
rerouted and visibly snapped. The preview path (applyGroupMovement ->
UpdateFlow) and the commit path (computeFlowAttachment ->
UpdateCloudAndFlow) used different geometry: UpdateFlow's along-axis
fallback only moves the valve. Flow creation never had this problem
because it routes its live preview through UpdateCloudAndFlow.

Unify on the creation codepath: generalize growInCreationFlow into
growEndpointDrag (pure, in flow-attach.ts), and give deriveRenderState
a branch mirroring the in-creation one that reroutes the flow and
dragged cloud live -- cloud follows the cursor over empty space, pins
to the stock edge over a stock. Preview and commit now call
UpdateCloudAndFlow with the same delta, so release is snap-free by
construction. The non-dragged endpoint's cloud is restored to its
committed position during the drag because UpdateFlow's cloud-to-cloud
path translates both clouds. UpdateFlow's valve/Z-routing math is
deliberately left alone (still used by stock-endpoint and stock-move
drags); dragging a stock-attached endpoint retains the old
stale-until-release behavior, tracked as issue 830.
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.87%. Comparing base (0ef9fdc) to head (6d69942).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #831      +/-   ##
==========================================
- Coverage   90.87%   90.87%   -0.01%     
==========================================
  Files         224      224              
  Lines      143133   143133              
==========================================
- Hits       130072   130069       -3     
- Misses      13061    13064       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 524a4fe853

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/diagram/project-controller.ts Outdated
}

return project;
return this.attachConnectorErrors(project);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compute connector warnings after preserving the live view

When updateProject() rebuilds after a view edit, it calls updateVariableErrors() before preserveLiveView() replaces the active view with the newest optimistic one. Because connector warnings are now attached here from view.elements, a second in-flight connector/view edit can leave connectorErrors computed against the stale engine view but rendered on the preserved live view, so users can see missing/stale-connector warnings that no longer match the diagram. Compute these annotations after the live-view merge, or pass the final view into the check.

Useful? React with 👍 / 👎.

Comment thread src/diagram/drawing/Label.tsx Outdated
Comment on lines +182 to +186
if (start && Math.hypot(e.clientX - start.x, e.clientY - start.y) < ClickDragThresholdPx) {
return;
}
inMove.current = true;
(e.target as Element).setPointerCapture(e.pointerId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Capture label drags before the threshold return

For mouse label drags that move a few pixels and leave the <text> before crossing the 5px threshold, this branch returns without pointer capture, so later pointermove/pointerup events go to whatever is under the cursor instead of this Label; the drag never calls onLabelDrag and the label cannot be moved. Capture on pointerdown or before the sub-threshold return while still delaying the onLabelDrag callback until the threshold is crossed.

Useful? React with 👍 / 👎.

@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown

Code review

Reviewed the caret-mapping / Cmd+Enter / dblclick-rename / connector-sync / cloud-drag fixes and their supporting refactors. No blocking bugs found.

Notes on things I looked at closely:

  • chooseSpanForClick (src/diagram/equation-caret.ts): the fallback-to-nearest-box path changes behavior for clicks in preview padding when any annotated span exists — instead of falling through to the coarse whole-preview glyph mapping, all clicks now resolve via the nearest annotated span. This is intentional and documented, and the KaTeX preview's tight box makes real-world impact negligible.
  • growEndpointDrag (src/diagram/flow-attach.ts) mirrors the old growInCreationFlow for isSource: false and generalizes the endpoint index for isSource: true; UpdateCloudAndFlow's cloudIsFirst / cloudIsLast dispatch handles both correctly.
  • The Canvas endpoint-drag preview (Canvas.tsx around line 934): the endpointEl?.type === 'cloud' guard correctly excludes stock-endpoint drags (which retain applyGroupMovement behavior), and the cloud-to-cloud fixup restoring the non-dragged cloud matches applyGroupMovement's UpdateFlow translate-both semantics.
  • computeConnectorErrors (src/diagram/connector-sync.ts): module exemptions (target and connector source), alias-hop resolution, self-ref exclusion, and stale de-duplication all look correct against the invariants stated in the diagram CLAUDE.md.
  • attachConnectorErrors (src/diagram/project-controller.ts): captures modelName up-front, checks disposed after each await, and only attaches when issuesByIdent.size > 0. The pre-existing updateVariableErrors race window (a later updateProject completing before an earlier one) is widened by the extra per-variable engine calls, but that's what tech-debt item 67 is tracking — not a new bug.
  • isNewlineChord (src/diagram/keyboard-shortcuts.ts): correctly handles code === 'NumpadEnter', excludes Alt, doesn't consult Shift (harmless — Cmd+Shift+Enter would still insert a break but isn't a conflicting binding).
  • Label click/drag threshold (src/diagram/drawing/Label.tsx): 5px hypot is compared against clientX/Y directly (screen space), matching ClickDragThresholdPx's screen-pixel semantics elsewhere.

Overall correctness: CORRECT. The changes are well-scoped, the removed deferredIsText code path was demonstrably unreachable for terminal dblclick events, and test coverage grows meaningfully (dedicated regression tests for each of the five fixes, including pre-fix failure verification per the PR description).

bpowers added 2 commits July 2, 2026 13:23
Review feedback on the connector-sync work: updateProject annotated
connector drift from the engine-serialized view and only afterwards
swapped in the newer optimistic live view via preserveLiveView, so a
connector the user just drew could transiently flag as missing (and a
just-deleted one could transiently escape the stale check) because the
annotations described a view the user was no longer looking at.

Stop tail-calling attachConnectorErrors from updateVariableErrors and
invoke it explicitly at each rebuild site on the view that path
actually renders: after preserveLiveView (and projectAttachData) in
updateProject, and directly on the opened project in the initial-open
and undo-reopen paths, where no newer live view exists.
Review feedback on the label drag threshold: capture was acquired only
once movement crossed the 5px threshold, on the element under the
pointer at move time. A grip near the edge of a short label can leave
the text's bounding box while still sub-threshold, after which no
pointermove reaches the label at all -- the drag can never start from
that grip point.

Capture on pointerdown instead (on currentTarget, since the press may
land on a child tspan), keeping the threshold gate on actually starting
the drag. The load-bearing part of the double-click reliability fix is
unchanged: sub-threshold wobble still fires no onLabelDrag, so there is
no selection or node churn between the two clicks, and capture lives on
the same node as the dblclick handler so click targeting is unaffected.
Verified in a real browser: double-click rename stays reliable
(selected and unselected variables, repeated) and an edge grip drags
the label.
@bpowers

bpowers commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review

I reviewed all 5 fixes and their follow-up commits (caret placement on rendered fractions, Cmd/Ctrl+Enter newline chord, double-click rename reliability, connector/equation sync warnings, live cloud-endpoint drag preview).

Each fix is well-scoped, has a clear root-cause explanation in its commit message, and comes with regression tests that fail against the pre-fix code. Notable strengths:

  • chooseSpanForClick correctly generalizes past the KaTeX layout-chrome problem (fraction bar, \frac v-list) by picking the smallest containing box, with a nearest-box fallback for padding clicks. Tests use real KaTeX client rects for the reported population/average_lifespan scenario.
  • isNewlineChord is a small, well-documented predicate; the units editor is deliberately excluded and the pre-existing plain-Enter gap there is filed rather than scope-crept.
  • The double-click fix cleanly removes the dead deferredIsText field from the interaction union, opens the editor synchronously in handleSetSelection (safe: dblclick's pointer-up already fired), and gates label drags on the same 5px screen-pixel threshold used elsewhere. Capturing on currentTarget at press time is correct (a child <tspan> under the pointer would otherwise lose subsequent moves), and the follow-up commit d3fa004 addresses the edge-grip case surfaced during review.
  • connector-sync.ts is a clean pure core with thorough unit tests (module-source exemption, alias resolution one hop, self-reference filtering, dangling endpoints, dedup of duplicate connectors). The ordering rationale in attachConnectorErrors — running AFTER preserveLiveView, not tail-called from updateVariableErrors — is well documented, and the follow-up commit c0888af explicitly fixes the transient-phantom-warning issue with a matching regression test. getIncomingLinks (not getLinks) is the right dependency source and the CLAUDE.md invariant captures why. The recompute-on-viewport-only-settles inefficiency is filed as tech-debt item 67 with fix options.
  • growEndpointDrag is a faithful generalization of growInCreationFlow (the source/sink asymmetry is captured by isSource/endIndex); the Canvas branch that invokes it correctly restores the non-dragged cloud in the cloud-to-cloud case (which UpdateFlow's applyGroupMovement path would otherwise translate) and defers to the old behavior for stock-endpoint drags (filed as Flow endpoint detach off a stock has no live preview (only valve moves) #830).

I did not find bugs to flag. The remaining considerations (e.g. tail-of-updateProject write-back after await racing a concurrent optimistic view update; per-variable getIncomingLinks cost on large models) are pre-existing patterns or already tracked.

Overall correctness verdict: Correct. The patch is well-tested, the design decisions are documented in commit messages and CLAUDE.md, and existing behavior is preserved for the scoped-out cases (stock-endpoint drag, units editor plain-Enter, modules as connector targets).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3fa00415a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// after preserveLiveView has swapped in the (possibly newer) live view --
// see attachConnectorErrors. Runs after projectAttachData too, so it
// preserves the attached series when it rewrites the active model's vars.
activeProject = await this.attachConnectorErrors(activeProject);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compute connector warnings when drilling into modules

When a user drills into a module model, no project rebuild runs: drillIntoModule() only switches modelName and calls refreshCachedErrors(). Since connector annotations are attached only from the rebuild/open paths such as this updateProject() call, variables in the newly active child model never receive connectorErrors on first navigation, so missing/stale connector warnings are absent until a later pan/edit forces updateProject(). Trigger attachConnectorErrors (or an equivalent annotation refresh) after changing the active model.

Useful? React with 👍 / 👎.

Review feedback: drilling into a module only flips the controller's
modelName and refreshes the error panel cache -- no project rebuild
runs -- so the child model's variables never received connector
warnings on first navigation. Verifying the finding surfaced a deeper
instance of the same mechanism: equation/unit error dots were missing
after drill-in too, because refreshCachedErrors never re-annotates
project variables and the model-scoped annotation passes only run from
the rebuild/open paths.

Add refreshActiveModelAnnotations, which re-runs both annotation
passes (updateVariableErrors + attachConnectorErrors) on the current
project for the newly-active model and commits the result only if
neither the project nor modelName moved while its engine round-trips
were in flight -- a rebuild or further navigation landing mid-flight
has already produced a fresher project and must not be clobbered.
Drill-in calls it in place of the bare panel refresh; back-navigation
keeps relying on its queueViewUpdate rebuild (which already
re-annotates), with the direct pass only as a no-stored-view fallback.
@bpowers

bpowers commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown

Automated review

Reviewed all five fixes end-to-end (caret placement, Cmd/Ctrl+Enter, double-click rename, connector-sync, cloud-endpoint drag) plus the follow-up commits. The functional-core / imperative-shell split is clean; the ordering rationale for attachConnectorErrors (runs against the post-preserveLiveView project, not tail-called from updateVariableErrors) is well-justified and the refreshActiveModelAnnotations mid-flight guard on (this.project, this.modelName) correctly handles the drill-in race.

A few small notes rather than blocking findings:

  • chooseSpanForClick uses strictly-less area comparison, so on an exact area tie (e.g. two nested annotated spans with byte-identical rects, which does happen for single-token operand nodes) the earliest-listed box wins. The doc comment already calls this out; noting for context.
  • In Canvas.tsx (~line 934) the if (endpointEl?.type === "cloud") gate correctly excludes stock endpoints from the new path, but for a multi-segment flow (points.length > 2) the growEndpointDrag branch still runs. UpdateCloudAndFlows multi-segment code path takes a different route than the straight-flow case; the tests here exercise 2-point flows only. Existing behavior seemed fine on a spot-check, but worth confirming manually if you have a saved model with 3+ point cloud-terminated flows.
  • attachConnectorErrors reads this.modelName at the top and again inside the fetch loop indirectly (via the captured local). If a drill-in fires between the callers preserveLiveView and attachConnectorErrors, connector annotations would briefly target the new active model while the returned project still has the old preserved view. The commit is guarded by the disposed-flag check in updateProject, and refreshActiveModelAnnotations will re-run and correct on drill-in, so the end state is right; the transient render mid-race is the only cost.

No blocking issues found.

Overall correctness verdict: CORRECT

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d92da0a21

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1235 to +1237
const variable = model.variables.get(el.ident);
if (!variable || variable.type === 'module' || seen.has(el.ident)) {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip connector sync for invalid equations

When a target variable already has equation/compile errors, this still queues it for getIncomingLinks. For invalid/no-AST equations the engine reports no dependencies, so any existing connector into that variable is then surfaced as a staleConnector even though the equation is simply unparsable; users fixing a syntax error would get bogus connector warnings alongside the real equation error. Skip targets whose dependencies are not authoritative, such as variables with fatal equation errors.

Useful? React with 👍 / 👎.

Review feedback: a variable whose equation fails to parse was still
queued for the connector consistency check. The engine reports no
dependencies for a broken AST, so every inbound connector surfaced as
a stale-connector warning (and every real dependency as missing) on
top of the actual equation error the user was already fixing.

Skip such targets at collection time: their dependency list is not
authoritative, and the equation error is the actionable signal. This
relies on updateVariableErrors annotating errors before
attachConnectorErrors runs, which every caller guarantees. Unit-only
errors do not skip -- the AST parses fine there, so dependencies stay
authoritative. Empty equations carry an EmptyEquation error, so an
inbound connector drawn to a not-yet-written variable reads as a
forward declaration rather than stale noise; the all-empty blank
sketch was already exempted via hasNoEquations.
@bpowers

bpowers commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8ae42d58d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/diagram/project-controller.ts Outdated
async attachConnectorErrors(project: Project): Promise<Project> {
const engine = this.engine;
const modelName = this.modelName;
if (!engine || isStdlibModel(modelName)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip connector sync when the model has no equations

When every variable only has EmptyEquation, updateVariableErrors() returns { ...project, hasNoEquations: true } before annotating variable.errors; this method then still collects those variables, getIncomingLinks() reports empty dependency lists, and any connector drawn in a brand-new sketch is shown as a stale connector. In the all-empty starter-model case, add project.hasNoEquations to this early return before fetching dependencies so users don't get connector warnings while drafting equations.

Useful? React with 👍 / 👎.

@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown

Code review

Solid, well-scoped work with strong test coverage across five separate fixes. Below are review findings.

[P2] attachConnectorErrors does not exempt the hasNoEquations blank-sketch case

File: src/diagram/project-controller.ts (around lines 1216-1258, comment at 1249)

The commit 9 comment claims the all-empty blank-sketch case is "already handled by the project.hasNoEquations early return above", but there is no hasNoEquations check anywhere in attachConnectorErrors — the only reader is recalculateStatus at line 1070. The skip mechanism relies on variable.errors && variable.errors.length > 0, but when every variable has only an EmptyEquation error, updateVariableErrors short-circuits at line 1162 and returns { ...project, hasNoEquations: true } without setting errors on any variable. In that state, an aux/flow/stock's errors is undefined, so the per-target skip check fails and every variable is checked. If the user has drawn preliminary connectors in the blank sketch (a common structure-first modeling workflow), each such connector will be flagged staleConnector and produce a warning dot on the target — the exact "not stale" forward-declaration semantics the CLAUDE.md paragraph promises. Concrete failure: a two-aux model where both equations are empty and one connector a -> b is drawn will surface b.connectorErrors = [{ kind: 'staleConnector', ident: 'a', name: 'a' }] and light up the warning dot on b, contradicting the design intent stated in the same commit. Either add an early if (project.hasNoEquations) return project; in attachConnectorErrors (matches the code comment), or make updateVariableErrors still attach EmptyEquation errors on individual variables in the hasNoEquations branch so the existing per-target skip catches them.


Overall correctness verdict

Correct with one minor caveat. The five reported editor bugs are well-diagnosed and each fix cleanly addresses its root cause. Test coverage is thorough (new tests were verified to fail against pre-fix code, adversarial verification narratives are documented in commit messages, and the fake-engine harness is a nice functional-core / imperative-shell separation). The hasNoEquations gap flagged above is a niche UX regression, not a blocking issue.

🤖 Generated with Claude Code

Review feedback: when every variable has only an empty equation,
updateVariableErrors takes its hasNoEquations branch and deliberately
skips annotating variable.errors (blank-sketch suppression), so the
per-variable fatal-error skip added in the previous commit never
triggers. getIncomingLinks then reports no dependencies for every
variable and each connector drawn in a brand-new sketch surfaced as
stale -- a wall of warnings exactly where the editor suppresses them
elsewhere during initial layout.

Add project.hasNoEquations to attachConnectorErrors's early return,
before any engine fetch. The previous commit's comment claimed this
case was already guarded; that claim was wrong (the hasNoEquations
check it pointed at lives in recalculateStatus) -- the comments now
distinguish the two suppression paths accurately: per-variable errors
skip for a mixed model's not-yet-written variable, hasNoEquations
early return for the all-empty starter model.
@bpowers

bpowers commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review

Reviewed all five fixes across the caret mapping, Cmd/Ctrl+Enter, double-click rename, connector-sync, and cloud-drag preview commits (plus the four adversarial-review follow-ups).

Overall correctness: correct. No findings.

Spot-checks that held up:

  • chooseSpanForClick disambiguation is sound: smallest containing box wins, with a documented nearest-box fallback for near-miss clicks. Consistent with the PR description's design note on fraction-bar clicks.
  • isNewlineChord covers key/code/NumpadEnter and correctly rejects Alt; wiring in VariableDetails routes only through equation + docs editors, not units.
  • The double-click fix removes deferredIsText and its dead resolver branch; opening the editor synchronously in handleSetSelection when isText is set is safe because label dblclick is the only path passing isText: true and it arrives after the second pointer-up. Label's press-time setPointerCapture(currentTarget) fires on the same node as the dblclick handler, so click targeting is unaffected; the threshold gate still prevents drag firing on wobble.
  • computeConnectorErrors correctly resolves alias endpoints one hop, exempts module sources (dotted module-output deps), deduplicates same-source connectors, and skips self-refs in both directions.
  • attachConnectorErrors ordering (after preserveLiveView in updateProject; before commit in open/undo-reopen), the hasNoEquations early return, the per-variable errors-non-empty skip, and refreshActiveModelAnnotations's project !== project || modelName !== modelName commit guard are all load-bearing and correctly placed.
  • growEndpointDrag routes preview via the same UpdateCloudAndFlow the commit uses; the Canvas swap correctly restores the non-dragged cloud in cloud-to-cloud flows (verified against applyGroupMovement's cloud-to-cloud branch which would otherwise translate both).
  • The JSON-round-trip test in datamodel.test.ts pins that errors/unitErrors/connectorErrors never serialize -- exactly the invariant that keeps annotations out of saved projects.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 6d69942a02

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@bpowers bpowers merged commit fdce053 into main Jul 3, 2026
13 of 15 checks passed
@bpowers bpowers deleted the editor-bug-burndown-2026-07 branch July 3, 2026 02:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant