MAS-027: M365監査ログ連携→作業時間の自動推定
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-027 |
| カテゴリ | 外部連携・AI活用・FP&A |
| Phase | P3 |
| 優先度 | ★ |
| 所要時間 | 6-8時間 |
| 対象ファイル | 500_import/503_m365_importer.js(新規作成)200_data/202_repository.js(M365AuditLogRepository / TimesheetRepository を末尾に追記)100_config/101_sys_config.js(schemas に 2 エントリ追加・confSheet.appendRow にシステムキー登録 2 件追加・MENU_DEFINITION に 2 項目追加)000_infra/002_constants.js(SHEET_DEFAULTS に 1 エントリ追加) |
| 前提案件 | MAS-026(TDABC:時間主導型ABC。推定タイムシートを TDABC の入力源とする。仕様書完了/未実装) MAS-105(工数入力・PJ原価連携。手動工数入力の補完・代替対象。仕様書完了/未実装) |
| 後続案件 | MAS-039(ABC分析・未着手) |
目的
M365 の監査ログ(Outlook メール送受信・Teams 会議参加・SharePoint / OneDrive ファイル編集等の操作履歴)を Microsoft Graph API 経由で日次取得し、ログの文脈(件名・フォルダ名・参加者・ファイルパス)から LLM (Gemini) で どの PJ に関する作業か を推定する。連続操作の時間差分を「セッション」として束ね、PJ × 日付ごとの 推定作業時間(分) を算出することで、工数入力の手動オペレーションを補完または完全代替し、MAS-026 TDABC(時間主導型ABC)に渡す入力データを自動化する。
結果レコードは ステータス = "未確認" で書き出され、ユーザーが内容を確認して承認(または 手動修正時間(分) で上書き)するまでは TDABC に反映しない Human-in-the-Loop フローとする(CLAUDE.md プロダクトポリシー準拠)。
現在のコード
新規機能のため置き換える既存コードは無い。本機能で 再利用する 既存コード:
Env.geminiApiKey()—000_infra/001_env.jsL53-56(スクリプトプロパティGEMINI_API_KEYを取得)Constants.getParam(key, defaultVal)—000_infra/002_constants.jsL147-167(03_sys_paramsからのパラメータ取得。第 2 引数がnumberならば戻り値もNumber()で変換)Utils.getSheetByKey(key, fallbackName)—000_infra/004_utils.jsL40-50(01_sys_configの登録名称を引き、見つからなければフォールバック名で参照)Utils.parseDateToYmd(val)—000_infra/004_utils.jsL108-119(ISO 文字列・Date・YYYY/MM/DD・YYYY年MM月DD日等をYYYY-MM-DDに正規化)- Repository パターン —
200_data/202_repository.jsL19-101 のモジュールスコープヘルパーreadSheetAsDtos_(sheet)/writeDtosToSheet_(sheet, headers, dtos)(データ行を全置換)/appendDtosToSheet_(sheet, headers, dtos, lastRowCol)(末尾追記)/findLastRow_(sheet, colIndex) OrderRepository—200_data/202_repository.jsL107-146 の_getSheet/findAll/save/appendパターン(新 Repository の雛形として踏襲)Utils.logInfo/Utils.logError/Utils.toastResult(既存各 importer が使用)- 既存 importer のパターン:
500_import/501_cc_importer.jsL14-85(正規化・名寄せ・ヘルパー)/500_import/502_bank_importer.js・500_import/502_receipt_reader.js(外部取込 → シート書き込みの基本フロー)
MAS-026 の TimesheetRepository(421_tdabc_allocator.js 内定義・43_trn_timesheet 参照)とは 別シート として運用する:MAS-026 は MAS-105 からの 手動入力 工数、MAS-027 は AI 自動推定 工数。MAS-105 / MAS-027 どちらか(または両方)を TDABC の入力源にするかは「人間が検討すべき事項」で整理する。
修正方針
全体データフロー(テキスト)
[Microsoft Graph API /auditLogs/directoryAudits, /auditLogs/signIns, /me/messages, /me/events]
│ UrlFetchApp.fetch + OAuth2 Bearer Token
▼
[50_log_m365_audit] — 生ログ保管(複合キー重複スキップ)
│ Gemini API (Env.geminiApiKey())
│ UrlFetchApp.fetch POST /v1beta/models/gemini-*/generateContent
▼
[PJ推定結果を 50_log_m365_audit の 「関連PJ(AI推定)」列に書き戻し]
│ セッション区切り(CFG_M365_SESSION_TIMEOUT_MINUTES 分)で連続操作を束ね
│ Math.max(0, timeDiff) で時間差分を集計
▼
[37_wrk_timesheet] — 日付 × ユーザー × PJ の推定作業時間(「未確認」で書き出し)
│ (Human-in-the-Loop)ユーザーが 37_wrk_timesheet を目視確認・手動修正
▼
[F-26 TDABC / F-39 ABC] の入力データとして参照
新規作成ファイル
500_import/503_m365_importer.js
500_import/ 配下の既存番号は 501 / 502 / 502_receipt_reader までが使用済みのため、次番 503 を使用(CLAUDE.md 「GAS ファイル番号体系」節に準拠)。
公開関数(メニューから起動):
importM365AuditLogs()— Graph API から監査ログを取得 →50_log_m365_auditへ複合キー重複スキップで追記estimateM365PjAndTimesheet()—50_log_m365_auditの未推定行に対し Gemini で PJ 推定 → セッション集計 →37_wrk_timesheetへ日付別 DELETE-INSERT
モジュール内部ヘルパー(ファイルローカル):
fetchGraphAuditLogs_(sinceYmd, untilYmd)—UrlFetchApp.fetch()を使用。OAuth2 Bearer Token の取得方式は 要調査 (後述 1-E-5)buildAuditLogDedupeKey_(userId, opTs, opContent, targetObj)—${userId}|${opTs}|${opContent}|${targetObj}の複合キーを返すcallGeminiForPjInference_(apiKey, promptPayload)— Gemini API をUrlFetchApp.fetch()で呼び出し、応答 JSON からPJ名を抽出。推定不可時は'未分類'を返すgroupLogsIntoSessions_(logs, sessionTimeoutMin)— 同一ユーザーIDで操作日時昇順ソート後、直前操作との差分がsessionTimeoutMin分を超えた時点でセッションを区切る。連続操作の時間差(分、Math.max(0, ...)でガード)を集計aggregateByDateUserProject_(sessions)—対象日 × ユーザーID × PJ名単位で推定作業時間(分)を合算
新規 DDL シート定義(2 件)
100_config/101_sys_config.js の setupAllSchemas 関数内(L826-901 の schemas オブジェクト)に以下を追加:
(a) LOG_M365 — 監査ログ生データ
| 列 | ヘッダー名 | 備考 |
|---|---|---|
| A | 取得日時 | YYYY-MM-DD HH:mm:ss(ジョブ実行時刻) |
| B | ユーザーID | Graph API userPrincipalName or id |
| C | 操作日時 | YYYY-MM-DD HH:mm:ss(Graph API の activityDateTime) |
| D | 操作内容 | MailSent / FileAccessed / TeamsMeetingJoin 等の操作種別 |
| E | 対象オブジェクト | メール件名・ファイルパス・会議名等のコンテキスト |
| F | 関連PJ(AI推定) | Gemini 推定結果(14_mst_project.プロジェクト名 と照合。特定不可時は "未分類") |
| G | 処理ステータス | 未推定 / 推定済 / 集計済 |
物理シート名(フォールバック): 50_log_m365_audit。500_import/ レイヤー対応で 50 番台を採用。90_log_m365_audit は避ける(CLAUDE.md「DDL で管理されないタブ」で 9X 番台が動的生成タブ用として予約されており、90_test_results と番号衝突する)。
(b) WRK_TMSH — 推定タイムシート
| 列 | ヘッダー名 | 備考 |
|---|---|---|
| A | 対象日 | YYYY-MM-DD |
| B | ユーザーID | Graph API userPrincipalName |
| C | PJ名 | 14_mst_project.プロジェクト名(推定不可時は "未分類") |
| D | 推定作業時間(分) | セッション集計後の整数分 |
| E | ステータス | 未確認 / 確認済 |
| F | 手動修正時間(分) | ユーザーが確認時に上書きした場合の値(空なら 推定作業時間(分) を使用) |
| G | 備考 | 補足メモ欄(任意入力) |
物理シート名(フォールバック): 37_wrk_timesheet。wrk_ プレフィックスは 3X 番台の既存グループ(31_wrk_order / 32_wrk_invoice / 33_wrk_bank / 34_wrk_card / 35_wrk_receipt / 36_wrk_bank_import)に揃える。71 番台は 71_bs と衝突するため不可。MAS-026 の 43_trn_timesheet(MAS-105 の手動入力工数)とも別シートとして並立させる。
Repository 追記(200_data/202_repository.js)
既存 OrderRepository と同じ _getSheet / findAll / save / append パターンで末尾に追記。
// =====================================================================
// M365AuditLogRepository — 50_log_m365_audit
// =====================================================================
var M365AuditLogRepository = {
_getSheet: function() { return Utils.getSheetByKey('LOG_M365', '50_log_m365_audit'); },
findAll: function() { return readSheetAsDtos_(M365AuditLogRepository._getSheet()); },
save: function(dtos) { /* headers 取得 → writeDtosToSheet_ */ },
append: function(dtos) { /* headers 取得 → appendDtosToSheet_(..., 1) */ },
};
// =====================================================================
// TimesheetRepository — 37_wrk_timesheet
// =====================================================================
var TimesheetRepository = { /* key: 'WRK_TMSH', fallback: '37_wrk_timesheet' 同パターン */ };
内部ヘルパー readSheetAsDtos_ / appendDtosToSheet_ / writeDtosToSheet_ は既存のまま再利用する。新規ヘルパーは追加しない。
システムキー登録(01_sys_config シート)
setupAllSchemas 内の confSheet.appendRow ブロック(L773-823)に 2 行追加:
if (!existKeys.includes('LOG_M365')) confSheet.appendRow(['LOG_M365', '', '50_log_m365_audit', 'ログ_M365監査ログ生データ(作業時間自動推定)']);
if (!existKeys.includes('WRK_TMSH')) confSheet.appendRow(['WRK_TMSH', '', '37_wrk_timesheet', 'サブ元帳_推定タイムシート(F-27 AI自動推定)']);
システムキーを登録しないと Utils.getSheetByKey() が getSheetNameByKey() → フォールバック名参照に落ちるため、DDL 実行後のシート名変更が反映されなくなる。必ず confSheet.appendRow 側も同タイミングで追加すること。
SHEET_DEFAULTS 追記(000_infra/002_constants.js)
37_wrk_timesheet の新規行デフォルト値として、タイムシート側 のみ追加する(50_log_m365_audit は手動追加想定外のため SHEET_DEFAULTS 登録不要)。
{ pattern: '37_wrk_timesheet', prefix: 'TMS_', defaults: { 'ステータス': '未確認' }, _dynamic: { '対象日': 'now' } },
形式は既存エントリ(L73-87)と同じ { pattern, prefix, defaults } オブジェクト配列。失敗パターン #18(SHEET_DEFAULTS の型誤認)を回避するため型を厳守。prefix: 'TMS_' は ID_PREFIX_MAP への追記は不要(タイムシートは ID 列を持たないため)。
セッションタイムアウトの取得
var sessionTimeoutMin = Constants.getParam('CFG_M365_SESSION_TIMEOUT_MINUTES', 30);
03_sys_params 未設定時は第 2 引数 30(分)をデフォルトとして返す(Constants.getParam L164-167 の規約:defaultVal が number のとき Number() で型変換される)。初回運用時の手動追加は「動作確認手順」に記載。
日付別 DELETE-INSERT の実装(estimateM365PjAndTimesheet())
writeDtosToSheet_(sheet, headers, dtos) は 全データ行を全置換(L38-59)であるため、日付別の差分更新は以下の手順で実現する:
TimesheetRepository.findAll()→{ headers, dtos }を取得var kept = dtos.filter(function(d) { return d['対象日'] !== targetYmd; });で対象日レコードを除外- 新規セッション集計結果(対象日分のみ)を
kept.concat(newDtos)で結合 TimesheetRepository.save(結合後DTO配列)で全置換
これにより、同一日付を 2 回以上処理しても結果は常に上書き(冪等性)となる。
LLM 呼び出し(PJ 推定)
var apiKey = Env.geminiApiKey();
if (!apiKey) { SpreadsheetApp.getUi().alert('GEMINI_API_KEY がスクリプトプロパティに設定されていません'); return; }
var url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=' + apiKey;
var payload = { contents: [{ parts: [{ text: promptText }] }] };
var res = UrlFetchApp.fetch(url, { method: 'post', contentType: 'application/json', payload: JSON.stringify(payload), muteHttpExceptions: true });
muteHttpExceptions: trueで HTTP エラー時も処理継続(1 件失敗で全体停止しない)- API キー漏洩防止のためログ出力時は
apiKey.substring(0,6) + '...'でマスク - プロンプトには
14_mst_projectの「プロジェクト名」一覧を含め、該当なしは"未分類"を返す指示を明記(初期バージョンは固定プロンプト、精度改善は別案件)
Graph API 認証(要調査項目)
本仕様書作成時点では docs/dev/dev_mas-155_*.md は存在しない。Graph API OAuth2 トークン取得方式は以下 3 案から未確定:
- 案①: GAS OAuth2 ライブラリ(
1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF)経由で Authorization Code Flow を実装(ユーザー初回のみブラウザ許可) - 案②: Azure AD アプリ登録 → Client Credentials Grant で取得したアクセストークンをスクリプトプロパティに手動保存(トークン有効期限 60 分のため定期更新ジョブ必要)
- 案③: Azure AD Refresh Token をスクリプトプロパティに保存 → リクエスト都度
UrlFetchApp.fetch()で新規アクセストークン取得(案①②のハイブリッド)
Phase 1 時点で既存コードに Graph API 呼び出しの前例がないことを確認済み(UrlFetchApp.fetch の呼び出しは Gemini / OCR 系のみ)。実装前にユーザー判断が必要(「人間が検討すべき事項」参照)。
メニュー追加
100_config/101_sys_config.js の Constants.MENU_DEFINITION(000_infra/002_constants.js L206-324)の 既存実在カテゴリ 📋 サイドバー: 🔍 消込・マッチング(L270-280)の末尾に以下 2 項目を追加(造語禁止。Phase 1 で確認した 📋 サイドバー: 🔍 消込・マッチング カテゴリは importBankStatement 等の外部取込系が集約されているため適合):
{ label: '📧 M365監査ログ取得', funcName: 'importM365AuditLogs', description: 'Graph APIからM365監査ログを取得し50_log_m365_audit に追記(複合キー重複スキップ)' },
{ label: '🧠 M365→PJ推定・工数集計', funcName: 'estimateM365PjAndTimesheet', description: '未推定ログをGeminiでPJ推定→セッション区切りで37_wrk_timesheetに日付別DELETE-INSERT' },
影響範囲
| 対象 | 影響内容 |
|---|---|
500_import/503_m365_importer.js | 新規作成(~400-500 行見積)。既存 500_import/* には手を入れない |
200_data/202_repository.js | 末尾に M365AuditLogRepository / TimesheetRepository を追記(約 60 行)。既存 Repository の定義は変更しない |
100_config/101_sys_config.js | schemas に LOG_M365 / WRK_TMSH 2 エントリ追加(約 6 行)、confSheet.appendRow 呼び出しを 2 行追加、setValiGroup_ 系のバリデーション追加は不要 |
000_infra/002_constants.js | SHEET_DEFAULTS に 37_wrk_timesheet 1 エントリ追加、MENU_DEFINITION の 📋 サイドバー: 🔍 消込・マッチング に 2 項目追加 |
03_sys_params | 手動で CFG_M365_SESSION_TIMEOUT_MINUTES = 30 を追加(動作確認手順に記載) |
appsscript.json | oauthScopes に https://graph.microsoft.com/.default(または使用個別スコープ AuditLog.Read.All / Mail.Read / Calendars.Read)と https://www.googleapis.com/auth/script.external_request を追加 |
| 既存機能 | なし(新規追加のみ) |
注意事項
writeDtosToSheet_は全行置換(200_data/202_repository.jsL38-59)。日付別差分更新は「findAll()→filterで対象日除外 → 新規結合 →save()」の 4 手順で実現すること。部分更新目的でappend()を使うと重複が混入するSheet.getLastColumn()で列範囲を動的取得しない(失敗パターン #21: YoY 差異列混入)。LOG_M365は 7 列、WRK_TMSHは 7 列の固定ハードコードを使用YYYY-MM形式の文字列をセルに書き込む際は apostrophe 前置(setValue("'2026-03"))またはsetNumberFormat('@')を使用(失敗パターン #23: Sheets が-を減算として誤パースする)。本仕様書ではタイムシートの「対象日」をYYYY-MM-DDのみ使用するため直接該当しないが、将来YYYY-MM列を追加する場合は必須- ラベル正規化は
.trim()のみでは不十分(失敗パターン #22: 全角スペース残存)。.replace(/[\s ]+/g, '')を14_mst_project.プロジェクト名と LLM 推定結果の突合に使用する Utils.parseDateToYmd()で操作日時を正規化してから日付比較すること(失敗パターン #17:Date文字列ソート崩れ)。Graph API が返す ISO 8601 タイムスタンプ(2026-04-20T10:15:30Z)を素朴に文字列比較するとタイムゾーン差で崩れる- 新シートのシステムキー
LOG_M365/WRK_TMSHを01_sys_configに登録しないとUtils.getSheetByKey()がフォールバック名でのみ解決する。DDL 実行直後に01_sys_configの内容を目視確認する - Gemini API キーをログに平文出力しない(
Env.geminiApiKey()の戻り値)。デバッグ出力時はapiKey.substring(0,6) + '...'でマスクする - 監査ログ(
50_log_m365_audit)はシート行数が月次で数千〜数万行に膨らむ可能性がある。Graph API 側で取得期間をsinceYmd/untilYmdで絞る。GAS の 6 分実行時間制限に配慮し、1 回のバッチでの処理件数に上限(例: 2000 件)を設けて時間切れ前に中断する - タイムシート(
37_wrk_timesheet)は GAS シートの行単位閲覧制御が困難。他ユーザーの推定作業時間が同一シート上で見える運用リスクがあり(「人間が検討すべき事項」で対策方針を確認)
エッジケース
| 条件 | 動作 | 理由 |
|---|---|---|
| LLM が PJ を特定できない(マスタ未登録/コンテキスト不足) | PJ名 = "未分類" として集計し、レコードは作成する | 推定不可ログも作業時間として計上し、総供給時間の漏洩を防ぐ |
| 対象日にログが存在しないユーザー | レコードを作成しない(0 分レコードは書き出さない) | 存在証明できないデータを生成しない |
| 時間差がマイナスになる(タイムスタンプ順序異常・タイムゾーン差) | Math.max(0, timeDiff) でガード | 負の作業時間は物理的に無意味 |
| Graph API から同一ログが重複取得された(ページネーション再試行等) | 操作日時 + ユーザーID + 操作内容 + 対象オブジェクト の複合キーでハッシュ比較し、重複はスキップ | 二重計上防止 |
CFG_M365_SESSION_TIMEOUT_MINUTES が 03_sys_params 未設定 | Constants.getParam('CFG_M365_SESSION_TIMEOUT_MINUTES', 30) のデフォルト値 30 分を使用 | 設定なしでも動作を保証(初回運用向け) |
手動修正時間(分) が入力済み | MAS-026 TDABC 等の後続処理では 手動修正時間(分) を正として使用(推定作業時間(分) より優先) | Human-in-the-Loop 原則(ユーザー確定値が AI 推定値より優先) |
ステータス = "未確認" のレコード | MAS-026 TDABC 等の後続処理は ステータス = "確認済" レコードのみを入力とする/全件を入力とするかは実装時に選択 | Human-in-the-Loop 原則(「人間が検討すべき事項」で最終確定) |
| 同日に複数回バッチが実行された(再実行・手動再試行) | 日付別 DELETE-INSERT により冪等。同日の結果は常に最新値で上書き | 夜間バッチの再実行を安全にする |
| Graph API のアクセストークンが期限切れ | 401 応答を検知して SpreadsheetApp.getUi().alert で再認証を促し処理中断 | 部分書き込みを避ける |
| Gemini API のレート制限 / タイムアウト | muteHttpExceptions: true で HTTP エラーを捕捉し、失敗行は 処理ステータス = "未推定" のまま 備考 列に失敗理由を記録 | 1 件失敗で全体停止しない |
| 1 回のバッチで GAS の 6 分実行時間制限に抵触 | 処理件数上限(例: 2000 件)で early return。残件は次回バッチで継続処理 | タイムアウトで中途半端な書き込みを回避 |
14_mst_project に登録されていない PJ 名を LLM が出力 | .replace(/[\s ]+/g, '') で正規化後に照合し、一致しなければ "未分類" に丸める | マスタ外の科目名を生成させない(CLAUDE.md 会計ロジック規約) |
| Graph API の返値にユーザー特定不可能なシステムアカウント操作が混ざる | ユーザーID が system / svc_* 等の場合は取り込み対象外とする | プライバシー配慮+集計対象を人間オペレーションに限定 |
実データ検証
実装前に MCP または GAS エディタで以下をファクトチェックする。仕様書の前提条件が崩れていないか確認する目的:
03_sys_paramsシートにCFG_M365_SESSION_TIMEOUT_MINUTESキーが存在するか。初回は DDL 実行後にユーザーが手動追加する必要がある(動作確認手順に記載)。既存キー一覧と衝突しないことを確認01_sys_configにLOG_M365/WRK_TMSHが登録されているか。setupAllSchemasのconfSheet.appendRowブロックで登録されるため、DDL 実行後に目視確認するappsscript.jsonのoauthScopesに Graph API の必要スコープ(AuditLog.Read.All/Mail.Read/Calendars.Readのいずれか、または.default集約スコープ)とhttps://www.googleapis.com/auth/script.external_requestが含まれているかEnv.geminiApiKey()が返す値(= スクリプトプロパティGEMINI_API_KEY)が dev・prod 両環境に設定されているか。未設定時はestimateM365PjAndTimesheet()がダイアログで警告して中断すること14_mst_projectのプロジェクト名列の実在値一覧を確認(LLM プロンプトに埋め込む PJ 選択肢)。有効フラグ = TRUEかつステータス != '完了'のレコードのみ対象とするか、全件対象とするかを実装時に決定
関連ドキュメント
| 仕様書 | 関連箇所 |
|---|---|
| CLAUDE.md — プロジェクトルール | GAS ファイル番号体系・DDL 非管理タブ・Human-in-the-Loop ポリシー・会計ロジック規約 |
| dev_mas-026_tdabc_cost_allocation.md | 後続処理(TDABC)での推定タイムシート参照方法。MAS-026 は 43_trn_timesheet(MAS-105 の手動入力工数)を前提としているが、MAS-027 の 37_wrk_timesheet(AI 推定工数)を代替/補完するかは「人間が検討すべき事項」で整理 |
| dev_mas-105_workforce_costing.md | 工数入力 UI パターン・70_bud_resource の工数(h)入力設計。手動入力 vs AI 推定のすみ分け方針 |
| MAS-155(Outlook 連携の想定案件) | 本仕様書作成時点では docs/dev/dev_mas-155_*.md は未作成。Graph API OAuth2 認証フローの確立は本案件内で実装する必要がある |
| TODO_future.md — MAS-027 行 | 案件定義・人間検討事項 |
| failure_patterns.md | #17 Date 文字列ソート崩れ / #18 SHEET_DEFAULTS 型誤認 / #21 getLastColumn 過剰列取得 / #22 全角スペース残存 / #23 YYYY-MM 減算パース |
人間が検討すべき事項
TODO_future.md MAS-027 行の原文:M365 テナントの監査ログ保持期間(E3 以上で 180 日)。推定精度の検証方法。プライバシーへの配慮(個人の活動を細かく追跡することへの抵抗感)。
これに加えて仕様書作成時に特定した判断事項:
- Graph API 認証フローの実装方式の選定 案①(GAS OAuth2 ライブラリによる Authorization Code Flow)・案②(Azure AD Client Credentials + スクリプトプロパティ保存)・案③(Refresh Token ハイブリッド)のいずれを採用するか。Phase 1 で既存コードに前例がないことを確認済み。実装前にユーザー判断が必要。Azure AD アプリ登録(テナント管理者権限)と BizLP 組織内の IT 承認フローが関連する
- M365 監査ログの取得スコープと保持期間 Outlook メール・Teams 会議・SharePoint/OneDrive のどの操作を取得対象とするか。E3 以下のテナントでは監査ログ保持期間が 90 日のため、取得漏れを防ぐ夜間バッチの実行頻度(日次/週次)を確定する必要がある
- プライバシー配慮とデータ可視性
37_wrk_timesheetは GAS シートの行単位閲覧制御が困難で、他ユーザーの推定作業時間が見える可能性がある。対策候補:(a) 全員分を 1 シートに書き出す現案を許容、(b) 個人別スプレッドシートに分離出力、(c) データはシートに書かずPropertiesService.getUserProperties()に個人別保管。運用ポリシー確認が必要 - 推定精度の検証方法 初期ベースラインの精度測定方法(例: 手動工数入力 MAS-105 と並走運用して 1 ヶ月の乖離率を測定)と、許容精度の閾値(例: PJ 別月次合計で ±20% 以内)を確定する必要がある
- LLM の PJ 推定プロンプト設計
プロンプト品質が推定精度に直結する。初期バージョンは固定プロンプト(
14_mst_project.プロジェクト名一覧+コンテキスト変数)とし、精度改善のプロンプトチューニングは別案件で反復改善する方針としたい。ユーザー確認が必要 - MAS-105(手動工数入力)と MAS-027(AI 推定工数)のすみ分け
両方を並行運用する(補完)か、MAS-027 で MAS-105 を完全代替するか。TDABC(MAS-026)が参照する入力源は
43_trn_timesheet(MAS-105)と37_wrk_timesheet(MAS-027)のどちらか/両方の合算か、いずれを採用するかを決定する ステータス = "未確認"レコードを TDABC に流すか MAS-026 TDABC が MAS-027 タイムシートを入力源とする場合、ステータス = "確認済"行のみに限定するか、未確認も含めて暫定値として使うか。未確認を含めると自動化メリットが大きいが、Human-in-the-Loop 原則とトレードオフ- Gemini 呼び出しコストと API 選択
日次数千ログ × PJ 推定を毎日走らせる想定で、Gemini の月次コスト試算が必要(
gemini-2.0-flashの単価基準)。コスト面で問題があれば、まとめ推定(1 プロンプトに複数ログを詰める)・ルールベース事前フィルタ(件名に PJ コードが含まれる場合は LLM スキップ)等の最適化を検討
実装プロンプト(Claude Code 用)
別セッションでも自己完結して着手できるよう、前提情報と手順をすべて記載する。コードブロック記法(バッククォート)は使わず、行頭スペース 4 つでインデント表示する。
あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者です。
案件 MAS-027「M365監査ログ連携→作業時間の自動推定」を実装してください。
【実行前タスク — 1-A〜1-E すべてを調査してから実装に進むこと】
1-A. docs/_internal/TODO_future.md を Grep し、MAS-027 / MAS-026 / MAS-105 行を読む。特に MAS-026 の 43_trn_timesheet と MAS-027 の 37_wrk_timesheet のすみ分け、人間検討事項の未確定項目(Graph API 認証方式・MAS-105 との代替 or 補完)を把握する。
1-B. 100_config/101_sys_config.js を Read し、以下を確認する:
- setupAllSchemas の schemas オブジェクト(L826-901)への新シート追加パターン(列ヘッダー配列のフォーマット・color 指定・validations 指定)
- confSheet.appendRow(L773-823)へのシステムキー登録パターン
- onOpen()(L323-349)は Constants.MENU_DEFINITION をループしてメニューを動的生成していること(メニュー追加は 002_constants.js 側)
- 既存の実在するサイドバーカテゴリ名(造語禁止): 「📋 サイドバー: 🔍 消込・マッチング」「📋 サイドバー: 📊 マート更新」「📋 サイドバー: ⚙️ メンテナンス」等
1-C. 200_data/202_repository.js を Read し、以下を把握する:
- readSheetAsDtos_(sheet) → { headers, dtos }(L19-29)
- writeDtosToSheet_(sheet, headers, dtos)(L38-59、全行置換)
- appendDtosToSheet_(sheet, headers, dtos, lastRowCol)(L85-101、末尾追記)
- findLastRow_(sheet, colIndex)(L68-74)
- OrderRepository の _getSheet / findAll / save / append パターン(L107-146)を新 Repository の雛形として踏襲する
1-D. 000_infra/001_env.js を Read し、Env.geminiApiKey() がスクリプトプロパティ GEMINI_API_KEY を参照していることを確認する(L53-56)。dev/prod 両環境に API キーが設定済みか GAS エディタで確認する。
1-E. 000_infra/002_constants.js を Read し、以下を確認する:
- SHEET_DEFAULTS(L73-87)の要素形式: 必ず { pattern, prefix, defaults } オブジェクト配列(失敗パターン #18 回避)
- ID_PREFIX_MAP(L93-112)の形式
- Constants.getParam(key, defaultVal)(L147-167): 第 2 引数が number なら戻り値も Number() 変換
- MENU_DEFINITION(L206-324)の既存カテゴリ「📋 サイドバー: 🔍 消込・マッチング」(L270-280)末尾への項目追加箇所
000_infra/004_utils.js を Read し、以下を確認する:
- Utils.getSheetByKey(key, fallbackName)(L40-50)
- Utils.parseDateToYmd(val)(L108-119)
- Utils.parseDateToYm(val)(L92-99)
500_import/501_cc_importer.js(または 500_import/502_bank_importer.js)を Read し、外部取込→シート書き込みの既存パターン(UrlFetchApp.fetch の使い方・エラーハンドリング・冪等性確保)を把握する。
【修正対象ファイル】
- 500_import/503_m365_importer.js(新規作成。次番 503 を使用。Graph API 取得+Gemini PJ 推定+セッション区切り集計+日付別 DELETE-INSERT)
- 200_data/202_repository.js(末尾に M365AuditLogRepository(key: LOG_M365, fallback: 50_log_m365_audit)と TimesheetRepository(key: WRK_TMSH, fallback: 37_wrk_timesheet)を追記)
- 100_config/101_sys_config.js(schemas に LOG_M365 / WRK_TMSH 2 エントリ追加、confSheet.appendRow 2 行追加)
- 000_infra/002_constants.js(SHEET_DEFAULTS に 37_wrk_timesheet の 1 エントリ追加、MENU_DEFINITION の「📋 サイドバー: 🔍 消込・マッチング」に 2 項目追加)
- appsscript.json(oauthScopes に https://graph.microsoft.com/.default 相当と https://www.googleapis.com/auth/script.external_request を追加)
【制約】
- 既存 Repository(OrderRepository / InvoiceRepository / JournalRepository 等)の実装を変更しない
- Sheet.getLastColumn() で列範囲を動的取得しない。LOG_M365 は 7 列・WRK_TMSH は 7 列の固定ハードコード(失敗パターン #21 回避)
- ラベル正規化は .trim() のみ使用せず .replace(/[\s ]+/g, '') を併用(失敗パターン #22 回避)
- YYYY-MM 文字列をセルに書く場合は apostrophe 前置または setNumberFormat('@') 使用(失敗パターン #23 回避)
- Utils.parseDateToYmd() で日付正規化してから Date 比較(失敗パターン #17 回避)
- SHEET_DEFAULTS への追加は { pattern, prefix, defaults } オブジェクト形式を厳守(失敗パターン #18 回避)
- 科目マスタに未登録の PJ 名を生成しない。LLM 推定結果が 14_mst_project.プロジェクト名 に無い場合は「未分類」に丸める(CLAUDE.md 会計ロジック規約)
【実装内容】
仕様書「## 修正方針」セクションの全項目を実装する。特に以下の点に留意すること:
- 日付別 DELETE-INSERT:
var all = TimesheetRepository.findAll();
var kept = all.dtos.filter(function(d) { return d['対象日'] !== targetYmd; });
var merged = kept.concat(newDtos);
TimesheetRepository.save(merged);
- セッションタイムアウト:
var sessionTimeoutMin = Constants.getParam('CFG_M365_SESSION_TIMEOUT_MINUTES', 30);
- LLM 呼び出し:
var apiKey = Env.geminiApiKey();
if (!apiKey) { SpreadsheetApp.getUi().alert('GEMINI_API_KEY 未設定'); return; }
var url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=' + apiKey;
UrlFetchApp.fetch(url, { method: 'post', contentType: 'application/json', payload: JSON.stringify(payload), muteHttpExceptions: true });
// ログ出力時は apiKey.substring(0,6) + '...' でマスク
- 複合キー重複スキップ:
var key = [opTs, userId, opContent, targetObj].join('|');
// findAll() で既存キー集合を構築 → Set に格納 → 新規ログの複合キーが含まれればスキップ
- 新 Repository の _getSheet:
_getSheet: function() { return Utils.getSheetByKey('LOG_M365', '50_log_m365_audit'); }
_getSheet: function() { return Utils.getSheetByKey('WRK_TMSH', '37_wrk_timesheet'); }
- 新シート DDL 追加(101_sys_config.js の schemas オブジェクト内):
'LOG_M365': { headers: ["取得日時","ユーザーID","操作日時","操作内容","対象オブジェクト","関連PJ(AI推定)","処理ステータス"], color: "#434343" },
'WRK_TMSH': { headers: ["対象日","ユーザーID","PJ名","推定作業時間(分)","ステータス","手動修正時間(分)","備考"], color: "#674ea7" },
- システムキー登録(101_sys_config.js の confSheet.appendRow ブロック内):
if (!existKeys.includes('LOG_M365')) confSheet.appendRow(['LOG_M365', '', '50_log_m365_audit', 'ログ_M365監査ログ生データ(作業時間自動推定)']);
if (!existKeys.includes('WRK_TMSH')) confSheet.appendRow(['WRK_TMSH', '', '37_wrk_timesheet', 'サブ元帳_推定タイムシート(MAS-027 AI自動推定)']);
- Graph API 認証方式は仕様書「人間が検討すべき事項」#1 の 3 案(①GAS OAuth2 ライブラリ / ②Client Credentials + スクリプトプロパティ / ③Refresh Token ハイブリッド)からユーザーに採用案を確認してから実装する。未確定の場合は Client Credentials + スクリプトプロパティ保存方式(案②)でスタブ実装し、TODO コメントで明示する
【エッジケース】
仕様書「## エッジケース」テーブルをそのまま転記し、全項目を実装時にカバーする:
- LLM が PJ 特定不可 → 「未分類」で集計
- 対象日ログなしユーザー → レコード作成しない
- 時間差マイナス → Math.max(0, timeDiff)
- 同一ログ重複 → 複合キー(操作日時+ユーザーID+操作内容+対象オブジェクト)でスキップ
- CFG_M365_SESSION_TIMEOUT_MINUTES 未設定 → デフォルト 30 分
- 手動修正時間入力済み → 手動修正時間を優先(後続処理で)
- 未確認レコード → TDABC が参照するか「人間が検討すべき事項」#7 で確定
- 同日再実行 → 日付別 DELETE-INSERT で冪等
- アクセストークン期限切れ → 401 検知で処理中断
- Gemini レート制限 → muteHttpExceptions で捕捉、失敗行は 備考 列に失敗理由記録
- 6 分実行制限 → 処理件数上限(2000 件)で early return
- 14_mst_project 未登録 PJ 名を LLM 出力 → 正規化後照合し「未分類」に丸め
- システムアカウント(system/svc_*)操作 → 取り込み対象外
【動作確認手順】
1. dev 環境の 03_sys_params に CFG_M365_SESSION_TIMEOUT_MINUTES = 30 を手動追加する
2. dev 環境のスクリプトプロパティに GEMINI_API_KEY が設定されていることを確認する
3. appsscript.json の oauthScopes を更新
4. npm run push:dev でデプロイ
5. GAS エディタで setupAllSchemas を実行し、50_log_m365_audit / 37_wrk_timesheet シートが作成され、01_sys_config に LOG_M365 / WRK_TMSH が登録されていることを確認
6. スプレッドシートを再読込(メニュー再構築)
7. サイドバー「📋 🔍 消込・マッチング」→「📧 M365監査ログ取得」を実行し、50_log_m365_audit に監査ログが追記されていることを確認
8. 「🧠 M365→PJ推定・工数集計」を実行し、50_log_m365_audit の「関連PJ(AI推定)」「処理ステータス」が更新され、37_wrk_timesheet に推定レコードが書き出されていること(ステータス列 = "未確認")を確認
9. 同じ日付を対象として再度バッチを実行し、37_wrk_timesheet のレコード件数が重複していない(日付別 DELETE-INSERT 冪等性)ことを確認
10. 「手動修正時間(分)」列に値を入力し、「ステータス」を "確認済" に変更。後続 MAS-026 TDABC(未実装時はスキップ)が手動修正値を優先することを確認
11. 900_test/901_test_runner.js で既存テストが壊れていないことを確認
12. npm run push:prod は実施しない(本番反映はユーザー判断)
【完了後のコミット】
git add 500_import/503_m365_importer.js 200_data/202_repository.js 100_config/101_sys_config.js 000_infra/002_constants.js appsscript.json
git commit -m "feat(MAS-027): add M365 audit log importer with AI worktime estimation"
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read/Grep) | あり | 既存パターンと固有名詞の確定 |
| コード実装 | 最小限 | 仕様書で確定済みの内容の書き下しに徹する |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
100_config/101_sys_config.js への DDL 追加・confSheet.appendRow 追加 | Claude Sonnet | 既存パターン踏襲。挿入位置は仕様書で明示済みだが、schemas オブジェクト末尾への安全な追加位置の判断が必要 |
000_infra/002_constants.js への SHEET_DEFAULTS / MENU_DEFINITION 追記 | Claude Sonnet | 挿入位置の特定・既存パターンの適用。中程度の判断 |
200_data/202_repository.js 末尾への M365AuditLogRepository / TimesheetRepository 追加 | Claude Sonnet | 既存 OrderRepository のコピーベース実装。中程度の判断 |
500_import/503_m365_importer.js の新規作成(Graph API 認証・LLM 呼び出し・セッション集計) | Claude Opus | 外部 API 設計(OAuth2 方式選定)・複数ファイル横断の判断(Repository 呼び分け・Env 参照・失敗パターン回避)が必要 |
appsscript.json oauthScopes 追加 | Claude Haiku | 追記位置・値が仕様書で明示済み、判断要素少 |
変更履歴
| 日時 | 変更内容 |
|---|---|
| 2026-04-21 | 初版作成。M365 Graph API → 監査ログ取得(50_log_m365_audit)→ Gemini による PJ 推定 → セッション区切り(CFG_M365_SESSION_TIMEOUT_MINUTES、デフォルト 30 分)ベースの作業時間集計 → 37_wrk_timesheet への日付別 DELETE-INSERT(冪等性保証)フローを定義。500_import/503_m365_importer.js 新規作成、200_data/202_repository.js に M365AuditLogRepository / TimesheetRepository 追記、100_config/101_sys_config.js の schemas に LOG_M365 / WRK_TMSH 2 エントリ+ confSheet.appendRow 2 行追加、000_infra/002_constants.js の SHEET_DEFAULTS と MENU_DEFINITION(📋 サイドバー: 🔍 消込・マッチング カテゴリ)に追記。Graph API OAuth2 認証方式(①GAS OAuth2 ライブラリ / ②Client Credentials / ③Refresh Token)を未確定項目として明示。Human-in-the-Loop 確認フロー(ステータス 列 未確認/確認済 + 手動修正時間(分) 列)を設計。エッジケース 13 項目・人間検討事項 8 項目を定義 |
仕様書作成プロンプト
展開して表示
本仕様書は tasks/prompts/task_F-27.md の <instruction> タグ内の指示に従って作成された。
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. 拡張思考の使い分け: Phase 1(設計)ではフル活用し、ファイル名・エッジケース一覧・Step分割粒度・固有名詞(関数名/シート名/列名/行番号)を完全に確定させる。Phase 2(清書)の各Step内では拡張思考を最小限に抑え、Phase 1で確定した内容の書き下しに徹する。出力途中で再考しない。
2. テキスト報告の禁止: 「〜を作成します」等のtextのみでtool_useなしにturnを終了しない。説明は1文以内。直ちにtoolを呼ぶ。
3. 4-5分割のWrite/Edit実行: 2-1(骨格)/2-2(概要〜注意事項)/2-3a(エッジケース〜人間検討事項)/2-3b(実装プロンプト〜変更履歴)/2-4(<details>記録)に分割。1回のWrite/Editは約300行以内を目安にする。
4. 各Stepで何を書くかを具体指示: 設計判断をPhase 2実行時に持ち込まない。Phase 1で全固有名詞・行番号・構造を確定させてから清書に入る。
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 MAS-027「M365監査ログ連携→作業時間の自動推定」の開発仕様書を作成してください。
作成後、docs/_config.json の nav 配列の §E.6(パイプライン・RPA・外部連携)セクションにも必ず追記してください。
【Phase 1: 案件情報の収集】
1-A. 案件定義の読み込み
docs/_internal/TODO_future.md を検索し、MAS-027 の案件名・概要・人間が検討すべき事項を取得する。
1-B. プロジェクト規約の読み込み
CLAUDE.md を Read し、以下を精読する:
- GAS ファイル番号体系セクション(各ディレクトリと番号レンジの対応)
- DDL (setupAllSchemas) で管理されないタブセクション(90_test_results 等の予約済みシート名)
- マイグレーションスクリプト運用ガイドライン
1-C. 既存仕様書の読み込み
docs/dev/dev_mas-026_*.md(TDABC仕様書)、docs/dev/dev_mas-105_*.md(工数入力)、docs/dev/dev_mas-155_*.md(Outlook 連携。存在しなければ「未作成」と明記)。
1-D. 関連コードの調査(Grep→Read の順)
100_config/101_sys_config.js(setupAllSchemas / onOpen)、000_infra/002_constants.js(SHEET_DEFAULTS / ID_PREFIX_MAP / getParam)、200_data/202_repository.js(readSheetAsDtos_ / appendDtosToSheet_ / writeDtosToSheet_ / _getSheet パターン)、000_infra/001_env.js(geminiApiKey)、500_import/501_cc_importer.js または 502_bank_importer.js(取込パターン)、000_infra/004_utils.js(getSheetByKey / parseDateToYmd)を Read。
1-E. Phase 2開始前に以下の設計判断を全て確定
1. 新規GASファイル名: 500_import/503_m365_importer.js が適切か
2. 新規シートの番号・名称: 90_log_m365_audit vs 50_log_m365_audit、71_wrk_timesheet の番号整合性
3. システムキー文字列: LOG_M365 / WRK_TMSH
4. DELETE-INSERT 実装方式: findAll → filter → save 全置換
5. Graph API 認証方式: MAS-155 参照 or 要調査
【Phase 2: 仕様書の分割作成】
出力先: docs/dev/dev_mas-027_m365_worktime_estimation.md
Step 2-1: 骨格(Write、~20 行)
Step 2-2: 概要〜注意事項(~300 行)
Step 2-3a: エッジケース〜人間検討事項(~200 行)
Step 2-3b: 実装プロンプト〜変更履歴(~250 行)
Step 2-4: 仕様書作成プロンプトを details で記録
【Phase 3: 保存と記録】
3-B. docs/_config.json への追記: nav §E.6 に { "file": "dev/dev_mas-027_m365_worktime_estimation.md", "title": "E.6.X MAS-027 M365監査ログ連携→作業時間の自動推定" }
3-C. docs/_internal/changelog.md への追記(初版作成エントリ)
3-D. git add / commit / push