Skip to content
Agent Delivery Studio
  1. Plannerstate: Complete
  2. Mock Builderstate: Blocked · technical block
  3. Reviewstate: Complete · clean
  4. Fixstate: Waiting · decision required
  5. 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.

Current: Verified pull request update on 2026-10-17 11:35Z (less than an hour ago).

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)

You

Review needed

Ready for review — non-blocking recommendations are available.

Recommendations did not trigger an automatic Fixer.

Agent work request · Not accepted2026-10-17 11:22Z · requested by admin systemease

Your request was not accepted.

Human request text — verbatim from Jira (secrets masked)
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 plan feature-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

  1. 1Screening types and mock composition fixtures

    Mock builder

    • frontend/src/domains/formulation/features/formulas/types/screening.ts
    • frontend/src/domains/formulation/features/formulas/lib/screening-mock-data.ts
  2. 2Aggregation and verdict rules as pure functions

    Mock builder · after step 1

    • frontend/src/domains/formulation/features/formulas/lib/screening-aggregation.ts
    • frontend/src/domains/formulation/features/formulas/lib/screening-verdict.ts
  3. 3Mock api and the screening hook

    Mock builder · after step 2

    • frontend/src/domains/formulation/features/formulas/api/formula-screening.ts
    • frontend/src/domains/formulation/features/formulas/description/hooks/use-formula-screening.ts
  4. 4Alert selector, matrix table, cells and summary

    Mock builder · after step 3

    • frontend/src/domains/formulation/features/formulas/description/components/screening/screening-tab.tsx
    • frontend/src/domains/formulation/features/formulas/description/components/screening/alert-selector.tsx
    • frontend/src/domains/formulation/features/formulas/description/components/screening/screening-table.tsx
    • frontend/src/domains/formulation/features/formulas/description/components/screening/screening-pictogram.tsx
    • frontend/src/domains/formulation/features/formulas/description/components/screening/screening-summary.tsx
  5. 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.tsx
    • frontend/src/domains/formulation/features/formulas/description/components/composition/composition-tab.tsx
    • frontend/src/domains/formulation/features/formulas/description/components/formula-description-tabs.tsx

Implementation

Latest accepted result
Technically validated · Review ·
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

From build report workflow-artifact-ffe14a5aecfcc383c2107dcbfb3a724030ac9b8b · 2026-10-17 06:53Z · accepted by a handoff

Gate reasons are reported by the agent. Agent-generated evidence — verify before acting

GateStatusCommandReason
backend unit testsNot executable herecd backend && ./gradlew test --no-daemon1478 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 spotlessCheckPassedcd backend && ./gradlew spotlessCheck --no-daemon—
frontend lintPassedcd frontend && npm run lint0 errors; 13 warnings, all pre-existing react-hooks/incompatible-library in files this change does not touch
frontend production buildPassedcd frontend && npm run build—
mock-only wiring (no new apiClient import on the diff)Passedgit 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

Review cleanADVISORY 3MINOR 32026-10-17 07:39Z

Verified review commit 85ab1b06085d

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

IdSeverityFileFinding
F1MINORdocs/superpowers/specs/2026-10-17-MP-727-screening-tab-design.mdDesign doc still states "the rows total 100" three lines above the owner's amendment saying the displayed sum may differ
F2MINORdocs/superpowers/specs/2026-10-17-MP-727-screening-tab-design.mdDesign doc's aggregation rules omit the "Not included" inheritance rule and understate the pre-mix route
F3MINOR[redacted]-pictogram.tsxformatUpdated re-derives the shared formatFullDate helper
F4ADVISORYfrontend/src/domains/formulation/features/formulas/api/formula-screening.tsThe mock decides isPremix from the formula id, so the preview's AC4 branch is unrelated to whether the formula really is a pre-mix
F5ADVISORYfrontend/src/domains/formulation/features/formulas/description/hooks/use-formula-screening.tsAlert labels and per-material warnings are read from page 0 only, so anything past 200 is dropped silently
F6ADVISORY[redacted]-table.tsxA 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

Agent work request · Not accepted2026-10-17 11:22Z · requested by admin systemease

Your request was not accepted.

Human request text — verbatim from Jira (secrets masked)
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 plan feature-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

WhenStepResultRecord
2026-10-17 07:39Zreview (mock)CLEANworkflow-handoff-717d5f5dcaad3df2cb7736f28a6ed47d
2026-10-17 07:06Zreview (mock)ISSUESworkflow-handoff-75b73e2e277e02afb37a4c64a45e8d9f
2026-10-17 06:53Zbuild (mock)CLEANworkflow-handoff-24d53062890f91981ebcab2d21a460ee
2026-10-17 06:09Zplan (mock)CLEANworkflow-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

Projected 2026-10-17 12:01Z · Delivery view · All workflows