> **EUPL 1.2 modification notice / EUPL 1.2 修改声明**
>
> - Upstream / 上游项目: GeoGebra — <https://github.com/geogebra/geogebra>
> - Official version / 官方版本: **5.4.926.0** (shown as 6.0.926.0 in
>   Web/Classic 6 numbering / Web 端显示为 6.0.926.0)
> - Base commit / 基准提交:
>   `7e907db1f687181650da05165ef91ea76591f125` ("Fix compile error",
>   2026-07-02 14:02:01 +0200)
> - Sources pulled / 源码拉取时间: 2026-07-03 (UTC), shallow clone of the
>   official repository / 自官方仓库浅克隆
>
> The GeoGebra app bundled in this package (`geogebra/5.0/web3d/`, GWT
> permutation `1AA944323119FEA358CBF9FEB79D3605`) is compiled from the
> author's modified sources containing **all** modifications documented
> below. As required by EUPL 1.2 Art. 5, the complete modified source
> tree is included in this package as `geogebra-source-optimized.tar.gz`.
>
> 本压缩包捆绑的 GeoGebra 应用（`geogebra/5.0/web3d/`，GWT permutation
> `1AA944323119FEA358CBF9FEB79D3605`）由作者的修改版源码编译，包含下文
> 记录的**全部**修改。按 EUPL 1.2 第 5 条要求，完整修改版源码已随包
> 提供：`geogebra-source-optimized.tar.gz`。
>
> (Historical note / 历史说明: ggbx v4.0c and earlier bundled an interim
> build that lacked section 4 below; v4.0e and earlier bundled a build
> that lacked section 6 below. / v4.0c 及更早版本捆绑的是缺少下文第 4 节
> 修复的中间版本；v4.0e 及更早版本捆绑的是缺少下文第 6 节修复的版本。)

# GeoGebra Performance Work — Change Log

Source checkout: `/root/workspace/ggb-src/geogebra` (shallow clone of the official
`geogebra/geogebra` repository). All changes below are relative to the pristine
clone (commit `7e907db "Fix compile error"`), and are included as-is in
`geogebra-source-optimized.tar.gz`.

This document lists every file that was modified or added, what each change does,
and why. Changes are grouped by feature, roughly in the order they were made.

## 1. Build environment fix

### Modified: `gradle.properties`

Added `-Djava.net.preferIPv4Stack=true` to `org.gradle.jvmargs`.

**Why:** the build/dev sandbox's outbound IPv6 connectivity is broken, but DNS
resolution still returns both A and AAAA records for Maven/Gradle repository
hosts. The JVM would intermittently pick the (broken) IPv6 address, causing
Gradle/dependency-resolution network failures that looked like DNS problems but
were actually a routing issue. This flag forces the JVM to prefer IPv4, which is
reachable.

**Effect:** build-environment compatibility only, no runtime behavior change.

## 2. Algebra View performance fix — O(N²) layout thrashing when adding many objects

### Modified: `source/web/web/src/main/java/org/geogebra/web/full/gui/view/algebra/AlgebraViewW.java`

**Problem:** `doAdd(GeoElement geo)` called `getOffsetWidth()` (forces a
synchronous browser layout/reflow) and conditionally `resize(0)` for *every
single* object added to the construction. `resolvePendingAdditions()` similarly
called `updateSelection()` (which scans every row in the tree) after every
batch of additions. For a construction with many objects (hundreds), this
produced one forced layout + one full tree scan per object — effectively
O(N²) work when adding N objects in a burst, causing visible UI lag/freezing
when loading or building large constructions.

**Fix:**
- Added two fields: `lastKnownWidth` (caches the last known panel width) and
  `deferredSyncScheduled` (guards against scheduling more than one deferred
  pass per burst).
- Added a new private method `scheduleDeferredSync()`: uses
  `Scheduler.get().scheduleDeferred(...)` to run, at most once per JS event-loop
  turn, a single pass that (a) re-checks the panel's actual width and calls
  `resize(0)` only if it changed, and (b) calls `updateSelection()` once.
- `doAdd()` no longer calls `getOffsetWidth()`/`resize(0)` synchronously per
  item; it calls `scheduleDeferredSync()` instead, and sets the new item's
  width from the cached `lastKnownWidth` (falling back to `getFullWidth()` if
  not yet attached to the DOM).
- `resolvePendingAdditions()` calls `scheduleDeferredSync()` instead of
  `updateSelection()` directly.

