Data dictionary for the CHIRPdb database prepared for the Alan Turing Institute Data Study Group. See the dataset overview for context, provenance and limitations. Content conventions follow DAMA-DMBOK.
What the database contains
This dictionary documents the full CHIRPdb schema. The database as delivered is populated end to end: every pipeline stage has run over the whole corpus. Expect data in:
- Reports & text -
documents,authors,sentences, and the report-derived incident facts indocument_particularsanddocument_vessels. Two corpora sit here, separated bydocuments.jurisdiction: 796 UK MAIB reports ('UK') and 456 US NTSB reports ('US'), 1,252 documents in total. - Incident metadata from the MAIB spreadsheets -
occurrences(9,063),vessels(9,830),affected_persons(3,040),affected_persons_ppe(924), the report↔occurrence link carried ondocuments.occurrence_id, plus the vocabularies feeding them (taxonomy_terms, 410 terms across eight trees;ppe_items, 9). MAIB-only: NTSB publishes no equivalent dataset, so these tables describe UK incidents alone and no NTSB document carries anoccurrence_id. This is much the larger incident record, and it is nearly disjoint from the report corpus - only 58 reports link to an occurrence (seedocuments). - Findings from the record pass -
safety_issues(11,254, of which 479 arerecord_type = 'safety_lesson'),recommendations(4,127), therecommendation_organisationslinks (3,658; 3,342 MAIB / 316 NTSB) and theorganisationsthey resolve to (802). Every document has been through the pass:documents.records_pass_atis set on all 1,252. - Passages -
passages(15,381; 13,892 MAIB / 1,489 NTSB) andpassage_sentences(15,939; 14,382 / 1,557), written by the record pass, one passage per record: the sentences a safety issue or recommendation was read from, so every record carries its own provenance. No general passage pass has run, so these hold record passages and nothing else. - Narrative entities -
narrative_entities(545,649) andnarrative_entity_mentions(1,267,349): one entity per distinct thing per document, with every in-text mention resolved to it and located by character span. - Embeddings -
passage_embeddingscarries a vector for all 15,381 passages under the active model,shield_code_embeddingsall 452 code × strategy vectors, andpassage_embedding_queueis drained (0 rows). See § 5. - Seeded reference vocabularies -
shield_layers/shield_code_categories/shield_codes(the full SHIELD taxonomy),narrative_entity_types(14 types, referenced by every entity row),taxonomy_termsandppe_items.embedding_modelsandembedding_strategiesare seeded on the same footing - they configure the embedding layer rather than describe incidents.
Five tables are empty as delivered:
passage_shield_codes- the one analytical link the database is missing. The taxonomy is loaded and every passage is embedded, but no code has been attached to any passage. See the table's own section for what the embedding layer offers in its place.analysis- a spare table for a future per-passage pass. Nothing writes it.legislationandsafety_issue_legislation- the legislation-attachment feature is not implemented.author_identifiers- nothing populates it at this stage.
Some columns of otherwise-populated tables are empty too, and they are flagged where they appear. The ones most likely to be reached for: sentences.relevance_score; every contact field on organisations and authors; safety_issues.name and recommendations.action (the analyst-authored summaries - the report's own wording is in the passage, not here); and every is_verified / verified_at pair.
How to read this document
Every column is classified in the Source column as one of:
| Label | Meaning |
|---|---|
| Source | Source-document field - value taken from the source report, MAIB or NTSB (PDF content or file metadata) |
| MAIB | MAIB Data Portal field - value taken from the MAIB spreadsheet extract. Used only on the spreadsheet-fed tables, which hold UK incidents alone |
| Pipeline | Pipeline-derived (deterministic) - computed by the ingestion pipeline with no model involvement |
| AI-U | AI-generated (unverified) - produced by an LLM or NLP model, not confirmed by a human |
| AI-V | AI-generated (human-verified) - AI-produced and confirmed by an analyst |
| System | System metadata - surrogate keys, timestamps, status flags, generated search columns, seeded reference data |
| Analyst | Authored by a person in the review surface - never produced by the pipeline or a model, and empty until someone fills it in |
A slashed label (Pipeline / AI-U) marks a column whose extractor is deterministic with a model fallback.
Four conventions apply throughout:
- Row-level verification. Tables carrying an
is_verifiedflag (passage_shield_codes,safety_issues,recommendations) hold AI-generated content whose verification status varies per row. Their content columns are marked AI-U (or Pipeline / AI-U where a deterministic extractor populates them today); treat rows whereis_verified = trueas AI-V. Sentences are the analogous case at document level:documents.status = 'verified'marks a document whose extracted text an analyst has confirmed.In this delivery, nothing has been verified. No row anywhere carries
is_verified = true, no document is atstatus = 'verified', and the Analyst columns (safety_issues.name,recommendations.action) are empty. The AI-V and Analyst labels below therefore describe what a column would hold after review, not what it holds now - read every AI-U column as machine output no human has checked. - Sentence text. Text is extracted by a vision LLM reading a rendered image of each page, with deterministic passes (structural-metadata normalisation, line rejoin, content-loss repair) applied on top of the model output. Sentence text and structure columns are therefore marked AI-U, qualified by the parent document's
status. - Two record sets, linked - not merged. Incident data comes from two sources kept in separate tables rather than reconciled into one row:
- the MAIB Data Portal spreadsheet →
occurrencesand its childrenvessels/affected_persons, plus the vocabulariestaxonomy_terms/ppe_items. Spreadsheet-only facts, UK-only;source_id/source_modified_atcarry MAIB's per-record identifier and revision date, driving upsert on each refresh so only MAIB-revised rows are touched. Columns on these tables carry the MAIB label. - the report PDF, MAIB or NTSB →
document_particulars(incident facts stated in the report) anddocument_vessels(vessels named in the report), each hanging offdocuments. Columns fed this way carry the Source label, which says nothing about which body published the report - readdocuments.jurisdictionfor that.
A MAIB report links to its occurrence through
documents.occurrence_id, withdocuments.occurrence_matched_onrecording how the link was made (publication_numberormanual); several reports can point at the same occurrence (e.g. an interim report followed by the final one). NTSB documents have no occurrence to link to, so only the report-PDF set applies to them. - the MAIB Data Portal spreadsheet →
- Jurisdiction split.
documents.jurisdictionis the column that separates the corpora -'UK'for MAIB,'US'for NTSB - and everything owned by a document (sentences,document_particulars,document_vessels,passages,safety_issues,recommendations,narrative_entities) inherits that split through itsdocument_id. Field coverage differs between the two: see the per-column notes ondocuments,document_particularsanddocument_vessels, and the fill-rate figures in the README.
All tables carry created_at and updated_at (timestamptz, defaulting to now(), with updated_at maintained by a BEFORE UPDATE trigger). These are listed once per table without repeating the explanation.
1. Reports and incident record
documents
One row per ingested report PDF - MAIB or NTSB. The root of the ownership chain: deleting a document cascades to everything extracted from it. 1,252 rows: 796 with jurisdiction = 'UK' (MAIB), 456 with 'US' (NTSB).
Keys: PK id. UNIQUE hash. FK author_id → authors(id) ON DELETE SET NULL. FK occurrence_id → occurrences(id) ON DELETE SET NULL. CHECK documents_lease_requires_start (claim_expires_at set implies claim_started_at set); CHECK documents_occurrence_link_has_provenance (occurrence_id set implies occurrence_matched_on set); CHECK occurrence_matched_on is null or in ('publication_number', 'manual'); CHECK documents_jurisdiction_check (jurisdiction is null or in ('UK', 'US')).
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| author_id | uuid | yes | Investigating body that published the report (MAIB or NTSB) | FK to authors; survives author deletion (SET NULL) | Pipeline |
| title | text | no | Report title | - | Source |
| url | text | yes | URL the report was retrieved from | - | Pipeline |
| filename | text | yes | Filename of the source PDF | - | Pipeline |
| hash | text | no | Hash of the source file; dedupes re-ingestion | unique | Pipeline |
| publication_number | text | yes | Publisher's report reference: MAIB Publication_No for UK rows, NTSB report number for US rows. For MAIB it is also the key a report is matched to a spreadsheet occurrence on | null on 128 rows - 125 MAIB, 3 NTSB. NTSB values are prefixed MAB- (265), MIR- (167) or MAR- (21) | Source |
| document_type | text | yes | GOV.UK publication class (Publication_Type), e.g. investigation-report, safety-bulletin | MAIB rows only - set on 794 of 796, null for every NTSB document | Source |
| occurrence_id | uuid | yes | The spreadsheet occurrence this report describes; resolved from publication_number. Several reports (e.g. interim + final) may point at one occurrence | FK to occurrences, SET NULL; MAIB rows only (58 populated), always null for NTSB - see the note below | Pipeline |
| occurrence_matched_on | text | yes | How the occurrence link was made | publication_number (auto) or manual; required when occurrence_id is set | Pipeline |
| publication_date | date | yes | Date the report was published | - | Source |
| status | document_status | no | Extraction-pipeline state | enum: queued, extracting, extracted, verified, extraction_failed; default queued | System |
| extraction_fallback | boolean | no | True if extraction fell back to a degraded result after retries were exhausted | default false | System |
| records_pass_at | timestamptz | yes | When the record pass last ran over this document; null means it never has, which is how a resumed pass finds its remaining work | - | System |
| records_rerunnable | boolean | no | True where the record pass left something worth another attempt - the only state a pass carries forward, so a resumed run re-picks the document | default false | System |
| records_fallback | boolean | no | True where the record pass reached its result through an LLM route rather than the deterministic one alone | default false | System |
| jurisdiction | text | yes | Reporting jurisdiction; the column that separates the two corpora | default 'UK'; 'UK' = MAIB (796), 'US' = NTSB (456) | Pipeline |
| page_count | integer | yes | Page count of the source PDF | - | Pipeline |
| pdf_subject | text | yes | Subject field from PDF file metadata | - | Source |
| pdf_author | text | yes | Author field from PDF file metadata | - | Source |
| claim_token | uuid | yes | Lease token held by the worker currently extracting this document; guards against double-processing | - | System |
| claim_started_at | timestamptz | yes | When the current extraction lease was taken | - | System |
| claim_expires_at | timestamptz | yes | When the lease expires and the document may be re-queued | requires claim_started_at (check-constrained) | System |
| title_tsv | tsvector | - | Full-text search index over title | generated column (always derived) | System |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
Document status lifecycle: queued → extracting → extracted → verified, with extracting → extraction_failed on an irrecoverable crash. extracted means the automated pipeline finished; verified means an analyst confirmed the results. The claim_* columns are the extraction lease used to recover documents stranded by a crashed worker. As delivered, all 1,252 documents are at extracted, and 201 carry extraction_fallback = true (182 MAIB, 19 NTSB).
The records_* columns track the separate record pass. As delivered every document has records_pass_at set - the pass has run over the whole corpus - and 981 carry records_fallback = true, meaning the deterministic extractor alone did not settle the result and an LLM route was used. That flag says how the records were reached, not whether they are wrong.
The report corpus and the spreadsheet barely overlap, and that is the database's sharpest limit. occurrence_id links only 58 reports to an occurrence. The cause is on the spreadsheet side: it only goes as far back as 2018. Since then, MAIB have investigated only 61 occurrences that get a publication_number, and 58 of which produced a report. So 738 of the 796 MAIB reports have no occurrence counterpart, and 9,005 occurrences have no adjoining report.
document_particulars
Incident facts as stated in the report PDF - the report-path counterpart to the spreadsheet's occurrences. One row per document (1:1), so 1,252 rows across both corpora. Columns are populated only where the report states the fact, and NTSB reports state fewer of them: of 456 NTSB rows, accident_date is set on 198, accident_location 208, accident_type 130, severity 71, loss_of_life 33 (MAIB, of 796: 796 / 731 / 279 / 305 / 183).
Keys: PK document_id. FK document_id → documents(id) ON DELETE CASCADE. CHECK loss_of_life is null or ≥ 0.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| document_id | uuid | no | The report these particulars are drawn from | PK, FK, cascade delete | Pipeline |
| accident_date | date | yes | Date of the incident as stated in the report | - | Source |
| accident_location | text | yes | Named place of the incident | free text from report | Source |
| severity | text | yes | Severity as stated in the report | free text from report | Source |
| accident_type | text | yes | Type of accident | free text; ⚠ unreliable - often holds the voyage or vessel-operation type instead (see note) | Source |
| loss_of_life | integer | yes | Number of deaths | null or ≥ 0 (check-constrained) | Source |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
accident_type does not reliably hold the accident type. The extraction pass frequently picks up the report's statement of the voyage or vessel-operation type (what the vessel was doing) rather than what happened to it, so the column mixes the two. Do not treat it as an accident classification without reading the values first; for MAIB documents, the spreadsheet's occurrences.main_event_term_id (the main_event taxonomy) is the classification to use instead.
document_vessels
Vessels named in the report PDF - the report-path counterpart to the spreadsheet's vessels. The schema allows a document many vessel rows, but as delivered there are 1,252 rows and exactly one per document: an incident involving several vessels is not represented as several rows here. Coverage differs by corpus:
| Column | MAIB (of 796) | NTSB (of 456) |
|---|---|---|
vessel_name | 299 | 400 |
vessel_type | 796 | 395 |
port_of_origin | 286 | 0 |
destination | 262 | 0 |
Keys: PK id. FK document_id → documents(id) ON DELETE CASCADE.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| document_id | uuid | no | Owning report | FK, cascade delete | Pipeline |
| vessel_name | text | yes | Vessel name as stated in the report | - | Source |
| vessel_type | text | yes | Vessel type as stated in the report | free text from report | Source |
| port_of_origin | text | yes | Port of origin | - | Source |
| destination | text | yes | Destination | - | Source |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
authors
Investigating bodies that publish reports (MAIB, NTSB). Shared reference data, not owned by any document. 22 rows as delivered, and they are not yet canonicalised: the same body appears under several name variants taken from PDF metadata (e.g. MAIB, Marine Accident Investigation Branch, www.maib.gov.uk; NTSB, National Transportation Safety Board, and the typo NSTB), alongside rows that are individual people named as the PDF's author. Group by the body yourself - or filter on documents.jurisdiction - rather than trusting authors.name as a key.
Keys: PK id.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| name | text | no | Name of the publishing body | - | Source |
| text | yes | Contact email for the publishing body | scraped, then manually curated/checked (final version); not populated here | Pipeline | |
| phone_number | text | yes | Contact phone | scraped, then manually curated/checked (final version); not populated here | Pipeline |
| address | text | yes | Postal address | scraped, then manually curated/checked (final version); not populated here | Pipeline |
| website_url | text | yes | Website | scraped, then manually curated/checked (final version); not populated here | Pipeline |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
author_identifiers
Curated identifiers that canonicalise an author, one row per identifier - so name variants (e.g. "MAIB" vs "Maritime Accident Investigation Branch") resolve to a single author rather than many near-duplicates.
Keys: PK id. FK author_id → authors(id) ON DELETE CASCADE.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| author_id | uuid | no | Owning author | FK, cascade delete | Pipeline |
| identifier | text | no | Curated identifier used to resolve name variants to one canonical author (e.g. "MAIB" and "Maritime Accident Investigation Branch" → the same body) | curated; empty as delivered | System |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
occurrences
One incident as recorded in the MAIB Data Portal spreadsheet - what happened, when, where, how severe. Purely spreadsheet-sourced; it holds no report-PDF fields, and it is UK-only: NTSB has no counterpart feed, so this table and its children describe MAIB incidents alone. The report(s) describing an occurrence point to it via documents.occurrence_id, and the report's own view of the incident lives in document_particulars.
9,063 rows. Well filled: short_description, description and severity are set on every row, latitude / longitude on 9,062, main_event_term_id on 9,061. Only 61 rows carry a publication_number, which is why so few reports link (see documents).
Keys: PK id. UNIQUE source_id. FK main_event_term_id → taxonomy_terms(id) ON DELETE SET NULL.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| source_id | text | no | MAIB's stable per-incident identifier (Occurrence_Id); upsert key for spreadsheet refresh | not null, unique | MAIB |
| source_modified_at | date | yes | MAIB's per-record revision date (Occurrence_Record_Modified_On); drives selective refresh | - | MAIB |
| publication_number | text | yes | MAIB report reference (Publication_No); the value a report's documents.publication_number is matched against to set documents.occurrence_id | - | MAIB |
| accident_date | date | yes | Month of the incident (Local_Date_Main_Event). ⚠ Month precision: the day is always 01 and carries no information - aggregate by month or year, never by day or weekday | - | MAIB |
| date_unconfirmed | boolean | yes | Whether MAIB's date is unconfirmed (Date_Main_Event_Unconfirmed) | true on 127 rows | MAIB |
| severity | text | yes | Incident severity (Occurrence_Severity) | clean 4-value vocab, no nulls: Marine Incident (5,000), Less Serious (3,294), Serious (457), Very Serious (312) | MAIB |
| main_event_term_id | uuid | yes | Principal event, as a taxonomy term (Main_Event_L1/L2) | FK to taxonomy_terms (main_event) | MAIB |
| short_description | text | yes | Brief description of the incident | - | MAIB |
| description | text | yes | Fuller description of the incident | - | MAIB |
| latitude | text | yes | Latitude, captured verbatim (deliberately text, not a geo type) | not validated as coordinates | MAIB |
| longitude | text | yes | Longitude, captured verbatim (see above) | not validated as coordinates | MAIB |
| year_reported | integer | yes | Year the incident was reported | - | MAIB |
| location_zone | text | yes | Sea-area zone (Occurrence_Location) | 12 values in Family -> Detail form; split on -> for the family: Coastal waters <= 12 nm (4,044), Internal waters (3,692, mostly -> Port area), Open sea (784, -> Within EEZ / -> Outside EEZ), Inland waters (542) | MAIB |
| accident_port | text | yes | Nearest port to the incident (Port_Of_Accident_L2) | - | MAIB |
| state_reporting | text | yes | Reporting jurisdiction (State_Reporting) | UK / Non-UK | MAIB |
| natural_light | text | yes | Natural light at the incident (Natural_Light) | - | MAIB |
| sea_state | text | yes | Sea state (Sea_State) | - | MAIB |
| visibility | text | yes | Visibility (Visibility) | - | MAIB |
| weather | text | yes | Weather (Weather) | - | MAIB |
| wind_force | text | yes | Wind force (Wind_Force) | - | MAIB |
| sar_intervention | text | yes | Whether search-and-rescue was involved (SAR_Intervention) | - | MAIB |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
main_event_term_id references the shared taxonomy_terms tree (see §4). Report-PDF incident facts (accident type, named location, loss of life) are not held here - they live in document_particulars, reached via documents.occurrence_id. A report is linked to its occurrence by documents.occurrence_id (the FK sits on documents, so several reports can share one occurrence).
vessels
Vessels involved in a spreadsheet occurrence - the particulars from the MAIB Data Portal, and so UK-only. An occurrence involves one or more vessels. Vessels as named in the report PDF are separate: see document_vessels.
9,830 rows over 9,063 occurrences, every one attached to an occurrence. ship_type_term_id and vessel_category_term_id are set on every row; place_on_board_term_id on 7,995. No vessel name is held here - the MAIB extract does not publish one.
Keys: PK id. UNIQUE source_id. FK occurrence_id → occurrences(id) ON DELETE CASCADE. FKs vessel_category_term_id, ship_type_term_id, place_on_board_term_id → taxonomy_terms(id) ON DELETE SET NULL.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| occurrence_id | uuid | yes | Owning occurrence | FK, cascade delete | Pipeline |
| source_id | text | no | MAIB's per-vessel identifier (Vessel_Profile_Id); upsert key | not null, unique | MAIB |
| source_modified_at | date | yes | MAIB's per-record revision date (Vessel_Record_Modified_On) | - | MAIB |
| flag_state | text | yes | Flag state (Flag_State) | - | MAIB |
| vessel_category_term_id | uuid | yes | Size band as a taxonomy term (Vessel_Category_L1/L2) | FK to taxonomy_terms (vessel_category) | MAIB |
| ship_type_term_id | uuid | yes | What the vessel is, as a taxonomy term (Ship_Craft_Type_L1-L4) | FK to taxonomy_terms (ship_type) | MAIB |
| is_commercial_vessel | boolean | yes | Commercial vs leisure (Is_Commercial_Vessel) | - | MAIB |
| hull_material | text | yes | Hull material (Hull_Material) | - | MAIB |
| gross_tonnage | numeric | yes | Gross tonnage (GT_Gross_Tonnage) | - | MAIB |
| length_metres | numeric | yes | Overall length in metres (LOA_Length_Overall_in_Metres, fallback Registered_Length_In_Metres) | - | MAIB |
| year_built | integer | yes | Year built | - | MAIB |
| port_of_origin | text | yes | Departure port (Port_Of_Departure_L2) | - | MAIB |
| voyage_segment | text | yes | Phase of voyage (Voyage_Segment) | e.g. Mid-water, Arrival, Anchored | MAIB |
| pilot_on_board | text | yes | Pilotage state (Pilot_On_Board) | - | MAIB |
| crew_count | integer | yes | Crew on the voyage (Crew_Voyage) | - | MAIB |
| place_on_board_term_id | uuid | yes | Where on the vessel the event occurred, as a taxonomy term (Place_On_Board_L1-L3) | FK to taxonomy_terms (vessel_place_on_board) | MAIB |
| did_vessel_sink | boolean | yes | Whether the vessel sank (Did_The_Vessel_Sink) | - | MAIB |
| vessel_abandoned | boolean | yes | Whether the vessel was abandoned (Vessel_Abandoned) | - | MAIB |
| damage_severity | text | yes | Damage-severity grade (Loss_Of_Vessel_Damage), verbatim from the spreadsheet | 10 values in the same Family -> Detail form as occurrences.location_zone: No damage (7,669), Minor damage (less than material damage) (1,440), Material damage (504 across four spellings), Loss of ship (206). ⚠ Not case-folded: 11 rows read No Damage | MAIB |
| damage_description | text | yes | Free-text damage detail (Damage_Description); distinct from the grade | - | MAIB |
| pollution_result | text | yes | Pollution outcome (Pollution_Result) | - | MAIB |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
Death and injury counts are not stored on vessels; count them from affected_persons by person_type and physical-condition outcome so there is one source of truth. Vessel classification lives entirely in taxonomy_terms references (vessel_category_term_id, ship_type_term_id); the report's own vessel names/types sit apart in document_vessels.
affected_persons
People affected aboard a vessel of a spreadsheet occurrence, so UK-only like its parents. A vessel carries zero or more affected persons. No names are stored - demographic and injury attributes only.
3,040 rows, every one attached to a vessel - so the great majority of the 9,830 vessels record no affected person. physical_condition_term_id is set on every row, deviation_term_id on 3,035, injury_type_term_id on 3,013, place_on_board_term_id on 2,854. person_type is Crew (2,132), Passenger (767) or Other (141); gender is Male (2,006), Female (743), Unknown (283) or Not available - PCF (8).
Keys: PK id. UNIQUE source_id. FK vessel_id → vessels(id) ON DELETE CASCADE. FKs physical_condition_term_id, injury_type_term_id, deviation_term_id, place_on_board_term_id → taxonomy_terms(id).
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| vessel_id | uuid | yes | Vessel the person was aboard | FK, cascade delete | Pipeline |
| source_id | text | yes | MAIB's per-person identifier (Affected_Person_Id); upsert key | unique | MAIB |
| source_modified_at | date | yes | MAIB's per-record revision date (Affected_Person_Record_Modified_On) | - | MAIB |
| sequence_no | integer | yes | Person's order within the incident (Sequence_No) | - | MAIB |
| age | integer | yes | Age | - | MAIB |
| gender | text | yes | Gender as stated | free text | MAIB |
| person_type | text | yes | Role category (Affected_Person_Type, e.g. crew, passenger) | free text | MAIB |
| rank | text | yes | Rank or position aboard | free text | MAIB |
| on_duty | boolean | yes | Whether on duty at the time | - | MAIB |
| nationality | text | yes | Nationality | free text | MAIB |
| physical_condition_term_id | uuid | yes | Outcome / severity / incapacitation as a taxonomy term (Physical_Condition_L1-L3) | FK to taxonomy_terms (physical_condition) | MAIB |
| injury_type_term_id | uuid | yes | Injury and subtype as a taxonomy term (Injury_Type_L1/L2) | FK to taxonomy_terms (injury_type) | MAIB |
| parts_of_body_injured | text | yes | Body parts injured (Parts_of_Body_Injured) | free text | MAIB |
| deviation_term_id | uuid | yes | Accident mechanism as a taxonomy term (Affected_Person_Deviation_L1/L2, e.g. slip/fall, body movement) | FK to taxonomy_terms (deviation) | MAIB |
| place_on_board_term_id | uuid | yes | Where the person was, as a taxonomy term (Affected_Person_Place_On_Board_L1-L5) | FK to taxonomy_terms (person_place_on_board) | MAIB |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
Injury type is a taxonomy_terms reference (injury_type_term_id). PPE worn and lacking is recorded per person in affected_persons_ppe (below), one row per item.
affected_persons_ppe
Junction: which PPE items a person wore or lacked. One row per person per item, split out of the spreadsheet's comma-joined PPE_Used / PPE_Deficient values so items can be counted and filtered.
924 rows - 575 used, 349 deficient - covering a small minority of the 3,040 affected persons. Absence of a row is not evidence the item was absent: the spreadsheet records PPE only where MAIB captured it.
Keys: Composite PK (affected_person_id, ppe_item_id, status). FKs affected_person_id → affected_persons(id), ppe_item_id → ppe_items(id).
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| affected_person_id | uuid | no | The person | PK part, FK | Pipeline |
| ppe_item_id | uuid | no | The PPE item | PK part, FK | Pipeline |
| status | text | no | Whether the item was worn or missing | used (from PPE_Used) / deficient (from PPE_Deficient) | MAIB |
| deficiency_reason | text | yes | Why the item was deficient, where recorded | - | MAIB |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
2. Parsed text
sentences
Every sentence of every report, in original reading order - 341,057 rows, 293,454 from MAIB documents and 47,603 from NTSB ones. The atomic unit of text; everything in the analysis layer traces back here. Sentences carry no jurisdiction of their own: join through document_id to split the corpora. Text is produced by a vision LLM reading a rendered image of each page, with deterministic passes applied on top - the stored text reflects that output, with the parent document's status indicating how much to trust it.
There is no sections table: a row with text_type = 'heading' is a section boundary, and a section's contents are the sentences whose sort_order falls between that heading and the next.
By text_type: 155,117 paragraph, 77,222 list_item, 47,648 heading, 32,532 frontmatter, 17,455 caption, 7,907 footnote, 3,176 table. frontmatter is cover pages, contents lists and boilerplate - exclude it from most analyses.
Keys: PK id. FK document_id → documents(id) ON DELETE CASCADE. CHECK heading_level BETWEEN 1 AND 6. CHECK page_number is null or ≥ 1.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| document_id | uuid | yes | Owning report | FK, cascade delete | Pipeline |
| text | text | no | Sentence text | - | AI-U (vision-LLM extracted from the page image, deterministic passes on top; see documents.status) |
| text_type | sentence_text_type | no | Kind of text | enum: heading, paragraph, list_item, footnote, caption, table, frontmatter | AI-U |
| sort_order | integer | yes | Position in original reading order (0-based) | - | Pipeline |
| relevance_score | integer | yes | AI-assigned score of a sentence's analytical relevance (e.g. ~0 for frontmatter, ~1 for a safety-issue / SHIELD-relevant sentence) | ⚠ empty - null on all 341,057 rows. The scoring pass was never run | AI-U |
| heading_level | smallint | yes | Heading depth; set only on headings | 1–6, check-constrained; null on 236 of the 47,648 headings | AI-U |
| page_number | integer | yes | Source PDF page the sentence was extracted from | null or ≥ 1 (check-constrained); populated on every row | Pipeline |
| block_id | text | yes | Identifier of the table a row came from - the only way to reassemble a table from its rows | set on table rows only: all 3,176 text_type = 'table' rows, and 3 others | Pipeline |
| text_tsv | tsvector | - | Full-text search index over text | generated column | System |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
passages
A grouped passage of consecutive sentences - the unit the analysis pipeline works on. Membership lives in passage_sentences, so the grouping strategy can change without touching sentence rows; the passage also carries its own document_id rather than relying on the transitive path through its sentences, which does not survive a re-extraction.
15,381 rows as delivered - 13,892 from MAIB documents, 1,489 from NTSB ones - all written by the record pass, every one carrying a document_id and non-null source_text. That count is exactly the number of findings (11,254 safety issues + 4,127 recommendations), one passage each.
Keys: PK id. FK document_id → documents(id) ON DELETE CASCADE. CHECK passages_source_valid (source in ('record_pass', 'manual')). A trigger (trg_validate_passage_document) rejects a document_id that contradicts the document derived from the passage's sentences; it validates rather than derives, so a passage with no sentences yet may name its document freely.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| document_id | uuid | yes | Report the passage belongs to, recorded directly rather than derived through passage_sentences | FK, cascade delete; nullable, but set on every delivered row | Pipeline |
| source | text | no | Which writer created the passage | record_pass or manual, CHECK-constrained; every delivered row is record_pass | System |
| source_text | text | yes | Text of the passage | Trigger-derived from member sentences; direct writes rejected | Pipeline (derived from member sentences) |
| source_text_tsv | tsvector | - | Full-text search index over source_text | generated column | System |
| source_text_sha256 | bytea | yes | Hash of source_text, so an embedding can say which wording it was computed from and go stale when the passage is rewritten | trigger-derived alongside source_text | System |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
document_id is nullable only because two cases genuinely have no answer - a passage orphaned by a re-ingestion, and a passage whose sentences span more than one document - and neither occurs in the delivered data. Deleting a document deletes its passages, matching sentences, safety_issues and recommendations.
passage_sentences
Junction: which sentences belong to which passage, and in what order. 15,939 rows across the 15,381 passages - 14,382 from MAIB documents and 1,557 from NTSB ones. Record passages are short, most of them a single sentence.
Keys: Composite PK (passage_id, sentence_id). FKs passage_id → passages(id), sentence_id → sentences(id), both ON DELETE CASCADE.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| passage_id | uuid | no | Passage | PK part, FK, cascade delete | Pipeline |
| sentence_id | uuid | no | Member sentence | PK part, FK, cascade delete | Pipeline |
| sort_order | integer | yes | Sentence position within the passage | auto-assigned by trigger when null | Pipeline |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
narrative_entities
Canonical named things - people, vessels, organisations, locations, equipment and nine more types - extracted from a document's text, one row per distinct thing per document, with a normalised canonical name. Coreference resolution is persisted here: many surface forms, one entity.
545,649 rows as delivered: 446,735 from MAIB documents and 98,914 from NTSB ones, roughly 436 entities per report. By type, the largest are equipment (118,152), condition (61,836), location (60,738), procedure (54,427) and document (39,787); 10,942 rows fell outside the vocabulary and carry type = 'other' with the extraction's own suggestion in proposed_type.
Keys: PK id. FK document_id → documents(id) ON DELETE CASCADE. FK type → narrative_entity_types(name) ON DELETE RESTRICT (natural-key FK). A database trigger enforces that an entity's mentions can only anchor to sentences of the entity's own document.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| document_id | uuid | no | Report the entity is scoped to | FK, cascade delete | Pipeline |
| type | text | no | Entity type | FK to narrative_entity_types(name), one of the 14 seeded types | AI-U |
| canonical_name | text | no | Normalised name for the entity | - | AI-U |
| proposed_type | text | yes | The type the extraction proposed for an entity the vocabulary has no term for, kept rather than discarded so the vocabulary can grow from what the corpus actually contains | CHECK: set if and only if type is other, and non-blank | AI-U |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
narrative_entity_mentions
Each occurrence of an entity in the text: the exact words used and their character position within the sentence.
1,267,349 rows as delivered: 1,032,182 from MAIB documents and 235,167 from NTSB ones. By mention_type: 894,775 definite_description, 251,847 name, 70,390 split_antecedent, 50,337 pronoun. confidence is populated on every row.
Keys: PK id. FKs narrative_entity_id → narrative_entities(id), sentence_id → sentences(id), both ON DELETE CASCADE. CHECK: char_span non-empty with lower bound ≥ 0. CHECK: confidence null or between 0 and 1.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| narrative_entity_id | uuid | no | Entity being mentioned | FK, cascade delete | AI-U |
| sentence_id | uuid | no | Sentence containing the mention | FK, cascade delete | AI-U |
| surface_form | text | no | Exact words used (e.g. "the Master", "Captain Smith") | - | AI-U |
| char_span | int4range | no | Half-open character range into the sentence text | non-empty, lower ≥ 0 (check-constrained) | AI-U |
| mention_type | text | yes | How the entity is referred to at this mention - the distinction coreference turns on, since a pronoun carries no name to match | CHECK: null or one of name, definite_description, pronoun, split_antecedent | AI-U |
| confidence | real | yes | The extraction's own confidence in this mention belonging to the entity | - | AI-U |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
3. Analysis and findings
analysis
Free-form per-passage analysis output - somewhere a future analytical pass can record results without a schema change. Several passes can sit side by side on the same passage. No verification flag exists, so any content here would be unverified.
Empty as delivered. Nothing in the current pipeline writes it.
Keys: PK id. FK passage_id → passages(id) ON DELETE CASCADE.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| passage_id | uuid | yes | Passage analysed | FK, cascade delete | Pipeline |
| analysis_type | text | yes | Optional label an analyst supplies when uploading an analysis of a passage | free text; no controlled vocabulary | System (analyst-provided) |
| content | text | yes | The analysis output itself | - | AI-U |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
passage_shield_codes
Application of a SHIELD causal-factor code to a passage - the link that makes incidents comparable and countable. Verification lives on the association itself, because an analyst verifies "this code applies to this passage".
Empty as delivered. All 113 SHIELD codes and all 15,381 passages are present and embedded, but no code has been matched to a passage yet.
What exists in place of it is a suggestion function, match_shield_codes(p_passage_id uuid, p_strategy text DEFAULT 'category_definition', p_limit integer DEFAULT 5, p_model text DEFAULT NULL). It ranks SHIELD codes by cosine proximity to the passage's stored vector and returns (shield_code_id, code, title, category, layer, similarity), where similarity is 1 - cosine_distance - higher is closer. It reads; it never writes. Populating this table from its output, and deciding what similarity is worth accepting, is open work.
Keys: Composite PK (passage_id, shield_code_id). FKs passage_id → passages(id), shield_code_id → shield_codes(id), both ON DELETE CASCADE.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| passage_id | uuid | no | Tagged passage | PK part, FK, cascade delete | AI-U (the tagging is the AI judgement; AI-V where is_verified) |
| shield_code_id | uuid | no | Applied SHIELD code | PK part, FK, cascade delete | AI-U (as above) |
| is_verified | boolean | yes | Analyst has confirmed this code applies to this passage | default false | System (records human verification action) |
| verified_at | timestamptz | yes | When verified | trigger-stamped when is_verified flips true, cleared when false | System |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
safety_issues
A named safety concern the report raises, owned by a document and optionally traced to the passage it was drawn from. passage_id cascades: a passage is deleted when it loses its last sentence, and the finding goes with it, because a finding whose source text no longer exists is not evidence of anything. The column stays nullable because a finding created standalone through the API has no passage at all.
11,254 rows as delivered: 10,038 from MAIB documents and 1,216 from NTSB ones. Split by record_type, 10,775 are safety_issue (9,734 MAIB / 1,041 NTSB) and 479 are safety_lesson (304 / 175). Every row came from the record pass; none is analyst-verified.
Keys: PK id. FK document_id → documents(id) ON DELETE CASCADE. FK passage_id → passages(id) ON DELETE CASCADE. CHECK safety_issues_record_type_valid (record_type in ('safety_issue', 'safety_lesson')). CHECK safety_issues_source_valid (source in ('record_pass', 'manual')). Partial index on record_type where record_type <> 'safety_issue' - nearly every row is a safety_issue, so lessons are the selective side worth indexing. A trigger validates (and derives, where unambiguous) document_id against the passage's actual document, falling back to passages.document_id when the passage has no sentences yet.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| document_id | uuid | yes | Owning report | FK, cascade delete; trigger-validated against passage | Pipeline |
| passage_id | uuid | yes | Passage the issue was drawn from | FK, cascade delete | Pipeline (provenance link) |
| name | text | yes | An analyst's name for the issue, where a nomenclature is being applied to group issues across reports. Not the report's own wording - that is the passage's sentences. Empty until reviewed | free text; no extraction pass writes it | Analyst |
| record_type | text | no | Whether the row is a finding about this accident or a general safety lesson to the industry | safety_issue or safety_lesson, CHECK-constrained; default safety_issue | Pipeline / AI-U (AI-V where is_verified) |
| source | text | no | Which writer created the row | record_pass or manual, CHECK-constrained; every delivered row is record_pass | System |
| is_contributory | boolean | yes | Did this issue contribute directly to the accident? | null means the report did not classify it (see note) | Pipeline / AI-U (AI-V where is_verified) |
| is_addressed | boolean | yes | Was the issue addressed, or did it result in recommendations? | null means the report did not classify it (see note) | Pipeline / AI-U (AI-V where is_verified) |
| is_verified | boolean | yes | Analyst has confirmed the finding | default false | System (records human verification action) |
| verified_at | timestamptz | yes | When verified | trigger-stamped | System |
| name_tsv | tsvector | - | Full-text search index over name | generated column | System |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
is_contributory and is_addressed split MAIB's four-way classification of safety issues (directly contributing, addressed or resulted in recommendations / other directly contributing / not directly contributing, addressed or resulted in recommendations / other not directly contributing) into the two independent facts it actually encodes. Both are nullable because only modern-era reports classify at all: mid-era reports print bare FINDINGS / OTHER FINDINGS, older ones list findings under a CONCLUSIONS part, and bulletins do not classify. Null means the report did not say - never "unknown, pending backfill". As delivered, is_contributory is set on 2,975 of the 11,254 rows and is_addressed on 3,035.
record_type distinguishes the two kinds of free-text observation the table holds. A safety_lesson is general advice to the industry, printed under a SAFETY LESSONS heading in bulletins and in the safety-flyer annexes of full reports; for several bulletins it is the only safety content in the document. Lessons share this table because they have the same shape as issues - free text, no addressee, no identifier - and simply leave the two classification booleans null.
recommendations
An action the report calls for, in the report's own words. Same shape and delete semantics as safety_issues.
4,127 rows as delivered: 3,854 from MAIB documents and 273 from NTSB ones. reference_number is set on 2,414 and addressee on 3,159; none is analyst-verified.
Keys: PK id. FK document_id → documents(id) ON DELETE CASCADE. FK passage_id → passages(id) ON DELETE CASCADE. CHECK recommendations_source_valid (source in ('record_pass', 'manual')). Partial index on reference_number where not null. Same document_id validation trigger as safety_issues.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| document_id | uuid | yes | Owning report | FK, cascade delete; trigger-validated against passage | Pipeline |
| passage_id | uuid | yes | Passage the recommendation was drawn from | FK, cascade delete | Pipeline (provenance link) |
| action | text | yes | An analyst's summary of the action to take. Not the report's own wording - that is the passage's sentences. Empty until reviewed | free text; no extraction pass writes it | Analyst |
| reference_number | text | yes | The publisher's own reference for the recommendation, as printed (e.g. 2026/149, S2024/137M) | set on 2,414 rows; indexed, deliberately not unique, so do not use it as a key | Pipeline / AI-U (AI-V where is_verified) |
| addressee | text | yes | Who the recommendation is aimed at, in the report's own words with its lead-in frame removed (e.g. "Poseidon Trawlers Limited", not "Poseidon Trawlers Limited is recommended to:") | free text; one lead-in governs several recommendations, so the same value repeats across rows | Pipeline / AI-U (AI-V where is_verified) |
| source | text | no | Which writer created the row | record_pass or manual, CHECK-constrained; every delivered row is record_pass | System |
| is_verified | boolean | yes | Analyst has confirmed the finding | default false | System (records human verification action) |
| verified_at | timestamptz | yes | When verified | trigger-stamped | System |
| action_tsv | tsvector | - | Full-text search index over action | generated column | System |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
reference_number is the publisher's sequential identifier across its whole annual output, so one report's recommendations run consecutively and a recommendation made in one report can be cited by name in a later one. Coverage is uneven: every NTSB recommendation carries one (273 of 273), but only 2,141 of the 3,854 MAIB ones do, the rest predating the numbering scheme.
addressee keeps the report's own wording rather than pointing at organisations, because the party addressed is often a class of people ("All UK Operators of small commercial high speed craft…") rather than a nameable company. It is set on 2,904 MAIB and 255 NTSB rows.
safety_issue_legislation
Junction linking a safety issue to legislation that bears on it, with the passage of text that made the connection - so the citation itself is traceable. Empty, along with legislation itself.
Keys: Composite PK (safety_issue_id, legislation_id). FKs to safety_issues(id) and legislation(id), both ON DELETE CASCADE.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| safety_issue_id | uuid | no | Safety issue | PK part, FK, cascade delete | AI-U (the link is the AI judgement) |
| legislation_id | uuid | no | Legislation cited | PK part, FK, cascade delete | AI-U |
| extracted_text | text | yes | The passage that made the connection | - | AI-U |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
recommendation_organisations
Junction linking a recommendation to the organisations it is directed at. Implementation is tracked per organisation, because each may act on the same recommendation at a different time.
Written by the record pass: each recommendation's addressee is cleaned of the report's own connective wording, split into the parties it names, and each party resolved - one row per party. That is why this is a junction rather than a foreign key on recommendations: "The International Chamber of Shipping, the International Maritime Pilots' Association and the International Harbour Masters' Association" is one addressee and three answerable bodies. The links are an enrichment; the verbatim addressee on the recommendation stays the record of who was addressed, and is what to fall back on where resolution produced no row.
3,658 rows as delivered - 3,342 from MAIB documents and 316 from NTSB ones - linking the 3,159 recommendations that name an addressee to the 802 organisations those addressees resolved to. Nothing has been marked implemented; is_implemented and implemented_at are for CHIRP's own follow-up and are empty.
Keys: Composite PK (recommendation_id, organisation_id). FKs to recommendations(id) and organisations(id), both ON DELETE CASCADE.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| recommendation_id | uuid | no | Recommendation | PK part, FK, cascade delete | AI-U (the directed-at link) |
| organisation_id | uuid | no | Addressee organisation | PK part, FK, cascade delete | AI-U |
| is_implemented | boolean | yes | Organisation has implemented the recommendation | default false | System (records human tracking action) |
| implemented_at | timestamptz | yes | When marked implemented | trigger-stamped when is_implemented flips true, cleared when false | System |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
4. Lookup and reference tables
shield_layers
The four layers of the SHIELD taxonomy - the top tier, grouping categories. Seeded reference data (4 layers). Referenced by natural key from shield_code_categories.layer, so a category row names its layer without a join.
Keys: PK name. UNIQUE sort_order.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| name | text | no | Layer name | PK; Acts, Preconditions, Operational Leadership, Organisation | System (seeded CHIRP taxonomy) |
| definition | text | no | What the layer covers, per the SHIELD paper | - | System (seeded CHIRP taxonomy) |
| sort_order | smallint | no | Position in the taxonomy, sharp end downwards | unique; 1 = Acts … 4 = Organisation | System (seeded CHIRP taxonomy) |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
shield_code_categories
Categories grouping SHIELD codes, each belonging to one layer. Seeded reference data (24 categories).
Keys: PK id. UNIQUE name, UNIQUE code. FK layer → shield_layers(name) ON UPDATE CASCADE (a layer can be renamed from its own row; categories follow).
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| name | text | no | Category name | unique | System (seeded CHIRP taxonomy) |
| layer | text | no | Layer this category sits in | FK, natural key | System (seeded CHIRP taxonomy) |
| code | text | no | Short category identifier (e.g. PPC) | unique | System (seeded CHIRP taxonomy) |
| definition | text | yes | Full definition of the category | - | System (seeded CHIRP taxonomy) |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
shield_codes
The SHIELD causal-factor taxonomy itself - CHIRP's controlled vocabulary of human factors in maritime incidents. Seeded reference data (113 codes in 24 categories under four layers); see Appendix A.
Keys: PK id. UNIQUE code. FK category_id → shield_code_categories(id) ON DELETE SET NULL (an uncategorised code remains valid).
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| category_id | uuid | yes | Grouping category | FK, SET NULL | System (seeded CHIRP taxonomy) |
| code | text | no | Short taxonomy identifier (e.g. PPC2) | unique | System (seeded CHIRP taxonomy) |
| title | text | no | Code title (e.g. "Fatigue") | - | System (seeded CHIRP taxonomy) |
| definition | text | yes | Full definition of the causal factor | - | System (seeded CHIRP taxonomy) |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
narrative_entity_types
Controlled vocabulary of entity types. Seeded reference data (14 types); referenced by natural key from narrative_entities.type, which is ON DELETE RESTRICT - a type in use cannot be removed.
The 14: person, group_of_people, organisation, vessel, equipment, cargo_or_substance, location, event, condition, procedure, document, legislation, time, other. other is the escape hatch - an entity that fits none of the thirteen takes it and records what the extraction would have called it in narrative_entities.proposed_type, so the vocabulary can be grown from what the corpus actually contains rather than guessed at up front.
Keys: PK id. UNIQUE name.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| name | text | no | Type name | unique; 14 seeded values, listed above | System (seeded vocabulary) |
| description | text | yes | What the type covers | - | System (seeded vocabulary) |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
legislation
Legislation register shared across safety issues - one law can back findings in many reports. When this feature ships, an agentic AI attaches relevant legislation to a safety issue, sourced from the report itself or pulled from external legal sources; every attachment needs analyst verification. Not yet implemented - empty in this database.
Keys: PK id.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| name | text | no | Name of the legislation | - | AI-U (AI-attached, requires verification) |
| reference | text | yes | Formal reference | - | AI-U (AI-attached, requires verification) |
| jurisdiction | text | yes | Jurisdiction of the legislation | - | AI-U (AI-attached, requires verification) |
| url | text | yes | Link to the legislation | - | AI-U (AI-attached, requires verification) |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
organisations
Parties that recommendations are directed at, resolved from each recommendation's addressee text as the record pass runs.
A row is not always a company. Reports frequently address a class of people - "All UK Operators of small commercial high speed craft…" - and the same class recurs across reports, so it earns a row of its own; kind says which sort of row it is. kind is guessed from the shape of the name and is an analyst's to correct.
802 rows as delivered: 710 company, 92 class. 71 are curated seeds; the other 731 were minted by resolution. Every contact column is empty - contact_name, email, phone_number, address and website_url are null on all 802. They exist for CHIRP's own follow-up workflow, which has not run; nothing in this delivery populates them.
One company is one row, enforced rather than intended: match_key is name normalised - case, accents, punctuation, a leading "The" and a trailing legal suffix folded away - and it is UNIQUE, so Poseidon Trawlers Ltd, Poseidon Trawlers Limited and The Poseidon Trawlers Ltd. cannot become three rows. Two limits follow. Resolution will not merge a name into one it merely resembles, so genuine near-duplicates that normalise differently survive as separate rows; and it trusts a curated row's spelling without re-parsing it, which is what is_curated marks and why only a person may set it.
Keys: PK id. UNIQUE match_key.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| name | text | no | Organisation or class name | - | AI-U (extracted from the recommendation) |
| match_key | text | no | name normalised for matching | generated always as org_match_key(name), stored; UNIQUE; NOT NULL, so a name that normalises to nothing is rejected | System (derived) |
| kind | organisation_kind | no | Whether the row is a named company or a class of people | enum: company, class; default company | AI-U (extracted from the recommendation) |
| is_curated | boolean | no | True where a person put the row here - a seed or an analyst; false where the record pass minted it. Only a curated row may settle an addressee's spelling without parsing it | default false; NOT NULL | System curated |
| contact_name | text | yes | Named contact at the organisation | empty - null on all 802 rows | AI-U (planned: from text, else web search, else manual) |
| text | yes | Contact email | empty - null on all 802 rows | AI-U (as above) | |
| phone_number | text | yes | Contact phone | empty - null on all 802 rows | AI-U (as above) |
| address | text | yes | Postal address | empty - null on all 802 rows | AI-U (as above) |
| website_url | text | yes | Website | empty - null on all 802 rows | AI-U (as above) |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
organisation_identifiers
Curated identifiers that canonicalise an organisation, one row per identifier - so spelling variants resolve to a single organisation rather than many near-duplicates. Mirrors author_identifiers.
It holds the spellings organisations.match_key cannot reach on its own: acronyms (BPA, UKMPG, OCIMF), predecessor names (the Marine Safety Agency, now the MCA), house styles and outright typos. Only a person adds one - by seeding, by merging two rows, or implicitly by renaming an organisation, which keeps the old spelling here so existing addressees still resolve. The record pass writes none.
The unique index on match_key spans the whole table, not one organisation: one spelling must never point at two organisations.
86 rows as delivered, all seeds. Coverage is therefore only as good as what was seeded - near-duplicate organisations that nobody has spotted remain separate rows.
Keys: PK id. UNIQUE match_key. FK organisation_id → organisations(id) ON DELETE CASCADE.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| organisation_id | uuid | no | Owning organisation | FK, cascade delete | Pipeline |
| identifier | text | no | Curated identifier used to resolve name variants to one canonical organisation | curated | System |
| match_key | text | no | identifier normalised for matching | generated always as org_match_key(identifier), stored; UNIQUE across the table; NOT NULL | System (derived) |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
taxonomy_terms
Shared lookup for every tree-shaped MAIB classification - spreadsheet-sourced, so referenced only from the UK-side tables. One row per term; parent_id links a term to its parent; taxonomy names which tree the term belongs to. Built from the spreadsheet's parallel level-columns (_L1, _L2, …), collapsed into one tree so a query can group by a type without knowing which level each row happens to fill.
Keys: PK id. UNIQUE (id, taxonomy) and (taxonomy, parent_id, label). Composite self-FK (parent_id, taxonomy) → (id, taxonomy) ON UPDATE CASCADE ON DELETE CASCADE - a term's parent must live in the same taxonomy.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| taxonomy | taxonomy_name | no | Which tree the term belongs to | enum: main_event, ship_type, vessel_category, vessel_place_on_board, person_place_on_board, physical_condition, injury_type, deviation | MAIB |
| parent_id | uuid | yes | Parent term; null at the root of a tree | self-FK, same taxonomy | MAIB |
| label | text | no | The term text (e.g. "General Cargo") | - | MAIB |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
410 terms across the eight trees: ship_type (98), vessel_place_on_board (83), person_place_on_board (82), deviation (48), injury_type (44), main_event (32), vessel_category (15), physical_condition (8).
Referenced from: occurrences.main_event_term_id; vessels.vessel_category_term_id, ship_type_term_id, place_on_board_term_id; affected_persons.physical_condition_term_id, injury_type_term_id, deviation_term_id, place_on_board_term_id. Place-on-board is split into two trees - vessel_place_on_board (referenced by vessels) and person_place_on_board (referenced by affected_persons).
ppe_items
Controlled vocabulary of PPE items, split out of the spreadsheet's comma-joined PPE_Used / PPE_Deficient cells. Referenced from affected_persons_ppe. Nine items: life jacket, head protection, hand and foot protection, face and eye protection, anti-exposure suit, protection from falls, working clothes, protective clothing, hearing protection.
Keys: PK id. UNIQUE label.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| label | text | no | PPE item name (e.g. "Life jacket") | unique | MAIB |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
5. Embeddings
The pgvector layer behind SHIELD code suggestion: passages and SHIELD codes encoded under a registered model and compared by cosine distance. No API route exposes any of it - a separate worker container is the only writer of vectors, and the read path is the single function match_shield_codes, documented under passage_shield_codes.
Fully populated as delivered. embedding_models holds the single active encoder (BAAI/bge-large-en-v1.5, 1024 dimensions) and embedding_strategies the four assembly strategies. passage_embeddings carries a vector for every one of the 15,381 passages under that model, shield_code_embeddings all 452 vectors (113 codes × 4 strategies), and passage_embedding_queue is drained to 0 - no passage is awaiting or failing embedding.
Two properties are worth knowing before reading the tables:
- Vectors are hash-addressed, not time-addressed. Every embedding row stores the SHA-256 of the exact text it was computed from, so a vector says what it embedded rather than when. To check whether one is current, compare its
source_text_sha256against the parent's -passages.source_text_sha256on one side,text_sha256(shield_code_embedding_text(...))on the other. Do not readupdated_atas a freshness signal: a superseded vector sits in the table between a text change and the re-run that replaces it, so a hash mismatch means out of date, not corrupt. Writes are guarded (see § Database-enforced behaviour), which prevents a wrong vector, not a stale one. - Model generations coexist.
model_idsits in every primary key here, so changing encoder is a newembedding_modelsrow plus a re-run, never an in-place edit. A unique partial index allows only one row withis_active = true, and that is the row the queue and worker act on.
The embedding columns are the only model output in this section. They are labelled AI-U because an NLP model produces them, but unlike other AI-U columns they assert nothing about an incident and no verification workflow applies - a vector is either current for its text or absent.
embedding_models
Registry of encoders. One row per model; the worker and queue use the single active one. Seeded reference data (1 row: BAAI/bge-large-en-v1.5).
Keys: PK id. UNIQUE name.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| id | uuid | no | Surrogate key | default gen_random_uuid() | System |
| name | text | no | Model identifier as the provider names it | unique; e.g. BAAI/bge-large-en-v1.5 | System (seeded) |
| provider | text | no | Which runtime loads the model | CHECK: sentence_transformers, openai | System (seeded) |
| dimensions | integer | no | Vector width the model emits | CHECK: must be 1024 - the width the columns index | System (seeded) |
| query_prefix | text | no | Instruction string the model wants prepended on the passage side | default ''; bge models require one | System (seeded) |
| is_active | boolean | no | Whether this is the model the queue and worker act on | default false; at most one row may be true (unique partial index) | System (seeded) |
| notes | text | yes | Free-text remarks on the model or its run | - | System |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
embedding_strategies
Which parts of the SHIELD taxonomy a named strategy assembles into the text that gets embedded. One boolean per taxonomy field, tier by tier - so a new combination is an INSERT, not a schema change, and the name-to-fields mapping lives in exactly one place. Seeded reference data (4 strategies).
The database function shield_code_embedding_text(shield_code_id, strategy) reads the row named here and joins the selected fields; the seeder never rebuilds those strings in Python, so the two sides cannot drift apart.
Keys: PK name. Referenced by shield_code_embeddings.strategy.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| name | text | no | Strategy identifier | PK; seeded: full, category_definition, title_definition, definition | System (seeded) |
| includes_layer_name | boolean | no | Include the layer's name (shield_layers.name) | true only for full | System (seeded) |
| includes_layer_definition | boolean | no | Include the layer's definition | true only for full | System (seeded) |
| includes_category_name | boolean | no | Include the category's name | true for full, category_definition | System (seeded) |
| includes_category_definition | boolean | no | Include the category's definition | true for full, category_definition | System (seeded) |
| includes_title | boolean | no | Include the code's title | true for all but definition | System (seeded) |
| includes_definition | boolean | no | Include the code's definition | true for all four | System (seeded) |
| description | text | no | What the strategy is for, and when to reach for it | - | System (seeded) |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System (seeded) |
The four seeded strategies, widest to narrowest: full (every field the taxonomy holds), category_definition (category name and definition grounding the code's title and definition - the shortlist strategy), title_definition (title and definition, no category), definition (the definition alone, where something downstream wants exactly one code).
passage_embeddings
One vector per passage per model. Written only by the embedding worker. 15,381 rows as delivered - one for every passage, under the single active model (13,892 MAIB, 1,489 NTSB).
Keys: PK (passage_id, model_id). FK passage_id → passages(id) ON DELETE CASCADE; FK model_id → embedding_models(id) ON DELETE RESTRICT (a model with vectors cannot be deleted out from under them). HNSW index on embedding (vector_cosine_ops).
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| passage_id | uuid | no | Passage embedded | PK part, FK, cascade delete | System |
| model_id | uuid | no | Model that produced the vector | PK part, FK, restrict delete | System |
| embedding | vector(1024) | no | The passage's vector | dimensions must match the model's | AI-U |
| source_text_sha256 | bytea | no | Hash of the passage text this vector was computed from | trigger-checked against passages.source_text_sha256; mismatch rejected | System |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
shield_code_embeddings
One vector per SHIELD code per model per strategy - so the same code carries several vectors, one for each way of assembling its text. Written only by the seeder. At full coverage this is 113 codes × 4 strategies = 452 rows per model.
452 rows as delivered - full coverage under the single active model.
Keys: PK (shield_code_id, model_id, strategy). FK shield_code_id → shield_codes(id) ON DELETE CASCADE; FK model_id → embedding_models(id) ON DELETE RESTRICT; FK strategy → embedding_strategies(name) ON DELETE RESTRICT. No vector index - the table is small enough to scan.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| shield_code_id | uuid | no | SHIELD code embedded | PK part, FK, cascade delete | System |
| model_id | uuid | no | Model that produced the vector | PK part, FK, restrict delete | System |
| strategy | text | no | Which assembly of taxonomy fields was embedded | PK part, FK to embedding_strategies(name) | System |
| embedding | vector(1024) | no | The strategy string's vector | dimensions must match the model's | AI-U |
| source_text_sha256 | bytea | no | Hash of the assembled strategy string this vector was computed from | trigger-checked against shield_code_embedding_text(...); a taxonomy edit invalidates it | System |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
passage_embedding_queue
Work list of passages awaiting embedding - one row per passage per model. Filled by trigger, not by application code: changing a passage's text enqueues it, and emptying a passage of text deletes its queue rows and its vectors together. A row leaves the queue when its vector is written. Empty as delivered, because every passage has been embedded - a non-zero depth means passage text changed after its vector was computed.
Keys: PK (passage_id, model_id). FK passage_id → passages(id) ON DELETE CASCADE; FK model_id → embedding_models(id) ON DELETE CASCADE.
| Column | Type | Nullable | Description | Valid values / constraints | Source |
|---|---|---|---|---|---|
| passage_id | uuid | no | Passage awaiting embedding | PK part, FK, cascade delete | System |
| model_id | uuid | no | Model the work is queued for | PK part, FK, cascade delete | System |
| target_sha256 | bytea | no | Hash of the text the worker should embed - what the vector must end up naming | - | System |
| attempts | smallint | no | Failed attempts so far; a row past the ceiling stops being handed out and stays for inspection | default 0 | System |
| claim_token | uuid | yes | Lease held by the worker currently processing the row; null when unclaimed | - | System |
| claimed_at | timestamptz | yes | When the lease was taken; an expired lease is reclaimable by another worker | null when unclaimed | System |
| last_error | text | yes | Error text from the most recent failed attempt | - | System |
| created_at / updated_at | timestamptz | yes | Audit timestamps | - | System |
Database-enforced behaviour
Constraints participants should know about when writing to (or reasoning about) the data:
updated_atis trigger-maintained on every table.verified_at/implemented_atare trigger-stamped from their boolean flags and cannot drift: application code writes only the boolean.passage_sentences.sort_orderis auto-assigned by trigger when inserted null.- Triggers validate that
safety_issues.document_idandrecommendations.document_idagree with the document derived from theirpassage_id- falling back topassages.document_idwhen the passage has no sentences attached yet - and that a narrative entity's mentions anchor only to sentences of the entity's own document. passages.document_idis likewise trigger-validated (trg_validate_passage_document): it may not contradict the document derived from the passage's sentences, but a passage with no sentences yet may name its document freely. Deleting a document cascades to its passages.documents.jurisdictionis CHECK-restricted to'UK'or'US'(or null);safety_issues.record_typeto'safety_issue'or'safety_lesson', NOT NULL, defaulting tosafety_issue;organisations.kindis a NOT NULL enum defaulting tocompany.recommendations.reference_numberis indexed but deliberately not unique - re-ingesting a document rewrites its records, so uniqueness would fight regeneration rather than protect anything.tsvectorcolumns (title_tsv,text_tsv,source_text_tsv,name_tsv,action_tsv) are generated columns and cannot be written directly.- Embedding rows are hash-addressed:
trg_validate_passage_embeddingandtrg_validate_shield_code_embeddingreject any vector whosesource_text_sha256disagrees with its parent's text at write time, or whose width disagrees with its model'sdimensions. This stops a wrong vector being written; it does not delete an existing vector when its parent text later changes, so compare hashes rather than timestamps to tell whether one is current. passage_embedding_queueis filled by trigger, never by application code:trg_enqueue_passage_embeddingenqueues a passage whosesource_textchanged, andtrg_clear_passage_embeddingsdeletes a passage's queue rows and vectors together when its text becomes null. Both fire onpassages.source_text, so sentence-level edits reach them through the passage derivation stack.embedding_models.dimensionsis CHECK-restricted to1024(the width the vector columns are declared at) andprovidertosentence_transformersoropenai. A unique partial index (embedding_models_single_active_idx) permits only one row withis_active = true.embedding_modelsdeleted out from under existing vectors is blocked by ON DELETE RESTRICT, as is anembedding_strategiesrow still named by ashield_code_embeddingsrow.organisations.match_keyandorganisation_identifiers.match_keyare generated (org_match_key(...)), stored, NOT NULL and UNIQUE - one company is one row and one spelling points at one organisation, whatever a caller intends.source_idis UNIQUE onoccurrences,vesselsandaffected_persons(and NOT NULL onoccurrences/vessels) - the key MAIB spreadsheet ingestion upserts on (ON CONFLICT (source_id) DO UPDATE), touching only rows whosesource_modified_atshows MAIB revised them.documents.hashis UNIQUE (dedupes re-ingestion). The extraction lease is guarded by a CHECK:claim_expires_atmay be set only whenclaim_started_atis.taxonomy_termsenforces that a term's parent is in the sametaxonomy(composite FK on(parent_id, taxonomy)).documents.occurrence_matched_onis CHECK-restricted topublication_numberormanual, and is required wheneverdocuments.occurrence_idis set;document_particulars.loss_of_lifemust be null or ≥ 0;sentences.page_numbermust be null or ≥ 1.
Appendix A - SHIELD taxonomy
SHIELD is CHIRP's controlled taxonomy of human factors in maritime incidents: 113 factors in 24 categories under four layers. Each factor has a short code (e.g. PPC2), a title (e.g. "Fatigue") and a full definition; categories and layers carry their own codes and definitions too. The taxonomy is seeded reference data - the complete list is reproduced below and lives in the shield_layers, shield_code_categories and shield_codes tables.
SHIELD is presented as an iceberg: the visible acts at the tip, the preconditions that set them up below the waterline, the operational leadership decisions deeper still, and the organisation that shapes all of them at the base. For the model and its rationale, see the published paper: SHIELD Human Factors Taxonomy and Database for Learning from Aviation and Maritime Safety Occurrences (Safety, 2023).
| Layer | Scope | Size |
|---|---|---|
| A · ACTS | Active failures by operators - Perception; Planning and Decision Making; Intentional Deviation; Response Execution; Communicating | 5 categories, 19 codes |
| P · PRECONDITIONS | Conditions that set the stage - Physical Environment; Equipment and Workplace; Interpersonal Communication; Team/Group; Misperception; Awareness; Memory; Mental Workload; Personal Factors; Physiological Condition; Drugs and Nutrition; Competence, Skills and Capability | 12 categories, 62 codes |
| L · OPERATIONAL LEADERSHIP | Leadership decisions affecting safety - Personnel Leadership; Operations Planning; Task Leadership | 3 categories, 15 codes |
| O · ORGANISATION | Decisions, policies or methods adopted at organisational level - Culture; Safety Management; Resources; Economy and Business | 4 categories, 17 codes |
The full taxonomy
All 113 factors, grouped by their four layers and 24 categories. Layers appear in code-prefix order (A, L, O, P); the summary table above gives the taxonomy's own top-to-bottom order.
A · ACTS
Communicating
| Code | Title | Definition |
|---|---|---|
| AC1 | Incorrect/unclear transmission of information | The operator transmits to other actors information which is incorrect or unclear, e.g. use of wrong callsign. |
| AC2 | No transmission of information | The operator does not transmit information which is necessary for other actors to operate safely / effectively. |
Intentional Deviation
| Code | Title | Definition |
|---|---|---|
| AI1 | Workaround in normal conditions | The operator decides to intentionally deviate from an agreed procedure or practice in a normal operating condition. |
| AI2 | Routine workaround | The operator habitually and intentionally deviates from an agreed procedure or practice on a regular basis. |
| AI3 | Workaround in exceptional conditions | The operator decides to intentionally deviate from an agreed procedure or practice in an exceptional operating condition. |
| AI4 | Sabotage | The operator decides to intentionally deviate from an agreed procedure or practice in order to create damage to the system or organisation. |
Perception
| Code | Title | Definition |
|---|---|---|
| AP1 | No/wrong/late visual detection | The operator does not detect (or detects too late or inaccurately) a visual signal necessary to formulate a proper action plan or make a correct decision. |
| AP2 | No/wrong/late auditory detection | The operator does not detect (or detects too late or inaccurately) an auditory signal necessary to formulate a proper action plan or make a correct decision. |
| AP3 | No/wrong/late kinaesthetic detection | The operator does not detect (or detects too late or inaccurately) a kinaesthetic signal necessary to formulate a proper action plan or make a correct decision. |
| AP4 | No/wrong/late with other senses (e.g. smell, temperature) | The operator does not perceive (or perceives too late or inaccurately) information received with senses different from vision, hearing and touch, which is necessary to formulate a proper action plan or make a correct decision. |
Planning and Decision Making
| Code | Title | Definition |
|---|---|---|
| AD1 | Incorrect decision or plan | The operator fails to elaborate an action plan or make a decision which is insufficient to manage the situation. |
| AD2 | Late decision or plan | The operator fails to elaborate a timely action plan or decision to manage the situation. |
| AD3 | No decision or plan | The operator fails to elaborate any action plan or decision to manage the situation. |
Response Execution
| Code | Title | Definition |
|---|---|---|
| AR1 | Timing error | The operator has planned to take an action which is appropriate for the situation, but executes it either too early or too late. |
| AR2 | Sequence error | The operator carries out a series of actions in the wrong sequence. |
| AR3 | Right action on the wrong object | The operator has planned to take an action which is appropriate for the perceived situation but - when executing it - selects an object (e.g. lever, knob, button, HMI element) different from the intended one. |
| AR4 | Wrong action on the right object | The operator selects the correct object (e.g. lever, knob, button, HMI element), but performs an action that is not the correct one. |
| AR5 | Lack of physical coordination | The operator takes an action which is appropriate for the perceived situation, but executes it in a wrong manner, due to lack of physical coordination. |
| AR6 | No action executed | The operator has planned to take an action which is appropriate for the perceived situation, but does not execute it. |
L · OPERATIONAL LEADERSHIP
Operations Planning
| Code | Title | Definition |
|---|---|---|
| LO1 | Inadequate risk assessment | Operations leader does not adequately evaluate the risks associated with a specific operation or scenario. |
| LO2 | Inadequate crew or team makeup or composition | Operations leader allows inadequate makeup or composition of crew or team for a given activity/operation. |
| LO3 | Inappropriate pressure to perform a task | Inappropriate pressure by operational leader causes an operator to feel pressured to perform a task or activity. |
| LO4 | Directed task with inadequate qualification or currency | Operations leader allows an operator to perform a task, while the qualification or currency of the operator are not in line with the training requirements for the associated activity. |
| LO5 | Directed task with limited experience | Operations leader allows operators to perform a task, while their experience for a specific operation, event or scenario is too limited. |
| LO6 | Directed task with inadequate equipment | Operations leader directs operators to undertake an activity that is beyond the capabilities of their equipment. |
Personnel Leadership
| Code | Title | Definition |
|---|---|---|
| LP1 | No personnel measures against regular risky behaviour | An operations leader does not identify an operator who regularly exhibits risky behaviours or does not institute the necessary remedial action. |
| LP2 | Inappropriate behaviour affects learning | Inappropriate behaviour of operations leader affects learning by operators, which manifests itself in actions that are either inappropriate to their skill level or violate standard procedures. |
| LP3 | Personality conflict | A personality conflict exists between an operations leader and an operator. |
| LP4 | Lack of feedback on safety reporting | Operations leader does not provide feedback to operator following his/her provision of information on a potential safety issue. |
Task Leadership
| Code | Title | Definition |
|---|---|---|
| LT1 | Inadequate leadership or supervision | Availability, competency, quality or timeliness of leadership or supervision does not meet task demands. |
| LT2 | No correction of unsafe practices | An operations leader does not correct known unsafe practices, conditions, guidance or procedure, which allows hazardous practices within the scope of his / her authority. |
| LT3 | No enforcement of existing rules | Operations leader does not enforce organisational and operating rules. |
| LT4 | Allowing unwritten policies to become standard | Operations leader allows policies that have not been formally recognized by the organisation to be perceived as standards. |
| LT5 | Directed deviation | Operations leader directs a subordinate to deviate from existing regulations, procedures or technical guidance. |
O · ORGANISATION
Culture
| Code | Title | Definition |
|---|---|---|
| OC1 | Safety culture | There is a poor safety culture or a low company morale in the organisation, leading to a poor attitude to safety in the organisation. |
| OC2 | Multi-cultural factors | Sociocultural barriers and conflicts cause misunderstandings between personnel and an unsafe working environment. |
Economy and Business
| Code | Title | Definition |
|---|---|---|
| OE1 | Contractors | Relationships, communications, or inter-operability between the organisation and contractors are not optimal and this creates an unsafe working relationship. |
| OE2 | External business environment | Strong competition, poor relationships with other organisations (other than contractors) and changes in the market put pressure on the organisation which has a negative impact on safety. |
| OE3 | Economic pressure | Economic conditions and pressures to keep schedule and costs put a pressure on the organisation resulting in a negative impact on safety. |
| OE4 | Tempo of operations | The speed with which an organisation is expected to operate leaves little space for anything else than getting the job done, thus reducing safety. |
Resources
| Code | Title | Definition |
|---|---|---|
| OR1 | Personnel | The organisation provides insufficient personnel who are suitably qualified and experienced to perform the tasks safely. |
| OR2 | Budgets | Budget is insufficient to allow the operation to proceed with adequate resources to operate safely. |
| OR3 | Equipment/parts/materials availability | Equipment, materials, or parts that operators have to use is not available to them at the time that the task has to be performed. |
| OR4 | Inadequate training program | There are inadequate or unavailable training programs (either one-time, recurrent, upgrade, transition or any other local training program). |
| OR5 | Design of equipment or procedures | The design of the equipment / procedure to support the operation / task is not suitable to allow the task to be performed safely. The equipment might also prevent operators working in an efficient manner and indirectly encourage short-cuts or workarounds. |
| OR6 | Operational information | Operational information necessary to perform the task safely, or meet the requirements of a safe operation is not available at the relevant points in the organisation. This information includes weather briefings/forecasts, intelligence, operational planning material or other information necessary for safe operations. |
Safety Management
| Code | Title | Definition |
|---|---|---|
| OS1 | Organisation structure/policy | Management commitment and responsibilities are not clear or insufficient. It is not clear who is accountable for safety issues. The structure of the organisation is not clear about key safety personnel. Safety plans including emergency response planning are not coordinated or unavailable. |
| OS2 | Safety risk management (proactive) | Identification of safety hazards and risk assessments and mitigation are not carried out, or are carried out but do not identify and make available relevant safety issues. |
| OS3 | Safety risk assurance (reactive) | Safety risk assurance is ineffective in identifying and repairing safety shortfalls. Safety risk assurance includes performance monitoring (inspections, review of tasks), testing, and incident reporting. |
| OS4 | Safety promotion | Safety promotion (education, communication) does not sufficiently reinforce the importance of safety. |
| OS5 | Publications/procedures/written guidance | Publications and written documentation are not sufficient to ensure that a task can be carried out safely. Procedures may exist, but are poor and cannot be used to perform the task they support. Even where good procedures exist they do not account for all the operational circumstances within which they are expected to be applied (work as imagined versus work as done). |
P · PRECONDITIONS
Awareness
| Code | Title | Definition |
|---|---|---|
| PAW1 | Channelized attention | Operator focuses all attention on a limited number of cues and excludes other cues of an equal or higher priority. This includes tunnel vision. |
| PAW2 | Confusion | Operator does not maintain a cohesive awareness of events and required actions, and this leads to confusion. |
| PAW3 | Distraction | Interruption and/or inappropriate redirection of operator's attention. |
| PAW4 | Inattention | Operator is not alert / ready to process immediately available information. |
| PAW5 | Geographically lost | The operator perceives to be at a different location compared to the one where s/he actually is. |
| PAW6 | Unsuitable mental model | Operator uses an unsuitable mental model to integrate information and arrives at a wrong understanding of the situation (e.g. wrong understanding of automation behaviour). |
| PAW7 | Pre-conceived notion or expectancy | Prevalence of expectations on a certain course of action regardless of other cues degrades operator performance. |
Competence, Skills and Capability
| Code | Title | Definition |
|---|---|---|
| PCS1 | Inadequate experience | Operator does not have sufficient experience with a task at hand. |
| PCS2 | Lack of proficiency | Operator capability to accomplish a task does not meet the performance levels expected from her/his skill level. |
| PCS3 | Inadequate training or currency | Operator does not meet general training or recurring training requirements for the task assigned to her/him. |
| PCS4 | Body size, strength or coordination limitations | Body size, strength, dexterity, coordination mobility or other biomechanical limitations of the operator degrade the task performance of the operator. |
Drugs and Nutrition
| Code | Title | Definition |
|---|---|---|
| PDN1 | Recreational drugs and alcohol | Recreational use of drugs or alcohol impairs or interferes with operator performance. |
| PDN2 | Prescribed drugs or OTC medications | Use of prescribed drugs or over-the-counter medications or supplements interferes with operator task performance. |
| PDN3 | Inadequate nutrition, hydration or dietary practice | Inadequate nutritional state, hydration or dietary practice degrade operator performance. |
Equipment and Workplace
| Code | Title | Definition |
|---|---|---|
| PEW1 | Ergonomics and human machine interface issues | The ergonomics of tools and human machines interface, including controls, switches, displays, warning systems and symbology degrade the operator's performance. |
| PEW2 | Technology creates an unsafe situation | The design, malfunction, failure, symbology, logic or other aspects of technical/automated systems degrade the operator's performance. |
| PEW3 | Workspace or working position incompatible with operation | The interior or exterior workspace layout or the working position are incompatible with the task requirements of the operator. |
| PEW4 | Personal protective equipment interference | Personal protection equipment (helmets, suits, gloves, etc.) interferes with normal duties of the operator. |
| PEW5 | Communication equipment inadequate | Communication equipment is inadequate or unavailable to support task demands. Communications can be voice, data or multi-sensory. |
| PEW6 | Fuels or materials | Working with fuels, chemicals or materials degrades the operator's performance. |
Interpersonal Communication
| Code | Title | Definition |
|---|---|---|
| PCO1 | Briefing or handover inadequate | Individual or team has not well completed the briefing or handover of an activity. |
| PCO2 | Inadequate communication due to team members' rank or position | Differences in rank or position within team limit or jeopardize team communication. |
| PCO3 | Language difficulties | Communication between operators with different native languages degrades operator's performance. |
| PCO4 | Non-standard or complex communication | Operators use terminology / phrases or hand signals differing from standards and training, or they use complex messages that may lead to misunderstanding. |
Memory
| Code | Title | Definition |
|---|---|---|
| PME1 | Forget actions/intentions | The operator has a temporary memory lapse and forgets planned actions or intentions. |
| PME2 | No/inaccurate recall of information | The operator does not retrieve or recall information accurately from memory. |
| PME3 | Negative habit | The operator uses a highly learned behaviour from past experiences which is inadequate to handle the present situation. |
Mental Workload
| Code | Title | Definition |
|---|---|---|
| PMW1 | High workload | High workload degrades the operator's performance. |
| PMW2 | Low workload | Low workload imposes idleness and waiting, and it degrades operator's performance. |
| PMW3 | Information processing overload | Available mental resources are insufficient to process the amount of complex information. |
| PMW4 | Startle effect | Available mental resources are insufficient to process sudden, high-intensity, unexpected information, leading to the startle effect. |
Misperception
| Code | Title | Definition |
|---|---|---|
| PER1 | Motion illusion | Erroneous physical sensation of orientation, motion or acceleration by the operator. |
| PER2 | Visual illusion | Erroneous perception of orientation, motion or acceleration following visual stimuli. |
| PER3 | Misperception of changing environment | Misperceived or misjudged altitude, separation, speed, closure rate, road/sea conditions, aircraft/vehicle location within the performance envelope. |
| PER4 | Misinterpreted or misread instrument | Misread, misinterpreted or not recognized significance of correct instrument reading. |
Personal Factors
| Code | Title | Definition |
|---|---|---|
| PPF1 | Emotional state | Strong positive or negative emotion degrade operator's performance. |
| PPF2 | Personality style | Operator's personality traits (e.g. authoritarian, over-conservative, impulsive, invulnerable, submissive) contribute to degraded performance and/or negatively affect interaction with other team members. |
| PPF3 | Confidence level | Inadequate estimation by the operator of personal capability, of the capability of others or the capability of equipment degrades performance. |
| PPF4 | Performance/peer pressure | Threat to self-image and feeling of pressure to perform an activity despite concerns about the risk associated degrades performance. |
| PPF5 | Motivation | Excessive or inadequate motivation to accomplish a task or prevalence of personal goals over organisation's goals degrades operator performance. |
| PPF6 | Pre-existing psychological condition | Pre-existing acknowledged personality, psychological or psychosocial disorder/problem degrades operator performance. |
| PPF7 | Risk underestimation | False sense of safety or complacency brings the operator to ignore hazards or to underestimate the risks associated to them, thus degrading performance. |
Physical Environment
| Code | Title | Definition |
|---|---|---|
| PPE1 | Vision affected by environment | Environmental conditions affect the operator's vision. |
| PPE2 | Operator movement affected by environment | Environmental conditions affect the operator's movement. |
| PPE3 | Hearing affected by environment | Environmental conditions affect the operator's ability to hear. |
| PPE4 | Mental processing affected by environment | Environmental conditions affect the operator's ability to mentally process or think about information. |
| PPE5 | Heat or cold stress | Exposure to heat or cold degrades the operator's performance capabilities. |
| PPE6 | Operation more difficult due to weather and environment | Weather conditions and/or environment make controlling the operation more difficult. |
| PPE7 | Acceleration | Forces acting on a body by acceleration or gravity degrade the operator's performance capabilities. |
| PPE8 | Vibration stress | Excessive exposure to vibration degrades operator physical or cognitive performance. |
| PPE9 | Long term isolation | An individual is subject to long term isolation and this degrades operator performance. |
Physiological Condition
| Code | Title | Definition |
|---|---|---|
| PPC1 | Injury or illness existed during operation | Pre-existing physical illness, injury, deficit or diminished physical capability due to the injury, illness or deficit, degrades operator performance. This includes situations where the operator intentionally performs duties with a known (disqualifying) medical condition. |
| PPC2 | Fatigue | Diminished mental capability due to fatigue, restricted or shortened sleep, mental activity during prolonged wakefulness or disturbance of circadian rhythm degrades operator performance. |
| PPC3 | Mentally exhausted (burnout) | Exhaustion associated with the wearing effects of high operational and/or lifestyle tempo, in which operational requirements impinge on the ability to satisfy personal requirements degrades operator performance. |
| PPC4 | Hypoxia | Insufficient oxygen supply to the body impairs operator performance. |
| PPC5 | Decompression sickness | Development of nitrogen bubbles in the blood and tissues as a result of too quick reduction of atmospheric pressure causes operator chest pains, bends, difficult breathing, skin irritation, cramps. |
Team/Group
| Code | Title | Definition |
|---|---|---|
| PTG1 | Team working towards different goals | Team members are working towards different goals, leading to loss of shared situational awareness and not anticipating potential problems. |
| PTG2 | No cross-check and speaking-up by team member | Decisions or actions by team members are not crosschecked. There is no multi-person approach to execution of critical tasks / procedures and no communication of concerns, not necessarily due to rank or position. |
| PTG3 | No monitoring and speaking-up of team status and functioning | Team members do not monitor the status and functioning of each other for signs of stress, fatigue, complacency, and task saturation, or do not speak up if status and functioning are monitored as being incorrect. |
| PTG4 | No adaptation of team performance in demanding situation | Team does not adapt to demanding circumstances. There is no change in team roles and task allocation, there is no team priorisation of plans and tasks, no workload management, and no management of interruptions and distractions. |
| PTG5 | Long term team confinement | Long term close proximity or confinement degrades team functioning and operator performance. |
| PTG6 | Group think | Excessive tendency towards group, team or organisation consensus and conformity over independent critical analysis and decision making degrades operator performance. |