흐름도

MODULE_FLOW.md 기준 · 갱신: 2026-05-01 22:00:52
/volume1/web/gpt/repair_jobs/MODULE_FLOW.md 2026-05-01 22:00:52
# 영일전기철물 AI 요청 흐름도 1차

last_updated: 2026-05-01 21:18:00
root: `/volume1/web/gpt`

---

## 0. 한줄판단

현재 시스템은 한 줄 요청이 들어와도 `api.php → web_capability 또는 engine/pipeline → Agent → ToolSelector → RoutePipeline/Tool → ToolResult → UI history` 흐름 중 하나를 탄다. 문제는 일부 읽기 전용 요청이 Agent를 우회하므로, 공통 세션/trace 처리는 Agent보다 앞단에서 챙겨야 한다.

---

## 1. 전체 요청 기본 흐름

```txt
사용자 입력
↓
/gpt/index.php 화면
↓
api.php
  ├─ 입력/trace_id/context 구성
  ├─ 건영아 관리자 세션 touch
  ├─ web_capability preflight 먼저 시도
  │   └─ 성공하면 Agent 우회 후 바로 응답
  └─ preflight가 처리 못 하면 engine/pipeline/Agent로 이동
↓
engine.php / pipeline
↓
Agent.php
  ├─ system_gate
  ├─ 건영아 fast path
  ├─ pending 승인 복원
  ├─ ToolSelector 호출
  ├─ system_execution_guard
  └─ executeTool
↓
ToolSelector.php
  ├─ RoutePipeline
  ├─ legacy rule
  └─ LLM selector fallback
↓
선택된 Tool 실행
↓
ToolResult normalize
↓
result_inspector
↓
ui_history 저장
↓
화면 출력
```

---

## 2. web_capability 직행 흐름

읽기 전용 조회는 Agent를 안 타고 끝날 수 있다.

```txt
사용자 입력: 고객 리스트 조회
↓
api.php
  ├─ [api.admin_session] last_seen 갱신
  └─ WEB_CAPABILITY_PREFLIGHT_V1
↓
gpt_web_capability_preflight()
↓
capability=business.data.lookup
↓
legacy_tool=data_lookup
↓
고객 목록 응답
```

중요 포인트:
- 이 경로는 `Agent.php`를 안 탄다.
- 그래서 관리자 세션 갱신을 `Agent.php`에만 두면 안 된다.
- 현재는 `api.php` 앞단에서 갱신하도록 보강했다.

---

## 3. Agent 일반 실행 흐름

```txt
사용자 입력
↓
api.php preflight 처리 실패 또는 우회 불가
↓
pipeline agent start
↓
Agent::run()
  ├─ memory load
  ├─ system_gate 분석
  ├─ fast path 검사
  ├─ pending 승인 처리
  ├─ ToolSelector 호출
  ├─ guardSystemExecution
  ├─ executeTool
  └─ normalizeToolResult
↓
응답 반환
```

---

## 4. 승인 pending 흐름

```txt
사용자 입력: 3333 도 테스트 고객이네 지워
↓
system_gate
  ├─ intent=customer
  ├─ operation=delete
  └─ permission=HIGH_RISK_APPROVAL_REQUIRED
↓
ToolSelector
  └─ customer 선택
↓
guardSystemExecution
  └─ blocked=true
↓
saveSystemPendingApproval()
↓
사용자에게 삭제 계획만 출력
↓
사용자 입력: 승인
↓
tryResolveSystemPendingApproval()
↓
저장된 tool_spec 복원
↓
executeTool(customer)
↓
CustomerTool 실행
```

주의:
- pending에 저장된 `tool_spec`이 chat이면 승인 후 chat으로 튀는 문제가 생긴다.
- 고객 삭제는 반드시 `customer`로 복원되어야 한다.

---

## 5. 고객 삭제 / 이미 삭제 힌트 흐름

