Video Stabilizer 2.9.3 — corresponding source and verification

2.9.3 packaged core loading
  Browser startup compiles unchanged local WASM bytes delivered by Chrome as
  a content script. No page-to-extension WASM request is made. Missing or
  corrupt data still restores original playback and reports an error.
  Update every file in the existing folder, reload the extension, then refresh
  existing YouTube pages. Processing defaults and current schema9/format2
  profiles are retained; older processing schemas use the new system defaults.
  Use Video-Stabilizer-2.9.3.zip and Video-Stabilizer-2.9.3-source.zip.
  See validation/embedded-core-browser-2.9.3.json for evidence and limitations.
  On 2026-11-05, the user reported no further Chrome Errors and normal
  stabilization with 2.9.3, and authorized public distribution. This is a
  user-reported real-use check; Edge remains unvalidated.



2.9.2 product rename (local release, not published)
  Video Stabilizer is the new product name; earlier releases used YouTube Steady.
  All features remain free, with optional one-time support and no subscription.
  Target: YouTube in desktop Google Chrome. Microsoft Edge is not validated.
  Processing, defaults, icon artwork and saved-profile identifiers are retained.
  Update the existing installed folder to keep its extension identity and data.
  Install: Video-Stabilizer-2.9.2.zip; source: Video-Stabilizer-2.9.2-source.zip.
  The historical repository URL is retained; no public release was changed.
  Validation: validation/rename-browser-2.9.2.json. Earlier notes follow.

2.9.1 stage, apply, then save
  Presets call PlayerUI.stageProfile and update draft state, never session.current.
  The form includes the preset Enable value, but UI preferences stay immediate.
  Apply is disabled when unchanged; the submit handler also rejects no-op applies.
  Save is disabled with a draft and snapshots session.current only, with the
  existing serialized persistence and late-result guards. A separate Retry
  handles processing errors without making unchanged Apply available.
  Closing restores applied fields and diagnostics without resetting motion.
  Direct Enable preserves other unfinished form values. Native/settings/profile
  files are unchanged. See validation/apply-confirm-browser-2.9.1.json.

2.9.0 one personal record and close-to-discard
  profiles.js retains schema9/format2 but reads only the current profile.
  record() writes no previous backup. Session has current/personal values and
  independent UI preferences; the undo history is removed.
  Save validates the same form as Apply, applies its snapshot when needed,
  then serializes persistence. An unchanged save drops an older previous
  field but otherwise does not write or reset analysis. Late completion cannot
  replace new-video settings or label newer edits saved. Read failures block
  overwrites; write failures keep the applied video and permit retry.
  Every real panel close calls cancelDraft before hiding, including Escape
  and the entry toggle. This clears errors/preview without resetting motion.
  Comparison state is removed from controller.js and renderer.js. The renderer
  still restores both output/matte on disable or failure, including zoom below1.
  Parameter help is hidden visually but remains in titles/aria-describedby;
  lattice/capacity warnings stay visible. Effective readings join Performance.
  Native API6, C++/compiled JS/WASM and settings.js defaults are unchanged.
  See validation/ui-simplify-browser-2.9.0.json for workflow and delivery checks.

2.8.1 control order and user-selected defaults
  Common controls are C, cutoff and My default. Advanced starts with damping,
  then mode/R/Q/center priority/S/trust/stab/subpixel/zoom/icon/language.
  Center priority is visible but disabled in single mode, without erasing its
  stored value. General defaults are single/R85/Q30/S30/C15/cutoff0.5/damping0.3,
  priority off, trust4/stab1/subpixel1/zoom1 and a visible icon.
  Schema9/format2 are unchanged. Valid personal profiles/backups are preserved;
  first use, invalid stored profiles and Restore general defaults use the new
  defaults. Geometry/cost regression fixtures explicitly retain their earlier
  values; browser tests separately exercise the new default and ordered UI.
  Native API6, compiled core and interpolation behavior are unchanged.
  Verification: validation/ui-defaults-browser-2.8.1.json.

