Seen as a design The generated document itself — its toggles and disclosures work here
Seen as a design The generated document itself — its toggles and disclosures work here
Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.
Everything in this panel, including the contents behind each URL, in one Markdown file — Large, so it goes as a file (copy is for 32 KB and under)
Build an HTML document with "Bow-tie Risk Analysis". This layout runs, so there is no composition to rebuild — what it needs is content. ## What this layout does Map threats and preventive barriers leading to a top event, then consequences and mitigative barriers after it. ## What you have to supply - Define one hazard and top event with two to six threats and consequences. - Assign one to three status-coded preventive barriers per threat and mitigative barriers per consequence. ## The shape of the input - `title` — string — リスクと対応を伝える見出し - `subtitle` — string? — 対象・評価時点・前提 - `hazard` — string — 損失につながり得る危険源または活動 - `topEvent` — string — 危険源の管理が失われた中心事象 - `threats` — array (id, label, preventiveBarriers) — 脅威2〜6件。各1〜3障壁 - `consequences` — array (id, label, severity, mitigativeBarriers) — 結果2〜6件。各1〜3障壁 schema.ts holds the machine-checkable validation. Ask rather than guess when an input is missing. ## When not to use it - Use a risk matrix when the task is only scoring likelihood and impact or ranking risks. - Do not use for multiple top events or general process sequences; this layout has one central event. ## How to build it Put the input in a JSON file and run: ```bash node scripts/render.mjs bowtie-risk-analysis <input.json> --theme ink -o out.html ``` The output is one self-contained file and fetches nothing at render time. ## The source - https://design-vault.pages.dev/designs/bowtie-risk-analysis/source/template.tsx - https://design-vault.pages.dev/designs/bowtie-risk-analysis/source/schema.ts - https://design-vault.pages.dev/designs/bowtie-risk-analysis/source/sample-data.json - https://design-vault.pages.dev/designs/bowtie-risk-analysis/sample.html
Template, input schema, sample data and metadata. This document was generated from those four. The generated document
/designs/bowtie-risk-analysis/source/template.tsx
import type { Props } from "./schema";
const CSS = String.raw`
.asset{font-family:var(--font-sans);color:var(--color-on-surface);background:var(--color-surface);margin:0;line-height:1.5}
.asset .diagram{display:block;width:1280px;height:720px;max-width:100%;margin:0 auto}
.asset .mobile{display:none;max-width:760px;margin:0 auto;padding:1.25rem 1rem}
.asset .mobile h1{font-size:1.5rem;line-height:1.35;overflow-wrap:anywhere}.asset .muted{color:var(--color-secondary)}
.asset details{max-width:1120px;margin:1rem auto;padding:.75rem 1rem;border-top:1px solid var(--color-outline)}
.asset summary{cursor:pointer;min-height:2.75rem;padding:.5rem 0;color:var(--color-secondary)}
.asset table{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums}.asset th,.asset td{text-align:left;vertical-align:top;padding:.65rem .75rem;border-bottom:1px solid var(--color-outline);overflow-wrap:anywhere}.asset th{color:var(--color-secondary)}
.asset ul{margin:.25rem 0;padding-left:1.25rem}.asset li+li{margin-top:.25rem}.asset .record-id{font:600 .8rem var(--font-mono);color:var(--color-secondary)}
@media(max-width:640px){.asset .diagram{display:none!important}.asset .mobile{display:block}.asset details{margin:.5rem 1rem;padding:.75rem 0}.asset table,.asset thead,.asset tbody,.asset tr,.asset td{display:block}.asset thead{display:none}.asset tr{padding:.6rem 0;border-bottom:1px solid var(--color-outline)}.asset td{display:grid;grid-template-columns:8rem minmax(0,1fr);gap:.75rem;border:0;padding:.2rem 0}.asset td:before{content:attr(data-label);font-size:.75rem;color:var(--color-secondary)}}
@media(min-width:641px){.asset .mobile{display:none!important}}
`;
const wrap = (value: string, size: number) => {
const chars = [...value];
const lines: string[] = [];
for (let i = 0; i < chars.length; i += size) lines.push(chars.slice(i, i + size).join(""));
return lines.length ? lines : [""];
};
const statusLabel: Record<string, string> = { effective: "有効", impaired: "低下", missing: "欠落" };
const severityLabel: Record<string, string> = { moderate: "中", major: "大", severe: "重大" };
function barrierSummary(items: Props["threats"][number]["preventiveBarriers"]) {
const counts = { effective: 0, impaired: 0, missing: 0 };
for (const item of items) counts[item.status] += 1;
return counts;
}
export default function Template(d: Props) {
const titleLines = wrap(d.title, 64);
const subtitleLines = wrap(d.subtitle ?? "", 100);
const lastTitleY = 78 + (titleLines.length - 1) * 26;
const lastSubtitleY = subtitleLines.length ? lastTitleY + 24 + (subtitleLines.length - 1) * 18 : lastTitleY;
const hazardY = lastSubtitleY + 24;
const columnY = hazardY + 34;
const firstRowY = columnY + 36;
const threatY = (index: number) => firstRowY + index * 62;
const consequenceY = (index: number) => firstRowY + index * 62;
const eventY = firstRowY + (Math.max(d.threats.length, d.consequences.length) - 1) * 31;
const eventLeft = 526;
const eventRight = 754;
const gateLeft = { x: 300, w: 174 };
const gateRight = { x: 806, w: 174 };
return <section className="asset asset-bowtie-risk-analysis">
<style>{CSS}</style>
<svg className="diagram" data-layout-still="true" width="1280" height="720" viewBox="0 0 1280 720" role="img" aria-labelledby="bowtie-title bowtie-desc">
<title id="bowtie-title">{d.title}</title>
<desc id="bowtie-desc">危険源 {d.hazard} から脅威と予防障壁を通って中心事象 {d.topEvent} へ進み、緩和障壁を経て結果へ至るBow-tie分析。</desc>
<rect x="0" y="0" width="1280" height="720" fill="var(--color-surface)" />
<text x="72" y="36" fontFamily="sans-serif" fontSize="14" fontWeight="700" letterSpacing="2" fill="var(--color-tertiary)">BOW-TIE RISK ANALYSIS</text>
{titleLines.map((line, i) => <text key={`title-${i}`} x="72" y={78 + i * 26} fontFamily="sans-serif" fontSize="24" fontWeight="700" fill="var(--color-on-surface)">{line}</text>)}
{subtitleLines.map((line, i) => <text key={`subtitle-${i}`} x="72" y={lastTitleY + 24 + i * 18} fontFamily="sans-serif" fontSize="13" fill="var(--color-secondary)">{line}</text>)}
<text x="72" y={hazardY} fontFamily="sans-serif" fontSize="13" fill="var(--color-secondary)">危険源: {d.hazard}</text>
<text x="72" y={columnY} fontFamily="sans-serif" fontSize="12" fontWeight="700" fill="var(--color-secondary)">脅威</text>
<text x="300" y={columnY} fontFamily="sans-serif" fontSize="12" fontWeight="700" fill="var(--color-secondary)">予防障壁</text>
<text x="548" y={columnY} fontFamily="sans-serif" fontSize="12" fontWeight="700" fill="var(--color-secondary)">中心事象</text>
<text x="806" y={columnY} fontFamily="sans-serif" fontSize="12" fontWeight="700" fill="var(--color-secondary)">緩和障壁</text>
<text x="1016" y={columnY} fontFamily="sans-serif" fontSize="12" fontWeight="700" fill="var(--color-secondary)">結果</text>
{d.threats.map((threat, i) => <line key={`tin-${threat.id}`} x1="267" y1={threatY(i)} x2={gateLeft.x} y2={threatY(i)} stroke="var(--color-outline)" strokeWidth="2" />)}
{d.threats.map((threat, i) => <line key={`t-event-${threat.id}`} x1={gateLeft.x + gateLeft.w} y1={threatY(i)} x2={eventLeft} y2={eventY} stroke="var(--color-outline)" strokeWidth="2" />)}
{d.consequences.map((outcome, i) => <line key={`event-g-${outcome.id}`} x1={eventRight} y1={eventY} x2={gateRight.x} y2={consequenceY(i)} stroke="var(--color-outline)" strokeWidth="2" />)}
{d.consequences.map((outcome, i) => <line key={`g-out-${outcome.id}`} x1={gateRight.x + gateRight.w} y1={consequenceY(i)} x2="1014" y2={consequenceY(i)} stroke="var(--color-outline)" strokeWidth="2" />)}
{d.threats.map((threat, i) => {
const y = threatY(i);
const lines = wrap(threat.label, 15);
const counts = barrierSummary(threat.preventiveBarriers);
return <g key={threat.id} data-threat-id={threat.id}>
<rect x="72" y={y - 26} width="195" height="52" rx="5" fill="var(--color-surface)" stroke="var(--color-outline)" strokeWidth="1.5" />
{lines.slice(0, 4).map((line, j) => <text key={j} x="84" y={y - ((Math.min(lines.length, 4) - 1) * 7) + j * 14 + 4} fontFamily="sans-serif" fontSize="10" fill="var(--color-on-surface)">{line}</text>)}
<rect x={gateLeft.x} y={y - 26} width={gateLeft.w} height="52" rx="5" fill="var(--color-surface)" stroke="var(--color-tertiary)" strokeWidth="2" />
<line x1={gateLeft.x + 13} x2={gateLeft.x + 13} y1={y - 18} y2={y + 18} stroke="var(--color-tertiary)" strokeWidth="3" />
<text x={gateLeft.x + 25} y={y - 3} fontFamily="sans-serif" fontSize="12" fontWeight="700" fill="var(--color-on-surface)">{threat.preventiveBarriers.length}件 · 有効 {counts.effective}</text>
<text x={gateLeft.x + 25} y={y + 15} fontFamily="sans-serif" fontSize="11" fill="var(--color-secondary)">低下 {counts.impaired} · 欠落 {counts.missing}</text>
</g>;
})}
<polygon points={`${eventLeft},${eventY} 570,${eventY - 66} 710,${eventY - 66} ${eventRight},${eventY} 710,${eventY + 66} 570,${eventY + 66}`} fill="var(--color-on-surface)" />
{wrap(d.topEvent, 15).slice(0, 4).map((line, i, lines) => <text key={i} x="640" y={eventY - ((lines.length - 1) * 9) + i * 18 + 5} textAnchor="middle" fontFamily="sans-serif" fontSize="15" fontWeight="700" fill="var(--color-surface)">{line}</text>)}
{d.consequences.map((outcome, i) => {
const y = consequenceY(i);
const lines = wrap(outcome.label, 15);
const counts = barrierSummary(outcome.mitigativeBarriers);
return <g key={outcome.id} data-consequence-id={outcome.id}>
<rect x={gateRight.x} y={y - 26} width={gateRight.w} height="52" rx="5" fill="var(--color-surface)" stroke="var(--color-tertiary)" strokeWidth="2" />
<line x1={gateRight.x + 13} x2={gateRight.x + 13} y1={y - 18} y2={y + 18} stroke="var(--color-tertiary)" strokeWidth="3" />
<text x={gateRight.x + 25} y={y - 3} fontFamily="sans-serif" fontSize="12" fontWeight="700" fill="var(--color-on-surface)">{outcome.mitigativeBarriers.length}件 · 有効 {counts.effective}</text>
<text x={gateRight.x + 25} y={y + 15} fontFamily="sans-serif" fontSize="11" fill="var(--color-secondary)">低下 {counts.impaired} · 欠落 {counts.missing}</text>
<rect x="1014" y={y - 26} width="194" height="52" rx="5" fill="var(--color-surface)" stroke="var(--color-outline)" strokeWidth="1.5" />
<text x="1026" y={y - 15} fontFamily="sans-serif" fontSize="10" fontWeight="700" fill="var(--color-tertiary)">重大度: {severityLabel[outcome.severity]}</text>
{lines.slice(0, 3).map((line, j) => <text key={j} x="1026" y={y - 1 + j * 12} fontFamily="sans-serif" fontSize="9" fill="var(--color-on-surface)">{line}</text>)}
</g>;
})}
<line x1="72" x2="1208" y1="625" y2="625" stroke="var(--color-outline)" />
<text x="72" y="654" fontFamily="sans-serif" fontSize="12" fill="var(--color-secondary)">障壁の状態は色と文言で示します。有効・低下・欠落の内容と責任は下の記録で確認します。</text>
<text x="72" y="687" fontFamily="sans-serif" fontSize="12" fill="var(--color-secondary)">線の流れ: 脅威 → 予防障壁 → 中心事象 → 緩和障壁 → 結果</text>
</svg>
<div className="mobile">
<p className="muted">BOW-TIE RISK ANALYSIS</p>
<h1>{d.title}</h1>
{d.subtitle && <p className="muted">{d.subtitle}</p>}
<p><strong>危険源:</strong> {d.hazard}</p>
<h2>脅威と予防障壁</h2>
<ol>{d.threats.map(threat => <li key={threat.id} data-bowtie-record={threat.id}>
<strong>{threat.label}</strong><ul>{threat.preventiveBarriers.map(control => <li key={control.id}>{control.label} · {statusLabel[control.status]}</li>)}</ul>
</li>)}</ol>
<h2>中心事象</h2><p>{d.topEvent}</p>
<h2>結果と緩和障壁</h2>
<ol>{d.consequences.map(outcome => <li key={outcome.id} data-bowtie-record={outcome.id}>
<strong>{outcome.label}</strong> · 重大度 {severityLabel[outcome.severity]}
<ul>{outcome.mitigativeBarriers.map(control => <li key={control.id}>{control.label} · {statusLabel[control.status]}</li>)}</ul>
</li>)}</ol>
</div>
<details open><summary>脅威・結果と個別障壁の記録</summary><table>
<thead><tr><th scope="col">区分</th><th scope="col">ID / 内容</th><th scope="col">障壁と状態</th></tr></thead>
<tbody>
{d.threats.map(threat => <tr key={threat.id} data-bowtie-record={threat.id}>
<td data-label="区分">脅威</td><td data-label="ID / 内容"><span className="record-id">{threat.id}</span><br/>{threat.label}</td>
<td data-label="予防障壁"><ul>{threat.preventiveBarriers.map(control => <li key={control.id} data-barrier-id={control.id}>{control.label} · {statusLabel[control.status]}</li>)}</ul></td>
</tr>)}
{d.consequences.map(outcome => <tr key={outcome.id} data-bowtie-record={outcome.id}>
<td data-label="区分">結果 · 重大度 {severityLabel[outcome.severity]}</td><td data-label="ID / 内容"><span className="record-id">{outcome.id}</span><br/>{outcome.label}</td>
<td data-label="緩和障壁"><ul>{outcome.mitigativeBarriers.map(control => <li key={control.id} data-barrier-id={control.id}>{control.label} · {statusLabel[control.status]}</li>)}</ul></td>
</tr>)}
</tbody>
</table></details>
</section>;
}
/designs/bowtie-risk-analysis/source/schema.ts
import { z } from "zod";
const heading = z.string().trim().min(1).max(160);
const id = z.string().trim().min(1).max(24);
const label = z.string().trim().min(1).max(72);
const barrier = z.object({
id: id.describe("管理策の一意なID"),
label: z.string().trim().min(1).max(48).describe("予防または緩和の管理策"),
status: z.enum(["effective", "impaired", "missing"]).describe("管理策の実効状態"),
}).strict();
const unique = (values: string[]) => new Set(values).size === values.length;
export const schema = z.object({
title: heading.describe("リスクと対応を伝える見出し"),
subtitle: heading.optional().describe("対象・評価時点・前提"),
hazard: label.describe("損失につながり得る危険源または活動"),
topEvent: label.describe("危険源の管理が失われた中心事象"),
threats: z.array(z.object({
id: id.describe("脅威の一意なID"),
label: label.describe("中心事象を起こし得る脅威"),
preventiveBarriers: z.array(barrier).min(1).max(3).describe("脅威と中心事象の間で発生を防ぐ管理策"),
}).strict()).min(2).max(6).describe("中心事象へ至る脅威と予防管理策"),
consequences: z.array(z.object({
id: id.describe("結果の一意なID"),
label: label.describe("中心事象がもたらし得る結果"),
severity: z.enum(["moderate", "major", "severe"]).describe("結果の重大度"),
mitigativeBarriers: z.array(barrier).min(1).max(3).describe("中心事象と結果の間で影響を抑える管理策"),
}).strict()).min(2).max(6).describe("中心事象から生じる結果と緩和管理策"),
}).strict().refine(data => {
const allIds = [
...data.threats.map(item => item.id),
...data.consequences.map(item => item.id),
...data.threats.flatMap(item => item.preventiveBarriers.map(control => control.id)),
...data.consequences.flatMap(item => item.mitigativeBarriers.map(control => control.id)),
];
return unique(allIds);
}, "脅威・結果・管理策のIDはすべて一意");
export type Props = z.infer<typeof schema>;
/designs/bowtie-risk-analysis/source/meta.yaml
id: bowtie-risk-analysis
name:
ja: Bow-tieリスク分析
en: Bow-tie Risk Analysis
purpose:
ja: 危険源から中心事象に至る脅威と予防障壁、事象後の結果と緩和障壁を因果経路として示す。
en: Map threats and preventive barriers leading to a top event, then consequences and mitigative barriers after it.
status: published
still: ./still.png
category: decision
catalogOrder: 790
jobs:
- prioritize-diagnose
useCases:
- prioritization
- problem-solution
layoutKinds:
- network
tags:
- bowtie
- barrier-management
- operational-risk
- offline
- responsive
whenToUse:
- ja: 一つの中心事象について、発生原因を防ぐ管理策と発生後の影響を抑える管理策を同時に合意する。
en: Use to agree on both prevention controls before one top event and mitigation controls after it.
- ja: 管理策の状態(有効・低下・欠落)を経路にひも付け、どの防護の穴が結果へつながるか説明する。
en: Use when control status must be tied to specific causal paths from threats through consequences.
avoidWhen:
- ja: 発生可能性と影響度のスコア付け・優先順位だけを並べる場合はリスクマトリクスを使う。
en: Use a risk matrix when the task is only scoring likelihood and impact or ranking risks.
- ja: 複数の中心事象や一般の工程順序を描く場合。この型は一つの中心事象を挟む因果経路に限る。
en: Do not use for multiple top events or general process sequences; this layout has one central event.
requiredInputs:
- ja: 危険源、中心事象、2〜6件の脅威と結果を定義する。
en: Define one hazard and top event with two to six threats and consequences.
- ja: 各脅威に1〜3件の予防障壁、各結果に1〜3件の緩和障壁を割り当て、状態を記録する。
en: Assign one to three status-coded preventive barriers per threat and mitigative barriers per consequence.
relatedLayouts:
- risk-matrix-register
- incident-review
dataSchema:
title: string — リスクと対応を伝える見出し
subtitle: string? — 対象・評価時点・前提
hazard: string — 損失につながり得る危険源または活動
topEvent: string — 危険源の管理が失われた中心事象
threats: array (id, label, preventiveBarriers) — 脅威2〜6件。各1〜3障壁
consequences: array (id, label, severity, mitigativeBarriers) — 結果2〜6件。各1〜3障壁
sources: []
/designs/bowtie-risk-analysis/source/sample-data.json
{
"title": "圧力放出を防ぐ障壁と事後緩和策を確認",
"subtitle": "架空サンプル|高圧配管の定期保全作業",
"hazard": "高圧配管内に蓄積された流体エネルギー",
"topEvent": "圧力の制御不能な放出",
"threats": [
{
"id": "TH-01",
"label": "遮断弁が完全に閉止されていない",
"preventiveBarriers": [
{ "id": "PB-01-1", "label": "作業前の二者照合", "status": "effective" },
{ "id": "PB-01-2", "label": "弁位置の現場表示", "status": "impaired" }
]
},
{
"id": "TH-02",
"label": "計器の劣化で圧力値を読み誤る",
"preventiveBarriers": [
{ "id": "PB-02-1", "label": "校正期限の管理", "status": "effective" },
{ "id": "PB-02-2", "label": "独立計器での再確認", "status": "effective" }
]
},
{
"id": "TH-03",
"label": "引継ぎ漏れで系統を誤認する",
"preventiveBarriers": [
{ "id": "PB-03-1", "label": "系統図とタグの照合", "status": "effective" },
{ "id": "PB-03-2", "label": "交代時の対面引継ぎ", "status": "missing" }
]
}
],
"consequences": [
{
"id": "CO-01",
"label": "作業者が高圧流体に接触する",
"severity": "severe",
"mitigativeBarriers": [
{ "id": "MB-01-1", "label": "危険区域の立入制限", "status": "effective" },
{ "id": "MB-01-2", "label": "緊急遮断の手動操作", "status": "effective" }
]
},
{
"id": "CO-02",
"label": "周辺設備が損傷し運転が停止する",
"severity": "major",
"mitigativeBarriers": [
{ "id": "MB-02-1", "label": "圧力放出の安全経路", "status": "effective" },
{ "id": "MB-02-2", "label": "予備設備への切替手順", "status": "impaired" }
]
},
{
"id": "CO-03",
"label": "流体が構内へ漏えいし環境へ影響する",
"severity": "severe",
"mitigativeBarriers": [
{ "id": "MB-03-1", "label": "排水口の封止設備", "status": "impaired" },
{ "id": "MB-03-2", "label": "漏えい検知と初動連絡", "status": "effective" }
]
}
]
}