```txt
승인 후 CustomerTool 실행
↓
CustomerTool::execute()
↓
isCustomerDeleteRequest()
↓
handleCustomerDeleteRequest()
  ├─ 승인 여부 확인
  ├─ 고객명 추출
  ├─ 삭제 이유 추출
  └─ findCustomerRowForDelete()
↓
live DB에서 대상 검색
  ├─ 찾으면 deleteCustomerRow()
  └─ 못 찾으면 findDeletedCustomerExportHintV1()
↓
db_exports/customers.md / entities.md 검색
↓
deleted / is_active=0 흔적 있으면 요약 출력
```

현재 3333 케이스:
- live DB 활성 고객에서는 없음
- `customers.md`에 `id=216`, `status=deleted`, `is_active=0` 기록 있음
- `entities.md`에 `id=198`, `status_label=deleted`, `is_active=0` 기록 있음

---

## 6. 건영아 호출어 / 세션 흐름

```txt
사용자 입력: 건영아
↓
Agent::run() 초입 fast path
↓
LLM/ChatTool 우회
↓
짧은 랜덤 답변만 출력
↓
data/runtime/geonyeong_admin_session.json 생성
↓
다음 명령부터 api.php 앞단에서 last_seen 갱신
↓
마지막 명령 후 10분 지나면 세션 만료
```

---

## 7. UI 운영자 패널 흐름

```txt
/gpt/
↓
partials/operator_status.php
↓
운영자 버튼
  ├─ SSH 답변
  ├─ 상황판
  ├─ 엔진 로그
  ├─ 기록
  ├─ 고객 점검
  ├─ 작업 상태
  ├─ 코드 지도
  ├─ 보고서
  ├─ 모듈 구조
  └─ 흐름도
```

---

## 8. 파일별 흐름 마커

### `api.php`
- L39: `gpt_ui_history_append_v2` → `if (!function_exists('gpt_ui_history_append_v2')) {`
- L40: `gpt_ui_history_append_v2` → `function gpt_ui_history_append_v2(string $q, string $answer, string $traceId = '', array $extra = []): void`
- L522: `gpt_ui_history_append_v2` → `gpt_ui_history_append_v2($histQ, $histAnswer, $__earlyTraceIdV1, $histExtra);`
- L551: `api_router_kernel_inject_trace_v1` → `if (!function_exists('api_router_kernel_inject_trace_v1')) {`
- L552: `api_router_kernel_inject_trace_v1` → `function api_router_kernel_inject_trace_v1($payload)`
- L692: `gpt_ui_history_append_v2` → `gpt_ui_history_append_v2($histQ, $histAnswer, $trace_id);`
- L778: `API_GEONYEONG_ADMIN_SESSION_TOUCH_BEFORE_PREFLIGHT_V1` → `// API_GEONYEONG_ADMIN_SESSION_TOUCH_BEFORE_PREFLIGHT_V1`
- L823: `WEB_CAPABILITY_PREFLIGHT_V1` → `// WEB_CAPABILITY_PREFLIGHT_V1`
- L836: `gpt_web_capability_preflight` → `if (function_exists('gpt_web_capability_preflight') === false) {`
- L872: `gpt_web_capability_preflight` → `if (!$___apiCodeKeywordBypassCapabilityV1 && function_exists('gpt_web_capability_preflight')) {`
- L878: `gpt_web_capability_preflight` → `$___capability_response = gpt_web_capability_preflight((string)$q, (string)($trace_id ?? ''), $___capability_context);`
- L880: `API_GEONYEONG_ADMIN_SESSION_TOUCH_BEFORE_PREFLIGHT_V1` → `// API_GEONYEONG_ADMIN_SESSION_TOUCH_BEFORE_PREFLIGHT_V1_TRACE_ATTACH`
- L904: `api_router_kernel_inject_trace_v1` → `echo json_encode(api_router_kernel_inject_trace_v1($___capability_response), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);`

### `engine.php`
- L129: `rag` → `$retrieveScript = $pythonDir . DIRECTORY_SEPARATOR . 'rag_retrieve.py';`
- L130: `rag` → `$askScript = $pythonDir . DIRECTORY_SEPARATOR . 'rag_ask_app' . DIRECTORY_SEPARATOR . 'cli.py';`
- L169: `rag` → `$ragDataDir = realpath($envRagDataDir) ?: $envRagDataDir;`
- L171: `rag` → `$ragDataDir = $gptRoot . DIRECTORY_SEPARATOR . 'reports';`
- L173: `rag` → `$ragDataDir = '/volume1/web/영일전기철물2/reports';`
- L174: `rag` → `if (!is_dir($ragDataDir)) {`
- L176: `rag` → `$ragDataDir = $projectRoot . DIRECTORY_SEPARATOR . 'reports';`
- L178: `rag` → `if (!is_dir($ragDataDir)) {`
- L180: `rag` → `$ragDataDir = 'C:\\yi_rag';`
- L182: `rag` → `if (!is_dir($ragDataDir)) {`
- L184: `rag` → `$ragDataDir = $gptRoot . DIRECTORY_SEPARATOR . 'reports';`
- L432: `pipeline` → `require_once __DIR__ . '/engine_modules/pipeline.php';`
- L433: `rag` → `require_once __DIR__ . '/engine_modules/storage.php';`
- L435: `agent` → `require_once __DIR__ . '/engine_modules/agent_gateway.php';`

### `engine_modules/agent/Agent.php`
- L53: `public function run` → `public function run(string $query, array $context = []): array`
- L75: `AGENT_GEONYEONG_WAKE_WORD_FAST_PATH_V1` → `// AGENT_GEONYEONG_WAKE_WORD_FAST_PATH_V1`
- L81: `AGENT_GEONYEONG_ADMIN_SESSION_SLIDING_10MIN_V2` → `// AGENT_GEONYEONG_ADMIN_SESSION_SLIDING_10MIN_V2`
- L132: `AGENT_GEONYEONG_ADMIN_SESSION_SLIDING_10MIN_V2` → `// AGENT_GEONYEONG_ADMIN_SESSION_SLIDING_10MIN_V2`
- L195: `tryResolveSystemPendingApproval` → `$pendingResolved = $this->tryResolveSystemPendingApproval($query, $context);`
- L607: `guardSystemExecution` → `// guardSystemExecution 안에서 force_tool=worklog가 찍혀도`
- L609: `executeTool` → `// worklog intent + 업무 작업 문장에서는 executeTool 직전에 toolSpec을 worklog로 동기화한다.`
- L647: `executeTool` → `$result = $this->executeTool($tool, $query, $context);`
- L664: `guardSystemExecution` → `// guardSystemExecution 안에서 force_tool=worklog가 찍혀도`
- L666: `executeTool` → `// worklog intent + 업무 작업 문장에서는 executeTool 직전에 toolSpec을 worklog로 동기화한다.`
- L704: `executeTool` → `$result = $this->executeTool($tool, $query, $context);`
- L708: `executeTool` → `// executeTool 내부 guard가 effective_tool을 돌려준 경우, 이후 context_bus 저장 키도 보정한다.`
- L898: `tryResolveSystemPendingApproval` → `private function tryResolveSystemPendingApproval(string $query, array $context): ?array`
- L1017: `executeTool` → `$result = $this->executeTool($toolSpec, $pendingQuery, $context);`
- L1036: `saveSystemPendingApproval` → `private function saveSystemPendingApproval(`
- L1077: `guardSystemExecution` → `private function guardSystemExecution(string $rawQuery, string $toolName, array $context): ?array`
- L1164: `executeTool` → `// worklog precheck를 통과한 명확한 작업기록 create는 승인대기 없이 executeTool로 보낸다.`
- L1230: `saveSystemPendingApproval` → `$pendingSave = $this->saveSystemPendingApproval(`
- L1264: `executeTool` → `'[system_execution_guard] execution blocked before executeTool body',`
- L1613: `executeTool` → `private function executeTool(array $toolSpec, string $rawQuery, array $context): array`
- L1642: `guardSystemExecution` → `$guardResult = $this->guardSystemExecution($rawQuery, $toolName, $context);`
- L1939: `normalizeToolResult` → `return $this->normalizeToolResult($result, $toolName, $rawQuery, $params, $context);`
- L1961: `normalizeToolResult` → `private function normalizeToolResult(`

### `engine_modules/tools/ToolSelector.php`
- L24: `RoutePipeline` → `require_once __DIR__ . '/selector_routes/SelectorEarlyRoutePipeline.php';`
- L99: `selectTools` → `public function selectTools(`
- L255: `customer_delete_beats_chat` → `$trace[] = "[tool_selector] decision: customer selected by customer_delete_beats_chat_v1";`
- L267: `customer_delete_beats_chat` → `'source' => 'customer_delete_beats_chat_v1',`
- L275: `RoutePipeline` → `$__earlyRouteV1 = SelectorEarlyRoutePipeline::match((string)$query, is_array($context) ? $context : [], $this->registry ?? null);`
- L1660: `selectByRules` → `// 매칭되면 즉시 반환하고, 매칭 안 되면 기존 selectByRules 레거시 로직으로 fallback한다.`
- L1661: `RoutePipeline` → `if (!class_exists('RoutePipeline')) {`
- L1662: `RoutePipeline` → `$routePipelineFile = __DIR__ . '/routing/RoutePipeline.php';`
- L1668: `RoutePipeline` → `if (class_exists('RoutePipeline')) {`
- L1669: `RoutePipeline` → `$pipelineDecision = RoutePipeline::match($query, $this->registry, $trace);`
- L1681: `selectByRules` → `$ruleTools = $this->selectByRules($query, $context, $trace);`
- L1700: `askLLMToSelect` → `$selectedTools = $this->askLLMToSelect($query, $toolDescriptions, $trace);`
- L1714: `selectTools` → `* selectTools() 비대화 방지용 분리 helper.`
- L1800: `selectTools` → `* RouteDecision compare trace를 selectTools 밖으로 분리.`
- L2286: `selectByRules` → `private function selectByRules(string $query, array $context, array &$trace): array`
- L2454: `selectByRules` → `$trace[] = "[tool_selector] enter selectByRules";`
- L3277: `askLLMToSelect` → `private function askLLMToSelect(`

### `engine_modules/tools/routing/RoutePipeline.php`
- L17: `public static function match` → `public static function match(string $query, object $registry, array &$trace): array`
- L19: `loadRoutes` → `self::loadRoutes();`
- L66: `routeClassToCategory` → `$category = self::routeClassToCategory($class);`
- L84: `routeClassToCategory` → `private static function routeClassToCategory(string $class): string`
- L126: `loadRoutes` → `private static function loadRoutes(): void`

### `engine_modules/capabilities/web_preflight.php`
- L1421: `legacy_tool` → `if (preg_match('/legacy_tool=([a-zA-Z0-9_.-]+)/u', $line, $m) === 1) {`
- L1428: `business.data.lookup` → `'business.data.lookup' => '업무 자료 조회',`
- L1524: `data_lookup` → `'business_data_lookup' => '업무 자료 조회 도구',`
- L1546: `gpt_web_capability_preflight` → `function gpt_web_capability_preflight(string $query, string $traceId = '', array $context = []): ?array`
- L1628: `legacy_tool` → `if (($meta['legacy_tool'] ?? null) === null) {`
- L1703: `legacy_tool` → `'legacy_tool' => $route['legacy_tool'] ?? null,`

