Repository navigation
feat(S2): #188 finished, reconciled onto main (1 of 2) - #278
Merged
Merged
Conversation
FillRateBasedControlTUNES.send_leakage_behaviour read its fill-level scale from self._fill_level_scalefill_level_scale, an attribute name pasted in twice, which has been there since #158. The AttributeError that raised was caught by the handler's broad except as if the post had failed, so every FRBC.LeakageBehaviour was answered with PERMANENT_ERROR and none was ever recorded. mypy could not see it: the type-check task only looks at files that contain `from typing import`, and the FRBC base class is not one of them, so the attribute lookup went to Any. The next commit widens that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
The type-check task fed mypy only the files that contain `from typing import`, which were seven of eighteen, and left out frbc_simple.py, both control-type __init__.py files and the example scripts. With --follow-imports skip, anything those files defined then looked like Any to the files that were checked, which is how the previous commit's misspelt attribute went unnoticed since #158. It now checks all of src, and with --check-untyped-defs, so the bodies of functions without annotations are checked too. What that surfaced, besides the attribute fixed in the previous commit: - get_access_token stored a token whose type could be None and then used it as a header value; it now returns the token, so the caller holds a str. - get_access_token and get_versions indexed a response typed as a dict or a list; each now says so plainly when the server answers with something other than an object, instead of failing on the lookup. - The S2 translations annotated their resolution as a string while frbc_simple passes a timedelta, which pandas accepts as readily; the annotation now says so. - FillRateBasedControlTUNES overrode trigger_schedule without the arguments the base class declares. It calls its own override without them and ignores them, so it now accepts and ignores them. - The example server indexed an account that can be None and spread a dict of mixed values into add_asset; it now says so when the demo user has no organisation, and types the dict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
None of the S2 integration had a coverage number, for two reasons that each hid the other. The S2 tests run in a CI job of their own, since they need the s2 extra, and that job never asked for coverage. And .coveragerc omitted src/flexmeasures_client/s2/* outright, since #183, so asking for it would have reported nothing about S2 while looking as though it did. The omission is gone, and the S2 job now reports and uploads its own partial report, which Coveralls merges with the other job's in the finalize step, as it already does across the test matrix. The poe task that runs the S2 tests locally collects it as well. The job that skips tests/s2 will now list the S2 files as unexecuted in its own report; the merged figure is the one that counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
A CEM's handlers run for days with nobody watching standard output, so anything they print is lost to a deployment, which keeps the log instead. FRBCSimple gave up on a schedule that way when no storage status had arrived, which is the very failure, a storage left without an instruction, that the handler otherwise goes to some length to say out loud. It now logs a warning. A pygrep hook keeps it that way for s2/control_types and s2/cem.py. The example scripts print on purpose, as do the client's interactive delete confirmations, which answer an input() prompt on the console, so the hook leaves those alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
- The print ban now matches a print anywhere on a line and with a space before its parenthesis, plus pprint and writes to stdout or stderr, and covers all of s2/ but the example scripts. It only caught an indented `print(` at the start of a line before, in two directories, and missed s2/__init__.py, utils.py and wrapper.py, which run inside the CEM as much as the handlers do. - The example scripts get an __init__.py. Without one, coverage never listed websockets_server.py at all, since no test imports it, so the S2 report left it out instead of showing it at 0%. Running them by path is unaffected. - The TUNES leakage test now pins what is posted, its value and its unit, rather than only that something reached the sensor. - A client without a token now has a test that it authorises with the one it fetches. - FillRateBasedControlTUNES.trigger_schedule is back to taking no arguments, with the override mismatch marked for mypy instead. Accepting the base class's arguments and ignoring them would have turned a caller's mistake, such as expecting its superseded-request protection, into a silent one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…s them in S2 lets the elements of an operation mode, and of a leakage behaviour, cover the fill-level range in pieces, and s2python checks that they are contiguous on a sorted copy, leaving the list in whatever order it was sent. Four places took an element by its position instead, and answered three different ways: - the measured power of an actuator status took elements[0], so a heat pump that derates as its buffer warms was recorded at its cold power when warm: 2000 W where it draws at most 1200 W; - the charging efficiency took elements[-1] as though it were the top of the range, so a mode listed high element first planned against the wrong ratio; - the storage efficiency took the leakage rate of elements[-1], on the same assumption, though the leakiest element need not be the top one; - the schedule translation selected by fill level, but settled a shared boundary by list order. element_for_fill_level now answers wherever a fill level is known: the element whose range holds it, the upper of two at a shared boundary, and past either end of the range the element that applies at that end, so the answer does not jump there. The measured power and the schedule translation use it. Before any storage status there is no fill level, so the measured power takes the bottom of the range and says in the log that it guessed. Where FlexMeasures takes one number for a whole schedule, it does not know which fill levels the schedule will pass through, so the most cautious element answers, which is the rule #253 already applies across modes: the lowest conversion efficiency, and the leakiest element. That is also independent of order. The efficiency now also skips an element that draws power while the storage does not fill, such as a standby or circulation mode. Before, such a mode gave a ratio of zero or below, which then won the minimum across modes, went onto the charging-efficiency sensor, and told the scheduler that no power could fill the storage at all; with the fill-level band priced rather than hard, never running the heat pump was then a feasible plan. Two guards were dropped rather than tested, because S2 makes them unreachable: every operation mode and leakage behaviour has at least one element, and every element at least one power range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ates Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…owest An adversarial review showed that the lowest efficiency over a mode's elements is not the cautious choice the previous commit called it. In FlexMeasures' linear program a charging efficiency scales what the actuator adds to the stock, so understating it protects the lower band only: the plan overshoots the upper band, buys more than it uses, and against a usage forecast the plan cannot relax, it can turn a problem the actuator can meet into one the scheduler finds infeasible. A heat pump at a COP of 4 cold and 2 warm, planned at 2, empties its planned buffer in a few hours while the real one keeps up. So within a mode, the element that applies at the fill level the schedule starts from answers. That is also the part of a schedule that runs before the next storage status asks for a new one, and it is what the actuator really does there. The storage efficiency likewise takes the leakage element at the latest fill level, and the leakiest only where no fill level is known yet. Across modes, the lowest still goes out, as #253 decided; the same argument applies to it, and that is left as a question rather than changed here. Also from the review: - A fill level that is not a number, which s2python lets through, counts as no fill level, and the selector refuses one, where it used to pick the bottom element silently. - The selector's docstring now says what is true: s2python checks contiguity for an operation mode only, never for a leakage behaviour; a single-point range, which the standard rules out, applies only at the top; and of two elements with the same range, the first listed answers. - Tests now cover the latest of several storage statuses, a fill-level scale other than 1 where scaling would pick another element, no warning for a mode with one element, and a schedule starting in the upper element. Each was a production mutation that passed the suite before. Formatted with black as well, which the previous commit was not: the pre-commit hooks were never installed in this repository, so they did not run on commit, and CI's check caught it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
S2 makes an operation mode's diagnostic label optional, and FillRateBasedControlTUNES tells its modes apart by looking for "THP" or "NES" inside it. On a mode without one that raised a TypeError, so a single unlabelled mode stopped both planning and the recording of every mode's conversion efficiency. A missing label now simply matches neither. Found by type checking against s2python's own types, which also asked for two variables that held a range and then a number, or a message and then a series, to be given a name for each. New tests cover the planning path, the efficiency path, and posting a usage forecast, none of which any test reached before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…inted request() ran check_content_type after its retry loop. It could never raise there: request_once parses every answer with aiohttp's json(), which refuses any content type but JSON, and the loop already reports that refusal as a ConnectionError, like every other failure to reach the API. Had it fired, it would have done harm. It built aiohttp's ContentTypeError from a message and a dict, where aiohttp expects the request's info and history, so anything that formatted the error, a log line included, raised an AttributeError of its own; and it read the body with response.text() without awaiting it. Its only test passed a mock whose text() was not a coroutine, and never formatted the error. The check is gone. The test that replaces it states where a non-JSON answer, such as a proxy's error page during a restart, is actually handled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ncluded mypy ran with --follow-imports skip, so s2python, pydantic, pandas and aiohttp were Any to it, and a misspelt attribute on an S2 message passed. It now follows imports, with pydantic's plugin, since S2 messages are pydantic models, and the pre-commit hook installs the s2 extra so those types are there to check. The plugin is also the guard against sliding back: without the extra it fails to load, rather than leaving every S2 type silently Any. A misspelt message_id on a leakage behaviour is now an error naming it. What it found, besides the two fixes in the previous commits: - get_validation_error_summary iterated a pydantic ValidationError, which is not iterable, so it raised a TypeError on any error it was given. Nothing called it, so it is gone rather than repaired. - Revocations were recorded by the RevokeObject's raw id, while every other id a handler keeps is text, so a lookup by the text of the id would have missed. They are recorded as text now. Nothing calls is_revoked yet, which means a revocation is acknowledged and then never consulted; that is recorded as a finding for later rather than changed here. - The rest is annotation: a message that arrives as a model, a dict or text is normalised to a dict once and checked as one; get_message returns the dict it always returned rather than claiming a str; the message type of a registered schema is read through model_fields rather than the deprecated __fields__; an operation mode's id is text where a label is wanted; and the actuator's power ranges are pairs rather than tuples of any length. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
… worked Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ested An adversarial review ran every claim on this branch, and three were false: - The removed content-type check could fire. aiohttp's json() accepts JSON variants such as application/problem+json and application/vnd.api+json, and the check refused those, with an error that could not be printed. Removing it is right, but it changes behaviour: such an answer is now read like any other. The comment at the removed call, the test's docstring and the changelog entry now say so, instead of calling it dead code. - "A non-JSON answer is reported as a ConnectionError, as before" was only true of a wrong content type. A body that is not JSON under a JSON content type is refused by json() itself, as a JSONDecodeError, as it was before. - One unlabelled TUNES mode stopped planning, and the recording of the efficiencies of the modes listed after it, not of every mode. Two more overstatements live only in pushed commit messages and the PR body: following imports leaves pandas Any, since it ships no type information and no stubs are installed, and the efficiency path was already reached by a test. Tests now cover what five mutations showed was unpinned: a revocation taken by the CEM itself, with no control type active; a model of a type a handler does not handle; a usage forecast starting off the quarter hour; and one averaged within a quarter hour rather than taking its highest value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ions The schedule translation keeps a running fill level, so that each step is answered with the operation mode for the fill level the storage is then at. It advanced that level by a fill rate per second times a step in hours, so it moved 3600 times too little, and every step of a twelve-hour schedule was answered at the fill level the schedule started from (finding C1). Fixing that alone would have been worse: the level would then rise at the real rate and never fall, and pin every step at the top. So the integrator gets one written contract, the one S2 states for all three rates: a change in fill level per second, in the resource manager's own units, with usage and leakage lowering it where positive. Its step is a duration rather than a number of some unit. - Leakage now counts (C2). It read leakage_rate_per_second, which S2 does not have, and an actuator's leakage_behaviour, which cannot exist, both through getattr with a default, so the branch never ran. The handler now passes the latest FRBC.LeakageBehaviour, and the element that applies at the fill level. - Usage now counts. The handler passes the latest FRBC.UsageForecast, read per step by usage_rate_at; a frame may still carry a usage_forecast column instead, per second, which is what TUNES does, now dividing by the quarter hour it posts in. - The step is the schedule's own (B5): its duration over its number of values, which is the resolution of the sensor FlexMeasures answered at. It was taken to be a quarter hour whatever it was, so an hourly sensor's twelve values were laid on forty-eight quarter hours, pandas refused, and no instruction went out. - An instruction goes out wherever the mode or its factor changes, not only where the power does: at a constant power the fill level can pass from one element into another, which needs another factor. - The factor is the one that draws the power asked for (C4), taken from the power range, rather than worked back out of the fill rate, which failed wherever the fill rate was one value and the power a range: asked for 1200 W from a mode running 800 to 1200 W, that asked for 800 W. Where the power is one value and the fill rate a range, every factor draws that power, so the one that fills most is asked for. op_mode_compute_factor is gone. - The storage efficiency compounds a leakage rate per second (B10), where it compounded it per hour, declaring a buffer that loses about 7% over twelve hours as holding its heat to within 0.002%. - The handler's power and fill-rate measurements cover its own resolution (D9), rather than a quarter hour whatever it planned on. - translations.py is linted again: it opened with `# flake8: noqa`, which hid nothing, as it turns out. TUNES shares the translation, so its instructions change with it, for the better: it carried the same per-hour integration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…showed it did not An adversarial review built its own integrator from S2's definitions, a second at a time, and found the previous commit agreed with it only while nothing changed within a step. Where something did, it did not: - A step that crossed from one element into another was integrated as if it had not: from 46, filling at 0.02 per second below 50 and 0.01 above, a quarter hour at factor 1 reaches 57, where holding the first element said 64, and at an hour's step the client thought a buffer full that was at 84. The same held for leakage. - Usage was sampled at the instant a step began, where FlexMeasures planned with the mean over the step, so a five-minute draw in each quarter hour counted fully or not at all, and a forecast starting off the step counted as nothing. follow_operation_mode now follows a step a minute at a time, as a resource manager follows an instruction: the mode and the factor are what the instruction fixes, while the element of the mode and of the leakage behaviour, and the usage a forecast expects, are each taken where and when they apply. The fill level is held between its bounds throughout, rather than only at the end of a step. Also from the review: - An instruction no longer goes out for a solver's own rounding: near factor 0, math.isclose without an absolute tolerance is exact equality, so 800 W give or take a ten-millionth from a mode running 800 to 1200 W sent 30 instructions in twelve hours instead of one. - A gap in a usage column, as FlexMeasures returns for a moment it holds nothing for, is no usage known, rather than a NaN that pinned the running fill level at the top of its bounds for the rest of the schedule. - FillRateBasedControlTUNES reads its usage forecast back divided by the fill-level scale it posted it with, which left it ten times too small at TUNES' default scale, and plans with its latest leakage behaviour. The previous commit message called TUNES' change "for the better"; without these it was not. - One power and a fill-rate range listed downwards asks for the top of the range, where operation_mode_conversion_efficiency declares the efficiency FlexMeasures plans with, so the instruction and the plan agree. - FRBCSimple refuses a schedule_duration that is not a whole number of resolution steps: FlexMeasures answers with whole steps only, so the step, read as the duration over the number of values, would come out wrong. - The handler's storage efficiency is what remains after one step of its own resolution, and FlexMeasures reads it per step of the leakage sensor's resolution, so the two have to agree. That is now said where the sensor is taken and where the value is posted. Tests cover each of these, and each of the four mutations the review found surviving, among them an on/off heat pump told to switch off: both modes ask for factor 0, and only the mode tells them apart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ed, and simulated The shipped example sent no instruction at all, for two reasons that only show with both halves running (findings A1, A2). Its resource manager sent its storage status before its system description, which planned nothing, since planning hangs off a storage status and needs a description to turn a schedule into instructions; and its description was dated 2026-01-15, while the server seeds prices from the day it starts, so a schedule's window had no prices. It also could not have shown steering if it had: one operation mode, with a power range of -0.5 to 0.5 W against a fill rate of -0.5 to 0.5 per second, which is 1.8 MW, over a declared range of 0.4. The resource manager is now a heat pump keeping a hot-water buffer warm. It is idle, or heats at anything from 400 to 1500 W, at a COP of 4 while the buffer is below 6 kWh and 3 above it. It declares its leakage, warmer losing more; a day of hot-water use with showers in the morning and washing up in the evening; and a comfort profile that pre-heats before the showers. It sends its description first, dated now, and then simulates its buffer from S2's own definitions, written separately from the CEM's code: it follows each instruction from the moment it is due, and reports its fill level every so often, which asks for a new schedule. That simulation is a second opinion on the CEM's arithmetic, and a test holds the two to agreement across an element boundary. The server gives the handler a sensor for every declaration (fill rate, band, usage, leakage, charging efficiency), so each can be seen next to the schedule planned against it, and plans from now rather than a day after the description. Its connection handling is split out of the aiohttp handler, so a test can serve the shipped resource manager with the shipped CEM, over an in-memory websocket, with only FlexMeasures stood in for. Two library changes the example needed: - A system description that arrives after the latest storage status, or that changes what the device can do, is planned for against that status (finding A3), rather than waiting for a status that a resource manager whose fill level holds steady may never send. It plans without re-posting the old status as a new measurement. - A schedule never starts before now. Planning happens on every storage status while a description can be days old, so a schedule anchored to the description alone slid into the past, and its instructions all arrived as one overdue burst. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…xMeasures FRBCSimple passed its energy_unit straight through as the flex model's soc-unit, while FlexMeasures validates soc-unit with OneOf(["kWh", "MWh"]). So four of the six units fill_rate_in_watts documents, J, kJ, MJ and Wh, made every trigger a 422, and the storage got no schedule at all (finding B2). The plan for this was to refuse those units when the handler is made. The carve source answers it better, by converting joules at the edge, and that is what happens now, for every supported unit: the state of charge and its two hard bounds go to FlexMeasures in kWh, converted from the recorded unit with the table fill_rate_in_watts already keeps, while kWh and MWh pass through as they are. The band, the usage and the leakage are read from sensors, which FlexMeasures converts itself, and the bounds the schedule is read back against stay in the recorded unit, since the operation modes are stated in it. An energy unit outside that table is now refused when the handler is made, where it used to surface only at planning time, as a logged failure to record a charging efficiency. The handler for that failure is gone, since nothing can now reach it. Not changed, and now pinned by a test: the measured state-of-charge sensor is never named in the flex model (finding B13 stands). Named there, FlexMeasures would save the state of charge it plans onto that sensor, beside the measurements. The carve source does name it, and the carve will have to leave that line behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…sures is sent Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…nges A second round of review showed that following a step a minute at a time traded one class of error for four others, and cost ten times the time: - Usage was sampled at the start of each minute. Half-minute draws starting on the half minute were never seen at all, and the same draws starting on the minute were counted twice. - A level settling on an element boundary, where filling stops above it, flipped across that boundary every step: 127 instructions in 144 steps, against one. - An element narrower than a minute's travel was skipped: a level that stops at 50 was carried to 80. - A 48-hour schedule against the largest forecast S2 allows took 1.7 s, on the event loop, scanning the forecast once a minute. Every rate is constant between the moments the usage changes and the moments the level reaches an element boundary, of the mode or of the leakage behaviour, or one of its bounds. So follow_operation_mode now steps from one such moment to the next and is exact. A level that reaches a boundary whose far side would push it back stays at that boundary, as a real storage does, and so does a level at one of its bounds, which rises again from there once filling wins. The usage forecast is read through a UsageProfile, built once per schedule and looked up by bisection. Also from the second round: - FillRateBasedControlTUNES posted its usage forecast as an amount per quarter hour labelled MW. FlexMeasures reads soc-usage as a power and multiplies it by the step's share of an hour, so it planned a quarter of the real draw. It now posts per hour, and reads back accordingly. - FRBCSimple refuses a resolution or schedule duration that is not positive, where a zero resolution raised a ZeroDivisionError. - The factor tolerance's justification was wrong: FlexMeasures rounds a schedule to six decimals of MW, so what it absorbs is the noise of converting to watts, not a solver's rounding. The tolerance stays; the comment and the changelog now say what it is for. - One of the first round's mutations had stopped applying after black split its line, so the gate's count overstated what was checked. The harness now rejects a mutant that cannot run, and every mutation is anchored to the formatted code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…2-example-heat-pump Signed-off-by: F.N. Claessen <claessen@seita.nl>
…, and what review found An adversarial review of the example, and of the two library changes it needed, found the example resource manager following every plan it had ever been sent. Each instruction ran in a task of its own, nothing cancelled the tasks of a replaced batch, and the server sent no revocations. In the live demo against a sandboxed FlexMeasures, thirty instructions for the same quarter hour fired at once when it came, one from each replan. The server now sets send_revocations, so a newer plan revokes what is left of the one before, and the resource manager cancels a revoked instruction it has not yet followed, and acknowledges the revocation. Also from the review: - The leakage, the usage forecast and the comfort profile were sent once, at connect, and the storage efficiency was recorded once, for twelve hours from the message, so planning went without leakage after twelve hours and without usage or a band after a day. The resource manager now sends all three again every S2_EXAMPLE_FORECAST_INTERVAL seconds, from then on, and the server seeds its demo prices again every twelve hours. - In the library, a storage efficiency is now recorded each time a schedule is asked for, over the window it covers and as a share of the fill level it starts from, and named in the flex model only where it was recorded. On arrival, it covered the wrong hours, at a fill level nobody had reported yet, and as a share of the top of the range it understated the loss at lower fill levels. - align_to_resolution floored the local clock, which in the repeated hour of an autumn DST change took the first pass of that hour, so a schedule could start 67 minutes in the past. It now floors the instant, in UTC. - A state of charge is posted as a reading at an instant, where it was posted over the minute after it, which FlexMeasures stored as a forecast. - The example declares what it does: transitions between its two modes, which S2 lists as the ones possible; no PowerForecast, since it sends none; and a consumer of electricity rather than a storage of it. - Claims corrected: a description that differs from the one before it, its validity included, asks for a schedule, not only one that "changes what the device can do"; and a schedule starts in the current step, which has begun, not "never before now". - The test that held the example's buffer to the CEM's integrator compared two copies of one discretisation. The CEM is now exact and the buffer steps a minute at a time, so the test bounds their difference by a minute of the rate difference at the crossing, which is what two correct discretisations owe each other. Tests cover everything the review's thirteen mutations found unpinned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
- The example now tells the CEM where each instruction stands: accepted on arrival, succeeded once carried out, revoked once revoked. Without that every instruction stayed NEW, so each replan revoked the whole previous batch, the instructions already carried out included. - The mode a transition reports leaving is the one the last carried-out instruction put the heat pump in. It was the mode the previous instruction to arrive asked for, which after a revocation the heat pump was never in. - A storage efficiency takes the loss as a share of the start fill level or of the middle of the storage's range, whichever is larger. A share of a near-empty start left almost nothing per step, and overstated the loss many times over once the plan filled the storage. - A state of charge is posted as an instant only on a sensor that records instants: FlexMeasures refuses a single value of no duration on a sensor with a resolution, so there it is posted over a minute, as before. - align_to_resolution floors on the moment's own clock, which is the grid FlexMeasures floors to, with the pass of an autumn DST hour taken from the moment. Flooring in UTC put hourly steps half an hour off in zones that are, and daily steps on the wrong midnight. - The example server's price reseeding survives a failed ingestion job, a wrong content type and a wrong API version, not only a lost connection. - Statuses about different steps that interleave within a second get belief times a second apart per step, where only the latest status was compared. The demo gate's first run had thirty statuses for one quarter hour arrive at once, and one was refused as a replacement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
- In the repeated hour of an autumn DST change, a boundary is the later of the two candidates that is not after the moment. Taking the pass from the moment put 03:10 at two-hourly steps on 02:00 of the first pass, three hours back. The candidates are compared as pandas timestamps, which compare instants. - The changelog says what the floor does: on the clock of the moment, in the offset it carries, which is how FlexMeasures floors a posted series. "The site's own clock" was not true for series a resource manager anchors in UTC. - A state-of-charge sensor stating a resolution that is not a duration is posted over a minute and logged, where it aborted the status before planning. The docstring says the sensor is asked until it answers, which it is. - An instruction that has become due is no longer pending, so a revocation arriving while it is carried out is not answered as REVOKED for an instruction the heat pump is following. - The comment on the storage efficiency's reference says that below the middle of the range the loss is understated, and by how much at most. - Tests pin the forgetting of belief times more than a day old. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…er way The demo gate's second run, against a sandboxed FlexMeasures, heated the buffer in the quarter hour the plan chose, but every replan inside that quarter replanned the same quarter, at 0.72 kW, then 0.67, 0.63 and on down to 0.51 kW. A schedule asked for mid-step started at the boundary that step began at, while it was planned from the fill level reported now, which FlexMeasures takes to be the level at the schedule's start. So each replan counted on the whole step where only part of it was left, asked for less power over what remained, and the step delivered less than any plan meant it to. A schedule now starts at the next boundary, or at now where now is one. The step under way keeps the instruction it is already following, which a newer plan does not revoke, since it has been carried out. The end-to-end test stands the CEM's clock on a boundary, so that its schedules start there and the first instruction is due within the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ix/frbc-energy-unit Signed-off-by: F.N. Claessen <claessen@seita.nl>
Black had split the expression around the trailing comment after the merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ulation stack Carved from #188, rewritten for the library rather than transcribed. #188 made every CEM find or create a FlexMeasures asset named after the resource manager that connected, with its sensors (config_utils.configure_site, a 12-tuple, a module-level id cache and logging configured at import), and registered an FRBCSimple wired to them, for every deployment. It also seeded constant prices from the wall clock's today with a belief time in 2026, which a simulation of 2022 never reads. - flexmeasures_client.s2.site holds FRBC_SITE_SENSORS, the sensors an FRBC site records on, which the example's table becomes; ensure_site, which finds or makes a site of a name with its sensors and graphs, and is safe to run again; and FRBCSiteConfigurator, which gives each resource manager a site named after it and an FRBCSimple wired to it, planning from now over 24 hours as #188 did, with the asset type, time zone, asset specs and the power sensor's attributes as options, and the site's id remembered for a reconnect. The sensor names the co-simulation reads by ("price", "production price", "power", "measured-power", "state of charge") are kept. - CEM(site_configurator=...) configures in the background once a resource manager's details arrive, and wires the site's measured-power sensor. Until the handler exists, the active control type's messages are answered with TEMPORARY_ERROR, which the resource manager retries; before, the CEM raised a KeyError on them. Closing the CEM stops a configuration in flight. - The example server takes S2_SITE_PER_RESOURCE_MANAGER: no fixed site and no demo prices, and a configurator for the server's lifetime, whose power sensors say the heat pumps cannot produce (is_strictly_non_positive), as #188's did. Its fixed site is set up with ensure_site, and gains the production price and measured-power sensors. - examples/S2-co-simulation holds the docker-compose overlay the co-simulation runs on, moved from #188's root docker-compose.override.yml, which compose applied to anyone running the repo's base file. It sets both switches, uses s2-python as this repo locks it (0.8.1, which the co-simulation's resource managers use too, where #188 pinned 0.8.2), runs one worker command for all three workers, logs at INFO, and drops the py-spy debugging setup. Its README says how to keep it apart from a development database: FlexMeasures keeps that database's data inside its checkout, so a project name does not. Not carried from #188: mirroring measured power onto the community's aggregate-power sensor (the maintainer dropped it, since nothing reads it back), and the price seeding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
Against the co-simulation it is for: - A reconnect overwrote the apartment's flex model with the configurator's, which reset what the co-simulation's controller writes into it (the stock's value at the end), and replaced its graphs, dropping the charts the co-simulation adds. A flex model is now merged into the asset's, and graphs are set only where an asset has none (the demo's own fixed site resets them, as before). They carry #188's titles again, since the controller adds to the graph titled "Power". - #188 always relaxed the site capacity; the configured sites in the example server do so again, explicitly. - Of several assets with a resource manager's name, the newest is taken, as the co-simulation takes it. In the CEM: - A power measurement arriving before a resource manager's site was set up was recorded on sensor 357, a fallback from #115 that named whichever sensor of whichever site had that id, and answered OK, so it was never sent again. It is now answered with TEMPORARY_ERROR while the site is being set up, and without a sensor for it, it is not recorded at all. The fixed site's measured-power sensor is now wired too. - A setup that failed was not tried again, and only two exception types were logged; anything else surfaced as a task exception never retrieved. A failed setup is now logged, whatever failed, and tried again on the next message that needs it. In the co-simulation stack: - The overlay's workers serve the reporting queue too, as the base file's do. - Its README says what isolates the database: the overlay mounts ../flexmeasures and ../flexmeasures-client, so a folder of its own with checkouts of those names, not a FlexMeasures worktree on its own. Mailhog's ports are listed. - Comments break after punctuation. Still not here: recording a co-simulation's measured power, which arrives in bursts stamped at past quarter hours and which the CEM's binning on now() does not catch. That is the next package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ce per step Carved from #188, rewritten for the library rather than transcribed. The CEM averaged a resource manager's measured power into five-minute bins ending at its own now, posted one at most every five minutes, and dropped whatever fell outside that bin. A co-simulation's resource manager sends a window's measurements together once the window has passed, each stamped at the start of its quarter hour, so none of them ever fell in the bin, and nothing was recorded; the buffer kept them all, in a class attribute that every CEM in the process shared. The values, which S2 states in watts, also went out labelled kW, a thousand times too large. #188 bypassed the binning and posted each value at its own timestamp, deduplicated in module-level globals. flexmeasures_client.s2.measurements.MeasurementRecorder takes each measurement into the step of the sensor's resolution its timestamp falls in, and records a step once it is over by the CEM's clock, as the average of what was measured in it, in watts, with the step's end as the moment it became known. Consecutive steps go in one request, so a co-simulation's window of quarter hours costs one. A step that could not be posted is kept and tried again. A server makes one recorder and hands it to every CEM, so a resource manager that reconnects and sends its series again, as a co-simulation's does every round, is not recorded twice: the first value for a step stands, for a week. The CEM hands measurements to its recorder, its own by default, and asks the resource manager to resend one it could not take at all, such as where the sensor's resolution could not be read. get_commodity_unit, which labelled power kW, goes; nothing else used it. Not carried from #188: a barrier awaiting the posts before planning, since the co-simulation's resource manager sends its storage status before its measurements, so the barrier never covered the step being planned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
- A step the server kept refusing blocked its sensor for good: steps were never dropped, and consecutive ones went in one request, so every later request carried the bad step and failed with it. Each step now goes in a request of its own, is tried a few times with a wait in between, and is given up on, with an error naming its values, after that. - One request for several steps gave the earlier ones a belief time after their own end. With a request per step, each is known at its own end, as the docstring said. The batching the first version claimed only ever happened after a failure, and its test checked nothing about it; both are gone. - Two measurements for one sensor at once, from two CEMs or a sample landing while its step's post was in flight, posted the step twice and then raised a KeyError the CEM did not catch, so no reception status went out. Recording is now one post at a time per sensor. - Posting held up each measurement's reception status, which #188 had kept off the co-simulation's critical path by posting in the background. Recording happens in the background again, a defect in it is logged, and a failed post is tried again a while later without waiting for another measurement, which a co-simulation's final window never gets. - A moment in the repeated hour of an autumn DST change could not be floored to its step, so such a measurement was refused on every resend. Its step is now the later boundary not after it, as align_to_resolution finds it. - Remembering recorded steps cost a rebuild of the whole set per measurement, which on an instantaneous sensor grew large; the oldest are now dropped from the front. - A message with measurements for several sensors was taken in part where one sensor could not be read, and, asked for again, had that part taken twice. Every sensor is now made ready before any value is taken. - The server's request handler is tested for handing each CEM its recorder, and the changelog says what changes for a live resource manager: one value per step of the sensor's resolution, recorded with the first measurement after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
… image Found by running the stack, from checkouts of its own, against FlexMeasures main (1.1.0) with the community scheduler plugin, and a two-home, one-day co-simulation through it (no community steering): every step ran, every home's measured power was recorded for every quarter hour, and no CEM error remained. It took four fixes to the overlay first. - The server and the workers installed this client with the system pip, while FlexMeasures' image runs FlexMeasures and its plugins from its own virtualenv, so the plugin could not import flexmeasures_client and the server never booted. They now install with that interpreter's pip, as the CEMs already did, and no longer try to install /app itself, which the current image has no project file in. - A checkout mounted without git history it can read, as a git worktree is, gives hatch-vcs no version, so the editable install failed. The install is given one, in the plain SETUPTOOLS_SCM_PRETEND_VERSION, since hatch-vcs does not match the per-distribution variable the CEMs had. - A development server trusts only loopback names, and the CEMs reach it as "server", so every request of theirs was refused as from an untrusted host. The server now trusts that name too. - The README says how to build FlexMeasures' image from a worktree (its version as a build argument) and how to move the ports where they are taken. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…t from a worktree Review found the README saying the stack runs FlexMeasures as its image installs it, while the mounted checkout is what runs, and an s2-python pin on the server and workers, which speak no S2 and whose locked environment it should not shift. The README now shows the build-argument overlay a worktree needs, says the UI may lack its fetched libraries, and the changelog no longer claims a FlexMeasures version the overlay does not pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
Every other value FRBCSimple records says when it became known; the state of charge did not, so FlexMeasures took the moment the request reached it. In the co-simulation gate that recorded readings of 2022 as known in 2026, 1436 days after they were taken. It is now posted with the handler's time as its prior, which in a co-simulation is the simulated time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…2-belief-times-on-the-clock Signed-off-by: F.N. Claessen <claessen@seita.nl>
Package D4 of the plan to finish #188. Each storage status is a planning request, and one arriving while a schedule was being planned started a second solve beside it, whose answer replaced the first's. A resource manager reporting faster than FlexMeasures plans kept several solves running on the server for nothing. FRBCSimple now plans one schedule at a time: a request waits for the one in flight and, when its turn comes, is dropped where a newer one arrived meanwhile, as a system description's plan already was where a status overtook it. A burst of statuses costs at most the solve in flight and one more, for the newest. No clock is involved, so a co-simulation's retrigger round, which shares a simulated time with the round before it, is planned as before, and the newest request is never left without a plan. The plan's revision 1 proposed a minimum replanning interval instead; its review showed that a deferred plan could strand the last status of a session, and that on a simulated clock a retrigger would always fall inside the interval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…s2-plan-throttle Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…est waits less Review found that a request overtaken while its schedule was being asked for kept the planning lock through every retry of a failed attempt, up to five attempts with a wait between each, and that the schedule they finally got was thrown away; only then was the newest request planned, where before this PR it would have started at once. Between attempts the request now checks whether a newer one has arrived, and stands down if so. The docstring, the PR's cost bound and the changelog line now say what holds: a burst costs at most the attempt in flight and one plan more, for the newest, which starts after that attempt rather than beside it. The docstring breaks after punctuation, and _plan documents its parameters again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
Found by running a steered two-home co-simulation through the stack: a steering round replans a window at the same simulated moment, so without FLEXMEASURES_ALLOW_DATA_OVERWRITE the scheduler could not save the second plan and the homes got no instructions for their window; and every home triggers a schedule each round from one account, far above the default trigger limit, so every trigger past it waited minutes. Neither is read from the environment, so the README says what to put in the instance config, with the solver time limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW Signed-off-by: F.N. Claessen <claessen@seita.nl>
…s2-cosim-instance-config Signed-off-by: F.N. Claessen <claessen@seita.nl>
Coverage Report for CI Build 42143335770Coverage decreased (-0.7%) to 94.941%Details
Uncovered Changes
Coverage Regressions1 previously-covered line in 1 file lost coverage.
Coverage Stats
💛 - Coveralls |
Flix6x
commented
Nov 7, 2026
| run: uv sync --frozen --extra s2 --group test | ||
| - name: Run S2 tests | ||
| run: uv run pytest tests/s2 -rFEx --durations 10 --color yes | ||
| run: uv run pytest tests/s2 -rFEx --durations 10 --color yes --cov |
Member
Author
There was a problem hiding this comment.
A short section in the PR description explaining what including the CEM does to the coverage reporting on the fm-client repo. I think the readme banner can report only one number, but it feels as if we should be making a distinction in the coverage reporting over the different installation types. Now it's just S2, but there may be other client flavours in the future.
| * ``FRBCSimple`` takes an optional ``production_price_sensor_id``, for a site where feeding back is priced differently from drawing, as under a feed-in tariff. Without it, both directions are priced by ``price_sensor_id``, as before [`#261 <https://github.com/FlexMeasures/flexmeasures-client/pull/261>`__] | ||
| * ``FRBCSimple`` takes ``relax_site_capacity``: ``True`` lets a schedule breach the site's connection capacity at a price, for a site whose capacity is a target set by others, and ``False`` keeps it a hard limit. Unset, it is left to FlexMeasures, which since v1.0 relaxes it by default, so a home whose connection is a physical limit has to pass ``False`` [`#261 <https://github.com/FlexMeasures/flexmeasures-client/pull/261>`__] | ||
| * An FRBC handler logs the instructions of a batch once, at debug level, by operation-mode name [`#261 <https://github.com/FlexMeasures/flexmeasures-client/pull/261>`__] | ||
| * ``flexmeasures_client.s2.clock``, with a ``WallClock`` and a ``SimulatedClock``, and ``CEM(clock=...)``: the time a CEM and every handler it registers go by, timers and the moment a state of charge is known as of included [`#264 <https://github.com/FlexMeasures/flexmeasures-client/pull/264>`__, `#268 <https://github.com/FlexMeasures/flexmeasures-client/pull/268>`__] |
Member
Author
There was a problem hiding this comment.
Sentence constructions like these reduce legibility. It's a widespread problem.
| the moment it is known as of | included
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.
#188 finished, reconciled onto
main. This is the first of two PRs that finish the S2 FRBC Customer Energy Manager. PR 2 (#279) stacks on this one and carries the remaining review findings.What this is
#188 (
dev/fix-handshake-handler) was carved into small PRs, and each was merged into the integration branchdev/s2-frbc-complete. Every one of them had:This PR is that branch up to package P1 (66 commits, 40 files).
What P1 covers:
src, coverage fortests/s2, and a lint againstprint()in unattended code.examples/S2-co-simulation.#188 itself is left open for you to close; this PR and the next one replace it.
Please merge with a merge commit, not a squash. The atomic history is what makes a diff this size reviewable. The repo currently allows squash merges only, so this needs the setting changed for the merge.
Evidence
Coverage reporting
This PR changes what the coverage badge measures.
main,.coveragercomits the S2 package, and the S2 tests upload no coverage. So the README badge (95.7%) covers only the code outside S2: 1042 lines onmain.What each flavour covers, measured locally on this branch's head (
ad9a12ba), statements only:So the badge drops slightly, from 95.7% to 94.9%. Core coverage does not get worse: the S2 package now counts, and it is covered a little less well. The S2 tests add nothing to the core code's coverage beyond what the core tests already reach.
Each job's own coverage report in the CI log measures the whole package, so the core job shows 35% and the S2 job 64%. Neither number describes its own flavour.
Telling the flavours apart. Coveralls gives one badge per branch, so a single README number cannot show both. Two ways to make the distinction visible:
--omit "*/s2/*"and the S2 job with--include "*/s2/*", each writing its number to the job summary. The uploads stay as they are, so the badge stays the combined number. This also scales to future client flavours: one job and one report scope per flavour.I'd do (1) in a follow-up and keep Coveralls, unless you want separate badges in the README.
Finding → commit → file map
Findings are numbered as in the review of the carve campaign.
PR 1 is
origin/main..ad9a12ba, "#188 finished, reconciled onto main" (55 non-merge commits).Rows: 20 (19 finding rows, including the stale-comment sub-row of A1, plus the P1.0 instruments row).
PR 1 commits that address no numbered finding
Changelog only:
Carve features and fixes with no review finding (session lifecycle, clock, sites, recorder, handler options):
Fixes found by reading the code, tied to no finding id:
Docs and examples:
Style and test-only:
Numbered findings in neither range, from the PR 1 viewpoint
All A, B, C findings land in PR 1 or PR 2 (see the PR 2 table).
Findings in neither range:
frbc_simple.pyon origin/main; no plan package names it, and no commit in either range addresses it.provides_*flags as features: out of scope per plan section 9, with P2.3 making each loud (PR 2).🤖 Generated with Claude Code
https://claude.ai/code/session_01CcpawsJs74cSwFLXkz7ZwW