@aiquants/daily-report 0.25.0 → 0.27.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,86 @@
2
2
 
3
3
  All notable changes to `@aiquants/daily-report` are documented here.
4
4
 
5
+ ## 0.27.0 (2026-10-05)
6
+
7
+ 0.26.0 の続き: Fetch Metadata による要求の隔離、ポインター専用のスクロールバー (名前の無い `scrollbar` と `slider` を出さない)、添付の格子の整数 px の列、要求が全員去った描画の結果の保持、SSE リーダーのポート、E2E のテスト用ハンドルの読み取り専用化。どれも README の該当の節が今の契約を記す。
8
+
9
+ ### Security
10
+
11
+ - **別オリジンのページからの要求は認証より前に 403**: API (`api.loader`・`api.action`)・SSE (`sse.loader`)・添付 (`attachment.loader`) のどの経路も、最初の文で要求の Fetch Metadata を見て (`isCrossSiteRequest`、`src/server/request-isolation.ts`)、`Sec-Fetch-Site` が `same-origin` でも `none` でもない要求 (同じサイトの兄弟のサブドメインの `same-site` を含む) を、認証・レートの課金・可視集合の解決・認可・サービスの呼び出しのどれより前に 403 (`{"error":{"message":"Cross-site request rejected"}}`・`Cache-Control: no-store`・`X-Content-Type-Options: nosniff`。`Set-Cookie` もログの行も無い) で断る。
12
+ 断らないのは、最上位のナビゲーション (`Sec-Fetch-Mode: navigate`・GET か HEAD・`Sec-Fetch-Dest` が `object` でも `embed` でもない。別のページのリンクから原本を開く) と、`Sec-Fetch-Site` の無い要求 (ブラウザーでないクライアント。下限のどのブラウザーも送る) だけ。添付の 403 は添付の失敗の組み立てを通るので、添付のどの応答とも同じヘッダー (`Cross-Origin-Resource-Policy: same-origin`・`nosniff`) を持つ。応答の側の `Cross-Origin-Resource-Policy` は多重の守りとして残す (README の Request isolation)。
13
+ 以前は、セッションの Cookie (通常の `SameSite=Lax`) が付く同じサイトの別オリジンのページから、フォームの POST が日報の作成・更新・公開・削除・コメント・スター・既読へ届き (フォームの送信はプリフライトを要さず、React Router はリソースの経路に CSRF の検査をかけない)、`<img>`・no-cors の `fetch`・HEAD の探りが認証・レートのトークン・可視集合の解決・認可まで走らせた (見える添付と見えない添付で応答までの仕事が違い、閲覧者のレートのバケットを空にして 429 にできた)。`createDailyReportServer` か `createDailyReportHandlers` を結線したホストは、この版へ上げるだけで守られる。ページと別のオリジンから API を呼ぶホストは断られる (パッケージのクライアントはページ自身のオリジンからしか呼ばない)。
14
+
15
+ ### Changed
16
+
17
+ - **スクロールバーはポインター専用 (名前の無い `scrollbar` と `slider` を出さない)**: 両ビューが渡す `enableArrowButtonTabStops: false` は、`@aiquants/virtualscroll` 3.10.0 で「ホストがキーボードのスクロールを自分で持つ」ことのただ 1 つの合図になり、バーはネイティブのスクロールバーと同じポインター専用の部品になる。バー全体が `aria-hidden="true"` なので支援技術には (名前の無いものも含め) `scrollbar` も `slider` も見えず、矢印は Tab 順に入らず、バーのどこを押しても既定動作を取り消してフォーカスを動かさない。ポインターのスクロール (矢印と長押し・溝・つまみ・タップスクロールサークル) は変わらない。
18
+ 以前は名前の無い `role="scrollbar"` と `role="slider"` が支援技術に見え (WCAG 4.1.2)、バーの押下は行にあったフォーカスをバー (`tabIndex={-1}`) へ移した。ラベルのカタログは virtualscroll の 11 個のエンジンキーを再利用し、新しい 4 キー (`verticalScrollBar`・`horizontalScrollBar`・`verticalScrollThumb`・`horizontalScrollThumb`) がバーとつまみの名前になる (隠れたバーの名前も `config.labels` で上書きできる)。キーは全 94 (エンジン 11 + 固有 83)。
19
+ - **添付の格子の列は整数 px**: 格子は自分の大きさの容器 (`data-testid="daily-report-attachment-grid-container"`。inline-size の容器で、格子の包含ブロック) の中に 1 つだけ置き、容器の内容の幅 W が列数・格子の幅・ファイル名を収める列幅のただ 1 つの入力になる。n 列の格子の幅は列幅 t が整数 px になる最も広い n·t + 16(n − 1) ≤ W (区切りごとに列数の隣に書く `round()` の幅。列は `1fr` のまま) で、列幅は t = ⌊(min(W, 1,264) − 16(n − 1)) / n⌋、隙間はどれもちょうど 16px、余り (W が整数 px なら 0〜n − 1px) は格子の外の終わりの側に残る。
20
+ 1280 × 800 の側面ペインの列幅は 234⅔ と 231⅓ から 234 と 231 になる (枠の高さ 156 と 154 は変わらない)。以前の端数の列幅はタイルの縁を画素の間に置き、比 1 では隙間を 16 と 17px で描いた。`round()` を持たない Chrome 121〜124 は幅の宣言だけを捨て、今までの端数の列幅で同じ列数に並ぶ。添付の節は大きさの容器でなくなり、ファイル名を収める幅の観測器は格子ではなく格子の容器を測る (格子そのものの幅は余りを除いた幅なので、そこからは列数が決まらない)。
21
+ - **全員が去った描画も、払い終えた結果を残す**: サムネイルの生成は、描画の締め切り (502 `render_timeout`) か、待っている要求が全員去ったとき (503 `aborted`。その時点で答える) に描画を待つのをやめ、どちらでもその描画を続いている描画の登録簿へ渡す。描画が内容で決まる結果 (出力の検証を通った縮小画像か `unsupported`) で決着したら、同一性を確かめた生成なら、誰が待っているかを問わずキャッシュに記録してから枠を返す。決着するまで、同じ内容の次の生成は展開を重ねずにそれを待つ。中断に応じる描画ポートの `failed` と例外は記録しない。
22
+ 以前は全員が去った生成の描画の結果を種別を見ずに捨てたので、タイルが描画の窓を出て戻ると、既定の 2 枠では同じ原本をもう 1 度読んで展開した (今は読み取り 1 回・描画 1 回)。
23
+ - **開発用の SSE のスイッチに名前**: `showDevControls` の SSE の切り替えのスイッチ (`role="switch"` のボタン) は、ボタン自身に `labels.devSseToggle` の `aria-label` と `title` を持つ。以前は囲む要素の `title` だけで、スイッチは名前を持たなかった (WCAG 4.1.2)。
24
+ - **強制配色のスクロールバー**: `VIEW_SCROLL_BAR_THEME_CLASS_NAME` は強制配色 (`forced-colors: active`) で、つまみを状態ごとの規則と同じ選択子でシステム色に言い直し (休み `CanvasText`、ホバーとドラッグ `Highlight`、無効 `GrayText`)、溝に内側の 1px の `CanvasText` の輪郭を付ける (配置は変わらない)。以前は強制配色でつまみも溝も `Canvas` になり、つまみが見えなかった。
25
+ - **E2E のテスト用ハンドルは読み取りと見せ方 1 つ** (Breaking を参照): `__virtualScroll` は VirtualScroll のハンドルの読み取りの部分 (`getViewportSize`・`getScrollPosition`・`getScrollAnchor`・`getRange`・`getFenwickSize`)、日報の検索 3 つ、ビューのスクロールの口を通る `revealIndex(index, options)` だけを持つ。`revealIndex` はキーの移動と同じく呼び出しの直後に錨を記録するので、直後の一覧の変化も見せた日報を同じ位置に留める。以前はハンドルを丸ごと広げたので、E2E が錨を通らずに位置を動かせた。
26
+ - **SSE リーダーはポートの型で受ける**: `createDailyReportHandlers` の設定の `sseReader` は、クラス `DailyReportSseReader` の型ではなくポート `DailyReportSseReaderPort` (`ready()` と `subscribe(onEntry, onError)`) で受ける。0.24.0 と 0.26.0 の Breaking が言う、ホストが自前で差し替えるリーダーが実装するのはこの型。以前のクラスの型は private の欄を持つので、同じ 2 つのメソッドを持つホストの実装も型の読み替えなしには渡せなかった。
27
+ - **開発者向け**: 公開のスクリプト (`publish:*`) は `pnpm run verify` が作ったビルドをビルドし直さずに測り、`node scripts/check-bundle-size.mjs --write`、続けて `--exact` を走らせる。縮んだ項目は `--write` が下げるので、縮みは公開する版の基準に作りとして記録される (以前は縮みも `--exact` で落ち、基準を記録し直すコミットが要った)。増えた項目は記録せずに `--exact` で止め、レビューを通した `--write --accept` を待つ。
28
+ 配置の格子の単位 u = 4 は `src/shared/layout-lattice.ts` の `LAYOUT_LATTICE_PX` ただ 1 つで、List の行の枠・DetailList の測る前の行の見積もり (88u = 352)・添付のタイルの寄せはどれもそこから読む (写しの宣言は `constants.spec.ts` が構文木で落とす)。SQL の結果のキャッシュのキーはどれも `cache-keys.ts` の組み立てが可視集合のダイジェストで閉じ、ID 一覧の名前空間の接頭辞は `DAILY_REPORT_IDS_CACHE_PREFIX` (`"daily-report:ids:"`)、営業日の解決のキーも組み立て (`businessDateCacheKey`) から作る (キーのバイト列は変わらない)。錨の spec は本物の VirtualScroll の上でも、窓の中と外の測った行の置き換えと、下端の揃えを保留している間の窓の中の削除を確かめる。
29
+
30
+ ### Added
31
+
32
+ - server の型 `DailyReportSseReaderPort`: 共有 SSE リーダーのポート (上の Changed)。
33
+
34
+ ### Breaking
35
+
36
+ - `__virtualScroll` (両ビューのテスト用ハンドル。README の Test hooks) から、VirtualScroll のハンドルの位置を動かす関数 (`scrollToIndex`・`scrollBy`・`scrollTo`・`applyWheel`・`updateItemSize`) と、`getContentSize`・`getFenwickTreeTotalHeight`・`focusItemAtIndex` を外した。Migration: `__virtualScroll.scrollToIndex(index, options)` は `__virtualScroll.revealIndex(index, options)` へ置き換える (揃え方と offset は同じ)。ほかの位置の変化は画面の操作 (キー・ホイール・クリック) で起こす。
37
+ - ピアの `@aiquants/virtualscroll` の下限を 3.10.0 へ上げる。両ビューは 3.10.0 の `enableArrowButtonTabStops: false` (ポインター専用のバー) と、バーとつまみの名前のキーに頼る (3.9.0 では名前の無い `scrollbar` と `slider` が支援技術に見え、バーの押下がフォーカスを移す)。Migration: `@aiquants/virtualscroll` を 3.10.0 以降へ上げる。`labels` で上書きできるエンジンのキーは 4 つ増える (上書きは任意)。
38
+
39
+ ## 0.26.0 (2026-10-04)
40
+
41
+ 0.25.0 の続き: 位置の変化ごとの錨の記録、行と面の 4 px の格子、読み込みの画面の枠とエラーの文言の色、コメントの節の 1 つの規則、添付の配信とサムネイルの生成の締め付け、共有 SSE リーダーの位置の固定。どれも README の該当の節が今の契約を記す。
42
+
43
+ ### Changed
44
+
45
+ - **錨は位置のどの変化の後にも記録する**: DetailList の行の測り直しはスクロールの口の `resizeRow` (VirtualScroll の `updateItemSize` の直後に錨を記録する) を通る。`updateItemSize` は可視の先頭より上の行の高さが変わるとその差だけ位置を自分でずらす (レイアウトのずれの補正) ので、直後の記録は補正の後の状態を読む。
46
+ VirtualScroll が自分で位置を動かしたとき (測り直した行の補正・描画の後の高さの照合・保留中の揃えの留め直し・クランプされた補正の再発行) は、`@aiquants/virtualscroll` 3.9.0 の `onScrollAdjust` で同期に知らされ、両ビューは錨をその場で記録し直す。
47
+ 以前は次のフレームの可視範囲の知らせまで錨が古いままで、その間に確定した一覧の変化 (SSE の挿入・削除) が、先頭に見えている日報を補正の差だけずらすか隣の日報に置き換えた。
48
+ キーボード操作とビューが受け取るハンドルは読み取りの関数だけの型 (`ListNavigationHandle`) になり、位置を動かす関数 (`scrollToIndex`・`scrollBy`・`scrollTo`・`applyWheel`・`updateItemSize`) を口の外で呼ぶと型検査で落ちる。利用者のスクロールの後の錨の進め方は、一様な高さの行 (List) では算術で 1 回、実測の高さの行 (DetailList) では通り過ぎた行だけをたどる。
49
+ - **ビューの高さはどの値も `ResizeObserver` から**: 最初の値も観測の最初の配達から受け取り、その配達は `flushSync` で描く前に確定させる (配達まではビューの中身を描かないので、推測の高さでは描かない)。0.25.0 は最初の値を計算済みの `block-size` から読み、有効数字 6 桁の文字列 (743.65625 が 743.656) のために端数のある高さを 2 回確定させた。
50
+ 箱を持たない根要素は、Chromium では最初の配達の 0、仕様どおり 0 × 0 から観測を始める描画エンジンでは箱を持つまで値が無い (VirtualScroll を描かない)。
51
+ - **行の枠と面は 4 px の格子の上**: List の行の枠の高さ P は 52 px + 6.75 rem を 4 px の倍数へ切り上げる (根 16 px は 160 のまま。根 12・20・24 px は 133・187・214 から 136・188・216 になり、カードの余裕が 4 px 未満だけ増える)。DetailList の測る前の行の見積もりは 352 px (以前の 350 は、窓より上の測っていない行 1 つごとに下の行を 2 px ずらした)。
52
+ DetailList の行の本体 (カード・スケルトン・編集の枠・エラー) は中身の高さを 4 px へ切り上げる (`calc-size(auto, round(up, size, 4px))`。根 16 px では変わらない。持たない描画エンジンは中身の高さのまま)。
53
+ 画像の添付のタイルは最後の行の下に 0〜3 px の寄せ (`round(up, h, 4px) − h`) を持ち、どのタイルも ⌈h / 4⌉ · 4 + 48 px になる (枠の高さ h(t) = round(2t / 3) と 3 : 2 の画像の埋まり方は変えない)。タイルの `100cqi` の容器はタイルの根になり、枠の包みは容器でなくなった。
54
+ 以前は k 段のタイルを持つ面が k · h(t) (mod 4) だけ格子を外れ、その下の DetailList の行は画素密度 1.25 で装置の画素の半分ずれて、枠線・選択の輪・アウトラインがにじんだ。
55
+ - **端に揃えた面は G より端へ寄らない**: `@aiquants/virtualscroll` 3.9.0 の装置の画素への揃えが揃えた端を守るので、下端・上端に揃えた行の面は端から G = 8 px より近づかず (足されるのは 1 装置画素未満)、ビューの高さと行の枠が装置の画素の整数ならちょうど G になる。以前はビューの高さが端数だと、下端に揃えた行の余白が最大で装置の画素の半分削られた (比 1 で高さ 705.5 px のビューの最後の行の余白が 7.5 px)。
56
+ - **読み込み中と読み込みの失敗の画面も同じページの枠と列**: どの画面もページの枠 (`VIEW_PAGE_FRAME_CLASS_NAME`。明 slate-50・暗 slate-950) と中央の列 (`VIEW_COLUMN_CLASS_NAME`。4 px の格子の原点を含む) を使う。以前の読み込み中と失敗の画面は明るい slate-50 の枠を直に書いたので、暗色では読み込みが終わるまで明るいページで、終わると切り替わった。
57
+ エラーの文言の色は 1 つのトークン (`ERROR_TEXT_CLASS_NAME`。明 red-700・暗 red-400) で、載るパッケージの面のどれにも明暗とも 6.17:1 以上。以前は失敗の画面の文言 (red-600) が暗色の面の上で 1.42:1、側面ペインの日報の読み込みの失敗 (red-500) が白の上で 3.81:1 だった。
58
+ - **コメントの節は 1 つの規則**: DetailList の行・側面ペイン・モバイルのオーバーレイは、コメントがあるか投稿できる間だけ、見出しごとコメントの節を描く。以前の側面ペインとオーバーレイは常に描き、投稿できない種別でコメントの無い日報に空の見出しを残した。
59
+ 削除を確定した後のフォーカスの行き先は残る構造から決める: 次のゴミ箱、前のゴミ箱、節が残るなら節の見出し、残らなければ日報自身の目印 (DetailList は行、側面ペインとオーバーレイはペインの日報の見出し)。以前の DetailList では、投稿できない種別の最後のコメントを消すと、フォーカスを移した見出しが節ごと消え、フォーカスが `body` へ落ちた。
60
+ - **営業日の吹き出しは `transform` で付いていく**: スクロールバーのつまみの営業日の吹き出しは一定の位置に置き、つまみの中心へは `transform` だけで動かす (遷移は付けない)。以前は `top` / `left` を書いたので、吹き出しを見せている間のスクロールの 1 段ごとに文書の根からのレイアウトを 1 回足した。
61
+ - **sharp を起動する前のたどりに歩数の上限**: JPEG の区切りと走査の読み飛ばしと WebP のチャンクのたどりは同期でイベントループを止めるので、訪れる 0xFF 1 つと WebP のチャンク 1 つを 1 歩と数え、`maxWalkSteps` = ⌊16 ms × 10⁶ ÷ (50 ns × 2)⌋ = 160,000 歩を超える原本は sharp を起動せずに `unsupported` (内容で決まる失敗として記録) にする。符号化済みのデータの 0xFF の連なりは 1 つの JS のループで読む。
62
+ 以前は 32 MiB の原本で、符号化済みのデータが 0xFF の連なりならイベントループを 0.97〜1.11 秒、`FF 00` や再同期の連なりなら 0.50〜0.59 秒止め、そのうえ sharp を起動した。上限までのたどりは、測ったホストで最も重い形 (`FF 00` の組を 3〜209 バイトおきに散らした原本) でも 5.0〜7.0 ms、ほかの形は 1〜2.4 ms。
63
+ - **添付のどの応答にも `Cross-Origin-Resource-Policy` と `nosniff`**: 失敗の応答 (400・401・403・404・405・413・429・500・502・503。GET と HEAD) も `Cross-Origin-Resource-Policy: same-origin` と `X-Content-Type-Options: nosniff` を持つ。どの応答の経路も 1 つのヘッダーの組を最後に広げる。
64
+ 0.25.0 は成功の応答 (原本の 200 と縮小画像の 200・304) にだけ方針を付けたので、同じサイトのページが no-cors の `fetch` で、見える添付と見えない添付をトークンごとに見分けられた。
65
+ - **内容キーごとに展開は 1 つ**: 描画の締め切りを過ぎても続く描画がある間、同じ内容の次の生成は枠を取らずにその決着を待ち、結論が記録されていればキャッシュから答える (締め切りの後の描画の待ちと枠の待ちを合わせて `queueWaitMs` の内。尽きたら展開を重ねずに 503 `queue`)。以前は既定の 2 枠のもう 1 つで、締め切りを越えさせた混雑の中で同じ原本をもう一度読んで展開した。
66
+ 描画の結果は締め切りの前に決着しても後に決着しても同じ 1 つの分類で決め、オブジェクトでない結果は 500 (`port_contract` / `code=result`) になる (以前は例外)。
67
+ - **共有 SSE リーダーは `$` から読まない**: 読み取り位置はいつも具体 ID で、ストリームの末尾を読めないときは `ready()` が拒否する (失敗した読取りは残さず、次の呼び出しが読み直す)。同梱の SSE ハンドラはその接続を閉じ (`producer-failed`)、クライアントはカーソルがあればそれで開き直して追いつく。位置を固定できない run は、ほかの失敗した run と同じく全購読者の `onError` を呼ぶ。
68
+ 以前は末尾を読めないと `"$"` へ縮退し、2 回の XREAD の合間に足されたエントリを誰にも届けない窓が残った (`"$"` は XREAD のたびにその時点の末尾へ解決される)。
69
+
70
+ ### Added
71
+
72
+ - server の `DAILY_REPORT_SHARP_THUMBNAIL_DECODE_WORK_MODEL`: 描画の段の締め切り `renderMs` (10,000) と、展開の仕事の上限 (`nanosecondsPerBlockVisit`・`margin`・`maxBlockVisits`) とたどりの上限 (`walkBudgetMs`・`nanosecondsPerWalkStep`・`walkMargin`・`maxWalkSteps`) を持つ凍結した表。ホストの適合の検査は締め切りを書き写さずにここから読む。
73
+ - **開発者向け**: 公開のスクリプトは `pnpm run verify` の直後に `node scripts/check-bundle-size.mjs --exact` を走らせる。測った大きさが基準の項目と 1 バイトでも違えば (大きくても小さくても) 落とし、そのビルドを記録する `--write --accept` のコマンドを示すので、公開する版はどれも自分のビルドの基準を持つ。SQL の結果のキャッシュのキーは 1 つのモジュール (`cache-keys.ts`) が組み、誰も書かないキーを消す無効化の呼び出しを除いた。
74
+
75
+ ### Removed
76
+
77
+ - server の `SqlResultCache` の `invalidate` と `flush` (同じ処理の 2 つの名前)。完全一致のキーで消す呼び出しはどこにも無い (詳細のキーは利用者と可視性で区切るので、完全一致のキーは誰も書かないキーを指していた)。消すのは `invalidatePrefix` と `clearAll` の 2 つだけ。
78
+
79
+ ### Breaking
80
+
81
+ - `DailyReportSseReader.ready()` はストリームの末尾を読めないとき拒否する (0.24.0 と 0.25.0 は `"$"` へ縮退して解決した)。Migration: 自前の SSE ハンドラで共有リーダーを使うホストは、`ready()` の拒否で接続を閉じてクライアントに開き直させる (同梱のハンドラはそうしている)。リーダーを自前で差し替えるホストの `ready()` も、位置を具体 ID に固定できないときは拒否する (自前のリーダーが実装する型は、後の版が公開した `DailyReportSseReaderPort`)。
82
+ - ピアの `@aiquants/virtualscroll` の下限を 3.9.0 へ上げる。両ビューは 3.9.0 の `onScrollAdjust` で錨を記録し直し、揃えた端を守る装置の画素への揃えに頼る。Migration: `@aiquants/virtualscroll` を 3.9.0 以降へ上げる。
83
+ - server の `SqlResultCache` から `invalidate` と `flush` を外した。Migration: 1 つのキーを消していた呼び出しは、そのキーを接頭辞とする `invalidatePrefix(<キー>)` か、全部を消す `clearAll()` へ置き換える。
84
+
5
85
  ## 0.25.0 (2026-10-03)
