- Plannerstate: Complete
- Mock Builderstate: Blocked · technical block
- 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)
Human review gate- Jira status
- Ready for Mock Review
- Stage
- Human review
- Your move
- Ready for review — non-blocking recommendations are available.
Ready for your review in Jira (Ready for Mock Review) since 4 hours.
- Jira
- Jira confirmed
- Jira read
- Lifecycle activity
- · Verified pull request update
Next action
- Routine
- Mock Builder (mock-builder)
- Pull request
- #152
- Tests
- 4 passed · 0 failed · 1 not executable · 0 not run
- Review
- Clean · 6 recommendations
- Attempt
- Mock · stage attempt entered 2026-10-17T06:59:53.987+0200
- Open requirements
- 1 open (1 request)
Your request was not accepted.
Resume work on PR #152 (branch claude/mock-MP-727-screening-tab). Apply the PO answer of 2026-10-17 to the open question on AC2. # Screening tab, Alerts view, "Phases" column: for a raw material that comes from a pre-mix, the column currently shows the raw material's phase inside the pre-mix (Tocopherol and Sunflower Seed Oil show "B" while their pre-mix line sits in phase A). Expected: it shows the phase of the pre-mix line in the formula being screened ("A" in that example), at every nesting level. A raw material that is both direct and from a pre-mix lists the phases of both contributions in the screened formula. (AC2, PO decision 2026-10-17) Do not change the mock raw-material ids or the warnings data source: the "not entered" cells on the mock preview are accepted and will be checked at the Fullstack Review. Do not change anything else in the PR.
The triage result could not be used; the ticket stays at the gate. request-routing-invalid
Next: Request agent work at the gate.
- Routing
- not triaged yet → decision
REFUSED - Human finding
- —
- Fix
- —
- Plan revision
sha256:5f2657d3b663ceec90a4411ab0d94e2180d5b2adc7a5c6df0175c453fec99050· accepted planfeature-contract-febd26166ca56c084d9762e4f264ce9e·8f0d19a5325d- Bound PR / SHA
- #152 @
85ab1b06085d - Lifecycle
- Not accepted · 2026-10-17 11:35Z
- Triage RAG context
- not collected
Plan and implementation
Plan
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.
Implementation steps
- 1Screening types and mock composition fixtures
Mock builder
frontend/src/domains/formulation/features/formulas/types/screening.tsfrontend/src/domains/formulation/features/formulas/lib/screening-mock-data.ts
- 2Aggregation and verdict rules as pure functions
Mock builder · after step 1
frontend/src/domains/formulation/features/formulas/lib/screening-aggregation.tsfrontend/src/domains/formulation/features/formulas/lib/screening-verdict.ts
- 3Mock api and the screening hook
Mock builder · after step 2
frontend/src/domains/formulation/features/formulas/api/formula-screening.tsfrontend/src/domains/formulation/features/formulas/description/hooks/use-formula-screening.ts
- 4Alert selector, matrix table, cells and summary
Mock builder · after step 3
frontend/src/domains/formulation/features/formulas/description/components/screening/screening-tab.tsxfrontend/src/domains/formulation/features/formulas/description/components/screening/alert-selector.tsxfrontend/src/domains/formulation/features/formulas/description/components/screening/screening-table.tsxfrontend/src/domains/formulation/features/formulas/description/components/screening/screening-pictogram.tsxfrontend/src/domains/formulation/features/formulas/description/components/screening/screening-summary.tsx
- 5Panel host extraction and the Screening tab entry
Mock builder · after step 4
frontend/src/domains/formulation/features/formulas/description/components/composition/raw-material-panel-host.tsxfrontend/src/domains/formulation/features/formulas/description/components/composition/composition-tab.tsxfrontend/src/domains/formulation/features/formulas/description/components/formula-description-tabs.tsx
Implementation
- Latest accepted result
- Technically validated · Review ·
- Pull request
- PR #152 (opens in a new tab)
- Verified review commit
85ab1b06085dCommit bound and verified by the Operator for the last review. Not necessarily the PR head.- Last attempted commit
85ab1b06085dCommit used by the latest agent attempt. Not necessarily the PR head.
Output commits
- Local (attempted) commit — diagnostic only
cc592a407f2f- Published commit (Broker receipt push-1327)
3f6528f22374- Verified PR head
- not verified yet
Test matrix
- 0 Failed
- 1 Not executable here
- 0 Not run
- 4 Passed
Gate reasons are reported by the agent. Agent-generated evidence — verify before acting
| Gate | Status | Command | Reason |
|---|---|---|---|
| backend unit tests | Not executable here | cd backend && ./gradlew test --no-daemon | 1478 of 2252 tests passed; all 774 failures are Spring ApplicationContext load failures caused by Testcontainers finding no Docker in this pod (Could not find a valid Docker environment / Keycloak container is not running). No backend file is changed at this stage. |
| backend spotlessCheck | Passed | cd backend && ./gradlew spotlessCheck --no-daemon | — |
| frontend lint | Passed | cd frontend && npm run lint | 0 errors; 13 warnings, all pre-existing react-hooks/incompatible-library in files this change does not touch |
| frontend production build | Passed | cd frontend && npm run build | — |
| mock-only wiring (no new apiClient import on the diff) | Passed | git diff --staged --name-only 3830b223cba49f2cf0cbffc1547d46f031e0d747 | grep -E '\.tsx?$' | xargs -r grep -nE "from ['\"]@/shared/lib/api-client" | No match across the 15 changed .ts/.tsx files; api/formula-screening.ts carries TODO(MP-727) |
Review findings and fixer
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.
Agent work requests
Your request was not accepted.
Resume work on PR #152 (branch claude/mock-MP-727-screening-tab). Apply the PO answer of 2026-10-17 to the open question on AC2. # Screening tab, Alerts view, "Phases" column: for a raw material that comes from a pre-mix, the column currently shows the raw material's phase inside the pre-mix (Tocopherol and Sunflower Seed Oil show "B" while their pre-mix line sits in phase A). Expected: it shows the phase of the pre-mix line in the formula being screened ("A" in that example), at every nesting level. A raw material that is both direct and from a pre-mix lists the phases of both contributions in the screened formula. (AC2, PO decision 2026-10-17) Do not change the mock raw-material ids or the warnings data source: the "not entered" cells on the mock preview are accepted and will be checked at the Fullstack Review. Do not change anything else in the PR.
The triage result could not be used; the ticket stays at the gate. request-routing-invalid
Next: Request agent work at the gate.
- Routing
- not triaged yet → decision
REFUSED - Human finding
- —
- Fix
- —
- Plan revision
sha256:5f2657d3b663ceec90a4411ab0d94e2180d5b2adc7a5c6df0175c453fec99050· accepted planfeature-contract-febd26166ca56c084d9762e4f264ce9e·8f0d19a5325d- Bound PR / SHA
- #152 @
85ab1b06085d - Lifecycle
- Not accepted · 2026-10-17 11:35Z
- Triage RAG context
- not collected
Accepted handoffs and binding
| When | Step | Result | Record |
|---|---|---|---|
| 2026-10-17 07:39Z | review (mock) | CLEAN | workflow-handoff-717d5f5dcaad3df2cb7736f28a6ed47d |
| 2026-10-17 07:06Z | review (mock) | ISSUES | workflow-handoff-75b73e2e277e02afb37a4c64a45e8d9f |
| 2026-10-17 06:53Z | build (mock) | CLEAN | workflow-handoff-24d53062890f91981ebcab2d21a460ee |
| 2026-10-17 06:09Z | plan (mock) | CLEAN | workflow-handoff-febd26166ca56c084d9762e4f264ce9e |
- Current binding
workflow-handoff-717d5f5dcaad3df2cb7736f28a6ed47d·142dc6e81185- Branch / last attempted commit
claude/mock-MP-727-screening-tab@85ab1b06085d
Historical 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
Raw records and logs
Raw
Raw records and logs are never loaded on this page. Open them on the technical views: