Skip to content
Agent Delivery Studio
  1. Plannerstate: Complete
  2. DB Designerstate: Complete
  3. Reviewstate: Not started · no automated review recorded for this stage
  4. Fixstate: Not started
  5. Human gatestate: Waiting · ready for review (current step)

MP-701

Raw Materials - Multisourcing - Ticket 1 : dedicated tab and group management

Human review gate
Jira status
DB Review
Stage
Human review
Your move
Ready for review — non-blocking recommendations are available.

Ready for your review in Jira (DB Review) since 3 days.

Current: Agent run on 2026-10-14 06:36Z (3 days ago).

Jira
Jira confirmed
Jira read
Lifecycle activity
· Agent run

Database design to review

What was produced: the DB Designer’s deliverable. The plan further down is what the Planner requested.

Migrations
Design only — no migration included
API impact
10 API impacts described
Published commit
396b8b28f3fa · Broker receipt push-1246
API impacts10

Agent-generated evidence — verify before acting

EndpointChangeReason
GET /v1/raw-materials/{id}/multisourcing-groupServed by a lookup on multisourcing_group_member.rawmaterial_id (unique), joined to multisourcing_group, with memberCount as a COUNT projection. 404 means the material is in no group.The mock's getGroupForRawMaterial swap marker; the unique key on rawmaterial_id is the at-most-one-group invariant and also the index this read uses (design Q1).
GET /v1/raw-materials/{id}/multisourcing-group/candidates?search=Set-equality query over the persisted identity rows (substance_id, percentage NUMERIC(10,6)), plus active = TRUE and not-already-a-member; search is literal containment over the joined code and name. Row cap undecided: Pageable or a stated hard cap.AC4, enforced server-side on the substance id and never a name. The mock marker asks for a persisted, indexed identity so this is an equality probe, not a per-request re-derivation; the row cap is the one shape the mock leaves open (question 2).
POST /v1/multisourcing-groupsOne transaction: the group row, one member row, and N identity rows aggregated as SUM(percentage_used) GROUP BY substance_id over the founding material's ACTIVE composition. Duplicate-code pre-check on LOWER(code), backed by the unique index.AC6 and AC-E2 need the refusal in any letter case across active and inactive groups; the identity rows are written once here and never recomputed (design section 5), and SUM hides a NULL percentage so the writer must assert completeness itself.
PUT /v1/raw-materials/{id}/multisourcing-groupRe-checks the identity rule for the single target and that the target is active, then inserts one member row; the unique key on rawmaterial_id is the ALREADY_IN_GROUP backstop.The candidate list is a read and can go stale; a retired group must never be rejoinable under its historical code, which is a service rule because active is a legal value of a legal row.
DELETE /v1/raw-materials/{id}/multisourcing-groupDeletes one multisourcing_group_member row, counts the remaining members and sets active = FALSE when it reaches zero. Idempotent; touches no pricing table.AC8: the group is never deleted, and no pricing table appears in either statement, which is what makes 'leaving touches no pricing data' structural rather than a promise.
GET /v1/multisourcing-groups?search=&page=&size=&sort=code,asc,ignorecasePaged read over every group, active and inactive, ordered by LOWER(code) (the same expression as the unique index), with totalElements from COUNT(*) and memberCount from a COUNT projection; search is literal containment over the space-joined code, name and comment, applied before the page is cut.AC-E4 to AC-E6. No compatibility, activity or membership filter may be applied here; the emitted SQL must spell LOWER(code) for the index to serve the order.
PATCH /v1/multisourcing-groups/{groupId}Updates code, name and comment only, with the duplicate-code check excluding the edited row (id <> :groupId). Name and comment are clearable: plain nullable request components assigned unconditionally, never Optional<T>.AC-E1 to AC-E3. The identity rows, the member list and active are never touched by this write, which is what makes editing from a catalog of retired groups safe; Jackson maps an omitted key and an explicit null alike to Optional.empty(), which would silently discard a clear.
DELETE /v1/raw-materials/{id} (existing)Must delete the raw material's multisourcing_group_member row and deactivate an emptied group before deleting the material; fk_multisourcing_group_member_rawmaterial is ON DELETE RESTRICT, so omitting the step aborts the delete.RawMaterialService.delete removes children by explicit repository call, so a new table is not in that list; under ON DELETE CASCADE the member row would vanish with no service code running and AC8's deactivation would be skipped silently.
DELETE /v1/substances/{id} (existing)Needs a pre-check against multisourcing_group_composition producing a readable in-use refusal; fk_multisourcing_group_composition_substance is ON DELETE RESTRICT.SubstanceService.delete carries no in-use guard today, so without the pre-check the new foreign key turns that endpoint into a raw constraint error rather than a refusal a user can act on.
/v1/raw-materials/{rawMaterialId}/multi-sourcing (existing, removed)All five endpoints are removed with the legacy stack (entity, DTOs, mapper, repository, service, controller) and the RawMaterialService call site, in the same change as DROP TABLE rawmaterial_multisourcing.AC9's sweep. ddl-auto is validate, so the entity and the drop must land together; the ticket's description of the stack as unused is true of the frontend only, as the controller is mounted and the table is writable today.
ProvenanceTechnical
Repository
cosmocomply/monorepo
Published commit
396b8b28f3fa
Push receipt
push-1246
Design document blob
94819597258c
Design artifact
workflow-artifact-e0bd6af273adbee1f23548ff195061aa3bd9f1f3 · e0bd6af273ad · 2026-10-14 06:36Z
Accepted by handoff
workflow-handoff-09c8980a9693f80c88f6953d90942988 · CLEAN · e374194c6081 · 2026-10-14 06:36Z
Validated against contract
feature-contract/v2.1 · 46f9da7f4aa4
Produced by
db-designer-mp-701-09c8980a

