Skip to content

feat: carry the manifest thumbnail through to the effect catalog (#12, data half) - #66

Merged
GeneralD merged 12 commits into
mainfrom
feat/effect-thumbnail-manifest
Oct 30, 2026
Merged

GeneralD merged 12 commits into
mainfrom
feat/effect-thumbnail-manifest

Conversation

@GeneralD

Copy link
Copy Markdown
Owner

agent type scope part of

概要

issue #12 (エフェクトピッカーのサムネイル) のデータ側。.lens の manifest.json に任意の thumbnail を足し、その画像のバイト列を DataSource が FileSystem port 経由で読んで、EffectCatalogEntry.thumbnail までカタログに載せる。View 側 (Presenter / Geometry / View / Router) は別 PR で進んでおり、セルへの配線・View での画像デコード・README Roadmap・Closes #12 は両方がマージされた後の統合 PR で行う。この PR では #12 を閉じない。Sources/Presenters・Sources/Views・Sources/AppRouter には触れていない。

契約

取り決めた契約はそのまま守っている:

  • EffectManifest.thumbnail: String?: パッケージ内の相対パス。書かれたとおりに保持する (trim しない)。キーが無い・null なら nil
  • EffectThumbnail (Sources/Entity/、Sendable, Hashable): data: Data
  • EffectCatalogEntry.thumbnail: EffectThumbnail?: init の最後の引数で、既定は nil。同梱エフェクト (Chromatic Grain) と broken なパッケージは nil
  • バイト列を読むのは DataSource だけで、View はファイルに触れない。既存の EffectCatalogEntry のフィールドの意味は変えていない

契約に足したもの (どれも既存の意味は変えない):

  • EffectThumbnail.format: EffectThumbnailFormat (png / jpeg): 拡張子ではなく先頭のシグネチャで決まる。View がデコーダを選ぶのに使える
  • EffectPackage.thumbnail: EffectThumbnail?: DataSource から Repository へ渡すため
  • FileSystemGateway.readData(_:upTo:): 上限つきでバイト列を読む。live は FileHandle.read(upToCount:) で、上限を超えるファイルを丸ごと読むことはない

不正な値の扱い

方針: 使えないサムネイルで失うのは絵だけ。 パッケージは .ready のまま thumbnail が nil になり、理由は Swift の文字列表記 (String(reflecting:)) でパスを引用してログに出す。黙って捨てることはない。詳しい理由は docs/ARCHITECTURE.md の Key Design Decisions、Thumbnails: a picture that can only be missing に書いた。

入力 判定する場所 結果 ログ
空文字列 パスの文字列 (読む前) 絵なし developer log is empty
絶対パス (/…) 同上 絵なし developer log is an absolute path, …
NUL を含む 同上 絵なし developer log holds a NUL character
.. の成分 (中に戻ってくるものも含む。パスの解決はしない) 同上 (/ のバイトで分割) 絵なし developer log has a \..` component, …`
拡張子が .png / .jpg / .jpeg (大文字小文字は問わない) 以外 同上 絵なし developer log is not a .png, .jpg or .jpeg file
ファイルが無い (fileNoSuchFile / fileReadNoSuchFile) 読み込み 絵なし (作られれば次の一覧で拾う) developer log is missing
ファイルはあるが読めない (権限など) 読み込み 絵なし error log could not be read: …
1 MiB を超える (1 MiB + 1 バイトまで読んで判定) バイト列 絵なし developer log is larger than 1048576 bytes
PNG / JPEG のシグネチャで始まらない バイト列 絵なし developer log is not a PNG or JPEG image
thumbnail が JSON の文字列でない (数値・オブジェクト) manifest のデコード パッケージが broken (manifestInvalid、author と同じ扱い) 既存の broken と同じ
  • 上限は 1 MiB。ピッカーのセルは数百 pt 幅で、512 × 512 の PNG でも十分収まる。一方、一覧のたびに全パッケージのサムネイルを読み直し、publish される state がそれを全部持つので、上限で一覧の I/O と state のメモリの両方を抑える
  • シンボリックリンクは manifest.json / effect.metal と同じく辿る。パスの検査で、パスがパッケージ内の名前に留まることは保証する。バイト列はプロセスの外に出ず、画像のシグネチャも必要
  • watch: 読んだ (無ければ読むはずだった) サムネイルのファイルがあればそれ自体を watch し、パッケージのディレクトリからファイルまでの途中にあるディレクトリも watch する。上書き保存も atomic save も、サブディレクトリの作成も拾える。文字列の検査で弾いたパスは何も足さない (変えられるのは manifest の編集だけ)
  • キャッシュはしない。shader と同じく一覧のたびに読み直し、package(_:) も読み直すので、2 つの答えが食い違わない。サムネイルだけが変わった一覧は、新しい state を publish するが、何も prepare し直さない

変更点

  • 508d0f0 feat(file-system): FileSystemGateway.readData(_:upTo:)、live、fake (InMemoryFileSystemGateway がバイト列を持つようになった: write(_:bytes:)、dataReads、UTF-8 でないテキストは live と同じく fileReadCorruptFile、存在しないファイルへの readData は FileHandle と同じ fileNoSuchFile) と、その smoke / contract テスト
  • 12a9864 feat(effect-package): Entity (EffectThumbnail、EffectManifest.thumbnail、EffectPackage.thumbnail)、純粋な規則の EffectThumbnailReading (パス分割・拡張子・サイズ・シグネチャ)、DataSource での読み込み・ログ・watch、manifest のデコード
  • 0da3f5f feat(catalog): EffectCatalogEntry.thumbnail と Repository での受け渡し、UseCase とループバック統合テスト
  • 1ef6fb9 feat(examples): Examples/Ripple.lens に 256 × 256 の thumbnail.png (50,333 バイト、揺れる縞) を足した。ExamplePackagesTests は全ファイルをバイト単位でコピーし、名前があるサムネイルは読めることを求める
  • 8c64a16 docs: docs/ARCHITECTURE.md (Entity の一覧、Physical Abstraction Table、file gateway の段落、.lens の段落、catalog / watch の段落、新しい Key Design Decision)、README (manifest の説明、パッケージの構成、JSON の例)、AGENTS.md、.claude/rules/testing.md

テスト

決定的なテストだけで、sleep も実ディスクも使わない (例外は既存の Gateway smoke と ExamplePackagesTests の読み取り):

  • manifest のデコード (EffectManifestDecodingTests): あり / なし / null / 書かれたとおり (trim しない、../ もデコード時点では保持) / 空 / 型違い (数値・オブジェクト) は broken。round trip でも thumbnail を任意のテキストとして扱う
  • 純粋な規則 (EffectThumbnailReadingTests): パス 27 行 (結合文字を含む ..\u{301} と ../\u{301} の区別、~、...、日本語やスペースを含む名前など)、シグネチャ 11 行、ちょうど 1 MiB は通り 1 バイト多いと tooLarge、ログの文言
  • DataSource (EffectThumbnailListingTests): fake から読めた / サブディレクトリ / 指定なし / 使えない 16 パターン (ログの出先、読んだか読まなかったか、watch の中身) / 引用 / broken なら読まない / 型違いは broken / 隣のパッケージに影響しない / 毎回読み直す (package(_:) も)
  • Repository (EffectPackageRepositoryThumbnailTests)、UseCase (EffectCatalogThumbnailTests: state がサムネイルを持つ、サムネイルだけの変更は publish して何も prepare しない)
  • ホットリロード (EffectCatalogIntegrationTests の anEditedThumbnailReachesTheCatalog): ループバック上でサムネイルのファイル自体の watch を発火させ、差し替えた絵がカタログに届く
  • live gateway の smoke (FileSystemGatewayImplTests): バイト列、上限、空ファイル、存在しないファイルは fileNoSuchFile、ディレクトリは missing ではない

確認

ローカル (origin/main 9153a42 起点):

  • make format: 差分なし / make lint: OK / make check: 全スクリプト OK
  • make build (App + ExtensionApp release) / make build-dev-harness: 成功
  • make test: 987 tests in 134 suites passed, 12 known issues。main の既知 10 件に加え、fake の contract テストで withKnownIssue を 2 件足した (ディレクトリにバイト列を書く、ファイルの下に書く: 既存の write の誤用テストと同じ形)
  • make test-gpu: 27 tests in 6 suites passed

version.txt はマージ直前に main の値から minor を上げて、別の chore(release) commit で入れる。

Part of #12

…ugh the gateway

FileSystemGateway gains readData(_:upTo:), which the live gateway answers
with FileHandle.read(upToCount:), so a caller that keeps only so many bytes
never reads a larger file whole. FileHandle reports a missing file as
fileNoSuchFile, not readString's fileReadNoSuchFile; the smoke suite pins
that, and the in-memory gateway now holds bytes and raises the same code.
…ckage

A manifest may name a thumbnail, a PNG or JPEG file inside the package. The
DataSource refuses a path by its text first (empty, absolute, holding a NUL,
or with a `..` component, split at the byte `/`), reads at most 1 MiB and one
byte, and keeps the bytes only when they begin with a PNG or JPEG signature,
which decides the format. A thumbnail that cannot be used costs the package
its picture and nothing else: the package stays ready, and the reason is
logged with the path quoted. The thumbnail's file and the directories on its
way are watched.

Part of #12
EffectCatalogEntry gains an optional thumbnail, which the Repository fills
from the package; the bundled effect and a broken package have none. A
listing that changes only a thumbnail publishes a new state and prepares
nothing, and on the loopback an edit of the thumbnail file reaches the
catalog.

Part of #12
Ripple.lens names a 256 x 256 thumbnail.png of its swaying stripes. The
example suite now copies every file byte for byte and requires a named
thumbnail to be read.
The README explains the optional `thumbnail` key, ARCHITECTURE records the
decisions in a Key Design Decisions entry (bytes rather than a path, the
lexical refusals, the 1 MiB limit, symbolic links, the logging, the watch),
and AGENTS.md and the testing rules describe the byte-holding fake.
@GeneralD GeneralD self-assigned this Oct 29, 2026
Copilot AI lite review requested due to automatic review settings October 29, 2026 19:29
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 35 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 34a4ebeb-4ef8-4818-96b3-ed5408019557
Commits

Reviewing files that changed from the base of the PR and between 83c3e4e and 60aa88f.

Files ignored due to path filters: 1
  • Examples/Ripple.lens/thumbnail.png is excluded by !**/*.png
Files selected for processing: 30
  • .claude/rules/testing.md
  • AGENTS.md
  • Examples/Ripple.lens/manifest.json
  • README.md
  • Sources/Domain/DataSource/EffectPackages.swift
  • Sources/Domain/Gateway/FileSystemGateway.swift
  • Sources/EffectPackageDataSource/EffectManifestDecoding.swift
  • Sources/EffectPackageDataSource/EffectPackageDataSourceImpl.swift
  • Sources/EffectPackageDataSource/EffectThumbnailReading.swift
  • Sources/EffectPackageRepository/EffectPackageRepositoryImpl.swift
  • Sources/Entity/EffectCatalog.swift
  • Sources/Entity/EffectManifest.swift
  • Sources/Entity/EffectPackage.swift
  • Sources/Entity/EffectThumbnail.swift
  • Sources/FileSystemGateway/FileSystemGatewayImpl.swift
  • Tests/EffectCatalogUseCaseTests/EffectCatalogThumbnailTests.swift
  • Tests/EffectPackageDataSourceTests/EffectManifestDecodingTests.swift
  • Tests/EffectPackageDataSourceTests/EffectManifestRoundTripTests.swift
  • Tests/EffectPackageDataSourceTests/EffectThumbnailListingTests.swift
  • Tests/EffectPackageDataSourceTests/EffectThumbnailReadingTests.swift
  • Tests/EffectPackageDataSourceTests/ExamplePackagesTests.swift
  • Tests/EffectPackageDataSourceTests/PackageRig.swift
  • Tests/EffectPackageRepositoryTests/EffectPackageRepositoryThumbnailTests.swift
  • Tests/FileSystemGatewayTests/FileSystemGatewayImplTests.swift
  • Tests/FramePathIntegrationTests/EffectCatalogIntegrationTests.swift
  • Tests/TestDoubles/EffectFixtures.swift
  • Tests/TestDoubles/InMemoryFileSystemGateway.swift
  • Tests/TestDoublesTests/InMemoryFileSystemGatewayTests.swift
  • docs/ARCHITECTURE.md
  • version.txt
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@codecov

codecov Bot commented Oct 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

… leave it out of package(_:)

FileHandle fails below a file with fileWriteUnknown and on a directory with
codes that differ between runs (fileNoSuchFile, fileWriteUnknown and
fileWriteNoPermission have all been seen), so sorting those reads by code
logged a missing thumbnail as an error, or a directory as missing. The
DataSource now asks isDirectory before it reads: a directory on the way that
is not there, or is a file, makes the thumbnail missing, and a directory where
the file should be is refused as such, both unread.

package(_:) is the read a source is prepared from, for the chosen package on
every listing and every choice, so reading the picture there read it again and
logged its problem again for a value nobody takes. It now answers a ready
package without its thumbnail, which travels with packages() alone.

The package is now built once, from the manifest and the source, instead of
being rebuilt field by field around the thumbnail, and the parents of the file
are found without a range that would trap on no components.
…hose

The smoke suite now pins fileWriteNoPermission for a file that may not be
read, and that a path below a file does not throw a missing code; for a
directory it pins only that the read throws, since the code is not stable.
The port says which code a missing file throws and that a directory is to be
asked about, and the fake no longer claims to mirror live for a directory.
…uire Ripple's

The UseCase test named a thumbnail change "of the chosen package or another"
but only changed the chosen one's. The example suite passed with Ripple's
thumbnail key removed (nil == nil); it now requires Ripple's PNG to be read.
… docs claim

The README and the doc comments described the effect picker as showing the
thumbnail, which nothing does yet. A `..` is refused as a path segment, not
anywhere in the text; a null thumbnail is no thumbnail, not the wrong type;
the watch adds the directories on a thumbnail's way that are there; the
1 MiB limit is per package. ARCHITECTURE no longer says the fake raises
fileReadNoSuchFile for every missing file, and records the directory check
and why package(_:) leaves the picture out.
…ect-thumbnail-manifest

# Conflicts:
#	docs/ARCHITECTURE.md
The merge joined main's wording of readString's missing code with this
branch's readData doc; readData now names its own code in the same form.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants