@aiquants/daily-report 0.41.0 → 0.43.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/CHANGELOG.md CHANGED
@@ -2,6 +2,311 @@
2
2
 
3
3
  All notable changes to `@aiquants/daily-report` are documented here.
4
4
 
5
+ ## 0.43.0 (2026-10-14)
6
+
7
+ ### Added
8
+
9
+ - **クライアントの入口の区分のトークンの畳み方** `foldDailyReportSourceTypeToken` (共有の入口と同じ関数): ブラウザーへ配る経路のモジュールは
10
+ `@aiquants/daily-report/client` から読む。共有の入口は束を分けないので、そこから読んだ経路のモジュールは共有のモジュールの 2 つ目の写しを経路のチャンクへ
11
+ 持ち込む (経路のチャンクごとに 3,992 B、gzip で 1,691 B)。
12
+ - **ストリームへ書く形の SSE のメッセージのスキーマと型** `dailyReportSseStreamMessageSchema`・`DailyReportSseStreamMessage` (server エントリ): 共有リーダーが読んだ
13
+ エントリの値 (`DailyReportSseEntryMessage.parsed`) はこの形 (下の Breaking)。
14
+ - **E2E の試験の口の行の 2 段目の描画の数** `rowPass.counters()` (`DailyReportViewTestHandle`。両ビューの `__virtualScroll`): ビューのマウントからの送り手の 4 つの数を
15
+ 読んだ時点の値で返す (`syncReleases`: 確定か知らせのマイクロタスクの確定の中で同期的に描いた単位・`oversizeFrames`: 最初の単位だけで分け前を超えたキーの無い
16
+ フレーム・`lateFrames`: 飛ばした遅れたキーの無いフレーム・`lateForcedFrames`: 遅れていても単位を当てはめたキーの無いフレーム)。ホストの性能の計測は押し続けの
17
+ 前と後に読んで差を取り、送り手の定数も規則も写さない。型だけの公開なのでバンドルは増えない。
18
+
19
+ ### Changed
20
+
21
+ - **検索窓の既定の入力例を短くした**: `searchPlaceholder` の既定を `検索` (en: `Search`) にした。320 px の画面の入力欄 (全角 4 字ぶん) でも切れない。
22
+ 名前 (`searchInputLabel`) は `日報を検索` (en: `Search reports`) のまま。
23
+ - **キーを押し続ける間も、行の単位はキーの無いフレームで 1 つの確定として描く** (行の 2 段目の描画): 殻の行の本体・カードの差し替え・検索の印の変化は、
24
+ どれもビューに 1 つの送り手が自分のフレームのコールバックの中で 1 つの確定 (`flushSync`) として当てはめ、そのフレームの間隔がキーの仕事を持たないときだけ
25
+ 当てはめる: 前のコールバックの後にビューのウィンドウへ keydown か keyup が配られず (受動の捕獲段の聞き手が数えるだけで、DOM を読まない)、長押しの流れが
26
+ フレームのコールバックで段を当てはめず、流れが溜めた段を持たないフレーム。送り手が自分のフレームで当てはめる単位は押し続けの外の 1 回の押下でも同じ規則で、
27
+ キーの描くフレームに載ることは無い (単位を確定する場所はほかに 2 つで、見えるようになった行の単位は見えるようにした確定の中で、押し続けの外で見えている行へ
28
+ 届いた入力は届いた確定か知らせた処理の終わりの確定で描く。下の「行は 1 つの入力の元から単位でだけ描く」)。
29
+ 眠った送り手が起きた後の最初のコールバックは測るだけ。0.42.0 の、次の繰り返しの keydown を見込んで `MessageChannel` の枠で当てはめる形はやめた。
30
+ - **単位の仕事はそのフレームで終わる**: DetailList の行の高さの監視は、配達を 1 つの同期の確定で確定するので、本体の高さ・VirtualScroll の位置 (可視の先頭より
31
+ 上の行の補正を含む)・遠い行を飛ばしてよいかの宣言は、本体を配置したフレームの中で決まり、次のキーのフレームで置き直さない。配達の確定が見せた行と
32
+ 箱を変えた本体は配達が終わる前に測って確定し、観測はビューの次のフレームのコールバックで始める (配達の中で観測を始めないので、
33
+ `ResizeObserver loop completed with undelivered notifications` を出さない)。行の高さの配達はビューに 1 つで、スクロール領域が付くところで行の 2 段目の描画と
34
+ 一緒にビューのウィンドウへつなぐので、観測器もフレームの時計もビューが描かれるウィンドウのもの (iframe の中のビューも、その iframe の観測器と時計を使う)。
35
+ - **フレームごとの単位の数は測った費用で決める**: 最初の単位はいつも、次の単位はその種類の測った費用 (最新 32 回の p90。8 回に満たなければ足さない) の和が
36
+ フレームの間隔の半分とフレームの残りに収まる間だけ当てはめる。費用は行の箱 (List の決まった箱・DetailList の測る本体) と単位の変化 (殻の上の本体・箱を
37
+ 変えうる変化・印だけ) の組の 6 つの種類ごとに測るので、安い印の単位がカードの見積もりを下げない。今の費用では DetailList はフレームにカード 1 枚、List は 1〜3 行。
38
+ - **遅れて始まったフレームは飛ばす**: コールバックの前に GC や長い処理が走ったフレームは、最初の単位の見積もりがフレームの残り (間隔から余白 2 ms と遅れを
39
+ 引いた時間) を超えれば当てはめずに次のキーの無いフレームを待つ。見える帯か届く帯の単位が待つ遅れたフレームは続けて 1 つまで。
40
+ - **押し続けの間は進む向きの先の行を先に描く**: 遠い帯の行のうち、進む向きの前のものは近い帯の後に距離の順に描き (押し続けが届く帯へ入れる行は先に本体を
41
+ 持つ)、後ろのものは押し続けが終わってから描く。同じ順位の単位は先に加えたものが先 (下の Fixed)。
42
+ - **行は 1 つの入力の元から単位でだけ描く**: 両ビューの行コンテナは、日報の詳細と検索の印を行ごとの入力の元から殻のマウントの時点で購読し、届いた値を行の単位で
43
+ 描く (届いた答えが行コンテナをその場で描き直さない)。見えている行へ届いた値は、押し続けの外なら届いた確定か知らせた処理の終わりの 1 つの確定で描く。
44
+ - **日報の詳細は日報ごとに 1 つの記録を分け合う**: 詳細の記録と読み込みは、画面の API の基点と詳細の読み方ごとに 1 つの置き場が日報 id ごとに持ち、id の最初の
45
+ 読み手が読み込みを始め、最後の読み手が止める (止めた後に届いた答えは捨てる)。キャッシュの書き込みはその id の読み手にだけ、答え 1 つに 1 回知らせる。
46
+ `useDailyReportDetail(reportHubId, businessDate)` は引数と返す値をそのままに、置き場の記録を既定の優先度の状態で返す。同じ日報を描く行と側面ペインは記録を
47
+ 分け合うので、どちらかが読む間は取り直しを重ねず、同じ読み込みを待った読み手は誤りも 1 回ずつ受ける。分け合うのは決着した日報で、決着した読み込みの誤りは
48
+ 分け合わない: 誤りを持つ記録へ後から加わった読み手 (載った行・入力の先導の帯に入った id・側面ペイン) は頼み直し、答えが誤りを消す。頼み直すのは新しい購読だけ
49
+ (描き直しと同じ聞き手の 2 度目の購読では頼み直さないので、誤りを描き続ける行が頼み直しを繰り返さない) で、閲覧者のセッションの断り (401・403) は日報と同じく
50
+ 分け合って頼み直さない。鮮度も新しい読み手が加わるときに確かめる: キャッシュの項目が期限切れになった記録へ、答えを待っていないときに新しい読み手が加わると
51
+ (日報を選び直して作り直される側面ペイン・載った行・先導の帯に入った id)、行が記録を開いたままでも `DAILY_REPORT_FETCH_DEBOUNCE_MS` の後に取り直す (期限切れの
52
+ 日報を描いたままで、答えまでに加わる読み手は 1 本の要求を分け合う)。描いている日報の取り直しが一時の失敗で終わっても日報を誤りに替えず (0.42.0 は読み手が誤りを
53
+ 描いた)、`[DailyReportDetailLoader]` へ警告を書いて、次の新しい読み手がまた取り直す。セッションの断りは描いている日報の上でも誤りになる。
54
+ - **検索の答えを見せている間、ビューは行の入力を先に読む**: 描いている行と、描いている範囲の両端の先の 24〜40 行の詳細と印を置き場へ購読させておく (進む側が
55
+ 24 行を切ると 8 行ずつ伸ばし、跳んだ後は両側 32 行)。押し続けの間に載る行は詳細と印を置き場に見つけ、最初の本体から印つきのカードを描く。押し続けの間の
56
+ 要求は置き場ごとに 40 回のキーで 4〜5 本、跳んだ後は置き場ごとに 1 本で、描いている窓と 2 × 32 行の 95〜96 件を運ぶ (描いている窓だけの約 31 件から増える)。
57
+ 借りは置き場ごとに持ち、置き場が替わると替わった置き場の購読だけを返す: 新しい答え (印の置き場の替わり) は印だけを借り直し、詳細の借りと送っている詳細の
58
+ 要求はそのまま (同じ ID をもう一度頼まない)。営業日で読む一覧は今までどおり営業日の先読み (`useDailyReportPrefetch`)。
59
+ - **検索の印は行の単位の確定で描く**: 印を知っているカードは最初の確定から印つきで描き (素の文字を塗ってから印を塗り直さない)、届いた印は行の 1 つの単位で描く
60
+ (印だけの変化は行の箱を変えないので、行は飛ばしてよいまま)。一致の説明は、カードを確定した処理のうちに揃う。印の葉は配置の効果 1 つで登録し、集計の登録は
61
+ 札を返して札の登録だけを消す。側面ペインは今までどおり、最初の描画を素の文字で描き、印を後追いの描画で塗る。組み立てた CSS は 0.42.0 と同じで、行の落ち着いた DOM は
62
+ 0.42.0 と画面に出さない文だけが違う (下の 2 つの項: カードの無い行の説明と List のカードの一致の説明)。
63
+ - 性能の試験 (9 つの条件、30 ms の繰り返しで 40 回のキーの押し続け、各 3 回): キーの届く範囲の内の keydown とキーのフレームの中で描いた行の本体は 0 (0.42.0 は
64
+ 実行ごとに keydown の中で 0〜7、キーのフレームで 2〜7)、キーの仕事と同じフレームで確定した単位 0、予算を超えたフレームは、0.42.0 の計測の数え方 (描画の段から次の描画の段まで) で
65
+ DetailList の行 110・16,000 行・80,000 行から (答えを見せたまま) 1 / 4 / 2 → 0 / 0 / 2、今の数え方 (ブラウザーのフレームごとに、vsync か前の確定から自分の
66
+ 確定まで。vsync の前の GC はそのフレームに入らない) で 0.43.0 は 0 / 0 / 1、表示の遅れの p95 は 25.57 / 26.28 ms → 20.52 / 21.95 ms (80,000 行からは 26.35 → 26.68 ms で、
67
+ キーのフレームの層の描き直しが決める)、キーの処理の時間の p95 は最大 3.85 ms、見える範囲へ入った行がカードを待つ時間 (0.42.0 の最大 331.5 ms) は 0。
68
+ CPU を 4 倍遅くした押し続けでは、見える範囲へ入るどの行もその確定でカードと印まで描き、次のフレームまでの p95 は 108.6 ms (0.42.0 は 50.2 ms。キーの
69
+ フレームそのものの描画が決める)。
70
+ - **要求の隔離は `@aiquants/request-isolation` の判定**: 別オリジンの判定 (`crossOriginRefusal`) を共有のパッケージから読む (依存 `@aiquants/request-isolation`
71
+ `^0.1.0` が加わる)。どの要求への答えも変わらない。拒否の記録の行は、判定が拒んだ理由を持つ (`reason=cross-origin-fetch` は Fetch Metadata が別のオリジンを
72
+ 示した要求、`reason=foreign-origin` は Fetch Metadata が無く、`Origin` が宛先のホストを名乗らない安全でないメソッドの要求。下の Breaking)。
73
+ - **自分の作成・更新・公開を、自分のほかのタブと端末は自分の既読とスターのまま受け取る**: 日報を埋め込む SSE の知らせ (`report-create`・`report-update`・
74
+ `report-publish`) は、閲覧者がその書き込みの操作者か (`viewerIsActor`) を接続ごとに持つ。クライアントは真なら知らせの既読とスターを自分の書き込みの答えとして
75
+ 書き (作った日報は既読)、偽なら今までどおりほかの閲覧者の書き込みとして閲覧者ごとの欄を手元の値か偽に合わせる。操作者の内部 ID は閲覧者へ届かない
76
+ (下の Breaking)。
77
+ - **作成の答えは読み直した日報だけ**: `createDailyReport` は確定の後に日報を読み直せなければ、書いた値から組んだ日報で答えず、名前の付いた誤りで止まる
78
+ (下の Breaking)。
79
+ - **カードの無い行は描いている状態で説明する**: 殻とカードの無い行は今までどおり営業日の ISO 表記で名付け、目に見える状態をアクティブな子孫の読み上げへ届ける
80
+ (`aria-describedby`): 読み込みに失敗した行は誤りの段落 (List は `reportSummaryLoadFailed`、DetailList は `reportLoadFailed`)、List のサーバーに無かった行は
81
+ 不在の段落 (`reportNotFound`)、DetailList の編集フォームの行 (表示中ユーザーの下書きが自動で開く編集を含む) は画面に出さない編集の状態 (新しいラベル
82
+ `rowEditingState({ isDraft })`: en `Editing draft` / `Editing`、ja 「下書きを編集中」 / 「編集中」。位置を外れた要素なので行の高さは変わらない)。殻と読み込み中の
83
+ スケルトンは説明を持たない。0.42.0 はどれも日付だけを読ませた。固有キーは 110 から 111 (文字列のキー 89・整形関数 22。キーの表は全 122 キー。下の Breaking)。
84
+ カードと誤りの面はどちらも単位の写しから描くので、単位を待つ間に届いた読み込みの誤りは、単位が描くまで行をスケルトンのまま読み込み中にする (「見つかりません」を
85
+ 挟まない)。
86
+ - **List のカードの一致の説明は DetailList の一致の印と同じ文を言う**: 検索の答えを見せている間の List のカードの隠した一致の説明は、同じ答えの下の同じ日報について
87
+ DetailList の行の一致の印と同じ 1 つの規則で、当たったキーワードの数の文 (2 つ以上のキーワードの OR)・当たりを描いた欄の文・表示外の文を言う (カードの葉が当たりを
88
+ 1 つも描かない日報、たとえばプレビューの外の本文にだけ当たった日報は、表示外の文)。0.42.0 の List は当たりを描いた欄の文だけで、答えの行の多くが何も言わなかった。
89
+ 見える印は持たず、要素は箱を持たないので行の高さは変わらない。
90
+ - **開発者向け**:
91
+ - `publish:*` は verify の後に基準を書き (`--write`)、ビルドし直してから `--exact` と漏洩の検査を走らせる (検査は `--exact` が測ったビルドを読む)。公開の配線の
92
+ 検査は、最後のビルドから漏洩の検査までに読み取り専用の検査だけを認める。
93
+ - 押し続けの下の行の 2 段目の描画は偽の時計の spec (`rows/hold-schedule.spec.ts`。本物の送り手と選択の置き場と長押しの流れ) が、行の高さの配達
94
+ (`rows/row-height-delivery.ts`) とその落ち着かせる範囲は仕様に沿った偽の ResizeObserver (`test-helpers/fake-resize-observer.ts`。偽物の規則そのものも
95
+ `test-helpers/fake-resize-observer.spec.ts` が確かめる) とテストが回すフレームのウィンドウの上の `rows/row-height-delivery.spec.ts` が確かめ、行の落ち着いた DOM は
96
+ 0.42.0 の写し (`components/__fixtures__/settled-rows-0.42.0.json`) に意図した変更 (画面に出さない文だけ) を当てたものと比べる。
97
+ - 送り手のフレームのコールバックの名前 (`rows/row-pass.ts` の `runFrame`。ホストの性能の計測が source map で単位のフレームを数える名前) は
98
+ `rows/row-pass.guard.spec.ts` が守る (名前を変える・移す・包むと、性能の計測の後ではなくパッケージのテストで落ちる)。
99
+
100
+ ### Fixed
101
+
102
+ - **同じ順位の待つ単位は先に加えたものが先**: 待つ単位の順位を位ごとに比べる。0.42.0 は一覧を去った日報の単位 (距離が分からない) どうしを NaN で比べ、後に
103
+ 加えたものを先にした。
104
+
105
+ ### Breaking
106
+
107
+ - **閲覧者へ届く日報の知らせは `viewerIsActor` を必ず持つ**: `DailyReportSseMessage` (共有の入口) の `report-create`・`report-update`・`report-publish` は
108
+ `viewerIsActor: boolean` を持ち、`dailyReportSseMessageSchema` はそれの無い知らせを通さない。
109
+ Migration: 日報の知らせを組み立てて SSE のハンドラーやクライアントへ渡すホストの試験は、`viewerIsActor` を足す (ほかの閲覧者の書き込みなら `false`)。
110
+ - **ストリームのエントリを読んだ値はストリームへ書く形**: `DailyReportSseEntryMessage.parsed` は `DailyReportSseStreamMessage` (日報を埋め込む 3 種別はサーバーだけが
111
+ 読む `actorRawUserId` を必ず持ち、`viewerIsActor` を持たない)。
112
+ Migration: 自分の共有リーダー (`DailyReportSseReaderPort`) を渡すホストは、エントリを `dailyReportSseStreamMessageSchema` で読む (server エントリの
113
+ `readDailyReportSseStreamEntry` がこの読み方をする)。ストリームへ直接書くホスト (結合試験など) は、日報を埋め込む知らせに書き込みの操作者の内部 ID を
114
+ `actorRawUserId` で書く。
115
+ - **SSE のワイヤの版は v4**: ストリームのキーは `<streamKey>:v4` で、0.42.0 が書いた v3 のエントリは読まない (開き直したクライアントは 1 度だけ一覧を合わせ直す)。
116
+ Migration: 同じストリームを読み書きするサーバーは同じ版へ揃えて配備する (版の違うサーバーは別のキーへ書き、互いのエントリを配らない)。v3 のストリームは
117
+ 書かれなくなるので、手で消してよい。
118
+ - **拒否の記録の行は `reason=` を持つ**: `cross_site_rejected route=<経路> reason=<cross-origin-fetch|foreign-origin> site=… mode=… dest=… method=…`。
119
+ Migration: 行を読むログの検索や監視は、`route=` の後の `reason=` を受ける。
120
+ - **作成が読み直せなければ誤りで止まる**: `createDailyReport` は、確定の後の知らせの中身の読み取りが日報を見つけなければ
121
+ `[daily-report] createDailyReport: report <id> was committed but its detail read returned nothing` (何も知らせない)、操作者の範囲の読み直しが日報を返さなければ
122
+ `[daily-report] createDailyReport: the actor's scope cannot read report <id> it just created` (知らせの後) を投げる。どちらも確定の後の競合か欠陥で、作成の関門を
123
+ 通った作成者は自分の下書きを読める。
124
+ Migration: 手で書いた db の代役でサービスの作成を試す試験は、作成の後の 2 つの読み取り (知らせの中身と操作者の範囲) に作った日報を答える。
125
+ - **閲覧者へ届く SSE の知らせは宛先を持たない**: `DailyReportSseMessage` (共有の入口) と `dailyReportSseMessageSchema` のどの種別も `recipientRawUserId` を宣言しない
126
+ (0.42.0 は `status-update`・`comment-add`・`comment-delete` と日報を埋め込む 3 種別が持った)。宛先はストリームへ書く形 (`DailyReportSseStreamMessage`・
127
+ `dailyReportSseStreamMessageSchema`) だけが持ち、`status-update`・`comment-add`・`comment-delete` も宛先を持つ書く形のスキーマで読む。届く形で読んだ値は宛先を落とす。
128
+ Migration: 知らせの宛先を読むホストのコード (自分の宛先の絞り込みなど) は、書く形 (`DailyReportSseStreamMessage`) から読む。ストリームへ直接書くホストは
129
+ 今までどおり宛先を書く (書く形が受ける)。
130
+ - **`DailyReportSseEntryMessage` は `text` を持たない**: 共有リーダーが読んだエントリの値は、書く形のスキーマが検証したメッセージ (`parsed`) と書かれたままの JSON の値
131
+ (`published`。宛先の絞り込みだけが読む) の 2 つ。
132
+ Migration: 自分の共有リーダーを渡すホストは、エントリの値に `text` を作らない (server エントリの `readDailyReportSseStreamEntry` の読み方)。
133
+ - **閲覧者へ書く SSE のフレームは、読んだメッセージから組んだ届く形の鍵だけの 1 行**: どのフレームも、書く形のスキーマが読んだメッセージからサーバーだけが読む欄
134
+ (`recipientRawUserId`・`actorRawUserId`) を除いて、届く形のメッセージとして組んだ JSON。書く形のスキーマが知らない鍵は届かず、書かれた値が整形の改行を持っていても
135
+ フレームは 1 行 (0.42.0 は宛先もコメントの秘匿も要らない知らせを書かれた字面のまま送り、ほかは書かれた値から宛先を除いて送ったので、知らない鍵も届いた)。
136
+ Migration: 知らせに欄を足すホストは、書く形と届く形の両方のスキーマに足す (書く形にだけ足した欄は閲覧者へ届かない)。届く形のスキーマは今までどおり知らない鍵を
137
+ 拒まずに落とすので、長く開いたタブは新しいサーバーが欄を足した知らせも読み続ける。
138
+ - **client の型 `DailyReportLabels` に必須のキー `rowEditingState({ isDraft })` が増えた** (上の Changed の「カードの無い行は描いている状態で説明する」)。
139
+ Migration: `DailyReportLabels` を丸ごと組むホスト (試験の代役など) はこのキーも書く。上書き (`DailyReportLabelOverrides`) は部分のままでよい。
140
+
141
+ ### Known issues
142
+
143
+ - **速いホイールの往復**: 描いている窓のずっと先まで 1 度にホイールで送り (DetailList で 20,000 px など)、同じ量で戻すと、始めの位置から 644〜1,116 px ずれた所に着く
144
+ (0.42.0 は始めの位置へ戻った)。送る間に殻のまま通り過ぎた行が後でペインの上で描かれて、測る前の見積もり (352 px。測ったどの行より低い) から測った高さへ伸び、
145
+ VirtualScroll の位置の補正が見えている行をその場に保つため (見えている行は動かない)。次の版で、2 段目の描画の順をペインの進む向きの距離で決め、測る前の見積もりを
146
+ 測った高さから導く。
147
+ - **遅い端末での押し続け**: CPU を 4 倍遅くすると、キーを押し続ける間の描画は約 9 枚/秒 (次のフレームまでの p95 は 108.6 ms で、目標の 33 ms を満たさない。
148
+ 0.42.0 は 50.2 ms)。どの行も見える範囲へ入るその確定でカードと印まで描き終える。時間を決めるのはキーのフレームそのもののスタイル・配置・塗りで、キー 1 つの
149
+ 仕事を安くするのは次の版の作業。
150
+
151
+ ## 0.42.0 (2026-10-12)
152
+
153
+ ### Added
154
+
155
+ - **可視のポートの記憶の部品 `createDailyReportVisibilityCache({ ttlMs, readViewerKey, resolve })`** (server エントリ。設定の型 `DailyReportVisibilityCacheConfig`、上限
156
+ `DAILY_REPORT_VISIBILITY_MAX_CACHE_TTL_MS` = 60,000): ホストの問い合わせを包み、ポートの記憶の規則を持つ可視のポートを返す。閲覧者のキーごとに答えを `ttlMs` (1〜60,000 ms の整数。
157
+ ほかは `RangeError`) の間使い回し (単調な時計)、記憶を読むのは `initial` だけで、`refresh` は問い合わせ直して書き戻し、並んだ問い合わせを共有し、失敗は覚えず (後から書いた答えは残す)、
158
+ 閲覧者を名指さない要求は問い合わせる前に投げる。ホストが文章から書き写した記憶はこれに置き換えられる。
159
+ - **区分の語彙** (共有エントリ): パッケージ自身の区分 `DAILY_REPORT_INTERNAL_SOURCE_TYPE` (`"Internal"`) と、可視のポートが返すトークンをサーバーが畳む関数
160
+ `foldDailyReportSourceTypeToken` (前後の空白を除いて小文字)。トークンを組むホストと起動のときの照合は、綴りと畳み方を書き写さずにこれを読む。
161
+ - **明示の opt-out の型 `DailyReportVisibilityOptOut`** (`{ unrestricted: <理由> }`。server エントリ。下の Breaking)。
162
+
163
+ ### Changed
164
+
165
+ - **キーは DOM のフォーカスを動かさず、アクティブな行を動かす** (下の Breaking): 両ビューの複合ウィジェット (VirtualScroll のコンテンツ要素。`role="treegrid"`・`tabIndex=0`) がビューのただ 1 つの
166
+ Tab の止まり先で、キーの間 DOM のフォーカスはそこに残る。キーが動かすのはアクティブな行 (支援技術のフォーカスが載る行) で、複合ウィジェットの `aria-activedescendant` は
167
+ `@aiquants/virtualscroll` の `activeItemIndex` を通して、名指す行と同じ描画で描かれる (行が描画窓の外へ出ても留め置かれるので、参照はいつも解決する)。キーの処理は `focus()` を呼ばず、
168
+ DOM の位置も読まない (キーの起点はアクティブな行の記録)。
169
+ - 複合ウィジェットでの Enter / Space は、両ビューでアクティブな行を起動する: List はカードの主ボタンの押下と同じ (モバイルの配置ではオーバーレイも開く)、DetailList はスクロールせずに選ぶ。
170
+ 既定動作はアクティブな行が無くてもいつも止める (ページはスクロールしない)。
171
+ - アクティブな行の中の操作部品 (★・既読・編集・削除・添付の繋がり・コメントの欄) で押した移動のキーは、その行から動かし、フォーカスを複合ウィジェットへ 1 度だけ戻す (続く繰り返しは
172
+ 複合ウィジェットへ届く)。Escape はアクティブな行を変えずに複合ウィジェットへ戻す。行の中の編集とコメントのフォームの入力欄とボタンからも戻り (打った文字は欄と編集の置き場に残る)、
173
+ 扱わないのは既定動作が止まった・IME の変換の間・Escape が自分の意味を持つ要素 (`select`・検索の欄・編集可能要素・操作部品を持つ再生の要素) の中・行までの祖先が
174
+ `aria-expanded="true"` のときだけ。移動のキーと出口は入力の領域の中では今までどおり扱わない。
175
+ - Tab は複合ウィジェットからアクティブな行の操作部品へ DOM の順に進み、最後の部品の次はビューの外へ出る。Tab は選択しない。既定の動作がアクティブな行の中に着き、その行が見えていなければ、
176
+ Tab と Shift+Tab の keydown の中で行を近いほうの端で見せてから既定動作に渡す (Chromium は `focusin` より前に焦点のスクロールを決めるので、`focusin` で見せても文書がスクロールする)。
177
+ 着く先はパッケージの Tab 順の模型で決め (`tabLandsInRow`)、ビューの外へ出る Tab と Shift+Tab は何も見せない (一覧は利用者が離れた位置のまま)。
178
+ - ビューの外から複合ウィジェットへ入ると、マウスかペンで押した行がアクティブになり (押した行は見せ直さない)、それ以外 (Tab) は見えているアクティブな行・見えている選択中の日報・
179
+ 可視の先頭の行の順で、一部しか見えていなければ近いほうの端で見せる。フォーカスがビューの外の要素へ出たとき (Tab・Shift+Tab・Ctrl+Home / Ctrl+End の出口・ビューの外の押下) と、
180
+ ビューの外にフォーカスがある間の可視範囲の変化も同じ規則でアクティブな行を選び直す (逆向きの Tab で入っても見えている行の操作部品に着く)。押下の行の記録は、ビューの中のどの
181
+ `focusin` と扱ったどのキーでも捨てる (ビューが自分で動かしたフォーカスは、中からの入りとして扱う)。
182
+ - ビューへフォーカスを戻す受け渡し (オーバーレイを閉じた・アクティブな行やコメント節が消えた・フォーカスを持つ操作部品がほかの理由で行から外れた・ページの状態の画面から本体への引き継ぎ) は、
183
+ 行をアクティブにして近いほうの端で見せ、最後の入力の様式で行き先を決める: キーボード (と入力の記録が無いとき) は複合ウィジェット、ポインターとタッチは行そのもののフォーカスを受ける要素
184
+ (`data-daily-report-row-focus`: List のカードの主ボタン、DetailList のカードの画面に出さない名前の見出し (`tabIndex=-1`)。カードの無い行では複合ウィジェット。
185
+ iOS VoiceOver・TalkBack・スイッチ操作は DOM のフォーカスを追う)。利用者がよそに置いたフォーカスは奪わない。
186
+ - 複合ウィジェットがキーボードのフォーカスを持つ間、アクティブな行が見えていればその行のアウトライン、行が無いか見えていなければ VirtualScroll のペインの 2 px の輪が印になる。
187
+ 表示域の縁に切られた行は、その縁で 4 px (G − アウトラインの offset) より多く見えるときだけ見えていると数える (`activeItemVisibleInset`) ので、アウトラインが欠ける重なりでは輪が残る。
188
+ 輪の色は VirtualScroll の変数 `--aqvs-focus-ring-color` を行のアウトラインの色 (blue-600、暗色 blue-400) へ結んだもので、1 つのフォーカスは配色ごとに 1 つの色で描かれる。
189
+ - 行の無い一覧では、空の文言を複合ウィジェットの外 (同じ箱の上の層) に描き、複合ウィジェットの `aria-describedby` の先頭がそれを名指す (キーの説明の前)。区分を 1 つも見られない閲覧者の
190
+ 空のビューも、名前・空の文言・キーの説明で読まれる。
191
+ - コメントの入力欄の名前はコメントの節の見出し (`labels.comments`。`aria-labelledby`) で、置き文字は手がかりのまま。
192
+ - 性能の試験 (9 つの条件): 押し続けあたりの `focus()` は 16.4〜18.7 ms から 0 ms、keydown の配送と押し続けのフレームの中の強制したスタイルと配置は 0、キーの処理の時間の p95 は最大 4.34 ms、
193
+ どの実行も 40 キーで 40 行を動かした。Chromium のアクセシビリティの木で、40 回の ArrowDown のどれでも、両ビューのアクティブな行の関係・名前・説明・選択・位置が正しい。
194
+ - **行の 2 段のマウント**: 見える範囲の外でマウントする行は、まず行の枠と空の面 (殻。名前は営業日の ISO 表記で `aria-busy="true"`) だけを描き、ビューに 1 つの送り手が、フレームの描画の直後の枠
195
+ (MessageChannel の処理 1 つ) で行を 1 つずつ本体へ進める (1 つの単位が 1 つの確定)。順は見える・アクティブな行の届く範囲・近い・遠いの帯で、帯の中ではアクティブな行からの距離の順
196
+ (同じ距離なら進む向きの側を先)。枠ごとに単位は 1 つ、次の単位が見えるか届く範囲の行ならもう 1 つ。見えるようになった行は、見えるようにした確定の中で本体を描くので、見える行が殻のまま
197
+ 塗られることは無い。届く帯か近い帯に入った待つ行は眠った送り手を起こすので、キーが見せうる行はそのキーより前の枠で本体を持つ。後から届いた日報のカード (読み込みの答え・SSE の更新) も
198
+ 同じ送り手が載せ (`useDeferredValue` では遅らせない)、すでに見えている行へ届いたカードは、押し続けの外なら届いた確定の中で載る (見えている行は押し続けの外では古い描画のまま塗られない)。
199
+ キーの押し続けの間は、送り手がキーの描くフレームを避ける: 遠い帯の行は殻のままで届いたカードも載せず (離した後に載せる)、枠は次の繰り返しの keydown (押し続けの最新の 8 つの keydown の
200
+ 間隔の中央値と最も早い位相から見込む) がその確定を描くフレームより前に来ないときだけ走り (枠を予約するフレームのコールバックで決め、走る時点でもう一度確かめる)、枠の 2 つ目の単位は
201
+ どちらの行も決まった箱 (List) のときだけ。見えている行へ届いたカードも、押し続けの間はこの枠を待つ (見える帯の単位が最初)。殻の箱は VirtualScroll の行の高さなので、本体が来ても何も動かない。
202
+ - **遠い行は描画を飛ばす**: 見える範囲を両側へ表示域 1 つぶん広げた帯と、アクティブな行の届く範囲 (前後の行から表示域 1 つぶん。1 行か 2 行の 1 段が見せうるどの行も、行の高さによらず
203
+ ここに入る) の外にあるマウント済みの行は、寸法が決まっていれば `content-visibility: hidden` と `aria-hidden="true"` で描画を飛ばす (`@aiquants/virtualscroll` の `itemZones`)。
204
+ 飛ばした行は支援技術の木・ブラウズモード・スワイプの読み上げ・ページ内検索から外れる。アクティブな行・その届く範囲・見える範囲の前後 1 表示域は外れない。行の箱は変わらないので、
205
+ 飛ばしても見せても何も動かない。DetailList の行は本体を測るまで、コメントの一覧が変われば測り直すまで飛ばさない。`content-visibility` を持たないエンジン (Safari 18 より前) では
206
+ 何も飛ばさず、何も隠さない。性能の試験で、スクロールしたキーあたりの塗りの段 (p50) は DetailList が 3.85〜4.47 ms から 2.07〜2.46 ms、List が 3.13〜3.81 ms から 2.11〜2.49 ms。
207
+ - **選択の輪とアウトラインは行の枠の状態から描く**: 選択の輪と強制配色の選択の線は行の枠の `aria-selected="true"` から、キーボードのフォーカスのアウトラインは、ビューの根がキーボードの
208
+ フォーカスの属性 (`data-daily-report-keyboard-focus`) を持ち、祖先の複合ウィジェットが `:focus-visible` の間に、アクティブな行 (`data-daily-report-active`) の面へ描く。押下は
209
+ アウトラインを出さない。幾何・色・詳細度は変わらず、行の画素は 0.41.0 と同じ。どの選択子も `aria-activedescendant` を読まない (キーごとに変わる値で部分木を無効にしない)。List の
210
+ カードの面は、主ボタンがキーボードのフォーカスを持つ間のアウトラインを描き続ける (ポインターとタッチの受け渡しの後に Enter などを押したとき)。
211
+ - **キー操作の説明の文言** (`labels.listKeyboardHelp`): Enter または Space で選択、Tab でその日報の操作部品へ、Escape で日報の行へ戻ることを足した。ja は
212
+ 「↑↓ で前後の日報、Home / End で先頭・末尾、PageUp / PageDown でページ移動、Enter または Space で選択、Tab でその日報の操作部品へ、Escape で日報の行へ戻ります。Ctrl+Home / Ctrl+End で一覧の直前・直後の操作部品があればそこへ移ります。」、
213
+ en は「Up and Down arrows: previous or next report. Home and End: first or last report. Page Up and Page Down: move by a page. Enter or Space: select. Tab: the report's controls. Escape: back to the report row. Control+Home and Control+End: move to the nearest control before or after the list, when there is one.」。
214
+ 上書きしているホストは文言を見直す。
215
+ - **見出しのストリームの札は 1 つの枠に置き、枠の大きさは局面で変わらない**: `DailyReportIdsStreamStatus` は見出しの並びに 1 つの枠を持ち、枠の 1 つの格子の升に、見えない寸法の札
216
+ (どの局面の札も、いまの桁数で最も広い形にして重ねる。件数の箱 `CounterText` と同じ組み立て) と、いまの札を持つ入れ物を重ねる。枠はどの局面でも (何も見せない間も) 同じ大きさで
217
+ 配置されるので、札が現れても消えても局面が変わっても、見出しの並びの行の数と下の検索の帯は動かない (幅が変わるのは件数の桁数が変わるときだけ)。空の入れ物は描かず読み上げない
218
+ (`visibility: hidden`。配置は残す。以前は `display: none` で配置しなかった)。最新化中の表示は、ほかの局面と同じ 24px の丸い札 (無彩色) になった。札の文字は枠の始まりに揃う
219
+ (`text-start`。ホストの見出しの右寄せを受け継がないので、狭い局面の札の名前が点から離れない)。フォーカスを持ったまま空になった入れ物は、描かれたビューの複合ウィジェット
220
+ (無ければ選ばれたビューのタブ) へフォーカスを渡す。
221
+ - **閲覧を決められない要求は「今は答えられない」で答える** (下の Breaking): 可視のポートが投げたか契約の外で答えた要求は、空の一覧ではなく、JSON の経路と ids のストリームが読み取りの前に
222
+ 503 `{"error":{"message":"Report visibility is unavailable"}}` (`Retry-After: 5`・`Cache-Control: no-store`)、action が利用者の解決の前に 503 `{"error":"Report visibility is unavailable"}`、
223
+ 添付が混雑と同じ 503、SSE の接続が 503 で答え、開いている SSE の流れは定期の再解決が失敗すると、まだ配っていないエントリにアンカーを書かずに閉じる (ブラウザーが開き直し、追いつきが配る)。
224
+ 記録は `[DailyReportVisibility]` の `visibility unavailable cause=<port_failure|port_contract> reason=<initial|refresh>` で、原因ごとに 60 秒の窓の最初の 1 行と窓の終わりの数の 1 行だけ。
225
+ 空の答え (`[]`) は今までどおり本当の剥奪。
226
+ - **ids のストリームの欠けた行はバイト列のまま持つ**: NDJSON の行の分け方は、行の終わりを届いたバイト列から探し、終わっていない行をバイト列 (チャンクから写した欠片) のまま持ち、
227
+ 行が完結したときに 1 度だけ復号する。以前はチャンクごとに復号した字面を行の欠片として持ち、速さを抑えたストリームでは行の届き終わるまでヒープの文字列が若い世代のごみ集めを生き残って
228
+ 古い世代へ昇格し、押し続けの間の大きいごみ集めを早めた。
229
+ - **開発者向け**:
230
+ - キャッシュの寿命の大域の上書き (`__DAILY_REPORT_CACHE_TTL__`・`__DAILY_REPORT_CACHE_REVIVE_MS__`) は、モジュールの読み込みでなく最初に使う時に 1 度ずつ読む。
231
+ - 100 万単位の検索の文字の試験と 228,222 行の試験は、網羅率を測らない `guards` のプロジェクトの `*.scale.spec.ts` で走り、DOM を使わない client の spec は node の環境で走る。
232
+ - ソースを走査する 4 つの守り (枠線の線種・動きの方針・余白の段・Tailwind のセレクターの範囲) は `*.guard.spec.ts` (`border-style`・`motion-policy`・`spacing-ladder`・
233
+ `tailwind-selector-scope`) で、同じ `guards` のプロジェクトで走る。
234
+
235
+ ### Fixed
236
+
237
+ - **最初の既読とスターの書き込みが重なっても 500 にならない**: `setStarStatus` と `setReadStatus` は、閲覧者の状態の行を `MERGE … WITH (HOLDLOCK)` の 1 文 (`db.execute`) で書く。ある行は旗を
238
+ 更新し、無い行はもう一方の旗を偽にして入れ、書いた行を出力から読む。値の変わらない付け直しは行を 1 バイトも変えない (監査の列も)。以前は行を読んでから入れる 2 文で、同じ日報と閲覧者への
239
+ 最初の 2 つの書き込み (日報を開いた自動の既読とスターなど) が両方とも「行が無い」と読み、主キーで衝突して 500 になった。
240
+ - **論理削除した日報は公開できない**: `publishDailyReport` は削除した日報に Not Found (404) で答え、何も書かない (下書きのラベルを外さず、検索の索引も書かない)。以前は削除した日報の下書きの
241
+ ラベルを外して索引を書き直し、null を答えた。
242
+ - **作った日報は作成者に既読で読める**: `createDailyReport` は作成者の状態の行 (既読、スター無し、監査は作成者) を、日報と同じトランザクションの中で索引より前に入れる。以前は作成の答えだけが
243
+ 既読で、読み直すと未読だった。
244
+ - **公開の答えは操作者の閲覧の範囲を通る** (下の Breaking): 公開の答えは操作者の範囲で読み直した日報で、コメントの次元が除く区分の他人のコメント (自分の投稿は残る) と外部のコメントを持たない。
245
+ 以前は範囲を当てない読み取りをそのまま答え、区分の規則とコメントの規則の外のコメントまで返した。公開の知らせ (`report-publish`) は日報の全体を運び、SSE の配信が受け手ごとに絞る。
246
+ 作成の答えも操作者の範囲で読み直す。
247
+ - **前後に空白のある区分の値は、どの経路でも見えない**: SQL の区分の述語は小文字にした格納の値とトークンをバイト列で比べる (`CAST(LOWER(source_type) AS VARBINARY(2n)) IN (…)`、
248
+ n は列の宣言した長さ)。以前の `LOWER(source_type) IN (…)` は SQL Server の文字列の比較が末尾の空白を埋めて比べ (幅を区別しない照合順序では全角と半角も同じに扱う) ので、`kintone␠` の
249
+ 日報が SQL の読み取りでは `kintone` を見る閲覧者に見え、JS の判定 (SSE・書き込みの関門・添付) では誰にも見えなかった。列より長いトークンは一覧に入れない (切り詰めた頭が一致しない)。
250
+ - **関門の後に確定した削除の上には書かない**: 自分の日報の更新・公開とコメントの追加と削除は、トランザクションの最初の文で生きている hub の行に書き込みの鍵を取り (値を変えない更新。
251
+ 監査の列も書かない)、行を選ばなければ Not Found で巻き戻す。削除は自分の更新に同じ条件を持つ。以前は関門と書き込みの間に確定した削除の上へ公開と更新を書き、削除した日報の下書きの
252
+ ラベルを外して検索の索引を書き直した。
253
+ - **見えない日報のコメントの削除は 404**: `deleteComment` はコメント自身の日報の書き込みの関門を投稿者より先に通す。以前は本文の次元だけを見て、他人の下書きや論理削除した日報にある
254
+ ほかの利用者のコメントには 403、無いコメントには 404 を答え、答えの違いから存在が漏れた。論理削除した日報や見えない区分の日報のコメントは、投稿者自身にも Not Found。
255
+ - **コメントの入力欄は置き文字だけで名付けない**: 入力欄の名前はコメントの節の見出し (「コメント」。`aria-labelledby`) で、置き文字 (`labels.commentPlaceholder`) は手がかりのまま。
256
+ 以前は置き文字 (「コメントを追加...」) だけが名前で、見えている見出しと欄が結び付いていなかった (置き文字は打ち始めると見えなくなる)。
257
+
258
+ ### Breaking
259
+
260
+ - **行は `treegrid` の `row`、面は `gridcell`**: ビューのコンテンツ要素は `role="treegrid"` (`role="list"` ではない) で、名前は `labels.title`、`aria-rowcount` は行が属する集合の大きさ
261
+ (分からない間は −1)。行の枠は `role="row"` (`role="listitem"` ではない) で、`id` はビューの複合ウィジェットの id と日報 id から `virtualScrollItemElementId` で導く id、
262
+ `aria-rowindex` = `aria-posinset` = 位置 + 1、`aria-setsize`、`aria-level="1"` を持つ。行の面 (`data-daily-report-row-surface`) は `role="gridcell"` で、行の名前の要素を
263
+ `aria-labelledby` で参照する。カードの行の名前は名前の要素 (List はカードの主ボタン「営業日 作成者」、DetailList は画面に出さない日報の見出し「営業日 作成者 件名」)、説明は状態・描いた目印・
264
+ プレビュー (List)・一致の文で、集合の大きさが分からない間だけ位置の文言を最後に足す。殻とカードの無い行は営業日の ISO 表記を `aria-label` に持つ。
265
+ Migration: 行を `[role="listitem"]` や `[role="list"]` で探すホストの試験と CSS は `[data-daily-report-row]` (と `[role="row"]`・`[role="treegrid"]`) で探す。位置は `aria-rowindex`・
266
+ `aria-posinset`、集合は `aria-setsize`・`aria-rowcount` で読む。ARIA のスナップショットは treegrid > row > gridcell で取り直す。
267
+ - **選択は `aria-selected`**: どの行も `aria-selected="true"` か `"false"` を明示し、`aria-current` は行にも List のカードの主ボタンにも無い。アクティブな行だけが `data-daily-report-active` を持つ。
268
+ Migration: 選択を `aria-current="true"` で読む所は行の枠の `aria-selected="true"` で読む。アクティブな行は複合ウィジェットの `aria-activedescendant` (行の `id`) か `[data-daily-report-active]` で読む。
269
+ - **フォーカスは複合ウィジェットに残る**: キーはフォーカスを行へ移さない (上の Changed)。List のカードの主ボタンは Tab の止まり先でなく (`tabIndex=-1`。クリックの的とポインターの受け渡しの行き先で、
270
+ `data-daily-report-button` を保つ)、行の枠は `tabindex` を持たない。DetailList の行は `data-daily-report-button` も `tabindex` も持たず、フォーカスを受けない。アクティブな行の操作部品だけが
271
+ Tab の順に入る (ほかの行の部品は `tabIndex=-1`)。
272
+ Migration: 「選んだ行がフォーカスを持つ」を確かめる試験は、複合ウィジェットが `document.activeElement` で、その `aria-activedescendant` が選んだ行の `id` を名指し、行が
273
+ `aria-selected="true"` と `data-daily-report-active` を持つことを確かめる。DetailList の行は `[data-daily-report-button="<id>"]` でなく `[data-daily-report-row="<id>"]` で探す。
274
+ Tab の道筋を数える試験は、複合ウィジェット・アクティブな行の操作部品・ビューの外の順で数える。
275
+ - **ピアの `@aiquants/virtualscroll` は `^3.17.0`**: ビューは `activeItemIndex` (アクティブな行の `aria-activedescendant` と留め置き)・`activeItemVisibleInset`・`itemZones` と
276
+ `useVirtualScrollItem` (帯と遠い行の描画の飛ばし)・`virtualScrollItemElementId`・空の複合ウィジェットの説明・`--aqvs-focus-ring-color` を使う。ピアの下限は 3.17.0
277
+ (`peer-floors.json`)。Migration: `@aiquants/virtualscroll` を 3.17.0 以上へ上げる。
278
+ - **可視のポート `resolveVisibleSourceTypes` は必須で、`null` は契約の外**: `DailyReportServerConfig` とハンドラーの設定のポートは必須 (省いた設定は型で書けず、型を外したホストは
279
+ 生成のときに `TypeError` `[daily-report] resolveVisibleSourceTypes must be a function`、関数でない値は `RangeError`)。ポートの答えは区分の配列・`{ read, comment? }`・
280
+ `{ unrestricted: "<理由>" }` の 3 つの形だけで、`null`・`undefined`・ほかの形 (知らない鍵・空の理由・文字列でない要素) は契約の外として閲覧を決められない要求になる (上の Changed の 503)。
281
+ 以前はポートを省くことも `null` を返すことも「全件」を意味し、区分を結線していないホストはどの閲覧者にもどの区分も見せていた。
282
+ Migration: 区分で絞る配備は今までどおり配列か `{ read, comment }` を返す (`null` を返していた所は無くす)。どの閲覧者にもどの区分も見せる配備は
283
+ `resolveVisibleSourceTypes: () => ({ unrestricted: "<この配備が区分で絞らない理由>" })` を渡す。
284
+ - **閲覧を決められない要求は 503** (上の Changed): ポートの失敗は全拒否 (空の答え) ではなく 503 か SSE の接続の終わりになる。Migration: 一時の障害を空の一覧として確かめる試験と監視は、
285
+ 503 (`Retry-After: 5`) と `visibility unavailable` の行を待つ。
286
+ - **`loginRedirectPath` は必須**: パッケージは既定のサインインのパス (`/auth/login`) を持たない。`createDailyReportServer` と `createDailyReportHandlers` は空白だけでない文字の
287
+ `loginRedirectPath` を受け、ほかは生成のときに `RangeError` (`[daily-report] loginRedirectPath must be the non-blank path of the host's sign-in route; got <value>`)。
288
+ Migration: 設定にホストのサインインの経路のパスを渡す (画面の loader を使わないホストも渡す)。
289
+ - **作成は閲覧の範囲を最初の引数に取り、`Internal` を見ない閲覧者の作成は 403**: `createDailyReport(scope, userId, businessDate, clientTempId)`。範囲がパッケージ自身の区分
290
+ (`Internal`) を見せなければ、どのクエリよりも前に `Forbidden` で断り、action は 403 `{"error":"Creating reports is restricted"}` で答える (行も状態の行も索引も知らせも書かない)。
291
+ 以前は作成が関門を通らず、区分の閲覧権の無い作成者が、自分のどの読み取りと書き込みも断る下書きを作れた。
292
+ Migration: サービスの作成を直接呼ぶ所は範囲を最初に渡す。日報を書く利用者には `Internal` の閲覧権を与える。
293
+ - **区分の列は `nvarchar(n)`**: 区分の SQL の述語はバイト列で比べるので、`tables.hub.sourceType` は長さ 1〜4,000 の `nvarchar(n)` の列でなければならず、ほかはサービスの生成のときに
294
+ `RangeError` (`[daily-report] tables.hub.sourceType must be an nvarchar(n) column with a declared length (source types compare as UTF-16 bytes); got "<SQL の型>"`)。
295
+ `defineDailyReportSchema` の列 (`nvarchar(20)`) はそのまま通る。Migration: 自前のモデルを注入するホストは区分の列の型を確かめる。
296
+ - **既にある日報への書き込みは hub の行の更新で始まる** (上の Fixed): 更新・公開・コメントの追加と削除のトランザクションの最初の文は hub の行の `update(...).set(...).output().where(...)`。
297
+ drizzle の `db` をそのまま渡していれば変更は要らない。Migration: 手で書いた db の代役を渡す試験は、更新の `output()` に選んだ行を答える。
298
+ - **`DailyReportService.getDailyReportsByBusinessDate` を外した**: 閲覧者ごとの規則 (他人の下書きを隠す・閲覧者の既読とスター) を当てない営業日の読み取りは公開のサービスに無い
299
+ (その読み取りのキャッシュの鍵も無い)。Migration: 営業日の日報は閲覧者ごとの読み取り `getDailyReportsByBusinessDateByExternalId` で読む。
300
+ - **自分の日報の更新・公開・削除は閲覧の範囲を最初の引数に取り、見えない日報は 404**: `updateDailyReport`・`publishDailyReport`・`deleteDailyReport` は最初の引数に
301
+ `DailyReportViewerScope` を取り、所有者の確かめの前に閲覧の関門 (読み取りと同じ述語) を通す。範囲の外の区分の日報・他人の下書き・無い日報・論理削除した日報には、どれも同じ 1 つの
302
+ SELECT の後に Not Found (404) で答え (以前は他人の日報に Unauthorized (403))、トランザクションを開かず何も書かない。見える他人の日報には今までどおり Unauthorized。
303
+ 公開の答えは操作者の範囲で読み直した日報 (上の Fixed)。パッケージの API の経路を使っていれば、要求の範囲が渡るので変更は要らない。
304
+ Migration: サービスを直接呼ぶ所 (結合試験など) は、要求の範囲を最初の引数に渡すか、範囲を当てない呼び出しには `DailyReportViewerScope.unrestricted(<理由>)` を渡す。下書きを他人が
305
+ 更新する試験は Unauthorized でなく Not Found を待つ (公開した日報なら Unauthorized のまま)。
306
+ - **状態の切り替えは `execute` の MERGE 1 文で書く** (上の Fixed): `setStarStatus` と `setReadStatus` は `db.execute` で状態の行の upsert を 1 文で流し、出力の行 (`recordset`) を
307
+ ちょうど 1 行として読む (読めなければ `[daily-report] user status: the upsert must output the one status row of report <id> and user <id>; got <output>`)。drizzle の `db` をそのまま渡していれば
308
+ 変更は要らない。Migration: 手で書いた db の代役を渡す試験は、`execute` が状態の行の upsert に出力の行を答えるようにする。
309
+
5
310
  ## 0.41.0 (2026-10-11)
6
311
 
7
312
  ### Added