diff --git a/CONTEXT.md b/CONTEXT.md index 9d60223d..33bf900a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -105,7 +105,7 @@ A Scenario constraint that takes a measure off the table for the optimiser. The _Avoid_: inclusions, allowed measures (as a stored concept — UI may present "allowed/excluded" toggles, but what's captured is the exclusion set) **Modelling run**: -One user-initiated act of triggering modelling: a set of Scenarios crossed with one filter-defined property selection within a Portfolio. An insert-only record of *what was asked* — the Run filters, the Scenarios, who triggered it, when, and the matched-property count shown at preview — so Plans can be traced back to the selection that produced them. Execution progress is a separate concern (the pipeline's task records own it); a Modelling run is the request and its provenance, not the work. A Scenario is the *question*; a Modelling run *asks* it (possibly again, possibly for a different property selection). Re-running an already-modelled property appends a newer Plan; latest wins on read. +One user-initiated act of triggering modelling: a set of Scenarios crossed with one filter-defined property selection within a Portfolio. A durable record of *what was asked* — the Run filters, the Scenarios, who triggered it, when, and the matched-property count shown at preview — so Plans can be traced back to the selection that produced them, alongside how the work is going. A Scenario is the *question*; a Modelling run *asks* it (possibly again, possibly for a different property selection). Re-running an already-modelled property appends a newer Plan; latest wins on read. _Avoid_: job, batch, trigger request, modelling task (the pipeline's execution records) **Run filter**: diff --git a/docs/adr/0008-modelling-runs-filters-to-distributor.md b/docs/adr/0008-modelling-runs-filters-to-distributor.md index 9d72e06f..dab4e11e 100644 --- a/docs/adr/0008-modelling-runs-filters-to-distributor.md +++ b/docs/adr/0008-modelling-runs-filters-to-distributor.md @@ -23,14 +23,23 @@ explicit marker when trigger wiring lands. This is that wiring. ## Decision -- **A `ModellingRun` is recorded, insert-only, by Next.js at trigger time**: - portfolio, the selected scenario ids, the Run filters, the matched-property - count shown at preview, who triggered it, when. It is the request and its - provenance — Plans become traceable to the selection that produced them. - It carries no status column (mirrors ADR-0003's derived-state rule). -- **The distributor receives `{run_id, portfolio_id, scenario_ids, filters}`** +- **A Modelling run is recorded as an app-created task** — the BulkUpload + trigger convention (`triggerAddressMatching`): Next.js inserts the `tasks` + row (service `modelling_run`, `source_id` = portfolio, status `waiting`) + plus one config `sub_task` whose `inputs` JSON carries + `{scenario_ids, filters, previewed_property_count, triggered_by}`, then + passes `task_id` to the distributor, which attaches its execution subtasks + to the same task. **No new table, no migration.** Run history and the + per-scenario in-flight badge read the portfolio's own `modelling_run` tasks + (portfolio-scoped, a handful of rows) and parse the app-authored inputs + JSON; execution progress ("61 of 104 ready") falls out of the existing + subtask-count summary pattern. +- **The distributor receives `{task_id, portfolio_id, scenario_ids, filters}`** — filters, never property-id lists. The payload stays tiny at any portfolio - size; the run row is the durable copy of the same config. + size; the config subtask is the durable copy of the same request. +- **Dispatch failure marks the task `failed`** (an improvement on the + bulk-upload flow, which leaves failed triggers at `waiting`), so history + never shows a ghost run as pending. - **The modelling backend is the single authority for resolving filters to properties.** Resolution precedence: landlord-override fact → EPC-derived value (new-approach EPC graph, lodged over predicted, or legacy property-row @@ -46,12 +55,11 @@ explicit marker when trigger wiring lands. This is that wiring. the static canonical enums plus Unknown. Filters are bounded (cap the postcode multi-select) as a UX guard; "all" is an absent filter, never an enumeration. -- **Run status is derived by correlation, not stored**: the distributor stamps - `run_id` on the task records the pipeline already writes - (`tasks`/`sub_tasks`, service `plan_engine`). Scenario badges gain a third - derived state — "Modelling in progress" — and a per-portfolio run history - lists each run's filters, counts, initiator and derived status. No live - progress bars in v1. +- **Run status is the task's status** — no correlation machinery needed: the + run and the execution share one record by construction. Scenario badges + gain a third derived state — "Modelling in progress" — and a per-portfolio + run history lists each run's filters, counts, initiator and derived status + with subtask-count progress. - **Concurrent runs warn, never block**: triggering a scenario with a run in flight shows who started it and when, but proceeds. Plans append and latest-per-property wins on read, so overlap is safe; a hard lock could leak @@ -59,6 +67,13 @@ explicit marker when trigger wiring lands. This is that wiring. ## Alternatives considered +- **A dedicated `modelling_run` table (+ scenarios join table)**: relational + scenario linkage and typed columns for who/filters/counts, but a migration, + a second record system beside tasks, and correlation machinery (either the + backend stamping run ids into tasks, or the app storing returned task ids). + The task system already records app-initiated pipeline work with inputs, + progress and status — and the badge/history queries are portfolio-scoped + over a handful of rows, so the JSON parse costs nothing that matters. - **Pass resolved property ids to the distributor**: makes preview-vs-run drift impossible by construction, but ~50k ids is a payload the async entry point can't take, and it moves the resolution rule into the app — the @@ -74,9 +89,10 @@ explicit marker when trigger wiring lands. This is that wiring. ## Consequences -- Two backend asks must land with the distributor: accept + persist `run_id` - into task records, and expose the dry-run/count endpoint sharing the - resolution code path. +- Two backend asks must land with the distributor: accept an app-created + `task_id` and attach execution subtasks to it (rather than creating its own + task, as the current `plan_engine` entrypoint does), and expose the + dry-run/count endpoint sharing the resolution code path. - The preview's already-modelled callout and matched counts are only as fresh as trigger time; a property added between preview and trigger is picked up by the run (filters re-resolve) — the run row records the previewed count so