Next action

Routine
DB Designer (db-designer)
Pull request
#132
Tests
6 passed · 0 failed · 0 not executable · 0 not run
Review
Human DB review only; no automated review recorded

You

Review needed

Ready for review — non-blocking recommendations are available.

Plan and implementation

Plan

What was requested: the Planner’s plan for this stage. What the DB Designer produced is under “Database design to review”.

Objective. Three new tables (A1): multisourcing_group with code, name, comment, active and audit columns and a unique index on LOWER(code), so a duplicate code is refused in any letter case across active and inactive groups; multisourcing_group_member as a junction with UNIQUE (rawmaterial_id) - the house way to say at most one group per raw material - and with no order column; and multisourcing_group_composition holding the group's identity as one (substance_id, percentage NUMERIC(10,6)) row per substance, written once at creation from the founding material's active composition so the identity survives the group losing every member. The legacy rawmaterial_multisourcing table is dropped in the same V035 migration. Reused unchanged: rawmaterial, the three composition tables, rawmaterial_manufacturer, the AuditableEntity audit columns and the junction pattern. Left out: a column on rawmaterial, a fingerprint hash column, any ordering column, the Flyway file and the entities (Backend stage), and every rule the service owns.

Approach. Target: three new tables in the cosmoreg-core schema plus the removal of the legacy stack's table. Illustrative DDL only - this stage produces a design document; the canonical Flyway file is authored at the Backend stage as V035__Create_Multisourcing_Group_Tables.sql (V034 is the highest version at the base and main added none, E9, E20), and entity and migration land together because ddl-auto is validate (E10). 1. multisourcing_group - the aggregate. id UUID PRIMARY KEY DEFAULT gen_random_uuid(); code VARCHAR(50) NOT NULL - the length is a named business rule (AC-E2: any text up to 50 characters), which is why it is not TEXT (E10); name VARCHAR(150), comment VARCHAR(500) - both nullable: AC-E3's blank values are stored as NULL, which is what the mock's write path does (E19); active BOOLEAN NOT NULL DEFAULT TRUE - declared inside the CREATE TABLE per the convention (E11); set FALSE when the last member leaves, and the row is never deleted (AC8); audit columns created_date/modified_date TIMESTAMPTZ and created_by/modified_by VARCHAR(100), nullable, supplied by AuditableEntity and SystemAwareAuditingEntityListener (E13); CONSTRAINT chk_multisourcing_group_code_trimmed CHECK (code = BTRIM(code) AND code <> '') - makes the whitespace-insensitivity of the uniqueness rule a property of the data rather than of one service method, and keeps the mandatory code from being blank; CREATE UNIQUE INDEX uq_multisourcing_group_code_lower ON multisourcing_group (LOWER(code)) - the AC-E2 and AC6 refusal, case-insensitive across active and inactive rows, and it also serves the catalog's ORDER BY LOWER(code) (the mock's sort=code,asc,ignorecase, E14). No second plain UNIQUE (code): the functional index subsumes it. An index on active is deliberately left to 'consider later' - the catalog does not filter on it and the candidate query is already narrowed by the identity rows. 2. multisourcing_group_member - membership, one row per member. id UUID PRIMARY KEY DEFAULT gen_random_uuid(); group_id UUID NOT NULL, CONSTRAINT fk_multisourcing_group_member_group FOREIGN KEY (group_id) REFERENCES multisourcing_group (id) ON DELETE RESTRICT - a group is never deleted, so a cascade would describe something that must not happen; rawmaterial_id UUID NOT NULL, CONSTRAINT fk_multisourcing_group_member_rawmaterial FOREIGN KEY (rawmaterial_id) REFERENCES rawmaterial (id) ON DELETE CASCADE - a deleted raw material has no membership; audit columns as above; CONSTRAINT uk_multisourcing_group_member_rawmaterial UNIQUE (rawmaterial_id) - this is the 'at most one group per raw material' rule, enforced by the database exactly as rawmaterial_manufacturer enforces its own (E7), and it serves the lookup 'which group does this material belong to'; CREATE INDEX idx_multisourcing_group_member_group ON multisourcing_group_member (group_id) - serves the member count shown on the identity card and in the catalog, and the 'did the last member just leave' check of AC8; No order, priority or rank column: the ticket states this ticket gives a group members, not a ranking, and that nothing here should record it; the sourcing priority arrives with MP-703. 3. multisourcing_group_composition - the group's identity, one row per substance. id UUID PRIMARY KEY DEFAULT gen_random_uuid(); group_id UUID NOT NULL, CONSTRAINT fk_multisourcing_group_composition_group FOREIGN KEY (group_id) REFERENCES multisourcing_group (id) ON DELETE CASCADE; substance_id UUID NOT NULL, CONSTRAINT fk_multisourcing_group_composition_substance FOREIGN KEY (substance_id) REFERENCES substance (id) ON DELETE RESTRICT; percentage NUMERIC(10,6) NOT NULL - the same type and scale as ingredient_composition.percentage_used and allergen_composition.percentage_used (E4, E5) and as the mock's 6-decimal rounding (E15), so two equal compositions always compare equal; NOT NULL is what makes the blank-percentage refusal structural (E6); audit columns as above; CONSTRAINT uk_multisourcing_group_composition_substance UNIQUE (group_id, substance_id) - one row per substance, which is sound because a substance cannot sit in both the ingredient and the allergen list of one composition (stated on the ticket, s17) and the rule merges the two lists (E15); CONSTRAINT chk_multisourcing_group_composition_percentage CHECK (percentage >= 0); CREATE INDEX idx_multisourcing_group_composition_substance ON multisourcing_group_composition (substance_id, percentage) - leading column first, serving the candidate query's lookup by (substance, percentage) pair; Writer, named once: the service inserts these rows in the same transaction as the group, from the founding material's ACTIVE composition (E3). Nothing else writes them and nothing recomputes them; interception of a later composition change is the ticket's excluded scope, and the design document says so where the derivable-data rule of backend/CLAUDE.md asks (E10). Named queries the indexes exist for. Q1 which group does this raw material belong to - multisourcing_group_member by rawmaterial_id (uk_...). Q2 the All groups catalog page - multisourcing_group ORDER BY LOWER(code) with limit and offset plus the total, and the member count per group from multisourcing_group_member (idx_..._group); the search narrows code and name (ILIKE); a trigram index is 'consider later', not core. Q3 join candidates (AC4) - the joiner's N identity pairs are matched against multisourcing_group_composition: the groups whose rows all match and whose own row count is also N, intersected with active = TRUE and with the absence of a member row for this material. The (substance_id, percentage) index serves the match, uk_..._substance guarantees the counts are comparable, and the equality is on the substance id, never on a name (E15). Q4 duplicate code (AC6, AC-E2) - uq_multisourcing_group_code_lower, plus a service pre-check so the user gets the field-level DUPLICATE_GROUP_CODE message rather than a raw constraint error (E14, and the invariant-placement table of E10). Q5 the last member left (AC8) - count members by group_id, then set active = FALSE; the row stays. Entities the Backend stage must land with the migration (ddl-auto validate, E10): MultisourcingGroup, MultisourcingGroupMember and MultisourcingGroupComposition, each extending AuditableEntity with @GeneratedValue(GenerationType.UUID) and SystemAwareAuditingEntityListener, FK joins named exactly as the migration names them (E13). None of the new names contains any AC9 identifier. Legacy sweep. DROP TABLE rawmaterial_multisourcing (no other table references it, E1) together with its entity, DTOs, mapper, repository, service and controller, in the same V035 migration and the same Backend-stage change (ticket s6; inherited Mock decision D7). The design document must state the one awkward consequence: that DROP statement necessarily spells the identifier AC9 greps for, so the gate has to read AC9 as 'no live code references it' with the removing migration as its single allowed occurrence. What the schema deliberately does not express, because the service owns it (E10's invariant table): the no-manufacturer refusal (read from rawmaterial_manufacturer, E7), the no-active-composition and blank-percentage refusals (read from rawmaterial_composition and its entry tables, E3, E6), the compatibility decision itself, and the choice to deactivate a group when its last member leaves. Multi-tenancy: names stay unqualified and no tenant column is added; Flyway runs the migration once per tenant schema, and the legacy drop therefore runs per tenant too (E9). — The evidence decides three things. First, nothing to reuse exists: the catalog's only multisourcing table is a per-material code plus priority order (E1) and the only other group table is a substance classification with neither code nor comment (E2), so group storage is net-new - which is also what the inherited Mock contract's db-plan concluded. Second, membership is a solved shape in this repository: rawmaterial_manufacturer expresses at most one per raw material as a junction with UNIQUE (rawmaterial_id), named FKs and an index (E7), mapped by an AuditableEntity with a UUID PK (E13), and rawmaterial_supplier and rawmaterial_internal_status repeat it; a nullable column on rawmaterial would be the one deviation from that pattern on the domain's busiest table, so A1 keeps the junction. Third, the group's identity must outlive its members: the ticket states that a group's identity is the composition its members share and that a group losing its last member becomes inactive and is never deleted (s5, restated on the branch in E21), which rules out A2, and the mock already stores that identity on the group record rather than deriving it (E16). Between stored rows (A1) and a stored fingerprint (A3) the rows win on auditability and exactness: the comparison the rule names is substance-by-substance at a 6-decimal scale (E15), which NUMERIC(10,6) columns express exactly as the composition tables already do (E4, E5), while a hash hides what a group is and loses the substance FK; backend/CLAUDE.md's derivable-column rule is satisfied by naming the single writer (creation) rather than by refusing to store it (E10). A4 is refused on grain and on AC9 (E1, E12). Group Code uniqueness is case-insensitive across active and inactive groups by AC-E2 and by the product owner's own test (reusing an emptied group's code in lower case must still be refused), which a unique index on LOWER(code) guarantees for every writer, not only for the service; functional and partial unique indexes are already house style (E9). No ordering column appears anywhere, because the ticket says this ticket gives a group members, not a ranking.