6
86
 
7
87
  0.23.0 のキーボード操作とサムネイルの仕上げの続きと、共有 SSE リーダーの取りこぼしの修正。どれも README の該当の節が今の契約を記す。
@@ -11,7 +91,7 @@ All notable changes to `@aiquants/daily-report` are documented here.
11
91
  - **スクロールの錨はスクロールの直後に記録する**: ビュー自身のスクロール (キーの移動と長押しのフレームごとの移動・キーボードのフォーカスを見せるスクロール・List のモバイルのオーバーレイを閉じた後の揃えと行の枠の高さ P が変わったときの位置の直し・DetailList の選択の揃えとコメントの投稿の後の見せ方) はどれも 1 つのスクロールの口を通り、口はスクロールの直後に VirtualScroll のハンドルから錨を記録する。以前は次のフレームの可視範囲の知らせで記録したので、キーと次のフレームの間に確定した SSE の挿入・削除が位置をキーの前へ戻した (End・PageDown・長押しが巻き戻り、DetailList ではフォーカスが body へ落ちた)。
12
92
  利用者のスクロール (ホイール・ドラッグ・スクロールバー・慣性) は今までどおり可視範囲の知らせで記録し、知らせの前の 1 フレームに一覧が変わったときは、記録してからスクロールした距離だけ錨を前の一覧の行の高さで進めてから戻す (利用者のスクロールを巻き戻さず、1 行もずらさない)。錨が譲るのは、日報の到着を待つ DetailList の選択の見せ方だけ。
