- Plannerstate: Complete
- Mock Builderstate: Complete
- Reviewstate: Complete · clean
- Fixstate: Waiting · decision required
- Human gatestate: Waiting · ready for review (current step)
MP-727
Formulation - Formulas - Regulatory screening - Ticket 1 : Screening tab and Alerts view (frontend + backend)
Ready for your review- Jira status
- Ready for Mock Review
- Stage
- Human review
- Your move
- Ready for review — non-blocking recommendations are available.
Jira confirmed · read 24s ago
What is happening?
Ready for your review in Jira (Ready for Mock Review) since 3 hours.
- Step
- Human gate: Waiting (ready for review)
- Outcome
- The agent's changes were published; the pull request has not confirmed them yet.
What do we need from you?
Scope, acceptance criteria and risks
Objective. A1: add a read-only Screening tab to the existing formula detail tabs. Extend FormulaDescriptionTabs with one entry; reuse RawMaterialPanel as is, SimpleDataTable for the raw materials x alerts matrix with runtime-built alert columns, and the existing getAlertLabels and getWarnings clients for data the backend already serves. Create, mock-first, only what nothing serves: screening types carrying MP-691's pre-mix and 'Not included' model locally, mock composition fixtures, pure aggregation and verdict libs, one mock api function (no apiClient, TODO(MP-727)) and one read-only hook, plus the screening components. Extract the raw-material panel host out of CompositionTab so both tabs share one exit lifecycle instead of a copy. Left out: the backend endpoint and the Java aggregation (Backend stage), any change to the live FormulaLine model, a standalone screening page, and the Origins and Natural indices views (MP-728).
Approach. Target: the formulas feature of the formulation domain, frontend/src/domains/formulation/features/formulas. Nothing under backend/ changes at this stage. 1. TYPES — types/screening.ts (new). ScreeningLineKind = "RAW_MATERIAL" | "PREMIX". ScreeningNotIncluded = null | "THIS_PREMIX" | "FINAL_FORMULA". ScreeningCompositionLine { id, kind, rawMaterialId: string | null, commercialName: string, premixCode: string | null, premixLines: ScreeningCompositionLine[] | null, phase: string, percentage: number | null, isQsp: boolean, notIncluded: ScreeningNotIncluded }. ScreeningRawMaterialRow { rawMaterialId, commercialName, totalPercentage: number, phases: string[], viaPremixCodes: string[] }. ScreeningRawMaterials { formulaId, isPremix: boolean, rows: ScreeningRawMaterialRow[] }. ScreeningCellStatus = Pictogram | "NOT_ENTERED" (Pictogram imported from the raw-materials warnings client). ScreeningCell { rawMaterialId, alertLabelId, status: ScreeningCellStatus, notes: string, lastUpdatedAt: string | null }. THIS FILE IS WHERE, AND THE ONLY PLACE WHERE, THE MP-691 DATA MODEL IS REPRESENTED. At the indexed base the line kind is a union of one value and no line carries a 'Not included' marker (E4, E5); the feature's own doc states that pre-mix lines, nested resolution and the exclusion level are MP-691 (E19), whose PR #143 is not on main and is not readable from this run. The screening types therefore mirror that model locally — "PREMIX" as a second kind, premixLines for the nesting, notIncluded for the level — and the live FormulaLine in types/index.ts is NOT edited. The backend-wired composition grid keeps its model, MP-691 can land without a conflict in it, and when it does land api/formula-screening.ts is the single place that re-maps the served lines onto these types. 2. MOCK FIXTURES — lib/screening-mock-data.ts (new). One mock formula composition as ScreeningCompositionLine[], plus a second fixture for a formula that is itself a pre-mix (AC4's second rule). The set must exercise every aggregation rule: a raw material in two phases, a raw material used directly and inside a pre-mix, a pre-mix containing a nested pre-mix, a QSP line, an empty line (rawMaterialId null), a line marked THIS_PREMIX and a line marked FINAL_FORMULA. The file is named screening-mock-data.ts rather than the usual lib/mock-data.ts because this feature is otherwise fully backend-wired and a bare mock-data.ts would read as 'the formulas feature is mock-first', which it is not. 3. AGGREGATION — lib/screening-aggregation.ts (new), pure functions only, in the style of composition-math.ts (E6). aggregateScreeningRows(lines, { isPremix }) applies, in order: (a) drop empty lines (rawMaterialId null and no premixLines) — AC2; (b) resolve the QSP line's percentage as 100 minus the sum of the other top-level lines, exactly as summariseComposition does (E6) — AC2; (c) expand pre-mix lines recursively at every nesting level, each contained raw material contributing parentPercentage x childPercentage / 100 and carrying the code of the pre-mix it came through — AC3; (d) remove 'Not included' lines — on a formula that is not a pre-mix every notIncluded line at every level, on a formula that is a pre-mix only the THIS_PREMIX ones — AC4; (e) renormalise the remaining contributions so they total 100 (each contribution divided by the surviving total, times 100) — AC4; (f) merge contributions of the same rawMaterialId into one row, summing percentages and taking the union of phases and of viaPremixCodes — AC2, AC3; (g) sort by totalPercentage descending — AC2. Rounding follows composition-math's two-decimal rule. 4. VERDICTS — lib/screening-verdict.ts (new), pure functions. alertVerdict(cells of one alert) returns "RED" when any cell is RED, otherwise "ORANGE" when any is QUESTION or NOT_ENTERED, otherwise "GREEN" — AC7; NA therefore counts as green and there is no cross-alert verdict. summariseAlert(cells, rowCount) returns the counts behind AC8's sentence (red, to check = QUESTION, not entered, total) together with the verdict so the sentence takes the verdict colour. 5. MOCK API — api/formula-screening.ts (new). async getFormulaScreeningRawMaterials(formulaId: string): Promise<ScreeningRawMaterials>, resolving the fixtures through aggregateScreeningRows. It imports NO apiClient and carries // TODO(MP-727): replace with apiClient GET /v1/formulas/{formulaId}/screening/raw-materials. This function is the contract boundary: at the flip it is the only file that changes on the data side. 6. HOOK — description/hooks/use-formula-screening.ts (new). useFormulaScreening(formulaId) owns the whole view's data and returns { rows, alertLabels, selectedAlertIds, toggleAlert, cellsByRawMaterial, loading, error, refetch }. It loads, in this order: the aggregated rows from the mock api; the alert labels from the existing getAlertLabels client (E7, E8), keeping those whose active flag is not false and sorting by name; then, for the loaded rows, getWarnings per raw material from the existing warnings client (E9), in one Promise.all. Cells are derived: for a row and an alert, the warning whose alertLabel.id matches gives status, notes and modifiedDate ?? createdDate; no matching warning gives status NOT_ENTERED with no date. The selection starts as every active alert and lives in plain useState — it is deliberately not persisted (AC5). Errors follow the feature's existing contract: the hook is the single error surface, holds an Error in error, and the tab renders a retry; a failed warnings call for one material leaves that material's cells NOT_ENTERED rather than failing the view. 7. COMPONENTS — description/components/screening/ (new folder, beside composition/). screening-tab.tsx: the tab orchestrator. Calls the hook, renders the loading skeleton, the error state with retry, the alert selector, the summary and the table, and hosts the raw-material panel. It takes selectedRawMaterial and onSelectRawMaterial from FormulaDescriptionTabs, exactly as CompositionTab does, and takes no mode or locked prop at all: the tab is read-only in every mode and status (AC1). alert-selector.tsx: the checkbox list of active alert labels over shared ui primitives. screening-table.tsx: SimpleDataTable (E17) with columns built at render — a first column for the commercial name rendered as a button that reports the material to the tab (AC9), a total-percentage column, a phases column ('A, C'), and one column per selected alert whose header renders the alert name with its verdict beneath it (AC7). Pagination props are not passed: the verdicts and the summary are computed over the whole set, so the table shows every row. screening-pictogram.tsx: the cell. Five states, no word (AC6) — GREEN a green smiley, RED a red exclamation mark, QUESTION an orange question mark, NA a green 'N/A', NOT_ENTERED an orange dashed marker visibly different from QUESTION — each with an accessible label and a shared Tooltip carrying the status, the notes and the last update date. A new component rather than the raw-material page's helper, which prints a word beside the emoji and has no 'not entered' state (E10). screening-summary.tsx: the AC8 sentence, rendered only when exactly one alert is selected, in the verdict's colour. 8. THE PANEL — composition/raw-material-panel-host.tsx (new) and composition/composition-tab.tsx (modified). The host takes { selected: SelectedRawMaterial | null, onClosed?: () => void } and holds what CompositionTab holds today: the exitingMaterial state, the timeout that guarantees removal, the derived panelMaterial / panelExiting pair and the fixed aside that renders RawMaterialPanel keyed by material id (E13, E14). Its behaviour is moved verbatim, not rewritten. CompositionTab renders the host instead of its own aside and keeps its own focus restoration to the opening cell in onClosed; ScreeningTab renders the same host and restores focus to the name button it opened from. RawMaterialPanel itself is reused unchanged (E12), and the page needs no change: it already reserves the drawer's width whenever a material is selected, whatever the active tab (E15, E16). 9. THE TAB ENTRY — description/components/formula-description-tabs.tsx (modified). TabValue becomes "composition" | "screening" | "history"; the ?tab= guard accepts the new value; one TabsTrigger labelled Screening is inserted between Composition and History (AC1) and one TabsContent renders ScreeningTab with selectedRawMaterial and onSelectRawMaterial passed through. The existing Composition and History entries are untouched. DATA FLOW: page (owns the panel selection) -> FormulaDescriptionTabs -> ScreeningTab -> useFormulaScreening -> mock api (aggregated rows) + getAlertLabels + getWarnings per row -> pure verdict helpers -> SimpleDataTable and the summary; clicking a name sets the page's selection, which the panel host turns into the drawer while the table stays beside it. WHAT THE LATER STAGES INHERIT: the response shape of getFormulaScreeningRawMaterials is the contract for GET /v1/formulas/{id}/screening/raw-materials, and lib/screening-aggregation.ts is the written specification of the rules FormulaCompositionService has to implement at the Backend stage; at the flip the hook calls the endpoint and the fixtures, the mock api and the frontend aggregation are deleted. — The ticket asks for a tab right after Composition (AC1), and the page already composes its tabs in one component where a third entry is one trigger plus one content block (E1, E2, E3) — which rules out A4, whose standalone page would also have to re-create the drawer width reservation the detail page already owns (E15). A3 is excluded by the evidence that alert labels and raw-material warnings are served today (E7, E8, E9): mocking them would be a duplication and a flip that changes nothing, so only the aggregated raw-material list — which has no source at all at the indexed base (E4, E5, E19) — is mock-first. Between A1 and A2 the question is where the panel host lives. The panel component itself is reusable unchanged (E12), but its host is two pieces of state, a timeout that guarantees removal and a fixed aside, all inside CompositionTab (E13, E14); copying them into the Screening tab would duplicate the one piece of logic in this feature whose failure mode is a drawer stuck on screen. A1 extracts them into a sibling component with the same behaviour and keeps focus restoration in each tab, so the Composition tab's observable behaviour is unchanged while the Screening tab gets the same panel for free. The matrix needs no new table: SimpleDataTable takes columns and data with pagination optional (E17), and one column definition per selected alert is built at render.
Scope
- User interface: A new screening folder in the formulas feature: types, mock fixtures, two pure lib modules, one mock api function, one hook and five components.; Two existing components edited: formula-description-tabs.tsx gains a third tab entry, composition-tab.tsx hands its panel host to a new sibling component.; No change to the formula detail page, to types/index.ts, to the composition grid's data layer or to anything under backend/.
- API and backend: None at this stage: no file under backend/ is touched and AC10's gradle gates are unaffected.; Expected at the Backend stage of this ticket: GET /v1/formulas/{formulaId}/screening/raw-materials serving the aggregated list this mock defines, with the aggregation itself in the formula composition service.; No new persistence: raw-material warnings and alert labels are already stored and already served.
Milestones
- Screening types and mock composition fixtures
- Aggregation and verdict rules as pure functions (after 1)
- Mock api and the screening hook (after 2)
- Alert selector, matrix table, cells and summary (after 3)
- Panel host extraction and the Screening tab entry (after 4)
Acceptance criteria
- The types compile and the fixtures cover a nested pre-mix, a raw material used both directly and via a pre-mix, a QSP line, an empty line and both 'Not included' levels; types/index.ts is untouched.
- On the fixtures the aggregation returns one row per raw material, pre-mixes expanded at every level, 'Not included' removed, the rest renormalised to 100 and sorted descending; the verdict and summary helpers follow AC7 and AC8.
- The api file imports no apiClient and carries TODO(MP-727) with the future endpoint; the hook returns rows, active alert labels, the full initial selection, derived cells, loading and error, reading labels and warnings from the existing clients.
- The view shows name, total percentage and phases plus one column per selected alert with wordless pictograms, hover detail, a verdict under each header and the single-alert sentence in the verdict colour.
- npm run build passes; Screening appears between Composition and History; clicking a name opens the shared panel with the table still visible; the Composition tab's panel opens, animates and closes exactly as before.
- id: RC1 · check: The formula detail page shows a Screening tab between Composition and History, it opens from ?tab=screening, and it renders the same read-only view whatever the grid mode and whatever the formula status (no edit affordance anywhere in the tab).
- id: RC2 · check: On the mock fixtures each raw material appears on exactly one row with its commercial name, total percentage and phases joined as 'A, C'; the empty line produces no row, the QSP line counts with its derived percentage, and rows are ordered by total percentage, highest first.
- id: RC3 · check: Pre-mix lines are expanded at every nesting level: each contained raw material contributes parent % x child % / 100, shows 'via pre-mix <code>', and a raw material used both directly and through a pre-mix has one row whose percentage is the sum of both contributions.
- id: RC4 · check: On an ordinary formula every 'Not included' line is dropped, including inside pre-mixes at any level; on the pre-mix fixture only the 'this pre-mix' lines are dropped and the 'final formula' ones are kept; after removal the remaining percentages total 100.
- id: RC5 · check: The alert selector lists the active alert labels from the real alert-label client, every one is selected when the view opens, each can be toggled, and leaving and returning to the tab restores the full selection rather than the previous one.
- id: RC6 · check: Every cell is a pictogram with no word, with the five distinct states GREEN, RED, QUESTION, NA and no-status-entered (an orange dashed marker clearly different from the orange question mark), and hovering a cell shows the status, the notes and the date of the warning's last update.
- id: RC7 · check: Each selected alert shows a verdict under its own column header: red when any raw material is RED, otherwise orange when any is QUESTION or has no status entered, otherwise green; NA alone stays green and no overall verdict across alerts is rendered anywhere.
- …and 7 more in the full plan.
Risks and dependencies
- The mock's pre-mix and 'Not included' shape may differ from the model MP-691 merges, since PR #143 is not on main and is not readable from this run. — Keep that shape only in types/screening.ts and the fixtures, never in the live FormulaLine, so the re-map when MP-691 lands is confined to api/formula-screening.ts.
- Extracting the panel host edits the live MP-690 Composition tab and could regress the drawer's animation, its removal timeout or focus restoration. — Move the state, the effect and the aside verbatim behind the host, keep focus restoration in each tab, and verify the Composition tab's open, close, Escape and reduced-motion paths (RC10).
- The hook issues one warnings request per aggregated raw material, which grows with the formula. — The fixtures keep the row count small at this stage, the calls run in one Promise.all, and the Backend stage may fold the cells into the screening endpoint, changing the hook only.
- AlertLabelResponse.active is optional, so an undefined flag could silently hide labels from the AC5 selection. — Treat undefined as active (keep active !== false) and confirm the served default at the Backend stage.
- The frontend aggregation is a stage artefact: the Backend stage ports it to Java and the flip deletes it, so rules buried in components would be lost or duplicated. — Keep every rule in the two pure lib modules with no React or api import, and review for rules leaking into components (RC13).
- Frontend only at this stage: no file under backend/ and no Flyway migration.
- api/formula-screening.ts and lib/screening-mock-data.ts import no @/shared/lib/api-client; the api function carries TODO(MP-727) naming the future endpoint.
- Alert labels and raw-material warnings are read through the existing raw-materials clients, never mocked.
- types/index.ts and the composition grid's FormulaLine model are not edited: MP-691's shape lives only in types/screening.ts.
- The Screening tab is read-only in every mode and every status, and takes no edit affordance.
- No new npm package, and no new shared component: the matrix uses SimpleDataTable and shared ui primitives.
- The Composition tab's panel behaviour must be observably unchanged after the host extraction.
Expected result
Mock builder takes over the Mock stage from this plan.
Delivery outcome
- Latest accepted result
- Technically validated · Review ·
- Pull request
- PR #152 (opens in a new tab)
The agent's changes were published; the pull request has not confirmed them yet.
Earlier attempts
- History Handoff: Technically validated · Review · 2026-10-17 07:39Z
- History Handoff: Handoff not accepted · Review · 2026-10-17 07:18Z
- History Stop record: Decision or dependency required · Review · 2026-10-17 07:18Z
- History Handoff: Corrections requested · Review · 2026-10-17 07:06Z
- History Handoff: Technically validated · Mock · 2026-10-17 06:53Z
- History Handoff: Technically validated · Plan · 2026-10-17 06:09Z
- History Stop record: Human answer needed · Plan · 2026-10-17 05:28Z
- History Handoff: Handoff not accepted · Plan · 2026-10-17 05:14Z
- History Stop record: Decision or dependency required · Plan · 2026-10-17 05:14Z
Technical details
Technical
- 0 Failed
- 1 Not executable here
- 0 Not run
- 4 Passed
Ready for review — non-blocking recommendations are available.
Recommendations did not trigger an automatic Fixer.
Agent-generated evidence — verify before acting
Blocking 0
No blocking finding.
Recommendations 6
| Id | Severity | File | Finding |
|---|---|---|---|
| F1 | MINOR | docs/superpowers/specs/2026-10-17-MP-727-screening-tab-design.md | Design doc still states "the rows total 100" three lines above the owner's amendment saying the displayed sum may differ |
| F2 | MINOR | docs/superpowers/specs/2026-10-17-MP-727-screening-tab-design.md | Design doc's aggregation rules omit the "Not included" inheritance rule and understate the pre-mix route |
| F3 | MINOR | [redacted]-pictogram.tsx | formatUpdated re-derives the shared formatFullDate helper |
| F4 | ADVISORY | frontend/src/domains/formulation/features/formulas/api/formula-screening.ts | The mock decides isPremix from the formula id, so the preview's AC4 branch is unrelated to whether the formula really is a pre-mix |
| F5 | ADVISORY | frontend/src/domains/formulation/features/formulas/description/hooks/use-formula-screening.ts | Alert labels and per-material warnings are read from page 0 only, so anything past 200 is dropped silently |
| F6 | ADVISORY | [redacted]-table.tsx | A merged row's "via pre-mix" label cannot distinguish a partly-direct material from a wholly pre-mixed one |
No fix needed.