**Effect:** adding many objects in one synchronous burst (e.g. loading/building
a large construction) now triggers at most one layout resync and one selection
refresh, instead of one per object. This was validated as the actual root cause
of "many objects → laggy UI" complaints (rendering itself, e.g.
Canvas2D/WebGL, was not the bottleneck for this scenario).

## 3. Ctrl/Shift+wheel zoom animation performance fix

### Modified: `source/shared/common/src/main/java/org/geogebra/common/euclidian/EuclidianView.java`
### Modified: `source/shared/common/src/main/java/org/geogebra/common/euclidian/CoordSystemAnimation.java`

**Problem:** Ctrl/Shift+wheel zoom (and a few other zoom modes) is implemented
as a short eased animation (`CoordSystemAnimation`, `MAX_STEPS = 15`,
`DELAY = 10` ms tick, `MAX_TIME = 400` ms cap; the mouse-wheel entry point uses
`steps = 4`). Every intermediate animation tick (`CoordSystemAnimation.step()`)
called `EuclidianView.setCoordSystem(...)`, which *unconditionally* — regardless
of the `repaint` argument — called `setRealWorldBounds()`. That method pushes
the new bounds into the `Corner[]`-backing objects and calls
`kernel.setEuclidianViewBounds(...)`, which calls
`Kernel.notifyEuclidianViewCE(EVProperty.ZOOM)` — a full dependency-graph
update that recomputes every registered `EuclidianViewCE` (`Corner[]`,
`AxisStepX[]`/`AxisStepY[]`, implicit curves, loci, etc.) and cascades to their
dependents (e.g. a custom axis built with `Sequence[]`/`Join[]`, which is
common in hand-built coordinate-system constructions).

In other words: a single Ctrl/Shift+wheel notch triggered a ~40 ms animation
made of 4 steps, and **every one of those 4 steps re-ran the full
dependency-graph recompute**, not just the final one — repeating expensive
work 4× (or up to 15× for other zoom modes) for what is visually a single zoom
action. Profiling (Chrome DevTools CPU profiler, via CDP) on a real user file
using a `Sequence[]`/`Join[]`-based custom axis showed ~47% of all captured
CPU time inside a single GWT `ArrayList`-copy primitive reached exclusively
through this cascade.

**Fix (in `EuclidianView.java`):**
- Extracted the field-update/validation part of the old `setCoordSystem(double,
  double, double, double, boolean)` into a new private helper
  `updateCoordSystemFields(...)`. The public method's behavior is unchanged
  (same validation, same field updates, same call to `setRealWorldBounds()`,
  same repaint/notify logic) — this is a pure refactor, not a behavior change.
- Added `setCoordSystemForAnimationStep(double xZero, double yZero, double
  xscale, double yscale)` (package-private): updates the view's scale/position
  fields and repaints drawables (`updateAllDrawablesForView(true)`), but
  **skips `setRealWorldBounds()`** entirely — i.e. skips pushing bounds into
  `Corner[]`/`AxisStepX[]`/etc. and skips the `Kernel.notifyEuclidianViewCE`
  dependency-graph cascade.
- Added `setRealWorldCoordSystemForAnimationStep(double xmin2, double xmax2,
  double ymin2, double ymax2)` (package-private): the real-world-coordinate
  counterpart, used by the `ZOOM_RW` animation mode; computes scale/origin the
  same way `setRealWorldCoordSystemVisible(...)` does (with `visible = true`,
  matching the only caller), then delegates to
  `setCoordSystemForAnimationStep(...)`.

**Fix (in `CoordSystemAnimation.java`):**
- `step()` (the intermediate-tick method — **not** `stopAnimation()`) now
  calls `setCoordSystemForAnimationStep(...)` / `setRealWorldCoordSystemForAnimationStep(...)`
  instead of the full `setCoordSystem(...)` / `setRealWorldCoordSystemVisible(...)`,
  for all five animation modes (`AXES_X`, `AXES_Y`, `ZOOM`, `ZOOM_RW`, `MOVE`).
- `stopAnimation()` (the final tick) is **completely unchanged** — it still
  calls the original, full `setCoordSystem`/`setRealWorldCoordSystemVisible`.
  This guarantees the dependency-graph recompute always runs at least once,
  exactly when the animation reaches its final, authoritative value — so the
  end state of every zoom animation is bit-for-bit identical to the
  pre-fix behavior. Only the *intermediate, transient* frames skip the
  expensive recompute.