13
93
  - **キャッシュに無い日報の最初の読み込みはすぐ始める**: `useDailyReportDetail` はキャッシュに無い日報の要求を効果の中で始める。以前は 0 ms のタイマーを挟んだので、キーの長押しが続く間ずっと後回しにされ、キーを離すまで要求が出なかった。間を置く (120 ms) のは期限切れのキャッシュの日報の再取得だけ。id が変わった後とアンマウントの後に届いた結果は捨て、同じ営業日の要求は 1 つにまとまる。
14
- - **ビューの高さの最初の値も配置の px**: ビューの箱の高さは最初の値も根要素の計算済みの `block-size` (配置の px) から読み、以後の `ResizeObserver` の値と同じ単位になった (以前の最初の値は `getBoundingClientRect` の見た目の px で、祖先が変形・拡大縮小しているときだけ 1 フレーム後の観測で直った)。最初の確定から正しい高さで、2 回目の確定は起きない。内部のフックの名前も測るものに合わせた (`useViewBoxHeight`)。
94
+ - **ビューの高さの最初の値も配置の px**: ビューの箱の高さは最初の値も根要素の計算済みの `block-size` (配置の px) から読み、以後の `ResizeObserver` の値と同じ単位になった (以前の最初の値は `getBoundingClientRect` の見た目の px で、祖先が変形・拡大縮小しているときだけ 1 フレーム後の観測で直った)。祖先の変形・拡大縮小で 2 回目の確定は起きない (ただし端数のある高さは、計算済みのスタイルが長さを有効数字 6 桁の文字列にするので、最初の値 (743.656) と観測の値 (743.65625) が食い違って 2 回確定する)。内部のフックの名前も測るものに合わせた (`useViewBoxHeight`)。
15
95
  - **空のビューの文言は 1 つ**: 空の List と DetailList は VirtualScroll の空の文言 (`noItems`。en "No items"・ja 「項目がありません」) だけを描く。以前はパッケージの `listEmpty` の文言と 2 つが一覧の上端で 8 px 重なった。文言の上下の寄せ 16 px と色 (明 slate-500・暗 slate-400。ページに対して 4.55:1・7.66:1。エンジンの既定の灰は暗色のページで 4.17:1) はビューが与える。
16
96
  - **両ビューの行の枠は配置の境界**: DetailList の行の枠も List と同じく大きさ・配置・スタイルを閉じ込め (`contain: size layout style`)、VirtualScroll の行の箱を埋める。行の高さは本体 (枠の直下の子) を測って 2G を足した値。行の中の変化 (後回しにした本体の描画・読み込みの完了・選択とフォーカスの表示) の配置はその行から始まり、キーの移動の配置は変わる行の中に留まる (以前の DetailList は、長押しのフレームごとに文書の根から配置し直した)。
17
97
  - **側面ペインの見出しと本文は同じ端で終わる**: 営業日と作成者の列とタブの並びは、本文と同じスクロールバーの溝を取る箱に入る (以前は古典的なスクロールバーで、本文の列が見出しより 10 px 手前で終わった)。
@@ -26,7 +106,7 @@ All notable changes to `@aiquants/daily-report` are documented here.
26
106
  `@aiquants/virtualscroll` 3.8.2 の配布 CSS が、動きを減らす設定 (`prefers-reduced-motion: reduce`) で、パッケージが動かす部品 (タップスクロールサークルとその中のすべての要素・バーのサークルの器・矢印ボタン・スクロールバーのつまみ・端へ戻るボタンとその覆い) の遷移とアニメーションを止める (3.8.2 をピアの下限にした。Breaking を参照)。
27
107
  - **JPEG の展開の仕事の上限**: sharp の描画ポートは、sharp を起動する前に JPEG の展開の仕事 (EOI までの走査ごとに、符号化する成分のブロックの数の和) をフレームヘッダーと同じ 1 回のたどりで見積もり、描画の締め切りから導いた上限 (2,000 万回の訪問) を超える原本と、仕事を見積もれない原本 (算術符号と階層のフレーム、規格の上限を超える走査の数、崩れた区切り) を `unsupported` (内容で決まる失敗として記録) で断る。以前は 0.2 MB で 2,647 走査の 4,096² のプログレッシブの JPEG が、画素数と作業領域の上限を通って描画 1 回に CPU 13〜25 秒をかけ、締め切りを越えても何も記録しないので表示のたびに展開し直した。可逆 (SOF3) のフレームは受け付ける。
28
108
  - **締め切りの後の描画の結果を残す**: 描画の締め切り (`render_timeout` の 502) の後に描画ポートが決着したら、出力の検証を通った縮小画像と `unsupported` は、同一性を確かめた生成ならキャッシュに記録してから枠を返す (以前は捨てた)。遅いが正当な原本の超過は 1 回で済み、次の表示はキャッシュから答える。生成は枠を渡されたときにキャッシュを見直すので、超過した描画の後ろで待っていた要求はもう 1 度展開しない。sharp の描画ポートは展開を始めた後に中断されても、展開の結果をそのまま返す (以前は `failed` に変えたので、残す結果が無かった)。
29
- - **添付の応答は同じオリジンだけが読み込める**: 原本の 200 (インラインとダウンロード、GET と HEAD) とサムネイルの 200・304 は `Cross-Origin-Resource-Policy: same-origin` を持つ。セッションの Cookie は同じサイトの別オリジンからの読み込みにも付くので、無いと同じサイトのページが推測したトークンを画像として読み込み、読めたかどうかから閲覧者の可視性を 1 件ずつ知りえた。
109
+ - **添付の応答は同じオリジンだけが読み込める**: 原本の 200 (インラインとダウンロード、GET と HEAD) とサムネイルの 200・304 は `Cross-Origin-Resource-Policy: same-origin` を持つ。セッションの Cookie は同じサイトの別オリジンからの読み込みにも付くので、無いと同じサイトのページが推測したトークンを画像として読み込み、読めたかどうかから閲覧者の可視性を 1 件ずつ知りえた。失敗の応答 (404 など) は方針を持たないので、no-cors の `fetch` では、見える添付 (方針を持つ応答をブラウザーが塞ぐ) と見えない添付 (404 が opaque な応答として解決する) をまだ見分けられる。
30
110
  - **開発者向け**: バンドルの予算は書かずに導く: `bundle-baseline.json` (パッケージの根) が入口ごとの gzip-9 の大きさと余裕 `headroomPercent` (3) を記録し、予算は ⌈基準 × (100 + 余裕) ÷ 100⌉ (整数の計算)。手で書いた `package.json#bundleBudget` は拒否する。`pnpm run bundle:ratchet` (`--write`) が新しいビルドの大きさを記録し、下げるのと古い項目を消すのは自由、増やすのと項目を足すのは `--write --accept` だけ。