2.8.0 region-relative search, one model
  For each region and axis: sampleOffset=min(floor(samples*S/100),
  floor((samples-1)/2)). Source offset is sampleOffset*regionSourceSize/samples.
  The global minimum cap is removed; C remains based on the full source frame.
  settings.js contains only the current grid/relative-sampling model. Historical
  pixel fields, conversion and adoption paths are removed. profiles.js accepts
  only schema9/format2 records and backups, otherwise using general defaults.
  The old storage key contributes UI preferences only; its bytes are not used
  for processing. A new save cannot resurrect an older backup.
  Source planning changes, but native API6 and compiled C++/JS/WASM stay fixed.
  Native conformance still runs; one test-only plan retains the original valid
  zero marker for the independent single-estimator/region equivalence check.
  A representable integer-raster motion replaces the obsolete fixed-sampling
  fixture. High-motion tests require multiple valid agreeing regions; lone
  central diagonal evidence remains rejected by the existing fusion rule.

2.7.2 logarithmic presentation only
  Cutoff position p is ln(f/fmin)/ln(fmax/fmin); f=fmin*(fmax/fmin)^p.
  Native range coordinates are 0..1 with continuous pointer travel. Only the
  cutoff slider maps through logarithms; every stored/applied value remains Hz.
  Four-decimal Hz rounding must not snap the low-end pointer back or stall
  repeated key steps, so the gesture position is retained until a field refresh.
  Slider aria-valuetext reports Hz and period, never raw logarithmic position.
  Period is 1000/f ms, not latency or a filter time constant. Invalid drafts
  display a dash, retain the last valid slider position and cannot be applied.
  C moves to common controls; R/Q/zoom move to Advanced without altering any
  processing field. Submit expands a hidden invalid field before reportValidity.
  Native core, settings schema/bounds/defaults and profile storage are unchanged.

2.7.1 cutoff UI simplification
  The existing cutoff field/slider moves out of Advanced. Text guides use the
  same ascending numeric direction; the Medium mark is computed from the
  unchanged slider limits and default 1 Hz. The coupled preset functions and
  duplicate profile-strength state are removed. New records omit the obsolete
  presetRevision; format1 readers still accept historical records unchanged.
  Processing schema8, native API6, compiled core and settings bounds/defaults
  remain unchanged. Profile snapshots, backup, undo and comparison remain.
  The old three-preset monotonicity test no longer applies: with C fixed, lower
  cutoff is not a universal shake-reduction guarantee. Original native
  conformance checks remain; browser checks prove that cutoff keeps C intact.

2.7.0 profile separation and display-only comparison
  profiles.js follows settings.js and owns pure snapshots, preset pairs,
  validation, preference extraction and five-step local undo. Processing schema
  remains 8; API 6 and the native JS/WASM are unchanged from 2.6.0.
  ytSteadySettings is read-only in the runtime. ytSteadyMyDefault stores a
  format1 full profile and previous backup. Language, icon visibility and
  position have separate keys; saving them cannot publish local processing.
  Session identity uses the watch video ID, not currentSrc or resolution.
  No service worker or permission is added. Reload discards local overrides.
  Reads merge per-key revisions. Writes are serialized in each document,
  snapshot-at-click and lifetime guarded. Across tabs the latest stored record
  wins, while active profiles remain local. Read/write errors are recoverable.
  Comparison keeps analysis running while hiding output and reduction matte.
  A temporarily unavailable paused source frame retries through the existing
  guarded timer; successful recovery does not repeatedly process a still frame.
  Run npm test and the visible profile and three-strength pixel tests on the
  foreground dev page. profile-fixture.js restores synthetic localhost data;
  the installable archive contains none of the fixture adapters or harnesses.

2.6.0 bounds, integration and icon lifecycle
  Schema 8 adds iconHidden/iconPosition without converting old geometry. The
  original values/defaults are retained. Text precision is 0.0001, independent
  of common slider bounds/nudges; unsupported geometry is rejected before Apply.
  R=0 has real corner regions in X/adaptive modes. S retains its sample-lattice
  cap. C retains its source request but caps both axes with one shared percentage
  at the native 65536-pixel maximum. Q's 8-sample floor is explicitly reported.
  Zoom input is positive and bounded at 32768 for this implementation; a separate
  unzoomed matte covers the original video for reductions and is removed on
  disable/error/destroy. No source video style or source pixels are altered.
  API 6 adds an implicit solution for stiff nonlinear spring/damping forces.
  The upstream float predictor/corrector remains in the well-conditioned path;
  three independent native reference profiles still use unchanged results.
  Monotone force terms bracket the implicit root between their centers. Double
  arithmetic and 56 bisections avoid 5/C and large-damping overflow. No Q, C or
  enabled flag is lowered as a numerical workaround. The existing final finite
  guard remains. Result slot 11 reports implicit steps for diagnostics.
  Native cutoff accepts 0.0001..120 and uses min(requested,fps/2). Nonnegative
  finite float damping is supported, including zero. Original trust/penalty,
  dimension, memory and frame-rate guards remain.
  PlayerUI positions icon and panel separately. Pointer capture, drag threshold,
  cancellation, keyboard move/reset, automatic control-strip clearance and
  viewport clipping use one layout. Preferences save without applySettings.
  Hidden-entry recovery uses the existing chrome.runtime panel message; local
  UI tests use an explicit test adapter, not an installed-extension claim.

