Skip to content
Agent Delivery Studio

MP-723 · Back to the workflow

Plan · Formulation - Formulas - Operating procedures - Ticket 1 : free-text procedures (general, per phase, per line) and phase rename (frontend + db + backend)

Objective

A1: extend the three tables that already own the rows the texts describe - one nullable TEXT column each on formula (AC1), formula_phase (AC2) and formula_phase_line (AC3). No new table, no new index, no reference-data seed, no change to an existing column. Four criteria need no schema at all: the rename and its refusal reuse formula_phase.label and its per-formula unique constraint (AC6, AC7), the lock reuses the existing composition guard (AC9), and History reuses the append-only audit event with the seeded ATTRIBUTE_MODIFIED type (AC11). AC12 needs no schema change but does need the two enumerated duplication copy statements extended, and the general procedure copied by the formula creation path - the document says both. One Flyway file planned at the next free version V035: three ADD COLUMN statements, nullable with no default, metadata-only on populated tables. Rejected: a polymorphic procedures table (A2) and 1:1 child tables (A3). This stage delivers the design document only; the migration, entity fields and endpoints belong to the Backend stage.

Current state

Ready for review

Ready for your review in Jira (DB Review) since 1 hour.

Next action

A person: Ready for review — non-blocking recommendations are available.

Accepted route

Database stage · approach A1 · next: Database designer
accepted

Scope and acceptance criteria

What changes for the product and how we know it is done.

User interface

  • None at this stage: no frontend file changes, and the approved mock on the branch stays exactly as the human DB Review finds it
  • The mock's procedures shape is the data contract this schema serves; every one of its fields maps to one column, so no shape change is implied
  • At the Backend stage's flip, only the mock api bodies and the mock store are replaced - the hook, the components and the types keep their contract
  • One behaviour becomes real with the schema: the texts survive a reload, which the mock cannot do; nothing else the user sees changes

Database

  • Three new nullable TEXT columns: formula.operating_procedure (AC1), formula_phase.operating_procedure (AC2), formula_phase_line.instruction (AC3)
  • One Flyway file planned at the next free version V035 with three ADD COLUMN statements - authored by the Backend Builder after DB Review, not at this stage
  • No new table, no new index, no new foreign key, no new enum and no reference-data seed; no existing column, constraint or index is changed
  • Four criteria need no schema change and the document says so explicitly: the phase rename and its refusal rule (AC6, AC7), the lock (AC9) and the History entries (AC11)
  • One entity field per table to land with the migration at the Backend stage, since ddl-auto is validate
  • The only artefact of this stage is the design document on the ticket branch

API and backend

  • Expected at the Backend stage: the Flyway file V035 with the three ADD COLUMN statements, plus one entity field each on Formula, FormulaPhase and FormulaPhaseLine (ddl-auto is validate, so they land together)
  • Five endpoints the mock already fixes: read the formula's procedures, write the general procedure, write a phase's procedure, write a line's instruction, patch a phase label
  • The phase rename must be a phase-level write that keeps the phase row, its sort_order and its procedure - not a loop of per-line relabels, which would move and delete the phase
  • copyPhases and copyLines must list the two new composition columns or AC12 fails silently; the general procedure is copied by the formula creation path, since copyComposition never touches the formula row
  • AC11 reuses formula_audit_event with the seeded ATTRIBUTE_MODIFIED type and the existing recordAttributeModified mechanism - no new event type row
  • Every procedure write reuses the existing composition lock refusal (AC9), and AC7's refusal reuses the existing per-formula label uniqueness
  • Backend verification gates (spotlessCheck, test) apply at that stage; nothing under backend/ changes at this stage

