索引に戻る

RACIマトリクス(責任分担・統制診断)

表
生成可能
RACIマトリクス(責任分担・統制診断)

作品として見る

持ち帰る

コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。

全部入り

raci-matrix-everything.md

このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown

依頼文

1.8 KB
「RACIマトリクス(責任分担・統制診断)」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。

## この型がすること
活動と役割の交点へR/A/C/Iを配置し、最終責任者Aの不在・重複と実行責任者Rの不在をデータから自動検出する。責任分担表、統制課題ledger、mobile activity ledgerが同じ活動・役割順・記号を共有し、診断を元の根拠へ戻して読める

## 渡す必要があるもの
- 活動または意思決定
- 関与する役割
- 活動と役割ごとのRACI記号

## 入力の形
- `title` — string — スライドタイトル。最優先の責任欠陥を結論として書く
- `subtitle` — string(任意)— 対象組織、時点、RACI定義など
- `roles` — { id, label }[] — 役割・部門(2〜6件)
- `activities` — { label, assignments, note? }[] — 3〜8件。assignmentsはrolesと同順・同数のR/A/C/I/-

入力の機械可読な検証は schema.ts が持ちます。足りない入力は推測せず質問してください。

## 向かない場合
- 製品や施策の優劣を評価軸で比較したい場合
- 工程の順序や所要時間を説明したい場合

## 作り方
入力を JSON にまとめ、次を実行します。

```bash
node scripts/render.mjs raci-matrix <入力.json> --theme ink -o out.html
```

出力は 1 ファイルで完結し、外部から何も読み込みません。

## 原本
- https://design-vault.pages.dev/designs/raci-matrix/source/template.tsx
- https://design-vault.pages.dev/designs/raci-matrix/source/schema.ts
- https://design-vault.pages.dev/designs/raci-matrix/source/sample-data.json
- https://design-vault.pages.dev/designs/raci-matrix/sample.html

ファイル

テンプレート・入力スキーマ・サンプルデータ・メタ情報。この文書はこの 4 つから生成されています。 生成された文書

template.tsx

19.2 KB

/designs/raci-matrix/source/template.tsx

中身を見る
import { useId } from "react";
import type {
  RaciActivity,
  RaciMark,
  RaciMatrixProps,
} from "./schema";

export type RaciIssueCode =
  | "missing-accountable"
  | "multiple-accountable"
  | "missing-responsible";
export type RaciIssue = {
  activityIndex: number;
  activityLabel: string;
  code: RaciIssueCode;
};

export function activityAuditSummary(activity: {
  assignments: readonly RaciMark[];
}) {
  return {
    accountable: activity.assignments.filter((mark) => mark === "A").length,
    responsible: activity.assignments.filter((mark) => mark === "R").length,
  };
}

export function auditActivities(
  activities: readonly {
    label: string;
    assignments: readonly RaciMark[];
  }[],
): RaciIssue[] {
  return activities.flatMap((activity, activityIndex) => {
    const issues: RaciIssue[] = [];
    const { accountable, responsible } = activityAuditSummary(activity);
    if (accountable === 0) {
      issues.push({
        activityIndex,
        activityLabel: activity.label,
        code: "missing-accountable",
      });
    }
    if (accountable > 1) {
      issues.push({
        activityIndex,
        activityLabel: activity.label,
        code: "multiple-accountable",
      });
    }
    if (responsible === 0) {
      issues.push({
        activityIndex,
        activityLabel: activity.label,
        code: "missing-responsible",
      });
    }
    return issues;
  });
}

export function issueEvidenceRoleIndexes(
  activity: { assignments: readonly RaciMark[] },
  issue: RaciIssue,
) {
  if (issue.code !== "multiple-accountable") return [];
  return activity.assignments.flatMap((mark, roleIndex) =>
    mark === "A" ? [roleIndex] : [],
  );
}

export function issueLabel(code: RaciIssueCode) {
  return (
    {
      "missing-accountable": "A不在",
      "multiple-accountable": "A重複",
      "missing-responsible": "R不在",
    } as const
  )[code];
}

const issueDescription = (code: RaciIssueCode) =>
  (
    {
      "missing-accountable": "最終説明責任を負う役割が定義されていない",
      "multiple-accountable": "最終判断者が複数で、意思決定が競合する",
      "missing-responsible": "実行責任を担う役割が定義されていない",
    } as const
  )[code];

