@aiquants/daily-report 0.18.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/server.d.mts +17 -0
- package/dist/server.d.ts +17 -0
- package/dist/server.js +10 -10
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +10 -10
- package/dist/server.mjs.map +1 -1
- package/package.json +10 -9
- package/src/client/components/daily-report-resize-panels.spec.tsx +3 -1
- package/src/server/service.ts +23 -2
- package/src/server/transaction-retry.spec.ts +63 -0
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts","../src/server/cache.ts","../src/shared/business-date.ts","../src/shared/ids-stream.ts","../src/shared/logger.ts","../src/shared/sse-schema.ts","../src/server/etag.ts","../src/server/response.ts","../src/server/sse-reader.ts","../src/server/visibility.ts","../src/server/handlers.ts","../src/server/service.ts","../src/server/authz.ts","../src/server/external-source.ts","../src/server/schema.ts"],"sourcesContent":["/**\n * Server entry of @aiquants/daily-report: schema factory, service, SSE reader, and handler factories.\n * @aiquants/daily-report の server エントリ。スキーマファクトリ・サービス・SSE リーダー・ハンドラ工場を公開。\n */\nimport { createEpochStore, SqlResultCache } from \"./server/cache\"\nimport type { DailyReportExternalSource } from \"./server/external-source\"\nimport { createDailyReportHandlers } from \"./server/handlers\"\nimport type { DailyReportAuthenticate, DailyReportReadAttachment, DailyReportResolveVisibleSourceTypes } from \"./server/ports\"\nimport { createDailyReportService, type DailyReportServiceConfig } from \"./server/service\"\nimport { DailyReportSseReader } from \"./server/sse-reader\"\n\nexport * from \"./server/authz\"\nexport * from \"./server/cache\"\nexport { generateETag } from \"./server/etag\"\nexport * from \"./server/external-source\"\nexport * from \"./server/handlers\"\nexport * from \"./server/ports\"\nexport * from \"./server/response\"\nexport * from \"./server/schema\"\nexport * from \"./server/service\"\nexport * from \"./server/sse-reader\"\nexport * from \"./server/visibility\"\n\n/** createDailyReportServer の設定 (サービス設定 + 認証ポート + ハンドラ設定)。 */\nexport type DailyReportServerConfig = Omit<DailyReportServiceConfig, \"cache\" | \"epochs\"> & {\n /** リクエスト認証ポート。 */\n authenticate: DailyReportAuthenticate\n /**\n * 可視 sourceType リゾルバ。未注入なら全件 (現行互換)。\n *\n * サーバー側の全データ経路 (一覧 / 詳細 / コメント / 添付 / SSE) に一貫して効く。\n * 戻り値は `DailyReportHub.sourceType` の値 (`knownSourceTypes` 参照) であり、認可リソースキーではない。\n * `undefined` / `null` は無制限、空配列は 0 件、例外は全拒否 (fail-close)。\n */\n resolveVisibleSourceTypes?: DailyReportResolveVisibleSourceTypes\n /** 添付ファイル読み取りポート。未注入なら添付エンドポイントは常に 404。 */\n readAttachment?: DailyReportReadAttachment\n /** 添付ファイルの最大バイト数 (既定 32 MiB)。サービス側のワイヤ上限より低く保つこと。 */\n attachmentMaxBytes?: number\n /** 添付エンドポイントの 1 分あたり呼び出し上限 (ユーザー単位・既定 60)。 */\n attachmentRateLimitPerMinute?: number\n /** 添付読み取りの同時実行上限 (プロセス単位・既定 4)。 */\n attachmentConcurrency?: number\n /** 未ログイン時のリダイレクト先 (index.loader 用、既定 \"/auth/login\")。 */\n loginRedirectPath?: string\n /**\n * `intent=clearCache` (全サーバーキャッシュの一括破棄) を許可するか。既定 false。\n * 開発用アフォーダンス。有効化すると認証済みなら誰でも全ワーカーのキャッシュを\n * 無制限破棄できるため、本番では無効のままにする (消費アプリは開発環境のみ true 注入)。\n */\n enableDevCacheClear?: boolean\n /** SQL 結果キャッシュの既定 TTL (既定 60,000ms)。 */\n cacheDefaultTtlMs?: number\n /** 外部ソースアダプタ群 (レガシー日報テーブル等)。 */\n externalSources?: DailyReportExternalSource[]\n}\n\n/**\n * One-stop factory wiring cache, service, SSE fan-out reader, and route handlers.\n * キャッシュ・サービス・SSE Fan-Out リーダー・ルートハンドラを一括結線するファクトリ。\n *\n * @example\n * const server = createDailyReportServer({ db, tables, userTable, resolveUserId, encodeUserId, authenticate, redis })\n * // routes:\n * // daily_report._index → server.index.loader (+ data() ラップ)\n * // daily_report.api.$endpoint → server.api.loader / server.api.action\n * // sse.daily_report.$endpoint → server.sse.loader\n */\nexport function createDailyReportServer(config: DailyReportServerConfig) {\n // redis 未注入は単一ワーカーなら正しく動く (プロセス内無効化は効く) が、\n // ワーカー横断の無効化と SSE 配信が黙って無効になる。強制はせず、気づける形にする\n if (!config.redis) {\n const logger = config.logger ?? console\n logger.warn(\"[DailyReport] redis provider is not injected: cross-worker cache invalidation is disabled (staleness is bounded only by TTL) and SSE publishing is skipped.\")\n }\n // epoch ストアと SQL 結果キャッシュ (サービスと SSE リーダーで同一インスタンスを共有する)\n const epochs = createEpochStore(config.redis)\n const cache = new SqlResultCache({ defaultTtlMs: config.cacheDefaultTtlMs ?? 60_000 }, epochs)\n\n const service = createDailyReportService({ ...config, cache, epochs })\n\n const sseReader = new DailyReportSseReader({\n redis: config.redis,\n streamKey: service.streamKey,\n cache,\n logger: config.logger,\n })\n\n // ❗ この呼び出しはスプレッドではなく明示列挙である。新しいポートをここへ足し忘れると\n // アプリが注入しても黙って捨てられ、添付エンドポイントは永久に 404 を返す。\n const handlers = createDailyReportHandlers({\n authenticate: config.authenticate,\n // ❗ 明示列挙。ここへ足し忘れると注入されたポートが黙って捨てられ、区分フィルタが\n // 一切効かないまま全件配信になる (認可バイパス)。server.spec.ts が配線を gate する\n resolveVisibleSourceTypes: config.resolveVisibleSourceTypes,\n service,\n encodeUserId: config.encodeUserId,\n attachmentIdCodec: config.attachmentIdCodec,\n readAttachment: config.readAttachment,\n attachmentMaxBytes: config.attachmentMaxBytes,\n attachmentRateLimitPerMinute: config.attachmentRateLimitPerMinute,\n attachmentConcurrency: config.attachmentConcurrency,\n redis: config.redis,\n sseReader,\n streamKey: service.streamKey,\n loginRedirectPath: config.loginRedirectPath,\n enableDevCacheClear: config.enableDevCacheClear,\n logger: config.logger,\n })\n\n return {\n /** データアクセスサービス (CRUD + キャッシュ + SSE publish)。 */\n service,\n /** SQL 結果キャッシュ (サービス・SSE リーダー共有インスタンス)。 */\n cache,\n /** クロスワーカー epoch ストア。 */\n epochs,\n /** SSE Fan-Out 共有リーダー。 */\n sseReader,\n /** SSE Redis Stream キー。 */\n streamKey: service.streamKey,\n /**\n * 既知の正準 sourceType 集合 (注入した `externalSources` の全 sourceType + `\"Internal\"`)。\n *\n * `resolveVisibleSourceTypes` が返すべき語彙そのものであり、消費側は起動時にこの集合と\n * 自前のマッピングを突き合わせて誤トークン (無診断で 0 件になる) を検出できる。\n */\n knownSourceTypes: [...new Set([\"Internal\", ...(config.externalSources ?? []).map((source) => source.sourceType)])] as readonly string[],\n ...handlers,\n }\n}\n","/**\n * In-memory SQL result cache with TTL, snapshot, and cross-worker epoch (via redis) support.\n * TTL・スナップショット・クロスワーカー epoch (redis 経由) を備えた SQL 結果のインメモリキャッシュ。\n *\n * epoch は任意注入の redis ポート経由で取得・更新する (未注入時は常に 0 = epoch 無効)。\n */\nimport type { DailyReportRedisProvider } from \"./ports\"\n\nexport type SqlResultCacheQueryOptions = {\n forceRefresh?: boolean\n snapshot?: boolean\n ttlMsOverride?: number\n epochKey?: string\n /**\n * ヒット時にバケット年齢が ttl × ratio を超えていたら、キャッシュ値を即返しつつ\n * 裏で 1 本だけ再フェッチして置き換える (未指定は従来動作 = 何もしない)。\n * 有効域は 0 <= ratio < 1: ヒット判定が「未失効」を要求するため年齢は常に ttl 未満であり、\n * ratio 1 は事実上発火しない。ratio 0 は毎ヒット裏更新 (undefined とは区別される)。\n * 定期的にアクセスされるキーの「TTL 切れ直後のコールド遭遇」を対話リクエストから排除する。\n */\n refreshAheadRatio?: number\n}\n\n/**\n * Creates epoch helpers bound to an optional redis provider.\n * 任意の redis プロバイダーに束縛された epoch ヘルパーを生成する処理。\n */\nexport const createEpochStore = (redis?: DailyReportRedisProvider) => ({\n /**\n * Gets the current epoch value, or null when it cannot be determined.\n * 現在のエポック値を取得する処理。判定不能なら null。\n *\n * - `null` = redis 未注入、または一時的な失敗。**エポックによる横断無効化は判定不能**。\n * - `0` = redis に到達したがキーが無い (一度も increment されていない正当な初期値)。\n *\n * ❗ `0` を「不明」の代用にしてはならない。記録側も `0` のとき両辺が一致してしまい、\n * stale 検出が黙って無効化される (無効化されたこと自体がどこにも現れない)。\n * 「常に 0 = safe 方向」が成り立つのは一時的失敗のときだけで、未注入時には偽である。\n */\n async getEpoch(epochKey: string): Promise<number | null> {\n try {\n const client = await redis?.getClient()\n if (!client) return null\n const val = await client.get(epochKey)\n return val ? Number(val) : 0\n } catch {\n return null\n }\n },\n /**\n * Increments the epoch counter in Redis.\n * Redis のエポックカウンターをインクリメントする。\n */\n async incrementEpoch(epochKey: string): Promise<void> {\n try {\n const client = await redis?.getClient()\n if (!client) return\n await client.incr(epochKey)\n } catch {\n // Redis エラー時は epoch 更新をスキップ (次回 GET は null = 判定不能 → TTL のみで判定)\n }\n },\n})\n\nexport type EpochStore = ReturnType<typeof createEpochStore>\n\n/**\n * In-memory cache for SQL query results with TTL and snapshot support.\n * TTLとスナップショット機能を備えたSQLクエリ結果のインメモリキャッシュ。\n */\nexport class SqlResultCache {\n // epoch は「バケット書き込み時点のエポック」。null = 当時エポックが判定不能だった\n private readonly buckets = new Map<string, { records: readonly unknown[]; expireAt: number; epoch: number | null }>()\n private readonly inFlight = new Map<string, Promise<readonly unknown[]>>()\n\n constructor(\n private readonly config: { defaultTtlMs: number },\n private readonly epochStore: EpochStore,\n ) {}\n\n /**\n * Invalidates a specific cache bucket.\n * 指定されたキャッシュバケットを無効化します。\n */\n invalidate = (cacheKey: string): void => {\n this.buckets.delete(cacheKey)\n this.inFlight.delete(cacheKey)\n }\n\n /**\n * Kicks a single background refetch when the bucket has aged past ttl * ratio.\n * バケット年齢が ttl × ratio を超えたヒット時に、裏で 1 本だけ再フェッチを起動する処理。\n *\n * 応答は誰も待たない。完了までは旧バケットがそのまま生きているため、並行するヒットは\n * 従来どおりキャッシュから即答する (バケットを先に消す forceRefresh 委譲はスタンピードに\n * なるため使わない)。バケットが有効な間は失敗しても旧バケットが残るだけ (safe 方向)。\n * 裏フェッチ中に TTL が完全に切れて in-flight へ相乗りした前面リクエストには、通常の\n * in-flight 共有と同じく失敗が伝播する (相乗りしなくても自前フェッチが同様に失敗する状況)。\n * in-flight が既に居れば起動しない (前面リクエストの結果で足りる)。\n */\n private maybeRefreshAhead = <T>(opts: { cacheKey: string; fetcher: () => Promise<readonly T[]> } & SqlResultCacheQueryOptions): void => {\n const ratio = opts.refreshAheadRatio\n if (ratio === undefined) {\n return\n }\n const { cacheKey } = opts\n // 年齢は呼び出し元のスナップショットではなく生きているバケットで測る。epoch 照合の\n // Redis await 中に進行中の裏フェッチが完了していると、古い expireAt のまま余分な\n // 2 本目を起動してしまう (置換直後のバケットは若く、再フェッチ不要)\n const bucket = this.buckets.get(cacheKey)\n if (!bucket) {\n return\n }\n const ttl = opts.ttlMsOverride ?? this.config.defaultTtlMs\n const age = ttl - (bucket.expireAt - Date.now())\n if (age < ttl * ratio) {\n return\n }\n if (this.inFlight.has(cacheKey)) {\n return\n }\n\n // 旧バケットを残したまま裏で取得し、完了時に差し替える。無効化レースの規約は\n // 前面経路と同一: 進行中に invalidate されたら (inFlight の同一性が崩れたら) 書かない\n let promise!: Promise<readonly T[]>\n promise = (async () => {\n // ❗ epoch はフェッチ「開始前」に採取する。完了後に採取すると、フェッチ中に他ワーカーが\n // 更新 (incrementEpoch) した後の値で旧データに新 epoch を刻んでしまい、epoch 照合\n // (staleness の主防御線) が丸ごと無効化される。開始前採取なら不一致 → 再フェッチで自己回復\n // epochKey 未指定は「エポック無し」— 0 (正当な初期値) ではなく null で表す\n const epoch = opts.epochKey ? await this.epochStore.getEpoch(opts.epochKey) : null\n const records = await opts.fetcher()\n // ガード判定と書き込みの間に await を挟まない (この間に落ちた invalidate の取りこぼし防止)\n if (this.inFlight.get(cacheKey) === promise) {\n this.buckets.set(cacheKey, { records, expireAt: Date.now() + ttl, epoch })\n }\n return records\n })()\n // 登録することで、裏フェッチ中に TTL 切れした前面リクエストは相乗りできる\n this.inFlight.set(cacheKey, promise as Promise<readonly unknown[]>)\n promise\n .catch(() => {})\n .finally(() => {\n // 無効化で消されたり別の登録に置き換わっていたら触らない\n if (this.inFlight.get(cacheKey) === promise) {\n this.inFlight.delete(cacheKey)\n }\n })\n }\n\n /**\n * Retrieves records from cache or fetches them if expired/missing.\n * キャッシュからレコードを取得するか、期限切れや未存在の場合はフェッチします。\n */\n getOrFetch = async <T>(\n opts: {\n cacheKey: string\n fetcher: () => Promise<readonly T[]>\n /**\n * 本呼び出しが実際にフェッチャーを起動した瞬間に 1 度だけ呼ばれる。\n *\n * 「返す配列がこの呼び出し自身の新規フェッチ結果である」ことの**因果的な証明**として使う。\n * バケットヒット・進行中フェッチへの相乗り・refresh-ahead の裏側更新では**呼ばれない** —\n * それらの経路が返す配列は本呼び出しより古くなりうるためである。\n *\n * ids ストリームはこの合図で高速先頭ページのクエリを起動する。合図の後に始まる以上、\n * 先頭ページは全量配列より古くなり得ず、「先頭ページに在って全量に無い行 = 削除済み」\n * という判定が初めて健全になる (逆順を許すと作成直後の日報を削除してしまう)。\n */\n onFetchStart?: () => void\n } & SqlResultCacheQueryOptions,\n ): Promise<readonly T[]> => {\n const { cacheKey, forceRefresh, snapshot } = opts\n const bucket = this.buckets.get(cacheKey)\n\n // 強制リフレッシュ時はキャッシュと進行中のリクエストをクリア\n if (forceRefresh) {\n this.buckets.delete(cacheKey)\n this.inFlight.delete(cacheKey)\n }\n\n // 有効なキャッシュがあれば返却 (epoch check 込み)\n if (!forceRefresh && bucket && bucket.expireAt > Date.now()) {\n if (opts.epochKey) {\n // epochKey 指定時は Redis の epoch と比較して stale を検出する。三値の行列:\n // 現在 null (未注入/一時失敗) → TTL のみで受理 (エポックは鮮度の加速器であって\n // 認可境界ではない。障害中に毎リクエスト再フェッチで DB を叩き続けない)\n // バケット null + 現在 数値 → stale (信号復帰後、バケットの世代は証明できない。\n // null === 数値 が偽なので下の等値比較が自然にこの行を与える)\n // 両方 数値 → 等値比較 (従来どおり)\n // ❗ 型を number | null に広げても分岐は強制されない (=== は union をそのまま受ける)。\n // この行列はテストだけが守っている — 特に「バケット数値 + 現在 null で受理」は\n // 明示節が無いと偽になり、redis 一時障害中のチャーンが再発する\n const currentEpoch = await this.epochStore.getEpoch(opts.epochKey)\n if (currentEpoch === null || bucket.epoch === currentEpoch) {\n this.maybeRefreshAhead(opts)\n return snapshot ? (bucket.records.map((r) => structuredClone(r)) as T[]) : (bucket.records as T[])\n }\n // epoch 不一致 → stale bucket を除去して re-fetch へ\n this.buckets.delete(cacheKey)\n } else {\n this.maybeRefreshAhead(opts)\n return snapshot ? (bucket.records.map((r) => structuredClone(r)) as T[]) : (bucket.records as T[])\n }\n }\n\n // 進行中のリクエストがあれば相乗り\n if (!forceRefresh && this.inFlight.has(cacheKey)) {\n const shared = (await this.inFlight.get(cacheKey)) as readonly T[]\n return snapshot ? shared.map((r) => structuredClone(r)) : shared\n }\n\n // 新規フェッチとキャッシュ更新\n // 自己参照 (this.inFlight.get(cacheKey) === promise) のため、定義前参照を回避する確定代入アサーション\n let promise!: Promise<readonly T[]>\n promise = (async () => {\n // ❗ epoch はフェッチ「開始前」に採取する (理由は maybeRefreshAhead と同一:\n // フェッチ中の他ワーカー更新を新 epoch で偽装しない。ガードと書き込みも同期隣接に保つ)\n // epochKey 未指定は「エポック無し」— 0 (正当な初期値) ではなく null で表す\n const epoch = opts.epochKey ? await this.epochStore.getEpoch(opts.epochKey) : null\n // ❗ フェッチャー起動の直前に合図する。これより後に始まる処理は、この配列より\n // 新しいスナップショットを見ることが保証される (順序の因果を呼び出し側へ渡す)。\n // refresh-ahead の裏側更新には**置かない** — 返す配列は古いバケットのままなので\n // 合図すると「新しい配列を得た」という嘘になる\n opts.onFetchStart?.()\n const records = await opts.fetcher()\n // invalidation 中に完了したリクエストはキャッシュに書き込まない(レース防止)\n // ただし forceRefresh の場合は常に書き込む\n if (forceRefresh || this.inFlight.get(cacheKey) === promise) {\n const expireAt = Date.now() + (opts.ttlMsOverride ?? this.config.defaultTtlMs)\n this.buckets.set(cacheKey, { records, expireAt, epoch })\n // 蓄積防止のため、サイズが一定値を超えたら期限切れキャッシュを一括クリーンアップ\n if (this.buckets.size > 500) {\n this.cleanExpired()\n }\n }\n return records\n })()\n\n if (!forceRefresh) this.inFlight.set(cacheKey, promise as Promise<readonly unknown[]>)\n\n try {\n const result = await promise\n return snapshot ? result.map((r) => structuredClone(r)) : result\n } finally {\n // ❗ identity チェック必須: 無効化が本リクエストの登録を消した後に後続リクエストが\n // 登録し直していた場合、無条件 delete は「後続」の書き込みガードまで壊してしまう\n // (2 本のフェッチが完走したのにバケットが空、という毒殺の温床)\n if (this.inFlight.get(cacheKey) === promise) {\n this.inFlight.delete(cacheKey)\n }\n }\n }\n\n /**\n * Clears the cache for a specific key.\n * 指定されたキーのキャッシュをクリアします。\n */\n flush = (cacheKey: string): void => {\n this.buckets.delete(cacheKey)\n this.inFlight.delete(cacheKey)\n }\n\n /**\n * Clears all cached buckets and in-flight requests.\n * すべてのキャッシュバケットと進行中のリクエストを全クリアします。\n */\n clearAll = (): void => {\n this.buckets.clear()\n this.inFlight.clear()\n }\n\n /**\n * Invalidates cache buckets matching a prefix.\n * 指定されたプレフィックスに一致するキャッシュバケットを無効化します。\n */\n invalidatePrefix = (prefix: string): void => {\n for (const key of this.buckets.keys()) {\n if (key.startsWith(prefix)) {\n this.buckets.delete(key)\n }\n }\n for (const key of this.inFlight.keys()) {\n if (key.startsWith(prefix)) {\n this.inFlight.delete(key)\n }\n }\n }\n\n /**\n * Cleans up all expired cache buckets to prevent memory accumulation.\n * メモリー蓄積を防ぐため、期限切れのキャッシュバケットをすべてクリーンアップする処理。\n */\n private cleanExpired(): void {\n const now = Date.now()\n // 期限切れのキーを削除\n for (const [key, bucket] of this.buckets.entries()) {\n if (bucket.expireAt <= now) {\n this.buckets.delete(key)\n }\n }\n }\n}\n","/**\n * Business date normalization and formatting helper functions.\n * 営業日の正規化とフォーマットを支援するユーティリティ群。\n */\n\ntype BusinessDateInput = string | Date | null | undefined\n\nconst ISO_DATE_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\nconst SLASH_DATE_PATTERN = /^\\d{4}\\/\\d{2}\\/\\d{2}$/\n\nconst zeroPad = (value: number): string => {\n return value < 10 ? `0${value}` : `${value}`\n}\n\nconst normalizeFromDate = (value: Date): string | null => {\n if (Number.isNaN(value.getTime())) {\n return null\n }\n\n const year = value.getFullYear()\n const month = zeroPad(value.getMonth() + 1)\n const day = zeroPad(value.getDate())\n return `${year}-${month}-${day}`\n}\n\n/**\n * Returns today's business-date key (YYYY-MM-DD) in the local calendar.\n * ローカルカレンダー上の「本日」の営業日キー (YYYY-MM-DD) を返す。\n *\n * `new Date().toISOString()` の日付部は UTC 日付になり、UTC より東のタイムゾーン\n * (JST など) では深夜〜午前帯に前日へずれる。営業日はブラウザのローカル暦日で判定する\n * ため、ローカル getter から直接組む。タイムゾーンはパッケージに固定しない (消費側の\n * 実行環境が決める) 方針に沿う。\n *\n * @param now Clock injection for tests (defaults to the current time). テスト用の時刻注入 (既定は現在時刻)。\n * @returns Local calendar day as YYYY-MM-DD. ローカル暦日の YYYY-MM-DD。\n */\nexport const resolveTodayBusinessDateKey = (now: Date = new Date()): string => {\n if (Number.isNaN(now.getTime())) {\n // 不正な Date が注入されたら現在時刻で解決する (空の営業日キーを返さない)\n return resolveTodayBusinessDateKey(new Date())\n }\n return `${now.getFullYear()}-${zeroPad(now.getMonth() + 1)}-${zeroPad(now.getDate())}`\n}\n\n/**\n * Normalizes business date input into YYYY-MM-DD format string.\n * 営業日を YYYY-MM-DD 形式の文字列へ正規化する。\n */\nexport const normalizeBusinessDateKey = (value: BusinessDateInput): string | null => {\n if (value === null || value === undefined) {\n return null\n }\n\n if (value instanceof Date) {\n return normalizeFromDate(value)\n }\n\n const trimmed = value.trim()\n if (trimmed === \"\") {\n return null\n }\n\n if (ISO_DATE_PATTERN.test(trimmed)) {\n return trimmed\n }\n\n if (SLASH_DATE_PATTERN.test(trimmed)) {\n return trimmed.replaceAll(\"/\", \"-\")\n }\n\n const parsed = new Date(trimmed)\n return normalizeFromDate(parsed)\n}\n\n/**\n * Converts normalized business date into display format YYYY-MM-DD.\n * 正規化済みの営業日を YYYY-MM-DD 形式の表示文字列へ変換する。\n */\nexport const formatBusinessDateDisplay = (value: BusinessDateInput): string | null => {\n const normalized = normalizeBusinessDateKey(value)\n if (!normalized) {\n return null\n }\n return normalized\n}\n\n/**\n * Parses normalized business date into Date object.\n * 正規化した営業日を Date オブジェクトへ変換する。\n */\nexport const parseBusinessDateKeyToDate = (value: BusinessDateInput): Date | null => {\n const normalized = normalizeBusinessDateKey(value)\n if (!normalized) {\n return null\n }\n\n const [year, month, day] = normalized.split(\"-\").map((part) => Number.parseInt(part, 10))\n if ([year, month, day].some((component) => Number.isNaN(component))) {\n return null\n }\n\n return new Date(year, month - 1, day)\n}\n","/**\n * Wire contract and ordering helpers for the daily-report ids NDJSON stream.\n * 日報 ID 一覧 NDJSON ストリームのワイヤ契約と並び順ヘルパー。\n *\n * サーバーは `GET {apiBasePath}/ids-stream` で 1 行 1 JSON の NDJSON を返す。\n * 行は原則 {@link DailyReportIdsStreamChunkLine} で、送出中に予期しない失敗が\n * 起きた場合のみ {@link DailyReportIdsStreamErrorLine} を 1 行流して正常クローズする\n * (クライアントは「isLast 無しで終端」を再開シグナルとして扱う)。\n *\n * 並び順は SQL の `ORDER BY business_date DESC, id DESC` (MSSQL の DESC は NULL を\n * 末尾に置く) と完全一致させる。カーソル再開はこの全順序に対する「厳密に後」の\n * 位置からのスライスとして定義する。\n */\nimport type { DailyReportItem } from \"./types\"\n\n/** ストリーム再開カーソル。直前チャンク末尾アイテムの並び順キー。 */\nexport type DailyReportIdsStreamCursor = {\n /** 営業日 (YYYY-MM-DD)。null は「営業日なし領域」(並び順の末尾) を表す。 */\n businessDate: string | null\n /** 日報 Hub ID。 */\n reportHubId: number\n}\n\n/** データを運ぶ NDJSON 行。 */\nexport type DailyReportIdsStreamChunkLine = {\n /**\n * 行種別。判別可能ユニオンの識別子。\n *\n * ❗ 行の意味は `kind` が宣言する。「`items` を持つかどうか」で判定していた時代は、\n * 新しい行種別を足すと既存クライアントがそれをエラー行と解釈して走査を中断した。\n * `kind` を欠く行は本識別子の導入前の送信元とみなし、分類器が従来判定へ落とす。\n */\n kind: \"chunk\"\n /** このチャンクのアイテム群 (並び順どおり)。 */\n items: DailyReportItem[]\n /** 次チャンクの再開カーソル。最終チャンクでは null。 */\n nextCursor: DailyReportIdsStreamCursor | null\n /** これが最終チャンクかどうか。 */\n isLast: boolean\n /** 全体件数。接続ごとの先頭行にのみ載る。 */\n total?: number\n /**\n * この行が**暫定** (未確認スナップショット) かどうか。既定 (未指定) は権威。\n *\n * 高速先頭ページは全量配列とは別のスナップショットなので、そのまま「観測した」と\n * 扱うと、送出後に消えた行が完走剪定を免れて幽霊行になる。暫定行はクライアント側で\n * 描画はするが観測集合へ刻まないため、権威パス (全量配列を先頭から流し直す) に\n * 現れなかった行が完走時に正確に落ちる。\n *\n * ❗ 暫定行には `nextCursor` を載せない (常に null)。未確認領域を指すカーソルで再開すると、\n * 三つ目のスナップショットへ継ぎ目が移るだけになる。\n */\n provisional?: boolean\n /**\n * この行を切り出した全量配列の同一性トークン ({@link computeIdsStreamSnapshotToken})。\n *\n * クライアントは再接続時にカーソルと**対で**送り返し、サーバーは現在の配列のトークンと\n * 照合して、同じ配列のときだけカーソルを信用する。別の配列にカーソルを当てると、\n * カーソルより前に入った行が丸ごと飛ぶ (再接続のたびの欠損)。\n *\n * ❗ resume 経路を含む**全ての権威行**に載せる。載せ忘れると最初の再開成功でクライアントの\n * トークンが失われ、2 度目以降の切断が全て全量再送へ退化する。\n * ❗ 暫定行と短絡行には載せない (全量配列を代表しないため、再開起点にしてはならない)。\n */\n snapshot?: string\n /**\n * 送られたカーソルを破棄して先頭から流し直したことの明示 (先頭行にのみ載る)。\n *\n * クライアントはこれを受けたらカーソル・トークン・観測集合・トゥームストーンを初期化する\n * (処理は行の吸収関数の内部で行われ、呼び出し側に順序の自由度は無い)。\n */\n restarted?: boolean\n}\n\n/** サーバー側の予期しない失敗を伝える NDJSON 行 (内部情報は含めない)。 */\nexport type DailyReportIdsStreamErrorLine = {\n /** 行種別。判別可能ユニオンの識別子。 */\n kind: \"error\"\n error: string\n}\n\n/** NDJSON 1 行のユニオン。 */\nexport type DailyReportIdsStreamLine = DailyReportIdsStreamChunkLine | DailyReportIdsStreamErrorLine\n\n/**\n * Classifies one parsed NDJSON line by its declared kind.\n * パース済みの NDJSON 行を宣言された種別で分類する処理。\n *\n * クライアントとテストはこの分類器**だけ**を使う。契約:\n *\n * - `\"chunk\"` / `\"error\"`: 宣言どおりに扱う。\n * - **未知の `kind` は `\"unknown\"`** — 将来のワイヤ拡張であり、エラー扱いすると\n * 走査が中断する (旧判定の罠が 1 段上で再発する)。呼び出し側は警告して読み飛ばす。\n * - `kind` を欠く行は本識別子の導入前の送信元 (配備入れ替わり窓)。従来判定\n * (`items` 配列の有無 → チャンク、`error` 文字列 → エラー) へ落とす。\n *\n * @param line Parsed NDJSON line. パース済みの NDJSON 行。\n * @returns Line classification. 行の分類。\n */\nexport const classifyIdsStreamLine = (line: unknown): \"chunk\" | \"error\" | \"unknown\" => {\n if (typeof line !== \"object\" || line === null) {\n return \"unknown\"\n }\n const record = line as { kind?: unknown; items?: unknown; error?: unknown }\n if (typeof record.kind === \"string\") {\n if (record.kind === \"chunk\") return \"chunk\"\n if (record.kind === \"error\") return \"error\"\n return \"unknown\"\n }\n if (Array.isArray(record.items)) return \"chunk\"\n if (typeof record.error === \"string\") return \"error\"\n return \"unknown\"\n}\n\n/**\n * Type guard for chunk lines (thin wrapper over the classifier).\n * チャンク行の型ガード ({@link classifyIdsStreamLine} の薄い包み)。\n *\n * @param line Parsed NDJSON line. パース済みの NDJSON 行。\n * @returns True when the line is a chunk line. チャンク行なら true。\n */\nexport const isIdsStreamChunkLine = (line: unknown): line is DailyReportIdsStreamChunkLine => {\n return classifyIdsStreamLine(line) === \"chunk\"\n}\n\n/**\n * Computes the identity token of a materialized ids array.\n * 確定済み ids 配列の同一性トークンを算出する処理。\n *\n * 内容から決まる値にする — プロセスごとにキャッシュバケットが別なので、別ワーカーが\n * 応答しても同じ配列なら同じトークンにならなければならない。畳み込むのは各行の\n * `reportHubId` と `businessDate` (= カーソルの位置を決める全順序の構成要素) + 件数。\n * `sourceType` は順序に関与しないため畳み込まない (変わっても位置は不変)。\n *\n * ❗ 衝突の方向を取り違えないこと。決定的ハッシュに「同じ配列なのに別トークン」(偽不一致 =\n * 無駄な再送) は構造上起こり得ない。危険なのはその逆 — **異なる配列が同一トークンになる\n * 偽一致**であり、その場合カーソルが変化後の配列に対して信用され、欠損・重複が無音で発生する\n * (認可には影響しない — 再開行は常に現在の認可済み配列から切り出される)。この確率を議論の\n * 余地なく無視できる水準 (~2^-64) にするため、オフセット基底の異なる FNV-1a 32bit を\n * 2 レーン走らせて連結する。費用は 22 万件で数 ms。\n *\n * @param items Stream-ordered items. ストリーム順のアイテム配列。\n * @returns Hex token (up to 16 chars). 16 進トークン (最長 16 桁)。\n */\nexport const computeIdsStreamSnapshotToken = (items: readonly DailyReportItem[]): string => {\n let laneA = 0x811c9dc5\n // レーン B は別の基底から出発する (同一基底 2 本では独立性が出ない)\n let laneB = 0x811c9dc5 ^ 0x5bd1e995\n for (const item of items) {\n const id = item.reportHubId\n laneA ^= id & 0xff\n laneA = Math.imul(laneA, 0x01000193)\n laneA ^= (id >>> 8) & 0xff\n laneA = Math.imul(laneA, 0x01000193)\n laneA ^= (id >>> 16) & 0xff\n laneA = Math.imul(laneA, 0x01000193)\n laneA ^= (id >>> 24) & 0xff\n laneA = Math.imul(laneA, 0x01000193)\n laneB ^= (id >>> 24) & 0xff\n laneB = Math.imul(laneB, 0x01000193)\n laneB ^= (id >>> 16) & 0xff\n laneB = Math.imul(laneB, 0x01000193)\n laneB ^= (id >>> 8) & 0xff\n laneB = Math.imul(laneB, 0x01000193)\n laneB ^= id & 0xff\n laneB = Math.imul(laneB, 0x01000193)\n // ❗ businessDate も畳み込む。カーソルの位置は (businessDate, id) の全順序で決まるため、\n // id 列が同一のまま日付だけ整合的に動いた配列 (外部取込の日付訂正等) を同一視すると、\n // 位置の変わったカーソルが信用され、未受信行が無音で飛ぶ (実測で再現された盲点)。\n // null 営業日は 1 バイトの番兵で表す (空文字列と区別する)\n const date = item.businessDate\n if (date === null) {\n laneA ^= 0x01\n laneA = Math.imul(laneA, 0x01000193)\n laneB ^= 0x01\n laneB = Math.imul(laneB, 0x01000193)\n } else {\n for (let i = 0; i < date.length; i++) {\n const code = date.charCodeAt(i)\n laneA ^= code & 0xff\n laneA = Math.imul(laneA, 0x01000193)\n laneB ^= code & 0xff\n laneB = Math.imul(laneB, 0x01000193)\n }\n }\n }\n // 件数も両レーンへ畳み込む (先頭が同じで末尾だけ欠けた配列を別物にする)\n laneA ^= items.length\n laneA = Math.imul(laneA, 0x01000193)\n laneB ^= items.length\n laneB = Math.imul(laneB, 0x01000193)\n return ((laneA >>> 0).toString(16).padStart(8, \"0\") + (laneB >>> 0).toString(16).padStart(8, \"0\")).replace(/^0+(?=.)/, \"\")\n}\n\n/**\n * Total order of the ids stream: businessDate DESC (NULL last), then id DESC.\n * ids ストリームの全順序。営業日降順 (NULL は末尾)、同日内は ID 降順。\n *\n * MSSQL の `ORDER BY business_date DESC, id DESC` と一致させることが唯一の存在理由。\n * 営業日は正規化済みの `YYYY-MM-DD` 文字列なので辞書順比較 = 時系列比較になる。\n *\n * @param a Left key. 左辺キー。\n * @param b Right key. 右辺キー。\n * @returns Negative when `a` streams before `b`. `a` が先に流れるなら負値。\n */\nexport const compareIdsStreamOrder = (a: DailyReportIdsStreamCursor, b: DailyReportIdsStreamCursor): number => {\n if (a.businessDate !== b.businessDate) {\n // null 営業日は並び順の末尾 (MSSQL DESC の NULL 末尾挙動へ揃える)\n if (a.businessDate === null) return 1\n if (b.businessDate === null) return -1\n return a.businessDate > b.businessDate ? -1 : 1\n }\n return b.reportHubId - a.reportHubId\n}\n\n/**\n * Checks whether items already follow the ids-stream total order.\n * アイテム配列が ids ストリームの全順序に既に従っているかを検査する処理。\n *\n * ストリームはソート順どおりに届いて末尾へ追記されるため通常は整列済みで、\n * この O(n) 検査で全件ソート (O(n log n)) を省略できる。\n *\n * @param items Items to probe. 検査対象のアイテム配列。\n * @returns True when already ordered. 整列済みなら true。\n */\nexport const isIdsStreamOrdered = (items: readonly DailyReportItem[]): boolean => {\n for (let i = 1; i < items.length; i++) {\n if (compareIdsStreamOrder(items[i - 1], items[i]) > 0) return false\n }\n return true\n}\n\n/**\n * Returns a copy with the item upserted at its ids-stream-ordered position.\n * アイテムを ids ストリーム順の正しい位置へ upsert した複製配列を返す処理。\n *\n * 前提: `items` は ids ストリーム順に整列済み (このモジュールの比較器を使う全書き込み元が\n * 維持する不変条件)。既存の同 ID は除去してから二分探索で挿入位置を求めるため、\n * 全件ソート無しの O(n) で並び順を保てる (以前は SSE 1 件ごとに 20 万件超を\n * localeCompare で全ソートしていた)。\n *\n * @param items Ordered items. 整列済みアイテム配列。\n * @param item Item to upsert. upsert するアイテム。\n * @returns New ordered array. 新しい整列済み配列。\n */\nexport const upsertInIdsStreamOrder = (items: readonly DailyReportItem[], item: DailyReportItem): DailyReportItem[] => {\n // 既存の同 ID を除いた複製 (位置移動を伴う更新もこれで単一経路になる)\n const next = items.filter((existing) => existing.reportHubId !== item.reportHubId)\n let low = 0\n let high = next.length\n while (low < high) {\n const mid = (low + high) >> 1\n if (compareIdsStreamOrder(next[mid], item) <= 0) {\n low = mid + 1\n } else {\n high = mid\n }\n }\n next.splice(low, 0, item)\n return next\n}\n\n/**\n * Finds the first index that streams strictly after the cursor.\n * カーソルより厳密に後ろへ並ぶ最初のインデックスを求める処理。\n *\n * 配列は必ずストリーム順 (SQL の ORDER BY) で渡されるが、比較器とのズレが\n * あっても壊れない線形走査で決定する (件数は高々数万・再開時のみの実行)。\n * 該当なし (カーソルが末尾以降) は `items.length` を返し、空チャンク +\n * `isLast: true` の正常終端に落ちる。\n *\n * @param items Stream-ordered items. ストリーム順のアイテム配列。\n * @param cursor Resume cursor. 再開カーソル。\n * @returns Index to resume emission from. 送出を再開するインデックス。\n */\nexport const findIdsStreamResumeIndex = (items: readonly DailyReportItem[], cursor: DailyReportIdsStreamCursor): number => {\n const index = items.findIndex((item) => compareIdsStreamOrder({ businessDate: item.businessDate, reportHubId: item.reportHubId }, cursor) > 0)\n return index === -1 ? items.length : index\n}\n","/**\n * Minimal level-filtered logger used internally by the daily-report package.\n * daily-report パッケージ内部で使う最小のレベルフィルタ付きロガー。\n *\n * 消費アプリのロガー実装 (console 互換の debug/info/warn/error) を注入でき、\n * 未注入時は console にフォールバックする。\n */\n\n/** 出力フィルタリング用のログレベル。 */\nexport enum LogLevel {\n DEBUG = 0,\n INFO = 1,\n WARN = 2,\n ERROR = 3,\n NONE = 4,\n}\n\n/** Console 互換のロガーインターフェース。 */\nexport interface DailyReportLogger {\n debug(message?: unknown, ...optionalParams: unknown[]): void\n info(message?: unknown, ...optionalParams: unknown[]): void\n warn(message?: unknown, ...optionalParams: unknown[]): void\n error(message?: unknown, ...optionalParams: unknown[]): void\n}\n\n/**\n * Creates a prefixed, level-filtered logger delegating to the given implementation.\n * 指定実装へ委譲するプレフィックス付き・レベルフィルタ付きロガーを生成する処理。\n */\nexport const createLogger = (level: LogLevel, prefix: string, impl: DailyReportLogger = console): DailyReportLogger => {\n // 文字列メッセージはプレフィックスを連結、それ以外は先頭引数として付加する\n const format = (message: unknown): unknown[] => (typeof message === \"string\" ? [`${prefix} ${message}`] : [prefix, message])\n return {\n debug: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.DEBUG) impl.debug(...format(message), ...rest)\n },\n info: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.INFO) impl.info(...format(message), ...rest)\n },\n warn: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.WARN) impl.warn(...format(message), ...rest)\n },\n error: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.ERROR) impl.error(...format(message), ...rest)\n },\n }\n}\n","/**\n * Zod schemas for daily-report SSE messages (client/server shared contract).\n * 日報 SSE メッセージの zod スキーマ (client / server 共有契約)。\n */\nimport { z } from \"zod\"\nimport type { DailyReportPostedComment as DeclaredComment, DailyReportDetail as DeclaredDetail } from \"./types\"\n\n// --- サブスキーマ(types.ts の型と構造的に一致させる) ---\n\nexport const dailyReportInterviewerSchema = z.object({\n name: z.string().nullish(),\n affiliation: z.string().nullish(),\n})\n\nexport const dailyReportExternalCommentSchema = z.object({\n name: z.string().nullish(),\n text: z.string().nullish(),\n color: z.string().nullish(),\n})\n\nexport const dailyReportLabelDefSchema = z.object({\n id: z.number(),\n name: z.string().nullish(),\n color: z.string().nullish(),\n})\n\nexport const dailyReportPostedCommentSchema = z.object({\n id: z.number(),\n userId: z.string().nullish(),\n userName: z.string().nullish(),\n content: z.string().nullish(),\n createdAt: z.string().nullish(),\n isMine: z.boolean(),\n})\n\n/**\n * 添付ファイル 1 件のスキーマ。\n *\n * 周囲のフィールドは `.nullish()` だが、ここは意図して `.nullable()` を使う。\n * `.nullish()` は推論キーを任意 (`?`) にするため、`types.ts` 側の必須 `fileType: string | null`\n * へ代入できず、ファイル末尾の `_AssertDetailCompat` が `never` に落ちてコンパイルエラーになる。\n * 既存の `.nullish()` フィールドはいずれも `types.ts` 側が `?` 付きの任意キーとペアになっており、\n * 「`.nullish()` ↔ 任意キー」「`.nullable()` ↔ 必須 `| null`」の対応で揃っている。\n * `| undefined` を混ぜないことは、詳細型を消費する DuckDB ステージ行の要請でもある。\n */\nexport const dailyReportAttachmentItemSchema = z.object({\n id: z.string(),\n fileName: z.string(),\n fileType: z.string().nullable(),\n fileSize: z.number().nullable(),\n createdAt: z.string().nullable(),\n state: z.enum([\"present\", \"absent\", \"unknown\"]),\n})\n\nexport const dailyReportDetailSchema = z.object({\n reportHubId: z.number(),\n date: z.string().nullish(),\n createdAt: z.string().nullish(),\n author: z.string().nullish(),\n userId: z.string().nullish(),\n sourceType: z.string().nullish(),\n employeeName: z.string().nullish(),\n updatedBy: z.string().nullish(),\n updatedAt: z.string().nullish(),\n category: z.string().nullish(),\n creationCategory: z.string().nullish(),\n visitTimeFrom: z.string().nullish(),\n visitTimeTo: z.string().nullish(),\n customerName: z.string().nullish(),\n interviewers: z.array(dailyReportInterviewerSchema),\n subject: z.string().nullish(),\n content: z.string().nullish(),\n externalComments: z.array(dailyReportExternalCommentSchema),\n isRead: z.boolean(),\n isStarred: z.boolean(),\n labels: z.array(dailyReportLabelDefSchema),\n postedComments: z.array(dailyReportPostedCommentSchema),\n // ❗ `.default([])` は必須。SSE の実体は永続 Redis Stream (MAXLEN 保持) であり、\n // **同一ワイヤ版の中でも**古いエントリが残る (共有リーダーは tail-anchor で起動するが、\n // 再接続クライアントの catch-up が過去エントリを読み直す)。このキーを必須にすると\n // 添付機能を入れる前に publish された全エントリが fail-closed で捨てられ、\n // 再接続タブへ更新が永久に届かなくなる。\n // ❗ 必須キーの改名など**互換を破る形の変更**はこの種の default では救えない —\n // その場合は service.ts の DAILY_REPORT_SSE_WIRE_VERSION を上げてストリームごと分離する。\n // 送信側の詰め忘れ検知は types.ts の必須フィールドと `_AssertDetailCompat`\n // (出力型は既定値適用後なので必須のまま) が担っており、ここを緩めても失われない。\n attachments: z.array(dailyReportAttachmentItemSchema).default([]),\n})\n\n// --- SSE メッセージスキーマ(8種) ---\n\nexport const connectedMessageSchema = z.object({\n type: z.literal(\"connected\"),\n})\n\nexport const statusUpdateMessageSchema = z.object({\n type: z.literal(\"status-update\"),\n reportHubId: z.number(),\n statusType: z.enum([\"star\", \"read\"]),\n value: z.boolean(),\n clientTempId: z.string(),\n // recipientRawUserId はサーバーサイドフィルタリング専用。\n // SSE ルートが raw JSON から取得してフィルタ後に除去する。\n // クライアントには到達しないが、サーバー側の publish で .parse() を通すため定義が必要。\n recipientRawUserId: z.number().optional(),\n})\n\n/**\n * 親日報の区分 (sourceType)。受信側の可視性フィルタ専用。\n *\n * ❗ `.optional()` は必須。SSE の実体は永続 Redis Stream (MAXLEN 保持) であり、このフィールドを\n * 載せる前に publish された**旧エントリが配備をまたいで残る**。必須にすると共有リーダーの\n * 再生と再接続クライアントの catch-up が fail-closed で全て捨てられ、更新が永久に届かなくなる。\n *\n * `recipientRawUserId` と異なりクライアントへ**除去せず転送する**。フィルタを通過した時点で\n * その区分は当該閲覧者に可視であり、`sourceType` は一覧・詳細で既に開示済みのため漏洩しない。\n */\nconst sseSourceTypeSchema = z.string().optional()\n\nexport const commentAddMessageSchema = z.object({\n type: z.literal(\"comment-add\"),\n reportHubId: z.number(),\n comment: dailyReportPostedCommentSchema,\n clientTempId: z.string(),\n // 下書き日報のコメントは所有者以外に見えてはならない (本文の全員配信を防ぐ)。\n // サーバーサイドフィルタ専用で、SSE ルートがフィルタ後に除去する\n recipientRawUserId: z.number().optional(),\n sourceType: sseSourceTypeSchema,\n})\n\nexport const commentDeleteMessageSchema = z.object({\n type: z.literal(\"comment-delete\"),\n reportHubId: z.number(),\n commentId: z.number(),\n clientTempId: z.string(),\n // comment-add と同じ理由の宛先フィルタ\n recipientRawUserId: z.number().optional(),\n sourceType: sseSourceTypeSchema,\n})\n\nexport const reportCreateMessageSchema = z.object({\n type: z.literal(\"report-create\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportUpdateMessageSchema = z.object({\n type: z.literal(\"report-update\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportPublishMessageSchema = z.object({\n type: z.literal(\"report-publish\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportDeleteMessageSchema = z.object({\n type: z.literal(\"report-delete\"),\n reportHubId: z.number(),\n clientTempId: z.string(),\n sourceType: sseSourceTypeSchema,\n})\n\n// --- Discriminated Union ---\n\nexport const dailyReportSseMessageSchema = z.discriminatedUnion(\"type\", [\n connectedMessageSchema,\n statusUpdateMessageSchema,\n commentAddMessageSchema,\n commentDeleteMessageSchema,\n reportCreateMessageSchema,\n reportUpdateMessageSchema,\n reportPublishMessageSchema,\n reportDeleteMessageSchema,\n])\n\n// --- 型エクスポート ---\n\nexport type DailyReportSseMessage = z.infer<typeof dailyReportSseMessageSchema>\n\n// --- コンパイル時の型互換チェック ---\n// Zod推論型が既存の型定義に代入可能であることを保証する。\n// ここでエラーが出たらスキーマと types.ts がずれている。\n\ntype _AssertDetailCompat = z.infer<typeof dailyReportDetailSchema> extends DeclaredDetail ? true : never\ntype _AssertCommentCompat = z.infer<typeof dailyReportPostedCommentSchema> extends DeclaredComment ? true : never\nconst _detailCheck: _AssertDetailCompat = true\nconst _commentCheck: _AssertCommentCompat = true\nvoid _detailCheck\nvoid _commentCheck\n","/**\n * ETag generation helper for JSON payloads.\n * JSON ペイロード向けの ETag 生成ヘルパー。\n */\nimport { createHash } from \"node:crypto\"\n\n/**\n * Generates a SHA-256 ETag for the given data.\n * 指定されたデータの SHA-256 ETag を生成します。\n *\n * @param data The data to hash (will be JSON stringified).\n * @returns The ETag string (wrapped in quotes).\n */\nexport const generateETag = (data: unknown): string => {\n const json = JSON.stringify(data)\n const hash = createHash(\"sha256\").update(json).digest(\"hex\")\n return `\"${hash}\"`\n}\n","/**\n * JSON response helper with shared security headers and ETag support.\n * 共有セキュリティヘッダーと ETag 対応を備えた JSON レスポンスヘルパー。\n */\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport { generateETag } from \"./etag\"\n\nconst defaultLogger = createLogger(LogLevel.INFO, \"[Response]\")\n\n/**\n * Creates JSON response with shared security headers and ETag support.\n * 共有のセキュリティヘッダーと ETag サポート付き JSON レスポンスを生成。\n */\nexport const jsonResponseWithETag = (request: Request, cookie: string | null, payload: Record<string, unknown>, status = 200, logger: DailyReportLogger = defaultLogger): Response => {\n const etag = generateETag(payload)\n const ifNoneMatch = request.headers.get(\"If-None-Match\")\n\n logger.info(`[jsonResponseWithETag] ETag: ${etag}, If-None-Match: ${ifNoneMatch}`)\n\n const headers = new Headers({\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"private, max-age=0, must-revalidate\",\n \"X-Content-Type-Options\": \"nosniff\",\n \"X-Frame-Options\": \"DENY\",\n \"Content-Security-Policy\": \"default-src 'none'\",\n ETag: etag,\n })\n if (cookie) {\n headers.append(\"Set-Cookie\", cookie)\n }\n\n if (request.method === \"GET\" && status === 200 && ifNoneMatch === etag) {\n return new Response(null, { status: 304, headers })\n }\n\n return new Response(JSON.stringify(payload), { status, headers })\n}\n","/**\n * Fan-Out shared reader for the daily report SSE Redis Stream.\n * 日報 SSE 用 Redis Stream の共有リーダーモジュール。\n *\n * 単一の xRead BLOCK ループで全接続にメッセージをブロードキャストし、\n * 接続ごとに Redis TCP を張る問題を解消する。\n */\n\nimport { EventEmitter } from \"node:events\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport type { SqlResultCache } from \"./cache\"\nimport type { DailyReportRedisBlockingClient, DailyReportRedisProvider } from \"./ports\"\n\n/**\n * Compares two Redis Stream IDs numerically.\n * Redis Stream ID を数値比較し、a <= b なら true を返す。\n *\n * Stream ID は `<timestamp_ms>-<sequence>` 形式。\n * 辞書順比較では sequence が可変長のとき不正確になるため、数値分割で比較する。\n */\nexport const isStreamIdLte = (a: string, b: string): boolean => {\n const [aMs, aSeq] = a.split(\"-\").map(Number)\n const [bMs, bSeq] = b.split(\"-\").map(Number)\n if (aMs !== bMs) return aMs < bMs\n return aSeq <= bSeq\n}\n\n/** xRead 結果の 1 エントリに相当する型。 */\nexport type StreamEntry = {\n id: string\n message: Record<string, string>\n}\n\nexport type DailyReportSseReaderConfig = {\n /** ブロッキング xRead 用の専用クライアントを生成する redis プロバイダー。 */\n redis?: DailyReportRedisProvider\n /** SSE Redis Stream キー。 */\n streamKey: string\n /** 受信メッセージに応じてローカル SQL キャッシュを無効化する対象キャッシュ。 */\n cache: SqlResultCache\n /** ロガー (既定は console ベース)。 */\n logger?: DailyReportLogger\n}\n\n/**\n * Shared xRead loop that fans out entries to all active SSE connections.\n * 全 SSE 接続に対してメッセージをファンアウトする共有 xRead ループ。\n *\n * - \"entry\" イベント: 新着エントリ (StreamEntry) をブロードキャスト\n * - \"error\" イベント: 回復不能エラーをブロードキャスト\n * */\nexport class DailyReportSseReader {\n private _emitter = new EventEmitter()\n private _state: \"idle\" | \"running\" | \"stopping\" = \"idle\"\n private _refCount = 0\n // \"$\" = 「購読開始以降の新着のみ」の番兵。ループ開始時に一度だけ具体 ID (ストリーム末尾) へ\n // 解決される (_startLoop 参照)。❗ \"0-0\" を初期値にしてはならない: 初回購読でストリーム全履歴を\n // 再生し、過去イベントの invalidatePrefix が初回コールドフェッチの書き込みガードを不成立にして\n // キャッシュ保存を毒殺する (実測: 履歴 10,006 件の再生で ids バケットが保存されず、次のロードも\n // 全量コールド)。起動直後のプロセスのキャッシュは空で、過去イベントの無効化は無意味。\n // 過去分の取りこぼし補償は接続単位の catch-up (Last-Event-ID + xRange) の責務。\n // 通常の stop/start では従来どおり前回位置を保持し、停止中の実イベントを再開時に追う\n private _lastId = \"$\"\n private _client: DailyReportRedisBlockingClient | undefined\n private readonly _logger: DailyReportLogger\n\n constructor(private readonly config: DailyReportSseReaderConfig) {\n // リスナー上限を緩和 (接続数分)\n this._emitter.setMaxListeners(0)\n this._logger = config.logger ?? createLogger(LogLevel.INFO, \"[SSE Reader]\")\n }\n\n /**\n * Subscribes to the shared reader. Starts the loop on first subscriber.\n * 共有リーダーを購読する。最初の購読者でループを開始する。\n *\n * @param onEntry - 新着エントリのコールバック\n * @param onError - 回復不能エラーのコールバック (省略可)\n * @returns unsubscribe 関数。呼び出すと両リスナーを解除し、最後の購読者解除でループを停止する。\n */\n subscribe(onEntry: (entry: StreamEntry) => void, onError?: (err: Error) => void): () => void {\n this._emitter.on(\"entry\", onEntry)\n if (onError) {\n this._emitter.on(\"error\", onError)\n }\n this._refCount++\n\n // 最初の購読者でループ開始\n if (this._refCount === 1) {\n void this._startLoop()\n }\n\n // unsubscribe\n return () => {\n this._emitter.removeListener(\"entry\", onEntry)\n if (onError) {\n this._emitter.removeListener(\"error\", onError)\n }\n this._refCount--\n if (this._refCount <= 0) {\n this._refCount = 0\n void this._stopLoop()\n }\n }\n }\n\n /**\n * Destroys the reader for test cleanup.\n * テストクリーンアップ用のデストラクタ。\n */\n async destroy(): Promise<void> {\n this._refCount = 0\n await this._stopLoop()\n this._emitter.removeAllListeners()\n this._lastId = \"$\"\n }\n\n /**\n * Internal xRead BLOCK loop.\n * 内部 xRead BLOCK ループ。\n */\n private async _startLoop(): Promise<void> {\n // stopping 中なら完了を待つ\n while (this._state === \"stopping\") {\n await new Promise((resolve) => setTimeout(resolve, 50))\n }\n if (this._state === \"running\") return\n // 停止完了を待つ間に最後の購読者が解除されていたら起動しない\n // (購読者 0 の孤児ループが専用 Redis 接続を握り続けるのを防ぐ)\n if (this._refCount === 0) return\n\n this._state = \"running\"\n\n // 専用クライアント取得。createClient が reject した場合も _state を idle へ戻し、\n // 再購読でループを再開できるようにする (注入 redis 実装が例外を投げても state-stuck しない)。\n try {\n this._client = await this.config.redis?.createClient()\n } catch (err) {\n this._state = \"idle\"\n this._emitError(err instanceof Error ? err : new Error(\"Failed to create Redis client for SSE reader\"))\n return\n }\n if (!this._client) {\n this._state = \"idle\"\n this._emitError(new Error(\"Failed to create Redis client for SSE reader\"))\n return\n }\n\n // エラーリスナー\n this._client.on(\"error\", (err) => {\n this._logger.error(\"Redis client error:\", err)\n })\n\n // ❗ \"$\" は xRead ごとにサーバー側で「その時点の末尾」へ再解決されるため、BLOCK タイムアウトで\n // 再武装する合間に届いたエントリを永続的に取りこぼす (\"$\" が有効なのは最初の 1 回だけ、が\n // Redis の規約)。ループへ入る前に一度だけ具体 ID へ固定する。空ストリームの \"0-0\" は\n // 履歴が存在しないので安全 (毒殺は「履歴があるのに 0-0」の場合のみ)。\n // 解決に失敗したら \"$\" のまま縮退する (取りこぼし窓が残るだけで、履歴再生よりは安全)\n if (this._lastId === \"$\") {\n try {\n const shared = await this.config.redis?.getClient()\n if (shared) {\n const latest = await shared.xRevRange(this.config.streamKey, \"+\", \"-\", { COUNT: 1 })\n this._lastId = latest.length > 0 ? latest[0].id : \"0-0\"\n }\n } catch (err) {\n this._logger.warn(\"Failed to resolve stream tail anchor; falling back to $:\", err)\n }\n }\n\n try {\n while (this._state === \"running\" && this._client?.isOpen) {\n const results = await this._client.xRead([{ key: this.config.streamKey, id: this._lastId }], { BLOCK: 5000, COUNT: 100 })\n if (!results) continue\n\n for (const stream of results) {\n for (const msg of stream.messages) {\n this._lastId = msg.id\n // epoch check の補完: SSE 経由でもローカルキャッシュを即座にクリア\n // (epoch check が主防御線。SSE は Redis ダウン時や非 epoch 対象キャッシュへのフォールバック)\n try {\n const parsed = msg.message?.data ? JSON.parse(msg.message.data) : null\n const msgType = parsed?.type\n if (msgType === \"report-create\" || msgType === \"report-delete\" || msgType === \"report-publish\") {\n this.config.cache.invalidatePrefix(\"daily-report:ids\")\n }\n // コメント・ステータス変更時は対象レポートの詳細キャッシュを無効化\n if (msgType === \"comment-add\" || msgType === \"comment-delete\" || msgType === \"status-update\") {\n const reportHubId = parsed?.reportHubId\n if (typeof reportHubId === \"number\") {\n this.config.cache.invalidate(`daily-report:detail:${reportHubId}`)\n this.config.cache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n }\n }\n } catch {\n // JSON パースエラーは無視 (invalidation スキップ = safe 方向)\n }\n this._emitter.emit(\"entry\", { id: msg.id, message: msg.message as Record<string, string> })\n }\n }\n }\n } catch (err) {\n // ClientClosedError は正常停止時に発生する\n const isClientClosed = (err as Error)?.constructor?.name === \"ClientClosedError\"\n if (!isClientClosed && this._state === \"running\") {\n this._logger.error(\"xRead loop error:\", err)\n this._emitError(err instanceof Error ? err : new Error(String(err)))\n }\n } finally {\n // クライアント切断\n if (this._client?.isOpen) {\n try {\n await this._client.quit()\n } catch (_e) {\n // 切断エラーは無視\n }\n }\n this._client = undefined\n this._state = \"idle\"\n\n // ループ終了後に購読者が残っていれば自動再起動\n if (this._refCount > 0) {\n void this._startLoop()\n }\n }\n }\n\n /**\n * Stops the xRead loop by closing the client.\n * クライアントを閉じて xRead ループを停止する。\n */\n private async _stopLoop(): Promise<void> {\n if (this._state !== \"running\") return\n this._state = \"stopping\"\n\n if (this._client?.isOpen) {\n try {\n await this._client.quit()\n } catch (_e) {\n // 切断エラーは無視\n }\n }\n\n // 自分が仕掛けた停止フェーズの完了だけを待つ。\"idle\" 限定で待つと、停止直後の同期的な\n // 自動再起動 (finally 内の idle → running 遷移には await が挟まらない) に追い越された場合に\n // このポーラが永遠に \"idle\" を観測できず、50ms タイマーが恒久的に残留する。\n // \"stopping\" を抜けたら (idle でも running でも) 停止処理としての責務は完了している。\n // _state は別の非同期ループが書き換えるが、TS は await を跨いだクラスフィールドの変化を\n // 追えず \"stopping\" に過剰 narrowing するため、宣言型へキャストして比較する。\n while ((this._state as \"idle\" | \"running\" | \"stopping\") === \"stopping\") {\n await new Promise((resolve) => setTimeout(resolve, 50))\n }\n }\n\n /**\n * Emits an \"error\" event only when a listener exists (bare \"error\" emit throws).\n * リスナーが居るときだけ \"error\" イベントを emit する処理 (リスナー 0 の \"error\" emit は\n * EventEmitter 仕様で throw し、非同期文脈では unhandled rejection = プロセス危険になるため)。\n */\n private _emitError(err: Error): void {\n if (this._emitter.listenerCount(\"error\") > 0) {\n this._emitter.emit(\"error\", err)\n } else {\n this._logger.error(\"SSE reader error (no error listeners):\", err)\n }\n }\n}\n","/**\n * Source-type visibility set: normalization, digest, and predicate helpers.\n * sourceType 可視集合の正規化・ダイジェスト・判定ヘルパー群。\n *\n * 中立文字列 (`DailyReportHub.sourceType` の値) のみを扱い、いかなる認可パッケージの型にも\n * 依存しない。可視集合は 2 状態を厳密に区別する:\n *\n * - `null` = 無制限 (全件)。ポート未注入時の既定であり、現行互換の意味を持つ。\n * - `Set` = その集合に限定。**空集合は 0 件 (fail-close)** であって無制限ではない。\n *\n * この 2 状態の取り違えは情報漏洩か機能停止に直結するため、判定・畳み込み・キー生成を\n * 本ファイルへ一元化し、ハンドラ層とサービス層の双方がここだけを参照する。\n *\n * 閲覧範囲は**次元ごと**にこの 2 状態を持つ:\n *\n * - `visible` = 本文 (日報行) を閲覧してよい区分。\n * - `commentable` = コメントを閲覧・記入してよい区分。常に `visible` の部分集合。\n *\n * 次元が増えても状態数は増やさない。「もう一方の次元に従う」は\n * {@link DailyReportViewerScope.restrictTo} が**構築時に解決**するため、各次元は最後まで\n * 厳密に 2 状態であり、本ファイルの判定ヘルパーは次元を問わずそのまま適用できる。\n */\nimport { inArray, type SQL, sql } from \"drizzle-orm\"\nimport type { AnyMsSqlColumn } from \"drizzle-orm/mssql-core\"\nimport type { DailyReportVisibilityDimensions } from \"./ports\"\n\n/**\n * Normalized visible set. null = unrestricted (all rows), ReadonlySet = restricted (empty = zero rows).\n * 正規化済みの可視集合。null は無制限 (全件)、ReadonlySet は限定 (空集合は 0 件)。\n */\nexport type VisibleSourceTypeSet = ReadonlySet<string> | null\n\n/**\n * Separator for digest concatenation; never occurs inside a sourceType value.\n * ダイジェスト連結用の区切り文字。sourceType の値には決して現れない制御文字。\n */\nconst DIGEST_SEPARATOR = \"\\u0001\"\n\n/**\n * Maximum digest length before falling back to a hash.\n * ハッシュへ退避する前のダイジェスト最大長。\n *\n * キャッシュキーが際限なく伸びるのを防ぐだけの閾値であり、通常の区分数 (数個) では到達しない。\n */\nconst DIGEST_RAW_MAX_LENGTH = 64\n\n/**\n * Folds a stored sourceType value into its comparison key (case-insensitive only).\n * 格納済みの sourceType 値を比較キーへ畳み込む処理 (大小文字非依存のみ)。\n *\n * ❗ ここで `trim()` してはならない。SQL 側の述語は `LOWER(source_type)` だけを掛けており\n * 列値をトリムしないため、JS 側だけ切り詰めると同じ行について SSE (JS 判定) は可視・\n * 読み取り (SQL 判定) は不可視という乖離が生まれる (SSE が fail-open 側へずれる)。\n * 両者を「列値は畳み込むだけ」に揃え、空白混じりの値は双方で不可視 = fail-close とする。\n */\nconst foldStoredSourceType = (raw: string): string => raw.toLowerCase()\n\n/**\n * Folds a resolver-supplied token into its comparison key (trimmed, case-insensitive).\n * リゾルバ由来のトークンを比較キーへ畳み込む処理 (前後空白除去・大小文字非依存)。\n *\n * トークンは人手で設定された設定値であり、前後の空白は書き間違いとして吸収してよい。\n * 一方で列値 ({@link foldStoredSourceType}) は DB のデータなので勝手に切り詰めない。\n */\nconst foldResolverToken = (raw: string): string => raw.trim().toLowerCase()\n\n/**\n * Normalizes a raw resolver return into a VisibleSourceTypeSet.\n * ポートの生戻り値を正規化済み可視集合へ変換する処理。\n *\n * `null` / `undefined` は無制限 (`null`) へ。配列は各要素を畳み込み・空文字除去・重複排除して\n * `Set` 化する。**空配列は空 `Set` (0 件) を返し、`null` (無制限) へは決して倒さない** —\n * この取り違えが情報漏洩か機能停止に直結するため。\n *\n * ❗ 引数は**単一次元のトークン配列に限る**。次元別オブジェクト\n * ({@link DailyReportVisibilityDimensions}) をここへ渡すと配列ガードに掛かって全拒否になるため、\n * 次元の解体は {@link DailyReportViewerScope.restrictTo} が行い、ここには常に配列だけが届く。\n *\n * ❗ オーバーロードで「**配列を渡した場合は決して `null` (無制限) を返さない**」という不変条件を\n * 型に載せている。これは実装の偶然ではなく本ファイルの中核契約 (空配列は 0 件であって無制限では\n * ない) そのものであり、呼び出し側が `null` 分岐を書いて無制限へ倒す余地を型で塞ぐ。\n *\n * @param raw Raw single-dimension resolver value. 単一次元の生値。\n * @returns Normalized visible set. 正規化済みの可視集合。\n */\nexport function normalizeVisibleSourceTypes(raw: readonly string[]): ReadonlySet<string>\nexport function normalizeVisibleSourceTypes(raw: readonly string[] | null | undefined): VisibleSourceTypeSet\nexport function normalizeVisibleSourceTypes(raw: readonly string[] | null | undefined): VisibleSourceTypeSet {\n if (raw == null) {\n return null\n }\n // 配列以外 (契約違反。例: リゾルバが誤って文字列 \"NI\" を返す) は fail-close で空集合にする。\n // ガードが無いと `for...of \"NI\"` が文字単位で反復し {\"n\",\"i\"} という誤った可視集合になる\n if (!Array.isArray(raw)) {\n return new Set<string>()\n }\n const set = new Set<string>()\n for (const token of raw) {\n // 非文字列要素は黙って捨てる (可視性を広げる方向には決して働かない)\n if (typeof token !== \"string\") {\n continue\n }\n const folded = foldResolverToken(token)\n if (folded.length > 0) {\n set.add(folded)\n }\n }\n return set\n}\n\n/**\n * Tests whether a report's sourceType is visible under the given set.\n * ある日報の sourceType が可視集合に含まれるかの三値判定。\n *\n * - 無制限 (`null`): 常に `true`。\n * - 限定 + sourceType 判明: 畳み込み一致で判定。\n * - 限定 + sourceType 不明 (`null` / `undefined`): `false` — 制限された閲覧者は「不明な区分」を\n * 見られない。区分を運ばない旧 SSE エントリ等を fail-close 側へ倒すための規則。\n *\n * @param set Normalized visible set. 正規化済みの可視集合。\n * @param sourceType The row's source type, if known. 判明していれば当該行のソース種別。\n * @returns True when visible. 可視なら true。\n */\nexport function isSourceTypeVisible(set: VisibleSourceTypeSet, sourceType: string | null | undefined): boolean {\n if (set === null) {\n return true\n }\n if (sourceType == null) {\n return false\n }\n return set.has(foldStoredSourceType(sourceType))\n}\n\n/**\n * Tells whether the set denies everything (zero rows).\n * 可視集合が全拒否 (0 件) を意味するかを判定する処理。\n *\n * 真のとき呼び出し側は DB へ問い合わせず即座に空を返してよい (無駄クエリの排除)。\n * これは性能上の短絡であり、認可の防御そのものは {@link buildVisibleSourceTypeCondition} が担う。\n *\n * @param set Normalized visible set. 正規化済みの可視集合。\n * @returns True when nothing is visible. 何も可視でないなら true。\n */\nexport function deniesEverySourceType(set: VisibleSourceTypeSet): boolean {\n return set !== null && set.size === 0\n}\n\n/**\n * Builds the SQL condition restricting rows to the visible source types.\n * 可視 sourceType へ行を限定する SQL 条件を生成する処理 (三値)。\n *\n * {@link isSourceTypeVisible} の SQL 版であり、同じ三値を同じ向きへ写像する:\n *\n * - 無制限 (`null`): `undefined` を返す。drizzle の `and(...)` は `undefined` を無視するため、\n * 既存の述語がそのまま残る (現行互換)。\n * - 全拒否 (空集合): `1 = 0` を返す。**決して `undefined` へ倒さない** — 呼び出し側の短絡漏れが\n * あっても生成器自身が fail-close する二重防御であり、ここを緩めると認可が反転する。\n * - 限定: `LOWER(<column>) IN (...)`。トークンは正規化時に小文字畳み込み済みなので両辺が小文字となり、\n * DB の照合順序 (collation) に依存せず決定的に大小文字非依存で一致する。\n *\n * @param sourceTypeColumn The source-type column to constrain. 制限対象の sourceType 列。\n * @param set Normalized visible set. 正規化済みの可視集合。\n * @returns Condition, or undefined when unrestricted. 条件 (無制限なら undefined)。\n */\nexport function buildVisibleSourceTypeCondition(sourceTypeColumn: AnyMsSqlColumn, set: VisibleSourceTypeSet): SQL | undefined {\n if (set === null) {\n return undefined\n }\n if (set.size === 0) {\n return sql`1 = 0`\n }\n return inArray(sql`LOWER(${sourceTypeColumn})`, [...set])\n}\n\n/**\n * Request-scoped viewer scope carrying the normalized visible source-type set.\n * 正規化済みの可視 sourceType 集合を運ぶ、リクエストスコープの閲覧範囲。\n *\n * このクラスは private フィールドを持つことで**名目型**になっており、`{ visible: null }` のような\n * 構造的偽造も、`{ ...scope, visible: null }` のようなスプレッドによる widening も型が拒否する\n * (`unique symbol` ブランドでは後者を防げないことを実測で確認済み)。\n *\n * 生成経路は {@link DailyReportViewerScope.restrictTo} と {@link DailyReportViewerScope.unrestricted}\n * の 2 つだけであり、`null` を「可視性」としてそのまま綴ることはできない。結果として、\n * **絞り込まれない読み取りは必ず `unrestricted(理由)` として現れる**ため、\n * `grep -rn \"DailyReportViewerScope.unrestricted(\"` が fail-open の全数リストになる。\n */\nexport class DailyReportViewerScope {\n /**\n * 正規化済みの本文可視集合を保持する private スロット。\n *\n * ❗ public な読み取り専用プロパティにしてはならない。`readonly` は型のみの制約であり、\n * `Object.assign(scope, { visible: null })` は型検査も実行時も素通りして実体を書き換える\n * (実測済み)。private スロット + getter なら代入が実行時にも失敗する。\n */\n readonly #visible: VisibleSourceTypeSet\n\n /**\n * 正規化済みのコメント可視集合を保持する private スロット。\n *\n * 構築時に確定済みであり、`#visible` と同じ 2 状態しか取らない (`null` = 無制限 / `Set` = 限定)。\n * 「`read` に従う」を遅延 sentinel として保持すると次元あたり 3 状態目が生まれ、判定・畳み込み・\n * キー生成の全てを分岐させる必要が出るため、必ず {@link DailyReportViewerScope.restrictTo} で\n * 解決してからここへ入れる。\n */\n readonly #commentable: VisibleSourceTypeSet\n\n private constructor(visible: VisibleSourceTypeSet, commentable: VisibleSourceTypeSet) {\n this.#visible = visible\n this.#commentable = commentable\n }\n\n /** 正規化済みの本文可視集合。`null` は無制限、空集合は 0 件 (fail-close)。 */\n get visible(): VisibleSourceTypeSet {\n // ❗ 実体をそのまま返さない。`ReadonlySet` は型のみの制約で、実体の `Set` は\n // `(scope.visible as unknown as Set<string>).add(...)` で in-place に広げられる (実測済み)。\n // 毎回コピーを返すことで、外部から可視集合を広げる経路を断つ\n return this.#visible === null ? null : new Set(this.#visible)\n }\n\n /**\n * 正規化済みのコメント可視集合。`null` は無制限、空集合は 0 件 (fail-close)。\n *\n * 常に `visible` の部分集合である (構築時に積集合を取っているため)。\n */\n get commentable(): VisibleSourceTypeSet {\n // `visible` と同じ理由でコピーを返す\n return this.#commentable === null ? null : new Set(this.#commentable)\n }\n\n /**\n * Builds a scope restricted to the given source types (empty input = zero rows).\n * 指定した sourceType へ限定した閲覧範囲を生成する処理 (空入力は 0 件)。\n *\n * 入力は {@link normalizeVisibleSourceTypes} を通るため、大小文字・前後空白・重複・非文字列は\n * ここで吸収される。素の配列は `{ read: <配列> }` と同義で、コメント次元は `read` に従う。\n *\n * ❗ 引数は `null` / `undefined` を**受け付けない**。受け付けると「restrict」という名前のまま\n * 無制限スコープを生む第 2 の入口になり、「絞り込まれない読み取りは必ず\n * {@link DailyReportViewerScope.unrestricted} に現れる」という監査不変条件が崩れる。\n * 無制限は必ず理由付きで明示すること。\n *\n * ❗ 判別に `Array.isArray` を使ってはならない。型述語が `arg is any[]` であり\n * `readonly string[]` を負の分岐から除去できないため、`.read` 参照が TS2339 になる\n * (実測済み)。`\"read\" in input` で判別する。文字列など object でない契約違反入力は\n * `in` が実行時に throw するので、先に `typeof` で弾いてから\n * {@link normalizeVisibleSourceTypes} の fail-close 経路へ落とす。\n *\n * @param input Visible source-type tokens, or per-dimension sets. 可視 sourceType のトークン群、または次元別の集合。\n * @returns The scope. 生成した閲覧範囲。\n */\n static restrictTo(input: readonly string[] | DailyReportVisibilityDimensions): DailyReportViewerScope {\n const isDimensions = typeof input === \"object\" && input !== null && \"read\" in input\n const read = normalizeVisibleSourceTypes(isDimensions ? input.read : input)\n // ❗ 実行時に届く値は型より広い (JS 消費側・外部設定由来)。宣言を nullish 込みに広げて\n // 「undefined 以外の nullish」を下で明示的に扱う\n const rawComment: readonly string[] | null | undefined = isDimensions ? input.comment : undefined\n // `comment` 省略時はコメント次元を `read` に従わせる (単一次元指定と同義・現行互換)。\n // ❗ `undefined` **だけ**がこの綴り。他の nullish を同じ意味にすると綴りが 2 つになる\n if (rawComment === undefined) {\n return new DailyReportViewerScope(read, read)\n }\n const comment = normalizeVisibleSourceTypes(rawComment)\n // ❗ `read` との積集合へ狭める。コメントは必ず read で絞られた詳細の内側にしか載らないため、\n // read 外のコメントトークンは意味を持たない。ここで落としておくと、後続の全判定が\n // 「commentable ⊆ visible」を前提にできる (設定ミスが可視性を広げる経路が消える)\n const narrowed = new Set<string>()\n // ❗ `?? []` は死んだ分岐ではない。`normalizeVisibleSourceTypes` は nullish を**無制限**へ\n // 写すため、契約違反の `comment: null` をそのまま採ると read より広い集合が生まれる\n // (fail-open)。ここで空反復へ倒し、全拒否 = fail-close に揃える\n for (const token of comment ?? []) {\n if (read.has(token)) {\n narrowed.add(token)\n }\n }\n return new DailyReportViewerScope(read, narrowed)\n }\n\n /**\n * Builds an unrestricted scope; every call site is an audited fail-open.\n * 無制限の閲覧範囲を生成する処理。全呼び出し箇所が監査対象の fail-open となる。\n *\n * ❗ 理由文字列は必須。読み捨てるが、呼び出し箇所の全数列挙と「なぜここは絞らなくてよいか」の\n * 記録を強制するために受け取る。\n *\n * ❗ **両次元**を無制限にする。片方だけ残すと「無制限」という名前が嘘になり、監査 grep の\n * 結果が「絞られていない読み取りの全数」でなくなる。\n *\n * @param auditReason Why unrestricted access is correct here. なぜここで無制限が正当かの理由。\n * @returns The unrestricted scope. 無制限の閲覧範囲。\n */\n static unrestricted(auditReason: string): DailyReportViewerScope {\n void auditReason\n return new DailyReportViewerScope(null, null)\n }\n}\n\n/**\n * Computes the cache-key digest for a whole viewer scope (all dimensions).\n * 閲覧範囲全体 (全次元) からキャッシュキー用ダイジェストを算出する処理。\n *\n * スコープに次元が増えてもキャッシュキーの生成点を 1 つに保つための入口。\n *\n * ❗ **必ず全次元を畳み込むこと。** 本文次元だけを畳み込むと、コメント権限だけを剥奪された\n * 同一ユーザーが剥奪前のキャッシュバケットを共有し続け、TTL の間コメント本文入りの詳細を\n * 受け取り続ける (キーが変わらないため無効化も走らない)。\n *\n * 両次元が一致する既定形 (コメント次元が `read` に従う) では**本文のみのダイジェストと同一値**を\n * 返す。既存配備のキャッシュキーは 1 文字も変わらず、キャッシュ再利用率は無退行となる。\n * 次元が分かれるときだけ `d:` 名前空間の下へ**長さ接頭辞付きで連結**するため、写像は単射であり\n * sentinel (`all` / `none`) や単一次元表記 (`s:` / `sh:`) と衝突しない。\n *\n * @param scope Viewer scope. 閲覧範囲。\n * @returns Digest usable as a cache-key component. キャッシュキー構成要素として使えるダイジェスト。\n */\nexport function computeViewerScopeDigest(scope: DailyReportViewerScope): string {\n const read = computeSourceTypeDigest(scope.visible)\n const comment = computeSourceTypeDigest(scope.commentable)\n // 既定 (コメント次元が read に従う) では現行キーと完全同一にする\n if (read === comment) {\n return read\n }\n // ❗ 長さ接頭辞が必須。素の連結では (\"s:x\", \"y\") と (\"s:xy\", \"\") のような異なる組が\n // 同じ文字列になりうる (単射でなくなり、別権限の閲覧者が同一バケットを共有する)\n return `d:${read.length}:${read}${comment}`\n}\n\n/**\n * Computes a stable and bounded cache-key digest of the set.\n * 可視集合から安定・有界なキャッシュキー用ダイジェストを算出する処理。\n *\n * 無制限は `\"all\"`、空集合は `\"none\"`、それ以外は `s:` 名前空間の下へ**長さ接頭辞付きトークンを昇順連結**する。\n * 連結は長さ接頭辞付きなので**単射**であり、通常経路 (`s:`) では論理的に等価な集合が必ず同一の、\n * 異なる集合が必ず別のダイジェストになる。sentinel (`all` / `none`) とも名前空間が分かれるため、\n * 限定集合が無制限 / 全拒否と同じバケットを共有することはない。権限が安定している間はキーが\n * 不変になるため、キャッシュは完全に再利用される (性能無退行)。\n *\n * ❗ 区分数が異常に多い場合だけキー長を抑えるため FNV-1a 32bit へ退避し `sh:` 接頭辞を付ける。\n * **この退避経路に限り理論上の衝突がありうる** (32bit)。衝突しても影響は同一ユーザーの\n * キャッシュ再利用に留まり、認可判定そのものは SQL 述語が担うため漏洩には至らない。\n * 通常の区分数 (数個) では到達しない。\n *\n * @param set Normalized visible set. 正規化済みの可視集合。\n * @returns Digest usable as a cache-key component. キャッシュキー構成要素として使えるダイジェスト。\n */\nexport function computeSourceTypeDigest(set: VisibleSourceTypeSet): string {\n if (set === null) {\n return \"all\"\n }\n if (set.size === 0) {\n return \"none\"\n }\n // 昇順ソートで要素順に依存しないキーにする (同じ集合は常に同じダイジェスト)。\n // ❗ 各トークンは長さ接頭辞付きで連結する。素の区切り文字だけでは、区切り文字を含む\n // トークンが別の集合と同じ連結結果になりうる (単射でなくなる)\n const joined = [...set]\n .sort()\n .map((token) => `${token.length}:${token}`)\n .join(DIGEST_SEPARATOR)\n // ❗ 限定集合の名前空間を `s:` で分ける。分けないと、`\"all\"` や `\"none\"` へ畳み込まれる\n // トークンだけを許された閲覧者が、無制限 / 全拒否と同じキャッシュバケットを共有してしまう\n if (joined.length <= DIGEST_RAW_MAX_LENGTH) {\n return `s:${joined}`\n }\n // 長大キーの退避のみハッシュ化する。衝突しても影響は同一ユーザーのキャッシュ再利用に限られ、\n // かつ TTL で失効するが、生連結と混同しないよう接頭辞で区別する\n let hash = 0x811c9dc5\n for (let i = 0; i < joined.length; i++) {\n hash ^= joined.charCodeAt(i)\n hash = Math.imul(hash, 0x01000193)\n }\n return `sh:${(hash >>> 0).toString(16)}`\n}\n","/**\n * React Router loader/action factories for the daily-report HTTP surface.\n * 日報 HTTP サーフェス向けの React Router loader / action 工場。\n *\n * - index.loader: 画面ルートの認証 + 難読化ユーザー ID 解決\n * - api.loader / api.action: `:endpoint` パラメータ式の認証付き API ルーター\n * - sse.loader: Redis Streams ベースのリアルタイム更新 SSE エンドポイント\n */\nimport { normalizeBusinessDateKey } from \"../shared/business-date\"\nimport { computeIdsStreamSnapshotToken, type DailyReportIdsStreamChunkLine, type DailyReportIdsStreamCursor, type DailyReportIdsStreamErrorLine, findIdsStreamResumeIndex } from \"../shared/ids-stream\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport { type DailyReportSseMessage, dailyReportSseMessageSchema } from \"../shared/sse-schema\"\nimport type { DailyReportDetail, DailyReportItem } from \"../shared/types\"\nimport type { DailyReportAttachmentFailure, DailyReportAuthenticate, DailyReportEncodeUserId, DailyReportIdCodec, DailyReportReadAttachment, DailyReportRedisProvider, DailyReportResolveVisibleSourceTypes, DailyReportVisibilityResolveReason } from \"./ports\"\nimport { jsonResponseWithETag } from \"./response\"\nimport type { DailyReportService } from \"./service\"\nimport type { DailyReportSseReader, StreamEntry } from \"./sse-reader\"\nimport { isStreamIdLte } from \"./sse-reader\"\nimport { DailyReportViewerScope, isSourceTypeVisible, type VisibleSourceTypeSet } from \"./visibility\"\n\ntype LoaderArgs = { request: Request; params: Record<string, string | undefined> }\n\n/** `data()` 相当の JSON レスポンス生成 (react-router 非依存)。`setCookie` 指定時は Set-Cookie を付与する。 */\nconst jsonData = (payload: unknown, init?: { status?: number; setCookie?: string }): Response => {\n const headers = new Headers({ \"Content-Type\": \"application/json\" })\n // セッション破棄の Set-Cookie は未認証応答にも載せる (捨てると無効セッションがブラウザに残る)\n if (init?.setCookie) {\n headers.append(\"Set-Cookie\", init.setCookie)\n }\n return new Response(JSON.stringify(payload), { status: init?.status ?? 200, headers })\n}\n\n// ---------------- ids NDJSON ストリームの定数とヘルパー (純粋・モジュールスコープ) ----------------\n\n/**\n * First-chunk size, tuned for time-to-first-render.\n * 初回チャンクのアイテム数。最初の描画までの時間を優先して小さく保つ。\n *\n * 一覧の可視行 + オーバースキャンを一度に満たせる件数であればよい。\n */\nconst IDS_STREAM_FIRST_CHUNK_SIZE = 30\n\n/**\n * 2 チャンク目以降のアイテム数。1 アイテムが数十バイト (id/日付/種別のみ) と軽いため\n * 大きめに運ぶ。実データは 20 万件超に達するため、小さすぎると行数 (= pull 往復と\n * クライアント側 publish 回数) が肥大する (4000 件 ≈ 240 KB/行、20 万件で約 50 行)。\n */\nconst IDS_STREAM_NEXT_CHUNK_SIZE = 4000\n\n/**\n * Sanitized message for mid-stream failures (no internal details).\n * ストリーム送出中の失敗をクライアントへ伝える定型文 (内部情報は載せない)。\n */\nconst IDS_STREAM_ERROR_CLIENT_MESSAGE = \"stream interrupted by server error\"\n\n/** 再開カーソルの営業日として受け付ける唯一の形式 (正規化済み YYYY-MM-DD)。 */\nconst IDS_STREAM_DATE_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\n\n/**\n * Heartbeat interval while the next chunk is pending (ms).\n * 次チャンク待ちの間に空行ハートビートを送る間隔 (ms)。\n *\n * 全量クエリ (20 万件超) の確定待ちで無音が続くと、クライアントの stall ウォッチドッグ\n * (既定 30s) が生きた接続を half-open と誤認する。それより十分短い間隔で空行を送り、\n * read を解決させて誤検知を防ぐ。空行はクライアント側で読み飛ばされる (`trim() === \"\"`)。\n */\nconst IDS_STREAM_HEARTBEAT_MS = 15_000\n\n/** next() 待ちがハートビート間隔に負けたことを表す番兵。 */\nconst IDS_STREAM_HEARTBEAT = Symbol(\"ids-stream-heartbeat\")\n\n/**\n * Keep-alive ticks between SSE visibility re-resolutions.\n * SSE の可視集合を再解決する間隔 (keep-alive tick 数)。\n *\n * keep-alive は 5 秒間隔なので 12 tick = 約 60 秒。読み取り経路がリクエストごとに再解決するのに対し\n * SSE は接続が長寿命なため、この間隔が権限失効の反映遅延の上限になる。\n */\nconst SSE_VISIBILITY_REFRESH_TICKS = 12\n\n/**\n * Parses and validates resume-cursor query params (fail-closed).\n * 再開カーソルのクエリパラメータを検証付きで解釈する処理 (不正は fail-closed)。\n *\n * - `cursor_report_hub_id` 単独 = 「営業日なし領域」のカーソル (businessDate: null)。\n * - `cursor_business_date` は `cursor_report_hub_id` 無しでは意味を持たないため 400。\n *\n * @param url Request URL. リクエスト URL。\n * @returns Cursor (null = 先頭から) か、検証失敗の印。\n */\n/** スナップショットトークンの形式 (computeIdsStreamSnapshotToken の出力域)。 */\nconst IDS_STREAM_SNAPSHOT_PATTERN = /^[0-9a-f]{1,16}$/\n\nconst parseIdsStreamCursor = (url: URL): { cursor: DailyReportIdsStreamCursor | null; snapshot: string | null } | { invalid: true } => {\n const rawId = url.searchParams.get(\"cursor_report_hub_id\")\n const rawDate = url.searchParams.get(\"cursor_business_date\")\n const rawSnapshot = url.searchParams.get(\"cursor_snapshot\")\n // ❗ トークンの形式違反は 400 にしない。「トークン無し」へ落とせば先頭からの再送で\n // 回復できる — 再開の失敗はデータ喪失ではなく、接続ごと落とすより常に安全側\n const snapshot = rawSnapshot !== null && IDS_STREAM_SNAPSHOT_PATTERN.test(rawSnapshot) ? rawSnapshot : null\n\n if (rawId === null) {\n // 営業日だけのカーソルは並び順上の位置を決められない\n return rawDate === null ? { cursor: null, snapshot } : { invalid: true }\n }\n\n const reportHubId = Number.parseInt(rawId, 10)\n if (!Number.isSafeInteger(reportHubId) || String(reportHubId) !== rawId || reportHubId <= 0) {\n return { invalid: true }\n }\n\n if (rawDate === null) {\n return { cursor: { businessDate: null, reportHubId }, snapshot }\n }\n if (!IDS_STREAM_DATE_PATTERN.test(rawDate)) {\n return { invalid: true }\n }\n return { cursor: { businessDate: rawDate, reportHubId }, snapshot }\n}\n\n/**\n * Emits a finalized id array as chunked NDJSON lines from a resume position.\n * 確定済み ID 配列を再開位置からチャンク行として送出するジェネレーター。\n *\n * `total` は最初に yield する行へ載せる (= 接続内で全量が確定した最初の行。\n * キャッシュヒットの新規接続と再開接続では先頭行、高速先頭ページ経路では 2 行目)。\n *\n * @param items Stream-ordered full array. ストリーム順の全量配列。\n * @param resumeKey Resume position (null = 先頭から)。\n * @param firstChunkSize このジェネレーターの最初のチャンクサイズ。\n */\nasync function* streamLinesFromFullArray(items: DailyReportItem[], resumeKey: DailyReportIdsStreamCursor | null, firstChunkSize: number, options: { snapshot: string; restarted?: boolean }): AsyncGenerator<DailyReportIdsStreamChunkLine> {\n let position = resumeKey ? findIdsStreamResumeIndex(items, resumeKey) : 0\n let isFirstYield = true\n while (true) {\n const chunkItems = items.slice(position, position + (isFirstYield ? firstChunkSize : IDS_STREAM_NEXT_CHUNK_SIZE))\n position += chunkItems.length\n const isLast = position >= items.length\n const lastItem = chunkItems[chunkItems.length - 1]\n yield {\n kind: \"chunk\",\n items: chunkItems,\n nextCursor: isLast || !lastItem ? null : { businessDate: lastItem.businessDate, reportHubId: lastItem.reportHubId },\n isLast,\n ...(isFirstYield ? { total: items.length } : {}),\n // 権威行は必ずトークンを運ぶ (resume 経路も含む)。載せ忘れると最初の再開成功で\n // クライアントのトークンが失われ、2 度目以降の切断が全量再送へ退化する\n snapshot: options.snapshot,\n // restarted は先頭行にのみ載せる。全行に載せるとクライアントが行ごとに初期化し、\n // 直前の行で受けたデータを捨てる\n ...(isFirstYield && options.restarted ? { restarted: true } : {}),\n }\n isFirstYield = false\n if (isLast) return\n }\n}\n\n/**\n * Wraps a line iterator into a pull-based NDJSON streaming Response.\n * 行イテレーターを pull 駆動の NDJSON ストリーミング Response へ包む処理。\n *\n * - 切断検知時はイテレーターを畳んで (`return()`) DB 待ちを解放し、close する\n * (素の return は desiredSize > 0 のまま pull が再帰する)。\n * - イテレーターの throw は内部情報を含まない定型エラー行 + 正常クローズへ倒す\n * (クライアントは「isLast 無し終端」をカーソル再開のシグナルとして扱う)。\n */\nconst buildIdsStreamResponse = (cookie: string | null, lines: AsyncGenerator<DailyReportIdsStreamChunkLine>, request: Request, logger: DailyReportLogger): Response => {\n const encoder = new TextEncoder()\n // cancel() の lines.return() は「pull が DB 待ちの next() を保留中」だと先取りできない。\n // 待ちが明けた後の enqueue が閉じたストリームへ落ちて catch に入るため、切断は\n // フラグで決定的に識別し、通常イベントとして静かに畳む (ERROR ログにしない —\n // 全量クエリ中の離脱は日常イベントで、本物の送出失敗が雑音に埋もれる)\n let cancelled = false\n // next() の解決はハートビートを跨いで保持する。ハートビートが先着したら空行を出し、\n // 同じ next() を次の pull で await し直す (毎回 next() を呼び直すと二重取得になる)。\n let pendingNext: Promise<IteratorResult<DailyReportIdsStreamChunkLine>> | null = null\n const stream = new ReadableStream<Uint8Array>({\n async pull(controller) {\n if (cancelled || request.signal.aborted) {\n await lines.return(undefined).catch(() => {})\n try {\n controller.close()\n } catch {\n // cancel 済みコントローラーの close は失敗してよい\n }\n return\n }\n let heartbeatTimer: ReturnType<typeof setTimeout> | null = null\n try {\n if (pendingNext === null) {\n pendingNext = lines.next()\n }\n // 次チャンクの確定待ちが長引く間、接続の生存を示す空行を送る\n const heartbeatPromise = new Promise<typeof IDS_STREAM_HEARTBEAT>((resolve) => {\n heartbeatTimer = setTimeout(() => resolve(IDS_STREAM_HEARTBEAT), IDS_STREAM_HEARTBEAT_MS)\n })\n const raced = await Promise.race([pendingNext, heartbeatPromise])\n if (heartbeatTimer !== null) {\n clearTimeout(heartbeatTimer)\n heartbeatTimer = null\n }\n if (raced === IDS_STREAM_HEARTBEAT) {\n // next() はまだ保留中。空行だけ送って pending は次の pull へ持ち越す\n if (cancelled || request.signal.aborted) {\n await lines.return(undefined).catch(() => {})\n try {\n controller.close()\n } catch {\n // cancel 済みコントローラーの close は失敗してよい\n }\n return\n }\n controller.enqueue(encoder.encode(\"\\n\"))\n return\n }\n pendingNext = null\n const { value, done } = raced\n if (done) {\n controller.close()\n return\n }\n // 保留中の next() の間に切断された場合はここで検知して静かに終える\n if (cancelled || request.signal.aborted) {\n await lines.return(undefined).catch(() => {})\n try {\n controller.close()\n } catch {\n // cancel 済みコントローラーの close は失敗してよい\n }\n return\n }\n controller.enqueue(encoder.encode(`${JSON.stringify(value)}\\n`))\n } catch (error) {\n if (heartbeatTimer !== null) {\n clearTimeout(heartbeatTimer)\n }\n // next() が throw した = このイテレーターは畳む。保留参照を残さない\n pendingNext = null\n if (cancelled || request.signal.aborted) {\n // 切断起因の enqueue/close 失敗はエラーではない\n return\n }\n logger.error(`ids-stream emission failed: ${error instanceof Error ? error.message : String(error)}`)\n try {\n // ❗ 素のリテラルを stringify に渡さない。JSON.stringify は any を受けるため、\n // 型を束縛しないとワイヤユニオンの `kind` 要求がここだけ素通りする\n const errorLine: DailyReportIdsStreamErrorLine = { kind: \"error\", error: IDS_STREAM_ERROR_CLIENT_MESSAGE }\n controller.enqueue(encoder.encode(`${JSON.stringify(errorLine)}\\n`))\n controller.close()\n } catch {\n // 既に切断済みなら何もできない\n }\n }\n },\n cancel() {\n cancelled = true\n void lines.return(undefined).catch(() => {})\n },\n })\n\n const headers = new Headers({\n \"Content-Type\": \"application/x-ndjson; charset=utf-8\",\n // no-transform: compression ミドルウェア/中間プロキシによるバッファリングを禁止する (SSE と同じ理由)\n \"Cache-Control\": \"private, no-cache, no-transform\",\n \"X-Content-Type-Options\": \"nosniff\",\n })\n if (cookie) {\n headers.append(\"Set-Cookie\", cookie)\n }\n return new Response(stream, { status: 200, headers })\n}\n\n// ---------------- 添付配信の定数とヘルパー (純粋・モジュールスコープ) ----------------\n\n/** 添付 1 件あたりの既定上限。サービス側のワイヤ上限 (既定 64 MiB) より必ず低く保つ。 */\nconst DEFAULT_ATTACHMENT_MAX_BYTES = 32 * 1024 * 1024\n\n/**\n * Default per-user rate limit (calls per minute), enforced per process.\n * ユーザー単位の既定レート上限 (1 分あたり)。ただし**プロセス単位**で計上する。\n *\n * バケットはハンドラーのクロージャに置く素の `Map` であり、プロセス間で共有されない。\n * 消費アプリが Node Cluster 等で複数ワーカーを起動する場合、コンテナ全体の実効上限は\n * この値のワーカー数倍になる。全体で厳密に効かせたいなら共有ストア (Redis 等) が要る。\n */\nconst DEFAULT_ATTACHMENT_RATE_LIMIT_PER_MINUTE = 60\n\n/**\n * Default simultaneous-read limit, enforced per process.\n * 既定の同時実行上限。**プロセス単位**で計上する。\n *\n * 1 リクエストが上限バイト数をヒープへ載せるため小さく保つ。ヒープ見積りは\n * 「この値 × `attachmentMaxBytes` × ワーカー数」であり、ワーカー数を掛け忘れると\n * コンテナのメモリ上限を実際の数倍で見誤る。消費アプリはワーカー数を織り込んだ値を\n * `attachmentConcurrency` で明示注入すること。\n *\n * スロットは**本文をクライアントへ送出し終えるまで**保持する。読み取り完了時点で\n * 解放すると、前の応答のバイト列がヒープに載ったまま次の読み取りが始まるため、\n * 上の見積りが成立しなくなる (実際の同時保持数は無制限になる)。\n */\nconst DEFAULT_ATTACHMENT_CONCURRENCY = 4\n\n/**\n * Backstop for releasing a concurrency slot when the client never drains the body.\n * クライアントが本文を読み切らない場合に同時実行スロットを解放するバックストップ。\n *\n * スロットを送出完了まで保持する以上、接続を張ったまま読み止めたクライアントが\n * スロットを永久に占有できてしまう。これは締め切りではなく枯渇防止の保険であり、\n * 超過しても応答は中断しない (単にゲートの会計上、保持されていない扱いにするだけ)。\n */\nconst ATTACHMENT_BODY_FLUSH_TIMEOUT_MS = 120_000\n\n/** レート制限テーブルの上限件数。無制限 Map はプロセス寿命の間だけ単調増加するリークになる。 */\nconst RATE_LIMITER_MAX_ENTRIES = 1024\n\n/** レート上限超過時に提示する再試行間隔 (秒)。 */\nconst RATE_LIMIT_RETRY_AFTER_SECONDS = 60\n\n/** 同時実行上限に達したときに提示する再試行間隔 (秒)。 */\nconst CONCURRENCY_RETRY_AFTER_SECONDS = 5\n\n/**\n * Media types allowed to render inline in the browser.\n * ブラウザーへインライン表示してよい media type。\n *\n * これ以外は必ず添付ダウンロードにし、Content-Type も `application/octet-stream` へ落とす。\n * `file_type` は外部システム由来の未検証値であり、`text/html` や `image/svg+xml` を\n * 自オリジンでインライン配信するとセッション Cookie を持つ文脈でスクリプトが動く。\n * `X-Content-Type-Options: nosniff` は「宣言型から外れた推測」を止めるだけで、\n * 宣言された型の実行は止めない。\n */\nconst INLINE_SAFE_MEDIA_TYPES: ReadonlySet<string> = new Set([\"image/png\", \"image/jpeg\", \"image/gif\", \"image/webp\", \"application/pdf\", \"text/plain\"])\n\n/** ヘッダー値として安全で、かつ MIME 型の形をしている値だけを通す。 */\nconst HEADER_SAFE_PATTERN = /^[\\x20-\\x7E]+$/\nconst MEDIA_TYPE_PATTERN = /^[\\w.+-]+\\/[\\w.+-]+/\n\n/**\n * Accepts a media type only when it is header-safe and well-formed.\n * ヘッダー安全かつ MIME 型の形をしている場合のみ採用する処理。\n *\n * CR/LF や 0x00-0xFF 外の文字が混ざった値を `new Headers()` へ渡すと TypeError になり、\n * 認可済みのダウンロードが 500 になる。\n *\n * **これは第 2 層である。** 現在の配信経路では、注入値はインライン許可リストに完全一致しないため\n * 必ず `application/octet-stream` へ落ち、ヘッダーには到達しない。つまり本関数を外しても\n * 現状の挙動は変わらない (ミューテーションテストで確認済み)。\n * 許可判定を前方一致や「DB の型を信じる」形へ変えた瞬間に効き始める防御なので、\n * 到達不能なまま放置せず **直接の単体テストで固定する**目的でエクスポートしている。\n *\n * @param value Candidate media type. 候補となる media type。\n * @returns The value when acceptable, otherwise null. 採用可なら値、それ以外は null。\n */\nexport const sanitizeMediaType = (value: string | null | undefined): string | null => (value && HEADER_SAFE_PATTERN.test(value) && MEDIA_TYPE_PATTERN.test(value) ? value : null)\n\n/**\n * Percent-encodes a file name for the RFC 8187 `filename*` parameter.\n * RFC 8187 の `filename*` 用にファイル名をパーセントエンコードする処理。\n *\n * `encodeURIComponent` は `'` `(` `)` `*` を残すが、いずれも RFC 8187 の attr-char ではない。\n *\n * @param name Raw file name. 生のファイル名。\n * @returns Encoded value. エンコード済みの値。\n */\nexport const encodeRfc8187 = (name: string): string => encodeURIComponent(name).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)\n\n/**\n * Builds the ASCII fallback used by the plain `filename` parameter.\n * 素の `filename` パラメータ用の ASCII フォールバックを組み立てる処理。\n *\n * @param name Raw file name. 生のファイル名。\n * @returns ASCII-only, quote-free name. ASCII のみで引用符を含まない名前。\n */\nexport const asciiFallbackFileName = (name: string): string => name.replace(/[^\\x20-\\x7E]/g, \"_\").replace(/[\"\\\\]/g, \"_\")\n\n/**\n * Maps a storage read failure onto the HTTP status the endpoint returns.\n * ストレージ読み取りの失敗種別を、エンドポイントが返す HTTP ステータスへ写像する処理。\n *\n * `not_found` と `denied` はどちらも 404 にする。認可判定は既に SQL 側で完了しているため\n * 横断的な情報漏洩は無いが、実体の有無を 403/404 で区別すると存在オラクルになる。\n *\n * @param reason Failure kind reported by the port. ポートが報告した失敗種別。\n * @returns HTTP status code. HTTP ステータスコード。\n */\nconst attachmentFailureToStatus = (reason: DailyReportAttachmentFailure): number => {\n switch (reason) {\n case \"not_found\":\n case \"denied\":\n case \"invalid_path\":\n return 404\n case \"too_large\":\n return 413\n default:\n return 502\n }\n}\n\n/**\n * Client-facing messages per failure status.\n * 失敗ステータスごとのクライアント向けメッセージ。\n *\n * 404 は「存在しない」「認可されない」「実体が消えた」の合流点なので、\n * どの経路から来ても同一の文面にする (差分が存在オラクルになるため)。\n */\nconst ATTACHMENT_FAILURE_MESSAGES: Readonly<Record<number, string>> = {\n 404: \"Attachment not found\",\n 413: \"Attachment too large\",\n 502: \"Attachment storage unavailable\",\n}\n\n/** レート制限 1 ユーザー分の状態。 */\ntype RateBucket = { tokens: number; lastRefillMs: number }\n\n/**\n * Creates a bounded per-user token-bucket rate limiter (process-local).\n * ユーザー単位のトークンバケット方式レート制限を、件数上限付きで生成する処理 (プロセス内限定)。\n *\n * 上限を設ける理由: 素の `Map` で保持するとプロセス寿命の間だけ単調増加し、\n * 「サーバー側の状態は上限を持つ」という設計方針 (SqlResultCache の >500 GC) に反する。\n * 上限到達時は挿入順が最も古いエントリから落とす。\n *\n * 状態はプロセス内に閉じるため、マルチワーカー配備では実効上限がワーカー数倍になる\n * (`DEFAULT_ATTACHMENT_RATE_LIMIT_PER_MINUTE` 参照)。\n *\n * @param limitPerMinute Allowed calls per minute. 1 分あたりの許可回数。\n * @returns A function that consumes one token and reports whether it was allowed. トークンを 1 つ消費し可否を返す関数。\n */\nconst createRateLimiter = (limitPerMinute: number) => {\n const buckets = new Map<number, RateBucket>()\n return (userId: number, nowMs: number): boolean => {\n const existing = buckets.get(userId)\n // アクセスのたびに再挿入して LRU 順序にする (Map は挿入順を保つ)\n if (existing) buckets.delete(userId)\n\n const bucket = existing ?? { tokens: limitPerMinute, lastRefillMs: nowMs }\n const elapsedMs = Math.max(0, nowMs - bucket.lastRefillMs)\n if (elapsedMs > 0) {\n bucket.tokens = Math.min(limitPerMinute, bucket.tokens + (elapsedMs * limitPerMinute) / 60_000)\n bucket.lastRefillMs = nowMs\n }\n\n const allowed = bucket.tokens >= 1\n if (allowed) bucket.tokens -= 1\n\n buckets.set(userId, bucket)\n while (buckets.size > RATE_LIMITER_MAX_ENTRIES) {\n const oldest = buckets.keys().next()\n if (oldest.done) break\n buckets.delete(oldest.value)\n }\n return allowed\n }\n}\n\n/**\n * Creates a fail-fast concurrency gate (no queueing, process-local).\n * 待ち行列を持たない即時失敗型の同時実行ゲートを生成する処理 (プロセス内限定)。\n *\n * 待たせるとリクエストを掴んだまま滞留するだけなので、上限超過は即座に 503 を返す。\n * カウンタはプロセス内に閉じるため、マルチワーカー配備ではコンテナ全体の同時読み取り数が\n * 上限のワーカー数倍になる (`DEFAULT_ATTACHMENT_CONCURRENCY` 参照)。\n *\n * @param limit Maximum simultaneous holders. 同時保持数の上限。\n * @returns Acquire/release pair. 取得と解放の組。\n */\nconst createConcurrencyGate = (limit: number) => {\n let active = 0\n return {\n tryAcquire: (): boolean => {\n if (active >= limit) return false\n active += 1\n return true\n },\n release: (): void => {\n if (active > 0) active -= 1\n },\n }\n}\n\n/**\n * Streams already-materialized bytes and releases the held slot once the body is drained.\n * materialize 済みのバイト列をストリームとして送出し、送出完了時に保持中のスロットを解放する処理。\n *\n * 読み取り完了時点で解放してはならない。その時点でバイト列はまだヒープ上にあり、\n * 応答が流し切られるまで参照が保持されるため、「同時実行上限 × 上限バイト数」という\n * ヒープ見積りが成立しなくなる。ここで送出完了まで保持することで見積りを実際に成立させる。\n *\n * 解放は必ず 1 回だけ行う (正常終了・キャンセル・バックストップのいずれか最初の 1 回)。\n *\n * @param bytes Body bytes to send. 送出する本文のバイト列。\n * @param release Idempotent slot release. 冪等なスロット解放関数。\n * @param timeoutMs Backstop before force-releasing. 強制解放までのバックストップ時間。\n * @returns A stream that emits the bytes once. バイト列を 1 度だけ流すストリーム。\n */\nconst streamAndRelease = (bytes: Uint8Array<ArrayBuffer>, release: () => void, timeoutMs: number): ReadableStream<Uint8Array> => {\n const timer: ReturnType<typeof setTimeout> = setTimeout(release, timeoutMs)\n // タイマーがプロセス終了を待たせないようにする (Node 以外では unref を持たない)\n ;(timer as unknown as { unref?: () => void }).unref?.()\n const finish = () => {\n clearTimeout(timer)\n release()\n }\n\n let sent = false\n return new ReadableStream<Uint8Array>({\n pull(controller) {\n // 1 回目の pull で本文を積み、消費された後の 2 回目の pull で閉じて解放する\n if (sent) {\n controller.close()\n finish()\n return\n }\n sent = true\n controller.enqueue(bytes)\n },\n cancel() {\n finish()\n },\n })\n}\n\n/**\n * Decides whether an SSE message is visible under the viewer's source-type set.\n * SSE メッセージが閲覧者の可視 sourceType 集合において可視かを判定する処理。\n *\n * - `connected` / `status-update`: 常に可視。前段で本人限定済みであり、区分も本文も載らない。\n * - `report-create` / `report-update` / `report-publish`: 埋め込み `report.sourceType` で判定する。\n * 値が欠落していれば `isSourceTypeVisible` の規則により制限閲覧者には不可視 (fail-close)。\n * - `comment-add` / `comment-delete`: 新規コメントは `addComment` の Internal 限定ゲートを通るため\n * 構造上 Internal のみ。明示 `sourceType` があればそれを使い、無い旧エントリは `\"Internal\"` として扱う。\n * - `report-delete`: 明示 `sourceType` で判定し、値の無い旧エントリは fail-close で落とす\n * (削除の取りこぼしは次回取得で自己修復する良性の劣化)。\n *\n * ❗ `comment-add` / `comment-delete` は**コメント次元**で判定する。本文次元で判定すると、\n * 「本文は読めるがコメントは読めない」閲覧者へコメント本文が届く。\n *\n * @param parsed Validated SSE message. 検証済みの SSE メッセージ。\n * @param raw Raw payload carrying server-only fields. サーバー専用フィールドを含む生ペイロード。\n * @param visible Normalized visible source-type set (body dimension). 正規化済みの可視 sourceType 集合 (本文次元)。\n * @param commentable Normalized commentable source-type set. 正規化済みのコメント可視 sourceType 集合。\n * @returns True when the message may be delivered. 配信してよいなら true。\n */\nconst isSseMessageSourceTypeVisible = (parsed: DailyReportSseMessage, raw: Record<string, unknown>, visible: VisibleSourceTypeSet, commentable: VisibleSourceTypeSet): boolean => {\n // 両次元とも無制限なら種別を問わず配信する (現行互換の高速経路)\n if (visible === null && commentable === null) {\n return true\n }\n const rawSourceType = typeof raw.sourceType === \"string\" ? raw.sourceType : undefined\n switch (parsed.type) {\n case \"connected\":\n case \"status-update\":\n return true\n case \"report-create\":\n case \"report-update\":\n case \"report-publish\":\n // 本文次元だけで配信可否を決める。コメント面はフィールド単位で落とす\n // (メッセージごと落とすと日報更新そのものが届かなくなる)\n return isSourceTypeVisible(visible, parsed.report.sourceType)\n case \"comment-add\":\n case \"comment-delete\":\n return isSourceTypeVisible(commentable, rawSourceType ?? \"Internal\")\n case \"report-delete\":\n return isSourceTypeVisible(visible, rawSourceType)\n }\n}\n\n/**\n * Empties the comment arrays of a `report-*` payload when the viewer may not read comments.\n * コメントを読めない閲覧者向けに `report-*` ペイロードのコメント配列を空にする処理。\n *\n * `report-create` / `report-update` / `report-publish` は `report: dailyReportDetailSchema` を\n * 埋め込んでおり、その detail は `externalComments` と `postedComments` を必須キーとして含む。ペイロードは\n * 発行時に**無制限スコープ**で構築されて Redis Stream へ載る共有物であるため、閲覧者ごとの\n * 絞り込みはこの配信直前が唯一の機会になる。\n *\n * ❗ キーを**省いてはならない**。必須キーの欠落は受信側の `safeParse` を失敗させ、メッセージ全体が\n * fail-close で捨てられる (日報更新そのものが永久に届かなくなる)。必ず空配列で上書きする。\n *\n * ❗ `raw` は購読者ごと・エントリごとに `JSON.parse` された固有オブジェクトなので、その場で\n * 書き換えてよい (他の購読者へ波及しない)。\n *\n * ❗ 戻り値は捨てないこと。配信側は元の生文字列 (`fields.data`) をそのまま送る高速経路を持って\n * おり、秘匿したのに再直列化しないと**書き換えが無かったことになる**。\n *\n * @param parsed Validated SSE message. 検証済みの SSE メッセージ。\n * @param raw Raw payload to redact in place. その場で秘匿する生ペイロード。\n * @param commentable Normalized commentable source-type set. 正規化済みのコメント可視 sourceType 集合。\n * @returns True when the payload was modified. ペイロードを書き換えたなら true。\n */\n/**\n * The two comment faces of a detail payload, as wire keys.\n * 詳細ペイロードのコメント 2 面 (ワイヤキー)。\n *\n * ❗ 文字列キーで直接書かない。秘匿対象の生ペイロードは `Record<string, unknown>` なので、\n * 素の文字列は改名を tsc が検知できず、秘匿が無音で no-op になる (= コメント本文が権限の\n * 無い閲覧者へ漏洩する)。`satisfies keyof` で型から導出しておけば、次の改名はこの定数を\n * コンパイルエラーにして追従を強制する。\n */\nconst COMMENT_FACE_KEYS = [\"externalComments\", \"postedComments\"] as const satisfies readonly (keyof DailyReportDetail)[]\n\nconst redactSseReportComments = (parsed: DailyReportSseMessage, raw: Record<string, unknown>, commentable: VisibleSourceTypeSet): boolean => {\n if (commentable === null) {\n return false\n }\n if (parsed.type !== \"report-create\" && parsed.type !== \"report-update\" && parsed.type !== \"report-publish\") {\n return false\n }\n if (isSourceTypeVisible(commentable, parsed.report.sourceType)) {\n return false\n }\n const report = raw.report\n if (typeof report !== \"object\" || report === null) {\n return false\n }\n const target = report as Record<string, unknown>\n for (const key of COMMENT_FACE_KEYS) {\n target[key] = []\n }\n return true\n}\n\nexport type DailyReportHandlersConfig = {\n /** リクエスト認証ポート。 */\n authenticate: DailyReportAuthenticate\n /**\n * 可視 sourceType リゾルバ。未注入なら全件 (現行互換)。\n * 全データ経路 (一覧 / 詳細 / コメント / 添付 / SSE) に一貫して効き、例外は全拒否として扱う。\n */\n resolveVisibleSourceTypes?: DailyReportResolveVisibleSourceTypes\n /** データアクセスサービス。 */\n service: DailyReportService\n /** 内部数値 ID の難読化ポート。 */\n encodeUserId: DailyReportEncodeUserId\n /**\n * 添付 ID の難読化コーデック。未注入なら添付エンドポイントは常に 404。\n * ユーザー ID 用とは別インスタンスを渡すこと (ID 空間の分離)。\n */\n attachmentIdCodec?: DailyReportIdCodec\n /** 添付ファイル読み取りポート。未注入なら添付エンドポイントは常に 404。 */\n readAttachment?: DailyReportReadAttachment\n /** 添付ファイルの最大バイト数 (既定 32 MiB)。サービス側のワイヤ上限より低く保つこと。 */\n attachmentMaxBytes?: number\n /** 添付エンドポイントの 1 分あたり呼び出し上限 (ユーザー単位・既定 60)。 */\n attachmentRateLimitPerMinute?: number\n /** 添付読み取りの同時実行上限 (プロセス単位・既定 4)。 */\n attachmentConcurrency?: number\n /** SSE 用 redis プロバイダー (catch-up の xRange / xRevRange に使用)。 */\n redis?: DailyReportRedisProvider\n /** SSE Fan-Out 共有リーダー。 */\n sseReader: DailyReportSseReader\n /** SSE Redis Stream キー。 */\n streamKey: string\n /** 未ログイン時のリダイレクト先 (index.loader 用、既定 \"/auth/login\")。 */\n loginRedirectPath?: string\n /**\n * `intent=clearCache` (全サーバーキャッシュの一括破棄) を許可するか。既定 false。\n * 開発用アフォーダンスであり、有効化すると認証済みユーザーなら誰でも全ワーカーの\n * キャッシュを無制限に破棄できるため、本番では必ず無効のままにする\n * (消費アプリは開発環境でのみ true を注入する)。\n */\n enableDevCacheClear?: boolean\n /** ロガー (既定は console ベース)。 */\n logger?: DailyReportLogger\n}\n\n/**\n * Creates the daily-report loaders/actions bound to the injected dependencies.\n * 注入依存に束縛された日報 loader / action 群を生成する処理。\n */\nexport function createDailyReportHandlers(config: DailyReportHandlersConfig) {\n const { authenticate, service, encodeUserId, redis, sseReader, streamKey } = config\n const loginRedirectPath = config.loginRedirectPath ?? \"/auth/login\"\n const apiLogger = config.logger ?? createLogger(LogLevel.ERROR, \"[DailyReportAPI]\")\n const visibilityLogger = config.logger ?? createLogger(LogLevel.ERROR, \"[DailyReportVisibility]\")\n const sseLogger = config.logger ?? createLogger(LogLevel.INFO, \"[DailyReportSSE]\")\n const attachmentLogger = config.logger ?? createLogger(LogLevel.INFO, \"[DailyReportAttachment]\")\n\n // 添付配信のプロセス内状態。ファクトリ 1 インスタンスにつき 1 組。\n const attachmentMaxBytes = config.attachmentMaxBytes ?? DEFAULT_ATTACHMENT_MAX_BYTES\n const consumeAttachmentRateToken = createRateLimiter(config.attachmentRateLimitPerMinute ?? DEFAULT_ATTACHMENT_RATE_LIMIT_PER_MINUTE)\n const attachmentGate = createConcurrencyGate(config.attachmentConcurrency ?? DEFAULT_ATTACHMENT_CONCURRENCY)\n\n /**\n * Resolves and normalizes the visible source-type set once per request (fail-closed).\n * リクエストごとに可視 sourceType 集合を 1 回だけ解決・正規化する処理 (fail-close)。\n *\n * ポート未注入なら `null` (無制限・現行互換)。ポートが例外を投げたら**空集合 (全拒否)**へ倒す。\n * 認可ストアの障害時に無制限へ倒れないための唯一の防波堤であり、ここを緩めてはならない。\n *\n * @param request Incoming request. 受信したリクエスト。\n * @param reason Why it is being resolved. 解決理由 (SSE の定期再解決は \"refresh\")。\n * @returns Normalized visible set. 正規化済みの可視集合。\n */\n const resolveViewerScope = async (request: Request, reason: DailyReportVisibilityResolveReason = \"initial\"): Promise<DailyReportViewerScope> => {\n const port = config.resolveVisibleSourceTypes\n if (!port) {\n return DailyReportViewerScope.unrestricted(\"no visibility port injected: this deployment does not restrict by source type\")\n }\n try {\n const resolved = await port(request, { reason })\n // ❗ ポートが nullish を返したら「この配備は区分で絞らない」という契約上の fail-open。\n // restrictTo へ渡して黙って無制限化すると、監査 grep に現れない 2 つ目の\n // 無制限生成経路になるため、ここで明示的に unrestricted として記録する\n return resolved == null ? DailyReportViewerScope.unrestricted(\"visibility port returned null/undefined: this deployment opts out of source-type filtering\") : DailyReportViewerScope.restrictTo(resolved)\n } catch (e) {\n visibilityLogger.error(\"resolveVisibleSourceTypes threw; failing closed (deny all)\", e)\n return DailyReportViewerScope.restrictTo([])\n }\n }\n\n // ---------------- index (画面ルート) ----------------\n\n /**\n * Document loader that authenticates and resolves the obfuscated internal user id.\n * 認証と難読化済み内部ユーザー ID の解決を行うドキュメントローダー。\n */\n const indexLoader = async ({ request }: LoaderArgs) => {\n const { user, cookie } = await authenticate(request, { failureRedirect: loginRedirectPath })\n const headers = new Headers()\n if (cookie) {\n headers.append(\"Set-Cookie\", cookie)\n }\n\n // 文字列モード (`failureRedirect: loginRedirectPath`) では認証失敗が redirect として\n // throw されるため `user` は必ず存在する (`DailyReportAuthenticate` のオーバーロードも\n // そう宣言している)。`if (user)` は到達しないので置かない\n const userId: number | null = await service.getUserIdByExternalId(user.id)\n\n const hashedUserId = userId ? encodeUserId(userId) : null\n\n return { data: { user, userId: hashedUserId }, headers }\n }\n\n // ---------------- api (:endpoint ルーター) ----------------\n\n type User = { id: string }\n type EndpointHandler = (url: URL, cookie: string | null, request: Request, user: User, scope: DailyReportViewerScope) => Promise<Response>\n\n /**\n * Handlers for each API endpoint.\n * 各 API エンドポイントのハンドラー定義。\n */\n const endpointHandlers: Record<string, EndpointHandler> = {\n /**\n * Retrieves daily reports for a specific business date.\n * 指定された営業日の日報一覧を取得する。\n */\n \"business-date\": async (url, cookie, request, user, scope) => {\n const normalizedBusinessDate = normalizeBusinessDateKey(url.searchParams.get(\"businessDate\"))\n const forceRefresh = url.searchParams.get(\"forceRefresh\") === \"true\"\n\n if (!normalizedBusinessDate) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Invalid business date\" } }, 400)\n }\n const reports = await service.getDailyReportsByBusinessDateByExternalId(scope, normalizedBusinessDate, user.id, { forceRefresh })\n return jsonResponseWithETag(request, cookie, { businessDate: normalizedBusinessDate, reports }, 200)\n },\n /**\n * Streams the full daily report id list as NDJSON chunks.\n * 日報 ID 一覧全件を NDJSON チャンクとして逐次配信する。\n *\n * 全量クエリ (`SqlResultCache` の TTL / Redis epoch / in-flight コアレッシングを\n * 素通しで維持) は常に即時に開始する。全量確定を待ってから送出するのは再開接続のみで、\n * その場合の DB エラーはストリーム開始前に throw されて apiLoader の catch が\n * 500 JSON を返す。新規接続はキャッシュ無しの TOP 200 高速先頭ページと競争させ、\n * キャッシュミス時は全量確定前に最初のチャンクを送出する (最初の描画までの時間が\n * 全体データ量に依存しない)。キャッシュヒット時は全量が即勝ちし、従来どおり\n * 確定済み配列のメモリスライス送出になる。\n */\n \"ids-stream\": async (url, cookie, request, user, scope) => {\n const parsed = parseIdsStreamCursor(url)\n if (\"invalid\" in parsed) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Invalid cursor\" } }, 400)\n }\n const forceRefresh = url.searchParams.get(\"forceRefresh\") === \"true\"\n\n // 高速先頭ページは「本リクエストが実際に全量クエリを起動した」ときだけ走らせる。\n // ❗ この因果が B* の健全性の土台である。バケットヒット・進行中フェッチへの相乗り・\n // refresh-ahead が返す全量配列は本リクエストより**古くなり得る**ため、そこへ\n // 新鮮な先頭ページを重ねると「ページに在って全量に無い行 = 削除済み」の判定が\n // 反転し、作成直後の日報を消してしまう。合図の後に始まるページは全量より古く\n // なり得ないので、この判定が初めて健全になる。\n // 副次効果として、キャッシュヒット時に走って捨てられていた TOP-N クエリが消える\n let pageQueryStarted = false\n let resolvePage: ((items: DailyReportItem[]) => void) | null = null\n let rejectPage: ((reason?: unknown) => void) | null = null\n const pagePromise = new Promise<DailyReportItem[]>((resolve, reject) => {\n resolvePage = resolve\n rejectPage = reject\n })\n // 未処理拒否警告を出さない (拒否はレース側で分類する)\n pagePromise.catch(() => {})\n\n // 全量 (SqlResultCache / epoch / コアレッシング維持) は常に即時に走らせ始める\n const fullArrayPromise = service.getDailyReportIdsByExternalId(scope, user.id, {\n forceRefresh,\n // 再開接続は先頭ページを使わない (クライアントは受信済みの続きだけを読む)\n onFetchStart: parsed.cursor\n ? undefined\n : () => {\n pageQueryStarted = true\n service.getDailyReportIdsFirstPageByExternalId(scope, user.id, IDS_STREAM_FIRST_CHUNK_SIZE).then(\n (items) => resolvePage?.(items),\n (error) => rejectPage?.(error),\n )\n },\n })\n // 拒否はイテレーターの await 側で扱う (ここで未処理拒否警告を出さない)\n fullArrayPromise.catch(() => {})\n\n if (parsed.cursor) {\n // 再開接続: クライアントは高速先頭を受信済みなので全量確定を待つ (エラーは 500)。\n // 全量クエリはアプリ側の requestTimeout (既定 60s) で境界づけられ、確定後は\n // スライスだけなので、接続確立後の無音はハートビート対象にならない\n const fullItems = await fullArrayPromise\n const snapshot = computeIdsStreamSnapshotToken(fullItems)\n // ❗ トークンが一致した配列にだけカーソルを信用する。別スナップショットへ当てると、\n // カーソルより前に入った行が丸ごと飛ぶ (再接続のたびの欠損)。不一致・トークン\n // 無し (旧クライアント) は先頭から流し直す — 配備跨ぎタブの再接続が全量再送に\n // なる費用は受容済み (仕様書に明記)\n const resumable = parsed.snapshot !== null && parsed.snapshot === snapshot\n return buildIdsStreamResponse(cookie, streamLinesFromFullArray(fullItems, resumable ? parsed.cursor : null, IDS_STREAM_FIRST_CHUNK_SIZE, { snapshot, restarted: !resumable }), request, apiLogger)\n }\n\n /**\n * 新規接続: 「高速先頭ページ (TOP n・キャッシュ無し)」と「全量 (キャッシュ経由)」を\n * 競争させ、先に確定した方で最初のチャンクを出す。キャッシュヒット時はページ自体が\n * 起動しないので全量が唯一の経路になる (従来どおり先頭行に total)。キャッシュミス時は\n * 先頭ページが先に届き、最初の描画は全量クエリの確定を待たない。\n *\n * ❗ 先頭ページは**暫定 (provisional)** として送る。全量配列とは別スナップショットなので\n * 「観測した」と扱うと、送出後に消えた行が完走剪定を免れて幽霊行になる。暫定行は\n * クライアントが描画はするが観測集合へ刻まないため、権威パスに現れなかった行が\n * 完走時に正確に落ちる。権威パスは**必ず先頭から**流し直すので、ページ送出後に\n * 先頭領域へ入ってきた行の取りこぼし (欠損) も構造的に起きない。\n */\n const lineIterator = (async function* (): AsyncGenerator<DailyReportIdsStreamChunkLine> {\n const winner = await Promise.race([\n fullArrayPromise.then(\n (items) => ({ kind: \"full\" as const, items }),\n (error) => ({ kind: \"full-error\" as const, error }),\n ),\n pagePromise.then(\n (items) => ({ kind: \"page\" as const, items }),\n () => ({ kind: \"page-error\" as const }),\n ),\n ])\n\n let fullItems: DailyReportItem[] | null = null\n let emittedProvisionalPage = false\n\n if (winner.kind === \"full\") {\n fullItems = winner.items\n } else if (winner.kind === \"page\") {\n const pageItems = winner.items\n if (pageItems.length < IDS_STREAM_FIRST_CHUNK_SIZE) {\n // 先頭ページが上限未満 = データセット全量。しかもページは全量クエリより\n // 後に始まっているので、これが最新の権威である。全量を待たずに完結する。\n // ❗ ここを provisional にしてはならない (小規模データセットが完走剪定で全消しになる)\n yield { kind: \"chunk\", items: pageItems, nextCursor: null, isLast: true, total: pageItems.length }\n return\n }\n // ❗ カーソルは載せない。未確認領域を指すカーソルで再開すると、継ぎ目が\n // 三つ目のスナップショットへ移るだけになる。失敗時は先頭からやり直す\n yield { kind: \"chunk\", items: pageItems, nextCursor: null, isLast: false, provisional: true }\n emittedProvisionalPage = true\n } else if (winner.kind === \"full-error\" && pageQueryStarted) {\n // 全量が先に死んでも、生きている先頭ページがあればまず届ける (暫定として)。\n // 完走しない走査では剪定が走らないため、暫定行はそのまま残って画面を埋める\n const pageItems = await pagePromise.catch(() => null)\n if (pageItems && pageItems.length > 0) {\n yield { kind: \"chunk\", items: pageItems, nextCursor: null, isLast: false, provisional: true }\n }\n }\n // page-error は全量へフォールバック。full-error はここで throw されエラー行になる\n if (fullItems === null) {\n fullItems = await fullArrayPromise\n }\n // ❗ 権威パスは常に index 0 から。ページのキーを再開カーソルとして使うと、\n // ページ送出後に先頭領域へ入った行が丸ごと飛ばされる (欠損の構造的原因)。\n // チャンクサイズは明示指定する — 三項演算子の既定へ落とすと、ページを出した\n // 経路で権威パスの初回チャンクが黙って先頭サイズへ縮み、行数が無駄に増える\n yield* streamLinesFromFullArray(fullItems, null, emittedProvisionalPage ? IDS_STREAM_NEXT_CHUNK_SIZE : IDS_STREAM_FIRST_CHUNK_SIZE, { snapshot: computeIdsStreamSnapshotToken(fullItems) })\n })()\n\n return buildIdsStreamResponse(cookie, lineIterator, request, apiLogger)\n },\n /**\n * Retrieves details for a specific daily report.\n * 指定された日報の詳細情報を取得する。\n */\n report: async (url, cookie, request, user, scope) => {\n const param = url.searchParams.get(\"reportHubId\")\n const forceRefresh = url.searchParams.get(\"forceRefresh\") === \"true\"\n const parsedId = param ? Number.parseInt(param, 10) : NaN\n\n if (!Number.isFinite(parsedId) || parsedId <= 0) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Invalid reportHubId\" } }, 400)\n }\n const detail = await service.getDailyReportDetailByIdByExternalId(scope, parsedId, user.id, { snapshot: true, forceRefresh })\n if (!detail) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Report not found\" } }, 404)\n }\n return jsonResponseWithETag(request, cookie, { report: detail }, 200)\n },\n }\n\n /**\n * Routes authenticated daily report API requests by endpoint.\n * エンドポイントごとに認証済みの日報 API リクエストを振り分ける。\n */\n const apiLoader = async ({ request, params }: LoaderArgs) => {\n const { user, cookie } = await authenticate(request, { failureRedirect: null })\n const sanitizedCookie = cookie ?? null\n\n if (!user) {\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Unauthorized\" } }, 401)\n }\n\n if (request.method !== \"GET\") {\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Method not allowed\" } }, 405)\n }\n const endpoint = params.endpoint ?? \"\"\n const handler = endpointHandlers[endpoint]\n if (!handler) {\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Unknown endpoint\" } }, 404)\n }\n const startTime = Date.now()\n try {\n const url = new URL(request.url)\n // 可視集合はリクエストごとに 1 回だけ解決する (キャッシュキーが戻り値に依存する)\n const scope = await resolveViewerScope(request)\n return await handler(url, sanitizedCookie, request, user, scope)\n } catch (error) {\n const elapsed = Date.now() - startTime\n const err = error instanceof Error ? error : new Error(String(error))\n apiLogger.error(`500 endpoint=${endpoint} elapsed=${elapsed}ms name=${err.name} code=${\"code\" in err ? (err as { code: unknown }).code : \"N/A\"} message=${err.message}`)\n apiLogger.error(\"Stack:\", err.stack)\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Internal Server Error\" } }, 500)\n }\n }\n\n /**\n * Handles data mutations for daily reports.\n * 日報データの変更操作を処理するアクション。\n */\n const apiAction = async ({ request, params }: LoaderArgs) => {\n const { user, cookie } = await authenticate(request, { failureRedirect: null })\n\n /**\n * Builds a JSON response that always carries the session cookie for this request.\n * 本リクエストのセッション cookie を必ず載せた JSON レスポンスの生成。\n *\n * 全分岐 (成功・4xx・未認証) で転送する必要がある。トークン更新の cookie を落とすとブラウザは\n * 期限切れトークンを持ち続け、以後のリクエストが毎回リフレッシュを試みて収束しない。\n * 破棄 cookie を落とせば無効セッションがブラウザに残り続ける。個別の `jsonData` へ付け回すと\n * 必ず取りこぼすため、この関数以外を本ハンドラ内で使わない。\n *\n * @param payload Response payload / レスポンス本文\n * @param init Status override / ステータス指定\n * @returns JSON response carrying the session cookie / セッション cookie を載せた JSON レスポンス\n */\n const respond = (payload: unknown, init?: { status?: number }): Response => jsonData(payload, { ...init, setCookie: cookie })\n\n if (!user) {\n return respond({ error: \"Unauthorized\" }, { status: 401 })\n }\n\n // apiAction には外側 try/catch が無く、`respond` を通らない例外 (service / DB / Redis の\n // 失敗) がハンドラの外へ抜けていた。抜けるとセッション cookie が載らないまま 500 になり、\n // トークン更新がブラウザへ届かない。apiLoader (:930) と同じ形で包む\n const actionStartTime = Date.now()\n try {\n const endpoint = params.endpoint\n if (endpoint !== \"action\") {\n return respond({ error: \"Unknown endpoint\" }, { status: 404 })\n }\n\n const userId = await service.getUserIdByExternalId(user.id)\n if (!userId) {\n return respond({ error: \"User not found\" }, { status: 404 })\n }\n\n // 可視集合はリクエストごとに 1 回だけ解決し、他者の日報に作用する 4 種へ渡す\n // (create / update / publish / delete / clearCache は所有者・開発者ゲートで別管理)\n const scope = await resolveViewerScope(request)\n\n const formData = await request.formData()\n const intent = formData.get(\"intent\")\n const reportHubIdRaw = formData.get(\"reportHubId\")\n const reportHubId = reportHubIdRaw ? Number(reportHubIdRaw) : NaN\n const businessDate = formData.get(\"businessDate\") as string | null\n const operationTimestamp = Number(formData.get(\"operationTimestamp\"))\n const clientTempId = formData.get(\"clientTempId\") as string | null\n\n if (intent === \"clearCache\") {\n // 開発専用アフォーダンス。既定は封鎖 (認証済みなら誰でも全ワーカーの\n // キャッシュを無制限破棄できてしまうため、注入で明示的に開けた環境のみ許可)\n if (!config.enableDevCacheClear) {\n return respond({ error: \"Invalid intent\" }, { status: 400 })\n }\n await service.clearCache()\n return respond({ status: \"OK\", intent: \"clearCache\" })\n }\n\n if (!clientTempId) {\n return respond({ error: \"clientTempId required\" }, { status: 400 })\n }\n\n if (intent !== \"create\" && (!reportHubId || Number.isNaN(reportHubId))) {\n return respond({ error: \"Invalid reportHubId\" }, { status: 400 })\n }\n\n /**\n * Runs a boolean user-status toggle and maps its outcome onto a response.\n * 真偽値のユーザー状態トグルを実行し、結果をレスポンスへ写像する処理。\n *\n * `toggleStar` と `toggleRead` は必須フィールド名と呼び出すサービスだけが異なるため、\n * 検証・応答整形・エラー写像をここへ集約する。\n *\n * @param intentName Intent echoed back to the client. クライアントへ返す intent 名。\n * @param fieldName Required boolean form field. 必須の真偽値フォームフィールド名。\n * @param apply Service call performing the toggle. トグルを実行するサービス呼び出し。\n * @returns The HTTP response. HTTP レスポンス。\n */\n const runStatusToggle = async (intentName: \"toggleStar\" | \"toggleRead\", fieldName: \"isStarred\" | \"isRead\", apply: (value: boolean) => Promise<{ isStarred: boolean; isRead: boolean }>): Promise<Response> => {\n const raw = formData.get(fieldName)\n if (raw === null) {\n return respond({ error: `${fieldName} required` }, { status: 400 })\n }\n try {\n const updatedStatus = await apply(raw === \"true\")\n // クライアントは isStarred/isRead のみ参照する。生内部 ID (userId/created_by/updated_by) を\n // 含む行全体は返さず、必要なフラグだけに絞る (内部 ID の自己開示防止)。\n return respond({ status: \"OK\", intent: intentName, updatedStatus: { isStarred: updatedStatus.isStarred, isRead: updatedStatus.isRead }, operationTimestamp, reportHubId: String(reportHubId), clientTempId })\n } catch (e: unknown) {\n // 不可視区分・不存在・削除済みは 404 へ写像する (未捕捉だと 500 になる)\n if (e instanceof Error && e.message === \"Not Found\") {\n return respond({ error: \"Report not found\" }, { status: 404 })\n }\n throw e\n }\n }\n\n switch (intent) {\n case \"create\": {\n if (!businessDate) {\n return respond({ error: \"businessDate required\" }, { status: 400 })\n }\n const newReport = await service.createDailyReport(userId, businessDate, clientTempId)\n return respond({\n status: \"OK\",\n intent: \"create\",\n report: newReport,\n reportHubId: String(newReport.reportHubId),\n clientTempId, // Echo back for validation\n })\n }\n case \"update\": {\n const title = formData.get(\"title\") as string | undefined\n const content = formData.get(\"content\") as string | undefined\n try {\n await service.updateDailyReport(reportHubId, userId, { title, content }, clientTempId)\n return respond({ status: \"OK\", intent: \"update\", reportHubId: String(reportHubId), clientTempId })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return respond({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return respond({ error: \"Report not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n case \"publish\": {\n try {\n const publishedReport = await service.publishDailyReport(reportHubId, userId, clientTempId)\n return respond({ status: \"OK\", intent: \"publish\", reportHubId: String(reportHubId), clientTempId, report: publishedReport })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return respond({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return respond({ error: \"Report not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n case \"delete\": {\n try {\n await service.deleteDailyReport(reportHubId, userId, clientTempId)\n return respond({\n status: \"OK\",\n intent: \"delete\",\n reportHubId: String(reportHubId),\n clientTempId, // Echo back for validation even on delete\n })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return respond({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return respond({ error: \"Report not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n case \"toggleStar\":\n return await runStatusToggle(\"toggleStar\", \"isStarred\", (value) => service.setStarStatus(scope, userId, reportHubId, businessDate, value, clientTempId))\n case \"toggleRead\":\n return await runStatusToggle(\"toggleRead\", \"isRead\", (value) => service.setReadStatus(scope, userId, reportHubId, businessDate, value, clientTempId))\n case \"addComment\": {\n const content = formData.get(\"content\") as string\n if (!content) {\n return respond({ error: \"Content required\" }, { status: 400 })\n }\n try {\n const newComment = await service.addComment(scope, userId, reportHubId, content, businessDate, clientTempId)\n const safeComment = { ...newComment, userId: newComment.userId }\n return respond({ status: \"OK\", intent: \"addComment\", newComment: safeComment, reportHubId: String(reportHubId), clientTempId })\n } catch (e: unknown) {\n if (e instanceof Error) {\n // 削除直後の日報への投稿レース等。500 ではなく意味のある状態コードへ写像する\n if (e.message === \"Not Found\") {\n return respond({ error: \"Report not found\" }, { status: 404 })\n }\n if (e.message.startsWith(\"Comments are restricted\")) {\n return respond({ error: \"Comments are restricted for this report\" }, { status: 403 })\n }\n }\n throw e\n }\n }\n case \"deleteComment\": {\n const commentId = Number(formData.get(\"commentId\"))\n if (!commentId || Number.isNaN(commentId)) {\n return respond({ error: \"Invalid commentId\" }, { status: 400 })\n }\n try {\n await service.deleteComment(scope, userId, reportHubId, commentId, businessDate, clientTempId)\n return respond({ status: \"OK\", reportHubId: String(reportHubId), deletedCommentId: String(commentId), clientTempId })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return respond({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return respond({ error: \"Comment not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n default:\n return respond({ error: \"Invalid intent\" }, { status: 400 })\n }\n } catch (error) {\n // React Router のリダイレクト等は握り潰さず素通しする\n if (error instanceof Response) throw error\n const elapsed = Date.now() - actionStartTime\n const err = error instanceof Error ? error : new Error(String(error))\n apiLogger.error(`500 action elapsed=${elapsed}ms name=${err.name} message=${err.message}`)\n apiLogger.error(\"Stack:\", err.stack)\n return respond({ error: \"Internal Server Error\" }, { status: 500 })\n }\n }\n\n // ---------------- sse (リアルタイム更新) ----------------\n\n /**\n * SSE endpoint for real-time daily report updates via Redis Streams.\n * Redis Streams を使用した日報リアルタイム更新の SSE エンドポイント。\n */\n const sseLoader = async ({ request, params }: LoaderArgs) => {\n // 認証チェック\n const { user, cookie } = await authenticate(request, { failureRedirect: null })\n // セッション cookie は**この応答がどれになろうと**転送する。トークンが更新された\n // リクエストがたまたま SSE 接続だった場合、ここで落とすと更新後のトークンがブラウザへ\n // 届かず、以降のリクエストが毎回リフレッシュをやり直すことになる。破棄 cookie を\n // 落とした場合はさらに悪く、無効セッションがブラウザに残り続ける (apiAction と同じ理由)。\n // SSE は接続が長命なぶん、更新を担うリクエストになりやすい\n const sessionHeaders = (extra?: Record<string, string>): Record<string, string> | undefined => {\n if (cookie === undefined) return extra\n return { ...(extra ?? {}), \"Set-Cookie\": cookie }\n }\n if (!user) {\n return new Response(\"Unauthorized\", { status: 401, headers: sessionHeaders() })\n }\n\n // 内部ユーザー ID の解決\n const internalUserId = await service.getUserIdByExternalId(user.id)\n if (!internalUserId) {\n return new Response(\"Forbidden\", { status: 403, headers: sessionHeaders() })\n }\n\n // エンドポイントの検証\n if (params.endpoint !== \"updates\") {\n return new Response(\"Not Found\", { status: 404, headers: sessionHeaders() })\n }\n\n // 可視集合は接続時に解決し、以降 keep-alive tick に相乗りして再解決する (D6)。\n // 接続生存期間で固定すると、権限失効後も本文付きイベントが届き続けてしまう。\n // ❗ 集合はここで 1 度だけ取り出して保持する。`scope.visible` / `scope.commentable` は\n // 毎回コピーを返す getter なので、メッセージごとに読むと配信 1 件あたり 2 個の Set を\n // 確保することになる (共有 Fan-Out の高頻度経路)\n const initialScope = await resolveViewerScope(request)\n let visibleSet = initialScope.visible\n let commentableSet = initialScope.commentable\n let keepAliveTick = 0\n let resolveGeneration = 0\n\n // URL から lastEventId を取得 (再接続時の catch-up 用)\n // クエリパラメータ優先、EventSource 自動再接続時の Last-Event-ID ヘッダーにフォールバック\n const url = new URL(request.url)\n const lastEventId = url.searchParams.get(\"lastEventId\") || request.headers.get(\"Last-Event-ID\")\n\n const encoder = new TextEncoder()\n let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null\n let keepAliveInterval: ReturnType<typeof setInterval> | null = null\n let unsubscribe: (() => void) | null = null\n let isCleaningUp = false\n\n // クリーンアップ処理\n const cleanup = () => {\n if (isCleaningUp) return\n isCleaningUp = true\n\n // keep-alive タイマー停止\n if (keepAliveInterval) {\n clearInterval(keepAliveInterval)\n keepAliveInterval = null\n }\n\n // Fan-Out 購読解除 (entry / error 両リスナーを解除)\n if (unsubscribe) {\n unsubscribe()\n unsubscribe = null\n }\n\n // SSE ストリームの終了\n if (controllerRef) {\n try {\n if (controllerRef.desiredSize !== null) {\n controllerRef.close()\n }\n } catch (_e) {\n // ストリームが既に閉じている場合は無視\n } finally {\n controllerRef = null\n }\n }\n\n isCleaningUp = false\n }\n\n /**\n * Processes a single Redis Stream entry and sends it to the SSE client.\n * Redis Stream のエントリを処理し、SSE クライアントに送信する。\n */\n /**\n * Emits a content-free frame that only advances the client's resume anchor.\n * 内容を伴わず、クライアントの再開アンカーだけを前進させるフレームを送出する処理。\n *\n * 種別は `connected` を使う。クライアントは既にこの種別を無視しつつ `lastEventId` を\n * 更新するため、フィルタで落としたエントリの ID を安全に伝播できる。\n *\n * @param entryId Redis Stream entry id. Redis Stream のエントリ ID。\n * @returns Nothing. なし。\n */\n const emitAnchorOnly = (entryId: string): void => {\n if (controllerRef && controllerRef.desiredSize !== null) {\n controllerRef.enqueue(encoder.encode(`id: ${entryId}\\ndata: ${JSON.stringify({ type: \"connected\" })}\\n\\n`))\n }\n }\n\n const processEntry = (entryId: string, fields: Record<string, string>) => {\n // data フィールドの存在チェック\n if (!fields.data) return\n\n try {\n const raw = JSON.parse(fields.data)\n const result = dailyReportSseMessageSchema.safeParse(raw)\n if (!result.success) {\n sseLogger.error(\"SSE message validation failed:\", result.error.format())\n return // Fail-Closed: 不正なメッセージは送信しない\n }\n const parsed = result.data\n\n // recipientRawUserId によるフィルタリング\n const recipientRawUserId = typeof raw.recipientRawUserId === \"number\" ? raw.recipientRawUserId : undefined\n\n if (parsed.type === \"status-update\") {\n if (recipientRawUserId === undefined || recipientRawUserId !== internalUserId) {\n return\n }\n } else if (recipientRawUserId !== undefined && recipientRawUserId !== internalUserId) {\n return\n }\n\n // 区分の可視性フィルタ (宛先フィルタを通過した後に適用する)\n if (!isSseMessageSourceTypeVisible(parsed, raw, visibleSet, commentableSet)) {\n // ❗ 内容は送らないが、再開アンカーだけは前へ進める。クライアントの\n // `lastEventId` は配信されたイベントでしか更新されないため、落とし続けると\n // アンカーが古いまま固定され、再接続のたびに同じ範囲を読み直す\n // (かつ catch-up の COUNT 上限を超えた分を取りこぼしうる)。\n // `connected` はクライアントが既に無視する種別なので、内容は漏れない\n emitAnchorOnly(entryId)\n return\n }\n\n // コメント次元が拒否する区分なら、埋め込み detail のコメント面だけを空にする\n const redacted = redactSseReportComments(parsed, raw, commentableSet)\n\n // recipientRawUserId をクライアントに送信しない (内部 ID 漏洩防止)\n let sanitizedMessage: string\n if (\"recipientRawUserId\" in raw) {\n const { recipientRawUserId: _, ...rest } = raw\n sanitizedMessage = JSON.stringify(rest)\n } else if (redacted) {\n // ❗ 秘匿したら必ず再直列化する。元の生文字列を送る高速経路のままだと\n // 書き換えが捨てられ、コメント本文がそのまま流れる\n sanitizedMessage = JSON.stringify(raw)\n } else {\n sanitizedMessage = fields.data\n }\n\n // SSE フォーマットで送信 (id フィールド付き)\n if (controllerRef && controllerRef.desiredSize !== null) {\n controllerRef.enqueue(encoder.encode(`id: ${entryId}\\ndata: ${sanitizedMessage}\\n\\n`))\n }\n } catch (e) {\n sseLogger.error(`[SSE:${internalUserId}] processEntry error:`, e)\n }\n }\n\n const stream = new ReadableStream({\n async start(controller) {\n // 1. controllerRef 設定\n controllerRef = controller\n\n // 2. Keep-Alive: 5 秒ごとにコメントを送信して接続維持\n keepAliveInterval = setInterval(() => {\n try {\n if (controllerRef && controllerRef.desiredSize !== null) {\n controllerRef.enqueue(encoder.encode(\": keep-alive\\n\\n\"))\n // 約 60 秒ごと (12 tick) に可視集合を再解決し、権限失効を有界化する。\n // 世代ガード: 順序が逆転した古い解決結果で最新を上書きしない\n // (上書きすると広い集合へ戻り fail-open 方向に倒れる)。\n // resolveVisibleSet 自身が fail-close なので catch は不要\n if (++keepAliveTick % SSE_VISIBILITY_REFRESH_TICKS === 0) {\n const generation = ++resolveGeneration\n resolveViewerScope(request, \"refresh\").then((next) => {\n if (generation === resolveGeneration) {\n // 両次元の集合を同一スコープから同時に差し替える。\n // 片方だけ更新すると次元間で世代がずれ、失効が半分しか効かない\n visibleSet = next.visible\n commentableSet = next.commentable\n }\n })\n }\n } else {\n cleanup()\n }\n } catch (_e) {\n cleanup()\n }\n }, 5000)\n\n // 3. クライアント切断時のクリーンアップ (全 await の前に登録)\n request.signal.addEventListener(\"abort\", () => {\n cleanup()\n })\n\n // catch-up の基準 ID (Fan-Out エントリとの重複排除に使用)\n let lastProcessedId = lastEventId || \"0-0\"\n\n /**\n * Fan-Out がライブ配信した最初のエントリ ID。catch-up の重複排除に使う。\n *\n * ❗ catch-up の判定に `lastProcessedId` を使ってはならない。`xRange` を待つ間に\n * ライブエントリが 1 件でも届くと共有の高水位が catch-up 範囲の先まで進み、\n * 「既に処理済み」と誤判定して**取得した範囲を丸ごと捨てて**しまう。\n * ライブで配ったのは「この ID 以降」だけなので、その境界で判定する。\n */\n let firstLiveEntryId: string | null = null\n\n // 4. Fan-Out 共有リーダーを購読 (catch-up より先に登録して取りこぼしを防止)\n unsubscribe = sseReader.subscribe(\n (entry: StreamEntry) => {\n // catch-up 済みエントリはスキップ (重複排除)\n if (isStreamIdLte(entry.id, lastProcessedId)) return\n if (firstLiveEntryId === null) {\n firstLiveEntryId = entry.id\n }\n lastProcessedId = entry.id\n processEntry(entry.id, entry.message)\n },\n (err: Error) => {\n sseLogger.error(`[SSE:${internalUserId}] Fan-Out reader error:`, err)\n cleanup()\n },\n )\n\n // 5. 初回接続時: 最新 Stream ID を取得して connected の id: に使用\n // (再接続時は lastEventId が既にあるためスキップ)\n if (!lastEventId) {\n try {\n const client = await redis?.getClient()\n if (client) {\n const latest = await client.xRevRange(streamKey, \"+\", \"-\", { COUNT: 1 })\n // Fan-Out が既に lastProcessedId を進めている場合は巻き戻さない\n if (latest.length > 0 && !isStreamIdLte(latest[0].id, lastProcessedId)) {\n lastProcessedId = latest[0].id\n }\n }\n } catch {\n /* 取得失敗時は現在の lastProcessedId を維持 */\n }\n }\n\n // 6. 接続完了通知 (id: 付きで再接続時の catch-up アンカーを提供)\n if (controllerRef && controllerRef.desiredSize !== null) {\n const connectedPayload = `data: ${JSON.stringify({ type: \"connected\" })}\\n\\n`\n const sseMessage = lastProcessedId !== \"0-0\" ? `id: ${lastProcessedId}\\n${connectedPayload}` : connectedPayload\n controllerRef.enqueue(encoder.encode(sseMessage))\n }\n\n // 7. catch-up: lastEventId が指定されている場合、それ以降のエントリを一括取得\n if (lastEventId) {\n try {\n const client = await redis?.getClient()\n if (client) {\n const catchUpEntries = await client.xRange(streamKey, lastEventId, \"+\", { COUNT: 1000 })\n for (const entry of catchUpEntries) {\n // xRange は inclusive なので lastEventId 自身はスキップ\n if (entry.id === lastEventId) continue\n // Fan-Out がライブ配信した境界以降は重複するのでスキップ。\n // 共有の高水位ではなくこの境界で判定することで、await 中に届いた\n // ライブ 1 件が catch-up 範囲全体を捨てさせる事故を防ぐ\n if (firstLiveEntryId !== null && isStreamIdLte(firstLiveEntryId, entry.id)) continue\n // 高水位は後退させない (ライブが先行している場合がある)\n if (!isStreamIdLte(entry.id, lastProcessedId)) {\n lastProcessedId = entry.id\n }\n processEntry(entry.id, entry.message as Record<string, string>)\n }\n }\n } catch (e) {\n sseLogger.error(`[SSE:${internalUserId}] catch-up xRange error:`, e)\n // catch-up 失敗時は Fan-Out のみで継続\n }\n }\n },\n cancel() {\n cleanup()\n },\n })\n\n return new Response(stream, {\n headers: sessionHeaders({\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n }),\n })\n }\n\n // ---------------- attachment (認可済みバイト配信) ----------------\n\n /**\n * Streams an authorized attachment's bytes to the client.\n * 認可済みの添付ファイルをバイト列としてクライアントへ返すローダー。\n *\n * 認可は `service.getAttachmentForUser` の SQL 述語で完結させる。ストレージ側の\n * 資格情報はサービス共通のものであり、利用者間の分離を一切提供しない。\n * したがってこの述語が唯一の認可境界であり、ここに欠陥があれば全テナント横断の読み取りになる。\n *\n * 「存在しない」と「認可されない」は区別せず 404 にする (存在オラクル回避)。\n *\n * 本体は必ず try/catch で包む。ここで漏らした例外はフレームワークの最終防衛線に落ち、\n * JSON ではないプレーンテキストの 500 になるうえ `Set-Cookie` (セッション延長) も失われる。\n * **認証ポートの呼び出しも try の内側**に置くこと。認証実装はセッション復号や warmup の\n * 失敗で素の例外を投げうるため、外に置くと同じ穴が残る。ただしリダイレクトは `Response` を\n * throw する正常な制御フローなので、catch では素通しする。\n */\n const attachmentLoader = async ({ request, params }: LoaderArgs) => {\n let sanitizedCookie: string | null = null\n\n /** エラー応答。`Set-Cookie` はすべての分岐で伝播させる。 */\n const fail = (status: number, message: string, retryAfterSeconds?: number): Response => {\n const headers = new Headers({ \"Content-Type\": \"application/json\" })\n if (retryAfterSeconds !== undefined) headers.set(\"Retry-After\", String(retryAfterSeconds))\n if (sanitizedCookie) headers.append(\"Set-Cookie\", sanitizedCookie)\n return new Response(JSON.stringify({ error: { message } }), { status, headers })\n }\n\n try {\n const { user, cookie } = await authenticate(request, { failureRedirect: null })\n sanitizedCookie = cookie ?? null\n\n if (!user) return fail(401, \"Unauthorized\")\n if (request.method !== \"GET\" && request.method !== \"HEAD\") return fail(405, \"Method not allowed\")\n\n const codec = config.attachmentIdCodec\n const readAttachment = config.readAttachment\n // ポート未注入は「この配備に添付機能が無い」であり、認可失敗と区別しない\n if (!(codec && readAttachment)) return fail(404, \"Attachment not found\")\n\n const token = params.token ?? \"\"\n const attachmentId = codec.decode(token)\n // `Number.isSafeInteger` であること。可逆難読化は入力トークンの正準性を検証しないため、\n // 正規トークンと同じ長さの入力から 2^53 を超える値が復号されうる。それを SQL パラメータへ\n // 渡すとドライバーの範囲検証が TypeError を投げ、400 のはずが 500 になる。\n if (attachmentId === null || !Number.isSafeInteger(attachmentId) || attachmentId <= 0) {\n return fail(400, \"Invalid attachment token\")\n }\n\n const internalUserId = await service.getUserIdByExternalId(user.id)\n if (!internalUserId) return fail(403, \"Forbidden\")\n\n // レート制限。トークンは列挙可能なので、これが実効的な唯一の緩和策\n if (!consumeAttachmentRateToken(internalUserId, Date.now())) {\n attachmentLogger.error(`429 attachment=${attachmentId} viewer=${internalUserId} reason=rate_limit`)\n return fail(429, \"Too many requests\", RATE_LIMIT_RETRY_AFTER_SECONDS)\n }\n\n // 認可判定 (SQL 側で完結)。null は「存在しない」「認可されない」「親が論理削除済み」の合流\n // 可視集合はこの try の内側で解決する (例外封じ込め規約の維持)\n const scope = await resolveViewerScope(request)\n const attachment = await service.getAttachmentForUser(scope, attachmentId, internalUserId)\n if (!attachment) {\n attachmentLogger.error(`404 attachment=${attachmentId} viewer=${internalUserId} reason=not_visible`)\n return fail(404, \"Attachment not found\")\n }\n\n // DB が既知サイズを持つ場合は読み取り前に弾く (サイズを報告しない取込元の行はここを通過する)\n if (attachment.fileSize != null && attachment.fileSize > attachmentMaxBytes) {\n attachmentLogger.error(`413 attachment=${attachmentId} viewer=${internalUserId} reason=db_size`)\n return fail(413, \"Attachment too large\")\n }\n\n if (!attachmentGate.tryAcquire()) {\n attachmentLogger.error(`503 attachment=${attachmentId} viewer=${internalUserId} reason=concurrency`)\n return fail(503, \"Too many concurrent downloads\", CONCURRENCY_RETRY_AFTER_SECONDS)\n }\n\n // スロットの解放は冪等にする。早期 return・例外・送出完了のどの経路からでも\n // ちょうど 1 回だけ返す必要がある\n let gateReleased = false\n const releaseGate = () => {\n if (gateReleased) return\n gateReleased = true\n attachmentGate.release()\n }\n // 本文の送出完了まで保持する経路に入ったかどうか (入った場合のみ finally での解放を見送る)\n let releaseDeferredToStream = false\n\n try {\n const isHead = request.method === \"HEAD\"\n const result = await readAttachment(attachment.filePath, { maxBytes: attachmentMaxBytes, head: isHead, principal: encodeUserId(internalUserId) })\n\n if (!result.ok) {\n const status = attachmentFailureToStatus(result.reason)\n // 実体消失は次回描画へ反映する。記録の失敗は配信結果に影響させない\n // (await しないので、拒否を捕まえないと未処理 Promise 拒否になる)\n if (result.reason === \"not_found\") {\n void service.markAttachmentMissing(attachmentId).catch((error: unknown) => {\n attachmentLogger.error(`markAttachmentMissing failed attachment=${attachmentId} message=${error instanceof Error ? error.message : \"unknown\"}`)\n })\n }\n // file_path / URI は絶対にログへ出さない (内部パス非露出の方針と整合させる)\n attachmentLogger.error(`${status} attachment=${attachmentId} viewer=${internalUserId} reason=${result.reason} code=${result.code ?? \"-\"}`)\n return fail(status, ATTACHMENT_FAILURE_MESSAGES[status] ?? \"Attachment storage unavailable\")\n }\n\n // インライン許可リスト外は宣言型ごと octet-stream へ落とす。構文検証だけでは\n // text/html や image/svg+xml の自オリジン実行を止められない\n const declared = sanitizeMediaType(attachment.fileType) ?? sanitizeMediaType(result.contentType) ?? \"application/octet-stream\"\n const inlineSafe = INLINE_SAFE_MEDIA_TYPES.has(declared)\n const wantsDownload = new URL(request.url).searchParams.get(\"download\") === \"1\"\n const contentType = inlineSafe ? declared : \"application/octet-stream\"\n const disposition = inlineSafe && !wantsDownload ? \"inline\" : \"attachment\"\n\n // HEAD は本体を読まないので `bytes` が空になる。RFC 9110 §9.3.2 が要求する\n // 「GET と同じ Content-Length」を満たすため、ポートが申告した実サイズを優先する\n const contentLength = result.size ?? result.bytes.byteLength\n const headers = new Headers({\n \"Content-Type\": contentType,\n \"Content-Length\": String(contentLength),\n \"Content-Disposition\": `${disposition}; filename=\"${asciiFallbackFileName(attachment.fileName)}\"; filename*=UTF-8''${encodeRfc8187(attachment.fileName)}`,\n \"Cache-Control\": \"private, no-store\",\n \"X-Content-Type-Options\": \"nosniff\",\n \"Content-Security-Policy\": \"default-src 'none'; sandbox\",\n \"X-Frame-Options\": \"SAMEORIGIN\",\n })\n if (sanitizedCookie) headers.append(\"Set-Cookie\", sanitizedCookie)\n\n // 実体を確認できたので消失記録を解除する。これが唯一の自動回復経路であり、\n // 片方向のままだと一時障害で立った absent を利用者が自力で戻せない\n // (UI は absent の添付にリンクを描画しないため再取得の手段が消える)\n void service.markAttachmentPresent(attachmentId).catch((error: unknown) => {\n attachmentLogger.error(`markAttachmentPresent failed attachment=${attachmentId} message=${error instanceof Error ? error.message : \"unknown\"}`)\n })\n\n attachmentLogger.info(`200 attachment=${attachmentId} viewer=${internalUserId} type=${contentType} disposition=${disposition} bytes=${contentLength}`)\n // HEAD は本文を持たないので即座に解放する。GET は送出完了まで保持し、\n // 「同時実行上限 × 上限バイト数」というヒープ見積りを実際に成立させる\n if (isHead) {\n return new Response(null, { status: 200, headers })\n }\n releaseDeferredToStream = true\n return new Response(streamAndRelease(result.bytes, releaseGate, ATTACHMENT_BODY_FLUSH_TIMEOUT_MS), { status: 200, headers })\n } finally {\n // 送出へ引き渡した場合を除き、早期 return でも例外でも必ずここで返す\n if (!releaseDeferredToStream) releaseGate()\n }\n } catch (error) {\n // 認証ポートはリダイレクトを Response として throw する。これは正常な制御フローなので素通しする\n if (error instanceof Response) throw error\n attachmentLogger.error(`500 attachment=? viewer=? reason=unexpected message=${error instanceof Error ? error.message : \"unknown\"}`)\n return fail(500, \"Attachment request failed\")\n }\n }\n\n return {\n index: { loader: indexLoader },\n api: { loader: apiLoader, action: apiAction },\n sse: { loader: sseLoader },\n attachment: { loader: attachmentLoader },\n }\n}\n","/**\n * Data-access service for daily reports (drizzle mssql), fully DI-configured.\n * 日報データアクセスサービス (drizzle mssql)。依存はすべて DI で注入する。\n *\n * キャッシュキー・無効化順序・SSE publish 順序を含むデータアクセスロジックの中核。\n */\nimport { aliasedTable, and, asc, desc, eq, getColumns, inArray, isNull, or, sql } from \"drizzle-orm\"\nimport { normalizeBusinessDateKey } from \"../shared/business-date\"\nimport { compareIdsStreamOrder, isIdsStreamOrdered } from \"../shared/ids-stream\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport { commentAddMessageSchema, commentDeleteMessageSchema, reportCreateMessageSchema, reportDeleteMessageSchema, reportPublishMessageSchema, reportUpdateMessageSchema, statusUpdateMessageSchema } from \"../shared/sse-schema\"\nimport type { DailyReportAttachmentSummary, DailyReportDetail, DailyReportExternalComment, DailyReportInterviewer, DailyReportItem, DailyReportLabelDef, DailyReportPostedComment } from \"../shared/types\"\nimport type { EpochStore, SqlResultCache, SqlResultCacheQueryOptions } from \"./cache\"\nimport type { DailyReportExternalSource } from \"./external-source\"\nimport type { DailyReportEncodeUserId, DailyReportIdCodec, DailyReportRedisProvider, DailyReportResolveUserId } from \"./ports\"\nimport type { DailyReportTables, DailyReportUserTable } from \"./schema\"\nimport { buildVisibleSourceTypeCondition, computeViewerScopeDigest, DailyReportViewerScope, deniesEverySourceType, isSourceTypeVisible, type VisibleSourceTypeSet } from \"./visibility\"\n\nexport type { SqlResultCacheQueryOptions }\n\n// ---- 行型 (drizzle $inferSelect の構造的置き換え) ----\n\n/** DailyReportHub の行型。 */\nexport type DailyReportHubRow = {\n id: number\n sourceType: string\n sourceId: string\n sourceIdNum: number | null\n businessDate: Date | string | null\n userId: number | null\n title: string | null\n summary: string | null\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n deletedAt: Date | string | null\n deletedBy: string | null\n}\n\n/** DailyReportInternal の行型。 */\nexport type DailyReportInternalRow = {\n hubId: number\n body: string | null\n metadata: string | null\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n}\n\n/** DailyReportUserStatus の行型。 */\nexport type DailyReportUserStatusRow = {\n hubId: number\n userId: number\n isRead: boolean\n isStarred: boolean\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n}\n\n/** 投稿コメント行 (物理テーブル DailyReportComment) の行型。 */\nexport type DailyReportCommentRow = {\n id: number\n hubId: number\n userId: number\n body: string\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n}\n\n// ---- 必要最小の drizzle ビルダ形 (公開契約はサービス関数のシグネチャで厳格化) ----\n\ntype Rows<T> = PromiseLike<T[]>\ninterface SelectChain<T> extends Rows<T> {\n from(t: unknown): SelectChain<T>\n innerJoin(t: unknown, on: unknown): SelectChain<T>\n leftJoin(t: unknown, on: unknown): SelectChain<T>\n where(cond: unknown): SelectChain<T>\n orderBy(...cols: unknown[]): SelectChain<T>\n top(n: number): SelectChain<T>\n}\ninterface InsertChain<T> {\n output(): { values(v: unknown): PromiseLike<T[]> }\n values(v: unknown): PromiseLike<unknown>\n}\ninterface UpdateChain<T> {\n set(v: unknown): {\n where(cond: unknown): PromiseLike<unknown>\n output(): { where(cond: unknown): PromiseLike<T[]> }\n }\n}\ninterface DeleteChain {\n where(cond: unknown): PromiseLike<unknown>\n}\n\n/** サービスが要求する drizzle mssql データベースの最小面 (トランザクション込み)。 */\nexport interface DailyReportDb {\n select<T = Record<string, unknown>>(fields?: unknown): SelectChain<T>\n insert<T = Record<string, unknown>>(t: unknown): InsertChain<T>\n update<T = Record<string, unknown>>(t: unknown): UpdateChain<T>\n delete(t: unknown): DeleteChain\n transaction<T>(fn: (tx: DailyReportDb) => Promise<T>, config?: unknown): Promise<T>\n}\n\n/** getColumns へ構造型テーブルを渡すための境界キャスト。 */\nconst cols = (t: unknown) => getColumns(t as Parameters<typeof getColumns>[0])\n\n// ---- サービス設定 ----\n\nexport type DailyReportServiceConfig = {\n /** drizzle mssql データベースハンドル。 */\n db: DailyReportDb\n /** 日報 7 テーブル (アプリ既存モデルまたは defineDailyReportSchema 生成物)。`attachment` のみ任意。 */\n tables: DailyReportTables\n /** 表示名解決に使う外部ユーザーテーブル ({ id, displayName })。 */\n userTable: DailyReportUserTable\n /** 外部ユーザー ID → 内部数値 ID の解決ポート。 */\n resolveUserId: DailyReportResolveUserId\n /** 内部数値 ID の難読化ポート。 */\n encodeUserId: DailyReportEncodeUserId\n /** SSE publish / epoch 用 redis (省略時は SSE publish スキップ・epoch 無効)。 */\n redis?: DailyReportRedisProvider\n /** レガシー等の外部ソースアダプタ群。 */\n externalSources?: DailyReportExternalSource[]\n /** SQL 結果キャッシュ (facade が生成して注入)。 */\n cache: SqlResultCache\n /** クロスワーカー epoch ストア (facade が生成して注入)。 */\n epochs: EpochStore\n /**\n * 下書きラベル名 (単一または配列で指定可能)。\n * 消費アプリ側で自 DB のラベル名や作成区分の候補名 (\"下書き\", \"DRAFT\" など) を注入できる。\n */\n draftLabelName?: string\n draftLabelNames?: string[]\n /** SSE Redis Stream キー (既定 \"daily-report:sse-stream\")。 */\n streamKey?: string\n /** SSE Stream の MAXLEN (既定 10000)。 */\n streamMaxLen?: number\n /** IDs 一覧キャッシュ TTL (既定 180,000ms)。 */\n idsTtlMs?: number\n /** 営業日別キャッシュ TTL (既定 300,000ms)。 */\n businessDateTtlMs?: number\n /** 外部 ID → 内部 ID のプロセス内キャッシュを無効化 (テスト用)。 */\n disableUserIdCache?: boolean\n /**\n * 添付 ID の難読化コーデック。\n *\n * 任意にしているのは、添付テーブルを注入しない消費アプリと既存テストのフィクスチャを\n * 壊さないため。未注入なら添付一覧は常に空になる (縮退動作)。\n * ユーザー ID 用のコーデックとは **別インスタンス**を渡すこと。同一だと\n * レスポンス中の `userId` トークンがそのまま添付トークンとして解釈できてしまう。\n */\n attachmentIdCodec?: DailyReportIdCodec\n /** ロガー (既定は console ベース)。 */\n logger?: DailyReportLogger\n}\n\n/** createDailyReportService の返却型。 */\nexport type DailyReportService = ReturnType<typeof createDailyReportService>\n\n/**\n * Creates the daily-report data-access service bound to the injected dependencies.\n * 注入された依存に束縛された日報データアクセスサービスを生成する処理。\n */\ntype DatePattern = \"YYYY-MM-DD HH:mm:ss\" | \"YYYY-MM-DD\"\n\n/**\n * Formats date-like values to a given pattern using local-time fields (no Intl allocation).\n * 日付相当の値をローカル時刻フィールドで指定パターンへ整形する処理 (Intl 非生成)。\n *\n * ローカル時刻フィールドの手組み整形。旧実装 (ja-JP ロケール + 2-digit toLocale*) と出力互換。\n * ❗ toLocale* 系へ戻さないこと: あれは呼び出しごとに Intl.DateTimeFormat を生成し\n * (実測 約 66μs/呼)、全量 ids の 227k 行 map で 15〜20 秒の同期 CPU 洪水になる。\n * business-date / SSE の全応答が人質化した実害バグの根本原因だった。\n * モジュールスコープの export であり、バレル (src/server.ts の `export *`) 経由で\n * `@aiquants/daily-report/server` の公開 API の一部になる (テストからの直接検証と\n * ホスト側での汎用日時整形の再利用を意図した、意図的な公開)。\n * 1000 年未満の年は旧実装 (年ゼロ詰めなし) から意図的に乖離し、ISO 準拠の 4 桁ゼロ詰めで出す\n * (負の年は符号を除いた絶対値を 4 桁詰め。DB の date/datetime からは到達しない文字列入力のみの領域)。\n */\nexport const formatDateValue = (value: Date | string | null | undefined, pattern: DatePattern): string | null => {\n // null / undefined / 空文字は null に正規化\n if (!value) {\n return null\n }\n\n // Date 以外 (文字列) は Date コンストラクタで解釈\n const dateValue = value instanceof Date ? value : new Date(value)\n if (Number.isNaN(dateValue.getTime())) {\n // 解釈不能な文字列は原文のまま返す (旧実装と同一の不変挙動)\n return typeof value === \"string\" ? value : null\n }\n\n // 年月日をゼロ詰めで組み立てる (負の年は符号を除いた絶対値を 4 桁詰め = ISO 8601 拡張表記に整合。\n // String(-5).padStart は \"00-5\" と符号ごと詰めてしまうため符号を分離する)\n const year = dateValue.getFullYear()\n const yyyy = year < 0 ? `-${String(-year).padStart(4, \"0\")}` : String(year).padStart(4, \"0\")\n const mm = String(dateValue.getMonth() + 1).padStart(2, \"0\")\n const dd = String(dateValue.getDate()).padStart(2, \"0\")\n if (pattern === \"YYYY-MM-DD\") {\n return `${yyyy}-${mm}-${dd}`\n }\n // 時分秒付きパターン\n const hh = String(dateValue.getHours()).padStart(2, \"0\")\n const mi = String(dateValue.getMinutes()).padStart(2, \"0\")\n const ss = String(dateValue.getSeconds()).padStart(2, \"0\")\n return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`\n}\n\nexport function createDailyReportService(config: DailyReportServiceConfig) {\n const { db, tables, userTable: users, resolveUserId, encodeUserId, redis, cache: sqlResultCache, epochs } = config\n const { hub: DailyReportHub, internal: DailyReportInternal, comment: DailyReportCommentModel, label: DailyReportLabel, hubLabel: DailyReportHub_Label, userStatus: DailyReportUserStatus } = tables\n const externalSources = config.externalSources ?? []\n const draftLabelNames = config.draftLabelNames && config.draftLabelNames.length > 0 ? config.draftLabelNames : [config.draftLabelName ?? \"下書き\"]\n const draftLabelName = draftLabelNames[0] ?? \"下書き\"\n const logger = config.logger ?? createLogger(LogLevel.INFO, \"[DailyReportService]\")\n\n const DAILY_REPORT_IDS_CACHE_KEY = \"daily-report:ids\"\n const DAILY_REPORT_IDS_TTL_MS = config.idsTtlMs ?? 180_000\n const DAILY_REPORT_IDS_EPOCH_KEY = \"daily-report:ids:epoch\"\n\n const DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX = \"daily-report:business-date:\"\n const DAILY_REPORT_BUSINESS_DATE_TTL_MS = config.businessDateTtlMs ?? 300_000\n\n /** 営業日キャッシュのクロスワーカー epoch プレフィックス */\n const DAILY_REPORT_DATE_EPOCH_PREFIX = \"daily-report:date-epoch:\"\n /** レポート詳細キャッシュのクロスワーカー epoch プレフィックス */\n const DAILY_REPORT_DETAIL_EPOCH_PREFIX = \"daily-report:detail-epoch:\"\n\n /**\n * SSE ワイヤ形式の版。埋め込み detail スキーマの互換を破る変更ごとに上げる。\n *\n * ❗ SSE の実体は配備を跨いで残る永続 Redis Stream (MAXLEN トリムのみ・時間上限なし)。\n * スキーマの必須キーを変えると、旧形式エントリが再接続 catch-up の safeParse で全滅する。\n * 版をキーへ焼き込むことで旧エントリは**構造的に到達不能**になり、スキーマに寿命無期限の\n * 互換シムを飼う必要がなくなる。設定キー (env 上書き含む) にも接尾辞を適用する —\n * 消費側の設定が版分離を黙って無効化できてはならない。\n * 旧版のストリームは xAdd が止まりトリムも走らないため残置される (手動 DEL 可)。\n * v2: comments/commentItems → externalComments/postedComments の改名 (2026-08)。\n */\n const DAILY_REPORT_SSE_WIRE_VERSION = \"v2\"\n const DAILY_REPORT_SSE_STREAM_KEY = `${config.streamKey ?? \"daily-report:sse-stream\"}:${DAILY_REPORT_SSE_WIRE_VERSION}`\n const DAILY_REPORT_SSE_STREAM_MAXLEN = config.streamMaxLen ?? 10000\n\n const incrementRedisEpoch = (key: string) => epochs.incrementEpoch(key)\n\n /**\n * Binds the shared visibility condition builder to this service's hub table.\n * 共有の可視性条件ビルダーを本サービスの hub テーブルへ束縛したもの。\n *\n * 三値の意味と fail-close の方向は {@link buildVisibleSourceTypeCondition} が唯一の真実源。\n */\n const visibleSourceTypeCondition = (visible: VisibleSourceTypeSet) => buildVisibleSourceTypeCondition(DailyReportHub.sourceType, visible)\n\n // ---- キャッシュキー生成 (ダイジェスト折り込みと無効化接頭辞の単一の真実源) ----\n // ❗ ユーザー別キャッシュキーは必ずここから作る。可視集合ダイジェストの付け忘れ (= 別権限へ\n // 古い結果を配信) と、無効化時の取りこぼしを構造的に防ぐ。接頭辞の末尾 \":\" は\n // ユーザー ID の前方一致衝突 (1 と 10) を避けるため必須。\n\n /** ID 一覧キャッシュのキー (ユーザー + 可視集合ダイジェスト)。 */\n const idsCacheKey = (userId: number, digest: string): string => `${DAILY_REPORT_IDS_CACHE_KEY}:user:${userId}:st:${digest}`\n /** ID 一覧キャッシュの当該ユーザー全ダイジェスト分の接頭辞。 */\n const idsUserPrefix = (userId: number): string => `${DAILY_REPORT_IDS_CACHE_KEY}:user:${userId}:`\n /** 営業日別キャッシュのキー (営業日 + ユーザー + 可視集合ダイジェスト)。 */\n const dateCacheKey = (normalizedBusinessDate: string, userId: number, digest: string): string => `daily-report:date:${normalizedBusinessDate}:user:${userId}:st:${digest}`\n /** 営業日別キャッシュの当該ユーザー全ダイジェスト分の接頭辞。 */\n const dateUserPrefix = (normalizedBusinessDate: string, userId: number): string => `daily-report:date:${normalizedBusinessDate}:user:${userId}:`\n /** 詳細キャッシュのキー (hub + ユーザー + 可視集合ダイジェスト)。 */\n const detailCacheKey = (reportHubId: number, userId: number, digest: string): string => `daily-report:detail:${reportHubId}:user:${userId}:st:${digest}`\n /** 詳細キャッシュの当該ユーザー全ダイジェスト分の接頭辞。 */\n const detailUserPrefix = (reportHubId: number, userId: number): string => `daily-report:detail:${reportHubId}:user:${userId}:`\n\n /**\n * Publishes a message to the SSE Redis Stream.\n * SSE 用の Redis Stream にメッセージを追加する。\n */\n const publishToSseStream = async (message: Record<string, unknown>, callerName: string): Promise<void> => {\n const client = await redis?.getClient()\n if (!client) {\n logger.warn(`[SSE] Redis client unavailable (${callerName})`)\n return\n }\n const publishStartMs = Date.now()\n try {\n await client.xAdd(DAILY_REPORT_SSE_STREAM_KEY, \"*\", { data: JSON.stringify(message) }, { TRIM: { strategy: \"MAXLEN\", strategyModifier: \"~\", threshold: DAILY_REPORT_SSE_STREAM_MAXLEN } })\n const publishDurationMs = Date.now() - publishStartMs\n if (publishDurationMs > 1000) {\n logger.warn(`[SSE] Slow publish (${callerName}): ${publishDurationMs}ms`)\n }\n } catch (e) {\n logger.error(`[SSE] Redis publish failed (${callerName}):`, e)\n }\n }\n\n /**\n * Masks a raw audit-actor value (created_by / updated_by) before client exposure.\n * 監査列 (created_by / updated_by) をクライアント公開前にマスクする処理。\n *\n * Internal 由来の値は String(userId) の生内部 ID のため、数値なら encodeUserId で難読化する。\n * 非数値 (外部ソース由来の氏名やシステム文字列) はそのまま返す。null / 空文字は null。\n * userId フィールドと同じ難読化を監査列にも適用し、内部 ID の横流し漏洩を防ぐ。\n */\n const maskAuditActor = (value: string | null | undefined): string | null => {\n if (value == null || value === \"\") return null\n return /^\\d+$/.test(value) ? encodeUserId(Number(value)) : value\n }\n\n type RawJsonComment = {\n id: number\n content: string\n createdAt: string\n userId: number\n userName: string\n }\n\n type RawJsonLabel = {\n id: number\n name: string\n color: string | null\n }\n\n /** 詳細クエリ 1 行 (外部ソース列は `ext_<sourceType>` キーで同居)。 */\n type HubQueryRow = {\n hub: DailyReportHubRow\n internal: DailyReportInternalRow | null\n isRead?: boolean | null\n isStarred?: boolean | null\n creatorName?: string | null\n } & Record<string, unknown>\n\n type HubRecord = HubQueryRow & {\n labels?: RawJsonLabel[]\n /** パッケージ自身のコメント表 (投稿コメント) の行。detail の postedComments になる。 */\n postedCommentRows?: RawJsonComment[]\n /**\n * 添付ファイル一覧。**任意にしない。**\n * `?` を付けると `mapHubRecord` 側で `?? []` が必要になり、注入を忘れた読み取り経路が\n * 黙って空配列を返す。必須にしておけば注入漏れは全呼び出し箇所でコンパイルエラーになる。\n */\n attachments: DailyReportAttachmentSummary[]\n }\n\n /** 外部ソースアダプタの select 追加フィールドを構築する。 */\n const externalSelections = (): Record<string, unknown> => {\n const out: Record<string, unknown> = {}\n for (const adapter of externalSources) {\n out[`ext_${adapter.sourceType}`] = cols(adapter.table)\n }\n return out\n }\n\n /** 外部ソースアダプタの LEFT JOIN を select チェーンへ適用する。 */\n const applyExternalJoins = <T>(chain: SelectChain<T>): SelectChain<T> => {\n let c = chain\n for (const adapter of externalSources) {\n c = c.leftJoin(adapter.table, and(eq(DailyReportHub.sourceType, adapter.sourceType), eq(DailyReportHub.sourceIdNum, adapter.idColumn)))\n }\n return c\n }\n\n /**\n * Converts a Hub record into a DailyReportDetail structure, applying the viewer's comment visibility.\n * Hub レコードを DailyReportDetail に変換し、閲覧者のコメント可視性を適用する処理。\n *\n * ❗ **コメント 2 面の唯一の生成点**。詳細取得 / 営業日一覧 / 管理経路 / publish 応答の全てが\n * ここを通るため、コメント次元の射影はここ 1 箇所に置く。SQL の `WHERE` 側で絞るのは不可 —\n * コメント表を引くクエリは `postedComments` にしか効かず、レガシー `comments` は外部ソース\n * アダプタの出力なのでクエリを通らない (半分しか塞げない)。\n *\n * ❗ 秘匿は**必ず空配列**で行う。キーごと省くと SSE 側は `dailyReportDetailSchema` の必須キー\n * 欠落で `safeParse` に失敗し、メッセージ全体が fail-close で落ちる (更新が永久に届かない)。\n * HTTP 側もクライアントの `DailyReportCache.merge` が `postedComments` を無ガードで参照する。\n *\n * @param scope Viewer scope carrying the comment dimension. コメント次元を運ぶ閲覧範囲。\n * @param row Hub record to convert. 変換対象の Hub レコード。\n * @param currentUserId Viewer's raw internal user id, if known. 判明していれば閲覧者の生内部ユーザー ID。\n * @returns The detail structure. 変換した詳細構造。\n */\n const mapHubRecord = (scope: DailyReportViewerScope, row: HubRecord, currentUserId?: number): DailyReportDetail => {\n const { hub, internal } = row\n\n let content = hub.summary\n let interviewers: DailyReportInterviewer[] = []\n let category: string | null = null\n let creationCategory: string | null = null\n let visitTimeFrom: string | null = null\n let visitTimeTo: string | null = null\n let customerName: string | null = null\n let employeeName: string | null = null\n let externalComments: DailyReportExternalComment[] = []\n\n // 外部ソースアダプタ優先 → Internal の順で表示フィールドを解決する\n const adapter = externalSources.find((a) => a.sourceType === hub.sourceType)\n const externalRow = adapter ? (row[`ext_${adapter.sourceType}`] as Record<string, unknown> | null | undefined) : undefined\n if (adapter && externalRow) {\n const fields = adapter.mapRow(externalRow)\n if (fields.content !== undefined) content = fields.content\n if (fields.employeeName !== undefined) employeeName = fields.employeeName\n if (fields.category !== undefined) category = fields.category\n if (fields.creationCategory !== undefined) creationCategory = fields.creationCategory\n if (fields.visitTimeFrom !== undefined) visitTimeFrom = fields.visitTimeFrom\n if (fields.visitTimeTo !== undefined) visitTimeTo = fields.visitTimeTo\n if (fields.customerName !== undefined) customerName = fields.customerName\n if (fields.interviewers !== undefined) interviewers = fields.interviewers\n if (fields.externalComments !== undefined) externalComments = fields.externalComments\n } else if (hub.sourceType === \"Internal\" && internal) {\n content = internal.body\n // Internal specific mappings if any\n }\n\n const labelsRaw = (row.labels ?? []).map((l) => ({\n id: l.id,\n name: l.name,\n color: l.color,\n }))\n const postedCommentsRaw = row.postedCommentRows ?? []\n\n // コメント次元がこの区分を拒否するなら、コメント 2 面を落として返す。\n // ❗ 自分のコメントだけは常に残す (要件 5)。隠すと「UI に見えないのに削除だけできる」\n // 非対称が生まれ、権限剥奪の瞬間に既存の自分のコメントが恒久的に削除不能になる。\n // 外部由来 `externalComments` はユーザー ID を持たない構造なので区分単位の二値にしかできない\n const commentsVisible = isSourceTypeVisible(scope.commentable, hub.sourceType)\n\n // 作成区分がアプリ設定の下書き候補に合致する場合、ラベル一覧に下書きラベルが含まれるよう補完\n const isCreationCategoryDraft = creationCategory ? draftLabelNames.includes(creationCategory) : false\n // 同期関数なので DB へは問い合わせず、既に解決済みのキャッシュがある場合だけ補完する\n const cachedPrimaryDraftLabelId = cachedDraftLabelIds?.[0] ?? null\n if (isCreationCategoryDraft && cachedPrimaryDraftLabelId && !labelsRaw.some((l) => l.id === cachedPrimaryDraftLabelId)) {\n labelsRaw.push({\n id: cachedPrimaryDraftLabelId,\n name: draftLabelName,\n color: null,\n })\n }\n\n return {\n reportHubId: hub.id,\n date: formatDateValue(hub.businessDate, \"YYYY-MM-DD\"),\n author: row.creatorName ?? maskAuditActor(hub.createdBy) ?? \"\",\n userId: hub.userId ? encodeUserId(hub.userId) : \"\",\n sourceType: hub.sourceType ?? \"Internal\",\n createdAt: formatDateValue(hub.createdAt, \"YYYY-MM-DD HH:mm:ss\"),\n updatedAt: formatDateValue(hub.updatedAt, \"YYYY-MM-DD HH:mm:ss\"),\n updatedBy: maskAuditActor(hub.updatedBy),\n employeeName: employeeName ?? row.creatorName ?? maskAuditActor(hub.createdBy),\n category,\n creationCategory,\n visitTimeFrom,\n visitTimeTo,\n customerName,\n interviewers,\n subject: hub.title,\n content,\n externalComments: commentsVisible ? externalComments : [],\n isRead: row.isRead ?? false,\n isStarred: row.isStarred ?? false,\n labels: labelsRaw,\n postedComments: postedCommentsRaw\n .filter((c) => commentsVisible || (currentUserId !== undefined && c.userId === currentUserId))\n .map((c) => ({\n ...c,\n userId: encodeUserId(c.userId),\n isMine: currentUserId ? c.userId === currentUserId : false,\n })),\n // `?? []` を書かない。HubRecord 側で必須にしてあるため、注入漏れはここではなく\n // 呼び出し箇所でコンパイルエラーになる (それが検知したい事象)。\n attachments: row.attachments,\n }\n }\n\n /**\n * Fetches daily report IDs alongside normalized business dates.\n * 日報 ID と正規化済み営業日を取得する内部処理。\n *\n * ❗ 可視性述語は他の 3 経路 (営業日一覧 / 詳細取得 / 添付取得) と**意図的に 1 つ緩い**。\n * ここだけ `isNull(DailyReportHub.userId)` を追加で許しており、投稿者を持たない\n * 取込元の日報を一覧から消さないための緩和である。返すのは ID・営業日・ソース種別だけで\n * 本文も添付も含まないため、緩めても内容は漏れない。\n * この差分は無自覚なものではなく、`service.spec.ts` が明示的に表明して固定している。\n * 他の 3 経路をこの形に合わせてはならない (合わせると内容が漏れる)。\n *\n * 区分 (sourceType) の可視性は緩和の対象外であり、他経路と完全に同一の条件を適用する。\n *\n * @param userId Internal numeric user id. 内部数値ユーザー ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Stream-ordered items. ストリーム順のアイテム一覧。\n */\n const fetchDailyReportIdsByUserId = async (scope: DailyReportViewerScope, userId: number): Promise<DailyReportItem[]> => {\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n\n const result = await db\n .select<{ reportHubId: number; businessDate: Date | string | null; sourceType: string }>({\n reportHubId: DailyReportHub.id,\n businessDate: DailyReportHub.businessDate,\n sourceType: DailyReportHub.sourceType,\n })\n .from(DailyReportHub)\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .where(and(isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DailyReportHub.userId), isNull(DraftLabelRelation.hubId)), visibleSourceTypeCondition(scope.visible)))\n .orderBy(desc(DailyReportHub.businessDate), desc(DailyReportHub.id))\n\n return result.map((item) => ({\n ...item,\n businessDate: formatDateValue(item.businessDate, \"YYYY-MM-DD\"),\n }))\n }\n\n /**\n * Fetches only the first page of report ids for time-to-first-render (uncached).\n * 最初の描画までの時間専用に、ID 一覧の先頭ページだけを取得する内部処理 (キャッシュ無し)。\n *\n * `ids-stream` の新規接続でキャッシュミスした場合、全量クエリの確定を待たずに\n * この軽量クエリ (TOP n・インデックス順で n 行走査した時点で打ち切り) の結果を\n * 最初のチャンクとして送出する。可視性述語と並び順は全量クエリと完全同一。\n * `.top(n)` は `.select()` の直後でなければならない (drizzle-mssql の構造的制約)。\n *\n * @param externalId External user id. 外部ユーザー ID。\n * @param limit Page size. ページサイズ。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns First-page items in stream order. ストリーム順の先頭ページ。\n */\n const getDailyReportIdsFirstPageByExternalId = async (scope: DailyReportViewerScope, externalId: string, limit: number): Promise<DailyReportItem[]> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return []\n }\n // 全拒否なら DB を引かずに空を返す (0 件 = fail-close)\n if (deniesEverySourceType(scope.visible)) {\n return []\n }\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n\n const result = await db\n .select<{ reportHubId: number; businessDate: Date | string | null; sourceType: string }>({\n reportHubId: DailyReportHub.id,\n businessDate: DailyReportHub.businessDate,\n sourceType: DailyReportHub.sourceType,\n })\n .top(limit)\n .from(DailyReportHub)\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .where(and(isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DailyReportHub.userId), isNull(DraftLabelRelation.hubId)), visibleSourceTypeCondition(scope.visible)))\n .orderBy(desc(DailyReportHub.businessDate), desc(DailyReportHub.id))\n\n return result.map((item) => ({\n ...item,\n businessDate: formatDateValue(item.businessDate, \"YYYY-MM-DD\"),\n }))\n }\n\n /**\n * Fetches daily report details for the provided business date.\n * 指定した営業日の日報詳細を取得する内部処理。\n */\n const fetchDailyReportsByBusinessDate = async (scope: DailyReportViewerScope, normalizedBusinessDate: string): Promise<DailyReportDetail[]> => {\n const rows = await applyExternalJoins(\n db\n .select<HubQueryRow>({\n hub: cols(DailyReportHub),\n internal: cols(DailyReportInternal),\n creatorName: users.displayName,\n ...externalSelections(),\n })\n .from(DailyReportHub),\n )\n .leftJoin(DailyReportInternal, eq(DailyReportHub.id, DailyReportInternal.hubId))\n .leftJoin(users, eq(DailyReportHub.userId, users.id))\n .where(and(eq(DailyReportHub.businessDate, sql`${normalizedBusinessDate}`), isNull(DailyReportHub.deletedAt), visibleSourceTypeCondition(scope.visible)))\n .orderBy(desc(DailyReportHub.id))\n\n const attachmentsMap = await getAttachmentsByHubIds(rows.map((r) => r.hub.id))\n return rows.map((row) => mapHubRecord(scope, { ...row, attachments: attachmentsMap.get(row.hub.id) ?? [] }))\n }\n\n /**\n * Builds a deterministic cache key for business-date caches.\n * 営業日キャッシュ用の一意キーを生成する処理。\n */\n const buildBusinessDateCacheKey = (normalizedBusinessDate: string): string => {\n return `${DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX}${normalizedBusinessDate}`\n }\n\n /**\n * Resolves the internal user ID from an external ID (with process-local caching).\n * 外部 ID から内部ユーザー ID を解決する (プロセス内キャッシュ付き)。\n */\n const userIdCache = new Map<string, number>()\n\n const getUserIdByExternalId = async (externalId: string): Promise<number | null> => {\n if (!config.disableUserIdCache && userIdCache.has(externalId)) {\n return userIdCache.get(externalId) ?? null\n }\n\n const userId = await resolveUserId(externalId)\n if (userId !== null && !config.disableUserIdCache) {\n userIdCache.set(externalId, userId)\n }\n return userId\n }\n\n /**\n * Retrieves daily report IDs and their normalized business dates.\n * 日報 ID と正規化済み営業日を取得する処理。\n *\n * @param externalId External user id. 外部ユーザー ID。\n * @param options Cache query options. キャッシュ問い合わせオプション。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Stream-ordered items. ストリーム順のアイテム一覧。\n */\n const getDailyReportIdsByExternalId = async (scope: DailyReportViewerScope, externalId: string, { forceRefresh = false, snapshot = false, ttlMsOverride, onFetchStart }: SqlResultCacheQueryOptions & { onFetchStart?: () => void } = {}): Promise<DailyReportItem[]> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return []\n }\n // 全拒否なら DB を引かずに空を返す (0 件 = fail-close)\n if (deniesEverySourceType(scope.visible)) {\n return []\n }\n\n // 可視集合ごとに別バケットへ隔離する (権限変更後に旧結果を配信しない)\n const cacheKey = idsCacheKey(userId, computeViewerScopeDigest(scope))\n return sqlResultCache.getOrFetch<DailyReportItem>({\n cacheKey,\n fetcher: async () => {\n const res = await fetchDailyReportIdsByUserId(scope, userId)\n // ❗ 並んでいない配列はカーソル再開の前提を破る (findIdsStreamResumeIndex は\n // 比較器で「最初に後ろへ来る位置」を探すため、並びが崩れていると誤った位置から\n // 再開する)。原因は SQL の ORDER BY と compareIdsStreamOrder の乖離、または\n // RCSI の無い配備でのスキャン中の行移動 (実運用 DB は dev / 本番とも\n // READ_COMMITTED_SNAPSHOT = ON を実機確認済みで、その配備では後者は起きない —\n // 2026-08-15 検証)。fetcher 内で治癒するのは、\n // ①検査が fetch 1 回につき 1 度で済み ②バケットとスナップショットトークンが\n // 治癒済み配列を見る、の両方を成立させるため。\n // ❗ 並べ直しても欠落・重複は直らない — 直るのはカーソル再開の二次被害だけ\n if (!isIdsStreamOrdered(res)) {\n logger.warn(\"[DailyReportService] ids array is not in stream order; re-sorting (check SQL ORDER BY vs compareIdsStreamOrder, or concurrent row movement during the scan)\")\n return [...res].sort(compareIdsStreamOrder)\n }\n return res\n },\n // 本呼び出しが実際に全量クエリを起動した瞬間だけ合図する (バケットヒット・相乗り・\n // refresh-ahead では呼ばれない)。ids ストリームはこれを高速先頭ページ起動の\n // 因果的な前提条件に使う\n onFetchStart,\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_IDS_TTL_MS,\n epochKey: DAILY_REPORT_IDS_EPOCH_KEY,\n // TTL の半分を過ぎたヒットは、返却後に裏で 1 本だけ再フェッチして差し替える。\n // 90 秒に 1 回以上アクセスがある限り、対話リクエストはコールド (全量再クエリ) を踏まない\n refreshAheadRatio: 0.5,\n }) as Promise<DailyReportItem[]>\n }\n\n /**\n * Retrieves daily report details for a business date (⚠️ NOT per-user filtered).\n * 指定した営業日に紐づく日報詳細を取得する処理 (⚠️ ユーザー別フィルタなし)。\n *\n * ⚠️ SECURITY: この関数は下書きラベルによる可視性フィルタ (他ユーザーの下書きを隠す) と\n * ユーザー別の既読/スター状態を **適用しない**。取得結果は全ユーザーの下書きを含み得るため、\n * HTTP レスポンス / SSR loader / SSE へ **直接返してはならない**。ユーザー向け配信には\n * 必ず {@link getDailyReportsByBusinessDateByExternalId} を使うこと。\n * (本関数は管理・バッチ・テスト用途に限定する。)\n */\n const getDailyReportsByBusinessDate = (scope: DailyReportViewerScope, businessDate: string, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail[]> => {\n const normalizedBusinessDate = normalizeBusinessDateKey(businessDate)\n if (!normalizedBusinessDate) {\n return Promise.resolve([])\n }\n // 全拒否なら DB を引かずに空を返す (0 件 = fail-close)\n if (deniesEverySourceType(scope.visible)) {\n return Promise.resolve([])\n }\n\n return sqlResultCache.getOrFetch<DailyReportDetail>({\n // 可視集合ごとに別バケットへ隔離する (管理経路でも権限混在を起こさない)\n cacheKey: `${buildBusinessDateCacheKey(normalizedBusinessDate)}:st:${computeViewerScopeDigest(scope)}`,\n fetcher: () => fetchDailyReportsByBusinessDate(scope, normalizedBusinessDate),\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_BUSINESS_DATE_TTL_MS,\n }) as Promise<DailyReportDetail[]>\n }\n\n /**\n * Retrieves every label id that counts as a draft.\n * 下書きとみなすラベル ID を**すべて**取得する処理 (キャッシュ付き)。\n *\n * ❗ 可視性述語は必ずこちら (複数形) を使うこと。`draftLabelNames` には複数のラベル名を\n * 注入できる (取込元ごとに下書きの呼び名が違うため) のに、1 個の ID だけで結合すると\n * **2 つ目以降の下書きラベルが付いた日報が誰にでも可視になる**。\n * 名前が 1 つも解決できない場合は空配列を返し、呼び出し側は結合対象を\n * 「存在しない ID」にして「下書きは存在しない」と解釈する。\n *\n * @returns Draft label ids, ascending. 昇順の下書きラベル ID 群。\n */\n let cachedDraftLabelIds: number[] | null = null\n const getDraftLabelIds = async (): Promise<number[]> => {\n if (cachedDraftLabelIds !== null) return cachedDraftLabelIds\n const rows = await db.select<{ id: number }>({ id: DailyReportLabel.id }).from(DailyReportLabel).where(inArray(DailyReportLabel.name, draftLabelNames)).orderBy(DailyReportLabel.id)\n cachedDraftLabelIds = rows.map((row) => row.id)\n return cachedDraftLabelIds\n }\n\n /**\n * Join targets for the draft-label relation, safe when no draft label exists.\n * 下書きラベル関連の結合対象。下書きラベルが 1 件も無い場合でも安全に働く値。\n *\n * `inArray` に空配列を渡すと方言によっては構文エラーや常に真になりうるため、\n * 実在しない ID (`-1`) を 1 個入れて「どの行にも一致しない」ことを明示する。\n *\n * @returns Ids to join on. 結合に使う ID 群。\n */\n const getDraftLabelJoinIds = async (): Promise<number[]> => {\n const ids = await getDraftLabelIds()\n return ids.length > 0 ? ids : [-1]\n }\n\n /**\n * Retrieves the canonical draft label id used when creating a draft.\n * 下書きを作成するときに付与する代表の下書きラベル ID を取得する処理。\n *\n * 付与は 1 個に決めなければならないため `draftLabelNames` の先頭に対応する ID を返す。\n * **可視性判定には使わないこと** (`getDraftLabelIds` を使う)。\n *\n * @returns The canonical draft label id, or null when it does not exist. 代表の下書きラベル ID (存在しなければ null)。\n */\n const getDraftLabelId = async (): Promise<number | null> => {\n const ids = await getDraftLabelIds()\n if (ids.length === 0) return null\n // 先頭の名前に対応する ID を選ぶ。ID 順ではなく注入順を正とする (代表名は draftLabelName)\n const [primary] = await db.select<{ id: number }>({ id: DailyReportLabel.id }).top(1).from(DailyReportLabel).where(eq(DailyReportLabel.name, draftLabelName))\n return primary?.id ?? ids[0]\n }\n\n /**\n * Derives the tri-state verification status of an attachment's backing object.\n * 添付ファイル実体の検証状態 (三値) を導出する処理。\n *\n * 判定不能 (`unknown`) を `absent` へ丸めない。存在確認そのものが失敗した場合を\n * 偽の真偽で埋めると、一時的な権限・通信障害が「消失」として表示される。\n */\n const deriveAttachmentState = (checkedAt: Date | string | null, missingAt: Date | string | null): DailyReportAttachmentSummary[\"state\"] => {\n if (missingAt != null) return \"absent\"\n if (checkedAt != null) return \"present\"\n return \"unknown\"\n }\n\n /**\n * Fetches attachments for multiple hubs in one query, keyed by hub id.\n * 複数 hub の添付ファイルを 1 クエリで取得し hub ID をキーにまとめる処理。\n *\n * 一覧描画は N 件の日報を同時に返すため、日報ごとに問い合わせると N+1 になる。\n * ラベル・コメントと同じ一括取得の流儀に揃える。\n *\n * @param hubIds Hub ids to fetch attachments for. 添付を取得する hub ID 群。\n * @returns Attachments grouped by hub id. hub ID ごとにまとめた添付一覧。\n */\n const getAttachmentsByHubIds = async (hubIds: number[]): Promise<Map<number, DailyReportAttachmentSummary[]>> => {\n const map = new Map<number, DailyReportAttachmentSummary[]>()\n const attachment = tables.attachment\n // codec は config 直参照。createDailyReportService の分割代入は 8 キーのみで、\n // 裸の識別子として書くと TS2304 になる (任意設定は config.x で読むのがこのファイルの様式)\n const codec = config.attachmentIdCodec\n if (hubIds.length === 0 || !attachment || !codec) {\n return map\n }\n\n const rows = await db\n .select<{ id: number; hubId: number; fileName: string; fileType: string | null; fileSize: number | null; createdAt: Date | string | null; objectCheckedAt: Date | string | null; objectMissingAt: Date | string | null }>({\n id: attachment.id,\n hubId: attachment.hubId,\n fileName: attachment.fileName,\n fileType: attachment.fileType,\n fileSize: attachment.fileSize,\n createdAt: attachment.createdAt,\n objectCheckedAt: attachment.objectCheckedAt,\n objectMissingAt: attachment.objectMissingAt,\n })\n .from(attachment)\n .where(inArray(attachment.hubId, hubIds))\n .orderBy(asc(attachment.createdAt), asc(attachment.id))\n\n for (const row of rows) {\n // 内部数値 ID はここで難読化トークンへ変換する。以降クライアントへ生 ID は出ない。\n // `uploaded_by` / `created_by` / `updated_by` / `file_path` は選択もしない\n const item: DailyReportAttachmentSummary = {\n id: codec.encode(Number(row.id)),\n fileName: String(row.fileName),\n fileType: row.fileType ? String(row.fileType) : null,\n fileSize: row.fileSize != null ? Number(row.fileSize) : null,\n createdAt: formatDateValue(row.createdAt, \"YYYY-MM-DD HH:mm:ss\"),\n state: deriveAttachmentState(row.objectCheckedAt, row.objectMissingAt),\n }\n const list = map.get(Number(row.hubId))\n if (list) {\n list.push(item)\n } else {\n map.set(Number(row.hubId), [item])\n }\n }\n\n return map\n }\n\n /**\n * Fetches one attachment together with its visibility check for a given user.\n * 指定ユーザーの可視性判定込みで添付ファイル 1 件を取得する処理。\n *\n * 認可は SQL 側で完結させる。アプリ層のフィルタリングに依存しない。\n * 認可されない / 存在しない / 親日報が論理削除済みのいずれでも `null` を返すため、\n * 呼び出し側は 403 と 404 を区別せず 404 とすること (存在オラクル回避)。\n *\n * 本関数は意図的に `SqlResultCache.getOrFetch` を通さない。これは読み取りキャッシュではなく\n * リクエストごとの**認可判定**であり、結果をキャッシュするとユーザーを跨いで再利用されうる。\n *\n * @param attachmentId Internal numeric attachment id. 内部数値の添付 ID。\n * @param userId Internal numeric user id of the viewer. 閲覧者の内部数値ユーザー ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Attachment row when visible, otherwise null. 可視なら添付行、それ以外は null。\n */\n const getAttachmentForUser = async (scope: DailyReportViewerScope, attachmentId: number, userId: number): Promise<{ fileName: string; filePath: string; fileType: string | null; fileSize: number | null } | null> => {\n const attachment = tables.attachment\n if (!attachment) {\n return null\n }\n // 全拒否なら DB を引かずに null を返す (0 件 = fail-close)\n if (deniesEverySourceType(scope.visible)) {\n return null\n }\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n\n // `.top(1)` は `.select()` の直後でなければならない。drizzle-mssql は top() を\n // db.select() が返す MsSqlSelectBuilder にしか定義しておらず、.from() 以降のクラスには\n // 存在しない。誤った順序は型 (構造型 SelectChain が全段階に top を宣言) でも\n // 単体テスト (モックが全段階で top を返す) でも検出されず、本番で TypeError になる。\n const rows = await db\n .select<{ fileName: string; filePath: string; fileType: string | null; fileSize: number | null }>({\n fileName: attachment.fileName,\n filePath: attachment.filePath,\n fileType: attachment.fileType,\n fileSize: attachment.fileSize,\n })\n .top(1)\n .from(attachment)\n .innerJoin(DailyReportHub, eq(DailyReportHub.id, attachment.hubId))\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .where(and(eq(attachment.id, attachmentId), isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DraftLabelRelation.hubId)), visibleSourceTypeCondition(scope.visible)))\n\n return rows[0] ?? null\n }\n\n /**\n * Records that an attachment's backing object was confirmed missing.\n * 添付ファイルの実体が消失していたことを記録する処理。\n *\n * 実配信で `not_found` を得たときに呼ぶ。次回描画で三値が `absent` になる。\n * 記録の失敗は配信結果に影響させない (呼び出し側は await しない)。\n *\n * @param attachmentId Internal numeric attachment id. 内部数値の添付 ID。\n */\n const markAttachmentMissing = async (attachmentId: number): Promise<void> => {\n const attachment = tables.attachment\n if (!attachment) return\n const now = new Date()\n try {\n await db.update(attachment).set({ objectCheckedAt: now, objectMissingAt: now, updatedAt: now }).where(eq(attachment.id, attachmentId))\n } catch (error) {\n logger.error(\"markAttachmentMissing failed\", error)\n }\n }\n\n /**\n * Records that an attachment's backing object was confirmed present.\n * 添付ファイルの実体が存在していたことを記録する処理。\n *\n * `markAttachmentMissing` と対にすること。消失記録が一方向だと、一時的な障害で\n * 一度 `absent` が立った添付は UI がリンクを描画しなくなり (押しても 404 なので当然)、\n * **利用者が再取得を発火させる手段そのものが消える**。実体が戻っても手動の\n * 一括検証を回すまで復帰しない。配信が 200 で成功した時点で解除するのが唯一の\n * 自動回復経路である。\n *\n * 記録の失敗は配信結果に影響させない (呼び出し側は await しない)。\n *\n * @param attachmentId Internal numeric attachment id. 内部数値の添付 ID。\n */\n const markAttachmentPresent = async (attachmentId: number): Promise<void> => {\n const attachment = tables.attachment\n if (!attachment) return\n const now = new Date()\n try {\n await db.update(attachment).set({ objectCheckedAt: now, objectMissingAt: null, updatedAt: now }).where(eq(attachment.id, attachmentId))\n } catch (error) {\n logger.error(\"markAttachmentPresent failed\", error)\n }\n }\n\n /**\n * Retrieves daily report details for a business date with user-specific status and relations.\n * 指定した営業日の日報詳細を、ユーザー状態と関連データを含めて取得する。\n */\n const getDailyReportsByBusinessDateByExternalId = async (scope: DailyReportViewerScope, businessDate: string, externalId: string, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail[]> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return []\n }\n\n const normalizedBusinessDate = normalizeBusinessDateKey(businessDate)\n if (!normalizedBusinessDate) {\n return []\n }\n // 全拒否なら DB を引かずに空を返す (0 件 = fail-close)\n if (deniesEverySourceType(scope.visible)) {\n return []\n }\n\n // 可視集合ごとに別バケットへ隔離する (権限変更後に旧結果を配信しない)\n const cacheKey = dateCacheKey(normalizedBusinessDate, userId, computeViewerScopeDigest(scope))\n\n return sqlResultCache.getOrFetch<DailyReportDetail>({\n cacheKey,\n fetcher: async () => {\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n\n const rows = await applyExternalJoins(\n db\n .select<HubQueryRow>({\n hub: cols(DailyReportHub),\n internal: cols(DailyReportInternal),\n isRead: DailyReportUserStatus.isRead,\n isStarred: DailyReportUserStatus.isStarred,\n creatorName: users.displayName,\n ...externalSelections(),\n })\n .from(DailyReportHub),\n )\n .leftJoin(DailyReportInternal, eq(DailyReportHub.id, DailyReportInternal.hubId))\n .leftJoin(DailyReportUserStatus, and(eq(DailyReportHub.id, DailyReportUserStatus.hubId), eq(DailyReportUserStatus.userId, userId)))\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .leftJoin(users, eq(DailyReportHub.userId, users.id))\n .where(and(eq(DailyReportHub.businessDate, sql`${normalizedBusinessDate}`), isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DraftLabelRelation.hubId)), visibleSourceTypeCondition(scope.visible)))\n .orderBy(desc(DailyReportHub.id))\n\n if (rows.length === 0) {\n return []\n }\n\n const hubIds = rows.map((r) => r.hub.id)\n\n // Drizzle ORM batch queries to fetch labels and comments\n const allLabels = await db\n .select<{ hubId: number; id: number; name: string; color: string | null }>({\n hubId: DailyReportHub_Label.hubId,\n id: DailyReportLabel.id,\n name: DailyReportLabel.name,\n color: DailyReportLabel.color,\n })\n .from(DailyReportHub_Label)\n .innerJoin(DailyReportLabel, eq(DailyReportHub_Label.labelId, DailyReportLabel.id))\n .where(inArray(DailyReportHub_Label.hubId, hubIds))\n\n const allComments = await db\n .select<{ hubId: number; id: number; body: string; createdAt: Date | string; userId: number; userName: string | null }>({\n hubId: DailyReportCommentModel.hubId,\n id: DailyReportCommentModel.id,\n body: DailyReportCommentModel.body,\n createdAt: DailyReportCommentModel.createdAt,\n userId: DailyReportCommentModel.userId,\n userName: users.displayName,\n })\n .from(DailyReportCommentModel)\n .leftJoin(users, eq(DailyReportCommentModel.userId, users.id))\n .where(inArray(DailyReportCommentModel.hubId, hubIds))\n .orderBy(asc(DailyReportCommentModel.createdAt))\n\n // 添付も同じ流儀で一括取得する (日報ごとに引くと N+1 になる)\n const attachmentsMap = await getAttachmentsByHubIds(hubIds)\n\n const labelsMap = new Map<number, RawJsonLabel[]>()\n const commentsMap = new Map<number, RawJsonComment[]>()\n\n for (const label of allLabels) {\n let list = labelsMap.get(label.hubId)\n if (!list) {\n list = []\n labelsMap.set(label.hubId, list)\n }\n list.push({\n id: label.id,\n name: label.name,\n color: label.color,\n })\n }\n\n for (const comment of allComments) {\n let list = commentsMap.get(comment.hubId)\n if (!list) {\n list = []\n commentsMap.set(comment.hubId, list)\n }\n list.push({\n id: comment.id,\n content: comment.body,\n createdAt: formatDateValue(comment.createdAt, \"YYYY-MM-DD HH:mm:ss\") || \"\",\n userId: comment.userId,\n userName: comment.userName || \"\",\n })\n }\n\n return rows.map((row) => {\n const hubId = row.hub.id\n return mapHubRecord(\n scope,\n {\n ...row,\n attachments: attachmentsMap.get(hubId) ?? [],\n labels: labelsMap.get(hubId) || [],\n postedCommentRows: commentsMap.get(hubId) || [],\n },\n userId,\n )\n })\n },\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_BUSINESS_DATE_TTL_MS,\n epochKey: `${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedBusinessDate}`,\n }) as Promise<DailyReportDetail[]>\n }\n\n /**\n * Retrieves a single daily report detail by ID with user-specific status and relations.\n * 指定した日報詳細を、ユーザー状態と関連データを含めて取得する。\n *\n * 可視性の述語は営業日一覧 (`getDailyReportsByBusinessDateByExternalId`) および\n * `getAttachmentForUser` と同一に保つこと。3 経路のどれか 1 つだけ緩いと、\n * 緩い経路がメタデータ (件名・本文・添付のファイル名やトークン) を渡してしまい、\n * 「SQL 述語が唯一の認可境界」という不変条件が崩れる。\n * 4 番目の経路である `fetchDailyReportIdsByUserId` だけは意図的に 1 つ緩い\n * (返すのが ID・営業日・ソース種別のみのため)。詳細は同関数の docstring を参照。\n *\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param userId Internal numeric user id of the viewer. 閲覧者の内部数値ユーザー ID。\n * @param options Cache query options. キャッシュ問い合わせオプション。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Detail when visible, otherwise null. 可視なら詳細、そうでなければ null。\n */\n const getDailyReportDetailById = async (scope: DailyReportViewerScope, reportHubId: number, userId: number, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail | null> => {\n // 全拒否なら DB を引かずに null を返す (0 件 = fail-close)\n if (deniesEverySourceType(scope.visible)) {\n return null\n }\n\n // 可視集合ごとに別バケットへ隔離する (権限変更後に旧結果を配信しない)\n const cacheKey = detailCacheKey(reportHubId, userId, computeViewerScopeDigest(scope))\n\n const results = (await sqlResultCache.getOrFetch<DailyReportDetail>({\n cacheKey,\n fetcher: async () => {\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n\n const rows = await applyExternalJoins(\n db\n .select<HubQueryRow>({\n hub: cols(DailyReportHub),\n internal: cols(DailyReportInternal),\n isRead: DailyReportUserStatus.isRead,\n isStarred: DailyReportUserStatus.isStarred,\n creatorName: users.displayName,\n ...externalSelections(),\n })\n .from(DailyReportHub),\n )\n .leftJoin(DailyReportInternal, eq(DailyReportHub.id, DailyReportInternal.hubId))\n .leftJoin(DailyReportUserStatus, and(eq(DailyReportHub.id, DailyReportUserStatus.hubId), eq(DailyReportUserStatus.userId, userId)))\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .leftJoin(users, eq(DailyReportHub.userId, users.id))\n .where(and(eq(DailyReportHub.id, reportHubId), isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DraftLabelRelation.hubId)), visibleSourceTypeCondition(scope.visible)))\n\n if (rows.length === 0) {\n return []\n }\n\n const hubId = rows[0].hub.id\n\n // Fetch labels and comments via Drizzle ORM\n const labels = await db\n .select<{ id: number; name: string; color: string | null }>({\n id: DailyReportLabel.id,\n name: DailyReportLabel.name,\n color: DailyReportLabel.color,\n })\n .from(DailyReportHub_Label)\n .innerJoin(DailyReportLabel, eq(DailyReportHub_Label.labelId, DailyReportLabel.id))\n .where(eq(DailyReportHub_Label.hubId, hubId))\n\n const comments = await db\n .select<{ id: number; body: string; createdAt: Date | string; userId: number; userName: string | null }>({\n id: DailyReportCommentModel.id,\n body: DailyReportCommentModel.body,\n createdAt: DailyReportCommentModel.createdAt,\n userId: DailyReportCommentModel.userId,\n userName: users.displayName,\n })\n .from(DailyReportCommentModel)\n .leftJoin(users, eq(DailyReportCommentModel.userId, users.id))\n .where(eq(DailyReportCommentModel.hubId, hubId))\n .orderBy(asc(DailyReportCommentModel.createdAt))\n\n const attachmentsMap = await getAttachmentsByHubIds([hubId])\n\n const labelsMapped = labels.map((l) => ({\n id: l.id,\n name: l.name,\n color: l.color,\n }))\n\n const commentsMapped = comments.map((c) => ({\n id: c.id,\n content: c.body,\n createdAt: formatDateValue(c.createdAt, \"YYYY-MM-DD HH:mm:ss\") || \"\",\n userId: c.userId,\n userName: c.userName || \"\",\n }))\n\n return [\n mapHubRecord(\n scope,\n {\n ...rows[0],\n attachments: attachmentsMap.get(hubId) ?? [],\n labels: labelsMapped,\n postedCommentRows: commentsMapped,\n },\n userId,\n ),\n ]\n },\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_BUSINESS_DATE_TTL_MS,\n epochKey: `${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`,\n })) as DailyReportDetail[]\n\n return results[0] ?? null\n }\n\n /**\n * Retrieves a single daily report detail by ID with user-specific status and relations.\n * 指定した日報詳細を、ユーザー状態と関連データを含めて取得する。\n *\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param externalId External user id. 外部ユーザー ID。\n * @param options Cache query options. キャッシュ問い合わせオプション。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Detail when visible, otherwise null. 可視なら詳細、そうでなければ null。\n */\n const getDailyReportDetailByIdByExternalId = async (scope: DailyReportViewerScope, reportHubId: number, externalId: string, options: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail | null> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return null\n }\n // ❗ 受け取った可視集合はそのまま透過する。ここで null へ潰すと外部 ID 経由の詳細取得だけ\n // フィルタが外れる (内部の create/update/publish が明示 null を渡すのとは別物)\n return getDailyReportDetailById(scope, reportHubId, userId, options)\n }\n\n /**\n * Loads the gate columns of a hub for a mutation, or null when it does not exist.\n * ミューテーション用に hub のゲート列を取得する処理 (存在しなければ null)。\n *\n * @param reportHubId Hub id. 対象 hub の ID。\n * @returns Source type and deletion marker, or null. ソース種別と論理削除印 (無ければ null)。\n */\n const findHubGateColumns = async (reportHubId: number, userId: number): Promise<{ sourceType: string; deletedAt: Date | string | null } | null> => {\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n\n // ❗ 可視性の述語は読み取り経路と同一に保つ。下書き条項を落とすと、閲覧できないはずの\n // 他人の下書きに対して既読 / スター / コメントを付けられる (書き込み可能な存在オラクル)\n const [hub] = await db\n .select<{ sourceType: string; deletedAt: Date | string | null }>({ sourceType: DailyReportHub.sourceType, deletedAt: DailyReportHub.deletedAt })\n .top(1)\n .from(DailyReportHub)\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .where(and(eq(DailyReportHub.id, reportHubId), or(eq(DailyReportHub.userId, userId), isNull(DraftLabelRelation.hubId))))\n return hub ?? null\n }\n\n /**\n * Rejects a status mutation targeting a missing, deleted, or invisible report.\n * 不存在・論理削除済み・不可視区分の日報に対する状態更新を拒否する処理。\n *\n * 認可失敗と不存在を区別せず一律 `Not Found` にする (存在オラクルの回避)。従来は hub を\n * 一切検証せず任意の `reportHubId` へ upsert できたため、その穴も同時に塞ぐ。\n *\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Nothing; throws when rejected. なし (拒否時は例外送出)。\n */\n const assertHubMutable = async (scope: DailyReportViewerScope, reportHubId: number, userId: number): Promise<void> => {\n const hub = await findHubGateColumns(reportHubId, userId)\n if (!hub || hub.deletedAt || !isSourceTypeVisible(scope.visible, hub.sourceType)) {\n throw new Error(\"Not Found\")\n }\n }\n\n /**\n * Sets the starred status of a daily report for a user.\n * ユーザーの日報スター状態を設定する。\n *\n * @param userId Internal numeric user id. 内部数値ユーザー ID。\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param businessDate Business date for cache invalidation. キャッシュ無効化に使う営業日。\n * @param isStarred Desired star state. 設定するスター状態。\n * @param clientTempId Optimistic-update correlation id. 楽観更新の相関 ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Updated status row. 更新後の状態行。\n */\n const setStarStatus = async (scope: DailyReportViewerScope, userId: number, reportHubId: number, businessDate: string | null, isStarred: boolean, clientTempId: string): Promise<DailyReportUserStatusRow> => {\n // 不可視区分・不存在・削除済みへの書き込みを先に弾く\n await assertHubMutable(scope, reportHubId, userId)\n\n const existing = await db\n .select<DailyReportUserStatusRow>()\n .top(1)\n .from(DailyReportUserStatus)\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n\n let updatedStatus: DailyReportUserStatusRow\n\n if (existing.length > 0) {\n if (existing[0].isStarred !== isStarred) {\n const rows = await db\n .update<DailyReportUserStatusRow>(DailyReportUserStatus)\n .set({\n isStarred: isStarred,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n .output()\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n updatedStatus = rows[0]\n } else {\n updatedStatus = existing[0]\n }\n } else {\n const rows = await db\n .insert<DailyReportUserStatusRow>(DailyReportUserStatus)\n .output()\n .values({\n hubId: reportHubId,\n userId,\n isStarred: isStarred,\n isRead: false,\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n updatedStatus = rows[0]\n }\n\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n // 可視集合ダイジェスト付きキーを取りこぼさないよう接頭辞で消す\n sqlResultCache.invalidatePrefix(dateUserPrefix(normalizedDate, userId))\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n // 可視集合ダイジェスト付きキーを取りこぼさないよう接頭辞で消す\n sqlResultCache.invalidatePrefix(detailUserPrefix(reportHubId, userId))\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n await publishToSseStream(\n statusUpdateMessageSchema.parse({\n type: \"status-update\",\n reportHubId: reportHubId,\n recipientRawUserId: userId,\n statusType: \"star\",\n value: isStarred,\n clientTempId,\n }),\n \"setStarStatus\",\n )\n\n return updatedStatus\n }\n\n /**\n * Sets the read status of a daily report for a user.\n * ユーザーの日報既読状態を設定する。\n *\n * @param userId Internal numeric user id. 内部数値ユーザー ID。\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param businessDate Business date for cache invalidation. キャッシュ無効化に使う営業日。\n * @param isRead Desired read state. 設定する既読状態。\n * @param clientTempId Optimistic-update correlation id. 楽観更新の相関 ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Updated status row. 更新後の状態行。\n */\n const setReadStatus = async (scope: DailyReportViewerScope, userId: number, reportHubId: number, businessDate: string | null, isRead: boolean, clientTempId: string): Promise<DailyReportUserStatusRow> => {\n // 不可視区分・不存在・削除済みへの書き込みを先に弾く\n await assertHubMutable(scope, reportHubId, userId)\n\n const existing = await db\n .select<DailyReportUserStatusRow>()\n .top(1)\n .from(DailyReportUserStatus)\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n\n let updatedStatus: DailyReportUserStatusRow\n\n if (existing.length > 0) {\n if (existing[0].isRead !== isRead) {\n const rows = await db\n .update<DailyReportUserStatusRow>(DailyReportUserStatus)\n .set({\n isRead: isRead,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n .output()\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n updatedStatus = rows[0]\n } else {\n updatedStatus = existing[0]\n }\n } else {\n const rows = await db\n .insert<DailyReportUserStatusRow>(DailyReportUserStatus)\n .output()\n .values({\n hubId: reportHubId,\n userId,\n isRead: isRead,\n isStarred: false,\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n updatedStatus = rows[0]\n }\n\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n // 可視集合ダイジェスト付きキーを取りこぼさないよう接頭辞で消す\n sqlResultCache.invalidatePrefix(dateUserPrefix(normalizedDate, userId))\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n // 可視集合ダイジェスト付きキーを取りこぼさないよう接頭辞で消す\n sqlResultCache.invalidatePrefix(detailUserPrefix(reportHubId, userId))\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n await publishToSseStream(\n statusUpdateMessageSchema.parse({\n type: \"status-update\",\n reportHubId: reportHubId,\n recipientRawUserId: userId,\n statusType: \"read\",\n value: isRead,\n clientTempId,\n }),\n \"setReadStatus\",\n )\n\n return updatedStatus\n }\n\n /**\n * Resolves the SSE recipient for comment events on a possibly-draft report.\n * 下書きの可能性がある日報のコメントイベントについて SSE の宛先を解決する処理。\n *\n * 下書きは所有者にしか見えないため、下書きラベルが付いている間はコメント本文も\n * 所有者だけに配信する (無指定だと本文が全ユーザーへブロードキャストされる)。\n * 公開済み (下書きラベル無し) は全員配信 = undefined を返す。\n *\n * @param hubId Parent hub id. 親日報の hub ID。\n * @param ownerUserId Hub owner's raw user id. 所有者の内部ユーザー ID。\n * @returns Recipient raw user id, or undefined to broadcast. 宛先 (全員配信は undefined)。\n */\n const resolveCommentRecipient = async (hubId: number, ownerUserId: number | null): Promise<number | undefined> => {\n // 所有者不明 (共有日報など) は従来どおり全員配信\n if (ownerUserId === null) return undefined\n const draftIds = await getDraftLabelJoinIds()\n const [rel] = await db\n .select<{ hubId: number }>({ hubId: DailyReportHub_Label.hubId })\n .top(1)\n .from(DailyReportHub_Label)\n .where(and(eq(DailyReportHub_Label.hubId, hubId), inArray(DailyReportHub_Label.labelId, draftIds)))\n return rel ? ownerUserId : undefined\n }\n\n /**\n * Adds a comment to a daily report.\n * 日報にコメントを追加する。\n *\n * @param userId Internal numeric user id. 内部数値ユーザー ID。\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param content Comment body. コメント本文。\n * @param businessDate Business date for cache invalidation. キャッシュ無効化に使う営業日。\n * @param clientTempId Optimistic-update correlation id. 楽観更新の相関 ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns The created comment. 追加されたコメント。\n */\n const addComment = async (scope: DailyReportViewerScope, userId: number, reportHubId: number, content: string, businessDate: string | null, clientTempId: string): Promise<DailyReportPostedComment> => {\n const [hub] = await db\n .select<{ sourceType: string; deletedAt: Date | string | null; userId: number | null }>({ sourceType: DailyReportHub.sourceType, deletedAt: DailyReportHub.deletedAt, userId: DailyReportHub.userId })\n .top(1)\n .from(DailyReportHub)\n .where(eq(DailyReportHub.id, reportHubId))\n // 存在しない / 論理削除済みの日報にはコメントを付けない (以前は hub 未取得でも\n // フォールスルーして孤児コメントを INSERT していた)\n if (!hub || hub.deletedAt) {\n throw new Error(\"Not Found\")\n }\n // 不可視区分・他人の下書きにはコメントできない。認可失敗と不存在を区別せず Not Found にする\n // (存在オラクルの回避)。読み取り経路と同一の可視性述語を共通ゲートで適用する\n await assertHubMutable(scope, reportHubId, userId)\n // コメント次元が拒否する区分には記入させない。本文は読めても書けない状態を表現するため、\n // 不存在 (Not Found) ではなく「制限」として返す — 閲覧者は当該日報を見えているので\n // 不存在を装っても存在オラクルにはならず、嘘をつくだけ UI の説明性が落ちる\n if (!isSourceTypeVisible(scope.commentable, hub.sourceType)) {\n throw new Error(\"Comments are restricted by the viewer's comment permission\")\n }\n if (hub.sourceType.toLowerCase() !== \"internal\") {\n throw new Error(\"Comments are restricted for external daily report sources\")\n }\n // 下書きなら SSE を所有者宛に絞る (本文の全員配信を防ぐ)\n const recipientRawUserId = await resolveCommentRecipient(reportHubId, hub.userId)\n\n const [inserted] = await db\n .insert<DailyReportCommentRow>(DailyReportCommentModel)\n .output()\n .values({\n hubId: reportHubId,\n userId,\n body: content,\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n\n const [user] = await db.select<{ displayName: string | null }>({ displayName: users.displayName }).top(1).from(users).where(eq(users.id, userId))\n const userName = user?.displayName ?? \"Unknown\"\n\n // コメントは全ユーザーに表示されるため、date キャッシュも全ユーザー分を無効化\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n sqlResultCache.invalidatePrefix(`daily-report:date:${normalizedDate}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n const commentItem: DailyReportPostedComment = {\n id: inserted.id,\n userId: encodeUserId(inserted.userId),\n userName: userName,\n content: inserted.body,\n createdAt: formatDateValue(inserted.createdAt, \"YYYY-MM-DD HH:mm:ss\") ?? \"\",\n isMine: true,\n }\n\n await publishToSseStream(\n commentAddMessageSchema.parse({\n type: \"comment-add\",\n reportHubId: reportHubId,\n comment: commentItem,\n clientTempId,\n recipientRawUserId,\n // 受信側の区分フィルタ用 (旧エントリ互換のためスキーマ上は任意)\n sourceType: hub.sourceType,\n }),\n \"addComment\",\n )\n\n return commentItem\n }\n\n /**\n * Finds a daily report comment by ID.\n * 日報コメントをIDで検索する。\n */\n const findDailyReportCommentById = async (tx: DailyReportDb, commentId: number) => {\n return await tx.select<DailyReportCommentRow>().top(1).from(DailyReportCommentModel).where(eq(DailyReportCommentModel.id, commentId))\n }\n\n /**\n * Deletes a comment from a daily report.\n * 日報のコメントを削除する。\n *\n * @param userId Internal numeric user id. 内部数値ユーザー ID。\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param commentId Comment id to delete. 削除対象のコメント ID。\n * @param businessDate Business date for cache invalidation. キャッシュ無効化に使う営業日。\n * @param clientTempId Optimistic-update correlation id. 楽観更新の相関 ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Nothing. なし。\n */\n const deleteComment = async (scope: DailyReportViewerScope, userId: number, reportHubId: number, commentId: number, businessDate: string | null, clientTempId: string): Promise<void> => {\n const comments = await findDailyReportCommentById(db, commentId)\n if (comments.length === 0) {\n throw new Error(\"Not Found\")\n }\n\n // 渡された reportHubId がコメントの実際の所属と一致することを確認する。\n // 不一致のまま進むと、無関係な日報のキャッシュ無効化と SSE 配信を引き起こす。\n // ❗ 所有者チェックより**前**に置く。後ろだと、攻撃者が自分に可視な hubId を添えて任意の\n // commentId を試すことで「他人のコメント = 403 / 存在しない = 404」と応答が割れ、\n // コメントの存在を列挙できてしまう\n if (comments[0].hubId !== reportHubId) {\n throw new Error(\"Not Found\")\n }\n\n // ❗ 区分ゲートも所有者チェックより前に置く。参照するのは**コメント自身の所属 hub** で\n // あり、呼び出し側が渡した ID ではない (上で一致を確認済み)。hub 行が引けない場合も\n // fail-close で Not Found にする (assertHubMutable と同一の規約)\n // ❗ ここで見るのは本文次元 (`scope.visible`) であって**コメント次元ではない**。意図的。\n // 自分のコメントは常に可視という契約 (mapHubRecord 参照) の対を成す規則であり、\n // コメント次元でゲートすると権限剥奪の瞬間に既存の自分のコメントが恒久的に削除不能に\n // なる。所有者チェックが単独で十分な防御になっている\n const [hubForDelete] = await db.select<{ userId: number | null; sourceType: string }>({ userId: DailyReportHub.userId, sourceType: DailyReportHub.sourceType }).top(1).from(DailyReportHub).where(eq(DailyReportHub.id, comments[0].hubId))\n if (!(hubForDelete && isSourceTypeVisible(scope.visible, hubForDelete.sourceType))) {\n throw new Error(\"Not Found\")\n }\n\n if (comments[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n // 下書きなら削除イベントも所有者宛に絞る (comment-add と対称)\n const recipientRawUserId = await resolveCommentRecipient(reportHubId, hubForDelete.userId)\n\n await db.delete(DailyReportCommentModel).where(eq(DailyReportCommentModel.id, commentId))\n\n // コメントは全ユーザーに表示されるため、date キャッシュも全ユーザー分を無効化\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n sqlResultCache.invalidatePrefix(`daily-report:date:${normalizedDate}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n await publishToSseStream(\n commentDeleteMessageSchema.parse({\n type: \"comment-delete\",\n reportHubId: reportHubId,\n commentId,\n clientTempId,\n recipientRawUserId,\n // 受信側の区分フィルタ用 (hub 行が引けない場合は undefined = 旧エントリ相当の扱い)\n sourceType: hubForDelete?.sourceType,\n }),\n \"deleteComment\",\n )\n }\n\n /**\n * Creates a new draft daily report.\n * 新しい日報(下書き)を作成する。\n */\n const createDailyReport = async (userId: number, businessDate: string, clientTempId: string): Promise<DailyReportDetail> => {\n logger.info(\"createDailyReport called\", { userId, businessDate })\n\n // ユーザー名取得\n const [user] = await db.select<{ displayName: string | null }>({ displayName: users.displayName }).from(users).where(eq(users.id, userId))\n const userName = user?.displayName ?? encodeUserId(userId)\n\n try {\n const result = await db.transaction(async (tx) => {\n logger.info(\"Starting transaction\")\n // 1. Hub作成\n const sourceId = `internal-temp-${Date.now()}-${Math.random()}` // 一時的なID\n const [hub] = await tx\n .insert<DailyReportHubRow>(DailyReportHub)\n .output()\n .values({\n sourceType: \"Internal\",\n sourceId: sourceId,\n businessDate: new Date(businessDate),\n userId: userId,\n title: \"(無題)\",\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n\n // sourceId を id と同じ値に更新 (Internal の場合の正規化)\n await tx\n .update(DailyReportHub)\n .set({ sourceId: String(hub.id) })\n .where(eq(DailyReportHub.id, hub.id))\n\n logger.info(\"Hub created\", hub)\n\n // 2. Internal作成\n await tx.insert(DailyReportInternal).values({\n hubId: hub.id,\n body: \"\",\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n logger.info(\"Internal created\")\n\n // 3. ラベル付与\n logger.info(\"Calling getDraftLabelId\")\n const draftLabelId = await getDraftLabelId()\n logger.info(\"draftLabelId\", draftLabelId)\n const labels: DailyReportLabelDef[] = []\n if (draftLabelId) {\n await tx.insert(DailyReportHub_Label).values({\n hubId: hub.id,\n labelId: draftLabelId,\n createdAt: new Date(),\n createdBy: String(userId),\n })\n labels.push({ id: draftLabelId, name: draftLabelName, color: null })\n }\n logger.info(\"Label assigned\")\n\n // 4. 詳細オブジェクト構築\n return {\n reportHubId: hub.id,\n date: businessDate,\n createdAt: formatDateValue(hub.createdAt, \"YYYY-MM-DD HH:mm:ss\"),\n author: userName,\n userId: encodeUserId(userId),\n sourceType: \"Internal\",\n employeeName: userName,\n // 監査列と同様、クライアント公開時は生内部 ID を難読化する (mapHubRecord と整合)\n updatedBy: encodeUserId(userId),\n updatedAt: formatDateValue(hub.updatedAt, \"YYYY-MM-DD HH:mm:ss\"),\n category: null,\n creationCategory: null,\n visitTimeFrom: null,\n visitTimeTo: null,\n customerName: null,\n interviewers: [],\n subject: hub.title,\n content: \"\",\n externalComments: [],\n isRead: true, // 自分で作ったので既読\n isStarred: false,\n labels: labels,\n postedComments: [],\n // 新規作成直後の日報に添付は存在しない\n attachments: [],\n }\n })\n\n // キャッシュ無効化\n const normalizedBusinessDate = normalizeBusinessDateKey(businessDate)\n if (normalizedBusinessDate) {\n // 可視集合ダイジェスト付きキーを取りこぼさないよう接頭辞で消す\n sqlResultCache.invalidatePrefix(dateUserPrefix(normalizedBusinessDate, userId))\n }\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n // ❗ createDailyReport には ids の広域 prefix 無効化が無く、この 1 行が作成者の ID 一覧を\n // 落とす唯一の経路。redis 未注入の配備では epoch も進まないため、ここを消すと\n // 作成した日報が自分の一覧から TTL 分 (既定 180 秒) 消える。接頭辞化のみ行う\n sqlResultCache.invalidatePrefix(idsUserPrefix(userId))\n // ❗ 順序重要: invalidate → incrementRedisEpoch → publishToSseStream\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n if (normalizedBusinessDate) {\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedBusinessDate}`)\n }\n\n // ❗ 明示 null (無制限)。所有者が自分で作った日報を取り直す経路なので、消費側の可視集合に\n // 作成区分 (\"Internal\") が含まれなくても取得と SSE 配信を壊さない。区分の出し分けは\n // 受信側の SSE フィルタと次回読み取り時のサーバーフィルタが担う\n const fullDetail = await getDailyReportDetailById(DailyReportViewerScope.unrestricted(\"post-write self read: the writer always sees the row they just wrote\"), result.reportHubId, userId, { forceRefresh: true })\n\n if (fullDetail) {\n // 下書き判定\n // 判定は代表 ID ではなく下書きラベル**全体**で行う (2 つ目の下書き名を見落とさない)\n const draftLabelIds = await getDraftLabelIds()\n const isDraft = fullDetail.labels.some((l) => draftLabelIds.includes(l.id))\n await publishToSseStream(\n reportCreateMessageSchema.parse({\n type: \"report-create\",\n reportHubId: fullDetail.reportHubId,\n report: fullDetail,\n clientTempId,\n recipientRawUserId: isDraft ? userId : undefined,\n }),\n \"createDailyReport\",\n )\n }\n\n return fullDetail ?? result\n } catch (e) {\n logger.error(\"Error in createDailyReport\", e)\n throw e\n }\n }\n\n // --- Internal Repository Layer ---\n\n /**\n * Finds a DailyReportHub by ID.\n * 日報HubをIDで検索する。\n */\n const findDailyReportHubById = async (tx: DailyReportDb, reportHubId: number) => {\n return await tx.select<DailyReportHubRow>().top(1).from(DailyReportHub).where(eq(DailyReportHub.id, reportHubId))\n }\n\n /**\n * Updates a DailyReportHub.\n * 日報Hubを更新する。\n */\n const updateDailyReportHub = async (tx: DailyReportDb, reportHubId: number, data: Partial<DailyReportHubRow>) => {\n await tx.update(DailyReportHub).set(data).where(eq(DailyReportHub.id, reportHubId))\n }\n\n /**\n * Updates a DailyReportInternal.\n * 日報Internalを更新する。\n */\n const updateDailyReportInternal = async (tx: DailyReportDb, hubId: number, data: Partial<DailyReportInternalRow>) => {\n await tx.update(DailyReportInternal).set(data).where(eq(DailyReportInternal.hubId, hubId))\n }\n\n /**\n * Deletes a label from a DailyReportHub.\n * 日報Hubからラベルを削除する。\n */\n const deleteDailyReportLabel = async (tx: DailyReportDb, hubId: number, labelId: number) => {\n await tx.delete(DailyReportHub_Label).where(and(eq(DailyReportHub_Label.hubId, hubId), eq(DailyReportHub_Label.labelId, labelId)))\n }\n\n // --- Service Layer ---\n\n /**\n * Logically deletes a daily report.\n * 日報を論理削除する。\n */\n const deleteDailyReport = async (reportHubId: number, userId: number, clientTempId: string): Promise<void> => {\n const report = await findDailyReportHubById(db, reportHubId)\n if (!report.length || report[0].deletedAt) {\n throw new Error(\"Not Found\")\n }\n if (report[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n await updateDailyReportHub(db, reportHubId, {\n deletedAt: new Date(),\n deletedBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n\n // キャッシュ無効化\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n // 削除されたレポートは全ユーザーの一覧から消えるため、全ユーザーの IDs キャッシュを invalidate\n sqlResultCache.invalidatePrefix(DAILY_REPORT_IDS_CACHE_KEY)\n // ❗ 順序重要: invalidatePrefix → incrementRedisEpoch → publishToSseStream\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n if (report[0].businessDate) {\n const normalizedDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (normalizedDate) {\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n\n await publishToSseStream(\n reportDeleteMessageSchema.parse({\n type: \"report-delete\",\n reportHubId: reportHubId,\n clientTempId,\n // 受信側の区分フィルタ用 (旧エントリ互換のためスキーマ上は任意)\n sourceType: report[0].sourceType,\n }),\n \"deleteDailyReport\",\n )\n }\n\n /**\n * Updates an existing daily report.\n * 日報を更新する。\n */\n const updateDailyReport = async (reportHubId: number, userId: number, data: { title?: string; content?: string }, clientTempId: string): Promise<void> => {\n const report = await findDailyReportHubById(db, reportHubId)\n if (!report.length || report[0].deletedAt) {\n throw new Error(\"Not Found\")\n }\n if (report[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n await db.transaction(async (tx) => {\n if (data.title !== undefined) {\n await updateDailyReportHub(tx, reportHubId, {\n title: data.title,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n }\n if (data.content !== undefined) {\n await updateDailyReportInternal(tx, reportHubId, {\n body: data.content,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n }\n })\n\n // キャッシュ無効化: 一覧プレビューは詳細キャッシュから引くため、詳細を全プロセス横断で落とす。\n // 以前は読み取りに使われないプレフィックスを無効化し epoch も上げていなかったため、\n // 編集後の件名/本文が他ワーカーで古いまま配信されていた\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n // 編集内容は全ユーザーの一覧に映るため、営業日別一覧は全ユーザー分 + 全ワーカー横断で\n // 無効化する (addComment と同じ流儀。編集者本人のキーだけでは他者へ古い件名が残る)\n const editedBusinessDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (editedBusinessDate) {\n sqlResultCache.invalidatePrefix(`daily-report:date:${editedBusinessDate}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${editedBusinessDate}`)\n }\n\n // ❗ 明示 null (無制限)。所有者が自分の日報を取り直す経路であり、区分の出し分けは\n // 受信側の SSE フィルタと次回読み取り時のサーバーフィルタが担う\n const fullDetail = await getDailyReportDetailById(DailyReportViewerScope.unrestricted(\"post-write self read: the writer always sees the row they just wrote\"), reportHubId, userId, { forceRefresh: true })\n if (fullDetail) {\n // 下書き判定\n // 判定は代表 ID ではなく下書きラベル**全体**で行う (2 つ目の下書き名を見落とさない)\n const draftLabelIds = await getDraftLabelIds()\n const isDraft = fullDetail.labels.some((l) => draftLabelIds.includes(l.id))\n await publishToSseStream(\n reportUpdateMessageSchema.parse({\n type: \"report-update\",\n reportHubId: fullDetail.reportHubId,\n report: fullDetail,\n clientTempId,\n recipientRawUserId: isDraft ? userId : undefined,\n }),\n \"updateDailyReport\",\n )\n }\n }\n\n /**\n * Publishes a draft daily report.\n * 日報を公開する(下書きラベルを削除)。\n */\n const publishDailyReport = async (reportHubId: number, userId: number, clientTempId: string): Promise<DailyReportDetail | null> => {\n const report = await findDailyReportHubById(db, reportHubId)\n if (!report.length) {\n throw new Error(\"Not Found\")\n }\n if (report[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n const draftLabelId = await getDraftLabelId()\n if (!draftLabelId) return null\n\n await deleteDailyReportLabel(db, reportHubId, draftLabelId)\n\n // キャッシュ無効化\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n // 公開によりドラフトが全ユーザーに可視化されるため、全ユーザーの IDs キャッシュを invalidate\n sqlResultCache.invalidatePrefix(DAILY_REPORT_IDS_CACHE_KEY)\n // ❗ 順序重要: invalidatePrefix → incrementRedisEpoch → publishToSseStream\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n if (report[0].businessDate) {\n const normalizedDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (normalizedDate) {\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n\n // ユーザーごとの営業日別一覧キャッシュも無効化\n const normalizedBusinessDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (normalizedBusinessDate) {\n // 可視集合ダイジェスト付きキーを取りこぼさないよう接頭辞で消す\n sqlResultCache.invalidatePrefix(dateUserPrefix(normalizedBusinessDate, userId))\n }\n\n // ❗ 明示 null (無制限)。所有者が自分の日報を取り直す経路であり、区分の出し分けは\n // 受信側の SSE フィルタと次回読み取り時のサーバーフィルタが担う\n const fullDetail = await getDailyReportDetailById(DailyReportViewerScope.unrestricted(\"post-write self read: the writer always sees the row they just wrote\"), reportHubId, userId, { forceRefresh: true })\n if (fullDetail) {\n await publishToSseStream(\n reportPublishMessageSchema.parse({\n type: \"report-publish\",\n reportHubId: fullDetail.reportHubId,\n report: fullDetail,\n clientTempId,\n }),\n \"publishDailyReport\",\n )\n }\n return fullDetail ?? null\n }\n\n /**\n * Invalidates the read caches that carry hub-scoped data across every process.\n * hub 単位のデータを載せている読み取りキャッシュを、全プロセス横断で無効化する処理。\n *\n * 用途: 通常の更新経路 (create / update / publish) を通らずに DB を書き換えた場合、\n * 例えば外部システムからの一括取込。プロセス内 Map を消すだけでは稼働中の\n * 別プロセス (別ワーカー・別コンテナ) のキャッシュが TTL まで stale のままになるため、\n * Redis の epoch を進めて全プロセスのキャッシュ行を一斉に無効化する。\n *\n * `clearCache()` との違い: `clearCache()` はプロセス内キャッシュの全消去 + 一覧 ID の\n * epoch のみ進める粗い操作。こちらは対象 hub / 営業日 / 一覧 ID の三系統を epoch まで\n * 含めて狙い撃ちで無効化する。日報本体に紐づくデータを書き換えたときはこちらを使うこと。\n *\n * @param hubIds Hub ids whose detail cache must be dropped. 詳細キャッシュを捨てる hub ID 群。\n * @param businessDates Business dates (YYYY-MM-DD) whose list cache must be dropped. 一覧キャッシュを捨てる営業日群。\n * @returns Nothing. なし。\n */\n const invalidateHubReadCaches = async (hubIds: readonly number[], businessDates: readonly string[]): Promise<void> => {\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n // 一覧 ID は「どの行が存在するか」そのもの。hub の行集合を変えうる呼び出し元\n // (取込・移行) がこの関数を頼る以上、詳細・営業日と同時に捨てる。\n // 現在の唯一の呼び出し元 (添付同期) は行集合を変えないため、これは将来の\n // 行集合変更呼び出し元に対する予防である\n sqlResultCache.invalidatePrefix(DAILY_REPORT_IDS_CACHE_KEY)\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n for (const hubId of new Set(hubIds)) {\n sqlResultCache.invalidatePrefix(`daily-report:detail:${hubId}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${hubId}`)\n }\n for (const businessDate of new Set(businessDates)) {\n const normalized = normalizeBusinessDateKey(businessDate)\n // 正規化できない値で epoch キーを作ると、誰も参照しないキーが増えるだけになる\n if (!normalized) continue\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalized}`)\n }\n }\n\n /**\n * Clears all server-side SQL result and user ID caches.\n * サーバー側のすべての SQL 結果キャッシュおよびユーザー ID キャッシュを全消去する。\n *\n * Redis で進める epoch は一覧 ID のもののみ (プロセス内キャッシュは全系統消える)。\n * 特定 hub / 営業日の epoch まで狙い撃ちで進めたい場合は `invalidateHubReadCaches` を使うこと。\n */\n const clearCache = async (): Promise<void> => {\n sqlResultCache.clearAll()\n userIdCache.clear()\n cachedDraftLabelIds = null\n if (epochs) {\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n }\n logger.info(\"[DailyReportService] Server-side DB/SQL caches cleared successfully.\")\n }\n\n return {\n // 定数 (アプリ側の互換 export 用)\n streamKey: DAILY_REPORT_SSE_STREAM_KEY,\n streamMaxLen: DAILY_REPORT_SSE_STREAM_MAXLEN,\n // キャッシュクリア\n clearCache,\n invalidateHubReadCaches,\n // ユーザー解決\n getUserIdByExternalId,\n // 参照系\n getDailyReportIdsByExternalId,\n getDailyReportIdsFirstPageByExternalId,\n getDailyReportsByBusinessDate,\n getDailyReportsByBusinessDateByExternalId,\n getDailyReportDetailById,\n getDailyReportDetailByIdByExternalId,\n getDraftLabelId,\n // 添付 (ハンドラーが認可判定と実体消失記録に使う)\n getAttachmentsByHubIds,\n getAttachmentForUser,\n markAttachmentMissing,\n markAttachmentPresent,\n // 更新系\n setStarStatus,\n setReadStatus,\n addComment,\n deleteComment,\n createDailyReport,\n updateDailyReport,\n publishDailyReport,\n deleteDailyReport,\n // リポジトリヘルパー。現時点で消費側の呼び出し元は無く、公開面としてのみ維持している\n // (「統合テストから直接利用されている」という以前の記述は誤り。実測で 0 件)。\n // ❗ どれも**コメント次元を適用しない**素のリポジトリ操作である。詳細を組み立てる用途へ\n // 流用すると mapHubRecord の射影を迂回してコメント本文が出るため、読み取り経路の\n // 素材として使わないこと\n findDailyReportHubById,\n updateDailyReportHub,\n updateDailyReportInternal,\n deleteDailyReportLabel,\n }\n}\n","/**\n * Authz helper utilities for @aiquants/daily-report: standard resource definitions & auto-seeding.\n * @aiquants/daily-report 用の認可ヘルパー。標準リソース定義および自動シード機能を提供。\n */\nimport { and, eq } from \"drizzle-orm\"\n\nexport type AuthzResourceItem = {\n resourceKey: string\n name: string\n description?: string | null\n}\n\nexport type DailyReportSourceTypeInput = {\n key: string\n name: string\n description?: string | null\n includeCommentResource?: boolean\n}\n\n/**\n * Dynamically builds neutral Authz resource definitions from provided source type inputs.\n * 指定されたソース種別定義から中立な認可リソース定義リストを生成するファクトリ。\n */\nexport function defineDailyReportAuthzResources(sources?: DailyReportSourceTypeInput[]): AuthzResourceItem[] {\n const list: AuthzResourceItem[] = []\n const targetSources =\n sources && sources.length > 0\n ? sources\n : [\n { key: \"internal\", name: \"Internal\", description: \"Internal daily reports\" },\n { key: \"external\", name: \"External\", description: \"External daily reports\" },\n ]\n\n for (const src of targetSources) {\n const keyLower = src.key.toLowerCase().replace(/[^a-z0-9_]/g, \"_\")\n const resourceKey = `daily_report_${keyLower}`\n list.push({\n resourceKey,\n name: src.name,\n description: src.description ?? `${src.name} daily report access`,\n })\n\n if (src.includeCommentResource !== false) {\n list.push({\n resourceKey: `${resourceKey}_comment`,\n name: `${src.name} Comment`,\n description: `${src.name} daily report comment access`,\n })\n }\n }\n\n return list\n}\n\ntype DbSelectChain = {\n from(t: unknown): DbSelectChain\n where(cond: unknown): PromiseLike<unknown[]>\n}\ntype DbInsertChain = {\n values(v: unknown): PromiseLike<unknown>\n}\ntype LooseDb = {\n select(fields?: unknown): DbSelectChain\n insert(t: unknown): DbInsertChain\n}\n\n/**\n * Ensures all daily-report standard resources exist in the authz TMResource table idempotently.\n * authz データベース内に日報機能の標準認可リソースが存在することを自動保証(冪等シード)する処理。\n */\nexport async function seedDailyReportAuthzResources(db: unknown, authzTables: { TMResource: unknown }, opts: { appKey: string; actor?: string; sources?: DailyReportSourceTypeInput[]; resources?: AuthzResourceItem[] }): Promise<void> {\n const d = db as LooseDb\n const TMResource = authzTables.TMResource as Record<string, unknown>\n const appKey = opts.appKey\n const actor = opts.actor ?? \"system:daily-report\"\n const now = new Date()\n\n const resourceList = opts.resources ?? defineDailyReportAuthzResources(opts.sources)\n\n for (const res of resourceList) {\n const existing = await d\n .select()\n .from(TMResource)\n .where(and(eq(TMResource.appKey as never, appKey as never), eq(TMResource.resourceKey as never, res.resourceKey as never)))\n\n if (!existing || existing.length === 0) {\n await d.insert(TMResource).values({\n appKey,\n resourceKey: res.resourceKey,\n name: res.name,\n description: res.description ?? null,\n createdAt: now,\n createdBy: actor,\n updatedAt: now,\n updatedBy: actor,\n })\n }\n }\n}\n","/**\n * External source adapter contract for legacy/foreign daily-report tables.\n * レガシー・外部由来の日報テーブルを取り込む外部ソースアダプタ契約。\n *\n * DailyReportHub.source_type がアダプタの `sourceType` に一致する行は、\n * `table` を `Hub.source_id_num = idColumn` で LEFT JOIN し、その行を\n * `mapRow` で表示フィールドへ変換する (例: 別システムのレガシー日報テーブルの取り込み)。\n */\nimport type { AnyMsSqlColumn } from \"drizzle-orm/mssql-core\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport type { DailyReportExternalComment, DailyReportInterviewer } from \"../shared/types\"\n\n/** 外部ソース行から詳細表示へ供給するフィールド群。 */\nexport type ExternalReportFields = {\n content?: string | null\n employeeName?: string | null\n category?: string | null\n creationCategory?: string | null\n visitTimeFrom?: string | null\n visitTimeTo?: string | null\n customerName?: string | null\n interviewers?: DailyReportInterviewer[]\n externalComments?: DailyReportExternalComment[]\n}\n\n/** 外部ソースアダプタ。 */\nexport type DailyReportExternalSource = {\n /** DailyReportHub.source_type の一致値 (例 \"legacy\")。 */\n sourceType: string\n /** LEFT JOIN する drizzle テーブル。 */\n table: unknown\n /** Hub.source_id_num と突き合わせる ID 列。 */\n idColumn: AnyMsSqlColumn\n /** 結合行を表示フィールドへ変換する処理。 */\n mapRow: (row: Record<string, unknown>) => ExternalReportFields\n}\n\nconst defaultLogger = createLogger(LogLevel.INFO, \"[DailyReportExternalSource]\")\n\n/**\n * Parses JSON array payloads and maps each element (fail-soft: returns [] on error).\n * JSON 配列のペイロードを解析し各要素を変換する処理 (エラー時は空配列)。\n */\nexport const transformJsonArray = <TRaw, TResult>(payload: string | null, label: string, mapper: (raw: TRaw) => TResult | null, logger: DailyReportLogger = defaultLogger): TResult[] => {\n if (!payload) {\n return []\n }\n\n try {\n const parsed = JSON.parse(payload)\n if (!Array.isArray(parsed)) {\n logger.warn(`Unexpected ${label} format: not an array`)\n return []\n }\n return (parsed as TRaw[]).map(mapper).filter((entry): entry is TResult => entry !== null)\n } catch (error) {\n logger.warn(`Failed to parse ${label}:`, error)\n return []\n }\n}\n","/**\n * Structural table types + drizzle (mssql) schema factory for the daily-report tables.\n * 日報テーブル群の構造的テーブル型と drizzle (mssql) スキーマファクトリ。\n *\n * 既存アプリは自前のモデル定義をそのまま `DailyReportTables` として注入できる (構造互換)。\n * 新規プロジェクトは `defineDailyReportSchema()` で同一構造のテーブル定義を生成できる。\n */\nimport { desc, sql } from \"drizzle-orm\"\nimport { type AnyMsSqlColumn, bigint, bit, date, datetime2, foreignKey, index, int, mssqlSchema, nvarchar, primaryKey, uniqueIndex } from \"drizzle-orm/mssql-core\"\n\n/** 注入する外部ユーザーテーブルの最小形 (id / display_name)。 */\nexport type DailyReportUserTable = { id: AnyMsSqlColumn; displayName: AnyMsSqlColumn }\n\n/** DailyReportHub テーブルの構造形。 */\nexport type DailyReportHubTable = {\n id: AnyMsSqlColumn\n sourceType: AnyMsSqlColumn\n sourceId: AnyMsSqlColumn\n sourceIdNum: AnyMsSqlColumn\n businessDate: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n title: AnyMsSqlColumn\n summary: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n deletedAt: AnyMsSqlColumn\n deletedBy: AnyMsSqlColumn\n}\n\n/** DailyReportInternal テーブルの構造形。 */\nexport type DailyReportInternalTable = {\n hubId: AnyMsSqlColumn\n body: AnyMsSqlColumn\n metadata: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/** 投稿コメントテーブル (物理名 DailyReportComment) の構造形。 */\nexport type DailyReportCommentTable = {\n id: AnyMsSqlColumn\n hubId: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n body: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/** DailyReportLabel テーブルの構造形。 */\nexport type DailyReportLabelTable = {\n id: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n name: AnyMsSqlColumn\n color: AnyMsSqlColumn\n sortOrder: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/** DailyReportHub_Label 中間テーブルの構造形。 */\nexport type DailyReportHubLabelTable = {\n hubId: AnyMsSqlColumn\n labelId: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n}\n\n/** DailyReportUserStatus テーブルの構造形。 */\nexport type DailyReportUserStatusTable = {\n hubId: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n isRead: AnyMsSqlColumn\n isStarred: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/**\n * DailyReportAttachment テーブルの構造形。\n *\n * `uploadedBy` は外部取込ソース由来の行に投稿者が存在しないため NULL 許容。\n * `sourceKey` は取込の冪等 upsert の自然キー。\n * `objectCheckedAt` / `objectMissingAt` から実体存在の三値 (unknown / present / absent) を導出する。\n */\nexport type DailyReportAttachmentTable = {\n id: AnyMsSqlColumn\n hubId: AnyMsSqlColumn\n uploadedBy: AnyMsSqlColumn\n sourceKey: AnyMsSqlColumn\n fileName: AnyMsSqlColumn\n filePath: AnyMsSqlColumn\n fileType: AnyMsSqlColumn\n fileSize: AnyMsSqlColumn\n objectCheckedAt: AnyMsSqlColumn\n objectMissingAt: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/**\n * サービスへ注入するテーブル一式 (アプリ既存モデルまたは本ファクトリ生成物)。\n *\n * `attachment` のみ任意。添付テーブルを持たない消費アプリでも本パッケージを使えるようにするため。\n * 未注入時は詳細の `attachments` が常に空配列になり、添付エンドポイントは全トークンで 404 を返す\n * (意図した縮退動作)。\n */\nexport type DailyReportTables = {\n hub: DailyReportHubTable\n internal: DailyReportInternalTable\n comment: DailyReportCommentTable\n attachment?: DailyReportAttachmentTable\n label: DailyReportLabelTable\n hubLabel: DailyReportHubLabelTable\n userStatus: DailyReportUserStatusTable\n}\n\n/**\n * Generic factory: build the seven daily-report tables under an arbitrary schema name.\n * 任意のスキーマ名の下に日報 7 テーブルを生成する汎用ファクトリ。\n *\n * 制約・索引名は `${schemaName}_<Table>_...` 規約で生成する。\n * ユーザーテーブルへの FK は注入された `userTable` を参照する。\n */\nexport function defineDailyReportSchema<S extends string>(schemaName: S, opts: { userTable: DailyReportUserTable }) {\n const s = mssqlSchema(schemaName)\n const users = opts.userTable\n\n const hub = s.table(\n \"DailyReportHub\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n sourceType: nvarchar(\"source_type\", { length: 20 }).notNull(),\n sourceId: nvarchar(\"source_id\", { length: 100 }).notNull(),\n sourceIdNum: bigint(\"source_id_num\", { mode: \"number\" }).generatedAlwaysAs(sql`TRY_CAST(source_id AS BIGINT)`),\n businessDate: date(\"business_date\"),\n userId: bigint(\"user_id\", { mode: \"number\" }),\n title: nvarchar(\"title\", { length: 200 }),\n summary: nvarchar(\"summary\", { length: \"max\" }),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n deletedAt: datetime2(\"deleted_at\"),\n deletedBy: nvarchar(\"deleted_by\", { length: 50 }),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportHub_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportHub_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n index(`${schemaName}_DailyReportHub_business_date_index`).on(table.businessDate),\n index(`${schemaName}_DailyReportHub_updated_at_index`).on(table.updatedAt),\n index(`${schemaName}_DailyReportHub_source_index`).on(table.sourceType, table.sourceId),\n index(`${schemaName}_DailyReportHub_user_id_index`).on(table.userId),\n index(`${schemaName}_DailyReportHub_business_date_id_index`).on(desc(table.businessDate), desc(table.id)),\n index(`${schemaName}_DailyReportHub_deleted_at_index`).on(table.deletedAt),\n ],\n )\n\n const internal = s.table(\n \"DailyReportInternal\",\n {\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n body: nvarchar(\"body\", { length: \"max\" }),\n metadata: nvarchar(\"metadata\", { length: \"max\" }),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportInternal_pk`, columns: [table.hubId] }),\n foreignKey({\n name: `${schemaName}_DailyReportInternal_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n ],\n )\n\n const comment = s.table(\n // ❗ 物理テーブル名。TS 側の型名改名 (DailyReportPostedComment 等) に**追従させない** —\n // これは DB の識別子であり、変更にはマイグレーションが要る。v0.17.0 の機械改名が\n // ここを巻き込み、実 DB で全コメントクエリが Msg 208 になる事故が実際に起きた\n \"DailyReportComment\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n userId: bigint(\"user_id\", { mode: \"number\" }).notNull(),\n body: nvarchar(\"body\", { length: \"max\" }).notNull(),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportComment_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportComment_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportComment_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n index(`${schemaName}_DailyReportComment_hub_id_index`).on(table.hubId),\n ],\n )\n\n const label = s.table(\n \"DailyReportLabel\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n userId: bigint(\"user_id\", { mode: \"number\" }),\n name: nvarchar(\"name\", { length: 50 }).notNull(),\n color: nvarchar(\"color\", { length: 20 }),\n sortOrder: int(\"sort_order\"),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportLabel_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportLabel_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n ],\n )\n\n const hubLabel = s.table(\n \"DailyReportHub_Label\",\n {\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n labelId: bigint(\"label_id\", { mode: \"number\" }).notNull(),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportHub_Label_pk`, columns: [table.hubId, table.labelId] }),\n foreignKey({\n name: `${schemaName}_DailyReportHub_Label_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportHub_Label_label_id_fk`,\n columns: [table.labelId],\n foreignColumns: [label.id],\n }),\n ],\n )\n\n const userStatus = s.table(\n \"DailyReportUserStatus\",\n {\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n userId: bigint(\"user_id\", { mode: \"number\" }).notNull(),\n isRead: bit(\"is_read\").default(false).notNull(),\n isStarred: bit(\"is_starred\").default(false).notNull(),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportUserStatus_pk`, columns: [table.hubId, table.userId] }),\n foreignKey({\n name: `${schemaName}_DailyReportUserStatus_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportUserStatus_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n ],\n )\n\n const attachment = s.table(\n \"DailyReportAttachment\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n // 外部取込ソース由来の行には投稿者が存在しないため NULL 許容\n uploadedBy: bigint(\"uploaded_by\", { mode: \"number\" }),\n // 取込の冪等 upsert の自然キー。`<source>:<external-id>` 形式を推奨 (値の規約は消費アプリが決める)\n sourceKey: nvarchar(\"source_key\", { length: 200 }).notNull(),\n fileName: nvarchar(\"file_name\", { length: 255 }).notNull(),\n // ストレージ URI。受け入れるのは `gcs://` のみ (取込側で表明・読み取り側で再検証)\n filePath: nvarchar(\"file_path\", { length: \"max\" }).notNull(),\n fileType: nvarchar(\"file_type\", { length: 100 }),\n fileSize: bigint(\"file_size\", { mode: \"number\" }),\n // 実体存在の三値をこの 2 列から導出する:\n // objectCheckedAt が null -> unknown (未検証)\n // objectMissingAt が非 null -> absent (消失を確認)\n // 上記以外 -> present (実在を確認)\n objectCheckedAt: datetime2(\"object_checked_at\"),\n objectMissingAt: datetime2(\"object_missing_at\"),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportAttachment_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportAttachment_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportAttachment_uploaded_by_fk`,\n columns: [table.uploadedBy],\n foreignColumns: [users.id],\n }),\n // 取込の冪等性を DB 側で担保する\n uniqueIndex(`${schemaName}_DailyReportAttachment_source_unique`).on(table.hubId, table.sourceKey),\n // 一括取得 (hub_id IN (...) ORDER BY created_at) を被覆する\n index(`${schemaName}_DailyReportAttachment_hub_id_index`).on(table.hubId, table.createdAt),\n ],\n )\n\n return { hub, internal, comment, attachment, label, hubLabel, userStatus } satisfies DailyReportTables\n}\n"],"mappings":"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","names":["server_exports","__export","DailyReportSseReader","DailyReportViewerScope","SqlResultCache","asciiFallbackFileName","buildVisibleSourceTypeCondition","computeSourceTypeDigest","computeViewerScopeDigest","createDailyReportHandlers","createDailyReportServer","createDailyReportService","createEpochStore","defineDailyReportAuthzResources","defineDailyReportSchema","deniesEverySourceType","encodeRfc8187","formatDateValue","generateETag","isSourceTypeVisible","isStreamIdLte","jsonResponseWithETag","normalizeVisibleSourceTypes","sanitizeMediaType","seedDailyReportAuthzResources","transformJsonArray","__toCommonJS","createEpochStore","redis","epochKey","client","val","SqlResultCache","config","epochStore","cacheKey","opts","ratio","bucket","ttl","promise","epoch","records","forceRefresh","snapshot","currentEpoch","r","shared","expireAt","result","prefix","key","now","ISO_DATE_PATTERN","SLASH_DATE_PATTERN","zeroPad","value","normalizeFromDate","year","month","day","normalizeBusinessDateKey","value","normalizeFromDate","trimmed","ISO_DATE_PATTERN","SLASH_DATE_PATTERN","parsed","computeIdsStreamSnapshotToken","items","laneA","laneB","item","id","date","i","code","compareIdsStreamOrder","a","b","isIdsStreamOrdered","findIdsStreamResumeIndex","items","cursor","index","item","compareIdsStreamOrder","createLogger","level","prefix","impl","format","message","rest","import_zod","dailyReportInterviewerSchema","dailyReportExternalCommentSchema","dailyReportLabelDefSchema","dailyReportPostedCommentSchema","dailyReportAttachmentItemSchema","dailyReportDetailSchema","connectedMessageSchema","statusUpdateMessageSchema","sseSourceTypeSchema","commentAddMessageSchema","commentDeleteMessageSchema","reportCreateMessageSchema","reportUpdateMessageSchema","reportPublishMessageSchema","reportDeleteMessageSchema","dailyReportSseMessageSchema","import_node_crypto","generateETag","data","json","defaultLogger","createLogger","jsonResponseWithETag","request","cookie","payload","status","logger","etag","generateETag","ifNoneMatch","headers","import_node_events","isStreamIdLte","a","b","aMs","aSeq","bMs","bSeq","DailyReportSseReader","config","createLogger","onEntry","onError","resolve","err","shared","latest","results","stream","msg","parsed","msgType","reportHubId","import_drizzle_orm","DIGEST_SEPARATOR","DIGEST_RAW_MAX_LENGTH","foldStoredSourceType","raw","foldResolverToken","normalizeVisibleSourceTypes","set","token","folded","isSourceTypeVisible","sourceType","deniesEverySourceType","buildVisibleSourceTypeCondition","sourceTypeColumn","_visible","_commentable","_DailyReportViewerScope","visible","commentable","__privateAdd","__privateSet","__privateGet","input","isDimensions","read","rawComment","comment","narrowed","auditReason","DailyReportViewerScope","computeViewerScopeDigest","scope","computeSourceTypeDigest","joined","hash","i","jsonData","payload","init","headers","IDS_STREAM_FIRST_CHUNK_SIZE","IDS_STREAM_NEXT_CHUNK_SIZE","IDS_STREAM_ERROR_CLIENT_MESSAGE","IDS_STREAM_DATE_PATTERN","IDS_STREAM_HEARTBEAT_MS","IDS_STREAM_HEARTBEAT","SSE_VISIBILITY_REFRESH_TICKS","IDS_STREAM_SNAPSHOT_PATTERN","parseIdsStreamCursor","url","rawId","rawDate","rawSnapshot","snapshot","reportHubId","streamLinesFromFullArray","items","resumeKey","firstChunkSize","options","position","findIdsStreamResumeIndex","isFirstYield","chunkItems","isLast","lastItem","buildIdsStreamResponse","cookie","lines","request","logger","encoder","cancelled","pendingNext","stream","controller","heartbeatTimer","heartbeatPromise","resolve","raced","value","done","error","errorLine","DEFAULT_ATTACHMENT_MAX_BYTES","DEFAULT_ATTACHMENT_RATE_LIMIT_PER_MINUTE","DEFAULT_ATTACHMENT_CONCURRENCY","ATTACHMENT_BODY_FLUSH_TIMEOUT_MS","RATE_LIMITER_MAX_ENTRIES","RATE_LIMIT_RETRY_AFTER_SECONDS","CONCURRENCY_RETRY_AFTER_SECONDS","INLINE_SAFE_MEDIA_TYPES","HEADER_SAFE_PATTERN","MEDIA_TYPE_PATTERN","sanitizeMediaType","encodeRfc8187","name","c","asciiFallbackFileName","attachmentFailureToStatus","reason","ATTACHMENT_FAILURE_MESSAGES","createRateLimiter","limitPerMinute","buckets","userId","nowMs","existing","bucket","elapsedMs","allowed","oldest","createConcurrencyGate","limit","active","streamAndRelease","bytes","release","timeoutMs","timer","finish","sent","isSseMessageSourceTypeVisible","parsed","raw","visible","commentable","rawSourceType","isSourceTypeVisible","COMMENT_FACE_KEYS","redactSseReportComments","report","target","key","createDailyReportHandlers","config","authenticate","service","encodeUserId","redis","sseReader","streamKey","loginRedirectPath","apiLogger","createLogger","visibilityLogger","sseLogger","attachmentLogger","attachmentMaxBytes","consumeAttachmentRateToken","attachmentGate","resolveViewerScope","port","DailyReportViewerScope","resolved","e","indexLoader","user","hashedUserId","endpointHandlers","scope","normalizedBusinessDate","normalizeBusinessDateKey","forceRefresh","jsonResponseWithETag","reports","pageQueryStarted","resolvePage","rejectPage","pagePromise","reject","fullArrayPromise","fullItems","computeIdsStreamSnapshotToken","resumable","lineIterator","winner","emittedProvisionalPage","pageItems","param","parsedId","detail","params","sanitizedCookie","endpoint","handler","startTime","elapsed","err","respond","actionStartTime","formData","intent","reportHubIdRaw","businessDate","operationTimestamp","clientTempId","runStatusToggle","intentName","fieldName","apply","updatedStatus","newReport","title","content","publishedReport","newComment","safeComment","commentId","sessionHeaders","extra","internalUserId","initialScope","visibleSet","commentableSet","keepAliveTick","resolveGeneration","lastEventId","controllerRef","keepAliveInterval","unsubscribe","isCleaningUp","cleanup","emitAnchorOnly","entryId","processEntry","fields","result","dailyReportSseMessageSchema","recipientRawUserId","redacted","sanitizedMessage","_","rest","generation","next","lastProcessedId","firstLiveEntryId","entry","isStreamIdLte","client","latest","connectedPayload","sseMessage","catchUpEntries","fail","status","message","retryAfterSeconds","codec","readAttachment","token","attachmentId","attachment","gateReleased","releaseGate","releaseDeferredToStream","isHead","declared","inlineSafe","wantsDownload","contentType","disposition","contentLength","import_drizzle_orm","cols","formatDateValue","value","pattern","dateValue","year","yyyy","mm","dd","hh","mi","ss","createDailyReportService","config","db","tables","users","resolveUserId","encodeUserId","redis","sqlResultCache","epochs","DailyReportHub","DailyReportInternal","DailyReportCommentModel","DailyReportLabel","DailyReportHub_Label","DailyReportUserStatus","externalSources","draftLabelNames","draftLabelName","logger","createLogger","DAILY_REPORT_IDS_CACHE_KEY","DAILY_REPORT_IDS_TTL_MS","DAILY_REPORT_IDS_EPOCH_KEY","DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX","DAILY_REPORT_BUSINESS_DATE_TTL_MS","DAILY_REPORT_DATE_EPOCH_PREFIX","DAILY_REPORT_DETAIL_EPOCH_PREFIX","DAILY_REPORT_SSE_STREAM_KEY","DAILY_REPORT_SSE_STREAM_MAXLEN","incrementRedisEpoch","key","visibleSourceTypeCondition","visible","buildVisibleSourceTypeCondition","idsCacheKey","userId","digest","idsUserPrefix","dateCacheKey","normalizedBusinessDate","dateUserPrefix","detailCacheKey","reportHubId","detailUserPrefix","publishToSseStream","message","callerName","client","publishStartMs","publishDurationMs","e","maskAuditActor","externalSelections","out","adapter","applyExternalJoins","chain","c","mapHubRecord","scope","row","currentUserId","hub","internal","content","interviewers","category","creationCategory","visitTimeFrom","visitTimeTo","customerName","employeeName","externalComments","a","externalRow","fields","labelsRaw","l","postedCommentsRaw","commentsVisible","isSourceTypeVisible","isCreationCategoryDraft","cachedPrimaryDraftLabelId","cachedDraftLabelIds","fetchDailyReportIdsByUserId","DraftLabelRelation","draftLabelJoinIds","getDraftLabelJoinIds","item","getDailyReportIdsFirstPageByExternalId","externalId","limit","getUserIdByExternalId","deniesEverySourceType","fetchDailyReportsByBusinessDate","rows","attachmentsMap","getAttachmentsByHubIds","r","buildBusinessDateCacheKey","userIdCache","getDailyReportIdsByExternalId","forceRefresh","snapshot","ttlMsOverride","onFetchStart","cacheKey","computeViewerScopeDigest","res","isIdsStreamOrdered","compareIdsStreamOrder","getDailyReportsByBusinessDate","businessDate","normalizeBusinessDateKey","getDraftLabelIds","ids","getDraftLabelId","primary","deriveAttachmentState","checkedAt","missingAt","hubIds","map","attachment","codec","list","getAttachmentForUser","attachmentId","markAttachmentMissing","now","error","markAttachmentPresent","getDailyReportsByBusinessDateByExternalId","allLabels","allComments","labelsMap","commentsMap","label","comment","hubId","getDailyReportDetailById","labels","comments","labelsMapped","commentsMapped","getDailyReportDetailByIdByExternalId","options","findHubGateColumns","assertHubMutable","setStarStatus","isStarred","clientTempId","existing","updatedStatus","normalizedDate","statusUpdateMessageSchema","setReadStatus","isRead","resolveCommentRecipient","ownerUserId","draftIds","rel","addComment","recipientRawUserId","inserted","user","userName","commentItem","commentAddMessageSchema","findDailyReportCommentById","tx","commentId","deleteComment","hubForDelete","commentDeleteMessageSchema","createDailyReport","result","sourceId","draftLabelId","fullDetail","DailyReportViewerScope","draftLabelIds","isDraft","reportCreateMessageSchema","findDailyReportHubById","updateDailyReportHub","data","updateDailyReportInternal","deleteDailyReportLabel","labelId","businessDates","normalized","report","editedBusinessDate","reportUpdateMessageSchema","reportPublishMessageSchema","reportDeleteMessageSchema","import_drizzle_orm","defineDailyReportAuthzResources","sources","list","targetSources","src","resourceKey","seedDailyReportAuthzResources","db","authzTables","opts","d","TMResource","appKey","actor","now","resourceList","res","existing","defaultLogger","createLogger","transformJsonArray","payload","label","mapper","logger","parsed","entry","error","import_drizzle_orm","import_mssql_core","defineDailyReportSchema","schemaName","opts","s","users","hub","table","internal","comment","label","hubLabel","userStatus","attachment","createDailyReportServer","config","epochs","createEpochStore","cache","SqlResultCache","service","createDailyReportService","sseReader","DailyReportSseReader","handlers","createDailyReportHandlers","source"]}
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/server/cache.ts","../src/shared/business-date.ts","../src/shared/ids-stream.ts","../src/shared/logger.ts","../src/shared/sse-schema.ts","../src/server/etag.ts","../src/server/response.ts","../src/server/sse-reader.ts","../src/server/visibility.ts","../src/server/handlers.ts","../src/server/service.ts","../src/server/authz.ts","../src/server/external-source.ts","../src/server/schema.ts"],"sourcesContent":["/**\n * Server entry of @aiquants/daily-report: schema factory, service, SSE reader, and handler factories.\n * @aiquants/daily-report の server エントリ。スキーマファクトリ・サービス・SSE リーダー・ハンドラ工場を公開。\n */\nimport { createEpochStore, SqlResultCache } from \"./server/cache\"\nimport type { DailyReportExternalSource } from \"./server/external-source\"\nimport { createDailyReportHandlers } from \"./server/handlers\"\nimport type { DailyReportAuthenticate, DailyReportReadAttachment, DailyReportResolveVisibleSourceTypes } from \"./server/ports\"\nimport { createDailyReportService, type DailyReportServiceConfig } from \"./server/service\"\nimport { DailyReportSseReader } from \"./server/sse-reader\"\n\nexport * from \"./server/authz\"\nexport * from \"./server/cache\"\nexport { generateETag } from \"./server/etag\"\nexport * from \"./server/external-source\"\nexport * from \"./server/handlers\"\nexport * from \"./server/ports\"\nexport * from \"./server/response\"\nexport * from \"./server/schema\"\nexport * from \"./server/service\"\nexport * from \"./server/sse-reader\"\nexport * from \"./server/visibility\"\n\n/** createDailyReportServer の設定 (サービス設定 + 認証ポート + ハンドラ設定)。 */\nexport type DailyReportServerConfig = Omit<DailyReportServiceConfig, \"cache\" | \"epochs\"> & {\n /** リクエスト認証ポート。 */\n authenticate: DailyReportAuthenticate\n /**\n * 可視 sourceType リゾルバ。未注入なら全件 (現行互換)。\n *\n * サーバー側の全データ経路 (一覧 / 詳細 / コメント / 添付 / SSE) に一貫して効く。\n * 戻り値は `DailyReportHub.sourceType` の値 (`knownSourceTypes` 参照) であり、認可リソースキーではない。\n * `undefined` / `null` は無制限、空配列は 0 件、例外は全拒否 (fail-close)。\n */\n resolveVisibleSourceTypes?: DailyReportResolveVisibleSourceTypes\n /** 添付ファイル読み取りポート。未注入なら添付エンドポイントは常に 404。 */\n readAttachment?: DailyReportReadAttachment\n /** 添付ファイルの最大バイト数 (既定 32 MiB)。サービス側のワイヤ上限より低く保つこと。 */\n attachmentMaxBytes?: number\n /** 添付エンドポイントの 1 分あたり呼び出し上限 (ユーザー単位・既定 60)。 */\n attachmentRateLimitPerMinute?: number\n /** 添付読み取りの同時実行上限 (プロセス単位・既定 4)。 */\n attachmentConcurrency?: number\n /** 未ログイン時のリダイレクト先 (index.loader 用、既定 \"/auth/login\")。 */\n loginRedirectPath?: string\n /**\n * `intent=clearCache` (全サーバーキャッシュの一括破棄) を許可するか。既定 false。\n * 開発用アフォーダンス。有効化すると認証済みなら誰でも全ワーカーのキャッシュを\n * 無制限破棄できるため、本番では無効のままにする (消費アプリは開発環境のみ true 注入)。\n */\n enableDevCacheClear?: boolean\n /** SQL 結果キャッシュの既定 TTL (既定 60,000ms)。 */\n cacheDefaultTtlMs?: number\n /** 外部ソースアダプタ群 (レガシー日報テーブル等)。 */\n externalSources?: DailyReportExternalSource[]\n}\n\n/**\n * One-stop factory wiring cache, service, SSE fan-out reader, and route handlers.\n * キャッシュ・サービス・SSE Fan-Out リーダー・ルートハンドラを一括結線するファクトリ。\n *\n * @example\n * const server = createDailyReportServer({ db, tables, userTable, resolveUserId, encodeUserId, authenticate, redis })\n * // routes:\n * // daily_report._index → server.index.loader (+ data() ラップ)\n * // daily_report.api.$endpoint → server.api.loader / server.api.action\n * // sse.daily_report.$endpoint → server.sse.loader\n */\nexport function createDailyReportServer(config: DailyReportServerConfig) {\n // redis 未注入は単一ワーカーなら正しく動く (プロセス内無効化は効く) が、\n // ワーカー横断の無効化と SSE 配信が黙って無効になる。強制はせず、気づける形にする\n if (!config.redis) {\n const logger = config.logger ?? console\n logger.warn(\"[DailyReport] redis provider is not injected: cross-worker cache invalidation is disabled (staleness is bounded only by TTL) and SSE publishing is skipped.\")\n }\n // epoch ストアと SQL 結果キャッシュ (サービスと SSE リーダーで同一インスタンスを共有する)\n const epochs = createEpochStore(config.redis)\n const cache = new SqlResultCache({ defaultTtlMs: config.cacheDefaultTtlMs ?? 60_000 }, epochs)\n\n const service = createDailyReportService({ ...config, cache, epochs })\n\n const sseReader = new DailyReportSseReader({\n redis: config.redis,\n streamKey: service.streamKey,\n cache,\n logger: config.logger,\n })\n\n // ❗ この呼び出しはスプレッドではなく明示列挙である。新しいポートをここへ足し忘れると\n // アプリが注入しても黙って捨てられ、添付エンドポイントは永久に 404 を返す。\n const handlers = createDailyReportHandlers({\n authenticate: config.authenticate,\n // ❗ 明示列挙。ここへ足し忘れると注入されたポートが黙って捨てられ、区分フィルタが\n // 一切効かないまま全件配信になる (認可バイパス)。server.spec.ts が配線を gate する\n resolveVisibleSourceTypes: config.resolveVisibleSourceTypes,\n service,\n encodeUserId: config.encodeUserId,\n attachmentIdCodec: config.attachmentIdCodec,\n readAttachment: config.readAttachment,\n attachmentMaxBytes: config.attachmentMaxBytes,\n attachmentRateLimitPerMinute: config.attachmentRateLimitPerMinute,\n attachmentConcurrency: config.attachmentConcurrency,\n redis: config.redis,\n sseReader,\n streamKey: service.streamKey,\n loginRedirectPath: config.loginRedirectPath,\n enableDevCacheClear: config.enableDevCacheClear,\n logger: config.logger,\n })\n\n return {\n /** データアクセスサービス (CRUD + キャッシュ + SSE publish)。 */\n service,\n /** SQL 結果キャッシュ (サービス・SSE リーダー共有インスタンス)。 */\n cache,\n /** クロスワーカー epoch ストア。 */\n epochs,\n /** SSE Fan-Out 共有リーダー。 */\n sseReader,\n /** SSE Redis Stream キー。 */\n streamKey: service.streamKey,\n /**\n * 既知の正準 sourceType 集合 (注入した `externalSources` の全 sourceType + `\"Internal\"`)。\n *\n * `resolveVisibleSourceTypes` が返すべき語彙そのものであり、消費側は起動時にこの集合と\n * 自前のマッピングを突き合わせて誤トークン (無診断で 0 件になる) を検出できる。\n */\n knownSourceTypes: [...new Set([\"Internal\", ...(config.externalSources ?? []).map((source) => source.sourceType)])] as readonly string[],\n ...handlers,\n }\n}\n","/**\n * In-memory SQL result cache with TTL, snapshot, and cross-worker epoch (via redis) support.\n * TTL・スナップショット・クロスワーカー epoch (redis 経由) を備えた SQL 結果のインメモリキャッシュ。\n *\n * epoch は任意注入の redis ポート経由で取得・更新する (未注入時は常に 0 = epoch 無効)。\n */\nimport type { DailyReportRedisProvider } from \"./ports\"\n\nexport type SqlResultCacheQueryOptions = {\n forceRefresh?: boolean\n snapshot?: boolean\n ttlMsOverride?: number\n epochKey?: string\n /**\n * ヒット時にバケット年齢が ttl × ratio を超えていたら、キャッシュ値を即返しつつ\n * 裏で 1 本だけ再フェッチして置き換える (未指定は従来動作 = 何もしない)。\n * 有効域は 0 <= ratio < 1: ヒット判定が「未失効」を要求するため年齢は常に ttl 未満であり、\n * ratio 1 は事実上発火しない。ratio 0 は毎ヒット裏更新 (undefined とは区別される)。\n * 定期的にアクセスされるキーの「TTL 切れ直後のコールド遭遇」を対話リクエストから排除する。\n */\n refreshAheadRatio?: number\n}\n\n/**\n * Creates epoch helpers bound to an optional redis provider.\n * 任意の redis プロバイダーに束縛された epoch ヘルパーを生成する処理。\n */\nexport const createEpochStore = (redis?: DailyReportRedisProvider) => ({\n /**\n * Gets the current epoch value, or null when it cannot be determined.\n * 現在のエポック値を取得する処理。判定不能なら null。\n *\n * - `null` = redis 未注入、または一時的な失敗。**エポックによる横断無効化は判定不能**。\n * - `0` = redis に到達したがキーが無い (一度も increment されていない正当な初期値)。\n *\n * ❗ `0` を「不明」の代用にしてはならない。記録側も `0` のとき両辺が一致してしまい、\n * stale 検出が黙って無効化される (無効化されたこと自体がどこにも現れない)。\n * 「常に 0 = safe 方向」が成り立つのは一時的失敗のときだけで、未注入時には偽である。\n */\n async getEpoch(epochKey: string): Promise<number | null> {\n try {\n const client = await redis?.getClient()\n if (!client) return null\n const val = await client.get(epochKey)\n return val ? Number(val) : 0\n } catch {\n return null\n }\n },\n /**\n * Increments the epoch counter in Redis.\n * Redis のエポックカウンターをインクリメントする。\n */\n async incrementEpoch(epochKey: string): Promise<void> {\n try {\n const client = await redis?.getClient()\n if (!client) return\n await client.incr(epochKey)\n } catch {\n // Redis エラー時は epoch 更新をスキップ (次回 GET は null = 判定不能 → TTL のみで判定)\n }\n },\n})\n\nexport type EpochStore = ReturnType<typeof createEpochStore>\n\n/**\n * In-memory cache for SQL query results with TTL and snapshot support.\n * TTLとスナップショット機能を備えたSQLクエリ結果のインメモリキャッシュ。\n */\nexport class SqlResultCache {\n // epoch は「バケット書き込み時点のエポック」。null = 当時エポックが判定不能だった\n private readonly buckets = new Map<string, { records: readonly unknown[]; expireAt: number; epoch: number | null }>()\n private readonly inFlight = new Map<string, Promise<readonly unknown[]>>()\n\n constructor(\n private readonly config: { defaultTtlMs: number },\n private readonly epochStore: EpochStore,\n ) {}\n\n /**\n * Invalidates a specific cache bucket.\n * 指定されたキャッシュバケットを無効化します。\n */\n invalidate = (cacheKey: string): void => {\n this.buckets.delete(cacheKey)\n this.inFlight.delete(cacheKey)\n }\n\n /**\n * Kicks a single background refetch when the bucket has aged past ttl * ratio.\n * バケット年齢が ttl × ratio を超えたヒット時に、裏で 1 本だけ再フェッチを起動する処理。\n *\n * 応答は誰も待たない。完了までは旧バケットがそのまま生きているため、並行するヒットは\n * 従来どおりキャッシュから即答する (バケットを先に消す forceRefresh 委譲はスタンピードに\n * なるため使わない)。バケットが有効な間は失敗しても旧バケットが残るだけ (safe 方向)。\n * 裏フェッチ中に TTL が完全に切れて in-flight へ相乗りした前面リクエストには、通常の\n * in-flight 共有と同じく失敗が伝播する (相乗りしなくても自前フェッチが同様に失敗する状況)。\n * in-flight が既に居れば起動しない (前面リクエストの結果で足りる)。\n */\n private maybeRefreshAhead = <T>(opts: { cacheKey: string; fetcher: () => Promise<readonly T[]> } & SqlResultCacheQueryOptions): void => {\n const ratio = opts.refreshAheadRatio\n if (ratio === undefined) {\n return\n }\n const { cacheKey } = opts\n // 年齢は呼び出し元のスナップショットではなく生きているバケットで測る。epoch 照合の\n // Redis await 中に進行中の裏フェッチが完了していると、古い expireAt のまま余分な\n // 2 本目を起動してしまう (置換直後のバケットは若く、再フェッチ不要)\n const bucket = this.buckets.get(cacheKey)\n if (!bucket) {\n return\n }\n const ttl = opts.ttlMsOverride ?? this.config.defaultTtlMs\n const age = ttl - (bucket.expireAt - Date.now())\n if (age < ttl * ratio) {\n return\n }\n if (this.inFlight.has(cacheKey)) {\n return\n }\n\n // 旧バケットを残したまま裏で取得し、完了時に差し替える。無効化レースの規約は\n // 前面経路と同一: 進行中に invalidate されたら (inFlight の同一性が崩れたら) 書かない\n let promise!: Promise<readonly T[]>\n promise = (async () => {\n // ❗ epoch はフェッチ「開始前」に採取する。完了後に採取すると、フェッチ中に他ワーカーが\n // 更新 (incrementEpoch) した後の値で旧データに新 epoch を刻んでしまい、epoch 照合\n // (staleness の主防御線) が丸ごと無効化される。開始前採取なら不一致 → 再フェッチで自己回復\n // epochKey 未指定は「エポック無し」— 0 (正当な初期値) ではなく null で表す\n const epoch = opts.epochKey ? await this.epochStore.getEpoch(opts.epochKey) : null\n const records = await opts.fetcher()\n // ガード判定と書き込みの間に await を挟まない (この間に落ちた invalidate の取りこぼし防止)\n if (this.inFlight.get(cacheKey) === promise) {\n this.buckets.set(cacheKey, { records, expireAt: Date.now() + ttl, epoch })\n }\n return records\n })()\n // 登録することで、裏フェッチ中に TTL 切れした前面リクエストは相乗りできる\n this.inFlight.set(cacheKey, promise as Promise<readonly unknown[]>)\n promise\n .catch(() => {})\n .finally(() => {\n // 無効化で消されたり別の登録に置き換わっていたら触らない\n if (this.inFlight.get(cacheKey) === promise) {\n this.inFlight.delete(cacheKey)\n }\n })\n }\n\n /**\n * Retrieves records from cache or fetches them if expired/missing.\n * キャッシュからレコードを取得するか、期限切れや未存在の場合はフェッチします。\n */\n getOrFetch = async <T>(\n opts: {\n cacheKey: string\n fetcher: () => Promise<readonly T[]>\n /**\n * 本呼び出しが実際にフェッチャーを起動した瞬間に 1 度だけ呼ばれる。\n *\n * 「返す配列がこの呼び出し自身の新規フェッチ結果である」ことの**因果的な証明**として使う。\n * バケットヒット・進行中フェッチへの相乗り・refresh-ahead の裏側更新では**呼ばれない** —\n * それらの経路が返す配列は本呼び出しより古くなりうるためである。\n *\n * ids ストリームはこの合図で高速先頭ページのクエリを起動する。合図の後に始まる以上、\n * 先頭ページは全量配列より古くなり得ず、「先頭ページに在って全量に無い行 = 削除済み」\n * という判定が初めて健全になる (逆順を許すと作成直後の日報を削除してしまう)。\n */\n onFetchStart?: () => void\n } & SqlResultCacheQueryOptions,\n ): Promise<readonly T[]> => {\n const { cacheKey, forceRefresh, snapshot } = opts\n const bucket = this.buckets.get(cacheKey)\n\n // 強制リフレッシュ時はキャッシュと進行中のリクエストをクリア\n if (forceRefresh) {\n this.buckets.delete(cacheKey)\n this.inFlight.delete(cacheKey)\n }\n\n // 有効なキャッシュがあれば返却 (epoch check 込み)\n if (!forceRefresh && bucket && bucket.expireAt > Date.now()) {\n if (opts.epochKey) {\n // epochKey 指定時は Redis の epoch と比較して stale を検出する。三値の行列:\n // 現在 null (未注入/一時失敗) → TTL のみで受理 (エポックは鮮度の加速器であって\n // 認可境界ではない。障害中に毎リクエスト再フェッチで DB を叩き続けない)\n // バケット null + 現在 数値 → stale (信号復帰後、バケットの世代は証明できない。\n // null === 数値 が偽なので下の等値比較が自然にこの行を与える)\n // 両方 数値 → 等値比較 (従来どおり)\n // ❗ 型を number | null に広げても分岐は強制されない (=== は union をそのまま受ける)。\n // この行列はテストだけが守っている — 特に「バケット数値 + 現在 null で受理」は\n // 明示節が無いと偽になり、redis 一時障害中のチャーンが再発する\n const currentEpoch = await this.epochStore.getEpoch(opts.epochKey)\n if (currentEpoch === null || bucket.epoch === currentEpoch) {\n this.maybeRefreshAhead(opts)\n return snapshot ? (bucket.records.map((r) => structuredClone(r)) as T[]) : (bucket.records as T[])\n }\n // epoch 不一致 → stale bucket を除去して re-fetch へ\n this.buckets.delete(cacheKey)\n } else {\n this.maybeRefreshAhead(opts)\n return snapshot ? (bucket.records.map((r) => structuredClone(r)) as T[]) : (bucket.records as T[])\n }\n }\n\n // 進行中のリクエストがあれば相乗り\n if (!forceRefresh && this.inFlight.has(cacheKey)) {\n const shared = (await this.inFlight.get(cacheKey)) as readonly T[]\n return snapshot ? shared.map((r) => structuredClone(r)) : shared\n }\n\n // 新規フェッチとキャッシュ更新\n // 自己参照 (this.inFlight.get(cacheKey) === promise) のため、定義前参照を回避する確定代入アサーション\n let promise!: Promise<readonly T[]>\n promise = (async () => {\n // ❗ epoch はフェッチ「開始前」に採取する (理由は maybeRefreshAhead と同一:\n // フェッチ中の他ワーカー更新を新 epoch で偽装しない。ガードと書き込みも同期隣接に保つ)\n // epochKey 未指定は「エポック無し」— 0 (正当な初期値) ではなく null で表す\n const epoch = opts.epochKey ? await this.epochStore.getEpoch(opts.epochKey) : null\n // ❗ フェッチャー起動の直前に合図する。これより後に始まる処理は、この配列より\n // 新しいスナップショットを見ることが保証される (順序の因果を呼び出し側へ渡す)。\n // refresh-ahead の裏側更新には**置かない** — 返す配列は古いバケットのままなので\n // 合図すると「新しい配列を得た」という嘘になる\n opts.onFetchStart?.()\n const records = await opts.fetcher()\n // invalidation 中に完了したリクエストはキャッシュに書き込まない(レース防止)\n // ただし forceRefresh の場合は常に書き込む\n if (forceRefresh || this.inFlight.get(cacheKey) === promise) {\n const expireAt = Date.now() + (opts.ttlMsOverride ?? this.config.defaultTtlMs)\n this.buckets.set(cacheKey, { records, expireAt, epoch })\n // 蓄積防止のため、サイズが一定値を超えたら期限切れキャッシュを一括クリーンアップ\n if (this.buckets.size > 500) {\n this.cleanExpired()\n }\n }\n return records\n })()\n\n if (!forceRefresh) this.inFlight.set(cacheKey, promise as Promise<readonly unknown[]>)\n\n try {\n const result = await promise\n return snapshot ? result.map((r) => structuredClone(r)) : result\n } finally {\n // ❗ identity チェック必須: 無効化が本リクエストの登録を消した後に後続リクエストが\n // 登録し直していた場合、無条件 delete は「後続」の書き込みガードまで壊してしまう\n // (2 本のフェッチが完走したのにバケットが空、という毒殺の温床)\n if (this.inFlight.get(cacheKey) === promise) {\n this.inFlight.delete(cacheKey)\n }\n }\n }\n\n /**\n * Clears the cache for a specific key.\n * 指定されたキーのキャッシュをクリアします。\n */\n flush = (cacheKey: string): void => {\n this.buckets.delete(cacheKey)\n this.inFlight.delete(cacheKey)\n }\n\n /**\n * Clears all cached buckets and in-flight requests.\n * すべてのキャッシュバケットと進行中のリクエストを全クリアします。\n */\n clearAll = (): void => {\n this.buckets.clear()\n this.inFlight.clear()\n }\n\n /**\n * Invalidates cache buckets matching a prefix.\n * 指定されたプレフィックスに一致するキャッシュバケットを無効化します。\n */\n invalidatePrefix = (prefix: string): void => {\n for (const key of this.buckets.keys()) {\n if (key.startsWith(prefix)) {\n this.buckets.delete(key)\n }\n }\n for (const key of this.inFlight.keys()) {\n if (key.startsWith(prefix)) {\n this.inFlight.delete(key)\n }\n }\n }\n\n /**\n * Cleans up all expired cache buckets to prevent memory accumulation.\n * メモリー蓄積を防ぐため、期限切れのキャッシュバケットをすべてクリーンアップする処理。\n */\n private cleanExpired(): void {\n const now = Date.now()\n // 期限切れのキーを削除\n for (const [key, bucket] of this.buckets.entries()) {\n if (bucket.expireAt <= now) {\n this.buckets.delete(key)\n }\n }\n }\n}\n","/**\n * Business date normalization and formatting helper functions.\n * 営業日の正規化とフォーマットを支援するユーティリティ群。\n */\n\ntype BusinessDateInput = string | Date | null | undefined\n\nconst ISO_DATE_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\nconst SLASH_DATE_PATTERN = /^\\d{4}\\/\\d{2}\\/\\d{2}$/\n\nconst zeroPad = (value: number): string => {\n return value < 10 ? `0${value}` : `${value}`\n}\n\nconst normalizeFromDate = (value: Date): string | null => {\n if (Number.isNaN(value.getTime())) {\n return null\n }\n\n const year = value.getFullYear()\n const month = zeroPad(value.getMonth() + 1)\n const day = zeroPad(value.getDate())\n return `${year}-${month}-${day}`\n}\n\n/**\n * Returns today's business-date key (YYYY-MM-DD) in the local calendar.\n * ローカルカレンダー上の「本日」の営業日キー (YYYY-MM-DD) を返す。\n *\n * `new Date().toISOString()` の日付部は UTC 日付になり、UTC より東のタイムゾーン\n * (JST など) では深夜〜午前帯に前日へずれる。営業日はブラウザのローカル暦日で判定する\n * ため、ローカル getter から直接組む。タイムゾーンはパッケージに固定しない (消費側の\n * 実行環境が決める) 方針に沿う。\n *\n * @param now Clock injection for tests (defaults to the current time). テスト用の時刻注入 (既定は現在時刻)。\n * @returns Local calendar day as YYYY-MM-DD. ローカル暦日の YYYY-MM-DD。\n */\nexport const resolveTodayBusinessDateKey = (now: Date = new Date()): string => {\n if (Number.isNaN(now.getTime())) {\n // 不正な Date が注入されたら現在時刻で解決する (空の営業日キーを返さない)\n return resolveTodayBusinessDateKey(new Date())\n }\n return `${now.getFullYear()}-${zeroPad(now.getMonth() + 1)}-${zeroPad(now.getDate())}`\n}\n\n/**\n * Normalizes business date input into YYYY-MM-DD format string.\n * 営業日を YYYY-MM-DD 形式の文字列へ正規化する。\n */\nexport const normalizeBusinessDateKey = (value: BusinessDateInput): string | null => {\n if (value === null || value === undefined) {\n return null\n }\n\n if (value instanceof Date) {\n return normalizeFromDate(value)\n }\n\n const trimmed = value.trim()\n if (trimmed === \"\") {\n return null\n }\n\n if (ISO_DATE_PATTERN.test(trimmed)) {\n return trimmed\n }\n\n if (SLASH_DATE_PATTERN.test(trimmed)) {\n return trimmed.replaceAll(\"/\", \"-\")\n }\n\n const parsed = new Date(trimmed)\n return normalizeFromDate(parsed)\n}\n\n/**\n * Converts normalized business date into display format YYYY-MM-DD.\n * 正規化済みの営業日を YYYY-MM-DD 形式の表示文字列へ変換する。\n */\nexport const formatBusinessDateDisplay = (value: BusinessDateInput): string | null => {\n const normalized = normalizeBusinessDateKey(value)\n if (!normalized) {\n return null\n }\n return normalized\n}\n\n/**\n * Parses normalized business date into Date object.\n * 正規化した営業日を Date オブジェクトへ変換する。\n */\nexport const parseBusinessDateKeyToDate = (value: BusinessDateInput): Date | null => {\n const normalized = normalizeBusinessDateKey(value)\n if (!normalized) {\n return null\n }\n\n const [year, month, day] = normalized.split(\"-\").map((part) => Number.parseInt(part, 10))\n if ([year, month, day].some((component) => Number.isNaN(component))) {\n return null\n }\n\n return new Date(year, month - 1, day)\n}\n","/**\n * Wire contract and ordering helpers for the daily-report ids NDJSON stream.\n * 日報 ID 一覧 NDJSON ストリームのワイヤ契約と並び順ヘルパー。\n *\n * サーバーは `GET {apiBasePath}/ids-stream` で 1 行 1 JSON の NDJSON を返す。\n * 行は原則 {@link DailyReportIdsStreamChunkLine} で、送出中に予期しない失敗が\n * 起きた場合のみ {@link DailyReportIdsStreamErrorLine} を 1 行流して正常クローズする\n * (クライアントは「isLast 無しで終端」を再開シグナルとして扱う)。\n *\n * 並び順は SQL の `ORDER BY business_date DESC, id DESC` (MSSQL の DESC は NULL を\n * 末尾に置く) と完全一致させる。カーソル再開はこの全順序に対する「厳密に後」の\n * 位置からのスライスとして定義する。\n */\nimport type { DailyReportItem } from \"./types\"\n\n/** ストリーム再開カーソル。直前チャンク末尾アイテムの並び順キー。 */\nexport type DailyReportIdsStreamCursor = {\n /** 営業日 (YYYY-MM-DD)。null は「営業日なし領域」(並び順の末尾) を表す。 */\n businessDate: string | null\n /** 日報 Hub ID。 */\n reportHubId: number\n}\n\n/** データを運ぶ NDJSON 行。 */\nexport type DailyReportIdsStreamChunkLine = {\n /**\n * 行種別。判別可能ユニオンの識別子。\n *\n * ❗ 行の意味は `kind` が宣言する。「`items` を持つかどうか」で判定していた時代は、\n * 新しい行種別を足すと既存クライアントがそれをエラー行と解釈して走査を中断した。\n * `kind` を欠く行は本識別子の導入前の送信元とみなし、分類器が従来判定へ落とす。\n */\n kind: \"chunk\"\n /** このチャンクのアイテム群 (並び順どおり)。 */\n items: DailyReportItem[]\n /** 次チャンクの再開カーソル。最終チャンクでは null。 */\n nextCursor: DailyReportIdsStreamCursor | null\n /** これが最終チャンクかどうか。 */\n isLast: boolean\n /** 全体件数。接続ごとの先頭行にのみ載る。 */\n total?: number\n /**\n * この行が**暫定** (未確認スナップショット) かどうか。既定 (未指定) は権威。\n *\n * 高速先頭ページは全量配列とは別のスナップショットなので、そのまま「観測した」と\n * 扱うと、送出後に消えた行が完走剪定を免れて幽霊行になる。暫定行はクライアント側で\n * 描画はするが観測集合へ刻まないため、権威パス (全量配列を先頭から流し直す) に\n * 現れなかった行が完走時に正確に落ちる。\n *\n * ❗ 暫定行には `nextCursor` を載せない (常に null)。未確認領域を指すカーソルで再開すると、\n * 三つ目のスナップショットへ継ぎ目が移るだけになる。\n */\n provisional?: boolean\n /**\n * この行を切り出した全量配列の同一性トークン ({@link computeIdsStreamSnapshotToken})。\n *\n * クライアントは再接続時にカーソルと**対で**送り返し、サーバーは現在の配列のトークンと\n * 照合して、同じ配列のときだけカーソルを信用する。別の配列にカーソルを当てると、\n * カーソルより前に入った行が丸ごと飛ぶ (再接続のたびの欠損)。\n *\n * ❗ resume 経路を含む**全ての権威行**に載せる。載せ忘れると最初の再開成功でクライアントの\n * トークンが失われ、2 度目以降の切断が全て全量再送へ退化する。\n * ❗ 暫定行と短絡行には載せない (全量配列を代表しないため、再開起点にしてはならない)。\n */\n snapshot?: string\n /**\n * 送られたカーソルを破棄して先頭から流し直したことの明示 (先頭行にのみ載る)。\n *\n * クライアントはこれを受けたらカーソル・トークン・観測集合・トゥームストーンを初期化する\n * (処理は行の吸収関数の内部で行われ、呼び出し側に順序の自由度は無い)。\n */\n restarted?: boolean\n}\n\n/** サーバー側の予期しない失敗を伝える NDJSON 行 (内部情報は含めない)。 */\nexport type DailyReportIdsStreamErrorLine = {\n /** 行種別。判別可能ユニオンの識別子。 */\n kind: \"error\"\n error: string\n}\n\n/** NDJSON 1 行のユニオン。 */\nexport type DailyReportIdsStreamLine = DailyReportIdsStreamChunkLine | DailyReportIdsStreamErrorLine\n\n/**\n * Classifies one parsed NDJSON line by its declared kind.\n * パース済みの NDJSON 行を宣言された種別で分類する処理。\n *\n * クライアントとテストはこの分類器**だけ**を使う。契約:\n *\n * - `\"chunk\"` / `\"error\"`: 宣言どおりに扱う。\n * - **未知の `kind` は `\"unknown\"`** — 将来のワイヤ拡張であり、エラー扱いすると\n * 走査が中断する (旧判定の罠が 1 段上で再発する)。呼び出し側は警告して読み飛ばす。\n * - `kind` を欠く行は本識別子の導入前の送信元 (配備入れ替わり窓)。従来判定\n * (`items` 配列の有無 → チャンク、`error` 文字列 → エラー) へ落とす。\n *\n * @param line Parsed NDJSON line. パース済みの NDJSON 行。\n * @returns Line classification. 行の分類。\n */\nexport const classifyIdsStreamLine = (line: unknown): \"chunk\" | \"error\" | \"unknown\" => {\n if (typeof line !== \"object\" || line === null) {\n return \"unknown\"\n }\n const record = line as { kind?: unknown; items?: unknown; error?: unknown }\n if (typeof record.kind === \"string\") {\n if (record.kind === \"chunk\") return \"chunk\"\n if (record.kind === \"error\") return \"error\"\n return \"unknown\"\n }\n if (Array.isArray(record.items)) return \"chunk\"\n if (typeof record.error === \"string\") return \"error\"\n return \"unknown\"\n}\n\n/**\n * Type guard for chunk lines (thin wrapper over the classifier).\n * チャンク行の型ガード ({@link classifyIdsStreamLine} の薄い包み)。\n *\n * @param line Parsed NDJSON line. パース済みの NDJSON 行。\n * @returns True when the line is a chunk line. チャンク行なら true。\n */\nexport const isIdsStreamChunkLine = (line: unknown): line is DailyReportIdsStreamChunkLine => {\n return classifyIdsStreamLine(line) === \"chunk\"\n}\n\n/**\n * Computes the identity token of a materialized ids array.\n * 確定済み ids 配列の同一性トークンを算出する処理。\n *\n * 内容から決まる値にする — プロセスごとにキャッシュバケットが別なので、別ワーカーが\n * 応答しても同じ配列なら同じトークンにならなければならない。畳み込むのは各行の\n * `reportHubId` と `businessDate` (= カーソルの位置を決める全順序の構成要素) + 件数。\n * `sourceType` は順序に関与しないため畳み込まない (変わっても位置は不変)。\n *\n * ❗ 衝突の方向を取り違えないこと。決定的ハッシュに「同じ配列なのに別トークン」(偽不一致 =\n * 無駄な再送) は構造上起こり得ない。危険なのはその逆 — **異なる配列が同一トークンになる\n * 偽一致**であり、その場合カーソルが変化後の配列に対して信用され、欠損・重複が無音で発生する\n * (認可には影響しない — 再開行は常に現在の認可済み配列から切り出される)。この確率を議論の\n * 余地なく無視できる水準 (~2^-64) にするため、オフセット基底の異なる FNV-1a 32bit を\n * 2 レーン走らせて連結する。費用は 22 万件で数 ms。\n *\n * @param items Stream-ordered items. ストリーム順のアイテム配列。\n * @returns Hex token (up to 16 chars). 16 進トークン (最長 16 桁)。\n */\nexport const computeIdsStreamSnapshotToken = (items: readonly DailyReportItem[]): string => {\n let laneA = 0x811c9dc5\n // レーン B は別の基底から出発する (同一基底 2 本では独立性が出ない)\n let laneB = 0x811c9dc5 ^ 0x5bd1e995\n for (const item of items) {\n const id = item.reportHubId\n laneA ^= id & 0xff\n laneA = Math.imul(laneA, 0x01000193)\n laneA ^= (id >>> 8) & 0xff\n laneA = Math.imul(laneA, 0x01000193)\n laneA ^= (id >>> 16) & 0xff\n laneA = Math.imul(laneA, 0x01000193)\n laneA ^= (id >>> 24) & 0xff\n laneA = Math.imul(laneA, 0x01000193)\n laneB ^= (id >>> 24) & 0xff\n laneB = Math.imul(laneB, 0x01000193)\n laneB ^= (id >>> 16) & 0xff\n laneB = Math.imul(laneB, 0x01000193)\n laneB ^= (id >>> 8) & 0xff\n laneB = Math.imul(laneB, 0x01000193)\n laneB ^= id & 0xff\n laneB = Math.imul(laneB, 0x01000193)\n // ❗ businessDate も畳み込む。カーソルの位置は (businessDate, id) の全順序で決まるため、\n // id 列が同一のまま日付だけ整合的に動いた配列 (外部取込の日付訂正等) を同一視すると、\n // 位置の変わったカーソルが信用され、未受信行が無音で飛ぶ (実測で再現された盲点)。\n // null 営業日は 1 バイトの番兵で表す (空文字列と区別する)\n const date = item.businessDate\n if (date === null) {\n laneA ^= 0x01\n laneA = Math.imul(laneA, 0x01000193)\n laneB ^= 0x01\n laneB = Math.imul(laneB, 0x01000193)\n } else {\n for (let i = 0; i < date.length; i++) {\n const code = date.charCodeAt(i)\n laneA ^= code & 0xff\n laneA = Math.imul(laneA, 0x01000193)\n laneB ^= code & 0xff\n laneB = Math.imul(laneB, 0x01000193)\n }\n }\n }\n // 件数も両レーンへ畳み込む (先頭が同じで末尾だけ欠けた配列を別物にする)\n laneA ^= items.length\n laneA = Math.imul(laneA, 0x01000193)\n laneB ^= items.length\n laneB = Math.imul(laneB, 0x01000193)\n return ((laneA >>> 0).toString(16).padStart(8, \"0\") + (laneB >>> 0).toString(16).padStart(8, \"0\")).replace(/^0+(?=.)/, \"\")\n}\n\n/**\n * Total order of the ids stream: businessDate DESC (NULL last), then id DESC.\n * ids ストリームの全順序。営業日降順 (NULL は末尾)、同日内は ID 降順。\n *\n * MSSQL の `ORDER BY business_date DESC, id DESC` と一致させることが唯一の存在理由。\n * 営業日は正規化済みの `YYYY-MM-DD` 文字列なので辞書順比較 = 時系列比較になる。\n *\n * @param a Left key. 左辺キー。\n * @param b Right key. 右辺キー。\n * @returns Negative when `a` streams before `b`. `a` が先に流れるなら負値。\n */\nexport const compareIdsStreamOrder = (a: DailyReportIdsStreamCursor, b: DailyReportIdsStreamCursor): number => {\n if (a.businessDate !== b.businessDate) {\n // null 営業日は並び順の末尾 (MSSQL DESC の NULL 末尾挙動へ揃える)\n if (a.businessDate === null) return 1\n if (b.businessDate === null) return -1\n return a.businessDate > b.businessDate ? -1 : 1\n }\n return b.reportHubId - a.reportHubId\n}\n\n/**\n * Checks whether items already follow the ids-stream total order.\n * アイテム配列が ids ストリームの全順序に既に従っているかを検査する処理。\n *\n * ストリームはソート順どおりに届いて末尾へ追記されるため通常は整列済みで、\n * この O(n) 検査で全件ソート (O(n log n)) を省略できる。\n *\n * @param items Items to probe. 検査対象のアイテム配列。\n * @returns True when already ordered. 整列済みなら true。\n */\nexport const isIdsStreamOrdered = (items: readonly DailyReportItem[]): boolean => {\n for (let i = 1; i < items.length; i++) {\n if (compareIdsStreamOrder(items[i - 1], items[i]) > 0) return false\n }\n return true\n}\n\n/**\n * Returns a copy with the item upserted at its ids-stream-ordered position.\n * アイテムを ids ストリーム順の正しい位置へ upsert した複製配列を返す処理。\n *\n * 前提: `items` は ids ストリーム順に整列済み (このモジュールの比較器を使う全書き込み元が\n * 維持する不変条件)。既存の同 ID は除去してから二分探索で挿入位置を求めるため、\n * 全件ソート無しの O(n) で並び順を保てる (以前は SSE 1 件ごとに 20 万件超を\n * localeCompare で全ソートしていた)。\n *\n * @param items Ordered items. 整列済みアイテム配列。\n * @param item Item to upsert. upsert するアイテム。\n * @returns New ordered array. 新しい整列済み配列。\n */\nexport const upsertInIdsStreamOrder = (items: readonly DailyReportItem[], item: DailyReportItem): DailyReportItem[] => {\n // 既存の同 ID を除いた複製 (位置移動を伴う更新もこれで単一経路になる)\n const next = items.filter((existing) => existing.reportHubId !== item.reportHubId)\n let low = 0\n let high = next.length\n while (low < high) {\n const mid = (low + high) >> 1\n if (compareIdsStreamOrder(next[mid], item) <= 0) {\n low = mid + 1\n } else {\n high = mid\n }\n }\n next.splice(low, 0, item)\n return next\n}\n\n/**\n * Finds the first index that streams strictly after the cursor.\n * カーソルより厳密に後ろへ並ぶ最初のインデックスを求める処理。\n *\n * 配列は必ずストリーム順 (SQL の ORDER BY) で渡されるが、比較器とのズレが\n * あっても壊れない線形走査で決定する (件数は高々数万・再開時のみの実行)。\n * 該当なし (カーソルが末尾以降) は `items.length` を返し、空チャンク +\n * `isLast: true` の正常終端に落ちる。\n *\n * @param items Stream-ordered items. ストリーム順のアイテム配列。\n * @param cursor Resume cursor. 再開カーソル。\n * @returns Index to resume emission from. 送出を再開するインデックス。\n */\nexport const findIdsStreamResumeIndex = (items: readonly DailyReportItem[], cursor: DailyReportIdsStreamCursor): number => {\n const index = items.findIndex((item) => compareIdsStreamOrder({ businessDate: item.businessDate, reportHubId: item.reportHubId }, cursor) > 0)\n return index === -1 ? items.length : index\n}\n","/**\n * Minimal level-filtered logger used internally by the daily-report package.\n * daily-report パッケージ内部で使う最小のレベルフィルタ付きロガー。\n *\n * 消費アプリのロガー実装 (console 互換の debug/info/warn/error) を注入でき、\n * 未注入時は console にフォールバックする。\n */\n\n/** 出力フィルタリング用のログレベル。 */\nexport enum LogLevel {\n DEBUG = 0,\n INFO = 1,\n WARN = 2,\n ERROR = 3,\n NONE = 4,\n}\n\n/** Console 互換のロガーインターフェース。 */\nexport interface DailyReportLogger {\n debug(message?: unknown, ...optionalParams: unknown[]): void\n info(message?: unknown, ...optionalParams: unknown[]): void\n warn(message?: unknown, ...optionalParams: unknown[]): void\n error(message?: unknown, ...optionalParams: unknown[]): void\n}\n\n/**\n * Creates a prefixed, level-filtered logger delegating to the given implementation.\n * 指定実装へ委譲するプレフィックス付き・レベルフィルタ付きロガーを生成する処理。\n */\nexport const createLogger = (level: LogLevel, prefix: string, impl: DailyReportLogger = console): DailyReportLogger => {\n // 文字列メッセージはプレフィックスを連結、それ以外は先頭引数として付加する\n const format = (message: unknown): unknown[] => (typeof message === \"string\" ? [`${prefix} ${message}`] : [prefix, message])\n return {\n debug: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.DEBUG) impl.debug(...format(message), ...rest)\n },\n info: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.INFO) impl.info(...format(message), ...rest)\n },\n warn: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.WARN) impl.warn(...format(message), ...rest)\n },\n error: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.ERROR) impl.error(...format(message), ...rest)\n },\n }\n}\n","/**\n * Zod schemas for daily-report SSE messages (client/server shared contract).\n * 日報 SSE メッセージの zod スキーマ (client / server 共有契約)。\n */\nimport { z } from \"zod\"\nimport type { DailyReportPostedComment as DeclaredComment, DailyReportDetail as DeclaredDetail } from \"./types\"\n\n// --- サブスキーマ(types.ts の型と構造的に一致させる) ---\n\nexport const dailyReportInterviewerSchema = z.object({\n name: z.string().nullish(),\n affiliation: z.string().nullish(),\n})\n\nexport const dailyReportExternalCommentSchema = z.object({\n name: z.string().nullish(),\n text: z.string().nullish(),\n color: z.string().nullish(),\n})\n\nexport const dailyReportLabelDefSchema = z.object({\n id: z.number(),\n name: z.string().nullish(),\n color: z.string().nullish(),\n})\n\nexport const dailyReportPostedCommentSchema = z.object({\n id: z.number(),\n userId: z.string().nullish(),\n userName: z.string().nullish(),\n content: z.string().nullish(),\n createdAt: z.string().nullish(),\n isMine: z.boolean(),\n})\n\n/**\n * 添付ファイル 1 件のスキーマ。\n *\n * 周囲のフィールドは `.nullish()` だが、ここは意図して `.nullable()` を使う。\n * `.nullish()` は推論キーを任意 (`?`) にするため、`types.ts` 側の必須 `fileType: string | null`\n * へ代入できず、ファイル末尾の `_AssertDetailCompat` が `never` に落ちてコンパイルエラーになる。\n * 既存の `.nullish()` フィールドはいずれも `types.ts` 側が `?` 付きの任意キーとペアになっており、\n * 「`.nullish()` ↔ 任意キー」「`.nullable()` ↔ 必須 `| null`」の対応で揃っている。\n * `| undefined` を混ぜないことは、詳細型を消費する DuckDB ステージ行の要請でもある。\n */\nexport const dailyReportAttachmentItemSchema = z.object({\n id: z.string(),\n fileName: z.string(),\n fileType: z.string().nullable(),\n fileSize: z.number().nullable(),\n createdAt: z.string().nullable(),\n state: z.enum([\"present\", \"absent\", \"unknown\"]),\n})\n\nexport const dailyReportDetailSchema = z.object({\n reportHubId: z.number(),\n date: z.string().nullish(),\n createdAt: z.string().nullish(),\n author: z.string().nullish(),\n userId: z.string().nullish(),\n sourceType: z.string().nullish(),\n employeeName: z.string().nullish(),\n updatedBy: z.string().nullish(),\n updatedAt: z.string().nullish(),\n category: z.string().nullish(),\n creationCategory: z.string().nullish(),\n visitTimeFrom: z.string().nullish(),\n visitTimeTo: z.string().nullish(),\n customerName: z.string().nullish(),\n interviewers: z.array(dailyReportInterviewerSchema),\n subject: z.string().nullish(),\n content: z.string().nullish(),\n externalComments: z.array(dailyReportExternalCommentSchema),\n isRead: z.boolean(),\n isStarred: z.boolean(),\n labels: z.array(dailyReportLabelDefSchema),\n postedComments: z.array(dailyReportPostedCommentSchema),\n // ❗ `.default([])` は必須。SSE の実体は永続 Redis Stream (MAXLEN 保持) であり、\n // **同一ワイヤ版の中でも**古いエントリが残る (共有リーダーは tail-anchor で起動するが、\n // 再接続クライアントの catch-up が過去エントリを読み直す)。このキーを必須にすると\n // 添付機能を入れる前に publish された全エントリが fail-closed で捨てられ、\n // 再接続タブへ更新が永久に届かなくなる。\n // ❗ 必須キーの改名など**互換を破る形の変更**はこの種の default では救えない —\n // その場合は service.ts の DAILY_REPORT_SSE_WIRE_VERSION を上げてストリームごと分離する。\n // 送信側の詰め忘れ検知は types.ts の必須フィールドと `_AssertDetailCompat`\n // (出力型は既定値適用後なので必須のまま) が担っており、ここを緩めても失われない。\n attachments: z.array(dailyReportAttachmentItemSchema).default([]),\n})\n\n// --- SSE メッセージスキーマ(8種) ---\n\nexport const connectedMessageSchema = z.object({\n type: z.literal(\"connected\"),\n})\n\nexport const statusUpdateMessageSchema = z.object({\n type: z.literal(\"status-update\"),\n reportHubId: z.number(),\n statusType: z.enum([\"star\", \"read\"]),\n value: z.boolean(),\n clientTempId: z.string(),\n // recipientRawUserId はサーバーサイドフィルタリング専用。\n // SSE ルートが raw JSON から取得してフィルタ後に除去する。\n // クライアントには到達しないが、サーバー側の publish で .parse() を通すため定義が必要。\n recipientRawUserId: z.number().optional(),\n})\n\n/**\n * 親日報の区分 (sourceType)。受信側の可視性フィルタ専用。\n *\n * ❗ `.optional()` は必須。SSE の実体は永続 Redis Stream (MAXLEN 保持) であり、このフィールドを\n * 載せる前に publish された**旧エントリが配備をまたいで残る**。必須にすると共有リーダーの\n * 再生と再接続クライアントの catch-up が fail-closed で全て捨てられ、更新が永久に届かなくなる。\n *\n * `recipientRawUserId` と異なりクライアントへ**除去せず転送する**。フィルタを通過した時点で\n * その区分は当該閲覧者に可視であり、`sourceType` は一覧・詳細で既に開示済みのため漏洩しない。\n */\nconst sseSourceTypeSchema = z.string().optional()\n\nexport const commentAddMessageSchema = z.object({\n type: z.literal(\"comment-add\"),\n reportHubId: z.number(),\n comment: dailyReportPostedCommentSchema,\n clientTempId: z.string(),\n // 下書き日報のコメントは所有者以外に見えてはならない (本文の全員配信を防ぐ)。\n // サーバーサイドフィルタ専用で、SSE ルートがフィルタ後に除去する\n recipientRawUserId: z.number().optional(),\n sourceType: sseSourceTypeSchema,\n})\n\nexport const commentDeleteMessageSchema = z.object({\n type: z.literal(\"comment-delete\"),\n reportHubId: z.number(),\n commentId: z.number(),\n clientTempId: z.string(),\n // comment-add と同じ理由の宛先フィルタ\n recipientRawUserId: z.number().optional(),\n sourceType: sseSourceTypeSchema,\n})\n\nexport const reportCreateMessageSchema = z.object({\n type: z.literal(\"report-create\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportUpdateMessageSchema = z.object({\n type: z.literal(\"report-update\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportPublishMessageSchema = z.object({\n type: z.literal(\"report-publish\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportDeleteMessageSchema = z.object({\n type: z.literal(\"report-delete\"),\n reportHubId: z.number(),\n clientTempId: z.string(),\n sourceType: sseSourceTypeSchema,\n})\n\n// --- Discriminated Union ---\n\nexport const dailyReportSseMessageSchema = z.discriminatedUnion(\"type\", [\n connectedMessageSchema,\n statusUpdateMessageSchema,\n commentAddMessageSchema,\n commentDeleteMessageSchema,\n reportCreateMessageSchema,\n reportUpdateMessageSchema,\n reportPublishMessageSchema,\n reportDeleteMessageSchema,\n])\n\n// --- 型エクスポート ---\n\nexport type DailyReportSseMessage = z.infer<typeof dailyReportSseMessageSchema>\n\n// --- コンパイル時の型互換チェック ---\n// Zod推論型が既存の型定義に代入可能であることを保証する。\n// ここでエラーが出たらスキーマと types.ts がずれている。\n\ntype _AssertDetailCompat = z.infer<typeof dailyReportDetailSchema> extends DeclaredDetail ? true : never\ntype _AssertCommentCompat = z.infer<typeof dailyReportPostedCommentSchema> extends DeclaredComment ? true : never\nconst _detailCheck: _AssertDetailCompat = true\nconst _commentCheck: _AssertCommentCompat = true\nvoid _detailCheck\nvoid _commentCheck\n","/**\n * ETag generation helper for JSON payloads.\n * JSON ペイロード向けの ETag 生成ヘルパー。\n */\nimport { createHash } from \"node:crypto\"\n\n/**\n * Generates a SHA-256 ETag for the given data.\n * 指定されたデータの SHA-256 ETag を生成します。\n *\n * @param data The data to hash (will be JSON stringified).\n * @returns The ETag string (wrapped in quotes).\n */\nexport const generateETag = (data: unknown): string => {\n const json = JSON.stringify(data)\n const hash = createHash(\"sha256\").update(json).digest(\"hex\")\n return `\"${hash}\"`\n}\n","/**\n * JSON response helper with shared security headers and ETag support.\n * 共有セキュリティヘッダーと ETag 対応を備えた JSON レスポンスヘルパー。\n */\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport { generateETag } from \"./etag\"\n\nconst defaultLogger = createLogger(LogLevel.INFO, \"[Response]\")\n\n/**\n * Creates JSON response with shared security headers and ETag support.\n * 共有のセキュリティヘッダーと ETag サポート付き JSON レスポンスを生成。\n */\nexport const jsonResponseWithETag = (request: Request, cookie: string | null, payload: Record<string, unknown>, status = 200, logger: DailyReportLogger = defaultLogger): Response => {\n const etag = generateETag(payload)\n const ifNoneMatch = request.headers.get(\"If-None-Match\")\n\n logger.info(`[jsonResponseWithETag] ETag: ${etag}, If-None-Match: ${ifNoneMatch}`)\n\n const headers = new Headers({\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"private, max-age=0, must-revalidate\",\n \"X-Content-Type-Options\": \"nosniff\",\n \"X-Frame-Options\": \"DENY\",\n \"Content-Security-Policy\": \"default-src 'none'\",\n ETag: etag,\n })\n if (cookie) {\n headers.append(\"Set-Cookie\", cookie)\n }\n\n if (request.method === \"GET\" && status === 200 && ifNoneMatch === etag) {\n return new Response(null, { status: 304, headers })\n }\n\n return new Response(JSON.stringify(payload), { status, headers })\n}\n","/**\n * Fan-Out shared reader for the daily report SSE Redis Stream.\n * 日報 SSE 用 Redis Stream の共有リーダーモジュール。\n *\n * 単一の xRead BLOCK ループで全接続にメッセージをブロードキャストし、\n * 接続ごとに Redis TCP を張る問題を解消する。\n */\n\nimport { EventEmitter } from \"node:events\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport type { SqlResultCache } from \"./cache\"\nimport type { DailyReportRedisBlockingClient, DailyReportRedisProvider } from \"./ports\"\n\n/**\n * Compares two Redis Stream IDs numerically.\n * Redis Stream ID を数値比較し、a <= b なら true を返す。\n *\n * Stream ID は `<timestamp_ms>-<sequence>` 形式。\n * 辞書順比較では sequence が可変長のとき不正確になるため、数値分割で比較する。\n */\nexport const isStreamIdLte = (a: string, b: string): boolean => {\n const [aMs, aSeq] = a.split(\"-\").map(Number)\n const [bMs, bSeq] = b.split(\"-\").map(Number)\n if (aMs !== bMs) return aMs < bMs\n return aSeq <= bSeq\n}\n\n/** xRead 結果の 1 エントリに相当する型。 */\nexport type StreamEntry = {\n id: string\n message: Record<string, string>\n}\n\nexport type DailyReportSseReaderConfig = {\n /** ブロッキング xRead 用の専用クライアントを生成する redis プロバイダー。 */\n redis?: DailyReportRedisProvider\n /** SSE Redis Stream キー。 */\n streamKey: string\n /** 受信メッセージに応じてローカル SQL キャッシュを無効化する対象キャッシュ。 */\n cache: SqlResultCache\n /** ロガー (既定は console ベース)。 */\n logger?: DailyReportLogger\n}\n\n/**\n * Shared xRead loop that fans out entries to all active SSE connections.\n * 全 SSE 接続に対してメッセージをファンアウトする共有 xRead ループ。\n *\n * - \"entry\" イベント: 新着エントリ (StreamEntry) をブロードキャスト\n * - \"error\" イベント: 回復不能エラーをブロードキャスト\n * */\nexport class DailyReportSseReader {\n private _emitter = new EventEmitter()\n private _state: \"idle\" | \"running\" | \"stopping\" = \"idle\"\n private _refCount = 0\n // \"$\" = 「購読開始以降の新着のみ」の番兵。ループ開始時に一度だけ具体 ID (ストリーム末尾) へ\n // 解決される (_startLoop 参照)。❗ \"0-0\" を初期値にしてはならない: 初回購読でストリーム全履歴を\n // 再生し、過去イベントの invalidatePrefix が初回コールドフェッチの書き込みガードを不成立にして\n // キャッシュ保存を毒殺する (実測: 履歴 10,006 件の再生で ids バケットが保存されず、次のロードも\n // 全量コールド)。起動直後のプロセスのキャッシュは空で、過去イベントの無効化は無意味。\n // 過去分の取りこぼし補償は接続単位の catch-up (Last-Event-ID + xRange) の責務。\n // 通常の stop/start では従来どおり前回位置を保持し、停止中の実イベントを再開時に追う\n private _lastId = \"$\"\n private _client: DailyReportRedisBlockingClient | undefined\n private readonly _logger: DailyReportLogger\n\n constructor(private readonly config: DailyReportSseReaderConfig) {\n // リスナー上限を緩和 (接続数分)\n this._emitter.setMaxListeners(0)\n this._logger = config.logger ?? createLogger(LogLevel.INFO, \"[SSE Reader]\")\n }\n\n /**\n * Subscribes to the shared reader. Starts the loop on first subscriber.\n * 共有リーダーを購読する。最初の購読者でループを開始する。\n *\n * @param onEntry - 新着エントリのコールバック\n * @param onError - 回復不能エラーのコールバック (省略可)\n * @returns unsubscribe 関数。呼び出すと両リスナーを解除し、最後の購読者解除でループを停止する。\n */\n subscribe(onEntry: (entry: StreamEntry) => void, onError?: (err: Error) => void): () => void {\n this._emitter.on(\"entry\", onEntry)\n if (onError) {\n this._emitter.on(\"error\", onError)\n }\n this._refCount++\n\n // 最初の購読者でループ開始\n if (this._refCount === 1) {\n void this._startLoop()\n }\n\n // unsubscribe\n return () => {\n this._emitter.removeListener(\"entry\", onEntry)\n if (onError) {\n this._emitter.removeListener(\"error\", onError)\n }\n this._refCount--\n if (this._refCount <= 0) {\n this._refCount = 0\n void this._stopLoop()\n }\n }\n }\n\n /**\n * Destroys the reader for test cleanup.\n * テストクリーンアップ用のデストラクタ。\n */\n async destroy(): Promise<void> {\n this._refCount = 0\n await this._stopLoop()\n this._emitter.removeAllListeners()\n this._lastId = \"$\"\n }\n\n /**\n * Internal xRead BLOCK loop.\n * 内部 xRead BLOCK ループ。\n */\n private async _startLoop(): Promise<void> {\n // stopping 中なら完了を待つ\n while (this._state === \"stopping\") {\n await new Promise((resolve) => setTimeout(resolve, 50))\n }\n if (this._state === \"running\") return\n // 停止完了を待つ間に最後の購読者が解除されていたら起動しない\n // (購読者 0 の孤児ループが専用 Redis 接続を握り続けるのを防ぐ)\n if (this._refCount === 0) return\n\n this._state = \"running\"\n\n // 専用クライアント取得。createClient が reject した場合も _state を idle へ戻し、\n // 再購読でループを再開できるようにする (注入 redis 実装が例外を投げても state-stuck しない)。\n try {\n this._client = await this.config.redis?.createClient()\n } catch (err) {\n this._state = \"idle\"\n this._emitError(err instanceof Error ? err : new Error(\"Failed to create Redis client for SSE reader\"))\n return\n }\n if (!this._client) {\n this._state = \"idle\"\n this._emitError(new Error(\"Failed to create Redis client for SSE reader\"))\n return\n }\n\n // エラーリスナー\n this._client.on(\"error\", (err) => {\n this._logger.error(\"Redis client error:\", err)\n })\n\n // ❗ \"$\" は xRead ごとにサーバー側で「その時点の末尾」へ再解決されるため、BLOCK タイムアウトで\n // 再武装する合間に届いたエントリを永続的に取りこぼす (\"$\" が有効なのは最初の 1 回だけ、が\n // Redis の規約)。ループへ入る前に一度だけ具体 ID へ固定する。空ストリームの \"0-0\" は\n // 履歴が存在しないので安全 (毒殺は「履歴があるのに 0-0」の場合のみ)。\n // 解決に失敗したら \"$\" のまま縮退する (取りこぼし窓が残るだけで、履歴再生よりは安全)\n if (this._lastId === \"$\") {\n try {\n const shared = await this.config.redis?.getClient()\n if (shared) {\n const latest = await shared.xRevRange(this.config.streamKey, \"+\", \"-\", { COUNT: 1 })\n this._lastId = latest.length > 0 ? latest[0].id : \"0-0\"\n }\n } catch (err) {\n this._logger.warn(\"Failed to resolve stream tail anchor; falling back to $:\", err)\n }\n }\n\n try {\n while (this._state === \"running\" && this._client?.isOpen) {\n const results = await this._client.xRead([{ key: this.config.streamKey, id: this._lastId }], { BLOCK: 5000, COUNT: 100 })\n if (!results) continue\n\n for (const stream of results) {\n for (const msg of stream.messages) {\n this._lastId = msg.id\n // epoch check の補完: SSE 経由でもローカルキャッシュを即座にクリア\n // (epoch check が主防御線。SSE は Redis ダウン時や非 epoch 対象キャッシュへのフォールバック)\n try {\n const parsed = msg.message?.data ? JSON.parse(msg.message.data) : null\n const msgType = parsed?.type\n if (msgType === \"report-create\" || msgType === \"report-delete\" || msgType === \"report-publish\") {\n this.config.cache.invalidatePrefix(\"daily-report:ids\")\n }\n // コメント・ステータス変更時は対象レポートの詳細キャッシュを無効化\n if (msgType === \"comment-add\" || msgType === \"comment-delete\" || msgType === \"status-update\") {\n const reportHubId = parsed?.reportHubId\n if (typeof reportHubId === \"number\") {\n this.config.cache.invalidate(`daily-report:detail:${reportHubId}`)\n this.config.cache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n }\n }\n } catch {\n // JSON パースエラーは無視 (invalidation スキップ = safe 方向)\n }\n this._emitter.emit(\"entry\", { id: msg.id, message: msg.message as Record<string, string> })\n }\n }\n }\n } catch (err) {\n // ClientClosedError は正常停止時に発生する\n const isClientClosed = (err as Error)?.constructor?.name === \"ClientClosedError\"\n if (!isClientClosed && this._state === \"running\") {\n this._logger.error(\"xRead loop error:\", err)\n this._emitError(err instanceof Error ? err : new Error(String(err)))\n }\n } finally {\n // クライアント切断\n if (this._client?.isOpen) {\n try {\n await this._client.quit()\n } catch (_e) {\n // 切断エラーは無視\n }\n }\n this._client = undefined\n this._state = \"idle\"\n\n // ループ終了後に購読者が残っていれば自動再起動\n if (this._refCount > 0) {\n void this._startLoop()\n }\n }\n }\n\n /**\n * Stops the xRead loop by closing the client.\n * クライアントを閉じて xRead ループを停止する。\n */\n private async _stopLoop(): Promise<void> {\n if (this._state !== \"running\") return\n this._state = \"stopping\"\n\n if (this._client?.isOpen) {\n try {\n await this._client.quit()\n } catch (_e) {\n // 切断エラーは無視\n }\n }\n\n // 自分が仕掛けた停止フェーズの完了だけを待つ。\"idle\" 限定で待つと、停止直後の同期的な\n // 自動再起動 (finally 内の idle → running 遷移には await が挟まらない) に追い越された場合に\n // このポーラが永遠に \"idle\" を観測できず、50ms タイマーが恒久的に残留する。\n // \"stopping\" を抜けたら (idle でも running でも) 停止処理としての責務は完了している。\n // _state は別の非同期ループが書き換えるが、TS は await を跨いだクラスフィールドの変化を\n // 追えず \"stopping\" に過剰 narrowing するため、宣言型へキャストして比較する。\n while ((this._state as \"idle\" | \"running\" | \"stopping\") === \"stopping\") {\n await new Promise((resolve) => setTimeout(resolve, 50))\n }\n }\n\n /**\n * Emits an \"error\" event only when a listener exists (bare \"error\" emit throws).\n * リスナーが居るときだけ \"error\" イベントを emit する処理 (リスナー 0 の \"error\" emit は\n * EventEmitter 仕様で throw し、非同期文脈では unhandled rejection = プロセス危険になるため)。\n */\n private _emitError(err: Error): void {\n if (this._emitter.listenerCount(\"error\") > 0) {\n this._emitter.emit(\"error\", err)\n } else {\n this._logger.error(\"SSE reader error (no error listeners):\", err)\n }\n }\n}\n","/**\n * Source-type visibility set: normalization, digest, and predicate helpers.\n * sourceType 可視集合の正規化・ダイジェスト・判定ヘルパー群。\n *\n * 中立文字列 (`DailyReportHub.sourceType` の値) のみを扱い、いかなる認可パッケージの型にも\n * 依存しない。可視集合は 2 状態を厳密に区別する:\n *\n * - `null` = 無制限 (全件)。ポート未注入時の既定であり、現行互換の意味を持つ。\n * - `Set` = その集合に限定。**空集合は 0 件 (fail-close)** であって無制限ではない。\n *\n * この 2 状態の取り違えは情報漏洩か機能停止に直結するため、判定・畳み込み・キー生成を\n * 本ファイルへ一元化し、ハンドラ層とサービス層の双方がここだけを参照する。\n *\n * 閲覧範囲は**次元ごと**にこの 2 状態を持つ:\n *\n * - `visible` = 本文 (日報行) を閲覧してよい区分。\n * - `commentable` = コメントを閲覧・記入してよい区分。常に `visible` の部分集合。\n *\n * 次元が増えても状態数は増やさない。「もう一方の次元に従う」は\n * {@link DailyReportViewerScope.restrictTo} が**構築時に解決**するため、各次元は最後まで\n * 厳密に 2 状態であり、本ファイルの判定ヘルパーは次元を問わずそのまま適用できる。\n */\nimport { inArray, type SQL, sql } from \"drizzle-orm\"\nimport type { AnyMsSqlColumn } from \"drizzle-orm/mssql-core\"\nimport type { DailyReportVisibilityDimensions } from \"./ports\"\n\n/**\n * Normalized visible set. null = unrestricted (all rows), ReadonlySet = restricted (empty = zero rows).\n * 正規化済みの可視集合。null は無制限 (全件)、ReadonlySet は限定 (空集合は 0 件)。\n */\nexport type VisibleSourceTypeSet = ReadonlySet<string> | null\n\n/**\n * Separator for digest concatenation; never occurs inside a sourceType value.\n * ダイジェスト連結用の区切り文字。sourceType の値には決して現れない制御文字。\n */\nconst DIGEST_SEPARATOR = \"\\u0001\"\n\n/**\n * Maximum digest length before falling back to a hash.\n * ハッシュへ退避する前のダイジェスト最大長。\n *\n * キャッシュキーが際限なく伸びるのを防ぐだけの閾値であり、通常の区分数 (数個) では到達しない。\n */\nconst DIGEST_RAW_MAX_LENGTH = 64\n\n/**\n * Folds a stored sourceType value into its comparison key (case-insensitive only).\n * 格納済みの sourceType 値を比較キーへ畳み込む処理 (大小文字非依存のみ)。\n *\n * ❗ ここで `trim()` してはならない。SQL 側の述語は `LOWER(source_type)` だけを掛けており\n * 列値をトリムしないため、JS 側だけ切り詰めると同じ行について SSE (JS 判定) は可視・\n * 読み取り (SQL 判定) は不可視という乖離が生まれる (SSE が fail-open 側へずれる)。\n * 両者を「列値は畳み込むだけ」に揃え、空白混じりの値は双方で不可視 = fail-close とする。\n */\nconst foldStoredSourceType = (raw: string): string => raw.toLowerCase()\n\n/**\n * Folds a resolver-supplied token into its comparison key (trimmed, case-insensitive).\n * リゾルバ由来のトークンを比較キーへ畳み込む処理 (前後空白除去・大小文字非依存)。\n *\n * トークンは人手で設定された設定値であり、前後の空白は書き間違いとして吸収してよい。\n * 一方で列値 ({@link foldStoredSourceType}) は DB のデータなので勝手に切り詰めない。\n */\nconst foldResolverToken = (raw: string): string => raw.trim().toLowerCase()\n\n/**\n * Normalizes a raw resolver return into a VisibleSourceTypeSet.\n * ポートの生戻り値を正規化済み可視集合へ変換する処理。\n *\n * `null` / `undefined` は無制限 (`null`) へ。配列は各要素を畳み込み・空文字除去・重複排除して\n * `Set` 化する。**空配列は空 `Set` (0 件) を返し、`null` (無制限) へは決して倒さない** —\n * この取り違えが情報漏洩か機能停止に直結するため。\n *\n * ❗ 引数は**単一次元のトークン配列に限る**。次元別オブジェクト\n * ({@link DailyReportVisibilityDimensions}) をここへ渡すと配列ガードに掛かって全拒否になるため、\n * 次元の解体は {@link DailyReportViewerScope.restrictTo} が行い、ここには常に配列だけが届く。\n *\n * ❗ オーバーロードで「**配列を渡した場合は決して `null` (無制限) を返さない**」という不変条件を\n * 型に載せている。これは実装の偶然ではなく本ファイルの中核契約 (空配列は 0 件であって無制限では\n * ない) そのものであり、呼び出し側が `null` 分岐を書いて無制限へ倒す余地を型で塞ぐ。\n *\n * @param raw Raw single-dimension resolver value. 単一次元の生値。\n * @returns Normalized visible set. 正規化済みの可視集合。\n */\nexport function normalizeVisibleSourceTypes(raw: readonly string[]): ReadonlySet<string>\nexport function normalizeVisibleSourceTypes(raw: readonly string[] | null | undefined): VisibleSourceTypeSet\nexport function normalizeVisibleSourceTypes(raw: readonly string[] | null | undefined): VisibleSourceTypeSet {\n if (raw == null) {\n return null\n }\n // 配列以外 (契約違反。例: リゾルバが誤って文字列 \"NI\" を返す) は fail-close で空集合にする。\n // ガードが無いと `for...of \"NI\"` が文字単位で反復し {\"n\",\"i\"} という誤った可視集合になる\n if (!Array.isArray(raw)) {\n return new Set<string>()\n }\n const set = new Set<string>()\n for (const token of raw) {\n // 非文字列要素は黙って捨てる (可視性を広げる方向には決して働かない)\n if (typeof token !== \"string\") {\n continue\n }\n const folded = foldResolverToken(token)\n if (folded.length > 0) {\n set.add(folded)\n }\n }\n return set\n}\n\n/**\n * Tests whether a report's sourceType is visible under the given set.\n * ある日報の sourceType が可視集合に含まれるかの三値判定。\n *\n * - 無制限 (`null`): 常に `true`。\n * - 限定 + sourceType 判明: 畳み込み一致で判定。\n * - 限定 + sourceType 不明 (`null` / `undefined`): `false` — 制限された閲覧者は「不明な区分」を\n * 見られない。区分を運ばない旧 SSE エントリ等を fail-close 側へ倒すための規則。\n *\n * @param set Normalized visible set. 正規化済みの可視集合。\n * @param sourceType The row's source type, if known. 判明していれば当該行のソース種別。\n * @returns True when visible. 可視なら true。\n */\nexport function isSourceTypeVisible(set: VisibleSourceTypeSet, sourceType: string | null | undefined): boolean {\n if (set === null) {\n return true\n }\n if (sourceType == null) {\n return false\n }\n return set.has(foldStoredSourceType(sourceType))\n}\n\n/**\n * Tells whether the set denies everything (zero rows).\n * 可視集合が全拒否 (0 件) を意味するかを判定する処理。\n *\n * 真のとき呼び出し側は DB へ問い合わせず即座に空を返してよい (無駄クエリの排除)。\n * これは性能上の短絡であり、認可の防御そのものは {@link buildVisibleSourceTypeCondition} が担う。\n *\n * @param set Normalized visible set. 正規化済みの可視集合。\n * @returns True when nothing is visible. 何も可視でないなら true。\n */\nexport function deniesEverySourceType(set: VisibleSourceTypeSet): boolean {\n return set !== null && set.size === 0\n}\n\n/**\n * Builds the SQL condition restricting rows to the visible source types.\n * 可視 sourceType へ行を限定する SQL 条件を生成する処理 (三値)。\n *\n * {@link isSourceTypeVisible} の SQL 版であり、同じ三値を同じ向きへ写像する:\n *\n * - 無制限 (`null`): `undefined` を返す。drizzle の `and(...)` は `undefined` を無視するため、\n * 既存の述語がそのまま残る (現行互換)。\n * - 全拒否 (空集合): `1 = 0` を返す。**決して `undefined` へ倒さない** — 呼び出し側の短絡漏れが\n * あっても生成器自身が fail-close する二重防御であり、ここを緩めると認可が反転する。\n * - 限定: `LOWER(<column>) IN (...)`。トークンは正規化時に小文字畳み込み済みなので両辺が小文字となり、\n * DB の照合順序 (collation) に依存せず決定的に大小文字非依存で一致する。\n *\n * @param sourceTypeColumn The source-type column to constrain. 制限対象の sourceType 列。\n * @param set Normalized visible set. 正規化済みの可視集合。\n * @returns Condition, or undefined when unrestricted. 条件 (無制限なら undefined)。\n */\nexport function buildVisibleSourceTypeCondition(sourceTypeColumn: AnyMsSqlColumn, set: VisibleSourceTypeSet): SQL | undefined {\n if (set === null) {\n return undefined\n }\n if (set.size === 0) {\n return sql`1 = 0`\n }\n return inArray(sql`LOWER(${sourceTypeColumn})`, [...set])\n}\n\n/**\n * Request-scoped viewer scope carrying the normalized visible source-type set.\n * 正規化済みの可視 sourceType 集合を運ぶ、リクエストスコープの閲覧範囲。\n *\n * このクラスは private フィールドを持つことで**名目型**になっており、`{ visible: null }` のような\n * 構造的偽造も、`{ ...scope, visible: null }` のようなスプレッドによる widening も型が拒否する\n * (`unique symbol` ブランドでは後者を防げないことを実測で確認済み)。\n *\n * 生成経路は {@link DailyReportViewerScope.restrictTo} と {@link DailyReportViewerScope.unrestricted}\n * の 2 つだけであり、`null` を「可視性」としてそのまま綴ることはできない。結果として、\n * **絞り込まれない読み取りは必ず `unrestricted(理由)` として現れる**ため、\n * `grep -rn \"DailyReportViewerScope.unrestricted(\"` が fail-open の全数リストになる。\n */\nexport class DailyReportViewerScope {\n /**\n * 正規化済みの本文可視集合を保持する private スロット。\n *\n * ❗ public な読み取り専用プロパティにしてはならない。`readonly` は型のみの制約であり、\n * `Object.assign(scope, { visible: null })` は型検査も実行時も素通りして実体を書き換える\n * (実測済み)。private スロット + getter なら代入が実行時にも失敗する。\n */\n readonly #visible: VisibleSourceTypeSet\n\n /**\n * 正規化済みのコメント可視集合を保持する private スロット。\n *\n * 構築時に確定済みであり、`#visible` と同じ 2 状態しか取らない (`null` = 無制限 / `Set` = 限定)。\n * 「`read` に従う」を遅延 sentinel として保持すると次元あたり 3 状態目が生まれ、判定・畳み込み・\n * キー生成の全てを分岐させる必要が出るため、必ず {@link DailyReportViewerScope.restrictTo} で\n * 解決してからここへ入れる。\n */\n readonly #commentable: VisibleSourceTypeSet\n\n private constructor(visible: VisibleSourceTypeSet, commentable: VisibleSourceTypeSet) {\n this.#visible = visible\n this.#commentable = commentable\n }\n\n /** 正規化済みの本文可視集合。`null` は無制限、空集合は 0 件 (fail-close)。 */\n get visible(): VisibleSourceTypeSet {\n // ❗ 実体をそのまま返さない。`ReadonlySet` は型のみの制約で、実体の `Set` は\n // `(scope.visible as unknown as Set<string>).add(...)` で in-place に広げられる (実測済み)。\n // 毎回コピーを返すことで、外部から可視集合を広げる経路を断つ\n return this.#visible === null ? null : new Set(this.#visible)\n }\n\n /**\n * 正規化済みのコメント可視集合。`null` は無制限、空集合は 0 件 (fail-close)。\n *\n * 常に `visible` の部分集合である (構築時に積集合を取っているため)。\n */\n get commentable(): VisibleSourceTypeSet {\n // `visible` と同じ理由でコピーを返す\n return this.#commentable === null ? null : new Set(this.#commentable)\n }\n\n /**\n * Builds a scope restricted to the given source types (empty input = zero rows).\n * 指定した sourceType へ限定した閲覧範囲を生成する処理 (空入力は 0 件)。\n *\n * 入力は {@link normalizeVisibleSourceTypes} を通るため、大小文字・前後空白・重複・非文字列は\n * ここで吸収される。素の配列は `{ read: <配列> }` と同義で、コメント次元は `read` に従う。\n *\n * ❗ 引数は `null` / `undefined` を**受け付けない**。受け付けると「restrict」という名前のまま\n * 無制限スコープを生む第 2 の入口になり、「絞り込まれない読み取りは必ず\n * {@link DailyReportViewerScope.unrestricted} に現れる」という監査不変条件が崩れる。\n * 無制限は必ず理由付きで明示すること。\n *\n * ❗ 判別に `Array.isArray` を使ってはならない。型述語が `arg is any[]` であり\n * `readonly string[]` を負の分岐から除去できないため、`.read` 参照が TS2339 になる\n * (実測済み)。`\"read\" in input` で判別する。文字列など object でない契約違反入力は\n * `in` が実行時に throw するので、先に `typeof` で弾いてから\n * {@link normalizeVisibleSourceTypes} の fail-close 経路へ落とす。\n *\n * @param input Visible source-type tokens, or per-dimension sets. 可視 sourceType のトークン群、または次元別の集合。\n * @returns The scope. 生成した閲覧範囲。\n */\n static restrictTo(input: readonly string[] | DailyReportVisibilityDimensions): DailyReportViewerScope {\n const isDimensions = typeof input === \"object\" && input !== null && \"read\" in input\n const read = normalizeVisibleSourceTypes(isDimensions ? input.read : input)\n // ❗ 実行時に届く値は型より広い (JS 消費側・外部設定由来)。宣言を nullish 込みに広げて\n // 「undefined 以外の nullish」を下で明示的に扱う\n const rawComment: readonly string[] | null | undefined = isDimensions ? input.comment : undefined\n // `comment` 省略時はコメント次元を `read` に従わせる (単一次元指定と同義・現行互換)。\n // ❗ `undefined` **だけ**がこの綴り。他の nullish を同じ意味にすると綴りが 2 つになる\n if (rawComment === undefined) {\n return new DailyReportViewerScope(read, read)\n }\n const comment = normalizeVisibleSourceTypes(rawComment)\n // ❗ `read` との積集合へ狭める。コメントは必ず read で絞られた詳細の内側にしか載らないため、\n // read 外のコメントトークンは意味を持たない。ここで落としておくと、後続の全判定が\n // 「commentable ⊆ visible」を前提にできる (設定ミスが可視性を広げる経路が消える)\n const narrowed = new Set<string>()\n // ❗ `?? []` は死んだ分岐ではない。`normalizeVisibleSourceTypes` は nullish を**無制限**へ\n // 写すため、契約違反の `comment: null` をそのまま採ると read より広い集合が生まれる\n // (fail-open)。ここで空反復へ倒し、全拒否 = fail-close に揃える\n for (const token of comment ?? []) {\n if (read.has(token)) {\n narrowed.add(token)\n }\n }\n return new DailyReportViewerScope(read, narrowed)\n }\n\n /**\n * Builds an unrestricted scope; every call site is an audited fail-open.\n * 無制限の閲覧範囲を生成する処理。全呼び出し箇所が監査対象の fail-open となる。\n *\n * ❗ 理由文字列は必須。読み捨てるが、呼び出し箇所の全数列挙と「なぜここは絞らなくてよいか」の\n * 記録を強制するために受け取る。\n *\n * ❗ **両次元**を無制限にする。片方だけ残すと「無制限」という名前が嘘になり、監査 grep の\n * 結果が「絞られていない読み取りの全数」でなくなる。\n *\n * @param auditReason Why unrestricted access is correct here. なぜここで無制限が正当かの理由。\n * @returns The unrestricted scope. 無制限の閲覧範囲。\n */\n static unrestricted(auditReason: string): DailyReportViewerScope {\n void auditReason\n return new DailyReportViewerScope(null, null)\n }\n}\n\n/**\n * Computes the cache-key digest for a whole viewer scope (all dimensions).\n * 閲覧範囲全体 (全次元) からキャッシュキー用ダイジェストを算出する処理。\n *\n * スコープに次元が増えてもキャッシュキーの生成点を 1 つに保つための入口。\n *\n * ❗ **必ず全次元を畳み込むこと。** 本文次元だけを畳み込むと、コメント権限だけを剥奪された\n * 同一ユーザーが剥奪前のキャッシュバケットを共有し続け、TTL の間コメント本文入りの詳細を\n * 受け取り続ける (キーが変わらないため無効化も走らない)。\n *\n * 両次元が一致する既定形 (コメント次元が `read` に従う) では**本文のみのダイジェストと同一値**を\n * 返す。既存配備のキャッシュキーは 1 文字も変わらず、キャッシュ再利用率は無退行となる。\n * 次元が分かれるときだけ `d:` 名前空間の下へ**長さ接頭辞付きで連結**するため、写像は単射であり\n * sentinel (`all` / `none`) や単一次元表記 (`s:` / `sh:`) と衝突しない。\n *\n * @param scope Viewer scope. 閲覧範囲。\n * @returns Digest usable as a cache-key component. キャッシュキー構成要素として使えるダイジェスト。\n */\nexport function computeViewerScopeDigest(scope: DailyReportViewerScope): string {\n const read = computeSourceTypeDigest(scope.visible)\n const comment = computeSourceTypeDigest(scope.commentable)\n // 既定 (コメント次元が read に従う) では現行キーと完全同一にする\n if (read === comment) {\n return read\n }\n // ❗ 長さ接頭辞が必須。素の連結では (\"s:x\", \"y\") と (\"s:xy\", \"\") のような異なる組が\n // 同じ文字列になりうる (単射でなくなり、別権限の閲覧者が同一バケットを共有する)\n return `d:${read.length}:${read}${comment}`\n}\n\n/**\n * Computes a stable and bounded cache-key digest of the set.\n * 可視集合から安定・有界なキャッシュキー用ダイジェストを算出する処理。\n *\n * 無制限は `\"all\"`、空集合は `\"none\"`、それ以外は `s:` 名前空間の下へ**長さ接頭辞付きトークンを昇順連結**する。\n * 連結は長さ接頭辞付きなので**単射**であり、通常経路 (`s:`) では論理的に等価な集合が必ず同一の、\n * 異なる集合が必ず別のダイジェストになる。sentinel (`all` / `none`) とも名前空間が分かれるため、\n * 限定集合が無制限 / 全拒否と同じバケットを共有することはない。権限が安定している間はキーが\n * 不変になるため、キャッシュは完全に再利用される (性能無退行)。\n *\n * ❗ 区分数が異常に多い場合だけキー長を抑えるため FNV-1a 32bit へ退避し `sh:` 接頭辞を付ける。\n * **この退避経路に限り理論上の衝突がありうる** (32bit)。衝突しても影響は同一ユーザーの\n * キャッシュ再利用に留まり、認可判定そのものは SQL 述語が担うため漏洩には至らない。\n * 通常の区分数 (数個) では到達しない。\n *\n * @param set Normalized visible set. 正規化済みの可視集合。\n * @returns Digest usable as a cache-key component. キャッシュキー構成要素として使えるダイジェスト。\n */\nexport function computeSourceTypeDigest(set: VisibleSourceTypeSet): string {\n if (set === null) {\n return \"all\"\n }\n if (set.size === 0) {\n return \"none\"\n }\n // 昇順ソートで要素順に依存しないキーにする (同じ集合は常に同じダイジェスト)。\n // ❗ 各トークンは長さ接頭辞付きで連結する。素の区切り文字だけでは、区切り文字を含む\n // トークンが別の集合と同じ連結結果になりうる (単射でなくなる)\n const joined = [...set]\n .sort()\n .map((token) => `${token.length}:${token}`)\n .join(DIGEST_SEPARATOR)\n // ❗ 限定集合の名前空間を `s:` で分ける。分けないと、`\"all\"` や `\"none\"` へ畳み込まれる\n // トークンだけを許された閲覧者が、無制限 / 全拒否と同じキャッシュバケットを共有してしまう\n if (joined.length <= DIGEST_RAW_MAX_LENGTH) {\n return `s:${joined}`\n }\n // 長大キーの退避のみハッシュ化する。衝突しても影響は同一ユーザーのキャッシュ再利用に限られ、\n // かつ TTL で失効するが、生連結と混同しないよう接頭辞で区別する\n let hash = 0x811c9dc5\n for (let i = 0; i < joined.length; i++) {\n hash ^= joined.charCodeAt(i)\n hash = Math.imul(hash, 0x01000193)\n }\n return `sh:${(hash >>> 0).toString(16)}`\n}\n","/**\n * React Router loader/action factories for the daily-report HTTP surface.\n * 日報 HTTP サーフェス向けの React Router loader / action 工場。\n *\n * - index.loader: 画面ルートの認証 + 難読化ユーザー ID 解決\n * - api.loader / api.action: `:endpoint` パラメータ式の認証付き API ルーター\n * - sse.loader: Redis Streams ベースのリアルタイム更新 SSE エンドポイント\n */\nimport { normalizeBusinessDateKey } from \"../shared/business-date\"\nimport { computeIdsStreamSnapshotToken, type DailyReportIdsStreamChunkLine, type DailyReportIdsStreamCursor, type DailyReportIdsStreamErrorLine, findIdsStreamResumeIndex } from \"../shared/ids-stream\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport { type DailyReportSseMessage, dailyReportSseMessageSchema } from \"../shared/sse-schema\"\nimport type { DailyReportDetail, DailyReportItem } from \"../shared/types\"\nimport type { DailyReportAttachmentFailure, DailyReportAuthenticate, DailyReportEncodeUserId, DailyReportIdCodec, DailyReportReadAttachment, DailyReportRedisProvider, DailyReportResolveVisibleSourceTypes, DailyReportVisibilityResolveReason } from \"./ports\"\nimport { jsonResponseWithETag } from \"./response\"\nimport type { DailyReportService } from \"./service\"\nimport type { DailyReportSseReader, StreamEntry } from \"./sse-reader\"\nimport { isStreamIdLte } from \"./sse-reader\"\nimport { DailyReportViewerScope, isSourceTypeVisible, type VisibleSourceTypeSet } from \"./visibility\"\n\ntype LoaderArgs = { request: Request; params: Record<string, string | undefined> }\n\n/** `data()` 相当の JSON レスポンス生成 (react-router 非依存)。`setCookie` 指定時は Set-Cookie を付与する。 */\nconst jsonData = (payload: unknown, init?: { status?: number; setCookie?: string }): Response => {\n const headers = new Headers({ \"Content-Type\": \"application/json\" })\n // セッション破棄の Set-Cookie は未認証応答にも載せる (捨てると無効セッションがブラウザに残る)\n if (init?.setCookie) {\n headers.append(\"Set-Cookie\", init.setCookie)\n }\n return new Response(JSON.stringify(payload), { status: init?.status ?? 200, headers })\n}\n\n// ---------------- ids NDJSON ストリームの定数とヘルパー (純粋・モジュールスコープ) ----------------\n\n/**\n * First-chunk size, tuned for time-to-first-render.\n * 初回チャンクのアイテム数。最初の描画までの時間を優先して小さく保つ。\n *\n * 一覧の可視行 + オーバースキャンを一度に満たせる件数であればよい。\n */\nconst IDS_STREAM_FIRST_CHUNK_SIZE = 30\n\n/**\n * 2 チャンク目以降のアイテム数。1 アイテムが数十バイト (id/日付/種別のみ) と軽いため\n * 大きめに運ぶ。実データは 20 万件超に達するため、小さすぎると行数 (= pull 往復と\n * クライアント側 publish 回数) が肥大する (4000 件 ≈ 240 KB/行、20 万件で約 50 行)。\n */\nconst IDS_STREAM_NEXT_CHUNK_SIZE = 4000\n\n/**\n * Sanitized message for mid-stream failures (no internal details).\n * ストリーム送出中の失敗をクライアントへ伝える定型文 (内部情報は載せない)。\n */\nconst IDS_STREAM_ERROR_CLIENT_MESSAGE = \"stream interrupted by server error\"\n\n/** 再開カーソルの営業日として受け付ける唯一の形式 (正規化済み YYYY-MM-DD)。 */\nconst IDS_STREAM_DATE_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\n\n/**\n * Heartbeat interval while the next chunk is pending (ms).\n * 次チャンク待ちの間に空行ハートビートを送る間隔 (ms)。\n *\n * 全量クエリ (20 万件超) の確定待ちで無音が続くと、クライアントの stall ウォッチドッグ\n * (既定 30s) が生きた接続を half-open と誤認する。それより十分短い間隔で空行を送り、\n * read を解決させて誤検知を防ぐ。空行はクライアント側で読み飛ばされる (`trim() === \"\"`)。\n */\nconst IDS_STREAM_HEARTBEAT_MS = 15_000\n\n/** next() 待ちがハートビート間隔に負けたことを表す番兵。 */\nconst IDS_STREAM_HEARTBEAT = Symbol(\"ids-stream-heartbeat\")\n\n/**\n * Keep-alive ticks between SSE visibility re-resolutions.\n * SSE の可視集合を再解決する間隔 (keep-alive tick 数)。\n *\n * keep-alive は 5 秒間隔なので 12 tick = 約 60 秒。読み取り経路がリクエストごとに再解決するのに対し\n * SSE は接続が長寿命なため、この間隔が権限失効の反映遅延の上限になる。\n */\nconst SSE_VISIBILITY_REFRESH_TICKS = 12\n\n/**\n * Parses and validates resume-cursor query params (fail-closed).\n * 再開カーソルのクエリパラメータを検証付きで解釈する処理 (不正は fail-closed)。\n *\n * - `cursor_report_hub_id` 単独 = 「営業日なし領域」のカーソル (businessDate: null)。\n * - `cursor_business_date` は `cursor_report_hub_id` 無しでは意味を持たないため 400。\n *\n * @param url Request URL. リクエスト URL。\n * @returns Cursor (null = 先頭から) か、検証失敗の印。\n */\n/** スナップショットトークンの形式 (computeIdsStreamSnapshotToken の出力域)。 */\nconst IDS_STREAM_SNAPSHOT_PATTERN = /^[0-9a-f]{1,16}$/\n\nconst parseIdsStreamCursor = (url: URL): { cursor: DailyReportIdsStreamCursor | null; snapshot: string | null } | { invalid: true } => {\n const rawId = url.searchParams.get(\"cursor_report_hub_id\")\n const rawDate = url.searchParams.get(\"cursor_business_date\")\n const rawSnapshot = url.searchParams.get(\"cursor_snapshot\")\n // ❗ トークンの形式違反は 400 にしない。「トークン無し」へ落とせば先頭からの再送で\n // 回復できる — 再開の失敗はデータ喪失ではなく、接続ごと落とすより常に安全側\n const snapshot = rawSnapshot !== null && IDS_STREAM_SNAPSHOT_PATTERN.test(rawSnapshot) ? rawSnapshot : null\n\n if (rawId === null) {\n // 営業日だけのカーソルは並び順上の位置を決められない\n return rawDate === null ? { cursor: null, snapshot } : { invalid: true }\n }\n\n const reportHubId = Number.parseInt(rawId, 10)\n if (!Number.isSafeInteger(reportHubId) || String(reportHubId) !== rawId || reportHubId <= 0) {\n return { invalid: true }\n }\n\n if (rawDate === null) {\n return { cursor: { businessDate: null, reportHubId }, snapshot }\n }\n if (!IDS_STREAM_DATE_PATTERN.test(rawDate)) {\n return { invalid: true }\n }\n return { cursor: { businessDate: rawDate, reportHubId }, snapshot }\n}\n\n/**\n * Emits a finalized id array as chunked NDJSON lines from a resume position.\n * 確定済み ID 配列を再開位置からチャンク行として送出するジェネレーター。\n *\n * `total` は最初に yield する行へ載せる (= 接続内で全量が確定した最初の行。\n * キャッシュヒットの新規接続と再開接続では先頭行、高速先頭ページ経路では 2 行目)。\n *\n * @param items Stream-ordered full array. ストリーム順の全量配列。\n * @param resumeKey Resume position (null = 先頭から)。\n * @param firstChunkSize このジェネレーターの最初のチャンクサイズ。\n */\nasync function* streamLinesFromFullArray(items: DailyReportItem[], resumeKey: DailyReportIdsStreamCursor | null, firstChunkSize: number, options: { snapshot: string; restarted?: boolean }): AsyncGenerator<DailyReportIdsStreamChunkLine> {\n let position = resumeKey ? findIdsStreamResumeIndex(items, resumeKey) : 0\n let isFirstYield = true\n while (true) {\n const chunkItems = items.slice(position, position + (isFirstYield ? firstChunkSize : IDS_STREAM_NEXT_CHUNK_SIZE))\n position += chunkItems.length\n const isLast = position >= items.length\n const lastItem = chunkItems[chunkItems.length - 1]\n yield {\n kind: \"chunk\",\n items: chunkItems,\n nextCursor: isLast || !lastItem ? null : { businessDate: lastItem.businessDate, reportHubId: lastItem.reportHubId },\n isLast,\n ...(isFirstYield ? { total: items.length } : {}),\n // 権威行は必ずトークンを運ぶ (resume 経路も含む)。載せ忘れると最初の再開成功で\n // クライアントのトークンが失われ、2 度目以降の切断が全量再送へ退化する\n snapshot: options.snapshot,\n // restarted は先頭行にのみ載せる。全行に載せるとクライアントが行ごとに初期化し、\n // 直前の行で受けたデータを捨てる\n ...(isFirstYield && options.restarted ? { restarted: true } : {}),\n }\n isFirstYield = false\n if (isLast) return\n }\n}\n\n/**\n * Wraps a line iterator into a pull-based NDJSON streaming Response.\n * 行イテレーターを pull 駆動の NDJSON ストリーミング Response へ包む処理。\n *\n * - 切断検知時はイテレーターを畳んで (`return()`) DB 待ちを解放し、close する\n * (素の return は desiredSize > 0 のまま pull が再帰する)。\n * - イテレーターの throw は内部情報を含まない定型エラー行 + 正常クローズへ倒す\n * (クライアントは「isLast 無し終端」をカーソル再開のシグナルとして扱う)。\n */\nconst buildIdsStreamResponse = (cookie: string | null, lines: AsyncGenerator<DailyReportIdsStreamChunkLine>, request: Request, logger: DailyReportLogger): Response => {\n const encoder = new TextEncoder()\n // cancel() の lines.return() は「pull が DB 待ちの next() を保留中」だと先取りできない。\n // 待ちが明けた後の enqueue が閉じたストリームへ落ちて catch に入るため、切断は\n // フラグで決定的に識別し、通常イベントとして静かに畳む (ERROR ログにしない —\n // 全量クエリ中の離脱は日常イベントで、本物の送出失敗が雑音に埋もれる)\n let cancelled = false\n // next() の解決はハートビートを跨いで保持する。ハートビートが先着したら空行を出し、\n // 同じ next() を次の pull で await し直す (毎回 next() を呼び直すと二重取得になる)。\n let pendingNext: Promise<IteratorResult<DailyReportIdsStreamChunkLine>> | null = null\n const stream = new ReadableStream<Uint8Array>({\n async pull(controller) {\n if (cancelled || request.signal.aborted) {\n await lines.return(undefined).catch(() => {})\n try {\n controller.close()\n } catch {\n // cancel 済みコントローラーの close は失敗してよい\n }\n return\n }\n let heartbeatTimer: ReturnType<typeof setTimeout> | null = null\n try {\n if (pendingNext === null) {\n pendingNext = lines.next()\n }\n // 次チャンクの確定待ちが長引く間、接続の生存を示す空行を送る\n const heartbeatPromise = new Promise<typeof IDS_STREAM_HEARTBEAT>((resolve) => {\n heartbeatTimer = setTimeout(() => resolve(IDS_STREAM_HEARTBEAT), IDS_STREAM_HEARTBEAT_MS)\n })\n const raced = await Promise.race([pendingNext, heartbeatPromise])\n if (heartbeatTimer !== null) {\n clearTimeout(heartbeatTimer)\n heartbeatTimer = null\n }\n if (raced === IDS_STREAM_HEARTBEAT) {\n // next() はまだ保留中。空行だけ送って pending は次の pull へ持ち越す\n if (cancelled || request.signal.aborted) {\n await lines.return(undefined).catch(() => {})\n try {\n controller.close()\n } catch {\n // cancel 済みコントローラーの close は失敗してよい\n }\n return\n }\n controller.enqueue(encoder.encode(\"\\n\"))\n return\n }\n pendingNext = null\n const { value, done } = raced\n if (done) {\n controller.close()\n return\n }\n // 保留中の next() の間に切断された場合はここで検知して静かに終える\n if (cancelled || request.signal.aborted) {\n await lines.return(undefined).catch(() => {})\n try {\n controller.close()\n } catch {\n // cancel 済みコントローラーの close は失敗してよい\n }\n return\n }\n controller.enqueue(encoder.encode(`${JSON.stringify(value)}\\n`))\n } catch (error) {\n if (heartbeatTimer !== null) {\n clearTimeout(heartbeatTimer)\n }\n // next() が throw した = このイテレーターは畳む。保留参照を残さない\n pendingNext = null\n if (cancelled || request.signal.aborted) {\n // 切断起因の enqueue/close 失敗はエラーではない\n return\n }\n logger.error(`ids-stream emission failed: ${error instanceof Error ? error.message : String(error)}`)\n try {\n // ❗ 素のリテラルを stringify に渡さない。JSON.stringify は any を受けるため、\n // 型を束縛しないとワイヤユニオンの `kind` 要求がここだけ素通りする\n const errorLine: DailyReportIdsStreamErrorLine = { kind: \"error\", error: IDS_STREAM_ERROR_CLIENT_MESSAGE }\n controller.enqueue(encoder.encode(`${JSON.stringify(errorLine)}\\n`))\n controller.close()\n } catch {\n // 既に切断済みなら何もできない\n }\n }\n },\n cancel() {\n cancelled = true\n void lines.return(undefined).catch(() => {})\n },\n })\n\n const headers = new Headers({\n \"Content-Type\": \"application/x-ndjson; charset=utf-8\",\n // no-transform: compression ミドルウェア/中間プロキシによるバッファリングを禁止する (SSE と同じ理由)\n \"Cache-Control\": \"private, no-cache, no-transform\",\n \"X-Content-Type-Options\": \"nosniff\",\n })\n if (cookie) {\n headers.append(\"Set-Cookie\", cookie)\n }\n return new Response(stream, { status: 200, headers })\n}\n\n// ---------------- 添付配信の定数とヘルパー (純粋・モジュールスコープ) ----------------\n\n/** 添付 1 件あたりの既定上限。サービス側のワイヤ上限 (既定 64 MiB) より必ず低く保つ。 */\nconst DEFAULT_ATTACHMENT_MAX_BYTES = 32 * 1024 * 1024\n\n/**\n * Default per-user rate limit (calls per minute), enforced per process.\n * ユーザー単位の既定レート上限 (1 分あたり)。ただし**プロセス単位**で計上する。\n *\n * バケットはハンドラーのクロージャに置く素の `Map` であり、プロセス間で共有されない。\n * 消費アプリが Node Cluster 等で複数ワーカーを起動する場合、コンテナ全体の実効上限は\n * この値のワーカー数倍になる。全体で厳密に効かせたいなら共有ストア (Redis 等) が要る。\n */\nconst DEFAULT_ATTACHMENT_RATE_LIMIT_PER_MINUTE = 60\n\n/**\n * Default simultaneous-read limit, enforced per process.\n * 既定の同時実行上限。**プロセス単位**で計上する。\n *\n * 1 リクエストが上限バイト数をヒープへ載せるため小さく保つ。ヒープ見積りは\n * 「この値 × `attachmentMaxBytes` × ワーカー数」であり、ワーカー数を掛け忘れると\n * コンテナのメモリ上限を実際の数倍で見誤る。消費アプリはワーカー数を織り込んだ値を\n * `attachmentConcurrency` で明示注入すること。\n *\n * スロットは**本文をクライアントへ送出し終えるまで**保持する。読み取り完了時点で\n * 解放すると、前の応答のバイト列がヒープに載ったまま次の読み取りが始まるため、\n * 上の見積りが成立しなくなる (実際の同時保持数は無制限になる)。\n */\nconst DEFAULT_ATTACHMENT_CONCURRENCY = 4\n\n/**\n * Backstop for releasing a concurrency slot when the client never drains the body.\n * クライアントが本文を読み切らない場合に同時実行スロットを解放するバックストップ。\n *\n * スロットを送出完了まで保持する以上、接続を張ったまま読み止めたクライアントが\n * スロットを永久に占有できてしまう。これは締め切りではなく枯渇防止の保険であり、\n * 超過しても応答は中断しない (単にゲートの会計上、保持されていない扱いにするだけ)。\n */\nconst ATTACHMENT_BODY_FLUSH_TIMEOUT_MS = 120_000\n\n/** レート制限テーブルの上限件数。無制限 Map はプロセス寿命の間だけ単調増加するリークになる。 */\nconst RATE_LIMITER_MAX_ENTRIES = 1024\n\n/** レート上限超過時に提示する再試行間隔 (秒)。 */\nconst RATE_LIMIT_RETRY_AFTER_SECONDS = 60\n\n/** 同時実行上限に達したときに提示する再試行間隔 (秒)。 */\nconst CONCURRENCY_RETRY_AFTER_SECONDS = 5\n\n/**\n * Media types allowed to render inline in the browser.\n * ブラウザーへインライン表示してよい media type。\n *\n * これ以外は必ず添付ダウンロードにし、Content-Type も `application/octet-stream` へ落とす。\n * `file_type` は外部システム由来の未検証値であり、`text/html` や `image/svg+xml` を\n * 自オリジンでインライン配信するとセッション Cookie を持つ文脈でスクリプトが動く。\n * `X-Content-Type-Options: nosniff` は「宣言型から外れた推測」を止めるだけで、\n * 宣言された型の実行は止めない。\n */\nconst INLINE_SAFE_MEDIA_TYPES: ReadonlySet<string> = new Set([\"image/png\", \"image/jpeg\", \"image/gif\", \"image/webp\", \"application/pdf\", \"text/plain\"])\n\n/** ヘッダー値として安全で、かつ MIME 型の形をしている値だけを通す。 */\nconst HEADER_SAFE_PATTERN = /^[\\x20-\\x7E]+$/\nconst MEDIA_TYPE_PATTERN = /^[\\w.+-]+\\/[\\w.+-]+/\n\n/**\n * Accepts a media type only when it is header-safe and well-formed.\n * ヘッダー安全かつ MIME 型の形をしている場合のみ採用する処理。\n *\n * CR/LF や 0x00-0xFF 外の文字が混ざった値を `new Headers()` へ渡すと TypeError になり、\n * 認可済みのダウンロードが 500 になる。\n *\n * **これは第 2 層である。** 現在の配信経路では、注入値はインライン許可リストに完全一致しないため\n * 必ず `application/octet-stream` へ落ち、ヘッダーには到達しない。つまり本関数を外しても\n * 現状の挙動は変わらない (ミューテーションテストで確認済み)。\n * 許可判定を前方一致や「DB の型を信じる」形へ変えた瞬間に効き始める防御なので、\n * 到達不能なまま放置せず **直接の単体テストで固定する**目的でエクスポートしている。\n *\n * @param value Candidate media type. 候補となる media type。\n * @returns The value when acceptable, otherwise null. 採用可なら値、それ以外は null。\n */\nexport const sanitizeMediaType = (value: string | null | undefined): string | null => (value && HEADER_SAFE_PATTERN.test(value) && MEDIA_TYPE_PATTERN.test(value) ? value : null)\n\n/**\n * Percent-encodes a file name for the RFC 8187 `filename*` parameter.\n * RFC 8187 の `filename*` 用にファイル名をパーセントエンコードする処理。\n *\n * `encodeURIComponent` は `'` `(` `)` `*` を残すが、いずれも RFC 8187 の attr-char ではない。\n *\n * @param name Raw file name. 生のファイル名。\n * @returns Encoded value. エンコード済みの値。\n */\nexport const encodeRfc8187 = (name: string): string => encodeURIComponent(name).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)\n\n/**\n * Builds the ASCII fallback used by the plain `filename` parameter.\n * 素の `filename` パラメータ用の ASCII フォールバックを組み立てる処理。\n *\n * @param name Raw file name. 生のファイル名。\n * @returns ASCII-only, quote-free name. ASCII のみで引用符を含まない名前。\n */\nexport const asciiFallbackFileName = (name: string): string => name.replace(/[^\\x20-\\x7E]/g, \"_\").replace(/[\"\\\\]/g, \"_\")\n\n/**\n * Maps a storage read failure onto the HTTP status the endpoint returns.\n * ストレージ読み取りの失敗種別を、エンドポイントが返す HTTP ステータスへ写像する処理。\n *\n * `not_found` と `denied` はどちらも 404 にする。認可判定は既に SQL 側で完了しているため\n * 横断的な情報漏洩は無いが、実体の有無を 403/404 で区別すると存在オラクルになる。\n *\n * @param reason Failure kind reported by the port. ポートが報告した失敗種別。\n * @returns HTTP status code. HTTP ステータスコード。\n */\nconst attachmentFailureToStatus = (reason: DailyReportAttachmentFailure): number => {\n switch (reason) {\n case \"not_found\":\n case \"denied\":\n case \"invalid_path\":\n return 404\n case \"too_large\":\n return 413\n default:\n return 502\n }\n}\n\n/**\n * Client-facing messages per failure status.\n * 失敗ステータスごとのクライアント向けメッセージ。\n *\n * 404 は「存在しない」「認可されない」「実体が消えた」の合流点なので、\n * どの経路から来ても同一の文面にする (差分が存在オラクルになるため)。\n */\nconst ATTACHMENT_FAILURE_MESSAGES: Readonly<Record<number, string>> = {\n 404: \"Attachment not found\",\n 413: \"Attachment too large\",\n 502: \"Attachment storage unavailable\",\n}\n\n/** レート制限 1 ユーザー分の状態。 */\ntype RateBucket = { tokens: number; lastRefillMs: number }\n\n/**\n * Creates a bounded per-user token-bucket rate limiter (process-local).\n * ユーザー単位のトークンバケット方式レート制限を、件数上限付きで生成する処理 (プロセス内限定)。\n *\n * 上限を設ける理由: 素の `Map` で保持するとプロセス寿命の間だけ単調増加し、\n * 「サーバー側の状態は上限を持つ」という設計方針 (SqlResultCache の >500 GC) に反する。\n * 上限到達時は挿入順が最も古いエントリから落とす。\n *\n * 状態はプロセス内に閉じるため、マルチワーカー配備では実効上限がワーカー数倍になる\n * (`DEFAULT_ATTACHMENT_RATE_LIMIT_PER_MINUTE` 参照)。\n *\n * @param limitPerMinute Allowed calls per minute. 1 分あたりの許可回数。\n * @returns A function that consumes one token and reports whether it was allowed. トークンを 1 つ消費し可否を返す関数。\n */\nconst createRateLimiter = (limitPerMinute: number) => {\n const buckets = new Map<number, RateBucket>()\n return (userId: number, nowMs: number): boolean => {\n const existing = buckets.get(userId)\n // アクセスのたびに再挿入して LRU 順序にする (Map は挿入順を保つ)\n if (existing) buckets.delete(userId)\n\n const bucket = existing ?? { tokens: limitPerMinute, lastRefillMs: nowMs }\n const elapsedMs = Math.max(0, nowMs - bucket.lastRefillMs)\n if (elapsedMs > 0) {\n bucket.tokens = Math.min(limitPerMinute, bucket.tokens + (elapsedMs * limitPerMinute) / 60_000)\n bucket.lastRefillMs = nowMs\n }\n\n const allowed = bucket.tokens >= 1\n if (allowed) bucket.tokens -= 1\n\n buckets.set(userId, bucket)\n while (buckets.size > RATE_LIMITER_MAX_ENTRIES) {\n const oldest = buckets.keys().next()\n if (oldest.done) break\n buckets.delete(oldest.value)\n }\n return allowed\n }\n}\n\n/**\n * Creates a fail-fast concurrency gate (no queueing, process-local).\n * 待ち行列を持たない即時失敗型の同時実行ゲートを生成する処理 (プロセス内限定)。\n *\n * 待たせるとリクエストを掴んだまま滞留するだけなので、上限超過は即座に 503 を返す。\n * カウンタはプロセス内に閉じるため、マルチワーカー配備ではコンテナ全体の同時読み取り数が\n * 上限のワーカー数倍になる (`DEFAULT_ATTACHMENT_CONCURRENCY` 参照)。\n *\n * @param limit Maximum simultaneous holders. 同時保持数の上限。\n * @returns Acquire/release pair. 取得と解放の組。\n */\nconst createConcurrencyGate = (limit: number) => {\n let active = 0\n return {\n tryAcquire: (): boolean => {\n if (active >= limit) return false\n active += 1\n return true\n },\n release: (): void => {\n if (active > 0) active -= 1\n },\n }\n}\n\n/**\n * Streams already-materialized bytes and releases the held slot once the body is drained.\n * materialize 済みのバイト列をストリームとして送出し、送出完了時に保持中のスロットを解放する処理。\n *\n * 読み取り完了時点で解放してはならない。その時点でバイト列はまだヒープ上にあり、\n * 応答が流し切られるまで参照が保持されるため、「同時実行上限 × 上限バイト数」という\n * ヒープ見積りが成立しなくなる。ここで送出完了まで保持することで見積りを実際に成立させる。\n *\n * 解放は必ず 1 回だけ行う (正常終了・キャンセル・バックストップのいずれか最初の 1 回)。\n *\n * @param bytes Body bytes to send. 送出する本文のバイト列。\n * @param release Idempotent slot release. 冪等なスロット解放関数。\n * @param timeoutMs Backstop before force-releasing. 強制解放までのバックストップ時間。\n * @returns A stream that emits the bytes once. バイト列を 1 度だけ流すストリーム。\n */\nconst streamAndRelease = (bytes: Uint8Array<ArrayBuffer>, release: () => void, timeoutMs: number): ReadableStream<Uint8Array> => {\n const timer: ReturnType<typeof setTimeout> = setTimeout(release, timeoutMs)\n // タイマーがプロセス終了を待たせないようにする (Node 以外では unref を持たない)\n ;(timer as unknown as { unref?: () => void }).unref?.()\n const finish = () => {\n clearTimeout(timer)\n release()\n }\n\n let sent = false\n return new ReadableStream<Uint8Array>({\n pull(controller) {\n // 1 回目の pull で本文を積み、消費された後の 2 回目の pull で閉じて解放する\n if (sent) {\n controller.close()\n finish()\n return\n }\n sent = true\n controller.enqueue(bytes)\n },\n cancel() {\n finish()\n },\n })\n}\n\n/**\n * Decides whether an SSE message is visible under the viewer's source-type set.\n * SSE メッセージが閲覧者の可視 sourceType 集合において可視かを判定する処理。\n *\n * - `connected` / `status-update`: 常に可視。前段で本人限定済みであり、区分も本文も載らない。\n * - `report-create` / `report-update` / `report-publish`: 埋め込み `report.sourceType` で判定する。\n * 値が欠落していれば `isSourceTypeVisible` の規則により制限閲覧者には不可視 (fail-close)。\n * - `comment-add` / `comment-delete`: 新規コメントは `addComment` の Internal 限定ゲートを通るため\n * 構造上 Internal のみ。明示 `sourceType` があればそれを使い、無い旧エントリは `\"Internal\"` として扱う。\n * - `report-delete`: 明示 `sourceType` で判定し、値の無い旧エントリは fail-close で落とす\n * (削除の取りこぼしは次回取得で自己修復する良性の劣化)。\n *\n * ❗ `comment-add` / `comment-delete` は**コメント次元**で判定する。本文次元で判定すると、\n * 「本文は読めるがコメントは読めない」閲覧者へコメント本文が届く。\n *\n * @param parsed Validated SSE message. 検証済みの SSE メッセージ。\n * @param raw Raw payload carrying server-only fields. サーバー専用フィールドを含む生ペイロード。\n * @param visible Normalized visible source-type set (body dimension). 正規化済みの可視 sourceType 集合 (本文次元)。\n * @param commentable Normalized commentable source-type set. 正規化済みのコメント可視 sourceType 集合。\n * @returns True when the message may be delivered. 配信してよいなら true。\n */\nconst isSseMessageSourceTypeVisible = (parsed: DailyReportSseMessage, raw: Record<string, unknown>, visible: VisibleSourceTypeSet, commentable: VisibleSourceTypeSet): boolean => {\n // 両次元とも無制限なら種別を問わず配信する (現行互換の高速経路)\n if (visible === null && commentable === null) {\n return true\n }\n const rawSourceType = typeof raw.sourceType === \"string\" ? raw.sourceType : undefined\n switch (parsed.type) {\n case \"connected\":\n case \"status-update\":\n return true\n case \"report-create\":\n case \"report-update\":\n case \"report-publish\":\n // 本文次元だけで配信可否を決める。コメント面はフィールド単位で落とす\n // (メッセージごと落とすと日報更新そのものが届かなくなる)\n return isSourceTypeVisible(visible, parsed.report.sourceType)\n case \"comment-add\":\n case \"comment-delete\":\n return isSourceTypeVisible(commentable, rawSourceType ?? \"Internal\")\n case \"report-delete\":\n return isSourceTypeVisible(visible, rawSourceType)\n }\n}\n\n/**\n * Empties the comment arrays of a `report-*` payload when the viewer may not read comments.\n * コメントを読めない閲覧者向けに `report-*` ペイロードのコメント配列を空にする処理。\n *\n * `report-create` / `report-update` / `report-publish` は `report: dailyReportDetailSchema` を\n * 埋め込んでおり、その detail は `externalComments` と `postedComments` を必須キーとして含む。ペイロードは\n * 発行時に**無制限スコープ**で構築されて Redis Stream へ載る共有物であるため、閲覧者ごとの\n * 絞り込みはこの配信直前が唯一の機会になる。\n *\n * ❗ キーを**省いてはならない**。必須キーの欠落は受信側の `safeParse` を失敗させ、メッセージ全体が\n * fail-close で捨てられる (日報更新そのものが永久に届かなくなる)。必ず空配列で上書きする。\n *\n * ❗ `raw` は購読者ごと・エントリごとに `JSON.parse` された固有オブジェクトなので、その場で\n * 書き換えてよい (他の購読者へ波及しない)。\n *\n * ❗ 戻り値は捨てないこと。配信側は元の生文字列 (`fields.data`) をそのまま送る高速経路を持って\n * おり、秘匿したのに再直列化しないと**書き換えが無かったことになる**。\n *\n * @param parsed Validated SSE message. 検証済みの SSE メッセージ。\n * @param raw Raw payload to redact in place. その場で秘匿する生ペイロード。\n * @param commentable Normalized commentable source-type set. 正規化済みのコメント可視 sourceType 集合。\n * @returns True when the payload was modified. ペイロードを書き換えたなら true。\n */\n/**\n * The two comment faces of a detail payload, as wire keys.\n * 詳細ペイロードのコメント 2 面 (ワイヤキー)。\n *\n * ❗ 文字列キーで直接書かない。秘匿対象の生ペイロードは `Record<string, unknown>` なので、\n * 素の文字列は改名を tsc が検知できず、秘匿が無音で no-op になる (= コメント本文が権限の\n * 無い閲覧者へ漏洩する)。`satisfies keyof` で型から導出しておけば、次の改名はこの定数を\n * コンパイルエラーにして追従を強制する。\n */\nconst COMMENT_FACE_KEYS = [\"externalComments\", \"postedComments\"] as const satisfies readonly (keyof DailyReportDetail)[]\n\nconst redactSseReportComments = (parsed: DailyReportSseMessage, raw: Record<string, unknown>, commentable: VisibleSourceTypeSet): boolean => {\n if (commentable === null) {\n return false\n }\n if (parsed.type !== \"report-create\" && parsed.type !== \"report-update\" && parsed.type !== \"report-publish\") {\n return false\n }\n if (isSourceTypeVisible(commentable, parsed.report.sourceType)) {\n return false\n }\n const report = raw.report\n if (typeof report !== \"object\" || report === null) {\n return false\n }\n const target = report as Record<string, unknown>\n for (const key of COMMENT_FACE_KEYS) {\n target[key] = []\n }\n return true\n}\n\nexport type DailyReportHandlersConfig = {\n /** リクエスト認証ポート。 */\n authenticate: DailyReportAuthenticate\n /**\n * 可視 sourceType リゾルバ。未注入なら全件 (現行互換)。\n * 全データ経路 (一覧 / 詳細 / コメント / 添付 / SSE) に一貫して効き、例外は全拒否として扱う。\n */\n resolveVisibleSourceTypes?: DailyReportResolveVisibleSourceTypes\n /** データアクセスサービス。 */\n service: DailyReportService\n /** 内部数値 ID の難読化ポート。 */\n encodeUserId: DailyReportEncodeUserId\n /**\n * 添付 ID の難読化コーデック。未注入なら添付エンドポイントは常に 404。\n * ユーザー ID 用とは別インスタンスを渡すこと (ID 空間の分離)。\n */\n attachmentIdCodec?: DailyReportIdCodec\n /** 添付ファイル読み取りポート。未注入なら添付エンドポイントは常に 404。 */\n readAttachment?: DailyReportReadAttachment\n /** 添付ファイルの最大バイト数 (既定 32 MiB)。サービス側のワイヤ上限より低く保つこと。 */\n attachmentMaxBytes?: number\n /** 添付エンドポイントの 1 分あたり呼び出し上限 (ユーザー単位・既定 60)。 */\n attachmentRateLimitPerMinute?: number\n /** 添付読み取りの同時実行上限 (プロセス単位・既定 4)。 */\n attachmentConcurrency?: number\n /** SSE 用 redis プロバイダー (catch-up の xRange / xRevRange に使用)。 */\n redis?: DailyReportRedisProvider\n /** SSE Fan-Out 共有リーダー。 */\n sseReader: DailyReportSseReader\n /** SSE Redis Stream キー。 */\n streamKey: string\n /** 未ログイン時のリダイレクト先 (index.loader 用、既定 \"/auth/login\")。 */\n loginRedirectPath?: string\n /**\n * `intent=clearCache` (全サーバーキャッシュの一括破棄) を許可するか。既定 false。\n * 開発用アフォーダンスであり、有効化すると認証済みユーザーなら誰でも全ワーカーの\n * キャッシュを無制限に破棄できるため、本番では必ず無効のままにする\n * (消費アプリは開発環境でのみ true を注入する)。\n */\n enableDevCacheClear?: boolean\n /** ロガー (既定は console ベース)。 */\n logger?: DailyReportLogger\n}\n\n/**\n * Creates the daily-report loaders/actions bound to the injected dependencies.\n * 注入依存に束縛された日報 loader / action 群を生成する処理。\n */\nexport function createDailyReportHandlers(config: DailyReportHandlersConfig) {\n const { authenticate, service, encodeUserId, redis, sseReader, streamKey } = config\n const loginRedirectPath = config.loginRedirectPath ?? \"/auth/login\"\n const apiLogger = config.logger ?? createLogger(LogLevel.ERROR, \"[DailyReportAPI]\")\n const visibilityLogger = config.logger ?? createLogger(LogLevel.ERROR, \"[DailyReportVisibility]\")\n const sseLogger = config.logger ?? createLogger(LogLevel.INFO, \"[DailyReportSSE]\")\n const attachmentLogger = config.logger ?? createLogger(LogLevel.INFO, \"[DailyReportAttachment]\")\n\n // 添付配信のプロセス内状態。ファクトリ 1 インスタンスにつき 1 組。\n const attachmentMaxBytes = config.attachmentMaxBytes ?? DEFAULT_ATTACHMENT_MAX_BYTES\n const consumeAttachmentRateToken = createRateLimiter(config.attachmentRateLimitPerMinute ?? DEFAULT_ATTACHMENT_RATE_LIMIT_PER_MINUTE)\n const attachmentGate = createConcurrencyGate(config.attachmentConcurrency ?? DEFAULT_ATTACHMENT_CONCURRENCY)\n\n /**\n * Resolves and normalizes the visible source-type set once per request (fail-closed).\n * リクエストごとに可視 sourceType 集合を 1 回だけ解決・正規化する処理 (fail-close)。\n *\n * ポート未注入なら `null` (無制限・現行互換)。ポートが例外を投げたら**空集合 (全拒否)**へ倒す。\n * 認可ストアの障害時に無制限へ倒れないための唯一の防波堤であり、ここを緩めてはならない。\n *\n * @param request Incoming request. 受信したリクエスト。\n * @param reason Why it is being resolved. 解決理由 (SSE の定期再解決は \"refresh\")。\n * @returns Normalized visible set. 正規化済みの可視集合。\n */\n const resolveViewerScope = async (request: Request, reason: DailyReportVisibilityResolveReason = \"initial\"): Promise<DailyReportViewerScope> => {\n const port = config.resolveVisibleSourceTypes\n if (!port) {\n return DailyReportViewerScope.unrestricted(\"no visibility port injected: this deployment does not restrict by source type\")\n }\n try {\n const resolved = await port(request, { reason })\n // ❗ ポートが nullish を返したら「この配備は区分で絞らない」という契約上の fail-open。\n // restrictTo へ渡して黙って無制限化すると、監査 grep に現れない 2 つ目の\n // 無制限生成経路になるため、ここで明示的に unrestricted として記録する\n return resolved == null ? DailyReportViewerScope.unrestricted(\"visibility port returned null/undefined: this deployment opts out of source-type filtering\") : DailyReportViewerScope.restrictTo(resolved)\n } catch (e) {\n visibilityLogger.error(\"resolveVisibleSourceTypes threw; failing closed (deny all)\", e)\n return DailyReportViewerScope.restrictTo([])\n }\n }\n\n // ---------------- index (画面ルート) ----------------\n\n /**\n * Document loader that authenticates and resolves the obfuscated internal user id.\n * 認証と難読化済み内部ユーザー ID の解決を行うドキュメントローダー。\n */\n const indexLoader = async ({ request }: LoaderArgs) => {\n const { user, cookie } = await authenticate(request, { failureRedirect: loginRedirectPath })\n const headers = new Headers()\n if (cookie) {\n headers.append(\"Set-Cookie\", cookie)\n }\n\n // 文字列モード (`failureRedirect: loginRedirectPath`) では認証失敗が redirect として\n // throw されるため `user` は必ず存在する (`DailyReportAuthenticate` のオーバーロードも\n // そう宣言している)。`if (user)` は到達しないので置かない\n const userId: number | null = await service.getUserIdByExternalId(user.id)\n\n const hashedUserId = userId ? encodeUserId(userId) : null\n\n return { data: { user, userId: hashedUserId }, headers }\n }\n\n // ---------------- api (:endpoint ルーター) ----------------\n\n type User = { id: string }\n type EndpointHandler = (url: URL, cookie: string | null, request: Request, user: User, scope: DailyReportViewerScope) => Promise<Response>\n\n /**\n * Handlers for each API endpoint.\n * 各 API エンドポイントのハンドラー定義。\n */\n const endpointHandlers: Record<string, EndpointHandler> = {\n /**\n * Retrieves daily reports for a specific business date.\n * 指定された営業日の日報一覧を取得する。\n */\n \"business-date\": async (url, cookie, request, user, scope) => {\n const normalizedBusinessDate = normalizeBusinessDateKey(url.searchParams.get(\"businessDate\"))\n const forceRefresh = url.searchParams.get(\"forceRefresh\") === \"true\"\n\n if (!normalizedBusinessDate) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Invalid business date\" } }, 400)\n }\n const reports = await service.getDailyReportsByBusinessDateByExternalId(scope, normalizedBusinessDate, user.id, { forceRefresh })\n return jsonResponseWithETag(request, cookie, { businessDate: normalizedBusinessDate, reports }, 200)\n },\n /**\n * Streams the full daily report id list as NDJSON chunks.\n * 日報 ID 一覧全件を NDJSON チャンクとして逐次配信する。\n *\n * 全量クエリ (`SqlResultCache` の TTL / Redis epoch / in-flight コアレッシングを\n * 素通しで維持) は常に即時に開始する。全量確定を待ってから送出するのは再開接続のみで、\n * その場合の DB エラーはストリーム開始前に throw されて apiLoader の catch が\n * 500 JSON を返す。新規接続はキャッシュ無しの TOP 200 高速先頭ページと競争させ、\n * キャッシュミス時は全量確定前に最初のチャンクを送出する (最初の描画までの時間が\n * 全体データ量に依存しない)。キャッシュヒット時は全量が即勝ちし、従来どおり\n * 確定済み配列のメモリスライス送出になる。\n */\n \"ids-stream\": async (url, cookie, request, user, scope) => {\n const parsed = parseIdsStreamCursor(url)\n if (\"invalid\" in parsed) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Invalid cursor\" } }, 400)\n }\n const forceRefresh = url.searchParams.get(\"forceRefresh\") === \"true\"\n\n // 高速先頭ページは「本リクエストが実際に全量クエリを起動した」ときだけ走らせる。\n // ❗ この因果が B* の健全性の土台である。バケットヒット・進行中フェッチへの相乗り・\n // refresh-ahead が返す全量配列は本リクエストより**古くなり得る**ため、そこへ\n // 新鮮な先頭ページを重ねると「ページに在って全量に無い行 = 削除済み」の判定が\n // 反転し、作成直後の日報を消してしまう。合図の後に始まるページは全量より古く\n // なり得ないので、この判定が初めて健全になる。\n // 副次効果として、キャッシュヒット時に走って捨てられていた TOP-N クエリが消える\n let pageQueryStarted = false\n let resolvePage: ((items: DailyReportItem[]) => void) | null = null\n let rejectPage: ((reason?: unknown) => void) | null = null\n const pagePromise = new Promise<DailyReportItem[]>((resolve, reject) => {\n resolvePage = resolve\n rejectPage = reject\n })\n // 未処理拒否警告を出さない (拒否はレース側で分類する)\n pagePromise.catch(() => {})\n\n // 全量 (SqlResultCache / epoch / コアレッシング維持) は常に即時に走らせ始める\n const fullArrayPromise = service.getDailyReportIdsByExternalId(scope, user.id, {\n forceRefresh,\n // 再開接続は先頭ページを使わない (クライアントは受信済みの続きだけを読む)\n onFetchStart: parsed.cursor\n ? undefined\n : () => {\n pageQueryStarted = true\n service.getDailyReportIdsFirstPageByExternalId(scope, user.id, IDS_STREAM_FIRST_CHUNK_SIZE).then(\n (items) => resolvePage?.(items),\n (error) => rejectPage?.(error),\n )\n },\n })\n // 拒否はイテレーターの await 側で扱う (ここで未処理拒否警告を出さない)\n fullArrayPromise.catch(() => {})\n\n if (parsed.cursor) {\n // 再開接続: クライアントは高速先頭を受信済みなので全量確定を待つ (エラーは 500)。\n // 全量クエリはアプリ側の requestTimeout (既定 60s) で境界づけられ、確定後は\n // スライスだけなので、接続確立後の無音はハートビート対象にならない\n const fullItems = await fullArrayPromise\n const snapshot = computeIdsStreamSnapshotToken(fullItems)\n // ❗ トークンが一致した配列にだけカーソルを信用する。別スナップショットへ当てると、\n // カーソルより前に入った行が丸ごと飛ぶ (再接続のたびの欠損)。不一致・トークン\n // 無し (旧クライアント) は先頭から流し直す — 配備跨ぎタブの再接続が全量再送に\n // なる費用は受容済み (仕様書に明記)\n const resumable = parsed.snapshot !== null && parsed.snapshot === snapshot\n return buildIdsStreamResponse(cookie, streamLinesFromFullArray(fullItems, resumable ? parsed.cursor : null, IDS_STREAM_FIRST_CHUNK_SIZE, { snapshot, restarted: !resumable }), request, apiLogger)\n }\n\n /**\n * 新規接続: 「高速先頭ページ (TOP n・キャッシュ無し)」と「全量 (キャッシュ経由)」を\n * 競争させ、先に確定した方で最初のチャンクを出す。キャッシュヒット時はページ自体が\n * 起動しないので全量が唯一の経路になる (従来どおり先頭行に total)。キャッシュミス時は\n * 先頭ページが先に届き、最初の描画は全量クエリの確定を待たない。\n *\n * ❗ 先頭ページは**暫定 (provisional)** として送る。全量配列とは別スナップショットなので\n * 「観測した」と扱うと、送出後に消えた行が完走剪定を免れて幽霊行になる。暫定行は\n * クライアントが描画はするが観測集合へ刻まないため、権威パスに現れなかった行が\n * 完走時に正確に落ちる。権威パスは**必ず先頭から**流し直すので、ページ送出後に\n * 先頭領域へ入ってきた行の取りこぼし (欠損) も構造的に起きない。\n */\n const lineIterator = (async function* (): AsyncGenerator<DailyReportIdsStreamChunkLine> {\n const winner = await Promise.race([\n fullArrayPromise.then(\n (items) => ({ kind: \"full\" as const, items }),\n (error) => ({ kind: \"full-error\" as const, error }),\n ),\n pagePromise.then(\n (items) => ({ kind: \"page\" as const, items }),\n () => ({ kind: \"page-error\" as const }),\n ),\n ])\n\n let fullItems: DailyReportItem[] | null = null\n let emittedProvisionalPage = false\n\n if (winner.kind === \"full\") {\n fullItems = winner.items\n } else if (winner.kind === \"page\") {\n const pageItems = winner.items\n if (pageItems.length < IDS_STREAM_FIRST_CHUNK_SIZE) {\n // 先頭ページが上限未満 = データセット全量。しかもページは全量クエリより\n // 後に始まっているので、これが最新の権威である。全量を待たずに完結する。\n // ❗ ここを provisional にしてはならない (小規模データセットが完走剪定で全消しになる)\n yield { kind: \"chunk\", items: pageItems, nextCursor: null, isLast: true, total: pageItems.length }\n return\n }\n // ❗ カーソルは載せない。未確認領域を指すカーソルで再開すると、継ぎ目が\n // 三つ目のスナップショットへ移るだけになる。失敗時は先頭からやり直す\n yield { kind: \"chunk\", items: pageItems, nextCursor: null, isLast: false, provisional: true }\n emittedProvisionalPage = true\n } else if (winner.kind === \"full-error\" && pageQueryStarted) {\n // 全量が先に死んでも、生きている先頭ページがあればまず届ける (暫定として)。\n // 完走しない走査では剪定が走らないため、暫定行はそのまま残って画面を埋める\n const pageItems = await pagePromise.catch(() => null)\n if (pageItems && pageItems.length > 0) {\n yield { kind: \"chunk\", items: pageItems, nextCursor: null, isLast: false, provisional: true }\n }\n }\n // page-error は全量へフォールバック。full-error はここで throw されエラー行になる\n if (fullItems === null) {\n fullItems = await fullArrayPromise\n }\n // ❗ 権威パスは常に index 0 から。ページのキーを再開カーソルとして使うと、\n // ページ送出後に先頭領域へ入った行が丸ごと飛ばされる (欠損の構造的原因)。\n // チャンクサイズは明示指定する — 三項演算子の既定へ落とすと、ページを出した\n // 経路で権威パスの初回チャンクが黙って先頭サイズへ縮み、行数が無駄に増える\n yield* streamLinesFromFullArray(fullItems, null, emittedProvisionalPage ? IDS_STREAM_NEXT_CHUNK_SIZE : IDS_STREAM_FIRST_CHUNK_SIZE, { snapshot: computeIdsStreamSnapshotToken(fullItems) })\n })()\n\n return buildIdsStreamResponse(cookie, lineIterator, request, apiLogger)\n },\n /**\n * Retrieves details for a specific daily report.\n * 指定された日報の詳細情報を取得する。\n */\n report: async (url, cookie, request, user, scope) => {\n const param = url.searchParams.get(\"reportHubId\")\n const forceRefresh = url.searchParams.get(\"forceRefresh\") === \"true\"\n const parsedId = param ? Number.parseInt(param, 10) : NaN\n\n if (!Number.isFinite(parsedId) || parsedId <= 0) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Invalid reportHubId\" } }, 400)\n }\n const detail = await service.getDailyReportDetailByIdByExternalId(scope, parsedId, user.id, { snapshot: true, forceRefresh })\n if (!detail) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Report not found\" } }, 404)\n }\n return jsonResponseWithETag(request, cookie, { report: detail }, 200)\n },\n }\n\n /**\n * Routes authenticated daily report API requests by endpoint.\n * エンドポイントごとに認証済みの日報 API リクエストを振り分ける。\n */\n const apiLoader = async ({ request, params }: LoaderArgs) => {\n const { user, cookie } = await authenticate(request, { failureRedirect: null })\n const sanitizedCookie = cookie ?? null\n\n if (!user) {\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Unauthorized\" } }, 401)\n }\n\n if (request.method !== \"GET\") {\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Method not allowed\" } }, 405)\n }\n const endpoint = params.endpoint ?? \"\"\n const handler = endpointHandlers[endpoint]\n if (!handler) {\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Unknown endpoint\" } }, 404)\n }\n const startTime = Date.now()\n try {\n const url = new URL(request.url)\n // 可視集合はリクエストごとに 1 回だけ解決する (キャッシュキーが戻り値に依存する)\n const scope = await resolveViewerScope(request)\n return await handler(url, sanitizedCookie, request, user, scope)\n } catch (error) {\n const elapsed = Date.now() - startTime\n const err = error instanceof Error ? error : new Error(String(error))\n apiLogger.error(`500 endpoint=${endpoint} elapsed=${elapsed}ms name=${err.name} code=${\"code\" in err ? (err as { code: unknown }).code : \"N/A\"} message=${err.message}`)\n apiLogger.error(\"Stack:\", err.stack)\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Internal Server Error\" } }, 500)\n }\n }\n\n /**\n * Handles data mutations for daily reports.\n * 日報データの変更操作を処理するアクション。\n */\n const apiAction = async ({ request, params }: LoaderArgs) => {\n const { user, cookie } = await authenticate(request, { failureRedirect: null })\n\n /**\n * Builds a JSON response that always carries the session cookie for this request.\n * 本リクエストのセッション cookie を必ず載せた JSON レスポンスの生成。\n *\n * 全分岐 (成功・4xx・未認証) で転送する必要がある。トークン更新の cookie を落とすとブラウザは\n * 期限切れトークンを持ち続け、以後のリクエストが毎回リフレッシュを試みて収束しない。\n * 破棄 cookie を落とせば無効セッションがブラウザに残り続ける。個別の `jsonData` へ付け回すと\n * 必ず取りこぼすため、この関数以外を本ハンドラ内で使わない。\n *\n * @param payload Response payload / レスポンス本文\n * @param init Status override / ステータス指定\n * @returns JSON response carrying the session cookie / セッション cookie を載せた JSON レスポンス\n */\n const respond = (payload: unknown, init?: { status?: number }): Response => jsonData(payload, { ...init, setCookie: cookie })\n\n if (!user) {\n return respond({ error: \"Unauthorized\" }, { status: 401 })\n }\n\n // apiAction には外側 try/catch が無く、`respond` を通らない例外 (service / DB / Redis の\n // 失敗) がハンドラの外へ抜けていた。抜けるとセッション cookie が載らないまま 500 になり、\n // トークン更新がブラウザへ届かない。apiLoader (:930) と同じ形で包む\n const actionStartTime = Date.now()\n try {\n const endpoint = params.endpoint\n if (endpoint !== \"action\") {\n return respond({ error: \"Unknown endpoint\" }, { status: 404 })\n }\n\n const userId = await service.getUserIdByExternalId(user.id)\n if (!userId) {\n return respond({ error: \"User not found\" }, { status: 404 })\n }\n\n // 可視集合はリクエストごとに 1 回だけ解決し、他者の日報に作用する 4 種へ渡す\n // (create / update / publish / delete / clearCache は所有者・開発者ゲートで別管理)\n const scope = await resolveViewerScope(request)\n\n const formData = await request.formData()\n const intent = formData.get(\"intent\")\n const reportHubIdRaw = formData.get(\"reportHubId\")\n const reportHubId = reportHubIdRaw ? Number(reportHubIdRaw) : NaN\n const businessDate = formData.get(\"businessDate\") as string | null\n const operationTimestamp = Number(formData.get(\"operationTimestamp\"))\n const clientTempId = formData.get(\"clientTempId\") as string | null\n\n if (intent === \"clearCache\") {\n // 開発専用アフォーダンス。既定は封鎖 (認証済みなら誰でも全ワーカーの\n // キャッシュを無制限破棄できてしまうため、注入で明示的に開けた環境のみ許可)\n if (!config.enableDevCacheClear) {\n return respond({ error: \"Invalid intent\" }, { status: 400 })\n }\n await service.clearCache()\n return respond({ status: \"OK\", intent: \"clearCache\" })\n }\n\n if (!clientTempId) {\n return respond({ error: \"clientTempId required\" }, { status: 400 })\n }\n\n if (intent !== \"create\" && (!reportHubId || Number.isNaN(reportHubId))) {\n return respond({ error: \"Invalid reportHubId\" }, { status: 400 })\n }\n\n /**\n * Runs a boolean user-status toggle and maps its outcome onto a response.\n * 真偽値のユーザー状態トグルを実行し、結果をレスポンスへ写像する処理。\n *\n * `toggleStar` と `toggleRead` は必須フィールド名と呼び出すサービスだけが異なるため、\n * 検証・応答整形・エラー写像をここへ集約する。\n *\n * @param intentName Intent echoed back to the client. クライアントへ返す intent 名。\n * @param fieldName Required boolean form field. 必須の真偽値フォームフィールド名。\n * @param apply Service call performing the toggle. トグルを実行するサービス呼び出し。\n * @returns The HTTP response. HTTP レスポンス。\n */\n const runStatusToggle = async (intentName: \"toggleStar\" | \"toggleRead\", fieldName: \"isStarred\" | \"isRead\", apply: (value: boolean) => Promise<{ isStarred: boolean; isRead: boolean }>): Promise<Response> => {\n const raw = formData.get(fieldName)\n if (raw === null) {\n return respond({ error: `${fieldName} required` }, { status: 400 })\n }\n try {\n const updatedStatus = await apply(raw === \"true\")\n // クライアントは isStarred/isRead のみ参照する。生内部 ID (userId/created_by/updated_by) を\n // 含む行全体は返さず、必要なフラグだけに絞る (内部 ID の自己開示防止)。\n return respond({ status: \"OK\", intent: intentName, updatedStatus: { isStarred: updatedStatus.isStarred, isRead: updatedStatus.isRead }, operationTimestamp, reportHubId: String(reportHubId), clientTempId })\n } catch (e: unknown) {\n // 不可視区分・不存在・削除済みは 404 へ写像する (未捕捉だと 500 になる)\n if (e instanceof Error && e.message === \"Not Found\") {\n return respond({ error: \"Report not found\" }, { status: 404 })\n }\n throw e\n }\n }\n\n switch (intent) {\n case \"create\": {\n if (!businessDate) {\n return respond({ error: \"businessDate required\" }, { status: 400 })\n }\n const newReport = await service.createDailyReport(userId, businessDate, clientTempId)\n return respond({\n status: \"OK\",\n intent: \"create\",\n report: newReport,\n reportHubId: String(newReport.reportHubId),\n clientTempId, // Echo back for validation\n })\n }\n case \"update\": {\n const title = formData.get(\"title\") as string | undefined\n const content = formData.get(\"content\") as string | undefined\n try {\n await service.updateDailyReport(reportHubId, userId, { title, content }, clientTempId)\n return respond({ status: \"OK\", intent: \"update\", reportHubId: String(reportHubId), clientTempId })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return respond({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return respond({ error: \"Report not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n case \"publish\": {\n try {\n const publishedReport = await service.publishDailyReport(reportHubId, userId, clientTempId)\n return respond({ status: \"OK\", intent: \"publish\", reportHubId: String(reportHubId), clientTempId, report: publishedReport })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return respond({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return respond({ error: \"Report not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n case \"delete\": {\n try {\n await service.deleteDailyReport(reportHubId, userId, clientTempId)\n return respond({\n status: \"OK\",\n intent: \"delete\",\n reportHubId: String(reportHubId),\n clientTempId, // Echo back for validation even on delete\n })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return respond({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return respond({ error: \"Report not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n case \"toggleStar\":\n return await runStatusToggle(\"toggleStar\", \"isStarred\", (value) => service.setStarStatus(scope, userId, reportHubId, businessDate, value, clientTempId))\n case \"toggleRead\":\n return await runStatusToggle(\"toggleRead\", \"isRead\", (value) => service.setReadStatus(scope, userId, reportHubId, businessDate, value, clientTempId))\n case \"addComment\": {\n const content = formData.get(\"content\") as string\n if (!content) {\n return respond({ error: \"Content required\" }, { status: 400 })\n }\n try {\n const newComment = await service.addComment(scope, userId, reportHubId, content, businessDate, clientTempId)\n const safeComment = { ...newComment, userId: newComment.userId }\n return respond({ status: \"OK\", intent: \"addComment\", newComment: safeComment, reportHubId: String(reportHubId), clientTempId })\n } catch (e: unknown) {\n if (e instanceof Error) {\n // 削除直後の日報への投稿レース等。500 ではなく意味のある状態コードへ写像する\n if (e.message === \"Not Found\") {\n return respond({ error: \"Report not found\" }, { status: 404 })\n }\n if (e.message.startsWith(\"Comments are restricted\")) {\n return respond({ error: \"Comments are restricted for this report\" }, { status: 403 })\n }\n }\n throw e\n }\n }\n case \"deleteComment\": {\n const commentId = Number(formData.get(\"commentId\"))\n if (!commentId || Number.isNaN(commentId)) {\n return respond({ error: \"Invalid commentId\" }, { status: 400 })\n }\n try {\n await service.deleteComment(scope, userId, reportHubId, commentId, businessDate, clientTempId)\n return respond({ status: \"OK\", reportHubId: String(reportHubId), deletedCommentId: String(commentId), clientTempId })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return respond({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return respond({ error: \"Comment not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n default:\n return respond({ error: \"Invalid intent\" }, { status: 400 })\n }\n } catch (error) {\n // React Router のリダイレクト等は握り潰さず素通しする\n if (error instanceof Response) throw error\n const elapsed = Date.now() - actionStartTime\n const err = error instanceof Error ? error : new Error(String(error))\n apiLogger.error(`500 action elapsed=${elapsed}ms name=${err.name} message=${err.message}`)\n apiLogger.error(\"Stack:\", err.stack)\n return respond({ error: \"Internal Server Error\" }, { status: 500 })\n }\n }\n\n // ---------------- sse (リアルタイム更新) ----------------\n\n /**\n * SSE endpoint for real-time daily report updates via Redis Streams.\n * Redis Streams を使用した日報リアルタイム更新の SSE エンドポイント。\n */\n const sseLoader = async ({ request, params }: LoaderArgs) => {\n // 認証チェック\n const { user, cookie } = await authenticate(request, { failureRedirect: null })\n // セッション cookie は**この応答がどれになろうと**転送する。トークンが更新された\n // リクエストがたまたま SSE 接続だった場合、ここで落とすと更新後のトークンがブラウザへ\n // 届かず、以降のリクエストが毎回リフレッシュをやり直すことになる。破棄 cookie を\n // 落とした場合はさらに悪く、無効セッションがブラウザに残り続ける (apiAction と同じ理由)。\n // SSE は接続が長命なぶん、更新を担うリクエストになりやすい\n const sessionHeaders = (extra?: Record<string, string>): Record<string, string> | undefined => {\n if (cookie === undefined) return extra\n return { ...(extra ?? {}), \"Set-Cookie\": cookie }\n }\n if (!user) {\n return new Response(\"Unauthorized\", { status: 401, headers: sessionHeaders() })\n }\n\n // 内部ユーザー ID の解決\n const internalUserId = await service.getUserIdByExternalId(user.id)\n if (!internalUserId) {\n return new Response(\"Forbidden\", { status: 403, headers: sessionHeaders() })\n }\n\n // エンドポイントの検証\n if (params.endpoint !== \"updates\") {\n return new Response(\"Not Found\", { status: 404, headers: sessionHeaders() })\n }\n\n // 可視集合は接続時に解決し、以降 keep-alive tick に相乗りして再解決する (D6)。\n // 接続生存期間で固定すると、権限失効後も本文付きイベントが届き続けてしまう。\n // ❗ 集合はここで 1 度だけ取り出して保持する。`scope.visible` / `scope.commentable` は\n // 毎回コピーを返す getter なので、メッセージごとに読むと配信 1 件あたり 2 個の Set を\n // 確保することになる (共有 Fan-Out の高頻度経路)\n const initialScope = await resolveViewerScope(request)\n let visibleSet = initialScope.visible\n let commentableSet = initialScope.commentable\n let keepAliveTick = 0\n let resolveGeneration = 0\n\n // URL から lastEventId を取得 (再接続時の catch-up 用)\n // クエリパラメータ優先、EventSource 自動再接続時の Last-Event-ID ヘッダーにフォールバック\n const url = new URL(request.url)\n const lastEventId = url.searchParams.get(\"lastEventId\") || request.headers.get(\"Last-Event-ID\")\n\n const encoder = new TextEncoder()\n let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null\n let keepAliveInterval: ReturnType<typeof setInterval> | null = null\n let unsubscribe: (() => void) | null = null\n let isCleaningUp = false\n\n // クリーンアップ処理\n const cleanup = () => {\n if (isCleaningUp) return\n isCleaningUp = true\n\n // keep-alive タイマー停止\n if (keepAliveInterval) {\n clearInterval(keepAliveInterval)\n keepAliveInterval = null\n }\n\n // Fan-Out 購読解除 (entry / error 両リスナーを解除)\n if (unsubscribe) {\n unsubscribe()\n unsubscribe = null\n }\n\n // SSE ストリームの終了\n if (controllerRef) {\n try {\n if (controllerRef.desiredSize !== null) {\n controllerRef.close()\n }\n } catch (_e) {\n // ストリームが既に閉じている場合は無視\n } finally {\n controllerRef = null\n }\n }\n\n isCleaningUp = false\n }\n\n /**\n * Processes a single Redis Stream entry and sends it to the SSE client.\n * Redis Stream のエントリを処理し、SSE クライアントに送信する。\n */\n /**\n * Emits a content-free frame that only advances the client's resume anchor.\n * 内容を伴わず、クライアントの再開アンカーだけを前進させるフレームを送出する処理。\n *\n * 種別は `connected` を使う。クライアントは既にこの種別を無視しつつ `lastEventId` を\n * 更新するため、フィルタで落としたエントリの ID を安全に伝播できる。\n *\n * @param entryId Redis Stream entry id. Redis Stream のエントリ ID。\n * @returns Nothing. なし。\n */\n const emitAnchorOnly = (entryId: string): void => {\n if (controllerRef && controllerRef.desiredSize !== null) {\n controllerRef.enqueue(encoder.encode(`id: ${entryId}\\ndata: ${JSON.stringify({ type: \"connected\" })}\\n\\n`))\n }\n }\n\n const processEntry = (entryId: string, fields: Record<string, string>) => {\n // data フィールドの存在チェック\n if (!fields.data) return\n\n try {\n const raw = JSON.parse(fields.data)\n const result = dailyReportSseMessageSchema.safeParse(raw)\n if (!result.success) {\n sseLogger.error(\"SSE message validation failed:\", result.error.format())\n return // Fail-Closed: 不正なメッセージは送信しない\n }\n const parsed = result.data\n\n // recipientRawUserId によるフィルタリング\n const recipientRawUserId = typeof raw.recipientRawUserId === \"number\" ? raw.recipientRawUserId : undefined\n\n if (parsed.type === \"status-update\") {\n if (recipientRawUserId === undefined || recipientRawUserId !== internalUserId) {\n return\n }\n } else if (recipientRawUserId !== undefined && recipientRawUserId !== internalUserId) {\n return\n }\n\n // 区分の可視性フィルタ (宛先フィルタを通過した後に適用する)\n if (!isSseMessageSourceTypeVisible(parsed, raw, visibleSet, commentableSet)) {\n // ❗ 内容は送らないが、再開アンカーだけは前へ進める。クライアントの\n // `lastEventId` は配信されたイベントでしか更新されないため、落とし続けると\n // アンカーが古いまま固定され、再接続のたびに同じ範囲を読み直す\n // (かつ catch-up の COUNT 上限を超えた分を取りこぼしうる)。\n // `connected` はクライアントが既に無視する種別なので、内容は漏れない\n emitAnchorOnly(entryId)\n return\n }\n\n // コメント次元が拒否する区分なら、埋め込み detail のコメント面だけを空にする\n const redacted = redactSseReportComments(parsed, raw, commentableSet)\n\n // recipientRawUserId をクライアントに送信しない (内部 ID 漏洩防止)\n let sanitizedMessage: string\n if (\"recipientRawUserId\" in raw) {\n const { recipientRawUserId: _, ...rest } = raw\n sanitizedMessage = JSON.stringify(rest)\n } else if (redacted) {\n // ❗ 秘匿したら必ず再直列化する。元の生文字列を送る高速経路のままだと\n // 書き換えが捨てられ、コメント本文がそのまま流れる\n sanitizedMessage = JSON.stringify(raw)\n } else {\n sanitizedMessage = fields.data\n }\n\n // SSE フォーマットで送信 (id フィールド付き)\n if (controllerRef && controllerRef.desiredSize !== null) {\n controllerRef.enqueue(encoder.encode(`id: ${entryId}\\ndata: ${sanitizedMessage}\\n\\n`))\n }\n } catch (e) {\n sseLogger.error(`[SSE:${internalUserId}] processEntry error:`, e)\n }\n }\n\n const stream = new ReadableStream({\n async start(controller) {\n // 1. controllerRef 設定\n controllerRef = controller\n\n // 2. Keep-Alive: 5 秒ごとにコメントを送信して接続維持\n keepAliveInterval = setInterval(() => {\n try {\n if (controllerRef && controllerRef.desiredSize !== null) {\n controllerRef.enqueue(encoder.encode(\": keep-alive\\n\\n\"))\n // 約 60 秒ごと (12 tick) に可視集合を再解決し、権限失効を有界化する。\n // 世代ガード: 順序が逆転した古い解決結果で最新を上書きしない\n // (上書きすると広い集合へ戻り fail-open 方向に倒れる)。\n // resolveVisibleSet 自身が fail-close なので catch は不要\n if (++keepAliveTick % SSE_VISIBILITY_REFRESH_TICKS === 0) {\n const generation = ++resolveGeneration\n resolveViewerScope(request, \"refresh\").then((next) => {\n if (generation === resolveGeneration) {\n // 両次元の集合を同一スコープから同時に差し替える。\n // 片方だけ更新すると次元間で世代がずれ、失効が半分しか効かない\n visibleSet = next.visible\n commentableSet = next.commentable\n }\n })\n }\n } else {\n cleanup()\n }\n } catch (_e) {\n cleanup()\n }\n }, 5000)\n\n // 3. クライアント切断時のクリーンアップ (全 await の前に登録)\n request.signal.addEventListener(\"abort\", () => {\n cleanup()\n })\n\n // catch-up の基準 ID (Fan-Out エントリとの重複排除に使用)\n let lastProcessedId = lastEventId || \"0-0\"\n\n /**\n * Fan-Out がライブ配信した最初のエントリ ID。catch-up の重複排除に使う。\n *\n * ❗ catch-up の判定に `lastProcessedId` を使ってはならない。`xRange` を待つ間に\n * ライブエントリが 1 件でも届くと共有の高水位が catch-up 範囲の先まで進み、\n * 「既に処理済み」と誤判定して**取得した範囲を丸ごと捨てて**しまう。\n * ライブで配ったのは「この ID 以降」だけなので、その境界で判定する。\n */\n let firstLiveEntryId: string | null = null\n\n // 4. Fan-Out 共有リーダーを購読 (catch-up より先に登録して取りこぼしを防止)\n unsubscribe = sseReader.subscribe(\n (entry: StreamEntry) => {\n // catch-up 済みエントリはスキップ (重複排除)\n if (isStreamIdLte(entry.id, lastProcessedId)) return\n if (firstLiveEntryId === null) {\n firstLiveEntryId = entry.id\n }\n lastProcessedId = entry.id\n processEntry(entry.id, entry.message)\n },\n (err: Error) => {\n sseLogger.error(`[SSE:${internalUserId}] Fan-Out reader error:`, err)\n cleanup()\n },\n )\n\n // 5. 初回接続時: 最新 Stream ID を取得して connected の id: に使用\n // (再接続時は lastEventId が既にあるためスキップ)\n if (!lastEventId) {\n try {\n const client = await redis?.getClient()\n if (client) {\n const latest = await client.xRevRange(streamKey, \"+\", \"-\", { COUNT: 1 })\n // Fan-Out が既に lastProcessedId を進めている場合は巻き戻さない\n if (latest.length > 0 && !isStreamIdLte(latest[0].id, lastProcessedId)) {\n lastProcessedId = latest[0].id\n }\n }\n } catch {\n /* 取得失敗時は現在の lastProcessedId を維持 */\n }\n }\n\n // 6. 接続完了通知 (id: 付きで再接続時の catch-up アンカーを提供)\n if (controllerRef && controllerRef.desiredSize !== null) {\n const connectedPayload = `data: ${JSON.stringify({ type: \"connected\" })}\\n\\n`\n const sseMessage = lastProcessedId !== \"0-0\" ? `id: ${lastProcessedId}\\n${connectedPayload}` : connectedPayload\n controllerRef.enqueue(encoder.encode(sseMessage))\n }\n\n // 7. catch-up: lastEventId が指定されている場合、それ以降のエントリを一括取得\n if (lastEventId) {\n try {\n const client = await redis?.getClient()\n if (client) {\n const catchUpEntries = await client.xRange(streamKey, lastEventId, \"+\", { COUNT: 1000 })\n for (const entry of catchUpEntries) {\n // xRange は inclusive なので lastEventId 自身はスキップ\n if (entry.id === lastEventId) continue\n // Fan-Out がライブ配信した境界以降は重複するのでスキップ。\n // 共有の高水位ではなくこの境界で判定することで、await 中に届いた\n // ライブ 1 件が catch-up 範囲全体を捨てさせる事故を防ぐ\n if (firstLiveEntryId !== null && isStreamIdLte(firstLiveEntryId, entry.id)) continue\n // 高水位は後退させない (ライブが先行している場合がある)\n if (!isStreamIdLte(entry.id, lastProcessedId)) {\n lastProcessedId = entry.id\n }\n processEntry(entry.id, entry.message as Record<string, string>)\n }\n }\n } catch (e) {\n sseLogger.error(`[SSE:${internalUserId}] catch-up xRange error:`, e)\n // catch-up 失敗時は Fan-Out のみで継続\n }\n }\n },\n cancel() {\n cleanup()\n },\n })\n\n return new Response(stream, {\n headers: sessionHeaders({\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n }),\n })\n }\n\n // ---------------- attachment (認可済みバイト配信) ----------------\n\n /**\n * Streams an authorized attachment's bytes to the client.\n * 認可済みの添付ファイルをバイト列としてクライアントへ返すローダー。\n *\n * 認可は `service.getAttachmentForUser` の SQL 述語で完結させる。ストレージ側の\n * 資格情報はサービス共通のものであり、利用者間の分離を一切提供しない。\n * したがってこの述語が唯一の認可境界であり、ここに欠陥があれば全テナント横断の読み取りになる。\n *\n * 「存在しない」と「認可されない」は区別せず 404 にする (存在オラクル回避)。\n *\n * 本体は必ず try/catch で包む。ここで漏らした例外はフレームワークの最終防衛線に落ち、\n * JSON ではないプレーンテキストの 500 になるうえ `Set-Cookie` (セッション延長) も失われる。\n * **認証ポートの呼び出しも try の内側**に置くこと。認証実装はセッション復号や warmup の\n * 失敗で素の例外を投げうるため、外に置くと同じ穴が残る。ただしリダイレクトは `Response` を\n * throw する正常な制御フローなので、catch では素通しする。\n */\n const attachmentLoader = async ({ request, params }: LoaderArgs) => {\n let sanitizedCookie: string | null = null\n\n /** エラー応答。`Set-Cookie` はすべての分岐で伝播させる。 */\n const fail = (status: number, message: string, retryAfterSeconds?: number): Response => {\n const headers = new Headers({ \"Content-Type\": \"application/json\" })\n if (retryAfterSeconds !== undefined) headers.set(\"Retry-After\", String(retryAfterSeconds))\n if (sanitizedCookie) headers.append(\"Set-Cookie\", sanitizedCookie)\n return new Response(JSON.stringify({ error: { message } }), { status, headers })\n }\n\n try {\n const { user, cookie } = await authenticate(request, { failureRedirect: null })\n sanitizedCookie = cookie ?? null\n\n if (!user) return fail(401, \"Unauthorized\")\n if (request.method !== \"GET\" && request.method !== \"HEAD\") return fail(405, \"Method not allowed\")\n\n const codec = config.attachmentIdCodec\n const readAttachment = config.readAttachment\n // ポート未注入は「この配備に添付機能が無い」であり、認可失敗と区別しない\n if (!(codec && readAttachment)) return fail(404, \"Attachment not found\")\n\n const token = params.token ?? \"\"\n const attachmentId = codec.decode(token)\n // `Number.isSafeInteger` であること。可逆難読化は入力トークンの正準性を検証しないため、\n // 正規トークンと同じ長さの入力から 2^53 を超える値が復号されうる。それを SQL パラメータへ\n // 渡すとドライバーの範囲検証が TypeError を投げ、400 のはずが 500 になる。\n if (attachmentId === null || !Number.isSafeInteger(attachmentId) || attachmentId <= 0) {\n return fail(400, \"Invalid attachment token\")\n }\n\n const internalUserId = await service.getUserIdByExternalId(user.id)\n if (!internalUserId) return fail(403, \"Forbidden\")\n\n // レート制限。トークンは列挙可能なので、これが実効的な唯一の緩和策\n if (!consumeAttachmentRateToken(internalUserId, Date.now())) {\n attachmentLogger.error(`429 attachment=${attachmentId} viewer=${internalUserId} reason=rate_limit`)\n return fail(429, \"Too many requests\", RATE_LIMIT_RETRY_AFTER_SECONDS)\n }\n\n // 認可判定 (SQL 側で完結)。null は「存在しない」「認可されない」「親が論理削除済み」の合流\n // 可視集合はこの try の内側で解決する (例外封じ込め規約の維持)\n const scope = await resolveViewerScope(request)\n const attachment = await service.getAttachmentForUser(scope, attachmentId, internalUserId)\n if (!attachment) {\n attachmentLogger.error(`404 attachment=${attachmentId} viewer=${internalUserId} reason=not_visible`)\n return fail(404, \"Attachment not found\")\n }\n\n // DB が既知サイズを持つ場合は読み取り前に弾く (サイズを報告しない取込元の行はここを通過する)\n if (attachment.fileSize != null && attachment.fileSize > attachmentMaxBytes) {\n attachmentLogger.error(`413 attachment=${attachmentId} viewer=${internalUserId} reason=db_size`)\n return fail(413, \"Attachment too large\")\n }\n\n if (!attachmentGate.tryAcquire()) {\n attachmentLogger.error(`503 attachment=${attachmentId} viewer=${internalUserId} reason=concurrency`)\n return fail(503, \"Too many concurrent downloads\", CONCURRENCY_RETRY_AFTER_SECONDS)\n }\n\n // スロットの解放は冪等にする。早期 return・例外・送出完了のどの経路からでも\n // ちょうど 1 回だけ返す必要がある\n let gateReleased = false\n const releaseGate = () => {\n if (gateReleased) return\n gateReleased = true\n attachmentGate.release()\n }\n // 本文の送出完了まで保持する経路に入ったかどうか (入った場合のみ finally での解放を見送る)\n let releaseDeferredToStream = false\n\n try {\n const isHead = request.method === \"HEAD\"\n const result = await readAttachment(attachment.filePath, { maxBytes: attachmentMaxBytes, head: isHead, principal: encodeUserId(internalUserId) })\n\n if (!result.ok) {\n const status = attachmentFailureToStatus(result.reason)\n // 実体消失は次回描画へ反映する。記録の失敗は配信結果に影響させない\n // (await しないので、拒否を捕まえないと未処理 Promise 拒否になる)\n if (result.reason === \"not_found\") {\n void service.markAttachmentMissing(attachmentId).catch((error: unknown) => {\n attachmentLogger.error(`markAttachmentMissing failed attachment=${attachmentId} message=${error instanceof Error ? error.message : \"unknown\"}`)\n })\n }\n // file_path / URI は絶対にログへ出さない (内部パス非露出の方針と整合させる)\n attachmentLogger.error(`${status} attachment=${attachmentId} viewer=${internalUserId} reason=${result.reason} code=${result.code ?? \"-\"}`)\n return fail(status, ATTACHMENT_FAILURE_MESSAGES[status] ?? \"Attachment storage unavailable\")\n }\n\n // インライン許可リスト外は宣言型ごと octet-stream へ落とす。構文検証だけでは\n // text/html や image/svg+xml の自オリジン実行を止められない\n const declared = sanitizeMediaType(attachment.fileType) ?? sanitizeMediaType(result.contentType) ?? \"application/octet-stream\"\n const inlineSafe = INLINE_SAFE_MEDIA_TYPES.has(declared)\n const wantsDownload = new URL(request.url).searchParams.get(\"download\") === \"1\"\n const contentType = inlineSafe ? declared : \"application/octet-stream\"\n const disposition = inlineSafe && !wantsDownload ? \"inline\" : \"attachment\"\n\n // HEAD は本体を読まないので `bytes` が空になる。RFC 9110 §9.3.2 が要求する\n // 「GET と同じ Content-Length」を満たすため、ポートが申告した実サイズを優先する\n const contentLength = result.size ?? result.bytes.byteLength\n const headers = new Headers({\n \"Content-Type\": contentType,\n \"Content-Length\": String(contentLength),\n \"Content-Disposition\": `${disposition}; filename=\"${asciiFallbackFileName(attachment.fileName)}\"; filename*=UTF-8''${encodeRfc8187(attachment.fileName)}`,\n \"Cache-Control\": \"private, no-store\",\n \"X-Content-Type-Options\": \"nosniff\",\n \"Content-Security-Policy\": \"default-src 'none'; sandbox\",\n \"X-Frame-Options\": \"SAMEORIGIN\",\n })\n if (sanitizedCookie) headers.append(\"Set-Cookie\", sanitizedCookie)\n\n // 実体を確認できたので消失記録を解除する。これが唯一の自動回復経路であり、\n // 片方向のままだと一時障害で立った absent を利用者が自力で戻せない\n // (UI は absent の添付にリンクを描画しないため再取得の手段が消える)\n void service.markAttachmentPresent(attachmentId).catch((error: unknown) => {\n attachmentLogger.error(`markAttachmentPresent failed attachment=${attachmentId} message=${error instanceof Error ? error.message : \"unknown\"}`)\n })\n\n attachmentLogger.info(`200 attachment=${attachmentId} viewer=${internalUserId} type=${contentType} disposition=${disposition} bytes=${contentLength}`)\n // HEAD は本文を持たないので即座に解放する。GET は送出完了まで保持し、\n // 「同時実行上限 × 上限バイト数」というヒープ見積りを実際に成立させる\n if (isHead) {\n return new Response(null, { status: 200, headers })\n }\n releaseDeferredToStream = true\n return new Response(streamAndRelease(result.bytes, releaseGate, ATTACHMENT_BODY_FLUSH_TIMEOUT_MS), { status: 200, headers })\n } finally {\n // 送出へ引き渡した場合を除き、早期 return でも例外でも必ずここで返す\n if (!releaseDeferredToStream) releaseGate()\n }\n } catch (error) {\n // 認証ポートはリダイレクトを Response として throw する。これは正常な制御フローなので素通しする\n if (error instanceof Response) throw error\n attachmentLogger.error(`500 attachment=? viewer=? reason=unexpected message=${error instanceof Error ? error.message : \"unknown\"}`)\n return fail(500, \"Attachment request failed\")\n }\n }\n\n return {\n index: { loader: indexLoader },\n api: { loader: apiLoader, action: apiAction },\n sse: { loader: sseLoader },\n attachment: { loader: attachmentLoader },\n }\n}\n","/**\n * Data-access service for daily reports (drizzle mssql), fully DI-configured.\n * 日報データアクセスサービス (drizzle mssql)。依存はすべて DI で注入する。\n *\n * キャッシュキー・無効化順序・SSE publish 順序を含むデータアクセスロジックの中核。\n */\n\nimport { runWithTransactionRetry, type TransactionRetryOptions } from \"@aiquants/mssql\"\nimport { aliasedTable, and, asc, desc, eq, getColumns, inArray, isNull, or, sql } from \"drizzle-orm\"\nimport { normalizeBusinessDateKey } from \"../shared/business-date\"\nimport { compareIdsStreamOrder, isIdsStreamOrdered } from \"../shared/ids-stream\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport { commentAddMessageSchema, commentDeleteMessageSchema, reportCreateMessageSchema, reportDeleteMessageSchema, reportPublishMessageSchema, reportUpdateMessageSchema, statusUpdateMessageSchema } from \"../shared/sse-schema\"\nimport type { DailyReportAttachmentSummary, DailyReportDetail, DailyReportExternalComment, DailyReportInterviewer, DailyReportItem, DailyReportLabelDef, DailyReportPostedComment } from \"../shared/types\"\nimport type { EpochStore, SqlResultCache, SqlResultCacheQueryOptions } from \"./cache\"\nimport type { DailyReportExternalSource } from \"./external-source\"\nimport type { DailyReportEncodeUserId, DailyReportIdCodec, DailyReportRedisProvider, DailyReportResolveUserId } from \"./ports\"\nimport type { DailyReportTables, DailyReportUserTable } from \"./schema\"\nimport { buildVisibleSourceTypeCondition, computeViewerScopeDigest, DailyReportViewerScope, deniesEverySourceType, isSourceTypeVisible, type VisibleSourceTypeSet } from \"./visibility\"\n\nexport type { SqlResultCacheQueryOptions }\n\n// ---- 行型 (drizzle $inferSelect の構造的置き換え) ----\n\n/** DailyReportHub の行型。 */\nexport type DailyReportHubRow = {\n id: number\n sourceType: string\n sourceId: string\n sourceIdNum: number | null\n businessDate: Date | string | null\n userId: number | null\n title: string | null\n summary: string | null\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n deletedAt: Date | string | null\n deletedBy: string | null\n}\n\n/** DailyReportInternal の行型。 */\nexport type DailyReportInternalRow = {\n hubId: number\n body: string | null\n metadata: string | null\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n}\n\n/** DailyReportUserStatus の行型。 */\nexport type DailyReportUserStatusRow = {\n hubId: number\n userId: number\n isRead: boolean\n isStarred: boolean\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n}\n\n/** 投稿コメント行 (物理テーブル DailyReportComment) の行型。 */\nexport type DailyReportCommentRow = {\n id: number\n hubId: number\n userId: number\n body: string\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n}\n\n// ---- 必要最小の drizzle ビルダ形 (公開契約はサービス関数のシグネチャで厳格化) ----\n\ntype Rows<T> = PromiseLike<T[]>\ninterface SelectChain<T> extends Rows<T> {\n from(t: unknown): SelectChain<T>\n innerJoin(t: unknown, on: unknown): SelectChain<T>\n leftJoin(t: unknown, on: unknown): SelectChain<T>\n where(cond: unknown): SelectChain<T>\n orderBy(...cols: unknown[]): SelectChain<T>\n top(n: number): SelectChain<T>\n}\ninterface InsertChain<T> {\n output(): { values(v: unknown): PromiseLike<T[]> }\n values(v: unknown): PromiseLike<unknown>\n}\ninterface UpdateChain<T> {\n set(v: unknown): {\n where(cond: unknown): PromiseLike<unknown>\n output(): { where(cond: unknown): PromiseLike<T[]> }\n }\n}\ninterface DeleteChain {\n where(cond: unknown): PromiseLike<unknown>\n}\n\n/** サービスが要求する drizzle mssql データベースの最小面 (トランザクション込み)。 */\nexport interface DailyReportDb {\n select<T = Record<string, unknown>>(fields?: unknown): SelectChain<T>\n insert<T = Record<string, unknown>>(t: unknown): InsertChain<T>\n update<T = Record<string, unknown>>(t: unknown): UpdateChain<T>\n delete(t: unknown): DeleteChain\n transaction<T>(fn: (tx: DailyReportDb) => Promise<T>, config?: unknown): Promise<T>\n}\n\n/** getColumns へ構造型テーブルを渡すための境界キャスト。 */\nconst cols = (t: unknown) => getColumns(t as Parameters<typeof getColumns>[0])\n\n// ---- サービス設定 ----\n\nexport type DailyReportServiceConfig = {\n /** drizzle mssql データベースハンドル。 */\n db: DailyReportDb\n /** 日報 7 テーブル (アプリ既存モデルまたは defineDailyReportSchema 生成物)。`attachment` のみ任意。 */\n tables: DailyReportTables\n /** 表示名解決に使う外部ユーザーテーブル ({ id, displayName })。 */\n userTable: DailyReportUserTable\n /** 外部ユーザー ID → 内部数値 ID の解決ポート。 */\n resolveUserId: DailyReportResolveUserId\n /** 内部数値 ID の難読化ポート。 */\n encodeUserId: DailyReportEncodeUserId\n /** SSE publish / epoch 用 redis (省略時は SSE publish スキップ・epoch 無効)。 */\n redis?: DailyReportRedisProvider\n /** レガシー等の外部ソースアダプタ群。 */\n externalSources?: DailyReportExternalSource[]\n /** SQL 結果キャッシュ (facade が生成して注入)。 */\n cache: SqlResultCache\n /** クロスワーカー epoch ストア (facade が生成して注入)。 */\n epochs: EpochStore\n /**\n * Retry policy for transactions SQL Server chose as the deadlock victim. See `@aiquants/mssql`.\n * サーバがロールバックしたトランザクションの再試行方針。\n *\n * 鍵の範囲が重なる並行操作はデッドロックしうる。犠牲に選ばれた側はサーバによって\n * ロールバックされる — 何もコミットされていないので、書き直す再実行が正しい。既定で 3 回試す。\n * `{ attempts: 1 }` で再試行しない。\n */\n transactionRetry?: TransactionRetryOptions\n /**\n * 下書きラベル名 (単一または配列で指定可能)。\n * 消費アプリ側で自 DB のラベル名や作成区分の候補名 (\"下書き\", \"DRAFT\" など) を注入できる。\n */\n draftLabelName?: string\n draftLabelNames?: string[]\n /** SSE Redis Stream キー (既定 \"daily-report:sse-stream\")。 */\n streamKey?: string\n /** SSE Stream の MAXLEN (既定 10000)。 */\n streamMaxLen?: number\n /** IDs 一覧キャッシュ TTL (既定 180,000ms)。 */\n idsTtlMs?: number\n /** 営業日別キャッシュ TTL (既定 300,000ms)。 */\n businessDateTtlMs?: number\n /** 外部 ID → 内部 ID のプロセス内キャッシュを無効化 (テスト用)。 */\n disableUserIdCache?: boolean\n /**\n * 添付 ID の難読化コーデック。\n *\n * 任意にしているのは、添付テーブルを注入しない消費アプリと既存テストのフィクスチャを\n * 壊さないため。未注入なら添付一覧は常に空になる (縮退動作)。\n * ユーザー ID 用のコーデックとは **別インスタンス**を渡すこと。同一だと\n * レスポンス中の `userId` トークンがそのまま添付トークンとして解釈できてしまう。\n */\n attachmentIdCodec?: DailyReportIdCodec\n /** ロガー (既定は console ベース)。 */\n logger?: DailyReportLogger\n}\n\n/** createDailyReportService の返却型。 */\nexport type DailyReportService = ReturnType<typeof createDailyReportService>\n\n/**\n * Creates the daily-report data-access service bound to the injected dependencies.\n * 注入された依存に束縛された日報データアクセスサービスを生成する処理。\n */\ntype DatePattern = \"YYYY-MM-DD HH:mm:ss\" | \"YYYY-MM-DD\"\n\n/**\n * Formats date-like values to a given pattern using local-time fields (no Intl allocation).\n * 日付相当の値をローカル時刻フィールドで指定パターンへ整形する処理 (Intl 非生成)。\n *\n * ローカル時刻フィールドの手組み整形。旧実装 (ja-JP ロケール + 2-digit toLocale*) と出力互換。\n * ❗ toLocale* 系へ戻さないこと: あれは呼び出しごとに Intl.DateTimeFormat を生成し\n * (実測 約 66μs/呼)、全量 ids の 227k 行 map で 15〜20 秒の同期 CPU 洪水になる。\n * business-date / SSE の全応答が人質化した実害バグの根本原因だった。\n * モジュールスコープの export であり、バレル (src/server.ts の `export *`) 経由で\n * `@aiquants/daily-report/server` の公開 API の一部になる (テストからの直接検証と\n * ホスト側での汎用日時整形の再利用を意図した、意図的な公開)。\n * 1000 年未満の年は旧実装 (年ゼロ詰めなし) から意図的に乖離し、ISO 準拠の 4 桁ゼロ詰めで出す\n * (負の年は符号を除いた絶対値を 4 桁詰め。DB の date/datetime からは到達しない文字列入力のみの領域)。\n */\nexport const formatDateValue = (value: Date | string | null | undefined, pattern: DatePattern): string | null => {\n // null / undefined / 空文字は null に正規化\n if (!value) {\n return null\n }\n\n // Date 以外 (文字列) は Date コンストラクタで解釈\n const dateValue = value instanceof Date ? value : new Date(value)\n if (Number.isNaN(dateValue.getTime())) {\n // 解釈不能な文字列は原文のまま返す (旧実装と同一の不変挙動)\n return typeof value === \"string\" ? value : null\n }\n\n // 年月日をゼロ詰めで組み立てる (負の年は符号を除いた絶対値を 4 桁詰め = ISO 8601 拡張表記に整合。\n // String(-5).padStart は \"00-5\" と符号ごと詰めてしまうため符号を分離する)\n const year = dateValue.getFullYear()\n const yyyy = year < 0 ? `-${String(-year).padStart(4, \"0\")}` : String(year).padStart(4, \"0\")\n const mm = String(dateValue.getMonth() + 1).padStart(2, \"0\")\n const dd = String(dateValue.getDate()).padStart(2, \"0\")\n if (pattern === \"YYYY-MM-DD\") {\n return `${yyyy}-${mm}-${dd}`\n }\n // 時分秒付きパターン\n const hh = String(dateValue.getHours()).padStart(2, \"0\")\n const mi = String(dateValue.getMinutes()).padStart(2, \"0\")\n const ss = String(dateValue.getSeconds()).padStart(2, \"0\")\n return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`\n}\n\nexport function createDailyReportService(config: DailyReportServiceConfig) {\n const { db, tables, userTable: users, resolveUserId, encodeUserId, redis, cache: sqlResultCache, epochs } = config\n const { hub: DailyReportHub, internal: DailyReportInternal, comment: DailyReportCommentModel, label: DailyReportLabel, hubLabel: DailyReportHub_Label, userStatus: DailyReportUserStatus } = tables\n const externalSources = config.externalSources ?? []\n const draftLabelNames = config.draftLabelNames && config.draftLabelNames.length > 0 ? config.draftLabelNames : [config.draftLabelName ?? \"下書き\"]\n const draftLabelName = draftLabelNames[0] ?? \"下書き\"\n const logger = config.logger ?? createLogger(LogLevel.INFO, \"[DailyReportService]\")\n /**\n * 全トランザクションの唯一の入口。サーバがロールバックした場合だけやり直す。\n *\n * ⚠️ **閉包を DB の外へ波及させないこと。** 再実行は毎回はじめから走るため、ロールバックで元に\n * 戻らない副作用 (キャッシュ無効化・epoch 更新・SSE 配信) を閉包へ入れてはならない。現在それらは\n * すべてトランザクションの**後**に置かれている。\n */\n // 閉包を capture で包む。包まないと ORM が 1205 を EABORT へ置き換えた後しか見えず、\n // デッドロックだと判定できなくなる\n const inTransaction = <T>(fn: (tx: DailyReportDb) => Promise<T>): Promise<T> => runWithTransactionRetry((capture) => db.transaction((tx) => capture(() => fn(tx))), config.transactionRetry)\n\n const DAILY_REPORT_IDS_CACHE_KEY = \"daily-report:ids\"\n const DAILY_REPORT_IDS_TTL_MS = config.idsTtlMs ?? 180_000\n const DAILY_REPORT_IDS_EPOCH_KEY = \"daily-report:ids:epoch\"\n\n const DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX = \"daily-report:business-date:\"\n const DAILY_REPORT_BUSINESS_DATE_TTL_MS = config.businessDateTtlMs ?? 300_000\n\n /** 営業日キャッシュのクロスワーカー epoch プレフィックス */\n const DAILY_REPORT_DATE_EPOCH_PREFIX = \"daily-report:date-epoch:\"\n /** レポート詳細キャッシュのクロスワーカー epoch プレフィックス */\n const DAILY_REPORT_DETAIL_EPOCH_PREFIX = \"daily-report:detail-epoch:\"\n\n /**\n * SSE ワイヤ形式の版。埋め込み detail スキーマの互換を破る変更ごとに上げる。\n *\n * ❗ SSE の実体は配備を跨いで残る永続 Redis Stream (MAXLEN トリムのみ・時間上限なし)。\n * スキーマの必須キーを変えると、旧形式エントリが再接続 catch-up の safeParse で全滅する。\n * 版をキーへ焼き込むことで旧エントリは**構造的に到達不能**になり、スキーマに寿命無期限の\n * 互換シムを飼う必要がなくなる。設定キー (env 上書き含む) にも接尾辞を適用する —\n * 消費側の設定が版分離を黙って無効化できてはならない。\n * 旧版のストリームは xAdd が止まりトリムも走らないため残置される (手動 DEL 可)。\n * v2: comments/commentItems → externalComments/postedComments の改名 (2026-08)。\n */\n const DAILY_REPORT_SSE_WIRE_VERSION = \"v2\"\n const DAILY_REPORT_SSE_STREAM_KEY = `${config.streamKey ?? \"daily-report:sse-stream\"}:${DAILY_REPORT_SSE_WIRE_VERSION}`\n const DAILY_REPORT_SSE_STREAM_MAXLEN = config.streamMaxLen ?? 10000\n\n const incrementRedisEpoch = (key: string) => epochs.incrementEpoch(key)\n\n /**\n * Binds the shared visibility condition builder to this service's hub table.\n * 共有の可視性条件ビルダーを本サービスの hub テーブルへ束縛したもの。\n *\n * 三値の意味と fail-close の方向は {@link buildVisibleSourceTypeCondition} が唯一の真実源。\n */\n const visibleSourceTypeCondition = (visible: VisibleSourceTypeSet) => buildVisibleSourceTypeCondition(DailyReportHub.sourceType, visible)\n\n // ---- キャッシュキー生成 (ダイジェスト折り込みと無効化接頭辞の単一の真実源) ----\n // ❗ ユーザー別キャッシュキーは必ずここから作る。可視集合ダイジェストの付け忘れ (= 別権限へ\n // 古い結果を配信) と、無効化時の取りこぼしを構造的に防ぐ。接頭辞の末尾 \":\" は\n // ユーザー ID の前方一致衝突 (1 と 10) を避けるため必須。\n\n /** ID 一覧キャッシュのキー (ユーザー + 可視集合ダイジェスト)。 */\n const idsCacheKey = (userId: number, digest: string): string => `${DAILY_REPORT_IDS_CACHE_KEY}:user:${userId}:st:${digest}`\n /** ID 一覧キャッシュの当該ユーザー全ダイジェスト分の接頭辞。 */\n const idsUserPrefix = (userId: number): string => `${DAILY_REPORT_IDS_CACHE_KEY}:user:${userId}:`\n /** 営業日別キャッシュのキー (営業日 + ユーザー + 可視集合ダイジェスト)。 */\n const dateCacheKey = (normalizedBusinessDate: string, userId: number, digest: string): string => `daily-report:date:${normalizedBusinessDate}:user:${userId}:st:${digest}`\n /** 営業日別キャッシュの当該ユーザー全ダイジェスト分の接頭辞。 */\n const dateUserPrefix = (normalizedBusinessDate: string, userId: number): string => `daily-report:date:${normalizedBusinessDate}:user:${userId}:`\n /** 詳細キャッシュのキー (hub + ユーザー + 可視集合ダイジェスト)。 */\n const detailCacheKey = (reportHubId: number, userId: number, digest: string): string => `daily-report:detail:${reportHubId}:user:${userId}:st:${digest}`\n /** 詳細キャッシュの当該ユーザー全ダイジェスト分の接頭辞。 */\n const detailUserPrefix = (reportHubId: number, userId: number): string => `daily-report:detail:${reportHubId}:user:${userId}:`\n\n /**\n * Publishes a message to the SSE Redis Stream.\n * SSE 用の Redis Stream にメッセージを追加する。\n */\n const publishToSseStream = async (message: Record<string, unknown>, callerName: string): Promise<void> => {\n const client = await redis?.getClient()\n if (!client) {\n logger.warn(`[SSE] Redis client unavailable (${callerName})`)\n return\n }\n const publishStartMs = Date.now()\n try {\n await client.xAdd(DAILY_REPORT_SSE_STREAM_KEY, \"*\", { data: JSON.stringify(message) }, { TRIM: { strategy: \"MAXLEN\", strategyModifier: \"~\", threshold: DAILY_REPORT_SSE_STREAM_MAXLEN } })\n const publishDurationMs = Date.now() - publishStartMs\n if (publishDurationMs > 1000) {\n logger.warn(`[SSE] Slow publish (${callerName}): ${publishDurationMs}ms`)\n }\n } catch (e) {\n logger.error(`[SSE] Redis publish failed (${callerName}):`, e)\n }\n }\n\n /**\n * Masks a raw audit-actor value (created_by / updated_by) before client exposure.\n * 監査列 (created_by / updated_by) をクライアント公開前にマスクする処理。\n *\n * Internal 由来の値は String(userId) の生内部 ID のため、数値なら encodeUserId で難読化する。\n * 非数値 (外部ソース由来の氏名やシステム文字列) はそのまま返す。null / 空文字は null。\n * userId フィールドと同じ難読化を監査列にも適用し、内部 ID の横流し漏洩を防ぐ。\n */\n const maskAuditActor = (value: string | null | undefined): string | null => {\n if (value == null || value === \"\") return null\n return /^\\d+$/.test(value) ? encodeUserId(Number(value)) : value\n }\n\n type RawJsonComment = {\n id: number\n content: string\n createdAt: string\n userId: number\n userName: string\n }\n\n type RawJsonLabel = {\n id: number\n name: string\n color: string | null\n }\n\n /** 詳細クエリ 1 行 (外部ソース列は `ext_<sourceType>` キーで同居)。 */\n type HubQueryRow = {\n hub: DailyReportHubRow\n internal: DailyReportInternalRow | null\n isRead?: boolean | null\n isStarred?: boolean | null\n creatorName?: string | null\n } & Record<string, unknown>\n\n type HubRecord = HubQueryRow & {\n labels?: RawJsonLabel[]\n /** パッケージ自身のコメント表 (投稿コメント) の行。detail の postedComments になる。 */\n postedCommentRows?: RawJsonComment[]\n /**\n * 添付ファイル一覧。**任意にしない。**\n * `?` を付けると `mapHubRecord` 側で `?? []` が必要になり、注入を忘れた読み取り経路が\n * 黙って空配列を返す。必須にしておけば注入漏れは全呼び出し箇所でコンパイルエラーになる。\n */\n attachments: DailyReportAttachmentSummary[]\n }\n\n /** 外部ソースアダプタの select 追加フィールドを構築する。 */\n const externalSelections = (): Record<string, unknown> => {\n const out: Record<string, unknown> = {}\n for (const adapter of externalSources) {\n out[`ext_${adapter.sourceType}`] = cols(adapter.table)\n }\n return out\n }\n\n /** 外部ソースアダプタの LEFT JOIN を select チェーンへ適用する。 */\n const applyExternalJoins = <T>(chain: SelectChain<T>): SelectChain<T> => {\n let c = chain\n for (const adapter of externalSources) {\n c = c.leftJoin(adapter.table, and(eq(DailyReportHub.sourceType, adapter.sourceType), eq(DailyReportHub.sourceIdNum, adapter.idColumn)))\n }\n return c\n }\n\n /**\n * Converts a Hub record into a DailyReportDetail structure, applying the viewer's comment visibility.\n * Hub レコードを DailyReportDetail に変換し、閲覧者のコメント可視性を適用する処理。\n *\n * ❗ **コメント 2 面の唯一の生成点**。詳細取得 / 営業日一覧 / 管理経路 / publish 応答の全てが\n * ここを通るため、コメント次元の射影はここ 1 箇所に置く。SQL の `WHERE` 側で絞るのは不可 —\n * コメント表を引くクエリは `postedComments` にしか効かず、レガシー `comments` は外部ソース\n * アダプタの出力なのでクエリを通らない (半分しか塞げない)。\n *\n * ❗ 秘匿は**必ず空配列**で行う。キーごと省くと SSE 側は `dailyReportDetailSchema` の必須キー\n * 欠落で `safeParse` に失敗し、メッセージ全体が fail-close で落ちる (更新が永久に届かない)。\n * HTTP 側もクライアントの `DailyReportCache.merge` が `postedComments` を無ガードで参照する。\n *\n * @param scope Viewer scope carrying the comment dimension. コメント次元を運ぶ閲覧範囲。\n * @param row Hub record to convert. 変換対象の Hub レコード。\n * @param currentUserId Viewer's raw internal user id, if known. 判明していれば閲覧者の生内部ユーザー ID。\n * @returns The detail structure. 変換した詳細構造。\n */\n const mapHubRecord = (scope: DailyReportViewerScope, row: HubRecord, currentUserId?: number): DailyReportDetail => {\n const { hub, internal } = row\n\n let content = hub.summary\n let interviewers: DailyReportInterviewer[] = []\n let category: string | null = null\n let creationCategory: string | null = null\n let visitTimeFrom: string | null = null\n let visitTimeTo: string | null = null\n let customerName: string | null = null\n let employeeName: string | null = null\n let externalComments: DailyReportExternalComment[] = []\n\n // 外部ソースアダプタ優先 → Internal の順で表示フィールドを解決する\n const adapter = externalSources.find((a) => a.sourceType === hub.sourceType)\n const externalRow = adapter ? (row[`ext_${adapter.sourceType}`] as Record<string, unknown> | null | undefined) : undefined\n if (adapter && externalRow) {\n const fields = adapter.mapRow(externalRow)\n if (fields.content !== undefined) content = fields.content\n if (fields.employeeName !== undefined) employeeName = fields.employeeName\n if (fields.category !== undefined) category = fields.category\n if (fields.creationCategory !== undefined) creationCategory = fields.creationCategory\n if (fields.visitTimeFrom !== undefined) visitTimeFrom = fields.visitTimeFrom\n if (fields.visitTimeTo !== undefined) visitTimeTo = fields.visitTimeTo\n if (fields.customerName !== undefined) customerName = fields.customerName\n if (fields.interviewers !== undefined) interviewers = fields.interviewers\n if (fields.externalComments !== undefined) externalComments = fields.externalComments\n } else if (hub.sourceType === \"Internal\" && internal) {\n content = internal.body\n // Internal specific mappings if any\n }\n\n const labelsRaw = (row.labels ?? []).map((l) => ({\n id: l.id,\n name: l.name,\n color: l.color,\n }))\n const postedCommentsRaw = row.postedCommentRows ?? []\n\n // コメント次元がこの区分を拒否するなら、コメント 2 面を落として返す。\n // ❗ 自分のコメントだけは常に残す (要件 5)。隠すと「UI に見えないのに削除だけできる」\n // 非対称が生まれ、権限剥奪の瞬間に既存の自分のコメントが恒久的に削除不能になる。\n // 外部由来 `externalComments` はユーザー ID を持たない構造なので区分単位の二値にしかできない\n const commentsVisible = isSourceTypeVisible(scope.commentable, hub.sourceType)\n\n // 作成区分がアプリ設定の下書き候補に合致する場合、ラベル一覧に下書きラベルが含まれるよう補完\n const isCreationCategoryDraft = creationCategory ? draftLabelNames.includes(creationCategory) : false\n // 同期関数なので DB へは問い合わせず、既に解決済みのキャッシュがある場合だけ補完する\n const cachedPrimaryDraftLabelId = cachedDraftLabelIds?.[0] ?? null\n if (isCreationCategoryDraft && cachedPrimaryDraftLabelId && !labelsRaw.some((l) => l.id === cachedPrimaryDraftLabelId)) {\n labelsRaw.push({\n id: cachedPrimaryDraftLabelId,\n name: draftLabelName,\n color: null,\n })\n }\n\n return {\n reportHubId: hub.id,\n date: formatDateValue(hub.businessDate, \"YYYY-MM-DD\"),\n author: row.creatorName ?? maskAuditActor(hub.createdBy) ?? \"\",\n userId: hub.userId ? encodeUserId(hub.userId) : \"\",\n sourceType: hub.sourceType ?? \"Internal\",\n createdAt: formatDateValue(hub.createdAt, \"YYYY-MM-DD HH:mm:ss\"),\n updatedAt: formatDateValue(hub.updatedAt, \"YYYY-MM-DD HH:mm:ss\"),\n updatedBy: maskAuditActor(hub.updatedBy),\n employeeName: employeeName ?? row.creatorName ?? maskAuditActor(hub.createdBy),\n category,\n creationCategory,\n visitTimeFrom,\n visitTimeTo,\n customerName,\n interviewers,\n subject: hub.title,\n content,\n externalComments: commentsVisible ? externalComments : [],\n isRead: row.isRead ?? false,\n isStarred: row.isStarred ?? false,\n labels: labelsRaw,\n postedComments: postedCommentsRaw\n .filter((c) => commentsVisible || (currentUserId !== undefined && c.userId === currentUserId))\n .map((c) => ({\n ...c,\n userId: encodeUserId(c.userId),\n isMine: currentUserId ? c.userId === currentUserId : false,\n })),\n // `?? []` を書かない。HubRecord 側で必須にしてあるため、注入漏れはここではなく\n // 呼び出し箇所でコンパイルエラーになる (それが検知したい事象)。\n attachments: row.attachments,\n }\n }\n\n /**\n * Fetches daily report IDs alongside normalized business dates.\n * 日報 ID と正規化済み営業日を取得する内部処理。\n *\n * ❗ 可視性述語は他の 3 経路 (営業日一覧 / 詳細取得 / 添付取得) と**意図的に 1 つ緩い**。\n * ここだけ `isNull(DailyReportHub.userId)` を追加で許しており、投稿者を持たない\n * 取込元の日報を一覧から消さないための緩和である。返すのは ID・営業日・ソース種別だけで\n * 本文も添付も含まないため、緩めても内容は漏れない。\n * この差分は無自覚なものではなく、`service.spec.ts` が明示的に表明して固定している。\n * 他の 3 経路をこの形に合わせてはならない (合わせると内容が漏れる)。\n *\n * 区分 (sourceType) の可視性は緩和の対象外であり、他経路と完全に同一の条件を適用する。\n *\n * @param userId Internal numeric user id. 内部数値ユーザー ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Stream-ordered items. ストリーム順のアイテム一覧。\n */\n const fetchDailyReportIdsByUserId = async (scope: DailyReportViewerScope, userId: number): Promise<DailyReportItem[]> => {\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n\n const result = await db\n .select<{ reportHubId: number; businessDate: Date | string | null; sourceType: string }>({\n reportHubId: DailyReportHub.id,\n businessDate: DailyReportHub.businessDate,\n sourceType: DailyReportHub.sourceType,\n })\n .from(DailyReportHub)\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .where(and(isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DailyReportHub.userId), isNull(DraftLabelRelation.hubId)), visibleSourceTypeCondition(scope.visible)))\n .orderBy(desc(DailyReportHub.businessDate), desc(DailyReportHub.id))\n\n return result.map((item) => ({\n ...item,\n businessDate: formatDateValue(item.businessDate, \"YYYY-MM-DD\"),\n }))\n }\n\n /**\n * Fetches only the first page of report ids for time-to-first-render (uncached).\n * 最初の描画までの時間専用に、ID 一覧の先頭ページだけを取得する内部処理 (キャッシュ無し)。\n *\n * `ids-stream` の新規接続でキャッシュミスした場合、全量クエリの確定を待たずに\n * この軽量クエリ (TOP n・インデックス順で n 行走査した時点で打ち切り) の結果を\n * 最初のチャンクとして送出する。可視性述語と並び順は全量クエリと完全同一。\n * `.top(n)` は `.select()` の直後でなければならない (drizzle-mssql の構造的制約)。\n *\n * @param externalId External user id. 外部ユーザー ID。\n * @param limit Page size. ページサイズ。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns First-page items in stream order. ストリーム順の先頭ページ。\n */\n const getDailyReportIdsFirstPageByExternalId = async (scope: DailyReportViewerScope, externalId: string, limit: number): Promise<DailyReportItem[]> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return []\n }\n // 全拒否なら DB を引かずに空を返す (0 件 = fail-close)\n if (deniesEverySourceType(scope.visible)) {\n return []\n }\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n\n const result = await db\n .select<{ reportHubId: number; businessDate: Date | string | null; sourceType: string }>({\n reportHubId: DailyReportHub.id,\n businessDate: DailyReportHub.businessDate,\n sourceType: DailyReportHub.sourceType,\n })\n .top(limit)\n .from(DailyReportHub)\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .where(and(isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DailyReportHub.userId), isNull(DraftLabelRelation.hubId)), visibleSourceTypeCondition(scope.visible)))\n .orderBy(desc(DailyReportHub.businessDate), desc(DailyReportHub.id))\n\n return result.map((item) => ({\n ...item,\n businessDate: formatDateValue(item.businessDate, \"YYYY-MM-DD\"),\n }))\n }\n\n /**\n * Fetches daily report details for the provided business date.\n * 指定した営業日の日報詳細を取得する内部処理。\n */\n const fetchDailyReportsByBusinessDate = async (scope: DailyReportViewerScope, normalizedBusinessDate: string): Promise<DailyReportDetail[]> => {\n const rows = await applyExternalJoins(\n db\n .select<HubQueryRow>({\n hub: cols(DailyReportHub),\n internal: cols(DailyReportInternal),\n creatorName: users.displayName,\n ...externalSelections(),\n })\n .from(DailyReportHub),\n )\n .leftJoin(DailyReportInternal, eq(DailyReportHub.id, DailyReportInternal.hubId))\n .leftJoin(users, eq(DailyReportHub.userId, users.id))\n .where(and(eq(DailyReportHub.businessDate, sql`${normalizedBusinessDate}`), isNull(DailyReportHub.deletedAt), visibleSourceTypeCondition(scope.visible)))\n .orderBy(desc(DailyReportHub.id))\n\n const attachmentsMap = await getAttachmentsByHubIds(rows.map((r) => r.hub.id))\n return rows.map((row) => mapHubRecord(scope, { ...row, attachments: attachmentsMap.get(row.hub.id) ?? [] }))\n }\n\n /**\n * Builds a deterministic cache key for business-date caches.\n * 営業日キャッシュ用の一意キーを生成する処理。\n */\n const buildBusinessDateCacheKey = (normalizedBusinessDate: string): string => {\n return `${DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX}${normalizedBusinessDate}`\n }\n\n /**\n * Resolves the internal user ID from an external ID (with process-local caching).\n * 外部 ID から内部ユーザー ID を解決する (プロセス内キャッシュ付き)。\n */\n const userIdCache = new Map<string, number>()\n\n const getUserIdByExternalId = async (externalId: string): Promise<number | null> => {\n if (!config.disableUserIdCache && userIdCache.has(externalId)) {\n return userIdCache.get(externalId) ?? null\n }\n\n const userId = await resolveUserId(externalId)\n if (userId !== null && !config.disableUserIdCache) {\n userIdCache.set(externalId, userId)\n }\n return userId\n }\n\n /**\n * Retrieves daily report IDs and their normalized business dates.\n * 日報 ID と正規化済み営業日を取得する処理。\n *\n * @param externalId External user id. 外部ユーザー ID。\n * @param options Cache query options. キャッシュ問い合わせオプション。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Stream-ordered items. ストリーム順のアイテム一覧。\n */\n const getDailyReportIdsByExternalId = async (scope: DailyReportViewerScope, externalId: string, { forceRefresh = false, snapshot = false, ttlMsOverride, onFetchStart }: SqlResultCacheQueryOptions & { onFetchStart?: () => void } = {}): Promise<DailyReportItem[]> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return []\n }\n // 全拒否なら DB を引かずに空を返す (0 件 = fail-close)\n if (deniesEverySourceType(scope.visible)) {\n return []\n }\n\n // 可視集合ごとに別バケットへ隔離する (権限変更後に旧結果を配信しない)\n const cacheKey = idsCacheKey(userId, computeViewerScopeDigest(scope))\n return sqlResultCache.getOrFetch<DailyReportItem>({\n cacheKey,\n fetcher: async () => {\n const res = await fetchDailyReportIdsByUserId(scope, userId)\n // ❗ 並んでいない配列はカーソル再開の前提を破る (findIdsStreamResumeIndex は\n // 比較器で「最初に後ろへ来る位置」を探すため、並びが崩れていると誤った位置から\n // 再開する)。原因は SQL の ORDER BY と compareIdsStreamOrder の乖離、または\n // RCSI の無い配備でのスキャン中の行移動 (実運用 DB は dev / 本番とも\n // READ_COMMITTED_SNAPSHOT = ON を実機確認済みで、その配備では後者は起きない —\n // 2026-08-15 検証)。fetcher 内で治癒するのは、\n // ①検査が fetch 1 回につき 1 度で済み ②バケットとスナップショットトークンが\n // 治癒済み配列を見る、の両方を成立させるため。\n // ❗ 並べ直しても欠落・重複は直らない — 直るのはカーソル再開の二次被害だけ\n if (!isIdsStreamOrdered(res)) {\n logger.warn(\"[DailyReportService] ids array is not in stream order; re-sorting (check SQL ORDER BY vs compareIdsStreamOrder, or concurrent row movement during the scan)\")\n return [...res].sort(compareIdsStreamOrder)\n }\n return res\n },\n // 本呼び出しが実際に全量クエリを起動した瞬間だけ合図する (バケットヒット・相乗り・\n // refresh-ahead では呼ばれない)。ids ストリームはこれを高速先頭ページ起動の\n // 因果的な前提条件に使う\n onFetchStart,\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_IDS_TTL_MS,\n epochKey: DAILY_REPORT_IDS_EPOCH_KEY,\n // TTL の半分を過ぎたヒットは、返却後に裏で 1 本だけ再フェッチして差し替える。\n // 90 秒に 1 回以上アクセスがある限り、対話リクエストはコールド (全量再クエリ) を踏まない\n refreshAheadRatio: 0.5,\n }) as Promise<DailyReportItem[]>\n }\n\n /**\n * Retrieves daily report details for a business date (⚠️ NOT per-user filtered).\n * 指定した営業日に紐づく日報詳細を取得する処理 (⚠️ ユーザー別フィルタなし)。\n *\n * ⚠️ SECURITY: この関数は下書きラベルによる可視性フィルタ (他ユーザーの下書きを隠す) と\n * ユーザー別の既読/スター状態を **適用しない**。取得結果は全ユーザーの下書きを含み得るため、\n * HTTP レスポンス / SSR loader / SSE へ **直接返してはならない**。ユーザー向け配信には\n * 必ず {@link getDailyReportsByBusinessDateByExternalId} を使うこと。\n * (本関数は管理・バッチ・テスト用途に限定する。)\n */\n const getDailyReportsByBusinessDate = (scope: DailyReportViewerScope, businessDate: string, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail[]> => {\n const normalizedBusinessDate = normalizeBusinessDateKey(businessDate)\n if (!normalizedBusinessDate) {\n return Promise.resolve([])\n }\n // 全拒否なら DB を引かずに空を返す (0 件 = fail-close)\n if (deniesEverySourceType(scope.visible)) {\n return Promise.resolve([])\n }\n\n return sqlResultCache.getOrFetch<DailyReportDetail>({\n // 可視集合ごとに別バケットへ隔離する (管理経路でも権限混在を起こさない)\n cacheKey: `${buildBusinessDateCacheKey(normalizedBusinessDate)}:st:${computeViewerScopeDigest(scope)}`,\n fetcher: () => fetchDailyReportsByBusinessDate(scope, normalizedBusinessDate),\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_BUSINESS_DATE_TTL_MS,\n }) as Promise<DailyReportDetail[]>\n }\n\n /**\n * Retrieves every label id that counts as a draft.\n * 下書きとみなすラベル ID を**すべて**取得する処理 (キャッシュ付き)。\n *\n * ❗ 可視性述語は必ずこちら (複数形) を使うこと。`draftLabelNames` には複数のラベル名を\n * 注入できる (取込元ごとに下書きの呼び名が違うため) のに、1 個の ID だけで結合すると\n * **2 つ目以降の下書きラベルが付いた日報が誰にでも可視になる**。\n * 名前が 1 つも解決できない場合は空配列を返し、呼び出し側は結合対象を\n * 「存在しない ID」にして「下書きは存在しない」と解釈する。\n *\n * @returns Draft label ids, ascending. 昇順の下書きラベル ID 群。\n */\n let cachedDraftLabelIds: number[] | null = null\n const getDraftLabelIds = async (): Promise<number[]> => {\n if (cachedDraftLabelIds !== null) return cachedDraftLabelIds\n const rows = await db.select<{ id: number }>({ id: DailyReportLabel.id }).from(DailyReportLabel).where(inArray(DailyReportLabel.name, draftLabelNames)).orderBy(DailyReportLabel.id)\n cachedDraftLabelIds = rows.map((row) => row.id)\n return cachedDraftLabelIds\n }\n\n /**\n * Join targets for the draft-label relation, safe when no draft label exists.\n * 下書きラベル関連の結合対象。下書きラベルが 1 件も無い場合でも安全に働く値。\n *\n * `inArray` に空配列を渡すと方言によっては構文エラーや常に真になりうるため、\n * 実在しない ID (`-1`) を 1 個入れて「どの行にも一致しない」ことを明示する。\n *\n * @returns Ids to join on. 結合に使う ID 群。\n */\n const getDraftLabelJoinIds = async (): Promise<number[]> => {\n const ids = await getDraftLabelIds()\n return ids.length > 0 ? ids : [-1]\n }\n\n /**\n * Retrieves the canonical draft label id used when creating a draft.\n * 下書きを作成するときに付与する代表の下書きラベル ID を取得する処理。\n *\n * 付与は 1 個に決めなければならないため `draftLabelNames` の先頭に対応する ID を返す。\n * **可視性判定には使わないこと** (`getDraftLabelIds` を使う)。\n *\n * @returns The canonical draft label id, or null when it does not exist. 代表の下書きラベル ID (存在しなければ null)。\n */\n const getDraftLabelId = async (): Promise<number | null> => {\n const ids = await getDraftLabelIds()\n if (ids.length === 0) return null\n // 先頭の名前に対応する ID を選ぶ。ID 順ではなく注入順を正とする (代表名は draftLabelName)\n const [primary] = await db.select<{ id: number }>({ id: DailyReportLabel.id }).top(1).from(DailyReportLabel).where(eq(DailyReportLabel.name, draftLabelName))\n return primary?.id ?? ids[0]\n }\n\n /**\n * Derives the tri-state verification status of an attachment's backing object.\n * 添付ファイル実体の検証状態 (三値) を導出する処理。\n *\n * 判定不能 (`unknown`) を `absent` へ丸めない。存在確認そのものが失敗した場合を\n * 偽の真偽で埋めると、一時的な権限・通信障害が「消失」として表示される。\n */\n const deriveAttachmentState = (checkedAt: Date | string | null, missingAt: Date | string | null): DailyReportAttachmentSummary[\"state\"] => {\n if (missingAt != null) return \"absent\"\n if (checkedAt != null) return \"present\"\n return \"unknown\"\n }\n\n /**\n * Fetches attachments for multiple hubs in one query, keyed by hub id.\n * 複数 hub の添付ファイルを 1 クエリで取得し hub ID をキーにまとめる処理。\n *\n * 一覧描画は N 件の日報を同時に返すため、日報ごとに問い合わせると N+1 になる。\n * ラベル・コメントと同じ一括取得の流儀に揃える。\n *\n * @param hubIds Hub ids to fetch attachments for. 添付を取得する hub ID 群。\n * @returns Attachments grouped by hub id. hub ID ごとにまとめた添付一覧。\n */\n const getAttachmentsByHubIds = async (hubIds: number[]): Promise<Map<number, DailyReportAttachmentSummary[]>> => {\n const map = new Map<number, DailyReportAttachmentSummary[]>()\n const attachment = tables.attachment\n // codec は config 直参照。createDailyReportService の分割代入は 8 キーのみで、\n // 裸の識別子として書くと TS2304 になる (任意設定は config.x で読むのがこのファイルの様式)\n const codec = config.attachmentIdCodec\n if (hubIds.length === 0 || !attachment || !codec) {\n return map\n }\n\n const rows = await db\n .select<{ id: number; hubId: number; fileName: string; fileType: string | null; fileSize: number | null; createdAt: Date | string | null; objectCheckedAt: Date | string | null; objectMissingAt: Date | string | null }>({\n id: attachment.id,\n hubId: attachment.hubId,\n fileName: attachment.fileName,\n fileType: attachment.fileType,\n fileSize: attachment.fileSize,\n createdAt: attachment.createdAt,\n objectCheckedAt: attachment.objectCheckedAt,\n objectMissingAt: attachment.objectMissingAt,\n })\n .from(attachment)\n .where(inArray(attachment.hubId, hubIds))\n .orderBy(asc(attachment.createdAt), asc(attachment.id))\n\n for (const row of rows) {\n // 内部数値 ID はここで難読化トークンへ変換する。以降クライアントへ生 ID は出ない。\n // `uploaded_by` / `created_by` / `updated_by` / `file_path` は選択もしない\n const item: DailyReportAttachmentSummary = {\n id: codec.encode(Number(row.id)),\n fileName: String(row.fileName),\n fileType: row.fileType ? String(row.fileType) : null,\n fileSize: row.fileSize != null ? Number(row.fileSize) : null,\n createdAt: formatDateValue(row.createdAt, \"YYYY-MM-DD HH:mm:ss\"),\n state: deriveAttachmentState(row.objectCheckedAt, row.objectMissingAt),\n }\n const list = map.get(Number(row.hubId))\n if (list) {\n list.push(item)\n } else {\n map.set(Number(row.hubId), [item])\n }\n }\n\n return map\n }\n\n /**\n * Fetches one attachment together with its visibility check for a given user.\n * 指定ユーザーの可視性判定込みで添付ファイル 1 件を取得する処理。\n *\n * 認可は SQL 側で完結させる。アプリ層のフィルタリングに依存しない。\n * 認可されない / 存在しない / 親日報が論理削除済みのいずれでも `null` を返すため、\n * 呼び出し側は 403 と 404 を区別せず 404 とすること (存在オラクル回避)。\n *\n * 本関数は意図的に `SqlResultCache.getOrFetch` を通さない。これは読み取りキャッシュではなく\n * リクエストごとの**認可判定**であり、結果をキャッシュするとユーザーを跨いで再利用されうる。\n *\n * @param attachmentId Internal numeric attachment id. 内部数値の添付 ID。\n * @param userId Internal numeric user id of the viewer. 閲覧者の内部数値ユーザー ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Attachment row when visible, otherwise null. 可視なら添付行、それ以外は null。\n */\n const getAttachmentForUser = async (scope: DailyReportViewerScope, attachmentId: number, userId: number): Promise<{ fileName: string; filePath: string; fileType: string | null; fileSize: number | null } | null> => {\n const attachment = tables.attachment\n if (!attachment) {\n return null\n }\n // 全拒否なら DB を引かずに null を返す (0 件 = fail-close)\n if (deniesEverySourceType(scope.visible)) {\n return null\n }\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n\n // `.top(1)` は `.select()` の直後でなければならない。drizzle-mssql は top() を\n // db.select() が返す MsSqlSelectBuilder にしか定義しておらず、.from() 以降のクラスには\n // 存在しない。誤った順序は型 (構造型 SelectChain が全段階に top を宣言) でも\n // 単体テスト (モックが全段階で top を返す) でも検出されず、本番で TypeError になる。\n const rows = await db\n .select<{ fileName: string; filePath: string; fileType: string | null; fileSize: number | null }>({\n fileName: attachment.fileName,\n filePath: attachment.filePath,\n fileType: attachment.fileType,\n fileSize: attachment.fileSize,\n })\n .top(1)\n .from(attachment)\n .innerJoin(DailyReportHub, eq(DailyReportHub.id, attachment.hubId))\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .where(and(eq(attachment.id, attachmentId), isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DraftLabelRelation.hubId)), visibleSourceTypeCondition(scope.visible)))\n\n return rows[0] ?? null\n }\n\n /**\n * Records that an attachment's backing object was confirmed missing.\n * 添付ファイルの実体が消失していたことを記録する処理。\n *\n * 実配信で `not_found` を得たときに呼ぶ。次回描画で三値が `absent` になる。\n * 記録の失敗は配信結果に影響させない (呼び出し側は await しない)。\n *\n * @param attachmentId Internal numeric attachment id. 内部数値の添付 ID。\n */\n const markAttachmentMissing = async (attachmentId: number): Promise<void> => {\n const attachment = tables.attachment\n if (!attachment) return\n const now = new Date()\n try {\n await db.update(attachment).set({ objectCheckedAt: now, objectMissingAt: now, updatedAt: now }).where(eq(attachment.id, attachmentId))\n } catch (error) {\n logger.error(\"markAttachmentMissing failed\", error)\n }\n }\n\n /**\n * Records that an attachment's backing object was confirmed present.\n * 添付ファイルの実体が存在していたことを記録する処理。\n *\n * `markAttachmentMissing` と対にすること。消失記録が一方向だと、一時的な障害で\n * 一度 `absent` が立った添付は UI がリンクを描画しなくなり (押しても 404 なので当然)、\n * **利用者が再取得を発火させる手段そのものが消える**。実体が戻っても手動の\n * 一括検証を回すまで復帰しない。配信が 200 で成功した時点で解除するのが唯一の\n * 自動回復経路である。\n *\n * 記録の失敗は配信結果に影響させない (呼び出し側は await しない)。\n *\n * @param attachmentId Internal numeric attachment id. 内部数値の添付 ID。\n */\n const markAttachmentPresent = async (attachmentId: number): Promise<void> => {\n const attachment = tables.attachment\n if (!attachment) return\n const now = new Date()\n try {\n await db.update(attachment).set({ objectCheckedAt: now, objectMissingAt: null, updatedAt: now }).where(eq(attachment.id, attachmentId))\n } catch (error) {\n logger.error(\"markAttachmentPresent failed\", error)\n }\n }\n\n /**\n * Retrieves daily report details for a business date with user-specific status and relations.\n * 指定した営業日の日報詳細を、ユーザー状態と関連データを含めて取得する。\n */\n const getDailyReportsByBusinessDateByExternalId = async (scope: DailyReportViewerScope, businessDate: string, externalId: string, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail[]> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return []\n }\n\n const normalizedBusinessDate = normalizeBusinessDateKey(businessDate)\n if (!normalizedBusinessDate) {\n return []\n }\n // 全拒否なら DB を引かずに空を返す (0 件 = fail-close)\n if (deniesEverySourceType(scope.visible)) {\n return []\n }\n\n // 可視集合ごとに別バケットへ隔離する (権限変更後に旧結果を配信しない)\n const cacheKey = dateCacheKey(normalizedBusinessDate, userId, computeViewerScopeDigest(scope))\n\n return sqlResultCache.getOrFetch<DailyReportDetail>({\n cacheKey,\n fetcher: async () => {\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n\n const rows = await applyExternalJoins(\n db\n .select<HubQueryRow>({\n hub: cols(DailyReportHub),\n internal: cols(DailyReportInternal),\n isRead: DailyReportUserStatus.isRead,\n isStarred: DailyReportUserStatus.isStarred,\n creatorName: users.displayName,\n ...externalSelections(),\n })\n .from(DailyReportHub),\n )\n .leftJoin(DailyReportInternal, eq(DailyReportHub.id, DailyReportInternal.hubId))\n .leftJoin(DailyReportUserStatus, and(eq(DailyReportHub.id, DailyReportUserStatus.hubId), eq(DailyReportUserStatus.userId, userId)))\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .leftJoin(users, eq(DailyReportHub.userId, users.id))\n .where(and(eq(DailyReportHub.businessDate, sql`${normalizedBusinessDate}`), isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DraftLabelRelation.hubId)), visibleSourceTypeCondition(scope.visible)))\n .orderBy(desc(DailyReportHub.id))\n\n if (rows.length === 0) {\n return []\n }\n\n const hubIds = rows.map((r) => r.hub.id)\n\n // Drizzle ORM batch queries to fetch labels and comments\n const allLabels = await db\n .select<{ hubId: number; id: number; name: string; color: string | null }>({\n hubId: DailyReportHub_Label.hubId,\n id: DailyReportLabel.id,\n name: DailyReportLabel.name,\n color: DailyReportLabel.color,\n })\n .from(DailyReportHub_Label)\n .innerJoin(DailyReportLabel, eq(DailyReportHub_Label.labelId, DailyReportLabel.id))\n .where(inArray(DailyReportHub_Label.hubId, hubIds))\n\n const allComments = await db\n .select<{ hubId: number; id: number; body: string; createdAt: Date | string; userId: number; userName: string | null }>({\n hubId: DailyReportCommentModel.hubId,\n id: DailyReportCommentModel.id,\n body: DailyReportCommentModel.body,\n createdAt: DailyReportCommentModel.createdAt,\n userId: DailyReportCommentModel.userId,\n userName: users.displayName,\n })\n .from(DailyReportCommentModel)\n .leftJoin(users, eq(DailyReportCommentModel.userId, users.id))\n .where(inArray(DailyReportCommentModel.hubId, hubIds))\n .orderBy(asc(DailyReportCommentModel.createdAt))\n\n // 添付も同じ流儀で一括取得する (日報ごとに引くと N+1 になる)\n const attachmentsMap = await getAttachmentsByHubIds(hubIds)\n\n const labelsMap = new Map<number, RawJsonLabel[]>()\n const commentsMap = new Map<number, RawJsonComment[]>()\n\n for (const label of allLabels) {\n let list = labelsMap.get(label.hubId)\n if (!list) {\n list = []\n labelsMap.set(label.hubId, list)\n }\n list.push({\n id: label.id,\n name: label.name,\n color: label.color,\n })\n }\n\n for (const comment of allComments) {\n let list = commentsMap.get(comment.hubId)\n if (!list) {\n list = []\n commentsMap.set(comment.hubId, list)\n }\n list.push({\n id: comment.id,\n content: comment.body,\n createdAt: formatDateValue(comment.createdAt, \"YYYY-MM-DD HH:mm:ss\") || \"\",\n userId: comment.userId,\n userName: comment.userName || \"\",\n })\n }\n\n return rows.map((row) => {\n const hubId = row.hub.id\n return mapHubRecord(\n scope,\n {\n ...row,\n attachments: attachmentsMap.get(hubId) ?? [],\n labels: labelsMap.get(hubId) || [],\n postedCommentRows: commentsMap.get(hubId) || [],\n },\n userId,\n )\n })\n },\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_BUSINESS_DATE_TTL_MS,\n epochKey: `${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedBusinessDate}`,\n }) as Promise<DailyReportDetail[]>\n }\n\n /**\n * Retrieves a single daily report detail by ID with user-specific status and relations.\n * 指定した日報詳細を、ユーザー状態と関連データを含めて取得する。\n *\n * 可視性の述語は営業日一覧 (`getDailyReportsByBusinessDateByExternalId`) および\n * `getAttachmentForUser` と同一に保つこと。3 経路のどれか 1 つだけ緩いと、\n * 緩い経路がメタデータ (件名・本文・添付のファイル名やトークン) を渡してしまい、\n * 「SQL 述語が唯一の認可境界」という不変条件が崩れる。\n * 4 番目の経路である `fetchDailyReportIdsByUserId` だけは意図的に 1 つ緩い\n * (返すのが ID・営業日・ソース種別のみのため)。詳細は同関数の docstring を参照。\n *\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param userId Internal numeric user id of the viewer. 閲覧者の内部数値ユーザー ID。\n * @param options Cache query options. キャッシュ問い合わせオプション。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Detail when visible, otherwise null. 可視なら詳細、そうでなければ null。\n */\n const getDailyReportDetailById = async (scope: DailyReportViewerScope, reportHubId: number, userId: number, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail | null> => {\n // 全拒否なら DB を引かずに null を返す (0 件 = fail-close)\n if (deniesEverySourceType(scope.visible)) {\n return null\n }\n\n // 可視集合ごとに別バケットへ隔離する (権限変更後に旧結果を配信しない)\n const cacheKey = detailCacheKey(reportHubId, userId, computeViewerScopeDigest(scope))\n\n const results = (await sqlResultCache.getOrFetch<DailyReportDetail>({\n cacheKey,\n fetcher: async () => {\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n\n const rows = await applyExternalJoins(\n db\n .select<HubQueryRow>({\n hub: cols(DailyReportHub),\n internal: cols(DailyReportInternal),\n isRead: DailyReportUserStatus.isRead,\n isStarred: DailyReportUserStatus.isStarred,\n creatorName: users.displayName,\n ...externalSelections(),\n })\n .from(DailyReportHub),\n )\n .leftJoin(DailyReportInternal, eq(DailyReportHub.id, DailyReportInternal.hubId))\n .leftJoin(DailyReportUserStatus, and(eq(DailyReportHub.id, DailyReportUserStatus.hubId), eq(DailyReportUserStatus.userId, userId)))\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .leftJoin(users, eq(DailyReportHub.userId, users.id))\n .where(and(eq(DailyReportHub.id, reportHubId), isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DraftLabelRelation.hubId)), visibleSourceTypeCondition(scope.visible)))\n\n if (rows.length === 0) {\n return []\n }\n\n const hubId = rows[0].hub.id\n\n // Fetch labels and comments via Drizzle ORM\n const labels = await db\n .select<{ id: number; name: string; color: string | null }>({\n id: DailyReportLabel.id,\n name: DailyReportLabel.name,\n color: DailyReportLabel.color,\n })\n .from(DailyReportHub_Label)\n .innerJoin(DailyReportLabel, eq(DailyReportHub_Label.labelId, DailyReportLabel.id))\n .where(eq(DailyReportHub_Label.hubId, hubId))\n\n const comments = await db\n .select<{ id: number; body: string; createdAt: Date | string; userId: number; userName: string | null }>({\n id: DailyReportCommentModel.id,\n body: DailyReportCommentModel.body,\n createdAt: DailyReportCommentModel.createdAt,\n userId: DailyReportCommentModel.userId,\n userName: users.displayName,\n })\n .from(DailyReportCommentModel)\n .leftJoin(users, eq(DailyReportCommentModel.userId, users.id))\n .where(eq(DailyReportCommentModel.hubId, hubId))\n .orderBy(asc(DailyReportCommentModel.createdAt))\n\n const attachmentsMap = await getAttachmentsByHubIds([hubId])\n\n const labelsMapped = labels.map((l) => ({\n id: l.id,\n name: l.name,\n color: l.color,\n }))\n\n const commentsMapped = comments.map((c) => ({\n id: c.id,\n content: c.body,\n createdAt: formatDateValue(c.createdAt, \"YYYY-MM-DD HH:mm:ss\") || \"\",\n userId: c.userId,\n userName: c.userName || \"\",\n }))\n\n return [\n mapHubRecord(\n scope,\n {\n ...rows[0],\n attachments: attachmentsMap.get(hubId) ?? [],\n labels: labelsMapped,\n postedCommentRows: commentsMapped,\n },\n userId,\n ),\n ]\n },\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_BUSINESS_DATE_TTL_MS,\n epochKey: `${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`,\n })) as DailyReportDetail[]\n\n return results[0] ?? null\n }\n\n /**\n * Retrieves a single daily report detail by ID with user-specific status and relations.\n * 指定した日報詳細を、ユーザー状態と関連データを含めて取得する。\n *\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param externalId External user id. 外部ユーザー ID。\n * @param options Cache query options. キャッシュ問い合わせオプション。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Detail when visible, otherwise null. 可視なら詳細、そうでなければ null。\n */\n const getDailyReportDetailByIdByExternalId = async (scope: DailyReportViewerScope, reportHubId: number, externalId: string, options: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail | null> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return null\n }\n // ❗ 受け取った可視集合はそのまま透過する。ここで null へ潰すと外部 ID 経由の詳細取得だけ\n // フィルタが外れる (内部の create/update/publish が明示 null を渡すのとは別物)\n return getDailyReportDetailById(scope, reportHubId, userId, options)\n }\n\n /**\n * Loads the gate columns of a hub for a mutation, or null when it does not exist.\n * ミューテーション用に hub のゲート列を取得する処理 (存在しなければ null)。\n *\n * @param reportHubId Hub id. 対象 hub の ID。\n * @returns Source type and deletion marker, or null. ソース種別と論理削除印 (無ければ null)。\n */\n const findHubGateColumns = async (reportHubId: number, userId: number): Promise<{ sourceType: string; deletedAt: Date | string | null } | null> => {\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n\n // ❗ 可視性の述語は読み取り経路と同一に保つ。下書き条項を落とすと、閲覧できないはずの\n // 他人の下書きに対して既読 / スター / コメントを付けられる (書き込み可能な存在オラクル)\n const [hub] = await db\n .select<{ sourceType: string; deletedAt: Date | string | null }>({ sourceType: DailyReportHub.sourceType, deletedAt: DailyReportHub.deletedAt })\n .top(1)\n .from(DailyReportHub)\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .where(and(eq(DailyReportHub.id, reportHubId), or(eq(DailyReportHub.userId, userId), isNull(DraftLabelRelation.hubId))))\n return hub ?? null\n }\n\n /**\n * Rejects a status mutation targeting a missing, deleted, or invisible report.\n * 不存在・論理削除済み・不可視区分の日報に対する状態更新を拒否する処理。\n *\n * 認可失敗と不存在を区別せず一律 `Not Found` にする (存在オラクルの回避)。従来は hub を\n * 一切検証せず任意の `reportHubId` へ upsert できたため、その穴も同時に塞ぐ。\n *\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Nothing; throws when rejected. なし (拒否時は例外送出)。\n */\n const assertHubMutable = async (scope: DailyReportViewerScope, reportHubId: number, userId: number): Promise<void> => {\n const hub = await findHubGateColumns(reportHubId, userId)\n if (!hub || hub.deletedAt || !isSourceTypeVisible(scope.visible, hub.sourceType)) {\n throw new Error(\"Not Found\")\n }\n }\n\n /**\n * Sets the starred status of a daily report for a user.\n * ユーザーの日報スター状態を設定する。\n *\n * @param userId Internal numeric user id. 内部数値ユーザー ID。\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param businessDate Business date for cache invalidation. キャッシュ無効化に使う営業日。\n * @param isStarred Desired star state. 設定するスター状態。\n * @param clientTempId Optimistic-update correlation id. 楽観更新の相関 ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Updated status row. 更新後の状態行。\n */\n const setStarStatus = async (scope: DailyReportViewerScope, userId: number, reportHubId: number, businessDate: string | null, isStarred: boolean, clientTempId: string): Promise<DailyReportUserStatusRow> => {\n // 不可視区分・不存在・削除済みへの書き込みを先に弾く\n await assertHubMutable(scope, reportHubId, userId)\n\n const existing = await db\n .select<DailyReportUserStatusRow>()\n .top(1)\n .from(DailyReportUserStatus)\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n\n let updatedStatus: DailyReportUserStatusRow\n\n if (existing.length > 0) {\n if (existing[0].isStarred !== isStarred) {\n const rows = await db\n .update<DailyReportUserStatusRow>(DailyReportUserStatus)\n .set({\n isStarred: isStarred,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n .output()\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n updatedStatus = rows[0]\n } else {\n updatedStatus = existing[0]\n }\n } else {\n const rows = await db\n .insert<DailyReportUserStatusRow>(DailyReportUserStatus)\n .output()\n .values({\n hubId: reportHubId,\n userId,\n isStarred: isStarred,\n isRead: false,\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n updatedStatus = rows[0]\n }\n\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n // 可視集合ダイジェスト付きキーを取りこぼさないよう接頭辞で消す\n sqlResultCache.invalidatePrefix(dateUserPrefix(normalizedDate, userId))\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n // 可視集合ダイジェスト付きキーを取りこぼさないよう接頭辞で消す\n sqlResultCache.invalidatePrefix(detailUserPrefix(reportHubId, userId))\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n await publishToSseStream(\n statusUpdateMessageSchema.parse({\n type: \"status-update\",\n reportHubId: reportHubId,\n recipientRawUserId: userId,\n statusType: \"star\",\n value: isStarred,\n clientTempId,\n }),\n \"setStarStatus\",\n )\n\n return updatedStatus\n }\n\n /**\n * Sets the read status of a daily report for a user.\n * ユーザーの日報既読状態を設定する。\n *\n * @param userId Internal numeric user id. 内部数値ユーザー ID。\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param businessDate Business date for cache invalidation. キャッシュ無効化に使う営業日。\n * @param isRead Desired read state. 設定する既読状態。\n * @param clientTempId Optimistic-update correlation id. 楽観更新の相関 ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Updated status row. 更新後の状態行。\n */\n const setReadStatus = async (scope: DailyReportViewerScope, userId: number, reportHubId: number, businessDate: string | null, isRead: boolean, clientTempId: string): Promise<DailyReportUserStatusRow> => {\n // 不可視区分・不存在・削除済みへの書き込みを先に弾く\n await assertHubMutable(scope, reportHubId, userId)\n\n const existing = await db\n .select<DailyReportUserStatusRow>()\n .top(1)\n .from(DailyReportUserStatus)\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n\n let updatedStatus: DailyReportUserStatusRow\n\n if (existing.length > 0) {\n if (existing[0].isRead !== isRead) {\n const rows = await db\n .update<DailyReportUserStatusRow>(DailyReportUserStatus)\n .set({\n isRead: isRead,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n .output()\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n updatedStatus = rows[0]\n } else {\n updatedStatus = existing[0]\n }\n } else {\n const rows = await db\n .insert<DailyReportUserStatusRow>(DailyReportUserStatus)\n .output()\n .values({\n hubId: reportHubId,\n userId,\n isRead: isRead,\n isStarred: false,\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n updatedStatus = rows[0]\n }\n\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n // 可視集合ダイジェスト付きキーを取りこぼさないよう接頭辞で消す\n sqlResultCache.invalidatePrefix(dateUserPrefix(normalizedDate, userId))\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n // 可視集合ダイジェスト付きキーを取りこぼさないよう接頭辞で消す\n sqlResultCache.invalidatePrefix(detailUserPrefix(reportHubId, userId))\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n await publishToSseStream(\n statusUpdateMessageSchema.parse({\n type: \"status-update\",\n reportHubId: reportHubId,\n recipientRawUserId: userId,\n statusType: \"read\",\n value: isRead,\n clientTempId,\n }),\n \"setReadStatus\",\n )\n\n return updatedStatus\n }\n\n /**\n * Resolves the SSE recipient for comment events on a possibly-draft report.\n * 下書きの可能性がある日報のコメントイベントについて SSE の宛先を解決する処理。\n *\n * 下書きは所有者にしか見えないため、下書きラベルが付いている間はコメント本文も\n * 所有者だけに配信する (無指定だと本文が全ユーザーへブロードキャストされる)。\n * 公開済み (下書きラベル無し) は全員配信 = undefined を返す。\n *\n * @param hubId Parent hub id. 親日報の hub ID。\n * @param ownerUserId Hub owner's raw user id. 所有者の内部ユーザー ID。\n * @returns Recipient raw user id, or undefined to broadcast. 宛先 (全員配信は undefined)。\n */\n const resolveCommentRecipient = async (hubId: number, ownerUserId: number | null): Promise<number | undefined> => {\n // 所有者不明 (共有日報など) は従来どおり全員配信\n if (ownerUserId === null) return undefined\n const draftIds = await getDraftLabelJoinIds()\n const [rel] = await db\n .select<{ hubId: number }>({ hubId: DailyReportHub_Label.hubId })\n .top(1)\n .from(DailyReportHub_Label)\n .where(and(eq(DailyReportHub_Label.hubId, hubId), inArray(DailyReportHub_Label.labelId, draftIds)))\n return rel ? ownerUserId : undefined\n }\n\n /**\n * Adds a comment to a daily report.\n * 日報にコメントを追加する。\n *\n * @param userId Internal numeric user id. 内部数値ユーザー ID。\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param content Comment body. コメント本文。\n * @param businessDate Business date for cache invalidation. キャッシュ無効化に使う営業日。\n * @param clientTempId Optimistic-update correlation id. 楽観更新の相関 ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns The created comment. 追加されたコメント。\n */\n const addComment = async (scope: DailyReportViewerScope, userId: number, reportHubId: number, content: string, businessDate: string | null, clientTempId: string): Promise<DailyReportPostedComment> => {\n const [hub] = await db\n .select<{ sourceType: string; deletedAt: Date | string | null; userId: number | null }>({ sourceType: DailyReportHub.sourceType, deletedAt: DailyReportHub.deletedAt, userId: DailyReportHub.userId })\n .top(1)\n .from(DailyReportHub)\n .where(eq(DailyReportHub.id, reportHubId))\n // 存在しない / 論理削除済みの日報にはコメントを付けない (以前は hub 未取得でも\n // フォールスルーして孤児コメントを INSERT していた)\n if (!hub || hub.deletedAt) {\n throw new Error(\"Not Found\")\n }\n // 不可視区分・他人の下書きにはコメントできない。認可失敗と不存在を区別せず Not Found にする\n // (存在オラクルの回避)。読み取り経路と同一の可視性述語を共通ゲートで適用する\n await assertHubMutable(scope, reportHubId, userId)\n // コメント次元が拒否する区分には記入させない。本文は読めても書けない状態を表現するため、\n // 不存在 (Not Found) ではなく「制限」として返す — 閲覧者は当該日報を見えているので\n // 不存在を装っても存在オラクルにはならず、嘘をつくだけ UI の説明性が落ちる\n if (!isSourceTypeVisible(scope.commentable, hub.sourceType)) {\n throw new Error(\"Comments are restricted by the viewer's comment permission\")\n }\n if (hub.sourceType.toLowerCase() !== \"internal\") {\n throw new Error(\"Comments are restricted for external daily report sources\")\n }\n // 下書きなら SSE を所有者宛に絞る (本文の全員配信を防ぐ)\n const recipientRawUserId = await resolveCommentRecipient(reportHubId, hub.userId)\n\n const [inserted] = await db\n .insert<DailyReportCommentRow>(DailyReportCommentModel)\n .output()\n .values({\n hubId: reportHubId,\n userId,\n body: content,\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n\n const [user] = await db.select<{ displayName: string | null }>({ displayName: users.displayName }).top(1).from(users).where(eq(users.id, userId))\n const userName = user?.displayName ?? \"Unknown\"\n\n // コメントは全ユーザーに表示されるため、date キャッシュも全ユーザー分を無効化\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n sqlResultCache.invalidatePrefix(`daily-report:date:${normalizedDate}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n const commentItem: DailyReportPostedComment = {\n id: inserted.id,\n userId: encodeUserId(inserted.userId),\n userName: userName,\n content: inserted.body,\n createdAt: formatDateValue(inserted.createdAt, \"YYYY-MM-DD HH:mm:ss\") ?? \"\",\n isMine: true,\n }\n\n await publishToSseStream(\n commentAddMessageSchema.parse({\n type: \"comment-add\",\n reportHubId: reportHubId,\n comment: commentItem,\n clientTempId,\n recipientRawUserId,\n // 受信側の区分フィルタ用 (旧エントリ互換のためスキーマ上は任意)\n sourceType: hub.sourceType,\n }),\n \"addComment\",\n )\n\n return commentItem\n }\n\n /**\n * Finds a daily report comment by ID.\n * 日報コメントをIDで検索する。\n */\n const findDailyReportCommentById = async (tx: DailyReportDb, commentId: number) => {\n return await tx.select<DailyReportCommentRow>().top(1).from(DailyReportCommentModel).where(eq(DailyReportCommentModel.id, commentId))\n }\n\n /**\n * Deletes a comment from a daily report.\n * 日報のコメントを削除する。\n *\n * @param userId Internal numeric user id. 内部数値ユーザー ID。\n * @param reportHubId Hub id. 対象 hub の ID。\n * @param commentId Comment id to delete. 削除対象のコメント ID。\n * @param businessDate Business date for cache invalidation. キャッシュ無効化に使う営業日。\n * @param clientTempId Optimistic-update correlation id. 楽観更新の相関 ID。\n * @param scope Viewer scope carrying the visible source types. 可視 sourceType を運ぶ閲覧範囲。\n * @returns Nothing. なし。\n */\n const deleteComment = async (scope: DailyReportViewerScope, userId: number, reportHubId: number, commentId: number, businessDate: string | null, clientTempId: string): Promise<void> => {\n const comments = await findDailyReportCommentById(db, commentId)\n if (comments.length === 0) {\n throw new Error(\"Not Found\")\n }\n\n // 渡された reportHubId がコメントの実際の所属と一致することを確認する。\n // 不一致のまま進むと、無関係な日報のキャッシュ無効化と SSE 配信を引き起こす。\n // ❗ 所有者チェックより**前**に置く。後ろだと、攻撃者が自分に可視な hubId を添えて任意の\n // commentId を試すことで「他人のコメント = 403 / 存在しない = 404」と応答が割れ、\n // コメントの存在を列挙できてしまう\n if (comments[0].hubId !== reportHubId) {\n throw new Error(\"Not Found\")\n }\n\n // ❗ 区分ゲートも所有者チェックより前に置く。参照するのは**コメント自身の所属 hub** で\n // あり、呼び出し側が渡した ID ではない (上で一致を確認済み)。hub 行が引けない場合も\n // fail-close で Not Found にする (assertHubMutable と同一の規約)\n // ❗ ここで見るのは本文次元 (`scope.visible`) であって**コメント次元ではない**。意図的。\n // 自分のコメントは常に可視という契約 (mapHubRecord 参照) の対を成す規則であり、\n // コメント次元でゲートすると権限剥奪の瞬間に既存の自分のコメントが恒久的に削除不能に\n // なる。所有者チェックが単独で十分な防御になっている\n const [hubForDelete] = await db.select<{ userId: number | null; sourceType: string }>({ userId: DailyReportHub.userId, sourceType: DailyReportHub.sourceType }).top(1).from(DailyReportHub).where(eq(DailyReportHub.id, comments[0].hubId))\n if (!(hubForDelete && isSourceTypeVisible(scope.visible, hubForDelete.sourceType))) {\n throw new Error(\"Not Found\")\n }\n\n if (comments[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n // 下書きなら削除イベントも所有者宛に絞る (comment-add と対称)\n const recipientRawUserId = await resolveCommentRecipient(reportHubId, hubForDelete.userId)\n\n await db.delete(DailyReportCommentModel).where(eq(DailyReportCommentModel.id, commentId))\n\n // コメントは全ユーザーに表示されるため、date キャッシュも全ユーザー分を無効化\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n sqlResultCache.invalidatePrefix(`daily-report:date:${normalizedDate}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n await publishToSseStream(\n commentDeleteMessageSchema.parse({\n type: \"comment-delete\",\n reportHubId: reportHubId,\n commentId,\n clientTempId,\n recipientRawUserId,\n // 受信側の区分フィルタ用 (hub 行が引けない場合は undefined = 旧エントリ相当の扱い)\n sourceType: hubForDelete?.sourceType,\n }),\n \"deleteComment\",\n )\n }\n\n /**\n * Creates a new draft daily report.\n * 新しい日報(下書き)を作成する。\n */\n const createDailyReport = async (userId: number, businessDate: string, clientTempId: string): Promise<DailyReportDetail> => {\n logger.info(\"createDailyReport called\", { userId, businessDate })\n\n // ユーザー名取得\n const [user] = await db.select<{ displayName: string | null }>({ displayName: users.displayName }).from(users).where(eq(users.id, userId))\n const userName = user?.displayName ?? encodeUserId(userId)\n\n try {\n const result = await inTransaction(async (tx) => {\n logger.info(\"Starting transaction\")\n // 1. Hub作成\n const sourceId = `internal-temp-${Date.now()}-${Math.random()}` // 一時的なID\n const [hub] = await tx\n .insert<DailyReportHubRow>(DailyReportHub)\n .output()\n .values({\n sourceType: \"Internal\",\n sourceId: sourceId,\n businessDate: new Date(businessDate),\n userId: userId,\n title: \"(無題)\",\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n\n // sourceId を id と同じ値に更新 (Internal の場合の正規化)\n await tx\n .update(DailyReportHub)\n .set({ sourceId: String(hub.id) })\n .where(eq(DailyReportHub.id, hub.id))\n\n logger.info(\"Hub created\", hub)\n\n // 2. Internal作成\n await tx.insert(DailyReportInternal).values({\n hubId: hub.id,\n body: \"\",\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n logger.info(\"Internal created\")\n\n // 3. ラベル付与\n logger.info(\"Calling getDraftLabelId\")\n const draftLabelId = await getDraftLabelId()\n logger.info(\"draftLabelId\", draftLabelId)\n const labels: DailyReportLabelDef[] = []\n if (draftLabelId) {\n await tx.insert(DailyReportHub_Label).values({\n hubId: hub.id,\n labelId: draftLabelId,\n createdAt: new Date(),\n createdBy: String(userId),\n })\n labels.push({ id: draftLabelId, name: draftLabelName, color: null })\n }\n logger.info(\"Label assigned\")\n\n // 4. 詳細オブジェクト構築\n return {\n reportHubId: hub.id,\n date: businessDate,\n createdAt: formatDateValue(hub.createdAt, \"YYYY-MM-DD HH:mm:ss\"),\n author: userName,\n userId: encodeUserId(userId),\n sourceType: \"Internal\",\n employeeName: userName,\n // 監査列と同様、クライアント公開時は生内部 ID を難読化する (mapHubRecord と整合)\n updatedBy: encodeUserId(userId),\n updatedAt: formatDateValue(hub.updatedAt, \"YYYY-MM-DD HH:mm:ss\"),\n category: null,\n creationCategory: null,\n visitTimeFrom: null,\n visitTimeTo: null,\n customerName: null,\n interviewers: [],\n subject: hub.title,\n content: \"\",\n externalComments: [],\n isRead: true, // 自分で作ったので既読\n isStarred: false,\n labels: labels,\n postedComments: [],\n // 新規作成直後の日報に添付は存在しない\n attachments: [],\n }\n })\n\n // キャッシュ無効化\n const normalizedBusinessDate = normalizeBusinessDateKey(businessDate)\n if (normalizedBusinessDate) {\n // 可視集合ダイジェスト付きキーを取りこぼさないよう接頭辞で消す\n sqlResultCache.invalidatePrefix(dateUserPrefix(normalizedBusinessDate, userId))\n }\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n // ❗ createDailyReport には ids の広域 prefix 無効化が無く、この 1 行が作成者の ID 一覧を\n // 落とす唯一の経路。redis 未注入の配備では epoch も進まないため、ここを消すと\n // 作成した日報が自分の一覧から TTL 分 (既定 180 秒) 消える。接頭辞化のみ行う\n sqlResultCache.invalidatePrefix(idsUserPrefix(userId))\n // ❗ 順序重要: invalidate → incrementRedisEpoch → publishToSseStream\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n if (normalizedBusinessDate) {\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedBusinessDate}`)\n }\n\n // ❗ 明示 null (無制限)。所有者が自分で作った日報を取り直す経路なので、消費側の可視集合に\n // 作成区分 (\"Internal\") が含まれなくても取得と SSE 配信を壊さない。区分の出し分けは\n // 受信側の SSE フィルタと次回読み取り時のサーバーフィルタが担う\n const fullDetail = await getDailyReportDetailById(DailyReportViewerScope.unrestricted(\"post-write self read: the writer always sees the row they just wrote\"), result.reportHubId, userId, { forceRefresh: true })\n\n if (fullDetail) {\n // 下書き判定\n // 判定は代表 ID ではなく下書きラベル**全体**で行う (2 つ目の下書き名を見落とさない)\n const draftLabelIds = await getDraftLabelIds()\n const isDraft = fullDetail.labels.some((l) => draftLabelIds.includes(l.id))\n await publishToSseStream(\n reportCreateMessageSchema.parse({\n type: \"report-create\",\n reportHubId: fullDetail.reportHubId,\n report: fullDetail,\n clientTempId,\n recipientRawUserId: isDraft ? userId : undefined,\n }),\n \"createDailyReport\",\n )\n }\n\n return fullDetail ?? result\n } catch (e) {\n logger.error(\"Error in createDailyReport\", e)\n throw e\n }\n }\n\n // --- Internal Repository Layer ---\n\n /**\n * Finds a DailyReportHub by ID.\n * 日報HubをIDで検索する。\n */\n const findDailyReportHubById = async (tx: DailyReportDb, reportHubId: number) => {\n return await tx.select<DailyReportHubRow>().top(1).from(DailyReportHub).where(eq(DailyReportHub.id, reportHubId))\n }\n\n /**\n * Updates a DailyReportHub.\n * 日報Hubを更新する。\n */\n const updateDailyReportHub = async (tx: DailyReportDb, reportHubId: number, data: Partial<DailyReportHubRow>) => {\n await tx.update(DailyReportHub).set(data).where(eq(DailyReportHub.id, reportHubId))\n }\n\n /**\n * Updates a DailyReportInternal.\n * 日報Internalを更新する。\n */\n const updateDailyReportInternal = async (tx: DailyReportDb, hubId: number, data: Partial<DailyReportInternalRow>) => {\n await tx.update(DailyReportInternal).set(data).where(eq(DailyReportInternal.hubId, hubId))\n }\n\n /**\n * Deletes a label from a DailyReportHub.\n * 日報Hubからラベルを削除する。\n */\n const deleteDailyReportLabel = async (tx: DailyReportDb, hubId: number, labelId: number) => {\n await tx.delete(DailyReportHub_Label).where(and(eq(DailyReportHub_Label.hubId, hubId), eq(DailyReportHub_Label.labelId, labelId)))\n }\n\n // --- Service Layer ---\n\n /**\n * Logically deletes a daily report.\n * 日報を論理削除する。\n */\n const deleteDailyReport = async (reportHubId: number, userId: number, clientTempId: string): Promise<void> => {\n const report = await findDailyReportHubById(db, reportHubId)\n if (!report.length || report[0].deletedAt) {\n throw new Error(\"Not Found\")\n }\n if (report[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n await updateDailyReportHub(db, reportHubId, {\n deletedAt: new Date(),\n deletedBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n\n // キャッシュ無効化\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n // 削除されたレポートは全ユーザーの一覧から消えるため、全ユーザーの IDs キャッシュを invalidate\n sqlResultCache.invalidatePrefix(DAILY_REPORT_IDS_CACHE_KEY)\n // ❗ 順序重要: invalidatePrefix → incrementRedisEpoch → publishToSseStream\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n if (report[0].businessDate) {\n const normalizedDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (normalizedDate) {\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n\n await publishToSseStream(\n reportDeleteMessageSchema.parse({\n type: \"report-delete\",\n reportHubId: reportHubId,\n clientTempId,\n // 受信側の区分フィルタ用 (旧エントリ互換のためスキーマ上は任意)\n sourceType: report[0].sourceType,\n }),\n \"deleteDailyReport\",\n )\n }\n\n /**\n * Updates an existing daily report.\n * 日報を更新する。\n */\n const updateDailyReport = async (reportHubId: number, userId: number, data: { title?: string; content?: string }, clientTempId: string): Promise<void> => {\n const report = await findDailyReportHubById(db, reportHubId)\n if (!report.length || report[0].deletedAt) {\n throw new Error(\"Not Found\")\n }\n if (report[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n await inTransaction(async (tx) => {\n if (data.title !== undefined) {\n await updateDailyReportHub(tx, reportHubId, {\n title: data.title,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n }\n if (data.content !== undefined) {\n await updateDailyReportInternal(tx, reportHubId, {\n body: data.content,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n }\n })\n\n // キャッシュ無効化: 一覧プレビューは詳細キャッシュから引くため、詳細を全プロセス横断で落とす。\n // 以前は読み取りに使われないプレフィックスを無効化し epoch も上げていなかったため、\n // 編集後の件名/本文が他ワーカーで古いまま配信されていた\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n // 編集内容は全ユーザーの一覧に映るため、営業日別一覧は全ユーザー分 + 全ワーカー横断で\n // 無効化する (addComment と同じ流儀。編集者本人のキーだけでは他者へ古い件名が残る)\n const editedBusinessDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (editedBusinessDate) {\n sqlResultCache.invalidatePrefix(`daily-report:date:${editedBusinessDate}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${editedBusinessDate}`)\n }\n\n // ❗ 明示 null (無制限)。所有者が自分の日報を取り直す経路であり、区分の出し分けは\n // 受信側の SSE フィルタと次回読み取り時のサーバーフィルタが担う\n const fullDetail = await getDailyReportDetailById(DailyReportViewerScope.unrestricted(\"post-write self read: the writer always sees the row they just wrote\"), reportHubId, userId, { forceRefresh: true })\n if (fullDetail) {\n // 下書き判定\n // 判定は代表 ID ではなく下書きラベル**全体**で行う (2 つ目の下書き名を見落とさない)\n const draftLabelIds = await getDraftLabelIds()\n const isDraft = fullDetail.labels.some((l) => draftLabelIds.includes(l.id))\n await publishToSseStream(\n reportUpdateMessageSchema.parse({\n type: \"report-update\",\n reportHubId: fullDetail.reportHubId,\n report: fullDetail,\n clientTempId,\n recipientRawUserId: isDraft ? userId : undefined,\n }),\n \"updateDailyReport\",\n )\n }\n }\n\n /**\n * Publishes a draft daily report.\n * 日報を公開する(下書きラベルを削除)。\n */\n const publishDailyReport = async (reportHubId: number, userId: number, clientTempId: string): Promise<DailyReportDetail | null> => {\n const report = await findDailyReportHubById(db, reportHubId)\n if (!report.length) {\n throw new Error(\"Not Found\")\n }\n if (report[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n const draftLabelId = await getDraftLabelId()\n if (!draftLabelId) return null\n\n await deleteDailyReportLabel(db, reportHubId, draftLabelId)\n\n // キャッシュ無効化\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n // 公開によりドラフトが全ユーザーに可視化されるため、全ユーザーの IDs キャッシュを invalidate\n sqlResultCache.invalidatePrefix(DAILY_REPORT_IDS_CACHE_KEY)\n // ❗ 順序重要: invalidatePrefix → incrementRedisEpoch → publishToSseStream\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n if (report[0].businessDate) {\n const normalizedDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (normalizedDate) {\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n\n // ユーザーごとの営業日別一覧キャッシュも無効化\n const normalizedBusinessDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (normalizedBusinessDate) {\n // 可視集合ダイジェスト付きキーを取りこぼさないよう接頭辞で消す\n sqlResultCache.invalidatePrefix(dateUserPrefix(normalizedBusinessDate, userId))\n }\n\n // ❗ 明示 null (無制限)。所有者が自分の日報を取り直す経路であり、区分の出し分けは\n // 受信側の SSE フィルタと次回読み取り時のサーバーフィルタが担う\n const fullDetail = await getDailyReportDetailById(DailyReportViewerScope.unrestricted(\"post-write self read: the writer always sees the row they just wrote\"), reportHubId, userId, { forceRefresh: true })\n if (fullDetail) {\n await publishToSseStream(\n reportPublishMessageSchema.parse({\n type: \"report-publish\",\n reportHubId: fullDetail.reportHubId,\n report: fullDetail,\n clientTempId,\n }),\n \"publishDailyReport\",\n )\n }\n return fullDetail ?? null\n }\n\n /**\n * Invalidates the read caches that carry hub-scoped data across every process.\n * hub 単位のデータを載せている読み取りキャッシュを、全プロセス横断で無効化する処理。\n *\n * 用途: 通常の更新経路 (create / update / publish) を通らずに DB を書き換えた場合、\n * 例えば外部システムからの一括取込。プロセス内 Map を消すだけでは稼働中の\n * 別プロセス (別ワーカー・別コンテナ) のキャッシュが TTL まで stale のままになるため、\n * Redis の epoch を進めて全プロセスのキャッシュ行を一斉に無効化する。\n *\n * `clearCache()` との違い: `clearCache()` はプロセス内キャッシュの全消去 + 一覧 ID の\n * epoch のみ進める粗い操作。こちらは対象 hub / 営業日 / 一覧 ID の三系統を epoch まで\n * 含めて狙い撃ちで無効化する。日報本体に紐づくデータを書き換えたときはこちらを使うこと。\n *\n * @param hubIds Hub ids whose detail cache must be dropped. 詳細キャッシュを捨てる hub ID 群。\n * @param businessDates Business dates (YYYY-MM-DD) whose list cache must be dropped. 一覧キャッシュを捨てる営業日群。\n * @returns Nothing. なし。\n */\n const invalidateHubReadCaches = async (hubIds: readonly number[], businessDates: readonly string[]): Promise<void> => {\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n // 一覧 ID は「どの行が存在するか」そのもの。hub の行集合を変えうる呼び出し元\n // (取込・移行) がこの関数を頼る以上、詳細・営業日と同時に捨てる。\n // 現在の唯一の呼び出し元 (添付同期) は行集合を変えないため、これは将来の\n // 行集合変更呼び出し元に対する予防である\n sqlResultCache.invalidatePrefix(DAILY_REPORT_IDS_CACHE_KEY)\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n for (const hubId of new Set(hubIds)) {\n sqlResultCache.invalidatePrefix(`daily-report:detail:${hubId}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${hubId}`)\n }\n for (const businessDate of new Set(businessDates)) {\n const normalized = normalizeBusinessDateKey(businessDate)\n // 正規化できない値で epoch キーを作ると、誰も参照しないキーが増えるだけになる\n if (!normalized) continue\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalized}`)\n }\n }\n\n /**\n * Clears all server-side SQL result and user ID caches.\n * サーバー側のすべての SQL 結果キャッシュおよびユーザー ID キャッシュを全消去する。\n *\n * Redis で進める epoch は一覧 ID のもののみ (プロセス内キャッシュは全系統消える)。\n * 特定 hub / 営業日の epoch まで狙い撃ちで進めたい場合は `invalidateHubReadCaches` を使うこと。\n */\n const clearCache = async (): Promise<void> => {\n sqlResultCache.clearAll()\n userIdCache.clear()\n cachedDraftLabelIds = null\n if (epochs) {\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n }\n logger.info(\"[DailyReportService] Server-side DB/SQL caches cleared successfully.\")\n }\n\n return {\n // 定数 (アプリ側の互換 export 用)\n streamKey: DAILY_REPORT_SSE_STREAM_KEY,\n streamMaxLen: DAILY_REPORT_SSE_STREAM_MAXLEN,\n // キャッシュクリア\n clearCache,\n invalidateHubReadCaches,\n // ユーザー解決\n getUserIdByExternalId,\n // 参照系\n getDailyReportIdsByExternalId,\n getDailyReportIdsFirstPageByExternalId,\n getDailyReportsByBusinessDate,\n getDailyReportsByBusinessDateByExternalId,\n getDailyReportDetailById,\n getDailyReportDetailByIdByExternalId,\n getDraftLabelId,\n // 添付 (ハンドラーが認可判定と実体消失記録に使う)\n getAttachmentsByHubIds,\n getAttachmentForUser,\n markAttachmentMissing,\n markAttachmentPresent,\n // 更新系\n setStarStatus,\n setReadStatus,\n addComment,\n deleteComment,\n createDailyReport,\n updateDailyReport,\n publishDailyReport,\n deleteDailyReport,\n // リポジトリヘルパー。現時点で消費側の呼び出し元は無く、公開面としてのみ維持している\n // (「統合テストから直接利用されている」という以前の記述は誤り。実測で 0 件)。\n // ❗ どれも**コメント次元を適用しない**素のリポジトリ操作である。詳細を組み立てる用途へ\n // 流用すると mapHubRecord の射影を迂回してコメント本文が出るため、読み取り経路の\n // 素材として使わないこと\n findDailyReportHubById,\n updateDailyReportHub,\n updateDailyReportInternal,\n deleteDailyReportLabel,\n }\n}\n","/**\n * Authz helper utilities for @aiquants/daily-report: standard resource definitions & auto-seeding.\n * @aiquants/daily-report 用の認可ヘルパー。標準リソース定義および自動シード機能を提供。\n */\nimport { and, eq } from \"drizzle-orm\"\n\nexport type AuthzResourceItem = {\n resourceKey: string\n name: string\n description?: string | null\n}\n\nexport type DailyReportSourceTypeInput = {\n key: string\n name: string\n description?: string | null\n includeCommentResource?: boolean\n}\n\n/**\n * Dynamically builds neutral Authz resource definitions from provided source type inputs.\n * 指定されたソース種別定義から中立な認可リソース定義リストを生成するファクトリ。\n */\nexport function defineDailyReportAuthzResources(sources?: DailyReportSourceTypeInput[]): AuthzResourceItem[] {\n const list: AuthzResourceItem[] = []\n const targetSources =\n sources && sources.length > 0\n ? sources\n : [\n { key: \"internal\", name: \"Internal\", description: \"Internal daily reports\" },\n { key: \"external\", name: \"External\", description: \"External daily reports\" },\n ]\n\n for (const src of targetSources) {\n const keyLower = src.key.toLowerCase().replace(/[^a-z0-9_]/g, \"_\")\n const resourceKey = `daily_report_${keyLower}`\n list.push({\n resourceKey,\n name: src.name,\n description: src.description ?? `${src.name} daily report access`,\n })\n\n if (src.includeCommentResource !== false) {\n list.push({\n resourceKey: `${resourceKey}_comment`,\n name: `${src.name} Comment`,\n description: `${src.name} daily report comment access`,\n })\n }\n }\n\n return list\n}\n\ntype DbSelectChain = {\n from(t: unknown): DbSelectChain\n where(cond: unknown): PromiseLike<unknown[]>\n}\ntype DbInsertChain = {\n values(v: unknown): PromiseLike<unknown>\n}\ntype LooseDb = {\n select(fields?: unknown): DbSelectChain\n insert(t: unknown): DbInsertChain\n}\n\n/**\n * Ensures all daily-report standard resources exist in the authz TMResource table idempotently.\n * authz データベース内に日報機能の標準認可リソースが存在することを自動保証(冪等シード)する処理。\n */\nexport async function seedDailyReportAuthzResources(db: unknown, authzTables: { TMResource: unknown }, opts: { appKey: string; actor?: string; sources?: DailyReportSourceTypeInput[]; resources?: AuthzResourceItem[] }): Promise<void> {\n const d = db as LooseDb\n const TMResource = authzTables.TMResource as Record<string, unknown>\n const appKey = opts.appKey\n const actor = opts.actor ?? \"system:daily-report\"\n const now = new Date()\n\n const resourceList = opts.resources ?? defineDailyReportAuthzResources(opts.sources)\n\n for (const res of resourceList) {\n const existing = await d\n .select()\n .from(TMResource)\n .where(and(eq(TMResource.appKey as never, appKey as never), eq(TMResource.resourceKey as never, res.resourceKey as never)))\n\n if (!existing || existing.length === 0) {\n await d.insert(TMResource).values({\n appKey,\n resourceKey: res.resourceKey,\n name: res.name,\n description: res.description ?? null,\n createdAt: now,\n createdBy: actor,\n updatedAt: now,\n updatedBy: actor,\n })\n }\n }\n}\n","/**\n * External source adapter contract for legacy/foreign daily-report tables.\n * レガシー・外部由来の日報テーブルを取り込む外部ソースアダプタ契約。\n *\n * DailyReportHub.source_type がアダプタの `sourceType` に一致する行は、\n * `table` を `Hub.source_id_num = idColumn` で LEFT JOIN し、その行を\n * `mapRow` で表示フィールドへ変換する (例: 別システムのレガシー日報テーブルの取り込み)。\n */\nimport type { AnyMsSqlColumn } from \"drizzle-orm/mssql-core\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport type { DailyReportExternalComment, DailyReportInterviewer } from \"../shared/types\"\n\n/** 外部ソース行から詳細表示へ供給するフィールド群。 */\nexport type ExternalReportFields = {\n content?: string | null\n employeeName?: string | null\n category?: string | null\n creationCategory?: string | null\n visitTimeFrom?: string | null\n visitTimeTo?: string | null\n customerName?: string | null\n interviewers?: DailyReportInterviewer[]\n externalComments?: DailyReportExternalComment[]\n}\n\n/** 外部ソースアダプタ。 */\nexport type DailyReportExternalSource = {\n /** DailyReportHub.source_type の一致値 (例 \"legacy\")。 */\n sourceType: string\n /** LEFT JOIN する drizzle テーブル。 */\n table: unknown\n /** Hub.source_id_num と突き合わせる ID 列。 */\n idColumn: AnyMsSqlColumn\n /** 結合行を表示フィールドへ変換する処理。 */\n mapRow: (row: Record<string, unknown>) => ExternalReportFields\n}\n\nconst defaultLogger = createLogger(LogLevel.INFO, \"[DailyReportExternalSource]\")\n\n/**\n * Parses JSON array payloads and maps each element (fail-soft: returns [] on error).\n * JSON 配列のペイロードを解析し各要素を変換する処理 (エラー時は空配列)。\n */\nexport const transformJsonArray = <TRaw, TResult>(payload: string | null, label: string, mapper: (raw: TRaw) => TResult | null, logger: DailyReportLogger = defaultLogger): TResult[] => {\n if (!payload) {\n return []\n }\n\n try {\n const parsed = JSON.parse(payload)\n if (!Array.isArray(parsed)) {\n logger.warn(`Unexpected ${label} format: not an array`)\n return []\n }\n return (parsed as TRaw[]).map(mapper).filter((entry): entry is TResult => entry !== null)\n } catch (error) {\n logger.warn(`Failed to parse ${label}:`, error)\n return []\n }\n}\n","/**\n * Structural table types + drizzle (mssql) schema factory for the daily-report tables.\n * 日報テーブル群の構造的テーブル型と drizzle (mssql) スキーマファクトリ。\n *\n * 既存アプリは自前のモデル定義をそのまま `DailyReportTables` として注入できる (構造互換)。\n * 新規プロジェクトは `defineDailyReportSchema()` で同一構造のテーブル定義を生成できる。\n */\nimport { desc, sql } from \"drizzle-orm\"\nimport { type AnyMsSqlColumn, bigint, bit, date, datetime2, foreignKey, index, int, mssqlSchema, nvarchar, primaryKey, uniqueIndex } from \"drizzle-orm/mssql-core\"\n\n/** 注入する外部ユーザーテーブルの最小形 (id / display_name)。 */\nexport type DailyReportUserTable = { id: AnyMsSqlColumn; displayName: AnyMsSqlColumn }\n\n/** DailyReportHub テーブルの構造形。 */\nexport type DailyReportHubTable = {\n id: AnyMsSqlColumn\n sourceType: AnyMsSqlColumn\n sourceId: AnyMsSqlColumn\n sourceIdNum: AnyMsSqlColumn\n businessDate: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n title: AnyMsSqlColumn\n summary: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n deletedAt: AnyMsSqlColumn\n deletedBy: AnyMsSqlColumn\n}\n\n/** DailyReportInternal テーブルの構造形。 */\nexport type DailyReportInternalTable = {\n hubId: AnyMsSqlColumn\n body: AnyMsSqlColumn\n metadata: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/** 投稿コメントテーブル (物理名 DailyReportComment) の構造形。 */\nexport type DailyReportCommentTable = {\n id: AnyMsSqlColumn\n hubId: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n body: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/** DailyReportLabel テーブルの構造形。 */\nexport type DailyReportLabelTable = {\n id: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n name: AnyMsSqlColumn\n color: AnyMsSqlColumn\n sortOrder: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/** DailyReportHub_Label 中間テーブルの構造形。 */\nexport type DailyReportHubLabelTable = {\n hubId: AnyMsSqlColumn\n labelId: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n}\n\n/** DailyReportUserStatus テーブルの構造形。 */\nexport type DailyReportUserStatusTable = {\n hubId: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n isRead: AnyMsSqlColumn\n isStarred: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/**\n * DailyReportAttachment テーブルの構造形。\n *\n * `uploadedBy` は外部取込ソース由来の行に投稿者が存在しないため NULL 許容。\n * `sourceKey` は取込の冪等 upsert の自然キー。\n * `objectCheckedAt` / `objectMissingAt` から実体存在の三値 (unknown / present / absent) を導出する。\n */\nexport type DailyReportAttachmentTable = {\n id: AnyMsSqlColumn\n hubId: AnyMsSqlColumn\n uploadedBy: AnyMsSqlColumn\n sourceKey: AnyMsSqlColumn\n fileName: AnyMsSqlColumn\n filePath: AnyMsSqlColumn\n fileType: AnyMsSqlColumn\n fileSize: AnyMsSqlColumn\n objectCheckedAt: AnyMsSqlColumn\n objectMissingAt: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/**\n * サービスへ注入するテーブル一式 (アプリ既存モデルまたは本ファクトリ生成物)。\n *\n * `attachment` のみ任意。添付テーブルを持たない消費アプリでも本パッケージを使えるようにするため。\n * 未注入時は詳細の `attachments` が常に空配列になり、添付エンドポイントは全トークンで 404 を返す\n * (意図した縮退動作)。\n */\nexport type DailyReportTables = {\n hub: DailyReportHubTable\n internal: DailyReportInternalTable\n comment: DailyReportCommentTable\n attachment?: DailyReportAttachmentTable\n label: DailyReportLabelTable\n hubLabel: DailyReportHubLabelTable\n userStatus: DailyReportUserStatusTable\n}\n\n/**\n * Generic factory: build the seven daily-report tables under an arbitrary schema name.\n * 任意のスキーマ名の下に日報 7 テーブルを生成する汎用ファクトリ。\n *\n * 制約・索引名は `${schemaName}_<Table>_...` 規約で生成する。\n * ユーザーテーブルへの FK は注入された `userTable` を参照する。\n */\nexport function defineDailyReportSchema<S extends string>(schemaName: S, opts: { userTable: DailyReportUserTable }) {\n const s = mssqlSchema(schemaName)\n const users = opts.userTable\n\n const hub = s.table(\n \"DailyReportHub\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n sourceType: nvarchar(\"source_type\", { length: 20 }).notNull(),\n sourceId: nvarchar(\"source_id\", { length: 100 }).notNull(),\n sourceIdNum: bigint(\"source_id_num\", { mode: \"number\" }).generatedAlwaysAs(sql`TRY_CAST(source_id AS BIGINT)`),\n businessDate: date(\"business_date\"),\n userId: bigint(\"user_id\", { mode: \"number\" }),\n title: nvarchar(\"title\", { length: 200 }),\n summary: nvarchar(\"summary\", { length: \"max\" }),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n deletedAt: datetime2(\"deleted_at\"),\n deletedBy: nvarchar(\"deleted_by\", { length: 50 }),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportHub_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportHub_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n index(`${schemaName}_DailyReportHub_business_date_index`).on(table.businessDate),\n index(`${schemaName}_DailyReportHub_updated_at_index`).on(table.updatedAt),\n index(`${schemaName}_DailyReportHub_source_index`).on(table.sourceType, table.sourceId),\n index(`${schemaName}_DailyReportHub_user_id_index`).on(table.userId),\n index(`${schemaName}_DailyReportHub_business_date_id_index`).on(desc(table.businessDate), desc(table.id)),\n index(`${schemaName}_DailyReportHub_deleted_at_index`).on(table.deletedAt),\n ],\n )\n\n const internal = s.table(\n \"DailyReportInternal\",\n {\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n body: nvarchar(\"body\", { length: \"max\" }),\n metadata: nvarchar(\"metadata\", { length: \"max\" }),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportInternal_pk`, columns: [table.hubId] }),\n foreignKey({\n name: `${schemaName}_DailyReportInternal_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n ],\n )\n\n const comment = s.table(\n // ❗ 物理テーブル名。TS 側の型名改名 (DailyReportPostedComment 等) に**追従させない** —\n // これは DB の識別子であり、変更にはマイグレーションが要る。v0.17.0 の機械改名が\n // ここを巻き込み、実 DB で全コメントクエリが Msg 208 になる事故が実際に起きた\n \"DailyReportComment\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n userId: bigint(\"user_id\", { mode: \"number\" }).notNull(),\n body: nvarchar(\"body\", { length: \"max\" }).notNull(),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportComment_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportComment_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportComment_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n index(`${schemaName}_DailyReportComment_hub_id_index`).on(table.hubId),\n ],\n )\n\n const label = s.table(\n \"DailyReportLabel\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n userId: bigint(\"user_id\", { mode: \"number\" }),\n name: nvarchar(\"name\", { length: 50 }).notNull(),\n color: nvarchar(\"color\", { length: 20 }),\n sortOrder: int(\"sort_order\"),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportLabel_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportLabel_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n ],\n )\n\n const hubLabel = s.table(\n \"DailyReportHub_Label\",\n {\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n labelId: bigint(\"label_id\", { mode: \"number\" }).notNull(),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportHub_Label_pk`, columns: [table.hubId, table.labelId] }),\n foreignKey({\n name: `${schemaName}_DailyReportHub_Label_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportHub_Label_label_id_fk`,\n columns: [table.labelId],\n foreignColumns: [label.id],\n }),\n ],\n )\n\n const userStatus = s.table(\n \"DailyReportUserStatus\",\n {\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n userId: bigint(\"user_id\", { mode: \"number\" }).notNull(),\n isRead: bit(\"is_read\").default(false).notNull(),\n isStarred: bit(\"is_starred\").default(false).notNull(),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportUserStatus_pk`, columns: [table.hubId, table.userId] }),\n foreignKey({\n name: `${schemaName}_DailyReportUserStatus_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportUserStatus_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n ],\n )\n\n const attachment = s.table(\n \"DailyReportAttachment\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n // 外部取込ソース由来の行には投稿者が存在しないため NULL 許容\n uploadedBy: bigint(\"uploaded_by\", { mode: \"number\" }),\n // 取込の冪等 upsert の自然キー。`<source>:<external-id>` 形式を推奨 (値の規約は消費アプリが決める)\n sourceKey: nvarchar(\"source_key\", { length: 200 }).notNull(),\n fileName: nvarchar(\"file_name\", { length: 255 }).notNull(),\n // ストレージ URI。受け入れるのは `gcs://` のみ (取込側で表明・読み取り側で再検証)\n filePath: nvarchar(\"file_path\", { length: \"max\" }).notNull(),\n fileType: nvarchar(\"file_type\", { length: 100 }),\n fileSize: bigint(\"file_size\", { mode: \"number\" }),\n // 実体存在の三値をこの 2 列から導出する:\n // objectCheckedAt が null -> unknown (未検証)\n // objectMissingAt が非 null -> absent (消失を確認)\n // 上記以外 -> present (実在を確認)\n objectCheckedAt: datetime2(\"object_checked_at\"),\n objectMissingAt: datetime2(\"object_missing_at\"),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportAttachment_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportAttachment_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportAttachment_uploaded_by_fk`,\n columns: [table.uploadedBy],\n foreignColumns: [users.id],\n }),\n // 取込の冪等性を DB 側で担保する\n uniqueIndex(`${schemaName}_DailyReportAttachment_source_unique`).on(table.hubId, table.sourceKey),\n // 一括取得 (hub_id IN (...) ORDER BY created_at) を被覆する\n index(`${schemaName}_DailyReportAttachment_hub_id_index`).on(table.hubId, table.createdAt),\n ],\n )\n\n return { hub, internal, comment, attachment, label, hubLabel, userStatus } satisfies DailyReportTables\n}\n"],"mappings":"2wBAAA,IAAAA,GAAA,GAAAC,GAAAD,GAAA,0BAAAE,GAAA,2BAAAC,GAAA,mBAAAC,GAAA,0BAAAC,GAAA,oCAAAC,GAAA,4BAAAC,GAAA,6BAAAC,GAAA,8BAAAC,GAAA,4BAAAC,GAAA,6BAAAC,GAAA,qBAAAC,GAAA,oCAAAC,GAAA,4BAAAC,GAAA,0BAAAC,GAAA,kBAAAC,GAAA,oBAAAC,GAAA,iBAAAC,GAAA,wBAAAC,GAAA,kBAAAC,GAAA,yBAAAC,GAAA,gCAAAC,GAAA,sBAAAC,GAAA,kCAAAC,GAAA,uBAAAC,KAAA,eAAAC,GAAA1B,IC2BO,IAAM2B,GAAoBC,IAAsC,CAYnE,MAAM,SAASC,EAA0C,CACrD,GAAI,CACA,IAAMC,EAAS,MAAMF,GAAO,UAAU,EACtC,GAAI,CAACE,EAAQ,OAAO,KACpB,IAAMC,EAAM,MAAMD,EAAO,IAAID,CAAQ,EACrC,OAAOE,EAAM,OAAOA,CAAG,EAAI,CAC/B,MAAQ,CACJ,OAAO,IACX,CACJ,EAKA,MAAM,eAAeF,EAAiC,CAClD,GAAI,CACA,IAAMC,EAAS,MAAMF,GAAO,UAAU,EACtC,GAAI,CAACE,EAAQ,OACb,MAAMA,EAAO,KAAKD,CAAQ,CAC9B,MAAQ,CAER,CACJ,CACJ,GAQaG,GAAN,KAAqB,CAKxB,YACqBC,EACAC,EACnB,CAFmB,YAAAD,EACA,gBAAAC,EALrB,KAAiB,QAAU,IAAI,IAC/B,KAAiB,SAAW,IAAI,IAWhC,gBAAcC,GAA2B,CACrC,KAAK,QAAQ,OAAOA,CAAQ,EAC5B,KAAK,SAAS,OAAOA,CAAQ,CACjC,EAaA,KAAQ,kBAAwBC,GAAwG,CACpI,IAAMC,EAAQD,EAAK,kBACnB,GAAIC,IAAU,OACV,OAEJ,GAAM,CAAE,SAAAF,CAAS,EAAIC,EAIfE,EAAS,KAAK,QAAQ,IAAIH,CAAQ,EACxC,GAAI,CAACG,EACD,OAEJ,IAAMC,EAAMH,EAAK,eAAiB,KAAK,OAAO,aAK9C,GAJYG,GAAOD,EAAO,SAAW,KAAK,IAAI,GACpCC,EAAMF,GAGZ,KAAK,SAAS,IAAIF,CAAQ,EAC1B,OAKJ,IAAIK,EACJA,GAAW,SAAY,CAKnB,IAAMC,EAAQL,EAAK,SAAW,MAAM,KAAK,WAAW,SAASA,EAAK,QAAQ,EAAI,KACxEM,EAAU,MAAMN,EAAK,QAAQ,EAEnC,OAAI,KAAK,SAAS,IAAID,CAAQ,IAAMK,GAChC,KAAK,QAAQ,IAAIL,EAAU,CAAE,QAAAO,EAAS,SAAU,KAAK,IAAI,EAAIH,EAAK,MAAAE,CAAM,CAAC,EAEtEC,CACX,GAAG,EAEH,KAAK,SAAS,IAAIP,EAAUK,CAAsC,EAClEA,EACK,MAAM,IAAM,CAAC,CAAC,EACd,QAAQ,IAAM,CAEP,KAAK,SAAS,IAAIL,CAAQ,IAAMK,GAChC,KAAK,SAAS,OAAOL,CAAQ,CAErC,CAAC,CACT,EAMA,gBAAa,MACTC,GAgBwB,CACxB,GAAM,CAAE,SAAAD,EAAU,aAAAQ,EAAc,SAAAC,CAAS,EAAIR,EACvCE,EAAS,KAAK,QAAQ,IAAIH,CAAQ,EASxC,GANIQ,IACA,KAAK,QAAQ,OAAOR,CAAQ,EAC5B,KAAK,SAAS,OAAOA,CAAQ,GAI7B,CAACQ,GAAgBL,GAAUA,EAAO,SAAW,KAAK,IAAI,EACtD,GAAIF,EAAK,SAAU,CAUf,IAAMS,EAAe,MAAM,KAAK,WAAW,SAAST,EAAK,QAAQ,EACjE,GAAIS,IAAiB,MAAQP,EAAO,QAAUO,EAC1C,YAAK,kBAAkBT,CAAI,EACpBQ,EAAYN,EAAO,QAAQ,IAAKQ,GAAM,gBAAgBA,CAAC,CAAC,EAAaR,EAAO,QAGvF,KAAK,QAAQ,OAAOH,CAAQ,CAChC,KACI,aAAK,kBAAkBC,CAAI,EACpBQ,EAAYN,EAAO,QAAQ,IAAKQ,GAAM,gBAAgBA,CAAC,CAAC,EAAaR,EAAO,QAK3F,GAAI,CAACK,GAAgB,KAAK,SAAS,IAAIR,CAAQ,EAAG,CAC9C,IAAMY,EAAU,MAAM,KAAK,SAAS,IAAIZ,CAAQ,EAChD,OAAOS,EAAWG,EAAO,IAAKD,GAAM,gBAAgBA,CAAC,CAAC,EAAIC,CAC9D,CAIA,IAAIP,EACJA,GAAW,SAAY,CAInB,IAAMC,EAAQL,EAAK,SAAW,MAAM,KAAK,WAAW,SAASA,EAAK,QAAQ,EAAI,KAK9EA,EAAK,eAAe,EACpB,IAAMM,EAAU,MAAMN,EAAK,QAAQ,EAGnC,GAAIO,GAAgB,KAAK,SAAS,IAAIR,CAAQ,IAAMK,EAAS,CACzD,IAAMQ,EAAW,KAAK,IAAI,GAAKZ,EAAK,eAAiB,KAAK,OAAO,cACjE,KAAK,QAAQ,IAAID,EAAU,CAAE,QAAAO,EAAS,SAAAM,EAAU,MAAAP,CAAM,CAAC,EAEnD,KAAK,QAAQ,KAAO,KACpB,KAAK,aAAa,CAE1B,CACA,OAAOC,CACX,GAAG,EAEEC,GAAc,KAAK,SAAS,IAAIR,EAAUK,CAAsC,EAErF,GAAI,CACA,IAAMS,EAAS,MAAMT,EACrB,OAAOI,EAAWK,EAAO,IAAKH,GAAM,gBAAgBA,CAAC,CAAC,EAAIG,CAC9D,QAAE,CAIM,KAAK,SAAS,IAAId,CAAQ,IAAMK,GAChC,KAAK,SAAS,OAAOL,CAAQ,CAErC,CACJ,EAMA,WAASA,GAA2B,CAChC,KAAK,QAAQ,OAAOA,CAAQ,EAC5B,KAAK,SAAS,OAAOA,CAAQ,CACjC,EAMA,cAAW,IAAY,CACnB,KAAK,QAAQ,MAAM,EACnB,KAAK,SAAS,MAAM,CACxB,EAMA,sBAAoBe,GAAyB,CACzC,QAAWC,KAAO,KAAK,QAAQ,KAAK,EAC5BA,EAAI,WAAWD,CAAM,GACrB,KAAK,QAAQ,OAAOC,CAAG,EAG/B,QAAWA,KAAO,KAAK,SAAS,KAAK,EAC7BA,EAAI,WAAWD,CAAM,GACrB,KAAK,SAAS,OAAOC,CAAG,CAGpC,CAjNG,CAuNK,cAAqB,CACzB,IAAMC,EAAM,KAAK,IAAI,EAErB,OAAW,CAACD,EAAKb,CAAM,IAAK,KAAK,QAAQ,QAAQ,EACzCA,EAAO,UAAYc,GACnB,KAAK,QAAQ,OAAOD,CAAG,CAGnC,CACJ,ECvSA,IAAME,GAAmB,sBACnBC,GAAqB,wBAErBC,GAAWC,GACNA,EAAQ,GAAK,IAAIA,CAAK,GAAK,GAAGA,CAAK,GAGxCC,GAAqBD,GAA+B,CACtD,GAAI,OAAO,MAAMA,EAAM,QAAQ,CAAC,EAC5B,OAAO,KAGX,IAAME,EAAOF,EAAM,YAAY,EACzBG,EAAQJ,GAAQC,EAAM,SAAS,EAAI,CAAC,EACpCI,EAAML,GAAQC,EAAM,QAAQ,CAAC,EACnC,MAAO,GAAGE,CAAI,IAAIC,CAAK,IAAIC,CAAG,EAClC,EA0BO,IAAMC,GAA4BC,GAA4C,CACjF,GAAIA,GAAU,KACV,OAAO,KAGX,GAAIA,aAAiB,KACjB,OAAOC,GAAkBD,CAAK,EAGlC,IAAME,EAAUF,EAAM,KAAK,EAC3B,GAAIE,IAAY,GACZ,OAAO,KAGX,GAAIC,GAAiB,KAAKD,CAAO,EAC7B,OAAOA,EAGX,GAAIE,GAAmB,KAAKF,CAAO,EAC/B,OAAOA,EAAQ,WAAW,IAAK,GAAG,EAGtC,IAAMG,EAAS,IAAI,KAAKH,CAAO,EAC/B,OAAOD,GAAkBI,CAAM,CACnC,ECuEO,IAAMC,GAAiCC,GAA8C,CACxF,IAAIC,EAAQ,WAERC,EAAQ,WACZ,QAAWC,KAAQH,EAAO,CACtB,IAAMI,EAAKD,EAAK,YAChBF,GAASG,EAAK,IACdH,EAAQ,KAAK,KAAKA,EAAO,QAAU,EACnCA,GAAUG,IAAO,EAAK,IACtBH,EAAQ,KAAK,KAAKA,EAAO,QAAU,EACnCA,GAAUG,IAAO,GAAM,IACvBH,EAAQ,KAAK,KAAKA,EAAO,QAAU,EACnCA,GAAUG,IAAO,GAAM,IACvBH,EAAQ,KAAK,KAAKA,EAAO,QAAU,EACnCC,GAAUE,IAAO,GAAM,IACvBF,EAAQ,KAAK,KAAKA,EAAO,QAAU,EACnCA,GAAUE,IAAO,GAAM,IACvBF,EAAQ,KAAK,KAAKA,EAAO,QAAU,EACnCA,GAAUE,IAAO,EAAK,IACtBF,EAAQ,KAAK,KAAKA,EAAO,QAAU,EACnCA,GAASE,EAAK,IACdF,EAAQ,KAAK,KAAKA,EAAO,QAAU,EAKnC,IAAMG,EAAOF,EAAK,aAClB,GAAIE,IAAS,KACTJ,GAAS,EACTA,EAAQ,KAAK,KAAKA,EAAO,QAAU,EACnCC,GAAS,EACTA,EAAQ,KAAK,KAAKA,EAAO,QAAU,MAEnC,SAASI,EAAI,EAAGA,EAAID,EAAK,OAAQC,IAAK,CAClC,IAAMC,EAAOF,EAAK,WAAWC,CAAC,EAC9BL,GAASM,EAAO,IAChBN,EAAQ,KAAK,KAAKA,EAAO,QAAU,EACnCC,GAASK,EAAO,IAChBL,EAAQ,KAAK,KAAKA,EAAO,QAAU,CACvC,CAER,CAEA,OAAAD,GAASD,EAAM,OACfC,EAAQ,KAAK,KAAKA,EAAO,QAAU,EACnCC,GAASF,EAAM,OACfE,EAAQ,KAAK,KAAKA,EAAO,QAAU,IAC1BD,IAAU,GAAG,SAAS,EAAE,EAAE,SAAS,EAAG,GAAG,GAAKC,IAAU,GAAG,SAAS,EAAE,EAAE,SAAS,EAAG,GAAG,GAAG,QAAQ,WAAY,EAAE,CAC7H,EAaaM,GAAwB,CAACC,EAA+BC,IAC7DD,EAAE,eAAiBC,EAAE,aAEjBD,EAAE,eAAiB,KAAa,EAChCC,EAAE,eAAiB,MAChBD,EAAE,aAAeC,EAAE,aADU,GACU,EAE3CA,EAAE,YAAcD,EAAE,YAahBE,GAAsBX,GAA+C,CAC9E,QAASM,EAAI,EAAGA,EAAIN,EAAM,OAAQM,IAC9B,GAAIE,GAAsBR,EAAMM,EAAI,CAAC,EAAGN,EAAMM,CAAC,CAAC,EAAI,EAAG,MAAO,GAElE,MAAO,EACX,EA6CO,IAAMM,GAA2B,CAACC,EAAmCC,IAA+C,CACvH,IAAMC,EAAQF,EAAM,UAAWG,GAASC,GAAsB,CAAE,aAAcD,EAAK,aAAc,YAAaA,EAAK,WAAY,EAAGF,CAAM,EAAI,CAAC,EAC7I,OAAOC,IAAU,GAAKF,EAAM,OAASE,CACzC,ECzPO,IAAMG,GAAe,CAACC,EAAiBC,EAAgBC,EAA0B,UAA+B,CAEnH,IAAMC,EAAUC,GAAiC,OAAOA,GAAY,SAAW,CAAC,GAAGH,CAAM,IAAIG,CAAO,EAAE,EAAI,CAACH,EAAQG,CAAO,EAC1H,MAAO,CACH,MAAO,CAACA,KAAsBC,IAAoB,CAC1CL,GAAS,GAAgBE,EAAK,MAAM,GAAGC,EAAOC,CAAO,EAAG,GAAGC,CAAI,CACvE,EACA,KAAM,CAACD,KAAsBC,IAAoB,CACzCL,GAAS,GAAeE,EAAK,KAAK,GAAGC,EAAOC,CAAO,EAAG,GAAGC,CAAI,CACrE,EACA,KAAM,CAACD,KAAsBC,IAAoB,CACzCL,GAAS,GAAeE,EAAK,KAAK,GAAGC,EAAOC,CAAO,EAAG,GAAGC,CAAI,CACrE,EACA,MAAO,CAACD,KAAsBC,IAAoB,CAC1CL,GAAS,GAAgBE,EAAK,MAAM,GAAGC,EAAOC,CAAO,EAAG,GAAGC,CAAI,CACvE,CACJ,CACJ,EC1CA,IAAAC,EAAkB,eAKLC,GAA+B,IAAE,OAAO,CACjD,KAAM,IAAE,OAAO,EAAE,QAAQ,EACzB,YAAa,IAAE,OAAO,EAAE,QAAQ,CACpC,CAAC,EAEYC,GAAmC,IAAE,OAAO,CACrD,KAAM,IAAE,OAAO,EAAE,QAAQ,EACzB,KAAM,IAAE,OAAO,EAAE,QAAQ,EACzB,MAAO,IAAE,OAAO,EAAE,QAAQ,CAC9B,CAAC,EAEYC,GAA4B,IAAE,OAAO,CAC9C,GAAI,IAAE,OAAO,EACb,KAAM,IAAE,OAAO,EAAE,QAAQ,EACzB,MAAO,IAAE,OAAO,EAAE,QAAQ,CAC9B,CAAC,EAEYC,GAAiC,IAAE,OAAO,CACnD,GAAI,IAAE,OAAO,EACb,OAAQ,IAAE,OAAO,EAAE,QAAQ,EAC3B,SAAU,IAAE,OAAO,EAAE,QAAQ,EAC7B,QAAS,IAAE,OAAO,EAAE,QAAQ,EAC5B,UAAW,IAAE,OAAO,EAAE,QAAQ,EAC9B,OAAQ,IAAE,QAAQ,CACtB,CAAC,EAYYC,GAAkC,IAAE,OAAO,CACpD,GAAI,IAAE,OAAO,EACb,SAAU,IAAE,OAAO,EACnB,SAAU,IAAE,OAAO,EAAE,SAAS,EAC9B,SAAU,IAAE,OAAO,EAAE,SAAS,EAC9B,UAAW,IAAE,OAAO,EAAE,SAAS,EAC/B,MAAO,IAAE,KAAK,CAAC,UAAW,SAAU,SAAS,CAAC,CAClD,CAAC,EAEYC,GAA0B,IAAE,OAAO,CAC5C,YAAa,IAAE,OAAO,EACtB,KAAM,IAAE,OAAO,EAAE,QAAQ,EACzB,UAAW,IAAE,OAAO,EAAE,QAAQ,EAC9B,OAAQ,IAAE,OAAO,EAAE,QAAQ,EAC3B,OAAQ,IAAE,OAAO,EAAE,QAAQ,EAC3B,WAAY,IAAE,OAAO,EAAE,QAAQ,EAC/B,aAAc,IAAE,OAAO,EAAE,QAAQ,EACjC,UAAW,IAAE,OAAO,EAAE,QAAQ,EAC9B,UAAW,IAAE,OAAO,EAAE,QAAQ,EAC9B,SAAU,IAAE,OAAO,EAAE,QAAQ,EAC7B,iBAAkB,IAAE,OAAO,EAAE,QAAQ,EACrC,cAAe,IAAE,OAAO,EAAE,QAAQ,EAClC,YAAa,IAAE,OAAO,EAAE,QAAQ,EAChC,aAAc,IAAE,OAAO,EAAE,QAAQ,EACjC,aAAc,IAAE,MAAML,EAA4B,EAClD,QAAS,IAAE,OAAO,EAAE,QAAQ,EAC5B,QAAS,IAAE,OAAO,EAAE,QAAQ,EAC5B,iBAAkB,IAAE,MAAMC,EAAgC,EAC1D,OAAQ,IAAE,QAAQ,EAClB,UAAW,IAAE,QAAQ,EACrB,OAAQ,IAAE,MAAMC,EAAyB,EACzC,eAAgB,IAAE,MAAMC,EAA8B,EAUtD,YAAa,IAAE,MAAMC,EAA+B,EAAE,QAAQ,CAAC,CAAC,CACpE,CAAC,EAIYE,GAAyB,IAAE,OAAO,CAC3C,KAAM,IAAE,QAAQ,WAAW,CAC/B,CAAC,EAEYC,GAA4B,IAAE,OAAO,CAC9C,KAAM,IAAE,QAAQ,eAAe,EAC/B,YAAa,IAAE,OAAO,EACtB,WAAY,IAAE,KAAK,CAAC,OAAQ,MAAM,CAAC,EACnC,MAAO,IAAE,QAAQ,EACjB,aAAc,IAAE,OAAO,EAIvB,mBAAoB,IAAE,OAAO,EAAE,SAAS,CAC5C,CAAC,EAYKC,GAAsB,IAAE,OAAO,EAAE,SAAS,EAEnCC,GAA0B,IAAE,OAAO,CAC5C,KAAM,IAAE,QAAQ,aAAa,EAC7B,YAAa,IAAE,OAAO,EACtB,QAASN,GACT,aAAc,IAAE,OAAO,EAGvB,mBAAoB,IAAE,OAAO,EAAE,SAAS,EACxC,WAAYK,EAChB,CAAC,EAEYE,GAA6B,IAAE,OAAO,CAC/C,KAAM,IAAE,QAAQ,gBAAgB,EAChC,YAAa,IAAE,OAAO,EACtB,UAAW,IAAE,OAAO,EACpB,aAAc,IAAE,OAAO,EAEvB,mBAAoB,IAAE,OAAO,EAAE,SAAS,EACxC,WAAYF,EAChB,CAAC,EAEYG,GAA4B,IAAE,OAAO,CAC9C,KAAM,IAAE,QAAQ,eAAe,EAC/B,YAAa,IAAE,OAAO,EACtB,OAAQN,GACR,aAAc,IAAE,OAAO,EACvB,mBAAoB,IAAE,OAAO,EAAE,SAAS,CAC5C,CAAC,EAEYO,GAA4B,IAAE,OAAO,CAC9C,KAAM,IAAE,QAAQ,eAAe,EAC/B,YAAa,IAAE,OAAO,EACtB,OAAQP,GACR,aAAc,IAAE,OAAO,EACvB,mBAAoB,IAAE,OAAO,EAAE,SAAS,CAC5C,CAAC,EAEYQ,GAA6B,IAAE,OAAO,CAC/C,KAAM,IAAE,QAAQ,gBAAgB,EAChC,YAAa,IAAE,OAAO,EACtB,OAAQR,GACR,aAAc,IAAE,OAAO,EACvB,mBAAoB,IAAE,OAAO,EAAE,SAAS,CAC5C,CAAC,EAEYS,GAA4B,IAAE,OAAO,CAC9C,KAAM,IAAE,QAAQ,eAAe,EAC/B,YAAa,IAAE,OAAO,EACtB,aAAc,IAAE,OAAO,EACvB,WAAYN,EAChB,CAAC,EAIYO,GAA8B,IAAE,mBAAmB,OAAQ,CACpET,GACAC,GACAE,GACAC,GACAC,GACAC,GACAC,GACAC,EACJ,CAAC,EClLD,IAAAE,GAA2B,kBASdC,GAAgBC,GAA0B,CACnD,IAAMC,EAAO,KAAK,UAAUD,CAAI,EAEhC,MAAO,OADM,eAAW,QAAQ,EAAE,OAAOC,CAAI,EAAE,OAAO,KAAK,CAC5C,GACnB,ECVA,IAAMC,GAAgBC,KAA4B,YAAY,EAMjDC,GAAuB,CAACC,EAAkBC,EAAuBC,EAAkCC,EAAS,IAAKC,EAA4BP,KAA4B,CAClL,IAAMQ,EAAOC,GAAaJ,CAAO,EAC3BK,EAAcP,EAAQ,QAAQ,IAAI,eAAe,EAEvDI,EAAO,KAAK,gCAAgCC,CAAI,oBAAoBE,CAAW,EAAE,EAEjF,IAAMC,EAAU,IAAI,QAAQ,CACxB,eAAgB,mBAChB,gBAAiB,sCACjB,yBAA0B,UAC1B,kBAAmB,OACnB,0BAA2B,qBAC3B,KAAMH,CACV,CAAC,EAKD,OAJIJ,GACAO,EAAQ,OAAO,aAAcP,CAAM,EAGnCD,EAAQ,SAAW,OAASG,IAAW,KAAOI,IAAgBF,EACvD,IAAI,SAAS,KAAM,CAAE,OAAQ,IAAK,QAAAG,CAAQ,CAAC,EAG/C,IAAI,SAAS,KAAK,UAAUN,CAAO,EAAG,CAAE,OAAAC,EAAQ,QAAAK,CAAQ,CAAC,CACpE,EC5BA,IAAAC,GAA6B,kBAYtB,IAAMC,GAAgB,CAACC,EAAWC,IAAuB,CAC5D,GAAM,CAACC,EAAKC,CAAI,EAAIH,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM,EACrC,CAACI,EAAKC,CAAI,EAAIJ,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM,EAC3C,OAAIC,IAAQE,EAAYF,EAAME,EACvBD,GAAQE,CACnB,EA0BaC,GAAN,KAA2B,CAe9B,YAA6BC,EAAoC,CAApC,YAAAA,EAd7B,KAAQ,SAAW,IAAI,gBACvB,KAAQ,OAA0C,OAClD,KAAQ,UAAY,EAQpB,KAAQ,QAAU,IAMd,KAAK,SAAS,gBAAgB,CAAC,EAC/B,KAAK,QAAUA,EAAO,QAAUC,KAA4B,cAAc,CAC9E,CAUA,UAAUC,EAAuCC,EAA4C,CACzF,YAAK,SAAS,GAAG,QAASD,CAAO,EAC7BC,GACA,KAAK,SAAS,GAAG,QAASA,CAAO,EAErC,KAAK,YAGD,KAAK,YAAc,GACd,KAAK,WAAW,EAIlB,IAAM,CACT,KAAK,SAAS,eAAe,QAASD,CAAO,EACzCC,GACA,KAAK,SAAS,eAAe,QAASA,CAAO,EAEjD,KAAK,YACD,KAAK,WAAa,IAClB,KAAK,UAAY,EACZ,KAAK,UAAU,EAE5B,CACJ,CAMA,MAAM,SAAyB,CAC3B,KAAK,UAAY,EACjB,MAAM,KAAK,UAAU,EACrB,KAAK,SAAS,mBAAmB,EACjC,KAAK,QAAU,GACnB,CAMA,MAAc,YAA4B,CAEtC,KAAO,KAAK,SAAW,YACnB,MAAM,IAAI,QAASC,GAAY,WAAWA,EAAS,EAAE,CAAC,EAE1D,GAAI,KAAK,SAAW,WAGhB,KAAK,YAAc,EAEvB,MAAK,OAAS,UAId,GAAI,CACA,KAAK,QAAU,MAAM,KAAK,OAAO,OAAO,aAAa,CACzD,OAASC,EAAK,CACV,KAAK,OAAS,OACd,KAAK,WAAWA,aAAe,MAAQA,EAAM,IAAI,MAAM,8CAA8C,CAAC,EACtG,MACJ,CACA,GAAI,CAAC,KAAK,QAAS,CACf,KAAK,OAAS,OACd,KAAK,WAAW,IAAI,MAAM,8CAA8C,CAAC,EACzE,MACJ,CAYA,GATA,KAAK,QAAQ,GAAG,QAAUA,GAAQ,CAC9B,KAAK,QAAQ,MAAM,sBAAuBA,CAAG,CACjD,CAAC,EAOG,KAAK,UAAY,IACjB,GAAI,CACA,IAAMC,EAAS,MAAM,KAAK,OAAO,OAAO,UAAU,EAClD,GAAIA,EAAQ,CACR,IAAMC,EAAS,MAAMD,EAAO,UAAU,KAAK,OAAO,UAAW,IAAK,IAAK,CAAE,MAAO,CAAE,CAAC,EACnF,KAAK,QAAUC,EAAO,OAAS,EAAIA,EAAO,CAAC,EAAE,GAAK,KACtD,CACJ,OAASF,EAAK,CACV,KAAK,QAAQ,KAAK,2DAA4DA,CAAG,CACrF,CAGJ,GAAI,CACA,KAAO,KAAK,SAAW,WAAa,KAAK,SAAS,QAAQ,CACtD,IAAMG,EAAU,MAAM,KAAK,QAAQ,MAAM,CAAC,CAAE,IAAK,KAAK,OAAO,UAAW,GAAI,KAAK,OAAQ,CAAC,EAAG,CAAE,MAAO,IAAM,MAAO,GAAI,CAAC,EACxH,GAAKA,EAEL,QAAWC,KAAUD,EACjB,QAAWE,KAAOD,EAAO,SAAU,CAC/B,KAAK,QAAUC,EAAI,GAGnB,GAAI,CACA,IAAMC,EAASD,EAAI,SAAS,KAAO,KAAK,MAAMA,EAAI,QAAQ,IAAI,EAAI,KAC5DE,EAAUD,GAAQ,KAKxB,IAJIC,IAAY,iBAAmBA,IAAY,iBAAmBA,IAAY,mBAC1E,KAAK,OAAO,MAAM,iBAAiB,kBAAkB,EAGrDA,IAAY,eAAiBA,IAAY,kBAAoBA,IAAY,gBAAiB,CAC1F,IAAMC,EAAcF,GAAQ,YACxB,OAAOE,GAAgB,WACvB,KAAK,OAAO,MAAM,WAAW,uBAAuBA,CAAW,EAAE,EACjE,KAAK,OAAO,MAAM,iBAAiB,uBAAuBA,CAAW,QAAQ,EAErF,CACJ,MAAQ,CAER,CACA,KAAK,SAAS,KAAK,QAAS,CAAE,GAAIH,EAAI,GAAI,QAASA,EAAI,OAAkC,CAAC,CAC9F,CAER,CACJ,OAASL,EAAK,CAGN,EADoBA,GAAe,aAAa,OAAS,sBACtC,KAAK,SAAW,YACnC,KAAK,QAAQ,MAAM,oBAAqBA,CAAG,EAC3C,KAAK,WAAWA,aAAe,MAAQA,EAAM,IAAI,MAAM,OAAOA,CAAG,CAAC,CAAC,EAE3E,QAAE,CAEE,GAAI,KAAK,SAAS,OACd,GAAI,CACA,MAAM,KAAK,QAAQ,KAAK,CAC5B,MAAa,CAEb,CAEJ,KAAK,QAAU,OACf,KAAK,OAAS,OAGV,KAAK,UAAY,GACZ,KAAK,WAAW,CAE7B,EACJ,CAMA,MAAc,WAA2B,CACrC,GAAI,KAAK,SAAW,UAGpB,IAFA,KAAK,OAAS,WAEV,KAAK,SAAS,OACd,GAAI,CACA,MAAM,KAAK,QAAQ,KAAK,CAC5B,MAAa,CAEb,CASJ,KAAQ,KAAK,SAA+C,YACxD,MAAM,IAAI,QAASD,GAAY,WAAWA,EAAS,EAAE,CAAC,EAE9D,CAOQ,WAAWC,EAAkB,CAC7B,KAAK,SAAS,cAAc,OAAO,EAAI,EACvC,KAAK,SAAS,KAAK,QAASA,CAAG,EAE/B,KAAK,QAAQ,MAAM,yCAA0CA,CAAG,CAExE,CACJ,ECpPA,IAAAS,GAAuC,uBAcvC,IAAMC,GAAmB,IAQnBC,GAAwB,GAWxBC,GAAwBC,GAAwBA,EAAI,YAAY,EAShEC,GAAqBD,GAAwBA,EAAI,KAAK,EAAE,YAAY,EAuBnE,SAASE,GAA4BF,EAAiE,CACzG,GAAIA,GAAO,KACP,OAAO,KAIX,GAAI,CAAC,MAAM,QAAQA,CAAG,EAClB,OAAO,IAAI,IAEf,IAAMG,EAAM,IAAI,IAChB,QAAWC,KAASJ,EAAK,CAErB,GAAI,OAAOI,GAAU,SACjB,SAEJ,IAAMC,EAASJ,GAAkBG,CAAK,EAClCC,EAAO,OAAS,GAChBF,EAAI,IAAIE,CAAM,CAEtB,CACA,OAAOF,CACX,CAeO,SAASG,GAAoBH,EAA2BI,EAAgD,CAC3G,OAAIJ,IAAQ,KACD,GAEPI,GAAc,KACP,GAEJJ,EAAI,IAAIJ,GAAqBQ,CAAU,CAAC,CACnD,CAYO,SAASC,GAAsBL,EAAoC,CACtE,OAAOA,IAAQ,MAAQA,EAAI,OAAS,CACxC,CAmBO,SAASM,GAAgCC,EAAkCP,EAA4C,CAC1H,GAAIA,IAAQ,KAGZ,OAAIA,EAAI,OAAS,EACN,iBAEJ,YAAQ,eAAYO,CAAgB,IAAK,CAAC,GAAGP,CAAG,CAAC,CAC5D,CA5KA,IAAAQ,GAAAC,GA2LaC,GAAN,MAAMA,EAAuB,CAoBxB,YAAYC,EAA+BC,EAAmC,CAZtFC,GAAA,KAASL,IAUTK,GAAA,KAASJ,IAGLK,GAAA,KAAKN,GAAWG,GAChBG,GAAA,KAAKL,GAAeG,EACxB,CAGA,IAAI,SAAgC,CAIhC,OAAOG,GAAA,KAAKP,MAAa,KAAO,KAAO,IAAI,IAAIO,GAAA,KAAKP,GAAQ,CAChE,CAOA,IAAI,aAAoC,CAEpC,OAAOO,GAAA,KAAKN,MAAiB,KAAO,KAAO,IAAI,IAAIM,GAAA,KAAKN,GAAY,CACxE,CAuBA,OAAO,WAAWO,EAAoF,CAClG,IAAMC,EAAe,OAAOD,GAAU,UAAYA,IAAU,MAAQ,SAAUA,EACxEE,EAAOnB,GAA4BkB,EAAeD,EAAM,KAAOA,CAAK,EAGpEG,EAAmDF,EAAeD,EAAM,QAAU,OAGxF,GAAIG,IAAe,OACf,OAAO,IAAIT,GAAuBQ,EAAMA,CAAI,EAEhD,IAAME,EAAUrB,GAA4BoB,CAAU,EAIhDE,EAAW,IAAI,IAIrB,QAAWpB,KAASmB,GAAW,CAAC,EACxBF,EAAK,IAAIjB,CAAK,GACdoB,EAAS,IAAIpB,CAAK,EAG1B,OAAO,IAAIS,GAAuBQ,EAAMG,CAAQ,CACpD,CAeA,OAAO,aAAaC,EAA6C,CAE7D,OAAO,IAAIZ,GAAuB,KAAM,IAAI,CAChD,CACJ,EApGaF,GAAA,YAUAC,GAAA,YAlBN,IAAMc,GAANb,GAgIA,SAASc,GAAyBC,EAAuC,CAC5E,IAAMP,EAAOQ,GAAwBD,EAAM,OAAO,EAC5CL,EAAUM,GAAwBD,EAAM,WAAW,EAEzD,OAAIP,IAASE,EACFF,EAIJ,KAAKA,EAAK,MAAM,IAAIA,CAAI,GAAGE,CAAO,EAC7C,CAoBO,SAASM,GAAwB1B,EAAmC,CACvE,GAAIA,IAAQ,KACR,MAAO,MAEX,GAAIA,EAAI,OAAS,EACb,MAAO,OAKX,IAAM2B,EAAS,CAAC,GAAG3B,CAAG,EACjB,KAAK,EACL,IAAKC,GAAU,GAAGA,EAAM,MAAM,IAAIA,CAAK,EAAE,EACzC,KAAKP,EAAgB,EAG1B,GAAIiC,EAAO,QAAUhC,GACjB,MAAO,KAAKgC,CAAM,GAItB,IAAIC,EAAO,WACX,QAASC,EAAI,EAAGA,EAAIF,EAAO,OAAQE,IAC/BD,GAAQD,EAAO,WAAWE,CAAC,EAC3BD,EAAO,KAAK,KAAKA,EAAM,QAAU,EAErC,MAAO,OAAOA,IAAS,GAAG,SAAS,EAAE,CAAC,EAC1C,CC7VA,IAAME,GAAW,CAACC,EAAkBC,IAA6D,CAC7F,IAAMC,EAAU,IAAI,QAAQ,CAAE,eAAgB,kBAAmB,CAAC,EAElE,OAAID,GAAM,WACNC,EAAQ,OAAO,aAAcD,EAAK,SAAS,EAExC,IAAI,SAAS,KAAK,UAAUD,CAAO,EAAG,CAAE,OAAQC,GAAM,QAAU,IAAK,QAAAC,CAAQ,CAAC,CACzF,EAUMC,GAA8B,GAO9BC,GAA6B,IAM7BC,GAAkC,qCAGlCC,GAA0B,sBAU1BC,GAA0B,KAG1BC,GAAuB,OAAO,sBAAsB,EASpDC,GAA+B,GAa/BC,GAA8B,mBAE9BC,GAAwBC,GAAyG,CACnI,IAAMC,EAAQD,EAAI,aAAa,IAAI,sBAAsB,EACnDE,EAAUF,EAAI,aAAa,IAAI,sBAAsB,EACrDG,EAAcH,EAAI,aAAa,IAAI,iBAAiB,EAGpDI,EAAWD,IAAgB,MAAQL,GAA4B,KAAKK,CAAW,EAAIA,EAAc,KAEvG,GAAIF,IAAU,KAEV,OAAOC,IAAY,KAAO,CAAE,OAAQ,KAAM,SAAAE,CAAS,EAAI,CAAE,QAAS,EAAK,EAG3E,IAAMC,EAAc,OAAO,SAASJ,EAAO,EAAE,EAC7C,MAAI,CAAC,OAAO,cAAcI,CAAW,GAAK,OAAOA,CAAW,IAAMJ,GAASI,GAAe,EAC/E,CAAE,QAAS,EAAK,EAGvBH,IAAY,KACL,CAAE,OAAQ,CAAE,aAAc,KAAM,YAAAG,CAAY,EAAG,SAAAD,CAAS,EAE9DV,GAAwB,KAAKQ,CAAO,EAGlC,CAAE,OAAQ,CAAE,aAAcA,EAAS,YAAAG,CAAY,EAAG,SAAAD,CAAS,EAFvD,CAAE,QAAS,EAAK,CAG/B,EAaA,eAAgBE,GAAyBC,EAA0BC,EAA8CC,EAAwBC,EAAmG,CACxO,IAAIC,EAAWH,EAAYI,GAAyBL,EAAOC,CAAS,EAAI,EACpEK,EAAe,GACnB,OAAa,CACT,IAAMC,EAAaP,EAAM,MAAMI,EAAUA,GAAYE,EAAeJ,EAAiBjB,GAA2B,EAChHmB,GAAYG,EAAW,OACvB,IAAMC,EAASJ,GAAYJ,EAAM,OAC3BS,EAAWF,EAAWA,EAAW,OAAS,CAAC,EAejD,GAdA,KAAM,CACF,KAAM,QACN,MAAOA,EACP,WAAYC,GAAU,CAACC,EAAW,KAAO,CAAE,aAAcA,EAAS,aAAc,YAAaA,EAAS,WAAY,EAClH,OAAAD,EACA,GAAIF,EAAe,CAAE,MAAON,EAAM,MAAO,EAAI,CAAC,EAG9C,SAAUG,EAAQ,SAGlB,GAAIG,GAAgBH,EAAQ,UAAY,CAAE,UAAW,EAAK,EAAI,CAAC,CACnE,EACAG,EAAe,GACXE,EAAQ,MAChB,CACJ,CAWA,IAAME,GAAyB,CAACC,EAAuBC,EAAsDC,EAAkBC,IAAwC,CACnK,IAAMC,EAAU,IAAI,YAKhBC,EAAY,GAGZC,EAA6E,KAC3EC,EAAS,IAAI,eAA2B,CAC1C,MAAM,KAAKC,EAAY,CACnB,GAAIH,GAAaH,EAAQ,OAAO,QAAS,CACrC,MAAMD,EAAM,OAAO,MAAS,EAAE,MAAM,IAAM,CAAC,CAAC,EAC5C,GAAI,CACAO,EAAW,MAAM,CACrB,MAAQ,CAER,CACA,MACJ,CACA,IAAIC,EAAuD,KAC3D,GAAI,CACIH,IAAgB,OAChBA,EAAcL,EAAM,KAAK,GAG7B,IAAMS,EAAmB,IAAI,QAAsCC,IAAY,CAC3EF,EAAiB,WAAW,IAAME,GAAQjC,EAAoB,EAAGD,EAAuB,CAC5F,CAAC,EACKmC,EAAQ,MAAM,QAAQ,KAAK,CAACN,EAAaI,CAAgB,CAAC,EAKhE,GAJID,IAAmB,OACnB,aAAaA,CAAc,EAC3BA,EAAiB,MAEjBG,IAAUlC,GAAsB,CAEhC,GAAI2B,GAAaH,EAAQ,OAAO,QAAS,CACrC,MAAMD,EAAM,OAAO,MAAS,EAAE,MAAM,IAAM,CAAC,CAAC,EAC5C,GAAI,CACAO,EAAW,MAAM,CACrB,MAAQ,CAER,CACA,MACJ,CACAA,EAAW,QAAQJ,EAAQ,OAAO;AAAA,CAAI,CAAC,EACvC,MACJ,CACAE,EAAc,KACd,GAAM,CAAE,MAAAO,EAAO,KAAAC,CAAK,EAAIF,EACxB,GAAIE,EAAM,CACNN,EAAW,MAAM,EACjB,MACJ,CAEA,GAAIH,GAAaH,EAAQ,OAAO,QAAS,CACrC,MAAMD,EAAM,OAAO,MAAS,EAAE,MAAM,IAAM,CAAC,CAAC,EAC5C,GAAI,CACAO,EAAW,MAAM,CACrB,MAAQ,CAER,CACA,MACJ,CACAA,EAAW,QAAQJ,EAAQ,OAAO,GAAG,KAAK,UAAUS,CAAK,CAAC;AAAA,CAAI,CAAC,CACnE,OAASE,EAAO,CAMZ,GALIN,IAAmB,MACnB,aAAaA,CAAc,EAG/BH,EAAc,KACVD,GAAaH,EAAQ,OAAO,QAE5B,OAEJC,EAAO,MAAM,+BAA+BY,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,EAAE,EACpG,GAAI,CAGA,IAAMC,EAA2C,CAAE,KAAM,QAAS,MAAOzC,EAAgC,EACzGiC,EAAW,QAAQJ,EAAQ,OAAO,GAAG,KAAK,UAAUY,CAAS,CAAC;AAAA,CAAI,CAAC,EACnER,EAAW,MAAM,CACrB,MAAQ,CAER,CACJ,CACJ,EACA,QAAS,CACLH,EAAY,GACPJ,EAAM,OAAO,MAAS,EAAE,MAAM,IAAM,CAAC,CAAC,CAC/C,CACJ,CAAC,EAEK7B,EAAU,IAAI,QAAQ,CACxB,eAAgB,sCAEhB,gBAAiB,kCACjB,yBAA0B,SAC9B,CAAC,EACD,OAAI4B,GACA5B,EAAQ,OAAO,aAAc4B,CAAM,EAEhC,IAAI,SAASO,EAAQ,CAAE,OAAQ,IAAK,QAAAnC,CAAQ,CAAC,CACxD,EAKM6C,GAA+B,GAAK,KAAO,KAU3CC,GAA2C,GAe3CC,GAAiC,EAUjCC,GAAmC,KAGnCC,GAA2B,KAG3BC,GAAiC,GAGjCC,GAAkC,EAYlCC,GAA+C,IAAI,IAAI,CAAC,YAAa,aAAc,YAAa,aAAc,kBAAmB,YAAY,CAAC,EAG9IC,GAAsB,iBACtBC,GAAqB,sBAkBdC,GAAqBd,GAAqDA,GAASY,GAAoB,KAAKZ,CAAK,GAAKa,GAAmB,KAAKb,CAAK,EAAIA,EAAQ,KAW/Je,GAAiBC,GAAyB,mBAAmBA,CAAI,EAAE,QAAQ,UAAYC,GAAM,IAAIA,EAAE,WAAW,CAAC,EAAE,SAAS,EAAE,EAAE,YAAY,CAAC,EAAE,EAS7IC,GAAyBF,GAAyBA,EAAK,QAAQ,gBAAiB,GAAG,EAAE,QAAQ,SAAU,GAAG,EAYjHG,GAA6BC,GAAiD,CAChF,OAAQA,EAAQ,CACZ,IAAK,YACL,IAAK,SACL,IAAK,eACD,MAAO,KACX,IAAK,YACD,MAAO,KACX,QACI,MAAO,IACf,CACJ,EASMC,GAAgE,CAClE,IAAK,uBACL,IAAK,uBACL,IAAK,gCACT,EAmBMC,GAAqBC,GAA2B,CAClD,IAAMC,EAAU,IAAI,IACpB,MAAO,CAACC,EAAgBC,IAA2B,CAC/C,IAAMC,EAAWH,EAAQ,IAAIC,CAAM,EAE/BE,GAAUH,EAAQ,OAAOC,CAAM,EAEnC,IAAMG,EAASD,GAAY,CAAE,OAAQJ,EAAgB,aAAcG,CAAM,EACnEG,EAAY,KAAK,IAAI,EAAGH,EAAQE,EAAO,YAAY,EACrDC,EAAY,IACZD,EAAO,OAAS,KAAK,IAAIL,EAAgBK,EAAO,OAAUC,EAAYN,EAAkB,GAAM,EAC9FK,EAAO,aAAeF,GAG1B,IAAMI,EAAUF,EAAO,QAAU,EAIjC,IAHIE,IAASF,EAAO,QAAU,GAE9BJ,EAAQ,IAAIC,EAAQG,CAAM,EACnBJ,EAAQ,KAAOhB,IAA0B,CAC5C,IAAMuB,EAASP,EAAQ,KAAK,EAAE,KAAK,EACnC,GAAIO,EAAO,KAAM,MACjBP,EAAQ,OAAOO,EAAO,KAAK,CAC/B,CACA,OAAOD,CACX,CACJ,EAaME,GAAyBC,GAAkB,CAC7C,IAAIC,EAAS,EACb,MAAO,CACH,WAAY,IACJA,GAAUD,EAAc,IAC5BC,GAAU,EACH,IAEX,QAAS,IAAY,CACbA,EAAS,IAAGA,GAAU,EAC9B,CACJ,CACJ,EAiBMC,GAAmB,CAACC,EAAgCC,EAAqBC,IAAkD,CAC7H,IAAMC,EAAuC,WAAWF,EAASC,CAAS,EAExEC,EAA4C,QAAQ,EACtD,IAAMC,EAAS,IAAM,CACjB,aAAaD,CAAK,EAClBF,EAAQ,CACZ,EAEII,EAAO,GACX,OAAO,IAAI,eAA2B,CAClC,KAAK9C,EAAY,CAEb,GAAI8C,EAAM,CACN9C,EAAW,MAAM,EACjB6C,EAAO,EACP,MACJ,CACAC,EAAO,GACP9C,EAAW,QAAQyC,CAAK,CAC5B,EACA,QAAS,CACLI,EAAO,CACX,CACJ,CAAC,CACL,EAuBME,GAAgC,CAACC,EAA+BC,EAA8BC,EAA+BC,IAA+C,CAE9K,GAAID,IAAY,MAAQC,IAAgB,KACpC,MAAO,GAEX,IAAMC,EAAgB,OAAOH,EAAI,YAAe,SAAWA,EAAI,WAAa,OAC5E,OAAQD,EAAO,KAAM,CACjB,IAAK,YACL,IAAK,gBACD,MAAO,GACX,IAAK,gBACL,IAAK,gBACL,IAAK,iBAGD,OAAOK,GAAoBH,EAASF,EAAO,OAAO,UAAU,EAChE,IAAK,cACL,IAAK,iBACD,OAAOK,GAAoBF,EAAaC,GAAiB,UAAU,EACvE,IAAK,gBACD,OAAOC,GAAoBH,EAASE,CAAa,CACzD,CACJ,EAkCME,GAAoB,CAAC,mBAAoB,gBAAgB,EAEzDC,GAA0B,CAACP,EAA+BC,EAA8BE,IAA+C,CAOzI,GANIA,IAAgB,MAGhBH,EAAO,OAAS,iBAAmBA,EAAO,OAAS,iBAAmBA,EAAO,OAAS,kBAGtFK,GAAoBF,EAAaH,EAAO,OAAO,UAAU,EACzD,MAAO,GAEX,IAAMQ,EAASP,EAAI,OACnB,GAAI,OAAOO,GAAW,UAAYA,IAAW,KACzC,MAAO,GAEX,IAAMC,EAASD,EACf,QAAWE,KAAOJ,GACdG,EAAOC,CAAG,EAAI,CAAC,EAEnB,MAAO,EACX,EAkDO,SAASC,GAA0BC,EAAmC,CACzE,GAAM,CAAE,aAAAC,EAAc,QAAAC,EAAS,aAAAC,EAAc,MAAAC,EAAO,UAAAC,EAAW,UAAAC,CAAU,EAAIN,EACvEO,EAAoBP,EAAO,mBAAqB,cAChDQ,EAAYR,EAAO,QAAUS,KAA6B,kBAAkB,EAC5EC,EAAmBV,EAAO,QAAUS,KAA6B,yBAAyB,EAC1FE,EAAYX,EAAO,QAAUS,KAA4B,kBAAkB,EAC3EG,EAAmBZ,EAAO,QAAUS,KAA4B,yBAAyB,EAGzFI,EAAqBb,EAAO,oBAAsBnD,GAClDiE,EAA6B/C,GAAkBiC,EAAO,8BAAgClD,EAAwC,EAC9HiE,EAAiBtC,GAAsBuB,EAAO,uBAAyBjD,EAA8B,EAarGiE,GAAqB,MAAOlF,EAAkB+B,EAA6C,YAA+C,CAC5I,IAAMoD,EAAOjB,EAAO,0BACpB,GAAI,CAACiB,EACD,OAAOC,GAAuB,aAAa,+EAA+E,EAE9H,GAAI,CACA,IAAMC,EAAW,MAAMF,EAAKnF,EAAS,CAAE,OAAA+B,CAAO,CAAC,EAI/C,OAAOsD,GAAY,KAAOD,GAAuB,aAAa,4FAA4F,EAAIA,GAAuB,WAAWC,CAAQ,CAC5M,OAASC,EAAG,CACR,OAAAV,EAAiB,MAAM,6DAA8DU,CAAC,EAC/EF,GAAuB,WAAW,CAAC,CAAC,CAC/C,CACJ,EAQMG,GAAc,MAAO,CAAE,QAAAvF,CAAQ,IAAkB,CACnD,GAAM,CAAE,KAAAwF,EAAM,OAAA1F,CAAO,EAAI,MAAMqE,EAAanE,EAAS,CAAE,gBAAiByE,CAAkB,CAAC,EACrFvG,EAAU,IAAI,QAChB4B,GACA5B,EAAQ,OAAO,aAAc4B,CAAM,EAMvC,IAAMsC,EAAwB,MAAMgC,EAAQ,sBAAsBoB,EAAK,EAAE,EAEnEC,EAAerD,EAASiC,EAAajC,CAAM,EAAI,KAErD,MAAO,CAAE,KAAM,CAAE,KAAAoD,EAAM,OAAQC,CAAa,EAAG,QAAAvH,CAAQ,CAC3D,EAWMwH,GAAoD,CAKtD,gBAAiB,MAAO9G,EAAKkB,EAAQE,EAASwF,EAAMG,IAAU,CAC1D,IAAMC,EAAyBC,GAAyBjH,EAAI,aAAa,IAAI,cAAc,CAAC,EACtFkH,EAAelH,EAAI,aAAa,IAAI,cAAc,IAAM,OAE9D,GAAI,CAACgH,EACD,OAAOG,GAAqB/F,EAASF,EAAQ,CAAE,MAAO,CAAE,QAAS,uBAAwB,CAAE,EAAG,GAAG,EAErG,IAAMkG,EAAU,MAAM5B,EAAQ,0CAA0CuB,EAAOC,EAAwBJ,EAAK,GAAI,CAAE,aAAAM,CAAa,CAAC,EAChI,OAAOC,GAAqB/F,EAASF,EAAQ,CAAE,aAAc8F,EAAwB,QAAAI,CAAQ,EAAG,GAAG,CACvG,EAaA,aAAc,MAAOpH,EAAKkB,EAAQE,EAASwF,EAAMG,IAAU,CACvD,IAAMrC,EAAS3E,GAAqBC,CAAG,EACvC,GAAI,YAAa0E,EACb,OAAOyC,GAAqB/F,EAASF,EAAQ,CAAE,MAAO,CAAE,QAAS,gBAAiB,CAAE,EAAG,GAAG,EAE9F,IAAMgG,EAAelH,EAAI,aAAa,IAAI,cAAc,IAAM,OAS1DqH,EAAmB,GACnBC,EAA2D,KAC3DC,EAAkD,KAChDC,EAAc,IAAI,QAA2B,CAAC3F,EAAS4F,IAAW,CACpEH,EAAczF,EACd0F,EAAaE,CACjB,CAAC,EAEDD,EAAY,MAAM,IAAM,CAAC,CAAC,EAG1B,IAAME,GAAmBlC,EAAQ,8BAA8BuB,EAAOH,EAAK,GAAI,CAC3E,aAAAM,EAEA,aAAcxC,EAAO,OACf,OACA,IAAM,CACF2C,EAAmB,GACnB7B,EAAQ,uCAAuCuB,EAAOH,EAAK,GAAIrH,EAA2B,EAAE,KACvFgB,GAAU+G,IAAc/G,CAAK,EAC7B0B,GAAUsF,IAAatF,CAAK,CACjC,CACJ,CACV,CAAC,EAID,GAFAyF,GAAiB,MAAM,IAAM,CAAC,CAAC,EAE3BhD,EAAO,OAAQ,CAIf,IAAMiD,EAAY,MAAMD,GAClBtH,EAAWwH,GAA8BD,CAAS,EAKlDE,EAAYnD,EAAO,WAAa,MAAQA,EAAO,WAAatE,EAClE,OAAOa,GAAuBC,EAAQZ,GAAyBqH,EAAWE,EAAYnD,EAAO,OAAS,KAAMnF,GAA6B,CAAE,SAAAa,EAAU,UAAW,CAACyH,CAAU,CAAC,EAAGzG,EAAS0E,CAAS,CACrM,CAcA,IAAMgC,GAAgB,iBAAkE,CACpF,IAAMC,EAAS,MAAM,QAAQ,KAAK,CAC9BL,GAAiB,KACZnH,IAAW,CAAE,KAAM,OAAiB,MAAAA,CAAM,GAC1C0B,IAAW,CAAE,KAAM,aAAuB,MAAAA,CAAM,EACrD,EACAuF,EAAY,KACPjH,IAAW,CAAE,KAAM,OAAiB,MAAAA,CAAM,GAC3C,KAAO,CAAE,KAAM,YAAsB,EACzC,CACJ,CAAC,EAEGoH,EAAsC,KACtCK,EAAyB,GAE7B,GAAID,EAAO,OAAS,OAChBJ,EAAYI,EAAO,cACZA,EAAO,OAAS,OAAQ,CAC/B,IAAME,EAAYF,EAAO,MACzB,GAAIE,EAAU,OAAS1I,GAA6B,CAIhD,KAAM,CAAE,KAAM,QAAS,MAAO0I,EAAW,WAAY,KAAM,OAAQ,GAAM,MAAOA,EAAU,MAAO,EACjG,MACJ,CAGA,KAAM,CAAE,KAAM,QAAS,MAAOA,EAAW,WAAY,KAAM,OAAQ,GAAO,YAAa,EAAK,EAC5FD,EAAyB,EAC7B,SAAWD,EAAO,OAAS,cAAgBV,EAAkB,CAGzD,IAAMY,EAAY,MAAMT,EAAY,MAAM,IAAM,IAAI,EAChDS,GAAaA,EAAU,OAAS,IAChC,KAAM,CAAE,KAAM,QAAS,MAAOA,EAAW,WAAY,KAAM,OAAQ,GAAO,YAAa,EAAK,EAEpG,CAEIN,IAAc,OACdA,EAAY,MAAMD,IAMtB,MAAOpH,GAAyBqH,EAAW,KAAMK,EAAyBxI,GAA6BD,GAA6B,CAAE,SAAUqI,GAA8BD,CAAS,CAAE,CAAC,CAC9L,GAAG,EAEH,OAAO1G,GAAuBC,EAAQ4G,EAAc1G,EAAS0E,CAAS,CAC1E,EAKA,OAAQ,MAAO9F,EAAKkB,EAAQE,EAASwF,EAAMG,IAAU,CACjD,IAAMmB,EAAQlI,EAAI,aAAa,IAAI,aAAa,EAC1CkH,EAAelH,EAAI,aAAa,IAAI,cAAc,IAAM,OACxDmI,EAAWD,EAAQ,OAAO,SAASA,EAAO,EAAE,EAAI,IAEtD,GAAI,CAAC,OAAO,SAASC,CAAQ,GAAKA,GAAY,EAC1C,OAAOhB,GAAqB/F,EAASF,EAAQ,CAAE,MAAO,CAAE,QAAS,qBAAsB,CAAE,EAAG,GAAG,EAEnG,IAAMkH,EAAS,MAAM5C,EAAQ,qCAAqCuB,EAAOoB,EAAUvB,EAAK,GAAI,CAAE,SAAU,GAAM,aAAAM,CAAa,CAAC,EAC5H,OAAKkB,EAGEjB,GAAqB/F,EAASF,EAAQ,CAAE,OAAQkH,CAAO,EAAG,GAAG,EAFzDjB,GAAqB/F,EAASF,EAAQ,CAAE,MAAO,CAAE,QAAS,kBAAmB,CAAE,EAAG,GAAG,CAGpG,CACJ,EAqtBA,MAAO,CACH,MAAO,CAAE,OAAQyF,EAAY,EAC7B,IAAK,CAAE,OAjtBO,MAAO,CAAE,QAAAvF,EAAS,OAAAiH,CAAO,IAAkB,CACzD,GAAM,CAAE,KAAAzB,EAAM,OAAA1F,CAAO,EAAI,MAAMqE,EAAanE,EAAS,CAAE,gBAAiB,IAAK,CAAC,EACxEkH,EAAkBpH,GAAU,KAElC,GAAI,CAAC0F,EACD,OAAOO,GAAqB/F,EAASkH,EAAiB,CAAE,MAAO,CAAE,QAAS,cAAe,CAAE,EAAG,GAAG,EAGrG,GAAIlH,EAAQ,SAAW,MACnB,OAAO+F,GAAqB/F,EAASkH,EAAiB,CAAE,MAAO,CAAE,QAAS,oBAAqB,CAAE,EAAG,GAAG,EAE3G,IAAMC,EAAWF,EAAO,UAAY,GAC9BG,EAAU1B,GAAiByB,CAAQ,EACzC,GAAI,CAACC,EACD,OAAOrB,GAAqB/F,EAASkH,EAAiB,CAAE,MAAO,CAAE,QAAS,kBAAmB,CAAE,EAAG,GAAG,EAEzG,IAAMG,EAAY,KAAK,IAAI,EAC3B,GAAI,CACA,IAAMzI,EAAM,IAAI,IAAIoB,EAAQ,GAAG,EAEzB2F,EAAQ,MAAMT,GAAmBlF,CAAO,EAC9C,OAAO,MAAMoH,EAAQxI,EAAKsI,EAAiBlH,EAASwF,EAAMG,CAAK,CACnE,OAAS9E,EAAO,CACZ,IAAMyG,EAAU,KAAK,IAAI,EAAID,EACvBE,EAAM1G,aAAiB,MAAQA,EAAQ,IAAI,MAAM,OAAOA,CAAK,CAAC,EACpE,OAAA6D,EAAU,MAAM,gBAAgByC,CAAQ,YAAYG,CAAO,WAAWC,EAAI,IAAI,SAAS,SAAUA,EAAOA,EAA0B,KAAO,KAAK,YAAYA,EAAI,OAAO,EAAE,EACvK7C,EAAU,MAAM,SAAU6C,EAAI,KAAK,EAC5BxB,GAAqB/F,EAASkH,EAAiB,CAAE,MAAO,CAAE,QAAS,uBAAwB,CAAE,EAAG,GAAG,CAC9G,CACJ,EAorB8B,OA9qBZ,MAAO,CAAE,QAAAlH,EAAS,OAAAiH,CAAO,IAAkB,CACzD,GAAM,CAAE,KAAAzB,EAAM,OAAA1F,CAAO,EAAI,MAAMqE,EAAanE,EAAS,CAAE,gBAAiB,IAAK,CAAC,EAexEwH,EAAU,CAACxJ,EAAkBC,IAAyCF,GAASC,EAAS,CAAE,GAAGC,EAAM,UAAW6B,CAAO,CAAC,EAE5H,GAAI,CAAC0F,EACD,OAAOgC,EAAQ,CAAE,MAAO,cAAe,EAAG,CAAE,OAAQ,GAAI,CAAC,EAM7D,IAAMC,EAAkB,KAAK,IAAI,EACjC,GAAI,CAEA,GADiBR,EAAO,WACP,SACb,OAAOO,EAAQ,CAAE,MAAO,kBAAmB,EAAG,CAAE,OAAQ,GAAI,CAAC,EAGjE,IAAMpF,EAAS,MAAMgC,EAAQ,sBAAsBoB,EAAK,EAAE,EAC1D,GAAI,CAACpD,EACD,OAAOoF,EAAQ,CAAE,MAAO,gBAAiB,EAAG,CAAE,OAAQ,GAAI,CAAC,EAK/D,IAAM7B,EAAQ,MAAMT,GAAmBlF,CAAO,EAExC0H,EAAW,MAAM1H,EAAQ,SAAS,EAClC2H,EAASD,EAAS,IAAI,QAAQ,EAC9BE,GAAiBF,EAAS,IAAI,aAAa,EAC3CzI,EAAc2I,GAAiB,OAAOA,EAAc,EAAI,IACxDC,EAAeH,EAAS,IAAI,cAAc,EAC1CI,EAAqB,OAAOJ,EAAS,IAAI,oBAAoB,CAAC,EAC9DK,EAAeL,EAAS,IAAI,cAAc,EAEhD,GAAIC,IAAW,aAGX,OAAKzD,EAAO,qBAGZ,MAAME,EAAQ,WAAW,EAClBoD,EAAQ,CAAE,OAAQ,KAAM,OAAQ,YAAa,CAAC,GAH1CA,EAAQ,CAAE,MAAO,gBAAiB,EAAG,CAAE,OAAQ,GAAI,CAAC,EAMnE,GAAI,CAACO,EACD,OAAOP,EAAQ,CAAE,MAAO,uBAAwB,EAAG,CAAE,OAAQ,GAAI,CAAC,EAGtE,GAAIG,IAAW,WAAa,CAAC1I,GAAe,OAAO,MAAMA,CAAW,GAChE,OAAOuI,EAAQ,CAAE,MAAO,qBAAsB,EAAG,CAAE,OAAQ,GAAI,CAAC,EAepE,IAAMQ,EAAkB,MAAOC,EAAyCC,EAAmCC,IAAmG,CAC1M,IAAM5E,GAAMmE,EAAS,IAAIQ,CAAS,EAClC,GAAI3E,KAAQ,KACR,OAAOiE,EAAQ,CAAE,MAAO,GAAGU,CAAS,WAAY,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEtE,GAAI,CACA,IAAME,GAAgB,MAAMD,EAAM5E,KAAQ,MAAM,EAGhD,OAAOiE,EAAQ,CAAE,OAAQ,KAAM,OAAQS,EAAY,cAAe,CAAE,UAAWG,GAAc,UAAW,OAAQA,GAAc,MAAO,EAAG,mBAAAN,EAAoB,YAAa,OAAO7I,CAAW,EAAG,aAAA8I,CAAa,CAAC,CAChN,OAASzC,GAAY,CAEjB,GAAIA,cAAa,OAASA,GAAE,UAAY,YACpC,OAAOkC,EAAQ,CAAE,MAAO,kBAAmB,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEjE,MAAMlC,EACV,CACJ,EAEA,OAAQqC,EAAQ,CACZ,IAAK,SAAU,CACX,GAAI,CAACE,EACD,OAAOL,EAAQ,CAAE,MAAO,uBAAwB,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEtE,IAAMa,EAAY,MAAMjE,EAAQ,kBAAkBhC,EAAQyF,EAAcE,CAAY,EACpF,OAAOP,EAAQ,CACX,OAAQ,KACR,OAAQ,SACR,OAAQa,EACR,YAAa,OAAOA,EAAU,WAAW,EACzC,aAAAN,CACJ,CAAC,CACL,CACA,IAAK,SAAU,CACX,IAAMO,EAAQZ,EAAS,IAAI,OAAO,EAC5Ba,EAAUb,EAAS,IAAI,SAAS,EACtC,GAAI,CACA,aAAMtD,EAAQ,kBAAkBnF,EAAamD,EAAQ,CAAE,MAAAkG,EAAO,QAAAC,CAAQ,EAAGR,CAAY,EAC9EP,EAAQ,CAAE,OAAQ,KAAM,OAAQ,SAAU,YAAa,OAAOvI,CAAW,EAAG,aAAA8I,CAAa,CAAC,CACrG,OAASzC,EAAY,CACjB,GAAIA,aAAa,MAAO,CACpB,GAAIA,EAAE,UAAY,eACd,OAAOkC,EAAQ,CAAE,MAAO,cAAe,EAAG,CAAE,OAAQ,GAAI,CAAC,EAE7D,GAAIlC,EAAE,UAAY,YACd,OAAOkC,EAAQ,CAAE,MAAO,kBAAmB,EAAG,CAAE,OAAQ,GAAI,CAAC,CAErE,CACA,MAAMlC,CACV,CACJ,CACA,IAAK,UACD,GAAI,CACA,IAAMkD,EAAkB,MAAMpE,EAAQ,mBAAmBnF,EAAamD,EAAQ2F,CAAY,EAC1F,OAAOP,EAAQ,CAAE,OAAQ,KAAM,OAAQ,UAAW,YAAa,OAAOvI,CAAW,EAAG,aAAA8I,EAAc,OAAQS,CAAgB,CAAC,CAC/H,OAASlD,EAAY,CACjB,GAAIA,aAAa,MAAO,CACpB,GAAIA,EAAE,UAAY,eACd,OAAOkC,EAAQ,CAAE,MAAO,cAAe,EAAG,CAAE,OAAQ,GAAI,CAAC,EAE7D,GAAIlC,EAAE,UAAY,YACd,OAAOkC,EAAQ,CAAE,MAAO,kBAAmB,EAAG,CAAE,OAAQ,GAAI,CAAC,CAErE,CACA,MAAMlC,CACV,CAEJ,IAAK,SACD,GAAI,CACA,aAAMlB,EAAQ,kBAAkBnF,EAAamD,EAAQ2F,CAAY,EAC1DP,EAAQ,CACX,OAAQ,KACR,OAAQ,SACR,YAAa,OAAOvI,CAAW,EAC/B,aAAA8I,CACJ,CAAC,CACL,OAASzC,EAAY,CACjB,GAAIA,aAAa,MAAO,CACpB,GAAIA,EAAE,UAAY,eACd,OAAOkC,EAAQ,CAAE,MAAO,cAAe,EAAG,CAAE,OAAQ,GAAI,CAAC,EAE7D,GAAIlC,EAAE,UAAY,YACd,OAAOkC,EAAQ,CAAE,MAAO,kBAAmB,EAAG,CAAE,OAAQ,GAAI,CAAC,CAErE,CACA,MAAMlC,CACV,CAEJ,IAAK,aACD,OAAO,MAAM0C,EAAgB,aAAc,YAAcrH,GAAUyD,EAAQ,cAAcuB,EAAOvD,EAAQnD,EAAa4I,EAAclH,EAAOoH,CAAY,CAAC,EAC3J,IAAK,aACD,OAAO,MAAMC,EAAgB,aAAc,SAAWrH,GAAUyD,EAAQ,cAAcuB,EAAOvD,EAAQnD,EAAa4I,EAAclH,EAAOoH,CAAY,CAAC,EACxJ,IAAK,aAAc,CACf,IAAMQ,EAAUb,EAAS,IAAI,SAAS,EACtC,GAAI,CAACa,EACD,OAAOf,EAAQ,CAAE,MAAO,kBAAmB,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEjE,GAAI,CACA,IAAMiB,EAAa,MAAMrE,EAAQ,WAAWuB,EAAOvD,EAAQnD,EAAasJ,EAASV,EAAcE,CAAY,EACrGW,EAAc,CAAE,GAAGD,EAAY,OAAQA,EAAW,MAAO,EAC/D,OAAOjB,EAAQ,CAAE,OAAQ,KAAM,OAAQ,aAAc,WAAYkB,EAAa,YAAa,OAAOzJ,CAAW,EAAG,aAAA8I,CAAa,CAAC,CAClI,OAASzC,EAAY,CACjB,GAAIA,aAAa,MAAO,CAEpB,GAAIA,EAAE,UAAY,YACd,OAAOkC,EAAQ,CAAE,MAAO,kBAAmB,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEjE,GAAIlC,EAAE,QAAQ,WAAW,yBAAyB,EAC9C,OAAOkC,EAAQ,CAAE,MAAO,yCAA0C,EAAG,CAAE,OAAQ,GAAI,CAAC,CAE5F,CACA,MAAMlC,CACV,CACJ,CACA,IAAK,gBAAiB,CAClB,IAAMqD,EAAY,OAAOjB,EAAS,IAAI,WAAW,CAAC,EAClD,GAAI,CAACiB,GAAa,OAAO,MAAMA,CAAS,EACpC,OAAOnB,EAAQ,CAAE,MAAO,mBAAoB,EAAG,CAAE,OAAQ,GAAI,CAAC,EAElE,GAAI,CACA,aAAMpD,EAAQ,cAAcuB,EAAOvD,EAAQnD,EAAa0J,EAAWd,EAAcE,CAAY,EACtFP,EAAQ,CAAE,OAAQ,KAAM,YAAa,OAAOvI,CAAW,EAAG,iBAAkB,OAAO0J,CAAS,EAAG,aAAAZ,CAAa,CAAC,CACxH,OAASzC,EAAY,CACjB,GAAIA,aAAa,MAAO,CACpB,GAAIA,EAAE,UAAY,eACd,OAAOkC,EAAQ,CAAE,MAAO,cAAe,EAAG,CAAE,OAAQ,GAAI,CAAC,EAE7D,GAAIlC,EAAE,UAAY,YACd,OAAOkC,EAAQ,CAAE,MAAO,mBAAoB,EAAG,CAAE,OAAQ,GAAI,CAAC,CAEtE,CACA,MAAMlC,CACV,CACJ,CACA,QACI,OAAOkC,EAAQ,CAAE,MAAO,gBAAiB,EAAG,CAAE,OAAQ,GAAI,CAAC,CACnE,CACJ,OAAS3G,EAAO,CAEZ,GAAIA,aAAiB,SAAU,MAAMA,EACrC,IAAMyG,EAAU,KAAK,IAAI,EAAIG,EACvBF,EAAM1G,aAAiB,MAAQA,EAAQ,IAAI,MAAM,OAAOA,CAAK,CAAC,EACpE,OAAA6D,EAAU,MAAM,sBAAsB4C,CAAO,WAAWC,EAAI,IAAI,YAAYA,EAAI,OAAO,EAAE,EACzF7C,EAAU,MAAM,SAAU6C,EAAI,KAAK,EAC5BC,EAAQ,CAAE,MAAO,uBAAwB,EAAG,CAAE,OAAQ,GAAI,CAAC,CACtE,CACJ,CA6cgD,EAC5C,IAAK,CAAE,OAtcO,MAAO,CAAE,QAAAxH,EAAS,OAAAiH,CAAO,IAAkB,CAEzD,GAAM,CAAE,KAAAzB,EAAM,OAAA1F,CAAO,EAAI,MAAMqE,EAAanE,EAAS,CAAE,gBAAiB,IAAK,CAAC,EAMxE4I,EAAkBC,IAChB/I,IAAW,OAAkB+I,GAC1B,CAAE,GAAIA,IAAS,CAAC,EAAI,aAAc/I,CAAO,EAEpD,GAAI,CAAC0F,EACD,OAAO,IAAI,SAAS,eAAgB,CAAE,OAAQ,IAAK,QAASoD,EAAe,CAAE,CAAC,EAIlF,IAAME,EAAiB,MAAM1E,EAAQ,sBAAsBoB,EAAK,EAAE,EAClE,GAAI,CAACsD,EACD,OAAO,IAAI,SAAS,YAAa,CAAE,OAAQ,IAAK,QAASF,EAAe,CAAE,CAAC,EAI/E,GAAI3B,EAAO,WAAa,UACpB,OAAO,IAAI,SAAS,YAAa,CAAE,OAAQ,IAAK,QAAS2B,EAAe,CAAE,CAAC,EAQ/E,IAAMG,EAAe,MAAM7D,GAAmBlF,CAAO,EACjDgJ,EAAaD,EAAa,QAC1BE,EAAiBF,EAAa,YAC9BG,EAAgB,EAChBC,EAAoB,EAKlBC,EADM,IAAI,IAAIpJ,EAAQ,GAAG,EACP,aAAa,IAAI,aAAa,GAAKA,EAAQ,QAAQ,IAAI,eAAe,EAExFE,EAAU,IAAI,YAChBmJ,EAAoE,KACpEC,EAA2D,KAC3DC,EAAmC,KACnCC,EAAe,GAGbC,EAAU,IAAM,CAClB,GAAI,CAAAD,EAgBJ,IAfAA,EAAe,GAGXF,IACA,cAAcA,CAAiB,EAC/BA,EAAoB,MAIpBC,IACAA,EAAY,EACZA,EAAc,MAIdF,EACA,GAAI,CACIA,EAAc,cAAgB,MAC9BA,EAAc,MAAM,CAE5B,MAAa,CAEb,QAAE,CACEA,EAAgB,IACpB,CAGJG,EAAe,GACnB,EAgBME,EAAkBC,IAA0B,CAC1CN,GAAiBA,EAAc,cAAgB,MAC/CA,EAAc,QAAQnJ,EAAQ,OAAO,OAAOyJ,EAAO;AAAA,QAAW,KAAK,UAAU,CAAE,KAAM,WAAY,CAAC,CAAC;AAAA;AAAA,CAAM,CAAC,CAElH,EAEMC,GAAe,CAACD,GAAiBE,KAAmC,CAEtE,GAAKA,GAAO,KAEZ,GAAI,CACA,IAAMtG,EAAM,KAAK,MAAMsG,GAAO,IAAI,EAC5BC,EAASC,GAA4B,UAAUxG,CAAG,EACxD,GAAI,CAACuG,EAAO,QAAS,CACjBjF,EAAU,MAAM,iCAAkCiF,EAAO,MAAM,OAAO,CAAC,EACvE,MACJ,CACA,IAAMxG,EAASwG,EAAO,KAGhBE,GAAqB,OAAOzG,EAAI,oBAAuB,SAAWA,EAAI,mBAAqB,OAEjG,GAAID,EAAO,OAAS,iBAChB,GAAI0G,KAAuB,QAAaA,KAAuBlB,EAC3D,eAEGkB,KAAuB,QAAaA,KAAuBlB,EAClE,OAIJ,GAAI,CAACzF,GAA8BC,EAAQC,EAAKyF,EAAYC,CAAc,EAAG,CAMzES,EAAeC,EAAO,EACtB,MACJ,CAGA,IAAMM,GAAWpG,GAAwBP,EAAQC,EAAK0F,CAAc,EAGhEiB,GACJ,GAAI,uBAAwB3G,EAAK,CAC7B,GAAM,CAAE,mBAAoB4G,GAAG,GAAGC,EAAK,EAAI7G,EAC3C2G,GAAmB,KAAK,UAAUE,EAAI,CAC1C,MAAWH,GAGPC,GAAmB,KAAK,UAAU3G,CAAG,EAErC2G,GAAmBL,GAAO,KAI1BR,GAAiBA,EAAc,cAAgB,MAC/CA,EAAc,QAAQnJ,EAAQ,OAAO,OAAOyJ,EAAO;AAAA,QAAWO,EAAgB;AAAA;AAAA,CAAM,CAAC,CAE7F,OAAS5E,EAAG,CACRT,EAAU,MAAM,QAAQiE,CAAc,wBAAyBxD,CAAC,CACpE,CACJ,EAEMjF,GAAS,IAAI,eAAe,CAC9B,MAAM,MAAMC,GAAY,CAEpB+I,EAAgB/I,GAGhBgJ,EAAoB,YAAY,IAAM,CAClC,GAAI,CACA,GAAID,GAAiBA,EAAc,cAAgB,MAM/C,GALAA,EAAc,QAAQnJ,EAAQ,OAAO;AAAA;AAAA,CAAkB,CAAC,EAKpD,EAAEgJ,EAAgBzK,KAAiC,EAAG,CACtD,IAAM4L,EAAa,EAAElB,EACrBjE,GAAmBlF,EAAS,SAAS,EAAE,KAAMsK,GAAS,CAC9CD,IAAelB,IAGfH,EAAasB,EAAK,QAClBrB,EAAiBqB,EAAK,YAE9B,CAAC,CACL,OAEAb,EAAQ,CAEhB,MAAa,CACTA,EAAQ,CACZ,CACJ,EAAG,GAAI,EAGPzJ,EAAQ,OAAO,iBAAiB,QAAS,IAAM,CAC3CyJ,EAAQ,CACZ,CAAC,EAGD,IAAIc,GAAkBnB,GAAe,MAUjCoB,EAAkC,KAqBtC,GAlBAjB,EAAchF,EAAU,UACnBkG,GAAuB,CAEhBC,GAAcD,EAAM,GAAIF,EAAe,IACvCC,IAAqB,OACrBA,EAAmBC,EAAM,IAE7BF,GAAkBE,EAAM,GACxBb,GAAaa,EAAM,GAAIA,EAAM,OAAO,EACxC,EACClD,GAAe,CACZ1C,EAAU,MAAM,QAAQiE,CAAc,0BAA2BvB,CAAG,EACpEkC,EAAQ,CACZ,CACJ,EAII,CAACL,EACD,GAAI,CACA,IAAMuB,EAAS,MAAMrG,GAAO,UAAU,EACtC,GAAIqG,EAAQ,CACR,IAAMC,EAAS,MAAMD,EAAO,UAAUnG,EAAW,IAAK,IAAK,CAAE,MAAO,CAAE,CAAC,EAEnEoG,EAAO,OAAS,GAAK,CAACF,GAAcE,EAAO,CAAC,EAAE,GAAIL,EAAe,IACjEA,GAAkBK,EAAO,CAAC,EAAE,GAEpC,CACJ,MAAQ,CAER,CAIJ,GAAIvB,GAAiBA,EAAc,cAAgB,KAAM,CACrD,IAAMwB,EAAmB,SAAS,KAAK,UAAU,CAAE,KAAM,WAAY,CAAC,CAAC;AAAA;AAAA,EACjEC,EAAaP,KAAoB,MAAQ,OAAOA,EAAe;AAAA,EAAKM,CAAgB,GAAKA,EAC/FxB,EAAc,QAAQnJ,EAAQ,OAAO4K,CAAU,CAAC,CACpD,CAGA,GAAI1B,EACA,GAAI,CACA,IAAMuB,EAAS,MAAMrG,GAAO,UAAU,EACtC,GAAIqG,EAAQ,CACR,IAAMI,EAAiB,MAAMJ,EAAO,OAAOnG,EAAW4E,EAAa,IAAK,CAAE,MAAO,GAAK,CAAC,EACvF,QAAWqB,MAASM,EAEZN,GAAM,KAAOrB,IAIboB,IAAqB,MAAQE,GAAcF,EAAkBC,GAAM,EAAE,IAEpEC,GAAcD,GAAM,GAAIF,EAAe,IACxCA,GAAkBE,GAAM,IAE5Bb,GAAaa,GAAM,GAAIA,GAAM,OAAiC,GAEtE,CACJ,OAASnF,EAAG,CACRT,EAAU,MAAM,QAAQiE,CAAc,2BAA4BxD,CAAC,CAEvE,CAER,EACA,QAAS,CACLmE,EAAQ,CACZ,CACJ,CAAC,EAED,OAAO,IAAI,SAASpJ,GAAQ,CACxB,QAASuI,EAAe,CACpB,eAAgB,oBAChB,gBAAiB,yBACjB,WAAY,YAChB,CAAC,CACL,CAAC,CACL,CAkK6B,EACzB,WAAY,CAAE,OA/IO,MAAO,CAAE,QAAA5I,EAAS,OAAAiH,CAAO,IAAkB,CAChE,IAAIC,EAAiC,KAG/B8D,EAAO,CAACC,EAAgBC,EAAiBC,IAAyC,CACpF,IAAMjN,EAAU,IAAI,QAAQ,CAAE,eAAgB,kBAAmB,CAAC,EAClE,OAAIiN,IAAsB,QAAWjN,EAAQ,IAAI,cAAe,OAAOiN,CAAiB,CAAC,EACrFjE,GAAiBhJ,EAAQ,OAAO,aAAcgJ,CAAe,EAC1D,IAAI,SAAS,KAAK,UAAU,CAAE,MAAO,CAAE,QAAAgE,CAAQ,CAAE,CAAC,EAAG,CAAE,OAAAD,EAAQ,QAAA/M,CAAQ,CAAC,CACnF,EAEA,GAAI,CACA,GAAM,CAAE,KAAAsH,EAAM,OAAA1F,CAAO,EAAI,MAAMqE,EAAanE,EAAS,CAAE,gBAAiB,IAAK,CAAC,EAG9E,GAFAkH,EAAkBpH,GAAU,KAExB,CAAC0F,EAAM,OAAOwF,EAAK,IAAK,cAAc,EAC1C,GAAIhL,EAAQ,SAAW,OAASA,EAAQ,SAAW,OAAQ,OAAOgL,EAAK,IAAK,oBAAoB,EAEhG,IAAMI,EAAQlH,EAAO,kBACfmH,EAAiBnH,EAAO,eAE9B,GAAI,EAAEkH,GAASC,GAAiB,OAAOL,EAAK,IAAK,sBAAsB,EAEvE,IAAMM,EAAQrE,EAAO,OAAS,GACxBsE,EAAeH,EAAM,OAAOE,CAAK,EAIvC,GAAIC,IAAiB,MAAQ,CAAC,OAAO,cAAcA,CAAY,GAAKA,GAAgB,EAChF,OAAOP,EAAK,IAAK,0BAA0B,EAG/C,IAAMlC,EAAiB,MAAM1E,EAAQ,sBAAsBoB,EAAK,EAAE,EAClE,GAAI,CAACsD,EAAgB,OAAOkC,EAAK,IAAK,WAAW,EAGjD,GAAI,CAAChG,EAA2B8D,EAAgB,KAAK,IAAI,CAAC,EACtD,OAAAhE,EAAiB,MAAM,kBAAkByG,CAAY,WAAWzC,CAAc,oBAAoB,EAC3FkC,EAAK,IAAK,oBAAqB5J,EAA8B,EAKxE,IAAMuE,GAAQ,MAAMT,GAAmBlF,CAAO,EACxCwL,EAAa,MAAMpH,EAAQ,qBAAqBuB,GAAO4F,EAAczC,CAAc,EACzF,GAAI,CAAC0C,EACD,OAAA1G,EAAiB,MAAM,kBAAkByG,CAAY,WAAWzC,CAAc,qBAAqB,EAC5FkC,EAAK,IAAK,sBAAsB,EAI3C,GAAIQ,EAAW,UAAY,MAAQA,EAAW,SAAWzG,EACrD,OAAAD,EAAiB,MAAM,kBAAkByG,CAAY,WAAWzC,CAAc,iBAAiB,EACxFkC,EAAK,IAAK,sBAAsB,EAG3C,GAAI,CAAC/F,EAAe,WAAW,EAC3B,OAAAH,EAAiB,MAAM,kBAAkByG,CAAY,WAAWzC,CAAc,qBAAqB,EAC5FkC,EAAK,IAAK,gCAAiC3J,EAA+B,EAKrF,IAAIoK,EAAe,GACbC,EAAc,IAAM,CAClBD,IACJA,EAAe,GACfxG,EAAe,QAAQ,EAC3B,EAEI0G,EAA0B,GAE9B,GAAI,CACA,IAAMC,EAAS5L,EAAQ,SAAW,OAC5B8J,EAAS,MAAMuB,EAAeG,EAAW,SAAU,CAAE,SAAUzG,EAAoB,KAAM6G,EAAQ,UAAWvH,EAAayE,CAAc,CAAE,CAAC,EAEhJ,GAAI,CAACgB,EAAO,GAAI,CACZ,IAAMmB,EAASnJ,GAA0BgI,EAAO,MAAM,EAGtD,OAAIA,EAAO,SAAW,aACb1F,EAAQ,sBAAsBmH,CAAY,EAAE,MAAO1K,GAAmB,CACvEiE,EAAiB,MAAM,2CAA2CyG,CAAY,YAAY1K,aAAiB,MAAQA,EAAM,QAAU,SAAS,EAAE,CAClJ,CAAC,EAGLiE,EAAiB,MAAM,GAAGmG,CAAM,eAAeM,CAAY,WAAWzC,CAAc,WAAWgB,EAAO,MAAM,SAASA,EAAO,MAAQ,GAAG,EAAE,EAClIkB,EAAKC,EAAQjJ,GAA4BiJ,CAAM,GAAK,gCAAgC,CAC/F,CAIA,IAAMY,EAAWpK,GAAkB+J,EAAW,QAAQ,GAAK/J,GAAkBqI,EAAO,WAAW,GAAK,2BAC9FgC,EAAaxK,GAAwB,IAAIuK,CAAQ,EACjDE,GAAgB,IAAI,IAAI/L,EAAQ,GAAG,EAAE,aAAa,IAAI,UAAU,IAAM,IACtEgM,GAAcF,EAAaD,EAAW,2BACtCI,GAAcH,GAAc,CAACC,GAAgB,SAAW,aAIxDG,GAAgBpC,EAAO,MAAQA,EAAO,MAAM,WAC5C5L,EAAU,IAAI,QAAQ,CACxB,eAAgB8N,GAChB,iBAAkB,OAAOE,EAAa,EACtC,sBAAuB,GAAGD,EAAW,eAAepK,GAAsB2J,EAAW,QAAQ,CAAC,uBAAuB9J,GAAc8J,EAAW,QAAQ,CAAC,GACvJ,gBAAiB,oBACjB,yBAA0B,UAC1B,0BAA2B,8BAC3B,kBAAmB,YACvB,CAAC,EAaD,OAZItE,GAAiBhJ,EAAQ,OAAO,aAAcgJ,CAAe,EAK5D9C,EAAQ,sBAAsBmH,CAAY,EAAE,MAAO1K,GAAmB,CACvEiE,EAAiB,MAAM,2CAA2CyG,CAAY,YAAY1K,aAAiB,MAAQA,EAAM,QAAU,SAAS,EAAE,CAClJ,CAAC,EAEDiE,EAAiB,KAAK,kBAAkByG,CAAY,WAAWzC,CAAc,SAASkD,EAAW,gBAAgBC,EAAW,UAAUC,EAAa,EAAE,EAGjJN,EACO,IAAI,SAAS,KAAM,CAAE,OAAQ,IAAK,QAAA1N,CAAQ,CAAC,GAEtDyN,EAA0B,GACnB,IAAI,SAAS7I,GAAiBgH,EAAO,MAAO4B,EAAaxK,EAAgC,EAAG,CAAE,OAAQ,IAAK,QAAAhD,CAAQ,CAAC,EAC/H,QAAE,CAEOyN,GAAyBD,EAAY,CAC9C,CACJ,OAAS7K,EAAO,CAEZ,GAAIA,aAAiB,SAAU,MAAMA,EACrC,OAAAiE,EAAiB,MAAM,uDAAuDjE,aAAiB,MAAQA,EAAM,QAAU,SAAS,EAAE,EAC3HmK,EAAK,IAAK,2BAA2B,CAChD,CACJ,CAM2C,CAC3C,CACJ,CC9lDA,IAAAmB,GAAsE,2BACtEC,EAAuF,uBAwGvF,IAAMC,GAAQ,MAAe,cAAW,CAAqC,EAoFhEC,GAAkB,CAACC,EAAyCC,IAAwC,CAE7G,GAAI,CAACD,EACD,OAAO,KAIX,IAAME,EAAYF,aAAiB,KAAOA,EAAQ,IAAI,KAAKA,CAAK,EAChE,GAAI,OAAO,MAAME,EAAU,QAAQ,CAAC,EAEhC,OAAO,OAAOF,GAAU,SAAWA,EAAQ,KAK/C,IAAMG,EAAOD,EAAU,YAAY,EAC7BE,EAAOD,EAAO,EAAI,IAAI,OAAO,CAACA,CAAI,EAAE,SAAS,EAAG,GAAG,CAAC,GAAK,OAAOA,CAAI,EAAE,SAAS,EAAG,GAAG,EACrFE,EAAK,OAAOH,EAAU,SAAS,EAAI,CAAC,EAAE,SAAS,EAAG,GAAG,EACrDI,EAAK,OAAOJ,EAAU,QAAQ,CAAC,EAAE,SAAS,EAAG,GAAG,EACtD,GAAID,IAAY,aACZ,MAAO,GAAGG,CAAI,IAAIC,CAAE,IAAIC,CAAE,GAG9B,IAAMC,EAAK,OAAOL,EAAU,SAAS,CAAC,EAAE,SAAS,EAAG,GAAG,EACjDM,EAAK,OAAON,EAAU,WAAW,CAAC,EAAE,SAAS,EAAG,GAAG,EACnDO,EAAK,OAAOP,EAAU,WAAW,CAAC,EAAE,SAAS,EAAG,GAAG,EACzD,MAAO,GAAGE,CAAI,IAAIC,CAAE,IAAIC,CAAE,IAAIC,CAAE,IAAIC,CAAE,IAAIC,CAAE,EAChD,EAEO,SAASC,GAAyBC,EAAkC,CACvE,GAAM,CAAE,GAAAC,EAAI,OAAAC,EAAQ,UAAWC,EAAO,cAAAC,EAAe,aAAAC,EAAc,MAAAC,EAAO,MAAOC,EAAgB,OAAAC,CAAO,EAAIR,EACtG,CAAE,IAAKS,EAAgB,SAAUC,EAAqB,QAASC,EAAyB,MAAOC,EAAkB,SAAUC,EAAsB,WAAYC,CAAsB,EAAIZ,EACvLa,GAAkBf,EAAO,iBAAmB,CAAC,EAC7CgB,GAAkBhB,EAAO,iBAAmBA,EAAO,gBAAgB,OAAS,EAAIA,EAAO,gBAAkB,CAACA,EAAO,gBAAkB,oBAAK,EACxIiB,GAAiBD,GAAgB,CAAC,GAAK,qBACvCE,GAASlB,EAAO,QAAUmB,KAA4B,sBAAsB,EAU5EC,GAAoBC,MAAsD,4BAAyBC,GAAYrB,EAAG,YAAasB,GAAOD,EAAQ,IAAMD,EAAGE,CAAE,CAAC,CAAC,EAAGvB,EAAO,gBAAgB,EAErLwB,GAA6B,mBAC7BC,GAA0BzB,EAAO,UAAY,KAC7C0B,EAA6B,yBAE7BC,EAA0C,8BAC1CC,EAAoC5B,EAAO,mBAAqB,IAGhE6B,EAAiC,2BAEjCC,EAAmC,6BAcnCC,EAA8B,GAAG/B,EAAO,WAAa,yBAAyB,MAC9EgC,EAAiChC,EAAO,cAAgB,IAExDiC,EAAuBC,GAAgB1B,EAAO,eAAe0B,CAAG,EAQhEC,EAA8BC,GAAkCC,GAAgC5B,EAAe,WAAY2B,CAAO,EAQlIE,EAAc,CAACC,EAAgBC,IAA2B,GAAGhB,EAA0B,SAASe,CAAM,OAAOC,CAAM,GAEnHC,GAAiBF,GAA2B,GAAGf,EAA0B,SAASe,CAAM,IAExFG,EAAe,CAACC,EAAgCJ,EAAgBC,IAA2B,qBAAqBG,CAAsB,SAASJ,CAAM,OAAOC,CAAM,GAElKI,EAAiB,CAACD,EAAgCJ,IAA2B,qBAAqBI,CAAsB,SAASJ,CAAM,IAEvIM,EAAiB,CAACC,EAAqBP,EAAgBC,IAA2B,uBAAuBM,CAAW,SAASP,CAAM,OAAOC,CAAM,GAEhJO,EAAmB,CAACD,EAAqBP,IAA2B,uBAAuBO,CAAW,SAASP,CAAM,IAMrHS,EAAqB,MAAOC,EAAkCC,IAAsC,CACtG,IAAMC,EAAS,MAAM7C,GAAO,UAAU,EACtC,GAAI,CAAC6C,EAAQ,CACTjC,GAAO,KAAK,mCAAmCgC,CAAU,GAAG,EAC5D,MACJ,CACA,IAAME,EAAiB,KAAK,IAAI,EAChC,GAAI,CACA,MAAMD,EAAO,KAAKpB,EAA6B,IAAK,CAAE,KAAM,KAAK,UAAUkB,CAAO,CAAE,EAAG,CAAE,KAAM,CAAE,SAAU,SAAU,iBAAkB,IAAK,UAAWjB,CAA+B,CAAE,CAAC,EACzL,IAAMqB,EAAoB,KAAK,IAAI,EAAID,EACnCC,EAAoB,KACpBnC,GAAO,KAAK,uBAAuBgC,CAAU,MAAMG,CAAiB,IAAI,CAEhF,OAASC,EAAG,CACRpC,GAAO,MAAM,+BAA+BgC,CAAU,KAAMI,CAAC,CACjE,CACJ,EAUMC,EAAkBlE,GAChBA,GAAS,MAAQA,IAAU,GAAW,KACnC,QAAQ,KAAKA,CAAK,EAAIgB,EAAa,OAAOhB,CAAK,CAAC,EAAIA,EAuCzDmE,EAAqB,IAA+B,CACtD,IAAMC,EAA+B,CAAC,EACtC,QAAWC,KAAW3C,GAClB0C,EAAI,OAAOC,EAAQ,UAAU,EAAE,EAAIvE,GAAKuE,EAAQ,KAAK,EAEzD,OAAOD,CACX,EAGME,EAAyBC,GAA0C,CACrE,IAAIC,EAAID,EACR,QAAWF,KAAW3C,GAClB8C,EAAIA,EAAE,SAASH,EAAQ,SAAO,UAAI,MAAGjD,EAAe,WAAYiD,EAAQ,UAAU,KAAG,MAAGjD,EAAe,YAAaiD,EAAQ,QAAQ,CAAC,CAAC,EAE1I,OAAOG,CACX,EAoBMC,GAAe,CAACC,EAA+BC,EAAgBC,IAA8C,CAC/G,GAAM,CAAE,IAAAC,EAAK,SAAAC,CAAS,EAAIH,EAEtBI,EAAUF,EAAI,QACdG,EAAyC,CAAC,EAC1CC,EAA0B,KAC1BC,EAAkC,KAClCC,EAA+B,KAC/BC,EAA6B,KAC7BC,EAA8B,KAC9BC,GAA8B,KAC9BC,GAAiD,CAAC,EAGhDlB,GAAU3C,GAAgB,KAAM8D,GAAMA,EAAE,aAAeX,EAAI,UAAU,EACrEY,GAAcpB,GAAWM,EAAI,OAAON,GAAQ,UAAU,EAAE,EAAmD,OACjH,GAAIA,IAAWoB,GAAa,CACxB,IAAMC,EAASrB,GAAQ,OAAOoB,EAAW,EACrCC,EAAO,UAAY,SAAWX,EAAUW,EAAO,SAC/CA,EAAO,eAAiB,SAAWJ,GAAeI,EAAO,cACzDA,EAAO,WAAa,SAAWT,EAAWS,EAAO,UACjDA,EAAO,mBAAqB,SAAWR,EAAmBQ,EAAO,kBACjEA,EAAO,gBAAkB,SAAWP,EAAgBO,EAAO,eAC3DA,EAAO,cAAgB,SAAWN,EAAcM,EAAO,aACvDA,EAAO,eAAiB,SAAWL,EAAeK,EAAO,cACzDA,EAAO,eAAiB,SAAWV,EAAeU,EAAO,cACzDA,EAAO,mBAAqB,SAAWH,GAAmBG,EAAO,iBACzE,MAAWb,EAAI,aAAe,YAAcC,IACxCC,EAAUD,EAAS,MAIvB,IAAMa,IAAahB,EAAI,QAAU,CAAC,GAAG,IAAKiB,IAAO,CAC7C,GAAIA,EAAE,GACN,KAAMA,EAAE,KACR,MAAOA,EAAE,KACb,EAAE,EACIC,GAAoBlB,EAAI,mBAAqB,CAAC,EAM9CmB,EAAkBC,GAAoBrB,EAAM,YAAaG,EAAI,UAAU,EAGvEmB,GAA0Bd,EAAmBvD,GAAgB,SAASuD,CAAgB,EAAI,GAE1Fe,GAA4BC,KAAsB,CAAC,GAAK,KAC9D,OAAIF,IAA2BC,IAA6B,CAACN,GAAU,KAAMC,GAAMA,EAAE,KAAOK,EAAyB,GACjHN,GAAU,KAAK,CACX,GAAIM,GACJ,KAAMrE,GACN,MAAO,IACX,CAAC,EAGE,CACH,YAAaiD,EAAI,GACjB,KAAM9E,GAAgB8E,EAAI,aAAc,YAAY,EACpD,OAAQF,EAAI,aAAeT,EAAeW,EAAI,SAAS,GAAK,GAC5D,OAAQA,EAAI,OAAS7D,EAAa6D,EAAI,MAAM,EAAI,GAChD,WAAYA,EAAI,YAAc,WAC9B,UAAW9E,GAAgB8E,EAAI,UAAW,qBAAqB,EAC/D,UAAW9E,GAAgB8E,EAAI,UAAW,qBAAqB,EAC/D,UAAWX,EAAeW,EAAI,SAAS,EACvC,aAAcS,IAAgBX,EAAI,aAAeT,EAAeW,EAAI,SAAS,EAC7E,SAAAI,EACA,iBAAAC,EACA,cAAAC,EACA,YAAAC,EACA,aAAAC,EACA,aAAAL,EACA,QAASH,EAAI,MACb,QAAAE,EACA,iBAAkBe,EAAkBP,GAAmB,CAAC,EACxD,OAAQZ,EAAI,QAAU,GACtB,UAAWA,EAAI,WAAa,GAC5B,OAAQgB,GACR,eAAgBE,GACX,OAAQrB,GAAMsB,GAAoBlB,IAAkB,QAAaJ,EAAE,SAAWI,CAAc,EAC5F,IAAKJ,IAAO,CACT,GAAGA,EACH,OAAQxD,EAAawD,EAAE,MAAM,EAC7B,OAAQI,EAAgBJ,EAAE,SAAWI,EAAgB,EACzD,EAAE,EAGN,YAAaD,EAAI,WACrB,CACJ,EAmBMwB,GAA8B,MAAOzB,EAA+BxB,IAA+C,CACrH,IAAMkD,KAAqB,gBAAa5E,EAAuE,aAAa,EACtH6E,EAAoB,MAAMC,GAAqB,EAarD,OAXe,MAAM1F,EAChB,OAAwF,CACrF,YAAaQ,EAAe,GAC5B,aAAcA,EAAe,aAC7B,WAAYA,EAAe,UAC/B,CAAC,EACA,KAAKA,CAAc,EACnB,SAASgF,KAAoB,UAAI,MAAGA,EAAmB,MAAOhF,EAAe,EAAE,KAAG,WAAQgF,EAAmB,QAASC,CAAiB,CAAC,CAAC,EACzI,SAAM,UAAI,UAAOjF,EAAe,SAAS,KAAG,SAAG,MAAGA,EAAe,OAAQ8B,CAAM,KAAG,UAAO9B,EAAe,MAAM,KAAG,UAAOgF,EAAmB,KAAK,CAAC,EAAGtD,EAA2B4B,EAAM,OAAO,CAAC,CAAC,EAC9L,WAAQ,QAAKtD,EAAe,YAAY,KAAG,QAAKA,EAAe,EAAE,CAAC,GAEzD,IAAKmF,IAAU,CACzB,GAAGA,EACH,aAAcxG,GAAgBwG,EAAK,aAAc,YAAY,CACjE,EAAE,CACN,EAgBMC,GAAyC,MAAO9B,EAA+B+B,EAAoBC,IAA8C,CACnJ,IAAMxD,EAAS,MAAMyD,EAAsBF,CAAU,EACrD,GAAI,CAACvD,EACD,MAAO,CAAC,EAGZ,GAAI0D,GAAsBlC,EAAM,OAAO,EACnC,MAAO,CAAC,EAEZ,IAAM0B,KAAqB,gBAAa5E,EAAuE,aAAa,EACtH6E,EAAoB,MAAMC,GAAqB,EAcrD,OAZe,MAAM1F,EAChB,OAAwF,CACrF,YAAaQ,EAAe,GAC5B,aAAcA,EAAe,aAC7B,WAAYA,EAAe,UAC/B,CAAC,EACA,IAAIsF,CAAK,EACT,KAAKtF,CAAc,EACnB,SAASgF,KAAoB,UAAI,MAAGA,EAAmB,MAAOhF,EAAe,EAAE,KAAG,WAAQgF,EAAmB,QAASC,CAAiB,CAAC,CAAC,EACzI,SAAM,UAAI,UAAOjF,EAAe,SAAS,KAAG,SAAG,MAAGA,EAAe,OAAQ8B,CAAM,KAAG,UAAO9B,EAAe,MAAM,KAAG,UAAOgF,EAAmB,KAAK,CAAC,EAAGtD,EAA2B4B,EAAM,OAAO,CAAC,CAAC,EAC9L,WAAQ,QAAKtD,EAAe,YAAY,KAAG,QAAKA,EAAe,EAAE,CAAC,GAEzD,IAAKmF,IAAU,CACzB,GAAGA,EACH,aAAcxG,GAAgBwG,EAAK,aAAc,YAAY,CACjE,EAAE,CACN,EAMMM,GAAkC,MAAOnC,EAA+BpB,IAAiE,CAC3I,IAAMwD,EAAO,MAAMxC,EACf1D,EACK,OAAoB,CACjB,IAAKd,GAAKsB,CAAc,EACxB,SAAUtB,GAAKuB,CAAmB,EAClC,YAAaP,EAAM,YACnB,GAAGqD,EAAmB,CAC1B,CAAC,EACA,KAAK/C,CAAc,CAC5B,EACK,SAASC,KAAqB,MAAGD,EAAe,GAAIC,EAAoB,KAAK,CAAC,EAC9E,SAASP,KAAO,MAAGM,EAAe,OAAQN,EAAM,EAAE,CAAC,EACnD,SAAM,UAAI,MAAGM,EAAe,aAAc,QAAMkC,CAAsB,EAAE,KAAG,UAAOlC,EAAe,SAAS,EAAG0B,EAA2B4B,EAAM,OAAO,CAAC,CAAC,EACvJ,WAAQ,QAAKtD,EAAe,EAAE,CAAC,EAE9B2F,EAAiB,MAAMC,GAAuBF,EAAK,IAAKG,GAAMA,EAAE,IAAI,EAAE,CAAC,EAC7E,OAAOH,EAAK,IAAKnC,GAAQF,GAAaC,EAAO,CAAE,GAAGC,EAAK,YAAaoC,EAAe,IAAIpC,EAAI,IAAI,EAAE,GAAK,CAAC,CAAE,CAAC,CAAC,CAC/G,EAMMuC,EAA6B5D,GACxB,GAAGhB,CAAuC,GAAGgB,CAAsB,GAOxE6D,EAAc,IAAI,IAElBR,EAAwB,MAAOF,GAA+C,CAChF,GAAI,CAAC9F,EAAO,oBAAsBwG,EAAY,IAAIV,CAAU,EACxD,OAAOU,EAAY,IAAIV,CAAU,GAAK,KAG1C,IAAMvD,EAAS,MAAMnC,EAAc0F,CAAU,EAC7C,OAAIvD,IAAW,MAAQ,CAACvC,EAAO,oBAC3BwG,EAAY,IAAIV,EAAYvD,CAAM,EAE/BA,CACX,EAWMkE,GAAgC,MAAO1C,EAA+B+B,EAAoB,CAAE,aAAAY,EAAe,GAAO,SAAAC,EAAW,GAAO,cAAAC,EAAe,aAAAC,CAAa,EAAgE,CAAC,IAAkC,CACrQ,IAAMtE,EAAS,MAAMyD,EAAsBF,CAAU,EACrD,GAAI,CAACvD,EACD,MAAO,CAAC,EAGZ,GAAI0D,GAAsBlC,EAAM,OAAO,EACnC,MAAO,CAAC,EAIZ,IAAM+C,EAAWxE,EAAYC,EAAQwE,GAAyBhD,CAAK,CAAC,EACpE,OAAOxD,EAAe,WAA4B,CAC9C,SAAAuG,EACA,QAAS,SAAY,CACjB,IAAME,EAAM,MAAMxB,GAA4BzB,EAAOxB,CAAM,EAU3D,OAAK0E,GAAmBD,CAAG,EAIpBA,GAHH9F,GAAO,KAAK,6JAA6J,EAClK,CAAC,GAAG8F,CAAG,EAAE,KAAKE,EAAqB,EAGlD,EAIA,aAAAL,EACA,aAAAH,EACA,SAAAC,EACA,cAAeC,GAAiBnF,GAChC,SAAUC,EAGV,kBAAmB,EACvB,CAAC,CACL,EAYMyF,GAAgC,CAACpD,EAA+BqD,EAAsB,CAAE,aAAAV,EAAe,GAAO,SAAAC,EAAW,GAAO,cAAAC,CAAc,EAAgC,CAAC,IAAoC,CACrN,IAAMjE,EAAyB0E,GAAyBD,CAAY,EACpE,OAAKzE,EAIDsD,GAAsBlC,EAAM,OAAO,EAC5B,QAAQ,QAAQ,CAAC,CAAC,EAGtBxD,EAAe,WAA8B,CAEhD,SAAU,GAAGgG,EAA0B5D,CAAsB,CAAC,OAAOoE,GAAyBhD,CAAK,CAAC,GACpG,QAAS,IAAMmC,GAAgCnC,EAAOpB,CAAsB,EAC5E,aAAA+D,EACA,SAAAC,EACA,cAAeC,GAAiBhF,CACpC,CAAC,EAdU,QAAQ,QAAQ,CAAC,CAAC,CAejC,EAcI2D,GAAuC,KACrC+B,GAAmB,UACjB/B,KAAwB,OAE5BA,IADa,MAAMtF,EAAG,OAAuB,CAAE,GAAIW,EAAiB,EAAG,CAAC,EAAE,KAAKA,CAAgB,EAAE,SAAM,WAAQA,EAAiB,KAAMI,EAAe,CAAC,EAAE,QAAQJ,EAAiB,EAAE,GACxJ,IAAKoD,GAAQA,EAAI,EAAE,GACvCuB,IAYLI,GAAuB,SAA+B,CACxD,IAAM4B,EAAM,MAAMD,GAAiB,EACnC,OAAOC,EAAI,OAAS,EAAIA,EAAM,CAAC,EAAE,CACrC,EAWMC,GAAkB,SAAoC,CACxD,IAAMD,EAAM,MAAMD,GAAiB,EACnC,GAAIC,EAAI,SAAW,EAAG,OAAO,KAE7B,GAAM,CAACE,CAAO,EAAI,MAAMxH,EAAG,OAAuB,CAAE,GAAIW,EAAiB,EAAG,CAAC,EAAE,IAAI,CAAC,EAAE,KAAKA,CAAgB,EAAE,SAAM,MAAGA,EAAiB,KAAMK,EAAc,CAAC,EAC5J,OAAOwG,GAAS,IAAMF,EAAI,CAAC,CAC/B,EASMG,GAAwB,CAACC,EAAiCC,IACxDA,GAAa,KAAa,SAC1BD,GAAa,KAAa,UACvB,UAaLtB,GAAyB,MAAOwB,GAA2E,CAC7G,IAAMC,EAAM,IAAI,IACVC,EAAa7H,EAAO,WAGpB8H,EAAQhI,EAAO,kBACrB,GAAI6H,EAAO,SAAW,GAAK,CAACE,GAAc,CAACC,EACvC,OAAOF,EAGX,IAAM3B,EAAO,MAAMlG,EACd,OAAyN,CACtN,GAAI8H,EAAW,GACf,MAAOA,EAAW,MAClB,SAAUA,EAAW,SACrB,SAAUA,EAAW,SACrB,SAAUA,EAAW,SACrB,UAAWA,EAAW,UACtB,gBAAiBA,EAAW,gBAC5B,gBAAiBA,EAAW,eAChC,CAAC,EACA,KAAKA,CAAU,EACf,SAAM,WAAQA,EAAW,MAAOF,CAAM,CAAC,EACvC,WAAQ,OAAIE,EAAW,SAAS,KAAG,OAAIA,EAAW,EAAE,CAAC,EAE1D,QAAW/D,KAAOmC,EAAM,CAGpB,IAAMP,EAAqC,CACvC,GAAIoC,EAAM,OAAO,OAAOhE,EAAI,EAAE,CAAC,EAC/B,SAAU,OAAOA,EAAI,QAAQ,EAC7B,SAAUA,EAAI,SAAW,OAAOA,EAAI,QAAQ,EAAI,KAChD,SAAUA,EAAI,UAAY,KAAO,OAAOA,EAAI,QAAQ,EAAI,KACxD,UAAW5E,GAAgB4E,EAAI,UAAW,qBAAqB,EAC/D,MAAO0D,GAAsB1D,EAAI,gBAAiBA,EAAI,eAAe,CACzE,EACMiE,EAAOH,EAAI,IAAI,OAAO9D,EAAI,KAAK,CAAC,EAClCiE,EACAA,EAAK,KAAKrC,CAAI,EAEdkC,EAAI,IAAI,OAAO9D,EAAI,KAAK,EAAG,CAAC4B,CAAI,CAAC,CAEzC,CAEA,OAAOkC,CACX,EAkBMI,GAAuB,MAAOnE,EAA+BoE,EAAsB5F,IAA6H,CAClN,IAAMwF,EAAa7H,EAAO,WAK1B,GAJI,CAAC6H,GAID9B,GAAsBlC,EAAM,OAAO,EACnC,OAAO,KAEX,IAAM2B,EAAoB,MAAMC,GAAqB,EAC/CF,KAAqB,gBAAa5E,EAAuE,aAAa,EAmB5H,OAba,MAAMZ,EACd,OAAiG,CAC9F,SAAU8H,EAAW,SACrB,SAAUA,EAAW,SACrB,SAAUA,EAAW,SACrB,SAAUA,EAAW,QACzB,CAAC,EACA,IAAI,CAAC,EACL,KAAKA,CAAU,EACf,UAAUtH,KAAgB,MAAGA,EAAe,GAAIsH,EAAW,KAAK,CAAC,EACjE,SAAStC,KAAoB,UAAI,MAAGA,EAAmB,MAAOhF,EAAe,EAAE,KAAG,WAAQgF,EAAmB,QAASC,CAAiB,CAAC,CAAC,EACzI,SAAM,UAAI,MAAGqC,EAAW,GAAII,CAAY,KAAG,UAAO1H,EAAe,SAAS,KAAG,SAAG,MAAGA,EAAe,OAAQ8B,CAAM,KAAG,UAAOkD,EAAmB,KAAK,CAAC,EAAGtD,EAA2B4B,EAAM,OAAO,CAAC,CAAC,GAEzL,CAAC,GAAK,IACtB,EAWMqE,GAAwB,MAAOD,GAAwC,CACzE,IAAMJ,EAAa7H,EAAO,WAC1B,GAAI,CAAC6H,EAAY,OACjB,IAAMM,EAAM,IAAI,KAChB,GAAI,CACA,MAAMpI,EAAG,OAAO8H,CAAU,EAAE,IAAI,CAAE,gBAAiBM,EAAK,gBAAiBA,EAAK,UAAWA,CAAI,CAAC,EAAE,SAAM,MAAGN,EAAW,GAAII,CAAY,CAAC,CACzI,OAASG,EAAO,CACZpH,GAAO,MAAM,+BAAgCoH,CAAK,CACtD,CACJ,EAgBMC,GAAwB,MAAOJ,GAAwC,CACzE,IAAMJ,EAAa7H,EAAO,WAC1B,GAAI,CAAC6H,EAAY,OACjB,IAAMM,EAAM,IAAI,KAChB,GAAI,CACA,MAAMpI,EAAG,OAAO8H,CAAU,EAAE,IAAI,CAAE,gBAAiBM,EAAK,gBAAiB,KAAM,UAAWA,CAAI,CAAC,EAAE,SAAM,MAAGN,EAAW,GAAII,CAAY,CAAC,CAC1I,OAASG,EAAO,CACZpH,GAAO,MAAM,+BAAgCoH,CAAK,CACtD,CACJ,EAMME,GAA4C,MAAOzE,EAA+BqD,EAAsBtB,EAAoB,CAAE,aAAAY,EAAe,GAAO,SAAAC,EAAW,GAAO,cAAAC,CAAc,EAAgC,CAAC,IAAoC,CAC3P,IAAMrE,EAAS,MAAMyD,EAAsBF,CAAU,EACrD,GAAI,CAACvD,EACD,MAAO,CAAC,EAGZ,IAAMI,EAAyB0E,GAAyBD,CAAY,EACpE,GAAI,CAACzE,EACD,MAAO,CAAC,EAGZ,GAAIsD,GAAsBlC,EAAM,OAAO,EACnC,MAAO,CAAC,EAIZ,IAAM+C,EAAWpE,EAAaC,EAAwBJ,EAAQwE,GAAyBhD,CAAK,CAAC,EAE7F,OAAOxD,EAAe,WAA8B,CAChD,SAAAuG,EACA,QAAS,SAAY,CACjB,IAAMpB,EAAoB,MAAMC,GAAqB,EAC/CF,KAAqB,gBAAa5E,EAAuE,aAAa,EAEtHsF,EAAO,MAAMxC,EACf1D,EACK,OAAoB,CACjB,IAAKd,GAAKsB,CAAc,EACxB,SAAUtB,GAAKuB,CAAmB,EAClC,OAAQI,EAAsB,OAC9B,UAAWA,EAAsB,UACjC,YAAaX,EAAM,YACnB,GAAGqD,EAAmB,CAC1B,CAAC,EACA,KAAK/C,CAAc,CAC5B,EACK,SAASC,KAAqB,MAAGD,EAAe,GAAIC,EAAoB,KAAK,CAAC,EAC9E,SAASI,KAAuB,UAAI,MAAGL,EAAe,GAAIK,EAAsB,KAAK,KAAG,MAAGA,EAAsB,OAAQyB,CAAM,CAAC,CAAC,EACjI,SAASkD,KAAoB,UAAI,MAAGA,EAAmB,MAAOhF,EAAe,EAAE,KAAG,WAAQgF,EAAmB,QAASC,CAAiB,CAAC,CAAC,EACzI,SAASvF,KAAO,MAAGM,EAAe,OAAQN,EAAM,EAAE,CAAC,EACnD,SAAM,UAAI,MAAGM,EAAe,aAAc,QAAMkC,CAAsB,EAAE,KAAG,UAAOlC,EAAe,SAAS,KAAG,SAAG,MAAGA,EAAe,OAAQ8B,CAAM,KAAG,UAAOkD,EAAmB,KAAK,CAAC,EAAGtD,EAA2B4B,EAAM,OAAO,CAAC,CAAC,EAChO,WAAQ,QAAKtD,EAAe,EAAE,CAAC,EAEpC,GAAI0F,EAAK,SAAW,EAChB,MAAO,CAAC,EAGZ,IAAM0B,GAAS1B,EAAK,IAAKG,GAAMA,EAAE,IAAI,EAAE,EAGjCmC,GAAY,MAAMxI,EACnB,OAA0E,CACvE,MAAOY,EAAqB,MAC5B,GAAID,EAAiB,GACrB,KAAMA,EAAiB,KACvB,MAAOA,EAAiB,KAC5B,CAAC,EACA,KAAKC,CAAoB,EACzB,UAAUD,KAAkB,MAAGC,EAAqB,QAASD,EAAiB,EAAE,CAAC,EACjF,SAAM,WAAQC,EAAqB,MAAOgH,EAAM,CAAC,EAEhDa,GAAc,MAAMzI,EACrB,OAAuH,CACpH,MAAOU,EAAwB,MAC/B,GAAIA,EAAwB,GAC5B,KAAMA,EAAwB,KAC9B,UAAWA,EAAwB,UACnC,OAAQA,EAAwB,OAChC,SAAUR,EAAM,WACpB,CAAC,EACA,KAAKQ,CAAuB,EAC5B,SAASR,KAAO,MAAGQ,EAAwB,OAAQR,EAAM,EAAE,CAAC,EAC5D,SAAM,WAAQQ,EAAwB,MAAOkH,EAAM,CAAC,EACpD,WAAQ,OAAIlH,EAAwB,SAAS,CAAC,EAG7CyF,GAAiB,MAAMC,GAAuBwB,EAAM,EAEpDc,GAAY,IAAI,IAChBC,GAAc,IAAI,IAExB,QAAWC,KAASJ,GAAW,CAC3B,IAAIR,GAAOU,GAAU,IAAIE,EAAM,KAAK,EAC/BZ,KACDA,GAAO,CAAC,EACRU,GAAU,IAAIE,EAAM,MAAOZ,EAAI,GAEnCA,GAAK,KAAK,CACN,GAAIY,EAAM,GACV,KAAMA,EAAM,KACZ,MAAOA,EAAM,KACjB,CAAC,CACL,CAEA,QAAWC,KAAWJ,GAAa,CAC/B,IAAIT,GAAOW,GAAY,IAAIE,EAAQ,KAAK,EACnCb,KACDA,GAAO,CAAC,EACRW,GAAY,IAAIE,EAAQ,MAAOb,EAAI,GAEvCA,GAAK,KAAK,CACN,GAAIa,EAAQ,GACZ,QAASA,EAAQ,KACjB,UAAW1J,GAAgB0J,EAAQ,UAAW,qBAAqB,GAAK,GACxE,OAAQA,EAAQ,OAChB,SAAUA,EAAQ,UAAY,EAClC,CAAC,CACL,CAEA,OAAO3C,EAAK,IAAKnC,GAAQ,CACrB,IAAM+E,GAAQ/E,EAAI,IAAI,GACtB,OAAOF,GACHC,EACA,CACI,GAAGC,EACH,YAAaoC,GAAe,IAAI2C,EAAK,GAAK,CAAC,EAC3C,OAAQJ,GAAU,IAAII,EAAK,GAAK,CAAC,EACjC,kBAAmBH,GAAY,IAAIG,EAAK,GAAK,CAAC,CAClD,EACAxG,CACJ,CACJ,CAAC,CACL,EACA,aAAAmE,EACA,SAAAC,EACA,cAAeC,GAAiBhF,EAChC,SAAU,GAAGC,CAA8B,GAAGc,CAAsB,EACxE,CAAC,CACL,EAmBMqG,GAA2B,MAAOjF,EAA+BjB,EAAqBP,EAAgB,CAAE,aAAAmE,EAAe,GAAO,SAAAC,EAAW,GAAO,cAAAC,CAAc,EAAgC,CAAC,IAAyC,CAE1O,GAAIX,GAAsBlC,EAAM,OAAO,EACnC,OAAO,KAIX,IAAM+C,EAAWjE,EAAeC,EAAaP,EAAQwE,GAAyBhD,CAAK,CAAC,EA2FpF,OAzFiB,MAAMxD,EAAe,WAA8B,CAChE,SAAAuG,EACA,QAAS,SAAY,CACjB,IAAMpB,EAAoB,MAAMC,GAAqB,EAC/CF,KAAqB,gBAAa5E,EAAuE,aAAa,EAEtHsF,EAAO,MAAMxC,EACf1D,EACK,OAAoB,CACjB,IAAKd,GAAKsB,CAAc,EACxB,SAAUtB,GAAKuB,CAAmB,EAClC,OAAQI,EAAsB,OAC9B,UAAWA,EAAsB,UACjC,YAAaX,EAAM,YACnB,GAAGqD,EAAmB,CAC1B,CAAC,EACA,KAAK/C,CAAc,CAC5B,EACK,SAASC,KAAqB,MAAGD,EAAe,GAAIC,EAAoB,KAAK,CAAC,EAC9E,SAASI,KAAuB,UAAI,MAAGL,EAAe,GAAIK,EAAsB,KAAK,KAAG,MAAGA,EAAsB,OAAQyB,CAAM,CAAC,CAAC,EACjI,SAASkD,KAAoB,UAAI,MAAGA,EAAmB,MAAOhF,EAAe,EAAE,KAAG,WAAQgF,EAAmB,QAASC,CAAiB,CAAC,CAAC,EACzI,SAASvF,KAAO,MAAGM,EAAe,OAAQN,EAAM,EAAE,CAAC,EACnD,SAAM,UAAI,MAAGM,EAAe,GAAIqC,CAAW,KAAG,UAAOrC,EAAe,SAAS,KAAG,SAAG,MAAGA,EAAe,OAAQ8B,CAAM,KAAG,UAAOkD,EAAmB,KAAK,CAAC,EAAGtD,EAA2B4B,EAAM,OAAO,CAAC,CAAC,EAExM,GAAIoC,EAAK,SAAW,EAChB,MAAO,CAAC,EAGZ,IAAM4C,EAAQ5C,EAAK,CAAC,EAAE,IAAI,GAGpB8C,GAAS,MAAMhJ,EAChB,OAA2D,CACxD,GAAIW,EAAiB,GACrB,KAAMA,EAAiB,KACvB,MAAOA,EAAiB,KAC5B,CAAC,EACA,KAAKC,CAAoB,EACzB,UAAUD,KAAkB,MAAGC,EAAqB,QAASD,EAAiB,EAAE,CAAC,EACjF,SAAM,MAAGC,EAAqB,MAAOkI,CAAK,CAAC,EAE1CG,GAAW,MAAMjJ,EAClB,OAAwG,CACrG,GAAIU,EAAwB,GAC5B,KAAMA,EAAwB,KAC9B,UAAWA,EAAwB,UACnC,OAAQA,EAAwB,OAChC,SAAUR,EAAM,WACpB,CAAC,EACA,KAAKQ,CAAuB,EAC5B,SAASR,KAAO,MAAGQ,EAAwB,OAAQR,EAAM,EAAE,CAAC,EAC5D,SAAM,MAAGQ,EAAwB,MAAOoI,CAAK,CAAC,EAC9C,WAAQ,OAAIpI,EAAwB,SAAS,CAAC,EAE7CyF,GAAiB,MAAMC,GAAuB,CAAC0C,CAAK,CAAC,EAErDI,GAAeF,GAAO,IAAKhE,KAAO,CACpC,GAAIA,GAAE,GACN,KAAMA,GAAE,KACR,MAAOA,GAAE,KACb,EAAE,EAEImE,GAAiBF,GAAS,IAAKrF,KAAO,CACxC,GAAIA,GAAE,GACN,QAASA,GAAE,KACX,UAAWzE,GAAgByE,GAAE,UAAW,qBAAqB,GAAK,GAClE,OAAQA,GAAE,OACV,SAAUA,GAAE,UAAY,EAC5B,EAAE,EAEF,MAAO,CACHC,GACIC,EACA,CACI,GAAGoC,EAAK,CAAC,EACT,YAAaC,GAAe,IAAI2C,CAAK,GAAK,CAAC,EAC3C,OAAQI,GACR,kBAAmBC,EACvB,EACA7G,CACJ,CACJ,CACJ,EACA,aAAAmE,EACA,SAAAC,EACA,cAAeC,GAAiBhF,EAChC,SAAU,GAAGE,CAAgC,GAAGgB,CAAW,EAC/D,CAAC,GAEc,CAAC,GAAK,IACzB,EAYMuG,GAAuC,MAAOtF,EAA+BjB,EAAqBgD,EAAoBwD,EAAsC,CAAC,IAAyC,CACxM,IAAM/G,EAAS,MAAMyD,EAAsBF,CAAU,EACrD,OAAKvD,EAKEyG,GAAyBjF,EAAOjB,EAAaP,EAAQ+G,CAAO,EAJxD,IAKf,EASMC,GAAqB,MAAOzG,EAAqBP,IAA4F,CAC/I,IAAMmD,EAAoB,MAAMC,GAAqB,EAC/CF,KAAqB,gBAAa5E,EAAuE,aAAa,EAItH,CAACqD,CAAG,EAAI,MAAMjE,EACf,OAAgE,CAAE,WAAYQ,EAAe,WAAY,UAAWA,EAAe,SAAU,CAAC,EAC9I,IAAI,CAAC,EACL,KAAKA,CAAc,EACnB,SAASgF,KAAoB,UAAI,MAAGA,EAAmB,MAAOhF,EAAe,EAAE,KAAG,WAAQgF,EAAmB,QAASC,CAAiB,CAAC,CAAC,EACzI,SAAM,UAAI,MAAGjF,EAAe,GAAIqC,CAAW,KAAG,SAAG,MAAGrC,EAAe,OAAQ8B,CAAM,KAAG,UAAOkD,EAAmB,KAAK,CAAC,CAAC,CAAC,EAC3H,OAAOvB,GAAO,IAClB,EAaMsF,GAAmB,MAAOzF,EAA+BjB,EAAqBP,IAAkC,CAClH,IAAM2B,EAAM,MAAMqF,GAAmBzG,EAAaP,CAAM,EACxD,GAAI,CAAC2B,GAAOA,EAAI,WAAa,CAACkB,GAAoBrB,EAAM,QAASG,EAAI,UAAU,EAC3E,MAAM,IAAI,MAAM,WAAW,CAEnC,EAcMuF,GAAgB,MAAO1F,EAA+BxB,EAAgBO,EAAqBsE,EAA6BsC,EAAoBC,IAA4D,CAE1M,MAAMH,GAAiBzF,EAAOjB,EAAaP,CAAM,EAEjD,IAAMqH,EAAW,MAAM3J,EAClB,OAAiC,EACjC,IAAI,CAAC,EACL,KAAKa,CAAqB,EAC1B,SAAM,UAAI,MAAGA,EAAsB,MAAOgC,CAAW,KAAG,MAAGhC,EAAsB,OAAQyB,CAAM,CAAC,CAAC,EAElGsH,EAkCJ,GAhCID,EAAS,OAAS,EACdA,EAAS,CAAC,EAAE,YAAcF,EAU1BG,GATa,MAAM5J,EACd,OAAiCa,CAAqB,EACtD,IAAI,CACD,UAAW4I,EACX,UAAW,IAAI,KACf,UAAW,OAAOnH,CAAM,CAC5B,CAAC,EACA,OAAO,EACP,SAAM,UAAI,MAAGzB,EAAsB,MAAOgC,CAAW,KAAG,MAAGhC,EAAsB,OAAQyB,CAAM,CAAC,CAAC,GACjF,CAAC,EAEtBsH,EAAgBD,EAAS,CAAC,EAgB9BC,GAba,MAAM5J,EACd,OAAiCa,CAAqB,EACtD,OAAO,EACP,OAAO,CACJ,MAAOgC,EACP,OAAAP,EACA,UAAWmH,EACX,OAAQ,GACR,UAAW,IAAI,KACf,UAAW,OAAOnH,CAAM,EACxB,UAAW,IAAI,KACf,UAAW,OAAOA,CAAM,CAC5B,CAAC,GACgB,CAAC,EAGtB6E,EAAc,CACd,IAAM0C,EAAiBzC,GAAyBD,CAAY,EACxD0C,IAEAvJ,EAAe,iBAAiBqC,EAAekH,EAAgBvH,CAAM,CAAC,EACtE,MAAMN,EAAoB,GAAGJ,CAA8B,GAAGiI,CAAc,EAAE,EAEtF,CACA,OAAAvJ,EAAe,WAAW,uBAAuBuC,CAAW,EAAE,EAE9DvC,EAAe,iBAAiBwC,EAAiBD,EAAaP,CAAM,CAAC,EACrE,MAAMN,EAAoB,GAAGH,CAAgC,GAAGgB,CAAW,EAAE,EAE7E,MAAME,EACF+G,GAA0B,MAAM,CAC5B,KAAM,gBACN,YAAajH,EACb,mBAAoBP,EACpB,WAAY,OACZ,MAAOmH,EACP,aAAAC,CACJ,CAAC,EACD,eACJ,EAEOE,CACX,EAcMG,GAAgB,MAAOjG,EAA+BxB,EAAgBO,EAAqBsE,EAA6B6C,EAAiBN,IAA4D,CAEvM,MAAMH,GAAiBzF,EAAOjB,EAAaP,CAAM,EAEjD,IAAMqH,EAAW,MAAM3J,EAClB,OAAiC,EACjC,IAAI,CAAC,EACL,KAAKa,CAAqB,EAC1B,SAAM,UAAI,MAAGA,EAAsB,MAAOgC,CAAW,KAAG,MAAGhC,EAAsB,OAAQyB,CAAM,CAAC,CAAC,EAElGsH,EAkCJ,GAhCID,EAAS,OAAS,EACdA,EAAS,CAAC,EAAE,SAAWK,EAUvBJ,GATa,MAAM5J,EACd,OAAiCa,CAAqB,EACtD,IAAI,CACD,OAAQmJ,EACR,UAAW,IAAI,KACf,UAAW,OAAO1H,CAAM,CAC5B,CAAC,EACA,OAAO,EACP,SAAM,UAAI,MAAGzB,EAAsB,MAAOgC,CAAW,KAAG,MAAGhC,EAAsB,OAAQyB,CAAM,CAAC,CAAC,GACjF,CAAC,EAEtBsH,EAAgBD,EAAS,CAAC,EAgB9BC,GAba,MAAM5J,EACd,OAAiCa,CAAqB,EACtD,OAAO,EACP,OAAO,CACJ,MAAOgC,EACP,OAAAP,EACA,OAAQ0H,EACR,UAAW,GACX,UAAW,IAAI,KACf,UAAW,OAAO1H,CAAM,EACxB,UAAW,IAAI,KACf,UAAW,OAAOA,CAAM,CAC5B,CAAC,GACgB,CAAC,EAGtB6E,EAAc,CACd,IAAM0C,EAAiBzC,GAAyBD,CAAY,EACxD0C,IAEAvJ,EAAe,iBAAiBqC,EAAekH,EAAgBvH,CAAM,CAAC,EACtE,MAAMN,EAAoB,GAAGJ,CAA8B,GAAGiI,CAAc,EAAE,EAEtF,CACA,OAAAvJ,EAAe,WAAW,uBAAuBuC,CAAW,EAAE,EAE9DvC,EAAe,iBAAiBwC,EAAiBD,EAAaP,CAAM,CAAC,EACrE,MAAMN,EAAoB,GAAGH,CAAgC,GAAGgB,CAAW,EAAE,EAE7E,MAAME,EACF+G,GAA0B,MAAM,CAC5B,KAAM,gBACN,YAAajH,EACb,mBAAoBP,EACpB,WAAY,OACZ,MAAO0H,EACP,aAAAN,CACJ,CAAC,EACD,eACJ,EAEOE,CACX,EAcMK,GAA0B,MAAOnB,EAAeoB,IAA4D,CAE9G,GAAIA,IAAgB,KAAM,OAC1B,IAAMC,EAAW,MAAMzE,GAAqB,EACtC,CAAC0E,CAAG,EAAI,MAAMpK,EACf,OAA0B,CAAE,MAAOY,EAAqB,KAAM,CAAC,EAC/D,IAAI,CAAC,EACL,KAAKA,CAAoB,EACzB,SAAM,UAAI,MAAGA,EAAqB,MAAOkI,CAAK,KAAG,WAAQlI,EAAqB,QAASuJ,CAAQ,CAAC,CAAC,EACtG,OAAOC,EAAMF,EAAc,MAC/B,EAcMG,GAAa,MAAOvG,EAA+BxB,EAAgBO,EAAqBsB,EAAiBgD,EAA6BuC,IAA4D,CACpM,GAAM,CAACzF,CAAG,EAAI,MAAMjE,EACf,OAAuF,CAAE,WAAYQ,EAAe,WAAY,UAAWA,EAAe,UAAW,OAAQA,EAAe,MAAO,CAAC,EACpM,IAAI,CAAC,EACL,KAAKA,CAAc,EACnB,SAAM,MAAGA,EAAe,GAAIqC,CAAW,CAAC,EAG7C,GAAI,CAACoB,GAAOA,EAAI,UACZ,MAAM,IAAI,MAAM,WAAW,EAQ/B,GAJA,MAAMsF,GAAiBzF,EAAOjB,EAAaP,CAAM,EAI7C,CAAC6C,GAAoBrB,EAAM,YAAaG,EAAI,UAAU,EACtD,MAAM,IAAI,MAAM,4DAA4D,EAEhF,GAAIA,EAAI,WAAW,YAAY,IAAM,WACjC,MAAM,IAAI,MAAM,2DAA2D,EAG/E,IAAMqG,EAAqB,MAAML,GAAwBpH,EAAaoB,EAAI,MAAM,EAE1E,CAACsG,CAAQ,EAAI,MAAMvK,EACpB,OAA8BU,CAAuB,EACrD,OAAO,EACP,OAAO,CACJ,MAAOmC,EACP,OAAAP,EACA,KAAM6B,EACN,UAAW,IAAI,KACf,UAAW,OAAO7B,CAAM,EACxB,UAAW,IAAI,KACf,UAAW,OAAOA,CAAM,CAC5B,CAAC,EAEC,CAACkI,CAAI,EAAI,MAAMxK,EAAG,OAAuC,CAAE,YAAaE,EAAM,WAAY,CAAC,EAAE,IAAI,CAAC,EAAE,KAAKA,CAAK,EAAE,SAAM,MAAGA,EAAM,GAAIoC,CAAM,CAAC,EAC1ImI,EAAWD,GAAM,aAAe,UAGtC,GAAIrD,EAAc,CACd,IAAM0C,GAAiBzC,GAAyBD,CAAY,EACxD0C,KACAvJ,EAAe,iBAAiB,qBAAqBuJ,EAAc,QAAQ,EAC3E,MAAM7H,EAAoB,GAAGJ,CAA8B,GAAGiI,EAAc,EAAE,EAEtF,CACAvJ,EAAe,WAAW,uBAAuBuC,CAAW,EAAE,EAC9DvC,EAAe,iBAAiB,uBAAuBuC,CAAW,QAAQ,EAC1E,MAAMb,EAAoB,GAAGH,CAAgC,GAAGgB,CAAW,EAAE,EAE7E,IAAM6H,EAAwC,CAC1C,GAAIH,EAAS,GACb,OAAQnK,EAAamK,EAAS,MAAM,EACpC,SAAUE,EACV,QAASF,EAAS,KAClB,UAAWpL,GAAgBoL,EAAS,UAAW,qBAAqB,GAAK,GACzE,OAAQ,EACZ,EAEA,aAAMxH,EACF4H,GAAwB,MAAM,CAC1B,KAAM,cACN,YAAa9H,EACb,QAAS6H,EACT,aAAAhB,EACA,mBAAAY,EAEA,WAAYrG,EAAI,UACpB,CAAC,EACD,YACJ,EAEOyG,CACX,EAMME,GAA6B,MAAOtJ,EAAmBuJ,IAClD,MAAMvJ,EAAG,OAA8B,EAAE,IAAI,CAAC,EAAE,KAAKZ,CAAuB,EAAE,SAAM,MAAGA,EAAwB,GAAImK,CAAS,CAAC,EAelIC,GAAgB,MAAOhH,EAA+BxB,EAAgBO,EAAqBgI,EAAmB1D,EAA6BuC,IAAwC,CACrL,IAAMT,EAAW,MAAM2B,GAA2B5K,EAAI6K,CAAS,EAC/D,GAAI5B,EAAS,SAAW,EACpB,MAAM,IAAI,MAAM,WAAW,EAQ/B,GAAIA,EAAS,CAAC,EAAE,QAAUpG,EACtB,MAAM,IAAI,MAAM,WAAW,EAU/B,GAAM,CAACkI,CAAY,EAAI,MAAM/K,EAAG,OAAsD,CAAE,OAAQQ,EAAe,OAAQ,WAAYA,EAAe,UAAW,CAAC,EAAE,IAAI,CAAC,EAAE,KAAKA,CAAc,EAAE,SAAM,MAAGA,EAAe,GAAIyI,EAAS,CAAC,EAAE,KAAK,CAAC,EAC1O,GAAI,EAAE8B,GAAgB5F,GAAoBrB,EAAM,QAASiH,EAAa,UAAU,GAC5E,MAAM,IAAI,MAAM,WAAW,EAG/B,GAAI9B,EAAS,CAAC,EAAE,SAAW3G,EACvB,MAAM,IAAI,MAAM,cAAc,EAIlC,IAAMgI,EAAqB,MAAML,GAAwBpH,EAAakI,EAAa,MAAM,EAKzF,GAHA,MAAM/K,EAAG,OAAOU,CAAuB,EAAE,SAAM,MAAGA,EAAwB,GAAImK,CAAS,CAAC,EAGpF1D,EAAc,CACd,IAAM0C,EAAiBzC,GAAyBD,CAAY,EACxD0C,IACAvJ,EAAe,iBAAiB,qBAAqBuJ,CAAc,QAAQ,EAC3E,MAAM7H,EAAoB,GAAGJ,CAA8B,GAAGiI,CAAc,EAAE,EAEtF,CACAvJ,EAAe,WAAW,uBAAuBuC,CAAW,EAAE,EAC9DvC,EAAe,iBAAiB,uBAAuBuC,CAAW,QAAQ,EAC1E,MAAMb,EAAoB,GAAGH,CAAgC,GAAGgB,CAAW,EAAE,EAE7E,MAAME,EACFiI,GAA2B,MAAM,CAC7B,KAAM,iBACN,YAAanI,EACb,UAAAgI,EACA,aAAAnB,EACA,mBAAAY,EAEA,WAAYS,GAAc,UAC9B,CAAC,EACD,eACJ,CACJ,EAMME,GAAoB,MAAO3I,EAAgB6E,EAAsBuC,IAAqD,CACxHzI,GAAO,KAAK,2BAA4B,CAAE,OAAAqB,EAAQ,aAAA6E,CAAa,CAAC,EAGhE,GAAM,CAACqD,CAAI,EAAI,MAAMxK,EAAG,OAAuC,CAAE,YAAaE,EAAM,WAAY,CAAC,EAAE,KAAKA,CAAK,EAAE,SAAM,MAAGA,EAAM,GAAIoC,CAAM,CAAC,EACnImI,EAAWD,GAAM,aAAepK,EAAakC,CAAM,EAEzD,GAAI,CACA,IAAM4I,EAAS,MAAM/J,GAAc,MAAOG,GAAO,CAC7CL,GAAO,KAAK,sBAAsB,EAElC,IAAMkK,EAAW,iBAAiB,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,GACvD,CAAClH,CAAG,EAAI,MAAM3C,EACf,OAA0Bd,CAAc,EACxC,OAAO,EACP,OAAO,CACJ,WAAY,WACZ,SAAU2K,EACV,aAAc,IAAI,KAAKhE,CAAY,EACnC,OAAQ7E,EACR,MAAO,iBACP,UAAW,IAAI,KACf,UAAW,OAAOA,CAAM,EACxB,UAAW,IAAI,KACf,UAAW,OAAOA,CAAM,CAC5B,CAAC,EAGL,MAAMhB,EACD,OAAOd,CAAc,EACrB,IAAI,CAAE,SAAU,OAAOyD,EAAI,EAAE,CAAE,CAAC,EAChC,SAAM,MAAGzD,EAAe,GAAIyD,EAAI,EAAE,CAAC,EAExChD,GAAO,KAAK,cAAegD,CAAG,EAG9B,MAAM3C,EAAG,OAAOb,CAAmB,EAAE,OAAO,CACxC,MAAOwD,EAAI,GACX,KAAM,GACN,UAAW,IAAI,KACf,UAAW,OAAO3B,CAAM,EACxB,UAAW,IAAI,KACf,UAAW,OAAOA,CAAM,CAC5B,CAAC,EACDrB,GAAO,KAAK,kBAAkB,EAG9BA,GAAO,KAAK,yBAAyB,EACrC,IAAMmK,EAAe,MAAM7D,GAAgB,EAC3CtG,GAAO,KAAK,eAAgBmK,CAAY,EACxC,IAAMpC,GAAgC,CAAC,EACvC,OAAIoC,IACA,MAAM9J,EAAG,OAAOV,CAAoB,EAAE,OAAO,CACzC,MAAOqD,EAAI,GACX,QAASmH,EACT,UAAW,IAAI,KACf,UAAW,OAAO9I,CAAM,CAC5B,CAAC,EACD0G,GAAO,KAAK,CAAE,GAAIoC,EAAc,KAAMpK,GAAgB,MAAO,IAAK,CAAC,GAEvEC,GAAO,KAAK,gBAAgB,EAGrB,CACH,YAAagD,EAAI,GACjB,KAAMkD,EACN,UAAWhI,GAAgB8E,EAAI,UAAW,qBAAqB,EAC/D,OAAQwG,EACR,OAAQrK,EAAakC,CAAM,EAC3B,WAAY,WACZ,aAAcmI,EAEd,UAAWrK,EAAakC,CAAM,EAC9B,UAAWnD,GAAgB8E,EAAI,UAAW,qBAAqB,EAC/D,SAAU,KACV,iBAAkB,KAClB,cAAe,KACf,YAAa,KACb,aAAc,KACd,aAAc,CAAC,EACf,QAASA,EAAI,MACb,QAAS,GACT,iBAAkB,CAAC,EACnB,OAAQ,GACR,UAAW,GACX,OAAQ+E,GACR,eAAgB,CAAC,EAEjB,YAAa,CAAC,CAClB,CACJ,CAAC,EAGKtG,EAAyB0E,GAAyBD,CAAY,EAChEzE,GAEApC,EAAe,iBAAiBqC,EAAeD,EAAwBJ,CAAM,CAAC,EAElFhC,EAAe,iBAAiBoB,CAAuC,EAIvEpB,EAAe,iBAAiBkC,GAAcF,CAAM,CAAC,EAErD,MAAMN,EAAoBP,CAA0B,EAChDiB,GACA,MAAMV,EAAoB,GAAGJ,CAA8B,GAAGc,CAAsB,EAAE,EAM1F,IAAM2I,EAAa,MAAMtC,GAAyBuC,GAAuB,aAAa,sEAAsE,EAAGJ,EAAO,YAAa5I,EAAQ,CAAE,aAAc,EAAK,CAAC,EAEjN,GAAI+I,EAAY,CAGZ,IAAME,EAAgB,MAAMlE,GAAiB,EACvCmE,EAAUH,EAAW,OAAO,KAAMrG,GAAMuG,EAAc,SAASvG,EAAE,EAAE,CAAC,EAC1E,MAAMjC,EACF0I,GAA0B,MAAM,CAC5B,KAAM,gBACN,YAAaJ,EAAW,YACxB,OAAQA,EACR,aAAA3B,EACA,mBAAoB8B,EAAUlJ,EAAS,MAC3C,CAAC,EACD,mBACJ,CACJ,CAEA,OAAO+I,GAAcH,CACzB,OAAS7H,EAAG,CACR,MAAApC,GAAO,MAAM,6BAA8BoC,CAAC,EACtCA,CACV,CACJ,EAQMqI,GAAyB,MAAOpK,EAAmBuB,IAC9C,MAAMvB,EAAG,OAA0B,EAAE,IAAI,CAAC,EAAE,KAAKd,CAAc,EAAE,SAAM,MAAGA,EAAe,GAAIqC,CAAW,CAAC,EAO9G8I,GAAuB,MAAOrK,EAAmBuB,EAAqB+I,IAAqC,CAC7G,MAAMtK,EAAG,OAAOd,CAAc,EAAE,IAAIoL,CAAI,EAAE,SAAM,MAAGpL,EAAe,GAAIqC,CAAW,CAAC,CACtF,EAMMgJ,GAA4B,MAAOvK,EAAmBwH,EAAe8C,IAA0C,CACjH,MAAMtK,EAAG,OAAOb,CAAmB,EAAE,IAAImL,CAAI,EAAE,SAAM,MAAGnL,EAAoB,MAAOqI,CAAK,CAAC,CAC7F,EAMMgD,GAAyB,MAAOxK,EAAmBwH,EAAeiD,IAAoB,CACxF,MAAMzK,EAAG,OAAOV,CAAoB,EAAE,SAAM,UAAI,MAAGA,EAAqB,MAAOkI,CAAK,KAAG,MAAGlI,EAAqB,QAASmL,CAAO,CAAC,CAAC,CACrI,EAkOA,MAAO,CAEH,UAAWjK,EACX,aAAcC,EAEd,WAfe,SAA2B,CAC1CzB,EAAe,SAAS,EACxBiG,EAAY,MAAM,EAClBjB,GAAsB,KAClB/E,GACA,MAAMyB,EAAoBP,CAA0B,EAExDR,GAAO,KAAK,sEAAsE,CACtF,EAQI,wBA3C4B,MAAO2G,EAA2BoE,IAAoD,CAClH1L,EAAe,iBAAiBoB,CAAuC,EAKvEpB,EAAe,iBAAiBiB,EAA0B,EAC1D,MAAMS,EAAoBP,CAA0B,EACpD,QAAWqH,KAAS,IAAI,IAAIlB,CAAM,EAC9BtH,EAAe,iBAAiB,uBAAuBwI,CAAK,QAAQ,EACpE,MAAM9G,EAAoB,GAAGH,CAAgC,GAAGiH,CAAK,EAAE,EAE3E,QAAW3B,KAAgB,IAAI,IAAI6E,CAAa,EAAG,CAC/C,IAAMC,EAAa7E,GAAyBD,CAAY,EAEnD8E,GACL,MAAMjK,EAAoB,GAAGJ,CAA8B,GAAGqK,CAAU,EAAE,CAC9E,CACJ,EA2BI,sBAAAlG,EAEA,8BAAAS,GACA,uCAAAZ,GACA,8BAAAsB,GACA,0CAAAqB,GACA,yBAAAQ,GACA,qCAAAK,GACA,gBAAA7B,GAEA,uBAAAnB,GACA,qBAAA6B,GACA,sBAAAE,GACA,sBAAAG,GAEA,cAAAkB,GACA,cAAAO,GACA,WAAAM,GACA,cAAAS,GACA,kBAAAG,GACA,kBAxMsB,MAAOpI,EAAqBP,EAAgBsJ,EAA4ClC,IAAwC,CACtJ,IAAMwC,EAAS,MAAMR,GAAuB1L,EAAI6C,CAAW,EAC3D,GAAI,CAACqJ,EAAO,QAAUA,EAAO,CAAC,EAAE,UAC5B,MAAM,IAAI,MAAM,WAAW,EAE/B,GAAIA,EAAO,CAAC,EAAE,SAAW5J,EACrB,MAAM,IAAI,MAAM,cAAc,EAGlC,MAAMnB,GAAc,MAAOG,GAAO,CAC1BsK,EAAK,QAAU,QACf,MAAMD,GAAqBrK,EAAIuB,EAAa,CACxC,MAAO+I,EAAK,MACZ,UAAW,IAAI,KACf,UAAW,OAAOtJ,CAAM,CAC5B,CAAC,EAEDsJ,EAAK,UAAY,QACjB,MAAMC,GAA0BvK,EAAIuB,EAAa,CAC7C,KAAM+I,EAAK,QACX,UAAW,IAAI,KACf,UAAW,OAAOtJ,CAAM,CAC5B,CAAC,CAET,CAAC,EAKDhC,EAAe,WAAW,uBAAuBuC,CAAW,EAAE,EAC9DvC,EAAe,iBAAiB,uBAAuBuC,CAAW,QAAQ,EAC1EvC,EAAe,iBAAiBoB,CAAuC,EACvE,MAAMM,EAAoB,GAAGH,CAAgC,GAAGgB,CAAW,EAAE,EAG7E,IAAMsJ,EAAqB/E,GAAyBjI,GAAgB+M,EAAO,CAAC,EAAE,aAAc,YAAY,CAAC,EACrGC,IACA7L,EAAe,iBAAiB,qBAAqB6L,CAAkB,QAAQ,EAC/E,MAAMnK,EAAoB,GAAGJ,CAA8B,GAAGuK,CAAkB,EAAE,GAKtF,IAAMd,EAAa,MAAMtC,GAAyBuC,GAAuB,aAAa,sEAAsE,EAAGzI,EAAaP,EAAQ,CAAE,aAAc,EAAK,CAAC,EAC1M,GAAI+I,EAAY,CAGZ,IAAME,EAAgB,MAAMlE,GAAiB,EACvCmE,EAAUH,EAAW,OAAO,KAAMrG,GAAMuG,EAAc,SAASvG,EAAE,EAAE,CAAC,EAC1E,MAAMjC,EACFqJ,GAA0B,MAAM,CAC5B,KAAM,gBACN,YAAaf,EAAW,YACxB,OAAQA,EACR,aAAA3B,EACA,mBAAoB8B,EAAUlJ,EAAS,MAC3C,CAAC,EACD,mBACJ,CACJ,CACJ,EA6II,mBAvIuB,MAAOO,EAAqBP,EAAgBoH,IAA4D,CAC/H,IAAMwC,EAAS,MAAMR,GAAuB1L,EAAI6C,CAAW,EAC3D,GAAI,CAACqJ,EAAO,OACR,MAAM,IAAI,MAAM,WAAW,EAE/B,GAAIA,EAAO,CAAC,EAAE,SAAW5J,EACrB,MAAM,IAAI,MAAM,cAAc,EAGlC,IAAM8I,EAAe,MAAM7D,GAAgB,EAC3C,GAAI,CAAC6D,EAAc,OAAO,KAW1B,GATA,MAAMU,GAAuB9L,EAAI6C,EAAauI,CAAY,EAG1D9K,EAAe,iBAAiB,uBAAuBuC,CAAW,QAAQ,EAC1EvC,EAAe,iBAAiBoB,CAAuC,EAEvEpB,EAAe,iBAAiBiB,EAA0B,EAE1D,MAAMS,EAAoBP,CAA0B,EAChDyK,EAAO,CAAC,EAAE,aAAc,CACxB,IAAMrC,EAAiBzC,GAAyBjI,GAAgB+M,EAAO,CAAC,EAAE,aAAc,YAAY,CAAC,EACjGrC,GACA,MAAM7H,EAAoB,GAAGJ,CAA8B,GAAGiI,CAAc,EAAE,CAEtF,CAGA,IAAMnH,EAAyB0E,GAAyBjI,GAAgB+M,EAAO,CAAC,EAAE,aAAc,YAAY,CAAC,EACzGxJ,GAEApC,EAAe,iBAAiBqC,EAAeD,EAAwBJ,CAAM,CAAC,EAKlF,IAAM+I,EAAa,MAAMtC,GAAyBuC,GAAuB,aAAa,sEAAsE,EAAGzI,EAAaP,EAAQ,CAAE,aAAc,EAAK,CAAC,EAC1M,OAAI+I,GACA,MAAMtI,EACFsJ,GAA2B,MAAM,CAC7B,KAAM,iBACN,YAAahB,EAAW,YACxB,OAAQA,EACR,aAAA3B,CACJ,CAAC,EACD,oBACJ,EAEG2B,GAAc,IACzB,EAsFI,kBAxPsB,MAAOxI,EAAqBP,EAAgBoH,IAAwC,CAC1G,IAAMwC,EAAS,MAAMR,GAAuB1L,EAAI6C,CAAW,EAC3D,GAAI,CAACqJ,EAAO,QAAUA,EAAO,CAAC,EAAE,UAC5B,MAAM,IAAI,MAAM,WAAW,EAE/B,GAAIA,EAAO,CAAC,EAAE,SAAW5J,EACrB,MAAM,IAAI,MAAM,cAAc,EAiBlC,GAdA,MAAMqJ,GAAqB3L,EAAI6C,EAAa,CACxC,UAAW,IAAI,KACf,UAAW,OAAOP,CAAM,EACxB,UAAW,IAAI,KACf,UAAW,OAAOA,CAAM,CAC5B,CAAC,EAGDhC,EAAe,iBAAiB,uBAAuBuC,CAAW,QAAQ,EAC1EvC,EAAe,iBAAiBoB,CAAuC,EAEvEpB,EAAe,iBAAiBiB,EAA0B,EAE1D,MAAMS,EAAoBP,CAA0B,EAChDyK,EAAO,CAAC,EAAE,aAAc,CACxB,IAAMrC,EAAiBzC,GAAyBjI,GAAgB+M,EAAO,CAAC,EAAE,aAAc,YAAY,CAAC,EACjGrC,GACA,MAAM7H,EAAoB,GAAGJ,CAA8B,GAAGiI,CAAc,EAAE,CAEtF,CAEA,MAAM9G,EACFuJ,GAA0B,MAAM,CAC5B,KAAM,gBACN,YAAazJ,EACb,aAAA6G,EAEA,WAAYwC,EAAO,CAAC,EAAE,UAC1B,CAAC,EACD,mBACJ,CACJ,EAsNI,uBAAAR,GACA,qBAAAC,GACA,0BAAAE,GACA,uBAAAC,EACJ,CACJ,CCx/DA,IAAAS,GAAwB,uBAmBjB,SAASC,GAAgCC,EAA6D,CACzG,IAAMC,EAA4B,CAAC,EAC7BC,EACFF,GAAWA,EAAQ,OAAS,EACtBA,EACA,CACI,CAAE,IAAK,WAAY,KAAM,WAAY,YAAa,wBAAyB,EAC3E,CAAE,IAAK,WAAY,KAAM,WAAY,YAAa,wBAAyB,CAC/E,EAEV,QAAWG,KAAOD,EAAe,CAE7B,IAAME,EAAc,gBADHD,EAAI,IAAI,YAAY,EAAE,QAAQ,cAAe,GAAG,CACrB,GAC5CF,EAAK,KAAK,CACN,YAAAG,EACA,KAAMD,EAAI,KACV,YAAaA,EAAI,aAAe,GAAGA,EAAI,IAAI,sBAC/C,CAAC,EAEGA,EAAI,yBAA2B,IAC/BF,EAAK,KAAK,CACN,YAAa,GAAGG,CAAW,WAC3B,KAAM,GAAGD,EAAI,IAAI,WACjB,YAAa,GAAGA,EAAI,IAAI,8BAC5B,CAAC,CAET,CAEA,OAAOF,CACX,CAkBA,eAAsBI,GAA8BC,EAAaC,EAAsCC,EAAkI,CACrO,IAAMC,EAAIH,EACJI,EAAaH,EAAY,WACzBI,EAASH,EAAK,OACdI,EAAQJ,EAAK,OAAS,sBACtBK,EAAM,IAAI,KAEVC,EAAeN,EAAK,WAAaT,GAAgCS,EAAK,OAAO,EAEnF,QAAWO,KAAOD,EAAc,CAC5B,IAAME,EAAW,MAAMP,EAClB,OAAO,EACP,KAAKC,CAAU,EACf,SAAM,WAAI,OAAGA,EAAW,OAAiBC,CAAe,KAAG,OAAGD,EAAW,YAAsBK,EAAI,WAAoB,CAAC,CAAC,GAE1H,CAACC,GAAYA,EAAS,SAAW,IACjC,MAAMP,EAAE,OAAOC,CAAU,EAAE,OAAO,CAC9B,OAAAC,EACA,YAAaI,EAAI,YACjB,KAAMA,EAAI,KACV,YAAaA,EAAI,aAAe,KAChC,UAAWF,EACX,UAAWD,EACX,UAAWC,EACX,UAAWD,CACf,CAAC,CAET,CACJ,CC7DA,IAAMK,GAAgBC,KAA4B,6BAA6B,EAMlEC,GAAqB,CAAgBC,EAAwBC,EAAeC,EAAuCC,EAA4BN,KAA6B,CACrL,GAAI,CAACG,EACD,MAAO,CAAC,EAGZ,GAAI,CACA,IAAMI,EAAS,KAAK,MAAMJ,CAAO,EACjC,OAAK,MAAM,QAAQI,CAAM,EAIjBA,EAAkB,IAAIF,CAAM,EAAE,OAAQG,GAA4BA,IAAU,IAAI,GAHpFF,EAAO,KAAK,cAAcF,CAAK,uBAAuB,EAC/C,CAAC,EAGhB,OAASK,EAAO,CACZ,OAAAH,EAAO,KAAK,mBAAmBF,CAAK,IAAKK,CAAK,EACvC,CAAC,CACZ,CACJ,ECpDA,IAAAC,GAA0B,uBAC1BC,EAA0I,kCA+HnI,SAASC,GAA0CC,EAAeC,EAA2C,CAChH,IAAMC,KAAI,eAAYF,CAAU,EAC1BG,EAAQF,EAAK,UAEbG,EAAMF,EAAE,MACV,iBACA,CACI,MAAI,UAAO,KAAM,CAAE,KAAM,QAAS,CAAC,EAAE,SAAS,EAAE,QAAQ,EACxD,cAAY,YAAS,cAAe,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,EAC5D,YAAU,YAAS,YAAa,CAAE,OAAQ,GAAI,CAAC,EAAE,QAAQ,EACzD,eAAa,UAAO,gBAAiB,CAAE,KAAM,QAAS,CAAC,EAAE,kBAAkB,qCAAkC,EAC7G,gBAAc,QAAK,eAAe,EAClC,UAAQ,UAAO,UAAW,CAAE,KAAM,QAAS,CAAC,EAC5C,SAAO,YAAS,QAAS,CAAE,OAAQ,GAAI,CAAC,EACxC,WAAS,YAAS,UAAW,CAAE,OAAQ,KAAM,CAAC,EAC9C,aAAW,aAAU,YAAY,EAAE,QAAQ,EAC3C,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,EAC1D,aAAW,aAAU,YAAY,EAAE,QAAQ,EAC3C,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,EAC1D,aAAW,aAAU,YAAY,EACjC,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,CACpD,EACCG,GAAU,IACP,cAAW,CAAE,KAAM,GAAGL,CAAU,qBAAsB,QAAS,CAACK,EAAM,EAAE,CAAE,CAAC,KAC3E,cAAW,CACP,KAAM,GAAGL,CAAU,6BACnB,QAAS,CAACK,EAAM,MAAM,EACtB,eAAgB,CAACF,EAAM,EAAE,CAC7B,CAAC,KACD,SAAM,GAAGH,CAAU,qCAAqC,EAAE,GAAGK,EAAM,YAAY,KAC/E,SAAM,GAAGL,CAAU,kCAAkC,EAAE,GAAGK,EAAM,SAAS,KACzE,SAAM,GAAGL,CAAU,8BAA8B,EAAE,GAAGK,EAAM,WAAYA,EAAM,QAAQ,KACtF,SAAM,GAAGL,CAAU,+BAA+B,EAAE,GAAGK,EAAM,MAAM,KACnE,SAAM,GAAGL,CAAU,wCAAwC,EAAE,MAAG,SAAKK,EAAM,YAAY,KAAG,SAAKA,EAAM,EAAE,CAAC,KACxG,SAAM,GAAGL,CAAU,kCAAkC,EAAE,GAAGK,EAAM,SAAS,CAC7E,CACJ,EAEMC,EAAWJ,EAAE,MACf,sBACA,CACI,SAAO,UAAO,SAAU,CAAE,KAAM,QAAS,CAAC,EAAE,QAAQ,EACpD,QAAM,YAAS,OAAQ,CAAE,OAAQ,KAAM,CAAC,EACxC,YAAU,YAAS,WAAY,CAAE,OAAQ,KAAM,CAAC,EAChD,aAAW,aAAU,YAAY,EAAE,QAAQ,EAC3C,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,EAC1D,aAAW,aAAU,YAAY,EAAE,QAAQ,EAC3C,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,CAC9D,EACCG,GAAU,IACP,cAAW,CAAE,KAAM,GAAGL,CAAU,0BAA2B,QAAS,CAACK,EAAM,KAAK,CAAE,CAAC,KACnF,cAAW,CACP,KAAM,GAAGL,CAAU,iCACnB,QAAS,CAACK,EAAM,KAAK,EACrB,eAAgB,CAACD,EAAI,EAAE,CAC3B,CAAC,CACL,CACJ,EAEMG,EAAUL,EAAE,MAId,qBACA,CACI,MAAI,UAAO,KAAM,CAAE,KAAM,QAAS,CAAC,EAAE,SAAS,EAAE,QAAQ,EACxD,SAAO,UAAO,SAAU,CAAE,KAAM,QAAS,CAAC,EAAE,QAAQ,EACpD,UAAQ,UAAO,UAAW,CAAE,KAAM,QAAS,CAAC,EAAE,QAAQ,EACtD,QAAM,YAAS,OAAQ,CAAE,OAAQ,KAAM,CAAC,EAAE,QAAQ,EAClD,aAAW,aAAU,YAAY,EAAE,QAAQ,EAC3C,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,EAC1D,aAAW,aAAU,YAAY,EAAE,QAAQ,EAC3C,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,CAC9D,EACCG,GAAU,IACP,cAAW,CAAE,KAAM,GAAGL,CAAU,yBAA0B,QAAS,CAACK,EAAM,EAAE,CAAE,CAAC,KAC/E,cAAW,CACP,KAAM,GAAGL,CAAU,gCACnB,QAAS,CAACK,EAAM,KAAK,EACrB,eAAgB,CAACD,EAAI,EAAE,CAC3B,CAAC,KACD,cAAW,CACP,KAAM,GAAGJ,CAAU,iCACnB,QAAS,CAACK,EAAM,MAAM,EACtB,eAAgB,CAACF,EAAM,EAAE,CAC7B,CAAC,KACD,SAAM,GAAGH,CAAU,kCAAkC,EAAE,GAAGK,EAAM,KAAK,CACzE,CACJ,EAEMG,EAAQN,EAAE,MACZ,mBACA,CACI,MAAI,UAAO,KAAM,CAAE,KAAM,QAAS,CAAC,EAAE,SAAS,EAAE,QAAQ,EACxD,UAAQ,UAAO,UAAW,CAAE,KAAM,QAAS,CAAC,EAC5C,QAAM,YAAS,OAAQ,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,EAC/C,SAAO,YAAS,QAAS,CAAE,OAAQ,EAAG,CAAC,EACvC,aAAW,OAAI,YAAY,EAC3B,aAAW,aAAU,YAAY,EAAE,QAAQ,EAC3C,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,EAC1D,aAAW,aAAU,YAAY,EAAE,QAAQ,EAC3C,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,CAC9D,EACCG,GAAU,IACP,cAAW,CAAE,KAAM,GAAGL,CAAU,uBAAwB,QAAS,CAACK,EAAM,EAAE,CAAE,CAAC,KAC7E,cAAW,CACP,KAAM,GAAGL,CAAU,+BACnB,QAAS,CAACK,EAAM,MAAM,EACtB,eAAgB,CAACF,EAAM,EAAE,CAC7B,CAAC,CACL,CACJ,EAEMM,EAAWP,EAAE,MACf,uBACA,CACI,SAAO,UAAO,SAAU,CAAE,KAAM,QAAS,CAAC,EAAE,QAAQ,EACpD,WAAS,UAAO,WAAY,CAAE,KAAM,QAAS,CAAC,EAAE,QAAQ,EACxD,aAAW,aAAU,YAAY,EAAE,QAAQ,EAC3C,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,CAC9D,EACCG,GAAU,IACP,cAAW,CAAE,KAAM,GAAGL,CAAU,2BAA4B,QAAS,CAACK,EAAM,MAAOA,EAAM,OAAO,CAAE,CAAC,KACnG,cAAW,CACP,KAAM,GAAGL,CAAU,kCACnB,QAAS,CAACK,EAAM,KAAK,EACrB,eAAgB,CAACD,EAAI,EAAE,CAC3B,CAAC,KACD,cAAW,CACP,KAAM,GAAGJ,CAAU,oCACnB,QAAS,CAACK,EAAM,OAAO,EACvB,eAAgB,CAACG,EAAM,EAAE,CAC7B,CAAC,CACL,CACJ,EAEME,EAAaR,EAAE,MACjB,wBACA,CACI,SAAO,UAAO,SAAU,CAAE,KAAM,QAAS,CAAC,EAAE,QAAQ,EACpD,UAAQ,UAAO,UAAW,CAAE,KAAM,QAAS,CAAC,EAAE,QAAQ,EACtD,UAAQ,OAAI,SAAS,EAAE,QAAQ,EAAK,EAAE,QAAQ,EAC9C,aAAW,OAAI,YAAY,EAAE,QAAQ,EAAK,EAAE,QAAQ,EACpD,aAAW,aAAU,YAAY,EAAE,QAAQ,EAC3C,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,EAC1D,aAAW,aAAU,YAAY,EAAE,QAAQ,EAC3C,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,CAC9D,EACCG,GAAU,IACP,cAAW,CAAE,KAAM,GAAGL,CAAU,4BAA6B,QAAS,CAACK,EAAM,MAAOA,EAAM,MAAM,CAAE,CAAC,KACnG,cAAW,CACP,KAAM,GAAGL,CAAU,mCACnB,QAAS,CAACK,EAAM,KAAK,EACrB,eAAgB,CAACD,EAAI,EAAE,CAC3B,CAAC,KACD,cAAW,CACP,KAAM,GAAGJ,CAAU,oCACnB,QAAS,CAACK,EAAM,MAAM,EACtB,eAAgB,CAACF,EAAM,EAAE,CAC7B,CAAC,CACL,CACJ,EAEMQ,EAAaT,EAAE,MACjB,wBACA,CACI,MAAI,UAAO,KAAM,CAAE,KAAM,QAAS,CAAC,EAAE,SAAS,EAAE,QAAQ,EACxD,SAAO,UAAO,SAAU,CAAE,KAAM,QAAS,CAAC,EAAE,QAAQ,EAEpD,cAAY,UAAO,cAAe,CAAE,KAAM,QAAS,CAAC,EAEpD,aAAW,YAAS,aAAc,CAAE,OAAQ,GAAI,CAAC,EAAE,QAAQ,EAC3D,YAAU,YAAS,YAAa,CAAE,OAAQ,GAAI,CAAC,EAAE,QAAQ,EAEzD,YAAU,YAAS,YAAa,CAAE,OAAQ,KAAM,CAAC,EAAE,QAAQ,EAC3D,YAAU,YAAS,YAAa,CAAE,OAAQ,GAAI,CAAC,EAC/C,YAAU,UAAO,YAAa,CAAE,KAAM,QAAS,CAAC,EAKhD,mBAAiB,aAAU,mBAAmB,EAC9C,mBAAiB,aAAU,mBAAmB,EAC9C,aAAW,aAAU,YAAY,EAAE,QAAQ,EAC3C,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,EAC1D,aAAW,aAAU,YAAY,EAAE,QAAQ,EAC3C,aAAW,YAAS,aAAc,CAAE,OAAQ,EAAG,CAAC,EAAE,QAAQ,CAC9D,EACCG,GAAU,IACP,cAAW,CAAE,KAAM,GAAGL,CAAU,4BAA6B,QAAS,CAACK,EAAM,EAAE,CAAE,CAAC,KAClF,cAAW,CACP,KAAM,GAAGL,CAAU,mCACnB,QAAS,CAACK,EAAM,KAAK,EACrB,eAAgB,CAACD,EAAI,EAAE,CAC3B,CAAC,KACD,cAAW,CACP,KAAM,GAAGJ,CAAU,wCACnB,QAAS,CAACK,EAAM,UAAU,EAC1B,eAAgB,CAACF,EAAM,EAAE,CAC7B,CAAC,KAED,eAAY,GAAGH,CAAU,sCAAsC,EAAE,GAAGK,EAAM,MAAOA,EAAM,SAAS,KAEhG,SAAM,GAAGL,CAAU,qCAAqC,EAAE,GAAGK,EAAM,MAAOA,EAAM,SAAS,CAC7F,CACJ,EAEA,MAAO,CAAE,IAAAD,EAAK,SAAAE,EAAU,QAAAC,EAAS,WAAAI,EAAY,MAAAH,EAAO,SAAAC,EAAU,WAAAC,CAAW,CAC7E,CdnRO,SAASE,GAAwBC,EAAiC,CAGhEA,EAAO,QACOA,EAAO,QAAU,SACzB,KAAK,6JAA6J,EAG7K,IAAMC,EAASC,GAAiBF,EAAO,KAAK,EACtCG,EAAQ,IAAIC,GAAe,CAAE,aAAcJ,EAAO,mBAAqB,GAAO,EAAGC,CAAM,EAEvFI,EAAUC,GAAyB,CAAE,GAAGN,EAAQ,MAAAG,EAAO,OAAAF,CAAO,CAAC,EAE/DM,EAAY,IAAIC,GAAqB,CACvC,MAAOR,EAAO,MACd,UAAWK,EAAQ,UACnB,MAAAF,EACA,OAAQH,EAAO,MACnB,CAAC,EAIKS,EAAWC,GAA0B,CACvC,aAAcV,EAAO,aAGrB,0BAA2BA,EAAO,0BAClC,QAAAK,EACA,aAAcL,EAAO,aACrB,kBAAmBA,EAAO,kBAC1B,eAAgBA,EAAO,eACvB,mBAAoBA,EAAO,mBAC3B,6BAA8BA,EAAO,6BACrC,sBAAuBA,EAAO,sBAC9B,MAAOA,EAAO,MACd,UAAAO,EACA,UAAWF,EAAQ,UACnB,kBAAmBL,EAAO,kBAC1B,oBAAqBA,EAAO,oBAC5B,OAAQA,EAAO,MACnB,CAAC,EAED,MAAO,CAEH,QAAAK,EAEA,MAAAF,EAEA,OAAAF,EAEA,UAAAM,EAEA,UAAWF,EAAQ,UAOnB,iBAAkB,CAAC,GAAG,IAAI,IAAI,CAAC,WAAY,IAAIL,EAAO,iBAAmB,CAAC,GAAG,IAAKW,GAAWA,EAAO,UAAU,CAAC,CAAC,CAAC,EACjH,GAAGF,CACP,CACJ","names":["server_exports","__export","DailyReportSseReader","DailyReportViewerScope","SqlResultCache","asciiFallbackFileName","buildVisibleSourceTypeCondition","computeSourceTypeDigest","computeViewerScopeDigest","createDailyReportHandlers","createDailyReportServer","createDailyReportService","createEpochStore","defineDailyReportAuthzResources","defineDailyReportSchema","deniesEverySourceType","encodeRfc8187","formatDateValue","generateETag","isSourceTypeVisible","isStreamIdLte","jsonResponseWithETag","normalizeVisibleSourceTypes","sanitizeMediaType","seedDailyReportAuthzResources","transformJsonArray","__toCommonJS","createEpochStore","redis","epochKey","client","val","SqlResultCache","config","epochStore","cacheKey","opts","ratio","bucket","ttl","promise","epoch","records","forceRefresh","snapshot","currentEpoch","r","shared","expireAt","result","prefix","key","now","ISO_DATE_PATTERN","SLASH_DATE_PATTERN","zeroPad","value","normalizeFromDate","year","month","day","normalizeBusinessDateKey","value","normalizeFromDate","trimmed","ISO_DATE_PATTERN","SLASH_DATE_PATTERN","parsed","computeIdsStreamSnapshotToken","items","laneA","laneB","item","id","date","i","code","compareIdsStreamOrder","a","b","isIdsStreamOrdered","findIdsStreamResumeIndex","items","cursor","index","item","compareIdsStreamOrder","createLogger","level","prefix","impl","format","message","rest","import_zod","dailyReportInterviewerSchema","dailyReportExternalCommentSchema","dailyReportLabelDefSchema","dailyReportPostedCommentSchema","dailyReportAttachmentItemSchema","dailyReportDetailSchema","connectedMessageSchema","statusUpdateMessageSchema","sseSourceTypeSchema","commentAddMessageSchema","commentDeleteMessageSchema","reportCreateMessageSchema","reportUpdateMessageSchema","reportPublishMessageSchema","reportDeleteMessageSchema","dailyReportSseMessageSchema","import_node_crypto","generateETag","data","json","defaultLogger","createLogger","jsonResponseWithETag","request","cookie","payload","status","logger","etag","generateETag","ifNoneMatch","headers","import_node_events","isStreamIdLte","a","b","aMs","aSeq","bMs","bSeq","DailyReportSseReader","config","createLogger","onEntry","onError","resolve","err","shared","latest","results","stream","msg","parsed","msgType","reportHubId","import_drizzle_orm","DIGEST_SEPARATOR","DIGEST_RAW_MAX_LENGTH","foldStoredSourceType","raw","foldResolverToken","normalizeVisibleSourceTypes","set","token","folded","isSourceTypeVisible","sourceType","deniesEverySourceType","buildVisibleSourceTypeCondition","sourceTypeColumn","_visible","_commentable","_DailyReportViewerScope","visible","commentable","__privateAdd","__privateSet","__privateGet","input","isDimensions","read","rawComment","comment","narrowed","auditReason","DailyReportViewerScope","computeViewerScopeDigest","scope","computeSourceTypeDigest","joined","hash","i","jsonData","payload","init","headers","IDS_STREAM_FIRST_CHUNK_SIZE","IDS_STREAM_NEXT_CHUNK_SIZE","IDS_STREAM_ERROR_CLIENT_MESSAGE","IDS_STREAM_DATE_PATTERN","IDS_STREAM_HEARTBEAT_MS","IDS_STREAM_HEARTBEAT","SSE_VISIBILITY_REFRESH_TICKS","IDS_STREAM_SNAPSHOT_PATTERN","parseIdsStreamCursor","url","rawId","rawDate","rawSnapshot","snapshot","reportHubId","streamLinesFromFullArray","items","resumeKey","firstChunkSize","options","position","findIdsStreamResumeIndex","isFirstYield","chunkItems","isLast","lastItem","buildIdsStreamResponse","cookie","lines","request","logger","encoder","cancelled","pendingNext","stream","controller","heartbeatTimer","heartbeatPromise","resolve","raced","value","done","error","errorLine","DEFAULT_ATTACHMENT_MAX_BYTES","DEFAULT_ATTACHMENT_RATE_LIMIT_PER_MINUTE","DEFAULT_ATTACHMENT_CONCURRENCY","ATTACHMENT_BODY_FLUSH_TIMEOUT_MS","RATE_LIMITER_MAX_ENTRIES","RATE_LIMIT_RETRY_AFTER_SECONDS","CONCURRENCY_RETRY_AFTER_SECONDS","INLINE_SAFE_MEDIA_TYPES","HEADER_SAFE_PATTERN","MEDIA_TYPE_PATTERN","sanitizeMediaType","encodeRfc8187","name","c","asciiFallbackFileName","attachmentFailureToStatus","reason","ATTACHMENT_FAILURE_MESSAGES","createRateLimiter","limitPerMinute","buckets","userId","nowMs","existing","bucket","elapsedMs","allowed","oldest","createConcurrencyGate","limit","active","streamAndRelease","bytes","release","timeoutMs","timer","finish","sent","isSseMessageSourceTypeVisible","parsed","raw","visible","commentable","rawSourceType","isSourceTypeVisible","COMMENT_FACE_KEYS","redactSseReportComments","report","target","key","createDailyReportHandlers","config","authenticate","service","encodeUserId","redis","sseReader","streamKey","loginRedirectPath","apiLogger","createLogger","visibilityLogger","sseLogger","attachmentLogger","attachmentMaxBytes","consumeAttachmentRateToken","attachmentGate","resolveViewerScope","port","DailyReportViewerScope","resolved","e","indexLoader","user","hashedUserId","endpointHandlers","scope","normalizedBusinessDate","normalizeBusinessDateKey","forceRefresh","jsonResponseWithETag","reports","pageQueryStarted","resolvePage","rejectPage","pagePromise","reject","fullArrayPromise","fullItems","computeIdsStreamSnapshotToken","resumable","lineIterator","winner","emittedProvisionalPage","pageItems","param","parsedId","detail","params","sanitizedCookie","endpoint","handler","startTime","elapsed","err","respond","actionStartTime","formData","intent","reportHubIdRaw","businessDate","operationTimestamp","clientTempId","runStatusToggle","intentName","fieldName","apply","updatedStatus","newReport","title","content","publishedReport","newComment","safeComment","commentId","sessionHeaders","extra","internalUserId","initialScope","visibleSet","commentableSet","keepAliveTick","resolveGeneration","lastEventId","controllerRef","keepAliveInterval","unsubscribe","isCleaningUp","cleanup","emitAnchorOnly","entryId","processEntry","fields","result","dailyReportSseMessageSchema","recipientRawUserId","redacted","sanitizedMessage","_","rest","generation","next","lastProcessedId","firstLiveEntryId","entry","isStreamIdLte","client","latest","connectedPayload","sseMessage","catchUpEntries","fail","status","message","retryAfterSeconds","codec","readAttachment","token","attachmentId","attachment","gateReleased","releaseGate","releaseDeferredToStream","isHead","declared","inlineSafe","wantsDownload","contentType","disposition","contentLength","import_mssql","import_drizzle_orm","cols","formatDateValue","value","pattern","dateValue","year","yyyy","mm","dd","hh","mi","ss","createDailyReportService","config","db","tables","users","resolveUserId","encodeUserId","redis","sqlResultCache","epochs","DailyReportHub","DailyReportInternal","DailyReportCommentModel","DailyReportLabel","DailyReportHub_Label","DailyReportUserStatus","externalSources","draftLabelNames","draftLabelName","logger","createLogger","inTransaction","fn","capture","tx","DAILY_REPORT_IDS_CACHE_KEY","DAILY_REPORT_IDS_TTL_MS","DAILY_REPORT_IDS_EPOCH_KEY","DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX","DAILY_REPORT_BUSINESS_DATE_TTL_MS","DAILY_REPORT_DATE_EPOCH_PREFIX","DAILY_REPORT_DETAIL_EPOCH_PREFIX","DAILY_REPORT_SSE_STREAM_KEY","DAILY_REPORT_SSE_STREAM_MAXLEN","incrementRedisEpoch","key","visibleSourceTypeCondition","visible","buildVisibleSourceTypeCondition","idsCacheKey","userId","digest","idsUserPrefix","dateCacheKey","normalizedBusinessDate","dateUserPrefix","detailCacheKey","reportHubId","detailUserPrefix","publishToSseStream","message","callerName","client","publishStartMs","publishDurationMs","e","maskAuditActor","externalSelections","out","adapter","applyExternalJoins","chain","c","mapHubRecord","scope","row","currentUserId","hub","internal","content","interviewers","category","creationCategory","visitTimeFrom","visitTimeTo","customerName","employeeName","externalComments","a","externalRow","fields","labelsRaw","l","postedCommentsRaw","commentsVisible","isSourceTypeVisible","isCreationCategoryDraft","cachedPrimaryDraftLabelId","cachedDraftLabelIds","fetchDailyReportIdsByUserId","DraftLabelRelation","draftLabelJoinIds","getDraftLabelJoinIds","item","getDailyReportIdsFirstPageByExternalId","externalId","limit","getUserIdByExternalId","deniesEverySourceType","fetchDailyReportsByBusinessDate","rows","attachmentsMap","getAttachmentsByHubIds","r","buildBusinessDateCacheKey","userIdCache","getDailyReportIdsByExternalId","forceRefresh","snapshot","ttlMsOverride","onFetchStart","cacheKey","computeViewerScopeDigest","res","isIdsStreamOrdered","compareIdsStreamOrder","getDailyReportsByBusinessDate","businessDate","normalizeBusinessDateKey","getDraftLabelIds","ids","getDraftLabelId","primary","deriveAttachmentState","checkedAt","missingAt","hubIds","map","attachment","codec","list","getAttachmentForUser","attachmentId","markAttachmentMissing","now","error","markAttachmentPresent","getDailyReportsByBusinessDateByExternalId","allLabels","allComments","labelsMap","commentsMap","label","comment","hubId","getDailyReportDetailById","labels","comments","labelsMapped","commentsMapped","getDailyReportDetailByIdByExternalId","options","findHubGateColumns","assertHubMutable","setStarStatus","isStarred","clientTempId","existing","updatedStatus","normalizedDate","statusUpdateMessageSchema","setReadStatus","isRead","resolveCommentRecipient","ownerUserId","draftIds","rel","addComment","recipientRawUserId","inserted","user","userName","commentItem","commentAddMessageSchema","findDailyReportCommentById","commentId","deleteComment","hubForDelete","commentDeleteMessageSchema","createDailyReport","result","sourceId","draftLabelId","fullDetail","DailyReportViewerScope","draftLabelIds","isDraft","reportCreateMessageSchema","findDailyReportHubById","updateDailyReportHub","data","updateDailyReportInternal","deleteDailyReportLabel","labelId","businessDates","normalized","report","editedBusinessDate","reportUpdateMessageSchema","reportPublishMessageSchema","reportDeleteMessageSchema","import_drizzle_orm","defineDailyReportAuthzResources","sources","list","targetSources","src","resourceKey","seedDailyReportAuthzResources","db","authzTables","opts","d","TMResource","appKey","actor","now","resourceList","res","existing","defaultLogger","createLogger","transformJsonArray","payload","label","mapper","logger","parsed","entry","error","import_drizzle_orm","import_mssql_core","defineDailyReportSchema","schemaName","opts","s","users","hub","table","internal","comment","label","hubLabel","userStatus","attachment","createDailyReportServer","config","epochs","createEpochStore","cache","SqlResultCache","service","createDailyReportService","sseReader","DailyReportSseReader","handlers","createDailyReportHandlers","source"]}
|