### `engine_modules/tools/CustomerTool.php`
- L45: `execute` → `public function execute(array $params): ToolResult`
- L61: `isCustomerDeleteRequest` → `if ($this->isCustomerDeleteRequest($query)) {`
- L65: `handleCustomerDeleteRequest` → `$result = $this->handleCustomerDeleteRequest($query, $params);`
- L716: `execute` → `$stmt->execute([':db' => $db]);`
- L1111: `execute` → `$stmt->execute([':name' => $customerName]);`
- L1117: `execute` → `$stmt->execute([':name' => '%' . $customerName . '%']);`
- L1233: `execute` → `$stmt->execute($params);`
- L1249: `isCustomerDeleteRequest` → `private function isCustomerDeleteRequest(string $query): bool`
- L1268: `handleCustomerDeleteRequest` → `private function handleCustomerDeleteRequest(string $query, array $params = []): array`
- L1307: `findCustomerRowForDelete` → `$target = $this->findCustomerRowForDelete($pdo, $tableInfo, $customerName);`
- L1348: `deleteCustomerRow` → `$deleteResult = $this->deleteCustomerRow($pdo, $tableInfo, $row);`
- L1463: `findDeletedCustomerExportHintV1` → `private function findDeletedCustomerExportHintV1(string $customerName): string`
- L1628: `findCustomerRowForDelete` → `private function findCustomerRowForDelete(PDO $pdo, array $tableInfo, string $customerName): array`
- L1635: `execute` → `$stmt->execute([':name' => $customerName]);`
- L1641: `execute` → `$stmt->execute([':name' => '%' . $customerName . '%']);`
- L1650: `findDeletedCustomerExportHintV1` → `$deletedHint = $this->findDeletedCustomerExportHintV1($customerName);`
- L1685: `deleteCustomerRow` → `private function deleteCustomerRow(PDO $pdo, array $tableInfo, array $row): array`
- L1705: `execute` → `$stmt->execute($params);`

### `partials/operator_status.php`
- L59: `operator-action` → `<div class="operator-actions">`
- L60: `operator-action` → `<a class="operator-action primary" href="web_command_console.php?latest=1" target="_blank" rel="noopener">`
- L64: `operator-action` → `<a class="operator-action" href="project_board.php" target="_blank" rel="noopener">`
- L69: `operator-action` → `<button class="operator-action" type="button" onclick="openEngineLogDrawerV1()">`
- L73: `operator-action` → `<button class="operator-action" type="button" onclick="toggleHistory()">`
- L77: `operator-action` → `<button class="operator-action soft" type="button" onclick="quickAsk('고객중에 특이사항 있는 고객 있어 ?')">`
- L82: `operator-action` → `<a class="operator-action state" href="work_state.php" target="_blank" rel="noopener">`
- L82: `work_state.php` → `<a class="operator-action state" href="work_state.php" target="_blank" rel="noopener">`
- L87: `operator-action` → `<a class="operator-action code-map" href="code_map.php" target="_blank" rel="noopener">`
- L87: `code_map.php` → `<a class="operator-action code-map" href="code_map.php" target="_blank" rel="noopener">`
- L92: `operator-action` → `<a class="operator-action report" href="work_report.php" target="_blank" rel="noopener">`
- L92: `work_report.php` → `<a class="operator-action report" href="work_report.php" target="_blank" rel="noopener">`
- L97: `operator-action` → `<a class="operator-action module-map" href="module_ownership.php" target="_blank" rel="noopener">`
- L97: `module_ownership.php` → `<a class="operator-action module-map" href="module_ownership.php" target="_blank" rel="noopener">`

---

## 9. 터졌을 때 추적 순서

### 고객 삭제가 이상할 때
1. UI trace에서 `customer.delete`가 있는지 확인
2. 없으면 ToolSelector/RoutePipeline 문제
3. 있으면 `customer_name`, `reason`, `matched_rows` 확인
4. `matched_rows=0`이면 `already_deleted_export_hint` 확인
5. 힌트가 있는데 출력이 이상하면 `findDeletedCustomerExportHintV1()` 확인

