# 新アーキテクチャ設計案

## 概要

`mdlint` を複数の文書タイプ（テキスト教材、クイズ、スライド、演習問題など）に対応した拡張可能なリンターに再設計する。DDDの考え方を参考に、各文書タイプをドメインモデルとして定義し、検証ルールをバリデーターオブジェクトとして分離する。**サブコマンド**で文書タイプを明示的に指定し、各サブコマンドに対応する**UseCase**がリンティングロジックを実行する。

## 現状の課題

```
src/
├── index.ts        # CLI エントリーポイント
├── linter.ts       # 全ルールをハードコード
└── rules/
    ├── frontmatter.ts
    ├── toc.ts
    ├── structure.ts
    ├── callouts.ts
    └── codeblocks.ts
```

- 単一の文書タイプ（テキスト教材）のみ対応
- ルールがハードコードされ拡張が困難
- 文書タイプの判別機能が曖昧
- ルールの依存関係が不明確

## CLI設計（サブコマンドベース）

```bash
# 使用例
mdlint text <file>        # テキスト教材をチェック
mdlint quiz <file>        # 理解度確認問題をチェック
mdlint slides <file>      # スライド教材をチェック
mdlint practice <file>    # 演習問題をチェック
mdlint requirements <file> # 研修要件定義書をチェック
mdlint design <file>      # 研修設計書をチェック
mdlint flyer <file>       # フライヤー要件をチェック
mdlint promotion <file>   # プロモーションをチェック

# 共通オプション
mdlint text <file> --fix          # 自動修正（将来拡張）
mdlint text <file> --format json  # JSON出力
mdlint --help                     # ヘルプ表示
```

## 対応する文書タイプ（ドメインモデル）

`_rules/` フォルダの分析結果から、以下の文書タイプを識別:

| サブコマンド | 文書タイプ | ルールファイル | 特徴 |
|-------------|-----------|---------------|------|
| `text` | テキスト教材 | text.md | frontmatter, toc, 学習目標, 学習項目, まとめ, :::step |
| `quiz` | 理解度確認問題 | quiz.md | frontmatter, toc, :::quiz, :::question, :::select, :::correct |
| `slides` | スライド教材 | slides.md | marp: true, `---`区切り, 箇条書き中心 |
| `practice` | 演習問題 | practice.md | frontmatter, toc, 概要, 問題, 解答形式, 解答例 |
| `requirements` | 研修要件定義書 | requirements.md | 研修概要, 学習目標, カリキュラム, 研修詳細 |
| `design` | 研修設計書 | design.md | 研修設計, 章構成, タイムスケジュール |
| `flyer` | フライヤー要件 | flyer.md | キャッチ文章, 特徴, カリキュラム |
| `promotion` | プロモーション | promotion.md | キャッチコピー, 研修概要, 特徴 |

## 新アーキテクチャ

### ディレクトリ構造

```
src/
├── index.ts                    # CLI エントリーポイント（サブコマンド定義）
│
├── core/
│   ├── types.ts                # 共通型定義（LintResult, DocumentMetadata等）
│   └── parser.ts               # Markdown パーサー（unified/remark）
│
├── domain/                     # ドメインモデル層
│   ├── shared/                 # 共有バリデーター
│   │   ├── BaseFrontmatterValidator.ts
│   │   ├── MarkdownSyntaxValidator.ts
│   │   └── CodeBlockLanguageValidator.ts
│   │
│   ├── text/                   # テキスト教材ドメイン
│   │   ├── TextDocument.ts     # ドメインモデル（バリデーター群を保持）
│   │   └── validators/
│   │       ├── FrontmatterValidator.ts
│   │       ├── TocValidator.ts
│   │       ├── StructureValidator.ts
│   │       ├── StepDirectiveValidator.ts
│   │       └── CodeBlockValidator.ts
│   │
│   ├── quiz/                   # 理解度確認問題ドメイン
│   │   ├── QuizDocument.ts
│   │   └── validators/
│   │       ├── FrontmatterValidator.ts
│   │       ├── QuizStructureValidator.ts
│   │       └── SelectOptionsValidator.ts
│   │
│   ├── slides/                 # スライド教材ドメイン
│   │   ├── SlidesDocument.ts
│   │   └── validators/
│   │       ├── MarpFrontmatterValidator.ts
│   │       ├── SlideDelimiterValidator.ts
│   │       └── ContentDensityValidator.ts
│   │
│   ├── practice/               # 演習問題ドメイン
│   │   ├── PracticeDocument.ts
│   │   └── validators/
│   │       ├── FrontmatterValidator.ts
│   │       ├── ProblemStructureValidator.ts
│   │       └── SolutionValidator.ts
│   │
│   ├── requirements/           # 研修要件定義書ドメイン
│   │   ├── RequirementsDocument.ts
│   │   └── validators/
│   │
│   ├── design/                 # 研修設計書ドメイン
│   │   ├── DesignDocument.ts
│   │   └── validators/
│   │
│   ├── flyer/                  # フライヤー要件ドメイン
│   │   ├── FlyerDocument.ts
│   │   └── validators/
│   │
│   └── promotion/              # プロモーションドメイン
│       ├── PromotionDocument.ts
│       └── validators/
│
├── usecase/                    # ユースケース層（サブコマンド毎）
│   ├── base/
│   │   └── LintUseCase.ts      # ユースケース基底インターフェース
│   │
│   ├── LintTextUseCase.ts      # text サブコマンド用
│   ├── LintQuizUseCase.ts      # quiz サブコマンド用
│   ├── LintSlidesUseCase.ts    # slides サブコマンド用
│   ├── LintPracticeUseCase.ts  # practice サブコマンド用
│   ├── LintRequirementsUseCase.ts
│   ├── LintDesignUseCase.ts
│   ├── LintFlyerUseCase.ts
│   └── LintPromotionUseCase.ts
│
├── presentation/               # プレゼンテーション層
│   ├── cli/
│   │   ├── commands/           # サブコマンド定義
│   │   │   ├── textCommand.ts
│   │   │   ├── quizCommand.ts
│   │   │   ├── slidesCommand.ts
│   │   │   ├── practiceCommand.ts
│   │   │   ├── requirementsCommand.ts
│   │   │   ├── designCommand.ts
│   │   │   ├── flyerCommand.ts
│   │   │   └── promotionCommand.ts
│   │   └── formatters/
│   │       ├── ConsoleFormatter.ts
│   │       └── JsonFormatter.ts
│   │
│   └── reporters/
│       └── LintReporter.ts     # 結果出力
│
└── infrastructure/             # インフラ層
    ├── FileReader.ts           # ファイル読み込み
    └── ConfigLoader.ts         # 設定ファイル読み込み（将来拡張）
```

### 主要コンポーネント

#### 1. 共通型定義

```typescript
// src/core/types.ts
import { Root } from 'mdast';

export interface LintResult {
  ruleId: string;
  severity: 'error' | 'warning' | 'info';
  message: string;
  line?: number;
  column?: number;
}

export interface DocumentMetadata {
  filePath: string;
  title?: string;
  frontmatter: Record<string, unknown>;
}

export interface LintContext {
  tree: Root;
  content: string;
  metadata: DocumentMetadata;
}

export interface Validator {
  readonly id: string;
  readonly name: string;
  validate(context: LintContext): LintResult[];
}
```

#### 2. ドメインモデル基底

```typescript
// src/domain/base/DocumentType.ts
import { Validator } from '../../core/types';

export interface DocumentType {
  readonly id: string;
  readonly name: string;
  readonly validators: Validator[];
}
```

#### 3. 具体的なドメインモデル例（テキスト教材）

```typescript
// src/domain/text/TextDocument.ts
import { DocumentType, Validator } from '../base/DocumentType';
import { FrontmatterValidator } from './validators/FrontmatterValidator';
import { TocValidator } from './validators/TocValidator';
import { StructureValidator } from './validators/StructureValidator';
import { StepDirectiveValidator } from './validators/StepDirectiveValidator';
import { CodeBlockValidator } from './validators/CodeBlockValidator';

export class TextDocument implements DocumentType {
  readonly id = 'text';
  readonly name = 'テキスト教材';

  readonly validators: Validator[] = [
    new FrontmatterValidator(),
    new TocValidator(),
    new StructureValidator(),
    new StepDirectiveValidator(),
    new CodeBlockValidator(),
  ];
}
```