2.5.2 compensation band
  overlay.js renders an even-odd SVG path between the source frame and its
  per-axis inset rectangle. Opposite bands may overlap; only drawing geometry
  is clipped, never the requested compensation. The band is fixed to output
  frame coordinates and shares its display transform, independently of the
  source translation, search range and visible-crop polygon.
  Node geometry tests and the browser overlay harness cover zero, overlapping
  edges, legacy axes, C greater than S, drafts, output pixels and recovery.
  A native smoothing-stage regression confirms accumulated compensation can
  exceed per-frame search. Core, settings and schema need no rebuild/change.

2.5.1 slider and color binding
  Nine numeric controls pair native range inputs with existing localized text
  inputs. Range min/max/step come from Settings.fields. Typed values retain their
  accepted precision; range movement uses nudge steps, and range keyboard steps
  preserve a precise starting offset. Invalid text leaves the last valid slider
  position; moving the slider can repair it. Changes use the existing draft path.
  overlay.js exports one parameterColors/parameterColor palette consumed by UI,
  SVG outlines/sample labels and the legend. Live inliers change fill/dots, not
  the R outline. No spatial box is invented for neutral non-geometric controls.
  dev/slider-harness.js covers synchronization, precision, bounds, localization,
  draft/storage separation, continuous input while playing, actual computed CSS
  color equality, live status separation, reset and legacy profiles. Programmatic
  event-stream checks are distinguished from native track-click/keyboard checks
  in validation/sliders-browser-2.5.1.json. No motion-core rebuild is required.

2.5.0 shared ratios and temporary SVG preview
  Schema 7 adds parameterModel, searchRange (S) and compensationLimit (C).
  New profiles are unified: R always defines the non-overlapping grid, S/C
  each multiply the source width and height, and Q controls sample dimensions.
  Search uses one common ceiling from all candidate sample lattices, then
  rounds each cell's offsets down. requestedSearch is retained across changes;
  effectiveSearch and searchLimited explain saturation. C keeps native soft
  limits. Native JS/WASM, correlation and smoothing calculations are unchanged.
  Existing profiles normalize as legacy and retain the old axis fields/units,
  geometry, search and sampling. simplify() creates a draft; equal X/Y ratios
  are retained, asymmetric values use clearly displayed new starting values.
  Old axis data stays stored but inactive in unified mode. There are no legacy
  width/height, X/Y search/limit or layout/search selectors in the new form.
  S6.25 follows the narrow-cell search at the former R50 default; C3 approximates
  the former 3.125/2.7778. These are engineering starting values, not native
  defaults or universal optimums. R50/Q25/zoom1 remain the starting choices.
  overlay.js owns an independent, pointer-transparent SVG and a 5000ms lifetime
  from the latest valid edit. Drafts use dashed outlines and cannot fabricate
  adaptive selections. Applied highlights come from the rendered frame's region
  IDs; geometry uses the actual rounded output translation. Q labels show sample
  dimensions without changing the region. Search rectangles represent offset
  bounds, C is explicitly soft, and zoom outlines represent visible source crop.
  Output and overlay share surfaceGeometry(). Crop polygons invert the real SVG
  screen matrix with the proposed relative zoom, clip the player viewport against
  source/canvas bounds, and account for letterboxing and CSS mirroring. SVG text
  is not part of VideoFrame input or the output canvas. Preview timers and nodes
  are destroyed with the controller, including extension invalidation.
  tests/overlay.test.cjs covers crop geometry, selection honesty and real timer
  logic; dev/overlay-harness.js exercises the form, preservation, actual output
  pixels, timing, transforms, source/player lifecycle and migration in Chrome.
  Scoped evidence: validation/overlay-browser-2.5.0.json.

