サービス関数内部のビジネスフローを宣言的ブロック(sequence)に分解する言語非依存の概念。
問題
既存のSSOTは関数の外部までしかカバーしない:
| 既存SSOT | カバー範囲 | 関数内部? |
|---|---|---|
| OpenAPI | APIパス、パラメータ、レスポンススキーマ | 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 nil | null -> return |
| guard exists | 存在 -> return |
| post/put/delete | DBエラー -> 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で確認できる。