#### 4. バリデーター例

```typescript
// src/domain/text/validators/FrontmatterValidator.ts
import { Validator, LintResult, LintContext } from '../../../core/types';
import { visit } from 'unist-util-visit';
import yaml from 'js-yaml';

export class FrontmatterValidator implements Validator {
  readonly id = 'text/frontmatter';
  readonly name = 'Frontmatter検証';

  validate(context: LintContext): LintResult[] {
    const results: LintResult[] = [];
    let foundFrontmatter = false;

    visit(context.tree, 'yaml', (node: any) => {
      foundFrontmatter = true;
      try {
        const fm = yaml.load(node.value) as Record<string, unknown>;

        if (!fm?.title) {
          results.push({
            ruleId: `${this.id}/missing-title`,
            severity: 'error',
            message: 'frontmatterに"title"フィールドが必要です',
            line: node.position?.start.line,
          });
        }

        if (!('draft' in fm)) {
          results.push({
            ruleId: `${this.id}/missing-draft`,
            severity: 'error',
            message: 'frontmatterに"draft"フィールドが必要です',
            line: node.position?.start.line,
          });
        }
      } catch {
        results.push({
          ruleId: `${this.id}/invalid-yaml`,
          severity: 'error',
          message: 'frontmatterのYAML構文が不正です',
        });
      }
    });

    if (!foundFrontmatter) {
      results.push({
        ruleId: `${this.id}/missing`,
        severity: 'error',
        message: 'frontmatterがありません',
      });
    }

    return results;
  }
}
```

#### 5. UseCase基底インターフェース

```typescript
// src/usecase/base/LintUseCase.ts
import { LintResult } from '../../core/types';

export interface LintUseCaseInput {
  filePath: string;
  options?: {
    fix?: boolean;
  };
}

export interface LintUseCaseOutput {
  filePath: string;
  documentType: string;
  results: LintResult[];
  success: boolean;
}

export interface LintUseCase {
  execute(input: LintUseCaseInput): Promise<LintUseCaseOutput>;
}
```

#### 6. 具体的なUseCase例（テキスト教材）

```typescript
// src/usecase/LintTextUseCase.ts
import { LintUseCase, LintUseCaseInput, LintUseCaseOutput } from './base/LintUseCase';
import { TextDocument } from '../domain/text/TextDocument';
import { MarkdownParser } from '../core/parser';
import { FileReader } from '../infrastructure/FileReader';
import { LintContext, LintResult, DocumentMetadata } from '../core/types';
import yaml from 'js-yaml';

export class LintTextUseCase implements LintUseCase {
  private readonly document = new TextDocument();
  private readonly parser = new MarkdownParser();
  private readonly fileReader = new FileReader();

  async execute(input: LintUseCaseInput): Promise<LintUseCaseOutput> {
    // 1. ファイル読み込み
    const content = await this.fileReader.read(input.filePath);

    // 2. Markdownパース
    const tree = this.parser.parse(content);

    // 3. メタデータ抽出
    const metadata = this.extractMetadata(input.filePath, tree);

    // 4. コンテキスト作成
    const context: LintContext = { tree, content, metadata };

    // 5. バリデーション実行
    const results: LintResult[] = [];
    for (const validator of this.document.validators) {
      const validatorResults = validator.validate(context);
      results.push(...validatorResults);
    }

    // 6. 結果返却
    return {
      filePath: input.filePath,
      documentType: this.document.name,
      results,
      success: results.filter(r => r.severity === 'error').length === 0,
    };
  }

  private extractMetadata(filePath: string, tree: any): DocumentMetadata {
    let frontmatter: Record<string, unknown> = {};

    for (const node of tree.children) {
      if (node.type === 'yaml') {
        try {
          frontmatter = yaml.load(node.value) as Record<string, unknown>;
        } catch {
          // ignore
        }
        break;
      }
    }

    return {
      filePath,
      title: frontmatter.title as string | undefined,
      frontmatter,
    };
  }
}
```

#### 7. サブコマンド定義

