@aiquants/daily-report 0.38.0 → 0.40.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,162 @@
2
2
 
3
3
  All notable changes to `@aiquants/daily-report` are documented here.
4
4
 
5
+ ## 0.40.0 (2026-10-09)
6
+
7
+ ### Added
8
+
9
+ - **ids のストリームの行の部品を入口から出す**: 共有エントリの `forEachIdsStreamRow(line, visit)` (厳格な読み手 `isIdsStreamChunkLine` が受けた行の行を、連ごとにストリーム順でたどって ID・営業日・区分を渡す。行のオブジェクトは作らない) とその列の型 `DailyReportIdsStreamRowColumns`、server エントリの `encodeIdsStreamRows(items, start, end)` (ストリーム順の行の範囲を行の列へ符号化する。クライアントの束に入れないよう server エントリだけに置く)。ストリームを自分で読み書きするホスト (統合テスト・性能の試験の模擬) は列を書き写さない。
10
+ - **`DAILY_REPORT_SEARCH_BATCHES_IN_FLIGHT`** (共有エントリ): パッケージのクライアントが API の基点ごとに同時に送る、まとめて読む要求 (`reports?ids=` と `search-marks`) の数 (2)。サーバーは閲覧者の取り分をこの数から決め、ホストの試験は書き写さずに読む。
11
+ - **`DailyReportIdsStreamClient.readRows()` と `DailyReportRowBatch`** (client エントリ): `readRows()` は最後の publish の時点の一覧を、行のまとまり 1 つ (`DailyReportRowBatch`: `length`・`ids`・営業日と区分の符号の型付き配列・行のオブジェクトをそのつど作る `itemAt(i)` と `items()`) へ写して返す。`readItems()` は同じ一覧を行のオブジェクトで返す。
12
+ - **SQL 結果キャッシュの見積もったバイト数の上限 `cacheMaxBytes`** (`createDailyReportServer` の任意の設定。1 以上の整数で、既定 512 MiB): バケットの数の上限 (`cacheMaxBuckets`) と並ぶ 2 つ目の上限 (下の Changed)。
13
+
14
+ ### Changed
15
+
16
+ - **検索の答えの行の詳細は、送る時に決めたまとまりで読む**: 検索の答えを見せている間の ID でまとめた詳細 (`reports?ids=`) は、検索の印と同じ需要で決める送り手 (`createIdBatchScheduler`) で送る。まとまりは送る時に、まだ待つ行の ID から新しい順に 100 件ずつ、API の基点ごとに同時に 2 本まで (止めた要求も fetch が落ち着くまで枠を持つ)。要求は `no-cache` で、要求ごとの合図を持つ。
17
+ 行が外れると、送る前のその ID は送らず、どの ID にも行の残らない要求は止め、戻った行は頼み直す (止めた要求が落ち着く前に戻っても、止めた要求の拒否はその行に届かない)。答えは今までどおり日報ごとのキャッシュにだけ入り、2xx の答えに無い日報だけが null。
18
+ 以前は、頼まれた順の待ち行列で古い行から送り、送る前に外れた行の ID も送り、どの行も待たなくなった要求も最後まで読んだ。
19
+ - **絞り込みの候補は `no-cache` で読む**: `search-facets` の要求は `no-store` でなく `no-cache` で送る。ブラウザーは前の答えの ETag で確かめ直し (`If-None-Match`)、変わらなければ 304 を受けて自分のキャッシュの 200 を返すので、変わらない候補の本文は送り直されない (同じ答えの候補の配列は今までどおり同じまま)。
20
+ - **開発者向け**: 検索の選択の規則 (`search/search-selection.ts`) も行を `rowAt` で読み、`src/client/row-reads.spec.ts` が許す直の `.at(` は `rowAt` 自身の 1 つだけになった。`Retry-After` の読み取りは 1 つ (`retryAfterMsOf`、`src/client/utils/retry-after.ts`) で、検索の印と ID でまとめた詳細が共有する。
21
+ - **検索の 4 つの経路は 1 つの部品で組み、クライアントが離れた要求にはどれも本文の無い 503 で答える**: `search`・`reports`・`search-marks`・`search-facets` は 1 つの部品 (`searchRouteHandler`) が同じ順
22
+ (検索の設定が無ければ 404 → 引数の 400 → 利用者の解決 (内部ユーザーが無ければ受け入れもデータの問い合わせも無しの空の答え) → 要求のシグナルでの受け入れ → 読み取り → 答え。枠はどの結末でも返す) で組む。
23
+ 受け入れを待つ間・枠を渡された時・読み取りの後に中断された要求には、どの経路も答えの本文を組まず行も書かずに、本文の無い `Cache-Control: no-store` の 503 (`Retry-After` 付き、ETag 無し) で答え、
24
+ 枠を渡された時にもう中断されていれば読み取りを走らせない。読み取りがほかの誤りで失敗すれば今までどおり 500。サービスへ要求のシグナルを渡すのは今までどおり検索の印だけ。
25
+ 以前は検索の印だけがそう答え、検索・ID でまとめた詳細・絞り込みの候補は、止めた要求に 429 で答え、読み取りの後に離れた要求の答えも組んだ。
26
+ - **正規化の版はサービスの生成のときに計算を始め、順番を譲りながら計算する**: 検索の設定を持つサービスは生成のときに正規化の版 (プロセスで 1 つ、約 1 秒) の計算を始め、計算は見本の各段の前で
27
+ イベントループに順番を譲る (`computeSearchNormalizerVersionYielding`。版は今までとバイトまで同じ)。保存と突き合わせは、トランザクションの前にその終わりを待つ。
28
+ 計算が失敗すれば、待っていた保存はその誤りで失敗し (元の表も索引も書かない)、次の保存が計算し直す (`createSearchNormalizerVersionReader`)。
29
+ 以前は、プロセスの最初の保存か突き合わせが同期で計算し、その間 (約 1 秒) イベントループを止めた。
30
+ - **論理削除は索引を書き直さない**: 削除した日報は検索・印・ID でまとめた詳細・絞り込みの候補のどの読み取りの範囲にも入らないので、論理削除は日報の鍵も索引の書き直しも無しに、
31
+ 日報の行だけをトランザクションの中で書く。索引を書き直す保存の経路は 5 つ (作成・更新・公開・コメントの追加と削除)。削除の取り消しはパッケージの外の書き込みなので、ほかの外の書き込みと同じく突き合わせが後を追う。
32
+ - **開発者向け**: 検索の印の待ちの上限 `SEARCH_MARKS_WAIT_MS` を `BATCHED_READ_WAIT_MS` (2,000 ms。ID でまとめた詳細と検索の印が分け合う) へ改名し、検索の待ちの上限 `SEARCH_WAIT_MS` (3,200 ms) を足した
33
+ (どちらも `src/server/search/search-limiter.ts`。パッケージの公開の入口には出ない)。中断された要求の答えの部品は `answerAbandonedSearchRequest` (4 つの経路で共有)。
34
+ - **ids のストリームの行は列で持つ**: クライアントの常駐の行は、ID (`Float64Array`)・営業日と区分の符号 (`Uint16Array`・`Uint8Array`。ページに 1 つの符号の表)・ID から位置を引くヒープの外の開番地法の表 1 本 (`Int32Array`、埋まりは半分まで、後ろ詰めの削除)・位置ごとの印の 1 バイト (生きている行・走査が観測した行) で持ち、行ごとのオブジェクトも `Map` と `Set` の項目も持たない。
35
+ publish の変化の行も列のまとまり (`DailyReportRowBatch`) で運び (下の Breaking)、プロバイダーの並びの塊も列で持つ。行のオブジェクトは位置で読むときだけ、ID ごとの同一性の控え (1,024 件、最後に読んだ順) を通して作るので、載っている行は publish をまたいで同じオブジェクトで、値の変わった行だけが新しいオブジェクトになる。
36
+ 228,000 行を流してプロバイダーの行を導いても JavaScript のヒープの増えは 8 MB 未満で、4,000 行の publish の確保はバッファー 5 つほど。以前は行ごとにオブジェクト・`Map` の項目・`Set` の項目・塊の枠を持ち (228,000 行で 21.6 MB、publish ごとのバッファーは約 114)、押し続けの間のごみ集めが常駐の行の数で伸びた (性能の試験で、押し続けごとのごみ集めは D80kA で 18.8 ms から 12.9 ms、L80kA で 10.7 ms から 0 ms)。
37
+ - **プロバイダーは変化 1 つをそのまま当てはめる**: 1 回の描画が受けた変化が 1 つなら畳み直さず (クライアントが組んだ正味の変化は ID で互いに素の組なので、畳み直しても同じ)、2 つ以上のときだけ 1 つに畳む (`foldItemsChanges`)。4,000 行の publish の導きは 1.07〜1.19 ms から 0.29〜0.56 ms。
38
+ - **ids の行の厳格な読み手はより厳しい**: `isIdsStreamChunkLine` は、隣り合う等しいキー・行の中の同じ ID の 2 度目 (同じ営業日でも違う営業日でも)・同じ営業日の隣り合う 2 つの連 (正準でない連。符号化は書かない) も拒む。`isIdsStreamOrdered` も隣り合う等しいキーを並んでいないとする (ID の重なりは行の読み手が拒む)。
39
+ - **200 でない JSON の答えはどれも `no-store`**: API の経路の 200 でない答え (400・401・404・405・429・500・503。経路がエンドポイントを知る前の 401 と 404、ストリームの本体より前の断り、検索の離れた要求の本文の無い 503、キャッシュの取りこぼしの 503 を含む) は、行の方針によらず ETag を持たない `Cache-Control: no-store` (`jsonResponderByStatus`)。行の方針 (`etag` か `no-store`) は 200 にだけ当たる。経路の跡の `debug` の行は ETag を持つ答えだけが `etag_match` を書く。
40
+ 以前は `etag` の行の断りと経路の 401・404 が、304 になりえない本文のハッシュの ETag と `must-revalidate` を持ち、同じ 400 が行ごとに 2 通りのキャッシュの意味を持った (`API_ROUTE_JSON_CACHE` は削除)。
41
+ - **検索の印は区切りで計算し、答えに予算がある** (下の Breaking): 欄の印は続きから進める歩み (`createSearchMarkWalk`) で求め、要求の欄が分け合う計器で、見積もり 8 ms か単調な時計で 5 ms を過ぎたら次の段を始めず (区切りの最初の段はいつも走る)、`setImmediate` で順番を譲ってシグナルを確かめてから続ける (欄の途中でも)。位置つきの正規化は連なりの境目が落ちる片だけに当てる。
42
+ 答え 1 つは範囲 500,000・印を付ける生の単位 4,000,000 まで (`DAILY_REPORT_SEARCH_MARKS_BUDGET`) で、最初の日報はいつも答え、超える日報から後は印を付けずに `deferred` に並べる。欄 1 つの範囲が 10,000 を超えれば範囲を載せずに密 (`dense: true`) と答える。
43
+ 100 万単位の欄の最長の同期の区切りは 574〜794 ms から 6〜14 ms、`ab` を 50 万回並べた欄を `a` で探す答えは欄あたり 7.9 MB の JSON から密になり (その欄の日報 100 件の要求は 4 件を答えて 96 件を後回しにする 718 B)、よくある 100 件の窓の CPU は 18 % 減。
44
+ 値の除去と行の分割、1 行の分け方 (`planSearchLine`) は索引の文字の処理の 1 回の呼び出しのままで、最も遅い内容の 100 万単位では 1 つで 45〜150 ms かかりうる (README の **Slices**)。
45
+ - **一致の印は後回しの ID を頼み直し、密の欄を素で描く**: 印の store は、送った epoch のうちに届いて日報を 1 件以上運んだ答えの `deferred` を、行がまだ購読していれば同じ送り手で頼み直し、購読の外れた ID は頼んでいないものに戻す (日報を運ばない後回しは頼み直さない)。密の欄は印を塗らずに素で描き、行の文はその欄を打った語の当たりとして名指し、`表示外で一致` には数えない。
46
+ - **印の要求はまとめの窓で送る**: 行が購読した最初の ID は、窓の外ならすぐ (同じ処理の購読と一緒に) 送り手へ渡して 240 ms の窓 (`SEARCH_MARKS_BATCH_WINDOW_MS`) を開き、窓の中の購読は窓の終わりにまとめて渡す。押し続けのキーが先読みの端に載せる行は見えるまで約 0.5 秒あるので、印は見える前に届く。性能の試験の押し続けごとの要求は 9 本から 4〜5 本。
47
+ - **載る行の印の手間**: 行の葉は行の印が届くまで集計へ登録せず (行の最初の描画の登録は 12 から 0)、集計は要約を読むときに作って、聞き手へは処理の終わりに 1 回だけ知らせる (印が届いた確定の知らせは行ごとに 13 から 1)。
48
+ - **行の要素を減らす (画素は同じ)**: DetailList の本文カードは見出しと本体を直に並べ (間を空けるだけの包みを外す)、メタデータの列は組を `div` で包まずに `dl` の直下の `dt` と `dd` で描き (組の間 8 px と見出し語と値の間 4 px は自分の余白)、日時も削除の枠も無いコメントは右の並びを描かず、コメントのフォームは自分で末尾へ寄って寄せるだけの包みを持たず、List のカードの営業日の札は日付の文字を直に持つ。
49
+ 行の要素は List のカードで 20 から 19、DetailList の本文カードで 125 から 113 (答えを見せていれば 142 から 130)。Chromium の描画は dpr 1・1.25・2、明暗、幅 952 px と 560 px で画素が同じ。
50
+ - **クラスは描くたびに重ねない (残り)**: `Button`・`Input`・`Textarea`・`Label` はバリアント・大きさ・呼び出し側のクラスの組ごとに 1 回だけ重ね (`createClassMerger`。DetailList の自分の下書きの行が開く編集で行あたり 7 回)、添付の節のリンク・タイル・行・見出しのクラスはモジュールの定数、大きさの注記と名前のクラスは呼び出し側のクラスごとに 1 回 (描き直しで 13 回から 0 回)。決まったクラスの重ね合わせはモジュールの直下にだけ書く (`src/client/ui/class-merge-sites.spec.ts` が出荷するクライアントのソースを型検査器で読む)。
51
+ - **SQL 結果キャッシュは見積もったバイト数でも縛る**: `SqlResultCache` はバケットの数 (`maxBuckets`) に加えて見積もったバイト数 (`maxBytes`。`estimateRecordsBytes`: 値ごとに 8 バイト、文字は UTF-16 の単位ごとに 2 バイト、バイナリの view はそのバイト数。上限を超えたところで止める) の合計でも、最も古く書いたバケットから捨て、1 つで上限を超える結果は持たない (読み取りには返す)。
52
+ 日報の本文とコメントの長さは書き手が決めるので、バケットの数だけでは営業日 1 つのバケットが何百 MB にもなりえた。
53
+ - **下書きかは詳細の `isDraft` で決める** (下の Breaking): どの詳細 (HTTP の答え・action の答え・SSE の中身) も、サーバーが下書きの 1 つの定義 (SQL の中でラベルの名前を引く `buildDraftLabelExists` か、下書きの名前の作成区分) で決めた必須の `isDraft` を持ち、ビューは名前を比べずにこの値で作成者の下書きを編集の形で開く。楽観的に作る日報は `isDraft: true` でラベルを持たず (下書きの札はサーバーの日報で出る)、公開は答えの前に `isDraft` を外し (ラベルの札は答えまで残る)、失敗で戻す。
54
+ - **開発者向け**:
55
+ - バンドルの大きさの増加は理由とともに記録する: `--accept` には `--reason '<文>'` が要り、`--write --accept --reason` は増えたファイルごとにコミットする台帳 `bundle-growth-ledger.json` (`version`・`from`・`to`・`deltaPercent`・`reason`) へ項目を足してから基準を書く。基準の項目が台帳の最後の大きさを超えるか台帳に項目が無ければ、どの検査も落ちる。`--write` は esbuild のメタファイル (`tsup --metafile`、tarball には入れない) から入口ごとのモジュールのバイト数を出す。
56
+ - spec の種から決まる乱数は 1 つの生成器 (`createSeededRandom`、`src/shared/test-helpers/seeded-random.ts`。mulberry32) で、`src/client/test-helpers/seeded-random-copies.spec.ts` がクライアントと共有の木にほかの生成器の写しが無いことを確かめる。
57
+ - ids のストリームのクライアントは中断を名前 (`AbortError`) で見分ける (別の realm の `DOMException` は `instanceof Error` でなく、jsdom ではその中断が読み直しに数えられた。ブラウザーと Node の振る舞いは同じ)。接続の層 (ヘッダーの待ち・`online` の起こし・中断の分類・読み手の取り消し・待ちの終わり) は spec がどの分岐も通る。
58
+ - `useLazyRef` (`src/client/hooks/use-lazy-ref.ts`) は初めの値を 1 回だけ作る ref (キーボードのフックと DetailList のスクロールの復元が使う)。行の要素の数 (`row-element-count.spec.tsx`)・行の状態の行列でのクラスの重ね合わせ (`row-class-merge.spec.tsx`)・常駐の行のヒープ (`resident-rows-heap.spec.ts`)・載る行の印の手間 (`row-marks-first-mount.spec.tsx`) を spec が固定する。
59
+ - `src/docs-examples.spec.ts` のスキーマの走査は、公開する README と CHANGELOG に加えて `docs/plans` と `docs/specs` の markdown も読む (角括弧のスキーマの名前は文字か `_` で始まるものだけを数え、配列の展開やファイル名の省ける部分を数えない)。
60
+
61
+ ### Fixed
62
+
63
+ - **混み合いの 429 で検索の答えの行が誤りにならない**: ID でまとめた詳細の 429 は、`Retry-After` が 5 秒 (`ID_BATCH_MAX_RETRY_AFTER_MS`。受け入れのゲートが満杯のときの 1 秒を含み、レートのバケットの窓の 60 秒より短い) の内なら、行を誤りも null も持たない読み込み中のまま待たせ、待ちの後にまだ待つ行の ID を 1 回だけ頼み直す。
64
+ 2 度目の 429 と、`Retry-After` の無い・読めない・上限を超える 429 は今までどおり `[daily-report] reports answered HTTP 429` の誤り。以前は、枠がすぐ空く 1 秒の 429 でも、まとまりのどの行もすぐに誤りを持った。
65
+ - **キャッシュの全消去の前に送った詳細の答えを、消した後のキャッシュへ書かない**: キャッシュの全消去 (ユーザーの切り替えと開発用の全消去) は、送っている ID でまとめた詳細の要求を止め、やり直しの待ちを消し、まだ待つ行の ID を新しい要求で頼み直す (やり直しも新しく 1 回持つ)。
66
+ 以前は、全消去の前に送った要求が最後まで読まれ、その答えが消した後のキャッシュへ入った。
67
+ - **混み合いのゲートで、利用者の検索と新しい答えの詳細をすぐに断らない**: 受け入れのゲートが満杯か閲覧者が取り分を持ちきっているとき、検索は `SEARCH_WAIT_MS` (3,200 ms。ステージングで測った最も遅い LIKE の検索
68
+ 1.6 秒のおよそ 2 倍) まで、ID でまとめた詳細は検索の印と同じ `BATCHED_READ_WAIT_MS` (2,000 ms) まで、閲覧者ごとに `DAILY_REPORT_SEARCH_BATCHES_IN_FLIGHT` 個・全体で受け入れの同時数まで枠を待ち、
69
+ 待ち行列か待ちの上限を超えれば今までどおり 1 秒の `Retry-After` の 429。すぐに断るのは絞り込みの候補だけ (クライアントは 1 度に 1 つしか送らない)。
70
+ 以前は、解除した前の検索やほかのビューの検索の SQL が閲覧者の枠を持つ間 (SQL は終わるまで枠を持つ) に確定した検索が「混み合っている」で終わり、前の答えの捨てられたまとまりが取り分を持つ間に届いた
71
+ 新しい答えの最初の詳細のまとまりもすぐに 429 で断られた。
72
+ - **絞り込みの行を開いた帯の下の隙間はちょうど G**: 帯の高さを格子のブロックの刻み q へ切り上げた余り (0 から q − 1 px) は、検索の行と絞り込みの行の間に来る。帯 (`VIEW_TOOLBAR_BAND_CLASS_NAME`) は縦の flex の列になり、
73
+ 検索窓のランドマーク (`SEARCH_BOX_CLASS_NAME`) が列の空きを受け取って 2 つの行の間に置く。行の最後の中身 (箱、伝えることがあれば状態の行) は帯の下端で終わり、閉じた行の操作部品と同じくビューの最初の面のちょうど G 上にある。
74
+ 行を開いても検索の行は動かず、検索の操作部品から見出しまでは G 以上 G + q 未満で、行の中の隙間 (見出しと部品の間 4・`sm` 未満の箱の間 G・状態の行の上 G) は変わらない (余りを見出しと部品の間に置くと、q = 8 でその間が 11 まで広がり、上の箱の部品のほうに近く読める)。
75
+ 以前は余りが帯の下端に残り、高さ 38px のホストの部品で、帯の最後の中身からビューの最初の面までが q = 4 で G + 2、q = 8 で G + 6 になった。
76
+ - **絞り込みの切り替えは開いた状態を見せ、AND/OR の切り替えと同じ見た目の文法に従う**: 2 つの切り替えは 1 つの面の組 (`SEARCH_TOGGLE_FACE_CLASS_NAMES`) から作る。12px の semibold の文字の 24px の丸い札で、
77
+ 絞り込みの切り替えは閉じていれば欄の面 (OR と同じ)、開いていれば塗りの面 (AND と同じ blue-700 に白の文字、暗色は blue-600。強制配色では `HighlightText` を載せた `Highlight`) で描く (`SEARCH_FILTERS_TOGGLE_CLASS_NAMES`)。
78
+ 面の間で違うのは色だけなので開け閉めで切り替えは動かず、名前・`aria-expanded`・`aria-controls` は今までどおり。AND の面も強制配色では `Highlight` で描く。
79
+ 以前は絞り込みの切り替えが角の丸い四角の欄の面 1 つで、開いても閉じても同じに見え (違うのは `aria-expanded` だけ)、AND/OR の切り替えと形も文字の太さも違った。
80
+ - **絞り込みの行は検索のランドマークの中にある**: 検索窓の `<search>` (`data-testid="daily-report-search"`) は検索の行と、開いている間は絞り込みの行を持つので、絞り込みの箱は検索の一部として支援技術に届く。
81
+ Tab の順 (入力欄・絞り込みの切り替え・絞り込みの箱) と帯の見た目は変わらず、ランドマークは名前を持たないまま。以前は絞り込みの行がランドマークの外 (その後ろの兄弟) にあった。
82
+ - **壊れた ids の行で一覧を欠いたまま完走しない**: JSON として読めない行と厳格な読み手が拒む行は、警告 1 行 (`rejected a malformed line`) で接続を止めて試行を終え、最後に受けた行のカーソルとトークンから読み直す (それまでに受けた行は残り、その前進は失敗の数を戻す)。拒み続ける行は、失敗の上限で最初の走査を `failed` にし、再検証は誤りを持って諦める (`complete` と完走の時刻のまま)。
83
+ 以前は壊れた行を警告して読み飛ばし、後の行の `isLast` で走査が `complete` になり (最初の走査は欠けた一覧で完走し)、再検証では完走の剪定がその行の日報 (最大 4,000) を一覧から消した。
84
+ - **再開の値はサーバーが読み返す形のときだけ採る**: 行の `nextCursor` は ID が正の安全な整数で営業日が正準のキーか null のとき (`isResumableIdsStreamCursor`。サーバーの再開の解釈と同じ判定)、`snapshot` はトークンの形のとき (`isIdsStreamSnapshotToken`)、`total` は 0 以上の安全な整数のときだけ採る (行の日報は受ける)。以前は数ならどのカーソルの ID (1.5・0・1e21) も採り、サーバーが再開をどれも 400 で拒んで走査が失敗するまで繰り返した。
85
+ - **ホストが同じ選択を続けて断っても、側面ペインは List の選択を見せる**: キー・行の起動・クリックの経路が選択の置き場へ書くたびに、合わせる効果が同じ確定で走る (頼みの数 `selectionRequest`)。以前は、断った矢印キーの後の Enter・2 度の矢印キー・2 度のクリックで置き場がホストの選択 (601) と違う日報 (602) のまま残り、側面ペインが List の選択していない日報を見せた。
86
+ - **添付のファイル名は利用者の文字の間隔の上書きでもはみ出さない (WCAG 1.4.12)**: 名前は計算済みの字間 (コードポイントごと) と語間 (語の区切りの文字ごと) まで数えて測る (canvas の `letterSpacing` は Safari 17.4 に無いので算術。負の値は数えない)。数えられない形 (パーセント・それを含む数式) の列は収めずに CSS の省略で描く。
87
+ 以前は書体だけで測ったので、字間 0.12em・語間 0.16em の上書きで、収めた名前が 240 px の列から 29〜70 px はみ出して隣のタイルの名前や行の操作に重なった (Chromium 148 の探りで 7 つのうち 2 つ、最大 64.2 px)。収めた後に掛かった上書きは次の収め直しまで残る (README の **File name** の Limit)。
88
+ - **文書の誤り**: README の添付の格子の例は細い古典的なスクロールバーの溝を 10 px とした (Chromium 148 の Linux では 11 px で、側面ペインの格子は W = 725、列 231 px で余り 0、電話の列は 165 px)。端の溝の実測の例は窓の高さを書かず、別の窓の高さの値を混ぜていた。0.37.0 の Migration の行は公開の後に足した前方への参照を外し、公開した文に戻した。
89
+
90
+ - **配布する CSS にサーバーのコードの語から作った規則を載せない**: standalone の CSS の走査は、spec とテストの補助に加えてサーバーのコード (`src/server/**`・`src/server.ts`) を外す。要素を描かないサーバーのコードの語 (列名の `ordinal`・文書の `lowercase` や `paused`) から作った `.ordinal`・`.lowercase`・`.paused` は、パッケージのどの要素にも付かないのに、同じクラス名を持つホストの要素の見た目を変えていた (`.paused` はアニメーションを止める)。README の Tailwind のホストの例も、公開物では `dist/client.mjs` だけを、モノレポではサーバーのコードを除いて走査する。
91
+
92
+ ### Breaking
93
+
94
+ - **`DailyReportDetail.isDraft` は必須で、SSE のストリームのキーは `:v3`**: どの詳細もサーバーが決めた `isDraft` を持ち、SSE のスキーマ (`dailyReportDetailSchema`) は既定値で補わずに必須で読む。`isDraft` を持たない前の版のエントリを読まないよう、ストリームのキーの版を上げた (`<streamKey>:v3`)。
95
+ Migration: データベースへ書くどのワーカーもこの版で一緒に配備する (前の版のワーカーは `:v2` へ書き続け、新しい版の接続には届かない)。前の版のストリーム (`<streamKey>:v2`) は書かれなくなるので、手で消せる。詳細を自分で組むホスト (代役のサービス・試験の固定値) は `isDraft` を足す。
96
+ - **クライアントの `config.draftLabelName` は削除**: ビューは名前を比べずに `isDraft` で下書きを決めるので、設定は下書きの名前を持たない (`draftLabelName` を渡すと閉じたキー集合の外の未知キーとして `RangeError`)。サーバーの `draftLabelNames` は今までどおり必須。
97
+ 楽観的に作った日報はサーバーの日報が届くまでラベルを持たないので、下書きの札はそのときに出る。公開は答えの前に下書きの状態を外し、下書きのラベルの札は答えまで残る。
98
+ Migration: `config` から `draftLabelName` を消す。
99
+ - **`search-marks` の答えは `deferred` を持ち、欄の印は密でもありうる**: 答えは `{ wordsDigest, reports, deferred }` で (`DailyReportSearchMarksAnswer.deferred` は必須。予算を超えて印を付けなかった読める日報の ID)、欄の印 (`DailyReportSearchFieldMarks`) は `{ fingerprint, ranges }` か `{ fingerprint, dense: true }` (範囲が 10,000 を超える欄。範囲を持たない)。
100
+ Migration: サーバーとクライアントを同じ版で配備する (前の版のクライアントは後回しの ID を頼み直さず、密の欄を持つ答えを丸ごと拒む。この版のクライアントは `deferred` の無い前の版の答えを拒む。どちらもその行は印なしになる)。答えを自分で読むホストは `deferred` の ID をもう一度頼み、`"ranges" in field` で密の欄を見分ける。`markDailyReportSearchHits` を直接呼ぶホストも同じ形を受ける。
101
+ - **`SqlResultCache` の構築は `maxBytes` を求める**: `new SqlResultCache({ defaultTtlMs, maxBuckets, maxBytes }, epochStore)` の `maxBytes` (見積もったバイト数の上限、1 以上の整数) は省けない。`createDailyReportServer` の `cacheMaxBytes` は任意 (既定 512 MiB)。
102
+ Migration: キャッシュを自分で作るホストは `maxBytes` を渡す (`createDailyReportServer` を使うホストは何も変えない)。
103
+ - **ids のストリームの変化の行は列のまとまり**: `DailyReportIdsStreamItemsChange` の `appended` と `removed` は `DailyReportRowBatch`、`replaced` は値の変わった行の前と後の値の組 `{ previous, next }` (`DailyReportIdsStreamItemReplacements`。2 つのまとまりの行 `i` は同じ日報) で、`DailyReportIdsStreamItemReplacement` (行ごとの値の変化) は無い。
104
+ Migration: 変化を読むホストは、まとまりの行を `itemAt(i)` (`length` まで) か `items()` で読み、値の変化は `replaced.previous.itemAt(i)` と `replaced.next.itemAt(i)` の組で読む。一覧全体は `DailyReportIdsStreamClient.readRows()` (列) か `readItems()` (行のオブジェクト) で読む。
105
+
106
+ ## 0.39.0 (2026-10-09)
107
+
108
+ ### Added
109
+
110
+ - **キーボードで移る読み上げも何に当たったかを言う**: 検索の答えを見せている間、行は印を描いた欄の見出しごとに 1 文を持つ (ビューの描く順)。打った語の印が 1 つでもある見出しは新しい整形関数 `searchFieldMatched(field)` (en `Author matched`、ja `作成者で一致`)、読みの印だけの見出しは `searchFieldMatchedByReading(field)` (en `Author matched by its romaji reading`、ja `作成者がローマ字の読みで一致`) で言い、`field` は欄の見出しの解決済みの文言 (見出しの札とメタデータの列の作成者は 1 つの `author`、面談者・添付・コメントの欄はそれぞれの節の見出し)。文は行の葉が集計へ登録したものから作る (`describeDrawnSearchMarks`) ので、ビューが描かない欄を言わない。
111
+ DetailList の行と側面ペインの一致の印の隠した文は、当たったキーワードの数の文・この文・表示外の文の順 (表示外の文は印を描いた欄が無いときだけ)。List のカードは見える印を持たず、画面が印の store を持つ間、主ボタンの説明 (`aria-describedby`) のプレビューの後に、カードの印の囲みの中の隠した説明 (`hidden`、`data-daily-report-search-match-description`) を足す。印の `aria-description` は閲覧の読み上げのために今までどおり持つ。固有キーは 108 から 110 (文字列のキー 89・整形関数 21。キーの表は全 121 キー)。
112
+ 以前は、矢印キーで行を移る読み上げに何に当たったかが届かなかった: List のカードの作成者の印は主ボタンの子で (ボタンの子は読み上げで平らになり、読みの印の説明は落ちる)、プレビューは説明として平らに読まれ、DetailList の行の説明は状態・添付・種別と当たったキーワードの数だけだった。
113
+ - **`DailyReportRows`** (client entry): 日報の行の形 (`Pick<ReadonlyArray<DailyReportItem>, "length" | "at"> & Iterable<DailyReportItem>`) の名前。`DailyReportActionState.items` と両ビューの `dailyReportItems` はこの名前で宣言し、0.37.0 で変わった `items` の型を、ホストは書き写さずに読み込める。
114
+
115
+ ### Changed
116
+
117
+ - **ids のストリームは行を列で運ぶ** (下の Breaking): チャンク行 (`DailyReportIdsStreamChunkLine`) は行のオブジェクトの配列 (`items`) を持たず、ストリーム順の ID (`ids`)・続いて等しい営業日の連 (`dates` と、連ごとの行の数 `counts`)・区分の辞書 (`sourceTypes`。null は区分を持たない行) と行ごとのその添え字 (`sources`) を運ぶ。エンドポイントの名前は `ids-columns` (`DAILY_REPORT_API_ENDPOINTS.idsStream`)。
118
+ 4,000 行の行は約 300 KB から約 37 KB になり (ステージングの 228,222 行の一覧の本文は 17,046,814 B から 2,113,845 B)、受け手の JSON の解釈は行ごとのオブジェクトを作らない。クライアントは連をたどり (`forEachIdsStreamRow`)、値の変わった行と新しい行だけにオブジェクトを作るので、値の同じ再検証は行を何も作らない。
119
+ Node での計測 (228,222 行): 行 1 つの解釈と組み立ては 1.49 ms から 0.45 ms、クライアントの全量の走査は 249.6 ms から 188.1 ms。性能の試験の押し続けでは、ストリームの読み (`streamOnce`) が 1 回の押し続けあたり 13.7〜20.0 ms から 7.4〜9.9 ms、解釈と行のつなぎの割り当てが 1.5〜1.9 MB から 0.26〜0.56 MB、80,000 行での GC が 17.4 / 24.0 ms から 1.9 / 2.2 ms になった。
120
+ - **`isIdsStreamChunkLine` はワイヤの行の唯一の厳格な読み手**: 受けるのは `kind` が `"chunk"` で、列の長さがそろい (ID の数 = 連の行の数の和 = 添え字の数、営業日の数 = 連の数)、どの連の数も 1 以上、どの添え字も辞書の範囲、辞書はどれも文字列か null、どの営業日も正準の `YYYY-MM-DD` か null、どの ID も正の安全な整数で、行がストリームの順に並び、`isLast` が真偽で `nextCursor` が null かカーソルの形の行だけ。クライアントは読めない行を警告して読み飛ばし、カーソルから再開する。
121
+ 行の分類 (`classifyIdsStreamLine`) は `kind` だけで決め、`kind` を持たない行は種類の分からない行 (警告して読み飛ばす)。以前は `kind` の無い行を `items` の配列か `error` の文字で分けた。
122
+ - **エンドポイントの表は JSON の答えのキャッシュの方針を持つ**: JSON の行は `cache` (`etag` か `no-store`) を持ち、経路はその方針の部品をハンドラーへ渡して、200・断り・経路の 405 と 500 のどれもその部品で組む。`no-store` は `search-marks` だけで、ほかの JSON のエンドポイントは `etag`。どの 429 も 1 つの断りの部品が組む。経路の跡の `debug` の行は、`etag` のエンドポイントで `<状態> endpoint=<名前> etag_match=<true|false>`、`search-marks` で `<状態> endpoint=<名前>`。
123
+ 以前は `search-marks` の 405 と 500 が ETag を持ち、跡の行は ETag の無い答えにも `etag_match` を書いた。
124
+ - **進捗の札の点は動かない**: ids のストリームの進捗の札の点は明滅しない (件数と割合が publish ごとに変わる)。終わらない動きは、合成器に入力の無いフレームを毎秒約 60 回描かせ続け、押し続けたキーのフレームはその描画の後ろで待つ (明滅のあった間は、ストリームの配信中に測ったどの条件でも、押し続けのキーの描画の前の待ちが p95 で 8.6〜16.2 ms、明滅が無ければ約 0.2 ms)。
125
+ 残る終わらない動きは、読み込みか再接続の間だけ出る 4 つ (再接続中と最新化中の札の回転・List の行と側面ペインの読み込み中のスケルトン・見えているサムネイルの枠の読み込み中の地) だけで、`src/client/ui/motion-policy.spec.ts` がソースと配布する CSS でその場所と数を理由と一緒に許す。
126
+ - **側面ペインは選択を選択の置き場から読む**: List のビューの選択と、長押しの間に側面ペインの中身が追う落ち着いた選択は、ビューごとの選択の置き場 (`keyboard/list-selection-store.ts`) にあり、側面ペインは `useSyncExternalStore` で購読する。キーの経路はホストへの選択と同じ同期の確定の中で置き場へ書くので、キー 1 つの確定は 1 回のまま。一覧と側面ペインを並べるパネルの組をコンテキストで包まないので、キーのたびに React がその部分木をたどって選択の読み手を探すことは無い (List の押し続けで、コンテキストの走査は 1 回の押し続けあたり 8.1〜10.1 ms から 4.6〜5.4 ms)。
127
+ - **行のクラスは描くたびに重ねない**: DetailList の行の部品 (メタデータの組・種別のバッジ・面談者・コメント・未読の目印) と両ビューの読み込み中のスケルトンは、決まったクラスをモジュールで 1 回、値で選ぶクラスを状態ごとに 1 回だけ重ねる (`createClassMerger`)。もう描いた状態の行を描いても tailwind-merge は呼ばれない (12 行を足して 0 回。重ねていたときは List で 64 回、DetailList で 288 回)。
128
+ - **添付の行の名前も測って収め、何も切らない**: 添付の行の名前は、行の残りの幅に伸びる名前の列 (`data-testid="daily-report-attachment-name-track"`) の幅へ、タイルの格子の名前と同じく測って収める (`observeNameTrackWidth`。文書ごとに 1 つの観測器が格子の容器と行の名前の列を見る)。収めた名前は切り取らない 1 つの文字列で、列幅を超えない (省略記号と拡張子も収まらない幅では、その形を幅に収まるだけの文字で切る)。列幅が届くまでの最初のフレームとサーバー描画は今までどおり CSS の省略。
129
+ 大きさの注記は自分の描画の層を持つ箱 (`relative truncate`) で省略する。添付の節で切り取る箱はどれも自分の描画の層を持つので、添付のある DetailList の行も、窓をずらすスクロールの 1 段で描き直すのは素の行と同じ 2 行 (Chromium 148、幅 952 px と 496 px のカード。以前は 9 行)。収まらない大きさの注記を持つ狭いカード (幅 260 px、列 122 px) では 10 行のまま (README の **File name** の Limit)。
130
+ - **画面に出さない文のトークンは layout を閉じ込める**: `VISUALLY_HIDDEN_CLASS_NAME` 自身が `contain-layout` を持つ (種別のバッジの展開した名前・一致の印の隠した文・ファイル名の読み上げ用の写しのどれも、1 px の箱からはみ出す文が外の配置に加わらない)。
131
+ - **側面ペインの作成者の行の間**: 作成者の行の部分の間は 8 px、折り返した行の間は 4 px (`gap-x-2 gap-y-1`)。種別のバッジは外側の余白を持たないので、折り返して行の頭に来たバッジは行の始まりにそろう。以前は部分の間が縦横とも 4 px で、側面ペインのバッジだけが左の余白 8 px を持ったので、作成者とバッジの間は 12 px、バッジと一致の印の枠の間は 4 px で、折り返したバッジは行の頭から 8 px 内側に始まった。
132
+ - **ログ: 注入した警告の書き先も接頭辞を持つ**: `DailyReportIdsStreamClientOptions.warn` へ注入した書き先は IdsStream のチャンネルのロガーとして包むので、どの行も `[DailyReportIdsStream]` の接頭辞を 1 つ持つ文面と、その後の行のほかの引数で受ける。以前は注入した書き先だけが接頭辞の無い文面を受けた。
133
+ - **`markDailyReportSearchHits` は要求のシグナルを確かめる**: `options` が `AbortSignal` を持つオブジェクトでなければ、どのクエリよりも前にホスト向けの誤りで止める (`options` か `signal` が無ければ `TypeError` の `[daily-report] markDailyReportSearchHits: options.signal must be the request's AbortSignal (new AbortController().signal for a call that is never aborted)`、ほかの値は `; got <値>` で終わる `RangeError`)。以前は分割代入とプロパティの読み取りのエンジンの例外で落ち、空の ID の並びでは `{}` をそのまま通した。
134
+ - **開発者向け**:
135
+ - `src/client/row-reads.spec.ts` は出荷するクライアントのソースを型検査器で読み、位置から日報の行を読む `.at(` が `rowAt` のほかに無いこと (検索の選択の 4 つの読み取りは名前で挙げた例外) と、行の形を `DailyReportRows` の 1 か所でだけ書くことを確かめる。
136
+ - `src/server/service.drafts.spec.ts` はサービスの SQL をメモリの SQLite で流し (`test-helpers/sqlite-report-db.ts`)、作り直した・後から入れた下書きのラベル、2 つの下書きのラベルの公開、ラベルの無い作成、作成区分の DTO を確かめる。
137
+ - 印の範囲 (`markSearchText`) は、出現を 1 つずつ並べる前の定義 (`test-helpers/search-mark-occurrences.ts`) とバイト単位で同じことを、種から再現する 3,000 の乱数の場合・4,000 単位を超える行・適合の語群で確かめる。
138
+ - 行の並び (`ReportRowList`) の編集は、塊の境目の近くの削除・置き換え・差し込みを種から再現する 16 × 200 の場合で素の配列と照合する (`test-helpers/seeded-random.ts`)。
139
+ - 一致の印と面のずれは、印の欄の鍵 14 個 × 面 4 つ (List のカード・側面ペインの記事と関連のタブ・DetailList の行) の表で確かめ、葉が登録する数は描いた `<mark>` の数と等しい。
140
+ - ID のまとまりの送り手は、止めた要求が送る処理の落ち着くまで枠を持つことを確かめる。
141
+
142
+ ### Fixed
143
+
144
+ - **印の計算は出現の数で伸びない**: `search-marks` は、欄ごとに候補の重なる出現を正規化した文字の中で 1 つの連なりにまとめてから生の値へ写し、連なりごとに tier の覆いへ足す (`forEachSearchMarkRun`)。出現ごとのオブジェクトを作らないので、欄の費用は欄の長さ × 候補で、答えは同じ (出現ごとの定義とバイト単位で同じ)。サービスは欄の間 (日報の間を含む) で event loop に順を譲り、欄の前ごとにシグナルを確かめる。
145
+ 以前は欄の出現を 1 つずつ並べてからまとめたので、1 人の作成者が自分の日報の長い本文 1 つで worker の event loop を秒の単位で止め、メモリを 1 GB 越えへ押し上げられた (同じ文字 100 万単位の欄 1 つを、その文字を 1〜30 個並べた 30 語で探す印の計算は、重なる出現が 3,000 万近くあり 4,541 ms・2,044 MB かかった。今は回る連なりが 1,000,029 で 659 ms・323 MB、そのうち 584 ms は行の長さだけに比例する位置つきの写し)。
146
+ - **下書きかの判断は、判断のたびに名前で SQL の中で**: SSE の宛先 (`report-create`・`report-update`・`report-publish` の中身と、`comment-add`・`comment-delete`) は、読み取りの規則と同じ名前と相関の EXISTS で、日報 1 件の 1 行の読み取り (`isDraftReport`) で決める。公開は下書きの名前のラベルをトランザクションの中ですべて外し、外した後も下書きなら公開のイベントを作成者だけへ送る。作成は正準のラベル (`draftLabelNames[0]`) を挿入と同じトランザクションの中で名前で引き、無ければ何も書かずにそのラベルを名乗って失敗する (下の Breaking)。作成区分が下書きの日報の DTO に足す下書きのラベルは、読み取りのたびに名前で引いた正準のラベル自身の ID を持つ。`getDraftLabelId()` は何も覚えず、呼ぶたびに名前で引く。
147
+ 以前は宛先をプロセスの最初に引いて覚えたラベルの ID で決めた (空の答えも覚えた) ので、下書きのラベルを作り直したり後から入れたりすると、worker は下書きの保存とコメントを全員へ配り、読み取りでは隠れたままだった。公開は正準のラベルだけを外したので、2 つ目の下書きの名前のラベルを持つ日報は読み取りで隠れたまま中身が全員へ届いた。作成は正準のラベルが無いとラベルの無い日報 (誰にでも見える) を作った。作成区分の DTO のラベルは、覚えた ID の最小 (ステージングでは「作成中」の 2) を正準の名前で見せた (今は正準のラベルの 3)。
148
+ - **一致の印の store は自分の失敗だけを黙って捨てる**: 握りつぶすのは要求の失敗 (通信の失敗・中断・ほかの状態・読めない本文) だけで、ホストの `onSessionExpired`・429 のやり直しの予約・答えを当てて行へ知らせることが投げた例外はまとまりの拒否になり、ID のまとまりの送り手が枠を空けた後に `reportError` で報告する (ページの `error` の出来事)。報告しないのは、止めた要求がその中断の理由で拒まれたときだけ。以前は 4 つをまとめて 1 つの `try` で捨て、送り手もどの拒否も捨てたので、ホストの処理や当てはめの不変条件の誤りがコンソールにも出来事にも残らなかった。
149
+ - **一覧の外の位置は行を読まない**: ビュー・キーボード・スクロールの位置から行を読む場所はどれも `rowAt` を通り、一覧の行でない位置 (負・端数・長さ以上) には undefined を答える。以前は 0.37.0 の `at` の読み取りが、行の `at` と同じく負の位置を末尾から数えたので、DetailList のスクロールの復元のアンカーが -1 なら末尾の行を復元の先にし、窓の比べ方は -1 の窓を末尾の行と比べえた。
150
+ - **文書の誤り**: README は `search-marks` を本文から ETag を作るエンドポイントに数え、読みの印の `aria-description` がキーボードの経路でも弱い当たりを伝えると書き、動きの一覧に進捗の札の明滅を載せず、注入した警告の書き先を接頭辞の約束の例外にしていた。
151
+
152
+ ### Breaking
153
+
154
+ - **ids のストリームの行の形とエンドポイントの名前**: チャンク行は `items` を持たず列を運び (上の Changed)、エンドポイントは `GET {apiBasePath}/ids-columns`。名前を行の形と一緒に変えたので、前の版のクライアント (開いたままの古いタブ) の要求は 404 で失敗し、黙って空の一覧を描かない。
155
+ Migration: サーバーとクライアントを同じ版で配備する (古いタブは再読み込みで新しい版になる)。ストリームを自分で読むホストは、行を `isIdsStreamChunkLine` で読み、`ids`・`dates` と `counts`・`sourceTypes` と `sources` から行を組む (`DAILY_REPORT_API_ENDPOINTS.idsStream` で経路を組む)。エッジやプロキシの規則が経路 `{apiBasePath}/ids-stream` を名指していれば (バッファリングを切るなど)、`ids-columns` へ移す。
156
+ - **`draftLabelNames` は必須** (`draftLabelName` は削除): サービスの設定 `draftLabelNames` は空白だけでない名前の 1 つ以上の並び (同じ名前の重複は不可。先頭が正準の名前) で、省略・`[]`・空白だけの名前・重複は生成時に `RangeError` (`[daily-report] draftLabelNames must be one or more non-blank label names; got <値>`)。パッケージの既定の名前 (`下書き`) は無い。
157
+ Migration: `draftLabelName: "X"` は `draftLabelNames: ["X"]` に、`draftLabelNames` を省いていたホストは自分のラベルの名前を渡す。正準のラベル (先頭の名前) が無いと日報の作成が失敗するので、配備の前にそのラベルがあることを確かめる。
158
+ - **クライアントの `draftLabelName` は必須**: `config.draftLabelName` はパッケージの既定 (`"draft"`) を持たず、型は `Partial` の外 (渡し忘れは型の誤り)。描画時は空白だけでない文字列でない値 (欠けた値を含む) が `RangeError` (`[daily-report] config.draftLabelName must be a non-blank string (the server's canonical draft label name, draftLabelNames[0]); got <値>`)。`defaultDailyReportClientConfig` と `DailyReportClientConfigDefaults` はこのキーを持たない。
159
+ Migration: サーバーの `draftLabelNames[0]` を `config.draftLabelName` に渡す。
160
+
5
161
  ## 0.38.0 (2026-10-09)
6
162
 
7
163
  ### Added