概要

項目内容
案件IDMAS-027
カテゴリ外部連携・AI活用・FP&A
PhaseP3
優先度
所要時間6-8時間
対象ファイル500_import/503_m365_importer.js(新規作成)
200_data/202_repository.jsM365AuditLogRepository / TimesheetRepository を末尾に追記)
100_config/101_sys_config.jsschemas に 2 エントリ追加・confSheet.appendRow にシステムキー登録 2 件追加・MENU_DEFINITION に 2 項目追加)
000_infra/002_constants.jsSHEET_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.js L53-56(スクリプトプロパティ GEMINI_API_KEY を取得)
  • Constants.getParam(key, defaultVal)000_infra/002_constants.js L147-167(03_sys_params からのパラメータ取得。第 2 引数が number ならば戻り値も Number() で変換)
  • Utils.getSheetByKey(key, fallbackName)000_infra/004_utils.js L40-50(01_sys_config の登録名称を引き、見つからなければフォールバック名で参照)
  • Utils.parseDateToYmd(val)000_infra/004_utils.js L108-119(ISO 文字列・DateYYYY/MM/DDYYYY年MM月DD日 等を YYYY-MM-DD に正規化)
  • Repository パターン — 200_data/202_repository.js L19-101 のモジュールスコープヘルパー readSheetAsDtos_(sheet) / writeDtosToSheet_(sheet, headers, dtos)(データ行を全置換)/ appendDtosToSheet_(sheet, headers, dtos, lastRowCol)(末尾追記)/ findLastRow_(sheet, colIndex)
  • OrderRepository200_data/202_repository.js L107-146 の _getSheet / findAll / save / append パターン(新 Repository の雛形として踏襲)
  • Utils.logInfo / Utils.logError / Utils.toastResult(既存各 importer が使用)
  • 既存 importer のパターン:500_import/501_cc_importer.js L14-85(正規化・名寄せ・ヘルパー)/500_import/502_bank_importer.js500_import/502_receipt_reader.js(外部取込 → シート書き込みの基本フロー)

MAS-026 の TimesheetRepository421_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.jssetupAllSchemas 関数内(L826-901 の schemas オブジェクト)に以下を追加:

(a) LOG_M365 — 監査ログ生データ

ヘッダー名備考
A取得日時YYYY-MM-DD HH:mm:ss(ジョブ実行時刻)
BユーザーIDGraph 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_audit500_import/ レイヤー対応で 50 番台を採用。90_log_m365_audit は避ける(CLAUDE.md「DDL で管理されないタブ」で 9X 番台が動的生成タブ用として予約されており、90_test_results と番号衝突する)。

(b) WRK_TMSH — 推定タイムシート

ヘッダー名備考
A対象日YYYY-MM-DD
BユーザーIDGraph API userPrincipalName
CPJ名14_mst_project.プロジェクト名(推定不可時は "未分類"
D推定作業時間(分)セッション集計後の整数分
Eステータス未確認 / 確認済
F手動修正時間(分)ユーザーが確認時に上書きした場合の値(空なら 推定作業時間(分) を使用)
G備考補足メモ欄(任意入力)

物理シート名(フォールバック): 37_wrk_timesheetwrk_ プレフィックスは 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 の規約:defaultValnumber のとき Number() で型変換される)。初回運用時の手動追加は「動作確認手順」に記載。

