タスクの循環依存を着手前に発見|GASで前後関係をdry-run監査
ツールおじ|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公認の商品ではありません。
