タスクの循環依存を着手前に発見|GASで前後関係をdry-run監査

タスクの循環依存を着手前に発見|GASで前後関係をdry-run監査

ツールおじ|GoogleアカウントをAIエージェント化

ツールおじ|GoogleアカウントをAIエージェント化

タスク表を整えたのに、「AはBの完了待ち、BはCの完了待ち、CはAの完了待ち」となって、誰も着手できないことがあります。この記事では、スプレッドシートのタスクIDと依存先を読み取り、参照漏れ・自己依存・循環依存を、セルを更新せずに事前確認します。

確認結果は行番号、タスクID、理由だけをログへ出します。タスク名や担当者名は初期設定ではログへ残しません。

先に知っておきたいこと

  • タスクの追加、更新、削除、通知、メール送信は行いません。
  • タスクID、依存先ID、状態、期限の入力だけを確認します。
  • 依存先が存在しない、同じタスクを参照している、循環している場合はHOLDにします。
  • 業務の優先順位や納期の妥当性を自動判断するものではありません。
  • 実データへ使う前に、コピーしたテスト用シートで試してください。

準備するシート

スプレッドシートに「タスク依存関係」シートを作り、1行目へ次の見出しを入れます。

  • A列: task_id
  • B列: task_name
  • C列: depends_on
  • D列: status
  • E列: due_date
  • F列: owner_role

2行目以降へタスクを入力します。depends_on は、先に完了している必要がある task_id をカンマ区切りで入力します。依存先がない場合は空欄で構いません。

status は backlog / ready / doing / done / hold のいずれかに統一します。owner_role には個人名ではなく、担当区分だけを入れると検証用データを作りやすくなります。

コピーして使える最小コード

const CONFIG = Object.freeze({
  DRY_RUN: true,
  SHEET_NAME: 'タスク依存関係',
  MAX_ROWS: 200,
  LOG_TASK_NAME: false,
  ALLOWED_STATUS: ['backlog', 'ready', 'doing', 'done', 'hold']
});

function auditTaskDependencyPlanFree() {
  if (CONFIG.DRY_RUN !== true) {
    throw new Error('安全のためDRY_RUNはtrue固定です');
  }
  if (!Number.isInteger(CONFIG.MAX_ROWS) ||
      CONFIG.MAX_ROWS < 1 || CONFIG.MAX_ROWS > 200) {
    throw new Error('MAX_ROWSは1〜200で指定してください');
  }

  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName(CONFIG.SHEET_NAME);
  if (!sheet) {
    throw new Error('タスク依存関係シートが見つかりません');
  }

  const values = sheet.getDataRange().getValues();
  if (values.length < 2) {
    throw new Error('確認するタスクを1件以上入力してください');
  }

  const expectedHeaders = [
    'task_id', 'task_name', 'depends_on',
    'status', 'due_date', 'owner_role'
  ];
  const actualHeaders = values[0].map(value => String(value).trim());
  if (expectedHeaders.some((header, index) => actualHeaders[index] !== header)) {
    throw new Error('1行目の見出しを指定どおりにしてください');
  }

  const issues = [];
  const taskMap = new Map();
  const rows = values.slice(1, CONFIG.MAX_ROWS + 1);

  rows.forEach((row, index) => {
    const rowNumber = index + 2;
    const taskId = String(row[0] || '').trim();
    const taskName = String(row[1] || '').trim();
    const dependencyIds = String(row[2] || '')
      .split(',')
      .map(value => value.trim())
      .filter(Boolean);
    const status = String(row[3] || '').trim().toLowerCase();
    const dueDate = row[4];

    if (!taskId) {
      issues.push({ row: rowNumber, taskId: '', reason: 'task_idが空欄' });
      return;
    }
    if (taskMap.has(taskId)) {
      issues.push({ row: rowNumber, taskId, reason: 'task_idが重複' });
      return;
    }
    if (!taskName) {
      issues.push({ row: rowNumber, taskId, reason: 'task_nameが空欄' });
    }
    if (!CONFIG.ALLOWED_STATUS.includes(status)) {
      issues.push({ row: rowNumber, taskId, reason: 'statusが許可値外' });
    }
    if (dueDate && !(dueDate instanceof Date) &&
        Number.isNaN(Date.parse(String(dueDate)))) {
      issues.push({ row: rowNumber, taskId, reason: 'due_dateを日付として解釈できない' });
    }
    if (dependencyIds.includes(taskId)) {
      issues.push({ row: rowNumber, taskId, reason: '自分自身へ依存' });
    }

    taskMap.set(taskId, {
      row: rowNumber,
      taskName,
      dependencyIds: [...new Set(dependencyIds)],
      status
    });
  });

  taskMap.forEach((task, taskId) => {
    task.dependencyIds.forEach(dependencyId => {
      if (!taskMap.has(dependencyId)) {
        issues.push({
          row: task.row,
          taskId,
          reason: '依存先task_idが存在しない',
          dependencyId
        });
        return;
      }
      const dependency = taskMap.get(dependencyId);
      if (task.status === 'done' && dependency.status !== 'done') {
        issues.push({
          row: task.row,
          taskId,
          reason: '未完了の依存先があるのにdone',
          dependencyId
        });
      }
    });
  });

  const visiting = new Set();
  const visited = new Set();
  const cycleKeys = new Set();

  function visit(taskId, route) {
    if (visiting.has(taskId)) {
      const start = route.indexOf(taskId);
      const cycle = [...route.slice(start), taskId];
      const cycleKey = cycle.join('>');
      if (!cycleKeys.has(cycleKey)) {
        cycleKeys.add(cycleKey);
        issues.push({
          row: taskMap.get(taskId).row,
          taskId,
          reason: '循環依存',
          route: cycle
        });
      }
      return;
    }
    if (visited.has(taskId) || !taskMap.has(taskId)) {
      return;
    }

    visiting.add(taskId);
    const task = taskMap.get(taskId);
    task.dependencyIds.forEach(dependencyId => {
      visit(dependencyId, [...route, taskId]);
    });
    visiting.delete(taskId);
    visited.add(taskId);
  }

  taskMap.forEach((value, taskId) => visit(taskId, []));

  const result = {
    status: issues.length ? 'HOLD' : 'PASS',
    dryRun: CONFIG.DRY_RUN,
    checkedCount: taskMap.size,
    issueCount: issues.length,
    issues
  };

  if (CONFIG.LOG_TASK_NAME === true) {
    result.warning = 'タスク名をログへ出す設定です。機密情報がないか確認してください。';
  }

  Logger.log(JSON.stringify(result));
  if (issues.length) {
    throw new Error('タスクの前後関係にHOLDがあります');
  }
}

