コンテンツにスキップ

プラグイン開発ガイド

kouchou-ai には3種類のプラグインシステムがあります:

  1. 入力プラグイン - 外部サービス(YouTube、Twitter等)からデータを取得
  2. 分析プラグイン - 分析パイプラインの処理ステップをカスタマイズ
  3. 可視化プラグイン - 分析結果の表示・可視化をカスタマイズ

サンプルとして YouTube 入力プラグインと階層リスト可視化プラグインを同梱しています。 入力は環境変数、可視化は visualization_config で有効化する運用です。


Part 1: 入力プラグイン(Input Plugins)

Admin UIから外部サービスのデータを取り込むためのプラグインシステムです。

概要

入力プラグインにより:

  • YouTube、Twitter等の外部サービスからコメント/投稿を取得
  • プラグインごとに必要な設定(APIキー等)を宣言的に管理
  • 設定不備の早期検出(サーバー起動時にエラー)
  • Admin UIでのプラグイン表示を自動化

クイックスタート:新しい入力プラグインの作成

Step 1: プラグインファイルを作成

apps/api/src/plugins/ に新しいPythonファイルを作成します。

# apps/api/src/plugins/twitter.py
"""
Twitter input plugin for fetching tweets.
"""

import pandas as pd
from typing import Any

from src.plugins.base import InputPlugin, PluginManifest, PluginSetting, SettingType
from src.plugins.registry import PluginRegistry


@PluginRegistry.register
class TwitterPlugin(InputPlugin):
    """Twitter投稿を取得するプラグイン"""

    manifest = PluginManifest(
        id="twitter",                          # 一意のID
        name="Twitter",                        # 表示名
        description="Twitterの投稿を取得します。",  # 説明
        version="1.0.0",                       # バージョン
        icon="twitter",                        # アイコン識別子
        placeholder="https://twitter.com/...", # URL入力欄のプレースホルダー
        enabled_by_default=False,              # デフォルトで無効
        settings=[                             # 必要な設定
            PluginSetting(
                key="TWITTER_API_KEY",
                label="Twitter API Key",
                description="Twitter API v2のAPIキー",
                setting_type=SettingType.SECRET,
                required=True,
            ),
            PluginSetting(
                key="TWITTER_API_SECRET",
                label="Twitter API Secret",
                description="Twitter API v2のAPIシークレット",
                setting_type=SettingType.SECRET,
                required=True,
            ),
        ],
    )

    def validate_source(self, source: str) -> tuple[bool, str | None]:
        """ソースURL/IDの検証"""
        if "twitter.com" not in source and "x.com" not in source:
            return False, "無効なTwitter URLです"
        return True, None

    def fetch_data(self, source: str, **options: Any) -> pd.DataFrame:
        """データを取得してDataFrameで返す"""
        # 設定が完了しているか確認(必須)
        self.ensure_configured()

        # URL検証
        is_valid, error = self.validate_source(source)
        if not is_valid:
            raise ValueError(error)

        # APIキーを取得
        api_key = self.manifest.settings[0].get_value()
        api_secret = self.manifest.settings[1].get_value()

        # データ取得処理...
        # (Twitter API呼び出しを実装)

        # 必須カラム: comment-id, comment-body, source, url
        # オプション: attribute_* で追加属性
        return pd.DataFrame([
            {
                "comment-id": "tweet_123",
                "comment-body": "投稿内容",
                "source": "Twitter",
                "url": "https://twitter.com/...",
                "attribute_author": "@username",
                "attribute_likes": 100,
            }
        ])

Step 2: 環境変数を設定

.env ファイルにプラグインの有効化と設定を追加:

# プラグインを有効化(必須)
ENABLE_TWITTER_INPUT_PLUGIN=true

# プラグイン固有の設定
TWITTER_API_KEY=your-api-key
TWITTER_API_SECRET=your-api-secret

以上で完了です! registry.py や Admin UI の修正は不要です。

プラグインの動作

