services-layer について
services-layerは、Whisperingのサービス層の変更を導くプロジェクト固有のスキルです。UIを含まないサービスの境界、エラー処理、プラットフォーム選択、サービスオブジェクトとファクトリーの使い分けを定めています。
主な機能
- 01UI非依存のサービス境界の定義
- 02Resultベースのエラー処理の指針
- 03ビルド時のプラットフォームマッピングの説明
- 04直接オブジェクトとファクトリーの区別
- 05サービスバレルの規約の説明
活用シーン
- 01Whisperingの新規サービス設計
- 02サービスの依存関係と境界の確認
- 03プラットフォーム別実装の選択
- 04オブジェクトとファクトリーの選択
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill services-layerインストール後、Claude Code で/services-layerを入力して読み込まれたか確認します。
同梱ファイルreferences/service-implementation-pattern.mdreferences/service-organization-platforms.md
詳細
サービスレイヤー
サービスに関する指針は、apps/whispering/src/lib/services、その利用元、および apps/whispering/package.json の #platform/* マッピングに基づいてください。過去の例や近接するスキルは手がかりとして扱い、現在のアーキテクチャとして扱わないでください。
プロダクトを表す一文
サービスレイヤーは UI に依存しない機能を担います。呼び出し元は明示的な入力を通じてアプリのポリシーを注入し、失敗する操作は Result を返し、プラットフォームの選択はビルド時に一か所で行います。
境界
サービスは IO を実行し、サービス内のランタイム状態を保持することもできます。純粋関数である必要はありません。ただし、UI やアプリが所有するポリシーには依存しないようにしてください。
- Svelte ストア、
settings、deviceConfig、トースト、reportをランタイムに読み取らないでください。 - 認証情報、モデル名、エンドポイント、パス、ユーザーの選択は入力として受け取ってください。
- プレゼンテーション用の文言や UI の状態ではなく、ドメインデータとエラーを返してください。
#platform/*の境界の両側で同じコントラクトを公開してください。
通常、$lib/operations はアプリの設定を読み取り、プロバイダーを選択して、サービスを組み合わせます。$lib/queries は、UI が必要とする場合にのみ、共通のクエリ ID と監視可能なライフサイクルを追加します。
文字起こしディレクトリには、プロバイダーレジストリデータと UI 向けの provider-ui.ts 結合モジュールもあります。これらの隣接するメタデータモジュールはサービスの実装ではありません。サービスの境界を緩める理由として使わないでください。
失敗する可能性を正確にモデル化する
通常のランタイムで失敗する可能性がある公開操作には Result<T, E> を使用してください。例外を投げるプラットフォームまたはライブラリの呼び出しは、error-handling スキルを使って適合させてください。
失敗しないインメモリアクションやクリーンアップに、見せかけの Result を使わないでください。現在の例では、ローカルショートカットの register / unregister は void を返し、リスナーのセットアップはクリーンアップ関数を返します。
export type DownloadService = {
downloadBlob(args: {
name: string;
blob: Blob;
}): Promise<Result<void, DownloadError>>;
};
エラーは、失敗を理解しているレイヤーに属します。サービスを組み合わせるときは、下位レイヤーのタグ付きエラーを保持してください。サービス自体が所有する失敗に限り、サービス内のバリアントを定義してください。バリアントの形式とメッセージのルールには define-errors を、適合と伝播には error-handling を使用してください。
直接オブジェクトまたはファクトリー
生成時の入力やライフサイクルが不要な場合は、直接 *ServiceLive オブジェクトを使うのが基本です。現在のダウンロード、分析、テキスト、文字起こしプロバイダーの実装では、この形式が使われています。ダウンロード、分析、テキストは satisfies <Service> を使って、共通のプラットフォームコントラクトを確認しています。
実際の生成入力、差し替え可能な依存関係、または状態を持つライフサイクルを管理する場合は、ファクトリーを使用してください。ブラウザーと CPAL のレコーダーファクトリーは、それぞれ停止、キャンセル、購読、ティアダウンの動作を持つ録音セッションを生成するため、この境界に意味があります。
状態を持たないオブジェクトに、機械的に create* と *Live の両方を追加しないでください。
現在の直接オブジェクトの例とファクトリーを選ぶ基準については、サービス実装パターンを参照してください。
プラットフォームとランタイムの選択
ブラウザーと Tauri の実装がある機能には、#platform/* インポートを使用してください。package.json#imports がビルド時に実装を選択するため、共通の呼び出し元は単一の安定した名前をインポートし、window.__TAURI_INTERNALS__ を調べません。
Tauri 専用の機能には、null 許容の #platform/tauri 名前空間を使用してください。
ユーザーが選ぶプロバイダーはプラットフォームの境界ではなく、ランタイムのポリシーです。ディスパッチは利用するオペレーションに置いてください。Whispering の文字起こしでは $lib/operations/transcribe.ts が選択されたプロバイダーを読み取り、クエリレイヤーはそのオペレーションを監視するだけです。
プラットフォームサービスを追加または移動する場合は、サービスの構成とプラットフォームバリアントを参照してください。
サービスのバレル
$lib/services/index.ts は、プラットフォーム間で安定して利用できる機能をまとめています。
export const services = {
analytics: AnalyticsServiceLive,
text: TextServiceLive,
blobs: BlobsLive,
blobSources: BlobSourcesLive,
download: DownloadServiceLive,
localShortcutManager: LocalShortcutManagerLive,
sound: PlaySoundServiceLive,
} as const;
すべてのプロバイダー実装をこのバレルに含める必要はありません。実行時のオペレーションがディスパッチテーブルを所有する場合、プロバイダー固有のサービスを直接インポートできます。
最終確認
- サービスは UI やアプリ所有の設定をインポートしません。
- すべてのアプリの選択肢は明示的な入力として渡されるか、呼び出し元によって選択されます。
- 失敗する可能性のある公開操作は
Resultを返し、失敗しない操作は通常の形式のままにします。 - ファクトリーは、単なる慣例ではなく、構築またはライフサイクルを担います。
- プラットフォームの選択は、1 つの
#platform/*マッピングを通じて行います。 - ランタイムプロバイダーの選択は、利用する操作の実行時に一度だけ行います。
- このサービスが新たな失敗を担う場合を除き、下位レイヤーのエラーはそのまま渡します。