2.4.1 lifetime and loading
  controller.js checks the captured Chrome runtime ID on scheduled/player/page
  work and before/after asynchronous loading/saves. Invalidation aborts pending
  loading, disposes the player and pipeline references, removes observers and
  event listeners, and prevents an old SPA callback from mounting again.
  Existing tabs require a refresh after extension re-enable; no scripting or
  host permission is added to reinject into arbitrary existing pages.
  renderer.js fetches WASM once, compiles bytes, then supplies a synchronous
  instantiateWasm hook. Errors propagate to the caller and clear the failed
  cache; a late failure from a disposed load cannot discard a newer module.
  The original JS/WASM/native build is unchanged. Node reference tests keep
  filesystem loading. Browser-loader tests also compile the actual WASM bytes.
  Player SVG markup is generated from src/brand/icon.svg by build-icons.cjs;
  node src/build-icons.cjs --player updates only the inline SVG without Sharp.
  The ordinary --check also verifies this markup matches the original artwork.
  tests/lifetime.test.cjs simulates invalidation at storage, WASM, playback and
  asynchronous readback boundaries. These deterministic tests do not claim to
  toggle the installed extension in Chrome. See validation/lifetime-2.4.1.json.

Runtime: desktop Chrome 120+ with VideoFrame, requestVideoFrameCallback,
WebAssembly SIMD. Tested on Chrome 156 / Windows with the included reports.
Other Chromium browsers are not separately qualified.
No Node, Python, compiler or local server is needed for normal extension use.

Icon artwork and rebuild (optional; no motion-core rebuild needed):
  src/brand/icon.svg is the original camera / stabilization vector source.
  npm run build:icons generates 16, 20, 24, 32, 48 and 128px PNGs plus the
  matching site/icon.svg. Rebuilding uses Sharp (this build: 0.35.5).
  Install Sharp locally, or set process-local NODE_PATH to an existing
  installation's node_modules directory. It is only a development tool.
  node src/build-icons.cjs --check verifies shipped PNG dimensions, source
  and file hashes, manifest paths and website artwork with Node alone.
  npm test and packaging include that check; no npm packages are needed
  to use the extension, check the shipped icons, or run the program tests.
  After npm run serve, /icon-preview shows actual sizes on light/dark panels.

Build prerequisites (official tools; not installed globally by this project):
  Emscripten SDK 6.0.13, CMake 3.24+ or 4.x, Ninja, PowerShell.
  https://emscripten.org/docs/getting_started/downloads.html
  git clone https://github.com/emscripten-core/emsdk.git <SDK>
  <SDK>\emsdk.bat install 6.0.13
  <SDK>\emsdk.bat activate 6.0.13 --embedded
  All paths below should be absolute. No persistent PATH edit is required.

FFTW:
  The matching source ZIP includes src/vendor/fftw/fftw-3.3.10.tar.gz.
  SHA256 56c932549852cddcfafdab3820b0200c7742675be92179e59e6215b340e26467
  Extract that archive to a build directory; it contains all original source,
  build inputs, licenses and documentation. The wasm build uses single
  precision, static linking, SIMD, no OpenMP/threads/Fortran or native DLLs.

From this project directory in PowerShell:
  .\src\build-fftw.ps1 -SdkRoot <SDK> -FftwRoot <EXTRACTED-FFTW> `
      -FftwBuild <FFTW-BUILD> -CMake <CMAKE-EXE> -Ninja <NINJA-EXE>
  .\src\build-core.ps1 -SdkRoot <SDK> -FftwRoot <EXTRACTED-FFTW> `
      -FftwBuild <FFTW-BUILD>
  Output: extension/depan-core.js and extension/depan-core.wasm.
  src/core-build.json records source/JS/WASM/library SHA256 and exported APIs.
  API version 5 adds per-region dimensions/search; only one fused
  ys_push_motion call advances smoothing for each output frame. The legacy
  ys_process path is kept and still checked against the native golden data.
  Production single-region processing uses ys_region too; 360 identical luma
  inputs compare its motion, trust and compensation with ys_process.
  Run build-core.ps1 under PowerShell; do not run em++.exe without the SDK
  configuration. The scripts set process-local EM_CONFIG/EMCC_TEMP_DIR.
  Set EM_CACHE to a writable task-local Emscripten cache when using a shared
  read-only SDK; seed it from the matching SDK cache including sanity stamps.
  PYTHONDONTWRITEBYTECODE=1 prevents writing Python caches into that SDK.