**Scope note:** this fix only touches `CoordSystemAnimation` (used by
Ctrl/Shift+wheel zoom, double-click reset, and a few other short animated
zoom/pan modes). Ordinary mouse-drag panning
(`EuclidianController.snapMoveView()` → `setCoordSystemFromMouseMove` →
`translateCoordSystemInPixels` → the original, full `setCoordSystem`) goes
through the same expensive cascade and was **not modified** — see section 4
below, which fixes the analogous problem for the algebra/graphics splitter
specifically, but general mouse-drag panning of the graphics view itself is
still out of scope. Both `EuclidianView.java` and `CoordSystemAnimation.java`
are in the shared `common` module, so desktop, web, and 3D views all benefit
(`EuclidianView3D` does not override any of the touched methods).

**Measured effect** (Chrome DevTools CPU profiler, via CDP, on the same real
user file, before vs. after):

| Metric | Before | After |
|---|---|---|
| Ctrl+wheel events processed in an 8 s window | 42 | 78 |
| Main-thread idle time in that window | 3.6% | 36% |
| Average "busy" CPU samples per wheel event | ~1191 | ~295 (↓ ~75%) |
| Samples in the dominant hotspot function per event | ~596 | ~92 (↓ ~84%) |

**Correctness verification:** screenshots taken before any zoom, after 8
zoom-in ticks, after 16 zoom-out ticks, and after 8 more zoom-in ticks (net
zoom change: zero) were pixel-identical to the initial screenshot, confirming
the custom axis tick marks (which depend on the skipped `Corner[]`/`AxisStepX[]`
cascade) always end up correct once the animation settles — the fix never
produces a wrong final state, only a briefly "frozen" (not re-rendered until
the animation's last frame) appearance for `Corner[]`/`AxisStepX[]`-dependent
objects during the ~40 ms of an individual zoom animation.

## 4. Algebra/Graphics splitter drag performance fix

### Modified: `source/web/web/src/main/java/org/geogebra/web/full/gui/layout/ZoomSplitLayoutPanel.java`
### Modified: `source/web/web/src/main/java/org/geogebra/web/full/gui/layout/DockSplitPaneW.java`

**Problem:** dragging the divider between the Algebra View and the Graphics
View is implemented by GeoGebra's own fork of GWT's `SplitLayoutPanel`
(`ZoomSplitLayoutPanel`, with `DockSplitPaneW` as the concrete pane used for
this split). Every `mousemove` while dragging calls
`Splitter.setAssociatedWidgetSize()`, which defers a `forceLayout()` via
`Scheduler.scheduleDeferred(...)` guarded by a "don't schedule twice in the
same turn" check — but real, browser-dispatched `mousemove` events are spaced
tens of milliseconds apart (far more than one JS turn), so this guard never
actually coalesces anything: **every single mousemove tick runs a full
`forceLayout()` → `onResize()` cascade.**

That cascade eventually reaches `AppW.ggwGraphicsViewDimChanged(width, height)`,
which calls `EuclidianSettings.setPreferredSize(...)`. Since the size differs
on essentially every tick, this unconditionally calls `settingChanged()` →
`notifyListeners()` (`AbstractSettings.java`), which reaches
`EuclidianViewCompanion.settingsChanged()` and from there either
`EuclidianView.setCoordSystem(...)` or `EuclidianView.updateBounds(true, true)`
— both of which trigger the *exact same* `kernel.setEuclidianViewBounds(...)` →
`Kernel.notifyEuclidianViewCE(EVProperty.ZOOM)` full dependency-graph cascade
described in section 3, just reached through a completely different call
path (settings-listener notification instead of the zoom-animation stepper).

Profiling (same method as section 3, on the same real user file, but
simulating a real mouse-drag on the splitter instead of a wheel zoom) showed
~40.5% of all captured CPU time in the same GWT `ArrayList`-copy hotspot, and
the browser could only process 118 mousemove events in an 8 s window — i.e.
the lag was severe enough to visibly slow down the browser's own input
processing.

**Fix:** rather than building a new "lightweight update" method (as in section
3), this fix reuses an existing, already-established mechanism:
`AbstractSettings.beginBatch()` / `endBatch()` (already used elsewhere in the
codebase, e.g. `EuclidianStyleBarW`'s grid-type switcher). While a batch is
open, `settingChanged()` just sets a dirty flag instead of calling
`notifyListeners()`; `endBatch()` fires the notification once, only if
something actually changed, and reads whatever the *latest* value is at that
point — so no new "does this only apply on the last step" logic was needed.

- **`ZoomSplitLayoutPanel.java`:** added a new protected `onDragStart()`
  no-op hook (mirroring the existing `onDragEnd()` hook, which
  `DockSplitPaneW` already overrides for unrelated UI logic), and a call to
  `splitPanel.onDragStart()` in `Splitter.startDrag()` right after
  `mouseDown = true` (mirroring `endDrag()`'s existing
  `splitPanel.onDragEnd()` call). Both `ONMOUSEDOWN` and `ONTOUCHSTART` go
  through `startDrag()`, so touch-based dragging is fixed too.
- **`DockSplitPaneW.java`:** added an `onDragStart()` override that calls
  `app.getSettings().getEuclidian(1).beginBatch()` (and also `getEuclidian(2)`
  if `app.hasEuclidianView2(1)` is true); the existing `onDragEnd()` override
  now calls the matching `endBatch()` on the same settings object(s) as its
  very first action (before the pre-existing "unbundled mode" panel-hiding
  logic, so the fix applies regardless of that mode).

**Scope note:** deliberately did *not* use `Settings.beginBatch()`/`endBatch()`
(the "batch literally everything" helper on the top-level `Settings` class) —
its own doc comment says it's "recommended to be used just for file loading;
in other situations individual setting containers should be used to start
batching." Batching only the two `EuclidianSettings` objects that are actually
involved (matching the granularity of the existing `EuclidianStyleBarW`
usage) avoids delaying unrelated settings-listener notifications (algebra
view, CAS, table, etc.) that have nothing to do with this resize. 3D views
(`getEuclidian(-1)`/`getEuclidian(3)`) were not specifically exercised, since
the reproduction file is a plain 2D construction.

**Measured effect** (same file and profiling method as section 3, dragging the
splitter instead of zooming):

| Metric | Before | After |
|---|---|---|
| Mousemove events processed in an 8 s window | 118 | 244 |
| Main-thread idle time in that window | 1.6% | 76% |
| Average "busy" CPU samples per resize event | ~371 | ~26 (↓ ~93%) |
| Dependency-graph-cascade hotspot | ~40.5% of all samples | gone entirely — remaining hotspots are genuine canvas draw calls (`setTransform`, `fillRect`, `getImageData`, etc.) |

**Correctness verification:** screenshots before dragging and after a drag
gesture confirm the Algebra View panel narrows, the Graphics View widens, and
the custom axis re-renders at the correct tick spacing for the new pixel
width (unchanged zoom level) — matching expected behavior exactly.

## 5. Curve-sampling Web Worker mechanism — experimental, opt-in only, NOT wired into production rendering

This is a separate, earlier line of investigation into a different lag report
(parametric-curve/trajectory redraw cost). It proves that the production curve
*sampling* algorithm (`CurvePlotter`/`CurveSegmentPlotter`, the same code the
main thread uses) can run inside a real Web Worker, reusing the already-compiled
GWT module (no extra build step, no separate GWT module). **It is not on any
default code path** — it only runs when `?curveSamplingSmokeTest` is present in
the page URL, and it is not called from `DrawParametricCurve`/the real
redraw/update cycle. Profiling later showed that ordinary function graphs
(`y = f(x)`) don't even use the code path this mechanism accelerates (they use
a separate `IntervalPlotter` pipeline instead), so this mechanism's practical
value is unproven and it should be treated as a prototype, not a shipped
optimization.

### New: `source/shared/common/src/main/java/org/geogebra/common/kernel/arithmetic/CurveExpressionCodec.java`
Encodes/decodes an `ExpressionNode` expression tree to/from a compact string,
so an arbitrary curve formula can be sent to a Worker as plain text and
rebuilt with the real `ExpressionNode`/`Operation` types (bypasses
`Parser`/`AlgebraProcessor` entirely — only handles `ExpressionNode`,
`FunctionVariable`, and `MyDouble` leaves; anything else, e.g. a leaf that
references another `GeoElement` such as a slider, throws
`UnsupportedOperationException`, a deliberate fail-closed behavior).

### New directory: `source/web/web-common/src/main/java/org/geogebra/web/html5/euclidian/curveworker/` (11 files)
- `WorkerApp.java` — headless `App` implementation (GUI/undo/CAS-dispatch/etc.
  all no-ops); `static Kernel createKernel()` builds a throwaway headless
  Kernel (a new one per sampling call, by design, to avoid unbounded
  `GeoCurveCartesian` accumulation in a long-lived `Construction`).
- `WorkerLocalization.java` — minimal `Localization` stub (English only).
- `WorkerFormatFactory.java` — minimal `FormatFactory` stub (plain
  `Double.toString`-based number formatting).
- `WorkerDefaultSettings.java` — minimal `DefaultSettings` stub.
- `WorkerAwtFactory.java` — extends the shared
  `org.geogebra.ggbjdk.factories.AwtFactoryHeadless` (the same base the real
  `AwtFactoryW` extends) to get correct pure-Java geometry value objects for
  free; only the ~8 methods that genuinely need a real rendering context
  (buffered images, fonts, gradients, textures) throw
  `UnsupportedOperationException` (never exercised by curve sampling).
- `WorkerEuclidianController.java` / `WorkerEuclidianView.java` — minimal
  headless `EuclidianController`/`EuclidianView` subclasses sufficient to run
  `CurvePlotter.plotCurve(...)`.
- `PointCollectorPathPlotter.java` — a `PathPlotter` that records sampled
  points into a flat `double[]` (`x, y, isLineTo` triples) instead of drawing.
- `CurveSamplingWorkerBridge.java` — the `@JsType(namespace = GLOBAL)`-exported
  Worker-side entry point: decodes the two curve expressions via
  `CurveExpressionCodec`, builds a `GeoCurveCartesian`, runs the real
  `CurvePlotter.plotCurve(...)`, returns the flat point array.
- `CurveSamplingWorkerHost.java` — main-thread Worker lifecycle manager. Reuses
  the main thread's already-compiled GWT permutation
  (`GWT.getModuleName()`/`GWT.getPermutationStrongName()`/
  `GWT.getModuleBaseURL()`) via `importScripts()` inside the Worker — no
  separate GWT module or extra build step.
- `CurveSamplingWorkerSmokeTest.java` — manual/opt-in test hook; only runs if
  `?curveSamplingSmokeTest` is present in the page URL; writes its result to
  `window.curveSamplingSmokeTestResult`.

### New: `source/web/web-common/src/main/resources/org/geogebra/web/resources/js/curveSamplingWorker.js`
Worker bootstrap script: fakes `self.window`/`self.$wnd` and a permissive
`document` stub (the compiled GWT bundle's unrelated top-level
feature-detection code touches `document` unconditionally during module load),
then `importScripts()`s the already-compiled `.cache.js` and dispatches
sampling requests to `CurveSamplingWorkerBridge`.

### Modified: `source/web/web-common/src/main/java/org/geogebra/web/html5/css/GuiResourcesSimple.java`
Added one `@Source(...)` / `TextResource` declaration to expose
`curveSamplingWorker.js` as a GWT `ClientBundle` resource.

### Modified: `source/web/web-common/src/main/java/org/geogebra/web/html5/euclidian/EuclidianViewW.java`
Added a single line at the end of the main constructor calling
`CurveSamplingWorkerSmokeTest.maybeRun()` — a no-op unless
`?curveSamplingSmokeTest` is in the URL.

**Validated result:** in a real production `gwtCompile` build, in a real
browser, sampling `x(t) = t, y(t) = sin(t)` via the Worker matched
`Math.sin` to floating-point precision (max error 1.11×10⁻¹⁶ across 125
points). This proves the underlying mechanism works, but again: it is
**not connected to any real redraw/update path** and should not be assumed to
fix any real-world lag on its own.

## 6. Slider-tool dialog ignores the user-entered "Step" value (always created a 0.1-increment slider)

This is an unrelated correctness bug fix (not a performance change), reported
after deliverables2: creating a slider by typing a command, e.g. `slider(0,5,1)`,
correctly produced a slider with increment `1`. But creating a slider via the
toolbar **Slider tool** (click the tool, click the graphics view, a dialog pops
up with Name/Min/Max/Step fields) always produced a slider whose *actual*
increment was locked at `0.1`, no matter what value was typed into the Step
field — min and max from the same dialog applied correctly, only Step was
silently ignored.

### Modified: `source/web/web/src/main/java/org/geogebra/web/full/gui/dialog/SliderDialog.java`

**Root cause:** `GeoNumeric` has an `autoStep` flag. When `autoStep` is `true`,
`GeoNumeric.getAnimationStep()` — the method actually consulted everywhere the
slider's live increment is used (dragging, animation, XML export) — **ignores**
the stored `animationIncrement` value entirely and instead returns an
auto-computed value, which has a hard floor of `DEFAULT_SLIDER_INCREMENT = 0.1`
for typical slider ranges. New `GeoNumeric`s created via
`GeoNumeric.setSliderFromDefault(...)` (as `SliderDialog.initResultGeo()` does)
inherit `autoStep = true` from the construction defaults.

`SliderDialog.createSlider()` (the handler wired to the dialog's "OK" button via
`setOnPositiveAction(this::createSlider)`) read the Step field and called
`geoResult.setAnimationStep(getNumberFromInput(stepValue))` — so the typed value
*was* stored — but never called `geoResult.setAutoStep(false)` afterwards.
Because `autoStep` stayed `true`, `getAnimationStep()` kept ignoring that stored
value and fell back to the auto-computed 0.1-floor value on every read, which is
exactly the reported symptom. By contrast, the `Slider[min,max,step]` command
processor (`CmdSlider.java`) already calls `slider.setAutoStep(false)` right
after setting the step for a finite step argument, which is why the typed-command
path worked correctly.

**Fix:** added a single line, `geoResult.setAutoStep(false);`, immediately after
`geoResult.setAnimationStep(getNumberFromInput(stepValue));` in
`SliderDialog.createSlider()` — mirroring the pattern `CmdSlider` already used
correctly, and the pattern the newer (not yet wired up)
`SliderInputDialogModel.createSlider(...)` also already implements correctly.

**Verified in a real browser** (Xvfb + Chromium + CDP, real `gwtCompile`
production build, real toolbar click → dialog → typed "1" in the Step field →
clicked "OK"):
- The created slider's exported XML now includes `<animation step="1" .../>`
  (before the fix, this attribute is omitted entirely, since
  `GeoElement.getXMLAnimationTags()` only writes it when `!isAutoStep()`).
- Dragging the resulting slider's handle now moves it in clean integer steps
  (e.g. `a=1` → `a=3` after a drag that spans roughly 1.85 units), instead of
  the previous 0.1-granularity behavior.

## 7. Packaging fix: first `deliverables3` build showed "Couldn't load web3d from Super Dev Mode server at http://:9876" on open

No source code was at fault here — this was a build-artifact contamination bug
in how `geogebra-web-app.tar.gz` was produced, introduced by the verification
step for section 6 above.

**Root cause:** to test the slider fix in a real browser, `./gradlew
:web:web:run` (GWT's SuperDevMode dev server) was started against the same
`source/web/web/war/` directory that `:web:web:gwtCompile` +
`:web:web:mergeDeploy` had just written the production build into. Loading
`full.html` through that dev server caused SuperDevMode to silently overwrite
`war/web3d/web3d.nocache.js` (the module `full.html` actually loads) with its
own small stub bootstrap file, whose entire job is to redirect the browser to
the SuperDevMode code server (default port `9876`) for on-demand compilation
instead of using the real precompiled JS. That stub file was still sitting in
`war/` when `geogebra-web-app.tar.gz` was packaged, so anyone opening the
packaged app — with no code server running — hit exactly the "Please make
sure this server is ready" prompt. The other three GWT modules (`web`,
`webSimple`, `tablet3d`) were untouched, since SuperDevMode only recompiles
whichever module the loaded page actually requests.

**Fix:** re-ran `:web:web:gwtCompile` (all 4 permutations) followed by
`:web:web:mergeDeploy` one more time, without starting `:web:web:run`
afterwards, to regenerate a clean `war/`, then repackaged
`geogebra-web-app.tar.gz` from that. `source/web/web/war/web3d/web3d.nocache.js`
is back to a genuine 6188-byte production bootstrap with zero occurrences of
`9876`/"Super Dev Mode" (matching deliverables2's file exactly), and the other
three modules were already clean.

**Verified:** served the rebuilt `war/` with a plain `python3 -m http.server`
(deliberately *not* `:web:web:run`, to avoid recontaminating it) and loaded
`full.html` in a real Chromium via CDP — both applets initialize with no
error dialog, no `javascriptDialogOpening` CDP event fires, and no console
output mentions `9876`/"Super Dev Mode"/`codesvr`.

**Lesson for future verification on this tree:** never test a `gwtCompile` +
`mergeDeploy` production build by pointing a browser at the same `war/`
directory through `:web:web:run` — that command's whole purpose is to let
SuperDevMode rewrite compiled output in place. Either serve `war/` with a
plain static file server for verification (as done here and as already noted
in `/root/workspace/ggb-src/PROGRESS.md`), or re-run `gwtCompile` +
`mergeDeploy` again as the last step before packaging, after any `:run`-based
testing session.
