Newer
Older
client_dwatch / lib / fixtures / task-status-rule.ts
/**
 * ★`clean_task_reports.status` の決め方(分布と巡回列)★ — ★定義はここ1箇所だけ★
 *
 * ★何のためのファイルか★: この規則は次の2か所が使う。
 *   - `lib/fixtures/clean-task-reports.ts` … `CleanTaskReportFixture.status`(DBテーブルの形)
 *   - `lib/fixtures/clean-logs.ts` … `CleanLogItem.task_status`(API の形。指示書AQ §5-2)
 * ★2か所が同じ清掃 id に同じ値を出さないと、`USE_FIXTURES` の true/false で
 *   清掃結果列が食い違う★。以前は `clean-logs.ts` が規則の★写し★を持っていた。
 *
 * ★★このファイルは何も import しないこと★★ — 循環 import を断つためにある。
 *   `clean-task-reports.ts` は ★モジュール評価時に★ `clean-logs.ts` の `cleanLogs` / `history` を読む。
 *   `clean-logs.ts` が `clean-task-reports.ts` を直接 import すると、`clean-logs.ts` が先に読まれた場合
 *   (`@/lib/fixtures/clean-logs` を直接 import するテストが複数ある)に `history` が undefined のまま
 *   あちらの本体が走り、読み込み時に `TypeError: Cannot read properties of undefined (reading 'filter')`
 *   で落ちる(2026-09-30 担当J2 が実測)。★依存の無いこの葉モジュールを両方が import する形で解消した★。
 *
 * ★値の意味は第10回 N-2 で受領済み(表は `lib/config` の `CLEAN_TASK_STATUS_LABELS`)★だが、
 *   ここには「何番が何か」を書かない(表を2箇所に持たない)。
 *   6 は Pudu の値で、先方も意味が分からない(第11回 P-4。D-158)。分かれば先方がデータを入れる。数字のまま表示する。
 * ★推測で `CLEAN_ERROR` に対応づけない★(指示書AQ 1-2 / D-077)。
 *
 * 分布と巡回列の期待値は `lib/fixtures/__tests__/clean-task-reports.test.ts`、
 * 2か所の一致は `lib/fixtures/__tests__/areas.test.ts` で固定している。
 */

/**
 * ★CSV実測189行の `status` 分布★(出所: ユーザー提供CSV `clean_task_reports.csv` 189行。2026-09-30 受領)。
 * 合計が巡回列の長さ(189)になる。
 */
export const CLEAN_TASK_REPORT_STATUS_DISTRIBUTION = [
  { status: 4, count: 161 },
  { status: 6, count: 22 },
  { status: 5, count: 4 },
  { status: 3, count: 2 },
] as const;

/** 巡回列の長さ(= CSV の行数 189) */
export const CLEAN_TASK_REPORT_STATUS_CYCLE_LENGTH =
  CLEAN_TASK_REPORT_STATUS_DISTRIBUTION.reduce((sum, s) => sum + s.count, 0);

/**
 * 実測分布どおりの189要素の巡回列を作る。
 *
 * ★累積で塊にしない★(先頭161件が全部 `4` になると、直近数日の画面に
 * `4` 以外が1件も出ず「4種そろっている」ことを画面で確かめられない)。
 * ★Sainte-Laguë(ウェブスター式)の配分★で、比率を保ったまま散らす。
 */
function buildStatusCycle(): number[] {
  const assigned = CLEAN_TASK_REPORT_STATUS_DISTRIBUTION.map(() => 0);
  const out: number[] = [];
  for (let i = 0; i < CLEAN_TASK_REPORT_STATUS_CYCLE_LENGTH; i += 1) {
    let best = 0;
    let bestScore = Infinity;
    for (let k = 0; k < CLEAN_TASK_REPORT_STATUS_DISTRIBUTION.length; k += 1) {
      const score = (assigned[k] + 0.5) / CLEAN_TASK_REPORT_STATUS_DISTRIBUTION[k].count;
      if (score < bestScore) {
        bestScore = score;
        best = k;
      }
    }
    assigned[best] += 1;
    out.push(CLEAN_TASK_REPORT_STATUS_DISTRIBUTION[best].status);
  }
  return out;
}

const STATUS_CYCLE: readonly number[] = buildStatusCycle();

/**
 * 清掃 id(= `clean_timetables.id` = `CleanLogItem.id`)から `status` を引く(★冪等・純粋★)。
 *
 * `clean_timetable_id` を NULL にする行でも、引数は元の清掃 id を渡す
 * (NULL にしているのは join キーだけ。`clean-task-reports.ts` の `cleanTaskReportId()` と同じ考え方)。
 */
export function cleanTaskReportStatus(cleanTimetableId: number): number {
  return STATUS_CYCLE[cleanTimetableId % CLEAN_TASK_REPORT_STATUS_CYCLE_LENGTH];
}