Automated tests (Node 20+; no npm packages required):
  npm test
  360 source-luma frames, including cuts and a long pan, compare the wasm
  estimator with the installed native DePanEstimate 2.12. Three parameter
  sets compare method=0 compensation with native DePan 2.15. The original
  native golden data, DLL hashes and input luma are in validation/native.
  These tests do not need AviSynth or the native DLLs to run.
  Native regeneration only: Python 3 + Pillow, installed AviSynth C API,
  DePan.dll, DePanEstimate.dll, ImageSeq.dll. See tests/native_reference.py.
  Neither the test nor the browser extension changes steady.avs.

Browser regression (synthetic media only):
  npm run serve
  Open http://127.0.0.1:8770/ in an active foreground Chrome tab.
  Click 製作測試片 once. It creates a local 12-second 1080p/30 WebM.
  Click each test button and inspect the JSON results. The test reads the
  actual output canvas: a fixed background landmark and an encoded source
  frame ID check stabilization and same-frame pairing independently.
  Deliberately using the prior compensation is a negative control.
  Other buttons cover forward/back/rapid seeks, pause, parameter changes,
  speed, video replacement, ads, original CSS, restricted frames and
  renderer failure/retry. The restricted-frame test intentionally serves
  synthetic media through localhost rather than 127.0.0.1; it does not
  bypass the browser's origin policy. Ctrl+C stops the local-only server.
  The region buttons cover 360p, 480p, 720p, 1080p, 4K and portrait VideoFrames,
  independently moving foreground, fixed corner graphics, actual output pixels,
  Q=25/50/100, profile migration, unit drafts, missing dimensions,
  range failures and an actual playing source resizing through captureStream.
  Analysis timings exclude fixture generation, decoding and output drawing;
  the separate playback/cadence tests measure the 1080p/30 live pipeline.
  The 216p/Q25 exploratory case remains a documented low-resolution tradeoff;
  Q50 reduces its error below the unchanged two-reference-pixel RMS threshold.
  The high-load recovery button injects a labeled 300ms read delay; it verifies
  continued output and recovery, not real CPU performance. Full records are in
  validation/sampling-browser-2.4.0.json, including initial failed observations.
  This development server is not used by the installed extension.

Algorithm scope and differences:
  The translation estimator uses original unnormalized single-precision FFTW
  cross-correlation and native peak/trust/subpixel calculations. Smoothing
  ports DePanStabilize method=0's nonlinear predictor/corrector, rolling base
  and soft travel limits. It is not a wrapper around an AviSynth DLL.
  src/depan_core.cpp records the deliberate fixes and removed features.
  Runtime analyzes and draws the same captured VideoFrame, in source-pixel
  units. Audio and the timeline remain owned by YouTube; output has one
  processing interval of delay. Cuts/low confidence reset compensation.
  A large moving foreground or textureless scene can still confuse the
  original global estimator. Rotation, optical rolling shutter, deliberate
  camera motion and zoom are not universally removable by translation.
  Border fill is black; no future-frame synthesis. PiP/casting uses original
  video. HDR output is not calibrated. Protected/non-readable media safely
  uses the original player. See 使用說明.txt for controls and tuning.

