サービス関数内部のビジネスフローを宣言的ブロック(sequence)に分解する言語非依存の概念。

問題

既存のSSOTは関数の外部までしかカバーしない:

既存SSOTカバー範囲関数内部?
OpenAPIAPIパス、パラメータ、レスポンススキーマX
SQL DDLテーブル構造、インデックス、制約X

関数内部 – 「取得 -> 検証 -> 作成 -> 応答」というビジネスフローを宣言する場所がない。実装コードを読まなければ分からない。この空白をsequenceが埋める。

SSOT 階層構造

SSaCは既存のSSOTの上に積み重なる第三の階層である。

OpenAPI       → API境界 (パス、パラメータ、レスポンス)
SQL DDL       → データ境界 (テーブル、制約、インデックス)
SSaC          → 関数内部 (ビジネスフロー、sequence宣言)
─────────────────────────────────────────────────
実装コード      → コードジェンが生成 (エラーハンドリング、ボイラープレート)

上記三つの階層が宣言を担当し、実装コードは宣言から派生する。人間が書くのは宣言だけである。

コアコンセプト

sequence

関数内部の実行ブロックをタイプ化した宣言単位。

what(何をするか)だけを宣言し、how(どうやるか)はコードジェンが埋める。

10個の固定タイプ

タイプ役割
authorize権限検証アクション/リソース/IDベースのポリシー問い合わせ
getリソース取得Model.FindByID(id) -> result
guard nil結果がnullなら終了存在しなければエラーで終了
guard exists結果が存在すれば終了既に存在すればエラーで終了
postリソース作成Model.Create(fields…) -> result
putリソース更新Model.Update(id, fields…)
deleteリソース削除Model.Delete(id)
passwordパスワード照合ハッシュ比較後、失敗時に終了
call外部呼び出しCRUDで表現できないリソース操作、失敗時に終了
responseレスポンス返却json, view, redirect

タイプが閉じているため、タイプごとのテンプレートマッチングによるコード生成が可能である。LLMなしでシンボリックコードジェンが成立する根拠。

modelの範囲

modelはDBテーブルに限定されない。CRUDで扱えるすべてのリソースがmodelである。DB問い合わせ、ファイル存在確認、外部API呼び出しのすべてをget/post/put/deleteで同様に表現する。

構文例 (Go PoC)

言語非依存の概念だが、リファレンス実装としてGoコメント構文を使用する。AIがGo ASTを既に理解しているため、追加の学習コストがない。

シンプル (作成)

// @sequence get
// @model Project.FindByID
// @param ProjectID request
// @result project Project

// @sequence guard nil project
// @message "프로젝트가 존재하지 않습니다"

// @sequence post
// @model Session.Create
// @param ProjectID request
// @param Command   request
// @result session Session

// @sequence response json
// @var session
func CreateSession(w http.ResponseWriter, r *http.Request) {}

複合 (削除 + 権限 + 検証 + 外部呼び出し)

// @sequence authorize
// @action delete
// @resource project
// @id ProjectID

// @sequence get
// @model Project.FindByID
// @param ProjectID request
// @result project Project

// @sequence guard nil project
// @message "프로젝트가 존재하지 않습니다"

// @sequence get
// @model Session.CountByProjectID
// @param ProjectID request
// @result sessionCount int

// @sequence guard exists sessionCount
// @message "하위 세션이 존재하여 삭제할 수 없습니다"

// @sequence call
// @component notification
// @param project.OwnerEmail
// @param "프로젝트가 삭제됩니다"

// @sequence call
// @func cleanupProjectResources
// @param project
// @result cleaned bool

// @sequence delete
// @model Project.Delete
// @param ProjectID request

// @sequence response json
func DeleteProject(w http.ResponseWriter, r *http.Request) {}

コードジェン結果

上記のsequence宣言から派生する実装:

func CreateSession(w http.ResponseWriter, r *http.Request) {
    // get
    project, err := projectModel.FindByID(projectID)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // guard nil
    if project == nil {
        http.Error(w, "프로젝트가 존재하지 않습니다", http.StatusNotFound)
        return
    }

    // post
    session, err := sessionModel.Create(projectID, command)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // response json
    json.NewEncoder(w).Encode(map[string]interface{}{
        "session": session,
    })
}

トークンコスト比較

sequence宣言実装コード
行数10~15行30~100行
表現内容what (何をするか)how (どうやるか)
エラーハンドリングなし (コードジェンが生成)すべて含む
ライブラリ依存なしimport、初期化、呼び出し
変更タイミングビジネスフロー変更時コードジェン再実行で自動反映

人間がメンテナンスするのはsequence宣言だけである。実装コードは宣言が変われば、コードジェンを再実行して上書きする。

設計原則

early return – すべてのタイプの共通契約

sequenceが線形列挙で成立する前提。responseを除くすべてのsequenceは、失敗時に即座に脱出する。callも例外ではない。分岐なしで上から下に読めば終わりだ。

タイプ失敗条件
authorize権限なし -> return
get取得エラー -> return
guard nilnull -> return
guard exists存在 -> return
post/put/deleteDBエラー -> return
password不一致 -> return
call失敗 -> return
response最後 (返却)

callがguardと同一の契約に従うため、callの後に別途guardを付ける必要がない。コードジェンがすべてのcallにエラーチェック + early returnを自動生成する。

@transaction

関数レベルのメタデータ。sequenceタイプ(10個)ではなく、関数全体のトランザクションラッピングの有無を宣言する。

// @transaction
// @sequence get
// @model Account.FindByID
// @param AccountID request
// @result account Account
// ...

early return構造と結合すると、トランザクション境界は自然に定まる:

@transaction 宣言あり
  → guard 失敗 → rollback + return
  → 最後のsequence成功 → commit
  → エラー発生 → rollback + return

分岐がないため、トランザクション境界も「関数全体」で単一である。begin/endの位置を指定する必要がなく、@transactionの有無だけでコードジェンがラッピングの有無を決定する。トランザクションが2つ必要なら、それは責務が2つあるということだ。関数を分離せよ。

call – modelの特殊形

callはmodelと同一の本質を持つ。modelがCRUD(get/post/put/delete)で表現されるリソース操作であるなら、callはCRUDで表現できないリソース操作である。どちらも外部依存のある作業を呼び出し、失敗すればreturnする。

リソース操作
  +-- model (CRUD)  → get/post/put/delete
  +-- call (非CRUD) → component または func
  • @component – 登録制。繰り返しパターンが3回以上現れたら昇格する。
  • @func – 固有ロジック。人間またはAIが直接実装する。

分岐後の合流、ループ内の条件付き処理などの複雑なロジックは、sequenceを拡張せずcallに委任する。委任するが、契約は同一だ – 失敗時に脱出。

@message

すべてのsequenceタイプにオプションで付与できるメタデータ。失敗時にユーザーに伝えるメッセージを指定する。

// @sequence guard nil project
// @message "프로젝트가 존재하지 않습니다"

// @sequence post
// @model Session.Create
// @param ProjectID request
// @param Command   request
// @result session Session
// @message "세션 생성에 실패했습니다"

@messageを省略すると、コードジェンがタイプ+モデル名からデフォルトメッセージを自動生成する。カスタムメッセージが必要な場合のみ宣言すればよい。

純粋関数はサービス層に存在しない

sequenceに宣言されるすべてのブロックは外部依存のある作業である。フォーマット変換、値の計算などの純粋な機能は、サービス層ではなくモデル内部または呼び出される関数の内部で処理する。サービス関数はオーケストレーション(orchestration)のみを行い、純粋な計算は呼び出し先に属する。

型参照

SSaCは独自の型を定義しない。既存のSSOTから産出された型を参照する。

ソース
DBスキーマ (SQL DDL)Project, Session
API仕様 (OpenAPI)CreateSessionRequest
インターフェース定義FileSystem, Cache

命名規則

区分規則
PascalCase、SSOTから取得Project, Session
変数camelCase、@resultで宣言project, sessionCount
requestフィールドPascalCase、リクエストから抽出ProjectID, Command
予約変数camelCase、フレームワーク提供currentUser, config

型と変数の大文字小文字が異なるため、宣言だけで区別できる。projectは変数、Projectは型である。

言語拡張

SSaCは言語非依存の概念である。GoはリファレンスのPoC実装であり、同一のsequence宣言から他の言語のコードを生成できる。

sequence宣言 (共通)
  |
  +-- Go コードジェン
  +-- Python コードジェン
  +-- TypeScript コードジェン
  +-- ...

スペック(型定義、構文、検証ルール)とコードジェンは分離される。

GoリファレンスのPoC実装はGitHubで確認できる。