const MARK_STYLES: Record<RaciMark, string> = {
  R: "bg-tertiary text-on-primary",
  A: "bg-primary text-on-primary",
  C: "bg-neutral text-secondary",
  I: "border border-outline bg-surface text-secondary",
  "-": "text-outline",
};

const stagger = (index: number) =>
  `calc(var(--stagger-step) * ${index})`;

function Mark({
  mark,
  activityIndex,
  roleLabel,
  evidence = false,
}: {
  mark: RaciMark;
  activityIndex: number;
  roleLabel: string;
  evidence?: boolean;
}) {
  return (
    <span
      tabIndex={0}
      data-raci-hotspot={activityIndex}
      data-raci-evidence={evidence ? "true" : undefined}
      aria-label={`${roleLabel}: ${mark === "-" ? "割当なし" : mark}`}
      className={`inline-flex size-8 items-center justify-center rounded-full font-mono text-label outline-none transition-[color,background-color,box-shadow,opacity] focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 ${MARK_STYLES[mark]} ${evidence ? "ring-2 ring-tertiary ring-offset-2" : ""}`}
    >
      {mark === "-" ? "·" : mark}
    </span>
  );
}

function ActivityIssueLabels({ issues }: { issues: readonly RaciIssue[] }) {
  if (issues.length === 0) return null;
  return (
    <span className="ml-2 inline-flex flex-wrap gap-1 align-middle">
      {issues.map((issue) => (
        <span
          key={issue.code}
          className="border-l-2 border-tertiary pl-1.5 font-mono text-label text-tertiary"
        >
          {issueLabel(issue.code)}
        </span>
      ))}
    </span>
  );
}

function MobileActivityRecord({
  activity,
  activityIndex,
  roles,
  issues,
  isDefaultPriority,
}: {
  activity: RaciActivity;
  activityIndex: number;
  roles: RaciMatrixProps["roles"];
  issues: readonly RaciIssue[];
  isDefaultPriority: boolean;
}) {
  return (
    <article
      data-raci-activity-record={activityIndex}
      data-raci-surface={activityIndex}
      data-raci-default-priority={isDefaultPriority ? "true" : undefined}
      className={`animate-rise border-b px-1 py-4 transition-[background-color,box-shadow,opacity] first:border-t ${
        isDefaultPriority
          ? "border-tertiary bg-gradient-to-r from-tertiary/10 to-surface"
          : "border-outline"
      }`}
      style={{ animationDelay: stagger(activityIndex) }}
    >
      <div
        tabIndex={0}
        data-raci-hotspot={activityIndex}
        className="flex items-start gap-2 outline-none focus-visible:ring-2 focus-visible:ring-primary"
      >
        <span className="pt-0.5 font-mono text-label text-secondary">
          {String(activityIndex + 1).padStart(2, "0")}
        </span>
        <h2 className="min-w-0 text-body-sm">
          {activity.label}
          <ActivityIssueLabels issues={issues} />
        </h2>
      </div>
      <div className="mt-3 grid grid-cols-2 gap-x-4 gap-y-2 pl-7">
        {activity.assignments.map((mark, roleIndex) => (
          <div
            key={roles[roleIndex].id}
            data-raci-role-index={roleIndex}
            data-raci-mark={mark}
            className="flex min-w-0 items-center justify-between gap-2 border-t border-outline/60 pt-2"
          >
            <span className="min-w-0 text-label text-secondary">
              {roles[roleIndex].label}
            </span>
            <Mark
              mark={mark}
              activityIndex={activityIndex}
              roleLabel={roles[roleIndex].label}
            />
          </div>
        ))}
      </div>
    </article>
  );
}