Spatial mode additions (2.4.0):
  Schema 6 keeps centerRatio, windowLayout and searchMode and adds sampleRatio,
  samplingMode and centerPriority. The central span is
  round(sourceSpan * R/100), the first side is floor(remaining/2), and the other
  side receives the remainder. Integer cells cover the complete source once.
  Modes assign spatial weights; zero-weight and smaller-than-32px cells are
  excluded. Existing profiles retain Custom geometry and Manual search, and
  original single-region profiles retain Source precision. No aspect ratio or
  prior resolution is guessed when migrating old window dimensions.
  New modes rasterize several source regions from the same VideoFrame into
  one sampling atlas, then convert RGB to Y. Each region retains raw preceding
  pixels and a cached FFT. Skipped adaptive cells rebuild their previous FFT
  when selected again. Different positions never share a preceding reference.
  R-grid cells do not overlap; legacy custom rectangles are deduplicated.
  Cross/X replace the old balanced/center menu, with a separate centerPriority
  flag. Old profiles retain fixed sampling until explicitly changed. Q scales
  each cell to integer sample dimensions, with an internal minimum of eight
  per axis; odd FFT dimensions and full 4K source sampling are supported.
  Adaptive selection chooses up to five of nine textured cells, with at most
  two per row/column and a 10% previous-selection preference.
  Each region uses the unchanged native peak calculation with a local floor
  max(0.1, trust/2). Fusion requires two agreeing cells, 60% of reliable votes,
  and rejects an equally strong disjoint rival. The agreement radius is 1.5
  sample pixels, with the source-proportion cap described below. Center priority
  doubles the central weight within that group;
  confidence weights are capped. Combined confidence is weighted mean trust
  times sqrt(inliers), capped at 100, and must meet the chosen trust threshold.
  This is an engineering heuristic, not a calibrated confidence probability.
  Each cell has its own forward/inverse FFT plans, dimensions, raw/spectrum
  history and search bounds. Bounded scratch buffers are shared serially.
  Automatic search uses each cell's quarter-window; manual source search is
  capped separately for each cell. Source-coordinate motion is compared using
  the RMS of both cells' sampling scales. Relative sampling also caps each
  comparison scale at sourceWidth/480 and sourceHeight/270, so a coarse sample
  does not merge distinct foreground/background motion in a small picture.
  Fixed compatibility profiles retain the prior tolerance. A single remaining
  cell uses the full native trust threshold.
  Legacy source-detail direct luma keeps the original valid-zero representation.
  New multi-region R grids retain the absolute confidence floor and spatial
  consensus but skip the individual temporal confidence-drop veto; unequal small
  cells otherwise reject coherent fast motion as cuts. Single/custom profiles
  keep that veto. Native math and method=0 remain independently tested.
  API mismatch fails with a localized extension/page reload message and uses
  original playback; it does not repeatedly execute a cached incompatible core.

Resource accounting (2.4.0):
  Native input/reference buffers are dynamically allocated, up to 8192 per axis.
  INITIAL_MEMORY=64MiB, ALLOW_MEMORY_GROWTH=1, MAXIMUM_MEMORY=2GiB,
  ABORTING_MALLOC=0. Core.activate refreshes input pointers after configuration;
  heap views are fetched when needed, never cached across growth. Reference
  ys_process allocates its previous-luma buffer lazily. Production uses regions.
  The builder compiles in work/build/core-stage, then copies completed JS/WASM;
  optimizer failure cannot expose its intermediate output in extension/.
  Sampling atlases use grid packing for relative R cells and bounded shelves
  for compatibility layouts. Work estimates include actual sampled dimensions,
  two steady-state transforms, up to three for changing adaptive selections,
  native buffers, readback arrays and a documented working-space allowance.
  Reference work is 1080p/R50/cross/Q25. Index values do not predict utilization
  or milliseconds; FFT factorization and source readback affect real timing.
  The overload stop was replaced by a nonfatal slow status and existing busy
  frame skipping. Gap tolerance follows observed processing duration; seeks and
  unexpected discontinuities still reset. Timing statistics restart on Apply.

License/provenance: GNU GPL v2 or later; see LICENSE.txt and NOTICE.txt.



UI/localization changes do not require rebuilding the motion core. Run
  npm run build:locales
after editing message catalogs, then npm test. See TRANSLATING.txt.
The browser harness also tests 12 languages, draft preservation, localized
validation, output recovery and compact 320x180 RTL layouts. Pixel readback
stress and ordinary playback cadence are separate observations; use the
Compare playback cadence button to record original and stabilized intervals.
Popup sizing must also be checked through the actual Chrome toolbar action.
An ordinary tab preview does not exercise Chrome's content-based auto-sizing.
For a narrow-start regression, preview at 128px viewport width and verify the
document still requests its full 332px width. Repeat at 332px and 800px in all
12 languages. The shipping popup and preview use the same CSS; the preview's
storage adapter prevents changing the installed extension's preferences.
An optional previous-release comparison can use process-local PORT,
STEADY_RUNTIME_ROOT (an extracted old extension), and STEADY_TEST_PAGE
(a matching fixed test HTML file). No installed extension is overwritten.

Packaged browser core
  node src/build-wasm-data.cjs regenerates extension/depan-wasm-data.js from
  extension/depan-core.wasm after checking src/core-build.json. --check verifies
  exact bytes and script ordering without writing. npm test and release packaging
  run this check. build-core.ps1 regenerates the data after rebuilding the core.
  For the network-denied browser regression, set STEADY_BLOCK_WASM_FETCH=1
  before starting dev/server.cjs, then use Test bundled core without network.