日付別 DELETE-INSERT の実装(estimateM365PjAndTimesheet()

writeDtosToSheet_(sheet, headers, dtos)全データ行を全置換(L38-59)であるため、日付別の差分更新は以下の手順で実現する:

  1. TimesheetRepository.findAll(){ headers, dtos } を取得
  2. var kept = dtos.filter(function(d) { return d['対象日'] !== targetYmd; }); で対象日レコードを除外
  3. 新規セッション集計結果(対象日分のみ)を kept.concat(newDtos) で結合
  4. 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.jsConstants.MENU_DEFINITION000_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.jsschemasLOG_M365 / WRK_TMSH 2 エントリ追加(約 6 行)、confSheet.appendRow 呼び出しを 2 行追加、setValiGroup_ 系のバリデーション追加は不要
000_infra/002_constants.jsSHEET_DEFAULTS37_wrk_timesheet 1 エントリ追加、MENU_DEFINITION📋 サイドバー: 🔍 消込・マッチング に 2 項目追加
03_sys_params手動で CFG_M365_SESSION_TIMEOUT_MINUTES = 30 を追加(動作確認手順に記載)
appsscript.jsonoauthScopeshttps://graph.microsoft.com/.default(または使用個別スコープ AuditLog.Read.All / Mail.Read / Calendars.Read)と https://www.googleapis.com/auth/script.external_request を追加
既存機能なし(新規追加のみ)

注意事項

  • writeDtosToSheet_ は全行置換200_data/202_repository.js L38-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_TMSH01_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_MINUTES03_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 の返値にユーザー特定不可能なシステムアカウント操作が混ざるユーザーIDsystem / svc_* 等の場合は取り込み対象外とするプライバシー配慮+集計対象を人間オペレーションに限定

実データ検証

実装前に MCP または GAS エディタで以下をファクトチェックする。仕様書の前提条件が崩れていないか確認する目的:

  • 03_sys_params シートに CFG_M365_SESSION_TIMEOUT_MINUTES キーが存在するか。初回は DDL 実行後にユーザーが手動追加する必要がある(動作確認手順に記載)。既存キー一覧と衝突しないことを確認
  • 01_sys_configLOG_M365 / WRK_TMSH が登録されているか。setupAllSchemasconfSheet.appendRow ブロックで登録されるため、DDL 実行後に目視確認する
  • appsscript.jsonoauthScopes に 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 日)。推定精度の検証方法。プライバシーへの配慮(個人の活動を細かく追跡することへの抵抗感)

これに加えて仕様書作成時に特定した判断事項:

  1. Graph API 認証フローの実装方式の選定 案①(GAS OAuth2 ライブラリによる Authorization Code Flow)・案②(Azure AD Client Credentials + スクリプトプロパティ保存)・案③(Refresh Token ハイブリッド)のいずれを採用するか。Phase 1 で既存コードに前例がないことを確認済み。実装前にユーザー判断が必要。Azure AD アプリ登録(テナント管理者権限)と BizLP 組織内の IT 承認フローが関連する
  2. M365 監査ログの取得スコープと保持期間 Outlook メール・Teams 会議・SharePoint/OneDrive のどの操作を取得対象とするか。E3 以下のテナントでは監査ログ保持期間が 90 日のため、取得漏れを防ぐ夜間バッチの実行頻度(日次/週次)を確定する必要がある
  3. プライバシー配慮とデータ可視性 37_wrk_timesheet は GAS シートの行単位閲覧制御が困難で、他ユーザーの推定作業時間が見える可能性がある。対策候補:(a) 全員分を 1 シートに書き出す現案を許容、(b) 個人別スプレッドシートに分離出力、(c) データはシートに書かず PropertiesService.getUserProperties() に個人別保管。運用ポリシー確認が必要
  4. 推定精度の検証方法 初期ベースラインの精度測定方法(例: 手動工数入力 MAS-105 と並走運用して 1 ヶ月の乖離率を測定)と、許容精度の閾値(例: PJ 別月次合計で ±20% 以内)を確定する必要がある
  5. LLM の PJ 推定プロンプト設計 プロンプト品質が推定精度に直結する。初期バージョンは固定プロンプト(14_mst_project.プロジェクト名 一覧+コンテキスト変数)とし、精度改善のプロンプトチューニングは別案件で反復改善する方針としたい。ユーザー確認が必要
  6. MAS-105(手動工数入力)と MAS-027(AI 推定工数)のすみ分け 両方を並行運用する(補完)か、MAS-027 で MAS-105 を完全代替するか。TDABC(MAS-026)が参照する入力源は 43_trn_timesheet(MAS-105)と 37_wrk_timesheet(MAS-027)のどちらか/両方の合算か、いずれを採用するかを決定する
  7. ステータス = "未確認" レコードを TDABC に流すか MAS-026 TDABC が MAS-027 タイムシートを入力源とする場合、ステータス = "確認済" 行のみに限定するか、未確認 も含めて暫定値として使うか。未確認を含めると自動化メリットが大きいが、Human-in-the-Loop 原則とトレードオフ
  8. 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.jsM365AuditLogRepository / TimesheetRepository 追記、100_config/101_sys_config.jsschemasLOG_M365 / WRK_TMSH 2 エントリ+ confSheet.appendRow 2 行追加、000_infra/002_constants.jsSHEET_DEFAULTSMENU_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