Implementation steps

  1. 1The Multisourcing group schema design document

    Database designer

    • docs/superpowers/specs/2026-10-14-mp-701-db-schema.md

Implementation

Latest accepted result
Technically validated · Database ·
Verified review commit
e07342b83f73Commit bound and verified by the Operator for the last review. Not necessarily the PR head.
Last attempted commit
e07342b83f73Commit used by the latest agent attempt. Not necessarily the PR head.

Output commits

Local (attempted) commit — diagnostic only
3812a9930173
Published commit (Broker receipt push-1246)
396b8b28f3fa
Verified PR head
396b8b28f3fa

Test matrix

  • 0 Failed
  • 0 Not executable here
  • 0 Not run
  • 6 Passed

From build report workflow-artifact-7e671297637458f2b43a670041dadd30c9aec03e · 2026-10-10 15:43Z · accepted by a handoff

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

GateStatusCommandReason
AC10: one TODO(MP-701) swap marker per data-layer functionPassedgrep -n 'TODO(MP-701)' frontend/src/domains/raw-materials/api/multisourcing-groups.ts; grep -nE '^export (async )?function' frontend/src/domains/raw-materials/api/multisourcing-groups.ts7 exported functions, 7 markers (one each); listAllGroups' marker names page, size and sort=code,asc,ignorecase and updateGroupIdentity's names the code/name/comment body with the 409 DUPLICATE_GROUP_CODE discriminator and the #/code field pointer
AC9 (frontend half): five dead-stack identifiers absentPassedgrep -rniE 'multisourcing-card|MultisourcingCard|useMultisourcing|RawMaterialMultiSourcing|rawmaterial_multisourcing' frontend/src/ CLAUDE.md frontend/CLAUDE.md backend/CLAUDE.md frontend/src/domains/raw-materials/CLAUDE.mdzero hits in frontend source and zero in the CLAUDE.md files; the backend half of the sweep is contract decision D7, deferred to the backend stage
contract bounds: frontend-only, no new file, no shared-component changePassedgit ls-files --others --exclude-standard; git status --porcelain | awk '{print $2}' | grep -E '^(backend/|etl/|infra/|k8s/)|auth\.ts|proxy\.ts|next\.config\.ts|shared/ui/scroll-area|alternative'zero untracked files (no file created), zero paths under backend/, etl/, infra/, k8s/, zero shared/ changes (shared/ui/scroll-area untouched for its 16 base callers), nothing named alternative_supplier
frontend lint (AC11)Passedcd /work/frontend && npm run lintexit 0; 13 problems, 0 errors, 13 warnings, all pre-existing react-hooks/incompatible-library on TanStack useReactTable call sites in files this change does not touch; zero mentions of multisourcing in the lint output
frontend production build (AC11)Passedcd /work/frontend && npm run buildexit 0; compiled successfully; /raw-materials/[id]/multisourcing present in the route manifest
mock-first: no apiClient import for group data (AC10)Passedgit diff --staged --name-only 5e62a467fa51c068e6386eac56e77943bcfa5a5c | grep -E '\.tsx?$' | xargs -r grep -nE "from ['\"]@/shared/lib/api-client"no matches in any changed file; the string shared/lib/api-client also appears nowhere in the feature's api, hooks, lib, types or components, including inside the TODO(MP-701) markers

