Back to index

RACI Matrix (Responsibility and Control)

Table
Generates documents
RACI Matrix (Responsibility and Control)

Seen as a design

Take it with you

Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.

Everything

raci-matrix-everything.md

Everything in this panel, including the contents behind each URL, in one Markdown file

Prompt

1.6 KB
Build an HTML document with "RACI Matrix (Responsibility and Control)". This layout runs, so there is no composition to rebuild — what it needs is content.

## What this layout does
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

## What you have to supply
- The activities or decisions
- The roles involved
- A RACI letter for each activity and role

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

schema.ts holds the machine-checkable validation. Ask rather than guess when an input is missing.

## When not to use it
- Comparing products or initiatives on criteria
- Explaining the order or duration of steps

## How to build it
Put the input in a JSON file and run:

```bash
node scripts/render.mjs raci-matrix <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/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

Files

Template, input schema, sample data and metadata. This document was generated from those four. The generated document

template.tsx

19.2 KB

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

Show contents
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

Show contents
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

Show contents
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

Show contents
{
  "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、障害傾向、改善アクションを月次レビュー" }
  ]
}