@aiquants/daily-report 0.40.0 → 0.42.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,305 @@
2
2
 
3
3
  All notable changes to `@aiquants/daily-report` are documented here.
4
4
 
5
+ ## 0.42.0 (2026-10-12)
6
+
7
+ ### Added
8
+
9
+ - **可視のポートの記憶の部品 `createDailyReportVisibilityCache({ ttlMs, readViewerKey, resolve })`** (server エントリ。設定の型 `DailyReportVisibilityCacheConfig`、上限
10
+ `DAILY_REPORT_VISIBILITY_MAX_CACHE_TTL_MS` = 60,000): ホストの問い合わせを包み、ポートの記憶の規則を持つ可視のポートを返す。閲覧者のキーごとに答えを `ttlMs` (1〜60,000 ms の整数。
11
+ ほかは `RangeError`) の間使い回し (単調な時計)、記憶を読むのは `initial` だけで、`refresh` は問い合わせ直して書き戻し、並んだ問い合わせを共有し、失敗は覚えず (後から書いた答えは残す)、
12
+ 閲覧者を名指さない要求は問い合わせる前に投げる。ホストが文章から書き写した記憶はこれに置き換えられる。
13
+ - **区分の語彙** (共有エントリ): パッケージ自身の区分 `DAILY_REPORT_INTERNAL_SOURCE_TYPE` (`"Internal"`) と、可視のポートが返すトークンをサーバーが畳む関数
14
+ `foldDailyReportSourceTypeToken` (前後の空白を除いて小文字)。トークンを組むホストと起動のときの照合は、綴りと畳み方を書き写さずにこれを読む。
15
+ - **明示の opt-out の型 `DailyReportVisibilityOptOut`** (`{ unrestricted: <理由> }`。server エントリ。下の Breaking)。
16
+
17
+ ### Changed
18
+
19
+ - **キーは DOM のフォーカスを動かさず、アクティブな行を動かす** (下の Breaking): 両ビューの複合ウィジェット (VirtualScroll のコンテンツ要素。`role="treegrid"`・`tabIndex=0`) がビューのただ 1 つの
20
+ Tab の止まり先で、キーの間 DOM のフォーカスはそこに残る。キーが動かすのはアクティブな行 (支援技術のフォーカスが載る行) で、複合ウィジェットの `aria-activedescendant` は
21
+ `@aiquants/virtualscroll` の `activeItemIndex` を通して、名指す行と同じ描画で描かれる (行が描画窓の外へ出ても留め置かれるので、参照はいつも解決する)。キーの処理は `focus()` を呼ばず、
22
+ DOM の位置も読まない (キーの起点はアクティブな行の記録)。
23
+ - 複合ウィジェットでの Enter / Space は、両ビューでアクティブな行を起動する: List はカードの主ボタンの押下と同じ (モバイルの配置ではオーバーレイも開く)、DetailList はスクロールせずに選ぶ。
24
+ 既定動作はアクティブな行が無くてもいつも止める (ページはスクロールしない)。
25
+ - アクティブな行の中の操作部品 (★・既読・編集・削除・添付の繋がり・コメントの欄) で押した移動のキーは、その行から動かし、フォーカスを複合ウィジェットへ 1 度だけ戻す (続く繰り返しは
26
+ 複合ウィジェットへ届く)。Escape はアクティブな行を変えずに複合ウィジェットへ戻す。行の中の編集とコメントのフォームの入力欄とボタンからも戻り (打った文字は欄と編集の置き場に残る)、
27
+ 扱わないのは既定動作が止まった・IME の変換の間・Escape が自分の意味を持つ要素 (`select`・検索の欄・編集可能要素・操作部品を持つ再生の要素) の中・行までの祖先が
28
+ `aria-expanded="true"` のときだけ。移動のキーと出口は入力の領域の中では今までどおり扱わない。
29
+ - Tab は複合ウィジェットからアクティブな行の操作部品へ DOM の順に進み、最後の部品の次はビューの外へ出る。Tab は選択しない。既定の動作がアクティブな行の中に着き、その行が見えていなければ、
30
+ Tab と Shift+Tab の keydown の中で行を近いほうの端で見せてから既定動作に渡す (Chromium は `focusin` より前に焦点のスクロールを決めるので、`focusin` で見せても文書がスクロールする)。
31
+ 着く先はパッケージの Tab 順の模型で決め (`tabLandsInRow`)、ビューの外へ出る Tab と Shift+Tab は何も見せない (一覧は利用者が離れた位置のまま)。
32
+ - ビューの外から複合ウィジェットへ入ると、マウスかペンで押した行がアクティブになり (押した行は見せ直さない)、それ以外 (Tab) は見えているアクティブな行・見えている選択中の日報・
33
+ 可視の先頭の行の順で、一部しか見えていなければ近いほうの端で見せる。フォーカスがビューの外の要素へ出たとき (Tab・Shift+Tab・Ctrl+Home / Ctrl+End の出口・ビューの外の押下) と、
34
+ ビューの外にフォーカスがある間の可視範囲の変化も同じ規則でアクティブな行を選び直す (逆向きの Tab で入っても見えている行の操作部品に着く)。押下の行の記録は、ビューの中のどの
35
+ `focusin` と扱ったどのキーでも捨てる (ビューが自分で動かしたフォーカスは、中からの入りとして扱う)。
36
+ - ビューへフォーカスを戻す受け渡し (オーバーレイを閉じた・アクティブな行やコメント節が消えた・フォーカスを持つ操作部品がほかの理由で行から外れた・ページの状態の画面から本体への引き継ぎ) は、
37
+ 行をアクティブにして近いほうの端で見せ、最後の入力の様式で行き先を決める: キーボード (と入力の記録が無いとき) は複合ウィジェット、ポインターとタッチは行そのもののフォーカスを受ける要素
38
+ (`data-daily-report-row-focus`: List のカードの主ボタン、DetailList のカードの画面に出さない名前の見出し (`tabIndex=-1`)。カードの無い行では複合ウィジェット。
39
+ iOS VoiceOver・TalkBack・スイッチ操作は DOM のフォーカスを追う)。利用者がよそに置いたフォーカスは奪わない。
40
+ - 複合ウィジェットがキーボードのフォーカスを持つ間、アクティブな行が見えていればその行のアウトライン、行が無いか見えていなければ VirtualScroll のペインの 2 px の輪が印になる。
41
+ 表示域の縁に切られた行は、その縁で 4 px (G − アウトラインの offset) より多く見えるときだけ見えていると数える (`activeItemVisibleInset`) ので、アウトラインが欠ける重なりでは輪が残る。
42
+ 輪の色は VirtualScroll の変数 `--aqvs-focus-ring-color` を行のアウトラインの色 (blue-600、暗色 blue-400) へ結んだもので、1 つのフォーカスは配色ごとに 1 つの色で描かれる。
43
+ - 行の無い一覧では、空の文言を複合ウィジェットの外 (同じ箱の上の層) に描き、複合ウィジェットの `aria-describedby` の先頭がそれを名指す (キーの説明の前)。区分を 1 つも見られない閲覧者の
44
+ 空のビューも、名前・空の文言・キーの説明で読まれる。
45
+ - コメントの入力欄の名前はコメントの節の見出し (`labels.comments`。`aria-labelledby`) で、置き文字は手がかりのまま。
46
+ - 性能の試験 (9 つの条件): 押し続けあたりの `focus()` は 16.4〜18.7 ms から 0 ms、keydown の配送と押し続けのフレームの中の強制したスタイルと配置は 0、キーの処理の時間の p95 は最大 4.34 ms、
47
+ どの実行も 40 キーで 40 行を動かした。Chromium のアクセシビリティの木で、40 回の ArrowDown のどれでも、両ビューのアクティブな行の関係・名前・説明・選択・位置が正しい。
48
+ - **行の 2 段のマウント**: 見える範囲の外でマウントする行は、まず行の枠と空の面 (殻。名前は営業日の ISO 表記で `aria-busy="true"`) だけを描き、ビューに 1 つの送り手が、フレームの描画の直後の枠
49
+ (MessageChannel の処理 1 つ) で行を 1 つずつ本体へ進める (1 つの単位が 1 つの確定)。順は見える・アクティブな行の届く範囲・近い・遠いの帯で、帯の中ではアクティブな行からの距離の順
50
+ (同じ距離なら進む向きの側を先)。枠ごとに単位は 1 つ、次の単位が見えるか届く範囲の行ならもう 1 つ。見えるようになった行は、見えるようにした確定の中で本体を描くので、見える行が殻のまま
51
+ 塗られることは無い。届く帯か近い帯に入った待つ行は眠った送り手を起こすので、キーが見せうる行はそのキーより前の枠で本体を持つ。後から届いた日報のカード (読み込みの答え・SSE の更新) も
52
+ 同じ送り手が載せ (`useDeferredValue` では遅らせない)、すでに見えている行へ届いたカードは、押し続けの外なら届いた確定の中で載る (見えている行は押し続けの外では古い描画のまま塗られない)。
53
+ キーの押し続けの間は、送り手がキーの描くフレームを避ける: 遠い帯の行は殻のままで届いたカードも載せず (離した後に載せる)、枠は次の繰り返しの keydown (押し続けの最新の 8 つの keydown の
54
+ 間隔の中央値と最も早い位相から見込む) がその確定を描くフレームより前に来ないときだけ走り (枠を予約するフレームのコールバックで決め、走る時点でもう一度確かめる)、枠の 2 つ目の単位は
55
+ どちらの行も決まった箱 (List) のときだけ。見えている行へ届いたカードも、押し続けの間はこの枠を待つ (見える帯の単位が最初)。殻の箱は VirtualScroll の行の高さなので、本体が来ても何も動かない。
56
+ - **遠い行は描画を飛ばす**: 見える範囲を両側へ表示域 1 つぶん広げた帯と、アクティブな行の届く範囲 (前後の行から表示域 1 つぶん。1 行か 2 行の 1 段が見せうるどの行も、行の高さによらず
57
+ ここに入る) の外にあるマウント済みの行は、寸法が決まっていれば `content-visibility: hidden` と `aria-hidden="true"` で描画を飛ばす (`@aiquants/virtualscroll` の `itemZones`)。
58
+ 飛ばした行は支援技術の木・ブラウズモード・スワイプの読み上げ・ページ内検索から外れる。アクティブな行・その届く範囲・見える範囲の前後 1 表示域は外れない。行の箱は変わらないので、
59
+ 飛ばしても見せても何も動かない。DetailList の行は本体を測るまで、コメントの一覧が変われば測り直すまで飛ばさない。`content-visibility` を持たないエンジン (Safari 18 より前) では
60
+ 何も飛ばさず、何も隠さない。性能の試験で、スクロールしたキーあたりの塗りの段 (p50) は DetailList が 3.85〜4.47 ms から 2.07〜2.46 ms、List が 3.13〜3.81 ms から 2.11〜2.49 ms。
61
+ - **選択の輪とアウトラインは行の枠の状態から描く**: 選択の輪と強制配色の選択の線は行の枠の `aria-selected="true"` から、キーボードのフォーカスのアウトラインは、ビューの根がキーボードの
62
+ フォーカスの属性 (`data-daily-report-keyboard-focus`) を持ち、祖先の複合ウィジェットが `:focus-visible` の間に、アクティブな行 (`data-daily-report-active`) の面へ描く。押下は
63
+ アウトラインを出さない。幾何・色・詳細度は変わらず、行の画素は 0.41.0 と同じ。どの選択子も `aria-activedescendant` を読まない (キーごとに変わる値で部分木を無効にしない)。List の
64
+ カードの面は、主ボタンがキーボードのフォーカスを持つ間のアウトラインを描き続ける (ポインターとタッチの受け渡しの後に Enter などを押したとき)。
65
+ - **キー操作の説明の文言** (`labels.listKeyboardHelp`): Enter または Space で選択、Tab でその日報の操作部品へ、Escape で日報の行へ戻ることを足した。ja は
66
+ 「↑↓ で前後の日報、Home / End で先頭・末尾、PageUp / PageDown でページ移動、Enter または Space で選択、Tab でその日報の操作部品へ、Escape で日報の行へ戻ります。Ctrl+Home / Ctrl+End で一覧の直前・直後の操作部品があればそこへ移ります。」、
67
+ 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.」。
68
+ 上書きしているホストは文言を見直す。
69
+ - **見出しのストリームの札は 1 つの枠に置き、枠の大きさは局面で変わらない**: `DailyReportIdsStreamStatus` は見出しの並びに 1 つの枠を持ち、枠の 1 つの格子の升に、見えない寸法の札
70
+ (どの局面の札も、いまの桁数で最も広い形にして重ねる。件数の箱 `CounterText` と同じ組み立て) と、いまの札を持つ入れ物を重ねる。枠はどの局面でも (何も見せない間も) 同じ大きさで
71
+ 配置されるので、札が現れても消えても局面が変わっても、見出しの並びの行の数と下の検索の帯は動かない (幅が変わるのは件数の桁数が変わるときだけ)。空の入れ物は描かず読み上げない
72
+ (`visibility: hidden`。配置は残す。以前は `display: none` で配置しなかった)。最新化中の表示は、ほかの局面と同じ 24px の丸い札 (無彩色) になった。札の文字は枠の始まりに揃う
73
+ (`text-start`。ホストの見出しの右寄せを受け継がないので、狭い局面の札の名前が点から離れない)。フォーカスを持ったまま空になった入れ物は、描かれたビューの複合ウィジェット
74
+ (無ければ選ばれたビューのタブ) へフォーカスを渡す。
75
+ - **閲覧を決められない要求は「今は答えられない」で答える** (下の Breaking): 可視のポートが投げたか契約の外で答えた要求は、空の一覧ではなく、JSON の経路と ids のストリームが読み取りの前に
76
+ 503 `{"error":{"message":"Report visibility is unavailable"}}` (`Retry-After: 5`・`Cache-Control: no-store`)、action が利用者の解決の前に 503 `{"error":"Report visibility is unavailable"}`、
77
+ 添付が混雑と同じ 503、SSE の接続が 503 で答え、開いている SSE の流れは定期の再解決が失敗すると、まだ配っていないエントリにアンカーを書かずに閉じる (ブラウザーが開き直し、追いつきが配る)。
78
+ 記録は `[DailyReportVisibility]` の `visibility unavailable cause=<port_failure|port_contract> reason=<initial|refresh>` で、原因ごとに 60 秒の窓の最初の 1 行と窓の終わりの数の 1 行だけ。
79
+ 空の答え (`[]`) は今までどおり本当の剥奪。
80
+ - **ids のストリームの欠けた行はバイト列のまま持つ**: NDJSON の行の分け方は、行の終わりを届いたバイト列から探し、終わっていない行をバイト列 (チャンクから写した欠片) のまま持ち、
81
+ 行が完結したときに 1 度だけ復号する。以前はチャンクごとに復号した字面を行の欠片として持ち、速さを抑えたストリームでは行の届き終わるまでヒープの文字列が若い世代のごみ集めを生き残って
82
+ 古い世代へ昇格し、押し続けの間の大きいごみ集めを早めた。
83
+ - **開発者向け**:
84
+ - キャッシュの寿命の大域の上書き (`__DAILY_REPORT_CACHE_TTL__`・`__DAILY_REPORT_CACHE_REVIVE_MS__`) は、モジュールの読み込みでなく最初に使う時に 1 度ずつ読む。
85
+ - 100 万単位の検索の文字の試験と 228,222 行の試験は、網羅率を測らない `guards` のプロジェクトの `*.scale.spec.ts` で走り、DOM を使わない client の spec は node の環境で走る。
86
+ - ソースを走査する 4 つの守り (枠線の線種・動きの方針・余白の段・Tailwind のセレクターの範囲) は `*.guard.spec.ts` (`border-style`・`motion-policy`・`spacing-ladder`・
87
+ `tailwind-selector-scope`) で、同じ `guards` のプロジェクトで走る。
88
+
89
+ ### Fixed
90
+
91
+ - **最初の既読とスターの書き込みが重なっても 500 にならない**: `setStarStatus` と `setReadStatus` は、閲覧者の状態の行を `MERGE … WITH (HOLDLOCK)` の 1 文 (`db.execute`) で書く。ある行は旗を
92
+ 更新し、無い行はもう一方の旗を偽にして入れ、書いた行を出力から読む。値の変わらない付け直しは行を 1 バイトも変えない (監査の列も)。以前は行を読んでから入れる 2 文で、同じ日報と閲覧者への
93
+ 最初の 2 つの書き込み (日報を開いた自動の既読とスターなど) が両方とも「行が無い」と読み、主キーで衝突して 500 になった。
94
+ - **論理削除した日報は公開できない**: `publishDailyReport` は削除した日報に Not Found (404) で答え、何も書かない (下書きのラベルを外さず、検索の索引も書かない)。以前は削除した日報の下書きの
95
+ ラベルを外して索引を書き直し、null を答えた。
96
+ - **作った日報は作成者に既読で読める**: `createDailyReport` は作成者の状態の行 (既読、スター無し、監査は作成者) を、日報と同じトランザクションの中で索引より前に入れる。以前は作成の答えだけが
97
+ 既読で、読み直すと未読だった。
98
+ - **公開の答えは操作者の閲覧の範囲を通る** (下の Breaking): 公開の答えは操作者の範囲で読み直した日報で、コメントの次元が除く区分の他人のコメント (自分の投稿は残る) と外部のコメントを持たない。
99
+ 以前は範囲を当てない読み取りをそのまま答え、区分の規則とコメントの規則の外のコメントまで返した。公開の知らせ (`report-publish`) は日報の全体を運び、SSE の配信が受け手ごとに絞る。
100
+ 作成の答えも操作者の範囲で読み直す。
101
+ - **前後に空白のある区分の値は、どの経路でも見えない**: SQL の区分の述語は小文字にした格納の値とトークンをバイト列で比べる (`CAST(LOWER(source_type) AS VARBINARY(2n)) IN (…)`、
102
+ n は列の宣言した長さ)。以前の `LOWER(source_type) IN (…)` は SQL Server の文字列の比較が末尾の空白を埋めて比べ (幅を区別しない照合順序では全角と半角も同じに扱う) ので、`kintone␠` の
103
+ 日報が SQL の読み取りでは `kintone` を見る閲覧者に見え、JS の判定 (SSE・書き込みの関門・添付) では誰にも見えなかった。列より長いトークンは一覧に入れない (切り詰めた頭が一致しない)。
104
+ - **関門の後に確定した削除の上には書かない**: 自分の日報の更新・公開とコメントの追加と削除は、トランザクションの最初の文で生きている hub の行に書き込みの鍵を取り (値を変えない更新。
105
+ 監査の列も書かない)、行を選ばなければ Not Found で巻き戻す。削除は自分の更新に同じ条件を持つ。以前は関門と書き込みの間に確定した削除の上へ公開と更新を書き、削除した日報の下書きの
106
+ ラベルを外して検索の索引を書き直した。
107
+ - **見えない日報のコメントの削除は 404**: `deleteComment` はコメント自身の日報の書き込みの関門を投稿者より先に通す。以前は本文の次元だけを見て、他人の下書きや論理削除した日報にある
108
+ ほかの利用者のコメントには 403、無いコメントには 404 を答え、答えの違いから存在が漏れた。論理削除した日報や見えない区分の日報のコメントは、投稿者自身にも Not Found。
109
+ - **コメントの入力欄は置き文字だけで名付けない**: 入力欄の名前はコメントの節の見出し (「コメント」。`aria-labelledby`) で、置き文字 (`labels.commentPlaceholder`) は手がかりのまま。
110
+ 以前は置き文字 (「コメントを追加...」) だけが名前で、見えている見出しと欄が結び付いていなかった (置き文字は打ち始めると見えなくなる)。
111
+
112
+ ### Breaking
113
+
114
+ - **行は `treegrid` の `row`、面は `gridcell`**: ビューのコンテンツ要素は `role="treegrid"` (`role="list"` ではない) で、名前は `labels.title`、`aria-rowcount` は行が属する集合の大きさ
115
+ (分からない間は −1)。行の枠は `role="row"` (`role="listitem"` ではない) で、`id` はビューの複合ウィジェットの id と日報 id から `virtualScrollItemElementId` で導く id、
116
+ `aria-rowindex` = `aria-posinset` = 位置 + 1、`aria-setsize`、`aria-level="1"` を持つ。行の面 (`data-daily-report-row-surface`) は `role="gridcell"` で、行の名前の要素を
117
+ `aria-labelledby` で参照する。カードの行の名前は名前の要素 (List はカードの主ボタン「営業日 作成者」、DetailList は画面に出さない日報の見出し「営業日 作成者 件名」)、説明は状態・描いた目印・
118
+ プレビュー (List)・一致の文で、集合の大きさが分からない間だけ位置の文言を最後に足す。殻とカードの無い行は営業日の ISO 表記を `aria-label` に持つ。
119
+ Migration: 行を `[role="listitem"]` や `[role="list"]` で探すホストの試験と CSS は `[data-daily-report-row]` (と `[role="row"]`・`[role="treegrid"]`) で探す。位置は `aria-rowindex`・
120
+ `aria-posinset`、集合は `aria-setsize`・`aria-rowcount` で読む。ARIA のスナップショットは treegrid > row > gridcell で取り直す。
121
+ - **選択は `aria-selected`**: どの行も `aria-selected="true"` か `"false"` を明示し、`aria-current` は行にも List のカードの主ボタンにも無い。アクティブな行だけが `data-daily-report-active` を持つ。
122
+ Migration: 選択を `aria-current="true"` で読む所は行の枠の `aria-selected="true"` で読む。アクティブな行は複合ウィジェットの `aria-activedescendant` (行の `id`) か `[data-daily-report-active]` で読む。
123
+ - **フォーカスは複合ウィジェットに残る**: キーはフォーカスを行へ移さない (上の Changed)。List のカードの主ボタンは Tab の止まり先でなく (`tabIndex=-1`。クリックの的とポインターの受け渡しの行き先で、
124
+ `data-daily-report-button` を保つ)、行の枠は `tabindex` を持たない。DetailList の行は `data-daily-report-button` も `tabindex` も持たず、フォーカスを受けない。アクティブな行の操作部品だけが
125
+ Tab の順に入る (ほかの行の部品は `tabIndex=-1`)。
126
+ Migration: 「選んだ行がフォーカスを持つ」を確かめる試験は、複合ウィジェットが `document.activeElement` で、その `aria-activedescendant` が選んだ行の `id` を名指し、行が
127
+ `aria-selected="true"` と `data-daily-report-active` を持つことを確かめる。DetailList の行は `[data-daily-report-button="<id>"]` でなく `[data-daily-report-row="<id>"]` で探す。
128
+ Tab の道筋を数える試験は、複合ウィジェット・アクティブな行の操作部品・ビューの外の順で数える。
129
+ - **ピアの `@aiquants/virtualscroll` は `^3.17.0`**: ビューは `activeItemIndex` (アクティブな行の `aria-activedescendant` と留め置き)・`activeItemVisibleInset`・`itemZones` と
130
+ `useVirtualScrollItem` (帯と遠い行の描画の飛ばし)・`virtualScrollItemElementId`・空の複合ウィジェットの説明・`--aqvs-focus-ring-color` を使う。ピアの下限は 3.17.0
131
+ (`peer-floors.json`)。Migration: `@aiquants/virtualscroll` を 3.17.0 以上へ上げる。
132
+ - **可視のポート `resolveVisibleSourceTypes` は必須で、`null` は契約の外**: `DailyReportServerConfig` とハンドラーの設定のポートは必須 (省いた設定は型で書けず、型を外したホストは
133
+ 生成のときに `TypeError` `[daily-report] resolveVisibleSourceTypes must be a function`、関数でない値は `RangeError`)。ポートの答えは区分の配列・`{ read, comment? }`・
134
+ `{ unrestricted: "<理由>" }` の 3 つの形だけで、`null`・`undefined`・ほかの形 (知らない鍵・空の理由・文字列でない要素) は契約の外として閲覧を決められない要求になる (上の Changed の 503)。
135
+ 以前はポートを省くことも `null` を返すことも「全件」を意味し、区分を結線していないホストはどの閲覧者にもどの区分も見せていた。
136
+ Migration: 区分で絞る配備は今までどおり配列か `{ read, comment }` を返す (`null` を返していた所は無くす)。どの閲覧者にもどの区分も見せる配備は
137
+ `resolveVisibleSourceTypes: () => ({ unrestricted: "<この配備が区分で絞らない理由>" })` を渡す。
138
+ - **閲覧を決められない要求は 503** (上の Changed): ポートの失敗は全拒否 (空の答え) ではなく 503 か SSE の接続の終わりになる。Migration: 一時の障害を空の一覧として確かめる試験と監視は、
139
+ 503 (`Retry-After: 5`) と `visibility unavailable` の行を待つ。
140
+ - **`loginRedirectPath` は必須**: パッケージは既定のサインインのパス (`/auth/login`) を持たない。`createDailyReportServer` と `createDailyReportHandlers` は空白だけでない文字の
141
+ `loginRedirectPath` を受け、ほかは生成のときに `RangeError` (`[daily-report] loginRedirectPath must be the non-blank path of the host's sign-in route; got <value>`)。
142
+ Migration: 設定にホストのサインインの経路のパスを渡す (画面の loader を使わないホストも渡す)。
143
+ - **作成は閲覧の範囲を最初の引数に取り、`Internal` を見ない閲覧者の作成は 403**: `createDailyReport(scope, userId, businessDate, clientTempId)`。範囲がパッケージ自身の区分
144
+ (`Internal`) を見せなければ、どのクエリよりも前に `Forbidden` で断り、action は 403 `{"error":"Creating reports is restricted"}` で答える (行も状態の行も索引も知らせも書かない)。
145
+ 以前は作成が関門を通らず、区分の閲覧権の無い作成者が、自分のどの読み取りと書き込みも断る下書きを作れた。
146
+ Migration: サービスの作成を直接呼ぶ所は範囲を最初に渡す。日報を書く利用者には `Internal` の閲覧権を与える。
147
+ - **区分の列は `nvarchar(n)`**: 区分の SQL の述語はバイト列で比べるので、`tables.hub.sourceType` は長さ 1〜4,000 の `nvarchar(n)` の列でなければならず、ほかはサービスの生成のときに
148
+ `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 の型>"`)。
149
+ `defineDailyReportSchema` の列 (`nvarchar(20)`) はそのまま通る。Migration: 自前のモデルを注入するホストは区分の列の型を確かめる。
150
+ - **既にある日報への書き込みは hub の行の更新で始まる** (上の Fixed): 更新・公開・コメントの追加と削除のトランザクションの最初の文は hub の行の `update(...).set(...).output().where(...)`。
151
+ drizzle の `db` をそのまま渡していれば変更は要らない。Migration: 手で書いた db の代役を渡す試験は、更新の `output()` に選んだ行を答える。
152
+ - **`DailyReportService.getDailyReportsByBusinessDate` を外した**: 閲覧者ごとの規則 (他人の下書きを隠す・閲覧者の既読とスター) を当てない営業日の読み取りは公開のサービスに無い
153
+ (その読み取りのキャッシュの鍵も無い)。Migration: 営業日の日報は閲覧者ごとの読み取り `getDailyReportsByBusinessDateByExternalId` で読む。
154
+ - **自分の日報の更新・公開・削除は閲覧の範囲を最初の引数に取り、見えない日報は 404**: `updateDailyReport`・`publishDailyReport`・`deleteDailyReport` は最初の引数に
155
+ `DailyReportViewerScope` を取り、所有者の確かめの前に閲覧の関門 (読み取りと同じ述語) を通す。範囲の外の区分の日報・他人の下書き・無い日報・論理削除した日報には、どれも同じ 1 つの
156
+ SELECT の後に Not Found (404) で答え (以前は他人の日報に Unauthorized (403))、トランザクションを開かず何も書かない。見える他人の日報には今までどおり Unauthorized。
157
+ 公開の答えは操作者の範囲で読み直した日報 (上の Fixed)。パッケージの API の経路を使っていれば、要求の範囲が渡るので変更は要らない。
158
+ Migration: サービスを直接呼ぶ所 (結合試験など) は、要求の範囲を最初の引数に渡すか、範囲を当てない呼び出しには `DailyReportViewerScope.unrestricted(<理由>)` を渡す。下書きを他人が
159
+ 更新する試験は Unauthorized でなく Not Found を待つ (公開した日報なら Unauthorized のまま)。
160
+ - **状態の切り替えは `execute` の MERGE 1 文で書く** (上の Fixed): `setStarStatus` と `setReadStatus` は `db.execute` で状態の行の upsert を 1 文で流し、出力の行 (`recordset`) を
161
+ ちょうど 1 行として読む (読めなければ `[daily-report] user status: the upsert must output the one status row of report <id> and user <id>; got <output>`)。drizzle の `db` をそのまま渡していれば
162
+ 変更は要らない。Migration: 手で書いた db の代役を渡す試験は、`execute` が状態の行の upsert に出力の行を答えるようにする。
163
+
164
+ ## 0.41.0 (2026-10-11)
165
+
166
+ ### Added
167
+
168
+ - **SSE のストリームのキーの組み立て `buildDailyReportSseStreamKey(streamKey)`** (server エントリ): サービスが SSE のエントリを書いて読む Redis Stream のキー (基のキーにワイヤ形式の版を付けた `<streamKey>:v3`) を作る。ストリームへ直接書き読みするホスト (結合試験など) は、版を書き写さずにこれでキーを作る (版を上げたときに、書き写したキーだけが古い版に残らない)。
169
+ - **`DailyReportRowBatch.idAt(i)`・`businessDateAt(i)`・`sourceTypeAt(i)`** (client エントリ): 行のまとまりの行の ID・営業日・区分を、行のオブジェクトを作らずに 1 つずつ読む (下の Breaking)。
170
+ - **一覧全体の公開の読み口 `readDailyReportIdsStreamRows()`** (client エントリ): セッションの一覧を最後の publish の時点で行のまとまり 1 つへ写して返す (セッションが無ければ空のまとまり)。
171
+ publish の状態は一覧を運ばず変化だけを運ぶので、32 の変化の履歴より遅れた購読者は一覧をこれで読み直す (セッションのクライアントのクラスは公開しない。下の Breaking)。
172
+ - **検索の印の答えの厳格な読み手 `readDailyReportSearchMarksAnswer(payload, expected)` と、要求が求めるものの型 `DailyReportSearchMarksExpectation`** (共有エントリ): パッケージのクライアントが使う読み手そのもので、
173
+ `search-marks` を自分で呼ぶホスト (結合試験など) は答えを型の主張で読まずにこれで読む (頼んだ ID・見せている答えのキーワードの数・日報が当たったキーワードの組を持つかを渡し、合わない答えは null)。
174
+ - **厳格な読み手が受けた行の列の型 `DailyReportIdsStreamRows`** (共有エントリ): `readIdsStreamChunkLine` の答えの `rows` の型で、`forEachIdsStreamRow` が受ける唯一の列 (下の Breaking)。
175
+
176
+ ### Changed
177
+
178
+ - **ids の行は読み手が確かめた値で読む** (下の Breaking): 共有エントリの厳格な読み手 `readIdsStreamChunkLine(line)` は、受けた行を `DailyReportIdsStreamChunk` で答え、壊れた行には null を答える。答えは行の列 (`rows`。読んだ行そのもので写さず、型は受けた列の `DailyReportIdsStreamRows`)・`isLast` と、任意の項目をクライアントが採る規則で読んだ値を持つ:
179
+ 再開のカーソル (`resumeCursor`。サーバーが読み返すカーソルだけで、ほかは null)・`total` (0 以上の安全な整数だけで、ほかは null)・`snapshot` (トークンの形だけで、ほかは null)・`provisional` と `restarted` (真偽の `true` だけが true)・`streamAnchor` (無い・値・受け取った値を持つ形の誤り)。任意の項目の形の誤りでは行を拒まない。答えのオブジェクトは行ごとに 1 つで、行のオブジェクトは作らない。
180
+ クライアントは行をこの読み手で 1 度だけ読み、答えの項目だけを使う (ワイヤの値を 6 か所で確かめ直さない)。再開の規則 (`isResumableIdsStreamCursor`・`isIdsStreamSnapshotToken`) は `src/shared/ids-stream.ts` に 1 つだけ置き、サーバーの再開の解釈 (`readIdsStreamQuery`) と行の読み手が共有する。
181
+ 以前の `isIdsStreamChunkLine` は行を `DailyReportIdsStreamChunkLine` へ絞り込むのに任意の 5 つの項目を確かめず、`total: "9"`・`restarted: "no"`・`provisional: 0`・`snapshot: 42`・`streamAnchor: 7` の行も確かめた型のまま通した。
182
+ - **営業日の符号は 32 ビット**: 常駐の行・変化の行のまとまり・プロバイダーの並びの営業日の符号は `Uint32Array` (行あたり 2 バイト増え、11 万行で約 220 KB) で、ワイヤが運べるどの営業日 (0001-01-01〜9999-12-31 の 3,652,059 日) にも符号がある。
183
+ 以前は 16 ビットの符号が 65,535 日で尽き、閲覧者の行がそれより多くの別々の営業日を持つと (書き手 1 人が別々の日付の日報を作るだけで起きる)、どの ids のストリームも同じカーソルで試みを繰り返して `failed` で終わった。区分の符号は今までどおり 255 種類まで (区分はサーバーの連携が決め、閲覧者の操作では増えない)。
184
+ - **SQL 結果キャッシュの見積もりは保持するヒープの上界**: `estimateRecordsBytes` は、たどった配列・オブジェクト・バイナリの view ごとに 32 バイトの頭、持つ値ごとに 8 バイトの欄、文字ごとに 24 バイトの頭と UTF-16 の単位ごとに 2 バイト、31 ビットの整数でない数 (小数・大きな整数・−0・NaN) ごとに 16 バイトの箱、view のバイト数を数える。
185
+ Node 24 の実測で、見積もりは 11 万行の ids のバケットの保持するヒープの 1.31 倍、詳細の形の行の 1.09〜1.16 倍、長い本文の 1.00 倍 (日本語)〜2.00 倍 (ASCII)。以前は頭と箱を数えず、よくあるバケットを 1.70〜1.89 倍少なく見積もった (既定の 512 MiB でワーカーあたり 0.9〜1 GB を持ちえた)。
186
+ 上限 (`cacheMaxBytes`) はキャッシュ 1 つ、つまりプロセスごと: ワーカーを N 個持つホストは N 倍までを持つので、ワーカーの数を知って決める。
187
+ - **単独で置いたストリームのバッジは、空のまま置いた領域で知らせる**: `DailyReportIdsStreamStatus` は `DailyReportPage` の中に無いとき (ページの中では、ページの常設の読み上げが知らせる)、入れ物の隣にパッケージの読み上げの部品 (`data-testid="daily-report-ids-stream-status-announcer"`) を置く。空のままマウントした `role="status"` と `role="alert"` の領域で、変わるのは文字だけ。
188
+ 再接続中 (`labels.streamRetrying`) は状態、ストリームの失敗 (`labels.streamFailed`) は警告で知らせ、マウントの時点の文字は書かない。局面ごとの札は読み上げの領域ではない。ページの常設の読み上げ (`daily-report-stream-announcer`) も同じ部品 (`src/client/ui/live-announcer.tsx`)。
189
+ 以前は、単独で置いたバッジの再接続中と失敗の札が、局面の変化で文字を持ったまま現れる `role="status"` で、読み上げられないことがあり (WCAG 4.1.3)、失敗をページの警告より弱い状態として伝えた。
190
+ - **デスクトップの側面ペインのスクロールバーはビューと同じ配色**: ビューの休みのつまみと溝の組 (つまみ slate-500、溝 slate-100・暗色 slate-900。明 4.35・暗 3.74) を 2 つの独自プロパティ (`--aqdr-scroll-bar-thumb`・`--aqdr-scroll-bar-track`) で 1 度だけ名指し、VirtualScroll のバーの配色と、デスクトップの側面ペインのスクロールの箱のブラウザー自身のバーの `scrollbar-color` をその上に組む。
191
+ 同じ中身を描くモバイルのオーバーレイの箱はブラウザー自身のバーのまま (組のつまみが、390×844・DPR 3 で右の列の添付のタイルのフォーカスの輪の右の縁の上に描かれるため)。
192
+ 以前は側面ペインのブラウザーのバーが既定の灰色 (暗色で #2c2c2c / #9f9f9f、明色で #fcfcfc / #8b8b8b) のまま、隣の List の slate のバーと同じ画面に並んだ。バーの幅と §3 の幅は変わらず、Safari 17.4 は `scrollbar-color` を読まずに既定のバーを描く。
193
+ - **行の同一性の控えは 2 つの世代**: 位置で読む行のオブジェクトの控えは 512 行の世代 2 つ (`REPORT_ROW_IDENTITY_CAPACITY` = 1,024) で、若い世代で当たる読み取りは何も書かず、古い世代で当たった行を若い世代へ移し、若い世代が 512 行になったら古い世代と入れ替える。
194
+ 両ビューの行のキー (`getItemKey`) は行のオブジェクトを通さずに ID の列を読む (`reportIdAt`)。同じ窓の描き直しは最初の読み取りの後に控えを書かず、窓の行の控えの読み取りは描画あたり 2 回から 1 回になった。
195
+ Node での計測 (11 万行・60 行の窓・押し続けあたり 40 キー・キーあたり描画 1 回): 押し続けあたりの確保と時間は List が 266.8 KiB・0.47 ms から 6.6 KiB・0.066 ms、DetailList が 395.7 KiB・0.69 ms から 6.6 KiB・0.13 ms。以前は読むたびに `Map` の項目を消して足し直し、V8 が表を作り直し続けた。
196
+ - **詳細はその場で読む 1 つの厳格な読み手で読む** (下の Breaking): 日報の詳細とその部品 (面談者・レガシー形式のコメント・ラベル・投稿したコメント) は 1 つの読み手 (`src/shared/detail-reader.ts`) が JSON の値を置かれた場所のまま確かめ、契約どおりなら同じ値を答える。
197
+ `dailyReportDetailSchema` はこの読み手の上に組み、SSE のメッセージ・action の答え・読み取りのエンドポイントの答えは同じ規則で読む。必須の鍵は補わず (`attachments` と添付の `hasThumbnail` も。下の Breaking)、知らない鍵の除去だけが変わる道のオブジェクトを新しく作り、渡した値は書き換えない。
198
+ 合わない値の問題は zod の言葉 (`invalid_type`・`invalid_enum_value`・ID のスキーマの問題) で今までと同じパスと順に答え、`describeWireMismatch` の文も変わらない。
199
+ Node での計測 (3.5 KB の日報 30 件): 日報あたり `{ reports }` の答えは 34〜40 µs・50.6〜57.3 KiB から 10.8〜11.6 µs・8.6 KiB (`JSON.parse` だけで 7.0〜7.5 µs・6.7 KiB)、SSE の更新は 42〜44 µs・58.5 KiB から 16.5〜17.4 µs・11.7 KiB。以前は zod の `z.object` が日報ごとに木を丸ごと写した。
200
+ - **検索の印は値の除去・行の分割・行の分け方の中でも区切る**: 印の歩みは値の除去・行の分割・行 1 つの分け方を、索引の文字の処理の段で進める版 (`stripInvisibleSearchCharactersMappedSteps`・`forEachSearchLineSteps`・`planSearchLineSteps`) で、見積もり 0.5 ms 以内の段ずつ進める。
201
+ 段の版の結果は 1 回で行う版とどこで区切っても同じで、索引の保存と正規化の版は 1 回で行う版のまま (正規化の版の文字は変わらない)。行ごとに決まった仕事の見積もり (2 µs) も払うので、空の行だけの値でも計器が時計を読む。見積もり (ns) は除去 30・行の分割 200 (単位あたり)・行 1 つ 2,000・分け方 90 (仕事の単位あたり)。
202
+ 100 万単位の値の最長の区切り (壁時計、ホストの負荷 0.4): 改行だけで 604 ms から 12.6 ms、見えない文字と改行の 50 万行で 326 ms から 9.4 ms、1 字の行の 50 万行で 61 ms から 10.9 ms、文脈によらない切れ目の無い全角の `AB` の行で 51 ms から 5.2 ms、空白の多い行で 69 ms から 8.0 ms。
203
+ 行の多い値では歩み全体の時間は増える (改行だけの 100 万単位で 0.7 秒・2 つの区切りから 2.4 秒・458 の区切り。行あたりの段の用意の費用)。
204
+ - **印の 429 の待ちは 60 秒まで**: 検索の印の 429 は、`Retry-After` が 60 秒 (`SEARCH_MARKS_MAX_RETRY_AFTER_MS`。サーバーのレートのバケットの窓 1 つ) の内ならその後に 1 回だけ頼み直し、`Retry-After` が無いか超える 429 は時計を置かずに、その epoch の間はその ID を印なしにする。
205
+ 以前はどの `Retry-After` もそのまま時計へ渡したので、2,147,484 秒以上の待ちは時計が溢れてすぐに頼み直し、大きな待ちの間は記録を持ち続けた。
206
+ - **日報を運ばない後回しの答えは拒む**: 検索の印の答えの読み手 (`readDailyReportSearchMarksAnswer`) は、日報を 1 件も運ばずに ID を後回しにする答えを丸ごと拒む (サーバーは最初の日報をいつも答えるので取り決めの破れで、頼み直しても前へ進まない)。以前は受け、store は前へ進まない答えとして扱った。
207
+ - **後から届く日報のカードは後追いの描画で載せる**: List のカードと DetailList の本体は読み込んだ日報を `useDeferredValue` を通して描くので、読み込みの答えや SSE の更新で届いた日報のカードは、それを運んだ更新の中ではなく、キーの押下が割り込める後追いの描画で載る。
208
+ その描画が確定するまで、カードをまだ描いていない行はスケルトンのまま `aria-busy="true"` (描くのを待つ読み込み済みの日報は不在ではない)。遅らせるのは描くことだけで、判断は読み込んだ日報そのものを読む:
209
+ 消えた日報の行を外す安全弁と、DetailList の本体の下書きか・自分の日報か・下書きを開く編集 (編集のボタンは確定した最後の読み込んだ日報を読み、無ければ何もしない)。
210
+ 性能の試験で、答えを見せた List の押し続けの予算を超えるフレームは、行 110 からで 2 から 1、80,000 からで 3 から 1 になり、どちらの条件も目標を満たした。
211
+ - **ID でまとめた詳細の読み手の後始末**: 詳細の読み手 (`useDailyReportDetail`) は、ID でまとめた読み込みを始めた実行だけが中断の合図の持ち主を作り、前もって作った 1 つの理由で止める。キャッシュの日報・営業日の読み込み・走らなかった待ちの再取得は何も作らず、何も止めない。
212
+ 性能の試験の押し続けあたりの後始末は 8.5〜8.7 ms から 0.73〜1.06 ms (理由を渡さない中断が呼ぶたびにスタックごと作っていた `AbortError` の例外と、押し続けあたり 460〜822 KB の確保が無くなった)。
213
+ - **検索の帯の件数は見出しの 1 つだけ**: 当たりの件数は見出しの件数 (`renderHeader` が受ける注記の中) にだけ出し、検索窓は件数を画面に書かない。検索窓の読み上げの領域は 1 つ
214
+ (`aria-live="polite"`。画面に出さない) で、検索中・件数 (見せている一覧の大きさ。ストリームが答えの日報を消せば下がる)・当たり無し・失敗を読む。注記を描かないホストでは件数は画面に出ない (総数と同じ)。
215
+ 以前は見出しと検索窓の状態の行が同じ件数を 2 度描き、390 px の電話で入力欄が約 90 px 狭まった。
216
+ - **検索の帯の失敗の行と欄の中の待ちの印**: 失敗 (検索語が正しくない・混み合っている・使えない・そのほか) は入力欄の下の失敗の行で見せ (読み上げの領域ではない)、入力欄の説明の先頭に入る。
217
+ 入力欄が `aria-invalid` を持つのは検索語が正しくないときだけ。検索中は入力欄の中の自分の場所に回る印 (読み上げから隠し、動きを減らす設定では回らない) を描き、消去のボタンは隠れずに押せる。
218
+ 入力欄は終わりの側に 24 px の場所を 2 つ空けておくので、検索の状態で入力欄の幅も文字の位置も変わらない (320 px の幅でも空の入力欄は全角 4 文字以上を持つ)。
219
+ - **検索の帯の切り替えは幅を変えない**: AND/OR と絞り込みの切り替えは、今の文字と見えない取り置きの文字 (もう一方の文字、最も多い値の数の文字) を 1 つの格子の升に重ね、広い方の幅を
220
+ 格子の最小の幅 (56 px・104 px) で切り上げる。押しても数が増えても入力欄の縁は動かず、既定の文言では入力欄の始まりと終わりが 4 px の格子に乗る。取り置きの文字は名前にも文字の内容にも入らない。
221
+ 以前は AND (50 px) と OR (41 px) の幅の差で、押すたびに入力欄の始まりが 9 px 動き、DPR 1.25 で縁がにじんだ (2.91:1)。
222
+ - **検索の答えの一覧は変わった日報から更新する**: 答えを見せている間の一覧は、publish の変化が答えの日報の項目を変えなければ作り直さない (性能の試験の押し続けあたりの確保は、一覧の導きが 827〜1,095 KB から 0、行の並びの読み取りが 448 KB から 0)。
223
+ - **ID でまとめた詳細の要求は行の出入りが止んでから**: 答えを見せている間の行の詳細の ID は、行の出入りが止んだ後の最初のアニメーションフレームで送り手へ並ぶ。速いスクロールの途中で
224
+ 載って外れる行の要求を始めず、止まった窓の行の待ちは 1 フレームまで。
225
+ - **`SqlResultCache` の上限は省ける**: コンストラクターの `defaultTtlMs`・`maxBuckets`・`maxBytes` はどれも省け、省けばクラスの既定値 (60,000 ms・4,096・512 MiB。`createDailyReportServer` の
226
+ `cacheDefaultTtlMs`・`cacheMaxBuckets`・`cacheMaxBytes` と同じ) で、誤りはファサードの設定の名前を名乗る (`[daily-report] cacheMaxBytes must be an integer >= 1; got 0`)。ファサードは値をそのまま渡し、検証は 1 度だけ。
227
+ - **`knownSourceTypes` は凍結した配列**: `createDailyReportServer` の `knownSourceTypes` は、パッケージ自身の区分 `Internal` を先頭に外部ソースの区分を順に並べた凍結した配列で、サービスの
228
+ `searchFilterSourceTypes` と同じ配列。
229
+ - **開発者向け**:
230
+ - バンドルの増加の台帳はファイルごとの理由と、出荷した版を持つ: 項目は `{ recordedAfter, release, file, from, to, deltaPercent, reason }` (`recordedAfter` は記録した時の `package.json` の版、`release` は出荷する版で、公開のスクリプトが刻むまでは `"Unreleased"`)。
231
+ `--reason` はファイルごと (`--reason '<file>=<text>'`。繰り返す) で、理由の無い増えたファイル・予算の対象でないファイルの理由・同じファイルの 2 つの理由は何も書かずに exit 2。公開のスクリプトは `pnpm version` の直後に `--stamp-release` で `"Unreleased"` を新しい版に変え、`prepublishOnly` の `--released` は `"Unreleased"` の残る台帳で公開を止める。
232
+ 既存の項目は `version` を `recordedAfter` へ改め、その項目を含む最初の公開のタグから `release` を付けた (0.35.0〜0.40.0)。
233
+ - 読み上げの領域の守り (`src/client/ui/live-regions.spec.ts`): 出荷するクライアントで読み上げの役割 (`status`・`alert`・`log`・`marquee`・`timer`)・`aria-live`・`<output>` を書くのは、パッケージの読み上げの部品 (`src/client/ui/live-announcer.tsx`) と、理由を付けてレビューした常設の 3 つの領域 (知らせの警告の領域・検索窓と絞り込みの行の状態の文字) だけ。
234
+ - ref の初期値の守り (`src/client/hooks/lazy-ref-initializers.guard.spec.ts`): 出荷するクライアントのどの `useRef` の引数も、描画のたびに何かを作らない (リポジトリの共有の走査 `.config/scripts/lib/lazy-ref-initializers.mjs`。`@aiquants/virtualscroll` の守りも同じものを使い、例外の一覧は無い)。
235
+ 描画のたびに配列・`Map`・オブジェクトを作っていた ref (誤りの文脈・コメント・action の文脈・DetailList の高さ・検索窓・表示中の印の store・絞り込みの候補) は `useLazyRef` かモジュールの定数になった。
236
+ - 決まったクラスの重ね合わせの守り (`src/client/ui/class-merge-sites.guard.spec.ts`) は例外の一覧を持たない (検索窓と絞り込みの行の 3 か所はモジュールの定数)。種から決まる乱数の守り (`src/client/test-helpers/seeded-random-copies.guard.spec.ts`) は `src/server` も読み、サーバーの spec の生成器の写しは `createSeededRandom` を使う。
237
+ - 内部の `resolveTodayBusinessDateKey` は、不正な `Date` を現在時刻で埋めずに拒む (`[daily-report] resolveTodayBusinessDateKey needs a valid Date`)。
238
+ - 文書のガードは関心ごとの 8 つのファイル (`src/readme-examples.guard.spec.ts`・`src/readme-identifiers.guard.spec.ts`・`src/spec-citations.guard.spec.ts`・`src/testing-spec-index.guard.spec.ts`・
239
+ `src/peer-floor-sentences.guard.spec.ts`・`src/doc-schema-names.guard.spec.ts`・`src/plan-open-decisions.guard.spec.ts`・`src/docs-current-state.guard.spec.ts`) で、網羅率を測らない `guards` のプロジェクトで走る。
240
+ コードブロック・その外の行・インラインコードはリポジトリの 1 つの読み手 (`.config/scripts/lib/markdown-blocks.mjs`。CommonMark の規則で読み、閉じていないコードブロックは開く行を名乗って落ちる。
241
+ `src/markdown-blocks.guard.spec.ts` が確かめる) で、経緯を語る行はリポジトリの 1 つの今の状態の確かめ (`.config/scripts/lib/markdown-current-state.mjs`: 版か日付を持つ見出し・地の文の
242
+ パッケージの版・昔を語る語・レビューの周・実行の ID・issue の参照) で読む。README と仕様書はコードブロックの外で経緯を語らず (版ごとの変更はこの変更履歴、経緯は計画書)、
243
+ README はインラインコードで入口が公開しない SCREAMING_SNAKE と `DailyReport` で始まる識別子を名指さない (README はホストの契約で、内部の識別子は仕様書が持つ)。
244
+ 以前は `src/docs-examples.spec.ts` の 1 つのファイルが 7 つのガードとコードブロックの読み手の 3 つの写しを持った。
245
+ - README はホストの契約だけを持つ: 目次・アンカーを持つ節 (設定・読み取りのポート・描画のポート・sharp の描画・`hasThumbnail`・縮小画像の経路・ログの水準・429 の行・
246
+ 文字の検索の各部)・Server wiring の下の Streaming routes・The API route・Request values・Realtime architecture。パッケージの開発の門 (型の検査・網羅率・ガード・バンドルの予算・
247
+ 公開の検査) はテストの仕様書 (§1.4) へ移し、内部の識別子と理由は設計の仕様書 (00) と検索の仕様書 (03) が 1 度だけ持つ。
248
+ - ゲートのスクリプトはリポジトリの共有のモジュール (`.config/scripts/lib` の `count-ratchet.mjs`・`ratchet-docstrings.mjs`・`ratchet-coverage.mjs`・`gate-cli.mjs`・`source-corpus.mjs`) を
249
+ `@aiquants/virtualscroll` と共有し、パッケージの `scripts/` はパッケージの値を渡す短い入口 (根はリポジトリの唯一の実装と 1 バイトも違わない `scripts/lib/repository-root.mjs` で見つける)。
250
+ - spec は置き場で 3 つのプロジェクトに入る (`vitest.config.ts`): 網羅率を測る `node` (`src` の直下・`src/server`・`src/shared`) と `jsdom` (`src/client`)、網羅率を測らない `guards`
251
+ (本番の処理を読み込まない `*.guard.spec.ts` と、乱択の軌跡の組の `*.traces.spec.ts`)。環境は置き場で決まり、違う環境が要る spec だけが 1 行目に指定を持つ。`verify` と `test:coverage` は
252
+ 網羅率を測るプロジェクトを先に、`guards` を後に走らせる。
253
+ - モジュールの置き場: サービスは合成の根 `src/server/service.ts` と関心ごとの `src/server/service/` (設定・SSE の書き込み・hub の写し・添付の記録・読み取り・閲覧者の状態・コメント・
254
+ 日報の書き込み)、アクションのプロバイダーの部品は `src/client/contexts/action/`、行は `src/client/rows/`、選択は `src/client/selection/`、ID のまとめは `src/client/batching/`。公開名は変わらない。
255
+ - 整数の設定のガード (`src/config-integer-settings.guard.spec.ts`): 公開の `*Config` の型のどの `number` の欄も、1 つの整数の検証でちょうど 1 度だけ読む。
256
+ - zod の読み込みのガード (`src/zod-imports.guard.spec.ts`): `src` のどのソースも zod を `zod/v3` の入口からだけ読む。
257
+
258
+ ### Fixed
259
+
260
+ - **zod 4 でも公開の宣言が型検査を通り、食い違いの code が同じ**: パッケージは zod を `zod/v3` の入口からだけ読む。zod 4.4.3 で共有エントリの宣言の型検査が 47 の誤りで落ち、
261
+ 食い違いの code が変わっていた (peer の範囲は zod 4 を受けていた)。
262
+ - **収めた後に掛かった文字の間隔の上書きも、次のフレームで収め直す (WCAG 1.4.12)**: 添付の収めたファイル名の見える文字列は中身の幅の箱 (`width: fit-content`) で、名前の列の幅の観測器がその描いた幅も見る (`observeFittedNameWidth`)。
263
+ 列へ最後に渡した列幅より広く描かれた文字列があれば、次のフレームでその列へ同じ列幅をもう一度渡し (溢れた文字列がいくつでも列ごとに 1 回)、列の名前は書体と間隔を読み直して収め直される。切り取りは無く、窓をずらすスクロールの 1 段の描き直しは今までどおり (Chromium 148 で、素の行 1 行・添付のある行 2 行)。
264
+ 以前は、収めた後に掛かった上書き (ブックマークレットや利用者のスタイル) は列幅を変えないので次の収め直しまで残り、名前が列から 11〜64 px はみ出して隣のタイルに重なった (Chromium 148 の探り。アプリの見た目の契約では 22〜57 px)。今は 2 フレーム目から列の中に収まる。
265
+ 上書きが掛かったフレームだけは名前を広く描き、外した上書きは次の収め直しまで名前を短いまま残す (README の **File name**)。
266
+ - **見出しの件数は総件数と当たりの件数で同じ幅**: 見出しの件数は検索の答えの有無のどちらでも同じ箱に描き、箱の幅は総件数と当たりの件数の最も広い文字 (総件数と答えの件数の大きいほうの桁数の最大の数で作る) の広い方が取るので、狭い幅で注記が折り返しても、答えを見せる・解除するたびに検索の帯とビューが上下に動かない (答えの件数が総件数より桁が多いときを除く)。以前は当たりの件数 (`0 件ヒット`) が総件数 (`総件数: 228,223 件`) より狭く、320 px の幅では注記が 2 行から 1 行に減って見出しが 12 px 縮み、検索の帯が上へ動いた。
267
+
268
+ ### Breaking
269
+
270
+ - **`isIdsStreamChunkLine` は `readIdsStreamChunkLine` に**: 共有エントリの行の読み手は真偽の絞り込みでなく、確かめた値 (`DailyReportIdsStreamChunk`) か null を答える。別名は無い。
271
+ Migration: `if (isIdsStreamChunkLine(line)) { … line.total … }` は `const chunk = readIdsStreamChunkLine(line)` と `if (chunk !== null) { … chunk.total … }` に書き換え、任意の項目は答えの値 (`resumeCursor`・`total`・`snapshot`・`provisional`・`restarted`・`streamAnchor`) だけを読み、行は `forEachIdsStreamRow(chunk.rows, visit)` でたどる。
272
+ - **`DailyReportRowBatch` は読み口の型で、列を持たない**: client エントリの `DailyReportRowBatch` はクラスでなく読み口 (`length`・`idAt`・`itemAt`・`items`・`businessDateAt`・`sourceTypeAt`) で、
273
+ `ids` の列も `businessDateCodes` と `sourceTypeCodes` (ページの中で値が初めて現れた順に配る符号) も無い (パッケージが共有する書ける列を渡さない)。0 から `length` − 1 の整数でない位置の読み取りは
274
+ 投げる (`[daily-report] A batch of <n> rows has no row <i>`)。
275
+ Migration: ID は `idAt(i)`、営業日と区分は `businessDateAt(i)`・`sourceTypeAt(i)` か `itemAt(i)` で読む (`batch.ids[i]` は `batch.idAt(i)`)。符号を読んでいたホストは値で読む。
276
+ - **詳細のスキーマは `ZodObject` でない**: `dailyReportDetailSchema` は `z.ZodType<T, z.ZodTypeDef, unknown>` (読み手の上に組んだ変換) で、`.shape`・`.extend`・`.pick` は無い (部品のスキーマはもう公開しない。下の項)。
277
+ 拒んだ詳細の問題は打ち切りの印 (`fatal: true`) を持ち、後の `.refine` や `.transform` へ値を渡さない。受ける値・出力・問題のパスと符号は今までどおり。
278
+ Migration: スキーマの形を広げたり選んだりしていたホストは、自分のスキーマを書く (詳細の型は `DailyReportDetail` ほか)。
279
+ - **zod の peer の下限は 3.25.76**: peer の範囲は `>=3.25.76 <5.0.0` (パッケージは zod を `zod/v3` の入口からだけ読み、その入口を持つ版から)。
280
+ Migration: zod 3.25.76 以上 (zod 4 を含む) を入れる。
281
+ - **サービスの数の設定は 1 以上の整数**: `idsTtlMs`・`businessDateTtlMs`・`streamMaxLen` は省いたときだけ既定値で、0・負の数・`NaN`・小数・2^53 以上は `createDailyReportService` と
282
+ `createDailyReportServer` で鍵を名乗る `RangeError` になる (`[daily-report] idsTtlMs must be an integer >= 1; got 0`)。以前はそのままキャッシュの TTL と Redis の MAXLEN へ渡った。
283
+ Migration: 3 つを 1 以上の整数で渡すか省く。
284
+ - **大小文字によらず同じ区分の外部ソースは作成で止まる**: 2 つの外部ソースの `sourceType` が大小文字によらず同じ (同じ字面も) か、1 つが `Internal` と大小文字によらず同じなら、
285
+ `createDailyReportService` と `createDailyReportServer` は `RangeError` で止まる (`[daily-report] externalSources[1].sourceType must be a source type that differs, ignoring case, from …; got …`)。
286
+ 以前は後の区分を絞り込みの候補から黙って落とした。Migration: 区分の名前を大小文字によらず別々にする。
287
+ - **`DailyReportService` の 5 つの欄を外した**: `streamMaxLen` と、閲覧の範囲を当てない hub の部品 `findDailyReportHubById`・`updateDailyReportHub`・`updateDailyReportInternal`・`deleteDailyReportLabel` は無い。
288
+ Migration: ストリームの上限は自分の設定 (`streamMaxLen`) から読み、日報は公開のサービスの読み取りと書き込み (`getDailyReportDetailById`・`updateDailyReport` ほか) で扱う。
289
+ - **SSE のエントリの区分と詳細の添付の欄は必須**: SSE のメッセージのスキーマは `comment-add`・`comment-delete`・`report-delete` の `sourceType` (文字) を求め、詳細の読み手は `attachments` と
290
+ 添付の `hasThumbnail` を既定値で補わない (欠ければ合わない値)。版を焼き込んだストリームのキー (`<streamKey>:v3`) の前の形のエントリは読まれない。
291
+ Migration: 自分でエントリを書くホスト (結合試験など) は、3 つの知らせに日報の `sourceType` を、詳細に `attachments` と添付ごとの `hasThumbnail` を書く。
292
+ - **`forEachIdsStreamRow` は厳格な読み手が受けた列だけを受ける**: 引数は `DailyReportIdsStreamRows` (`readIdsStreamChunkLine` の答えの `rows`) で、`encodeIdsStreamRows` の出力や
293
+ 手で組んだ列 (`DailyReportIdsStreamRowColumns`) は型検査で渡せない。Migration: 行は `readIdsStreamChunkLine(line)` で読み、受けた答えの `rows` をたどる。
294
+ - **ids のストリームのクライアントのクラスを公開しない**: client エントリの `DailyReportIdsStreamClient` と `DailyReportIdsStreamClientOptions` は無い (ホストが作ると 2 本目のストリームを開く)。
295
+ Migration: 一覧全体は `readDailyReportIdsStreamRows()`、状態は `useDailyReportIdsStream()`、読み直しは `resyncDailyReportIdsStream()` で扱う。
296
+ - **共有エントリはメッセージと詳細のスキーマを 1 つずつだけ公開する**: メッセージごとのスキーマ (`connectedMessageSchema`・`reportCreateMessageSchema`・`reportUpdateMessageSchema`・
297
+ `reportPublishMessageSchema`・`reportDeleteMessageSchema`・`statusUpdateMessageSchema`・`commentAddMessageSchema`・`commentDeleteMessageSchema`)・部品のスキーマ
298
+ (`dailyReportInterviewerSchema`・`dailyReportExternalCommentSchema`・`dailyReportLabelDefSchema`・`dailyReportPostedCommentSchema`)・`mergeComments`・`isDailyReportSseStreamId` は無い。
299
+ server エントリの `DailyReportHubRow` と `DailyReportInternalRow` も無い (公開のどの宣言も参照しない)。
300
+ Migration: メッセージは `dailyReportSseMessageSchema` で読んで `type` で分け、詳細は `dailyReportDetailSchema` で読む。コメントは `useDailyReportComments` が合わせた並びを読む。
301
+ - **型の公開名はどれも `DailyReport` を名乗る**: server エントリの `ExternalReportFields`・`StreamEntry`・`SharpLike` は `DailyReportExternalReportFields`・`DailyReportSseStreamEntry`・`DailyReportSharpLike`、
302
+ client エントリの `SourceTypeConfig` は `DailyReportSourceTypeConfig`。Migration: 型の読み込みの名前を改める (別名は無い)。
303
+
5
304
  ## 0.40.0 (2026-10-09)
6
305
 
7
306
  ### Added