31
111
  ビューのスクロールの口を通らないハンドルのスクロールを落とす守り (`view-scroller.spec.ts`)、VirtualScroll の代役と本物を同じ props で比べる適合の spec (空の出力・矢印の Tab の止まり先・ビューが呼ぶハンドル)、`utils/constants.ts` が export するどの定数にも出荷するコードの読み手があることの守り (読み手の無かった `EMPTY_DETAIL` も消した) を加えた。
32
112
 
@@ -80,7 +160,7 @@ All notable changes to `@aiquants/daily-report` are documented here.
80
160
  - **`subscribe` の `onError` が必須に**: `DailyReportSseReader.subscribe(onEntry, onError)` の `onError` は省略できない。run の失敗は内部の `"error"` イベントでは
81
161
  なく各購読者の `onError` へだけ届き、呼ばれた時点で購読は解除済みなので、呼び出し側は接続を閉じてクライアントに開き直させる (同梱の SSE ハンドラはそうしている)。
82
162
  購読者が残っていても失敗した run は再起動せず、次の購読が新しい run を起動する。購読より前に読んだエントリは再生しない。`destroy()` は全購読者を通知なしで
83
- 外して run を止め、読み取り位置を初期化する (実行中の `subscribe` は例外になる)。リーダーを自前で差し替えるホストは `ready()` も実装する。
163
+ 外して run を止め、読み取り位置を初期化する (実行中の `subscribe` は例外になる)。リーダーを自前で差し替えるホストは `ready()` も実装する (実装する型は、後の版が公開した `DailyReportSseReaderPort`)。
84
164
 
85
165
  ## 0.23.0 (2026-10-03)
86
166