Upgrade to Pro — share decks privately, control downloads, hide ads and more …

増え続けるプロダクトの品質をどう担保するか。ProductSpecとTypeScriptによる...

Avatar for joe_re joe_re
October 31, 2026

 増え続けるプロダクトの品質をどう担保するか。ProductSpecとTypeScriptによる取り組み

#TSKaigiSendai

PeopleXは、組織全体でサービスのコア技術にTypeScriptを採用しています。PeopleX AI面接のリリース以降、AIロープレ、AI面談、AIセールスとTypeScript製のプロダクトは次々に増えており、今後も拡大していく予定です。

プロダクトが増えるほど、可観測性、エラーハンドリング、権限制御といった「どのプロダクトでも守るべき品質」はばらつきやすくなります。私たちはこの課題に向き合うため、全プロダクトに共通する仕様「ProductSpec」を定義する取り組みを進めています。

本セッションでは、試行錯誤の中で見えてきた仕様の定義の仕方と、定義した仕様をTypeScriptのコードとテストで守り続けるためのプラクティスをお話しします。特に、コーディングエージェントが実装できる粒度で仕様を書く方法と、仕様からE2Eテストのケースを機械的に導出し、仕様への準拠を検証し続ける仕組みを紹介します。

Avatar for joe_re

joe_re

October 31, 2026

More Decks by joe_re

Other Decks in Technology