Review findings and fixer

This gate concerns the database design: no automated review is recorded for it, and the human DB review is the check. Accepting the design establishes no implementation or testing outcome.

Agent work requests

No agent work request for this ticket.

Accepted handoffs and binding

WhenStepResultRecord
2026-10-14 06:36Zdesign (db)CLEANworkflow-handoff-09c8980a9693f80c88f6953d90942988
2026-10-14 05:33Zplan (db)CLEANworkflow-handoff-cc1fb3c5c9d1356020a98ef98eef88bc
2026-10-11 03:51Zreview (mock)CLEANworkflow-handoff-6618083e441b230de021bd55903db8bd
2026-10-11 03:21Zfix (mock)CLEANworkflow-handoff-ea99c0abf10cbd1d8cabd430b2a4589f
2026-10-11 03:11Zreview (mock)ISSUESworkflow-handoff-7a21c4091b7796f617939f1d10a90ddb
2026-10-11 02:45Zfix (mock)CLEANworkflow-handoff-05a25245508979c570c390079017d6d9
2026-10-11 02:34Zreview (mock)ISSUESworkflow-handoff-c1f07b83583251465fd14533600d8513
2026-10-11 02:06Zfix (mock)CLEANworkflow-handoff-cb58c47684c7bb50fec08b6ad75aebbc
2026-10-11 01:48Zreview (mock)ISSUESworkflow-handoff-bb8c5e8308c1a3ad3613e992c9cfc43b
2026-10-11 01:13Zfix (mock)ISSUESworkflow-handoff-4aeeda61bb1ef4c99a041b721abeb3d1
2026-10-11 00:50Zreview (mock)ISSUESworkflow-handoff-ff2c90c3ea495a3f6401477dcd1c5f85
2026-10-11 00:22Zfix (mock)CLEANworkflow-handoff-21b83c290777dc04583e9a145ac6f594
2026-10-10 23:51Zreview (mock)ISSUESworkflow-handoff-5c6d3a2bec1831d57631643401da068b
2026-10-10 23:18Zfix (mock)CLEANworkflow-handoff-d7a52a124cff8511a639c96b02d82ef4
2026-10-10 23:03Zreview (mock)ISSUESworkflow-handoff-055b124cfd29d4514f5a17608c1750e5
2026-10-10 22:25Zfix (mock)CLEANworkflow-handoff-bd3ac3ac52392e5a330dff21a34cb930
2026-10-10 22:14Zreview (mock)ISSUESworkflow-handoff-0c9a6fb78d134465a74bb979705d30ea
2026-10-10 19:29Zreview (mock)ISSUESworkflow-handoff-d4531444580669a535774ccbfa07ecc0
2026-10-10 15:43Zbuild (mock)CLEANworkflow-handoff-41597b3678e604f9f4732870609d51b1
2026-10-10 14:55Zplan (mock)CLEANworkflow-handoff-57d63ffa69c6e38b1641524de03216eb
Current binding
workflow-handoff-09c8980a9693f80c88f6953d90942988 · e374194c6081
Branch / last attempted commit
claude/mock-MP-701-multisourcing-groups @ e07342b83f73

