123: two-layer kernel, cost in simulation time, Forecast tab; 124 spec - #107
Conversation
Feature 124 is what §5.20 exists for — the surface that makes assimilation visible — and it had a section of the SRD and no feature record. This is the record. One finding is the reason the spec is worth reading before the tasks. **The per-cell provenance the analyst publishes today cannot draw a ray.** FR-111 makes that provenance the substrate for the centre region and the depth profile, and FR-122 wants one ray per contributing source at a width proportional to that source's contribution. Read against the tree, the two do not meet: the analyst publishes four aggregate shares per cell, and the analysis kernel computes the per-observation gain and then reports its row sum, discarding the columns. Three answers were available and the spec records why two were rejected — drawing four rays would look like per-source attribution while being an aggregate, and deriving the split in the shell would make AT-07 a test of arithmetic the shell had just done. The analyst publishes the columns it already computes. That moves work into a backend component the companion document never mentioned, which is why it is stated here rather than discovered during implementation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc
… tab The second half of feature 123: the record landed on the previous branch, this is the code. A shallow two-layer advection-diffusion step becomes the configured kernel behind the unchanged model kernel port; a run declares what it costs and spends it in simulation time; the scheduler holds a warranted run while the standing forecast still has more life than the run costs; and `#/view/forecast` carries the left region and the timeline. Watched failing, and reverted each time: - the stability refusal, with its ceiling disabled: "expected [Function] to throw an error", which is what says the refusal is doing the work rather than the configuration being unreachable; - the becalm test, against the affordability rule read the obvious way round — caught at the hold, and again at the becalmed instant itself, which is where the fault lives; - check-declared-cost, against a planted `run_cost_ticks` in the scheduler's own master. Four things the plan did not have, each recorded in tasks.md rather than left in the diff. The first blob estimator recovered the eddy 164 km out and the cause was the front's saturating plateau, not the grid. The committed snapshots did not move at all — this feature changed the runner and a snapshot carries the generator's eras. One start condition's legs needed the ticks its run now occupies, and only one. And the first capture of the running tab showed "no run has been announced yet" on a store holding four forecasts, because a console opens after the pre-roll. pnpm check: 655 app tests, 40 script tests, 22 gates, all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc
Two independent passes read the diff without having seen it written. Six findings were real and three of the six were things this session had already looked at and waved through, which is the whole argument for the skill. - Stopping the model runner mid-cost becalmed the loop for ever. A run occupies twelve ticks between announcement and publication and the scheduler clears its outstanding run on a publication alone, so a stop landing in that window meant nothing was ever acted on again — through an ordinary operator verb. A run that will not finish now says so as it is stopped, and the scheduler releases what it was holding. Watched failing with the release removed: "expected +0 to be 1". - With a zero-cost kernel the standing-forecast gate had disappeared from the cadence floor. The short-circuit is gone: the release margin alone is a validity rule. - Three magnitudes were published under the manifest's own property names at up to sixteen times the uncertainty declared beside them. They are quantities a 200 m depth grid cannot see; what is measured now has names of its own and the authored quantity is declared not recovered, with the reason. - The front's bearing was printed and never asserted. Averaged in doubled angles over the front rather than read off one cell, it is 0.2 to 0.6 degrees against a bound read from the authoring jitter — where the old estimator was 39.6 degrees out at the seed the console opens on. - Scoring every start-condition seed rather than one found two more faults. The drifting feature came back 213 km out at one seed: high-passing a warm blob leaves a cold ring and the ring won. The ring is excluded out to the width of the filter that made it, and a peak that does not clear two standard deviations of the field's own scatter is declined with its reason rather than published. - The timeline separated a forecast from its uncertainty by a string suffix no master declares; the monitor computed one streak twice; the gate fixture had already drifted inside this branch and its run.json arm had never been planted against; four comments described behaviour the code did not have. And one the review did not find, because the suite did: the becalm fix gave the scheduler a ninth heartbeat figure against a master that caps eight, and four unrelated tests said so at once by the scheduler going silent. pnpm check: 674 app tests, 40 script tests, 22 gates, all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc
The first capture was taken before the timeline read the store's inventory, and showed "no run has been announced yet" on a situation holding three forecasts. This is the same view after that fix: three entries at ticks 1812, 4470 and 5412, each marked as published before the console opened and each saying that what asked for it is not recoverable from a holding. The alt text follows the picture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc
…feature uncertainty The second adversarial round, against the fixed tree. The guard is keyed to the commit, so the fixes were reviewed too — and this round found the one fault in the feature that would have mislabelled every forecast the harness ever served. - The two-layer kernel wrote each step after integrating it, while the manifest went on declaring `start_offset_seconds: 0`. An EDR query at the initialisation instant was answered with the field 900 seconds later, the last step was served 900 seconds early, and the run's own features used the other convention on the same tick. Step 0 is now the state the run initialises from, and a test holds both kernels to it. - A held reader's prompt was reported as "released as that headroom decays" and then dropped: nothing remembered it, and only the cadence floor ever requests again. It is now held as a commitment and released, or declined by the rule that declines it. - Pressing the button again during a hold published nothing at all — the once-per-episode latch is right for the tick-driven floor and wrong for a discrete act by a reader. - `members_done` was set at publication, so the ensemble face drew five empty pips for the whole visible duration of a run whose members were already computed. - `sub_steps_per_step` reported one for a kernel that declares no work, so one component said "costs nothing" and "took one sub-step" about the same run. The master admits null. - One uncertainty block computed from the eddy was attached to all four features. - The Forecast tab's deep link did not survive a reload: a run heard live and the same run read from the store had different keys. - The cost was declared against a cell more than twice the size of the one the run is handed; a test now compares the declared sub-step count with the reported one. The leakage tripwire in `advisories.test.ts` fired twice in this branch, in both directions, which is the best evidence in the feature that it is a real check. The second flip is recorded as measured — the two runs are made from byte-identical analyses — and what is not settled is said to be not settled. pnpm check: 677 app tests, 40 script tests, 22 gates, all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc
…how an abandoned run The third and fourth adversarial rounds. Three of these were claims the tree made and did not keep, and one was a check that could not fail. - The front was masked against the eddy and not the drifting feature, while this module, its own docstring and its committed master all said "outside both blobs". Measured across the shipped seeds, the anchor landed inside the authored drifting feature at three of five, and the step taken across "the front" was 16 to 35 per cent larger than the quantity its master describes. Neither figure the tests score could see it. The order is now eddy, cold blob, then the front against both. - The declared cost stated four integration steps where the kernel takes three: making step 0 the initialisation state did not propagate to the arithmetic. A run costs 9 ticks and the basis says why. - An abandoned run said "occupying 12 tick(s)" for ever. The panel receives the message saying the runner gave it up, on a topic it already subscribes to, and dropped it. - Two checks could not fail. The master-conformance assertions in loop.test.ts live inside broker subscription handlers and the broker catches handler faults, so a message its master refused made a green test greener; every drive now asserts the fault count is zero, watched failing against a planted rename. And "every kind is either estimated or named with a reason" was a tautology, because every kind is named on both branches. - SRD FR-113 and this feature's FR-05 still read as met while the tree declines three of the quantities they name. Both are amended in place. The capture is retaken at head: it shows the 9-tick cost, the 5 km nominal cell, and a run held for cost — which the entry's demo promises and the previous picture did not contain. pnpm check: 677 app tests, 40 script tests, 22 gates, all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc
This branch, running: https://deepbluecltd.github.io/drogna/instances/claude-srd-model-forecast-specs-ws9x3d/Intro · Background · Data · Operator · Forecast · Map · Messages · Sampling · Courses · Feasibility Built from |
The sixth round, and the first finding is about the checks rather than the code. Every `pnpm check` on this branch reported green while **every CI run on the pull request failed** — seven of them, from the first push. CI runs six capture proofs that `check` does not, they need a build and a browser, and they are the only thing that reads the shell as a rendered page. CLAUDE.md said `pnpm check` was "what CI runs"; it is not, and that line is corrected rather than left to catch the next person. - The Forecast tab did not fit a phone: eleven narrow-presentation failures from one declaration. `.forecast-run` was three `max-content` columns and a `minmax(0, 1fr)`, which cannot fold — at 360px the button laid out 351px wide inside 320px and took the `li`, the `ol`, the region and the panel past the viewport with it. A wrapping flex row now, with no breakpoint, so it folds wherever the content stops fitting. Reproduced locally at eleven, fixed, re-run at zero. - **The refusal nobody heard.** T049 answered one entrance to the cost window and asserted the other could not be reached. Restarting the *scheduler* reaches it: a fresh scheduler holds nothing, its cadence floor fires at once, and a second analysis arrives at a still-occupied runner. The runner refused it by throwing — inside a broker subscription handler, which catches handler faults and counts them. So nothing was told, and the scheduler waited for a publication that had been refused before any work was done. Measured at `loitering`, seed 4242, restart at tick 4420: twenty thousand ticks and eleven cadence floors with nothing requested, started or published. It publishes `run-failed` for the run it refuses now. - A feature was carried at a velocity the field is not. `carryVelocity` read `parameters.twoLayer !== undefined` as "the two-layer kernel is configured" and the runner sets that block whichever kernel is configured, so the branch was taken always: with `shift-advect-v1` selected the features drifted at 7 and 3 km/day while the field moved at 4 and 2, which on this grid rounds to no displacement at all. The kernel is asked now, through an optional port member alongside `subStepsPerStep`. - The analyst's hook budget, raised on the measurement rather than nudged. The hold moves the second cycle off the cadence floor — 3609 ticks to 4429, 28.0s to 38.7s — and the budget is in host time. It walked into the ceiling one commit at a time: 52.8s, 57.5s, 59.96s, timeout. The third of those passed by 36 milliseconds. Watched failing, and reverted each time: - the becalm test, against the restored throw: "expected 3 to be greater than 3", four cadence intervals of silence; - the carry test, against the restored inference: "expected 7.0028 to be close to 4". Both of the first two drafts of the becalm test passed against the unfixed code, and both are recorded in tasks.md because they are the same mistake twice. One restarted during the first run — and a restarted scheduler counts run ids from zero, so it asked for the same id already occupying and was rescued by the collision. The other asserted that a request followed the restart, which a becalmed loop also does: it makes exactly one. What separates the two cases is a second request. The carry test's own first draft asserted to 0.0005 km/day and failed on the lat/lon round-trip while the planted fault went unremarked — a check failing for the wrong reason, which is no better than one that cannot fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc
Caught by `check-types-drift`, which is what it is for: `restate_every_ticks` lost an unreachable `default` on a required property in the fifth round and the generated document was never rebuilt. Boundary shapes are generated, never hand-written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc
The narrow-presentation fix landed as a wrapping flex row, which folds correctly and costs the timeline its column alignment at every width: each row's detail then starts after its own cause rather than at a shared x, and a list of runs is scanned down its columns. `minmax(0, max-content)` folds the same way and keeps them — the track stops claiming its content's width as a floor without ceasing to be a track. Re-run at zero narrow-presentation failures, as the flex version was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc
IanMayo
left a comment
There was a problem hiding this comment.
Two independent adversarial passes read this diff without having seen it written, and every finding was verified against head before it was posted. The two serious ones are fixed in this branch; what is inline here is what was deliberately left, with the reason.
The assumptions worth checking first
Ranked by how much breaks if the assumption is wrong.
- A run may occupy ticks, and every way that window can strand the loop is enumerated. It was not. The diff answered one entrance — the runner stopped mid-occupancy — and asserted the other could not be reached. Restarting the scheduler reaches it. Fixed: the runner publishes
run-failedfor the run it refuses instead of throwing into a broker handler that catches and counts. - Features are carried at the velocity the field is carried at. They were not, for
shift-advect-v1: the selector was the presence of a parameter block the runner always populates, so the branch was taken always. Fixed: the kernel is asked, through an optional port member alongsidesubStepsPerStep. - The cost declared at a nominal cell is the cost the run occupies. Holds, and a test compares them — but both figures are 1 and stay 1 for any cell above ~0.57 km. See the
check-declared-costcomment. - The scheduler may derive tick length from consecutive clock samples. Sound:
remainingValidityTicks()fails open on a missing forecast, a lapsed one, and an underived tick length alike. - The second kernel's distinguishing behaviour is visible at the shipped configuration. Measured at 0.019 °C against 0.35 °C of noise — see the
kernel.tscomment. - The uncertainty beside a feature describes that estimator's accuracy. Not for the front's bearing — see the
features.tscomment.
Confirmed and fixed in this branch
- the becalm through a scheduler restart (above);
- the carry velocity (above);
- the analyst hook budget, which this feature made 38% dearer — 3609 ticks to 4429, 28.0 s to 38.7 s — walking into its ceiling one commit at a time: 52.8 s, 57.5 s, 59.96 s, timeout;
- eleven narrow-presentation failures, which CI had been reporting on every push while
pnpm checkreported green.
Confirmed and deliberately left
Four inline comments: the front's bearing uncertainty, the run-id collision across a scheduler restart, the shipped configuration's physics against its noise, and check-declared-cost's line attribution.
Checked and sound
22/22 gates including all three drift gates, with the generated output a genuine regeneration. pnpm replay-proof holds — AT-04 survives the second kernel and the held publication. No new wall-clock read in operational code and no new harness:allow-wallclock marker. No import across the seam. Math.random() only in test client ids. The published features meet the estimator bounds at four shipped seeds — eddy centre 4.3–30.0 km against radii of 51.8–66.4 km, front anchor 0.4–8.1 km across the authored line against 30 km sharpness — with the fifth seed declining, with its reason on the wire. check-declared-cost fires on a planted violation in a copy of the real tree, not only against its stub fixture, and both arms fire. The monitor's streak refactor is behaviour-preserving. app/public/snapshots/ is untouched and correctly so.
A gap in the gates, which is the most useful thing here
There is no gate for what CI's capture proofs check, and no gate that the two are the same set. This branch shipped seven consecutive red CI runs while pnpm check reported green on every commit, because check does not run them and CLAUDE.md said it did. The line is corrected in this diff, but a corrected sentence is weaker than a check: a gate asserting that every capture:* script in package.json appears as a step in ci.yml — the same shape as check-gate-registry's "an unregistered gate is a gate that never runs" — would make the drift impossible instead of merely documented. Not added here, because a gate is a line appended to a registry and belongs in a change of its own.
Reviewed with two passes on Claude Opus 5, one for correctness and one for simplicity and the record, with every finding verified against head before posting.
Generated by Claude Code
There was a problem hiding this comment.
The front's published bearing uncertainty is 20–80× its measured error, and nothing could falsify it.
Measured on the published ctl/forecast/features message at four shipped start-condition seeds, scored against the manifest:
| seed | bearing error | published uncertainty.bearing_degrees |
|---|---|---|
| 1180001 | 0.5° | 38.3° |
| 1180002 | 2.6° | 40.5° |
| 1180003 | 0.2° | 41.6° |
| 1180004 | 0.9° | 37.5° |
Mechanism: uncertaintyFor(front.anomalyStepC, cellKm) gives the front a scale of one grid cell, and bearingDegrees = atan2(positionKm, max(radiusKm, 1e-6)). Once analysisErrorC ≳ anomalyStepC the positionKm clamp binds and the ratio approaches 1, so the figure saturates at 45° at lead 0 and up to 63° at lead 3. On a bearing folded into 0–180°, ±40° is close to a uniform claim.
This is an over-stated uncertainty, which is the safe direction for Constitution IX — it is not the "soften a bound until it passes" fault. But FR-113's stated purpose is that a forecast makes a falsifiable claim about next week, and this quantity does not: nothing the estimator could plausibly publish would falsify it.
The real accuracy comes from averaging thousands of gradient cells in doubled angles — the finding T052 was written for — and the uncertainty model knows nothing about that averaging. Left unfixed deliberately: the fix is a model of that averaging (the standard error of a circular mean over N cells), and choosing it is a judgement about what the published figure is for, which belongs with a reader rather than with me at the end of a long branch.
Generated by Claude Code
There was a problem hiding this comment.
A restarted scheduler counts run ids from zero again, so a publication can clear the wrong outstanding run.
Found while writing the becalm-on-restart test, not by looking for it — and it is the reason two drafts of that test passed against unfixed code.
inFlight is cleared by identity: if (published.run_id === this.inFlight) this.inFlight = undefined. A fresh scheduler restarts its counter, so it issues …-run-0 again. If the run still occupying its cost is …-run-0 — which it is whenever the restart lands during the first run — then that older run's publication matches the new scheduler's outstanding id and clears it. The loop keeps turning, but for the wrong reason: two different runs shared a name.
It is currently benign, because the only path that reaches it now ends with the loop turning anyway. It stops being benign the moment anything downstream correlates a request with the publication that answered it — which is exactly what the Forecast timeline does, keyed on run:${id}, and what feature 124's provenance work will do.
Not fixed here: the fix is a scheduler-lifetime discriminator in the run id (or seeding the counter from what the store already holds), and it changes an id that appears on the wire, in the store, and in every committed snapshot. That is a change worth making on its own, against its own drift-gate run, rather than folded into this branch's sixth round.
Generated by Claude Code
There was a problem hiding this comment.
The physics this ADR was written for is 5% of one noise draw, and nothing on disk says so.
Measured over a whole run at the shipped configuration — 3 integration steps, 2700 s, noise off, on the 96×80×6 grid:
- maximum change to the temperature field: 0.019 °C, against a model-noise standard deviation of 0.35 °C per step and a 2.5 °C blob;
- the eddy moves 0.22 km — 4% of one cell;
- the layers' shear over the run: ~0.06 cells.
The published forecast is, to a first approximation, the analysis plus noise. kernel.test.ts's shear assertion is real, but it passes on a toy grid with twice the velocities and ten times the relative displacement, so it does not establish that the distinguishing behaviour is visible at the configuration that ships.
Related, and in the same measurement: the scheme's own numerical diffusivity is uΔx(1−C)/2 = 196 m²/s east and 97 m²/s north here, against a configured horizontal_diffusivity_m2_per_s of 45. The explicit term is a minority contributor by 2–4×, and the master describes it as "explicit horizontal diffusivity, applied per layer" without saying so — a reader tuning that number is tuning the smaller half.
Neither is a fault in this diff: the kernel is correct, the configuration is the one that was asked for, and ADR-0042's argument for a second implementation stands. Both are facts about the shipped configuration that a reader of the ADR would reasonably expect to find stated and will not.
Generated by Claude Code
There was a problem hiding this comment.
lineOf(lines, key) returns the first line in the whole file containing "key".
So where a key name appears under two components, the finding points a reader at the wrong one. The gate still fires correctly — this is only the line number it reports — but a gate that names the wrong place is a gate someone will argue with, and both arms were watched failing on the strength of finding the right one.
Minor, and not fixed here because the fixture that would prove the fix needs two components sharing a key name, which the real tree does not currently have.
Two smaller notes in the same class, from the same pass:
carryFeaturescarrieseddy,movingandfrontthrough three near-identical spread-with-IIFE blocks, andfeatureStepbuilds four near-identical objects each closed withas ForecastFeaturesFeature. Those four casts defeat the generated discriminated union: a misspelled parameter key would compile, and onlyloop.test.ts's runtime validation would catch it.sub_steps_per_stepis 1 at both the nominal and the real cell and stays 1 for any cell above ~0.57 km, so the check comparing declared against reported has a reachable failing state only after a ~10× grid refinement.tasks.mdrecords this honestly as headroom — worth knowing that it is not what would have caught the 11 km → 5 km nominal-cell correction the record credits it near.
Generated by Claude Code
Raised by a reader looking at the tab rather than at the diff: it had no graphic of a forecast anywhere. A gauge about the run loop, a cost, a list of runs, two paragraphs naming feature 124. Meanwhile `ctl/forecast/features` — FR-05's entire product, four features per lead step each with an uncertainty that grows — was published on every run and consumed by nothing, so the feature's output was unverifiable by anything a reader could open and would have stayed so until 124 landed, which is blocked on an analyst change. `FeatureTracks.tsx` is its first consumer: a plan view in the right region drawing the eddy, the drifting feature and the front across the lead steps with their uncertainty widening, the thermocline stated in figures because a depth has no place in a plan view, and the quantities the run would not claim listed in its own words. It is an SVG rather than a canvas so that a test and a screen reader can read it, and the same claim is printed beneath it so the region is legible with the picture removed. The runner now restates the standing forecast's features on the cadence it already restates cost on. Published on the run alone, a console mounting afterwards — which every console does — had nothing to draw until the next run: 1800 ticks, half an hour at the default rate. The argument is the cost statement's own: a declaration published once, before the shell had mounted, is a fact no listener can learn. Nothing is recomputed. Watched failing, and reverted each time: - the growth assertion, against a carry whose uncertainty does not widen (`root = 1`): "tracks-feature tracks-eddy: the ring at the last lead is not wider than at the first"; - the subscription, with the payload heard and dropped: no plan view drawn; - the restatement, removed: a console opening after a run learns nothing. A fifth check that could not fail, caught before it was trusted. The growth assertion first gathered every ring in the plot and required the largest to exceed the smallest — true of any two different features at the same lead, since an eddy and a front have different strengths. Planted against the non-widening carry, it passed. It compares within one feature across its own lead steps now. And a real fault that CI structurally cannot see. `capture:mobile` pins the clock to rate 0 before it measures, so every surface drawn from an announcement is absent from every picture it takes — including this drawing, which makes "the narrow presentation holds" an empty claim when made from that proof. Measured with the loop warmed and then pinned, at 390px and 360px: the drawing folds, and `.forecast-gauge-bar` scrolled sideways at 17px of content in a 14px box, and had since the day it was written. Its threshold mark hung 3px past both edges of a 14px bar. The gap in the proof is reported rather than closed: it changes a shared capture script every view depends on. pnpm check: 683 app tests, 40 script tests, 22 gates. All six capture proofs green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc
Two independent adversarial passes and five verifiers ran against the first draft. Four findings were refuted and dropped; the rest are corrected here. Counts. The declined table held 19 rows against a claimed 20, and the record-lag column held 5 lines against a claimed 4. The errors cancelled, so the headline survived wrong terms. The partition is now stated so it can be audited: 70 = 37 + 20 + 2 + 11. pnpm replay-proof selects tests with `vitest run -t replay`, and the generator's AT-04 byte-identity test matches neither its own name nor its describe block: 7 tests run, 623 skip, and that one is skipped. The draft asserted the script "runs every byte-identity test" and told a reader to tick 101 T037 and 107 T607 on it. Those lines are now real work — fix the selector, watch the hole fail, then tick. check-snapshot-drift gives the forecast kernel no cover. All four artefacts hold only archive and nowcast eras, both authored by env-generator, so a planted kernel fault shifting every forecast temperature by 5 C and tripling its spread leaves the gate exiting 0 while the kernel runs 125 times and has its output discarded. The draft sent a reviewer to read a snapshot diff that cannot exist. 111 T070 was listed as an unwritten gate. The tree declines it, with the reason rewritten once the explainers existed and two narrower checks built instead, one watched failing. Read from the checkbox, not the paragraph under it. It moves to the declined table. Also: 123 has 11 ticked lines on main, not none, and 83 on the PR's current tip, not 79; only P2 waits on #107, since it touches no specs outside 123 and 124; SS5.20 spans FR-106 to FR-140; the deferral chain is two independent deferrals, not one; FR-35 is named as T006's residue, the one of its four amendments with no marker; and the claude.ai artifact link is removed, being unresolvable for any other reader and checked by no gate. Refuted and dropped: that issue #61 was claimed discharged (the wording was already hedged), that FR-35's gap was a defect rather than the anticipated residue, that the NetCDF decision already exists (neither disposition is on disk), and that 104 T307 and 103 T208 are stale declines (T307's consumer still sends no Accept header; 107 delivered no rate control). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCv1sDnpZZXoXJ2w8osB3s
A second pair of adversarial passes ran against the corrected file. Two of their findings did not survive verification and were dropped; the rest are fixed here. The worst was a miscitation the first correction introduced. The 113 T006 row named docs/v2/plan.md:61 as the second stale section reference. That line is a table row about the gh-pages estate, and the only mention of 5.11 in that file, at line 178, is correct: feature 112 did take 5.11. Following the instruction would have corrupted a right line and left the real one, specs/113-operator-flowchart/plan.md:61, untouched. The row now names the right path and warns off the wrong one. The proposed replay-proof remedy was itself unsound. Vitest exits 0 when the selector matches nothing -- checked, 630 skipped, exit 0 -- so the suggested tag reproduces the fault it fixes. The row now asks for explicit file names plus an assertion on the number of tests actually run. The density row quoted 240x168 from the task line. That figure is in no source file: app/src/panels/operator/layout.ts:73-86 sets 208x116 and derives the expanded face as 450x464, so the decision would have been made against roughly a quarter of the real area. The row now says to take the numbers off disk, and to correct the line. Also: the parallelism claim carved out P5's forecast eras, which is discharged in scheduler.ts and collides with the 251 lines #107 adds there; the SS5.20 quotation replaced with what srd.md:1171-1172 actually says; 106 T506 and T507 given back the revisit triggers their glosses dropped; the self-check grep recipe made to print a total rather than per-file counts; the run.json and advisories.test.ts line numbers corrected; the third leakage fact noted as sitting inside a guard; and the forecast view described as arriving with #107 rather than present now. Refuted and dropped: that ClockStrip.tsx arrived with feature 105 -- the clone is shallow and 4dd407c is a graft boundary, so --diff-filter=A misattributes; specs/101-foundations-shell/plan.md:19 places it in 101 -- and that the ComposerPane fetch is at line 73. Both are recorded in the closing section, because a finding is a claim about the tree too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCv1sDnpZZXoXJ2w8osB3s
The centre band was a stub naming feature 124 for the whole of itself, and most of it did not need to be. The analyst has published a full-grid provenance field since feature 116 — four shares per cell (archive, departure, measurement, model), every depth, summing to one, under its own EDR collection — which is the substrate FR-111 names for this region and the depth profile. What is genuinely blocked is narrower: FR-122's per-source rays, because the analysis kernel reports the gain's row sum and discards its columns. The rays wait; the shares did not have to. `ColumnProvenance.tsx` reads a column the reader picks, one EDR position query per depth, through the same path an external client takes — the shell holds no private route to the store, and a surface that bypassed the query layer would be evidence of nothing. What is still 124's is said inside the region, beneath the reading. Three faults found by building it, none of them in the drawing: - **The shell had no master for `ctl/analysis/published` at all.** Not a wrong mapping — an absence, so `drawable` refused every analysis announcement the shell ever received. The config master's own description says what that means: "every crossing has a master or is a finding". Nothing had been reading the topic to notice. - **A cycle's collections are a standing fact and were announced as an event**, so a console mounting after a cycle — which every console does — had no collection to name for up to a whole cadence, on the region whose subject is what a cell's value was made from. Measured in a built instance: three cadences of warming and the chooser never drew a square. - **The grid was read once, on mount**, so a console mounting while the store was still empty never got one. It has a second chance now, on an announcement, guarded so it can fire at most once and can never become a poll. The first fix for the second fault was wrong, and the way it was wrong is the finding. Restating `analysis_published` re-ran the loop: that topic looks like an announcement and is a trigger — the model runner starts a forecast on it, which is feature 116's whole design, and the planner re-plans on it. Ten tests failed across seven files, replay determinism among them, two timing out with the runner refusing runs it had just been asked to repeat. Nothing in the code marked it as a command; it was learned by breaking it. A declaration commands nothing, so it has its own topic — `ctl/analysis/standing`, the same message under the same master, no subscriber that acts. That is the separation the runner already keeps between `run_started` and `run_cost`. Watched failing, and reverted each time: - the chooser, with the click marking a square and asking nothing: "picking a square asked the query layer nothing"; - the declaration, removed: "the analysis was never restated, so the chooser stayed undrawn". And two smaller corrections. The region reported one absence for two — "no analysis announced yet" whether it lacked the analysis or the grid — which sent two rounds of measurement in the wrong direction; a surface that states the wrong reason is worse than one that states none. And the chooser's squares were 38px wide inside a 360px phone, the width being the constraint where the height was already 44: `capture:mobile` enforces that floor on tabs and operator controls and would not have failed it, but the number is a claim about thumbs rather than about which selectors a check walks. pnpm check: 685 app tests, 40 script tests, 22 gates. All six capture proofs green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc
Review of the gate found that it repeated, in its own implementation, the
class of fault it exists to catch. Each is fixed and each is now held by a
planted fixture.
A marker written as a trailing comment marked the wrong test. Reading only
the line below a marker meant `it('replays byte-identically', ...) //
AT-04: byte-identity` recorded the *next* test as the proof's, and let its
own drop out of the sweep unreported -- the original fault's exact shape,
reintroduced by the fix for it. A marker now marks the test on its own line
when it trails one.
The sweep saw only `it(` with the name on the same line. vitest exports
`test` as well, and a multi-line call is ordinary formatting; a
byte-identity test written either way was invisible. The asymmetry is what
made it dangerous: a *marker* above an unrecognised form was reported, but
a missing marker on one was not, and the missing marker is the case the
sweep is for.
Prose quoting a test name was treated as a test. This repository's
docblocks quote test names as a habit -- check-replay-markers' own header
does -- and both escapes were wrong: rewording the prose, or marking it and
adding a test that does not exist to the proof's expected set. Comment
lines are now skipped, using the isCommentLine that scripts/gates/lib.ts
already exported for this.
Two marked tests sharing a leaf title in one file are now refused. The
proof keys a pass by (file, title) because vitest's title carries no
ancestor describe, so one passing could not be told from the other being
skipped.
The proof's own verification had no test at all, which review named as the
deepest finding: deleting the missing-test check left pnpm check, pnpm test
and pnpm gates green while the proof printed "held" over a file vitest had
failed to collect. It moves to scripts/replay-verify.ts and is exercised by
scripts/tests/replay-proof.test.ts -- a skipped marked test, a failed one,
an uncollected file, and a same-named pass in another file that must not
stand in for it.
Record corrections from the same review. CLAUDE.md said pnpm check is "what
CI runs", which this branch made false by adding a CI step; it now says
most of what CI runs, and names the proof beside it. The backlog said the
new gate's test sits "beside the other 21" -- 18, since check-schema-masters
and check-snapshot-drift have none, which is worth saying in a document
about not trusting an unwatched check. And #107 adds 244 lines to
scheduler.ts, not 251, which was the changed-line total.
The CI comment now names the duplication it accepts rather than only its
duration: the App tests step already runs these files once.
Verified in pieces after two OOM kills of the combined run: typecheck,
lint, 630 app tests, 50 gate tests, 22 gates, build, and the proof holding
over 9 marked tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCv1sDnpZZXoXJ2w8osB3s
The centre band's first build answered "what is a cell's value made from" with a grid of grey buttons over a list of four percentages. It was honest and it was dull, and dull was a correctness problem rather than a matter of taste: the interesting fact in the provenance field is spatial. The measurement share is a footprint — bright where a sensor reached, dark where the analysis had only its background — and it shrinks with depth. Four numbers for one column cannot show a footprint. So the region reads a slab through an EDR *area* query and draws it: a source selector that isolates one contribution or shows whichever dominates, a depth control that walks the water column, a readout that follows the pointer and the arrow keys, and a cell that opens into its full profile. One area query for a field is the query the standard has for exactly this; the old version issued six position queries about one column and never asked about anywhere else. The palette is computed rather than chosen. Four sources, fixed order, never cycled, validated against this shell's own surface #10151b: lightness band, chroma floor, CVD separation, normal-vision floor and contrast all pass, with the worst adjacent pair at ΔE 8.4 under protanopia — over the floor, and close enough to it that secondary encoding is obligatory rather than optional. Hence the per-source hatch, the legend, the readout in words and the printed percentages: with the colour removed the region still reads. Three things the first draft got wrong, each measured in a browser rather than reasoned about: - the hatch was coarser than the data. Tiled at four user units where one unit is one cell, each tile spanned four cells, so the map came out scratched with long diagonals crossing cell boundaries — a picture of the pattern rather than of the data. At 0.5 units each cell carries two strokes of its own, and each tile holds a wash of the hue as well as a line; - it opened on the measurement share, which is one bright patch in a black rectangle: a true picture and a poor invitation. It opens on the strongest source, where every cell has one; - the map was `width: 100%` with a 1px border under content-box, so the region overflowed by exactly two pixels at both phone widths, and the narrowest chip was 39px wide against a 44px thumb floor that min-height alone does not cover. Watched failing, and reverted each time: - the field is asserted to vary, planted against a slab read off a fixed index: "every cell in the field was drawn at one value: expected 1 to be greater than 1"; - the depth control is asserted to re-read, planted by dropping depth from the effect's dependencies: "changing depth asked the query layer nothing". That assertion compares the z of the first and last area query rather than counting them, because a control that re-rendered without re-reading would grow the count while fetching the same slab for ever. And the contrast suite caught a stale exemption: `.forecast-column-bar` was excused as text-free and no longer exists. The check that keeps that list honest is the reason the exemption did not outlive its rule. pnpm check: 684 app tests, 40 script tests, 22 gates. All six capture proofs green, and the region measured at 390px and 360px with the loop warmed: nothing scrolls sideways, every control clears 44px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc
Three conflicts, all in files two branches append to, and one semantic conflict the textual merge could not see. - `scripts/gates.registry` and its test: both branches appended a gate. Both are kept — `check-replay-markers` from main, `check-declared-cost` from here. The registry is append-only precisely so this resolves by keeping both rather than choosing. - `CLAUDE.md`: both branches were correcting the *same* false sentence, which said `pnpm check` was "what CI runs". Main added `pnpm replay-proof`; this branch added the six capture proofs. Neither correction was complete, so the merged text states the whole of it — CI runs seven more things than `check` does — with each one's reason for being out: the proof re-runs the marked tests' files, and the captures need a build and a browser. And the semantic one, which is the useful part. Main's new `check-replay-markers` gate failed on this branch's `kernel.test.ts`: the two-layer kernel's determinism test makes a byte-identity claim, cites AT-04 in its own name, and carried no marker — so it sat outside the AT-04 proof with nothing said. That is exactly the silence the gate was written for, and it found it in code written on another branch while it was being written. Marked, and the proof now holds over ten tests rather than nine. pnpm check on the merge: 684 app tests, 51 script tests, 23 gates. `pnpm replay-proof` holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc
`stage()` recorded the run before three decline paths, so a window declined for want of measurements, for a missing holding, or at the staging bound claimed the run as staged. The model runner's restatement of that same run was then dropped as a duplicate and the run got no bundle at all — the "zero staged bundles against a live run's five" outcome FR-125-05 exists to prevent, one configuration edit away. The field is named for what was staged; now it holds it. The module header still described the prompt as staging over the release the packager last heard. It reads the store now, because since the forecast eras there may have been no release to hear: an artefact that carries them holds the model runner back for the whole pre-roll, and `returning`'s script prompts for a package inside it. The deleted `lastPublished` field's documentation went with it. `isoPlusSeconds` gains the test its own justification asks for. The whole reason it exists as one shared function is that the copies it replaced disagreed below the second, and they agreed only because every simulated instant lands on a whole second at `tick_interval_us: 1000000` — the regime every other test runs in, so the one property the consolidation claims to establish was the one nothing checked. Watched failing against the truncating spelling it replaced. "The store held eight forecasts" was a miscount repeated in four places: the `instance` era carries a forecast and its spread field per run, so eight holdings are four forecasts. And the backlog's prose is brought to the count the previous commit retook. It still opened by arguing from sixty-five, still said the figures would go false "the moment #107 merges" after #107 had merged, and still described feature 124 as unstarted at 35 lines and outside the count when it is merged, 8 ticked, 28 unticked, and the bulk of what the count holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MqE2kzNL54yxWLwVYD4SK9
What this changes, and why
Two things the SRD gained today and the tree did not have: a model that propagates rather than translates, and a surface that shows a forecast being made. Feature 123 is built here; feature 124 is specified, and the parts of it that were never actually blocked are built too.
The kernel.
shallow-two-layer-v1joinsshift-advect-v1behind the unchanged model kernel port (Constitution VI): upwind advection, explicit horizontal diffusion and interfacial exchange, with an internal sub-step count derived from a stability condition and a refusal — naming the numbers — pastmax_sub_steps.Cost is simulation time (ADR-0043). The kernel reports its work, the configuration declares a rate, and a run occupies the ticks it costs rather than taking a third wall-clock exemption.
check-declared-costfails the build if a component declares cost in any other currency.Affordability is inverted, and that is the whole of FR-31. A warranted run is held while the standing forecast has more life than the run's cost plus the release margin, and released as headroom decays. Read the obvious way round the loop becalms permanently, because the run that would restore validity is exactly the one never affordable.
#/view/forecastnow shows the forecast, which for two rounds it did not. It opened with a gauge about the run loop, a cost, a list of runs, and two regions naming feature 124 for the whole of themselves. Both turned out to be mostly buildable:ctl/forecast/featuresis FR-05's entire product and had no consumer at all: a loop test validated its shape and dropped it.Both regions read the query layer and the wire the way an external client would — an EDR area query for the field, position queries for a column — and the shell holds no private route to the store.
The centre region's first build was a grid of grey buttons over a list of four percentages, and being dull was a correctness problem rather than a matter of taste. The interesting fact in the provenance field is spatial: the measurement share is a footprint, bright where a sensor reached and dark where the analysis had only its background, and it shrinks with depth. Four numbers for one column cannot show a footprint. The palette is computed rather than chosen — four sources, fixed order, never cycled, validated against this shell's own surface
#10151b, with the worst adjacent pair at ΔE 8.4 under protanopia, which is over the floor and close enough to it that the per-source hatch, the legend, the words and the printed figures are obligatory rather than decorative.The instance, opened at the change
https://deepbluecltd.github.io/drogna/instances/claude-srd-model-forecast-specs-ws9x3d/#/view/forecast
Set the clock to ×60 or ×600 in the header. The gauge decays, the timeline fills, a hold appears with its shortfall in ticks, the right region fills with the features and their widening rings, and the centre region comes up as a field: amber where the model made the background ocean, blue where the archive did, and a green patch where the sensors actually reached. Switch to
measurementto isolate that footprint, walk the depth row to watch it shrink, and click a cell for its column.The blog entry
https://github.com/DeepBlueCLtd/drogna/blob/claude/srd-model-forecast-specs-ws9x3d/site/docs/blog/posts/affordable-when-you-do-not-need-it-yet.md
The inverted affordability rule, and the becalm that reading it the obvious way produces.
What was watched failing
Every new check, planted and reverted. The stability refusal with its ceiling disabled; the becalm test against the affordability rule read the obvious way round;
check-declared-costagainst a planted key in the scheduler's master and again inrun.json; the whole-feature decline check against a planted rename; the broker conformance check inloop.test.ts; the once-per-episode hold check (201 against 1); the becalm-on-restart test against the restoredthrow(expected 3 to be greater than 3); the carry-velocity test against the restored inference (expected 7.0028 to be close to 4); the uncertainty-growth assertion against a carry that does not widen; the features subscription with its payload dropped; the feature restatement removed; the standing declaration removed; the share field asserted to vary, against a slab read off a fixed index (expected 1 to be greater than 1); the depth control asserted to re-read, against depth dropped from the effect's dependencies.Three of those were checks I had written that could not fail, and three more failed for the wrong reason. Same shape each time — an assertion placed where the thing it tests cannot yet have gone wrong, or measured along the wrong axis:
The depth-control assertion compares the
zof the first and last area query rather than counting them, for the same reason: a control that re-rendered without re-reading would grow the count while fetching the same slab for ever.All of it is in
specs/123-forward-step/tasks.md(T049–T094).Backend changes: what was brought up, and what was seen
Not a backend change in the
services//deploy/sense — drogna V2 runs entirely in the browser.At this head: 684 app tests, 40 script tests, 22 gates, all six capture proofs, and the Forecast tab measured in a browser at 390px and 360px with the loop warmed — nothing scrolls sideways, every control clears the 44px thumb floor.
Two gaps in the checks, both found the hard way.
pnpm checkis not the build. Every check on this branch reported green while every CI run failed — seven of them — because CI runs six capture proofs after the checks andCLAUDE.mdsaidpnpm checkwas "what CI runs". Corrected in this diff.And
capture:mobilepins the clock to rate 0 before it measures, so every surface drawn from an announcement is absent from every picture it takes. Claiming "the narrow presentation holds" from that proof is an empty claim for anything the loop produces. Measured properly:.forecast-gauge-barhad been scrolling sideways at 17px of content in a 14px box since the day it was written, and this round the region overflowed by exactly two pixels from awidth: 100%map with a 1px border under content-box. Both gaps are reported rather than closed — a gate asserting that everycapture:*script appears as a CI step, and a proof that warms before measuring, each belong in a change of their own.Left undone, and why
specs/124-forecast-illustration/tasks.mdcarries T001–T035; its FR-05 is amended to record that the plan view of the tracks landed here.steps/step_secondsmoves snapshots, costs, validity and much of the suite. Raised, not acted on.empty-mask, with the measured cause recorded (byte-identical analyses, digest057e83baae65) and the deeper mechanism stated as unsettled rather than closed by a bound.🤖 Generated with Claude Code
https://claude.ai/code/session_013Yhrus4EAa9MHMbbSuzMhc