実行前の準備

1. テスト用のスプレッドシートをコピーします。

2. 「タスク依存関係」シートへ5件ほど入力します。

3. Apps Scriptへコードを貼り付けます。

4. auditTaskDependencyPlanFree を実行します。

5. 実行ログの status、issueCount、issues を確認します。

6. HOLDが出た行を直してから、もう一度コピー環境で確認します。

ログの読み方

  • PASS: 参照漏れ、自己依存、循環依存が見つかっていません。タスクは更新されていません。
  • HOLD: 着手前に直すべき行があります。
  • taskId: 修正対象のタスクIDです。
  • dependencyId: 見つからない依存先、または未完了の依存先です。
  • route: 循環しているタスクIDの経路です。

停止条件

  • task_id が空欄または重複している
  • 依存先 task_id が表に存在しない
  • 自分自身を depends_on に指定している
  • A→B→C→Aのような循環依存がある
  • 未完了の依存先があるのに status が done になっている
  • status や due_date の形式が不正

どれか1つでも当てはまる場合は、自動更新や通知処理を追加せず、タスク表を直してから再確認してください。

制限と安全上の注意

  • この無料コードは入力監査専用です。セルの更新、シート作成、通知、トリガー作成は行いません。
  • タスクの優先順位、担当人数、納期の妥当性までは判断しません。
  • MAX_ROWSを超えた行は確認しません。最初は少量のサンプルで試してください。
  • LOG_TASK_NAMEをtrueにするとタスク名がログへ出る設計へ変更しやすくなりますが、通常はfalseのまま使ってください。
  • Google公式・Google公認の商品ではありません。

公開前チェックリスト

  • [ ] コピー環境で実行した
  • [ ] DRY_RUNがtrueのまま
  • [ ] issueCountが0
  • [ ] LOG_TASK_NAMEがfalse
  • [ ] セル更新、通知、削除、トリガー作成のコードを追加していない

まとめ

タスク管理を自動化する前に、前後関係が正しくつながっているかを確認すると、着手不能や完了扱いの誤りを減らせます。このコードならタスク表を変更せず、参照漏れ、自己依存、循環依存を先に見つけられます。

公開準備のタスク、確認項目、抜け漏れをまとめて管理したい方は、関連する有料Tipsも確認できます。

個人クリエイター公開準備チェックGAS Pro

GasおじのTips商品一覧を見る

GasおじのTips商品一覧

※学習・検証用の例です。実データへ適用する前にコピー環境で確認してください。Google公式・Google公認の商品ではありません。


あなたも記事の投稿・販売を
始めてみませんか?

Tipsなら簡単に記事を販売できます!
登録無料で始められます!

Tipsなら、無料ですぐに記事の販売をはじめることができます Tipsの詳細はこちら
 

この記事のライター

ツールおじ|GoogleアカウントをAIエージェント化

おう、Gasおじだ。 AIツールとSaaSに散々突っ込んで遠回りした結果、最後に強かったのは「今あるGoogleアカウント」だった。 今ではAIエージェントであらゆるタスクを自動化して無双してるぞ。 GAS、スプレッドシート、フォーム、Gmail、AIエージェントを組み合わせれば、個人でも小さな会社でも、手作業だらけの業務はかなり自動化できる。 ここでは机上のAI論じゃなく、現場で動くテンプレ、コード、手順だけを置いていくぞ。 高いツールを増やす前に、まずお前さんのGoogleアカウントを仕事するエージェントに変えようじゃないか。

このライターが書いた他の記事

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)
  • カレンダーの二重登録を作成前に発見|GASで予定候補をdry-run監査

  • Googleフォームの採点ミスを回答公開前に発見|GASで正解キーをdry-run監査

関連のおすすめ記事

  • USBメモリで持ち運べるLinux Mint 22.1と永続的な書き込み領域の作成法【Rufus だけで作成する方法を初心者向けに徹底解説】

    ¥500
    1 %獲得
    (5 円相当)
    SmartSenior

    SmartSenior

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化

  • 毎朝のToDoを区分・優先度・期限つきでまとめる Morning ToDo Mail Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化