### 고객 리스트 조회가 이상할 때
1. `web_capability preflight start` 확인
2. `business.data.lookup` 확인
3. `legacy_tool=data_lookup` 확인
4. Agent를 안 탔으므로 `api.admin_session` 같은 공통 처리는 api.php에서 확인

### 승인 누르면 엉뚱한 답변이 나올 때
1. `[system_pending] execute_query` 확인
2. `[executor.input] raw_tool_spec` 확인
3. tool_spec name/type이 chat이면 pending 저장 당시 라우팅 문제
4. tool_spec은 맞는데 결과 이상하면 해당 Tool 실행부 문제

---

## 10. 다음 문서화 순서

1. `MODULE_DEPENDENCY.md` 생성: require/include/class 참조 연결 지도
2. `MODULE_FUNCTION_GROUPS.md` 생성: 함수들을 승인/라우팅/고객/세션/UI 등으로 그룹화
3. `REFACTOR_ROADMAP.md` 생성: 실제 분리 순서표
---

## 추가 완료: 의존성 지도 1차

완료 시각: 2026-05-01 21:20:22

추가된 항목:
- `repair_jobs/MODULE_DEPENDENCY.md`
- `/gpt/module_dependency.php`
- 운영자 패널 `[의존성]` 버튼

내용:
- PHP 파일 스캔
- require/include 연결
- class 선언 위치
- new/Class:: 참조 연결
- 핵심 파일별 in/out 의존성
- 의존성 기준 위험 판정

다음 문서화:
1. `MODULE_FUNCTION_GROUPS.md`
2. `REFACTOR_ROADMAP.md`
3. 문서 자동 갱신 스크립트
---

## 추가 완료: 의존성 지도 v2 정밀 재생성

완료 시각: 2026-05-01 21:22:24

수정 내용:
- `repair_jobs/logs` 제외
- `repair_jobs/backups` 제외
- `repair_jobs/ssh_answers` 제외
- `data`, `reports`, `vendor`, `node_modules` 제외
- 백업/임시/테스트성 PHP 파일명 제외
- `src_ToolSelector.php` 같은 로그 추출본이 실제 의존성으로 잡히는 문제 제거

판정:
- MODULE_DEPENDENCY.md는 이제 실제 운영 파일 중심으로 보는 기준점으로 사용 가능.
---

## 추가 완료: 의존성 지도 v3 엄격 스캔

완료 시각: 2026-05-01 21:24:12

수정 내용:
- `include/require`를 줄 시작 PHP 문장 기준으로만 인정
- JS 문자열/headers/fetch 옵션 오인식 제거
- `tools/,`, `headers: {` 같은 잘못된 의존성 제거
- 임시/로그/백업 경로 제외 유지

판정:
- MODULE_DEPENDENCY.md는 v3부터 실제 운영 파일 의존성 기준으로 사용 가능.
---

## 추가 완료: 의존성 지도 v4 PHP 빌더

완료 시각: 2026-05-01 21:25:53

수정 내용:
- `tools/build_module_dependency_v4.php` 생성
- Python 일회성 생성 대신 PHP 빌더로 재생성
- 운영 경로만 스캔
- 줄 시작 `include/require` PHP 문장만 인정
- JS 문자열/headers/fetch 옵션 오인식 제거

판정:
- MODULE_DEPENDENCY.md는 v4부터 실제 운영 의존성 기준으로 사용.
---

## 추가 완료: 함수 역할 그룹 지도 1차

완료 시각: 2026-05-01 21:28:12

추가된 항목:
- `repair_jobs/MODULE_FUNCTION_GROUPS.md`
- `/gpt/module_function_groups.php`
- `tools/build_module_function_groups_v1.php`
- 운영자 패널 `[함수그룹]` 버튼

내용:
- 진입/API/공통 앞단
- 건영아 호출어 / 관리자 세션
- Agent 실행 루프
- 승인 / pending / 고위험 실행 차단
- 라우팅 / ToolSelector / RoutePipeline
- 고객 조회 / 고객 삭제 / 고객 수정 / 고객 점검
- web_capability / Agent 우회 읽기
- 운영자 UI / 문서 뷰어