有効化の仕組み

環境変数 APIキー 動作
未設定 - プラグイン無効(Admin UIに「設定が必要です」と表示)
=true 未設定 サーバー起動エラー(設定漏れを早期検出)
=true 設定済み プラグイン有効(Admin UIで使用可能)

自動検出の仕組み

src/plugins/ 配下のPythonファイルは自動的に検出・登録されます:

apps/api/src/plugins/
├── __init__.py      # エクスポート(変更不要)
├── base.py          # 基底クラス(変更不要)
├── registry.py      # レジストリ(変更不要)
├── youtube.py       # YouTubeプラグイン
└── twitter.py       # 新規追加 → 自動で登録される

PluginManifest フィールド

フィールド 説明
id str 一意のプラグインID(例: "youtube", "twitter"
name str Admin UIに表示される名前
description str プラグインの説明
version str セマンティックバージョン(例: "1.0.0"
icon str \| None アイコン識別子(将来のUI用)
placeholder str URL入力フィールドのプレースホルダー
enabled_by_default bool False を推奨(明示的有効化が必要)
settings list[PluginSetting] 必要な設定のリスト

PluginSetting フィールド

フィールド 説明
key str 環境変数名(例: "YOUTUBE_API_KEY"
label str Admin UIに表示されるラベル
description str 設定の説明
setting_type SettingType STRING, SECRET, INTEGER, BOOLEAN, URL
required bool 必須かどうか
default Any デフォルト値(required=False の場合)

fetch_data の戻り値

fetch_data() は以下のカラムを持つ pd.DataFrame を返す必要があります:

必須カラム

カラム名 説明
comment-id コメントの一意ID
comment-body コメント本文
source ソース名(例: "YouTube", "Twitter"
url 元のコメントへのURL

オプションカラム(属性)

attribute_ プレフィックスで追加属性を含められます:

{
    "attribute_author": "投稿者名",
    "attribute_published_at": "2024-01-01T00:00:00Z",
    "attribute_like_count": 100,
    "attribute_video_title": "動画タイトル",
}

依存関係の追加

外部ライブラリが必要な場合、apps/api/pyproject.toml にオプション依存として追加:

[project.optional-dependencies]
youtube = ["google-api-python-client>=2.150.0"]
twitter = ["tweepy>=4.14.0"]
all-plugins = [
    "google-api-python-client>=2.150.0",
    "tweepy>=4.14.0",
]

インストール:

pip install -e ".[twitter]"
# または全プラグイン
pip install -e ".[all-plugins]"

既存プラグイン: YouTube

参考実装として apps/api/src/plugins/youtube.py を参照してください。

機能: - YouTube動画URLからコメントを取得 - プレイリストURLから複数動画のコメントを一括取得 - 返信コメントの取得(オプション)

環境変数:

ENABLE_YOUTUBE_INPUT_PLUGIN=true
YOUTUBE_API_KEY=your-youtube-data-api-v3-key


Part 2: 分析プラグイン(Analysis Plugins)

analysis-core のプラグインシステムを使用して、カスタム分析ステップを追加する方法を説明します。

概要

analysis-core は、分析パイプラインの各ステップをプラグインとして実装するアーキテクチャを採用しています。これにより:

  • コアコードを変更せずに新しい分析ステップを追加できる
  • 既存のステップを置き換えたり拡張したりできる
  • ワークフロー定義で柔軟にステップを組み合わせられる

プラグインの種類

1. 組み込みプラグイン

analysis_core.plugins.builtin に含まれる標準プラグイン:

プラグインID 説明
analysis.extraction コメントから意見を抽出
analysis.embedding 埋め込みベクトル生成
analysis.hierarchical_clustering 階層クラスタリング
analysis.hierarchical_initial_labelling 初期ラベリング
analysis.hierarchical_merge_labelling 統合ラベリング
analysis.hierarchical_overview 概要生成
analysis.hierarchical_aggregation 結果集約
analysis.hierarchical_visualization 可視化HTML生成

2. 外部プラグイン

plugins/analysis/ ディレクトリに配置するカスタムプラグイン。

クイックスタート:関数ベースプラグイン

最も簡単なプラグイン作成方法は @step_plugin デコレータを使用することです。

from analysis_core.plugin import (
    step_plugin,
    StepContext,
    StepInputs,
    StepOutputs,
)

@step_plugin(
    id="mycompany.custom_analysis",
    version="1.0.0",
    name="Custom Analysis",
    description="カスタム分析ステップ",
    inputs=["arguments"],  # 依存するアーティファクト
    outputs=["custom_result"],  # 生成するアーティファクト
    use_llm=False,  # LLMを使用するかどうか
)
def custom_analysis_plugin(
    ctx: StepContext,
    inputs: StepInputs,
    config: dict,
) -> StepOutputs:
    """カスタム分析を実行"""

    # 入力ファイルを読み込み
    args_path = inputs.artifacts.get("arguments")

    # 処理を実行
    # ...

    # 結果を保存
    output_path = ctx.output_dir / "custom_result.csv"
    # result_df.to_csv(output_path, index=False)

    return StepOutputs(
        artifacts={"custom_result": output_path},
        token_usage=0,
    )

プラグインインターフェース

StepContext

ステップ実行時のコンテキスト情報:

@dataclass
class StepContext:
    output_dir: Path      # 出力ディレクトリ
    input_dir: Path       # 入力ディレクトリ
    dataset: str          # データセット名
    provider: str         # LLMプロバイダー (openai, azure, etc.)
    model: str            # LLMモデル名
    local_llm_address: str | None  # ローカルLLMアドレス
    user_api_key: str | None       # ユーザーAPIキー

StepInputs

ステップへの入力:

@dataclass
class StepInputs:
    artifacts: dict[str, Path]  # アーティファクトID → ファイルパス
    config: dict[str, Any]      # 追加の設定

StepOutputs

ステップからの出力:

@dataclass
class StepOutputs:
    artifacts: dict[str, Path]  # 生成したアーティファクト
    token_usage: int = 0        # 使用したトークン数
    token_input: int = 0        # 入力トークン数
    token_output: int = 0       # 出力トークン数
    metadata: dict[str, Any]    # 追加のメタデータ

外部プラグインの作成

ディレクトリ構造

plugins/analysis/
└── my-custom-step/
    ├── manifest.yaml    # プラグインメタデータ
    └── plugin.py        # プラグイン実装

manifest.yaml

id: mycompany.custom_step
version: "1.0.0"
name: "My Custom Step"
description: "カスタム分析ステップの説明"
entry: plugin:custom_step_plugin  # モジュール:属性名

inputs:
  - arguments
  - clusters

outputs:
  - custom_result

use_llm: false

# オプション: 設定スキーマ
config_schema:
  type: object
  properties:
    threshold:
      type: number
      default: 0.5

plugin.py

from analysis_core.plugin import (
    step_plugin,
    StepContext,
    StepInputs,
    StepOutputs,
)

@step_plugin(
    id="mycompany.custom_step",
    version="1.0.0",
    name="My Custom Step",
    description="カスタム分析ステップ",
    inputs=["arguments", "clusters"],
    outputs=["custom_result"],
)
def custom_step_plugin(
    ctx: StepContext,
    inputs: StepInputs,
    config: dict,
) -> StepOutputs:
    # 設定を取得
    threshold = config.get("threshold", 0.5)

    # 入力を読み込み
    args_path = inputs.artifacts.get("arguments")
    clusters_path = inputs.artifacts.get("clusters")

    # 処理...

    # 出力
    output_path = ctx.output_dir / "custom_result.json"
    return StepOutputs(artifacts={"custom_result": output_path})

プラグインの登録と使用

手動登録

from analysis_core.plugin import get_registry

# グローバルレジストリを取得
registry = get_registry()

# 組み込みプラグインを登録
registry.register_builtin_plugins()

# カスタムプラグインを登録
registry.register(custom_step_plugin)

# プラグインを取得
plugin = registry.get("mycompany.custom_step")

ディレクトリから読み込み

from pathlib import Path
from analysis_core.plugin import (
    load_plugins_from_directory,
    get_registry,
)

# プラグインディレクトリから読み込み
plugins_dir = Path("plugins/analysis")
loaded = load_plugins_from_directory(plugins_dir, get_registry())

print(f"Loaded {len(loaded)} plugins")
for p in loaded:
    print(f"  - {p.manifest.id} v{p.manifest.version}")

環境変数で指定

# プラグインディレクトリを環境変数で指定
export ANALYSIS_PLUGINS_PATH=/path/to/plugins:/another/path

# パイプライン実行時に自動的に読み込まれる
python -m analysis_core config.json

ワークフローでの使用

カスタムプラグインをワークフローで使用するには:

from analysis_core.workflow import WorkflowDefinition, WorkflowStep

workflow = WorkflowDefinition(
    id="custom-workflow",
    version="1.0.0",
    name="Custom Analysis Workflow",
    steps=[
        WorkflowStep(
            id="extract",
            plugin="analysis.extraction",
        ),
        WorkflowStep(
            id="embed",
            plugin="analysis.embedding",
            depends_on=["extract"],
        ),
        WorkflowStep(
            id="custom",
            plugin="mycompany.custom_step",  # カスタムプラグイン
            depends_on=["extract"],
            config={"threshold": 0.7},
        ),
    ],
)

LLMを使用するプラグイン

LLMを使用するプラグインの例:

@step_plugin(
    id="mycompany.llm_analysis",
    version="1.0.0",
    inputs=["arguments"],
    outputs=["analysis_result"],
    use_llm=True,  # LLM使用フラグ
)
def llm_analysis_plugin(
    ctx: StepContext,
    inputs: StepInputs,
    config: dict,
) -> StepOutputs:
    from analysis_core.services.llm import request_to_chat_ai

    # プロンプトを取得(configから、またはデフォルト)
    prompt = config.get("prompt", "Analyze the following...")
    model = config.get("model", ctx.model)

    # LLMリクエスト
    response = request_to_chat_ai(
        messages=[{"role": "user", "content": prompt}],
        model=model,
        provider=ctx.provider,
    )

    # トークン使用量を記録
    return StepOutputs(
        artifacts={"analysis_result": output_path},
        token_usage=response.get("total_tokens", 0),
        token_input=response.get("prompt_tokens", 0),
        token_output=response.get("completion_tokens", 0),
    )

テスト

プラグインのテスト例:

import pytest
from pathlib import Path
from analysis_core.plugin import StepContext, StepInputs

def test_custom_plugin():
    # テスト用コンテキスト
    ctx = StepContext(
        output_dir=Path("/tmp/test_output"),
        input_dir=Path("/tmp/test_input"),
        dataset="test",
        provider="openai",
        model="gpt-4o-mini",
    )

    # テスト用入力
    inputs = StepInputs(
        artifacts={"arguments": Path("/tmp/test_input/args.csv")},
    )

    # プラグイン実行
    result = custom_step_plugin(ctx, inputs, {"threshold": 0.5})

    # 検証
    assert result.artifacts.get("custom_result") is not None
    assert result.artifacts["custom_result"].exists()

ベストプラクティス

  1. ID命名規則: organization.step_name 形式を使用(例: mycompany.sentiment_analysis

  2. バージョニング: セマンティックバージョニングを使用(例: 1.0.0

  3. 入出力の明示: inputsoutputs を明確に定義して依存関係を明示

  4. エラーハンドリング: 適切な例外処理を実装

  5. トークン追跡: LLM使用時は token_usage を正確に報告

  6. 設定スキーマ: config_schema でバリデーション可能な設定を定義

トラブルシューティング

プラグインが見つからない

PluginNotFoundError: Plugin 'mycompany.custom_step' not found

解決策: - manifest.yaml の id とコードの id が一致しているか確認 - entry フィールドのモジュール名と属性名が正しいか確認 - プラグインディレクトリが正しく設定されているか確認

インポートエラー

PluginLoadError: Import error: No module named 'some_dependency'

解決策: - プラグインが必要とする依存関係がインストールされているか確認 - 依存関係を pyproject.toml または requirements.txt に追加

型エラー

PluginLoadError: Attribute 'plugin' is not an AnalysisStepPlugin

解決策: - @step_plugin デコレータが正しく適用されているか確認 - 関数シグネチャが正しいか確認(ctx, inputs, config の3引数)


Part 3: 可視化プラグイン(Visualization Plugins)

分析結果の表示・可視化をカスタマイズするためのプラグインシステムです。

概要

可視化プラグインにより:

  • 分析結果のグラフ・チャートのカスタマイズ
  • 新しい可視化コンポーネントの追加
  • チャートモードの追加・削除
  • レポートごとに表示するチャートの設定

アーキテクチャ

apps/public-viewer/components/charts/
├── plugins/
│   ├── types.ts          # プラグインの型定義
│   ├── registry.ts       # プラグインレジストリ
│   ├── scatter.tsx       # 散布図プラグイン (scatterAll, scatterDensity)
│   ├── treemap.tsx       # ツリーマップ プラグイン
│   └── hierarchy-list.tsx # 階層リストプラグイン
├── ScatterChart.tsx       # 散布図コンポーネント
├── TreemapChart.tsx       # ツリーマップコンポーネント
├── HierarchyListChart.tsx # 階層リストコンポーネント
└── SelectChartButton.tsx  # チャート選択UI

クイックスタート:新しいチャートプラグインの作成

Step 1: チャートコンポーネントを作成

// apps/public-viewer/components/charts/MyCustomChart.tsx
import type { Argument, Cluster } from "@/type";

type Props = {
  clusterList: Cluster[];
  argumentList: Argument[];
  onHover?: () => void;
  filteredArgumentIds?: string[];
};

export function MyCustomChart({ clusterList, argumentList, filteredArgumentIds }: Props) {
  return (
    <div>
      {/* チャートの実装 */}
    </div>
  );
}

Step 2: プラグインファイルを作成

// apps/public-viewer/components/charts/plugins/my-custom.tsx
import { MyCustomIcon } from "@/components/icons/ViewIcons";
import { MyCustomChart } from "../MyCustomChart";
import type { ChartPlugin, ChartRenderContext } from "./types";

export const myCustomPlugin: ChartPlugin = {
  manifest: {
    id: "my-custom",                    // 一意のプラグインID
    name: "カスタムチャート",           // 表示名
    description: "カスタム可視化を表示", // 説明
    version: "1.0.0",                   // バージョン
    icon: MyCustomIcon,                 // アイコンコンポーネント
    modes: [                            // このプラグインが提供するモード
      {
        id: "customView",               // モードID(ChartTypeに追加が必要)
        label: "カスタム",              // 表示ラベル
        icon: MyCustomIcon,
      },
    ],
  },

  canHandle: (mode: string) => {
    return mode === "customView";
  },

  render: (context: ChartRenderContext) => {
    const { result, filteredArgumentIds, onHover } = context;

    return (
      <MyCustomChart
        clusterList={result.clusters}
        argumentList={result.arguments}
        onHover={onHover}
        filteredArgumentIds={filteredArgumentIds}
      />
    );
  },
};

Step 3: ChartTypeは更新不要(任意)

ChartType は既知のID + 任意文字列を許容するため、サーバや共有スキーマの更新は不要です。 補完を強くしたい場合のみ、型定義の例を参考にしてください。

// apps/public-viewer/type.ts
export type ChartType =
  | "scatterAll"
  | "scatterDensity"
  | "treemap"
  | "hierarchyList"
  | (string & {});

Step 4: レジストリにプラグインを登録

// apps/public-viewer/components/charts/plugins/registry.ts
import { myCustomPlugin } from "./my-custom";

export function loadBuiltinChartPlugins(): void {
  // ... 既存のプラグイン登録
  chartRegistry.register(myCustomPlugin);
}

プラグインインターフェース

ChartPlugin

interface ChartPlugin {
  /** プラグインのメタデータ */
  manifest: ChartPluginManifest;

  /** 指定されたモードを処理できるか判定 */
  canHandle: (mode: string) => boolean;

  /** チャートをレンダリング */
  render: (context: ChartRenderContext) => React.ReactNode;
}

ChartPluginManifest

interface ChartPluginManifest {
  /** 一意のプラグインID */
  id: string;
  /** 表示名 */
  name: string;
  /** 説明 */
  description: string;
  /** バージョン(semver) */
  version: string;
  /** アイコンコンポーネント */
  icon: ComponentType;
  /** このプラグインが提供するモード */
  modes: ChartMode[];
}

ChartMode

interface ChartMode {
  /** モードID(selectedChartの値) */
  id: string;
  /** 表示ラベル */
  label: string;
  /** アイコンコンポーネント */
  icon: ComponentType;
  /** データに基づいて無効化できるか */
  canBeDisabled?: boolean;
  /** 無効化判定関数 */
  isDisabled?: (result: Result) => boolean;
  /** 無効時のツールチップ */
  disabledTooltip?: string;
}

ChartRenderContext

interface ChartRenderContext {
  /** 分析結果データ */
  result: Result;
  /** 選択中のチャートモード */
  selectedChart: string;
  /** フルスクリーンモードか */
  isFullscreen: boolean;
  /** フィルター適用時の引数ID */
  filteredArgumentIds?: string[];
  /** クラスターラベル表示 */
  showClusterLabels?: boolean;
  /** ツリーマップのズームレベル */
  treemapLevel?: string;
  /** ツリーマップナビゲーション */
  onTreeZoom?: (level: string) => void;
  /** ホバーイベント */
  onHover?: () => void;
}

組み込みプラグイン

1. Scatter Plugin (scatter)

散布図を表示するプラグイン。2つのモードを提供:

モード 説明
scatterAll 全データの散布図表示
scatterDensity 密度フィルター適用済み表示(条件付きで無効化)

2. Treemap Plugin (treemap)

階層構造をツリーマップで可視化。

モード 説明
treemap 階層ツリーマップ表示

3. Hierarchy List Plugin (hierarchy-list)

クラスタを展開可能なリストで表示。

モード 説明
hierarchyList 階層リスト表示

レポートごとのチャート設定

visualization_config

レポートごとに表示するチャートを設定できます:

// apps/public-viewer/type.ts
interface ReportDisplayConfig {
  version: string;
  /** 有効なチャートタイプ */
  enabledCharts: ChartType[];
  /** デフォルトで表示するチャート */
  defaultChart?: ChartType;
  /** チャートの表示順序 */
  chartOrder?: ChartType[];
  /** チャート固有のパラメータ */
  params?: DisplayParams;
}

使用例

{
  "visualizationConfig": {
    "version": "1.0",
    "enabledCharts": ["treemap", "hierarchyList"],
    "defaultChart": "hierarchyList",
    "chartOrder": ["hierarchyList", "treemap"]
  }
}

これにより、散布図を非表示にしてツリーマップと階層リストのみを表示できます。

条件付きモード無効化

特定の条件でモードを無効化できます:

modes: [
  {
    id: "scatterDensity",
    label: "濃い意見",
    icon: DenseViewIcon,
    canBeDisabled: true,
    isDisabled: (result) => {
      // 最大レベルが1以下の場合は無効
      const maxLevel = Math.max(...result.clusters.map((c) => c.level));
      return maxLevel <= 1;
    },
    disabledTooltip: "この設定条件では抽出できませんでした",
  },
]

プラグインレジストリ

主要メソッド

// プラグイン登録
chartRegistry.register(plugin: ChartPlugin): void;

// プラグイン取得
chartRegistry.get(pluginId: string): ChartPlugin | undefined;

// モードからプラグイン取得
chartRegistry.getByMode(modeId: string): ChartPlugin | undefined;

// 全モード取得
chartRegistry.getAllModes(): ChartMode[];

// モード存在チェック
chartRegistry.hasMode(modeId: string): boolean;

衝突検出

レジストリは重複するプラグインIDやモードIDを検出し、警告を出力します:

Chart plugin collision: "scatter" is already registered. Overwriting with new plugin (v2.0.0).
Chart mode collision: "scatterAll" is already registered by plugin "scatter". Overwriting with plugin "scatter-v2".

テスト

プラグインのテスト例:

// components/charts/plugins/__tests__/plugins.test.tsx
import { myCustomPlugin } from "../my-custom";

describe("myCustomPlugin", () => {
  it("has correct plugin ID", () => {
    expect(myCustomPlugin.manifest.id).toBe("my-custom");
  });

  it("canHandle returns true for customView", () => {
    expect(myCustomPlugin.canHandle("customView")).toBe(true);
  });

  it("renders without error", () => {
    const result = { clusters: [], arguments: [], config: { title: "Test" } };
    const element = myCustomPlugin.render({
      result,
      selectedChart: "customView",
      isFullscreen: false,
    });
    expect(element).not.toBeNull();
  });
});

バリデーションシステム

プラグインシステムには、コンパイル時チェックに似た事前検証機能があります。これにより、実行時のデバッグ困難なバグを早期に検出できます。

バリデーション関数

import {
  validatePlugin,
  validatePluginManifest,
  validateChartMode,
  validateVisualizationConfig,
  validateResultData,
  formatValidationResult,
  assertValidation,
  logValidationWarnings,
} from "@/components/charts/plugins";
関数 用途
validatePlugin(plugin) プラグイン全体を検証
validatePluginManifest(manifest) マニフェストのみを検証
validateChartMode(mode, pluginId) 個別モードを検証
validateVisualizationConfig(config, registry) visualization_configを検証
validateResultData(result) 結果データ構造を検証
formatValidationResult(result) 検証結果をフォーマット
assertValidation(result, context) エラー時に例外をスロー
logValidationWarnings(result, context) 警告をコンソールに出力

エラーコード

マニフェスト関連 (MANIFEST_*)

コード 重要度 説明
MANIFEST_MISSING_ID error プラグインIDが未定義
MANIFEST_MISSING_NAME error 表示名が未定義
MANIFEST_MISSING_VERSION error バージョンが未定義
MANIFEST_INVALID_VERSION_FORMAT warning semver形式でないバージョン
MANIFEST_MISSING_ICON error アイコンが未定義
MANIFEST_MODES_NOT_ARRAY error modesが配列でない
MANIFEST_NO_MODES warning モードが定義されていない
MANIFEST_DUPLICATE_MODE_IDS error モードIDが重複

モード関連 (MODE_*)

コード 重要度 説明
MODE_MISSING_ID error モードIDが未定義
MODE_MISSING_LABEL error ラベルが未定義
MODE_MISSING_ICON error アイコンが未定義
MODE_ISDISABLED_WITHOUT_FLAG warning isDisabledがあるがcanBeDisabledがfalse
MODE_CANBEDISABLED_WITHOUT_ISDISABLED warning canBeDisabledがtrueだがisDisabledがない

プラグイン関連 (PLUGIN_*)

コード 重要度 説明
PLUGIN_CANHANDLE_NOT_FUNCTION error canHandleが関数でない
PLUGIN_CANHANDLE_MISMATCH error canHandleが宣言モードでfalseを返す
PLUGIN_CANHANDLE_THROWS error canHandleが例外をスロー
PLUGIN_RENDER_NOT_FUNCTION error renderが関数でない

設定関連 (CONFIG_*)

コード 重要度 説明
CONFIG_ENABLED_CHARTS_NOT_ARRAY error enabledChartsが配列でない
CONFIG_UNKNOWN_CHART_TYPE error 未登録のチャートタイプ
CONFIG_DUPLICATE_ENABLED_CHARTS warning enabledChartsに重複
CONFIG_EMPTY_ENABLED_CHARTS warning enabledChartsが空
CONFIG_UNKNOWN_DEFAULT_CHART error defaultChartが未登録
CONFIG_DEFAULT_NOT_IN_ENABLED error defaultChartがenabledChartsに含まれない
CONFIG_CHART_ORDER_NOT_ARRAY error chartOrderが配列でない
CONFIG_UNKNOWN_CHART_IN_ORDER warning chartOrderに未登録のチャート
CONFIG_ENABLED_NOT_IN_ORDER warning enabledChartsのチャートがchartOrderにない

結果データ関連 (RESULT_*)

コード 重要度 説明
RESULT_NULL error 結果データがnull
RESULT_CLUSTERS_NOT_ARRAY error clustersが配列でない
RESULT_NO_CLUSTERS warning clustersが空
CLUSTER_MISSING_ID error クラスターにidがない
CLUSTER_MISSING_LEVEL error クラスターにlevelがない
RESULT_ARGUMENTS_NOT_ARRAY error argumentsが配列でない

ストリクトモード

レジストリをストリクトモードに設定すると、バリデーションエラー時に例外をスローします:

import { chartRegistry } from "@/components/charts/plugins";

// 開発環境でストリクトモードを有効化
if (process.env.NODE_ENV === "development") {
  chartRegistry.setStrictMode(true);
}

// エラーのあるプラグインを登録しようとすると例外がスロー
chartRegistry.register(invalidPlugin); // throws Error

バリデーションの使用例

import {
  validatePlugin,
  validateVisualizationConfig,
  formatValidationResult,
  chartRegistry,
} from "@/components/charts/plugins";

// プラグインの検証
const plugin = createMyPlugin();
const pluginResult = validatePlugin(plugin);

if (!pluginResult.valid) {
  console.error("Plugin validation failed:");
  console.error(formatValidationResult(pluginResult));
}

// visualization_configの検証
const config = { enabledCharts: ["scatterAll", "unknownChart"] };
const configResult = validateVisualizationConfig(config, chartRegistry);

if (!configResult.valid) {
  console.error("Config validation failed:");
  console.error(formatValidationResult(configResult));
}
// Output:
// ✗ 1 error(s):
//   [CONFIG_UNKNOWN_CHART_TYPE] visualizationConfig.enabledCharts contains unknown chart type 'unknownChart'. Available: scatterAll, scatterDensity, treemap, hierarchyList

自動バリデーション

ClientContainer コンポーネントは、ロード時に自動的にバリデーションを実行し、警告をコンソールに出力します:

// apps/public-viewer/components/report/ClientContainer.tsx
useEffect(() => {
  const resultValidation = validateResultData(result);
  if (!resultValidation.valid || resultValidation.warnings.length > 0) {
    console.warn(`Result data validation:\n${formatValidationResult(resultValidation)}`);
  }

  const configValidation = validateVisualizationConfig(result.visualizationConfig, chartRegistry);
  if (!configValidation.valid || configValidation.warnings.length > 0) {
    console.warn(`Visualization config validation:\n${formatValidationResult(configValidation)}`);
  }
}, [result]);

関連ファイル

  • apps/public-viewer/components/charts/plugins/ - プラグインシステム
  • apps/public-viewer/components/charts/plugins/validation.ts - バリデーションシステム
  • apps/public-viewer/components/charts/ - チャートコンポーネント
  • apps/public-viewer/components/report/ClientContainer.tsx - visualization_config読み込みとバリデーション
  • apps/public-viewer/type.ts - 型定義(ChartType, ReportDisplayConfig)