Skip to main content

Legal state lives in untyped JSONB

Every investigative action (diligência) stores all of its structured content, validation flags, document status, and uploads inside a single inquiry_actions.step_data JSONB column. Nothing is relational.

Examples of what lives only in JSON keys:

  • step_data.<stepKey>._validated — whether a sub-step is legally validated
  • step_data.<deliverableKey>._statusdraft / completed / approved of the formal document
  • step_data.preparation.witness_name, step_data.questions[], step_data.info.* — the substantive record of a witness hearing, employee hearing, document analysis, etc.
  • step_data.hearing_minutes._status — approval of the Ata da Inquirição

Two consequences:

  1. Unqueryable and unconstrained. You cannot ask the database "which actions are validated?" or "which hearings lack approved minutes?" without scanning JSON. There is no CHECK, no foreign key, no NOT NULL — a typo in a key silently loses data. A phase's legal completeness is buried in untyped JSON.

  2. Two status vocabularies coexist and are easy to conflate: the action-level status (draft / in_progress / completed / validated) and the deliverable-level status (draft / completed / approved). They mean different things.

The witness path (InquiryActionDrawerHearingMinutesDrawer, hardcoded 5 steps / 7 sections) and the generic path (GenericActionDrawerActionDeliverableDrawer, config-driven from inquiry-action-types.ts) both write into the same JSONB contract but duplicate the logic.

The tacit assumption: the shape of step_data is stable and correct because the only writer is the matching drawer. There is no schema to enforce it.

Rebuild implication

Promote the load-bearing fields (validation state, deliverable status, key facts) to typed, queryable columns/tables with constraints. Keep JSON only for genuinely free-form notes. Unify the two deliverable subsystems behind one contract.