@aiquants/daily-report 0.31.0 → 0.32.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,82 @@
2
2
 
3
3
  All notable changes to `@aiquants/daily-report` are documented here.
4
4
 
5
+ ## 0.32.0 (2026-10-07)
6
+
7
+ 0.31.0 の続き: 添付の設定を 1 つの任意の入れ子のブロック (`attachments`) にまとめること、action の本文の上限と、送られた値 (件名・本文・コメント・真偽値) と `forceRefresh` を 1 つの厳格な解釈で読むこと、SSE のイベント 1 件の上限、ids ストリームのスナップショットトークンを取得ごとに 1 回だけ作ることと publish が一覧を写さないこと、アクションのプロバイダーの操作の値を描画をまたいで同じに保つこと、削除の印の付いた行の外し漏れ、件数の補間が動きを減らす設定に従うこと。どれも README の該当の節が今の契約を記す。
8
+
9
+ ### Fixed
10
+
11
+ - **action の本文は 1 MiB まで**: action はフォームの本文を `ACTION_FORM_MAX_BYTES` (1,048,576 バイト) までしか読まない。宣言した長さ (`Content-Length`) がそれを超える本文は読まずに、宣言の無い本文 (チャンク転送) と長さを偽る本文は読んだバイトを数えて上限を超えたところで読むのをやめて、413 (`{"error":"Form too large"}`) で答える。どのサービスも呼ばず、error の行も書かない。
12
+ 記録は 60 秒の窓ごとに warn の 2 行まで (既定のロガーの接頭辞は `[DailyReportAction]`): 窓の最初の拒否の `413 action reason=form_too_large measured=<declared|counted> limit=1048576` と、窓の終わりに数えた拒否があれば `form_too_large_suppressed count=<N> since=<窓の始まりの ISO 8601> measured=declared:<n>,counted:<n>`。
13
+ SSE のストリームへ書くイベント 1 件の JSON は `SSE_EVENT_MAX_BYTES` (6,356,992 バイト: フォームの上限を JSON の最大の伸びの 6 倍に伸ばし、包みの 64 KiB を足した大きさ) までで、それより大きいイベントは書かずに warn の `[SSE] Event too large (<操作>): <バイト数> bytes > 6356992; not published` を残す。
14
+ 受け付けた action の文字がこの上限を超えるイベントを作ることは無く、超えうるのは日報の詳細を丸ごと運ぶイベント (作成・更新・公開) のうち、積み上がった詳細 (本文とコメントの全件) が約 6 MiB を超えた日報だけで、キャッシュの無効化は書く前に済むので、閲覧者は次の読み込みで最新を読む。
15
+ 以前は本文に上限が無く (前段のリバースプロキシの上限まで)、8 MiB のコメントも 200 で格納され、永続の SSE のストリームへ書かれて、接続中のすべての閲覧者と再接続の追いつきへ届いた。
16
+ - **action の送られた値も 1 つの厳格な解釈で読む**: フォームを読むのは `src/server/action-form.ts` だけで、操作の実行はフォームではなく解釈し終えた指示を受け取る。
17
+ - 更新 (`update`) は送られた欄だけを変える: 送られない欄は格納した値のまま、空文字はその欄を消して NULL で格納する。件名は注入した件名の列が宣言した長さ (パッケージのスキーマでは `nvarchar(200)`: UTF-16 の符号単位で 200) までで、超えれば 400 (`Invalid title`)。
18
+ - スターと既読の切り替えの `isStarred` / `isRead` は `true` か `false` だけで、ほかの値 (`TRUE`・`1`・`yes`・空文字) は 400 (`Invalid isStarred` / `Invalid isRead`)、無ければ 400 (`isStarred required` / `isRead required`)。コメントの追加の `content` は空でない文字 (無いか空なら `Content required`)。
19
+ - フォームのどこかにファイルの部分があれば 400 (`Invalid form`)。フォームは作りからテキストだけになる。
20
+ - API の `business-date`・`report`・`ids-stream` の `forceRefresh` は、無ければ偽で、`true` と `false` だけを受け、ほかの値は 400 (`{"error":{"message":"Invalid forceRefresh"}}`。`ids-stream` は HEAD の短絡より前に答える)。
21
+ - action のどの 400 も、内部ユーザーの解決と可視集合の解決より前に答える (封鎖された `clearCache` の 400 も)。
22
+ 以前は件名・本文・コメントを `form.get()` のまま読んだので、送られない件名は NULL で格納され、ファイルの部分は文字列の型のままサービスへ届いた。`isStarred` / `isRead` と `forceRefresh` は `true` 以外のどの値も偽と読み (`TRUE`・`1`・`yes`・空文字がスターを外し、未読に戻した)、201 文字の件名はドライバーの切り詰めの失敗で 500 になり、`isStarred required` と `Content required` は利用者と可視集合の問い合わせの後に答えた。
23
+ - **削除の印の付いた行は、ストリームの正味の変化が値の変化でも見せない**: 行の導き (`deriveReportRows`) は、見せている行に削除の印があれば、ストリームの変化がその日報の値の変化 (同じ合体窓の削除と中継で正味が値の変化になったもの、印が付いた後に導いた値の変化) でも行を外す。
24
+ 以前は印を見ると値の変化を当てはめずに戻ったので、一覧全体から導けば隠れる行が見えたまま残り、印の後の値の変化では前の値の行が残って、後から届いた削除がその行を見つけられず、行は二度と外れなかった。
25
+ - **件数の補間は動きを減らす設定に従う**: ヘッダーの総件数と ids ストリームの走査中の件数は、件数を描く要素のウィンドウが `prefers-reduced-motion: reduce` のとき、値が変わったその描画で目標の値を描き、アニメーションのフレームを 1 つも求めない (`useAnimatedNumber`)。以前は設定を読まずに 250 ms かけて補間した (README の「動きを減らす設定では、スクリプトの動きも含めて何も動かない」に反していた)。
26
+
27
+ ### Changed
28
+
29
+ - **アクションのプロバイダーの操作は 1 つの同じ値**: `DailyReportActionProvider` は操作 (関数・楽観的な状態の台帳・表示中ユーザー) を 1 つの値として 1 回だけ作り、その同一性は表示中ユーザーが替わるまで変わらない。関数はどれも最後に確定した描画の実装を呼び (実装はレイアウトの副作用で入れ替え、描画中に ref を書かない)、状態 (行・台帳の版・SSE の有効と接続の状態) は別のコンテキストで配る。
30
+ パッケージの行・List のカード・DetailList の行と本文カード・側面ペインの中身・編集のフォームは操作だけを読むので、ストリームの publish・SSE の知らせ・接続の状態の変化で描き直さない。台帳の版の変化で描き直すのは日報のコメントの部品 (`ReportComments`) だけ。
31
+ 公開のフック `useDailyReportActionContext` は今までどおり操作と状態を 1 つの値で返し、状態が変わらない描画では同じ値を返す (状態が変わるたびに呼び出し側は描き直す)。
32
+ 以前はプロバイダーが描画のたびに新しいオブジェクトを配り、操作の関数もほとんどが描画ごとの閉包だったので、ストリームの publish のたびに、描かれているすべての行・カード・側面ペインの中身・編集のフォームが memo を越えて描き直された。
33
+ - **ids ストリームのスナップショットトークンは取得ごとに 1 回**: サービスは ID 一覧の全量を取得したとき (キャッシュのフェッチャーの中、整列の治癒の後) に 1 回だけトークンを作り、一覧と同じ包みに持たせる (`getDailyReportIdsByExternalId` の戻り値 `{ items, streamAnchor, snapshot }`。行の無い包みは空の一覧のトークン)。ids ストリームの配信はそれを読む。
34
+ 以前は要求ごとに全件をたどって作り直したので、228,222 件では要求ごとにイベントループを p50 7 ms (静かなホスト)、負荷の下で 11〜26 ms 止めた。
35
+ - **ids ストリームの publish は一覧を写さない**: クライアントの publish が配るスナップショットは一覧そのものを持たず、publish の仕事は変化の大きさに比例する。一覧全体が要る購読者は読むときに作る (`DailyReportIdsStreamClient.readItems()`: 最後の publish の時点の一覧で、合体窓の中のまだ publish していない変化を戻す)。
36
+ プロバイダーは一覧全体から導き直すときだけ読み、ページは中身を見せるかを `loadedCount > 0` で決める。以前は publish ごとに一覧全体を写し (`Array.from`)、228,222 件では publish ごとに p50 1.29 ms、最初の訪問の 59 回の publish で約 56 ms を、publish の描画と同じタスクで使った。
37
+ - **開発者向け**: 添付の設定の解決は `src/server/attachment-delivery/settings.ts` の `resolveAttachmentDelivery` 1 つ (ブロックの検証と既定値の解決) と、平たいキーの拒否 `rejectFlatAttachmentKeys` (`FLAT_ATTACHMENT_KEYS` の表) にある。添付の経路は `service.attachmentDelivery` の解決済みの値だけを読み、ブロックが無ければ配信の状態を作らず、サムネイルが無ければサムネイルのバケット・ゲート・キャッシュを作らない。
38
+ action のフォームの読み取りと解釈は `src/server/action-form.ts` (`readActionCommand`) にあり、ハンドラーの操作の実行 (`runActionCommand`) はフォームを受け取らない。2 つの上限は `src/server/payload-limits.ts` (`ACTION_FORM_MAX_BYTES`・`SSE_EVENT_MAX_BYTES`)。`src/server/wire-params.ts` の `formFieldText` は無く、`parseWireBoolean`・`parseForceRefreshParam`・`readFormTextFields` が加わった。件名と本文の空文字を NULL で格納する規則はサービスの `storedText` 1 か所。
39
+ プロバイダーの描画中の行の導きは `useDerivedReportRows` (`src/client/contexts/daily-report-rows-derivation.ts`) 1 つで、本物のクライアントの publish ごとに触る行の数を試験が数える。行の導きは、乱数の操作列の性質の試験 (8 つの種 × 2 通り × 400 の操作列) で一覧全体から導いた行と比べる。両ビューの描画の回数の試験は本物のプロバイダーを描く (アクションのフックを代役にしない)。
40
+ アニメーションのフレームを求めてよいのは理由を添えた許可の一覧のファイルだけ (`src/client/ui/motion-policy.spec.ts`: 件数の補間は動きを減らす設定を読むこと、キーの長押しのまとめと添付の格子の列幅の受け渡しは動きではない用途)。ビューの Tab の順を全部たどる重い jsdom の試験の時間の上限は 1 つの定数 `HEAVY_JSDOM_TEST_TIMEOUT_MS` (60 秒。`src/client/test-helpers/heavy-jsdom-timeout.ts`) で、公開の verify が負荷の高いホストで Vitest の既定の 5 秒を越えて落ちない。
41
+ 配置の格子の画素密度の一覧 (`LAYOUT_LATTICE_DEVICE_PIXEL_RATIOS`) は試験用の部品 (`src/client/test-helpers/layout-lattice-ratios.ts`) へ移り、`dist/client.mjs` に入らない。使われていない `upsertInIdsStreamOrder` は無い。クライアントは ids ストリームの次のカーソルの営業日を、サーバーと同じ `isCanonicalBusinessDateKey` で確かめる (暦に無い日を受けない)。ページはホストの `onSessionExpired` を描画中ではなくレイアウトの副作用で写す。
42
+
43
+ ### Added
44
+
45
+ - server の型 `DailyReportAttachmentsConfig` と `DailyReportAttachmentThumbnailsConfig` (添付の設定のブロックとそのサムネイル。下の Breaking)。型だけの公開なので、バンドルは増えない。
46
+ - サービスの `titleMaxLength` (件名の上限: 注入した hub テーブルの件名の列が宣言した長さ) と、`getDailyReportIdsByExternalId` の戻り値の `snapshot` (上の Changed)。
47
+ - client の `DailyReportIdsStreamClient` の `readItems()` (上の Changed)。
48
+
49
+ ### Breaking
50
+
51
+ - 添付の設定は 1 つの任意のブロック `attachments` (`DailyReportAttachmentsConfig`) にまとまった: サービスの設定 (と、それを受け継ぐ `createDailyReportServer` の設定) の `attachments` は、ID コーデック (`idCodec`) と読み取りポート (`read`) を必ず持ち、大きさの上限と原本の経路の調整値、任意の `thumbnails` (`DailyReportAttachmentThumbnailsConfig`: 描画ポート `renderer` と調整値) を持つ。ブロックを省けば添付機能の無い配備。
52
+ 効かない組み合わせ (読み取りの無いコーデック、読み取りの無い描画、ポートの無い調整値) は型で書けず、型の外 (JS のホスト・読み込んだ設定) から来た誤りは、入れ子のパスを名乗る `TypeError` (欠けたポート: `[daily-report] attachments.read must be injected when attachments is given`) か `RangeError` (不正な値: `[daily-report] attachments.thumbnails.cacheBytes must be an integer >= 0; got -1`) で生成を止める。
53
+ ブロックの外の平たい 9 つのキーは受けない: どれか 1 つでも持つ設定は (値が `undefined` でも)、`createDailyReportService`・`createDailyReportHandlers`・`createDailyReportServer` の生成を、値を置くパスとキーを名乗る `TypeError` で止める (`[daily-report] attachments.concurrency must be given in place of the flat key attachmentConcurrency`)。ポートを渡したのに添付が一覧に出ない・調整値が効かない、といった黙った縮退にはならない。ハンドラーの設定 (`DailyReportHandlersConfig`) は添付のキーを持たない。
54
+ サービスの `attachmentDelivery` (`DailyReportAttachmentDelivery`) は、ブロックの既定値を解決して凍結した `{ idCodec, read, maxBytes, rateLimitPerMinute, concurrency, thumbnails }` (`thumbnails` はサムネイルの無い配備で `null`、ほかは `{ renderer, rateLimitPerMinute, concurrency, cacheBytes }`) で、ブロックの無い配備で `null` (以前は `{ idCodec, readAttachment, thumbnailRenderer, maxBytes }` で、注入しないポートが `undefined`)。
55
+ Migration: 平たいキーをブロックの中のパスへ移す。
56
+
57
+ | 平たいキー (0.31.0 まで) | 以前の持ち主 | ブロックの中のパス |
58
+ | --- | --- | --- |
59
+ | `attachmentIdCodec` | サービス | `attachments.idCodec` |
60
+ | `readAttachment` | サービス | `attachments.read` |
61
+ | `attachmentMaxBytes` | サービス | `attachments.maxBytes` |
62
+ | `attachmentRateLimitPerMinute` | ハンドラー (ファサードが転送) | `attachments.rateLimitPerMinute` |
63
+ | `attachmentConcurrency` | ハンドラー (ファサードが転送) | `attachments.concurrency` |
64
+ | `attachmentThumbnailRenderer` | サービス | `attachments.thumbnails.renderer` |
65
+ | `attachmentThumbnailRateLimitPerMinute` | ハンドラー (ファサードが転送) | `attachments.thumbnails.rateLimitPerMinute` |
66
+ | `attachmentThumbnailConcurrency` | ハンドラー (ファサードが転送) | `attachments.thumbnails.concurrency` |
67
+ | `attachmentThumbnailCacheBytes` | ハンドラー (ファサードが転送) | `attachments.thumbnails.cacheBytes` |
68
+
69
+ 原本だけの配備は `attachments: { idCodec, read }` (上限と原本の調整値は任意)、サムネイルも出す配備はそれに `thumbnails: { renderer }` (調整値は任意) を足す。既定値と下限は変わらない。`createDailyReportHandlers` へ手書きのサービスの代役を渡すホストの試験は、代役の `attachmentDelivery` を新しい形か `null` にする。
70
+ - action の更新の契約: 送らなかった欄は格納した値のまま、空文字は欄を消す (NULL で格納。上の Fixed)。以前は送らなかった欄も NULL で格納した。パッケージのクライアントはいつも件名と本文の両方を送り、client の `updateReport(reportHubId, data, businessDate)` (`useDailyReportActionContext` の操作) の `data` は `{ title: string; content: string }` で両方が必須 (以前は `{ title?: string; content?: string }` で、空にした欄を送らなかった)。
71
+ Migration: 欄を消すときは空文字を送る (欄を省くと格納した値のまま)。`updateReport` には件名と本文の両方を、打たれたとおりに渡す。
72
+ - action と API の値の解釈が厳しくなった (上の Fixed): `isStarred` / `isRead` と `forceRefresh` は `true` と `false` だけ、件名は件名の列の長さまで、フォームのファイルの部分は 400、本文は 1 MiB まで (413)。
73
+ Migration: 真偽値は `true` / `false` で送る。`forceRefresh` は省くか `true` / `false` で送る。action のフォームにファイルを入れない。
74
+ - client の `useDailyReportIdsStream` の状態 (`DailyReportIdsStreamState`) は `items` を持たない (上の Changed)。
75
+ Migration: 件数は `loadedCount`、届いた行があるかは `loadedCount > 0` で読む。見せている行はプロバイダーの `items` (`useDailyReportActionContext().items`) で読み、自分で作った `DailyReportIdsStreamClient` の一覧は `readItems()` で読む。
76
+ - server のサービスは、注入した hub テーブルの件名の列が長さを宣言していなければ、生成時に `RangeError` (`[daily-report] tables.hub.title must be a character column with a declared length; got <値>`) を投げる。`createDailyReportHandlers` は、サービスの `titleMaxLength` が 1 以上の整数でなければ生成時に `RangeError` (`[daily-report] service.titleMaxLength must be an integer >= 1; got <値>`) を投げる。ids ストリームの配信はサービスの `getDailyReportIdsByExternalId` が返す `snapshot` を読む。
77
+ Migration: 件名の列は長さを宣言する (`defineDailyReportSchema` の列は宣言済み)。`createDailyReportHandlers` へ渡す手書きのサービスの代役は `titleMaxLength` を持ち、`getDailyReportIdsByExternalId` から `snapshot` を返す。
78
+ - ピアの `@aiquants/virtualscroll` の下限を 3.11.5 へ上げた (`peer-floors.json`)。両ビューが 3.11.5 の変更に頼る所は無いが、この版の試験はすべて 3.11.5 の上で通したので、公開する範囲 (`^3.11.5`) と文書の下限をそれに合わせた。
79
+ Migration: `@aiquants/virtualscroll` を 3.11.5 以降へ上げる。
80
+
5
81
  ## 0.31.0 (2026-10-06)
6
82
 
7
83
  0.30.0 の続き: 編集中の入力が描画の窓の外や選択の移動を越えて残ること、要求の値 (営業日・ID・操作の時刻・フォーム) の厳格な解釈と作成した日報の営業日を UTC の 0 時で格納すること、原本の `Content-Length` を渡す本文から決めること、一覧の行を ids ストリームの変化だけで導くことと SSE の知らせの合体、合体窓を単調な時計で測ること、行の集合の大きさ (`aria-setsize`) と見出しの総件数を申告した総数で決めること、ホバーの浮き上がりの定義域を 1 から 3 までの 1/4 の倍数のすべてへ広げること、要求の隔離の結線を型で決めること。どれも README の該当の節が今の契約を記す。