```typescript
// src/presentation/cli/commands/textCommand.ts
import { Command } from 'commander';
import { LintTextUseCase } from '../../../usecase/LintTextUseCase';
import { ConsoleFormatter } from '../formatters/ConsoleFormatter';
import { JsonFormatter } from '../formatters/JsonFormatter';
import fs from 'fs';
import path from 'path';

export function createTextCommand(): Command {
  const command = new Command('text');

  command
    .description('テキスト教材をチェック')
    .argument('<file>', 'チェック対象のMarkdownファイル')
    .option('--format <format>', '出力形式 (console|json)', 'console')
    .option('--fix', '自動修正（将来拡張）', false)
    .action(async (file: string, options) => {
      const absolutePath = path.resolve(process.cwd(), file);

      if (!fs.existsSync(absolutePath)) {
        console.error(`エラー: ファイルが見つかりません: ${file}`);
        process.exit(1);
      }

      const useCase = new LintTextUseCase();
      const result = await useCase.execute({
        filePath: absolutePath,
        options: { fix: options.fix },
      });

      // フォーマッター選択
      const formatter = options.format === 'json'
        ? new JsonFormatter()
        : new ConsoleFormatter();

      formatter.format(result);

      if (!result.success) {
        process.exit(1);
      }
    });

  return command;
}
```

#### 8. CLIエントリーポイント

```typescript
// src/index.ts
import { Command } from 'commander';
import { createTextCommand } from './presentation/cli/commands/textCommand';
import { createQuizCommand } from './presentation/cli/commands/quizCommand';
import { createSlidesCommand } from './presentation/cli/commands/slidesCommand';
import { createPracticeCommand } from './presentation/cli/commands/practiceCommand';
import { createRequirementsCommand } from './presentation/cli/commands/requirementsCommand';
import { createDesignCommand } from './presentation/cli/commands/designCommand';
import { createFlyerCommand } from './presentation/cli/commands/flyerCommand';
import { createPromotionCommand } from './presentation/cli/commands/promotionCommand';

const program = new Command();

program
  .name('mdlint')
  .description('Markdown文書をスタイルガイドラインに沿ってチェック')
  .version('2.0.0');

// サブコマンド登録
program.addCommand(createTextCommand());
program.addCommand(createQuizCommand());
program.addCommand(createSlidesCommand());
program.addCommand(createPracticeCommand());
program.addCommand(createRequirementsCommand());
program.addCommand(createDesignCommand());
program.addCommand(createFlyerCommand());
program.addCommand(createPromotionCommand());

program.parse();
```

#### 9. フォーマッター

```typescript
// src/presentation/cli/formatters/ConsoleFormatter.ts
import chalk from 'chalk';
import { LintUseCaseOutput } from '../../../usecase/base/LintUseCase';

export class ConsoleFormatter {
  format(output: LintUseCaseOutput): void {
    console.log(chalk.blue(`文書タイプ: ${output.documentType}`));
    console.log(chalk.gray(`ファイル: ${output.filePath}`));
    console.log('');

    if (output.results.length === 0) {
      console.log(chalk.green('問題は見つかりませんでした'));
      return;
    }

    console.log(chalk.red(`${output.results.length}件の問題が見つかりました:\n`));

    for (const result of output.results) {
      const icon = result.severity === 'error' ? chalk.red('✖')
                 : result.severity === 'warning' ? chalk.yellow('⚠')
                 : chalk.blue('ℹ');
      const location = result.line ? `:${result.line}` : '';
      console.log(`${icon} [${result.ruleId}]${location}: ${result.message}`);
    }
  }
}
```

```typescript
// src/presentation/cli/formatters/JsonFormatter.ts
import { LintUseCaseOutput } from '../../../usecase/base/LintUseCase';

export class JsonFormatter {
  format(output: LintUseCaseOutput): void {
    console.log(JSON.stringify(output, null, 2));
  }
}
```

## データフロー