export default function RaciMatrix({
  title,
  subtitle,
  roles,
  activities,
  instanceId,
}: RaciMatrixProps) {
  const autoId = useId();
  const uid =
    (instanceId ?? autoId).replace(
      // 捨てる操作は単射でないので、schema を通る別々の値が同じ id になる
      // (`sales:a` と `sales/a` がどちらも `salesa`)。同じ文書へ 2 つ置くと
      // id と radio の name が衝突して、片方の操作がもう片方に効く。
      // 逃がし文字(`_`)自身も逃がすので、この変換は単射。
      /[^a-zA-Z0-9-]/g,
      (c) => `_${c.charCodeAt(0).toString(16)}_`,
    ) || "raci";
  const issues = auditActivities(activities);
  const firstIssueActivity = issues[0]?.activityIndex;
  const notes = activities.filter((activity) => activity.note);
  const issuesByActivity = activities.map((_, activityIndex) =>
    issues.filter((issue) => issue.activityIndex === activityIndex),
  );
  const interactionCss = activities
    .map(
      (_, activityIndex) => `
[data-raci-root="${uid}"]:has([data-raci-hotspot="${activityIndex}"]:is(:hover,:focus-visible)) [data-raci-surface="${activityIndex}"] {
  background-color: color-mix(in srgb, var(--color-primary) 9%, var(--color-surface));
  box-shadow: inset 3px 0 0 var(--color-primary);
  color: var(--color-on-surface);
  opacity: 1;
}
[data-raci-root="${uid}"]:has([data-raci-hotspot="${activityIndex}"]:is(:hover,:focus-visible)) [data-raci-default-priority="true"]:not([data-raci-surface="${activityIndex}"]) {
  filter: saturate(.28) brightness(1.08);
}`,
    )
    .join("\n");

  return (
    <section
      data-raci-root={uid}
      className="group/raci mx-auto max-w-slide bg-surface p-12 font-sans text-on-surface"
    >
      <style>{interactionCss}</style>
      <input
        id={`${uid}-matrix`}
        name={`${uid}-view`}
        type="radio"
        defaultChecked
        className="raci-matrix-view sr-only"
      />
      <input
        id={`${uid}-issues`}
        name={`${uid}-view`}
        type="radio"
        className="raci-issues-view sr-only"
      />
      <header className="mb-7 flex flex-wrap items-end justify-between gap-4">
        <div className="max-w-3xl">
          <h1 className="text-h1">{title}</h1>
          {subtitle && (
            <p className="mt-2 text-body-sm text-secondary">{subtitle}</p>
          )}
        </div>
        <div className="flex rounded-sm border border-outline bg-surface-subtle p-1 shadow-card">
          <label
            htmlFor={`${uid}-matrix`}
            className="cursor-pointer rounded-sm px-4 py-1.5 text-label text-secondary group-has-[.raci-matrix-view:checked]/raci:bg-primary group-has-[.raci-matrix-view:checked]/raci:text-on-primary"
          >
            責任分担
          </label>
          <label
            htmlFor={`${uid}-issues`}
            className="cursor-pointer rounded-sm px-4 py-1.5 text-label text-secondary group-has-[.raci-issues-view:checked]/raci:bg-primary group-has-[.raci-issues-view:checked]/raci:text-on-primary"
          >
            統制課題
          </label>
        </div>
      </header>

      <div
        data-testid="raci-matrix"
        className="block group-has-[.raci-issues-view:checked]/raci:hidden"
      >
        <div data-raci-view="matrix-desktop" className="hidden sm:block">
          <table className="w-full table-fixed border-collapse">
            <colgroup>
              <col className="w-[29%]" />
              {roles.map((role) => (
                <col key={role.id} />
              ))}
            </colgroup>
            <thead>
              <tr className="border-y border-outline">
                <th className="px-3 py-3 text-left font-mono text-label text-secondary">
                  ACTIVITY
                </th>
                {roles.map((role) => (
                  <th
                    key={role.id}
                    className="px-1 py-3 text-center text-label text-secondary"
                  >
                    {role.label}
                  </th>
                ))}
              </tr>
            </thead>
            <tbody>
              {activities.map((activity, activityIndex) => {
                const activityIssues = issuesByActivity[activityIndex];
                const isDefaultPriority =
                  activityIndex === firstIssueActivity;
                return (
                  <tr
                    key={activityIndex}
                    data-raci-activity-record={activityIndex}
                    data-raci-surface={activityIndex}
                    data-raci-default-priority={
                      isDefaultPriority ? "true" : undefined
                    }
                    data-has-issue={
                      activityIssues.length > 0 ? "true" : "false"
                    }
                    className={`animate-rise border-b transition-[background-color,box-shadow,opacity] ${
                      isDefaultPriority
                        ? "border-tertiary bg-gradient-to-r from-tertiary/10 to-surface"
                        : "border-outline"
                    }`}
                    style={{ animationDelay: stagger(activityIndex) }}
                  >
                    <th scope="row" className="px-3 py-3 text-left">
                      <span
                        tabIndex={0}
                        data-raci-hotspot={activityIndex}
                        className="block text-body-sm outline-none focus-visible:ring-2 focus-visible:ring-primary"
                      >
                        {activity.label}
                        <ActivityIssueLabels issues={activityIssues} />
                      </span>
                    </th>
                    {activity.assignments.map((mark, roleIndex) => (
                      <td
                        key={roles[roleIndex].id}
                        data-raci-role-index={roleIndex}
                        data-raci-mark={mark}
                        className="px-1 py-3 text-center"
                      >
                        <Mark
                          mark={mark}
                          activityIndex={activityIndex}
                          roleLabel={roles[roleIndex].label}
                        />
                      </td>
                    ))}
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>

        <div data-raci-view="matrix-mobile" className="sm:hidden">
          {activities.map((activity, activityIndex) => (
            <MobileActivityRecord
              key={activityIndex}
              activity={activity}
              activityIndex={activityIndex}
              roles={roles}
              issues={issuesByActivity[activityIndex]}
              isDefaultPriority={activityIndex === firstIssueActivity}
            />
          ))}
        </div>
        <p className="mt-3 text-right font-mono text-label text-secondary">
          R 実行 · A 最終責任 · C 相談 · I 報告
        </p>
      </div>

      <div
        data-testid="raci-issues"
        data-raci-view="issues"
        className="hidden group-has-[.raci-issues-view:checked]/raci:block"
      >
        {issues.length === 0 ? (
          <div className="border-y border-outline bg-surface-subtle p-8 text-center">
            <p className="text-h3">
              責任上の欠陥は検出されませんでした
            </p>
            <p className="mt-2 text-body-sm text-secondary">
              全活動にAが1件、Rが1件以上あります
            </p>
          </div>
        ) : (
          <div>
            {issues.map((issue, issueIndex) => {
              const activity = activities[issue.activityIndex];
              const summary = activityAuditSummary(activity);
              const evidenceIndexes = new Set(
                issueEvidenceRoleIndexes(activity, issue),
              );
              const absence =
                issue.code === "missing-accountable"
                  ? { mark: "A", count: summary.accountable }
                  : issue.code === "missing-responsible"
                    ? { mark: "R", count: summary.responsible }
                    : null;
              return (
                <article
                  key={`${issue.activityIndex}-${issue.code}`}
                  data-priority={issueIndex === 0 ? "true" : "false"}
                  data-raci-default-priority={
                    issueIndex === 0 ? "true" : undefined
                  }
                  data-raci-issue-record={issueIndex}
                  data-raci-issue-activity={issue.activityIndex}
                  data-raci-surface={issue.activityIndex}
                  className={`animate-rise border-b px-1 py-5 transition-[background-color,box-shadow,filter,opacity] first:border-t ${
                    issueIndex === 0
                      ? "border-tertiary bg-gradient-to-r from-tertiary/15 via-surface to-surface"
                      : "border-outline bg-surface"
                  }`}
                  style={{ animationDelay: stagger(issueIndex) }}
                >
                  <div className="grid gap-4 lg:grid-cols-[minmax(11rem,1.3fr)_minmax(20rem,3fr)_minmax(13rem,1.5fr)] lg:items-center">
                    <div
                      tabIndex={0}
                      data-raci-hotspot={issue.activityIndex}
                      className="outline-none focus-visible:ring-2 focus-visible:ring-primary"
                    >
                      <div className="flex items-center gap-2">
                        <span className="font-mono text-label text-secondary">
                          {String(issueIndex + 1).padStart(2, "0")}
                        </span>
                        <span className="border-l-2 border-tertiary pl-2 font-mono text-label text-tertiary">
                          {issueLabel(issue.code)}
                        </span>
                      </div>
                      <h2 className="mt-1 text-body-sm">
                        {issue.activityLabel}
                      </h2>
                    </div>

                    <div className="flex flex-wrap gap-x-4 gap-y-3">
                      {activity.assignments.map((mark, roleIndex) => {
                        const evidence = evidenceIndexes.has(roleIndex);
                        return (
                          <div
                            key={roles[roleIndex].id}
                            data-raci-role-index={roleIndex}
                            data-raci-mark={mark}
                            className={`flex min-w-28 flex-1 items-center justify-between gap-2 border-t pt-2 ${
                              evidence
                                ? "border-tertiary"
                                : "border-outline"
                            }`}
                          >
                            <span className="text-label text-secondary">
                              {roles[roleIndex].label}
                            </span>
                            <Mark
                              mark={mark}
                              activityIndex={issue.activityIndex}
                              roleLabel={roles[roleIndex].label}
                              evidence={evidence}
                            />
                          </div>
                        );
                      })}
                    </div>

                    <div className="border-l-2 border-outline pl-3">
                      <p className="text-body-sm text-secondary">
                        {issueDescription(issue.code)}
                      </p>
                      {absence && (
                        <span
                          data-raci-absence={absence.mark}
                          className="mt-2 inline-flex items-baseline gap-2 font-mono text-label text-tertiary"
                        >
                          <strong className="text-h3">
                            {absence.mark} {absence.count}
                          </strong>
                          <span>/ 必要数 1</span>
                        </span>
                      )}
                      {issue.code === "multiple-accountable" && (
                        <span className="mt-2 block font-mono text-label text-tertiary">
                          A {summary.accountable} / 必要数 1
                        </span>
                      )}
                    </div>
                  </div>
                </article>
              );
            })}
          </div>
        )}
      </div>

      {notes.length > 0 && (
        <details
          data-inline-disclosure="true"
          className="group mt-6 rounded-lg border border-outline bg-surface-subtle"
        >
          <summary className="flex cursor-pointer list-none items-center px-4 py-3 text-label text-secondary focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tertiary">
            <span>補足を確認</span>
            <span className="ml-auto font-mono">{notes.length}件</span>
            <span
              aria-hidden="true"
              className="ml-2 text-tertiary transition-transform group-open:rotate-45"
            >
              +
            </span>
          </summary>
          <div className="grid gap-3 border-t border-outline p-4 sm:grid-cols-2">
            {notes.map((activity, index) => (
              <article
                key={index}
                data-note-item="true"
                className="rounded-md bg-surface p-4"
              >
                <h3 className="text-label">{activity.label}</h3>
                <p className="mt-2 text-body-sm">{activity.note}</p>
              </article>
            ))}
          </div>
        </details>
      )}
    </section>
  );
}

schema.ts

1.9 KB

/designs/raci-matrix/source/schema.ts

中身を見る
import { z } from "zod";

export const raciMarkSchema = z.enum(["R", "A", "C", "I", "-"]);

export const raciRoleSchema = z
  .object({
    id: z.string().min(1).describe("役割の一意なID"),
    label: z.string().min(1).describe("役割・部門名"),
  })
  .strict();

export const raciActivitySchema = z
  .object({
    label: z.string().min(1).describe("業務・意思決定・成果物名"),
    assignments: z.array(raciMarkSchema).describe("rolesと同じ順・同じ件数のR/A/C/I/-"),
    note: z.string().optional().describe("責任境界、判断条件、例外などの補足"),
  })
  .strict();

export const schema = z
  .object({
    title: z.string().describe("スライドタイトル。最優先の責任欠陥を結論として書く"),
    subtitle: z.string().optional().describe("対象組織、時点、RACI定義など"),
    roles: z.array(raciRoleSchema).min(2).max(6).describe("役割・部門(2〜6件)"),
    activities: z.array(raciActivitySchema).min(3).max(8).describe("活動・判断(3〜8件)"),
    instanceId: z.string().min(1).optional().describe("表示切替radioの識別子"),
  })
  .strict()
  .superRefine((data, ctx) => {
    const roleIds = new Set<string>();
    data.roles.forEach((role, index) => {
      if (roleIds.has(role.id)) {
        ctx.addIssue({ code: "custom", message: "role.id は一意にすること", path: ["roles", index, "id"] });
      }
      roleIds.add(role.id);
    });
    data.activities.forEach((activity, index) => {
      if (activity.assignments.length !== data.roles.length) {
        ctx.addIssue({ code: "custom", message: `assignments は roles と同じ${data.roles.length}件にすること`, path: ["activities", index, "assignments"] });
      }
    });
  });

export type RaciMark = z.infer<typeof raciMarkSchema>;
export type RaciRole = z.infer<typeof raciRoleSchema>;
export type RaciActivity = z.infer<typeof raciActivitySchema>;
export type RaciMatrixProps = z.infer<typeof schema>;

meta.yaml

2.5 KB

/designs/raci-matrix/source/meta.yaml

中身を見る
id: raci-matrix
name:
  ja: "RACIマトリクス(責任分担・統制診断)"
  en: "RACI Matrix (Responsibility and Control)"
purpose:
  ja: "活動と役割の交点へR/A/C/Iを配置し、最終責任者Aの不在・重複と実行責任者Rの不在をデータから自動検出する。責任分担表、統制課題ledger、mobile activity ledgerが同じ活動・役割順・記号を共有し、診断を元の根拠へ戻して読める"
  en: "R/A/C/I at each intersection of activity and role, with a missing or duplicated accountable owner, and a missing responsible party, found from the data. The table, the control ledger and the mobile ledger share one order of activities and roles"
status: published
# 索引カードの図版。pnpm layout:render が原本から描く。
still: ./still.png
category: governance
catalogOrder: 100
jobs:
  - assign-governance
  - prioritize-diagnose
useCases:
  - org
layoutKinds:
  - table
tags: [raci, governance, responsibility, operating-model, data-driven, interactive]
whenToUse:
  - ja: "プロジェクト・運用・移管体制の責任境界を明確にする場合"
    en: "Making the boundaries of responsibility explicit on a project or a handover"
  - ja: "承認者や実行担当の不在・重複を診断したい場合"
    en: "Diagnosing a missing or duplicated approver"
  - ja: "選択肢の比較ではなく、活動ごとの役割分担を示す場合"
    en: "Showing who does what per activity, rather than comparing options"
  - ja: "数値スコアではなくRACI記号の整合性を確認する場合"
    en: "Checking RACI consistency rather than numeric scores"
avoidWhen:
  - ja: "製品や施策の優劣を評価軸で比較したい場合"
    en: "Comparing products or initiatives on criteria"
  - ja: "工程の順序や所要時間を説明したい場合"
    en: "Explaining the order or duration of steps"
requiredInputs:
  - ja: "活動または意思決定"
    en: "The activities or decisions"
  - ja: "関与する役割"
    en: "The roles involved"
  - ja: "活動と役割ごとのRACI記号"
    en: "A RACI letter for each activity and role"
relatedLayouts: [comparison-table, process-flow, heatmap]
dataSchema:
  title: "string — スライドタイトル。最優先の責任欠陥を結論として書く"
  subtitle: "string(任意)— 対象組織、時点、RACI定義など"
  roles: "{ id, label }[] — 役割・部門(2〜6件)"
  activities: "{ label, assignments, note? }[] — 3〜8件。assignmentsはrolesと同順・同数のR/A/C/I/-"
sources: []

sample-data.json

1.3 KB

/designs/raci-matrix/source/sample-data.json

中身を見る
{
  "title": "運用移管の責任設計 — 障害一次判断の最終責任者が未配置",
  "subtitle": "新基幹システム運用モデル案(R:実行、A:最終責任、C:相談、I:報告)",
  "roles": [
    { "id": "business", "label": "業務部門" },
    { "id": "service", "label": "サービス責任者" },
    { "id": "ops", "label": "運用チーム" },
    { "id": "vendor", "label": "保守ベンダー" },
    { "id": "security", "label": "セキュリティ" }
  ],
  "activities": [
    { "label": "リリース承認", "assignments": ["C", "A", "R", "C", "I"], "note": "サービス責任者が業務影響と試験結果を踏まえて最終承認" },
    { "label": "障害一次判断", "assignments": ["I", "-", "R", "C", "C"], "note": "重大度判定の最終責任が未定義。15分以内の判断目標に影響" },
    { "label": "復旧作業", "assignments": ["I", "A", "R", "R", "C"], "note": "運用チームが指揮し、保守ベンダーが技術作業を実施" },
    { "label": "恒久対策承認", "assignments": ["C", "A", "R", "C", "C"], "note": "再発防止策と残存リスクをサービス責任者が承認" },
    { "label": "月次SLA報告", "assignments": ["I", "A", "R", "C", "I"], "note": "SLA、障害傾向、改善アクションを月次レビュー" }
  ]
}