Transcript

  1. TSKaigi Sendai 2026 増え続けるプロダクトの品質を どう担保するか ProductSpec と TypeScript による取り組み 野口

    将人 / @joe_re 株式会社 PeopleX 専門役員 / AI 製品技術責任者 #TSKaigiSendai
  2. 自己紹介 野口 将人(のぐち まさと) X: @joe_re / GitHub: @joe-re 株式会社

    PeopleX 専門役員 / AI 製品技術責任者 GraphQL Tokyo オーガナイザー #TSKaigiSendai 2 / 26
  3. PEOPLEX 「働く」を、AI で再定義する会社 2024.04 78.2 億円 90 名+ 3,000 社+

    創業 累計資金調達 本体メンバー グループ支援実績 異常速度で社会実装する 2 年で 90 名+、10 事業、毎年 3 つ以上の新プロダクト。2 年で 3 社を M&A 小さな成功ではなく、歴史に残る成功を目指す #TSKaigiSendai 4 / 26
  4. PEOPLEX · PRODUCTS 製品・サービス AI 面接 面接支援 AI AI セールス

    採用支援 · 対話型 AI 面接 採用支援 · 人の面接を記録・解析 営業支援 · 商談に入る AI エージェント AI 面談 AI ロープレ AI セキュリティガバナンス 活躍支援 · 対話型 AI 面談 活躍支援 · 対話型 AI ロープレ セキュリティ対策支援 · 訓練と育成 #TSKaigiSendai 5 / 26
  5. 増え続けるプロダクト 11+ 10 AI アセスメント & ラ ーニング タレントインテリジェ ンス

    タレントインテリジェ ンス AI セキュリティガバ ナンス AI セキュリティガバ ナンス AI セールス AI セールス 6 面接支援 AI 面接支援 AI AgenticHR 基盤 AgenticHR 基盤 AgenticHR 基盤 4 AI Copilot 人材紹介 AI Copilot 人材紹介 AI Copilot 人材紹介 AI ロープレ AI ロープレ AI ロープレ AI ロープレ 2 AI 面談 AI 面談 AI 面談 AI 面談 1 AI 面接 AI 面接 AI 面接 AI 面接 AI 面接 PeopleWork PeopleWork PeopleWork PeopleWork PeopleWork PeopleWork 2024.04 創業 2025.04 2025.12 〜 2026.01 2026.02 〜 03 2026.10 2026.11 〜 #TSKaigiSendai 6 / 26
  6. 今日の主張 仕様を唯一の正解として書き、 人も LLM も期待値を考えなくて済む形で 検証し続ける 土台の品質 機能の品質 ProductSpec 仕様駆動

    E2E 可観測性・セキュリティ・インフラ。全プロダクト共通の 仕様 機能ごとの仕様 YAML から E2E ケースを機械的に導出 8 / 26
  7. openai/symphony の SPEC.md SPEC.md から借りたもの SYMPHONY とは issue トラッカーから仕事を読み、issue ごとに隔離した

    作業環境でコーディングエージェントを走らせるサービ スの仕様書。 OpenAI が「コードではなく仕様」を本体として公開し た。README の案内は 1 行、「この仕様に従って Symphony を実装して」とエージェントに SPEC.md を 渡す。 参考実装は Elixir。実装言語はエージェントに任せる      RFC 2119 の規範言語。MUST / SHOULD / MAY で曖 昧さを消す 言語非依存の参照アルゴリズム。擬似コードで振る 舞いを固定する Test and Validation Matrix。Core / Extension / Real Integration の 3 プロファイル Implementation Checklist = Definition of Done。満たせば完了 曖昧性のない仕様を定義することで、コーディングエージェントはゴールに向かうことができる。 仕様駆動開発の考え方をプロダクト仕様としても適用する #TSKaigiSendai 11 / 26
  8. PART 1 · PRODUCTSPEC ProductSpec: プロダクト横断の土台の仕様 1 つのリポジトリに、ドメインごとの SPEC.md を置く

    Observability Backend Analytics 読み手は人ではなく、コーディングエージェント。仕様 から直接実装できる粒度で書く Data Access CI Security Access Gate DNS WAF Vuln Scan 例: 新規プロダクトの立ち上げ #TSKaigiSendai 12 / 26
  9. SPEC.md の構成 0 規範言語 MUST / SHOULD / MAY は

    RFC 2119。Implementation-defined は「決めてよいが文書化は 必須」 1 Problem Statement 運用者が答えられるべき問いを列挙する 4 ドメインモデル フィールド単位。型・必須性・誰が発行するかまで 5〜 規範要件 禁止には「そうしないと何が起きるか」を対で書く n−2 擬似コード 言語非依存。規範例は TS / NestJS / Next.js n−1, n 検証マトリクス + DoD #TSKaigiSendai 完了条件。エージェントに SPEC.md を渡し、ここを満たすことを完了とする 13 / 26
  10. 例 · APPLICATION BACKEND SPEC §4.2 GraphQL のエラーメッセージがスキーマを漏らす POST /graphql

    # 存在しないフィールド emial を問い合わせる query { me { イントロスペクションを無効にしていても、名 前を総当たりで送れば候補からスキーマを復元 できる emial } } 200 OK 型名 User と正しいフィールド名 email が返っ てくる # Apollo Server の既定のレスポンス { 実際に、復元したフィールド名を使った悪意あ るリクエストを観測した "errors": [{ "message": "Cannot query field \"emial\" on type \"User\". Did you mean \"email\"?", "extensions": { "code": "GRAPHQL_VALIDATION_FAILED" } }] } #TSKaigiSendai 14 / 26
  11. 例 · 規範要件と擬似コード MUST を 3 つに絞り、言語非依存の擬似コードを添える MUS T 全エンドポイント・全環境に共通の

    formatError を適用する function formatError(formatted, error): original = unwrapResolverError(error) code = mapAppException(original) MU ST 検証・構文・入力エラーの message を固定文 言に置き換える ?? formatted.extensions.code ?? "INTERNAL_SERVER_ERROR" captureToSentry(original, level) return { MU ST message: clientMessage(code), extensions.code は必ず返す。クライアントはコ ードで種別を判別する locations, path, extensions: { code, ...appDefined } } #TSKaigiSendai 15 / 26
  12. 例 · 検証マトリクス 検証マトリクスが完了条件  { __typenam } に対して「クエリが不正です」と GRAPHQL_VALIDATION_FAILED

    が返り、候補が含まれない  variables の不明フィールドに対して「入力内容が不正です」と BAD_USER_INPUT が返り、候補も入力値も含ま れない  NODE_ENV が production 以外でもマスキングされる。レスポンスは完全一致で検証する エージェントに SPEC.md を渡し、マトリクスと DoD を満たすことを完了条件にする 仕様と実装が乖離したら、仕様の改訂か実装の修正かを判断し、仕様側にも履歴を残す #TSKaigiSendai 16 / 26
  13. 全体の流れ 前提 機能実装の前 機能実装の後 1 2 3 4 5 仕様駆動開発を前

    提にする PRD / SPEC を最 初に作る テストケースを導 出する Playwright を整 備する E2E テストを実装 する 仕様が先、実装が後。 仕様が唯一の正解 人が検査する仕様 書。以降はすべてこ こから作る PRD / SPEC から YAML 定義へ。この時 点で実装は書かない PRD / SPEC の製品 仕様を Playwright の コードとして書いて おく YAML 定義に照らして 実装。乖離があれば 内容を確認し、テス トケースを満たすよ うに実装する テストケースは仕様から先に決まっている。実装がそれに合わせにいく #TSKaigiSendai 19 / 26
  14. 仕様 YAML: 実装を書かず、振る舞いだけを書く permissions: meeting.delete: grants: { ADMIN: allow, MEMBER:

    allow, VIEWER: deny } 書かないもの ボタン名・セレクタ・URL・GraphQL 操作名・エラ ー型名。書くと実装を変えただけで仕様が変わる on_deny: 行メニューに削除を出さず、直接実行しても拒否 verification: [api, browser] requirements: 要件は EARS 型 ubiquitous / event / state / unwanted / optional。 1 要件 1 振る舞い - id: R-001 type: unwanted text: 閲覧者が商談を削除しようとした場合、 システムは拒否し、削除状態を変えてはならない 出典に無い振る舞いは要件にしない 必ず仕様書に書いてから要件にする covers: [MT-AC-33] refs: [meeting.delete] verification: [api] context: { role: VIEWER } #TSKaigiSendai 21 / 26
  15. TYPESCRIPT の実装紹介 · 1 / 3 E2E の判定を never で守る

    // _classify.ts: サーバの応答(結果型の union)→ 判定の種類 E2E の中の位置 const classifySaveResult = (result: SaveResult): ContactOutcome => { ログイン済みブラウザの fetch の検査 switch (result.__typename) { case 'ContactCreated': case 'ContactUpdated': return { outcome: 'success' }; case 'ContactSaveUnavailable': return { outcome: 'access_denied' }; case 'ContactDuplicateEmail': return { outcome: 'rejected', reason: 'duplicate_email' }; default: { const unknownResult: never = result; return broken(`知らない結果型: ${JSON.stringify(unknownResult)}`); } } スキーマに結果型が増えると、tsc が落ちる。 codegen の union が広がり、never への代入が 通らなくなる。 サーバの失敗を「拒否」として PASS させ ない。 実行時も default は broken に設定。 broken はそもそもテストが壊れているケースな ので拒否にするのではなく、検知できるように しておく。 分類表を更新し忘れたまま、テストが嘘をつく道を型で 塞ぐ }; #TSKaigiSendai 23 / 26
  16. TYPESCRIPT の実装紹介 · 2 / 3 判定の種類を discriminated union で表現

    // src/e2e/outcome.ts: E2E の判定はこの 6 つだけ テストでの使い方 type Outcome<Reason extends string = string> = expect(outcome).toEqual({ outcome: 'rejected', reason: 'duplicate_email' }) | { outcome: 'success' } | { outcome: 'access_denied' } | { outcome: 'rejected'; reason: Reason } | { outcome: 'invalid_input'; fields: RejectedField[] } | { outcome: 'not_found' } | { outcome: 'broken'; detail: string }; // 機能ごとに、拒否の理由を文字列リテラルで定義 type ContactOutcome = Outcome<'duplicate_email' | 'invalid_organization'>; // broken は Outcome<never>。共変なので、どの機能の Outcome にも入る const broken = (detail: string): Outcome<never> => ({ outcome: 'broken', detail }); #TSKaigiSendai 期待値は型で表現する。 期待値の間違いや、その機能に無い理由は tsc が落とす。 「壊れている」状態は分けて定義する。 サーバの失敗やクエリの誤りを、拒否とも成功 とも混ぜない。 never は底の型なのでどのケースでも共通の broken が使える。 ケース表の期待値・分類表・判定のまとめが、同じ 6 語 になっている 24 / 26
  17. TYPESCRIPT の実装紹介 · 3 / 3 ページオブジェクトが画面仕様を表し、ケース実装と分ける 層 1 ·

    ページオブジェクト = 画面仕様 層 2 · ケース実装 src/pages。画面仕様(Feature Spec)を表現する e2e/<機能>。表駆動のスクリプトと、LLM が書く操作手順 // 画面の状態を、画面仕様で決めた値で返す type ControlState = 'enabled' | 'disabled' | 'hidden'; readCreateButtonState(): Promise<ControlState> readNotice(): Promise<ManualAccessNotice> // 操作もメソッドで。Locator と非同期処理はこの層に閉じる openRowMenu(name): Promise<RowMenu> deleteContact(name): Promise<ContactDeleteOutcome> 画面仕様を変えるときはここを先に直す。 ルール関数も同じ型で「あるべき状態」を返す  // 層 1 が定義した型と操作だけで書く。Locator は出てこない const list = new ContactListPage(page); await list.goto({ q: email }); // 期待はルール関数とケース表から expect(await list.readNotice()) .toBe(expectedManualAccess(c.context)); expect(await list.readCreateButtonState()).toBe('hidden'); 期待は層 1 が提供する型の中でしか書けない。 「表示されていない」も hidden という値で、toBeVisible は書かない 画面の作りが変わっても直すのは層 1 だけ。LLM は層 2 しか書かないので、定義された型定義の中で記述する。 #TSKaigiSendai 25 / 26
  18. まとめ 仕様を唯一の正解として書き、 人も LLM も期待値を考えなくて済む形で検証し続ける ProductSpec 仕様駆動 E2E エージェントが実装できる粒度で土台の仕様を書き、 検証マトリクスと

    DoD を完了条件にする 仕様 YAML から機械的にケースを導出し、LLM には画 面の操作手順だけ書かせる PeopleX はエンジニアを募集しています! ご興味ある方はぜひお話ししましょう! @joe_re · #TSKaigiSendai 26 / 26