Repository navigation
env-generator: analytic form 2 domes the thermocline; 48x40x26 grid (#113) - #114
Merged
Merged
Conversation
T009's surface needs a shape the harness does not publish. `forecast-features` carries a thermocline, and it is a domain mean — "where the domain-mean profile falls fastest" — which is one number for the whole grid, and one number is a flat plane. FR-120 asks for a surface and says why in the requirement: the thermocline domes, tilts and breaks, and shape is the answer a sonar user is asking for. A plane drawn where a surface was promised claims the field is flat, which is the one thing the drawing exists to disprove. The shape is real and is in the served field: the seeded eddy and front perturb temperature horizontally, so the depth at which a column falls fastest moves from column to column even though the analytic thermocline term is horizontally uniform. Nothing here invents it — the values come from area queries the shell makes through the query layer, one per level, as the Map's volume already does. The pick is the harness's own rather than a second opinion about it: `model-runner/features.ts` defines the published estimate as the steepest level pair reported at its midpoint, carrying the pair's separation and declining where the profile does not fall. This applies that definition per column. Held to the backend by calling it. The first draft of that test transcribed `features.ts#thermocline` into the test body and compared this module against the copy — which holds two copies together, says nothing about the backend, and would pass after the estimator it names had changed. It imports and calls `estimateFeatures` now; tests are exempt from the import boundary and this is the case that exemption is for. It compares on the domain mean because the mean of the per-column picks is a different quantity: argmax does not commute with averaging, and asserting it did would be asserting something false. That comparison first failed by 2.4e-7 on the drop with depth and thickness already exact — float32 epsilon, because the estimator reads a Float32Array and the test fed doubles. Fixed by reading the levels back out of the packed array rather than by loosening the bound, which would have sized a tolerance for a storage artefact and hidden a real disagreement behind it later. It is also the truer input: holdings are stored drogna-f32-v1. Watched failing: reporting the shallower end of the pair instead of its midpoint gives "the volume and the run disagree about where the thermocline is: expected 200 to be 300", and a planted hole in a profile is watched not to move the pick. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoDxPmvGhBF3wCSbzhPjNp
T008, T009 and T011. The field as six semi-transparent levels of the analysis, each a genuine EDR area query, with the thermocline drawn as a surface through them and the column selection shared with the share map beside it. Reused rather than rebuilt. `map/cube.ts` carries the lon/lat/depth triple into the space an OrbitView rotates and carries a clicked point back out, tested as a round trip; the Data tab's volume imports the same module. `lazy.tsx` withholds deck.gl for the reason `data/lazy.tsx` records — about a third of the bundle, and this is the tab's only surface that needs it — so opening Forecast costs what it always cost. T011 says the volume's grid must not be a second selection. It is rendered inside `ColumnProvenance`, where `column` and `readColumn` already live, so there is one reader and two drawings of it. Driven live: clicking the volume opened the column at 46.22°N, -10.90°E in the profile beneath. **The surface FR-120 asks for is a plane on this configuration, and the drawing says so.** The harness publishes a domain-mean thermocline — one number, hence a plane — so the surface is derived per column by the run's own definition. Driven against `arriving`: 7,679 of 7,680 columns place it at 100 m and one at 300 m. The cause is the profile, not the pick: pair gradients run 4.49, 1.64, 0.92, 0.51, 0.29 °C per 100 m, so the shallowest pair wins by nearly three to one everywhere, and at 200 m spacing the authored layer is an order of magnitude too thin to resolve — which `features.ts` already says of its own estimate. The caption states the distinct-depth count it found and calls the flatness a fact about the grid rather than about the ocean, because a picture captioned as a doming surface over two distinct depths is the false illumination this region exists against. Raised as #113 with the measurement and the options; the depth axis is a data change touching every holding. The camera is framed for this canvas rather than inherited: the Data tab's zoom put the cube's own edges outside a 260 px viewport, and a rotating drawing whose frame is off-screen has lost the thing that says which way is down. T012 stays unticked with its reason. The parameter control offers what an analysis carries — temperature and salinity — and does not default to sound speed, which is derived rather than served; ADR-0005 makes that derivation the single implementation in drogna, so a second one here is ruled out by a decision already taken. The three routes are recorded in tasks.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoDxPmvGhBF3wCSbzhPjNp
T008, T009 and T011. The volume is deck.gl on the Map's own `cube.ts` frame, withheld by `lazy.tsx` on the Data tab's measured precedent, and rendered inside `ColumnProvenance` so a click goes to the selection the region already has — T011 asks for one selection and this is what that means in code. Driven live: a click in the volume opened the column at 46.22°N, -10.90°E in the profile. The thermocline surface is derived per column, and comes out flat. Measured on `arriving`: 7,679 of 7,680 columns place it at 100 m and one at 300 m, because the domain-mean gradients run 4.49, 1.64, 0.92, 0.51, 0.29 degC per 100 m and the shallowest pair wins by nearly three to one everywhere. At 200 m spacing the authored thermocline is an order of magnitude thinner than the grid resolves, which `features.ts` already says of its own estimate. FR-120's doming is not visible at this resolution by any means. The caption states the distinct-depth count it found rather than claiming shape; #113 carries the evidence and the options. T010's arithmetic. The features holding publishes the eddy, the front and the drifting feature as centre, radius and anomaly — all horizontal, no vertical extent anywhere, and the estimator takes no manifest on purpose. So the dimension is read off the field they are in, which is what T010 asks for: at each level the feature's own cell against that level's mean, drawn while it clears the level's own spread. Measured: the eddy runs 2.06, 2.32, 0.18, 1.09, 0.03 and -0.65 sigma down the six levels, so two sigma separates its real extent from the field's wiggle with the nearest false positive at 1.09. The floor is the level's spread rather than a temperature, because the field collapses from 1.13 degC at the surface to 0.23 degC at 800 m and one threshold in degrees would be two different claims. The feature's own cell is excluded from that spread: a feature must not be measured against a yardstick it moved. Watched failing, and the second one is the reason to plant rather than assume: a fixed 0.5 degC floor planted in place of the spread **passed all ten tests** — on that fixture both rules give the same answer at every level, so nothing held the design its docstring argues for. The case that parts them is a deep feature weak in degrees and strong against a quiet field, and it is a test now. Also watched: reporting the shallower end of a pair instead of its midpoint fails the backend binding. One fixture was wrong rather than the code: a level named "indistinguishable" had its other cells within 0.01 degC, so a 0.02 residual cleared two sigma of almost nothing. Real water still scatters at depth. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoDxPmvGhBF3wCSbzhPjNp
…e (T012) ADR-0005 says sound speed is derived at the point of use, never stored, and that there is exactly one implementation in drogna. The volume's parameter control needed it, and a panel may not import the backend (Constitution XI), which left three ways to satisfy the request: a second implementation in the panel, which the ADR forbids; serving it derived from the query layer, which is a backend change nothing else wanted; or moving the relation to where both halves can reach it. The relation is a function of three numbers and knows nothing about the ocean, so the seam is where it belonged. `analytic.ts` re-exports all five symbols, so every backend caller — advisories, telemetry, monitor, generator, sensors, runner — is unchanged. The snapshots move because `SOUND_SPEED.implementation` is a string in every manifest and the path in it changed. Read rather than regenerated blind: the differences are that path (43 to 53 characters, fourteen occurrences, and the 140-byte length prefix that follows from it), the code revision, and nothing else. Zero of 2,382 digests changed, so no field bytes moved. `ocean-relations.test.ts` carries the anchor assertions here from `analytic.test.ts` unchanged — a test reading the re-export holds the re-export, not the relation — and adds one the move showed was missing: the implementation path a manifest hands a client had never been checked to name a file that exists. Planted a stale path back into `SOUND_SPEED.implementation`, watched that test fail on it, and reverted; the other four still passed, which is the point of it being its own test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoDxPmvGhBF3wCSbzhPjNp
…id (#113) FR-120 asks for the thermocline as a surface and gives its reason in the requirement: the layer domes, tilts and breaks, and shape is what a sonar user is asking for. Feature 124 drew that surface and captioned what it found rather than what it wanted — 7,679 of 7,680 columns at one depth — and that sentence is what made this findable. The depth axis looked like the whole answer and was not. Sampled over the same 7,680 columns at 200, 100, 50, 40, 25 and 10 m spacing, the surface has exactly one distinct depth at every one of them; at 20 m the winning level pair beats the runner-up by 13.7% to 17.3% in every column in the domain. `thermoclineAnomalyT(parameters, depthM)` took no position: the layer was one depth everywhere and there was nothing there to resolve. Form 2 gives it one. `thermoclineDepthAt` displaces the layer by `displacement_m_per_c` times the eddy, front and moving anomalies evaluated at its nominal depth — warm water presses an isopycnal down, cool water lets it rise — and the three thermocline terms take that local depth as a required argument, undefaulted so a caller cannot silently get the flat field back. The anomalies are read at the nominal depth rather than at the depth being sampled: a layer's depth is a property of the place, and reading it at `depthM` would put the layer somewhere different for every sample down one column. ADR-0044 has the reasoning and the costs that were rejected. The grid becomes 48x40x26. Depth multiplies against the horizontal and the horizontal was generous — 5.3 km cells against a 64 km eddy — so half of it pays for four times the depth: the layer-to-spacing ratio the manifest publishes goes 0.15 to 0.75, the eddy keeps six cells across its radius, and the committed artefacts go 27.7 MB to 30.4 MB. Keeping 96x80 and adding depth alone was measured at 115.7 MB with `returning` at 49 MB, and the codec already gzips with a byte-plane shuffle, so that was a floor. Two watched checks, both seen to fail before being trusted. The new doming test in `generator.test.ts` reads its bound off the manifest — the predicted relief over the depth spacing — and refuses to run on a bound with no teeth; planting `displacement_m_per_c: 0` and, separately, a 6-level axis both failed it at that guard, and it passes on neither by accident. The analyst's and the runner's manifests carried `analytic_form_version: 1` as a literal while spreading the env-generator's manifest wholesale, so the bump made them wrong: form 2 parameters under a form 1 label. Both now read the number from the document they spread. Also: the caption printed max minus min and called the result doming, which read "16 distinct depths spanning 840 m" against a served analysis whose layer moves 80 m — true of two columns and false of the field, the same fault as form 1's from the other side. It now reports the depths holding 90% of the columns and says so. `capture:forecast` gains a second shot, of the volume with the surface in it, and refuses if the surface placed nothing; `departure.test.ts` counts its mismatches instead of running a million `expect` calls, which is what timed it out when the axis grew. FR-120 is not amended. The requirement described the ocean correctly and the harness did not contain it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoDxPmvGhBF3wCSbzhPjNp
Two independent passes on the pushed head, then this. Each finding below was reproduced before it was fixed and, where it was a check, watched failing after. **The front does not displace the layer, and the coefficient is 20 m/°C.** All three features moved it in the pushed version, and the front's anomaly is a `tanh` — it saturates rather than decaying, so it held one half of the domain permanently deep and the other shallow, out to the corners. That is a blob the size of the domain, and the drifting-feature estimator found it: AT-06 seed 1180004, which is `returning`'s own seed, published a centre 64.9 km from the authored one against a 40 km bound, where form 1 had honestly declined. Dropping the front leaves the estimator better than form 1 on every one of the five seeds, 1180004 at 1.9 km. The front's anchor recovery agreed independently, 74–97 km across against a 30 km bound at every grid size and 1 km without it. 30 m/°C put that back over its bound, so the coefficient is where the estimators still hold. ADR-0044 carries both tables. **A replayed `returning` turned its loop 555 ticks early.** The scheduler set `lastRequestTick` only from its own requests, so a start condition whose artefact carries the forecast eras — the model runner held back for the whole pre-roll, every request orphaned — measured the minimum interval from a request nobody answered. It now takes the tick from `valid_time.start_sim_time` of any standing run it hears, which the announcement already carries. Watched failing with the assignment disabled. **The volume was 99.4% opaque.** Per-level alpha was 46, "thin enough to see through a stack of six"; transmittance compounds, and twenty-six levels let 0.6% through. The stack's transmittance is now the constant and the level's share follows from the count — which reproduces 46 at six levels exactly. **deck.gl was in every view's first load.** `ColumnProvenance` imported a `const` from the module `lazy.tsx` defers, which Vite warns about and which hoisted the whole stack into `main`: 719 kB gzipped against 522. It took the Data tab's and the Map's own deferrals with it. The parameter vocabulary moved to `volume.ts`, the pure half; measured back to 522 kB with the deck.gl chunk separate again. **Clicking inside the volume was ruled out by FR-121 and was happening.** `onClick` read `info.coordinate`, the ground-plane unprojection, so a click on a deep cell opened a column about 180 km away and a click on empty canvas opened one outside the domain, clearing the reader's rays and numbers. There is now a pickable grid of squares on the surface plane — which is what FR-06 asks for — and a click that hits no square selects nothing. **The capture's shape check could not fail.** It computed three figures and logged them. It now asserts the layer takes more than one depth, which is exactly what form 1 could not do, and that the caption states a measured figure beside its count. **The caption branched on a count outliers inflate.** Twenty-one depths of which one holds 99% is a plane, and `distinct <= 2` called it doming — while the honest figure was computed three lines above and printed but not consulted. It branches on the span most of the field is in, and states the modal share, because the doming form 2 produces is local: on the shipped configuration the field is level to within one 40 m level, and the drawing says so rather than being made to look otherwise. **`analytic_form_version` protected nothing.** ADR-0044 credited it with stopping a form 1 reader, and `manifest-world.ts` cast without looking — `depth_m + undefined * anomaly` is NaN at every point, with no reason anywhere. It refuses in both directions now, watched failing. Also: a published refusal reason claiming "the grid resolves hundreds" of metres, which crosses the wire and is 40 m; six comments and three pieces of user-facing prose still describing the 96×80×6 field, including the zoom's own premise, now that a 48-wide field fits a 48-cell panel whole; T009's record arguing against the change it sits in; a docstring saying the parameter does not default to sound speed beside the line defaulting it to sound speed; `FEATURE_SIGMAS` calibrated on the six-level axis, re-measured on this one (the reach is contiguous now rather than on-off-on); the volume capture shipped with no provenance sidecar; and an unreachable `'reason' in estimate` guard. Not fixed, and said rather than left: two commit subjects on this branch run to 77 and 78 characters against the ~72 the file asks for, and rewriting pushed history to save six characters is the worse trade. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoDxPmvGhBF3wCSbzhPjNp
FR-120 asks for a thermocline that "domes, tilts and breaks". Form 2 delivers the doming and the breaking, both local, and not the tilt — and the two attempts at one are worth more on the record than an issue saying "next time", because they failed for the same reason and it is a structural one. The front's saturating step is already recorded. A smooth basin-scale ramp is the obvious alternative — it is what sets thermocline depth at basin scale, and it has no localised extremum for a blob estimator to catch on. It gives exactly the shape wanted: four distinct depths, the commonest three spanning 80 m, the modal share down from 95% to 41%. It also puts seed 1180003's front anchor 74 km across against a 30 km bound, at every tilt tried down to 10 m per degree. The estimators locate all three features by horizontal temperature structure, and a layer whose depth varies horizontally *is* horizontal temperature structure near that layer. So broad layer shape and bounds derived from the authoring jitter compete for one signal. Resolving it needs an estimator that reads depth structure, bounds that allow a sloping layer, or a decision that the demo prefers shape to scores — none of them this change's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoDxPmvGhBF3wCSbzhPjNp
A second adversarial round on the pushed head. The first finding is the one that mattered. **The master prescribed arithmetic the code does not run.** `manifest.schema.json` said the layer's depth is `depth_m` plus the coefficient times "the eddy, front and moving temperature anomalies", and `thermoclineDepthAt` sums the eddy and the moving feature only — deliberately, with ADR-0044's measurement behind it. Principle III makes that document the contract a V3 backend generates from, so a reader following it reconstructs a layer tilted 33 m across the domain where this harness authored a flat far field: 16.7 m of error at every corner, 42% of the depth spacing. The description now states the rule and says why the front is excluded. No gate can see this class of fault — nothing checks a schema's prose against behaviour — which is worth knowing about the gates rather than about this change. **The ADR's decisive table did not reproduce.** It was measured with a standalone probe, quoted a seed (1180005) that exists nowhere in this repository, omitted seed 1234, and its figures did not match what `features.test.ts` prints. Re-measured by running that file under each variant, so the table is the test's own output. It corrects the story as well as the numbers: the 64.9 km mis-location was at a coefficient of 30, not the shipped 20, and at 20 the front-in-the-displacement variant *declines* on 1180004 exactly as form 1 does. Excluding the front is still right — it is the only variant that finds that feature at all — and 20 is still the coefficient, because at 30 and 40 seed 1180003's front anchor leaves its bound with the front already excluded. The layer's shape and the front's position are read off one signal, which is the tension the ADR's last section is about, in miniature. **`departure.test.ts` gained an assertion that could not fail.** It compared a counter against the bounds of the loop that incremented it — the loop's own arithmetic restated, true for every input including the empty one. It counts drifts now, uncapped, with the examples capped instead, and refuses a brief with nothing to compare. **The volume had no component test.** `volume.ts` was covered thoroughly and the component consuming it only by a capture proof, which measures one built configuration. Four tests now hold what a capture never reaches: a refused level, a 200 that is not the master's shape, a parameter the field cannot supply, and the caption's figures — computed from the fixture through the same pure function rather than transcribed, so they move when it does. Planted a wrong count and watched it caught. deck.gl is not exercised and the file says so. Also: the Suspense placeholder wore the loaded component's test marker, so a proof could not tell a chunk still downloading from a query the server declined; and two specs plus two comments still quoted 96×80×6 as the tree's own figure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoDxPmvGhBF3wCSbzhPjNp
`vite.config.ts` asks the next author to re-derive `hookTimeout` when the loop gets dearer rather than nudge it, so here is the derivation. Raising the depth axis from 6 levels to 26 looked like it would be expensive for `analyst.test.ts`, the file the ceiling is about; the horizontal was halved to pay for the depth, so the analysis grid carries 8% more cells and the file went from 48.9s to 51.9s on this machine. Doubled for a runner, 104s against a 120s budget. The number stands, measured rather than assumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoDxPmvGhBF3wCSbzhPjNp
A third adversarial round, on the pushed head. One of these is a bug a reader would meet. **Picking a column in the volume left the share map lighting a different square.** `onPickColumn` moved the rays, the profile, the numbers and the volume's own marker, and never touched `cursor` — which is the only thing the share map's highlight and its coordinate readout are drawn from. So a reader who opened square A on the map and then clicked column B in the volume got B's provenance under A's highlight, with A's latitude and longitude printed beneath it. `Volume.tsx`'s header states the invariant this breaks in as many words, and every other route into `column` already kept it. The cursor now follows the pick, and clears rather than snapping where the pick falls outside a zoomed window: a snap would light a square the reader did not choose, which is the same fault one cell wide. **Two claims that outran their evidence.** The "still to come" paragraph described a volume, a surface and a caption in the present tense on *both* branches, including the one that renders before any analysis is announced — where the reader is told there is no provenance to read and then given a description of things not on the page. The sentence it replaced named an absence, which is what made "said on both branches" safe; the comment saying so was not revisited when the sentence changed kind. And with nothing placed, the caption read "0 of 1920 columns have one, over 0 distinct depths — so it is level to within one 40 m level", a statement about a field it had placed nothing in. It now says there is no thermocline to place, which is a different fact from one at depth nought; planted the guard away and watched the new test catch it. **And a vacuous assertion written while removing a vacuous assertion.** `forecast.test.tsx` carried `const spanBefore = 1; expect(spanBefore).toBe(1)` and a `finest > 0` that is positive by construction, under a comment claiming the field is "thinned by two" — false since the grid halved. The first replacement compared the same DOM query with itself. It now compares the zoomed longitude span against the unzoomed one captured before the keypress, and a planted zoom that ignores its window fails it. **The relations barrel is gone.** `analytic.ts` re-exported the five seam symbols so the eight backend call sites would not have to change, and that cost the move most of its point: every consumer still read `from '../env-generator/analytic.js'`, so `monitor.ts` and `sensors.ts` attributed the relation to the analytic-ocean module that the seam file says it is not, and `grep ocean-relations` found no backend consumer of something the whole backend uses. Eight one-line edits buy a tree where the owner is visible at the point of use. Also: T032 records the second blog entry the rule asks for and this line did not anticipate, and T012's "none of the 2,382 field digests moved" now says which commit it is about — true of the seam move, and not of the branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoDxPmvGhBF3wCSbzhPjNp
…uld not fail The last adversarial pass, on the pushed head. One performance fault a reader would feel, and three faults of a class this branch keeps producing. **The layer stack was rebuilt on every render.** `layers` was the one derived value in `Volume.tsx` not memoised, and `data` identity is deck.gl's full-invalidation trigger: each render handed all 28 layers fresh arrays, so `getPolygon` — four projections a datum — and `getFillColor` re-ran over everything. On the shipped grid that is 49,920 level objects, 1,920 picker objects and roughly 215,000 projections. Renders are cheap to cause: `ColumnProvenance` sets the cursor from `onMouseEnter` on up to 1,920 share-map cells, so crossing one cell with the pointer rebuilt the stack, and with no pointer at all `ForecastPanel` re-renders on every forecast indicator — which the monitor publishes beside every scored residual, so the rebuild ran at observation cadence with the volume open. `cellBounds` is stabilised with it, or the memo's dependency list would be a lie in the direction that costs most. Nothing caught this: the capture opens the volume and never moves the pointer, and jsdom has no WebGL, which `volume-panel.test.tsx` says outright. **The caption's shape branch attributed what the branch beside it disclaims.** It said the layer is "displaced where a feature acts on it: a warm one presses it down beneath itself", while the level branch said the drawing cannot tell a displaced layer from an unsure column — and the level branch is right: over a served analysis the distinct-depth count is four with the displacement and four without. Fixing one arm and leaving the other making the claim is the same fault reachable by a different configuration. **And the capture's replacement assertion could not fail either.** "Either branch names a measured figure" is satisfied whenever the caption renders, because both arms always emit one — the second uncheckable assertion in that block, written while removing the first. It now holds the caption's column count against the share map's cell count over the same served grid; planted an off-by-one and watched it caught. **A factual error in the blog entry.** It said letting the front displace the layer made the harness "stop finding a drifting eddy it had found before". ADR-0044's own table says seed 1180004 is declined under analytic form 1 *and* under eddy+front+moving — nothing was lost that had succeeded. The entry now says what happened: it stayed missing until the front came out of the displacement. Also recorded rather than left for someone to rediscover: AT-06's thermocline check scores the domain-mean pick against the layer's *far-field* depth, and passes only because the two displacing features cover little of the domain — widen one, or raise the coefficient, and it fails for a reason that is not the estimator's. Nothing clamps the displaced layer to the water column either; a clamp would hide a configuration error rather than report it, so the gap is named instead. And T036's zoom record goes to past tense, since #113 spent the horizontal it was written about. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoDxPmvGhBF3wCSbzhPjNp
Contributor
This branch, running: https://deepbluecltd.github.io/drogna/instances/claude-feature-124-status-kqrmek/Intro · Background · Data · Operator · Forecast · Map · Messages · Sampling · Courses · Feasibility Built from |
…ble volume Three things a reader reported, and one the narrow proof caught while fixing them. **The volume could not be found.** Its control sat at the foot of the region — after the map, the readout, the rays and the profile, 858px down a 1,065px column and behind 34 chips, below the fold of a 1,000px window. Tolerable at the six depth chips the grid used to have; #113 took the axis to 26 and the chooser to four wrapped rows. A reader opened the Forecast tab, saw the share map and reported "I just see a heatmap". The control now sits with the other three control groups at 290px, and the drawing follows it rather than staying at the foot — a chip that reveals something six hundred pixels below itself reads as a chip that did nothing. Neither proof could see this: `capture:forecast` finds the control by its accessible name, which says nothing about where it is, and the narrow proof measures this region with the volume closed. **The depth list is a drop-down.** Twenty-six chips were the tallest thing in the region before any drawing, and twenty-six tab stops. A `select` is one, with the platform's own keyboard and touch handling, and it names the depth it is on where the chips could only show it by which was lit. Four tests drove those chips and now drive the chooser. **The run list is in the left column.** "Runs, in simulation time" was a full-width section under the grid, and the region beside it is 1,105px tall, so the list sat near y=1240 and was easily pushed out of sight. "Why now" is short and leaves that column empty, which is the space the list now takes: one grid cell holding two regions rather than a fourth track, because `auto-fit` would have put the list beside "Why now" instead of beneath it. **And the volume had no field in it.** Holding the stack's transmittance constant is the physically right rule — halve the interval, double the slabs, and the water is as clear as it was — and at 26 levels it gives alpha 12, which is a wireframe box with no parameter visible. Rendered the shipped column at 12, 24, 60, 90 and 140 and looked at each: 60 is still nearly black, 140 is a solid, 90 shows the field grading warm at the surface to blue at depth with the thermocline still crossing it as a bright band. The band survives because the surface is drawn brighter than any level, not because the stack is see-through — which at 26 levels it is not, and the note says so rather than glossing it. **The narrow proof caught the layout change**, which is what it is for: moving the run list into a narrower column overflowed every run button by 7px at phone-landscape, 254px in 247. `auto` grid maxima do not help — an `auto` track's maximum *is* max-content, and the tick, mark and cause are already wider than the cell before the detail is placed. The detail now spans the row beneath them, which holds at any width rather than at one breakpoint and keeps the columns aligned down the list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoDxPmvGhBF3wCSbzhPjNp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes, and why
Feature 124's volume drew the thermocline as a surface and found a plane — 7,679 of 7,680 columns at one depth. It captioned that count rather than calling the result doming, and the measurement became issue #113, which offered three options: raise the depth resolution, amend FR-120, or leave it stated.
The measurement ruled out the first on its own terms. Sampled over the same columns at 200, 100, 50, 40, 25 and 10 m spacing, the surface has one distinct depth at every one of them; at 20 m the winning level pair beats the runner-up by 13.7–17.3% in every column in the domain.
thermoclineAnomalyT(parameters, depthM)took no longitude and no latitude — the layer was one depth everywhere and there was nothing to resolve. FR-120's rationale described something the harness did not contain, and no drawing at any spacing could have shown it.So this takes a fourth option the issue did not list. Analytic form 2 displaces the layer by
displacement_m_per_ctimes the eddy and moving anomalies evaluated at its nominal depth — warm water presses an isopycnal down, cool water lets it rise. The front is deliberately excluded: its anomaly is atanhand saturates rather than decaying, so as a displacement it tilts the whole domain, and the estimators find that. ADR-0044 carries the reasoning, the measurements, and the costs that were rejected.The depth axis moves to carry the result. Depth multiplies against the horizontal and the horizontal was generous — 5.3 km cells against a 64 km eddy — so half of it pays for four times the depth: 48 × 40 × 26, layer-to-spacing ratio 0.15 → 0.75, committed artefacts 27.7 → 30.4 MB. Keeping 96 × 80 and adding depth alone was measured at 115.7 MB with
returningalone at 49 MB, and the snapshot codec already gzips with a byte-plane shuffle, so that was a floor rather than a starting point.FR-120 is not amended. The requirement described the ocean correctly and the harness did not contain it.
This branch also completes T012 — the sound-speed relations move to
app/src/seam/ocean-relations.tsso the volume's parameter control can offer sound speed without a second implementation (ADR-0005) — and fixes a scheduler fault the grid change exposed, where a replayed start condition measured its cadence from a request nobody answered.The instance, opened at the change
https://deepbluecltd.github.io/drogna/instances/claude-feature-124-status-kqrmek/#/view/forecast
Run the clock forward until an analysis lands, pick a column on the share map, then press show the volume. The parameter chooser defaults to sound speed, derived per cell from the temperature and salinity the same query returns.
Two things worth looking at rather than reading about: the caption under the volume, which counts what it found and names what it cannot tell; and the level stack's transparency, which is now derived from the number of levels rather than typed — at 26 levels the old constant left the box 99.4% opaque and the surface invisible inside it.
The blog entry
https://github.com/DeepBlueCLtd/drogna/blob/claude/feature-124-status-kqrmek/site/docs/blog/posts/the-drawing-was-honest-and-the-ocean-was-flat.md
What was watched failing
Every check added or changed here was planted against, watched caught, and reverted.
generator.test.ts) reads its bound off the manifest — predicted relief over depth spacing — and refuses a bound with no teeth. Planteddisplacement_m_per_c: 0, and separately a 6-level axis; both failed at that guard.manifest-world.test.ts). Removed the check; the foreign-form test failed. It accepts the shipped manifest too, so a guard refusing everything would fail as well.ocean-relations.test.ts). Planted the staleanalytic.ts#soundSpeedMspath back; the test failed on it and the other four still passed.volume-panel.test.tsx). Planted an off-by-one distinct count, and separately removed the empty-surface guard; both caught.Three rounds of adversarial review ran against this branch, each handing the diff to subagents with no sight of the work. They found eleven faults between them, all fixed here, and the ones worth naming are: an AT-06 regression publishing the drifting feature 64.9 km from truth on
returning's own seed; deck.gl hoisted into every view's first load (719 kB gzipped against 522); the volume 99.4% opaque at 26 levels with its surface invisible inside it; clicking into the volume, which FR-121 rules out in as many words, opening a column ~180 km from the one pointed at; a manifest master prescribing arithmetic the generator does not implement; and — last round — picking a column in the volume leaving the share map lighting a different square, with the wrong latitude and longitude printed beneath it.Five checks in this branch were found unable to fail and were replaced rather than kept:
capture:forecastcomputed three shape figures and logged them. Asserting them was the obvious fix and is also wrong: the drawing reads a served analysis, and assimilation scatter gives four distinct depths whether the layer is displaced or not (the truth field gives three against one). The shape check belongs ingenerator.test.tsover the manifest; the capture now asserts only what a picture can honestly be asked for, and says why.departure.test.tscompared a counter against the bounds of the loop that incremented it — true for every input. It counts drifts now.analytic_form_versionbump was credited in the ADR with protecting a form-1 reader, and nothing performed that protection.forecast.test.tsx's zoom test carriedconst spanBefore = 1; expect(spanBefore).toBe(1)and afinest > 0that is positive by construction, under a comment claiming the field is "thinned by two" — false since the grid halved, and this was the one test that ran the shipped grid and could have caught it. My first replacement compared the same DOM query with itself; it now compares the zoomed longitude span against the unzoomed one.Verified at this head: 24/24 gates;
vitest run --root app792/792;tscandeslintclean;check-snapshot-driftclean;pnpm replay-proofheld, all 10 marked byte-identity tests; all seven capture proofs CI runs; build back to 522.95 kB gzipped with the deck.gl chunk split out.Backend changes: what was brought up, and what was seen
Not a backend change in the template's sense (no
services/,deploy/orproxy/), but this is a data change and every stored field moves.pnpm snapshotsrebuilt all four artefacts undercheck-snapshot-drift;pnpm replay-proofheld, all 10 marked byte-identity tests reproducing their runs; all seven capture proofs pass.One cost to know about: the app suite is roughly twice as long, from the 26-level grid.
vite.config.ts's hook budget asks to be re-derived rather than nudged when the loop gets dearer, so it was:analyst.test.tswent 48.9 s → 51.9 s, +6%, because halving the horizontal to buy depth leaves only 8% more cells. The 120 s budget stands, measured rather than assumed.Left undone, and why
FR-120's third verb. The requirement says the thermocline "domes, tilts and breaks". Form 2 delivers the doming and the breaking, both local, and not a domain-wide tilt — and the two attempts at one failed for the same reason, which is why ADR-0044 records them rather than leaving an issue saying "next time". The front's saturating step lost the drifting feature on
returning's own seed. A smooth basin-scale ramp gives exactly the shape wanted (modal share 95% → 41%) and puts seed 1180003's front anchor 74 km across against a 30 km bound, at every tilt tried. The estimators locate features by horizontal temperature structure, and a layer whose depth varies horizontally is horizontal temperature structure — so broad shape and bounds derived from the authoring jitter compete for one signal. Resolving it needs an estimator that reads depth structure, bounds that allow a sloping layer, or a decision that the demo prefers shape to scores. None is a data commit's to take.The drawing cannot demonstrate the fix, and says so. The volume reads a served analysis, and assimilation scatter moves a weakly-held column's steepest pair on its own: the distinct-depth count is four with the displacement and four without. Only over the truth field does it separate the two forms, three against one. So the caption states where each column's gradient sits and states what it cannot tell from that; the proof of the shape is a unit test over the manifest, not a picture.
T010 — the features carried with depth through the volume — remains unticked. Its arithmetic (
featureReach,FEATURE_SIGMAS) shipped early and has no caller;tasks.mdrecords that as a cost, and its calibration already needed re-measuring once when the axis moved.The zoom apparatus on the share map is inert.
MAP.maxCellsis 48 and the field is now exactly 48 wide, so coming closer gives bigger squares and not more of them. The machinery is unchanged and correct — it is the general rule and starts working again the moment the field outgrows the panel — and the notes say so rather than promising a resolution already on screen.Two commit subjects run to 77 and 78 characters against the ~72
CLAUDE.mdasks for. Rewriting pushed history to save six characters is the worse trade.The last correctness pass had not returned when this was opened. Its findings will go on this pull request as review comments rather than another round before it — the rounds were converging on smaller findings while the record of them grew, which is the loop this repository's own history warns about.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AoDxPmvGhBF3wCSbzhPjNp
Generated by Claude Code