Acceptance criteria

  • The document covers the three columns with types and nullability, the mappings to the mock's fields and to the entity fields, the criteria needing no schema change, the AC12 copy-statement impact, the V035 version and the compatibility reasoning; four DB critics clean; ticket at DB Review.
  • id: RC1 · check: The design places the general procedure on the formula row, the phase procedure on the formula_phase row and the line instruction on the formula_phase_line row, as one nullable column each, and introduces no new table, junction or child table for them.
  • id: RC2 · check: All three columns are TEXT with no length limit anywhere in the document, and the document states the reason (no cap per the criterion, matching the formula's notes field).
  • id: RC3 · check: The document states the empty-value convention once and unambiguously: NULL means not set, the API serves an empty string, a blank write is stored as NULL, and no CHECK or default is added.
  • id: RC4 · check: Every field of the approved mock's procedures shape maps to exactly one column, with nothing unstored and nothing invented: the general text, the per-phase text keyed by phase id and the per-line text keyed by line id.
  • id: RC5 · check: The document shows the phase rename needs no schema change: the existing label column is the stored name, and the existing per-formula unique constraint on (formula_id, label) is the already-used refusal rule on trimmed labels. No new uniqueness rule, case-insensitive index or label-history table is added.
  • id: RC6 · check: The document states that a phase procedure disappears with its phase row - by the existing cascade and the service's delete of an emptied phase - and that no orphan copy or soft delete is kept, which is what the user is warned about.
  • id: RC7 · check: The migration is named at the next free version V035, with three ADD COLUMN statements, each nullable with no default, no backfill and no new NOT NULL or UNIQUE, and the document gives the populated-table reasoning. No file under db/migration is created at this stage.
  • id: RC8 · check: The document justifies no new index on the three columns by naming the composition read that already serves them, and keeps any speculative index in a consider-later section.
  • id: RC9 · check: The document tells the Backend stage that the two enumerated duplication copy statements must list the new phase and line columns, and that the general procedure is copied by the formula creation path because the composition copy never touches the formula row.
  • id: RC10 · check: The document maps the History entries onto the existing append-only audit event table with the already-seeded attribute-modified type, and seeds no new event-type row, so the migration carries no data step.
  • id: RC11 · check: The document records the column-to-entity-field mapping for all three tables and states that field and column must land together because ddl-auto is validate.
  • id: RC12 · check: The document states that procedure writes are refused on a locked composition through the existing composition lock guard, with no new flag or column for the lock.
  • id: RC13 · check: No frontend file is changed by this stage and the document implies no change to the mock's shapes: the approved mock stays the contract and only the api bodies are replaced at the flip.
  • id: RC14 · check: The only file added by this stage is the design document on the ticket branch; nothing under backend/, db/migration or frontend/ is touched.

Assumptions and constraints

  • Design document only: no Flyway file, no entity, no backend or frontend code is written at this stage, and nothing under backend/ or db/migration changes
  • The approved mock on the branch is the data contract: every field of its procedures shape must map to exactly one column, and no field may be added or renamed here
  • No new table, no new reference-data table and no Java enum: the three texts extend existing rows
  • No length limit on any of the three columns (AC13); TEXT, never VARCHAR(n)
  • No new uniqueness rule for the phase label: uq_formula_phase_formula_label is AC7's rule as it stands
  • No new formula_audit_event_type row: AC11 reuses the seeded ATTRIBUTE_MODIFIED type, so the migration stays pure DDL
  • Next free Flyway version is V035; the Backend Builder authors that file after a human approves the design
  • Every column is added nullable with no default and no backfill, so the migration is metadata-only on populated tables

Risks, dependencies and expected result

  • R1
    AC12 can fail silently: the duplication copy statements enumerate their columns, so a new column is simply not copied and a duplicate loses its procedures with no error.

    Mitigation: The design document names both statements and the formula creation path as required changes, a review check (RC9) targets it, and the Backend stage covers a duplicate with a test.

  • R2
    ddl-auto is validate, so a migration landing without its entity field - or the reverse - fails the application at startup rather than at review.

    Mitigation: The document carries the column-to-field mapping for all three entities and states that field and column land together in the Backend stage's change (RC11).

  • R3
    A phase procedure is lost whenever its phase loses its last line, because the service deletes the emptied phase row and the cascade takes the text with it.

    Mitigation: This is the product answer the ticket chose: AC8's confirmation warns the user first. The document states the behaviour explicitly so it is deliberate, not discovered later.

  • R4
    Preview databases are restored with production-shaped data, so a careless NOT NULL or a backfill on these three columns could abort the migration.

    Mitigation: Every column is nullable with no default and no backfill, which is metadata-only in PostgreSQL - the reasoning V029 already recorded for this repository (RC7).

  • R5
    MP-725's structured steps may later want a table of their own, and a reader could read these three columns as the place to put them.

    Mitigation: The document scopes the columns as the free-text layer of this ticket and leaves structured steps to MP-725, which is explicitly excluded and blocked by this ticket.

  • R6
    An ADVISORY review observation is still open on the branch: inside the procedure and instruction areas, the shared editor footer's Cancel and Save stay right-aligned in a full-width grid row and can sit off-screen.

    Mitigation: Not DB work and not a regression of this delta: the reviewer's verdict was CLEAN, it is routed as a recommendation and the ledger lists no open requirement. Carried for the next frontend touch.

Expected result

Database designer takes over the Database stage from this plan.

Milestones

The ordered steps and how each one is known to be done. File-level detail is under Technical details.

  1. 1DB design document for the three operating-procedure columns

    Database designer

    Done when: The document covers the three columns with types and nullability, the mappings to the mock's fields and to the entity fields, the criteria needing no schema change, the AC12 copy-statement impact, the V035 version and the compatibility reasoning; four DB critics clean; ticket at DB Review.

Technical details

For developers and platform engineers: expand a section to see the exact technical content.

Decision, design and alternatives3
Selected
A1
Architecture
Target: three nullable TEXT columns on three existing tables, one Flyway migration at the next free version, three entity fields, and no other schema object. This stage produces the design document only; the migration and the entities are authored at the Backend stage, which is the lifecycle of this repository (Planner, DB Designer, human DB Review, then the Backend Builder writes the canonical migration). 1. formula.operating_procedure - TEXT, nullable, no default. AC1's general procedure. It sits beside formula.notes rather than inside it: V029 deliberately split the formula's free text by lifetime and purpose, notes being the formulator's lab notebook (E4, E5). TEXT and not VARCHAR(n) because AC13 forbids a length limit and the repository reserves VARCHAR(n) for limits that are business rules. 2. formula_phase.operating_procedure - TEXT, nullable, no default. AC2's phase procedure, on the phase row. A rename writes only label, so the procedure survives it (AC6); the phase's cascade and the service's delete of an emptied phase destroy the procedure with the phase, which is precisely the loss AC8 warns about (E1). 3. formula_phase_line.instruction - TEXT, nullable, no default. AC3's line instruction, on the line row, so a reorder, a phase-cell change, a drag into another phase and a phase rename all leave it on its line with no extra mechanism (E2). Empty and absent: NULL means 'not set'. The mock's contract serves '' for a text that is not set (E19, E21), so the mapping is NULL on read becomes '', and a blank or whitespace-only write becomes NULL. The design document states this in one line so the Backend stage does not end up with two spellings of empty. No CHECK constraint is added: '' and NULL would mean the same thing and the service normalises. Keys: nothing keys on the phase label. The mock keys the texts by phase id and line id (E20, E22, E23) and these columns sit on those very rows, so the key is each table's primary key and the Mock contract's phaseId/lineId decision is honoured by construction. Phase rename (AC6, AC7) - no schema change. formula_phase.label is already the stored name, at most 255 characters and trimmed by the service (E1, E17), and uq_formula_phase_formula_label on (formula_id, label) is already AC7's 'a name already used by another phase of the same formula', with the comparison the server applies today. A rename is a label update on one row that keeps id and sort_order. Renaming the 'No phase' group is the same update on the row whose label is '', and a collision is refused by the same constraint. The design document must say explicitly that no new uniqueness rule, no case-insensitive index and no label history table is added. History (AC11) - no schema change. formula_audit_event is append-only, cascades with its formula and already carries attribute with previous_value and new_value as TEXT (E6); ATTRIBUTE_MODIFIED is already seeded (E7); recordAttributeModified already writes a field key with resolved display strings (E8). AC11 asks for exactly 'how attribute changes are recorded today', so the Backend stage raises ATTRIBUTE_MODIFIED events with attribute keys for the general procedure, the procedure of a named phase, the instruction of the line holding a given raw material, and a phase rename. No new formula_audit_event_type row is seeded, so V035 carries no data step. Duplication (AC12) - no schema change, but two existing statements must learn the new columns. copyPhases and copyLines are native INSERT ... SELECT statements with explicit column lists (E9, E10): a new column is silently not copied until it is listed there, which is the one way this design can fail quietly. The general procedure is on the formula row, which copyComposition never touches (E11), so it is copied by the formula creation path instead. Both facts belong in the design document as impact on the Backend stage, with the note that the lot and the batch stay deliberately uncopied. Lock (AC9) - no schema change. Every procedure write goes through the existing guard that refuses a composition write when the formula's status row locks the composition (E12). Migration: one Flyway file, V035, three ADD COLUMN statements, one per table. V034 is the highest version at the base, the branch adds no migration and main has not moved since the index, so there is no version collision (E18). Each column is nullable with no default, which is a metadata-only change in PostgreSQL and cannot abort on populated tables - the compatibility reasoning V029 already wrote down for this repository's preview databases restored from production-shaped data (E5). No backfill, no narrowing, no new UNIQUE and no NOT NULL, so no existing row can violate anything and the migration is reversible by dropping three columns. Entities: ddl-auto is validate, so the entity field and the column must land together at the Backend stage - one String field on Formula, on FormulaPhase and on FormulaPhaseLine, each mapped to its column with no length attribute (E15, E16, E17). The design document gives the column-to-field mapping so that pairing is unambiguous. Indexes: none. A procedure text is never filtered, joined or sorted on; it is read only as part of a composition read, which is already served by idx_formula_phase_line_formula_id and by the leading column of uq_formula_phase_formula_label (E1, E3). A speculative index belongs in the document's 'consider later' section, not in the design. Readers: nothing existing changes, so every current entity, DTO, specification and frontend type is unaffected - an added nullable column is invisible until the Backend stage maps it. The frontend needs no change from this stage: the mock's five signatures stay the contract and the flip replaces their bodies at the Backend stage. Deliverable: one design document on the ticket branch, docs/superpowers/specs/2026-10-17-MP-723-operating-procedures-db-schema.md, covering the three columns with types and nullability, the illustrative DDL, the column-to-field and column-to-mock-field mappings, the four criteria that need no schema change (AC6, AC7, AC9, AC11), the AC12 copy-statement impact, the compatibility reasoning, and the open questions it leaves to the Backend stage.
Rationale
The code decides this one. Each of the three texts describes exactly one existing row, and that row's lifecycle is already the behaviour the acceptance criteria ask for: a phase is a row with a stored label that a rename updates in place (E1, E17), so AC6's 'the phase keeps its position and its procedure' is free; the phase row cascades to its lines and is deleted once its last line leaves (E1, E2), so AC8's 'its procedure will be deleted with it' is the row disappearing rather than a cleanup rule; and a line row keeps its identity through reorders, phase changes and drags (E2, E22), so AC3's 'the instruction stays on its line' needs no work at all. A2 and A3 would have to re-create each of those three guarantees with foreign keys, an exclusive-arc CHECK and extra copy statements, and would add one or two queries to every composition read, which is already indexed to fetch a whole composition in one pass (E3). The DB guidance's own order - reuse, then extend a column, then create a table - points the same way, and no second home for the concept is created. TEXT with no length is settled by AC13 and by the repository's rule that VARCHAR(n) is for a real business limit, with formula.notes as the live precedent; the general procedure is a separate column from notes because V029 split the formula's free text by purpose on purpose, and folding manufacturing instructions back into the lab notebook would undo that (E4, E5). Two criteria need no schema at all and are recorded so the designer does not invent any: AC7's 'already used' is exactly uq_formula_phase_formula_label on trimmed labels (E1), and AC11 is the existing append-only formula_audit_event with the already-seeded ATTRIBUTE_MODIFIED type and the recordAttributeModified mechanism the ticket names (E6, E7, E8) - which keeps V035 pure DDL with no reference-data seed. The one place where 'just add a column' is not enough is AC12: copyPhases and copyLines enumerate their column lists and copyComposition never touches the formula row (E9, E10, E11), so the design document must say both that the two statements gain the new columns and that the general procedure is copied by the formula creation path. A2's only real advantage is extensibility towards MP-725, and MP-725 is explicitly out of this ticket and blocked by it, so paying its cost now is speculative.

Design by area

User interface

Decided at the Backend stage (the mock-to-real flip). What this stage fixes for it: the three texts are stored on the formula, phase and line rows themselves, so the client's existing keying by phase id and line id maps one-to-one onto primary keys, and a procedure keeps following its phase through a rename and its line through a reorder exactly as the mock already behaves. A text that is not set is NULL in the database and '' in the API, which is the value the mock already renders as 'Not set', so no component changes. Texts stay unbounded, so no character cap appears in the editors (AC13).

Database

Three columns, one per table that already owns the row the text describes. Full detail, with the reasoning, is in the architecture section's decision.architecture; this is the layer's specification.

Columns
1. formula.operating_procedure - TEXT, NULL allowed, no default. The formula's general procedure (AC1). A separate column from formula.notes, which V029 established as the lab notebook (E4, E5).
2. formula_phase.operating_procedure - TEXT, NULL allowed, no default. One named phase's procedure (AC2). On the phase row, so a rename keeps it (AC6) and the phase's disappearance destroys it (AC8).
3. formula_phase_line.instruction - TEXT, NULL allowed, no default. One composition line's instruction (AC3). On the line row, so it follows the line through reorders, phase changes and drags.

Types and nullability. TEXT because AC13 forbids a length limit and the repository reserves VARCHAR(n) for limits that are real business rules, with formula.notes as the precedent (E4, E5). NULL means 'not set'; the mock serves '' for the same state (E19, E21), so the mapper reads NULL as '' and writes a blank as NULL. No CHECK and no default: '' and NULL would carry one meaning and the service normalises.

Constraints. None added. The three columns are free text with no integrity the database can assert. The existing constraints already do the work the criteria need: fk_formula_phase_formula ON DELETE CASCADE and the service's delete of an emptied phase take the phase procedure with its phase (AC8), the composite FK from formula_phase_line cascades the line's instruction, and uq_formula_phase_formula_label on (formula_id, label) is AC7's 'already used' rule on trimmed labels (E1, E2).

Indexes. None. A procedure text is never filtered, joined or sorted on, and it is read only as part of a composition read that idx_formula_phase_line_formula_id and the leading column of uq_formula_phase_formula_label already serve (E1, E3). Any index on these columns belongs in the document's 'consider later' section.

Migration strategy. One file, V035, next free version: V034 is the highest at the base, the branch adds no migration and main has not moved since the index, so no collision is pending (E18). Three ADD COLUMN statements, each nullable with no default - metadata-only in PostgreSQL, so it cannot abort on populated tables, which matters because preview databases are restored with production-shaped data (E5). No backfill, no narrowing, no new UNIQUE, no NOT NULL; rolling back is dropping three columns. The file itself is written by the Backend Builder after a human approves this design, so no db/migration path is touched by this stage.

Entity mapping (ddl-auto validate, landing with the migration at the Backend stage): one String field on Formula, on FormulaPhase and on FormulaPhaseLine, each mapped to its column with no length attribute (E15, E16, E17). The document carries the column-to-field table so the pairing cannot drift.

Compatibility. Nothing existing changes, so no current reader breaks: an added nullable column is invisible to every entity, DTO, specification and frontend type until the Backend stage maps it. Existing rows read NULL, which the API serves as '' - the same 'Not set' the mock already renders.

What the document must also record, because the schema alone does not deliver it: the two enumerated copy statements must list the new phase and line columns or AC12 fails silently, and the general procedure is copied by the formula creation path since copyComposition never touches the formula row (E9, E10, E11); AC11 maps onto the existing append-only audit event with the seeded ATTRIBUTE_MODIFIED type and needs no new event type (E6, E7, E8); AC9's refusal is the existing composition lock guard (E12).

API and backend

Decided at the Backend stage. What this stage fixes for it: the storage shape (three nullable TEXT columns - formula.operating_procedure, formula_phase.operating_procedure, formula_phase_line.instruction), the NULL-means-not-set convention that the API serves as '', the column-to-entity mapping that must land with the migration, and the four criteria that need no schema change (AC6 and AC7 on the existing label and its per-formula unique constraint, AC9 on the existing lock guard, AC11 on the existing attribute-modified audit event). It also fixes two things the Backend stage must not discover late: the duplication copy statements enumerate their columns, so the new ones must be added there for AC12, and the general procedure is copied by the formula creation path rather than by copyComposition. The endpoint contract itself is already pinned by the approved mock's signatures on the branch.

Alternatives considered

  • A1 — Extend the three tables that already own the rows the texts describe: one nullable TEXT column each on formula (general procedure), formula_phase (phase procedure) and formula_phase_line (line instruction). No new table, no new index, no new reference data, no change to an existing column. The phase rename reuses formula_phase.label and its existing per-formula unique constraint, History reuses formula_audit_event with the seeded ATTRIBUTE_MODIFIED type, and the lock reuses the existing composition guard. One Flyway file at the next free version V035 with three ADD COLUMN statements, authored …

    Pros: Smallest correct change: three columns instead of a new table, which is the repository's stated preference for a value that belongs to an existing row; AC6 comes free: a rename writes only label, so the phase keeps its id, its position and its procedure; AC8 comes free: the phase row's cascade and the emptied-phase delete already destroy the procedure with its phase; AC3 comes free: an instruction on the line row follows that line through reorders, phase changes, drags and renames; No extra read cost: the composition read already fetches the phases and lines of a formula, so the texts arrive with it and need no index; AC12 is two extra column references in statements that already exist. Cons: Three ALTERs across three tables rather than one new object; A long text is loaded with every composition read, since a column cannot be fetched lazily here; MP-725's structured steps will still need their own table later, so the free-text layer stays separate from it.

  • A2 — Create one polymorphic table, formula_operating_procedure, holding every procedure as a row: id, formula_id, a nullable phase_id, a nullable line_id, a scope discriminator and the text. The three composition tables stay untouched and the procedures are read with one or two extra queries per composition.

    Pros: One object owns every procedure, so the concept has a single home; Room to grow into MP-725's structured steps without touching the composition tables; A text can be attached or detached without writing to a phase or line row. Cons: Needs a three-way exclusive-arc CHECK plus three foreign keys, each with its own ON DELETE, to re-create the lifecycle the parent rows already give for free; Adds one or two queries (or joins) to every composition read, and a new index to serve them; Duplication becomes a third copy statement that must remap both the new phase ids and the new line ids, which the label join cannot do for lines; The scope discriminator is a new value set the code alone fixes and no admin manages, so it is an enum the repository would rather not add; Stores one data column per row in a table whose key is harder to get right than the column it replaces.

  • A3 — Create two 1:1 child tables, one for the phase procedure and one for the line instruction, each with a unique foreign key to its parent, and put the general procedure on the formula row.

    Pros: Keeps the phase and line rows narrow; A text is read only when it is asked for. Cons: Two new tables for two nullable texts with no lifecycle of their own - the 1:1 split the DB guidance calls duplication; Three reads instead of two for a composition, each needing its own index; AC12 needs two further copy statements, each remapping parent ids; Every reader of a phase or line that wants its text now needs a join.

API0

API contract

No API contract in this plan.

Backend design

API and backend

Decided at the Backend stage. What this stage fixes for it: the storage shape (three nullable TEXT columns - formula.operating_procedure, formula_phase.operating_procedure, formula_phase_line.instruction), the NULL-means-not-set convention that the API serves as '', the column-to-entity mapping that must land with the migration, and the four criteria that need no schema change (AC6 and AC7 on the existing label and its per-formula unique constraint, AC9 on the existing lock guard, AC11 on the existing attribute-modified audit event). It also fixes two things the Backend stage must not discover late: the duplication copy statements enumerate their columns, so the new ones must be added there for AC12, and the general procedure is copied by the formula creation path rather than by copyComposition. The endpoint contract itself is already pinned by the approved mock's signatures on the branch.

Backend changes

No file changes listed.

Data and database2

Data contract

Database

Each field of the mock's procedures shape maps to exactly one column. general -> formula.operating_procedure. byPhaseId[phaseId] -> formula_phase.operating_procedure of the row with that id. byLineId[lineId] -> formula_phase_line.instruction of the row with that id. formulaId is the formula row's id and is not stored again. The phase label a rename writes stays formula_phase.label, unchanged in type and constraint. Texts are unbounded; NULL in the database is '' in the API, and a blank write is stored as NULL. Ids are UUIDs, which the mock already treats as UUID strings.

Database design

Database

Three columns, one per table that already owns the row the text describes. Full detail, with the reasoning, is in the architecture section's decision.architecture; this is the layer's specification.

Columns
1. formula.operating_procedure - TEXT, NULL allowed, no default. The formula's general procedure (AC1). A separate column from formula.notes, which V029 established as the lab notebook (E4, E5).
2. formula_phase.operating_procedure - TEXT, NULL allowed, no default. One named phase's procedure (AC2). On the phase row, so a rename keeps it (AC6) and the phase's disappearance destroys it (AC8).
3. formula_phase_line.instruction - TEXT, NULL allowed, no default. One composition line's instruction (AC3). On the line row, so it follows the line through reorders, phase changes and drags.

Types and nullability. TEXT because AC13 forbids a length limit and the repository reserves VARCHAR(n) for limits that are real business rules, with formula.notes as the precedent (E4, E5). NULL means 'not set'; the mock serves '' for the same state (E19, E21), so the mapper reads NULL as '' and writes a blank as NULL. No CHECK and no default: '' and NULL would carry one meaning and the service normalises.

Constraints. None added. The three columns are free text with no integrity the database can assert. The existing constraints already do the work the criteria need: fk_formula_phase_formula ON DELETE CASCADE and the service's delete of an emptied phase take the phase procedure with its phase (AC8), the composite FK from formula_phase_line cascades the line's instruction, and uq_formula_phase_formula_label on (formula_id, label) is AC7's 'already used' rule on trimmed labels (E1, E2).

Indexes. None. A procedure text is never filtered, joined or sorted on, and it is read only as part of a composition read that idx_formula_phase_line_formula_id and the leading column of uq_formula_phase_formula_label already serve (E1, E3). Any index on these columns belongs in the document's 'consider later' section.

Migration strategy. One file, V035, next free version: V034 is the highest at the base, the branch adds no migration and main has not moved since the index, so no collision is pending (E18). Three ADD COLUMN statements, each nullable with no default - metadata-only in PostgreSQL, so it cannot abort on populated tables, which matters because preview databases are restored with production-shaped data (E5). No backfill, no narrowing, no new UNIQUE, no NOT NULL; rolling back is dropping three columns. The file itself is written by the Backend Builder after a human approves this design, so no db/migration path is touched by this stage.

Entity mapping (ddl-auto validate, landing with the migration at the Backend stage): one String field on Formula, on FormulaPhase and on FormulaPhaseLine, each mapped to its column with no length attribute (E15, E16, E17). The document carries the column-to-field table so the pairing cannot drift.

Compatibility. Nothing existing changes, so no current reader breaks: an added nullable column is invisible to every entity, DTO, specification and frontend type until the Backend stage maps it. Existing rows read NULL, which the API serves as '' - the same 'Not set' the mock already renders.

What the document must also record, because the schema alone does not deliver it: the two enumerated copy statements must list the new phase and line columns or AC12 fails silently, and the general procedure is copied by the formula creation path since copyComposition never touches the formula row (E9, E10, E11); AC11 maps onto the existing append-only audit event with the seeded ATTRIBUTE_MODIFIED type and needs no new event type (E6, E7, E8); AC9's refusal is the existing composition lock guard (E12).

Database changes

FileChangeWhy
docs/superpowers/specs/2026-10-17-MP-723-operating-procedures-db-schema.mdcreateThe DB design document - the only artefact of this stage: the three columns with types, nullability and illustrative DDL, the column-to-entity and column-to-mock-field mappings, the criteria that need no schema change (AC6, AC7, AC9, AC11), the AC12 copy-statement impact, the V035 version choice, the compatibility reasoning and the open questions left to the Backend stage.
Files and components1
AreaFileChangeWhy
Databasedocs/superpowers/specs/2026-10-17-MP-723-operating-procedures-db-schema.mdcreateThe DB design document - the only artefact of this stage: the three columns with types, nullability and illustrative DDL, the column-to-entity and column-to-mock-field mappings, the criteria that need no schema change (AC6, AC7, AC9, AC11), the AC12 copy-statement impact, the V035 version choice, the compatibility reasoning and the open questions left to the Backend stage.

Files named by the steps

  • docs/superpowers/specs/2026-10-17-MP-723-operating-procedures-db-schema.md

Components the review expects

  • kind: create · path: docs/superpowers/specs/2026-10-17-MP-723-operating-procedures-db-schema.md
Tests and review checks14

Review checks

  • id: RC1 · check: The design places the general procedure on the formula row, the phase procedure on the formula_phase row and the line instruction on the formula_phase_line row, as one nullable column each, and introduces no new table, junction or child table for them.
  • id: RC2 · check: All three columns are TEXT with no length limit anywhere in the document, and the document states the reason (no cap per the criterion, matching the formula's notes field).
  • id: RC3 · check: The document states the empty-value convention once and unambiguously: NULL means not set, the API serves an empty string, a blank write is stored as NULL, and no CHECK or default is added.
  • id: RC4 · check: Every field of the approved mock's procedures shape maps to exactly one column, with nothing unstored and nothing invented: the general text, the per-phase text keyed by phase id and the per-line text keyed by line id.
  • id: RC5 · check: The document shows the phase rename needs no schema change: the existing label column is the stored name, and the existing per-formula unique constraint on (formula_id, label) is the already-used refusal rule on trimmed labels. No new uniqueness rule, case-insensitive index or label-history table is added.
  • id: RC6 · check: The document states that a phase procedure disappears with its phase row - by the existing cascade and the service's delete of an emptied phase - and that no orphan copy or soft delete is kept, which is what the user is warned about.
  • id: RC7 · check: The migration is named at the next free version V035, with three ADD COLUMN statements, each nullable with no default, no backfill and no new NOT NULL or UNIQUE, and the document gives the populated-table reasoning. No file under db/migration is created at this stage.
  • id: RC8 · check: The document justifies no new index on the three columns by naming the composition read that already serves them, and keeps any speculative index in a consider-later section.
  • id: RC9 · check: The document tells the Backend stage that the two enumerated duplication copy statements must list the new phase and line columns, and that the general procedure is copied by the formula creation path because the composition copy never touches the formula row.
  • id: RC10 · check: The document maps the History entries onto the existing append-only audit event table with the already-seeded attribute-modified type, and seeds no new event-type row, so the migration carries no data step.
  • id: RC11 · check: The document records the column-to-entity-field mapping for all three tables and states that field and column must land together because ddl-auto is validate.
  • id: RC12 · check: The document states that procedure writes are refused on a locked composition through the existing composition lock guard, with no new flag or column for the lock.
  • id: RC13 · check: No frontend file is changed by this stage and the document implies no change to the mock's shapes: the approved mock stays the contract and only the api bodies are replaced at the flip.
  • id: RC14 · check: The only file added by this stage is the design document on the ticket branch; nothing under backend/, db/migration or frontend/ is touched.

Test files

No test files named.

Reuse to verify

  • area: db · element: formula_operating_procedure (one polymorphic procedures table) · action: create · why: Rejected alternative A2: it must not appear in the design. It would re-create with foreign keys and an exclusive-arc CHECK the lifecycle the parent rows already give, add queries to every composition read and need a third copy statement remapping phase and line ids.
  • area: db · element: formula_phase_procedure / formula_phase_line_instruction (1:1 child tables) · action: create · why: Rejected alternative A3: two new tables for two nullable texts with no lifecycle of their own is the 1:1 split the DB guidance calls duplication.
  • area: db · element: formula.notes · action: reuse · why: Unchanged and not reused as storage: V029 split the formula's free text by purpose, so the general procedure is its own column and must not be folded back into the lab notebook.
  • area: db · element: uq_formula_phase_formula_label on (formula_id, label) · action: reuse · why: It already is the rename refusal rule; a second uniqueness rule or a case-insensitive variant would duplicate it and change today's comparison.
  • area: db · element: formula_audit_event_type seeded codes · action: reuse · why: The attribute-modified code already exists; seeding a new procedure event type would duplicate the vocabulary and add a data step to the migration.
  • area: backend · element: FormulaPhaseRepository.copyPhases and FormulaPhaseLineRepository.copyLines · action: extend · why: The only silent failure in this design: both statements enumerate their columns, so duplication drops the new texts until they are listed there.
  • area: frontend · element: api/formula-procedures.ts mock signatures · action: reuse · why: They stay the API contract the schema serves; the design must not imply a renamed or restructured field.
Handoff and bindings22

Database designer takes over the Database stage from this plan.

RAG revision
6ff9aee4994f
Catalog
ead4cae607ae
Base SHA (RAG indexed)
3830b223cba4
Feature base
3830b223cba4
Branch head
ed0ae3200407
Main
3830b223cba4
Main overlay
none
Branch overlay
ffcdf347abc8
Branch
claude/mock-MP-723-operating-procedures
PR
#151
Freshness
EXACT
RAG mode / profile
required
Stage binding
2182ae3e1ae0
Context manifest
5f1e7e61490f
Policy binding
85afed9311b4
Writes
disabled
TicketSnapshot schema
ticket-snapshot/v2
TicketSnapshot hash
d551b207e9ee
TicketSnapshot size
18059 B
TicketSnapshot fetched
2026-10-17T09:52:57Z
FeatureContract schema
feature-contract/v2.1
FeatureContract hash
95b63d982b03
Evidence and provenance23
Contract
feature-contract/v2.1 · 95b63d982b03406e2d1068f5516b0a398c644e5ab53e4c8464a720e01a07c4b0
Aggregate
95b63d982b03406e2d1068f5516b0a398c644e5ab53e4c8464a720e01a07c4b0
RAG
RAG · EXACT

Cited evidence

IdSourceWhereClaim
E1git-basebackend/applications/cosmoreg-core/src/main/resources/db/migration/V032__Create_Formula_Phase_And_Line_Tables.sql:38-55 @ 3830b22formula_phase stores id, formula_id, label VARCHAR(255) NOT NULL DEFAULT '' and sort_order plus audit columns - no procedure column; its FK to formula is ON DELETE CASCADE and uq_formula_phase_formula_label makes (formula_id, label) unique.
E2git-basebackend/applications/cosmoreg-core/src/main/resources/db/migration/V032__Create_Formula_Phase_And_Line_Tables.sql:83-115 @ 3830b22formula_phase_line stores raw material, percentage, QSP flag, lot, batch and sort_order - no instruction column; its composite FK to formula_phase is ON DELETE CASCADE, so a line's data dies with its phase.
E3git-basebackend/applications/cosmoreg-core/src/main/resources/db/migration/V032__Create_Formula_Phase_And_Line_Tables.sql:117-124 @ 3830b22The composition read path is already indexed: idx_formula_phase_line_formula_id reads a whole composition in one pass, which is the only query a procedure text is ever fetched by.
E4git-basebackend/applications/cosmoreg-core/src/main/resources/db/migration/V028__Create_Formula_Status_And_Formula_Tables.sql:113-143 @ 3830b22The formula table carries notes TEXT as its only free-text field and has no operating-procedure column; TEXT is the established type for unbounded formula free text.
E5git-basebackend/applications/cosmoreg-core/src/main/resources/db/migration/V029__Split_Formula_Notes_And_Version_Reason.sql:4-33 @ 3830b22V029 split the formula's free text by purpose: notes stays TEXT as the lab notebook, VARCHAR(n) is used only when a length limit is a real business rule, and ADD COLUMN with no NOT NULL and no DEFAULT is metadata-only and safe on populated tables.
E6git-basebackend/applications/cosmoreg-core/src/main/resources/db/migration/V028__Create_Formula_Status_And_Formula_Tables.sql:162-198 @ 3830b22formula_audit_event is append-only, cascades with its formula, and already carries attribute VARCHAR(100) with previous_value and new_value as TEXT - the shape an attribute change is recorded in today.
E7git-basebackend/applications/cosmoreg-core/src/main/resources/db/migration/V028__Create_Formula_Status_And_Formula_Tables.sql:92-102 @ 3830b22formula_audit_event_type is reference data already seeded with six codes including ATTRIBUTE_MODIFIED ('Formula attribute modified'), so AC11 needs no new event-type row.
E8git-basebackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaAuditService.java:144-163 @ 3830b22recordAttributeModified writes an ATTRIBUTE_MODIFIED event with the request field key as attribute and already-resolved display strings as previous/new value - the mechanism AC11 says the procedure changes must follow.
E9git-basebackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaPhaseRepository.java:46-57 @ 3830b22copyPhases is a native INSERT ... SELECT with an explicit column list (formula_id, label, sort_order, audit columns), so a newly added phase column is NOT copied on duplication until it is added to this statement.
E10git-basebackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaPhaseLineRepository.java:89-103 @ 3830b22copyLines is a native INSERT ... SELECT with an explicit column list that deliberately nulls lot and batch and attaches each copied line to the target phase of the same label; a new line column must be added here for AC12.
E11git-basebackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaCompositionService.java:276-290 @ 3830b22copyComposition copies only the phases and the lines, in two statements called from FormulaService#create; it never touches the formula row, so a formula-level general procedure is copied by the creation path, not here.
E12git-basebackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaCompositionService.java:292-301 @ 3830b22loadUnlockedFormula refuses every composition write when the formula's status row has locksComposition, raising the same business-rule exception - the refusal AC9 asks the procedure writes to reuse.
E13catalogbackend/applications/cosmoreg-core/src/main/resources/db/migration/V032__Create_Formula_Phase_And_Line_Tables.sql:38-55 @ 3830b22The catalog's db.table record for formula_phase lists its eight columns (created_by, created_date, formula_id, id, label, modified_by, modified_date, sort_order): none stores a procedure text.
E14catalogbackend/applications/cosmoreg-core/src/main/resources/db/migration/V032__Create_Formula_Phase_And_Line_Tables.sql:83-115 @ 3830b22The catalog's db.table record for formula_phase_line lists its thirteen columns: none stores an instruction text. Catalog queries for a db.column named 'procedure' and one named 'instruction' both returned zero records across the whole schema.
E15catalogbackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/domain/formula/FormulaPhase.java:39-69 @ 3830b22The be.entity FormulaPhase maps-table formula_phase with exactly the fields id, formulaId, label and sortOrder, so a new column needs a matching entity field in the same change (ddl-auto is validate).
E16catalogbackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/domain/formula/FormulaPhaseLine.java:46-99 @ 3830b22The be.entity FormulaPhaseLine maps-table formula_phase_line with the fields id, formulaId, phase, rawMaterial, percentage, isQsp, lot, batch and sortOrder - no instruction field.
E17git-basebackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/domain/formula/FormulaPhase.java:47-69 @ 3830b22FormulaPhase is an AuditableEntity whose label is a trimmed free-text column of at most 255 characters, documented as unique per formula and '' for lines entered without a phase - the row a rename updates and a procedure would hang from.
E18git-basebackend/applications/cosmoreg-core/src/main/resources/db/migration/V034__Add_Formula_And_Default_Flags_To_Internal_Status.sql:1-12 @ 3830b22V034 is the highest Flyway version present at the indexed base (the migration directory runs V000 to V034), so the next free version is V035; main has not moved since the index and the branch adds no migration.
E19branch-overlayfrontend/src/domains/formulation/features/formulas/api/formula-procedures.ts:1-40 @ ed0ae32Branch content: the approved mock fixes the data contract - a procedures read returning formulaId, general, byPhaseId and byLineId, three text writes, one phase-label patch; texts are UNBOUNDED, '' means not set, ids are UUID strings, and writes are refused on a locked composition.
E20branch-overlayfrontend/src/domains/formulation/features/formulas/lib/mock-data.ts:1-40 @ ed0ae32Branch content: the mock store holds the general text plus maps keyed by phase id and line id, and states that the procedures are net-new persistence with no table and no endpoint, living only for the browser session.
E21branch-overlayfrontend/src/domains/formulation/features/formulas/description/hooks/use-formula-procedures.ts:1-40 @ ed0ae32Branch content: the FormulaProcedures shape the controller hook consumes is {formulaId, general, byPhaseId, byLineId}, with per-phase and per-line texts defaulting to '' - the exact set of values the schema must hold.
E22branch-overlayfrontend/src/domains/formulation/features/formulas/types/index.ts:382-390 @ ed0ae32Branch content: every composition line carries the phase's id, served all along, because the procedures and the rename key on that identity and not on the label, which a rename changes.
E23branch-overlayfrontend/src/domains/formulation/features/formulas/lib/composition-math.ts:102-112 @ ed0ae32Branch content: a phase group exposes the stored phase's id taken from its first row, since one label is one stored phase and its rows are contiguous; phase-keyed data attaches to that id.

Reuse and anti-duplication

AreaElementActionJustification
dbformula.operating_procedure (new column on the formula table) (db.table:backend/applications/cosmoreg-core/src/main/resources/db/migration/V028__Create_Formula_Status_And_Formula_Tables.sql#formula)createSearched the catalog for a db.column named procedure (0 records) and read the formula table: its only free text is notes TEXT, with version_reason VARCHAR(255) beside it (E4). Reusing notes is not viable - V029 split the formula's free text by purpose, notes being the lab notebook, so folding manufacturing instructions into it would undo that split (E5). Nothing else on the formula row can hold it…
dbformula_phase.operating_procedure (new column on the phase table) (db.table:backend/applications/cosmoreg-core/src/main/resources/db/migration/V032__Create_Formula_Phase_And_Line_Tables.sql#formula_phase)createThe catalog's formula_phase record lists eight columns, none a text field (E13), and the DDL confirms it (E1); catalog queries for a db.column named procedure or instruction returned nothing in the whole schema (E14). No existing table stores anything per phase, so there is nothing to reuse or extend; a new table was weighed as A2 and A3 and rejected because the phase row's own cascade already giv…
dbformula_phase_line.instruction (new column on the line table) (db.table:backend/applications/cosmoreg-core/src/main/resources/db/migration/V032__Create_Formula_Phase_And_Line_Tables.sql#formula_phase_line)createThe catalog's formula_phase_line record lists thirteen columns - raw material, percentage, QSP, lot, batch, order and audit - none of them a free-text instruction (E14), as the DDL confirms (E2). The existing lot VARCHAR(255) is a lot number, not an instruction, and is deliberately not copied on duplication, so it cannot be reused. The column is on the line row so AC3's follow-the-line behaviour i…
dbformula_phase.label and uq_formula_phase_formula_label (db.table:backend/applications/cosmoreg-core/src/main/resources/db/migration/V032__Create_Formula_Phase_And_Line_Tables.sql#formula_phase)reuseThe phase name is already a stored, trimmed column of at most 255 characters, unique per formula (E1, E17). It is AC6's rename target and AC7's already-used rule exactly as it stands, so no schema change, no second uniqueness rule and no label-history table is designed.
dbformula_audit_event and the seeded ATTRIBUTE_MODIFIED event typereuseThe audit table is append-only, cascades with its formula and already carries attribute, previous_value and new_value (E6); the ATTRIBUTE_MODIFIED code is already seeded reference data (E7). AC11 asks for exactly how attribute changes are recorded today, so no new event type is seeded and the migration stays pure DDL.
dbidx_formula_phase_line_formula_id and the existing composition read indexesreuseA procedure text is never filtered, joined or sorted on; it is read only with the composition, which these indexes already serve in one pass (E1, E3). No new index is designed, and any speculative one goes to the document's consider-later section.
dbFlyway migration V035 (three ADD COLUMN statements)createV034 is the highest version at the base, the branch adds no migration and main has not moved since the index, so V035 is the next free version with no collision risk (E18). No existing migration can be amended - applied files are immutable - so a new file is the only way to add a column. It is authored by the Backend Builder after DB Review, not at this stage.
backendFormulaPhaseRepository.copyPhases and FormulaPhaseLineRepository.copyLinesextendBoth are native INSERT ... SELECT statements with explicit column lists (E9, E10), so AC12 silently fails until the new phase and line columns are listed there. The statements are extended compatibly at the Backend stage; copyComposition itself needs no change (E11).
backendFormulaAuditService.recordAttributeModified and the composition lock guardreuseThe recorder already writes an attribute key with resolved display values (E8) and loadUnlockedFormula already refuses writes on a locked composition (E12). AC11 and AC9 are satisfied by calling them, so no new audit method shape and no new lock mechanism is designed.
backendProcedure read/write endpoints, DTOs and the phase-rename routeunknownDecided at the Backend stage. The approved mock already pins the five signatures the backend must serve (E19), and this stage pins the storage behind them; the controller, DTO and mapper design is not this stage's work.
frontendapi/formula-procedures.ts and the FormulaProcedures shape (general, byPhaseId, byLineId)reuseThe approved mock on the branch is the data contract this schema serves, field for field (E19, E21). It stays unchanged: the DB stage adds no field and renames none, and the flip at the Backend stage replaces only the api bodies.
frontendphaseId / lineId keying carried through the types, mapper and phase groupsreuseThe branch already carries the phase id onto every line and exposes it per group (E22, E23), honouring the Mock contract's decision to key on identity rather than on the label. The chosen columns sit on those very rows, so the keying maps onto primary keys with no further change.

Bound to

RAG revision
6ff9aee4994f
Catalog
ead4cae607ae
RAG indexed base
3830b223cba4
Feature base
3830b223cba4
Branch head
ed0ae3200407
Main
3830b223cba4
Ticket snapshot
d551b207e9ee

Contract sections

  • architecture · 13342 B · dd185901a1e9 · shown
  • frontend-plan · 1342 B · 7bdb3ccdf81c · shown
  • db-plan · 5841 B · 00a4bddf9b92 · shown
  • backend-plan · 2268 B · 778303071ea8 · shown
  • evidence · 13520 B · ed92f494ba40 · shown
  • review-plan · 6117 B · ac87165994b9 · shown
Raw logsRaw

Logs are not loaded on this page. Open the Planner run to read them.