Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
ts-morph でプロジェクト固有のアーキテクチャガードレールを作る
Search
PKSHA Technology(パークシャテクノロジー)
PRO
May 22, 2026
710
4
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
ts-morph でプロジェクト固有のアーキテクチャガードレールを作る
PKSHA Technology(パークシャテクノロジー)
PRO
May 22, 2026
More Decks by PKSHA Technology(パークシャテクノロジー)
See All by PKSHA Technology(パークシャテクノロジー)
過去のレビュー知見をSkillsで資産化した話
pkshadeck
PRO
1
2.7k
Databricksを用いたセキュアなデータ基盤構築とAIプロダクトへの応用.pdf
pkshadeck
PRO
0
480
人とAIのコミュニケーション方法の違い
pkshadeck
PRO
1
890
ドキュメントからはじめる未来のソフトウェア
pkshadeck
PRO
5
3.5k
「協働」で拓くAI開発の最前線 VoC活⽤と⽣成AIエージェント開発の舞台裏
pkshadeck
PRO
0
840
自動システムテストのための テスト再設計と人材育成
pkshadeck
PRO
0
2.3k
脱 argparse! Typer + Rich を使った型安全でモダンな CLI 開発 / Modern, Type-Safe CLI Development with Typer and Rich — Moving Beyond argparse
pkshadeck
PRO
1
680
AIエージェント縛りで社内ハッカソンを開催した話(20250723-名古屋LLM_MeetUp#7_LT)
pkshadeck
PRO
3
1.7k
AIエージェント開発を加速させるLLM実験基盤
pkshadeck
PRO
3
1.3k
Featured
See All Featured
First, design no harm
axbom
PRO
2
1.2k
Claude Code どこまでも/ Claude Code Everywhere
nwiizo
65
56k
Building Adaptive Systems
keathley
44
3.1k
From π to Pie charts
rasagy
0
210
The untapped power of vector embeddings
frankvandijk
2
1.8k
brightonSEO & MeasureFest 2025 - Christian Goodrich - Winning strategies for Black Friday CRO & PPC
cargoodrich
3
730
Exploring the relationship between traditional SERPs and Gen AI search
raygrieselhuber
PRO
2
4k
End of SEO as We Know It (SMX Advanced Version)
ipullrank
3
4.2k
Reflections from 52 weeks, 52 projects
jeffersonlam
356
21k
Information Architects: The Missing Link in Design Systems
soysaucechin
0
970
Statistics for Hackers
jakevdp
799
230k
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
620
Transcript
ts-morph で プロジェクト固有の アーキテクチャガードレールを作る 須藤 路真 ( @michimasa_suto ) 株式会社
PKSHA Technology 01 / 28
こういう経験、ありませんか? AI が書いたコード ガイドライン(CLAUDE.mdなど) にルールを 書いても、意図に沿わないコードが返ってくる プロンプトで縛っても、 ⻑いコンテキストの中で次第に守られなくなる ⼈間が書いたコード Pull
Request で、同じアーキテクチャ違反 が繰り返し指摘される レビュアーは毎回同じ指摘で疲弊 レビューされる側も申し訳ない気持ちに... 02 / 28
ドキュメントもプロンプトも、結局 “お願い”でしかない お願いベースのルールは、コードや組織の成⻑とともに形骸化する 03 / 28
「お願い」ではなく、⼈も AI も⾃然と準拠するための「ガードレール」を引こう お願いベースのルールは、コードや組織の成⻑とともに形骸化する 04 / 28 ドキュメントもプロンプトも、結局 “お願い”でしかない
フロントエンド構成 TECH STACK ARCHITECTURE ⼦孫‧兄弟への参照は可能 それ以外(親‧いとこ‧おじ/おば...)へ の参照は禁⽌ features/ ├── TaskList/
│ ├── index.tsx │ └── features/ │ └── TaskExport/ │ └── index.tsx └── TaskDetail/ └── index.tsx featuresは再帰的にネスト可能 再帰的な features 構成 05 / 28 ⾔語‧ライブラリ AI ツール CONTEXT
再帰的な features 構成 の詳細は Zenn の記事を↓ 🔒 zenn.dev/pksha/articles/recursive-features-directory-structure 06 /
28 CONTEXT
TODAY'S TALK このルールを静的解析で検知できるようにしたい features/ ├── TaskList/ │ ├── index.tsx │
└── features/ │ └── TaskExport/ │ └── index.tsx └── TaskDetail/ └── index.tsx 07 / 28
TOOLING 技術選定 08 / 28 モジュール間の import 制約を 宣⾔的に書ける 優れた
Linter がある dependency-cruiser ESLint Biome ディレクトリの親⼦関係 でルール判定したい今回の構成において、 上記のツールでは⼗分に表現しきれなかった Oxlint
TOOLING 技術選定 モジュール間の import 制約を 宣⾔的に書ける 優れた Linter がある ディレクトリの親⼦関係
でルール判定したい今回の構成において、 上記のツールでは⼗分に表現しきれなかった 09 / 28 ts-morph を採⽤ dependency-cruiser ESLint Biome Oxlint
TOOLING ts-morph とは ‧AST(抽象構⽂⽊) を使って、コードを 構造として読み取れる ‧TypeScript Compiler API を扱いやすく包んだラッパー
‧Lint ツールよりも ⾃由度の⾼いコード解析‧変換 ができる 10 / 28
TOOLING ts-morph とは import { Project } from "ts-morph"; const
project = new Project({ tsConfigFilePath: "./tsconfig.json" }); const sf = project.getSourceFile("src/Button.tsx")!; sf.getImportDeclarations(); // import 一覧を取得 sf.getExportedDeclarations(); // export 一覧を取得 sf.getFunctions(); // 関数の一覧を取得 11 / 28
GOAL これから作るもの ─ 違反 を CI で検知する features/ ├── TaskList/
│ ├── index.tsx │ └── features/ │ └── TaskExport/ │ └── index.tsx └── TaskDetail/ └── index.tsx 12 / 28
APPROACH どう検知する? 1 ファイルパスと import パス のペアを取得する FILE features/TaskDetail/index.tsx IMPORT
import { xxx } from "../../features/TaskList/features/TaskExport/index.tsx" ペアが違反していないかを「ホワイトリスト」で判定する isAllowedReference ( "features/TaskDetail/index.tsx" , "../../features/TaskList/features/TaskExport/index.tsx" ) 2 13 / 28
APPROACH どう検知する? ファイルパスと import パス のペアを取得する FILE features/TaskDetail/index.tsx IMPORT import
{ xxx } from "../../features/TaskList/features/TaskExport/index.tsx" ペアが違反していないかを「ホワイトリスト」で判定する isAllowedReference ( "features/TaskDetail/index.tsx" , "../../features/TaskList/features/TaskExport/index.tsx" ) 2 14 / 28 1
CODE · 1 / 5 1. ファイルパスと import パス のペアを取得する
const project = new Project({ tsConfigFilePath: "./tsconfig.json" }); for (const source of project.getSourceFiles()) { for (const decl of sf.getImportDeclarations()) { const target = decl.getModuleSpecifierSourceFile(); if (!target) continue; if (!isAllowedReference(source.getFilePath(), target.getFilePath())) { throw new Error(`${source.getFilePath()} → ${target.getFilePath()}`); } } } STEP 01 tsconfig を読み込んで Project を作る 15 / 28
CODE · 2 / 5 1. ファイルパスと import パス のペアを取得する
const project = new Project({ tsConfigFilePath: "./tsconfig.json" }); for (const source of project.getSourceFiles()) { for (const decl of sf.getImportDeclarations()) { const target = decl.getModuleSpecifierSourceFile(); if (!target) continue; if (!isAllowedReference(source.getFilePath(), target.getFilePath())) { throw new Error(`${source.getFilePath()} → ${target.getFilePath()}`); } } } STEP 02 全ファイル内を探索 import ⽂をすべて確認 16 / 28
CODE · 3 / 5 1. ファイルパスと import パス のペアを取得する
const project = new Project({ tsConfigFilePath: "./tsconfig.json" }); for (const source of project.getSourceFiles()) { for (const decl of sf.getImportDeclarations()) { const target = decl.getModuleSpecifierSourceFile(); if (!target) continue; if (!isAllowedReference(source.getFilePath(), target.getFilePath())) { throw new Error(`${source.getFilePath()} → ${target.getFilePath()}`); } } } STEP 03 import ⽂から対応するファ イルを取得する 17 / 28
CODE · 4 / 5 1. ファイルパスと import パス のペアを取得する
const project = new Project({ tsConfigFilePath: "./tsconfig.json" }); for (const source of project.getSourceFiles()) { for (const decl of sf.getImportDeclarations()) { const target = decl.getModuleSpecifierSourceFile(); if (!target) continue; if (!isAllowedReference(source.getFilePath(), target.getFilePath())) { throw new Error(`${source.getFilePath()} → ${target.getFilePath()}`); } } } STEP 04 ファイルパスとimportパスの ペアを元にルールを判定 →違反ならerror をthrow > isAllowedReference の中身は 次のスライドで説明 18 / 28
APPROACH どう検知する? 1 ファイルパスと import パス のペアを取得する FILE features/TaskDetail/index.tsx IMPORT
import { xxx } from "../../features/TaskList/features/TaskExport/index.tsx" ペアが 違反していないか を「ホワイトリスト」で判定する isAllowedReference ( "features/TaskDetail/index.tsx", "../../features/TaskList/features/TaskExport/index.tsx" ) 2 19 / 28
APPROACH ホワイトリスト? 20 / 28 ⼦孫‧兄弟 への参照は可能 それ以外(親‧いとこ‧おじ/おば...)への参照は禁⽌ features/ ├──
TaskList/ │ ├── index.tsx │ └── features/ │ └── TaskExport/ │ └── index.tsx └── TaskDetail/ └── index.tsx
2. ペアが違反していないかを「ホワイトリスト」で判定する STEP 01 最も深い feature まで のパス を抜き出すhelper関数 正規表現で
/features/X を全マッチさ せ、 ⼀番深いものまでを採⽤。 TaskList/features/TaskExport/index.tsx → TaskList/features/TaskExport function getFeaturePath(filePath: string): string | null { const matches = [...filePath.matchAll(/\/features\/([^/]+)/g)]; if (matches.length ..= 0) return null; const last = matches[matches.length - 1]; return filePath.substring(0, last.index! + last[0].length); } function isAllowedReference(source: string, target: string): boolean { const sourceFeature = getFeaturePath(source); const targetFeature = getFeaturePath(target); if (!sourceFeature .| !targetFeature) return true; return( target.startsWith(`${sourceFeature}/`) .| sourceFeature.substring(0, sourceFeature.lastIndexOf("/")) ..= targetFeature.substring(0, targetFeature.lastIndexOf("/")); )} 21 / 28 CODE · 1 / 3
function getFeaturePath(filePath: string): string | null { const matches =
[...filePath.matchAll(/\/features\/([^/]+)/g)]; if (matches.length ..= 0) return null; const last = matches[matches.length - 1]; return filePath.substring(0, last.index! + last[0].length); } function isAllowedReference(source: string, target: string): boolean { const sourceFeature = getFeaturePath(source); const targetFeature = getFeaturePath(target); if (!sourceFeature .| !targetFeature) return true; return( target.startsWith(`${sourceFeature}/`) .| sourceFeature.substring(0, sourceFeature.lastIndexOf("/")) ..= targetFeature.substring(0, targetFeature.lastIndexOf("/")); )} CODE · 2 / 3 22 / 28 2. ペアが違反していないかを「ホワイトリスト」で判定する STEP 02 source / target の Feature を 特定 import元‧import先がそれぞれどの Feature に属するかを特定。Feature 外な らルール対象外として無視
function getFeaturePath(filePath: string): string | null { const matches =
[...filePath.matchAll(/\/features\/([^/]+)/g)]; if (matches.length ..= 0) return null; const last = matches[matches.length - 1]; return filePath.substring(0, last.index! + last[0].length); } function isAllowedReference(source: string, target: string): boolean { const sourceFeature = getFeaturePath(source); const targetFeature = getFeaturePath(target); if (!sourceFeature .| !targetFeature) return true; return( target.startsWith(`${sourceFeature}/`) .| sourceFeature.substring(0, sourceFeature.lastIndexOf("/")) ..= targetFeature.substring(0, targetFeature.lastIndexOf("/")); )} CODE · 2 / 3 23 / 28 2. ペアが違反していないかを「ホワイトリスト」で判定する STEP 03 source → target の依存が 「⼦孫」か「兄弟」ならOK ⼦孫か兄弟かを、 prefix を⾒てホワ イトリストで判定する
24 / 28 OUTPUT · 4 / 4 ツールの出⼒サンプル $
pnpm run feature-lint ✘ src/features/TaskList/index.tsx:12:1 rule: feature-reference 別ラインの feature を参照しています: src/features/TaskDetail/components/DetailHeader 10 | 11 | import { TaskTable } from "./components/TaskTable"; > 12 | import { DetailHeader } from "../TaskDetail/components/DetailHeader"; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 13 | hint: 直系(祖先と子孫の関係)の feature 同士のみ参照できます
‧AI エージェントが⽣成したコードも CI で検知できるようになった OUTCOME 実際に導⼊してみて ‧既存のコード内で ルール違反 が数⼗件⾒つかった ‧レビューで指摘していた内容が
事前に弾けるようになった 25 / 28
まとめ ‧AST解析でガードレールは⾃作できる ‧プロジェクト固有のルールもコードを⾛査して静的に検出できる ‧ts-morph は直感的な関数操作ができて敷居が低い ‧ぜひ⾃分のプロジェクトでも使ってみてください! ‧AI時代こそ、⾃動ガードレールが効く ‧プロンプトやドキュメントでお願いするより、ガードレールで担保する 26 /
28 WRAP-UP
⾃⼰紹介 須藤 路真 @michimasa_suto 所属 株式会社 PKSHA Technology ( 25卒
) 担当 コンタクトセンター向け SaaS の開発‧運⽤ 今年の⽬標 毎⽉ Zenn を書く 27 / 28
Thank you for listening! ご質問は Ask The Speaker か懇親会でぜひ 🙌
28 / 28