Historical attempts

  • Earlier stage Fix: Complete · 2026-10-11 03:21Z
  • Earlier stage Review: Complete · clean · 2026-10-11 03:51Z
  • History Handoff: Technically validated · Database · 2026-10-14 06:36Z
  • History Handoff: Technically validated · Plan · 2026-10-14 05:33Z
  • History Handoff: Handoff not accepted · Plan · 2026-10-13 20:17Z
  • History Stop record: Technical failure, no work lost · Plan · 2026-10-13 20:17Z
  • History Stop record: Technical failure, no work lost · Plan · 2026-10-13 19:48Z
  • History Handoff: Technically validated · Review · 2026-10-11 03:51Z
  • History Handoff: Technically validated · Review · 2026-10-11 03:21Z
  • History Handoff: Corrections requested · Review · 2026-10-11 03:11Z
  • History Handoff: Technically validated · Review · 2026-10-11 02:45Z
  • History Handoff: Corrections requested · Review · 2026-10-11 02:34Z
  • History Handoff: Technically validated · Review · 2026-10-11 02:06Z
  • History Handoff: Corrections requested · Review · 2026-10-11 01:48Z
  • History Handoff: Corrections requested · Review · 2026-10-11 01:13Z
  • History Handoff: Corrections requested · Review · 2026-10-11 00:50Z
  • History Handoff: Technically validated · Review · 2026-10-11 00:22Z
  • History Handoff: Corrections requested · Review · 2026-10-10 23:51Z
  • History Handoff: Technically validated · Review · 2026-10-10 23:18Z
  • History Handoff: Corrections requested · Review · 2026-10-10 23:03Z
  • History Handoff: Technically validated · Review · 2026-10-10 22:25Z
  • History Handoff: Corrections requested · Review · 2026-10-10 22:14Z

Raw records and logs

Raw

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