@aiquants/daily-report 0.27.0 → 0.29.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,84 @@
2
2
 
3
3
  All notable changes to `@aiquants/daily-report` are documented here.
4
4
 
5
+ ## 0.29.0 (2026-10-06)
6
+
7
+ 0.28.0 の続き: 添付の経路のフレームワークのデータの要求 (`<token>.data`) の拒否、本体をストリームで返す 2 つの答え (SSE と ID 一覧) の HEAD、読み取りポートの失敗の理由 `busy` と `deadline`、窓の終わりに数を書く拒否と 429 の記録、行のサイズの裏付けの無い `too_large` をキャッシュしない縮小画像の生成、ポインターで押したフォーカスをキーボードのフォーカスと見なさない判定、枠を越えない折り返す文言と側面ペインのパネルの最小の幅、DetailList のカードの日付の `<time>`。どれも README の該当の節が今の契約を記す。
8
+
9
+ ### Security
10
+
11
+ - **添付の経路はフレームワークのデータの要求を断る**: React Router のシングルフェッチのデータの要求 (経路が `.data` で終わる `<token>.data`) には、どのオリジンからでも、GET も HEAD も、インライン・ダウンロード・サムネイルのどれも、添付の経路の 404 (`Attachment not found`) で答える。断るのはハンドラー工場の要求の隔離の包みで (経路の方針の `frameworkDataRefusal`、判定は `isFrameworkDataRequest`)、認証・レートの課金・認可・ストレージの読み取りのどれより前に投げ、拒否の記録にも書かない (別オリジンの拒否ではない)。
12
+ 以前は、別オリジンのページのリンクから開く最上位のナビゲーションとして `<token>.data?inline=1` が隔離を通り、原本をストレージから読んだ後、React Router が本体を最後まで読んで文字列へ符号化し、`Set-Cookie` のほかのヘッダー (`private, no-store`・CSP の sandbox・`nosniff`・`Cross-Origin-Resource-Policy`・`X-Frame-Options`・`Content-Disposition`) を捨てて答えた (8 MiB の原本で約 1,250 万文字、ヒープの見積り「同時実行 × 上限」の外で約 37 MiB)。API・action・SSE の経路はデータの要求を今までどおり受ける。本体をストリームで返す 2 つはホストの経路で断る (README の Server wiring の Streaming routes)。
13
+
14
+ ### Fixed
15
+
16
+ - **SSE と ID 一覧のストリームの HEAD は本体の仕事を始めない**: React Router はリソースの経路の loader を HEAD にも走らせ、本体を読みも取り消しもせずに捨てる (アダプターは送り終えた応答の要求のシグナルも中断しない)。`sse.loader` は HEAD に GET と同じ状態とヘッダーで答え、Fan-Out の購読・heartbeat・可視集合の再解決のタイマーを始めない。ID 一覧もカーソルの検証 (400) までを GET と同じに行ってから、GET の 200 と同じヘッダーで本体なしに答え、全量と先頭ページのクエリを走らせない。既に中断された GET も同じ。
17
+ 以前は認証済みの HEAD の要求 1 つが、購読・heartbeat・可視集合の再解決のタイマー (DB への問い合わせ) を、誰も読まないままプロセスが終わるまで残した。
18
+ - **API の経路のエンドポイントとメソッド**: 順は認証 (401)・エンドポイント (知らない名前は 404。`toString` のような継承した名前も)・メソッド (エンドポイントが答えないメソッドは 405 と `Allow`。JSON のエンドポイントは `GET`、ID 一覧は `GET, HEAD`)。以前は GET 以外を `Allow` の無い 405 で断り、エンドポイントの名前を素のオブジェクトから引いた。
19
+ - **行のサイズの裏付けの無い `too_large` をキャッシュしない**: サムネイルの生成は、読み取りポートの `too_large` を、行が上限を超えるサイズを記録しているときだけ内容で決まる結論として記録する。サイズを記録していない行の `too_large` (裏付けが無い。ホストが一時的な拒否を `too_large` と取り違えても同じに見える) には、その要求に 404 (`Cache-Control: no-store`、検証子なし) で答えて記録せず、行は `reason=too_large size=unrecorded` (info。食い違いではないので `size_mismatch` は書かない)。次の表示は読み取りポートへもう一度問う。
20
+ 以前は全利用者に共有され期限の無い 404 をキャッシュしたので、ストレージの接続あたりのストリームの上限で 1 度断られただけの画像が、プロセスの寿命のあいだ誰にもプレビューを出さなかった (1 回の読み取りで 404・404・404)。
21
+ - **ポインターで押したフォーカスはキーボードのフォーカスではない**: キーボードのフォーカスの判定 (`isKeyboardFocused`) は、`:focus-visible` に一致し、要素の文書の最後の入力がポインターの押下でないこと。最後の入力は、その文書のビューがマウントされている間、文書ごとに記録する (`observeInputModality`: 捕獲段の `pointerdown` でポインター、修飾キーだけでない `keydown` でキーボード)。行を見せるスクロールとビューの根要素の `data-daily-report-keyboard-focus` は、どちらもこの判定だけを読む。
22
+ 以前は `:focus-visible` だけで決めたので、ポインターの押下でもブラウザーが `:focus-visible` とする文字入力の要素 (行のコメント欄・編集フォーム) をクリックすると、ビューが行を見せるスクロールをし、根要素に属性を付けて、タップスクロールのサークルを次の押下まで隠した。
23
+ - **折り返す文言は枠を越えない**: `WRAPPING_LABEL_CLASS_NAME` は `text-balance wrap-anywhere [word-break:auto-phrase]`。足した `overflow-wrap: anywhere` は 1 行に収まらない文節や語だけをその中で折り、その折り目を最小の内容の幅にも数えるので、flex の項目の自動の最小の幅が文言を最長の文節の幅に留めない (継承する性質なので、タイルの文言の箱の匿名の flex の項目にも届く)。文節が収まるところの行は変わらない。
24
+ 以前は側面ペインの選択を促す文言 (ja) が flex の項目の自動の最小の幅で最長の文節 「選択してください。」 (126 px) に留まり、ペインの幅 160〜197 px で破線の仮の面を左右 7 px ずつ越えた。
25
+ - **側面ペインのパネルの最小の幅は 200 px**: 選択を促す文言を囲む枠 (パネルの余白 2G 16・カードの面の寄せ 2 × 16・仮の面の寄せ 2 × 12 = 72) と、既定の根での文言の最長の文節 (9 字 × 0.875 rem = 126 px) の和 198 を、4 px の格子へ切り上げた値 (`SIDE_PANE_PANEL_MIN_WIDTH_PX`)。既定の根では ja の文言が文節の境だけで折り返し、はぐれた字の行を残さない。一覧のパネルの最小は 160 px のまま (以前は両方とも 160 px)。
26
+
27
+ ### Changed
28
+
29
+ - **読み取りポートの失敗の理由に `busy` と `deadline`**: `DailyReportAttachmentFailure` は閉じた 1 つのユニオンで、どの理由にも答えるステータスが決まる (`not_found`・`denied`・`invalid_path` は 404、`too_large` は原本 413・縮小画像 404、`unavailable` は 502)。新しい `busy` (ストレージが今は受けない一時的な拒否。接続あたりのストリームの上限ですぐ断られたなど) は 503 と `Retry-After: 5`、`deadline` (ストレージの呼び出しがポート自身の締め切りを過ぎた) は 504 で、原本と縮小画像で同じ。縮小画像の生成はどちらも結論にせず、キャッシュにも実体消失にも書かない (生成の合流を待った要求は同じ失敗を受け、次の要求は読み直す)。ログは warn。
30
+ ユニオンの外の理由は、推測で 502 へ寄せずにポートの契約違反の 500 (`reason=port_contract code=failure_reason`、error) にする (以前は 502)。縮小画像の 503 の文面は原因によらず `Thumbnail temporarily unavailable` (以前は `Too many concurrent thumbnail requests`)、504 は `Thumbnail storage timed out`。原本の 503 は `Attachment storage busy`、504 は `Attachment storage timed out`。README の読み取りポートの例は、自前の締め切りだけが止めた呼び出しを `deadline` で返す。
31
+ - **拒否と 429 の数は窓の終わりに書く**: 要求の隔離の拒否の記録と、添付の 3 つのレートのバケットの 429 の記録は、1 つの窓の仕組み (`createSuppressionWindows`。状態はインスタンスが持つ) を使う。キーの最初の事象がその場で自分の行を書いて窓を開き、窓の中の後の事象は数えるだけで、窓自身のタイマー (unref) が窓の終わりに、数えたものがあれば数の行を書いて窓を閉じる。
32
+ 数の行は `cross_site_rejected_suppressed count=<N> since=<窓の始まりの ISO 8601> routes=api:<n>,action:<n>,sse:<n>,attachment:<n>` と `rate_limit_suppressed count=<N> since=<…> viewer=<id>` (縮小画像は `variant=thumbnail` を足し、再検証のバケットは `revalidation_rate_limit_suppressed`)。429 の窓は閲覧者とバケットごとで、長さは `RATE_LIMIT_LOG_WINDOW_MS` (60 秒、429 の `Retry-After`)。キーごとに窓 1 つにつき 2 行まで。窓を決めるのはタイマーだけで、壁時計は `since=` を刻むだけ (時計が戻ったときの規則は無くなった)。
33
+ 以前は数を次の事象まで持ち越したので、続きが止まった窓の数は書かれず、ずっと後の無関係な拒否がその時刻で窓の始まりも経路も無く書いた。429 は利用者の次の許可を待ったので、拒否が止まったかバケットが追い出された数は失われ、要求を送り続けるクライアントには補充のたびに 2 行 (上限 60/分で 1 分に約 120 行) を書かせた。
34
+ - **DetailList のカードの日付はどれも `<time datetime>`**: メタデータの列の作成日時・更新日時・営業日も、見出しの札 (営業日・更新日時) と同じ部品 (`DateText`) で描く。無い日付の `-` は文字のまま。以前は同じ行の同じ日付を、見出しでは `<time>`、メタデータの列では平らな文字で公開した。
35
+ - **配置の格子の単位の契約の文言**: `DAILY_REPORT_LAYOUT_LATTICE_PX` の docstring と README の View height は、ホストができることだけを書く。ホストはビューの周りのバー (ヘッダーとフッター) をこの単位の倍数にそろえ、ビューの上の縁を格子に乗せる。ウィンドウの高さは決められないので、単位で割った余りはビューの中に残る。
36
+ 端へ揃えた行は、ビューの端と行の箱が整数の装置の画素にあればちょうど G (整数の画素密度ではビューの端が整数の CSS px にあるとき、1.25・1.5・1.75 ではビューの端が格子にあるとき)、それ以外 (0.9・1.1・1.33 などのブラウザーの拡大率を含む) では端を守る寄せで G 以上 G + 1 装置画素未満。クラスを字面で書くホスト (Tailwind) は、字面をこの値へ単体テストで結び付ける。
37
+ - **開発者向け**: ID 一覧の行の分割 (`createNdjsonLineSplitter`) は、テストのためだけの数え (`scannedCharacters`) をクライアントのバンドルに持たない。線形時間の spec は `String.prototype.indexOf` と `split` が調べた文字を数え、1 MiB を 1 KiB ずつ送って長さの 2 倍以下を求める (持ち越して全体を分け直す旧い形では約 5 億 3,800 万文字)。
38
+ `src/docs-examples.spec.ts` は、README と `docs/specs` の文のうちピアの下限を名乗る文が、`peer-floors.json` の下限のほかの版を書くと落ちる (公開の検査は manifest しか読まない)。ID 一覧のエンドポイントの名前はクライアントとサーバーが `DAILY_REPORT_IDS_STREAM_ENDPOINT` を共有する。
39
+
40
+ ### Added
41
+
42
+ - server の値 `DAILY_REPORT_IDS_STREAM_ENDPOINT` (`"ids-stream"`): ID 一覧の NDJSON を流す API のエンドポイントの名前。クライアントの URL とサーバーのエンドポイントの表がこれを使い、ホストは字面を書き写さずにこのエンドポイントだけを見分ける (React Router のデータの要求をこのエンドポイントについて断るなど。README の Server wiring の Streaming routes)。
43
+ - server の型 `DailyReportAttachmentFailure` の `busy` と `deadline` (上の Changed)。
44
+
45
+ ### Breaking
46
+
47
+ - 読み取りポートが `DailyReportAttachmentFailure` の外の理由を返すと、どちらの配信もポートの契約違反の 500 で答える (以前は 502)。`DailyReportAttachmentFailure` を
48
+ 網羅して分岐するホストのコード (理由ごとの表など) は、型検査が `busy` と `deadline` の欠けを示す。
49
+ Migration: ポートは 7 つの理由だけを返す (ストレージの一時的な拒否は `busy`、自前の締め切りを過ぎた呼び出しは `deadline`)。理由で分岐するホストのコードに 2 つを足す。
50
+
51
+ ## 0.28.0 (2026-10-05)
52
+
53
+ 0.27.0 の続き: 要求の隔離の許可の一覧化 (別オリジンから受けるのは添付の原本を開く最上位の文書のナビゲーションだけ)、Fetch Metadata の無い要求の `Origin` による判定、拒否の上限付きの記録、添付のどの応答にも枠の方針、配置の格子の単位と E2E のテスト用ハンドルの型の公開、ID 一覧のストリームの行の線形な分割、折り返す文言の改行の規則。どれも README の該当の節が今の契約を記す。
54
+
55
+ ### Security
56
+
57
+ - **別オリジンから受けるのは、添付の原本を開く最上位の文書のナビゲーションだけ**: 要求の隔離は許可の一覧になった。経路ごとの方針 (`RequestIsolationPolicy`) が受けると宣言したナビゲーション (`GET`・`Sec-Fetch-Mode: navigate`・`Sec-Fetch-Dest` がちょうど `document`) だけを通し、それを宣言するのは添付の経路の原本 (インラインとダウンロード。ローダーと同じ厳格なクエリの解釈で決める) だけ。API・SSE・サムネイルは別オリジンからのナビゲーションを受けない。
58
+ 枠 (`iframe`・`frame`)・`fencedframe`・行き先の無いナビゲーション・`HEAD` のナビゲーション (ブラウザーは `HEAD` でナビゲーションしない)・これから仕様に増える行き先は、並べなくても作りから拒む。隔離はハンドラー工場が返す前に 1 つの包みで 4 つの経路へ当て (経路の中の手で置いた判定は無くなった)、添付の経路では形の崩れたクエリも 405 になるメソッドも、別オリジンからなら認証の前に 403 になる (以前は認証の後の 400 か 405)。
59
+ 以前は `object` と `embed` 以外の行き先の GET か HEAD のナビゲーションをどの経路でも通したので、同じサイトの兄弟のサブドメインのページの `<iframe>` が、閲覧者のセッションで、サムネイルのバケットを空にし (閲覧者自身のタイルが 429 になる)、見える添付と見えない添付の応答の時間の差を測り、ID 一覧の全件のクエリを走らせ、SSE の購読を開けた。応答の側の `X-Frame-Options` は枠に描くことを止めるだけで、要求とその仕事は止めない。
60
+ - **Fetch Metadata の無い安全でないメソッドは `Origin` で決める**: ブラウザーは潜在的に信頼できないオリジン (`localhost` 以外の素の HTTP) への要求に `Sec-Fetch-*` を付けないので、ヘッダーの無い要求をすべて通すと、素の HTTP のホストでは兄弟のページのフォームの POST が状態を変えた。`Sec-Fetch-Site` の無い安全でないメソッドは、`Origin` が無ければ通し (ブラウザーのページではない)、要求の宛先のホスト (`new URL(request.url).host`) と同じなら通し、`null`・URL として読めない値・別のホスト・別のポートなら 403 にする (Go の `net/http` の `CrossOriginProtection` と同じ規則)。安全なメソッドは今までどおり通す。隔離を完全にするには、潜在的に信頼できるオリジン (HTTPS) で配る。
61
+ - **添付のどの応答も `X-Frame-Options: SAMEORIGIN`**: 枠への埋め込みの方針は、`nosniff` と `Cross-Origin-Resource-Policy: same-origin` と同じ 1 つの組 (`ATTACHMENT_RESPONSE_SECURITY_HEADERS`) から、失敗の応答 (隔離の 403 を含むどのステータスも。GET と HEAD)・304・2 つの配信の 200 へ付く。以前は 200 だけが持ったので、別オリジンのページは添付の URL を枠に読み込み、塞がれた枠 (見える添付の 200) と描かれた枠 (見えない添付の 404) を見分けられた。
62
+
63
+ ### Added
64
+
65
+ - client の値 `DAILY_REPORT_LAYOUT_LATTICE_PX`: 配置の格子の単位 (4 CSS px)。パッケージ自身の格子の単位そのもので、ホストがビューの周りのバー (ヘッダーとフッター) の大きさをこの倍数にそろえ、ビューの上の縁を格子に乗せる契約 (ウィンドウの高さはホストが決められないので、単位で割った余りはビューの中に残る。README の View height の G-symmetric frame)。公開する配置の値はこれだけで、行の大きさは公開しない。
66
+ - client の型 `DailyReportViewTestHandle`・`DailyReportViewTestReadHandle`・`DailyReportRevealOptions`: E2E のテスト用ハンドル (`__virtualScroll`) の契約とその読み取りの部分、`revealIndex` の揃えの型 (VirtualScroll の `scrollToIndex` の第 2 引数。virtualscroll 3.11.0 の見せる操作 `{ align: "nearest" }` も含む)。型だけの公開なので、バンドルは増えない。ホストのテストは形を書き写さずに読み込む。
67
+
68
+ ### Changed
69
+
70
+ - **拒否は上限付きの記録に残る**: 隔離の拒否は、ハンドラー工場ごとの記録 (`createCrossSiteRejectionRecorder`) が窓 1 つ (`CROSS_SITE_REJECTION_LOG_WINDOW_MS`、60 秒) につき warn の行を 2 行まで書く: 窓の最初の拒否の `cross_site_rejected route=<api|action|sse|attachment> site=<値> mode=<値> dest=<値> method=<メソッド>` と、窓の後の最初の拒否が書く前の窓の数の `cross_site_rejected_suppressed count=<数>` (数えた拒否があるときだけ)。仕様の形 (小文字と `-` だけの 32 文字まで) でない値は、先頭の 32 文字の JSON の文字列で書く。書き先は設定の `logger`、無ければ `[DailyReportIsolation]` を頭に付けた warn の水準の console。受けた要求は窓を開きも閉じもしない。以前は拒否を一切記録しなかったので、同じサイトのページからの探りや CSRF の試みが運用から見えなかった。
71
+ - **ID 一覧のストリームの行の分割は線形**: クライアントは届いたチャンクを復号するたびに、新しく復号した文字だけを改行まで調べ、行が完結したときに欠片を 1 度だけつなぐ (`createNdjsonLineSplitter`)。以前は持ち越した欠け行と新しいチャンクを連結して全体を分け直したので、4,000 件で約 300 KB の行が届き終わるまで、チャンクのたびに同じ前半を調べ直した (同じ形の合成の 16 MB のストリームを 4 KiB のチャンクで分けるベンチで、調べる文字は 1,600 万でなく 6 億、時間は 11.8 ms でなく 287.6 ms)。
72
+ ストリームの終わりの改行の無い行も 1 行として読む (以前は捨てた)。終わりで切れた行は JSON として読めないので、警告 1 件で読み飛ばし、今までどおりカーソルから読み直す。
73
+ - **折り返す文言の改行**: 側面ペインの選択を促す表示と、添付のタイルの表示できない旨の文言は、1 つの規則 (`WRAPPING_LABEL_CLASS_NAME`: `text-wrap: balance` と `word-break: auto-phrase`) で折り返す。行の長さを揃えるので最後の行に 1-2 文字だけが残らず、ホストの文書の `lang` が `ja` なら日本語を文節の境で折り返す (171 px の列で 「プレビューを表示できませ / ん」 が 「プレビューを / 表示できません」 になる)。`auto-phrase` を持たない Firefox と Safari は宣言を捨てて今までどおりに折り返し、`balance` を持たない Safari 17.4 は行の長さを揃えない (README の Browser floor)。
74
+ - **開発者向け**: `pnpm run verify` の単体テストは、リポジトリーの公開の漏洩検査を配布する Markdown (`package.json` の `files` が配る `README.md`・`CHANGELOG.md`) へ検査自身の規則で当てる (`src/shipped-markdown-leaks.spec.ts`)。内部の名前は公開の段ではなく検証の段で落ちる。
75
+ `StreamEntry` と描画ポートの契約の定数 (`DAILY_REPORT_ATTACHMENT_THUMBNAIL_OUTPUT_MEDIA_TYPES`・`DAILY_REPORT_ATTACHMENT_THUMBNAIL_MAX_OUTPUT_BYTES`) はポートのモジュール (`src/server/ports.ts`) に置き、ポートは他のサーバーのモジュールを読み込まない (型の循環を断った。公開の名前と入口は変わらない)。
76
+ DetailList の行の spec は、本物の VirtualScroll の上で、差の和が 0 の測り直しの組 (1 回の ResizeObserver の配達で行 2 つが +40 と −40) の後も行の枠がそれぞれの実測の高さに置き直され、間の行だけが動き、錨が変わらないことを確かめる。直すのは `@aiquants/virtualscroll` の次の版 (CHANGELOG の 3.11.0) で、spec はワークスペースの virtualscroll のソースで走る。ピアの下限はその版の公開と一緒に上げる。
77
+
78
+ ### Breaking
79
+
80
+ - ピアの `@aiquants/virtualscroll` の下限を 3.11.0 へ上げた (`peer-floors.json`)。テスト用のハンドルの `DailyReportRevealOptions` は 3.11.0 の `scrollToIndex` の引数 (`align: "nearest"` を含む) で、DetailList の行が差の和が 0 の測り直しの組の後も木の上端と高さに戻るのは 3.11.0 の行の memo が木の版に従うため。
81
+ Migration: `@aiquants/virtualscroll` を 3.11.0 以降へ上げる。
82
+
5
83
  ## 0.27.0 (2026-10-05)
6
84
 
7
85
  0.26.0 の続き: Fetch Metadata による要求の隔離、ポインター専用のスクロールバー (名前の無い `scrollbar` と `slider` を出さない)、添付の格子の整数 px の列、要求が全員去った描画の結果の保持、SSE リーダーのポート、E2E のテスト用ハンドルの読み取り専用化。どれも README の該当の節が今の契約を記す。