mirror of
https://github.com/andre482/O-tie.git
synced 2026-07-22 07:44:11 +00:00
308 lines
9.5 KiB
JavaScript
308 lines
9.5 KiB
JavaScript
|
|
// Regenerates the committed example .bowtie files in the current schema.
|
||
|
|
//
|
||
|
|
// Authoring the scenarios through the real model guarantees the files validate
|
||
|
|
// and stay in canonical form. Run: npm run examples
|
||
|
|
|
||
|
|
import { writeFileSync } from "node:fs";
|
||
|
|
import { dirname, join } from "node:path";
|
||
|
|
import { fileURLToPath } from "node:url";
|
||
|
|
import { loadEngine } from "./engine.mjs";
|
||
|
|
|
||
|
|
const scriptsDir = dirname(fileURLToPath(import.meta.url));
|
||
|
|
const examplesDir = join(scriptsDir, "..", "examples");
|
||
|
|
const FIXED_TS = "2026-06-12T00:00:00.000Z";
|
||
|
|
|
||
|
|
const engine = await loadEngine();
|
||
|
|
const model = engine.model;
|
||
|
|
|
||
|
|
function fieldColor(fieldKey, label) {
|
||
|
|
const field = model.BARRIER_STACK_FIELDS.find((f) => f.key === fieldKey);
|
||
|
|
return field?.options.find((o) => o.label === label)?.color;
|
||
|
|
}
|
||
|
|
|
||
|
|
function barrier(label, opts = {}) {
|
||
|
|
const b = model.createBarrier(label);
|
||
|
|
if (opts.notes) b.notes = opts.notes;
|
||
|
|
if (opts.stack) {
|
||
|
|
b.stack = opts.stack.map(([field, value]) => {
|
||
|
|
const color = fieldColor(field, value);
|
||
|
|
if (color === undefined) throw new Error(`Unknown ${field} option: ${value}`);
|
||
|
|
return model.createBarrierStackItem(value, field, color);
|
||
|
|
});
|
||
|
|
model.sortBarrierStack(b.stack);
|
||
|
|
b.stackCollapsed = opts.expanded !== true;
|
||
|
|
}
|
||
|
|
if (opts.chains) {
|
||
|
|
b.degradationChains = opts.chains.map((c) => {
|
||
|
|
const chain = model.createDegradationChain(c.factor);
|
||
|
|
chain.safeguards = (c.safeguards ?? []).map((s) => model.createEscalationNode(s));
|
||
|
|
return chain;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return b;
|
||
|
|
}
|
||
|
|
|
||
|
|
function threat(label, notes, barriers) {
|
||
|
|
const t = model.createThreat(label);
|
||
|
|
if (notes) t.notes = notes;
|
||
|
|
t.preventionBarriers = barriers;
|
||
|
|
return t;
|
||
|
|
}
|
||
|
|
|
||
|
|
function consequence(label, notes, barriers) {
|
||
|
|
const c = model.createConsequence(label);
|
||
|
|
if (notes) c.notes = notes;
|
||
|
|
c.mitigationBarriers = barriers;
|
||
|
|
return c;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Stable, readable ids so regeneration produces minimal diffs.
|
||
|
|
function reassignIds(bowtie) {
|
||
|
|
let n = 0;
|
||
|
|
const id = (prefix) => `${prefix}-${n++}`;
|
||
|
|
bowtie.id = id("bowtie");
|
||
|
|
for (const event of bowtie.events) {
|
||
|
|
event.id = id("event");
|
||
|
|
event.transitionBarriers.forEach(reassignBarrier(id));
|
||
|
|
}
|
||
|
|
for (const t of bowtie.threats) {
|
||
|
|
t.id = id("threat");
|
||
|
|
t.preventionBarriers.forEach(reassignBarrier(id));
|
||
|
|
}
|
||
|
|
for (const c of bowtie.consequences) {
|
||
|
|
c.id = id("consequence");
|
||
|
|
c.mitigationBarriers.forEach(reassignBarrier(id));
|
||
|
|
}
|
||
|
|
bowtie.createdAt = FIXED_TS;
|
||
|
|
bowtie.updatedAt = FIXED_TS;
|
||
|
|
return bowtie;
|
||
|
|
|
||
|
|
function reassignBarrier(idFn) {
|
||
|
|
return (b) => {
|
||
|
|
b.id = idFn("barrier");
|
||
|
|
for (const item of b.stack ?? []) item.id = idFn("stack");
|
||
|
|
for (const chain of b.degradationChains) {
|
||
|
|
chain.id = idFn("chain");
|
||
|
|
for (const sg of chain.safeguards) sg.id = idFn("safeguard");
|
||
|
|
chain.degradationFactor.id = idFn("degradation");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildAmmonia() {
|
||
|
|
const bt = model.createBowtie("Cold storage ammonia refrigeration");
|
||
|
|
bt.events[0].label = "Loss of primary containment from ammonia refrigeration circuit";
|
||
|
|
bt.events[0].hazard = "Pressurised anhydrous ammonia (NH3) refrigeration inventory";
|
||
|
|
|
||
|
|
bt.threats = [
|
||
|
|
threat(
|
||
|
|
"Corrosion or fatigue of piping, vessels, or evaporator coils",
|
||
|
|
"Credible in aged plant with cyclic thermal loading, condensate, or corrosion under insulation.",
|
||
|
|
[
|
||
|
|
barrier("Mechanical integrity inspection program", {
|
||
|
|
notes: "NDT, thickness monitoring, and replacement intervals per IIAR-6 / company standards.",
|
||
|
|
expanded: true,
|
||
|
|
stack: [
|
||
|
|
["type", "Active Hardware"],
|
||
|
|
["effectiveness", "Good"],
|
||
|
|
["criticality", "Very High"],
|
||
|
|
["responsible", "Tech Department"],
|
||
|
|
["validation", "Inspection"],
|
||
|
|
["status", "Available"],
|
||
|
|
],
|
||
|
|
chains: [
|
||
|
|
{
|
||
|
|
factor: "Deferred inspection due to production pressure",
|
||
|
|
safeguards: ["QA audit of mechanical integrity backlog"],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
}),
|
||
|
|
barrier("Materials of construction and corrosion allowance", {
|
||
|
|
notes: "Compatible metallurgy, coatings, and design margin for operating conditions.",
|
||
|
|
stack: [
|
||
|
|
["type", "Passive Hardware"],
|
||
|
|
["effectiveness", "Good"],
|
||
|
|
["criticality", "High"],
|
||
|
|
["status", "Available"],
|
||
|
|
],
|
||
|
|
}),
|
||
|
|
]
|
||
|
|
),
|
||
|
|
threat(
|
||
|
|
"Forklift or vehicle impact on exposed ammonia piping",
|
||
|
|
"Common in warehouse aisles adjacent to machine rooms or overhead pipe runs.",
|
||
|
|
[
|
||
|
|
barrier("Physical protection and traffic segregation", {
|
||
|
|
notes: "Bollards, rack guards, marked routes, and restricted machine-room access.",
|
||
|
|
stack: [
|
||
|
|
["type", "Passive Hardware"],
|
||
|
|
["effectiveness", "Good"],
|
||
|
|
["criticality", "High"],
|
||
|
|
["status", "Available"],
|
||
|
|
],
|
||
|
|
chains: [
|
||
|
|
{
|
||
|
|
factor: "Bollards removed or bypassed for convenience",
|
||
|
|
safeguards: ["Supervisor housekeeping and barrier inspection rounds"],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
}),
|
||
|
|
]
|
||
|
|
),
|
||
|
|
threat("Overpressure in low-side or high-side refrigeration circuit", undefined, [
|
||
|
|
barrier("Pressure relief devices and high-pressure interlocks", {
|
||
|
|
notes: "PSVs discharge to a safe location; alarm and trip on abnormal pressure.",
|
||
|
|
stack: [
|
||
|
|
["type", "Active Hardware"],
|
||
|
|
["effectiveness", "Good"],
|
||
|
|
["criticality", "Very High"],
|
||
|
|
["status", "Available"],
|
||
|
|
],
|
||
|
|
chains: [
|
||
|
|
{
|
||
|
|
factor: "Relief device isolated, blocked, or inspection overdue",
|
||
|
|
safeguards: ["PSV inspection and register management program"],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
}),
|
||
|
|
]),
|
||
|
|
threat("Maintenance isolation or re-commissioning error", undefined, [
|
||
|
|
barrier("Permit-to-work and LOTO procedures", {
|
||
|
|
notes: "Verified isolation, zero-energy confirmation, and ammonia-specific competency.",
|
||
|
|
stack: [
|
||
|
|
["type", "Active Human"],
|
||
|
|
["effectiveness", "Good"],
|
||
|
|
["criticality", "Very High"],
|
||
|
|
["status", "Available"],
|
||
|
|
],
|
||
|
|
chains: [
|
||
|
|
{
|
||
|
|
factor: "Technician competency not verified for ammonia work",
|
||
|
|
safeguards: ["Competency assessment and authorization records"],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
}),
|
||
|
|
]),
|
||
|
|
threat("Operator error during startup, shutdown, or abnormal operation", undefined, [
|
||
|
|
barrier("Operating procedures and abnormal situation guidance", {
|
||
|
|
notes: "Written procedures for charge management, alarms, and emergency slowdown.",
|
||
|
|
stack: [
|
||
|
|
["type", "Active Human"],
|
||
|
|
["effectiveness", "Good"],
|
||
|
|
["criticality", "High"],
|
||
|
|
["status", "Available"],
|
||
|
|
],
|
||
|
|
}),
|
||
|
|
barrier("Operator training and competency assurance", {
|
||
|
|
notes: "Refresher training on ammonia properties, alarms, and first actions.",
|
||
|
|
stack: [
|
||
|
|
["type", "Active Human"],
|
||
|
|
["effectiveness", "Fair"],
|
||
|
|
["criticality", "Medium"],
|
||
|
|
["status", "Available"],
|
||
|
|
],
|
||
|
|
}),
|
||
|
|
]),
|
||
|
|
];
|
||
|
|
|
||
|
|
bt.consequences = [
|
||
|
|
consequence("Personnel harm due to toxic ammonia exposure", undefined, [
|
||
|
|
barrier("Fixed ammonia gas detection and alarm", {
|
||
|
|
notes: "Audible/visual alarm, with potential automatic isolation or ventilation actuation.",
|
||
|
|
expanded: true,
|
||
|
|
stack: [
|
||
|
|
["type", "Active Hardware"],
|
||
|
|
["effectiveness", "Good"],
|
||
|
|
["criticality", "Very High"],
|
||
|
|
["responsible", "HSE Department"],
|
||
|
|
["validation", "Barrier Test"],
|
||
|
|
["status", "Available"],
|
||
|
|
],
|
||
|
|
chains: [
|
||
|
|
{
|
||
|
|
factor: "Sensor calibration overdue or drift",
|
||
|
|
safeguards: ["Calibration management and functional test program"],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
}),
|
||
|
|
barrier("Emergency ventilation and isolation valves", {
|
||
|
|
notes: "Reduce airborne concentration; close isolation valves where safe to do so.",
|
||
|
|
stack: [
|
||
|
|
["type", "Active Hardware"],
|
||
|
|
["effectiveness", "Good"],
|
||
|
|
["criticality", "Very High"],
|
||
|
|
["status", "Available"],
|
||
|
|
],
|
||
|
|
}),
|
||
|
|
]),
|
||
|
|
consequence("Environmental damage due to ammonia release", undefined, [
|
||
|
|
barrier("Containment sump and emergency dilution / knock-down", {
|
||
|
|
notes: "Limit spread to drains, waterways, or off-site receptors.",
|
||
|
|
stack: [
|
||
|
|
["type", "Passive Hardware"],
|
||
|
|
["effectiveness", "Fair"],
|
||
|
|
["criticality", "High"],
|
||
|
|
["status", "Degraded"],
|
||
|
|
],
|
||
|
|
}),
|
||
|
|
]),
|
||
|
|
consequence("Operational loss due to cold-chain interruption", undefined, [
|
||
|
|
barrier("Emergency response plan and qualified contractor standby", {
|
||
|
|
notes: "Ammonia-qualified emergency repair and product salvage / transfer plan.",
|
||
|
|
stack: [
|
||
|
|
["type", "Active Human"],
|
||
|
|
["effectiveness", "Good"],
|
||
|
|
["criticality", "High"],
|
||
|
|
["responsible", "Management"],
|
||
|
|
["status", "Available"],
|
||
|
|
],
|
||
|
|
chains: [
|
||
|
|
{
|
||
|
|
factor: "Contractor availability or response time not assured",
|
||
|
|
safeguards: ["Pre-qualified contractor agreement and annual drill"],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
}),
|
||
|
|
]),
|
||
|
|
];
|
||
|
|
|
||
|
|
return reassignIds(bt);
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildSteamcracker() {
|
||
|
|
const bt = model.createBowtie("Defective steamcracker");
|
||
|
|
bt.events[0].label = "Loss of containment in steamcracker furnace";
|
||
|
|
bt.events[0].hazard = "High-pressure ethylene cracking system";
|
||
|
|
|
||
|
|
bt.threats = [
|
||
|
|
threat("Tube corrosion / fatigue", undefined, [
|
||
|
|
barrier("Periodic tube inspection program"),
|
||
|
|
barrier("Material selection standards"),
|
||
|
|
]),
|
||
|
|
threat("Operator error during startup", undefined, [
|
||
|
|
barrier("Standard operating procedures"),
|
||
|
|
]),
|
||
|
|
];
|
||
|
|
bt.consequences = [
|
||
|
|
consequence("Fire / explosion", undefined, [
|
||
|
|
barrier("Deluge and fire suppression systems"),
|
||
|
|
]),
|
||
|
|
consequence("Toxic gas release", undefined, [
|
||
|
|
barrier("Gas detection and isolation"),
|
||
|
|
barrier("Emergency response plan"),
|
||
|
|
]),
|
||
|
|
];
|
||
|
|
|
||
|
|
return reassignIds(bt);
|
||
|
|
}
|
||
|
|
|
||
|
|
function write(name, bowtie) {
|
||
|
|
const file = join(examplesDir, name);
|
||
|
|
writeFileSync(file, `${model.serializeBowtie(bowtie)}\n`);
|
||
|
|
console.log(`Wrote ${file}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
write("cold-storage-ammonia.bowtie", buildAmmonia());
|
||
|
|
write("steamcracker.bowtie", buildSteamcracker());
|