```
┌─────────────────────────────────────────────────────────────────────┐
│                          CLI (index.ts)                             │
│  mdlint text <file>                                           │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    Presentation Layer                               │
│  textCommand.ts → LintTextUseCase を呼び出し                         │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                       UseCase Layer                                 │
│  LintTextUseCase.execute()                                          │
│  1. FileReader でファイル読み込み                                     │
│  2. MarkdownParser でパース                                          │
│  3. TextDocument.validators でバリデーション                          │
│  4. LintUseCaseOutput を返却                                         │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                       Domain Layer                                  │
│  TextDocument                                                       │
│  └── validators: [                                                  │
│        FrontmatterValidator,                                        │
│        TocValidator,                                                │
│        StructureValidator,                                          │
│        StepDirectiveValidator,                                      │
│        CodeBlockValidator                                           │
│      ]                                                              │
└───────────────────────────────┬─────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    Presentation Layer                               │
│  ConsoleFormatter / JsonFormatter で結果出力                         │
└─────────────────────────────────────────────────────────────────────┘
```

## 拡張性のポイント

### 1. 新しい文書タイプの追加

```typescript
// 1. ドメインフォルダを作成: src/domain/newtype/
// 2. ドメインモデルを実装
export class NewTypeDocument implements DocumentType {
  readonly id = 'newtype';
  readonly name = '新しい文書タイプ';
  readonly validators = [ /* ... */ ];
}

// 3. ユースケースを作成: src/usecase/LintNewTypeUseCase.ts
export class LintNewTypeUseCase implements LintUseCase {
  private readonly document = new NewTypeDocument();
  // ...
}

// 4. サブコマンドを作成: src/presentation/cli/commands/newtypeCommand.ts
// 5. index.ts にサブコマンドを登録
program.addCommand(createNewTypeCommand());
```

### 2. 新しいバリデーターの追加

```typescript
// 既存ドメインに新しいバリデーターを追加
export class NewValidator implements Validator {
  readonly id = 'text/new-rule';
  readonly name = '新しいルール';

  validate(context: LintContext): LintResult[] {
    // 検証ロジック
  }
}

// TextDocument.ts の validators 配列に追加
```

### 3. 共有バリデーターの利用

```typescript
// 複数のドメインで共通のバリデーターを使用
import { CodeBlockLanguageValidator } from '../shared/CodeBlockLanguageValidator';

export class TextDocument implements DocumentType {
  readonly validators = [
    new FrontmatterValidator(),          // テキスト専用
    new CodeBlockLanguageValidator(),    // 共有
  ];
}
```

### 4. 新しい出力形式の追加

```typescript
// src/presentation/cli/formatters/SarifFormatter.ts
export class SarifFormatter {
  format(output: LintUseCaseOutput): void {
    // SARIF形式で出力（GitHub Actions連携用）
  }
}
```

## 設定ファイル対応（将来拡張）

```yaml
# .mdlint.yml
rules:
  text/frontmatter:
    severity: error
  text/structure:
    severity: warning
    options:
      requireSummary: true

extends:
  - ./custom-rules.js  # カスタムルールの追加
```

## マイグレーション計画

### Phase 1: コア構造の構築
1. `src/core/` を作成（types.ts, parser.ts）
2. `src/usecase/base/` を作成（LintUseCase.ts）
3. `src/presentation/cli/` を作成（formatters/）

### Phase 2: テキスト教材ドメインの移行
1. 既存の `src/rules/` を `src/domain/text/validators/` に移行
2. TextDocument を実装
3. LintTextUseCase を実装
4. textCommand を実装

### Phase 3: 追加ドメインの実装
1. quiz, slides, practice ドメインを順次実装
2. 各ドメインのUseCaseとコマンドを実装
3. 共有バリデーターを抽出

### Phase 4: 残りのドメイン
1. requirements, design, flyer, promotion ドメインを実装
2. 設定ファイルサポートを追加

## まとめ

本アーキテクチャにより:

- **サブコマンドで明確な文書タイプ指定**: `mdlint text <file>` のように使用
- **UseCaseパターンで責務分離**: サブコマンドごとにUseCaseが対応し、ロジックが明確
- **文書タイプの追加が容易**: Domain + UseCase + Command の3点セットを追加
- **ルールの追加・変更が独立**: 各バリデーターは独立しており、他に影響しない
- **共通ロジックの再利用**: shared バリデーターで重複を排除
- **出力形式の柔軟性**: Formatterパターンでconsole/JSON/SARIF等に対応可能