다음:
- `REFACTOR_ROADMAP.md` 생성
- 구조 문서 자동 갱신 통합 스크립트
---

## 추가 완료: 함수 역할 그룹 v2 인라인 마커 보강

완료 시각: 2026-05-01 21:29:33

수정 내용:
- `tools/build_module_function_groups_v2.php` 생성
- v1 함수명 분류 결과에 인라인/마커 기반 역할 블록 추가
- `건영아 호출어 / 관리자 세션`이 함수 0개로 보이는 문제 보정
- 고객 삭제 pending 재라우팅, 이미 삭제 고객 힌트, api web_capability 앞단 마커까지 별도 표시

판정:
- MODULE_FUNCTION_GROUPS.md는 v2부터 함수 + 인라인 블록 기준으로 사용.
---

## 기존 MD 합류 요약 v1

합류 시각: 2026-05-01 21:50:13

### 1. `reports/router_map.md`에서 반영할 핵심

- ToolSelector / RoutePipeline / Route 모듈의 라우팅 규칙을 한눈에 보기 위한 정적 분석 지도였다.
- RoutePipeline은 `customer`, `news`, `planner`, `quotation`, `schedule`, `stock`, `weather`, `worksite` 계열 Route를 순서대로 확인하는 구조로 봐야 한다.
- ToolSelector는 여전히 guard/우선순위/legacy fallback이 남아 있어, 문제 발생 시 RoutePipeline보다 ToolSelector 내부 우선순위가 먼저 먹었는지 확인해야 한다.
- 코드/패치/수정 요청은 code 계열로 빠져야 하고, 고객/수금/재고/일정 같은 업무 질의와 충돌하지 않게 route guard가 필요하다.

### 2. `reports/pipeline_data_source_map.md`에서 반영할 핵심

- 요청은 도메인별로 `schedule`, `memory`, `payment`, `quotation`, `customer`, `worksite`, `worklog`, `code_rag` 계열로 나눠진다.
- 읽기와 쓰기는 같은 도메인이라도 흐름이 다르다.
- pending approval 파일은 저장/승인 흐름의 중간 상태로 봐야 한다.
- 일정 테이블의 `source_type=payment` 같은 참조 데이터는 원장급 데이터가 아니므로, 돈/수금 판단에서는 income 계열을 우선해야 한다.

### 3. 흐름도 판단 보강

- 라우팅 문제를 볼 때는 `api.php → web_capability → Agent → ToolSelector → RoutePipeline → Tool` 중 어느 단계에서 끝났는지 먼저 갈라야 한다.
- 읽기 전용 조회는 Agent를 우회할 수 있으므로, 공통 세션/trace는 api.php 앞단에서 확인해야 한다.
---

## 완료 처리: ScheduleOverlayPolicy 1차 분리

완료 시각: 2026-05-01 22:00:52

결과:
- `ScheduleOverlayPolicy.php` 생성 완료
- `PlannerHintAdapter.php`에서 `schedule_overlay` hint 연결 완료
- `ToolSelector.php` 기존 schedule_overlay fallback 앞에 policy bridge 추가 완료
- 기존 fallback 블록은 삭제하지 않고 안전장치로 유지

live smoke 판정:
- `올해 일정에서 놓친 게 있나` → `2026년 일정 요약`으로 정규화, schedule 실행
- `2026년 4월 일정` → policy 미대상, 월간 일정 경로 유지
- `고객 리스트 조회` → data_lookup/customer_list 유지
- `견적 전체 삭제해줘` → 실제 삭제 안 됨, system_pending 승인대기 저장

최종 판정:
- `SCHEDULE_OVERLAY_POLICY_LIVE_SMOKE_OK`

다음 후보:
- ToolSelector 안의 남은 planner_hint / fallback_chat / stock_read_exception 중 작은 것부터 추가 분리 검토